diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 4de1b3040..9052869c2 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -27,22 +27,40 @@ body: placeholder: The code to place a is missing b and c... validations: required: false + - type: dropdown + id: platform + attributes: + label: Platform + description: Which server platform is Iris running on? Pick the loader or server software, not the launcher. + options: + - Paper + - Purpur + - Leaf + - Canvas + - Folia + - Spigot / CraftBukkit + - Fabric + - Forge + - NeoForge + - Other (describe under Problem) + validations: + required: true - type: dropdown id: mcversion attributes: label: Minecraft Version - description: What version of Minecraft is the server on? + description: What version of Minecraft is the server on? Iris 4.x targets 26.2 only; older versions are not supported. options: - - 1.14.X - - 1.15.X - - 1.16.X - - 1.17 - - 1.17.1 - - 1.18 - - 1.19 - - 1.20 - - 1.21 - - 1.22 + - '26.2' + - Other (unsupported) + validations: + required: true + - type: input + id: loaderversion + attributes: + label: Platform / Loader Version + description: Exact server or loader build (see console). For example "Paper 26.2-60", "Fabric Loader 0.19.3", "NeoForge 26.2.0.12-beta". + placeholder: DO NOT SAY "LATEST" validations: required: true - type: input diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..a085db701 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +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 diff --git a/.gitignore b/.gitignore index 203047ef2..3b51b24f3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,62 @@ libs/ collection/ -/core/src/main/java/com/volmit/iris/util/uniques/ +out/ +tools/simd-bench/out/ +tools/simd-bench/simd-bench.jar + +DataPackExamples/ + +TreeGenStuff/ +dist/ +core/plugins/ +adapters/bukkit/plugin/plugins/ + +# Dev-run artifacts. `run/` is a loom/ForgeGradle/ModDevGradle working directory and `logs/` is +# server output; neither is ever source. No source directory in this repo is named run or logs. +run/ +logs/ +*.log +*.log.gz +crash-reports/ +hs_err_pid*.log +replay_pid*.log + +.codegraph/ + +plans/ + +.env +.env.* +!.env.example +local.properties +*.jks +*.p12 +*.pfx +*.pem +*.key +*.hprof +credentials.json +service-account*.json + +# docs/ is hand-written and tracked (flat numbered product docs). Nothing under it is +# generated, so there is nothing to ignore here. + +.qa/ +.repro/ + +# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source. +.apiwt/ +.papiwt/ + +__pycache__/ + +# Doc authoring scratch +.docs-wip/ + +AGENTS.md + +# Local build tooling, not part of the project +build-lt.sh +BUILDS/ +.run/ diff --git a/README.md b/README.md index cad6bacba..7c2dadadb 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,303 @@ # Iris -The master branch is for the latest version of minecraft. +Iris is a world generation engine for Minecraft servers and mod loaders. It generates terrain, +biomes, caves, structures, objects, and entities from editable JSON packs, with a full in-game +studio authoring workflow. The same engine runs as a Bukkit-family plugin and as a Fabric, Forge, +or NeoForge server mod. Cross-platform generation targets deterministic parity for identical +artifacts, pack bytes, seeds, and test areas. The master branch targets Minecraft 26.2. -# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [Git](https://github.com/IrisDimensions) +# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md) **|** [Git](https://github.com/IrisDimensions) -# Building +Authoritative docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository, which is the source for the hosted wiki. -Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java -development. +Consider supporting development by buying Iris on Spigot. -Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone. +## Language and localization -## Preface: if you need help compiling and you are a developer / intend to help out in the community or with development we would love to help you regardless in the discord! however do not come to the discord asking for free copies, or a tutorial on how to compile. +Canonical English is defined in the typed Java catalogs under `core/src/main/java/art/arcane/iris/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/.json` can override only selected server messages; omitted entries resolve from the bundle and then code-owned English. -### Command Line Builds +## Platforms -1. Install [Java JDK 21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html) -2. Set the JDK installation path to `JAVA_HOME` as an environment variable. - * Windows - 1. Start > Type `env` and press Enter - 2. Advanced > Environment Variables - 3. Under System Variables, click `New...` - 4. Variable Name: `JAVA_HOME` - 5. Variable Value: `C:\Program Files\Java\jdk-21.0.1` (verify this exists after installing java don't just copy - the example text) - * MacOS - 1. Run `/usr/libexec/java_home -V` and look for Java 21 - 2. Run `sudo nano ~/.zshenv` - 3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line - 4. Use `CTRL + X`, then Press `Y`, Then `ENTER` - 5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory -3. Once the project has setup, run `gradlew iris` -4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot! +| Platform | Artifact | Minecraft | Notes | +|---|---|---|---| +| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.2 | Full feature set | +| Folia | plugin jar | 26.1.2 - 26.2 | Region-safe scheduling throughout | +| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.2 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable | +| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ | +| Forge | mod jar | 26.2 | Server worldgen + client HUD; current target is Forge 26.2-65.1.1 | +| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; current target is NeoForge 26.2.0.59 | -### IDE Builds (for development) +Java 25 is required on every platform. -* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle) -* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you - prefer. -* Resync the project & run your newly created task (under the development folder in gradle tasks!) +The modded feature set matches the plugin wherever the operation is not Bukkit-bound: worldgen +from Iris packs, authoring with mod blocks/items/entities (with validation suggestions), studio +workspaces with schema autocomplete over the server's live registries, entity spawning parity +including death loot, pregeneration with a boss bar or client HUD, the `/iris` command tree, and +the goldenhash determinism gate, which is interchangeable across all four platforms. -# Iris Toolbelt +### Native worldgen over Iris terrain -Everyone needs a tool-belt. +Iris replaces the chunk generator, so vanilla and mod worldgen only runs where Iris runs it. This +is identical on every platform. -```java -package com.volmit.iris.core.tools; +| Vanilla / mod worldgen | Over Iris terrain | Control | +|---|---|---| +| Structures (vanilla, datapack, mod) | Yes, on by default | `importedStructures.disabled` denies individual keys | +| Placed features: ores, trees, plants, springs, geodes | Yes, **off by default** | `importedFeatures.enabled` per dimension, with per-step and per-key filters | +| Carvers (caves, canyons, mod carvers) | Never - architectural | Iris has no `NoiseGeneratorSettings` for a carver to sample; use pack `caves`/`carvings` | +| Surface builders and surface rules | Never | Iris builds its surface from pack palettes | +| Mod biomes | Only as a `derivative`, `vanillaDerivative`, `biomeScatter` or `biomeSkyScatter` target | Iris chooses biomes from the pack, not from a biome source | +| Mob spawning, including mod mobs | Yes | Biome spawn tables are merged with the vanilla derivative's | -// Get IrisDataManager from a world -IrisToolbelt.access(anyWorld).getCompound().getData(); +With `importedFeatures` off - the default - chunk output is byte-for-byte what Iris has always +produced. See [94 - API - Modded](https://github.com/VolmitSoftware/docs/blob/master/iris/94-api-modded.md) and +[01 - Installation & Platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md) for the full control reference, including +which `pointed_dripstone` keys the 26.2 `speleothem` rename does and does not affect. -// Get Default Engine from world -IrisToolbelt.access(anyWorld).getCompound().getDefaultEngine(); +Independently of that flag, Iris custom biomes now inherit the biome tags of their vanilla +derivative on every platform, so the emitted datapack tag files change. Anything driven by biome +tags therefore applies to Iris custom biomes: mob variants, spawn rules, and any vanilla or mod +content selecting on `#minecraft:is_overworld` and friends. -// Get the engine at the given height -IrisToolbelt.access(anyWorld).getCompound().getEngineForHeight(68); +## Install -// IS THIS THING ON? -boolean yes=IrisToolbelt.isIrisWorld(world); +**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start +the server. First boot performs no pack download. Run `/iris download pack=overworld`, +`/iris download pack=underworld`, or `/iris download link=https://host/path/pack.zip`, waiting for +each download to finish before starting another. The 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 +the early-bootstrap `/iris replace` path for canonical Overworld, Nether, or End slots. -// GTFO for worlds (moves players to any other world, just not this one) -IrisToolbelt.evacuate(world); +**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is +self-contained (core, SPI, and required Fabric API modules are bundled). First boot compiles only +packs already on disk and never accesses the network. `/iris download` installs a pack atomically +without stopping the server. 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 +before it run with fallback registry data. + +**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the +Create New World screen; the integrated server runs the same engine. + +## The client mod + +Installing the mod jar on a client adds a native pregeneration HUD: a top-left panel with a +progress bar, chunks done/total, percent, chunks per second, and ETA, turning yellow while the +pregen is paused. The `H` key (rebindable, under the "Iris" controls category) toggles it. + +The HUD works against modded Iris servers and against Bukkit/Paper Iris servers, both over the +`irisworldgen:main` channel (custom payloads on modded, plugin messaging on Bukkit). Vanilla +clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client +mod is inert. + +## Quickstart + +Complete the platform's pack, external-datapack, and registry-restart workflow above first. Then +create and enter an Iris world. + +Plugin (optional arguments are keyed): -IrisAccess access=IrisToolbelt.createWorld() // If you like builders... - .name("myWorld") // The world name - .dimension("terrifyinghands") - .seed(69133742) // The world seed - .pregen(PregenTask // Define a pregen job to run - .builder() - .center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks) - .radius(4) // Radius in REGIONS. Rad of 4 means a 9x9 Region map. - .build()) - .create(); ``` +/iris create myworld type=overworld seed=1337 +/iris tp myworld +``` + +Mod (positional arguments): + +``` +/iris create myworld overworld 1337 +/iris tp irisworldgen:myworld +``` + +On Paper-family servers with early bootstrap (not plain Spigot), the shipping pair can instead +replace the canonical portal-linked slots after their registry workflow is complete: + +``` +/iris replace minecraft:overworld type=overworld seed=123456789 +/iris replace minecraft:the_nether type=underworld seed=-987654321 +``` + +Restart once after both commands report staged. The independent seeds apply to their respective +slots, and vanilla portals keep routing between the canonical Overworld and Nether identities. + +Pregeneration requires a radius in blocks. On the plugin, optional arguments are keyed; on modded +servers they are positional and composable: + +``` +/iris pregen start 352 world=myworld center=0,0 gui=false +/iris pregen start 352 irisworldgen:myworld at 0 0 sync +``` + +Players with the client mod see the native HUD; everyone else gets a boss bar (modded) or +console/status output. `/iris pregen status` reports progress on the plugin. + +## Studio and VSCode workspace + +The studio is the pack authoring environment, available on all platforms. Studio worlds are +transient - they are deleted on close and purged at startup. + +Plugin: + +``` +/iris studio create name= [template=] +/iris studio open [seed=1337] +/iris studio vscode [dimension=] +/iris studio update [dimension=] +/iris studio close +``` + +Mod: + +``` +/iris studio create [name] [template] +/iris studio open [seed] +/iris studio vscode [pack] +/iris studio update [pack] +/iris studio close +``` + +The generated VSCode workspace wires per-type JSON schemas (dimensions, biomes, regions, objects, +loot, entities, snippets) for full autocomplete. Schemas are generated from the server's live +registries, so on modded servers block, item, entity, enchantment, and potion-effect completion +includes installed mod content (for example `create:brass_ingot`). Editing an open studio's pack +files hotloads the changes and regenerates the schemas. + +## PlaceholderAPI + +Iris registers the `iris` expansion when PlaceholderAPI is enabled. Paths are dot-separated, +lowercase, and never contain an underscore. Every value is plain text: no colour codes, no unit +suffixes, no `%` character, `.` as the decimal separator, and no thousands grouping. + +Three answers are possible. A path that is not in the list below returns nothing, so PlaceholderAPI +re-emits the literal `%iris_...%` and a typo stays visible. A known path with no value right now +returns `---`. A real zero returns `0`. + +| Placeholder | Value | +|---|---| +| `%iris_available%` | `true` when the Iris terrain service is live | +| `%iris_world.available%` | `true` when the reading player is in an Iris world and a reading exists | +| `%iris_world.biome%` | Surface biome display name at the player, e.g. `Hot Desert Dunes` | +| `%iris_world.biome-key%` | Surface biome load key, e.g. `desert/hot-dunes` | +| `%iris_world.region%` | Region display name at the player | +| `%iris_world.region-key%` | Region load key | +| `%iris_world.dimension%` | Dimension (pack) load key of the player's world | +| `%iris_pregen.available%` | `true` while a pregeneration job is running | +| `%iris_pregen.world%` | World name the running job is pregenerating | +| `%iris_pregen.percent%` | Completion, `0.00` to `100.00`, no `%` character | +| `%iris_pregen.eta%` | Estimated seconds remaining, whole number | +| `%iris_pregen.eta-text%` | Same estimate as `2m 5s` or `1h 30m` | +| `%iris_pregen.chunks%` | Chunks generated so far | +| `%iris_pregen.total%` | Chunks in the job | +| `%iris_pregen.chunks-per-second%` | Current rate | +| `%iris_pregen.paused%` | `true` while the job is paused | + +The world values are the surface reading at the player's block column. Walking refreshes them at most +once per second per player, so a whole board of `world.*` keys costs one refresh per player per +second no matter how many of them are on it, and a value may lag a sprinting player by up to a +second. A jump that is not walking - joining, respawning, changing worlds, stepping through a portal, +or any teleport including `/iris goto`, `/tp`, an ender pearl and a random teleport - is published +immediately, so a player who arrives somewhere and then stands still never keeps reading the biome, +region or dimension of where they came from. `pregen.*` is global: there is one pregeneration job per +server, and `%iris_pregen.world%` says which world it is. + +### Migration from the pre-2.0 keys + +The old underscore keys are gone. There is no alias and no dual-accept window; an old key now +renders literally so it is visible rather than silently wrong. + +| Old key | New key | Why | +|---|---|---| +| `%iris_biome_name%` | `%iris_world.biome%` | Renamed onto the dot grammar | +| `%iris_biome_id%` | `%iris_world.biome-key%` | Renamed; `id` was always the load key | +| `%iris_region_name%` | `%iris_world.region%` | Renamed onto the dot grammar | +| `%iris_region_id%` | `%iris_world.region-key%` | Renamed; `id` was always the load key | +| `%iris_biome_file%` | removed | Rendered an absolute server path into player-visible text, and threw on packs with no backing file | +| `%iris_region_file%` | removed | Same as `biome_file` | +| `%iris_world_seed%` | removed | Handed the world seed to anyone who could read a scoreboard, and a placeholder has no permission context to gate on | +| `%iris_terrain_height%` | removed | Reported the *generated* height, before objects and player edits, so it disagreed with the block under the player's feet | +| `%iris_terrain_slope%` | removed | Three extra noise samples per read for an unformatted pack-authoring diagnostic | +| `%iris_world_mode%` | removed | Studio or Production; a studio world exists for seconds during authoring and is never on a live board | +| `%iris_world_speed%` | removed | Mutated engine rate-window state every time it was read. `%iris_pregen.chunks-per-second%` answers the same question from a snapshot | + +The old keys also read the *cave* biome for a player standing under an overhang, because they +sampled two blocks above the player's feet. The new `world.biome` is always the surface biome, +which is what a board reader means. + +## Building from source + +Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else. + +``` +./gradlew buildAllToOut +``` + +builds every platform artifact into `dist/`: + +``` +Iris v [CraftBukkit] .jar +Iris v [Fabric] +.jar +Iris v [Forge] +.jar +Iris v [NeoForge] +.jar +``` + +Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The +SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by +`./gradlew :spi:jar`. + +`./gradlew buildAll` 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`. + +If you need help compiling as a developer or contributor, ask in the Discord. + +## Adapters / modded development + +`core/` and `spi/` are pure JVM. `adapters/bukkit/` is part of the root Gradle build; the three +modded adapters (`adapters/fabric`, `adapters/forge`, `adapters/neoforge`) are standalone builds +with their own `settings.gradle`, which is what keeps Loom, ForgeGradle, and ModDevGradle off one +plugin classpath. Drive them with `-p`: + +``` +./gradlew -p adapters/fabric runServer # or runClient +./gradlew -p adapters/forge runServer +./gradlew -p adapters/neoforge runServer +./gradlew -p adapters/fabric test # shared adapters/modded-common test suite +``` + +Each `runServer` accepts determinism and world-integrity flags, forwarded to the game as system +properties: + +| Flag | System property | Purpose | +|---|---|---| +| `-PirisParity=` | `iris.parity` | Run the cross-platform parity harness for a pack | +| `-PirisParityGolden=` | `iris.parity.golden` | Compare against a captured golden-hash file | +| `-PirisParityDeep=true` | `iris.parity.deep` | Deep (per-block) parity instead of hash-only | +| `-PirisWorldCheck=` | `iris.worldcheck` | Post-generation world integrity check | + +Fabric additionally takes `-PirisClientRunDir=` to relocate the `runClient` working directory. +Shared code lives in `adapters/minecraft-common` (all adapters), `adapters/modded-common` +(loaders + the shared test suite), and `adapters/client-common` (client HUD and world-type +screens); every adapter adds those source directories, so one edit reaches all three loaders. + +For IDE import you can surface the three adapter builds in the root composite with +`-PincludeModdedAdapters=true`. It is off by default: each adapter includes the root build back to +substitute `art.arcane:core` and `art.arcane:spi`, so including them from the root closes a +composite cycle. The build and release paths do not need it. + +## Documentation + +Full product docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository. Start with: + +- [Overview and index](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md) +- [Installation and platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md) +- [Getting started](https://github.com/VolmitSoftware/docs/blob/master/iris/02-getting-started.md) +- [Structures overview](https://github.com/VolmitSoftware/docs/blob/master/iris/18-structures-overview.md) +- [API — Getting Started](https://github.com/VolmitSoftware/docs/blob/master/iris/90-api-getting-started.md) +- [Maintainer — MC version bump](https://github.com/VolmitSoftware/docs/blob/master/iris/85-maintainer-mc-version-bump.md) +- [Maintainer — release checklist](https://github.com/VolmitSoftware/docs/blob/master/iris/86-maintainer-release-checklist.md) diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CurrentPaperWorldDataWriter.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CurrentPaperWorldDataWriter.java new file mode 100644 index 000000000..1b58d0b95 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CurrentPaperWorldDataWriter.java @@ -0,0 +1,130 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import art.arcane.iris.core.lifecycle.WorldReplacementSeed; +import art.arcane.iris.util.common.scheduling.J; +import io.papermc.paper.world.saveddata.PaperLevelOverrides; +import io.papermc.paper.world.saveddata.PaperWorldMetadata; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.PrimaryLevelData; +import net.minecraft.world.level.storage.SavedDataStorage; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.CraftServer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +final class CurrentPaperWorldDataWriter { + private static final long SNAPSHOT_TIMEOUT_SECONDS = 30L; + + private CurrentPaperWorldDataWriter() { + } + + static void write( + Path sourceWorldDirectory, + Path targetWorldDirectory, + long seed + ) throws IOException { + CraftServer craftServer = (CraftServer) Bukkit.getServer(); + MinecraftServer server = craftServer.getHandle().getServer(); + PaperLevelOverrides levelOverrides = captureLevelOverrides(craftServer, server); + + Path targetWorld = targetWorldDirectory.toAbsolutePath().normalize(); + UUID metadataUuid = UUID.randomUUID(); + WorldReplacementSeed.copyWithAuthoritativeSeed(sourceWorldDirectory, targetWorld, seed); + try (SavedDataStorage savedDataStorage = new SavedDataStorage( + targetWorld.resolve("data"), + server.getFixerUpper(), + server.registryAccess() + )) { + savedDataStorage.set(PaperWorldMetadata.TYPE, new PaperWorldMetadata(metadataUuid)); + savedDataStorage.set(PaperLevelOverrides.TYPE, levelOverrides); + } + + List requiredDataFiles = List.of( + targetWorld.resolve("data/minecraft/world_gen_settings.dat"), + targetWorld.resolve("data/paper/metadata.dat"), + targetWorld.resolve("data/paper/level_overrides.dat") + ); + for (Path requiredDataFile : requiredDataFiles) { + if (!Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Current Paper world data was not written: " + requiredDataFile); + } + } + + long writtenSeed = WorldReplacementSeed.readAuthoritativeSeed(targetWorld); + if (writtenSeed != seed) { + throw new IOException("Current Paper world data did not retain the requested seed."); + } + + try (SavedDataStorage verificationStorage = new SavedDataStorage( + targetWorld.resolve("data"), + server.getFixerUpper(), + server.registryAccess() + )) { + PaperWorldMetadata metadata = verificationStorage.get(PaperWorldMetadata.TYPE); + if (metadata == null || !metadataUuid.equals(metadata.uuid())) { + throw new IOException("Current Paper world metadata could not be verified."); + } + PaperLevelOverrides overrides = verificationStorage.get(PaperLevelOverrides.TYPE); + if (overrides == null || overrides.isInitialized()) { + throw new IOException("Current Paper level overrides could not be verified."); + } + } + } + + private static PaperLevelOverrides captureLevelOverrides( + CraftServer craftServer, + MinecraftServer server + ) throws IOException { + if (craftServer.isGlobalTickThread()) { + return createLevelOverrides(craftServer, server); + } + if (J.isFolia() && J.isPrimaryThread()) { + throw new IOException("Current Paper world data cannot be staged from a Folia region tick thread."); + } + + CompletableFuture captured = new CompletableFuture<>(); + boolean scheduled = J.runGlobal(() -> { + try { + captured.complete(createLevelOverrides(craftServer, server)); + } catch (Throwable failure) { + captured.completeExceptionally(failure); + } + }); + if (!scheduled) { + throw new IOException("Could not schedule the current Paper level-data snapshot on the global thread."); + } + + try { + return captured.get(SNAPSHOT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while capturing current Paper level data.", failure); + } catch (ExecutionException failure) { + throw new IOException("Could not capture current Paper level data.", failure.getCause()); + } catch (TimeoutException failure) { + throw new IOException("Timed out while capturing current Paper level data.", failure); + } + } + + private static PaperLevelOverrides createLevelOverrides( + CraftServer craftServer, + MinecraftServer server + ) throws IOException { + if (!craftServer.isGlobalTickThread()) { + throw new IOException("Current Paper level data must be captured on the global tick thread."); + } + if (!(server.getWorldData().overworldData() instanceof PrimaryLevelData primaryLevelData)) { + throw new IOException("Paper primary level data is unavailable for current world data staging."); + } + return PaperLevelOverrides.createFromLiveLevelData(primaryLevelData); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java new file mode 100644 index 000000000..43d6dad87 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java @@ -0,0 +1,696 @@ +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.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 biomeCustomRegistry; + private final Registry biomeRegistry; + private final AtomicCache registryAccess = new AtomicCache<>(); + private final Holder fallbackBiome; + private final ConcurrentHashMap> noiseBiomeCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> structureBiomeCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> surfaceStructureBiomeCache = new ConcurrentHashMap<>(); + private volatile KMap> customBiomes; + private volatile Map> 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> getAllBiomes(Registry customRegistry, Registry registry, Engine engine) { + LinkedHashSet> biomes = new LinkedHashSet<>(); + + for (IrisBiome i : engine.getAllBiomes()) { + Holder 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 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 fieldForClass(Class 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> collectPossibleBiomes() { + return possibleStructureBiomes().stream(); + } + + Set> 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 customRegistry = ((RegistryAccess) getFor( + RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())) + .lookup(Registries.BIOME).orElse(null); + Registry worldRegistry = ((CraftWorld) world).getHandle().registryAccess() + .lookup(Registries.BIOME).orElse(null); + return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine)); + } + } + + private KMap> fillCustomBiomes(Registry customRegistry, Engine engine, Holder fallback) { + KMap> m = new KMap<>(); + if (customRegistry == null) { + return m; + } + + for (IrisBiome i : engine.getAllBiomes()) { + if (i.isCustom()) { + for (IrisBiomeCustom j : i.getCustomDerivitives()) { + Holder 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> fillVanillaSpawnBiomes(Registry customRegistry, Registry registry, Engine engine) { + IdentityHashMap> spawnBiomes = new IdentityHashMap<>(); + if (customRegistry == null || registry == null) { + return Collections.unmodifiableMap(spawnBiomes); + } + + for (IrisBiome irisBiome : engine.getAllBiomes()) { + if (!irisBiome.isCustom()) { + continue; + } + Holder vanillaHolder = NMSBinding.biomeToBiomeBase(registry, irisBiome.getVanillaDerivative()); + if (vanillaHolder == null) { + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + Holder customHolder = resolveCustomBiomeHolder(customRegistry, engine, customBiome.getId()); + if (customHolder != null) { + spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder); + } + } + } + + return Collections.unmodifiableMap(spawnBiomes); + } + + Holder getVanillaSpawnBiome(Holder 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 codec() { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Holder 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 cachedHolder = structureBiomeCache.get(cacheKey); + if (cachedHolder != null) { + return cachedHolder; + } + + Holder resolvedHolder = resolveStructureBiomeHolder(x, y, z); + Holder existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder); + if (existingHolder != null) { + return existingHolder; + } + + if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { + structureBiomeCache.clear(); + } + + return resolvedHolder; + } + } + + @Override + public Pair> findBiomeHorizontal( + int x, + int y, + int z, + int searchRadius, + Predicate> 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> 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> 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 getSurfaceStructureBiomeHolder(int x, int z) { + long columnKey = packColumnKey(x, z); + Holder surfaceHolder = surfaceStructureBiomeCache.get(columnKey); + if (surfaceHolder != null) { + return surfaceHolder; + } + Holder resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z); + Holder 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 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 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 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)) { + if (!isRuntimeAvailable()) { + throw new IllegalStateException("Iris visible biome lookup has no active engine runtime"); + } + ensureCachesCurrent(); + long cacheKey = packNoiseKey(x, y, z); + Holder cachedHolder = noiseBiomeCache.get(cacheKey); + if (cachedHolder != null) { + return cachedHolder; + } + + Holder resolvedHolder = resolveVisibleBiomeHolder(x, y, z); + Holder 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> refreshedCustomBiomes = fillCustomBiomes( + biomeCustomRegistry, engine, fallbackBiome); + Map> refreshedSpawnBiomes = fillVanillaSpawnBiomes( + biomeCustomRegistry, biomeRegistry, engine); + noiseBiomeCache.clear(); + structureBiomeCache.clear(); + surfaceStructureBiomeCache.clear(); + customBiomes = refreshedCustomBiomes; + vanillaSpawnBiomes = refreshedSpawnBiomes; + cacheDimension = dimension; + cacheRuntimeId = runtimeId; + } + } + + private Holder 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 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 resolveVisibleBiomeHolder(int x, int y, int z) { + BiomeResolution resolution = resolveBiomeResolution(x, y, z); + if (resolution == null) { + return getFallbackBiome(); + } + + if (resolution.irisBiome.isCustom()) { + return resolveCustomHolder(resolution); + } + + org.bukkit.block.Biome vanillaBiome = resolution.underground + ? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ) + : resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); + Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome); + if (holder != null) { + return holder; + } + + return getFallbackBiome(); + } + + private Holder resolveCustomHolder(BiomeResolution resolution) { + IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); + if (customBiome != null) { + Holder holder = customBiomes.get(customBiome.getId()); + if (holder != null) { + return holder; + } + } + + return getFallbackBiome(); + } + + private BiomeResolution resolveBiomeResolution(int x, int y, int z) { + if (engine == null || engine.isClosed()) { + return null; + } + + if (engine.getComplex() == null) { + return null; + } + + int blockX = x << 2; + int blockZ = z << 2; + int blockY = y << 2; + int worldMinHeight = engine.getWorld().minHeight(); + int internalY = blockY - worldMinHeight; + int 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) + : 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; + } + + 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 getFallbackBiome() { + if (fallbackBiome != null) { + return fallbackBiome; + } + + Holder 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 resolveCustomBiomeHolder(Registry 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> optionalBiomeKey = customRegistry.getResourceKey(biome); + if (optionalBiomeKey.isEmpty()) { + return null; + } + + Optional> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get()); + if (optionalReferenceHolder.isEmpty()) { + return null; + } + + return optionalReferenceHolder.get(); + } + + private static Holder resolveBiomeHolder(Registry 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 resolveFallbackBiome(Registry registry, Registry customRegistry) { + Holder plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS); + if (plains != null) { + return plains; + } + + Holder vanilla = firstHolder(registry); + if (vanilla != null) { + return vanilla; + } + + return firstHolder(customRegistry); + } + + private static Holder firstHolder(Registry registry) { + if (registry == null) { + return null; + } + + for (Biome biome : registry) { + Optional> optionalBiomeKey = registry.getResourceKey(biome); + if (optionalBiomeKey.isEmpty()) { + continue; + } + + Optional> 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; + } + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/DatapackStructureStateFilter.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/DatapackStructureStateFilter.java new file mode 100644 index 000000000..3c2346d0f --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/DatapackStructureStateFilter.java @@ -0,0 +1,509 @@ +package art.arcane.iris.core.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 net.minecraft.core.Holder; +import net.minecraft.core.HolderOwner; +import net.minecraft.core.Vec3i; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +final class DatapackStructureStateFilter { + private DatapackStructureStateFilter() { + } + + static Selection filter( + List> structureSets, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources + ) { + return filter(structureSets, scopeIndex, declaredSources, new IrisImportedStructureControl()); + } + + static Selection filter( + List> structureSets, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + IrisImportedStructureControl importedStructures + ) { + return filter( + structureSets, + scopeIndex, + declaredSources, + importedStructures, + keyIndex(structureSets)); + } + + static Selection filter( + List> structureSets, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + IrisImportedStructureControl importedStructures, + StructureSetKeyIndex keyIndex + ) { + if (scopeIndex.isEmpty()) { + return new Selection( + scaleFrequencyOverrides(structureSets, importedStructures, keyIndex), + 0, + 0); + } + Map> holdersByKey = new HashMap<>(); + for (Holder holder : structureSets) { + String key = structureSetKey(holder, keyIndex); + if (key != null) { + holdersByKey.putIfAbsent(key, holder); + } + } + Map, Holder> scopedByIdentity = new IdentityHashMap<>(); + Set> visiting = Collections.newSetFromMap(new IdentityHashMap<>()); + List> filteredSets = new ArrayList<>(structureSets.size()); + int retainedManagedSets = 0; + int excludedManagedSets = 0; + for (Holder holder : structureSets) { + String setKey = structureSetKey(holder, keyIndex); + boolean managedSet = setKey != null && scopeIndex.isManagedStructureSet(setKey); + Holder scopedHolder = scopeHolder( + holder, + scopeIndex, + declaredSources, + holdersByKey, + scopedByIdentity, + visiting, + keyIndex); + if (scopedHolder == null) { + if (managedSet) { + excludedManagedSets++; + } + continue; + } + if (managedSet) { + retainedManagedSets++; + } + filteredSets.add(scopedHolder); + } + return new Selection( + scaleFrequencyOverrides( + List.copyOf(filteredSets), importedStructures, keyIndex), + retainedManagedSets, + excludedManagedSets); + } + + static String structureSetKey(Holder holder) { + return structureSetKey(holder, keyIndex(List.of(holder))); + } + + static StructureSetKeyIndex keyIndex( + Iterable> registeredStructureSets + ) { + return StructureSetKeyIndex.create(registeredStructureSets); + } + + private static String structureSetKey( + Holder holder, + StructureSetKeyIndex keyIndex + ) { + return keyIndex.resolve(holder) + .map(key -> key.identifier().toString()) + .orElse(null); + } + + private static List filterEntries( + List entries, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources + ) { + List filtered = new ArrayList<>(entries.size()); + for (StructureSet.StructureSelectionEntry entry : entries) { + String structureKey = entry.structure().unwrapKey() + .map(key -> key.identifier().toString()) + .orElse(null); + if (structureKey == null || scopeIndex.allowsStructure(structureKey, declaredSources)) { + filtered.add(entry); + } + } + return filtered; + } + + private static Holder scopeHolder( + Holder holder, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + Map> holdersByKey, + Map, Holder> scopedByIdentity, + Set> visiting, + StructureSetKeyIndex keyIndex + ) { + if (scopedByIdentity.containsKey(holder)) { + return scopedByIdentity.get(holder); + } + if (!visiting.add(holder)) { + return null; + } + try { + String setKey = structureSetKey(holder, keyIndex); + if (setKey != null && scopeIndex.isManagedStructureSet(setKey) + && !scopeIndex.allowsStructureSet(setKey, declaredSources)) { + scopedByIdentity.put(holder, null); + return null; + } + StructureSet originalSet = holder.value(); + List entries = filterEntries( + originalSet.structures(), scopeIndex, declaredSources); + if (entries.isEmpty()) { + scopedByIdentity.put(holder, null); + return null; + } + StructurePlacement placement = scopePlacement( + originalSet.placement(), + scopeIndex, + declaredSources, + holdersByKey, + scopedByIdentity, + visiting, + keyIndex); + if (entries.size() == originalSet.structures().size() + && placement == originalSet.placement()) { + scopedByIdentity.put(holder, holder); + return holder; + } + Holder scoped = replacementHolder( + holder, new StructureSet(entries, placement), keyIndex); + scopedByIdentity.put(holder, scoped); + return scoped; + } finally { + visiting.remove(holder); + } + } + + static Optional exclusionZone(StructurePlacement placement) { + Object value = declaredFieldValue(StructurePlacement.class, placement, Optional.class); + if (value instanceof Optional optional && (optional.isEmpty() + || optional.get() instanceof StructurePlacement.ExclusionZone)) { + @SuppressWarnings("unchecked") + Optional resolved = + (Optional) optional; + return resolved; + } + throw new IllegalStateException("Could not read structure placement exclusion zone from " + + placement.getClass().getName()); + } + + private static StructurePlacement scopePlacement( + StructurePlacement placement, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + Map> holdersByKey, + Map, Holder> scopedByIdentity, + Set> visiting, + StructureSetKeyIndex keyIndex + ) { + Optional currentZone = exclusionZone(placement); + if (currentZone.isEmpty()) { + return placement; + } + + Holder target = currentZone.get().otherSet(); + String targetKey = structureSetKey(target, keyIndex); + if (targetKey != null) { + target = holdersByKey.getOrDefault(targetKey, target); + } + Holder scopedTarget = scopeHolder( + target, + scopeIndex, + declaredSources, + holdersByKey, + scopedByIdentity, + visiting, + keyIndex); + Optional scopedZone = Optional.empty(); + if (scopedTarget != null) { + scopedZone = Optional.of(new StructurePlacement.ExclusionZone( + scopedTarget, + currentZone.get().chunkCount())); + } + if (scopedTarget == currentZone.get().otherSet()) { + return placement; + } + return copyPlacement(placement, scopedZone, 1D); + } + + private static List> scaleFrequencyOverrides( + List> structureSets, + IrisImportedStructureControl importedStructures, + StructureSetKeyIndex keyIndex + ) { + if (!importedStructures.hasFrequencyOverrides()) { + return structureSets; + } + Map> holdersByKey = new HashMap<>(); + for (Holder holder : structureSets) { + String key = structureSetKey(holder, keyIndex); + if (key != null) { + holdersByKey.putIfAbsent(key, holder); + } + } + Map, Holder> scaledByIdentity = new IdentityHashMap<>(); + List> scaledSets = new ArrayList<>(structureSets.size()); + boolean changed = false; + for (Holder holder : structureSets) { + Holder scaled = scaleFrequencyHolder( + holder, + importedStructures, + holdersByKey, + scaledByIdentity, + keyIndex); + scaledSets.add(scaled); + changed |= scaled != holder; + } + return changed ? List.copyOf(scaledSets) : structureSets; + } + + private static Holder scaleFrequencyHolder( + Holder holder, + IrisImportedStructureControl importedStructures, + Map> holdersByKey, + Map, Holder> scaledByIdentity, + StructureSetKeyIndex keyIndex + ) { + if (scaledByIdentity.containsKey(holder)) { + return scaledByIdentity.get(holder); + } + if (!dependsOnFrequencyOverride( + holder, importedStructures, holdersByKey, keyIndex)) { + scaledByIdentity.put(holder, holder); + return holder; + } + + ResourceKey holderKey = keyIndex.resolve(holder).orElseThrow(() -> + new IllegalStateException( + "An affected native structure-set exclusion graph has an unkeyed holder")); + ReboundStructureSetHolder scaledHolder = new ReboundStructureSetHolder(holderKey); + scaledByIdentity.put(holder, scaledHolder); + + StructureSet originalSet = holder.value(); + StructurePlacement originalPlacement = originalSet.placement(); + Optional originalZone = exclusionZone(originalPlacement); + Optional scaledZone = originalZone; + if (originalZone.isPresent()) { + Holder target = canonicalHolder( + originalZone.get().otherSet(), holdersByKey, keyIndex); + Holder scaledTarget = scaleFrequencyHolder( + target, + importedStructures, + holdersByKey, + scaledByIdentity, + keyIndex); + if (scaledTarget != originalZone.get().otherSet()) { + scaledZone = Optional.of(new StructurePlacement.ExclusionZone( + scaledTarget, originalZone.get().chunkCount())); + } + } + + double multiplier = importedStructures.frequencyMultiplier( + structureSetKey(holder, keyIndex)); + StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone) + ? originalPlacement + : copyPlacement(originalPlacement, scaledZone, multiplier); + scaledHolder.bind(new StructureSet(originalSet.structures(), scaledPlacement)); + return scaledHolder; + } + + private static boolean dependsOnFrequencyOverride( + Holder holder, + IrisImportedStructureControl importedStructures, + Map> holdersByKey, + StructureSetKeyIndex keyIndex + ) { + Set> visited = Collections.newSetFromMap(new IdentityHashMap<>()); + Holder current = holder; + while (visited.add(current)) { + if (importedStructures.frequencyMultiplier( + structureSetKey(current, keyIndex)) != 1D) { + return true; + } + Optional zone = + exclusionZone(current.value().placement()); + if (zone.isEmpty()) { + return false; + } + current = canonicalHolder(zone.get().otherSet(), holdersByKey, keyIndex); + } + return false; + } + + private static Holder canonicalHolder( + Holder holder, + Map> holdersByKey, + StructureSetKeyIndex keyIndex + ) { + String key = structureSetKey(holder, keyIndex); + return key == null ? holder : holdersByKey.getOrDefault(key, holder); + } + + private static Holder replacementHolder( + Holder original, + StructureSet replacement, + StructureSetKeyIndex keyIndex + ) { + Optional> key = keyIndex.resolve(original); + if (key.isEmpty()) { + return Holder.direct(replacement); + } + ReboundStructureSetHolder holder = new ReboundStructureSetHolder(key.get()); + holder.bind(replacement); + return holder; + } + + private static StructurePlacement copyPlacement( + StructurePlacement placement, + Optional exclusionZone, + double frequencyMultiplier + ) { + Vec3i locateOffset = (Vec3i) declaredFieldValue( + StructurePlacement.class, placement, Vec3i.class); + StructurePlacement.FrequencyReductionMethod frequencyReductionMethod = + (StructurePlacement.FrequencyReductionMethod) declaredFieldValue( + StructurePlacement.class, + placement, + StructurePlacement.FrequencyReductionMethod.class); + float frequency = (float) declaredFieldValue( + StructurePlacement.class, placement, float.class); + int salt = (int) declaredFieldValue( + StructurePlacement.class, placement, int.class); + + if (placement instanceof RandomSpreadStructurePlacement randomSpread) { + NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread( + frequency, randomSpread.spacing(), randomSpread.separation(), frequencyMultiplier); + return new RandomSpreadStructurePlacement( + locateOffset, + frequencyReductionMethod, + scale.frequency(), + salt, + exclusionZone, + scale.spacing(), + randomSpread.separation(), + randomSpread.spreadType()); + } + if (placement.getClass() == ConcentricRingsStructurePlacement.class) { + ConcentricRingsStructurePlacement rings = + (ConcentricRingsStructurePlacement) placement; + float scaledFrequency = NativeStructureFrequencyScale.probability( + frequency, frequencyMultiplier); + return new ConcentricRingsStructurePlacement( + locateOffset, + frequencyReductionMethod, + scaledFrequency, + salt, + exclusionZone, + rings.distance(), + rings.spread(), + rings.count(), + rings.preferredBiomes()); + } + throw new IllegalStateException("Unsupported native structure placement in an affected " + + "frequency-override or datapack-scope graph: " + + placement.getClass().getName()); + } + + private static Object declaredFieldValue( + Class declaringType, + Object target, + Class fieldType + ) { + Field match = null; + for (Field field : declaringType.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != fieldType) { + continue; + } + if (match != null) { + throw new IllegalStateException("Ambiguous " + fieldType.getName() + " field on " + + declaringType.getName()); + } + match = field; + } + if (match == null) { + throw new IllegalStateException("Missing " + fieldType.getName() + " field on " + + declaringType.getName()); + } + try { + match.setAccessible(true); + return match.get(target); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not read " + fieldType.getName() + " field on " + + declaringType.getName(), e); + } + } + + static final class StructureSetKeyIndex { + private final Map, ResourceKey> + keysByEntries; + + private StructureSetKeyIndex( + Map, ResourceKey> + keysByEntries + ) { + this.keysByEntries = keysByEntries; + } + + private static StructureSetKeyIndex create( + Iterable> registeredStructureSets + ) { + Map, ResourceKey> keys = + new IdentityHashMap<>(); + for (Holder holder : registeredStructureSets) { + Optional> key = holder.unwrapKey(); + if (key.isEmpty()) { + continue; + } + List entries = holder.value().structures(); + ResourceKey previous = keys.put(entries, key.get()); + if (previous != null && !previous.equals(key.get())) { + throw new IllegalStateException( + "Multiple registered structure sets share the same entry list"); + } + } + return new StructureSetKeyIndex(keys); + } + + private Optional> resolve(Holder holder) { + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + return key; + } + return Optional.ofNullable(keysByEntries.get(holder.value().structures())); + } + } + + record Selection( + List> structureSets, + int retainedManagedSets, + int excludedManagedSets + ) { + } + + private static final class ReboundStructureSetHolder extends Holder.Reference { + private ReboundStructureSetHolder(ResourceKey key) { + super(Type.STAND_ALONE, new HolderOwner<>() { + }, key, null); + } + + private void bind(StructureSet structureSet) { + bindValue(structureSet); + } + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/ImportedFeatureStage.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/ImportedFeatureStage.java new file mode 100644 index 000000000..db370e592 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/ImportedFeatureStage.java @@ -0,0 +1,399 @@ +package art.arcane.iris.core.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.spi.IrisLogging; +import it.unimi.dsi.fastutil.ints.IntArraySet; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.FeatureSorter; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with + * {@code importedFeatures.enabled} the vanilla placed-feature pass runs over Iris terrain, and with the + * control off nothing here allocates or runs. + * + *

Iris does its own native structure pass and calls the delegate with {@code addVanillaDecorations=false}, + * so the vanilla feature half never runs on its own. This reproduces that half only - structures are never + * placed twice. + * + *

Threading: called from {@code applyBiomeDecoration} on the worldgen thread that owns the chunk. The + * FEATURES chunk step is not parallel-safe. + */ +final class ImportedFeatureStage { + private static final String CYCLE_MARKER = "Feature order cycle found"; + + private final Engine engine; + private volatile FeatureTable featureTable; + private volatile IrisDimension inertDimension; + private volatile int settledRuntimeId; + + ImportedFeatureStage(Engine engine) { + this.engine = engine; + } + + /** + * Generation settings for one biome holder, mapping Iris custom biomes onto their vanilla derivative. + * Pass-through while the control is off, which is what keeps {@code BiomeFilter} behaving exactly as it + * does today. + */ + BiomeGenerationSettings generationSettings(Holder biome) { + FeatureTable table = featureTable; + if (table == null) { + return null; + } + Holder mapped = table.derivatives().get(holderKey(biome)); + return mapped == null ? null : mapped.value().getGenerationSettings(); + } + + /** + * Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is + * reported once here and degrades to features-off; it never reaches chunk generation as a crash. + * + *

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)) { + return; + } + synchronized (this) { + if (settled(dimension, runtimeId)) { + return; + } + build(level, dimension); + settledRuntimeId = runtimeId; + } + } + + 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; + } + + private void 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; + } + if (!control.shouldGenerateFeatures()) { + markInert(dimension); + return; + } + FeatureTable built; + try { + built = buildTable(level, control, dimension); + } catch (Throwable error) { + IrisLogging.error("Iris importedFeatures is off for " + dimensionKey() + + ": feature table construction failed: " + error); + markInert(dimension); + return; + } + if (built == null) { + markInert(dimension); + return; + } + 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; + } + + private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control, + IrisDimension dimension) { + Registry registry = level.registryAccess().lookupOrThrow(Registries.BIOME); + Set visibleKeys = visibleBiomeKeys(); + // Registry-ordered walk, never a hash-ordered set: FeatureSorter's cycle detection walks this list and + // an unordered walk makes detection depend on JVM hash order. + List> biomes = new ArrayList<>(); + Map> byKey = new HashMap<>(); + registry.listElements().forEach((Holder.Reference reference) -> { + String key = holderKey(reference); + if (key != null && visibleKeys.contains(key)) { + biomes.add(reference); + byKey.put(key, reference); + } + }); + if (biomes.isEmpty()) { + IrisLogging.error("Iris importedFeatures is on but " + dimensionKey() + + " exposes no registered biomes; features off"); + return null; + } + Map> derivatives = customBiomeDerivatives(registry, byKey); + List steps; + try { + steps = FeatureSorter.buildFeaturesPerStep(biomes, + (Holder biome) -> settingsFor(biome, derivatives).features(), true); + } catch (IllegalStateException error) { + String message = error.getMessage(); + if (message == null || !message.contains(CYCLE_MARKER)) { + throw error; + } + IrisLogging.error("Iris importedFeatures is off for " + dimensionKey() + + ": the registered placed features cannot be ordered. " + message + + ". Remove or reorder the conflicting content, or leave importedFeatures.enabled false."); + return null; + } + boolean filtered = control.getDisabled() != null && !control.getDisabled().isEmpty(); + return new FeatureTable(dimension, control, List.copyOf(biomes), Set.copyOf(biomes), + Map.copyOf(byKey), steps, Map.copyOf(derivatives), filtered); + } + + /** + * Every biome key Iris can write into a chunk section: the structure derivative, the raw derivative, every + * scatter entry, and every generated custom biome. + */ + private Set visibleBiomeKeys() { + Set keys = new LinkedHashSet<>(); + String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT); + for (IrisBiome irisBiome : engine.getAllBiomes()) { + addKey(keys, irisBiome.getStructureDerivativeKey()); + addKey(keys, irisBiome.getDerivativeKey()); + addKey(keys, irisBiome.getVanillaDerivativeKey()); + for (String scatter : irisBiome.getBiomeScatter()) { + addKey(keys, scatter); + } + for (String scatter : irisBiome.getBiomeSkyScatter()) { + addKey(keys, scatter); + } + if (!irisBiome.isCustom()) { + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + addKey(keys, namespace + ":" + customBiome.getId()); + } + } + return keys; + } + + /** + * Maps every generated Iris custom biome key onto the registry holder of its Iris biome's vanilla + * derivative. The custom biome's own datapack JSON carries no features by design. + */ + private Map> customBiomeDerivatives(Registry registry, + Map> byKey) { + Map> derivatives = new HashMap<>(); + String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT); + for (IrisBiome irisBiome : engine.getAllBiomes()) { + if (!irisBiome.isCustom()) { + continue; + } + String derivativeKey = normalizeKey(irisBiome.getVanillaDerivativeKey()); + // Resolve from the registry, not only from the visible biome set: a sea or shore biome's structure + // derivative is rewritten away from its vanilla derivative, so the derivative whose features we + // want is not always a biome Iris can emit. + Holder derivative = byKey.get(derivativeKey); + if (derivative == null) { + derivative = resolveHolder(registry, derivativeKey); + } + if (derivative == null) { + IrisLogging.warn("Iris importedFeatures: vanilla derivative " + derivativeKey + " of biome " + + irisBiome.getLoadKey() + + " is not registered; its custom biomes generate no imported features"); + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + derivatives.put(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT), derivative); + } + } + return derivatives; + } + + private static Holder resolveHolder(Registry registry, String key) { + if (registry == null || key == null || key.isBlank()) { + return null; + } + Identifier identifier = Identifier.tryParse(key); + if (identifier == null) { + return null; + } + return registry.get(identifier).>map((Holder.Reference reference) -> reference) + .orElse(null); + } + + private static BiomeGenerationSettings settingsFor(Holder biome, + Map> derivatives) { + Holder mapped = derivatives.get(holderKey(biome)); + return mapped == null + ? biome.value().getGenerationSettings() + : mapped.value().getGenerationSettings(); + } + + /** + * Runs the vanilla placed-feature pass for one chunk on the calling worldgen thread. A no-op while the + * control is disabled or the table degraded. + */ + void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) { + FeatureTable table = featureTable; + if (table == null) { + return; + } + ChunkPos centerPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.of(centerPos, level.getMinSectionY()); + BlockPos origin = sectionPos.origin(); + Registry featureRegistry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + List steps = table.steps(); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); + Set> chunkBiomes = chunkBiomes(level, sectionPos, table); + + 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, + random, decorationSeed, origin, stepIndex); + } + } catch (Throwable error) { + throw new IllegalStateException("Iris imported feature placement failed for chunk " + + centerPos.x() + "," + centerPos.z(), error); + } finally { + level.setCurrentlyGenerating(null); + } + } + + private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData, + Registry featureRegistry, Set> chunkBiomes, + ChunkGenerator owner, WorldgenRandom random, long decorationSeed, + BlockPos origin, int stepIndex) { + IntSet stepFeatures = new IntArraySet(); + for (Holder biome : chunkBiomes) { + List> biomeFeatures = settingsFor(biome, table.derivatives()).features(); + if (stepIndex >= biomeFeatures.size()) { + continue; + } + for (Holder feature : biomeFeatures.get(stepIndex)) { + stepFeatures.add(stepData.indexMapping().applyAsInt(feature.value())); + } + } + if (stepFeatures.isEmpty()) { + return; + } + // Sorted global indices: identical ordering to vanilla, and each feature's seed comes from its own + // global index, so denying one feature never shifts another. + int[] featureIndices = stepFeatures.toIntArray(); + Arrays.sort(featureIndices); + for (int globalIndex : featureIndices) { + PlacedFeature feature = stepData.features().get(globalIndex); + if (table.filtered()) { + Identifier featureId = featureRegistry.getKey(feature); + if (featureId != null && !table.control().shouldGenerate(featureId.toString())) { + continue; + } + } + random.setFeatureSeed(decorationSeed, globalIndex, stepIndex); + level.setCurrentlyGenerating(() -> describeFeature(featureRegistry, feature)); + feature.placeWithBiomeCheck(level, owner, random, origin); + } + } + + private static String describeFeature(Registry registry, PlacedFeature feature) { + Identifier id = registry.getKey(feature); + return id == null ? feature.toString() : id.toString(); + } + + private Set> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) { + List> 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); + } + }); + Set> present = new LinkedHashSet<>(); + for (Holder biome : collected) { + if (table.biomeSet().contains(biome)) { + present.add(biome); + continue; + } + String key = holderKey(biome); + Holder canonical = key == null ? null : table.byKey().get(key); + if (canonical != null) { + present.add(canonical); + } + } + return present; + } + + private static void addKey(Set keys, String key) { + String normalized = normalizeKey(key); + if (normalized != null) { + keys.add(normalized); + } + } + + private String dimensionKey() { + IrisDimension dimension = engine.getDimension(); + return dimension == null ? "" : dimension.getLoadKey(); + } + + private static String holderKey(Holder holder) { + return holder.unwrapKey() + .map(key -> key.identifier().toString().toLowerCase(Locale.ROOT)) + .orElse(null); + } + + private static String normalizeKey(String key) { + Identifier identifier = key == null ? null : Identifier.tryParse(key); + return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT); + } + + private record FeatureTable(IrisDimension dimension, IrisImportedFeatureControl control, + List> biomes, Set> biomeSet, + Map> byKey, + List steps, + Map> derivatives, boolean filtered) { + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java new file mode 100644 index 000000000..7c106f661 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java @@ -0,0 +1,1215 @@ +package art.arcane.iris.core.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.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.nativegen.NativeStructureGenerationException; +import art.arcane.iris.nativegen.NativeStructureStartInjector; +import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope; +import art.arcane.iris.nativegen.NativeStructureLocateResults; +import art.arcane.iris.nativegen.NativeStructureLocatePersistence; +import art.arcane.iris.nativegen.NativeStructureOwnershipRecovery; +import art.arcane.iris.nativegen.NativeStructurePostProcessor; +import art.arcane.iris.nativegen.NativeStructureReferenceRepair; +import art.arcane.iris.nativegen.NativeStructureSurfaceFitter; +import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator; +import art.arcane.iris.nativegen.NativeStructureVegetationClearer; +import art.arcane.iris.nativegen.NativeStructureVerticalPlacer; +import art.arcane.iris.nativegen.NativeStructureVanillaLocator; +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.volmlib.util.math.RNG; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.random.Weighted; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.blending.Blender; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.bukkit.craftbukkit.generator.CustomChunkGenerator; +import org.bukkit.block.data.BlockData; +import org.spigotmc.SpigotWorldConfig; + +import javax.annotation.Nullable; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.IntBinaryOperator; + +public class IrisChunkGenerator extends CustomChunkGenerator { + private static final WrappedField BIOME_SOURCE; + private static final WrappedReturningMethod SET_HEIGHT; + private static final Runnable NO_OP = () -> { + }; + private final ChunkGenerator delegate; + private final Engine engine; + private final CustomBiomeSource customBiomeSource; + private final ServerLevel runtimeLevel; + private final @Nullable BukkitChunkGenerator platformGenerator; + private final int runtimeMinY; + private final int runtimeHeight; + private final int runtimeSeaLevel; + private final ConcurrentHashMap> mergedSpawnTables = new ConcurrentHashMap<>(); + private volatile IrisDimension spawnTableDimension; + private volatile int spawnTableRuntimeId; + private final ImportedFeatureStage importedFeatures; + private final AtomicReference retainedStudioStructureState = new AtomicReference<>(); + private volatile ReachableStructureCache reachableStructureCache; + private volatile StructureStepCache structureStepCache; + + public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) { + this(delegate, engine, world, new CustomBiomeSource(seed, engine, world)); + } + + private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) { + super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), world.getGenerator()); + this.delegate = delegate; + this.engine = engine; + this.customBiomeSource = customBiomeSource; + this.importedFeatures = new ImportedFeatureStage(engine); + ServerLevel level = ((CraftWorld) world).getHandle(); + this.runtimeLevel = level; + this.platformGenerator = world.getGenerator() instanceof BukkitChunkGenerator bukkitGenerator + ? bukkitGenerator + : null; + this.runtimeMinY = level.getMinY(); + this.runtimeHeight = level.getHeight(); + this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight(); + installNativeStructureVolumeIndex(level); + } + + private void installNativeStructureVolumeIndex(ServerLevel level) { + WeakReference levelReference = new WeakReference<>(level); + WeakReference generatorReference = new WeakReference<>(this); + WeakReference biomeSourceReference = new WeakReference<>(customBiomeSource); + NativeStructureVolumeIndex.install(engine, new NativeStructureVolumeIndex.Context( + level.registryAccess(), + level.getServer().getStructureManager(), + level.dimension(), + LevelHeightAccessor.create(runtimeMinY, runtimeHeight), + generatorReference::get, + biomeSourceReference::get, + () -> { + ServerLevel active = levelReference.get(); + return active == null ? null : active.getChunkSource().getGeneratorState(); + })); + } + + @Override + public @Nullable Pair> findNearestMapStructure(ServerLevel level, HolderSet holders, BlockPos pos, int radius, boolean findUnexplored) { + if (platformGenerator != null && !platformGenerator.shouldGenerateStructures()) { + return null; + } + if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) { + return null; + } + try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + HolderSet reachable = filterReachableStructures(level, holders); + NativeStructureVanillaLocator.Candidate nativeCandidate = + reachable == null || reachable.size() == 0 ? null + : NativeStructureVanillaLocator.predict( + level, reachable, pos, radius, findUnexplored); + return findNearestIrisStructure( + level, holders, pos, Math.max(0, radius), + findUnexplored, nativeCandidate); + } + } + + private Pair> findNearestIrisStructure(ServerLevel level, + HolderSet holders, + BlockPos pos, int radius, + boolean findUnexplored, + NativeStructureVanillaLocator.Candidate nativeCandidate) { + Pair> nativeLocated = + nativeCandidate == null ? null : nativeCandidate.result(); + Runnable nativeReference = () -> { + if (nativeCandidate != null) { + nativeCandidate.reference(level.structureManager()); + } + }; + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List searches = new ArrayList<>(holders.size()); + NativeStructureLocatePersistence.ProbeBudget budget = NativeStructureLocatePersistence.probeBudget(); + for (Holder holder : holders) { + Object id = registry.getKey(holder.value()); + if (id == null) { + throw new IllegalStateException("Native structure locate received an unregistered structure holder"); + } + String structureId = id.toString(); + if (!IrisStructureLocator.hasNativePlacement(engine, structureId)) { + continue; + } + NativeStructureLocatePersistence.Probe probe = NativeStructureLocatePersistence.probe( + level, holder.value(), findUnexplored, budget); + searches.add(new IrisNativeLocateSearch( + holder, structureId, NativeStructureLocatePersistence.search( + engine, structureId, pos.getX(), pos.getZ(), radius, probe))); + } + searches.sort(Comparator.comparing(IrisNativeLocateSearch::structureId)); + for (int attempt = 0; attempt < NativeStructureLocatePersistence.MAX_SELECTED_CANDIDATE_RETRIES; attempt++) { + IrisNativeLocateSearch bestSearch = null; + IrisStructureLocator.LocateResult bestResult = null; + long bestDistance = Long.MAX_VALUE; + for (IrisNativeLocateSearch search : searches) { + IrisStructureLocator.LocateResult result = search.search().predict(); + if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { + throw new IllegalStateException("Iris structure locate reached its safety limit for " + + search.structureId() + " within " + radius + " placement rings"); + } + if (!result.found()) { + continue; + } + long dx = (long) result.originX() - pos.getX(); + long dz = (long) result.originZ() - pos.getZ(); + long distance = dx * dx + dz * dz; + if (distance < bestDistance) { + bestDistance = distance; + bestSearch = search; + bestResult = result; + } + } + if (bestSearch == null) { + return NativeStructureLocateResults.selectAndReference( + pos, null, () -> { }, nativeLocated, nativeReference); + } + Pair> predicted = Pair.of( + new BlockPos(bestResult.originX(), bestResult.baseY(), bestResult.originZ()), + bestSearch.holder()); + if (NativeStructureLocateResults.nearest(pos, predicted, nativeLocated) != predicted) { + return NativeStructureLocateResults.selectAndReference( + pos, predicted, () -> { }, nativeLocated, nativeReference); + } + NativeStructureLocatePersistence.VerifiedStart verified = + bestSearch.search().verify(bestResult); + if (verified == null) { + bestSearch.search().reject(bestResult); + continue; + } + BlockPos located = new BlockPos( + bestResult.originX(), verified.ownership().locatorY(), + bestResult.originZ()); + Pair> irisLocated = Pair.of(located, bestSearch.holder()); + IrisNativeLocateSearch selectedSearch = bestSearch; + NativeStructureLocatePersistence.VerifiedStart selectedStart = verified; + return NativeStructureLocateResults.selectAndReference( + pos, irisLocated, () -> selectedSearch.search().reference(selectedStart), + nativeLocated, nativeReference); + } + throw new IllegalStateException("Iris structure locate rejected too many selected candidates within " + + radius + " placement rings"); + } + + private HolderSet filterReachableStructures(ServerLevel level, HolderSet holders) { + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List candidates = new ArrayList<>(holders.size()); + for (Holder holder : holders) { + Object id = registry.getKey(holder.value()); + if (id == null) { + throw new IllegalStateException("Native structure filtering received an unregistered structure holder"); + } + String key = id.toString(); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, + key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step())); + if (!decision.generate()) { + continue; + } + candidates.add(new NativeLocateCandidate(holder, key)); + } + if (candidates.isEmpty()) { + return HolderSet.direct(List.of()); + } + Set reachable = reachableStructureKeys(level); + List> kept = new ArrayList<>(candidates.size()); + for (NativeLocateCandidate candidate : candidates) { + if (reachable.contains(candidate.key())) { + kept.add(candidate.holder()); + } + } + if (kept.size() == holders.size()) { + return holders; + } + return HolderSet.direct(kept); + } + + private Set reachableStructureKeys(ServerLevel level) { + IrisDimension dimension = engine.getDimension(); + int runtimeId = engine.getCacheID(); + ReachableStructureCache cached = reachableStructureCache; + if (cached != null && cached.dimension() == dimension && cached.runtimeId() == runtimeId) { + return cached.keys(); + } + synchronized (this) { + cached = reachableStructureCache; + if (cached != null && cached.dimension() == dimension && cached.runtimeId() == runtimeId) { + return cached.keys(); + } + Set reachable = Set.copyOf( + VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource)); + reachableStructureCache = new ReachableStructureCache(dimension, runtimeId, reachable); + return reachable; + } + } + + @Override + protected MapCodec codec() { + return MapCodec.unit(null); + } + + @Override + public ChunkGenerator getDelegate() { + if (delegate instanceof CustomChunkGenerator chunkGenerator) + return chunkGenerator.getDelegate(); + return delegate; + } + + @Override + public int getMinY() { + return runtimeMinY; + } + + @Override + public int getSeaLevel() { + return runtimeSeaLevel; + } + + @Override + public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey levelKey) { + if (platformGenerator != null && !platformGenerator.shouldGenerateStructures()) { + return; + } + if (runtimeLevel.getChunkSource().getGenerator() != this + || runtimeLevel.getChunkSource().getGeneratorState() != structureState) { + return; + } + try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_structures"); + GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + Map previousStarts = new HashMap<>(access.getAllStarts()); + super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey); + Map configuredStarts = NativeStructureStartInjector.inject( + new NativeStructureStartInjector.InjectionContext( + engine, + registryAccess, + structureState, + structureManager, + access, + templateManager, + levelKey, + this, + customBiomeSource + )); + adjustGeneratedStructures( + registryAccess, access, previousStarts, configuredStarts, templateManager); + } + } + + private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, + Map previousStarts, + Map configuredStarts, + StructureTemplateManager templateManager) { + Registry registry = registryAccess.lookupOrThrow(Registries.STRUCTURE); + ChunkPos chunkPos = access.getPos(); + for (Map.Entry entry : access.getAllStarts().entrySet()) { + Structure structure = entry.getKey(); + StructureStart start = entry.getValue(); + 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()); + } + boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step()); + IrisNativeStructureDecision decision; + try { + decision = NativeStructureGenerationPolicy.resolve(engine, + structureId, undergroundStep); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "policy resolution", structureId, chunkPos.x(), chunkPos.z(), error); + } + if (!decision.generate()) { + access.setStartForStructure(structure, StructureStart.INVALID_START); + continue; + } + try { + NativeStructureVerticalPlacer.applyVerticalPlacement( + start, + structureId, + decision.yShift(), + getSeaLevel(), + access.getMinY(), + access.getMinY() + access.getHeight(), + undergroundStep, + decision.preserveSourceY(), + decision.yBand(), + (x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight()); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication( + start, structure, start.getReferences(), + NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()), + structureId); + access.setStartForStructure(structure, wrapped); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error); + } + } + } + + @Override + public ChunkGeneratorStructureState createState(HolderLookup holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) { + return delegate.createState(holderlookup, randomstate, i, conf); + } + + void retainStudioStructureState( + ServerLevel level, + ChunkMap chunkMap, + ChunkGeneratorStructureState structureState + ) { + requireCurrentStructureOwner(level, chunkMap); + StudioStructureState retained = new StudioStructureState( + level, + chunkMap, + Objects.requireNonNull(structureState, "Studio native structure state")); + if (!retainedStudioStructureState.compareAndSet(null, retained)) { + throw new IllegalStateException("Studio native structure state is already retained."); + } + } + + StudioStructureState retainedStudioStructureState(ServerLevel level, ChunkMap chunkMap) { + StudioStructureState retained = retainedStudioStructureState.get(); + if (retained == null) { + return null; + } + requireCurrentStructureOwner(level, chunkMap); + if (retained.level() != level || retained.chunkMap() != chunkMap) { + throw new IllegalStateException("Retained Studio native structure state belongs to another world runtime."); + } + if (level.getChunkSource().getGeneratorState() != retained.structureState()) { + throw new IllegalStateException("Studio native structure state is no longer current."); + } + return retained; + } + + void claimStudioStructureState(StudioStructureState retained) { + if (!retainedStudioStructureState.compareAndSet(retained, null)) { + throw new IllegalStateException("Studio native structure state changed before activation began."); + } + } + + void abandonStudioStructureState() { + retainedStudioStructureState.set(null); + } + + CompletableFuture initializeAndPublishStructureState( + ChunkGeneratorStructureState structureState, + StructureStatePublisher publisher + ) { + return startStructureStateBootstrap( + structureState, + NO_OP, + () -> publishStructureState(publisher)); + } + + CompletableFuture activateStudioStructureState(StudioStructureState retained) { + Objects.requireNonNull(retained, "Retained Studio native structure state"); + return startStructureStateBootstrap( + retained.structureState(), + () -> { + StudioStructureState current = retainedStudioStructureState( + retained.level(), retained.chunkMap()); + if (current != retained) { + throw new IllegalStateException("Studio native structure state changed before activation."); + } + claimStudioStructureState(retained); + }, + NO_OP); + } + + private CompletableFuture startStructureStateBootstrap( + ChunkGeneratorStructureState structureState, + Runnable claim, + Runnable activation + ) { + if (!(engine instanceof IrisEngine irisEngine)) { + throw new IllegalStateException("Native structure bootstrap requires an IrisEngine runtime."); + } + AtomicReference> registeredCompletion = new AtomicReference<>(); + CompletableFuture completion = irisEngine.startNativeStructureBootstrap( + claim, + () -> { + CompletableFuture rings = initializeStructureState(structureState); + registeredCompletion.set(rings); + return rings; + }, + () -> { + CompletableFuture rings = Objects.requireNonNull( + registeredCompletion.get(), + "Registered native structure ring completion"); + if (rings.isCompletedExceptionally()) { + throw new IllegalStateException( + "Minecraft native structure ring bootstrap failed before activation."); + } + activation.run(); + }); + completion.whenComplete((ignored, failure) -> { + if (failure != null) { + Throwable cause = failure instanceof CompletionException && failure.getCause() != null + ? failure.getCause() + : failure; + IrisLogging.reportError("Native structure ring bootstrap failed for world '" + + runtimeLevel.getWorld().getName() + "'.", cause); + } + }); + return completion; + } + + private CompletableFuture initializeStructureState(ChunkGeneratorStructureState structureState) { + Map ringPositions = structureRingPositions(structureState); + structureState.ensureStructuresGenerated(); + return structureRingCompletion(ringPositions); + } + + private Map structureRingPositions(ChunkGeneratorStructureState structureState) { + try { + Field field = structureRingPositionsField(); + field.setAccessible(true); + Object value = field.get(structureState); + if (!(value instanceof Map ringPositions)) { + throw new IllegalStateException("Minecraft native structure ring state is unavailable."); + } + return ringPositions; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not bind Minecraft native structure ring completions.", e); + } + } + + private CompletableFuture structureRingCompletion(Map ringPositions) { + List> futures = new ArrayList<>(ringPositions.size()); + for (Object candidate : ringPositions.values()) { + if (candidate instanceof CompletableFuture future) { + futures.add(future); + } else { + throw new IllegalStateException( + "Minecraft native structure ring completion is not a future."); + } + } + CompletableFuture[] completions = futures.toArray(new CompletableFuture[0]); + return CompletableFuture.allOf(completions); + } + + private Field structureRingPositionsField() { + List candidates = new ArrayList<>(1); + for (Field field : ChunkGeneratorStructureState.class.getDeclaredFields()) { + if (!Map.class.isAssignableFrom(field.getType())) { + continue; + } + Type genericType = field.getGenericType(); + if (!(genericType instanceof ParameterizedType parameterizedType)) { + continue; + } + Type[] arguments = parameterizedType.getActualTypeArguments(); + if (arguments.length == 2 + && arguments[1].getTypeName().contains(CompletableFuture.class.getName())) { + candidates.add(field); + } + } + if (candidates.size() != 1) { + throw new IllegalStateException("Expected one Minecraft native structure ring-future map, found " + + candidates.size() + "."); + } + return candidates.getFirst(); + } + + private void requireCurrentStructureOwner(ServerLevel level, ChunkMap chunkMap) { + if (runtimeLevel != level + || level.getChunkSource().chunkMap != chunkMap + || level.getChunkSource().getGenerator() != this) { + throw new IllegalStateException("Iris native structure state no longer belongs to the active world runtime."); + } + } + + private void publishStructureState(StructureStatePublisher publisher) { + try { + publisher.publish(); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not publish Minecraft native structure state.", e); + } + } + + @Override + public void createReferences(WorldGenLevel generatoraccessseed, StructureManager structuremanager, ChunkAccess ichunkaccess) { + if (platformGenerator != null && !platformGenerator.shouldGenerateStructures()) { + return; + } + if (runtimeLevel.getChunkSource().getGenerator() != this) { + return; + } + try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_references"); + GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_references"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + NativeStructureReferenceRepair.createReferences( + engine, generatoraccessseed, structuremanager, ichunkaccess); + } + } + + @Override + public CompletableFuture createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) { + try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_biomes"); + GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler()); + return CompletableFuture.completedFuture(ichunkaccess); + } + } + + @Override + public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) { + delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess); + } + + @Override + public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) { + delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess); + } + + @Override + public CompletableFuture fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) { + BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage( + ichunkaccess.getPos(), + "bukkit_nms_chunk_pipeline"); + GenerationSessionLease lease; + try { + lease = requireGenerationLease("bukkit_nms_chunk_pipeline"); + } catch (RuntimeException | Error failure) { + stage.close(); + throw failure; + } + try { + CompletableFuture pipeline = delegate + .fillFromNoise(blender, randomstate, structuremanager, ichunkaccess) + .thenApply(filled -> { + try (IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + primeWorldgenHeightmaps(filled); + return filled; + } + }); + CompletableFuture completion = new CompletableFuture<>(); + pipeline.whenComplete((filled, failure) -> { + boolean cancelled = isCancellationFailure(failure); + lease.close(); + stage.close(); + if (failure == null) { + completion.complete(filled); + } else if (cancelled) { + completion.cancel(false); + } else { + completion.completeExceptionally(failure); + } + }); + return completion; + } catch (RuntimeException | Error failure) { + lease.close(); + stage.close(); + throw failure; + } + } + + private static boolean isCancellationFailure(Throwable failure) { + Throwable current = failure; + while (current instanceof CompletionException && current.getCause() != null) { + current = current.getCause(); + } + return current instanceof CancellationException; + } + + private void primeWorldgenHeightmaps(ChunkAccess chunkAccess) { + WorldgenTerrainHeightmaps.primeTerrain(chunkAccess, worldgenSurfaceHeight(), worldgenFloorHeight()); + } + + private IntBinaryOperator worldgenSurfaceHeight() { + return (x, z) -> engine.getHeight(x, z, false) + runtimeMinY + 1; + } + + private IntBinaryOperator worldgenFloorHeight() { + return (x, z) -> engine.getHeight(x, z, true) + runtimeMinY + 1; + } + + @Override + public WeightedList getMobsAt(Holder holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) { + Holder vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder); + if (vanillaSpawnBiome == null) { + return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition); + } + + WeightedList vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(enumcreaturetype); + WeightedList resolvedSpawns = delegate.getMobsAt( + vanillaSpawnBiome, structuremanager, enumcreaturetype, blockposition); + if (resolvedSpawns != vanillaSpawns) { + return resolvedSpawns; + } + + WeightedList explicitSpawns = holder.value().getMobSettings().getMobs(enumcreaturetype); + if (explicitSpawns.isEmpty()) { + return vanillaSpawns; + } + if (vanillaSpawns.isEmpty()) { + 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); + return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns)); + } + + @Override + public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) { + applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, true); + } + + @Override + public void addDebugScreenInfo(List list, RandomState randomstate, BlockPos blockposition) { + delegate.addDebugScreenInfo(list, randomstate, blockposition); + } + + @Override + public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) { + try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_biome_decoration"); + 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); + } + } + + /** + * Iris custom biomes carry no features in their datapack JSON by design; when importedFeatures is on they + * inherit the generation settings of the vanilla biome their Iris biome derives from. This is the gate + * {@code BiomeFilter} consults, so it has to agree with the feature pass. With the control off this is + * exactly the inherited behaviour. + */ + @Override + public BiomeGenerationSettings getBiomeGenerationSettings(Holder holder) { + BiomeGenerationSettings imported = importedFeatures.generationSettings(holder); + return imported == null ? super.getBiomeGenerationSettings(holder) : imported; + } + + private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) { + if (!structureManager.shouldGenerateStructures()) { + ChunkPos disabledChunk = chunk.getPos(); + throw new IllegalStateException("Iris cannot generate native structures in chunk " + + disabledChunk.x() + "," + disabledChunk.z() + + " because structure generation is disabled outside the pack; enable native structure generation " + + "and deny families through importedStructures.disabled or complete keys through importedStructures.disabledExact"); + } + ChunkPos chunkPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY()); + BlockPos origin = sectionPos.origin(); + Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> byStep = structuresByStep(registry); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); + BoundingBox area = writableArea(chunk); + int steps = GenerationStep.Decoration.values().length; + List placementGroups = new ArrayList<>(); + List heightmapStarts = new ArrayList<>(); + List vegetationTargets = new ArrayList<>(); + List terrainTargets = new ArrayList<>(); + for (int step = 0; step < steps; step++) { + int index = 0; + for (Structure structure : byStep.get(step)) { + Object id = registry.getKey(structure); + String structureId = id == null ? null : id.toString(); + if (structureId == null) { + throw NativeStructureGenerationException.failure( + "resolution", null, chunkPos.x(), chunkPos.z()); + } + try { + IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(engine, + structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + List starts = structureManager.startsForStructure(sectionPos, structure); + List resolvedPlacements = new ArrayList<>(starts.size()); + for (StructureStart start : starts) { + NativeStructureOwnershipRecord ownership = + NativeStructureOwnershipRecovery.resolve( + engine, world.getLevel(), structureId, structure, start); + IrisNativeStructureDecision decision = + ownership == null ? sourceDecision : ownership.restoredDecision(); + if (!decision.generate()) { + continue; + } + resolvedPlacements.add(new NativePlacement(start, decision)); + heightmapStarts.add(start); + terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget( + structureId, start, + NativeStructureTerrainIntegrator.resolveNativeTerrain( + start, decision.terrain()))); + vegetationTargets.add(start); + } + if (!resolvedPlacements.isEmpty()) { + placementGroups.add(new NativePlacementGroup( + structureId, index, step, List.copyOf(resolvedPlacements))); + } + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "resolution", structureId, chunkPos.x(), chunkPos.z(), error); + } + index++; + } + } + try { + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, chunkPos, heightmapStarts, worldgenSurfaceHeight(), worldgenFloorHeight()); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "heightmap priming", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + try { + NativeStructureVegetationClearer.clearIntersectingVegetation( + world, chunk, area, vegetationTargets); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "vegetation cleanup", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan; + try { + vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures( + world, area, terrainTargets, + (x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight()); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "terrain integration", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + try { + NativeStructurePostProcessor.prepareTerrain( + world, area, terrainTargets, this::resolvePaletteBlock); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "terrain preparation", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + for (NativePlacementGroup group : placementGroups) { + random.setFeatureSeed(decoSeed, group.featureIndex(), group.step()); + try { + for (NativePlacement placement : group.placements()) { + placeVanillaStructure(world, structureManager, random, area, chunkPos, + group.structureId(), placement.start(), placement.decision()); + } + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "placement", group.structureId(), chunkPos.x(), chunkPos.z(), error); + } + } + try { + NativeStructureSurfaceFitter.repairVacuumFoundations( + world, area, vacuumFoundationPlan); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "foundation repair", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + } + + private static String nativeStructureBatchContext(List placementGroups) { + if (placementGroups.isEmpty()) { + return ""; + } + StringBuilder context = new StringBuilder("["); + for (int i = 0; i < placementGroups.size(); i++) { + if (i > 0) { + context.append(", "); + } + context.append(placementGroups.get(i).structureId()); + } + return context.append(']').toString(); + } + + 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()); + 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()); + } finally { + world.setCurrentlyGenerating(null); + } + } + + private List> structuresByStep(Registry registry) { + StructureStepCache cached = structureStepCache; + if (cached != null && cached.registry() == registry) { + return cached.structures(); + } + synchronized (this) { + cached = structureStepCache; + if (cached != null && cached.registry() == registry) { + return cached.structures(); + } + int steps = GenerationStep.Decoration.values().length; + List> grouped = new ArrayList<>(steps); + for (int step = 0; step < steps; step++) { + grouped.add(new ArrayList<>()); + } + for (Structure structure : registry) { + grouped.get(structure.step().ordinal()).add(structure); + } + for (int step = 0; step < steps; step++) { + grouped.set(step, List.copyOf(grouped.get(step))); + } + List> resolved = List.copyOf(grouped); + structureStepCache = new StructureStepCache(registry, resolved); + return resolved; + } + } + + private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) { + PlatformBlockState platformState = palette.get(rng, x, y, z, engine.getData()); + if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) { + throw new IllegalStateException("Configured native structure palette did not resolve a Bukkit block at " + + x + "," + y + "," + z); + } + if (blockData instanceof IrisCustomData customData) { + blockData = customData.getBase(); + } + if (blockData instanceof CraftBlockData craftBlockData) { + return craftBlockData.getState(); + } + throw new IllegalStateException("Configured native structure palette resolved unsupported Bukkit block data " + + blockData.getClass().getName() + " at " + x + "," + y + "," + z); + } + + private BoundingBox writableArea(ChunkAccess chunk) { + ChunkPos cp = chunk.getPos(); + int i = cp.getMinBlockX(); + int j = cp.getMinBlockZ(); + int minY = chunk.getMinY() + 1; + int maxY = chunk.getMinY() + chunk.getHeight() - 1; + return new BoundingBox(i, minY, j, i + 15, maxY, j + 15); + } + + @Override + public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) { + try (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); + + Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING); + Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES); + int minHeight = engine.getMinHeight(); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int wX = x + blockPos.getX(); + int wZ = z + blockPos.getZ(); + + int terrainTop = engine.getHeight(wX, wZ, false) + minHeight + 1; + SET_HEIGHT.invoke(motion, x, z, terrainTop); + SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop); + } + } + + Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS); + } catch (GenerationSessionException e) { + throw new IllegalStateException("Iris heightmap generation could not acquire its engine runtime.", e); + } + } + + @Override + public void spawnOriginalMobs(WorldGenRegion region) { + ChunkPos center = region.getCenter(); + Holder visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); + Holder 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); + } + + private static WeightedList mergeSpawnTables( + WeightedList vanillaSpawns, + WeightedList explicitSpawns) { + List> entries = new ArrayList<>( + vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size()); + Set> explicitTypes = new HashSet<>(); + for (Weighted entry : explicitSpawns.unwrap()) { + explicitTypes.add(entry.value().type()); + } + for (Weighted entry : vanillaSpawns.unwrap()) { + if (!explicitTypes.contains(entry.value().type())) { + entries.add(entry); + } + } + entries.addAll(explicitSpawns.unwrap()); + return WeightedList.of(entries); + } + + @Override + public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) { + return delegate.getSpawnHeight(levelheightaccessor); + } + + @Override + public int getGenDepth() { + return runtimeHeight; + } + + @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"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + return levelheightaccessor.getMinY() + + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + + 1; + } catch (GenerationSessionException e) { + throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e); + } + } + + @Override + public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) { + try (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); + BlockState[] column = new BlockState[levelheightaccessor.getHeight()]; + for (int k = 0; k < column.length; k++) { + if (k <= block) { + column[k] = Blocks.STONE.defaultBlockState(); + } else if (k <= water) { + column[k] = Blocks.WATER.defaultBlockState(); + } else { + column[k] = Blocks.AIR.defaultBlockState(); + } + } + return new NoiseColumn(levelheightaccessor.getMinY(), column); + } catch (GenerationSessionException e) { + throw new IllegalStateException("Iris base column query could not acquire its engine runtime.", e); + } + } + + private GenerationSessionLease requireGenerationLease(String operation) { + try { + return engine.acquireGenerationLease(operation); + } catch (GenerationSessionException exception) { + throw new IllegalStateException("Iris " + operation + " could not acquire its engine runtime.", exception); + } + } + + 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 getTypeNameForDataFixer() { + return delegate.getTypeNameForDataFixer(); + } + + @Override + public void validate() { + delegate.validate(); + } + + static { + List biomeSources = new ArrayList<>(1); + for (Field field : ChunkGenerator.class.getDeclaredFields()) { + if (!field.getType().equals(BiomeSource.class)) + continue; + biomeSources.add(field); + } + if (biomeSources.size() != 1) + throw new IllegalStateException("Expected exactly one BiomeSource field in ChunkGenerator, found " + + biomeSources.size() + " " + biomeSources.stream().map(Field::getName).toList()); + Field biomeSource = biomeSources.getFirst(); + + List setHeights = new ArrayList<>(1); + for (Method method : Heightmap.class.getDeclaredMethods()) { + Class[] types = method.getParameterTypes(); + if (!method.getName().equals("setHeight") + || !Arrays.equals(types, new Class[]{int.class, int.class, int.class}) + || !method.getReturnType().equals(void.class)) + continue; + setHeights.add(method); + } + if (setHeights.size() != 1) + throw new IllegalStateException("Expected exactly one Heightmap.setHeight(int,int,int) method, found " + + setHeights.size()); + Method setHeight = setHeights.getFirst(); + + BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName()); + SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes()); + } + + private static ChunkGenerator edit(ChunkGenerator generator, BiomeSource source) { + try { + BIOME_SOURCE.set(generator, source); + if (generator instanceof CustomChunkGenerator custom) + BIOME_SOURCE.set(custom.getDelegate(), source); + + return generator; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private record SpawnTableKey(Biome biome, MobCategory category) { + } + + private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set keys) { + } + + private record StructureStepCache(Registry registry, List> structures) { + } + + record StudioStructureState( + ServerLevel level, + ChunkMap chunkMap, + ChunkGeneratorStructureState structureState + ) { + } + + @FunctionalInterface + interface StructureStatePublisher { + void publish() throws IllegalAccessException; + } + + private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) { + } + + private record NativePlacementGroup(String structureId, int featureIndex, int step, + List placements) { + } + + private record NativeLocateCandidate(Holder holder, String key) { + } + + private record IrisNativeLocateSearch(Holder holder, String structureId, + NativeStructureLocatePersistence.Search search) { + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java new file mode 100644 index 000000000..cdf42970c --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java @@ -0,0 +1,1882 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +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.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.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.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.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.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.matter.MatterBiomeInject; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.util.nbt.common.mca.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; +import art.arcane.volmlib.util.nbt.mca.palette.MCAIdMap; +import art.arcane.volmlib.util.nbt.mca.palette.MCAIdMapper; +import art.arcane.volmlib.util.nbt.mca.palette.MCAPalette; +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 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; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CollectionTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.EndTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.commands.data.BlockDataAccessor; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ThreadedLevelLightEngine; +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; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; +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; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.levelgen.FlatLevelSource; +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.structure.StructureCheck; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; +import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature; +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.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.bukkit.craftbukkit.block.CraftBlockStates; +import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.bukkit.craftbukkit.generator.CraftChunkData; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.awt.Color; +import java.io.File; +import java.io.IOException; +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; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.List; +import java.util.Set; +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 { + private final KMap baseBiomeCache = new KMap<>(); + private volatile DataVersion dataVersion; + private final BlockData AIR = Material.AIR.createBlockData(); + private final AtomicCache> biomeMapCache = new AtomicCache<>(); + private final AtomicBoolean injected = new AtomicBoolean(); + private volatile ResettableClassFileTransformer levelStorageAccessTransformer; + private volatile ResettableClassFileTransformer serverLevelTransformer; + private final AtomicCache> registryCache = new AtomicCache<>(); + private final AtomicCache> globalCache = new AtomicCache<>(); + private final AtomicCache registryAccess = new AtomicCache<>(); + private final AtomicCache byIdRef = new AtomicCache<>(); + + 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 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; + } + + 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; + } + + @SuppressWarnings("unchecked") + private static T fieldForClass(Class 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; + } + + private static Class getClassType(Class type, int ordinal) { + return type.getDeclaredClasses()[ordinal]; + } + + @Override + public boolean hasTile(Material material) { + return !CraftBlockState.class.equals(CraftBlockStates.getBlockStateType(material)); + } + + @Override + public String getEntitySpawnCategory(String key) { + Identifier identifier = Identifier.parse(key); + if (!BuiltInRegistries.ENTITY_TYPE.containsKey(identifier)) { + throw new IllegalArgumentException("Unknown native entity type: " + key); + } + EntityType type = BuiltInRegistries.ENTITY_TYPE.getValue(identifier); + return type.getCategory().getSerializedName().toLowerCase(Locale.ROOT); + } + + @Override + public boolean hasTile(Location l) { + return ((CraftWorld) l.getWorld()).getHandle().getBlockEntity(new BlockPos(l.getBlockX(), l.getBlockY(), l.getBlockZ())) != null; + } + + @Override + @SuppressWarnings("unchecked") + public KMap serializeTile(Location location) { + BlockEntity e = ((CraftWorld) location.getWorld()).getHandle().getBlockEntity(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ())); + + if (e == null) { + return null; + } + + net.minecraft.nbt.CompoundTag tag = e.saveWithoutMetadata(registry()); + return (KMap) convertFromTag(tag, 0, 64); + } + + @Contract(value = "null, _, _ -> null", pure = true) + private Object convertFromTag(Tag tag, int depth, int maxDepth) { + if (tag == null || depth > maxDepth) return null; + return switch (tag) { + case CollectionTag collection -> { + KList list = new KList<>(); + + for (Object i : collection) { + if (i instanceof Tag t) + list.add(convertFromTag(t, depth + 1, maxDepth)); + else list.add(i); + } + yield list; + } + case net.minecraft.nbt.CompoundTag compound -> { + KMap map = new KMap<>(); + + for (String key : compound.keySet()) { + var child = compound.get(key); + if (child == null) continue; + var value = convertFromTag(child, depth + 1, maxDepth); + if (value == null) continue; + map.put(key, value); + } + yield map; + } + case NumericTag numeric -> numeric.box(); + default -> tag.asString().orElse(null); + }; + } + + @Override + public void deserializeTile(KMap map, Location pos) { + if (map == null || pos == null || pos.getWorld() == null) { + return; + } + + Tag converted = convertToTag(map, 0, 64); + if (!(converted instanceof net.minecraft.nbt.CompoundTag tag)) { + return; + } + + ServerLevel level = ((CraftWorld) pos.getWorld()).getHandle(); + BlockPos blockPos = new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); + int chunkX = pos.getBlockX() >> 4; + int chunkZ = pos.getBlockZ() >> 4; + if (J.isOwnedByCurrentRegion(pos.getWorld(), chunkX, chunkZ)) { + merge(level, blockPos, tag); + return; + } + if (!J.runAt(pos, () -> merge(level, blockPos, tag))) { + IrisLogging.warn("[NMS] Failed to schedule tile deserialize at " + blockPos + " in world " + pos.getWorld().getName()); + } + } + + private void merge(ServerLevel level, BlockPos blockPos, net.minecraft.nbt.CompoundTag tag) { + if (level == null || blockPos == null || tag == null) { + return; + } + + try { + var blockEntity = level.getBlockEntity(blockPos); + if (blockEntity == null) { + IrisLogging.warn("[NMS] BlockEntity not found at " + blockPos); + var state = level.getBlockState(blockPos); + if (!state.hasBlockEntity()) { + return; + } + + blockEntity = ((EntityBlock) state.getBlock()) + .newBlockEntity(blockPos, state); + } + + var accessor = new BlockDataAccessor(blockEntity, blockPos); + accessor.setData(accessor.getData().merge(tag)); + } catch (Throwable e) { + IrisLogging.warn("[NMS] Failed to merge tile data at " + blockPos + ": " + e.getMessage()); + IrisLogging.reportError(e); + } + } + + private Tag convertToTag(Object object, int depth, int maxDepth) { + if (object == null || depth > maxDepth) return EndTag.INSTANCE; + return switch (object) { + case Map map -> { + var tag = new net.minecraft.nbt.CompoundTag(); + for (var i : map.entrySet()) { + tag.put(i.getKey().toString(), convertToTag(i.getValue(), depth + 1, maxDepth)); + } + yield tag; + } + case List list -> { + var tag = new ListTag(); + for (var i : list) { + tag.add(convertToTag(i, depth + 1, maxDepth)); + } + yield tag; + } + case Byte number -> ByteTag.valueOf(number); + case Short number -> ShortTag.valueOf(number); + case Integer number -> IntTag.valueOf(number); + case Long number -> LongTag.valueOf(number); + case Float number -> FloatTag.valueOf(number); + case Double number -> DoubleTag.valueOf(number); + case String string -> StringTag.valueOf(string); + default -> EndTag.INSTANCE; + }; + } + + @Override + public CompoundTag serializeEntity(Entity location) { + return null;// TODO: + } + + @Override + public Entity deserializeEntity(CompoundTag s, Location newPosition) { + return null;// TODO: + } + + @Override + public boolean supportsCustomHeight() { + return true; + } + + 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; + } + + private Registry getCustomBiomeRegistry() { + return registry().lookup(Registries.BIOME).orElseThrow(() -> new IllegalStateException( + "Iris cannot resolve the Minecraft biome registry on this server version")); + } + + private Registry getBlockRegistry() { + return registry().lookup(Registries.BLOCK).orElse(null); + } + + @Override + public Object getBiomeBaseFromId(int id) { + Object raw = getCustomBiomeRegistry().get(id); + if (raw instanceof java.util.Optional opt) { + raw = opt.orElse(null); + } + if (raw instanceof Holder holder) { + raw = holder.value(); + } + return raw; + } + + @Override + public int getMinHeight(World world) { + return world.getMinHeight(); + } + + @Override + public boolean supportsCustomBiomes() { + return true; + } + + @Override + public boolean supportsIrisWorldGeneration() { + return true; + } + + @Override + public int getTrueBiomeBaseId(Object biomeBase) { + return getCustomBiomeRegistry().getId(((Holder) biomeBase).value()); + } + + @Override + public Object getTrueBiomeBase(Location location) { + return ((CraftWorld) location.getWorld()).getHandle().getBiome(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ())); + } + + @Override + public String getTrueBiomeBaseKey(Location location) { + return getKeyForBiomeBase(getTrueBiomeBase(location)); + } + + @Override + public Object getCustomBiomeBaseFor(String mckey) { + return getCustomBiomeRegistry().getValue(net.minecraft.resources.Identifier.parse(mckey)); + } + + @Override + public Object getCustomBiomeBaseHolderFor(String mckey) { + return getCustomBiomeRegistry().get(getTrueBiomeBaseId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(mckey)))).orElse(null); + } + + public int getBiomeBaseIdForKey(String key) { + return getCustomBiomeRegistry().getId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(key)).map(Holder::value).orElse(null)); + } + + @Override + public String getKeyForBiomeBase(Object biomeBase) { + net.minecraft.world.level.biome.Biome biome; + if (biomeBase instanceof Holder holder) { + Object value = holder.value(); + if (!(value instanceof net.minecraft.world.level.biome.Biome held)) { + throw new IllegalArgumentException("Iris cannot read a biome key from holder value " + + (value == null ? "null" : value.getClass().getName())); + } + biome = held; + } else if (biomeBase instanceof net.minecraft.world.level.biome.Biome direct) { + biome = direct; + } else { + throw new IllegalArgumentException("Iris cannot read a biome key from " + + (biomeBase == null ? "null" : biomeBase.getClass().getName())); + } + + Identifier key = getCustomBiomeRegistry().getKey(biome); + if (key == null) { + throw new IllegalStateException("Iris found no registry key for biome " + biome); + } + return key.getPath(); // something, not something:something + } + + @Override + public Object getBiomeBase(World world, Biome biome) { + return biomeToBiomeBase(((CraftWorld) world).getHandle() + .registryAccess().lookup(Registries.BIOME).orElse(null), biome); + } + + @Override + public Object getBiomeBase(Object registry, Biome biome) { + Object v = baseBiomeCache.get(biome); + + if (v != null) { + return v; + } + //noinspection unchecked + v = biomeToBiomeBase((Registry) registry, biome); + if (v == null) { + // Ok so there is this new biome name called "CUSTOM" in Paper's new releases. + // But, this does NOT exist within CraftBukkit which makes it return an error. + // So, we will just return the ID that the plains biome returns instead. + //noinspection unchecked + return biomeToBiomeBase((Registry) registry, Biome.PLAINS); + } + baseBiomeCache.put(biome, v); + return v; + } + + @Override + public KList getBiomes() { + KList biomes = new KList<>(); + for (Biome biome : org.bukkit.Registry.BIOME) { + biomes.add(biome); + } + return biomes; + } + + @Override + public KList getStructureKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.STRUCTURE).keySet().forEach(k -> keys.add(k.toString())); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to read registered structure keys from the Minecraft registry", e); + } + return keys; + } + + @Override + public KList getJigsawStructureKeys() { + KList keys = new KList<>(); + try { + Registry structures = registry().lookupOrThrow(Registries.STRUCTURE); + for (Map.Entry, Structure> entry : structures.entrySet()) { + if (entry.getValue() instanceof JigsawStructure) { + keys.add(entry.getKey().identifier().toString()); + } + } + } catch (RuntimeException e) { + throw new IllegalStateException( + "Iris failed to read registered jigsaw structure keys from the Minecraft registry", e); + } + return keys; + } + + @Override + public KList getTemplatePoolKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.TEMPLATE_POOL).keySet() + .forEach(key -> keys.add(key.toString())); + } catch (RuntimeException e) { + throw new IllegalStateException( + "Iris failed to read registered template pool keys from the Minecraft registry", e); + } + return keys; + } + + @Override + public JigsawSourceMetadata getJigsawSourceMetadata(String structureKey) { + try { + Identifier identifier = Identifier.tryParse(structureKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered structure key: " + structureKey); + } + Registry structures = registry().lookupOrThrow(Registries.STRUCTURE); + Structure structure = structures.getValue(identifier); + if (structure == null) { + throw new IllegalArgumentException("Registered structure does not exist: " + structureKey); + } + if (!(structure instanceof JigsawStructure jigsaw)) { + throw new IllegalArgumentException("Registered structure is not a jigsaw: " + structureKey); + } + MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer(); + return NativeStructureFactory.sourceMetadata( + registry(), server.getStructureManager(), jigsaw); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve live jigsaw metadata for registered structure '" + + structureKey + "'", error); + } + } + + @Override + public int getTemplatePoolHorizontalSpan(String templatePoolKey) { + try { + MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer(); + return NativeStructureFactory.templatePoolHorizontalSpan( + registry(), server.getStructureManager(), templatePoolKey); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve the live horizontal span for registered " + + "template pool '" + templatePoolKey + "'", error); + } + } + + @Override + public int getJigsawStartPoolHorizontalSpan(String structureKey, String templatePoolKey) { + try { + Identifier identifier = Identifier.tryParse(structureKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered structure key: " + structureKey); + } + Structure structure = registry().lookupOrThrow(Registries.STRUCTURE).getValue(identifier); + if (!(structure instanceof JigsawStructure jigsaw)) { + throw new IllegalArgumentException("Registered structure is not a jigsaw: " + structureKey); + } + MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer(); + return NativeStructureFactory.jigsawStartPoolHorizontalSpan( + registry(), server.getStructureManager(), jigsaw, templatePoolKey); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve the effective start-pool span for registered " + + "jigsaw structure '" + structureKey + "' and pool '" + templatePoolKey + "'", error); + } + } + + @Override + public KList getStructureSetKeys() { + KList keys = new KList<>(); + try { + registry().lookupOrThrow(Registries.STRUCTURE_SET).keySet().forEach(k -> keys.add(k.toString())); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to read registered structure-set keys from the Minecraft registry", e); + } + return keys; + } + + @Override + public KList getReachableStructureKeys(World world) { + KList keys = new KList<>(); + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource(); + keys.addAll(VanillaStructureBiomes.reachableStructureKeys(level, source)); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to resolve reachable structures for Bukkit world '" + + (world == null ? "" : world.getName()) + "'", e); + } + return keys; + } + + @Override + public KList getStructureBiomeKeys(String structureKey) { + KList keys = new KList<>(); + try { + RegistryAccess access = registry(); + if (access == null) { + throw new IllegalStateException("Minecraft registry access is unavailable"); + } + keys.addAll(VanillaStructureBiomes.structureBiomeKeys(access, structureKey)); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to resolve biome keys for registered structure '" + + structureKey + "'", e); + } + return keys; + } + + @Override + public KList getPossibleBiomeKeys(World world) { + KList keys = new KList<>(); + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource(); + keys.addAll(VanillaStructureBiomes.possibleBiomeKeys(source)); + } catch (RuntimeException e) { + throw new IllegalStateException("Iris failed to resolve possible structure biome keys for Bukkit world '" + + (world == null ? "" : world.getName()) + "'", e); + } + return keys; + } + + @Override + public KList getObjectFeatureKeys() { + KList keys = new KList<>(); + try { + Registry> reg = registry().lookupOrThrow(Registries.CONFIGURED_FEATURE); + for (Identifier id : reg.keySet()) { + ConfiguredFeature cf = reg.getValue(id); + if (cf == null) { + continue; + } + String group = classifyFeature(cf.feature()); + if (group == null) { + continue; + } + keys.add(group + "|" + id); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + return keys; + } + + private static String classifyFeature(Feature feature) { + if (feature instanceof TreeFeature) { + return "trees"; + } + if (feature instanceof FallenTreeFeature) { + return "fallen_trees"; + } + if (feature instanceof AbstractHugeMushroomFeature) { + return "mushrooms"; + } + return null; + } + + @Override + public boolean placeFeature(World world, int x, int y, int z, String featureKey, long seed) { + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + net.minecraft.world.level.chunk.ChunkGenerator generator = level.getChunkSource().getGenerator(); + Registry> reg = registry().lookupOrThrow(Registries.CONFIGURED_FEATURE); + ConfiguredFeature cf = reg.getValue(Identifier.parse(featureKey)); + if (cf == null) { + return false; + } + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed)); + return cf.place(level, generator, random, new BlockPos(x, y, z)); + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public int[] placeStructure(World world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) { + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + net.minecraft.world.level.chunk.ChunkGenerator generator = level.getChunkSource().getGenerator(); + Registry reg = registry().lookupOrThrow(Registries.STRUCTURE); + net.minecraft.world.level.levelgen.structure.Structure structure = reg.getValue(Identifier.parse(structureKey)); + if (structure == null) { + return null; + } + net.minecraft.core.Holder holder = reg.wrapAsHolder(structure); + net.minecraft.world.level.levelgen.RandomState randomState = level.getChunkSource().randomState(); + net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager templateManager = level.getStructureManager(); + net.minecraft.world.level.StructureManager structureManager = level.structureManager(); + net.minecraft.world.level.biome.BiomeSource biomeSource = generator.getBiomeSource(); + net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ); + + net.minecraft.world.level.levelgen.structure.StructureStart start = structure.generate( + holder, + level.dimension(), + level.registryAccess(), + generator, + biomeSource, + randomState, + templateManager, + seed, + chunkPos, + 0, + level, + biome -> true); + + if (start == null || !start.isValid()) { + return null; + } + + BoundingBox box = start.getBoundingBox(); + int spanX = box.maxX() - box.minX() + 1; + int spanY = box.maxY() - box.minY() + 1; + int spanZ = box.maxZ() - box.minZ() + 1; + if (maxSpan > 0 && (spanX > maxSpan || spanZ > maxSpan || spanY > maxSpan)) { + return null; + } + + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed)); + int minCX = box.minX() >> 4; + int maxCX = box.maxX() >> 4; + int minCZ = box.minZ() >> 4; + int maxCZ = box.maxZ() >> 4; + for (int cx = minCX; cx <= maxCX; cx++) { + for (int cz = minCZ; cz <= maxCZ; cz++) { + level.getChunk(cx, cz); + net.minecraft.world.level.ChunkPos cp = new net.minecraft.world.level.ChunkPos(cx, cz); + BoundingBox chunkBox = new BoundingBox( + cp.getMinBlockX(), box.minY(), cp.getMinBlockZ(), + cp.getMaxBlockX(), box.maxY(), cp.getMaxBlockZ()); + start.placeInChunk(level, structureManager, generator, random, chunkBox, cp); + } + } + return new int[]{box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()}; + } catch (RuntimeException e) { + throw NativeStructureGenerationException.failure( + "capture placement", structureKey, chunkX, chunkZ, e); + } + } + + @Override + public boolean supportsStructureCapture() { + return true; + } + + @Override + public KList nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) { + return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ); + } + + @Override + public int getBiomeId(Biome biome) { + for (World i : Bukkit.getWorlds()) { + if (i.getEnvironment().equals(World.Environment.NORMAL)) { + Registry registry = ((CraftWorld) i).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null); + if (registry == null) { + continue; + } + Object baseRaw = getBiomeBase(registry, biome); + net.minecraft.world.level.biome.Biome base; + if (baseRaw instanceof Holder holder) { + Object value = holder.value(); + if (!(value instanceof net.minecraft.world.level.biome.Biome resolved)) { + continue; + } + base = resolved; + } else if (baseRaw instanceof net.minecraft.world.level.biome.Biome resolved) { + base = resolved; + } else { + continue; + } + return registry.getId(base); + } + } + + List biomes = new ArrayList<>(); + for (Biome entry : org.bukkit.Registry.BIOME) { + biomes.add(entry); + } + int index = biomes.indexOf(biome); + return Math.max(index, 0); + } + + private MCAIdMap getBiomeMapping() { + return biomeMapCache.aquire(() -> new MCAIdMap<>() { + @NotNull + @Override + public Iterator iterator() { + return getCustomBiomeRegistry().iterator(); + } + + @Override + public int getId(net.minecraft.world.level.biome.Biome paramT) { + return getCustomBiomeRegistry().getId(paramT); + } + + @Override + public net.minecraft.world.level.biome.Biome byId(int paramInt) { + return (net.minecraft.world.level.biome.Biome) getBiomeBaseFromId(paramInt); + } + }); + } + + @NotNull + private MCABiomeContainer getBiomeContainerInterface(MCAIdMap biomeMapping, MCAChunkBiomeContainer base) { + return new MCABiomeContainer() { + @Override + public int[] getData() { + return base.writeBiomes(); + } + + @Override + public void setBiome(int x, int y, int z, int id) { + base.setBiome(x, y, z, biomeMapping.byId(id)); + } + + @Override + public int getBiome(int x, int y, int z) { + return biomeMapping.getId(base.getBiome(x, y, z)); + } + }; + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max) { + MCAChunkBiomeContainer base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max); + return getBiomeContainerInterface(getBiomeMapping(), base); + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max, int[] data) { + MCAChunkBiomeContainer base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max, data); + return getBiomeContainerInterface(getBiomeMapping(), base); + } + + @Override + public int countCustomBiomes() { + AtomicInteger a = new AtomicInteger(0); + + getCustomBiomeRegistry().keySet().forEach((i) -> { + if (i.getNamespace().equals("minecraft")) { + return; + } + + a.incrementAndGet(); + IrisLogging.debug("Custom Biome: " + i); + }); + + return a.get(); + } + + public boolean supportsDataPacks() { + return true; + } + + public void setBiomes(int cx, int cz, World world, Hunk biomes) { + LevelChunk c = ((CraftWorld) world).getHandle().getChunk(cx, cz); + biomes.iterateSync((x, y, z, b) -> c.setNoiseBiome(x, y, z, (Holder) b)); + c.markUnsaved(); + } + + @Override + public MCAPaletteAccess createPalette() { + MCAIdMapper registry = registryCache.aquireNastyPrint(() -> { + Field cf = IdMapper.class.getDeclaredField("tToId"); + Field df = IdMapper.class.getDeclaredField("idToT"); + Field bf = IdMapper.class.getDeclaredField("nextId"); + cf.setAccessible(true); + df.setAccessible(true); + bf.setAccessible(true); + IdMapper blockData = Block.BLOCK_STATE_REGISTRY; + int b = bf.getInt(blockData); + Object2IntMap c = (Object2IntMap) cf.get(blockData); + List d = (List) df.get(blockData); + return new MCAIdMapper(c, d, b); + }); + if (registry == null) { + throw new IllegalStateException("Iris cannot mirror the Minecraft block state id map on this server version"); + } + MCAPalette global = globalCache.aquireNastyPrint(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState())); + if (global == null) { + throw new IllegalStateException("Iris cannot build the global block state palette on this server version"); + } + java.util.Map innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); + java.util.Map outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); + MCAPalettedContainer container = new MCAPalettedContainer<>(global, registry, + i -> innerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState()), + i -> NBTWorld.getCompound(CraftBlockData.createData(i)), + ((CraftBlockData) AIR).getState()); + return new MCAWrappedPalettedContainer<>(container, + i -> NBTWorld.getCompound(CraftBlockData.createData(i)), + i -> outerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState())); + } + + @Override + public void injectBiomesFromMantle(Chunk e, Mantle mantle) { + ChunkAccess chunk = ((CraftChunk) e).getHandle(ChunkStatus.FULL); + AtomicInteger c = new AtomicInteger(); + AtomicInteger r = new AtomicInteger(); + mantle.iterateChunk(e.getX(), e.getZ(), MatterBiomeInject.class, (x, y, z, b) -> { + if (b != null) { + if (b.isCustom()) { + chunk.setNoiseBiome(x, y, z, getCustomBiomeRegistry().get(b.getBiomeId()).get()); + c.getAndIncrement(); + } else { + chunk.setNoiseBiome(x, y, z, getCustomBiomeRegistry() + .get(net.minecraft.resources.Identifier.parse(b.getBiomeKey())) + .orElseGet(() -> getCustomBiomeRegistry() + .get(net.minecraft.resources.Identifier.parse("minecraft:plains")) + .orElseThrow())); + r.getAndIncrement(); + } + } + }); + } + + @Override + public boolean applyChunkBlocks(Chunk bukkitChunk, TerrainChunk data) { + if (!(data.getChunkData() instanceof CraftChunkData chunkData)) { + return false; + } + + try { + ServerLevel level = ((CraftWorld) bukkitChunk.getWorld()).getHandle(); + LevelChunk chunk = level.getChunk(bukkitChunk.getX(), bukkitChunk.getZ()); + ChunkAccess source = chunkData.getHandle(); + removeBlockEntities(chunk); + + int minY = level.getMinY(); + int baseX = chunk.getPos().getMinBlockX(); + int baseZ = chunk.getPos().getMinBlockZ(); + for (int i = 0; i < chunk.getSectionsCount(); i++) { + LevelChunkSection target = chunk.getSection(i); + LevelChunkSection from = source.getSection(i); + if (from.hasOnlyAir() && target.hasOnlyAir()) { + continue; + } + + int sectionBaseY = minY + (i << 4); + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + BlockState state = from.getBlockState(x, y, z); + target.setBlockState(x, y, z, state, false); + if (state.hasBlockEntity() && state.getBlock() instanceof EntityBlock entityBlock) { + BlockPos pos = new BlockPos(baseX + x, sectionBaseY + y, baseZ + z); + BlockEntity entity = entityBlock.newBlockEntity(pos, state); + if (entity != null) { + chunk.setBlockEntity(entity); + } + } + } + } + } + } + + finishChunkRewrite(level, chunk); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public boolean applyChunkDataBlocks(ChunkGenerator.ChunkData chunkData, Hunk data) { + if (!(chunkData instanceof CraftChunkData craftChunkData)) { + return false; + } + + try { + ChunkAccess access = craftChunkData.getHandle(); + int minY = craftChunkData.getMinHeight(); + int height = craftChunkData.getMaxHeight() - minY; + int accessMinY = access.getMinY(); + int accessMaxY = accessMinY + access.getHeight(); + int yStart = Math.max(0, accessMinY - minY); + int yEnd = Math.min(height, accessMaxY - minY); + int baseX = access.getPos().getMinBlockX(); + int baseZ = access.getPos().getMinBlockZ(); + ChunkDataHunkHolder holder = data instanceof ChunkDataHunkHolder chunkDataHolder ? chunkDataHolder : null; + for (int z = 0; z < 16; z++) { + for (int y = yStart; y < yEnd; y++) { + int blockY = y + minY; + int sectionIndex = (blockY - accessMinY) >> 4; + LevelChunkSection section = access.getSection(sectionIndex); + int sectionY = blockY & 15; + for (int x = 0; x < 16; x++) { + PlatformBlockState platformState = holder == null ? data.getRaw(x, y, z) : holder.getStoredRaw(x, y, z); + if (platformState == null) { + continue; + } + + BlockData blockData = (BlockData) platformState.nativeHandle(); + if (blockData instanceof IrisCustomData customData) { + blockData = customData.getBase(); + } + if (!(blockData instanceof CraftBlockData craftBlockData)) { + return false; + } + + BlockState state = craftBlockData.getState(); + BlockState oldState = section.setBlockState(x, sectionY, z, state, false); + if (state.hasBlockEntity()) { + BlockPos pos = new BlockPos(baseX + x, blockY, baseZ + z); + BlockEntity entity = ((EntityBlock) state.getBlock()).newBlockEntity(pos, state); + if (entity == null) { + access.removeBlockEntity(pos); + } else { + access.setBlockEntity(entity); + } + } else if (oldState != null && oldState.hasBlockEntity()) { + access.removeBlockEntity(new BlockPos(baseX + x, blockY, baseZ + z)); + } + } + } + } + + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public boolean forceEvictChunk(World world, int chunkX, int chunkZ) { + try { + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return true; + } + return world.unloadChunk(chunkX, chunkZ, true); + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public boolean saveAndUnloadChunk(World world, int x, int z) { + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + ChunkHolderManager chm = level.moonrise$getChunkTaskScheduler().chunkHolderManager; + chm.processTicketUpdates(); + NewChunkHolder holder = chm.getChunkHolder(x, z); + if (holder != null) { + holder.save(false); + } + chm.processUnloads(); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public void flushChunkIO(World world) { + try { + ServerLevel level = ((CraftWorld) world).getHandle(); + MoonriseRegionFileIO.flush(level); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + @Override + public boolean clearChunkBlocks(Chunk bukkitChunk) { + try { + ServerLevel level = ((CraftWorld) bukkitChunk.getWorld()).getHandle(); + LevelChunk chunk = level.getChunk(bukkitChunk.getX(), bukkitChunk.getZ()); + removeBlockEntities(chunk); + + BlockState air = ((CraftBlockData) AIR).getState(); + for (int i = 0; i < chunk.getSectionsCount(); i++) { + LevelChunkSection section = chunk.getSection(i); + if (section.hasOnlyAir()) { + continue; + } + + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + section.setBlockState(x, y, z, air, false); + } + } + } + } + + finishChunkRewrite(level, chunk); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + private void removeBlockEntities(LevelChunk chunk) { + for (BlockPos pos : new ArrayList<>(chunk.getBlockEntities().keySet())) { + chunk.removeBlockEntity(pos); + } + } + + private void finishChunkRewrite(ServerLevel level, LevelChunk chunk) { + Heightmap.primeHeightmaps(chunk, ChunkStatus.FULL.heightmapsAfter()); + chunk.markUnsaved(); + ThreadedLevelLightEngine lightEngine = (ThreadedLevelLightEngine) level.getChunkSource().getLightEngine(); + lightEngine.starlight$serverRelightChunks(List.of(chunk.getPos()), p -> { + }, c -> { + }); + } + + public ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException { + if (customNbt != null && !customNbt.isEmpty()) { + net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack); + + try { + net.minecraft.nbt.CompoundTag tag = TagParser.parseCompoundFully((new JSONObject(customNbt)).toString()); + tag.merge(s.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag()); + s.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); + } catch (CommandSyntaxException var5) { + throw new IllegalArgumentException(var5); + } + + return CraftItemStack.asBukkitCopy(s); + } else { + return itemStack; + } + } + + public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException { + ServerLevel level = ((CraftWorld) world).getHandle(); + validateDimensionContract(engine, world, level); + + ChunkMap chunkMap = level.getChunkSource().chunkMap; + Field worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class); + worldGenContextField.setAccessible(true); + WorldGenContext worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap); + + IrisChunkGenerator irisGenerator = new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world); + WorldGenContext newContext = new WorldGenContext( + worldGenContext.level(), irisGenerator, + worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener()); + + worldGenContextField.set(chunkMap, newContext); + net.minecraft.world.level.chunk.ChunkGenerator activeGenerator = level.getChunkSource().getGenerator(); + if (activeGenerator != irisGenerator) { + throw new IllegalStateException("Iris generator injection did not become the active Paper chunk generator; active=" + + activeGenerator.getClass().getName()); + } + retargetStructureCheck(level, irisGenerator); + } + + @Override + public DatapackStructureScopeResult scopeDatapackStructures( + World world, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + IrisImportedStructureControl importedStructures + ) throws NoSuchFieldException, IllegalAccessException { + ServerLevel level = ((CraftWorld) world).getHandle(); + ChunkMap chunkMap = level.getChunkSource().chunkMap; + ChunkGeneratorStructureState currentState = level.getChunkSource().getGeneratorState(); + net.minecraft.world.level.chunk.ChunkGenerator generator = level.getChunkSource().getGenerator(); + ChunkGeneratorStructureState scopedState = createStructureState(level, generator, currentState); + Registry structureSetRegistry = + level.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET); + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + scopedState.possibleStructureSets(), + scopeIndex, + declaredSources, + importedStructures, + DatapackStructureStateFilter.keyIndex( + structureSetRegistry.listElements().toList())); + + Field possibleSetsField = getField(scopedState.getClass(), List.class); + possibleSetsField.setAccessible(true); + possibleSetsField.set(scopedState, selection.structureSets()); + + Field stateField = getField(chunkMap.getClass(), ChunkGeneratorStructureState.class); + stateField.setAccessible(true); + BukkitChunkGenerator platformGenerator = world.getGenerator() instanceof BukkitChunkGenerator bukkitGenerator + ? bukkitGenerator + : null; + boolean studioBootstrap = platformGenerator != null + && platformGenerator.isStudioEntryBootstrapActive(); + boolean jigsawStudio = platformGenerator != null + && platformGenerator.isJigsawStudioActive(); + if (jigsawStudio) { + requireIrisGenerator(generator); + if (currentState.possibleStructureSets().isEmpty()) { + currentState.ensureStructuresGenerated(); + } else { + ChunkGeneratorStructureState bootstrapState = createStructureState(level, generator, currentState); + Field bootstrapSetsField = getField(bootstrapState.getClass(), List.class); + bootstrapSetsField.setAccessible(true); + bootstrapSetsField.set(bootstrapState, List.of()); + bootstrapState.ensureStructuresGenerated(); + stateField.set(chunkMap, bootstrapState); + } + } else if (studioBootstrap) { + IrisChunkGenerator irisGenerator = requireIrisGenerator(generator); + irisGenerator.retainStudioStructureState(level, chunkMap, scopedState); + try { + stateField.set(chunkMap, scopedState); + } catch (IllegalAccessException | RuntimeException | Error failure) { + irisGenerator.abandonStudioStructureState(); + throw failure; + } + } else { + initializeAndPublishStructureState( + generator, + scopedState, + () -> stateField.set(chunkMap, scopedState)); + } + return new DatapackStructureScopeResult( + selection.retainedManagedSets(), + selection.excludedManagedSets()); + } + + @Override + public void completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException { + ServerLevel level = ((CraftWorld) world).getHandle(); + ChunkMap chunkMap = level.getChunkSource().chunkMap; + IrisChunkGenerator generator = requireIrisGenerator(level.getChunkSource().getGenerator()); + IrisChunkGenerator.StudioStructureState retained = + generator.retainedStudioStructureState(level, chunkMap); + if (retained == null) { + return; + } + generator.activateStudioStructureState(retained); + } + + @Override + public void abandonStudioStructureBootstrap(World world) { + ServerLevel level = ((CraftWorld) world).getHandle(); + net.minecraft.world.level.chunk.ChunkGenerator generator = level.getChunkSource().getGenerator(); + if (generator instanceof IrisChunkGenerator irisGenerator) { + irisGenerator.abandonStudioStructureState(); + } + } + + private ChunkGeneratorStructureState createStructureState( + ServerLevel level, + net.minecraft.world.level.chunk.ChunkGenerator generator, + ChunkGeneratorStructureState currentState + ) { + return generator.createState( + level.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET), + currentState.randomState(), + currentState.getLevelSeed(), + level.spigotConfig); + } + + private void initializeAndPublishStructureState( + net.minecraft.world.level.chunk.ChunkGenerator generator, + ChunkGeneratorStructureState structureState, + IrisChunkGenerator.StructureStatePublisher publisher + ) throws IllegalAccessException { + if (generator instanceof IrisChunkGenerator irisGenerator) { + irisGenerator.initializeAndPublishStructureState(structureState, publisher); + return; + } + structureState.ensureStructuresGenerated(); + publisher.publish(); + } + + private IrisChunkGenerator requireIrisGenerator( + net.minecraft.world.level.chunk.ChunkGenerator generator + ) { + if (generator instanceof IrisChunkGenerator irisGenerator) { + return irisGenerator; + } + throw new IllegalStateException("Studio native structure state is not owned by the active Iris generator."); + } + + private void validateDimensionContract(Engine engine, World world, ServerLevel level) { + DimensionType actualType = level.dimensionType(); + String actualTypeKey = level.dimensionTypeRegistration().unwrapKey() + .map(key -> key.identifier().toString()) + .orElse(""); + IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected(engine.getDimension(), "iris"); + IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract( + actualTypeKey, + actualType.minY(), + actualType.height(), + actualType.logicalHeight()); + String runtimeName = "Bukkit world '" + world.getName() + "'"; + expected.requireExact(runtimeName, actual); + expected.requireHeight(runtimeName, level.getMinY(), level.getHeight()); + expected.requireHeight(runtimeName, world.getMinHeight(), world.getMaxHeight() - world.getMinHeight()); + IrisLogging.debug("Loaded world " + world.getName() + " with exact Iris dimension type " + actualTypeKey); + } + + private static void retargetStructureCheck(ServerLevel level, IrisChunkGenerator generator) throws NoSuchFieldException, IllegalAccessException { + Field structureCheckField = getField(level.getClass(), StructureCheck.class); + structureCheckField.setAccessible(true); + Object structureCheck = structureCheckField.get(level); + if (structureCheck == null) { + return; + } + Field generatorField = getField(structureCheck.getClass(), net.minecraft.world.level.chunk.ChunkGenerator.class); + generatorField.setAccessible(true); + generatorField.set(structureCheck, generator); + Field biomeSourceField = getField(structureCheck.getClass(), BiomeSource.class); + biomeSourceField.setAccessible(true); + biomeSourceField.set(structureCheck, generator.getBiomeSource()); + } + + public Vector3d getBoundingbox(org.bukkit.entity.EntityType entity) { + if (entity == null) { + return null; + } + + try { + // Registry lookup instead of an EntityType static-field scan: 26.2 moved the constants + // to a separate EntityTypes holder class that 26.1.2 does not have, while the registry + // resolves identically on both. ENTITY_TYPE is a DefaultedRegistry, so guard containsKey + // to avoid silently resolving unknown keys to the default entry. + Identifier key = Identifier.fromNamespaceAndPath(entity.getKey().getNamespace(), entity.getKey().getKey()); + if (!BuiltInRegistries.ENTITY_TYPE.containsKey(key)) { + return null; + } + EntityType entityType = BuiltInRegistries.ENTITY_TYPE.getValue(key); + if (entityType == null) { + return null; + } + return new Vector3d(entityType.getWidth(), entityType.getHeight(), entityType.getWidth()); + } catch (Throwable e) { + IrisLogging.error("Unable to get entity dimensions for " + entity + "!"); + IrisLogging.reportError(e); + return null; + } + } + + + @Override + public Entity spawnEntity(Location location, org.bukkit.entity.EntityType type, CreatureSpawnEvent.SpawnReason reason) { + if (location == null || location.getWorld() == null || type == null || type.getEntityClass() == null) { + return null; + } + return ((CraftWorld) location.getWorld()).spawn(location, type.getEntityClass(), null, reason); + } + + @Override + public Color getBiomeColor(Location location, BiomeColor type) { + ServerLevel reader = ((CraftWorld) location.getWorld()).getHandle(); + var pos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + var holder = reader.getBiome(pos); + var biome = holder.value(); + if (biome == null) throw new IllegalArgumentException("Invalid biome: " + holder.unwrapKey().orElse(null)); + + var attributes = reader.environmentAttributes(); + int rgba = switch (type) { + case FOG -> attributes.getValue(EnvironmentAttributes.FOG_COLOR, pos); + case WATER -> biome.getWaterColor(); + case WATER_FOG -> attributes.getValue(EnvironmentAttributes.WATER_FOG_COLOR, pos); + case SKY -> attributes.getValue(EnvironmentAttributes.SKY_COLOR, pos); + case FOLIAGE -> biome.getFoliageColor(); + case GRASS -> biome.getGrassColor(location.getBlockX(), location.getBlockZ()); + }; + if (rgba == 0) { + if (BiomeColor.FOLIAGE == type && biome.getSpecialEffects().foliageColorOverride().isEmpty()) + return null; + if (BiomeColor.GRASS == type && biome.getSpecialEffects().grassColorOverride().isEmpty()) + return null; + } + return new Color(rgba, true); + } + + private static Field getField(Class clazz, Class fieldType) throws NoSuchFieldException { + try { + for (Field f : clazz.getDeclaredFields()) { + if (f.getType().equals(fieldType)) + return f; + } + throw new NoSuchFieldException(fieldType.getName()); + } catch (NoSuchFieldException var4) { + Class superClass = clazz.getSuperclass(); + if (superClass == null) { + throw var4; + } else { + return getField(superClass, fieldType); + } + } + } + + public static Holder biomeToBiomeBase(Registry registry, Biome biome) { + if (registry == null || biome == null) { + return null; + } + + NamespacedKey biomeKey = resolveBiomeKey(biome); + if (biomeKey == null) { + return null; + } + + ResourceKey key = ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biomeKey)); + return registry.get(key).orElse(null); + } + + private static NamespacedKey resolveBiomeKey(Biome biome) { + Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull", new Class[0]); + if (keyOrNullValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow", new Class[0]); + if (keyOrThrowValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + Object keyValue = invokeNoThrow(biome, "getKey", new Class[0]); + if (keyValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + return null; + } + + private static Object invokeNoThrow(Object target, String methodName, Class[] parameterTypes, Object... args) { + if (target == null) { + return null; + } + + try { + Method method = target.getClass().getMethod(methodName, parameterTypes); + return method.invoke(target, args); + } catch (Throwable ignored) { + return null; + } + } + + @Override + public DataVersion getDataVersion() { + DataVersion cached = dataVersion; + if (cached == null) { + MinecraftVersion detected = MinecraftVersion.detect(Bukkit.getServer()); + cached = detected != null && detected.isSameRelease(26, 1, 2) ? DataVersion.V26_1_2 : DataVersion.V26_2; + dataVersion = cached; + } + return cached; + } + + @Override + public int getSpawnChunkCount(World world) { + return 0; + } + + @Override + public boolean missingDimensionTypes(String... keys) { + var type = registry().lookupOrThrow(Registries.DIMENSION_TYPE); + return !Arrays.stream(keys) + .map(key -> Identifier.fromNamespaceAndPath("iris", key)) + .allMatch(type::containsKey); + } + + @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.error(C.RED + "Failed to inject Bukkit"); + e.printStackTrace(); + 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; + } + } + } + + @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"); + } + } + + @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.error(C.RED + "Failed to remove Bukkit world lifecycle injection"); + e.printStackTrace(); + } + } + } + } + + @Override + public void writeCurrentPaperWorldData( + Path sourceWorldDirectory, + Path targetWorldDirectory, + long seed + ) throws IOException { + CurrentPaperWorldDataWriter.write(sourceWorldDirectory, targetWorldDirectory, seed); + } + + @Override + public boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) { + MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer(); + return ServerShutdownBoundary.await( + () -> server.hasFullyShutdown, + server.getRunningThread(), + timeout, + unit + ); + } + + @Override + public KMap> getBlockProperties() { + KMap> states = new KMap<>(); + + for (var block : registry().lookupOrThrow(Registries.BLOCK)) { + var state = block.defaultBlockState(); + if (state == null) state = block.getStateDefinition().any(); + final var finalState = state; + + states.put(CraftMagicNumbers.getMaterial(block), block.getStateDefinition() + .getProperties() + .stream() + .map(p -> createProperty(p, finalState)) + .toList()); + } + return states; + } + + private > BlockProperty createProperty(Property property, BlockState state) { + return new BlockProperty(property.getName(), property.getValueClass(), state.getValue(property), property.getPossibleValues(), property::getName); + } + + @Override + public Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) { + if (!(registryAccess instanceof RegistryAccess access)) { + throw new IllegalStateException("Runtime LevelStem creation requires a RegistryAccess instance."); + } + if (!(raw instanceof PlatformChunkGenerator generator)) { + throw new IllegalStateException("Generator is not platform chunk generator!"); + } + + Identifier dimensionKey = Identifier.fromNamespaceAndPath("iris", generator.getTarget().getDimension().getDimensionTypeKey()); + Holder.Reference dimensionType = access.lookupOrThrow(Registries.DIMENSION_TYPE) + .getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, dimensionKey)); + return new LevelStem(dimensionType, chunkGenerator(access)); + } + + private net.minecraft.world.level.chunk.ChunkGenerator chunkGenerator(RegistryAccess access) { + var settings = new FlatLevelGeneratorSettings(Optional.empty(), access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID), List.of()); + settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR)); + 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 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 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); + } + } + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccess.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccess.java new file mode 100644 index 000000000..cc4ff499f --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccess.java @@ -0,0 +1,581 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps; +import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.QuartPos; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.attribute.EnvironmentAttributeReader; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.EmptyLevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.LevelTickAccess; +import net.minecraft.world.ticks.ScheduledTick; +import org.bukkit.event.entity.CreatureSpawnEvent; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.IntBinaryOperator; +import java.util.function.Predicate; +import java.util.function.Supplier; + +final class NativeStructureWorldgenAccess implements WorldGenLevel { + private static final int WRITE_RADIUS = 1; + + private final WorldGenLevel delegate; + private final ChunkPos generationCenter; + private final IntBinaryOperator surfaceFirstFreeY; + private final IntBinaryOperator floorFirstFreeY; + private final Holder fallbackBiome; + private final BiomeManager biomeManager; + private final AABB generationBounds; + private final LevelTickAccess blockTicks; + private final LevelTickAccess fluidTicks; + private final Long2LongOpenHashMap terrainHeights; + private final Long2ObjectOpenHashMap outsideChunks; + + private NativeStructureWorldgenAccess(WorldGenLevel delegate, Boundary boundary) { + this.delegate = Objects.requireNonNull(delegate, "Native structure world access requires a delegate"); + this.generationCenter = Objects.requireNonNull( + boundary.generationCenter(), "Native structure world access requires a generation center"); + this.surfaceFirstFreeY = Objects.requireNonNull( + boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver"); + this.floorFirstFreeY = Objects.requireNonNull( + boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver"); + BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel()); + this.fallbackBiome = delegate.getBiome(biomeSample); + this.biomeManager = delegate.getBiomeManager().withDifferentSource(this); + this.generationBounds = new AABB( + generationCenter.getMinBlockX() - 16, + delegate.getMinY(), + generationCenter.getMinBlockZ() - 16, + generationCenter.getMaxBlockX() + 17, + delegate.getMinY() + delegate.getHeight(), + generationCenter.getMaxBlockZ() + 17); + this.blockTicks = new BoundedTickAccess<>(delegate.getBlockTicks(), this::isWritable); + this.fluidTicks = new BoundedTickAccess<>(delegate.getFluidTicks(), this::isWritable); + this.terrainHeights = new Long2LongOpenHashMap(); + this.terrainHeights.defaultReturnValue(Long.MIN_VALUE); + this.outsideChunks = new Long2ObjectOpenHashMap<>(); + } + + static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter, + IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { + return new NativeStructureWorldgenAccess(delegate, new Boundary( + generationCenter, surfaceFirstFreeY, floorFirstFreeY)); + } + + @Override + public long getSeed() { + return delegate.getSeed(); + } + + @Override + public boolean ensureCanWrite(BlockPos position) { + return isWritable(position) && delegate.ensureCanWrite(position); + } + + @Override + public void setCurrentlyGenerating(Supplier description) { + delegate.setCurrentlyGenerating(description); + } + + @Override + public ServerLevel getLevel() { + return delegate.getLevel(); + } + + @Override + public DifficultyInstance getCurrentDifficultyAt(BlockPos position) { + if (isReadable(position)) { + return delegate.getCurrentDifficultyAt(position); + } + return delegate.getCurrentDifficultyAt(generationCenter.getMiddleBlockPosition( + Mth.clamp(position.getY(), getMinY(), getMaxY() - 1))); + } + + @Override + public long nextSubTickCount() { + return delegate.nextSubTickCount(); + } + + @Override + public LevelTickAccess getBlockTicks() { + return blockTicks; + } + + @Override + public LevelTickAccess getFluidTicks() { + return fluidTicks; + } + + @Override + public LevelData getLevelData() { + return delegate.getLevelData(); + } + + @Override + public MinecraftServer getServer() { + return delegate.getServer(); + } + + @Override + public ChunkSource getChunkSource() { + return delegate.getChunkSource(); + } + + @Override + public RandomSource getRandom() { + return delegate.getRandom(); + } + + @Override + public void updateNeighborsAt(BlockPos position, Block block) { + if (isWritableNeighbourhood(position)) { + delegate.updateNeighborsAt(position, block); + } + } + + @Override + public void neighborShapeChanged(Direction direction, BlockPos position, + BlockPos neighbourPosition, BlockState neighbourState, + int updateFlags, int updateLimit) { + if (isWritable(position) && isWritable(neighbourPosition)) { + delegate.neighborShapeChanged( + direction, position, neighbourPosition, neighbourState, updateFlags, updateLimit); + } + } + + @Override + public void playSound(Entity source, BlockPos position, SoundEvent sound, + SoundSource soundSource, float volume, float pitch) { + if (isWritable(position)) { + delegate.playSound(source, position, sound, soundSource, volume, pitch); + } + } + + @Override + public void addParticle(ParticleOptions particle, double x, double y, double z, + double velocityX, double velocityY, double velocityZ) { + if (isWritable(BlockPos.containing(x, y, z))) { + delegate.addParticle(particle, x, y, z, velocityX, velocityY, velocityZ); + } + } + + @Override + public void levelEvent(Entity source, int eventId, BlockPos position, int data) { + if (isWritable(position)) { + delegate.levelEvent(source, eventId, position, data); + } + } + + @Override + public void gameEvent(Holder event, Vec3 position, GameEvent.Context context) { + if (isWritable(BlockPos.containing(position))) { + delegate.gameEvent(event, position, context); + } + } + + @Override + public ChunkAccess getChunk(int chunkX, int chunkZ) { + if (isInsideGenerationRegion(chunkX, chunkZ)) { + return delegate.getChunk(chunkX, chunkZ); + } + return outsideChunk(chunkX, chunkZ); + } + + @Override + public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) { + if (isInsideGenerationRegion(chunkX, chunkZ)) { + return delegate.getChunk(chunkX, chunkZ, status, create); + } + return create ? outsideChunk(chunkX, chunkZ) : null; + } + + @Override + public ChunkAccess getChunkIfLoadedImmediately(int chunkX, int chunkZ) { + if (!isInsideGenerationRegion(chunkX, chunkZ)) { + return null; + } + return delegate.getChunkIfLoadedImmediately(chunkX, chunkZ); + } + + @Override + public boolean hasChunk(int chunkX, int chunkZ) { + return isInsideGenerationRegion(chunkX, chunkZ) && delegate.hasChunk(chunkX, chunkZ); + } + + @Override + public int getHeight(Heightmap.Types type, int x, int z) { + if (isInsideGenerationRegion(x >> 4, z >> 4)) { + return delegate.getHeight(type, x, z); + } + return height(type, x, z); + } + + @Override + public int getSkyDarken() { + return delegate.getSkyDarken(); + } + + @Override + public BiomeManager getBiomeManager() { + return biomeManager; + } + + @Override + public Holder getNoiseBiome(int quartX, int quartY, int quartZ) { + return getUncachedNoiseBiome(quartX, quartY, quartZ); + } + + @Override + public Holder getUncachedNoiseBiome(int quartX, int quartY, int quartZ) { + if (isInsideGenerationRegion(QuartPos.toSection(quartX), QuartPos.toSection(quartZ))) { + return delegate.getUncachedNoiseBiome(quartX, quartY, quartZ); + } + return fallbackBiome; + } + + @Override + public boolean isClientSide() { + return delegate.isClientSide(); + } + + @Override + public int getSeaLevel() { + return delegate.getSeaLevel(); + } + + @Override + public DimensionType dimensionType() { + return delegate.dimensionType(); + } + + @Override + public int getMinY() { + return delegate.getMinY(); + } + + @Override + public int getHeight() { + return delegate.getHeight(); + } + + @Override + public RegistryAccess registryAccess() { + return delegate.registryAccess(); + } + + @Override + public FeatureFlagSet enabledFeatures() { + return delegate.enabledFeatures(); + } + + @Override + public EnvironmentAttributeReader environmentAttributes() { + return delegate.environmentAttributes(); + } + + @Override + public LevelLightEngine getLightEngine() { + return delegate.getLightEngine(); + } + + @Override + public int getBrightness(LightLayer layer, BlockPos position) { + if (isReadable(position)) { + return delegate.getBrightness(layer, position); + } + return layer == LightLayer.SKY && canSeeSky(position) ? 15 : 0; + } + + @Override + public int getRawBrightness(BlockPos position, int ambientDarkening) { + if (isReadable(position)) { + return delegate.getRawBrightness(position, ambientDarkening); + } + return Math.max(0, getBrightness(LightLayer.SKY, position) - ambientDarkening); + } + + @Override + public boolean canSeeSky(BlockPos position) { + if (isReadable(position)) { + return delegate.canSeeSky(position); + } + return position.getY() >= height(Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ()); + } + + @Override + public WorldBorder getWorldBorder() { + return delegate.getWorldBorder(); + } + + @Override + public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) { + if (isInsideGenerationRegion(chunkX, chunkZ)) { + return delegate.getChunkForCollisions(chunkX, chunkZ); + } + return outsideChunk(chunkX, chunkZ); + } + + @Override + public BlockEntity getBlockEntity(BlockPos position) { + return isReadable(position) ? delegate.getBlockEntity(position) : null; + } + + @Override + public Optional getBlockEntity( + BlockPos position, BlockEntityType type) { + return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty(); + } + + @Override + public BlockState getBlockState(BlockPos position) { + return isReadable(position) ? delegate.getBlockState(position) : terrainState(position); + } + + @Override + public BlockState getBlockStateIfLoaded(BlockPos position) { + return isReadable(position) ? delegate.getBlockStateIfLoaded(position) : null; + } + + @Override + public FluidState getFluidState(BlockPos position) { + return isReadable(position) ? delegate.getFluidState(position) : terrainState(position).getFluidState(); + } + + @Override + public FluidState getFluidIfLoaded(BlockPos position) { + return isReadable(position) ? delegate.getFluidIfLoaded(position) : null; + } + + @Override + public List getEntities(Entity source, AABB area, Predicate predicate) { + AABB boundedArea = boundedArea(area); + return boundedArea == null ? List.of() : delegate.getEntities(source, boundedArea, predicate); + } + + @Override + public List getEntities( + EntityTypeTest type, + AABB area, Predicate predicate) { + AABB boundedArea = boundedArea(area); + return boundedArea == null ? List.of() : delegate.getEntities(type, boundedArea, predicate); + } + + @Override + public List players() { + return delegate.players(); + } + + @Override + public boolean isStateAtPosition(BlockPos position, Predicate predicate) { + return predicate.test(getBlockState(position)); + } + + @Override + public boolean isFluidAtPosition(BlockPos position, Predicate predicate) { + return predicate.test(getFluidState(position)); + } + + @Override + public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) { + return position.atY(getHeight(type, position.getX(), position.getZ())); + } + + @Override + public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) { + return isWritable(position) && delegate.setBlock(position, state, updateFlags, updateLimit); + } + + @Override + public boolean removeBlock(BlockPos position, boolean move) { + return isWritable(position) && 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); + } + + @Override + public boolean addFreshEntity(Entity entity) { + return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity); + } + + @Override + public boolean addFreshEntity(Entity entity, CreatureSpawnEvent.SpawnReason reason) { + return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity, reason); + } + + private boolean isReadable(BlockPos position) { + return isInsideGenerationRegion(position.getX() >> 4, position.getZ() >> 4) + && !isOutsideBuildHeight(position); + } + + private boolean isWritable(BlockPos position) { + return isReadable(position); + } + + private boolean isWritableNeighbourhood(BlockPos position) { + return isWritable(position) + && isWritable(position.north()) + && isWritable(position.south()) + && isWritable(position.east()) + && isWritable(position.west()) + && isWritable(position.above()) + && isWritable(position.below()); + } + + private AABB boundedArea(AABB area) { + double minX = Math.max(area.minX, generationBounds.minX); + double minY = Math.max(area.minY, generationBounds.minY); + double minZ = Math.max(area.minZ, generationBounds.minZ); + double maxX = Math.min(area.maxX, generationBounds.maxX); + double maxY = Math.min(area.maxY, generationBounds.maxY); + double maxZ = Math.min(area.maxZ, generationBounds.maxZ); + if (minX >= maxX || minY >= maxY || minZ >= maxZ) { + return null; + } + if (minX == area.minX && minY == area.minY && minZ == area.minZ + && maxX == area.maxX && maxY == area.maxY && maxZ == area.maxZ) { + return area; + } + return new AABB(minX, minY, minZ, maxX, maxY, maxZ); + } + + boolean isInsideGenerationRegion(int chunkX, int chunkZ) { + return Math.abs(chunkX - generationCenter.x()) <= WRITE_RADIUS + && Math.abs(chunkZ - generationCenter.z()) <= WRITE_RADIUS; + } + + @Override + public boolean isOutsideBuildHeight(BlockPos position) { + return position.getY() < getMinY() || position.getY() >= getMaxY(); + } + + private BlockState terrainState(BlockPos position) { + if (isOutsideBuildHeight(position)) { + return Blocks.VOID_AIR.defaultBlockState(); + } + long heights = heights(position.getX(), position.getZ()); + int surface = (int) (heights >> 32); + int floor = (int) heights; + if (position.getY() < floor) { + return Blocks.STONE.defaultBlockState(); + } + if (position.getY() < surface) { + return Blocks.WATER.defaultBlockState(); + } + return Blocks.AIR.defaultBlockState(); + } + + private int height(Heightmap.Types type, int x, int z) { + long heights = heights(x, z); + if (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG) { + return (int) heights; + } + return (int) (heights >> 32); + } + + private long heights(int x, int z) { + long key = ((long) x << 32) ^ (z & 0xffffffffL); + long cached = terrainHeights.get(key); + if (cached != Long.MIN_VALUE) { + return cached; + } + int floor = Mth.clamp(floorFirstFreeY.applyAsInt(x, z), getMinY(), getMaxY()); + int surface = Mth.clamp(surfaceFirstFreeY.applyAsInt(x, z), floor, getMaxY()); + long heights = ((long) surface << 32) | (floor & 0xffffffffL); + terrainHeights.put(key, heights); + return heights; + } + + private ChunkAccess outsideChunk(int chunkX, int chunkZ) { + long key = ChunkPos.pack(chunkX, chunkZ); + ChunkAccess cached = outsideChunks.get(key); + if (cached != null) { + return cached; + } + EmptyLevelChunk chunk = new EmptyLevelChunk(getLevel(), new ChunkPos(chunkX, chunkZ), fallbackBiome); + WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY, floorFirstFreeY); + outsideChunks.put(key, chunk); + return chunk; + } + + private record Boundary(ChunkPos generationCenter, + IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { + } + + private static final class BoundedTickAccess implements LevelTickAccess { + private final LevelTickAccess delegate; + private final Predicate writable; + + private BoundedTickAccess(LevelTickAccess delegate, Predicate writable) { + this.delegate = delegate; + this.writable = writable; + } + + @Override + public void schedule(ScheduledTick tick) { + if (writable.test(tick.pos())) { + delegate.schedule(tick); + } + } + + @Override + public boolean hasScheduledTick(BlockPos position, T type) { + return writable.test(position) && delegate.hasScheduledTick(position, type); + } + + @Override + public int count() { + return delegate.count(); + } + + @Override + public boolean willTickThisTick(BlockPos position, T type) { + return writable.test(position) && delegate.willTickThisTick(position, type); + } + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomes.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomes.java new file mode 100644 index 000000000..a9f94b0df --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomes.java @@ -0,0 +1,80 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import art.arcane.iris.nativegen.NativeStructureGenerationKeys; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.levelgen.structure.Structure; + +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +final class VanillaStructureBiomes { + private VanillaStructureBiomes() { + } + + static Set possibleBiomeKeys(BiomeSource source) { + if (source == null) { + throw new IllegalStateException("Minecraft chunk generator has no biome source"); + } + Set keys = new LinkedHashSet<>(); + Set> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource + ? customBiomeSource.possibleStructureBiomes() + : source.possibleBiomes(); + for (Holder holder : possibleBiomes) { + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + if (keys.isEmpty()) { + throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes"); + } + return keys; + } + + static Set structureBiomeKeys(RegistryAccess access, String structureKey) { + if (access == null) { + throw new IllegalStateException("Minecraft registry access is unavailable"); + } + if (structureKey == null || structureKey.isBlank()) { + throw new IllegalArgumentException("Registered structure key must not be blank"); + } + Set keys = new LinkedHashSet<>(); + Registry registry = access.lookupOrThrow(Registries.STRUCTURE); + Structure structure = registry.getValue(Identifier.parse(structureKey)); + if (structure == null) { + throw new IllegalArgumentException("Registered structure does not exist: " + structureKey); + } + boolean hasFilterEntries = false; + for (Holder holder : structure.biomes()) { + hasFilterEntries = true; + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + // An empty biome filter is legal datapack content (opt-in structures whose tags only + // reference absent modded biomes); the structure is unreachable, not an error state. + if (keys.isEmpty() && hasFilterEntries) { + throw new IllegalStateException("Registered structure '" + structureKey + + "' has biome filter entries but none resolve to registered biome keys"); + } + return keys; + } + + static Set reachableStructureKeys(ServerLevel level, BiomeSource source) { + if (level == null) { + throw new IllegalStateException("Minecraft server level is unavailable"); + } + Set possible = possibleBiomeKeys(source); + return NativeStructureGenerationKeys.reachable(level, possible); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java new file mode 100644 index 000000000..76fb17d62 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java @@ -0,0 +1,52 @@ +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)")); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java new file mode 100644 index 000000000..d1365b6c5 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java @@ -0,0 +1,491 @@ +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> findNearestMapStructure"); + int locateEnd = source.indexOf("private HolderSet filterReachableStructures", locateStart); + String locate = source.substring(locateStart, locateEnd); + int reachabilityStart = source.indexOf("private Set 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 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 initializeStructureState("); + int initializeEnd = source.indexOf("private Map structureRingPositions", initializeStart); + String initialize = source.substring(initializeStart, initializeEnd); + int completionStart = source.indexOf( + "private CompletableFuture 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 activateStudioStructureState("); + int activationEnd = source.indexOf( + "private CompletableFuture startStructureStateBootstrap", activationStart); + String activation = source.substring(activationStart, activationEnd); + int bootstrapStart = activationEnd; + int bootstrapEnd = source.indexOf( + "private CompletableFuture 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 fillFromNoise"); + int fillEnd = source.indexOf("public WeightedList 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 fillFromNoiseLeaseSpansTheDelegateAndHeightmapPipeline() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n"); + int fillStart = source.indexOf("public CompletableFuture fillFromNoise"); + int fillEnd = source.indexOf("private static boolean isCancellationFailure", fillStart); + String fill = source.substring(fillStart, fillEnd); + int completionStart = fill.indexOf("pipeline.whenComplete("); + int completionEnd = fill.indexOf(" return completion;", completionStart); + assertTrue(completionStart >= 0); + assertTrue(completionEnd > completionStart); + String completion = fill.substring(completionStart, completionEnd); + + assertBefore(fill, + "BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(", + "GenerationSessionLease lease"); + assertBefore(fill, + "requireNoiseGenerationStage(", + "\"bukkit_nms_chunk_pipeline\")"); + assertBefore(fill, + "lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")", + ".fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)"); + assertTrue(fill.contains("IrisContext.open(engine, lease.sessionId(), null)")); + assertBefore(fill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete("); + assertTrue(fill.contains("CompletableFuture 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(fill.contains("catch (RuntimeException | Error failure)")); + assertTrue(fill.contains("lease.close();\n stage.close();\n throw failure;")); + assertTrue(fill.contains("return completion;")); + assertFalse(fill.contains("return pipeline;")); + assertFalse(fill.contains("pipeline.cancel(")); + assertFalse(fill.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 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 delegate = new CompletableFuture<>(); + CompletableFuture 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 fillFromNoise", + "private static boolean isCancellationFailure"); + String admission = method( + source, + "private BukkitChunkGenerator.GenerationStagePermit requireNoiseGenerationStage", + "public Optional 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 createBiomes"); + String biomes = method(source, "public CompletableFuture createBiomes", "public void buildSurface"); + String noise = method(source, "public CompletableFuture 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")); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java new file mode 100644 index 000000000..3b4b2f4af --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java @@ -0,0 +1,190 @@ +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> findNearestIrisStructure(", findStart); + int filterStart = source.indexOf("private HolderSet 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 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> irisNear = Pair.of(new BlockPos(4, 70, 0), null); + Pair> nativeFar = Pair.of(new BlockPos(8, 70, 0), null); + Pair> nativeNear = Pair.of(new BlockPos(2, 70, 0), null); + Pair> 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> irisNear = Pair.of(new BlockPos(4, 70, 0), null); + Pair> nativeFar = Pair.of(new BlockPos(8, 70, 0), null); + AtomicInteger irisReferences = new AtomicInteger(); + AtomicInteger nativeReferences = new AtomicInteger(); + + Pair> irisSelected = + NativeStructureLocateResults.selectAndReference( + origin, + irisNear, () -> irisReferences.incrementAndGet(), + nativeFar, () -> nativeReferences.incrementAndGet()); + + assertSame(irisNear, irisSelected); + assertEquals(1, irisReferences.get()); + assertEquals(0, nativeReferences.get()); + + Pair> nativeNear = Pair.of(new BlockPos(2, 70, 0), null); + Pair> 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 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")); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingCurrentPaperWorldDataContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingCurrentPaperWorldDataContractTest.java new file mode 100644 index 000000000..f86afcf2f --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingCurrentPaperWorldDataContractTest.java @@ -0,0 +1,132 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import art.arcane.iris.core.nms.INMSBinding; +import org.junit.Test; + +import java.io.InputStream; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class NMSBindingCurrentPaperWorldDataContractTest { + @Test + public void bindingDelegatesWithoutLinkingPaperSavedDataClasses() throws Exception { + String bindingSource = Files.readString(bindingSourcePath()).replace("\r\n", "\n"); + String writer = section( + bindingSource, + "public void writeCurrentPaperWorldData(", + "public boolean awaitServerShutdownBoundary(" + ); + + assertTrue(writer.contains("CurrentPaperWorldDataWriter.write(")); + assertFalse(bindingSource.contains("PaperWorldMetadata")); + assertFalse(bindingSource.contains("PaperLevelOverrides")); + assertFalse(bindingSource.contains("io.papermc.paper.world.saveddata")); + + InputStream classResource = NMSBindingCurrentPaperWorldDataContractTest.class + .getResourceAsStream("NMSBinding.class"); + assertNotNull(classResource); + try (InputStream input = classResource) { + String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1); + assertFalse(classFile.contains("PaperWorldMetadata")); + assertFalse(classFile.contains("PaperLevelOverrides")); + assertFalse(classFile.contains("io/papermc/paper/world/saveddata")); + } + } + + @Test + public void stagesAllCurrentPaperWorldDataFromLiveServerState() throws Exception { + String writer = Files.readString(writerSourcePath()).replace("\r\n", "\n"); + + assertTrue(writer.contains("WorldReplacementSeed.copyWithAuthoritativeSeed(")); + assertTrue(writer.contains("UUID metadataUuid = UUID.randomUUID()")); + assertTrue(writer.contains("new PaperWorldMetadata(metadataUuid)")); + assertTrue(writer.contains("captureLevelOverrides(craftServer, server)")); + assertTrue(writer.indexOf("captureLevelOverrides(craftServer, server)") + < writer.indexOf("WorldReplacementSeed.copyWithAuthoritativeSeed(")); + assertTrue(writer.contains("new SavedDataStorage(")); + assertTrue(writer.contains("server.getFixerUpper()")); + assertTrue(writer.contains("server.registryAccess()")); + assertTrue(writer.contains("data/minecraft/world_gen_settings.dat")); + assertTrue(writer.contains("data/paper/metadata.dat")); + assertTrue(writer.contains("data/paper/level_overrides.dat")); + assertTrue(writer.contains("WorldReplacementSeed.readAuthoritativeSeed(targetWorld)")); + assertTrue(writer.contains("verificationStorage.get(PaperWorldMetadata.TYPE)")); + assertTrue(writer.contains("metadataUuid.equals(metadata.uuid())")); + assertTrue(writer.contains("verificationStorage.get(PaperLevelOverrides.TYPE)")); + assertTrue(writer.contains("overrides == null || overrides.isInitialized()")); + assertTrue(writer.contains("Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)")); + assertFalse(writer.toLowerCase().contains("migrat")); + assertFalse(writer.toLowerCase().contains("fallback")); + } + + @Test + public void capturesOnlyLiveLevelOverridesOnTheGlobalThread() throws Exception { + String source = Files.readString(writerSourcePath()).replace("\r\n", "\n"); + String capture = section( + source, + "private static PaperLevelOverrides captureLevelOverrides(", + "private static PaperLevelOverrides createLevelOverrides(" + ); + String create = section( + source, + "private static PaperLevelOverrides createLevelOverrides(", + "\n }\n}" + ); + + assertTrue(capture.contains("craftServer.isGlobalTickThread()")); + assertTrue(capture.contains("J.isFolia() && J.isPrimaryThread()")); + assertTrue(capture.contains("J.runGlobal(")); + assertTrue(capture.contains("createLevelOverrides(craftServer, server)")); + assertTrue(capture.contains("captured.get(SNAPSHOT_TIMEOUT_SECONDS")); + assertTrue(capture.contains("Thread.currentThread().interrupt()")); + assertTrue(create.contains("if (!craftServer.isGlobalTickThread())")); + assertTrue(create.indexOf("if (!craftServer.isGlobalTickThread())") + < create.indexOf("server.getWorldData().overworldData()")); + assertTrue(create.contains("PaperLevelOverrides.createFromLiveLevelData(primaryLevelData)")); + assertFalse(capture.contains("WorldReplacementSeed")); + assertFalse(capture.contains("SavedDataStorage")); + assertFalse(capture.contains("Files.")); + assertFalse(create.contains("WorldReplacementSeed")); + assertFalse(create.contains("SavedDataStorage")); + assertFalse(create.contains("Files.")); + } + + @Test + public void unsupportedBindingsRejectCurrentPaperWorldDataStaging() { + INMSBinding binding = (INMSBinding) Proxy.newProxyInstance( + INMSBinding.class.getClassLoader(), + new Class[]{INMSBinding.class}, + (proxy, method, arguments) -> InvocationHandler.invokeDefault(proxy, method, arguments) + ); + + UnsupportedOperationException error = assertThrows( + UnsupportedOperationException.class, + () -> binding.writeCurrentPaperWorldData(Path.of("source"), Path.of("target"), 1L) + ); + assertTrue(error.getMessage().contains("does not support current Paper world data staging")); + } + + private static Path bindingSourcePath() { + return Path.of(System.getProperty("iris.nmsBindingSource")); + } + + private static Path writerSourcePath() { + return bindingSourcePath().resolveSibling("CurrentPaperWorldDataWriter.java"); + } + + 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); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingDatapackStructureScopeTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingDatapackStructureScopeTest.java new file mode 100644 index 000000000..3dac36b41 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingDatapackStructureScopeTest.java @@ -0,0 +1,605 @@ +package art.arcane.iris.core.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.volmlib.util.collection.KList; +import com.mojang.datafixers.util.Either; +import net.minecraft.SharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderOwner; +import net.minecraft.core.Vec3i; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Bootstrap; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import org.junit.Test; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class NMSBindingDatapackStructureScopeTest { + private static final String SOURCE = "https://example.test/managed.zip"; + + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void managedSetContainingVanillaStructureIsAbsentOutsideDeclaringDimension() { + Holder vanillaStructure = structureHolder("minecraft:pillager_outpost"); + Holder managedSet = structureSetHolder( + "managed:illager_barracks", vanillaStructure); + DatapackStructureScopeIndex index = index( + List.of(), + List.of("managed:illager_barracks")); + + DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter( + List.of(managedSet), index, Set.of()); + DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter( + List.of(managedSet), index, index.declaredSources(List.of(SOURCE))); + + assertEquals(0, vanilla.structureSets().size()); + assertEquals(1, vanilla.excludedManagedSets()); + assertEquals(1, declaring.structureSets().size()); + assertSame(managedSet, declaring.structureSets().getFirst()); + } + + @Test + public void unmanagedSetRetainsOnlyDefinitionsAllowedInTheWorld() { + Holder vanillaStructure = structureHolder("minecraft:village_plains"); + Holder managedStructure = structureHolder("managed:tavern"); + Holder vanillaSet = structureSetHolder( + "minecraft:villages", vanillaStructure, managedStructure); + DatapackStructureScopeIndex index = index( + List.of("managed:tavern"), + List.of()); + + DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter( + List.of(vanillaSet), index, Set.of()); + DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter( + List.of(vanillaSet), index, index.declaredSources(List.of(SOURCE))); + + assertEquals(1, vanilla.structureSets().size()); + assertEquals(1, vanilla.structureSets().getFirst().value().structures().size()); + assertSame(vanillaStructure, + vanilla.structureSets().getFirst().value().structures().getFirst().structure()); + assertSame(vanillaSet.value().placement(), + vanilla.structureSets().getFirst().value().placement()); + assertSame(vanillaSet, declaring.structureSets().getFirst()); + } + + @Test + public void setWithNoAllowedDefinitionsIsRemoved() { + Holder unmanagedSet = structureSetHolder( + "minecraft:custom", structureHolder("managed:only")); + DatapackStructureScopeIndex index = index(List.of("managed:only"), List.of()); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(unmanagedSet), index, Set.of()); + + assertEquals(0, selection.structureSets().size()); + } + + @Test + public void spigotDirectHolderRecoversItsRegistryKeyFromSharedEntries() { + ResourceKey key = ResourceKey.create( + Registries.STRUCTURE_SET, + Identifier.parse("managed:illager_barracks")); + List entries = List.of( + new StructureSet.StructureSelectionEntry( + structureHolder("minecraft:pillager_outpost"), 1)); + Holder registered = new KeyedHolder<>(key, new StructureSet( + entries, + new RandomSpreadStructurePlacement( + 34, 8, RandomSpreadType.LINEAR, 10387312))); + Holder spigotDirect = Holder.direct(new StructureSet( + entries, + new RandomSpreadStructurePlacement( + 34, 8, RandomSpreadType.LINEAR, 14357620))); + DatapackStructureScopeIndex scopeIndex = index( + List.of(), List.of("managed:illager_barracks")); + DatapackStructureStateFilter.StructureSetKeyIndex keyIndex = + DatapackStructureStateFilter.keyIndex(List.of(registered)); + + DatapackStructureStateFilter.Selection excluded = + DatapackStructureStateFilter.filter( + List.of(spigotDirect), + scopeIndex, + Set.of(), + new IrisImportedStructureControl(), + keyIndex); + DatapackStructureStateFilter.Selection retained = + DatapackStructureStateFilter.filter( + List.of(spigotDirect), + scopeIndex, + scopeIndex.declaredSources(List.of(SOURCE)), + new IrisImportedStructureControl(), + keyIndex); + + assertEquals(0, excluded.structureSets().size()); + assertEquals(1, excluded.excludedManagedSets()); + assertSame(spigotDirect, retained.structureSets().getFirst()); + assertEquals(1, retained.retainedManagedSets()); + } + + @Test + public void structureScopeDoesNotLinkPaperOnlyPlacementClasses() throws IOException { + InputStream classResource = NMSBindingDatapackStructureScopeTest.class + .getResourceAsStream("DatapackStructureStateFilter.class"); + assertNotNull(classResource); + try (InputStream input = classResource) { + String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1); + assertFalse(classFile.contains("KeyedRandomSpreadStructurePlacement")); + } + } + + @Test + public void exactFrequencyOverridesScaleOnlyTheNamedNativeStructureSet() { + Holder complexes = structureSetHolder( + "minecraft:nether_complexes", + new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232), + structureHolder("minecraft:fortress")); + Holder fossils = structureSetHolder( + "minecraft:nether_fossils", + new RandomSpreadStructurePlacement(2, 1, RandomSpreadType.LINEAR, 14357921), + structureHolder("minecraft:nether_fossil")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("minecraft:nether_complexes") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(complexes, fossils), index(List.of(), List.of()), Set.of(), control); + + RandomSpreadStructurePlacement scaledComplexes = + (RandomSpreadStructurePlacement) selection.structureSets().get(0).value().placement(); + RandomSpreadStructurePlacement unchangedFossils = + (RandomSpreadStructurePlacement) selection.structureSets().get(1).value().placement(); + assertEquals(26, scaledComplexes.spacing()); + assertEquals(4, scaledComplexes.separation()); + assertEquals(2, unchangedFossils.spacing()); + assertSame(fossils, selection.structureSets().get(1)); + assertEquals(27, ((RandomSpreadStructurePlacement) complexes.value().placement()).spacing()); + } + + @Test + public void frequencyOnlyPassLeavesUnrelatedCustomPlacementUntouched() { + Holder exclusionTarget = structureSetHolder( + "example:target", structureHolder("example:target")); + Holder custom = structureSetHolder( + "example:custom", + new UnsupportedPlacement(Optional.of( + new StructurePlacement.ExclusionZone(exclusionTarget, 1))), + structureHolder("example:custom")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("minecraft:nether_complexes") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(custom), index(List.of(), List.of()), Set.of(), control); + + assertSame(custom, selection.structureSets().getFirst()); + } + + @Test + public void frequencyOnlyPassRebindsDependentExclusionTarget() { + Holder target = structureSetHolder( + "example:target", + new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232), + structureHolder("example:target")); + RandomSpreadStructurePlacement dependentPlacement = new RandomSpreadStructurePlacement( + Vec3i.ZERO, + StructurePlacement.FrequencyReductionMethod.DEFAULT, + 1.0F, + 4567, + Optional.of(new StructurePlacement.ExclusionZone(target, 1)), + 32, + 8, + RandomSpreadType.LINEAR); + Holder dependent = structureSetHolder( + "example:dependent", + dependentPlacement, + structureHolder("example:dependent")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("example:target") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(dependent, target), index(List.of(), List.of()), Set.of(), control); + + Holder scaledDependent = selection.structureSets().get(0); + Holder scaledTarget = selection.structureSets().get(1); + assertNotSame(dependent, scaledDependent); + assertNotSame(target, scaledTarget); + assertSame(scaledTarget, DatapackStructureStateFilter.exclusionZone( + scaledDependent.value().placement()).orElseThrow().otherSet()); + assertEquals(26, ((RandomSpreadStructurePlacement) + scaledTarget.value().placement()).spacing()); + } + + @Test + public void frequencyOnlyPassRebindsAffectedExclusionCycle() { + MutableStructureSetHolder first = new MutableStructureSetHolder("example:first"); + MutableStructureSetHolder second = new MutableStructureSetHolder("example:second"); + first.bind(structureSet( + new RandomSpreadStructurePlacement( + Vec3i.ZERO, + StructurePlacement.FrequencyReductionMethod.DEFAULT, + 1.0F, + 101, + Optional.of(new StructurePlacement.ExclusionZone(second, 1)), + 32, + 8, + RandomSpreadType.LINEAR), + "example:first")); + second.bind(structureSet( + new RandomSpreadStructurePlacement( + Vec3i.ZERO, + StructurePlacement.FrequencyReductionMethod.DEFAULT, + 1.0F, + 102, + Optional.of(new StructurePlacement.ExclusionZone(first, 1)), + 27, + 4, + RandomSpreadType.LINEAR), + "example:second")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("example:second") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(first, second), index(List.of(), List.of()), Set.of(), control); + + Holder scaledFirst = selection.structureSets().get(0); + Holder scaledSecond = selection.structureSets().get(1); + assertSame(scaledSecond, DatapackStructureStateFilter.exclusionZone( + scaledFirst.value().placement()).orElseThrow().otherSet()); + assertSame(scaledFirst, DatapackStructureStateFilter.exclusionZone( + scaledSecond.value().placement()).orElseThrow().otherSet()); + assertEquals(26, ((RandomSpreadStructurePlacement) + scaledSecond.value().placement()).spacing()); + } + + @Test + public void randomSpreadSubtypeUsesTheCanonicalPlacementContract() { + Holder custom = structureSetHolder( + "example:custom", + new CustomRandomSpreadPlacement(), + structureHolder("example:custom")); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("example:custom") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(custom), index(List.of(), List.of()), Set.of(), control); + StructurePlacement placement = selection.structureSets().getFirst().value().placement(); + + assertEquals(RandomSpreadStructurePlacement.class, placement.getClass()); + assertEquals(26, ((RandomSpreadStructurePlacement) placement).spacing()); + } + + @Test + public void excludedManagedSetCannotSuppressAnAllowedSetThroughExclusionZone() { + Holder managedSet = structureSetHolder( + "managed:blocked", + structureHolder("managed:blocked")); + RandomSpreadStructurePlacement originalPlacement = new RandomSpreadStructurePlacement( + Vec3i.ZERO, + StructurePlacement.FrequencyReductionMethod.DEFAULT, + 1.0F, + 4567, + Optional.of(new StructurePlacement.ExclusionZone(managedSet, 1)), + 32, + 8, + RandomSpreadType.LINEAR); + Holder vanillaSet = structureSetHolder( + "minecraft:allowed", + originalPlacement, + structureHolder("minecraft:village_plains")); + DatapackStructureScopeIndex index = index( + List.of("managed:blocked"), + List.of("managed:blocked")); + + DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter( + List.of(vanillaSet, managedSet), index, Set.of()); + + assertEquals(1, selection.structureSets().size()); + StructurePlacement scopedPlacement = selection.structureSets().getFirst().value().placement(); + assertEquals(0, DatapackStructureStateFilter.exclusionZone(scopedPlacement).stream().count()); + } + + @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 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 standardCompletionActivatesTheAlreadyPublishedStateWithoutReplacingIt() 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 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 activation = method.indexOf("generator.activateStudioStructureState(retained);"); + + assertTrue(retained >= 0); + 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 structureKeys, + List structureSetKeys + ) { + return DatapackStructureScopeIndex.create(List.of( + new DatapackIngestService.StructureScopeResources( + SOURCE, + structureKeys, + structureSetKeys))); + } + + private static Holder structureHolder(String key) { + return new KeyedHolder<>(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(key)), null); + } + + private static Holder structureSetHolder( + String key, + Holder... structures + ) { + return structureSetHolder( + key, + new RandomSpreadStructurePlacement(32, 8, RandomSpreadType.LINEAR, 12345), + structures); + } + + private static Holder structureSetHolder( + String key, + StructurePlacement placement, + Holder... structures + ) { + List entries = Stream.of(structures) + .map(structure -> new StructureSet.StructureSelectionEntry(structure, 1)) + .toList(); + StructureSet value = new StructureSet(entries, placement); + return new KeyedHolder<>( + ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), + value); + } + + private static StructureSet structureSet(StructurePlacement placement, String structureKey) { + return new StructureSet( + List.of(new StructureSet.StructureSelectionEntry( + structureHolder(structureKey), 1)), + placement); + } + + private static final class KeyedHolder implements Holder { + private final ResourceKey key; + private final T value; + + private KeyedHolder(ResourceKey key, T value) { + this.key = key; + this.value = value; + } + + @Override + public T value() { + return value; + } + + @Override + public boolean isBound() { + return true; + } + + @Override + public boolean areComponentsBound() { + return true; + } + + @Override + public boolean is(Identifier identifier) { + return key.identifier().equals(identifier); + } + + @Override + public boolean is(ResourceKey candidate) { + return key.equals(candidate); + } + + @Override + public boolean is(Predicate> predicate) { + return predicate.test(key); + } + + @Override + public boolean is(TagKey tag) { + return false; + } + + @Override + public boolean is(Holder holder) { + return holder == this; + } + + @Override + public Stream> tags() { + return Stream.empty(); + } + + @Override + public DataComponentMap components() { + return DataComponentMap.EMPTY; + } + + @Override + public Either, T> unwrap() { + return Either.left(key); + } + + @Override + public Optional> unwrapKey() { + return Optional.of(key); + } + + @Override + public Kind kind() { + return Kind.REFERENCE; + } + + @Override + public boolean canSerializeIn(HolderOwner owner) { + return true; + } + } + + private static final class UnsupportedPlacement extends StructurePlacement { + private UnsupportedPlacement(Optional exclusionZone) { + super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, exclusionZone); + } + + @Override + protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) { + return false; + } + + @Override + public StructurePlacementType type() { + return null; + } + } + + private static final class MutableStructureSetHolder extends Holder.Reference { + private MutableStructureSetHolder(String key) { + super(Type.STAND_ALONE, new HolderOwner<>() { + }, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), null); + } + + private void bind(StructureSet structureSet) { + bindValue(structureSet); + } + } + + private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement { + private CustomRandomSpreadPlacement() { + super(27, 4, RandomSpreadType.LINEAR, 30084232); + } + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingPlacementFailureContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingPlacementFailureContractTest.java new file mode 100644 index 000000000..0a536d13a --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingPlacementFailureContractTest.java @@ -0,0 +1,29 @@ +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")); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingServerLevelAdviceContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingServerLevelAdviceContractTest.java new file mode 100644 index 000000000..fd1f4e4b8 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingServerLevelAdviceContractTest.java @@ -0,0 +1,103 @@ +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 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 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); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingShutdownBoundaryContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingShutdownBoundaryContractTest.java new file mode 100644 index 000000000..7af5cd242 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingShutdownBoundaryContractTest.java @@ -0,0 +1,28 @@ +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> 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); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingStructureFailureContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingStructureFailureContractTest.java new file mode 100644 index 000000000..a1e62be35 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingStructureFailureContractTest.java @@ -0,0 +1,43 @@ +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 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 " + 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); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingTileDeserializeContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingTileDeserializeContractTest.java new file mode 100644 index 000000000..e54707044 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NMSBindingTileDeserializeContractTest.java @@ -0,0 +1,30 @@ +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); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java new file mode 100644 index 000000000..8a9cc68c4 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java @@ -0,0 +1,323 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.LevelTickAccess; +import net.minecraft.world.ticks.ScheduledTick; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +import 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.assertTrue; + +public class NativeStructureWorldgenAccessTest { + private static final int GENERATION_CENTER_X = 60; + private static final int GENERATION_CENTER_Z = 15; + private static final int SURFACE_FIRST_FREE_Y = 80; + private static final int FLOOR_FIRST_FREE_Y = 70; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void distanceTwoTerrainReadsNeverReachTheDelegate() { + RecordingDelegate recording = new RecordingDelegate(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + int x = generationCenter.getMiddleBlockX(); + int z = (generationCenter.z() + 2) << 4; + + assertSame(Blocks.STONE, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y - 1, z)).getBlock()); + assertSame(Blocks.WATER, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)).getBlock()); + assertSame(Blocks.AIR, access.getBlockState(new BlockPos(x, SURFACE_FIRST_FREE_Y, z)).getBlock()); + assertSame(Blocks.WATER.defaultBlockState().getFluidState(), + access.getFluidState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z))); + assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)); + assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)); + assertNull(access.getBlockStateIfLoaded(new BlockPos(x, FLOOR_FIRST_FREE_Y, z))); + assertNull(access.getChunk(generationCenter.x(), generationCenter.z() + 2, ChunkStatus.FEATURES, false)); + assertNull(access.getChunkIfLoadedImmediately( + generationCenter.x(), generationCenter.z() + 2)); + List streamed = access.getBlockStates(new AABB( + x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList(); + + assertEquals(1, streamed.size()); + assertSame(Blocks.WATER, streamed.getFirst().getBlock()); + assertEquals(0, recording.terrainReads); + assertEquals(0, recording.heightReads); + assertEquals(0, recording.chunkReads); + } + + @Test + public void distanceTwoMutationsAndSideEffectsNeverReachTheDelegate() { + RecordingDelegate recording = new RecordingDelegate(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + BlockPos position = new BlockPos( + generationCenter.getMiddleBlockX(), + FLOOR_FIRST_FREE_Y, + (generationCenter.z() + 2) << 4); + + assertFalse(access.ensureCanWrite(position)); + assertFalse(access.setBlock(position, Blocks.DIRT.defaultBlockState(), 2)); + assertFalse(access.removeBlock(position, false)); + assertFalse(access.destroyBlock(position, false)); + access.updateNeighborsAt(position, Blocks.DIRT); + access.neighborShapeChanged( + Direction.NORTH, position, position.north(), + Blocks.DIRT.defaultBlockState(), 2, 512); + access.levelEvent(null, 2001, position, 0); + access.gameEvent(null, Vec3.atCenterOf(position), null); + access.addParticle(null, position.getX(), position.getY(), position.getZ(), 0, 0, 0); + access.getBlockTicks().schedule(new ScheduledTick<>( + Blocks.DIRT, position, 1L, 0L)); + + assertEquals(0, recording.mutations); + assertEquals(0, recording.events); + assertEquals(0, recording.blockTicks.count()); + } + + @Test + public void radiusOneReadsWritesAndTicksRemainUnchanged() { + RecordingDelegate recording = new RecordingDelegate(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + BlockPos position = new BlockPos( + (generationCenter.x() + 1) << 4, + FLOOR_FIRST_FREE_Y, + (generationCenter.z() - 1) << 4); + + assertTrue(access.isInsideGenerationRegion( + generationCenter.x() + 1, generationCenter.z() - 1)); + assertSame(Blocks.DIRT, access.getBlockState(position).getBlock()); + assertEquals(91, access.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ())); + assertTrue(access.ensureCanWrite(position)); + assertTrue(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2)); + assertTrue(access.removeBlock(position, false)); + assertTrue(access.destroyBlock(position, false)); + access.getBlockTicks().schedule(new ScheduledTick<>( + Blocks.DIRT, position, 1L, 0L)); + + assertEquals(1, recording.terrainReads); + assertEquals(1, recording.heightReads); + assertEquals(4, recording.mutations); + assertEquals(1, recording.blockTicks.count()); + } + + @Test + public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() { + RecordingDelegate recording = new RecordingDelegate(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + + assertNull(access.getChunk(generationCenter.getWorldPosition())); + + assertEquals(1, recording.statuslessChunkReads); + assertEquals(0, recording.statusChunkReads); + } + + @Test + public void entityQueriesRejectDisjointAreasAndClampOverlaps() { + RecordingDelegate recording = new RecordingDelegate(); + NativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + int safeMaxX = generationCenter.getMaxBlockX() + 17; + int safeMinZ = generationCenter.getMinBlockZ() - 16; + int safeMaxZ = generationCenter.getMaxBlockZ() + 17; + AABB disjoint = new AABB( + safeMaxX, -64, safeMinZ, + safeMaxX + 16, 320, safeMaxZ); + AABB overlap = new AABB( + safeMaxX - 8, -128, safeMinZ - 8, + safeMaxX + 16, 400, safeMaxZ + 8); + EntityTypeTest type = EntityTypeTest.forClass(Entity.class); + + assertTrue(access.getEntities((Entity) null, disjoint, entity -> true).isEmpty()); + assertTrue(access.getEntities(type, disjoint, entity -> true).isEmpty()); + access.getEntities((Entity) null, overlap, entity -> true); + access.getEntities(type, overlap, entity -> true); + + assertEquals(2, recording.entityAreas.size()); + for (AABB delegatedArea : recording.entityAreas) { + assertEquals(safeMaxX - 8, delegatedArea.minX, 0D); + assertEquals(-64, delegatedArea.minY, 0D); + assertEquals(safeMinZ, delegatedArea.minZ, 0D); + assertEquals(safeMaxX, delegatedArea.maxX, 0D); + assertEquals(320, delegatedArea.maxY, 0D); + assertEquals(safeMaxZ, delegatedArea.maxZ, 0D); + } + } + + private static NativeStructureWorldgenAccess access(RecordingDelegate recording) { + return NativeStructureWorldgenAccess.create( + recording.world(), generationCenter(), + (x, z) -> SURFACE_FIRST_FREE_Y, + (x, z) -> FLOOR_FIRST_FREE_Y); + } + + private static ChunkPos generationCenter() { + return new ChunkPos(GENERATION_CENTER_X, GENERATION_CENTER_Z); + } + + private static final class RecordingDelegate implements InvocationHandler { + private final Holder biome; + private final BiomeManager biomeManager; + private final RecordingTicks blockTicks; + private final RecordingTicks fluidTicks; + private final List entityAreas; + private int terrainReads; + private int heightReads; + private int chunkReads; + private int statuslessChunkReads; + private int statusChunkReads; + private int mutations; + private int events; + + private RecordingDelegate() { + this.biome = Holder.direct((Biome) null); + this.biomeManager = new BiomeManager((x, y, z) -> biome, 13L); + this.blockTicks = new RecordingTicks<>(); + this.fluidTicks = new RecordingTicks<>(); + this.entityAreas = new ArrayList<>(); + } + + private WorldGenLevel world() { + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), + new Class[]{WorldGenLevel.class}, this); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] arguments) { + String name = method.getName(); + if (name.equals("getSeaLevel")) { + return 63; + } + if (name.equals("getBiome")) { + return biome; + } + if (name.equals("getBiomeManager")) { + return biomeManager; + } + if (name.equals("getBlockTicks")) { + return blockTicks; + } + if (name.equals("getFluidTicks")) { + return fluidTicks; + } + if (name.equals("getMinY")) { + return -64; + } + if (name.equals("getHeight") && arguments == null) { + return 384; + } + if (name.equals("getHeight")) { + heightReads++; + return 91; + } + if (name.equals("getBlockState")) { + terrainReads++; + return Blocks.DIRT.defaultBlockState(); + } + if (name.equals("getFluidState")) { + terrainReads++; + return Blocks.WATER.defaultBlockState().getFluidState(); + } + if (name.equals("getChunk")) { + chunkReads++; + if (arguments.length == 2) { + statuslessChunkReads++; + } else { + statusChunkReads++; + } + return null; + } + if (name.equals("getChunkIfLoadedImmediately")) { + chunkReads++; + return null; + } + if (name.equals("getEntities")) { + entityAreas.add((AABB) arguments[1]); + return List.of(); + } + if (name.equals("ensureCanWrite") || name.equals("setBlock") + || name.equals("removeBlock") || name.equals("destroyBlock")) { + mutations++; + return true; + } + if (name.equals("updateNeighborsAt") || name.equals("neighborShapeChanged")) { + mutations++; + return null; + } + if (name.equals("levelEvent") || name.equals("gameEvent") || name.equals("addParticle")) { + events++; + return null; + } + if (name.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (name.equals("equals")) { + return proxy == arguments[0]; + } + if (name.equals("toString")) { + return "native-structure-worldgen-test-delegate"; + } + throw new UnsupportedOperationException(method.toString()); + } + } + + private static final class RecordingTicks implements LevelTickAccess { + private int count; + + @Override + public void schedule(ScheduledTick tick) { + count++; + } + + @Override + public boolean hasScheduledTick(BlockPos position, T type) { + return false; + } + + @Override + public int count() { + return count; + } + + @Override + public boolean willTickThisTick(BlockPos position, T type) { + return false; + } + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomesEmptyFilterContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomesEmptyFilterContractTest.java new file mode 100644 index 000000000..9112fff88 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomesEmptyFilterContractTest.java @@ -0,0 +1,38 @@ +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")); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmapsTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmapsTest.java new file mode 100644 index 000000000..e7219736c --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmapsTest.java @@ -0,0 +1,328 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps; +import com.mojang.serialization.Codec; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.IdMapper; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerFactory; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.Strategy; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.IglooPieces; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.IntBinaryOperator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class WorldgenTerrainHeightmapsTest { + private static final int MIN_Y = -64; + private static final int HEIGHT = 384; + private static final int LAND_TERRAIN_TOP = 70; + private static final int LAND_CANOPY_TOP = 80; + private static final int OCEAN_FLOOR_TOP = 60; + private static final int OCEAN_FLUID_TOP = 64; + private static final int LAND_X = 3; + private static final int LAND_Z = 5; + private static final int OCEAN_X = 8; + private static final int OCEAN_Z = 8; + private static final int TERRAIN_SLAB_DEPTH = 4; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() { + ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); + + assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG)); + + WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY()); + + assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG)); + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z)); + assertEquals(OCEAN_FLUID_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z)); + assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight( + Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z)); + } + + @Test + public void finalHeightmapPrimingDoesNotClobberTheWorldgenHeightmaps() { + ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); + + WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY()); + Heightmap.primeHeightmaps(chunk, ChunkStatus.FINAL_HEIGHTMAPS); + + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z)); + assertEquals(LAND_CANOPY_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE, LAND_X, LAND_Z)); + } + + @Test + public void unprimedWorldgenHeightmapResolvesTheIrisCanopyInstead() { + ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); + + assertEquals(LAND_CANOPY_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + } + + @Test + public void primeTerrainOverwritesAnAlreadyLazyPrimedCanopyHeight() { + ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); + + assertEquals(LAND_CANOPY_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY()); + + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + } + + @Test + public void structurePlacementPrimesTheWestNeighbourAnIglooEntranceReads() { + Map chunks = new HashMap<>(); + chunks.put(ChunkPos.pack(0, 0), terrainChunk(new ChunkPos(0, 0))); + chunks.put(ChunkPos.pack(-1, 0), terrainChunk(new ChunkPos(-1, 0))); + WorldGenLevel world = world(chunks); + StructureStart start = startInOrigin(); + + int canopySnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world); + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, new ChunkPos(0, 0), List.of(start), surfaceFirstFreeY(), floorFirstFreeY()); + int terrainSnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world); + + assertEquals(LAND_TERRAIN_TOP + 1, world.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, -2, 5)); + assertEquals(LAND_TERRAIN_TOP, terrainSnap); + assertEquals(LAND_CANOPY_TOP, canopySnap); + assertEquals(LAND_CANOPY_TOP - LAND_TERRAIN_TOP, canopySnap - terrainSnap); + } + + @Test + public void structurePlacementPrimesEveryChunkTheStartFootprintTouches() { + Map chunks = new HashMap<>(); + for (int chunkX = -1; chunkX <= 1; chunkX++) { + for (int chunkZ = -1; chunkZ <= 1; chunkZ++) { + chunks.put(ChunkPos.pack(chunkX, chunkZ), terrainChunk(new ChunkPos(chunkX, chunkZ))); + } + } + WorldGenLevel world = world(chunks); + + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, new ChunkPos(0, 0), List.of(startInOrigin()), + surfaceFirstFreeY(), floorFirstFreeY()); + + for (ChunkAccess chunk : chunks.values()) { + assertTrue(chunk.getPos().toString(), + chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertTrue(chunk.getPos().toString(), + chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG)); + assertEquals(LAND_TERRAIN_TOP, chunk.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + } + } + + @Test + public void structurePlacementDoesNotPrimeALoadedDistanceTwoChunk() { + Map chunks = new HashMap<>(); + ProtoChunk origin = terrainChunk(new ChunkPos(0, 0)); + ProtoChunk neighbour = terrainChunk(new ChunkPos(1, 0)); + ProtoChunk distanceTwo = terrainChunk(new ChunkPos(2, 0)); + chunks.put(ChunkPos.pack(0, 0), origin); + chunks.put(ChunkPos.pack(1, 0), neighbour); + chunks.put(ChunkPos.pack(2, 0), distanceTwo); + WorldGenLevel world = world(chunks); + StructureStart shifted = startInOrigin(); + shifted.getPieces().forEach(piece -> piece.move(16, 0, 0)); + + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, new ChunkPos(0, 0), List.of(shifted), + surfaceFirstFreeY(), floorFirstFreeY()); + + assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertTrue(neighbour.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertFalse(distanceTwo.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + } + + @Test + public void structurePlacementSkipsChunksOutsideTheGenerationRegion() { + Map chunks = new HashMap<>(); + ProtoChunk origin = terrainChunk(new ChunkPos(0, 0)); + chunks.put(ChunkPos.pack(0, 0), origin); + WorldGenLevel world = world(chunks); + + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, new ChunkPos(0, 0), List.of(startInOrigin()), + surfaceFirstFreeY(), floorFirstFreeY()); + + assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG)); + assertEquals(LAND_TERRAIN_TOP, origin.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z)); + } + + private static int iglooSnapOffset(WorldGenLevel world) { + return world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, -2, 5) + - IglooPieces.GENERATION_HEIGHT - 1; + } + + private static StructureStart startInOrigin() { + Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); + SwampHutPiece piece = new SwampHutPiece(RandomSource.create(13L), 0, 0); + return new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + } + + private static IntBinaryOperator surfaceFirstFreeY() { + return (x, z) -> isOcean(x, z) ? OCEAN_FLUID_TOP + 1 : LAND_TERRAIN_TOP + 1; + } + + private static IntBinaryOperator floorFirstFreeY() { + return (x, z) -> isOcean(x, z) ? OCEAN_FLOOR_TOP + 1 : LAND_TERRAIN_TOP + 1; + } + + private static boolean isOcean(int x, int z) { + return Math.floorMod(x, 16) == OCEAN_X && Math.floorMod(z, 16) == OCEAN_Z; + } + + private static ProtoChunk terrainChunk(ChunkPos pos) { + ProtoChunk chunk = new ProtoChunk( + pos, UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT), + containerFactory(), null); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + boolean ocean = isOcean(pos.getMinBlockX() + x, pos.getMinBlockZ() + z); + int stoneTop = ocean ? OCEAN_FLOOR_TOP : LAND_TERRAIN_TOP; + for (int y = stoneTop - TERRAIN_SLAB_DEPTH; y <= stoneTop; y++) { + write(chunk, x, y, z, Blocks.STONE.defaultBlockState()); + } + if (ocean) { + for (int y = OCEAN_FLOOR_TOP + 1; y <= OCEAN_FLUID_TOP; y++) { + write(chunk, x, y, z, Blocks.WATER.defaultBlockState()); + } + continue; + } + for (int y = LAND_TERRAIN_TOP + 1; y <= LAND_TERRAIN_TOP + 6; y++) { + write(chunk, x, y, z, Blocks.SPRUCE_LOG.defaultBlockState()); + } + for (int y = LAND_TERRAIN_TOP + 7; y <= LAND_CANOPY_TOP; y++) { + write(chunk, x, y, z, Blocks.SPRUCE_LEAVES.defaultBlockState()); + } + } + } + return chunk; + } + + private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) { + LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y)); + section.setBlockState(x, y & 15, z, state, false); + } + + private static PalettedContainerFactory containerFactory() { + Strategy blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY); + BlockState air = Blocks.AIR.defaultBlockState(); + IdMapper> biomeIds = new IdMapper<>(); + Holder defaultBiome = Holder.direct((Biome) null); + biomeIds.add(defaultBiome); + Strategy> biomes = Strategy.createForBiomes(biomeIds); + Codec> biomeCodec = Codec.STRING.xmap( + name -> defaultBiome, holder -> "iris-test-biome"); + return new PalettedContainerFactory( + blockStates, + air, + PalettedContainer.codecRW(BlockState.CODEC, blockStates, air), + biomes, + defaultBiome, + PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome), + PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome)); + } + + private static WorldGenLevel world(Map chunks) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("hasChunk")) { + return chunks.containsKey(ChunkPos.pack((int) arguments[0], (int) arguments[1])); + } + if (methodName.equals("getChunk") && arguments.length == 2) { + ChunkAccess chunk = chunks.get(ChunkPos.pack((int) arguments[0], (int) arguments[1])); + if (chunk == null) { + throw new AssertionError("Heightmap priming requested an unavailable chunk " + + arguments[0] + "," + arguments[1]); + } + return chunk; + } + if (methodName.equals("getHeight") && arguments.length == 3) { + Heightmap.Types type = (Heightmap.Types) arguments[0]; + int x = (int) arguments[1]; + int z = (int) arguments[2]; + ChunkAccess chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4)); + if (chunk == null) { + throw new AssertionError("Heightmap read requested an unavailable chunk at " + + x + "," + z); + } + return chunk.getHeight(type, x & 15, z & 15) + 1; + } + if (methodName.equals("getBlockState")) { + BlockPos position = (BlockPos) arguments[0]; + ChunkAccess chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4)); + if (chunk == null) { + throw new AssertionError("Block read requested an unavailable chunk at " + position); + } + return chunk.getBlockState(position); + } + if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (methodName.equals("equals")) { + return proxy == arguments[0]; + } + if (methodName.equals("toString")) { + return "worldgen-heightmap-test-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), new Class[]{WorldGenLevel.class}, handler); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/structure/VillageImporterListPoolElementRuntimeContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/structure/VillageImporterListPoolElementRuntimeContractTest.java new file mode 100644 index 000000000..3c472a150 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/structure/VillageImporterListPoolElementRuntimeContractTest.java @@ -0,0 +1,294 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.core.structure.authoring.StructureCapability; +import art.arcane.iris.core.structure.authoring.StructureLoss; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class VillageImporterListPoolElementRuntimeContractTest { + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void pillagerOutpostTowerCompositeProducesOnePhysicalTowerChoice() throws Exception { + StructurePoolElement tower = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower") + .apply(StructureTemplatePool.Projection.RIGID); + StructurePoolElement overgrown = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower_overgrown") + .apply(StructureTemplatePool.Projection.RIGID); + ListPoolElement towers = new ListPoolElement( + List.of(tower, overgrown), + StructureTemplatePool.Projection.RIGID + ); + + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(towers); + Map entry = VillageImporter.piecePoolEntry( + "qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", + 1 + ); + StructureLoss loss = VillageImporter.listElementFallbackLoss( + resolution, + "minecraft:pillager_outpost/towers" + ); + Map basePlatePiece = VillageImporter.pieceJson( + "qa/pillager_outpost/piece/minecraft/pillager_outpost/base_plate", + List.of(Map.of("name", "minecraft:bottom")), + 0 + ); + Map watchtowerPiece = VillageImporter.pieceJson( + "qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", + List.of(), + 1155 + ); + VillageImporter.PoolMemberNormalization connectorScaffold = VillageImporter.normalizePoolMember( + "minecraft:pillager_outpost/features", + "qa/pillager_outpost/pool/minecraft/pillager_outpost/features", + false, + 2, + "minecraft:empty", + "minecraft:pillager_outpost/feature_plate", + "qa/pillager_outpost/piece/minecraft/pillager_outpost/feature_plate", + 1, + 0, + List.of(Map.of("name", "minecraft:bottom"))); + + assertSame(tower, resolution.physicalElement()); + assertEquals("minecraft:pillager_outpost/watchtower", resolution.templateLocation()); + assertEquals(1, resolution.omittedElements()); + assertEquals("qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", entry.get("piece")); + assertEquals(1, entry.get("weight")); + assertEquals(StructureCapability.LIST_ELEMENTS, loss.capability()); + assertEquals("list_pool_overlays_not_imported", loss.code()); + assertEquals(false, basePlatePiece.get("collidable")); + assertFalse(watchtowerPiece.containsKey("collidable")); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, connectorScaffold.disposition()); + assertTrue(connectorScaffold.losses().isEmpty()); + } + + @Test + public void actualWaystoneLikeAllAirMemberNormalizesToEmpty() throws Exception { + StructurePoolElement waystone = StructurePoolElement + .legacy("dungeons_and_taverns:waystones/waystone_scaffold") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(waystone); + VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember( + "dungeons_and_taverns:waystones", + "qa/pool/dungeons_and_taverns/waystones", + false, + 1, + "dungeons_and_taverns:waystones", + resolution.templateLocation(), + "qa/piece/dungeons_and_taverns/waystones/waystone_scaffold", + 2, + 0, + List.of()); + + assertSame(waystone, resolution.physicalElement()); + assertEquals("dungeons_and_taverns:waystones/waystone_scaffold", resolution.templateLocation()); + assertEquals(VillageImporter.PoolMemberDisposition.EMPTY, normalization.disposition()); + assertEquals(true, normalization.poolEntry().get("empty")); + assertEquals(2, normalization.poolEntry().get("weight")); + assertEquals("connectorless_all_air_member_normalized_empty", + normalization.losses().getFirst().code()); + } + + @Test + public void actualNonAirOrphanMemberIsOmitted() throws Exception { + StructurePoolElement orphan = StructurePoolElement + .legacy("test:orphan_house") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(orphan); + VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember( + "test:orphan_pool", + "qa/pool/test/orphan_pool", + false, + 1, + null, + resolution.templateLocation(), + "qa/piece/test/orphan_house", + 4, + 37, + List.of()); + + assertSame(orphan, resolution.physicalElement()); + assertEquals("test:orphan_house", resolution.templateLocation()); + assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition()); + assertTrue(normalization.poolEntry().isEmpty()); + assertEquals("connectorless_non_air_member_omitted", + normalization.losses().getFirst().code()); + assertTrue(normalization.losses().getFirst().detail().contains("37 non-air block(s)")); + assertTrue(normalization.losses().getFirst().detail().contains("no source fallback")); + assertTrue(normalization.losses().getFirst().detail().contains("selection weights")); + assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption")); + } + + @Test + public void actualConnectorlessStartMemberRemainsPhysical() throws Exception { + StructurePoolElement start = StructurePoolElement + .legacy("test:air_start") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(start); + VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember( + "test:start", + "qa/pool/test/start", + true, + 2, + "test:fallback", + resolution.templateLocation(), + "qa/piece/test/air_start", + 6, + 0, + List.of()); + + assertSame(start, resolution.physicalElement()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, normalization.disposition()); + assertEquals("qa/piece/test/air_start", normalization.poolEntry().get("piece")); + assertTrue(normalization.losses().isEmpty()); + } + + @Test + public void actualMixedPoolAllAirMemberIsOmittedWithoutBecomingEmpty() throws Exception { + StructurePoolElement inert = StructurePoolElement + .legacy("test:mixed_air_inert") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(inert); + VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember( + "test:mixed", + "qa/pool/test/mixed", + false, + 4, + "test:mixed", + resolution.templateLocation(), + "qa/piece/test/mixed_air_inert", + 9, + 0, + List.of()); + + assertSame(inert, resolution.physicalElement()); + assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition()); + assertTrue(normalization.poolEntry().isEmpty()); + assertEquals("connectorless_all_air_mixed_member_omitted", + normalization.losses().getFirst().code()); + assertTrue(normalization.losses().getFirst().detail().contains("selection weights")); + assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption")); + } + + @Test + public void actualMixedDistinctFallbackRetainsAllConnectorlessMembers() throws Exception { + StructurePoolElement allAirElement = StructurePoolElement + .legacy("test:mixed_all_air_primary") + .apply(StructureTemplatePool.Projection.RIGID); + StructurePoolElement nonAirElement = StructurePoolElement + .legacy("test:mixed_non_air_primary") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement); + VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement); + VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember( + "test:mixed", + "qa/pool/test/mixed", + false, + 4, + "test:fallback", + allAirResolution.templateLocation(), + "qa/piece/test/mixed_all_air_primary", + 9, + 0, + List.of()); + VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember( + "test:mixed", + "qa/pool/test/mixed", + false, + 4, + "test:fallback", + nonAirResolution.templateLocation(), + "qa/piece/test/mixed_non_air_primary", + 12, + 41, + List.of()); + + assertSame(allAirElement, allAirResolution.physicalElement()); + assertSame(nonAirElement, nonAirResolution.physicalElement()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition()); + assertEquals("qa/piece/test/mixed_all_air_primary", allAir.poolEntry().get("piece")); + assertEquals("qa/piece/test/mixed_non_air_primary", nonAir.poolEntry().get("piece")); + assertTrue(allAir.losses().isEmpty()); + assertTrue(nonAir.losses().isEmpty()); + } + + @Test + public void actualSingletonDistinctFallbackRetainsAllAirAndNonAirMembers() throws Exception { + StructurePoolElement allAirElement = StructurePoolElement + .legacy("test:all_air_primary") + .apply(StructureTemplatePool.Projection.RIGID); + StructurePoolElement nonAirElement = StructurePoolElement + .legacy("test:non_air_primary") + .apply(StructureTemplatePool.Projection.RIGID); + VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement); + VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement); + VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember( + "test:primary", + "qa/pool/test/primary", + false, + 1, + "test:fallback", + allAirResolution.templateLocation(), + "qa/piece/test/all_air_primary", + 5, + 0, + List.of()); + VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember( + "test:primary", + "qa/pool/test/primary", + false, + 1, + "test:fallback", + nonAirResolution.templateLocation(), + "qa/piece/test/non_air_primary", + 7, + 29, + List.of()); + + assertSame(allAirElement, allAirResolution.physicalElement()); + assertSame(nonAirElement, nonAirResolution.physicalElement()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition()); + assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition()); + assertEquals("qa/piece/test/all_air_primary", allAir.poolEntry().get("piece")); + assertEquals("qa/piece/test/non_air_primary", nonAir.poolEntry().get("piece")); + assertTrue(allAir.losses().isEmpty()); + assertTrue(nonAir.losses().isEmpty()); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java new file mode 100644 index 000000000..22904dc89 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java @@ -0,0 +1,508 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.object.IrisJigsawConfiguration; +import art.arcane.iris.engine.object.IrisStructureTerrain; +import com.mojang.datafixers.util.Either; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.MapCodec; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderOwner; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Vec3i; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.FixedBiomeSource; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.blending.Blender; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.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; +import java.util.concurrent.CompletableFuture; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class NativeStructureFactoryTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void genericSourceDoesNotRequireJigsawType() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + + Structure configured = NativeStructureFactory.configure( + null, source, null, false, 80); + + assertSame(source, configured); + } + + @Test + public void jigsawOptionsFailClosedForNonJigsawSource() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + + assertThrows(IllegalStateException.class, () -> NativeStructureFactory.configure( + null, source, new IrisJigsawConfiguration(), false, 80)); + } + + @Test + public void sourceMetadataReadsScalarAndAxisSpecificJigsawRanges() { + CompoundTag scalar = new CompoundTag(); + scalar.putInt("max_distance_from_center", 80); + CompoundTag axisSpecific = new CompoundTag(); + CompoundTag distance = new CompoundTag(); + distance.putInt("horizontal", 96); + distance.putInt("vertical", 64); + axisSpecific.put("max_distance_from_center", distance); + + assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "horizontal")); + assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "vertical")); + assertEquals(96, NativeStructureFactory.sourceDistance(axisSpecific, "horizontal")); + assertEquals(64, NativeStructureFactory.sourceDistance(axisSpecific, "vertical")); + } + + @Test + public void sourceMetadataIncludesAdjustBoundingBoxExpansion() { + Structure none = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); + Structure beardBox = new SwampHutStructure(new Structure.StructureSettings.Builder(HolderSet.empty()) + .terrainAdapation(TerrainAdjustment.BEARD_BOX) + .build()); + + assertEquals(0, NativeStructureFactory.horizontalReferenceExpansion(none)); + assertEquals(12, NativeStructureFactory.horizontalReferenceExpansion(beardBox)); + } + + @Test + public void templatePoolSpanIncludesWeightedListsFallbacksAndRotations() { + StructurePoolElement narrow = inlineElement(30, 8, 70); + StructurePoolElement wide = inlineElement(96, 8, 12); + ListPoolElement list = new ListPoolElement( + List.of(narrow, wide, EmptyPoolElement.INSTANCE), + StructureTemplatePool.Projection.RIGID); + BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:fallback"); + StructureTemplatePool fallback = new StructureTemplatePool( + fallbackHolder, + List.of(Pair.of(inlineElement(110, 8, 20), 1))); + fallbackHolder.bind(fallback); + BoundPoolHolder startHolder = new BoundPoolHolder("test:start"); + StructureTemplatePool start = new StructureTemplatePool( + fallbackHolder, + List.of(Pair.of(list, 5), Pair.of(EmptyPoolElement.INSTANCE, 1))); + startHolder.bind(start); + + assertEquals(110, NativeStructureTemplatePoolBounds.horizontalSpan(start, null)); + } + + @Test + public void oversizedInlineStartTemplateReportsItsFullSpan() { + BoundPoolHolder holder = new BoundPoolHolder("test:oversized"); + StructureTemplatePool pool = new StructureTemplatePool( + holder, + List.of(Pair.of(inlineElement(160, 8, 9), 1))); + holder.bind(pool); + + assertEquals(160, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null)); + } + + @Test + public void sourceSpanIncludesOnlyAliasesThatCanReplaceTheStartPool() { + BoundPoolHolder emptyHolder = new BoundPoolHolder("test:empty"); + StructureTemplatePool empty = new StructureTemplatePool(emptyHolder, List.of()); + emptyHolder.bind(empty); + BoundPoolHolder startHolder = new BoundPoolHolder("test:start"); + StructureTemplatePool start = new StructureTemplatePool( + emptyHolder, List.of(Pair.of(inlineElement(20, 8, 12), 1))); + startHolder.bind(start); + BoundPoolHolder replacementHolder = new BoundPoolHolder("test:replacement"); + StructureTemplatePool replacement = new StructureTemplatePool( + emptyHolder, List.of(Pair.of(inlineElement(30, 8, 12), 1))); + replacementHolder.bind(replacement); + BoundPoolHolder unrelatedHolder = new BoundPoolHolder("test:unrelated"); + StructureTemplatePool unrelated = new StructureTemplatePool( + emptyHolder, List.of(Pair.of(inlineElement(200, 8, 12), 1))); + unrelatedHolder.bind(unrelated); + ResourceKey startKey = startHolder.unwrapKey().orElseThrow(); + ResourceKey replacementKey = replacementHolder.unwrapKey().orElseThrow(); + ResourceKey unrelatedKey = unrelatedHolder.unwrapKey().orElseThrow(); + ResourceKey childAlias = ResourceKey.create( + Registries.TEMPLATE_POOL, Identifier.parse("test:child_alias")); + List aliases = List.of( + PoolAliasBinding.direct(startKey, replacementKey), + PoolAliasBinding.direct(childAlias, unrelatedKey)); + Map, StructureTemplatePool> pools = Map.of( + replacementKey, replacement, + unrelatedKey, unrelated); + + assertEquals(30, NativeStructureTemplatePoolBounds.sourceHorizontalSpan( + null, start, startKey, aliases, pools::get)); + } + + @Test + public void sourceStartSpanDoesNotIncludeItsFallbackPool() { + BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:large_fallback"); + StructureTemplatePool fallback = new StructureTemplatePool( + fallbackHolder, List.of(Pair.of(inlineElement(180, 8, 10), 1))); + fallbackHolder.bind(fallback); + BoundPoolHolder startHolder = new BoundPoolHolder("test:small_start"); + StructureTemplatePool start = new StructureTemplatePool( + fallbackHolder, List.of(Pair.of(inlineElement(24, 8, 10), 1))); + startHolder.bind(start); + + assertEquals(180, NativeStructureTemplatePoolBounds.horizontalSpan(start, null)); + assertEquals(24, NativeStructureTemplatePoolBounds.sourceHorizontalSpan( + null, start, startHolder.unwrapKey().orElseThrow(), List.of(), ignored -> null)); + } + + @Test + public void customPoolElementUsesItsBoundingBoxCapability() { + BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:custom_fallback"); + StructureTemplatePool fallback = new StructureTemplatePool(fallbackHolder, List.of()); + fallbackHolder.bind(fallback); + BoundPoolHolder poolHolder = new BoundPoolHolder("test:custom"); + StructureTemplatePool pool = new StructureTemplatePool( + fallbackHolder, List.of(Pair.of(new BoundingPoolElement(37, 11), 1))); + poolHolder.bind(pool); + + assertEquals(37, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null)); + } + + @Test + public void forcedGeneratorBypassesBiomeEligibilityWithoutFabricatingTerrain() { + ChunkGenerator delegate = new TestChunkGenerator(); + Holder biome = Holder.direct((Biome) null); + LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(-64, 384); + ForcedStructureChunkGenerator generator = new ForcedStructureChunkGenerator( + delegate, biome); + + assertEquals(63, generator.getSeaLevel()); + assertEquals(64, generator.getBaseHeight( + 0, 0, Heightmap.Types.WORLD_SURFACE_WG, heightAccessor, null)); + NoiseColumn column = generator.getBaseColumn( + 0, 0, heightAccessor, null); + assertSame(Blocks.STONE, column.getBlock(80).getBlock()); + assertSame(Blocks.STONE, column.getBlock(81).getBlock()); + assertNotSame(delegate.getBiomeSource(), generator.getBiomeSource()); + } + + @Test + public void undergroundRelocationMovesGenericPiecesAndReturnsFreshStart() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), 0, 0); + StructureStart start = new StructureStart( + source, + new ChunkPos(0, 0), + 0, + new PiecesContainer(List.of(piece)) + ); + + StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY( + start, source, -20, LevelHeightAccessor.create(-64, 384)); + + assertNotSame(start, relocated); + assertEquals(-20, relocated.getPieces().getFirst().getBoundingBox().minY()); + assertEquals(-20, relocated.getBoundingBox().minY()); + } + + @Test + public void invalidGeneratedStartIsANormalSkippedCandidate() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + StructureStart valid = new StructureStart( + source, + new ChunkPos(0, 0), + 0, + new PiecesContainer(List.of( + new SwampHutPiece(RandomSource.create(23L), 0, 0))) + ); + + assertFalse(NativeStructureStartInjector.isUsableGeneratedStart( + StructureStart.INVALID_START)); + assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(null)); + assertFalse(NativeStructureStartInjector.isUsableGeneratedStart( + new StructureStart(source, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of())))); + assertTrue(NativeStructureStartInjector.isUsableGeneratedStart(valid)); + } + + @Test + public void unrepresentableGeneratedContentBecomesAnUnpublishedInvalidStart() { + Structure source = new SwampHutStructure( + new Structure.StructureSettings(HolderSet.empty())); + SwampHutPiece boundaryPiece = new SwampHutPiece(RandomSource.create(29L), 0, 0); + boundaryPiece.move(143 - boundaryPiece.getBoundingBox().maxX(), 0, 0); + StructureStart boundary = new StructureStart( + source, + new ChunkPos(0, 0), + 0, + new PiecesContainer(List.of(boundaryPiece)) + ); + SwampHutPiece oversizedPiece = new SwampHutPiece(RandomSource.create(31L), 0, 0); + oversizedPiece.move(144 - oversizedPiece.getBoundingBox().maxX(), 0, 0); + StructureStart oversized = new StructureStart( + source, + new ChunkPos(0, 0), + 0, + new PiecesContainer(List.of(oversizedPiece)) + ); + + StructureStart boundaryResult = NativeStructureReferenceEnvelope.wrapForPublication( + boundary, source, 0, new IrisStructureTerrain(), "test:boundary_content"); + + StructureStart result = NativeStructureReferenceEnvelope.wrapForPublication( + oversized, source, 0, + new IrisStructureTerrain(), + "test:oversized_content"); + + assertTrue(boundaryResult.isValid()); + assertSame(StructureStart.INVALID_START, result); + assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(result)); + assertEquals(1, oversized.getPieces().size()); + } + + @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))); + } + + @Override + protected MapCodec codec() { + return MapCodec.unit(this); + } + + @Override + public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, + BiomeManager biomeManager, StructureManager structureManager, + ChunkAccess chunk) { + } + + @Override + public void buildSurface(WorldGenRegion level, StructureManager structureManager, + RandomState randomState, ChunkAccess protoChunk) { + } + + @Override + public void spawnOriginalMobs(WorldGenRegion worldGenRegion) { + } + + @Override + public int getGenDepth() { + return 384; + } + + @Override + public CompletableFuture fillFromNoise( + Blender blender, RandomState randomState, + StructureManager structureManager, ChunkAccess centerChunk) { + return CompletableFuture.completedFuture(centerChunk); + } + + @Override + public int getSeaLevel() { + return 63; + } + + @Override + public int getMinY() { + return -64; + } + + @Override + public int getBaseHeight(int x, int z, Heightmap.Types type, + LevelHeightAccessor heightAccessor, RandomState randomState) { + return 64; + } + + @Override + public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, + RandomState randomState) { + BlockState[] states = new BlockState[heightAccessor.getHeight()]; + for (int index = 0; index < states.length; index++) { + states[index] = Blocks.STONE.defaultBlockState(); + } + return new NoiseColumn(heightAccessor.getMinY(), states); + } + + @Override + public void addDebugScreenInfo(List result, RandomState randomState, + BlockPos feetPos) { + } + } + + private static StructurePoolElement inlineElement(int sizeX, int sizeY, int sizeZ) { + StructureTemplate template = new StructureTemplate(); + CompoundTag tag = new CompoundTag(); + ListTag size = new ListTag(); + size.add(IntTag.valueOf(sizeX)); + size.add(IntTag.valueOf(sizeY)); + size.add(IntTag.valueOf(sizeZ)); + tag.put("size", size); + template.load(BuiltInRegistries.BLOCK, tag); + return new InlinePoolElement(template); + } + + private static final class InlinePoolElement extends SinglePoolElement { + private final StructureTemplate inlineTemplate; + + private InlinePoolElement(StructureTemplate template) { + super(Either.right(template), Holder.direct(new StructureProcessorList(List.of())), + StructureTemplatePool.Projection.RIGID, Optional.empty()); + inlineTemplate = template; + } + + @Override + public BoundingBox getBoundingBox(StructureTemplateManager templateManager, + BlockPos position, Rotation rotation) { + return inlineTemplate.getBoundingBox( + new StructurePlaceSettings().setRotation(rotation), position); + } + } + + private static final class BoundingPoolElement extends StructurePoolElement { + private final int width; + private final int depth; + + private BoundingPoolElement(int width, int depth) { + super(StructureTemplatePool.Projection.RIGID); + this.width = width; + this.depth = depth; + } + + @Override + public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) { + return new Vec3i(width, 1, depth); + } + + @Override + public List getShuffledJigsawBlocks( + StructureTemplateManager templateManager, BlockPos position, + Rotation rotation, RandomSource random) { + return List.of(); + } + + @Override + public BoundingBox getBoundingBox(StructureTemplateManager templateManager, + BlockPos position, Rotation rotation) { + return new BoundingBox( + position.getX(), position.getY(), position.getZ(), + position.getX() + width - 1, position.getY(), + position.getZ() + depth - 1); + } + + @Override + public boolean place(StructureTemplateManager templateManager, + WorldGenLevel world, + StructureManager structureManager, ChunkGenerator chunkGenerator, + BlockPos position, BlockPos pivot, Rotation rotation, + BoundingBox area, RandomSource random, + LiquidSettings liquidSettings, boolean keepJigsaws) { + return true; + } + + @Override + public StructurePoolElementType getType() { + return StructurePoolElementType.EMPTY; + } + } + + private static final class BoundPoolHolder extends Holder.Reference { + private BoundPoolHolder(String key) { + super(Type.STAND_ALONE, new HolderOwner<>() { + }, ResourceKey.create(Registries.TEMPLATE_POOL, Identifier.parse(key)), null); + } + + private void bind(StructureTemplatePool pool) { + bindValue(pool); + } + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureGenerationKeysContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureGenerationKeysContractTest.java new file mode 100644 index 000000000..0519e6007 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureGenerationKeysContractTest.java @@ -0,0 +1,60 @@ +package art.arcane.iris.nativegen; + +import net.minecraft.SharedConstants; +import net.minecraft.core.Vec3i; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NativeStructureGenerationKeysContractTest { + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void 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))); + assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(-0.1F))); + assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(Float.NaN))); + assertTrue(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.25F))); + } + + private static RandomSpreadStructurePlacement placement(float frequency) { + return new RandomSpreadStructurePlacement( + Vec3i.ZERO, + StructurePlacement.FrequencyReductionMethod.DEFAULT, + frequency, + 1, + Optional.empty(), + 2, + 1, + RandomSpreadType.LINEAR); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureLocatePersistenceTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureLocatePersistenceTest.java new file mode 100644 index 000000000..c1f0885ff --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureLocatePersistenceTest.java @@ -0,0 +1,197 @@ +package art.arcane.iris.nativegen; + +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.ChunkPos; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NativeStructureLocatePersistenceTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void storageProbeScansEachCandidateOnlyOnce() { + AtomicInteger reads = new AtomicInteger(); + CompoundTag chunk = new CompoundTag(); + chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version()); + chunk.putString("Status", "full"); + CompoundTag start = new CompoundTag(); + start.putString("id", "minecraft:village_plains"); + start.putInt("references", 0); + CompoundTag starts = new CompoundTag(); + starts.put("minecraft:village_plains", start); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + chunk.put("structures", structures); + NativeStructureLocatePersistence.ProbeBudget budget = + new NativeStructureLocatePersistence.ProbeBudget( + 512, + (chunkPos, visitor) -> { + reads.incrementAndGet(); + chunk.acceptAsRoot(visitor); + return CompletableFuture.completedFuture(null); + }); + ChunkPos chunkPos = new ChunkPos(7, -11); + + assertTrue(budget.acceptsStored( + null, chunkPos, "minecraft:village_plains", true)); + assertTrue(budget.acceptsStored( + null, chunkPos, "minecraft:village_plains", true)); + assertEquals(1, reads.get()); + assertEquals(1, budget.used()); + } + + @Test + public void legacyStoredChunkIsDatafixedBeforeItsStartIsClassified() { + AtomicBoolean datafixed = new AtomicBoolean(); + CompoundTag legacy = new CompoundTag(); + legacy.putInt("DataVersion", 100); + legacy.putString("Status", "full"); + NativeStructureLocatePersistence.ProbeBudget budget = + new NativeStructureLocatePersistence.ProbeBudget( + 512, + (chunkPos, visitor) -> { + legacy.acceptAsRoot(visitor); + return CompletableFuture.completedFuture(null); + }, + (level, storedChunk) -> { + datafixed.set(true); + CompoundTag fixed = storedChunk.copy(); + fixed.putInt("DataVersion", + SharedConstants.getCurrentVersion().dataVersion().version()); + CompoundTag start = new CompoundTag(); + start.putString("id", "minecraft:village_plains"); + start.putInt("references", 1); + CompoundTag starts = new CompoundTag(); + starts.put("minecraft:village_plains", start); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + fixed.put("structures", structures); + return fixed; + }); + + assertFalse(budget.acceptsStored( + null, new ChunkPos(3, 4), "minecraft:village_plains", true)); + assertTrue(datafixed.get()); + } + + @Test + public void partialDatafixFailureFallsBackToSelectedChunkVerification() { + CompoundTag legacy = new CompoundTag(); + legacy.putInt("DataVersion", 100); + NativeStructureLocatePersistence.ProbeBudget budget = + new NativeStructureLocatePersistence.ProbeBudget( + 512, + (chunkPos, visitor) -> { + legacy.acceptAsRoot(visitor); + return CompletableFuture.completedFuture(null); + }, + (level, storedChunk) -> { + throw new IllegalStateException("broken partial datafix"); + }); + + assertTrue(budget.acceptsStored( + null, new ChunkPos(9, -2), "minecraft:village_plains", true)); + } + + @Test + public void missingStartsContainerFallsBackToSelectedChunkVerification() { + CompoundTag chunk = currentChunk(); + NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk); + + assertTrue(budget.acceptsStored( + null, new ChunkPos(5, 8), "minecraft:village_plains", false)); + assertTrue(budget.acceptsStored( + null, new ChunkPos(5, 8), "minecraft:village_plains", true)); + } + + @Test + public void presentEmptyStartsContainerRejectsTheCandidateWithoutLoading() { + CompoundTag chunk = currentChunk(); + CompoundTag structures = new CompoundTag(); + structures.put("starts", new CompoundTag()); + chunk.put("structures", structures); + NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk); + + assertFalse(budget.acceptsStored( + null, new ChunkPos(-3, 2), "minecraft:village_plains", false)); + } + + @Test + public void collisionTombstoneIsRejectedForNormalAndUnexploredLocate() { + CompoundTag chunk = currentChunk(); + CompoundTag tombstone = new CompoundTag(); + tombstone.putString("id", "INVALID"); + CompoundTag starts = new CompoundTag(); + starts.put("minecraft:village_plains", tombstone); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + chunk.put("structures", structures); + ChunkPos chunkPos = new ChunkPos(12, -9); + + assertFalse(budget(chunk).acceptsStored( + null, chunkPos, "minecraft:village_plains", false)); + assertFalse(budget(chunk).acceptsStored( + null, chunkPos, "minecraft:village_plains", true)); + } + + @Test + public void malformedNonCompoundStartIsRejectedWithoutLoading() { + CompoundTag chunk = currentChunk(); + CompoundTag starts = new CompoundTag(); + starts.putInt("minecraft:village_plains", 1); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + chunk.put("structures", structures); + + assertFalse(budget(chunk).acceptsStored( + null, new ChunkPos(1, 1), "minecraft:village_plains", false)); + } + + @Test + public void referencedStoredStartIsOnlyRejectedForUnexploredLocate() { + CompoundTag chunk = currentChunk(); + CompoundTag start = new CompoundTag(); + start.putString("id", "minecraft:village_plains"); + start.putInt("references", 1); + CompoundTag starts = new CompoundTag(); + starts.put("minecraft:village_plains", start); + CompoundTag structures = new CompoundTag(); + structures.put("starts", starts); + chunk.put("structures", structures); + ChunkPos chunkPos = new ChunkPos(-4, -7); + + assertTrue(budget(chunk).acceptsStored( + null, chunkPos, "minecraft:village_plains", false)); + assertFalse(budget(chunk).acceptsStored( + null, chunkPos, "minecraft:village_plains", true)); + } + + private static CompoundTag currentChunk() { + CompoundTag chunk = new CompoundTag(); + chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version()); + return chunk; + } + + private static NativeStructureLocatePersistence.ProbeBudget budget(CompoundTag chunk) { + return new NativeStructureLocatePersistence.ProbeBudget( + 512, + (chunkPos, visitor) -> { + chunk.acceptAsRoot(visitor); + return CompletableFuture.completedFuture(null); + }); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprintTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprintTest.java new file mode 100644 index 000000000..542e869ef --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprintTest.java @@ -0,0 +1,346 @@ +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.volmlib.util.collection.KList; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.ListTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class NativeStructureOwnershipFingerprintTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void monumentFingerprintSurvivesSourceYRegenerationWithoutSyntheticPieces() { + long seed = 8372619L; + ChunkPos origin = new ChunkPos(9, -4); + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + StructureStart generated = monumentStart(structure, origin, seed); + NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel( + generated, 0, 80, -64, 320); + int alignedLocatorY = NativeStructureOwnershipFingerprint.locatorY(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(24); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, + structure, + 0, + terrain); + String beforeReload = NativeStructureOwnershipFingerprint.fingerprint( + "minecraft:monument", wrapped); + + ListTag serializedPieces = (ListTag) new PiecesContainer(wrapped.getPieces()).save(null); + PiecesContainer loadedPieces = PiecesContainer.load(serializedPieces, null); + assertEquals(1, loadedPieces.pieces().size()); + assertTrue(loadedPieces.pieces().getFirst() + instanceof OceanMonumentPieces.MonumentBuilding); + PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad( + origin, seed, loadedPieces); + StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces); + String afterReload = NativeStructureOwnershipFingerprint.fingerprint( + "minecraft:monument", reloaded); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + "minecraft:monument", + wrapped, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()), + NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain)); + + assertEquals(beforeReload, afterReload); + assertTrue(NativeStructureOwnershipFingerprint.matches(ownership, reloaded)); + assertNotEquals(alignedLocatorY, NativeStructureOwnershipFingerprint.locatorY(reloaded)); + assertEquals(alignedLocatorY, ownership.locatorY()); + assertEquals(1, reloaded.getPieces().size()); + assertEquals(1, wrapped.getPieces().size()); + } + + @Test + public void sameKeyAndOriginStillRequiresMatchingContentIdentity() { + long seed = 18273L; + ChunkPos origin = new ChunkPos(2, 3); + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + StructureStart expected = monumentStart(structure, origin, seed); + StructureStart moved = monumentStart(structure, origin, seed); + for (StructurePiece piece : moved.getPieces()) { + piece.move(1, 0, 0); + } + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + "minecraft:monument", + expected, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(expected).minY()), + expected.getBoundingBox()); + + assertFalse(NativeStructureOwnershipFingerprint.matches(ownership, moved)); + } + + @Test + public void persistedEnvelopeRepairsReloadedMonumentReferenceOutsideLiveBounds() { + long seed = 772931L; + ChunkPos origin = new ChunkPos(4, -7); + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + StructureStart generated = monumentStart(structure, origin, seed); + NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel( + generated, 0, 80, -64, 320); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(24); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, + structure, + 0, + terrain); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + "minecraft:monument", + wrapped, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()), + NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain)); + PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad( + origin, seed, new PiecesContainer(wrapped.getPieces())); + StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces); + ChunkPos target = outsideLiveBounds(ownership, reloaded); + + assertNotNull(target); + assertTrue(NativeStructureReferenceRepair.requiresReference( + target, "minecraft:monument", reloaded, ownership)); + assertFalse(NativeStructureReferenceRepair.requiresReference( + target, "minecraft:monument", reloaded, null)); + } + + @Test + public void overlappingManualStartsSixteenChunksApartRemainIndependentlyReferenced() { + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + ChunkPos westOrigin = new ChunkPos(0, 0); + ChunkPos eastOrigin = new ChunkPos(16, 0); + StructureStart west = monumentStart(structure, westOrigin, 317L); + StructureStart east = monumentStart(structure, eastOrigin, 941L); + west.getPieces().getFirst().move(112, 0, 0); + east.getPieces().getFirst().move(-96, 0, 0); + NativeStructureOwnershipRecord westOwnership = ownership(westOrigin, west, structure); + NativeStructureOwnershipRecord eastOwnership = ownership(eastOrigin, east, structure); + ChunkPos shared = new ChunkPos(8, 0); + + assertTrue(NativeStructureReferenceEnvelope.contentBounds(west).intersects( + NativeStructureReferenceEnvelope.contentBounds(east))); + assertTrue(NativeStructureReferenceRepair.requiresReference( + shared, "minecraft:monument", west, westOwnership)); + assertTrue(NativeStructureReferenceRepair.requiresReference( + shared, "minecraft:monument", east, eastOwnership)); + } + + @Test + public void denseAdjacentManualStartsDoNotSuppressEachOther() { + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + ChunkPos firstOrigin = new ChunkPos(0, 0); + ChunkPos secondOrigin = new ChunkPos(1, 0); + StructureStart first = monumentStart(structure, firstOrigin, 17L); + StructureStart second = monumentStart(structure, secondOrigin, 23L); + NativeStructureOwnershipRecord firstOwnership = ownership(firstOrigin, first, structure); + NativeStructureOwnershipRecord secondOwnership = ownership(secondOrigin, second, structure); + ChunkPos shared = new ChunkPos(0, 0); + + assertTrue(NativeStructureReferenceEnvelope.contentBounds(first).intersects( + NativeStructureReferenceEnvelope.contentBounds(second))); + assertTrue(NativeStructureReferenceRepair.requiresReference( + shared, "minecraft:monument", first, firstOwnership)); + assertTrue(NativeStructureReferenceRepair.requiresReference( + shared, "minecraft:monument", second, secondOwnership)); + } + + @Test + public void customPoolElementIdentityDoesNotDependOnProcessLocalToString() { + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + ChunkPos origin = new ChunkPos(3, -2); + StructureStart first = poolStart(structure, origin, new IdentityStringPoolElement()); + StructureStart reloaded = poolStart(structure, origin, new IdentityStringPoolElement()); + + assertEquals( + NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", first), + NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", reloaded)); + } + + @Test + public void namedPoolTemplateRemainsPartOfTheStableFingerprint() { + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + ChunkPos origin = new ChunkPos(3, -2); + StructurePoolElement firstElement = StructurePoolElement.single("test:first") + .apply(StructureTemplatePool.Projection.RIGID); + StructurePoolElement secondElement = StructurePoolElement.single("test:second") + .apply(StructureTemplatePool.Projection.RIGID); + + assertNotEquals( + NativeStructureOwnershipFingerprint.fingerprint( + "test:named_pool", poolStart(structure, origin, firstElement)), + NativeStructureOwnershipFingerprint.fingerprint( + "test:named_pool", poolStart(structure, origin, secondElement))); + } + + private static StructureStart monumentStart(OceanMonumentStructure structure, + ChunkPos origin, long seed) { + WorldgenRandom random = new WorldgenRandom( + new LegacyRandomSource(RandomSupport.generateUniqueSeed())); + random.setLargeFeatureSeed(seed, origin.x(), origin.z()); + Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random); + OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding( + random, + origin.getMinBlockX() - 29, + origin.getMinBlockZ() - 29, + orientation + ); + return new StructureStart( + structure, + origin, + 0, + new PiecesContainer(List.of(building)) + ); + } + + private static NativeStructureStartPlan plan(ChunkPos origin, int baseY) { + IrisNativeStructure source = new IrisNativeStructure() + .setStructure("minecraft:monument") + .setWeight(1); + IrisStructurePlacement placement = new IrisStructurePlacement() + .setPlacementId("monument-test") + .setNativeStructures(new KList().qadd(source)); + return new NativeStructureStartPlan( + placement, + source, + origin.x(), + origin.z(), + baseY + ); + } + + private static NativeStructureOwnershipRecord ownership( + ChunkPos origin, StructureStart start, OceanMonumentStructure structure) { + return NativeStructureOwnershipFingerprint.capture( + "minecraft:monument", + start, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()), + NativeStructureReferenceEnvelope.referenceBounds( + start, structure, new IrisStructureTerrain()) + ); + } + + private static StructureStart poolStart( + OceanMonumentStructure structure, ChunkPos origin, StructurePoolElement element) { + BlockPos position = new BlockPos( + origin.getMiddleBlockX(), 64, origin.getMiddleBlockZ()); + PoolElementStructurePiece piece = new PoolElementStructurePiece( + null, element, position, 0, Rotation.NONE, + new BoundingBox(position), LiquidSettings.APPLY_WATERLOGGING); + return new StructureStart( + structure, origin, 0, new PiecesContainer(List.of(piece))); + } + + private static final class IdentityStringPoolElement extends StructurePoolElement { + private IdentityStringPoolElement() { + super(StructureTemplatePool.Projection.RIGID); + } + + @Override + public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) { + return new Vec3i(1, 1, 1); + } + + @Override + public List getShuffledJigsawBlocks( + StructureTemplateManager templateManager, BlockPos position, + Rotation rotation, RandomSource random) { + return List.of(); + } + + @Override + public BoundingBox getBoundingBox(StructureTemplateManager templateManager, + BlockPos position, Rotation rotation) { + return new BoundingBox(position); + } + + @Override + public boolean place(StructureTemplateManager templateManager, + WorldGenLevel world, StructureManager structureManager, + ChunkGenerator chunkGenerator, BlockPos position, BlockPos pivot, + Rotation rotation, BoundingBox area, RandomSource random, + LiquidSettings liquidSettings, boolean keepJigsaws) { + return true; + } + + @Override + public StructurePoolElementType getType() { + return StructurePoolElementType.EMPTY; + } + + @Override + public String toString() { + return "IdentityStringPoolElement@" + + Integer.toHexString(System.identityHashCode(this)); + } + } + + private static ChunkPos outsideLiveBounds(NativeStructureOwnershipRecord ownership, + StructureStart start) { + for (int chunkX = ownership.referenceMinChunkX(); + chunkX <= ownership.referenceMaxChunkX(); chunkX++) { + for (int chunkZ = ownership.referenceMinChunkZ(); + chunkZ <= ownership.referenceMaxChunkZ(); chunkZ++) { + ChunkPos candidate = new ChunkPos(chunkX, chunkZ); + if (!start.getBoundingBox().intersects( + candidate.getMinBlockX(), candidate.getMinBlockZ(), + candidate.getMaxBlockX(), candidate.getMaxBlockZ())) { + return candidate; + } + } + } + return null; + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecoveryTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecoveryTest.java new file mode 100644 index 000000000..e4bee55cf --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecoveryTest.java @@ -0,0 +1,314 @@ +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.volmlib.util.collection.KList; +import net.minecraft.SharedConstants; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class NativeStructureOwnershipRecoveryTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void missingSidecarRecoversExactPolicyEnvelopeAndLocatorAfterReload() { + long seed = 5519231L; + ChunkPos origin = new ChunkPos(7, -5); + OceanMonumentStructure structure = structure(); + StructureStart expected = monumentStart(structure, origin, seed); + NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel( + expected, 0, 80, -64, 320); + int expectedLocatorY = NativeStructureOwnershipFingerprint.locatorY(expected); + NativeStructureStartPlan plan = plan(origin, "recovery-exact", 24); + StructureStart reloaded = new StructureStart( + structure, + origin, + expected.getReferences(), + OceanMonumentStructure.regeneratePiecesAfterLoad( + origin, seed, new PiecesContainer(expected.getPieces())) + ); + + NativeStructureOwnershipRecord recovered = + NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, reloaded, + plan, expected, false); + + assertNotNull(recovered); + assertEquals(StructurePlacementGrid.placementIdentity(plan.placement()), + recovered.placementIdentity()); + assertEquals(expectedLocatorY, recovered.locatorY()); + assertNotEquals(NativeStructureOwnershipFingerprint.locatorY(reloaded), + recovered.locatorY()); + assertEquals(IrisStructureTerrainMode.FORCE_CARVE, + recovered.restoredDecision().terrain().resolvedMode()); + assertEquals(24, recovered.restoredDecision().terrain().getHorizontalPadding()); + assertTrue(recovered.referenceMinChunkX() + < NativeStructureReferenceEnvelope.contentBounds(reloaded).minX() >> 4); + } + + @Test + public void naturalSameKeyStartRemainsUnownedWhenOriginIsAmbiguous() { + long seed = 22817L; + ChunkPos origin = new ChunkPos(-3, 6); + OceanMonumentStructure structure = structure(); + StructureStart natural = monumentStart(structure, origin, seed); + NativeStructureStartPlan plan = plan(origin, "natural-ambiguous", 0); + + NativeStructureOwnershipRecord recovered = + NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, natural, + plan, natural, true); + + assertNull(recovered); + } + + @Test + public void changedPlacementIdentityOrGeometryCannotRecoverOwnership() { + long seed = 991723L; + ChunkPos origin = new ChunkPos(2, 4); + OceanMonumentStructure structure = structure(); + StructureStart expected = monumentStart(structure, origin, seed); + StructureStart changedGeometry = monumentStart(structure, origin, seed); + for (StructurePiece piece : changedGeometry.getPieces()) { + piece.move(1, 0, 0); + } + NativeStructureStartPlan originalIdentity = plan( + origin, "original-identity", 0); + NativeStructureStartPlan changedIdentity = plan( + new ChunkPos(origin.x() + 1, origin.z()), "changed-identity", 0); + + assertNotEquals( + StructurePlacementGrid.placementIdentity(originalIdentity.placement()), + StructurePlacementGrid.placementIdentity(changedIdentity.placement())); + assertNull(NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, expected, + changedIdentity, expected, false)); + assertNull(NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, changedGeometry, + plan(origin, "geometry-check", 0), expected, false)); + } + + @Test + public void recoveredEnvelopeStillRequiresExactFingerprint() { + long seed = 81337L; + ChunkPos origin = new ChunkPos(-8, -9); + OceanMonumentStructure structure = structure(); + StructureStart expected = monumentStart(structure, origin, seed); + NativeStructureOwnershipRecord recovered = + NativeStructureOwnershipRecovery.proveCandidate( + "minecraft:monument", structure, expected, + plan(origin, "fingerprint-check", 16), expected, false); + assertNotNull(recovered); + + StructureStart moved = monumentStart(structure, origin, seed); + for (StructurePiece piece : moved.getPieces()) { + piece.move(0, 0, 1); + } + + assertFalse(NativeStructureOwnershipFingerprint.matches(recovered, moved)); + } + + @Test + public void staleVacuumEnvelopeRefreshesWithoutReplacingOwnershipIdentity() { + String structureKey = "minecraft:monument"; + long seed = 648231L; + ChunkPos origin = new ChunkPos(5, -6); + OceanMonumentStructure structure = structure(); + StructureStart start = monumentStart(structure, origin, seed); + NativeStructureStartPlan plan = plan( + origin, "vacuum-envelope-refresh", IrisStructureTerrainMode.VACUUM, 0); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start); + NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture( + structureKey, start, plan, content); + + NativeStructureOwnershipRecord refreshed = + NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, start, stale); + + assertNotNull(refreshed); + assertNotEquals(stale, refreshed); + assertEquals(stale.schema(), refreshed.schema()); + assertEquals(stale.ownershipKey(), refreshed.ownershipKey()); + assertEquals(stale.placementIdentity(), refreshed.placementIdentity()); + assertEquals(stale.baseY(), refreshed.baseY()); + assertEquals(stale.locatorY(), refreshed.locatorY()); + assertEquals(stale.contentFingerprint(), refreshed.contentFingerprint()); + assertEquals(stale.decision(), refreshed.decision()); + assertEquals(IrisStructureTerrainMode.VACUUM, + refreshed.restoredDecision().terrain().resolvedMode()); + BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, plan.placement().resolvedTerrain(), structureKey); + assertEquals(expected.minX() >> 4, refreshed.referenceMinChunkX()); + assertEquals(expected.maxX() >> 4, refreshed.referenceMaxChunkX()); + assertEquals(expected.minZ() >> 4, refreshed.referenceMinChunkZ()); + assertEquals(expected.maxZ() >> 4, refreshed.referenceMaxChunkZ()); + assertTrue(hasExpandedCoverage(stale, refreshed)); + assertSame(refreshed, NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, start, refreshed)); + } + + @Test + public void staleEnvelopeCannotRefreshAgainstDifferentContent() { + String structureKey = "minecraft:monument"; + long seed = 412987L; + ChunkPos origin = new ChunkPos(-2, 7); + OceanMonumentStructure structure = structure(); + StructureStart expected = monumentStart(structure, origin, seed); + NativeStructureStartPlan plan = plan( + origin, "vacuum-content-check", IrisStructureTerrainMode.VACUUM, 0); + NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture( + structureKey, expected, plan, + NativeStructureReferenceEnvelope.contentBounds(expected)); + StructureStart moved = monumentStart(structure, origin, seed); + for (StructurePiece piece : moved.getPieces()) { + piece.move(1, 0, 0); + } + + assertNull(NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, moved, stale)); + } + + @Test + public void currentNonVacuumEnvelopeRemainsThePersistedAuthority() { + String structureKey = "minecraft:monument"; + long seed = 927451L; + ChunkPos origin = new ChunkPos(3, 8); + OceanMonumentStructure structure = structure(); + StructureStart start = monumentStart(structure, origin, seed); + NativeStructureStartPlan plan = plan(origin, "current-force-carve", 24); + BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, plan.placement().resolvedTerrain(), structureKey); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + structureKey, start, plan, envelope); + + assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, start, ownership)); + } + + @Test + public void clippedVacuumEnvelopeRemainsStableAtTheReferenceLimit() { + String structureKey = "minecraft:monument"; + long seed = 381729L; + ChunkPos origin = new ChunkPos(0, 0); + OceanMonumentStructure structure = structure(); + StructureStart start = monumentStart(structure, origin, seed); + BoundingBox initial = NativeStructureReferenceEnvelope.contentBounds(start); + int maximumReferenceBlockX = ((origin.x() + + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15; + int shiftX = maximumReferenceBlockX - initial.maxX(); + for (StructurePiece piece : start.getPieces()) { + piece.move(shiftX, 0, 0); + } + NativeStructureStartPlan plan = plan( + origin, "clipped-vacuum", IrisStructureTerrainMode.VACUUM, 0); + BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, plan.placement().resolvedTerrain(), structureKey); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + structureKey, start, plan, envelope); + + assertEquals(origin.x() + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS, + ownership.referenceMaxChunkX()); + assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope( + structureKey, structure, start, ownership)); + } + + private static OceanMonumentStructure structure() { + return new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + } + + private static StructureStart monumentStart(OceanMonumentStructure structure, + ChunkPos origin, long seed) { + WorldgenRandom random = new WorldgenRandom( + new LegacyRandomSource(RandomSupport.generateUniqueSeed())); + random.setLargeFeatureSeed(seed, origin.x(), origin.z()); + Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random); + OceanMonumentPieces.MonumentBuilding building = + new OceanMonumentPieces.MonumentBuilding( + random, + origin.getMinBlockX() - 29, + origin.getMinBlockZ() - 29, + orientation + ); + return new StructureStart( + structure, origin, 0, new PiecesContainer(List.of(building))); + } + + private static NativeStructureStartPlan plan(ChunkPos origin, + String placementId, + int horizontalPadding) { + return plan(origin, placementId, + IrisStructureTerrainMode.FORCE_CARVE, horizontalPadding); + } + + private static NativeStructureStartPlan plan(ChunkPos origin, + String placementId, + IrisStructureTerrainMode terrainMode, + int horizontalPadding) { + IrisNativeStructure source = new IrisNativeStructure() + .setStructure("minecraft:monument") + .setWeight(1); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(terrainMode) + .setHorizontalPadding(horizontalPadding); + IrisStructurePlacement placement = new IrisStructurePlacement() + .setPlacementId(placementId) + .setTerrain(terrain) + .setNativeStructures(new KList().qadd(source)); + return new NativeStructureStartPlan( + placement, + source, + origin.x(), + origin.z(), + NativeStructureReferenceEnvelope.contentBounds( + monumentStart(structure(), origin, 1L)).minY() + ); + } + + private static boolean hasExpandedCoverage( + NativeStructureOwnershipRecord stale, + NativeStructureOwnershipRecord refreshed) { + for (int chunkX = refreshed.referenceMinChunkX(); + chunkX <= refreshed.referenceMaxChunkX(); chunkX++) { + for (int chunkZ = refreshed.referenceMinChunkZ(); + chunkZ <= refreshed.referenceMaxChunkZ(); chunkZ++) { + if (!stale.covers(chunkX, chunkZ)) { + return true; + } + } + } + return false; + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java new file mode 100644 index 000000000..4b7c4ccc1 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java @@ -0,0 +1,547 @@ +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.volmlib.util.math.RNG; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.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; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class NativeStructurePostProcessorEncaseTest { + private static final long TEST_SEED = 1234L; + + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void encaseFillsAirAndLiquidWithoutOverwritingExistingTerrain() { + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1, + bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1); + Map blocks = new HashMap<>(); + put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState()); + put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState()); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "minecraft:stronghold", start, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE) + .setHorizontalPadding(1) + .setCeilingPadding(1) + .setFloorPadding(1), + null); + + assertEquals(Blocks.DIRT.defaultBlockState(), + state(blocks, bounds.minX(), bounds.minY(), bounds.minZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1)); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1)); + } + + @Test + public void defaultEncasePaletteSplitsStoneAboveZeroAndDeepslateBelow() { + StructureStart start = start(TerrainAdjustment.BURY, -1); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + Map blocks = new HashMap<>(); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), bounds, "minecraft:stronghold", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE), + null); + + assertEquals(-1, bounds.minY()); + assertEquals(Blocks.DEEPSLATE.defaultBlockState(), + state(blocks, bounds.minX(), -1, bounds.minZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX(), 0, bounds.minZ())); + } + + @Test + public void defaultEncaseMaterialFollowsVanillaDimensionTerrain() { + assertEquals(Blocks.NETHERRACK.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.NETHER, 64)); + assertEquals(Blocks.END_STONE.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.END, 64)); + assertEquals(Blocks.STONE.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.OVERWORLD, 64)); + } + + @Test + public void sourceEncaseMaterialFollowsNearbyIrisTerrain() { + Map blocks = new HashMap<>(); + BlockPos fill = new BlockPos(4, 64, 8); + put(blocks, fill.getX() + 3, fill.getY(), fill.getZ(), Blocks.TUFF.defaultBlockState()); + + assertEquals(Blocks.TUFF.defaultBlockState(), + NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill)); + } + + @Test + public void sourceMaterialSnapshotIgnoresPreviouslyDecoratedAdjacentChunks() { + Map blocks = new HashMap<>(); + BlockPos fill = new BlockPos(0, 64, 8); + put(blocks, -1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState()); + put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState()); + WorldGenLevel world = world(blocks); + NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain = + sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15)); + + assertEquals(Blocks.TUFF.defaultBlockState(), + NativeStructureTerrainIntegrator.sourceEncaseBlock( + world, fill, sourceTerrain)); + } + + @Test + public void sourceMaterialSnapshotIsStableAcrossEarlierFillsInTheSameChunk() { + Map blocks = new HashMap<>(); + BlockPos fill = new BlockPos(0, 64, 8); + put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState()); + WorldGenLevel world = world(blocks); + NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain = + sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15)); + put(blocks, 1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState()); + + assertEquals(Blocks.TUFF.defaultBlockState(), + NativeStructureTerrainIntegrator.sourceEncaseBlock( + world, fill, sourceTerrain)); + } + + @Test + public void sourceMaterialRejectsStructureBlocksAndOresBeforeDimensionFallback() { + Map blocks = new HashMap<>(); + BlockPos fill = new BlockPos(4, -20, 8); + put(blocks, 3, -20, 8, Blocks.OAK_PLANKS.defaultBlockState()); + put(blocks, 5, -20, 8, Blocks.STONE_BRICKS.defaultBlockState()); + put(blocks, 4, -20, 7, Blocks.IRON_ORE.defaultBlockState()); + + assertEquals(Blocks.DEEPSLATE.defaultBlockState(), + NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill)); + } + + @Test + public void sourceMaterialSnapshotOnlyAllocatesActualFillLayers() { + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox area = new BoundingBox(0, -2032, 0, 15, 2030, 15); + NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain = + NativeStructureTerrainIntegrator.captureSourceTerrain( + world(new HashMap<>()), area, + List.of(new NativeStructureTerrainIntegrator.TerrainTarget( + "test:bounded-snapshot", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE)))); + + assertNotNull(sourceTerrain); + assertEquals(23 * 16 * 16, sourceTerrain.sampledCells()); + } + + @Test + public void customDimensionKeyUsesRegisteredVanillaDimensionType() { + ResourceKey customDimension = ResourceKey.create( + Registries.DIMENSION, Identifier.parse("iris:custom_nether")); + + assertEquals(Blocks.NETHERRACK.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock( + customDimension, BuiltinDimensionTypes.NETHER, null, 64)); + assertEquals(Blocks.END_STONE.defaultBlockState(), + NativeStructureTerrainIntegrator.defaultEncaseBlock( + customDimension, BuiltinDimensionTypes.END, null, 64)); + } + + @Test + public void configuredEncasePaletteReplacesTheDefaultMaterial() { + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + Map blocks = new HashMap<>(); + IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff"); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), bounds, "minecraft:stronghold", start, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE) + .setEncasePalette(palette), + NativeStructurePostProcessorEncaseTest::tuffBlock); + + assertEquals(Blocks.TUFF.defaultBlockState(), + 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()) { + IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain( + start(adjustment, 64), null); + + assertEquals(IrisStructureTerrainMode.SOURCE, resolved.resolvedMode()); + } + } + + @Test + public void sourceBuryUsesTheVanillaSixByTwelveInfluenceEnvelope() { + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + int groundY = bounds.minY(); + BoundingBox area = new BoundingBox( + bounds.minX() - 7, groundY - 13, bounds.minZ(), + bounds.maxX(), groundY + 13, bounds.maxZ()); + Map blocks = new HashMap<>(); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "test:bury", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null); + + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX() - 5, groundY, bounds.minZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX(), groundY + 11, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX() - 6, groundY, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX(), groundY + 12, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX() - 7, groundY, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX(), groundY + 13, bounds.minZ())); + } + + @Test + public void sourceEncapsulateWrapsTheFullPieceBounds() { + StructureStart start = start(TerrainAdjustment.ENCAPSULATE, 64); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 13, bounds.minY(), bounds.minZ(), + bounds.maxX(), bounds.maxY() + 13, bounds.maxZ()); + Map blocks = new HashMap<>(); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "test:encapsulate", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null); + + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX() - 11, bounds.minY(), bounds.minZ())); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX(), bounds.maxY() + 11, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX() - 12, bounds.minY(), bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX(), bounds.maxY() + 12, bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX() - 13, bounds.minY(), bounds.minZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.minX(), bounds.maxY() + 13, bounds.minZ())); + } + + @Test + public void sourceTerrainDensityOnlyUsesRigidPoolPieces() { + PoolElementStructurePiece terrainMatching = poolPiece(StructureTemplatePool.Projection.TERRAIN_MATCHING); + PoolElementStructurePiece rigid = poolPiece(StructureTemplatePool.Projection.RIGID); + + assertFalse(NativeStructureTerrainIntegrator.isSourceRigidPiece(terrainMatching)); + assertTrue(NativeStructureTerrainIntegrator.isSourceRigidPiece(rigid)); + } + + @Test + public void sourceJunctionTerrainUsesTheStrictLowerTwelveBlockEnvelope() { + assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -1, 0)); + assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -11, 0)); + assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, 0, 0)); + assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -12, 0)); + assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(12, -1, 0)); + } + + @Test + public void explicitTerrainConfigurationWinsOverAutoEncase() { + IrisStructureTerrain configured = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.PRESERVE); + + assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain( + start(TerrainAdjustment.BURY, 64), configured)); + } + + @Test + public void legacyTemplateAirIsClearedAfterEveryTerrainFillPath() { + IrisStructureTerrain source = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.SOURCE); + IrisStructureTerrain preserve = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.PRESERVE); + IrisStructureTerrain encase = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE); + + assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.BURY, 64), source)); + assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.BEARD_BOX, 64), source)); + assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.NONE, 64), encase)); + assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.NONE, 64), source)); + assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start(TerrainAdjustment.BURY, 64), preserve)); + } + + @Test + public void encaseReservesTheNeighborChunkEnvelope() { + StructureStart generated = start(TerrainAdjustment.BURY, 64); + BoundingBox sourceBounds = generated.getBoundingBox(); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE) + .setHorizontalPadding(4); + + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, + generated.getStructure(), + 0, + terrain); + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + wrapped, wrapped.getStructure(), terrain); + + assertEquals(sourceBounds, wrapped.getBoundingBox()); + assertEquals(content.minX() - 4, referenceBounds.minX()); + assertEquals(content.maxX() + 4, referenceBounds.maxX()); + assertEquals(content.minZ() - 4, referenceBounds.minZ()); + assertEquals(content.maxZ() + 4, referenceBounds.maxZ()); + assertEquals(generated.getPieces().size(), wrapped.getPieces().size()); + } + + @Test + public void referenceEnvelopeRejectsChunkCoordinateOverflow() { + StructureStart generated = start(TerrainAdjustment.NONE, 64); + StructureStart extreme = new StructureStart( + generated.getStructure(), new ChunkPos(Integer.MAX_VALUE, 0), 0, + new PiecesContainer(generated.getPieces())); + + assertThrows(IllegalStateException.class, () -> + NativeStructureReferenceEnvelope.referenceBounds( + extreme, extreme.getStructure(), new IrisStructureTerrain())); + } + + @Test + public void explicitTerrainEnvelopeDoesNotInheritSourceAdjustment() { + StructureStart generated = start(TerrainAdjustment.BURY, 64); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE) + .setHorizontalPadding(112); + + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, generated.getStructure(), 0, terrain); + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + wrapped, wrapped.getStructure(), terrain); + + assertEquals(content.minX() - 112, referenceBounds.minX()); + assertEquals(content.maxX() + 112, referenceBounds.maxX()); + } + + @Test + public void yBandRelocatesTheStartMidpointDeterministically() { + IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20); + StructureStart first = start(TerrainAdjustment.BURY, 64); + StructureStart second = start(TerrainAdjustment.BURY, 64); + + NativeStructureVerticalPlacer.applyVerticalShift( + first, -64, -256, 320, true, false, band, (x, z) -> 40); + NativeStructureVerticalPlacer.applyVerticalShift( + second, -64, -256, 320, true, false, band, (x, z) -> 40); + + BoundingBox bounds = first.getBoundingBox(); + assertEquals(bounds.minY(), second.getBoundingBox().minY()); + assertTrue(bounds.minY() >= -120); + assertTrue(bounds.maxY() <= -20); + + int repeated = NativeStructureVerticalPlacer.applyVerticalShift( + first, -64, -256, 320, true, false, band, (x, z) -> 40); + assertEquals(0, repeated); + } + + @Test + public void yBandClampsWhenTheBandCannotContainTheStructure() { + IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45); + StructureStart start = start(TerrainAdjustment.BURY, 64); + + NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, -256, 320, true, false, band, (x, z) -> 40); + + BoundingBox bounds = start.getBoundingBox(); + int midpoint = bounds.minY() + (bounds.maxY() - bounds.minY()) / 2; + assertTrue(midpoint >= -50); + assertTrue(midpoint <= -45); + } + + @Test + public void yBandReplacesBothTheBlindShiftAndBurial() { + IrisStructureYBand band = new IrisStructureYBand().setMin(-64).setMax(-64); + StructureStart start = start(TerrainAdjustment.BURY, 64); + BoundingBox bounds = start.getBoundingBox(); + int height = bounds.maxY() - bounds.minY(); + + NativeStructureVerticalPlacer.applyVerticalShift( + start, -200, -256, 320, true, false, band, (x, z) -> 40); + + assertEquals(-64 - height / 2, start.getBoundingBox().minY()); + } + + @Test + public void preserveSourceYWinsOverTheConfiguredBand() { + IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20); + StructureStart start = start(TerrainAdjustment.BURY, 64); + int minY = start.getBoundingBox().minY(); + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, -256, 320, true, true, band, (x, z) -> 40); + + assertEquals(0, offset); + assertEquals(minY, start.getBoundingBox().minY()); + } + + private static BlockState tuffBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) { + assertNotNull(palette); + assertNotNull(rng); + return Blocks.TUFF.defaultBlockState(); + } + + private static StructureStart start(TerrainAdjustment adjustment, int minY) { + Structure structure = new DesertPyramidStructure(new Structure.StructureSettings( + HolderSet.empty(), Map.of(), GenerationStep.Decoration.STRONGHOLDS, adjustment)); + StructurePiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0); + piece.move(0, minY - piece.getBoundingBox().minY(), 0); + return new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + } + + private static PoolElementStructurePiece poolPiece(StructureTemplatePool.Projection projection) { + BlockPos position = new BlockPos(0, 64, 0); + StructurePoolElement element = StructurePoolElement.single( + "minecraft:empty", + Holder.direct(new StructureProcessorList(List.of( + BlockIgnoreProcessor.STRUCTURE_AND_AIR))), + LiquidSettings.APPLY_WATERLOGGING + ).apply(projection); + return new PoolElementStructurePiece( + null, + element, + position, + 0, + Rotation.NONE, + new BoundingBox(position), + LiquidSettings.APPLY_WATERLOGGING); + } + + private static WorldGenLevel world(Map blocks) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("getBlockState")) { + BlockPos position = (BlockPos) arguments[0]; + return state(blocks, position.getX(), position.getY(), position.getZ()); + } + if (methodName.equals("setBlock")) { + BlockPos position = (BlockPos) arguments[0]; + BlockState blockState = (BlockState) arguments[1]; + put(blocks, position.getX(), position.getY(), position.getZ(), blockState); + return true; + } + if (methodName.equals("getSeed")) { + return TEST_SEED; + } + if (methodName.equals("getLevel")) { + return null; + } + if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (methodName.equals("equals")) { + return proxy == arguments[0]; + } + if (methodName.equals("toString")) { + return "encase-test-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), new Class[]{WorldGenLevel.class}, handler); + } + + private static NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceSnapshot( + WorldGenLevel world, BoundingBox area) { + BitSet requiredLayers = new BitSet(area.getYSpan()); + requiredLayers.set(0, area.getYSpan()); + return NativeStructureTerrainIntegrator.SourceTerrainSnapshot.capture( + world, area, requiredLayers); + } + + private static void put(Map blocks, + int x, int y, int z, BlockState state) { + blocks.put(new BlockPos(x, y, z), state); + } + + private static BlockState state(Map blocks, int x, int y, int z) { + return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState()); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java new file mode 100644 index 000000000..ccb4971a8 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java @@ -0,0 +1,202 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.object.IrisStructureTerrain; +import art.arcane.iris.engine.object.IrisStructureTerrainMode; +import net.minecraft.SharedConstants; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class NativeStructurePostProcessorMonumentTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() { + StructureStart start = monumentStart(1337L); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0); + + assertEquals(0, offset); + assertEquals(39, start.getBoundingBox().minY()); + assertEquals(61, start.getBoundingBox().maxY()); + } + + @Test + public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() { + StructureStart start = monumentStart(1337L); + OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start); + List children = NativeStructureVerticalPlacer.monumentChildPieces(building); + int[] childMinY = minimumYs(children); + BoundingBox cachedBounds = start.getBoundingBox(); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); + + assertEquals(-13, offset); + assertSame(cachedBounds, start.getBoundingBox()); + assertEquals(26, building.getBoundingBox().minY()); + assertEquals(48, building.getBoundingBox().maxY()); + assertEquals(26, start.getBoundingBox().minY()); + assertEquals(48, start.getBoundingBox().maxY()); + assertTrue(children.size() > 20); + for (int i = 0; i < children.size(); i++) { + assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY()); + } + + int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); + assertEquals(0, repeatedOffset); + } + + @Test + public void configuredOffsetIsRelativeToTheActualSeaLevel() { + StructureStart start = monumentStart(1337L); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0); + + assertEquals(-10, offset); + assertEquals(29, start.getBoundingBox().minY()); + assertEquals(51, start.getBoundingBox().maxY()); + } + + @Test + public void referenceEnvelopeDoesNotAddMonumentPieces() { + StructureStart start = monumentStart(1337L); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + start, + start.getStructure(), + 0, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(4)); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + wrapped, "minecraft:monument", 0, 63, -64, 320, + false, false, null, (x, z) -> 0); + + assertEquals(0, offset); + assertEquals(1, wrapped.getPieces().size()); + } + + @Test + public void vanillaReloadRegenerationIsRealignedBeforePlacement() { + long seed = 1337L; + ChunkPos chunkPos = new ChunkPos(0, 0); + StructureStart initial = monumentStart(seed); + NativeStructureVerticalPlacer.applyVerticalPlacement( + initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); + + PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad( + chunkPos, seed, new PiecesContainer(initial.getPieces())); + StructureStart reloaded = new StructureStart( + monumentStructure(), chunkPos, 0, regenerated); + + assertEquals(39, reloaded.getBoundingBox().minY()); + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); + assertEquals(-13, offset); + assertEquals(26, reloaded.getBoundingBox().minY()); + assertEquals(48, reloaded.getBoundingBox().maxY()); + } + + @Test + public void neighboringChunkPlacementRealignsReloadedMonumentOnce() throws Exception { + long seed = 1337L; + ChunkPos chunkPos = new ChunkPos(0, 0); + StructureStart initial = monumentStart(seed); + NativeStructureVerticalPlacer.applyVerticalPlacement( + initial, "minecraft:monument", 0, 50, -256, 512, + false, false, null, (x, z) -> 0); + PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad( + chunkPos, seed, new PiecesContainer(initial.getPieces())); + StructureStart reloaded = new StructureStart( + monumentStructure(), chunkPos, 0, regenerated); + ExecutorService executor = Executors.newFixedThreadPool(8); + CountDownLatch release = new CountDownLatch(1); + List> futures = new ArrayList<>(); + try { + for (int task = 0; task < 16; task++) { + futures.add(executor.submit(() -> { + release.await(); + NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment( + reloaded, "minecraft:monument", 0, 50, -256, 512); + return null; + })); + } + release.countDown(); + for (Future future : futures) { + future.get(); + } + } finally { + executor.shutdownNow(); + } + + assertEquals(26, reloaded.getBoundingBox().minY()); + assertEquals(48, reloaded.getBoundingBox().maxY()); + } + + @Test + public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() { + StructureStart start = monumentStart(1337L); + try { + NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0); + } catch (IllegalStateException error) { + assertTrue(error.getMessage().contains("cannot align")); + return; + } + throw new AssertionError("Expected an out-of-bounds monument alignment to fail"); + } + + private static StructureStart monumentStart(long seed) { + ChunkPos chunkPos = new ChunkPos(0, 0); + OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding( + RandomSource.create(seed), -29, -29, Direction.NORTH); + return new StructureStart( + monumentStructure(), chunkPos, 0, new PiecesContainer(List.of(building))); + } + + private static OceanMonumentStructure monumentStructure() { + return new OceanMonumentStructure(new Structure.StructureSettings(HolderSet.empty())); + } + + private static OceanMonumentPieces.MonumentBuilding monumentBuilding(StructureStart start) { + return (OceanMonumentPieces.MonumentBuilding) start.getPieces().get(0); + } + + private static int[] minimumYs(List pieces) { + int[] ys = new int[pieces.size()]; + for (int i = 0; i < pieces.size(); i++) { + ys[i] = pieces.get(i).getBoundingBox().minY(); + } + return ys; + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java new file mode 100644 index 000000000..c560dc1b4 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java @@ -0,0 +1,198 @@ +package art.arcane.iris.nativegen; + +import net.minecraft.SharedConstants; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; +import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece; +import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class NativeStructurePostProcessorScatteredFeatureTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void desertPyramidUsesTheLowestFootprintSurfaceAndLocksVanillaReanchor() { + TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(11L), 0, 0); + StructureStart start = desertStart(piece); + BoundingBox cachedBounds = start.getBoundingBox(); + BoundingBox footprint = piece.getBoundingBox(); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, + (x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92); + + assertEquals(1, offset); + assertSame(cachedBounds, start.getBoundingBox()); + assertEquals(65, piece.getBoundingBox().minY()); + assertEquals(65, start.getBoundingBox().minY()); + assertTrue(piece.attemptVanillaAlignment(-2)); + assertEquals(65, piece.getBoundingBox().minY()); + } + + @Test + public void jungleTempleUsesAverageFootprintSurfaceAndHonorsConfiguredShift() { + TestJungleTemplePiece piece = new TestJungleTemplePiece(RandomSource.create(19L), 32, -16); + StructureStart start = jungleStart(piece); + BoundingBox footprint = piece.getBoundingBox(); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null, + (x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70); + + assertEquals(11, offset); + assertEquals(75, piece.getBoundingBox().minY()); + assertTrue(piece.attemptVanillaAlignment()); + assertEquals(75, piece.getBoundingBox().minY()); + } + + @Test + public void repeatedAlignmentIsIdempotent() { + TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0); + StructureStart start = desertStart(piece); + + int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80); + int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80); + + assertEquals(17, initialOffset); + assertEquals(0, repeatedOffset); + assertEquals(81, piece.getBoundingBox().minY()); + } + + @Test + public void scatteredAlignmentStaysInsideWorldBounds() { + TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0); + StructureStart start = desertStart(piece); + + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318); + + assertEquals(241, offset); + assertEquals(305, piece.getBoundingBox().minY()); + assertEquals(319, start.getBoundingBox().maxY()); + } + + @Test + public void scatteredStructuresDoNotOptIntoFullFootprintSurfaceFitting() { + TestDesertPyramidPiece desert = new TestDesertPyramidPiece(RandomSource.create(31L), 0, 0); + TestJungleTemplePiece jungle = new TestJungleTemplePiece(RandomSource.create(37L), 0, 0); + SwampHutPiece swamp = new SwampHutPiece(RandomSource.create(41L), 0, 0); + StructureStart desertStart = desertStart(desert); + StructureStart jungleStart = jungleStart(jungle); + StructureStart swampStart = swampStart(swamp); + + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart)); + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart)); + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart)); + + AtomicInteger terrainQueries = new AtomicInteger(); + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( + swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null, + (x, z) -> terrainQueries.incrementAndGet()); + assertEquals(0, offset); + assertEquals(0, terrainQueries.get()); + assertEquals(64, swamp.getBoundingBox().minY()); + } + + @Test + public void scatteredHeightFieldMatchesTheRuntimeContract() { + Field field = NativeStructureReflection.resolveScatteredHeightPositionField(); + + assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass()); + assertEquals(int.class, field.getType()); + assertTrue(Modifier.isProtected(field.getModifiers())); + assertFalse(Modifier.isFinal(field.getModifiers())); + assertFalse(Modifier.isStatic(field.getModifiers())); + assertTrue(field.trySetAccessible()); + } + + @Test + public void datapackOverridesOfSpecialVanillaIdsUseGenericPlacement() { + StructureStart pyramidOverride = swampStart( + new SwampHutPiece(RandomSource.create(43L), 0, 0)); + int pyramidMinY = pyramidOverride.getBoundingBox().minY(); + int pyramidOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( + pyramidOverride, "minecraft:desert_pyramid", 5, + 63, -64, 320, false, false, null, (x, z) -> 200); + + assertEquals(5, pyramidOffset); + assertEquals(pyramidMinY + 5, pyramidOverride.getBoundingBox().minY()); + + StructureStart monumentOverride = swampStart( + new SwampHutPiece(RandomSource.create(47L), 0, 0)); + int monumentMinY = monumentOverride.getBoundingBox().minY(); + NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment( + monumentOverride, "minecraft:monument", 0, 40, -64, 320); + + assertEquals(monumentMinY, monumentOverride.getBoundingBox().minY()); + } + + private static StructureStart desertStart(StructurePiece piece) { + Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(HolderSet.empty())); + return start(structure, piece); + } + + private static StructureStart jungleStart(StructurePiece piece) { + Structure structure = new JungleTempleStructure(new Structure.StructureSettings(HolderSet.empty())); + return start(structure, piece); + } + + private static StructureStart swampStart(StructurePiece piece) { + Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); + return start(structure, piece); + } + + private static StructureStart start(Structure structure, StructurePiece piece) { + return new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + } + + private static final class TestDesertPyramidPiece extends DesertPyramidPiece { + private TestDesertPyramidPiece(RandomSource random, int west, int north) { + super(random, west, north); + } + + private boolean attemptVanillaAlignment(int offset) { + return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, offset); + } + } + + private static final class TestJungleTemplePiece extends JungleTemplePiece { + private TestJungleTemplePiece(RandomSource random, int west, int north) { + super(random, west, north); + } + + private boolean attemptVanillaAlignment() { + return updateAverageGroundHeight((LevelAccessor) null, null, 0); + } + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java new file mode 100644 index 000000000..cd8704091 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java @@ -0,0 +1,2375 @@ +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 com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.IdMapper; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerFactory; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.Strategy; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction; +import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.AlwaysTrueTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlockStateMatchTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +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.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +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.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +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.assertNotSame; +import static org.junit.Assert.assertSame; +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_MIN_Y = 64; + private static final int SLAB_PADDING = 14; + private static final int SLAB_WIDTH = 16; + private static final long TEST_SEED = 8675309L; + + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void sourceBeardAdjustmentsPrepareOnlyAtTheSurfaceStructuresStep() { + assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.SURFACE_STRUCTURES)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.FLUID_SPRINGS)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.UNDERGROUND_STRUCTURES)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.UNDERGROUND_DECORATION)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.STRONGHOLDS)); + } + + @Test + public void nonBeardAdjustmentsDoNotPrepareSurfaceTerrain() { + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.BURY, GenerationStep.Decoration.SURFACE_STRUCTURES)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.ENCAPSULATE, GenerationStep.Decoration.SURFACE_STRUCTURES)); + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( + TerrainAdjustment.NONE, GenerationStep.Decoration.FLUID_SPRINGS)); + } + + @Test + public void shallowUndergroundBeardBoxDoesNotMutateTheTopSurface() throws Exception { + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 1, 0, Blocks.DEEPSLATE_BRICKS.defaultBlockState())))), + new BoundingBox(0, 67, 0, 0, 74, 0), 1, Rotation.NONE); + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.UNDERGROUND_DECORATION, + TerrainAdjustment.BEARD_BOX)); + StructureStart start = new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of(piece))); + BoundingBox area = new BoundingBox(0, 48, 0, 0, 80, 0); + Map blocks = flatTerrain(area, 64); + Map before = new HashMap<>(blocks); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(surfaceTarget(start)), + (x, z) -> 64); + + assertEquals(before, blocks); + } + + @Test + public void explicitTerrainOverrideDisablesSourceBeardFitting() { + StructureStart start = desertStart(TerrainAdjustment.BEARD_BOX); + + assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain( + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:beard_box", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE)))); + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain( + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:beard_box", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.PRESERVE)))); + } + + @Test + public void explicitVacuumUsesIndependentTerrainFittingWithoutAuthoredAdaptation() { + StructureStart none = desertStart(TerrainAdjustment.NONE); + StructureStart box = desertStart(TerrainAdjustment.BEARD_BOX); + NativeStructureTerrainIntegrator.TerrainTarget sourceNone = + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:none", none, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE)); + NativeStructureTerrainIntegrator.TerrainTarget vacuumNone = + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:none", none, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM)); + NativeStructureTerrainIntegrator.TerrainTarget vacuumBox = + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:box", box, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM)); + + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(sourceNone)); + assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(vacuumNone)); + assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(vacuumBox)); + assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + none, vacuumNone.terrain())); + } + + @Test + public void sourceSurfaceAnchorMeetsOneBlockBelowTheAuthoredGroundPlane() { + BoundingBox bounds = new BoundingBox(0, 64, 0, 4, 90, 4); + NativeStructureSurfaceFitter.SurfaceAnchor anchor = + NativeStructureSurfaceFitter.surfaceAnchor(bounds, 68, 2); + + assertEquals(67, anchor.meetY()); + assertEquals(0, anchor.minX()); + assertEquals(4, anchor.maxX()); + assertEquals(0, anchor.minZ()); + assertEquals(4, anchor.maxZ()); + } + + @Test + public void rigidFootprintsBoundTerrainAcrossGapsLargerThanTheBeardKernel() { + NativeStructureSurfaceFitter.SurfaceAnchor rigid = anchor(72, 2); + NativeStructureSurfaceFitter.SurfaceAnchor junction = anchor(72, 1); + + assertEquals(46, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(rigid), 2, 2, 40)); + assertEquals(46, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(junction), 2, 2, 40)); + } + + @Test + public void beardSkirtBoundsAndGradesUpwardAndDownwardSymmetrically() { + int originalY = 64; + NativeStructureSurfaceFitter.SurfaceAnchor high = anchor(96, 2); + NativeStructureSurfaceFitter.SurfaceAnchor low = anchor(32, 2); + int[] raised = new int[13]; + int[] lowered = new int[13]; + for (int outset = 0; outset <= 12; outset++) { + int x = 4 + outset; + raised[outset] = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(high), x, 2, originalY); + lowered[outset] = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(low), x, 2, originalY); + } + + assertArrayEquals( + new int[]{70, 69, 68, 67, 67, 66, 66, 65, 65, 64, 64, 64, 64}, + raised); + assertArrayEquals( + new int[]{58, 59, 60, 61, 61, 62, 62, 63, 63, 64, 64, 64, 64}, + lowered); + for (int outset = 0; outset <= 12; outset++) { + assertEquals(raised[outset] - originalY, originalY - lowered[outset]); + } + } + + @Test + public void projectedCenterAndRigidChildrenUseProcessedFoundationCells() throws Exception { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + TerrainAdjustment.BEARD_BOX)); + PoolElementStructurePiece center = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(1, 1, 1, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 71, 0, 3, 80, 3), 1, Rotation.NONE); + PoolElementStructurePiece child = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(1, 7, 1, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(10, 65, 0, 13, 76, 3), 7, Rotation.NONE); + StructureStart start = new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of(center, child))); + BoundingBox area = new BoundingBox(0, 60, 0, 15, 90, 15); + Map blocks = new HashMap<>(); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + put(blocks, x, 64, z, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 65, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + } + + List targets = List.of( + new NativeStructureTerrainIntegrator.TerrainTarget( + "nova_structures:tavern_oak", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, targets, + (x, z) -> 65); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 1, 71, 1)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 11, 71, 1)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 11, 70, 1)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 2, 65, 2)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 12, 65, 2)); + } + + @Test + public void sparseRigidFootprintsIgnoreAirAndSolidsAboveTheGroundPlane() throws Exception { + StructureTemplate sparseTemplate = template(List.of( + block(0, 1, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(15, 1, 0, Blocks.AIR.defaultBlockState()), + block(30, 5, 0, Blocks.STONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(sparseTemplate), + new BoundingBox(0, 68, 0, 30, 76, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(piece), TerrainAdjustment.BEARD_THIN); + BoundingBox area = new BoundingBox(0, 52, 0, 30, 80, 0); + Map blocks = flatTerrain(area, 64); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(surfaceTarget(start)), + (x, z) -> 64); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 68, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 64, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 30, 64, 0)); + } + + @Test + public void extremeRigidFoundationGapDoesNotCreateATerrainPillar() throws Exception { + StructureTemplate template = template(List.of( + block(0, 1, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template), + new BoundingBox(0, 90, 0, 0, 96, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(piece), TerrainAdjustment.BEARD_THIN); + BoundingBox area = new BoundingBox(0, 30, 0, 0, 100, 0); + Map blocks = flatTerrain(area, 40); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(surfaceTarget(start)), + (x, z) -> 40); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 46, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 47, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 89, 0)); + } + + @Test + public void terrainMatchingFootprintsFollowProcessedLowestSolidColumns() throws Exception { + StructureTemplate pathTemplate = template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(0, 2, 0, Blocks.DIRT.defaultBlockState()), + block(15, 0, 0, Blocks.AIR.defaultBlockState()), + block(30, 0, 0, Blocks.DIRT.defaultBlockState()), + block(45, 0, 0, Blocks.DANDELION.defaultBlockState()))); + InlineSinglePoolElement element = new InlineSinglePoolElement( + pathTemplate, List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING); + PoolElementStructurePiece piece = rigidTemplatePiece( + element, new BoundingBox(0, 68, 0, 45, 74, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(piece), TerrainAdjustment.BEARD_THIN); + BoundingBox area = new BoundingBox(0, 52, 0, 45, 76, 0); + Map blocks = flatTerrain(area, 64); + blocks.remove(new BlockPos(0, 63, 0)); + blocks.remove(new BlockPos(0, 64, 0)); + put(blocks, 0, 59, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 60, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 30, 71, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 30, 72, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(surfaceTarget(start)), + (x, z) -> x == 0 ? 60 : x == 30 ? 72 : 64); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 61, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 30, 71, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 30, 72, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 64, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 65, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 45, 64, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 45, 65, 0)); + } + + @Test + public void vacuumFitsOnlyProcessedSolidTemplateColumns() throws Exception { + StructureTemplate sparseTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(15, 0, 0, Blocks.STONE.defaultBlockState()))); + InlineSinglePoolElement element = new InlineSinglePoolElement( + sparseTemplate, List.of(replaceBlockProcessor( + Blocks.STONE.defaultBlockState(), Blocks.AIR.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + element, new BoundingBox(0, 64, 0, 15, 70, 3), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 56, 0, 15, 72, 3); + Map blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 64, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 60, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 61, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 64, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 60, 3)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 64, 3)); + } + + @Test + public void vacuumSupportsGroundDeltaZeroFoundations() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template), + new BoundingBox(0, 66, 0, 0, 70, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 3, 72, 3); + Map blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 65, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 64, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 66, 0)); + } + + @Test + public void vacuumFindsActualTerrainBelowNominalOpenAirSurface() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template), + new BoundingBox(0, 75, 0, 0, 81, 0), 2, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 3, 84, 3); + Map blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 80); + + for (int y = 61; y <= 74; y++) { + assertFalse(state(blocks, 0, y, 0).isAir()); + } + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 74, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 75, 0)); + } + + @Test + public void vacuumSupportsFoundationBelowHigherNaturalSurface() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineLegacyPoolElement(template), + new BoundingBox(0, 75, 0, 0, 81, 0), 2, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 15, 84, 3); + Map blocks = flatTerrain(area, 60); + put(blocks, 0, 78, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 79, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 79); + + for (int y = 61; y <= 74; y++) { + assertFalse(state(blocks, 0, y, 0).isAir()); + } + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 75, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 78, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 79, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 60, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 61, 0)); + } + + @Test + public void vacuumRepairUsesOnlyProcessedSolidFoundationColumns() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.STONE.defaultBlockState()), + block(2, 0, 0, Blocks.AIR.defaultBlockState()), + block(2, 2, 0, Blocks.COBBLESTONE.defaultBlockState()))); + InlineSinglePoolElement element = new InlineSinglePoolElement( + template, List.of(replaceBlockProcessor( + Blocks.STONE.defaultBlockState(), Blocks.AIR.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + element, new BoundingBox(0, 65, 0, 2, 72, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 3, 74, 3); + Map blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.VacuumFoundationPlan plan = + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + for (int y = 61; y <= 64; y++) { + put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState()); + } + put(blocks, 0, 65, 0, Blocks.COBBLESTONE.defaultBlockState()); + put(blocks, 1, 65, 0, Blocks.COBBLESTONE.defaultBlockState()); + put(blocks, 2, 67, 0, Blocks.COBBLESTONE.defaultBlockState()); + BlockState processorAirSupport = state(blocks, 1, 64, 0); + BlockState authoredAirSupport = state(blocks, 2, 66, 0); + + NativeStructureSurfaceFitter.repairVacuumFoundations( + world(blocks), area, plan); + + for (int y = 61; y <= 64; y++) { + assertFalse(state(blocks, 0, y, 0).isAir()); + } + assertEquals(processorAirSupport, state(blocks, 1, 64, 0)); + assertEquals(authoredAirSupport, state(blocks, 2, 66, 0)); + } + + @Test + public void vacuumRepairPreservesOverlappingAuthoredInterior() throws Exception { + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(0, 1, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 61, 0, 0, 62, 0), 0, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 65, 0, 0, 70, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(lower, upper), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 3, 72, 3); + Map blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.VacuumFoundationPlan plan = + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + put(blocks, 0, 65, 0, Blocks.COBBLESTONE.defaultBlockState()); + + NativeStructureSurfaceFitter.repairVacuumFoundations( + world(blocks), area, plan); + + for (int y = 61; y <= 64; y++) { + assertTrue(state(blocks, 0, y, 0).isAir()); + } + } + + @Test + public void vacuumRepairIsStableAcrossSplitChunkAreas() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineLegacyPoolElement(template), + new BoundingBox(15, 65, 0, 16, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + NativeStructureTerrainIntegrator.TerrainTarget target = + surfaceTarget(start, IrisStructureTerrainMode.VACUUM); + BoundingBox wideArea = new BoundingBox(0, 54, 0, 31, 72, 15); + BoundingBox westArea = new BoundingBox(0, 54, 0, 15, 72, 15); + BoundingBox eastArea = new BoundingBox(16, 54, 0, 31, 72, 15); + Map wideBlocks = flatTerrain(wideArea, 60); + Map splitBlocks = flatTerrain(wideArea, 60); + + NativeStructureSurfaceFitter.VacuumFoundationPlan widePlan = + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(wideBlocks), wideArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.VacuumFoundationPlan westPlan = + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(splitBlocks), westArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.VacuumFoundationPlan eastPlan = + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(splitBlocks), eastArea, List.of(target), (x, z) -> 60); + for (int x = 15; x <= 16; x++) { + for (int y = 61; y <= 64; y++) { + put(wideBlocks, x, y, 0, Blocks.CAVE_AIR.defaultBlockState()); + put(splitBlocks, x, y, 0, Blocks.CAVE_AIR.defaultBlockState()); + } + put(wideBlocks, x, 65, 0, Blocks.COBBLESTONE.defaultBlockState()); + put(splitBlocks, x, 65, 0, Blocks.COBBLESTONE.defaultBlockState()); + } + + NativeStructureSurfaceFitter.repairVacuumFoundations( + world(wideBlocks), wideArea, widePlan); + NativeStructureSurfaceFitter.repairVacuumFoundations( + world(splitBlocks), eastArea, eastPlan); + NativeStructureSurfaceFitter.repairVacuumFoundations( + world(splitBlocks), westArea, westPlan); + + assertEquals(wideBlocks, splitBlocks); + } + + @Test + public void vacuumNeverLowersExistingTerrainAroundSparsePieces() throws Exception { + StructureTemplate sparseTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(sparseTemplate), + new BoundingBox(0, 56, 0, 7, 62, 7), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 48, 0, 7, 70, 7); + Map blocks = flatTerrain(area, 60); + Map originalBlocks = new HashMap<>(blocks); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + assertEquals(originalBlocks, blocks); + } + + @Test + public void vacuumDoesNotRaiseTerrainToRoofsAboveLegacyAir() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(0, 2, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineLegacyPoolElement(template), + new BoundingBox(0, 66, 0, 7, 72, 7), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 15, 74, 15); + Map blocks = flatTerrain(area, 60); + Map originalBlocks = new HashMap<>(blocks); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + assertEquals(originalBlocks, blocks); + } + + @Test + public void vacuumCapsNeighborTaperAtAuthoredAirGroundPlanes() throws Exception { + PoolElementStructurePiece roof = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(0, 2, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(1, 66, 0, 1, 72, 0), 1, Rotation.NONE); + PoolElementStructurePiece highChild = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 90, 0, 0, 94, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(roof, highChild), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 15, 96, 15); + Map blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 1, 66, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 1, 67, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 89, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 90, 0)); + } + + @Test + public void vacuumDoesNotAnchorRoofsAboveProcessorCreatedAir() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.STONE.defaultBlockState()), + block(0, 2, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template, List.of(replaceBlockProcessor( + Blocks.STONE.defaultBlockState(), Blocks.AIR.defaultBlockState()))), + new BoundingBox(0, 66, 0, 7, 72, 7), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 15, 74, 15); + Map blocks = flatTerrain(area, 60); + Map originalBlocks = new HashMap<>(blocks); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + assertEquals(originalBlocks, blocks); + } + + @Test + public void vacuumDoesNotTreatSparseRoofCellsAsFoundations() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.SANDSTONE.defaultBlockState()), + block(1, 20, 0, Blocks.SANDSTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template), + new BoundingBox(0, 66, 0, 20, 100, 20), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 20, 102, 20); + Map blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 65, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 66, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 1, 67, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 1, 86, 0)); + } + + @Test + public void vacuumDoesNotFlattenTerrainMatchingPoolPieces() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement( + template, List.of(), + StructureTemplatePool.Projection.TERRAIN_MATCHING), + new BoundingBox(0, 68, 0, 15, 72, 15), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 15, 74, 15); + Map blocks = flatTerrain(area, 60); + Map originalBlocks = new HashMap<>(blocks); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + assertEquals(originalBlocks, blocks); + } + + @Test + public void vacuumRetainsJunctionSmoothingForRigidFeaturePieces() { + StructurePoolElement feature = StructurePoolElement.feature( + Holder.direct(null)) + .apply(StructureTemplatePool.Projection.RIGID); + PoolElementStructurePiece piece = rigidTemplatePiece( + feature, new BoundingBox(4, 66, 4, 4, 70, 4), 1, Rotation.NONE); + piece.addJunction(new JigsawJunction( + 4, 66, 4, 0, StructureTemplatePool.Projection.RIGID)); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 15, 72, 15); + Map blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 4, 61, 4)); + } + + @Test + public void vacuumDoesNotInventNonPoolBoundingBoxFootprints() { + StructureStart start = desertStart(TerrainAdjustment.NONE); + BoundingBox bounds = start.getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 12, 40, bounds.minZ() - 12, + bounds.maxX() + 12, bounds.maxY() + 12, bounds.maxZ() + 12); + Map blocks = flatTerrain(area, 50); + Map originalBlocks = new HashMap<>(blocks); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 50); + + assertEquals(originalBlocks, blocks); + } + + @Test + public void vacuumBudgetExhaustionSkipsFittingWithoutFailing() throws Exception { + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 66, 0, 0, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 15, 72, 15); + Map blocks = flatTerrain(area, 60); + Map originalBlocks = new HashMap<>(blocks); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60, 1); + + assertEquals(originalBlocks, blocks); + } + + @Test + public void vacuumContinuouslySupportsProcessedChildBases() throws Exception { + PoolElementStructurePiece center = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 60, 0, 0, 64, 0), 1, Rotation.NONE); + StructureTemplate childTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece child = rigidTemplatePiece( + new InlineSinglePoolElement(childTemplate), + new BoundingBox(10, 66, 0, 10, 72, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(center, child), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 54, 0, 15, 74, 3); + Map blocks = flatTerrain(area, 60); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, + List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + (x, z) -> 60); + + for (int y = 61; y <= 65; y++) { + assertFalse(state(blocks, 10, y, 0).isAir()); + } + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 10, 65, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 10, 66, 0)); + } + + @Test + public void vacuumSurfaceFittingIsStableAcrossSplitChunkAreas() throws Exception { + StructureTemplate template = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece piece = rigidTemplatePiece( + new InlineSinglePoolElement(template), + new BoundingBox(15, 66, 0, 16, 72, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); + NativeStructureTerrainIntegrator.TerrainTarget target = + surfaceTarget(start, IrisStructureTerrainMode.VACUUM); + BoundingBox wideArea = new BoundingBox(0, 54, 0, 31, 74, 15); + BoundingBox westArea = new BoundingBox(0, 54, 0, 15, 74, 15); + BoundingBox eastArea = new BoundingBox(16, 54, 0, 31, 74, 15); + Map wideBlocks = flatTerrain(wideArea, 60); + Map splitBlocks = flatTerrain(wideArea, 60); + Map reverseSplitBlocks = flatTerrain(wideArea, 60); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(wideBlocks), wideArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(splitBlocks), westArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(splitBlocks), eastArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(reverseSplitBlocks), eastArea, List.of(target), (x, z) -> 60); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(reverseSplitBlocks), westArea, List.of(target), (x, z) -> 60); + + assertEquals(wideBlocks, splitBlocks); + assertEquals(wideBlocks, reverseSplitBlocks); + } + + @Test + public void surfaceAnchorCapsInsideAndIsUnchangedAtRadius() { + NativeStructureSurfaceFitter.SurfaceAnchor anchor = anchor(72, 2); + + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(anchor), 2, 2, 64)); + assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(anchor), 16, 2, 64)); + assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(anchor), 17, 2, 64)); + } + + @Test + public void surfaceAnchorRaisesAndLowersThroughTheTaper() { + NativeStructureSurfaceFitter.SurfaceAnchor raised = anchor(68, 2); + NativeStructureSurfaceFitter.SurfaceAnchor lowered = anchor(64, 2); + + assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(raised), 10, 2, 64)); + assertEquals(67, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(lowered), 10, 2, 68)); + } + + @Test + public void containingRigidFloorsHaveDeterministicPriority() { + NativeStructureSurfaceFitter.SurfaceAnchor rigid = anchor(70, 2); + NativeStructureSurfaceFitter.SurfaceAnchor junction = anchor(74, 1); + + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(rigid, junction), 2, 2, 64)); + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(junction, rigid), 2, 2, 64)); + + NativeStructureSurfaceFitter.SurfaceAnchor weakTie = anchor(58, 1); + NativeStructureSurfaceFitter.SurfaceAnchor strongTie = anchor(70, 2); + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(weakTie, strongTie), 2, 2, 64)); + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(strongTie, weakTie), 2, 2, 64)); + } + + @Test + public void postClearSupportUsesUpperOccupancyOverLowerLegacyAir() throws Exception { + StructureTemplate lowerTemplate = template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(1, 0, 0, Blocks.AIR.defaultBlockState()))); + StructureTemplate upperTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(lowerTemplate), + new BoundingBox(0, 62, 0, 1, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineLegacyPoolElement(upperTemplate), + new BoundingBox(0, 64, 0, 1, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 1, 72, 0); + Map blocks = new HashMap<>(); + for (int x = 0; x <= 1; x++) { + put(blocks, x, 61, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 62, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + put(blocks, 1, 63, 0, Blocks.DIRT.defaultBlockState()); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 1, 63, 0)); + } + + @Test + public void rotatedLowestAuthoredVoidFluidAirAndJigsawColumnsRemainOpen() throws Exception { + List upperBlocks = new ArrayList<>(); + upperBlocks.add(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())); + upperBlocks.add(block(1, 0, 0, Blocks.AIR.defaultBlockState())); + upperBlocks.add(block(2, 0, 0, Blocks.STRUCTURE_VOID.defaultBlockState())); + upperBlocks.add(block(3, 0, 0, Blocks.WATER.defaultBlockState())); + upperBlocks.add(block(4, 0, 0, Blocks.JIGSAW.defaultBlockState())); + for (int x = 1; x <= 4; x++) { + upperBlocks.add(block(x, 1, 0, Blocks.STONE.defaultBlockState())); + } + StructureTemplate upperTemplate = template(upperBlocks); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(10, 62, 10, 10, 65, 14), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(upperTemplate), + new BoundingBox(10, 64, 10, 10, 70, 14), 1, + Rotation.CLOCKWISE_90); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(10, 58, 10, 10, 72, 14); + Map blocks = new HashMap<>(); + for (int z = 10; z <= 14; z++) { + put(blocks, 10, 61, z, Blocks.DIRT.defaultBlockState()); + put(blocks, 10, 62, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + StructurePlaceSettings settings = new StructurePlaceSettings() + .setRotation(Rotation.CLOCKWISE_90); + BlockPos origin = upper.getPosition(); + BlockPos supportedBase = origin.offset( + StructureTemplate.calculateRelativePosition(settings, BlockPos.ZERO)); + assertEquals(Set.of(supportedBase.below().asLong()), written); + assertEquals(Blocks.DIRT.defaultBlockState(), state( + blocks, supportedBase.getX(), supportedBase.getY() - 1, supportedBase.getZ())); + for (int localX = 1; localX <= 4; localX++) { + BlockPos vetoedBase = origin.offset(StructureTemplate.calculateRelativePosition( + settings, new BlockPos(localX, 0, 0))); + assertEquals(Blocks.AIR.defaultBlockState(), state( + blocks, vetoedBase.getX(), vetoedBase.getY() - 1, vetoedBase.getZ())); + } + } + + @Test + public void listElementsUseDeclaredOverlayOrderForLowestAuthoredCells() throws Exception { + InlineLegacyPoolElement legacy = new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))); + InlineSinglePoolElement single = new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(1, 0, 0, Blocks.STONE.defaultBlockState())))); + ListPoolElement list = new ListPoolElement( + List.of(legacy, single), StructureTemplatePool.Projection.RIGID); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 1, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + list, new BoundingBox(0, 64, 0, 1, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 1, 72, 0); + Map blocks = new HashMap<>(); + for (int x = 0; x <= 1; x++) { + put(blocks, x, 61, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 62, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(1, 63, 0)), written); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 1, 63, 0)); + } + + @Test + public void surfaceSupportIsChunkClippedAndStableAcrossSplitAreas() throws Exception { + StructureTemplate upperTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(15, 62, 0, 16, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(upperTemplate), + new BoundingBox(15, 64, 0, 16, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox wideArea = new BoundingBox(0, 58, 0, 31, 72, 15); + BoundingBox westArea = new BoundingBox(0, 58, 0, 15, 72, 15); + BoundingBox eastArea = new BoundingBox(16, 58, 0, 31, 72, 15); + Map wideBlocks = supportTerrain(15, 16); + Map splitBlocks = supportTerrain(15, 16); + + Set wide = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(wideBlocks), wideArea, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + Set split = new HashSet<>( + NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(splitBlocks), westArea, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager)); + assertEquals(Blocks.AIR.defaultBlockState(), state(splitBlocks, 16, 63, 0)); + split.addAll(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(splitBlocks), eastArea, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager)); + + assertEquals(Set.of( + BlockPos.asLong(15, 63, 0), BlockPos.asLong(16, 63, 0)), wide); + assertEquals(wide, split); + assertEquals(wideBlocks, splitBlocks); + } + + @Test + public void supportNeverPairsRigidAnchorsAcrossStarts() 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), 1, Rotation.NONE); + StructureStart lowerStart = rigidSurfaceStart(List.of(lower)); + StructureStart upperStart = rigidSurfaceStart(List.of(upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(lowerStart)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty()); + assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(upperStart)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty()); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 63, 0)); + } + + @Test + public void vacuumSupportsUnauthoredTerrainModesButLongMeetGapsRemainOpen() throws Exception { + StructureTemplate upperTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()))); + 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(upperTemplate), + new BoundingBox(0, 64, 0, 0, 70, 0), 1, Rotation.NONE); + StructureStart vacuumStart = rigidSurfaceStart( + List.of(lower, upper), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map vacuumBlocks = supportTerrain(0, 0); + + Set vacuumWritten = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(vacuumBlocks), area, + List.of(surfaceTarget(vacuumStart, IrisStructureTerrainMode.VACUUM)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), vacuumWritten); + assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(supportTerrain(0, 0)), area, + List.of(surfaceTarget(vacuumStart, IrisStructureTerrainMode.SOURCE)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty()); + + PoolElementStructurePiece distantUpper = rigidTemplatePiece( + new InlineSinglePoolElement(upperTemplate), + new BoundingBox(0, 65, 0, 0, 71, 0), 1, Rotation.NONE); + StructureStart distantStart = rigidSurfaceStart( + List.of(lower, distantUpper), TerrainAdjustment.NONE); + assertTrue(NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(supportTerrain(0, 0)), area, + List.of(surfaceTarget(distantStart, IrisStructureTerrainMode.VACUUM)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager).isEmpty()); + } + + @Test + public void threeRigidPlanesCannotBuildAnUpwardSupportChain() 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 middle = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 69, 0), 0, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.STONE.defaultBlockState())))), + new BoundingBox(0, 65, 0, 0, 70, 0), 0, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, middle, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 64, 0)); + } + + @Test + public void reversingTargetsCannotCreateCrossStartSupportChains() throws Exception { + PoolElementStructurePiece ground = 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 middle = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 69, 0), 0, Rotation.NONE); + PoolElementStructurePiece raisedGround = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 64, 0, 0, 69, 0), 0, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.STONE.defaultBlockState())))), + new BoundingBox(0, 65, 0, 0, 70, 0), 0, Rotation.NONE); + StructureStart lowerStart = rigidSurfaceStart(List.of(ground, middle)); + StructureStart upperStart = rigidSurfaceStart(List.of(raisedGround, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map forwardBlocks = supportTerrain(0, 0); + Map reverseBlocks = supportTerrain(0, 0); + NativeStructureTerrainIntegrator.TerrainTarget lowerTarget = surfaceTarget(lowerStart); + NativeStructureTerrainIntegrator.TerrainTarget upperTarget = surfaceTarget(upperStart); + + Set forward = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(forwardBlocks), area, List.of(lowerTarget, upperTarget), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + Set reverse = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(reverseBlocks), area, List.of(upperTarget, lowerTarget), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), forward); + assertEquals(forward, reverse); + assertEquals(forwardBlocks, reverseBlocks); + assertEquals(Blocks.AIR.defaultBlockState(), state(forwardBlocks, 0, 64, 0)); + } + + @Test + public void preserveDuplicateCannotSuppressVacuumSupport() 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), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(lower, upper), TerrainAdjustment.NONE); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of( + surfaceTarget(start, IrisStructureTerrainMode.PRESERVE), + surfaceTarget(start, IrisStructureTerrainMode.VACUUM)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + } + + @Test + public void processorCreatedAirAndFluidVetoSurfaceSupport() throws Exception { + StructureTemplate upperTemplate = template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), + block(1, 0, 0, Blocks.STONE.defaultBlockState()))); + InlineSinglePoolElement upperElement = new InlineSinglePoolElement( + upperTemplate, List.of( + replaceBlockProcessor( + Blocks.COBBLESTONE.defaultBlockState(), + Blocks.AIR.defaultBlockState()), + replaceBlockProcessor( + Blocks.STONE.defaultBlockState(), + Blocks.WATER.defaultBlockState()))); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))), + new BoundingBox(0, 62, 0, 1, 65, 0), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + upperElement, new BoundingBox(0, 64, 0, 1, 70, 0), + 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 1, 72, 0); + Map blocks = supportTerrain(0, 1); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertTrue(written.isEmpty()); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 1, 63, 0)); + } + + @Test + public void solidJigsawFinalStateCreatesSurfaceSupport() throws Exception { + CompoundTag jigsawData = new CompoundTag(); + jigsawData.putString("final_state", "minecraft:cobblestone"); + StructureTemplate upperTemplate = template(List.of( + new StructureTemplate.StructureBlockInfo( + BlockPos.ZERO, Blocks.JIGSAW.defaultBlockState(), jigsawData))); + 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(upperTemplate), + new BoundingBox(0, 64, 0, 0, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0)); + } + + @Test + public void laterLegacyAirDoesNotOverlayEarlierListSolid() throws Exception { + InlineSinglePoolElement solid = new InlineSinglePoolElement(template(List.of( + block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))); + InlineLegacyPoolElement legacyAir = new InlineLegacyPoolElement(template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState())))); + ListPoolElement list = new ListPoolElement( + List.of(solid, legacyAir), StructureTemplatePool.Projection.RIGID); + 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( + list, new BoundingBox(0, 64, 0, 0, 70, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart(List.of(lower, upper)); + BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0); + Map blocks = supportTerrain(0, 0); + + Set written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world(blocks), area, List.of(surfaceTarget(start)), + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written); + } + + @Test + public void stackedRigidPiecesPreserveTheLowerProcessedFoundation() throws Exception { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + TerrainAdjustment.BEARD_BOX)); + PoolElementStructurePiece lower = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(2, 1, 2, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 62, 0, 4, 65, 4), 1, Rotation.NONE); + PoolElementStructurePiece upper = rigidTemplatePiece( + new InlineSinglePoolElement(template(List.of( + block(2, 1, 2, Blocks.COBBLESTONE.defaultBlockState())))), + new BoundingBox(0, 66, 0, 4, 78, 4), 1, Rotation.NONE); + StructureStart start = new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of(upper, lower))); + BoundingBox area = new BoundingBox(0, 58, 0, 4, 78, 4); + Map blocks = new HashMap<>(); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + put(blocks, x, 59, z, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 60, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + } + + WorldGenLevel world = world(blocks); + List targets = + List.of(surfaceTarget(start)); + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world, area, targets, (x, z) -> 60); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 2, 62, 2)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 2, 65, 2)); + } + + @Test + public void containingFootprintOverridesAnAdjacentPiecesFalloff() { + NativeStructureSurfaceFitter.SurfaceAnchor local = + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, 66, 2); + NativeStructureSurfaceFitter.SurfaceAnchor adjacent = + new NativeStructureSurfaceFitter.SurfaceAnchor(5, 9, 0, 4, 70, 2); + + assertEquals(69, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(adjacent), 4, 2, 64)); + assertEquals(66, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(local, adjacent), 4, 2, 64)); + assertEquals(66, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(adjacent, local), 4, 2, 64)); + } + + @Test + public void opposingFalloffsBlendWithoutAnAbruptMidpointSeam() { + NativeStructureSurfaceFitter.SurfaceAnchor high = + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 70, 2); + NativeStructureSurfaceFitter.SurfaceAnchor low = + new NativeStructureSurfaceFitter.SurfaceAnchor(12, 12, 0, 0, 58, 2); + int previous = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(high, low), 0, 0, 64); + + for (int x = 1; x <= 12; x++) { + int forward = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(high, low), x, 0, 64); + int reversed = NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(low, high), x, 0, 64); + assertEquals(forward, reversed); + assertTrue(Math.abs(forward - previous) <= 6); + previous = forward; + } + assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget( + List.of(high, low), 6, 0, 64)); + } + + @Test + public void surfaceColumnsMutateTerrainAndRemoveUnsupportedDecoration() { + Map lowered = new HashMap<>(); + put(lowered, 0, 61, 0, Blocks.STONE.defaultBlockState()); + put(lowered, 0, 62, 0, Blocks.DIRT.defaultBlockState()); + put(lowered, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(lowered, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(lowered, 0, 65, 0, Blocks.DANDELION.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(lowered), new BlockPos.MutableBlockPos(), + 0, 0, 64, 62, -64, 319); + + assertEquals(Blocks.STONE.defaultBlockState(), state(lowered, 0, 61, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(lowered, 0, 62, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 63, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 64, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 65, 0)); + + Map raised = new HashMap<>(); + put(raised, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(raised, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(raised, 0, 65, 0, Blocks.DANDELION.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(raised), new BlockPos.MutableBlockPos(), + 0, 0, 64, 68, -64, 319); + + assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 65, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 66, 0)); + assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 67, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(raised, 0, 68, 0)); + } + + @Test + public void rigidSurfaceBaseClosesAOneBlockSubsurfaceGap() { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.STONE.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 64, -64, 319, true); + + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 64, 0)); + } + + @Test + public void rigidSurfaceBaseReconstructsAMissingMeetLayer() { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.STONE.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 64, -64, 319, true); + + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 64, 0)); + } + + @Test + public void rigidSurfaceBaseReconstructsAMissingMeetLayerAtTheWorldFloor() { + Map blocks = new HashMap<>(); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, -64, -64, -64, 319, true); + + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, -64, 0)); + } + + @Test + public void raisedSurfaceTerrainDoesNotSliceTreeBlocks() { + Map blocks = new HashMap<>(); + BlockState log = Blocks.OAK_LOG.defaultBlockState(); + BlockState leaves = Blocks.OAK_LEAVES.defaultBlockState(); + put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 0, 66, 0, log); + put(blocks, 0, 68, 0, leaves); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 68, -64, 319); + + assertEquals(log, state(blocks, 0, 66, 0)); + assertEquals(leaves, state(blocks, 0, 68, 0)); + } + + @Test + public void clearedIntersectingTreeCellsBecomeRaisedTerrainSupport() { + ProtoChunk chunk = new ProtoChunk( + new ChunkPos(0, 0), UpgradeData.EMPTY, + LevelHeightAccessor.create(-64, 384), containerFactory(), null); + write(chunk, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + write(chunk, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + write(chunk, 0, 66, 0, Blocks.OAK_LOG.defaultBlockState()); + write(chunk, 0, 68, 0, Blocks.OAK_LEAVES.defaultBlockState()); + WorldGenLevel world = world(chunk); + BoundingBox area = new BoundingBox(0, 63, 0, 0, 68, 0); + + NativeStructureVegetationClearer.clearIntersectingVegetation( + world, chunk, area, List.of(desertStart())); + NativeStructureSurfaceFitter.applySurfaceColumn( + world, new BlockPos.MutableBlockPos(), + 0, 0, 64, 68, -64, 319); + + assertEquals(Blocks.DIRT.defaultBlockState(), chunk.getBlockState(new BlockPos(0, 65, 0))); + assertEquals(Blocks.DIRT.defaultBlockState(), chunk.getBlockState(new BlockPos(0, 66, 0))); + assertEquals(Blocks.DIRT.defaultBlockState(), chunk.getBlockState(new BlockPos(0, 67, 0))); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), chunk.getBlockState(new BlockPos(0, 68, 0))); + } + + @Test + public void loweredFluidColumnsRemainFluidFilled() { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRAVEL.defaultBlockState()); + put(blocks, 0, 65, 0, Blocks.WATER.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 62, -64, 319); + + assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 63, 0)); + assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 64, 0)); + assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 65, 0)); + assertEquals(Blocks.GRAVEL.defaultBlockState(), state(blocks, 0, 62, 0)); + } + + @Test + public void preserveSourceYSkipsBurialAndKeepsTheVanillaStartY() { + StructureStart start = desertStart(); + int minY = start.getBoundingBox().minY(); + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, -64, 320, true, true, null, (x, z) -> 40); + + assertEquals(0, offset); + assertEquals(minY, start.getBoundingBox().minY()); + } + + @Test + public void preserveSourceYStillAppliesAnExplicitShift() { + StructureStart start = desertStart(); + int minY = start.getBoundingBox().minY(); + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, -8, -64, 320, true, true, null, (x, z) -> 40); + + assertEquals(-8, offset); + assertEquals(minY - 8, start.getBoundingBox().minY()); + } + + @Test + public void burialStillSinksTheStructureBelowTheLowestTerrainColumn() { + StructureStart start = desertStart(); + int minY = start.getBoundingBox().minY(); + int maxY = start.getBoundingBox().maxY(); + int expected = 40 - 1 - maxY; + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, -64, 320, true, false, null, (x, z) -> 40); + + assertEquals(expected, offset); + assertEquals(minY + expected, start.getBoundingBox().minY()); + } + + @Test + public void unfittableBurialClampsToTheWorldFloorInsteadOfAborting() { + StructureStart start = desertStart(); + int minY = start.getBoundingBox().minY(); + int worldMinY = minY - 4; + + int offset = NativeStructureVerticalPlacer.applyVerticalShift( + start, 0, worldMinY, 320, true, false, null, (x, z) -> worldMinY); + + assertEquals(-4, offset); + assertEquals(worldMinY, start.getBoundingBox().minY()); + } + + @Test + public void nativeVacuumLeavesPieceBlocksForSurfaceFitting() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + Map blocks = new HashMap<>(); + put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.STONE.defaultBlockState()); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), bounds, "minecraft:desert_pyramid", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM), null); + + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, bounds.minX(), bounds.minY(), bounds.minZ())); + } + + @Test + public void nativeForceCarveHonorsConfiguredPadding() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 1, bounds.minY(), bounds.minZ() - 1, + bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1); + Map blocks = new HashMap<>(); + put(blocks, bounds.maxX() + 1, bounds.minY(), bounds.maxZ(), + Blocks.STONE.defaultBlockState()); + put(blocks, bounds.maxX(), bounds.maxY() + 1, bounds.maxZ(), + Blocks.STONE.defaultBlockState()); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "minecraft:desert_pyramid", start, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(1) + .setCeilingPadding(1), null); + + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.maxX() + 1, bounds.minY(), bounds.maxZ())); + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, bounds.maxX(), bounds.maxY() + 1, bounds.maxZ())); + } + + @Test + public void nativeForceCarveUsesPieceUnionInsteadOfCombinedBounds() { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings(HolderSet.empty())); + DesertPyramidPiece first = new DesertPyramidPiece(RandomSource.create(7L), 0, 0); + DesertPyramidPiece second = new DesertPyramidPiece(RandomSource.create(8L), 0, 0); + second.move(64, 0, 0); + StructureStart start = new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(List.of(first, second))); + BoundingBox firstBounds = first.getBoundingBox(); + BoundingBox secondBounds = second.getBoundingBox(); + int gapX = (firstBounds.maxX() + secondBounds.minX()) / 2; + int y = firstBounds.minY(); + int z = firstBounds.minZ(); + BoundingBox area = new BoundingBox( + firstBounds.minX(), y, z, + secondBounds.maxX(), y, z); + Map blocks = new HashMap<>(); + put(blocks, firstBounds.minX(), y, z, Blocks.STONE.defaultBlockState()); + put(blocks, gapX, y, z, Blocks.STONE.defaultBlockState()); + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "minecraft:ancient_city", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FORCE_CARVE), null); + + assertEquals(Blocks.AIR.defaultBlockState(), + state(blocks, firstBounds.minX(), y, z)); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, gapX, y, z)); + } + + @Test + public void templateBackedColumnsUseTheTemplateAirComplement() throws Exception { + StructureTemplate template = template(List.of( + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 0, 0), Blocks.AIR.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 1, 0), Blocks.AIR.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 2, 0), Blocks.DEEPSLATE_BRICKS.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(1, 0, 0), Blocks.STRUCTURE_VOID.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(1, 1, 0), Blocks.DEEPSLATE.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(1, 2, 0), Blocks.DEEPSLATE.defaultBlockState(), null))); + Map columns = new HashMap<>(); + + assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns( + List.of(template), new BlockPos(0, 0, 0), Rotation.NONE, + new BoundingBox(0, 0, 0, 1, 2, 0), + (x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL, + new int[]{minY, maxY}))); + + assertEquals(2, columns.size()); + assertArrayEquals(new int[]{2, 2}, columns.get(0L)); + assertArrayEquals(new int[]{1, 2}, columns.get(1L << 32)); + } + + @Test + public void fullyVoidTemplateColumnsAreNotCarveSources() throws Exception { + StructureTemplate template = template(List.of( + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 0, 0), Blocks.AIR.defaultBlockState(), null), + new StructureTemplate.StructureBlockInfo( + new BlockPos(0, 1, 0), Blocks.STRUCTURE_VOID.defaultBlockState(), null))); + Map columns = new HashMap<>(); + + assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns( + List.of(template), new BlockPos(0, 0, 0), Rotation.NONE, + new BoundingBox(0, 0, 0, 0, 1, 0), + (x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL, + new int[]{minY, maxY}))); + + assertTrue(columns.isEmpty()); + } + + @Test + public void nonTemplatePiecesFallBackToTheirBoundingBoxColumns() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + + StructureCarvingFootprint footprint = NativeStructureTerrainIntegrator.carveFootprint( + start, 4, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertEquals(bounds.minX() - 4, footprint.minX()); + assertEquals(bounds.maxX() + 4, footprint.maxX()); + assertEquals(bounds.minZ() - 4, footprint.minZ()); + assertEquals(bounds.maxZ() + 4, footprint.maxZ()); + assertEquals(0L, footprint.distanceSquaredAt(bounds.minX(), bounds.minZ())); + assertEquals(32L, footprint.distanceSquaredAt(bounds.minX() - 4, bounds.minZ() - 4)); + assertEquals(bounds.minY(), footprint.sourceMinYAt(bounds.minX(), bounds.minZ())); + assertEquals(bounds.maxY(), footprint.sourceMaxYAt(bounds.minX(), bounds.minZ())); + } + + @Test + public void carveFootprintIsComputedOncePerStartAndPadding() { + StructureStart start = desertStart(); + + StructureCarvingFootprint first = NativeStructureTerrainIntegrator.carveFootprint( + start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + StructureCarvingFootprint repeated = NativeStructureTerrainIntegrator.carveFootprint( + start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + StructureCarvingFootprint widened = NativeStructureTerrainIntegrator.carveFootprint( + start, 7, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + StructureCarvingFootprint other = NativeStructureTerrainIntegrator.carveFootprint( + desertStart(), 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + + assertSame(first, repeated); + assertNotSame(first, widened); + assertNotSame(first, other); + } + + @Test + public void concurrentCarveRequestsShareOneFootprintBuild() throws Exception { + StructureStart start = desertStart(); + int requestCount = 16; + ExecutorService executor = Executors.newFixedThreadPool(requestCount); + CountDownLatch ready = new CountDownLatch(requestCount); + CountDownLatch release = new CountDownLatch(1); + List> futures = new ArrayList<>(); + try { + for (int request = 0; request < requestCount; request++) { + futures.add(executor.submit(() -> { + ready.countDown(); + release.await(); + return NativeStructureTerrainIntegrator.carveFootprint( + start, 11, + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + })); + } + assertTrue(ready.await(5, TimeUnit.SECONDS)); + release.countDown(); + StructureCarvingFootprint expected = futures.getFirst().get(); + for (Future future : futures) { + assertSame(expected, future.get()); + } + } finally { + executor.shutdownNow(); + } + } + + @Test + public void carveFootprintCacheStaysInsideItsCellBudget() { + for (int index = 0; index < 40; index++) { + NativeStructureTerrainIntegrator.carveFootprint( + desertStart(), 128, + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); + } + + assertTrue(NativeStructureTerrainIntegrator.cachedCarveFootprintCells() + <= NativeStructureTerrainIntegrator.maximumCachedCarveFootprintCells()); + } + + @Test + public void organicCarveNeverCutsBelowTheColumnSupportingFloor() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + NativeStructureTerrainIntegrator.OrganicCarve carve = organicCarve(start, 6); + BoundingBox area = new BoundingBox( + bounds.minX() - 6, bounds.minY() - 4, bounds.minZ() - 6, + bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6); + Map blocks = fill(area); + + NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area, carve); + + int centerX = bounds.minX() + bounds.getXSpan() / 2; + int centerZ = bounds.minZ() + bounds.getZSpan() / 2; + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, centerX, bounds.minY(), centerZ)); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int y = area.minY(); y < bounds.minY(); y++) { + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, x, y, z)); + } + } + } + } + + @Test + public void lobedCarveStaysInsideTheUniformCarveAndRemovesLess() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 10, bounds.minY(), bounds.minZ() - 10, + bounds.maxX() + 10, bounds.maxY() + 12, bounds.maxZ() + 10); + Map uniformBlocks = fill(area); + Map lobedBlocks = fill(area); + + NativeStructureTerrainIntegrator.carveOrganicColumns( + world(uniformBlocks), area, organicCarve(start, 10, 0D)); + NativeStructureTerrainIntegrator.carveOrganicColumns( + world(lobedBlocks), area, organicCarve(start, 10, 0.85D)); + + int uniform = 0; + int lobed = 0; + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int y = area.minY(); y <= area.maxY(); y++) { + boolean uniformAir = state(uniformBlocks, x, y, z).isAir(); + boolean lobedAir = state(lobedBlocks, x, y, z).isAir(); + assertTrue("lobe carved outside the uniform padding at " + + x + "," + y + "," + z, uniformAir || !lobedAir); + uniform += uniformAir ? 1 : 0; + lobed += lobedAir ? 1 : 0; + } + } + } + + assertTrue(lobed > 0); + assertTrue("uniform " + uniform + " lobed " + lobed, lobed < uniform); + } + + @Test + public void lobedCarveDepthWandersAlongAStraightFootprintEdge() { + int[] uniform = slabEdgeCarveDepths(0D); + int[] lobed = slabEdgeCarveDepths(0.85D); + + assertTrue("uniform depths wander " + span(uniform), span(uniform) <= 2); + assertTrue("lobed depths wander " + span(lobed), span(lobed) >= 6); + for (int depth : lobed) { + assertTrue(depth >= 0 && depth <= SLAB_PADDING); + } + } + + @Test + public void organicCarveIsIdenticalAcrossNeighboringChunkContexts() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox wide = new BoundingBox( + bounds.minX() - 6, bounds.minY(), bounds.minZ() - 6, + bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6); + BoundingBox narrow = new BoundingBox( + bounds.maxX() - 3, bounds.minY(), bounds.maxZ() - 3, + bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6); + Map wideBlocks = fill(wide); + Map narrowBlocks = fill(narrow); + + // Each chunk context rebuilds its own noise channels from the shared start identity. + NativeStructureTerrainIntegrator.carveOrganicColumns( + world(wideBlocks), wide, organicCarve(start, 6)); + NativeStructureTerrainIntegrator.carveOrganicColumns( + world(narrowBlocks), narrow, organicCarve(start, 6)); + + int carved = 0; + for (int x = narrow.minX(); x <= narrow.maxX(); x++) { + for (int z = narrow.minZ(); z <= narrow.maxZ(); z++) { + for (int y = narrow.minY(); y <= narrow.maxY(); y++) { + BlockState expected = state(wideBlocks, x, y, z); + assertEquals(expected, state(narrowBlocks, x, y, z)); + if (expected.isAir()) { + carved++; + } + } + } + } + assertTrue(carved > 0); + } + + @Test + public void erodedForceCarveShrinkwrapsWithoutUnderminingTheFloor() { + StructureStart start = desertStart(); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BoundingBox area = new BoundingBox( + bounds.minX() - 6, bounds.minY() - 2, bounds.minZ() - 6, + bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6); + Map blocks = fill(area); + int centerX = bounds.minX() + bounds.getXSpan() / 2; + int centerZ = bounds.minZ() + bounds.getZSpan() / 2; + + NativeStructureTerrainIntegrator.integrateTerrain( + world(blocks), area, "minecraft:ancient_city", start, + new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setShape(IrisStructureCarveShape.ERODED) + .setHorizontalPadding(6) + .setCeilingPadding(8) + .setFloorPadding(0) + .setErosionStrength(1D) + .setErosionFrequency(0.05D), null); + + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, centerX, bounds.minY(), centerZ)); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, centerX, bounds.minY() - 1, centerZ)); + assertEquals(Blocks.STONE.defaultBlockState(), + state(blocks, area.minX(), area.maxY(), area.minZ())); + } + + @Test + public void sparseStiltGridIsDeterministicAndPreflightsGround() { + assertTrue(NativeStructureFoundationBuilder.isStiltColumn(0, 0, 4)); + assertTrue(NativeStructureFoundationBuilder.isStiltColumn(-4, 8, 4)); + assertFalse(NativeStructureFoundationBuilder.isStiltColumn(1, 0, 4)); + assertTrue(NativeStructureFoundationBuilder.isStiltColumn(1, 1, 1)); + + Map blocks = new HashMap<>(); + put(blocks, 0, 7, 0, Blocks.DEEPSLATE.defaultBlockState()); + put(blocks, 0, 8, 0, Blocks.SCULK_VEIN.defaultBlockState()); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + + assertEquals(7, NativeStructureFoundationBuilder.findStiltAnchorY( + world(blocks), 0, 0, 10, 2, -64, -64, position)); + assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY( + world(blocks), 0, 0, 10, 1, -64, -64, position)); + assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY( + world(new HashMap<>()), 0, 0, 10, 64, -64, -64, position)); + } + + @Test + public void nativeTerrainEnvelopePersistsHorizontalReferenceCoverage() { + StructureStart generated = desertStart(); + BoundingBox content = generated.getBoundingBox(); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(24); + + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, + generated.getStructure(), + 0, + terrain); + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + wrapped, wrapped.getStructure(), terrain); + + assertEquals(content, NativeStructureReferenceEnvelope.contentBounds(wrapped)); + assertEquals(content, wrapped.getBoundingBox()); + assertEquals(content.minX() - 24, referenceBounds.minX()); + assertEquals(content.minZ() - 24, referenceBounds.minZ()); + assertEquals(content.maxX() + 24, referenceBounds.maxX()); + assertEquals(content.maxZ() + 24, referenceBounds.maxZ()); + assertEquals(generated.getPieces().size(), wrapped.getPieces().size()); + } + + @Test + public void nativeVacuumReservesItsFixedSurfaceFalloff() { + StructureStart generated = desertStart(TerrainAdjustment.NONE); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.VACUUM) + .setHorizontalPadding(64); + + BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds( + generated, generated.getStructure(), terrain); + + assertEquals(content.minX() - NativeStructureSurfaceFitter.surfaceTerrainRadius(), + references.minX()); + assertEquals(content.maxX() + NativeStructureSurfaceFitter.surfaceTerrainRadius(), + references.maxX()); + assertEquals(content.minZ() - NativeStructureSurfaceFitter.surfaceTerrainRadius(), + references.minZ()); + assertEquals(content.maxZ() + NativeStructureSurfaceFitter.surfaceTerrainRadius(), + references.maxZ()); + } + + @Test + public void nativeTerrainEnvelopeClipsOptionalCoverageWithoutDroppingContent() { + StructureStart generated = desertStart(); + ChunkPos origin = generated.getChunkPos(); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(124); + + BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds( + generated, generated.getStructure(), terrain, "test:clipped_terrain"); + StructureStart published = NativeStructureReferenceEnvelope.wrapForPublication( + generated, generated.getStructure(), 0, terrain, "test:clipped_terrain"); + + assertEquals(Math.max(content.minX() - 124, (origin.x() - 8) << 4), references.minX()); + assertEquals(Math.min(content.maxX() + 124, ((origin.x() + 8) << 4) + 15), references.maxX()); + assertEquals(Math.max(content.minZ() - 124, (origin.z() - 8) << 4), references.minZ()); + assertEquals(Math.min(content.maxZ() + 124, ((origin.z() + 8) << 4) + 15), references.maxZ()); + assertTrue(references.isInside(content.getCenter())); + assertTrue(published.isValid()); + assertEquals(1, generated.getPieces().size()); + } + + @Test + public void singlePoolTemplateFieldMatchesTheRuntimeContract() { + Field field = NativeStructureReflection.resolveSinglePoolTemplateField(); + + assertEquals(SinglePoolElement.class, field.getDeclaringClass()); + assertEquals(Either.class, field.getType()); + assertTrue(Modifier.isProtected(field.getModifiers())); + assertTrue(Modifier.isFinal(field.getModifiers())); + assertFalse(Modifier.isStatic(field.getModifiers())); + assertTrue(field.trySetAccessible()); + } + + @Test + public void runtimeTemplatesAndLegacyAirUseTheExactContract() { + StructureTemplate runtimeTemplate = new StructureTemplate(); + + assertEquals(runtimeTemplate, NativeStructureReflection.resolveTemplateReference( + Either.right(runtimeTemplate), null)); + assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(79, 80, false)); + assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(80, 80, false)); + assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, false)); + assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, true)); + } + + @Test + public void rotatedTemplateAirClearsOnlyInsideTheChunkAndAboveTheFloor() throws Exception { + StructureTemplate.StructureBlockInfo clear = new StructureTemplate.StructureBlockInfo( + new BlockPos(1, 0, 0), Blocks.AIR.defaultBlockState(), null); + StructureTemplate.StructureBlockInfo belowFloor = new StructureTemplate.StructureBlockInfo( + new BlockPos(0, -1, 0), Blocks.AIR.defaultBlockState(), null); + StructureTemplate.StructureBlockInfo outsideChunk = new StructureTemplate.StructureBlockInfo( + new BlockPos(3, 0, 0), Blocks.AIR.defaultBlockState(), null); + StructureTemplate template = template(List.of(clear, belowFloor, outsideChunk)); + BlockPos origin = new BlockPos(10, 80, 10); + StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(Rotation.CLOCKWISE_90); + BlockPos clearPosition = origin.offset(StructureTemplate.calculateRelativePosition(settings, clear.pos())); + BlockPos belowFloorPosition = origin.offset( + StructureTemplate.calculateRelativePosition(settings, belowFloor.pos())); + BlockPos outsidePosition = origin.offset( + StructureTemplate.calculateRelativePosition(settings, outsideChunk.pos())); + BoundingBox area = new BoundingBox( + Math.min(clearPosition.getX(), belowFloorPosition.getX()), + Math.min(clearPosition.getY(), belowFloorPosition.getY()), + Math.min(clearPosition.getZ(), belowFloorPosition.getZ()), + Math.max(clearPosition.getX(), belowFloorPosition.getX()), + Math.max(clearPosition.getY(), belowFloorPosition.getY()), + Math.max(clearPosition.getZ(), belowFloorPosition.getZ())); + settings.setBoundingBox(area); + assertFalse(area.isInside(outsidePosition)); + Map blocks = new HashMap<>(); + blocks.put(clearPosition, Blocks.DIRT.defaultBlockState()); + blocks.put(belowFloorPosition, Blocks.DIRT.defaultBlockState()); + blocks.put(outsidePosition, Blocks.DIRT.defaultBlockState()); + + NativeStructureTerrainIntegrator.clearTemplateAir( + world(blocks), template, origin, 80, settings); + + assertEquals(Blocks.AIR.defaultBlockState(), blocks.get(clearPosition)); + assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(belowFloorPosition)); + assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(outsidePosition)); + } + + @Test + public void unrelatedPieceBoundsAreRejectedBeforeTemplateScanning() { + BoundingBox area = new BoundingBox(0, -64, 0, 15, 319, 15); + + assertTrue(NativeStructureTerrainIntegrator.intersects( + new BoundingBox(15, 60, 15, 30, 90, 30), area)); + assertFalse(NativeStructureTerrainIntegrator.intersects( + new BoundingBox(16, 60, 16, 30, 90, 30), area)); + assertFalse(NativeStructureTerrainIntegrator.intersects( + new BoundingBox(0, 320, 0, 15, 350, 15), area)); + } + + @Test + public void templateAirDoesNotEraseTreesInsideVillagePieces() throws Exception { + BlockPos origin = new BlockPos(0, 80, 0); + StructureTemplate template = template(List.of( + new StructureTemplate.StructureBlockInfo(BlockPos.ZERO, Blocks.AIR.defaultBlockState(), null))); + StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(Rotation.NONE); + Map blocks = new HashMap<>(); + BlockState log = Blocks.OAK_LOG.defaultBlockState(); + blocks.put(origin, log); + + NativeStructureTerrainIntegrator.clearTemplateAir( + world(blocks), template, origin, 80, settings); + + assertEquals(log, blocks.get(origin)); + } + + private static NativeStructureSurfaceFitter.SurfaceAnchor anchor(int meetY, int strength) { + return new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, meetY, strength); + } + + private static PoolElementStructurePiece rigidPiece(BoundingBox bounds, int groundLevelDelta) { + StructurePoolElement element = StructurePoolElement.single( + "minecraft:empty").apply(StructureTemplatePool.Projection.RIGID); + return new PoolElementStructurePiece( + null, element, + new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()), + groundLevelDelta, Rotation.NONE, bounds, + LiquidSettings.APPLY_WATERLOGGING); + } + + private static PoolElementStructurePiece rigidTemplatePiece( + StructurePoolElement element, BoundingBox bounds, + int groundLevelDelta, Rotation rotation) { + return new PoolElementStructurePiece( + null, element, + new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()), + groundLevelDelta, rotation, bounds, + LiquidSettings.APPLY_WATERLOGGING); + } + + private static StructureStart rigidSurfaceStart(List pieces) { + return rigidSurfaceStart(pieces, TerrainAdjustment.BEARD_BOX); + } + + private static StructureStart rigidSurfaceStart( + List pieces, TerrainAdjustment adjustment) { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + adjustment)); + List structurePieces = new ArrayList<>(pieces); + return new StructureStart( + structure, new ChunkPos(0, 0), 0, + new PiecesContainer(structurePieces)); + } + + private static NativeStructureTerrainIntegrator.TerrainTarget surfaceTarget( + StructureStart start) { + return surfaceTarget(start, IrisStructureTerrainMode.SOURCE); + } + + private static NativeStructureTerrainIntegrator.TerrainTarget surfaceTarget( + StructureStart start, IrisStructureTerrainMode mode) { + return new NativeStructureTerrainIntegrator.TerrainTarget( + "nova_structures:tavern_oak", start, + new IrisStructureTerrain().setMode(mode)); + } + + private static StructureTemplate.StructureBlockInfo block( + int x, int y, int z, BlockState state) { + return new StructureTemplate.StructureBlockInfo( + new BlockPos(x, y, z), state, null); + } + + private static Map supportTerrain(int minimumX, int maximumX) { + Map blocks = new HashMap<>(); + for (int x = minimumX; x <= maximumX; x++) { + put(blocks, x, 61, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, x, 62, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + } + return blocks; + } + + private static Map flatTerrain(BoundingBox area, int surfaceY) { + Map blocks = new HashMap<>(); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + put(blocks, x, surfaceY - 1, z, Blocks.DIRT.defaultBlockState()); + put(blocks, x, surfaceY, z, Blocks.GRASS_BLOCK.defaultBlockState()); + } + } + return blocks; + } + + private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve( + StructureStart start, int horizontalPadding) { + return organicCarve(start, horizontalPadding, 0.85D); + } + + private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve( + StructureStart start, int horizontalPadding, double lobeStrength) { + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setShape(IrisStructureCarveShape.ERODED) + .setHorizontalPadding(horizontalPadding) + .setCeilingPadding(8) + .setFloorPadding(0) + .setErosionStrength(1D) + .setErosionFrequency(0.05D) + .setLobeStrength(lobeStrength); + return NativeStructureTerrainIntegrator.organicCarve( + NativeStructureTerrainIntegrator.carveFootprint(start, horizontalPadding, + NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager), + terrain, IrisStructureCarveShape.ERODED, TEST_SEED); + } + + /** + * Outward carve depth for every column along the straight +X edge of a slab footprint, measured + * inside the source vertical span so the boundary is decided purely by the horizontal threshold. + * The slab spans several lobe wavelengths so a lobed boundary is distinguishable from a uniform one. + */ + private static int[] slabEdgeCarveDepths(double lobeStrength) { + StructureCarvingFootprint footprint = StructureCarvingFootprint.fromColumns(sink -> { + for (int x = 0; x < SLAB_WIDTH; x++) { + for (int z = 0; z < SLAB_DEPTH; z++) { + sink.column(x, z, SLAB_MIN_Y, SLAB_MIN_Y + 8); + } + } + return true; + }, SLAB_PADDING, 1_000_000); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setShape(IrisStructureCarveShape.ERODED) + .setHorizontalPadding(SLAB_PADDING) + .setCeilingPadding(8) + .setFloorPadding(0) + .setErosionStrength(1D) + .setErosionFrequency(0.05D) + .setLobeStrength(lobeStrength); + int transectY = SLAB_MIN_Y + 4; + BoundingBox area = new BoundingBox( + SLAB_WIDTH, transectY, 0, + SLAB_WIDTH - 1 + SLAB_PADDING, transectY, SLAB_DEPTH - 1); + Map blocks = fill(area); + + NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area, + NativeStructureTerrainIntegrator.organicCarve( + footprint, terrain, IrisStructureCarveShape.ERODED, TEST_SEED)); + + int[] depths = new int[SLAB_DEPTH]; + for (int z = 0; z < SLAB_DEPTH; z++) { + int depth = 0; + while (depth < SLAB_PADDING + && state(blocks, SLAB_WIDTH + depth, transectY, z).isAir()) { + depth++; + } + depths[z] = depth; + } + return depths; + } + + private static int span(int[] values) { + int lowest = Integer.MAX_VALUE; + int highest = Integer.MIN_VALUE; + for (int value : values) { + lowest = Math.min(lowest, value); + highest = Math.max(highest, value); + } + return highest - lowest; + } + + private static StructureTemplateManager forbiddenTemplateManager() { + throw new AssertionError("Bounding-box carve columns must not resolve templates"); + } + + private static Map fill(BoundingBox area) { + Map blocks = new HashMap<>(); + for (int x = area.minX(); x <= area.maxX(); x++) { + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int y = area.minY(); y <= area.maxY(); y++) { + put(blocks, x, y, z, Blocks.STONE.defaultBlockState()); + } + } + } + return blocks; + } + + private static StructureTemplate template( + List blocks) throws Exception { + Constructor constructor = + StructureTemplate.Palette.class.getDeclaredConstructor(List.class); + assertTrue(constructor.trySetAccessible()); + StructureTemplate.Palette palette = constructor.newInstance(blocks); + StructureTemplate template = new StructureTemplate(); + template.palettes.add(palette); + return template; + } + + private static final class InlineSinglePoolElement extends SinglePoolElement { + private InlineSinglePoolElement(StructureTemplate template) { + this(template, List.of()); + } + + private InlineSinglePoolElement( + StructureTemplate template, List processors) { + this(template, processors, StructureTemplatePool.Projection.RIGID); + } + + private InlineSinglePoolElement( + StructureTemplate template, List processors, + StructureTemplatePool.Projection projection) { + super(Either.right(template), + Holder.direct(new StructureProcessorList(processors)), + projection, + Optional.empty()); + } + } + + private static final class InlineLegacyPoolElement extends LegacySinglePoolElement { + private InlineLegacyPoolElement(StructureTemplate template) { + super(Either.right(template), + Holder.direct(new StructureProcessorList(List.of())), + StructureTemplatePool.Projection.RIGID, + Optional.empty()); + } + } + + private static StructureProcessor replaceBlockProcessor(BlockState source, BlockState replacement) { + // Vanilla RuleProcessor instead of a custom StructureProcessor subtype: StructureProcessor + // is an abstract class on 26.1.2 and an interface on 26.2, so a direct subtype cannot + // compile against both dev bundles. BlockStateMatchTest matches the exact source state, + // matching the previous custom processor's identity-equality semantics. + return new RuleProcessor(List.of(new ProcessorRule( + new BlockStateMatchTest(source), AlwaysTrueTest.INSTANCE, replacement))); + } + + private static StructureStart desertStart() { + return desertStart(TerrainAdjustment.NONE); + } + + private static StructureStart desertStart(TerrainAdjustment adjustment) { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, adjustment)); + DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0); + return new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + } + + private static WorldGenLevel world(Map blocks) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("getBlockState")) { + BlockPos position = (BlockPos) arguments[0]; + return state(blocks, position.getX(), position.getY(), position.getZ()); + } + if (methodName.equals("setBlock")) { + BlockPos position = (BlockPos) arguments[0]; + BlockState blockState = (BlockState) arguments[1]; + put(blocks, position.getX(), position.getY(), position.getZ(), blockState); + return true; + } + if (methodName.equals("getSeed")) { + return TEST_SEED; + } + if (methodName.equals("getHeight")) { + int x = (int) arguments[1]; + int z = (int) arguments[2]; + int highest = Integer.MIN_VALUE; + for (Map.Entry entry : blocks.entrySet()) { + BlockPos position = entry.getKey(); + if (position.getX() == x && position.getZ() == z + && !entry.getValue().isAir()) { + highest = Math.max(highest, position.getY()); + } + } + return highest == Integer.MIN_VALUE ? 0 : highest + 1; + } + if (methodName.equals("getLevel")) { + return null; + } + if (methodName.equals("holderLookup")) { + return BuiltInRegistries.BLOCK; + } + if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (methodName.equals("equals")) { + return proxy == arguments[0]; + } + if (methodName.equals("toString")) { + return "surface-test-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), new Class[]{WorldGenLevel.class}, handler); + } + + private static WorldGenLevel world(ChunkAccess chunk) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("getBlockState")) { + return chunk.getBlockState((BlockPos) arguments[0]); + } + if (methodName.equals("setBlock")) { + return chunk.setBlockState( + ((BlockPos) arguments[0]).immutable(), + (BlockState) arguments[1], (int) arguments[2]) != null; + } + if (methodName.equals("getSeed")) { + return TEST_SEED; + } + if (methodName.equals("getLevel")) { + return null; + } + if (methodName.equals("holderLookup")) { + return BuiltInRegistries.BLOCK; + } + if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (methodName.equals("equals")) { + return proxy == arguments[0]; + } + if (methodName.equals("toString")) { + return "surface-test-chunk-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), new Class[]{WorldGenLevel.class}, handler); + } + + private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) { + LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y)); + section.setBlockState(x, y & 15, z, state, false); + } + + private static PalettedContainerFactory containerFactory() { + Strategy blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY); + BlockState air = Blocks.AIR.defaultBlockState(); + IdMapper> biomeIds = new IdMapper<>(); + Holder defaultBiome = Holder.direct((Biome) null); + biomeIds.add(defaultBiome); + Strategy> biomes = Strategy.createForBiomes(biomeIds); + Codec> biomeCodec = Codec.STRING.xmap( + name -> defaultBiome, holder -> "iris-test-biome"); + return new PalettedContainerFactory( + blockStates, + air, + PalettedContainer.codecRW(BlockState.CODEC, blockStates, air), + biomes, + defaultBiome, + PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome), + PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome)); + } + + private static void put(Map blocks, + int x, int y, int z, BlockState state) { + blocks.put(new BlockPos(x, y, z), state); + } + + private static BlockState state(Map blocks, int x, int y, int z) { + return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState()); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java new file mode 100644 index 000000000..0c464fc4d --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java @@ -0,0 +1,291 @@ +package art.arcane.iris.nativegen; + +import com.mojang.serialization.Codec; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.IdMapper; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerFactory; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.Strategy; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NativeStructurePostProcessorVegetationTest { + private static final long TEST_SEED = 1234L; + private static final int MIN_Y = -64; + private static final int MAX_Y = 319; + private static final int GROUND_Y = 64; + private static final BoundingBox AREA = new BoundingBox(0, MIN_Y, 0, 15, MAX_Y, 15); + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void canopyAboveTheStructureRoofSurvivesVegetationClearing() { + ProtoChunk chunk = chunk(); + StructureStart start = desertStart(0, 0, GROUND_Y + 15); + BoundingBox bounds = start.getBoundingBox(); + for (int y = bounds.maxY() - 1; y <= GROUND_Y + 40; y++) { + fillLayer(chunk, y, leaves()); + } + write(chunk, 4, bounds.minY() - 1, 4, log()); + + clearVegetation(chunk, start); + + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, GROUND_Y + 40, z))); + assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, bounds.maxY() + 1, z))); + assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY(), z))); + assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY() - 1, z))); + } + } + assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4))); + } + + @Test + public void buriedStructuresPreserveTheForestAboveThem() { + ProtoChunk chunk = chunk(); + StructureStart start = desertStart(0, 0, -20); + write(chunk, 8, GROUND_Y + 1, 8, log()); + write(chunk, 8, GROUND_Y + 2, 8, log()); + write(chunk, 8, GROUND_Y + 3, 8, log()); + write(chunk, 8, GROUND_Y + 4, 8, leaves()); + + clearVegetation(chunk, start); + + assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 1, 8))); + assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 2, 8))); + assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 3, 8))); + assertEquals(leaves(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 4, 8))); + } + + @Test + public void columnsOutsideThePieceFootprintKeepEveryTreeBlock() { + ProtoChunk chunk = chunk(); + StructureStart start = desertStart(8, 8, GROUND_Y + 15); + BoundingBox bounds = start.getBoundingBox(); + int inside = bounds.minY() + 4; + write(chunk, 0, inside, 0, log()); + write(chunk, 0, GROUND_Y + 40, 0, leaves()); + write(chunk, 12, inside, 12, log()); + write(chunk, 12, GROUND_Y + 40, 12, leaves()); + + clearVegetation(chunk, start); + + assertEquals(log(), chunk.getBlockState(new BlockPos(0, inside, 0))); + assertEquals(leaves(), chunk.getBlockState(new BlockPos(0, GROUND_Y + 40, 0))); + assertEquals(air(), chunk.getBlockState(new BlockPos(12, inside, 12))); + assertEquals(leaves(), chunk.getBlockState(new BlockPos(12, GROUND_Y + 40, 12))); + } + + @Test + public void clearingStopsAtThePieceEnvelopeBoundaries() { + ProtoChunk chunk = chunk(); + StructureStart start = desertStart(0, 0, GROUND_Y + 15); + BoundingBox bounds = start.getBoundingBox(); + write(chunk, 4, bounds.maxY() + 1, 4, log()); + write(chunk, 4, bounds.maxY(), 4, log()); + write(chunk, 4, bounds.minY() - 1, 4, log()); + write(chunk, 4, bounds.minY() - 2, 4, log()); + + clearVegetation(chunk, start); + + assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.maxY() + 1, 4))); + assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.maxY(), 4))); + assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4))); + assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 2, 4))); + } + + @Test + public void overlappingPieceEnvelopesMergeIntoOneClearedRange() { + ProtoChunk chunk = chunk(); + StructureStart low = desertStart(0, 0, GROUND_Y); + StructureStart high = desertStart(0, 0, GROUND_Y + 40); + int between = GROUND_Y + 20; + write(chunk, 4, between, 4, log()); + write(chunk, 4, high.getBoundingBox().maxY() + 1, 4, log()); + write(chunk, 4, low.getBoundingBox().minY() - 2, 4, log()); + + NativeStructureVegetationClearer.clearIntersectingVegetation( + world(chunk), chunk, AREA, List.of(low, high)); + + assertEquals(air(), chunk.getBlockState(new BlockPos(4, between, 4))); + assertEquals(log(), chunk.getBlockState( + new BlockPos(4, high.getBoundingBox().maxY() + 1, 4))); + assertEquals(log(), chunk.getBlockState( + new BlockPos(4, low.getBoundingBox().minY() - 2, 4))); + } + + @Test + public void validStartsReachIntersectionProcessing() { + assertTrue(NativeStructureVegetationClearer.shouldProcessStart( + desertStart(0, 0, GROUND_Y + 15))); + assertFalse(NativeStructureVegetationClearer.shouldProcessStart( + StructureStart.INVALID_START)); + } + + @Test + public void allUndergroundGenerationStepsShareOneClassification() { + assertTrue(NativeStructureVegetationClearer.isUndergroundStep( + GenerationStep.Decoration.UNDERGROUND_STRUCTURES)); + assertTrue(NativeStructureVegetationClearer.isUndergroundStep( + GenerationStep.Decoration.UNDERGROUND_DECORATION)); + assertTrue(NativeStructureVegetationClearer.isUndergroundStep( + GenerationStep.Decoration.STRONGHOLDS)); + assertFalse(NativeStructureVegetationClearer.isUndergroundStep( + GenerationStep.Decoration.SURFACE_STRUCTURES)); + } + + @Test + public void undergroundStructuresUseTheLowestTerrainColumn() { + BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0); + int offset = NativeStructureVerticalPlacer.resolveBuriedOffset( + bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100); + assertEquals(-5, offset); + } + + @Test + public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() { + BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0); + assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset( + bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100)); + } + + private static void clearVegetation(ProtoChunk chunk, StructureStart start) { + NativeStructureVegetationClearer.clearIntersectingVegetation( + world(chunk), chunk, AREA, List.of(start)); + } + + private static StructureStart desertStart(int originX, int originZ, int topY) { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE)); + DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), originX, originZ); + piece.move(0, topY - piece.getBoundingBox().maxY(), 0); + return new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + } + + private static ProtoChunk chunk() { + return new ProtoChunk( + new ChunkPos(0, 0), UpgradeData.EMPTY, + LevelHeightAccessor.create(MIN_Y, MAX_Y - MIN_Y + 1), containerFactory(), null); + } + + private static void fillLayer(ProtoChunk chunk, int y, BlockState state) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + write(chunk, x, y, z, state); + } + } + } + + private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) { + LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y)); + section.setBlockState(x, y & 15, z, state, false); + } + + private static BlockState log() { + return Blocks.OAK_LOG.defaultBlockState(); + } + + private static BlockState leaves() { + return Blocks.OAK_LEAVES.defaultBlockState(); + } + + private static BlockState air() { + return Blocks.AIR.defaultBlockState(); + } + + private static WorldGenLevel world(ChunkAccess chunk) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("getBlockState")) { + return chunk.getBlockState((BlockPos) arguments[0]); + } + if (methodName.equals("setBlock")) { + return chunk.setBlockState( + ((BlockPos) arguments[0]).immutable(), + (BlockState) arguments[1], (int) arguments[2]) != null; + } + if (methodName.equals("getSeed")) { + return TEST_SEED; + } + if (methodName.equals("getLevel")) { + return null; + } + if (methodName.equals("holderLookup")) { + return BuiltInRegistries.BLOCK; + } + if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (methodName.equals("equals")) { + return proxy == arguments[0]; + } + if (methodName.equals("toString")) { + return "vegetation-test-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), new Class[]{WorldGenLevel.class}, handler); + } + + private static PalettedContainerFactory containerFactory() { + Strategy blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY); + BlockState air = Blocks.AIR.defaultBlockState(); + IdMapper> biomeIds = new IdMapper<>(); + Holder defaultBiome = Holder.direct((Biome) null); + biomeIds.add(defaultBiome); + Strategy> biomes = Strategy.createForBiomes(biomeIds); + Codec> biomeCodec = Codec.STRING.xmap( + name -> defaultBiome, holder -> "iris-test-biome"); + return new PalettedContainerFactory( + blockStates, + air, + PalettedContainer.codecRW(BlockState.CODEC, blockStates, air), + biomes, + defaultBiome, + PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome), + PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome)); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java new file mode 100644 index 000000000..0e8d00137 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java @@ -0,0 +1,236 @@ +package art.arcane.iris.nativegen; + +import com.mojang.datafixers.util.Either; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.Bootstrap; +import net.minecraft.tags.TagKey; +import net.minecraft.util.valueproviders.ConstantInt; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlackstoneReplaceProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlockAgeProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.CappedProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.JigsawReplacementProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.LavaSubmergedBlockProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.NopProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.ProtectedBlockProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NativeStructureTemplateOccupancyTest { + private static final BoundingBox PROCESSING_AREA = new BoundingBox(0, 56, 0, 15, 80, 0); + private static final int TEMPLATE_WIDTH = 32; + private static final int TERRAIN_HEIGHT = 64; + + @BeforeClass + public static void bootstrapMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void straddlingGravityPieceHeightReadsHonorTheVanillaClipContract() throws Exception { + ColumnRecorder recorder = new ColumnRecorder(); + + resolve(straddlingPiece( + List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder); + + // 26.2 vanilla clips processor evaluation to the placement bounding box; 26.1.2 vanilla + // runs processors across the whole piece. Iris mirrors the pinned version's semantics. + assertEquals(expectedClippedColumns(), recorder.heightColumns); + } + + @Test + public void cappedStraddlingPieceRunsProcessorsAcrossTheWholePiece() throws Exception { + ColumnRecorder recorder = new ColumnRecorder(); + + resolve(straddlingPiece(List.of( + new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0), + new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))), recorder); + + assertEquals(columns(0, TEMPLATE_WIDTH - 1), recorder.heightColumns); + } + + @Test + public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() throws Exception { + Method contract = clipContractMethod(); + Assume.assumeTrue("evaluatesEntirePieceState only exists on 26.2+", contract != null); + + for (StructureProcessor processor : List.of( + BlockIgnoreProcessor.STRUCTURE_BLOCK, + JigsawReplacementProcessor.INSTANCE, + LavaSubmergedBlockProcessor.INSTANCE, + NopProcessor.INSTANCE, + BlackstoneReplaceProcessor.INSTANCE, + new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0), + new RuleProcessor(List.of()), + new BlockAgeProcessor(0.5F), + protectedBlockProcessor())) { + assertFalse(processor.getClass().getName(), + (Boolean) contract.invoke(processor)); + } + assertTrue((Boolean) contract.invoke( + new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4)))); + } + + @Test + public void placementSettingsCarryTheProcessingAreaAsTheirClipBox() throws Exception { + PoolElementStructurePiece piece = straddlingPiece(List.of()); + + StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings( + (SinglePoolElement) piece.getElement(), piece, PROCESSING_AREA); + + assertEquals(PROCESSING_AREA, settings.getBoundingBox()); + } + + private static void resolve(PoolElementStructurePiece piece, ColumnRecorder recorder) { + NativeStructureTemplateOccupancy.resolve( + world(recorder), piece, piece.getPosition(), PROCESSING_AREA, + NativeStructureTemplateOccupancyTest::forbiddenTemplateManager, + PROCESSING_AREA::isInside, amount -> { + }); + } + + private static PoolElementStructurePiece straddlingPiece( + List processors) throws Exception { + BoundingBox bounds = new BoundingBox(0, 70, 0, TEMPLATE_WIDTH - 1, 76, 0); + return new PoolElementStructurePiece( + null, new InlineSinglePoolElement(template(), processors), + new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()), + 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING); + } + + private static Set columns(int minimumX, int maximumX) { + Set columns = new TreeSet<>(); + for (int x = minimumX; x <= maximumX; x++) { + columns.add(x); + } + return columns; + } + + private static Method clipContractMethod() { + try { + return StructureProcessor.class.getMethod("evaluatesEntirePieceState"); + } catch (NoSuchMethodException absent) { + return null; + } + } + + private static Set expectedClippedColumns() { + return clipContractMethod() != null + ? columns(PROCESSING_AREA.minX(), PROCESSING_AREA.maxX()) + : columns(0, TEMPLATE_WIDTH - 1); + } + + private static StructureProcessor protectedBlockProcessor() throws Exception { + // Constructed reflectively: the constructor takes HolderSet on 26.2 and TagKey on 26.1.2. + Constructor constructor = ProtectedBlockProcessor.class.getConstructors()[0]; + Class parameter = constructor.getParameterTypes()[0]; + Object argument = parameter == HolderSet.class + ? HolderSet.empty() + : TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("minecraft", "air")); + return (StructureProcessor) constructor.newInstance(argument); + } + + private static StructureTemplateManager forbiddenTemplateManager() { + throw new AssertionError("Inline templates must not resolve a template manager"); + } + + private static StructureTemplate template() throws Exception { + List blocks = new ArrayList<>(); + for (int x = 0; x < TEMPLATE_WIDTH; x++) { + blocks.add(new StructureTemplate.StructureBlockInfo( + new BlockPos(x, 0, 0), Blocks.COBBLESTONE.defaultBlockState(), null)); + } + Constructor constructor = + StructureTemplate.Palette.class.getDeclaredConstructor(List.class); + assertTrue(constructor.trySetAccessible()); + StructureTemplate template = new StructureTemplate(); + template.palettes.add(constructor.newInstance(blocks)); + return template; + } + + private static WorldGenLevel world(ColumnRecorder recorder) { + InvocationHandler handler = (proxy, method, arguments) -> { + String methodName = method.getName(); + if (methodName.equals("getHeight") && arguments.length == 3) { + recorder.heightColumns.add((Integer) arguments[1]); + return TERRAIN_HEIGHT; + } + if (methodName.equals("getBlockState")) { + recorder.stateColumns.add(((BlockPos) arguments[0]).getX()); + return Blocks.STONE.defaultBlockState(); + } + if (methodName.equals("getLevel")) { + return null; + } + if (methodName.equals("holderLookup")) { + return BuiltInRegistries.BLOCK; + } + if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (methodName.equals("equals")) { + return proxy == arguments[0]; + } + if (methodName.equals("toString")) { + return "occupancy-test-world"; + } + throw new UnsupportedOperationException(method.toString()); + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), + new Class[]{WorldGenLevel.class}, handler); + } + + private static final class ColumnRecorder { + private final Set heightColumns = new TreeSet<>(); + private final Set stateColumns = new TreeSet<>(); + } + + private static final class InlineSinglePoolElement extends SinglePoolElement { + private InlineSinglePoolElement( + StructureTemplate template, List processors) { + super(Either.right(template), + Holder.direct(new StructureProcessorList(processors)), + StructureTemplatePool.Projection.RIGID, + Optional.empty()); + } + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java new file mode 100644 index 000000000..edda00a7b --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java @@ -0,0 +1,160 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeStructureVolume; +import art.arcane.volmlib.util.collection.KList; +import net.minecraft.SharedConstants; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; +import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class NativeStructureVolumeIndexTest { + private static final String STRUCTURE_KEY = "minecraft:swamp_hut"; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void assembledPiecesBecomeWorldSpacePieceVolumes() { + StructureStart start = swampHut(0, 0, 4, 6); + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + + KList volumes = NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, start); + + assertEquals(1, volumes.size()); + NativeStructureVolume volume = volumes.getFirst(); + assertEquals(STRUCTURE_KEY, volume.structure()); + assertEquals(bounds.minX(), volume.minX()); + assertEquals(bounds.minY(), volume.minY()); + assertEquals(bounds.minZ(), volume.minZ()); + assertEquals(bounds.maxX(), volume.maxX()); + assertEquals(bounds.maxY(), volume.maxY()); + assertEquals(bounds.maxZ(), volume.maxZ()); + } + + @Test + public void invalidStartsContributeNoVolumes() { + assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START)); + assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, null)); + } + + @Test + public void coldAndCachedQueriesResolveIdenticalVolumes() { + CountingResolver resolver = new CountingResolver(0, 0); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver); + + KList cold = index.resolve(null, 0, 0, 15, 15); + int coldResolutions = resolver.resolutions(); + KList cached = index.resolve(null, 0, 0, 15, 15); + + assertTrue(coldResolutions > 0); + assertFalse(cold.isEmpty()); + assertEquals(cold, cached); + assertEquals(coldResolutions, resolver.resolutions()); + } + + @Test + public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() { + NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0)); + NativeStructureVolumeIndex second = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0)); + + assertEquals(first.resolve(null, 0, 0, 15, 15), second.resolve(null, 0, 0, 15, 15)); + assertEquals(first.resolve(null, -32, -32, 47, 47), second.resolve(null, -32, -32, 47, 47)); + } + + @Test + public void volumesOutsideTheQueryRectAreExcluded() { + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0)); + BoundingBox bounds = swampHut(0, 0, 4, 6).getPieces().getFirst().getBoundingBox(); + + KList hit = index.resolve(null, bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ()); + KList miss = index.resolve(null, + bounds.maxX() + 1, bounds.minZ(), bounds.maxX() + 8, bounds.maxZ()); + + assertEquals(1, hit.size()); + assertTrue(miss.isEmpty()); + } + + @Test + public void startsWithinTheOriginReachContributeAndBeyondItDoNot() { + int reach = NativeStructureVolumeIndex.originReachChunks(); + NativeStructureVolumeIndex reachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach, 0)); + NativeStructureVolumeIndex unreachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach + 1, 0)); + + assertFalse(reachable.resolve(null, 0, 0, 15, 15).isEmpty()); + assertTrue(unreachable.resolve(null, 0, 0, 15, 15).isEmpty()); + } + + @Test + public void volumeResolutionNeverReadsChunkOrOwnershipState() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource"))); + + assertFalse(source.contains("StructureManager")); + assertFalse(source.contains("getStartForStructure")); + assertFalse(source.contains("getAllStarts")); + assertFalse(source.contains("ChunkAccess")); + assertFalse(source.contains("NativeStructureOwnershipStore")); + assertFalse(source.contains("NativeStructureOwnershipRecovery")); + } + + @Test + public void vanillaVolumesAreGatedOnPackPolicy() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource"))); + + assertTrue(source.contains("NativeStructureGenerationPolicy.resolve(")); + assertTrue(source.contains("if (!decision.generate())")); + assertTrue(source.contains("isStructureChunk(")); + } + + private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) { + Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); + SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z); + return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece))); + } + + private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver { + private final AtomicInteger resolutions = new AtomicInteger(); + private final int originChunkX; + private final int originChunkZ; + + private CountingResolver(int originChunkX, int originChunkZ) { + this.originChunkX = originChunkX; + this.originChunkZ = originChunkZ; + } + + private int resolutions() { + return resolutions.get(); + } + + @Override + public KList volumesAt(Engine engine, int chunkX, int chunkZ) { + resolutions.incrementAndGet(); + if (chunkX != originChunkX || chunkZ != originChunkZ) { + return NativeStructureVolume.NONE; + } + return NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6)); + } + } +} diff --git a/adapters/bukkit/plugin/build.gradle b/adapters/bukkit/plugin/build.gradle new file mode 100644 index 000000000..88404947e --- /dev/null +++ b/adapters/bukkit/plugin/build.gradle @@ -0,0 +1,77 @@ +String apiVersion = providers.gradleProperty('apiVersion').get() +def mainClass = 'art.arcane.iris.Iris' +def bootstrapperClass = 'art.arcane.iris.IrisBootstrap' + +String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') + .orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186') + .get() + +dependencies { + compileOnly(project(':core')) + compileOnly(libs.lombok) + annotationProcessor(libs.lombok) + compileOnly(volmLibCoordinate) { + transitive = false + } + compileOnly(libs.paper.api) + // LogFilterSVC (constructed in Iris.enable's explicit service list) implements the + // log4j-core Filter interface, so its constructor reference needs the type resolvable. + compileOnly(libs.log4j.core) + testImplementation('junit:junit:4.13.2') + testImplementation('org.mockito:mockito-core:5.23.0') + testImplementation(libs.paper.api) + testImplementation(libs.bstats) + testImplementation(libs.sentry) + testImplementation(project(':core')) + testImplementation(volmLibCoordinate) { + transitive = false + } + compileOnly(libs.placeholderApi) + testImplementation(libs.placeholderApi) { + transitive = false + } +} + +tasks.named('processResources').configure { + def pluginProperties = [ + name : rootProject.name, + version : rootProject.version, + apiVersion: apiVersion, + main : mainClass, + bootstrapper: bootstrapperClass, + ] + inputs.properties(pluginProperties) + filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) { + expand(pluginProperties) + } +} + +tasks.named('compileJava', JavaCompile).configure { + // lombok.config decides how equals/hashCode/toString are generated (fields, never getters), so editing it + // changes this module's bytecode. Gradle does not know that on its own and would serve a stale up-to-date + // build until something else in the module changed. + inputs.file(rootProject.file('lombok.config')) + .withPropertyName('lombokConfig') + .withPathSensitivity(PathSensitivity.NONE) +} + +tasks.named('jar', Jar).configure { + archiveBaseName.set('iris-bukkit-plugin') +} + +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) +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java new file mode 100644 index 000000000..40e5da5cc --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -0,0 +1,1273 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris; + +import art.arcane.iris.engine.IrisEngineEffects; +import art.arcane.iris.engine.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.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.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.LogLevel; +import art.arcane.volmlib.integration.ReloadAware; +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.volmlib.util.function.NastyRunnable; +import art.arcane.volmlib.util.hotload.ConfigHotloadEngine; +import art.arcane.volmlib.util.hud.HudActionBar; +import art.arcane.volmlib.util.hud.HudBossBarLane; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.io.InstanceState; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; +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.volmlib.util.scheduling.Queue; +import art.arcane.volmlib.util.scheduling.ShurikenQueue; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileOutputStream; +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.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@SuppressWarnings("CanBeFinal") +public class Iris extends VolmitPlugin implements Listener, ReloadAware { + private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L; + private static final long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L; + private static final Queue syncJobs = new ShurikenQueue<>(); + + static { + System.setProperty("iris.cache.fast", "true"); + } + + public static Iris instance; + public static Bindings.Adventure audiences; + public static MultiverseCoreLink linkMultiverseCore; + public static IrisCompat compat; + public static ConfigHotloadEngine configHotloadEngine; + public static ChunkTickets tickets; + private static VolmitSender sender; + private static Thread shutdownHook; + private static final StackWalker DEBUG_STACK_WALKER = StackWalker.getInstance(); + static { + try { + InstanceState.updateInstanceId(); + } catch (Throwable ex) { + System.err.println("[Iris] Failed to update instance id: " + ex.getClass().getSimpleName() + + (ex.getMessage() == null ? "" : " - " + ex.getMessage())); + ex.printStackTrace(); + } + } + + private static final Object TEARDOWN_LOCK = new Object(); + private final AtomicBoolean alreadyDrained = new AtomicBoolean(false); + private final AtomicBoolean postStopFinisherStarted = new AtomicBoolean(false); + private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false); + private final AtomicBoolean servicesDisabled = new AtomicBoolean(false); + private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false); + private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false); + private volatile PlaceholderRegistration papiRegistration; + private volatile IrisPapiListener papiListener; + private volatile IrisPapiState papiState; + private KMap, IrisService> services; + // Copy-on-write: mutated on the main thread during enable() and iterated by the JVM + // shutdown-hook thread during teardown; a plain list would CME and abort the teardown. + private final List enabledServices = new CopyOnWriteArrayList<>(); + private final List deferredShutdownGenerators = new CopyOnWriteArrayList<>(); + private final IrisWorldGeneratorResolver generatorResolver = new IrisWorldGeneratorResolver(this); + private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this); + private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this); + private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this); + private volatile SettingsHotloadWatch settingsHotloadWatch; + private volatile Thread serverLifecycleThread; + + public static VolmitSender getSender() { + if (sender == null) { + sender = new VolmitSender(Bukkit.getConsoleSender()); + sender.setTag(instance.getTag()); + } + return sender; + } + + @SuppressWarnings("unchecked") + public static T service(Class c) { + return (T) instance.services.get(c); + } + + public static void callEvent(Event e) { + Runnable dispatcher = () -> { + try { + Bukkit.getPluginManager().callEvent(e); + } catch (Throwable ex) { + reportError("Event dispatch failed for \"" + e.getEventName() + "\".", ex); + if (ex instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (ex instanceof Error error) { + throw error; + } + throw new IllegalStateException(ex); + } + }; + if (!e.isAsynchronous()) { + J.s(dispatcher); + } else { + dispatcher.run(); + } + } + + static boolean isConcreteImplementation(Class candidate, Class requiredType) { + int modifiers = candidate.getModifiers(); + return requiredType.isAssignableFrom(candidate) + && !candidate.isInterface() + && !Modifier.isAbstract(modifiers); + } + + public static void sq(Runnable r) { + synchronized (syncJobs) { + syncJobs.queue(r); + } + } + + public static File getTemp() { + return instance.getDataFolder("cache", "temp"); + } + + public static void msg(String string) { + try { + getSender().sendMessage(string); + } catch (Throwable e) { + try { + instance.getLogger().info(instance.getTag() + IrisLogging.clean(string)); + } catch (Throwable inner) { + System.err.println("[Iris] Failed to emit log message: " + inner.getMessage()); + inner.printStackTrace(System.err); + } + } + } + + /** + * A warning raised here is the same kind of thing as one raised in core, so it takes the same route: the + * plugin logger at WARNING, where a log scan of logs/latest.log finds it. + */ + public static void warn(String format, Object... objs) { + diagnostic(Level.WARNING, safeFormat(format, objs)); + } + + public static void error(String format, Object... objs) { + diagnostic(Level.SEVERE, safeFormat(format, objs)); + } + + public static void debug(String string) { + if (!IrisSettings.get().getGeneral().isDebug()) { + return; + } + + StackWalker.StackFrame frame = null; + try { + frame = DEBUG_STACK_WALKER.walk(stream -> stream.skip(1).findFirst().orElse(null)); + } catch (Throwable ignored) { + } + + if (frame == null) { + debug("Origin", -1, string); + return; + } + + String className = frame.getClassName(); + String[] cc = className == null ? new String[0] : className.split("\\Q.\\E"); + int line = frame.getLineNumber(); + + if (cc.length > 5) { + debug(cc[3] + "/" + cc[4] + "/" + cc[cc.length - 1], line, string); + return; + } + + if (cc.length > 4) { + debug(cc[3] + "/" + cc[4], line, string); + return; + } + + if (cc.length > 0) { + debug(cc[cc.length - 1], line, string); + return; + } + + debug("Origin", line, string); + } + + public static void debug(String category, int line, String string) { + if (!IrisSettings.get().getGeneral().isDebug()) { + return; + } + if (IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { + msg("" + category + " <#bf3b76>" + line + " " + C.LIGHT_PURPLE + string.replaceAll("\\Q<\\E", "[").replaceAll("\\Q>\\E", "]")); + } else { + msg(C.BLUE + category + ":" + C.AQUA + line + C.RESET + C.LIGHT_PURPLE + " " + string.replaceAll("\\Q<\\E", "[").replaceAll("\\Q>\\E", "]")); + + } + } + + public static void verbose(String string) { + debug(string); + } + + public static void success(String string) { + msg(C.IRIS + string); + } + + public static void info(String format, Object... args) { + msg(C.WHITE + safeFormat(format, args)); + } + + private static String safeFormat(String format, Object... args) { + return IrisLogging.format(format, args); + } + + public static void later(NastyRunnable object) { + try { + J.a(() -> { + try { + object.run(); + } catch (Throwable e) { + e.printStackTrace(); + Iris.reportError(e); + } + }, RNG.r.i(100, 1200)); + } catch (IllegalPluginAccessException ex) { + Iris.verbose("Skipping deferred task registration because plugin access is unavailable: " + + ex.getClass().getSimpleName() + + (ex.getMessage() == null ? "" : " - " + ex.getMessage())); + } + } + + public static int jobCount() { + return syncJobs.size(); + } + + public static void clearQueues() { + synchronized (syncJobs) { + syncJobs.clear(); + } + } + + public static int getJavaVersion() { + String version = System.getProperty("java.version"); + if (version.startsWith("1.")) { + version = version.substring(2, 3); + } else { + int dot = version.indexOf("."); + if (dot != -1) { + version = version.substring(0, dot); + } + } + return Integer.parseInt(version); + } + + public static String getJava() { + String javaRuntimeName = System.getProperty("java.vm.name"); + String javaRuntimeVendor = System.getProperty("java.vendor"); + String javaRuntimeVersion = System.getProperty("java.vm.version"); + return String.format("%s %s (build %s)", javaRuntimeName, javaRuntimeVendor, javaRuntimeVersion); + } + + public static void reportErrorChunk(int x, int z, Throwable e, String extra) { + if (IrisSettings.get().getGeneral().isDebug()) { + File f = instance.getDataFile("debug", "chunk-errors", "chunk." + x + "." + z + ".txt"); + + if (!f.exists()) { + J.attempt(() -> { + PrintWriter pw = new PrintWriter(f); + pw.println("Thread: " + Thread.currentThread().getName()); + pw.println("First: " + new Date(M.ms())); + e.printStackTrace(pw); + pw.close(); + }); + } + + Iris.debug("Chunk " + x + "," + z + " Exception Logged: " + e.getClass().getSimpleName() + ": " + C.RESET + "" + C.LIGHT_PURPLE + e.getMessage()); + } + } + + public static void reportError(Throwable e) { + if (e == null) { + 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) { + debug = false; + } + } + + if (debug) { + String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber(); + + if (e.getCause() != null) { + n += "-" + e.getCause().getStackTrace()[0].getClassName() + "-" + e.getCause().getStackTrace()[0].getLineNumber(); + } + + File f = instance.getDataFile("debug", "caught-exceptions", n + ".txt"); + + if (!f.exists()) { + J.attempt(() -> { + PrintWriter pw = new PrintWriter(f); + pw.println("Thread: " + Thread.currentThread().getName()); + pw.println("First: " + new Date(M.ms())); + e.printStackTrace(pw); + pw.close(); + }); + } + + Iris.debug("Exception Logged: " + e.getClass().getSimpleName() + ": " + C.RESET + "" + C.LIGHT_PURPLE + e.getMessage()); + } + } + + 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; + + try { + if (instance != null) { + Iris.error(message); + } else { + System.err.println("[Iris] " + message); + } + } catch (Throwable inner) { + System.err.println("[Iris] " + message); + inner.printStackTrace(System.err); + } + + reportError(error); + error.printStackTrace(System.err); + } + + public static void dump() { + try { + File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt"); + FileOutputStream fos = new FileOutputStream(fi); + Map f = Thread.getAllStackTraces(); + PrintWriter pw = new PrintWriter(fos); + for (Thread i : f.keySet()) { + pw.println("========================================"); + pw.println("Thread: '" + i.getName() + "' ID: " + i.threadId() + " STATUS: " + i.getState().name()); + + for (StackTraceElement j : f.get(i)) { + pw.println(" @ " + j.toString()); + } + + pw.println("========================================"); + pw.println(); + pw.println(); + } + pw.println("[%%__USER__%%,%%__RESOURCE__%%,%%__PRODUCT__%%,%%__BUILTBYBIT__%%]"); + + pw.close(); + Iris.info("DUMPED! See " + fi.getAbsolutePath()); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + public static void panic() { + EnginePanic.panic(); + } + + public static void addPanic(String s, String v) { + EnginePanic.add(s, v); + } + + public Iris() { + instance = this; + BukkitPlatform.hostPlugin(this); + BukkitPlatform.hostConsoleSender(Iris::getSender); + BukkitPlatform.hostBridge(new BukkitPlatform.HostBridge( + Iris::bridgeLog, + 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())); + SlimJar.load(); + } + + private static void bridgeLog(LogLevel level, String message) { + LogLevel target = level == null ? LogLevel.INFO : level; + Level diagnostic = diagnosticLevel(target); + if (diagnostic != null) { + diagnostic(diagnostic, message); + return; + } + if (target == LogLevel.DEBUG) { + Iris.debug(message); + return; + } + Iris.info(message); + } + + /** + * The java.util.logging level a message keeps, or null when it belongs on the console sender path. + *

+ * Core states a severity and every other adapter honours it; on Bukkit a coloured line through the + * console sender reaches the terminal but not the instance's logs/latest.log, so a WARN-level scan of + * that file never saw a single core warning. Diagnostics go to the plugin logger at their own level + * instead, and NOTICE carries the handful of lifecycle lines that have to land there too. Player-facing + * text still goes through {@code IrisLogging.msg}. + */ + static Level diagnosticLevel(LogLevel level) { + return switch (level) { + case NOTICE -> Level.INFO; + case WARN -> Level.WARNING; + case ERROR -> Level.SEVERE; + case DEBUG, INFO -> null; + }; + } + + private static void diagnostic(Level level, String message) { + String line = IrisLogging.clean(message); + Iris plugin = instance; + if (plugin != null) { + try { + plugin.getLogger().log(level, line); + return; + } catch (Throwable unavailable) { + // Paper runs the bootstrap before a plugin logger exists; the streams below are all there is. + } + } + if (level.intValue() >= Level.WARNING.intValue()) { + System.err.println("[Iris] " + line); + return; + } + System.out.println("[Iris] " + line); + } + + /** + * @return false when the bootstrap was aborted (unsupported server version); the caller + * must bail out of onEnable without touching any further setup. + */ + private boolean enable() { + if (!INMS.isBound()) { + Throwable bindFailure = INMS.bindFailure(); + Iris.error("Iris cannot start: " + (bindFailure == null + ? "no NMS binding is available for this server version." + : bindFailure.getMessage())); + // Deferred one tick: disablePlugin from inside onEnable re-enters onDisable + // synchronously and the loader then continues registering the half-enabled plugin. + J.s(() -> Bukkit.getPluginManager().disablePlugin(this), 1); + return false; + } + alreadyDrained.set(false); + postStopFinisherStarted.set(false); + serverStopTeardownDeferred.set(false); + servicesDisabled.set(false); + sharedRuntimeClosed.set(false); + terminalCleanupCompleted.set(false); + deferredShutdownGenerators.clear(); + MultiBurst.burst.reopen(); + MultiBurst.ioBurst.reopen(); + IrisLanguage.initialize(); + PaperLibBootstrap.install(); + SimdSupport.install(); + services = new KMap<>(); + setupAudience(); + 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. + List orderedServices = List.of( + new PreservationSVC(), + new GlobalCacheSVC(), + new LogFilterSVC(), + new ExternalDataSVC(), + new EditSVC(), + new ObjectSVC(), + new ObjectStudioSaveService(), + new JigsawStudioService(), + new StudioSVC(), + new DatapackStructureScopeSVC(), + new IrisEngineSVC(), + new IrisTerrainSVC(), + new TreeSVC(), + new TreeFellerSVC(), + new EntityRiseSVC(), + new WandSVC(), + new BoardSVC(), + new IrisIntegrationService(), + new MultiverseSVC(), + new IrisProtocolService(), + new IrisApiEventSVC(), + new CommandSVC() + ); + for (IrisService i : orderedServices) { + Class serviceType = i.getClass().asSubclass(IrisService.class); + services.put(serviceType, i); + IrisServices.register(serviceType, i); + } + IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class)); + IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class)); + compat = IrisCompat.configured(getDataFile("compat.json")); + IrisServices.register(IrisCompat.class, compat); + ServerConfigurator.configure(); + StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup(); + if (datapackValidation == StartupValidationOutcome.READY) { + generatorResolver.validateAllPacks(); + } + IrisSafeguard.execute(); + getSender().setTag(getTag()); + // A cosmetic banner must never abort the bootstrap. + J.attempt(this::splash); + IrisSafeguard.printReports(); + IrisSafeguard.printFooter(); + // 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(); + tickets = new ChunkTickets(); + linkMultiverseCore = new MultiverseCoreLink(); + IrisServices.register(MultiverseCoreLink.class, linkMultiverseCore); + IrisServices.register(EngineComponentCleanup.class, (EngineComponentCleanup) BukkitPlatform::unregisterListener); + IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) IrisEngineEffects::new); + IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks()); + IrisServices.register(EngineWorldManagerProvider.class, + (EngineWorldManagerProvider) IrisWorldManager::new); + IrisServices.register(WorldDeletionQueue.class, pendingWorldDeletes); + IrisServices.register(ManagedWorldLoader.class, (ManagedWorldLoader) this::loadManagedWorld); + SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("settings.json")); + settingsHotloadWatch = watch; + configHotloadEngine = new ConfigHotloadEngine( + watch::isSettingsFile, + watch::knownSettingsFiles, + watch::readSettingsContent, + watch::normalizeSettingsContent + ); + configHotloadEngine.configure(500L, 3_000L, List.of(watch.settingsFile()), List.of()); + // 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/ without dimensions/). + IO.delete(getTemp()); + // 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) { + try { + service.onEnable(); + enabledServices.add(service); + } 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, + // continue degraded, and clean up whatever the partial onEnable started + // (a failed service is excluded from the teardown loop). + Iris.reportError("Failed to enable " + service.getClass().getSimpleName() + "; continuing with a degraded runtime.", e); + try { + service.onDisable(); + } catch (Throwable cleanup) { + Iris.reportError("Failed to clean up partially enabled " + service.getClass().getSimpleName() + ".", cleanup); + } + } + } + for (IrisService service : enabledServices) { + try { + registerListener(service); + } catch (Throwable e) { + Iris.reportError("Failed to register listener for " + service.getClass().getSimpleName() + ".", e); + } + } + addShutdownHook(); + pendingWorldReplacements.processPendingStartupReplacements(); + pendingWorldDeletes.processPendingStartupWorldDeletes(); + + if (J.isFolia() && IrisStartupValidation.isReady()) { + J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1); + } + + J.s(() -> { + pendingWorldReplacements.captureVanillaLevelContext(); + pendingWorldReplacements.verifyLoadedPublishedWorlds(); + J.a(this::bstats); + J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 10); + J.sr(this::tickQueue, 0); + J.s(this::setupPapi); + if (IrisStartupValidation.isReady()) { + J.a(DatapackIngestService::runPostStartupTasks, 60); + autoStartStudio(); + } + if (!J.isFolia() && IrisStartupValidation.isReady()) { + worldReconciler.checkForBukkitWorlds(s -> true); + } + 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()); + }); + return true; + } + + public void addShutdownHook() { + removeShutdownHook(); + serverLifecycleThread = Thread.currentThread(); + shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook"); + try { + Runtime.getRuntime().addShutdownHook(shutdownHook); + } catch (IllegalStateException ex) { + Iris.debug("Skipping shutdown hook registration because JVM shutdown is already in progress."); + } + } + + /** + * The static-field guard in addShutdownHook is dead across a plugin reload (fresh + * classloader, fresh static), so onDisable must deregister the hook explicitly or each + * reload stacks another hook pinning the previous plugin classloader for the JVM's life. + */ + public void removeShutdownHook() { + Thread hook = shutdownHook; + shutdownHook = null; + if (hook == null) { + return; + } + try { + Runtime.getRuntime().removeShutdownHook(hook); + } catch (IllegalStateException ignored) { + Iris.debug("Skipping shutdown hook removal because JVM shutdown is already in progress."); + } + } + + public BukkitWorldReconciler worldReconciler() { + return worldReconciler; + } + + /** + * The load every integration goes through. {@code /iris load} and the Multiverse guard both land here, + * so a world loaded from outside Iris still gets the keyed creator, the pack's environment and the + * bukkit.yml reconciliation that make it an Iris world rather than a vanilla one under the same name. + */ + private CompletableFuture loadManagedWorld(String configuredWorldName) { + return worldReconciler.loadWorld(ServerProperties.BUKKIT_YML, configuredWorldName) + .thenApply(result -> new ManagedWorldLoader.ManagedWorldLoad(result.succeeded(), result.message())); + } + + public PendingWorldReplacementManager pendingWorldReplacements() { + return pendingWorldReplacements; + } + + private void autoStartStudio() { + if (IrisSettings.get().getStudio().isAutoStartDefaultStudio()) { + Iris.info("Starting up auto Studio!"); + try { + Player r = new KList<>(getServer().getOnlinePlayers()).getRandom(); + Iris.service(StudioSVC.class).open(r != null ? new VolmitSender(r) : getSender(), 1337, IrisSettings.get().getGenerator().getDefaultWorldType(), (w) -> { + J.s(() -> { + final Location spawn = w.getSpawnLocation(); + for (Player i : getServer().getOnlinePlayers()) { + final Runnable playerTask = () -> { + i.setGameMode(GameMode.CREATIVE); + BukkitPlatform.teleportAsync(i, spawn); + }; + if (!J.runEntity(i, playerTask)) { + playerTask.run(); + } + } + }); + }); + } catch (IrisException e) { + reportError(e); + } + } + } + + private void setupAudience() { + try { + audiences = new Bindings.Adventure(this); + BukkitPlatform.hostAudiences(audiences); + } catch (Throwable e) { + e.printStackTrace(); + IrisSettings.get().getGeneral().setUseConsoleCustomColors(false); + IrisSettings.get().getGeneral().setUseCustomColorsIngame(false); + Iris.error("Failed to setup Adventure API... No custom colors :("); + } + } + + public void onEnable() { + IrisPlatforms.bind(new BukkitPlatform()); + IrisStartupValidation.begin(); + Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this); + Bukkit.getPluginManager().registerEvents(pendingWorldReplacements, this); + pendingWorldReplacements.registerPlatformEntryListener(); + boolean enabled; + try { + enabled = enable(); + } catch (Throwable failure) { + refuseVanillaFallback(failure); + throw failure; + } + if (!enabled) { + refuseVanillaFallback(null); + return; + } + BukkitGuiHost.install(); + // super.onEnable() already registers this instance as a listener. + super.onEnable(); + } + + /** + * Stops a server whose Iris worlds would otherwise be generated by the vanilla generator. + *

+ * A disabled Iris gets no {@code getDefaultWorldGenerator} call at all, so the server falls back to + * vanilla for every world bukkit.yml points at Iris and writes vanilla terrain into their region files. + * There is no Bukkit API that refuses a world at that point, so the server is stopped instead. This is + * damage control, not prevention: level creation runs in the same startup step that enables plugins, so + * spawn chunks of the affected worlds can still be written before the stop takes effect. The prevention + * lives in IrisBootstrap, which refuses startup before any level is created. + */ + private static void refuseVanillaFallback(Throwable failure) { + File levelRoot; + try { + levelRoot = IrisWorldStorage.levelRoot(); + } catch (Throwable unavailable) { + return; + } + if (!IrisWorldStorage.hasManagedWorldStorage(levelRoot)) { + return; + } + Iris.error("Iris did not enable and this server has Iris worlds; stopping the server before they generate vanilla terrain."); + if (failure != null) { + Iris.reportError("Iris enable failed", failure); + } + try { + Bukkit.shutdown(); + } catch (Throwable unavailable) { + Iris.error("Could not stop the server: " + unavailable.getClass().getSimpleName()); + } + } + + public void onDisable() { + teardownPapi(); + boolean serverStopping = IrisToolbelt.isServerStopping(); + if (serverStopping) { + quiesceRuntimeForServerShutdown("onDisable"); + startPostStopFinisher(); + } else { + teardownRuntime("onDisable", 30L); + removeShutdownHook(); + } + if (BukkitPlatform.hasHud()) { + BukkitPlatform.hudBar().shutdown(); + BukkitPlatform.hudLanes().shutdown(); + } + if (configHotloadEngine != null) { + configHotloadEngine.clear(); + configHotloadEngine = null; + } + // super.onDisable() cancels plugin tasks and unregisters every listener. + super.onDisable(); + if (!serverStopping) { + finishTerminalCleanup(); + } + } + + @Override + public void onPreUnload(ReloadAware.PreUnloadReason reason) { + teardownPapi(); + if (IrisToolbelt.isServerStopping()) { + quiesceRuntimeForServerShutdown("pre-unload:" + reason); + startPostStopFinisher(); + Iris.info("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers."); + return; + } + if (alreadyDrained.get()) { + Iris.info("Pre-unload hook skipped; Iris already drained."); + return; + } + Iris.info("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds."); + drainOnce("pre-unload:" + reason, 45L); + } + + /** + * Drains the world generators exactly once. Serialized against the JVM shutdown hook so a + * second caller cannot rip the pools or services out from under an in-flight drain. + */ + private void drainOnce(String reason, long timeoutSeconds) { + synchronized (TEARDOWN_LOCK) { + if (alreadyDrained.compareAndSet(false, true)) { + drainWorldGenerators(reason, timeoutSeconds); + } + } + } + + /** + * Full teardown: generators, then services, then the shared pools and the service map. + * Both onDisable and the JVM shutdown hook route through here; whichever runs second is a no-op. + */ + private void teardownRuntime(String reason, long timeoutSeconds) { + synchronized (TEARDOWN_LOCK) { + if (alreadyDrained.compareAndSet(false, true)) { + drainWorldGenerators(reason, timeoutSeconds); + } + + if (services != null && servicesDisabled.compareAndSet(false, true)) { + // Only services whose onEnable actually completed; disabling a service that + // never initialized runs teardown against uninitialized state. + for (IrisService service : enabledServices) { + try { + service.onDisable(); + } catch (Throwable e) { + Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e); + } + } + } + + if (!sharedRuntimeClosed.compareAndSet(false, true)) { + return; + } + + J.attempt(MultiBurst.burst::close); + J.attempt(MultiBurst.ioBurst::close); + clearQueues(); + IrisServices.clear(); + } + } + + private void quiesceRuntimeForServerShutdown(String reason) { + serverStopTeardownDeferred.set(true); + JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class); + if (jigsawStudioService != null) { + try { + jigsawStudioService.quiesceForServerShutdown(); + } catch (Throwable e) { + Iris.reportError("Failed to quiesce Jigsaw Studio before server shutdown.", e); + } + } + StudioSVC studioService = IrisServices.getOrNull(StudioSVC.class); + if (studioService != null) { + studioService.quiesceDownloadsForShutdown(); + } + + try { + PregeneratorJob.shutdownAndWait(SERVER_STOP_PREGEN_TIMEOUT_MILLIS); + } catch (Throwable e) { + Iris.reportError("Failed to quiesce the Iris pregenerator before server shutdown.", e); + } + + for (World world : Bukkit.getWorlds()) { + PlatformChunkGenerator generator = IrisToolbelt.access(world); + if (generator == null) { + continue; + } + IrisToolbelt.beginWorldMaintenance(world, reason, true); + if (!deferredShutdownGenerators.contains(generator)) { + deferredShutdownGenerators.add(generator); + } + generator.quiesceForServerShutdown(); + } + } + + private void startPostStopFinisher() { + if (!postStopFinisherStarted.compareAndSet(false, true)) { + return; + } + Thread activeServerThread = serverLifecycleThread; + if (activeServerThread == null) { + Iris.warn("Iris could not start its post-stop runtime finisher because the server lifecycle thread is unavailable."); + return; + } + + Thread finisher = new Thread(() -> { + if (!awaitServerThreadTermination(activeServerThread)) { + return; + } + finishDeferredRuntimeTeardown("post-server-stop", 30L); + }, "Iris-PostStop-Finisher"); + finisher.setDaemon(false); + finisher.start(); + } + + static boolean awaitServerThreadTermination(Thread serverThread) { + if (serverThread == null || serverThread == Thread.currentThread()) { + return false; + } + try { + serverThread.join(); + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Iris.reportError("Iris post-stop runtime finisher was interrupted.", e); + return false; + } + } + + private void runShutdownHook() { + if (!awaitServerShutdownBoundary()) { + Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary."); + return; + } + finishDeferredRuntimeTeardown("shutdown-hook", 30L); + } + + private boolean awaitServerShutdownBoundary() { + if (!INMS.isBound()) { + return true; + } + try { + return INMS.get().awaitServerShutdownBoundary( + SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS, + TimeUnit.SECONDS + ); + } catch (Throwable e) { + Iris.reportError("Failed to await Paper's post-world-close shutdown boundary.", e); + return false; + } + } + + private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) { + teardownRuntime(reason, timeoutSeconds); + finishTerminalCleanup(); + } + + private void finishTerminalCleanup() { + if (!terminalCleanupCompleted.compareAndSet(false, true)) { + return; + } + J.attempt(() -> INMS.get().uninjectBukkit()); + try { + runPostShutdown(); + } catch (Throwable e) { + Iris.reportError("Failed to run Iris post-shutdown cleanup.", e); + } finally { + IrisPlatforms.unbind(); + } + } + + private void drainWorldGenerators(String reason, long timeoutSeconds) { + List irisWorlds = new ArrayList<>(); + List generators = new ArrayList<>(); + if (serverStopTeardownDeferred.get()) { + generators.addAll(deferredShutdownGenerators); + } else { + for (World world : Bukkit.getWorlds()) { + PlatformChunkGenerator generator = IrisToolbelt.access(world); + if (generator != null) { + irisWorlds.add(world); + generators.add(generator); + } + } + } + if (generators.isEmpty()) { + Iris.info("No Iris worlds to freeze."); + return; + } + + for (World world : irisWorlds) { + IrisToolbelt.beginWorldMaintenance(world, reason, true); + } + + J.attempt(PregeneratorJob::shutdownInstance); + + List> closes = new ArrayList<>(); + for (PlatformChunkGenerator generator : generators) { + try { + closes.add(generator.closeAsync()); + } catch (Throwable t) { + Iris.reportError(t); + } + } + + if (closes.isEmpty()) return; + + try { + CompletableFuture.allOf(closes.toArray(new CompletableFuture[0])) + .get(timeoutSeconds, TimeUnit.SECONDS); + Iris.info("All Iris chunk generators parked. Safe to unload."); + } catch (TimeoutException e) { + Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway."); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Iris.warn("Iris generator drain interrupted; unload proceeding."); + } catch (ExecutionException e) { + Iris.reportError(e.getCause() == null ? e : e.getCause()); + } + } + + private void setupPapi() { + if (!PlaceholderRegistration.isPlaceholderApiEnabled()) { + return; + } + + IrisPapiState state = new IrisPapiState(() -> IrisServices.getOrNull(IrisTerrainService.class)); + PlaceholderRegistration registration = new PlaceholderRegistration(getLogger()); + + if (!IrisPapiInstaller.install(registration, state, getLogger())) { + return; + } + + IrisPapiListener listener = new IrisPapiListener(state); + + try { + Bukkit.getPluginManager().registerEvents(listener, this); + } catch (Throwable failure) { + registration.unregister(); + Iris.warn("Failed to attach the Iris PlaceholderAPI listener: " + + failure.getClass().getName() + ": " + failure.getMessage()); + return; + } + + papiState = state; + papiListener = listener; + papiRegistration = registration; + } + + private void teardownPapi() { + IrisPapiListener listener = papiListener; + papiListener = null; + + if (listener != null) { + HandlerList.unregisterAll(listener); + } + + PlaceholderRegistration registration = papiRegistration; + papiRegistration = null; + + if (registration != null) { + registration.unregister(); + } + + IrisPapiState state = papiState; + papiState = null; + + if (state != null) { + state.clear(); + } + } + + @Override + public void start() { + + } + + @Override + public void stop() { + + } + + @Override + public String getTag(String subTag) { + return IrisSafeguard.mode().tag(subTag); + } + + private void tickQueue() { + synchronized (Iris.syncJobs) { + if (!Iris.syncJobs.hasNext()) { + return; + } + + long ms = M.ms(); + + while (Iris.syncJobs.hasNext() && M.ms() - ms < 25) { + try { + Iris.syncJobs.next().run(); + } catch (Throwable e) { + e.printStackTrace(); + Iris.reportError(e); + } + } + } + } + + private void bstats() { + if (IrisSettings.get().getGeneral().isPluginMetrics()) { + Bindings.setupBstats(this); + } + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + return super.onCommand(sender, command, label, args); + } + + public void imsg(CommandSender s, String msg) { + s.sendMessage(C.IRIS + "[" + C.DARK_GRAY + "Iris" + C.IRIS + "]" + C.GRAY + ": " + msg); + } + + @Nullable + @Override + public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) { + return generatorResolver.resolveDefaultBiomeProvider(worldName, id, () -> super.getDefaultBiomeProvider(worldName, id)); + } + + @Nullable + @Override + public ChunkGenerator getDefaultWorldGenerator(@NotNull String worldName, @Nullable String id) { + return generatorResolver.resolveDefaultWorldGenerator(worldName, id); + } + + public void splash() { + Iris.info("Custom Biomes: " + INMS.get().countCustomBiomes()); + printPacks(); + + IrisSafeguard.mode().trySplash(); + } + + private void printPacks() { + File packFolder = Iris.service(StudioSVC.class).getWorkspaceFolder(); + for (String line : IrisSplashComposer.composePackLines(packFolder, Iris::reportError)) { + Iris.info(line); + } + } + + public int getIrisVersion() { + String input = Iris.instance.getDescription().getVersion(); + int hyphenIndex = input.indexOf('-'); + if (hyphenIndex != -1) { + String result = input.substring(0, hyphenIndex); + result = result.replaceAll("\\.", ""); + return Integer.parseInt(result); + } + return -1; + } + + public int getMCVersion() { + try { + String version = Bukkit.getVersion(); + Matcher matcher = Pattern.compile("\\(MC: ([\\d.]+)\\)").matcher(version); + if (matcher.find()) { + version = matcher.group(1).replaceAll("\\.", ""); + long versionNumber = Long.parseLong(version); + if (versionNumber > Integer.MAX_VALUE) { + return -1; + } + return (int) versionNumber; + } + return -1; + } catch (Exception e) { + return -1; + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/IrisBootstrap.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/IrisBootstrap.java new file mode 100644 index 000000000..039b3a5f5 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/IrisBootstrap.java @@ -0,0 +1,173 @@ +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 io.papermc.paper.plugin.bootstrap.BootstrapContext; +import io.papermc.paper.plugin.bootstrap.PluginBootstrap; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.function.Consumer; + +@SuppressWarnings("UnstableApiUsage") +public final class IrisBootstrap implements PluginBootstrap { + @Override + public void bootstrap(BootstrapContext context) { + WorldReplacementBootstrapMarker.markBootstrapped(); + try { + BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent(); + reconcilePendingWorldReplacements(context, startupPaths); + quarantineWorthlessHusks(startupPaths, message -> context.getLogger().warn(message)); + requireUsableWorldStorage(startupPaths); + ProvisionResult provisioned = provision(context, startupPaths); + Path datapackRoot = provisioned.datapackRoot(); + context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot); + } catch (Throwable failure) { + armStartupFailure(context, failure); + } + } + + private static void reconcilePendingWorldReplacements( + BootstrapContext context, + BukkitStartupPaths startupPaths + ) { + try { + WorldReplacementBootstrap.ReconcileResult result = WorldReplacementBootstrap.reconcile( + context.getDataDirectory(), + startupPaths.levelRoot(), + startupPaths.bukkitConfiguration(), + message -> context.getLogger().info(message) + ); + if (result.transactions() > 0) { + context.getLogger().info( + "Reconciled {} pending Iris world replacement(s): {} published, {} rolled back, {} retained", + result.transactions(), + result.published(), + result.rolledBack(), + result.retained() + ); + } + } catch (IOException failure) { + throw new IllegalStateException( + "Unable to reconcile pending Iris world replacements before registry bootstrap", + failure + ); + } + } + + /** + * Moves a hot-deleted world's husk out of {@code /dimensions/iris} before anything enumerates + * it. + *

+ * Deleting a loaded world's folder and letting the server save the level back leaves a directory holding + * only the server's own {@code data/} skeleton. Left there it stops the boot whichever way it is + * classified: as unusable storage it trips the guard below, and as an empty folder it is excluded from + * the dimension registry, which trips Paper's interactive world-migration gate - a prompt that consumes + * no console input and hangs startup forever. The folder is moved rather than deleted, so it stays + * recoverable; {@link HuskWorldQuarantine} refuses anything that is not provably worthless. + */ + static void quarantineWorthlessHusks(BukkitStartupPaths startupPaths, Consumer warn) { + HuskWorldQuarantine.quarantineWorthlessHusks(startupPaths.levelRoot(), warn); + } + + /** + * Stops startup when a world the server is about to load carries Iris storage Iris cannot generate from. + *

+ * The server enumerates levels from {@code /dimensions//} on disk, so a world + * whose folder is present is going to be loaded whatever Iris does; and CraftBukkit swallows whatever + * {@code getDefaultWorldGenerator} throws and falls back to the vanilla generator, which writes vanilla + * terrain straight into that world's region files. A pack snapshot that has gone missing therefore has + * to be found here, before any level is created, exactly as a corrupt {@code dimensions/.json} is. + *

+ * A world with no folder at all is not a startup failure: nothing enumerates it, nothing loads it, and + * its bukkit.yml and Multiverse entries are what make restoring the folder a complete recovery. Neither + * is a folder that holds no pack snapshot and no world data - {@link #quarantineWorthlessHusks} has + * already moved those out of the dimensions tree, and one that survives it (a Spigot-layout world folder, + * or a move that failed) owns nothing a vanilla generator could destroy. Both are only reported. + */ + static void requireUsableWorldStorage(BukkitStartupPaths startupPaths) throws IOException { + Path levelRoot = startupPaths.levelRoot(); + Path levelName = levelRoot.getFileName(); + if (levelName == null || levelName.toString().isBlank()) { + throw new IOException("Configured level root has no startup level id: " + levelRoot); + } + List entries = BukkitWorldConfiguration.auditIrisWorldStorage( + startupPaths.bukkitConfiguration().toFile(), + levelName.toString(), + levelRoot + ); + StringBuilder unusable = new StringBuilder(); + for (IrisWorldStorageEntry entry : entries) { + switch (entry.state()) { + case PRESENT -> { + } + case MISSING -> MissingWorldStorageLog.warnOnce( + entry.configuredWorldName(), + entry.storagePath()); + case EMPTY -> MissingWorldStorageLog.warnEmptyOnce( + entry.configuredWorldName(), + entry.storagePath()); + case UNUSABLE -> unusable.append(unusable.isEmpty() ? "" : "; ") + .append(entry.configuredWorldName()) + .append(" (") + .append(entry.storagePath()) + .append(": ") + .append(entry.detail() == null ? "storage is unusable" : entry.detail()) + .append(')'); + } + } + if (unusable.isEmpty()) { + return; + } + throw new IllegalStateException("Iris world storage is unusable and the server would generate vanilla" + + " terrain over it: " + unusable + + ". Restore each world folder from a backup, or move it aside; a world with no folder is" + + " reported and skipped, and the server starts."); + } + + static void armStartupFailure(BootstrapContext context, Throwable failure) { + armStartupFailure(context, failure, LifecycleEvents.DATAPACK_DISCOVERY); + } + + static void armStartupFailure( + BootstrapContext context, + Throwable failure, + LifecycleEventType eventType + ) { + context.getLifecycleManager().registerEventHandler(eventType, event -> { + throw new IllegalStateException("Iris bootstrap did not establish safe world-generation state.", failure); + }); + try { + context.getLogger().error( + "Iris bootstrap failed; registry and world startup will be stopped at datapack discovery.", + failure + ); + } catch (Throwable loggingFailure) { + failure.addSuppressed(loggingFailure); + failure.printStackTrace(System.err); + } + } + + private static ProvisionResult provision(BootstrapContext context, BukkitStartupPaths startupPaths) { + try { + return DefaultPackBootstrapProvisioner.provision( + context.getDataDirectory(), + message -> context.getLogger().info(message), + startupPaths + ); + } catch (IOException e) { + throw new IllegalStateException("Unable to provision the Iris startup datapack", e); + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenPhase.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenPhase.java new file mode 100644 index 000000000..32d916e50 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenPhase.java @@ -0,0 +1,11 @@ +package art.arcane.iris.api.pregen; + +public enum IrisPregenPhase { + STARTED, + TICK, + PAUSED, + RESUMED, + SAVING, + COMPLETED, + CANCELLED +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenProgress.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenProgress.java new file mode 100644 index 000000000..e0365d053 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenProgress.java @@ -0,0 +1,31 @@ +package art.arcane.iris.api.pregen; + +import java.util.Objects; + +public record IrisPregenProgress( + String worldName, + String worldIdentity, + double percent, + long generatedChunks, + long totalChunks, + long remainingChunks, + long failedChunks, + double chunksPerSecond, + long etaMillis, + long elapsedMillis, + String method, + boolean paused) { + public IrisPregenProgress { + Objects.requireNonNull(worldIdentity, "worldIdentity"); + worldName = worldName == null ? worldIdentity : worldName; + method = method == null ? "" : method; + percent = Double.isFinite(percent) ? Math.clamp(percent, 0D, 100D) : 0D; + generatedChunks = Math.max(0L, generatedChunks); + totalChunks = Math.max(0L, totalChunks); + remainingChunks = Math.max(0L, remainingChunks); + failedChunks = Math.max(0L, failedChunks); + chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D; + etaMillis = Math.max(0L, etaMillis); + elapsedMillis = Math.max(0L, elapsedMillis); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenerationEvent.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenerationEvent.java new file mode 100644 index 000000000..f8b08037f --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/pregen/IrisPregenerationEvent.java @@ -0,0 +1,35 @@ +package art.arcane.iris.api.pregen; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import java.util.Objects; + +public class IrisPregenerationEvent extends Event { + private static final HandlerList HANDLERS = new HandlerList(); + + private final IrisPregenPhase phase; + private final IrisPregenProgress progress; + + public IrisPregenerationEvent(IrisPregenPhase phase, IrisPregenProgress progress) { + this.phase = Objects.requireNonNull(phase, "phase"); + this.progress = Objects.requireNonNull(progress, "progress"); + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + public IrisPregenPhase getPhase() { + return phase; + } + + public IrisPregenProgress getProgress() { + return progress; + } + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnField.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnField.java new file mode 100644 index 000000000..1d3748e42 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnField.java @@ -0,0 +1,7 @@ +package art.arcane.iris.api.terrain; + +public enum IrisColumnField { + SURFACE_HEIGHT, + SURFACE_KIND, + BIOME_KEY +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnQuery.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnQuery.java new file mode 100644 index 000000000..b1ce3aa3b --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnQuery.java @@ -0,0 +1,61 @@ +package art.arcane.iris.api.terrain; + +import java.util.EnumSet; +import java.util.Objects; + +public record IrisColumnQuery( + int minBlockX, + int minBlockZ, + int maxBlockX, + int maxBlockZ, + int strideBlocks, + EnumSet fields) { + public IrisColumnQuery { + Objects.requireNonNull(fields, "fields"); + if (fields.isEmpty()) { + throw new IllegalArgumentException("at least one field is required"); + } + if (maxBlockX < minBlockX || maxBlockZ < minBlockZ) { + throw new IllegalArgumentException("query bounds are inverted"); + } + if (strideBlocks < 1) { + throw new IllegalArgumentException("strideBlocks must be at least 1"); + } + fields = EnumSet.copyOf(fields); + } + + public static IrisColumnQuery rect( + int minBlockX, + int minBlockZ, + int maxBlockX, + int maxBlockZ, + int strideBlocks, + EnumSet fields) { + return new IrisColumnQuery(minBlockX, minBlockZ, maxBlockX, maxBlockZ, strideBlocks, fields); + } + + public long columnCount() { + long columnsX = (((long) maxBlockX - (long) minBlockX) / strideBlocks) + 1L; + long columnsZ = (((long) maxBlockZ - (long) minBlockZ) / strideBlocks) + 1L; + return saturatedProduct(columnsX, columnsZ); + } + + public long chunkCount() { + long chunksX = ((long) (maxBlockX >> 4) - (long) (minBlockX >> 4)) + 1L; + long chunksZ = ((long) (maxBlockZ >> 4) - (long) (minBlockZ >> 4)) + 1L; + return saturatedProduct(chunksX, chunksZ); + } + + private static long saturatedProduct(long left, long right) { + try { + return Math.multiplyExact(left, right); + } catch (ArithmeticException overflow) { + return Long.MAX_VALUE; + } + } + + @Override + public EnumSet fields() { + return EnumSet.copyOf(fields); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSink.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSink.java new file mode 100644 index 000000000..4c6bccffe --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSink.java @@ -0,0 +1,6 @@ +package art.arcane.iris.api.terrain; + +@FunctionalInterface +public interface IrisColumnSink { + void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey); +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisSurfaceKind.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisSurfaceKind.java new file mode 100644 index 000000000..be60360b3 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisSurfaceKind.java @@ -0,0 +1,9 @@ +package art.arcane.iris.api.terrain; + +public enum IrisSurfaceKind { + UNKNOWN, + LAND, + SHORE, + OCEAN, + VOID +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisTerrainService.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisTerrainService.java new file mode 100644 index 000000000..dc4dd7666 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisTerrainService.java @@ -0,0 +1,32 @@ +package art.arcane.iris.api.terrain; + +import org.bukkit.World; + +import java.util.Optional; +import java.util.OptionalInt; + +public interface IrisTerrainService { + boolean isIrisWorld(World world); + + Optional worldInfo(World world); + + OptionalInt surfaceHeight(World world, int blockX, int blockZ); + + IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ); + + Optional surfaceBiomeKey(World world, int blockX, int blockZ); + + Optional surfaceBiomeName(World world, int blockX, int blockZ); + + Optional biomeKey(World world, int blockX, int blockY, int blockZ); + + Optional regionKey(World world, int blockX, int blockZ); + + Optional regionName(World world, int blockX, int blockZ); + + int maxSampleColumns(); + + int maxSampleChunks(); + + boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink); +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisWorldInfo.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisWorldInfo.java new file mode 100644 index 000000000..617021983 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisWorldInfo.java @@ -0,0 +1,24 @@ +package art.arcane.iris.api.terrain; + +import java.util.Objects; + +public record IrisWorldInfo( + String dimensionKey, + String worldIdentity, + long seed, + int minHeight, + int maxHeight, + int fluidHeight, + boolean studio) { + public IrisWorldInfo { + Objects.requireNonNull(dimensionKey, "dimensionKey"); + Objects.requireNonNull(worldIdentity, "worldIdentity"); + if (maxHeight <= minHeight) { + throw new IllegalArgumentException("maxHeight must exceed minHeight"); + } + } + + public int height() { + return maxHeight - minHeight; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/IrisTreeFellerService.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/IrisTreeFellerService.java new file mode 100644 index 000000000..944062bf1 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/IrisTreeFellerService.java @@ -0,0 +1,12 @@ +package art.arcane.iris.api.tree; + +import org.bukkit.block.Block; +import org.bukkit.event.block.BlockBreakEvent; + +public interface IrisTreeFellerService { + boolean tryFell(BlockBreakEvent event, TreeFellerOptions options); + + boolean isManagedBreak(BlockBreakEvent event); + + boolean isTreeBlock(Block block); +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerAccess.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerAccess.java new file mode 100644 index 000000000..17631b89d --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerAccess.java @@ -0,0 +1,6 @@ +package art.arcane.iris.api.tree; + +public enum TreeFellerAccess { + STANDALONE, + INTEGRATION_OVERRIDE +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerOptions.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerOptions.java new file mode 100644 index 000000000..4a7191764 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerOptions.java @@ -0,0 +1,32 @@ +package art.arcane.iris.api.tree; + +import java.util.Objects; + +public record TreeFellerOptions( + TreeFellerAccess access, + int durabilityPreservationChance, + TreeFellerRunHooks runHooks +) { + public TreeFellerOptions { + Objects.requireNonNull(access, "access"); + Objects.requireNonNull(runHooks, "runHooks"); + if (durabilityPreservationChance < 0 || durabilityPreservationChance > 100) { + throw new IllegalArgumentException("durabilityPreservationChance must be between 0 and 100"); + } + } + + public static TreeFellerOptions standalone() { + return new TreeFellerOptions(TreeFellerAccess.STANDALONE, 0, TreeFellerRunHooks.NONE); + } + + public static TreeFellerOptions integrationOverride( + int durabilityPreservationChance, + TreeFellerRunHooks runHooks + ) { + return new TreeFellerOptions( + TreeFellerAccess.INTEGRATION_OVERRIDE, + durabilityPreservationChance, + runHooks + ); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerRunHooks.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerRunHooks.java new file mode 100644 index 000000000..ba4ee17ee --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/tree/TreeFellerRunHooks.java @@ -0,0 +1,30 @@ +package art.arcane.iris.api.tree; + +public interface TreeFellerRunHooks { + TreeFellerRunHooks NONE = new TreeFellerRunHooks() { + @Override + public void onActivationAccepted() { + } + + @Override + public boolean reserveLogCost() { + return true; + } + + @Override + public void commitLogCost() { + } + + @Override + public void refundLogCost() { + } + }; + + void onActivationAccepted(); + + boolean reserveLogCost(); + + void commitLogCost(); + + void refundLogCost(); +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/world/IrisWorldEngineEvent.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/world/IrisWorldEngineEvent.java new file mode 100644 index 000000000..162057879 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/world/IrisWorldEngineEvent.java @@ -0,0 +1,44 @@ +package art.arcane.iris.api.world; + +import art.arcane.iris.api.terrain.IrisWorldInfo; +import org.bukkit.World; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import java.util.Objects; +import java.util.Optional; + +public class IrisWorldEngineEvent extends Event { + private static final HandlerList HANDLERS = new HandlerList(); + + private final World world; + private final IrisWorldPhase phase; + private final IrisWorldInfo info; + + public IrisWorldEngineEvent(World world, IrisWorldPhase phase, IrisWorldInfo info) { + this.world = Objects.requireNonNull(world, "world"); + this.phase = Objects.requireNonNull(phase, "phase"); + this.info = info; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + public World getWorld() { + return world; + } + + public IrisWorldPhase getPhase() { + return phase; + } + + public Optional getInfo() { + return Optional.ofNullable(info); + } + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/world/IrisWorldPhase.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/world/IrisWorldPhase.java new file mode 100644 index 000000000..51d63f515 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/world/IrisWorldPhase.java @@ -0,0 +1,7 @@ +package art.arcane.iris.api.world; + +public enum IrisWorldPhase { + ENGINE_READY, + ENGINE_HOTLOADED, + ENGINE_CLOSING +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java new file mode 100644 index 000000000..e49e955be --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java @@ -0,0 +1,794 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration; +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.platform.bukkit.BukkitEnvironment; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.misc.ServerProperties; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.generator.ChunkGenerator; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public final class BukkitWorldReconciler { + private static final long WORLD_CREATE_TIMEOUT_SECONDS = 120L; + + private final Backend backend; + private final LifecycleOperationCoordinator coordinator; + + public BukkitWorldReconciler(Iris plugin) { + this(new BukkitBackend(plugin), LifecycleOperationCoordinator.get()); + } + + BukkitWorldReconciler(Backend backend, LifecycleOperationCoordinator coordinator) { + this.backend = Objects.requireNonNull(backend, "backend"); + this.coordinator = Objects.requireNonNull(coordinator, "coordinator"); + } + + public CompletableFuture loadWorld( + File configurationFile, + String worldName + ) { + NamespacedKey worldKey; + try { + worldKey = IrisWorldStorage.managedKeyFromName(worldName); + } catch (Throwable failure) { + return CompletableFuture.completedFuture(LoadResult.validationFailure(worldName, failure)); + } + + LifecycleOperationCoordinator.Lease lease; + try { + lease = acquireWorldLoad(worldKey); + } catch (LifecycleOperationCoordinator.BusyException failure) { + return CompletableFuture.completedFuture(LoadResult.busy(worldKey, failure)); + } + + DimensionResolution dimensionResolution; + Long configuredSeed; + String configuredWorldName; + try { + configuredWorldName = backend.configuredWorldName(worldKey); + dimensionResolution = backend.resolveDimension(worldKey); + configuredSeed = dimensionResolution.succeeded() + ? backend.configuredSeed(configuredWorldName) + : null; + } catch (Throwable failure) { + dimensionResolution = DimensionResolution.failed(failure); + configuredSeed = null; + configuredWorldName = IrisWorldStorage.logicalName(worldKey); + } + if (!dimensionResolution.succeeded()) { + lease.close(); + return CompletableFuture.completedFuture(LoadResult.dimensionFailure( + worldKey, + dimensionResolution.failure())); + } + return loadWithLease( + configurationFile, + configuredWorldName, + worldKey, + dimensionResolution.dimension(), + configuredSeed, + lease); + } + + public CompletableFuture checkForBukkitWorlds(Predicate filter) { + Predicate requiredFilter = Objects.requireNonNull(filter, "filter"); + Map configuredWorlds; + try { + configuredWorlds = backend.configuredWorlds(); + } catch (Throwable failure) { + Iris.reportError("Failed while reading staged Bukkit worlds.", failure); + return CompletableFuture.completedFuture(new BatchResult(List.of(), failure)); + } + + CompletableFuture> chain = CompletableFuture.completedFuture(new ArrayList<>()); + for (Map.Entry entry : configuredWorlds.entrySet()) { + String worldName = entry.getKey(); + boolean selected; + try { + selected = requiredFilter.test(worldName); + } catch (Throwable failure) { + Iris.reportError("Failed while filtering staged Bukkit world \"" + worldName + "\".", failure); + return CompletableFuture.completedFuture(new BatchResult(List.of(), failure)); + } + if (!selected) { + continue; + } + NamespacedKey worldKey; + try { + worldKey = backend.worldKeyFromConfiguration(worldName); + } catch (Throwable failure) { + chain = chain.thenApply(results -> { + results.add(LoadResult.validationFailure(worldName, failure)); + return results; + }); + continue; + } + String dimension = entry.getValue(); + Long seed; + try { + seed = backend.configuredSeed(worldName); + } catch (Throwable failure) { + chain = chain.thenApply(results -> { + results.add(LoadResult.configurationFailure(worldKey, failure)); + return results; + }); + continue; + } + chain = chain.thenCompose(results -> loadConfiguredWorld( + ServerProperties.BUKKIT_YML, + worldName, + worldKey, + dimension, + seed) + .thenApply(result -> { + results.add(result); + return results; + })); + } + + return chain.thenApply(results -> { + BatchResult batchResult = new BatchResult(results, null); + reportBatch(batchResult); + return batchResult; + }); + } + + private CompletableFuture loadConfiguredWorld( + File configurationFile, + String configuredWorldName, + NamespacedKey worldKey, + String dimension, + Long seed + ) { + LifecycleOperationCoordinator.Lease lease; + try { + lease = acquireWorldLoad(worldKey); + } catch (LifecycleOperationCoordinator.BusyException failure) { + return CompletableFuture.completedFuture(LoadResult.busy(worldKey, failure)); + } + + return loadWithLease(configurationFile, configuredWorldName, worldKey, dimension, seed, lease); + } + + private LifecycleOperationCoordinator.Lease acquireWorldLoad(NamespacedKey worldKey) { + return coordinator.acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_LOAD, + worldKey.toString()); + } + + private CompletableFuture loadWithLease( + File configurationFile, + String configuredWorldName, + NamespacedKey worldKey, + String dimension, + Long seed, + LifecycleOperationCoordinator.Lease lease + ) { + + try { + IrisStartupValidation.requireWorldCreationReady(); + backend.requireDimensionLoadable(worldKey, dimension); + } catch (Throwable failure) { + lease.close(); + return CompletableFuture.completedFuture(LoadResult.dimensionFailure(worldKey, failure)); + } + + BukkitWorldConfiguration.Registration registration; + try { + registration = BukkitWorldConfiguration.register( + configurationFile, + configuredWorldName, + dimension, + seed); + } catch (Throwable failure) { + lease.close(); + return CompletableFuture.completedFuture(LoadResult.configurationFailure(worldKey, failure)); + } + + CompletableFuture reconciliation; + try { + reconciliation = reconcile(worldKey, dimension, seed); + } catch (Throwable failure) { + reconciliation = CompletableFuture.completedFuture(ReconciliationResult.createFailure(worldKey, failure)); + } + + return reconciliation.handle((result, failure) -> { + ReconciliationResult settled = failure == null + ? result + : ReconciliationResult.createFailure(worldKey, unwrap(failure)); + if (settled == null) { + settled = ReconciliationResult.createFailure( + worldKey, + new IllegalStateException("World reconciliation completed without a result.")); + } + if (settled.succeeded() + || settled.status() == ReconciliationStatus.RESTART_REQUIRED + || registration != BukkitWorldConfiguration.Registration.CREATED) { + return new LoadResult(settled, registration, false, true, null); + } + try { + boolean rolledBack = BukkitWorldConfiguration.removeIfMatching( + configurationFile, + configuredWorldName, + dimension, + seed); + return new LoadResult(settled, registration, true, rolledBack, null); + } catch (Throwable rollbackFailure) { + return new LoadResult(settled, registration, true, false, rollbackFailure); + } + }) + .whenComplete((result, failure) -> lease.close()); + } + + private CompletableFuture reconcile( + NamespacedKey worldKey, + String dimension, + Long seed + ) { + Optional loaded = backend.loadedWorld(worldKey); + if (loaded.isPresent()) { + return CompletableFuture.completedFuture(verifyLoadedWorld(worldKey, loaded.get(), true)); + } + + CompletableFuture created; + try { + created = Objects.requireNonNull( + backend.createWorld(worldKey, dimension, seed), + "World backend returned no creation future."); + } catch (Throwable failure) { + return CompletableFuture.completedFuture(classifyCreationFailure(worldKey, failure)); + } + + CompletableFuture guardedCreation = guardCreateCompletion( + created, + worldKey, + TimeUnit.SECONDS.toMillis(WORLD_CREATE_TIMEOUT_SECONDS), + () -> ServerConfigurator.restart("World load timed out for \"" + worldKey + "\".")); + return guardedCreation.handle((createdWorld, failure) -> { + if (failure != null) { + return classifyCreationFailure(worldKey, unwrap(failure)); + } + if (createdWorld == null) { + return ReconciliationResult.notLoaded(worldKey); + } + + NamespacedKey createdKey; + try { + createdKey = WorldIdentity.key(createdWorld); + } catch (Throwable identityFailure) { + return ReconciliationResult.createFailure(worldKey, identityFailure); + } + if (!worldKey.equals(createdKey)) { + return ReconciliationResult.identityMismatch(worldKey, createdWorld, createdKey); + } + + Optional resolved = backend.loadedWorld(worldKey); + if (resolved.isEmpty()) { + return ReconciliationResult.notLoaded(worldKey); + } + return verifyLoadedWorld(worldKey, resolved.get(), false); + }); + } + + private ReconciliationResult verifyLoadedWorld(NamespacedKey worldKey, World loadedWorld, boolean alreadyLoaded) { + NamespacedKey loadedKey; + try { + loadedKey = WorldIdentity.key(loadedWorld); + } catch (Throwable identityFailure) { + return ReconciliationResult.createFailure(worldKey, identityFailure); + } + if (!worldKey.equals(loadedKey)) { + return ReconciliationResult.identityMismatch(worldKey, loadedWorld, loadedKey); + } + if (!backend.isIrisWorld(loadedWorld)) { + return ReconciliationResult.identityConflict(worldKey, loadedWorld); + } + return alreadyLoaded + ? ReconciliationResult.alreadyLoaded(worldKey, loadedWorld) + : ReconciliationResult.loaded(worldKey, loadedWorld); + } + + private static ReconciliationResult classifyCreationFailure(NamespacedKey worldKey, Throwable failure) { + Throwable cause = unwrap(failure); + if (cause instanceof TimeoutException || containsCreateWorldUnsupportedOperation(cause)) { + return ReconciliationResult.restartRequired(worldKey, cause); + } + return ReconciliationResult.createFailure(worldKey, cause); + } + + static CompletableFuture guardCreateCompletion( + CompletableFuture source, + NamespacedKey worldKey, + long timeoutMillis, + Runnable timeoutAction + ) { + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(worldKey, "worldKey"); + Objects.requireNonNull(timeoutAction, "timeoutAction"); + if (timeoutMillis < 1L) { + throw new IllegalArgumentException("timeoutMillis must be positive"); + } + + CompletableFuture guarded = new CompletableFuture<>(); + AtomicBoolean settled = new AtomicBoolean(false); + source.whenComplete((world, throwable) -> { + if (!settled.compareAndSet(false, true)) { + return; + } + if (throwable == null) { + guarded.complete(world); + } else { + guarded.completeExceptionally(unwrap(throwable)); + } + }); + CompletableFuture.delayedExecutor(timeoutMillis, TimeUnit.MILLISECONDS).execute(() -> { + if (!settled.compareAndSet(false, true)) { + return; + } + TimeoutException timeout = new TimeoutException( + "World load did not settle within " + timeoutMillis + " milliseconds for \"" + + worldKey + "\"."); + try { + timeoutAction.run(); + } catch (Throwable failure) { + timeout.addSuppressed(failure); + } + guarded.completeExceptionally(timeout); + }); + return guarded; + } + + private static void reportBatch(BatchResult batchResult) { + for (LoadResult result : batchResult.results()) { + if (result.succeeded()) { + Iris.info(C.LIGHT_PURPLE + result.message()); + continue; + } + if (result.status() == ReconciliationStatus.BUSY) { + Iris.warn(result.message()); + continue; + } + Iris.error(result.message()); + Throwable failure = result.failure(); + if (failure != null) { + Iris.reportError("Failed to reconcile staged world \"" + result.worldKey() + "\".", failure); + } + } + } + + private static Throwable unwrap(Throwable failure) { + Throwable current = failure; + while (current instanceof CompletionException && current.getCause() != null) { + current = current.getCause(); + } + return current; + } + + private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) { + Throwable cursor = throwable; + while (cursor != null) { + if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) { + for (StackTraceElement element : cursor.getStackTrace()) { + if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName()) + && "createWorld".equals(element.getMethodName())) { + return true; + } + } + } + cursor = cursor.getCause(); + } + return false; + } + + interface Backend { + Map configuredWorlds(); + + Long configuredSeed(String worldName); + + String configuredWorldName(NamespacedKey worldKey); + + NamespacedKey worldKeyFromConfiguration(String configuredWorldName); + + Optional loadedWorld(NamespacedKey worldKey); + + CompletableFuture createWorld(NamespacedKey worldKey, String dimension, Long seed); + + boolean isIrisWorld(World world); + + DimensionResolution resolveDimension(NamespacedKey worldKey); + + void requireDimensionLoadable(NamespacedKey worldKey, String dimension); + } + + public enum ReconciliationStatus { + LOADED, + ALREADY_LOADED, + BUSY, + INVALID_WORLD, + DIMENSION_UNRESOLVED, + CONFIGURATION_FAILED, + CREATE_FAILED, + RESTART_REQUIRED, + IDENTITY_MISMATCH, + IDENTITY_CONFLICT, + NOT_LOADED + } + + public record ReconciliationResult( + ReconciliationStatus status, + NamespacedKey worldKey, + World world, + Throwable failure, + String message + ) { + public ReconciliationResult { + Objects.requireNonNull(status, "status"); + Objects.requireNonNull(message, "message"); + } + + public boolean succeeded() { + return status == ReconciliationStatus.LOADED || status == ReconciliationStatus.ALREADY_LOADED; + } + + private static ReconciliationResult loaded(NamespacedKey worldKey, World world) { + return new ReconciliationResult( + ReconciliationStatus.LOADED, + worldKey, + world, + null, + "Loaded Iris world \"" + worldKey + "\"."); + } + + private static ReconciliationResult alreadyLoaded(NamespacedKey worldKey, World world) { + return new ReconciliationResult( + ReconciliationStatus.ALREADY_LOADED, + worldKey, + world, + null, + "Iris world \"" + worldKey + "\" is already loaded."); + } + + private static ReconciliationResult createFailure(NamespacedKey worldKey, Throwable failure) { + return new ReconciliationResult( + ReconciliationStatus.CREATE_FAILED, + worldKey, + null, + failure, + "Failed to create Iris world \"" + worldKey + "\": " + failure.getMessage()); + } + + private static ReconciliationResult restartRequired(NamespacedKey worldKey, Throwable failure) { + return new ReconciliationResult( + ReconciliationStatus.RESTART_REQUIRED, + worldKey, + null, + failure, + "The server cannot load exact Iris world \"" + worldKey + "\" at this runtime phase."); + } + + private static ReconciliationResult identityMismatch( + NamespacedKey worldKey, + World world, + NamespacedKey actualKey + ) { + return new ReconciliationResult( + ReconciliationStatus.IDENTITY_MISMATCH, + worldKey, + world, + null, + "World creation returned \"" + actualKey + "\" instead of \"" + worldKey + "\"."); + } + + private static ReconciliationResult identityConflict(NamespacedKey worldKey, World world) { + return new ReconciliationResult( + ReconciliationStatus.IDENTITY_CONFLICT, + worldKey, + world, + null, + "World \"" + worldKey + "\" is loaded, but it is not an Iris world."); + } + + private static ReconciliationResult notLoaded(NamespacedKey worldKey) { + return new ReconciliationResult( + ReconciliationStatus.NOT_LOADED, + worldKey, + null, + null, + "World creation completed without loading exact Iris world \"" + worldKey + "\"."); + } + } + + public record LoadResult( + ReconciliationResult reconciliation, + BukkitWorldConfiguration.Registration registration, + boolean rollbackAttempted, + boolean rollbackSucceeded, + Throwable rollbackFailure + ) { + public LoadResult { + Objects.requireNonNull(reconciliation, "reconciliation"); + } + + public boolean succeeded() { + return reconciliation.succeeded() && rollbackFailure == null; + } + + public ReconciliationStatus status() { + return reconciliation.status(); + } + + public NamespacedKey worldKey() { + return reconciliation.worldKey(); + } + + public World world() { + return reconciliation.world(); + } + + public Throwable failure() { + return rollbackFailure == null ? reconciliation.failure() : rollbackFailure; + } + + public String message() { + if (rollbackFailure != null) { + return reconciliation.message() + " Failed to roll back bukkit.yml: " + rollbackFailure.getMessage(); + } + if (rollbackAttempted && rollbackSucceeded) { + return reconciliation.message() + " The new bukkit.yml entry was rolled back."; + } + if (rollbackAttempted && !rollbackSucceeded) { + return reconciliation.message() + " The new bukkit.yml entry was no longer an exact match and was not modified."; + } + return reconciliation.message(); + } + + private static LoadResult validationFailure(String worldName, Throwable failure) { + ReconciliationResult reconciliation = new ReconciliationResult( + ReconciliationStatus.INVALID_WORLD, + null, + null, + failure, + "Invalid Iris world identifier \"" + worldName + "\": " + failure.getMessage()); + return new LoadResult(reconciliation, null, false, true, null); + } + + private static LoadResult busy(NamespacedKey worldKey, LifecycleOperationCoordinator.BusyException failure) { + ReconciliationResult reconciliation = new ReconciliationResult( + ReconciliationStatus.BUSY, + worldKey, + null, + failure, + failure.getMessage()); + return new LoadResult(reconciliation, null, false, true, null); + } + + private static LoadResult configurationFailure(NamespacedKey worldKey, Throwable failure) { + ReconciliationResult reconciliation = new ReconciliationResult( + ReconciliationStatus.CONFIGURATION_FAILED, + worldKey, + null, + failure, + "Failed to register Iris world \"" + worldKey + "\" in bukkit.yml: " + failure.getMessage()); + return new LoadResult(reconciliation, null, false, true, null); + } + + private static LoadResult dimensionFailure(NamespacedKey worldKey, Throwable failure) { + ReconciliationResult reconciliation = new ReconciliationResult( + ReconciliationStatus.DIMENSION_UNRESOLVED, + worldKey, + null, + failure, + "Could not determine one Iris dimension for world \"" + worldKey + "\": " + failure.getMessage()); + return new LoadResult(reconciliation, null, false, true, null); + } + } + + record DimensionResolution(String dimension, Throwable failure) { + DimensionResolution { + if ((dimension == null) == (failure == null)) { + throw new IllegalArgumentException("Dimension resolution must contain exactly one outcome."); + } + } + + static DimensionResolution resolved(String dimension) { + return new DimensionResolution(Objects.requireNonNull(dimension, "dimension"), null); + } + + static DimensionResolution failed(Throwable failure) { + return new DimensionResolution(null, Objects.requireNonNull(failure, "failure")); + } + + boolean succeeded() { + return dimension != null; + } + } + + public record BatchResult(List results, Throwable failure) { + public BatchResult { + results = List.copyOf(Objects.requireNonNull(results, "results")); + } + + public boolean succeeded() { + return failure == null && results.stream().allMatch(LoadResult::succeeded); + } + } + + private static final class BukkitBackend implements Backend { + private final Iris plugin; + + private BukkitBackend(Iris plugin) { + this.plugin = Objects.requireNonNull(plugin, "plugin"); + } + + @Override + public Map configuredWorlds() { + return new LinkedHashMap<>(IrisWorlds.readBukkitWorlds()); + } + + @Override + public Long configuredSeed(String worldName) { + return IrisWorlds.readBukkitWorldSeed(worldName); + } + + @Override + public String configuredWorldName(NamespacedKey worldKey) { + return IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName()); + } + + @Override + public NamespacedKey worldKeyFromConfiguration(String configuredWorldName) { + return IrisWorldStorage.keyFromConfiguredWorldName( + configuredWorldName, + IrisWorldStorage.levelRoot().getName() + ); + } + + @Override + public Optional loadedWorld(NamespacedKey worldKey) { + return WorldIdentity.resolve(worldKey); + } + + @Override + public CompletableFuture createWorld(NamespacedKey worldKey, String dimension, Long seed) { + try { + String logicalWorldName = IrisWorldStorage.logicalName(worldKey); + String configuredWorldName = configuredWorldName(worldKey); + Iris.info("Loading World: %s | Generator: %s", logicalWorldName, dimension); + ChunkGenerator generator = plugin.getDefaultWorldGenerator(configuredWorldName, dimension); + IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension); + if (generator == null || irisDimension == null) { + throw new IllegalStateException("Could not resolve the Iris generator or dimension \"" + dimension + "\"."); + } + + Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + logicalWorldName + " using Iris:" + dimension + "..."); + WorldCreator creator = WorldCreatorCompat.ofPersistentKey(worldKey) + .generator(generator) + .environment(BukkitEnvironment.from(irisDimension.getEnvironment())); + if (seed != null) { + creator.seed(seed); + } + return INMS.get().createWorldAsync(creator); + } catch (Throwable failure) { + return CompletableFuture.failedFuture(failure); + } + } + + @Override + public boolean isIrisWorld(World world) { + return IrisToolbelt.isIrisWorld(world); + } + + @Override + public DimensionResolution resolveDimension(NamespacedKey worldKey) { + File dimensionsDirectory = new File(snapshotRoot(worldKey), "dimensions"); + if (!dimensionsDirectory.isDirectory()) { + return DimensionResolution.failed(new IllegalStateException("The world has no Iris dimensions directory.")); + } + + List dimensions = new ArrayList<>(); + Path dimensionsRoot = dimensionsDirectory.toPath().toAbsolutePath().normalize(); + try (Stream paths = Files.walk(dimensionsRoot)) { + paths.filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) + .filter(path -> path.getFileName().toString().endsWith(".json")) + .forEach(path -> { + String relative = dimensionsRoot.relativize(path).toString().replace(File.separatorChar, '/'); + dimensions.add(relative.substring(0, relative.length() - 5)); + }); + } catch (IOException failure) { + return DimensionResolution.failed(new IllegalStateException( + "The Iris dimensions directory could not be read.", + failure)); + } + Collections.sort(dimensions); + + String registeredDimension = IrisWorlds.get().getWorlds().get(worldKey.toString()); + if (registeredDimension != null && dimensions.contains(registeredDimension)) { + return DimensionResolution.resolved(registeredDimension); + } + if (dimensions.size() == 1) { + return DimensionResolution.resolved(dimensions.getFirst()); + } + if (dimensions.isEmpty()) { + return DimensionResolution.failed(new IllegalStateException("No dimension definitions were found.")); + } + return DimensionResolution.failed(new IllegalStateException( + "Multiple dimension definitions were found without an exact registered dimension: " + + String.join(", ", dimensions))); + } + + @Override + public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) { + File snapshotRoot = snapshotRoot(worldKey); + IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot); + String configuredWorldName = configuredWorldName(worldKey); + IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension); + if (irisDimension == null) { + throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\"."); + } + } + + private File snapshotRoot(NamespacedKey worldKey) { + File levelRoot = IrisWorldStorage.levelRoot(); + File dimensionRoot = IrisWorldStorage.requireFrozenDimensionRoot( + Bukkit.getWorldContainer(), + levelRoot, + configuredWorldName(worldKey), + worldKey + ); + File expectedRoot = WorldCreatorCompat.persistentDimensionRoot(worldKey); + if (!dimensionRoot.toPath().toAbsolutePath().normalize() + .equals(expectedRoot.toPath().toAbsolutePath().normalize())) { + throw new IllegalStateException("Iris world storage does not match the current platform layout for " + + worldKey + "."); + } + return IrisWorldStorage.requireFrozenPackRoot(dimensionRoot); + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisProbeChunkGenerator.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisProbeChunkGenerator.java new file mode 100644 index 000000000..c38ea4c9e --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisProbeChunkGenerator.java @@ -0,0 +1,101 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import org.bukkit.HeightMap; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.BlockPopulator; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.generator.WorldInfo; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.Random; + +/** + * Answer to a generator-plugin discovery probe. + *

+ * Multiverse-Core calls {@code getDefaultWorldGenerator} with an empty dimension id and the name of + * an already loaded world purely to find out whether a plugin is a generator plugin, then throws the + * returned instance away. A non-null answer keeps Iris in {@code /mv generators} and in Multiverse's + * generator tab-completion without Multiverse logging a warning block on every boot. + *

+ * Nothing here can build terrain: every generation entry point refuses, so an instance that escapes + * the probe fails loudly instead of silently producing vanilla chunks. + */ +final class IrisProbeChunkGenerator extends ChunkGenerator { + private final String worldName; + + IrisProbeChunkGenerator(String worldName) { + this.worldName = Objects.requireNonNull(worldName, "worldName"); + } + + @Override + public void generateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) { + throw refusal(); + } + + @Override + public void generateSurface(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) { + throw refusal(); + } + + @Override + public void generateBedrock(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) { + throw refusal(); + } + + @Override + public void generateCaves(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) { + throw refusal(); + } + + @Override + public ChunkData generateChunkData(@NotNull World world, @NotNull Random random, int x, int z, @NotNull BiomeGrid biome) { + throw refusal(); + } + + @Override + public BiomeProvider getDefaultBiomeProvider(@NotNull WorldInfo worldInfo) { + throw refusal(); + } + + @Override + public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) { + throw refusal(); + } + + @Override + public List getDefaultPopulators(@NotNull World world) { + throw refusal(); + } + + @Override + public Location getFixedSpawnLocation(@NotNull World world, @NotNull Random random) { + throw refusal(); + } + + private IllegalStateException refusal() { + return new IllegalStateException("Iris generator-discovery probe for '" + worldName + + "' was asked to generate terrain. Iris worlds are created with /iris create."); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisStartupAdmissionListener.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisStartupAdmissionListener.java new file mode 100644 index 000000000..3ec38ddf1 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisStartupAdmissionListener.java @@ -0,0 +1,15 @@ +package art.arcane.iris.core; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; + +public final class IrisStartupAdmissionListener implements Listener { + @EventHandler(priority = EventPriority.LOWEST) + public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) { + IrisStartupValidation.denialReason().ifPresent(reason -> event.disallow( + AsyncPlayerPreLoginEvent.Result.KICK_OTHER, + reason + " Check the server console, correct the reported Iris state, and restart.")); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java new file mode 100644 index 000000000..534c099a1 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java @@ -0,0 +1,500 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.lifecycle.WorldLifecycleStaging; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.pack.PackDownloader; +import art.arcane.iris.core.pack.PackDirectoryResolver; +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.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import art.arcane.iris.util.common.plugin.VolmitPlugin; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import lombok.NonNull; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Pack validation, dimension lookup, and the world generator / biome provider resolution that the + * Bukkit plugin entry points delegate to. + */ +public final class IrisWorldGeneratorResolver { + private static final int VALIDATION_STABILITY_ATTEMPTS = 2; + private static final Object SNAPSHOT_VALIDATION_LOCK = new Object(); + private static final String IRIS_DIMENSION_NAMESPACE = "iris"; + private static final String PLOT_SQUARED_DISCOVERY_WORLD = "CheckingPlotSquaredGenerator"; + + private final VolmitPlugin plugin; + + public IrisWorldGeneratorResolver(VolmitPlugin plugin) { + this.plugin = plugin; + } + + public void validateAllPacks() { + File packsRoot = plugin.getDataFolder("packs"); + List packDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot); + PackValidationRegistry.clear(); + List packNames = packDirs.stream().map(File::getName).sorted().toList(); + Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath(); + ServerConfigurator.PackContentSnapshot contentSnapshot = + new ServerConfigurator.PackContentSnapshot("", Map.of()); + String contextFingerprint = ""; + Optional> cached = Optional.empty(); + try { + contentSnapshot = ServerConfigurator.computePackContentSnapshot(packsRoot); + contextFingerprint = PackValidationCache.contextFingerprint(); + cached = PackValidationCache.load( + cacheFile, + contentSnapshot.content(), + contextFingerprint, + packNames); + } catch (RuntimeException exception) { + Iris.reportError("Could not evaluate the persisted pack-validation cache", exception); + } + + List results; + if (cached.isPresent()) { + results = cached.get(); + Iris.info("Reused persisted validation for " + results.size() + + " unchanged Iris pack(s); full pack parsing was skipped."); + } else { + FreshValidation validation = validateStablePacks(packsRoot, packDirs, contentSnapshot); + packDirs = validation.packDirs(); + contentSnapshot = validation.contentSnapshot(); + results = validation.results(); + if (validation.stable()) { + try { + PackValidationCache.save( + cacheFile, + contentSnapshot.content(), + contextFingerprint, + results); + } catch (IOException exception) { + Iris.reportError("Could not persist Iris pack-validation results", exception); + } + } + } + + Map packFingerprints = contentSnapshot.packContents(); + for (PackValidationResult result : results) { + PackValidationRegistry.publish(result); + String packFingerprint = packFingerprints.get(result.getPackName()); + File packDirectory = PackDirectoryResolver.resolveExisting(packsRoot, result.getPackName()); + if (packDirectory != null && packFingerprint != null && !packFingerprint.isBlank()) { + PackValidationRegistry.publish(packDirectory.toPath(), result, packFingerprint); + } + if (!result.isLoadable()) { + Iris.error("Pack '" + result.getPackName() + + "' FAILED validation - world and Studio creation with this pack will be refused. Reasons:"); + for (String reason : result.getBlockingErrors()) { + Iris.error(" - " + reason); + } + } else if (!result.getWarnings().isEmpty()) { + Iris.info("Pack '" + result.getPackName() + "' validated (" + + result.getWarnings().size() + " warning(s))."); + for (String warning : result.getWarnings()) { + Iris.warn(" [" + result.getPackName() + "] " + warning); + } + } else if (cached.isEmpty()) { + Iris.success("Pack '" + result.getPackName() + "' validated."); + } + } + IrisStartupValidation.markPacksReady(); + } + + private static FreshValidation validateStablePacks( + File packsRoot, + List initialPackDirs, + ServerConfigurator.PackContentSnapshot initialSnapshot + ) { + List packDirs = initialPackDirs; + ServerConfigurator.PackContentSnapshot before = initialSnapshot; + for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) { + List packNames = packDirs.stream().map(File::getName).sorted().toList(); + List results = validatePacks(packDirs); + ServerConfigurator.PackContentSnapshot after; + try { + after = ServerConfigurator.computePackContentSnapshot(packsRoot); + } catch (RuntimeException exception) { + Iris.reportError("Could not verify Iris pack bytes after validation", exception); + after = new ServerConfigurator.PackContentSnapshot("", Map.of()); + } + List afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot); + List afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList(); + if (!before.content().isBlank() + && before.content().equals(after.content()) + && packNames.equals(afterPackNames)) { + return new FreshValidation(afterPackDirs, after, results, true); + } + packDirs = afterPackDirs; + before = after; + } + Iris.error("Iris pack files kept changing during validation; validation was refused until writes stop."); + List failures = new ArrayList<>(packDirs.size()); + for (File packDir : packDirs) { + failures.add(new PackValidationResult( + packDir.getName(), + List.of("Pack files changed while validation was in progress; retry after writes stop."), + List.of(), + System.currentTimeMillis())); + } + return new FreshValidation(packDirs, before, failures, false); + } + + private static List validatePacks(List packDirs) { + List results = new ArrayList<>(packDirs.size()); + for (File packDir : packDirs) { + try { + results.add(PackValidator.validate(packDir)); + } catch (Throwable exception) { + Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", exception); + String detail = exception.getMessage(); + if (detail == null || detail.isBlank()) { + detail = exception.getClass().getSimpleName(); + } + results.add(new PackValidationResult( + packDir.getName(), + List.of("Pack validation failed with " + exception.getClass().getSimpleName() + + ": " + detail), + List.of(), + System.currentTimeMillis())); + } + } + return results; + } + + static PackValidationResult requireSnapshotLoadable(File packRoot) { + Path normalizedRoot = packRoot.toPath().toAbsolutePath().normalize(); + PackValidationResult result = PackValidationRegistry.get(normalizedRoot); + if (result == null) { + synchronized (SNAPSHOT_VALIDATION_LOCK) { + result = PackValidationRegistry.get(normalizedRoot); + if (result == null) { + PackValidationRegistry.ValidationTicket ticket = + PackValidationRegistry.tryBeginValidation(normalizedRoot); + if (ticket != null) { + try { + result = PackValidator.validate(normalizedRoot.toFile()); + } catch (Throwable exception) { + Iris.reportError("Snapshot pack validation failed for '" + normalizedRoot + "'", exception); + String detail = exception.getMessage(); + if (detail == null || detail.isBlank()) { + detail = exception.getClass().getSimpleName(); + } + result = new PackValidationResult( + normalizedRoot.getFileName().toString(), + List.of("Pack validation failed with " + exception.getClass().getSimpleName() + + ": " + detail), + List.of(), + System.currentTimeMillis()); + } + PackValidationRegistry.publishIfCurrent(ticket, result); + } + } + } + } + return PackValidationRegistry.requireLoadable(normalizedRoot); + } + + @Nullable + public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) { + File levelRoot = IrisWorldStorage.levelRoot(); + NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot); + String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName()); + File pack = IrisWorldStorage.frozenDimensionRoot( + Bukkit.getWorldContainer(), + levelRoot, + configuredWorldName, + worldKey + ) + .map(IrisWorldStorage::requireFrozenPackRoot) + .orElse(null); + IrisDimension dimension = pack == null ? null : IrisData.get(pack).getDimensionLoader().load(id); + if (dimension == null) dimension = IrisData.loadAnyDimension(id, null); + if (dimension == null) { + File packsRoot = IrisPlatforms.get().packsFolderNoCreate(); + if (PackDownloader.isPackPresent(packsRoot, id)) { + Iris.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath() + + " but its dimension failed to load; not redownloading. Fix or delete the pack folder."); + return null; + } + Iris.warn("Unable to find dimension type " + id + ". Install its pack with " + + PackDownloader.downloadCommandFor(id) + " and restart the server."); + } + + return dimension; + } + + /** + * Resolves the Iris key behind a Bukkit world name, accepting the runtime keyed name alongside the + * startup name. + *

+ * Paper names a world built from {@code WorldCreator.ofKey} {@code _}, and Multiverse + * loads Iris worlds that way because its own keyed creator refuses a non-{@code minecraft} namespace. + * That is not the startup name, so the configured-name parser reads {@code iris_moon} as + * {@code iris:iris_moon} and loses the world. + *

+ * Only a name whose decoded key already has Iris storage is remapped, and only when the literal + * reading has none, so a world genuinely created as {@code /iris create "iris moon"} keeps its own + * identity and a server without Multiverse resolves exactly the names it resolved before. + */ + static NamespacedKey configuredWorldKey(String worldName, String levelName, File levelRoot) { + NamespacedKey literal = IrisWorldStorage.keyFromConfiguredWorldName(worldName, levelName); + String runtimePrefix = IRIS_DIMENSION_NAMESPACE + "_"; + if (!worldName.startsWith(runtimePrefix) + || worldName.startsWith(levelName + "_" + runtimePrefix) + || IrisWorldStorage.isExistingManagedDimensionRoot(levelRoot, literal)) { + return literal; + } + NamespacedKey runtime; + try { + runtime = IrisWorldStorage.managedKeyFromName( + IRIS_DIMENSION_NAMESPACE + ":" + worldName.substring(runtimePrefix.length()), + levelName); + } catch (RuntimeException notAManagedKey) { + return literal; + } + if (!IrisWorldStorage.isExistingManagedDimensionRoot(levelRoot, runtime)) { + return literal; + } + IrisLogging.debug("Resolved runtime keyed world name " + worldName + " as " + runtime); + return runtime; + } + + /** + * The key a refusal message should name for a world name, without the storage-existence guard. + *

+ * {@link #configuredWorldKey} only remaps the runtime keyed name when the decoded key has storage, so an + * orphan keeps the literal reading and a message built from it names {@code iris:iris_moon} and tells + * the admin to create a world that never existed under that name. Messaging does not have to be safe + * against mis-mapping a live world, only correct about what the admin typed, so it drops that guard; + * generation keeps it. + */ + static NamespacedKey messageWorldKey(String worldName, String levelName) { + NamespacedKey literal = IrisWorldStorage.keyFromConfiguredWorldName(worldName, levelName); + String runtimePrefix = IRIS_DIMENSION_NAMESPACE + "_"; + if (!worldName.startsWith(runtimePrefix) + || worldName.startsWith(levelName + "_" + runtimePrefix)) { + return literal; + } + try { + return IrisWorldStorage.managedKeyFromName( + IRIS_DIMENSION_NAMESPACE + ":" + worldName.substring(runtimePrefix.length()), + levelName); + } catch (RuntimeException notAManagedKey) { + return literal; + } + } + + /** + * Resolves the biome provider for a world, falling back to the supplied Bukkit default when + * Iris has nothing staged. + */ + @Nullable + public BiomeProvider resolveDefaultBiomeProvider(String worldName, @Nullable String id, Supplier fallback) { + BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName); + if (stagedBiomeProvider != null) { + Iris.debug("Using staged runtime biome provider for " + worldName); + return stagedBiomeProvider; + } + Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id); + return fallback.get(); + } + + @Nullable + public ChunkGenerator resolveDefaultWorldGenerator(String worldName, @Nullable String id) { + if (isPlotSquaredGeneratorDiscoveryProbe(worldName, id)) { + Iris.debug("Ignoring PlotSquared generator discovery probe"); + return null; + } + if (isGeneratorDiscoveryProbe(worldName, id)) { + Iris.debug("Generator discovery probe for loaded world " + worldName); + return new IrisProbeChunkGenerator(worldName); + } + IrisStartupValidation.requireWorldCreationReady(); + ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName); + if (stagedGenerator != null) { + Iris.debug("Using staged runtime generator for " + worldName); + return stagedGenerator; + } + Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id); + if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType(); + Iris.debug("Generator ID: " + id + " requested by bukkit/plugin"); + + File levelRoot = IrisWorldStorage.levelRoot(); + NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot); + requireWorldKeyAvailable(worldName, worldKey); + requireOwnedWorld(worldName, levelRoot, worldKey); + + try { + return resolveFrozenWorldGenerator(worldName, id); + } catch (RuntimeException failure) { + Iris.reportError("Refusing to load configured Iris world '" + worldName + + "' because its frozen world-local pack snapshot could not be used.", failure); + Bukkit.shutdown(); + throw failure; + } + } + + private static boolean isPlotSquaredGeneratorDiscoveryProbe(String worldName, String id) { + return PLOT_SQUARED_DISCOVERY_WORLD.equals(worldName) && id != null && id.isEmpty(); + } + + /** + * Multiverse-Core probes every enabled plugin by asking for a generator with an empty dimension + * id and the name of a world that is already loaded. Bukkit never creates a world that is + * already loaded, so that pair only ever means discovery, never generation. + */ + private static boolean isGeneratorDiscoveryProbe(String worldName, String id) { + return id != null && id.isEmpty() && Bukkit.getWorld(worldName) != null; + } + + /** + * Refuses a second generator for a world key that is already live. Multiverse imports accept a + * world name that maps onto a loaded Iris key, which would otherwise start a second engine on + * the same storage with a different seed. + */ + private static void requireWorldKeyAvailable(String worldName, NamespacedKey worldKey) { + World loaded = WorldIdentity.resolve(worldKey).orElse(null); + if (loaded == null) { + return; + } + throw new IllegalStateException("Refusing to generate '" + worldName + "': " + worldKey + + " is already loaded as '" + loaded.getName() + "'."); + } + + /** + * Refuses worlds Iris does not own before the fail-fast path can see them. A world without Iris + * storage is somebody else's create or import, and returning null there would silently hand the + * caller a vanilla world. + *

+ * The {@code iris} dimension namespace is Iris-exclusive, so a directory in it is ownership on + * its own. Every server already has {@code dimensions/minecraft/*}, so a vanilla slot counts as + * Iris' only once it carries a frozen pack snapshot. + */ + private static void requireOwnedWorld(String worldName, File levelRoot, NamespacedKey worldKey) { + File dimensionRoot; + try { + dimensionRoot = IrisWorldStorage.frozenDimensionRoot( + Bukkit.getWorldContainer(), + levelRoot, + worldName, + worldKey + ).orElse(null); + } catch (RuntimeException unusableStorage) { + // Storage exists but cannot be resolved: an owned world, left to the fail-fast path. + return; + } + if (dimensionRoot != null + && (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasFrozenPack(dimensionRoot))) { + return; + } + NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName()); + throw new IllegalStateException("'" + worldName + "' (" + messageKey + + ") has no Iris world storage, so Iris cannot generate it." + + " Create Iris worlds with /iris create " + IrisWorldStorage.logicalName(messageKey) + + " type=; Iris registers them with Multiverse itself."); + } + + private static boolean hasFrozenPack(File dimensionRoot) { + Path packRoot = dimensionRoot.toPath().toAbsolutePath().normalize().resolve("iris").resolve("pack"); + return Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS); + } + + private ChunkGenerator resolveFrozenWorldGenerator(String worldName, String id) { + File levelRoot = IrisWorldStorage.levelRoot(); + NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot); + File dimensionRoot = IrisWorldStorage.requireFrozenDimensionRoot( + Bukkit.getWorldContainer(), + levelRoot, + worldName, + worldKey + ); + File expectedDimensionRoot = WorldCreatorCompat.persistentDimensionRoot(worldKey); + if (!dimensionRoot.toPath().toAbsolutePath().normalize() + .equals(expectedDimensionRoot.toPath().toAbsolutePath().normalize())) { + throw new IllegalStateException("Frozen Iris world storage does not match the current platform layout for " + + worldKey + "."); + } + File snapshotRoot = IrisWorldStorage.requireFrozenPackRoot(dimensionRoot); + try { + requireSnapshotLoadable(snapshotRoot); + } catch (BrokenPackException exception) { + Iris.error("Refusing to create world '" + worldName + "' using broken snapshot at '" + + snapshotRoot + "':"); + for (String reason : exception.getReasons()) { + Iris.error(" - " + reason); + } + throw exception; + } + + IrisDimension dimension = IrisData.get(snapshotRoot).getDimensionLoader().load(id, false); + if (dimension == null) { + throw new IllegalStateException("Frozen Iris pack snapshot at " + snapshotRoot + + " does not contain dimension " + id + "."); + } + + Iris.debug("Assuming IrisDimension: " + dimension.getName()); + + IrisWorld world = IrisWorld.builder() + .platformIdentity(worldKey.toString()) + .name(worldName) + .seed(1337) + .worldFolder(dimensionRoot) + .minHeight(dimension.getMinHeight()) + .maxHeight(dimension.getMaxHeight()) + .build(); + + Iris.debug("Generator Config: " + world); + + return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey()); + } + + private record FreshValidation( + List packDirs, + ServerConfigurator.PackContentSnapshot contentSnapshot, + List results, + boolean stable + ) { + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java new file mode 100644 index 000000000..8b25b8d70 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java @@ -0,0 +1,54 @@ +package art.arcane.iris.core; + +import io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent; +import net.kyori.adventure.text.Component; +import org.bukkit.Location; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; + +import java.util.Objects; +import java.util.UUID; + +public final class PaperWorldReplacementEntryListener implements Listener { + private final PendingWorldReplacementManager manager; + + public PaperWorldReplacementEntryListener(PendingWorldReplacementManager manager) { + this.manager = Objects.requireNonNull(manager, "manager"); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onAsyncPlayerSpawnLocation(AsyncPlayerSpawnLocationEvent event) { + UUID playerId = event.getConnection().getProfile().getId(); + if (playerId == null) { + return; + } + try { + PendingWorldReplacementManager.ReplacementEntryRedirect redirect = manager.prepareReplacementEntry( + playerId, + event.getSpawnLocation(), + event.isNewPlayer() + ); + if (redirect == null) { + return; + } + Location location = redirect.location(); + event.setSpawnLocation(location); + if (redirect.acknowledgementRequired()) { + manager.expectReplacementEntryAcknowledgement(playerId, redirect.transactionId()); + } + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + refuseUnsafeEntry(event, playerId, failure); + } catch (Throwable failure) { + refuseUnsafeEntry(event, playerId, failure); + } + } + + private void refuseUnsafeEntry(AsyncPlayerSpawnLocationEvent event, UUID playerId, Throwable failure) { + manager.reportUnsafeEntry(playerId, failure); + event.getConnection().disconnect(Component.text( + "Iris could not verify a safe login location after the Overworld replacement. Retry after startup completes." + )); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldDeleteQueue.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldDeleteQueue.java new file mode 100644 index 000000000..8bab4dd8b --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldDeleteQueue.java @@ -0,0 +1,548 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.runtime.WorldDeletionQueue; +import art.arcane.iris.core.runtime.TransientWorldCleanupSupport; +import art.arcane.iris.core.tools.IrisCreator; +import art.arcane.iris.util.common.plugin.VolmitPlugin; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +public final class PendingWorldDeleteQueue implements WorldDeletionQueue { + private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt"; + private static final String EXACT_PREFIX = "exact:"; + private static final Pattern SAFE_LOGICAL_NAME = Pattern.compile("^[a-z0-9_-]+$"); + private static final Pattern QUARANTINE_NAME = Pattern.compile("^\\.iris-delete-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"); + private static final Set VANILLA_DIMENSION_ALIASES = Set.of("overworld", "the_nether", "the_end"); + + private final VolmitPlugin plugin; + + public PendingWorldDeleteQueue(VolmitPlugin plugin) { + this.plugin = plugin; + } + + @Override + public synchronized int queueExactForStartupDeletion(Collection worldNames) throws IOException { + return queueWorldDeletionOnStartup(worldNames, true); + } + + @Override + public synchronized int queueFamilyForStartupDeletion(Collection worldNames) throws IOException { + return queueWorldDeletionOnStartup(worldNames, false); + } + + private int queueWorldDeletionOnStartup(Collection worldNames, boolean exact) throws IOException { + if (worldNames == null || worldNames.isEmpty()) { + return 0; + } + + File levelRoot = IrisWorldStorage.levelRoot(); + ArrayList normalizedNames = new ArrayList<>(worldNames.size()); + for (String worldName : worldNames) { + String normalized = normalizeQueueEntry(worldName, levelRoot.getName()); + if (normalized == null) { + throw new IllegalArgumentException("Unsafe Iris world deletion target: " + worldName); + } + normalizedNames.add(exact && !QUARANTINE_NAME.matcher(normalized).matches() + ? EXACT_PREFIX + normalized + : normalized); + } + + File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE); + LinkedHashMap queue = loadPendingWorldDeleteMap(queueFile, levelRoot.getName()); + int before = queue.size(); + for (String normalized : normalizedNames) { + mergeQueueEntry(queue, normalized); + } + + if (queue.size() != before) { + writePendingWorldDeleteMap(queueFile, queue); + } + return queue.size() - before; + } + + public synchronized void processPendingStartupWorldDeletes() { + try { + unregisterTransientStudioWorlds(); + + File levelRoot = IrisWorldStorage.levelRoot(); + File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE); + LinkedHashMap queue = loadPendingWorldDeleteMap(queueFile, levelRoot.getName()); + for (String discoveredName : discoverStartupWorldNames(levelRoot)) { + mergeQueueEntry(queue, discoveredName); + } + if (queue.isEmpty()) { + if (queueFile.exists()) { + writePendingWorldDeleteMap(queueFile, queue); + } + return; + } + + LinkedHashMap remaining = new LinkedHashMap<>(); + for (String worldName : queue.values()) { + processEntry(levelRoot, worldName, remaining); + } + writePendingWorldDeleteMap(queueFile, remaining); + } catch (Throwable failure) { + Iris.reportError("Failed to process queued startup world deletions.", failure); + } + } + + @Nullable + static String normalizeQueueEntry(String worldName, String levelName) { + if (worldName == null) { + return null; + } + + String candidate = worldName.trim(); + if (candidate.isEmpty()) { + return null; + } + if (QUARANTINE_NAME.matcher(candidate).matches()) { + return candidate; + } + + String logicalName = candidate.startsWith("iris:") ? candidate.substring("iris:".length()) : candidate; + if (!SAFE_LOGICAL_NAME.matcher(logicalName).matches()) { + return null; + } + String normalizedLevelName = Objects.requireNonNull(levelName, "levelName").trim().toLowerCase(Locale.ROOT); + if (VANILLA_DIMENSION_ALIASES.contains(logicalName) + || logicalName.equals(normalizedLevelName) + || logicalName.equals(normalizedLevelName + "_nether") + || logicalName.equals(normalizedLevelName + "_the_end")) { + return null; + } + + try { + NamespacedKey key = IrisWorldStorage.managedKeyFromName(candidate, normalizedLevelName); + return key.getKey().equals(logicalName) ? logicalName : null; + } catch (IllegalArgumentException failure) { + return null; + } + } + + static LinkedHashMap loadPendingWorldDeleteMap(File queueFile, String levelName) throws IOException { + LinkedHashMap queue = new LinkedHashMap<>(); + Path queuePath = Objects.requireNonNull(queueFile, "queueFile").toPath(); + if (!Files.exists(queuePath, LinkOption.NOFOLLOW_LINKS)) { + return queue; + } + + try (BufferedReader reader = Files.newBufferedReader(queuePath, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + String normalized = normalizeStoredQueueEntry(line, levelName); + if (normalized != null) { + mergeQueueEntry(queue, normalized); + } + } + } + return queue; + } + + @Nullable + private static String normalizeStoredQueueEntry(String storedEntry, String levelName) { + if (storedEntry == null) { + return null; + } + String candidate = storedEntry.trim(); + boolean exact = candidate.startsWith(EXACT_PREFIX); + String rawName = exact ? candidate.substring(EXACT_PREFIX.length()) : candidate; + String normalized = normalizeQueueEntry(rawName, levelName); + if (normalized == null || QUARANTINE_NAME.matcher(normalized).matches()) { + return normalized; + } + return exact ? EXACT_PREFIX + normalized : normalized; + } + + private static void mergeQueueEntry(Map queue, String storedEntry) { + String logicalKey = storedEntry.startsWith(EXACT_PREFIX) + ? storedEntry.substring(EXACT_PREFIX.length()) + : storedEntry; + String key = logicalKey.toLowerCase(Locale.ROOT); + String existing = queue.get(key); + if (existing == null || (existing.startsWith(EXACT_PREFIX) && !storedEntry.startsWith(EXACT_PREFIX))) { + queue.put(key, storedEntry); + } + } + + static void writePendingWorldDeleteMap(File queueFile, Map queue) throws IOException { + Path queuePath = Objects.requireNonNull(queueFile, "queueFile").toPath().toAbsolutePath().normalize(); + Path parent = queuePath.getParent(); + if (parent == null) { + throw new IOException("Queue file has no parent directory: " + queuePath); + } + Files.createDirectories(parent); + + StringBuilder content = new StringBuilder(); + for (String worldName : Objects.requireNonNull(queue, "queue").values()) { + content.append(worldName).append('\n'); + } + + Path temporary = parent.resolve(queuePath.getFileName() + ".tmp-" + UUID.randomUUID()); + IOException failure = null; + try { + byte[] bytes = content.toString().getBytes(StandardCharsets.UTF_8); + try (FileChannel channel = FileChannel.open( + temporary, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE + )) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + + replaceQueueFile(temporary, queuePath); + forceDirectory(parent); + } catch (IOException writeFailure) { + failure = writeFailure; + throw writeFailure; + } finally { + try { + Files.deleteIfExists(temporary); + } catch (IOException cleanupFailure) { + if (failure != null) { + failure.addSuppressed(cleanupFailure); + } else { + throw cleanupFailure; + } + } + } + } + + static LinkedHashSet discoverStartupWorldNames(File levelRoot) throws IOException { + LinkedHashSet worldNames = new LinkedHashSet<>(); + Path root = Objects.requireNonNull(levelRoot, "levelRoot").toPath().toAbsolutePath().normalize(); + Path dimensions = root.resolve("dimensions"); + Path irisNamespace = dimensions.resolve("iris"); + if (!Files.exists(irisNamespace, LinkOption.NOFOLLOW_LINKS)) { + return worldNames; + } + if (Files.isSymbolicLink(dimensions) || Files.isSymbolicLink(irisNamespace)) { + throw new IOException("Iris dimension storage contains a symbolic link: " + irisNamespace); + } + if (!Files.isDirectory(irisNamespace, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Iris dimension storage is not a directory: " + irisNamespace); + } + + try (DirectoryStream children = Files.newDirectoryStream(irisNamespace)) { + for (Path child : children) { + if (Files.isSymbolicLink(child) || !Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + + String name = child.getFileName().toString(); + if (QUARANTINE_NAME.matcher(name).matches()) { + worldNames.add(name); + continue; + } + + String transientBaseName = TransientWorldCleanupSupport.transientStudioBaseWorldName(name); + String normalized = normalizeQueueEntry(transientBaseName, root.getFileName().toString()); + if (normalized != null) { + worldNames.add(normalized); + } + } + } + return worldNames; + } + + static List resolveQueueEntryPaths(File levelRoot, String worldName) throws IOException { + QueueEntry entry = QueueEntry.parse(worldName, levelRoot.getName()); + List targets = entry.targets(levelRoot); + ArrayList paths = new ArrayList<>(targets.size()); + for (DeleteTarget target : targets) { + paths.add(target.path()); + } + return List.copyOf(paths); + } + + private static void replaceQueueFile(Path temporary, Path queuePath) throws IOException { + try { + Files.move(temporary, queuePath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + Files.move(temporary, queuePath, StandardCopyOption.REPLACE_EXISTING); + } + } + + private static void forceDirectory(Path directory) throws IOException { + // A directory can only be opened and fsynced on a POSIX filesystem; Windows rejects the + // open outright. Matches DirectoryDurability and DatapackIngestService, which already + // skip the barrier there. + if (!Files.getFileStore(directory).supportsFileAttributeView("posix")) { + return; + } + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + private static void unregisterTransientStudioWorlds() { + try { + int unregistered = IrisCreator.removeTransientStudioWorldsFromBukkitYml(); + if (unregistered > 0) { + Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup."); + } + } catch (Throwable failure) { + Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", failure); + } + } + + private static void processEntry( + File levelRoot, + String worldName, + LinkedHashMap remaining + ) { + try { + EntryDeletionResult result = attemptEntry( + levelRoot, + worldName, + PendingWorldDeleteQueue::isLoaded + ); + if (result.loaded()) { + Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded."); + remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); + return; + } + + for (Path deleted : result.deleted()) { + Iris.info("Deleted queued world folder \"" + deleted.getFileName() + "\"."); + } + for (DeletionFailure deletionFailure : result.failures()) { + Iris.reportError( + "Failed to delete queued world folder \"" + deletionFailure.path() + "\".", + deletionFailure.failure() + ); + } + if (!result.foundAny()) { + Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing)."); + return; + } + if (result.retainQueueEntry()) { + remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); + } + } catch (Throwable failure) { + remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); + Iris.reportError("Failed to safely process queued world deletion for \"" + worldName + "\".", failure); + } + } + + static EntryDeletionResult attemptEntry( + File levelRoot, + String worldName, + LoadedTargetCheck loadedTargetCheck + ) throws IOException { + QueueEntry entry = QueueEntry.parse(worldName, levelRoot.getName()); + List targets = entry.targets(levelRoot); + if (targets.stream().anyMatch(target -> loadedTargetCheck.isLoaded(target.key(), target.path()))) { + return new EntryDeletionResult(true, false, List.of(), List.of()); + } + + boolean foundAny = false; + ArrayList deleted = new ArrayList<>(targets.size()); + ArrayList failures = new ArrayList<>(targets.size()); + for (DeleteTarget target : targets) { + Path worldFolder = target.path(); + if (!Files.exists(worldFolder, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + if (Files.isSymbolicLink(worldFolder) || !Files.isDirectory(worldFolder, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Queued world target is not a safe directory: " + worldFolder); + } + + foundAny = true; + try { + SnapshotDirectoryTreeDeleter.delete(worldFolder); + deleted.add(worldFolder); + } catch (IOException failure) { + failures.add(new DeletionFailure(worldFolder, failure)); + } + } + return new EntryDeletionResult( + false, + foundAny, + List.copyOf(deleted), + List.copyOf(failures) + ); + } + + private static boolean isLoaded(@Nullable NamespacedKey key, Path path) { + if (key != null && WorldIdentity.resolve(key).isPresent()) { + return true; + } + + Path targetPath = path.toAbsolutePath().normalize(); + for (World world : Bukkit.getWorlds()) { + if (world.getWorldFolder().toPath().toAbsolutePath().normalize().equals(targetPath)) { + return true; + } + } + return false; + } + + private enum QueueEntryType { + EXACT, + LOGICAL, + QUARANTINE + } + + private record QueueEntry(String storedName, QueueEntryType type) { + private static QueueEntry parse(String worldName, String levelName) { + String stored = normalizeStoredQueueEntry(worldName, levelName); + if (stored == null) { + throw new IllegalArgumentException("Unsafe queued Iris world deletion target: " + worldName); + } + boolean exact = stored.startsWith(EXACT_PREFIX); + String normalized = exact ? stored.substring(EXACT_PREFIX.length()) : stored; + QueueEntryType type = QUARANTINE_NAME.matcher(normalized).matches() + ? QueueEntryType.QUARANTINE + : exact ? QueueEntryType.EXACT : QueueEntryType.LOGICAL; + return new QueueEntry(normalized, type); + } + + private List targets(File levelRoot) throws IOException { + if (type == QueueEntryType.QUARANTINE) { + return List.of(new DeleteTarget(null, requireSafeQuarantinePath(levelRoot, storedName))); + } + + if (type == QueueEntryType.EXACT) { + NamespacedKey key = IrisWorldStorage.managedKeyFromName(storedName, levelRoot.getName()); + Path path = currentStorageRoot(levelRoot, key); + return List.of(new DeleteTarget(key, path)); + } + + ArrayList targets = new ArrayList<>(3); + for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(storedName)) { + NamespacedKey key = IrisWorldStorage.managedKeyFromName(familyWorldName, levelRoot.getName()); + Path path = currentStorageRoot(levelRoot, key); + targets.add(new DeleteTarget(key, path)); + } + return targets; + } + + private static Path currentStorageRoot(File levelRoot, NamespacedKey key) { + File worldContainer = levelRoot.getAbsoluteFile().getParentFile(); + if (worldContainer == null) { + throw new IllegalArgumentException("Selected level root has no world container: " + levelRoot); + } + String configuredWorldName = IrisWorldStorage.configuredWorldName(key, levelRoot.getName()); + Path directRoot = IrisWorldStorage.requireSafeManagedDimensionRoot(levelRoot, key) + .toPath() + .toAbsolutePath() + .normalize(); + Path dimensionRoot = IrisWorldStorage.frozenDimensionRoot( + worldContainer, + levelRoot, + configuredWorldName, + key + ).map(file -> file.toPath().toAbsolutePath().normalize()).orElse(directRoot); + if (dimensionRoot.equals(directRoot)) { + return directRoot; + } + Path configuredDimensionRoot = IrisWorldStorage.configuredDimensionRoot( + worldContainer, + levelRoot, + key + ).toPath().toAbsolutePath().normalize(); + if (!dimensionRoot.equals(configuredDimensionRoot)) { + throw new IllegalStateException("Iris world storage does not match the current platform layout."); + } + return IrisWorldStorage.configuredLevelRoot(worldContainer, levelRoot, key) + .toPath() + .toAbsolutePath() + .normalize(); + } + } + + private static Path requireSafeQuarantinePath(File levelRoot, String quarantineName) throws IOException { + if (!QUARANTINE_NAME.matcher(quarantineName).matches()) { + throw new IOException("Invalid Iris quarantine directory name: " + quarantineName); + } + + Path root = levelRoot.toPath().toAbsolutePath().normalize(); + Path dimensions = root.resolve("dimensions"); + Path irisNamespace = dimensions.resolve("iris"); + Path target = irisNamespace.resolve(quarantineName).normalize(); + if (!Objects.equals(target.getParent(), irisNamespace)) { + throw new IOException("Iris quarantine target escapes its namespace: " + target); + } + for (Path path : List.of(dimensions, irisNamespace, target)) { + if (Files.isSymbolicLink(path)) { + throw new IOException("Iris quarantine storage contains a symbolic link: " + path); + } + } + return target; + } + + private record DeleteTarget(@Nullable NamespacedKey key, Path path) { + } + + @FunctionalInterface + interface LoadedTargetCheck { + boolean isLoaded(@Nullable NamespacedKey key, Path path); + } + + record EntryDeletionResult( + boolean loaded, + boolean foundAny, + List deleted, + List failures + ) { + boolean retainQueueEntry() { + return loaded || !failures.isEmpty(); + } + } + + record DeletionFailure(Path path, IOException failure) { + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java new file mode 100644 index 000000000..ec7b71f94 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java @@ -0,0 +1,1179 @@ +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.ExactWorldSlotPathPolicy.SlotKind; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.GeneratorReplacement; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot; +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import art.arcane.iris.core.lifecycle.WorldReplacementBootstrap; +import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker; +import art.arcane.iris.core.lifecycle.WorldReplacementEntryGuard; +import art.arcane.iris.core.lifecycle.WorldReplacementFilesystem; +import art.arcane.iris.core.lifecycle.WorldReplacementFilesystem.ReplacementPaths; +import art.arcane.iris.core.lifecycle.WorldReplacementJournal; +import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase; +import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Transaction; +import art.arcane.iris.core.lifecycle.WorldReplacementSeed; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.core.runtime.WorldRuntimeControlService; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisEnvironment; +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.common.misc.ServerProperties; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.world.WorldLoadEvent; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public final class PendingWorldReplacementManager implements Listener { + private static final WorldSlotKey OVERWORLD_KEY = WorldSlotKey.minecraft("overworld"); + private static final long SAFE_ENTRY_TIMEOUT_SECONDS = 30L; + + private final Iris plugin; + private final Set cleanupInFlight = new HashSet<>(); + private final Set verificationInFlight = new HashSet<>(); + private final ConcurrentHashMap pendingEntryAcknowledgements = new ConcurrentHashMap<>(); + private volatile WorldReplacementEntryGuard.Entry overworldEntryGuard; + private volatile Path overworldEntryWorldDirectory; + private volatile CompletableFuture overworldSafeEntry; + private volatile UUID overworldEntryAwaitingVerification; + private volatile World overworldEntryWorld; + private volatile boolean paperEntryListenerRegistered; + + public PendingWorldReplacementManager(Iris plugin) { + this.plugin = Objects.requireNonNull(plugin, "plugin"); + } + + public void registerPlatformEntryListener() { + ClassLoader loader = getClass().getClassLoader(); + try { + Class.forName("io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent", false, loader); + } catch (ClassNotFoundException | LinkageError unavailable) { + return; + } + try { + Class listenerType = Class.forName( + "art.arcane.iris.core.PaperWorldReplacementEntryListener", + true, + loader + ); + Constructor constructor = listenerType.getConstructor(PendingWorldReplacementManager.class); + Listener listener = (Listener) constructor.newInstance(this); + Bukkit.getPluginManager().registerEvents(listener, plugin); + paperEntryListenerRegistered = true; + } catch (InvocationTargetException failure) { + Throwable cause = failure.getCause() == null ? failure : failure.getCause(); + throw new IllegalStateException("Paper replacement entry listener registration failed.", cause); + } catch (ReflectiveOperationException | LinkageError failure) { + throw new IllegalStateException("Paper replacement entry listener is unavailable.", failure); + } + } + + public NamespacedKey resolveRequestedWorldKey(String requestedName) { + NamespacedKey worldKey = IrisWorldStorage.replacementKeyFromName( + requestedName, + IrisWorldStorage.levelRoot().getName() + ); + ExactWorldSlotPathPolicy.resolve(IrisWorldStorage.levelRoot().toPath(), toWorldSlotKey(worldKey)); + return worldKey; + } + + public synchronized StagedReplacement stageReplacement( + VolmitSender sender, + NamespacedKey worldKey, + IrisDimension dimension, + Long requestedSeed + ) throws IOException { + VolmitSender requiredSender = Objects.requireNonNull(sender, "sender"); + NamespacedKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey"); + WorldSlotKey requiredWorldSlotKey = toWorldSlotKey(requiredWorldKey); + IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension"); + OptionalLong seedSelection = requestedSeed == null + ? OptionalLong.empty() + : OptionalLong.of(requestedSeed.longValue()); + IrisStartupValidation.requireWorldReplacementStagingReady(); + if (!WorldReplacementBootstrapMarker.wasBootstrappedThisProcess()) { + throw new IOException("Exact world replacement requires a full Paper-family startup bootstrap."); + } + PackValidationRegistry.requireLoadable(requiredDimension.getLoader().getDataFolder().getName()); + LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get(); + try (LifecycleOperationCoordinator.Lease ignored = coordinator.acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_REPLACE, + requiredWorldKey.toString() + )) { + if (findTransaction(requiredWorldSlotKey) != null) { + throw new IOException("A replacement is already pending for " + requiredWorldKey + "."); + } + ExactWorldSlotPathPolicy.Target target = resolveTarget(requiredWorldSlotKey); + requireCompatibleEnvironment(target.slotKind(), requiredDimension.getEnvironment()); + requireVanillaSlotEnabled(target.slotKind()); + UUID transactionId = UUID.randomUUID(); + ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transactionId); + WorldReplacementFilesystem.requireExistingTarget(paths); + String worldName = WorldReplacementJournal.logicalWorldName(target.levelRoot(), requiredWorldSlotKey); + DatapackInstallResult datapacks = ServerConfigurator.installDataPacksIfChanged(true); + if (!datapacks.succeeded()) { + throw new IOException("Iris could not compile the dimension datapacks."); + } + + boolean targetPresent = true; + WorldGeneratorSnapshot originalConfiguration = BukkitWorldConfiguration.snapshot( + ServerProperties.BUKKIT_YML, + worldName + ); + Transaction transaction = null; + boolean journalWritten = false; + boolean configurationApplied = false; + try { + Files.createDirectory(paths.stage()); + IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld( + requiredSender, + requiredDimension, + paths.stage().toFile() + ); + if (installed == null) { + throw new IOException("Iris could not stage the dimension pack."); + } + requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment()); + long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed( + paths.target(), + paths.stage(), + seedSelection + ); + if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) { + WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId); + } + File stagedPack = paths.stage().resolve("iris/pack").toFile(); + IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack); + String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath()); + transaction = new Transaction( + transactionId, + requiredWorldSlotKey, + worldName, + target.levelRoot(), + installed.getLoadKey(), + effectiveSeed, + packFingerprint, + originalConfiguration, + targetPresent, + Phase.PREPARED + ); + writeTransaction(transaction); + journalWritten = true; + GeneratorReplacement replacement = BukkitWorldConfiguration.replaceIfMatching( + ServerProperties.BUKKIT_YML, + worldName, + originalConfiguration, + installed.getLoadKey(), + effectiveSeed + ); + if (!replacement.applied()) { + throw new IOException("bukkit.yml changed while the replacement was being staged."); + } + configurationApplied = true; + transaction = transaction.withPhase(Phase.ARMED); + writeTransaction(transaction); + return new StagedReplacement( + requiredWorldKey, + worldName, + installed.getLoadKey(), + effectiveSeed, + targetPresent, + datapacks.restartRequired() + ); + } catch (Throwable failure) { + if (configurationApplied && transaction != null) { + try { + WorldGeneratorSnapshot replacement = WorldReplacementBootstrap.replacementSnapshot(transaction); + if (!BukkitWorldConfiguration.restoreIfMatching( + ServerProperties.BUKKIT_YML, + worldName, + replacement, + originalConfiguration + )) { + failure.addSuppressed(new IOException( + "bukkit.yml changed before the failed replacement could be restored.")); + } + } catch (Throwable restoreFailure) { + failure.addSuppressed(restoreFailure); + } + } + if (!configurationApplied || configurationMatches(originalConfiguration, worldName)) { + try { + WorldReplacementFilesystem.discardStage(paths); + if (journalWritten) { + deleteJournal(transactionId); + } + } catch (Throwable cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + } + if (failure instanceof IOException ioFailure) { + throw ioFailure; + } + throw new IOException("Failed to stage replacement for " + requiredWorldKey + ".", failure); + } + } + } + + public synchronized void processPendingStartupReplacements() { + ArrayList failures = new ArrayList<>(); + ArrayList restartBoundaries = new ArrayList<>(); + try { + loadOverworldEntryGuard(); + } catch (Throwable failure) { + String message = "Iris could not load the Overworld replacement entry guard: " + detail(failure); + Iris.reportError(message, failure); + IrisStartupValidation.markPacksInvalid(List.of(message)); + return; + } + if (overworldEntryGuard != null && !paperEntryListenerRegistered) { + String message = "A pending Overworld replacement requires the Paper safe-entry capability."; + Iris.error(message); + IrisStartupValidation.markPacksInvalid(List.of(message)); + return; + } + List transactions; + try { + transactions = loadTransactions(); + } catch (Throwable failure) { + Iris.reportError("Failed to read pending Iris world replacements.", failure); + IrisStartupValidation.markPacksInvalid(List.of( + "Pending Iris world replacement journal validation failed: " + detail(failure))); + return; + } + for (Transaction transaction : transactions) { + if (OVERWORLD_KEY.equals(transaction.worldKey()) && transaction.phase() == Phase.PUBLISHED) { + overworldEntryAwaitingVerification = transaction.id(); + } + try { + inspectStartupTransaction(transaction); + } catch (RestartBoundaryRequired boundary) { + String message = "Pending replacement for " + transaction.worldKey() + + " still requires a complete server restart: " + detail(boundary); + restartBoundaries.add(message); + Iris.warn(message); + } catch (Throwable failure) { + String message = "Pending replacement for " + transaction.worldKey() + + " failed safely: " + detail(failure); + failures.add(message); + Iris.reportError(message, failure); + } + } + scheduleLoadedOverworldEntryPreparation(); + if (!failures.isEmpty()) { + IrisStartupValidation.markPacksInvalid(failures); + } + if (!restartBoundaries.isEmpty()) { + IrisStartupValidation.requireRestart(restartBoundaries.getFirst()); + } + } + + public void verifyLoadedPublishedWorlds() { + J.a(this::discoverLoadedPublishedWorlds); + } + + private void discoverLoadedPublishedWorlds() { + List transactions; + try { + synchronized (this) { + transactions = loadTransactions(); + } + } catch (Throwable failure) { + Iris.reportError("Failed to inspect published Iris world replacements.", failure); + return; + } + for (Transaction transaction : transactions) { + if (transaction.phase() == Phase.CLEANUP_PENDING) { + scheduleCommittedCleanup(transaction); + } else if (transaction.phase() == Phase.PUBLISHED) { + scheduleRuntimeCapture(transaction, 0); + } + } + scheduleLoadedOverworldEntryPreparation(); + } + + private void scheduleLoadedOverworldEntryPreparation() { + if (overworldEntryGuard == null) { + return; + } + try { + J.s(this::prepareLoadedOverworldEntry); + } catch (Throwable failure) { + failOverworldEntryPreparation(failure); + } + } + + private void prepareLoadedOverworldEntry() { + World world; + try { + world = WorldIdentity.resolve(toNamespacedKey(OVERWORLD_KEY)).orElse(null); + } catch (Throwable failure) { + failOverworldEntryPreparation(failure); + return; + } + if (world != null) { + prepareOverworldEntry(world); + } + } + + private void prepareOverworldEntry(World world) { + WorldReplacementEntryGuard.Entry guard = overworldEntryGuard; + if (guard == null + || guard.transactionId().equals(overworldEntryAwaitingVerification) + || !OVERWORLD_KEY.equals(toWorldSlotKey(WorldIdentity.key(world)))) { + return; + } + CompletableFuture targetFuture; + synchronized (this) { + if (overworldSafeEntry != null) { + return; + } + targetFuture = new CompletableFuture<>(); + overworldSafeEntry = targetFuture; + overworldEntryWorld = world; + } + try { + PlatformChunkGenerator generator = IrisToolbelt.access(world); + if (!(generator instanceof BukkitChunkGenerator bukkitGenerator)) { + throw new IOException("The replaced Overworld does not have a Bukkit Iris generator."); + } + Location anchor = bukkitGenerator.getInitialSpawnLocation(world); + int chunkX = anchor.getBlockX() >> 4; + int chunkZ = anchor.getBlockZ() >> 4; + WorldRuntimeControlService runtime = WorldRuntimeControlService.get(); + CompletableFuture chunkFuture = runtime.requestChunkAsync(world, chunkX, chunkZ, true); + if (chunkFuture == null) { + throw new IOException("The replacement spawn chunk request was not accepted."); + } + chunkFuture + .thenCompose(chunk -> runtime.resolveSafeEntry(world, anchor)) + .thenCompose(this::applyOverworldSpawn) + .thenCompose(this::persistOverworldSpawn) + .whenComplete((safeEntry, failure) -> { + if (failure != null) { + targetFuture.completeExceptionally(failure); + Iris.reportError("Could not prepare a safe spawn for the replaced Overworld.", failure); + return; + } + targetFuture.complete(safeEntry.clone()); + retireOverworldEntryIfComplete(guard.transactionId()); + }); + } catch (Throwable failure) { + targetFuture.completeExceptionally(failure); + Iris.reportError("Could not prepare a safe spawn for the replaced Overworld.", failure); + } + } + + private CompletableFuture applyOverworldSpawn(Location safeEntry) { + Location requiredSafeEntry = Objects.requireNonNull(safeEntry, "safeEntry").clone(); + World world = Objects.requireNonNull(requiredSafeEntry.getWorld(), "safeEntry.world"); + CompletableFuture applied = new CompletableFuture<>(); + int chunkX = requiredSafeEntry.getBlockX() >> 4; + int chunkZ = requiredSafeEntry.getBlockZ() >> 4; + boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> { + try { + if (!world.setSpawnLocation(requiredSafeEntry)) { + throw new IOException("The server rejected the replacement spawn location."); + } + applied.complete(requiredSafeEntry.clone()); + } catch (Throwable failure) { + applied.completeExceptionally(failure); + } + }); + if (!scheduled) { + applied.completeExceptionally(new IOException("Could not schedule the replacement spawn update.")); + } + return applied; + } + + private CompletableFuture persistOverworldSpawn(Location safeEntry) { + Location requiredSafeEntry = Objects.requireNonNull(safeEntry, "safeEntry").clone(); + World world = Objects.requireNonNull(requiredSafeEntry.getWorld(), "safeEntry.world"); + CompletableFuture persisted = new CompletableFuture<>(); + boolean scheduled = J.runGlobal(() -> { + try { + world.save(); + persisted.complete(requiredSafeEntry.clone()); + } catch (Throwable failure) { + persisted.completeExceptionally(failure); + } + }); + if (!scheduled) { + persisted.completeExceptionally(new IOException("Could not schedule replacement spawn persistence.")); + } + return persisted; + } + + private CompletableFuture inspectLoginCollision(Location location) { + Location requiredLocation = Objects.requireNonNull(location, "location").clone(); + World world = Objects.requireNonNull(requiredLocation.getWorld(), "location.world"); + int chunkX = requiredLocation.getBlockX() >> 4; + int chunkZ = requiredLocation.getBlockZ() >> 4; + WorldRuntimeControlService runtime = WorldRuntimeControlService.get(); + CompletableFuture chunkFuture = runtime.requestChunkAsync(world, chunkX, chunkZ, true); + if (chunkFuture == null) { + return CompletableFuture.failedFuture(new IOException("The saved login chunk request was not accepted.")); + } + return chunkFuture.thenCompose(chunk -> inspectLoadedLoginCollision(requiredLocation)); + } + + private CompletableFuture inspectLoadedLoginCollision(Location location) { + World world = Objects.requireNonNull(location.getWorld(), "location.world"); + CompletableFuture inspected = new CompletableFuture<>(); + int chunkX = location.getBlockX() >> 4; + int chunkZ = location.getBlockZ() >> 4; + boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> { + try { + int blockY = location.getBlockY(); + if (blockY < world.getMinHeight() || blockY + 1 >= world.getMaxHeight()) { + inspected.complete(false); + return; + } + boolean feetPassable = world.getBlockAt(location.getBlockX(), blockY, location.getBlockZ()) + .isPassable(); + boolean headPassable = world.getBlockAt(location.getBlockX(), blockY + 1, location.getBlockZ()) + .isPassable(); + inspected.complete(feetPassable && headPassable); + } catch (Throwable failure) { + inspected.completeExceptionally(failure); + } + }); + if (!scheduled) { + inspected.completeExceptionally(new IOException("Could not schedule the saved login collision check.")); + } + return inspected; + } + + private void completeOverworldEntry(UUID playerId, UUID transactionId) { + try { + J.a(() -> completeOverworldEntryAsync(playerId, transactionId)); + } catch (Throwable failure) { + Iris.reportError("Could not record a completed Overworld replacement entry for " + playerId + ".", failure); + } + } + + private void completeOverworldEntryAsync(UUID playerId, UUID transactionId) { + boolean retire = false; + try { + synchronized (this) { + WorldReplacementEntryGuard.Entry current = overworldEntryGuard; + Path worldDirectory = overworldEntryWorldDirectory; + if (current == null + || worldDirectory == null + || !current.transactionId().equals(transactionId) + || !current.pendingPlayers().contains(playerId)) { + return; + } + Optional updated = WorldReplacementEntryGuard.completePlayer( + worldDirectory, + transactionId, + playerId + ); + overworldEntryGuard = updated.orElse(null); + retire = overworldEntryGuard != null && overworldEntryGuard.pendingPlayers().isEmpty(); + } + } catch (Throwable failure) { + Iris.reportError("Could not record a completed Overworld replacement entry for " + playerId + ".", failure); + return; + } + if (retire) { + retireOverworldEntryIfCompleteAsync(transactionId); + } + } + + private void retireOverworldEntryIfComplete(UUID transactionId) { + try { + J.a(() -> retireOverworldEntryIfCompleteAsync(transactionId)); + } catch (Throwable failure) { + Iris.reportError("Could not schedule Overworld replacement entry marker retirement.", failure); + } + } + + private synchronized void retireOverworldEntryIfCompleteAsync(UUID transactionId) { + WorldReplacementEntryGuard.Entry current = overworldEntryGuard; + Path worldDirectory = overworldEntryWorldDirectory; + CompletableFuture safeEntry = overworldSafeEntry; + if (current == null + || worldDirectory == null + || !current.transactionId().equals(transactionId) + || !current.pendingPlayers().isEmpty() + || safeEntry == null + || !safeEntry.isDone() + || safeEntry.isCompletedExceptionally()) { + return; + } + try { + if (!WorldReplacementEntryGuard.retireIfEmpty(worldDirectory, transactionId)) { + return; + } + overworldEntryGuard = null; + overworldEntryWorldDirectory = null; + overworldSafeEntry = null; + overworldEntryWorld = null; + pendingEntryAcknowledgements.entrySet().removeIf(entry -> entry.getValue().equals(transactionId)); + } catch (Throwable failure) { + Iris.reportError("Could not retire the completed Overworld replacement entry marker.", failure); + } + } + + void reportUnsafeEntry(UUID playerId, Throwable failure) { + Iris.reportError("Refused unsafe Overworld replacement entry for " + playerId + ".", failure); + } + + private synchronized void failOverworldEntryPreparation(Throwable failure) { + CompletableFuture future = overworldSafeEntry; + if (future == null) { + future = new CompletableFuture<>(); + overworldSafeEntry = future; + } + future.completeExceptionally(failure); + Iris.reportError("Could not prepare a safe spawn for the replaced Overworld.", failure); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldLoad(WorldLoadEvent event) { + WorldSlotKey worldKey; + try { + worldKey = toWorldSlotKey(WorldIdentity.key(event.getWorld())); + } catch (Throwable failure) { + Iris.reportError("Failed to capture a loaded world identity for replacement verification.", failure); + return; + } + if (OVERWORLD_KEY.equals(worldKey)) { + prepareOverworldEntry(event.getWorld()); + } + J.a(() -> discoverLoadedWorldTransaction(worldKey)); + } + + ReplacementEntryRedirect prepareReplacementEntry(UUID playerId, Location savedLocation, boolean newPlayer) + throws IOException, InterruptedException, ExecutionException, TimeoutException { + WorldReplacementEntryGuard.Entry guard = overworldEntryGuard; + if (guard == null || playerId == null) { + return null; + } + boolean pendingPlayer = guard.pendingPlayers().contains(playerId); + if (!pendingPlayer && !newPlayer) { + return null; + } + Location requiredSavedLocation = Objects.requireNonNull(savedLocation, "savedLocation").clone(); + World replacementWorld = overworldEntryWorld; + if (replacementWorld == null) { + throw new IOException("The replacement Overworld is not ready."); + } + if (requiredSavedLocation.getWorld() != replacementWorld) { + if (pendingPlayer) { + completeOverworldEntry(playerId, guard.transactionId()); + } + return null; + } + if (!newPlayer) { + boolean collisionSafe = inspectLoginCollision(requiredSavedLocation) + .get(SAFE_ENTRY_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (collisionSafe) { + completeOverworldEntry(playerId, guard.transactionId()); + return null; + } + } + CompletableFuture safeEntry = overworldSafeEntry; + if (safeEntry == null) { + throw new IOException("The replacement safe spawn is not ready."); + } + Location prepared = safeEntry.get(SAFE_ENTRY_TIMEOUT_SECONDS, TimeUnit.SECONDS).clone(); + prepared.setYaw(requiredSavedLocation.getYaw()); + prepared.setPitch(requiredSavedLocation.getPitch()); + return new ReplacementEntryRedirect(guard.transactionId(), prepared, pendingPlayer); + } + + void expectReplacementEntryAcknowledgement(UUID playerId, UUID transactionId) throws IOException { + WorldReplacementEntryGuard.Entry guard = overworldEntryGuard; + if (guard == null + || !guard.transactionId().equals(transactionId) + || !guard.pendingPlayers().contains(playerId)) { + throw new IOException("The Overworld replacement entry receipt is no longer active."); + } + pendingEntryAcknowledgements.put(playerId, transactionId); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerJoin(PlayerJoinEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + UUID transactionId = pendingEntryAcknowledgements.remove(playerId); + if (transactionId == null) { + return; + } + boolean scheduled = J.runEntity(event.getPlayer(), () -> { + try { + event.getPlayer().saveData(); + completeOverworldEntry(playerId, transactionId); + } catch (Throwable failure) { + Iris.reportError("Could not persist safe Overworld replacement entry for " + playerId + ".", failure); + } + }); + if (!scheduled) { + Iris.error("Could not schedule safe Overworld replacement entry persistence for " + playerId + "."); + } + } + + private void discoverLoadedWorldTransaction(WorldSlotKey worldKey) { + Transaction transaction; + try { + synchronized (this) { + transaction = findTransaction(worldKey); + } + } catch (Throwable failure) { + Iris.reportError("Failed to inspect a loaded Iris world replacement.", failure); + return; + } + if (transaction == null) { + return; + } + if (transaction.phase() == Phase.PUBLISHED) { + scheduleRuntimeCapture(transaction, 1); + } else if (transaction.phase() == Phase.CLEANUP_PENDING) { + scheduleCommittedCleanup(transaction); + } + } + + private void scheduleRuntimeCapture(Transaction transaction, int delayTicks) { + synchronized (this) { + if (!verificationInFlight.add(transaction.id())) { + return; + } + } + try { + J.s(() -> captureLoadedPublishedWorldOnGlobal(transaction), delayTicks); + } catch (Throwable failure) { + finishRuntimeVerification(transaction.id()); + Iris.reportError("Could not schedule runtime verification for " + transaction.worldKey() + ".", failure); + } + } + + private void captureLoadedPublishedWorldOnGlobal(Transaction transaction) { + World world; + try { + world = WorldIdentity.resolve(toNamespacedKey(transaction.worldKey())).orElse(null); + } catch (Throwable failure) { + dispatchRuntimeCaptureFailure(transaction, failure); + return; + } + if (world == null) { + finishRuntimeVerification(transaction.id()); + return; + } + PublishedWorldRuntimeState runtimeState; + try { + runtimeState = capturePublishedWorldRuntime(world); + } catch (Throwable failure) { + dispatchRuntimeCaptureFailure(transaction, failure); + return; + } + try { + J.a(() -> runPublishedWorldVerification(runtimeState, transaction)); + } catch (Throwable failure) { + finishRuntimeVerification(transaction.id()); + Iris.reportError("Could not dispatch runtime verification for " + transaction.worldKey() + ".", failure); + } + } + + private void dispatchRuntimeCaptureFailure(Transaction transaction, Throwable failure) { + try { + J.a(() -> runPublishedWorldCaptureFailure(transaction, failure)); + } catch (Throwable dispatchFailure) { + finishRuntimeVerification(transaction.id()); + dispatchFailure.addSuppressed(failure); + Iris.reportError("Could not dispatch a failed runtime capture for " + + transaction.worldKey() + ".", dispatchFailure); + } + } + + private void runPublishedWorldCaptureFailure(Transaction transaction, Throwable failure) { + try { + initiateRollback(transaction, failure); + } finally { + finishRuntimeVerification(transaction.id()); + } + } + + private void runPublishedWorldVerification( + PublishedWorldRuntimeState runtimeState, + Transaction transaction + ) { + try { + verifyPublishedWorld(runtimeState, transaction); + } finally { + finishRuntimeVerification(transaction.id()); + } + } + + static PublishedWorldRuntimeState capturePublishedWorldRuntime(World world) { + World requiredWorld = Objects.requireNonNull(world, "world"); + WorldSlotKey worldKey = toWorldSlotKey(WorldIdentity.key(requiredWorld)); + boolean irisWorld = IrisToolbelt.isIrisWorld(requiredWorld); + long seed = requiredWorld.getSeed(); + World.Environment bukkitEnvironment = requiredWorld.getEnvironment(); + PlatformChunkGenerator generator = irisWorld ? IrisToolbelt.access(requiredWorld) : null; + String dimension = null; + IrisEnvironment dimensionEnvironment = null; + if (generator != null) { + IrisDimension runtimeDimension = generator.getTarget().getDimension(); + dimension = runtimeDimension.getLoadKey(); + dimensionEnvironment = runtimeDimension.getEnvironment(); + } + return new PublishedWorldRuntimeState( + worldKey, + irisWorld, + seed, + bukkitEnvironment, + dimension, + dimensionEnvironment + ); + } + + private void verifyPublishedWorld(PublishedWorldRuntimeState runtimeState, Transaction transaction) { + try { + ExactWorldSlotPathPolicy.Target target = resolveTransactionTarget(transaction); + validatePublishedWorldRuntime(runtimeState, transaction, target.slotKind()); + ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id()); + String fingerprint = WorldReplacementFilesystem.fingerprintPack( + paths.target().resolve("iris/pack")); + if (!transaction.packFingerprint().equals(fingerprint)) { + throw new IOException("The replacement pack changed before runtime verification."); + } + WorldGeneratorSnapshot configured = BukkitWorldConfiguration.snapshot( + ServerProperties.BUKKIT_YML, + transaction.worldName() + ); + if (!configured.matchesGeneratorAndSeed(WorldReplacementBootstrap.replacementSnapshot(transaction))) { + throw new IOException("bukkit.yml changed before the replacement could be committed."); + } + } catch (Throwable failure) { + initiateRollback(transaction, failure); + return; + } + Transaction committed = transaction.withPhase(Phase.CLEANUP_PENDING); + try { + writeTransaction(committed); + Iris.success("Committed Iris world replacement for " + transaction.worldKey() + "."); + if (OVERWORLD_KEY.equals(transaction.worldKey())) { + overworldEntryAwaitingVerification = null; + scheduleLoadedOverworldEntryPreparation(); + } + scheduleCommittedCleanup(committed); + } catch (Throwable failure) { + Iris.reportError("The replacement for " + transaction.worldKey() + + " was verified, but its cleanup journal could not be advanced." + + " The retained backup was not removed.", failure); + } + } + + static void validatePublishedWorldRuntime( + PublishedWorldRuntimeState runtimeState, + Transaction transaction, + SlotKind slotKind + ) throws IOException { + PublishedWorldRuntimeState requiredRuntimeState = Objects.requireNonNull(runtimeState, "runtimeState"); + Transaction requiredTransaction = Objects.requireNonNull(transaction, "transaction"); + SlotKind requiredSlotKind = Objects.requireNonNull(slotKind, "slotKind"); + if (!requiredTransaction.worldKey().equals(requiredRuntimeState.worldKey())) { + throw new IOException("Loaded world identity does not match the replacement journal."); + } + if (!requiredRuntimeState.irisWorld()) { + throw new IOException("The replaced world did not load with an Iris generator."); + } + if (requiredRuntimeState.seed() != requiredTransaction.seed()) { + throw new IOException("The replaced world loaded with an unexpected seed."); + } + World.Environment expectedEnvironment = expectedEnvironment(requiredTransaction.worldKey()); + if (expectedEnvironment != null && requiredRuntimeState.bukkitEnvironment() != expectedEnvironment) { + throw new IOException("The replaced world loaded with an unexpected environment."); + } + if (requiredRuntimeState.dimension() == null + || requiredRuntimeState.dimensionEnvironment() == null + || !requiredTransaction.dimension().equals(requiredRuntimeState.dimension())) { + throw new IOException("The replaced world loaded an unexpected Iris dimension."); + } + requireCompatibleEnvironment(requiredSlotKind, requiredRuntimeState.dimensionEnvironment()); + } + + private synchronized void finishRuntimeVerification(UUID transactionId) { + verificationInFlight.remove(transactionId); + } + + private void initiateRollback(Transaction transaction, Throwable failure) { + Iris.reportError("Iris world replacement verification failed for " + transaction.worldKey() + + "; the retained world will be restored on restart.", failure); + if (OVERWORLD_KEY.equals(transaction.worldKey())) { + clearOverworldEntryGuard(); + } + try { + Transaction rollback = transaction.withPhase(Phase.ROLLBACK_PENDING); + writeTransaction(rollback); + } catch (Throwable rollbackFailure) { + failure.addSuppressed(rollbackFailure); + IrisStartupValidation.markPacksInvalid(List.of( + "Iris could not arm rollback for " + transaction.worldKey() + ": " + detail(rollbackFailure))); + Iris.reportError("Failed to arm Iris world replacement rollback for " + + transaction.worldKey() + ". Stop the server and preserve the replacement artifacts.", rollbackFailure); + } finally { + ServerConfigurator.restart("An Iris world replacement failed verification and requires a cold restart."); + } + } + + private void inspectStartupTransaction(Transaction transaction) throws IOException { + ExactWorldSlotPathPolicy.Target target = resolveTransactionTarget(transaction); + ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id()); + WorldGeneratorSnapshot current = BukkitWorldConfiguration.snapshot( + ServerProperties.BUKKIT_YML, + transaction.worldName() + ); + WorldGeneratorSnapshot replacement = WorldReplacementBootstrap.replacementSnapshot(transaction); + if (transaction.phase() == Phase.CLEANUP_PENDING) { + if (!current.matchesGeneratorAndSeed(replacement)) { + throw new IOException("A verified replacement no longer matches bukkit.yml."); + } + WorldReplacementFilesystem.validateCommittedTarget(paths, transaction.packFingerprint()); + return; + } + if (transaction.phase() == Phase.ROLLBACK_PENDING + || transaction.phase() == Phase.ROLLBACK_CLEANUP + || transaction.phase() == Phase.ARMED) { + throw new RestartBoundaryRequired("The world-storage transaction has not reached its cold bootstrap."); + } + if (transaction.phase() == Phase.PREPARED) { + if (current.matchesGeneratorAndSeed(transaction.originalConfiguration())) { + WorldReplacementFilesystem.discardStage(paths); + deleteJournal(transaction.id()); + Iris.warn("Cancelled incomplete Iris world replacement for " + transaction.worldKey() + "."); + return; + } else if (current.matchesGeneratorAndSeed(replacement)) { + writeTransaction(transaction.withPhase(Phase.ARMED)); + throw new RestartBoundaryRequired("The replacement was armed before its journal phase was durable."); + } else { + throw new IOException("bukkit.yml does not match the prepared replacement or its original state."); + } + } + if (transaction.phase() == Phase.PUBLISHED) { + if (!current.matchesGeneratorAndSeed(replacement)) { + throw new IOException("bukkit.yml changed after the replacement was published."); + } + WorldReplacementFilesystem.validatePublishedTarget( + paths, + transaction.originalTargetPresent(), + transaction.packFingerprint() + ); + } + } + + private synchronized void scheduleCommittedCleanup(Transaction transaction) { + if (!cleanupInFlight.add(transaction.id())) { + return; + } + try { + J.a(() -> cleanupCommittedReplacement(transaction)); + } catch (Throwable failure) { + cleanupInFlight.remove(transaction.id()); + Iris.reportError("Could not schedule retained-backup cleanup for " + transaction.worldKey() + + "; cleanup will retry without rolling back the verified world.", failure); + } + } + + private void cleanupCommittedReplacement(Transaction transaction) { + try { + synchronized (this) { + Transaction current = findTransaction(transaction.worldKey()); + if (current == null + || !current.id().equals(transaction.id()) + || current.phase() != Phase.CLEANUP_PENDING) { + return; + } + WorldGeneratorSnapshot configured = BukkitWorldConfiguration.snapshot( + ServerProperties.BUKKIT_YML, + current.worldName() + ); + if (!configured.matchesGeneratorAndSeed(WorldReplacementBootstrap.replacementSnapshot(current))) { + throw new IOException("bukkit.yml changed before the retained backup could be removed."); + } + ExactWorldSlotPathPolicy.Target target = resolveTransactionTarget(current); + ReplacementPaths paths = WorldReplacementFilesystem.paths(target, current.id()); + WorldReplacementFilesystem.validateCommittedTarget(paths, current.packFingerprint()); + WorldReplacementFilesystem.cleanupBackup(paths); + deleteJournal(current.id()); + Iris.success("Removed the retained backup for " + current.worldKey() + "."); + } + } catch (Throwable failure) { + Iris.reportError("Could not clean the retained backup for " + transaction.worldKey() + + "; cleanup will retry without rolling back the verified world.", failure); + } finally { + synchronized (this) { + cleanupInFlight.remove(transaction.id()); + } + } + } + + private static ExactWorldSlotPathPolicy.Target resolveTarget(WorldSlotKey worldKey) { + return ExactWorldSlotPathPolicy.resolve(IrisWorldStorage.levelRoot().toPath(), worldKey); + } + + private Transaction findTransaction(NamespacedKey worldKey) throws IOException { + return findTransaction(toWorldSlotKey(worldKey)); + } + + private Transaction findTransaction(WorldSlotKey worldKey) throws IOException { + for (Transaction transaction : loadTransactions()) { + if (transaction.worldKey().equals(worldKey)) { + return transaction; + } + } + return null; + } + + private List loadTransactions() throws IOException { + Path levelRoot = IrisWorldStorage.levelRoot().toPath(); + List transactions = WorldReplacementJournal.load(dataDirectory(), levelRoot); + List applicable = new ArrayList<>(transactions.size()); + for (Transaction transaction : transactions) { + // A journal staged against another level root is not this server's transaction; + // skip it (the bootstrap already told the operator how to resolve it). + if (WorldReplacementJournal.appliesTo(transaction, levelRoot)) { + applicable.add(transaction); + } else { + Iris.warn("Ignoring pending world replacement " + transaction.id() + " for " + + transaction.worldKey() + "; it was staged against level root " + + transaction.levelRoot() + "."); + } + } + return applicable; + } + + private void writeTransaction(Transaction transaction) throws IOException { + WorldReplacementJournal.write(dataDirectory(), transaction); + } + + private void deleteJournal(UUID id) throws IOException { + WorldReplacementJournal.delete(dataDirectory(), id); + } + + private Path dataDirectory() { + return plugin.getDataFolder().toPath().toAbsolutePath().normalize(); + } + + private synchronized void loadOverworldEntryGuard() throws IOException { + ExactWorldSlotPathPolicy.Target target = resolveTarget(OVERWORLD_KEY); + Optional loaded = WorldReplacementEntryGuard.load(target.worldDirectory()); + overworldEntryGuard = loaded.orElse(null); + overworldEntryWorldDirectory = loaded.isPresent() ? target.worldDirectory() : null; + overworldSafeEntry = null; + overworldEntryAwaitingVerification = null; + } + + private synchronized void clearOverworldEntryGuard() { + overworldEntryGuard = null; + overworldEntryWorldDirectory = null; + overworldSafeEntry = null; + overworldEntryAwaitingVerification = null; + overworldEntryWorld = null; + pendingEntryAcknowledgements.clear(); + } + + private ExactWorldSlotPathPolicy.Target resolveTransactionTarget(Transaction transaction) throws IOException { + return WorldReplacementJournal.resolveTarget(transaction, IrisWorldStorage.levelRoot().toPath()); + } + + private static boolean configurationMatches(WorldGeneratorSnapshot expected, String worldName) { + try { + return BukkitWorldConfiguration.snapshot(ServerProperties.BUKKIT_YML, worldName) + .matchesGeneratorAndSeed(expected); + } catch (IOException failure) { + return false; + } + } + + static void requireCompatibleEnvironment(SlotKind slotKind, IrisEnvironment environment) { + IrisEnvironment expected = switch (slotKind) { + case VANILLA_OVERWORLD -> IrisEnvironment.NORMAL; + case VANILLA_NETHER -> IrisEnvironment.NETHER; + case VANILLA_END -> IrisEnvironment.THE_END; + case IRIS_MANAGED -> null; + }; + if (expected != null && environment != expected) { + throw new IllegalArgumentException("The " + slotKind.name().toLowerCase(Locale.ENGLISH) + + " slot requires a pack environment of " + expected.name() + "."); + } + } + + /** + * Captures vanilla-slot availability on the main thread at startup so staging never blocks + * on a main-thread hop while holding the manager monitor. The context is stable for the + * process lifetime because slot replacements only commit across a restart. + */ + public void captureVanillaLevelContext() { + try { + vanillaLevelContext = new VanillaLevelContext( + Iris.instance.getServer().getAllowNether(), + Iris.instance.getServer().getAllowEnd() + ); + } catch (Throwable failure) { + Iris.debug("Could not capture vanilla-slot availability yet: " + detail(failure)); + } + } + + private static void requireVanillaSlotEnabled(SlotKind slotKind) throws IOException { + if (slotKind != SlotKind.VANILLA_NETHER && slotKind != SlotKind.VANILLA_END) { + return; + } + VanillaLevelContext context = vanillaLevelContext; + if (context == null) { + context = resolveVanillaLevelContext(); + vanillaLevelContext = context; + } + requireVanillaSlotEnabled(slotKind, context.allowNether(), context.allowEnd()); + } + + static void requireVanillaSlotEnabled(SlotKind slotKind, boolean allowNether, boolean allowEnd) + throws IOException { + if (slotKind == SlotKind.VANILLA_NETHER && !allowNether) { + throw new IOException("allow-nether must be true before the vanilla Nether can be replaced."); + } + if (slotKind == SlotKind.VANILLA_END && !allowEnd) { + throw new IOException("Bukkit allow-end must be true before the vanilla End can be replaced."); + } + } + + private static VanillaLevelContext resolveVanillaLevelContext() throws IOException { + CompletableFuture contextFuture = J.sfut(() -> new VanillaLevelContext( + Iris.instance.getServer().getAllowNether(), + Iris.instance.getServer().getAllowEnd() + )); + if (contextFuture == null) { + throw new IOException("Could not schedule vanilla-slot availability resolution."); + } + try { + return contextFuture.get(30L, TimeUnit.SECONDS); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IOException("Vanilla-slot availability resolution was interrupted.", failure); + } catch (ExecutionException | TimeoutException failure) { + throw new IOException("Could not resolve vanilla-slot availability.", failure); + } + } + + private static World.Environment expectedEnvironment(WorldSlotKey worldKey) { + if (WorldSlotKey.minecraft("overworld").equals(worldKey)) { + return World.Environment.NORMAL; + } + if (WorldSlotKey.minecraft("the_nether").equals(worldKey)) { + return World.Environment.NETHER; + } + if (WorldSlotKey.minecraft("the_end").equals(worldKey)) { + return World.Environment.THE_END; + } + return null; + } + + private static WorldSlotKey toWorldSlotKey(NamespacedKey worldKey) { + NamespacedKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey"); + return new WorldSlotKey(requiredWorldKey.getNamespace(), requiredWorldKey.getKey()); + } + + private static NamespacedKey toNamespacedKey(WorldSlotKey worldKey) { + WorldSlotKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey"); + return new NamespacedKey(requiredWorldKey.namespace(), requiredWorldKey.key()); + } + + private static String detail(Throwable failure) { + String message = failure.getMessage(); + return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message; + } + + public record StagedReplacement( + NamespacedKey worldKey, + String worldName, + String dimension, + long seed, + boolean replacedExistingTarget, + boolean datapackRestartRequired + ) { + public StagedReplacement { + Objects.requireNonNull(worldKey, "worldKey"); + Objects.requireNonNull(worldName, "worldName"); + Objects.requireNonNull(dimension, "dimension"); + } + } + + private static volatile VanillaLevelContext vanillaLevelContext; + + private record VanillaLevelContext(boolean allowNether, boolean allowEnd) { + } + + record ReplacementEntryRedirect( + UUID transactionId, + Location location, + boolean acknowledgementRequired + ) { + ReplacementEntryRedirect { + Objects.requireNonNull(transactionId, "transactionId"); + location = Objects.requireNonNull(location, "location").clone(); + } + + @Override + public Location location() { + return location.clone(); + } + } + + private static final class RestartBoundaryRequired extends IOException { + private RestartBoundaryRequired(String message) { + super(message); + } + } + + record PublishedWorldRuntimeState( + WorldSlotKey worldKey, + boolean irisWorld, + long seed, + World.Environment bukkitEnvironment, + String dimension, + IrisEnvironment dimensionEnvironment + ) { + PublishedWorldRuntimeState { + Objects.requireNonNull(worldKey, "worldKey"); + Objects.requireNonNull(bukkitEnvironment, "bukkitEnvironment"); + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/SettingsHotloadWatch.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/SettingsHotloadWatch.java new file mode 100644 index 000000000..9ccd9c57a --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/SettingsHotloadWatch.java @@ -0,0 +1,126 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.volmlib.util.hotload.ConfigHotloadEngine; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +/** + * Identity and hotload handling for settings.json. Supplies the predicates the + * {@link ConfigHotloadEngine} is built from and drains the touched-file queue. + */ +public final class SettingsHotloadWatch { + private static final int MAX_SETTINGS_BYTES = 2 * 1024 * 1024; + + private final File settingsFile; + + public SettingsHotloadWatch(File settingsFile) { + this.settingsFile = settingsFile; + } + + public File settingsFile() { + return settingsFile; + } + + public void checkConfigHotload(ConfigHotloadEngine engine) { + if (engine == null) { + return; + } + + for (ConfigHotloadEngine.StableContentSnapshot snapshot : engine.pollTouchedSnapshots()) { + if ("missing".equals(snapshot.signature())) { + engine.processSnapshotChange(snapshot, ignored -> true, null); + Iris.warn("settings.json was removed; retaining the last valid runtime settings."); + continue; + } + engine.processSnapshotChange( + snapshot, + stable -> applySettingsSnapshot(stable.file(), stable.normalizedContent()), + ignored -> Iris.info("Hotloaded settings.json ") + ); + } + IrisLanguage.update(); + } + + public boolean isSettingsFile(File file) { + if (file == null || settingsFile == null) { + return false; + } + return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile()); + } + + public List knownSettingsFiles() { + if (settingsFile == null) { + return List.of(); + } + return List.of(settingsFile); + } + + public String readSettingsContent(File file) { + if (file == null || !file.exists() || !file.isFile()) { + return null; + } + + try (InputStream input = Files.newInputStream(file.toPath())) { + byte[] content = input.readNBytes(MAX_SETTINGS_BYTES + 1); + if (content.length > MAX_SETTINGS_BYTES) { + throw new IOException("Settings exceed " + MAX_SETTINGS_BYTES + " bytes: " + file); + } + return new String(content, StandardCharsets.UTF_8); + } catch (Throwable ex) { + Iris.warn("Failed to read settings file %s: %s%s", + file.getAbsolutePath(), + ex.getClass().getSimpleName(), + ex.getMessage() == null ? "" : " - " + ex.getMessage()); + Iris.reportError(ex); + return null; + } + } + + public String normalizeSettingsContent(String text) { + if (text == null) { + return null; + } + + return text.replace("\r\n", "\n").trim(); + } + + private boolean applySettingsSnapshot(File file, String content) { + if (content == null) { + return false; + } + try { + return IrisSettings.applyHotloadSnapshot(content, IrisLanguage::reload); + } catch (RuntimeException failure) { + Iris.warn("Rejected invalid settings hotload from %s: %s", + file.getAbsolutePath(), + failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage()); + Iris.reportError(failure); + return false; + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java new file mode 100644 index 000000000..41a62cbc2 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java @@ -0,0 +1,75 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +import art.arcane.iris.core.datapack.DatapackIngestService; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; + +import java.util.List; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.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") +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( + @Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", descriptionKey = "iris.director.commanddatapack.param.restart_server_when_new_datapacks_are_installed_required_new_structures_register_generate", defaultValue = "false") + boolean restart + ) { + VolmitSender sender = sender(); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_STARTING_DATAPACK_INGEST)); + J.a(() -> DatapackIngestService.ingestAll(sender, restart)); + } + + @Director(description = "List configured datapack imports and their installed versions", descriptionKey = "iris.director.commanddatapack.director.list_configured_datapack_imports_their_installed_versions", aliases = {"ls"}) + public void list() { + VolmitSender sender = sender(); + KList configured = DatapackIngestService.collectConfiguredImports(); + List installed = DatapackIngestService.installed(); + + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_CONFIGURED_DATAPACK_IMPORTS, MessageArgument.untrusted("value", configured.size()))); + for (String url : configured) { + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_MESSAGE, MessageArgument.untrusted("url", url))); + } + + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_INSTALLED_DATAPACKS, MessageArgument.untrusted("value", installed.size()))); + for (DatapackIngestService.Entry entry : installed) { + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_MESSAGE_2, MessageArgument.untrusted("value", entry.id), MessageArgument.untrusted("value2", (entry.versionNumber == null ? "?" : entry.versionNumber)))); + } + + if (configured.isEmpty()) { + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_ADD_MODRINTH_URLS_DIMENSION_S_DATAPACKIMPORTS_LIST_THEN_RUN_IRIS)); + } + } + + @Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", descriptionKey = "iris.director.commanddatapack.director.remove_installed_datapack_by_id_also_delete_its_url_from_datapackimports_keep", aliases = {"rm"}) + public void remove( + @Param(description = "The datapack id (folder name) shown by /iris datapack list", descriptionKey = "iris.director.commanddatapack.param.datapack_id_folder_name_shown_by_iris_datapack_list") + String id + ) { + DatapackIngestService.remove(sender(), id); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java new file mode 100644 index 000000000..36b5e9ffe --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java @@ -0,0 +1,434 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +import com.google.gson.JsonObject; +import art.arcane.iris.Iris; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.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.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.CountingDataInputStream; +import art.arcane.volmlib.util.mantle.runtime.TectonicPlate; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.iris.util.nbt.common.mca.MCAFile; +import art.arcane.iris.util.nbt.common.mca.MCAUtil; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.nio.file.Files; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; +import java.util.TreeMap; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.BukkitCommandMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.iris.core.localization.BukkitCommandMessagesExtended; +import art.arcane.iris.core.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) + public void EngineStatus() { + Iris.service(IrisEngineSVC.class) + .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) + World world, + @Param(description = "Radius in chunks around the center", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_center", defaultValue = "4") + int radius, + @Param(description = "Center chunk X", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_x", defaultValue = "0") + int centerX, + @Param(description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z", defaultValue = "0") + int centerZ) { + if (world == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_WORLD_IS_NULL)); + return; + } + + VolmitSender sender = sender(); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DEVELOPER_GENHASH_STARTED_CHUNKS, MessageArgument.untrusted("value", ((radius * 2 + 1) * (radius * 2 + 1))))); + J.a(() -> runGenhash(sender, world, radius, centerX, centerZ)); + } + + private void runGenhash(VolmitSender sender, World world, int radius, int centerX, int centerZ) { + long startMs = M.ms(); + int minY = world.getMinHeight(); + int maxY = world.getMaxHeight(); + long globalHash = 0L; + long solidBlocks = 0L; + Map histogram = new TreeMap<>(); + JsonObject chunkHashes = new JsonObject(); + + for (int rx = centerX - radius; rx <= centerX + radius; rx++) { + for (int rz = centerZ - radius; rz <= centerZ + radius; rz++) { + org.bukkit.ChunkSnapshot snapshot; + try { + org.bukkit.Chunk loaded = art.arcane.iris.platform.bukkit.BukkitPlatform.chunkAtAsync(world, rx, rz, true).get(); + snapshot = loaded.getChunkSnapshot(false, false, false); + } catch (Throwable e) { + Iris.reportError(e); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DEVELOPER_GENHASH_FAILED_AT_CHUNK, MessageArgument.untrusted("rx", rx), MessageArgument.untrusted("rz", rz), MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + return; + } + long chunkHash = 0L; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int worldX = (rx << 4) + x; + int worldZ = (rz << 4) + z; + for (int y = minY; y < maxY; y++) { + org.bukkit.Material material = snapshot.getBlockType(x, y, z); + long positionSeed = ((long) worldX * 0x9E3779B97F4A7C15L) + ^ ((long) y * 0xC2B2AE3D27D4EB4FL) + ^ ((long) worldZ * 0x165667B19E3779F9L); + long blockHash = genHashMix(positionSeed ^ ((long) (material.ordinal() + 1) * 0xD6E8FEB86659FD93L)); + chunkHash ^= blockHash; + if (material != org.bukkit.Material.AIR + && material != org.bukkit.Material.CAVE_AIR + && material != org.bukkit.Material.VOID_AIR) { + histogram.merge(material.name(), 1L, Long::sum); + solidBlocks++; + } + } + } + } + globalHash ^= chunkHash; + chunkHashes.addProperty(rx + "," + rz, Long.toHexString(chunkHash)); + } + } + + int side = radius * 2 + 1; + JsonObject result = new JsonObject(); + result.addProperty("global", Long.toHexString(globalHash)); + result.addProperty("chunks", side * side); + result.addProperty("solidBlocks", solidBlocks); + result.addProperty("minY", minY); + result.addProperty("maxY", maxY); + JsonObject hist = new JsonObject(); + for (Map.Entry entry : histogram.entrySet()) { + hist.addProperty(entry.getKey(), entry.getValue()); + } + result.add("histogram", hist); + result.add("chunkHashes", chunkHashes); + + File out = new File(Iris.instance.getDataFolder(), "genhash.json"); + try { + Files.writeString(out.toPath(), result.toString()); + } catch (IOException e) { + Iris.reportError(e); + } + + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DEVELOPER_GENHASH_GLOBAL_CHUNKS_SOLID, MessageArgument.untrusted("value", Long.toHexString(globalHash)), MessageArgument.untrusted("value2", (side * side)), MessageArgument.untrusted("solidBlocks", solidBlocks), MessageArgument.untrusted("value3", Form.duration((long) (M.ms() - startMs), 1)))); + Iris.info("genhash world=" + world.getName() + " global=" + Long.toHexString(globalHash) + + " chunks=" + (side * side) + " solidBlocks=" + solidBlocks + " -> " + out.getAbsolutePath()); + } + + private static long genHashMix(long z) { + z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L; + z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL; + return z ^ (z >>> 31); + } + + @Director(description = "Update the pack of a world (UNSAFE!)", descriptionKey = "iris.director.commanddeveloper.director.update_pack_world_unsafe", 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, + @Param(description = "The pack to install into the world", descriptionKey = "iris.director.commanddeveloper.param.pack_install_into_world", contextual = true, contextualOverride = true, aliases = "dimension") + IrisDimension pack, + @Param(description = "Make sure to make a backup & read the warnings first!", descriptionKey = "iris.director.commanddeveloper.param.make_sure_make_backup_read_warnings_first", defaultValue = "false", aliases = "c") + boolean confirm + ) { + if (!confirm) { + sender().sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_DEVELOPER_UPDATE_WORLD_WARNING, + MessageArgument.untrusted("world", world.getName()), + MessageArgument.untrusted("pack", pack.getLoadKey()) + )); + return; + } + + File folder = world.getWorldFolder(); + folder.mkdirs(); + + try (LifecycleOperationCoordinator.Lease lease = LifecycleOperationCoordinator.get().acquire( + LifecycleOperationCoordinator.Domain.PACK_MUTATION, + LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH, + pack.getLoadKey() + )) { + Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder); + } catch (LifecycleOperationCoordinator.BusyException e) { + sender().sendMessage(C.YELLOW + e.getMessage()); + } + } + + @Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test") + public void mantle( + @Param(name = "plate", description = "Dump the whole tectonic plate instead of a single section", descriptionKey = "iris.director.commanddeveloper.param.dump_whole_tectonic_plate_instead_single_section", defaultValue = "false") + boolean plate, + @Param(name = "name", description = "The dump file id under plugins/Iris/dump (pv..*)", descriptionKey = "iris.director.commanddeveloper.param.dump_file_id_under_plugins_iris_dump_pv_id", defaultValue = "21474836474") + String name + ) throws Throwable { + Engine activeEngine = engine(); + if (activeEngine == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_TARGET_IRIS_WORLD_BEFORE_READING_MANTLE_DUMP)); + return; + } + File base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin"); + File section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin"); + + try (IrisMatterContext.Scope scope = IrisMatterContext.open(activeEngine.getData())) { + if (plate) { + try (CountingDataInputStream in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) { + TectonicPlate.read(1088, in, true, IrisEngineMantle.createRuntimeDataAdapter(activeEngine.getData()), IrisEngineMantle.createRuntimeHooks()); + } catch (Throwable e) { + e.printStackTrace(); + } + } else { + Matter.read(section); + } + } + if (!TectonicPlate.hasError()) { + Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath()); + } + } + + @Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_2") + public void packBenchmark( + @Param(description = "The pack to bench", descriptionKey = "iris.director.commanddeveloper.param.pack_bench", aliases = {"pack"}, defaultValue = "overworld") + IrisDimension dimension, + @Param(description = "Radius in regions", descriptionKey = "iris.director.commanddeveloper.param.radius_regions", defaultValue = "2048") + int radius, + @Param(description = "Open GUI while benchmarking", descriptionKey = "iris.director.commanddeveloper.param.open_gui_while_benchmarking", defaultValue = "false") + boolean gui + ) { + new IrisPackBenchmarking(dimension, radius, gui); + } + + @Director(description = "Upgrade to another Minecraft version", descriptionKey = "iris.director.commanddeveloper.director.upgrade_another_minecraft_version") + public void upgrade( + @Param(description = "The version to upgrade to", descriptionKey = "iris.director.commanddeveloper.param.version_upgrade", defaultValue = "latest") DataVersion version) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_UPGRADING, MessageArgument.untrusted("value", version.getVersion()))); + ServerConfigurator.installDataPacks(version.get(), false); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DONE_UPGRADING_YOU_CAN_NOW_UPDATE_YOUR_SERVER_VERSION, MessageArgument.untrusted("value", version.getVersion()))); + } + + @Director(description = "test", descriptionKey = "iris.director.commanddeveloper.director.test_3") + public void mca ( + @Param(description = "The world folder to scan for .mca region files", descriptionKey = "iris.director.commanddeveloper.param.world_folder_scan_mca_region_files") String world) { + try { + File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca")); + for (File mca : McaFiles) { + MCAFile MCARegion = MCAUtil.read(mca); + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + + @Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER) + public void deleteChunk( + @Param(description = "Radius in chunks around your current chunk", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_your_current_chunk", defaultValue = "0") + int radius + ) { + if (radius < 0) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER)); + return; + } + + Player player = player(); + VolmitSender commandSender = sender(); + World world = player.getWorld(); + if (!IrisToolbelt.isIrisWorld(world)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD)); + return; + } + + PlatformChunkGenerator access = IrisToolbelt.access(world); + if (access == null || access.getEngine() == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL)); + return; + } + + Engine engine = access.getEngine(); + int chunks = (radius * 2 + 1) * (radius * 2 + 1); + + // The player position must be read on the thread owning the player; ChunkClearer hops per chunk itself. + if (!J.runEntity(player, () -> { + int centerX = player.getLocation().getBlockX() >> 4; + int centerZ = player.getLocation().getBlockZ() >> 4; + + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ))); + + new ChunkClearer(world, engine, commandSender, centerX, centerZ, radius).start(); + })) { + Iris.warn("Could not schedule delete-chunk on the thread owning " + player.getName() + "."); + } + } + + @Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_4", aliases = {"ip"}) + public void network() { + try { + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + for (NetworkInterface ni : Collections.list(networkInterfaces)) { + Iris.info("Display Name: %s", ni.getDisplayName()); + Enumeration inetAddresses = ni.getInetAddresses(); + for (InetAddress ia : Collections.list(inetAddresses)) { + Iris.info("IP: %s", ia.getHostAddress()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // --- Regen --- + + @Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER) + public void regen( + @Param(name = "radius", description = "The radius of nearby chunks", descriptionKey = "iris.director.commanddeveloper.param.radius_nearby_chunks", defaultValue = "5") + int radius + ) { + if (radius < 0) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_2)); + return; + } + + Player player = player(); + VolmitSender commandSender = sender(); + World world = player.getWorld(); + if (!IrisToolbelt.isIrisWorld(world)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN)); + return; + } + + Engine engine = IrisToolbelt.access(world).getEngine(); + if (engine == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST)); + return; + } + + int chunks = (radius * 2 + 1) * (radius * 2 + 1); + + // The player position must be read on the thread owning the player; the regenerator hops per chunk itself. + if (!J.runEntity(player, () -> { + int centerX = player.getLocation().getBlockX() >> 4; + int centerZ = player.getLocation().getBlockZ() >> 4; + + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ))); + Iris.info("Regen run start: world=" + world.getName() + + " center=" + centerX + "," + centerZ + + " radius=" + radius + + " chunks=" + chunks); + + new InPlaceChunkRegenerator(world, engine, commandSender, centerX, centerZ, radius).start(); + })) { + Iris.warn("Could not schedule regen on the thread owning " + player.getName() + "."); + } + } + + @Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Deletes the world's entire mantle - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH) + public void goldenhash( + @Param(description = "The world to scan", descriptionKey = "iris.director.commanddeveloper.param.world_scan", contextual = true, contextualOverride = true) + World world, + @Param(name = "radius", description = "Radius in chunks around the center", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_center_2", defaultValue = "8") + int radius, + @Param(name = "center-x", description = "Center chunk X", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_x_2", defaultValue = "0") + int centerX, + @Param(name = "center-z", description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z_2", defaultValue = "0") + int centerZ, + @Param(name = "reset-mantle", description = "Delete the world's entire mantle folder first for full regeneration from scratch", descriptionKey = "iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch", defaultValue = "true") + boolean resetMantle, + @Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", descriptionKey = "iris.director.commanddeveloper.param.concurrent_chunk_generations_1_strictly_serial_order_dependence_testing", defaultValue = "8") + int threads, + @Param(name = "deep", description = "Also dump full per-chunk non-air blockstates for offline diffing", descriptionKey = "iris.director.commanddeveloper.param.also_dump_full_per_chunk_non_air_blockstates_offline_diffing", defaultValue = "false") + boolean deep + ) { + if (radius < 0) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_3)); + return; + } + + if (world == null || !IrisToolbelt.isIrisWorld(world)) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_TARGET_MUST_BE_IRIS_WORLD)); + return; + } + + PlatformChunkGenerator access = IrisToolbelt.access(world); + if (access == null || access.getEngine() == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_2)); + return; + } + + int chunks = (radius * 2 + 1) * (radius * 2 + 1); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_GOLDENHASH_STARTED_CHUNK_S_AROUND_BUFFERS_WORLD_UNTOUCHED, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ))); + Iris.info("goldenhash start: world=" + world.getName() + + " center=" + centerX + "," + centerZ + + " radius=" + radius + + " chunks=" + chunks); + + new GoldenHashScanner(world, access.getEngine(), sender(), centerX, centerZ, radius, resetMantle, threads, deep).start(); + } + +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java new file mode 100644 index 000000000..cdfe9c10b --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java @@ -0,0 +1,122 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.iris.util.common.format.C; + +import java.awt.Desktop; +import java.awt.GraphicsEnvironment; + + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.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 { + private boolean noStudio() { + if (!sender().isPlayer()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_PLAYERS_ONLY)); + return true; + } + if (!Iris.service(StudioSVC.class).isProjectOpen()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_NO_STUDIO_WORLD_IS_OPEN)); + return true; + } + if (!engine().isStudio()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_YOU_MUST_BE_STUDIO_WORLD)); + return true; + } + + if (GraphicsEnvironment.isHeadless()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_OPEN_FILES_HEADLESS_ENVIRONMENTS)); + return true; + } + + if (!Desktop.isDesktopSupported()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_DESKTOP_IS_NOT_SUPPORTED_BY_THIS_ENVIRONMENT)); + return true; + } + return false; + } + + + @Director(description = "Edit the biome you specified", descriptionKey = "iris.director.commandedit.director.edit_biome_you_specified", aliases = {"b"}, origin = DirectorOrigin.PLAYER) + public void biome(@Param(contextual = false, description = "The biome to edit", descriptionKey = "iris.director.commandedit.param.biome_edit") IrisBiome biome) { + if (noStudio()) { + return; + } + try { + if (biome == null || biome.getLoadFile() == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM)); + return; + } + Desktop.getDesktop().open(biome.getLoadFile()); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_OPENING_VSCODE, MessageArgument.untrusted("value", biome.getTypeName()), MessageArgument.untrusted("value2", biome.getLoadFile().getName().split("\\Q.\\E")[0]))); + } catch (Throwable e) { + Iris.reportError(e); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST)); + } + } + + @Director(description = "Edit the region you specified", descriptionKey = "iris.director.commandedit.director.edit_region_you_specified", aliases = {"r"}, origin = DirectorOrigin.PLAYER) + public void region(@Param(contextual = false, description = "The region to edit", descriptionKey = "iris.director.commandedit.param.region_edit") IrisRegion region) { + if (noStudio()) { + return; + } + try { + if (region == null || region.getLoadFile() == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_2)); + return; + } + Desktop.getDesktop().open(region.getLoadFile()); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_OPENING_VSCODE_2, MessageArgument.untrusted("value", region.getTypeName()), MessageArgument.untrusted("value2", region.getLoadFile().getName().split("\\Q.\\E")[0]))); + } catch (Throwable e) { + Iris.reportError(e); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_2)); + } + } + + @Director(description = "Edit the dimension you specified", descriptionKey = "iris.director.commandedit.director.edit_dimension_you_specified", aliases = {"d"}, origin = DirectorOrigin.PLAYER) + public void dimension(@Param(contextual = false, description = "The dimension to edit", descriptionKey = "iris.director.commandedit.param.dimension_edit") IrisDimension dimension) { + if (noStudio()) { + return; + } + try { + if (dimension == null || dimension.getLoadFile() == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_3)); + return; + } + Desktop.getDesktop().open(dimension.getLoadFile()); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_OPENING_VSCODE_3, MessageArgument.untrusted("value", dimension.getTypeName()), MessageArgument.untrusted("value2", dimension.getLoadFile().getName().split("\\Q.\\E")[0]))); + } catch (Throwable e) { + Iris.reportError(e); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_3)); + } + } + +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java new file mode 100644 index 000000000..a3577b6d8 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java @@ -0,0 +1,738 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +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.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.volmlib.util.collection.KList; +import art.arcane.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.volmlib.util.localization.MessageArgument; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.generator.structure.Structure; +import org.bukkit.util.StructureSearchResult; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto") +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") + IrisBiome biome, + @Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported", defaultValue = "true") + boolean teleport + ) { + Engine e = engine(); + + if (e == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD)); + return; + } + + EngineBukkitOps.gotoBiome(e, biome, player(), teleport); + } + + @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") + IrisRegion region, + @Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true") + boolean teleport + ) { + Engine e = engine(); + + if (e == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD_2)); + return; + } + + EngineBukkitOps.gotoRegion(e, region, player(), teleport); + } + + @Director(description = "Find a point of interest.", descriptionKey = "iris.director.commandfind.director.find_point_interest") + public void poi( + @Param(description = "The type of PoI to look for.", descriptionKey = "iris.director.commandfind.param.type_poi_look") + String type, + @Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_3", defaultValue = "true") + boolean teleport + ) { + Engine e = engine(); + if (e == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD_3)); + return; + } + + EngineBukkitOps.gotoPOI(e, type, player(), teleport); + } + + @Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", 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) + String structure + ) { + VolmitSender commandSender = sender(); + if (commandSender == null) { + Iris.reportError("Structure lookup started without a command sender context.", new IllegalStateException("Missing command sender context")); + return; + } + + Engine e = engine(); + + if (e == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_NOT_IRIS_WORLD)); + return; + } + + Player target = player(); + if (target == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE)); + return; + } + + String structureKey = structure == null ? "" : structure.trim(); + Structure nativeStructure = resolveNativeStructure(structureKey); + boolean registered = nativeStructure != null; + IrisNativeStructureDecision decision = registered + ? NativeStructureGenerationPolicy.resolve(e, structureKey, false) + : null; + boolean nativePlacement = IrisStructureLocator.hasNativePlacement(e, structureKey); + boolean locatableNativePlacement = IrisStructureLocator.hasLocatableNativePlacement(e, structureKey); + boolean locatableEditablePlacement = IrisStructureLocator.hasLocatableEditablePlacement(e, structureKey); + World targetWorld = target.getWorld(); + boolean nativeGenerationEnabled = targetWorld.canGenerateStructures(); + boolean requiresReachability = registered && decision.generate() + && decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS + && nativeGenerationEnabled; + boolean reachable = !requiresReachability || StructureReachability.isReachable(e, structureKey); + StructureLookupRoute route = selectStructureLookupRoute( + registered, decision, nativePlacement, locatableNativePlacement, + locatableEditablePlacement, nativeGenerationEnabled, reachable); + + if (route == StructureLookupRoute.UNKNOWN) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey))); + return; + } + if (route == StructureLookupRoute.POLICY_DISABLED) { + commandSender.sendMessage(C.RED + NativeStructureGenerationPolicy.generationStatusMessage( + structureKey, decision.status())); + return; + } + if (route == StructureLookupRoute.NO_ACTIVE_PLACEMENT) { + commandSender.sendMessage(C.YELLOW + structureKey + + " has no active placement in this world."); + return; + } + if (route == StructureLookupRoute.WORLD_DISABLED) { + commandSender.sendMessage(C.YELLOW + structureKey + + " cannot generate because native structure generation is disabled for this world."); + return; + } + if (route == StructureLookupRoute.UNREACHABLE) { + KList miss = StructureReachability.missingBiomeKeys(e, structureKey); + commandSender.sendMessage(C.YELLOW + structureKey + + " cannot generate in this world (its required biomes are not produced by this pack" + + (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ")."); + return; + } + if (route == StructureLookupRoute.IRIS) { + locateIrisStructure(e, structureKey, commandSender); + return; + } + + Location origin = target.getLocation(); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_LOCATING, MessageArgument.untrusted("structureKey", structureKey))); + J.s(() -> { + try { + StructureSearchResult result = targetWorld.locateNearestStructure( + origin, nativeStructure, 100, false); + if (result == null || result.getLocation() == null) { + sendStructureMessage(target, commandSender, + C.YELLOW + "No " + structureKey + " found within range of you."); + return; + } + prepareStructureTeleport( + target, targetWorld, commandSender, structureKey, result.getLocation(), false); + } catch (Throwable t) { + sendStructureMessage(target, commandSender, + C.RED + "Could not locate " + structureKey + ": " + t.getClass().getSimpleName()); + Iris.reportError("Could not locate structure '" + structureKey + "'.", t); + } + }); + } + + @Director(description = "Print every structure excluded from /iris goto and its rejection reason to the server console", sync = true) + public void unregistered() { + VolmitSender commandSender = sender(); + if (commandSender == null) { + Iris.reportError("Structure exclusion report started without a command sender context.", + new IllegalStateException("Missing command sender context")); + return; + } + Engine activeEngine = engine(); + if (activeEngine == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_NOT_IRIS_WORLD)); + return; + } + Player target = player(); + if (target == null) { + commandSender.sendMessage(C.RED + "Run this command from the Iris world to inspect its structures."); + return; + } + + StructureExclusionReport snapshot; + try { + snapshot = collectStructureExclusions( + activeEngine, target.getWorld().canGenerateStructures()); + } catch (Throwable error) { + commandSender.sendMessage(C.RED + "Could not build the structure exclusion report; see the server console."); + Iris.reportError("Could not snapshot /iris goto unregistered report for world '" + + target.getWorld().getName() + "'.", error); + return; + } + + String worldName = target.getWorld().getName(); + J.a(() -> { + try { + StructureExclusionReport report = includeManagedDatapackStructures( + snapshot, DatapackIngestService.installed()); + printStructureExclusionReport(worldName, report); + sendStructureMessage(target, commandSender, C.GREEN + "Printed " + report.entries().size() + + " non-generating structure candidate(s) and their reasons to the server console. " + + "This was an eligibility check; no chunks were searched."); + } catch (Throwable error) { + sendStructureMessage(target, commandSender, + C.RED + "Could not finish the structure exclusion report; see the server console."); + Iris.reportError("Could not finish /iris goto unregistered report for world '" + + worldName + "'.", error); + } + }); + } + + private static StructureExclusionReport collectStructureExclusions( + Engine engine, boolean nativeGenerationEnabled) { + PlatformStructureHooks structureHooks = IrisPlatforms.get().structureHooks(); + Map registeredKeys = distinctStructureKeys(structureHooks.structureKeys()); + Set reachableKeys = StructureReachability.reachableKeys(engine); + Set possibleBiomeKeys = normalizedStructureKeys( + structureHooks.possibleBiomeKeys(engine.getWorld().platformWorld())); + List exclusions = new ArrayList<>(); + int registeredExcluded = 0; + + for (Map.Entry entry : registeredKeys.entrySet()) { + String normalizedKey = entry.getKey(); + String key = entry.getValue(); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false); + boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key); + boolean locatableNativePlacement = IrisStructureLocator.hasLocatableNativePlacement(engine, key); + boolean locatableEditablePlacement = IrisStructureLocator.hasLocatableEditablePlacement(engine, key); + StructureLookupRoute route = selectStructureLookupRoute( + true, decision, nativePlacement, locatableNativePlacement, + locatableEditablePlacement, nativeGenerationEnabled, + reachableKeys.contains(normalizedKey)); + if (route == StructureLookupRoute.IRIS || route == StructureLookupRoute.NATIVE) { + continue; + } + List requiredBiomes = needsNativeReachabilityReason(route, decision) + ? structureHooks.structureBiomeKeys(key) + : List.of(); + exclusions.add(new StructureExclusion( + StructureExclusionKind.EXCLUDED, key, null, + describeStructureExclusion( + route, key, decision.status(), nativePlacement, + requiredBiomes, possibleBiomeKeys))); + registeredExcluded++; + } + + int configuredUnregistered = 0; + Set configuredUnregisteredKeys = new LinkedHashSet<>(); + for (String configuredKey : IrisStructureLocator.placedKeys(engine)) { + String normalizedKey = normalizeStructureKey(configuredKey); + if (normalizedKey.isEmpty() + || registeredKeys.containsKey(normalizedKey) + || !IrisStructureLocator.hasNativePlacement(engine, configuredKey) + || !configuredUnregisteredKeys.add(normalizedKey)) { + continue; + } + exclusions.add(new StructureExclusion( + StructureExclusionKind.UNREGISTERED, configuredKey, null, + describeConfiguredUnregisteredNative( + IrisStructureLocator.hasLocatableNativePlacement(engine, configuredKey)))); + configuredUnregistered++; + } + + int editableUnplaced = 0; + Set unplacedEditableKeys = new LinkedHashSet<>(); + Set locatableEditableKeys = normalizedStructureKeys( + IrisStructureLocator.locatableEditableKeys(engine)); + for (String editableKey : engine.getData().getStructureLoader().getPossibleKeys()) { + String normalizedKey = normalizeStructureKey(editableKey); + if (!isUnplacedEditableCandidate( + normalizedKey, registeredKeys.keySet(), locatableEditableKeys, + IrisStructureLocator.hasNativePlacement(engine, editableKey)) + || !unplacedEditableKeys.add(normalizedKey)) { + continue; + } + boolean configuredPlacement = IrisStructureLocator.hasEditablePlacement(engine, editableKey); + String reason = configuredPlacement + ? "editable Iris structure has a configured placement, but no matching placement is active " + + "because its density is not positive or its Y band does not intersect this world" + : "editable Iris structure exists in this pack, but no structure placement references it"; + exclusions.add(new StructureExclusion( + StructureExclusionKind.UNPLACED, editableKey, null, reason)); + editableUnplaced++; + } + + Set configuredImportUrls = normalizedImportUrls( + engine.getDimension().getDatapackImports()); + return new StructureExclusionReport( + List.copyOf(exclusions), Set.copyOf(registeredKeys.keySet()), + configuredImportUrls, registeredExcluded, configuredUnregistered, + editableUnplaced, 0); + } + + static boolean isUnplacedEditableCandidate( + String normalizedKey, Set registeredKeys, + Set locatableEditableKeys, boolean nativePlacement) { + return normalizedKey != null + && !normalizedKey.isEmpty() + && !registeredKeys.contains(normalizedKey) + && !locatableEditableKeys.contains(normalizedKey) + && !nativePlacement; + } + + private static StructureExclusionReport includeManagedDatapackStructures( + StructureExclusionReport snapshot, List installedEntries) { + Map unregisteredStructures = new LinkedHashMap<>(); + for (DatapackIngestService.Entry entry : installedEntries) { + String importUrl = normalizeImportUrl(entry.url); + if (importUrl.isEmpty() || !snapshot.configuredImportUrls().contains(importUrl)) { + continue; + } + String sourceId = entry.id == null || entry.id.isBlank() ? "unknown" : entry.id.trim(); + List structureKeys = entry.structureKeys == null ? List.of() : entry.structureKeys; + for (String key : structureKeys) { + String normalizedKey = normalizeStructureKey(key); + if (normalizedKey.isEmpty() || snapshot.registeredKeys().contains(normalizedKey)) { + continue; + } + ManagedDatapackStructure managed = unregisteredStructures.computeIfAbsent( + normalizedKey, ignored -> new ManagedDatapackStructure(key.trim())); + managed.sourceIds().add(sourceId); + } + } + + List entries = new ArrayList<>(); + int configuredUnregistered = snapshot.configuredUnregistered(); + for (StructureExclusion exclusion : snapshot.entries()) { + boolean replacedByManagedSource = exclusion.kind() == StructureExclusionKind.UNREGISTERED + && unregisteredStructures.containsKey(normalizeStructureKey(exclusion.key())); + if (replacedByManagedSource) { + configuredUnregistered--; + continue; + } + entries.add(exclusion); + } + for (ManagedDatapackStructure managed : unregisteredStructures.values()) { + entries.add(new StructureExclusion( + StructureExclusionKind.UNREGISTERED, managed.key(), + String.join(",", managed.sourceIds()), + "declared by an Iris-managed datapack but absent from the live registry; " + + "restart, enablement, or datapack validation may be required")); + } + sortStructureExclusions(entries); + return new StructureExclusionReport( + List.copyOf(entries), snapshot.registeredKeys(), snapshot.configuredImportUrls(), + snapshot.registeredExcluded(), Math.max(0, configuredUnregistered), + snapshot.editableUnplaced(), unregisteredStructures.size()); + } + + private static void printStructureExclusionReport( + String worldName, StructureExclusionReport report) { + Iris.info("Iris goto unregistered report for world '%s': %d non-generating structure candidate(s).", + worldName, report.entries().size()); + for (StructureExclusion exclusion : report.entries()) { + String source = exclusion.source() == null + ? "" + : " (source " + exclusion.source() + ")"; + Iris.info("[%s] %s%s: %s", + exclusion.kind().label(), exclusion.key(), source, exclusion.reason()); + } + Iris.info("Iris goto unregistered summary: %d registered key(s) excluded, " + + "%d managed datapack key(s) unregistered, %d configured native key(s) unregistered, " + + "%d editable Iris structure(s) unplaced.", + report.registeredExcluded(), report.managedUnregistered(), + report.configuredUnregistered(), report.editableUnplaced()); + Iris.info("Eligibility report only; no chunks were searched and existing generated starts were not scanned."); + } + + private static boolean needsNativeReachabilityReason( + StructureLookupRoute route, IrisNativeStructureDecision decision) { + return route == StructureLookupRoute.UNREACHABLE + || route == StructureLookupRoute.NO_ACTIVE_PLACEMENT && decision.generate(); + } + + static String describeStructureExclusion( + StructureLookupRoute route, String key, NativeStructureGenerationStatus status, + boolean nativePlacement, List requiredBiomes, Set possibleBiomeKeys) { + return switch (route) { + case UNKNOWN -> "the key is not registered by the active server/datapack"; + case POLICY_DISABLED -> NativeStructureGenerationPolicy.generationStatusMessage(key, status); + case WORLD_DISABLED -> "native structure generation is disabled for this world"; + case UNREACHABLE -> nativeReachabilityReason(requiredBiomes, possibleBiomeKeys); + case NO_ACTIVE_PLACEMENT -> { + String placementType = nativePlacement + ? "configured nativeStructures placement" + : "configured Iris replacement"; + String reason = placementType + " is inactive: no matching placement has positive density " + + "when density-based and a Y band intersecting this world"; + if (status == NativeStructureGenerationStatus.GENERATE_NATIVE) { + reason += "; the native route is also inactive because " + + nativeReachabilityReason(requiredBiomes, possibleBiomeKeys); + } + yield reason; + } + case IRIS, NATIVE -> throw new IllegalArgumentException( + "Active structure route cannot be described as excluded: " + route); + }; + } + + static String nativeReachabilityReason( + List requiredBiomes, Set possibleBiomeKeys) { + if (requiredBiomes == null || requiredBiomes.isEmpty()) { + return "its resolved biome filter is empty"; + } + Set possible = possibleBiomeKeys == null ? Set.of() : possibleBiomeKeys; + for (String requiredBiome : requiredBiomes) { + if (possible.contains(normalizeStructureKey(requiredBiome))) { + return "no active positive-weight, positive-frequency structure-set entry includes it in this world"; + } + } + return "this pack does not produce any of its required biome(s): " + + String.join("/", requiredBiomes); + } + + static String describeConfiguredUnregisteredNative(boolean locatablePlacement) { + String reason = "configured in nativeStructures, but the key is not registered by the active " + + "server/datapack, so Minecraft cannot create its native structure start"; + return locatablePlacement + ? reason + : reason + "; its Iris placement is also inactive because no matching placement has positive " + + "density when density-based and a Y band intersecting this world"; + } + + private static Map distinctStructureKeys(List keys) { + Map distinctKeys = new LinkedHashMap<>(); + for (String key : keys) { + String normalizedKey = normalizeStructureKey(key); + if (!normalizedKey.isEmpty()) { + distinctKeys.putIfAbsent(normalizedKey, key.trim()); + } + } + return distinctKeys; + } + + private static Set normalizedStructureKeys(Iterable keys) { + Set normalizedKeys = new LinkedHashSet<>(); + if (keys == null) { + return normalizedKeys; + } + for (String key : keys) { + String normalizedKey = normalizeStructureKey(key); + if (!normalizedKey.isEmpty()) { + normalizedKeys.add(normalizedKey); + } + } + return normalizedKeys; + } + + private static Set normalizedImportUrls(Iterable urls) { + Set normalizedUrls = new LinkedHashSet<>(); + if (urls == null) { + return normalizedUrls; + } + for (String url : urls) { + String normalizedUrl = normalizeImportUrl(url); + if (!normalizedUrl.isEmpty()) { + normalizedUrls.add(normalizedUrl); + } + } + return Set.copyOf(normalizedUrls); + } + + private static String normalizeImportUrl(String url) { + return url == null ? "" : url.trim(); + } + + private static void sortStructureExclusions(List exclusions) { + exclusions.sort((left, right) -> { + int kindComparison = left.kind().compareTo(right.kind()); + return kindComparison == 0 + ? left.key().compareToIgnoreCase(right.key()) + : kindComparison; + }); + } + + private static String normalizeStructureKey(String key) { + return key == null ? "" : key.trim().toLowerCase(Locale.ROOT); + } + + static StructureLookupRoute selectStructureLookupRoute( + boolean registered, IrisNativeStructureDecision decision, boolean nativePlacement, + boolean locatableNativePlacement, boolean locatableEditablePlacement, + boolean nativeGenerationEnabled, boolean reachable) { + if (!registered) { + return !nativePlacement && locatableEditablePlacement + ? StructureLookupRoute.IRIS + : StructureLookupRoute.UNKNOWN; + } + if (decision == null) { + throw new IllegalArgumentException("Registered structure lookup requires a generation decision"); + } + if (!decision.generate() + && decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + return StructureLookupRoute.POLICY_DISABLED; + } + if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + if (!nativePlacement) { + return locatableEditablePlacement + ? StructureLookupRoute.IRIS + : StructureLookupRoute.NO_ACTIVE_PLACEMENT; + } + if (!nativeGenerationEnabled) { + return StructureLookupRoute.WORLD_DISABLED; + } + return locatableNativePlacement + ? StructureLookupRoute.NATIVE + : StructureLookupRoute.NO_ACTIVE_PLACEMENT; + } + if (!nativeGenerationEnabled) { + return StructureLookupRoute.WORLD_DISABLED; + } + if (nativePlacement && locatableNativePlacement) { + return StructureLookupRoute.NATIVE; + } + if (reachable) { + return StructureLookupRoute.NATIVE; + } + return nativePlacement + ? StructureLookupRoute.NO_ACTIVE_PLACEMENT + : StructureLookupRoute.UNREACHABLE; + } + + private static Structure resolveNativeStructure(String structureKey) { + Registry structureRegistry = Bukkit.getRegistry(Structure.class); + if (structureRegistry == null) { + return null; + } + for (Structure candidate : structureRegistry) { + NamespacedKey key = structureRegistry.getKey(candidate); + if (key != null && key.toString().equalsIgnoreCase(structureKey)) { + return candidate; + } + } + return null; + } + + private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) { + Player target = player(); + if (target == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE_2)); + return; + } + World targetWorld = target.getWorld(); + Location origin = target.getLocation(); + int blockX = origin.getBlockX(); + int blockZ = origin.getBlockZ(); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_LOCATING_2, MessageArgument.untrusted("structure", structure))); + J.a(() -> { + try { + IrisStructureLocator.LocateResult result = + IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024); + if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { + sendStructureMessage(target, commandSender, + C.YELLOW + "Unable to locate " + structure + + ": the density search safety limit was reached before the full 1024-chunk radius was searched."); + return; + } + if (!result.found()) { + sendStructureMessage(target, commandSender, + C.YELLOW + "No " + structure + " found within 1024 chunks of you."); + return; + } + Location destination = new Location( + targetWorld, result.originX(), result.baseY(), result.originZ()); + prepareStructureTeleport(target, targetWorld, commandSender, structure, destination, true); + } catch (Throwable t) { + sendStructureMessage(target, commandSender, + C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName()); + Iris.reportError("Could not locate Iris-placed structure '" + structure + "'.", t); + } + }); + } + + @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) + String object, + @Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true") + boolean teleport + ) { + Engine e = engine(); + + if (e == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD_4)); + return; + } + + Player studioPlayer = player(); + if (studioPlayer != null) { + try { + if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_OBJECT_STUDIO_TELEPORTING, MessageArgument.untrusted("object", object))); + return; + } + } catch (Throwable t) { + Iris.reportError(t); + } + } + + if (e.hasObjectPlacement(object)) { + EngineBukkitOps.gotoObject(e, object, player(), teleport); + return; + } + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS, MessageArgument.untrusted("object", object))); + } + + 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); + 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 + "."); + } + }); + } + + private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure, + Location at, boolean useLocatedY) { + try { + int y = useLocatedY + ? 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()))); + }); + } 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 sendStructureMessage(Player target, VolmitSender commandSender, String message) { + J.runEntity(target, () -> commandSender.sendMessage(message)); + } + + enum StructureLookupRoute { + UNKNOWN, + POLICY_DISABLED, + NO_ACTIVE_PLACEMENT, + WORLD_DISABLED, + UNREACHABLE, + IRIS, + NATIVE + } + + private enum StructureExclusionKind { + UNREGISTERED("unregistered"), + EXCLUDED("excluded"), + UNPLACED("unplaced"); + + private final String label; + + StructureExclusionKind(String label) { + this.label = label; + } + + private String label() { + return label; + } + } + + private record StructureExclusion( + StructureExclusionKind kind, String key, String source, String reason) { + } + + private record StructureExclusionReport( + List entries, Set registeredKeys, + Set configuredImportUrls, int registeredExcluded, + int configuredUnregistered, int editableUnplaced, int managedUnregistered) { + } + + private record ManagedDatapackStructure(String key, Set sourceIds) { + private ManagedDatapackStructure(String key) { + this(key, new LinkedHashSet<>()); + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java new file mode 100644 index 000000000..f7bc3e3ae --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java @@ -0,0 +1,1011 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.BukkitWorldReconciler; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisStartupValidation; +import art.arcane.iris.core.DatapackInstallResult; +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.BukkitWorldConfiguration; +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.nms.INMS; +import art.arcane.iris.core.pack.AtomicDirectoryPublisher; +import art.arcane.iris.core.pack.PackDownloader; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.core.pack.PackValidationRegistry; +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.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 org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +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 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.volmlib.util.localization.MessageArgument; +import art.arcane.iris.core.localization.BukkitCommandMessagesExtended; +import art.arcane.iris.core.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__"; + private static final String PRESERVE_REPLACEMENT_SEED = "preserve"; + private static final long WORLD_UNLOAD_TIMEOUT_SECONDS = 150L; + + private CommandStudio studio; + private CommandPregen pregen; + private CommandObject object; + private CommandStructure structure; + private CommandJigsaw jigsaw; + private CommandWhat what; + private CommandEdit edit; + private CommandDeveloper developer; + private CommandPack pack; + private CommandFind find; + private CommandDatapack datapack; + VolmitSender sender = Iris.getSender(); + + @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") + String name, + @Param( + aliases = {"dimension", "pack"}, + description = "The dimension/pack to create the world with", descriptionKey = "iris.director.commandiris.param.dimension_pack_create_world_with", + defaultValue = "default", + customHandler = PackDimensionTypeHandler.class + ) + String type, + @Param(description = "The seed to generate the world with", descriptionKey = "iris.director.commandiris.param.seed_generate_world_with", defaultValue = "1337") + long seed + ) { + NamespacedKey worldKey; + try { + worldKey = IrisWorldStorage.managedKeyFromName(name); + IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey); + } catch (IllegalArgumentException e) { + sender().sendMessage(C.RED + e.getMessage()); + return; + } + String worldName = IrisWorldStorage.logicalName(worldKey); + if (worldName.equalsIgnoreCase("iris")) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_IRIS_CREATING_WORLDS_AS_IRIS)); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD)); + return; + } + + if (worldName.equalsIgnoreCase("benchmark")) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_BENCHMARK_CREATING_WORLDS_AS_IRIS)); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD_2)); + return; + } + + if (IrisWorldStorage.dimensionRoot(worldName).exists()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS)); + return; + } + + String resolvedType = type.equalsIgnoreCase("default") + ? IrisSettings.get().getGenerator().getDefaultWorldType() + : type; + + IrisDimension dimension = IrisToolbelt.getDimension(resolvedType); + if (dimension == null) { + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND, + MessageArgument.untrusted("dimension", resolvedType) + )); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND)); + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART, + MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType)) + )); + return; + } + + if (J.isFolia()) { + boolean staged = stageFoliaWorldCreation(worldName, dimension, seed); + if (!staged) { + return; + } + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD, MessageArgument.untrusted("worldName", worldName))); + ServerConfigurator.restart("Iris staged Folia world \"" + worldName + "\" for startup."); + return; + } + + try { + IrisToolbelt.createWorld() + .dimension(resolvedType) + .name(worldName) + .seed(seed) + .sender(sender()) + .studio(false) + .create(); + } catch (Throwable e) { + if (reportExpectedCreationInterruption(e)) { + return; + } + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS)); + Iris.reportError("Exception raised during world creation for \"" + worldName + "\".", e); + return; + } + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD)); + } + + @Director( + description = "Replace an existing world with Iris generation on the next restart", + descriptionKey = "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart", + aliases = {"override", "overwrite"} + ) + public void replace( + @Param( + name = "target", + aliases = "world-name", + description = "The exact existing world slot to replace", + descriptionKey = "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart" + ) + String target, + @Param( + aliases = {"dimension", "pack"}, + description = "The dimension/pack to replace the world with", + defaultValue = "default", + customHandler = PackDimensionTypeHandler.class + ) + String type, + @Param( + name = "seed", + aliases = "s", + description = "The replacement seed; omit to preserve the target world's seed", + defaultValue = PRESERVE_REPLACEMENT_SEED, + customHandler = ReplacementSeedHandler.class + ) + Long seed + ) { + NamespacedKey worldKey; + try { + worldKey = Iris.instance.pendingWorldReplacements().resolveRequestedWorldKey(target); + } catch (IllegalArgumentException e) { + sender().sendMessage(C.RED + e.getMessage()); + return; + } + + String resolvedType = type.equalsIgnoreCase("default") + ? IrisSettings.get().getGenerator().getDefaultWorldType() + : type; + IrisDimension dimension = IrisToolbelt.getDimension(resolvedType); + if (dimension == null) { + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND, + MessageArgument.untrusted("dimension", resolvedType) + )); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND)); + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART, + MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType)) + )); + return; + } + + try { + PendingWorldReplacementManager.StagedReplacement staged = Iris.instance + .pendingWorldReplacements() + .stageReplacement(sender(), worldKey, dimension, seed); + String seedDetail = seed == null + ? " preserving seed " + staged.seed() + : " using seed " + staged.seed(); + 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 (Throwable failure) { + Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure); + String detail = failure.getMessage() == null || failure.getMessage().isBlank() + ? failure.getClass().getSimpleName() + : failure.getMessage(); + sender().sendMessage(C.RED + "Could not stage the world replacement: " + detail); + } + } + + private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed) { + try { + IrisStartupValidation.requireWorldCreationReady(); + PackValidationRegistry.requireLoadable( + dimension.getLoader().getDataFolder().getName()); + } catch (RuntimeException exception) { + sender().sendMessage(C.RED + exception.getMessage()); + return false; + } + NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(name); + LifecycleOperationCoordinator.Lease worldLease = null; + File worldFolder = IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey); + Path stagedWorld = null; + try { + LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get(); + worldLease = coordinator.acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_CREATE, + worldKey.toString()); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA)); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_PREPARING_WORLD_FILES_BUKKIT_YML_NEXT_STARTUP)); + if (worldFolder.exists()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS)); + return false; + } + + DatapackInstallResult datapackResult = ServerConfigurator.installDataPacksIfChanged(true); + if (!datapackResult.succeeded()) { + sender().sendMessage(C.RED + "Failed to compile the Iris datapack. No world files were staged."); + return false; + } + + Path targetWorld = worldFolder.toPath().toAbsolutePath().normalize(); + Path namespaceRoot = targetWorld.getParent(); + if (namespaceRoot == null) { + throw new IOException("Iris world target has no namespace directory: " + targetWorld); + } + Files.createDirectories(namespaceRoot); + stagedWorld = Files.createTempDirectory(namespaceRoot, ".iris-create-" + worldKey.getKey() + "-"); + Path sourceOverworld = IrisWorldStorage.dimensionRoot( + IrisWorldStorage.levelRoot(), + NamespacedKey.minecraft("overworld") + ).toPath(); + INMS.get().writeCurrentPaperWorldData(sourceOverworld, stagedWorld, seed); + + IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld( + sender(), + dimension, + stagedWorld.toFile() + ); + if (installed == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey()))); + return false; + } + + try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publishAbsent( + stagedWorld, + targetWorld + )) { + stagedWorld = null; + if (!registerWorldInBukkitYml(worldKey, dimension.getLoadKey(), seed)) { + return false; + } + publication.commit(); + } + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_STAGED_IRIS_WORLD_WITH_GENERATOR_IRIS_SEED, MessageArgument.untrusted("name", name), MessageArgument.untrusted("value", dimension.getLoadKey()), MessageArgument.untrusted("seed", seed))); + return true; + } catch (LifecycleOperationCoordinator.BusyException e) { + sender().sendMessage(C.YELLOW + e.getMessage()); + return false; + } catch (Throwable e) { + sender().sendMessage(C.RED + "Failed to stage the complete Iris world: " + e.getMessage()); + Iris.reportError("Failed to stage complete Folia world \"" + worldKey + "\".", e); + return false; + } finally { + if (stagedWorld != null) { + deleteDirectorySafely(stagedWorld.toFile()); + } + if (worldLease != null) { + worldLease.close(); + } + } + } + + private boolean registerWorldInBukkitYml(NamespacedKey worldKey, String dimension, Long seed) { + String configuredWorldName = IrisWorldStorage.configuredWorldName( + worldKey, + IrisWorldStorage.levelRoot().getName() + ); + try { + BukkitWorldConfiguration.register(BUKKIT_YML, configuredWorldName, dimension, seed); + Iris.info("Registered \"" + configuredWorldName + "\" in bukkit.yml"); + return true; + } catch (IOException e) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + Iris.error("Failed to update bukkit.yml!"); + Iris.reportError(e); + return false; + } + } + + private void deleteDirectorySafely(File directory) { + try { + AtomicDirectoryPublisher.deleteTree(directory.toPath()); + } catch (IOException e) { + Iris.reportError("Failed to roll back staged world folder \"" + directory.getAbsolutePath() + "\".", e); + } + } + + private boolean reportExpectedCreationInterruption(Throwable failure) { + Throwable current = failure; + while (current != null) { + if (current instanceof LifecycleOperationCoordinator.BusyException) { + sender().sendMessage(C.YELLOW + current.getMessage()); + return true; + } + current = current.getCause(); + } + String message = failure.getMessage(); + if (message != null && message.contains("queued a restart")) { + sender().sendMessage(C.YELLOW + message); + return true; + } + return false; + } + + @Director(description = "Teleport to another world", descriptionKey = "iris.director.commandiris.director.teleport_another_world", aliases = {"tp"}, sync = true) + public void teleport( + @Param(description = "World to teleport to", descriptionKey = "iris.director.commandiris.param.world_teleport") + World world, + @Param(description = "Player to teleport", descriptionKey = "iris.director.commandiris.param.player_teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class) + Player player + ) { + if (player == null && sender().isPlayer()) + player = sender().player(); + + final Player target = player; + if (target == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST)); + return; + } + + final Location spawn = world.getSpawnLocation(); + final Runnable teleportTask = () -> { + BukkitPlatform.teleportAsync(target, spawn); + new VolmitSender(target).sendMessage(C.GREEN + IrisLanguage.text( + RuntimeUiMessages.TELEPORTED_TO_WORLD, + MessageArgument.untrusted("world", world.getName()) + )); + }; + if (!J.runEntity(target, teleportTask)) { + teleportTask.run(); + } + } + + @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(description = "Print world height information", descriptionKey = "iris.director.commandiris.director.print_world_height_information", origin = DirectorOrigin.PLAYER) + public void height() { + if (sender().isPlayer()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TO, MessageArgument.untrusted("value", sender().player().getWorld().getMinHeight()), MessageArgument.untrusted("value2", sender().player().getWorld().getMaxHeight()))); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TOTAL_HEIGHT, MessageArgument.untrusted("value", (sender().player().getWorld().getMaxHeight() - sender().player().getWorld().getMinHeight())))); + } else { + World mainWorld = getServer().getWorlds().get(0); + Iris.info(C.GREEN + "" + mainWorld.getMinHeight() + " to " + mainWorld.getMaxHeight()); + Iris.info(C.GREEN + "Total Height: " + (mainWorld.getMaxHeight() - mainWorld.getMinHeight())); + } + } + + @Director(description = "Check access of all worlds.", descriptionKey = "iris.director.commandiris.director.check_access_all_worlds", aliases = {"accesslist"}) + public void worlds() { + KList IrisWorlds = new KList<>(); + KList BukkitWorlds = new KList<>(); + + for (World w : Bukkit.getServer().getWorlds()) { + try { + Engine engine = IrisToolbelt.access(w).getEngine(); + if (engine != null) { + IrisWorlds.add(w); + } + } catch (Exception e) { + BukkitWorlds.add(w); + } + } + + if (sender().isPlayer()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_WORLDS)); + for (World IrisWorld : IrisWorlds.copy()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MESSAGE, MessageArgument.untrusted("value", IrisWorld.getName()))); + } + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_BUKKIT_WORLDS)); + for (World BukkitWorld : BukkitWorlds.copy()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MESSAGE_2, MessageArgument.untrusted("value", BukkitWorld.getName()))); + } + } else { + Iris.info(C.BLUE + "Iris Worlds: "); + for (World IrisWorld : IrisWorlds.copy()) { + Iris.info(C.IRIS + "- " +IrisWorld.getName()); + } + Iris.info(C.GOLD + "Bukkit Worlds: "); + for (World BukkitWorld : BukkitWorlds.copy()) { + Iris.info(C.GRAY + "- " +BukkitWorld.getName()); + } + + } + } + + @Director(description = "Remove an Iris world", descriptionKey = "iris.director.commandiris.director.remove_iris_world", aliases = {"rm"}, sync = true) + public void remove( + @Param(description = "The loaded or disk-only Iris world to remove", descriptionKey = "iris.director.commandiris.param.world_remove", customHandler = ManagedWorldNameHandler.class) + String world, + @Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", descriptionKey = "iris.director.commandiris.param.whether_also_remove_folder_if_set_false_just_does_not_load_world", defaultValue = "true") + boolean delete + ) { + VolmitSender responseSender = sender(); + responseSender.sendMessage(C.GRAY + "Removing Iris world '" + world + "'..."); + IrisWorldRemovalService.get().remove(world, delete).whenComplete((result, throwable) -> { + Runnable response = () -> reportRemovalResult(responseSender, world, result, throwable); + if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) { + return; + } + J.s(response); + }); + } + + private void reportRemovalResult( + VolmitSender responseSender, + String requestedWorld, + IrisWorldRemovalService.RemovalResult result, + Throwable throwable + ) { + if (throwable != null || result == null) { + Throwable failure = throwable == null + ? new IllegalStateException("World removal returned no result.") + : throwable; + responseSender.sendMessage(C.RED + "World removal failed unexpectedly; nothing further was deleted."); + Iris.reportError("Unexpected world removal failure for \"" + requestedWorld + "\".", failure); + return; + } + + switch (result.status()) { + case UNREGISTERED -> responseSender.sendMessage(C.GREEN + "Unloaded and unregistered '" + + result.target().logicalName() + "'; its files were preserved."); + case DELETED -> responseSender.sendMessage(C.GREEN + "Removed Iris world '" + + result.target().logicalName() + "' and deleted its folder."); + case DELETE_QUEUED -> responseSender.sendMessage(C.YELLOW + "Removed Iris world '" + + result.target().logicalName() + "'; its quarantined folder will be deleted at startup."); + case BUSY -> responseSender.sendMessage(C.YELLOW + "World changes are busy with " + + result.blockingOperation().kind().name().toLowerCase(Locale.ROOT) + " for '" + + result.blockingOperation().target() + "'. Try again when it completes."); + case INVALID_IDENTIFIER, PROTECTED_WORLD, NOT_IRIS_WORLD, UNSAFE_PATH, NOT_FOUND -> + responseSender.sendMessage(C.RED + removalFailureDetail(result)); + default -> { + responseSender.sendMessage(C.RED + "World removal stopped at " + + result.status().name().toLowerCase(Locale.ROOT) + ": " + removalFailureDetail(result)); + if (result.quarantineDirectory() != null) { + responseSender.sendMessage(C.YELLOW + "The recoverable world folder is " + + result.quarantineDirectory().toAbsolutePath() + "."); + } else if (result.configurationChanged() || result.registryChanged()) { + responseSender.sendMessage(C.YELLOW + "Removal changed registration state before stopping; " + + "the original world folder was not deleted."); + } + if (result.failure() != null) { + Iris.reportError("World removal failed for \"" + requestedWorld + "\" at " + + result.status().name() + ".", result.failure()); + } + } + } + } + + private String removalFailureDetail(IrisWorldRemovalService.RemovalResult result) { + Throwable failure = result.failure(); + if (failure == null || failure.getMessage() == null || failure.getMessage().isBlank()) { + return result.status().name().toLowerCase(Locale.ROOT).replace('_', ' '); + } + 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) + String pack, + @Param(name = "link", description = "A direct HTTP or HTTPS zip link", defaultValue = NO_DOWNLOAD_SOURCE) + String link + ) { + String builtInPack = NO_DOWNLOAD_SOURCE.equals(pack) ? null : pack; + String directLink = NO_DOWNLOAD_SOURCE.equals(link) ? null : link; + if ((builtInPack == null) == (directLink == null)) { + sender().sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_SOURCE)); + return; + } + if (builtInPack != null) { + Iris.service(StudioSVC.class).downloadBuiltIn(sender(), builtInPack); + return; + } + if (!PackDownloader.isDirectZipUrl(directLink)) { + sender().sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_URL)); + return; + } + Iris.service(StudioSVC.class).downloadUrl(sender(), directLink); + } + + @Director(description = "Get metrics for your world", descriptionKey = "iris.director.commandiris.director.get_metrics_your_world", aliases = "measure", origin = DirectorOrigin.PLAYER) + public void metrics() { + if (!IrisToolbelt.isIrisWorld(world())) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_MUST_BE_IRIS_WORLD)); + return; + } + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SENDING_METRICS)); + engine().printMetrics(sender()); + } + + @Director(description = "Reload configuration file (this is also done automatically)", descriptionKey = "iris.director.commandiris.director.reload_configuration_file_this_is_also_done_automatically") + public void reload() { + IrisSettings.invalidate(); + IrisSettings.get(); + boolean localeLoaded = IrisLanguage.reload(); + if (localeLoaded) { + sender().sendMessage(C.GREEN + IrisLanguage.text( + IrisMessages.COMMAND_RELOAD_SUCCESS, + MessageArgument.trusted("locale", IrisLanguage.activeLocale()) + )); + return; + } + sender().sendMessage(C.YELLOW + IrisLanguage.text( + IrisMessages.COMMAND_RELOAD_FAILED, + MessageArgument.untrusted("locale", IrisSettings.get().getGeneral().getLanguage()), + MessageArgument.trusted("activeLocale", IrisLanguage.activeLocale()) + )); + } + + + @Director(description = "Unload an Iris World", descriptionKey = "iris.director.commandiris.director.unload_iris_world", origin = DirectorOrigin.PLAYER, sync = true) + public void unloadWorld( + @Param(description = "The world to unload", descriptionKey = "iris.director.commandiris.param.world_unload") + World world + ) { + if (!IrisToolbelt.isIrisWorld(world)) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_2, MessageArgument.untrusted("value", String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList())))); + return; + } + VolmitSender responseSender = sender(); + responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_UNLOADING_WORLD, MessageArgument.untrusted("value", world.getName()))); + LifecycleOperationCoordinator.Lease lease; + try { + lease = LifecycleOperationCoordinator.get().acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_UNLOAD, + WorldIdentity.serialize(world) + ); + } catch (LifecycleOperationCoordinator.BusyException e) { + responseSender.sendMessage(C.YELLOW + e.getMessage()); + return; + } + PlatformChunkGenerator generator = IrisToolbelt.access(world); + IrisToolbelt.beginWorldMaintenance(world, "world-unload", true); + try { + AtomicBoolean terminalTimeout = new AtomicBoolean(false); + CompletableFuture sequence = IrisToolbelt.evacuateAsync(world) + .thenCompose(evacuated -> { + if (terminalTimeout.get()) { + return CompletableFuture.failedFuture(new TimeoutException( + "World unload stopped after its terminal timeout.")); + } + if (!Boolean.TRUE.equals(evacuated)) { + return CompletableFuture.completedFuture(false); + } + return WorldLifecycleService.get().unloadAsync(world, true); + }) + .thenCompose(unloaded -> { + if (terminalTimeout.get()) { + return CompletableFuture.failedFuture(new TimeoutException( + "World unload stopped after its terminal timeout.")); + } + if (!Boolean.TRUE.equals(unloaded) || generator == null) { + return CompletableFuture.completedFuture(Boolean.TRUE.equals(unloaded)); + } + return generator.closeAsync().thenApply(ignored -> true); + }); + guardUnloadCompletion(sequence, terminalTimeout, world.getName()) + .whenComplete((unloaded, throwable) -> { + IrisToolbelt.endWorldMaintenance(world, "world-unload", true); + lease.close(); + Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable); + if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) { + return; + } + J.s(response); + }); + } catch (Exception e) { + IrisToolbelt.endWorldMaintenance(world, "world-unload", true); + lease.close(); + responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e); + } + } + + private CompletableFuture guardUnloadCompletion( + CompletableFuture source, + AtomicBoolean terminalTimeout, + String worldName + ) { + CompletableFuture guarded = new CompletableFuture<>(); + AtomicBoolean settled = new AtomicBoolean(false); + source.whenComplete((unloaded, throwable) -> { + if (!settled.compareAndSet(false, true)) { + return; + } + if (throwable == null) { + guarded.complete(Boolean.TRUE.equals(unloaded)); + } else { + guarded.completeExceptionally(throwable); + } + }); + CompletableFuture.delayedExecutor(WORLD_UNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).execute(() -> { + if (!settled.compareAndSet(false, true)) { + return; + } + terminalTimeout.set(true); + TimeoutException timeout = new TimeoutException( + "World unload did not settle within " + WORLD_UNLOAD_TIMEOUT_SECONDS + + " seconds for \"" + worldName + "\"."); + ServerConfigurator.restart("World unload timed out for \"" + worldName + "\"."); + guarded.completeExceptionally(timeout); + }); + return guarded; + } + + private void reportUnloadResult(VolmitSender responseSender, World world, Boolean unloaded, Throwable throwable) { + if (throwable != null) { + responseSender.sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, + MessageArgument.untrusted("value", String.valueOf(throwable.getMessage())) + )); + Iris.reportError("Failed to unload world \"" + world.getName() + "\".", throwable); + return; + } + if (Boolean.TRUE.equals(unloaded)) { + responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_WORLD_UNLOADED_SUCCESSFULLY)); + } else { + responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_2)); + } + } + + @Director(description = "Load an Iris World", descriptionKey = "iris.director.commandiris.director.load_iris_world", origin = DirectorOrigin.PLAYER, sync = true, aliases = {"import"}) + public void loadWorld( + @Param( + description = "The name of the world to load", + descriptionKey = "iris.director.commandiris.param.name_world_load", + customHandler = ManagedWorldNameHandler.class) + String world + ) { + NamespacedKey worldKey; + try { + worldKey = IrisWorldStorage.managedKeyFromName(world); + IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey); + } catch (IllegalArgumentException failure) { + sender().sendMessage(C.RED + failure.getMessage()); + return; + } + String logicalWorldName = IrisWorldStorage.logicalName(worldKey); + boolean worldExists = doesWorldExist(logicalWorldName); + + if (!worldExists) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_DOESNT_EXIST_ON_SERVER, MessageArgument.untrusted("logicalWorldName", logicalWorldName))); + return; + } + + VolmitSender responseSender = sender(); + responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_LOADING_WORLD, MessageArgument.untrusted("logicalWorldName", logicalWorldName))); + Iris.instance.worldReconciler() + .loadWorld(BUKKIT_YML, worldKey.toString()) + .whenComplete((result, failure) -> { + Runnable response = () -> reportLoadWorldResult( + responseSender, + logicalWorldName, + result, + failure); + if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) { + return; + } + J.s(response); + }); + } + + private void reportLoadWorldResult( + VolmitSender responseSender, + String logicalWorldName, + BukkitWorldReconciler.LoadResult result, + Throwable failure + ) { + if (failure != null) { + responseSender.sendMessage(C.RED + "Failed to load Iris world \"" + logicalWorldName + "\": " + failure.getMessage()); + Iris.reportError("Failed to load Iris world \"" + logicalWorldName + "\".", failure); + return; + } + if (result == null) { + IllegalStateException missingResult = new IllegalStateException("World load completed without a result."); + responseSender.sendMessage(C.RED + missingResult.getMessage()); + Iris.reportError("Failed to load Iris world \"" + logicalWorldName + "\".", missingResult); + return; + } + if (result.succeeded()) { + responseSender.sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_LOADED_SUCCESSFULLY, + MessageArgument.untrusted("logicalWorldName", logicalWorldName))); + return; + } + + C color = result.status() == BukkitWorldReconciler.ReconciliationStatus.BUSY + || result.status() == BukkitWorldReconciler.ReconciliationStatus.RESTART_REQUIRED + ? C.YELLOW + : C.RED; + responseSender.sendMessage(color + result.message()); + Throwable resultFailure = result.failure(); + if (resultFailure != null + && result.status() != BukkitWorldReconciler.ReconciliationStatus.BUSY + && result.status() != BukkitWorldReconciler.ReconciliationStatus.RESTART_REQUIRED) { + Iris.reportError("Failed to load Iris world \"" + logicalWorldName + "\".", resultFailure); + } + } + + @Director(description = "Evacuate an iris world", descriptionKey = "iris.director.commandiris.director.evacuate_iris_world", origin = DirectorOrigin.PLAYER, sync = true) + public void evacuate( + @Param(description = "Evacuate the world", descriptionKey = "iris.director.commandiris.param.evacuate_world") + World world + ) { + if (!IrisToolbelt.isIrisWorld(world)) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_3, MessageArgument.untrusted("value", String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList())))); + return; + } + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_EVACUATING_WORLD, MessageArgument.untrusted("value", world.getName()))); + IrisToolbelt.evacuate(world); + } + + boolean doesWorldExist(String worldName) { + NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(worldName); + File levelRoot = IrisWorldStorage.levelRoot(); + return IrisWorldStorage.frozenDimensionRoot( + Bukkit.getWorldContainer(), + levelRoot, + IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName()), + worldKey + ).isPresent(); + } + + public static class ManagedWorldNameHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + Set options = new LinkedHashSet<>(); + for (World world : Bukkit.getWorlds()) { + if (!IrisToolbelt.isIrisWorld(world)) { + continue; + } + options.add(IrisWorldStorage.logicalName(world)); + String startupName = configuredStartupName(world); + if (startupName != null) { + options.add(startupName); + } + } + for (String identity : IrisWorlds.get().getWorlds().keySet()) { + try { + options.add(IrisWorldStorage.logicalName(WorldIdentity.parse(identity))); + } catch (IllegalArgumentException ignored) { + } + } + + File namespace = new File(IrisWorldStorage.levelRoot(), "dimensions/iris"); + File[] diskWorlds = namespace.listFiles(File::isDirectory); + if (diskWorlds != null) { + for (File diskWorld : diskWorlds) { + if (!Files.isSymbolicLink(diskWorld.toPath()) + && diskWorld.getName().matches("[a-z0-9_-]+")) { + options.add(diskWorld.getName()); + } + } + } + return new KList<>(options); + } + + /** + * "iris worlds" prints the Bukkit startup name, so it has to be offered and accepted here too. + */ + private static String configuredStartupName(World world) { + try { + String configured = IrisWorldStorage.configuredWorldName( + WorldIdentity.key(world), + IrisWorldStorage.levelRoot().getName() + ); + return configured.equals(world.getName()) ? configured : null; + } catch (RuntimeException failure) { + return null; + } + } + + @Override + public String toString(String value) { + return value == null ? "" : value; + } + + @Override + public String parse(String in, boolean force) throws DirectorParsingException { + if (in == null || in.isBlank()) { + throw new DirectorParsingException("World identifier cannot be empty"); + } + return in.trim(); + } + + @Override + public boolean supports(Class type) { + return type == String.class; + } + } + + public static class PackDimensionTypeHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + return packDimensionOptions(Iris.instance.getDataFolder("packs")); + } + + static KList packDimensionOptions(File packsFolder) { + Set options = new LinkedHashSet<>(); + + for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsFolder)) { + options.add(pack.getName()); + + try { + IrisData data = IrisData.get(pack); + for (String key : data.getDimensionLoader().getPossibleKeys()) { + options.add(packDimensionOption(pack.getName(), key)); + } + } catch (Throwable ex) { + Iris.warn("Failed to read dimension keys from pack %s: %s%s", + pack.getName(), + ex.getClass().getSimpleName(), + ex.getMessage() == null ? "" : " - " + ex.getMessage()); + Iris.reportError(ex); + } + } + + return new KList<>(options); + } + + static String packDimensionOption(String packName, String dimensionKey) { + return packName.equalsIgnoreCase(dimensionKey) + ? packName + : packName + ":" + dimensionKey; + } + + @Override + public String toString(String value) { + return value == null ? "" : value; + } + + @Override + public String parse(String in, boolean force) throws DirectorParsingException { + if (in == null || in.trim().isEmpty()) { + throw new DirectorParsingException("World type cannot be empty"); + } + + return in.trim(); + } + + @Override + public boolean supports(Class type) { + return type == String.class; + } + } + + public static class ReplacementSeedHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + return null; + } + + @Override + public String toString(Long value) { + return value == null ? PRESERVE_REPLACEMENT_SEED : Long.toString(value); + } + + @Override + public Long parse(String in, boolean force) throws DirectorParsingException { + String seed = in == null ? "" : in.trim(); + if (PRESERVE_REPLACEMENT_SEED.equalsIgnoreCase(seed)) { + return null; + } + try { + return Long.parseLong(seed); + } catch (NumberFormatException failure) { + throw new DirectorParsingException("Seed must be a signed 64-bit integer"); + } + } + + @Override + public boolean supports(Class type) { + return type == Long.class; + } + + @Override + public String getRandomDefault() { + return "1337"; + } + } + + public static class DownloadPackHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + return new KList<>(PackDownloader.builtInPacks()); + } + + @Override + public String toString(String value) { + return value == null ? "" : value; + } + + @Override + public String parse(String in, boolean force) throws DirectorParsingException { + if (NO_DOWNLOAD_SOURCE.equals(in)) { + return null; + } + String pack = in == null ? "" : in.trim().toLowerCase(Locale.ROOT); + if (!PackDownloader.isBuiltInPack(pack)) { + throw new DirectorParsingException("Pack must be 'overworld' or 'underworld'"); + } + return pack; + } + + @Override + public boolean supports(Class type) { + return type == String.class; + } + } + +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java new file mode 100644 index 000000000..6c0d302c6 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java @@ -0,0 +1,2209 @@ +package art.arcane.iris.core.commands; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.runtime.WorldRuntimeControlService; +import art.arcane.iris.core.runtime.StudioOpenCoordinator; +import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype; +import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarTopology; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioGraphMapper; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioGraphEditor; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolEditor; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioProjectCreator; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioStructureEditor; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant; +import art.arcane.iris.core.service.JigsawStudioGraphEvaluation; +import art.arcane.iris.core.service.JigsawStudioService; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.core.structure.StructureImporter; +import art.arcane.iris.core.structure.VillageImporter; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionDiagnostic; +import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionInputKind; +import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionPlan; +import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionRequest; +import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionResult; +import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionService; +import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionStrategy; +import art.arcane.iris.core.structure.export.VanillaJigsawDatapackExporter; +import art.arcane.iris.core.structure.export.VanillaJigsawExportDiagnostic; +import art.arcane.iris.core.structure.export.VanillaJigsawExportFormat; +import art.arcane.iris.core.structure.export.VanillaJigsawExportRequest; +import art.arcane.iris.core.structure.export.VanillaJigsawExportResult; +import art.arcane.iris.core.structure.export.VanillaJigsawExportSource; +import art.arcane.iris.engine.framework.structure.PlanarJigsawWorkcellResolver; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisJigsawCompatibility; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawThemeSet; +import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.specialhandlers.IrisStructureHandler; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.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.GameMode; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.type.Jigsaw; +import org.bukkit.entity.Player; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +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.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +@Director(name = "jigsaw", aliases = {"jig", "jgs"}, description = "Iris Jigsaw Studio tools", + origin = DirectorOrigin.PLAYER) +public class CommandJigsaw implements DirectorExecutor { + private static final long DEFAULT_STUDIO_SEED = 1337L; + static final StudioOpenCoordinator.StudioOpenKind STUDIO_OPEN_KIND = + StudioOpenCoordinator.StudioOpenKind.JIGSAW; + private static final Pattern EXPORT_OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}"); + private static final Map PLAYER_PACKS = new ConcurrentHashMap<>(); + private static final Map ADOPTION_PLANS = new ConcurrentHashMap<>(); + private static final Map ADOPTION_SERVICES = new ConcurrentHashMap<>(); + private static final Set EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet(); + private static final Set EXPORT_DESTINATIONS = ConcurrentHashMap.newKeySet(); + + 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/.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()); + 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/.json", + customHandler = IrisStructureHandler.class) String structure, + @Param(description = "Studio world seed", defaultValue = "1337") long seed + ) { + openProject(player(), sender(), dimension, structure, seed); + } + + private 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)) { + sendError(commandSender, "Jigsaw Studio is already owned by another player session."); + return; + } + UUID openingOwner = JigsawStudioActivation.openingOwnerId(); + if (openingOwner != null) { + sendError(commandSender, openingOwner.equals(playerId) + ? "Your Jigsaw Studio is already opening." + : "Jigsaw Studio is already opening for another player."); + return; + } + ActiveContext previous = active(targetPlayer); + if (ownerId != null && previous == null) { + sendError(commandSender, "Your active Jigsaw Studio binding is unavailable. Close it before opening another project."); + return; + } + ActiveContext dirty = findDirtyContext(); + if (dirty != null) { + sendError(commandSender, + "The current Jigsaw Studio is waiting for autosave. Wait for it to finish or close with discard=true."); + return; + } + IrisData data = requireData(dimension, commandSender); + if (data == null) { + return; + } + IrisStructure irisStructure = data.load(IrisStructure.class, structure, false); + if (irisStructure == null) { + 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); + sendError(commandSender, "Could not map this jigsaw graph: " + exception.getMessage()); + return; + } + String packKey = dimension.getLoadKey(); + if (!JigsawStudioActivation.tryBeginOpen(playerId)) { + 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); + 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(), + 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); + 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, + 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); + sendError(commandSender, "Jigsaw Studio open failed: " + exception.getMessage()); + } + } + + @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 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 = 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 = 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(exportStartError(exportStart)); + return; + } + if (!beginExport(playerId, destination)) { + studioService.finishExport(requestId); + sendError("A vanilla Jigsaw export for this player or output is already in progress."); + return; + } + ExportLease lease = new ExportLease(() -> { + finishExport(playerId, destination); + studioService.finishExport(requestId); + }); + ExportOperation operation = new ExportOperation( + targetPlayer, + sender(), + requestId, + context.request().structureKey(), + request, + lease); + sender().sendMessage(C.GRAY + "Exporting '" + operation.structureKey() + + "' in the background to " + destination + "..."); + try { + J.a(() -> 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 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()); + } + + private static void pruneAdoptionPlanBindings() { + for (Map.Entry entry : ADOPTION_PLANS.entrySet()) { + AdoptionPlanBinding binding = entry.getValue(); + if (binding.service().plan(entry.getKey()).isEmpty()) { + ADOPTION_PLANS.remove(entry.getKey(), binding); + } + } + } + + private static boolean studioIsActiveOrOpening() { + return JigsawStudioActivation.activeOwnerId() != null + || JigsawStudioActivation.openingOwnerId() != null; + } + + private static void runAdoptionApply( + Player targetPlayer, + VolmitSender commandSender, + UUID planId, + AdoptionPlanBinding binding + ) { + if (studioIsActiveOrOpening()) { + ADOPTION_PLANS.putIfAbsent(planId, binding); + J.runEntity(targetPlayer, () -> 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 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) { + 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) { + 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(); + openProject(targetPlayer, commandSender, binding.dimension(), target.path(), 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; + }; + } + + private 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()); + } + ActiveContext current = 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 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); + } + + private 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()); + } + + private 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 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); + 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()); + PLAYER_PACKS.put(targetPlayer.getUniqueId(), new 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; + } + + private void sendError(String message) { + sender().sendMessage(C.RED + message); + } + + private 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."); + }; + } + + private 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 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 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 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; + } + + 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 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(); + } + } + + private record SessionBinding(String packKey, UUID requestId) { + } + + private 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(); + } + } + } + + private record AdoptionPlanBinding( + UUID ownerId, + IrisDimension dimension, + IrisData data, + IrisStructureAdoptionService service, + IrisStructureAdoptionPlan plan + ) { + } + + public static final class JigsawModeHandler implements DirectorParameterHandler { + @Override + public KList 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 { + @Override + public KList 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; + } + } + + public static final class JigsawAdoptionStrategyHandler implements DirectorParameterHandler { + @Override + public KList 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 { + @Override + public KList 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; + } + } + + @Director(name = "adopt", description = "Inspect and apply safe ownership adoption plans", + origin = DirectorOrigin.PLAYER) + public static class CommandJigsawAdopt implements DirectorExecutor { + @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 = requireData(dimension, commandSender); + if (data == null) { + return; + } + String sourceKey; + Optional targetKey; + IrisStructureAdoptionStrategy adoptionStrategy; + Path root; + try { + sourceKey = parseAdoptionSource(source); + targetKey = parseAdoptionTarget(target); + adoptionStrategy = parseAdoptionStrategy(strategy); + root = canonicalAdoptionRoot(data.getDataFolder().toPath()); + } catch (IOException | IllegalArgumentException exception) { + 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); + 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) { + sendError(commandSender, "Adoption plan ID must be a UUID."); + return; + } + if (studioIsActiveOrOpening()) { + 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)) { + 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); + sendError(commandSender, "Adoption plan '" + parsedPlanId + + "' expired or was already consumed. Run adopt inspect again."); + return; + } + if (!ADOPTION_PLANS.remove(parsedPlanId, binding)) { + 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); + sendError(commandSender, "Jigsaw adoption could not be scheduled: " + exception.getMessage()); + } + } + } + + @Director(name = "connector", description = "Inspect and configure targeted jigsaw connectors", + origin = DirectorOrigin.PLAYER) + public static 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + Block target = player().getTargetBlockExact(8); + if (target == null || !(target.getBlockData() instanceof Jigsaw)) { + 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) { + sendError(this, "The targeted marker must be inside a workcell with an active variant."); + return; + } + if (!activeVariant.owned()) { + 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) { + 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) { + sendError(this, "Connector channel update failed: " + exception.getMessage()); + return; + } + String normalized = channel == null || channel.isBlank() || "none".equalsIgnoreCase(channel) + ? "none" + : channel.trim(); + runGraphMutation(player(), context, () -> { + JigsawStudioGraphEditor.updateConnectorChannel( + context.request().source().getDataFolder().toPath(), + context.request().structureKey(), + activeVariant.pieceKey(), + position, + channel); + return mappedMutationResult( + context, + "", + "", + "Persisted connector channel '" + normalized + "' on piece '" + + activeVariant.pieceKey() + "' at " + position.getX() + "," + position.getY() + + "," + position.getZ() + "."); + }); + } + } + + @Director(name = "pool", description = "Manage owned jigsaw pools", origin = DirectorOrigin.PLAYER) + public static 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey; + runGraphMutation(player(), context, () -> { + JigsawStudioGraphEditor.createPool( + context.request().source().getDataFolder().toPath(), + context.request().structureKey(), + poolKey, + fallback); + return mappedMutationResult( + context, + "", + "", + "Created owned pool '" + poolKey + "' with fallback '" + + (fallback.isBlank() ? "none" : fallback) + "'."); + }); + } + } + + @Director(name = "piece", description = "Manage contextual Studio pieces", origin = DirectorOrigin.PLAYER) + public static 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + 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 = selectedBay(context); + } else { + context.session().selectBay(contextual.stableId()); + } + if (contextual == null || contextual.topology().isEmpty()) { + sendError(this, "Stand in or select a planar topology workcell before creating a variant."); + return; + } + topology = contextual.topology().get(); + targetWorkcell = contextual; + } + if (targetWorkcell == null) { + 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(); + runGraphMutation(player(), context, () -> { + JigsawStudioGraphEditor.createPiece( + context.request().source().getDataFolder().toPath(), + context.request().structureKey(), + poolKey, + pieceKey, + weight, + dimensions, + selectedTopology); + return 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + IrisJigsawPiece piece = context.request().source().getJigsawPieceLoader().load(pieceKey); + if (piece == null) { + sendError(this, "No jigsaw piece '" + pieceKey + "' exists in this pack."); + return; + } + if (piece.getObject() == null || piece.getObject().isBlank()) { + sendError(this, "Jigsaw piece '" + pieceKey + "' does not declare an object."); + return; + } + IrisObject object = context.request().source().getObjectLoader().load(piece.getObject()); + if (object == null) { + sendError(this, "Jigsaw piece '" + pieceKey + "' has no loadable object."); + return; + } + PieceWorkcellResolution resolution; + try { + resolution = resolvePieceWorkcell(context.session().layout(), piece, object); + } catch (IllegalArgumentException exception) { + 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(); + 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(); + 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 = loadPersistedLayout(context); + String activationWorkcellId = 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioVariant selected = selectedVariant(context); + if (selected == null) { + sendError(this, "Select a workcell with an active variant before removing its pool entry."); + return; + } + String pieceKey = selected.pieceKey(); + 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 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioVariant selected = selectedVariant(context); + if (selected == null) { + sendError(this, "Select a workcell with an active variant before changing rotation."); + return; + } + if (!rotatable + && context.request().compatibilityTarget() + == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE) { + sendError(this, "Vanilla-portable pieces must remain rotatable."); + return; + } + String pieceKey = selected.pieceKey(); + runGraphMutation(player(), context, () -> { + JigsawStudioGraphEditor.updateRotatable( + context.request().source().getDataFolder().toPath(), + context.request().structureKey(), + pieceKey, + rotatable); + return 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() { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioBay workcell = selectedBay(context); + if (workcell == null) { + sendError(this, "Select a workcell before expanding its active variant."); + return; + } + JigsawStudioService.get().expandVariantToCell(player(), workcell.stableId()); + } + } + + @Director(name = "workcell", aliases = "cell", description = "Manage the selected Studio workcell", + origin = DirectorOrigin.PLAYER) + public static 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioBay selected = selectedBay(context); + if (selected == null) { + sendError(this, "Select a workcell before changing its capacity."); + return; + } + JigsawStudioCellDimensions dimensions; + try { + dimensions = new JigsawStudioCellDimensions(width, height, depth); + } catch (IllegalArgumentException exception) { + 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioBay selected = selectedBay(context); + if (selected == null) { + 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() { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioBay selected = selectedBay(context); + if (selected == null) { + sendError(this, "Select a workcell before resetting its label."); + return; + } + JigsawStudioService.get().updateWorkcellDisplayName(player(), selected.stableId(), ""); + } + } + + @Director(name = "variant", description = "Manage selected jigsaw variants", origin = DirectorOrigin.PLAYER) + public static 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioVariant selected = selectedVariant(context); + if (selected == null) { + sendError(this, "Select a workcell with an active variant before changing its weight."); + return; + } + String pieceKey = selected.pieceKey(); + 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 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioBay selectedWorkcell = selectedBay(context); + JigsawStudioVariant selectedVariant = selectedVariant(context); + if (selectedWorkcell == null || selectedVariant == null) { + 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) { + 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioBay selectedWorkcell = selectedBay(context); + JigsawStudioVariant selectedVariant = selectedVariant(context); + if (selectedWorkcell == null || selectedVariant == null) { + 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() { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioBay selectedWorkcell = selectedBay(context); + JigsawStudioVariant selectedVariant = selectedVariant(context); + if (selectedWorkcell == null || selectedVariant == null) { + 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() { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + JigsawStudioBay selected = selectedBay(context); + if (selected == null || selectedVariant(context) == null) { + 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + String selectedTheme = themeKey; + if ("next".equalsIgnoreCase(themeKey)) { + try { + selectedTheme = nextThemeKey(context); + } catch (IOException | ArithmeticException exception) { + sendError(this, exception.getMessage()); + return; + } + } + JigsawStudioService.get().duplicateActiveFamily(player(), selectedTheme); + } + } + + @Director(name = "rules", aliases = "rule", description = "Manage structure-wide jigsaw rules", + origin = DirectorOrigin.PLAYER) + public static 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + if (context.request().compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE + && (maxDepth > 20 || maxSizeChunks > 8)) { + sendError(this, "Vanilla-portable rules require maxDepth <= 20 and maxSizeChunks <= 8."); + return; + } + runGraphMutation(player(), context, () -> { + JigsawStudioStructureEditor.updateLimits( + context.request().source().getDataFolder().toPath(), + context.request().structureKey(), + maxDepth, + maxSizeChunks); + return 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 + ) { + ActiveContext context = active(player()); + if (context == null) { + sendError(this, "Open a Jigsaw Studio project first."); + return; + } + String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey; + runGraphMutation(player(), context, () -> { + JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.updateFallback( + context.request().source().getDataFolder().toPath(), + context.request().structureKey(), + poolKey, + fallback); + if (!update.changed()) { + return mappedMutationResult(context, "", "", "Pool fallback is unchanged."); + } + return mappedMutationResult( + context, + "", + "", + "Persisted pool '" + poolKey + "' fallback as '" + + (fallback.isBlank() ? "none" : fallback) + "'."); + }); + } + } + + @Director(name = "preview", description = "Assemble a read-only jigsaw preview", + origin = DirectorOrigin.PLAYER) + public static 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); + } + + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java new file mode 100644 index 000000000..e12d3e682 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java @@ -0,0 +1,835 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +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.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.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.data.Cuboid; +import art.arcane.iris.util.common.data.IrisCustomData; +import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler; +import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.math.Direction; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.GameMode; +import org.bukkit.HeightMap; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; + +import java.io.File; +import java.io.IOException; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.BukkitCommandMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.iris.core.localization.BukkitCommandMessagesExtended; +import art.arcane.iris.core.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 PASTE_TRANSPARENT_BLOCKS = Set.of( + Material.AIR, + Material.CAVE_AIR, + Material.VOID_AIR, + Material.SHORT_GRASS, + Material.SNOW, + Material.VINE, + Material.TORCH, + Material.DEAD_BUSH, + Material.POPPY, + Material.DANDELION + ); + + static boolean isPasteTarget(Material material) { + return !PASTE_TRANSPARENT_BLOCKS.contains(material); + } + + @Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", descriptionKey = "iris.director.commandobject.director.open_object_studio_world_grid_every_object_dimension_optional_defaults_all_packs", sync = true) + public void studio( + @Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", descriptionKey = "iris.director.commandobject.param.optional_dimension_whose_object_pack_lay_out_omit_aggregate_objects_from_every", aliases = "dim", customHandler = NullableDimensionHandler.class) + IrisDimension dimension, + @Param(defaultValue = "1337", description = "The seed to generate the studio with", descriptionKey = "iris.director.commandobject.param.seed_generate_studio_with", aliases = "s") + long seed + ) { + VolmitSender commandSender = sender(); + Map sources = new LinkedHashMap<>(); + IrisDimension hostDimension = dimension; + + if (dimension != null) { + IrisData data = dimension.getLoader(); + if (data == null) { + data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile()); + } + sources.put(data.getDataFolder().getName(), data); + } else { + File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder(); + for (File pack : PackDirectoryResolver.listVisiblePackDirectories(workspace)) { + File dimensionsDir = new File(pack, "dimensions"); + if (!dimensionsDir.isDirectory()) continue; + IrisData data = IrisData.get(pack); + String[] keys = data.getObjectLoader().getPossibleKeys(); + if (keys == null || keys.length == 0) continue; + sources.put(pack.getName(), data); + if (hostDimension == null) { + File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json")); + if (dimFiles != null && dimFiles.length > 0) { + String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", ""); + IrisDimension loaded = data.getDimensionLoader().load(loadKey); + if (loaded != null) { + hostDimension = loaded; + } + } + } + } + } + + if (hostDimension == null || sources.isEmpty()) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_NO_PACKS_WITH_OBJECTS_WERE_FOUND_ON_THIS_SERVER)); + return; + } + + int totalObjects = 0; + for (IrisData d : sources.values()) { + String[] k = d.getObjectLoader().getPossibleKeys(); + if (k != null) totalObjects += k.length; + } + if (totalObjects == 0) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_NO_OBJECTS_PLACE_ACROSS_SELECTED_PACK_S)); + return; + } + + // ObjectStudioActivation carries the buffet state; mutating the shared cached + // IrisDimension here leaked OBJECT_BUFFET into later pack exports of this dimension. + ObjectStudioActivation.activate(hostDimension.getLoadKey()); + ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources); + + String scope = dimension == null + ? ("all packs [" + sources.size() + "]") + : ("\"" + hostDimension.getName() + "\""); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_OPENING_OBJECT_STUDIO_OBJECTS, MessageArgument.untrusted("scope", scope), MessageArgument.untrusted("totalObjects", totalObjects))); + + IrisDimension finalHost = hostDimension; + try { + Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> { + if (world == null) return; + try { + WorldRuntimeControlService.get().applyObjectStudioWorldRules(world); + } catch (Throwable e) { + Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e); + } + + if (commandSender.isPlayer()) { + Player p = commandSender.player(); + if (p != null) { + Location target = new Location(world, 0.5D, 66D, 0.5D); + J.runEntity(p, () -> { + BukkitPlatform.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE)); + }); + } + } + }); + } catch (Throwable e) { + Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_FAILED_OPEN_OBJECT_STUDIO, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + } + } + + public static IObjectPlacer createPlacer(World world, Map futureBlockChanges, Engine targetEngine) { + return new IObjectPlacer() { + @Override + public int getHighest(int x, int z, IrisData data) { + return world.getHighestBlockYAt(x, z); + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return world.getHighestBlockYAt(x, z, ignoreFluid ? HeightMap.OCEAN_FLOOR : HeightMap.MOTION_BLOCKING); + } + + @Override + public void set(int x, int y, int z, PlatformBlockState s) { + BlockData d = (BlockData) s.nativeHandle(); + Block block = world.getBlockAt(x, y, z); + + //Prevent blocks being set in or bellow bedrock + if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return; + + futureBlockChanges.putIfAbsent(block, block.getBlockData()); + + if (d instanceof IrisCustomData data) { + block.setBlockData(data.getBase(), false); + Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!"); + } else block.setBlockData(d, false); + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + return BukkitBlockState.of(world.getBlockAt(x, y, z).getBlockData()); + } + + @Override + public boolean isPreventingDecay() { + return false; + } + + @Override + public boolean isCarved(int x, int y, int z) { + return false; + } + + @Override + public boolean isSolid(int x, int y, int z) { + return world.getBlockAt(x, y, z).getType().isSolid(); + } + + @Override + public boolean isUnderwater(int x, int z) { + return false; + } + + @Override + public int getFluidHeight() { + return 63; + } + + @Override + public boolean isDebugSmartBore() { + return false; + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + tile.toBukkitTry(world.getBlockAt(xx, yy, zz)); + } + + @Override + public void setData(int xx, int yy, int zz, T data) { + + } + + @Override + public T getData(int xx, int yy, int zz, Class t) { + return null; + } + + @Override + public Engine getEngine() { + return targetEngine; + } + }; + } + + @Director(description = "Check the composition of an object", descriptionKey = "iris.director.commandobject.director.check_composition_object") + public void analyze( + @Param(description = "The object to analyze", descriptionKey = "iris.director.commandobject.param.object_analyze", customHandler = ObjectHandler.class) + String object + ) { + IrisObject o = IrisData.loadAnyObject(object, data()); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_OBJECT_SIZE, MessageArgument.untrusted("value", o.getW()), MessageArgument.untrusted("value2", o.getH()), MessageArgument.untrusted("value3", o.getD()))); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_BLOCKS_USED, MessageArgument.untrusted("value", NumberFormat.getIntegerInstance().format(o.getBlocks().size())))); + + var queue = o.getBlocks().values(); + Map> unsorted = new HashMap<>(); + Map amounts = new HashMap<>(); + Map materials = new HashMap<>(); + while (queue.hasNext()) { + BlockData block = (BlockData) queue.next().nativeHandle(); + + //unsorted.put(block.getMaterial(), block); + + if (!amounts.containsKey(block)) { + amounts.put(block, 1); + + + } else + amounts.put(block, amounts.get(block) + 1); + + if (!materials.containsKey(block.getMaterial())) { + materials.put(block.getMaterial(), 1); + unsorted.put(block.getMaterial(), new HashSet<>()); + unsorted.get(block.getMaterial()).add(block); + } else { + materials.put(block.getMaterial(), materials.get(block.getMaterial()) + 1); + unsorted.get(block.getMaterial()).add(block); + } + + } + + List sortedMatsList = amounts.keySet().stream().map(BlockData::getMaterial) + .sorted().toList(); + Set sortedMats = new TreeSet<>(Comparator.comparingInt(materials::get).reversed()); + sortedMats.addAll(sortedMatsList); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_BLOCKS_OBJECT)); + + int n = 0; + for (Material mat : sortedMats) { + int amount = materials.get(mat); + List set = new ArrayList<>(unsorted.get(mat)); + set.sort(Comparator.comparingInt(amounts::get).reversed()); + BlockData data = set.get(0); + int dataAmount = amounts.get(data); + + String string = " - " + mat.toString() + "*" + amount; + if (data.getAsString(true).contains("[")) { + string = string + " --> [" + data.getAsString(true).split("\\[")[1] + .replaceAll("true", ChatColor.GREEN + "true" + ChatColor.GRAY) + .replaceAll("false", ChatColor.RED + "false" + ChatColor.GRAY) + "*" + dataAmount; + } + + sender().sendMessage(string); + + n++; + + if (n >= 10) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_OTHER_BLOCK_TYPES, MessageArgument.untrusted("value", (sortedMats.size() - n)))); + return; + } + } + } + + @Director(description = "Shrink an object to its minimum size", descriptionKey = "iris.director.commandobject.director.shrink_object_its_minimum_size") + public void shrink(@Param(description = "The object to shrink", descriptionKey = "iris.director.commandobject.param.object_shrink", customHandler = ObjectHandler.class) String object) { + IrisObject o = IrisData.loadAnyObject(object, data()); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_CURRENT_OBJECT_SIZE, MessageArgument.untrusted("value", o.getW()), MessageArgument.untrusted("value2", o.getH()), MessageArgument.untrusted("value3", o.getD()))); + o.shrinkwrap(); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NEW_OBJECT_SIZE, MessageArgument.untrusted("value", o.getW()), MessageArgument.untrusted("value2", o.getH()), MessageArgument.untrusted("value3", o.getD()))); + try { + o.write(o.getLoadFile()); + } catch (IOException e) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", o.getLoadFile()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage())))); + e.printStackTrace(); + } + } + + @Director(description = "Grow organic branches through the canopy so every leaf survives vanilla decay", descriptionKey = "iris.director.commandobject.director.grow_organic_branches_through_canopy_so_every_leaf_survives_vanilla_decay", + origin = DirectorOrigin.BOTH) + public void plausibilize( + @Param(description = "Object key, prefix (trees/), or filesystem path", descriptionKey = "iris.director.commandobject.param.object_key_prefix_trees_filesystem_path", + customHandler = ObjectTargetHandler.class) + String target, + @Param(name = "dryrun", description = "dryrun=true analyzes only, writes nothing", descriptionKey = "iris.director.commandobject.param.dryrun_true_analyzes_only_writes_nothing", defaultValue = "false") + boolean dryRun, + @Param(name = "reach", description = "reach=N max branch length in blocks from existing wood; farther leaf clusters are pinned persistent instead. reach=0 grows unlimited", descriptionKey = "iris.director.commandobject.param.reach_n_max_branch_length_blocks_from_existing_wood_farther_leaf_clusters", defaultValue = "12") + int reach + ) { + IrisData nearest = data(); + List targets = TreePlausibilizeBatch.resolve(target, nearest); + if (targets.isEmpty() && nearest == null) { + targets = resolveFromPacks(target); + } + if (targets.isEmpty()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_OBJECTS_MATCHED, MessageArgument.untrusted("target", target))); + return; + } + + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_OBJECT_PLAUSIBILIZE_REACH_QUEUED_OBJECT_S, + MessageArgument.trusted("reach", reach), + MessageArgument.trusted("value", dryRun ? IrisLanguage.text(RuntimeUiMessages.TREE_DRY_SUFFIX) : ""), + MessageArgument.trusted("value2", targets.size()) + )); + + org.bukkit.command.CommandSender s = sender(); + List queued = targets; + J.a(() -> TreePlausibilizeBatch.run(queued, dryRun, reach, nearest, (TreePlausibilizeBatch.Output output) -> + s.sendMessage(output.headline() + ? C.IRIS + output.text() + : C.GRAY + " " + output.text()))); + } + + private static List resolveFromPacks(String target) { + List out = new ArrayList<>(); + File packsFolder = Iris.instance.getDataFolder("packs"); + for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsFolder)) { + File objectsRoot = new File(pack, "objects"); + if (!objectsRoot.isDirectory()) { + continue; + } + File candidate = new File(objectsRoot, target + ".iob"); + if (candidate.isFile()) { + out.add(new TreePlausibilizeBatch.Target(pack.getName() + "/" + target, candidate)); + continue; + } + File candidateDir = new File(objectsRoot, target); + if (candidateDir.isDirectory()) { + TreePlausibilizeBatch.walkIob(candidateDir, objectsRoot, out); + } + } + return out; + } + + @Director(description = "Convert .schem files in the 'convert' folder to .iob files.", descriptionKey = "iris.director.commandobject.director.convert_schem_files_convert_folder_iob_files") + public void convert () { + try { + IrisConverter.convertSchematics(sender()); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + @Director(description = "Get a powder that reveals objects", descriptionKey = "iris.director.commandobject.director.get_powder_that_reveals_objects", aliases = "d") + public void dust() { + VolmitSender commandSender = sender(); + Player player = player(); + + onPlayerThread(player, () -> { + player.getInventory().addItem(WandSVC.createDust()); + commandSender.playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f); + }); + } + + @Director(description = "Contract a selection based on your looking direction", descriptionKey = "iris.director.commandobject.director.contract_selection_based_on_your_looking_direction", aliases = "-") + public void contract( + @Param(description = "The amount to inset by", descriptionKey = "iris.director.commandobject.param.amount_inset_by", defaultValue = "1") + int amount + ) { + VolmitSender commandSender = sender(); + Player player = player(); + + onPlayerThread(player, () -> { + if (!WandSVC.isHoldingWand(player)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND)); + return; + } + + + Location[] b = WandSVC.getCuboid(player); + if (b == null || b[0] == null || b[1] == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED)); + return; + } + Location a1 = b[0].clone(); + Location a2 = b[1].clone(); + Cuboid cursor = new Cuboid(a1, a2); + Direction d = Direction.closest(player.getLocation().getDirection()).reverse(); + assert d != null; + cursor = cursor.expand(d.f(), -amount); + b[0] = cursor.getLowerNE(); + b[1] = cursor.getUpperSW(); + player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); + player.updateInventory(); + commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); + }); + } + + @Director(description = "Set point 1 to look", descriptionKey = "iris.director.commandobject.director.set_point_1_look", aliases = "p1") + public void position1( + @Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look", defaultValue = "true") + boolean here + ) { + VolmitSender commandSender = sender(); + Player player = player(); + + onPlayerThread(player, () -> { + if (!WandSVC.isHoldingWand(player)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND)); + return; + } + + if (WandSVC.isHoldingWand(player)) { + Location[] g = WandSVC.getCuboid(player); + + if (g == null) { + return; + } + if (!here) { + // TODO: WARNING HEIGHT + g[1] = player.getTargetBlock(null, 256).getLocation().clone(); + } else { + g[1] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0); + } + player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1])); + } + }); + } + + @Director(description = "Set point 2 to look", descriptionKey = "iris.director.commandobject.director.set_point_2_look", aliases = "p2") + public void position2( + @Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look_2", defaultValue = "true") + boolean here + ) { + VolmitSender commandSender = sender(); + Player player = player(); + + onPlayerThread(player, () -> { + if (!WandSVC.isHoldingWand(player)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2)); + return; + } + + if (WandSVC.isHoldingIrisWand(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])); + } + }); + } + + @Director(description = "Paste an object", descriptionKey = "iris.director.commandobject.director.paste_object", sync = true) + public void paste( + @Param(description = "The object to paste", descriptionKey = "iris.director.commandobject.param.object_paste", customHandler = ObjectHandler.class) + String object, + @Param(description = "Whether or not to edit the object (need to hold wand)", descriptionKey = "iris.director.commandobject.param.whether_not_edit_object_need_hold_wand", defaultValue = "false") + 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 = "The scale interpolator to use", descriptionKey = "iris.director.commandobject.param.scale_interpolator_use", defaultValue = "none") +// IrisObjectPlacementScaleInterpolator interpolator + ) { + IrisObject o = IrisData.loadAnyObject(object, data()); + double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1); + if (scale > maxScale) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_INDICATED_SCALE_EXCEEDS_MAXIMUM_DOWNSCALED_MAXIMUM, MessageArgument.untrusted("maxScale", maxScale))); + scale = maxScale; + } + + IrisObjectPlacement placement = new IrisObjectPlacement(); + placement.setRotation(IrisObjectRotation.of(0, rotate, 0)); + + VolmitSender commandSender = sender(); + Player player = player(); + ItemStack wand = player.getInventory().getItemInMainHand(); + Block targetBlock = player.getTargetBlock(PASTE_TRANSPARENT_BLOCKS, 256); + if (!isPasteTarget(targetBlock.getType())) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY)); + return; + } + Location block = targetBlock.getLocation().clone().add(0, 1, 0); + + commandSender.playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f); + + Map futureChanges = new HashMap<>(); + + if (scale != 1) { + o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC); + } + + // 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); + J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges)); + + if (!edit) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object))); + return; + } + + onPlayerThread(player, () -> { + ObjectPasteBounds bounds = ObjectPasteBounds.resolve(placed, placement.getRotation(), block.getBlockX(), + block.getBlockY() + placed.getCenter().getBlockY(), block.getBlockZ()); + Location minimum = new Location(block.getWorld(), bounds.minX(), bounds.minY(), bounds.minZ()); + Location maximum = new Location(block.getWorld(), bounds.maxX(), bounds.maxY(), bounds.maxZ()); + ItemStack newWand = WandSVC.createWand(maximum, minimum); + if (WandSVC.isWand(wand)) { + player.getInventory().setItemInMainHand(newWand); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey()))); + } else { + int slot = WandSVC.findWand(player.getInventory()); + if (slot == -1) { + player.getInventory().addItem(newWand); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey()))); + } else { + player.getInventory().setItem(slot, newWand); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", placed.getLoadKey()))); + } + } + }); + })) { + Iris.warn("Could not schedule the object paste at " + block.getBlockX() + ", " + block.getBlockY() + ", " + block.getBlockZ() + "."); + } + } + + /** + * Runs the body on the thread owning the player, reporting when the hop cannot be scheduled. + */ + private void onPlayerThread(Player player, Runnable body) { + if (player == null) { + return; + } + + if (!J.runEntity(player, body)) { + Iris.warn("Could not schedule /iris object on the thread owning " + player.getName() + "."); + } + } + + @Director(description = "Save an object", descriptionKey = "iris.director.commandobject.director.save_object") + public void save( + @Param(description = "The dimension to store the object in", descriptionKey = "iris.director.commandobject.param.dimension_store_object", contextual = true, contextualOverride = true) + IrisDimension dimension, + @Param(description = "The file to store it in, can use / for subfolders", descriptionKey = "iris.director.commandobject.param.file_store_it_can_use_subfolders") + String name, + @Param(description = "Overwrite existing object files", descriptionKey = "iris.director.commandobject.param.overwrite_existing_object_files", defaultValue = "false", aliases = "force") + boolean overwrite, + @Param(description = "Use legacy TileState serialization if possible", descriptionKey = "iris.director.commandobject.param.use_legacy_tilestate_serialization_if_possible", defaultValue = "true") + boolean legacy + ) { + IrisObject o = WandSVC.createSchematic(player(), legacy); + + if (o == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_NEED_HOLD_YOUR_WAND)); + return; + } + + File file = Iris.service(StudioSVC.class).getWorkspaceFile(dimension.getLoadKey(), "objects", name + ".iob"); + + if (file.exists() && !overwrite) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FILE_ALREADY_EXISTS_SET_OVERWRITE_TRUE_OVERWRITE_IT)); + return; + } + try { + o.write(file, sender()); + } catch (IOException e) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT_BECAUSE_IOEXCEPTION, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + Iris.reportError(e); + return; + } + + sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_SUCCESSFULLY_OBJECT_SAVED_OBJECTS, MessageArgument.untrusted("value", dimension.getLoadKey()), MessageArgument.untrusted("name", name))); + } + + @Director(description = "Shift a selection in your looking direction", descriptionKey = "iris.director.commandobject.director.shift_selection_your_looking_direction") + public void shift( + @Param(description = "The amount to shift by", descriptionKey = "iris.director.commandobject.param.amount_shift_by", defaultValue = "1") + int amount + ) { + VolmitSender commandSender = sender(); + Player player = player(); + + onPlayerThread(player, () -> { + if (!WandSVC.isHoldingWand(player)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2)); + return; + } + + Location[] b = WandSVC.getCuboid(player); + if (b == null || b[0] == null || b[1] == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2)); + return; + } + Location a1 = b[0].clone(); + Location a2 = b[1].clone(); + Direction d = Direction.closest(player.getLocation().getDirection()).reverse(); + if (d == null) { + return; // HOW DID THIS HAPPEN + } + a1.add(d.toVector().multiply(amount)); + a2.add(d.toVector().multiply(amount)); + Cuboid cursor = new Cuboid(a1, a2); + b[0] = cursor.getLowerNE(); + b[1] = cursor.getUpperSW(); + player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); + player.updateInventory(); + commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); + }); + } + + @Director(description = "Undo a number of pastes", descriptionKey = "iris.director.commandobject.director.undo_number_pastes", aliases = "u") + public void undo( + @Param(description = "The amount of pastes to undo", descriptionKey = "iris.director.commandobject.param.amount_pastes_undo", defaultValue = "1") + int amount + ) { + ObjectSVC service = Iris.service(ObjectSVC.class); + int actualReverts = Math.min(service.getUndos().size(), amount); + service.revertChanges(actualReverts); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_REVERTED_PASTES, MessageArgument.untrusted("actualReverts", actualReverts))); + } + + @Director(description = "Gets an object wand and grabs the current WorldEdit selection.", descriptionKey = "iris.director.commandobject.director.gets_object_wand_grabs_current_worldedit_selection", aliases = "we", origin = DirectorOrigin.PLAYER) + public void we() { + VolmitSender commandSender = sender(); + Player player = player(); + + if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW)); + return; + } + + Cuboid locs = WorldEditLink.getSelection(player); + + if (locs == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD)); + return; + } + + onPlayerThread(player, () -> { + player.getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW())); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT)); + }); + } + + @Director(description = "Get an object wand", descriptionKey = "iris.director.commandobject.director.get_object_wand", sync = true) + public void wand() { + player().getInventory().addItem(WandSVC.createWand()); + sender().playSound(Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f, 1.5f); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_POOF_GOOD_LUCK_BUILDING)); + } + + @Director(name = "x&y", description = "Autoselect up, down & out", descriptionKey = "iris.director.commandobject.director.autoselect_up_down_out", sync = true) + public void xay() { + if (!WandSVC.isHoldingWand(player())) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_3)); + return; + } + + Location[] b = WandSVC.getCuboid(player()); + if (b == null || b[0] == null || b[1] == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_3)); + return; + } + Location a1 = b[0].clone(); + Location a2 = b[1].clone(); + Location a1x = b[0].clone(); + Location a2x = b[1].clone(); + Cuboid cursor = new Cuboid(a1, a2); + Cuboid cursorx = new Cuboid(a1, a2); + + while (!cursor.containsOnly(Material.AIR)) { + a1.add(new org.bukkit.util.Vector(0, 1, 0)); + a2.add(new org.bukkit.util.Vector(0, 1, 0)); + cursor = new Cuboid(a1, a2); + } + + a1.add(new org.bukkit.util.Vector(0, -1, 0)); + a2.add(new org.bukkit.util.Vector(0, -1, 0)); + + while (!cursorx.containsOnly(Material.AIR)) { + a1x.add(new org.bukkit.util.Vector(0, -1, 0)); + a2x.add(new org.bukkit.util.Vector(0, -1, 0)); + cursorx = new Cuboid(a1x, a2x); + } + + a1x.add(new org.bukkit.util.Vector(0, 1, 0)); + a2x.add(new Vector(0, 1, 0)); + b[0] = a1; + b[1] = a2x; + cursor = new Cuboid(b[0], b[1]); + cursor = cursor.contract(Cuboid.CuboidDirection.North); + cursor = cursor.contract(Cuboid.CuboidDirection.South); + cursor = cursor.contract(Cuboid.CuboidDirection.East); + cursor = cursor.contract(Cuboid.CuboidDirection.West); + b[0] = cursor.getLowerNE(); + b[1] = cursor.getUpperSW(); + player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); + player().updateInventory(); + sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_AUTO_SELECT_COMPLETE)); + } + + @Director(name = "x+y", description = "Autoselect up & out", descriptionKey = "iris.director.commandobject.director.autoselect_up_out", sync = true) + public void xpy() { + if (!WandSVC.isHoldingWand(player())) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_4)); + return; + } + + Location[] b = WandSVC.getCuboid(player()); + if (b == null || b[0] == null || b[1] == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_4)); + return; + } + b[0].add(new Vector(0, 1, 0)); + b[1].add(new Vector(0, 1, 0)); + Location a1 = b[0].clone(); + Location a2 = b[1].clone(); + Cuboid cursor = new Cuboid(a1, a2); + + while (!cursor.containsOnly(Material.AIR)) { + a1.add(new Vector(0, 1, 0)); + a2.add(new Vector(0, 1, 0)); + cursor = new Cuboid(a1, a2); + } + + a1.add(new Vector(0, -1, 0)); + a2.add(new Vector(0, -1, 0)); + b[0] = a1; + a2 = b[1]; + cursor = new Cuboid(a1, a2); + cursor = cursor.contract(Cuboid.CuboidDirection.North); + cursor = cursor.contract(Cuboid.CuboidDirection.South); + cursor = cursor.contract(Cuboid.CuboidDirection.East); + cursor = cursor.contract(Cuboid.CuboidDirection.West); + b[0] = cursor.getLowerNE(); + b[1] = cursor.getUpperSW(); + player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); + player().updateInventory(); + sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_AUTO_SELECT_COMPLETE_2)); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java new file mode 100644 index 000000000..8f8c32b00 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java @@ -0,0 +1,332 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.core.pack.PackResourceCleanup; +import art.arcane.iris.core.pack.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.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.volmlib.util.localization.TextKey; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.BukkitCommandMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.iris.core.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 { + /** + * Director treats a blank defaultValue as "required", which made the all-packs branch + * unreachable from chat. The "*" sentinel keeps the parameter genuinely optional; a blank + * value (the documented "pack=" escape hatch) still means every pack. + */ + static boolean wantsAllPacks(String pack) { + return pack == null || pack.isBlank() || "*".equals(pack.trim()); + } + + @Director(description = "Validate a pack (or all packs) and re-publish results", descriptionKey = "iris.director.commandpack.director.validate_pack_all_packs_re_publish_results", aliases = {"v"}) + public void validate( + @Param(description = "The pack folder name to validate, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name_validate_leave_empty_all", defaultValue = "*") + String pack + ) { + VolmitSender s = sender(); + File packsRoot = Iris.instance.getDataFolder("packs"); + if (!packsRoot.isDirectory()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACKS_FOLDER_NOT_FOUND)); + return; + } + + if (wantsAllPacks(pack)) { + List dirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot); + if (dirs.isEmpty()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_PACKS_VALIDATE)); + return; + } + int broken = 0; + for (File dir : dirs) { + PackValidationResult result = runValidate(s, dir); + if (result != null && !result.isLoadable()) { + broken++; + } + } + persistValidationCache(packsRoot); + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_VALIDATION_COMPLETE_BROKEN_PACKS, MessageArgument.untrusted("broken", String.valueOf(broken)), MessageArgument.untrusted("value", String.valueOf(dirs.size())))); + return; + } + + File target = PackDirectoryResolver.resolveExisting(packsRoot, pack); + if (target == null) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS, MessageArgument.untrusted("pack", String.valueOf(pack)))); + return; + } + runValidate(s, target); + persistValidationCache(packsRoot); + } + + @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") + String pack, + @Param(description = "preview or apply", descriptionKey = "iris.director.commandpack.param.preview_apply", defaultValue = "preview") + String mode + ) { + VolmitSender s = sender(); + File packFolder = findPack(s, pack); + if (packFolder == null) { + return; + } + if ("apply".equalsIgnoreCase(mode)) { + PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder); + if (!result.success()) { + s.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_FAILED, + MessageArgument.untrusted("error", String.valueOf(result.error())) + )); + reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_STILL_QUARANTINED); + return; + } + if (!result.changed()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack)))); + return; + } + PackValidationRegistry.remove(packFolder.getName()); + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath())))); + reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED); + return; + } + if (!"preview".equalsIgnoreCase(mode)) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_MODE_MUST_BE_PREVIEW_APPLY)); + return; + } + PackResourceCleanup.Preview preview = PackResourceCleanup.preview(packFolder); + if (!preview.success()) { + s.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_FAILED, + MessageArgument.untrusted("error", String.valueOf(preview.error())) + )); + return; + } + if (!preview.hasCandidates()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK_2, MessageArgument.untrusted("pack", String.valueOf(pack)))); + return; + } + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_PREVIEW_PACK_CANDIDATE_S_NO_FILES_WERE_CHANGED, MessageArgument.untrusted("pack", String.valueOf(pack)), MessageArgument.untrusted("size", String.valueOf(preview.candidatePaths().size())))); + reportPaths(s, preview.candidatePaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_CANDIDATE); + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RUN_IRIS_PACK_CLEANUP_MODE_APPLY_QUARANTINE_THESE_CANDIDATES_AFTER_FRESH_SCAN, MessageArgument.untrusted("pack", String.valueOf(pack)))); + } + + @Director(description = "Preview or apply restoration of the latest quarantine", descriptionKey = "iris.director.commandpack.director.preview_apply_restoration_latest_quarantine", aliases = {"r"}) + public void restore( + @Param(description = "The pack folder name to restore", descriptionKey = "iris.director.commandpack.param.pack_folder_name_restore") + String pack, + @Param(description = "preview or apply", descriptionKey = "iris.director.commandpack.param.preview_apply_2", defaultValue = "preview") + String mode + ) { + VolmitSender s = sender(); + File packFolder = findPack(s, pack); + if (packFolder == null) { + return; + } + if ("apply".equalsIgnoreCase(mode)) { + PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder); + if (!result.conflicts().isEmpty()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_REFUSED_BECAUSE_DESTINATION_S_ALREADY_EXIST, MessageArgument.untrusted("size", String.valueOf(result.conflicts().size())))); + reportPaths(s, result.conflicts(), BukkitRuntimeMessages.COMMAND_PACK_PATH_CONFLICT); + return; + } + if (!result.success()) { + s.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_PACK_RESTORE_FAILED, + MessageArgument.untrusted("error", String.valueOf(result.error())) + )); + return; + } + if (!result.changed()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack)))); + return; + } + PackValidationRegistry.remove(packFolder.getName()); + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath())))); + reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED); + return; + } + if (!"preview".equalsIgnoreCase(mode)) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_MODE_MUST_BE_PREVIEW_APPLY)); + return; + } + PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(packFolder); + if (!preview.success()) { + s.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_PACK_RESTORE_FAILED, + MessageArgument.untrusted("error", String.valueOf(preview.error())) + )); + return; + } + if (!preview.hasFiles()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK_2, MessageArgument.untrusted("pack", String.valueOf(pack)))); + return; + } + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_PREVIEW_FILE_S_NO_FILES_WERE_CHANGED, MessageArgument.untrusted("dumpPath", String.valueOf(preview.dumpPath())), MessageArgument.untrusted("size", String.valueOf(preview.filePaths().size())))); + reportPaths(s, preview.filePaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_FILE); + if (!preview.conflicts().isEmpty()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_IS_BLOCKED_BY_EXISTING_DESTINATION_S, MessageArgument.untrusted("size", String.valueOf(preview.conflicts().size())))); + reportPaths(s, preview.conflicts(), BukkitRuntimeMessages.COMMAND_PACK_PATH_CONFLICT); + return; + } + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RUN_IRIS_PACK_RESTORE_MODE_APPLY_RESTORE_AFTER_FRESH_CONFLICT_CHECK, MessageArgument.untrusted("pack", String.valueOf(pack)))); + } + + @Director(description = "Show cached validation status for a pack", descriptionKey = "iris.director.commandpack.director.show_cached_validation_status_pack", aliases = {"s"}) + public void status( + @Param(description = "The pack folder name, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name", defaultValue = "*") + String pack + ) { + VolmitSender s = sender(); + if (wantsAllPacks(pack)) { + if (PackValidationRegistry.snapshot().isEmpty()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST)); + return; + } + PackValidationRegistry.snapshot().forEach((name, result) -> { + TextKey key = result.isLoadable() + ? BukkitRuntimeMessages.COMMAND_PACK_STATUS_OK + : BukkitRuntimeMessages.COMMAND_PACK_STATUS_BROKEN; + s.sendMessage(IrisLanguage.text( + key, + MessageArgument.untrusted("pack", name), + MessageArgument.trusted("blocking", result.getBlockingErrors().size()), + MessageArgument.trusted("warnings", result.getWarnings().size()) + )); + }); + return; + } + PackValidationResult result = PackValidationRegistry.get(pack); + if (result == null) { + s.sendMessage(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; + } + reportResult(s, result); + } + + private PackValidationResult runValidate(VolmitSender s, File packFolder) { + try { + PackValidationResult result = PackValidator.validate(packFolder); + PackValidationRegistry.publish(result); + reportResult(s, result); + return result; + } catch (Throwable e) { + Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e); + String detail = e.getMessage() == null || e.getMessage().isBlank() + ? e.getClass().getSimpleName() + : e.getMessage(); + PackValidationResult result = new PackValidationResult( + packFolder.getName(), + List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail), + List.of(), + System.currentTimeMillis()); + PackValidationRegistry.publish(result); + reportResult(s, result); + return result; + } + } + + private void persistValidationCache(File packsRoot) { + List packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot); + List results = new ArrayList<>(packDirectories.size()); + for (File packDirectory : packDirectories) { + PackValidationResult result = PackValidationRegistry.get(packDirectory.getName()); + if (result == null) { + // The boot cache only loads when it covers every pack, so a partial write is + // pointless - but say so instead of silently skipping the persist forever. + Iris.warn("Pack validation cache not written: \"" + packDirectory.getName() + + "\" has no registered result. Run /iris pack validate all to repopulate it."); + return; + } + results.add(result); + } + try { + PackValidationCache.save( + Iris.instance.getDataFile("cache", "pack-validation.json").toPath(), + PackValidationCache.contentFingerprint(packsRoot), + PackValidationCache.contextFingerprint(), + results); + } catch (IOException | RuntimeException e) { + Iris.reportError("Could not persist refreshed pack-validation results", e); + } + } + + private void reportResult(VolmitSender s, PackValidationResult result) { + if (result.isLoadable()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACK_IS_LOADABLE_WARNINGS, MessageArgument.untrusted("packName", String.valueOf(result.getPackName())), MessageArgument.untrusted("size", String.valueOf(result.getWarnings().size())))); + } else { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACK_IS_BROKEN, MessageArgument.untrusted("packName", String.valueOf(result.getPackName())))); + for (String reason : result.getBlockingErrors()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_MESSAGE, MessageArgument.untrusted("reason", String.valueOf(reason)))); + } + } + int wMax = Math.min(10, result.getWarnings().size()); + for (int i = 0; i < wMax; i++) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_MESSAGE_2, MessageArgument.untrusted("value", String.valueOf(result.getWarnings().get(i))))); + } + if (result.getWarnings().size() > wMax) { + s.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_PACK_MORE_WARNING_S, + MessageArgument.trusted("count", result.getWarnings().size() - wMax) + )); + } + } + + private File findPack(VolmitSender sender, String pack) { + if (pack == null || pack.isBlank()) { + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_PACK_YOU_MUST_SPECIFY_PACK_NAME)); + return null; + } + File packFolder = PackDirectoryResolver.resolveExisting(Iris.instance.getDataFolder("packs"), pack); + if (packFolder == null) { + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS, MessageArgument.untrusted("pack", pack))); + return null; + } + return packFolder; + } + + private void reportPaths(VolmitSender sender, List paths, TextKey label) { + int max = Math.min(10, paths.size()); + for (int i = 0; i < max; i++) { + sender.sendMessage(IrisLanguage.text( + BukkitCommandMessages.COMMAND_PACK_MESSAGE, + MessageArgument.untrusted("label", IrisLanguage.text(label)), + MessageArgument.untrusted("value", paths.get(i)) + )); + } + if (paths.size() > max) { + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_PACK_MORE, MessageArgument.untrusted("value", (paths.size() - max)))); + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java new file mode 100644 index 000000000..51e0a654e --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java @@ -0,0 +1,137 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.pregenerator.PregenTask; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.math.Position2; +import org.bukkit.World; +import org.bukkit.util.Vector; + +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.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 { + @Director(description = "Pregenerate a world", descriptionKey = "iris.director.commandpregen.director.pregenerate_world") + public void start( + @Param(description = "The radius of the pregen in blocks", descriptionKey = "iris.director.commandpregen.param.radius_pregen_blocks", aliases = "size") + int radius, + @Param(description = "The world to pregen", descriptionKey = "iris.director.commandpregen.param.world_pregen", contextual = true, contextualOverride = true) + World world, + @Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", descriptionKey = "iris.director.commandpregen.param.center_location_pregen_use_me_your_current_location", defaultValue = "0,0") + Vector center, + @Param(description = "Open the Iris pregen gui", descriptionKey = "iris.director.commandpregen.param.open_iris_pregen_gui", defaultValue = "true") + boolean gui, + @Param(name = "serial", description = "Generate only one chunk at a time", descriptionKey = "iris.director.commandpregen.param.generate_only_one_chunk_at_time", defaultValue = "false") + boolean serial + ) { + if (radius <= 0) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PREGEN_RADIUS_MUST_BE_GREATER_THAN_ZERO_BLOCKS)); + return; + } + if (serial && !IrisToolbelt.supportsStrictSerialPregeneration()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_STRICT_SERIAL_PREGENERATION_REQUIRES_PAPER_PAPER_COMPATIBLE_SERVER)); + return; + } + + 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)); + return; + } + + try { + if (sender().isPlayer() && access() == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_ENGINE_ACCESS_THIS_WORLD_IS_NULL)); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PLEASE_MAKE_SURE_WORLD_IS_LOADED_ENGINE_IS_INITIALIZED_GENERATE)); + } + PregenTask task = PregenTask + .builder() + .center(new Position2(center.getBlockX(), center.getBlockZ())) + .gui(gui) + .radiusX(radius) + .radiusZ(radius) + .build(); + if (serial) { + IrisToolbelt.pregenerateSerial(task, world); + } else { + IrisToolbelt.pregenerate(task, world); + } + String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ(); + sender().sendMessage(msg); + Iris.info(msg); + } catch (Throwable e) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_FAILED_START_PREGENERATION_SEE_CONSOLE_DETAILS)); + Iris.reportError(e); + e.printStackTrace(); + } + } + + @Director(description = "Stop the active pregeneration task", descriptionKey = "iris.director.commandpregen.director.stop_active_pregeneration_task", aliases = "x") + public void stop() { + if (PregeneratorJob.shutdownInstance()) { + String message = C.BLUE + "Pregen stop requested; finishing active work before cancellation."; + sender().sendMessage(message); + Iris.info(message); + } else { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_STOP)); + } + } + + @Director(description = "Pause / continue the active pregeneration task", descriptionKey = "iris.director.commandpregen.director.pause_continue_active_pregeneration_task", aliases = {"resume"}) + public void pause() { + if (PregeneratorJob.pauseResume()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PAUSED_UNPAUSED_PREGENERATION_TASK_NOW, MessageArgument.trusted("value", IrisLanguage.text(PregeneratorJob.isPaused() ? RuntimeUiMessages.STATUS_PAUSED : RuntimeUiMessages.STATUS_RUNNING)))); + } else { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_PAUSE_UNPAUSE)); + } + } + + @Director(description = "Show the active pregeneration status", descriptionKey = "iris.director.commandpregen.director.show_active_pregeneration_status") + public void status() { + PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot(); + if (progress == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASK)); + return; + } + + String world = progress.worldName() == null ? "?" : progress.worldName(); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PREGEN, MessageArgument.untrusted("world", world), MessageArgument.untrusted("value", Form.f(progress.generated())), MessageArgument.untrusted("value2", Form.f(progress.totalChunks())), MessageArgument.untrusted("value3", String.format("%.1f", progress.percent())), MessageArgument.untrusted("value4", (progress.paused() ? C.YELLOW + " PAUSED" : "")))); + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_PREGEN_SPEED_S_ETA_ELAPSED_METHOD, + MessageArgument.untrusted("overall", Form.f(progress.overallChunksPerSecond(), 1)), + MessageArgument.untrusted("tenSecond", Form.f(progress.chunksPerSecond(), 1)), + MessageArgument.untrusted("thirtySecond", Form.f(progress.thirtySecondChunksPerSecond(), 1)), + MessageArgument.untrusted("sixtySecond", Form.f(progress.sixtySecondChunksPerSecond(), 1)), + MessageArgument.untrusted("eta", Form.duration(progress.eta(), 2)), + MessageArgument.untrusted("elapsed", Form.duration(progress.elapsed(), 2)), + MessageArgument.untrusted("method", progress.method()), + MessageArgument.untrusted("failures", progress.failed() > 0 ? C.RED + " Failed: " + Form.f(progress.failed()) : ""))); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java new file mode 100644 index 000000000..36203662f --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java @@ -0,0 +1,392 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +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.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.bukkit.generator.structure.Structure; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +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.volmlib.util.localization.MessageArgument; +import art.arcane.iris.core.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) + public void list( + @Param(description = "The dimension whose pack to index", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_index", aliases = "dim") + IrisDimension dimension + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey()))); + return; + } + File file = StructureIndexService.write(data); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_WROTE_STRUCTURE_INDEX, MessageArgument.untrusted("value", file.getPath()))); + } + + @Director(name = "import", description = "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place.", descriptionKey = "iris.director.commandstructure.director.import_every_structure_vanilla_ingested_datapacks_into_this_pack_as_editable_iris", aliases = {"import-all", "reimport", "imp", "all"}, origin = DirectorOrigin.BOTH) + public void importAll( + @Param(description = "The dimension whose pack to import into", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_import_into", aliases = "dim") + IrisDimension dimension + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_2, MessageArgument.untrusted("value", dimension.getLoadKey()))); + return; + } + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_IMPORTING_ALL_VANILLA_DATAPACK_STRUCTURES_INTO_OVERWRITE, MessageArgument.untrusted("value", dimension.getLoadKey()))); + BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender()); + BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender()); + BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender()); + StructureCaptureImporter.Report captured = StructureCaptureImporter.importStructures( + data, StructureImporter.Mode.OVERWRITE, sender(), jigsaws.captureCandidates()); + int imported = jigsaws.imported() + templates.imported() + groups.imported() + captured.imported(); + int failed = jigsaws.failed() + templates.failed() + groups.failed() + captured.failed(); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_IMPORT_COMPLETE_STRUCTURES_OBJECTS_WRITTEN_FAILED, MessageArgument.untrusted("imported", imported), MessageArgument.untrusted("failed", failed))); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_REFERENCE_THEM_FROM_BIOME_REGION_DIMENSION_STRUCTURES_LIST_RUN_IRIS, MessageArgument.untrusted("value", dimension.getLoadKey()))); + } + + @Director(name = "capture", description = "Capture live registered structures into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. This standalone command overwrites its owned outputs; structures wider/taller than the capture cap and anything that will not generate in a flat overworld are skipped. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. /iris structure import runs a restricted final capture pass only for non-jigsaw structures that have no loadable NBT template.", descriptionKey = "iris.director.commandstructure.director.capture_code_generated_structures_that_have_no_nbt_template_swamp_huts_igloos", aliases = {"cap"}, origin = DirectorOrigin.BOTH) + public void capture( + @Param(description = "The dimension whose pack to capture into", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_capture_into", aliases = "dim") + IrisDimension dimension + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_3, MessageArgument.untrusted("value", dimension.getLoadKey()))); + return; + } + StructureCaptureImporter.Report report = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender()); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_CAPTURED_STRUCTURES_PLACE_THEM_FROM_STRUCTURES_LIST_REGENERATE_CHUNKS_DELETE, MessageArgument.untrusted("value", report.imported()))); + } + + @Director(description = "Verify native structure eligibility and locate Iris-placed structures without running blocking native searches.", descriptionKey = "iris.director.commandstructure.director.verify_native_structure_eligibility_locate_iris_placed_structures_without_running_blocking_native", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true) + public void verify( + @Param(description = "The dimension to verify", descriptionKey = "iris.director.commandstructure.param.dimension_verify", aliases = "dim") + IrisDimension dimension, + @Param(description = "Search radius in chunks around the origin (larger is much slower)", descriptionKey = "iris.director.commandstructure.param.search_radius_chunks_around_origin_larger_is_much_slower", defaultValue = "48") + int radius + ) { + World world = resolveIrisWorld(dimension); + if (world == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_NO_LOADED_IRIS_WORLD_FOUND_JOIN_CREATE_ONE_FIRST_SEARCH, MessageArgument.untrusted("value", dimension.getLoadKey()))); + return; + } + boolean senderIsPlayer = sender() != null && sender().isPlayer(); + Location center = senderIsPlayer && player().getWorld() == world + ? player().getLocation() + : world.getSpawnLocation(); + int searchRadius = Math.max(1, Math.min(radius, 1000)); + PlatformChunkGenerator access = IrisToolbelt.access(world); + Engine engine = access == null ? null : access.getEngine(); + if (engine == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_SELECTED_IRIS_WORLD_HAS_NO_ACTIVE_GENERATOR_ENGINE)); + return; + } + KList structureKeys = new KList<>(); + Registry structureRegistry = Bukkit.getRegistry(Structure.class); + if (structureRegistry != null) { + for (Structure structure : structureRegistry) { + NamespacedKey key = structureRegistry.getKey(structure); + if (key != null) { + structureKeys.add(key.toString()); + } + } + } + for (String placedKey : IrisStructureLocator.placedKeys(engine)) { + if (!structureKeys.contains(placedKey)) { + structureKeys.add(placedKey); + } + } + VolmitSender commandSender = sender(); + Player target = senderIsPlayer ? player() : null; + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STRUCTURE_VERIFYING_STRUCTURES_FROM_WITHIN_CHUNKS, MessageArgument.untrusted("value", world.getName()), MessageArgument.untrusted("value2", center.getBlockX()), MessageArgument.untrusted("value3", center.getBlockZ()), MessageArgument.untrusted("searchRadius", searchRadius))); + int centerX = center.getBlockX(); + int centerZ = center.getBlockZ(); + J.a(() -> runVerification(engine, structureKeys, centerX, centerZ, searchRadius, commandSender, target)); + } + + private void runVerification(Engine engine, KList structureKeys, int centerX, int centerZ, + int searchRadius, VolmitSender commandSender, Player target) { + KList messages = new KList<>(); + Map decisions = new HashMap<>(structureKeys.size()); + boolean requiresNativeReachability = false; + for (String keyName : structureKeys) { + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, keyName, false); + decisions.put(keyName, decision); + requiresNativeReachability |= !IrisStructureLocator.isPlaced(engine, keyName) + && decision.generate(); + } + Set reachable = Set.of(); + if (requiresNativeReachability) { + try { + reachable = StructureReachability.reachableKeys(engine); + } catch (Throwable error) { + messages.add(C.RED + "Structure verification could not resolve native biome reachability."); + sendVerificationMessages(commandSender, target, messages); + Iris.reportError("Could not resolve native structure biome reachability for verification.", error); + return; + } + } + int located = 0; + int nativeEligible = 0; + int disabled = 0; + int unreachable = 0; + int searchLimited = 0; + int errors = 0; + for (String keyName : structureKeys) { + IrisNativeStructureDecision decision = decisions.get(keyName); + if (IrisStructureLocator.isPlaced(engine, keyName)) { + try { + IrisStructureLocator.LocateResult result = + IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius); + if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { + searchLimited++; + messages.add(C.YELLOW + "[iris-search-limit] " + C.WHITE + keyName + C.YELLOW + + ": unable to complete the density search before its safety limit was reached"); + continue; + } + if (!result.found()) { + messages.add(C.YELLOW + "[iris-not-found] " + C.WHITE + keyName); + continue; + } + located++; + messages.add(C.AQUA + "[iris-planned] " + C.WHITE + keyName + C.GREEN + " @ " + + result.originX() + "," + result.baseY() + "," + result.originZ()); + } catch (Throwable error) { + errors++; + messages.add(C.RED + "[error] " + C.WHITE + keyName + C.RED + ": " + + error.getClass().getSimpleName()); + Iris.reportError("Could not verify Iris-placed structure '" + keyName + "'.", error); + } + continue; + } + if (!decision.generate()) { + disabled++; + messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName); + continue; + } + if (!reachable.contains(keyName.toLowerCase(Locale.ROOT))) { + unreachable++; + KList missing = StructureReachability.missingBiomeKeys(engine, keyName); + messages.add(C.YELLOW + "[unreachable] " + C.WHITE + keyName + + (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing))); + continue; + } + nativeEligible++; + messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName); + } + messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placement plans found, " + + C.WHITE + nativeEligible + C.GREEN + " native structures eligible, " + + C.WHITE + disabled + C.GREEN + " disabled by policy, " + + C.WHITE + unreachable + C.GREEN + " biome-unreachable, " + + C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, " + + C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates."); + sendVerificationMessages(commandSender, target, messages); + } + + private void sendVerificationMessages(VolmitSender commandSender, Player target, + KList messages) { + Runnable send = () -> { + for (String message : messages) { + commandSender.sendMessage(message); + } + }; + if (target != null) { + J.runEntity(target, send); + } else { + J.s(send); + } + } + + private World resolveIrisWorld(IrisDimension dimension) { + if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) { + PlatformChunkGenerator playerGenerator = IrisToolbelt.access(player().getWorld()); + if (matchesDimension(playerGenerator, dimension.getLoadKey())) { + return player().getWorld(); + } + } + for (World w : Bukkit.getWorlds()) { + if (!IrisToolbelt.isIrisWorld(w)) { + continue; + } + PlatformChunkGenerator gen = IrisToolbelt.access(w); + if (matchesDimension(gen, dimension.getLoadKey())) { + return w; + } + } + return null; + } + + static boolean matchesDimension(PlatformChunkGenerator generator, String dimensionKey) { + return dimensionKey != null + && generator != null + && generator.getEngine() != null + && generator.getEngine().getDimension() != null + && dimensionKey.equals(generator.getEngine().getDimension().getLoadKey()); + } + + @Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", descriptionKey = "iris.director.commandstructure.director.resolve_iris_structure_s_jigsaw_graph_report_piece_count_bounds", origin = DirectorOrigin.BOTH) + public void info( + @Param(description = "The dimension whose pack holds the structure", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_holds_structure", aliases = "dim") + IrisDimension dimension, + @Param(description = "The iris structure key to inspect", descriptionKey = "iris.director.commandstructure.param.iris_structure_key_inspect") + String structure + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_4, MessageArgument.untrusted("value", dimension.getLoadKey()))); + return; + } + IrisStructure s = data.load(IrisStructure.class, structure, false); + if (s == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK, MessageArgument.untrusted("structure", structure))); + return; + } + StructureAssembler assembler = StructureAssembler.forData( + data, s, new IrisPosition(0, 64, 0)); + StructureAssemblyResult assembly = assembler.assemble(new RNG(1234)); + List pieces = assembly.pieces(); + if (!assembly.hasOutput()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES_CHECK_STARTPOOL, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", s.getStartPool()))); + return; + } + int minX = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (PlacedStructurePiece p : pieces) { + minX = Math.min(minX, p.getMinX()); + minZ = Math.min(minZ, p.getMinZ()); + maxX = Math.max(maxX, p.getMaxX()); + maxZ = Math.max(maxZ, p.getMaxZ()); + } + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_PIECES_FOOTPRINT_X_BLOCKS_SAMPLE_SEED_1234, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", pieces.size()), MessageArgument.untrusted("value2", (maxX - minX + 1)), MessageArgument.untrusted("value3", (maxZ - minZ + 1)))); + } + + @Director(description = "Assemble and place an iris structure at your location (studio testing)", descriptionKey = "iris.director.commandstructure.director.assemble_place_iris_structure_at_your_location_studio_testing", aliases = {"p"}, origin = DirectorOrigin.PLAYER, sync = true) + public void place( + @Param(description = "The dimension whose pack holds the structure", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_holds_structure_2", aliases = "dim") + IrisDimension dimension, + @Param(description = "The iris structure key to place", descriptionKey = "iris.director.commandstructure.param.iris_structure_key_place") + String structure + ) { + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_5, MessageArgument.untrusted("value", dimension.getLoadKey()))); + return; + } + IrisStructure s = data.load(IrisStructure.class, structure, false); + if (s == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK_2, MessageArgument.untrusted("structure", structure))); + return; + } + Location loc = player().getLocation(); + StructureAssembler assembler = StructureAssembler.forData( + data, s, new IrisPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ())); + RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ()); + StructureAssemblyResult assembly = assembler.assemble(rng); + List pieces = assembly.pieces(); + if (!assembly.hasOutput()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES, MessageArgument.untrusted("structure", structure))); + return; + } + Map future = new HashMap<>(); + World targetWorld = player().getWorld(); + PlatformChunkGenerator targetGenerator = IrisToolbelt.access(targetWorld); + Engine targetEngine = targetGenerator == null ? null : targetGenerator.getEngine(); + IObjectPlacer placer = CommandObject.createPlacer(targetWorld, future, targetEngine); + for (PlacedStructurePiece p : pieces) { + IrisObjectPlacement config = new IrisObjectPlacement(); + config.setMode(ObjectPlaceMode.STRUCTURE_PIECE); + config.setRotation(p.getRotation()); + config.getPlace().add(p.getObject().getLoadKey()); + if (!s.getEdit().isEmpty()) { + config.setEdit(s.getEdit()); + } + p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data); + } + int blockChanges = 0; + for (Map.Entry entry : future.entrySet()) { + if (!entry.getKey().getBlockData().equals(entry.getValue())) { + blockChanges++; + } + } + if (blockChanges == 0) { + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS, + MessageArgument.untrusted("structure", structure), + MessageArgument.untrusted("value", pieces.size()))); + return; + } + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION, + MessageArgument.untrusted("structure", structure), + MessageArgument.untrusted("value", pieces.size()), + MessageArgument.untrusted("value2", blockChanges))); + } + +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java new file mode 100644 index 000000000..ca41119b0 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java @@ -0,0 +1,943 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.Iris; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.gui.NoiseExplorerGUI; +import art.arcane.iris.core.gui.VisionGUI; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.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.IrisWorld; +import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.handlers.DimensionHandler; +import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler; +import art.arcane.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.NoiseProvider; +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.math.Spiraler; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.O; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.FluidCollisionMode; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.util.Vector; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.BukkitCommandMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.iris.core.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; + private CommandEdit edit; + //private CommandDeepSearch deepSearch; + + public static String hrf(Duration duration) { + return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase(); + } + + @Director(description = "Open a new studio world", descriptionKey = "iris.director.commandstudio.director.open_new_studio_world", aliases = "o", sync = true) + public void open( + @Param(description = "The dimension pack to open a studio for", descriptionKey = "iris.director.commandstudio.param.dimension_pack_open_studio", aliases = "dim", customHandler = DimensionHandler.class) + IrisDimension dimension, + @Param(defaultValue = "1337", description = "The seed to generate the studio with", descriptionKey = "iris.director.commandstudio.param.seed_generate_studio_with", aliases = "s") + long seed) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED, MessageArgument.untrusted("value", dimension.getName()), MessageArgument.untrusted("seed", seed))); + Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey()); + } + + @Director(description = "Import vanilla trees, mushrooms & objects (and structures/jigsaw) from the server into a pack's objects/vanilla folder", descriptionKey = "iris.director.commandstudio.director.import_vanilla_trees_mushrooms_objects_structures_jigsaw_from_server_into_pack_s", aliases = {"importv", "iv"}, origin = DirectorOrigin.BOTH) + public void importvanilla( + @Param(description = "The dimension pack to import vanilla content into", descriptionKey = "iris.director.commandstudio.param.dimension_pack_import_vanilla_content_into", aliases = {"pack", "dim"}, customHandler = DimensionHandler.class) + IrisDimension dimension, + @Param(description = "How many variants to capture per tree/object feature", descriptionKey = "iris.director.commandstudio.param.how_many_variants_capture_per_tree_object_feature", defaultValue = "3") + int variants, + @Param(description = "Also import vanilla & datapack structures/jigsaw into the pack", descriptionKey = "iris.director.commandstudio.param.also_import_vanilla_datapack_structures_jigsaw_into_pack", defaultValue = "true") + boolean structures + ) { + if (dimension == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PROVIDE_DIMENSION_PACK_IRIS_STD_IMPORTVANILLA_PACK_DIMENSION)); + return; + } + IrisData data = dimension.getLoader(); + if (data == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_COULD_NOT_RESOLVE_PACK_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey()))); + return; + } + + VolmitSender sender = sender(); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_IMPORTING_VANILLA_CONTENT_INTO, MessageArgument.untrusted("value", dimension.getLoadKey()))); + + FeatureImporter.Report features = FeatureImporter.importAllObjectFeatures(data, variants, sender); + int imported = features.imported(); + int failed = features.failed(); + + if (structures) { + BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender); + BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender); + BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender); + imported += jigsaws.imported() + templates.imported() + groups.imported(); + failed += jigsaws.failed() + templates.failed() + groups.failed(); + } + + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_IMPORTVANILLA_COMPLETE_OBJECTS_STRUCTURES_WRITTEN_FAILED, MessageArgument.untrusted("imported", imported), MessageArgument.untrusted("failed", failed))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_TREES_OBJECTS_ARE_UNDER_OBJECTS_VANILLA_REFERENCE_THEM_FROM_BIOME)); + } + + @Director(description = "Open VSCode for a dimension", descriptionKey = "iris.director.commandstudio.director.open_vscode_dimension", aliases = {"vsc"}) + public void vscode( + @Param(defaultValue = "default", description = "The dimension to open VSCode for", descriptionKey = "iris.director.commandstudio.param.dimension_open_vscode", aliases = "dim", customHandler = DimensionHandler.class) + IrisDimension dimension + ) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_VSCODE_PACK, MessageArgument.untrusted("value", dimension.getName()))); + Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey()); + } + + @Director(description = "Close an open studio project", descriptionKey = "iris.director.commandstudio.director.close_open_studio_project", aliases = {"x"}, sync = true) + public void close() { + VolmitSender commandSender = sender(); + if (!Iris.service(StudioSVC.class).isProjectOpen()) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_NO_OPEN_STUDIO_PROJECTS)); + return; + } + + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_CLOSING_STUDIO)); + Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> { + if (throwable != null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSE_FAILED, MessageArgument.untrusted("value", String.valueOf(throwable.getMessage())))); + return; + } + + if (result != null && result.failureCause() != null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSE_FAILED_2, MessageArgument.untrusted("value", String.valueOf(result.failureCause().getMessage())))); + return; + } + + if (result != null && result.startupCleanupQueued()) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSED_REMAINING_WORLD_FAMILY_CLEANUP_WAS_QUEUED_STARTUP_FALLBACK)); + return; + } + + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSED)); + })); + } + + @Director(description = "Toggle your Studio debug scoreboard", descriptionKey = "iris.director.commandstudio.director.toggle_your_studio_debug_scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER) + public void scoreboard() { + Player target = player(); + VolmitSender commandSender = sender(); + if (!J.runEntity(target, () -> { + PlatformChunkGenerator generator = IrisToolbelt.access(target.getWorld()); + if (generator == null || !generator.isStudio()) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD_TOGGLE_DEBUG_SCOREBOARD)); + return; + } + + boolean visible = Iris.service(BoardSVC.class).toggle(target); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_DEBUG_SCOREBOARD, MessageArgument.trusted("value", visible ? C.GREEN : C.YELLOW), MessageArgument.trusted("value2", IrisLanguage.text(visible ? RuntimeUiMessages.STATUS_ENABLED : RuntimeUiMessages.STATUS_DISABLED)))); + })) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_COULD_NOT_UPDATE_STUDIO_DEBUG_SCOREBOARD_RIGHT_NOW)); + } + } + + @Director(description = "Create a new studio project", descriptionKey = "iris.director.commandstudio.director.create_new_studio_project", aliases = "+") + public void create( + @Param(description = "The name of this new Iris Project.", descriptionKey = "iris.director.commandstudio.param.name_this_new_iris_project", defaultValue = "studio") + String name, + @Param( + description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.", descriptionKey = "iris.director.commandstudio.param.copy_contents_existing_project_your_packs_folder_use_it_as_template_this", + defaultValue = "null", + contextual = true, + contextualOverride = true, + customHandler = NullableDimensionHandler.class + ) + IrisDimension template) { + if (template != null) { + Iris.service(StudioSVC.class).create(sender(), name, template); + } else { + Iris.service(StudioSVC.class).create(sender(), name); + } + } + + @Director(description = "Get the version of a pack", descriptionKey = "iris.director.commandstudio.director.get_version_pack") + public void version( + @Param(defaultValue = "default", description = "The dimension get the version of", descriptionKey = "iris.director.commandstudio.param.dimension_get_version", aliases = "dim", contextual = true, contextualOverride = true, customHandler = DimensionHandler.class) + IrisDimension dimension + ) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PACK_HAS_VERSION, MessageArgument.untrusted("value", dimension.getName()), MessageArgument.untrusted("value2", dimension.getVersion()))); + } + + @Director(description = "Open the noise explorer (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_noise_explorer_external_gui", aliases = {"nmap"}) + public void noise( + @Param(description = "Optional pack generator to preview", descriptionKey = "iris.director.commandstudio.param.optional_pack_generator_preview", defaultValue = "null", contextual = true, contextualOverride = true) + IrisGenerator generator, + @Param(description = "The seed to preview the generator with", descriptionKey = "iris.director.commandstudio.param.seed_preview_generator_with", defaultValue = "12345") + long seed + ) { + if (noGUI()) return; + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER)); + + if (generator == null) { + NoiseExplorerGUI.launch(); + return; + } + + Supplier> supplier = () -> (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax()); + NoiseExplorerGUI.launch(supplier, "Custom Generator"); + } + + @Director(description = "Show loot if a chest were right here", descriptionKey = "iris.director.commandstudio.director.show_loot_if_chest_were_right_here", origin = DirectorOrigin.PLAYER, sync = true) + public void loot( + @Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", descriptionKey = "iris.director.commandstudio.param.fast_insertion_items_virtual_inventory_may_cause_performance_drop", defaultValue = "false") + boolean fast, + @Param(description = "Whether or not to append to the inventory currently open (if false, clears opened inventory)", descriptionKey = "iris.director.commandstudio.param.whether_not_append_inventory_currently_open_if_false_clears_opened_inventory", defaultValue = "true") + boolean add + ) { + if (noStudio()) return; + + KList tables = EngineBukkitOps.getLootTables(engine(), RNG.r, player().getLocation().getBlock()); + Inventory inv = Bukkit.createInventory(null, 27 * 2); + + try { + EngineBukkitOps.addItems(engine(), true, inv, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ()); + } catch (Throwable e) { + Iris.reportError(e); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + return; + } + + + O ta = new O<>(); + ta.set(-1); + + var sender = sender(); + var player = player(); + var engine = engine(); + + ta.set(J.sr(() -> + { + if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) { + J.csr(ta.get()); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY)); + return; + } + + if (!add) { + inv.clear(); + } + + EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ()); + }, fast ? 5 : 35)); + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW)); + player().openInventory(inv); + } + + @Director(description = "Calculate the chance for each region to generate", descriptionKey = "iris.director.commandstudio.director.calculate_chance_each_region_generate", origin = DirectorOrigin.PLAYER) + public void regions(@Param(description = "The radius in chunks", descriptionKey = "iris.director.commandstudio.param.radius_chunks", defaultValue = "500") int radius) { + var engine = engine(); + if (engine == null) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD)); + return; + } + if (radius <= 0 || radius > 2048) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REGIONS_RADIUS_OUT_OF_RANGE)); + return; + } + var sender = sender(); + var player = player(); + Thread.ofVirtual() + .start(() -> { + int d = radius * 2; + KMap data = new KMap<>(); + // Everything acquired below is released in the finally: a throw mid-scan + // previously leaked the whole sampler ForkJoinPool, the self-rescheduling + // progress task and both HUD claims for the rest of the server's uptime. + MultiBurst multiBurst = null; + boolean progressShown = false; + int c = -1; + try { + engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0))); + multiBurst = new MultiBurst("Region Sampler"); + var executor = multiBurst.burst(Math.min(radius * radius, 1 << 20)); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_GENERATING_DATA)); + var loc = player.getLocation(); + int totalTasks = d * d; + AtomicInteger completedTasks = new AtomicInteger(0); + progressShown = true; + c = J.ar(() -> { + double jobProgress = (double) completedTasks.get() / totalTasks; + sender.sendProgress(jobProgress, IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS)); + BukkitPlatform.showProgressLane(player, "iris:job", IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS) + " " + Form.pc(jobProgress, 0), jobProgress, 4000L); + }, 0); + new Spiraler(d, d, (x, z) -> executor.queue(() -> { + var region = engine.getRegion((x << 4) + 8, (z << 4) + 8); + data.computeIfAbsent(region.getLoadKey(), (k) -> new AtomicInteger(0)) + .incrementAndGet(); + completedTasks.incrementAndGet(); + })).setOffset(loc.getBlockX(), loc.getBlockZ()).drain(); + executor.complete(); + + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_DONE)); + var loader = engine.getData().getRegionLoader(); + data.forEach((k, v) -> sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_MESSAGE, MessageArgument.untrusted("k", k), MessageArgument.untrusted("value", loader.load(k).getRarity()), MessageArgument.untrusted("value2", Form.f((double) v.get() / totalTasks * 100, 2))))); + } catch (Throwable e) { + Iris.reportError(e); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REGIONS_SCAN_FAILED, + MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + } finally { + if (c != -1) { + J.car(c); + } + if (progressShown) { + sender.sendAction(" "); + BukkitPlatform.hudLanes().hide(player, "iris:job"); + } + if (multiBurst != null) { + multiBurst.close(); + } + } + }); + } + + @Director(description = "Render a world map (External GUI)", descriptionKey = "iris.director.commandstudio.director.render_world_map_external_gui", aliases = "render") + public void map( + @Param(name = "world", description = "The world to open the generator for", descriptionKey = "iris.director.commandstudio.param.world_open_generator", contextual = true, contextualOverride = true) + World world + ) { + if (noGUI()) return; + + if (!IrisToolbelt.isIrisWorld(world)) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD)); + return; + } + + VisionGUI.launch(IrisToolbelt.access(world).getEngine()); + 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) + IrisDimension dimension + ) { + // Todo: Make this more accurate + File pack = dimension.getLoadFile().getParentFile().getParentFile(); + File report = Iris.instance.getDataFile("profile.txt"); + IrisProject project = new IrisProject(pack); + IrisData data = IrisData.get(pack); + PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension); + Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine(); + + if (activeEngine != null) { + IrisToolbelt.applyPregenPerformanceProfile(activeEngine); + } else { + IrisToolbelt.applyPregenPerformanceProfile(); + } + + KList fileText = new KList<>(); + + KMap styleTimings = new KMap<>(); + KMap interpolatorTimings = new KMap<>(); + KMap generatorTimings = new KMap<>(); + KMap biomeTimings = new KMap<>(); + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CALCULATING_PERFORMANCE_METRICS_NOISE_GENERATORS)); + + for (NoiseStyle i : NoiseStyle.values()) { + CNG c = i.create(new RNG(i.hashCode())); + + for (int j = 0; j < 3000; j++) { + c.noise(j, j + 1000, j * j); + c.noise(j, -j); + } + + PrecisionStopwatch px = PrecisionStopwatch.start(); + + for (int j = 0; j < 100000; j++) { + c.noise(j, j + 1000, j * j); + c.noise(j, -j); + } + + styleTimings.put(i, px.getMilliseconds()); + } + + fileText.add("Noise Style Performance Impacts: "); + + for (NoiseStyle i : styleTimings.sortKNumber()) { + fileText.add(i.name() + ": " + styleTimings.get(i)); + } + + fileText.add(""); + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CALCULATING_INTERPOLATOR_TIMINGS)); + + for (InterpolationMethod i : InterpolationMethod.values()) { + IrisInterpolator in = new IrisInterpolator(); + in.setFunction(i); + in.setHorizontalScale(8); + + NoiseProvider np = (x, z) -> Math.random(); + + for (int j = 0; j < 3000; j++) { + in.interpolate(j, -j, np); + } + + PrecisionStopwatch px = PrecisionStopwatch.start(); + + for (int j = 0; j < 100000; j++) { + in.interpolate(j + 10000, -j - 100000, np); + } + + interpolatorTimings.put(i, px.getMilliseconds()); + } + + fileText.add("Noise Interpolator Performance Impacts: "); + + for (InterpolationMethod i : interpolatorTimings.sortKNumber()) { + fileText.add(i.name() + ": " + interpolatorTimings.get(i)); + } + + fileText.add(""); + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PROCESSING_GENERATOR_SCORES)); + + KMap> btx = new KMap<>(); + + for (String i : data.getGeneratorLoader().getPossibleKeys()) { + KList vv = new KList<>(); + IrisGenerator g = data.getGeneratorLoader().load(i); + KList composites = g.getAllComposites(); + double score = 0; + int m = 0; + for (IrisNoiseGenerator j : composites) { + m++; + score += styleTimings.get(j.getStyle().getStyle()); + vv.add("Composite Noise Style " + m + " " + j.getStyle().getStyle().name() + ": " + styleTimings.get(j.getStyle().getStyle())); + } + + score += interpolatorTimings.get(g.getInterpolator().getFunction()); + vv.add("Interpolator " + g.getInterpolator().getFunction().name() + ": " + interpolatorTimings.get(g.getInterpolator().getFunction())); + generatorTimings.put(i, score); + btx.put(i, vv); + } + + fileText.add("Project Generator Performance Impacts: "); + + for (String i : generatorTimings.sortKNumber()) { + fileText.add(i + ": " + generatorTimings.get(i)); + + btx.get(i).forEach((ii) -> fileText.add(" " + ii)); + } + + fileText.add(""); + + KMap> bt = new KMap<>(); + + for (String i : data.getBiomeLoader().getPossibleKeys()) { + KList vv = new KList<>(); + IrisBiome b = data.getBiomeLoader().load(i); + double score = 0; + + int m = 0; + for (IrisBiomePaletteLayer j : b.getLayers()) { + m++; + score += styleTimings.get(j.getStyle().getStyle()); + vv.add("Palette Layer " + m + ": " + styleTimings.get(j.getStyle().getStyle())); + } + + score += styleTimings.get(b.getBiomeStyle().getStyle()); + vv.add("Biome Style: " + styleTimings.get(b.getBiomeStyle().getStyle())); + score += styleTimings.get(b.getChildStyle().getStyle()); + vv.add("Child Style: " + styleTimings.get(b.getChildStyle().getStyle())); + biomeTimings.put(i, score); + bt.put(i, vv); + } + + fileText.add("Project Biome Performance Impacts: "); + + for (String i : biomeTimings.sortKNumber()) { + fileText.add(i + ": " + biomeTimings.get(i)); + + bt.get(i).forEach((ff) -> fileText.add(" " + ff)); + } + + fileText.add(""); + + double m = 0; + for (double i : biomeTimings.v()) { + m += i; + } + m /= biomeTimings.size(); + double mm = 0; + for (double i : generatorTimings.v()) { + mm += i; + } + mm /= generatorTimings.size(); + m += mm; + + fileText.add("Average Score: " + m); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SCORE, MessageArgument.untrusted("value", Form.duration(m, 0)))); + + try { + IO.writeAll(report, fileText.toString("\n")); + } catch (IOException e) { + Iris.reportError(e); + e.printStackTrace(); + } + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DONE, MessageArgument.untrusted("value", report.getPath()))); + } + + private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) { + StudioSVC studioService = Iris.service(StudioSVC.class); + if (studioService != null && studioService.isProjectOpen()) { + IrisProject activeProject = studioService.getActiveProject(); + if (activeProject != null) { + PlatformChunkGenerator activeProvider = activeProject.getActiveProvider(); + if (isGeneratorDimension(activeProvider, dimension)) { + return activeProvider; + } + } + } + + if (!sender().isPlayer()) { + return null; + } + + Player player = sender().player(); + if (player == null) { + return null; + } + + PlatformChunkGenerator worldAccess = IrisToolbelt.access(player.getWorld()); + if (isGeneratorDimension(worldAccess, dimension)) { + return worldAccess; + } + + return null; + } + + private boolean isGeneratorDimension(PlatformChunkGenerator generator, IrisDimension dimension) { + if (generator == null || generator.getEngine() == null || dimension == null || dimension.getLoadKey() == null) { + return false; + } + + IrisDimension engineDimension = generator.getEngine().getDimension(); + if (engineDimension == null || engineDimension.getLoadKey() == null) { + return false; + } + + return engineDimension.getLoadKey().equalsIgnoreCase(dimension.getLoadKey()); + } + + @Director(description = "Spawn an Iris entity", descriptionKey = "iris.director.commandstudio.director.spawn_iris_entity", aliases = "summon", origin = DirectorOrigin.PLAYER) + public void spawn( + @Param(description = "The entity to spawn", descriptionKey = "iris.director.commandstudio.param.entity_spawn") + IrisEntity entity, + @Param(description = "The location to spawn the entity at", descriptionKey = "iris.director.commandstudio.param.location_spawn_entity_at", contextual = true, contextualOverride = true) + Vector location + ) { + VolmitSender commandSender = sender(); + Engine spawnEngine = engine(); + + if (!IrisToolbelt.isIrisWorld(player().getWorld())) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN)); + } + + // Entity creation must run on the thread owning the destination chunk. + Location at = new Location(world(), location.getX(), location.getY(), location.getZ()); + if (!J.runAt(at, () -> entity.spawn(spawnEngine, at))) { + Iris.warn("Could not schedule the entity spawn at " + at.getBlockX() + ", " + at.getBlockY() + ", " + at.getBlockZ() + "."); + } + } + + @Director(description = "Teleport to the active studio world", descriptionKey = "iris.director.commandstudio.director.teleport_active_studio_world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true) + public void tpstudio() { + if (!Iris.service(StudioSVC.class).isProjectOpen()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN)); + return; + } + + if (IrisToolbelt.isIrisWorld(world()) && engine().isStudio()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_ARE_ALREADY_STUDIO_WORLD)); + return; + } + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SENDING_YOU_STUDIO_WORLD)); + Player player = player(); + IrisWorld studioWorld = Iris.service(StudioSVC.class) + .getActiveProject() + .getActiveProvider() + .getTarget() + .getWorld(); + BukkitPlatform.teleportAsync(player, BukkitWorldBinding.spawnLocation(studioWorld)) + .thenRun(() -> player.setGameMode(GameMode.CREATIVE)); + } + + @Director(description = "Update your dimension projects VSCode workspace", descriptionKey = "iris.director.commandstudio.director.update_your_dimension_projects_vscode_workspace") + public void update( + @Param(description = "The dimension to update the workspace of", descriptionKey = "iris.director.commandstudio.param.dimension_update_workspace", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class) + IrisDimension dimension + ) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATING_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName()))); + if (new IrisCodeWorkspace(new IrisProject(dimension.getLoader().getDataFolder())).updateWorkspace()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATED_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName()))); + } else { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_INVALID_PROJECT_TRY_DELETING_CODE_WORKSPACE_FILE_TRY_AGAIN, MessageArgument.untrusted("value", dimension.getName()))); + } + } + + @Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks", descriptionKey = "iris.director.commandstudio.director.capture_igendata_chunk_report_nearby_chunks", origin = DirectorOrigin.PLAYER) + public void objects() { + if (!IrisToolbelt.isIrisWorld(player().getWorld())) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD)); + return; + } + + World world = player().getWorld(); + + if (!IrisToolbelt.isIrisWorld(world)) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD_2)); + return; + } + KList chunks = new KList<>(); + Player reporter = player(); + CountDownLatch gathered = new CountDownLatch(1); + + // The raycast and the chunk loads need the thread owning the player; the report itself stays off it. + boolean scheduled = J.runEntity(reporter, () -> { + try { + int bx = reporter.getLocation().getChunk().getX(); + int bz = reporter.getLocation().getChunk().getZ(); + + try { + Location l = reporter.getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation(); + + int cx = l.getChunk().getX(); + int cz = l.getChunk().getZ(); + new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain(); + } catch (Throwable e) { + Iris.reportError(e); + } + + new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain(); + } finally { + gathered.countDown(); + } + }); + + if (!scheduled) { + Iris.warn("Could not schedule the chunk report scan on the thread owning " + reporter.getName() + "."); + return; + } + + try { + if (!gathered.await(CHUNK_SCAN_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + Iris.warn("Timed out waiting for the chunk report scan of " + reporter.getName() + "."); + return; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, MessageArgument.untrusted("value", chunks.size()))); + File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt"); + try (PrintWriter pw = new PrintWriter(ff)) { + pw.println("=== Iris Chunk Report ==="); + pw.println("== General Info =="); + pw.println("Iris Version: " + Iris.instance.getDescription().getVersion()); + pw.println("Bukkit Version: " + Bukkit.getBukkitVersion()); + pw.println("MC Version: " + Bukkit.getVersion()); + pw.println("PaperSpigot: " + (BukkitPlatform.isPaperServer() ? "Yup!" : "Nope!")); + pw.println("Report Captured At: " + new Date()); + pw.println("Chunks: (" + chunks.size() + "): "); + + for (Chunk i : chunks) { + pw.println("- [" + i.getX() + ", " + i.getZ() + "]"); + } + + int regions = 0; + long size = 0; + String age = "No idea..."; + + try { + for (File i : Objects.requireNonNull(new File(world.getWorldFolder(), "region").listFiles())) { + if (i.isFile()) { + size += i.length(); + } + } + } catch (Throwable e) { + Iris.reportError(e); + } + + try { + FileTime creationTime = (FileTime) Files.getAttribute(world.getWorldFolder().toPath(), "creationTime"); + age = hrf(Duration.of(M.ms() - creationTime.toMillis(), ChronoUnit.MILLIS)); + } catch (IOException e) { + Iris.reportError(e); + } + + KList biomes = new KList<>(); + KList caveBiomes = new KList<>(); + KMap>> objects = new KMap<>(); + + for (Chunk i : chunks) { + for (int j = 0; j < 16; j += 3) { + + for (int k = 0; k < 16; k += 3) { + + assert engine() != null; + IrisBiome bb = engine().getSurfaceBiome((i.getX() * 16) + j, (i.getZ() * 16) + k); + IrisBiome bxf = engine().getCaveBiome((i.getX() * 16) + j, (i.getZ() * 16) + k); + biomes.addIfMissing(bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")"); + caveBiomes.addIfMissing(bxf.getName() + " (" + bxf.getLoadFile().getName() + ")"); + exportObjects(bb, pw, engine(), objects); + exportObjects(bxf, pw, engine(), objects); + } + } + } + + String[] regionFiles = new File(world.getWorldFolder(), "region").list(); + regions = regionFiles == null ? 0 : regionFiles.length; + + pw.println(); + pw.println("== World Info =="); + pw.println("World Name: " + world.getName()); + pw.println("Age: " + age); + pw.println("Folder: " + world.getWorldFolder().getPath()); + pw.println("Regions: " + Form.f(regions)); + pw.println("Chunks: max. " + Form.f(regions * 32 * 32)); + pw.println("World Size: min. " + Form.fileSize(size)); + pw.println(); + pw.println("== Biome Info =="); + pw.println("Found " + biomes.size() + " Biome(s): "); + + for (String i : biomes) { + pw.println("- " + i); + } + pw.println(); + + pw.println("== Object Info =="); + + for (String i : objects.k()) { + pw.println("- " + i); + + for (String j : objects.get(i).k()) { + pw.println(" @ " + j); + + for (String k : objects.get(i).get(j)) { + pw.println(" * " + k); + } + } + } + + pw.println(); + + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REPORTED, MessageArgument.untrusted("value", ff.getPath()))); + } catch (Throwable e) { + e.printStackTrace(); + Iris.reportError(e); + } + } + + private void exportObjects(IrisBiome bb, PrintWriter pw, Engine g, KMap>> objects) { + String n1 = bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")"; + int m = 0; + KSet stop = new KSet<>(); + for (IrisObjectPlacement f : bb.getObjects()) { + m++; + String n2 = "Placement #" + m + " (" + f.getPlace().size() + " possible objects)"; + + for (String i : f.getPlace()) { + String nn3 = i + ": [ERROR] Failed to find object!"; + + try { + if (stop.contains(i)) { + continue; + } + + File ff = g.getData().getObjectLoader().findFile(i); + art.arcane.iris.util.common.math.IrisBlockVector sz = IrisObject.sampleSize(ff); + nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]"; + stop.add(i); + } catch (Throwable e) { + Iris.reportError(e); + } + + String n3 = nn3; + objects.computeIfAbsent(n1, (k1) -> new KMap<>()) + .computeIfAbsent(n2, (k) -> new KList<>()).addIfMissing(n3); + } + } + } + + /** + * @return true if server GUIs are not enabled + */ + private boolean noGUI() { + if (!IrisSettings.get().getGui().isUseServerLaunchedGuis()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS)); + return true; + } + return false; + } + + /** + * @return true if no studio is open or the player is not in one + */ + private boolean noStudio() { + if (!sender().isPlayer()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PLAYERS_ONLY)); + return true; + } + if (!Iris.service(StudioSVC.class).isProjectOpen()) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN_2)); + return true; + } + if (!IrisToolbelt.isStudio(world())) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD)); + return true; + } + return false; + } + + + public void files(File clean, KList files) { + if (clean.isDirectory()) { + for (File i : clean.listFiles()) { + files(i, files); + } + } else if (clean.getName().endsWith(".json")) { + try { + files.add(clean); + } catch (Throwable e) { + Iris.reportError(e); + Iris.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!"); + } + } + } + + private void fixBlocks(JSONObject obj) { + for (String i : obj.keySet()) { + Object o = obj.get(i); + + if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) { + obj.put(i, "minecraft:" + o); + } + + if (o instanceof JSONObject) { + fixBlocks((JSONObject) o); + } else if (o instanceof JSONArray) { + fixBlocks((JSONArray) o); + } + } + } + + private void fixBlocks(JSONArray obj) { + for (int i = 0; i < obj.length(); i++) { + Object o = obj.get(i); + + if (o instanceof JSONObject) { + fixBlocks((JSONObject) o); + } else if (o instanceof JSONArray) { + fixBlocks((JSONArray) o); + } + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java new file mode 100644 index 000000000..73f7f0acd --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java @@ -0,0 +1,290 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.commands; + +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.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.matter.MatterMarker; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.FluidCollisionMode; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +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.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 { + @Director(description = "What is in my hand?", descriptionKey = "iris.director.commandwhat.director.what_is_my_hand", origin = DirectorOrigin.PLAYER) + public void hand() { + try { + BlockData bd = player().getInventory().getItemInMainHand().getType().createBlockData(); + if (!bd.getMaterial().equals(Material.AIR)) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL, MessageArgument.untrusted("value", bd.getMaterial().name()))); + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL, MessageArgument.untrusted("value", bd.getAsString(true)))); + } else { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM)); + } + } catch (Throwable e) { + Iris.reportError(e); + Material bd = player().getInventory().getItemInMainHand().getType(); + if (!bd.equals(Material.AIR)) { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_2, MessageArgument.untrusted("value", bd.name()))); + } else { + sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM_2)); + } + } + } + + @Director(description = "What biome am i in?", descriptionKey = "iris.director.commandwhat.director.what_biome_am_i", origin = DirectorOrigin.PLAYER) + public void biome() { + try { + IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().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()))); + + } catch (Throwable e) { + Iris.reportError(e); + Biome biome = player().getLocation().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))); + + 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()))))); + } catch (Throwable ee) { + Iris.reportError(ee); + } + } + } + } + + @Director(description = "What region am i in?", descriptionKey = "iris.director.commandwhat.director.what_region_am_i", origin = DirectorOrigin.PLAYER) + public void region() { + VolmitSender commandSender = sender(); + Player player = player(); + World world = world(); + Engine engine = engine(); + + // Chunk access must happen on the thread owning the player's chunk. + onPlayerThread(player, () -> { + try { + Chunk chunk = world.getChunkAt(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4); + 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 (Throwable e) { + Iris.reportError(e); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY)); + } + }); + } + + @Director(description = "What block am i looking at?", descriptionKey = "iris.director.commandwhat.director.what_block_am_i_looking_at", origin = DirectorOrigin.PLAYER) + public void block() { + VolmitSender commandSender = sender(); + Player player = player(); + + // The raycast reads blocks, so it has to run on the thread owning the player. + onPlayerThread(player, () -> { + BlockData bd; + try { + bd = player.getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData(); + } catch (NullPointerException e) { + Iris.reportError(e); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY)); + bd = null; + } + + if (bd != null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name()))); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true)))); + + if (BukkitBlockResolution.isStorage(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE)); + } + + if (BukkitBlockResolution.isLit(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE)); + } + + if (BukkitBlockResolution.isFoliage(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK)); + } + + if (BukkitBlockResolution.isDecorant(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK)); + } + + if (BukkitBlockResolution.isFluid(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK)); + } + + if (BukkitBlockResolution.isFoliagePlantable(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK)); + } + + if (BukkitBlockResolution.isSolid(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK)); + } + } + }); + } + + // Matches ModdedWhatCommands.MAX_MARKERS so both loaders truncate identically. + private static final int MAX_MARKER_HITS = 8_192; + private static final int MARKER_SIGNAL_BATCH = 128; + + @Director(description = "Show markers in chunk", descriptionKey = "iris.director.commandwhat.director.show_markers_chunk", origin = DirectorOrigin.PLAYER) + public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling", descriptionKey = "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling") String marker) { + VolmitSender commandSender = sender(); + Player player = player(); + if (player == null) { + return; + } + + // Only the chunk coordinates need the owning thread; the 81-chunk mantle sweep loads + // tectonic plates from disk and must never run on the tick thread (the modded adapter + // already scans async, leased and capped — mirror it). + onPlayerThread(player, () -> { + World world = player.getWorld(); + Chunk c = player.getLocation().getChunk(); + int chunkX = c.getX(); + int chunkZ = c.getZ(); + + if (!IrisToolbelt.isIrisWorld(world)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2)); + return; + } + Engine engine = IrisToolbelt.access(world).getEngine(); + + J.a(() -> { + KList hits = new KList<>(); + MatterMarker matterMarker = new MatterMarker(marker); + try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_what_markers"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + scan: + for (int xxx = chunkX - 4; xxx <= chunkX + 4; xxx++) { + for (int zzz = chunkZ - 4; zzz <= chunkZ + 4; zzz++) { + for (Location i : engine.getMantle().findMarkers(xxx, zzz, matterMarker) + .convert((p) -> BukkitPlatform.toLocation(p, world))) { + hits.add(i); + if (hits.size() >= MAX_MARKER_HITS) { + break scan; + } + } + } + } + } catch (GenerationSessionException e) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2)); + return; + } + + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", hits.size()), MessageArgument.untrusted("marker", marker))); + emitMarkerSignals(player, engine, hits, 0); + }); + }); + } + + private void emitMarkerSignals(Player player, Engine engine, KList hits, int from) { + if (from >= hits.size() || !player.isOnline() || engine.isClosed() || engine.isClosing()) { + return; + } + int to = Math.min(from + MARKER_SIGNAL_BATCH, hits.size()); + for (int i = from; i < to; i++) { + Location at = hits.get(i); + BlockSignal.of(at.getWorld(), at.getBlockX(), at.getBlockY(), at.getBlockZ(), 100); + } + J.s(() -> emitMarkerSignals(player, engine, hits, to), 1); + } + + /** + * Runs the body on the thread owning the player, reporting when the hop cannot be scheduled. + */ + private void onPlayerThread(Player player, Runnable body) { + if (player == null) { + return; + } + + if (!J.runEntity(player, body)) { + Iris.warn("Could not schedule /iris what on the thread owning " + player.getName() + "."); + } + } + + private NamespacedKey resolveBiomeKey(Biome biome) { + Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull"); + if (keyOrNullValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow"); + if (keyOrThrowValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + Object keyValue = invokeNoThrow(biome, "getKey"); + if (keyValue instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + + return null; + } + + private Object invokeNoThrow(Biome biome, String methodName) { + if (biome == null) { + return null; + } + + try { + Method method = biome.getClass().getMethod(methodName); + return method.invoke(biome); + } catch (Throwable ignored) { + return null; + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/ObjectPasteBounds.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/ObjectPasteBounds.java new file mode 100644 index 000000000..b03e75cdf --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/ObjectPasteBounds.java @@ -0,0 +1,57 @@ +package art.arcane.iris.core.commands; + +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.util.common.math.IrisBlockVector; + +import java.util.Objects; + +record ObjectPasteBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + static ObjectPasteBounds resolve( + IrisObject object, + IrisObjectRotation rotation, + int anchorX, + int anchorY, + int anchorZ + ) { + IrisObject placedObject = Objects.requireNonNull(object, "object"); + IrisObjectRotation placementRotation = Objects.requireNonNull(rotation, "rotation"); + int[] xOffsets = { + -placedObject.getCenter().getBlockX(), + placedObject.getW() - 1 - placedObject.getCenter().getBlockX() + }; + int[] yOffsets = { + -placedObject.getCenter().getBlockY(), + placedObject.getH() - 1 - placedObject.getCenter().getBlockY() + }; + int[] zOffsets = { + -placedObject.getCenter().getBlockZ(), + placedObject.getD() - 1 - placedObject.getCenter().getBlockZ() + }; + int minimumX = Integer.MAX_VALUE; + int minimumY = Integer.MAX_VALUE; + int minimumZ = Integer.MAX_VALUE; + int maximumX = Integer.MIN_VALUE; + int maximumY = Integer.MIN_VALUE; + int maximumZ = Integer.MIN_VALUE; + + for (int xOffset : xOffsets) { + for (int yOffset : yOffsets) { + for (int zOffset : zOffsets) { + IrisBlockVector transformed = placementRotation.rotate(new IrisBlockVector(xOffset, yOffset, zOffset)); + int worldX = anchorX + (int) Math.round(transformed.getX()); + int worldY = anchorY + (int) Math.round(transformed.getY()); + int worldZ = anchorZ + (int) Math.round(transformed.getZ()); + minimumX = Math.min(minimumX, worldX); + minimumY = Math.min(minimumY, worldY); + minimumZ = Math.min(minimumZ, worldZ); + maximumX = Math.max(maximumX, worldX); + maximumY = Math.max(maximumY, worldY); + maximumZ = Math.max(maximumZ, worldZ); + } + } + } + + return new ObjectPasteBounds(minimumX, minimumY, minimumZ, maximumX, maximumY, maximumZ); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java new file mode 100644 index 000000000..a08e7ed1b --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java @@ -0,0 +1,56 @@ +package art.arcane.iris.core.commands; + +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; +import org.bukkit.command.CommandSender; + +import java.util.Collection; +import java.util.List; + +/** + * Paper-only command registration. Lives outside {@code art.arcane.iris.core.service} 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). + */ +public final class PaperCommandRegistrar { + private static final String ROOT_COMMAND = "iris"; + + private PaperCommandRegistrar() { + } + + public static void register(Iris plugin, CommandSVC commandService) { + plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> + event.registrar().register( + ROOT_COMMAND, + "Iris world generation command.", + List.of("ir", "irs"), + command(commandService) + ) + ); + } + + static BasicCommand command(CommandSVC commandService) { + return new PaperCommand(commandService); + } + + private record PaperCommand(CommandSVC commandService) implements BasicCommand { + @Override + public void execute(CommandSourceStack source, String[] args) { + commandService.executeRoot(source.getSender(), ROOT_COMMAND, args); + } + + @Override + public Collection suggest(CommandSourceStack source, String[] args) { + return commandService.tabCompleteRoot(source.getSender(), ROOT_COMMAND, args); + } + + @Override + public boolean canUse(CommandSender sender) { + return true; + } + } +} diff --git a/core/src/main/java/com/volmit/iris/core/edit/BlockEditor.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BlockEditor.java similarity index 97% rename from core/src/main/java/com/volmit/iris/core/edit/BlockEditor.java rename to adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BlockEditor.java index 0df8f32af..fcd952048 100644 --- a/core/src/main/java/com/volmit/iris/core/edit/BlockEditor.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BlockEditor.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.edit; +package art.arcane.iris.core.edit; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; diff --git a/core/src/main/java/com/volmit/iris/core/edit/BukkitBlockEditor.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BukkitBlockEditor.java similarity index 75% rename from core/src/main/java/com/volmit/iris/core/edit/BukkitBlockEditor.java rename to adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BukkitBlockEditor.java index 9ef3ce600..e0a7510a5 100644 --- a/core/src/main/java/com/volmit/iris/core/edit/BukkitBlockEditor.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BukkitBlockEditor.java @@ -16,28 +16,31 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.edit; +package art.arcane.iris.core.edit; -import com.volmit.iris.util.math.M; +import art.arcane.volmlib.util.math.M; import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; -@SuppressWarnings("ClassCanBeRecord") public class BukkitBlockEditor implements BlockEditor { private final World world; + private volatile long last; public BukkitBlockEditor(World world) { this.world = world; + this.last = M.ms(); } @Override public void set(int x, int y, int z, BlockData d) { + touch(); world.getBlockAt(x, y, z).setBlockData(d, false); } @Override public BlockData get(int x, int y, int z) { + touch(); return world.getBlockAt(x, y, z).getBlockData(); } @@ -48,28 +51,38 @@ public class BukkitBlockEditor implements BlockEditor { @Override public long last() { - return M.ms(); + return last; } - @SuppressWarnings("deprecation") @Override public void setBiome(int x, int z, Biome b) { - world.setBiome(x, z, b); + touch(); + int minHeight = world.getMinHeight(); + int maxHeight = world.getMaxHeight(); + for (int y = minHeight; y < maxHeight; y++) { + world.setBiome(x, y, z, b); + } } @Override public void setBiome(int x, int y, int z, Biome b) { + touch(); world.setBiome(x, y, z, b); } @Override public Biome getBiome(int x, int y, int z) { + touch(); return world.getBiome(x, y, z); } - @SuppressWarnings("deprecation") @Override public Biome getBiome(int x, int z) { - return world.getBiome(x, z); + touch(); + return world.getBiome(x, world.getMinHeight(), z); + } + + private void touch() { + last = M.ms(); } } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitGuiHost.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitGuiHost.java new file mode 100644 index 000000000..8eb3b800a --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitGuiHost.java @@ -0,0 +1,91 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.events.IrisEngineHotloadEvent; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class BukkitGuiHost implements GuiHost.Provider { + private final Map hotloadHooks = new ConcurrentHashMap<>(); + + public static void install() { + GuiHost.set(new BukkitGuiHost()); + } + + @Override + public Engine findActiveEngine() { + try { + for (World world : new ArrayList<>(Bukkit.getWorlds())) { + try { + PlatformChunkGenerator access = IrisToolbelt.access(world); + if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) { + return access.getEngine(); + } + } catch (Throwable ignored) { + } + } + } catch (Throwable ignored) { + } + return null; + } + + @Override + public void registerHotloadHook(Runnable onHotload) { + Listener listener = new HotloadListener(onHotload); + hotloadHooks.put(onHotload, listener); + Iris.instance.registerListener(listener); + } + + @Override + public void unregisterHotloadHook(Runnable onHotload) { + Listener listener = hotloadHooks.remove(onHotload); + if (listener != null) { + Iris.instance.unregisterListener(listener); + } + } + + @Override + public GuiOverlay overlayFor(Engine engine) { + return engine == null ? null : new BukkitVisionOverlay(engine); + } + + private static final class HotloadListener implements Listener { + private final Runnable onHotload; + + private HotloadListener(Runnable onHotload) { + this.onHotload = onHotload; + } + + @EventHandler + public void on(IrisEngineHotloadEvent event) { + onHotload.run(); + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java new file mode 100644 index 000000000..6202bb0b6 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java @@ -0,0 +1,182 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.format.Form; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH; + +public final class BukkitVisionOverlay implements GuiOverlay { + private final Engine engine; + private final AtomicBoolean playerRefreshQueued = new AtomicBoolean(); + private volatile List playerMarkers = List.of(); + + public BukkitVisionOverlay(Engine engine) { + this.engine = engine; + } + + /** + * Called from the AWT event thread, so it may only hand back the last snapshot + * built by a server thread. + */ + @Override + public List players() { + queuePlayerRefresh(); + return playerMarkers; + } + + private void queuePlayerRefresh() { + if (!playerRefreshQueued.compareAndSet(false, true)) { + return; + } + + boolean scheduled = J.runGlobal(() -> { + try { + List markers = new ArrayList<>(); + for (Player player : BukkitWorldBinding.players(engine.getWorld())) { + Location at = player.getLocation(); + markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ())); + } + playerMarkers = List.copyOf(markers); + } finally { + playerRefreshQueued.set(false); + } + }); + + if (!scheduled) { + playerRefreshQueued.set(false); + } + } + + @Override + public void requestEntities(Consumer> sink) { + J.runGlobal(() -> { + IrisWorld target = engine.getWorld(); + World world = BukkitWorldBinding.world(target); + if (world == null) { + sink.accept(List.of()); + return; + } + + List living = new ArrayList<>(); + for (LivingEntity entity : BukkitWorldBinding.entities(target, LivingEntity.class)) { + if (!(entity instanceof Player)) { + living.add(entity); + } + } + + if (living.isEmpty()) { + sink.accept(List.of()); + return; + } + + List collected = Collections.synchronizedList(new ArrayList<>(living.size())); + AtomicInteger pending = new AtomicInteger(living.size()); + Runnable complete = () -> { + if (pending.decrementAndGet() == 0) { + sink.accept(List.copyOf(collected)); + } + }; + + for (LivingEntity entity : living) { + Location at = entity.getLocation(); + Runnable read = () -> { + try { + collected.add(marker(entity, at)); + } catch (Throwable ignored) { + } finally { + complete.run(); + } + }; + + if (!J.runRegion(world, at.getBlockX() >> 4, at.getBlockZ() >> 4, read)) { + complete.run(); + } + } + }); + } + + private GuiMarker marker(LivingEntity entity, Location at) { + String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")); + double maxHealth = 0; + try { + maxHealth = entity.getAttribute(MAX_HEALTH).getValue(); + } catch (Throwable ignored) { + } + return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth); + } + + @Override + public void teleport(double worldX, double worldZ) { + IrisWorld target = engine.getWorld(); + if (!target.hasPlatformWorld()) { + return; + } + J.runGlobal(() -> { + List players = BukkitWorldBinding.players(target); + if (players.isEmpty()) { + return; + } + Player player = players.get(0); + World world = player.getWorld(); + int xx = (int) worldX; + int zz = (int) worldZ; + J.runRegion(world, xx >> 4, zz >> 4, () -> { + int yy = world.getHighestBlockYAt(xx, zz) + 1; + Location destination = new Location(world, xx, yy, zz); + J.runEntity(player, () -> BukkitPlatform.teleportAsync(player, destination)); + }); + }); + } + + @Override + public String openInEditor(double worldX, double worldZ, RenderType type) { + IrisComplex complex = engine.getComplex(); + File file = switch (type) { + case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT -> + complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode(); + case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode(); + case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode(); + case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode(); + case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode(); + default -> null; + }; + return file == null ? null : file.getName(); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiExpansion.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiExpansion.java new file mode 100644 index 000000000..fb3b0c377 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiExpansion.java @@ -0,0 +1,59 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.link; + +import art.arcane.volmlib.util.bukkit.papi.PlaceholderKeyRegistry; +import art.arcane.volmlib.util.bukkit.papi.VolmitPlaceholderExpansion; + +import java.util.Objects; +import java.util.logging.Logger; + +public final class IrisPapiExpansion extends VolmitPlaceholderExpansion { + public static final String IDENTIFIER = "iris"; + public static final String AUTHOR = "Volmit Software"; + public static final String VERSION = "2.0.0"; + public static final String REQUIRED_PLUGIN = "Iris"; + + public IrisPapiExpansion(IrisPapiState state, Logger logger) { + super(IDENTIFIER, AUTHOR, VERSION, REQUIRED_PLUGIN, registry(state), logger); + } + + public static PlaceholderKeyRegistry registry(IrisPapiState state) { + Objects.requireNonNull(state, "state"); + + return PlaceholderKeyRegistry.builder() + .key("available", state::available) + .key("world.available", state::worldAvailable) + .key("world.biome", state::biome) + .key("world.biome-key", state::biomeKey) + .key("world.region", state::region) + .key("world.region-key", state::regionKey) + .key("world.dimension", state::dimension) + .key("pregen.available", state::pregenAvailable) + .key("pregen.world", state::pregenWorld) + .key("pregen.percent", state::pregenPercent) + .key("pregen.eta", state::pregenEta) + .key("pregen.eta-text", state::pregenEtaText) + .key("pregen.chunks", state::pregenChunks) + .key("pregen.total", state::pregenTotal) + .key("pregen.chunks-per-second", state::pregenChunksPerSecond) + .key("pregen.paused", state::pregenPaused) + .build(); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiInstaller.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiInstaller.java new file mode 100644 index 000000000..5341c02b2 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiInstaller.java @@ -0,0 +1,14 @@ +package art.arcane.iris.core.link; + +import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration; + +import java.util.logging.Logger; + +public final class IrisPapiInstaller { + private IrisPapiInstaller() { + } + + public static boolean install(PlaceholderRegistration registration, IrisPapiState state, Logger logger) { + return registration.register(() -> new IrisPapiExpansion(state, logger)); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiListener.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiListener.java new file mode 100644 index 000000000..3256659a3 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiListener.java @@ -0,0 +1,104 @@ +package art.arcane.iris.core.link; + +import art.arcane.iris.api.pregen.IrisPregenerationEvent; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerPortalEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerTeleportEvent; + +import java.util.Objects; +import java.util.UUID; + +public final class IrisPapiListener implements Listener { + private final IrisPapiState state; + + public IrisPapiListener(IrisPapiState state) { + this.state = Objects.requireNonNull(state, "state"); + } + + static void track(IrisPapiState state, UUID playerId, Location location) { + publish(state, playerId, location, false); + } + + static void trackNow(IrisPapiState state, UUID playerId, Location location) { + publish(state, playerId, location, true); + } + + private static void publish(IrisPapiState state, UUID playerId, Location location, boolean immediate) { + if (state == null || playerId == null || location == null) { + return; + } + + World world = location.getWorld(); + + if (world == null) { + return; + } + + if (immediate) { + state.trackPositionNow(playerId, world, location.getBlockX(), location.getBlockZ()); + return; + } + + state.trackPosition(playerId, world, location.getBlockX(), location.getBlockZ()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); + track(state, player == null ? null : player.getUniqueId(), event.getTo()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerTeleport(PlayerTeleportEvent event) { + Player player = event.getPlayer(); + trackNow(state, player == null ? null : player.getUniqueId(), event.getTo()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerPortal(PlayerPortalEvent event) { + Player player = event.getPlayer(); + trackNow(state, player == null ? null : player.getUniqueId(), event.getTo()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerRespawn(PlayerRespawnEvent event) { + Player player = event.getPlayer(); + trackNow(state, player == null ? null : player.getUniqueId(), event.getRespawnLocation()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + trackNow(state, player == null ? null : player.getUniqueId(), player == null ? null : player.getLocation()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerChangedWorld(PlayerChangedWorldEvent event) { + Player player = event.getPlayer(); + trackNow(state, player == null ? null : player.getUniqueId(), player == null ? null : player.getLocation()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + + if (player != null) { + state.release(player.getUniqueId()); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPregeneration(IrisPregenerationEvent event) { + state.publishPregen(event.getPhase(), event.getProgress()); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiPosition.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiPosition.java new file mode 100644 index 000000000..b6df177dd --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiPosition.java @@ -0,0 +1,9 @@ +package art.arcane.iris.core.link; + +import org.bukkit.World; + +public record IrisPapiPosition(World world, int blockX, int blockZ, long publishedAtMs) { + public boolean sameColumn(World other, int otherBlockX, int otherBlockZ) { + return world == other && blockX == otherBlockX && blockZ == otherBlockZ; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiPregenView.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiPregenView.java new file mode 100644 index 000000000..2358daa68 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiPregenView.java @@ -0,0 +1,51 @@ +package art.arcane.iris.core.link; + +import art.arcane.iris.api.pregen.IrisPregenProgress; +import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues; + +public record IrisPapiPregenView( + String world, + String percent, + String eta, + String etaText, + String chunks, + String total, + String chunksPerSecond, + String paused) { + private static final long MILLIS_PER_SECOND = 1_000L; + private static final long SECONDS_PER_MINUTE = 60L; + private static final long SECONDS_PER_HOUR = 3_600L; + + public static IrisPapiPregenView of(IrisPregenProgress progress) { + if (progress == null) { + return null; + } + + return new IrisPapiPregenView( + PlaceholderValues.text(progress.worldName()), + PlaceholderValues.num(progress.percent()), + PlaceholderValues.count(progress.etaMillis() / MILLIS_PER_SECOND), + duration(progress.etaMillis()), + PlaceholderValues.count(progress.generatedChunks()), + PlaceholderValues.count(progress.totalChunks()), + PlaceholderValues.num(progress.chunksPerSecond()), + PlaceholderValues.bool(progress.paused())); + } + + static String duration(long millis) { + long totalSeconds = millis / MILLIS_PER_SECOND; + long hours = totalSeconds / SECONDS_PER_HOUR; + long minutes = (totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; + long seconds = totalSeconds % SECONDS_PER_MINUTE; + + if (hours > 0L) { + return hours + "h " + minutes + "m"; + } + + if (minutes > 0L) { + return minutes + "m " + seconds + "s"; + } + + return seconds + "s"; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiState.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiState.java new file mode 100644 index 000000000..13c282bec --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiState.java @@ -0,0 +1,204 @@ +package art.arcane.iris.core.link; + +import art.arcane.iris.api.pregen.IrisPregenPhase; +import art.arcane.iris.api.pregen.IrisPregenProgress; +import art.arcane.iris.api.terrain.IrisTerrainService; +import art.arcane.iris.api.terrain.IrisWorldInfo; +import art.arcane.volmlib.util.bukkit.papi.PlaceholderSnapshot; +import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues; +import art.arcane.volmlib.util.bukkit.papi.PlayerSnapshotStore; +import org.bukkit.World; + +import java.util.Objects; +import java.util.UUID; +import java.util.function.LongSupplier; +import java.util.function.Supplier; + +public final class IrisPapiState { + static final long VIEW_TTL_MS = 1_000L; + static final long POSITION_INTERVAL_MS = 1_000L; + + private final Supplier terrain; + private final LongSupplier clock; + private final PlayerSnapshotStore positions = new PlayerSnapshotStore<>(); + private final PlayerSnapshotStore views = new PlayerSnapshotStore<>(); + private final PlaceholderSnapshot pregen = new PlaceholderSnapshot<>(); + + public IrisPapiState(Supplier terrain) { + this(terrain, System::currentTimeMillis); + } + + public IrisPapiState(Supplier terrain, LongSupplier clock) { + this.terrain = Objects.requireNonNull(terrain, "terrain"); + this.clock = Objects.requireNonNull(clock, "clock"); + } + + public void trackPosition(UUID playerId, World world, int blockX, int blockZ) { + publishPosition(playerId, world, blockX, blockZ, false); + } + + public void trackPositionNow(UUID playerId, World world, int blockX, int blockZ) { + publishPosition(playerId, world, blockX, blockZ, true); + } + + private void publishPosition(UUID playerId, World world, int blockX, int blockZ, boolean immediate) { + if (playerId == null || world == null) { + return; + } + + IrisPapiPosition current = positions.get(playerId); + + if (current != null && current.sameColumn(world, blockX, blockZ)) { + return; + } + + long now = clock.getAsLong(); + + if (!immediate && current != null && current.world() == world + && now - current.publishedAtMs() < POSITION_INTERVAL_MS) { + return; + } + + positions.publish(playerId, new IrisPapiPosition(world, blockX, blockZ, now)); + } + + public void release(UUID playerId) { + positions.publish(playerId, null); + views.publish(playerId, null); + } + + public void publishPregen(IrisPregenPhase phase, IrisPregenProgress progress) { + if (phase == null || progress == null) { + return; + } + + pregen.publish(switch (phase) { + case STARTED, TICK, PAUSED, RESUMED, SAVING -> IrisPapiPregenView.of(progress); + case COMPLETED, CANCELLED -> null; + }); + } + + public void clear() { + positions.clear(); + views.clear(); + pregen.publish(null); + } + + public String available(UUID playerId) { + return PlaceholderValues.bool(terrain.get() != null); + } + + public String worldAvailable(UUID playerId) { + IrisPapiWorldView view = viewOf(playerId); + return view == null ? PlaceholderValues.FALSE : view.available(); + } + + public String biome(UUID playerId) { + IrisPapiWorldView view = viewOf(playerId); + return view == null ? PlaceholderValues.UNAVAILABLE : view.biome(); + } + + public String biomeKey(UUID playerId) { + IrisPapiWorldView view = viewOf(playerId); + return view == null ? PlaceholderValues.UNAVAILABLE : view.biomeKey(); + } + + public String region(UUID playerId) { + IrisPapiWorldView view = viewOf(playerId); + return view == null ? PlaceholderValues.UNAVAILABLE : view.region(); + } + + public String regionKey(UUID playerId) { + IrisPapiWorldView view = viewOf(playerId); + return view == null ? PlaceholderValues.UNAVAILABLE : view.regionKey(); + } + + public String dimension(UUID playerId) { + IrisPapiWorldView view = viewOf(playerId); + return view == null ? PlaceholderValues.UNAVAILABLE : view.dimension(); + } + + public String pregenAvailable(UUID playerId) { + return pregen.available(); + } + + public String pregenWorld(UUID playerId) { + IrisPapiPregenView view = pregen.get(); + return view == null ? PlaceholderValues.UNAVAILABLE : view.world(); + } + + public String pregenPercent(UUID playerId) { + IrisPapiPregenView view = pregen.get(); + return view == null ? PlaceholderValues.UNAVAILABLE : view.percent(); + } + + public String pregenEta(UUID playerId) { + IrisPapiPregenView view = pregen.get(); + return view == null ? PlaceholderValues.UNAVAILABLE : view.eta(); + } + + public String pregenEtaText(UUID playerId) { + IrisPapiPregenView view = pregen.get(); + return view == null ? PlaceholderValues.UNAVAILABLE : view.etaText(); + } + + public String pregenChunks(UUID playerId) { + IrisPapiPregenView view = pregen.get(); + return view == null ? PlaceholderValues.UNAVAILABLE : view.chunks(); + } + + public String pregenTotal(UUID playerId) { + IrisPapiPregenView view = pregen.get(); + return view == null ? PlaceholderValues.UNAVAILABLE : view.total(); + } + + public String pregenChunksPerSecond(UUID playerId) { + IrisPapiPregenView view = pregen.get(); + return view == null ? PlaceholderValues.UNAVAILABLE : view.chunksPerSecond(); + } + + public String pregenPaused(UUID playerId) { + IrisPapiPregenView view = pregen.get(); + return view == null ? PlaceholderValues.UNAVAILABLE : view.paused(); + } + + private IrisPapiWorldView viewOf(UUID playerId) { + IrisPapiPosition position = positions.get(playerId); + + if (position == null) { + return null; + } + + IrisPapiWorldView cached = views.get(playerId); + long now = clock.getAsLong(); + + if (cached != null && cached.position() == position && now - cached.builtAtMs() < VIEW_TTL_MS) { + return cached; + } + + IrisPapiWorldView built = build(position, now); + views.publish(playerId, built); + return built; + } + + private IrisPapiWorldView build(IrisPapiPosition position, long now) { + IrisTerrainService service = terrain.get(); + World world = position.world(); + + if (service == null || !service.isIrisWorld(world)) { + return IrisPapiWorldView.absent(position, now); + } + + int blockX = position.blockX(); + int blockZ = position.blockZ(); + + return IrisPapiWorldView.present( + position, + now, + service.surfaceBiomeName(world, blockX, blockZ), + service.surfaceBiomeKey(world, blockX, blockZ), + service.regionName(world, blockX, blockZ), + service.regionKey(world, blockX, blockZ), + service.worldInfo(world).map(IrisWorldInfo::dimensionKey)); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiWorldView.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiWorldView.java new file mode 100644 index 000000000..b5692f485 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/link/IrisPapiWorldView.java @@ -0,0 +1,50 @@ +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 biome, + Optional biomeKey, + Optional region, + Optional regionKey, + Optional dimension) { + return new IrisPapiWorldView( + position, + builtAtMs, + PlaceholderValues.TRUE, + text(biome), + text(biomeKey), + text(region), + text(regionKey), + text(dimension)); + } + + static String text(Optional value) { + return value == null || value.isEmpty() ? PlaceholderValues.UNAVAILABLE : PlaceholderValues.text(value.get()); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java new file mode 100644 index 000000000..9f03179f5 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java @@ -0,0 +1,137 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import art.arcane.iris.api.world.IrisWorldPhase; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.datapack.DatapackIngestService; +import art.arcane.iris.core.events.IrisEngineHotloadEvent; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.project.IrisCodeWorkspace; +import art.arcane.iris.core.service.IrisApiEventSVC; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.core.tools.WorldMaintenance; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.EnginePlatformHooks; +import art.arcane.iris.engine.framework.NativeStructureVolume; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionRuntimeContract; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import org.bukkit.World; + +public final class BukkitEnginePlatformHooks implements EnginePlatformHooks { + @Override + public KList nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) { + return INMS.get().nativeStructureVolumes(engine, minX, minZ, maxX, maxZ); + } + + @Override + public void refreshWorkspace(Engine engine) { + new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace(); + } + + @Override + public void refreshDatapackWorkspace(Engine engine) { + DatapackIngestService.refreshWorkspace(engine.getData()); + } + + @Override + public void reloadDatapacks(Engine engine) { + synchronized (ServerConfigurator.class) { + ServerConfigurator.installDataPacks(false); + } + } + + @Override + public void fireHotloadEvent(Engine engine) { + IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(engine)); + IrisApiEventSVC.fireWorldPhase(BukkitWorldBinding.world(engine.getWorld()), IrisWorldPhase.ENGINE_HOTLOADED); + } + + @Override + public void validateDimensionHotload(Engine engine, IrisDimension replacement) { + IrisDimensionRuntimeContract.requireHotloadCompatible( + "Bukkit Studio world '" + engine.getWorld().name() + "'", + engine.getDimension(), + replacement, + "iris"); + } + + @Override + public boolean isPregeneratorActive(Engine engine) { + IrisWorld world = engine.getWorld(); + if (world == null) { + return false; + } + PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); + return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(world.identity()); + } + + @Override + public void shutdownPregenerator(Engine engine) { + IrisWorld world = engine.getWorld(); + if (world != null) { + PregeneratorJob.shutdownInstanceForWorld(world.identity()); + } + } + + @Override + public boolean shouldDisableChunkContextCache(Engine engine) { + IrisWorld world = engine.getWorld(); + if (!J.isFolia() || world == null || !world.hasPlatformWorld()) { + return false; + } + boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(world.identity()); + return EngineMode.shouldDisableContextCacheForMaintenance(maintenanceActive, isPregeneratorActive(engine)); + } + + @Override + public boolean shouldSkipMantleCleanup(Engine engine) { + IrisWorld world = engine.getWorld(); + return world != null + && WorldMaintenance.isWorldMaintenanceActive(world.identity()) + && !isPregeneratorActive(engine); + } + + @Override + public boolean shouldSkipMantleMarkerRead(Engine engine, int chunkX, int chunkZ) { + IrisWorld irisWorld = engine.getWorld(); + if (!J.isFolia() || irisWorld == null || !irisWorld.hasPlatformWorld()) { + return false; + } + World world = BukkitWorldBinding.world(irisWorld); + return world != null && J.isOwnedByCurrentRegion(world, chunkX, chunkZ); + } + + @Override + public boolean shouldBypassMantleStages(Engine engine) { + if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) { + return false; + } + World world = BukkitWorldBinding.world(engine.getWorld()); + return world != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(world); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/CommandSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/CommandSVC.java new file mode 100644 index 000000000..11c03e553 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/CommandSVC.java @@ -0,0 +1,332 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.commands.CommandIris; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.util.common.director.DirectorContext; +import art.arcane.iris.util.common.director.DirectorContextHandler; +import art.arcane.iris.util.common.director.DirectorSystem; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.director.DirectorEngineOptions; +import art.arcane.volmlib.util.director.compat.DirectorEngineFactory; +import art.arcane.volmlib.util.director.context.DirectorContextRegistry; +import art.arcane.volmlib.util.director.runtime.DirectorExecutionMode; +import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult; +import art.arcane.volmlib.util.director.runtime.DirectorInvocation; +import art.arcane.volmlib.util.director.runtime.DirectorInvocationHook; +import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine; +import art.arcane.volmlib.util.director.runtime.DirectorRuntimeNode; +import art.arcane.volmlib.util.director.runtime.DirectorSender; +import art.arcane.volmlib.util.director.help.DirectorMiniMenu; +import art.arcane.volmlib.util.director.help.DirectorMiniMenu.DirectorHelpPage; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.Sound; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.IrisMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook { + private static final String ROOT_COMMAND = "iris"; + private static final String ROOT_PERMISSION = "iris.all"; + + private final transient AtomicCache directorCache = new AtomicCache<>(); + private final transient ThreadLocal dispatchSenders = new ThreadLocal<>(); + + @Override + public void onEnable() { + PluginCommand command = findBukkitCommand(); + if (command != null) { + command.setExecutor(this); + command.setTabCompleter(this); + } else { + registerPaperCommand(); + } + J.a(this::getDirector); + } + + @Override + public void onDisable() { + + } + + public DirectorRuntimeEngine getDirector() { + return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create( + new CommandIris(), + DirectorEngineOptions.builder() + .contexts(buildDirectorContexts()) + .dispatcher(this::dispatchDirector) + .invocationHook(this) + .legacyHandlers(DirectorSystem.handlers) + .textResolver(IrisLanguage.directorResolver()) + .build() + )); + } + + private DirectorContextRegistry buildDirectorContexts() { + DirectorContextRegistry contexts = new DirectorContextRegistry(); + + for (Map.Entry, DirectorContextHandler> entry : DirectorContextHandler.contextHandlers.entrySet()) { + registerContextHandler(contexts, entry.getKey(), entry.getValue()); + } + + return contexts; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void registerContextHandler(DirectorContextRegistry contexts, Class type, DirectorContextHandler handler) { + contexts.register((Class) type, (invocation, map) -> { + if (invocation.getSender() instanceof BukkitDirectorSender sender) { + return ((DirectorContextHandler) handler).handle(new VolmitSender(sender.sender())); + } + + return null; + }); + } + + private void dispatchDirector(DirectorExecutionMode mode, Runnable runnable) { + if (mode != DirectorExecutionMode.SYNC) { + runnable.run(); + return; + } + CommandSender sender = dispatchSenders.get(); + if (sender instanceof Player player) { + if (!J.runEntity(player, runnable)) { + throw new IllegalStateException("Failed to schedule player command on its entity thread"); + } + return; + } + J.s(runnable); + } + + @Override + public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) { + if (invocation.getSender() instanceof BukkitDirectorSender sender) { + DirectorContext.touch(new VolmitSender(sender.sender())); + } + } + + @Override + public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) { + DirectorContext.remove(); + } + + @Nullable + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { + if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) { + return List.of(); + } + + return tabCompleteRoot(sender, alias, args); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) { + return false; + } + + executeRoot(sender, label, args); + return true; + } + + public void executeRoot(CommandSender sender, String label, String[] args) { + if (!sender.hasPermission(ROOT_PERMISSION)) { + sender.sendMessage(IrisLanguage.text( + IrisMessages.COMMAND_PERMISSION_DENIED, + MessageArgument.trusted("permission", ROOT_PERMISSION) + )); + return; + } + + J.aBukkit(() -> executeCommand(sender, label, args)); + } + + public List tabCompleteRoot(CommandSender sender, String alias, String[] args) { + List suggestions = runDirectorTab(sender, alias, args); + if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) { + player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f)); + } + return suggestions; + } + + private PluginCommand findBukkitCommand() { + try { + return Iris.instance.getCommand(ROOT_COMMAND); + } catch (UnsupportedOperationException ignored) { + return null; + } + } + + private void registerPaperCommand() { + try { + Class registrarType = Class.forName( + "art.arcane.iris.core.commands.PaperCommandRegistrar", + true, + getClass().getClassLoader() + ); + Method register = registrarType.getDeclaredMethod("register", Iris.class, CommandSVC.class); + register.invoke(null, Iris.instance, this); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new IllegalStateException("Paper command registration failed", cause); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Paper command registrar is unavailable", e); + } catch (LinkageError e) { + throw new IllegalStateException("Paper command APIs are unavailable", e); + } + } + + private void executeCommand(CommandSender sender, String label, String[] args) { + if (sendHelpIfRequested(sender, args)) { + playSuccessSound(sender); + return; + } + + DirectorExecutionResult result = runDirector(sender, label, args); + + if (result.isSuccess()) { + playSuccessSound(sender); + return; + } + + playFailureSound(sender); + if (result.getMessage() == null || result.getMessage().trim().isEmpty()) { + new VolmitSender(sender).sendMessage(C.RED + IrisLanguage.text(IrisMessages.COMMAND_UNKNOWN)); + } + } + + private boolean sendHelpIfRequested(CommandSender sender, String[] args) { + Optional request = DirectorMiniMenu.resolveHelp(getDirector(), Arrays.asList(args)); + if (request.isEmpty()) { + return false; + } + + 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 { + 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 { + dispatchSenders.remove(); + } + } + + private List runDirectorTab(CommandSender sender, String alias, String[] args) { + DirectorContext.touch(new VolmitSender(sender)); + try { + return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args))); + } catch (Throwable e) { + Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage()); + return List.of(); + } finally { + DirectorContext.remove(); + } + } + + private void playFailureSound(CommandSender sender) { + if (!IrisSettings.get().getGeneral().isCommandSounds()) { + return; + } + + if (sender instanceof Player player) { + J.runEntity(player, () -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f, + Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f)); + } + } + + private void playSuccessSound(CommandSender sender) { + if (!IrisSettings.get().getGeneral().isCommandSounds()) { + return; + } + + if (sender instanceof Player player) { + J.runEntity(player, () -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f, + Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f)); + } + } + + private static final java.util.concurrent.atomic.AtomicBoolean SOUND_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false); + + private void playSounds(Player player, Sound a, float av, float ap, Sound b, float bv, float bp) { + try { + player.playSound(player.getLocation(), a, av, ap); + player.playSound(player.getLocation(), b, bv, bp); + } catch (Throwable e) { + if (SOUND_LOGGED.compareAndSet(false, true)) { + try { + java.io.File f = Iris.instance.getDataFile("adventure-debug.txt"); + java.nio.file.Files.writeString(f.toPath(), "SOUND FAIL: " + e.getClass().getName() + ": " + e.getMessage() + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); + } catch (Throwable ignored) { + } + } + } + } + + private record BukkitDirectorSender(CommandSender sender) implements DirectorSender { + @Override + public String getName() { + return sender.getName(); + } + + @Override + public boolean isPlayer() { + return sender instanceof Player; + } + + @Override + public void sendMessage(String message) { + if (message != null && !message.trim().isEmpty()) { + sender.sendMessage(message); + } + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/DatapackStructureScopeSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/DatapackStructureScopeSVC.java new file mode 100644 index 000000000..cf86b2587 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/DatapackStructureScopeSVC.java @@ -0,0 +1,121 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.IrisStartupValidation; +import art.arcane.iris.core.datapack.DatapackIngestService; +import art.arcane.iris.core.datapack.DatapackStructureScopeIndex; +import art.arcane.iris.core.nms.DatapackStructureScopeResult; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.engine.object.IrisImportedStructureControl; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.IrisService; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.world.WorldInitEvent; +import org.bukkit.event.world.WorldUnloadEvent; + +import java.io.IOException; +import java.util.Set; + +public final class DatapackStructureScopeSVC implements IrisService { + private DatapackStructureScopeIndex scopeIndex = DatapackStructureScopeIndex.create(null); + + @Override + public void onEnable() { + try { + scopeIndex = DatapackStructureScopeIndex.create( + DatapackIngestService.installedStructureScopeResources()); + } catch (IOException e) { + // A missing manifest entry is the condition validateOnStartup already classified as + // non-fatal; degrade to an empty scope and lock world creation instead of aborting + // the whole plugin bootstrap (which would skip listeners and journal reconciliation). + scopeIndex = DatapackStructureScopeIndex.create(null); + IrisLogging.error("Could not establish ownership for installed datapack structure sets; " + + "datapack structure scoping is disabled until the manifest is repaired: " + e.getMessage()); + IrisStartupValidation.markDatapacksInvalid( + "Iris could not establish ownership for installed datapack structure sets: " + e.getMessage()); + } + for (World world : Bukkit.getWorlds()) { + try { + IrisImportedStructureControl importedStructures = importedStructures(world); + if (!scopeIndex.isEmpty() || importedStructures != null && importedStructures.hasFrequencyOverrides()) { + applyScope(world, importedStructures); + } + } catch (Throwable e) { + // Per-world containment: one world's scoping failure must not abort the + // service enable (and with it the whole bootstrap's containment contract). + IrisLogging.error("Could not scope datapack structures for world '" + world.getName() + "'."); + IrisLogging.reportError(e); + } + } + } + + @Override + public void onDisable() { + for (World world : Bukkit.getWorlds()) { + INMS.get().abandonStudioStructureBootstrap(world); + } + scopeIndex = DatapackStructureScopeIndex.create(null); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onWorldInit(WorldInitEvent event) { + boolean studioEntryBootstrap = event.getWorld().getGenerator() + instanceof BukkitChunkGenerator generator + && generator.isStudioEntryBootstrapActive(); + IrisImportedStructureControl importedStructures = importedStructures(event.getWorld()); + if (shouldApplyScope(scopeIndex.isEmpty(), studioEntryBootstrap, + importedStructures != null && importedStructures.hasFrequencyOverrides())) { + applyScope(event.getWorld(), importedStructures); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onWorldUnload(WorldUnloadEvent event) { + INMS.get().abandonStudioStructureBootstrap(event.getWorld()); + } + + static boolean shouldApplyScope(boolean scopeIndexEmpty, boolean studioEntryBootstrap, + boolean hasFrequencyOverrides) { + return !scopeIndexEmpty || studioEntryBootstrap || hasFrequencyOverrides; + } + + private void applyScope(World world, IrisImportedStructureControl importedStructures) { + Set declaredSources = declaredSources(world); + IrisImportedStructureControl activeControl = importedStructures == null + ? new IrisImportedStructureControl() + : importedStructures; + try { + DatapackStructureScopeResult result = INMS.get().scopeDatapackStructures( + world, scopeIndex, declaredSources, activeControl); + IrisLogging.info("Scoped Iris-managed datapack structure sets for world '" + + world.getName() + "': " + result.retainedManagedSets() + " retained, " + + result.excludedManagedSets() + " excluded."); + } catch (Throwable error) { + throw new IllegalStateException("Could not scope Iris-managed datapack structure sets for world '" + + world.getName() + "'", error); + } + } + + private IrisImportedStructureControl importedStructures(World world) { + PlatformChunkGenerator generator = IrisToolbelt.access(world); + if (generator == null || generator.getTarget() == null + || generator.getTarget().getDimension() == null) { + return null; + } + return generator.getTarget().getDimension().getImportedStructures(); + } + + private Set declaredSources(World world) { + PlatformChunkGenerator generator = IrisToolbelt.access(world); + if (generator == null) { + return Set.of(); + } + return scopeIndex.declaredSources( + generator.getTarget().getDimension().getDatapackImports()); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/EditSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/EditSVC.java new file mode 100644 index 000000000..a62037704 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/EditSVC.java @@ -0,0 +1,126 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.edit.BlockEditor; +import art.arcane.iris.core.edit.BukkitBlockEditor; +import art.arcane.iris.engine.framework.BlockEditAccess; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.math.M; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.event.EventHandler; +import org.bukkit.event.world.WorldUnloadEvent; + +public class EditSVC implements IrisService, BlockEditAccess { + private KMap editors; + private int updateTaskId = -1; + + @Override + public void onEnable() { + this.editors = new KMap<>(); + updateTaskId = J.sr(this::update, 1000); + } + + @Override + public void onDisable() { + if (updateTaskId != -1) { + J.csr(updateTaskId); + updateTaskId = -1; + } + if (editors == null) { + return; + } + flushNow(); + } + + public BlockData get(World world, int x, int y, int z) { + return open(world).get(x, y, z); + } + + public void set(World world, int x, int y, int z, BlockData d) { + open(world).set(x, y, z, d); + } + + public void setBiome(World world, int x, int y, int z, Biome d) { + open(world).setBiome(x, y, z, d); + } + + public void setBiome(World world, int x, int z, Biome d) { + open(world).setBiome(x, z, d); + } + + public Biome getBiome(World world, int x, int y, int z) { + return open(world).getBiome(x, y, z); + } + + public Biome getBiome(World world, int x, int z) { + return open(world).getBiome(x, z); + } + + @EventHandler + public void on(WorldUnloadEvent e) { + if (editors == null) { + return; + } + if (editors.containsKey(e.getWorld())) { + editors.remove(e.getWorld()).close(); + } + } + + + public void update() { + if (editors == null) { + return; + } + for (World i : editors.k()) { + if (M.ms() - editors.get(i).last() > 1000) { + editors.remove(i).close(); + } + } + } + + public void flushNow() { + if (editors == null) { + return; + } + for (World i : editors.k()) { + editors.remove(i).close(); + } + } + + public BlockEditor open(World world) { + if (editors == null) { + editors = new KMap<>(); + } + if (editors.containsKey(world)) { + return editors.get(world); + } + + BlockEditor e = new BukkitBlockEditor(world); + editors.put(world, e); + + return e; + } + +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java new file mode 100644 index 000000000..088a40b64 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java @@ -0,0 +1,46 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.tree.TreeFellerRunHooks; +import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate; +import art.arcane.iris.core.service.TreeFellerModel.TreeClaim; +import art.arcane.iris.core.service.TreeFellerModel.TreeMember; +import org.bukkit.Location; +import org.bukkit.inventory.ItemStack; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +final class FellingRun { + final TreeClaim claim; + final TreeCandidate candidate; + final int preservationChance; + final TreeFellerRunHooks runHooks; + final int heldSlot; + final TreeFellerPresentation presentation; + final AtomicBoolean finished = new AtomicBoolean(); + final AtomicInteger cursor = new AtomicInteger(); + final AtomicInteger processed = new AtomicInteger(); + volatile int blocksPerPulse = 1; + volatile int effectStride = 1; + volatile ItemStack expectedTool; + volatile List work = List.of(); + volatile String abortReason; + + FellingRun( + TreeClaim claim, + TreeCandidate candidate, + int preservationChance, + TreeFellerRunHooks runHooks, + int heldSlot, + Location fallbackLocation + ) { + this.claim = claim; + this.candidate = candidate; + this.preservationChance = preservationChance; + this.runHooks = runHooks; + this.heldSlot = heldSlot; + this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation); + this.expectedTool = candidate.tool().clone(); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java new file mode 100644 index 000000000..b90df0706 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java @@ -0,0 +1,106 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.Iris; +import art.arcane.iris.api.pregen.IrisPregenPhase; +import art.arcane.iris.api.pregen.IrisPregenProgress; +import art.arcane.iris.api.pregen.IrisPregenerationEvent; +import art.arcane.iris.api.terrain.IrisWorldInfo; +import art.arcane.iris.api.world.IrisWorldEngineEvent; +import art.arcane.iris.api.world.IrisWorldPhase; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.pregenerator.PregenApiPhase; +import art.arcane.iris.core.pregenerator.PregenApiSink; +import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.plugin.IrisService; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.event.Event; + +public class IrisApiEventSVC implements IrisService, PregenApiSink { + public static void fireWorldPhase(World world, IrisWorldPhase phase) { + if (phase == null) { + return; + } + if (world == null) { + IrisLogging.debug("Iris world API skipped phase " + phase + + " because the engine has no bound platform world."); + return; + } + + try { + deliver(new IrisWorldEngineEvent(world, phase, describe(world, phase))); + } catch (Throwable error) { + IrisLogging.reportError("Iris world API dispatch failed for phase " + phase + + " on world \"" + world.getName() + "\".", error); + } + } + + private static IrisWorldInfo describe(World world, IrisWorldPhase phase) { + try { + return IrisWorldInfoFactory.forWorld(world); + } catch (Throwable error) { + IrisLogging.reportError("Iris world API could not describe world \"" + world.getName() + + "\" for phase " + phase + "; the event is delivered without world info.", error); + return null; + } + } + + private static void deliver(Event event) { + if (Bukkit.isPrimaryThread()) { + Bukkit.getPluginManager().callEvent(event); + return; + } + + Iris.callEvent(event); + } + + private static IrisPregenPhase toApi(PregenApiPhase phase) { + return switch (phase) { + case STARTED -> IrisPregenPhase.STARTED; + case TICK -> IrisPregenPhase.TICK; + case PAUSED -> IrisPregenPhase.PAUSED; + case RESUMED -> IrisPregenPhase.RESUMED; + case SAVING -> IrisPregenPhase.SAVING; + case COMPLETED -> IrisPregenPhase.COMPLETED; + case CANCELLED -> IrisPregenPhase.CANCELLED; + }; + } + + private static IrisPregenProgress toApi(PregeneratorJob.PregenProgress progress) { + return new IrisPregenProgress( + progress.worldName(), + progress.worldIdentity(), + progress.percent(), + progress.generated(), + progress.totalChunks(), + progress.chunksRemaining(), + progress.failed(), + progress.chunksPerSecond(), + progress.eta(), + progress.elapsed(), + progress.method(), + progress.paused() + ); + } + + @Override + public void onEnable() { + IrisServices.register(PregenApiSink.class, this); + } + + @Override + public void onDisable() { + IrisServices.remove(PregenApiSink.class); + } + + @Override + public void pregen(PregenApiPhase phase, PregeneratorJob.PregenProgress progress) { + if (phase == null || progress == null || progress.worldIdentity() == null) { + return; + } + + Iris.callEvent(new IrisPregenerationEvent(toApi(phase), toApi(progress))); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java new file mode 100644 index 000000000..3f10b882d --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java @@ -0,0 +1,801 @@ +package art.arcane.iris.core.service; + +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.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.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.format.Form; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.event.world.WorldUnloadEvent; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +public final class IrisEngineSVC implements IrisService { + private static final long MAINTENANCE_PERIOD_MILLIS = 2_000L; + private static final long METRICS_PERIOD_MILLIS = 1_000L; + private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L; + + private final Object registrationLock = new Object(); + private final Set closingGenerators = new HashSet<>(); + private final AtomicReference metrics = + new AtomicReference<>(IrisEngineStatus.MaintenanceMetrics.EMPTY); + private final AtomicReference telemetry = + new AtomicReference<>(IrisTelemetrySnapshot.EMPTY); + private final Map> pendingRegistrations = new HashMap<>(); + private final Map worlds = new ConcurrentHashMap<>(); + private final IrisWorldPhaseLedger phases = new IrisWorldPhaseLedger(); + private volatile ScheduledThreadPoolExecutor service; + private volatile ScheduledFuture metricsTask; + + @Override + public void onEnable() { + ScheduledThreadPoolExecutor current = service; + if (current != null && !current.isShutdown()) { + return; + } + + IrisSettings.IrisSettingsEngineSVC settings = IrisSettings.get().getPerformance().getEngineSVC(); + ThreadFactory threadFactory = (settings.isUseVirtualThreads() + ? Thread.ofVirtual() + : Thread.ofPlatform().priority(settings.getPriority())) + .name("Iris EngineSVC-", 0) + .factory(); + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor( + EngineMaintenance.workerParallelism(), threadFactory); + executor.setRemoveOnCancelPolicy(true); + executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + service = executor; + + for (World world : Bukkit.getWorlds()) { + add(world); + } + metricsTask = executor.scheduleWithFixedDelay( + this::updateMetricsSafely, + 0L, + METRICS_PERIOD_MILLIS, + TimeUnit.MILLISECONDS); + } + + @Override + public void onDisable() { + ScheduledThreadPoolExecutor activeService = service; + service = null; + + ScheduledFuture activeMetricsTask = metricsTask; + metricsTask = null; + if (activeMetricsTask != null) { + activeMetricsTask.cancel(false); + } + + List teardowns = new ArrayList<>(); + List> generatorCloses; + synchronized (registrationLock) { + for (Map.Entry entry : worlds.entrySet()) { + Registered registered = entry.getValue(); + registered.close(); + teardowns.add(new Teardown(entry.getKey(), registered, reserveClose(registered))); + } + worlds.clear(); + pendingRegistrations.clear(); + generatorCloses = new ArrayList<>(closingGenerators.size()); + for (ClosingGenerator closing : closingGenerators) { + generatorCloses.add(closing.completion()); + } + } + + for (Teardown teardown : teardowns) { + phases.closing(teardown.world()); + } + shutdownAndDrain(activeService); + for (Teardown teardown : teardowns) { + startClose(teardown.registered(), teardown.closing()); + } + awaitGeneratorShutdown(generatorCloses); + resetMetrics(); + } + + public void engineStatus(VolmitSender sender) { + ScheduledThreadPoolExecutor activeService = service; + ScheduledFuture activeMetricsTask = metricsTask; + boolean serviceRunning = activeService != null && !activeService.isShutdown(); + boolean metricsRunning = activeMetricsTask != null + && !activeMetricsTask.isDone() + && !activeMetricsTask.isCancelled(); + + IrisEngineStatus.send(sender, new IrisEngineStatus.Snapshot( + serviceRunning, + metricsRunning, + MAINTENANCE_PERIOD_MILLIS, + serviceRunning ? activeService.getCorePoolSize() : 0, + TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()), + MantleHeapPressure.usedFraction(), + metrics.get())); + } + + IrisTelemetrySnapshot telemetrySnapshot() { + return telemetry.get(); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onWorldUnload(WorldUnloadEvent event) { + World world = event.getWorld(); + CompletionStage unloadBoundary = WorldUnloadBoundaryRegistry.claim( + WorldIdentity.serialize(world)); + if (remove(world, unloadBoundary) || unloadBoundary != null) { + return; + } + closeUntrackedGenerator(world); + } + + /** + * Closes the engine of a world this service never registered. + *

+ * Registration is skipped while a previous generator for the same identity is still closing and while + * the world is not yet the server's world for its UID, and an unload that lands in that window would + * otherwise leave a live engine bound to a dead world. Only an unload Iris did not drive is covered: + * an Iris-driven unload carries a boundary and closes the generator in its own phase. The close is + * CAS-idempotent, so a redundant call returns the in-flight completion. + */ + private void closeUntrackedGenerator(World world) { + PlatformChunkGenerator generator = IrisToolbelt.access(world); + if (generator == null || generator.isClosing()) { + return; + } + IrisLogging.debug("EngineSVC: closing untracked generator for unloaded world " + world.getName()); + CompletableFuture close = generator.closeAsync(); + if (close == null) { + return; + } + close.whenComplete((ignored, failure) -> { + if (failure != null) { + Throwable cause = failure.getCause() == null ? failure : failure.getCause(); + reportFailure("Failed to close untracked generator for " + world.getName(), cause); + } + }); + } + + @EventHandler + public void onWorldLoad(WorldLoadEvent event) { + add(event.getWorld()); + } + + private void add(World world) { + ScheduledThreadPoolExecutor activeService = service; + if (world == null || activeService == null || activeService.isShutdown() || !isCurrentWorld(world)) { + return; + } + + PlatformChunkGenerator access = IrisToolbelt.access(world); + if (access == null) { + return; + } + + RegistrationIdentity registrationIdentity = RegistrationIdentity.from(world); + Registered replaced = null; + ClosingGenerator replacementClose = null; + CompletableFuture retryAfter = null; + boolean registered = false; + try { + synchronized (registrationLock) { + if (service != activeService || activeService.isShutdown() || !isCurrentWorld(world)) { + return; + } + + Registered current = worlds.get(world); + if (current != null && !current.isClosed() && current.uses(access)) { + pendingRegistrations.remove(world); + return; + } + if (current != null) { + worlds.remove(world, current); + current.close(); + replaced = current; + replacementClose = reserveClose(current); + retryAfter = replacementClose.completion(); + } else { + ClosingGenerator closing = findClosingGenerator(registrationIdentity); + if (closing != null) { + retryAfter = closing.completion(); + } else if (!access.isClosing() && !hasRegisteredConflict(registrationIdentity)) { + Registration registration = new Registration(world.getName(), access, registrationIdentity); + worlds.put(world, new Registered(registration, activeService)); + pendingRegistrations.remove(world); + registered = true; + } + } + } + } catch (RejectedExecutionException exception) { + if (!activeService.isShutdown()) { + reportFailure("Failed to register engine maintenance for " + world.getName(), exception); + } + } + + if (registered) { + phases.ready(world); + } + if (replacementClose != null) { + phases.closing(world); + retryRegistrationAfterClose(world, retryAfter); + startClose(replaced, replacementClose); + return; + } + if (retryAfter != null) { + retryRegistrationAfterClose(world, retryAfter); + } + } + + /** + * @return true when a registration was found and its close was reserved + */ + private boolean remove(World world, CompletionStage unloadBoundary) { + if (world == null) { + return false; + } + + Registered registered; + ClosingGenerator closing = null; + synchronized (registrationLock) { + pendingRegistrations.remove(world); + registered = worlds.remove(world); + if (registered != null) { + registered.close(); + closing = reserveClose(registered); + } + } + if (closing == null) { + return false; + } + phases.closing(world); + deferCloseUntilWorldUnload(world, registered, closing, unloadBoundary); + return true; + } + + private void deferCloseUntilWorldUnload( + World world, + Registered registered, + ClosingGenerator closing, + CompletionStage unloadBoundary + ) { + if (unloadBoundary == null) { + J.sfut(() -> startClose(registered, closing), 1) + .whenComplete((ignored, failure) -> { + if (failure != null) { + Throwable cause = failure.getCause() == null ? failure : failure.getCause(); + reportFailure("Failed to defer generator close for " + registered.name(), cause); + completeClose(closing, cause); + } + }); + return; + } + unloadBoundary.whenComplete((unloaded, failure) -> { + if (failure == null && Boolean.TRUE.equals(unloaded)) { + startClose(registered, closing); + return; + } + abandonClose(world, closing); + }); + } + + private void abandonClose(World world, ClosingGenerator closing) { + synchronized (registrationLock) { + closingGenerators.remove(closing); + } + closing.completion().complete(null); + J.sfut(() -> { + if (isCurrentWorld(world)) { + add(world); + } + }, 1).whenComplete((ignored, failure) -> { + if (failure != null) { + Throwable cause = failure.getCause() == null ? failure : failure.getCause(); + reportFailure("Failed to restore generator maintenance for " + world.getName(), cause); + } + }); + } + + private ClosingGenerator reserveClose(Registered registered) { + ClosingGenerator closing = new ClosingGenerator( + registered.registrationIdentity(), + new CompletableFuture<>()); + closingGenerators.add(closing); + return closing; + } + + private void startClose(Registered registered, ClosingGenerator closing) { + try { + CompletableFuture close = registered.closeGenerator(); + if (close == null) { + throw new IllegalStateException("Generator close returned no completion future for " + registered.name()); + } + close.whenComplete((ignored, failure) -> completeClose(closing, failure)); + } catch (Throwable exception) { + reportFailure("Failed to start generator close for " + registered.name(), exception); + completeClose(closing, exception); + } + } + + private void completeClose(ClosingGenerator closing, Throwable failure) { + if (failure == null) { + synchronized (registrationLock) { + closingGenerators.remove(closing); + } + closing.completion().complete(null); + } else { + // A failed close must still stop conflicting with future registrations, + // otherwise the world never regains its maintenance task after a reload. + synchronized (registrationLock) { + closingGenerators.remove(closing); + } + closing.completion().completeExceptionally(failure); + } + } + + private ClosingGenerator findClosingGenerator(RegistrationIdentity registrationIdentity) { + for (ClosingGenerator closing : closingGenerators) { + if (closing.registrationIdentity().conflictsWith(registrationIdentity)) { + return closing; + } + } + return null; + } + + private boolean hasRegisteredConflict(RegistrationIdentity registrationIdentity) { + for (Registered registered : worlds.values()) { + if (!registered.isClosed() && registered.registrationIdentity().conflictsWith(registrationIdentity)) { + return true; + } + } + return false; + } + + private void retryRegistrationAfterClose(World world, CompletableFuture completion) { + synchronized (registrationLock) { + if (service == null || pendingRegistrations.get(world) == completion) { + return; + } + pendingRegistrations.put(world, completion); + } + + completion.whenComplete((ignored, failure) -> { + synchronized (registrationLock) { + if (pendingRegistrations.get(world) != completion) { + return; + } + pendingRegistrations.remove(world); + } + if (failure != null) { + return; + } + + ScheduledThreadPoolExecutor activeService = service; + if (activeService == null || activeService.isShutdown()) { + return; + } + try { + J.s(() -> add(world), 1); + } catch (Throwable exception) { + if (service != null) { + reportFailure("Failed to retry engine maintenance registration for " + world.getName(), exception); + } + } + }); + } + + private static boolean isCurrentWorld(World world) { + return Bukkit.getWorld(world.getUID()) == world; + } + + private void updateMetricsSafely() { + try { + updateMetrics(); + } catch (Throwable exception) { + reportFailure("Failed to update engine maintenance metrics", exception); + } + } + + private void updateMetrics() { + long now = System.currentTimeMillis(); + List worldSnapshots = new ArrayList<>(worlds.size()); + int maintenanceTasks = 0; + int irisWorlds = 0; + + for (Registered registered : worlds.values()) { + if (registered.isClosed()) { + continue; + } + + irisWorlds++; + if (registered.maintenanceInFlight()) { + maintenanceTasks++; + } + + try { + Engine engine = registered.getEngine(); + if (engine == null) { + continue; + } + if (engine.isClosing() && !engine.isClosed()) { + continue; + } + if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) { + registered.close(); + continue; + } + + double chunksPerSecond = registered.sampleChunksPerSecond(engine.getGenerated(), now); + worldSnapshots.add(EngineTelemetrySnapshot.capture(engine, chunksPerSecond, now)); + registered.clearMetricsFailure(); + } catch (Throwable exception) { + if (EngineMaintenance.isMantleClosed(exception)) { + if (registered.generatorClosing()) { + registered.close(); + } + } else { + registered.reportMetricsFailure(exception); + } + } + } + + EngineTelemetrySnapshot.Aggregate aggregate = EngineTelemetrySnapshot.aggregate(worldSnapshots); + metrics.set(new IrisEngineStatus.MaintenanceMetrics( + aggregate.mantleResidentPlates(), + aggregate.mantleQueuedPlates(), + aggregate.loadedChunks(), + irisWorlds, + maintenanceTasks, + aggregate.mantleIdleAverageMs(), + aggregate.mantleIdleMaxMs(), + aggregate.mantleIdleMinMs())); + + ScheduledThreadPoolExecutor activeService = service; + int workers = activeService == null || activeService.isShutdown() + ? 0 + : activeService.getCorePoolSize(); + int closingCount; + int pendingCount; + synchronized (registrationLock) { + closingCount = closingGenerators.size(); + pendingCount = pendingRegistrations.size(); + } + double heapUsage = MantleHeapPressure.usedFraction(); + telemetry.set(new IrisTelemetrySnapshot( + now, + worldSnapshots, + aggregate, + maintenanceTasks, + workers, + closingCount, + pendingCount, + heapUsage, + MantleHeapPressure.reclaimUrgency(heapUsage), + summarizeCaches(), + pregeneratorSnapshot())); + } + + private IrisTelemetrySnapshot.CacheSnapshot summarizeCaches() { + IrisTelemetrySnapshot.CacheBucket resource = IrisTelemetrySnapshot.CacheBucket.EMPTY; + IrisTelemetrySnapshot.CacheBucket stream2d = IrisTelemetrySnapshot.CacheBucket.EMPTY; + IrisTelemetrySnapshot.CacheBucket stream3d = IrisTelemetrySnapshot.CacheBucket.EMPTY; + IrisTelemetrySnapshot.CacheBucket other = IrisTelemetrySnapshot.CacheBucket.EMPTY; + PreservationSVC preservation = IrisServices.get(PreservationSVC.class); + List caches = preservation == null ? List.of() : preservation.getCaches(); + + for (MeteredCache cache : caches) { + IrisTelemetrySnapshot.CacheBucket sample = new IrisTelemetrySnapshot.CacheBucket( + 1, + cache.getSize(), + cache.getMaxSize()); + if (cache instanceof ResourceLoader) { + resource = resource.plus(sample); + } else if (cache instanceof CachedStream2D || cache instanceof CachedDoubleStream2D) { + stream2d = stream2d.plus(sample); + } else if (cache instanceof CachedStream3D) { + stream3d = stream3d.plus(sample); + } else { + other = other.plus(sample); + } + } + + IrisTelemetrySnapshot.CacheBucket total = resource.plus(stream2d).plus(stream3d).plus(other); + return new IrisTelemetrySnapshot.CacheSnapshot(total, resource, stream2d, stream3d, other); + } + + private IrisTelemetrySnapshot.PregenSnapshot pregeneratorSnapshot() { + PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot(); + if (progress == null) { + return IrisTelemetrySnapshot.PregenSnapshot.INACTIVE; + } + return new IrisTelemetrySnapshot.PregenSnapshot( + true, + progress.worldIdentity(), + progress.worldName(), + progress.paused(), + progress.percent(), + progress.generated(), + progress.totalChunks(), + progress.chunksRemaining(), + progress.chunksPerSecond(), + progress.eta(), + progress.elapsed(), + progress.failed()); + } + + private void shutdownAndDrain(ScheduledThreadPoolExecutor activeService) { + if (activeService == null) { + return; + } + + activeService.shutdown(); + try { + if (!activeService.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + activeService.shutdownNow(); + } + } catch (InterruptedException exception) { + activeService.shutdownNow(); + Thread.currentThread().interrupt(); + reportFailure("Interrupted while draining engine maintenance", exception); + } + } + + private void awaitGeneratorShutdown(List> closes) { + if (closes.isEmpty()) { + return; + } + + try { + CompletableFuture.allOf(closes.toArray(new CompletableFuture[0])) + .get(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + reportFailure("Interrupted while closing Iris generators", exception); + } catch (ExecutionException exception) { + Throwable cause = exception.getCause() == null ? exception : exception.getCause(); + reportFailure("Unexpected generator close aggregation failure", cause); + } catch (TimeoutException exception) { + reportFailure("Timed out while closing Iris generators", exception); + } + } + + private void resetMetrics() { + metrics.set(IrisEngineStatus.MaintenanceMetrics.EMPTY); + telemetry.set(IrisTelemetrySnapshot.EMPTY); + } + + private static void reportFailure(String message, Throwable exception) { + IrisLogging.reportError(exception); + IrisLogging.error("EngineSVC: " + message); + exception.printStackTrace(); + } + + private final class Registered { + private final String name; + private final PlatformChunkGenerator access; + private final RegistrationIdentity registrationIdentity; + private final AtomicBoolean metricsFailureReported = new AtomicBoolean(); + private final AtomicReference> activeMaintenance = new AtomicReference<>(); + private volatile ScheduledFuture maintenance; + private volatile boolean closed; + private long lastGeneratedSampleAtMs; + private int lastGeneratedCount; + + private Registered(Registration registration, ScheduledThreadPoolExecutor executor) { + name = registration.name(); + access = registration.access(); + registrationIdentity = registration.registrationIdentity(); + long offset = ThreadLocalRandom.current().nextLong(MAINTENANCE_PERIOD_MILLIS); + maintenance = executor.scheduleWithFixedDelay( + this::maintain, + offset, + MAINTENANCE_PERIOD_MILLIS, + TimeUnit.MILLISECONDS); + } + + private void maintain() { + if (closed) { + return; + } + CompletableFuture completion = new CompletableFuture<>(); + if (!activeMaintenance.compareAndSet(null, completion)) { + return; + } + try { + if (closed) { + return; + } + Engine engine = getEngine(); + if (engine == null) { + return; + } + if (!EngineMaintenance.isAvailable(engine)) { + return; + } + + boolean pregeneratorTargetsWorld = EngineMaintenance.pregeneratorTargets(engine); + if (pregeneratorTargetsWorld || !EngineMaintenance.shouldRun(engine)) { + return; + } + + EngineMaintenance.Outcome outcome = EngineMaintenance.run(engine); + if (outcome.unloadedTectonicPlates() > 0) { + IrisLogging.debug(C.GOLD + "Unloaded " + C.YELLOW + + outcome.unloadedTectonicPlates() + " TectonicPlates in " + C.RED + + Form.duration(outcome.unloadDurationMillis(), 2) + " for " + name); + } + } catch (Throwable exception) { + if (EngineMaintenance.isMantleClosed(exception)) { + if (generatorClosing()) { + close(); + } + return; + } + reportFailure("Failed maintenance for " + name, exception); + } finally { + completion.complete(null); + activeMaintenance.compareAndSet(completion, null); + } + } + + private boolean uses(PlatformChunkGenerator candidate) { + return access == candidate; + } + + private String name() { + return name; + } + + private RegistrationIdentity registrationIdentity() { + return registrationIdentity; + } + + private boolean generatorClosing() { + return access.isClosing(); + } + + private boolean isClosed() { + return closed; + } + + private boolean maintenanceInFlight() { + return activeMaintenance.get() != null; + } + + private double sampleChunksPerSecond(int generatedCount, long sampledAtMs) { + int safeGeneratedCount = Math.max(0, generatedCount); + long previousAt = lastGeneratedSampleAtMs; + int previousCount = lastGeneratedCount; + lastGeneratedSampleAtMs = sampledAtMs; + lastGeneratedCount = safeGeneratedCount; + if (previousAt <= 0L || sampledAtMs <= previousAt) { + return 0D; + } + int generatedDelta = Math.max(0, safeGeneratedCount - previousCount); + return generatedDelta * 1000D / (sampledAtMs - previousAt); + } + + private void close() { + if (closed) { + return; + } + closed = true; + + ScheduledFuture activeMaintenance = maintenance; + maintenance = null; + if (activeMaintenance != null) { + activeMaintenance.cancel(false); + } + } + + private CompletableFuture closeGenerator() { + CompletableFuture runningMaintenance = activeMaintenance.get(); + if (runningMaintenance == null) { + return invokeGeneratorClose(); + } + return runningMaintenance.thenCompose(ignored -> invokeGeneratorClose()); + } + + private CompletableFuture invokeGeneratorClose() { + try { + CompletableFuture future = access.closeAsync(); + if (future == null) { + IllegalStateException exception = new IllegalStateException( + "Generator close returned no completion future for " + name); + reportFailure("Failed to close generator for " + name, exception); + return CompletableFuture.failedFuture(exception); + } + return future.whenComplete((ignored, failure) -> { + if (failure != null) { + Throwable cause = failure.getCause() == null ? failure : failure.getCause(); + reportFailure("Failed to close generator for " + name, cause); + } + }); + } catch (Throwable exception) { + reportFailure("Failed to close generator for " + name, exception); + return CompletableFuture.failedFuture(exception); + } + } + + private Engine getEngine() { + return closed ? null : access.getEngine(); + } + + private void clearMetricsFailure() { + metricsFailureReported.set(false); + } + + private void reportMetricsFailure(Throwable exception) { + if (metricsFailureReported.compareAndSet(false, true)) { + reportFailure("Failed to sample metrics for " + name, exception); + } + } + } + + private record Registration(String name, PlatformChunkGenerator access, + RegistrationIdentity registrationIdentity) { + } + + record RegistrationIdentity(String worldIdentity, Path worldFolder) { + RegistrationIdentity { + worldIdentity = Objects.requireNonNull(worldIdentity); + worldFolder = Objects.requireNonNull(worldFolder).toAbsolutePath().normalize(); + } + + private static RegistrationIdentity from(World world) { + return new RegistrationIdentity( + WorldIdentity.serialize(world), + world.getWorldFolder().toPath()); + } + + boolean conflictsWith(RegistrationIdentity candidate) { + return worldIdentity.equals(candidate.worldIdentity) + || worldFolder.equals(candidate.worldFolder); + } + } + + private record ClosingGenerator(RegistrationIdentity registrationIdentity, + CompletableFuture completion) { + } + + private record Teardown(World world, Registered registered, ClosingGenerator closing) { + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineStatus.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineStatus.java new file mode 100644 index 000000000..646c61e61 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineStatus.java @@ -0,0 +1,104 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.framework.MeteredCache; +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.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.volmlib.util.localization.MessageArgument; +final class IrisEngineStatus { + private IrisEngineStatus() { + } + + static void send(VolmitSender sender, Snapshot snapshot) { + CacheSummary caches = summarizeCaches(); + MaintenanceMetrics metrics = snapshot.metrics(); + + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MESSAGE)); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_STATUS)); + sender.sendMessage(IrisLanguage.text( + BukkitCommandMessages.IRIS_ENGINE_STATUS_SERVICE, + MessageArgument.trusted("value", status(snapshot.serviceRunning())) + )); + sender.sendMessage(IrisLanguage.text( + BukkitCommandMessages.IRIS_ENGINE_STATUS_METRICS, + MessageArgument.trusted("value", status(snapshot.metricsRunning())) + )); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MAINTENANCE_PERIOD, MessageArgument.untrusted("value", Form.duration(snapshot.maintenancePeriodMillis())))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_WORKER_PARALLELISM, MessageArgument.untrusted("value", snapshot.workerParallelism()))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_ACTIVE_WORLD_TASKS, MessageArgument.untrusted("value", metrics.activeTasks()))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_TECTONIC_PLATES)); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_CONFIGURED_RETENTION, MessageArgument.untrusted("value", Form.duration(snapshot.retentionMillis())))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_HEAP_USAGE, MessageArgument.untrusted("value", Form.pc(snapshot.heapUsage())))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_RESIDENT, MessageArgument.untrusted("value", metrics.residentTectonicPlates()))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_QUEUED, MessageArgument.untrusted("value", metrics.queuedTectonicPlates()))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_AVERAGE_IDLE_DURATION, MessageArgument.untrusted("value", Form.duration(metrics.averageIdleDuration(), 2)))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MAX_IDLE_DURATION, MessageArgument.untrusted("value", Form.duration(metrics.maxIdleDuration(), 2)))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MIN_IDLE_DURATION, MessageArgument.untrusted("value", Form.duration(metrics.minIdleDuration(), 2)))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_CACHES)); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_RESOURCE, MessageArgument.untrusted("value", caches.sizes()[0]), MessageArgument.untrusted("value2", caches.counts()[0]))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_2D_STREAM, MessageArgument.untrusted("value", caches.sizes()[1]), MessageArgument.untrusted("value2", caches.counts()[1]))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_3D_STREAM, MessageArgument.untrusted("value", caches.sizes()[2]), MessageArgument.untrusted("value2", caches.counts()[2]))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_OTHER, MessageArgument.untrusted("value", caches.sizes()[3]), MessageArgument.untrusted("value2", caches.counts()[3]))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_OTHER_2)); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_IRIS_WORLDS, MessageArgument.untrusted("value", metrics.worlds()))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_LOADED_CHUNKS, MessageArgument.untrusted("value", metrics.loadedChunks()))); + sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MESSAGE_2)); + } + + private static String status(boolean running) { + return IrisLanguage.text(running ? RuntimeUiMessages.STATUS_RUNNING : RuntimeUiMessages.STATUS_STOPPED); + } + + private static CacheSummary summarizeCaches() { + long[] sizes = new long[4]; + long[] counts = new long[4]; + PreservationSVC preservation = IrisServices.get(PreservationSVC.class); + List caches = preservation == null ? List.of() : preservation.getCaches(); + + for (MeteredCache cache : caches) { + int type = switch (cache) { + case ResourceLoader ignored -> 0; + case CachedStream2D ignored -> 1; + case CachedDoubleStream2D ignored -> 1; + case CachedStream3D ignored -> 2; + default -> 3; + }; + sizes[type] += cache.getSize(); + counts[type]++; + } + return new CacheSummary(sizes, counts); + } + + record Snapshot(boolean serviceRunning, + boolean metricsRunning, + long maintenancePeriodMillis, + int workerParallelism, + long retentionMillis, + double heapUsage, + MaintenanceMetrics metrics) { + } + + record MaintenanceMetrics(long residentTectonicPlates, + long queuedTectonicPlates, + long loadedChunks, + int worlds, + int activeTasks, + double averageIdleDuration, + double maxIdleDuration, + double minIdleDuration) { + static final MaintenanceMetrics EMPTY = new MaintenanceMetrics(0L, 0L, 0L, 0, 0, 0D, 0D, 0D); + } + + private record CacheSummary(long[] sizes, long[] counts) { + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisIntegrationService.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisIntegrationService.java new file mode 100644 index 000000000..c8804e983 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisIntegrationService.java @@ -0,0 +1,385 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.Iris; +import art.arcane.iris.engine.framework.EngineTelemetrySnapshot; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.volmlib.integration.IntegrationHandshakeRequest; +import art.arcane.volmlib.integration.IntegrationHandshakeResponse; +import art.arcane.volmlib.integration.IntegrationHeartbeat; +import art.arcane.volmlib.integration.IntegrationMetricDescriptor; +import art.arcane.volmlib.integration.IntegrationMetricGroup; +import art.arcane.volmlib.integration.IntegrationMetricSample; +import art.arcane.volmlib.integration.IntegrationMetricSchema; +import art.arcane.volmlib.integration.IntegrationProtocolNegotiator; +import art.arcane.volmlib.integration.IntegrationProtocolVersion; +import art.arcane.volmlib.integration.IntegrationServiceContract; +import org.bukkit.Bukkit; +import org.bukkit.plugin.ServicePriority; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +public class IrisIntegrationService implements IrisService, IntegrationServiceContract { + private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2); + private static final Set SUPPORTED_PROTOCOLS = Set.of( + new IntegrationProtocolVersion(1, 0), + new IntegrationProtocolVersion(1, 1), + CURRENT_PROTOCOL + ); + private static final Set CAPABILITIES = Set.of( + "handshake", + "heartbeat", + "metrics", + "metric-groups", + "iris-engine-metrics", + "iris-world-metrics" + ); + private static final Map TIMING_KEYS = Map.ofEntries( + Map.entry("total", IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS), + Map.entry("updates", IntegrationMetricSchema.IRIS_GENERATION_UPDATES_MS), + Map.entry("terrain", IntegrationMetricSchema.IRIS_GENERATION_TERRAIN_MS), + Map.entry("biome", IntegrationMetricSchema.IRIS_GENERATION_BIOME_MS), + Map.entry("post", IntegrationMetricSchema.IRIS_GENERATION_POST_MS), + Map.entry("perfection", IntegrationMetricSchema.IRIS_GENERATION_PERFECTION_MS), + Map.entry("decoration", IntegrationMetricSchema.IRIS_GENERATION_DECORATION_MS), + Map.entry("cave", IntegrationMetricSchema.IRIS_GENERATION_CAVE_MS), + Map.entry("deposit", IntegrationMetricSchema.IRIS_GENERATION_DEPOSIT_MS), + Map.entry("carve.resolve", IntegrationMetricSchema.IRIS_GENERATION_CARVE_RESOLVE_MS), + Map.entry("carve.apply", IntegrationMetricSchema.IRIS_GENERATION_CARVE_APPLY_MS), + Map.entry("context.prefill", IntegrationMetricSchema.IRIS_GENERATION_CONTEXT_PREFILL_MS), + Map.entry("pregen.wait.permit", IntegrationMetricSchema.IRIS_PREGEN_WAIT_PERMIT_MS), + Map.entry("pregen.wait.adaptive", IntegrationMetricSchema.IRIS_PREGEN_WAIT_ADAPTIVE_MS) + ); + + private final Supplier telemetrySupplier; + + public IrisIntegrationService() { + this(IrisIntegrationService::currentTelemetry); + } + + IrisIntegrationService(Supplier telemetrySupplier) { + this.telemetrySupplier = telemetrySupplier == null + ? () -> IrisTelemetrySnapshot.EMPTY + : telemetrySupplier; + } + + @Override + public void onEnable() { + Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal); + Iris.verbose("Integration provider registered for Iris"); + } + + @Override + public void onDisable() { + Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this); + } + + @Override + public String pluginId() { + return "iris"; + } + + @Override + public String pluginVersion() { + return Iris.instance.getDescription().getVersion(); + } + + @Override + public Set supportedProtocols() { + return SUPPORTED_PROTOCOLS; + } + + @Override + public Set capabilities() { + return CAPABILITIES; + } + + @Override + public Set metricDescriptors() { + Set descriptors = new LinkedHashSet<>(); + for (String key : IntegrationMetricSchema.irisKeys()) { + descriptors.add(IntegrationMetricSchema.descriptor(key)); + } + return Set.copyOf(descriptors); + } + + @Override + public IntegrationHandshakeResponse handshake(IntegrationHandshakeRequest request) { + long now = System.currentTimeMillis(); + if (request == null) { + return new IntegrationHandshakeResponse( + pluginId(), + pluginVersion(), + false, + null, + SUPPORTED_PROTOCOLS, + CAPABILITIES, + "missing request", + now + ); + } + + Optional negotiated = IntegrationProtocolNegotiator.negotiate( + SUPPORTED_PROTOCOLS, + request.supportedProtocols() + ); + if (negotiated.isEmpty()) { + return new IntegrationHandshakeResponse( + pluginId(), + pluginVersion(), + false, + null, + SUPPORTED_PROTOCOLS, + CAPABILITIES, + "no-common-protocol", + now + ); + } + + return new IntegrationHandshakeResponse( + pluginId(), + pluginVersion(), + true, + negotiated.get(), + SUPPORTED_PROTOCOLS, + CAPABILITIES, + "ok", + now + ); + } + + @Override + public IntegrationHeartbeat heartbeat() { + return new IntegrationHeartbeat(CURRENT_PROTOCOL, true, System.currentTimeMillis(), "ok"); + } + + @Override + public Map sampleMetrics(Set metricKeys) { + Set requested = metricKeys == null || metricKeys.isEmpty() + ? IntegrationMetricSchema.irisKeys() + : Set.copyOf(metricKeys); + IrisTelemetrySnapshot snapshot = safeSnapshot(); + long sampledAtMs = snapshot.sampledAtMs() > 0L + ? snapshot.sampledAtMs() + : System.currentTimeMillis(); + if (snapshot.sampledAtMs() <= 0L) { + return unavailableMetrics(requested, "telemetry-not-ready", sampledAtMs); + } + + Map available = globalSamples(snapshot); + Map selected = new LinkedHashMap<>(requested.size()); + for (String key : requested) { + IntegrationMetricSample sample = available.get(key); + selected.put(key, sample == null + ? unavailable(key, "unsupported-key", sampledAtMs) + : sample); + } + return Map.copyOf(selected); + } + + @Override + public List metricGroups() { + IrisTelemetrySnapshot snapshot = safeSnapshot(); + if (snapshot.sampledAtMs() <= 0L || snapshot.worlds().isEmpty()) { + return List.of(); + } + + List worlds = new ArrayList<>(snapshot.worlds()); + worlds.sort(Comparator.comparing(EngineTelemetrySnapshot::worldIdentity)); + List groups = new ArrayList<>(worlds.size()); + for (EngineTelemetrySnapshot world : worlds) { + groups.add(new IntegrationMetricGroup( + "world", + world.worldIdentity(), + world.worldName(), + Map.of( + "plugin", "iris", + "dimension", world.dimensionKey() + ), + worldSamples(world, snapshot.pregenerator()) + )); + } + return List.copyOf(groups); + } + + private Map globalSamples(IrisTelemetrySnapshot snapshot) { + long sampledAtMs = snapshot.sampledAtMs(); + EngineTelemetrySnapshot.Aggregate aggregate = snapshot.aggregate(); + Map samples = new LinkedHashMap<>(); + put(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT, aggregate.worldCount(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, aggregate.active(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, aggregate.closing() + snapshot.closingEngines(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, aggregate.failed(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, aggregate.studio(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS, snapshot.pendingRegistrations(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, aggregate.loadedChunks(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, aggregate.loadedEntities(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, aggregate.entitySaturationMax(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, aggregate.generatedSession(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, aggregate.generatedTotal(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, aggregate.chunksPerSecond(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, aggregate.blockUpdatesPerSecond(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, aggregate.parallelism(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, aggregate.activeGenerationLeases(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, aggregate.hotloadsTotal(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_ACTIVE_TASKS, snapshot.maintenanceActiveTasks(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_WORKERS, snapshot.maintenanceWorkers(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, aggregate.mantleResidentPlates(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, aggregate.mantleQueuedPlates(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, aggregate.mantleIdleAverageMs(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MAX_MS, aggregate.mantleIdleMaxMs(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MIN_MS, aggregate.mantleIdleMinMs(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_HEAP_USAGE, snapshot.heapUsage(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_RECLAIM_URGENCY, snapshot.reclaimUrgency(), sampledAtMs); + putCacheSamples(samples, snapshot.caches(), sampledAtMs); + putPregenSamples(samples, snapshot.pregenerator(), sampledAtMs); + putTimingSamples(samples, aggregate.generationTimingMaximaMs(), sampledAtMs); + return Map.copyOf(samples); + } + + private Map worldSamples( + EngineTelemetrySnapshot world, + IrisTelemetrySnapshot.PregenSnapshot pregenerator + ) { + long sampledAtMs = world.sampledAtMs(); + Map samples = new LinkedHashMap<>(); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, world.active() ? 1 : 0, sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, world.closing() ? 1 : 0, sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, world.failed() ? 1 : 0, sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, world.studio() ? 1 : 0, sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, world.loadedChunks(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, world.loadedEntities(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, world.entitySaturation(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, world.generatedSession(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, world.generatedTotal(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, world.chunksPerSecond(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, world.blockUpdatesPerSecond(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, world.parallelism(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, world.activeGenerationLeases(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, world.hotloadsTotal(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, world.mantleResidentPlates(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, world.mantleQueuedPlates(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, world.mantleIdleMs(), sampledAtMs); + boolean targetsWorld = pregenerator.active() + && world.worldIdentity().equals(pregenerator.worldIdentity()); + putPregenSamples(samples, targetsWorld ? pregenerator : IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, sampledAtMs); + putTimingSamples(samples, world.generationTimingsMs(), sampledAtMs); + samples.keySet().retainAll(IntegrationMetricSchema.irisWorldKeys()); + return Map.copyOf(samples); + } + + private void putCacheSamples( + Map samples, + IrisTelemetrySnapshot.CacheSnapshot caches, + long sampledAtMs + ) { + putCacheBucket(samples, caches.total(), IntegrationMetricSchema.IRIS_CACHE_COUNT, IntegrationMetricSchema.IRIS_CACHE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_USAGE, sampledAtMs); + putCacheBucket(samples, caches.resource(), IntegrationMetricSchema.IRIS_CACHE_RESOURCE_COUNT, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_USAGE, sampledAtMs); + putCacheBucket(samples, caches.stream2d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_USAGE, sampledAtMs); + putCacheBucket(samples, caches.stream3d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_USAGE, sampledAtMs); + putCacheBucket(samples, caches.other(), IntegrationMetricSchema.IRIS_CACHE_OTHER_COUNT, IntegrationMetricSchema.IRIS_CACHE_OTHER_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_OTHER_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_OTHER_USAGE, sampledAtMs); + } + + private void putCacheBucket( + Map samples, + IrisTelemetrySnapshot.CacheBucket bucket, + String countKey, + String entriesKey, + String capacityKey, + String usageKey, + long sampledAtMs + ) { + put(samples, countKey, bucket.count(), sampledAtMs); + put(samples, entriesKey, bucket.entries(), sampledAtMs); + put(samples, capacityKey, bucket.capacity(), sampledAtMs); + put(samples, usageKey, bucket.usage(), sampledAtMs); + } + + private void putPregenSamples( + Map samples, + IrisTelemetrySnapshot.PregenSnapshot pregenerator, + long sampledAtMs + ) { + put(samples, IntegrationMetricSchema.IRIS_PREGEN_ACTIVE, pregenerator.active() ? 1 : 0, sampledAtMs); + if (!pregenerator.active()) { + for (String key : Set.of( + IntegrationMetricSchema.IRIS_PREGEN_PAUSED, + IntegrationMetricSchema.IRIS_PREGEN_PROGRESS, + IntegrationMetricSchema.IRIS_PREGEN_GENERATED, + IntegrationMetricSchema.IRIS_PREGEN_TOTAL, + IntegrationMetricSchema.IRIS_PREGEN_QUEUE, + IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, + IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, + IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS, + IntegrationMetricSchema.IRIS_PREGEN_FAILED)) { + samples.put(key, unavailable(key, "pregen-inactive", sampledAtMs)); + } + return; + } + + put(samples, IntegrationMetricSchema.IRIS_PREGEN_PAUSED, pregenerator.paused() ? 1 : 0, sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_PREGEN_PROGRESS, pregenerator.progressPercent(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_PREGEN_TOTAL, pregenerator.total(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.remaining(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, pregenerator.chunksPerSecond(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, pregenerator.etaMs(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS, pregenerator.elapsedMs(), sampledAtMs); + put(samples, IntegrationMetricSchema.IRIS_PREGEN_FAILED, pregenerator.failed(), sampledAtMs); + } + + private void putTimingSamples( + Map samples, + Map timings, + long sampledAtMs + ) { + for (Map.Entry entry : TIMING_KEYS.entrySet()) { + Double value = timings.get(entry.getKey()); + samples.put(entry.getValue(), value == null + ? unavailable(entry.getValue(), "timing-not-available", sampledAtMs) + : available(entry.getValue(), value, sampledAtMs)); + } + } + + private void put(Map samples, String key, double value, long sampledAtMs) { + samples.put(key, available(key, value, sampledAtMs)); + } + + private IntegrationMetricSample available(String key, double value, long sampledAtMs) { + return IntegrationMetricSample.available(IntegrationMetricSchema.descriptor(key), value, sampledAtMs); + } + + private IntegrationMetricSample unavailable(String key, String reason, long sampledAtMs) { + return IntegrationMetricSample.unavailable(IntegrationMetricSchema.descriptor(key), reason, sampledAtMs); + } + + private Map unavailableMetrics( + Set keys, + String reason, + long sampledAtMs + ) { + Map samples = new LinkedHashMap<>(keys.size()); + for (String key : keys) { + samples.put(key, unavailable(key, reason, sampledAtMs)); + } + return Map.copyOf(samples); + } + + private IrisTelemetrySnapshot safeSnapshot() { + IrisTelemetrySnapshot snapshot = telemetrySupplier.get(); + return snapshot == null ? IrisTelemetrySnapshot.EMPTY : snapshot; + } + + private static IrisTelemetrySnapshot currentTelemetry() { + IrisEngineSVC service = IrisServices.get(IrisEngineSVC.class); + return service == null ? IrisTelemetrySnapshot.EMPTY : service.telemetrySnapshot(); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java new file mode 100644 index 000000000..af6687e74 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java @@ -0,0 +1,220 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.protocol.EngineResolver; +import art.arcane.iris.core.protocol.IrisProtocolServer; +import art.arcane.iris.core.protocol.IrisServerTransport; +import art.arcane.iris.core.protocol.IrisSession; +import art.arcane.iris.core.protocol.IrisSessionRegistry; +import art.arcane.iris.core.protocol.IrisVisionRequestService; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisProtocol; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.messaging.Messenger; +import org.bukkit.plugin.messaging.PluginMessageListener; + +import java.util.UUID; + +public class IrisProtocolService implements IrisService, PluginMessageListener, IrisServerTransport { + private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN + | IrisProtocol.CAPABILITY_VISION + | IrisProtocol.CAPABILITY_CURSOR + | IrisProtocol.CAPABILITY_STUDIO; + private static final int DIMENSION_SYNC_RETRY_TICKS = 20; + private static final int DIMENSION_SYNC_MAX_ATTEMPTS = 15; + + private IrisSessionRegistry registry; + private IrisProtocolServer protocolServer; + private IrisVisionRequestService visionService; + + @Override + public void onEnable() { + Messenger messenger = Bukkit.getMessenger(); + messenger.registerOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL); + messenger.registerIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this); + registry = new IrisSessionRegistry(); + protocolServer = new IrisProtocolServer(registry, SERVER_CAPABILITIES, brand(), true); + EngineResolver engineResolver = IrisProtocolService::resolveEngine; + protocolServer.setEngineResolver(engineResolver); + visionService = IrisVisionRequestService.create(engineResolver, registry); + protocolServer.setVisionTileHandler(visionService); + IrisServices.register(IrisProtocolServer.class, protocolServer); + for (Player player : Bukkit.getOnlinePlayers()) { + registry.register(new IrisSession(player.getUniqueId().toString(), this)); + } + } + + @Override + public void onDisable() { + IrisServices.remove(IrisProtocolServer.class); + Messenger messenger = Bukkit.getMessenger(); + messenger.unregisterIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this); + messenger.unregisterOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL); + IrisSessionRegistry current = registry; + if (current != null) { + for (IrisSession session : current.all()) { + current.unregister(session.id()); + } + } + registry = null; + protocolServer = null; + visionService = null; + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + IrisSessionRegistry current = registry; + if (current == null) { + return; + } + current.register(new IrisSession(event.getPlayer().getUniqueId().toString(), this)); + syncDimension(event.getPlayer(), DIMENSION_SYNC_MAX_ATTEMPTS); + } + + @EventHandler + public void onPlayerChangedWorld(PlayerChangedWorldEvent event) { + if (registry == null) { + return; + } + syncDimension(event.getPlayer(), 1); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + IrisSessionRegistry current = registry; + if (current == null) { + return; + } + String sessionId = event.getPlayer().getUniqueId().toString(); + current.unregister(sessionId); + IrisVisionRequestService vision = visionService; + if (vision != null) { + vision.clearSession(sessionId); + } + } + + @Override + public void onPluginMessageReceived(String channel, Player player, byte[] message) { + IrisProtocolServer current = protocolServer; + if (current == null || !IrisProtocol.CHANNEL.equals(channel)) { + return; + } + current.onClientFrame(player.getUniqueId().toString(), message); + } + + @Override + public void sendToClient(String sessionId, byte[] frame) { + if (sessionId == null || frame == null) { + return; + } + UUID playerId = parseUuid(sessionId); + if (playerId == null) { + return; + } + Player player = Bukkit.getPlayer(playerId); + if (player == null || !player.isOnline()) { + return; + } + J.runEntity(player, () -> deliver(player, frame)); + } + + private void deliver(Player player, byte[] frame) { + if (!player.isOnline() || !player.getListeningPluginChannels().contains(IrisProtocol.CHANNEL)) { + return; + } + player.sendPluginMessage(Iris.instance, IrisProtocol.CHANNEL, frame); + } + + private void syncDimension(Player player, int attemptsLeft) { + IrisProtocolServer protocol = protocolServer; + IrisSessionRegistry current = registry; + if (protocol == null || current == null || !player.isOnline()) { + return; + } + String sessionId = player.getUniqueId().toString(); + IrisSession session = current.get(sessionId); + if (session == null) { + return; + } + if (!session.isReady()) { + if (attemptsLeft > 1) { + J.runEntity(player, () -> syncDimension(player, attemptsLeft - 1), DIMENSION_SYNC_RETRY_TICKS); + } + return; + } + World world = player.getWorld(); + Engine engine = engineFor(world); + if (engine != null) { + protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(), + world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true); + return; + } + protocol.sendDimensionStatus(sessionId, WorldIdentity.serialize(world), "", world.getSeed(), world.getMinHeight(), world.getMaxHeight(), false); + } + + private static Engine resolveEngine(String sessionId) { + UUID playerId = parseUuid(sessionId); + if (playerId == null) { + return null; + } + Player player = Bukkit.getPlayer(playerId); + if (player == null || !player.isOnline()) { + return null; + } + return engineFor(player.getWorld()); + } + + private static Engine engineFor(World world) { + PlatformChunkGenerator access = IrisToolbelt.access(world); + if (access == null) { + return null; + } + Engine engine = access.getEngine(); + if (engine == null || engine.isClosed()) { + return null; + } + return engine; + } + + private static UUID parseUuid(String sessionId) { + try { + return UUID.fromString(sessionId); + } catch (IllegalArgumentException invalid) { + return null; + } + } + + private static String brand() { + return "Iris " + Iris.instance.getDescription().getVersion() + " (Bukkit)"; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTelemetrySnapshot.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTelemetrySnapshot.java new file mode 100644 index 000000000..25cfea29b --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTelemetrySnapshot.java @@ -0,0 +1,140 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.engine.framework.EngineTelemetrySnapshot; + +import java.util.List; + +record IrisTelemetrySnapshot( + long sampledAtMs, + List worlds, + EngineTelemetrySnapshot.Aggregate aggregate, + int maintenanceActiveTasks, + int maintenanceWorkers, + int closingEngines, + int pendingRegistrations, + double heapUsage, + double reclaimUrgency, + CacheSnapshot caches, + PregenSnapshot pregenerator +) { + static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot( + 0L, + List.of(), + EngineTelemetrySnapshot.aggregate(List.of()), + 0, + 0, + 0, + 0, + 0D, + 0D, + CacheSnapshot.EMPTY, + PregenSnapshot.INACTIVE + ); + + IrisTelemetrySnapshot { + worlds = worlds == null ? List.of() : List.copyOf(worlds); + aggregate = aggregate == null ? EngineTelemetrySnapshot.aggregate(worlds) : aggregate; + maintenanceActiveTasks = Math.max(0, maintenanceActiveTasks); + maintenanceWorkers = Math.max(0, maintenanceWorkers); + closingEngines = Math.max(0, closingEngines); + pendingRegistrations = Math.max(0, pendingRegistrations); + heapUsage = finiteRatio(heapUsage); + reclaimUrgency = finiteRatio(reclaimUrgency); + caches = caches == null ? CacheSnapshot.EMPTY : caches; + pregenerator = pregenerator == null ? PregenSnapshot.INACTIVE : pregenerator; + } + + private static double finiteRatio(double value) { + return Double.isFinite(value) ? Math.max(0D, Math.min(1D, value)) : 0D; + } + + record CacheSnapshot( + CacheBucket total, + CacheBucket resource, + CacheBucket stream2d, + CacheBucket stream3d, + CacheBucket other + ) { + static final CacheSnapshot EMPTY = new CacheSnapshot( + CacheBucket.EMPTY, + CacheBucket.EMPTY, + CacheBucket.EMPTY, + CacheBucket.EMPTY, + CacheBucket.EMPTY + ); + + CacheSnapshot { + total = total == null ? CacheBucket.EMPTY : total; + resource = resource == null ? CacheBucket.EMPTY : resource; + stream2d = stream2d == null ? CacheBucket.EMPTY : stream2d; + stream3d = stream3d == null ? CacheBucket.EMPTY : stream3d; + other = other == null ? CacheBucket.EMPTY : other; + } + } + + record CacheBucket(int count, long entries, long capacity) { + static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L); + + CacheBucket { + count = Math.max(0, count); + entries = Math.max(0L, entries); + capacity = Math.max(0L, capacity); + } + + double usage() { + return capacity <= 0L ? 0D : Math.min(1D, entries / (double) capacity); + } + + CacheBucket plus(CacheBucket other) { + if (other == null) { + return this; + } + return new CacheBucket(count + other.count, entries + other.entries, capacity + other.capacity); + } + } + + record PregenSnapshot( + boolean active, + String worldIdentity, + String worldName, + boolean paused, + double progressPercent, + long generated, + long total, + long remaining, + double chunksPerSecond, + long etaMs, + long elapsedMs, + long failed + ) { + static final PregenSnapshot INACTIVE = new PregenSnapshot( + false, + "", + "", + false, + 0D, + 0L, + 0L, + 0L, + 0D, + 0L, + 0L, + 0L + ); + + PregenSnapshot { + worldIdentity = worldIdentity == null ? "" : worldIdentity.trim(); + worldName = worldName == null ? "" : worldName.trim(); + progressPercent = Double.isFinite(progressPercent) + ? Math.max(0D, Math.min(100D, progressPercent)) + : 0D; + generated = Math.max(0L, generated); + total = Math.max(0L, total); + remaining = Math.max(0L, remaining); + chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D; + etaMs = Math.max(0L, etaMs); + elapsedMs = Math.max(0L, elapsedMs); + failed = Math.max(0L, failed); + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java new file mode 100644 index 000000000..ee1ed09ec --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java @@ -0,0 +1,316 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.terrain.IrisColumnField; +import art.arcane.iris.api.terrain.IrisColumnQuery; +import art.arcane.iris.api.terrain.IrisColumnSink; +import art.arcane.iris.api.terrain.IrisSurfaceKind; +import art.arcane.iris.api.terrain.IrisTerrainService; +import art.arcane.iris.api.terrain.IrisWorldInfo; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.service.terrain.IrisApiFaultGuard; +import art.arcane.iris.core.service.terrain.IrisColumnWalk; +import art.arcane.iris.core.service.terrain.IrisSampleLimits; +import art.arcane.iris.core.service.terrain.IrisSurfaceClassifier; +import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.InferredType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.plugin.IrisService; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.plugin.ServicePriority; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.concurrent.atomic.AtomicBoolean; + +public class IrisTerrainSVC implements IrisService, IrisTerrainService { + private static final long FAULT_REPORT_INTERVAL_MILLIS = 60_000L; + + private final AtomicBoolean serviceEnabled = new AtomicBoolean(); + private final IrisApiFaultGuard queryFaults = new IrisApiFaultGuard(FAULT_REPORT_INTERVAL_MILLIS); + private final IrisApiFaultGuard sinkFaults = new IrisApiFaultGuard(FAULT_REPORT_INTERVAL_MILLIS); + + @Override + public void onEnable() { + serviceEnabled.set(true); + Bukkit.getServicesManager().register( + IrisTerrainService.class, + this, + BukkitPlatform.plugin(), + ServicePriority.Normal + ); + IrisServices.register(IrisTerrainService.class, this); + } + + @Override + public void onDisable() { + serviceEnabled.set(false); + Bukkit.getServicesManager().unregister(IrisTerrainService.class, this); + IrisServices.remove(IrisTerrainService.class); + } + + @Override + public boolean isIrisWorld(World world) { + return generatorOf(world) != null; + } + + @Override + public Optional worldInfo(World world) { + PlatformChunkGenerator generator = liveGeneratorOf(world); + if (generator == null) { + return Optional.empty(); + } + + try { + return Optional.ofNullable(IrisWorldInfoFactory.from(generator)); + } catch (Throwable error) { + reportQueryFault("worldInfo", world, error); + return Optional.empty(); + } + } + + @Override + public OptionalInt surfaceHeight(World world, int blockX, int blockZ) { + Engine engine = liveEngineOf(world); + if (engine == null) { + return OptionalInt.empty(); + } + + try { + return OptionalInt.of(engine.getHeight(blockX, blockZ) + engine.getMinHeight()); + } catch (Throwable error) { + reportQueryFault("surfaceHeight", world, error); + return OptionalInt.empty(); + } + } + + @Override + public IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ) { + Engine engine = liveEngineOf(world); + if (engine == null) { + return IrisSurfaceKind.UNKNOWN; + } + + try { + int surface = engine.getHeight(blockX, blockZ); + int fluid = engine.getDimension().getFluidHeight(); + InferredType inferredType = null; + if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) { + IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ); + inferredType = biome == null ? null : biome.getInferredType(); + } + return IrisSurfaceClassifier.classify(surface, fluid, inferredType); + } catch (Throwable error) { + reportQueryFault("surfaceKind", world, error); + return IrisSurfaceKind.UNKNOWN; + } + } + + @Override + public Optional surfaceBiomeKey(World world, int blockX, int blockZ) { + Engine engine = liveEngineOf(world); + if (engine == null) { + return Optional.empty(); + } + + try { + return key(engine.getSurfaceBiome(blockX, blockZ)); + } catch (Throwable error) { + reportQueryFault("surfaceBiomeKey", world, error); + return Optional.empty(); + } + } + + @Override + public Optional surfaceBiomeName(World world, int blockX, int blockZ) { + Engine engine = liveEngineOf(world); + if (engine == null) { + return Optional.empty(); + } + + try { + return name(engine.getSurfaceBiome(blockX, blockZ)); + } catch (Throwable error) { + reportQueryFault("surfaceBiomeName", world, error); + return Optional.empty(); + } + } + + @Override + public Optional biomeKey(World world, int blockX, int blockY, int blockZ) { + Engine engine = liveEngineOf(world); + if (engine == null) { + return Optional.empty(); + } + + try { + return key(engine.getBiome(blockX, blockY - engine.getMinHeight(), blockZ)); + } catch (Throwable error) { + reportQueryFault("biomeKey", world, error); + return Optional.empty(); + } + } + + @Override + public Optional regionKey(World world, int blockX, int blockZ) { + Engine engine = liveEngineOf(world); + if (engine == null) { + return Optional.empty(); + } + + try { + IrisRegion region = engine.getRegion(blockX, blockZ); + String loadKey = region == null ? null : region.getLoadKey(); + return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey); + } catch (Throwable error) { + reportQueryFault("regionKey", world, error); + return Optional.empty(); + } + } + + @Override + public Optional regionName(World world, int blockX, int blockZ) { + Engine engine = liveEngineOf(world); + if (engine == null) { + return Optional.empty(); + } + + try { + IrisRegion region = engine.getRegion(blockX, blockZ); + String name = region == null ? null : region.getName(); + return name == null || name.isEmpty() ? Optional.empty() : Optional.of(name); + } catch (Throwable error) { + reportQueryFault("regionName", world, error); + return Optional.empty(); + } + } + + @Override + public int maxSampleColumns() { + return IrisSampleLimits.maxColumns(noiseCacheChunks()); + } + + @Override + public int maxSampleChunks() { + return IrisSampleLimits.maxChunks(noiseCacheChunks()); + } + + @Override + public boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink) { + if (query == null || sink == null) { + return false; + } + + Engine engine = liveEngineOf(world); + if (engine == null) { + return false; + } + + int noiseCacheChunks = noiseCacheChunks(); + if (!IrisSampleLimits.withinLimits( + query, + IrisSampleLimits.maxColumns(noiseCacheChunks), + IrisSampleLimits.maxChunks(noiseCacheChunks))) { + return false; + } + + EnumSet fields = query.fields(); + boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT); + boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND); + boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY); + + try { + int minHeight = engine.getMinHeight(); + int fluid = engine.getDimension().getFluidHeight(); + long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> { + if (engine.isClosed()) { + return false; + } + + int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0; + boolean needsBiome = wantBiome + || (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)); + IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null; + IrisSurfaceKind kind = wantKind + ? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType()) + : IrisSurfaceKind.UNKNOWN; + String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null; + sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey); + return true; + }); + return visited == query.columnCount(); + } catch (Throwable error) { + reportSinkFault(world, error); + return false; + } + } + + private static Optional key(IrisBiome biome) { + String loadKey = biome == null ? null : biome.getLoadKey(); + return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey); + } + + private static Optional name(IrisBiome biome) { + String name = biome == null ? null : biome.getName(); + return name == null || name.isEmpty() ? Optional.empty() : Optional.of(name); + } + + private static int noiseCacheChunks() { + return IrisSettings.get().getPerformance().getNoiseCacheSize(); + } + + static boolean answerable(boolean serviceEnabled, boolean worldPresent) { + return serviceEnabled && worldPresent; + } + + private PlatformChunkGenerator generatorOf(World world) { + if (!answerable(serviceEnabled.get(), world != null)) { + return null; + } + + ChunkGenerator generator = world.getGenerator(); + return generator instanceof PlatformChunkGenerator platform ? platform : null; + } + + private PlatformChunkGenerator liveGeneratorOf(World world) { + PlatformChunkGenerator generator = generatorOf(world); + return generator == null || generator.isClosing() ? null : generator; + } + + private static Engine engineOf(PlatformChunkGenerator generator) { + if (generator == null) { + return null; + } + + Engine engine = generator.getEngine(); + return engine == null || engine.isClosed() ? null : engine; + } + + private Engine liveEngineOf(World world) { + return engineOf(liveGeneratorOf(world)); + } + + private void reportQueryFault(String operation, World world, Throwable error) { + if (queryFaults.record(System.currentTimeMillis())) { + IrisLogging.reportError("Iris terrain API query \"" + operation + "\" failed for world \"" + + (world == null ? "null" : world.getName()) + "\" (" + queryFaults.faults() + + " terrain API query faults so far).", error); + } + } + + private void reportSinkFault(World world, Throwable error) { + if (sinkFaults.record(System.currentTimeMillis())) { + IrisLogging.reportError("Iris terrain API column sample failed for world \"" + + (world == null ? "null" : world.getName()) + "\" (" + sinkFaults.faults() + + " terrain API sample faults so far). A third-party sink that throws is treated as a refusal.", error); + } + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisWorldPhaseLedger.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisWorldPhaseLedger.java new file mode 100644 index 000000000..76a2d3849 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisWorldPhaseLedger.java @@ -0,0 +1,49 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.world.IrisWorldPhase; +import org.bukkit.World; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +final class IrisWorldPhaseLedger { + @FunctionalInterface + interface Dispatch { + void fire(World world, IrisWorldPhase phase); + } + + private final Set announced = ConcurrentHashMap.newKeySet(); + private final Dispatch dispatch; + + IrisWorldPhaseLedger() { + this(IrisApiEventSVC::fireWorldPhase); + } + + IrisWorldPhaseLedger(Dispatch dispatch) { + this.dispatch = Objects.requireNonNull(dispatch, "dispatch"); + } + + void ready(World world) { + UUID identity = identityOf(world); + if (identity == null || !announced.add(identity)) { + return; + } + + dispatch.fire(world, IrisWorldPhase.ENGINE_READY); + } + + void closing(World world) { + UUID identity = identityOf(world); + if (identity == null || !announced.remove(identity)) { + return; + } + + dispatch.fire(world, IrisWorldPhase.ENGINE_CLOSING); + } + + private static UUID identityOf(World world) { + return world == null ? null : world.getUID(); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/RoutedBlockBreakEvent.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/RoutedBlockBreakEvent.java new file mode 100644 index 000000000..a5c39a796 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/RoutedBlockBreakEvent.java @@ -0,0 +1,22 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.service.tree.BlockDropRouter; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.block.BlockBreakEvent; + +final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter { + private final TreeFellerSVC service; + private final FellingRun run; + + RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) { + super(block, player); + this.run = run; + this.service = service; + } + + @Override + public boolean routeDrop(Object drop) { + return service.isServiceEnabled() && run.presentation.routeDrop(drop); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerModel.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerModel.java new file mode 100644 index 000000000..180c51b97 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerModel.java @@ -0,0 +1,81 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.tree.TreeFellerAccess; +import art.arcane.iris.api.tree.TreeFellerRunHooks; +import art.arcane.iris.core.service.tree.TreeMarkerTraversal; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.UUID; + +final class TreeFellerModel { + private TreeFellerModel() { + } + + record PendingFell( + TreeCandidate candidate, + int preservationChance, + TreeFellerRunHooks runHooks + ) { + PendingFell withAccess(TreeFellerAccess access) { + return new PendingFell(candidate.withAccess(access), preservationChance, runHooks); + } + } + + record TreeCandidate( + TreeContext context, + World world, + Player player, + ItemStack tool, + TreeFellerAccess access, + TreeMarkerTraversal.Position trigger + ) { + TreeCandidate withAccess(TreeFellerAccess access) { + return new TreeCandidate(context, world, player, tool, access, trigger); + } + } + + record TreeContext( + Engine engine, + String marker, + TreeBlockMaterial expectedMaterial, + int minimumY, + int maximumY + ) { + } + + record TreeClaim(UUID worldId, String marker) { + } + + record ProvenanceSnapshot( + Engine engine, + World world, + int minimumY, + TreeMarkerTraversal.Position position, + String marker, + TreeBlockMaterial material + ) { + } + + record ChunkPosition(int x, int z) { + } + + record TreeMember( + TreeMarkerTraversal.Position position, + boolean log, + TreeBlockMaterial expectedMaterial, + int erosionOrder + ) { + } + + record DamageReservation( + ItemStack toolForDrops, + boolean charged, + boolean broke, + boolean logCostReserved + ) { + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerPresentation.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerPresentation.java new file mode 100644 index 000000000..9b64a287d --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerPresentation.java @@ -0,0 +1,308 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.ExperienceOrb; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +final class TreeFellerPresentation { + private static final int MIN_BLOCKS_PER_PULSE = 4; + private static final int MAX_BLOCKS_PER_PULSE = 64; + private static final int TARGET_EROSION_PULSES = 60; + private static final int MAX_EFFECT_ORIGINS_PER_PULSE = 16; + + private final Player player; + private final World sourceWorld; + private final Queue pendingDrops = new ConcurrentLinkedQueue<>(); + private final AtomicBoolean flushScheduled = new AtomicBoolean(); + private final AtomicBoolean fallbackScheduled = new AtomicBoolean(); + private final AtomicBoolean completed = new AtomicBoolean(); + private final AtomicBoolean effectFailureReported = new AtomicBoolean(); + private final AtomicBoolean deliveryFailureReported = new AtomicBoolean(); + private final AtomicInteger deliveryPulses = new AtomicInteger(); + private volatile Location fallbackLocation; + + TreeFellerPresentation(Player player, World sourceWorld, Location fallbackLocation) { + this.player = player; + this.sourceWorld = sourceWorld; + this.fallbackLocation = fallbackLocation.clone(); + } + + static int blocksPerPulse(int blockCount) { + int requested = Math.max(1, (blockCount + TARGET_EROSION_PULSES - 1) / TARGET_EROSION_PULSES); + return Math.max(MIN_BLOCKS_PER_PULSE, Math.min(requested, MAX_BLOCKS_PER_PULSE)); + } + + static int effectStride(int blocksPerPulse) { + return Math.max( + 1, + (blocksPerPulse + MAX_EFFECT_ORIGINS_PER_PULSE - 1) / MAX_EFFECT_ORIGINS_PER_PULSE + ); + } + + static List consolidateDrops(Collection drops) { + List consolidated = new ArrayList<>(); + for (ItemStack drop : drops) { + mergeDrop(consolidated, drop); + } + return List.copyOf(consolidated); + } + + void activate(Block block) { + try { + Location center = block.getLocation().clone().add(0.5D, 0.5D, 0.5D); + World world = block.getWorld(); + world.spawnParticle(Particle.ENCHANT, center, 24, 0.45D, 0.45D, 0.45D, 0.18D); + world.spawnParticle(Particle.END_ROD, center, 8, 0.25D, 0.25D, 0.25D, 0.035D); + world.playSound(center, Sound.BLOCK_ENCHANTMENT_TABLE_USE, 0.55F, 1.35F); + world.playSound(center, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.4F, 0.8F); + } catch (Throwable error) { + reportEffectFailure(error); + } + } + + void erode( + Location source, + BlockData visualData, + int erosionOrder, + int processed, + int blocksPerPulse, + int effectStride, + int totalBlocks + ) { + if (processed % effectStride != 0 || source.getWorld() == null) { + return; + } + try { + World world = source.getWorld(); + world.spawnParticle(Particle.BLOCK, source, 5, 0.3D, 0.3D, 0.3D, 0.04D, visualData); + world.spawnParticle(Particle.ENCHANT, source, 3, 0.28D, 0.28D, 0.28D, 0.12D); + if (processed % blocksPerPulse == 0) { + double progress = totalBlocks <= 1 ? 1D : (double) erosionOrder / (double) (totalBlocks - 1); + float pitch = (float) Math.min(1.95D, 0.65D + (progress * 1.25D)); + world.playSound(source, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.22F, pitch); + } + } catch (Throwable error) { + reportEffectFailure(error); + } + } + + boolean routeDrop(Object drop) { + if (!(drop instanceof ItemStack item)) { + return false; + } + if (item.getType() == Material.AIR || item.getAmount() <= 0) { + return true; + } + return enqueue(new PendingDrop(item.clone(), 0)); + } + + boolean routeExperience(int experience) { + return experience <= 0 || enqueue(new PendingDrop(null, experience)); + } + + void finish() { + completed.set(true); + if (!pendingDrops.isEmpty()) { + scheduleFlush(); + } + } + + private boolean enqueue(PendingDrop drop) { + pendingDrops.add(drop); + if (scheduleFlush()) { + return true; + } + return !pendingDrops.remove(drop); + } + + private boolean scheduleFlush() { + if (pendingDrops.isEmpty() || !flushScheduled.compareAndSet(false, true)) { + return true; + } + fallbackScheduled.set(false); + Runnable retired = () -> { + if (!scheduleFallbackFlush()) { + flushScheduled.set(false); + IrisLogging.error("Unable to deliver queued Iris tree-feller drops after the player retired."); + } + }; + if (J.runEntity(player, this::flushAtPlayer, 1, retired)) { + return true; + } + return scheduleFallbackFlush(); + } + + private boolean scheduleFallbackFlush() { + if (!fallbackScheduled.compareAndSet(false, true)) { + return true; + } + Location fallback = fallbackLocation.clone(); + if (J.runAt(fallback, () -> flushAtFallback(fallback), 1)) { + return true; + } + fallbackScheduled.set(false); + flushScheduled.set(false); + return false; + } + + private void flushAtPlayer() { + if (!player.isOnline() || !player.getWorld().equals(sourceWorld)) { + if (!scheduleFallbackFlush()) { + IrisLogging.error("Unable to deliver queued Iris tree-feller drops at their fallback location."); + } + return; + } + Location target = player.getLocation().clone().add(0D, 0.15D, 0D); + fallbackLocation = target.clone(); + deliverBatch(target, drainPendingDrops(), true); + completeFlush(); + } + + private void flushAtFallback(Location fallback) { + deliverBatch(fallback, drainPendingDrops(), false); + completeFlush(); + } + + private DropBatch drainPendingDrops() { + List items = new ArrayList<>(); + long experience = 0L; + PendingDrop pending; + while ((pending = pendingDrops.poll()) != null) { + if (pending.item() != null) { + items.add(pending.item()); + } + experience = Math.min(Integer.MAX_VALUE, experience + pending.experience()); + } + return new DropBatch(consolidateDrops(items), (int) experience); + } + + private void deliverBatch(Location target, DropBatch batch, boolean atPlayer) { + World world = target.getWorld(); + if (world == null) { + requeueBatch(batch); + return; + } + int deliveredItems = 0; + for (int index = 0; index < batch.items().size(); index++) { + ItemStack item = batch.items().get(index); + Item dropped; + try { + dropped = world.dropItem(target, item); + } catch (Throwable error) { + requeueUndelivered(batch, index); + reportDeliveryFailure(error); + return; + } + deliveredItems++; + try { + dropped.setVelocity(new Vector(0D, 0.08D, 0D)); + } catch (Throwable error) { + reportEffectFailure(error); + } + } + if (batch.experience() > 0) { + try { + ExperienceOrb orb = world.spawn(target.clone().add(0D, 0.35D, 0D), ExperienceOrb.class); + orb.setExperience(batch.experience()); + } catch (Throwable error) { + pendingDrops.add(new PendingDrop(null, batch.experience())); + reportDeliveryFailure(error); + } + } + try { + int enchantParticles = Math.min(32, 6 + (deliveredItems * 2)); + Location effectLocation = target.clone().add(0D, 0.35D, 0D); + world.spawnParticle(Particle.ENCHANT, effectLocation, enchantParticles, 0.35D, 0.25D, 0.35D, 0.15D); + world.spawnParticle(Particle.END_ROD, effectLocation, Math.min(8, 2 + deliveredItems), 0.2D, 0.2D, 0.2D, 0.025D); + int deliveryPulse = deliveryPulses.incrementAndGet(); + if (atPlayer && (deliveryPulse == 1 || deliveryPulse % 4 == 0 || completed.get())) { + world.playSound(target, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.2F, 1.65F); + } + } catch (Throwable error) { + reportEffectFailure(error); + } + } + + private void completeFlush() { + fallbackScheduled.set(false); + flushScheduled.set(false); + if (!pendingDrops.isEmpty() && !scheduleFlush()) { + IrisLogging.error("Unable to schedule the remaining Iris tree-feller drops."); + } + } + + private void requeueBatch(DropBatch batch) { + requeueUndelivered(batch, 0); + } + + private void requeueUndelivered(DropBatch batch, int firstUndeliveredItem) { + for (int index = firstUndeliveredItem; index < batch.items().size(); index++) { + pendingDrops.add(new PendingDrop(batch.items().get(index), 0)); + } + if (batch.experience() > 0) { + pendingDrops.add(new PendingDrop(null, batch.experience())); + } + } + + private void reportEffectFailure(Throwable error) { + if (effectFailureReported.compareAndSet(false, true)) { + IrisLogging.reportError("Failed to render an Iris tree-feller effect.", error); + } + } + + private void reportDeliveryFailure(Throwable error) { + if (deliveryFailureReported.compareAndSet(false, true)) { + IrisLogging.reportError("Failed to deliver an Iris tree-feller drop; it remains queued for retry.", error); + } + } + + private static void mergeDrop(List consolidated, ItemStack source) { + if (source == null || source.getType() == Material.AIR || source.getAmount() <= 0) { + return; + } + int remaining = source.getAmount(); + for (ItemStack existing : consolidated) { + if (!existing.isSimilar(source) || existing.getAmount() >= existing.getMaxStackSize()) { + continue; + } + int transferable = Math.min(remaining, existing.getMaxStackSize() - existing.getAmount()); + existing.setAmount(existing.getAmount() + transferable); + remaining -= transferable; + if (remaining == 0) { + return; + } + } + int maximumStackSize = Math.max(1, source.getMaxStackSize()); + while (remaining > 0) { + ItemStack stack = source.clone(); + stack.setAmount(Math.min(remaining, maximumStackSize)); + consolidated.add(stack); + remaining -= stack.getAmount(); + } + } + + private record PendingDrop(ItemStack item, int experience) { + } + + private record DropBatch(List items, int experience) { + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerSVC.java new file mode 100644 index 000000000..3d2eb0a8e --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerSVC.java @@ -0,0 +1,330 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.tree.IrisTreeFellerService; +import art.arcane.iris.api.tree.TreeFellerAccess; +import art.arcane.iris.api.tree.TreeFellerOptions; +import art.arcane.iris.api.tree.TreeFellerRunHooks; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.service.TreeFellerModel.PendingFell; +import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot; +import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate; +import art.arcane.iris.core.service.TreeFellerModel.TreeClaim; +import art.arcane.iris.core.service.tree.TreeDefinitionIndex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.ExperienceOrb; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerItemHeldEvent; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.ServicePriority; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public class TreeFellerSVC implements IrisService, IrisTreeFellerService { + private static final String PERMISSION = "iris.treefeller"; + + private final AtomicBoolean serviceEnabled = new AtomicBoolean(); + private final Map pending = Collections.synchronizedMap(new IdentityHashMap<>()); + final Set managedEvents = Collections.synchronizedSet( + Collections.newSetFromMap(new IdentityHashMap<>()) + ); + final Set activeClaims = ConcurrentHashMap.newKeySet(); + final Map> activeRuns = new ConcurrentHashMap<>(); + private final Map definitions = Collections.synchronizedMap(new WeakHashMap<>()); + private final TreeProvenance provenance = new TreeProvenance(definitions); + private final TreeFellingRunner runner = new TreeFellingRunner(this, provenance); + + @Override + public void onEnable() { + serviceEnabled.set(true); + Bukkit.getServicesManager().register( + IrisTreeFellerService.class, + this, + BukkitPlatform.plugin(), + ServicePriority.Normal + ); + IrisServices.register(IrisTreeFellerService.class, this); + } + + @Override + public void onDisable() { + serviceEnabled.set(false); + Bukkit.getServicesManager().unregister(IrisTreeFellerService.class, this); + IrisServices.remove(IrisTreeFellerService.class); + pending.clear(); + managedEvents.clear(); + for (Set runs : activeRuns.values()) { + for (FellingRun run : runs) { + runner.finish(run); + } + } + activeRuns.clear(); + activeClaims.clear(); + definitions.clear(); + } + + @Override + public boolean tryFell(BlockBreakEvent event, TreeFellerOptions options) { + if (!serviceEnabled.get() + || event == null + || options == null + || event.isCancelled()) { + return false; + } + // An event that already carries a pending fell may still be upgraded to + // INTEGRATION_OVERRIDE; only internal probes are refused outright. + if (!pending.containsKey(event) && isManagedBreak(event)) { + return false; + } + if (!canUse(event.getPlayer(), options.access())) { + return false; + } + + TreeCandidate candidate; + try { + candidate = provenance.resolveCandidate(event.getBlock(), event.getPlayer()); + } catch (Throwable error) { + IrisLogging.reportError("Failed to resolve an Iris tree-feller request.", error); + return false; + } + if (candidate == null) { + return false; + } + + PendingFell requested = new PendingFell( + candidate, + resolvePreservationChance(options), + options.runHooks() + ); + synchronized (pending) { + PendingFell existing = pending.get(event); + if (existing != null && existing.candidate().access() == TreeFellerAccess.INTEGRATION_OVERRIDE) { + return options.access() == TreeFellerAccess.INTEGRATION_OVERRIDE; + } + if (existing == null || options.access() == TreeFellerAccess.INTEGRATION_OVERRIDE) { + pending.put(event, requested.withAccess(options.access())); + } + } + managedEvents.add(event); + return true; + } + + @Override + public boolean isManagedBreak(BlockBreakEvent event) { + return event != null && managedEvents.contains(event); + } + + @Override + public boolean isTreeBlock(Block block) { + if (block == null) { + return false; + } + try { + return provenance.resolveTreeContext(block) != null; + } catch (Throwable error) { + IrisLogging.reportError("Failed to inspect Iris tree provenance.", error); + return false; + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void requestStandalone(BlockBreakEvent event) { + tryFell(event, TreeFellerOptions.standalone()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void finalizeBreak(BlockBreakEvent event) { + PendingFell requested; + synchronized (pending) { + requested = pending.remove(event); + } + + if (requested == null) { + if (!event.isCancelled() && !isManagedBreak(event)) { + deferSuccessfulBreakCleanup(event); + } + return; + } + + if (event.isCancelled()) { + releaseManagedLater(event); + return; + } + + TreeCandidate current; + try { + current = provenance.resolveCandidate(event.getBlock(), event.getPlayer()); + } catch (Throwable error) { + IrisLogging.reportError("Failed to finalize an Iris tree-feller request.", error); + deferSuccessfulBreakCleanup(event); + releaseManagedLater(event); + return; + } + if (current == null || !current.context().marker().equals(requested.candidate().context().marker())) { + deferSuccessfulBreakCleanup(event); + releaseManagedLater(event); + return; + } + + TreeClaim claim = new TreeClaim(current.world().getUID(), current.context().marker()); + if (!activeClaims.add(claim)) { + event.setCancelled(true); + event.setDropItems(false); + event.setExpToDrop(0); + releaseManagedLater(event); + return; + } + + event.setCancelled(true); + event.setDropItems(false); + event.setExpToDrop(0); + releaseManagedLater(event); + + FellingRun run = new FellingRun( + claim, + current, + requested.preservationChance(), + requested.runHooks(), + event.getPlayer().getInventory().getHeldItemSlot(), + event.getPlayer().getLocation().clone().add(0D, 0.15D, 0D) + ); + activeRuns.computeIfAbsent(event.getPlayer().getUniqueId(), ignored -> ConcurrentHashMap.newKeySet()).add(run); + notifyActivationAccepted(run.runHooks); + run.presentation.activate(event.getBlock()); + runner.discover(run); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void haltWhenSneakingStops(PlayerToggleSneakEvent event) { + if (!event.isSneaking()) { + runner.finishRuns(event.getPlayer().getUniqueId()); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void haltWhenHeldSlotChanges(PlayerItemHeldEvent event) { + if (event.getNewSlot() != event.getPreviousSlot()) { + runner.finishRuns(event.getPlayer().getUniqueId()); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void haltWhenHandsSwap(PlayerSwapHandItemsEvent event) { + runner.finishRuns(event.getPlayer().getUniqueId()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void clearPlacedProvenance(BlockPlaceEvent event) { + ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlockPlaced()); + if (snapshot == null) { + return; + } + Location location = event.getBlockPlaced().getLocation(); + Runnable cleanup = () -> { + if (!event.isCancelled()) { + provenance.clearProvenanceIfMatching(snapshot); + } + }; + if (!J.runAt(location, cleanup, 1) && !J.isFolia()) { + J.s(cleanup, 1); + } + } + + boolean isServiceEnabled() { + return serviceEnabled.get(); + } + + private boolean canUse(Player player, TreeFellerAccess access) { + if (access == TreeFellerAccess.INTEGRATION_OVERRIDE) { + return true; + } + IrisSettings.IrisSettingsTreeFeller settings = IrisSettings.get().getTreeFeller(); + return settings != null && settings.isEnabled() && player.hasPermission(PERMISSION); + } + + private int resolvePreservationChance(TreeFellerOptions options) { + if (options.access() == TreeFellerAccess.INTEGRATION_OVERRIDE) { + return options.durabilityPreservationChance(); + } + IrisSettings.IrisSettingsTreeFeller settings = IrisSettings.get().getTreeFeller(); + return settings == null ? 0 : settings.getDurabilityPreservationChance(); + } + + private void notifyActivationAccepted(TreeFellerRunHooks runHooks) { + try { + runHooks.onActivationAccepted(); + } catch (Throwable error) { + IrisLogging.reportError("An Iris tree-feller integration activation callback failed.", error); + } + } + + private void releaseManagedLater(BlockBreakEvent event) { + J.s(() -> managedEvents.remove(event), 1); + } + + private void deferSuccessfulBreakCleanup(BlockBreakEvent event) { + ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlock()); + if (snapshot == null) { + return; + } + Location location = event.getBlock().getLocation(); + Runnable cleanup = () -> { + if (!event.isCancelled()) { + provenance.clearProvenanceIfMatching(snapshot); + } + }; + if (!J.runAt(location, cleanup, 1) && !J.isFolia()) { + J.s(cleanup, 1); + } + } + + static boolean isAxe(ItemStack item) { + return item != null && item.getType() != Material.AIR && item.getType().name().endsWith("_AXE"); + } + + void routeDrops(FellingRun run, List drops, Location source) { + World world = source.getWorld(); + if (world == null) { + return; + } + for (ItemStack drop : drops) { + if (!run.presentation.routeDrop(drop)) { + world.dropItem(source, drop); + } + } + } + + void dropExperience(Location location, int experience) { + if (experience <= 0 || location.getWorld() == null) { + return; + } + ExperienceOrb orb = location.getWorld().spawn( + location, + ExperienceOrb.class + ); + orb.setExperience(experience); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java new file mode 100644 index 000000000..a1360af20 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java @@ -0,0 +1,636 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.service.TreeFellerModel.ChunkPosition; +import art.arcane.iris.core.service.TreeFellerModel.DamageReservation; +import art.arcane.iris.core.service.TreeFellerModel.TreeContext; +import art.arcane.iris.core.service.TreeFellerModel.TreeMember; +import art.arcane.iris.core.service.tree.TreeMarkerTraversal; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.Damageable; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +final class TreeFellingRunner { + private final TreeFellerSVC service; + private final TreeProvenance provenance; + + TreeFellingRunner(TreeFellerSVC service, TreeProvenance provenance) { + this.service = service; + this.provenance = provenance; + } + + void discover(FellingRun run) { + J.a(() -> { + if (run.candidate.context().engine().isClosed()) { + finish(run); + return; + } + try { + TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover( + run.candidate.trigger(), + run.candidate.context().marker(), + run.candidate.context().minimumY(), + run.candidate.context().maximumY(), + (x, y, z) -> provenance.markerAt( + run.candidate.context().engine(), + run.candidate.context().minimumY(), + x, + y, + z + ) + ); + List positions = positionsForFelling(discovery, run.candidate.trigger()); + if (!discovery.complete()) { + run.abortReason = "discovery-incomplete"; + } + preflight(run, positions, discovery.complete()); + } catch (Throwable error) { + IrisLogging.reportError("Failed to discover an Iris tree for felling.", error); + preflight(run, List.of(run.candidate.trigger()), false); + } + }); + } + + private void preflight(FellingRun run, List positions, boolean allowFallback) { + Map> grouped = groupByChunk(positions); + if (grouped.isEmpty()) { + finish(run); + return; + } + Map erosionOrder = new HashMap<>(positions.size()); + for (int index = 0; index < positions.size(); index++) { + erosionOrder.put(positions.get(index), index); + } + + List members = Collections.synchronizedList(new ArrayList<>()); + AtomicBoolean failed = new AtomicBoolean(); + AtomicInteger remaining = new AtomicInteger(grouped.size()); + AtomicBoolean completed = new AtomicBoolean(); + + for (Map.Entry> entry : grouped.entrySet()) { + ChunkPosition chunk = entry.getKey(); + Runnable task = () -> { + try { + if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) { + run.abortReason = "chunk-unloaded"; + failed.set(true); + return; + } + for (TreeMarkerTraversal.Position position : entry.getValue()) { + TreeMember member = inspectMember(run, position, erosionOrder.getOrDefault(position, Integer.MAX_VALUE)); + if (member != null) { + members.add(member); + } + } + } catch (Throwable error) { + failed.set(true); + IrisLogging.reportError( + "Failed to preflight an Iris tree-feller chunk at " + chunk.x() + "," + chunk.z() + ".", + error + ); + } finally { + completePreflightGroup(run, members, failed, remaining, completed, allowFallback); + } + }; + if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) { + failed.set(true); + completePreflightGroup(run, members, failed, remaining, completed, allowFallback); + } + } + } + + static List positionsForFelling( + TreeMarkerTraversal.Discovery discovery, + TreeMarkerTraversal.Position trigger + ) { + return discovery.complete() ? discovery.members() : List.of(trigger); + } + + private void completePreflightGroup( + FellingRun run, + List members, + AtomicBoolean failed, + AtomicInteger remaining, + AtomicBoolean completed, + boolean allowFallback + ) { + if (remaining.decrementAndGet() != 0 || !completed.compareAndSet(false, true)) { + return; + } + if (failed.get() && allowFallback) { + preflight(run, List.of(run.candidate.trigger()), false); + return; + } + if (failed.get()) { + finish(run); + return; + } + + List ordered = orderMembers(run.candidate.trigger(), members); + if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) { + run.abortReason = "trigger-drift"; + finish(run); + return; + } + run.work = ordered; + run.blocksPerPulse = TreeFellerPresentation.blocksPerPulse(ordered.size()); + run.effectStride = TreeFellerPresentation.effectStride(run.blocksPerPulse); + processNext(run); + } + + private TreeMember inspectMember(FellingRun run, TreeMarkerTraversal.Position position, int erosionOrder) { + World world = run.candidate.world(); + Block block = world.getBlockAt(position.x(), position.y(), position.z()); + TreeContext context = run.candidate.context(); + if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) { + return null; + } + TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position); + if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + if (block.getType().isAir()) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + return new TreeMember(position, Tag.LOGS.isTagged(block.getType()), expected, erosionOrder); + } + + private List orderMembers( + TreeMarkerTraversal.Position trigger, + Collection discovered + ) { + Comparator erosionOrder = Comparator + .comparingInt(TreeMember::erosionOrder) + .thenComparingInt(member -> member.position().y()) + .thenComparingInt(member -> member.position().x()) + .thenComparingInt(member -> member.position().z()); + List ordered = discovered.stream().sorted(erosionOrder).toList(); + if (ordered.isEmpty() || !ordered.getFirst().position().equals(trigger)) { + return List.of(); + } + return ordered; + } + + private void processNext(FellingRun run) { + if (run.finished.get()) { + return; + } + int index = run.cursor.getAndIncrement(); + if (index >= run.work.size()) { + finish(run); + return; + } + + TreeMember member = run.work.get(index); + ChunkPosition chunk = new ChunkPosition(member.position().x() >> 4, member.position().z() >> 4); + Runnable task = () -> runTask( + run, + "Failed to prepare an Iris tree-feller block.", + () -> prepareBreak(run, member) + ); + if (!runMemberRegion(run, chunk.x(), chunk.z(), task)) { + if (member.log()) { + run.abortReason = "schedule-refused"; + finish(run); + } else { + continueRun(run); + } + } + } + + // On Paper, J.runRegion/J.runEntity always defer a full tick even when already on the + // main thread, which collapsed the pulse pacing to one block per tick and forced players + // to hold sneak for 10+ seconds on large trees. Folia keeps the scheduler hop. + static boolean shouldRunInline(boolean folia, boolean primaryThread) { + return !folia && primaryThread; + } + + private boolean runMemberRegion(FellingRun run, int chunkX, int chunkZ, Runnable task) { + if (shouldRunInline(J.isFolia(), Bukkit.isPrimaryThread())) { + task.run(); + return true; + } + return J.runRegion(run.candidate.world(), chunkX, chunkZ, task); + } + + private boolean runMemberEntity(FellingRun run, Runnable task) { + if (shouldRunInline(J.isFolia(), Bukkit.isPrimaryThread())) { + task.run(); + return true; + } + return J.runEntity(run.candidate.player(), task); + } + + private void prepareBreak(FellingRun run, TreeMember member) { + Block block = liveMemberBlock(run, member); + if (block == null) { + if (member.log()) { + run.abortReason = "member-drift"; + finish(run); + } else { + continueRun(run); + } + return; + } + + if (!member.log()) { + runMutationTask( + run, + member, + new DamageReservation(run.expectedTool.clone(), false, false, false), + new AtomicBoolean() + ); + return; + } + + Runnable task = () -> runTask( + run, + "Failed to reserve Iris tree-feller tool durability.", + () -> reserveDamage(run, member) + ); + if (!runMemberEntity(run, task)) { + run.abortReason = "schedule-refused"; + finish(run); + } + } + + private void reserveDamage(FellingRun run, TreeMember member) { + Player player = run.candidate.player(); + if (!isRunControlActive(run, player)) { + run.abortReason = "control-released"; + finish(run); + return; + } + PlayerInventory inventory = player.getInventory(); + ItemStack current = inventory.getItem(run.heldSlot); + if (current == null + || inventory.getHeldItemSlot() != run.heldSlot + || !current.isSimilar(run.expectedTool) + || !TreeFellerSVC.isAxe(current)) { + run.abortReason = "tool-changed"; + finish(run); + return; + } + + if (!reserveLogCost(run)) { + run.abortReason = "log-cost-refused"; + finish(run); + return; + } + + ItemStack before = current.clone(); + ItemMeta meta = current.getItemMeta(); + if (meta.isUnbreakable() || ThreadLocalRandom.current().nextInt(100) < run.preservationChance) { + scheduleMutation(run, member, new DamageReservation(before, false, false, true)); + return; + } + if (!(meta instanceof Damageable damageable) || current.getType().getMaxDurability() <= 0) { + refundAndFinish(run, new DamageReservation(before, false, false, true)); + return; + } + + int nextDamage = damageable.getDamage() + 1; + boolean broke = nextDamage >= current.getType().getMaxDurability(); + if (broke) { + inventory.setItem(run.heldSlot, new ItemStack(Material.AIR)); + run.expectedTool = new ItemStack(Material.AIR); + } else { + damageable.setDamage(nextDamage); + current.setItemMeta(meta); + inventory.setItem(run.heldSlot, current); + run.expectedTool = current.clone(); + } + scheduleMutation(run, member, new DamageReservation(before, true, broke, true)); + } + + private void scheduleMutation(FellingRun run, TreeMember member, DamageReservation reservation) { + TreeMarkerTraversal.Position position = member.position(); + AtomicBoolean mutationSucceeded = new AtomicBoolean(); + Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded); + boolean scheduled; + try { + scheduled = runMemberRegion( + run, + position.x() >> 4, + position.z() >> 4, + task + ); + } catch (Throwable error) { + IrisLogging.reportError("Failed to schedule an Iris tree-feller block removal.", error); + refundAndFinish(run, reservation); + return; + } + if (!scheduled) { + refundAndFinish(run, reservation); + } + } + + private void runMutationTask( + FellingRun run, + TreeMember member, + DamageReservation reservation, + AtomicBoolean mutationSucceeded + ) { + if (run.finished.get()) { + refundAndFinish(run, reservation); + return; + } + try { + probeAndMutate(run, member, reservation, mutationSucceeded); + } catch (Throwable error) { + IrisLogging.reportError("Failed to remove an Iris tree-feller block.", error); + if (mutationSucceeded.get()) { + finish(run); + } else { + refundAndFinish(run, reservation); + } + } + } + + private void probeAndMutate( + FellingRun run, + TreeMember member, + DamageReservation reservation, + AtomicBoolean mutationSucceeded + ) { + Block block = liveMemberBlock(run, member); + if (block == null) { + refundAndFinish(run, reservation); + return; + } + + BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service); + service.managedEvents.add(probe); + try { + Bukkit.getPluginManager().callEvent(probe); + } catch (Throwable error) { + probe.setCancelled(true); + IrisLogging.reportError("Failed to dispatch an Iris tree-feller block probe.", error); + } finally { + service.managedEvents.remove(probe); + } + + try { + if (probe.isCancelled()) { + if (reservation.charged() || reservation.logCostReserved()) { + run.abortReason = "probe-cancelled"; + refundAndFinish(run, reservation); + } else if (member.log()) { + run.abortReason = "probe-cancelled"; + finish(run); + } else { + continueRun(run); + } + return; + } + + block = liveMemberBlock(run, member); + if (run.finished.get() || block == null) { + if (block == null) { + run.abortReason = "member-drift"; + } + probe.setCancelled(true); + refundAndFinish(run, reservation); + return; + } + + Location source = block.getLocation().clone().add(0.5D, 0.5D, 0.5D); + BlockData visualData = block.getBlockData().clone(); + List vanillaDrops = probe.isDropItems() + ? block.getDrops(reservation.toolForDrops()).stream() + .map(ItemStack::clone) + .toList() + : List.of(); + block.setType(Material.AIR, false); + if (!block.getType().isAir()) { + probe.setCancelled(true); + refundAndFinish(run, reservation); + return; + } + mutationSucceeded.set(true); + run.presentation.erode( + source, + visualData, + member.erosionOrder(), + run.processed.get(), + run.blocksPerPulse, + run.effectStride, + run.work.size() + ); + + provenance.clearProvenance( + run.candidate.context().engine(), + run.candidate.context().minimumY(), + member.position() + ); + service.routeDrops(run, vanillaDrops, source); + if (!run.presentation.routeExperience(probe.getExpToDrop())) { + service.dropExperience(source, probe.getExpToDrop()); + } + if (reservation.logCostReserved()) { + completeLogCost(run, reservation); + return; + } + completeSuccessfulMutation(run, reservation); + } catch (RuntimeException | Error error) { + if (!mutationSucceeded.get()) { + probe.setCancelled(true); + } + throw error; + } + } + + private Block liveMemberBlock(FellingRun run, TreeMember member) { + World world = run.candidate.world(); + TreeMarkerTraversal.Position position = member.position(); + if (!world.isChunkLoaded(position.x() >> 4, position.z() >> 4)) { + return null; + } + Block block = world.getBlockAt(position.x(), position.y(), position.z()); + TreeContext context = run.candidate.context(); + if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) { + return null; + } + if (block.getType().isAir() || member.log() != Tag.LOGS.isTagged(block.getType())) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position); + if (member.expectedMaterial() != null && !member.expectedMaterial().equals(expected)) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + return block; + } + + private void refundAndFinish(FellingRun run, DamageReservation reservation) { + if (!reservation.charged() && !reservation.logCostReserved()) { + finish(run); + return; + } + if (!J.runEntity(run.candidate.player(), () -> { + if (reservation.charged()) { + PlayerInventory inventory = run.candidate.player().getInventory(); + ItemStack current = inventory.getItem(run.heldSlot); + boolean expectedAir = run.expectedTool.getType() == Material.AIR; + boolean currentMatches = expectedAir + ? current == null || current.getType() == Material.AIR + : current != null && current.isSimilar(run.expectedTool); + if (currentMatches) { + inventory.setItem(run.heldSlot, reservation.toolForDrops().clone()); + run.expectedTool = reservation.toolForDrops().clone(); + } + } + if (reservation.logCostReserved()) { + refundLogCost(run); + } + finish(run); + })) { + finish(run); + } + } + + private boolean isRunControlActive(FellingRun run, Player player) { + return player.isOnline() + && player.getGameMode() == GameMode.SURVIVAL + && player.isSneaking() + && player.getWorld().equals(run.candidate.world()); + } + + private boolean reserveLogCost(FellingRun run) { + try { + return run.runHooks.reserveLogCost(); + } catch (Throwable error) { + IrisLogging.reportError("An Iris tree-feller integration log-cost reservation failed.", error); + return false; + } + } + + private void completeLogCost(FellingRun run, DamageReservation reservation) { + if (!J.runEntity(run.candidate.player(), () -> { + try { + run.runHooks.commitLogCost(); + } catch (Throwable error) { + IrisLogging.reportError("An Iris tree-feller integration log-cost commit failed.", error); + finish(run); + return; + } + completeSuccessfulMutation(run, reservation); + })) { + finish(run); + } + } + + private void refundLogCost(FellingRun run) { + try { + run.runHooks.refundLogCost(); + } catch (Throwable error) { + IrisLogging.reportError("An Iris tree-feller integration log-cost refund failed.", error); + } + } + + private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) { + if (reservation.broke()) { + run.abortReason = "tool-broke"; + finish(run); + return; + } + continueRun(run); + } + + private void continueRun(FellingRun run) { + int processed = run.processed.incrementAndGet(); + if (processed % run.blocksPerPulse == 0) { + J.s(() -> runTask(run, "Failed to continue an Iris tree-feller run.", () -> processNext(run)), 1); + } else { + processNext(run); + } + } + + private void runTask(FellingRun run, String context, Runnable task) { + if (run.finished.get() || !service.isServiceEnabled()) { + finish(run); + return; + } + try { + task.run(); + } catch (Throwable error) { + IrisLogging.reportError(context, error); + finish(run); + } + } + + void finish(FellingRun run) { + if (run.finished.compareAndSet(false, true)) { + service.activeClaims.remove(run.claim); + Set runs = service.activeRuns.get(run.candidate.player().getUniqueId()); + if (runs != null) { + runs.remove(run); + if (runs.isEmpty()) { + service.activeRuns.remove(run.candidate.player().getUniqueId(), runs); + } + } + run.presentation.finish(); + IrisLogging.debug("Tree feller run ended: members=" + run.work.size() + + " processed=" + run.processed.get() + + " reason=" + (run.abortReason == null ? "complete" : run.abortReason)); + } + } + + void finishRuns(UUID playerId) { + Set runs = service.activeRuns.get(playerId); + if (runs == null) { + return; + } + for (FellingRun run : List.copyOf(runs)) { + if (run.abortReason == null) { + run.abortReason = "halted"; + } + finish(run); + } + } + + private Map> groupByChunk( + List positions + ) { + Map> grouped = new LinkedHashMap<>(); + for (TreeMarkerTraversal.Position position : positions) { + ChunkPosition chunk = new ChunkPosition(position.x() >> 4, position.z() >> 4); + grouped.computeIfAbsent(chunk, ignored -> new ArrayList<>()).add(position); + } + return grouped; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeProvenance.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeProvenance.java new file mode 100644 index 000000000..273073372 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeProvenance.java @@ -0,0 +1,151 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.tree.TreeFellerAccess; +import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot; +import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate; +import art.arcane.iris.core.service.TreeFellerModel.TreeContext; +import art.arcane.iris.core.service.tree.TreeDefinitionIndex; +import art.arcane.iris.core.service.tree.TreeMarkerTraversal; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.StructurePlacementMarker; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import org.bukkit.GameMode; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; +import java.util.Objects; + +final class TreeProvenance { + private final Map definitions; + + TreeProvenance(Map definitions) { + this.definitions = definitions; + } + + TreeCandidate resolveCandidate(Block block, Player player) { + if (player.getGameMode() != GameMode.SURVIVAL || !player.isSneaking()) { + return null; + } + if (!Tag.LOGS.isTagged(block.getType())) { + return null; + } + ItemStack tool = player.getInventory().getItemInMainHand(); + if (!TreeFellerSVC.isAxe(tool)) { + return null; + } + TreeContext context = resolveTreeContext(block); + if (context == null) { + return null; + } + return new TreeCandidate( + context, + block.getWorld(), + player, + tool.clone(), + TreeFellerAccess.STANDALONE, + new TreeMarkerTraversal.Position(block.getX(), block.getY(), block.getZ()) + ); + } + + TreeContext resolveTreeContext(Block block) { + if (block.getType().isAir()) { + return null; + } + PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld()); + if (access == null || access.getEngine() == null) { + return null; + } + Engine engine = access.getEngine(); + World world = block.getWorld(); + int minimumY = world.getMinHeight(); + int maximumY = world.getMaxHeight(); + int relativeY = block.getY() - minimumY; + String marker = markerAt(engine, minimumY, block.getX(), block.getY(), block.getZ()); + StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker); + if (decoded == null || decoded.structureAware()) { + return null; + } + TreeBlockMaterial expected = engine.getMantle().getMantle().get( + block.getX(), + relativeY, + block.getZ(), + TreeBlockMaterial.class + ); + if (expected != null && !matchesExpectedMaterial(block, expected)) { + return null; + } + if (expected == null + && !decoded.objectKey().startsWith("trees/") + && !definitionIndex(engine).isTreeMarker(marker)) { + return null; + } + return new TreeContext(engine, marker, expected, minimumY, maximumY); + } + + TreeDefinitionIndex definitionIndex(Engine engine) { + synchronized (definitions) { + return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build); + } + } + + String markerAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) { + return markerAt(engine, minimumY, position.x(), position.y(), position.z()); + } + + String markerAt(Engine engine, int minimumY, int x, int y, int z) { + return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class); + } + + TreeBlockMaterial materialAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) { + return engine.getMantle().getMantle().get( + position.x(), + position.y() - minimumY, + position.z(), + TreeBlockMaterial.class + ); + } + + ProvenanceSnapshot captureProvenance(Block block) { + PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld()); + if (access == null || access.getEngine() == null) { + return null; + } + Engine engine = access.getEngine(); + TreeMarkerTraversal.Position position = new TreeMarkerTraversal.Position( + block.getX(), + block.getY(), + block.getZ() + ); + int minimumY = block.getWorld().getMinHeight(); + String marker = markerAt(engine, minimumY, position); + TreeBlockMaterial material = materialAt(engine, minimumY, position); + if (marker == null && material == null) { + return null; + } + return new ProvenanceSnapshot(engine, block.getWorld(), minimumY, position, marker, material); + } + + void clearProvenanceIfMatching(ProvenanceSnapshot snapshot) { + String marker = markerAt(snapshot.engine(), snapshot.minimumY(), snapshot.position()); + TreeBlockMaterial material = materialAt(snapshot.engine(), snapshot.minimumY(), snapshot.position()); + if (Objects.equals(snapshot.marker(), marker) && Objects.equals(snapshot.material(), material)) { + clearProvenance(snapshot.engine(), snapshot.minimumY(), snapshot.position()); + } + } + + void clearProvenance(Engine engine, int minimumY, TreeMarkerTraversal.Position position) { + int relativeY = position.y() - minimumY; + engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), String.class); + engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), TreeBlockMaterial.class); + } + + boolean matchesExpectedMaterial(Block block, TreeBlockMaterial expected) { + return expected.matches(block.getBlockData().getAsString()); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/service/WandSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java similarity index 80% rename from core/src/main/java/com/volmit/iris/core/service/WandSVC.java rename to adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java index e55991245..5afa2c8f6 100644 --- a/core/src/main/java/com/volmit/iris/core/service/WandSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java @@ -16,26 +16,36 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.service; +package art.arcane.iris.core.service; -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.edit.DustRevealer; -import com.volmit.iris.core.link.WorldEditLink; -import com.volmit.iris.core.wand.WandSelection; -import com.volmit.iris.engine.object.IrisObject; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.data.Cuboid; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.matter.Matter; -import com.volmit.iris.util.matter.WorldMatter; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.SR; -import com.volmit.iris.util.scheduling.jobs.Job; -import org.bukkit.*; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.World; +import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.core.edit.DustRevealer; +import art.arcane.iris.core.link.WorldEditLink; +import art.arcane.iris.core.wand.WandSelection; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.util.project.matter.WorldMatter; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.data.Cuboid; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.SR; +import art.arcane.iris.util.common.scheduling.jobs.Job; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -46,17 +56,16 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; import org.bukkit.util.BlockVector; import org.bukkit.util.Vector; import java.awt.Color; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Objects; import java.util.concurrent.CountDownLatch; -import static com.volmit.iris.util.data.registry.Particles.CRIT_MAGIC; -import static com.volmit.iris.util.data.registry.Particles.REDSTONE; +import static art.arcane.iris.util.common.data.registry.Particles.CRIT_MAGIC; +import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE; public class WandSVC implements IrisService { private static final int MS_PER_TICK = Integer.parseInt(System.getProperty("iris.ms_per_tick", "30")); @@ -92,12 +101,12 @@ public class WandSVC implements IrisService { var latch = new CountDownLatch(1); var holder = Iris.tickets.getHolder(p.getWorld()); new Job() { - private int i; + private volatile int i; private Chunk chunk; @Override public String getName() { - return "Scanning Selection"; + return IrisLanguage.text(RuntimeUiMessages.JOB_SCANNING_SELECTION); } @Override @@ -206,7 +215,9 @@ public class WandSVC implements IrisService { if (f.length != 2) return null; String[] g = f[0].split("\\Q,\\E"); if (g.length != 3) return null; - return new Location(Bukkit.getWorld(f[1]), Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2])); + World world = WorldIdentity.resolve(f[1]).orElse(null); + if (world == null) return null; + return new Location(world, Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2])); } catch (Throwable e) { Iris.reportError(e); return null; @@ -224,7 +235,7 @@ public class WandSVC implements IrisService { return "<#>"; } - return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + loc.getWorld().getName(); + return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + WorldIdentity.serialize(loc.getWorld()); } /** @@ -245,10 +256,11 @@ public class WandSVC implements IrisService { ItemStack is = new ItemStack(Material.GLOWSTONE_DUST); is.addUnsafeEnchantment(Enchantment.FIRE_ASPECT, 1); ItemMeta im = is.getItemMeta(); - im.setDisplayName(C.BOLD + "" + C.YELLOW + "Dust of Revealing"); + im.setDisplayName(C.BOLD + "" + C.YELLOW + IrisLanguage.text(RuntimeUiMessages.DUST_NAME)); im.setUnbreakable(true); im.addItemFlags(ItemFlag.values()); - im.setLore(new KList().qadd("Right click on a block to reveal it's placement structure!")); + im.setLore(new KList().qadd(IrisLanguage.text(RuntimeUiMessages.DUST_LORE))); + im.getPersistentDataContainer().set(dustKey(), PersistentDataType.BYTE, (byte) 1); is.setItemMeta(im); return is; @@ -261,19 +273,11 @@ public class WandSVC implements IrisService { * @return The slot number the wand is in. Or -1 if none are found */ public static int findWand(Inventory inventory) { - ItemStack wand = createWand(); //Create blank wand - ItemMeta meta = wand.getItemMeta(); - meta.setLore(new ArrayList<>()); //We are resetting the lore as the lore differs between wands - wand.setItemMeta(meta); - for (int s = 0; s < inventory.getSize(); s++) { ItemStack stack = inventory.getItem(s); - if (stack == null) continue; - meta = stack.getItemMeta(); - meta.setLore(new ArrayList<>()); //Reset the lore on this too so we can compare them - stack.setItemMeta(meta); //We dont need to clone the item as items from .get are cloned - - if (wand.isSimilar(stack)) return s; //If the name, material and NBT is the same + if (stack != null && isWand(stack)) { + return s; + } } return -1; } @@ -289,10 +293,14 @@ public class WandSVC implements IrisService { ItemStack is = new ItemStack(Material.BLAZE_ROD); is.addUnsafeEnchantment(Enchantment.FIRE_ASPECT, 1); ItemMeta im = is.getItemMeta(); - im.setDisplayName(C.BOLD + "" + C.GOLD + "Wand of Iris"); + im.setDisplayName(C.BOLD + "" + C.GOLD + IrisLanguage.text(RuntimeUiMessages.WAND_NAME)); im.setUnbreakable(true); im.addItemFlags(ItemFlag.values()); - im.setLore(new KList().add(locationToString(a), locationToString(b))); + im.setLore(new KList().add( + a == null ? IrisLanguage.text(RuntimeUiMessages.WAND_LORE_FIRST) : locationToString(a), + b == null ? IrisLanguage.text(RuntimeUiMessages.WAND_LORE_SECOND) : locationToString(b) + )); + im.getPersistentDataContainer().set(wandKey(), PersistentDataType.BYTE, (byte) 1); is.setItemMeta(im); return is; @@ -335,7 +343,13 @@ public class WandSVC implements IrisService { * @return True if it is */ public static boolean isWand(ItemStack is) { - if (is.getItemMeta() == null) return false; + if (is == null || is.getItemMeta() == null) { + return false; + } + Byte marker = is.getItemMeta().getPersistentDataContainer().get(wandKey(), PersistentDataType.BYTE); + if (marker != null && marker == (byte) 1) { + return true; + } return is.getType().equals(wand.getType()) && is.getItemMeta().getDisplayName().equals(wand.getItemMeta().getDisplayName()) && is.getItemMeta().getEnchants().equals(wand.getItemMeta().getEnchants()) && @@ -347,11 +361,7 @@ public class WandSVC implements IrisService { wand = createWand(); dust = createDust(); - J.ar(() -> { - for (Player i : Bukkit.getOnlinePlayers()) { - tick(i); - } - }, 0); + J.ar(this::tickAll, 0); } @Override @@ -359,6 +369,22 @@ public class WandSVC implements IrisService { } + /** + * Async driver tick. The online player list is only read from the thread that owns it, + * and every wand draw is dispatched to the thread owning that player. + */ + private void tickAll() { + try { + J.runGlobal(() -> { + for (Player p : Bukkit.getOnlinePlayers()) { + J.runEntity(p, () -> tick(p)); + } + }); + } catch (Throwable e) { + Iris.reportError(e); + } + } + public void tick(Player p) { try { try { @@ -509,7 +535,19 @@ public class WandSVC implements IrisService { * @return True if it is */ public boolean isDust(ItemStack is) { - return is.isSimilar(dust); + if (is == null || is.getItemMeta() == null) { + return false; + } + Byte marker = is.getItemMeta().getPersistentDataContainer().get(dustKey(), PersistentDataType.BYTE); + return (marker != null && marker == (byte) 1) || is.isSimilar(dust); + } + + private static NamespacedKey wandKey() { + return new NamespacedKey(Iris.instance, "wand"); + } + + private static NamespacedKey dustKey() { + return new NamespacedKey(Iris.instance, "dust"); } /** @@ -528,7 +566,7 @@ public class WandSVC implements IrisService { Location[] f = getCuboidFromItem(item); Location other = left ? f[1] : f[0]; - if (other != null && !other.getWorld().getName().equals(a.getWorld().getName())) { + if (other != null && !WorldIdentity.key(other.getWorld()).equals(WorldIdentity.key(a.getWorld()))) { other = null; } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisApiFaultGuard.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisApiFaultGuard.java new file mode 100644 index 000000000..9d1aa8950 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisApiFaultGuard.java @@ -0,0 +1,31 @@ +package art.arcane.iris.core.service.terrain; + +import java.util.concurrent.atomic.AtomicLong; + +public final class IrisApiFaultGuard { + private static final long NEVER = Long.MIN_VALUE; + + private final long reportIntervalMillis; + private final AtomicLong faults = new AtomicLong(); + private final AtomicLong lastReportedAt = new AtomicLong(NEVER); + + public IrisApiFaultGuard(long reportIntervalMillis) { + if (reportIntervalMillis < 0L) { + throw new IllegalArgumentException("reportIntervalMillis must not be negative"); + } + this.reportIntervalMillis = reportIntervalMillis; + } + + public long faults() { + return faults.get(); + } + + public boolean record(long nowMillis) { + faults.incrementAndGet(); + long last = lastReportedAt.get(); + if (last != NEVER && nowMillis - last < reportIntervalMillis) { + return false; + } + return lastReportedAt.compareAndSet(last, nowMillis); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisColumnWalk.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisColumnWalk.java new file mode 100644 index 000000000..8e110b85b --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisColumnWalk.java @@ -0,0 +1,49 @@ +package art.arcane.iris.core.service.terrain; + +import art.arcane.iris.api.terrain.IrisColumnQuery; + +public final class IrisColumnWalk { + private IrisColumnWalk() { + } + + public static long walk(IrisColumnQuery query, ColumnVisitor visitor) { + int stride = query.strideBlocks(); + int minChunkX = query.minBlockX() >> 4; + int maxChunkX = query.maxBlockX() >> 4; + int minChunkZ = query.minBlockZ() >> 4; + int maxChunkZ = query.maxBlockZ() >> 4; + long visited = 0L; + + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + int chunkMinBlockZ = Math.max(query.minBlockZ(), chunkZ << 4); + int chunkMaxBlockZ = Math.min(query.maxBlockZ(), (chunkZ << 4) + 15); + + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + int chunkMinBlockX = Math.max(query.minBlockX(), chunkX << 4); + int chunkMaxBlockX = Math.min(query.maxBlockX(), (chunkX << 4) + 15); + + for (long blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) { + for (long blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) { + if (!visitor.visit((int) blockX, (int) blockZ)) { + return visited; + } + visited++; + } + } + } + } + + return visited; + } + + private static long align(int origin, int lowerBound, int stride) { + long offset = (long) lowerBound - (long) origin; + long steps = (offset + stride - 1L) / stride; + return origin + steps * stride; + } + + @FunctionalInterface + public interface ColumnVisitor { + boolean visit(int blockX, int blockZ); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSampleLimits.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSampleLimits.java new file mode 100644 index 000000000..0409a77f3 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSampleLimits.java @@ -0,0 +1,24 @@ +package art.arcane.iris.core.service.terrain; + +import art.arcane.iris.api.terrain.IrisColumnQuery; + +public final class IrisSampleLimits { + public static final int MINIMUM_CHUNKS = 64; + public static final int CACHE_SHARE_DIVISOR = 4; + + private IrisSampleLimits() { + } + + public static int maxChunks(int noiseCacheChunks) { + return Math.max(MINIMUM_CHUNKS, noiseCacheChunks / CACHE_SHARE_DIVISOR); + } + + public static int maxColumns(int noiseCacheChunks) { + long columns = (long) maxChunks(noiseCacheChunks) * 256L; + return (int) Math.min(columns, Integer.MAX_VALUE); + } + + public static boolean withinLimits(IrisColumnQuery query, int maxColumns, int maxChunks) { + return query.columnCount() <= maxColumns && query.chunkCount() <= maxChunks; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifier.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifier.java new file mode 100644 index 000000000..75be7325a --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifier.java @@ -0,0 +1,25 @@ +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; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java new file mode 100644 index 000000000..3226a70ef --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java @@ -0,0 +1,71 @@ +package art.arcane.iris.core.service.terrain; + +import art.arcane.iris.api.terrain.IrisWorldInfo; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import org.bukkit.World; +import org.bukkit.generator.ChunkGenerator; + +public final class IrisWorldInfoFactory { + private IrisWorldInfoFactory() { + } + + public static IrisWorldInfo forWorld(World world) { + if (world == null) { + return null; + } + + ChunkGenerator generator = world.getGenerator(); + return generator instanceof PlatformChunkGenerator platform ? from(platform) : null; + } + + public static IrisWorldInfo from(PlatformChunkGenerator generator) { + if (generator == null) { + return null; + } + + Engine engine = generator.getEngine(); + if (engine == null || engine.isClosed()) { + return null; + } + + IrisWorld irisWorld = engine.getWorld(); + IrisDimension dimension = engine.getDimension(); + if (irisWorld == null || dimension == null) { + return null; + } + + return build( + dimension.getLoadKey(), + irisWorld.identity(), + irisWorld.getRawWorldSeed(), + engine.getMinHeight(), + engine.getMaxHeight(), + dimension.getFluidHeight(), + generator.isStudio()); + } + + static IrisWorldInfo build( + String dimensionKey, + String worldIdentity, + long seed, + int minHeight, + int maxHeight, + int fluidHeightAboveMinimum, + boolean studio) { + if (dimensionKey == null || worldIdentity == null || maxHeight <= minHeight) { + return null; + } + + return new IrisWorldInfo( + dimensionKey, + worldIdentity, + seed, + minHeight, + maxHeight, + fluidHeightAboveMinimum + minHeight, + studio); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/wand/WandSelection.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java similarity index 93% rename from core/src/main/java/com/volmit/iris/core/wand/WandSelection.java rename to adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java index bbd7428e5..3808a2f31 100644 --- a/core/src/main/java/com/volmit/iris/core/wand/WandSelection.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java @@ -16,18 +16,18 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.wand; +package art.arcane.iris.core.wand; -import com.volmit.iris.util.data.Cuboid; -import com.volmit.iris.util.math.M; +import art.arcane.volmlib.util.data.Cuboid; +import art.arcane.volmlib.util.math.M; import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.entity.Player; import org.bukkit.util.Vector; -import java.awt.*; +import java.awt.Color; -import static com.volmit.iris.util.data.registry.Particles.REDSTONE; +import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE; public class WandSelection { private final Cuboid c; @@ -41,6 +41,10 @@ public class WandSelection { public void draw() { Location playerLoc = p.getLocation(); + if (c.getWorld() == null || !c.getWorld().equals(playerLoc.getWorld())) { + return; + } + double maxDistanceSquared = 256 * 256; int particleCount = 0; diff --git a/adapters/bukkit/plugin/src/main/resources/paper-plugin.yml b/adapters/bukkit/plugin/src/main/resources/paper-plugin.yml new file mode 100644 index 000000000..025e0363e --- /dev/null +++ b/adapters/bukkit/plugin/src/main/resources/paper-plugin.yml @@ -0,0 +1,79 @@ +name: ${name} +version: ${version} +main: ${main} +bootstrapper: ${bootstrapper} +folia-supported: true +api-version: '${apiVersion}' +load: STARTUP +authors: [ cyberpwn, NextdoorPsycho, Vatuu ] +website: VolmitSoftware.com +description: More than a Dimension! +permissions: + iris.all: + description: Allows use of the full /iris command tree (worlds, studio, pregen, packs, developer tools). + default: op + iris.treefeller: + description: Allows survival players to fell Iris-managed trees with an axe. + default: op +dependencies: + server: + PlaceholderAPI: + load: BEFORE + required: false + join-classpath: true + CraftEngine: + load: BEFORE + required: false + join-classpath: true + Nexo: + load: BEFORE + required: false + join-classpath: true + ItemsAdder: + load: BEFORE + required: false + join-classpath: true + SCore: + load: BEFORE + required: false + join-classpath: true + ExecutableItems: + load: BEFORE + required: false + join-classpath: false + MythicLib: + load: BEFORE + required: false + join-classpath: true + MMOItems: + load: BEFORE + required: false + join-classpath: true + eco: + load: BEFORE + required: false + join-classpath: true + EcoItems: + load: BEFORE + required: false + join-classpath: true + MythicMobs: + load: BEFORE + required: false + join-classpath: true + MythicCrucible: + load: BEFORE + required: false + join-classpath: true + KGenerators: + load: BEFORE + required: false + join-classpath: true + Multiverse-Core: + load: AFTER + required: false + join-classpath: true + WorldEdit: + load: BEFORE + required: false + join-classpath: true diff --git a/adapters/bukkit/plugin/src/main/resources/plugin.yml b/adapters/bukkit/plugin/src/main/resources/plugin.yml new file mode 100644 index 000000000..65e3c0a88 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/resources/plugin.yml @@ -0,0 +1,35 @@ +name: ${name} +version: ${version} +main: ${main} +folia-supported: true +load: STARTUP +softdepend: + - PlaceholderAPI + - CraftEngine + - Nexo + - ItemsAdder + - SCore + - ExecutableItems + - MythicLib + - MMOItems + - eco + - EcoItems + - MythicMobs + - MythicCrucible + - KGenerators + - WorldEdit +loadbefore: [ Multiverse-Core ] +authors: [ cyberpwn, NextdoorPsycho, Vatuu ] +website: VolmitSoftware.com +description: More than a Dimension! +commands: + iris: + aliases: [ ir, irs ] +permissions: + iris.all: + description: Allows use of the full /iris command tree (worlds, studio, pregen, packs, developer tools). + default: op + iris.treefeller: + description: Allows survival players to fell Iris-managed trees with an axe. + default: op +api-version: '${apiVersion}' diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisApiServiceDiscoveryTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisApiServiceDiscoveryTest.java new file mode 100644 index 000000000..805be1530 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisApiServiceDiscoveryTest.java @@ -0,0 +1,22 @@ +package art.arcane.iris; + +import art.arcane.iris.core.pregenerator.PregenApiSink; +import art.arcane.iris.core.service.IrisApiEventSVC; +import art.arcane.iris.core.service.IrisTerrainSVC; +import art.arcane.iris.util.common.plugin.IrisService; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class IrisApiServiceDiscoveryTest { + @Test + public void bothApiServicesSatisfyTheServiceLoaderRule() { + assertTrue(Iris.isConcreteImplementation(IrisTerrainSVC.class, IrisService.class)); + assertTrue(Iris.isConcreteImplementation(IrisApiEventSVC.class, IrisService.class)); + } + + @Test + public void theEventServiceIsTheSinkThePregeneratorLooksUp() { + assertTrue(PregenApiSink.class.isAssignableFrom(IrisApiEventSVC.class)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisBootstrapFailureContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisBootstrapFailureContractTest.java new file mode 100644 index 000000000..ea0e6a038 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisBootstrapFailureContractTest.java @@ -0,0 +1,65 @@ +package art.arcane.iris; + +import io.papermc.paper.plugin.bootstrap.BootstrapContext; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; +import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; +import io.papermc.paper.plugin.lifecycle.event.handler.configuration.LifecycleEventHandlerConfiguration; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +import org.junit.Test; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IrisBootstrapFailureContractTest { + @Test + public void bootstrapFailureAbortsDatapackDiscoveryInsteadOfDisablingOnlyIris() { + BootstrapContext context = mock(BootstrapContext.class); + ComponentLogger logger = mock(ComponentLogger.class); + CapturingLifecycleManager manager = new CapturingLifecycleManager(); + IllegalStateException cause = new IllegalStateException("unsafe transaction"); + LifecycleEventType eventType = lifecycleEventType(); + when(context.getLogger()).thenReturn(logger); + when(context.getLifecycleManager()).thenReturn(manager); + + IrisBootstrap.armStartupFailure(context, cause, eventType); + + assertSame(eventType, manager.eventType); + IllegalStateException failure = assertThrows(IllegalStateException.class, manager::runCapturedHandler); + assertSame(cause, failure.getCause()); + } + + @SuppressWarnings("unchecked") + private static LifecycleEventType lifecycleEventType() { + return (LifecycleEventType) mock(LifecycleEventType.class); + } + + private static final class CapturingLifecycleManager implements LifecycleEventManager { + private LifecycleEventType eventType; + private LifecycleEventHandler handler; + + @Override + public void registerEventHandler( + LifecycleEventType eventType, + LifecycleEventHandler eventHandler + ) { + this.eventType = eventType; + this.handler = eventHandler; + } + + @Override + public void registerEventHandler( + LifecycleEventHandlerConfiguration handlerConfiguration + ) { + throw new AssertionError("The direct handler overload must be used."); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void runCapturedHandler() { + ((LifecycleEventHandler) handler).run(null); + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisBootstrapWorldStorageContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisBootstrapWorldStorageContractTest.java new file mode 100644 index 000000000..8e9216bec --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisBootstrapWorldStorageContractTest.java @@ -0,0 +1,194 @@ +package art.arcane.iris; + +import art.arcane.iris.core.lifecycle.BukkitStartupPaths; +import art.arcane.iris.core.lifecycle.MissingWorldStorageLog; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * The server enumerates levels from disk and CraftBukkit falls back to the vanilla generator whenever the + * plugin cannot supply one, so a world with storage Iris cannot use has to stop startup before any level is + * created. A world with no storage at all is only reported. + */ +public class IrisBootstrapWorldStorageContractTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void clearOrphanReports() { + MissingWorldStorageLog.reset(); + } + + @Test + public void startupIsRefusedWhenAConfiguredWorldLostItsPackSnapshot() throws Exception { + Path serverRoot = temporaryFolder.newFolder("broken-server").toPath(); + Files.createDirectories(serverRoot.resolve("world/dimensions/iris/orphan2/region")); + Files.writeString(serverRoot.resolve("world/dimensions/iris/orphan2/region/r.0.0.mca"), "terrain"); + writeServerProperties(serverRoot); + writeBukkitWorlds(serverRoot, "world_iris_orphan2"); + + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot))); + + assertTrue(failure.getMessage(), failure.getMessage().contains("world_iris_orphan2")); + assertTrue(failure.getMessage(), + failure.getMessage().contains(serverRoot.toRealPath().resolve("world/dimensions/iris/orphan2") + .toString())); + } + + /** + * Deleting a live world's folder and letting the server save it back leaves a data/ skeleton with no + * regions and no iris/ directory. It owns nothing, so refusing to boot over it only strands the server. + */ + @Test + public void startupContinuesForAServerRewrittenHuskAndReportsItOnce() throws Exception { + Path serverRoot = temporaryFolder.newFolder("husk-server").toPath(); + Files.createDirectories(serverRoot.resolve("world/dimensions/iris/husk/data/paper")); + Files.writeString(serverRoot.resolve("world/dimensions/iris/husk/data/paper/level_overrides.dat"), "x"); + writeServerProperties(serverRoot); + writeBukkitWorlds(serverRoot, "world_iris_husk"); + + IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot)); + + assertTrue(MissingWorldStorageLog.hasWarned("world_iris_husk")); + } + + /** + * Both classifications of a hot-deleted world's husk stop the boot: UNUSABLE trips Iris' fail-closed + * guard, and EMPTY excludes it from the dimension registry, which trips Paper's interactive world + * migration gate. The husk has to leave the dimensions tree before either can happen. + */ + @Test + public void aHotDeletedWorldsHuskIsMovedOutOfTheDimensionsTree() throws Exception { + Path serverRoot = temporaryFolder.newFolder("husk-quarantine").toPath(); + Files.createDirectories(serverRoot.resolve("world/dimensions/iris/husk/data/paper")); + Files.writeString(serverRoot.resolve("world/dimensions/iris/husk/data/paper/level_overrides.dat"), "x"); + writeServerProperties(serverRoot); + writeBukkitWorlds(serverRoot, "world_iris_husk"); + List warnings = new ArrayList<>(); + + IrisBootstrap.quarantineWorthlessHusks(startupPaths(serverRoot), warnings::add); + IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot)); + + assertFalse(Files.exists(serverRoot.resolve("world/dimensions/iris/husk"))); + assertEquals(1, warnings.size()); + assertTrue(warnings.getFirst(), + warnings.getFirst().contains("/iris remove world=world_iris_husk delete=true")); + assertTrue(MissingWorldStorageLog.hasWarned("world_iris_husk")); + } + + /** + * Paper enumerates the dimensions tree from disk, so a husk whose bukkit.yml entry is already gone stops + * the boot exactly the same way. The sweep is over the tree, not over the configuration. + */ + @Test + public void aHuskWithNoBukkitConfigurationEntryIsStillMovedOut() throws Exception { + Path serverRoot = temporaryFolder.newFolder("husk-unconfigured").toPath(); + Files.createDirectories(serverRoot.resolve("world/dimensions/iris/husk/data/minecraft")); + Files.writeString(serverRoot.resolve("world/dimensions/iris/husk/data/minecraft/raids.dat"), "x"); + writeServerProperties(serverRoot); + writeBukkitWorlds(serverRoot); + List warnings = new ArrayList<>(); + + IrisBootstrap.quarantineWorthlessHusks(startupPaths(serverRoot), warnings::add); + + assertFalse(Files.exists(serverRoot.resolve("world/dimensions/iris/husk"))); + assertEquals(1, warnings.size()); + } + + @Test + public void aWorldWithRealDataIsNeverQuarantinedAndStillStopsStartup() throws Exception { + Path serverRoot = temporaryFolder.newFolder("husk-with-data").toPath(); + Files.createDirectories(serverRoot.resolve("world/dimensions/iris/kept/region")); + Files.writeString(serverRoot.resolve("world/dimensions/iris/kept/region/r.0.0.mca"), "terrain"); + writeServerProperties(serverRoot); + writeBukkitWorlds(serverRoot, "world_iris_kept"); + List warnings = new ArrayList<>(); + + IrisBootstrap.quarantineWorthlessHusks(startupPaths(serverRoot), warnings::add); + + assertTrue(Files.isDirectory(serverRoot.resolve("world/dimensions/iris/kept"))); + assertTrue(warnings.isEmpty()); + assertThrows( + IllegalStateException.class, + () -> IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot))); + } + + /** + * The engine rebuilds iris/engine-data under a world folder the server itself recreated during save-all, + * so an iris/ directory with no pack snapshot is the ambiguous state, not a worthless one. + */ + @Test + public void anIrisMarkerWithNoPackSnapshotStillFailsClosed() throws Exception { + Path serverRoot = temporaryFolder.newFolder("husk-marker").toPath(); + Files.createDirectories(serverRoot.resolve("world/dimensions/iris/marked/iris/engine-data")); + writeServerProperties(serverRoot); + writeBukkitWorlds(serverRoot, "world_iris_marked"); + List warnings = new ArrayList<>(); + + IrisBootstrap.quarantineWorthlessHusks(startupPaths(serverRoot), warnings::add); + + assertTrue(Files.isDirectory(serverRoot.resolve("world/dimensions/iris/marked/iris/engine-data"))); + assertTrue(warnings.isEmpty()); + assertThrows( + IllegalStateException.class, + () -> IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot))); + } + + @Test + public void startupContinuesForAWorldWhoseFolderIsGoneAndReportsItOnce() throws Exception { + Path serverRoot = temporaryFolder.newFolder("orphan-server").toPath(); + Files.createDirectories(serverRoot.resolve("world/dimensions/iris")); + writeServerProperties(serverRoot); + writeBukkitWorlds(serverRoot, "world_iris_orphan1"); + + IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot)); + + assertTrue(MissingWorldStorageLog.hasWarned("world_iris_orphan1")); + } + + @Test + public void startupContinuesForHealthyWorlds() throws Exception { + Path serverRoot = temporaryFolder.newFolder("healthy-server").toPath(); + Files.createDirectories(serverRoot.resolve("world/dimensions/iris/moon/iris/pack")); + writeServerProperties(serverRoot); + writeBukkitWorlds(serverRoot, "world_iris_moon"); + + IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot)); + + assertFalse(MissingWorldStorageLog.hasWarned("world_iris_moon")); + } + + private static BukkitStartupPaths startupPaths(Path serverRoot) throws Exception { + return BukkitStartupPaths.resolve(serverRoot, new String[0]); + } + + private static void writeServerProperties(Path serverRoot) throws Exception { + Files.writeString(serverRoot.resolve("server.properties"), "level-name=world\n", StandardCharsets.UTF_8); + } + + private static void writeBukkitWorlds(Path serverRoot, String... configuredWorldNames) throws Exception { + File configuration = serverRoot.resolve("bukkit.yml").toFile(); + YamlConfiguration yaml = new YamlConfiguration(); + for (String configuredWorldName : configuredWorldNames) { + yaml.set("worlds." + configuredWorldName + ".generator", "Iris:overworld"); + } + yaml.save(configuration); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticLogLevelTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticLogLevelTest.java new file mode 100644 index 000000000..bcb0649c4 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticLogLevelTest.java @@ -0,0 +1,89 @@ +package art.arcane.iris; + +import art.arcane.iris.spi.LogLevel; +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Level; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Core states a severity on every message it logs and the modded adapters honour it. On Bukkit the same + * message used to become a coloured console line, which the server logs at INFO, so no core warning ever + * appeared in a WARN-level scan of logs/latest.log - including the orphaned-world-storage reports the + * bootstrap replays there specifically for operators to find. + */ +public class IrisDiagnosticLogLevelTest { + @Test + public void coreDiagnosticsKeepTheirSeverity() { + assertEquals(Level.WARNING, Iris.diagnosticLevel(LogLevel.WARN)); + assertEquals(Level.SEVERE, Iris.diagnosticLevel(LogLevel.ERROR)); + } + + @Test + public void informationalAndDebugMessagesStayOnTheConsolePath() { + assertNull(Iris.diagnosticLevel(LogLevel.INFO)); + assertNull(Iris.diagnosticLevel(LogLevel.DEBUG)); + } + + /** + * Console sender output reaches the terminal but not the instance's logs/latest.log, which is the only + * log most operators read after the fact. A handful of lifecycle lines go to the plugin logger instead. + */ + @Test + public void lifecycleNoticesReachThePluginLoggerAtInfo() { + assertEquals(Level.INFO, Iris.diagnosticLevel(LogLevel.NOTICE)); + } + + /** + * A warning raised by the adapter is the same kind of thing as a warning raised by core. Routing one + * through the plugin logger and the other through the console sender makes the level depend on which + * side of the SPI the call happened to be written on. + */ + @Test + public void adapterSideWarningsCarryTheSameSeverityAsCoreWarnings() throws Exception { + String source = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")).replace("\r\n", "\n"); + + String warn = method(source, "public static void warn(String format, Object... objs)"); + assertTrue(warn, warn.contains("diagnostic(Level.WARNING")); + assertFalse(warn, warn.contains("msg(")); + + String error = method(source, "public static void error(String format, Object... objs)"); + assertTrue(error, error.contains("diagnostic(Level.SEVERE")); + assertFalse(error, error.contains("msg(")); + } + + @Test + public void theCoreLogBridgeRoutesDiagnosticsToThePluginLogger() throws Exception { + String source = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")).replace("\r\n", "\n"); + String bridge = method(source, "private static void bridgeLog(LogLevel level, String message)"); + + assertTrue(bridge, bridge.contains("diagnosticLevel(target)")); + assertTrue(bridge, bridge.contains("diagnostic(diagnostic, message)")); + assertTrue(source.contains("plugin.getLogger().log(level, line)")); + } + + private static String method(String source, String signature) { + int start = source.indexOf(signature); + assertTrue("method not found: " + signature, start >= 0); + int open = source.indexOf('{', start); + int depth = 0; + for (int i = open; i < source.length(); i++) { + char c = source.charAt(i); + if (c == '{') { + depth++; + } else if (c == '}') { + depth--; + if (depth == 0) { + return source.substring(start, i + 1); + } + } + } + throw new AssertionError("unterminated method: " + signature); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticsTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticsTest.java new file mode 100644 index 000000000..279409e80 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticsTest.java @@ -0,0 +1,91 @@ +package art.arcane.iris; + +import art.arcane.iris.core.splash.IrisSplashPackScanner; +import art.arcane.iris.core.service.CommandSVC; +import art.arcane.iris.util.common.plugin.IrisService; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisDiagnosticsTest { + @Test + public void serviceInitializationSkipsPackageHelperClasses() throws Exception { + Class registrar = Class.forName("art.arcane.iris.core.commands.PaperCommandRegistrar"); + + assertFalse(Iris.isConcreteImplementation(registrar, IrisService.class)); + assertTrue(Iris.isConcreteImplementation(CommandSVC.class, IrisService.class)); + // The service package is eagerly Class.forName-scanned by JarScanner at enable; a class + // referencing Paper-only types there prints an NCDFE stack trace on plain Spigot. + assertFalse( + "Paper-only command registrar must stay out of the JarScanner-scanned services package", + "art.arcane.iris.core.service".equals(registrar.getPackageName())); + } + + @Test + public void reportErrorWithContextPrintsFullStacktrace() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + PrintStream originalErr = System.err; + System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8)); + try { + Iris.reportError("Runtime world creation failed.", new IllegalStateException("outer", new IllegalArgumentException("inner"))); + } finally { + System.setErr(originalErr); + } + + String text = output.toString(StandardCharsets.UTF_8); + assertTrue(text.contains("Runtime world creation failed.")); + assertTrue(text.contains("IllegalStateException")); + assertTrue(text.contains("IllegalArgumentException")); + assertTrue(text.contains("inner")); + } + + @Test + public void splashPackScanSkipsInternalAndInvalidFolders() throws Exception { + Path root = Files.createTempDirectory("iris-splash"); + try { + Path validPack = root.resolve("overworld"); + Files.createDirectories(validPack.resolve("dimensions")); + Files.writeString(validPack.resolve("dimensions").resolve("overworld.json"), "{\"version\":\"4000\"}"); + + Files.createDirectories(root.resolve("datapack-imports")); + + Path brokenPack = root.resolve("broken"); + Files.createDirectories(brokenPack.resolve("dimensions")); + Files.writeString(brokenPack.resolve("dimensions").resolve("broken.json"), "{"); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + PrintStream originalErr = System.err; + System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8)); + List packs; + try { + packs = IrisSplashPackScanner.collect(root.toFile(), Iris::reportError); + } finally { + System.setErr(originalErr); + } + + assertEquals(1, packs.size()); + assertEquals("overworld", packs.get(0).name()); + assertEquals("4000", packs.get(0).version()); + + String text = output.toString(StandardCharsets.UTF_8); + assertTrue(text.contains("Failed to read splash metadata for dimension pack \"broken\".")); + assertTrue(text.contains("Json")); + } finally { + Files.walk(root) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisShutdownOrderingTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisShutdownOrderingTest.java new file mode 100644 index 000000000..dadb1b80a --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisShutdownOrderingTest.java @@ -0,0 +1,166 @@ +package art.arcane.iris; + +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 IrisShutdownOrderingTest { + @Test + public void drainWorldGenerators_closesGeneratorsWithoutEagerMantleSave() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.startupSource"))); + String drain = section(source, "private void drainWorldGenerators", "private void setupPapi"); + + assertTrue("Shutdown must close each Iris generator", drain.contains("generator.closeAsync()")); + assertFalse("Shutdown must not close Mantle plates before generation drains", drain.contains("saveAllNow()")); + } + + @Test + public void serverStop_defersRuntimeTeardownUntilPaperShutdownBoundary() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.startupSource"))); + String onDisable = section(source, "public void onDisable()", "public void onPreUnload"); + String quiesce = section(source, "private void quiesceRuntimeForServerShutdown", "private void startPostStopFinisher"); + String shutdownHook = section(source, "private void runShutdownHook()", "private boolean awaitServerShutdownBoundary"); + String finisher = section(source, "private void startPostStopFinisher()", "static boolean awaitServerThreadTermination"); + String serverJoin = section(source, "static boolean awaitServerThreadTermination", "private void runShutdownHook"); + String generatorSource = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource"))); + String generatorQuiesce = section(generatorSource, + "public void quiesceForServerShutdown()", "public boolean isStudio()"); + + assertOrdered(onDisable, + "if (serverStopping)", + "quiesceRuntimeForServerShutdown(\"onDisable\")", + "startPostStopFinisher()", + "else", + "teardownRuntime(\"onDisable\", 30L)"); + assertOrdered(shutdownHook, + "awaitServerShutdownBoundary()", + "finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)"); + assertOrdered(finisher, + "finisher.setDaemon(false)", + "finisher.start()"); + assertOrdered(finisher, + "awaitServerThreadTermination(activeServerThread)", + "finishDeferredRuntimeTeardown(\"post-server-stop\", 30L)"); + assertOrdered(serverJoin, + "serverThread == Thread.currentThread()", + "serverThread.join()"); + assertTrue("Server-stop quiescence must leave queued Paper generation admitted", + quiesce.contains("generator.quiesceForServerShutdown()")); + assertOrdered(quiesce, + "jigsawStudioService.quiesceForServerShutdown()", + "PregeneratorJob.shutdownAndWait", + "generator.quiesceForServerShutdown()"); + assertOrdered(onDisable, + "quiesceRuntimeForServerShutdown(\"onDisable\")", + "super.onDisable()"); + assertFalse("Server-stop quiescence must not begin generator close before Paper's boundary", + generatorQuiesce.contains("closing = true")); + assertFalse("Server-stop quiescence must not dispatch generator close before Paper's boundary", + generatorQuiesce.contains("closeAsync()")); + } + + @Test + public void deferredTeardown_keepsGeneratorClosingServicesBehindGeneratorDrain() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.startupSource"))); + String engineService = Files.readString(Path.of(System.getProperty("iris.engineSvcSource"))); + String studioService = Files.readString(Path.of(System.getProperty("iris.studioSvcSource"))); + String generatorSource = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource"))); + String teardown = section(source, "private void teardownRuntime", "private void quiesceRuntimeForServerShutdown"); + String engineDisable = section(engineService, "public void onDisable()", "public void engineStatus"); + String studioDisable = section(studioService, "public void onDisable()", "public IrisDimension installIntoWorld"); + String generatorClose = section(generatorSource, + "public void close()", "public CompletableFuture closeAsync()"); + String generatorCloseAsync = section(generatorSource, + "public CompletableFuture closeAsync()", "public void quiesceForServerShutdown()"); + + assertOrdered(teardown, + "drainWorldGenerators(reason, timeoutSeconds)", + "service.onDisable()", + "MultiBurst.burst::close", + "MultiBurst.ioBurst::close"); + assertTrue("Engine service must retain its generator close behind deferred service teardown", + engineDisable.contains("startClose(")); + assertTrue("Studio service must retain its generator close behind deferred service teardown", + studioDisable.contains("generator.close()")); + assertTrue("Service-level generator close must delegate to the shared idempotent close future", + generatorClose.contains("closeAsync()")); + assertTrue("Repeated post-boundary closes must return the already-published close future", + generatorCloseAsync.contains("return existing;")); + assertFalse("A completed generator close must never be re-dispatched", + generatorCloseAsync.contains("!existing.isDone()")); + } + + @Test + public void preUnload_doesNotDrainGeneratorsDuringServerStop() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.startupSource"))); + String preUnload = section(source, "public void onPreUnload", "private void drainOnce"); + + assertOrdered(preUnload, + "IrisToolbelt.isServerStopping()", + "quiesceRuntimeForServerShutdown", + "startPostStopFinisher()", + "return;", + "drainOnce("); + } + + @Test + public void jigsawStudio_quiescesOnceBeforeDeferredServiceTeardown() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.jigsawStudioSvcSource"))); + String enable = section(source, "public void onEnable()", "public void onDisable()"); + String disable = section(source, "public void onDisable()", "public void quiesceForServerShutdown()"); + String quiesce = section(source, "public void quiesceForServerShutdown()", "public void register("); + String register = section(source, "public void register(", "public void activationCommitted("); + String activation = section(source, "public void activationCommitted(", "public void markChunkGenerated("); + String chunkGenerated = section(source, "public void markChunkGenerated(", "private void markChunkAvailable("); + + assertOrdered(enable, "disableStarted.set(false)", "enabled = true"); + assertTrue("Deferred service teardown must reuse the idempotent early disable", + disable.contains("quiesceForServerShutdown();")); + assertOrdered(quiesce, + "disableStarted.compareAndSet(false, true)", + "finalizeAllJigsawTileWatches()", + "drainAutosavesBeforeDisable()", + "enabled = false", + "activeMenuController.closeAll()", + "previewRenderer.removeAll()", + "studios.clear()"); + assertTrue("Registration must reject work after shutdown begins", + occurrences(register, "!enabled || disableStarted.get()") >= 2); + assertTrue("Activation must reject work after shutdown begins", + activation.contains("!enabled || disableStarted.get()")); + assertTrue("Chunk-generation callbacks must reject work after shutdown begins", + occurrences(chunkGenerated, "!enabled || disableStarted.get()") >= 2); + } + + 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); + } + + private static void assertOrdered(String source, String... markers) { + int previous = -1; + for (String marker : markers) { + int current = source.indexOf(marker); + assertTrue("Missing source marker " + marker, current >= 0); + assertTrue("Source marker is out of order: " + marker, current > previous); + previous = current; + } + } + + private static int occurrences(String source, String marker) { + int count = 0; + int offset = 0; + while ((offset = source.indexOf(marker, offset)) >= 0) { + count++; + offset += marker.length(); + } + return count; + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/PaperPluginMetadataTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/PaperPluginMetadataTest.java new file mode 100644 index 000000000..c85e359a9 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/PaperPluginMetadataTest.java @@ -0,0 +1,121 @@ +package art.arcane.iris; + +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginLoadOrder; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.junit.Test; + +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class PaperPluginMetadataTest { + private static final List OPTIONAL_PLUGIN_IDS = List.of( + "PlaceholderAPI", + "CraftEngine", + "Nexo", + "ItemsAdder", + "SCore", + "ExecutableItems", + "MythicLib", + "MMOItems", + "eco", + "EcoItems", + "MythicMobs", + "MythicCrucible", + "KGenerators", + "Multiverse-Core", + "WorldEdit" + ); + private static final List JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream() + .filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId)) + .toList(); + private static final List BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream() + .filter(pluginId -> !"Multiverse-Core".equals(pluginId)) + .toList(); + + @Test + public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception { + String metadata; + try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/paper-plugin.yml")) { + assertNotNull(stream); + metadata = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + + assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName())); + assertTrue(metadata.contains("folia-supported: true")); + assertTrue(metadata.contains("load: STARTUP")); + assertFalse(metadata.contains("commands:")); + assertTrue(metadata.contains("permissions:\n iris.all:\n" + + " description: Allows use of the full /iris command tree (worlds, studio, pregen, packs, developer tools).\n" + + " default: op\n" + + " iris.treefeller:\n" + + " description: Allows survival players to fell Iris-managed trees with an axe.\n" + + " default: op")); + for (String pluginId : JOINED_PLUGIN_IDS) { + assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true))); + } + assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false))); + assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true))); + } + + @Test + public void bukkitMetadataRetainsStartupCommandAndAliases() throws Exception { + PluginDescriptionFile metadata; + try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/plugin.yml")) { + assertNotNull(stream); + metadata = new PluginDescriptionFile(stream); + } + + assertEquals(Iris.class.getName(), metadata.getMain()); + assertEquals(PluginLoadOrder.STARTUP, metadata.getLoad()); + Map> commands = metadata.getCommands(); + assertTrue(commands.containsKey("iris")); + assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases")); + assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend()); + assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins()); + Permission all = metadata.getPermissions().stream() + .filter(permission -> "iris.all".equals(permission.getName())) + .findFirst() + .orElse(null); + assertNotNull(all); + assertEquals( + "Allows use of the full /iris command tree (worlds, studio, pregen, packs, developer tools).", + all.getDescription()); + assertEquals(PermissionDefault.OP, all.getDefault()); + Permission treeFeller = metadata.getPermissions().stream() + .filter(permission -> "iris.treefeller".equals(permission.getName())) + .findFirst() + .orElse(null); + assertNotNull(treeFeller); + assertEquals("Allows survival players to fell Iris-managed trees with an axe.", treeFeller.getDescription()); + assertEquals(PermissionDefault.OP, treeFeller.getDefault()); + } + + @Test + public void processedResourcesContainBothMetadataFormats() throws Exception { + URL paperMetadata = PaperPluginMetadataTest.class.getResource("/paper-plugin.yml"); + assertNotNull(paperMetadata); + assertEquals("file", paperMetadata.getProtocol()); + Path bukkitMetadata = Path.of(paperMetadata.toURI()).resolveSibling("plugin.yml"); + assertTrue(Files.isRegularFile(bukkitMetadata)); + assertFalse(Files.readString(bukkitMetadata, StandardCharsets.UTF_8).contains("${")); + } + + private static String optionalDependencyBlock(String pluginId, String loadOrder, boolean joinClasspath) { + return " " + pluginId + ":\n" + + " load: " + loadOrder + "\n" + + " required: false\n" + + " join-classpath: " + joinClasspath + "\n"; + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/IrisApiSurfacePurityTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/IrisApiSurfacePurityTest.java new file mode 100644 index 000000000..164935583 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/IrisApiSurfacePurityTest.java @@ -0,0 +1,154 @@ +package art.arcane.iris.api; + +import art.arcane.iris.api.terrain.IrisTerrainService; +import org.junit.Test; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.GenericArrayType; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class IrisApiSurfacePurityTest { + private static final String API_PACKAGE = "art.arcane.iris.api"; + private static final List ALLOWED_PREFIXES = List.of( + "java.", + "javax.", + "org.bukkit.", + API_PACKAGE + "." + ); + + @Test + public void everyApiTypeIsReachableWithoutIris() throws IOException, URISyntaxException, ClassNotFoundException { + List> apiTypes = apiTypes(); + assertTrue("the api package must contain types", apiTypes.size() >= 11); + + Set violations = new LinkedHashSet<>(); + for (Class apiType : apiTypes) { + collect(apiType.getGenericSuperclass(), violations, apiType); + for (Type implemented : apiType.getGenericInterfaces()) { + collect(implemented, violations, apiType); + } + for (Method method : apiType.getDeclaredMethods()) { + if (!isExported(method.getModifiers())) { + continue; + } + collect(method.getGenericReturnType(), violations, apiType); + for (Type parameter : method.getGenericParameterTypes()) { + collect(parameter, violations, apiType); + } + for (Type thrown : method.getGenericExceptionTypes()) { + collect(thrown, violations, apiType); + } + } + for (Constructor constructor : apiType.getDeclaredConstructors()) { + if (!isExported(constructor.getModifiers())) { + continue; + } + for (Type parameter : constructor.getGenericParameterTypes()) { + collect(parameter, violations, apiType); + } + } + for (Field field : apiType.getDeclaredFields()) { + if (!isExported(field.getModifiers())) { + continue; + } + collect(field.getGenericType(), violations, apiType); + } + } + + assertEquals("Iris API types must only expose java, bukkit and Iris API types: " + violations, + Set.of(), violations); + } + + @Test + public void theTerrainServiceIsPartOfTheScannedSurface() throws IOException, URISyntaxException, ClassNotFoundException { + assertTrue(apiTypes().contains(IrisTerrainService.class)); + } + + private static boolean isExported(int modifiers) { + return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers); + } + + private static void collect(Type type, Set violations, Class owner) { + if (type == null) { + return; + } + + switch (type) { + case Class raw -> { + Class component = raw; + while (component.isArray()) { + component = component.getComponentType(); + } + if (component.isPrimitive()) { + return; + } + String name = component.getName(); + if (ALLOWED_PREFIXES.stream().noneMatch(name::startsWith)) { + violations.add(owner.getName() + " -> " + name); + } + } + case ParameterizedType parameterized -> { + collect(parameterized.getRawType(), violations, owner); + for (Type argument : parameterized.getActualTypeArguments()) { + collect(argument, violations, owner); + } + } + case GenericArrayType array -> collect(array.getGenericComponentType(), violations, owner); + case WildcardType wildcard -> { + for (Type bound : wildcard.getUpperBounds()) { + collect(bound, violations, owner); + } + for (Type bound : wildcard.getLowerBounds()) { + collect(bound, violations, owner); + } + } + case TypeVariable variable -> { + for (Type bound : variable.getBounds()) { + collect(bound, violations, owner); + } + } + default -> violations.add(owner.getName() + " -> unresolvable type " + type); + } + } + + private static List> apiTypes() throws IOException, URISyntaxException, ClassNotFoundException { + Path classesRoot = Path.of(IrisTerrainService.class.getProtectionDomain() + .getCodeSource().getLocation().toURI()); + Path apiRoot = classesRoot.resolve(API_PACKAGE.replace('.', '/')); + assertTrue("compiled api package not found at " + apiRoot, Files.isDirectory(apiRoot)); + + List names = new ArrayList<>(); + try (Stream files = Files.walk(apiRoot)) { + files.filter(path -> path.getFileName().toString().endsWith(".class")) + .forEach(path -> names.add(classesRoot.relativize(path).toString() + .replace('/', '.') + .replace('\\', '.') + .replaceAll("\\.class$", ""))); + } + + names.sort(String::compareTo); + List> types = new ArrayList<>(names.size()); + for (String name : names) { + types.add(Class.forName(name, false, IrisApiSurfacePurityTest.class.getClassLoader())); + } + return types; + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/pregen/IrisPregenProgressTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/pregen/IrisPregenProgressTest.java new file mode 100644 index 000000000..e16b259e5 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/pregen/IrisPregenProgressTest.java @@ -0,0 +1,50 @@ +package art.arcane.iris.api.pregen; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class IrisPregenProgressTest { + @Test + public void anAbsentWorldNameFallsBackToTheIdentity() { + IrisPregenProgress progress = new IrisPregenProgress( + null, "minecraft:world", 12D, 1L, 2L, 1L, 0L, 3D, 4L, 5L, null, false); + + assertEquals("minecraft:world", progress.worldName()); + assertEquals("", progress.method()); + } + + @Test + public void hostileNumbersAreNormalisedRatherThanPropagated() { + IrisPregenProgress progress = new IrisPregenProgress( + "world", "minecraft:world", 400D, -1L, -2L, -3L, -4L, -5D, -6L, -7L, "hybrid", true); + + assertEquals(100D, progress.percent(), 0D); + assertEquals(0L, progress.generatedChunks()); + assertEquals(0L, progress.totalChunks()); + assertEquals(0L, progress.remainingChunks()); + assertEquals(0L, progress.failedChunks()); + assertEquals(0D, progress.chunksPerSecond(), 0D); + assertEquals(0L, progress.etaMillis()); + assertEquals(0L, progress.elapsedMillis()); + } + + @Test + public void nonFiniteRatesCollapseToZeroInsteadOfLeaking() { + IrisPregenProgress nan = new IrisPregenProgress( + "world", "minecraft:world", Double.NaN, 0L, 0L, 0L, 0L, Double.NaN, 0L, 0L, "hybrid", false); + IrisPregenProgress infinite = new IrisPregenProgress( + "world", "minecraft:world", Double.POSITIVE_INFINITY, 0L, 0L, 0L, 0L, + Double.POSITIVE_INFINITY, 0L, 0L, "hybrid", false); + + assertEquals(0D, nan.percent(), 0D); + assertEquals(0D, nan.chunksPerSecond(), 0D); + assertEquals(0D, infinite.percent(), 0D); + assertEquals(0D, infinite.chunksPerSecond(), 0D); + } + + @Test(expected = NullPointerException.class) + public void aProgressWithoutAWorldIdentityIsRejected() { + new IrisPregenProgress("world", null, 0D, 0L, 0L, 0L, 0L, 0D, 0L, 0L, "hybrid", false); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/terrain/IrisColumnQueryTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/terrain/IrisColumnQueryTest.java new file mode 100644 index 000000000..edf9403f0 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/terrain/IrisColumnQueryTest.java @@ -0,0 +1,77 @@ +package art.arcane.iris.api.terrain; + +import org.junit.Test; + +import java.util.EnumSet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class IrisColumnQueryTest { + @Test + public void fieldsAreCopiedOnBothSidesOfTheBoundary() { + EnumSet supplied = EnumSet.of(IrisColumnField.SURFACE_KIND); + IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 15, 15, 1, supplied); + + supplied.add(IrisColumnField.BIOME_KEY); + assertEquals(EnumSet.of(IrisColumnField.SURFACE_KIND), query.fields()); + + EnumSet returned = query.fields(); + returned.add(IrisColumnField.SURFACE_HEIGHT); + assertEquals(EnumSet.of(IrisColumnField.SURFACE_KIND), query.fields()); + } + + @Test + public void columnCountAndChunkCountAreDecoupledByStride() { + IrisColumnQuery query = IrisColumnQuery.rect( + 0, 0, 6399, 6399, 64, EnumSet.of(IrisColumnField.SURFACE_KIND)); + + assertEquals(10_000L, query.columnCount()); + assertEquals(160_000L, query.chunkCount()); + assertTrue("chunk span must dominate the column count for a strided query", + query.chunkCount() > query.columnCount()); + } + + @Test + public void countsAreExactForASingleChunk() { + IrisColumnQuery query = IrisColumnQuery.rect( + 0, 0, 15, 15, 1, EnumSet.of(IrisColumnField.SURFACE_HEIGHT)); + + assertEquals(256L, query.columnCount()); + assertEquals(1L, query.chunkCount()); + } + + @Test + public void countsSurviveNegativeCoordinates() { + IrisColumnQuery query = IrisColumnQuery.rect( + -32, -32, -1, -1, 8, EnumSet.of(IrisColumnField.SURFACE_HEIGHT)); + + assertEquals(16L, query.columnCount()); + assertEquals(4L, query.chunkCount()); + } + + @Test + public void countsSaturateInsteadOfWrappingNegativePastTheCaps() { + IrisColumnQuery query = IrisColumnQuery.rect( + Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, 1, + EnumSet.of(IrisColumnField.SURFACE_HEIGHT)); + + assertEquals(Long.MAX_VALUE, query.columnCount()); + assertTrue(query.chunkCount() > 0L); + } + + @Test(expected = IllegalArgumentException.class) + public void emptyFieldSetIsRejected() { + IrisColumnQuery.rect(0, 0, 15, 15, 1, EnumSet.noneOf(IrisColumnField.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void invertedBoundsAreRejected() { + IrisColumnQuery.rect(16, 0, 0, 15, 1, EnumSet.of(IrisColumnField.SURFACE_KIND)); + } + + @Test(expected = IllegalArgumentException.class) + public void zeroStrideIsRejected() { + IrisColumnQuery.rect(0, 0, 15, 15, 0, EnumSet.of(IrisColumnField.SURFACE_KIND)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/terrain/IrisWorldInfoTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/terrain/IrisWorldInfoTest.java new file mode 100644 index 000000000..81906fa12 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/terrain/IrisWorldInfoTest.java @@ -0,0 +1,30 @@ +package art.arcane.iris.api.terrain; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class IrisWorldInfoTest { + @Test + public void heightIsDerivedFromTheAbsoluteBounds() { + IrisWorldInfo info = new IrisWorldInfo("overworld", "minecraft:world", 42L, -64, 320, 63, false); + + assertEquals(384, info.height()); + assertEquals(63, info.fluidHeight()); + } + + @Test(expected = IllegalArgumentException.class) + public void collapsedHeightRangeIsRejected() { + new IrisWorldInfo("overworld", "minecraft:world", 42L, 0, 0, 63, false); + } + + @Test(expected = NullPointerException.class) + public void nullDimensionKeyIsRejected() { + new IrisWorldInfo(null, "minecraft:world", 42L, -64, 320, 63, false); + } + + @Test(expected = NullPointerException.class) + public void nullWorldIdentityIsRejected() { + new IrisWorldInfo("overworld", null, 42L, -64, 320, 63, false); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/tree/TreeFellerOptionsTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/tree/TreeFellerOptionsTest.java new file mode 100644 index 000000000..210266e7c --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/tree/TreeFellerOptionsTest.java @@ -0,0 +1,28 @@ +package art.arcane.iris.api.tree; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TreeFellerOptionsTest { + @Test + public void factoriesExposeStableAccessModes() { + TreeFellerOptions standalone = TreeFellerOptions.standalone(); + TreeFellerOptions override = TreeFellerOptions.integrationOverride(75, TreeFellerRunHooks.NONE); + + assertEquals(TreeFellerAccess.STANDALONE, standalone.access()); + assertEquals(0, standalone.durabilityPreservationChance()); + assertEquals(TreeFellerAccess.INTEGRATION_OVERRIDE, override.access()); + assertEquals(75, override.durabilityPreservationChance()); + } + + @Test(expected = IllegalArgumentException.class) + public void chanceBelowRangeIsRejected() { + TreeFellerOptions.integrationOverride(-1, TreeFellerRunHooks.NONE); + } + + @Test(expected = IllegalArgumentException.class) + public void chanceAboveRangeIsRejected() { + TreeFellerOptions.integrationOverride(101, TreeFellerRunHooks.NONE); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/world/IrisWorldEngineEventTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/world/IrisWorldEngineEventTest.java new file mode 100644 index 000000000..f8ba91376 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/world/IrisWorldEngineEventTest.java @@ -0,0 +1,64 @@ +package art.arcane.iris.api.world; + +import art.arcane.iris.api.terrain.IrisWorldInfo; +import org.bukkit.World; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Optional; + +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; + +public class IrisWorldEngineEventTest { + private static final IrisWorldInfo INFO = + new IrisWorldInfo("overworld", "minecraft:world", 7L, -64, 320, -1, false); + + @Test + public void aPhaseIsStillReportableWhenTheEngineCannotBeDescribed() { + IrisWorldEngineEvent event = new IrisWorldEngineEvent(world(), IrisWorldPhase.ENGINE_CLOSING, null); + + assertEquals(IrisWorldPhase.ENGINE_CLOSING, event.getPhase()); + assertFalse(event.getInfo().isPresent()); + } + + @Test + public void aDescribableEngineCarriesItsInfo() { + World world = world(); + IrisWorldEngineEvent event = new IrisWorldEngineEvent(world, IrisWorldPhase.ENGINE_READY, INFO); + + assertSame(world, event.getWorld()); + assertEquals(Optional.of(INFO), event.getInfo()); + } + + @Test + public void theWorldAndPhaseAreAlwaysRequired() { + assertThrows(NullPointerException.class, + () -> new IrisWorldEngineEvent(null, IrisWorldPhase.ENGINE_READY, INFO)); + assertThrows(NullPointerException.class, + () -> new IrisWorldEngineEvent(world(), null, INFO)); + } + + @Test + public void everyPhaseSharesOneHandlerList() { + assertNotNull(IrisWorldEngineEvent.getHandlerList()); + assertSame(IrisWorldEngineEvent.getHandlerList(), + new IrisWorldEngineEvent(world(), IrisWorldPhase.ENGINE_HOTLOADED, null).getHandlers()); + } + + private static World world() { + return (World) Proxy.newProxyInstance( + IrisWorldEngineEventTest.class.getClassLoader(), + new Class[]{World.class}, + (Object proxy, Method method, Object[] arguments) -> switch (method.getName()) { + case "getName", "toString" -> "world"; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == arguments[0]; + default -> throw new UnsupportedOperationException(method.getName()); + }); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/BukkitWorldReconcilerTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/BukkitWorldReconcilerTest.java new file mode 100644 index 000000000..9ab263a4f --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/BukkitWorldReconcilerTest.java @@ -0,0 +1,419 @@ +package art.arcane.iris.core; + +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration; +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Proxy; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class BukkitWorldReconcilerTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @After + public void disableStartupValidation() { + IrisStartupValidation.disable(); + } + + @Test + public void loadLeaseCoversRegistrationThroughExactWorldCompletion() throws Exception { + File configuration = temporaryFolder.newFile("bukkit.yml"); + LifecycleOperationCoordinator coordinator = coordinator(); + FakeBackend backend = new FakeBackend(); + CompletableFuture creation = new CompletableFuture<>(); + backend.creation = creation; + World exactWorld = world(backend.worldKey); + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator); + + CompletableFuture load = reconciler.loadWorld(configuration, backend.worldKey.toString()); + + assertFalse(load.isDone()); + assertEquals(LifecycleOperationCoordinator.OperationKind.WORLD_LOAD, + coordinator.active(LifecycleOperationCoordinator.Domain.WORLD_MUTATION) + .orElseThrow() + .kind()); + assertEquals("Iris:overworld", YamlConfiguration.loadConfiguration(configuration) + .getString("worlds.probe.generator")); + assertThrows(LifecycleOperationCoordinator.BusyException.class, + () -> coordinator.acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_REMOVE, + backend.worldKey.toString())); + + backend.loaded = Optional.of(exactWorld); + creation.complete(exactWorld); + BukkitWorldReconciler.LoadResult result = load.join(); + + assertTrue(result.succeeded()); + assertEquals(BukkitWorldReconciler.ReconciliationStatus.LOADED, result.status()); + assertEquals(BukkitWorldConfiguration.Registration.CREATED, result.registration()); + assertFalse(result.rollbackAttempted()); + assertTrue(coordinator.isIdle()); + } + + @Test + public void failedCreationRollsBackOnlyNewRegistration() throws Exception { + File configuration = temporaryFolder.newFile("bukkit.yml"); + FakeBackend backend = new FakeBackend(); + backend.creation = CompletableFuture.failedFuture(new IllegalStateException("create failed")); + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator()); + + BukkitWorldReconciler.LoadResult result = reconciler.loadWorld(configuration, backend.worldKey.toString()).join(); + + assertEquals(BukkitWorldReconciler.ReconciliationStatus.CREATE_FAILED, result.status()); + assertEquals(BukkitWorldConfiguration.Registration.CREATED, result.registration()); + assertTrue(result.rollbackAttempted()); + assertTrue(result.rollbackSucceeded()); + assertNull(YamlConfiguration.loadConfiguration(configuration).get("worlds.probe")); + } + + @Test + public void failedCreationPreservesPreexistingMatchingRegistration() throws Exception { + File configuration = temporaryFolder.newFile("bukkit.yml"); + BukkitWorldConfiguration.register(configuration, "probe", "overworld", 1337L); + FakeBackend backend = new FakeBackend(); + backend.configuredSeed = 1337L; + backend.creation = CompletableFuture.failedFuture(new IllegalStateException("create failed")); + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator()); + + BukkitWorldReconciler.LoadResult result = reconciler.loadWorld(configuration, backend.worldKey.toString()).join(); + + assertEquals(BukkitWorldConfiguration.Registration.UNCHANGED, result.registration()); + assertFalse(result.rollbackAttempted()); + assertEquals("Iris:overworld", YamlConfiguration.loadConfiguration(configuration) + .getString("worlds.probe.generator")); + assertEquals(1337L, YamlConfiguration.loadConfiguration(configuration) + .getLong("worlds.probe.seed")); + } + + @Test + public void rollbackDoesNotRemoveARegistrationChangedDuringCreation() throws Exception { + File configuration = temporaryFolder.newFile("bukkit.yml"); + FakeBackend backend = new FakeBackend(); + CompletableFuture creation = new CompletableFuture<>(); + backend.creation = creation; + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator()); + CompletableFuture load = reconciler + .loadWorld(configuration, backend.worldKey.toString()); + YamlConfiguration changed = YamlConfiguration.loadConfiguration(configuration); + changed.set("worlds.probe.generator", "Other:generator"); + changed.save(configuration); + + creation.completeExceptionally(new IllegalStateException("create failed")); + BukkitWorldReconciler.LoadResult result = load.join(); + + assertTrue(result.rollbackAttempted()); + assertFalse(result.rollbackSucceeded()); + assertEquals("Other:generator", YamlConfiguration.loadConfiguration(configuration) + .getString("worlds.probe.generator")); + } + + @Test + public void mismatchedCreatedWorldIsNotReportedAsLoaded() throws Exception { + File configuration = temporaryFolder.newFile("bukkit.yml"); + FakeBackend backend = new FakeBackend(); + backend.creation = CompletableFuture.completedFuture(world(new NamespacedKey("iris", "other"))); + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator()); + + BukkitWorldReconciler.LoadResult result = reconciler.loadWorld(configuration, backend.worldKey.toString()).join(); + + assertFalse(result.succeeded()); + assertEquals(BukkitWorldReconciler.ReconciliationStatus.IDENTITY_MISMATCH, result.status()); + assertTrue(result.rollbackSucceeded()); + assertNull(YamlConfiguration.loadConfiguration(configuration).get("worlds.probe")); + } + + @Test + public void mismatchedResolvedWorldIsNotReportedAsLoaded() throws Exception { + File configuration = temporaryFolder.newFile("bukkit.yml"); + FakeBackend backend = new FakeBackend(); + World exactCreatedWorld = world(backend.worldKey); + backend.creation = CompletableFuture.completedFuture(exactCreatedWorld); + backend.loaded = Optional.of(world(new NamespacedKey("iris", "other"))); + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator()); + + BukkitWorldReconciler.LoadResult result = reconciler + .loadWorld(configuration, backend.worldKey.toString()) + .join(); + + assertFalse(result.succeeded()); + assertEquals(BukkitWorldReconciler.ReconciliationStatus.IDENTITY_MISMATCH, result.status()); + assertTrue(result.rollbackSucceeded()); + } + + @Test + public void exactNonIrisWorldIsAnIdentityConflict() throws Exception { + File configuration = temporaryFolder.newFile("bukkit.yml"); + FakeBackend backend = new FakeBackend(); + backend.loaded = Optional.of(world(backend.worldKey)); + backend.irisWorld = false; + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator()); + + BukkitWorldReconciler.LoadResult result = reconciler.loadWorld(configuration, backend.worldKey.toString()).join(); + + assertEquals(BukkitWorldReconciler.ReconciliationStatus.IDENTITY_CONFLICT, result.status()); + assertEquals(0, backend.createCount.get()); + assertTrue(result.rollbackSucceeded()); + } + + @Test + public void unresolvedDimensionAndBusyLifecycleDoNotTouchConfiguration() throws Exception { + File unresolvedConfiguration = temporaryFolder.newFile("unresolved.yml"); + FakeBackend unresolvedBackend = new FakeBackend(); + unresolvedBackend.dimensionResolution = BukkitWorldReconciler.DimensionResolution.failed( + new IllegalStateException("ambiguous")); + BukkitWorldReconciler unresolved = new BukkitWorldReconciler(unresolvedBackend, coordinator()); + + BukkitWorldReconciler.LoadResult unresolvedResult = unresolved + .loadWorld(unresolvedConfiguration, unresolvedBackend.worldKey.toString()) + .join(); + + assertEquals(BukkitWorldReconciler.ReconciliationStatus.DIMENSION_UNRESOLVED, unresolvedResult.status()); + assertEquals(0L, unresolvedConfiguration.length()); + + File busyConfiguration = temporaryFolder.newFile("busy.yml"); + LifecycleOperationCoordinator coordinator = coordinator(); + LifecycleOperationCoordinator.Lease removal = coordinator.acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_REMOVE, + "iris:other"); + FakeBackend busyBackend = new FakeBackend(); + BukkitWorldReconciler busy = new BukkitWorldReconciler(busyBackend, coordinator); + try { + BukkitWorldReconciler.LoadResult busyResult = busy.loadWorld(busyConfiguration, busyBackend.worldKey.toString()).join(); + assertEquals(BukkitWorldReconciler.ReconciliationStatus.BUSY, busyResult.status()); + assertEquals(0, busyBackend.createCount.get()); + assertEquals(0L, busyConfiguration.length()); + } finally { + removal.close(); + } + } + + @Test + public void pendingRestartRefusesWorldLoadBeforeRegistration() throws Exception { + File configuration = temporaryFolder.newFile("bukkit.yml"); + LifecycleOperationCoordinator coordinator = coordinator(); + assertTrue(coordinator.quiesceForRestart(() -> { + })); + FakeBackend backend = new FakeBackend(); + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator); + + BukkitWorldReconciler.LoadResult result = reconciler.loadWorld(configuration, backend.worldKey.toString()).join(); + + assertEquals(BukkitWorldReconciler.ReconciliationStatus.BUSY, result.status()); + assertEquals(LifecycleOperationCoordinator.OperationKind.SERVER_RESTART, + ((LifecycleOperationCoordinator.BusyException) result.failure()).operationKind()); + assertEquals(0, backend.createCount.get()); + assertEquals(0L, configuration.length()); + } + + @Test + public void pendingStartupValidationRefusesWorldLoadBeforeRegistration() throws Exception { + File configuration = temporaryFolder.newFile("startup-pending.yml"); + FakeBackend backend = new FakeBackend(); + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator()); + IrisStartupValidation.begin(); + + BukkitWorldReconciler.LoadResult result = reconciler + .loadWorld(configuration, backend.worldKey.toString()) + .join(); + + assertEquals(BukkitWorldReconciler.ReconciliationStatus.DIMENSION_UNRESOLVED, result.status()); + assertEquals(0, backend.createCount.get()); + assertEquals(0L, configuration.length()); + } + + @Test + public void invalidDimensionRefusesWorldLoadBeforeRegistration() throws Exception { + File configuration = temporaryFolder.newFile("invalid-dimension.yml"); + FakeBackend backend = new FakeBackend(); + backend.validationFailure = new IllegalStateException("pack is invalid"); + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator()); + + BukkitWorldReconciler.LoadResult result = reconciler + .loadWorld(configuration, backend.worldKey.toString()) + .join(); + + assertEquals(BukkitWorldReconciler.ReconciliationStatus.DIMENSION_UNRESOLVED, result.status()); + assertEquals(0, backend.createCount.get()); + assertEquals(0L, configuration.length()); + } + + @Test + public void terminalCreateTimeoutWinsOverLateWorldCompletion() { + NamespacedKey worldKey = new NamespacedKey("iris", "probe"); + CompletableFuture source = new CompletableFuture<>(); + AtomicInteger timeoutActions = new AtomicInteger(); + + CompletableFuture guarded = BukkitWorldReconciler.guardCreateCompletion( + source, + worldKey, + 1L, + timeoutActions::incrementAndGet); + + CompletionException failure = assertThrows(CompletionException.class, guarded::join); + assertTrue(failure.getCause() instanceof TimeoutException); + assertEquals(1, timeoutActions.get()); + source.complete(world(worldKey)); + assertTrue(guarded.isCompletedExceptionally()); + } + + @Test + public void timedOutCreationPreservesNewRegistrationForRestartReconciliation() throws Exception { + File configuration = temporaryFolder.newFile("timeout.yml"); + FakeBackend backend = new FakeBackend(); + backend.creation = CompletableFuture.failedFuture(new TimeoutException("create timed out")); + BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator()); + + BukkitWorldReconciler.LoadResult result = reconciler + .loadWorld(configuration, backend.worldKey.toString()) + .join(); + + assertEquals(BukkitWorldReconciler.ReconciliationStatus.RESTART_REQUIRED, result.status()); + assertFalse(result.rollbackAttempted()); + assertEquals("Iris:overworld", YamlConfiguration.loadConfiguration(configuration) + .getString("worlds.probe.generator")); + } + + private static LifecycleOperationCoordinator coordinator() throws Exception { + Constructor constructor = LifecycleOperationCoordinator.class + .getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } + + private static World world(NamespacedKey worldKey) { + return (World) Proxy.newProxyInstance( + BukkitWorldReconcilerTest.class.getClassLoader(), + new Class[]{World.class}, + (proxy, method, arguments) -> switch (method.getName()) { + case "getKey" -> worldKey; + case "getName" -> worldKey.getKey(); + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == arguments[0]; + case "toString" -> worldKey.toString(); + default -> defaultValue(method.getReturnType()); + }); + } + + private static Object defaultValue(Class type) { + if (!type.isPrimitive()) { + return null; + } + if (type == boolean.class) { + return false; + } + if (type == char.class) { + return '\0'; + } + if (type == byte.class) { + return (byte) 0; + } + if (type == short.class) { + return (short) 0; + } + if (type == int.class) { + return 0; + } + if (type == long.class) { + return 0L; + } + if (type == float.class) { + return 0F; + } + if (type == double.class) { + return 0D; + } + throw new IllegalStateException("Unsupported primitive type: " + type); + } + + private static final class FakeBackend implements BukkitWorldReconciler.Backend { + private final NamespacedKey worldKey; + private final AtomicInteger createCount; + private CompletableFuture creation; + private Optional loaded; + private boolean irisWorld; + private Long configuredSeed; + private BukkitWorldReconciler.DimensionResolution dimensionResolution; + private RuntimeException validationFailure; + + private FakeBackend() { + worldKey = new NamespacedKey("iris", "probe"); + createCount = new AtomicInteger(); + creation = CompletableFuture.completedFuture(null); + loaded = Optional.empty(); + irisWorld = true; + configuredSeed = null; + dimensionResolution = BukkitWorldReconciler.DimensionResolution.resolved("overworld"); + validationFailure = null; + } + + @Override + public Map configuredWorlds() { + return Map.of("probe", "overworld"); + } + + @Override + public Long configuredSeed(String worldName) { + return configuredSeed; + } + + @Override + public String configuredWorldName(NamespacedKey requestedWorldKey) { + return requestedWorldKey.getKey(); + } + + @Override + public NamespacedKey worldKeyFromConfiguration(String configuredWorldName) { + return worldKey; + } + + @Override + public Optional loadedWorld(NamespacedKey requestedWorldKey) { + return worldKey.equals(requestedWorldKey) ? loaded : Optional.empty(); + } + + @Override + public CompletableFuture createWorld(NamespacedKey requestedWorldKey, String dimension, Long seed) { + createCount.incrementAndGet(); + return creation; + } + + @Override + public boolean isIrisWorld(World world) { + return irisWorld; + } + + @Override + public BukkitWorldReconciler.DimensionResolution resolveDimension(NamespacedKey requestedWorldKey) { + return dimensionResolution; + } + + @Override + public void requireDimensionLoadable(NamespacedKey requestedWorldKey, String dimension) { + if (validationFailure != null) { + throw validationFailure; + } + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/IrisStartupAdmissionListenerTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/IrisStartupAdmissionListenerTest.java new file mode 100644 index 000000000..750e807e7 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/IrisStartupAdmissionListenerTest.java @@ -0,0 +1,78 @@ +package art.arcane.iris.core; + +import com.destroystokyo.paper.profile.PlayerProfile; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; +import org.junit.After; +import org.junit.Test; + +import java.net.InetAddress; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; + +public class IrisStartupAdmissionListenerTest { + private final IrisStartupAdmissionListener listener = new IrisStartupAdmissionListener(); + + @After + public void disableValidation() { + IrisStartupValidation.disable(); + } + + @Test + public void pendingStartupValidationDeniesLogin() throws Exception { + IrisStartupValidation.begin(); + AsyncPlayerPreLoginEvent event = event(); + + listener.onAsyncPlayerPreLogin(event); + + assertEquals(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, event.getLoginResult()); + assertNotNull(event.kickMessage()); + } + + @Test + public void invalidDatapacksDenyLogin() throws Exception { + IrisStartupValidation.begin(); + IrisStartupValidation.markDatapacksInvalid("datapack failure"); + IrisStartupValidation.markPacksReady(); + AsyncPlayerPreLoginEvent event = event(); + + listener.onAsyncPlayerPreLogin(event); + + assertEquals(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, event.getLoginResult()); + } + + @Test + public void restartRequiredDeniesLogin() throws Exception { + IrisStartupValidation.begin(); + IrisStartupValidation.requireRestart("restart required"); + IrisStartupValidation.markPacksReady(); + AsyncPlayerPreLoginEvent event = event(); + + listener.onAsyncPlayerPreLogin(event); + + assertEquals(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, event.getLoginResult()); + } + + @Test + public void readyValidationAllowsLogin() throws Exception { + IrisStartupValidation.begin(); + IrisStartupValidation.markDatapacksReady(); + IrisStartupValidation.markPacksReady(); + AsyncPlayerPreLoginEvent event = event(); + + listener.onAsyncPlayerPreLogin(event); + + assertEquals(AsyncPlayerPreLoginEvent.Result.ALLOWED, event.getLoginResult()); + } + + private AsyncPlayerPreLoginEvent event() throws Exception { + return new AsyncPlayerPreLoginEvent( + "ValidationTest", + InetAddress.getLoopbackAddress(), + UUID.randomUUID(), + false, + mock(PlayerProfile.class)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/IrisStartupOrderingTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/IrisStartupOrderingTest.java new file mode 100644 index 000000000..6c587b1c2 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/IrisStartupOrderingTest.java @@ -0,0 +1,49 @@ +package art.arcane.iris.core; + +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertTrue; + +public class IrisStartupOrderingTest { + @Test + public void admissionGateIsRegisteredBeforeStartupValidationBegins() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.startupSource"))); + String onEnable = section(source, "public void onEnable()", "public void onDisable()"); + + assertOrdered(onEnable, + "IrisStartupValidation.begin();", + "registerEvents(new IrisStartupAdmissionListener(), this);", + "enable();"); + } + + @Test + public void externalDatapacksValidateBeforeDimensionPacks() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.startupSource"))); + String enable = section(source, "private boolean enable()", "public void addShutdownHook()"); + + assertOrdered(enable, + "DatapackIngestService.validateOnStartup();", + "generatorResolver.validateAllPacks();"); + } + + 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); + } + + private static void assertOrdered(String source, String... markers) { + int previous = -1; + for (String marker : markers) { + int current = source.indexOf(marker); + assertTrue("Missing source marker " + marker, current >= 0); + assertTrue("Source marker is out of order: " + marker, current > previous); + previous = current; + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/IrisWorldGeneratorResolverTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/IrisWorldGeneratorResolverTest.java new file mode 100644 index 000000000..71818d532 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/IrisWorldGeneratorResolverTest.java @@ -0,0 +1,439 @@ +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.core.pack.PackValidationResult; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.generator.WorldInfo; +import org.mockito.MockedStatic; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +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; +import static org.mockito.Mockito.when; + +public class IrisWorldGeneratorResolverTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @After + public void clearValidationRegistry() { + PackValidationRegistry.clear(); + } + + @Test + public void snapshotValidationIsLazyAndExactRootScoped() throws Exception { + File packRoot = temporaryFolder.newFolder("world", "iris", "pack"); + writeValidPack(packRoot.toPath()); + PackValidationResult unrelatedNamedFailure = new PackValidationResult( + "pack", List.of("unrelated basename failure"), List.of(), 1L); + PackValidationRegistry.publish(unrelatedNamedFailure); + + PackValidationResult result = IrisWorldGeneratorResolver.requireSnapshotLoadable(packRoot); + + assertTrue(result.isLoadable()); + assertEquals(result, PackValidationRegistry.get(packRoot.toPath())); + assertEquals(unrelatedNamedFailure, PackValidationRegistry.get("pack")); + } + + @Test + public void invalidatedSnapshotIsValidatedAgainBeforeAuthorization() throws Exception { + File packRoot = temporaryFolder.newFolder("replace", "iris", "pack"); + writeValidPack(packRoot.toPath()); + assertTrue(IrisWorldGeneratorResolver.requireSnapshotLoadable(packRoot).isLoadable()); + + Files.writeString( + packRoot.toPath().resolve("dimensions/main.json"), + "{", + StandardCharsets.UTF_8); + PackValidationRegistry.remove(packRoot.toPath()); + + assertThrows(BrokenPackException.class, + () -> IrisWorldGeneratorResolver.requireSnapshotLoadable(packRoot)); + PackValidationResult invalid = PackValidationRegistry.get(packRoot.toPath()); + assertNotNull(invalid); + assertFalse(invalid.getBlockingErrors().toString(), invalid.isLoadable()); + } + + @Test + public void startupValidationPublishesFingerprintBoundExactRootResults() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n"); + int validateAll = source.indexOf("public void validateAllPacks()"); + int snapshot = source.indexOf("ServerConfigurator.computePackContentSnapshot(packsRoot)", validateAll); + int perPackFingerprint = source.indexOf("contentSnapshot.packContents()", snapshot); + int exactRootPublish = source.indexOf( + "PackValidationRegistry.publish(packDirectory.toPath(), result, packFingerprint)", + perPackFingerprint); + + assertTrue(validateAll >= 0); + assertTrue(snapshot > validateAll); + assertTrue(perPackFingerprint > snapshot); + assertTrue(exactRootPublish > perPackFingerprint); + } + + @Test + public void paperStartupAliasResolvesToCanonicalRuntimeKey() throws Exception { + File levelRoot = ownedLevelRoot("startup-alias", "moon"); + + assertEquals( + new NamespacedKey("iris", "moon"), + IrisWorldGeneratorResolver.configuredWorldKey("world_iris_moon", "world", levelRoot) + ); + assertEquals( + new NamespacedKey("iris", "moon"), + IrisWorldGeneratorResolver.configuredWorldKey("moon", "world", levelRoot) + ); + assertEquals( + NamespacedKey.minecraft("overworld"), + IrisWorldGeneratorResolver.configuredWorldKey("world", "world", levelRoot) + ); + } + + @Test + public void multiverseRuntimeKeyedNameResolvesToTheStoredIrisWorld() throws Exception { + File levelRoot = ownedLevelRoot("runtime-keyed", "moon"); + + assertEquals( + "mv load names an Iris world iris_ because its keyed creator refuses the namespace", + new NamespacedKey("iris", "moon"), + IrisWorldGeneratorResolver.configuredWorldKey("iris_moon", "world", levelRoot) + ); + } + + @Test + public void runtimeKeyedNameWithoutStorageKeepsItsLiteralKey() throws Exception { + File levelRoot = ownedLevelRoot("runtime-keyed-missing", "elsewhere"); + + assertEquals( + new NamespacedKey("iris", "iris_moon"), + IrisWorldGeneratorResolver.configuredWorldKey("iris_moon", "world", levelRoot) + ); + } + + @Test + public void literalWorldStorageWinsOverTheRuntimeKeyedReading() throws Exception { + File levelRoot = ownedLevelRoot("runtime-keyed-collision", "moon"); + assertTrue(new File(levelRoot, "dimensions/iris/iris_moon").mkdirs()); + + assertEquals( + "a world genuinely created as iris_moon keeps its own identity", + new NamespacedKey("iris", "iris_moon"), + IrisWorldGeneratorResolver.configuredWorldKey("iris_moon", "world", levelRoot) + ); + } + + @Test + public void levelNamedIrisStillResolvesItsOwnStartupNames() throws Exception { + File levelRoot = temporaryFolder.newFolder("level-named-iris", "iris"); + assertTrue(new File(levelRoot, "dimensions/iris/moon").mkdirs()); + + assertEquals( + new NamespacedKey("iris", "moon"), + IrisWorldGeneratorResolver.configuredWorldKey("iris_iris_moon", "iris", levelRoot) + ); + } + + /** + * A refusal has to name the world the admin typed. The generation guard keeps the storage-existence + * check that stops a genuine iris_moon world being mis-mapped; the message must not, or an orphan is + * described as iris:iris_orphan1 and the remedy names a world that never existed. + */ + @Test + public void refusalMessagesNameTheRuntimeKeyedWorldWithoutItsStorage() { + assertEquals( + new NamespacedKey("iris", "orphan1"), + IrisWorldGeneratorResolver.messageWorldKey("iris_orphan1", "world") + ); + assertEquals( + new NamespacedKey("iris", "moon"), + IrisWorldGeneratorResolver.messageWorldKey("world_iris_moon", "world") + ); + assertEquals( + "a world genuinely created as iris_moon keeps its own identity", + new NamespacedKey("iris", "iris_moon"), + IrisWorldGeneratorResolver.messageWorldKey("world_iris_iris_moon", "world") + ); + assertEquals( + NamespacedKey.minecraft("overworld"), + IrisWorldGeneratorResolver.messageWorldKey("world", "world") + ); + } + + private File ownedLevelRoot(String scope, String worldKey) throws Exception { + File levelRoot = temporaryFolder.newFolder(scope, "world"); + assertTrue(new File(levelRoot, "dimensions/iris/" + worldKey).mkdirs()); + return levelRoot; + } + + @Test + public void configuredWorldResolutionUsesOnlyFrozenWorldLocalSnapshot() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n"); + int resolverStart = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator("); + int resolverEnd = source.indexOf("private record FreshValidation", resolverStart); + String resolver = source.substring(resolverStart, resolverEnd); + + int dimensionRoot = resolver.indexOf("IrisWorldStorage.requireFrozenDimensionRoot("); + int currentPlatformRoot = resolver.indexOf("WorldCreatorCompat.persistentDimensionRoot(worldKey)"); + int layoutRefusal = resolver.indexOf( + "Frozen Iris world storage does not match the current platform layout", + currentPlatformRoot + ); + int snapshotRoot = resolver.indexOf("IrisWorldStorage.requireFrozenPackRoot(dimensionRoot)"); + int validation = resolver.indexOf("requireSnapshotLoadable(snapshotRoot)"); + int exactLoad = resolver.indexOf( + "IrisData.get(snapshotRoot).getDimensionLoader().load(id, false)" + ); + int canonicalIdentity = resolver.indexOf(".platformIdentity(worldKey.toString())"); + int resolvedStorage = resolver.indexOf(".worldFolder(dimensionRoot)"); + + assertTrue(dimensionRoot >= 0); + assertTrue(currentPlatformRoot > dimensionRoot); + assertTrue(layoutRefusal > currentPlatformRoot); + assertTrue(snapshotRoot > layoutRefusal); + assertTrue(validation > snapshotRoot); + assertTrue(exactLoad > validation); + assertTrue(canonicalIdentity > exactLoad); + assertTrue(resolvedStorage > canonicalIdentity); + assertFalse(resolver.contains("loadDimension(")); + assertFalse(resolver.contains("loadAnyDimension(")); + assertFalse(resolver.contains("replaceIntoWorld(")); + assertFalse(resolver.contains("installIntoWorld(")); + } + + @Test + public void ownedWorldSnapshotFailureStopsStartupAndRethrows() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n"); + int resolverStart = source.indexOf("public ChunkGenerator resolveDefaultWorldGenerator("); + int resolverEnd = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator(", resolverStart); + String resolver = source.substring(resolverStart, resolverEnd); + + int plotSquaredProbe = resolver.indexOf("isPlotSquaredGeneratorDiscoveryProbe(worldName, id)"); + int probe = resolver.indexOf("isGeneratorDiscoveryProbe(worldName, id)", plotSquaredProbe); + int readiness = resolver.indexOf("IrisStartupValidation.requireWorldCreationReady()"); + int duplicateGuard = resolver.indexOf("requireWorldKeyAvailable(worldName, worldKey)"); + int ownership = resolver.indexOf("requireOwnedWorld(worldName, levelRoot, worldKey)"); + int frozen = resolver.indexOf("return resolveFrozenWorldGenerator(", ownership); + int failureCapture = resolver.indexOf("catch (RuntimeException failure)", frozen); + int report = resolver.indexOf("Iris.reportError(", failureCapture); + int shutdown = resolver.indexOf("Bukkit.shutdown()", report); + int rethrow = resolver.indexOf("throw failure", shutdown); + + assertTrue(plotSquaredProbe >= 0); + assertTrue(probe > plotSquaredProbe); + assertTrue(readiness > probe); + assertTrue(duplicateGuard > readiness); + assertTrue(ownership > duplicateGuard); + assertTrue(frozen > ownership); + assertTrue(failureCapture > frozen); + assertTrue(report > failureCapture); + assertTrue(shutdown > report); + assertTrue(rethrow > shutdown); + + String shutdownScope = "the fail-fast shutdown must stay scoped to the frozen snapshot of an owned world"; + assertEquals(shutdownScope, resolverStart + shutdown, source.indexOf("Bukkit.shutdown()")); + assertEquals(shutdownScope, resolverStart + shutdown, source.lastIndexOf("Bukkit.shutdown()")); + } + + @Test + public void plotSquaredDiscoveryProbeIsIgnoredQuietly() { + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic iris = mockStatic(Iris.class)) { + bukkit.when(() -> Bukkit.getWorld("CheckingPlotSquaredGenerator")).thenReturn(null); + + ChunkGenerator probe = new IrisWorldGeneratorResolver(null) + .resolveDefaultWorldGenerator("CheckingPlotSquaredGenerator", ""); + + assertNull("Iris cannot be used as a PlotSquared base generator", probe); + bukkit.verify(Bukkit::shutdown, never()); + } + } + + @Test + public void plotSquaredSentinelWithDimensionIdUsesNormalOwnershipChecks() throws Exception { + File worldContainer = temporaryFolder.newFolder("plotsquared-non-probe"); + File levelRoot = new File(worldContainer, "world"); + assertTrue(levelRoot.mkdirs()); + + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic iris = mockStatic(Iris.class)) { + Server server = mock(Server.class); + when(server.getLevelDirectory()).thenReturn(levelRoot.toPath()); + bukkit.when(Bukkit::getServer).thenReturn(server); + bukkit.when(Bukkit::getWorldContainer).thenReturn(worldContainer); + bukkit.when(Bukkit::getWorlds).thenReturn(List.of()); + + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> new IrisWorldGeneratorResolver(null) + .resolveDefaultWorldGenerator("CheckingPlotSquaredGenerator", "overworld")); + + assertTrue(failure.getMessage(), failure.getMessage().contains("CheckingPlotSquaredGenerator")); + bukkit.verify(Bukkit::shutdown, never()); + } + } + + @Test + public void discoveryProbeOfLoadedWorldReturnsInertGeneratorQuietly() { + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic iris = mockStatic(Iris.class)) { + bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mock(World.class)); + + ChunkGenerator probe = new IrisWorldGeneratorResolver(null) + .resolveDefaultWorldGenerator("world", ""); + + assertNotNull("Multiverse drops Iris from /mv generators when the probe returns null", probe); + bukkit.verify(Bukkit::shutdown, never()); + IllegalStateException refusal = assertThrows( + IllegalStateException.class, + () -> probe.generateNoise(mock(WorldInfo.class), new Random(), 0, 0, null)); + assertTrue(refusal.getMessage(), refusal.getMessage().contains("'world'")); + } + } + + @Test + public void externalCreateWithoutIrisStorageThrowsWithoutShutdown() throws Exception { + File worldContainer = temporaryFolder.newFolder("external-create"); + File levelRoot = new File(worldContainer, "world"); + assertTrue(levelRoot.mkdirs()); + + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic iris = mockStatic(Iris.class)) { + Server server = mock(Server.class); + when(server.getLevelDirectory()).thenReturn(levelRoot.toPath()); + bukkit.when(Bukkit::getServer).thenReturn(server); + bukkit.when(Bukkit::getWorldContainer).thenReturn(worldContainer); + bukkit.when(Bukkit::getWorlds).thenReturn(List.of()); + + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> new IrisWorldGeneratorResolver(null) + .resolveDefaultWorldGenerator("mvtest", "overworld")); + + assertTrue(failure.getMessage(), failure.getMessage().contains("mvtest")); + assertTrue(failure.getMessage(), failure.getMessage().contains("/iris create")); + bukkit.verify(Bukkit::shutdown, never()); + } + } + + @Test + public void ownedIrisWorldWithUnusableSnapshotStillStopsTheServer() throws Exception { + File worldContainer = temporaryFolder.newFolder("owned-broken"); + File levelRoot = new File(worldContainer, "world"); + assertTrue(levelRoot.mkdirs()); + assertTrue(new File(worldContainer, "world_iris_moon/dimensions/iris/moon").mkdirs()); + + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic iris = mockStatic(Iris.class)) { + Server server = mock(Server.class); + when(server.getLevelDirectory()).thenReturn(levelRoot.toPath()); + bukkit.when(Bukkit::getServer).thenReturn(server); + bukkit.when(Bukkit::getWorldContainer).thenReturn(worldContainer); + bukkit.when(Bukkit::getWorlds).thenReturn(List.of()); + + assertThrows( + IllegalStateException.class, + () -> new IrisWorldGeneratorResolver(null) + .resolveDefaultWorldGenerator("world_iris_moon", "overworld")); + + bukkit.verify(Bukkit::shutdown); + } + } + + @Test + public void vanillaDimensionSlotWithoutFrozenPackIsNotOwned() throws Exception { + File worldContainer = temporaryFolder.newFolder("vanilla-slot"); + File levelRoot = new File(worldContainer, "world"); + assertTrue(new File(levelRoot, "dimensions/minecraft/the_nether").mkdirs()); + + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic iris = mockStatic(Iris.class)) { + Server server = mock(Server.class); + when(server.getLevelDirectory()).thenReturn(levelRoot.toPath()); + bukkit.when(Bukkit::getServer).thenReturn(server); + bukkit.when(Bukkit::getWorldContainer).thenReturn(worldContainer); + bukkit.when(Bukkit::getWorlds).thenReturn(List.of()); + + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> new IrisWorldGeneratorResolver(null) + .resolveDefaultWorldGenerator("world_nether", "overworld")); + + assertTrue(failure.getMessage(), failure.getMessage().contains("minecraft:the_nether")); + assertTrue(failure.getMessage(), failure.getMessage().contains("/iris create")); + bukkit.verify(Bukkit::shutdown, never()); + } + } + + @Test + public void alreadyLoadedIrisKeyIsRefusedBeforeAnyEngineCanStart() throws Exception { + File worldContainer = temporaryFolder.newFolder("duplicate-key"); + File levelRoot = new File(worldContainer, "world"); + assertTrue(levelRoot.mkdirs()); + writeValidPack(new File(levelRoot, "dimensions/iris/irisworld/iris/pack").toPath()); + + World loaded = mock(World.class); + when(loaded.getKey()).thenReturn(new NamespacedKey("iris", "irisworld")); + when(loaded.getName()).thenReturn("world_iris_irisworld"); + + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic iris = mockStatic(Iris.class)) { + Server server = mock(Server.class); + when(server.getLevelDirectory()).thenReturn(levelRoot.toPath()); + bukkit.when(Bukkit::getServer).thenReturn(server); + bukkit.when(Bukkit::getWorldContainer).thenReturn(worldContainer); + bukkit.when(Bukkit::getWorlds).thenReturn(List.of(loaded)); + + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> new IrisWorldGeneratorResolver(null) + .resolveDefaultWorldGenerator("irisworld", "overworld")); + + assertTrue(failure.getMessage(), failure.getMessage().contains("iris:irisworld")); + assertTrue(failure.getMessage(), failure.getMessage().contains("world_iris_irisworld")); + bukkit.verify(Bukkit::shutdown, never()); + } + } + + private static void writeValidPack(Path packRoot) throws Exception { + Files.createDirectories(packRoot.resolve("dimensions")); + Files.createDirectories(packRoot.resolve("regions")); + Files.createDirectories(packRoot.resolve("biomes")); + Files.writeString( + packRoot.resolve("dimensions/main.json"), + "{\"regions\":[\"region\"]}", + StandardCharsets.UTF_8); + Files.writeString( + packRoot.resolve("regions/region.json"), + "{\"landBiomes\":[\"biome\"]}", + StandardCharsets.UTF_8); + Files.writeString( + packRoot.resolve("biomes/biome.json"), + "{\"name\":\"Biome\"}", + StandardCharsets.UTF_8); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/PendingWorldDeleteQueueTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/PendingWorldDeleteQueueTest.java new file mode 100644 index 000000000..af7ee556f --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/PendingWorldDeleteQueueTest.java @@ -0,0 +1,222 @@ +package art.arcane.iris.core; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class PendingWorldDeleteQueueTest { + private static final String QUARANTINE_NAME = ".iris-delete-6a4fd7fd-8e75-4f2f-b9fa-523b90c41f45"; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void acceptsOnlyCanonicalManagedNamesAndStrictQuarantines() { + assertEquals("alpha", PendingWorldDeleteQueue.normalizeQueueEntry("alpha", "world")); + assertEquals("alpha", PendingWorldDeleteQueue.normalizeQueueEntry("iris:alpha", "world")); + assertEquals(QUARANTINE_NAME, PendingWorldDeleteQueue.normalizeQueueEntry(QUARANTINE_NAME, "world")); + + for (String rejected : List.of( + "world", + "world_nether", + "world_the_end", + "overworld", + "the_nether", + "the_end", + "minecraft:overworld", + "minecraft:the_nether", + "minecraft:the_end", + "Alpha", + "alpha beta", + "../alpha", + "alpha/beta", + ".iris-delete-6a4fd7fd-8e75-4f2f-b9fa-523b90c41f4", + ".iris-delete-6A4FD7FD-8E75-4F2F-B9FA-523B90C41F45" + )) { + assertNull(rejected, PendingWorldDeleteQueue.normalizeQueueEntry(rejected, "world")); + } + } + + @Test + public void loadFiltersUnsafeEntriesAndCanonicalizesDuplicates() throws IOException { + File queueFile = temporaryFolder.newFile("pending-world-deletes.txt"); + Files.writeString( + queueFile.toPath(), + String.join("\n", "alpha", "iris:alpha", "world", "../escape", QUARANTINE_NAME), + StandardCharsets.UTF_8 + ); + + LinkedHashMap queue = PendingWorldDeleteQueue.loadPendingWorldDeleteMap(queueFile, "world"); + + assertEquals(List.of("alpha", QUARANTINE_NAME), List.copyOf(queue.values())); + } + + @Test + public void queueFileReplacementIsCompleteAndLeavesNoTemporaryFile() throws IOException { + File queueFile = new File(temporaryFolder.getRoot(), "state/pending-world-deletes.txt"); + LinkedHashMap first = new LinkedHashMap<>(); + first.put("alpha", "alpha"); + first.put(QUARANTINE_NAME, QUARANTINE_NAME); + PendingWorldDeleteQueue.writePendingWorldDeleteMap(queueFile, first); + + assertEquals("alpha\n" + QUARANTINE_NAME + "\n", Files.readString(queueFile.toPath())); + + LinkedHashMap replacement = new LinkedHashMap<>(); + replacement.put("beta", "beta"); + PendingWorldDeleteQueue.writePendingWorldDeleteMap(queueFile, replacement); + + assertEquals("beta\n", Files.readString(queueFile.toPath())); + try (Stream files = Files.list(queueFile.toPath().getParent())) { + assertFalse(files.anyMatch(path -> path.getFileName().toString().contains(".tmp-"))); + } + } + + @Test + public void emptyQueueIsDurablyRepresentedByAnEmptyFile() throws IOException { + File queueFile = new File(temporaryFolder.getRoot(), "pending-world-deletes.txt"); + + PendingWorldDeleteQueue.writePendingWorldDeleteMap(queueFile, new LinkedHashMap<>()); + + assertTrue(queueFile.isFile()); + assertEquals(0L, queueFile.length()); + } + + @Test + public void writeFailuresPropagateToTheCaller() throws IOException { + File parentFile = temporaryFolder.newFile("not-a-directory"); + File queueFile = new File(parentFile, "pending-world-deletes.txt"); + + assertThrows( + IOException.class, + () -> PendingWorldDeleteQueue.writePendingWorldDeleteMap(queueFile, new LinkedHashMap<>()) + ); + } + + @Test + public void discoversOnlyDirectNonSymlinkStartupDirectories() throws IOException { + File levelRoot = temporaryFolder.newFolder("world"); + Path irisNamespace = levelRoot.toPath().resolve("dimensions/iris"); + Files.createDirectories(irisNamespace); + Files.createDirectory(irisNamespace.resolve(QUARANTINE_NAME)); + Files.createDirectory(irisNamespace.resolve(".iris-delete-not-a-uuid")); + String transientName = "iris-45ba411e-bf7c-493a-bf41-aa020754990b"; + Files.createDirectory(irisNamespace.resolve(transientName + "_nether")); + Path nested = irisNamespace.resolve("ordinary/nested"); + Files.createDirectories(nested); + Files.createDirectory(nested.resolve(".iris-delete-f70b8c21-9174-43a2-b7b7-a84fc0b2fe4a")); + Path symlinkTarget = temporaryFolder.newFolder("quarantine-target").toPath(); + Files.createSymbolicLink( + irisNamespace.resolve(".iris-delete-b8c7ff2d-2efd-410d-b228-6da5d5a46c36"), + symlinkTarget + ); + + LinkedHashSet discovered = PendingWorldDeleteQueue.discoverStartupWorldNames(levelRoot); + + assertEquals(Set.of(QUARANTINE_NAME, transientName), discovered); + } + + @Test + public void refusesSymlinkedIrisNamespace() throws IOException { + File levelRoot = temporaryFolder.newFolder("world"); + Path dimensions = levelRoot.toPath().resolve("dimensions"); + Files.createDirectories(dimensions); + Path external = temporaryFolder.newFolder("external-iris").toPath(); + Files.createSymbolicLink(dimensions.resolve("iris"), external); + + assertThrows( + IOException.class, + () -> PendingWorldDeleteQueue.discoverStartupWorldNames(levelRoot) + ); + } + + @Test + public void quarantineEntriesResolveToOnlyTheirExactDirectory() throws IOException { + File levelRoot = temporaryFolder.newFolder("world"); + + List paths = PendingWorldDeleteQueue.resolveQueueEntryPaths(levelRoot, QUARANTINE_NAME); + + assertEquals(List.of( + levelRoot.toPath().resolve("dimensions/iris").resolve(QUARANTINE_NAME).toAbsolutePath() + ), paths); + } + + @Test + public void exactLogicalEntriesDoNotExpandIntoDimensionFamilies() throws IOException { + File levelRoot = temporaryFolder.newFolder("world"); + + List exact = PendingWorldDeleteQueue.resolveQueueEntryPaths(levelRoot, "exact:alpha"); + List family = PendingWorldDeleteQueue.resolveQueueEntryPaths(levelRoot, "alpha"); + + assertEquals(List.of( + levelRoot.toPath().resolve("dimensions/iris/alpha").toAbsolutePath() + ), exact); + assertEquals(List.of( + levelRoot.toPath().resolve("dimensions/iris/alpha").toAbsolutePath(), + levelRoot.toPath().resolve("dimensions/iris/alpha_nether").toAbsolutePath(), + levelRoot.toPath().resolve("dimensions/iris/alpha_the_end").toAbsolutePath() + ), family); + } + + @Test + public void exactLogicalEntryResolvesCurrentCraftBukkitLevelStorage() throws IOException { + Path worldContainer = temporaryFolder.newFolder("configured-delete-server").toPath(); + File levelRoot = Files.createDirectory(worldContainer.resolve("world")).toFile(); + Path configuredLevelRoot = worldContainer.resolve("world_iris_alpha"); + Files.createDirectories(configuredLevelRoot.resolve("dimensions/iris/alpha")); + + assertEquals( + List.of(configuredLevelRoot.toAbsolutePath().normalize()), + PendingWorldDeleteQueue.resolveQueueEntryPaths(levelRoot, "exact:alpha") + ); + } + + @Test + public void failedSafeDeletionSignalsQueueRetentionAndSucceedsOnRetry() throws IOException { + File levelRoot = temporaryFolder.newFolder("retry-world"); + Path quarantine = levelRoot.toPath().resolve("dimensions/iris").resolve(QUARANTINE_NAME); + Files.createDirectories(quarantine); + Path external = temporaryFolder.newFolder("retry-external").toPath(); + Path link = Files.createSymbolicLink(quarantine.resolve("unsafe-link"), external); + + PendingWorldDeleteQueue.EntryDeletionResult first = PendingWorldDeleteQueue.attemptEntry( + levelRoot, + QUARANTINE_NAME, + (key, path) -> false + ); + + assertTrue(first.retainQueueEntry()); + assertEquals(1, first.failures().size()); + assertTrue(Files.isSymbolicLink(link)); + assertTrue(Files.exists(external)); + + Files.delete(link); + Files.writeString(quarantine.resolve("safe.dat"), "safe"); + + PendingWorldDeleteQueue.EntryDeletionResult retry = PendingWorldDeleteQueue.attemptEntry( + levelRoot, + QUARANTINE_NAME, + (key, path) -> false + ); + + assertFalse(retry.retainQueueEntry()); + assertTrue(retry.failures().isEmpty()); + assertFalse(Files.exists(quarantine)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/PendingWorldReplacementManagerPolicyTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/PendingWorldReplacementManagerPolicyTest.java new file mode 100644 index 000000000..ae099e225 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/PendingWorldReplacementManagerPolicyTest.java @@ -0,0 +1,126 @@ +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.ExactWorldSlotPathPolicy.SlotKind; +import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisEnvironment; +import art.arcane.iris.util.common.plugin.VolmitSender; +import org.bukkit.NamespacedKey; +import org.junit.After; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.io.IOException; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +public class PendingWorldReplacementManagerPolicyTest { + @After + public void disableValidation() { + IrisStartupValidation.disable(); + } + + @Test + public void irisManagedSlotsAllowEveryReplacementEnvironment() { + for (IrisEnvironment environment : IrisEnvironment.values()) { + PendingWorldReplacementManager.requireCompatibleEnvironment(SlotKind.IRIS_MANAGED, environment); + } + } + + @Test + public void vanillaSlotsRequireTheirIdentityEnvironment() { + List expectations = List.of( + new EnvironmentExpectation(SlotKind.VANILLA_OVERWORLD, IrisEnvironment.NORMAL), + new EnvironmentExpectation(SlotKind.VANILLA_NETHER, IrisEnvironment.NETHER), + new EnvironmentExpectation(SlotKind.VANILLA_END, IrisEnvironment.THE_END) + ); + + for (EnvironmentExpectation expectation : expectations) { + for (IrisEnvironment environment : IrisEnvironment.values()) { + if (environment == expectation.environment()) { + PendingWorldReplacementManager.requireCompatibleEnvironment( + expectation.slotKind(), + environment + ); + continue; + } + assertThrows( + IllegalArgumentException.class, + () -> PendingWorldReplacementManager.requireCompatibleEnvironment( + expectation.slotKind(), + environment + ) + ); + } + } + } + + @Test + public void disabledVanillaSlotsFailWithoutRestrictingOtherSlots() throws Exception { + PendingWorldReplacementManager.requireVanillaSlotEnabled(SlotKind.IRIS_MANAGED, false, false); + PendingWorldReplacementManager.requireVanillaSlotEnabled(SlotKind.VANILLA_OVERWORLD, false, false); + PendingWorldReplacementManager.requireVanillaSlotEnabled(SlotKind.VANILLA_NETHER, true, false); + PendingWorldReplacementManager.requireVanillaSlotEnabled(SlotKind.VANILLA_END, false, true); + + IOException netherFailure = assertThrows( + IOException.class, + () -> PendingWorldReplacementManager.requireVanillaSlotEnabled( + SlotKind.VANILLA_NETHER, + false, + true + ) + ); + IOException endFailure = assertThrows( + IOException.class, + () -> PendingWorldReplacementManager.requireVanillaSlotEnabled( + SlotKind.VANILLA_END, + true, + false + ) + ); + + assertEquals( + "allow-nether must be true before the vanilla Nether can be replaced.", + netherFailure.getMessage() + ); + assertEquals( + "Bukkit allow-end must be true before the vanilla End can be replaced.", + endFailure.getMessage() + ); + } + + @Test + public void replacementStagingPassesTheRestartBoundaryWithoutUnlockingCreation() { + IrisStartupValidation.begin(); + IrisStartupValidation.markDatapacksReady(); + IrisStartupValidation.markPacksReady(); + IrisStartupValidation.requireRestart("restart boundary"); + PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class)); + VolmitSender sender = mock(VolmitSender.class); + IrisDimension dimension = mock(IrisDimension.class); + + assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldCreationReady); + try (MockedStatic bootstrapMarker = + mockStatic(WorldReplacementBootstrapMarker.class)) { + bootstrapMarker.when(WorldReplacementBootstrapMarker::wasBootstrappedThisProcess).thenReturn(false); + + IOException failure = assertThrows( + IOException.class, + () -> manager.stageReplacement(sender, NamespacedKey.minecraft("the_nether"), dimension, null) + ); + + assertEquals( + "Exact world replacement requires a full Paper-family startup bootstrap.", + failure.getMessage() + ); + } + } + + private record EnvironmentExpectation(SlotKind slotKind, IrisEnvironment environment) { + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/PendingWorldReplacementThreadAffinityTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/PendingWorldReplacementThreadAffinityTest.java new file mode 100644 index 000000000..3300dbcb7 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/PendingWorldReplacementThreadAffinityTest.java @@ -0,0 +1,311 @@ +package art.arcane.iris.core; + +import art.arcane.iris.core.ExactWorldSlotPathPolicy.SlotKind; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot; +import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase; +import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Transaction; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisEnvironment; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +public class PendingWorldReplacementThreadAffinityTest { + @Test + public void runtimeStateIsCapturedIntoAnImmutableDetachedSnapshot() { + World world = mock(World.class); + PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class); + EngineTarget target = mock(EngineTarget.class); + IrisDimension dimension = mock(IrisDimension.class); + when(world.getKey()).thenReturn(NamespacedKey.minecraft("the_nether")); + when(world.getSeed()).thenReturn(-18273645L); + when(world.getEnvironment()).thenReturn(World.Environment.NETHER); + when(generator.getTarget()).thenReturn(target); + when(target.getDimension()).thenReturn(dimension); + when(dimension.getLoadKey()).thenReturn("underworld"); + when(dimension.getEnvironment()).thenReturn(IrisEnvironment.NETHER); + + PendingWorldReplacementManager.PublishedWorldRuntimeState runtimeState; + try (MockedStatic toolbelt = mockStatic(IrisToolbelt.class)) { + toolbelt.when(() -> IrisToolbelt.isIrisWorld(world)).thenReturn(true); + toolbelt.when(() -> IrisToolbelt.access(world)).thenReturn(generator); + runtimeState = PendingWorldReplacementManager.capturePublishedWorldRuntime(world); + } + + assertEquals(WorldSlotKey.minecraft("the_nether"), runtimeState.worldKey()); + assertTrue(runtimeState.irisWorld()); + assertEquals(-18273645L, runtimeState.seed()); + assertEquals(World.Environment.NETHER, runtimeState.bukkitEnvironment()); + assertEquals("underworld", runtimeState.dimension()); + assertEquals(IrisEnvironment.NETHER, runtimeState.dimensionEnvironment()); + } + + @Test + public void runtimeValidationRejectsIdentitySeedAndEnvironmentMismatches() throws Exception { + Transaction transaction = transaction(); + PendingWorldReplacementManager.PublishedWorldRuntimeState valid = runtimeState( + WorldSlotKey.minecraft("the_nether"), + 918273645L, + World.Environment.NETHER, + IrisEnvironment.NETHER + ); + PendingWorldReplacementManager.validatePublishedWorldRuntime( + valid, + transaction, + SlotKind.VANILLA_NETHER + ); + + IOException identityFailure = assertThrows( + IOException.class, + () -> PendingWorldReplacementManager.validatePublishedWorldRuntime( + runtimeState( + WorldSlotKey.minecraft("overworld"), + 918273645L, + World.Environment.NETHER, + IrisEnvironment.NETHER + ), + transaction, + SlotKind.VANILLA_NETHER + ) + ); + IOException seedFailure = assertThrows( + IOException.class, + () -> PendingWorldReplacementManager.validatePublishedWorldRuntime( + runtimeState( + WorldSlotKey.minecraft("the_nether"), + 1L, + World.Environment.NETHER, + IrisEnvironment.NETHER + ), + transaction, + SlotKind.VANILLA_NETHER + ) + ); + IOException bukkitEnvironmentFailure = assertThrows( + IOException.class, + () -> PendingWorldReplacementManager.validatePublishedWorldRuntime( + runtimeState( + WorldSlotKey.minecraft("the_nether"), + 918273645L, + World.Environment.NORMAL, + IrisEnvironment.NETHER + ), + transaction, + SlotKind.VANILLA_NETHER + ) + ); + IllegalArgumentException irisEnvironmentFailure = assertThrows( + IllegalArgumentException.class, + () -> PendingWorldReplacementManager.validatePublishedWorldRuntime( + runtimeState( + WorldSlotKey.minecraft("the_nether"), + 918273645L, + World.Environment.NETHER, + IrisEnvironment.NORMAL + ), + transaction, + SlotKind.VANILLA_NETHER + ) + ); + + assertEquals("Loaded world identity does not match the replacement journal.", identityFailure.getMessage()); + assertEquals("The replaced world loaded with an unexpected seed.", seedFailure.getMessage()); + assertEquals("The replaced world loaded with an unexpected environment.", + bukkitEnvironmentFailure.getMessage()); + assertTrue(irisEnvironmentFailure.getMessage().contains("requires a pack environment of NETHER")); + } + + @Test + public void bukkitAccessIsConfinedToTheGlobalCaptureStage() throws Exception { + String managerSource = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java")).replace("\r\n", "\n"); + String irisSource = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")).replace("\r\n", "\n"); + String startup = method(managerSource, "public void verifyLoadedPublishedWorlds()"); + String worldLoad = method(managerSource, "public void onWorldLoad(WorldLoadEvent event)"); + String discovery = method(managerSource, "private void discoverLoadedPublishedWorlds()"); + String capture = method(managerSource, + "private void captureLoadedPublishedWorldOnGlobal(Transaction transaction)"); + String snapshot = method(managerSource, "static PublishedWorldRuntimeState capturePublishedWorldRuntime(World world)"); + String verification = method(managerSource, + "private void verifyPublishedWorld(PublishedWorldRuntimeState runtimeState, Transaction transaction)"); + + assertTrue(startup.contains("J.a(this::discoverLoadedPublishedWorlds)")); + assertTrue(worldLoad.contains("WorldIdentity.key(event.getWorld())")); + assertTrue(worldLoad.contains("J.a(() -> discoverLoadedWorldTransaction(worldKey))")); + assertTrue(capture.contains("WorldIdentity.resolve(")); + assertBefore(capture, "capturePublishedWorldRuntime(world)", + "J.a(() -> runPublishedWorldVerification(runtimeState, transaction))"); + assertTrue(snapshot.contains("WorldIdentity.key(requiredWorld)")); + assertTrue(snapshot.contains("IrisToolbelt.isIrisWorld(requiredWorld)")); + assertTrue(snapshot.contains("requiredWorld.getSeed()")); + assertTrue(snapshot.contains("requiredWorld.getEnvironment()")); + assertTrue(snapshot.contains("IrisToolbelt.access(requiredWorld)")); + assertNoBukkitRuntimeAccess(discovery); + assertNoBukkitRuntimeAccess(verification); + assertTrue(verification.contains("WorldReplacementFilesystem.fingerprintPack(")); + assertTrue(irisSource.contains("pendingWorldReplacements.verifyLoadedPublishedWorlds();")); + assertFalse(irisSource.contains("J.a(pendingWorldReplacements::verifyLoadedPublishedWorlds)")); + } + + @Test + public void paperLoginHookIsIsolatedFromAlwaysLoadedSpigotClasses() throws Exception { + String managerSource = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java")).replace("\r\n", "\n"); + String irisSource = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")).replace("\r\n", "\n"); + String listenerSource = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java")).replace("\r\n", "\n"); + String registration = method(managerSource, "public void registerPlatformEntryListener()"); + + assertFalse(managerSource.contains("import io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent")); + assertFalse(managerSource.contains("AsyncPlayerSpawnLocationEvent event")); + assertFalse(irisSource.contains("AsyncPlayerSpawnLocationEvent")); + assertTrue(registration.contains("Class.forName(\"io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent\"")); + assertTrue(registration.contains("Class.forName(")); + assertTrue(registration.contains("PaperWorldReplacementEntryListener")); + assertTrue(irisSource.contains("pendingWorldReplacements.registerPlatformEntryListener();")); + assertTrue(listenerSource.contains("onAsyncPlayerSpawnLocation(AsyncPlayerSpawnLocationEvent event)")); + } + + @Test + public void redirectedPlayerReceiptSurvivesUntilTheMaterializedPositionIsSaved() throws Exception { + String managerSource = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java")).replace("\r\n", "\n"); + String listenerSource = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java")).replace("\r\n", "\n"); + String preparation = method( + managerSource, + "ReplacementEntryRedirect prepareReplacementEntry(UUID playerId, Location savedLocation, boolean newPlayer)" + ); + String acknowledgement = method( + managerSource, + "void expectReplacementEntryAcknowledgement(UUID playerId, UUID transactionId)" + ); + String join = method(managerSource, "public void onPlayerJoin(PlayerJoinEvent event)"); + String listener = method( + listenerSource, + "public void onAsyncPlayerSpawnLocation(AsyncPlayerSpawnLocationEvent event)" + ); + + assertTrue(preparation.contains("return new ReplacementEntryRedirect(guard.transactionId(), prepared, pendingPlayer)")); + assertFalse(preparation.substring(preparation.indexOf("CompletableFuture safeEntry")) + .contains("completeOverworldEntry(")); + assertBefore(listener, "event.setSpawnLocation(location)", + "manager.expectReplacementEntryAcknowledgement(playerId, redirect.transactionId())"); + assertTrue(acknowledgement.contains("pendingEntryAcknowledgements.put(playerId, transactionId)")); + assertBefore(join, "event.getPlayer().saveData()", "completeOverworldEntry(playerId, transactionId)"); + } + + @Test + public void replacementSpawnIsPersistedBeforeFinalMarkerRetirement() throws Exception { + String managerSource = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java")).replace("\r\n", "\n"); + String listenerSource = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java")).replace("\r\n", "\n"); + String preparation = method(managerSource, "private void prepareOverworldEntry(World world)"); + String persistence = method(managerSource, "private CompletableFuture persistOverworldSpawn(Location safeEntry)"); + String retirement = method( + managerSource, + "private synchronized void retireOverworldEntryIfCompleteAsync(UUID transactionId)" + ); + String listener = method( + listenerSource, + "public void onAsyncPlayerSpawnLocation(AsyncPlayerSpawnLocationEvent event)" + ); + String generatorSource = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource"))); + + assertBefore(preparation, ".thenCompose(this::applyOverworldSpawn)", + ".thenCompose(this::persistOverworldSpawn)"); + assertBefore(preparation, "targetFuture.complete(safeEntry.clone())", + "retireOverworldEntryIfComplete(guard.transactionId())"); + assertTrue(persistence.contains("world.save()")); + assertTrue(retirement.contains("!current.pendingPlayers().isEmpty()")); + assertTrue(retirement.contains("!safeEntry.isDone()")); + assertTrue(retirement.contains("safeEntry.isCompletedExceptionally()")); + assertTrue(listener.contains("event.isNewPlayer()")); + assertTrue(generatorSource.contains("world.getHighestBlockYAt(initialSpawn) + 1")); + } + + private static PendingWorldReplacementManager.PublishedWorldRuntimeState runtimeState( + WorldSlotKey worldKey, + long seed, + World.Environment bukkitEnvironment, + IrisEnvironment irisEnvironment + ) { + return new PendingWorldReplacementManager.PublishedWorldRuntimeState( + worldKey, + true, + seed, + bukkitEnvironment, + "underworld", + irisEnvironment + ); + } + + private static Transaction transaction() { + return new Transaction( + UUID.fromString("2e488654-c259-4587-a7f2-8a053d59b60f"), + WorldSlotKey.minecraft("the_nether"), + "world_nether", + Path.of("build", "replacement-thread-test", "world"), + "underworld", + 918273645L, + "fingerprint", + new WorldGeneratorSnapshot(false, false, false, null, false, null), + true, + Phase.PUBLISHED + ); + } + + private static void assertNoBukkitRuntimeAccess(String source) { + assertFalse(source.contains("WorldIdentity.")); + assertFalse(source.contains("IrisToolbelt.")); + assertFalse(source.contains("getSeed()")); + assertFalse(source.contains("getEnvironment()")); + assertFalse(source.contains("Bukkit.")); + } + + 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); + } + + private static String method(String source, String signature) { + int start = source.indexOf(signature); + assertTrue("Missing source contract signature: " + signature, start >= 0); + int openBrace = source.indexOf('{', start); + assertTrue("Missing source contract method body: " + signature, openBrace >= 0); + int depth = 0; + for (int index = openBrace; index < source.length(); index++) { + char current = source.charAt(index); + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return source.substring(start, index + 1); + } + } + } + throw new IllegalArgumentException("Unclosed source contract method: " + signature); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/StudioRuntimeCleanupTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/StudioRuntimeCleanupTest.java new file mode 100644 index 000000000..fe6ee5b9c --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/StudioRuntimeCleanupTest.java @@ -0,0 +1,47 @@ +package art.arcane.iris.core; + +import art.arcane.iris.core.commands.CommandStudio; +import art.arcane.iris.core.tools.IrisCreator; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertFalse; + +public class StudioRuntimeCleanupTest { + @Test + public void pregenSettingsNoLongerExposeStartupNoisemapPrebake() { + boolean found = Arrays.stream(IrisSettings.IrisSettingsPregen.class.getDeclaredFields()) + .anyMatch(field -> field.getName().equals("startupNoisemapPrebake")); + + assertFalse(found); + } + + @Test + public void studioCommandNoLongerExposesProfilecache() { + boolean found = Arrays.stream(CommandStudio.class.getDeclaredMethods()) + .anyMatch(method -> method.getName().equals("profilecache")); + + assertFalse(found); + } + + @Test + public void studioCreatorNoLongerContainsPrewarmOrPrebakeHelpers() { + boolean found = Arrays.stream(IrisCreator.class.getDeclaredMethods()) + .map(method -> method.getName().toLowerCase()) + .anyMatch(name -> name.contains("prewarm") || name.contains("prebake")); + + assertFalse(found); + } + + @Test + public void noisemapPrebakePipelineClassIsRemoved() { + try { + Class.forName("art.arcane.iris.engine.IrisNoisemapPrebakePipeline"); + } catch (ClassNotFoundException ignored) { + return; + } + + throw new AssertionError("IrisNoisemapPrebakePipeline should not exist."); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisCreateOverwriteContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisCreateOverwriteContractTest.java new file mode 100644 index 000000000..70fcb612b --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisCreateOverwriteContractTest.java @@ -0,0 +1,98 @@ +package art.arcane.iris.core.commands; + +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.junit.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class CommandIrisCreateOverwriteContractTest { + @Test + public void createOnlyAcceptsNameTypeAndSeed() throws Exception { + Method command = CommandIris.class.getDeclaredMethod( + "create", + String.class, + String.class, + long.class + ); + + assertEquals(3, command.getParameterCount()); + assertFalse(Arrays.stream(command.getParameterTypes()).anyMatch(type -> type == boolean.class)); + } + + @Test + public void replaceOwnsOverrideAndOverwriteAliasesWithOptionalSeed() throws Exception { + Method command = CommandIris.class.getDeclaredMethod( + "replace", + String.class, + String.class, + Long.class + ); + Director director = command.getAnnotation(Director.class); + Parameter targetParameter = command.getParameters()[0]; + Param target = targetParameter.getAnnotation(Param.class); + Parameter typeParameter = command.getParameters()[1]; + Param type = typeParameter.getAnnotation(Param.class); + Parameter seedParameter = command.getParameters()[2]; + Param seed = seedParameter.getAnnotation(Param.class); + + assertTrue(Arrays.asList(director.aliases()).contains("override")); + assertTrue(Arrays.asList(director.aliases()).contains("overwrite")); + assertEquals( + "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart", + director.descriptionKey() + ); + assertEquals("target", target.name()); + assertEquals(director.descriptionKey(), target.descriptionKey()); + assertEquals("default", type.defaultValue()); + assertEquals(CommandIris.PackDimensionTypeHandler.class, type.customHandler()); + assertEquals("seed", seed.name()); + assertEquals("preserve", seed.defaultValue()); + assertEquals(CommandIris.ReplacementSeedHandler.class, seed.customHandler()); + assertEquals(Long.class, seedParameter.getType()); + assertFalse(Arrays.stream(command.getParameterTypes()).anyMatch(parameterType -> parameterType == long.class)); + } + + @Test + public void replacementSeedHandlerPreservesOrParsesTheFullLongRange() throws Exception { + CommandIris.ReplacementSeedHandler handler = new CommandIris.ReplacementSeedHandler(); + + assertNull(handler.parse("preserve", false)); + assertEquals(Long.valueOf(Long.MIN_VALUE), handler.parse(Long.toString(Long.MIN_VALUE), false)); + assertEquals(Long.valueOf(Long.MAX_VALUE), handler.parse(Long.toString(Long.MAX_VALUE), false)); + assertThrows(DirectorParsingException.class, () -> handler.parse("9223372036854775808", false)); + } + + @Test + public void createAndReplaceUseLocalizedStyledMissingDimensionFeedback() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/commands/CommandIris.java" + )).replace("\r\n", "\n"); + + assertEquals(2, occurrences(source, "COMMAND_IRIS_DIMENSION_NOT_FOUND")); + assertEquals(2, occurrences(source, "COMMAND_IRIS_INSTALL_PACK_AND_RESTART")); + assertFalse(source.contains("sendMessage(\"Could not find dimension")); + assertFalse(source.contains("sendMessage(\"Install its pack with")); + } + + private static int occurrences(String value, String match) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(match, offset)) >= 0) { + count++; + offset += match.length(); + } + return count; + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisDownloadContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisDownloadContractTest.java new file mode 100644 index 000000000..7de9dff59 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisDownloadContractTest.java @@ -0,0 +1,82 @@ +package art.arcane.iris.core.commands; + +import art.arcane.volmlib.util.director.annotations.Param; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class CommandIrisDownloadContractTest { + @Test + public void commandExposesOnlyPackAndLinkParameters() throws NoSuchMethodException { + Method method = CommandIris.class.getDeclaredMethod("download", String.class, String.class); + Parameter[] parameters = method.getParameters(); + Param pack = parameters[0].getAnnotation(Param.class); + Param link = parameters[1].getAnnotation(Param.class); + List downloadMethods = Arrays.stream(CommandIris.class.getDeclaredMethods()) + .filter((Method candidate) -> candidate.getName().equals("download")) + .toList(); + + assertEquals(1, downloadMethods.size()); + assertEquals(2, downloadMethods.getFirst().getParameterCount()); + assertEquals("pack", pack.name()); + assertEquals(0L, Arrays.stream(pack.aliases()).filter((String alias) -> !alias.isBlank()).count()); + assertEquals(CommandIris.DownloadPackHandler.class, pack.customHandler()); + assertEquals("link", link.name()); + assertEquals(0L, Arrays.stream(link.aliases()).filter((String alias) -> !alias.isBlank()).count()); + } + + @Test + public void packHandlerAcceptsOnlyTheTwoBuiltInPacks() throws Exception { + CommandIris.DownloadPackHandler handler = new CommandIris.DownloadPackHandler(); + + assertEquals("overworld", handler.parse("OVERWORLD", false)); + assertEquals("underworld", handler.parse("underworld", false)); + assertEquals(Arrays.asList("overworld", "underworld"), handler.getPossibilities()); + assertNull(handler.parse("__none__", false)); + assertThrows(Exception.class, () -> handler.parse("custom", false)); + } + + @Test + public void commandDelegatesAcceptedDownloadsWithoutRawPreamble() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/commands/CommandIris.java" + )).replace("\r\n", "\n"); + String download = method(source, "public void download("); + + assertTrue(download.contains("downloadBuiltIn(sender(), builtInPack)")); + assertTrue(download.contains("downloadUrl(sender(), directLink)")); + assertFalse(download.contains("Downloading built-in Iris pack")); + assertFalse(download.contains("Downloading Iris pack from")); + assertFalse(download.contains("sendMessage(directLink")); + } + + private static String method(String source, String signature) { + int start = source.indexOf(signature); + assertTrue(start >= 0); + int openBrace = source.indexOf('{', start); + int depth = 0; + for (int index = openBrace; index < source.length(); index++) { + char current = source.charAt(index); + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return source.substring(start, index + 1); + } + } + } + throw new IllegalArgumentException("Unclosed method: " + signature); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisFoliaCreateContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisFoliaCreateContractTest.java new file mode 100644 index 000000000..adf249d9c --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisFoliaCreateContractTest.java @@ -0,0 +1,48 @@ +package art.arcane.iris.core.commands; + +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertTrue; + +public class CommandIrisFoliaCreateContractTest { + @Test + public void ordinaryFoliaCreateRestartsOnlyAfterSuccessfulStagingAndFeedback() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.commandIrisSource"))); + String foliaCreate = source.substring( + source.indexOf("if (J.isFolia()) {"), + source.indexOf(" try {", source.indexOf("if (J.isFolia()) {")) + ); + + int stage = foliaCreate.indexOf("stageFoliaWorldCreation(worldName, dimension, seed)"); + int failureExit = foliaCreate.indexOf("if (!staged)"); + int feedback = foliaCreate.indexOf("COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD"); + int restart = foliaCreate.indexOf("ServerConfigurator.restart(\"Iris staged Folia world"); + + assertTrue(stage >= 0); + assertTrue(stage < failureExit); + assertTrue(failureExit < feedback); + assertTrue(feedback < restart); + } + + @Test + public void foliaStagePublishesCurrentPaperDataBeforeRegisteringStartupAlias() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.commandIrisSource"))); + int methodStart = source.indexOf("private boolean stageFoliaWorldCreation("); + int methodEnd = source.indexOf("private boolean registerWorldInBukkitYml(", methodStart); + String staging = source.substring(methodStart, methodEnd); + + int currentPaperData = staging.indexOf("INMS.get().writeCurrentPaperWorldData("); + int pack = staging.indexOf("installIntoWorld("); + int publication = staging.indexOf("AtomicDirectoryPublisher.publishAbsent("); + int registration = staging.indexOf("registerWorldInBukkitYml(worldKey"); + + assertTrue(currentPaperData >= 0); + assertTrue(currentPaperData < pack); + assertTrue(pack < publication); + assertTrue(publication < registration); + assertTrue(source.contains("IrisWorldStorage.configuredWorldName(")); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisLoadWorldContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisLoadWorldContractTest.java new file mode 100644 index 000000000..edb31b400 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisLoadWorldContractTest.java @@ -0,0 +1,47 @@ +package art.arcane.iris.core.commands; + +import art.arcane.iris.core.BukkitWorldReconciler; +import art.arcane.volmlib.util.director.annotations.Param; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CommandIrisLoadWorldContractTest { + @Test + public void commandUsesTypedAsyncReconciliationBeforeReportingSuccess() throws Exception { + Method reconciliation = BukkitWorldReconciler.class.getDeclaredMethod( + "loadWorld", + File.class, + String.class); + assertEquals(CompletableFuture.class, reconciliation.getReturnType()); + Method commandMethod = CommandIris.class.getDeclaredMethod("loadWorld", String.class); + Parameter worldParameter = commandMethod.getParameters()[0]; + assertEquals(CommandIris.ManagedWorldNameHandler.class, + worldParameter.getAnnotation(Param.class).customHandler()); + + String source = Files.readString(Path.of(System.getProperty("iris.commandIrisSource"))); + String command = source.substring( + source.indexOf("public void loadWorld("), + source.indexOf("private void reportLoadWorldResult(")); + String reporter = source.substring( + source.indexOf("private void reportLoadWorldResult("), + source.indexOf("@Director(description = \"Evacuate an iris world\"")); + + assertTrue(command.contains("IrisWorldStorage.managedKeyFromName(world)")); + assertTrue(command.contains(".loadWorld(BUKKIT_YML, worldKey.toString())")); + assertTrue(command.contains(".whenComplete((result, failure) ->")); + assertFalse(command.contains("checkForBukkitWorlds")); + assertFalse(command.contains("COMMAND_IRIS_LOADED_SUCCESSFULLY")); + assertTrue(reporter.contains("if (result.succeeded())")); + assertTrue(reporter.contains("COMMAND_IRIS_LOADED_SUCCESSFULLY")); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisPackDimensionTypeHandlerTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisPackDimensionTypeHandlerTest.java new file mode 100644 index 000000000..345328834 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisPackDimensionTypeHandlerTest.java @@ -0,0 +1,41 @@ +package art.arcane.iris.core.commands; + +import art.arcane.volmlib.util.collection.KList; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CommandIrisPackDimensionTypeHandlerTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void emptyPackDirectoryDoesNotAdvertiseDefaultSentinel() throws Exception { + File packsFolder = temporaryFolder.newFolder("packs"); + + KList options = CommandIris.PackDimensionTypeHandler.packDimensionOptions(packsFolder); + + assertTrue(options.isEmpty()); + assertFalse(options.contains("default")); + } + + @Test + public void matchingPackAndDimensionUsesBarePackName() { + assertEquals("overworld", + CommandIris.PackDimensionTypeHandler.packDimensionOption("overworld", "overworld")); + assertEquals("OverWorld", + CommandIris.PackDimensionTypeHandler.packDimensionOption("OverWorld", "overworld")); + } + + @Test + public void differingDimensionUsesExplicitPackReference() { + assertEquals("custom_pack:dimensions/sky", + CommandIris.PackDimensionTypeHandler.packDimensionOption("custom_pack", "dimensions/sky")); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisRemovalCommandContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisRemovalCommandContractTest.java new file mode 100644 index 000000000..b80044622 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisRemovalCommandContractTest.java @@ -0,0 +1,27 @@ +package art.arcane.iris.core.commands; + +import art.arcane.volmlib.util.director.annotations.Param; +import org.bukkit.World; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class CommandIrisRemovalCommandContractTest { + @Test + public void removalAcceptsDiskOnlyNamesAndUsesManagedSuggestions() throws NoSuchMethodException { + Method command = CommandIris.class.getDeclaredMethod("remove", String.class, boolean.class); + Parameter worldParameter = command.getParameters()[0]; + Param world = worldParameter.getAnnotation(Param.class); + + assertEquals(CommandIris.ManagedWorldNameHandler.class, world.customHandler()); + assertFalse(Arrays.stream(CommandIris.class.getDeclaredMethods()) + .anyMatch(method -> method.getName().equals("remove") + && method.getParameterCount() == 2 + && method.getParameterTypes()[0] == World.class)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisUnloadContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisUnloadContractTest.java new file mode 100644 index 000000000..0abf5a1c8 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisUnloadContractTest.java @@ -0,0 +1,26 @@ +package art.arcane.iris.core.commands; + +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertTrue; + +public class CommandIrisUnloadContractTest { + @Test + public void unloadAwaitsEvacuationAndHasATerminalLifecycleTimeout() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.commandIrisSource"))); + String command = source.substring( + source.indexOf("public void unloadWorld("), + source.indexOf("private void reportUnloadResult(")); + + assertTrue(command.contains("IrisToolbelt.evacuateAsync(world)")); + assertTrue(command.indexOf("IrisToolbelt.evacuateAsync(world)") + < command.indexOf("WorldLifecycleService.get().unloadAsync(world, true)")); + assertTrue(command.contains("guardUnloadCompletion(sequence, terminalTimeout, world.getName())")); + assertTrue(command.contains("ServerConfigurator.restart(\"World unload timed out")); + assertTrue(command.indexOf("ServerConfigurator.restart(\"World unload timed out") + < command.indexOf("guarded.completeExceptionally(timeout)")); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandJigsawContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandJigsawContractTest.java new file mode 100644 index 000000000..36965b6bc --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandJigsawContractTest.java @@ -0,0 +1,609 @@ +package art.arcane.iris.core.commands; + +import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.runtime.StudioOpenCoordinator; +import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioWorkcellSpec; +import art.arcane.iris.core.service.JigsawStudioService; +import art.arcane.iris.core.structure.authoring.StructureBackend; +import art.arcane.iris.core.structure.authoring.StructureCapability; +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureSource; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionInputKind; +import art.arcane.iris.core.structure.export.VanillaJigsawExportFormat; +import art.arcane.iris.core.tools.IrisCreator; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.util.common.director.specialhandlers.IrisStructureHandler; +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 org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +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.UUID; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class CommandJigsawContractTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void commandIrisRegistersJigsawTree() throws Exception { + Field field = CommandIris.class.getDeclaredField("jigsaw"); + + assertEquals(CommandJigsaw.class, field.getType()); + assertNotNull(CommandJigsaw.class.getDeclaredField("piece")); + assertNotNull(CommandJigsaw.class.getDeclaredField("pool")); + assertNotNull(CommandJigsaw.class.getDeclaredField("connector")); + assertNotNull(CommandJigsaw.class.getDeclaredField("variant")); + assertNotNull(CommandJigsaw.class.getDeclaredField("workcell")); + assertNotNull(CommandJigsaw.class.getDeclaredField("rules")); + assertNotNull(CommandJigsaw.class.getDeclaredField("preview")); + assertNotNull(CommandJigsaw.class.getDeclaredField("adopt")); + } + + @Test + public void exposesStudioLifecycleAndAuthoringCommands() throws Exception { + assertCommand("create", IrisDimension.class, String.class, String.class, String.class, + int.class, int.class, int.class, long.class); + assertCommand("convert", IrisDimension.class, String.class, String.class, long.class); + assertCommand("open", IrisDimension.class, String.class, long.class); + assertCommand("close", boolean.class); + assertCommand("delete", boolean.class); + assertCommand("status"); + assertCommand("menu"); + assertCommand("select"); + assertCommand("bounds", int.class, int.class, int.class); + assertCommand("save", String.class); + assertCommand("gotoBay", String.class); + assertCommand("particles", boolean.class); + assertCommand("export", String.class, String.class, String.class, boolean.class); + } + + @Test + public void jigsawStudioUsesItsDedicatedOpenLifecycle() { + assertEquals( + StudioOpenCoordinator.StudioOpenKind.JIGSAW, + CommandJigsaw.STUDIO_OPEN_KIND); + assertFalse(CommandJigsaw.STUDIO_OPEN_KIND.openWorkspace()); + assertFalse(CommandJigsaw.STUDIO_OPEN_KIND.teleportThroughStandardEntry()); + assertEquals( + IrisCreator.DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY, + CommandJigsaw.STUDIO_OPEN_KIND.datapackPreparation()); + } + + @Test + public void packValidationDenialsAreExpectedOpenFailures() { + BrokenPackException failure = new BrokenPackException( + "overworld", List.of("Biome 'broken' has no resolvable regions.")); + + assertTrue(CommandJigsaw.isExpectedOpenDenial(failure)); + assertTrue(CommandJigsaw.isExpectedOpenDenial( + new CompletionException(failure))); + assertFalse(CommandJigsaw.isExpectedOpenDenial( + new IllegalStateException("Unexpected Studio failure"))); + } + + @Test + public void committedActivationStartsInitialEvaluationBeforePlayerBinding() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java")).replace("\r\n", "\n"); + int commit = source.indexOf("JigsawStudioActivation.commit(staged)"); + int evaluation = source.indexOf( + "studioService.activationCommitted(world, request.requestId())", + commit); + int binding = source.indexOf("PLAYER_PACKS.put", evaluation); + + assertTrue(commit >= 0); + assertTrue(evaluation > commit); + assertTrue(binding > evaluation); + } + + @Test + public void convertIsAddOnlyWorkflowWithAliasesAndDefaults() throws Exception { + Method convert = CommandJigsaw.class.getDeclaredMethod( + "convert", IrisDimension.class, String.class, String.class, long.class); + Director command = convert.getAnnotation(Director.class); + Parameter[] parameters = convert.getParameters(); + + assertEquals(List.of("import", "import-vanilla"), List.of(command.aliases())); + assertParameter(parameters[2], "auto", null); + assertParameter(parameters[3], "1337", null); + NamespacedKey source = CommandJigsaw.parseRegisteredStructureKey("minecraft:village_plains"); + assertEquals("minecraft_village_plains", CommandJigsaw.resolveConversionTarget(source, "auto")); + assertEquals("villages/plains", CommandJigsaw.resolveConversionTarget(source, "iris:villages/plains")); + NamespacedKey ancientCity = CommandJigsaw.parseRegisteredStructureKey( + "minecraft:ancient_city"); + assertEquals("minecraft_ancient_city", + CommandJigsaw.resolveConversionTarget(ancientCity, "auto")); + assertThrows(IllegalArgumentException.class, + () -> CommandJigsaw.resolveConversionTarget(source, "custom:village")); + } + + @Test + public void adoptionCommandsExposeTwoStepPlanContract() throws Exception { + Method inspect = CommandJigsaw.CommandJigsawAdopt.class.getDeclaredMethod( + "inspect", IrisDimension.class, String.class, String.class, String.class); + Method apply = CommandJigsaw.CommandJigsawAdopt.class.getDeclaredMethod("apply", String.class); + Parameter[] inspectParameters = inspect.getParameters(); + + assertNotNull(inspect.getAnnotation(Director.class)); + assertNotNull(apply.getAnnotation(Director.class)); + assertParameter(inspectParameters[2], "auto", null); + assertParameter(inspectParameters[3], "auto", CommandJigsaw.JigsawAdoptionStrategyHandler.class); + assertEquals(CommandJigsaw.JigsawAdoptionPlanHandler.class, + apply.getParameters()[0].getAnnotation(Param.class).customHandler()); + + CommandJigsaw.JigsawAdoptionStrategyHandler strategyHandler = + new CommandJigsaw.JigsawAdoptionStrategyHandler(); + assertEquals(List.of("auto", "in-place", "clone"), strategyHandler.getPossibilities()); + assertEquals("in-place", strategyHandler.parse("claim", false)); + assertEquals("clone", strategyHandler.parse("copy", false)); + assertThrows(DirectorParsingException.class, () -> strategyHandler.parse("overwrite", false)); + } + + @Test + public void adoptionInputKindUsesOnlyOwnershipProvenance() throws Exception { + Path root = temporaryFolder.newFolder("adoption-provenance").toPath(); + + assertEquals(IrisStructureAdoptionInputKind.UNOWNED_IRIS, + CommandJigsaw.adoptionInputKind(root, "unowned")); + writeManifest(root, "datapack-created", StructureSource.Kind.DATAPACK, + StructureOwnershipManifest.Provenance.created()); + assertEquals(IrisStructureAdoptionInputKind.UNOWNED_IRIS, + CommandJigsaw.adoptionInputKind(root, "datapack-created")); + writeManifest(root, "managed-provenance", StructureSource.Kind.IRIS, managedProvenance()); + assertEquals(IrisStructureAdoptionInputKind.MANAGED_DATAPACK, + CommandJigsaw.adoptionInputKind(root, "managed-provenance")); + } + + @Test + public void legacyGraphWritersShareTheServiceMutationContract() throws Exception { + Method mutation = CommandJigsaw.class.getDeclaredMethod( + "runGraphMutation", + Player.class, + CommandJigsaw.ActiveContext.class, + JigsawStudioService.CommandGraphMutation.class); + + assertEquals(boolean.class, mutation.getReturnType()); + assertNotNull(JigsawStudioService.CommandGraphMutation.class.getDeclaredMethod("run")); + assertNotNull(JigsawStudioService.CommandGraphMutationResult.class.getDeclaredConstructor( + JigsawStudioLayout.class, + String.class, + String.class, + String.class)); + } + + @Test + public void exposesAutosaveDynamicEvaluationAndSelectedWorkcellResize() throws Exception { + Method save = CommandJigsaw.class.getDeclaredMethod("save", String.class); + Method status = CommandJigsaw.class.getDeclaredMethod("status"); + Method bounds = CommandJigsaw.class.getDeclaredMethod( + "bounds", int.class, int.class, int.class); + + assertEquals("Flush the automatic save for a workcell now", + save.getAnnotation(Director.class).description()); + assertEquals("Show active Jigsaw Studio and dynamic evaluation state", + status.getAnnotation(Director.class).description()); + assertEquals("Set the selected Studio workcell capacity", + bounds.getAnnotation(Director.class).description()); + assertThrows(NoSuchMethodException.class, + () -> CommandJigsaw.class.getDeclaredMethod("validate")); + } + + @Test + public void createDefaultsToPlanarIrisWithCompleteCellAndSeedDefaults() throws Exception { + Method create = CommandJigsaw.class.getDeclaredMethod( + "create", + IrisDimension.class, + String.class, + String.class, + String.class, + int.class, + int.class, + int.class, + long.class); + Parameter[] parameters = create.getParameters(); + + assertParameter(parameters[2], "planar", CommandJigsaw.JigsawModeHandler.class); + assertParameter(parameters[3], "iris", CommandJigsaw.JigsawCompatibilityHandler.class); + assertParameter(parameters[4], "15", null); + assertParameter(parameters[5], "15", null); + assertParameter(parameters[6], "15", null); + assertParameter(parameters[7], "1337", null); + } + + @Test + public void structureKeysExplainTheirResourceAndOpenSupportsEditingAliases() throws Exception { + Method create = CommandJigsaw.class.getDeclaredMethod( + "create", + IrisDimension.class, + String.class, + String.class, + String.class, + int.class, + int.class, + int.class, + long.class); + Method open = CommandJigsaw.class.getDeclaredMethod( + "open", IrisDimension.class, String.class, long.class); + Param createKey = create.getParameters()[1].getAnnotation(Param.class); + Param openKey = open.getParameters()[1].getAnnotation(Param.class); + Director openCommand = open.getAnnotation(Director.class); + + assertEquals("key", createKey.name()); + assertEquals(List.of("structure", "name"), List.of(createKey.aliases())); + assertEquals("New key written as structures/.json", createKey.description()); + assertEquals("key", openKey.name()); + assertEquals(List.of("structure", "name"), List.of(openKey.aliases())); + assertEquals("Existing key loaded from structures/.json", openKey.description()); + assertEquals(IrisStructureHandler.class, openKey.customHandler()); + assertEquals(List.of("edit", "reopen"), List.of(openCommand.aliases())); + } + + @Test + public void createChoiceHandlersExposeCanonicalCompletionsAndRetainAliases() throws Exception { + CommandJigsaw.JigsawModeHandler modeHandler = new CommandJigsaw.JigsawModeHandler(); + CommandJigsaw.JigsawCompatibilityHandler compatibilityHandler = + new CommandJigsaw.JigsawCompatibilityHandler(); + + assertEquals(List.of("planar", "spatial"), modeHandler.getPossibilities()); + assertEquals("planar", modeHandler.parse("2d", false)); + assertEquals("spatial", modeHandler.parse("3d", false)); + assertEquals(List.of("iris", "vanilla"), compatibilityHandler.getPossibilities()); + assertEquals("iris", compatibilityHandler.parse("extended", false)); + assertEquals("vanilla", compatibilityHandler.parse("portable", false)); + assertThrows(DirectorParsingException.class, () -> modeHandler.parse("volume", false)); + assertThrows(DirectorParsingException.class, () -> compatibilityHandler.parse("mixed", false)); + } + + @Test + public void exposesNestedPieceVariantAndPreviewCommands() throws Exception { + assertNotNull(CommandJigsaw.CommandJigsawPool.class + .getDeclaredMethod("create", String.class, String.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawConnector.class + .getDeclaredMethod("channel", String.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawPiece.class + .getDeclaredMethod("create", String.class, String.class, int.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawPiece.class + .getDeclaredMethod("add", String.class, String.class, int.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawPiece.class + .getDeclaredMethod("remove", String.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawPiece.class + .getDeclaredMethod("rotatable", boolean.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawPiece.class + .getDeclaredMethod("expand") + .getAnnotation(Director.class)); + assertEquals("Resize the selected piece object exactly to workcell capacity", + CommandJigsaw.CommandJigsawPiece.class + .getDeclaredMethod("expand") + .getAnnotation(Director.class) + .description()); + assertNotNull(CommandJigsaw.CommandJigsawVariant.class + .getDeclaredMethod("weight", String.class, int.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawVariant.class + .getDeclaredMethod("resize", int.class, int.class, int.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawVariant.class + .getDeclaredMethod("label", String.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawVariant.class + .getDeclaredMethod("labelReset") + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawVariant.class + .getDeclaredMethod("duplicate") + .getAnnotation(Director.class)); + Method duplicateFamily = CommandJigsaw.CommandJigsawVariant.class + .getDeclaredMethod("duplicateFamily", String.class); + assertNotNull(duplicateFamily.getAnnotation(Director.class)); + assertParameter(duplicateFamily.getParameters()[0], "next", null); + assertNotNull(CommandJigsaw.CommandJigsawWorkcell.class + .getDeclaredMethod("capacity", int.class, int.class, int.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawWorkcell.class + .getDeclaredMethod("label", String.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawWorkcell.class + .getDeclaredMethod("labelReset") + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawRules.class + .getDeclaredMethod("limits", int.class, int.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawRules.class + .getDeclaredMethod("fallback", String.class, String.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawPreview.class + .getDeclaredMethod("assemble", long.class) + .getAnnotation(Director.class)); + assertNotNull(CommandJigsaw.CommandJigsawPreview.class + .getDeclaredMethod("gotoPreview") + .getAnnotation(Director.class)); + } + + @Test + public void pieceCreateUsesResolvedWorkcellCapacityInsteadOfLayoutDefault() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java")).replace("\r\n", "\n"); + int pieceCommands = source.indexOf("public static class CommandJigsawPiece"); + int create = source.indexOf("public void create(", pieceCommands); + int add = source.indexOf("public void add(", create); + String createSource = source.substring(create, add); + + assertTrue(createSource.contains("targetWorkcell = contextual;")); + assertTrue(createSource.contains("JigsawStudioCellDimensions dimensions = targetWorkcell.capacity();")); + assertFalse(createSource.contains("layout().cellDimensions()")); + } + + @Test + public void pieceAddUsesCanonicalAxesForRotatedRectangularPlanarWorkcells() { + JigsawStudioLayout layout = nonuniformPlanarLayout(); + IrisJigsawPiece eastEnd = new IrisJigsawPiece(); + eastEnd.getConnectors().add(new IrisJigsawConnector().setDirection(IrisDirection.EAST_POSITIVE_X)); + IrisObject exactObject = new IrisObject(7, 5, 13); + + CommandJigsaw.PieceWorkcellResolution exact = CommandJigsaw.resolvePieceWorkcell( + layout, + eastEnd, + exactObject); + CommandJigsaw.PieceWorkcellResolution oversized = CommandJigsaw.resolvePieceWorkcell( + layout, + eastEnd, + new IrisObject(8, 5, 13)); + + assertEquals(JigsawPlanarArchetype.END.stableId(), exact.workcell().stableId()); + assertEquals(new IrisPosition(13, 5, 7), exact.requiredDimensions()); + assertEquals(new JigsawStudioCellDimensions(13, 5, 7), exact.workcell().capacity()); + assertTrue(exact.fits()); + assertTrue(exactObject.getD() > exact.workcell().capacity().depth()); + assertFalse(oversized.fits()); + } + + @Test + public void pieceAddKeepsRawObjectAxesForSpatialWorkcells() { + JigsawStudioCellDimensions capacity = new JigsawStudioCellDimensions(7, 5, 13); + JigsawStudioLayout layout = JigsawStudioLayout.create( + JigsawStudioMode.SPATIAL_JIGSAW, + capacity, + JigsawStudioVariantCatalog.empty()); + IrisJigsawPiece spatialPiece = new IrisJigsawPiece(); + spatialPiece.getConnectors().add( + new IrisJigsawConnector().setDirection(IrisDirection.EAST_POSITIVE_X)); + + CommandJigsaw.PieceWorkcellResolution resolution = CommandJigsaw.resolvePieceWorkcell( + layout, + spatialPiece, + new IrisObject(7, 5, 13)); + + assertEquals(JigsawStudioLayout.SPATIAL_WORKCELL_ID, resolution.workcell().stableId()); + assertEquals(new IrisPosition(7, 5, 13), resolution.requiredDimensions()); + assertEquals(capacity, resolution.workcell().capacity()); + assertTrue(resolution.fits()); + } + + @Test + public void everyExecutableCommandRejectsNonPlayerOrigins() { + Class[] commandTypes = { + CommandJigsaw.class, + CommandJigsaw.CommandJigsawConnector.class, + CommandJigsaw.CommandJigsawPool.class, + CommandJigsaw.CommandJigsawPiece.class, + CommandJigsaw.CommandJigsawVariant.class, + CommandJigsaw.CommandJigsawWorkcell.class, + CommandJigsaw.CommandJigsawRules.class, + CommandJigsaw.CommandJigsawPreview.class, + CommandJigsaw.CommandJigsawAdopt.class + }; + + for (Class commandType : commandTypes) { + for (Method method : commandType.getDeclaredMethods()) { + Director director = method.getAnnotation(Director.class); + if (director != null) { + assertEquals(method.toString(), DirectorOrigin.PLAYER, director.origin()); + } + } + } + } + + @Test + public void exportOutputIsOneSafeChildArtifact() { + Path root = Path.of("build", "jigsaw-exports").toAbsolutePath().normalize(); + + assertEquals(root.resolve("village.zip"), CommandJigsaw.resolveExportDestination( + root, "village", VanillaJigsawExportFormat.ZIP)); + assertEquals(root.resolve("village"), CommandJigsaw.resolveExportDestination( + root, "village", VanillaJigsawExportFormat.DIRECTORY)); + assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination( + root, "", VanillaJigsawExportFormat.DIRECTORY)); + assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination( + root, ".", VanillaJigsawExportFormat.DIRECTORY)); + assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination( + root, "../all-exports", VanillaJigsawExportFormat.DIRECTORY)); + assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination( + root, "nested/export", VanillaJigsawExportFormat.DIRECTORY)); + } + + @Test + public void exportLeaseRejectsDuplicatePlayerAndDestination() { + UUID firstPlayer = UUID.randomUUID(); + UUID secondPlayer = UUID.randomUUID(); + Path firstDestination = Path.of("build", "jigsaw-exports", "first.zip"); + Path secondDestination = Path.of("build", "jigsaw-exports", "second.zip"); + + assertEquals(true, CommandJigsaw.beginExport(firstPlayer, firstDestination)); + try { + assertEquals(false, CommandJigsaw.beginExport(firstPlayer, secondDestination)); + assertEquals(false, CommandJigsaw.beginExport(secondPlayer, firstDestination)); + } finally { + CommandJigsaw.finishExport(firstPlayer, firstDestination); + } + + assertEquals(true, CommandJigsaw.beginExport(secondPlayer, firstDestination)); + CommandJigsaw.finishExport(secondPlayer, firstDestination); + } + + @Test + public void exportStartFailuresHavePreciseOperatorMessages() { + assertEquals("The active Jigsaw Studio is no longer available.", + CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.NOT_ACTIVE)); + assertEquals("Only the Jigsaw Studio owner can export this project.", + CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.NOT_OWNER)); + assertEquals("Wait for the pending autosave or discard the edits before exporting the on-disk graph.", + CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.DIRTY)); + assertEquals("The active Jigsaw Studio is closing and cannot be exported.", + CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.CLOSING)); + assertEquals("Wait for the current Jigsaw Studio save to finish before exporting.", + CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.SAVE_IN_PROGRESS)); + assertEquals("Wait for the current Jigsaw Studio operation to finish before exporting.", + CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.OPERATION_IN_PROGRESS)); + assertEquals("A Jigsaw Studio export is already in progress.", + CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.IN_PROGRESS)); + assertThrows(IllegalArgumentException.class, + () -> CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.STARTED)); + } + + @Test + public void exportLeaseReleaseActionRunsExactlyOnce() { + AtomicInteger releases = new AtomicInteger(); + CommandJigsaw.ExportLease lease = new CommandJigsaw.ExportLease(releases::incrementAndGet); + + lease.release(); + lease.release(); + + assertEquals(1, releases.get()); + } + + @Test + public void exportSourceAcquiresStudioLeaseBeforeStaticLeaseAndReleasesBothPaths() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java")).replace("\r\n", "\n"); + int serviceLease = source.indexOf("studioService.tryBeginExport(requestId, playerId)"); + int staticLease = source.indexOf("beginExport(playerId, destination)", serviceLease); + int dispatch = source.indexOf("J.a(() -> runExport(operation))", staticLease); + int schedulingRelease = source.indexOf("operation.lease().release()", dispatch); + int exporter = source.indexOf("new VanillaJigsawDatapackExporter().export(operation.request())", dispatch); + int completionRelease = source.indexOf("operation.lease().release()", exporter); + + assertTrue(serviceLease >= 0); + assertTrue(staticLease > serviceLease); + assertTrue(dispatch > staticLease); + assertTrue(schedulingRelease > dispatch); + assertTrue(exporter > schedulingRelease); + assertTrue(completionRelease > exporter); + } + + private static void assertCommand(String name, Class... parameterTypes) throws Exception { + Method method = CommandJigsaw.class.getDeclaredMethod(name, parameterTypes); + assertNotNull(method.getAnnotation(Director.class)); + } + + private static JigsawStudioLayout nonuniformPlanarLayout() { + List workcells = List.of( + workcell(JigsawPlanarArchetype.BLANK, 3, 1, 3), + workcell(JigsawPlanarArchetype.END, 13, 5, 7), + workcell(JigsawPlanarArchetype.STRAIGHT, 5, 2, 11), + workcell(JigsawPlanarArchetype.CORNER, 9, 3, 6), + workcell(JigsawPlanarArchetype.TEE, 12, 4, 8), + workcell(JigsawPlanarArchetype.CROSS, 10, 6, 10)); + return JigsawStudioLayout.createPlanar( + new JigsawStudioCellDimensions(3, 1, 3), + workcells, + JigsawStudioVariantCatalog.empty()); + } + + private static JigsawStudioWorkcellSpec workcell( + JigsawPlanarArchetype archetype, + int width, + int height, + int depth + ) { + return new JigsawStudioWorkcellSpec( + archetype, + "", + new JigsawStudioCellDimensions(width, height, depth), + true); + } + + private static void assertParameter(Parameter parameter, String defaultValue, Class customHandler) { + Param annotation = parameter.getAnnotation(Param.class); + + assertNotNull(annotation); + assertEquals(defaultValue, annotation.defaultValue()); + if (customHandler != null) { + assertEquals(customHandler, annotation.customHandler()); + } + } + + private static void writeManifest( + Path root, + String structure, + StructureSource.Kind sourceKind, + StructureOwnershipManifest.Provenance provenance + ) throws Exception { + StructureKey structureKey = new StructureKey("iris", structure); + StructureOwnershipManifest manifest = new StructureOwnershipManifest( + StructureOwnershipManifest.CURRENT_SCHEMA_VERSION, + structureKey, + StructureSource.of(sourceKind, new StructureKey("iris", "source/" + structure)), + StructureBackend.IRIS_ASSEMBLY, + List.of(StructureCapability.BLOCKS, StructureCapability.CONNECTORS), + List.of(), + Map.of("structures/" + structure + ".json", StructureHash.sha256( + structure.getBytes(StandardCharsets.UTF_8))), + provenance); + Path manifestPath = new StructureTransactionWriter(root).ownershipManifestPath(structureKey); + Files.createDirectories(manifestPath.getParent()); + Files.write(manifestPath, manifest.toJson()); + } + + private static StructureOwnershipManifest.Provenance managedProvenance() { + String path = "structures/source.json"; + String hash = StructureHash.sha256("source".getBytes(StandardCharsets.UTF_8)); + return new StructureOwnershipManifest.Provenance( + StructureOwnershipManifest.Origin.MANAGED_DATAPACK, + UUID.randomUUID().toString(), + StructureHash.sha256("plan".getBytes(StandardCharsets.UTF_8)), + StructureHash.sha256("closure".getBytes(StandardCharsets.UTF_8)), + 1L, + Map.of(path, hash), + Map.of(path, path), + StructureOwnershipManifest.RollbackDisposition.NONE); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandObjectPasteRaycastTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandObjectPasteRaycastTest.java new file mode 100644 index 000000000..87253a21b --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandObjectPasteRaycastTest.java @@ -0,0 +1,28 @@ +package art.arcane.iris.core.commands; + +import org.bukkit.Material; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CommandObjectPasteRaycastTest { + @Test + public void rejectsEveryAirVariantAndConfiguredFoliageAsPasteTargets() { + assertFalse(CommandObject.isPasteTarget(Material.AIR)); + assertFalse(CommandObject.isPasteTarget(Material.CAVE_AIR)); + assertFalse(CommandObject.isPasteTarget(Material.VOID_AIR)); + assertFalse(CommandObject.isPasteTarget(Material.SHORT_GRASS)); + assertFalse(CommandObject.isPasteTarget(Material.SNOW)); + assertFalse(CommandObject.isPasteTarget(Material.VINE)); + assertFalse(CommandObject.isPasteTarget(Material.TORCH)); + assertFalse(CommandObject.isPasteTarget(Material.DEAD_BUSH)); + assertFalse(CommandObject.isPasteTarget(Material.POPPY)); + assertFalse(CommandObject.isPasteTarget(Material.DANDELION)); + } + + @Test + public void acceptsSolidBlocksAsPasteTargets() { + assertTrue(CommandObject.isPasteTarget(Material.STONE)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandPackOptionalPackContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandPackOptionalPackContractTest.java new file mode 100644 index 000000000..70f091b9c --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandPackOptionalPackContractTest.java @@ -0,0 +1,63 @@ +package art.arcane.iris.core.commands; + +import art.arcane.volmlib.util.director.compat.DirectorAnnotationCompatibility; +import art.arcane.volmlib.util.director.runtime.DirectorNodeDescriptor; +import art.arcane.volmlib.util.director.runtime.DirectorParameterDescriptor; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CommandPackOptionalPackContractTest { + private static DirectorParameterDescriptor packParam(String method) throws Exception { + DirectorNodeDescriptor node = DirectorAnnotationCompatibility + .fromMethod(CommandPack.class.getDeclaredMethod(method, String.class)) + .orElseThrow(); + DirectorParameterDescriptor pack = node.getParameters().get(0); + assertEquals("pack", pack.getName()); + return pack; + } + + @Test + public void bareValidateAndStatusAreInvocableWithoutAPackArgument() throws Exception { + for (String method : List.of("validate", "status")) { + DirectorParameterDescriptor pack = packParam(method); + assertFalse(method + " must not require a pack", pack.isRequired()); + assertFalse(method + " needs a non-blank default for Director to treat it as optional", + pack.getDefaultValue().isBlank()); + } + } + + @Test + public void cleanupAndRestoreStillRequireAPack() throws Exception { + for (String method : List.of("cleanup", "restore")) { + DirectorNodeDescriptor node = DirectorAnnotationCompatibility + .fromMethod(findMethod(method)) + .orElseThrow(); + DirectorParameterDescriptor pack = node.getParameters().get(0); + assertEquals("pack", pack.getName()); + assertTrue(method + " must keep requiring a pack", pack.isRequired()); + } + } + + @Test + public void allPacksPredicateAcceptsSentinelBlankAndNull() { + assertTrue(CommandPack.wantsAllPacks(null)); + assertTrue(CommandPack.wantsAllPacks("")); + assertTrue(CommandPack.wantsAllPacks(" ")); + assertTrue(CommandPack.wantsAllPacks("*")); + assertFalse(CommandPack.wantsAllPacks("overworld")); + } + + private static java.lang.reflect.Method findMethod(String name) { + for (java.lang.reflect.Method method : CommandPack.class.getDeclaredMethods()) { + if (method.getName().equals(name)) { + return method; + } + } + throw new AssertionError("CommandPack must declare " + name); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandPregenCompletionContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandPregenCompletionContractTest.java new file mode 100644 index 000000000..6339c5bfd --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandPregenCompletionContractTest.java @@ -0,0 +1,48 @@ +package art.arcane.iris.core.commands; + +import art.arcane.volmlib.util.director.compat.DirectorEngineFactory; +import art.arcane.volmlib.util.director.runtime.DirectorInvocation; +import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine; +import art.arcane.volmlib.util.director.runtime.DirectorSender; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CommandPregenCompletionContractTest { + @Test + public void startCompletesEveryConfigurableValueWithItsCanonicalKey() { + DirectorRuntimeEngine engine = DirectorEngineFactory.create(new CommandPregen()); + DirectorInvocation invocation = new DirectorInvocation(new TestSender(), "iris", List.of("start", "")); + + List suggestions = engine.tabComplete(invocation); + + assertTrue(suggestions.contains("radius=")); + assertTrue(suggestions.contains("world=")); + assertTrue(suggestions.contains("center=")); + assertTrue(suggestions.contains("gui=false")); + assertTrue(suggestions.contains("gui=true")); + assertTrue(suggestions.contains("serial=false")); + assertTrue(suggestions.contains("serial=true")); + assertFalse(suggestions.contains("true")); + assertFalse(suggestions.contains("false")); + } + + private static final class TestSender implements DirectorSender { + @Override + public String getName() { + return "test"; + } + + @Override + public boolean isPlayer() { + return false; + } + + @Override + public void sendMessage(String message) { + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandStudioCreationContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandStudioCreationContractTest.java new file mode 100644 index 000000000..4a75cb3d0 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandStudioCreationContractTest.java @@ -0,0 +1,27 @@ +package art.arcane.iris.core.commands; + +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class CommandStudioCreationContractTest { + @Test + public void createRunsAsynchronouslyAndTemplateIsOptional() throws NoSuchMethodException { + Method command = CommandStudio.class.getDeclaredMethod("create", String.class, IrisDimension.class); + Director director = command.getAnnotation(Director.class); + Parameter templateParameter = command.getParameters()[1]; + Param template = templateParameter.getAnnotation(Param.class); + + assertFalse(director.sync()); + assertEquals("null", template.defaultValue()); + assertEquals(NullableDimensionHandler.class, template.customHandler()); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/IrisStructureLocateCommandContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/IrisStructureLocateCommandContractTest.java new file mode 100644 index 000000000..01d9eb4ea --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/IrisStructureLocateCommandContractTest.java @@ -0,0 +1,300 @@ +package art.arcane.iris.core.commands; + +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisStructureLocateCommandContractTest { + @Test + public void findReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource"))); + + assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED")); + assertTrue(source.contains("the density search safety limit was reached")); + assertTrue(source.contains("result.originX(), result.baseY(), result.originZ()")); + assertFalse(source.contains("at[0] + 8")); + assertFalse(source.contains("at[2] + 8")); + } + + @Test + public void findRoutesOnlyActiveWorldStructures() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource"))); + int methodStart = source.indexOf("public void structure("); + int methodEnd = source.indexOf("static StructureLookupRoute selectStructureLookupRoute", methodStart); + String method = source.substring(methodStart, methodEnd); + int nativeResolution = method.indexOf("resolveNativeStructure(structureKey)"); + int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(", nativeResolution); + int nativeLocatableCheck = method.indexOf( + "IrisStructureLocator.hasLocatableNativePlacement(e, structureKey)", policyResolution); + int editableLocatableCheck = method.indexOf( + "IrisStructureLocator.hasLocatableEditablePlacement(e, structureKey)", nativeLocatableCheck); + int worldGenerationCheck = method.indexOf("targetWorld.canGenerateStructures()", editableLocatableCheck); + int routeResolution = method.indexOf("selectStructureLookupRoute(", editableLocatableCheck); + int nativeLocate = method.indexOf("targetWorld.locateNearestStructure(", routeResolution); + + assertTrue(nativeResolution >= 0); + assertTrue(policyResolution > nativeResolution); + assertTrue(nativeLocatableCheck > policyResolution); + assertTrue(editableLocatableCheck > nativeLocatableCheck); + assertTrue(worldGenerationCheck > editableLocatableCheck); + assertTrue(routeResolution > editableLocatableCheck); + assertTrue(nativeLocate > routeResolution); + assertFalse(method.contains("IrisStructureLocator.isPlaced(e, structureKey)")); + assertFalse(method.contains("NativeStructureLocateCapability")); + } + + @Test + public void findRejectsUnregisteredNativeAndDormantPlacements() { + assertEquals(CommandFind.StructureLookupRoute.UNKNOWN, + CommandFind.selectStructureLookupRoute( + false, null, true, true, false, true, true)); + assertEquals(CommandFind.StructureLookupRoute.UNKNOWN, + CommandFind.selectStructureLookupRoute( + false, null, false, false, false, true, true)); + assertEquals(CommandFind.StructureLookupRoute.NO_ACTIVE_PLACEMENT, + CommandFind.selectStructureLookupRoute( + true, decision(NativeStructureGenerationStatus.REPLACED_BY_IRIS), + false, false, false, true, true)); + assertEquals(CommandFind.StructureLookupRoute.NO_ACTIVE_PLACEMENT, + CommandFind.selectStructureLookupRoute( + true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE), + true, false, false, true, false)); + } + + @Test + public void findAllowsLocatableEditablePlacementsWithoutNativeGeneration() { + assertEquals(CommandFind.StructureLookupRoute.IRIS, + CommandFind.selectStructureLookupRoute( + false, null, false, false, true, false, false)); + assertEquals(CommandFind.StructureLookupRoute.IRIS, + CommandFind.selectStructureLookupRoute( + true, decision(NativeStructureGenerationStatus.REPLACED_BY_IRIS), + false, false, true, false, false)); + } + + @Test + public void findRequiresWorldGenerationAndReachabilityForNativeRoutes() { + assertEquals(CommandFind.StructureLookupRoute.WORLD_DISABLED, + CommandFind.selectStructureLookupRoute( + true, decision(NativeStructureGenerationStatus.REPLACED_BY_IRIS), + true, true, false, false, true)); + assertEquals(CommandFind.StructureLookupRoute.WORLD_DISABLED, + CommandFind.selectStructureLookupRoute( + true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE), + false, false, false, false, true)); + assertEquals(CommandFind.StructureLookupRoute.UNREACHABLE, + CommandFind.selectStructureLookupRoute( + true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE), + false, false, false, true, false)); + assertEquals(CommandFind.StructureLookupRoute.NATIVE, + CommandFind.selectStructureLookupRoute( + true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE), + true, true, false, true, false)); + assertEquals(CommandFind.StructureLookupRoute.NATIVE, + CommandFind.selectStructureLookupRoute( + true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE), + true, false, false, true, true)); + } + + @Test + public void findHonorsPackDisableBeforePlacementRouting() { + assertEquals(CommandFind.StructureLookupRoute.POLICY_DISABLED, + CommandFind.selectStructureLookupRoute( + true, decision(NativeStructureGenerationStatus.DISABLED_BY_PACK), + false, false, true, true, true)); + } + + @Test + public void unregisteredDiagnosticExplainsEveryRejectedRoute() { + assertEquals( + "Native structure minecraft:village is disabled by this dimension's importedStructures settings.", + CommandFind.describeStructureExclusion( + CommandFind.StructureLookupRoute.POLICY_DISABLED, "minecraft:village", + NativeStructureGenerationStatus.DISABLED_BY_PACK, + false, List.of(), Set.of())); + assertEquals( + "native structure generation is disabled for this world", + CommandFind.describeStructureExclusion( + CommandFind.StructureLookupRoute.WORLD_DISABLED, "minecraft:village", + NativeStructureGenerationStatus.GENERATE_NATIVE, + false, List.of(), Set.of())); + assertEquals( + "its resolved biome filter is empty", + CommandFind.describeStructureExclusion( + CommandFind.StructureLookupRoute.UNREACHABLE, "towns_and_towers:exclusive", + NativeStructureGenerationStatus.GENERATE_NATIVE, + false, List.of(), Set.of())); + assertEquals( + "this pack does not produce any of its required biome(s): terralith:alpine_grove/bwg:aspen_boreal", + CommandFind.describeStructureExclusion( + CommandFind.StructureLookupRoute.UNREACHABLE, "towns_and_towers:exclusive", + NativeStructureGenerationStatus.GENERATE_NATIVE, + false, List.of("terralith:alpine_grove", "bwg:aspen_boreal"), + Set.of("minecraft:plains"))); + assertEquals( + "no active positive-weight, positive-frequency structure-set entry includes it in this world", + CommandFind.describeStructureExclusion( + CommandFind.StructureLookupRoute.UNREACHABLE, "example:dormant", + NativeStructureGenerationStatus.GENERATE_NATIVE, + false, List.of("minecraft:plains"), Set.of("minecraft:plains"))); + assertEquals( + "no active positive-weight, positive-frequency structure-set entry includes it in this world", + CommandFind.describeStructureExclusion( + CommandFind.StructureLookupRoute.UNREACHABLE, "example:partial_overlap", + NativeStructureGenerationStatus.GENERATE_NATIVE, + false, List.of("minecraft:plains", "mod:absent"), Set.of("minecraft:plains"))); + } + + @Test + public void unregisteredDiagnosticExplainsInactiveAndMissingNativePlacements() { + assertEquals( + "configured Iris replacement is inactive: no matching placement has positive density when " + + "density-based and a Y band intersecting this world", + CommandFind.describeStructureExclusion( + CommandFind.StructureLookupRoute.NO_ACTIVE_PLACEMENT, "example:replacement", + NativeStructureGenerationStatus.REPLACED_BY_IRIS, + false, List.of(), Set.of())); + assertEquals( + "configured nativeStructures placement is inactive: no matching placement has positive density " + + "when density-based and a Y band intersecting this world; the native route is also " + + "inactive because its resolved biome filter is empty", + CommandFind.describeStructureExclusion( + CommandFind.StructureLookupRoute.NO_ACTIVE_PLACEMENT, "example:native", + NativeStructureGenerationStatus.GENERATE_NATIVE, + true, List.of(), Set.of())); + assertTrue(CommandFind.describeConfiguredUnregisteredNative(true) + .contains("key is not registered by the active server/datapack")); + assertFalse(CommandFind.describeConfiguredUnregisteredNative(true) + .contains("Iris placement is also inactive")); + assertTrue(CommandFind.describeConfiguredUnregisteredNative(false) + .contains("Iris placement is also inactive")); + } + + @Test + public void unplacedEditableDiagnosticDefersToRegistryAndNativeCategories() { + assertTrue(CommandFind.isUnplacedEditableCandidate( + "pack:editable", Set.of(), Set.of(), false)); + assertFalse(CommandFind.isUnplacedEditableCandidate( + "pack:editable", Set.of("pack:editable"), Set.of(), false)); + assertFalse(CommandFind.isUnplacedEditableCandidate( + "pack:editable", Set.of(), Set.of("pack:editable"), false)); + assertFalse(CommandFind.isUnplacedEditableCandidate( + "pack:editable", Set.of(), Set.of(), true)); + } + + @Test + public void unregisteredDiagnosticPrintsDetailsOnlyToConsole() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource"))); + int methodStart = source.indexOf("public void unregistered()"); + int methodEnd = source.indexOf("private static StructureExclusionReport", methodStart); + String method = source.substring(methodStart, methodEnd); + + assertTrue(methodStart >= 0); + assertTrue(source.contains("Iris.info(\"[%s] %s%s: %s\"")); + assertTrue(method.contains("non-generating structure candidate(s) and their reasons to the server console")); + assertTrue(method.contains("no chunks were searched")); + assertTrue(method.indexOf("DatapackIngestService.installed()") > method.indexOf("J.a(() ->")); + assertTrue(source.contains("engine.getDimension().getDatapackImports()")); + assertTrue(source.contains("snapshot.configuredImportUrls().contains(importUrl)")); + assertTrue(source.contains("engine.getData().getStructureLoader().getPossibleKeys()")); + assertFalse(method.contains("commandSender.sendMessage(exclusion")); + } + + @Test + public void findNativePolicyMessagesMatchModdedDiagnostics() { + assertEquals( + "Native structure minecraft:village_plains is disabled by this dimension's importedStructures settings.", + NativeStructureGenerationPolicy.generationStatusMessage( + "minecraft:village_plains", NativeStructureGenerationStatus.DISABLED_BY_PACK)); + assertEquals( + "Native structure minecraft:ancient_city is replaced by an Iris placement in this pack and locates through that explicit replacement.", + NativeStructureGenerationPolicy.generationStatusMessage( + "minecraft:ancient_city", NativeStructureGenerationStatus.REPLACED_BY_IRIS)); + assertEquals( + "Native structure minecraft:stronghold generates natively.", + NativeStructureGenerationPolicy.generationStatusMessage( + "minecraft:stronghold", NativeStructureGenerationStatus.GENERATE_NATIVE)); + } + + @Test + public void structureVerifyReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource"))); + + assertTrue(source.contains("[iris-search-limit]")); + assertTrue(source.contains("density searches safety-limited")); + assertTrue(source.contains("result.originX() + \",\" + result.baseY() + \",\" + result.originZ()")); + assertFalse(source.contains("at[0] + 8")); + assertFalse(source.contains("at[2] + 8")); + } + + @Test + public void bulkStructureImportRestrictsCaptureWithoutChangingStandaloneCapture() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource"))); + int importStart = source.indexOf("public void importAll("); + int importEnd = source.indexOf("@Director(name = \"capture\"", importStart); + int captureStart = source.indexOf("public void capture(", importEnd); + int captureEnd = source.indexOf("@Director(description = \"Verify", captureStart); + + assertTrue(importStart >= 0); + assertTrue(importEnd > importStart); + assertTrue(captureStart > importEnd); + assertTrue(captureEnd > captureStart); + String importMethod = source.substring(importStart, importEnd); + String captureMethod = source.substring(captureStart, captureEnd); + assertTrue(importMethod.contains("StructureCaptureImporter.importStructures(")); + assertTrue(importMethod.contains("jigsaws.captureCandidates()")); + assertFalse(importMethod.contains("StructureCaptureImporter.importAllStructures(")); + assertTrue(captureMethod.contains("StructureCaptureImporter.importAllStructures(")); + } + + @Test + public void structurePlaceSeparatesPackContentFromTargetWorldEngine() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource"))); + int methodStart = source.indexOf("public void place("); + assertTrue(methodStart >= 0); + String method = source.substring(methodStart); + assertTrue(method.contains("IrisData data = dimension.getLoader()")); + assertTrue(method.contains("PlatformChunkGenerator targetGenerator = IrisToolbelt.access(targetWorld)")); + assertTrue(method.contains("CommandObject.createPlacer(targetWorld, future, targetEngine)")); + assertTrue(method.contains("if (blockChanges == 0)")); + assertTrue(method.contains("COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS")); + Path structureSource = Path.of(System.getProperty("iris.commandStructureSource")); + String objectSource = Files.readString(structureSource.resolveSibling("CommandObject.java")); + assertTrue(objectSource.contains("futureBlockChanges.putIfAbsent(block, block.getBlockData())")); + assertFalse(method.contains("data.getEngine()")); + } + + @Test + public void structureVerifyPartitionsPolicyBeforeNativeReachability() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource"))); + int methodStart = source.indexOf("private void runVerification("); + int methodEnd = source.indexOf("private void sendVerificationMessages(", methodStart); + String method = source.substring(methodStart, methodEnd); + int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(engine, keyName, false)"); + int nativeRequirement = method.indexOf( + "requiresNativeReachability |= !IrisStructureLocator.isPlaced", policyResolution); + int reachabilityGuard = method.indexOf("if (requiresNativeReachability)", nativeRequirement); + int reachabilityLookup = method.indexOf("StructureReachability.reachableKeys(engine)", reachabilityGuard); + + assertTrue(policyResolution >= 0); + assertTrue(nativeRequirement > policyResolution); + assertTrue(reachabilityGuard > nativeRequirement); + assertTrue(reachabilityLookup > reachabilityGuard); + } + + private static IrisNativeStructureDecision decision(NativeStructureGenerationStatus status) { + return new IrisNativeStructureDecision( + status, 0, null, false, null, null); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/ObjectPasteBoundsTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/ObjectPasteBoundsTest.java new file mode 100644 index 000000000..a141763d9 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/ObjectPasteBoundsTest.java @@ -0,0 +1,53 @@ +package art.arcane.iris.core.commands; + +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ObjectPasteBoundsTest { + private static final int ANCHOR_X = 100; + private static final int ANCHOR_Y = 65; + private static final int ANCHOR_Z = -30; + + @Test + public void preservesPasteBoundsWithoutRotation() { + ObjectPasteBounds bounds = resolve(0); + + assertEquals(new ObjectPasteBounds(98, 64, -31, 101, 66, -30), bounds); + } + + @Test + public void followsQuarterTurnPasteFootprint() { + ObjectPasteBounds bounds = resolve(90); + + assertEquals(new ObjectPasteBounds(99, 64, -31, 100, 66, -28), bounds); + } + + @Test + public void followsHalfTurnPasteOffset() { + ObjectPasteBounds bounds = resolve(180); + + assertEquals(new ObjectPasteBounds(99, 64, -30, 102, 66, -29), bounds); + } + + @Test + public void followsThreeQuarterTurnPasteOffset() { + ObjectPasteBounds bounds = resolve(270); + + assertEquals(new ObjectPasteBounds(100, 64, -32, 101, 66, -29), bounds); + } + + @Test + public void enclosesRoundedArbitraryAnglePasteFootprint() { + ObjectPasteBounds bounds = resolve(45); + + assertEquals(new ObjectPasteBounds(98, 64, -31, 101, 66, -29), bounds); + } + + private ObjectPasteBounds resolve(int rotation) { + IrisObject object = new IrisObject(4, 3, 2); + return ObjectPasteBounds.resolve(object, IrisObjectRotation.of(0, rotation, 0), ANCHOR_X, ANCHOR_Y, ANCHOR_Z); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/PaperCommandRegistrarTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/PaperCommandRegistrarTest.java new file mode 100644 index 000000000..30de6e3e6 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/PaperCommandRegistrarTest.java @@ -0,0 +1,116 @@ +package art.arcane.iris.core.commands; + +import art.arcane.iris.core.service.CommandSVC; +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.junit.Test; + +import java.lang.reflect.Proxy; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class PaperCommandRegistrarTest { + @Test + public void paperCommandDelegatesExecutionAndSuggestions() { + RecordingCommandService service = new RecordingCommandService(); + BasicCommand command = PaperCommandRegistrar.command(service); + CommandSender sender = sender(); + CommandSourceStack source = new TestCommandSourceStack(sender); + String[] arguments = {"create", "world"}; + + command.execute(source, arguments); + assertSame(sender, service.sender); + assertEquals("iris", service.label); + assertArrayEquals(arguments, service.arguments); + assertEquals(List.of("first", "second"), command.suggest(source, new String[]{"cr"})); + assertTrue(command.canUse(sender)); + } + + private static CommandSender sender() { + return (CommandSender) Proxy.newProxyInstance( + CommandSender.class.getClassLoader(), + new Class[]{CommandSender.class}, + (proxy, method, arguments) -> primitiveDefault(method.getReturnType()) + ); + } + + private static Object primitiveDefault(Class type) { + if (type == boolean.class) { + return false; + } + if (type == byte.class) { + return (byte) 0; + } + if (type == short.class) { + return (short) 0; + } + if (type == int.class) { + return 0; + } + if (type == long.class) { + return 0L; + } + if (type == float.class) { + return 0F; + } + if (type == double.class) { + return 0D; + } + if (type == char.class) { + return '\0'; + } + return null; + } + + private static final class RecordingCommandService extends CommandSVC { + private CommandSender sender; + private String label; + private String[] arguments; + + @Override + public void executeRoot(CommandSender sender, String label, String[] args) { + this.sender = sender; + this.label = label; + this.arguments = args; + } + + @Override + public List tabCompleteRoot(CommandSender sender, String alias, String[] args) { + return List.of("first", "second"); + } + } + + private record TestCommandSourceStack(CommandSender sender) implements CommandSourceStack { + @Override + public Location getLocation() { + return null; + } + + @Override + public CommandSender getSender() { + return sender; + } + + @Override + public Entity getExecutor() { + return null; + } + + @Override + public CommandSourceStack withLocation(Location location) { + return this; + } + + @Override + public CommandSourceStack withExecutor(Entity executor) { + return this; + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/StudioScoreboardCommandTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/StudioScoreboardCommandTest.java new file mode 100644 index 000000000..6d6d88e28 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/StudioScoreboardCommandTest.java @@ -0,0 +1,25 @@ +package art.arcane.iris.core.commands; + +import art.arcane.volmlib.util.director.DirectorOrigin; +import art.arcane.volmlib.util.director.annotations.Director; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class StudioScoreboardCommandTest { + @Test + public void scoreboardCommandIsPlayerScopedAndEntityScheduled() throws NoSuchMethodException { + Method command = CommandStudio.class.getDeclaredMethod("scoreboard"); + Director director = command.getAnnotation(Director.class); + + assertEquals(DirectorOrigin.PLAYER, director.origin()); + assertFalse(director.sync()); + assertTrue(List.of(director.aliases()).contains("board")); + assertTrue(List.of(director.aliases()).contains("sidebar")); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/FakeIrisTerrainService.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/FakeIrisTerrainService.java new file mode 100644 index 000000000..de2890d3c --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/FakeIrisTerrainService.java @@ -0,0 +1,117 @@ +package art.arcane.iris.core.link; + +import art.arcane.iris.api.terrain.IrisColumnQuery; +import art.arcane.iris.api.terrain.IrisColumnSink; +import art.arcane.iris.api.terrain.IrisSurfaceKind; +import art.arcane.iris.api.terrain.IrisTerrainService; +import art.arcane.iris.api.terrain.IrisWorldInfo; +import org.bukkit.World; + +import java.util.HashSet; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; + +final class FakeIrisTerrainService implements IrisTerrainService { + private final Set irisWorlds = new HashSet<>(); + + private String dimensionKey = "overworld"; + private String biomeName = "Hot Desert Dunes"; + private String biomeKey = "desert/hot-dunes"; + private String regionName = "Scorched Expanse"; + private String regionKey = "scorched"; + private int builds; + private int lastBlockX; + private int lastBlockZ; + + void addIrisWorld(World world) { + irisWorlds.add(world); + } + + void describe(String dimensionKey, String biomeName, String biomeKey, String regionName, String regionKey) { + this.dimensionKey = dimensionKey; + this.biomeName = biomeName; + this.biomeKey = biomeKey; + this.regionName = regionName; + this.regionKey = regionKey; + } + + int builds() { + return builds; + } + + String sampledColumn() { + return lastBlockX + "," + lastBlockZ; + } + + @Override + public boolean isIrisWorld(World world) { + return irisWorlds.contains(world); + } + + @Override + public Optional worldInfo(World world) { + if (!isIrisWorld(world)) { + return Optional.empty(); + } + + return Optional.of(new IrisWorldInfo(dimensionKey, "identity", 42L, -64, 320, 63, false)); + } + + @Override + public OptionalInt surfaceHeight(World world, int blockX, int blockZ) { + throw new AssertionError("a placeholder must never sample terrain height"); + } + + @Override + public IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ) { + throw new AssertionError("a placeholder must never classify the surface"); + } + + @Override + public Optional surfaceBiomeKey(World world, int blockX, int blockZ) { + return isIrisWorld(world) ? Optional.ofNullable(biomeKey) : Optional.empty(); + } + + @Override + public Optional surfaceBiomeName(World world, int blockX, int blockZ) { + if (!isIrisWorld(world)) { + return Optional.empty(); + } + + builds++; + lastBlockX = blockX; + lastBlockZ = blockZ; + return Optional.ofNullable(biomeName); + } + + @Override + public Optional biomeKey(World world, int blockX, int blockY, int blockZ) { + throw new AssertionError("a placeholder must never resolve a three dimensional biome"); + } + + @Override + public Optional regionKey(World world, int blockX, int blockZ) { + return isIrisWorld(world) ? Optional.ofNullable(regionKey) : Optional.empty(); + } + + @Override + public Optional regionName(World world, int blockX, int blockZ) { + return isIrisWorld(world) ? Optional.ofNullable(regionName) : Optional.empty(); + } + + @Override + public int maxSampleColumns() { + return 0; + } + + @Override + public int maxSampleChunks() { + return 0; + } + + @Override + public boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink) { + throw new AssertionError("a placeholder must never run a column sample"); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiExpansionTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiExpansionTest.java new file mode 100644 index 000000000..5a8269cc3 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiExpansionTest.java @@ -0,0 +1,249 @@ +package art.arcane.iris.core.link; + +import art.arcane.iris.api.pregen.IrisPregenPhase; +import art.arcane.iris.api.pregen.IrisPregenProgress; +import art.arcane.iris.api.terrain.IrisTerrainService; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class IrisPapiExpansionTest { + private static final UUID PLAYER_ID = UUID.fromString("00000000-0000-0000-0000-0000000000b2"); + private static final String DASH = "---"; + + private static final List PUBLISHED_KEYS = List.of( + "available", + "pregen.available", + "pregen.chunks", + "pregen.chunks-per-second", + "pregen.eta", + "pregen.eta-text", + "pregen.paused", + "pregen.percent", + "pregen.total", + "pregen.world", + "world.available", + "world.biome", + "world.biome-key", + "world.dimension", + "world.region", + "world.region-key"); + + private static final List RETIRED_KEYS = List.of( + "biome_name", + "biome_id", + "biome_file", + "region_name", + "region_id", + "region_file", + "terrain_slope", + "terrain_height", + "world_mode", + "world_seed", + "world_speed"); + + private static Logger quietLogger() { + Logger logger = Logger.getLogger("IrisPapiExpansionTest"); + logger.setUseParentHandlers(false); + logger.setLevel(Level.OFF); + return logger; + } + + private static IrisPapiExpansion expansion(IrisPapiState state) { + return new IrisPapiExpansion(state, quietLogger()); + } + + private static IrisPapiState populatedState() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + state.trackPosition(PLAYER_ID, world, 64, 64); + state.publishPregen(IrisPregenPhase.TICK, new IrisPregenProgress( + "sandbox", "sandbox:identity", 42.5D, 1234L, 4096L, 2862L, 0L, 12.5D, 125_000L, 60_000L, "async", false)); + return state; + } + + @Test + public void theExpansionPublishesExactlyTheDocumentedKeySet() { + assertEquals(PUBLISHED_KEYS, expansion(populatedState()).getPlaceholders()); + } + + @Test + public void everyPublishedKeyResolvesToAValue() { + IrisPapiExpansion expansion = expansion(populatedState()); + OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID); + + for (String key : PUBLISHED_KEYS) { + String value = expansion.onRequest(player, key); + assertNotNull("published key " + key + " must resolve", value); + assertFalse("published key " + key + " must not resolve to an empty string", value.isEmpty()); + } + } + + @Test + public void aFullyPopulatedBoardRendersRealValues() { + IrisPapiExpansion expansion = expansion(populatedState()); + OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID); + + assertEquals("true", expansion.onRequest(player, "available")); + assertEquals("true", expansion.onRequest(player, "world.available")); + assertEquals("Hot Desert Dunes", expansion.onRequest(player, "world.biome")); + assertEquals("desert/hot-dunes", expansion.onRequest(player, "world.biome-key")); + assertEquals("Scorched Expanse", expansion.onRequest(player, "world.region")); + assertEquals("scorched", expansion.onRequest(player, "world.region-key")); + assertEquals("overworld", expansion.onRequest(player, "world.dimension")); + assertEquals("true", expansion.onRequest(player, "pregen.available")); + assertEquals("sandbox", expansion.onRequest(player, "pregen.world")); + assertEquals("42.50", expansion.onRequest(player, "pregen.percent")); + assertEquals("125", expansion.onRequest(player, "pregen.eta")); + assertEquals("2m 5s", expansion.onRequest(player, "pregen.eta-text")); + assertEquals("1234", expansion.onRequest(player, "pregen.chunks")); + assertEquals("4096", expansion.onRequest(player, "pregen.total")); + assertEquals("12.50", expansion.onRequest(player, "pregen.chunks-per-second")); + assertEquals("false", expansion.onRequest(player, "pregen.paused")); + } + + @Test + public void everyPublishedKeyObeysTheSuiteGrammar() { + for (String key : expansion(populatedState()).getPlaceholders()) { + assertFalse("a placeholder path may never contain '_': " + key, key.indexOf('_') >= 0); + assertEquals("a placeholder path is lowercase ascii: " + key, key.toLowerCase(Locale.ROOT), key); + + for (String segment : key.split("\\.", -1)) { + assertFalse("empty segment in " + key, segment.isEmpty()); + assertTrue("segment must match [a-z0-9-] in " + key, segment.matches("[a-z0-9-]+")); + } + } + } + + @Test + public void anUnknownPathReturnsNullSoTheTypoStaysVisible() { + IrisPapiExpansion expansion = expansion(populatedState()); + OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID); + + assertNull(expansion.onRequest(player, "world.biom")); + assertNull(expansion.onRequest(player, "world")); + assertNull(expansion.onRequest(player, "world.")); + assertNull(expansion.onRequest(player, ".biome")); + assertNull(expansion.onRequest(player, "definitely-not-a-key")); + } + + @Test + public void theRetiredUnderscoreGrammarNoLongerResolves() { + IrisPapiExpansion expansion = expansion(populatedState()); + OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID); + + for (String retired : RETIRED_KEYS) { + assertNull("the old key " + retired + " must render literally, not silently answer", + expansion.onRequest(player, retired)); + } + } + + @Test + public void blankParamsReturnNull() { + IrisPapiExpansion expansion = expansion(populatedState()); + OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID); + + assertNull(expansion.onRequest(player, null)); + assertNull(expansion.onRequest(player, "")); + assertNull(expansion.onRequest(player, " ")); + } + + @Test + public void pathsAreLowercasedBeforeDispatch() { + IrisPapiExpansion expansion = expansion(populatedState()); + OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID); + + assertEquals("Hot Desert Dunes", expansion.onRequest(player, "WORLD.BIOME")); + assertEquals("Hot Desert Dunes", expansion.onRequest(player, "World.Biome")); + } + + @Test + public void anUntrackedPlayerGetsTheUnavailableSentinelRatherThanALie() { + IrisPapiExpansion expansion = expansion(populatedState()); + OfflinePlayer stranger = IrisPapiTestSupport.player(UUID.fromString("00000000-0000-0000-0000-0000000000c3")); + + assertEquals("false", expansion.onRequest(stranger, "world.available")); + assertEquals(DASH, expansion.onRequest(stranger, "world.biome")); + assertEquals("true", expansion.onRequest(stranger, "available")); + } + + @Test + public void aNullPlayerStillAnswersTheGlobalKeys() { + IrisPapiExpansion expansion = expansion(populatedState()); + + assertEquals("true", expansion.onRequest(null, "available")); + assertEquals("42.50", expansion.onRequest(null, "pregen.percent")); + assertEquals(DASH, expansion.onRequest(null, "world.biome")); + } + + @Test + public void aResolverThatThrowsIsCaughtAndReportedAsUnavailable() { + IrisPapiState exploding = new IrisPapiState(() -> { + throw new IllegalStateException("terrain service exploded"); + }, new IrisPapiTestSupport.Clock()); + + assertEquals(DASH, expansion(exploding).onRequest(IrisPapiTestSupport.player(PLAYER_ID), "available")); + } + + @Test + public void metadataIsHardcodedAndTheOwningPluginIsDeclared() { + IrisPapiExpansion expansion = expansion(populatedState()); + + assertEquals("iris", expansion.getIdentifier()); + assertEquals("Volmit Software", expansion.getAuthor()); + assertEquals("2.0.0", expansion.getVersion()); + assertEquals("Iris", expansion.getRequiredPlugin()); + assertTrue(expansion.persist()); + } + + @Test + public void thePlaceholderPathNeverTouchesTheEngineOrAPluginStatic() throws Exception { + for (String file : List.of("IrisPapiExpansion.java", "IrisPapiState.java", "IrisPapiWorldView.java")) { + String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/link/" + file)).replace("\r\n", "\n"); + + assertFalse(file + " must not import the engine", source.contains("art.arcane.iris.engine.")); + assertFalse(file + " must not reach into the toolbelt", source.contains("IrisToolbelt")); + assertFalse(file + " must not read the plugin static", source.contains("Iris.instance")); + assertFalse(file + " must not derive metadata from the description", source.contains("getDescription()")); + assertFalse(file + " must not take a lock", source.contains("synchronized")); + } + } + + @Test + public void theReadmeDocumentsEveryPublishedKeyAndEveryRetiredOne() throws Exception { + String readme = Files.readString(Path.of(System.getProperty("iris.readmeSource"))); + + for (String key : PUBLISHED_KEYS) { + assertTrue("README must document %iris_" + key + "%", readme.contains("%iris_" + key + "%")); + } + + for (String retired : RETIRED_KEYS) { + assertTrue("README must carry the migration row for %iris_" + retired + "%", + readme.contains("%iris_" + retired + "%")); + } + } + + @Test + public void theTerrainServiceIsTheOnlyDoorIntoIris() { + IrisTerrainService service = new FakeIrisTerrainService(); + IrisPapiState state = new IrisPapiState(() -> service, new IrisPapiTestSupport.Clock()); + + assertEquals("true", expansion(state).onRequest(IrisPapiTestSupport.player(PLAYER_ID), "available")); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiLifecycleTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiLifecycleTest.java new file mode 100644 index 000000000..193e614fa --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiLifecycleTest.java @@ -0,0 +1,134 @@ +package art.arcane.iris.core.link; + +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 IrisPapiLifecycleTest { + private static final Path PLUGIN_SOURCE = Path.of("src/main/java/art/arcane/iris/Iris.java"); + private static final String SETUP = "private void setupPapi() {"; + private static final String TEARDOWN = "private void teardownPapi() {"; + + private static String source() throws Exception { + // The assertions below match against literals written with \n, so a CRLF checkout has to + // be folded first or every multi-line expectation misses. + return Files.readString(PLUGIN_SOURCE).replace("\r\n", "\n"); + } + + private static String body(String declaration) throws Exception { + String source = source(); + int start = source.indexOf(declaration); + + assertTrue("Iris.java must declare " + declaration, start >= 0); + + int open = source.indexOf('{', start); + int depth = 0; + + for (int index = open; index < source.length(); index++) { + char character = source.charAt(index); + + if (character == '{') { + depth++; + continue; + } + + if (character != '}') { + continue; + } + + depth--; + + if (depth == 0) { + return source.substring(open + 1, index); + } + } + + throw new AssertionError(declaration + " is not brace balanced"); + } + + @Test + public void registrationIsGatedOnPlaceholderApiBeingEnabled() throws Exception { + assertTrue("setupPapi must bail out when PlaceholderAPI is absent", + body(SETUP).contains("if (!PlaceholderRegistration.isPlaceholderApiEnabled()) {")); + } + + @Test + public void theRegistrationTheListenerAndTheStateAreAllRetained() throws Exception { + String source = source(); + + assertTrue(source.contains("private volatile PlaceholderRegistration papiRegistration;")); + assertTrue(source.contains("private volatile IrisPapiListener papiListener;")); + assertTrue(source.contains("private volatile IrisPapiState papiState;")); + } + + @Test + public void teardownUnregistersTheExpansionTheListenerAndClearsTheState() throws Exception { + String teardown = body(TEARDOWN); + + assertTrue("the retained registration must be unregistered inside teardownPapi", + teardown.contains("registration.unregister();")); + assertTrue("the retained listener must be detached inside teardownPapi", + teardown.contains("HandlerList.unregisterAll(listener);")); + assertTrue("the retained state must drop every held world reference inside teardownPapi", + teardown.contains("state.clear();")); + assertTrue("teardownPapi must drop the retained listener", teardown.contains("papiListener = null;")); + assertTrue("teardownPapi must drop the retained registration", teardown.contains("papiRegistration = null;")); + assertTrue("teardownPapi must drop the retained state", teardown.contains("papiState = null;")); + } + + @Test + public void bothDisablePathsTearThePlaceholderSurfaceDown() throws Exception { + String source = source(); + + assertTrue("onDisable must tear the expansion down", + source.contains("public void onDisable() {\n teardownPapi();")); + assertTrue("the BileTools pre-unload hook must tear the expansion down", + source.contains("public void onPreUnload(ReloadAware.PreUnloadReason reason) {\n teardownPapi();")); + } + + @Test + public void aFailedListenerAttachDoesNotLeaveTheExpansionRegistered() throws Exception { + String setup = body(SETUP); + int attach = setup.indexOf("registerEvents(listener, this)"); + + assertTrue("the listener must be attached inside setupPapi", attach >= 0); + + int rescue = setup.indexOf("} catch (Throwable failure) {", attach); + + assertTrue("the attach must be guarded inside setupPapi", rescue > attach); + + int rollback = setup.indexOf("registration.unregister();", rescue); + + assertTrue("a failed attach must roll the registration back inside setupPapi", rollback > rescue); + + int bail = setup.indexOf("return;", rollback); + + assertTrue("a failed attach must leave setupPapi", bail > rollback); + + int retained = setup.indexOf("papiRegistration = registration;"); + + assertTrue("setupPapi must retain the registration", retained > 0); + assertTrue("the registration may only be retained after a successful attach", retained > bail); + } + + @Test + public void theExpansionIsNeverConstructedFromAPluginStatic() throws Exception { + String setup = body(SETUP); + + assertFalse("the plugin class must not construct the expansion: that forces PlaceholderExpansion to load during enable and crashes a server without PlaceholderAPI", + setup.contains("new IrisPapiExpansion(")); + assertTrue("the expansion takes its state as a constructor argument, built inside the installer", + installerSource().contains("new IrisPapiExpansion(state, logger)")); + assertFalse("the hand rolled PlaceholderAPI presence check must be gone", + source().contains("isPluginEnabled(\"PlaceholderAPI\")")); + } + + private static String installerSource() throws Exception { + return java.nio.file.Files.readString(java.nio.file.Path.of( + "src/main/java/art/arcane/iris/core/link/IrisPapiInstaller.java")).replace("\r\n", "\n"); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiListenerTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiListenerTest.java new file mode 100644 index 000000000..7a2f9fdfa --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiListenerTest.java @@ -0,0 +1,316 @@ +package art.arcane.iris.core.link; + +import art.arcane.iris.api.pregen.IrisPregenPhase; +import art.arcane.iris.api.pregen.IrisPregenProgress; +import art.arcane.iris.api.pregen.IrisPregenerationEvent; +import net.kyori.adventure.text.Component; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerPortalEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class IrisPapiListenerTest { + private static final UUID PLAYER = UUID.fromString("00000000-0000-0000-0000-0000000000d4"); + private static final String DASH = "---"; + + private static IrisPregenProgress progress(double percent) { + return new IrisPregenProgress( + "sandbox", + "sandbox:identity", + percent, + 1234L, + 4096L, + 2862L, + 0L, + 12.5D, + 125_000L, + 60_000L, + "async", + false); + } + + private static final class Harness { + private final FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + private final IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock(); + private final IrisPapiState state = new IrisPapiState(() -> terrain, clock); + private final IrisPapiListener listener = new IrisPapiListener(state); + private final World sandbox = IrisPapiTestSupport.world("sandbox"); + private final World hub = IrisPapiTestSupport.world("hub"); + private final IrisPapiTestSupport.StandingPlayer player; + + private Harness() { + terrain.addIrisWorld(sandbox); + player = new IrisPapiTestSupport.StandingPlayer(PLAYER, at(sandbox, 10, 10)); + } + + private static Location at(World world, int blockX, int blockZ) { + return new Location(world, blockX + 0.5D, 71.0D, blockZ + 0.5D); + } + } + + @Test + public void theListenerIsABukkitListener() { + assertTrue(Listener.class.isAssignableFrom(IrisPapiListener.class)); + } + + @Test + public void everyPositionSourceIsHandledAtMonitorPriority() { + Set> handled = new HashSet<>(); + + for (Method method : IrisPapiListener.class.getDeclaredMethods()) { + EventHandler handler = method.getAnnotation(EventHandler.class); + + if (handler == null) { + continue; + } + + assertEquals("placeholder bookkeeping must never influence an event: " + method.getName(), + EventPriority.MONITOR, handler.priority()); + assertEquals("a handler takes exactly one event: " + method.getName(), + 1, method.getParameterCount()); + handled.add(method.getParameterTypes()[0]); + } + + assertEquals(Set.of( + PlayerMoveEvent.class, + PlayerTeleportEvent.class, + PlayerPortalEvent.class, + PlayerRespawnEvent.class, + PlayerJoinEvent.class, + PlayerChangedWorldEvent.class, + PlayerQuitEvent.class, + IrisPregenerationEvent.class), + handled); + } + + @Test + public void everyStepOfTheMoveHierarchyOwnsItsHandlerListSoNoneIsCoveredByItsParent() throws Exception { + List> hierarchy = + List.of(PlayerMoveEvent.class, PlayerTeleportEvent.class, PlayerPortalEvent.class); + Set> handled = handledEventTypes(); + + for (int index = 1; index < hierarchy.size(); index++) { + Class child = hierarchy.get(index); + Class parent = hierarchy.get(index - 1); + + assertSame(child.getName() + " no longer extends " + parent.getName(), + parent, child.getSuperclass()); + assertNotSame(child.getName() + " has its own HandlerList, so a fired " + child.getSimpleName() + + " never reaches a " + parent.getSimpleName() + " handler", + handlerList(parent), handlerList(child)); + assertTrue(child.getName() + " is a position source that no other handler can cover", + handled.contains(child)); + } + + assertTrue(PlayerMoveEvent.class.getName() + " must still be handled", + handled.contains(PlayerMoveEvent.class)); + } + + private static HandlerList handlerList(Class type) throws Exception { + return (HandlerList) type.getDeclaredMethod("getHandlerList").invoke(null); + } + + private static Set> handledEventTypes() { + Set> handled = new HashSet<>(); + + for (Method method : IrisPapiListener.class.getDeclaredMethods()) { + if (method.getAnnotation(EventHandler.class) != null) { + handled.add(method.getParameterTypes()[0]); + } + } + + return handled; + } + + @Test + public void joiningPublishesTheColumnThePlayerLandsOn() { + Harness harness = new Harness(); + + assertEquals("false", harness.state.worldAvailable(PLAYER)); + + harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty())); + + assertEquals("true", harness.state.worldAvailable(PLAYER)); + assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER)); + assertEquals("10,10", harness.terrain.sampledColumn()); + } + + @Test + public void movingPublishesTheDestinationColumn() { + Harness harness = new Harness(); + Location from = harness.player.standing(); + Location to = Harness.at(harness.sandbox, 40, -80); + + harness.listener.onPlayerMove(new PlayerMoveEvent(harness.player.handle(), from, to)); + + assertEquals("true", harness.state.worldAvailable(PLAYER)); + assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER)); + assertEquals("40,-80", harness.terrain.sampledColumn()); + } + + @Test + public void aSameWorldTeleportUpdatesTheBoardWithoutAnyFurtherMovement() { + Harness harness = new Harness(); + + harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty())); + assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER)); + assertEquals("10,10", harness.terrain.sampledColumn()); + + harness.terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier"); + + Location from = harness.player.standing(); + Location to = Harness.at(harness.sandbox, 2000, -2000); + harness.player.standAt(to); + harness.listener.onPlayerTeleport(new PlayerTeleportEvent( + harness.player.handle(), from, to, PlayerTeleportEvent.TeleportCause.COMMAND)); + + assertEquals("a same world teleport must republish the position immediately," + + " with no move, no world change and no clock advance", + "Frozen Shelf", harness.state.biome(PLAYER)); + assertEquals("2000,-2000", harness.terrain.sampledColumn()); + assertEquals("cold/shelf", harness.state.biomeKey(PLAYER)); + assertEquals("Glacier", harness.state.region(PLAYER)); + } + + @Test + public void aSameWorldPortalUpdatesTheBoardWithoutAnyFurtherMovement() { + Harness harness = new Harness(); + + harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty())); + assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER)); + + harness.terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier"); + + Location from = harness.player.standing(); + Location to = Harness.at(harness.sandbox, 512, 512); + harness.player.standAt(to); + harness.listener.onPlayerPortal(new PlayerPortalEvent( + harness.player.handle(), from, to, PlayerTeleportEvent.TeleportCause.NETHER_PORTAL)); + + assertEquals("Frozen Shelf", harness.state.biome(PLAYER)); + assertEquals("512,512", harness.terrain.sampledColumn()); + } + + @Test + public void aSameWorldRespawnUpdatesTheBoardWithoutAnyFurtherMovement() { + Harness harness = new Harness(); + + harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty())); + assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER)); + + harness.terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier"); + + Location bed = Harness.at(harness.sandbox, -333, 777); + harness.player.standAt(bed); + harness.listener.onPlayerRespawn(new PlayerRespawnEvent( + harness.player.handle(), bed, true, false, false, PlayerRespawnEvent.RespawnReason.DEATH)); + + assertEquals("Frozen Shelf", harness.state.biome(PLAYER)); + assertEquals("-333,777", harness.terrain.sampledColumn()); + } + + @Test + public void changingWorldsRepublishesAgainstTheNewWorld() { + Harness harness = new Harness(); + + harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty())); + assertEquals("true", harness.state.worldAvailable(PLAYER)); + + harness.player.standAt(Harness.at(harness.hub, 0, 0)); + harness.listener.onPlayerChangedWorld(new PlayerChangedWorldEvent(harness.player.handle(), harness.sandbox)); + + assertEquals("false", harness.state.worldAvailable(PLAYER)); + assertEquals(DASH, harness.state.biome(PLAYER)); + assertEquals(DASH, harness.state.dimension(PLAYER)); + } + + @Test + public void quittingEvictsThePositionAndTheViewSoNoWorldIsHeld() { + Harness harness = new Harness(); + + harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty())); + assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER)); + + harness.listener.onPlayerQuit(new PlayerQuitEvent( + harness.player.handle(), Component.empty(), PlayerQuitEvent.QuitReason.DISCONNECTED)); + + assertEquals("false", harness.state.worldAvailable(PLAYER)); + assertEquals(DASH, harness.state.biome(PLAYER)); + assertEquals(DASH, harness.state.region(PLAYER)); + assertEquals(DASH, harness.state.dimension(PLAYER)); + } + + @Test + public void thePregenHandlerLatchesProgressAndRetiresItOnATerminalPhase() { + Harness harness = new Harness(); + + assertEquals("false", harness.state.pregenAvailable(PLAYER)); + + harness.listener.onPregeneration(new IrisPregenerationEvent(IrisPregenPhase.TICK, progress(42.5D))); + + assertEquals("true", harness.state.pregenAvailable(PLAYER)); + assertEquals("sandbox", harness.state.pregenWorld(PLAYER)); + assertEquals("42.50", harness.state.pregenPercent(PLAYER)); + assertEquals("2m 5s", harness.state.pregenEtaText(PLAYER)); + + harness.listener.onPregeneration(new IrisPregenerationEvent(IrisPregenPhase.COMPLETED, progress(100.0D))); + + assertEquals("false", harness.state.pregenAvailable(PLAYER)); + assertEquals(DASH, harness.state.pregenPercent(PLAYER)); + } + + @Test + public void trackingPublishesTheBlockColumnOfTheLocation() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + + IrisPapiListener.track(state, PLAYER, new Location(world, 128.7D, 71.0D, -512.2D)); + + assertEquals("true", state.worldAvailable(PLAYER)); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + assertEquals("128,-513", terrain.sampledColumn()); + } + + @Test + public void trackingIgnoresMissingInputsInsteadOfThrowing() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + + IrisPapiListener.track(null, PLAYER, new Location(world, 0.0D, 0.0D, 0.0D)); + IrisPapiListener.track(state, null, new Location(world, 0.0D, 0.0D, 0.0D)); + IrisPapiListener.track(state, PLAYER, null); + IrisPapiListener.track(state, PLAYER, new Location(null, 0.0D, 0.0D, 0.0D)); + IrisPapiListener.trackNow(null, PLAYER, new Location(world, 0.0D, 0.0D, 0.0D)); + IrisPapiListener.trackNow(state, null, new Location(world, 0.0D, 0.0D, 0.0D)); + IrisPapiListener.trackNow(state, PLAYER, null); + IrisPapiListener.trackNow(state, PLAYER, new Location(null, 0.0D, 0.0D, 0.0D)); + + assertEquals("false", state.worldAvailable(PLAYER)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiStateTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiStateTest.java new file mode 100644 index 000000000..0e2d568e6 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiStateTest.java @@ -0,0 +1,394 @@ +package art.arcane.iris.core.link; + +import art.arcane.iris.api.pregen.IrisPregenPhase; +import art.arcane.iris.api.pregen.IrisPregenProgress; +import art.arcane.iris.api.terrain.IrisTerrainService; +import org.bukkit.World; +import org.junit.Test; + +import java.util.List; +import java.util.UUID; +import java.util.function.Function; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; + +public class IrisPapiStateTest { + private static final UUID PLAYER = UUID.fromString("00000000-0000-0000-0000-0000000000a1"); + private static final String DASH = "---"; + private static final long POSITION_INTERVAL = IrisPapiState.POSITION_INTERVAL_MS; + + private static IrisPregenProgress progress(double percent, long etaMillis, boolean paused) { + return new IrisPregenProgress( + "sandbox", + "sandbox:identity", + percent, + 1234L, + 4096L, + 2862L, + 0L, + 12.5D, + etaMillis, + 60_000L, + "async", + paused); + } + + @Test + public void availableReportsWhetherTheTerrainServiceIsRegistered() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + IrisPapiState absent = new IrisPapiState(() -> null, new IrisPapiTestSupport.Clock()); + IrisPapiState present = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + + assertEquals("false", absent.available(PLAYER)); + assertEquals("true", present.available(PLAYER)); + } + + @Test + public void worldKeysAreUnavailableUntilAPositionIsTracked() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + + assertEquals("false", state.worldAvailable(PLAYER)); + assertEquals(DASH, state.biome(PLAYER)); + assertEquals(DASH, state.biomeKey(PLAYER)); + assertEquals(DASH, state.region(PLAYER)); + assertEquals(DASH, state.regionKey(PLAYER)); + assertEquals(DASH, state.dimension(PLAYER)); + assertEquals(0, terrain.builds()); + } + + @Test + public void worldKeysResolveTheBiomeRegionAndDimensionAtTheTrackedColumn() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + + state.trackPosition(PLAYER, world, 128, -512); + + assertEquals("true", state.worldAvailable(PLAYER)); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + assertEquals("desert/hot-dunes", state.biomeKey(PLAYER)); + assertEquals("Scorched Expanse", state.region(PLAYER)); + assertEquals("scorched", state.regionKey(PLAYER)); + assertEquals("overworld", state.dimension(PLAYER)); + } + + @Test + public void aNonIrisWorldAnswersFalseAndDashesWithoutQueryingTerrain() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World hub = IrisPapiTestSupport.world("hub"); + IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + + state.trackPosition(PLAYER, hub, 0, 0); + + assertEquals("false", state.worldAvailable(PLAYER)); + assertEquals(DASH, state.biome(PLAYER)); + assertEquals(DASH, state.biomeKey(PLAYER)); + assertEquals(DASH, state.region(PLAYER)); + assertEquals(DASH, state.regionKey(PLAYER)); + assertEquals(DASH, state.dimension(PLAYER)); + assertEquals(0, terrain.builds()); + } + + @Test + public void aWholeBoardOfKeysBuildsTheViewExactlyOncePerSecond() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock(); + IrisPapiState state = new IrisPapiState(() -> terrain, clock); + + state.trackPosition(PLAYER, world, 10, 10); + + for (int pass = 0; pass < 6; pass++) { + state.worldAvailable(PLAYER); + state.biome(PLAYER); + state.biomeKey(PLAYER); + state.region(PLAYER); + state.regionKey(PLAYER); + state.dimension(PLAYER); + } + + assertEquals(1, terrain.builds()); + } + + @Test + public void theViewIsRebuiltOnceTheOneSecondTtlElapses() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock(); + IrisPapiState state = new IrisPapiState(() -> terrain, clock); + + state.trackPosition(PLAYER, world, 10, 10); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + + terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier"); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + + clock.advance(IrisPapiState.VIEW_TTL_MS); + assertEquals("Frozen Shelf", state.biome(PLAYER)); + assertEquals(2, terrain.builds()); + } + + @Test + public void asecondColumnWithinTheSameSecondDoesNotRepublishThePosition() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock(); + IrisPapiState state = new IrisPapiState(() -> terrain, clock); + + state.trackPosition(PLAYER, world, 10, 10); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + + terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier"); + + for (int step = 1; step <= 30; step++) { + clock.advance(40L); + state.trackPosition(PLAYER, world, 10 + step, 10); + state.biome(PLAYER); + } + + assertEquals("a sprinting player must not force more than one view build per second", + 2, terrain.builds()); + } + + @Test + public void aMoveInsideTheSameBlockColumnNeverRepublishesThePosition() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock(); + IrisPapiState state = new IrisPapiState(() -> terrain, clock); + + state.trackPosition(PLAYER, world, 10, 10); + clock.advance(POSITION_INTERVAL / 2L); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + + terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier"); + clock.advance(POSITION_INTERVAL / 2L); + state.trackPosition(PLAYER, world, 10, 10); + clock.advance(POSITION_INTERVAL / 5L); + + assertEquals("a look around inside one block column must not invalidate the memoised view", + "Hot Desert Dunes", state.biome(PLAYER)); + assertEquals(1, terrain.builds()); + } + + @Test + public void anImmediatePublishIgnoresTheOneSecondPositionGate() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock(); + IrisPapiState state = new IrisPapiState(() -> terrain, clock); + + state.trackPosition(PLAYER, world, 10, 10); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + assertEquals("10,10", terrain.sampledColumn()); + + terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier"); + state.trackPositionNow(PLAYER, world, 2000, -2000); + + assertEquals("a discrete jump must not be swallowed by the move rate limiter", + "Frozen Shelf", state.biome(PLAYER)); + assertEquals("2000,-2000", terrain.sampledColumn()); + } + + @Test + public void anImmediatePublishInsideTheSameBlockColumnStillCostsNothing() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock(); + IrisPapiState state = new IrisPapiState(() -> terrain, clock); + + state.trackPosition(PLAYER, world, 10, 10); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + + terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier"); + state.trackPositionNow(PLAYER, world, 10, 10); + + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + assertEquals(1, terrain.builds()); + } + + @Test + public void crossingIntoAnotherWorldRepublishesImmediately() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World sandbox = IrisPapiTestSupport.world("sandbox"); + World hub = IrisPapiTestSupport.world("hub"); + terrain.addIrisWorld(sandbox); + IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock(); + IrisPapiState state = new IrisPapiState(() -> terrain, clock); + + state.trackPosition(PLAYER, sandbox, 10, 10); + assertEquals("true", state.worldAvailable(PLAYER)); + + state.trackPosition(PLAYER, hub, 0, 0); + assertEquals("false", state.worldAvailable(PLAYER)); + assertEquals(DASH, state.biome(PLAYER)); + } + + @Test + public void releasingAPlayerClearsTheTrackedPositionAndView() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + + state.trackPosition(PLAYER, world, 10, 10); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + + state.release(PLAYER); + + assertEquals("false", state.worldAvailable(PLAYER)); + assertEquals(DASH, state.biome(PLAYER)); + assertEquals(DASH, state.dimension(PLAYER)); + } + + @Test + public void clearingTheStateDropsEveryTrackedPlayerAndTheLatchedPregen() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + + state.trackPosition(PLAYER, world, 10, 10); + state.publishPregen(IrisPregenPhase.TICK, progress(42.5D, 125_000L, false)); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + assertEquals("true", state.pregenAvailable(PLAYER)); + + state.clear(); + + assertEquals(DASH, state.biome(PLAYER)); + assertEquals("false", state.pregenAvailable(PLAYER)); + } + + @Test + public void pregenKeysAreUnavailableUntilAnEventArrives() { + IrisPapiState state = new IrisPapiState(FakeIrisTerrainService::new, new IrisPapiTestSupport.Clock()); + + assertEquals("false", state.pregenAvailable(PLAYER)); + assertEquals(DASH, state.pregenWorld(PLAYER)); + assertEquals(DASH, state.pregenPercent(PLAYER)); + assertEquals(DASH, state.pregenEta(PLAYER)); + assertEquals(DASH, state.pregenEtaText(PLAYER)); + assertEquals(DASH, state.pregenChunks(PLAYER)); + assertEquals(DASH, state.pregenTotal(PLAYER)); + assertEquals(DASH, state.pregenChunksPerSecond(PLAYER)); + assertEquals(DASH, state.pregenPaused(PLAYER)); + } + + @Test + public void pregenKeysRenderTheLatchedProgress() { + IrisPapiState state = new IrisPapiState(FakeIrisTerrainService::new, new IrisPapiTestSupport.Clock()); + + state.publishPregen(IrisPregenPhase.TICK, progress(42.5D, 125_000L, false)); + + assertEquals("true", state.pregenAvailable(PLAYER)); + assertEquals("sandbox", state.pregenWorld(PLAYER)); + assertEquals("42.50", state.pregenPercent(PLAYER)); + assertEquals("125", state.pregenEta(PLAYER)); + assertEquals("2m 5s", state.pregenEtaText(PLAYER)); + assertEquals("1234", state.pregenChunks(PLAYER)); + assertEquals("4096", state.pregenTotal(PLAYER)); + assertEquals("12.50", state.pregenChunksPerSecond(PLAYER)); + assertEquals("false", state.pregenPaused(PLAYER)); + } + + @Test + public void aPausedPregenReportsPaused() { + IrisPapiState state = new IrisPapiState(FakeIrisTerrainService::new, new IrisPapiTestSupport.Clock()); + + state.publishPregen(IrisPregenPhase.PAUSED, progress(42.5D, 125_000L, true)); + + assertEquals("true", state.pregenPaused(PLAYER)); + } + + @Test + public void pregenIsClearedWhenTheJobFinishesOrIsCancelled() { + for (IrisPregenPhase terminal : List.of(IrisPregenPhase.COMPLETED, IrisPregenPhase.CANCELLED)) { + IrisPapiState state = new IrisPapiState(FakeIrisTerrainService::new, new IrisPapiTestSupport.Clock()); + state.publishPregen(IrisPregenPhase.TICK, progress(99.0D, 1_000L, false)); + assertEquals("true", state.pregenAvailable(PLAYER)); + + state.publishPregen(terminal, progress(100.0D, 0L, false)); + + assertEquals(terminal + " must retire the pregen snapshot", "false", state.pregenAvailable(PLAYER)); + assertEquals(DASH, state.pregenPercent(PLAYER)); + } + } + + @Test + public void noPublishedValueEverCarriesAPercentOrSectionCharacter() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + terrain.describe("over%world", "Hot \u00A7cDesert", "hot%key", "Region\u00A7a", "reg%ion"); + IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock()); + + state.trackPosition(PLAYER, world, 0, 0); + state.publishPregen(IrisPregenPhase.TICK, progress(100.0D, 3_600_000L, false)); + + List> resolvers = List.of( + state::available, + state::worldAvailable, + state::biome, + state::biomeKey, + state::region, + state::regionKey, + state::dimension, + state::pregenAvailable, + state::pregenWorld, + state::pregenPercent, + state::pregenEta, + state::pregenEtaText, + state::pregenChunks, + state::pregenTotal, + state::pregenChunksPerSecond, + state::pregenPaused); + + for (Function resolver : resolvers) { + String value = resolver.apply(PLAYER); + assertFalse("a placeholder value may never contain '%': " + value, value.indexOf('%') >= 0); + assertFalse("a placeholder value may never contain a legacy colour code: " + value, + value.indexOf('\u00A7') >= 0); + } + + assertNotEquals("Hot \u00A7cDesert", state.biome(PLAYER)); + } + + @Test + public void etaTextCollapsesSecondsMinutesAndHours() { + assertEquals("0s", IrisPapiPregenView.duration(0L)); + assertEquals("45s", IrisPapiPregenView.duration(45_000L)); + assertEquals("2m 5s", IrisPapiPregenView.duration(125_000L)); + assertEquals("1h 0m", IrisPapiPregenView.duration(3_600_000L)); + assertEquals("2h 3m", IrisPapiPregenView.duration(7_380_000L)); + } + + @Test + public void aTerrainServiceThatDisappearsStopsAnsweringWithoutThrowing() { + FakeIrisTerrainService terrain = new FakeIrisTerrainService(); + World world = IrisPapiTestSupport.world("sandbox"); + terrain.addIrisWorld(world); + IrisTerrainService[] holder = new IrisTerrainService[]{terrain}; + IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock(); + IrisPapiState state = new IrisPapiState(() -> holder[0], clock); + + state.trackPosition(PLAYER, world, 10, 10); + assertEquals("Hot Desert Dunes", state.biome(PLAYER)); + + holder[0] = null; + clock.advance(IrisPapiState.VIEW_TTL_MS); + + assertEquals("false", state.available(PLAYER)); + assertEquals("false", state.worldAvailable(PLAYER)); + assertEquals(DASH, state.biome(PLAYER)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiTestSupport.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiTestSupport.java new file mode 100644 index 000000000..24dc2c8c7 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPapiTestSupport.java @@ -0,0 +1,104 @@ +package art.arcane.iris.core.link; + +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.LongSupplier; + +final class IrisPapiTestSupport { + private IrisPapiTestSupport() { + } + + static World world(String name) { + InvocationHandler handler = (Object proxy, Method method, Object[] args) -> switch (method.getName()) { + case "getName" -> name; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + case "toString" -> "World[" + name + "]"; + default -> throw new AssertionError("a placeholder must not call World#" + method.getName()); + }; + + return (World) Proxy.newProxyInstance( + World.class.getClassLoader(), + new Class[]{World.class}, + handler); + } + + static OfflinePlayer player(UUID id) { + InvocationHandler handler = (Object proxy, Method method, Object[] args) -> switch (method.getName()) { + case "getUniqueId" -> id; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + case "toString" -> "OfflinePlayer[" + id + "]"; + default -> throw new AssertionError("a placeholder must not call OfflinePlayer#" + method.getName()); + }; + + return (OfflinePlayer) Proxy.newProxyInstance( + OfflinePlayer.class.getClassLoader(), + new Class[]{OfflinePlayer.class}, + handler); + } + + static final class StandingPlayer { + private final UUID id; + private final AtomicReference standing = new AtomicReference<>(); + private final Player handle; + + StandingPlayer(UUID id, Location location) { + this.id = id; + this.standing.set(location); + InvocationHandler handler = (Object proxy, Method method, Object[] args) -> switch (method.getName()) { + case "getUniqueId" -> this.id; + case "getLocation" -> this.standing.get(); + case "getWorld" -> this.standing.get().getWorld(); + case "getName" -> "StandingPlayer"; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + case "toString" -> "Player[" + this.id + "]"; + default -> throw new AssertionError("a placeholder listener must not call Player#" + method.getName()); + }; + + this.handle = (Player) Proxy.newProxyInstance( + Player.class.getClassLoader(), + new Class[]{Player.class}, + handler); + } + + UUID id() { + return id; + } + + Player handle() { + return handle; + } + + Location standing() { + return standing.get(); + } + + void standAt(Location location) { + standing.set(location); + } + } + + static final class Clock implements LongSupplier { + private final AtomicLong now = new AtomicLong(1_000_000L); + + void advance(long millis) { + now.addAndGet(millis); + } + + @Override + public long getAsLong() { + return now.get(); + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPlaceholderAbsenceTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPlaceholderAbsenceTest.java new file mode 100644 index 000000000..d83f2fa46 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/link/IrisPlaceholderAbsenceTest.java @@ -0,0 +1,82 @@ +package art.arcane.iris.core.link; + +import org.junit.Test; + +import java.net.URL; +import java.net.URLClassLoader; + + +public class IrisPlaceholderAbsenceTest { + private static final String[] LOADS_WITHOUT_PLACEHOLDER_API = { + "art.arcane.iris.core.link.IrisPapiState", + "art.arcane.iris.core.link.IrisPapiListener", + "art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration" + }; + + private static final String[] REQUIRES_PLACEHOLDER_API = { + "art.arcane.iris.core.link.IrisPapiInstaller", + "art.arcane.iris.core.link.IrisPapiExpansion" + }; + + @Test + public void everyEnablePathClassLoadsWhenPlaceholderApiIsAbsent() { + ClassLoader hidden = new PlaceholderApiHidingLoader(); + + for (String name : LOADS_WITHOUT_PLACEHOLDER_API) { + try { + Class.forName(name, true, hidden); + } catch (Throwable failure) { + throw new AssertionError(name + " must load when PlaceholderAPI is not installed", failure); + } + } + } + + @Test + public void theExpansionItselfStillDependsOnPlaceholderApi() { + ClassLoader hidden = new PlaceholderApiHidingLoader(); + + for (String name : REQUIRES_PLACEHOLDER_API) { + boolean threw = false; + + try { + Class.forName(name, true, hidden); + } catch (Throwable failure) { + threw = true; + } + + if (!threw) { + throw new AssertionError(name + " is expected to depend on PlaceholderAPI, so the split above is what keeps the plugin loadable"); + } + } + } + + private static final class PlaceholderApiHidingLoader extends URLClassLoader { + private PlaceholderApiHidingLoader() { + super(classpath(), ClassLoader.getPlatformClassLoader()); + } + + private static URL[] classpath() { + String[] entries = System.getProperty("java.class.path").split(java.io.File.pathSeparator); + URL[] resolved = new URL[entries.length]; + + for (int i = 0; i < entries.length; i++) { + try { + resolved[i] = new java.io.File(entries[i]).toURI().toURL(); + } catch (Throwable failure) { + throw new IllegalStateException(entries[i], failure); + } + } + + return resolved; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("me.clip.")) { + throw new ClassNotFoundException(name); + } + + return super.loadClass(name, resolve); + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/runtime/BukkitEngineLifecycleContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/runtime/BukkitEngineLifecycleContractTest.java new file mode 100644 index 000000000..f4ee99165 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/runtime/BukkitEngineLifecycleContractTest.java @@ -0,0 +1,90 @@ +package art.arcane.iris.core.runtime; + +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 BukkitEngineLifecycleContractTest { + @Test + public void closeFutureIsPublishedBeforeCloseWorkIsScheduled() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource"))); + String closeAsync = method(source, "public CompletableFuture closeAsync()"); + + assertBefore(closeAsync, "closeFuture.compareAndSet(null, future)", "withExclusiveControlFuture("); + assertTrue(closeAsync.contains("while (!closeFuture.compareAndSet(null, future))")); + assertTrue(closeAsync.contains("return existing;")); + assertTrue(closeAsync.contains("operation.whenComplete(")); + assertFalse(closeAsync.contains("!existing.isDone()")); + + String exclusiveFuture = method(source, "public CompletableFuture withExclusiveControlFuture(Runnable r)"); + assertTrue(exclusiveFuture.contains("J.a(() -> completeExclusiveControlFuture(loadLock, r, future))")); + String exclusiveCompletion = method(source, "static void completeExclusiveControlFuture("); + assertBefore(exclusiveCompletion, "activeGate.releaseExclusive();", "outward.complete(null);"); + assertBefore(exclusiveCompletion, "activeGate.releaseExclusive();", "outward.completeExceptionally(failure);"); + + String baseHeight = method(source, "public int getBaseHeight("); + assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")")); + assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)")); + assertTrue(baseHeight.contains("catch (GenerationSessionException e)")); + + String generation = method(source, "public void generateNoise("); + assertTrue(generation.contains("throw new IllegalStateException")); + assertTrue(generation.contains("engine.acquireGenerationLease(\"bukkit_terrain_stage\")")); + assertTrue(generation.contains("IrisContext.open(engine, lease.sessionId(), null)")); + assertBefore(generation, "engine.acquireGenerationLease(\"bukkit_terrain_stage\")", "blocks.apply()"); + assertFalse(generation.contains("RED_GLAZED_TERRACOTTA")); + } + + @Test + public void targetedPregeneratorShutdownChecksWorldIdentity() throws IOException { + String jobSource = Files.readString(Path.of(System.getProperty("iris.pregeneratorJobSource"))); + String targetedShutdown = method(jobSource, + "public static boolean shutdownInstanceForWorld(String worldIdentity)"); + + assertBefore(targetedShutdown, "inst.targetsWorldIdentity(worldIdentity)", + "shutdownAndWait(inst, WORLD_SHUTDOWN_TIMEOUT_MILLIS)"); + + String waitedShutdown = method(jobSource, + "private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs)"); + assertBefore(waitedShutdown, "inst.worker.interrupt()", "inst.worker.join("); + assertTrue(waitedShutdown.contains("inst.worker.isAlive()")); + + String hooksSource = Files.readString(Path.of(System.getProperty("iris.bukkitEnginePlatformHooksSource"))); + String hookShutdown = method(hooksSource, "public void shutdownPregenerator(Engine engine)"); + assertTrue(hookShutdown.contains("PregeneratorJob.shutdownInstanceForWorld(world.identity());")); + assertFalse(hookShutdown.contains("PregeneratorJob.shutdownInstance();")); + } + + 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); + } + + private static String method(String source, String signature) { + int start = source.indexOf(signature); + assertTrue("Missing source contract signature: " + signature, start >= 0); + int openBrace = source.indexOf('{', start); + assertTrue("Missing source contract method body: " + signature, openBrace >= 0); + int depth = 0; + for (int index = openBrace; index < source.length(); index++) { + char current = source.charAt(index); + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return source.substring(start, index + 1); + } + } + } + throw new IllegalArgumentException("Unclosed source contract method: " + signature); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/runtime/StudioPlayerModeContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/runtime/StudioPlayerModeContractTest.java new file mode 100644 index 000000000..078fd6ed2 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/runtime/StudioPlayerModeContractTest.java @@ -0,0 +1,24 @@ +package art.arcane.iris.core.runtime; + +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 StudioPlayerModeContractTest { + @Test + public void studioEntryKeepsPlayersEligibleForNaturalSpawning() throws IOException { + String plugin = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")).replace("\r\n", "\n"); + String commands = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/commands/CommandStudio.java")).replace("\r\n", "\n"); + + assertFalse(plugin.contains("GameMode.SPECTATOR")); + assertFalse(commands.contains("GameMode.SPECTATOR")); + assertTrue(plugin.contains("GameMode.CREATIVE")); + assertTrue(commands.contains("GameMode.CREATIVE")); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/DatapackStructureScopeSVCTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/DatapackStructureScopeSVCTest.java new file mode 100644 index 000000000..78e4c50ce --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/DatapackStructureScopeSVCTest.java @@ -0,0 +1,42 @@ +package art.arcane.iris.core.service; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.world.WorldInitEvent; +import org.bukkit.event.world.WorldUnloadEvent; +import org.junit.Test; + +import java.lang.reflect.Method; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class DatapackStructureScopeSVCTest { + @Test + public void structureScopeRunsAfterIrisGeneratorInjectionAndBeforeSpawnChunks() throws NoSuchMethodException { + Method handler = DatapackStructureScopeSVC.class.getMethod("onWorldInit", WorldInitEvent.class); + EventHandler annotation = handler.getAnnotation(EventHandler.class); + + assertNotNull(annotation); + assertEquals(EventPriority.HIGHEST, annotation.priority()); + } + + @Test + public void unloadAbandonsRetainedStudioStateBeforeEngineTeardown() throws NoSuchMethodException { + Method handler = DatapackStructureScopeSVC.class.getMethod("onWorldUnload", WorldUnloadEvent.class); + EventHandler annotation = handler.getAnnotation(EventHandler.class); + + assertNotNull(annotation); + assertEquals(EventPriority.LOWEST, annotation.priority()); + } + + @Test + public void emptyScopeStillAppliesToJigsawStudioBootstrap() { + assertTrue(DatapackStructureScopeSVC.shouldApplyScope(true, true, false)); + assertFalse(DatapackStructureScopeSVC.shouldApplyScope(true, false, false)); + assertTrue(DatapackStructureScopeSVC.shouldApplyScope(false, false, false)); + assertTrue(DatapackStructureScopeSVC.shouldApplyScope(true, false, true)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisApiWiringContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisApiWiringContractTest.java new file mode 100644 index 000000000..cfadd6843 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisApiWiringContractTest.java @@ -0,0 +1,195 @@ +package art.arcane.iris.core.service; + +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 IrisApiWiringContractTest { + @Test + public void theTerrainServiceRegistersOnBothRegistriesAndReleasesBoth() throws IOException { + String source = source("iris.terrainSvcSource"); + String onEnable = method(source, "public void onEnable()"); + String onDisable = method(source, "public void onDisable()"); + + assertTrue(onEnable.contains("Bukkit.getServicesManager().register(")); + assertTrue(onEnable.contains("IrisTerrainService.class")); + assertTrue(onEnable.contains("IrisServices.register(IrisTerrainService.class, this)")); + assertTrue(onDisable.contains("Bukkit.getServicesManager().unregister(IrisTerrainService.class, this)")); + assertTrue(onDisable.contains("IrisServices.remove(IrisTerrainService.class)")); + assertBefore(onDisable, "serviceEnabled.set(false)", "Bukkit.getServicesManager().unregister("); + } + + @Test + public void theTerrainServiceNeverForcesEngineInitialisationOrLoadsTheMantle() throws IOException { + String source = source("iris.terrainSvcSource"); + + assertFalse("isIrisWorld must not touch the generator", source.contains("IrisToolbelt")); + assertFalse("isIrisWorld must not touch the generator", source.contains(".touch(")); + assertFalse("terrain queries must not load mantle chunks", source.contains("getMantle()")); + assertFalse("terrain queries must not load mantle chunks", source.contains("getObjectsAt(")); + assertFalse("terrain queries must not load mantle chunks", source.contains("getPOIsAt(")); + assertFalse("terrain queries must not load mantle chunks", source.contains("getCaveOrMantleBiome(")); + assertFalse("terrain queries must never block", source.contains(".join()")); + assertFalse("terrain queries must never block", source.contains("synchronized")); + assertFalse("terrain queries must never block", source.contains("J.sfut(")); + } + + @Test + public void sampleColumnsBoundsTheQueryBeforeItWalksAndTreatsAThrowingSinkAsARefusal() throws IOException { + String sampleColumns = method(source("iris.terrainSvcSource"), + "public boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink)"); + + assertBefore(sampleColumns, "IrisSampleLimits.withinLimits(", "IrisColumnWalk.walk("); + assertBefore(sampleColumns, "IrisColumnWalk.walk(", "catch (Throwable error)"); + assertTrue(sampleColumns.contains("reportSinkFault(world, error)")); + assertTrue(sampleColumns.contains("return false;")); + assertTrue("the walk must abort when the engine closes underneath it", + sampleColumns.contains("engine.isClosed()")); + } + + @Test + public void thePregeneratorDispatchesPhasesFromItsExistingTickAndNeverBlocksOnTheSink() throws IOException { + String source = source("iris.pregeneratorJobSource"); + String dispatch = method(source, "private void dispatchApiPhases(List phases)"); + + assertTrue(dispatch.contains("IrisServices.getOrNull(PregenApiSink.class)")); + assertTrue(dispatch.contains("catch (Throwable error)")); + assertBefore(dispatch, "sink.pregen(phase, progress)", "catch (Throwable error)"); + assertFalse(dispatch.contains(".join()")); + + assertTrue(method(source, "public void onTick(").contains("dispatchApiPhases(apiPhases.onTick(paused()))")); + assertTrue(method(source, "public void onSaving()").contains("dispatchApiPhases(apiPhases.onSaving())")); + assertTrue(method(source, "public void onClose()").contains("dispatchApiPhases(apiPhases.onClose(reachedTotal()))")); + } + + @Test + public void worldPhasesAreFiredOutsideTheRegistrationLock() throws IOException { + String source = source("iris.engineSvcSource"); + + String add = method(source, "private void add(World world)"); + assertBefore(add, "catch (RejectedExecutionException exception)", "phases.ready(world)"); + assertBefore(add, "registered = true;", "phases.ready(world)"); + + String remove = method(source, + "private boolean remove(World world, CompletionStage unloadBoundary)"); + assertBefore(remove, "registered = worlds.remove(world)", "phases.closing(world)"); + assertBefore(remove, "phases.closing(world)", + "deferCloseUntilWorldUnload(world, registered, closing, unloadBoundary)"); + } + + @Test + public void everyRegisteredWorldIsAnnouncedClosingWhenTheServiceShutsDown() throws IOException { + String onDisable = method(source("iris.engineSvcSource"), "public void onDisable()"); + + assertTrue("service shutdown must announce ENGINE_CLOSING for every registered world", + onDisable.contains("phases.closing(teardown.world())")); + assertBefore(onDisable, "worlds.clear()", "phases.closing(teardown.world())"); + assertBefore(onDisable, "phases.closing(teardown.world())", "shutdownAndDrain(activeService)"); + assertBefore(onDisable, "phases.closing(teardown.world())", + "startClose(teardown.registered(), teardown.closing())"); + } + + @Test + public void aReplacedEngineIsAnnouncedClosingBeforeTheRetryReRegistersTheWorld() throws IOException { + String add = method(source("iris.engineSvcSource"), "private void add(World world)"); + + assertBefore(add, "catch (RejectedExecutionException exception)", "phases.closing(world)"); + assertBefore(add, "phases.closing(world)", "retryRegistrationAfterClose(world, retryAfter)"); + assertBefore(add, "phases.closing(world)", "startClose(replaced, replacementClose)"); + } + + @Test + public void aWorldPhaseIsNeverConditionalOnAnotherServicesRegistration() throws IOException { + String source = source("iris.apiEventSvcSource"); + String fire = method(source, "public static void fireWorldPhase(World world, IrisWorldPhase phase)"); + + assertFalse("world lifecycle must not resolve a swappable service to build its payload", + fire.contains("IrisServices")); + assertFalse("world lifecycle must not resolve a swappable service to build its payload", + fire.contains("IrisTerrainService")); + assertTrue(fire.contains("deliver(new IrisWorldEngineEvent(world, phase, describe(world, phase)))")); + + String describe = method(source, "private static IrisWorldInfo describe(World world, IrisWorldPhase phase)"); + assertTrue(describe.contains("IrisWorldInfoFactory.forWorld(world)")); + assertTrue("an undescribable world must be reported, not swallowed", + describe.contains("IrisLogging.reportError(")); + assertTrue("an undescribable world must still deliver the phase", describe.contains("return null;")); + } + + @Test + public void aWorldPhaseRaisedFromAServerThreadIsDeliveredBeforeThatThreadMovesOn() throws IOException { + String deliver = method(source("iris.apiEventSvcSource"), "private static void deliver(Event event)"); + + assertBefore(deliver, "Bukkit.isPrimaryThread()", "Bukkit.getPluginManager().callEvent(event)"); + assertBefore(deliver, "Bukkit.getPluginManager().callEvent(event)", "Iris.callEvent(event)"); + } + + @Test + public void theWorldInfoFactoryNeverForcesEngineInitialisationOrLoadsTheMantle() throws IOException { + String source = source("iris.worldInfoFactorySource"); + + assertFalse("the factory must not touch the generator", source.contains("IrisToolbelt")); + assertFalse("the factory must not touch the generator", source.contains(".touch(")); + assertFalse("the factory must not load mantle chunks", source.contains("getMantle()")); + assertFalse("the factory must never block", source.contains(".join()")); + assertFalse("the factory must never block", source.contains("synchronized")); + assertTrue("the factory must refuse a closed engine", source.contains("engine.isClosed()")); + } + + @Test + public void theHotloadHookStillFiresTheLegacyEventAlongsideTheApiEvent() throws IOException { + String hook = method(source("iris.bukkitEnginePlatformHooksSource"), "public void fireHotloadEvent(Engine engine)"); + + assertBefore(hook, "new IrisEngineHotloadEvent(engine)", + "IrisApiEventSVC.fireWorldPhase(BukkitWorldBinding.world(engine.getWorld()), IrisWorldPhase.ENGINE_HOTLOADED)"); + } + + @Test + public void theEventServiceNeverLetsAThirdPartyFailureEscapeALifecyclePath() throws IOException { + String fire = method(source("iris.apiEventSvcSource"), + "public static void fireWorldPhase(World world, IrisWorldPhase phase)"); + + assertTrue(fire.contains("catch (Throwable error)")); + assertTrue(fire.contains("IrisLogging.reportError(")); + assertFalse(fire.contains("printStackTrace")); + } + + private static String source(String property) throws IOException { + String path = System.getProperty(property); + assertTrue("missing source property " + property, path != null && !path.isBlank()); + return Files.readString(Path.of(path)); + } + + 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); + } + + private static String method(String source, String signature) { + int start = source.indexOf(signature); + assertTrue("Missing source contract signature: " + signature, start >= 0); + int openBrace = source.indexOf('{', start); + assertTrue("Missing source contract method body: " + signature, openBrace >= 0); + int depth = 0; + for (int index = openBrace; index < source.length(); index++) { + char current = source.charAt(index); + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return source.substring(start, index + 1); + } + } + } + throw new IllegalArgumentException("Unclosed source contract method: " + signature); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisEngineLifecycleContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisEngineLifecycleContractTest.java new file mode 100644 index 000000000..f8b0dfe0b --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisEngineLifecycleContractTest.java @@ -0,0 +1,124 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.engine.framework.EngineAssignedWorldManager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.world.WorldUnloadEvent; +import org.junit.Test; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class IrisEngineLifecycleContractTest { + @Test + public void engineServiceExclusivelyOwnsWorldUnload() throws NoSuchMethodException { + assertMonitorUnloadHandler(IrisEngineSVC.class.getMethod("onWorldUnload", WorldUnloadEvent.class)); + for (Method method : EngineAssignedWorldManager.class.getDeclaredMethods()) { + boolean handlesWorldUnload = method.getParameterCount() == 1 + && method.getParameterTypes()[0] == WorldUnloadEvent.class + && method.isAnnotationPresent(EventHandler.class); + assertFalse("EngineAssignedWorldManager must defer world unload ownership to IrisEngineSVC.", handlesWorldUnload); + } + } + + @Test + public void registrationIdentityConflictsByWorldIdentityOrFolder() { + Path sharedFolder = Path.of("build", "worlds", "shared"); + IrisEngineSVC.RegistrationIdentity first = + new IrisEngineSVC.RegistrationIdentity("minecraft:first", sharedFolder); + IrisEngineSVC.RegistrationIdentity sameIdentity = + new IrisEngineSVC.RegistrationIdentity("minecraft:first", Path.of("build", "worlds", "other")); + IrisEngineSVC.RegistrationIdentity sameFolder = + new IrisEngineSVC.RegistrationIdentity("minecraft:other", sharedFolder); + IrisEngineSVC.RegistrationIdentity distinct = + new IrisEngineSVC.RegistrationIdentity("minecraft:distinct", Path.of("build", "worlds", "distinct")); + + assertTrue(first.conflictsWith(sameIdentity)); + assertTrue(first.conflictsWith(sameFolder)); + assertFalse(first.conflictsWith(distinct)); + } + + @Test + public void registrationRetryWaitsAsynchronouslyForClose() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.engineSvcSource"))); + String add = method(source, "private void add(World world)"); + String remove = method(source, "private boolean remove(World world, CompletionStage unloadBoundary)"); + String completeClose = method(source, "private void completeClose("); + String retry = method(source, "private void retryRegistrationAfterClose("); + String invokeClose = method(source, "private CompletableFuture invokeGeneratorClose()"); + + assertBefore(add, "findClosingGenerator(registrationIdentity)", "new Registered("); + assertBefore(remove, "registered.close()", "reserveClose(registered)"); + assertBefore(remove, "reserveClose(registered)", "deferCloseUntilWorldUnload("); + assertBefore(completeClose, "if (failure == null)", "closingGenerators.remove(closing)"); + assertBefore(completeClose, "closingGenerators.remove(closing)", "closing.completion().complete(null)"); + assertBefore(completeClose, "} else {", "closing.completion().completeExceptionally(failure)"); + assertTrue(retry.contains("completion.whenComplete(")); + assertTrue(retry.contains("J.s(() -> add(world), 1);")); + assertBefore(retry, "if (failure != null)", "J.s(() -> add(world), 1);"); + assertFalse(retry.contains("completion.get(")); + assertFalse(retry.contains("completion.join(")); + assertTrue(invokeClose.contains("CompletableFuture.failedFuture(")); + assertTrue(invokeClose.contains("future.whenComplete(")); + } + + @Test + public void worldUnloadDefersGeneratorCloseUntilTheRawBoundaryCompletesTrue() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.engineSvcSource"))); + String handler = method(source, "public void onWorldUnload(WorldUnloadEvent event)"); + String remove = method(source, "private boolean remove(World world, CompletionStage unloadBoundary)"); + String defer = method(source, "private void deferCloseUntilWorldUnload("); + + assertBefore(handler, "WorldUnloadBoundaryRegistry.claim(", "remove(world, unloadBoundary)"); + assertBefore(remove, "registered.close()", "deferCloseUntilWorldUnload("); + assertFalse(remove.contains("startClose(registered, closing)")); + assertTrue(defer.contains("J.sfut(() -> startClose(registered, closing), 1)")); + assertTrue(defer.contains("unloadBoundary.whenComplete(")); + String managedBoundary = defer.substring(defer.indexOf("unloadBoundary.whenComplete(")); + assertBefore(managedBoundary, "failure == null && Boolean.TRUE.equals(unloaded)", + "startClose(registered, closing)"); + assertBefore(managedBoundary, "startClose(registered, closing)", "abandonClose(world, closing)"); + } + + private static void assertMonitorUnloadHandler(Method method) { + EventHandler eventHandler = method.getAnnotation(EventHandler.class); + assertNotNull(eventHandler); + assertEquals(EventPriority.MONITOR, eventHandler.priority()); + assertTrue(eventHandler.ignoreCancelled()); + } + + 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); + } + + private static String method(String source, String signature) { + int start = source.indexOf(signature); + assertTrue("Missing source contract signature: " + signature, start >= 0); + int openBrace = source.indexOf('{', start); + assertTrue("Missing source contract method body: " + signature, openBrace >= 0); + int depth = 0; + for (int index = openBrace; index < source.length(); index++) { + char current = source.charAt(index); + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return source.substring(start, index + 1); + } + } + } + throw new IllegalArgumentException("Unclosed source contract method: " + signature); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisEngineSVCUntrackedUnloadTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisEngineSVCUntrackedUnloadTest.java new file mode 100644 index 000000000..e014d930a --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisEngineSVCUntrackedUnloadTest.java @@ -0,0 +1,59 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.event.world.WorldUnloadEvent; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Multiverse unloads a world through {@code Bukkit.unloadWorld}, which carries no Iris unload boundary. + * {@code IrisEngineSVC} closes the engine of every world it registered, but registration is skipped while + * a previous generator for the same identity is still closing, and an unload landing in that window used + * to leave a live engine bound to a dead world. + */ +public class IrisEngineSVCUntrackedUnloadTest { + @Test + public void externalUnloadClosesAnIrisGeneratorTheServiceNeverRegistered() { + BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class); + when(generator.closeAsync()).thenReturn(CompletableFuture.completedFuture(null)); + + new IrisEngineSVC().onWorldUnload(new WorldUnloadEvent(irisWorld("mvtest", generator))); + + verify(generator).closeAsync(); + } + + @Test + public void externalUnloadOfANonIrisWorldClosesNothing() { + World world = mock(World.class); + when(world.getName()).thenReturn("plots"); + when(world.getKey()).thenReturn(new NamespacedKey("minecraft", "plots")); + + new IrisEngineSVC().onWorldUnload(new WorldUnloadEvent(world)); + } + + @Test + public void externalUnloadDoesNotReenterAGeneratorThatIsAlreadyClosing() { + BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class); + when(generator.isClosing()).thenReturn(true); + + new IrisEngineSVC().onWorldUnload(new WorldUnloadEvent(irisWorld("closing", generator))); + + verify(generator, never()).closeAsync(); + } + + private static World irisWorld(String key, BukkitChunkGenerator generator) { + World world = mock(World.class); + when(world.getName()).thenReturn("world_iris_" + key); + when(world.getKey()).thenReturn(new NamespacedKey("iris", key)); + when(world.getGenerator()).thenReturn(generator); + return world; + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisIntegrationServiceTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisIntegrationServiceTest.java new file mode 100644 index 000000000..3596783f2 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisIntegrationServiceTest.java @@ -0,0 +1,174 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.engine.framework.EngineTelemetrySnapshot; +import art.arcane.volmlib.integration.IntegrationMetricGroup; +import art.arcane.volmlib.integration.IntegrationMetricSample; +import art.arcane.volmlib.integration.IntegrationMetricSchema; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisIntegrationServiceTest { + @Test + public void unavailableTelemetryDoesNotPublishFalseZeroes() { + IrisIntegrationService service = new IrisIntegrationService(() -> IrisTelemetrySnapshot.EMPTY); + Map samples = service.sampleMetrics(Set.of( + IntegrationMetricSchema.IRIS_WORLD_COUNT, + IntegrationMetricSchema.IRIS_PREGEN_QUEUE, + IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS)); + + for (IntegrationMetricSample sample : samples.values()) { + assertFalse(sample.available()); + assertEquals("telemetry-not-ready", sample.message()); + } + assertTrue(service.metricGroups().isEmpty()); + } + + @Test + public void publishesAggregateAndPerWorldMetricsFromOneImmutableSnapshot() { + long now = System.currentTimeMillis(); + EngineTelemetrySnapshot first = world("minecraft:overworld", "world", 12L, 20L, 4D, now); + EngineTelemetrySnapshot second = world("minecraft:the_nether", "world_nether", 8L, 30L, 2D, now); + IrisTelemetrySnapshot snapshot = telemetry( + List.of(first, second), + new IrisTelemetrySnapshot.PregenSnapshot( + true, + first.worldIdentity(), + first.worldName(), + false, + 50D, + 100L, + 200L, + 100L, + 8D, + 2_000L, + 5_000L, + 1L + ), + now + ); + IrisIntegrationService service = new IrisIntegrationService(() -> snapshot); + + Map samples = service.sampleMetrics(Set.of( + IntegrationMetricSchema.IRIS_WORLD_COUNT, + IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS, + IntegrationMetricSchema.IRIS_LOADED_CHUNKS, + IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, + IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, + IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS, + "iris.unsupported" + )); + + assertEquals(2D, value(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT), 0D); + assertEquals(3D, value(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS), 0D); + assertEquals(20D, value(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS), 0D); + assertEquals(50D, value(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL), 0D); + assertEquals(8D, value(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D); + assertEquals(4D, value(samples, IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS), 0D); + assertFalse(samples.get("iris.unsupported").available()); + assertEquals("unsupported-key", samples.get("iris.unsupported").message()); + + List groups = service.metricGroups(); + assertEquals(2, groups.size()); + IntegrationMetricGroup overworld = groups.get(0); + IntegrationMetricGroup nether = groups.get(1); + assertEquals("minecraft:overworld", overworld.scopeId()); + assertEquals(1D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D); + assertEquals(8D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D); + assertEquals("minecraft:the_nether", nether.scopeId()); + assertEquals(0D, value(nether.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D); + assertFalse(nether.samples().get(IntegrationMetricSchema.IRIS_PREGEN_QUEUE).available()); + } + + @Test + public void publishesEveryManagedWorldWithoutAWorldCountCap() { + long now = System.currentTimeMillis(); + List worlds = new ArrayList<>(); + for (int index = 0; index < 48; index++) { + worlds.add(world("iris:world_" + index, "world_" + index, index, index, index, now)); + } + IrisIntegrationService service = new IrisIntegrationService( + () -> telemetry(worlds, IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, now) + ); + + List groups = service.metricGroups(); + + assertEquals(48, groups.size()); + assertEquals(48, groups.stream().map(IntegrationMetricGroup::scopeId).distinct().count()); + } + + private static IrisTelemetrySnapshot telemetry( + List worlds, + IrisTelemetrySnapshot.PregenSnapshot pregenerator, + long now + ) { + IrisTelemetrySnapshot.CacheBucket resource = new IrisTelemetrySnapshot.CacheBucket(2, 10L, 100L); + IrisTelemetrySnapshot.CacheSnapshot caches = new IrisTelemetrySnapshot.CacheSnapshot( + resource, + resource, + IrisTelemetrySnapshot.CacheBucket.EMPTY, + IrisTelemetrySnapshot.CacheBucket.EMPTY, + IrisTelemetrySnapshot.CacheBucket.EMPTY + ); + return new IrisTelemetrySnapshot( + now, + worlds, + EngineTelemetrySnapshot.aggregate(worlds), + 1, + 4, + 0, + 3, + 0.5D, + 0.1D, + caches, + pregenerator + ); + } + + private static EngineTelemetrySnapshot world( + String identity, + String name, + long loadedChunks, + long generatedTotal, + double generationMs, + long now + ) { + return new EngineTelemetrySnapshot( + now, + identity, + name, + "dimension", + true, + false, + false, + false, + loadedChunks, + 3L, + 0.25D, + generatedTotal, + generatedTotal, + 2D, + 4L, + 2, + 1, + 1L, + 4L, + 1L, + 10D, + Map.of("total", generationMs) + ); + } + + private static double value(Map samples, String key) { + IntegrationMetricSample sample = samples.get(key); + assertTrue(sample.available()); + return sample.valueOr(-1D); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisProtocolServiceCapabilitiesTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisProtocolServiceCapabilitiesTest.java new file mode 100644 index 000000000..3b06b9017 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisProtocolServiceCapabilitiesTest.java @@ -0,0 +1,21 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.spi.protocol.IrisProtocol; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.junit.Assert.assertEquals; + +public class IrisProtocolServiceCapabilitiesTest { + @Test + public void advertisesEveryServerFeatureThatSendsProtocolFrames() throws Exception { + Field field = IrisProtocolService.class.getDeclaredField("SERVER_CAPABILITIES"); + field.setAccessible(true); + + assertEquals(IrisProtocol.CAPABILITY_PREGEN + | IrisProtocol.CAPABILITY_VISION + | IrisProtocol.CAPABILITY_CURSOR + | IrisProtocol.CAPABILITY_STUDIO, field.getLong(null)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisTerrainSVCTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisTerrainSVCTest.java new file mode 100644 index 000000000..5e9a2dec1 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisTerrainSVCTest.java @@ -0,0 +1,100 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.terrain.IrisColumnField; +import art.arcane.iris.api.terrain.IrisColumnQuery; +import art.arcane.iris.api.terrain.IrisSurfaceKind; +import art.arcane.iris.api.terrain.IrisTerrainService; +import art.arcane.iris.util.common.plugin.IrisService; +import org.bukkit.World; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.EnumSet; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisTerrainSVCTest { + private static final IrisColumnQuery SMALL = IrisColumnQuery.rect( + 0, 0, 15, 15, 4, EnumSet.of(IrisColumnField.SURFACE_HEIGHT, IrisColumnField.SURFACE_KIND)); + + @Test + public void theServiceImplementsBothContracts() throws NoSuchMethodException { + assertTrue(IrisService.class.isAssignableFrom(IrisTerrainSVC.class)); + assertTrue(IrisTerrainService.class.isAssignableFrom(IrisTerrainSVC.class)); + IrisTerrainSVC.class.getDeclaredConstructor(); + } + + @Test + public void aQueryIsOnlyAnswerableWhileEnabledAndAgainstARealWorld() { + assertTrue(IrisTerrainSVC.answerable(true, true)); + assertFalse("a disabled service must never resolve a generator", + IrisTerrainSVC.answerable(false, true)); + assertFalse(IrisTerrainSVC.answerable(true, false)); + assertFalse(IrisTerrainSVC.answerable(false, false)); + } + + @Test + public void aServiceThatIsNotEnabledAnswersAbsenceInsteadOfThrowing() { + IrisTerrainSVC service = new IrisTerrainSVC(); + + assertFalse(service.isIrisWorld(null)); + assertTrue(service.worldInfo(null).isEmpty()); + assertEquals(OptionalInt.empty(), service.surfaceHeight(null, 0, 0)); + assertEquals(IrisSurfaceKind.UNKNOWN, service.surfaceKind(null, 0, 0)); + assertTrue(service.surfaceBiomeKey(null, 0, 0).isEmpty()); + assertTrue(service.surfaceBiomeName(null, 0, 0).isEmpty()); + assertTrue(service.biomeKey(null, 0, 64, 0).isEmpty()); + assertTrue(service.regionKey(null, 0, 0).isEmpty()); + assertTrue(service.regionName(null, 0, 0).isEmpty()); + } + + @Test + public void theDisplayNameAccessorsReadNamesRatherThanLoadKeys() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.terrainSvcSource"))); + + assertTrue("surfaceBiomeName must read the biome display name", + source.contains("return name(engine.getSurfaceBiome(blockX, blockZ));")); + assertTrue("the biome name helper must read getName()", + source.contains("String name = biome == null ? null : biome.getName();")); + assertTrue("regionName must read the region display name", + source.contains("String name = region == null ? null : region.getName();")); + } + + @Test + public void theServiceExposesDisplayNamesAlongsideLoadKeys() throws NoSuchMethodException { + assertEquals(Optional.class, + IrisTerrainService.class.getMethod("surfaceBiomeName", World.class, int.class, int.class) + .getReturnType()); + assertEquals(Optional.class, + IrisTerrainService.class.getMethod("regionName", World.class, int.class, int.class) + .getReturnType()); + } + + @Test + public void anUnanswerableSampleNeverTouchesTheSink() { + IrisTerrainSVC service = new IrisTerrainSVC(); + AtomicInteger sinkCalls = new AtomicInteger(); + + boolean answered = service.sampleColumns(null, SMALL, + (int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey) + -> sinkCalls.incrementAndGet()); + + assertFalse(answered); + assertEquals(0, sinkCalls.get()); + } + + @Test + public void nullArgumentsAreRefusedRatherThanDereferenced() { + IrisTerrainSVC service = new IrisTerrainSVC(); + + assertFalse(service.sampleColumns(null, null, null)); + assertFalse(service.sampleColumns(null, SMALL, null)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisWorldPhaseLedgerTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisWorldPhaseLedgerTest.java new file mode 100644 index 000000000..0d6573a11 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisWorldPhaseLedgerTest.java @@ -0,0 +1,119 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.world.IrisWorldPhase; +import org.bukkit.World; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; + +public class IrisWorldPhaseLedgerTest { + @Test + public void aWorldIsAnnouncedReadyOnceNoMatterHowOftenRegistrationRuns() { + List fired = new ArrayList<>(); + IrisWorldPhaseLedger ledger = ledger(fired); + World world = world("alpha"); + + ledger.ready(world); + ledger.ready(world); + ledger.ready(world); + + assertEquals(List.of("alpha:ENGINE_READY"), fired); + } + + @Test + public void closingIsNeverAnnouncedForAWorldThatWasNeverAnnouncedReady() { + List fired = new ArrayList<>(); + IrisWorldPhaseLedger ledger = ledger(fired); + World world = world("alpha"); + + ledger.closing(world); + + assertEquals(List.of(), fired); + } + + @Test + public void everyReadyWorldIsAnnouncedClosingExactlyOnce() { + List fired = new ArrayList<>(); + IrisWorldPhaseLedger ledger = ledger(fired); + World world = world("alpha"); + + ledger.ready(world); + ledger.closing(world); + ledger.closing(world); + + assertEquals(List.of("alpha:ENGINE_READY", "alpha:ENGINE_CLOSING"), fired); + } + + @Test + public void anEngineReplacementClosesBeforeItIsAnnouncedReadyAgain() { + List fired = new ArrayList<>(); + IrisWorldPhaseLedger ledger = ledger(fired); + World world = world("alpha"); + + ledger.ready(world); + ledger.closing(world); + ledger.ready(world); + ledger.closing(world); + + assertEquals(List.of( + "alpha:ENGINE_READY", + "alpha:ENGINE_CLOSING", + "alpha:ENGINE_READY", + "alpha:ENGINE_CLOSING"), fired); + } + + @Test + public void worldsAreTrackedIndependently() { + List fired = new ArrayList<>(); + IrisWorldPhaseLedger ledger = ledger(fired); + World first = world("alpha"); + World second = world("beta"); + + ledger.ready(first); + ledger.ready(second); + ledger.closing(first); + ledger.ready(first); + + assertEquals(List.of( + "alpha:ENGINE_READY", + "beta:ENGINE_READY", + "alpha:ENGINE_CLOSING", + "alpha:ENGINE_READY"), fired); + } + + @Test + public void aWorldThatIsNoLongerAddressableIsNeverAnnounced() { + List fired = new ArrayList<>(); + IrisWorldPhaseLedger ledger = ledger(fired); + + ledger.ready(null); + ledger.closing(null); + + assertEquals(List.of(), fired); + } + + private static IrisWorldPhaseLedger ledger(List fired) { + return new IrisWorldPhaseLedger((World world, IrisWorldPhase phase) -> + fired.add(world.getName() + ":" + phase.name())); + } + + private static World world(String name) { + UUID identity = UUID.nameUUIDFromBytes(name.getBytes()); + return (World) Proxy.newProxyInstance( + IrisWorldPhaseLedgerTest.class.getClassLoader(), + new Class[]{World.class}, + (Object proxy, Method method, Object[] arguments) -> switch (method.getName()) { + case "getUID" -> identity; + case "getName", "toString" -> name; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == arguments[0]; + default -> throw new UnsupportedOperationException(method.getName()); + }); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerEventOrderTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerEventOrderTest.java new file mode 100644 index 000000000..6aec41a00 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerEventOrderTest.java @@ -0,0 +1,60 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.service.tree.TreeMarkerTraversal; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerItemHeldEvent; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TreeFellerEventOrderTest { + @Test + public void standaloneIntentPrecedesMonitorFinalization() throws Exception { + Method request = TreeFellerSVC.class.getMethod("requestStandalone", org.bukkit.event.block.BlockBreakEvent.class); + Method finalize = TreeFellerSVC.class.getMethod("finalizeBreak", org.bukkit.event.block.BlockBreakEvent.class); + EventHandler requestHandler = request.getAnnotation(EventHandler.class); + EventHandler finalizeHandler = finalize.getAnnotation(EventHandler.class); + + assertEquals(EventPriority.HIGHEST, requestHandler.priority()); + assertTrue(requestHandler.ignoreCancelled()); + assertEquals(EventPriority.MONITOR, finalizeHandler.priority()); + assertFalse(finalizeHandler.ignoreCancelled()); + } + + @Test + public void incompleteDiscoveryFallsOnlyTheTrigger() { + TreeMarkerTraversal.Position trigger = new TreeMarkerTraversal.Position(4, 80, -2); + TreeMarkerTraversal.Discovery incomplete = new TreeMarkerTraversal.Discovery( + List.of(trigger, new TreeMarkerTraversal.Position(4, 81, -2)), + false + ); + + assertEquals(List.of(trigger), TreeFellingRunner.positionsForFelling(incomplete, trigger)); + } + + @Test + public void playerControlChangesHaltRunsAtMonitor() throws Exception { + Method sneak = TreeFellerSVC.class.getMethod("haltWhenSneakingStops", PlayerToggleSneakEvent.class); + Method held = TreeFellerSVC.class.getMethod("haltWhenHeldSlotChanges", PlayerItemHeldEvent.class); + Method hands = TreeFellerSVC.class.getMethod("haltWhenHandsSwap", PlayerSwapHandItemsEvent.class); + + assertMonitorCancellationHandler(sneak); + assertMonitorCancellationHandler(held); + assertMonitorCancellationHandler(hands); + } + + private void assertMonitorCancellationHandler(Method method) { + EventHandler handler = method.getAnnotation(EventHandler.class); + + assertEquals(EventPriority.MONITOR, handler.priority()); + assertTrue(handler.ignoreCancelled()); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerPresentationTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerPresentationTest.java new file mode 100644 index 000000000..3126da560 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerPresentationTest.java @@ -0,0 +1,27 @@ +package art.arcane.iris.core.service; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TreeFellerPresentationTest { + @Test + public void pulseSizeKeepsSmallTreesReadableAndLargeTreesBounded() { + assertEquals(4, TreeFellerPresentation.blocksPerPulse(1)); + assertEquals(4, TreeFellerPresentation.blocksPerPulse(240)); + assertEquals(10, TreeFellerPresentation.blocksPerPulse(600)); + assertEquals(64, TreeFellerPresentation.blocksPerPulse(100_000)); + } + + @Test + public void effectSamplingStaysBoundedPerPulse() { + assertEquals(1, TreeFellerPresentation.effectStride(4)); + assertEquals(1, TreeFellerPresentation.effectStride(16)); + assertEquals(2, TreeFellerPresentation.effectStride(17)); + assertEquals(2, TreeFellerPresentation.effectStride(31)); + assertEquals(2, TreeFellerPresentation.effectStride(32)); + assertEquals(3, TreeFellerPresentation.effectStride(33)); + assertEquals(4, TreeFellerPresentation.effectStride(64)); + } + +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellingRunnerPacingTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellingRunnerPacingTest.java new file mode 100644 index 000000000..a3ab672e7 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellingRunnerPacingTest.java @@ -0,0 +1,16 @@ +package art.arcane.iris.core.service; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TreeFellingRunnerPacingTest { + @Test + public void memberTasksRunInlineOnlyOnNonFoliaMainThread() { + assertTrue(TreeFellingRunner.shouldRunInline(false, true)); + assertFalse(TreeFellingRunner.shouldRunInline(false, false)); + assertFalse(TreeFellingRunner.shouldRunInline(true, true)); + assertFalse(TreeFellingRunner.shouldRunInline(true, false)); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisApiFaultGuardTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisApiFaultGuardTest.java new file mode 100644 index 000000000..8da069dd6 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisApiFaultGuardTest.java @@ -0,0 +1,52 @@ +package art.arcane.iris.core.service.terrain; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisApiFaultGuardTest { + @Test + public void theFirstFaultIsAlwaysReported() { + IrisApiFaultGuard guard = new IrisApiFaultGuard(60_000L); + + assertTrue(guard.record(0L)); + assertEquals(1L, guard.faults()); + } + + @Test + public void faultsInsideTheIntervalAreCountedButNotReported() { + IrisApiFaultGuard guard = new IrisApiFaultGuard(60_000L); + guard.record(1_000L); + + assertFalse(guard.record(2_000L)); + assertFalse(guard.record(60_999L)); + assertEquals(3L, guard.faults()); + } + + @Test + public void reportingResumesOnceTheIntervalElapses() { + IrisApiFaultGuard guard = new IrisApiFaultGuard(60_000L); + guard.record(1_000L); + guard.record(2_000L); + + assertTrue(guard.record(61_000L)); + assertFalse(guard.record(61_001L)); + assertEquals(4L, guard.faults()); + } + + @Test + public void aZeroIntervalReportsEveryFault() { + IrisApiFaultGuard guard = new IrisApiFaultGuard(0L); + + assertTrue(guard.record(5L)); + assertTrue(guard.record(5L)); + assertEquals(2L, guard.faults()); + } + + @Test(expected = IllegalArgumentException.class) + public void aNegativeIntervalIsRejected() { + new IrisApiFaultGuard(-1L); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisColumnWalkTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisColumnWalkTest.java new file mode 100644 index 000000000..25794ecb9 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisColumnWalkTest.java @@ -0,0 +1,87 @@ +package art.arcane.iris.core.service.terrain; + +import art.arcane.iris.api.terrain.IrisColumnField; +import art.arcane.iris.api.terrain.IrisColumnQuery; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class IrisColumnWalkTest { + private static final EnumSet ANY = EnumSet.of(IrisColumnField.SURFACE_HEIGHT); + + @Test + public void visitsExactlyTheAdvertisedColumnCount() { + IrisColumnQuery query = IrisColumnQuery.rect(-40, -40, 39, 39, 8, ANY); + List visited = new ArrayList<>(); + + long count = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> visited.add(new long[]{blockX, blockZ})); + + assertEquals(query.columnCount(), count); + assertEquals(query.columnCount(), visited.size()); + } + + @Test + public void everyColumnIsStrideAlignedAndInsideTheRect() { + IrisColumnQuery query = IrisColumnQuery.rect(-37, -21, 60, 44, 7, ANY); + + IrisColumnWalk.walk(query, (int blockX, int blockZ) -> { + assertTrue(blockX >= query.minBlockX() && blockX <= query.maxBlockX()); + assertTrue(blockZ >= query.minBlockZ() && blockZ <= query.maxBlockZ()); + assertEquals(0, (blockX - query.minBlockX()) % query.strideBlocks()); + assertEquals(0, (blockZ - query.minBlockZ()) % query.strideBlocks()); + return true; + }); + } + + @Test + public void everyChunkIsVisitedOnceAsAContiguousRun() { + IrisColumnQuery query = IrisColumnQuery.rect(-33, -33, 47, 47, 4, ANY); + List chunkRuns = new ArrayList<>(); + + IrisColumnWalk.walk(query, (int blockX, int blockZ) -> { + long chunkKey = (((long) (blockX >> 4)) << 32) ^ ((blockZ >> 4) & 0xFFFFFFFFL); + if (chunkRuns.isEmpty() || chunkRuns.get(chunkRuns.size() - 1) != chunkKey) { + chunkRuns.add(chunkKey); + } + return true; + }); + + Set distinct = new LinkedHashSet<>(chunkRuns); + assertEquals("a chunk must never be revisited after the walk leaves it", + distinct.size(), chunkRuns.size()); + } + + @Test + public void aRefusingVisitorStopsTheWalkAndReportsWhatItSaw() { + IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 63, 63, 1, ANY); + int[] seen = new int[1]; + + long count = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> { + seen[0]++; + return seen[0] < 10; + }); + + assertEquals(9L, count); + assertEquals(10, seen[0]); + assertTrue(count < query.columnCount()); + } + + @Test + public void aSingleColumnRectVisitsExactlyThatColumn() { + IrisColumnQuery query = IrisColumnQuery.rect(-1, -1, -1, -1, 16, ANY); + List visited = new ArrayList<>(); + + long count = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> visited.add(new long[]{blockX, blockZ})); + + assertEquals(1L, count); + assertEquals(-1L, visited.get(0)[0]); + assertEquals(-1L, visited.get(0)[1]); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSampleLimitsTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSampleLimitsTest.java new file mode 100644 index 000000000..13bd2029a --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSampleLimitsTest.java @@ -0,0 +1,58 @@ +package art.arcane.iris.core.service.terrain; + +import art.arcane.iris.api.terrain.IrisColumnField; +import art.arcane.iris.api.terrain.IrisColumnQuery; +import org.junit.Test; + +import java.util.EnumSet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisSampleLimitsTest { + @Test + public void chunkCapIsAShareOfTheNoiseCacheWithAFloor() { + assertEquals(256, IrisSampleLimits.maxChunks(1_024)); + assertEquals(IrisSampleLimits.MINIMUM_CHUNKS, IrisSampleLimits.maxChunks(0)); + assertEquals(IrisSampleLimits.MINIMUM_CHUNKS, IrisSampleLimits.maxChunks(16)); + } + + @Test + public void columnCapIsDerivedFromTheChunkCapAndDoesNotOverflow() { + assertEquals(65_536, IrisSampleLimits.maxColumns(1_024)); + assertTrue(IrisSampleLimits.maxColumns(Integer.MAX_VALUE) > 0); + } + + @Test + public void strideCannotSmuggleAQueryPastTheChunkCap() { + IrisColumnQuery smuggled = IrisColumnQuery.rect( + 0, 0, 6399, 6399, 64, EnumSet.of(IrisColumnField.SURFACE_KIND)); + + int maxColumns = IrisSampleLimits.maxColumns(1_024); + int maxChunks = IrisSampleLimits.maxChunks(1_024); + + assertTrue("this query must pass a column-only cap", smuggled.columnCount() <= maxColumns); + assertFalse("but it must be refused on chunk span", + IrisSampleLimits.withinLimits(smuggled, maxColumns, maxChunks)); + } + + @Test + public void aQueryInsideBothCapsIsAccepted() { + IrisColumnQuery accepted = IrisColumnQuery.rect( + 0, 0, 255, 255, 4, EnumSet.of(IrisColumnField.SURFACE_KIND)); + + assertTrue(IrisSampleLimits.withinLimits( + accepted, IrisSampleLimits.maxColumns(1_024), IrisSampleLimits.maxChunks(1_024))); + } + + @Test + public void aDenseQueryOverManyColumnsIsRefused() { + IrisColumnQuery dense = IrisColumnQuery.rect( + 0, 0, 1023, 1023, 1, EnumSet.of(IrisColumnField.SURFACE_HEIGHT)); + + assertTrue(dense.columnCount() > IrisSampleLimits.maxColumns(1_024)); + assertFalse(IrisSampleLimits.withinLimits( + dense, IrisSampleLimits.maxColumns(1_024), IrisSampleLimits.maxChunks(1_024))); + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifierTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifierTest.java new file mode 100644 index 000000000..a9368289b --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifierTest.java @@ -0,0 +1,61 @@ +package art.arcane.iris.core.service.terrain; + +import art.arcane.iris.api.terrain.IrisSurfaceKind; +import art.arcane.iris.engine.object.InferredType; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisSurfaceClassifierTest { + private static final int FLUID = 127; + + @Test + public void columnAtOrBelowWorldMinimumIsVoid() { + assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(0, FLUID, InferredType.LAND)); + assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(-8, FLUID, InferredType.SEA)); + } + + @Test + public void oceanIsExactlyTheEngineUnderwaterPredicate() { + assertEquals(IrisSurfaceKind.OCEAN, IrisSurfaceClassifier.classify(FLUID, FLUID, InferredType.LAND)); + assertEquals(IrisSurfaceKind.OCEAN, IrisSurfaceClassifier.classify(FLUID - 1, FLUID, InferredType.LAND)); + assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(FLUID + 1, FLUID, InferredType.LAND)); + } + + @Test + public void shoreOnlyAppliesAboveTheFluidLine() { + assertEquals(IrisSurfaceKind.SHORE, IrisSurfaceClassifier.classify(FLUID + 1, FLUID, InferredType.SHORE)); + assertEquals(IrisSurfaceKind.OCEAN, IrisSurfaceClassifier.classify(FLUID, FLUID, InferredType.SHORE)); + } + + @Test + public void caveAndAbsentTypesFallBackToLandAboveWater() { + assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(FLUID + 10, FLUID, InferredType.CAVE)); + assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(FLUID + 10, FLUID, InferredType.SEA)); + assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(FLUID + 10, FLUID, null)); + } + + @Test + public void biomeIsOnlyRequiredWhenTheAnswerCanDependOnIt() { + assertFalse(IrisSurfaceClassifier.requiresSurfaceBiome(0, FLUID)); + assertFalse(IrisSurfaceClassifier.requiresSurfaceBiome(FLUID, FLUID)); + assertTrue(IrisSurfaceClassifier.requiresSurfaceBiome(FLUID + 1, FLUID)); + } + + @Test + public void whenBiomeIsNotRequiredEveryInferredTypeYieldsTheSameKind() { + for (int surface = -4; surface <= FLUID; surface++) { + if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, FLUID)) { + continue; + } + + IrisSurfaceKind expected = IrisSurfaceClassifier.classify(surface, FLUID, null); + for (InferredType inferredType : InferredType.values()) { + assertEquals("surface=" + surface + " type=" + inferredType, + expected, IrisSurfaceClassifier.classify(surface, FLUID, inferredType)); + } + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactoryTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactoryTest.java new file mode 100644 index 000000000..9227ee86e --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactoryTest.java @@ -0,0 +1,52 @@ +package art.arcane.iris.core.service.terrain; + +import art.arcane.iris.api.terrain.IrisWorldInfo; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class IrisWorldInfoFactoryTest { + @Test + public void aDescribableWorldReportsFluidHeightInAbsoluteWorldY() { + IrisWorldInfo info = IrisWorldInfoFactory.build( + "overworld", "minecraft:world", 42L, -64, 320, 63, false); + + assertNotNull(info); + assertEquals("overworld", info.dimensionKey()); + assertEquals("minecraft:world", info.worldIdentity()); + assertEquals(42L, info.seed()); + assertEquals(-64, info.minHeight()); + assertEquals(320, info.maxHeight()); + assertEquals(-1, info.fluidHeight()); + assertEquals(384, info.height()); + assertFalse(info.studio()); + } + + @Test + public void aStudioWorldIsReportedAsStudio() { + IrisWorldInfo info = IrisWorldInfoFactory.build( + "overworld", "minecraft:studio", 1L, 0, 256, 63, true); + + assertNotNull(info); + assertTrue(info.studio()); + assertEquals(63, info.fluidHeight()); + } + + @Test + public void anIndescribableWorldIsAbsentRatherThanHalfBuilt() { + assertNull(IrisWorldInfoFactory.build(null, "minecraft:world", 1L, 0, 256, 63, false)); + assertNull(IrisWorldInfoFactory.build("overworld", null, 1L, 0, 256, 63, false)); + assertNull(IrisWorldInfoFactory.build("overworld", "minecraft:world", 1L, 256, 256, 63, false)); + assertNull(IrisWorldInfoFactory.build("overworld", "minecraft:world", 1L, 320, 0, 63, false)); + } + + @Test + public void anAbsentGeneratorOrWorldIsDescribedAsNothing() { + assertNull(IrisWorldInfoFactory.from(null)); + assertNull(IrisWorldInfoFactory.forWorld(null)); + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/ClientPacketSink.java b/adapters/client-common/src/main/java/art/arcane/iris/client/ClientPacketSink.java new file mode 100644 index 000000000..826f32383 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/ClientPacketSink.java @@ -0,0 +1,6 @@ +package art.arcane.iris.client; + +@FunctionalInterface +public interface ClientPacketSink { + void send(byte[] frame); +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClient.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClient.java new file mode 100644 index 000000000..eb7c3168e --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClient.java @@ -0,0 +1,170 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisMessageCodec; +import art.arcane.iris.spi.protocol.IrisProtocol; +import art.arcane.iris.spi.protocol.ProtocolException; +import net.minecraft.resources.Identifier; + +public final class IrisClient { + public static final Identifier HUD_ELEMENT_ID = Identifier.fromNamespaceAndPath("irisworldgen", "pregen_hud"); + public static final Identifier KEYBIND_CATEGORY_ID = Identifier.fromNamespaceAndPath("irisworldgen", "iris"); + public static final String KEYBIND_TOGGLE_HUD = "key.irisworldgen.toggle_pregen_hud"; + public static final String KEYBIND_OPEN_MAP = "key.irisworldgen.open_vision_map"; + public static final String KEYBIND_TOGGLE_WHAT = "key.irisworldgen.toggle_what_overlay"; + + private static final IrisClientSession SESSION = new IrisClientSession(); + private static final IrisClientPregenState PREGEN = new IrisClientPregenState(); + private static final IrisClientDimension DIMENSION = new IrisClientDimension(); + private static final IrisClientTileCache TILES = new IrisClientTileCache(IrisClient::sendFrame, System::currentTimeMillis); + private static final IrisClientCursor CURSOR = new IrisClientCursor(IrisClient::sendFrame, System::currentTimeMillis); + private static final IrisClientMarkers MARKERS = new IrisClientMarkers(); + private static final IrisClientRegionMap REGIONS = new IrisClientRegionMap(); + private static final IrisClientToasts TOASTS = new IrisClientToasts(); + private static volatile ClientPacketSink sink; + private static volatile boolean hudVisible = true; + private static volatile boolean whatVisible = false; + + private IrisClient() { + } + + public static IrisClientSession session() { + return SESSION; + } + + public static IrisClientPregenState pregen() { + return PREGEN; + } + + public static IrisClientDimension dimension() { + return DIMENSION; + } + + public static IrisClientTileCache tiles() { + return TILES; + } + + public static IrisClientCursor cursor() { + return CURSOR; + } + + public static IrisClientMarkers markers() { + return MARKERS; + } + + public static IrisClientRegionMap regions() { + return REGIONS; + } + + public static IrisClientToasts toasts() { + return TOASTS; + } + + public static boolean hudVisible() { + return hudVisible; + } + + public static void toggleHud() { + hudVisible = !hudVisible; + } + + public static boolean whatVisible() { + return whatVisible; + } + + public static void toggleWhat() { + whatVisible = !whatVisible; + } + + public static boolean visionAvailable() { + return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_VISION) != 0L; + } + + public static boolean cursorAvailable() { + return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_CURSOR) != 0L; + } + + public static void bindSender(ClientPacketSink boundSink) { + sink = boundSink; + SESSION.bind(boundSink); + } + + public static void sendFrame(byte[] frame) { + ClientPacketSink activeSink = sink; + if (activeSink != null) { + activeSink.send(frame); + } + } + + public static void onWorldJoin() { + clearWorldState(); + SESSION.sendHello(); + } + + public static void onDisconnect() { + SESSION.reset(); + clearWorldState(); + } + + public static void tick() { + SESSION.tick(); + } + + private static void clearWorldState() { + PREGEN.clear(); + DIMENSION.clear(); + TILES.clear(); + CURSOR.clear(); + MARKERS.clear(); + REGIONS.clear(); + TOASTS.clear(); + } + + public static void onInbound(byte[] frame) { + if (frame == null) { + return; + } + IrisMessage message; + try { + message = IrisMessageCodec.decode(frame); + } catch (ProtocolException rejected) { + return; + } + if (message == null) { + return; + } + route(message); + } + + private static void route(IrisMessage message) { + switch (message) { + case IrisMessage.ServerHello serverHello -> SESSION.onServerHello(serverHello); + case IrisMessage.PregenProgress progress -> PREGEN.onProgress(progress); + case IrisMessage.PregenEnd end -> onPregenEnd(end.jobId()); + case IrisMessage.DimensionStatus status -> onDimensionStatus(status); + case IrisMessage.CursorInfo cursorInfo -> CURSOR.onCursorInfo(cursorInfo); + case IrisMessage.VisionTile visionTile -> TILES.onVisionTile(visionTile); + case IrisMessage.VisionMarkers visionMarkers -> MARKERS.onMarkers(visionMarkers); + case IrisMessage.PregenRegionDelta delta -> REGIONS.onDelta(delta, PREGEN.activeJobId()); + case IrisMessage.StudioHotload hotload -> TOASTS.enqueueHotload(hotload.packKey(), hotload.changedFiles(), hotload.failed(), hotload.message()); + case IrisMessage.Toast toast -> TOASTS.enqueue(toast.kind(), toast.title(), toast.body()); + default -> { + } + } + } + + private static void onPregenEnd(long jobId) { + PREGEN.onEnd(jobId); + REGIONS.onEnd(jobId); + } + + private static void onDimensionStatus(IrisMessage.DimensionStatus status) { + boolean changed = DIMENSION.onDimensionStatus(status); + if (changed) { + TILES.clear(); + MARKERS.clear(); + CURSOR.clear(); + REGIONS.clear(); + } + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientCursor.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientCursor.java new file mode 100644 index 000000000..5cc00d789 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientCursor.java @@ -0,0 +1,59 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisMessageCodec; + +import java.util.function.LongSupplier; + +public final class IrisClientCursor { + private static final long MIN_REQUEST_INTERVAL_MILLIS = 500L; + private static final long SAME_POSITION_REFRESH_MILLIS = 2_000L; + + private final ClientPacketSink sink; + private final LongSupplier clock; + private volatile IrisMessage.CursorInfo latest; + private int lastRequestedX; + private int lastRequestedZ; + private boolean requested; + private long lastRequestMillis; + + public IrisClientCursor(ClientPacketSink sink, LongSupplier clock) { + this.sink = sink; + this.clock = clock; + this.latest = null; + this.lastRequestedX = 0; + this.lastRequestedZ = 0; + this.requested = false; + this.lastRequestMillis = 0L; + } + + public synchronized void requestFor(int blockX, int blockZ) { + long now = clock.getAsLong(); + boolean samePosition = requested && blockX == lastRequestedX && blockZ == lastRequestedZ; + if (samePosition && now - lastRequestMillis < SAME_POSITION_REFRESH_MILLIS) { + return; + } + if (now - lastRequestMillis < MIN_REQUEST_INTERVAL_MILLIS) { + return; + } + sink.send(IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(blockX, blockZ))); + lastRequestedX = blockX; + lastRequestedZ = blockZ; + requested = true; + lastRequestMillis = now; + } + + public void onCursorInfo(IrisMessage.CursorInfo info) { + latest = info; + } + + public IrisMessage.CursorInfo latest() { + return latest; + } + + public synchronized void clear() { + latest = null; + requested = false; + lastRequestMillis = 0L; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientDimension.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientDimension.java new file mode 100644 index 000000000..2ff5f3bcb --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientDimension.java @@ -0,0 +1,29 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; + +public final class IrisClientDimension { + private volatile IrisMessage.DimensionStatus status; + + public boolean onDimensionStatus(IrisMessage.DimensionStatus incoming) { + IrisMessage.DimensionStatus previous = status; + status = incoming; + if (previous == null) { + return true; + } + return !previous.equals(incoming); + } + + public IrisMessage.DimensionStatus status() { + return status; + } + + public boolean irisWorld() { + IrisMessage.DimensionStatus current = status; + return current != null && current.irisWorld(); + } + + public void clear() { + status = null; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientHud.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientHud.java new file mode 100644 index 000000000..1df3a5c30 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientHud.java @@ -0,0 +1,28 @@ +package art.arcane.iris.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; + +/** + * CLIENT DIST ONLY. References net.minecraft.client and must never be reachable from + * art.arcane.iris.modded or art.arcane.iris.nativegen. ModdedClientPackageIsolationTest enforces that + * direction; there is no @Environment annotation because net.fabricmc.api is not on the Forge or NeoForge + * compile classpath and this source set builds for all three loaders. + */ +public final class IrisClientHud { + private IrisClientHud() { + } + + /** + * Per client tick, independent of the HUD layer. Toasts are pumped here rather than from + * {@link #render(GuiGraphicsExtractor)} because the whole layered HUD draw is skipped while hideGui (F1) + * is on, which would silently strand every queued toast until the player pressed F1 again. + */ + public static void tick() { + IrisToastPresenter.pump(); + } + + public static void render(GuiGraphicsExtractor graphics) { + IrisPregenHud.render(graphics); + IrisWhatOverlay.render(graphics); + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientKeybinds.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientKeybinds.java new file mode 100644 index 000000000..e50cfbe09 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientKeybinds.java @@ -0,0 +1,45 @@ +package art.arcane.iris.client; + +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import org.lwjgl.glfw.GLFW; + +/** + * CLIENT DIST ONLY. Static KeyMapping fields plus LWJGL constants; loading this on a dedicated server is a + * NoClassDefFoundError. Reachable only from the per-loader client shims, which are Dist.CLIENT gated. + * ModdedClientPackageIsolationTest enforces that no modded or nativegen class reaches it. No @Environment + * annotation: net.fabricmc.api is absent from the Forge and NeoForge compile classpath. + */ +public final class IrisClientKeybinds { + private static final KeyMapping.Category CATEGORY = KeyMapping.Category.register(IrisClient.KEYBIND_CATEGORY_ID); + public static final KeyMapping TOGGLE_HUD = new KeyMapping(IrisClient.KEYBIND_TOGGLE_HUD, GLFW.GLFW_KEY_H, CATEGORY); + public static final KeyMapping OPEN_MAP = new KeyMapping(IrisClient.KEYBIND_OPEN_MAP, GLFW.GLFW_KEY_M, CATEGORY); + public static final KeyMapping TOGGLE_WHAT = new KeyMapping(IrisClient.KEYBIND_TOGGLE_WHAT, GLFW.GLFW_KEY_J, CATEGORY); + + private IrisClientKeybinds() { + } + + public static KeyMapping.Category category() { + return CATEGORY; + } + + public static void pollToggle() { + while (TOGGLE_HUD.consumeClick()) { + IrisClient.toggleHud(); + } + while (TOGGLE_WHAT.consumeClick()) { + IrisClient.toggleWhat(); + } + while (OPEN_MAP.consumeClick()) { + openVisionScreen(); + } + } + + private static void openVisionScreen() { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft == null || minecraft.level == null || minecraft.player == null) { + return; + } + minecraft.setScreenAndShow(new IrisVisionScreen()); + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientMarkers.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientMarkers.java new file mode 100644 index 000000000..6582cbbaf --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientMarkers.java @@ -0,0 +1,42 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Marker overlays keyed by tile, straight off the wire. Access-ordered and capped: the server decides how many + * distinct tiles it sends markers for, so an unbounded map here is a remote memory dial. + */ +public final class IrisClientMarkers { + static final int MAX_TILES = 64; + + private final LinkedHashMap> byTile; + + public IrisClientMarkers() { + this.byTile = new LinkedHashMap<>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > MAX_TILES; + } + }; + } + + public synchronized void onMarkers(IrisMessage.VisionMarkers markers) { + byTile.put(new IrisTileKey(markers.tileX(), markers.tileZ(), markers.zoomLevel()), List.copyOf(markers.markers())); + } + + public synchronized List forTile(IrisTileKey key) { + return byTile.get(key); + } + + public synchronized int trackedTiles() { + return byTile.size(); + } + + public synchronized void clear() { + byTile.clear(); + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientPregenState.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientPregenState.java new file mode 100644 index 000000000..6e5171f16 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientPregenState.java @@ -0,0 +1,100 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.LongSupplier; + +/** + * Pregeneration progress as last seen on the wire. Capped by job count, and each entry carries the receive + * time so the HUD can tell a live job from a server that stopped sending - progress frames only arrive while a + * job ticks, so a stalled or crashed job otherwise leaves a frozen bar on screen forever. + */ +public final class IrisClientPregenState { + /** Beyond this the panel reads as unreliable and the HUD mutes it. */ + public static final long STALE_AFTER_MILLIS = 5_000L; + /** Beyond this the HUD stops drawing the panel at all. */ + public static final long EXPIRE_AFTER_MILLIS = 30_000L; + static final int MAX_JOBS = 32; + + private final LongSupplier clock; + private final LinkedHashMap jobs; + private Long activeJobId; + + public IrisClientPregenState() { + this(System::currentTimeMillis); + } + + IrisClientPregenState(LongSupplier clock) { + this.clock = clock; + this.jobs = new LinkedHashMap<>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_JOBS; + } + }; + this.activeJobId = null; + } + + public synchronized void onProgress(IrisMessage.PregenProgress progress) { + jobs.put(progress.jobId(), new Entry(progress, clock.getAsLong())); + activeJobId = progress.jobId(); + } + + public synchronized void onEnd(long jobId) { + jobs.remove(jobId); + Long current = activeJobId; + if (current != null && current == jobId) { + // Promote nothing: the server runs a single live job, so any remaining entry is + // dead or orphaned — and the access-ordered map's head is the STALEST entry, + // which is what the HUD used to promote. + activeJobId = null; + } + } + + public synchronized IrisMessage.PregenProgress active() { + Entry entry = activeEntry(); + return entry == null ? null : entry.progress(); + } + + /** Millis since the active job last reported, or -1 when there is no active job. */ + public synchronized long activeAgeMillis() { + Entry entry = activeEntry(); + return entry == null ? -1L : Math.max(0L, clock.getAsLong() - entry.receivedAtMillis()); + } + + public synchronized boolean activeStale() { + long age = activeAgeMillis(); + return age >= STALE_AFTER_MILLIS; + } + + public synchronized boolean activeExpired() { + long age = activeAgeMillis(); + return age >= EXPIRE_AFTER_MILLIS; + } + + public synchronized Long activeJobId() { + return activeJobId; + } + + public synchronized int trackedJobs() { + return jobs.size(); + } + + public synchronized void clear() { + jobs.clear(); + activeJobId = null; + } + + private Entry activeEntry() { + Long current = activeJobId; + if (current == null) { + return null; + } + return jobs.get(current); + } + + private record Entry(IrisMessage.PregenProgress progress, long receivedAtMillis) { + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientRegionMap.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientRegionMap.java new file mode 100644 index 000000000..980677403 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientRegionMap.java @@ -0,0 +1,118 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class IrisClientRegionMap { + private static final int MAX_REGIONS = 4096; + + private final ConcurrentHashMap states; + private volatile boolean tracking; + private volatile long jobId; + private volatile Bounds bounds; + + public IrisClientRegionMap() { + this.states = new ConcurrentHashMap<>(); + this.tracking = false; + this.jobId = 0L; + this.bounds = null; + } + + public void onDelta(IrisMessage.PregenRegionDelta delta, Long activeJobId) { + if (activeJobId == null) { + return; + } + long active = activeJobId; + if (delta.jobId() != active) { + return; + } + if (!tracking || jobId != delta.jobId()) { + resetTo(delta.jobId()); + } + record(delta.regionX(), delta.regionZ(), delta.state()); + } + + public void onEnd(long endedJobId) { + if (tracking && jobId == endedJobId) { + clear(); + } + } + + public boolean hasData() { + return tracking && !states.isEmpty(); + } + + public Bounds bounds() { + return bounds; + } + + public void forEachCell(CellConsumer consumer) { + for (Map.Entry entry : states.entrySet()) { + long packed = entry.getKey(); + consumer.accept(regionX(packed), regionZ(packed), entry.getValue()); + } + } + + public void clear() { + states.clear(); + bounds = null; + tracking = false; + jobId = 0L; + } + + private void resetTo(long newJobId) { + states.clear(); + bounds = null; + jobId = newJobId; + tracking = true; + } + + private void record(int regionX, int regionZ, int state) { + long packed = pack(regionX, regionZ); + if (!states.containsKey(packed)) { + if (states.size() >= MAX_REGIONS) { + return; + } + Bounds current = bounds; + bounds = current == null ? new Bounds(regionX, regionZ, regionX, regionZ) : current.union(regionX, regionZ); + } + states.put(packed, state); + } + + private static long pack(int regionX, int regionZ) { + return ((long) regionX << 32) | (regionZ & 0xFFFFFFFFL); + } + + private static int regionX(long packed) { + return (int) (packed >> 32); + } + + private static int regionZ(long packed) { + return (int) packed; + } + + @FunctionalInterface + public interface CellConsumer { + void accept(int regionX, int regionZ, int state); + } + + public record Bounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + public Bounds union(int regionX, int regionZ) { + return new Bounds( + Math.min(minRegionX, regionX), + Math.min(minRegionZ, regionZ), + Math.max(maxRegionX, regionX), + Math.max(maxRegionZ, regionZ)); + } + + public int regionsWide() { + return maxRegionX - minRegionX + 1; + } + + public int regionsTall() { + return maxRegionZ - minRegionZ + 1; + } + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientSession.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientSession.java new file mode 100644 index 000000000..296529685 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientSession.java @@ -0,0 +1,133 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisMessageCodec; +import art.arcane.iris.spi.protocol.IrisProtocol; + +import java.util.function.LongSupplier; + +public final class IrisClientSession { + private static final long CLIENT_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR | IrisProtocol.CAPABILITY_STUDIO; + private static final long HELLO_RETRY_MILLIS = 2_000L; + private static final int MAX_HELLO_ATTEMPTS = 5; + + private final LongSupplier clock; + private volatile State state; + private volatile long serverCapabilities; + private volatile int serverProtocolVersion; + private volatile boolean irisActive; + private volatile String serverBrand; + private volatile ClientPacketSink sink; + private volatile long nextHelloAt; + private volatile int helloAttempts; + + public IrisClientSession() { + this(System::currentTimeMillis); + } + + IrisClientSession(LongSupplier clock) { + this.clock = clock; + this.state = State.IDLE; + this.serverCapabilities = 0L; + this.serverProtocolVersion = 0; + this.irisActive = false; + this.serverBrand = ""; + this.sink = null; + this.nextHelloAt = Long.MAX_VALUE; + this.helloAttempts = 0; + } + + public void bind(ClientPacketSink boundSink) { + this.sink = boundSink; + } + + public State state() { + return state; + } + + public boolean isReady() { + return state == State.READY; + } + + public boolean irisActive() { + return irisActive; + } + + public long serverCapabilities() { + return serverCapabilities; + } + + /** The version the server reported, retained even on a mismatch so the UI can name it. 0 before any reply. */ + public int serverProtocolVersion() { + return serverProtocolVersion; + } + + public String serverBrand() { + return serverBrand; + } + + public void sendHello() { + helloAttempts = 0; + state = State.AWAITING_HELLO; + sendHelloAttempt(); + } + + public void tick() { + if (state != State.AWAITING_HELLO || clock.getAsLong() < nextHelloAt) { + return; + } + if (helloAttempts >= MAX_HELLO_ATTEMPTS) { + state = State.UNSUPPORTED; + return; + } + sendHelloAttempt(); + } + + private void sendHelloAttempt() { + ClientPacketSink activeSink = sink; + if (activeSink == null) { + // No sink yet means the loader has not bound the channel. Still burn an attempt and arm the retry + // clock, otherwise nextHelloAt stays MAX_VALUE, tick() never fires again and the UI sits on + // "connecting" for the whole session. + helloAttempts++; + nextHelloAt = clock.getAsLong() + HELLO_RETRY_MILLIS; + return; + } + byte[] frame = IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, CLIENT_CAPABILITIES)); + state = State.AWAITING_HELLO; + helloAttempts++; + nextHelloAt = clock.getAsLong() + HELLO_RETRY_MILLIS; + activeSink.send(frame); + } + + public void onServerHello(IrisMessage.ServerHello hello) { + this.serverProtocolVersion = hello.protocolVersion(); + if (hello.protocolVersion() != IrisProtocol.PROTOCOL_VERSION) { + this.serverBrand = hello.serverBrand(); + this.state = State.INCOMPATIBLE; + return; + } + this.serverCapabilities = hello.capabilities(); + this.irisActive = hello.irisActive(); + this.serverBrand = hello.serverBrand(); + this.state = State.READY; + } + + public void reset() { + this.state = State.IDLE; + this.serverCapabilities = 0L; + this.serverProtocolVersion = 0; + this.irisActive = false; + this.serverBrand = ""; + this.nextHelloAt = Long.MAX_VALUE; + this.helloAttempts = 0; + } + + public enum State { + IDLE, + AWAITING_HELLO, + READY, + UNSUPPORTED, + INCOMPATIBLE + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientTileCache.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientTileCache.java new file mode 100644 index 000000000..1918211cc --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientTileCache.java @@ -0,0 +1,143 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisMessageCodec; +import art.arcane.iris.spi.protocol.IrisProtocol; +import art.arcane.iris.spi.protocol.ProtocolException; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.LongSupplier; + +public final class IrisClientTileCache { + /** Enough for a 1080p viewport plus a ring of prefetch. 128x128 ARGB tiles are 64KB each. */ + static final int DEFAULT_MAX_CACHED_TILES = 256; + /** Ceiling for {@link #ensureCapacity(int)}: 4K at scale 1 needs ~660 tiles, so ~64MB of tile images. */ + static final int ABSOLUTE_MAX_CACHED_TILES = 2048; + private static final long REQUEST_RETRY_MILLIS = 3000L; + private static final int REQUESTS_PER_SECOND = IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND; + + private final ClientPacketSink sink; + private final LongSupplier clock; + private final IrisTileAssembler assembler; + private final LinkedHashMap cache; + private final Map pending; + private final Deque queue; + private final Set queued; + private int capacity; + private long windowStartMillis; + private int sentInWindow; + private long droppedMalformed; + + public IrisClientTileCache(ClientPacketSink sink, LongSupplier clock) { + this.sink = sink; + this.clock = clock; + this.assembler = new IrisTileAssembler(); + this.capacity = DEFAULT_MAX_CACHED_TILES; + this.cache = new LinkedHashMap<>(64, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > capacity; + } + }; + this.pending = new HashMap<>(); + this.queue = new ArrayDeque<>(); + this.queued = new HashSet<>(); + this.windowStartMillis = 0L; + this.sentInWindow = 0; + this.droppedMalformed = 0L; + } + + /** + * Raises the retained-tile budget to cover what the viewport can show at once. Without this a 4K screen at + * scale 1 evicts tiles it is still drawing, so every frame re-requests them and the map never fills. + */ + public synchronized void ensureCapacity(int visibleTiles) { + capacity = Math.max(DEFAULT_MAX_CACHED_TILES, Math.min(ABSOLUTE_MAX_CACHED_TILES, visibleTiles)); + } + + public synchronized void onVisionTile(IrisMessage.VisionTile tile) { + IrisTileImage image; + try { + image = assembler.add(tile); + } catch (ProtocolException malformed) { + droppedMalformed++; + return; + } + if (image == null) { + return; + } + IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel()); + cache.put(key, image); + pending.remove(key); + queued.remove(key); + } + + public synchronized IrisTileImage get(IrisTileKey key) { + return cache.get(key); + } + + public synchronized long droppedMalformedCount() { + return droppedMalformed; + } + + public synchronized void resetRequestQueue() { + queue.clear(); + queued.clear(); + } + + public synchronized void request(IrisTileKey key) { + if (cache.containsKey(key)) { + return; + } + long now = clock.getAsLong(); + Long lastRequest = pending.get(key); + if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) { + return; + } + if (queued.add(key)) { + queue.addLast(key); + } + } + + public synchronized void pump() { + long now = clock.getAsLong(); + if (now - windowStartMillis >= 1000L) { + windowStartMillis = now; + sentInWindow = 0; + // An in-flight marker is only meaningful for one retry window. Sweeping it here keeps the map + // bounded by tiles requested in the last few seconds instead of by every tile ever panned over. + pending.values().removeIf((Long requestedAt) -> now - requestedAt >= REQUEST_RETRY_MILLIS); + } + while (sentInWindow < REQUESTS_PER_SECOND && !queue.isEmpty()) { + IrisTileKey key = queue.pollFirst(); + queued.remove(key); + if (cache.containsKey(key)) { + continue; + } + Long lastRequest = pending.get(key); + if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) { + continue; + } + sink.send(IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(key.tileX(), key.tileZ(), key.zoom()))); + pending.put(key, now); + sentInWindow++; + } + } + + public synchronized void clear() { + assembler.clear(); + cache.clear(); + pending.clear(); + queue.clear(); + queued.clear(); + capacity = DEFAULT_MAX_CACHED_TILES; + sentInWindow = 0; + windowStartMillis = 0L; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java new file mode 100644 index 000000000..0521211a8 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java @@ -0,0 +1,78 @@ +package art.arcane.iris.client; + +import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.volmlib.util.localization.MessageArgument; + +import java.util.ArrayDeque; + +public final class IrisClientToasts { + private static final int MAX_PENDING = 4; + + private final ArrayDeque pending; + + public IrisClientToasts() { + this.pending = new ArrayDeque<>(); + } + + public void enqueue(int kind, String title, String body) { + Pending entry = new Pending(kind, normalize(title), normalize(body)); + synchronized (pending) { + while (pending.size() >= MAX_PENDING) { + pending.pollFirst(); + } + pending.addLast(entry); + } + } + + public void enqueueHotload(String packKey, int changedFiles, boolean failed, String message) { + int kind = failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS; + enqueue(kind, IrisLanguage.plain(ClientUiMessages.TOAST_STUDIO_HOTLOAD), hotloadBody(packKey, changedFiles, failed, message)); + } + + public Pending poll() { + synchronized (pending) { + return pending.pollFirst(); + } + } + + public void clear() { + synchronized (pending) { + pending.clear(); + } + } + + private static String normalize(String value) { + return value == null ? "" : value; + } + + private static String hotloadBody(String packKey, int changedFiles, boolean failed, String message) { + StringBuilder builder = new StringBuilder(); + String pack = normalize(packKey); + if (!pack.isEmpty()) { + builder.append(pack); + } + if (changedFiles != 0) { + append(builder, IrisLanguage.plain(ClientUiMessages.TOAST_CHANGED_FILES, MessageArgument.trusted("count", changedFiles))); + } + String text = normalize(message); + if (!text.isEmpty()) { + append(builder, text); + } + if (builder.isEmpty()) { + return IrisLanguage.plain(failed ? ClientUiMessages.TOAST_RELOAD_FAILED : ClientUiMessages.TOAST_RELOADED); + } + return builder.toString(); + } + + private static void append(StringBuilder builder, String part) { + if (!builder.isEmpty()) { + builder.append(" "); + } + builder.append(part); + } + + public record Pending(int kind, String title, String body) { + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java new file mode 100644 index 000000000..8dfa599a5 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java @@ -0,0 +1,182 @@ +package art.arcane.iris.client; + +import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.volmlib.util.localization.MessageArgument; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +/** + * CLIENT DIST ONLY. See {@link IrisClientHud} for why the dist marker is a javadoc contract plus a bytecode + * test rather than an @Environment annotation. + */ +public final class IrisPregenHud { + private static final int PANEL_COLOR = 0xC0101010; + private static final int TITLE_COLOR = 0xFF66BB6A; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int MUTED_COLOR = 0xFFD7D7D7; + private static final int BAR_BACK_COLOR = 0xFF2B2B2B; + private static final int BAR_RUNNING_COLOR = 0xFF66BB6A; + private static final int PAUSED_COLOR = 0xFFFFD54F; + private static final int STALE_COLOR = 0xFF8A8A8A; + private static final int GRID_BACK_COLOR = 0xFF161616; + private static final int CELL_PENDING_COLOR = 0xFF3A3A3A; + private static final int CELL_GENERATING_COLOR = 0xFFFFD54F; + private static final int CELL_DONE_COLOR = 0xFF66BB6A; + private static final int ORIGIN_X = 6; + private static final int ORIGIN_Y = 6; + private static final int PADDING = 4; + private static final int ROW_GAP = 2; + private static final int BAR_HEIGHT = 6; + private static final int MIN_WIDTH = 130; + private static final int MINIMAP_MAX_PX = 64; + private static final int MINIMAP_MAX_CELL = 6; + private static final int MINIMAP_GAP = 4; + + private IrisPregenHud() { + } + + public static void render(GuiGraphicsExtractor graphics) { + if (!IrisClient.hudVisible()) { + return; + } + IrisClientPregenState pregen = IrisClient.pregen(); + IrisMessage.PregenProgress progress = pregen.active(); + if (progress == null || pregen.activeExpired()) { + return; + } + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft == null || minecraft.player == null) { + return; + } + Font font = minecraft.font; + boolean stale = pregen.activeStale(); + boolean paused = progress.state() == IrisMessage.PregenProgress.STATE_PAUSED; + double percent = progress.chunksTotal() > 0L + ? clampPercent((double) progress.chunksDone() / (double) progress.chunksTotal() * 100.0D) + : 0.0D; + String title = IrisLanguage.plain(RuntimeUiMessages.PREGEN_HEADER); + String stats = IrisLanguage.plain( + ClientUiMessages.PREGEN_STATS, + MessageArgument.trusted("done", String.format("%,d", progress.chunksDone())), + MessageArgument.trusted("total", String.format("%,d", progress.chunksTotal())), + MessageArgument.trusted("percent", String.format("%.1f", percent)) + ); + String tail = tail(progress, pregen, stale, paused); + int accent = stale ? STALE_COLOR : paused ? PAUSED_COLOR : BAR_RUNNING_COLOR; + + int lineHeight = font.lineHeight; + int contentWidth = Math.max(MIN_WIDTH, Math.max(font.width(title), Math.max(font.width(stats), font.width(tail)))); + int contentHeight = lineHeight * 3 + ROW_GAP * 3 + BAR_HEIGHT; + + IrisClientRegionMap regionMap = IrisClient.regions(); + IrisClientRegionMap.Bounds bounds = regionMap.hasData() ? regionMap.bounds() : null; + boolean showMap = bounds != null; + int cellPx = 0; + int gridWidth = 0; + int gridHeight = 0; + if (showMap) { + int span = Math.max(bounds.regionsWide(), bounds.regionsTall()); + cellPx = Math.max(1, Math.min(MINIMAP_MAX_CELL, MINIMAP_MAX_PX / span)); + gridWidth = Math.min(MINIMAP_MAX_PX, bounds.regionsWide() * cellPx); + gridHeight = Math.min(MINIMAP_MAX_PX, bounds.regionsTall() * cellPx); + } + + int panelWidth = showMap ? Math.max(contentWidth, gridWidth) : contentWidth; + int panelHeight = showMap ? contentHeight + MINIMAP_GAP + gridHeight : contentHeight; + + graphics.fill(ORIGIN_X - PADDING, ORIGIN_Y - PADDING, ORIGIN_X + panelWidth + PADDING, ORIGIN_Y + panelHeight + PADDING, PANEL_COLOR); + + int cursorY = ORIGIN_Y; + graphics.text(font, title, ORIGIN_X, cursorY, stale ? STALE_COLOR : TITLE_COLOR); + cursorY += lineHeight + ROW_GAP; + graphics.text(font, stats, ORIGIN_X, cursorY, stale ? STALE_COLOR : TEXT_COLOR); + cursorY += lineHeight + ROW_GAP; + + int fillWidth = (int) Math.round(contentWidth * (percent / 100.0D)); + graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + contentWidth, cursorY + BAR_HEIGHT, BAR_BACK_COLOR); + if (fillWidth > 0) { + graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + fillWidth, cursorY + BAR_HEIGHT, accent); + } + cursorY += BAR_HEIGHT + ROW_GAP; + + graphics.text(font, tail, ORIGIN_X, cursorY, stale ? STALE_COLOR : paused ? PAUSED_COLOR : MUTED_COLOR); + + if (showMap) { + renderMinimap(graphics, regionMap, bounds, cellPx, gridWidth, gridHeight, ORIGIN_Y + contentHeight + MINIMAP_GAP); + } + } + + private static void renderMinimap(GuiGraphicsExtractor graphics, IrisClientRegionMap regionMap, IrisClientRegionMap.Bounds bounds, int cellPx, int gridWidth, int gridHeight, int gridTop) { + int mapLeft = ORIGIN_X; + int mapRight = mapLeft + gridWidth; + int mapBottom = gridTop + gridHeight; + graphics.fill(mapLeft, gridTop, mapRight, mapBottom, GRID_BACK_COLOR); + int minRegionX = bounds.minRegionX(); + int minRegionZ = bounds.minRegionZ(); + regionMap.forEachCell((regionX, regionZ, state) -> { + int px = mapLeft + (regionX - minRegionX) * cellPx; + int py = gridTop + (regionZ - minRegionZ) * cellPx; + if (px + cellPx > mapRight || py + cellPx > mapBottom) { + return; + } + graphics.fill(px, py, px + cellPx, py + cellPx, cellColor(state)); + }); + } + + private static int cellColor(int state) { + return switch (state) { + case IrisMessage.PregenRegionDelta.STATE_DONE -> CELL_DONE_COLOR; + case IrisMessage.PregenRegionDelta.STATE_GENERATING -> CELL_GENERATING_COLOR; + default -> CELL_PENDING_COLOR; + }; + } + + private static String tail(IrisMessage.PregenProgress progress, IrisClientPregenState pregen, boolean stale, boolean paused) { + if (stale) { + return IrisLanguage.plain( + ClientUiMessages.PREGEN_STALE, + MessageArgument.trusted("seconds", pregen.activeAgeMillis() / 1000L)); + } + return paused ? IrisLanguage.plain(ClientUiMessages.PREGEN_PAUSED) : rateAndEta(progress); + } + + private static String rateAndEta(IrisMessage.PregenProgress progress) { + String rate = String.format("%,.0f", progress.chunksPerSecond()); + if (progress.etaMillis() > 0L) { + return IrisLanguage.plain( + ClientUiMessages.PREGEN_RATE_ETA, + MessageArgument.trusted("rate", rate), + MessageArgument.trusted("eta", formatDuration(progress.etaMillis())) + ); + } + return IrisLanguage.plain(ClientUiMessages.PREGEN_RATE, MessageArgument.trusted("rate", rate)); + } + + private static String formatDuration(long etaMillis) { + long totalSeconds = etaMillis / 1000L; + long hours = totalSeconds / 3600L; + long minutes = (totalSeconds % 3600L) / 60L; + long seconds = totalSeconds % 60L; + if (hours > 0L) { + return IrisLanguage.plain(ClientUiMessages.DURATION_HOURS_MINUTES, MessageArgument.trusted("hours", hours), MessageArgument.trusted("minutes", minutes)); + } + if (minutes > 0L) { + return IrisLanguage.plain(ClientUiMessages.DURATION_MINUTES_SECONDS, MessageArgument.trusted("minutes", minutes), MessageArgument.trusted("seconds", seconds)); + } + return IrisLanguage.plain(ClientUiMessages.DURATION_SECONDS, MessageArgument.trusted("seconds", seconds)); + } + + private static double clampPercent(double value) { + if (value < 0.0D) { + return 0.0D; + } + if (value > 100.0D) { + return 100.0D; + } + return value; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java new file mode 100644 index 000000000..175843eea --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java @@ -0,0 +1,104 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisProtocol; +import art.arcane.iris.spi.protocol.ProtocolException; + +import java.util.LinkedHashMap; +import java.util.Map; + +final class IrisTileAssembler { + /** More chunks than the largest legal tile can occupy means the header is lying. */ + static final int MAX_CHUNK_COUNT = + (IrisTileCodec.MAX_DECODED_BYTES + IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES - 1) + / IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES + 1; + /** Half-assembled tiles retained at once; the oldest is dropped past this. */ + static final int MAX_PENDING_TILES = 64; + + private final Map partials; + + IrisTileAssembler() { + this.partials = new LinkedHashMap<>() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_PENDING_TILES; + } + }; + } + + /** + * @return the finished image once the last chunk of a set lands, or null while the set is incomplete or the + * frame is structurally unusable (impossible index or count, missing payload, a chunk from a set + * older than the one being assembled) + * @throws ProtocolException when a complete set decodes to a malformed blob + */ + IrisTileImage add(IrisMessage.VisionTile tile) throws ProtocolException { + int chunkCount = tile.chunkCount(); + int chunkIndex = tile.chunkIndex(); + if (chunkCount <= 0 || chunkCount > MAX_CHUNK_COUNT || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) { + return null; + } + IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel()); + Partial partial = partials.get(key); + if (partial != null && tile.sequence() < partial.sequence()) { + return null; + } + if (partial == null || tile.sequence() > partial.sequence() || partial.chunkCount() != chunkCount) { + partial = new Partial(tile.sequence(), chunkCount); + partials.put(key, partial); + } + if (!partial.accept(chunkIndex, tile.data())) { + return null; + } + partials.remove(key); + return IrisTileCodec.decode(partial.concat()); + } + + void clear() { + partials.clear(); + } + + private static final class Partial { + private final int sequence; + private final int chunkCount; + private final byte[][] chunks; + private int received; + + Partial(int sequence, int chunkCount) { + this.sequence = sequence; + this.chunkCount = chunkCount; + this.chunks = new byte[chunkCount][]; + this.received = 0; + } + + int sequence() { + return sequence; + } + + int chunkCount() { + return chunkCount; + } + + boolean accept(int chunkIndex, byte[] data) { + if (chunks[chunkIndex] == null) { + chunks[chunkIndex] = data; + received++; + } + return received >= chunkCount; + } + + byte[] concat() { + int total = 0; + for (byte[] chunk : chunks) { + total += chunk.length; + } + byte[] blob = new byte[total]; + int position = 0; + for (byte[] chunk : chunks) { + System.arraycopy(chunk, 0, blob, position, chunk.length); + position += chunk.length; + } + return blob; + } + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java new file mode 100644 index 000000000..a4a501bec --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java @@ -0,0 +1,119 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.ProtocolException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +public final class IrisTileCodec { + public static final int MODE_RAW_RGB = 0; + public static final int MODE_PALETTE = 1; + private static final int MAX_DIMENSION = 512; + private static final int OPAQUE = 0xFF000000; + static final int MAX_DECODED_BYTES = 9 + 4 + 3 * MAX_DIMENSION * MAX_DIMENSION; + + private IrisTileCodec() { + } + + /** + * Decodes an assembled tile blob. + * + * @return null when {@code deflatedBlob} is null or empty - nothing to decode is not an error + * @throws ProtocolException when the blob is present but malformed: a corrupt or stalled deflate stream, an + * oversized payload, bad dimensions, an unknown pixel mode, a bad palette or a + * truncated pixel run. Callers drop the tile and count it; the wire is untrusted. + */ + public static IrisTileImage decode(byte[] deflatedBlob) throws ProtocolException { + if (deflatedBlob == null || deflatedBlob.length == 0) { + return null; + } + byte[] raw = inflate(deflatedBlob); + try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(raw))) { + int width = in.readInt(); + int height = in.readInt(); + if (width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) { + throw new ProtocolException("vision tile dimensions " + width + "x" + height + " out of range"); + } + int mode = in.readUnsignedByte(); + int[] argb = new int[width * height]; + return switch (mode) { + case MODE_PALETTE -> decodePalette(in, width, height, argb); + case MODE_RAW_RGB -> decodeRaw(in, width, height, argb); + default -> throw new ProtocolException("unknown vision tile mode " + mode); + }; + } catch (EOFException truncated) { + throw new ProtocolException("vision tile blob truncated"); + } catch (IOException failure) { + throw new ProtocolException("vision tile blob unreadable: " + failure.getMessage()); + } + } + + private static IrisTileImage decodePalette(DataInputStream in, int width, int height, int[] argb) throws IOException, ProtocolException { + int paletteSize = in.readInt(); + if (paletteSize <= 0 || paletteSize > 256) { + throw new ProtocolException("vision tile palette size " + paletteSize + " out of range"); + } + int[] palette = new int[paletteSize]; + for (int index = 0; index < paletteSize; index++) { + int red = in.readUnsignedByte(); + int green = in.readUnsignedByte(); + int blue = in.readUnsignedByte(); + palette[index] = OPAQUE | red << 16 | green << 8 | blue; + } + for (int pixel = 0; pixel < argb.length; pixel++) { + int paletteIndex = in.readUnsignedByte(); + if (paletteIndex >= paletteSize) { + throw new ProtocolException("vision tile palette index " + paletteIndex + " beyond size " + paletteSize); + } + argb[pixel] = palette[paletteIndex]; + } + return new IrisTileImage(width, height, argb); + } + + private static IrisTileImage decodeRaw(DataInputStream in, int width, int height, int[] argb) throws IOException { + for (int pixel = 0; pixel < argb.length; pixel++) { + int red = in.readUnsignedByte(); + int green = in.readUnsignedByte(); + int blue = in.readUnsignedByte(); + argb[pixel] = OPAQUE | red << 16 | green << 8 | blue; + } + return new IrisTileImage(width, height, argb); + } + + private static byte[] inflate(byte[] input) throws ProtocolException { + Inflater inflater = new Inflater(); + inflater.setInput(input); + ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length * 2)); + byte[] buffer = new byte[8192]; + try { + while (!inflater.finished()) { + int produced = inflater.inflate(buffer); + if (produced == 0) { + if (inflater.finished()) { + break; + } + // needsInput here means the stream ended mid-member; needsDictionary means it wants a + // preset dictionary the encoder never uses. Either way there is no more progress to make, + // and looping on a zero-progress inflater spins a render thread forever. + throw new ProtocolException(inflater.needsDictionary() + ? "vision tile stream wants a preset dictionary" + : "vision tile stream ended before the deflate member finished"); + } + if (out.size() + produced > MAX_DECODED_BYTES) { + throw new ProtocolException("vision tile inflates beyond " + MAX_DECODED_BYTES + " bytes"); + } + out.write(buffer, 0, produced); + } + } catch (DataFormatException malformed) { + throw new ProtocolException("vision tile stream malformed: " + malformed.getMessage()); + } finally { + inflater.end(); + } + return out.toByteArray(); + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileImage.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileImage.java new file mode 100644 index 000000000..e29f5288d --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileImage.java @@ -0,0 +1,4 @@ +package art.arcane.iris.client; + +public record IrisTileImage(int width, int height, int[] argb) { +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileKey.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileKey.java new file mode 100644 index 000000000..d60daa05c --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileKey.java @@ -0,0 +1,4 @@ +package art.arcane.iris.client; + +public record IrisTileKey(int tileX, int tileZ, int zoom) { +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisToastPresenter.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisToastPresenter.java new file mode 100644 index 000000000..a806084a8 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisToastPresenter.java @@ -0,0 +1,39 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.toasts.SystemToast; +import net.minecraft.client.gui.components.toasts.ToastManager; +import net.minecraft.network.chat.Component; + +/** + * CLIENT DIST ONLY. See {@link IrisClientHud} for why the dist marker is a javadoc contract plus a bytecode + * test rather than an @Environment annotation. + */ +public final class IrisToastPresenter { + private IrisToastPresenter() { + } + + public static void pump() { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft == null || minecraft.gui == null) { + return; + } + ToastManager manager = minecraft.gui.toastManager(); + IrisClientToasts toasts = IrisClient.toasts(); + IrisClientToasts.Pending next = toasts.poll(); + while (next != null) { + SystemToast.addOrUpdate(manager, tokenFor(next.kind()), Component.literal(next.title()), Component.literal(next.body())); + next = toasts.poll(); + } + } + + private static SystemToast.SystemToastId tokenFor(int kind) { + return switch (kind) { + case IrisMessage.Toast.KIND_SUCCESS -> SystemToast.SystemToastId.WORLD_BACKUP; + case IrisMessage.Toast.KIND_WARNING -> SystemToast.SystemToastId.UNSECURE_SERVER_WARNING; + case IrisMessage.Toast.KIND_ERROR -> SystemToast.SystemToastId.PACK_LOAD_FAILURE; + default -> SystemToast.SystemToastId.PERIODIC_NOTIFICATION; + }; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java new file mode 100644 index 000000000..4ca86a448 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java @@ -0,0 +1,402 @@ +package art.arcane.iris.client; + +import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.volmlib.util.localization.MessageArgument; +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * CLIENT DIST ONLY. See {@link IrisClientHud} for why the dist marker is a javadoc contract plus a bytecode + * test rather than an @Environment annotation. + */ +public final class IrisVisionScreen extends Screen { + private static final int TILE_PIXELS = 128; + private static final int MIN_ZOOM = 0; + private static final int MAX_ZOOM = 8; + private static final int DEFAULT_ZOOM = 2; + /** Floor for the GPU texture budget, so a small window still keeps a prefetch ring resident. */ + private static final int MIN_TEXTURES = 220; + /** Ceiling, matching the tile cache: 4K at scale 1 draws ~660 tiles at once. */ + private static final int MAX_TEXTURES = IrisClientTileCache.ABSOLUTE_MAX_CACHED_TILES; + /** Extra rings of tiles kept resident beyond what is on screen right now. */ + private static final int TEXTURE_SLACK_RINGS = 1; + private static final int BACKGROUND_COLOR = 0xF00B0E14; + private static final int PLACEHOLDER_COLOR = 0xFF161A22; + private static final int GRID_COLOR = 0x33FFFFFF; + private static final int HEADER_COLOR = 0xC0101010; + private static final int TITLE_COLOR = 0xFF66BB6A; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int MUTED_COLOR = 0xFFB9B9B9; + private static final int PLAYER_FILL_COLOR = 0xFFFFDD33; + private static final int PLAYER_BORDER_COLOR = 0xFF101010; + private static final int MARKER_COLOR = 0xFF4FC3F7; + private static final int MARKER_LABEL_BG = 0xE0101010; + + private final Map textures; + private double centerBlockX; + private double centerBlockZ; + private int zoom; + private boolean initialized; + private IrisMessage.DimensionStatus renderedDimension; + + public IrisVisionScreen() { + super(Component.literal(IrisLanguage.plain(ClientUiMessages.VISION_TITLE))); + this.textures = new LinkedHashMap<>(64, 0.75f, true); + this.centerBlockX = 0.0D; + this.centerBlockZ = 0.0D; + this.zoom = DEFAULT_ZOOM; + this.initialized = false; + this.renderedDimension = null; + } + + @Override + protected void init() { + if (!initialized) { + centerOnPlayer(); + initialized = true; + } + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) { + graphics.fill(0, 0, width, height, BACKGROUND_COLOR); + IrisClientSession session = IrisClient.session(); + if (!session.isReady()) { + drawCentered(graphics, handshakeMessage(session), MUTED_COLOR); + drawHeader(graphics, IrisLanguage.plain(ClientUiMessages.VISION_TITLE), IrisLanguage.plain(ClientUiMessages.VISION_NOT_CONNECTED)); + return; + } + IrisMessage.DimensionStatus status = IrisClient.dimension().status(); + if (!IrisClient.visionAvailable() || status == null) { + drawCentered(graphics, IrisLanguage.plain(ClientUiMessages.VISION_NOT_IRIS_WORLD), MUTED_COLOR); + drawHeader(graphics, IrisLanguage.plain(ClientUiMessages.VISION_TITLE), status == null ? IrisLanguage.plain(ClientUiMessages.VISION_NO_DIMENSION_DATA) : label(status)); + return; + } + syncWorld(status); + renderTiles(graphics, mouseX, mouseY, status); + } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { + // The map itself has nothing clickable, but swallowing every click also swallows the ones widgets and + // the parent screen need. Let Screen route it; drag and scroll are handled below. + return super.mouseClicked(event, doubleClick); + } + + @Override + public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) { + int blocksPerPixel = 1 << zoom; + centerBlockX -= dragX * blocksPerPixel; + centerBlockZ -= dragY * blocksPerPixel; + return true; + } + + /** + * Known limitation, not a bug to chase: a zoom change makes every cached tile the wrong scale, because the + * zoom level is part of the tile key. The new level's tiles are re-requested at 8/s, so the map repaints + * from placeholders. Rendering the old level scaled would need a second texture path for a transient view. + */ + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (scrollY == 0.0D) { + return false; + } + int previousBlocksPerPixel = 1 << zoom; + double anchorBlockX = centerBlockX + (mouseX - width / 2.0D) * previousBlocksPerPixel; + double anchorBlockZ = centerBlockZ + (mouseY - height / 2.0D) * previousBlocksPerPixel; + int updated = zoom + (scrollY > 0.0D ? -1 : 1); + zoom = Math.max(MIN_ZOOM, Math.min(updated, MAX_ZOOM)); + int newBlocksPerPixel = 1 << zoom; + centerBlockX = anchorBlockX - (mouseX - width / 2.0D) * newBlocksPerPixel; + centerBlockZ = anchorBlockZ - (mouseY - height / 2.0D) * newBlocksPerPixel; + return true; + } + + @Override + public void removed() { + releaseTextures(); + super.removed(); + } + + private void renderTiles(GuiGraphicsExtractor graphics, int mouseX, int mouseY, IrisMessage.DimensionStatus status) { + int blocksPerPixel = 1 << zoom; + int tileSpanBlocks = TILE_PIXELS * blocksPerPixel; + int originX = (int) Math.floor(width / 2.0D - centerBlockX / blocksPerPixel); + int originY = (int) Math.floor(height / 2.0D - centerBlockZ / blocksPerPixel); + int minTileX = (int) Math.floor((0.0D - originX) / TILE_PIXELS) - 1; + int maxTileX = (int) Math.floor((width - originX) / (double) TILE_PIXELS) + 1; + int minTileZ = (int) Math.floor((0.0D - originY) / TILE_PIXELS) - 1; + int maxTileZ = (int) Math.floor((height - originY) / (double) TILE_PIXELS) + 1; + int centerTileX = Math.floorDiv((int) Math.floor(centerBlockX), tileSpanBlocks); + int centerTileZ = Math.floorDiv((int) Math.floor(centerBlockZ), tileSpanBlocks); + + int visibleTiles = (maxTileX - minTileX + 1) * (maxTileZ - minTileZ + 1); + IrisClientTileCache cache = IrisClient.tiles(); + cache.ensureCapacity(visibleTiles); + cache.resetRequestQueue(); + List missing = new ArrayList<>(); + + // Evict before the blit loop, never after. A DynamicTexture released mid-frame is a closed GPU handle + // that the already-recorded blit still points at, which is a use-after-close on the render thread. + evictTextures(textureCapacity(visibleTiles)); + + for (int tileX = minTileX; tileX <= maxTileX; tileX++) { + for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) { + int screenX = originX + tileX * TILE_PIXELS; + int screenY = originY + tileZ * TILE_PIXELS; + IrisTileKey key = new IrisTileKey(tileX, tileZ, zoom); + IrisTileImage image = cache.get(key); + if (image == null) { + graphics.fill(screenX, screenY, screenX + TILE_PIXELS, screenY + TILE_PIXELS, PLACEHOLDER_COLOR); + missing.add(key); + continue; + } + Identifier texture = ensureTexture(key, image); + graphics.blit(RenderPipelines.GUI_TEXTURED, texture, screenX, screenY, 0.0F, 0.0F, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS); + } + } + + requestMissing(cache, missing, centerTileX, centerTileZ); + cache.pump(); + + drawMarkers(graphics, mouseX, mouseY, minTileX, maxTileX, minTileZ, maxTileZ, originX, originY, blocksPerPixel); + drawPlayer(graphics, blocksPerPixel); + drawHeader( + graphics, + IrisLanguage.plain(ClientUiMessages.VISION_TITLE), + IrisLanguage.plain( + ClientUiMessages.VISION_HEADER_DETAIL, + MessageArgument.trusted("status", label(status)), + MessageArgument.trusted("zoom", zoom), + MessageArgument.trusted("x", (long) centerBlockX), + MessageArgument.trusted("z", (long) centerBlockZ) + ) + ); + drawFooter(graphics, IrisLanguage.plain(ClientUiMessages.VISION_FOOTER_HINT)); + } + + private void requestMissing(IrisClientTileCache cache, List missing, int centerTileX, int centerTileZ) { + missing.sort((left, right) -> Long.compare(distanceSquared(left, centerTileX, centerTileZ), distanceSquared(right, centerTileX, centerTileZ))); + for (IrisTileKey key : missing) { + cache.request(key); + } + } + + private void drawMarkers(GuiGraphicsExtractor graphics, int mouseX, int mouseY, int minTileX, int maxTileX, int minTileZ, int maxTileZ, int originX, int originY, int blocksPerPixel) { + IrisClientMarkers markers = IrisClient.markers(); + String hoverLabel = null; + int hoverX = 0; + int hoverY = 0; + for (int tileX = minTileX; tileX <= maxTileX; tileX++) { + for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) { + List tileMarkers = markers.forTile(new IrisTileKey(tileX, tileZ, zoom)); + if (tileMarkers == null) { + continue; + } + for (IrisMessage.VisionMarkers.Marker marker : tileMarkers) { + int screenX = originX + (int) Math.floor((double) marker.blockX() / blocksPerPixel); + int screenY = originY + (int) Math.floor((double) marker.blockZ() / blocksPerPixel); + graphics.fill(screenX - 2, screenY - 2, screenX + 2, screenY + 2, MARKER_COLOR); + if (Math.abs(mouseX - screenX) <= 4 && Math.abs(mouseY - screenY) <= 4 && marker.label() != null && !marker.label().isEmpty()) { + hoverLabel = marker.label(); + hoverX = screenX; + hoverY = screenY; + } + } + } + } + if (hoverLabel != null) { + int labelWidth = font.width(hoverLabel); + graphics.fill(hoverX + 6, hoverY - 6, hoverX + 12 + labelWidth, hoverY + font.lineHeight, MARKER_LABEL_BG); + graphics.text(font, hoverLabel, hoverX + 9, hoverY - 4, TEXT_COLOR); + } + } + + private void drawPlayer(GuiGraphicsExtractor graphics, int blocksPerPixel) { + LocalPlayer player = Minecraft.getInstance().player; + if (player == null) { + return; + } + int screenX = (int) Math.round(width / 2.0D + (player.getBlockX() - centerBlockX) / blocksPerPixel); + int screenY = (int) Math.round(height / 2.0D + (player.getBlockZ() - centerBlockZ) / blocksPerPixel); + graphics.fill(screenX - 4, screenY - 4, screenX + 4, screenY + 4, PLAYER_BORDER_COLOR); + graphics.fill(screenX - 3, screenY - 3, screenX + 3, screenY + 3, PLAYER_FILL_COLOR); + } + + private void drawHeader(GuiGraphicsExtractor graphics, String title, String detail) { + int lineHeight = font.lineHeight; + int headerHeight = lineHeight + 8; + graphics.fill(0, 0, width, headerHeight, HEADER_COLOR); + graphics.text(font, title, 8, 4, TITLE_COLOR); + graphics.text(font, detail, 12 + font.width(title), 4, MUTED_COLOR); + } + + private void drawFooter(GuiGraphicsExtractor graphics, String hint) { + int lineHeight = font.lineHeight; + int footerTop = height - lineHeight - 8; + graphics.fill(0, footerTop, width, height, HEADER_COLOR); + graphics.text(font, hint, 8, footerTop + 4, MUTED_COLOR); + } + + private void drawCentered(GuiGraphicsExtractor graphics, String text, int color) { + graphics.text(font, text, (width - font.width(text)) / 2, height / 2 - font.lineHeight / 2, color); + } + + private Identifier ensureTexture(IrisTileKey key, IrisTileImage image) { + TileTexture existing = textures.get(key); + if (existing != null) { + if (existing.image() == image) { + return existing.id(); + } + // IrisClientTileCache replaced the tile with a freshly decoded image (new sequence from the + // server). Identity is the generation counter: a re-decode is always a new record instance, so an + // upload keyed only on the tile coordinates would show the stale render forever. + Minecraft.getInstance().getTextureManager().release(existing.id()); + textures.remove(key); + } + int tileWidth = image.width(); + int tileHeight = image.height(); + int[] argb = image.argb(); + NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, tileWidth, tileHeight, false); + for (int y = 0; y < tileHeight; y++) { + int row = y * tileWidth; + for (int x = 0; x < tileWidth; x++) { + nativeImage.setPixelABGR(x, y, toAbgr(argb[row + x])); + } + } + DynamicTexture texture = new DynamicTexture(() -> "iris_vision_tile", nativeImage); + Identifier id = Identifier.fromNamespaceAndPath("irisworldgen", texturePath(key)); + Minecraft.getInstance().getTextureManager().register(id, texture); + textures.put(key, new TileTexture(id, image)); + return id; + } + + /** + * Texture budget for the current viewport: everything on screen plus a slack ring, clamped to sane bounds. + * A fixed 220 is under half of what 4K at scale 1 draws, so the eviction pass used to fight the blit loop. + */ + private static int textureCapacity(int visibleTiles) { + long required = (long) visibleTiles * (1 + 2 * TEXTURE_SLACK_RINGS); + return (int) Math.max(MIN_TEXTURES, Math.min(MAX_TEXTURES, required)); + } + + private void evictTextures(int capacity) { + if (textures.size() <= capacity) { + return; + } + TextureManager manager = Minecraft.getInstance().getTextureManager(); + Iterator> iterator = textures.entrySet().iterator(); + while (textures.size() > capacity && iterator.hasNext()) { + Map.Entry entry = iterator.next(); + manager.release(entry.getValue().id()); + iterator.remove(); + } + } + + private void releaseTextures() { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft != null) { + TextureManager manager = minecraft.getTextureManager(); + for (TileTexture texture : textures.values()) { + manager.release(texture.id()); + } + } + textures.clear(); + } + + private void syncWorld(IrisMessage.DimensionStatus status) { + if (renderedDimension == null) { + renderedDimension = status; + return; + } + if (!renderedDimension.equals(status)) { + renderedDimension = status; + releaseTextures(); + centerOnPlayer(); + } + } + + private void centerOnPlayer() { + LocalPlayer player = Minecraft.getInstance().player; + if (player != null) { + centerBlockX = player.getBlockX(); + centerBlockZ = player.getBlockZ(); + } + } + + /** + * Distinct copy per terminal handshake state. UNSUPPORTED means the hello retries ran out with no answer - + * no Iris on the server. INCOMPATIBLE means Iris answered with a different wire version. Reporting either + * as "connecting" left the screen sitting on a spinner that would never resolve. + */ + private static String handshakeMessage(IrisClientSession session) { + return switch (session.state()) { + case UNSUPPORTED -> IrisLanguage.plain(ClientUiMessages.VISION_SERVER_WITHOUT_IRIS); + case INCOMPATIBLE -> IrisLanguage.plain(ClientUiMessages.VISION_VERSION_MISMATCH); + default -> IrisLanguage.plain(ClientUiMessages.VISION_CONNECTING); + }; + } + + private static long distanceSquared(IrisTileKey key, int centerTileX, int centerTileZ) { + long deltaX = (long) key.tileX() - centerTileX; + long deltaZ = (long) key.tileZ() - centerTileZ; + return deltaX * deltaX + deltaZ * deltaZ; + } + + private static int toAbgr(int argb) { + int alpha = argb >>> 24 & 0xFF; + int red = argb >> 16 & 0xFF; + int green = argb >> 8 & 0xFF; + int blue = argb & 0xFF; + return alpha << 24 | blue << 16 | green << 8 | red; + } + + private static String texturePath(IrisTileKey key) { + return "vision/t_" + safe(key.tileX()) + "_" + safe(key.tileZ()) + "_" + key.zoom(); + } + + private static String safe(int value) { + return value < 0 ? "m" + (-(long) value) : Integer.toString(value); + } + + private static String label(IrisMessage.DimensionStatus status) { + if (status.packKey() == null || status.packKey().isEmpty()) { + return String.valueOf(status.dimensionKey()); + } + return IrisLanguage.plain( + ClientUiMessages.VISION_DIMENSION_PACK, + MessageArgument.untrusted("dimension", String.valueOf(status.dimensionKey())), + MessageArgument.untrusted("pack", status.packKey()) + ); + } + + /** + * The registered texture id plus the exact image instance uploaded into it. The DynamicTexture itself is + * not retained: TextureManager owns it after register, and release(id) is the only handle needed. Holding + * the source image is what lets {@link #ensureTexture(IrisTileKey, IrisTileImage)} notice a re-decode. + */ + private record TileTexture(Identifier id, IrisTileImage image) { + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java new file mode 100644 index 000000000..7c284fd21 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java @@ -0,0 +1,101 @@ +package art.arcane.iris.client; + +import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.volmlib.util.localization.MessageArgument; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; + +import java.util.ArrayList; +import java.util.List; + +/** + * CLIENT DIST ONLY. See {@link IrisClientHud} for why the dist marker is a javadoc contract plus a bytecode + * test rather than an @Environment annotation. + */ +public final class IrisWhatOverlay { + private static final int PANEL_COLOR = 0xC0101010; + private static final int TITLE_COLOR = 0xFF66BB6A; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int MUTED_COLOR = 0xFFC7C7C7; + private static final int PADDING = 4; + private static final int ROW_GAP = 2; + private static final int CURSOR_OFFSET = 12; + + private IrisWhatOverlay() { + } + + public static void render(GuiGraphicsExtractor graphics) { + if (!IrisClient.whatVisible() || !IrisClient.cursorAvailable()) { + return; + } + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft == null || minecraft.player == null) { + return; + } + LocalPlayer player = minecraft.player; + int blockX = player.getBlockX(); + int blockZ = player.getBlockZ(); + HitResult hit = minecraft.hitResult; + if (hit != null && hit.getType() == HitResult.Type.BLOCK && hit instanceof BlockHitResult blockHit) { + BlockPos pos = blockHit.getBlockPos(); + blockX = pos.getX(); + blockZ = pos.getZ(); + } + IrisClient.cursor().requestFor(blockX, blockZ); + + IrisMessage.CursorInfo info = IrisClient.cursor().latest(); + List lines = new ArrayList<>(); + if (info == null) { + lines.add(new OverlayLine(IrisLanguage.plain(ClientUiMessages.WHAT_QUERYING, MessageArgument.trusted("x", blockX), MessageArgument.trusted("z", blockZ)), false)); + } else { + lines.add(new OverlayLine(IrisLanguage.plain(ClientUiMessages.WHAT_BIOME, MessageArgument.untrusted("biome", display(info.biomeKey()))), false)); + lines.add(new OverlayLine(IrisLanguage.plain(ClientUiMessages.WHAT_REGION, MessageArgument.untrusted("region", display(info.regionKey()))), false)); + if (info.caveBiomeKey() != null && !info.caveBiomeKey().isEmpty()) { + lines.add(new OverlayLine(IrisLanguage.plain(ClientUiMessages.WHAT_CAVE, MessageArgument.untrusted("cave", display(info.caveBiomeKey()))), false)); + } + lines.add(new OverlayLine(IrisLanguage.plain( + ClientUiMessages.WHAT_HEIGHT, + MessageArgument.trusted("height", info.height()), + MessageArgument.trusted("x", info.blockX()), + MessageArgument.trusted("z", info.blockZ()) + ), true)); + } + draw(graphics, minecraft.font, lines); + } + + private static void draw(GuiGraphicsExtractor graphics, Font font, List lines) { + int lineHeight = font.lineHeight; + String title = IrisLanguage.plain(ClientUiMessages.WHAT_TITLE); + int contentWidth = font.width(title); + for (OverlayLine line : lines) { + contentWidth = Math.max(contentWidth, font.width(line.text())); + } + int originX = graphics.guiWidth() / 2 + CURSOR_OFFSET; + int originY = graphics.guiHeight() / 2 + CURSOR_OFFSET; + int rows = lines.size() + 1; + int contentHeight = lineHeight * rows + ROW_GAP * rows; + graphics.fill(originX - PADDING, originY - PADDING, originX + contentWidth + PADDING, originY + contentHeight + PADDING, PANEL_COLOR); + + int cursorY = originY; + graphics.text(font, title, originX, cursorY, TITLE_COLOR); + cursorY += lineHeight + ROW_GAP; + for (OverlayLine line : lines) { + graphics.text(font, line.text(), originX, cursorY, line.muted() ? MUTED_COLOR : TEXT_COLOR); + cursorY += lineHeight + ROW_GAP; + } + } + + private static String display(String key) { + return key == null || key.isEmpty() ? "-" : key; + } + + private record OverlayLine(String text, boolean muted) { + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldOpenFlowsMixin.java b/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldOpenFlowsMixin.java new file mode 100644 index 000000000..fad50a286 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldOpenFlowsMixin.java @@ -0,0 +1,121 @@ +package art.arcane.iris.client.mixin; + +import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedMixinFlags; +import com.mojang.serialization.Lifecycle; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.AlertScreen; +import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen; +import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; +import net.minecraft.client.gui.screens.worldselection.WorldOpenFlows; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.WorldStem; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.presets.WorldPreset; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Optional; + +/** + * CLIENT DIST ONLY. Registered from irisworldgen.client.mixins.json, whose "client" block already restricts + * application to the client dist. Target verified against MC 26.2: + * {@code WorldOpenFlows.confirmWorldCreation(Minecraft, CreateWorldScreen, Lifecycle, Runnable, boolean)} is + * static, and {@code openWorldCheckWorldStemCompatibility} is a private instance method. + */ +@Mixin(WorldOpenFlows.class) +public abstract class IrisWorldOpenFlowsMixin { + @Invoker("openWorldLoadBundledResourcePack") + protected abstract void iris$openWorldLoadBundledResourcePack( + LevelStorageSource.LevelStorageAccess worldAccess, + WorldStem worldStem, + PackRepository packRepository, + Runnable onCancel); + + @Inject(method = "confirmWorldCreation", at = @At("HEAD"), cancellable = true) + private static void iris$confirmWorldCreation( + Minecraft minecraft, + CreateWorldScreen parent, + Lifecycle lifecycle, + Runnable task, + boolean skipWarning, + CallbackInfo info) { + ModdedMixinFlags.markWorldOpenFlows(); + if (!iris$selectedPresetIsIris(parent)) { + return; + } + if (!parent.getUiState().isGenerateStructures()) { + // Iris runs its own placement through the vanilla structure step, so a world created with + // Generate Structures off is refused at load by IrisModdedChunkGenerator. Stop it here, at the + // one screen that still has a toggle to fix, instead of at the load that would only report it. + iris$showStructuresRequired(minecraft, parent); + info.cancel(); + return; + } + if (skipWarning || lifecycle == Lifecycle.stable()) { + return; + } + task.run(); + info.cancel(); + } + + private static void iris$showStructuresRequired(Minecraft minecraft, CreateWorldScreen parent) { + // 5-arg ctor with shouldCloseOnEsc=false: ESC on the 3-arg ctor closes to the title screen, + // skipping CreateWorldScreen.onClose and leaking its temp datapack directory. + minecraft.gui.setScreen(new AlertScreen( + () -> minecraft.gui.setScreen(parent), + Component.literal(IrisLanguage.plain(ClientUiMessages.CREATE_STRUCTURES_REQUIRED_TITLE)), + Component.literal(IrisLanguage.plain(ClientUiMessages.CREATE_STRUCTURES_REQUIRED_BODY)), + CommonComponents.GUI_BACK, + false)); + } + + @Inject(method = "openWorldCheckWorldStemCompatibility", at = @At("HEAD"), cancellable = true) + private void iris$openWorldCheckWorldStemCompatibility( + LevelStorageSource.LevelStorageAccess worldAccess, + WorldStem worldStem, + PackRepository packRepository, + Runnable onCancel, + CallbackInfo info) { + ModdedMixinFlags.markWorldOpenFlows(); + if (!iris$containsIrisGenerator(worldStem)) { + return; + } + iris$openWorldLoadBundledResourcePack(worldAccess, worldStem, packRepository, onCancel); + info.cancel(); + } + + private static boolean iris$selectedPresetIsIris(CreateWorldScreen parent) { + WorldCreationUiState.WorldTypeEntry worldType = parent.getUiState().getWorldType(); + Holder preset = worldType.preset(); + if (preset == null) { + return false; + } + Optional> key = preset.unwrapKey(); + return key.isPresent() && "irisworldgen".equals(key.get().identifier().getNamespace()); + } + + private static boolean iris$containsIrisGenerator(WorldStem worldStem) { + Registry dimensions = worldStem.registries() + .compositeAccess() + .lookupOrThrow(Registries.LEVEL_STEM); + for (LevelStem dimension : dimensions) { + if (dimension.generator() instanceof IrisModdedChunkGenerator) { + return true; + } + } + return false; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldTypeEntryMixin.java b/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldTypeEntryMixin.java new file mode 100644 index 000000000..b961cf949 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldTypeEntryMixin.java @@ -0,0 +1,43 @@ +package art.arcane.iris.client.mixin; + +import art.arcane.iris.modded.ModdedMixinFlags; +import art.arcane.iris.modded.ModdedWorldgenIds; +import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; +import net.minecraft.core.Holder; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.levelgen.presets.WorldPreset; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Optional; + +/** + * CLIENT DIST ONLY. Registered from irisworldgen.client.mixins.json, whose "client" block already restricts + * application to the client dist. + */ +@Mixin(WorldCreationUiState.WorldTypeEntry.class) +public class IrisWorldTypeEntryMixin { + @Shadow + @Final + private Holder preset; + + @Inject(method = "describePreset", at = @At("HEAD"), cancellable = true) + private void iris$describePreset(CallbackInfoReturnable info) { + ModdedMixinFlags.markWorldTypeEntry(); + Optional> key = preset == null + ? Optional.empty() + : preset.unwrapKey(); + if (key.isEmpty() || !"irisworldgen".equals(key.get().identifier().getNamespace())) { + return; + } + String label = ModdedWorldgenIds.displayName(key.get().identifier().getPath()); + if (label != null) { + info.setReturnValue(Component.literal(label)); + } + } +} diff --git a/adapters/fabric/build.gradle b/adapters/fabric/build.gradle new file mode 100644 index 000000000..de22ce325 --- /dev/null +++ b/adapters/fabric/build.gradle @@ -0,0 +1,314 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.jvm.toolchain.JavaLanguageVersion + +plugins { + id 'java' + id 'net.fabricmc.fabric-loom' version '1.17.14' + alias(libs.plugins.shadow) +} + +Properties rootProperties = new Properties() +file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } +String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2')) +String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) +String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3')) +String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')) +Closure irisArtifactName = { String platform, String targetVersion -> + return "Iris v${project.version} [${platform}] ${targetVersion}.jar" +} + +group = 'art.arcane' +version = irisVersion + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +sourceSets { + main { + java { + srcDir '../modded-common/src/main/java' + srcDir '../minecraft-common/src/main/java' + srcDir '../client-common/src/main/java' + } + resources { + srcDir '../modded-common/src/main/resources' + } + } + test { + java { + srcDir '../modded-common/src/test/java' + } + } +} + +repositories { + mavenCentral() + maven { + name = 'FabricMC' + url = uri('https://maven.fabricmc.net/') + } + maven { url = uri('https://repo.codemc.org/repository/maven-public/') } + maven { url = uri('https://jitpack.io') } + maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') } +} + +configurations { + bundle { + canBeConsumed = false + canBeResolved = true + } + devBundle { + canBeConsumed = false + canBeResolved = true + } +} + +configurations.named('bundle').configure { + exclude(group: 'com.google.code.gson') + exclude(group: 'com.google.guava') + exclude(group: 'commons-io') + exclude(group: 'org.apache.commons', module: 'commons-lang3') + exclude(group: 'it.unimi.dsi', module: 'fastutil') + exclude(group: 'org.slf4j') + exclude(group: 'org.apache.logging.log4j') + exclude(group: 'de.crazydev22.slimjar.helper') + exclude(group: 'de.crazydev22.slimjar') + exclude(group: 'io.github.slimjar') +} + +configurations.compileClasspath.extendsFrom(configurations.devBundle) +configurations.runtimeClasspath.extendsFrom(configurations.devBundle) +configurations.testCompileClasspath.extendsFrom(configurations.devBundle) +configurations.testRuntimeClasspath.extendsFrom(configurations.devBundle) + +// Jar-in-jar payload: exactly the Fabric API modules declared below, nothing else. Kept +// non-transitive so the bundled set matches the `jars` list in fabric.mod.json one-for-one — a +// transitive pull (fabric-transitive-access-wideners-v1) used to land in META-INF/jars undeclared. +configurations.create('jij') { + transitive = false +} + +dependencies { + minecraft("com.mojang:minecraft:${minecraftVersion}") + implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}") + testImplementation('junit:junit:4.13.2') + // registrySync and resourceLoader are LOAD-BEARING despite zero imports anywhere in Iris: + // fabric-registry-sync-v0 delays registry freeze past mod init, which is what lets + // IrisFabricBootstrap register the CHUNK_GENERATOR codec in + // onInitialize instead of crashing on a frozen registry. + // fabric-resource-loader-v1 is what makes assets/irisworldgen/lang/*.json (and the keybind + // labels) resolve from a mod jar. + // Removing either one produces a boot crash / silent missing-translation regression, not a + // compile error. Do not prune them as unused. + List fabricApi = [ + libs.fabricApi.base, + libs.fabricApi.registrySync, + libs.fabricApi.resourceLoader, + libs.fabricApi.lifecycleEvents, + libs.fabricApi.commandApi, + libs.fabricApi.eventsInteraction, + libs.fabricApi.networking, + libs.fabricApi.rendering, + libs.fabricApi.keyMapping, + libs.fabricApi.permission + ] + fabricApi.each { Object notation -> + add('jij', notation) + add('implementation', notation) + } + compileOnly('org.slf4j:slf4j-api:2.0.17') + compileOnly(libs.spigot) { + transitive = false + } + add('bundle', volmLibCoordinate) { + transitive = false + } + add('devBundle', volmLibCoordinate) { + transitive = false + } + add('bundle', libs.zip) { + transitive = false + } + add('devBundle', libs.zip) { + transitive = false + } + + List shared = [ + "art.arcane:core:${irisVersion}".toString(), + "art.arcane:spi:${irisVersion}".toString(), + libs.paralithic, + libs.lru, + libs.caffeine, + libs.dom4j, + libs.jaxen, + libs.sentry + ] + shared.each { Object notation -> + add('bundle', notation) + add('devBundle', notation) + } +} + +tasks.named('compileJava', JavaCompile).configure { + options.encoding = 'UTF-8' + options.release.set(25) +} + +tasks.named('test').configure { + systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath) +} + +loom { + accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener') + runs { + client { + runDir(providers.gradleProperty('irisClientRunDir').getOrElse('run')) + vmArg('-Xmx8G') + } + server { + runDir(providers.gradleProperty('irisServerRunDir').getOrElse('run')) + String parity = providers.gradleProperty('irisParity').getOrNull() + if (parity != null) { + property('iris.parity', parity) + } + String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull() + if (parityGolden != null) { + property('iris.parity.golden', parityGolden) + } + String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull() + if (parityDeep != null) { + property('iris.parity.deep', parityDeep) + } + String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull() + if (worldCheck != null) { + property('iris.worldcheck', worldCheck) + } + vmArg('-Xmx8G') + programArgs('nogui') + } + } +} + +processResources { + inputs.property('version', project.version) + inputs.property('minecraftVersion', minecraftVersion) + filesMatching('fabric.mod.json') { + expand('version': project.version, 'minecraftVersion': minecraftVersion) + } +} + +// META-INF/jars filenames are declared verbatim in the fabric.mod.json `jars[]` array, and Fabric +// loader resolves them by that exact string. Map every bundled module to its declared filename +// instead of stripping the version with a regex: a version containing a '-' silently defeats the +// strip and leaves an undeclared nested jar (which loader then ignores) behind. +Map nestedFabricApiJars = [ + 'fabric-api-base' : 'fabric-api-base.jar', + 'fabric-registry-sync-v0' : 'fabric-registry-sync-v0.jar', + 'fabric-resource-loader-v1' : 'fabric-resource-loader-v1.jar', + 'fabric-lifecycle-events-v1' : 'fabric-lifecycle-events-v1.jar', + 'fabric-command-api-v2' : 'fabric-command-api-v2.jar', + 'fabric-events-interaction-v0': 'fabric-events-interaction-v0.jar', + 'fabric-networking-api-v1' : 'fabric-networking-api-v1.jar', + 'fabric-rendering-v1' : 'fabric-rendering-v1.jar', + 'fabric-key-mapping-api-v1' : 'fabric-key-mapping-api-v1.jar', + 'fabric-permission-api-v1' : 'fabric-permission-api-v1.jar' +] + +tasks.named('shadowJar', ShadowJar).configure { + doFirst { + delete(fileTree(layout.buildDirectory.dir('libs')) { + include('Iris v* [Fabric] *.jar') + include('iris-fabric-*.jar') + }) + delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-fabric.jar")) + } + archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}")) + configurations = [project.configurations.named('bundle').get()] + mergeServiceFiles() + exclude('META-INF/maven/**') + exclude('META-INF/proguard/**') + exclude('META-INF/*.SF') + exclude('META-INF/*.DSA') + exclude('META-INF/*.RSA') + // The only multi-release entry any bundled dependency contributes is an OSGi manifest. Keeping + // it forces `Multi-Release: true` onto the mod jar, which makes the loaders treat the whole + // archive as versioned and re-scan it per release directory for nothing. + exclude('META-INF/versions/**') + addMultiReleaseAttribute.set(false) + // Invalid package name on Java 9+ ('enum' is a keyword); a module-path scan chokes on it. + exclude('org/apache/commons/lang/enum/**') + // Compile-only annotation carriers. Shipping them duplicates packages other mods also ship. + exclude('org/jspecify/**') + exclude('com/google/errorprone/**') + exclude('com/google/j2objc/**') + exclude('org/checkerframework/**') + exclude('org/jetbrains/annotations/**') + exclude('org/intellij/lang/**') + // The Bukkit platform binding cannot load without org.bukkit on the classpath, which no mod + // loader has. Shipping it only adds dead classes that reference a missing package. + exclude('art/arcane/iris/platform/bukkit/**') + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm') + relocate('io.sentry', 'art.arcane.iris.shadow.sentry') + // Split-package guard. A co-installed mod that ships any of these at their original coordinates + // puts two copies of the same package on one module layer, which the loaders reject at boot + // (Terra ships paralithic; caffeine and dom4j are common in mod dependency trees). First-party + // art.arcane.volmlib is deliberately NOT relocated. + relocate('com.github.benmanes.caffeine', 'art.arcane.iris.shadow.caffeine') + relocate('com.googlecode.concurrentlinkedhashmap', 'art.arcane.iris.shadow.clhm') + relocate('com.dfsek.paralithic', 'art.arcane.iris.shadow.paralithic') + relocate('org.dom4j', 'art.arcane.iris.shadow.dom4j') + relocate('org.jaxen', 'art.arcane.iris.shadow.jaxen') + relocate('org.zeroturnaround.zip', 'art.arcane.iris.shadow.ztzip') + from(project.configurations.named('jij')) { + into('META-INF/jars') + rename { String fileName -> + String module = nestedFabricApiJars.keySet() + .sort { String key -> -key.length() } + .find { String key -> fileName.startsWith(key + '-') } + if (module == null) { + throw new GradleException("Undeclared Fabric API nested jar: ${fileName}. Add it to " + + 'nestedFabricApiJars and to the jars[] array in fabric.mod.json.') + } + + return nestedFabricApiJars.get(module) + } + } +} + +// The thin `jar`/`remapJar` outputs carry valid fabric.mod.json metadata but bundle zero +// dependencies. Installing one is an instant NoClassDefFoundError, and they sit in build/libs right +// next to the real artifact. shadowJar is the only shippable Fabric jar. +tasks.named('jar').configure { + enabled = false +} + +tasks.matching { it.name == 'remapJar' }.configureEach { + enabled = false +} + +tasks.named('assemble').configure { + dependsOn(tasks.named('shadowJar')) +} diff --git a/adapters/fabric/gradle.properties b/adapters/fabric/gradle.properties new file mode 100644 index 000000000..af258fbfc --- /dev/null +++ b/adapters/fabric/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.caching=true +org.gradle.configuration-cache=false diff --git a/adapters/fabric/settings.gradle b/adapters/fabric/settings.gradle new file mode 100644 index 000000000..c46bcfa7c --- /dev/null +++ b/adapters/fabric/settings.gradle @@ -0,0 +1,52 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +pluginManagement { + repositories { + maven { + name = 'FabricMC' + url = uri('https://maven.fabricmc.net/') + } + gradlePluginPortal() + mavenCentral() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + +rootProject.name = 'iris-fabric' + +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files('../../gradle/libs.versions.toml')) + } + } +} + +// Shared VolmLib source resolution; see gradle/volmlib-resolution.settings.gradle. +apply from: new File(settingsDir, '../../gradle/volmlib-resolution.settings.gradle').canonicalFile + +includeBuild('../..') { + dependencySubstitution { + substitute(module('art.arcane:core')).using(project(':core')) + substitute(module('art.arcane:spi')).using(project(':spi')) + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricForcedDatapackSources.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricForcedDatapackSources.java new file mode 100644 index 000000000..a67f60f07 --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricForcedDatapackSources.java @@ -0,0 +1,40 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.fabric; + +import art.arcane.iris.modded.ModdedForcedDatapack; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; + +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +public final class FabricForcedDatapackSources { + private FabricForcedDatapackSources() { + } + + public static void attach(PackRepository repository) { + PackRepository activeRepository = Objects.requireNonNull(repository, + "Iris cannot attach the forced startup datapack source to a null repository"); + Set merged = new LinkedHashSet<>(activeRepository.sources); + merged.add(ModdedForcedDatapack.repositorySource()); + activeRepository.sources = merged; + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricModdedLoader.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricModdedLoader.java new file mode 100644 index 000000000..8856e5cf9 --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricModdedLoader.java @@ -0,0 +1,111 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.fabric; + +import art.arcane.iris.modded.ModdedLoader; +import art.arcane.iris.modded.service.ModdedTreeFellerService; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; +import net.fabricmc.fabric.api.permission.v1.PermissionContextOwner; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.PermissionLevel; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import java.io.File; +import java.nio.file.Path; + +public final class FabricModdedLoader implements ModdedLoader { + private static final Identifier TREE_FELLER_PERMISSION = Identifier.fromNamespaceAndPath("irisworldgen", "treefeller"); + + @Override + public String platformName() { + return "fabric"; + } + + @Override + public String minecraftVersion() { + return FabricLoader.getInstance().getModContainer("minecraft") + .map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString()) + .orElse("unknown"); + } + + @Override + public String modVersion() { + return FabricLoader.getInstance().getModContainer("irisworldgen") + .map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString()) + .orElse("unknown"); + } + + @Override + public MinecraftServer currentServer() { + Object instance = FabricLoader.getInstance().getGameInstance(); + return instance instanceof MinecraftServer server ? server : null; + } + + @Override + public void invalidateLevelCache(MinecraftServer server) { + // Intentionally empty: Fabric keeps no cached level view of its own (getAllLevels reads the live + // map). Off-thread readers rely on the ModdedServerLevels snapshot, which the caller republishes. + } + + @Override + public boolean clientEnvironment() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } + + @Override + public Path configDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + public File modJar() { + return FabricLoader.getInstance().getModContainer("irisworldgen") + .flatMap((ModContainer container) -> container.getOrigin().getPaths().stream().findFirst()) + .map((Path p) -> p.toFile()) + .orElse(null); + } + + @Override + public boolean hasTreeFellerPermission(ServerPlayer player) { + return ((PermissionContextOwner) player).checkPermission( + TREE_FELLER_PERMISSION, + PermissionLevel.GAMEMASTERS + ); + } + + @Override + public boolean canTreeFellerBreak( + ServerLevel level, + ServerPlayer player, + BlockPos position, + BlockState state + ) { + BlockEntity blockEntity = state.hasBlockEntity() ? level.getBlockEntity(position) : null; + return ModdedTreeFellerService.runBreakProbe(() -> PlayerBlockBreakEvents.BEFORE.invoker() + .beforeBlockBreak(level, player, position, state, blockEntity)); + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricProtocolNetworking.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricProtocolNetworking.java new file mode 100644 index 000000000..c5465c243 --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricProtocolNetworking.java @@ -0,0 +1,54 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.fabric; + +import art.arcane.iris.modded.ModdedIrisPayload; +import art.arcane.iris.modded.ModdedProtocolChannel; +import art.arcane.iris.modded.ModdedProtocolHandler; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.server.level.ServerPlayer; + +public final class FabricProtocolNetworking { + private FabricProtocolNetworking() { + } + + public static void install() { + PayloadTypeRegistry.clientboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC); + PayloadTypeRegistry.serverboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC); + ServerPlayNetworking.registerGlobalReceiver(ModdedIrisPayload.TYPE, + (payload, context) -> ModdedProtocolHandler.onInbound(context.player(), payload.data())); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> ModdedProtocolHandler.onPlayerJoin(handler.player)); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> ModdedProtocolHandler.onPlayerDisconnect(handler.player)); + ModdedProtocolHandler.bindChannel(new FabricProtocolChannel()); + } + + private static final class FabricProtocolChannel implements ModdedProtocolChannel { + @Override + public boolean canReceive(ServerPlayer player) { + return ServerPlayNetworking.canSend(player, ModdedIrisPayload.TYPE); + } + + @Override + public void send(ServerPlayer player, ModdedIrisPayload payload) { + ServerPlayNetworking.send(player, payload); + } + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java new file mode 100644 index 000000000..357e26a91 --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java @@ -0,0 +1,86 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.fabric; + +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedBlockBreakHandler; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.command.IrisModdedCommands; +import art.arcane.iris.modded.command.ModdedWandService; +import com.mojang.brigadier.CommandDispatcher; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.player.AttackBlockCallback; +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +public final class IrisFabricBootstrap implements ModInitializer { + @Override + public void onInitialize() { + ModdedEngineBootstrap.bootCommon(new FabricModdedLoader(), "Fabric", + () -> Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC)); + FabricProtocolNetworking.install(); + ServerLifecycleEvents.SERVER_STARTING.register((MinecraftServer server) -> ModdedEngineBootstrap.start(server)); + ServerLifecycleEvents.SERVER_STARTED.register((MinecraftServer server) -> ModdedEngineBootstrap.serverStarted(server)); + ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop()); + ServerLevelEvents.LOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelLoaded(level)); + ServerLevelEvents.UNLOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelUnloaded(level)); + CommandRegistrationCallback.EVENT.register((CommandDispatcher dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher)); + PlayerBlockBreakEvents.BEFORE.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> { + if (level instanceof ServerLevel serverLevel && player instanceof ServerPlayer serverPlayer) { + ModdedBlockBreakHandler.prepare(serverLevel, serverPlayer, pos, state); + } + return true; + }); + PlayerBlockBreakEvents.CANCELED.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> { + if (level instanceof ServerLevel serverLevel) { + ModdedBlockBreakHandler.cancel(serverLevel, pos); + } + }); + AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) -> + ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS); + UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) -> + ModdedWandService.useBlock(player, level, hand, hit.getBlockPos()) ? InteractionResult.SUCCESS : InteractionResult.PASS); + ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> { + ModdedEngineBootstrap.tick(server); + ModdedWandService.serverTick(server); + }); + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricClient.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricClient.java new file mode 100644 index 000000000..4eaa7b332 --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricClient.java @@ -0,0 +1,54 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.fabric; + +import art.arcane.iris.client.IrisClient; +import art.arcane.iris.client.IrisClientHud; +import art.arcane.iris.client.IrisClientKeybinds; +import art.arcane.iris.modded.ModdedIrisPayload; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; + +public final class IrisFabricClient implements ClientModInitializer { + @Override + public void onInitializeClient() { + ClientPlayNetworking.registerGlobalReceiver(ModdedIrisPayload.TYPE, + (payload, context) -> IrisClient.onInbound(payload.data())); + IrisClient.bindSender(frame -> { + if (ClientPlayNetworking.canSend(ModdedIrisPayload.TYPE)) { + ClientPlayNetworking.send(new ModdedIrisPayload(frame)); + } + }); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> IrisClient.onWorldJoin()); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> IrisClient.onDisconnect()); + KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.TOGGLE_HUD); + KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.OPEN_MAP); + KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.TOGGLE_WHAT); + HudElementRegistry.addLast(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics)); + ClientTickEvents.END_CLIENT_TICK.register(client -> { + IrisClient.tick(); + IrisClientHud.tick(); + IrisClientKeybinds.pollToggle(); + }); + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/BlockItemMixin.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/BlockItemMixin.java new file mode 100644 index 000000000..723c4696b --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/BlockItemMixin.java @@ -0,0 +1,25 @@ +package art.arcane.iris.fabric.mixin; + +import art.arcane.iris.modded.ModdedBlockBreakHandler; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BlockItem.class) +public final class BlockItemMixin { + @Inject(method = "placeBlock", at = @At("RETURN")) + private void iris$clearTreeProvenance( + BlockPlaceContext context, + BlockState state, + CallbackInfoReturnable info + ) { + if (info.getReturnValue() && context.getLevel() instanceof ServerLevel level) { + ModdedBlockBreakHandler.clearPlacedProvenance(level, context.getClickedPos()); + } + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/BlockMixin.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/BlockMixin.java new file mode 100644 index 000000000..76fd7366d --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/BlockMixin.java @@ -0,0 +1,61 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.fabric.mixin; + +import art.arcane.iris.modded.ModdedBlockBreakHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemInstance; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; +import java.util.List; + +@Mixin(Block.class) +public class BlockMixin { + @Inject(method = "getDrops(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/entity/BlockEntity;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/item/ItemInstance;)Ljava/util/List;", at = @At("RETURN"), cancellable = true) + private static void iris$applyBlockDrops(BlockState state, ServerLevel level, BlockPos position, BlockEntity blockEntity, + Entity breaker, ItemInstance tool, CallbackInfoReturnable> info) { + if (!(breaker instanceof Player)) { + return; + } + ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.completePrepared(level, position); + if (result == null) { + return; + } + if (result.routeCombinedDrops(info.getReturnValue())) { + info.setReturnValue(List.of()); + return; + } + List drops = result.replaceVanillaDrops() + ? new ArrayList<>() + : new ArrayList<>(info.getReturnValue()); + drops.addAll(result.drops()); + info.setReturnValue(drops); + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/PackRepositoryMixin.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/PackRepositoryMixin.java new file mode 100644 index 000000000..1b2669521 --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/PackRepositoryMixin.java @@ -0,0 +1,49 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.fabric.mixin; + +import art.arcane.iris.fabric.FabricForcedDatapackSources; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; +import net.minecraft.server.packs.repository.ServerPacksSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +@Mixin(PackRepository.class) +public class PackRepositoryMixin { + @Inject(method = "", at = @At("RETURN")) + private void iris$addForcedDatapackSource(RepositorySource[] sources, CallbackInfo info) { + for (RepositorySource source : sources) { + if (source instanceof ServerPacksSource) { + FabricForcedDatapackSources.attach((PackRepository) (Object) this); + return; + } + } + // Client resource-pack repositories legitimately have no ServerPacksSource; a missing server-data + // repository is reported once at boot by ModdedForcedDatapack.verifyInjected(). + LoggerFactory.getLogger("Iris").debug( + "Iris forced datapack source not attached: no ServerPacksSource among {} source(s) {}", + sources.length, Arrays.toString(sources)); + } +} diff --git a/adapters/fabric/src/main/resources/fabric.mod.json b/adapters/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 000000000..bb8a25631 --- /dev/null +++ b/adapters/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": 1, + "id": "irisworldgen", + "version": "${version}", + "name": "Iris", + "description": "Iris World Generation Engine (Fabric adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)", + "authors": ["Arcane Arts (Volmit Software)"], + "contributors": ["cyberpwn", "NextdoorPsycho", "Vatuu"], + "contact": { + "homepage": "https://docs.volmit.com/iris/", + "issues": "https://github.com/VolmitSoftware/Iris/issues", + "sources": "https://github.com/VolmitSoftware/Iris" + }, + "license": "GPL-3.0", + "icon": "assets/irisworldgen/icon.png", + "environment": "*", + "accessWidener": "irisworldgen.accesswidener", + "mixins": [ + "irisworldgen.mixins.json", + "irisworldgen.entity.mixins.json", + { + "config": "irisworldgen.client.mixins.json", + "environment": "client" + } + ], + "entrypoints": { + "main": ["art.arcane.iris.fabric.IrisFabricBootstrap"], + "client": ["art.arcane.iris.fabric.IrisFabricClient"] + }, + "jars": [ + { "file": "META-INF/jars/fabric-api-base.jar" }, + { "file": "META-INF/jars/fabric-registry-sync-v0.jar" }, + { "file": "META-INF/jars/fabric-resource-loader-v1.jar" }, + { "file": "META-INF/jars/fabric-lifecycle-events-v1.jar" }, + { "file": "META-INF/jars/fabric-command-api-v2.jar" }, + { "file": "META-INF/jars/fabric-events-interaction-v0.jar" }, + { "file": "META-INF/jars/fabric-networking-api-v1.jar" }, + { "file": "META-INF/jars/fabric-rendering-v1.jar" }, + { "file": "META-INF/jars/fabric-key-mapping-api-v1.jar" }, + { "file": "META-INF/jars/fabric-permission-api-v1.jar" } + ], + "depends": { + "fabricloader": ">=0.19.3", + "minecraft": "~${minecraftVersion}", + "java": ">=25" + } +} diff --git a/adapters/fabric/src/main/resources/irisworldgen.accesswidener b/adapters/fabric/src/main/resources/irisworldgen.accesswidener new file mode 100644 index 000000000..573ad0649 --- /dev/null +++ b/adapters/fabric/src/main/resources/irisworldgen.accesswidener @@ -0,0 +1,6 @@ +accessWidener v2 official +accessible field net/minecraft/server/MinecraftServer levels Ljava/util/Map; +accessible field net/minecraft/server/MinecraftServer executor Ljava/util/concurrent/Executor; +accessible field net/minecraft/server/MinecraftServer storageSource Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess; +accessible field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set; +mutable field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set; diff --git a/adapters/fabric/src/main/resources/irisworldgen.mixins.json b/adapters/fabric/src/main/resources/irisworldgen.mixins.json new file mode 100644 index 000000000..916a8d081 --- /dev/null +++ b/adapters/fabric/src/main/resources/irisworldgen.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "art.arcane.iris.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "BlockItemMixin", + "BlockMixin", + "PackRepositoryMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/adapters/forge/build.gradle b/adapters/forge/build.gradle new file mode 100644 index 000000000..832c1c97e --- /dev/null +++ b/adapters/forge/build.gradle @@ -0,0 +1,288 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.jvm.toolchain.JavaLanguageVersion + +plugins { + id 'java' + id 'net.minecraftforge.gradle' version '7.0.31' + alias(libs.plugins.shadow) +} + +Properties rootProperties = new Properties() +file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } +String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2')) +String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) +String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.1.1')) +String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')) +Closure loaderDisplayVersion = { String loaderVersion -> + String coordinatePrefix = "${minecraftVersion}-" + if (loaderVersion.startsWith(coordinatePrefix)) { + return loaderVersion.substring(coordinatePrefix.length()) + } + + return loaderVersion +} +Closure irisArtifactName = { String platform, String targetVersion -> + return "Iris v${project.version} [${platform}] ${targetVersion}.jar" +} + +group = 'art.arcane' +version = irisVersion + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +sourceSets { + main { + java { + srcDir '../modded-common/src/main/java' + srcDir '../minecraft-common/src/main/java' + srcDir '../client-common/src/main/java' + } + resources { + srcDir '../modded-common/src/main/resources' + } + } + test { + java { + srcDir '../modded-common/src/test/java' + } + } +} + +repositories { + minecraft.mavenizer(it) + maven fg.forgeMaven + maven fg.minecraftLibsMaven + mavenCentral() + maven { url = uri('https://repo.codemc.org/repository/maven-public/') } + maven { url = uri('https://jitpack.io') } + maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') } +} + +configurations { + bundle { + canBeConsumed = false + canBeResolved = true + } + devBundle { + canBeConsumed = false + canBeResolved = true + } + runBundle { + canBeConsumed = false + canBeResolved = true + } +} + +configurations.named('bundle').configure { + exclude(group: 'com.google.code.gson') + exclude(group: 'com.google.guava') + exclude(group: 'commons-io') + exclude(group: 'org.apache.commons', module: 'commons-lang3') + exclude(group: 'it.unimi.dsi', module: 'fastutil') + exclude(group: 'org.slf4j') + exclude(group: 'org.apache.logging.log4j') + exclude(group: 'de.crazydev22.slimjar.helper') + exclude(group: 'de.crazydev22.slimjar') + exclude(group: 'io.github.slimjar') +} + +configurations.named('runBundle').configure { + exclude(group: 'de.crazydev22.slimjar.helper') + exclude(group: 'de.crazydev22.slimjar') + exclude(group: 'io.github.slimjar') +} + +configurations.compileClasspath.extendsFrom(configurations.devBundle) +configurations.runtimeClasspath.extendsFrom(configurations.runBundle) +configurations.testCompileClasspath.extendsFrom(configurations.compileClasspath, configurations.devBundle) +configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath, configurations.runBundle) + +['compileClasspath', 'runtimeClasspath', 'testCompileClasspath', 'testRuntimeClasspath'].each { String name -> + configurations.named(name).configure { + resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { + selectHighestVersion() + } + } +} + +dependencies { + implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}") + testImplementation('junit:junit:4.13.2') + compileOnly('org.slf4j:slf4j-api:2.0.17') + compileOnly(libs.spigot) { + transitive = false + } + add('bundle', volmLibCoordinate) { + transitive = false + } + add('devBundle', volmLibCoordinate) { + transitive = false + } + add('runBundle', volmLibCoordinate) { + transitive = false + } + add('bundle', libs.zip) { + transitive = false + } + add('devBundle', libs.zip) { + transitive = false + } + add('runBundle', libs.zip) { + transitive = false + } + + List shared = [ + "art.arcane:core:${irisVersion}".toString(), + "art.arcane:spi:${irisVersion}".toString(), + libs.paralithic, + libs.lru, + libs.caffeine, + libs.dom4j, + libs.jaxen, + libs.sentry + ] + shared.each { Object notation -> + add('bundle', notation) + add('devBundle', notation) + add('runBundle', notation) + } +} + +minecraft { + accessTransformer.from(file('src/main/resources/META-INF/accesstransformer.cfg')) + runs { + register('client') { + workingDir = layout.projectDirectory.dir('run') + jvmArgs('-Xmx8G') + args('--mixin.config', 'irisworldgen.entity.mixins.json', + '--mixin.config', 'irisworldgen.client.mixins.json') + } + register('server') { + workingDir = layout.projectDirectory.dir('run') + String parity = providers.gradleProperty('irisParity').getOrNull() + if (parity != null) { + systemProperty('iris.parity', parity) + } + String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull() + if (parityGolden != null) { + systemProperty('iris.parity.golden', parityGolden) + } + String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull() + if (parityDeep != null) { + systemProperty('iris.parity.deep', parityDeep) + } + String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull() + if (worldCheck != null) { + systemProperty('iris.worldcheck', worldCheck) + } + jvmArgs('-Xmx8G') + args('--mixin.config', 'irisworldgen.entity.mixins.json', '--nogui') + } + } +} + +tasks.named('compileJava', JavaCompile).configure { + options.encoding = 'UTF-8' + options.release.set(25) +} + +tasks.named('test').configure { + systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath) +} + +processResources { + inputs.property('version', project.version) + inputs.property('minecraftVersion', minecraftVersion) + filesMatching('META-INF/mods.toml') { + expand('version': project.version, 'minecraftVersion': minecraftVersion) + } +} + +tasks.named('shadowJar', ShadowJar).configure { + doFirst { + delete(fileTree(layout.buildDirectory.dir('libs')) { + include('Iris v* [Forge] *.jar') + include('iris-forge-*.jar') + }) + delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar")) + } + manifest { + // FML 26.2 has no TOML mixin handling at all (ModFileParser reads neither [[mixins]] nor + // accessTransformers on Forge). This manifest attribute is the only registration mechanism; + // do not move it into mods.toml. + attributes('MixinConfigs': 'irisworldgen.entity.mixins.json,irisworldgen.client.mixins.json') + } + archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")) + configurations = [project.configurations.named('bundle').get()] + from(sourceSets.main.output) + mergeServiceFiles() + exclude('META-INF/maven/**') + exclude('META-INF/proguard/**') + exclude('META-INF/*.SF') + exclude('META-INF/*.DSA') + exclude('META-INF/*.RSA') + // The only multi-release entry any bundled dependency contributes is an OSGi manifest. Keeping + // it forces `Multi-Release: true` onto the mod jar, which makes the loaders treat the whole + // archive as versioned and re-scan it per release directory for nothing. + exclude('META-INF/versions/**') + addMultiReleaseAttribute.set(false) + // Invalid package name on Java 9+ ('enum' is a keyword); a module-path scan chokes on it. + exclude('org/apache/commons/lang/enum/**') + // Compile-only annotation carriers. Shipping them duplicates packages other mods also ship. + exclude('org/jspecify/**') + exclude('com/google/errorprone/**') + exclude('com/google/j2objc/**') + exclude('org/checkerframework/**') + exclude('org/jetbrains/annotations/**') + exclude('org/intellij/lang/**') + // The Bukkit platform binding cannot load without org.bukkit on the classpath, which no mod + // loader has. Shipping it only adds dead classes that reference a missing package. + exclude('art/arcane/iris/platform/bukkit/**') + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm') + relocate('io.sentry', 'art.arcane.iris.shadow.sentry') + // Split-package guard. A co-installed mod that ships any of these at their original coordinates + // puts two copies of the same package on one module layer, which the loaders reject at boot + // (Terra ships paralithic; caffeine and dom4j are common in mod dependency trees). First-party + // art.arcane.volmlib is deliberately NOT relocated. + relocate('com.github.benmanes.caffeine', 'art.arcane.iris.shadow.caffeine') + relocate('com.googlecode.concurrentlinkedhashmap', 'art.arcane.iris.shadow.clhm') + relocate('com.dfsek.paralithic', 'art.arcane.iris.shadow.paralithic') + relocate('org.dom4j', 'art.arcane.iris.shadow.dom4j') + relocate('org.jaxen', 'art.arcane.iris.shadow.jaxen') + relocate('org.zeroturnaround.zip', 'art.arcane.iris.shadow.ztzip') +} + +// The thin `jar` output carries valid mods.toml metadata plus the MixinConfigs manifest attribute +// but bundles zero dependencies. Installing it is an instant NoClassDefFoundError, and it sits in +// build/libs right next to the real artifact. shadowJar is the only shippable Forge jar. +tasks.named('jar').configure { + enabled = false +} + +tasks.named('assemble').configure { + dependsOn(tasks.named('shadowJar')) +} diff --git a/adapters/forge/gradle.properties b/adapters/forge/gradle.properties new file mode 100644 index 000000000..2be0c18a2 --- /dev/null +++ b/adapters/forge/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.caching=true +org.gradle.configuration-cache=false +net.minecraftforge.gradle.merge-source-sets=true diff --git a/adapters/forge/settings.gradle b/adapters/forge/settings.gradle new file mode 100644 index 000000000..d24d880c0 --- /dev/null +++ b/adapters/forge/settings.gradle @@ -0,0 +1,52 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'MinecraftForge' + url = uri('https://maven.minecraftforge.net/') + } + mavenCentral() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + +rootProject.name = 'iris-forge' + +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files('../../gradle/libs.versions.toml')) + } + } +} + +// Shared VolmLib source resolution; see gradle/volmlib-resolution.settings.gradle. +apply from: new File(settingsDir, '../../gradle/volmlib-resolution.settings.gradle').canonicalFile + +includeBuild('../..') { + dependencySubstitution { + substitute(module('art.arcane:core')).using(project(':core')) + substitute(module('art.arcane:spi')).using(project(':spi')) + } +} diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeClientProtocol.java b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeClientProtocol.java new file mode 100644 index 000000000..fdef8f260 --- /dev/null +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeClientProtocol.java @@ -0,0 +1,31 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.forge; + +import art.arcane.iris.client.IrisClient; +import net.minecraftforge.event.network.CustomPayloadEvent; + +public final class ForgeClientProtocol { + private ForgeClientProtocol() { + } + + public static void handleInbound(CustomPayloadEvent.Context context, byte[] data) { + context.enqueueWork(() -> IrisClient.onInbound(data)); + } +} diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeModdedLoader.java b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeModdedLoader.java new file mode 100644 index 000000000..663cfacb6 --- /dev/null +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeModdedLoader.java @@ -0,0 +1,120 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.forge; + +import art.arcane.iris.modded.ModdedLoader; +import art.arcane.iris.modded.service.ModdedTreeFellerService; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.util.Result; +import net.minecraftforge.event.level.BlockEvent; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLLoader; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.forgespi.language.IModFileInfo; +import net.minecraftforge.server.ServerLifecycleHooks; +import net.minecraftforge.server.permission.PermissionAPI; +import net.minecraftforge.server.permission.nodes.PermissionNode; +import net.minecraftforge.server.permission.nodes.PermissionTypes; + +import java.io.File; +import java.nio.file.Path; + +public final class ForgeModdedLoader implements ModdedLoader { + public static final PermissionNode TREE_FELLER_PERMISSION = new PermissionNode<>( + "irisworldgen", + "treefeller", + PermissionTypes.BOOLEAN, + (player, playerId, contexts) -> + player != null && Commands.LEVEL_GAMEMASTERS.check(player.permissions()) + ); + + @Override + public String platformName() { + return "forge"; + } + + @Override + public String minecraftVersion() { + return FMLLoader.versionInfo().mcVersion(); + } + + @Override + public String modVersion() { + return ModList.getModContainerById("irisworldgen") + .map((net.minecraftforge.fml.ModContainer container) -> container.getModInfo().getVersion().toString()) + .orElse("unknown"); + } + + @Override + public MinecraftServer currentServer() { + return ServerLifecycleHooks.getCurrentServer(); + } + + @Override + public void invalidateLevelCache(MinecraftServer server) { + server.markWorldsDirty(); + } + + @Override + public boolean clientEnvironment() { + return FMLEnvironment.dist.isClient(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public File modJar() { + IModFileInfo info = ModList.getModFileById("irisworldgen"); + return info == null ? null : info.getFile().getFilePath().toFile(); + } + + @Override + public boolean hasTreeFellerPermission(ServerPlayer player) { + return PermissionAPI.getPermission(player, TREE_FELLER_PERMISSION); + } + + @Override + public boolean canTreeFellerBreak( + ServerLevel level, + ServerPlayer player, + BlockPos position, + BlockState state + ) { + return ModdedTreeFellerService.runBreakProbe(() -> { + BlockEvent.BreakEvent event = new BlockEvent.BreakEvent( + level, + position, + state, + player, + Result.DEFAULT + ); + BlockEvent.BreakEvent.BUS.post(event); + return !event.getResult().isDenied(); + }); + } +} diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeProtocolNetworking.java b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeProtocolNetworking.java new file mode 100644 index 000000000..c04f252af --- /dev/null +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeProtocolNetworking.java @@ -0,0 +1,89 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.forge; + +import art.arcane.iris.modded.ModdedIrisPayload; +import art.arcane.iris.modded.ModdedProtocolChannel; +import art.arcane.iris.modded.ModdedProtocolHandler; +import art.arcane.iris.spi.protocol.IrisProtocol; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.network.CustomPayloadEvent; +import net.minecraftforge.network.Channel; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.NetworkProtocol; +import net.minecraftforge.network.PacketDistributor; + +public final class ForgeProtocolNetworking { + private static volatile Channel channel; + + private ForgeProtocolNetworking() { + } + + public static void register() { + Channel boundChannel = ChannelBuilder.named(Identifier.parse(IrisProtocol.CHANNEL)) + .optional() + .payloadChannel() + .protocol(NetworkProtocol.PLAY) + .bidirectional() + .add(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC, ForgeProtocolNetworking::onPayload) + .build(); + channel = boundChannel; + ModdedProtocolHandler.bindChannel(new ForgeProtocolChannel(boundChannel)); + } + + public static Channel channel() { + return channel; + } + + private static void onPayload(ModdedIrisPayload payload, CustomPayloadEvent.Context context) { + context.setPacketHandled(true); + if (context.isClientSide()) { + ForgeClientProtocol.handleInbound(context, payload.data()); + return; + } + ServerPlayer player = context.getSender(); + if (player == null) { + return; + } + ModdedProtocolHandler.onInbound(player, payload.data()); + } + + private static final class ForgeProtocolChannel implements ModdedProtocolChannel { + private final Channel channel; + + private ForgeProtocolChannel(Channel channel) { + this.channel = channel; + } + + @Override + public boolean canReceive(ServerPlayer player) { + // Forge tracks the channels the remote side announced during handshake. Without this the + // server pushes Iris payloads at vanilla clients, which drop them as unknown custom + // payloads. Mirrors the NeoForge ICommonPacketListener.hasChannel check. + return channel.isRemotePresent(player.connection.getConnection()); + } + + @Override + public void send(ServerPlayer player, ModdedIrisPayload payload) { + channel.send(payload, PacketDistributor.PLAYER.with(player)); + } + } +} diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBlockLootModifier.java b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBlockLootModifier.java new file mode 100644 index 000000000..1fecce628 --- /dev/null +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBlockLootModifier.java @@ -0,0 +1,78 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.forge; + +import art.arcane.iris.modded.ModdedBlockBreakHandler; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.loot.LootContext; +import net.minecraft.world.level.storage.loot.LootTable; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.loot.IGlobalLootModifier; +import net.minecraftforge.common.loot.LootModifier; +import org.jetbrains.annotations.NotNull; + +public final class IrisForgeBlockLootModifier extends LootModifier { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> + codecStart(instance).apply(instance, IrisForgeBlockLootModifier::new)); + + public IrisForgeBlockLootModifier(LootItemCondition[] conditions) { + super(conditions); + } + + @Override + public MapCodec codec() { + return CODEC; + } + + @NotNull + @Override + protected ObjectArrayList doApply(LootTable table, ObjectArrayList generatedLoot, LootContext context) { + Entity breaker = context.getOptionalParameter(LootContextParams.THIS_ENTITY); + BlockState state = context.getOptionalParameter(LootContextParams.BLOCK_STATE); + Vec3 origin = context.getOptionalParameter(LootContextParams.ORIGIN); + if (!(breaker instanceof Player) || state == null || origin == null) { + return generatedLoot; + } + ServerLevel level = context.getLevel(); + BlockPos position = BlockPos.containing(origin); + ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.completePrepared(level, position); + if (result == null) { + return generatedLoot; + } + if (result.routeCombinedDrops(generatedLoot)) { + generatedLoot.clear(); + return generatedLoot; + } + if (result.replaceVanillaDrops()) { + generatedLoot.clear(); + } + generatedLoot.addAll(result.drops()); + return generatedLoot; + } +} diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java new file mode 100644 index 000000000..253a01690 --- /dev/null +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java @@ -0,0 +1,147 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.forge; + +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedBlockBreakHandler; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedForcedDatapack; +import art.arcane.iris.modded.ModdedProtocolHandler; +import art.arcane.iris.modded.command.IrisModdedCommands; +import art.arcane.iris.modded.command.ModdedWandService; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.PackType; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraftforge.event.level.BlockEvent; +import net.minecraftforge.event.level.LevelEvent; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStartingEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.common.loot.IGlobalLootModifier; +import net.minecraftforge.common.util.BlockSnapshot; +import net.minecraftforge.eventbus.api.listener.Priority; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLLoader; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.server.permission.events.PermissionGatherEvent; + +import java.util.function.Predicate; + +@Mod("irisworldgen") +public final class IrisForgeBootstrap { + public IrisForgeBootstrap(FMLJavaModLoadingContext context) { + ModdedEngineBootstrap.bootCommon(new ForgeModdedLoader(), "Forge " + FMLLoader.versionInfo().forgeVersion(), () -> { + DeferredRegister> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen"); + chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC); + chunkGenerators.register(context.getModBusGroup()); + DeferredRegister> lootModifiers = DeferredRegister.create(ForgeRegistries.GLOBAL_LOOT_MODIFIER_SERIALIZERS, "irisworldgen"); + lootModifiers.register("block_drops", () -> IrisForgeBlockLootModifier.CODEC); + lootModifiers.register(context.getModBusGroup()); + }); + + ForgeProtocolNetworking.register(); + + // Dist gate for every client-tainted class. DistExecutor is gone in Forge 26.2 (65.x) - only + // net.minecraftforge.api.distmarker.Dist survives, from mergetool-api - so the guard is this branch. + // It is equally safe: IrisForgeClient appears only as an invokestatic target, so the JVM resolves the + // constant-pool entry lazily on first execution and a dedicated server never loads the class. + if (FMLEnvironment.dist == Dist.CLIENT) { + IrisForgeClient.init(); + } + + ServerAboutToStartEvent.BUS.addListener((ServerAboutToStartEvent event) -> ModdedEngineBootstrap.serverAboutToStart(event.getServer())); + ServerStartingEvent.BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer())); + ServerStartedEvent.BUS.addListener((ServerStartedEvent event) -> ModdedEngineBootstrap.serverStarted(event.getServer())); + ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop()); + LevelEvent.Load.BUS.addListener((LevelEvent.Load event) -> { + if (event.getLevel() instanceof ServerLevel level) { + ModdedEngineBootstrap.levelLoaded(level); + } + }); + LevelEvent.Unload.BUS.addListener((LevelEvent.Unload event) -> { + if (event.getLevel() instanceof ServerLevel level) { + ModdedEngineBootstrap.levelUnloaded(level); + } + }); + PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + ModdedProtocolHandler.onPlayerJoin(player); + } + }); + PlayerEvent.PlayerLoggedOutEvent.BUS.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + ModdedProtocolHandler.onPlayerDisconnect(player); + } + }); + AddPackFindersEvent.BUS.addListener((AddPackFindersEvent event) -> { + if (event.getPackType() == PackType.SERVER_DATA) { + event.addRepositorySource(ModdedForcedDatapack.repositorySource()); + } + }); + RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher())); + PermissionGatherEvent.Nodes.BUS.addListener((PermissionGatherEvent.Nodes event) -> + event.addNodes(ForgeModdedLoader.TREE_FELLER_PERMISSION)); + BlockEvent.BreakEvent.BUS.addListener((BlockEvent.BreakEvent event) -> { + if (event.getLevel() instanceof ServerLevel level + && event.getPlayer() instanceof ServerPlayer player + && !event.getResult().isDenied()) { + ModdedBlockBreakHandler.prepare(level, player, event.getPos(), event.getState()); + } + }); + // EventBus 7: the (priority, boolean, consumer) overload's boolean means "always cancels"; passing + // false is rejected at registration for listeners that never cancel. Use (priority, consumer). + BlockEvent.EntityPlaceEvent.BUS.addListener(Priority.MONITOR, (BlockEvent.EntityPlaceEvent event) -> { + if (!(event instanceof BlockEvent.EntityMultiPlaceEvent) + && event.getLevel() instanceof ServerLevel level) { + ModdedBlockBreakHandler.clearPlacedProvenance(level, event.getPos()); + } + }); + BlockEvent.EntityMultiPlaceEvent.BUS.addListener( + Priority.MONITOR, + (BlockEvent.EntityMultiPlaceEvent event) -> { + for (BlockSnapshot snapshot : event.getReplacedBlockSnapshots()) { + if (snapshot.getLevel() instanceof ServerLevel level) { + ModdedBlockBreakHandler.clearPlacedProvenance(level, snapshot.getPos()); + } + } + } + ); + PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate) (PlayerInteractEvent.LeftClickBlock event) -> + ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())); + PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate) (PlayerInteractEvent.RightClickBlock event) -> + ModdedWandService.useBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())); + TickEvent.ServerTickEvent.Post.BUS.addListener((TickEvent.ServerTickEvent.Post event) -> { + ModdedEngineBootstrap.tick(event.server()); + ModdedWandService.serverTick(event.server()); + }); + } +} diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeClient.java b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeClient.java new file mode 100644 index 000000000..0dedbcdd4 --- /dev/null +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeClient.java @@ -0,0 +1,64 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.forge; + +import art.arcane.iris.client.IrisClient; +import art.arcane.iris.client.IrisClientHud; +import art.arcane.iris.client.IrisClientKeybinds; +import art.arcane.iris.modded.ModdedIrisPayload; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.network.Channel; +import net.minecraftforge.network.PacketDistributor; + +public final class IrisForgeClient { + private IrisForgeClient() { + } + + public static void init() { + IrisClient.bindSender(IrisForgeClient::sendToServer); + AddGuiOverlayLayersEvent.BUS.addListener((AddGuiOverlayLayersEvent event) -> + event.getLayeredDraw().add(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics))); + RegisterKeyMappingsEvent.BUS.addListener((RegisterKeyMappingsEvent event) -> { + event.register(IrisClientKeybinds.TOGGLE_HUD); + event.register(IrisClientKeybinds.OPEN_MAP); + event.register(IrisClientKeybinds.TOGGLE_WHAT); + }); + ClientPlayerNetworkEvent.LoggingIn.BUS.addListener((ClientPlayerNetworkEvent.LoggingIn event) -> IrisClient.onWorldJoin()); + ClientPlayerNetworkEvent.LoggingOut.BUS.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> IrisClient.onDisconnect()); + InputEvent.Key.BUS.addListener((InputEvent.Key event) -> IrisClientKeybinds.pollToggle()); + TickEvent.ClientTickEvent.Post.BUS.addListener((TickEvent.ClientTickEvent.Post event) -> { + IrisClientKeybinds.pollToggle(); + IrisClient.tick(); + IrisClientHud.tick(); + }); + } + + private static void sendToServer(byte[] frame) { + Channel channel = ForgeProtocolNetworking.channel(); + if (channel == null) { + return; + } + channel.send(new ModdedIrisPayload(frame), PacketDistributor.SERVER.noArg()); + } +} diff --git a/adapters/forge/src/main/resources/META-INF/accesstransformer.cfg b/adapters/forge/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 000000000..5a2faeeb3 --- /dev/null +++ b/adapters/forge/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,3 @@ +public net.minecraft.server.MinecraftServer levels +public net.minecraft.server.MinecraftServer executor +public net.minecraft.server.MinecraftServer storageSource diff --git a/adapters/forge/src/main/resources/META-INF/mods.toml b/adapters/forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 000000000..a55737f00 --- /dev/null +++ b/adapters/forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,28 @@ +modLoader = "javafml" +loaderVersion = "[65,66)" +license = "GPL-3.0" +issueTrackerURL = "https://github.com/VolmitSoftware/Iris/issues" + +[[mods]] +modId = "irisworldgen" +version = "${version}" +displayName = "Iris" +description = "Iris World Generation Engine (Forge adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)" +authors = "Arcane Arts (Volmit Software)" +credits = "cyberpwn, NextdoorPsycho, Vatuu" +displayURL = "https://docs.volmit.com/iris/" +logoFile = "assets/irisworldgen/icon.png" + +[[dependencies.irisworldgen]] +modId = "forge" +mandatory = true +versionRange = "[65,66)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.irisworldgen]] +modId = "minecraft" +mandatory = true +versionRange = "[${minecraftVersion}]" +ordering = "NONE" +side = "BOTH" diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/ForcedStructureChunkGenerator.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/ForcedStructureChunkGenerator.java new file mode 100644 index 000000000..d59ed631f --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/ForcedStructureChunkGenerator.java @@ -0,0 +1,94 @@ +package art.arcane.iris.nativegen; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.FixedBiomeSource; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.blending.Blender; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +final class ForcedStructureChunkGenerator extends ChunkGenerator { + private final ChunkGenerator delegate; + + ForcedStructureChunkGenerator(ChunkGenerator delegate, Holder sourceBiome) { + super(new FixedBiomeSource(sourceBiome)); + this.delegate = delegate; + } + + @Override + protected MapCodec codec() { + return MapCodec.unit(this); + } + + @Override + public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, + BiomeManager biomeManager, StructureManager structureManager, + ChunkAccess chunk) { + delegate.applyCarvers(region, seed, randomState, biomeManager, structureManager, chunk); + } + + @Override + public void buildSurface(WorldGenRegion level, StructureManager structureManager, + RandomState randomState, ChunkAccess protoChunk) { + delegate.buildSurface(level, structureManager, randomState, protoChunk); + } + + @Override + public void spawnOriginalMobs(WorldGenRegion worldGenRegion) { + delegate.spawnOriginalMobs(worldGenRegion); + } + + @Override + public int getGenDepth() { + return delegate.getGenDepth(); + } + + @Override + public CompletableFuture fillFromNoise(Blender blender, RandomState randomState, + StructureManager structureManager, + ChunkAccess centerChunk) { + return delegate.fillFromNoise( + blender, randomState, structureManager, centerChunk); + } + + @Override + public int getSeaLevel() { + return delegate.getSeaLevel(); + } + + @Override + public int getMinY() { + return delegate.getMinY(); + } + + @Override + public int getBaseHeight(int x, int z, Heightmap.Types type, + LevelHeightAccessor heightAccessor, RandomState randomState) { + return delegate.getBaseHeight(x, z, type, heightAccessor, randomState); + } + + @Override + public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, + RandomState randomState) { + return delegate.getBaseColumn(x, z, heightAccessor, randomState); + } + + @Override + public void addDebugScreenInfo(List result, RandomState randomState, + BlockPos feetPos) { + delegate.addDebugScreenInfo(result, randomState, feetPos); + } + +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeBuildFutures.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeBuildFutures.java new file mode 100644 index 000000000..d4782f6f1 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeBuildFutures.java @@ -0,0 +1,31 @@ +package art.arcane.iris.nativegen; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * Shared waiter for the racy build caches in this package: the loser of a putIfAbsent race + * must surface the same exception the builder threw, not a CompletionException wrapper. + */ +final class NativeBuildFutures { + private NativeBuildFutures() { + } + + static T awaitBuild(CompletableFuture future, String what) { + try { + return future.join(); + } catch (CompletionException error) { + Throwable cause = error.getCause(); + if (cause instanceof RuntimeException runtime) { + throw runtime; + } + if (cause instanceof Error fatal) { + throw fatal; + } + if (cause == null) { + throw error; + } + throw new IllegalStateException(what + " failed", cause); + } + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFactory.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFactory.java new file mode 100644 index 000000000..e0ab72059 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFactory.java @@ -0,0 +1,284 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.NativeStructureStartPlan; +import art.arcane.iris.engine.object.IrisJigsawConfiguration; +import art.arcane.iris.engine.object.IrisJigsawHeightmap; +import art.arcane.iris.engine.object.IrisJigsawLiquidSettings; +import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.Locale; +import java.util.Objects; +import java.util.function.IntBinaryOperator; +import java.util.function.Predicate; + +public final class NativeStructureFactory { + private NativeStructureFactory() { + } + + public static StructureStart generate(GenerationContext context, Holder sourceHolder, + NativeStructureStartPlan plan, int references) { + Structure source = sourceHolder.value(); + Structure configured = configure( + context.registryAccess(), source, plan.source().getJigsaw(), + plan.placement().isUnderground(), plan.baseY()); + Holder sourceBiome = source.biomes().stream().findFirst() + .or(() -> context.biomeSource().possibleBiomes().stream().findFirst()) + .orElseThrow(() -> new IllegalStateException("Configured native structure '" + + plan.source().getStructure() + "' has no usable generation biome")); + ChunkGenerator forcedGenerator = new ForcedStructureChunkGenerator( + context.generator(), sourceBiome); + StructureStart generated = configured.generate( + sourceHolder, + context.levelKey(), + context.registryAccess(), + forcedGenerator, + forcedGenerator.getBiomeSource(), + context.randomState(), + context.templateManager(), + context.levelSeed(), + new ChunkPos(plan.chunkX(), plan.chunkZ()), + references, + context.heightAccessor(), + context.biomePredicate() + ); + if (generated == null || !generated.isValid()) { + return StructureStart.INVALID_START; + } + StructureStart positioned; + if (plan.placement().isUnderground()) { + positioned = NativeStructureVerticalPlacer.relocateToMinY( + generated, source, plan.baseY(), context.heightAccessor()); + } else { + NativeStructureVerticalPlacer.applyVerticalPlacement( + generated, + plan.source().getStructure(), + 0, + context.seaLevel(), + context.heightAccessor().getMinY(), + Math.addExact(context.heightAccessor().getMinY(), context.heightAccessor().getHeight()), + false, + false, + null, + context.surfaceHeight()); + positioned = generated; + } + if (!positioned.isValid()) { + return StructureStart.INVALID_START; + } + return NativeStructureReferenceEnvelope.wrapForPublication( + positioned, + source, + references, + plan.placement().resolvedTerrain(), + plan.source().getStructure() + ); + } + + public static JigsawSourceMetadata sourceMetadata(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + JigsawStructure source) { + CompoundTag sourceTag = encodeJigsaw(registryAccess, source); + return new JigsawSourceMetadata( + sourceDistance(sourceTag, "horizontal"), + horizontalReferenceExpansion(source), + NativeStructureTemplatePoolBounds.sourceHorizontalSpan( + registryAccess, templateManager, source)); + } + + public static int templatePoolHorizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + String templatePoolKey) { + return NativeStructureTemplatePoolBounds.horizontalSpan( + registryAccess, templateManager, templatePoolKey); + } + + public static int jigsawStartPoolHorizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + JigsawStructure source, + String templatePoolKey) { + return NativeStructureTemplatePoolBounds.horizontalSpan( + registryAccess, templateManager, source, templatePoolKey); + } + + static Structure configure(RegistryAccess registryAccess, Structure source, + IrisJigsawConfiguration configuration, + boolean underground, int baseY) { + Objects.requireNonNull(source, "Native structure source must not be null"); + if (!(source instanceof JigsawStructure sourceJigsaw)) { + if (configuration != null) { + throw new IllegalStateException("Jigsaw options require a registered jigsaw structure, found " + + source.getClass().getName()); + } + return source; + } + RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, registryAccess); + CompoundTag structureTag = encodeJigsaw(registryOps, sourceJigsaw); + CompoundTag configuredTag = structureTag.copy(); + if (underground) { + CompoundTag height = new CompoundTag(); + height.putInt("absolute", baseY); + configuredTag.put("start_height", height); + configuredTag.remove("project_start_to_heightmap"); + } + applyConfiguration(configuredTag, configuration); + Structure decoded = Structure.DIRECT_CODEC.parse(registryOps, configuredTag).getOrThrow(); + if (!(decoded instanceof JigsawStructure configured)) { + throw new IllegalStateException("Configured native jigsaw codec produced " + + decoded.getClass().getName()); + } + return configured; + } + + private static CompoundTag encodeJigsaw(RegistryAccess registryAccess, + JigsawStructure source) { + return encodeJigsaw(RegistryOps.create(NbtOps.INSTANCE, registryAccess), source); + } + + private static CompoundTag encodeJigsaw(RegistryOps registryOps, + JigsawStructure source) { + Tag encoded = Structure.DIRECT_CODEC.encodeStart(registryOps, source).getOrThrow(); + if (!(encoded instanceof CompoundTag structureTag)) { + throw new IllegalStateException("Native jigsaw codec did not produce a compound"); + } + return structureTag; + } + + private static void applyConfiguration(CompoundTag tag, IrisJigsawConfiguration configuration) { + if (configuration == null) { + return; + } + if (configuration.getStartPool() != null && !configuration.getStartPool().isBlank()) { + tag.putString("start_pool", configuration.getStartPool().trim()); + } + if (configuration.getStartJigsawName() != null + && !configuration.getStartJigsawName().isBlank()) { + if ("NONE".equalsIgnoreCase(configuration.getStartJigsawName().trim())) { + tag.remove("start_jigsaw_name"); + } else { + tag.putString("start_jigsaw_name", configuration.getStartJigsawName().trim()); + } + } + if (configuration.getMaxDepth() != null) { + tag.putInt("size", configuration.getMaxDepth()); + } + applyDistance(tag, configuration); + if (configuration.getUseExpansionHack() != null) { + tag.putBoolean("use_expansion_hack", configuration.getUseExpansionHack()); + } + applyHeightmap(tag, configuration.getProjectStartToHeightmap()); + applyDimensionPadding(tag, configuration); + applyLiquidSettings(tag, configuration.getLiquidSettings()); + } + + private static void applyDistance(CompoundTag tag, IrisJigsawConfiguration configuration) { + Integer horizontal = configuration.getMaxDistanceHorizontal(); + Integer vertical = configuration.getMaxDistanceVertical(); + if (horizontal == null && vertical == null) { + return; + } + int resolvedHorizontal = horizontal == null ? sourceDistance(tag, "horizontal") : horizontal; + int resolvedVertical = vertical == null ? sourceDistance(tag, "vertical") : vertical; + if (resolvedHorizontal == resolvedVertical) { + tag.putInt("max_distance_from_center", resolvedHorizontal); + return; + } + CompoundTag distance = new CompoundTag(); + distance.putInt("horizontal", resolvedHorizontal); + distance.putInt("vertical", resolvedVertical); + tag.put("max_distance_from_center", distance); + } + + static int sourceDistance(CompoundTag tag, String axis) { + Tag raw = tag.get("max_distance_from_center"); + if (raw instanceof CompoundTag compound) { + return compound.getIntOr(axis, 128); + } + return tag.getIntOr("max_distance_from_center", 128); + } + + static int horizontalReferenceExpansion(Structure structure) { + BoundingBox content = new BoundingBox(0, 0, 0, 0, 0, 0); + BoundingBox adjusted = structure.adjustBoundingBox(content); + int expansion = Math.max(content.minX() - adjusted.minX(), adjusted.maxX() - content.maxX()); + expansion = Math.max(expansion, content.minZ() - adjusted.minZ()); + return Math.max(0, Math.max(expansion, adjusted.maxZ() - content.maxZ())); + } + + private static void applyHeightmap(CompoundTag tag, IrisJigsawHeightmap heightmap) { + if (heightmap == null || heightmap == IrisJigsawHeightmap.SOURCE) { + return; + } + if (heightmap == IrisJigsawHeightmap.NONE) { + tag.remove("project_start_to_heightmap"); + return; + } + tag.putString("project_start_to_heightmap", heightmap.name()); + } + + private static void applyDimensionPadding(CompoundTag tag, IrisJigsawConfiguration configuration) { + Integer bottom = configuration.getDimensionPaddingBottom(); + Integer top = configuration.getDimensionPaddingTop(); + if (bottom == null && top == null) { + return; + } + int resolvedBottom = bottom == null ? sourcePadding(tag, "bottom") : bottom; + int resolvedTop = top == null ? sourcePadding(tag, "top") : top; + if (resolvedBottom == resolvedTop) { + tag.putInt("dimension_padding", resolvedBottom); + return; + } + CompoundTag padding = new CompoundTag(); + padding.putInt("bottom", resolvedBottom); + padding.putInt("top", resolvedTop); + tag.put("dimension_padding", padding); + } + + private static int sourcePadding(CompoundTag tag, String side) { + Tag raw = tag.get("dimension_padding"); + if (raw instanceof CompoundTag compound) { + return compound.getIntOr(side, 0); + } + return tag.getIntOr("dimension_padding", 0); + } + + private static void applyLiquidSettings(CompoundTag tag, IrisJigsawLiquidSettings settings) { + if (settings == null || settings == IrisJigsawLiquidSettings.SOURCE) { + return; + } + tag.putString("liquid_settings", settings.name().toLowerCase(Locale.ROOT)); + } + + public record GenerationContext( + RegistryAccess registryAccess, + ChunkGenerator generator, + BiomeSource biomeSource, + RandomState randomState, + StructureTemplateManager templateManager, + long levelSeed, + ResourceKey levelKey, + LevelHeightAccessor heightAccessor, + Predicate> biomePredicate, + int seaLevel, + IntBinaryOperator surfaceHeight + ) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFoundationBuilder.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFoundationBuilder.java new file mode 100644 index 000000000..161d9e436 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFoundationBuilder.java @@ -0,0 +1,212 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.object.IrisStructureStiltSettings; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SupportType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Objects; +import java.util.function.IntBinaryOperator; + +public final class NativeStructureFoundationBuilder { + private static final int FOUNDATION_VERTICAL_TOLERANCE = 1; + + private NativeStructureFoundationBuilder() { + } + + private static List foundationEnvelope(BoundingBox area, StructureStart start) { + BoundingBox structure = NativeStructureReferenceEnvelope.contentBounds(start); + int minX = Math.max(area.minX(), structure.minX()); + int minZ = Math.max(area.minZ(), structure.minZ()); + int maxX = Math.min(area.maxX(), structure.maxX()); + int maxZ = Math.min(area.maxZ(), structure.maxZ()); + if (minX > maxX || minZ > maxZ) { + return List.of(); + } + List pieces = start.getPieces(); + List columns = new ArrayList<>((maxX - minX + 1) * (maxZ - minZ + 1)); + BitSet envelope = new BitSet(area.getYSpan()); + for (int z = minZ; z <= maxZ; z++) { + for (int x = minX; x <= maxX; x++) { + envelope.clear(); + markFoundationEnvelope(envelope, pieces, area, x, z); + int cellCount = envelope.cardinality(); + if (cellCount == 0) { + continue; + } + int[] ys = new int[cellCount]; + int cell = 0; + for (int bit = envelope.nextSetBit(0); bit >= 0; bit = envelope.nextSetBit(bit + 1)) { + ys[cell++] = area.minY() + bit; + } + columns.add(new FoundationColumn(x, z, ys)); + } + } + return List.copyOf(columns); + } + + private static void markFoundationEnvelope(BitSet envelope, List pieces, BoundingBox area, + int x, int z) { + for (StructurePiece piece : pieces) { + BoundingBox bounds = piece.getBoundingBox(); + if (x < bounds.minX() || x > bounds.maxX() || z < bounds.minZ() || z > bounds.maxZ()) { + continue; + } + int groundY = bounds.minY(); + if (piece instanceof PoolElementStructurePiece poolPiece) { + groundY += poolPiece.getGroundLevelDelta(); + if (groundY < bounds.minY()) { + continue; + } + } + int minY = Math.max(area.minY(), bounds.minY()); + int maxY = Math.min(area.maxY(), Math.min(bounds.maxY(), groundY + FOUNDATION_VERTICAL_TOLERANCE)); + if (minY <= maxY) { + envelope.set(minY - area.minY(), maxY - area.minY() + 1); + } + } + } + + static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureStiltSettings settings, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver, + IntBinaryOperator surfaceHeight, + boolean surfaceStructure) { + Objects.requireNonNull(surfaceHeight, "Structure stilts require an Iris terrain height resolver"); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + int structureHash = structureId == null ? 0 : structureId.hashCode(); + RNG rng = new RNG(world.getSeed() ^ structureHash); + for (FoundationColumn column : foundationEnvelope(area, start)) { + if (!isStiltColumn(column.x(), column.z(), settings.getSpacing())) { + continue; + } + int foundationY = findFoundationY(world, column, position); + if (foundationY == Integer.MIN_VALUE) { + continue; + } + int terrainY = surfaceStructure + ? Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z()))) + : area.minY() - 1; + int anchorY = findStiltAnchorY( + world, column.x(), column.z(), foundationY, + Math.max(1, settings.getMaxDepth()), terrainY, area.minY(), position); + if (anchorY == Integer.MIN_VALUE) { + continue; + } + for (int y = foundationY - 1; y > anchorY; y--) { + position.set(column.x(), y, column.z()); + BlockState stilt = settings.getPalette() == null + ? Blocks.COBBLESTONE.defaultBlockState() + : Objects.requireNonNull( + paletteBlockResolver.resolve( + settings.getPalette(), rng, column.x(), y, column.z()), + "Stilt palette returned no block for " + structureId + " at " + + column.x() + "," + y + "," + column.z()); + world.setBlock(position, stilt, 2); + } + } + } + + static boolean isStiltColumn(int x, int z, int spacing) { + int resolvedSpacing = Math.max(1, spacing); + return resolvedSpacing == 1 + || Math.floorMod(x, resolvedSpacing) == 0 + && Math.floorMod(z, resolvedSpacing) == 0; + } + + static int findStiltAnchorY( + WorldGenLevel world, int x, int z, int foundationY, int maxDepth, + int terrainY, int areaMinY, BlockPos.MutableBlockPos position) { + int minimumAnchorY = Math.max( + areaMinY, Math.max(terrainY, foundationY - maxDepth - 1)); + for (int y = foundationY - 1; y >= minimumAnchorY; y--) { + BlockState state = world.getBlockState(position.set(x, y, z)); + boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + if (!vegetation && state.isFaceSturdy( + world, position, Direction.UP, SupportType.FULL)) { + return y; + } + } + return Integer.MIN_VALUE; + } + + public static StiltSupportAudit auditStiltSupport(WorldGenLevel world, BoundingBox area, + StructureStart start, BlockState expectedStilt, + IntBinaryOperator surfaceHeight) { + Objects.requireNonNull(expectedStilt, "Expected stilt state must not be null"); + Objects.requireNonNull(surfaceHeight, "Structure stilt audit requires an Iris terrain height resolver"); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + int baseColumns = 0; + int stiltBlocks = 0; + int stiltColumns = 0; + int unsupportedColumns = 0; + for (FoundationColumn column : foundationEnvelope(area, start)) { + int foundationY = findFoundationY(world, column, position); + if (foundationY == Integer.MIN_VALUE) { + continue; + } + baseColumns++; + int terrainY = Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z()))); + boolean grounded = foundationY <= terrainY + 1; + boolean stiltColumn = false; + for (int y = foundationY - 1; y >= area.minY(); y--) { + if (y <= terrainY) { + grounded = true; + break; + } + BlockState state = world.getBlockState(position.set(column.x(), y, column.z())); + if (state.is(expectedStilt.getBlock())) { + stiltBlocks++; + stiltColumn = true; + if (y == area.minY()) { + grounded = true; + } + continue; + } + boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + grounded = state.isSolid() && !vegetation; + break; + } + if (stiltColumn) { + stiltColumns++; + } + if (!grounded) { + unsupportedColumns++; + } + } + return new StiltSupportAudit(baseColumns, stiltBlocks, stiltColumns, unsupportedColumns); + } + + private static int findFoundationY(WorldGenLevel world, FoundationColumn column, + BlockPos.MutableBlockPos position) { + for (int cell = 0; cell < column.ys().length; cell++) { + int y = column.ys()[cell]; + BlockState state = world.getBlockState(position.set(column.x(), y, column.z())); + if (state.isSolid()) { + return y; + } + } + return Integer.MIN_VALUE; + } + + private record FoundationColumn(int x, int z, int[] ys) { + } + + public record StiltSupportAudit(int baseColumns, int stiltBlocks, int stiltColumns, + int unsupportedColumns) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationException.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationException.java new file mode 100644 index 000000000..738613a3e --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationException.java @@ -0,0 +1,35 @@ +package art.arcane.iris.nativegen; + +public final class NativeStructureGenerationException extends IllegalStateException { + private NativeStructureGenerationException(FailureContext context, Throwable cause) { + super(message(context), cause); + } + + private NativeStructureGenerationException(FailureContext context) { + super(message(context)); + } + + public static NativeStructureGenerationException failure(String operation, String structureContext, + int chunkX, int chunkZ, Throwable cause) { + return new NativeStructureGenerationException( + new FailureContext(operation, structureContext, chunkX, chunkZ), cause); + } + + public static NativeStructureGenerationException failure(String operation, String structureContext, + int chunkX, int chunkZ) { + return new NativeStructureGenerationException( + new FailureContext(operation, structureContext, chunkX, chunkZ)); + } + + private static String message(FailureContext context) { + String resolvedContext = context.structureContext() == null || context.structureContext().isBlank() + ? "" + : context.structureContext(); + return "Iris native structure " + context.operation() + " failed for " + resolvedContext + + " in chunk " + context.chunkX() + "," + context.chunkZ() + + "; chunk generation was aborted to prevent partial structure state"; + } + + private record FailureContext(String operation, String structureContext, int chunkX, int chunkZ) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationKeys.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationKeys.java new file mode 100644 index 000000000..aa88abae9 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationKeys.java @@ -0,0 +1,117 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.nativegen; + +import net.minecraft.core.Holder; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public final class NativeStructureGenerationKeys { + private static final Method FREQUENCY_METHOD = resolveFrequencyMethod(); + + private NativeStructureGenerationKeys() { + } + + public static Set reachable(ServerLevel level, Set possibleBiomeKeys) { + ServerLevel activeLevel = Objects.requireNonNull(level, "Native structure reachability requires a level"); + Set possibleBiomes = Objects.requireNonNull( + possibleBiomeKeys, "Native structure reachability requires possible biome keys"); + if (!activeLevel.getServer().getWorldGenSettings().options().generateStructures()) { + return Collections.emptySet(); + } + + Set keys = new LinkedHashSet<>(); + List> structureSets = + activeLevel.getChunkSource().getGeneratorState().possibleStructureSets(); + for (Holder structureSetHolder : structureSets) { + StructureSet structureSet = structureSetHolder.value(); + if (!isEnabledPlacement(structureSet.placement())) { + continue; + } + for (StructureSet.StructureSelectionEntry entry : structureSet.structures()) { + if (entry.weight() <= 0 || !hasPossibleBiome(entry.structure().value(), possibleBiomes)) { + continue; + } + Optional> key = entry.structure().unwrapKey(); + key.ifPresent(resourceKey -> keys.add(resourceKey.identifier().toString())); + } + } + return Collections.unmodifiableSet(keys); + } + + static boolean hasPossibleBiome(Structure structure, Set possibleBiomeKeys) { + for (Holder holder : structure.biomes()) { + Optional> key = holder.unwrapKey(); + if (key.isPresent() && possibleBiomeKeys.contains(key.get().identifier().toString())) { + return true; + } + } + return false; + } + + static boolean isEnabledPlacement(StructurePlacement placement) { + return placementFrequency(placement) > 0.0F; + } + + private static float placementFrequency(StructurePlacement placement) { + StructurePlacement activePlacement = Objects.requireNonNull( + placement, "Native structure placement frequency requires a placement"); + try { + return ((Float) FREQUENCY_METHOD.invoke(activePlacement)).floatValue(); + } catch (ReflectiveOperationException error) { + throw new IllegalStateException( + "Unable to read native structure placement frequency from " + + activePlacement.getClass().getName(), error); + } + } + + private static Method resolveFrequencyMethod() { + Method frequencyMethod = null; + for (Method method : StructurePlacement.class.getDeclaredMethods()) { + if (Modifier.isStatic(method.getModifiers()) + || method.getParameterCount() != 0 + || method.getReturnType() != float.class) { + continue; + } + if (frequencyMethod != null) { + throw new IllegalStateException( + "Native structure placement exposes multiple zero-argument float accessors"); + } + frequencyMethod = method; + } + if (frequencyMethod == null || !frequencyMethod.trySetAccessible()) { + throw new IllegalStateException( + "Native structure placement frequency accessor is unavailable"); + } + return frequencyMethod; + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureLocatePersistence.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureLocatePersistence.java new file mode 100644 index 000000000..fb5279fcd --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureLocatePersistence.java @@ -0,0 +1,351 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.IrisStructureLocator; +import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.SharedConstants; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.StreamTagVisitor; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.visitors.CollectFields; +import net.minecraft.nbt.visitors.FieldSelector; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public final class NativeStructureLocatePersistence { + public static final int MAX_SELECTED_CANDIDATE_RETRIES = 64; + private static final int MAX_STORAGE_PROBES = 512; + private static final Method PAPER_LEVEL_TYPE_KEY = paperLevelTypeKey(); + + private NativeStructureLocatePersistence() { + } + + public static Probe probe(ServerLevel level, Structure structure, boolean requireUnreferenced) { + return probe(level, structure, requireUnreferenced, new ProbeBudget(MAX_STORAGE_PROBES)); + } + + public static ProbeBudget probeBudget() { + return new ProbeBudget(MAX_STORAGE_PROBES); + } + + public static Probe probe(ServerLevel level, Structure structure, boolean requireUnreferenced, + ProbeBudget budget) { + return new Probe(level, structure, requireUnreferenced, budget); + } + + public static Search search(Engine engine, String structureKey, + int blockX, int blockZ, int radius, Probe probe) { + return new Search(engine, structureKey, blockX, blockZ, radius, probe); + } + + public static final class Probe { + private final ServerLevel level; + private final Structure structure; + private final String structureKey; + private final boolean requireUnreferenced; + private final ProbeBudget budget; + private final Map storedDecisions = new HashMap<>(); + + Probe(ServerLevel level, Structure structure, boolean requireUnreferenced, + ProbeBudget budget) { + this.level = level; + this.structure = structure; + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Identifier identifier = registry.getKey(structure); + if (identifier == null) { + throw new IllegalStateException("Cannot probe an unregistered native structure"); + } + this.structureKey = identifier.toString(); + this.requireUnreferenced = requireUnreferenced; + this.budget = budget; + } + + public boolean accepts(int chunkX, int chunkZ) { + ChunkAccess loaded = level.getChunkSource().getChunk( + chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); + if (loaded != null) { + StructureStart start = loaded.getStartForStructure(structure); + return start != null && start.isValid() + && (!requireUnreferenced || start.canBeReferenced()); + } + + ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + Boolean storedDecision = storedDecisions.get(chunkPos.pack()); + if (storedDecision != null) { + return storedDecision; + } + boolean accepted = budget.acceptsStored( + level, chunkPos, structureKey, requireUnreferenced); + storedDecisions.put(chunkPos.pack(), accepted); + return accepted; + } + + public StructureStart verifySelected(int chunkX, int chunkZ) { + ChunkAccess chunk = level.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS); + StructureStart start = chunk.getStartForStructure(structure); + if (start == null || !start.isValid()) { + return null; + } + if (requireUnreferenced) { + if (!start.canBeReferenced()) { + return null; + } + } + return start; + } + + private void reference(StructureStart start) { + if (requireUnreferenced) { + level.structureManager().addReference(start); + } + } + } + + public static final class ProbeBudget { + private final int maximum; + private final Map storedChunks = new HashMap<>(); + private final ChunkStorageScanner scanner; + private final StoredChunkDatafixer datafixer; + private int used; + private boolean scanFailureReported; + + private ProbeBudget(int maximum) { + this(maximum, null, ProbeBudget::datafix); + } + + ProbeBudget(int maximum, ChunkStorageScanner scanner) { + this(maximum, scanner, ProbeBudget::datafix); + } + + ProbeBudget(int maximum, ChunkStorageScanner scanner, + StoredChunkDatafixer datafixer) { + this.maximum = maximum; + this.scanner = scanner; + this.datafixer = Objects.requireNonNull( + datafixer, "Stored chunk datafixer must not be null"); + } + + private void claim() { + if (used >= maximum) { + throw new IrisStructureLocator.CandidateSearchLimitException(); + } + used++; + } + + private StoredChunkState storedState(ServerLevel level, ChunkPos chunkPos) { + StoredChunkState cached = storedChunks.get(chunkPos.pack()); + if (cached != null) { + return cached; + } + claim(); + CollectFields fields = new CollectFields( + new FieldSelector(IntTag.TYPE, "DataVersion"), + new FieldSelector("Level", "Structures", CompoundTag.TYPE, "Starts"), + new FieldSelector("structures", CompoundTag.TYPE, "starts")); + ChunkStorageScanner activeScanner = scanner == null + ? level.getChunkSource().chunkMap.chunkScanner()::scanChunk : scanner; + try { + activeScanner.scan(chunkPos, fields).join(); + } catch (RuntimeException error) { + if (!scanFailureReported) { + scanFailureReported = true; + IrisLogging.reportError( + "Native structure locate could not scan stored chunk state; candidates will be verified by loading their structure starts.", + error); + } + StoredChunkState unresolved = StoredChunkState.missing(); + storedChunks.put(chunkPos.pack(), unresolved); + return unresolved; + } + Tag result = fields.getResult(); + StoredChunkState resolved; + try { + resolved = result instanceof CompoundTag storedChunk + ? StoredChunkState.parse(datafixer.datafix(level, storedChunk)) + : StoredChunkState.missing(); + } catch (RuntimeException error) { + if (!scanFailureReported) { + scanFailureReported = true; + IrisLogging.reportError( + "Native structure locate could not datafix stored chunk state; candidates will be verified by loading their structure starts.", + error); + } + resolved = StoredChunkState.missing(); + } + storedChunks.put(chunkPos.pack(), resolved); + return resolved; + } + + private static CompoundTag datafix(ServerLevel level, CompoundTag storedChunk) { + int dataVersion = NbtUtils.getDataVersion(storedChunk); + int currentDataVersion = SharedConstants.getCurrentVersion().dataVersion().version(); + if (dataVersion >= currentDataVersion) { + return storedChunk; + } + if (level == null) { + throw new IllegalStateException("Stored chunk datafix requires an active level"); + } + CompoundTag context = chunkDataFixContext(level); + SimpleRegionStorage.injectDatafixingContext(storedChunk, context); + return DataFixTypes.CHUNK.updateToCurrentVersion( + level.getServer().getFixerUpper(), storedChunk, dataVersion); + } + + private static CompoundTag chunkDataFixContext(ServerLevel level) { + CompoundTag context = new CompoundTag(); + String levelIdentifier = level.dimension().identifier().toString(); + if (PAPER_LEVEL_TYPE_KEY == null) { + context.putString("dimension", levelIdentifier); + } else { + context.putString("dimension", paperLevelTypeKey(level).identifier().toString()); + context.putString("level_identifier", levelIdentifier); + } + level.getChunkSource().getGenerator().getTypeNameForDataFixer() + .ifPresent(identifier -> context.putString("generator", identifier.toString())); + return context; + } + + boolean acceptsStored(ServerLevel level, ChunkPos chunkPos, + String structureKey, boolean requireUnreferenced) { + return storedState(level, chunkPos).accepts(structureKey, requireUnreferenced); + } + + int used() { + return used; + } + } + + public static final class Search { + private final Engine engine; + private final String structureKey; + private final int blockX; + private final int blockZ; + private final int radius; + private final Probe probe; + private final Set rejectedChunks = new HashSet<>(); + + private Search(Engine engine, String structureKey, int blockX, int blockZ, + int radius, Probe probe) { + this.engine = engine; + this.structureKey = structureKey; + this.blockX = blockX; + this.blockZ = blockZ; + this.radius = radius; + this.probe = probe; + } + + public IrisStructureLocator.LocateResult predict() { + if (rejectedChunks.size() >= MAX_SELECTED_CANDIDATE_RETRIES) { + return new IrisStructureLocator.LocateResult( + IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0); + } + return IrisStructureLocator.locateInPlacementRings( + engine, structureKey, blockX, blockZ, radius, + (chunkX, chunkZ) -> !rejectedChunks.contains(ChunkPos.pack(chunkX, chunkZ)) + && probe.accepts(chunkX, chunkZ)); + } + + public VerifiedStart verify(IrisStructureLocator.LocateResult result) { + int chunkX = result.originX() >> 4; + int chunkZ = result.originZ() >> 4; + StructureStart start = probe.verifySelected(chunkX, chunkZ); + if (start == null) { + return null; + } + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecovery.resolve( + engine, probe.level, structureKey, probe.structure, start); + if (ownership == null) { + return null; + } + return new VerifiedStart(start, ownership); + } + + public void reject(IrisStructureLocator.LocateResult result) { + rejectedChunks.add(ChunkPos.pack(result.originX() >> 4, result.originZ() >> 4)); + } + + public void reference(VerifiedStart verified) { + probe.reference(verified.start()); + } + } + + private static Method paperLevelTypeKey() { + try { + return ServerLevel.class.getMethod("getTypeKey"); + } catch (NoSuchMethodException ignored) { + return null; + } + } + + private static ResourceKey paperLevelTypeKey(ServerLevel level) { + try { + Object value = PAPER_LEVEL_TYPE_KEY.invoke(level); + if (value instanceof ResourceKey key) { + return key; + } + throw new IllegalStateException("Paper level type key has an unexpected value"); + } catch (IllegalAccessException | InvocationTargetException error) { + throw new IllegalStateException("Cannot read Paper level type key", error); + } + } + + public record VerifiedStart(StructureStart start, + NativeStructureOwnershipRecord ownership) { + } + + @FunctionalInterface + interface ChunkStorageScanner { + CompletableFuture scan(ChunkPos chunkPos, StreamTagVisitor visitor); + } + + @FunctionalInterface + interface StoredChunkDatafixer { + CompoundTag datafix(ServerLevel level, CompoundTag storedChunk); + } + + private record StoredChunkState(boolean stored, boolean startsPresent, CompoundTag starts) { + private static StoredChunkState missing() { + return new StoredChunkState(false, false, new CompoundTag()); + } + + private static StoredChunkState parse(CompoundTag chunk) { + CompoundTag structures = chunk.getCompoundOrEmpty("structures"); + return structures.getCompound("starts") + .map(starts -> new StoredChunkState(true, true, starts)) + .orElseGet(() -> new StoredChunkState(true, false, new CompoundTag())); + } + + private boolean accepts(String structureKey, boolean requireUnreferenced) { + if (!stored || !startsPresent) { + return true; + } + return starts.getCompound(structureKey).map(start -> { + String id = start.getStringOr("id", ""); + return !StructureStart.INVALID_START_ID.equals(id) + && (!requireUnreferenced || start.getIntOr("references", 0) == 0); + }).orElse(false); + } + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureLocateResults.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureLocateResults.java new file mode 100644 index 000000000..af9fb21eb --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureLocateResults.java @@ -0,0 +1,58 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.nativegen; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; + +public final class NativeStructureLocateResults { + private NativeStructureLocateResults() { + } + + public static Pair nearest(BlockPos origin, Pair replacement, + Pair nativeResult) { + if (replacement == null) { + return nativeResult; + } + if (nativeResult == null) { + return replacement; + } + return horizontalDistanceSquared(origin, nativeResult.getFirst()) + <= horizontalDistanceSquared(origin, replacement.getFirst()) ? nativeResult : replacement; + } + + public static Pair selectAndReference( + BlockPos origin, + Pair replacement, Runnable replacementReference, + Pair nativeResult, Runnable nativeReference) { + Pair selected = nearest(origin, replacement, nativeResult); + if (selected == replacement && replacement != null) { + replacementReference.run(); + } else if (selected == nativeResult && nativeResult != null) { + nativeReference.run(); + } + return selected; + } + + private static long horizontalDistanceSquared(BlockPos origin, BlockPos target) { + long dx = (long) target.getX() - origin.getX(); + long dz = (long) target.getZ() - origin.getZ(); + return dx * dx + dz * dz; + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprint.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprint.java new file mode 100644 index 000000000..febd5aada --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureOwnershipFingerprint.java @@ -0,0 +1,239 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; +import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner; +import art.arcane.iris.engine.framework.NativeStructureStartPlan; +import art.arcane.iris.engine.framework.StructurePlacementGrid; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +public final class NativeStructureOwnershipFingerprint { + private static final Comparator PIECE_ORDER = Comparator + .comparing(PieceIdentity::type) + .thenComparingInt(PieceIdentity::minX) + .thenComparingInt(PieceIdentity::minY) + .thenComparingInt(PieceIdentity::minZ) + .thenComparingInt(PieceIdentity::maxX) + .thenComparingInt(PieceIdentity::maxY) + .thenComparingInt(PieceIdentity::maxZ) + .thenComparingInt(PieceIdentity::orientation) + .thenComparingInt(PieceIdentity::generationDepth) + .thenComparing(PieceIdentity::detail); + + private NativeStructureOwnershipFingerprint() { + } + + public static NativeStructureOwnershipRecord capture(String structureKey, + StructureStart start, + NativeStructureStartPlan plan, + BoundingBox referenceEnvelope) { + StructureStart resolvedStart = requireValid(start); + NativeStructureStartPlan resolvedPlan = Objects.requireNonNull( + plan, "Native structure start plan must not be null"); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(resolvedStart); + BoundingBox references = Objects.requireNonNull( + referenceEnvelope, "Native structure reference envelope must not be null"); + ChunkPos origin = resolvedStart.getChunkPos(); + return NativeStructureOwnershipRecord.create( + structureKey, + origin.x(), + origin.z(), + StructurePlacementGrid.placementIdentity(resolvedPlan.placement()), + resolvedPlan.baseY(), + content.minX(), + content.minY(), + content.minZ(), + content.maxX(), + content.maxY(), + content.maxZ(), + locatorY(resolvedStart), + references.minX() >> 4, + references.maxX() >> 4, + references.minZ() >> 4, + references.maxZ() >> 4, + fingerprint(structureKey, resolvedStart), + NativeStructurePlacementPlanner.decisionFor(resolvedPlan) + ); + } + + public static boolean matches(NativeStructureOwnershipRecord record, StructureStart start) { + if (record == null || start == null || !start.isValid()) { + return false; + } + ChunkPos origin = start.getChunkPos(); + if (origin.x() != record.originChunkX() + || origin.z() != record.originChunkZ() + || !fingerprint(record.structureKey(), start).equals(record.contentFingerprint())) { + return false; + } + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start); + if (content.getXSpan() != (record.contentMaxX() - record.contentMinX()) + 1 + || content.getYSpan() != (record.contentMaxY() - record.contentMinY()) + 1 + || content.getZSpan() != (record.contentMaxZ() - record.contentMinZ()) + 1) { + return false; + } + return start.getStructure() instanceof OceanMonumentStructure + || content.minX() == record.contentMinX() + && content.minY() == record.contentMinY() + && content.minZ() == record.contentMinZ() + && content.maxX() == record.contentMaxX() + && content.maxY() == record.contentMaxY() + && content.maxZ() == record.contentMaxZ(); + } + + public static String fingerprint(String structureKey, StructureStart start) { + StructureStart resolvedStart = requireValid(start); + MessageDigest digest = sha256(); + updateString(digest, Objects.requireNonNull(structureKey, + "Native structure key must not be null").trim().toLowerCase(Locale.ROOT)); + updateInt(digest, resolvedStart.getChunkPos().x()); + updateInt(digest, resolvedStart.getChunkPos().z()); + BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(resolvedStart); + List pieces = contentPieces(resolvedStart, content.minY()); + updateInt(digest, pieces.size()); + for (PieceIdentity piece : pieces) { + updateString(digest, piece.type()); + updateInt(digest, piece.minX()); + updateInt(digest, piece.minY()); + updateInt(digest, piece.minZ()); + updateInt(digest, piece.maxX()); + updateInt(digest, piece.maxY()); + updateInt(digest, piece.maxZ()); + updateInt(digest, piece.orientation()); + updateInt(digest, piece.generationDepth()); + updateString(digest, piece.detail()); + } + return HexFormat.of().formatHex(digest.digest()); + } + + public static int locatorY(StructureStart start) { + StructureStart resolvedStart = requireValid(start); + if (!resolvedStart.getPieces().isEmpty()) { + return resolvedStart.getPieces().getFirst().getLocatorPosition().getY(); + } + throw new IllegalStateException("Native structure contains no locatable content pieces"); + } + + private static List contentPieces(StructureStart start, int contentMinY) { + List pieces = new ArrayList<>(); + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + Identifier type = BuiltInRegistries.STRUCTURE_PIECE.getKey(piece.getType()); + Direction orientation = piece.getOrientation(); + String detail = pieceDetail(piece, contentMinY); + pieces.add(new PieceIdentity( + type == null ? "" : type.toString(), + bounds.minX(), bounds.minY() - contentMinY, bounds.minZ(), + bounds.maxX(), bounds.maxY() - contentMinY, bounds.maxZ(), + orientation == null ? -1 : orientation.get2DDataValue(), + piece.getGenDepth(), + detail + )); + } + if (pieces.isEmpty()) { + throw new IllegalStateException("Native structure contains no content pieces"); + } + pieces.sort(PIECE_ORDER); + return List.copyOf(pieces); + } + + private static String pieceDetail(StructurePiece piece, int contentMinY) { + if (!(piece instanceof PoolElementStructurePiece poolPiece)) { + return ""; + } + return poolElementIdentity(poolPiece.getElement()) + "|" + + poolPiece.getPosition().getX() + "," + + (poolPiece.getPosition().getY() - contentMinY) + "," + + poolPiece.getPosition().getZ() + "|" + + poolPiece.getGroundLevelDelta() + "|" + + poolPiece.getRotation().getSerializedName(); + } + + private static String poolElementIdentity(StructurePoolElement element) { + Identifier type = BuiltInRegistries.STRUCTURE_POOL_ELEMENT.getKey(element.getType()); + StringBuilder identity = new StringBuilder(element.getClass().getName()) + .append('|') + .append(type == null ? "" : type) + .append('|') + .append(element.getProjection().getSerializedName()) + .append('|') + .append(element.getGroundLevelDelta()); + if (element == EmptyPoolElement.INSTANCE) { + return identity.append("|empty").toString(); + } + if (element instanceof SinglePoolElement single) { + try { + return identity.append("|template=") + .append(single.getTemplateLocation()).toString(); + } catch (RuntimeException inlineTemplate) { + return identity.append("|runtime-template").toString(); + } + } + if (element instanceof ListPoolElement list) { + identity.append("|list["); + for (StructurePoolElement child : list.getElements()) { + String childIdentity = poolElementIdentity(child); + identity.append(childIdentity.length()).append(':').append(childIdentity); + } + return identity.append(']').toString(); + } + return identity.append("|bounded-custom").toString(); + } + + private static StructureStart requireValid(StructureStart start) { + StructureStart resolved = Objects.requireNonNull( + start, "Native structure start must not be null"); + if (!resolved.isValid()) { + throw new IllegalArgumentException("Native structure start must be valid"); + } + return resolved; + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException error) { + throw new IllegalStateException("SHA-256 is unavailable", error); + } + } + + private static void updateString(MessageDigest digest, String value) { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + updateInt(digest, encoded.length); + digest.update(encoded); + } + + private static void updateInt(MessageDigest digest, int value) { + digest.update((byte) (value >>> 24)); + digest.update((byte) (value >>> 16)); + digest.update((byte) (value >>> 8)); + digest.update((byte) value); + } + + private record PieceIdentity(String type, + int minX, int minY, int minZ, + int maxX, int maxY, int maxZ, + int orientation, int generationDepth, String detail) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecovery.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecovery.java new file mode 100644 index 000000000..70c756c7d --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureOwnershipRecovery.java @@ -0,0 +1,195 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; +import art.arcane.iris.engine.framework.NativeStructureOwnershipStore; +import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner; +import art.arcane.iris.engine.framework.NativeStructureStartPlan; +import art.arcane.iris.engine.framework.StructurePlacementGrid; +import art.arcane.iris.engine.object.IrisStructureTerrain; +import art.arcane.iris.engine.object.IrisStructureTerrainMode; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import art.arcane.iris.engine.object.NativeStructureSuppression; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; + +import java.util.Locale; +import java.util.Objects; + +public final class NativeStructureOwnershipRecovery { + private NativeStructureOwnershipRecovery() { + } + + public static NativeStructureOwnershipRecord resolve( + Engine engine, ServerLevel level, String structureKey, + Structure structure, StructureStart start) { + Objects.requireNonNull(engine, "Native structure ownership recovery requires an engine"); + ServerLevel activeLevel = Objects.requireNonNull( + level, "Native structure ownership recovery requires a level"); + Structure activeStructure = Objects.requireNonNull( + structure, "Native structure ownership recovery requires a structure"); + if (start == null || !start.isValid() || start.getStructure() != activeStructure) { + return null; + } + ChunkPos origin = start.getChunkPos(); + NativeStructureOwnershipRecord persisted = NativeStructureOwnershipStore.findPersisted( + engine, structureKey, origin.x(), origin.z()); + if (persisted != null) { + NativeStructureOwnershipRecord refreshed = refreshReferenceEnvelope( + structureKey, activeStructure, start, persisted); + if (refreshed != null) { + if (refreshed != persisted) { + NativeStructureOwnershipStore.record(engine, refreshed); + } + return refreshed; + } + NativeStructureOwnershipStore.discard( + engine, structureKey, origin.x(), origin.z()); + } + NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan( + engine, structureKey, origin.x(), origin.z()); + if (!matchesPlan(structureKey, origin, plan)) { + return null; + } + Registry registry = activeLevel.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Holder holder = registry.wrapAsHolder(activeStructure); + ChunkGeneratorStructureState state = activeLevel.getChunkSource().getGeneratorState(); + if (naturalStartIsAmbiguous(engine, structureKey, activeStructure, holder, state, plan)) { + return null; + } + StructureStart expected = generateExpected( + engine, activeLevel, holder, plan, start.getReferences()); + NativeStructureOwnershipRecord recovered = proveCandidate( + structureKey, activeStructure, start, plan, expected, false); + if (recovered == null) { + return null; + } + NativeStructureOwnershipStore.record(engine, recovered); + return recovered; + } + + static NativeStructureOwnershipRecord refreshReferenceEnvelope( + String structureKey, Structure structure, StructureStart start, + NativeStructureOwnershipRecord ownership) { + if (structure == null || start == null || !start.isValid() + || start.getStructure() != structure || ownership == null + || !ownership.structureKey().equals(normalize(structureKey)) + || !NativeStructureOwnershipFingerprint.matches(ownership, start)) { + return null; + } + IrisStructureTerrain terrain = NativeStructureTerrainIntegrator.resolveNativeTerrain( + start, ownership.restoredDecision().terrain()); + if (terrain.resolvedMode() != IrisStructureTerrainMode.VACUUM) { + return ownership; + } + BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, terrain, structureKey); + int referenceMinChunkX = expected.minX() >> 4; + int referenceMaxChunkX = expected.maxX() >> 4; + int referenceMinChunkZ = expected.minZ() >> 4; + int referenceMaxChunkZ = expected.maxZ() >> 4; + if (ownership.referenceMinChunkX() == referenceMinChunkX + && ownership.referenceMaxChunkX() == referenceMaxChunkX + && ownership.referenceMinChunkZ() == referenceMinChunkZ + && ownership.referenceMaxChunkZ() == referenceMaxChunkZ) { + return ownership; + } + return ownership.withReferenceEnvelope( + referenceMinChunkX, referenceMaxChunkX, + referenceMinChunkZ, referenceMaxChunkZ); + } + + static NativeStructureOwnershipRecord proveCandidate( + String structureKey, Structure structure, StructureStart persisted, + NativeStructureStartPlan plan, StructureStart expected, + boolean naturalStartAmbiguous) { + if (naturalStartAmbiguous || structure == null + || persisted == null || !persisted.isValid() + || expected == null || !expected.isValid() + || persisted.getStructure() != structure + || expected.getStructure() != structure + || !matchesPlan(structureKey, persisted.getChunkPos(), plan) + || !persisted.getChunkPos().equals(expected.getChunkPos())) { + return null; + } + BoundingBox referenceEnvelope = NativeStructureReferenceEnvelope.referenceBounds( + expected, structure, plan.placement().resolvedTerrain(), structureKey); + NativeStructureOwnershipRecord candidate = NativeStructureOwnershipFingerprint.capture( + structureKey, expected, plan, referenceEnvelope); + if (candidate.placementIdentity() + != StructurePlacementGrid.placementIdentity(plan.placement())) { + return null; + } + return NativeStructureOwnershipFingerprint.matches(candidate, persisted) + ? candidate : null; + } + + private static boolean matchesPlan(String structureKey, ChunkPos origin, + NativeStructureStartPlan plan) { + if (structureKey == null || structureKey.isBlank() || plan == null) { + return false; + } + return plan.chunkX() == origin.x() + && plan.chunkZ() == origin.z() + && normalize(structureKey).equals(normalize(plan.source().getStructure())); + } + + private static StructureStart generateExpected( + Engine engine, ServerLevel level, Holder holder, + NativeStructureStartPlan plan, int references) { + ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState(); + ChunkGenerator generator = level.getChunkSource().getGenerator(); + BiomeSource biomeSource = generator.getBiomeSource(); + NativeStructureFactory.GenerationContext generationContext = + new NativeStructureFactory.GenerationContext( + level.registryAccess(), + generator, + biomeSource, + state.randomState(), + level.getStructureManager(), + state.getLevelSeed(), + level.dimension(), + level, + biome -> true, + generator.getSeaLevel(), + (x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight() + ); + return NativeStructureFactory.generate(generationContext, holder, plan, references); + } + + private static boolean naturalStartIsAmbiguous( + Engine engine, String structureKey, Structure structure, + Holder holder, ChunkGeneratorStructureState state, + NativeStructureStartPlan plan) { + if (plan.placement().getNativeSuppression() == NativeStructureSuppression.REPLACE_SOURCE) { + return false; + } + NativeStructureGenerationStatus sourceStatus = NativeStructureGenerationPolicy.resolve( + engine, structureKey, + NativeStructureVegetationClearer.isUndergroundStep(structure.step())).status(); + if (sourceStatus == NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + return false; + } + for (StructurePlacement placement : state.getPlacementsForStructure(holder)) { + if (placement.isStructureChunk(state, plan.chunkX(), plan.chunkZ())) { + return true; + } + } + return false; + } + + private static String normalize(String structureKey) { + return structureKey == null ? "" : structureKey.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java new file mode 100644 index 000000000..6996e36fe --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java @@ -0,0 +1,66 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.IrisStructureStiltSettings; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.List; +import java.util.function.IntBinaryOperator; + +public final class NativeStructurePostProcessor { + private NativeStructurePostProcessor() { + } + + public static void place(WorldGenLevel world, StructureManager structureManager, ChunkGenerator generator, + WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, String structureId, + StructureStart start, IrisNativeStructureDecision decision, + PaletteBlockResolver paletteBlockResolver, + IntBinaryOperator surfaceHeight) { + IrisStructureStiltSettings stilt = decision.stilt(); + NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(start, structureId, decision.yShift(), + generator.getSeaLevel(), area.minY(), area.maxY() + 1); + start.placeInChunk(world, structureManager, generator, random, area, chunkPos); + if (stilt != null) { + NativeStructureFoundationBuilder.placeStilts(world, area, structureId, start, stilt, + paletteBlockResolver, surfaceHeight, + !NativeStructureVegetationClearer.isUndergroundStep(start.getStructure().step())); + } + } + + public static void prepareTerrain(WorldGenLevel world, BoundingBox area, + List targets, + PaletteBlockResolver paletteBlockResolver) { + if (targets == null || targets.isEmpty()) { + return; + } + NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain = + NativeStructureTerrainIntegrator.captureSourceTerrain(world, area, targets); + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + NativeStructureTerrainIntegrator.integrateTerrain(world, area, target.structureId(), target.start(), + target.terrain(), paletteBlockResolver, sourceTerrain); + } + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + if (NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + target.start(), target.terrain())) { + NativeStructureTerrainIntegrator.clearLegacyTemplateAir( + world, area, target.start(), () -> world.getLevel().getStructureManager()); + } + } + NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport( + world, area, targets, () -> world.getLevel().getStructureManager()); + } + + @FunctionalInterface + public interface PaletteBlockResolver { + BlockState resolve(IrisMaterialPalette palette, RNG rng, int x, int y, int z); + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReferenceEnvelope.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReferenceEnvelope.java new file mode 100644 index 000000000..1c0bb0b97 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReferenceEnvelope.java @@ -0,0 +1,238 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.object.IrisStructureTerrain; +import art.arcane.iris.engine.object.IrisStructureTerrainMode; +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; + +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class NativeStructureReferenceEnvelope { + private static final int MAX_REFERENCE_DISTANCE_CHUNKS = 8; + private static final Set WARNED_CLIPPED_TERRAIN = ConcurrentHashMap.newKeySet(); + private static final Set WARNED_SKIPPED_CONTENT = ConcurrentHashMap.newKeySet(); + + private NativeStructureReferenceEnvelope() { + } + + public static StructureStart wrap(StructureStart generated, Structure source, int references, + IrisStructureTerrain terrain) { + return wrap(generated, source, references, terrain, null); + } + + public static StructureStart wrap(StructureStart generated, Structure source, int references, + IrisStructureTerrain terrain, String structureKey) { + referenceBounds(generated, source, terrain, structureKey); + return new StructureStart( + source, + generated.getChunkPos(), + references, + new PiecesContainer(List.copyOf(generated.getPieces())) + ); + } + + public static StructureStart wrapForPublication( + StructureStart generated, Structure source, int references, + IrisStructureTerrain terrain, String structureKey) { + try { + return wrap(generated, source, references, terrain, structureKey); + } catch (UnrepresentableContentException exception) { + warnSkippedContent(generated, source, structureKey); + return StructureStart.INVALID_START; + } + } + + public static BoundingBox referenceBounds(StructureStart start, Structure source, + IrisStructureTerrain terrain) { + return referenceBounds(start, source, terrain, null); + } + + public static BoundingBox referenceBounds(StructureStart start, Structure source, + IrisStructureTerrain terrain, + String structureKey) { + IrisStructureTerrainMode mode = terrain == null + ? IrisStructureTerrainMode.PRESERVE : terrain.resolvedMode(); + boolean usesEnvelope = mode == IrisStructureTerrainMode.BORE + || mode == IrisStructureTerrainMode.FORCE_CARVE + || mode == IrisStructureTerrainMode.VACUUM + || mode == IrisStructureTerrainMode.ENCASE; + int horizontalPadding = mode == IrisStructureTerrainMode.VACUUM + ? NativeStructureSurfaceFitter.surfaceTerrainRadius() + : usesEnvelope ? Math.max(0, terrain.getHorizontalPadding()) : 0; + BoundingBox content = contentBounds(start); + if (!fitsReferenceRange(start.getChunkPos(), content)) { + throw new UnrepresentableContentException("Native structure content at " + + start.getChunkPos().x() + "," + start.getChunkPos().z() + + " exceeds Minecraft's " + MAX_REFERENCE_DISTANCE_CHUNKS + + "-chunk structure reference range"); + } + BoundingBox adjustedEnvelope; + boolean arithmeticClipped = false; + try { + BoundingBox envelope = horizontalPadding == 0 + ? content + : new BoundingBox( + Math.subtractExact(content.minX(), horizontalPadding), + content.minY(), + Math.subtractExact(content.minZ(), horizontalPadding), + Math.addExact(content.maxX(), horizontalPadding), + content.maxY(), + Math.addExact(content.maxZ(), horizontalPadding) + ); + adjustedEnvelope = mode == IrisStructureTerrainMode.SOURCE + ? source.adjustBoundingBox(content) : envelope; + adjustedEnvelope = encapsulating(adjustedEnvelope, content); + } catch (ArithmeticException exception) { + adjustedEnvelope = maximumReferenceBounds(start.getChunkPos(), content); + arithmeticClipped = true; + } + BoundingBox referencedEnvelope = clampReferenceRange(start.getChunkPos(), adjustedEnvelope); + if (arithmeticClipped || !sameHorizontalBounds(adjustedEnvelope, referencedEnvelope)) { + warnClippedTerrain(start, source, structureKey); + return referencedEnvelope; + } + return adjustedEnvelope; + } + + public static boolean contentFitsReferenceRange(StructureStart start) { + try { + return fitsReferenceRange(start.getChunkPos(), contentBounds(start)); + } catch (UnrepresentableContentException exception) { + return false; + } + } + + public static BoundingBox contentBounds(StructureStart start) { + return contentBounds(start.getPieces()); + } + + private static BoundingBox contentBounds(List pieces) { + BoundingBox bounds = null; + for (StructurePiece piece : pieces) { + bounds = bounds == null + ? copy(piece.getBoundingBox()) + : bounds.encapsulate(piece.getBoundingBox()); + } + if (bounds == null) { + throw new IllegalStateException("Native jigsaw generated no content pieces"); + } + return bounds; + } + + private static BoundingBox clampReferenceRange(ChunkPos startChunk, BoundingBox envelope) { + int minX = minimumReferenceBlock(startChunk.x()); + int maxX = maximumReferenceBlock(startChunk.x()); + int minZ = minimumReferenceBlock(startChunk.z()); + int maxZ = maximumReferenceBlock(startChunk.z()); + return new BoundingBox( + Math.max(envelope.minX(), minX), + envelope.minY(), + Math.max(envelope.minZ(), minZ), + Math.min(envelope.maxX(), maxX), + envelope.maxY(), + Math.min(envelope.maxZ(), maxZ) + ); + } + + private static BoundingBox maximumReferenceBounds(ChunkPos startChunk, BoundingBox content) { + return new BoundingBox( + minimumReferenceBlock(startChunk.x()), content.minY(), + minimumReferenceBlock(startChunk.z()), + maximumReferenceBlock(startChunk.x()), content.maxY(), + maximumReferenceBlock(startChunk.z()) + ); + } + + private static boolean fitsReferenceRange(ChunkPos startChunk, BoundingBox content) { + return content.minX() >= minimumReferenceBlock(startChunk.x()) + && content.maxX() <= maximumReferenceBlock(startChunk.x()) + && content.minZ() >= minimumReferenceBlock(startChunk.z()) + && content.maxZ() <= maximumReferenceBlock(startChunk.z()); + } + + private static int minimumReferenceBlock(int originChunk) { + return checkedReferenceBlock( + ((long) originChunk - MAX_REFERENCE_DISTANCE_CHUNKS) << 4, originChunk); + } + + private static int maximumReferenceBlock(int originChunk) { + return checkedReferenceBlock( + (((long) originChunk + MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15L, originChunk); + } + + private static int checkedReferenceBlock(long block, int originChunk) { + if (block < Integer.MIN_VALUE || block > Integer.MAX_VALUE) { + throw new UnrepresentableContentException("Native structure origin chunk " + originChunk + + " exceeds Minecraft's block coordinate range"); + } + return (int) block; + } + + private static boolean sameHorizontalBounds(BoundingBox left, BoundingBox right) { + return left.minX() == right.minX() + && left.minZ() == right.minZ() + && left.maxX() == right.maxX() + && left.maxZ() == right.maxZ(); + } + + private static BoundingBox copy(BoundingBox bounds) { + return new BoundingBox( + bounds.minX(), bounds.minY(), bounds.minZ(), + bounds.maxX(), bounds.maxY(), bounds.maxZ()); + } + + private static BoundingBox encapsulating(BoundingBox left, BoundingBox right) { + return new BoundingBox( + Math.min(left.minX(), right.minX()), + Math.min(left.minY(), right.minY()), + Math.min(left.minZ(), right.minZ()), + Math.max(left.maxX(), right.maxX()), + Math.max(left.maxY(), right.maxY()), + Math.max(left.maxZ(), right.maxZ()) + ); + } + + private static void warnClippedTerrain( + StructureStart start, Structure source, String structureKey) { + String key = warningKey(source, structureKey); + if (WARNED_CLIPPED_TERRAIN.add(key)) { + IrisLogging.warn("Clipping optional terrain envelope for native structure '" + + key + "' at " + start.getChunkPos().x() + "," + start.getChunkPos().z() + + " to Minecraft's " + MAX_REFERENCE_DISTANCE_CHUNKS + + "-chunk reference range; all generated content remains exactly referenced"); + } + } + + private static void warnSkippedContent( + StructureStart start, Structure source, String structureKey) { + String key = warningKey(source, structureKey); + if (WARNED_SKIPPED_CONTENT.add(key)) { + IrisLogging.warn("Skipping native structure '" + key + "' at " + + start.getChunkPos().x() + "," + start.getChunkPos().z() + + " because its generated content exceeds Minecraft's " + + MAX_REFERENCE_DISTANCE_CHUNKS + + "-chunk reference range; no structure start, ownership, references, or blocks were published"); + } + } + + private static String warningKey(Structure source, String structureKey) { + if (structureKey != null && !structureKey.isBlank()) { + return structureKey.trim().toLowerCase(Locale.ROOT); + } + return source.getClass().getName(); + } + + public static final class UnrepresentableContentException extends IllegalStateException { + private UnrepresentableContentException(String message) { + super(message); + } + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReferenceRepair.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReferenceRepair.java new file mode 100644 index 000000000..68866a784 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReferenceRepair.java @@ -0,0 +1,224 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; +import art.arcane.iris.engine.framework.NativeStructureOwnershipStore; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.spi.IrisLogging; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.core.Registry; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class NativeStructureReferenceRepair { + private static final int REFERENCE_DISTANCE_CHUNKS = + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS; + private static final Set WARNED_POLICY_INVALIDATIONS = ConcurrentHashMap.newKeySet(); + + private NativeStructureReferenceRepair() { + } + + public static void createReferences(Engine engine, WorldGenLevel level, + StructureManager structureManager, ChunkAccess targetChunk) { + ChunkPos target = targetChunk.getPos(); + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + ServerLevel serverLevel = level.getLevel(); + List starts = new ArrayList<>(); + for (int originChunkX = target.x() - REFERENCE_DISTANCE_CHUNKS; + originChunkX <= target.x() + REFERENCE_DISTANCE_CHUNKS; originChunkX++) { + for (int originChunkZ = target.z() - REFERENCE_DISTANCE_CHUNKS; + originChunkZ <= target.z() + REFERENCE_DISTANCE_CHUNKS; originChunkZ++) { + ChunkAccess originChunk = level.getChunk( + originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS); + for (Map.Entry entry : originChunk.getAllStarts().entrySet()) { + ScannedStart start = scanStart( + engine, serverLevel, structureManager, targetChunk, registry, + originChunk, entry.getKey(), entry.getValue()); + if (start != null && isTargetRelevant(engine, targetChunk, start)) { + starts.add(start); + } + } + } + } + for (ScannedStart start : starts) { + boolean valid; + synchronized (start.originChunk()) { + StructureStart current = start.originChunk().getStartForStructure(start.structure()); + valid = current == start.start() && current.isValid(); + } + if (!valid) { + continue; + } + structureManager.addReferenceForStructure( + SectionPos.bottomOf(targetChunk), start.structure(), + start.origin().pack(), targetChunk); + } + } + + private static boolean isTargetRelevant( + Engine engine, ChunkAccess targetChunk, ScannedStart scanned) { + StructureStart start = scanned.start(); + Structure structure = scanned.structure(); + ChunkPos target = targetChunk.getPos(); + if (scanned.ownership() != null) { + return requiresReference(target, scanned.structureKey(), start, scanned.ownership()); + } + if (scanned.registered()) { + return requiresNaturalReference( + engine, target, scanned.structureKey(), structure, start); + } + return NativeStructureReferenceEnvelope.contentBounds(start).intersects( + target.getMinBlockX(), target.getMinBlockZ(), + target.getMaxBlockX(), target.getMaxBlockZ()); + } + + private static ScannedStart scanStart( + Engine engine, ServerLevel level, StructureManager structureManager, + ChunkAccess targetChunk, Registry registry, + ChunkAccess originChunk, Structure structure, StructureStart start) { + Identifier identifier = registry.getKey(structure); + String structureKey = identifier == null + ? structure.getClass().getName() : identifier.toString(); + if (start == null || !start.isValid()) { + return null; + } + if (!NativeStructureReferenceEnvelope.contentFitsReferenceRange(start)) { + if (identifier != null) { + NativeStructureOwnershipStore.discard( + engine, structureKey, start.getChunkPos().x(), start.getChunkPos().z()); + } + invalidateStart(structureManager, originChunk, structure, start, targetChunk); + return null; + } + ChunkPos startOrigin = start.getChunkPos(); + NativeStructureOwnershipRecord ownership = identifier == null ? null + : NativeStructureOwnershipRecovery.resolve( + engine, level, structureKey, structure, start); + if (ownership != null) { + return new ScannedStart( + originChunk, structure, start, structureKey, ownership, true); + } + if (identifier != null && !naturalPolicyAllows(engine, structureKey, structure)) { + invalidateStart(structureManager, originChunk, structure, start, targetChunk); + if (WARNED_POLICY_INVALIDATIONS.add(structureKey)) { + IrisLogging.warn("Invalidating persisted natural structure start '" + + structureKey + "' because the current Iris dimension policy disables it"); + } + return null; + } + return new ScannedStart( + originChunk, structure, start, structureKey, null, identifier != null); + } + + private static boolean naturalPolicyAllows( + Engine engine, String structureKey, Structure structure) { + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve( + engine, structureKey, + NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + return naturalDecisionAllows(decision); + } + + static boolean naturalDecisionAllows(IrisNativeStructureDecision decision) { + return Objects.requireNonNull( + decision, "Native structure decision must not be null").generate(); + } + + private static void invalidateStart( + StructureManager structureManager, ChunkAccess originChunk, + Structure structure, StructureStart start, ChunkAccess targetChunk) { + synchronized (originChunk) { + StructureStart current = originChunk.getStartForStructure(structure); + if (current == start && current.isValid()) { + structureManager.setStartForStructure( + SectionPos.bottomOf(originChunk), structure, + StructureStart.INVALID_START, originChunk); + } + } + if (targetChunk != null) { + removeTargetReference(targetChunk, structure, start.getChunkPos().pack()); + } + } + + private static void removeTargetReference( + ChunkAccess targetChunk, Structure structure, long origin) { + LongSet current = targetChunk.getAllReferences().get(structure); + if (current == null || !current.contains(origin)) { + return; + } + Map updated = new HashMap<>(targetChunk.getAllReferences()); + LongSet retained = new LongOpenHashSet(current); + retained.remove(origin); + if (retained.isEmpty()) { + updated.remove(structure); + } else { + updated.put(structure, retained); + } + targetChunk.setAllReferences(updated); + } + + static boolean requiresNaturalReference(Engine engine, ChunkPos target, String structureKey, + Structure structure, StructureStart start) { + if (target == null || start == null || !start.isValid()) { + return false; + } + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve( + engine, structureKey, + NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + if (!decision.generate()) { + return false; + } + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + start, structure, + NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()), + structureKey); + return referenceBounds.intersects( + target.getMinBlockX(), target.getMinBlockZ(), + target.getMaxBlockX(), target.getMaxBlockZ()); + } + + static boolean requiresReference(ChunkPos target, String structureKey, + StructureStart start, + NativeStructureOwnershipRecord ownership) { + if (target == null || start == null || !start.isValid() + || ownership == null || !ownership.structureKey().equals(structureKey) + || !ownership.covers(target.x(), target.z())) { + return false; + } + return NativeStructureOwnershipFingerprint.matches(ownership, start); + } + + private record ScannedStart( + ChunkAccess originChunk, + Structure structure, + StructureStart start, + String structureKey, + NativeStructureOwnershipRecord ownership, + boolean registered + ) { + private ChunkPos origin() { + return ownership == null + ? start.getChunkPos() + : new ChunkPos(ownership.originChunkX(), ownership.originChunkZ()); + } + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReflection.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReflection.java new file mode 100644 index 000000000..8a7a41c3d --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReflection.java @@ -0,0 +1,352 @@ +package art.arcane.iris.nativegen; + +import com.mojang.datafixers.util.Either; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +final class NativeStructureReflection { + private NativeStructureReflection() { + } + + static Field resolveScatteredHeightPositionField() { + Field resolved = null; + for (Field field : ScatteredFeaturePiece.class.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) || field.getType() != int.class + || !Modifier.isProtected(modifiers) || Modifier.isFinal(modifiers)) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("ScatteredFeaturePiece has multiple mutable protected int fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("ScatteredFeaturePiece height-position field is missing"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("ScatteredFeaturePiece height-position field is inaccessible"); + } + return resolved; + } + + private static Field resolveMonumentChildPiecesField() { + Field resolved = null; + for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("Ocean Monument has multiple instance List fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("Ocean Monument child-pieces List field is missing"); + } + if (!Modifier.isPrivate(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("Ocean Monument child-pieces field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("Ocean Monument child-pieces field is inaccessible"); + } + return resolved; + } + + static Field resolveSinglePoolTemplateField() { + Field resolved = null; + for (Field field : SinglePoolElement.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != Either.class) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("SinglePoolElement has multiple instance Either fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("SinglePoolElement template Either field is missing"); + } + if (!Modifier.isProtected(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("SinglePoolElement template field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("SinglePoolElement template field is inaccessible"); + } + return resolved; + } + + static StructureTemplate resolveTemplate(SinglePoolElement element, + Supplier templates) { + Object value; + try { + value = SinglePoolTemplateAccess.FIELD.get(element); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read native structure pool template", error); + } + if (!(value instanceof Either reference)) { + throw new IllegalStateException("Native structure pool template field is not an Either"); + } + return resolveTemplateReference(reference, templates); + } + + @SuppressWarnings("unchecked") + static List resolveTemplateBlocks( + StructureTemplate template, StructurePlaceSettings settings, + BlockPos origin) { + Object value; + try { + value = StructureTemplatePalettesAccess.FIELD.get(template); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read native structure template palettes", error); + } + if (!(value instanceof List paletteValues)) { + throw new IllegalStateException("Native structure template palettes field is not a List"); + } + List palettes = + (List) paletteValues; + if (palettes.isEmpty()) { + return List.of(); + } + return settings.getRandomPalette(palettes, origin).blocks(); + } + + static StructurePlaceSettings resolvePlacementSettings( + SinglePoolElement element, PoolElementStructurePiece piece, + BoundingBox area) { + LiquidSettings liquidSettings; + try { + liquidSettings = (LiquidSettings) PoolPieceLiquidSettingsAccess.FIELD.get(piece); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read native structure piece liquid settings", error); + } + try { + return (StructurePlaceSettings) SinglePoolSettingsAccess.METHOD.invoke( + element, piece.getRotation(), area, liquidSettings, false); + } catch (IllegalAccessException | InvocationTargetException error) { + throw new IllegalStateException("Cannot create native structure placement settings", error); + } + } + + @SuppressWarnings("unchecked") + static List processTemplateBlocks( + ServerLevelAccessor world, BlockPos origin, BlockPos referencePos, + StructurePlaceSettings settings, + List blocks, + StructureTemplate template) { + Method forgeMethod = StructureTemplateProcessingAccess.FORGE_METHOD; + if (forgeMethod == null) { + return StructureTemplate.processBlockInfos( + world, origin, referencePos, settings, blocks); + } + try { + return (List) forgeMethod.invoke( + null, world, origin, referencePos, settings, blocks, template); + } catch (IllegalAccessException | InvocationTargetException error) { + throw new IllegalStateException("Cannot process native structure template blocks", error); + } + } + + static StructureTemplate resolveTemplateReference(Either reference, + Supplier templates) { + return reference.map( + location -> resolveNamedTemplate(location, templates), + NativeStructureReflection::requireRuntimeTemplate); + } + + private static StructureTemplate resolveNamedTemplate(Object value, + Supplier templates) { + if (!(value instanceof Identifier identifier)) { + throw new IllegalStateException("Native structure pool template identifier is " + + (value == null ? "null" : value.getClass().getName())); + } + return Objects.requireNonNull(templates == null ? null : templates.get(), + "Native structure template manager is unavailable").getOrCreate(identifier); + } + + private static StructureTemplate requireRuntimeTemplate(Object value) { + if (!(value instanceof StructureTemplate template)) { + throw new IllegalStateException("Native runtime structure pool template is " + + (value == null ? "null" : value.getClass().getName())); + } + return template; + } + + private static Field resolveStructureTemplatePalettesField() { + Field resolved = null; + for (Field field : StructureTemplate.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class + || !isPaletteList(field.getGenericType())) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("StructureTemplate has multiple palette List fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("StructureTemplate palette List field is missing"); + } + if (!Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("StructureTemplate palette field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("StructureTemplate palette field is inaccessible"); + } + return resolved; + } + + private static boolean isPaletteList(Type type) { + if (!(type instanceof ParameterizedType parameterized)) { + return false; + } + Type[] arguments = parameterized.getActualTypeArguments(); + return arguments.length == 1 && arguments[0] == StructureTemplate.Palette.class; + } + + private static Method resolveSinglePoolSettingsMethod() { + Method resolved = null; + Class[] parameterTypes = { + Rotation.class, BoundingBox.class, LiquidSettings.class, boolean.class + }; + for (Method method : SinglePoolElement.class.getDeclaredMethods()) { + if (Modifier.isStatic(method.getModifiers()) + || method.getReturnType() != StructurePlaceSettings.class + || !Arrays.equals(method.getParameterTypes(), parameterTypes)) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("SinglePoolElement has multiple placement-settings methods"); + } + resolved = method; + } + if (resolved == null) { + throw new IllegalStateException("SinglePoolElement placement-settings method is missing"); + } + if (!Modifier.isProtected(resolved.getModifiers())) { + throw new IllegalStateException("SinglePoolElement placement-settings method has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("SinglePoolElement placement-settings method is inaccessible"); + } + return resolved; + } + + private static Field resolvePoolPieceLiquidSettingsField() { + Field resolved = null; + for (Field field : PoolElementStructurePiece.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) + || field.getType() != LiquidSettings.class) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("PoolElementStructurePiece has multiple liquid-settings fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("PoolElementStructurePiece liquid-settings field is missing"); + } + if (!Modifier.isPrivate(resolved.getModifiers()) + || !Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("PoolElementStructurePiece liquid-settings field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("PoolElementStructurePiece liquid-settings field is inaccessible"); + } + return resolved; + } + + private static Method resolveForgeTemplateProcessingMethod() { + Class[] parameterTypes = { + ServerLevelAccessor.class, BlockPos.class, BlockPos.class, + StructurePlaceSettings.class, List.class, StructureTemplate.class + }; + Method resolved = null; + for (Method method : StructureTemplate.class.getDeclaredMethods()) { + if (!Modifier.isPublic(method.getModifiers()) + || !Modifier.isStatic(method.getModifiers()) + || method.getReturnType() != List.class + || !Arrays.equals(method.getParameterTypes(), parameterTypes)) { + continue; + } + if (resolved != null) { + throw new IllegalStateException( + "StructureTemplate has multiple Forge block-processing methods"); + } + resolved = method; + } + return resolved; + } + + static final class MonumentChildPiecesAccess { + static final Field FIELD = resolveMonumentChildPiecesField(); + + private MonumentChildPiecesAccess() { + } + } + + static final class ScatteredHeightPositionAccess { + static final Field FIELD = resolveScatteredHeightPositionField(); + + private ScatteredHeightPositionAccess() { + } + } + + private static final class SinglePoolTemplateAccess { + private static final Field FIELD = resolveSinglePoolTemplateField(); + + private SinglePoolTemplateAccess() { + } + } + + private static final class StructureTemplatePalettesAccess { + private static final Field FIELD = resolveStructureTemplatePalettesField(); + + private StructureTemplatePalettesAccess() { + } + } + + private static final class SinglePoolSettingsAccess { + private static final Method METHOD = resolveSinglePoolSettingsMethod(); + + private SinglePoolSettingsAccess() { + } + } + + private static final class PoolPieceLiquidSettingsAccess { + private static final Field FIELD = resolvePoolPieceLiquidSettingsField(); + + private PoolPieceLiquidSettingsAccess() { + } + } + + private static final class StructureTemplateProcessingAccess { + private static final Method FORGE_METHOD = resolveForgeTemplateProcessingMethod(); + + private StructureTemplateProcessingAccess() { + } + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureStartInjector.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureStartInjector.java new file mode 100644 index 000000000..b868bb077 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureStartInjector.java @@ -0,0 +1,147 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; +import art.arcane.iris.engine.framework.NativeStructureOwnershipStore; +import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner; +import art.arcane.iris.engine.framework.NativeStructureStartPlan; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import art.arcane.iris.engine.object.NativeStructureSuppression; +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class NativeStructureStartInjector { + private static final Set WARNED_DUPLICATE_STRUCTURES = ConcurrentHashMap.newKeySet(); + + private NativeStructureStartInjector() { + } + + public static Map inject(InjectionContext context) { + Objects.requireNonNull(context, "Native structure injection context must not be null"); + ChunkAccess chunk = context.chunk(); + Registry registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE); + SectionPos section = SectionPos.bottomOf(chunk); + Map configuredStarts = new LinkedHashMap<>(); + for (NativeStructureStartPlan plan : NativeStructurePlacementPlanner.plansAt( + context.engine(), chunk.getPos().x(), chunk.getPos().z())) { + Identifier identifier = Identifier.tryParse(plan.source().getStructure()); + if (identifier == null) { + throw new IllegalStateException("Configured native structure key is invalid: " + + plan.source().getStructure()); + } + Structure structure = registry.getValue(identifier); + if (structure == null) { + throw new IllegalStateException("Configured native structure is not registered: " + identifier); + } + Holder holder = registry.wrapAsHolder(structure); + if (configuredStarts.containsKey(structure)) { + if (WARNED_DUPLICATE_STRUCTURES.add(identifier.toString())) { + IrisLogging.warn("Ignoring duplicate native structure placements for '" + + identifier + "'; the first deterministic candidate owns each chunk start"); + } + continue; + } + StructureStart existing = context.structureManager().getStartForStructure( + section, structure, chunk); + boolean replacement = plan.placement().getNativeSuppression() + == NativeStructureSuppression.REPLACE_SOURCE; + if (!replacement && existing != null && existing.isValid()) { + NativeStructureGenerationStatus sourceStatus = NativeStructureGenerationPolicy.resolve( + context.engine(), identifier.toString(), + NativeStructureVegetationClearer.isUndergroundStep(structure.step())).status(); + replacement = sourceStatus == NativeStructureGenerationStatus.REPLACED_BY_IRIS; + } + if (!replacement && existing != null && existing.isValid()) { + continue; + } + int references = existing != null && existing.isValid() ? existing.getReferences() : 0; + NativeStructureFactory.GenerationContext generationContext = + new NativeStructureFactory.GenerationContext( + context.registryAccess(), + context.generator(), + context.biomeSource(), + context.structureState().randomState(), + context.templateManager(), + context.structureState().getLevelSeed(), + context.levelKey(), + chunk, + biome -> true, + context.generator().getSeaLevel(), + (x, z) -> context.engine().getHeight(x, z, true) + + context.engine().getMinHeight() + ); + StructureStart generated = NativeStructureFactory.generate( + generationContext, holder, plan, references); + if (!isUsableGeneratedStart(generated)) { + if (replacement) { + context.structureManager().setStartForStructure( + section, structure, StructureStart.INVALID_START, chunk); + } + NativeStructureOwnershipStore.discard( + context.engine(), identifier.toString(), + chunk.getPos().x(), chunk.getPos().z()); + continue; + } + BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds( + generated, structure, plan.placement().resolvedTerrain(), identifier.toString()); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + identifier.toString(), generated, plan, referenceBounds); + NativeStructureOwnershipStore.record(context.engine(), ownership); + try { + context.structureManager().setStartForStructure( + section, structure, generated, chunk); + } catch (RuntimeException | Error publicationError) { + try { + NativeStructureOwnershipStore.discard( + context.engine(), identifier.toString(), + chunk.getPos().x(), chunk.getPos().z()); + } catch (RuntimeException | Error cleanupError) { + publicationError.addSuppressed(cleanupError); + } + throw publicationError; + } + configuredStarts.put(structure, plan); + } + return Map.copyOf(configuredStarts); + } + + static boolean isUsableGeneratedStart(StructureStart start) { + return start != null && start.isValid(); + } + + public record InjectionContext( + Engine engine, + RegistryAccess registryAccess, + ChunkGeneratorStructureState structureState, + StructureManager structureManager, + ChunkAccess chunk, + StructureTemplateManager templateManager, + ResourceKey levelKey, + ChunkGenerator generator, + BiomeSource biomeSource + ) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceFitter.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceFitter.java new file mode 100644 index 000000000..8e54eea10 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceFitter.java @@ -0,0 +1,921 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.object.IrisObjectVacuum; +import art.arcane.iris.engine.object.IrisStructureTerrain; +import art.arcane.iris.engine.object.IrisStructureTerrainMode; +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntBinaryOperator; + +public final class NativeStructureSurfaceFitter { + private static final double SURFACE_TERRAIN_FALLOFF = 2.0; + private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L; + private static final int SURFACE_TERRAIN_RADIUS = 12; + private static final int MAX_SOURCE_SURFACE_DELTA = 6; + private static final int MAX_SURFACE_TEMPLATE_CELLS = 4_194_304; + private static final Set WARNED_SOURCE_BUDGET = + ConcurrentHashMap.newKeySet(); + private static final Set WARNED_VACUUM_BUDGET = + ConcurrentHashMap.newKeySet(); + + private NativeStructureSurfaceFitter() { + } + + public static VacuumFoundationPlan prepareSurfaceStructures( + WorldGenLevel world, BoundingBox area, + List targets, + IntBinaryOperator surfaceHeight) { + return prepareSurfaceStructures( + world, area, targets, surfaceHeight, MAX_SURFACE_TEMPLATE_CELLS); + } + + static VacuumFoundationPlan prepareSurfaceStructures( + WorldGenLevel world, BoundingBox area, + List targets, + IntBinaryOperator surfaceHeight, int maximumTemplateCells) { + if (targets == null || targets.isEmpty()) { + return VacuumFoundationPlan.empty(); + } + Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver"); + List anchors = collectSourceSurfaceAnchors( + world, area, targets, maximumTemplateCells); + if (!anchors.isEmpty()) { + fitSurfaceTerrain(world, area, anchors, surfaceHeight); + } + VacuumFootprint vacuum = collectVacuumFootprint( + world, area, targets, maximumTemplateCells); + if (!vacuum.anchors().isEmpty()) { + fitVacuumTerrain(world, area, vacuum, surfaceHeight); + } + return VacuumFoundationPlan.create( + vacuum.foundationBases(), vacuum.occupiedCells(), area); + } + + public static void repairVacuumFoundations( + WorldGenLevel world, BoundingBox area, + VacuumFoundationPlan plan) { + if (plan == null || plan.foundationBases.isEmpty()) { + return; + } + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (long packedBase : plan.foundationBases) { + BlockPos base = BlockPos.of(packedBase); + if (!area.isInside(base)) { + continue; + } + BlockState baseState = world.getBlockState(position.set(base)); + if (!NativeStructureTemplateOccupancy.isSolidBase(baseState)) { + continue; + } + int supportY = findVacuumFoundationSupport( + world, area, position, base.getX(), base.getY() - 1, base.getZ()); + if (supportY == Integer.MIN_VALUE || supportY == base.getY() - 1) { + continue; + } + if (intersectsVacuumOccupancy( + plan.occupiedCells, base.getX(), base.getZ(), + supportY + 1, base.getY() - 1)) { + continue; + } + BlockState fill = resolveSurfaceMaterials( + world, position, base.getX(), base.getZ(), supportY, + area.minY()).subsurface(); + for (int y = supportY + 1; y < base.getY(); y++) { + BlockState existing = world.getBlockState( + position.set(base.getX(), y, base.getZ())); + if (existing.isAir() || !existing.getFluidState().isEmpty()) { + world.setBlock(position, fill, 2); + } + } + } + } + + static boolean shouldPrepareSurfaceTerrain(TerrainAdjustment adjustment, + GenerationStep.Decoration step) { + return step == GenerationStep.Decoration.SURFACE_STRUCTURES + && (adjustment == TerrainAdjustment.BEARD_THIN + || adjustment == TerrainAdjustment.BEARD_BOX); + } + + static int surfaceTerrainRadius() { + return SURFACE_TERRAIN_RADIUS; + } + + static int resolveSurfaceTarget(List anchors, int worldX, int worldZ, + int originalY) { + return resolveSurface(anchors, worldX, worldZ, originalY).targetY(); + } + + private static SurfaceResolution resolveSurface(List anchors, + int worldX, int worldZ, int originalY) { + int localTargetY = originalY; + SurfaceAnchor selectedLocal = null; + long totalInfluence = 0L; + long weightedTargetY = 0L; + long maximumInfluence = 0L; + for (SurfaceAnchor anchor : anchors) { + int outX = IrisObjectVacuum.outset(worldX, anchor.minX(), anchor.maxX()); + int outZ = IrisObjectVacuum.outset(worldZ, anchor.minZ(), anchor.maxZ()); + boolean containsColumn = outX == 0 && outZ == 0; + long distanceSquared = (long) outX * outX + (long) outZ * outZ; + long radiusSquared = (long) SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS; + if (distanceSquared > radiusSquared) { + continue; + } + double distance = Math.sqrt(distanceSquared); + double factor = Math.pow( + 1D - distance / SURFACE_TERRAIN_RADIUS, SURFACE_TERRAIN_FALLOFF); + if (factor <= 0D) { + continue; + } + int boundedTargetY = boundedSurfaceTarget(originalY, anchor.meetY()); + if (containsColumn) { + if (precedes(anchor, selectedLocal)) { + localTargetY = boundedTargetY; + selectedLocal = anchor; + } + continue; + } + long influence = Math.round(factor * SURFACE_TERRAIN_INFLUENCE_SCALE); + if (influence <= 0L) { + continue; + } + long weightedInfluence = influence * Math.max(1, anchor.strength()); + totalInfluence += weightedInfluence; + weightedTargetY += weightedInfluence * boundedTargetY; + maximumInfluence = Math.max(maximumInfluence, influence); + } + if (selectedLocal != null) { + return new SurfaceResolution( + localTargetY, + selectedLocal.strength() > 1 + && localTargetY == selectedLocal.meetY()); + } + if (totalInfluence == 0L) { + return new SurfaceResolution(originalY, false); + } + double blendedTargetY = weightedTargetY / (double) totalInfluence; + double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE; + return new SurfaceResolution( + blendSurfaceTarget(originalY, blendedTargetY, factor), false); + } + + private static int boundedSurfaceTarget(int originalY, int meetY) { + int delta = Math.max( + -MAX_SOURCE_SURFACE_DELTA, + Math.min(MAX_SOURCE_SURFACE_DELTA, meetY - originalY)); + return originalY + delta; + } + + private static int blendSurfaceTarget(int originalY, double targetY, double factor) { + double delta = (targetY - originalY) * factor; + int magnitude = (int) Math.round(Math.abs(delta)); + return originalY + (delta < 0D ? -magnitude : magnitude); + } + + private static boolean precedes(SurfaceAnchor candidate, SurfaceAnchor selected) { + if (selected == null) { + return true; + } + if (candidate.strength() != selected.strength()) { + return candidate.strength() > selected.strength(); + } + if (candidate.meetY() != selected.meetY()) { + return candidate.meetY() < selected.meetY(); + } + if (candidate.minX() != selected.minX()) { + return candidate.minX() < selected.minX(); + } + if (candidate.minZ() != selected.minZ()) { + return candidate.minZ() < selected.minZ(); + } + if (candidate.maxX() != selected.maxX()) { + return candidate.maxX() < selected.maxX(); + } + if (candidate.maxZ() != selected.maxZ()) { + return candidate.maxZ() < selected.maxZ(); + } + return false; + } + + private static List collectSourceSurfaceAnchors( + WorldGenLevel world, BoundingBox area, + List targets, + int maximumTemplateCells) { + BoundingBox influenceArea = new BoundingBox( + area.minX() - SURFACE_TERRAIN_RADIUS, area.minY(), + area.minZ() - SURFACE_TERRAIN_RADIUS, + area.maxX() + SURFACE_TERRAIN_RADIUS, area.maxY(), + area.maxZ() + SURFACE_TERRAIN_RADIUS); + List anchors = new ArrayList<>(); + Set seenStarts = Collections.newSetFromMap( + new IdentityHashMap<>()); + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + if (!requiresSourceSurfaceTerrain(target)) { + continue; + } + StructureStart start = target.start(); + if (!seenStarts.add(start)) { + continue; + } + BoundingBox firstBounds = start.getPieces().getFirst().getBoundingBox(); + BlockPos firstCenter = firstBounds.getCenter(); + BlockPos referencePosition = new BlockPos( + firstCenter.getX(), firstBounds.minY(), firstCenter.getZ()); + TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells); + boolean budgetExceeded = false; + for (StructurePiece piece : start.getPieces()) { + if (piece instanceof PoolElementStructurePiece poolPiece) { + StructureTemplatePool.Projection projection = + poolPiece.getElement().getProjection(); + if (!budgetExceeded + && (projection == StructureTemplatePool.Projection.RIGID + || projection == StructureTemplatePool.Projection.TERRAIN_MATCHING) + && intersectsHorizontally(poolPiece.getBoundingBox(), influenceArea)) { + try { + List pieceAnchors = new ArrayList<>(); + addProcessedSurfaceAnchors( + world, influenceArea, poolPiece, referencePosition, + projection, budget, pieceAnchors); + anchors.addAll(pieceAnchors); + } catch (TemplateBudgetExceeded ignored) { + budgetExceeded = true; + warnSourceBudget(target); + } + } + for (JigsawJunction junction : poolPiece.getJunctions()) { + anchors.add(new SurfaceAnchor( + junction.getSourceX(), junction.getSourceX(), + junction.getSourceZ(), junction.getSourceZ(), + junction.getSourceGroundY() - 1, 1)); + } + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + anchors.add(surfaceAnchor(bounds, bounds.minY(), 2)); + } + } + return List.copyOf(anchors); + } + + private static void addProcessedSurfaceAnchors( + WorldGenLevel world, BoundingBox influenceArea, + PoolElementStructurePiece piece, BlockPos referencePosition, + StructureTemplatePool.Projection projection, + TemplateCellBudget budget, + List anchors) { + NativeStructureTemplateOccupancy.OccupancyResult occupancy = + NativeStructureTemplateOccupancy.resolve( + world, piece, referencePosition, influenceArea, + () -> world.getLevel().getStructureManager(), + influenceArea::isInside, budget::consume); + if (!occupancy.resolved()) { + return; + } + int maximumFoundationY = piece.getBoundingBox().minY() + + piece.getGroundLevelDelta(); + Map lowest = + NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells()); + for (NativeStructureTemplateOccupancy.LowestCell cell : lowest.values()) { + budget.consume(1); + if (projection == StructureTemplatePool.Projection.RIGID + && cell.y() > maximumFoundationY) { + continue; + } + int groundY = projection == StructureTemplatePool.Projection.RIGID + ? maximumFoundationY : cell.y(); + BoundingBox column = new BoundingBox( + cell.x(), groundY, cell.z(), cell.x(), groundY, cell.z()); + anchors.add(surfaceAnchor(column, groundY, 2)); + } + } + + private static void warnSourceBudget( + NativeStructureTerrainIntegrator.TerrainTarget target) { + String structureId = target.structureId() == null + ? target.start().getStructure().getClass().getName() + : target.structureId(); + if (WARNED_SOURCE_BUDGET.add(structureId)) { + IrisLogging.warn("Native structure SOURCE fitting for '" + + structureId + "' exceeded its bounded template budget; " + + "skipping remaining processed footprints for this start"); + } + } + + private static VacuumFootprint collectVacuumFootprint( + WorldGenLevel world, BoundingBox area, + List targets, + int maximumTemplateCells) { + BoundingBox influenceArea = new BoundingBox( + area.minX() - SURFACE_TERRAIN_RADIUS, area.minY(), + area.minZ() - SURFACE_TERRAIN_RADIUS, + area.maxX() + SURFACE_TERRAIN_RADIUS, area.maxY(), + area.maxZ() + SURFACE_TERRAIN_RADIUS); + Map anchors = new HashMap<>(); + Map columnCaps = new HashMap<>(); + Set foundationBases = new HashSet<>(); + Set occupiedCells = new HashSet<>(); + Set seenStarts = Collections.newSetFromMap( + new IdentityHashMap<>()); + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + if (target == null || target.terrain() == null + || target.terrain().resolvedMode() != IrisStructureTerrainMode.VACUUM + || target.start() == null || !target.start().isValid() + || !seenStarts.add(target.start())) { + continue; + } + StructureStart start = target.start(); + BoundingBox firstBounds = start.getPieces().getFirst().getBoundingBox(); + BlockPos firstCenter = firstBounds.getCenter(); + BlockPos referencePosition = new BlockPos( + firstCenter.getX(), firstBounds.minY(), firstCenter.getZ()); + Map targetAnchors = new HashMap<>(); + Map targetCaps = new HashMap<>(); + Set targetFoundationBases = new HashSet<>(); + Set targetOccupiedCells = new HashSet<>(); + TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells); + try { + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + if (!intersectsHorizontally(bounds, influenceArea) + || !(piece instanceof PoolElementStructurePiece poolPiece) + || poolPiece.getElement().getProjection() + != StructureTemplatePool.Projection.RIGID) { + continue; + } + addVacuumJunctionAnchors( + anchors, influenceArea, poolPiece); + NativeStructureTemplateOccupancy.OccupancyResult occupancy = + NativeStructureTemplateOccupancy.resolve( + world, poolPiece, referencePosition, influenceArea, + () -> world.getLevel().getStructureManager(), + influenceArea::isInside, budget::consume); + if (!occupancy.resolved()) { + continue; + } + targetOccupiedCells.addAll(occupancy.cells().keySet()); + Map lowest = + NativeStructureTemplateOccupancy.lowestCells( + occupancy.cells()); + int groundY = bounds.minY() + + poolPiece.getGroundLevelDelta() - 1; + for (NativeStructureTemplateOccupancy.LowestCell cell : lowest.values()) { + budget.consume(1); + long column = NativeStructureTemplateOccupancy.columnKey( + cell.x(), cell.z()); + targetCaps.merge( + column, Math.min(cell.y(), groundY), Math::min); + if (cell.occupancy().blocker() + || cell.y() > groundY + 1 + || !NativeStructureTemplateOccupancy.isSolidBase( + cell.occupancy().state())) { + continue; + } + addVacuumAnchor(targetAnchors, cell.x(), cell.z(), + new VacuumAnchor(cell.y() - 1, 2)); + targetFoundationBases.add(BlockPos.asLong( + cell.x(), cell.y(), cell.z())); + } + } + } catch (TemplateBudgetExceeded ignored) { + warnVacuumBudget(target); + continue; + } + targetAnchors.forEach((column, anchor) -> anchors.merge( + column, anchor, NativeStructureSurfaceFitter::preferredVacuumAnchor)); + targetCaps.forEach((column, cap) -> columnCaps.merge( + column, cap, Math::min)); + foundationBases.addAll(targetFoundationBases); + occupiedCells.addAll(targetOccupiedCells); + } + return new VacuumFootprint( + Map.copyOf(anchors), Map.copyOf(columnCaps), + Set.copyOf(foundationBases), Set.copyOf(occupiedCells)); + } + + private static void addVacuumJunctionAnchors( + Map anchors, BoundingBox influenceArea, + PoolElementStructurePiece poolPiece) { + for (JigsawJunction junction : poolPiece.getJunctions()) { + if (junction.getSourceX() < influenceArea.minX() + || junction.getSourceX() > influenceArea.maxX() + || junction.getSourceZ() < influenceArea.minZ() + || junction.getSourceZ() > influenceArea.maxZ()) { + continue; + } + addVacuumAnchor(anchors, + junction.getSourceX(), junction.getSourceZ(), + new VacuumAnchor(junction.getSourceGroundY() - 1, 1)); + } + } + + private static void warnVacuumBudget( + NativeStructureTerrainIntegrator.TerrainTarget target) { + String structureId = target.structureId() == null + ? target.start().getStructure().getClass().getName() + : target.structureId(); + if (WARNED_VACUUM_BUDGET.add(structureId)) { + IrisLogging.warn("Native structure VACUUM fitting for '" + + structureId + "' exceeded its bounded template budget; " + + "using jigsaw junction smoothing for oversized starts"); + } + } + + private static boolean intersectsHorizontally( + BoundingBox first, BoundingBox second) { + return first.maxX() >= second.minX() && first.minX() <= second.maxX() + && first.maxZ() >= second.minZ() && first.minZ() <= second.maxZ(); + } + + private static void addVacuumAnchor( + Map anchors, int x, int z, + VacuumAnchor candidate) { + anchors.merge(NativeStructureTemplateOccupancy.columnKey(x, z), candidate, + NativeStructureSurfaceFitter::preferredVacuumAnchor); + } + + private static VacuumAnchor preferredVacuumAnchor( + VacuumAnchor first, VacuumAnchor second) { + if (first.strength() != second.strength()) { + return first.strength() > second.strength() ? first : second; + } + return first.surfaceY() <= second.surfaceY() ? first : second; + } + + static SurfaceAnchor surfaceAnchor(BoundingBox bounds, int groundY, int strength) { + return new SurfaceAnchor(bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(), + groundY - 1, strength); + } + + static boolean requiresSurfaceTerrain(StructureStart start) { + return requiresSurfaceTerrain(new NativeStructureTerrainIntegrator.TerrainTarget( + null, start, new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))); + } + + static boolean requiresSurfaceTerrain(NativeStructureTerrainIntegrator.TerrainTarget target) { + if (target == null || target.terrain() == null) { + return false; + } + StructureStart start = target.start(); + if (start == null || !start.isValid()) { + return false; + } + IrisStructureTerrainMode mode = target.terrain().resolvedMode(); + return mode == IrisStructureTerrainMode.VACUUM + || requiresSourceSurfaceTerrain(target); + } + + private static boolean requiresSourceSurfaceTerrain( + NativeStructureTerrainIntegrator.TerrainTarget target) { + return target != null && target.terrain() != null + && target.start() != null && target.start().isValid() + && target.terrain().resolvedMode() == IrisStructureTerrainMode.SOURCE + && shouldPrepareSurfaceTerrain( + target.start().getStructure().terrainAdaptation(), + target.start().getStructure().step()); + } + + private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area, + List anchors, + IntBinaryOperator surfaceHeight) { + int width = area.getXSpan(); + int depth = area.getZSpan(); + int[] originalHeights = new int[width * depth]; + int[] targetHeights = new int[width * depth]; + boolean[] rigidBaseSupport = new boolean[width * depth]; + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + int originalY = Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(x, z))); + SurfaceResolution resolution = resolveSurface(anchors, x, z, originalY); + originalHeights[column] = originalY; + targetHeights[column] = Math.max(area.minY(), Math.min( + area.maxY(), resolution.targetY())); + rigidBaseSupport[column] = resolution.rigidBaseSupport(); + } + } + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + applySurfaceColumn(world, position, x, z, + originalHeights[column], targetHeights[column], area.minY(), area.maxY(), + rigidBaseSupport[column]); + } + } + } + + private static void fitVacuumTerrain( + WorldGenLevel world, BoundingBox area, VacuumFootprint footprint, + IntBinaryOperator surfaceHeight) { + int width = area.getXSpan(); + int depth = area.getZSpan(); + int[] originalHeights = new int[width * depth]; + int[] targetHeights = new int[width * depth]; + boolean[] rigidBaseSupport = new boolean[width * depth]; + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + int nominalY = Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(x, z))); + long columnKey = NativeStructureTemplateOccupancy.columnKey(x, z); + VacuumAnchor local = footprint.anchors().get(columnKey); + Integer cap = footprint.columnCaps().get(columnKey); + boolean suppressed = local != null && local.strength() > 1 + && cap != null && cap < local.surfaceY(); + int scanStartY = local != null && local.strength() > 1 && !suppressed + ? Math.max(area.minY(), Math.min(area.maxY(), local.surfaceY())) + : nominalY; + int originalY = resolveVacuumSurfaceHeight( + world, position, x, z, scanStartY, area.minY()); + SurfaceResolution resolution = resolveVacuumSurface( + footprint, x, z, originalY); + originalHeights[column] = originalY; + targetHeights[column] = Math.max(originalY, Math.min( + area.maxY(), resolution.targetY())); + rigidBaseSupport[column] = resolution.rigidBaseSupport() + && targetHeights[column] > originalY; + } + } + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + applySurfaceColumn(world, position, x, z, + originalHeights[column], targetHeights[column], + area.minY(), area.maxY(), rigidBaseSupport[column]); + } + } + } + + private static int resolveVacuumSurfaceHeight( + WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int nominalY, int worldMinY) { + for (int y = nominalY; y >= worldMinY; y--) { + if (isTerrainBlock(world.getBlockState(position.set(x, y, z)))) { + return y; + } + } + return nominalY; + } + + private static int findVacuumFoundationSupport( + WorldGenLevel world, BoundingBox area, + BlockPos.MutableBlockPos position, int x, int startY, int z) { + if (startY < area.minY()) { + return Integer.MIN_VALUE; + } + int maximumY = Math.min(area.maxY(), startY); + for (int y = maximumY; y >= area.minY(); y--) { + BlockState state = world.getBlockState(position.set(x, y, z)); + if (isTerrainBlock(state)) { + return y; + } + if (!state.isAir() && state.getFluidState().isEmpty()) { + return Integer.MIN_VALUE; + } + } + return Integer.MIN_VALUE; + } + + private static boolean intersectsVacuumOccupancy( + Set occupiedCells, int x, int z, int minimumY, int maximumY) { + for (int y = minimumY; y <= maximumY; y++) { + if (occupiedCells.contains(BlockPos.asLong(x, y, z))) { + return true; + } + } + return false; + } + + private static SurfaceResolution resolveVacuumSurface( + VacuumFootprint footprint, int worldX, int worldZ, int originalY) { + long column = NativeStructureTemplateOccupancy.columnKey(worldX, worldZ); + Integer cap = footprint.columnCaps().get(column); + VacuumAnchor local = footprint.anchors().get(column); + if (local != null && local.strength() > 1) { + int localTargetY = cap == null + ? local.surfaceY() : Math.min(local.surfaceY(), cap); + if (localTargetY <= originalY) { + return new SurfaceResolution(originalY, false); + } + return new SurfaceResolution( + localTargetY, localTargetY == local.surfaceY()); + } + long totalInfluence = 0L; + long weightedSurfaceY = 0L; + long maximumInfluence = 0L; + int radiusSquared = SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS; + for (int offsetX = -SURFACE_TERRAIN_RADIUS; + offsetX <= SURFACE_TERRAIN_RADIUS; offsetX++) { + for (int offsetZ = -SURFACE_TERRAIN_RADIUS; + offsetZ <= SURFACE_TERRAIN_RADIUS; offsetZ++) { + int horizontalDistanceSquared = offsetX * offsetX + offsetZ * offsetZ; + if (horizontalDistanceSquared > radiusSquared) { + continue; + } + VacuumAnchor anchor = footprint.anchors().get( + NativeStructureTemplateOccupancy.columnKey( + worldX + offsetX, worldZ + offsetZ)); + if (anchor == null || anchor.surfaceY() <= originalY) { + continue; + } + int verticalDistance = Math.abs(anchor.surfaceY() + 1 - originalY); + long distanceSquared = horizontalDistanceSquared + + (long) verticalDistance * verticalDistance; + double factor = 0D; + if (distanceSquared <= radiusSquared) { + factor = surfaceFactor(Math.sqrt(distanceSquared)); + } + if (anchor.strength() > 1 + && verticalDistance > SURFACE_TERRAIN_RADIUS) { + double rescueProgress = Math.min(1D, + (verticalDistance - SURFACE_TERRAIN_RADIUS) + / (double) SURFACE_TERRAIN_RADIUS); + double rescueWeight = rescueProgress * rescueProgress + * (3D - 2D * rescueProgress); + factor = Math.max(factor, + surfaceFactor(Math.sqrt(horizontalDistanceSquared)) + * rescueWeight); + } + long influence = Math.round( + factor * SURFACE_TERRAIN_INFLUENCE_SCALE); + if (influence <= 0L) { + continue; + } + long weightedInfluence = influence * Math.max(1, anchor.strength()); + totalInfluence += weightedInfluence; + weightedSurfaceY += weightedInfluence * anchor.surfaceY(); + maximumInfluence = Math.max(maximumInfluence, influence); + } + } + if (totalInfluence == 0L) { + return new SurfaceResolution(originalY, false); + } + double blendedSurfaceY = weightedSurfaceY / (double) totalInfluence; + double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE; + int targetY = (int) Math.round( + originalY + ((blendedSurfaceY - originalY) * factor)); + if (cap != null) { + targetY = Math.min(targetY, cap); + } + return new SurfaceResolution(Math.max(originalY, targetY), false); + } + + private static double surfaceFactor(double distance) { + return Math.pow(1D - distance / SURFACE_TERRAIN_RADIUS, + SURFACE_TERRAIN_FALLOFF); + } + + static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int originalY, int targetY, + int worldMinY, int worldMaxY) { + applySurfaceColumn(world, position, x, z, originalY, targetY, + worldMinY, worldMaxY, false); + } + + static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int originalY, int targetY, + int worldMinY, int worldMaxY, + boolean requireRigidBaseSupport) { + if (targetY == originalY) { + if (requireRigidBaseSupport) { + ensureRigidBaseTerrain(world, position, x, z, targetY, worldMinY); + } + return; + } + SurfaceMaterials materials = resolveSurfaceMaterials(world, position, x, z, originalY, worldMinY); + if (targetY < originalY) { + BlockState clearedState = clearSurfaceDecorationAndResolveFill( + world, position, x, z, originalY, worldMaxY); + for (int y = originalY; y > targetY; y--) { + world.setBlock(position.set(x, y, z), clearedState, 2); + } + world.setBlock(position.set(x, targetY, z), materials.surface(), 2); + if (requireRigidBaseSupport) { + ensureRigidBaseTerrain(world, position, x, z, targetY, worldMinY, materials); + } + return; + } + for (int y = originalY + 1; y < targetY; y++) { + position.set(x, y, z); + if (!NativeStructureVegetationClearer.isTreeBlock(world.getBlockState(position))) { + world.setBlock(position, materials.subsurface(), 2); + } + } + position.set(x, targetY, z); + if (!NativeStructureVegetationClearer.isTreeBlock(world.getBlockState(position))) { + world.setBlock(position, materials.surface(), 2); + } + if (requireRigidBaseSupport) { + ensureRigidBaseTerrain(world, position, x, z, targetY, worldMinY, materials); + } + } + + private static void ensureRigidBaseTerrain(WorldGenLevel world, + BlockPos.MutableBlockPos position, + int x, int z, int targetY, int worldMinY) { + int supportY = targetY - 1; + boolean targetIsTerrain = isTerrainBlock(world.getBlockState(position.set(x, targetY, z))); + boolean supportIsTerrain = supportY < worldMinY + || isTerrainBlock(world.getBlockState(position.set(x, supportY, z))); + if (targetIsTerrain && supportIsTerrain) { + return; + } + SurfaceMaterials materials = resolveSurfaceMaterials( + world, position, x, z, targetY, worldMinY); + ensureRigidBaseTerrain(world, position, x, z, targetY, worldMinY, materials); + } + + private static void ensureRigidBaseTerrain(WorldGenLevel world, + BlockPos.MutableBlockPos position, + int x, int z, int targetY, int worldMinY, + SurfaceMaterials materials) { + int supportY = targetY - 1; + position.set(x, targetY, z); + if (!isTerrainBlock(world.getBlockState(position))) { + world.setBlock(position, materials.surface(), 2); + } + if (supportY < worldMinY) { + return; + } + position.set(x, supportY, z); + if (!isTerrainBlock(world.getBlockState(position))) { + world.setBlock(position, materials.subsurface(), 2); + } + } + + private static BlockState clearSurfaceDecorationAndResolveFill( + WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int originalY, int worldMaxY) { + BlockState air = Blocks.AIR.defaultBlockState(); + for (int y = originalY + 1; y <= worldMaxY; y++) { + BlockState state = world.getBlockState(position.set(x, y, z)); + if (state.isAir()) { + return air; + } + if (!state.getFluidState().isEmpty()) { + BlockState fluid = state.getFluidState().createLegacyBlock(); + if (state != fluid) { + world.setBlock(position, fluid, 2); + } + return fluid; + } + if (state.isSolid() || NativeStructureVegetationClearer.isTreeBlock(state)) { + return air; + } + world.setBlock(position, air, 2); + } + return air; + } + + private static SurfaceMaterials resolveSurfaceMaterials(WorldGenLevel world, + BlockPos.MutableBlockPos position, + int x, int z, int originalY, + int worldMinY) { + BlockState surface = world.getBlockState(position.set(x, originalY, z)); + BlockState subsurface = null; + for (int y = originalY - 1; y >= worldMinY; y--) { + BlockState candidate = world.getBlockState(position.set(x, y, z)); + if (isTerrainBlock(candidate)) { + subsurface = candidate; + break; + } + } + if (subsurface == null) { + subsurface = isTerrainBlock(surface) ? surface : Blocks.STONE.defaultBlockState(); + } + if (!isTerrainBlock(surface)) { + surface = subsurface; + } + return new SurfaceMaterials(surface, subsurface); + } + + private static boolean isTerrainBlock(BlockState state) { + return state.isSolid() && !NativeStructureVegetationClearer.isTreeBlock(state); + } + + record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) { + } + + private record SurfaceMaterials(BlockState surface, BlockState subsurface) { + } + + private record SurfaceResolution(int targetY, boolean rigidBaseSupport) { + } + + private record VacuumFootprint( + Map anchors, Map columnCaps, + Set foundationBases, Set occupiedCells) { + } + + private record VacuumAnchor(int surfaceY, int strength) { + } + + public static final class VacuumFoundationPlan { + private static final VacuumFoundationPlan EMPTY = + new VacuumFoundationPlan(List.of(), Set.of()); + + private final List foundationBases; + private final Set occupiedCells; + + private VacuumFoundationPlan( + List foundationBases, Set occupiedCells) { + this.foundationBases = foundationBases; + this.occupiedCells = occupiedCells; + } + + private static VacuumFoundationPlan empty() { + return EMPTY; + } + + private static VacuumFoundationPlan create( + Set packedBases, Set packedOccupancy, + BoundingBox area) { + if (packedBases.isEmpty()) { + return EMPTY; + } + List bases = new ArrayList<>(packedBases.size()); + Set columns = new HashSet<>(); + for (long packedBase : packedBases) { + BlockPos base = BlockPos.of(packedBase); + if (!area.isInside(base)) { + continue; + } + bases.add(packedBase); + columns.add(NativeStructureTemplateOccupancy.columnKey( + base.getX(), base.getZ())); + } + if (bases.isEmpty()) { + return EMPTY; + } + bases.sort((first, second) -> { + int yOrder = Integer.compare( + BlockPos.of(first).getY(), BlockPos.of(second).getY()); + return yOrder == 0 ? Long.compare(first, second) : yOrder; + }); + Set occupancy = new HashSet<>(); + for (long packedCell : packedOccupancy) { + BlockPos cell = BlockPos.of(packedCell); + if (columns.contains(NativeStructureTemplateOccupancy.columnKey( + cell.getX(), cell.getZ()))) { + occupancy.add(packedCell); + } + } + return new VacuumFoundationPlan( + List.copyOf(bases), Set.copyOf(occupancy)); + } + } + + private static final class TemplateCellBudget { + private final int maximum; + private int consumed; + + private TemplateCellBudget(int maximum) { + if (maximum < 0) { + throw new IllegalArgumentException( + "Native structure surface template budget cannot be negative"); + } + this.maximum = maximum; + } + + private void consume(int amount) { + if (amount < 0 || consumed > maximum - amount) { + throw TemplateBudgetExceeded.INSTANCE; + } + consumed += amount; + } + } + + private static final class TemplateBudgetExceeded extends RuntimeException { + private static final TemplateBudgetExceeded INSTANCE = + new TemplateBudgetExceeded(); + + private TemplateBudgetExceeded() { + super(null, null, false, false); + } + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceSupportBuilder.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceSupportBuilder.java new file mode 100644 index 000000000..27e929c11 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceSupportBuilder.java @@ -0,0 +1,299 @@ +package art.arcane.iris.nativegen; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +final class NativeStructureSurfaceSupportBuilder { + private static final int MAX_BATCH_CELLS = 524_288; + private static final int MAX_BRIDGE_SPAN = 2; + + private NativeStructureSurfaceSupportBuilder() { + } + + static Set bridgeRigidPieceSupport( + WorldGenLevel world, BoundingBox area, + List targets, + Supplier templates) { + if (targets == null || targets.isEmpty()) { + return Set.of(); + } + Map planned; + try { + BatchCellBudget budget = new BatchCellBudget(); + List requests = new ArrayList<>(); + Set seenStarts = Collections.newSetFromMap( + new IdentityHashMap<>()); + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + if (target == null + || !NativeStructureSurfaceFitter.requiresSurfaceTerrain(target) + || !seenStarts.add(target.start())) { + continue; + } + collectTargetRequests( + world, area, target, templates, budget, requests); + } + planned = planWrites(world, area, requests, budget); + } catch (BatchBudgetExceeded ignored) { + return Set.of(); + } + if (planned.isEmpty()) { + return Set.of(); + } + List> ordered = new ArrayList<>(planned.entrySet()); + ordered.sort(Map.Entry.comparingByKey()); + Set written = new HashSet<>(); + for (Map.Entry entry : ordered) { + BlockPos position = BlockPos.of(entry.getKey()); + BlockState existing = world.getBlockState(position); + if (!existing.isSolid() + && !NativeStructureVegetationClearer.isTreeBlock(existing) + && existing.getFluidState().isEmpty() + && world.setBlock(position, entry.getValue(), 2)) { + written.add(entry.getKey()); + } + } + return Set.copyOf(written); + } + + private static void collectTargetRequests( + WorldGenLevel world, BoundingBox area, + NativeStructureTerrainIntegrator.TerrainTarget target, + Supplier templates, + BatchCellBudget budget, List requests) { + if (!NativeStructureSurfaceFitter.requiresSurfaceTerrain(target)) { + return; + } + List anchors = rigidAnchors(target.start()); + if (anchors.size() < 2) { + return; + } + BlockPos referencePos = referencePosition(target.start()); + for (RigidAnchor upper : anchors) { + Map lowerMeets = candidateLowerMeets( + area, upper, anchors, budget); + if (lowerMeets.isEmpty()) { + continue; + } + NativeStructureTemplateOccupancy.OccupancyResult occupancy = + NativeStructureTemplateOccupancy.resolve( + world, upper.piece(), referencePos, area, templates, + position -> retain(position, area, lowerMeets), budget::consume); + if (!occupancy.resolved()) { + continue; + } + Map lowest = + NativeStructureTemplateOccupancy.lowestCells(occupancy.cells()); + for (Map.Entry entry + : lowest.entrySet()) { + NativeStructureTemplateOccupancy.LowestCell cell = entry.getValue(); + Integer lowerMeet = lowerMeets.get(entry.getKey()); + if (lowerMeet == null || cell.occupancy().blocker() + || !NativeStructureTemplateOccupancy.isSolidBase( + cell.occupancy().state()) + || cell.y() - lowerMeet < 2 + || cell.y() - lowerMeet > MAX_BRIDGE_SPAN) { + continue; + } + budget.consume(1); + requests.add(new SupportRequest( + cell.x(), cell.z(), lowerMeet, cell.y())); + } + } + } + + private static List rigidAnchors(StructureStart start) { + List anchors = new ArrayList<>(); + for (StructurePiece piece : start.getPieces()) { + if (!(piece instanceof PoolElementStructurePiece poolPiece) + || poolPiece.getElement().getProjection() + != StructureTemplatePool.Projection.RIGID) { + continue; + } + BoundingBox bounds = poolPiece.getBoundingBox(); + int meetY = bounds.minY() + poolPiece.getGroundLevelDelta() - 1; + anchors.add(new RigidAnchor(poolPiece, bounds, meetY)); + } + return List.copyOf(anchors); + } + + private static Map candidateLowerMeets( + BoundingBox area, RigidAnchor upper, List anchors, + BatchCellBudget budget) { + Map lowerMeets = new HashMap<>(); + for (RigidAnchor lower : anchors) { + budget.consume(1); + int span = upper.meetY() - lower.meetY(); + if (lower.piece() == upper.piece() || span < 1 + || span > MAX_BRIDGE_SPAN + || lower.meetY() < area.minY() || lower.meetY() > area.maxY()) { + continue; + } + int minimumX = Math.max(area.minX(), + Math.max(upper.bounds().minX(), lower.bounds().minX())); + int maximumX = Math.min(area.maxX(), + Math.min(upper.bounds().maxX(), lower.bounds().maxX())); + int minimumZ = Math.max(area.minZ(), + Math.max(upper.bounds().minZ(), lower.bounds().minZ())); + int maximumZ = Math.min(area.maxZ(), + Math.min(upper.bounds().maxZ(), lower.bounds().maxZ())); + if (minimumX > maximumX || minimumZ > maximumZ) { + continue; + } + for (int x = minimumX; x <= maximumX; x++) { + for (int z = minimumZ; z <= maximumZ; z++) { + budget.consume(1); + lowerMeets.merge(columnKey(x, z), lower.meetY(), Math::max); + } + } + } + return lowerMeets; + } + + private static boolean retain( + BlockPos position, BoundingBox area, Map lowerMeets) { + if (!area.isInside(position)) { + return false; + } + Integer lowerMeet = lowerMeets.get(columnKey(position.getX(), position.getZ())); + return lowerMeet != null && position.getY() <= lowerMeet + MAX_BRIDGE_SPAN; + } + + private static Map planWrites( + WorldGenLevel world, BoundingBox area, + List requests, BatchCellBudget budget) { + List eligible = new ArrayList<>(); + Set materialKeys = new HashSet<>(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (SupportRequest request : requests) { + BlockState lowerTerrain = world.getBlockState(position.set( + request.x(), request.lowerMeetY(), request.z())); + if (!isTerrainSupport(lowerTerrain) + || !gapIsClear(world, area, position, request, budget)) { + continue; + } + eligible.add(request); + materialKeys.add(new MaterialKey( + request.x(), request.z(), request.lowerMeetY())); + } + Map materials = new HashMap<>(); + for (MaterialKey key : materialKeys) { + materials.put(key, resolveSupportState( + world, position, key.x(), key.z(), key.lowerMeetY(), budget)); + } + Map planned = new HashMap<>(); + Set conflicts = new HashSet<>(); + eligible.sort(Comparator.comparingInt(SupportRequest::x) + .thenComparingInt(SupportRequest::z) + .thenComparingInt(SupportRequest::lowerMeetY) + .thenComparingInt(SupportRequest::baseY)); + for (SupportRequest request : eligible) { + MaterialKey materialKey = new MaterialKey( + request.x(), request.z(), request.lowerMeetY()); + BlockState material = materials.get(materialKey); + for (int y = request.lowerMeetY() + 1; y < request.baseY(); y++) { + budget.consume(1); + long positionKey = BlockPos.asLong(request.x(), y, request.z()); + if (conflicts.contains(positionKey)) { + continue; + } + BlockState existing = planned.putIfAbsent(positionKey, material); + if (existing != null && !existing.equals(material)) { + planned.remove(positionKey); + conflicts.add(positionKey); + } + } + } + return planned; + } + + private static boolean gapIsClear( + WorldGenLevel world, BoundingBox area, BlockPos.MutableBlockPos position, + SupportRequest request, BatchCellBudget budget) { + for (int y = request.lowerMeetY() + 1; y < request.baseY(); y++) { + budget.consume(1); + if (!area.isInside(position.set(request.x(), y, request.z()))) { + return false; + } + BlockState state = world.getBlockState(position); + if (state.isSolid() || NativeStructureVegetationClearer.isTreeBlock(state) + || !state.getFluidState().isEmpty()) { + return false; + } + } + return true; + } + + private static BlockState resolveSupportState( + WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int lowerMeetY, BatchCellBudget budget) { + budget.consume(1); + BlockState below = world.getBlockState(position.set(x, lowerMeetY - 1, z)); + if (isTerrainSupport(below)) { + return below; + } + return world.getBlockState(position.set(x, lowerMeetY, z)); + } + + private static boolean isTerrainSupport(BlockState state) { + return state.isSolid() + && !NativeStructureVegetationClearer.isTreeBlock(state) + && state.getFluidState().isEmpty(); + } + + private static BlockPos referencePosition(StructureStart start) { + BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); + BlockPos center = bounds.getCenter(); + return new BlockPos(center.getX(), bounds.minY(), center.getZ()); + } + + private static long columnKey(int x, int z) { + return (long) x << 32 ^ z & 0xffffffffL; + } + + private record RigidAnchor( + PoolElementStructurePiece piece, BoundingBox bounds, int meetY) { + } + + private record SupportRequest(int x, int z, int lowerMeetY, int baseY) { + } + + private record MaterialKey(int x, int z, int lowerMeetY) { + } + + private static final class BatchCellBudget { + private int consumed; + + private void consume(int amount) { + if (amount < 0 || consumed > MAX_BATCH_CELLS - amount) { + throw BatchBudgetExceeded.INSTANCE; + } + consumed += amount; + } + } + + private static final class BatchBudgetExceeded extends RuntimeException { + private static final BatchBudgetExceeded INSTANCE = new BatchBudgetExceeded(); + + private BatchBudgetExceeded() { + super(null, null, false, false); + } + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancy.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancy.java new file mode 100644 index 000000000..f904a2127 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancy.java @@ -0,0 +1,165 @@ +package art.arcane.iris.nativegen; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +final class NativeStructureTemplateOccupancy { + private static final OccupancyCell BLOCKER = new OccupancyCell(null, true); + + private NativeStructureTemplateOccupancy() { + } + + static OccupancyResult resolve( + WorldGenLevel world, PoolElementStructurePiece piece, + BlockPos referencePosition, BoundingBox processingArea, + Supplier templates, + PositionFilter filter, CellBudget budget) { + List leaves = new ArrayList<>(); + if (!flattenSingles(piece.getElement(), leaves)) { + return new OccupancyResult(false, Map.of()); + } + List placements = new ArrayList<>(leaves.size()); + Map occupancy = new HashMap<>(); + for (SinglePoolElement leaf : leaves) { + StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings( + leaf, piece, processingArea); + StructureTemplate template = NativeStructureReflection.resolveTemplate(leaf, templates); + List rawBlocks = + NativeStructureReflection.resolveTemplateBlocks( + template, settings, piece.getPosition()); + budget.consume(rawBlocks.size()); + placements.add(new LeafPlacement(settings, rawBlocks, template)); + for (StructureTemplate.StructureBlockInfo raw : rawBlocks) { + BlockPos position = piece.getPosition().offset( + StructureTemplate.calculateRelativePosition(settings, raw.pos())); + if (filter.retain(position)) { + occupancy.put(position.asLong(), BLOCKER); + } + } + } + for (LeafPlacement placement : placements) { + List processed = + NativeStructureReflection.processTemplateBlocks( + world, piece.getPosition(), referencePosition, + placement.settings(), placement.rawBlocks(), placement.template()); + budget.consume(processed.size()); + for (StructureTemplate.StructureBlockInfo block : processed) { + BlockPos position = block.pos(); + if (!filter.retain(position)) { + continue; + } + BlockState state = block.state() + .mirror(placement.settings().getMirror()) + .rotate(placement.settings().getRotation()); + occupancy.put(position.asLong(), new OccupancyCell(state, false)); + } + } + return new OccupancyResult(true, occupancy); + } + + static Map lowestCells(Map occupancy) { + Map lowest = new HashMap<>(); + for (Map.Entry entry : occupancy.entrySet()) { + OccupancyCell cell = entry.getValue(); + BlockPos position = BlockPos.of(entry.getKey()); + long column = columnKey(position.getX(), position.getZ()); + LowestCell current = lowest.get(column); + if (current == null || position.getY() < current.y()) { + lowest.put(column, new LowestCell( + position.getX(), position.getY(), position.getZ(), cell)); + } + } + return lowest; + } + + static Map lowestProcessedSolidCells(Map occupancy) { + Map lowest = new HashMap<>(); + for (Map.Entry entry : occupancy.entrySet()) { + OccupancyCell cell = entry.getValue(); + if (cell.blocker() || !isSolidBase(cell.state())) { + continue; + } + BlockPos position = BlockPos.of(entry.getKey()); + long column = columnKey(position.getX(), position.getZ()); + LowestCell current = lowest.get(column); + if (current == null || position.getY() < current.y()) { + lowest.put(column, new LowestCell( + position.getX(), position.getY(), position.getZ(), cell)); + } + } + return lowest; + } + + static boolean isSolidBase(BlockState state) { + return state != null && state.isSolid() + && !state.is(Blocks.STRUCTURE_VOID) + && !state.is(Blocks.JIGSAW) + && state.getFluidState().isEmpty(); + } + + static long columnKey(int x, int z) { + return (long) x << 32 ^ z & 0xffffffffL; + } + + private static boolean flattenSingles( + StructurePoolElement element, List leaves) { + if (element instanceof ListPoolElement listElement) { + for (StructurePoolElement child : listElement.getElements()) { + if (!flattenSingles(child, leaves)) { + return false; + } + } + return true; + } + if (element == EmptyPoolElement.INSTANCE) { + return true; + } + if (element instanceof SinglePoolElement singleElement) { + leaves.add(singleElement); + return true; + } + return false; + } + + @FunctionalInterface + interface PositionFilter { + boolean retain(BlockPos position); + } + + @FunctionalInterface + interface CellBudget { + void consume(int amount); + } + + record OccupancyResult(boolean resolved, Map cells) { + } + + record OccupancyCell(BlockState state, boolean blocker) { + } + + record LowestCell(int x, int y, int z, OccupancyCell occupancy) { + } + + private record LeafPlacement( + StructurePlaceSettings settings, + List rawBlocks, + StructureTemplate template) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplatePoolBounds.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplatePoolBounds.java new file mode 100644 index 000000000..c5335542a --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplatePoolBounds.java @@ -0,0 +1,228 @@ +package art.arcane.iris.nativegen; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.FeaturePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.pools.alias.DirectPoolAlias; +import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding; +import net.minecraft.world.level.levelgen.structure.pools.alias.RandomGroupPoolAlias; +import net.minecraft.world.level.levelgen.structure.pools.alias.RandomPoolAlias; +import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public final class NativeStructureTemplatePoolBounds { + private NativeStructureTemplatePoolBounds() { + } + + public static int sourceHorizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + JigsawStructure source) { + Registry pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL); + return sourceHorizontalSpan( + templateManager, + source.getStartPool(), + source.getPoolAliases(), + key -> pools.getValue(key.identifier())); + } + + public static int horizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + String templatePoolKey) { + Identifier identifier = Identifier.tryParse(templatePoolKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered template pool key: " + templatePoolKey); + } + Registry pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL); + StructureTemplatePool pool = pools.getValue(identifier); + if (pool == null) { + throw new IllegalArgumentException("Registered template pool does not exist: " + templatePoolKey); + } + return horizontalSpan(pool, templateManager); + } + + public static int horizontalSpan(RegistryAccess registryAccess, + StructureTemplateManager templateManager, + JigsawStructure source, + String templatePoolKey) { + Identifier identifier = Identifier.tryParse(templatePoolKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered template pool key: " + templatePoolKey); + } + Registry pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL); + ResourceKey startPoolKey = ResourceKey.create( + Registries.TEMPLATE_POOL, identifier); + StructureTemplatePool startPool = pools.getValue(identifier); + if (startPool == null) { + throw new IllegalArgumentException("Registered template pool does not exist: " + templatePoolKey); + } + return sourceHorizontalSpan( + templateManager, + startPool, + startPoolKey, + source.getPoolAliases(), + key -> pools.getValue(key.identifier())); + } + + static int sourceHorizontalSpan(StructureTemplateManager templateManager, + Holder startPool, + List aliases, + Function, StructureTemplatePool> poolLookup) { + return sourceHorizontalSpan(templateManager, startPool.value(), + startPool.unwrapKey().orElse(null), aliases, poolLookup); + } + + static int sourceHorizontalSpan(StructureTemplateManager templateManager, + StructureTemplatePool startPool, + ResourceKey startPoolKey, + List aliases, + Function, StructureTemplatePool> poolLookup) { + int maximumSpan = directHorizontalSpan(startPool, templateManager); + if (startPoolKey == null) { + return maximumSpan; + } + Set> targets = new HashSet<>(); + for (PoolAliasBinding binding : aliases) { + collectTargets(binding, startPoolKey, targets); + } + for (ResourceKey target : targets) { + StructureTemplatePool targetPool = poolLookup.apply(target); + if (targetPool == null) { + throw new IllegalStateException("Jigsaw start-pool alias target does not exist: " + + target.identifier()); + } + maximumSpan = Math.max(maximumSpan, directHorizontalSpan(targetPool, templateManager)); + } + return maximumSpan; + } + + private static void collectTargets(PoolAliasBinding binding, + ResourceKey startPoolKey, + Set> targets) { + if (binding instanceof DirectPoolAlias direct) { + if (direct.alias().equals(startPoolKey)) { + targets.add(direct.target()); + } + return; + } + if (binding instanceof RandomPoolAlias random) { + if (random.alias().equals(startPoolKey)) { + random.targets().unwrap().forEach(entry -> targets.add(entry.value())); + } + return; + } + if (binding instanceof RandomGroupPoolAlias group) { + group.groups().unwrap().forEach(entry -> entry.value().forEach( + nested -> collectTargets(nested, startPoolKey, targets))); + return; + } + throw new IllegalStateException("Unsupported jigsaw pool alias type: " + + binding.getClass().getName()); + } + + static int horizontalSpan(StructureTemplatePool pool, + StructureTemplateManager templateManager) { + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + return horizontalSpan(pool, templateManager, visited); + } + + private static int horizontalSpan(StructureTemplatePool pool, + StructureTemplateManager templateManager, + Set visited) { + if (!visited.add(pool)) { + return 0; + } + int maximumSpan = directHorizontalSpan(pool, templateManager); + Holder fallback = pool.getFallback(); + if (fallback == null || !fallback.isBound()) { + throw new IllegalStateException("Template pool has an unresolved fallback"); + } + return Math.max(maximumSpan, horizontalSpan(fallback.value(), templateManager, visited)); + } + + private static int directHorizontalSpan(StructureTemplatePool pool, + StructureTemplateManager templateManager) { + int maximumSpan = 0; + for (Pair entry : pool.getTemplates()) { + StructurePoolElement element = entry.getFirst(); + validateElement(element, templateManager); + maximumSpan = Math.max(maximumSpan, horizontalSpan(element, templateManager)); + } + return maximumSpan; + } + + private static void validateElement(StructurePoolElement element, + StructureTemplateManager templateManager) { + if (element == EmptyPoolElement.INSTANCE || element instanceof FeaturePoolElement) { + return; + } + if (element instanceof SinglePoolElement single) { + Identifier template; + try { + template = single.getTemplateLocation(); + } catch (RuntimeException inlineTemplate) { + return; + } + if (templateManager.get(template).isEmpty()) { + throw new IllegalStateException("Template pool element does not resolve a structure template: " + + template); + } + return; + } + if (element instanceof ListPoolElement list) { + for (StructurePoolElement child : list.getElements()) { + validateElement(child, templateManager); + } + return; + } + } + + private static int horizontalSpan(StructurePoolElement element, + StructureTemplateManager templateManager) { + if (element == EmptyPoolElement.INSTANCE) { + return 0; + } + long maximumSpan = 0L; + for (Rotation rotation : Rotation.values()) { + BoundingBox box; + try { + box = element.getBoundingBox(templateManager, BlockPos.ZERO, rotation); + } catch (RuntimeException | LinkageError error) { + throw new IllegalStateException("Template pool element " + + element.getClass().getName() + + " could not provide bounded geometry for rotation " + rotation, error); + } + if (box == null) { + throw new IllegalStateException("Template pool element " + + element.getClass().getName() + + " returned no bounded geometry for rotation " + rotation); + } + long xSpan = (long) box.maxX() - box.minX() + 1L; + long zSpan = (long) box.maxZ() - box.minZ() + 1L; + maximumSpan = Math.max(maximumSpan, Math.max(xSpan, zSpan)); + } + if (maximumSpan < 0L || maximumSpan > Integer.MAX_VALUE) { + throw new IllegalStateException("Template pool element has an unbounded horizontal span"); + } + return (int) maximumSpan; + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTerrainIntegrator.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTerrainIntegrator.java new file mode 100644 index 000000000..47eeaddb3 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTerrainIntegrator.java @@ -0,0 +1,1096 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.mantle.components.StructureCarveEnvelope; +import art.arcane.iris.engine.mantle.components.StructureCarvingFootprint; +import art.arcane.iris.engine.object.IrisMaterialPalette; +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.util.project.noise.CNG; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.WorldGenLevel; +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.dimension.BuiltinDimensionTypes; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction; +import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +public final class NativeStructureTerrainIntegrator { + private static final long CARVE_CEILING_ROLL_SIGNATURE = 0x2A17L; + private static final long CARVE_FLOOR_ROLL_SIGNATURE = 0x5B3DL; + private static final long CARVE_LOBE_SIGNATURE = 0x7C41L; + private static final int MAX_CACHED_CARVE_CELLS = 2_000_000; + private static final int MAX_CARVE_COLUMNS = 2_000_000; + private static final int MAX_TEMPLATE_OCCUPANCY_CELLS = 4_194_304; + private static final int SOURCE_BURY_HORIZONTAL_RADIUS = 6; + private static final int SOURCE_BURY_VERTICAL_RADIUS = 12; + private static final int SOURCE_ENCAPSULATE_RADIUS = 12; + private static final int SOURCE_JUNCTION_RADIUS = 12; + private static final int SOURCE_MATERIAL_SAMPLE_RADIUS = 8; + private static final List TEMPLATE_VOID_BLOCKS = List.of(Blocks.AIR, Blocks.STRUCTURE_VOID); + private static final Set VANILLA_SOURCE_TERRAIN_BLOCKS = Set.of( + Blocks.STONE, Blocks.GRANITE, Blocks.DIORITE, Blocks.ANDESITE, + Blocks.TUFF, Blocks.DEEPSLATE, Blocks.NETHERRACK, Blocks.BASALT, + Blocks.BLACKSTONE, Blocks.DIRT, Blocks.COARSE_DIRT, Blocks.ROOTED_DIRT, + Blocks.GRASS_BLOCK, Blocks.PODZOL, Blocks.MYCELIUM, Blocks.MUD, + Blocks.MOSS_BLOCK, Blocks.SAND, Blocks.RED_SAND, Blocks.TERRACOTTA, + Blocks.CRIMSON_NYLIUM, Blocks.WARPED_NYLIUM, Blocks.SNOW_BLOCK, + Blocks.ICE, Blocks.PACKED_ICE, Blocks.BLUE_ICE, Blocks.SOUL_SAND, + Blocks.SOUL_SOIL, Blocks.END_STONE, Blocks.GRAVEL, Blocks.CLAY, + Blocks.CALCITE, Blocks.DRIPSTONE_BLOCK, Blocks.SANDSTONE, + Blocks.RED_SANDSTONE, Blocks.SCULK); + // Weakly keyed on the StructureStart so retired starts (world unload, generation done) + // are collectable instead of pinned forever by a process-wide static; values hold only + // primitives, so there is no value-to-key cycle defeating the weak keys. + private static final Map> CARVE_FOOTPRINTS = + new WeakHashMap<>(16); + private static final ConcurrentHashMap> + CARVE_FOOTPRINT_BUILDS = new ConcurrentHashMap<>(); + private static int cachedCarveCells; + + private NativeStructureTerrainIntegrator() { + } + + public static IrisStructureTerrain resolveNativeTerrain(StructureStart start, + IrisStructureTerrain configuredTerrain) { + return configuredTerrain == null + ? new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE) + : configuredTerrain; + } + + static void integrateTerrain(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureTerrain configuredTerrain, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver) { + SourceTerrainSnapshot sourceTerrain = requiresSourceTerrainFill(start, configuredTerrain) + ? captureSourceTerrain(world, area, start, configuredTerrain) : null; + integrateTerrain(world, area, structureId, start, configuredTerrain, + paletteBlockResolver, sourceTerrain); + } + + static void integrateTerrain(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureTerrain configuredTerrain, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver, + SourceTerrainSnapshot sourceTerrain) { + IrisStructureTerrain terrain = configuredTerrain == null + ? new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE) + : configuredTerrain; + IrisStructureTerrainMode mode = terrain.resolvedMode(); + if (mode == IrisStructureTerrainMode.SOURCE) { + integrateSourceTerrain(world, area, start, sourceTerrain); + return; + } + if (mode == IrisStructureTerrainMode.PRESERVE) { + return; + } + if (mode == IrisStructureTerrainMode.VACUUM) { + return; + } + if (mode == IrisStructureTerrainMode.ENCASE) { + encasePieces(world, area, structureId, start, terrain, paletteBlockResolver); + return; + } + if (mode != IrisStructureTerrainMode.BORE && mode != IrisStructureTerrainMode.FORCE_CARVE) { + throw new IllegalStateException("Native structure terrain mode " + mode + + " is not implemented for '" + structureId + "'"); + } + IrisStructureCarveShape shape = mode == IrisStructureTerrainMode.BORE + ? IrisStructureCarveShape.BOX : terrain.resolvedShape(); + if (shape == IrisStructureCarveShape.BOX) { + carvePieceBoxes(world, area, start, terrain); + return; + } + carveOrganicColumns(world, area, organicCarve( + carveFootprint(start, Math.max(0, terrain.getHorizontalPadding()), + () -> world.getLevel().getStructureManager()), + terrain, shape, carveNoiseIdentity(world, structureId, start))); + } + + static SourceTerrainSnapshot captureSourceTerrain( + WorldGenLevel world, BoundingBox area, List targets) { + BitSet requiredLayers = new BitSet(area.getYSpan()); + boolean requiresSnapshot = false; + for (TerrainTarget target : targets) { + if (target != null && requiresSourceTerrainFill(target.start(), target.terrain())) { + requiresSnapshot = true; + markSourceTerrainLayers(area, target.start(), target.terrain(), requiredLayers); + } + } + return requiresSnapshot ? SourceTerrainSnapshot.capture(world, area, requiredLayers) : null; + } + + private static SourceTerrainSnapshot captureSourceTerrain( + WorldGenLevel world, BoundingBox area, StructureStart start, + IrisStructureTerrain configuredTerrain) { + BitSet requiredLayers = new BitSet(area.getYSpan()); + markSourceTerrainLayers(area, start, configuredTerrain, requiredLayers); + return SourceTerrainSnapshot.capture(world, area, requiredLayers); + } + + private static void markSourceTerrainLayers( + BoundingBox area, StructureStart start, IrisStructureTerrain configuredTerrain, + BitSet requiredLayers) { + if (!requiresSourceTerrainFill(start, configuredTerrain)) { + return; + } + TerrainAdjustment adjustment = start.getStructure().terrainAdaptation(); + for (StructurePiece piece : start.getPieces()) { + if (!isSourceRigidPiece(piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + long horizontalDistanceSquared = minimumHorizontalDistanceSquared(area, bounds); + if (adjustment == TerrainAdjustment.BURY) { + long remainingDistanceSquared = (long) SOURCE_BURY_VERTICAL_RADIUS + * SOURCE_BURY_VERTICAL_RADIUS - 1L - horizontalDistanceSquared * 4L; + if (remainingDistanceSquared < 0L) { + continue; + } + int verticalRadius = (int) Math.floor(Math.sqrt(remainingDistanceSquared)); + long groundY = bounds.minY(); + if (piece instanceof PoolElementStructurePiece poolPiece) { + groundY += poolPiece.getGroundLevelDelta(); + } + markLayers(area, requiredLayers, + groundY - verticalRadius, groundY + verticalRadius); + } else { + long remainingDistanceSquared = (long) SOURCE_ENCAPSULATE_RADIUS + * SOURCE_ENCAPSULATE_RADIUS - 1L - horizontalDistanceSquared; + if (remainingDistanceSquared < 0L) { + continue; + } + int verticalRadius = (int) Math.floor(Math.sqrt(remainingDistanceSquared)); + markLayers(area, requiredLayers, + (long) bounds.minY() - verticalRadius, + (long) bounds.maxY() + verticalRadius); + } + } + markSourceJunctionLayers(area, start, requiredLayers); + } + + private static void markSourceJunctionLayers( + BoundingBox area, StructureStart start, BitSet requiredLayers) { + Set anchors = sourceJunctionAnchors(start); + for (JunctionAnchor anchor : anchors) { + long deltaX = intervalDistance(area.minX(), area.maxX(), anchor.x(), anchor.x()); + long deltaZ = intervalDistance(area.minZ(), area.maxZ(), anchor.z(), anchor.z()); + long horizontalDistanceSquared = deltaX * deltaX + deltaZ * deltaZ; + long remainingDistanceSquared = (long) SOURCE_JUNCTION_RADIUS + * SOURCE_JUNCTION_RADIUS - 1L - horizontalDistanceSquared; + if (remainingDistanceSquared < 1L) { + continue; + } + int verticalRadius = (int) Math.floor(Math.sqrt(remainingDistanceSquared)); + markLayers(area, requiredLayers, + (long) anchor.y() - verticalRadius, (long) anchor.y() - 1L); + } + } + + private static long minimumHorizontalDistanceSquared(BoundingBox area, BoundingBox bounds) { + long deltaX = intervalDistance(area.minX(), area.maxX(), bounds.minX(), bounds.maxX()); + long deltaZ = intervalDistance(area.minZ(), area.maxZ(), bounds.minZ(), bounds.maxZ()); + return deltaX * deltaX + deltaZ * deltaZ; + } + + private static long intervalDistance(int firstMin, int firstMax, int secondMin, int secondMax) { + if (firstMax < secondMin) { + return (long) secondMin - firstMax; + } + if (firstMin > secondMax) { + return (long) firstMin - secondMax; + } + return 0L; + } + + private static void markLayers( + BoundingBox area, BitSet requiredLayers, long minimumY, long maximumY) { + long clippedMinimumY = Math.max(area.minY(), minimumY); + long clippedMaximumY = Math.min(area.maxY(), maximumY); + if (clippedMinimumY > clippedMaximumY) { + return; + } + int fromIndex = Math.toIntExact(clippedMinimumY - area.minY()); + int toIndex = Math.toIntExact(clippedMaximumY - area.minY() + 1L); + requiredLayers.set(fromIndex, toIndex); + } + + private static boolean requiresSourceTerrainFill( + StructureStart start, IrisStructureTerrain configuredTerrain) { + if (start == null || !start.isValid()) { + return false; + } + IrisStructureTerrain terrain = configuredTerrain == null + ? new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE) + : configuredTerrain; + if (terrain.resolvedMode() != IrisStructureTerrainMode.SOURCE) { + return false; + } + TerrainAdjustment adjustment = start.getStructure().terrainAdaptation(); + return adjustment == TerrainAdjustment.BURY + || adjustment == TerrainAdjustment.ENCAPSULATE; + } + + static boolean clearsLegacyTemplateAir(StructureStart start, IrisStructureTerrain terrain) { + if (start == null || !start.isValid() || terrain == null) { + return false; + } + IrisStructureTerrainMode mode = terrain.resolvedMode(); + return mode == IrisStructureTerrainMode.ENCASE + || mode == IrisStructureTerrainMode.SOURCE + && start.getStructure().terrainAdaptation() != TerrainAdjustment.NONE; + } + + private static void integrateSourceTerrain(WorldGenLevel world, BoundingBox area, + StructureStart start, + SourceTerrainSnapshot sourceTerrain) { + if (start == null || !start.isValid()) { + return; + } + TerrainAdjustment adjustment = start.getStructure().terrainAdaptation(); + if (adjustment == TerrainAdjustment.BURY) { + fillBuriedTerrain(world, area, start, requireSourceTerrain( + world, area, start, sourceTerrain)); + } else if (adjustment == TerrainAdjustment.ENCAPSULATE) { + fillEncapsulatedTerrain(world, area, start, requireSourceTerrain( + world, area, start, sourceTerrain)); + } + } + + private static SourceTerrainSnapshot requireSourceTerrain( + WorldGenLevel world, BoundingBox area, StructureStart start, + SourceTerrainSnapshot sourceTerrain) { + return sourceTerrain == null + ? captureSourceTerrain(world, area, start, null) : sourceTerrain; + } + + private static void fillBuriedTerrain(WorldGenLevel world, BoundingBox area, + StructureStart start, + SourceTerrainSnapshot sourceTerrain) { + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (StructurePiece piece : start.getPieces()) { + if (!isSourceRigidPiece(piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + int groundY = bounds.minY(); + if (piece instanceof PoolElementStructurePiece poolPiece) { + groundY += poolPiece.getGroundLevelDelta(); + } + int minX = Math.max(area.minX(), bounds.minX() - SOURCE_BURY_HORIZONTAL_RADIUS); + int maxX = Math.min(area.maxX(), bounds.maxX() + SOURCE_BURY_HORIZONTAL_RADIUS); + int minY = Math.max(area.minY(), groundY - SOURCE_BURY_VERTICAL_RADIUS); + int maxY = Math.min(area.maxY(), groundY + SOURCE_BURY_VERTICAL_RADIUS); + int minZ = Math.max(area.minZ(), bounds.minZ() - SOURCE_BURY_HORIZONTAL_RADIUS); + int maxZ = Math.min(area.maxZ(), bounds.maxZ() + SOURCE_BURY_HORIZONTAL_RADIUS); + for (int x = minX; x <= maxX; x++) { + int outX = outset(x, bounds.minX(), bounds.maxX()); + for (int z = minZ; z <= maxZ; z++) { + int outZ = outset(z, bounds.minZ(), bounds.maxZ()); + for (int y = minY; y <= maxY; y++) { + int vertical = Math.abs(y - groundY); + if (!insideBurialEnvelope(outX, vertical, outZ)) { + continue; + } + fillEncaseable(world, position.set(x, y, z), sourceTerrain); + } + } + } + } + fillSourceJunctionTerrain(world, area, start, position, sourceTerrain); + } + + private static void fillEncapsulatedTerrain(WorldGenLevel world, BoundingBox area, + StructureStart start, + SourceTerrainSnapshot sourceTerrain) { + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (StructurePiece piece : start.getPieces()) { + if (!isSourceRigidPiece(piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + int minX = Math.max(area.minX(), bounds.minX() - SOURCE_ENCAPSULATE_RADIUS); + int maxX = Math.min(area.maxX(), bounds.maxX() + SOURCE_ENCAPSULATE_RADIUS); + int minY = Math.max(area.minY(), bounds.minY() - SOURCE_ENCAPSULATE_RADIUS); + int maxY = Math.min(area.maxY(), bounds.maxY() + SOURCE_ENCAPSULATE_RADIUS); + int minZ = Math.max(area.minZ(), bounds.minZ() - SOURCE_ENCAPSULATE_RADIUS); + int maxZ = Math.min(area.maxZ(), bounds.maxZ() + SOURCE_ENCAPSULATE_RADIUS); + for (int x = minX; x <= maxX; x++) { + int outX = outset(x, bounds.minX(), bounds.maxX()); + for (int z = minZ; z <= maxZ; z++) { + int outZ = outset(z, bounds.minZ(), bounds.maxZ()); + for (int y = minY; y <= maxY; y++) { + int outY = outset(y, bounds.minY(), bounds.maxY()); + if (!insideEncapsulationEnvelope(outX, outY, outZ)) { + continue; + } + fillEncaseable(world, position.set(x, y, z), sourceTerrain); + } + } + } + } + fillSourceJunctionTerrain(world, area, start, position, sourceTerrain); + } + + static boolean insideBurialEnvelope(int outX, int verticalDistance, int outZ) { + long horizontalSquared = (long) outX * outX + (long) outZ * outZ; + long verticalSquared = (long) verticalDistance * verticalDistance; + return horizontalSquared * 4L + verticalSquared + < (long) SOURCE_BURY_VERTICAL_RADIUS * SOURCE_BURY_VERTICAL_RADIUS; + } + + static boolean insideEncapsulationEnvelope(int outX, int outY, int outZ) { + return (long) outX * outX + (long) outY * outY + (long) outZ * outZ + < (long) SOURCE_ENCAPSULATE_RADIUS * SOURCE_ENCAPSULATE_RADIUS; + } + + static boolean isSourceRigidPiece(StructurePiece piece) { + if (piece == null) { + return false; + } + return !(piece instanceof PoolElementStructurePiece poolPiece) + || poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID; + } + + static boolean insideSourceJunctionEnvelope(int deltaX, int deltaY, int deltaZ) { + if (deltaY >= 0) { + return false; + } + return (long) deltaX * deltaX + (long) deltaY * deltaY + (long) deltaZ * deltaZ + < (long) SOURCE_JUNCTION_RADIUS * SOURCE_JUNCTION_RADIUS; + } + + private static void fillSourceJunctionTerrain(WorldGenLevel world, BoundingBox area, + StructureStart start, + BlockPos.MutableBlockPos position, + SourceTerrainSnapshot sourceTerrain) { + Set anchors = sourceJunctionAnchors(start); + for (JunctionAnchor anchor : anchors) { + int minX = Math.max(area.minX(), anchor.x() - SOURCE_JUNCTION_RADIUS + 1); + int maxX = Math.min(area.maxX(), anchor.x() + SOURCE_JUNCTION_RADIUS - 1); + int minY = Math.max(area.minY(), anchor.y() - SOURCE_JUNCTION_RADIUS + 1); + int maxY = Math.min(area.maxY(), anchor.y() - 1); + int minZ = Math.max(area.minZ(), anchor.z() - SOURCE_JUNCTION_RADIUS + 1); + int maxZ = Math.min(area.maxZ(), anchor.z() + SOURCE_JUNCTION_RADIUS - 1); + for (int x = minX; x <= maxX; x++) { + int deltaX = x - anchor.x(); + for (int z = minZ; z <= maxZ; z++) { + int deltaZ = z - anchor.z(); + for (int y = minY; y <= maxY; y++) { + if (insideSourceJunctionEnvelope(deltaX, y - anchor.y(), deltaZ)) { + fillEncaseable(world, position.set(x, y, z), sourceTerrain); + } + } + } + } + } + } + + private static Set sourceJunctionAnchors(StructureStart start) { + Set anchors = new HashSet<>(); + for (StructurePiece piece : start.getPieces()) { + if (!(piece instanceof PoolElementStructurePiece poolPiece)) { + continue; + } + for (JigsawJunction junction : poolPiece.getJunctions()) { + anchors.add(new JunctionAnchor( + junction.getSourceX(), junction.getSourceGroundY(), junction.getSourceZ())); + } + } + return anchors; + } + + private static int outset(int value, int minimum, int maximum) { + if (value < minimum) { + return minimum - value; + } + return Math.max(0, value - maximum); + } + + private static void fillEncaseable(WorldGenLevel world, BlockPos position, + SourceTerrainSnapshot sourceTerrain) { + if (isEncaseable(world.getBlockState(position))) { + world.setBlock(position, sourceEncaseBlock(world, position, sourceTerrain), 2); + } + } + + static BlockState sourceEncaseBlock(WorldGenLevel world, BlockPos position) { + int minX = Math.subtractExact(position.getX(), SOURCE_MATERIAL_SAMPLE_RADIUS); + int minZ = Math.subtractExact(position.getZ(), SOURCE_MATERIAL_SAMPLE_RADIUS); + int maxX = Math.addExact(position.getX(), SOURCE_MATERIAL_SAMPLE_RADIUS); + int maxZ = Math.addExact(position.getZ(), SOURCE_MATERIAL_SAMPLE_RADIUS); + BoundingBox sampleArea = new BoundingBox(minX, position.getY(), minZ, + maxX, position.getY(), maxZ); + BitSet requiredLayers = new BitSet(1); + requiredLayers.set(0); + SourceTerrainSnapshot sourceTerrain = SourceTerrainSnapshot.capture( + world, sampleArea, requiredLayers); + return sourceEncaseBlock(world, position, sourceTerrain); + } + + static BlockState sourceEncaseBlock(WorldGenLevel world, BlockPos position, + SourceTerrainSnapshot sourceTerrain) { + BlockPos.MutableBlockPos probe = new BlockPos.MutableBlockPos(); + for (int distance = 1; distance <= SOURCE_MATERIAL_SAMPLE_RADIUS; distance++) { + BlockState sampled = sourceTerrain.stateAt(probe.set( + position.getX() - distance, position.getY(), position.getZ())); + if (sampled != null) { + return sampled; + } + sampled = sourceTerrain.stateAt(probe.set( + position.getX() + distance, position.getY(), position.getZ())); + if (sampled != null) { + return sampled; + } + sampled = sourceTerrain.stateAt(probe.set( + position.getX(), position.getY(), position.getZ() - distance)); + if (sampled != null) { + return sampled; + } + sampled = sourceTerrain.stateAt(probe.set( + position.getX(), position.getY(), position.getZ() + distance)); + if (sampled != null) { + return sampled; + } + } + return defaultEncaseBlock(world, position.getY()); + } + + private static BlockState sourceTerrainBlock(BlockState state) { + if (!state.isSolid() || NativeStructureVegetationClearer.isTreeBlock(state)) { + return null; + } + return VANILLA_SOURCE_TERRAIN_BLOCKS.contains(state.getBlock()) + || state.is(BlockTags.BASE_STONE_OVERWORLD) + || state.is(BlockTags.BASE_STONE_NETHER) + || state.is(BlockTags.SUBSTRATE_OVERWORLD) + || state.is(BlockTags.DIRT) + || state.is(BlockTags.SAND) + || state.is(BlockTags.TERRACOTTA) + || state.is(BlockTags.MUD) + || state.is(BlockTags.MOSS_BLOCKS) + || state.is(BlockTags.GRASS_BLOCKS) + || state.is(BlockTags.NYLIUM) + || state.is(BlockTags.SNOW) + || state.is(BlockTags.ICE) + || state.is(BlockTags.CORAL_BLOCKS) + || state.is(BlockTags.SOUL_FIRE_BASE_BLOCKS) + ? state : null; + } + + static List contentPieceBounds(StructureStart start) { + List bounds = new ArrayList<>(start.getPieces().size()); + for (StructurePiece piece : start.getPieces()) { + bounds.add(piece.getBoundingBox()); + } + return List.copyOf(bounds); + } + + /** + * Per-column occupancy of the whole start, cached because a single structure spans many chunks and + * every one of them carves against the same shrinkwrapped footprint. + */ + static StructureCarvingFootprint carveFootprint(StructureStart start, int horizontalPadding, + Supplier templates) { + CarveFootprintKey key = new CarveFootprintKey(start, horizontalPadding); + StructureCarvingFootprint cached = cachedCarveFootprint(key); + if (cached != null) { + return cached; + } + CompletableFuture build = new CompletableFuture<>(); + CompletableFuture active = CARVE_FOOTPRINT_BUILDS.putIfAbsent(key, build); + if (active != null) { + return awaitCarveFootprint(active); + } + // Close the check-then-claim window: a racer whose cache check missed before the + // previous builder cached can claim the build slot after that builder retired it, + // and would rebuild a second instance without this re-check. + StructureCarvingFootprint published = cachedCarveFootprint(key); + if (published != null) { + build.complete(published); + CARVE_FOOTPRINT_BUILDS.remove(key, build); + return published; + } + try { + StructureCarvingFootprint footprint = StructureCarvingFootprint.fromColumns( + sink -> emitCarveColumns(start, templates, sink), horizontalPadding, MAX_CARVE_COLUMNS); + if (footprint == null) { + throw new IllegalStateException("Native structure carve footprint is empty or exceeds " + + MAX_CARVE_COLUMNS + " columns"); + } + cacheCarveFootprint(key, footprint); + build.complete(footprint); + return footprint; + } catch (RuntimeException | Error error) { + build.completeExceptionally(error); + throw error; + } finally { + CARVE_FOOTPRINT_BUILDS.remove(key, build); + } + } + + static int cachedCarveFootprintCells() { + synchronized (CARVE_FOOTPRINTS) { + cachedCarveCells = liveCarveCells(); + return cachedCarveCells; + } + } + + private static int liveCarveCells() { + int total = 0; + for (Map perStart : CARVE_FOOTPRINTS.values()) { + for (CachedCarveFootprint cached : perStart.values()) { + total += cached.cells(); + } + } + return total; + } + + static int maximumCachedCarveFootprintCells() { + return MAX_CACHED_CARVE_CELLS; + } + + private static StructureCarvingFootprint cachedCarveFootprint(CarveFootprintKey key) { + synchronized (CARVE_FOOTPRINTS) { + Map perStart = CARVE_FOOTPRINTS.get(key.start()); + CachedCarveFootprint cached = perStart == null ? null : perStart.get(key.padding()); + return cached == null ? null : cached.footprint(); + } + } + + private static StructureCarvingFootprint awaitCarveFootprint( + CompletableFuture future) { + return NativeBuildFutures.awaitBuild(future, "Native structure carve footprint build"); + } + + private static void cacheCarveFootprint(CarveFootprintKey key, + StructureCarvingFootprint footprint) { + int cells = Math.multiplyExact(footprint.width(), footprint.depth()); + synchronized (CARVE_FOOTPRINTS) { + // Weak keys expunge silently as the GC drops retired starts; re-derive the live + // total so the cell budget tracks reality instead of drifting. + cachedCarveCells = liveCarveCells(); + Map perStart = + CARVE_FOOTPRINTS.computeIfAbsent(key.start(), ignored -> new LinkedHashMap<>(4)); + CachedCarveFootprint previous = perStart.remove(key.padding()); + if (previous != null) { + cachedCarveCells -= previous.cells(); + } + Iterator>> eviction = + CARVE_FOOTPRINTS.entrySet().iterator(); + while (cachedCarveCells + cells > MAX_CACHED_CARVE_CELLS && eviction.hasNext()) { + Map.Entry> victim = eviction.next(); + if (victim.getKey() == key.start()) { + continue; + } + for (CachedCarveFootprint evicted : victim.getValue().values()) { + cachedCarveCells -= evicted.cells(); + } + eviction.remove(); + } + perStart.put(key.padding(), new CachedCarveFootprint(footprint, cells)); + cachedCarveCells += cells; + } + } + + static OrganicCarve organicCarve(StructureCarvingFootprint footprint, IrisStructureTerrain terrain, + IrisStructureCarveShape shape, long identity) { + double strength = terrain.resolvedErosionStrength(); + double lobeStrength = terrain.resolvedLobeStrength(); + RNG noiseRng = new RNG(identity); + CNG blob = null; + CNG ceilingRoll = null; + CNG floorRoll = null; + CNG lobe = null; + if (shape == IrisStructureCarveShape.ERODED && strength > 0D) { + blob = CNG.signature(noiseRng); + ceilingRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_CEILING_ROLL_SIGNATURE)); + floorRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_FLOOR_ROLL_SIGNATURE)); + } + if (shape == IrisStructureCarveShape.ERODED && lobeStrength > 0D) { + // A plain single octave channel: the fractured signature noise has no usable low frequency band. + lobe = new CNG(noiseRng.nextParallelRNG(CARVE_LOBE_SIGNATURE), 1D, 1); + } + return new OrganicCarve(footprint, shape, + Math.max(0, terrain.getHorizontalPadding()), + Math.max(0, terrain.getCeilingPadding()), + Math.max(0, terrain.getFloorPadding()), + strength, terrain.resolvedErosionFrequency(), blob, ceilingRoll, floorRoll, + lobe, terrain.resolvedLobeFrequency(), lobeStrength); + } + + static void carveOrganicColumns(WorldGenLevel world, BoundingBox area, OrganicCarve carve) { + StructureCarvingFootprint footprint = carve.footprint(); + int minX = Math.max(area.minX(), footprint.minX()); + int maxX = Math.min(area.maxX(), footprint.maxX()); + int minZ = Math.max(area.minZ(), footprint.minZ()); + int maxZ = Math.min(area.maxZ(), footprint.maxZ()); + if (minX > maxX || minZ > maxZ) { + return; + } + boolean eroded = carve.blob() != null; + BlockState air = Blocks.AIR.defaultBlockState(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int z = minZ; z <= maxZ; z++) { + for (int x = minX; x <= maxX; x++) { + int index = footprint.indexAt(x, z); + long horizontalDistanceSquared = footprint.distanceSquaredAt(index); + double sideReach = StructureCarveEnvelope.lobedSideReach(carve.lobe(), + carve.lobeFrequency(), carve.lobeStrength(), x, z, carve.horizontalPadding()); + double sideReachSquared = sideReach * sideReach; + if (horizontalDistanceSquared > sideReachSquared) { + continue; + } + double normalizedHorizontal = sideReachSquared == 0D + ? 0D : horizontalDistanceSquared / sideReachSquared; + int sourceMinY = footprint.sourceMinYAt(index); + int sourceMaxY = footprint.sourceMaxYAt(index); + double upReach = eroded + ? StructureCarveEnvelope.lobedUpReach(carve.lobe(), carve.lobeFrequency(), + carve.lobeStrength(), x, z, + StructureCarveEnvelope.erodedUpReach(carve.ceilingRoll(), + carve.frequency(), carve.strength(), x, z, + carve.ceilingPadding())) + : Math.max(1D, carve.ceilingPadding()); + double floorReach = eroded + ? StructureCarveEnvelope.erodedDownReach(carve.floorRoll(), carve.frequency(), + carve.strength(), x, z, carve.floorPadding()) + : carve.floorPadding(); + int columnMinY = Math.max(area.minY(), sourceMinY - (int) Math.floor(floorReach)); + int columnMaxY = Math.min(area.maxY(), + sourceMaxY + (eroded ? (int) Math.ceil(upReach) : carve.ceilingPadding())); + double downReach = Math.max(1D, floorReach); + for (int y = columnMinY; y <= columnMaxY; y++) { + double normalizedVertical = StructureCarveEnvelope.normalizedVerticalDistance( + y, sourceMinY, sourceMaxY, upReach, downReach); + double distanceSquared = normalizedHorizontal + + normalizedVertical * normalizedVertical; + if (distanceSquared > 1D) { + continue; + } + if (distanceSquared > 0D && eroded) { + double noise = carve.blob().fitDouble(0D, 1D, + x * carve.frequency(), y * carve.frequency(), z * carve.frequency()); + if (!StructureCarveEnvelope.shouldCarveOverboreCell( + carve.shape(), distanceSquared, noise, carve.strength())) { + continue; + } + } + world.setBlock(position.set(x, y, z), air, 2); + } + } + } + } + + private static boolean emitCarveColumns(StructureStart start, + Supplier templates, + StructureCarvingFootprint.ColumnSink sink) { + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + if (!(piece instanceof PoolElementStructurePiece poolPiece) + || !emitTemplateColumns(pieceTemplates(poolPiece, templates), + poolPiece.getPosition(), poolPiece.getRotation(), bounds, sink)) { + emitBoxColumns(bounds, sink); + } + } + return true; + } + + /** + * Derives per-column vertical extents from the complement of the template's air and structure-void + * cells, so a carve tracks the actual silhouette instead of the piece's rectangular bounding box. + */ + static boolean emitTemplateColumns(List templates, BlockPos position, + Rotation rotation, BoundingBox bounds, + StructureCarvingFootprint.ColumnSink sink) { + if (templates.isEmpty()) { + return false; + } + int width = bounds.getXSpan(); + int depth = bounds.getZSpan(); + long cells = (long) width * depth * bounds.getYSpan(); + if (cells < 1L || cells > MAX_TEMPLATE_OCCUPANCY_CELLS) { + return false; + } + StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(rotation); + boolean[] voidCells = null; + for (StructureTemplate template : templates) { + boolean[] templateVoid = new boolean[(int) cells]; + for (Block ignored : TEMPLATE_VOID_BLOCKS) { + for (StructureTemplate.StructureBlockInfo info + : template.filterBlocks(position, settings, ignored)) { + BlockPos voidPosition = info.pos(); + if (bounds.isInside(voidPosition)) { + templateVoid[templateCellIndex(bounds, width, depth, + voidPosition.getX(), voidPosition.getY(), voidPosition.getZ())] = true; + } + } + } + if (voidCells == null) { + voidCells = templateVoid; + continue; + } + for (int cell = 0; cell < voidCells.length; cell++) { + voidCells[cell] &= templateVoid[cell]; + } + } + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + for (int y = bounds.minY(); y <= bounds.maxY(); y++) { + if (voidCells[templateCellIndex(bounds, width, depth, x, y, z)]) { + continue; + } + if (minY == Integer.MAX_VALUE) { + minY = y; + } + maxY = y; + } + if (minY != Integer.MAX_VALUE) { + sink.column(x, z, minY, maxY); + } + } + } + return true; + } + + private static void emitBoxColumns(BoundingBox bounds, StructureCarvingFootprint.ColumnSink sink) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + sink.column(x, z, bounds.minY(), bounds.maxY()); + } + } + } + + private static int templateCellIndex(BoundingBox bounds, int width, int depth, + int x, int y, int z) { + return ((y - bounds.minY()) * depth + z - bounds.minZ()) * width + x - bounds.minX(); + } + + private static List pieceTemplates(PoolElementStructurePiece poolPiece, + Supplier templates) { + List resolved = new ArrayList<>(1); + collectElementTemplates(poolPiece.getElement(), templates, resolved); + return resolved; + } + + private static void collectElementTemplates(StructurePoolElement element, + Supplier templates, + List resolved) { + if (element instanceof ListPoolElement listElement) { + for (StructurePoolElement child : listElement.getElements()) { + collectElementTemplates(child, templates, resolved); + } + return; + } + if (element instanceof SinglePoolElement singleElement) { + resolved.add(NativeStructureReflection.resolveTemplate(singleElement, templates)); + } + } + + private static long carveNoiseIdentity(WorldGenLevel world, String structureId, + StructureStart start) { + return world.getSeed() + ^ ((long) start.getChunkPos().x() * 341873128712L) + ^ ((long) start.getChunkPos().z() * 132897987541L) + ^ (structureId == null ? 0 : structureId.hashCode()); + } + + private static void encasePieces(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureTerrain terrain, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver) { + IrisMaterialPalette palette = terrain.getEncasePalette(); + RNG rng = null; + if (palette != null) { + Objects.requireNonNull(paletteBlockResolver, + "Native structure encase palette requires a platform block resolver"); + rng = new RNG(world.getSeed() ^ (structureId == null ? 0 : structureId.hashCode())); + } + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (BoundingBox bounds : contentPieceBounds(start)) { + BoundingBox shell = paddedPieceArea(area, bounds, terrain); + if (shell == null) { + continue; + } + for (int x = shell.minX(); x <= shell.maxX(); x++) { + for (int z = shell.minZ(); z <= shell.maxZ(); z++) { + for (int y = shell.minY(); y <= shell.maxY(); y++) { + BlockState existing = world.getBlockState(position.set(x, y, z)); + if (!isEncaseable(existing)) { + continue; + } + BlockState fill = palette == null + ? defaultEncaseBlock(world, y) + : Objects.requireNonNull( + paletteBlockResolver.resolve(palette, rng, x, y, z), + "Encase palette returned no block for " + structureId + " at " + + x + "," + y + "," + z); + world.setBlock(position, fill, 2); + } + } + } + } + } + + static boolean isEncaseable(BlockState state) { + return state.isAir() || !state.getFluidState().isEmpty(); + } + + static BlockState defaultEncaseBlock(int y) { + return y < 0 ? Blocks.DEEPSLATE.defaultBlockState() : Blocks.STONE.defaultBlockState(); + } + + static BlockState defaultEncaseBlock(WorldGenLevel world, int y) { + ServerLevel level = world == null ? null : world.getLevel(); + if (level == null) { + return defaultEncaseBlock(y); + } + Holder dimensionType = level.dimensionTypeRegistration(); + ResourceKey dimensionTypeKey = dimensionType.unwrapKey().orElse(null); + return defaultEncaseBlock(level.dimension(), dimensionTypeKey, dimensionType.value(), y); + } + + static BlockState defaultEncaseBlock(ResourceKey dimension, int y) { + return defaultEncaseBlock(dimension, null, null, y); + } + + static BlockState defaultEncaseBlock(ResourceKey dimension, + ResourceKey dimensionTypeKey, + DimensionType dimensionType, int y) { + if (Level.NETHER.equals(dimension)) { + return Blocks.NETHERRACK.defaultBlockState(); + } + if (Level.END.equals(dimension)) { + return Blocks.END_STONE.defaultBlockState(); + } + if (BuiltinDimensionTypes.NETHER.equals(dimensionTypeKey) + || dimensionType != null && dimensionType.hasCeiling() && !dimensionType.hasSkyLight()) { + return Blocks.NETHERRACK.defaultBlockState(); + } + if (BuiltinDimensionTypes.END.equals(dimensionTypeKey) + || dimensionType != null && (dimensionType.hasEnderDragonFight() + || dimensionType.hasEndFlashes())) { + return Blocks.END_STONE.defaultBlockState(); + } + return defaultEncaseBlock(y); + } + + private static void carvePieceBoxes(WorldGenLevel world, BoundingBox area, StructureStart start, + IrisStructureTerrain terrain) { + BlockState air = Blocks.AIR.defaultBlockState(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (BoundingBox bounds : contentPieceBounds(start)) { + BoundingBox carve = paddedPieceArea(area, bounds, terrain); + if (carve == null) { + continue; + } + for (int x = carve.minX(); x <= carve.maxX(); x++) { + for (int z = carve.minZ(); z <= carve.maxZ(); z++) { + for (int y = carve.minY(); y <= carve.maxY(); y++) { + world.setBlock(position.set(x, y, z), air, 2); + } + } + } + } + } + + private static BoundingBox paddedPieceArea(BoundingBox area, BoundingBox bounds, + IrisStructureTerrain terrain) { + int horizontalPadding = Math.max(0, terrain.getHorizontalPadding()); + int minX = Math.max(area.minX(), bounds.minX() - horizontalPadding); + int minY = Math.max(area.minY(), bounds.minY() - Math.max(0, terrain.getFloorPadding())); + int minZ = Math.max(area.minZ(), bounds.minZ() - horizontalPadding); + int maxX = Math.min(area.maxX(), bounds.maxX() + horizontalPadding); + int maxY = Math.min(area.maxY(), bounds.maxY() + Math.max(0, terrain.getCeilingPadding())); + int maxZ = Math.min(area.maxZ(), bounds.maxZ() + horizontalPadding); + if (minX > maxX || minY > maxY || minZ > maxZ) { + return null; + } + return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + } + + static void clearLegacyTemplateAir(WorldGenLevel world, BoundingBox area, + StructureStart start, + Supplier templates) { + for (StructurePiece piece : start.getPieces()) { + if (!(piece instanceof PoolElementStructurePiece poolPiece) + || poolPiece.getElement().getProjection() != StructureTemplatePool.Projection.RIGID + || !intersects(poolPiece.getBoundingBox(), area)) { + continue; + } + int groundY = poolPiece.getBoundingBox().minY() + poolPiece.getGroundLevelDelta(); + StructurePlaceSettings settings = new StructurePlaceSettings() + .setRotation(poolPiece.getRotation()) + .setBoundingBox(area); + clearLegacyTemplateAir(world, poolPiece.getElement(), poolPiece.getPosition(), + groundY, settings, templates); + } + } + + private static void clearLegacyTemplateAir(WorldGenLevel world, StructurePoolElement element, + BlockPos position, int groundY, + StructurePlaceSettings settings, + Supplier templates) { + if (element instanceof ListPoolElement listElement) { + for (StructurePoolElement child : listElement.getElements()) { + clearLegacyTemplateAir(world, child, position, groundY, settings, templates); + } + return; + } + if (!(element instanceof LegacySinglePoolElement legacyElement)) { + return; + } + StructureTemplate template = NativeStructureReflection.resolveTemplate(legacyElement, templates); + clearTemplateAir(world, template, position, groundY, settings); + } + + static void clearTemplateAir(WorldGenLevel world, StructureTemplate template, + BlockPos position, int groundY, + StructurePlaceSettings settings) { + List airBlocks = template.filterBlocks( + position, settings, Blocks.AIR); + BlockState air = Blocks.AIR.defaultBlockState(); + for (StructureTemplate.StructureBlockInfo airBlock : airBlocks) { + BlockPos airPosition = airBlock.pos(); + BlockState existingState = world.getBlockState(airPosition); + if (shouldClearLegacyAir( + airPosition.getY(), groundY, existingState.isAir()) + && !NativeStructureVegetationClearer.isTreeBlock(existingState)) { + world.setBlock(airPosition, air, 2); + } + } + } + + static boolean shouldClearLegacyAir(int airY, int groundY, boolean existingAir) { + return airY >= groundY && !existingAir; + } + + static boolean intersects(BoundingBox first, BoundingBox second) { + return first.maxX() >= second.minX() && first.minX() <= second.maxX() + && first.maxY() >= second.minY() && first.minY() <= second.maxY() + && first.maxZ() >= second.minZ() && first.minZ() <= second.maxZ(); + } + + // StructureStart has no value equality, so the key pins the exact start instance a chunk carves against. + private record CarveFootprintKey(StructureStart start, int padding) { + } + + private record CachedCarveFootprint(StructureCarvingFootprint footprint, int cells) { + } + + private record JunctionAnchor(int x, int y, int z) { + } + + static final class SourceTerrainSnapshot { + private final BoundingBox area; + private final int width; + private final BlockState[][] statesByLayer; + private final int sampledCells; + + private SourceTerrainSnapshot(BoundingBox area, int width, + BlockState[][] statesByLayer, int sampledCells) { + this.area = area; + this.width = width; + this.statesByLayer = statesByLayer; + this.sampledCells = sampledCells; + } + + static SourceTerrainSnapshot capture( + WorldGenLevel world, BoundingBox area, BitSet requiredLayers) { + Objects.requireNonNull(world, "Source terrain snapshot requires a generation level"); + Objects.requireNonNull(area, "Source terrain snapshot requires writable bounds"); + Objects.requireNonNull(requiredLayers, "Source terrain snapshot requires sampled layers"); + int width = area.getXSpan(); + int depth = area.getZSpan(); + int height = area.getYSpan(); + int horizontalCells = Math.multiplyExact(width, depth); + BlockState[][] statesByLayer = new BlockState[height][]; + int sampledCells = 0; + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int layer = requiredLayers.nextSetBit(0); + layer >= 0; layer = requiredLayers.nextSetBit(layer + 1)) { + if (layer >= height) { + throw new IllegalArgumentException( + "Source terrain snapshot layer exceeds writable bounds: " + layer); + } + BlockState[] states = new BlockState[horizontalCells]; + statesByLayer[layer] = states; + sampledCells = Math.addExact(sampledCells, horizontalCells); + int y = Math.addExact(area.minY(), layer); + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int index = (z - area.minZ()) * width + x - area.minX(); + states[index] = sourceTerrainBlock( + world.getBlockState(position.set(x, y, z))); + } + } + } + return new SourceTerrainSnapshot(new BoundingBox( + area.minX(), area.minY(), area.minZ(), + area.maxX(), area.maxY(), area.maxZ()), width, + statesByLayer, sampledCells); + } + + BlockState stateAt(BlockPos position) { + if (!area.isInside(position)) { + return null; + } + BlockState[] states = statesByLayer[position.getY() - area.minY()]; + if (states == null) { + return null; + } + int index = (position.getZ() - area.minZ()) * width + + position.getX() - area.minX(); + return states[index]; + } + + int sampledCells() { + return sampledCells; + } + } + + record OrganicCarve(StructureCarvingFootprint footprint, IrisStructureCarveShape shape, + int horizontalPadding, int ceilingPadding, int floorPadding, + double strength, double frequency, CNG blob, CNG ceilingRoll, CNG floorRoll, + CNG lobe, double lobeFrequency, double lobeStrength) { + } + + public record TerrainTarget(String structureId, StructureStart start, + IrisStructureTerrain terrain) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVanillaLocator.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVanillaLocator.java new file mode 100644 index 000000000..b04eaf2f1 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVanillaLocator.java @@ -0,0 +1,224 @@ +package art.arcane.iris.nativegen; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureCheckResult; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class NativeStructureVanillaLocator { + private NativeStructureVanillaLocator() { + } + + public static Candidate predict(ServerLevel level, HolderSet holders, + BlockPos origin, int radius, boolean findUnexplored) { + if (SharedConstants.DEBUG_DISABLE_FEATURES + || !level.getServer().getWorldGenSettings().options().generateStructures()) { + return null; + } + ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState(); + Map>> byPlacement = new LinkedHashMap<>(); + for (Holder holder : holders) { + for (StructurePlacement placement : state.getPlacementsForStructure(holder)) { + byPlacement.computeIfAbsent(placement, ignored -> new LinkedHashSet<>()).add(holder); + } + } + if (byPlacement.isEmpty()) { + return null; + } + + StructureManager structureManager = level.structureManager(); + Candidate best = null; + double bestDistance = Double.MAX_VALUE; + List>>> randomPlacements = + new ArrayList<>(byPlacement.size()); + for (Map.Entry>> entry : byPlacement.entrySet()) { + StructurePlacement placement = entry.getKey(); + if (placement instanceof ConcentricRingsStructurePlacement concentric) { + Candidate candidate = predictConcentric( + entry.getValue(), level, structureManager, origin, findUnexplored, + state, concentric); + if (candidate != null) { + double distance = origin.distSqr(candidate.result().getFirst()); + if (distance < bestDistance) { + best = candidate; + bestDistance = distance; + } + } + } else if (placement instanceof RandomSpreadStructurePlacement randomSpread) { + randomPlacements.add(Map.entry(randomSpread, entry.getValue())); + } + } + + int centerChunkX = SectionPos.blockToSectionCoord(origin.getX()); + int centerChunkZ = SectionPos.blockToSectionCoord(origin.getZ()); + int searchRadius = Math.max(0, radius); + for (int ring = 0; ring <= searchRadius; ring++) { + boolean foundInRing = false; + for (Map.Entry>> entry + : randomPlacements) { + Candidate candidate = predictRandomSpread( + entry.getValue(), level, structureManager, + centerChunkX, centerChunkZ, ring, findUnexplored, + state.getLevelSeed(), entry.getKey()); + if (candidate == null) { + continue; + } + foundInRing = true; + double distance = origin.distSqr(candidate.result().getFirst()); + if (distance < bestDistance) { + best = candidate; + bestDistance = distance; + } + } + if (foundInRing) { + return best; + } + } + return best; + } + + private static Candidate predictConcentric(Set> holders, + ServerLevel level, + StructureManager structureManager, + BlockPos origin, + boolean findUnexplored, + ChunkGeneratorStructureState state, + ConcentricRingsStructurePlacement placement) { + List positions = state.getRingPositionsFor(placement); + if (positions == null) { + throw new IllegalStateException( + "Tried to locate structures for an unavailable concentric placement"); + } + Candidate best = null; + double bestDistance = Double.MAX_VALUE; + for (ChunkPos position : positions) { + BlockPos locatePosition = new BlockPos( + SectionPos.sectionToBlockCoord(position.x(), 8), + 32, + SectionPos.sectionToBlockCoord(position.z(), 8)); + double distance = locatePosition.distSqr(origin); + if (best != null && distance >= bestDistance) { + continue; + } + Candidate candidate = predictAt( + holders, level, structureManager, findUnexplored, placement, position); + if (candidate != null) { + best = candidate; + bestDistance = distance; + } + } + return best; + } + + private static Candidate predictRandomSpread(Set> holders, + ServerLevel level, + StructureManager structureManager, + int centerChunkX, int centerChunkZ, + int ring, boolean findUnexplored, + long seed, + RandomSpreadStructurePlacement placement) { + int spacing = placement.spacing(); + for (int offsetX = -ring; offsetX <= ring; offsetX++) { + boolean edgeX = offsetX == -ring || offsetX == ring; + for (int offsetZ = -ring; offsetZ <= ring; offsetZ++) { + boolean edgeZ = offsetZ == -ring || offsetZ == ring; + if (!edgeX && !edgeZ) { + continue; + } + int gridChunkX = centerChunkX + spacing * offsetX; + int gridChunkZ = centerChunkZ + spacing * offsetZ; + ChunkPos candidatePosition = placement.getPotentialStructureChunk( + seed, gridChunkX, gridChunkZ); + Candidate candidate = predictAt( + holders, level, structureManager, + findUnexplored, placement, candidatePosition); + if (candidate != null) { + return candidate; + } + } + } + return null; + } + + private static Candidate predictAt(Set> holders, + ServerLevel level, + StructureManager structureManager, + boolean findUnexplored, + StructurePlacement placement, + ChunkPos candidatePosition) { + for (Holder holder : holders) { + Structure structure = holder.value(); + StructureCheckResult result = structureManager.checkStructurePresence( + candidatePosition, structure, placement, findUnexplored); + if (result == StructureCheckResult.START_NOT_PRESENT) { + continue; + } + if (!findUnexplored && result == StructureCheckResult.START_PRESENT) { + return new Candidate( + Pair.of(placement.getLocatePos(candidatePosition), holder), null); + } + ChunkAccess chunk = level.getChunk( + candidatePosition.x(), candidatePosition.z(), ChunkStatus.STRUCTURE_STARTS); + StructureStart start = structureManager.getStartForStructure( + SectionPos.bottomOf(chunk), structure, chunk); + if (start == null || !start.isValid() + || findUnexplored && !start.canBeReferenced()) { + continue; + } + return new Candidate( + Pair.of(placement.getLocatePos(start.getChunkPos()), holder), + findUnexplored ? start : null); + } + return null; + } + + public static final class Candidate { + private final Pair> result; + private final StructureStart referenceStart; + private final AtomicBoolean committed; + + private Candidate(Pair> result, + StructureStart referenceStart) { + this.result = result; + this.referenceStart = referenceStart; + this.committed = new AtomicBoolean(); + } + + public Pair> result() { + return result; + } + + public boolean reference(StructureManager structureManager) { + if (referenceStart == null || !committed.compareAndSet(false, true)) { + return false; + } + if (!referenceStart.canBeReferenced()) { + return false; + } + structureManager.addReference(referenceStart); + return true; + } + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVegetationClearer.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVegetationClearer.java new file mode 100644 index 000000000..88674aacf --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVegetationClearer.java @@ -0,0 +1,174 @@ +package art.arcane.iris.nativegen; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; + +public final class NativeStructureVegetationClearer { + private NativeStructureVegetationClearer() { + } + + public static boolean isUndergroundStep(GenerationStep.Decoration step) { + return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES + || step == GenerationStep.Decoration.UNDERGROUND_DECORATION + || step == GenerationStep.Decoration.STRONGHOLDS; + } + + public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area, + List starts) { + if (starts == null || starts.isEmpty()) { + return; + } + VegetationSnapshot snapshot = captureVegetation(chunk, area); + if (snapshot.treeBlockCount() == 0) { + return; + } + int columns = area.getXSpan() * area.getZSpan(); + int[] clearMinY = new int[columns]; + int[] clearMaxY = new int[columns]; + Arrays.fill(clearMinY, Integer.MAX_VALUE); + Arrays.fill(clearMaxY, Integer.MIN_VALUE); + for (StructureStart start : starts) { + if (shouldProcessStart(start)) { + markVegetationColumns(area, snapshot, start, clearMinY, clearMaxY); + } + } + clearVegetationColumns(world, area, snapshot, clearMinY, clearMaxY); + } + + static boolean shouldProcessStart(StructureStart start) { + return start != null && start.isValid(); + } + + private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) { + int width = area.getXSpan(); + int depth = area.getZSpan(); + BitSet[] columns = new BitSet[width * depth]; + int treeBlockCount = 0; + LevelChunkSection[] sections = chunk.getSections(); + int chunkMinX = chunk.getPos().getMinBlockX(); + int chunkMinZ = chunk.getPos().getMinBlockZ(); + for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + LevelChunkSection section = sections[sectionIndex]; + int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4; + int minY = Math.max(area.minY(), sectionMinY); + int maxY = Math.min(area.maxY(), sectionMinY + 15); + if (minY > maxY || section.hasOnlyAir() + || !section.maybeHas(NativeStructureVegetationClearer::isTreeBlock)) { + continue; + } + for (int y = minY; y <= maxY; y++) { + int localY = y - sectionMinY; + for (int z = area.minZ(); z <= area.maxZ(); z++) { + int localZ = z - chunkMinZ; + for (int x = area.minX(); x <= area.maxX(); x++) { + int localX = x - chunkMinX; + if (!isTreeBlock(section.getBlockState(localX, localY, localZ))) { + continue; + } + int column = (z - area.minZ()) * width + x - area.minX(); + BitSet treeBlocks = columns[column]; + if (treeBlocks == null) { + treeBlocks = new BitSet(area.getYSpan()); + columns[column] = treeBlocks; + } + treeBlocks.set(y - area.minY()); + treeBlockCount++; + } + } + } + } + return new VegetationSnapshot(columns, treeBlockCount); + } + + private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot, + StructureStart start, int[] clearMinY, int[] clearMaxY) { + int width = area.getXSpan(); + int[] pieceTops = new int[clearMinY.length]; + int[] pieceBottoms = new int[clearMinY.length]; + Arrays.fill(pieceTops, Integer.MIN_VALUE); + Arrays.fill(pieceBottoms, Integer.MAX_VALUE); + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + int minX = Math.max(area.minX(), bounds.minX()); + int maxX = Math.min(area.maxX(), bounds.maxX()); + int minZ = Math.max(area.minZ(), bounds.minZ()); + int maxZ = Math.min(area.maxZ(), bounds.maxZ()); + if (minX > maxX || minZ > maxZ) { + continue; + } + for (int z = minZ; z <= maxZ; z++) { + for (int x = minX; x <= maxX; x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + pieceTops[column] = Math.max(pieceTops[column], bounds.maxY()); + pieceBottoms[column] = Math.min(pieceBottoms[column], bounds.minY()); + } + } + } + for (int column = 0; column < pieceTops.length; column++) { + if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) { + continue; + } + clearMinY[column] = Math.min(clearMinY[column], pieceBottoms[column] - 1); + clearMaxY[column] = Math.max(clearMaxY[column], pieceTops[column]); + } + } + + private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area, + VegetationSnapshot snapshot, int[] clearMinY, int[] clearMaxY) { + int width = area.getXSpan(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + BlockState air = Blocks.AIR.defaultBlockState(); + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + BitSet treeBlocks = snapshot.columns()[column]; + if (treeBlocks == null || clearMaxY[column] < clearMinY[column]) { + continue; + } + int firstBit = Math.max(0, clearMinY[column] - area.minY()); + int lastBit = Math.min(area.getYSpan() - 1, clearMaxY[column] - area.minY()); + if (firstBit > lastBit) { + continue; + } + for (int bit = treeBlocks.nextSetBit(firstBit); bit >= 0 && bit <= lastBit; + bit = treeBlocks.nextSetBit(bit + 1)) { + int y = area.minY() + bit; + position.set(x, y, z); + BlockState state = world.getBlockState(position); + if (isTreeBlock(state)) { + world.setBlock(position, air, 2); + } + } + } + } + } + + static boolean isTreeBlock(BlockState state) { + if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) { + return true; + } + // Field reads instead of a registry byValue map hit — this predicate runs per block + // probe on the hottest native-structure scans. + String path = state.getBlock().builtInRegistryHolder().key().identifier().getPath(); + return path.endsWith("_log") || path.endsWith("_wood") + || path.endsWith("_stem") || path.endsWith("_hyphae") + || path.endsWith("_leaves"); + } + + private record VegetationSnapshot(BitSet[] columns, int treeBlockCount) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVerticalPlacer.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVerticalPlacer.java new file mode 100644 index 000000000..789f24716 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVerticalPlacer.java @@ -0,0 +1,384 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.StructureVerticalBounds; +import art.arcane.iris.engine.object.IrisStructureYBand; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; +import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece; +import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.IntBinaryOperator; + +public final class NativeStructureVerticalPlacer { + private static final String DESERT_PYRAMID_ID = "minecraft:desert_pyramid"; + private static final String JUNGLE_PYRAMID_ID = "minecraft:jungle_pyramid"; + private static final int MAX_BURIAL_COLUMNS = 2_000_000; + private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24; + private static final String OCEAN_MONUMENT_ID = "minecraft:monument"; + private static final int UNDERGROUND_SURFACE_CLEARANCE = 1; + + private NativeStructureVerticalPlacer() { + } + + public static int applyVerticalPlacement(StructureStart start, String structureId, int requestedOffset, + int seaLevel, int worldMinY, int worldMaxYExclusive, + boolean underground, boolean preserveSourceY, + IrisStructureYBand yBand, + IntBinaryOperator surfaceHeight) { + if (isOceanMonument(start, structureId)) { + return alignOceanMonumentToSeaLevel( + start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive); + } + if (isAdjustedScatteredStructure(start, structureId)) { + return alignScatteredStructureToSurface( + start, structureId, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight); + } + return applyVerticalShift(start, requestedOffset, worldMinY, worldMaxYExclusive, + underground, preserveSourceY, yBand, surfaceHeight); + } + + public static StructureStart relocateToMinY(StructureStart start, Structure source, int targetMinY, + LevelHeightAccessor heightAccessor) { + Objects.requireNonNull(start, "Native structure start must not be null"); + Objects.requireNonNull(source, "Native structure source must not be null"); + Objects.requireNonNull(heightAccessor, "Native structure height accessor must not be null"); + if (!start.isValid()) { + return StructureStart.INVALID_START; + } + List pieces = start.getPieces(); + int minY = Integer.MAX_VALUE; + for (StructurePiece piece : pieces) { + minY = Math.min(minY, piece.getBoundingBox().minY()); + } + if (minY == Integer.MAX_VALUE) { + return StructureStart.INVALID_START; + } + int offsetY = Math.subtractExact(targetMinY, minY); + if (offsetY != 0) { + for (StructurePiece piece : pieces) { + moveStructurePiece(piece, offsetY); + } + } + int worldMinY = heightAccessor.getMinY() + 1; + int worldMaxYExclusive = Math.addExact( + heightAccessor.getMinY(), heightAccessor.getHeight()); + for (StructurePiece piece : pieces) { + BoundingBox bounds = piece.getBoundingBox(); + if (bounds.minY() < worldMinY || bounds.maxY() >= worldMaxYExclusive) { + throw new IllegalStateException("Native structure cannot fit target minimum Y " + + targetMinY + " inside world bounds [" + worldMinY + "," + + worldMaxYExclusive + ")"); + } + } + return new StructureStart( + source, + start.getChunkPos(), + start.getReferences(), + new PiecesContainer(List.copyOf(pieces)) + ); + } + + static int alignScatteredStructureToSurface(StructureStart start, String structureId, + int configuredOffset, int worldMinY, + int worldMaxYExclusive, + IntBinaryOperator surfaceHeight) { + Objects.requireNonNull(surfaceHeight, "Scattered native structure requires a terrain height resolver"); + ScatteredFeaturePiece piece = requireAdjustedScatteredPiece(start, structureId); + BoundingBox bounds = start.getBoundingBox(); + BoundingBox pieceBounds = piece.getBoundingBox(); + int surfaceY = representativeScatteredSurfaceY(structureId, pieceBounds, surfaceHeight); + int targetMinY = Math.addExact(Math.addExact(surfaceY, 1), configuredOffset); + int requestedMove = Math.subtractExact(targetMinY, pieceBounds.minY()); + int offsetY = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), requestedMove, worldMinY, worldMaxYExclusive); + if (offsetY != 0) { + moveStructureStart(start, bounds, offsetY); + } + setScatteredHeightPosition(piece, Math.max(0, piece.getBoundingBox().minY())); + return offsetY; + } + + public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY, + int worldMaxYExclusive, boolean underground, + boolean preserveSourceY, IrisStructureYBand yBand, + IntBinaryOperator surfaceHeight) { + BoundingBox bounds = start.getBoundingBox(); + int resolvedOffset = resolveShiftOffset(start, bounds, requestedOffset, worldMinY, + worldMaxYExclusive, underground, preserveSourceY, yBand, surfaceHeight); + int offsetY = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), resolvedOffset, worldMinY, worldMaxYExclusive); + if (offsetY == 0) { + return 0; + } + moveStructureStart(start, bounds, offsetY); + return offsetY; + } + + private static int resolveShiftOffset(StructureStart start, BoundingBox bounds, int requestedOffset, + int worldMinY, int worldMaxYExclusive, boolean underground, + boolean preserveSourceY, IrisStructureYBand yBand, + IntBinaryOperator surfaceHeight) { + if (preserveSourceY) { + return requestedOffset; + } + if (yBand != null) { + return resolveYBandOffset(bounds, yBand, start.getChunkPos()); + } + if (underground) { + return resolveBuriedOffset( + bounds, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight); + } + return requestedOffset; + } + + static int alignOceanMonumentToSeaLevel(StructureStart start, int configuredOffset, int seaLevel, + int worldMinY, int worldMaxYExclusive) { + OceanMonumentPieces.MonumentBuilding building = requireOceanMonumentBuilding(start); + BoundingBox bounds = start.getBoundingBox(); + int targetMinY = Math.addExact( + Math.subtractExact(seaLevel, MONUMENT_BASE_BELOW_SEA_LEVEL), configuredOffset); + int requestedOffset = Math.subtractExact(targetMinY, building.getBoundingBox().minY()); + int offsetY = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive); + if (offsetY != requestedOffset) { + throw new IllegalStateException("Ocean monument cannot align to sea level " + seaLevel + + " with configured offset " + configuredOffset + " inside world bounds [" + + worldMinY + "," + worldMaxYExclusive + ")"); + } + if (offsetY == 0) { + return 0; + } + moveStructureStart(start, bounds, offsetY); + return offsetY; + } + + static void ensureMonumentSeaLevelAlignment(StructureStart start, String structureId, + int configuredOffset, int seaLevel, + int worldMinY, int worldMaxYExclusive) { + if (!isOceanMonument(start, structureId)) { + return; + } + synchronized (start) { + alignOceanMonumentToSeaLevel( + start, configuredOffset, seaLevel, worldMinY, worldMaxYExclusive); + } + } + + private static boolean isOceanMonument(StructureStart start, String structureId) { + return OCEAN_MONUMENT_ID.equals(structureId) + && start != null + && start.getStructure() instanceof OceanMonumentStructure; + } + + private static boolean isAdjustedScatteredStructure(StructureStart start, String structureId) { + if (start == null) { + return false; + } + Structure source = start.getStructure(); + return (DESERT_PYRAMID_ID.equals(structureId) && source instanceof DesertPyramidStructure) + || (JUNGLE_PYRAMID_ID.equals(structureId) && source instanceof JungleTempleStructure); + } + + private static ScatteredFeaturePiece requireAdjustedScatteredPiece(StructureStart start, + String structureId) { + Objects.requireNonNull(start, "Scattered native structure start must not be null"); + List pieces = start.getPieces(); + if (pieces.size() != 1) { + throw new IllegalStateException(structureId + " must contain exactly one scattered piece, found " + + pieces.size()); + } + StructurePiece piece = pieces.get(0); + if (DESERT_PYRAMID_ID.equals(structureId) && piece instanceof DesertPyramidPiece desertPyramid) { + return desertPyramid; + } + if (JUNGLE_PYRAMID_ID.equals(structureId) && piece instanceof JungleTemplePiece jungleTemple) { + return jungleTemple; + } + throw new IllegalStateException(structureId + " contains unexpected piece " + + piece.getClass().getName()); + } + + private static int representativeScatteredSurfaceY(String structureId, BoundingBox bounds, + IntBinaryOperator surfaceHeight) { + if (DESERT_PYRAMID_ID.equals(structureId)) { + return lowestSurfaceY(bounds, surfaceHeight); + } + if (JUNGLE_PYRAMID_ID.equals(structureId)) { + return averageSurfaceY(bounds, surfaceHeight); + } + throw new IllegalStateException("Unsupported scattered native structure " + structureId); + } + + private static int lowestSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) { + int lowestY = Integer.MAX_VALUE; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + lowestY = Math.min(lowestY, surfaceHeight.applyAsInt(x, z)); + } + } + if (lowestY == Integer.MAX_VALUE) { + throw new IllegalStateException("Scattered native structure has an empty terrain footprint"); + } + return lowestY; + } + + private static int averageSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) { + long totalY = 0L; + long columns = 0L; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + totalY += surfaceHeight.applyAsInt(x, z); + columns++; + } + } + if (columns == 0L) { + throw new IllegalStateException("Scattered native structure has an empty terrain footprint"); + } + return Math.toIntExact(totalY / columns); + } + + private static void setScatteredHeightPosition(ScatteredFeaturePiece piece, int heightPosition) { + try { + NativeStructureReflection.ScatteredHeightPositionAccess.FIELD.setInt(piece, heightPosition); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot lock scattered native structure height", error); + } + } + + private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) { + Objects.requireNonNull(start, "Ocean monument start must not be null"); + List pieces = start.getPieces(); + OceanMonumentPieces.MonumentBuilding building = null; + for (StructurePiece piece : pieces) { + if (piece instanceof OceanMonumentPieces.MonumentBuilding monumentBuilding) { + building = monumentBuilding; + } + } + if (pieces.size() != 1 || building == null) { + throw new IllegalStateException("minecraft:monument must contain exactly one MonumentBuilding, found " + + pieces.size() + " pieces"); + } + return building; + } + + private static void moveStructureStart(StructureStart start, BoundingBox cachedBounds, int offsetY) { + for (StructurePiece piece : start.getPieces()) { + moveStructurePiece(piece, offsetY); + } + cachedBounds.move(0, offsetY, 0); + } + + private static void moveStructurePiece(StructurePiece piece, int offsetY) { + piece.move(0, offsetY, 0); + if (piece instanceof OceanMonumentPieces.MonumentBuilding building) { + for (StructurePiece child : monumentChildPieces(building)) { + child.move(0, offsetY, 0); + } + } + if (piece instanceof PoolElementStructurePiece poolPiece) { + List junctions = poolPiece.getJunctions(); + for (int i = 0; i < junctions.size(); i++) { + JigsawJunction junction = junctions.get(i); + junctions.set(i, new JigsawJunction( + junction.getSourceX(), + junction.getSourceGroundY() + offsetY, + junction.getSourceZ(), + junction.getDeltaY(), + junction.getDestProjection())); + } + } + } + + static List monumentChildPieces(OceanMonumentPieces.MonumentBuilding building) { + Object value; + try { + value = NativeStructureReflection.MonumentChildPiecesAccess.FIELD.get(building); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read Ocean Monument child pieces", error); + } + if (!(value instanceof List children)) { + throw new IllegalStateException("Ocean Monument child-pieces field is not a list"); + } + List pieces = new ArrayList<>(children.size()); + for (Object child : children) { + if (!(child instanceof StructurePiece monumentPiece) + || child.getClass().getEnclosingClass() != OceanMonumentPieces.class) { + throw new IllegalStateException("Ocean Monument child-pieces list contains " + + (child == null ? "null" : child.getClass().getName())); + } + pieces.add(monumentPiece); + } + return List.copyOf(pieces); + } + + static int resolveYBandOffset(BoundingBox bounds, IrisStructureYBand yBand, ChunkPos startChunk) { + Objects.requireNonNull(bounds, "Native structure bounds must not be null"); + Objects.requireNonNull(startChunk, "Native structure Y band requires a start chunk"); + int target = yBandTargetMidpointY( + bounds.minY(), bounds.maxY(), yBand.resolvedMin(), yBand.resolvedMax(), startChunk); + return Math.subtractExact(target, midpointY(bounds.minY(), bounds.maxY())); + } + + static int yBandTargetMidpointY(int minY, int maxY, int bandMin, int bandMax, ChunkPos startChunk) { + int height = Math.subtractExact(maxY, minY); + int lowest = Math.addExact(bandMin, height / 2); + int highest = Math.subtractExact(bandMax, height - height / 2); + if (lowest > highest) { + // The band is shorter than the structure, so only its midpoint can be honoured. + return Math.floorDiv(Math.addExact(bandMin, bandMax), 2); + } + int span = highest - lowest + 1; + if (span == 1) { + return lowest; + } + long identity = (long) startChunk.x() * 341873128712L ^ (long) startChunk.z() * 132897987541L; + return lowest + new RNG(identity).nextInt(span); + } + + private static int midpointY(int minY, int maxY) { + return minY + (maxY - minY) / 2; + } + + static int resolveBuriedOffset(BoundingBox bounds, int requestedOffset, int worldMinY, + int worldMaxYExclusive, IntBinaryOperator surfaceHeight) { + Objects.requireNonNull(bounds, "Native structure bounds must not be null"); + Objects.requireNonNull(surfaceHeight, "Underground native structure requires a terrain height resolver"); + long width = (long) bounds.maxX() - bounds.minX() + 1L; + long depth = (long) bounds.maxZ() - bounds.minZ() + 1L; + if (width <= 0L || depth <= 0L || width > MAX_BURIAL_COLUMNS / depth) { + throw new IllegalStateException("Underground native structure burial footprint is invalid or exceeds " + + MAX_BURIAL_COLUMNS + " columns"); + } + int maximumOffset = requestedOffset; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + int allowedTopY = surfaceHeight.applyAsInt(x, z) - UNDERGROUND_SURFACE_CLEARANCE; + maximumOffset = Math.min(maximumOffset, allowedTopY - bounds.maxY()); + } + } + int clampedOffset = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), maximumOffset, worldMinY, worldMaxYExclusive); + if (clampedOffset > maximumOffset) { + IrisLogging.warn("Native structure burial at " + bounds.minX() + "," + bounds.minZ() + + " clamped to world floor: wanted " + maximumOffset + ", used " + clampedOffset); + } + return clampedOffset; + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java new file mode 100644 index 000000000..2e5ba4e7a --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java @@ -0,0 +1,395 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; +import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner; +import art.arcane.iris.engine.framework.NativeStructureStartPlan; +import art.arcane.iris.engine.framework.NativeStructureVolume; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntBinaryOperator; +import java.util.function.Supplier; + +/** + * Resolves the world-space piece bounds of every native structure that will generate around a query rect. + * + *

The answer is derived from the seed, the registries and this pack's structure policy only: candidate origins + * come from the placement grid and from vanilla structure placements, and each candidate is assembled through the + * same {@link NativeStructureFactory} path chunk generation uses. Nothing here reads chunk state, ownership records + * or generation progress, so the same query answers identically no matter which chunks already exist. + */ +public final class NativeStructureVolumeIndex { + private static final int ORIGIN_REACH_CHUNKS = NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS; + private static final int MAX_CACHED_ORIGIN_CHUNKS = 16_384; + private static final int MAX_CACHED_QUERY_CHUNKS = 4_096; + private static final Map INDEXES = + Collections.synchronizedMap(new WeakHashMap<>()); + private static final Set WARNED_RESOLUTION_FAILURES = ConcurrentHashMap.newKeySet(); + + private final Context context; + private final OriginResolver origins; + private final Map> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS); + private final Map> queryCache = lru(MAX_CACHED_QUERY_CHUNKS); + private final ConcurrentHashMap>> originBuilds = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap>> queryBuilds = + new ConcurrentHashMap<>(); + + private NativeStructureVolumeIndex(Context context) { + this.context = Objects.requireNonNull(context, "Native structure volume context must not be null"); + this.origins = this::buildOriginVolumes; + } + + private NativeStructureVolumeIndex(OriginResolver origins) { + this.context = null; + this.origins = Objects.requireNonNull(origins, "Native structure origin resolver must not be null"); + } + + public static void install(Engine engine, Context context) { + Objects.requireNonNull(engine, "Native structure volume index requires an engine"); + INDEXES.put(engine, new NativeStructureVolumeIndex(context)); + } + + public static void uninstall(Engine engine) { + if (engine != null) { + INDEXES.remove(engine); + } + } + + public static KList volumes(Engine engine, int minX, int minZ, int maxX, int maxZ) { + NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine); + return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ); + } + + static NativeStructureVolumeIndex forTesting(OriginResolver origins) { + return new NativeStructureVolumeIndex(origins); + } + + static int originReachChunks() { + return ORIGIN_REACH_CHUNKS; + } + + KList resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) { + int fromChunkX = Math.min(minX, maxX) >> 4; + int toChunkX = Math.max(minX, maxX) >> 4; + int fromChunkZ = Math.min(minZ, maxZ) >> 4; + int toChunkZ = Math.max(minZ, maxZ) >> 4; + KList matches = null; + for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) { + for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) { + for (NativeStructureVolume volume : chunkVolumes(engine, chunkX, chunkZ)) { + if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) { + continue; + } + if (matches == null) { + matches = new KList<>(); + } + if (!matches.contains(volume)) { + matches.add(volume); + } + } + } + } + return matches == null ? NativeStructureVolume.NONE : matches; + } + + private KList chunkVolumes(Engine engine, int chunkX, int chunkZ) { + return cached(queryCache, queryBuilds, chunkKey(chunkX, chunkZ), + () -> buildChunkVolumes(engine, chunkX, chunkZ)); + } + + private KList buildChunkVolumes(Engine engine, int chunkX, int chunkZ) { + int minX = chunkX << 4; + int minZ = chunkZ << 4; + int maxX = minX + 15; + int maxZ = minZ + 15; + KList volumes = null; + for (int originX = chunkX - ORIGIN_REACH_CHUNKS; originX <= chunkX + ORIGIN_REACH_CHUNKS; originX++) { + for (int originZ = chunkZ - ORIGIN_REACH_CHUNKS; originZ <= chunkZ + ORIGIN_REACH_CHUNKS; originZ++) { + for (NativeStructureVolume volume : originVolumes(engine, originX, originZ)) { + if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) { + continue; + } + if (volumes == null) { + volumes = new KList<>(); + } + volumes.add(volume); + } + } + } + return volumes == null ? NativeStructureVolume.NONE : volumes; + } + + KList originVolumes(Engine engine, int chunkX, int chunkZ) { + return cached(originCache, originBuilds, chunkKey(chunkX, chunkZ), + () -> origins.volumesAt(engine, chunkX, chunkZ)); + } + + private KList buildOriginVolumes(Engine engine, int chunkX, int chunkZ) { + ChunkGeneratorStructureState state = context.structureState().get(); + ChunkGenerator generator = context.generator().get(); + BiomeSource biomeSource = context.biomeSource().get(); + if (state == null || generator == null || biomeSource == null) { + return NativeStructureVolume.NONE; + } + Registry registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Bindings bindings = new Bindings(state, generator, biomeSource); + KList volumes = appendPlannedVolumes(engine, bindings, registry, chunkX, chunkZ, null); + volumes = appendVanillaVolumes(engine, bindings, registry, chunkX, chunkZ, volumes); + return volumes == null ? NativeStructureVolume.NONE : volumes; + } + + private KList appendPlannedVolumes(Engine engine, Bindings bindings, + Registry registry, int chunkX, int chunkZ, + KList volumes) { + KList plans; + try { + plans = NativeStructurePlacementPlanner.plansAt(engine, chunkX, chunkZ); + } catch (RuntimeException | Error error) { + warnOnce("iris-placements", error); + return volumes; + } + + KList target = volumes; + for (NativeStructureStartPlan plan : plans) { + IrisNativeStructureDecision decision = NativeStructurePlacementPlanner.decisionFor(plan); + if (!decision.generate()) { + continue; + } + Identifier identifier = Identifier.tryParse(plan.source().getStructure()); + if (identifier == null) { + continue; + } + Structure structure = registry.getValue(identifier); + if (structure == null) { + continue; + } + String structureId = identifier.toString(); + try { + StructureStart generated = NativeStructureFactory.generate( + generationContext(engine, bindings), registry.wrapAsHolder(structure), plan, 0); + target = appendPieces(target, structureId, generated); + } catch (RuntimeException | Error error) { + warnOnce(structureId, error); + } + } + return target; + } + + private KList appendVanillaVolumes(Engine engine, Bindings bindings, + Registry registry, int chunkX, int chunkZ, + KList volumes) { + ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + IntBinaryOperator surfaceHeight = surfaceHeight(engine); + int worldMinY = context.heightAccessor().getMinY(); + int worldMaxYExclusive = worldMinY + context.heightAccessor().getHeight(); + KList target = volumes; + for (Holder setHolder : bindings.state().possibleStructureSets()) { + StructureSet set = setHolder.value(); + if (!set.placement().isStructureChunk(bindings.state(), chunkX, chunkZ)) { + continue; + } + for (StructureSet.StructureSelectionEntry entry : set.structures()) { + Holder holder = entry.structure(); + Structure structure = holder.value(); + Identifier identifier = registry.getKey(structure); + if (identifier == null) { + continue; + } + String structureId = identifier.toString(); + boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step()); + try { + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve( + engine, structureId, undergroundStep); + if (!decision.generate()) { + continue; + } + StructureStart generated = structure.generate( + holder, + context.levelKey(), + context.registryAccess(), + bindings.generator(), + bindings.biomeSource(), + bindings.state().randomState(), + context.templateManager(), + bindings.state().getLevelSeed(), + chunkPos, + 0, + context.heightAccessor(), + structure.biomes()::contains + ); + if (generated == null || !generated.isValid()) { + continue; + } + NativeStructureVerticalPlacer.applyVerticalPlacement( + generated, + structureId, + decision.yShift(), + bindings.generator().getSeaLevel(), + worldMinY, + worldMaxYExclusive, + undergroundStep, + decision.preserveSourceY(), + decision.yBand(), + surfaceHeight); + target = appendPieces(target, structureId, generated); + } catch (RuntimeException | Error error) { + warnOnce(structureId, error); + } + } + } + return target; + } + + private NativeStructureFactory.GenerationContext generationContext(Engine engine, Bindings bindings) { + return new NativeStructureFactory.GenerationContext( + context.registryAccess(), + bindings.generator(), + bindings.biomeSource(), + bindings.state().randomState(), + context.templateManager(), + bindings.state().getLevelSeed(), + context.levelKey(), + context.heightAccessor(), + biome -> true, + bindings.generator().getSeaLevel(), + surfaceHeight(engine) + ); + } + + private record Bindings(ChunkGeneratorStructureState state, ChunkGenerator generator, BiomeSource biomeSource) { + } + + @FunctionalInterface + interface OriginResolver { + KList volumesAt(Engine engine, int chunkX, int chunkZ); + } + + private static IntBinaryOperator surfaceHeight(Engine engine) { + return (x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight(); + } + + static KList appendPieces(KList volumes, + String structureId, StructureStart start) { + if (start == null || !start.isValid()) { + return volumes; + } + KList target = volumes; + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + if (target == null) { + target = new KList<>(); + } + target.add(new NativeStructureVolume(structureId, + bounds.minX(), bounds.minY(), bounds.minZ(), + bounds.maxX(), bounds.maxY(), bounds.maxZ())); + } + return target; + } + + private KList cached(Map> cache, + ConcurrentHashMap>> builds, + long key, Supplier> loader) { + synchronized (cache) { + KList hit = cache.get(key); + if (hit != null) { + return hit; + } + } + + CompletableFuture> future = new CompletableFuture<>(); + CompletableFuture> existing = builds.putIfAbsent(key, future); + if (existing != null) { + return NativeBuildFutures.awaitBuild(existing, "Native structure volume build"); + } + + // Close the check-then-claim window: a racer whose cache check missed before the + // previous builder cached could claim the build slot after it retired and rebuild. + synchronized (cache) { + KList published = cache.get(key); + if (published != null) { + future.complete(published); + builds.remove(key, future); + return published; + } + } + + try { + KList built = loader.get(); + synchronized (cache) { + cache.put(key, built); + } + future.complete(built); + return built; + } catch (RuntimeException | Error error) { + future.completeExceptionally(error); + throw error; + } finally { + builds.remove(key, future); + } + } + + private static void warnOnce(String structureId, Throwable error) { + if (WARNED_RESOLUTION_FAILURES.add(structureId)) { + IrisLogging.reportError("Native structure volume resolution failed for '" + structureId + + "'; objects will not be vetoed against it.", error); + } + } + + private static long chunkKey(int chunkX, int chunkZ) { + return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL); + } + + private static Map> lru(int capacity) { + return new LinkedHashMap<>(16, 0.75F, true) { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > capacity; + } + }; + } + + /** + * The generator, biome source and structure state arrive as suppliers so an installed index never holds the + * level (and through it the engine) strongly. That keeps the weakly keyed registry collectable on world unload. + */ + public record Context( + RegistryAccess registryAccess, + StructureTemplateManager templateManager, + ResourceKey levelKey, + LevelHeightAccessor heightAccessor, + Supplier generator, + Supplier biomeSource, + Supplier structureState + ) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/WorldgenTerrainHeightmaps.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/WorldgenTerrainHeightmaps.java new file mode 100644 index 000000000..6bd3e81e7 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/WorldgenTerrainHeightmaps.java @@ -0,0 +1,113 @@ +package art.arcane.iris.nativegen; + +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; +import net.minecraft.util.SimpleBitStorage; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntBinaryOperator; + +/** + * Writes WORLD_SURFACE_WG and OCEAN_FLOOR_WG from Iris engine heights. + * + * Iris writes its terrain straight into LevelChunkSection, so neither vanilla's noise fill nor + * ProtoChunk.setBlockState ever updates the two worldgen heightmaps. Without these writes + * ChunkAccess.getHeight lazily primes the map from finished blocks - Iris trees, leaves and snow + * included - and vanilla pieces that self-snap against WORLD_SURFACE_WG (igloo) or OCEAN_FLOOR_WG + * (ocean ruins, buried treasure, mineshaft interiors) anchor to the canopy instead of the terrain. + * + * Both resolvers take world block coordinates and return the absolute first free Y, matching + * Heightmap's internal "first available" semantics: WORLD_SURFACE_WG counts fluid, OCEAN_FLOOR_WG + * does not. + */ +public final class WorldgenTerrainHeightmaps { + private static final int COLUMNS = 256; + private static final int PLACEMENT_CHUNK_MARGIN = 1; + + private WorldgenTerrainHeightmaps() { + } + + public static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { + Objects.requireNonNull(chunk, "Iris worldgen heightmap priming requires a chunk"); + Objects.requireNonNull(surfaceFirstFreeY, + "Iris worldgen heightmap priming requires a surface height resolver"); + Objects.requireNonNull(floorFirstFreeY, + "Iris worldgen heightmap priming requires an ocean floor height resolver"); + write(chunk, Heightmap.Types.WORLD_SURFACE_WG, surfaceFirstFreeY); + write(chunk, Heightmap.Types.OCEAN_FLOOR_WG, floorFirstFreeY); + } + + public static void primeStructurePlacement(WorldGenLevel world, ChunkPos generationCenter, + List starts, + IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { + Objects.requireNonNull(world, + "Iris worldgen heightmap priming requires a generation level"); + Objects.requireNonNull(generationCenter, + "Iris worldgen heightmap priming requires a generation center"); + if (starts == null || starts.isEmpty()) { + return; + } + Set primed = new HashSet<>(); + for (StructureStart start : starts) { + if (start == null || !start.isValid()) { + continue; + } + BoundingBox bounds = start.getBoundingBox(); + int minChunkX = SectionPos.blockToSectionCoord(bounds.minX()) - PLACEMENT_CHUNK_MARGIN; + int maxChunkX = SectionPos.blockToSectionCoord(bounds.maxX()) + PLACEMENT_CHUNK_MARGIN; + int minChunkZ = SectionPos.blockToSectionCoord(bounds.minZ()) - PLACEMENT_CHUNK_MARGIN; + int maxChunkZ = SectionPos.blockToSectionCoord(bounds.maxZ()) + PLACEMENT_CHUNK_MARGIN; + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + if (Math.abs(chunkX - generationCenter.x()) > PLACEMENT_CHUNK_MARGIN + || Math.abs(chunkZ - generationCenter.z()) > PLACEMENT_CHUNK_MARGIN) { + continue; + } + if (!primed.add(ChunkPos.pack(chunkX, chunkZ))) { + continue; + } + if (!world.hasChunk(chunkX, chunkZ)) { + continue; + } + primeTerrain(world.getChunk(chunkX, chunkZ), surfaceFirstFreeY, floorFirstFreeY); + } + } + } + } + + private static void write(ChunkAccess chunk, Heightmap.Types type, IntBinaryOperator firstFreeY) { + ChunkPos pos = chunk.getPos(); + int minY = chunk.getMinY(); + int height = chunk.getHeight(); + int baseX = pos.getMinBlockX(); + int baseZ = pos.getMinBlockZ(); + SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), COLUMNS); + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + int firstFree = firstFreeY.applyAsInt(baseX + x, baseZ + z) - minY; + storage.set(x + z * 16, Mth.clamp(firstFree, 0, height)); + } + } + Heightmap heightmap = chunk.getOrCreateHeightmapUnprimed(type); + long[] packed = storage.getRaw(); + int expected = heightmap.getRawData().length; + if (expected != packed.length) { + throw new IllegalStateException("Iris cannot prime " + type + " for chunk " + + pos.x() + "," + pos.z() + ": packed " + packed.length + + " words for a heightmap storing " + expected + + "; Heightmap.setRawData would silently fall back to block scanning"); + } + heightmap.setRawData(chunk, type, packed); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/InitialSpawnQueue.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/InitialSpawnQueue.java new file mode 100644 index 000000000..46b903c71 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/InitialSpawnQueue.java @@ -0,0 +1,185 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; + +final class InitialSpawnQueue { + private static final long DEFAULT_MAX_AGE_NANOS = TimeUnit.MINUTES.toNanos(5); + + private final int capacity; + private final long maxAgeNanos; + private final LongSupplier nanoTime; + private final ArrayDeque queue; + private final ArrayDeque expiryOrder; + private final Map pending; + private final Set queued; + private boolean closed; + + InitialSpawnQueue(int capacity) { + this(capacity, DEFAULT_MAX_AGE_NANOS, System::nanoTime); + } + + InitialSpawnQueue(int capacity, long maxAgeNanos, LongSupplier nanoTime) { + if (capacity < 1) { + throw new IllegalArgumentException("capacity must be positive"); + } + if (maxAgeNanos < 1L) { + throw new IllegalArgumentException("maxAgeNanos must be positive"); + } + this.capacity = capacity; + this.maxAgeNanos = maxAgeNanos; + this.nanoTime = nanoTime; + this.queue = new ArrayDeque<>(Math.min(capacity, 256)); + this.expiryOrder = new ArrayDeque<>(Math.min(capacity, 256)); + this.pending = new HashMap<>(); + this.queued = new HashSet<>(); + } + + synchronized boolean offer(long key) { + if (closed || pending.containsKey(key)) { + return false; + } + if (pending.size() >= capacity) { + expire(nanoTime.getAsLong()); + } + if (pending.size() >= capacity) { + return false; + } + long offeredAt = nanoTime.getAsLong(); + pending.put(key, offeredAt); + expiryOrder.addLast(new Expiry(key, offeredAt)); + if (queued.add(key)) { + queue.addLast(key); + } + return true; + } + + synchronized int batchSize(int limit) { + if (closed || limit <= 0) { + return 0; + } + return Math.min(limit, queue.size()); + } + + synchronized Long poll() { + long now = nanoTime.getAsLong(); + Long key; + while ((key = queue.pollFirst()) != null) { + queued.remove(key); + Long offeredAt = pending.get(key); + if (offeredAt == null) { + continue; + } + if (expired(offeredAt, now)) { + pending.remove(key); + continue; + } + expire(now); + return key; + } + expire(now); + return null; + } + + synchronized void retry(long key) { + Long offeredAt = pending.get(key); + if (closed || offeredAt == null || expired(offeredAt, nanoTime.getAsLong())) { + pending.remove(key); + queued.remove(key); + return; + } + if (!queued.add(key)) { + return; + } + queue.addLast(key); + } + + synchronized void complete(long key) { + pending.remove(key); + if (queued.remove(key)) { + queue.removeFirstOccurrence(key); + } + expire(nanoTime.getAsLong()); + } + + synchronized boolean isEmpty() { + return queue.isEmpty(); + } + + synchronized int size() { + return pending.size(); + } + + synchronized void clear() { + queue.clear(); + expiryOrder.clear(); + pending.clear(); + queued.clear(); + } + + synchronized void close() { + closed = true; + clear(); + } + + /** + * Drops offers that aged out, plus entries for keys that already left {@code pending}. + * {@code expiryOrder} is append-only and {@code nanoTime} is monotonic, so the deque is sorted by + * offer time: the loop stops at the first live, unexpired entry. That makes the saturated-queue + * call O(1) in the common case instead of the O(capacity) scan it used to be under the monitor, + * and running it from poll/complete keeps the deque from growing past the live offers. + * + *

Expired keys are normally left in {@code queue}/{@code queued}: {@link #poll()} already drops keys + * that are no longer pending, so the drain deque self-cleans without an O(n) sweep. That only holds while + * something polls - a producer that keeps offering into a queue nobody drains would grow both past + * {@code capacity} forever - so once the deque is over capacity it is swept against {@code pending} here. + */ + private void expire(long now) { + Expiry head; + while ((head = expiryOrder.peekFirst()) != null) { + Long offeredAt = pending.get(head.key()); + boolean live = offeredAt != null && offeredAt.longValue() == head.offeredAt(); + if (live && !expired(head.offeredAt(), now)) { + break; + } + expiryOrder.pollFirst(); + if (live) { + pending.remove(head.key()); + } + } + if (queue.size() > capacity) { + queue.removeIf(key -> !pending.containsKey(key)); + queued.retainAll(pending.keySet()); + } + } + + private boolean expired(long offeredAt, long now) { + return now - offeredAt >= maxAgeNanos; + } + + private record Expiry(long key, long offeredAt) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java new file mode 100644 index 000000000..de3e09a03 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java @@ -0,0 +1,900 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +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.util.project.context.IrisContext; +import art.arcane.volmlib.util.math.RNG; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.QuartPos; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +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 net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.function.Predicate; +import java.util.stream.Stream; + +final class IrisModdedBiomeSource extends BiomeSource { + private static final int UNRESOLVED_WARN_KEYS_MAX = 256; + 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 BiomeSource serializedSource; + private final Set warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet(); + private final Set structureStateSources = ConcurrentHashMap.newKeySet(); + // Pack generation. Every cache on this source is keyed by it, which is what keeps memoized tables from + // surviving a repoint with the previous pack's content. + private final AtomicLong packGeneration = new AtomicLong(); + private volatile BiomeHolderTable visibleBiomeCache = new BiomeHolderTable(); + private volatile BiomeHolderTable structureBiomeCache = new BiomeHolderTable(); + private volatile BiomeHolderTable surfaceStructureBiomeCache = new BiomeHolderTable(); + private volatile IrisModdedChunkGenerator generator; + private volatile Set possibleStructureBiomeKeys; + private volatile BiomeKeySets biomeKeySets; + private volatile PossibleBiomes possibleBiomesCache; + + IrisModdedBiomeSource(BiomeSource serializedSource) { + this.serializedSource = serializedSource; + } + + void bind(IrisModdedChunkGenerator generator) { + this.generator = generator; + } + + void clearCaches() { + // Republish empty tables instead of iterating: a repoint must not walk hundreds of thousands of slots + // on the calling thread, and every reader is a pure function of the key so a lost entry is only a miss. + // An empty table allocates no slot array, and a reader that captured the previous table writes its + // in-flight value there, which is what keeps a pre-repoint holder from ever landing in the new table. + packGeneration.incrementAndGet(); + visibleBiomeCache = new BiomeHolderTable(); + structureBiomeCache = new BiomeHolderTable(); + surfaceStructureBiomeCache = new BiomeHolderTable(); + warnedUnresolvedBiomeKeys.clear(); + possibleStructureBiomeKeys = null; + biomeKeySets = null; + possibleBiomesCache = null; + for (StructureStateBiomeSource source : structureStateSources) { + source.clearCache(); + } + } + + /** + * Pack generation counter. Platform code that memoizes anything derived from this source (the imported + * feature table) keys its memo on this value so {@code repoint} cannot leave stale content behind. + */ + long packGeneration() { + return packGeneration.get(); + } + + BiomeSource forStructureState(HolderLookup structureSets) { + LinkedHashSet> possible = new LinkedHashSet<>(); + Registry registry = biomeRegistry(); + if (registry == null) { + throw new IllegalStateException("Iris cannot create structure state without the biome registry"); + } + Set generatedBiomeKeys = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys()); + Set missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys); + missingBiomeKeys.removeAll(registeredBiomeKeys(registry)); + if (!missingBiomeKeys.isEmpty()) { + throw new IllegalStateException("Iris structure biomes are not registered: " + missingBiomeKeys); + } + structureSets.listElements().forEach((Holder.Reference structureSet) -> { + for (StructureSet.StructureSelectionEntry entry : structureSet.value().structures()) { + for (Holder biome : entry.structure().value().biomes()) { + String key = holderKey(biome); + if (isGeneratedBiomeKey(key, generatedBiomeKeys)) { + possible.add(biome); + } + } + } + }); + StructureStateBiomeSource source = new StructureStateBiomeSource(this, Set.copyOf(possible)); + structureStateSources.add(source); + return source; + } + + @Override + protected MapCodec codec() { + throw new UnsupportedOperationException("IrisModdedBiomeSource is serialized through IrisModdedChunkGenerator"); + } + + @Override + protected Stream> collectPossibleBiomes() { + return resolvePossibleBiomes().ordered().stream(); + } + + /** + * Overridden because {@link BiomeSource#possibleBiomes()} memoizes its answer for the lifetime of the + * instance, and this source outlives a {@code repoint} to a different pack. The returned set keeps + * biome-registry iteration order: vanilla builds its feature-per-step table with + * {@code List.copyOf(possibleBiomes())}, and FeatureSorter's cycle detection walks that list, so an + * unordered set makes cycle detection depend on JVM hash order. + */ + @Override + public Set> possibleBiomes() { + return resolvePossibleBiomes().set(); + } + + /** + * Registry-ordered view of {@link #possibleBiomes()} for platform code that has to build a feature table. + */ + List> orderedPossibleBiomes() { + return resolvePossibleBiomes().ordered(); + } + + /** + * Resolves any registered biome holder by key, whether or not this source can emit it. The imported feature + * pass needs this: a biome's vanilla derivative is where its features come from, and a sea or shore biome's + * structure derivative is rewritten away from that derivative, so the derivative itself is not always one of + * the biomes this source claims. Null when the key is unknown or the registry is not up yet. + */ + Holder registeredBiome(String key) { + Registry registry = biomeRegistry(); + return registry == null ? null : resolveHolder(registry, key); + } + + /** + * Deliberately lock-free: resolving can bind an engine, which takes the generator monitor, and the + * generator takes its monitor before invalidating this cache. A lock here would close that cycle. Two + * threads racing only duplicate idempotent work. + */ + private PossibleBiomes resolvePossibleBiomes() { + long generation = packGeneration.get(); + PossibleBiomes cached = possibleBiomesCache; + if (cached != null && cached.generation() == generation) { + return cached; + } + List> ordered = collectPossibleBiomeHolders(); + PossibleBiomes resolved = new PossibleBiomes(generation, ordered, + Collections.unmodifiableSet(new LinkedHashSet<>(ordered))); + possibleBiomesCache = resolved; + return resolved; + } + + private List> collectPossibleBiomeHolders() { + BiomeKeySets keys = biomeKeySets(); + Set generatedBiomeKeys = requireConfiguredStructureBiomeKeys(keys.required()); + Set visibleBiomeKeys = keys.visibleOnly(); + Registry registry = biomeRegistry(); + LinkedHashSet> possible = new LinkedHashSet<>(); + if (registry == null) { + for (Holder biome : serializedSource.possibleBiomes()) { + String key = holderKey(biome); + if (isGeneratedBiomeKey(key, generatedBiomeKeys) + || isGeneratedBiomeKey(key, visibleBiomeKeys)) { + possible.add(biome); + } + } + } else { + Set missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys); + missingBiomeKeys.removeAll(registeredBiomeKeys(registry)); + if (!missingBiomeKeys.isEmpty()) { + throw new IllegalStateException("Iris structure biomes are not registered: " + + missingBiomeKeys); + } + // Registry-ordered, never a hash-ordered walk: see possibleBiomes(). + registry.listElements().forEach((Holder.Reference reference) -> { + String key = holderKey(reference); + if (isGeneratedBiomeKey(key, generatedBiomeKeys) + || isGeneratedBiomeKey(key, visibleBiomeKeys)) { + possible.add(reference); + } + }); + warnUnregisteredVisibleBiomes(registry, visibleBiomeKeys); + } + if (possible.isEmpty()) { + String phase = registry == null ? "serialized biome bootstrap" : "biome registry"; + throw new IllegalStateException("Iris configured structure biomes are absent from the " + + phase + ": " + generatedBiomeKeys); + } + return List.copyOf(possible); + } + + /** + * Scatter and derivative keys are advisory: a typo there must not stop a world from loading the way a + * missing structure biome does, so they are reported once and skipped. + */ + private void warnUnregisteredVisibleBiomes(Registry registry, Set visibleBiomeKeys) { + if (visibleBiomeKeys.isEmpty()) { + return; + } + Set missing = new LinkedHashSet<>(visibleBiomeKeys); + missing.removeAll(registeredBiomeKeys(registry)); + for (String key : missing) { + if (!warnedUnresolvedBiomeKeys.add(key)) { + continue; + } + if (warnedUnresolvedBiomeKeys.size() > UNRESOLVED_WARN_KEYS_MAX) { + warnedUnresolvedBiomeKeys.clear(); + } + ModdedIrisLog.warn("Iris biome " + key + " is referenced by derivative or scatter but is not" + + " registered; it is dropped from this dimension's biome source"); + } + } + + @Override + public Holder getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) { + Engine engine = engineOrNull(); + if (engine == null) { + return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler); + } + GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_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 (!isReady(engine)) { + throw new IllegalStateException("Iris structure biome lookup has no active engine runtime"); + } + return getNoiseBiome(engine, quartX, quartY, quartZ, sampler); + } + } + + private Holder getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ, + Climate.Sampler sampler) { + if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) { + return getSurfaceStructureBiome(engine, quartX, quartZ, sampler); + } + long key = packNoiseKey(quartX, quartY, quartZ); + BiomeHolderTable cache = structureBiomeCache; + Holder cached = cache.get(key); + if (cached != null) { + return cached; + } + Holder resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler); + cache.put(key, resolved); + return resolved; + } + + Holder getVisibleNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) { + Engine engine = engineOrNull(); + if (engine == null) { + return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler); + } + GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_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)) { + if (!isReady(engine)) { + throw new IllegalStateException("Iris visible biome lookup has no active engine runtime"); + } + long key = packNoiseKey(quartX, quartY, quartZ); + BiomeHolderTable cache = visibleBiomeCache; + Holder cached = cache.get(key); + if (cached != null) { + return cached; + } + Holder resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler); + cache.put(key, resolved); + return resolved; + } + } + + boolean isStructureReachable(Holder structure) { + Engine engine = engineOrNull(); + if (engine == null) { + return isStructureReachable(structure, possibleStructureBiomeKeys()); + } + GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_reachability"); + if (lease == null) { + throw new IllegalStateException("Iris structure reachability was rejected during an engine transition"); + } + try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + return isStructureReachable(structure, possibleStructureBiomeKeys()); + } + } + + private boolean isStructureReachable(Holder structure, Set possible) { + for (Holder biome : structure.value().biomes()) { + String key = holderKey(biome); + if (isGeneratedBiomeKey(key, possible)) { + return true; + } + } + return false; + } + + @Override + public Set> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) { + int minQuartY = QuartPos.fromBlock(y - radius); + Engine engine = engineOrNull(); + if (engine == null) { + return super.getBiomesWithin(x, y, z, radius, sampler); + } + GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_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 (!isReady(engine)) { + throw new IllegalStateException("Iris biome radius lookup has no active engine runtime"); + } + boolean monumentQuery = isMonumentSurfaceBiomeQuery( + y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight()); + if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) { + 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> biomes = new HashSet<>(columns); + for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) { + for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) { + biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler)); + } + } + return biomes; + } + } + + private Holder getSurfaceStructureBiome(Engine engine, int quartX, int quartZ, + Climate.Sampler sampler) { + long key = packColumnKey(quartX, quartZ); + BiomeHolderTable cache = surfaceStructureBiomeCache; + Holder cached = cache.get(key); + if (cached != null) { + return cached; + } + Holder resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler); + cache.put(key, resolved); + return resolved; + } + + private Holder resolveSurfaceStructureBiome(Engine engine, int quartX, int quartZ, + Climate.Sampler sampler) { + Registry registry = biomeRegistry(); + if (!isReady(engine)) { + throw new IllegalStateException("Iris structure biome lookup ran before engine binding"); + } + if (registry == null) { + throw new IllegalStateException("Iris structure biome lookup has no biome registry"); + } + IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2); + if (irisBiome == null) { + throw new IllegalStateException("Iris returned no surface structure biome at quart " + + quartX + "," + quartZ); + } + Holder resolved = resolveHolder(registry, irisBiome.getStructureDerivativeKey()); + if (resolved == null) { + throw new IllegalStateException("Iris structure biome derivative '" + + irisBiome.getStructureDerivativeKey() + "' is not registered at quart " + + quartX + "," + quartZ); + } + return resolved; + } + + private Holder resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ, + Climate.Sampler sampler) { + Registry registry = biomeRegistry(); + BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ); + if (resolution == null) { + throw new IllegalStateException("Iris returned no structure biome at quart " + + quartX + "," + quartY + "," + quartZ); + } + if (registry == null) { + throw new IllegalStateException("Iris structure biome lookup has no biome registry"); + } + Holder resolved = resolveHolder(registry, resolution.irisBiome().getStructureDerivativeKey()); + if (resolved == null) { + throw new IllegalStateException("Iris structure biome derivative '" + + resolution.irisBiome().getStructureDerivativeKey() + "' is not registered at block " + + resolution.blockX() + "," + resolution.blockY() + "," + resolution.blockZ()); + } + return resolved; + } + + private Holder resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ, + Climate.Sampler sampler) { + Registry registry = biomeRegistry(); + BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ); + if (resolution == null || registry == null) { + return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler); + } + String biomeKey; + if (resolution.irisBiome().isCustom()) { + IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome( + resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ()); + if (customBiome == null) { + return fallbackBiome(registry, "custom derivative of '" + + resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler); + } + biomeKey = ModdedWorldgenIds.biomeRef(engine, customBiome.getId()); + } else if (resolution.underground()) { + biomeKey = resolution.irisBiome().getGroundBiomeKey( + resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ()); + } else { + biomeKey = resolution.irisBiome().getSkyBiomeKey( + resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ()); + } + Holder resolved = resolveHolder(registry, biomeKey); + return resolved == null + ? fallbackBiome(registry, biomeKey, quartX, quartY, quartZ, sampler) + : resolved; + } + + private BiomeResolution resolveBiomeResolution(Engine engine, int quartX, int quartY, int quartZ) { + if (!isReady(engine)) { + return null; + } + int blockX = quartX << 2; + int blockY = quartY << 2; + int blockZ = quartZ << 2; + int internalY = blockY - engine.getMinHeight(); + int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40); + boolean underground = false; + IrisBiome irisBiome; + if (internalY <= caveSwitchY) { + int surfaceY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue(); + underground = isUnderground(internalY, surfaceY); + irisBiome = underground + ? engine.getCaveBiome(blockX, internalY, blockZ) + : 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; + } + IrisModdedChunkGenerator current = generator; + long worldSeed = current == null ? engine.getWorld().getRawWorldSeed() : current.visibleBiomeSeed(); + long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ); + return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, new RNG(seed)); + } + + private Holder resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) { + IrisModdedChunkGenerator current = generator; + if (current == null) { + throw new IllegalStateException("Iris structure biome source is not bound to its generator"); + } + Engine engine = current.awaitStructureEngine(); + GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_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 (!isReady(engine)) { + throw new IllegalStateException("Iris structure biome lookup has no active engine runtime"); + } + Registry registry = biomeRegistry(); + if (registry == null) { + throw new IllegalStateException("Iris structure biome lookup has no biome registry"); + } + IrisBiome irisBiome; + if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) { + irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2); + } else { + BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ); + irisBiome = resolution == null ? null : resolution.irisBiome(); + } + if (irisBiome == null) { + throw new IllegalStateException("Iris returned no structure biome at quart " + + quartX + "," + quartY + "," + quartZ); + } + String derivativeKey = irisBiome.getStructureDerivativeKey(); + Holder resolved = resolveHolder(registry, derivativeKey); + if (resolved == null) { + throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey + + "' is not registered at quart " + quartX + "," + quartY + "," + quartZ); + } + return resolved; + } + } + + static long biomeResolutionSeed(long worldSeed, int blockX, int blockY, int blockZ) { + return worldSeed + ^ ((long) blockX * 341873128712L) + ^ ((long) blockY * 132897987541L) + ^ ((long) blockZ * 42317861L); + } + + static boolean isGeneratedBiomeKey(String key, Set generatedBiomeKeys) { + return key != null && generatedBiomeKeys.contains(key.toLowerCase(Locale.ROOT)); + } + + static Set requireConfiguredStructureBiomeKeys(Set configuredBiomeKeys) { + if (configuredBiomeKeys == null || configuredBiomeKeys.isEmpty()) { + throw new IllegalStateException("Iris has no configured structure biomes"); + } + return configuredBiomeKeys; + } + + static boolean isGuaranteedSurfaceBiome(int quartY, int minHeight) { + int internalY = (quartY << 2) - minHeight; + int caveSwitchY = Math.max(-8 - minHeight, 40); + return internalY > caveSwitchY; + } + + static boolean isUnderground(int internalY, int surfaceY) { + return internalY <= surfaceY - 8; + } + + static boolean isMonumentSurfaceBiomeQuery(int blockY, int radius, int minHeight, int fluidHeight) { + return radius == 29 && blockY == minHeight + fluidHeight; + } + + static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) { + return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS + ? STRONGHOLD_RING_SEARCH_QUART_STEP + : 1; + } + + private static boolean isReady(Engine engine) { + return engine != null && !engine.isClosed() && engine.getComplex() != null; + } + + private Engine engineOrNull() { + IrisModdedChunkGenerator current = generator; + return current == null ? null : current.structureEngineOrNull(); + } + + private GenerationSessionLease tryAcquireGenerationLease(Engine engine, String operation) { + if (engine == null || 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 Set possibleStructureBiomeKeys() { + Set cached = possibleStructureBiomeKeys; + if (cached != null) { + return cached; + } + Set possible = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys()); + Registry registry = biomeRegistry(); + if (registry == null) { + throw new IllegalStateException("Iris cannot resolve structure biomes without the biome registry"); + } + Set missing = new LinkedHashSet<>(possible); + missing.removeAll(registeredBiomeKeys(registry)); + if (!missing.isEmpty()) { + throw new IllegalStateException("Iris structure biomes are not registered: " + missing); + } + Set resolved = Set.copyOf(possible); + possibleStructureBiomeKeys = resolved; + return resolved; + } + + private Set exactStructureBiomeKeys() { + return biomeKeySets().required(); + } + + /** + * Required and visible-only biome keys for the current pack generation. Required keys are the structure + * derivatives and the generated custom biomes: a missing one is fatal, exactly as before. Visible-only + * keys are the raw derivative plus every {@code biomeScatter} and {@code biomeSkyScatter} entry - biomes + * Iris writes into chunk sections but which vanilla previously dropped, because + * {@code applyBiomeDecoration} intersects the chunk's biomes with {@code possibleBiomes()}. + */ + private BiomeKeySets biomeKeySets() { + long generation = packGeneration.get(); + BiomeKeySets cached = biomeKeySets; + if (cached != null && cached.generation() == generation) { + return cached; + } + BiomeKeySets resolved = collectBiomeKeySets(generation); + biomeKeySets = resolved; + return resolved; + } + + private BiomeKeySets collectBiomeKeySets(long generation) { + IrisModdedChunkGenerator current = generator; + if (current == null) { + return new BiomeKeySets(generation, Set.of(), Set.of()); + } + Engine engine = current.structureEngineOrNull(); + if (engine == null) { + return new BiomeKeySets(generation, current.configuredStructureBiomeKeys(), Set.of()); + } + GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome_keys"); + if (lease == null) { + throw new IllegalStateException("Iris structure biome key lookup was rejected during an engine transition"); + } + try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!isReady(engine)) { + throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime"); + } + LinkedHashSet required = new LinkedHashSet<>(); + LinkedHashSet visible = new LinkedHashSet<>(); + for (IrisBiome irisBiome : engine.getAllBiomes()) { + String derivative = normalizeKey(irisBiome.getStructureDerivativeKey()); + if (derivative != null) { + required.add(derivative); + } + if (irisBiome.isCustom()) { + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + required.add(ModdedWorldgenIds.biomeRef(engine, customBiome.getId())); + } + } + addVisibleBiomeKey(visible, irisBiome.getDerivativeKey()); + for (String scatter : irisBiome.getBiomeScatter()) { + addVisibleBiomeKey(visible, scatter); + } + for (String scatter : irisBiome.getBiomeSkyScatter()) { + addVisibleBiomeKey(visible, scatter); + } + } + visible.removeAll(required); + return new BiomeKeySets(generation, Set.copyOf(required), Set.copyOf(visible)); + } + } + + private static void addVisibleBiomeKey(Set target, String key) { + String normalized = normalizeKey(key); + if (normalized != null) { + target.add(normalized); + } + } + + private static Set registeredBiomeKeys(Registry registry) { + Set registered = new HashSet<>(); + registry.listElements().forEach((Holder.Reference reference) -> { + String key = holderKey(reference); + if (key != null) { + registered.add(key); + } + }); + return registered; + } + + private Registry biomeRegistry() { + MinecraftServer server = ModdedEngineBootstrap.currentServer(); + return server == null ? null : server.registryAccess().lookupOrThrow(Registries.BIOME); + } + + private static Holder resolveHolder(Registry registry, String key) { + if (key == null || key.isBlank()) { + return null; + } + Identifier identifier = Identifier.tryParse(key); + if (identifier == null) { + return null; + } + return registry.get(identifier).>map((Holder.Reference reference) -> reference).orElse(null); + } + + private static String holderKey(Holder holder) { + return holder.unwrapKey() + .map((ResourceKey key) -> key.identifier().toString().toLowerCase(Locale.ROOT)) + .orElse(null); + } + + private static String normalizeKey(String key) { + Identifier identifier = key == null ? null : Identifier.tryParse(key); + return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT); + } + + private Holder fallbackBiome(Registry registry, String unresolvedKey, + int quartX, int quartY, int quartZ, + Climate.Sampler sampler) { + Holder plains = resolveHolder(registry, "minecraft:plains"); + warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains", + quartX, quartY, quartZ); + return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains; + } + + private void warnUnresolvedBiome(String unresolvedKey, String fallback, + int quartX, int quartY, int quartZ) { + String key = unresolvedKey == null || unresolvedKey.isBlank() ? "" : unresolvedKey; + if (!warnedUnresolvedBiomeKeys.add(key)) { + return; + } + if (warnedUnresolvedBiomeKeys.size() > UNRESOLVED_WARN_KEYS_MAX) { + warnedUnresolvedBiomeKeys.clear(); + } + ModdedIrisLog.warn("Iris biome " + key + " is not registered; using " + fallback + + " at quart " + quartX + "," + quartY + "," + quartZ + + " (wrong biome generates; regenerate the forced datapack and restart)"); + } + + 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 record BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, + int blockZ, RNG rng) { + } + + private record BiomeKeySets(long generation, Set required, Set visibleOnly) { + } + + private record PossibleBiomes(long generation, List> ordered, Set> set) { + } + + /** + * Fixed-capacity open-addressed long-to-holder cache. Sized once and never resized, so there is no + * stop-the-world clear when it fills: a colliding write past the probe limit simply displaces the entry at + * the home slot, and the displaced key resolves again on its next lookup. Every cached value is a pure + * function of its key, so displacement can only cost work, never correctness. Entries are published as one + * immutable record, which is what keeps a concurrent writer from ever pairing one key with another's value. + * + *

The slot array is installed on the first put, never in the field initializer. One biome source is + * constructed per emitted world preset at datapack load, and every create-world screen builds them all, so + * eager arrays cost tens of megabytes of tables that nothing ever reads. An empty table answers every get + * with a miss, which is the same answer a cold table gives. + */ + private static final class BiomeHolderTable { + private static final int SLOTS = 32768; + private static final int MASK = SLOTS - 1; + private static final int PROBE_LIMIT = 8; + + private volatile AtomicReferenceArray table; + + Holder get(long key) { + AtomicReferenceArray slots = table; + if (slots == null) { + return null; + } + int home = home(key); + for (int probe = 0; probe < PROBE_LIMIT; probe++) { + Entry entry = slots.get((home + probe) & MASK); + if (entry == null) { + return null; + } + if (entry.key() == key) { + return entry.value(); + } + } + return null; + } + + void put(long key, Holder value) { + AtomicReferenceArray slots = table; + if (slots == null) { + slots = install(); + } + int home = home(key); + Entry entry = new Entry(key, value); + for (int probe = 0; probe < PROBE_LIMIT; probe++) { + int slot = (home + probe) & MASK; + Entry existing = slots.get(slot); + if (existing == null || existing.key() == key) { + slots.set(slot, entry); + return; + } + } + slots.set(home, entry); + } + + private synchronized AtomicReferenceArray install() { + AtomicReferenceArray existing = table; + if (existing != null) { + return existing; + } + AtomicReferenceArray created = new AtomicReferenceArray<>(SLOTS); + table = created; + return created; + } + + private static int home(long key) { + long mixed = key * 0x9E3779B97F4A7C15L; + return (int) ((mixed ^ (mixed >>> 32)) & MASK); + } + + private record Entry(long key, Holder value) { + } + } + + private static final class StructureStateBiomeSource extends BiomeSource { + private final IrisModdedBiomeSource delegate; + private final Set> possibleBiomes; + private volatile BiomeHolderTable resolvedBiomes = new BiomeHolderTable(); + + private StructureStateBiomeSource(IrisModdedBiomeSource delegate, Set> possibleBiomes) { + this.delegate = delegate; + this.possibleBiomes = possibleBiomes; + } + + private void clearCache() { + resolvedBiomes = new BiomeHolderTable(); + } + + @Override + protected MapCodec codec() { + throw new UnsupportedOperationException("Structure state biome sources are not serializable"); + } + + @Override + protected Stream> collectPossibleBiomes() { + return possibleBiomes.stream(); + } + + @Override + public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { + long key = packNoiseKey(x, y, z); + BiomeHolderTable cache = resolvedBiomes; + Holder cached = cache.get(key); + if (cached != null) { + return cached; + } + Holder resolved = delegate.resolveRequiredStructureBiome(x, y, z); + cache.put(key, resolved); + return resolved; + } + + @Override + public Pair> findBiomeHorizontal( + int x, + int y, + int z, + int searchRadius, + Predicate> 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); + } + + @Override + public Set> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) { + return super.getBiomesWithin(x, y, z, radius, sampler); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java new file mode 100644 index 000000000..c93b07ceb --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java @@ -0,0 +1,998 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy; +import art.arcane.iris.engine.framework.GenerationSessionException; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.framework.NativeStructureStartPlan; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisImportedStructureControl; +import art.arcane.iris.nativegen.NativeStructureStartInjector; +import art.arcane.iris.nativegen.NativeStructureReferenceRepair; +import art.arcane.iris.nativegen.NativeStructureVanillaLocator; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.iris.util.project.hunk.Hunk; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.BiomeResolver; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.blending.Blender; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.IntBinaryOperator; + +public final class IrisModdedChunkGenerator extends ChunkGenerator { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + // Vanilla-shaped fallback for an unbound generator (matches IrisDimension defaults). getMinY, + // getSeaLevel and getGenDepth are called from world creation and client screens, so they must + // answer without disk I/O and without throwing before a level is bound. + private static final ModdedDimensionMetadata.DimensionMetadata UNBOUND_HEIGHTS = + new ModdedDimensionMetadata.DimensionMetadata(-64, 320, 63); + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance instance) -> instance.group( + BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.serializedBiomeSource), + Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey) + ).apply(instance, IrisModdedChunkGenerator::new)); + + public static void startGenPool() { + ModdedGenPool.start(); + } + + public static void shutdownGenPool() { + ModdedGenPool.shutdown(); + } + + private final String dimensionKey; + private final String defaultPack; + private final String defaultDimensionKey; + private final BiomeSource serializedBiomeSource; + final IrisModdedBiomeSource structureBiomeSource; + private final ModdedEngineBinding engineBinding = new ModdedEngineBinding<>(60L, TimeUnit.SECONDS); + private final ModdedNativeStructureStage nativeStructures = new ModdedNativeStructureStage(this); + private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this); + private final ModdedImportedFeatureStage importedFeatures; + private final AtomicBoolean announced = new AtomicBoolean(false); + private volatile boolean unloading; + private volatile Engine engine; + private volatile String activePack; + private volatile String activeDimensionKey; + private volatile long seedOverride = Long.MIN_VALUE; + private volatile long lastChunkGenAt = 0L; + private volatile Set configuredStructureBiomeKeys; + private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack; + private volatile ModdedDimensionMetadata.DimensionMetadata heightMetadata; + private volatile ServerLevel boundLevel; + + public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) { + this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource)); + } + + private IrisModdedChunkGenerator(BiomeSource serializedBiomeSource, String dimensionKey, IrisModdedBiomeSource structureBiomeSource) { + this(serializedBiomeSource, dimensionKey, structureBiomeSource, + new ModdedImportedFeatureStage(structureBiomeSource)); + } + + private IrisModdedChunkGenerator(BiomeSource serializedBiomeSource, String dimensionKey, + IrisModdedBiomeSource structureBiomeSource, + ModdedImportedFeatureStage importedFeatures) { + // Two-argument ChunkGenerator constructor: the getter maps Iris custom-biome holders onto the + // generation settings of their vanilla derivative, which is what feeds the per-step feature lists and + // BiomeFilter's hasFeature gate. It is a pass-through to vanilla's default getter until a pack turns + // importedFeatures on, so with the control off nothing about generation changes. + super(structureBiomeSource, importedFeatures::generationSettings); + this.importedFeatures = importedFeatures; + importedFeatures.bind(this); + this.dimensionKey = dimensionKey; + this.serializedBiomeSource = serializedBiomeSource; + this.structureBiomeSource = structureBiomeSource; + this.structureBiomeSource.bind(this); + int colon = dimensionKey.indexOf(':'); + this.defaultPack = colon >= 0 ? dimensionKey.substring(0, colon) : dimensionKey; + this.defaultDimensionKey = colon >= 0 ? dimensionKey.substring(colon + 1) : dimensionKey; + this.activePack = defaultPack; + this.activeDimensionKey = defaultDimensionKey; + } + + public synchronized void repoint(String pack, String packDimensionKey, long seed) { + ServerLevel level = boundLevel(); + if (level != null) { + repointAndBind(level, pack, packDimensionKey, seed); + return; + } + applyUnboundConfiguration(pack, packDimensionKey, seed); + } + + synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) { + requireBindingAllowed(); + if (level.getChunkSource().getGenerator() != this) { + throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'"); + } + requireGlobalStructureGeneration( + level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey); + Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed); + try { + ModdedWorldEngines.installReplacement(level, replacement); + } catch (Throwable error) { + try { + ModdedWorldEngines.closeUnregistered(replacement); + } catch (Throwable cleanupError) { + error.addSuppressed(cleanupError); + } + if (error instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (error instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to replace its engine", error); + } + this.boundLevel = level; + this.activePack = pack; + this.activeDimensionKey = packDimensionKey; + this.seedOverride = seed; + this.engine = replacement; + this.configuredStructureBiomeKeys = null; + this.configuredPack = null; + this.heightMetadata = engineHeights(replacement); + this.engineBinding.reset(); + this.engineBinding.complete(replacement); + this.announced.set(false); + this.structureBiomeSource.clearCaches(); + this.importedFeatures.invalidate(); + this.nativeStructures.clearWorldCheckStructureShifts(); + this.spawnTables.resetVanillaSpawnBiomes(); + // Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off. + // Never waits on a build owned by another thread: this method owns the generator monitor and the build + // path can need it. + this.importedFeatures.prepareWithoutWaiting(replacement); + } + + public synchronized void unbindEngine() { + unloading = true; + ServerLevel level = boundLevel(); + unbindEngine(level); + } + + synchronized void unbindEngine(ServerLevel level) { + unloading = true; + if (level != null) { + ModdedWorldEngines.evictOrThrow(level); + } + clearEngineBinding(); + } + + private void clearEngineBinding() { + this.engine = null; + this.boundLevel = null; + this.configuredStructureBiomeKeys = null; + this.configuredPack = null; + this.engineBinding.reset(); + this.announced.set(false); + this.structureBiomeSource.clearCaches(); + this.importedFeatures.invalidate(); + this.nativeStructures.clearWorldCheckStructureShifts(); + this.spawnTables.resetVanillaSpawnBiomes(); + } + + private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) { + this.activePack = pack; + this.activeDimensionKey = packDimensionKey; + this.seedOverride = seed; + this.engine = null; + this.boundLevel = null; + this.configuredStructureBiomeKeys = null; + this.configuredPack = null; + this.engineBinding.reset(); + this.announced.set(false); + this.structureBiomeSource.clearCaches(); + this.importedFeatures.invalidate(); + this.nativeStructures.clearWorldCheckStructureShifts(); + this.spawnTables.resetVanillaSpawnBiomes(); + primeHeightMetadata(); + } + + public synchronized void resetToDefault() { + repoint(defaultPack, defaultDimensionKey, Long.MIN_VALUE); + } + + public String activePack() { + return activePack; + } + + public String activeDimensionKey() { + return activeDimensionKey; + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + public ChunkGeneratorStructureState createState(HolderLookup structureSets, RandomState randomState, long seed) { + ChunkGeneratorStructureState state = ChunkGeneratorStructureState.createForNormal( + randomState, seed, structureBiomeSource.forStructureState(structureSets), structureSets); + return ModdedStructureSetFrequencyOverrides.apply(state, configuredImportedStructures()); + } + + private IrisImportedStructureControl configuredImportedStructures() { + Engine current = engine; + if (current != null && !current.isClosed() && !current.isClosing()) { + IrisDimension dimension = current.getDimension(); + if (dimension != null && dimension.getImportedStructures() != null) { + return dimension.getImportedStructures(); + } + } + IrisImportedStructureControl importedStructures = configuredPack().dimension().getImportedStructures(); + return importedStructures == null ? new IrisImportedStructureControl() : importedStructures; + } + + @Override + public Pair> findNearestMapStructure(ServerLevel level, HolderSet holders, + BlockPos pos, int radius, + boolean findUnexplored) { + Engine current = engine(); + try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + HolderSet reachable = nativeStructures.filterReachableNativeStructures( + level, holders, current); + NativeStructureVanillaLocator.Candidate nativeCandidate = + reachable.size() == 0 ? null + : NativeStructureVanillaLocator.predict( + level, reachable, pos, radius, findUnexplored); + return nativeStructures.findNearestIrisStructure( + level, holders, pos, Math.max(0, radius), findUnexplored, current, nativeCandidate); + } + } + + public boolean isNativeStructureReachable(Holder structure) { + return structure != null && structureBiomeSource.isStructureReachable(structure); + } + + private ServerLevel boundLevel() { + ServerLevel cached = boundLevel; + if (cached != null) { + return cached; + } + MinecraftServer server = ModdedEngineBootstrap.currentServer(); + if (server == null) { + return null; + } + // Snapshot, never server.getAllLevels(): this runs off the server thread from data queries. + for (ServerLevel level : ModdedServerLevels.levels(server)) { + if (level.getChunkSource().getGenerator() == this) { + boundLevel = level; + return level; + } + } + return null; + } + + Engine engine() { + requireBindingAllowed(); + Engine cached = engine; + requireCompletedShutdown(cached); + if (cached != null && !cached.isClosed()) { + return cached; + } + ServerLevel level = boundLevel(); + if (level == null) { + throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet"); + } + return bindEngine(level); + } + + synchronized void bindLevel(ServerLevel level) { + if (level.getChunkSource().getGenerator() != this) { + throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'"); + } + requireCompletedShutdown(engine); + unloading = false; + Engine bound = bindEngine(level); + nativeStructures.installVolumeIndex(level, bound); + // Bind time: a feature-order cycle is reported here, once, and degrades to features-off. Non-waiting for + // the same reason as repointAndBind: this method owns the generator monitor. + importedFeatures.prepareWithoutWaiting(bound); + LOGGER.info("Iris bound {}: chunk system {}", level.dimension().identifier(), ModdedGenPool.describeChunkSystem()); + } + + private Engine bindEngine(ServerLevel level) { + requireBindingAllowed(); + try { + requireGlobalStructureGeneration( + level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey); + } catch (RuntimeException error) { + engineBinding.fail(error); + throw error; + } + // Cache the owning level so hot paths never scan the level map to find themselves. + boundLevel = level; + Engine cached = engine; + requireCompletedShutdown(cached); + if (cached != null && !cached.isClosed() && cached.getComplex() != null) { + engineBinding.complete(cached); + return cached; + } + synchronized (this) { + requireBindingAllowed(); + Engine existing = engine; + requireCompletedShutdown(existing); + if (existing != null && !existing.isClosed() && existing.getComplex() != null) { + engineBinding.complete(existing); + return existing; + } + try { + Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride); + requireCompletedShutdown(created); + if (created.isClosed() || created.getComplex() == null) { + throw new IllegalStateException("Iris generator '" + dimensionKey + + "' created an engine without a ready biome complex"); + } + engine = created; + boundLevel = level; + heightMetadata = engineHeights(created); + configuredStructureBiomeKeys = null; + structureBiomeSource.clearCaches(); + engineBinding.complete(created); + return created; + } catch (Throwable error) { + engineBinding.fail(error); + if (error instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (error instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind", error); + } + } + } + + private void requireCompletedShutdown(Engine current) { + if (current == null || !current.isClosing() || current.isClosed()) { + return; + } + // closing && !closed is usually a TRANSIENT seal: hotloadComplex/hotloadSilently set closing while + // they swap the runtime and clear it on completion. Wait the transition out instead of crashing the + // chunk pipeline; only a seal that never resolves is a genuine incomplete shutdown. + long deadline = System.currentTimeMillis() + 30_000L; + while (System.currentTimeMillis() < deadline && !current.hasFailed()) { + if (current.isClosed() || !current.isClosing()) { + return; + } + try { + Thread.sleep(10L); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + break; + } + } + throw new IllegalStateException("Iris generator '" + dimensionKey + + "' cannot bind while its previous engine shutdown remains incomplete" + + (current.hasFailed() ? " (engine failed)" : " (waited 30s)")); + } + + private void requireBindingAllowed() { + if (unloading) { + throw new IllegalStateException("Iris generator '" + dimensionKey + "' is unloading and cannot bind an engine"); + } + } + + static void requireGlobalStructureGeneration(boolean enabled, String dimensionKey) { + if (enabled) { + return; + } + String remedy = integratedEnvironment() + ? "enable 'Generate Structures' for this world; Iris requires it, then deny families through " + + "importedStructures.disabled or complete keys through importedStructures.disabledExact" + : "set generate-structures=true in server.properties, restart the server, " + + "then deny families through importedStructures.disabled or complete keys through " + + "importedStructures.disabledExact"; + throw new IllegalStateException("Iris generator '" + dimensionKey + + "' cannot bind while generate-structures=false; " + remedy); + } + + private static boolean integratedEnvironment() { + try { + return ModdedEngineBootstrap.loader().clientEnvironment(); + } catch (Throwable e) { + // No loader bound (unit tests, very early boot): assume dedicated wording. + return false; + } + } + + Engine engineOrNull() { + requireBindingAllowed(); + Engine cached = engine; + requireCompletedShutdown(cached); + if (cached != null && !cached.isClosed()) { + return cached; + } + try { + return engine(); + } catch (Throwable ignored) { + return null; + } + } + + Engine structureEngineOrNull() { + requireBindingAllowed(); + engineBinding.throwIfFailed(dimensionKey); + Engine cached = engine; + requireCompletedShutdown(cached); + if (cached != null && !cached.isClosed() && cached.getComplex() != null) { + return cached; + } + ServerLevel level = boundLevel(); + return level == null ? null : bindEngine(level); + } + + Engine awaitStructureEngine() { + requireBindingAllowed(); + Engine current = engine; + requireCompletedShutdown(current); + if (current != null && !current.isClosed() && current.getComplex() != null) { + return current; + } + Engine bound = engineBinding.await(dimensionKey); + requireCompletedShutdown(bound); + if (bound.isClosed() || bound.getComplex() == null) { + throw new IllegalStateException("Iris generator '" + dimensionKey + + "' completed bootstrap without a ready biome complex"); + } + return bound; + } + + private Engine requireDataQueryEngine(String operation) { + requireBindingAllowed(); + Engine current = engine; + requireCompletedShutdown(current); + if (current != null && !current.isClosed() && current.getComplex() != null) { + return current; + } + ServerLevel level = boundLevel(); + if (level != null) { + return bindEngine(level); + } + MinecraftServer server = ModdedEngineBootstrap.currentServer(); + if (server == null) { + throw new IllegalStateException("Iris generator '" + dimensionKey + "' cannot answer " + + operation + " without an active server"); + } + if (server.isSameThread()) { + throw new IllegalStateException("Iris generator '" + dimensionKey + "' cannot answer " + + operation + " on the server thread before its ServerLevel is bound"); + } + return awaitStructureEngine(); + } + + long visibleBiomeSeed() { + long configuredSeed = seedOverride; + if (configuredSeed != Long.MIN_VALUE) { + return configuredSeed; + } + ServerLevel level = boundLevel(); + if (level != null) { + return level.getSeed(); + } + Engine current = engine; + return current == null ? 0L : current.getWorld().getRawWorldSeed(); + } + + Set configuredStructureBiomeKeys() { + Set cached = configuredStructureBiomeKeys; + if (cached != null) { + return cached; + } + synchronized (this) { + if (configuredStructureBiomeKeys != null) { + return configuredStructureBiomeKeys; + } + Engine current = engine; + if (current != null && !current.isClosed() && !current.isClosing()) { + try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + Set resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys( + current.getAllBiomes(), current.getDimension().getLoadKey()); + configuredStructureBiomeKeys = resolved; + return resolved; + } catch (GenerationSessionException e) { + if (!current.isClosing() && !e.isExpectedTeardown()) { + throw new IllegalStateException("Iris configured biome lookup could not acquire its engine runtime.", e); + } + } + } + ModdedDimensionMetadata.ConfiguredPack configured = configuredPack(); + Set resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys( + configured.dimension(), configured.data()); + configuredStructureBiomeKeys = resolved; + return resolved; + } + } + + private static ModdedDimensionMetadata.DimensionMetadata engineHeights(Engine engine) { + IrisDimension dimension = engine.getDimension(); + int minY = engine.getMinHeight(); + return new ModdedDimensionMetadata.DimensionMetadata(minY, engine.getMaxHeight(), + dimension == null ? minY : minY + dimension.getFluidHeight()); + } + + /** + * Resolves the pack height metadata on the calling thread so the vanilla height accessors stay pure + * reads. Never fatal: a pack that cannot be read here falls back to {@link #UNBOUND_HEIGHTS} until a + * bind succeeds. + */ + private void primeHeightMetadata() { + try { + heightMetadata = configuredPack().metadata(); + } catch (Throwable e) { + LOGGER.warn("Iris generator '{}' could not pre-resolve pack heights for {}:{}: {}", + dimensionKey, activePack, activeDimensionKey, e.toString()); + } + } + + private ModdedDimensionMetadata.DimensionMetadata heightMetadata() { + ModdedDimensionMetadata.DimensionMetadata cached = heightMetadata; + if (cached != null) { + return cached; + } + ModdedDimensionMetadata.ConfiguredPack pack = configuredPack; + if (pack == null) { + return UNBOUND_HEIGHTS; + } + ModdedDimensionMetadata.DimensionMetadata resolved = pack.metadata(); + heightMetadata = resolved; + return resolved; + } + + private ModdedDimensionMetadata.ConfiguredPack configuredPack() { + ModdedDimensionMetadata.ConfiguredPack cached = configuredPack; + if (cached != null) { + return cached; + } + synchronized (this) { + if (configuredPack != null) { + return configuredPack; + } + PackValidationRegistry.requireLoadable(activePack); + File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey); + IrisData data = IrisData.get(packDirectory); + IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey); + if (dimension == null) { + throw new IllegalStateException("Iris dimension '" + activeDimensionKey + + "' missing from pack " + packDirectory.getAbsolutePath()); + } + ModdedDimensionMetadata.ConfiguredPack resolved = new ModdedDimensionMetadata.ConfiguredPack( + data, dimension, ModdedDimensionMetadata.dimensionMetadata(dimension)); + configuredPack = resolved; + heightMetadata = resolved.metadata(); + return resolved; + } + } + + public String dimensionKey() { + return dimensionKey; + } + + /** + * Operator-facing importedFeatures state: null when the control is off, "on" when the feature table is + * live, "degraded" when the control is enabled but the table failed to build (feature-order cycle). + */ + public String importedFeaturesStatus() { + Engine current = engineIfBound(); + if (current == null || !NativeFeatureGenerationPolicy.isEnabled(current)) { + return null; + } + return importedFeatures.active() ? "on" : "degraded"; + } + + public Engine engineIfBound() { + Engine current = engine; + return unloading || current == null || current.isClosing() || current.isClosed() ? null : current; + } + + public Engine commandEngine() { + return engine(); + } + + public long lastChunkGenAt() { + return lastChunkGenAt; + } + + public void onHotload() { + configuredStructureBiomeKeys = null; + structureBiomeSource.clearCaches(); + importedFeatures.invalidate(); + nativeStructures.clearWorldCheckStructureShifts(); + spawnTables.resetVanillaSpawnBiomes(); + } + + @Override + public WeightedList getMobsAt( + Holder biome, StructureManager structureManager, MobCategory category, BlockPos pos) { + WeightedList explicitSpawns = biome.value().getMobSettings().getMobs(category); + WeightedList resolvedSpawns = super.getMobsAt( + biome, structureManager, category, pos); + if (resolvedSpawns != explicitSpawns) { + return resolvedSpawns; + } + + Registry registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME); + spawnTables.initializeVanillaSpawnBiomes(registry); + Holder vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value()); + if (vanillaSpawnBiome == null) { + return explicitSpawns; + } + + WeightedList vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(category); + if (explicitSpawns.isEmpty()) { + return vanillaSpawns; + } + if (vanillaSpawns.isEmpty()) { + return explicitSpawns; + } + + return spawnTables.mergedSpawnTable(biome.value(), category, vanillaSpawns, explicitSpawns); + } + + @Override + public CompletableFuture createBiomes(RandomState randomState, Blender blender, + StructureManager structureManager, ChunkAccess chunk) { + Engine current = engine(); + try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_biomes"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler()); + return CompletableFuture.completedFuture(chunk); + } + } + + @Override + public CompletableFuture fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) { + Engine generationEngine = engine(); + ChunkPos pos = chunk.getPos(); + lastChunkGenAt = System.currentTimeMillis(); + LOGGER.debug("Iris generating chunk {},{}", pos.x(), pos.z()); + + PlatformBlockState air = IrisPlatforms.get().registries().air(); + + if (ModdedGenPool.parallelChunkSystem()) { + return CompletableFuture.completedFuture( + generateTerrain(chunk, generationEngine, pos, air)); + } + return CompletableFuture.supplyAsync( + () -> generateTerrain(chunk, generationEngine, pos, air), + ModdedGenPool.pool()); + } + + private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, + PlatformBlockState air) { + try (GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline"); + IrisContext.Scope ignored = IrisContext.open(generationEngine, lease.sessionId(), null)) { + if (announced.compareAndSet(false, true)) { + LOGGER.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})", + dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z()); + } + int dimMinY = generationEngine.getMinHeight(); + int dimMaxY = generationEngine.getMaxHeight(); + int height = dimMaxY - dimMinY; + ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air); + Hunk biomes = Hunk.newArrayHunk(16, height, 16); + generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false); + + writeBlocks(chunk, blocks, dimMinY, height); + writeTerrainHeightmaps(chunk, generationEngine, pos, height); + Heightmap.primeHeightmaps(chunk, EnumSet.of( + Heightmap.Types.MOTION_BLOCKING, + Heightmap.Types.MOTION_BLOCKING_NO_LEAVES)); + ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z()); + return chunk; + } catch (GenerationSessionException e) { + if (generationEngine.isClosing() || e.isExpectedTeardown()) { + LOGGER.debug("Iris chunk {},{} skipped: engine sealed for hotload/teardown", pos.x(), pos.z()); + throw new IllegalStateException( + "Iris chunk generation was rejected during an engine transition.", e); + } + LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e); + throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e); + } catch (Throwable e) { + LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e); + throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e); + } + } + + private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) { + int baseX = pos.getMinBlockX(); + int baseZ = pos.getMinBlockZ(); + writeTerrainHeightmap(chunk, Heightmap.Types.WORLD_SURFACE_WG, height, + (x, z) -> generationEngine.getHeight(baseX + x, baseZ + z, false) + 1); + writeTerrainHeightmap(chunk, Heightmap.Types.OCEAN_FLOOR_WG, height, + (x, z) -> generationEngine.getHeight(baseX + x, baseZ + z, true) + 1); + } + + private void writeTerrainHeightmap(ChunkAccess chunk, Heightmap.Types type, int height, + IntBinaryOperator heightResolver) { + Heightmap heightmap = chunk.getOrCreateHeightmapUnprimed(type); + heightmap.setRawData(chunk, type, ModdedHeightmaps.terrainRawData(height, heightResolver)); + } + + public BiomeResolver regenBiomeResolver() { + engine(); + return structureBiomeSource::getVisibleNoiseBiome; + } + + private void writeBlocks(ChunkAccess chunk, ModdedBlockBuffer blocks, int dimMinY, int height) { + int chunkMinY = chunk.getMinY(); + int chunkMaxY = chunkMinY + chunk.getHeight(); + int from = Math.max(dimMinY, chunkMinY); + int to = Math.min(dimMinY + height, chunkMaxY); + int baseX = chunk.getPos().getMinBlockX(); + int baseZ = chunk.getPos().getMinBlockZ(); + + for (int y = from; y < to; ) { + int sectionIndex = chunk.getSectionIndex(y); + LevelChunkSection section = chunk.getSection(sectionIndex); + int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4; + int sectionEnd = Math.min(sectionMinY + 16, to); + section.acquire(); + try { + for (int blockY = y; blockY < sectionEnd; blockY++) { + int bufferY = blockY - dimMinY; + int localY = blockY & 15; + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + PlatformBlockState state = blocks.rawOrNull(x, bufferY, z); + if (state == null) { + continue; + } + BlockState blockState = (BlockState) state.nativeHandle(); + section.setBlockState(x, localY, z, blockState, false); + if (blockState.hasBlockEntity()) { + createDefaultBlockEntity(chunk, new BlockPos(baseX + x, blockY, baseZ + z), blockState); + } + } + } + } + } finally { + section.release(); + } + y = sectionEnd; + } + } + + static void createDefaultBlockEntity(ChunkAccess chunk, BlockPos position, BlockState state) { + if (!(state.getBlock() instanceof EntityBlock entityBlock)) { + return; + } + BlockEntity blockEntity = entityBlock.newBlockEntity(position, state); + if (blockEntity != null) { + chunk.setBlockEntity(blockEntity); + } + } + + @Override + public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk) { + } + + @Override + public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) { + } + + @Override + public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) { + Engine current = engine(); + // Self-heal for an engine bound through a data-query path instead of bindLevel; a no-op once prepared. + importedFeatures.prepare(current); + try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + nativeStructures.placeVanillaStructures(level, chunk, structureManager); + // Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk + // step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the + // dimension set importedFeatures.enabled. + importedFeatures.run(level, chunk, current); + } + } + + @Override + public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey levelKey) { + Engine current = engine(); + try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + Map previousStarts = new HashMap<>(chunk.getAllStarts()); + super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey); + Map configuredStarts = NativeStructureStartInjector.inject( + new NativeStructureStartInjector.InjectionContext( + current, + registryAccess, + structureState, + structureManager, + chunk, + templateManager, + levelKey, + this, + structureBiomeSource + )); + nativeStructures.adjustGeneratedStructures( + registryAccess, chunk, previousStarts, configuredStarts, current, templateManager); + } + } + + @Override + public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) { + Engine current = engine(); + try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + NativeStructureReferenceRepair.createReferences( + current, level, structureManager, chunk); + } + } + + Integer worldCheckStructureShift(String structureId, ChunkPos startChunk) { + return nativeStructures.worldCheckStructureShift(structureId, startChunk); + } + + @Override + public void spawnOriginalMobs(WorldGenRegion region) { + Registry registry = region.registryAccess().lookupOrThrow(Registries.BIOME); + spawnTables.initializeVanillaSpawnBiomes(registry); + ChunkPos center = region.getCenter(); + Holder visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); + Holder vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value()); + 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 int getGenDepth() { + Engine current = engine; + return current == null || current.isClosed() + ? heightMetadata().depth() + : current.getMaxHeight() - current.getMinHeight(); + } + + @Override + public int getSeaLevel() { + Engine current = engine; + return current == null || current.isClosed() + ? heightMetadata().seaLevel() + : current.getMinHeight() + current.getDimension().getFluidHeight(); + } + + @Override + public int getMinY() { + Engine current = engine; + return current == null || current.isClosed() + ? heightMetadata().minY() + : current.getMinHeight(); + } + + @Override + public int getSpawnHeight(LevelHeightAccessor heightAccessor) { + return ModdedDimensionMetadata.clampSpawnHeight(heightAccessor.getMinY(), heightAccessor.getHeight()); + } + + @Override + public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) { + Engine current = requireDataQueryEngine("base height"); + boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState()); + try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1; + } catch (GenerationSessionException e) { + throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e); + } + } + + @Override + public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) { + int minY = heightAccessor.getMinY(); + BlockState[] states = new BlockState[heightAccessor.getHeight()]; + Engine current = requireDataQueryEngine("base column"); + try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + BlockState airState = Blocks.AIR.defaultBlockState(); + int surface = current.getHeight(x, z, true); + int fluid = current.getHeight(x, z, false); + BlockState stone = Blocks.STONE.defaultBlockState(); + BlockState water = Blocks.WATER.defaultBlockState(); + int solidEnd = Math.max(0, Math.min(states.length, surface + 1)); + int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid + 1))); + Arrays.fill(states, 0, solidEnd, stone); + Arrays.fill(states, solidEnd, fluidEnd, water); + Arrays.fill(states, fluidEnd, states.length, airState); + return new NoiseColumn(minY, states); + } catch (GenerationSessionException e) { + throw new IllegalStateException("Iris base column query could not acquire its engine runtime.", e); + } + } + + GenerationSessionLease requireGenerationLease(Engine current, String operation) { + try { + return current.acquireGenerationLease(operation); + } catch (GenerationSessionException exception) { + throw new IllegalStateException("Iris " + operation + " could not acquire its engine runtime.", exception); + } + } + + @Override + public void addDebugScreenInfo(List info, RandomState randomState, BlockPos pos) { + info.add("Iris dimension: " + dimensionKey); + } + +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/MainWorldService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/MainWorldService.java new file mode 100644 index 000000000..1f07f0c31 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/MainWorldService.java @@ -0,0 +1,373 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public final class MainWorldService { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final String MARKER_NAME = "mainworld.pending"; + private static final String PROPERTIES_NAME = "server.properties"; + /** + * Distinct exit status for the staged main-world restart, so a wrapper can tell it apart from a clean + * operator stop (0) and from a crash. 64 is the conventional first application-defined status. + */ + private static final int AUTO_RESTART_EXIT_STATUS = 64; + private static final String[] VANILLA_DIMENSION_FOLDERS = { + "region", + "entities", + "poi", + "mantle", + "dimensions/minecraft/overworld", + "dimensions/minecraft/the_nether", + "dimensions/minecraft/the_end", + "DIM-1", + "DIM1" + }; + + private MainWorldService() { + } + + public static String presetIdFor(String packRef) { + String value = packRef.trim(); + int colon = value.indexOf(':'); + String pack = colon >= 0 ? value.substring(0, colon) : value; + String dimension = colon >= 0 ? value.substring(colon + 1) : value; + return ModdedWorldgenIds.presetRef(pack, dimension); + } + + public static void reconcileEarly() { + try { + ModdedModConfig config = ModdedModConfig.get(); + String pack = config.mainWorldPack(); + if (pack == null || pack.isBlank()) { + return; + } + Path instanceRoot = verifiedInstanceRoot("reconcile the Iris main world"); + if (instanceRoot == null) { + return; + } + Path properties = instanceRoot.resolve(PROPERTIES_NAME); + String target = presetIdFor(pack); + String currentType = readProperty(properties, "level-type"); + if (!target.equals(currentType)) { + writeLevelProperties(properties, target, config.mainWorldSeed()); + markPending(); + LOGGER.warn("Iris main world '{}' staged: {} level-type set to {}. Restart again to generate it (this boot still uses the previous overworld; player data is kept).", + pack, properties, target); + if (config.mainWorldAutoRestart()) { + LOGGER.warn("Iris mainWorldAutoRestart is enabled; stopping the JVM now with exit status {} so a restart wrapper brings the server back on the new main world.", + AUTO_RESTART_EXIT_STATUS); + LOGGER.warn("Configure the start script to restart the server on exit status {} (status 0 means a clean stop, so it must not be reused for this).", + AUTO_RESTART_EXIT_STATUS); + System.exit(AUTO_RESTART_EXIT_STATUS); + } + return; + } + if (!isPending()) { + return; + } + Path worldRoot; + try { + worldRoot = resolveWorldRoot(instanceRoot, properties); + } catch (MissingWorldRootException missing) { + // First boot of a brand new instance, or a --universe/--world layout Iris cannot see from mod + // bootstrap: there is no prior overworld to move aside, so this is nothing to quarantine, not a + // reason to refuse startup. + clearPending(); + LOGGER.warn("Iris main world '{}' had nothing to quarantine: {} does not exist. Continuing boot; the overworld generates as {}.", + pack, missing.path(), target); + return; + } + Path recovery = quarantineVanillaDimensions(worldRoot); + clearPending(); + LOGGER.warn("Iris main world '{}' generated fresh: moved the prior overworld/nether/end data from {} to {} so this boot regenerates them as {} (player data kept).", + pack, worldRoot, recovery, target); + } catch (Throwable e) { + LOGGER.error("Iris main world reconciliation failed", e); + throw new IllegalStateException( + "Iris refused startup after main-world reconciliation failed", e); + } + } + + public static boolean stage(String packRef, long seed) { + if (ModdedEngineBootstrap.loader().clientEnvironment()) { + LOGGER.error("Iris main-world replacement is only available on dedicated servers; use the Create World generator selector in singleplayer"); + return false; + } + Path instanceRoot = verifiedInstanceRoot("stage the Iris main world"); + if (instanceRoot == null) { + return false; + } + try { + writeLevelProperties(instanceRoot.resolve(PROPERTIES_NAME), presetIdFor(packRef), seed); + markPending(); + return true; + } catch (IOException e) { + LOGGER.error("Iris failed to stage the main world in server.properties", e); + return false; + } + } + + public static void clearOverride() { + try { + clearPending(); + } catch (IOException e) { + LOGGER.error("Iris failed to clear the pending main world marker", e); + } + } + + /** + * net.minecraft.server.Main reads server.properties as Paths.get("server.properties"), so the authoritative + * instance root is the JVM working directory - not configDir().getParent(), which points somewhere else + * entirely whenever the loader config tree is relocated (-Dfabric.configDir, a shared config mount, a + * launcher that starts the server from another directory). Refuse loudly rather than write or move files + * against a guessed root: every caller treats null as "not a dedicated instance we may touch". + */ + private static Path verifiedInstanceRoot(String operation) { + Path workingDirectory = Path.of("").toAbsolutePath().normalize(); + if (Files.isRegularFile(workingDirectory.resolve(PROPERTIES_NAME))) { + return workingDirectory; + } + LOGGER.error("Iris refuses to {}: no {} in the server working directory {}", operation, PROPERTIES_NAME, workingDirectory); + LOGGER.error("Iris only edits main-world properties in the directory the dedicated server reads {} from, and it moves no world data outside it. Start the server from its instance directory, or clear mainWorldPack in irisworldgen/modded.json.", PROPERTIES_NAME); + return null; + } + + private static Path markerFile() { + return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve(MARKER_NAME); + } + + private static boolean isPending() { + return Files.isRegularFile(markerFile()); + } + + private static void markPending() throws IOException { + Path marker = markerFile(); + Files.createDirectories(marker.getParent()); + Files.writeString(marker, "pending", StandardCharsets.UTF_8); + } + + private static void clearPending() throws IOException { + Files.deleteIfExists(markerFile()); + } + + private static String readProperty(Path properties, String key) throws IOException { + if (!Files.isRegularFile(properties)) { + return null; + } + List lines = Files.readAllLines(properties, StandardCharsets.UTF_8); + String prefix = key + "="; + for (String line : lines) { + if (line.startsWith(prefix)) { + return unescape(line.substring(prefix.length()).trim()); + } + } + return null; + } + + /** + * Temp file plus ATOMIC_MOVE, the same publish shape ModdedForcedDatapack.writePublishedHash uses. A + * truncated server.properties bricks the next boot, and this write happens during bootstrap where a crash + * or a kill is entirely plausible. + */ + private static void writeLevelProperties(Path properties, String target, long seed) throws IOException { + List lines = Files.isRegularFile(properties) + ? new ArrayList<>(Files.readAllLines(properties, StandardCharsets.UTF_8)) + : new ArrayList<>(); + setProperty(lines, "level-type", escape(target)); + if (seed != 0L) { + setProperty(lines, "level-seed", Long.toString(seed)); + } + + Path temp = properties.resolveSibling(PROPERTIES_NAME + ".iris-tmp-" + UUID.randomUUID()); + Files.write(temp, lines, StandardCharsets.UTF_8); + try { + Files.move(temp, properties, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException atomicUnsupported) { + Files.move(temp, properties, StandardCopyOption.REPLACE_EXISTING); + } + } + + private static void setProperty(List lines, String key, String value) { + String prefix = key + "="; + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).startsWith(prefix)) { + lines.set(i, prefix + value); + return; + } + } + lines.add(prefix + value); + } + + /** + * Mirrors net.minecraft.server.Main world resolution: the universe root is --universe (default the working + * directory) and the world folder name is --world, falling back to the level-name property. + */ + private static Path resolveWorldRoot(Path instanceRoot, Path properties) throws IOException { + List arguments = processArguments(); + Path universe = universeRoot(instanceRoot, commandLineOption(arguments, "universe")); + String levelName = firstNonBlank(commandLineOption(arguments, "world"), + firstNonBlank(readProperty(properties, "level-name"), "world")); + return resolveWorldRoot(universe, levelName); + } + + static Path universeRoot(Path instanceRoot, String universeOption) { + return (universeOption == null || universeOption.isBlank() + ? instanceRoot + : instanceRoot.resolve(universeOption)).toAbsolutePath().normalize(); + } + + static Path resolveWorldRoot(Path universe, String levelName) throws IOException { + if (!Files.isDirectory(universe)) { + throw new MissingWorldRootException("Server universe directory does not exist: " + universe, universe); + } + Path worldRoot = universe.resolve(levelName).toAbsolutePath().normalize(); + if (worldRoot.equals(universe) || !worldRoot.startsWith(universe)) { + throw new IOException("Unsafe world name outside the server universe " + universe + ": " + levelName); + } + if (!Files.isDirectory(worldRoot)) { + throw new MissingWorldRootException("Server world directory does not exist: " + worldRoot, worldRoot); + } + return worldRoot; + } + + /** + * A universe or world directory that is simply absent. Separated from every other IO failure so + * reconciliation can treat it as nothing-to-quarantine instead of refusing startup; an unsafe world name + * stays a plain IOException and still refuses. + */ + static final class MissingWorldRootException extends IOException { + private static final long serialVersionUID = 1L; + + private final Path path; + + MissingWorldRootException(String message, Path path) { + super(message); + this.path = path; + } + + Path path() { + return path; + } + } + + /** + * Best effort read of a dedicated-server launch option. The parsed OptionSet is not reachable from mod + * bootstrap, so read the process arguments; when they are unavailable we fall back to the vanilla defaults, + * which is what the unpatched code assumed unconditionally. + */ + static String commandLineOption(List arguments, String name) { + String flag = "--" + name; + for (int index = 0; index < arguments.size(); index++) { + String argument = arguments.get(index); + if (argument.equals(flag)) { + return index + 1 < arguments.size() ? arguments.get(index + 1) : null; + } + if (argument.startsWith(flag + "=")) { + return argument.substring(flag.length() + 1); + } + } + return null; + } + + private static List processArguments() { + try { + Optional arguments = ProcessHandle.current().info().arguments(); + if (arguments.isPresent() && arguments.get().length > 0) { + return List.of(arguments.get()); + } + } catch (RuntimeException unavailable) { + LOGGER.debug("Iris could not read the process arguments", unavailable); + } + // Whitespace split only: sun.java.command is a flattened string with no quoting information, so a + // --universe or --world value containing spaces cannot be recovered from it. Deliberately not parsed + // further - a half-correct quote parser would hand world resolution a wrong directory, and the missing + // world root path already degrades to the vanilla defaults instead of failing the boot. + String command = System.getProperty("sun.java.command"); + if (command == null || command.isBlank()) { + return List.of(); + } + return List.of(command.trim().split("\\s+")); + } + + private static Path quarantineVanillaDimensions(Path worldRoot) throws IOException { + Path recovery = markerFile().getParent().resolve("mainworld-recovery-" + UUID.randomUUID()); + List moved = new ArrayList<>(); + moveToRecovery(worldRoot, worldRoot.resolve("level.dat"), recovery, moved); + moveToRecovery(worldRoot, worldRoot.resolve("level.dat_old"), recovery, moved); + for (String folder : VANILLA_DIMENSION_FOLDERS) { + moveToRecovery(worldRoot, worldRoot.resolve(folder), recovery, moved); + } + if (moved.isEmpty()) { + Files.deleteIfExists(recovery); + } + return recovery; + } + + private static void moveToRecovery(Path worldRoot, Path source, Path recovery, + List moved) throws IOException { + if (!Files.exists(source)) { + return; + } + Path relative = worldRoot.relativize(source); + Path target = recovery.resolve(relative); + Files.createDirectories(target.getParent()); + try { + Files.move(source, target); + moved.add(relative); + } catch (IOException failure) { + for (int index = moved.size() - 1; index >= 0; index--) { + Path rollbackRelative = moved.get(index); + Path rollbackSource = recovery.resolve(rollbackRelative); + Path rollbackTarget = worldRoot.resolve(rollbackRelative); + try { + Files.createDirectories(rollbackTarget.getParent()); + Files.move(rollbackSource, rollbackTarget); + } catch (IOException rollbackFailure) { + failure.addSuppressed(rollbackFailure); + } + } + throw failure; + } + } + + private static String escape(String value) { + return value.replace(":", "\\:"); + } + + private static String unescape(String value) { + return value.replace("\\:", ":").replace("\\=", "="); + } + + private static String firstNonBlank(String value, String fallback) { + return value == null || value.isBlank() ? fallback : value; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiome.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiome.java new file mode 100644 index 000000000..18867b739 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiome.java @@ -0,0 +1,58 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.PlatformBiome; +import net.minecraft.world.level.biome.Biome; + +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedBiome implements PlatformBiome { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + + private final Biome biome; + private final String key; + private final String namespace; + + private ModdedBiome(Biome biome, String key) { + this.biome = biome; + this.key = key; + int colon = key.indexOf(':'); + this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft"; + } + + public static ModdedBiome of(Biome biome, String key) { + return CACHE.computeIfAbsent(key, (String k) -> new ModdedBiome(biome, k)); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public Object nativeHandle() { + return biome; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomeWriter.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomeWriter.java new file mode 100644 index 000000000..ed0914a2b --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomeWriter.java @@ -0,0 +1,233 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBiomeWriter; +import art.arcane.iris.util.project.context.IrisContext; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.biome.Biome; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +public final class ModdedBiomeWriter implements PlatformBiomeWriter { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final String VANILLA_FALLBACK_KEY = "minecraft:plains"; + private static final int MAX_CACHED_IDS = 4096; + /** NUL cannot occur in a pack or registry key, so the composite cache key stays unambiguous. */ + private static final char SCOPE_SEPARATOR = (char) 0; + + private final Supplier server; + private final AtomicBoolean serverMissingReported = new AtomicBoolean(); + private volatile RegistryCache cache; + + public ModdedBiomeWriter(Supplier server) { + this.server = server; + } + + @Override + public int biomeIdFor(String key) { + Registry registry = biomeRegistry(); + if (registry == null) { + reportMissingServer("resolve the biome id for '" + key + "'", "using biome id 0"); + return 0; + } + if (key == null) { + LOGGER.warn("Iris biome writer got a null biome key; falling back to {}", VANILLA_FALLBACK_KEY); + return fallbackId(registry); + } + + RegistryCache cached = cacheFor(registry); + String scoped = scopedBiomeKey(key); + // The scoped key depends on the calling engine, so the same pack key can resolve differently per + // dimension. Cache on both halves; the derivative path below reads the raw key. + String cacheKey = scoped.equals(key) ? key : key + SCOPE_SEPARATOR + scoped; + Integer hit = cached.ids.get(cacheKey); + if (hit != null) { + return hit; + } + + int resolved = resolve(registry, key, scoped); + if (cached.ids.size() < MAX_CACHED_IDS) { + cached.ids.put(cacheKey, resolved); + } + return resolved; + } + + private int resolve(Registry registry, String key, String scoped) { + int direct = idForKey(registry, scoped); + if (direct >= 0) { + return direct; + } + int derivative = idForDerivative(registry, key); + if (derivative >= 0) { + return derivative; + } + return fallbackId(registry); + } + + private String scopedBiomeKey(String key) { + IrisContext context = IrisContext.get(); + if (context == null || key == null) { + return key; + } + Engine engine = context.getEngine(); + String prefix = engine.getDimension().getLoadKey() + ":"; + if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) { + return key; + } + return ModdedWorldgenIds.biomeRef(engine, key.substring(prefix.length())); + } + + @Override + public List allBiomes() { + Registry registry = biomeRegistry(); + if (registry == null) { + reportMissingServer("enumerate the biome registry", "returning no biomes"); + return new ArrayList<>(); + } + + RegistryCache cached = cacheFor(registry); + List snapshot = cached.biomes; + if (snapshot == null) { + List built = new ArrayList<>(); + for (Identifier identifier : registry.keySet()) { + Biome biome = registry.getValue(identifier); + if (biome != null) { + built.add(ModdedBiome.of(biome, identifier.toString())); + } + } + snapshot = List.copyOf(built); + cached.biomes = snapshot; + } + return new ArrayList<>(snapshot); + } + + private int idForKey(Registry registry, String key) { + Identifier identifier = Identifier.tryParse(key); + if (identifier == null) { + return -1; + } + Biome biome = registry.getValue(identifier); + return biome == null ? -1 : registry.getId(biome); + } + + private int idForDerivative(Registry registry, String key) { + int colon = key.indexOf(':'); + if (colon <= 0 || colon >= key.length() - 1) { + return -1; + } + String dimensionLoadKey = key.substring(0, colon); + String customBiomeId = key.substring(colon + 1); + IrisBiome owner = findCustomBiomeOwner(dimensionLoadKey, customBiomeId); + if (owner == null) { + return -1; + } + String derivativeKey = owner.getVanillaDerivativeKey(); + if (derivativeKey == null) { + return -1; + } + return idForKey(registry, derivativeKey); + } + + private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) { + for (Engine engine : ModdedWorldEngines.activeEngines()) { + if (engine == null || engine.isClosed()) { + continue; + } + if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) { + continue; + } + for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) { + if (!biome.isCustom()) { + continue; + } + for (IrisBiomeCustom custom : biome.getCustomDerivitives()) { + if (customBiomeId.equals(custom.getId())) { + return biome; + } + } + } + } + return null; + } + + private int fallbackId(Registry registry) { + int plains = idForKey(registry, VANILLA_FALLBACK_KEY); + return plains >= 0 ? plains : 0; + } + + private Registry biomeRegistry() { + MinecraftServer instance = server.get(); + if (instance == null) { + return null; + } + // Read before write: this runs for every biome id on every generation thread, and an unconditional + // store on a shared cache line is a contended write on the hot path for a flag that is almost always + // already false. + if (serverMissingReported.get()) { + serverMissingReported.set(false); + } + return instance.registryAccess().lookupOrThrow(Registries.BIOME); + } + + /** + * The SPI requires biome writers to cache their registry lookups: biomeIdFor runs from generation threads + * for every biome a pack names, and the derivative path walks every active engine and every custom biome. + * The cache is keyed on the biome Registry instance, which the server replaces whenever datapacks reload, + * so a reload invalidates everything for free. + */ + private RegistryCache cacheFor(Registry registry) { + RegistryCache current = cache; + if (current != null && current.registry == registry) { + return current; + } + RegistryCache replacement = new RegistryCache(registry); + cache = replacement; + return replacement; + } + + private void reportMissingServer(String operation, String fallback) { + if (serverMissingReported.compareAndSet(false, true)) { + LOGGER.warn("Iris cannot {} before the Minecraft server is available; {}", operation, fallback); + } + } + + private static final class RegistryCache { + private final Registry registry; + private final ConcurrentHashMap ids = new ConcurrentHashMap<>(); + private volatile List biomes; + + private RegistryCache(Registry registry) { + this.registry = registry; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockBreakHandler.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockBreakHandler.java new file mode 100644 index 000000000..4d344d94a --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockBreakHandler.java @@ -0,0 +1,391 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBlockData; +import art.arcane.iris.engine.object.IrisBlockDrops; +import art.arcane.iris.engine.object.IrisLoot; +import art.arcane.iris.engine.object.IrisMarker; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.modded.service.ModdedTreeFellerService; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterMarker; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedBlockBreakHandler { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final ConcurrentHashMap PENDING = new ConcurrentHashMap<>(); + + private ModdedBlockBreakHandler() { + } + + public static void prepare( + ServerLevel level, + ServerPlayer player, + BlockPos position, + BlockState brokenState + ) { + if (ModdedTreeFellerService.isBreakProbe()) { + return; + } + if (engineFor(level) == null) { + return; + } + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + // finishPending is the only thing that evicts an unconsumed entry. With no scheduler there is no + // sweep, so an entry inserted here would leak for the rest of the server uptime. + LOGGER.debug("Iris skipped block-break provenance at {},{},{}: scheduler unavailable", + position.getX(), position.getY(), position.getZ()); + return; + } + BreakKey key = new BreakKey(level, position.asLong()); + ModdedTreeFellerService treeFeller = treeFellerService(); + ModdedTreeFellerService.PreparedOrigin preparedOrigin = treeFeller == null + ? null + : treeFeller.prepare(level, player, position, brokenState); + PendingBreak pending = new PendingBreak(brokenState, preparedOrigin); + PENDING.put(key, pending); + scheduler.laterGlobal(() -> finishPending(key, pending, position), 1); + } + + public static void clear() { + PENDING.clear(); + } + + public static void cancel(ServerLevel level, BlockPos position) { + PENDING.remove(new BreakKey(level, position.asLong())); + } + + public static Result complete(ServerLevel level, BlockPos position, BlockState fallbackState) { + BreakKey key = new BreakKey(level, position.asLong()); + PendingBreak pending = PENDING.remove(key); + PendingBreak resolved = pending == null ? new PendingBreak(fallbackState, null) : pending; + return evaluateSafely(level, position, resolved); + } + + public static Result completePrepared(ServerLevel level, BlockPos position) { + BreakKey key = new BreakKey(level, position.asLong()); + PendingBreak pending = PENDING.get(key); + if (pending == null || level.getBlockState(position).equals(pending.brokenState())) { + return null; + } + return PENDING.remove(key, pending) ? evaluateSafely(level, position, pending) : null; + } + + public static void spawn(ServerLevel level, BlockPos position, KList drops) { + for (ItemStack stack : drops) { + ItemEntity item = createDrop(level, position, stack); + if (item != null) { + level.addFreshEntity(item); + } + } + } + + public static ItemEntity createDrop(ServerLevel level, BlockPos position, ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return null; + } + RandomSource random = level.getRandom(); + double x = position.getX() + 0.5D + Mth.nextDouble(random, -0.25D, 0.25D); + double y = position.getY() + 0.5D + Mth.nextDouble(random, -0.25D, 0.25D) - EntityTypes.ITEM.getHeight() / 2D; + double z = position.getZ() + 0.5D + Mth.nextDouble(random, -0.25D, 0.25D); + ItemEntity item = new ItemEntity(level, x, y, z, stack); + item.setDefaultPickUpDelay(); + return item; + } + + private static void finishPending(BreakKey key, PendingBreak pending, BlockPos position) { + if (!PENDING.remove(key, pending)) { + return; + } + ServerLevel level = key.level(); + if (level.getBlockState(position).equals(pending.brokenState())) { + return; + } + Result result = evaluateSafely(level, position, pending); + if (!result.routeCombinedDrops(List.of())) { + spawn(level, position, result.drops()); + } + } + + private static Result evaluateSafely(ServerLevel level, BlockPos position, PendingBreak pending) { + try { + return evaluate(level, position, pending); + } catch (Throwable error) { + LOGGER.error("Iris block-break processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(), + level.dimension().identifier(), error); + return Result.empty(); + } + } + + private static Result evaluate(ServerLevel level, BlockPos position, PendingBreak pending) { + Engine engine = engineFor(level); + if (engine == null || engine.isClosed()) { + return Result.empty(); + } + + removeMarker(engine, position); + Result drops = evaluateDrops(level, position, pending.brokenState(), engine); + ModdedTreeFellerService treeFeller = treeFellerService(); + ModdedTreeFellerService.OriginDropRoute route = treeFeller == null || pending.preparedOrigin() == null + ? null + : treeFeller.completeOrigin(pending.preparedOrigin()); + if (route == null) { + clearTreeProvenance(engine, position); + } + return drops.withRoute(route); + } + + public static Result evaluateManagedDrops(ServerLevel level, BlockPos position, BlockState brokenState) { + Engine engine = engineFor(level); + if (engine == null || engine.isClosed()) { + return Result.empty(); + } + try { + return evaluateDrops(level, position, brokenState, engine); + } catch (Throwable error) { + LOGGER.error("Iris managed block-drop processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(), + level.dimension().identifier(), error); + return Result.empty(); + } + } + + public static void completeManagedBreak(ServerLevel level, BlockPos position) { + Engine engine = engineFor(level); + if (engine != null && !engine.isClosed()) { + removeMarker(engine, position); + clearTreeProvenance(engine, position); + } + } + + public static void clearPlacedProvenance(ServerLevel level, BlockPos position) { + completeManagedBreak(level, position); + } + + private static Result evaluateDrops( + ServerLevel level, + BlockPos position, + BlockState brokenState, + Engine engine + ) { + KList providers = providers(engine, position, brokenState); + if (providers.isEmpty()) { + return Result.empty(); + } + + KList drops = new KList<>(); + boolean replaceVanillaDrops = false; + for (IrisBlockDrops provider : providers) { + replaceVanillaDrops |= provider.isReplaceVanillaDrops(); + for (IrisLoot loot : provider.getDrops()) { + if (!LootResolver.oneIn(RNG.r, loot.getRarity())) { + continue; + } + ItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, level); + if (stack != null && !stack.isEmpty()) { + drops.add(stack); + } + } + } + return new Result(drops, replaceVanillaDrops, null); + } + + private static void removeMarker(Engine engine, BlockPos position) { + int mantleY = position.getY() - engine.getMinHeight(); + MatterMarker marker = engine.getMantle().getMantle().get(position.getX(), mantleY, position.getZ(), MatterMarker.class); + if (marker == null) { + return; + } + String tag = marker.getTag(); + if ("cave_floor".equals(tag) || "cave_ceiling".equals(tag)) { + return; + } + IrisMarker configured = engine.getData().getMarkerLoader().load(tag); + if (configured == null || configured.isRemoveOnChange()) { + engine.getMantle().getMantle().remove(position.getX(), mantleY, position.getZ(), MatterMarker.class); + } + } + + private static void clearTreeProvenance(Engine engine, BlockPos position) { + int mantleY = position.getY() - engine.getMinHeight(); + engine.getMantle().getMantle().remove(position.getX(), mantleY, position.getZ(), String.class); + engine.getMantle().getMantle().remove(position.getX(), mantleY, position.getZ(), TreeBlockMaterial.class); + } + + private static KList providers(Engine engine, BlockPos position, BlockState brokenState) { + KList providers = new KList<>(); + IrisData data = engine.getData(); + int relativeY = position.getY() - engine.getMinHeight(); + IrisBiome biome = engine.getBiome(position.getX(), relativeY, position.getZ()); + if (biome != null) { + addMatching(providers, biome.getBlockDrops(), brokenState, data); + } + if (skipsParents(providers)) { + return providers; + } + IrisRegion region = engine.getRegion(position.getX(), position.getZ()); + if (region != null) { + addMatching(providers, region.getBlockDrops(), brokenState, data); + } + addMatching(providers, engine.getDimension().getBlockDrops(), brokenState, data); + return providers; + } + + private static void addMatching(KList matches, KList candidates, BlockState brokenState, IrisData data) { + for (IrisBlockDrops candidate : candidates) { + if (matches(candidate, brokenState, data)) { + matches.add(candidate); + } + } + } + + private static boolean skipsParents(KList providers) { + for (IrisBlockDrops provider : providers) { + if (provider.isSkipParents()) { + return true; + } + } + return false; + } + + private static boolean matches(IrisBlockDrops provider, BlockState brokenState, IrisData data) { + for (IrisBlockData configuredBlock : provider.getBlocks()) { + PlatformBlockState resolved = configuredBlock.getBlockData(data); + if (resolved == null || !(resolved.nativeHandle() instanceof BlockState configuredState)) { + continue; + } + if (matchesState(configuredState, brokenState, provider.isExactBlocks())) { + return true; + } + } + return false; + } + + static boolean matchesState(BlockState configuredState, BlockState brokenState, boolean exact) { + return exact ? configuredState.equals(brokenState) : configuredState.getBlock() == brokenState.getBlock(); + } + + public static Engine engineFor(ServerLevel level) { + ChunkGenerator generator = level.getChunkSource().getGenerator(); + if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) { + return null; + } + Engine engine = irisGenerator.engineIfBound(); + if (engine != null) { + return engine; + } + try { + return irisGenerator.commandEngine(); + } catch (Throwable error) { + LOGGER.error("Iris could not resolve the engine for a block break in {}", level.dimension().identifier(), error); + return null; + } + } + + private static ModdedTreeFellerService treeFellerService() { + return ModdedEngineBootstrap.services().service(ModdedTreeFellerService.class); + } + + public static final class Result { + private final KList drops; + private final boolean replaceVanillaDrops; + private final ModdedTreeFellerService.OriginDropRoute route; + + private Result( + KList drops, + boolean replaceVanillaDrops, + ModdedTreeFellerService.OriginDropRoute route + ) { + this.drops = drops; + this.replaceVanillaDrops = replaceVanillaDrops; + this.route = route; + } + + public KList drops() { + return drops; + } + + public boolean replaceVanillaDrops() { + return replaceVanillaDrops; + } + + public boolean routeCombinedDrops(Iterable vanillaDrops) { + if (route == null) { + return false; + } + return route.route(combinedDrops(vanillaDrops)); + } + + public KList combinedDrops(Iterable vanillaDrops) { + KList combined = new KList<>(); + if (!replaceVanillaDrops) { + for (ItemStack stack : vanillaDrops) { + if (stack != null && !stack.isEmpty()) { + combined.add(stack.copy()); + } + } + } + for (ItemStack stack : drops) { + if (stack != null && !stack.isEmpty()) { + combined.add(stack.copy()); + } + } + return combined; + } + + private Result withRoute(ModdedTreeFellerService.OriginDropRoute route) { + return new Result(drops, replaceVanillaDrops, route); + } + + private static Result empty() { + return new Result(new KList<>(), false, null); + } + } + + private record BreakKey(ServerLevel level, long position) { + } + + private record PendingBreak( + BlockState brokenState, + ModdedTreeFellerService.PreparedOrigin preparedOrigin + ) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockBuffer.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockBuffer.java new file mode 100644 index 000000000..52d68111c --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockBuffer.java @@ -0,0 +1,98 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; + +public final class ModdedBlockBuffer implements Hunk { + private final PlatformBlockState[] data; + private final PlatformBlockState air; + private final int height; + + public ModdedBlockBuffer(int height, PlatformBlockState air) { + this.data = new PlatformBlockState[16 * height * 16]; + this.air = air; + this.height = height; + } + + private int index(int x, int y, int z) { + return (y * 16 + z) * 16 + x; + } + + public boolean isAir(int x, int y, int z) { + return data[index(x, y, z)] == null; + } + + /** + * Direct slot read, null when unset — lets writeBlocks pay one index + one array load per + * block instead of the isAir + getRaw pair. + */ + public PlatformBlockState rawOrNull(int x, int y, int z) { + return data[index(x, y, z)]; + } + + @Override + public int getWidth() { + return 16; + } + + @Override + public int getDepth() { + return 16; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public void set(int x, int y, int z, PlatformBlockState t) { + if (t == null) { + return; + } + if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= height || z >= 16) { + return; + } + data[index(x, y, z)] = t; + } + + @Override + public void setRaw(int x, int y, int z, PlatformBlockState t) { + if (t == null) { + return; + } + data[index(x, y, z)] = t; + } + + @Override + public PlatformBlockState getRaw(int x, int y, int z) { + PlatformBlockState state = data[index(x, y, z)]; + return state == null ? air : state; + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= height || z >= 16) { + return air; + } + return getRaw(x, y, z); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java new file mode 100644 index 000000000..77d73be35 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java @@ -0,0 +1,549 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.modded.api.ModdedCustomContentRegistry; +import art.arcane.iris.modded.api.ModdedBlockData; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.data.UnresolvedKeyLog; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.commands.arguments.blocks.BlockStateParser; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.EmptyBlockGetter; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LeavesBlock; +import net.minecraft.world.level.block.PointedDripstoneBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.SpeleothemThickness; +import net.minecraft.world.level.block.state.properties.Property; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public final class ModdedBlockResolution { + private static final Set FOLIAGE = blockSet( + "poppy", "dandelion", "cornflower", "sweet_berry_bush", "crimson_roots", "warped_roots", + "nether_sprouts", "allium", "azure_bluet", "blue_orchid", "oxeye_daisy", "lily_of_the_valley", + "wither_rose", "dark_oak_sapling", "acacia_sapling", "jungle_sapling", "birch_sapling", + "spruce_sapling", "oak_sapling", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip", + "fern", "large_fern", "short_grass", "tall_grass"); + private static final Set DECORANT = decorantSet(); + private static final Set LIT = blockSet( + "glowstone", "amethyst_cluster", "small_amethyst_bud", "medium_amethyst_bud", "large_amethyst_bud", + "end_rod", "soul_sand", "torch", "redstone_torch", "soul_torch", "redstone_wall_torch", "wall_torch", + "soul_wall_torch", "lantern", "candle", "jack_o_lantern", "redstone_lamp", "magma_block", "light", + "shroomlight", "sea_lantern", "soul_lantern", "fire", "soul_fire", "sea_pickle", "brewing_stand", + "redstone_ore"); + private static final Set STORAGE = blockSet( + "chest", "smoker", "trapped_chest", "shulker_box", "white_shulker_box", "orange_shulker_box", + "magenta_shulker_box", "light_blue_shulker_box", "yellow_shulker_box", "lime_shulker_box", + "pink_shulker_box", "gray_shulker_box", "light_gray_shulker_box", "cyan_shulker_box", + "purple_shulker_box", "blue_shulker_box", "brown_shulker_box", "green_shulker_box", + "red_shulker_box", "black_shulker_box", "barrel", "dispenser", "dropper", "hopper", "furnace", + "blast_furnace"); + private static final Set STORAGE_CHEST = storageChestSet(); + private static final Set DEEPSLATE = blockSet( + "deepslate", "deepslate_bricks", "deepslate_brick_slab", "deepslate_brick_stairs", + "deepslate_brick_wall", "deepslate_tile_slab", "deepslate_tiles", "deepslate_tile_stairs", + "deepslate_tile_wall", "cracked_deepslate_tiles", "deepslate_coal_ore", "deepslate_iron_ore", + "deepslate_copper_ore", "deepslate_diamond_ore", "deepslate_emerald_ore", "deepslate_gold_ore", + "deepslate_lapis_ore", "deepslate_redstone_ore"); + private static final Map NORMAL_TO_DEEPSLATE = oreMap(false); + private static final Map DEEPSLATE_TO_NORMAL = oreMap(true); + private static final Set FOLIAGE_PLANTABLE_STATE = blockSet( + "grass_block", "moss_block", "rooted_dirt", "dirt", "coarse_dirt", "podzol"); + private static final Set FOLIAGE_PLANTABLE_MATERIAL = blockSet( + "grass_block", "moss_block", "dirt", "tall_grass", "tall_seagrass", "large_fern", "sunflower", + "peony", "lilac", "rose_bush", "rooted_dirt", "coarse_dirt", "podzol"); + private static final Set PLACE_ONTO_LEAVES = blockSet( + "acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves"); + private static final BlockState AIR = Blocks.AIR.defaultBlockState(); + private static final UnresolvedKeyLog UNRESOLVED = new UnresolvedKeyLog("Iris modded block resolution", 30_000L); + private static final int REPORTED_FAILURE_KEYS_MAX = 256; + private static final Set REPORTED_FAILURE_KEYS = ConcurrentHashMap.newKeySet(); + + private ModdedBlockResolution() { + } + + record Parsed(BlockState state, Map, Comparable> properties, String deferredPlacementKey) { + } + + private static Set blockSet(String... ids) { + Set blocks = new HashSet<>(); + for (String id : ids) { + Identifier identifier = Identifier.parse("minecraft:" + id); + if (BuiltInRegistries.BLOCK.containsKey(identifier)) { + blocks.add(BuiltInRegistries.BLOCK.getValue(identifier)); + } + } + return blocks; + } + + private static Set decorantSet() { + Set blocks = blockSet( + "short_grass", "tall_grass", "fern", "large_fern", "cornflower", "sunflower", "chorus_flower", + "poppy", "dandelion", "oxeye_daisy", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip", + "lilac", "dead_bush", "sweet_berry_bush", "rose_bush", "wither_rose", "allium", "blue_orchid", + "lily_of_the_valley", "crimson_fungus", "warped_fungus", "red_mushroom", "brown_mushroom", + "crimson_roots", "azure_bluet", "cactus", "weeping_vines", "weeping_vines_plant", "warped_roots", + "nether_sprouts", "twisting_vines", "twisting_vines_plant", "sugar_cane", "wheat", "potatoes", + "carrots", "beetroots", "nether_wart", "sea_pickle", "seagrass", "tall_seagrass", + "acacia_button", "birch_button", "crimson_button", "dark_oak_button", "jungle_button", + "oak_button", "polished_blackstone_button", "spruce_button", "stone_button", "warped_button", + "torch", "soul_torch", "glow_lichen", "vine", "sculk_vein"); + blocks.addAll(FOLIAGE); + return blocks; + } + + private static Set storageChestSet() { + Set blocks = new HashSet<>(STORAGE); + blocks.remove(Blocks.SMOKER); + blocks.remove(Blocks.FURNACE); + blocks.remove(Blocks.BLAST_FURNACE); + return blocks; + } + + private static Map oreMap(boolean deepslateToNormal) { + String[][] pairs = { + {"coal_ore", "deepslate_coal_ore"}, + {"emerald_ore", "deepslate_emerald_ore"}, + {"diamond_ore", "deepslate_diamond_ore"}, + {"copper_ore", "deepslate_copper_ore"}, + {"gold_ore", "deepslate_gold_ore"}, + {"iron_ore", "deepslate_iron_ore"}, + {"lapis_ore", "deepslate_lapis_ore"}, + {"redstone_ore", "deepslate_redstone_ore"} + }; + Map map = new HashMap<>(); + for (String[] pair : pairs) { + Block normal = BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:" + pair[0])); + Block deep = BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:" + pair[1])); + if (deepslateToNormal) { + map.put(deep, normal); + } else { + map.put(normal, deep); + } + } + return map; + } + + private static void reportResolveFailure(String key, Throwable error) { + String failureKey = key == null ? "" : key; + if (!REPORTED_FAILURE_KEYS.add(failureKey)) { + return; + } + if (REPORTED_FAILURE_KEYS.size() > REPORTED_FAILURE_KEYS_MAX) { + REPORTED_FAILURE_KEYS.clear(); + } + IrisLogging.reportError("Iris block data '" + failureKey + "' failed to resolve", error); + } + + private static void warnUnresolved(String key, String message) { + if (UNRESOLVED.firstOccurrence(key)) { + IrisLogging.warn(message); + } + String summary = UNRESOLVED.pollSummary(); + if (summary != null) { + IrisLogging.warn(summary); + } + } + + public static BlockState airState() { + return AIR; + } + + public static ModdedBlockState getAir() { + return ModdedBlockState.of(AIR, null); + } + + public static ModdedBlockState get(String bdxf) { + Parsed parsed = resolveGet(bdxf); + return stateFrom(parsed); + } + + public static ModdedBlockState getNoCompat(String bdxf) { + Parsed parsed = resolveNoCompat(bdxf); + return stateFrom(parsed); + } + + public static ModdedBlockState getOrNull(String bdxf) { + return getOrNull(bdxf, false); + } + + public static ModdedBlockState getOrNull(String bdxf, boolean warn) { + Parsed parsed = resolveOrNull(bdxf, warn); + return parsed == null ? null : stateFrom(parsed); + } + + private static ModdedBlockState stateFrom(Parsed parsed) { + return parsed.deferredPlacementKey() == null + ? ModdedBlockState.of(parsed.state(), parsed.properties()) + : ModdedBlockState.deferred(parsed.state(), parsed.properties(), parsed.deferredPlacementKey()); + } + + static Parsed resolveGet(String bdxf) { + // Mirrors the Bukkit path: an unknown key warns (rate limited) and falls back to air, instead of + // resolving to air with no output at all. + return resolveNoCompat(bdxf); + } + + static Parsed resolveNoCompat(String bdxf) { + Parsed parsed = resolveOrNull(bdxf, true); + if (parsed != null) { + return parsed; + } + return new Parsed(AIR, null, null); + } + + /** + * Resolves a key, returning null when nothing claims it. Never substitutes air - {@link #resolveNoCompat(String)} + * owns the air fallback. + */ + static Parsed resolveOrNull(String bdxf, boolean warn) { + try { + String bd = bdxf.trim(); + + if (bd.startsWith("minecraft:cauldron[level=")) { + bd = bd.replaceAll("\\Q:cauldron[\\E", ":water_cauldron["); + } + + if (bd.equals("minecraft:grass_path")) { + return new Parsed(Blocks.DIRT_PATH.defaultBlockState(), null, null); + } + + Parsed bdx = parseBlockData(bd, warn); + + if (bdx == null) { + ModdedBlockData provided = ModdedCustomContentRegistry.resolveBlock(bd); + if (provided != null) { + return new Parsed(provided.state(), null, provided.deferredPlacement() ? bd : null); + } + if (warn) { + warnUnresolved(bd, "Unknown Block Data '" + bd + "'"); + } + return null; + } + + return bdx; + } catch (Throwable e) { + reportResolveFailure(bdxf, e); + if (warn) { + warnUnresolved(bdxf, "Unknown Block Data '" + bdxf + "'"); + } + } + + return null; + } + + public static ModdedBlockState strictParse(String key) { + Parsed parsed = parseStrict(key); + return stateFrom(parsed); + } + + private static Parsed parseStrict(String key) { + StringReader reader = new StringReader(key); + BlockStateParser.BlockResult result; + try { + result = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, reader, false); + } catch (CommandSyntaxException e) { + throw new IllegalArgumentException("Could not parse data: " + key, e); + } + if (reader.canRead()) { + throw new IllegalArgumentException("Could not parse remainder: " + reader.getRemaining()); + } + return new Parsed(result.blockState(), result.properties(), null); + } + + private static Parsed createBlockData(String s, boolean warn) { + try { + return parseStrict(s); + } catch (IllegalArgumentException e) { + if (s.contains("[")) { + String base = s.split("\\Q[\\E")[0]; + Parsed stripped = createBlockData(base, warn); + if (stripped != null && warn) { + // Dedup on the base block key, not the full state string. UnresolvedKeyLog interns every key it + // is handed into a set it never trims, and a rejected property is usually rejected for every + // value and every combination a pack uses - keying on the state string would intern one entry per + // distinct state (16 levels x 6 facings x ...) for a single authoring mistake. + warnUnresolved("props:" + base, + "Block '" + base + "' rejected state '" + propertySection(s) + "'; using its default state"); + } + return stripped; + } + } + + if (warn) { + warnUnresolved(s, "Can't find block data for " + s); + } + return null; + } + + private static String propertySection(String key) { + int open = key.indexOf('['); + if (open < 0) { + return ""; + } + int close = key.indexOf(']', open); + return close < 0 ? key.substring(open + 1) : key.substring(open + 1, close); + } + + private static Parsed materialBlockData(String ix) { + if (ix.contains("[") || ix.contains(":")) { + return null; + } + Identifier identifier = Identifier.tryParse("minecraft:" + ix.toLowerCase(Locale.ROOT)); + if (identifier == null || !BuiltInRegistries.BLOCK.containsKey(identifier)) { + return null; + } + return new Parsed(BuiltInRegistries.BLOCK.getValue(identifier).defaultBlockState(), null, null); + } + + private static Parsed parseBlockData(String ix, boolean warn) { + try { + Parsed bx = createBlockData(ix.toLowerCase(), warn); + + if (bx == null) { + bx = createBlockData("minecraft:" + ix.toLowerCase(), warn); + } + + if (bx == null) { + bx = materialBlockData(ix); + } + + if (bx == null) { + if (warn) { + warnUnresolved(ix, "Unknown Block Data: " + ix); + } + return null; + } + + if (bx.state().getBlock() instanceof LeavesBlock) { + BlockState mutated = bx.state().setValue(LeavesBlock.PERSISTENT, shouldPreventLeafDecay()); + bx = new Parsed(mutated, bx.properties(), bx.deferredPlacementKey()); + } + + return bx; + } catch (Throwable e) { + String block = ix.contains(":") ? ix.split(":")[1].toLowerCase() : ix.toLowerCase(); + String state = block.contains("[") ? block.split("\\Q[\\E")[1].split("\\Q]\\E")[0] : ""; + Map stateMap = new HashMap<>(); + if (!state.equals("")) { + Arrays.stream(state.split(",")).forEach((String s) -> stateMap.put(s.split("=")[0], s.split("=")[1])); + } + block = block.split("\\Q[\\E")[0]; + + switch (block) { + case "cauldron" -> block = "water_cauldron"; + case "grass_path" -> block = "dirt_path"; + case "concrete" -> block = "white_concrete"; + case "wool" -> block = "white_wool"; + case "beetroots" -> { + if (stateMap.containsKey("age")) { + String updated = stateMap.get("age"); + switch (updated) { + case "7" -> updated = "3"; + case "3", "4", "5" -> updated = "2"; + case "1", "2" -> updated = "1"; + } + stateMap.put("age", updated); + } + } + } + + Map newStates = new HashMap<>(); + for (String key : stateMap.keySet()) { + createBlockData(block + "[" + key + "=" + stateMap.get(key) + "]", false); + newStates.put(key, stateMap.get(key)); + } + + String joined = newStates.entrySet().stream() + .map((Map.Entry entry) -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining(",")); + if (!joined.equals("")) { + joined = "[" + joined + "]"; + } + String newBlock = block + joined; + IrisLogging.debug("Converting " + ix + " to " + newBlock); + + try { + return createBlockData(newBlock, false); + } catch (Throwable e1) { + IrisLogging.reportError(e1); + } + + return null; + } + } + + private static boolean shouldPreventLeafDecay() { + return IrisSettings.get().getGenerator().isPreventLeafDecay(); + } + + public static BlockState toDeepSlateOre(BlockState block, BlockState ore) { + Block key = ore.getBlock(); + + if (isDeepSlate(block)) { + Block mapped = NORMAL_TO_DEEPSLATE.get(key); + if (mapped != null) { + return mapped.defaultBlockState(); + } + } else { + Block mapped = DEEPSLATE_TO_NORMAL.get(key); + if (mapped != null) { + return mapped.defaultBlockState(); + } + } + + return ore; + } + + public static boolean isDeepSlate(BlockState state) { + return DEEPSLATE.contains(state.getBlock()); + } + + public static boolean isOre(BlockState state) { + return BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore"); + } + + public static boolean isAir(BlockState state) { + if (state == null) { + return true; + } + Block block = state.getBlock(); + return block == Blocks.AIR || block == Blocks.CAVE_AIR || block == Blocks.VOID_AIR; + } + + public static boolean isSolid(BlockState state) { + if (state == null) { + return false; + } + return isSolidMaterial(state.getBlock()); + } + + private static boolean isSolidMaterial(Block block) { + return block.defaultBlockState().blocksMotion(); + } + + public static boolean isOccluding(BlockState state) { + return state.getBlock().defaultBlockState().isRedstoneConductor(EmptyBlockGetter.INSTANCE, BlockPos.ZERO); + } + + public static boolean isFluid(BlockState state) { + Block block = state.getBlock(); + return block == Blocks.WATER || block == Blocks.LAVA; + } + + public static boolean isWater(BlockState state) { + return state.getBlock() == Blocks.WATER; + } + + public static boolean isWaterLogged(BlockState state) { + return state.hasProperty(BlockStateProperties.WATERLOGGED) && state.getValue(BlockStateProperties.WATERLOGGED); + } + + public static boolean isLit(BlockState state) { + return LIT.contains(state.getBlock()); + } + + public static boolean isUpdatable(BlockState state) { + return isStorage(state) + || (state.getBlock() instanceof PointedDripstoneBlock + && state.getValue(PointedDripstoneBlock.THICKNESS) == SpeleothemThickness.TIP); + } + + public static boolean isFoliage(BlockState state) { + return FOLIAGE.contains(state.getBlock()); + } + + public static boolean isFoliagePlantable(BlockState state) { + return FOLIAGE_PLANTABLE_STATE.contains(state.getBlock()); + } + + public static boolean isDecorant(BlockState state) { + return DECORANT.contains(state.getBlock()); + } + + public static boolean isStorage(BlockState state) { + return STORAGE.contains(state.getBlock()); + } + + public static boolean isStorageChest(BlockState state) { + return STORAGE_CHEST.contains(state.getBlock()); + } + + public static boolean isVineBlock(BlockState state) { + Block block = state.getBlock(); + return block == Blocks.VINE || block == Blocks.SCULK_VEIN || block == Blocks.GLOW_LICHEN; + } + + public static boolean hasTileEntity(BlockState state) { + return state.getBlock().defaultBlockState().hasBlockEntity(); + } + + public static boolean canPlaceOnto(Block mat, Block onto) { + if (mat == Blocks.CACTUS) { + return onto == Blocks.CACTUS || onto == Blocks.SAND || onto == Blocks.RED_SAND; + } + + if ((onto == Blocks.CRIMSON_NYLIUM || onto == Blocks.WARPED_NYLIUM) + && (mat == Blocks.CRIMSON_FUNGUS || mat == Blocks.CRIMSON_ROOTS + || mat == Blocks.WARPED_FUNGUS || mat == Blocks.WARPED_ROOTS)) { + return true; + } + + if (FOLIAGE.contains(mat)) { + if (!FOLIAGE_PLANTABLE_MATERIAL.contains(onto)) { + return false; + } + } + + if (onto == Blocks.AIR || onto == Blocks.CAVE_AIR || onto == Blocks.VOID_AIR) { + return false; + } + + if (onto == Blocks.GRASS_BLOCK && mat == Blocks.DEAD_BUSH) { + return false; + } + + if (onto == Blocks.DIRT_PATH) { + if (!isSolidMaterial(mat)) { + return false; + } + } + + if (PLACE_ONTO_LEAVES.contains(onto)) { + return isSolidMaterial(mat); + } + + return true; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockState.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockState.java new file mode 100644 index 000000000..f4be1800f --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockState.java @@ -0,0 +1,359 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.PlatformBlockState; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public final class ModdedBlockState implements PlatformBlockState { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + + private final BlockState state; + private final Map, Comparable> parsedProperties; + private final String key; + private final String namespace; + private final String deferredPlacementKey; + private volatile String materialKey; + private volatile Boolean air; + private volatile Boolean solid; + private volatile Boolean occluding; + private volatile Boolean fluid; + private volatile Boolean water; + private volatile Boolean foliage; + private volatile Boolean treeBlock; + private volatile Boolean decorant; + + private ModdedBlockState(BlockState state, Map, Comparable> parsedProperties, String key, String deferredPlacementKey) { + this.state = state; + this.parsedProperties = parsedProperties; + this.key = key; + this.namespace = parseNamespace(key); + this.deferredPlacementKey = deferredPlacementKey; + } + + public static ModdedBlockState of(BlockState state, Map, Comparable> parsedProperties) { + String key = serialize(state); + return CACHE.computeIfAbsent(key, (String k) -> new ModdedBlockState(state, parsedProperties, k, null)); + } + + public static ModdedBlockState deferred(BlockState state, Map, Comparable> parsedProperties, String placementKey) { + return new ModdedBlockState(state, parsedProperties, serialize(state), Objects.requireNonNull(placementKey)); + } + + public static String serialize(BlockState state) { + StringBuilder result = new StringBuilder(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString()); + if (!state.isSingletonState()) { + result.append('['); + result.append(state.getValues() + .map((Property.Value value) -> value.property().getName() + "=" + value.valueName()) + .collect(Collectors.joining(","))); + result.append(']'); + } + return result.toString(); + } + + public BlockState handle() { + return state; + } + + Map, Comparable> parsedProperties() { + return parsedProperties; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ModdedBlockState blockState)) { + return false; + } + return state.equals(blockState.state) && Objects.equals(deferredPlacementKey, blockState.deferredPlacementKey); + } + + @Override + public int hashCode() { + return Objects.hash(state, deferredPlacementKey); + } + + private static String parseNamespace(String key) { + String base = key; + int bracket = base.indexOf('['); + if (bracket >= 0) { + base = base.substring(0, bracket); + } + int colon = base.indexOf(':'); + return colon >= 0 ? base.substring(0, colon) : "minecraft"; + } + + private static String mergeProperty(String key, String name, String value) { + int bracket = key.indexOf('['); + if (bracket < 0) { + return key + "[" + name + "=" + value + "]"; + } + String base = key.substring(0, bracket); + String body = key.substring(bracket + 1, key.lastIndexOf(']')); + LinkedHashMap properties = new LinkedHashMap<>(); + for (String entry : body.split(",")) { + int equals = entry.indexOf('='); + if (equals < 0) { + continue; + } + properties.put(entry.substring(0, equals).trim(), entry.substring(equals + 1).trim()); + } + properties.put(name, value); + StringBuilder merged = new StringBuilder(base).append('['); + boolean first = true; + for (Map.Entry property : properties.entrySet()) { + if (!first) { + merged.append(','); + } + merged.append(property.getKey()).append('=').append(property.getValue()); + first = false; + } + return merged.append(']').toString(); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public String materialKey() { + String cached = materialKey; + if (cached == null) { + int bracket = key.indexOf('['); + cached = bracket < 0 ? key : key.substring(0, bracket); + materialKey = cached; + } + return cached; + } + + @Override + public boolean isAir() { + Boolean cached = air; + if (cached == null) { + cached = ModdedBlockResolution.isAir(state); + air = cached; + } + return cached; + } + + @Override + public boolean isSolid() { + Boolean cached = solid; + if (cached == null) { + cached = ModdedBlockResolution.isSolid(state); + solid = cached; + } + return cached; + } + + @Override + public boolean isOccluding() { + Boolean cached = occluding; + if (cached == null) { + cached = ModdedBlockResolution.isOccluding(state); + occluding = cached; + } + return cached; + } + + @Override + public boolean isCustom() { + return deferredPlacementKey != null; + } + + @Override + public String deferredPlacementKey() { + return deferredPlacementKey; + } + + @Override + public PlatformBlockState placementBaseState() { + return deferredPlacementKey == null ? this : of(state, parsedProperties); + } + + @Override + public boolean isFluid() { + Boolean cached = fluid; + if (cached == null) { + cached = ModdedBlockResolution.isFluid(state); + fluid = cached; + } + return cached; + } + + @Override + public boolean isWater() { + Boolean cached = water; + if (cached == null) { + cached = ModdedBlockResolution.isWater(state); + water = cached; + } + return cached; + } + + @Override + public boolean isWaterLogged() { + return ModdedBlockResolution.isWaterLogged(state); + } + + @Override + public boolean isLit() { + return ModdedBlockResolution.isLit(state); + } + + @Override + public boolean isUpdatable() { + return ModdedBlockResolution.isUpdatable(state); + } + + @Override + public boolean isFoliage() { + Boolean cached = foliage; + if (cached == null) { + cached = ModdedBlockResolution.isFoliage(state); + foliage = cached; + } + return cached; + } + + @Override + public boolean isTreeBlock() { + Boolean cached = treeBlock; + if (cached == null) { + cached = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + treeBlock = cached; + } + return cached; + } + + @Override + public boolean isFoliagePlantable() { + return ModdedBlockResolution.isFoliagePlantable(state); + } + + @Override + public boolean isDecorant() { + Boolean cached = decorant; + if (cached == null) { + cached = ModdedBlockResolution.isDecorant(state); + decorant = cached; + } + return cached; + } + + @Override + public boolean isStorage() { + return ModdedBlockResolution.isStorage(state); + } + + @Override + public boolean isStorageChest() { + return ModdedBlockResolution.isStorageChest(state); + } + + @Override + public boolean isOre() { + return ModdedBlockResolution.isOre(state); + } + + @Override + public boolean isDeepSlate() { + return ModdedBlockResolution.isDeepSlate(state); + } + + @Override + public boolean isVineBlock() { + return ModdedBlockResolution.isVineBlock(state); + } + + @Override + public boolean canPlaceOnto(PlatformBlockState onto) { + return ModdedBlockResolution.canPlaceOnto(state.getBlock(), ((BlockState) onto.nativeHandle()).getBlock()); + } + + @Override + public boolean matches(PlatformBlockState other) { + if (!(other instanceof ModdedBlockState blockState)) { + return false; + } + if (state.getBlock() != blockState.state.getBlock()) { + return false; + } + if (state.equals(blockState.state)) { + return true; + } + if (blockState.parsedProperties == null) { + return false; + } + BlockState merged = state; + for (Map.Entry, Comparable> entry : blockState.parsedProperties.entrySet()) { + merged = applyProperty(merged, entry.getKey(), entry.getValue()); + } + return merged.equals(state); + } + + @SuppressWarnings("unchecked") + private static > BlockState applyProperty(BlockState state, Property property, Comparable value) { + return state.setValue((Property) property, (T) value); + } + + @Override + public boolean hasTileEntity() { + return ModdedBlockResolution.hasTileEntity(state); + } + + @Override + public PlatformBlockState withProperty(String name, String value) { + String merged = mergeProperty(key, name, value); + ModdedBlockState resolved = ModdedBlockResolution.strictParse(merged); + return withHandle(resolved.handle(), resolved.parsedProperties()); + } + + ModdedBlockState withHandle(BlockState updated, Map, Comparable> properties) { + if (updated == state && properties == parsedProperties) { + return this; + } + return deferredPlacementKey == null + ? of(updated, properties) + : deferred(updated, properties, deferredPlacementKey); + } + + @Override + public Object nativeHandle() { + return state; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDeathLoot.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDeathLoot.java new file mode 100644 index 000000000..06ac3f694 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDeathLoot.java @@ -0,0 +1,178 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.InventorySlotType; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.vehicle.ContainerEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.chunk.ChunkGenerator; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedDeathLoot { + private static final String TAG_PREFIX = "iris_loot|"; + private static final Set WARNED_TABLES = ConcurrentHashMap.newKeySet(); + private static final Set WARNED_ENTITY_TYPES = ConcurrentHashMap.newKeySet(); + + private ModdedDeathLoot() { + } + + public static void bind(Engine engine, Entity entity, KList tableKeys, int spawnX, int spawnY, int spawnZ, RNG rng) { + if (engine == null || entity == null || tableKeys == null || tableKeys.isEmpty()) { + return; + } + LootBinding binding = new LootBinding(rng.getSeed(), spawnX, spawnY, spawnZ, new KList<>(tableKeys)); + if (entity instanceof Mob) { + entity.addTag(encode(binding)); + return; + } + if (entity instanceof ContainerEntity container && entity.level() instanceof ServerLevel level) { + fillContainer(engine, level, container, binding); + return; + } + + String type = String.valueOf(BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType())); + if (WARNED_ENTITY_TYPES.add(type)) { + IrisLogging.warn("Iris entity loot: entity type '" + type + "' does not expose a vanilla lootable path"); + } + } + + public static boolean replaceBaseLoot(LivingEntity entity) { + if (entity == null || !(entity.level() instanceof ServerLevel level)) { + return false; + } + String encoded = findTag(entity); + if (encoded == null) { + return false; + } + LootBinding binding = decode(encoded); + if (binding == null) { + return true; + } + Engine engine = engineFor(level); + if (engine == null) { + return true; + } + KList stacks = resolve(engine, level, binding); + emit(level, entity, stacks); + return true; + } + + static boolean hasBindingTag(Set tags) { + for (String tag : tags) { + if (tag.startsWith(TAG_PREFIX)) { + return true; + } + } + return false; + } + + private static void fillContainer(Engine engine, ServerLevel level, ContainerEntity container, LootBinding binding) { + container.setContainerLootTable(null); + container.clearItemStacks(); + KList items = resolve(engine, level, binding); + ModdedLootApplier.fillContainer(container, items, new RNG(binding.seed())); + } + + private static KList resolve(Engine engine, ServerLevel level, LootBinding binding) { + KList drops = new KList<>(); + for (String key : binding.tableKeys()) { + IrisLootTable table = engine.getData().getLootLoader().load(key); + if (table == null) { + if (WARNED_TABLES.add(key)) { + IrisLogging.warn("Iris death loot: unknown loot table '" + key + "'"); + } + continue; + } + drops.addAll(ModdedItemTranslator.loot(table, engine.getSeedManager().getLoot(), InventorySlotType.STORAGE, level, + binding.spawnX(), binding.spawnY(), binding.spawnZ())); + } + return drops; + } + + private static void emit(ServerLevel level, LivingEntity entity, KList drops) { + for (ItemStack stack : drops) { + if (stack == null || stack.isEmpty()) { + continue; + } + entity.spawnAtLocation(level, stack); + } + } + + private static String encode(LootBinding binding) { + return TAG_PREFIX + binding.seed() + "|" + binding.spawnX() + "|" + binding.spawnY() + "|" + + binding.spawnZ() + "|" + String.join(",", binding.tableKeys()); + } + + private static LootBinding decode(String encoded) { + String[] parts = encoded.substring(TAG_PREFIX.length()).split("\\|", 5); + if (parts.length < 5) { + IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'"); + return null; + } + try { + long seed = Long.parseLong(parts[0]); + int spawnX = Integer.parseInt(parts[1]); + int spawnY = Integer.parseInt(parts[2]); + int spawnZ = Integer.parseInt(parts[3]); + KList tableKeys = new KList<>(); + for (String key : parts[4].split(",")) { + String trimmed = key.trim(); + if (!trimmed.isEmpty()) { + tableKeys.add(trimmed); + } + } + return new LootBinding(seed, spawnX, spawnY, spawnZ, tableKeys); + } catch (NumberFormatException error) { + IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'"); + return null; + } + } + + private static String findTag(Entity entity) { + for (String tag : entity.entityTags()) { + if (tag.startsWith(TAG_PREFIX)) { + return tag; + } + } + return null; + } + + private static Engine engineFor(ServerLevel level) { + ChunkGenerator generator = level.getChunkSource().getGenerator(); + if (generator instanceof IrisModdedChunkGenerator irisGenerator) { + return irisGenerator.engineIfBound(); + } + return null; + } + + private record LootBinding(long seed, int spawnX, int spawnY, int spawnZ, KList tableKeys) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDecoratorHooks.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDecoratorHooks.java new file mode 100644 index 000000000..5a46711ab --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDecoratorHooks.java @@ -0,0 +1,117 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.decorator.DecoratorPlatformHooks; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.EmptyBlockGetter; +import net.minecraft.world.level.block.SupportType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.Property; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFixer, DecoratorPlatformHooks.SurfaceSturdiness { + private static final Direction[] CARTESIAN = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN}; + private static final String[] FACE_NAMES = {"north", "east", "south", "west", "up", "down"}; + + @Override + public PlatformBlockState fixFaces(PlatformBlockState state, Hunk hunk, int rX, int rZ, int x, int y, int z, EngineMantle mantle) { + ModdedBlockState fabric = (ModdedBlockState) state; + BlockState cloned = fabric.handle(); + Map allowed = faceProperties(cloned); + + for (Map.Entry entry : allowed.entrySet()) { + if (cloned.getValue(entry.getValue())) { + cloned = cloned.setValue(entry.getValue(), Boolean.FALSE); + } + } + + boolean found = false; + for (Direction f : CARTESIAN) { + int yy = y + f.getStepY(); + + PlatformBlockState rs = null; + if (mantle != null) { + rs = mantle.getMantle().get(x + f.getStepX(), yy, z + f.getStepZ(), PlatformBlockState.class); + } + BlockState r = rs == null ? (BlockState) EngineMantle.AIR.nativeHandle() : (BlockState) rs.nativeHandle(); + if (isFaceSturdy(r, f.getOpposite())) { + BooleanProperty property = allowed.get(f.getSerializedName()); + if (property != null) { + found = true; + cloned = cloned.setValue(property, Boolean.TRUE); + } + continue; + } + + int xx = rX + f.getStepX(); + int zz = rZ + f.getStepZ(); + if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) { + continue; + } + + r = (BlockState) hunk.get(xx, yy, zz).nativeHandle(); + if (isFaceSturdy(r, f.getOpposite())) { + BooleanProperty property = allowed.get(f.getSerializedName()); + if (property != null) { + found = true; + cloned = cloned.setValue(property, Boolean.TRUE); + } + } + } + + if (!found) { + String fallback = allowed.containsKey("down") ? "down" : "up"; + BooleanProperty property = allowed.get(fallback); + if (property != null) { + cloned = cloned.setValue(property, Boolean.TRUE); + } + } + + return fabric.withHandle(cloned, fabric.parsedProperties()); + } + + @Override + public boolean canGoOn(PlatformBlockState surface) { + return isFaceSturdy((BlockState) surface.nativeHandle(), Direction.UP); + } + + private static boolean isFaceSturdy(BlockState state, Direction face) { + return state.isFaceSturdy(EmptyBlockGetter.INSTANCE, BlockPos.ZERO, face, SupportType.FULL); + } + + private static Map faceProperties(BlockState state) { + Map properties = new LinkedHashMap<>(); + for (String name : FACE_NAMES) { + for (Property property : state.getProperties()) { + if (property.getName().equals(name) && property instanceof BooleanProperty bool) { + properties.put(name, bool); + } + } + } + return properties; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java new file mode 100644 index 000000000..322084ea6 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java @@ -0,0 +1,449 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDimension; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.entity.Relative; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.biome.FixedBiomeSource; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.storage.DerivedLevelData; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.level.storage.WorldData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +public final class ModdedDimensionManager { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Object LOCK = new Object(); + private static final ConcurrentHashMap HANDLES = new ConcurrentHashMap<>(); + private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT, + TicketType.FLAG_LOADING | TicketType.FLAG_KEEP_DIMENSION_ACTIVE); + private static final long TELEPORT_WARM_TIMEOUT_SECONDS = 30L; + private static volatile ModdedServerAccess access; + + private ModdedDimensionManager() { + } + + public static synchronized ModdedServerAccess bindAccess(ModdedServerAccess serverAccess) { + ModdedServerAccess previous = access; + access = serverAccess; + return previous; + } + + public static synchronized void restoreAccess(ModdedServerAccess serverAccess) { + access = serverAccess; + } + + public static void clear() { + HANDLES.clear(); + } + + public static Handle handle(String dimensionId) { + return HANDLES.get(dimensionId); + } + + public static List handles() { + return new ArrayList<>(HANDLES.values()); + } + + public static ServerLevel level(MinecraftServer server, String dimensionId) { + Handle handle = HANDLES.get(dimensionId); + if (handle != null && handle.level().getServer() == server) { + return handle.level(); + } + ResourceKey key = levelKey(dimensionId); + // Server thread only (create/remove hold LOCK, teleport and the primary-world router tick, command + // handlers). Off-thread callers must use ModdedServerLevels.level instead of the live map. + for (ServerLevel level : server.getAllLevels()) { + if (level.dimension().equals(key)) { + return level; + } + } + return null; + } + + public static Engine engine(MinecraftServer server, String dimensionId) { + ServerLevel level = level(server, dimensionId); + if (level == null) { + return null; + } + if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) { + return null; + } + return generator.commandEngine(); + } + + public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) { + ModdedStartup.requirePackForWorldCreation(pack); + ModdedServerAccess serverAccess = requireAccess(); + synchronized (LOCK) { + ResourceKey key = levelKey(dimensionId); + Handle existing = HANDLES.get(dimensionId); + if (existing != null && serverAccess.hasLevel(server, key)) { + existing.generator().repointAndBind(existing.level(), pack, packDimensionKey, seed); + Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator()); + HANDLES.put(dimensionId, refreshed); + return refreshed; + } + if (serverAccess.hasLevel(server, key)) { + ServerLevel present = level(server, dimensionId); + if (present == null || !(present.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) { + throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded"); + } + LOGGER.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId); + generator.repointAndBind(present, pack, packDimensionKey, seed); + Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator); + HANDLES.put(dimensionId, handle); + return handle; + } + + try { + Handle handle = inject(server, serverAccess, dimensionId, key, pack, packDimensionKey, seed); + HANDLES.put(dimensionId, handle); + LOGGER.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed); + return handle; + } catch (Throwable e) { + LOGGER.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed, e); + throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e); + } + } + } + + public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) { + ModdedDimensionRegistryStore.PersistentDimension previous = + ModdedDimensionRegistryStore.get(server, dimensionId); + ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed)); + try { + return create(server, dimensionId, pack, packDimensionKey, seed); + } catch (Throwable e) { + try { + if (previous == null) { + ModdedDimensionRegistryStore.remove(server, dimensionId); + } else { + ModdedDimensionRegistryStore.put(server, previous); + } + } catch (Throwable rollbackFailure) { + e.addSuppressed(rollbackFailure); + } + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (e instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e); + } + } + + public static boolean removePersistent(MinecraftServer server, String dimensionId, boolean wipeStorage) { + ModdedDimensionRegistryStore.PersistentDimension previous = + ModdedDimensionRegistryStore.get(server, dimensionId); + ModdedDimensionRegistryStore.remove(server, dimensionId); + try { + return remove(server, dimensionId, wipeStorage); + } catch (Throwable e) { + if (previous != null) { + try { + ModdedDimensionRegistryStore.put(server, previous); + } catch (Throwable rollbackFailure) { + e.addSuppressed(rollbackFailure); + } + } + throw e; + } + } + + public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage) { + ModdedServerAccess serverAccess = requireAccess(); + synchronized (LOCK) { + ResourceKey key = levelKey(dimensionId); + ServerLevel level = level(server, dimensionId); + if (level == null) { + HANDLES.remove(dimensionId); + if (wipeStorage) { + ModdedDimensionStorage.wipe(server, key); + } + return false; + } + IrisModdedChunkGenerator generator = level.getChunkSource().getGenerator() + instanceof IrisModdedChunkGenerator irisGenerator + ? irisGenerator + : null; + boolean generatorUnbound = false; + try { + evacuate(server, level); + if (generator != null) { + generator.unbindEngine(level); + generatorUnbound = true; + } + ModdedWorldEngines.evictOrThrow(level); + level.save(null, true, false); + serverAccess.removeLevel(server, key); + // Undo snapshots pin the ServerLevel and could replay into the dead level. + art.arcane.iris.modded.command.ModdedObjectUndo.forget(level); + level.close(); + HANDLES.remove(dimensionId); + if (wipeStorage) { + ModdedDimensionStorage.wipe(server, key); + } + LOGGER.info("Iris removed runtime dimension '{}'", dimensionId); + return true; + } catch (Throwable e) { + rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e); + LOGGER.error("Iris failed to remove runtime dimension '{}'", dimensionId, e); + throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e); + } + } + } + + private static void rollbackRemoval(MinecraftServer server, ModdedServerAccess serverAccess, + ResourceKey key, ServerLevel level, + IrisModdedChunkGenerator generator, boolean generatorUnbound, + Throwable failure) { + try { + if (!generatorUnbound || generator == null || !serverAccess.hasLevel(server, key)) { + return; + } + generator.bindLevel(level); + } catch (Throwable rollbackFailure) { + if (rollbackFailure != failure) { + failure.addSuppressed(rollbackFailure); + } + LOGGER.error("Iris failed to restore the engine for retained runtime dimension '{}'", + key.identifier(), rollbackFailure); + } + } + + public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) { + ServerLevel level = level(server, dimensionId); + if (level == null) { + return false; + } + int blockX = (int) Math.floor(x); + int blockZ = (int) Math.floor(z); + ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4); + if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) { + completeTeleport(player, level, x, y, z, blockX, blockZ); + return true; + } + UUID playerId = player.getUUID(); + CompletableFuture + .supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1), server) + .thenCompose((CompletableFuture inner) -> inner) + // The ticket has no timeout of its own: bound the wait so the release below always runs. + .orTimeout(TELEPORT_WARM_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .whenComplete((Object result, Throwable error) -> server.execute(() -> { + level.getChunkSource().removeTicketWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1); + if (error != null) { + LOGGER.warn("Iris chunk warm for teleport into '{}' at {},{} failed: {}", dimensionId, chunkPos.x(), chunkPos.z(), error.toString()); + } + ServerPlayer target = server.getPlayerList().getPlayer(playerId); + if (target == null) { + return; + } + completeTeleport(target, level, x, y, z, blockX, blockZ); + })); + return true; + } + + private static void completeTeleport(ServerPlayer player, ServerLevel level, double x, double y, double z, int blockX, int blockZ) { + double targetY = y; + if (y == Double.MIN_VALUE) { + targetY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ); + } + player.teleportTo(level, x, targetY, z, Set.of(), player.getYRot(), player.getXRot(), false); + } + + private static Holder resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) { + Registry registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE); + IrisDimension dimension = loadPackDimension(pack, packDimensionKey); + String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey); + ResourceKey typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef)); + ModdedRuntimeRegistry.ensureCustomBiomes(registryAccess, dimension, pack); + ModdedRuntimeRegistry.ensureDimensionType(registry, typeKey, typeRef); + return ModdedForcedDatapack.requireRegisteredDimensionType( + typeRef, registry.get(typeKey), pack, packDimensionKey); + } + + private static IrisDimension loadPackDimension(String pack, String packDimensionKey) { + try { + File packFolder = ModdedWorldEngines.packFolder(pack); + if (!packFolder.isDirectory()) { + throw new IllegalStateException("Iris pack directory is missing: " + packFolder.getAbsolutePath()); + } + IrisDimension dimension = IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey); + if (dimension == null) { + throw new IllegalStateException("Iris pack '" + pack + + "' does not contain dimension '" + packDimensionKey + "'"); + } + return dimension; + } catch (Throwable e) { + LOGGER.error("Iris could not load pack '{}' dimension '{}' for dimension type resolution", + pack, packDimensionKey, e); + if (e instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Iris could not load pack '" + pack + "' dimension '" + + packDimensionKey + "' for dimension type resolution", e); + } + } + + private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey key, String pack, String packDimensionKey, long seed) { + RegistryAccess registryAccess = server.registryAccess(); + Holder dimensionType = resolveDimensionType(registryAccess, pack, packDimensionKey); + Holder plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS); + FixedBiomeSource biomeSource = new FixedBiomeSource(plains); + String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey; + IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef); + generator.repoint(pack, packDimensionKey, seed); + LevelStem stem = new LevelStem(dimensionType, generator); + + WorldData worldData = server.getWorldData(); + ServerLevelData overworldData = worldData.overworldData(); + DerivedLevelData derivedLevelData = new DerivedLevelData(worldData, overworldData); + + Executor executor = serverAccess.levelExecutor(server); + LevelStorageSource.LevelStorageAccess storage = serverAccess.levelStorage(server); + long obfuscatedSeed = BiomeManager.obfuscateSeed(seed); + + ServerLevel level = new ServerLevel( + server, + executor, + storage, + derivedLevelData, + key, + stem, + false, + obfuscatedSeed, + List.of(), + false); + + try { + generator.bindLevel(level); + Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator); + ServerLevel previous = serverAccess.putLevelIfAbsent(server, key, level); + if (previous != null) { + throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + + "': the level was registered concurrently"); + } + server.getPlayerList().addWorldborderListener(level); + return handle; + } catch (Throwable error) { + rollbackInjection(server, serverAccess, key, level, generator, error); + if (error instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (error instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, error); + } + } + + private static void rollbackInjection(MinecraftServer server, ModdedServerAccess serverAccess, + ResourceKey key, ServerLevel level, + IrisModdedChunkGenerator generator, Throwable failure) { + try { + if (serverAccess.hasLevel(server, key)) { + ServerLevel removed = serverAccess.removeLevel(server, key); + if (removed != null && removed != level) { + serverAccess.putLevel(server, key, removed); + throw new IllegalStateException("Iris injection rollback encountered another registered level for " + + key.identifier()); + } + } + } catch (Throwable cleanupError) { + failure.addSuppressed(cleanupError); + LOGGER.error("Iris failed to remove a partially injected level for {}", key.identifier(), cleanupError); + } + try { + generator.unbindEngine(level); + } catch (Throwable cleanupError) { + failure.addSuppressed(cleanupError); + LOGGER.error("Iris failed to close a partially bound engine for {}", key.identifier(), cleanupError); + } + try { + level.close(); + } catch (Throwable cleanupError) { + failure.addSuppressed(cleanupError); + LOGGER.error("Iris failed to close a partially injected level for {}", key.identifier(), cleanupError); + } + } + + public static int evacuate(MinecraftServer server, ServerLevel from) { + ServerLevel fallback = server.overworld(); + if (fallback == from) { + return 0; + } + BlockPos spawn = fallback.getRespawnData().pos(); + int spawnY = fallback.getHeight(Heightmap.Types.MOTION_BLOCKING, spawn.getX(), spawn.getZ()); + List players = new ArrayList<>(from.players()); + for (ServerPlayer player : players) { + player.teleportTo(fallback, spawn.getX() + 0.5D, spawnY, spawn.getZ() + 0.5D, Set.of(), player.getYRot(), player.getXRot(), false); + } + return players.size(); + } + + private static ResourceKey levelKey(String dimensionId) { + Identifier identifier = Identifier.parse(dimensionId); + return ResourceKey.create(Registries.DIMENSION, identifier); + } + + private static ModdedServerAccess requireAccess() { + ModdedServerAccess bound = access; + if (bound == null) { + throw new IllegalStateException("Iris modded server access is not bound; the loader bootstrap must bind ModdedServerAccess before runtime dimension injection"); + } + return bound; + } + + public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java new file mode 100644 index 000000000..3f6090d80 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java @@ -0,0 +1,98 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.loader.IrisData; +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.IrisRegion; +import net.minecraft.resources.Identifier; + +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; + +final class ModdedDimensionMetadata { + private ModdedDimensionMetadata() { + } + + static DimensionMetadata dimensionMetadata(IrisDimension dimension) { + int minY = dimension.getMinHeight(); + int maxY = dimension.getMaxHeight(); + if (maxY <= minY) { + throw new IllegalStateException("Iris dimension '" + dimension.getLoadKey() + + "' has invalid height range " + minY + ".." + maxY); + } + return new DimensionMetadata(minY, maxY, minY + dimension.getFluidHeight()); + } + + static Set collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) { + LinkedHashSet keys = new LinkedHashSet<>( + collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey())); + for (IrisRegion region : dimension.getAllRegions(() -> data)) { + if (region == null) { + continue; + } + if (!region.getSeaBiomes().isEmpty()) { + keys.add("minecraft:the_void"); + } + if (!region.getShoreBiomes().isEmpty()) { + keys.add("minecraft:beach"); + } + } + return Set.copyOf(keys); + } + + static Set collectConfiguredBiomeKeys(Iterable biomes, String dimensionLoadKey) { + LinkedHashSet keys = new LinkedHashSet<>(); + String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT); + for (IrisBiome irisBiome : biomes) { + if (irisBiome == null) { + continue; + } + Identifier derivative = Identifier.tryParse(irisBiome.getStructureDerivativeKey()); + if (derivative != null) { + keys.add(derivative.toString().toLowerCase(Locale.ROOT)); + } + if (!irisBiome.isCustom()) { + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT)); + } + } + return Set.copyOf(keys); + } + + static int clampSpawnHeight(int minY, int height) { + int minimum = minY + 1; + int maximum = minY + height - 2; + return Math.max(minimum, Math.min(maximum, 96)); + } + + record DimensionMetadata(int minY, int maxY, int seaLevel) { + int depth() { + return maxY - minY; + } + } + + record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRegistryStore.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRegistryStore.java new file mode 100644 index 000000000..91c2cd579 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRegistryStore.java @@ -0,0 +1,246 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class ModdedDimensionRegistryStore { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final String FILE_NAME = "iris-dimensions.json"; + private static final Pattern ID_FIELD = Pattern.compile("\"id\"\\s*:\\s*\"([^\"]+)\""); + + private ModdedDimensionRegistryStore() { + } + + public static List load(MinecraftServer server) { + return load(storeFile(server)); + } + + static List load(Path file) { + return contents(file).dimensions(); + } + + /** + * Boot-safe load: a corrupt registry must not abort server start. The broken file is renamed aside so the + * next write starts clean, and every id we can still recognise in the raw text is reported as lost. + */ + public static List loadForStartup(MinecraftServer server) { + return loadForStartup(storeFile(server)); + } + + static List loadForStartup(Path file) { + try { + return load(file); + } catch (RuntimeException corrupt) { + LOGGER.error("Iris persistent dimension registry at {} is corrupt; quarantining it and continuing boot", + file, corrupt); + List lostIds = salvageIds(file); + if (lostIds.isEmpty()) { + LOGGER.error("Iris could not recover any dimension ids from the corrupt registry; re-create the worlds with /iris world create"); + } else { + LOGGER.error("Iris lost {} persistent dimension(s) from the corrupt registry: {}", + lostIds.size(), String.join(", ", lostIds)); + } + quarantine(file); + return List.of(); + } + } + + private static List salvageIds(Path file) { + List ids = new ArrayList<>(); + try { + Matcher matcher = ID_FIELD.matcher(Files.readString(file, StandardCharsets.UTF_8)); + while (matcher.find()) { + String id = matcher.group(1); + if (!ids.contains(id)) { + ids.add(id); + } + } + } catch (IOException | RuntimeException unreadable) { + LOGGER.warn("Iris could not scan the corrupt persistent dimension registry at {} for lost ids", file, unreadable); + } + return ids; + } + + private static void quarantine(Path file) { + Path broken = file.resolveSibling(FILE_NAME + ".broken-" + System.currentTimeMillis()); + try { + Files.move(file, broken, StandardCopyOption.REPLACE_EXISTING); + LOGGER.error("Iris moved the corrupt persistent dimension registry to {}", broken); + } catch (IOException failure) { + LOGGER.error("Iris could not quarantine the corrupt persistent dimension registry at {}; delete it by hand", + file, failure); + } + } + + private static Contents contents(Path file) { + if (!Files.isRegularFile(file)) { + return new Contents(new ArrayList<>(), new ArrayList<>()); + } + try { + JSONObject root = new JSONObject(Files.readString(file, StandardCharsets.UTF_8)); + JSONArray entries = root.optJSONArray("dimensions"); + if (entries == null) { + throw new IllegalArgumentException("registry root has no dimensions array"); + } + Map deduplicated = new LinkedHashMap<>(); + List unparsed = new ArrayList<>(); + for (int index = 0; index < entries.length(); index++) { + Object raw = entries.opt(index); + try { + JSONObject entry = entries.getJSONObject(index); + String id = required(entry, "id", index, file); + String pack = required(entry, "pack", index, file); + String dimension = required(entry, "dimension", index, file); + if (!entry.has("seed")) { + throw new IllegalArgumentException("missing seed"); + } + PersistentDimension previous = deduplicated.putIfAbsent( + id, new PersistentDimension(id, pack, dimension, entry.getLong("seed"))); + if (previous != null) { + LOGGER.warn("Iris persistent dimension registry entry {} in {} duplicates id '{}'; keeping the first", + index, file, id); + } + } catch (RuntimeException invalidEntry) { + if (raw != null) { + unparsed.add(raw); + } + LOGGER.warn("Iris persistent dimension registry entry {} in {} is invalid ({}); kept verbatim: {}", + index, file, invalidEntry.getMessage(), raw); + } + } + return new Contents(new ArrayList<>(deduplicated.values()), unparsed); + } catch (RuntimeException | IOException e) { + throw new IllegalStateException("Iris persistent dimension registry at " + file + + " could not be read; refusing to discard persistent worlds", e); + } + } + + public static PersistentDimension get(MinecraftServer server, String id) { + return index(load(server)).get(id); + } + + public static synchronized void put(MinecraftServer server, PersistentDimension dimension) { + Path file = storeFile(server); + Contents contents = contents(file); + Map current = index(contents.dimensions()); + current.put(dimension.id(), dimension); + write(file, new ArrayList<>(current.values()), contents.unparsed()); + } + + public static synchronized void remove(MinecraftServer server, String id) { + Path file = storeFile(server); + Contents contents = contents(file); + Map current = index(contents.dimensions()); + if (current.remove(id) != null) { + write(file, new ArrayList<>(current.values()), contents.unparsed()); + } + } + + private static Map index(List dimensions) { + Map map = new LinkedHashMap<>(); + for (PersistentDimension dimension : dimensions) { + map.put(dimension.id(), dimension); + } + return map; + } + + private static String required(JSONObject entry, String key, int index, Path file) { + String value = entry.optString(key, null); + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("entry " + index + " in " + file + " has no " + key); + } + return value; + } + + static void write(Path file, List dimensions) { + write(file, dimensions, List.of()); + } + + private static void write(Path file, List dimensions, List unparsed) { + JSONArray entries = new JSONArray(); + for (PersistentDimension dimension : dimensions) { + JSONObject entry = new JSONObject(); + entry.put("id", dimension.id()); + entry.put("pack", dimension.pack()); + entry.put("dimension", dimension.dimension()); + entry.put("seed", dimension.seed()); + entries.put(entry); + } + for (Object entry : unparsed) { + entries.put(entry); + } + JSONObject root = new JSONObject(); + root.put("dimensions", entries); + Path temp = file.resolveSibling(FILE_NAME + ".tmp"); + try { + Files.createDirectories(file.getParent()); + Files.writeString(temp, root.toString(2), StandardCharsets.UTF_8); + try (FileChannel channel = FileChannel.open(temp, StandardOpenOption.WRITE)) { + channel.force(true); + } + moveReplacing(temp, file); + } catch (IOException e) { + try { + Files.deleteIfExists(temp); + } catch (IOException cleanupFailure) { + e.addSuppressed(cleanupFailure); + } + throw new IllegalStateException("Iris failed to write persistent dimension registry at " + file, e); + } + } + + private static void moveReplacing(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } + + private static Path storeFile(MinecraftServer server) { + return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME); + } + + public record PersistentDimension(String id, String pack, String dimension, long seed) { + } + + private record Contents(List dimensions, List unparsed) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionStorage.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionStorage.java new file mode 100644 index 000000000..58f4d28bc --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionStorage.java @@ -0,0 +1,72 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.LevelResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +public final class ModdedDimensionStorage { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final List CHUNK_DATA_FOLDERS = List.of("region", "entities", "poi", "mantle"); + + private ModdedDimensionStorage() { + } + + public static File storageFolder(MinecraftServer server, ResourceKey dimension) { + return DimensionType.getStorageFolder(dimension, server.getWorldPath(LevelResource.ROOT)).toFile(); + } + + public static void wipe(MinecraftServer server, ResourceKey dimension) { + File storageFolder = storageFolder(server, dimension); + try { + for (String folder : CHUNK_DATA_FOLDERS) { + deleteRecursively(new File(storageFolder, folder).toPath()); + } + } catch (IOException e) { + throw new IllegalStateException( + "Iris failed to completely wipe dimension storage at " + + storageFolder.getAbsolutePath(), e); + } + LOGGER.info("Iris wiped dimension storage at {}", storageFolder.getAbsolutePath()); + } + + private static void deleteRecursively(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + try (Stream walk = Files.walk(root)) { + for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) { + Files.deleteIfExists(path); + } + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBinding.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBinding.java new file mode 100644 index 000000000..845e03f46 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBinding.java @@ -0,0 +1,77 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +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; + +final class ModdedEngineBinding { + private final long timeout; + private final TimeUnit timeoutUnit; + private volatile CompletableFuture future = new CompletableFuture<>(); + + ModdedEngineBinding(long timeout, TimeUnit timeoutUnit) { + this.timeout = timeout; + this.timeoutUnit = timeoutUnit; + } + + T await(String dimensionKey) { + try { + return future.get(timeout, timeoutUnit); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting for Iris generator '" + + dimensionKey + "' to bind", error); + } catch (ExecutionException error) { + throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind", + error.getCause()); + } catch (TimeoutException error) { + throw new IllegalStateException("Timed out waiting for Iris generator '" + + dimensionKey + "' to bind", error); + } + } + + void complete(T value) { + future.complete(value); + } + + void fail(Throwable error) { + future.completeExceptionally(error); + } + + void throwIfFailed(String dimensionKey) { + CompletableFuture current = future; + if (!current.isCompletedExceptionally()) { + return; + } + try { + current.join(); + } catch (CompletionException error) { + throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind", + error.getCause()); + } + } + + void reset() { + future = new CompletableFuture<>(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java new file mode 100644 index 000000000..edf174067 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java @@ -0,0 +1,485 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.gui.GuiHost; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.engine.decorator.DecoratorPlatformHooks; +import art.arcane.iris.engine.framework.Engine; +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.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.object.BlockDataMergeSupport; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.modded.api.ModdedCustomContentRegistry; +import art.arcane.iris.modded.command.IrisModdedCommands; +import art.arcane.iris.modded.command.ModdedGuiHost; +import art.arcane.iris.modded.command.ModdedObjectUndo; +import art.arcane.iris.modded.command.ModdedPregenBossBar; +import art.arcane.iris.modded.command.ModdedPregenJob; +import art.arcane.iris.modded.command.ModdedStudioCommands; +import art.arcane.iris.modded.command.ModdedWandService; +import art.arcane.iris.modded.service.ModdedChunkUpdateService; +import art.arcane.iris.modded.service.ModdedEngineMaintenanceService; +import art.arcane.iris.modded.service.ModdedEntitySpawnService; +import art.arcane.iris.modded.service.ModdedLogFilterService; +import art.arcane.iris.modded.service.ModdedPreservationService; +import art.arcane.iris.modded.service.ModdedSettingsHotloadService; +import art.arcane.iris.modded.service.ModdedStudioHotloadService; +import art.arcane.iris.modded.service.ModdedTreeFellerService; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.parallel.MultiBurst; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.storage.LevelData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayDeque; + +public final class ModdedEngineBootstrap { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final String[] CORE_SELF_TEST_CLASSES = { + "art.arcane.iris.engine.IrisEngine", + "art.arcane.iris.util.common.data.B", + "art.arcane.iris.core.loader.IrisData" + }; + private static final Object LOCK = new Object(); + private static final ModdedServiceManager UNBOUND_SERVICE_MANAGER = new ModdedServiceManager(); + private static volatile ModdedLoader loader; + private static volatile BoundRuntime runtime; + private static volatile MinecraftServer currentServer; + private static volatile MinecraftServer spawnCaptureServer; + private static volatile boolean initialSpawnWasDefault; + + private ModdedEngineBootstrap() { + } + + public static ModdedServiceManager services() { + BoundRuntime bound = runtime; + return bound == null ? UNBOUND_SERVICE_MANAGER : bound.services(); + } + + public static ModdedScheduler schedulerOrNull() { + BoundRuntime bound = runtime; + return bound == null ? null : bound.platform().moddedScheduler(); + } + + public static void tick(MinecraftServer server) { + ModdedScheduler.tick(server); + ModdedStartup.runOnce(server); + ModdedPrimaryWorldRouter.tick(server); + services().tick(server); + ModdedPregenBossBar.tick(server); + ModdedProtocolHandler.tickDimensionSync(server); + } + + public static void start(MinecraftServer server) { + captureInitialSpawn(server); + currentServer = server; + bind(); + // Pair of the stop() burst-pools stage. Load-bearing on integrated servers: once + // closed, MultiBurst falls back to a same-thread executor, so a second world load + // without reopen() would silently run every burst inline. + MultiBurst.burst.reopen(); + MultiBurst.ioBurst.reopen(); + ModdedScheduler scheduler = schedulerOrNull(); + if (scheduler != null) { + scheduler.reset(); + } + IrisModdedCommands.openDownloadAdmission(); + ModdedStartup.prepareForStartup(); + IrisModdedChunkGenerator.startGenPool(); + bindWorldGenerators(server); + services().enableAll(); + ModdedProtocolHandler.start(server); + ModdedSentry.start(loader()); + } + + private static void bindWorldGenerators(MinecraftServer server) { + for (ServerLevel level : server.getAllLevels()) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) { + generator.bindLevel(level); + } + } + } + + public static void serverAboutToStart(MinecraftServer server) { + captureInitialSpawn(server); + ModdedStartup.prepareForStartup(); + } + + public static void serverStarted(MinecraftServer server) { + // Prime the off-thread level snapshot before anything can read it; the per-tick refresh in + // ModdedScheduler.tick has not run yet at this point. + ModdedServerLevels.refreshIfStale(server); + bindWorldGenerators(server); + ModdedStartup.runOnce(server); + reconcileSpawn(server); + ModdedWorldCheck.serverStarted(server); + } + + public static void levelLoaded(ServerLevel level) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) { + generator.bindLevel(level); + } + } + + public static void levelUnloaded(ServerLevel level) { + if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) { + return; + } + + try { + generator.unbindEngine(level); + } catch (Throwable exception) { + LOGGER.error("Iris engine unload failed for {}", level.dimension().identifier(), exception); + if (exception instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (exception instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Iris engine unload failed for " + + level.dimension().identifier(), exception); + } + } + + public static void stop() { + MinecraftServer stoppingServer = currentServer; + Throwable failure = null; + failure = runStopStage(failure, "pack downloads", IrisModdedCommands::shutdownDownloads); + failure = runStopStage(failure, "world check", () -> ModdedWorldCheck.serverStopped(stoppingServer)); + failure = runStopStage(failure, "protocol", ModdedProtocolHandler::stop); + failure = runStopStage(failure, "pregenerator", ModdedPregenJob::shutdown); + failure = runStopStage(failure, "object undo", ModdedObjectUndo::clearAll); + failure = runStopStage(failure, "wand service", ModdedWandService::clearAll); + failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear); + failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear); + failure = runStopStage(failure, "gui host", ModdedGuiHost::clear); + failure = runStopStage(failure, "services", () -> services().disableAll()); + failure = runStopStage(failure, "world engines", ModdedWorldEngines::shutdown); + failure = runStopStage(failure, "primary world router", ModdedPrimaryWorldRouter::clear); + failure = runStopStage(failure, "dimension manager", ModdedDimensionManager::clear); + ModdedScheduler scheduler = schedulerOrNull(); + if (scheduler != null) { + failure = runStopStage(failure, "scheduler", scheduler::shutdown); + } else { + // No bound runtime: the scheduler shutdown that normally drops the level snapshot never runs, and a + // static snapshot of a stopped server keeps its whole level graph alive. + failure = runStopStage(failure, "level snapshot", ModdedServerLevels::forget); + } + failure = runStopStage(failure, "generation pool", IrisModdedChunkGenerator::shutdownGenPool); + failure = runStopStage(failure, "burst pools", () -> { + MultiBurst.burst.close(); + MultiBurst.ioBurst.close(); + }); + failure = runStopStage(failure, "sentry", ModdedSentry::flush); + failure = runStopStage(failure, "startup state", ModdedStartup::reset); + failure = runStopStage(failure, "server state", () -> { + currentServer = null; + spawnCaptureServer = null; + initialSpawnWasDefault = false; + }); + if (failure != null) { + // The shutdown path must not propagate: propagating aborts the remaining loader stop handlers and + // can leave the level unsaved. Every stage already logged its own failure. + LOGGER.error("Iris modded shutdown completed with failures", failure); + } + } + + private static Throwable runStopStage(Throwable failure, String stage, StopAction action) { + try { + action.run(); + return failure; + } catch (Throwable stageFailure) { + LOGGER.error("Iris modded shutdown stage '{}' failed", stage, stageFailure); + if (failure == null) { + return stageFailure; + } + if (stageFailure != failure) { + failure.addSuppressed(stageFailure); + } + return failure; + } + } + + private static void captureInitialSpawn(MinecraftServer server) { + if (spawnCaptureServer == server) { + return; + } + LevelData.RespawnData respawnData = server.getRespawnData(); + initialSpawnWasDefault = respawnData == null || LevelData.RespawnData.DEFAULT.equals(respawnData); + spawnCaptureServer = server; + } + + private static void reconcileSpawn(MinecraftServer server) { + LevelData.RespawnData current = server.getRespawnData(); + if (current == null) { + return; + } + ServerLevel level = server.getLevel(current.dimension()); + if (level == null || !(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) { + return; + } + String dimensionId = level.dimension().identifier().toString(); + boolean studio = dimensionId.startsWith("irisworldgen:studio_"); + if (!shouldReconcileSpawn(initialSpawnWasDefault, studio, current.pos().getX(), current.pos().getZ())) { + return; + } + LevelChunk originChunk = level.getChunk(0, 0); + int surfaceY = originChunk.getHeight(Heightmap.Types.MOTION_BLOCKING, 0, 0) + 1; + BlockPos position = reconciledSpawnPosition(surfaceY, level.getMinY(), level.getHeight()); + server.setRespawnData(LevelData.RespawnData.of( + level.dimension(), position, current.yaw(), current.pitch())); + LOGGER.info("Iris spawn reconciled for {} at {},{},{}", dimensionId, + position.getX(), position.getY(), position.getZ()); + } + + static boolean shouldReconcileSpawn(boolean initialDefault, boolean studio, int currentX, int currentZ) { + return initialDefault || studio || currentX == 0 && currentZ == 0; + } + + static BlockPos reconciledSpawnPosition(int surfaceY, int minY, int height) { + int y = Math.max(minY + 1, Math.min(minY + height - 2, surfaceY)); + return new BlockPos(0, y, 0); + } + + public static void bootCommon(ModdedLoader moddedLoader, String loaderDescription, Runnable chunkGeneratorRegistration) { + loader = moddedLoader; + ModdedIrisLog.info("Iris " + moddedLoader.modVersion() + " bootstrapping on Minecraft " + moddedLoader.minecraftVersion() + " (" + loaderDescription + ")"); + selfTest(moddedLoader.getClass().getClassLoader()); + bind(); + IrisLanguage.initialize(); + ModdedStartup.prefetchStartupDatapack(); + if (!moddedLoader.clientEnvironment()) { + MainWorldService.reconcileEarly(); + } + chunkGeneratorRegistration.run(); + ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris"); + armParityProbe(); + armWorldCheck(); + } + + private static void armParityProbe() { + String parity = System.getProperty("iris.parity"); + if (parity == null) { + return; + } + ModdedIrisLog.info("Iris parity probe armed: " + parity); + ModdedParityProbe.schedule(parity); + } + + private static void armWorldCheck() { + if (System.getProperty("iris.worldcheck") == null) { + return; + } + ModdedIrisLog.info("Iris world check armed"); + ModdedWorldCheck.schedule(); + } + + public static ModdedLoader loader() { + ModdedLoader bound = loader; + if (bound == null) { + throw new IllegalStateException("Iris modded loader is not initialized; the loader bootstrap must call ModdedEngineBootstrap.bootCommon first"); + } + return bound; + } + + public static MinecraftServer currentServer() { + MinecraftServer tracked = currentServer; + return tracked != null ? tracked : loader().currentServer(); + } + + private static void selfTest(ClassLoader classLoader) { + int loadedClasses = 0; + for (String className : CORE_SELF_TEST_CLASSES) { + try { + Class.forName(className, true, classLoader); + loadedClasses++; + } catch (Throwable error) { + LOGGER.error("Iris core self-test failed to initialize {}", className, error); + } + } + + if (loadedClasses != CORE_SELF_TEST_CLASSES.length) { + throw new IllegalStateException("Iris core self-test failed: only " + loadedClasses + " of " + CORE_SELF_TEST_CLASSES.length + " engine classes initialized"); + } + + ModdedIrisLog.info("Iris core loaded (" + loadedClasses + " classes ok)"); + } + + public static ModdedPlatform bind() { + BoundRuntime bound = runtime; + if (bound != null) { + return bound.platform(); + } + synchronized (LOCK) { + BoundRuntime synchronizedBound = runtime; + if (synchronizedBound != null) { + return synchronizedBound.platform(); + } + ModdedLoader boundLoader = loader(); + ModdedPlatform created = new ModdedPlatform(boundLoader); + ModdedServiceManager createdServices = new ModdedServiceManager(); + BindRollback rollback = new BindRollback(); + try { + boolean previousDesktopSuppression = GuiHost.isDesktopSuppressed(); + GuiHost.suppressDesktop(boundLoader.clientEnvironment()); + rollback.add(() -> GuiHost.suppressDesktop(previousDesktopSuppression)); + + IrisPlatform previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; + IrisPlatforms.bind(created); + rollback.add(() -> restorePlatform(previousPlatform)); + + ModdedServerAccess previousServerAccess = ModdedDimensionManager.bindAccess( + new ModdedServerLevels(boundLoader::invalidateLevelCache)); + rollback.add(() -> ModdedDimensionManager.restoreAccess(previousServerAccess)); + + IrisObjectRotation.StateRotator previousRotator = IrisObjectRotation.bindPlatformRotator(new ModdedStateRotator()); + rollback.add(() -> IrisObjectRotation.restorePlatformRotator(previousRotator)); + + BlockDataMergeSupport.StateMerger previousMerger = BlockDataMergeSupport.bindPlatformMerger(new ModdedStateMerger()); + rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger(previousMerger)); + + TileData.TileReader previousTileReader = TileData.bindPlatformReader(new ModdedTileReader(boundLoader::currentServer)); + rollback.add(() -> TileData.restorePlatformReader(previousTileReader)); + + TileData.TileFactory previousTileFactory = TileData.bindPlatformFactory(ModdedTileData::fromProperties); + rollback.add(() -> TileData.restorePlatformFactory(previousTileFactory)); + + GuiHost.Provider previousGuiHost = GuiHost.get(); + ModdedGuiHost.install(); + rollback.add(() -> GuiHost.set(previousGuiHost)); + + ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks(); + DecoratorPlatformHooks.Bindings previousDecoratorBindings = DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks); + rollback.add(() -> DecoratorPlatformHooks.restore(previousDecoratorBindings)); + + ModdedPreservationService preservation = createdServices.register( + ModdedPreservationService.class, new ModdedPreservationService()); + createdServices.register(ModdedLogFilterService.class, new ModdedLogFilterService()); + createdServices.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService()); + createdServices.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService()); + ModdedStudioHotloadService studioHotloadService = createdServices.register( + ModdedStudioHotloadService.class, new ModdedStudioHotloadService()); + createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService()); + createdServices.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService()); + createdServices.register(ModdedTreeFellerService.class, new ModdedTreeFellerService()); + + bindService(PreservationRegistry.class, preservation, rollback); + bindService(EngineEffectsProvider.class, + (EngineEffectsProvider) ModdedEngineEffects::new, rollback); + bindService(EnginePlatformHooks.class, studioHotloadService, rollback); + bindService(EngineWorldManagerProvider.class, + (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine), rollback); + + ModdedCustomContentRegistry.Discovery customContentDiscovery = + ModdedCustomContentRegistry.discover(); + rollback.add(customContentDiscovery::rollback); + try { + ModdedIrisSplash.print(boundLoader); + } catch (Throwable splashFailure) { + // A cosmetic banner must never roll back the platform bind. + LOGGER.warn("Iris splash could not be printed", splashFailure); + } + createdServices.enableAll(); + runtime = new BoundRuntime(created, createdServices); + rollback.clear(); + return created; + } catch (Throwable failure) { + createdServices.rollback(failure); + rollback.restore(failure); + LOGGER.error("Iris modded platform binding failed", failure); + if (failure instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (failure instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Iris modded platform binding failed", failure); + } + } + } + + private static void bindService(Class type, T service, BindRollback rollback) { + T previous = IrisServices.getOrNull(type); + IrisServices.register(type, service); + rollback.add(() -> restoreService(type, previous)); + } + + private static void restoreService(Class type, T service) { + if (service == null) { + IrisServices.remove(type); + return; + } + IrisServices.register(type, service); + } + + private static void restorePlatform(IrisPlatform platform) { + IrisPlatforms.unbind(); + if (platform != null) { + IrisPlatforms.bind(platform); + } + } + + private record BoundRuntime(ModdedPlatform platform, ModdedServiceManager services) { + } + + @FunctionalInterface + private interface RollbackAction { + void restore() throws Throwable; + } + + @FunctionalInterface + private interface StopAction { + void run() throws Throwable; + } + + private static final class BindRollback { + private final ArrayDeque actions = new ArrayDeque<>(); + + void add(RollbackAction action) { + actions.push(action); + } + + void restore(Throwable failure) { + while (!actions.isEmpty()) { + try { + actions.pop().restore(); + } catch (Throwable rollbackFailure) { + if (rollbackFailure != failure) { + failure.addSuppressed(rollbackFailure); + } + } + } + } + + void clear() { + actions.clear(); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineEffects.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineEffects.java new file mode 100644 index 000000000..600de8b39 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineEffects.java @@ -0,0 +1,499 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedComponent; +import art.arcane.iris.engine.framework.EngineEffects; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisCommand; +import art.arcane.iris.engine.object.IrisCommandRegistry; +import art.arcane.iris.engine.object.IrisEffect; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.Holder; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.particles.SimpleParticleType; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class ModdedEngineEffects extends EngineAssignedComponent implements EngineEffects { + private static final long EFFECT_BUDGET_NANOS = 1_500_000L; + private static final long SAMPLE_INTERVAL_MILLIS = 55L; + private static final double SAMPLE_DISTANCE_SQUARED = 81.0D; + private static final double PLAYER_REFRESH_CHANCE = 0.02D; + private static final Set UNKNOWN_POTIONS = ConcurrentHashMap.newKeySet(); + private static final Set UNKNOWN_SOUNDS = ConcurrentHashMap.newKeySet(); + private static final Set UNKNOWN_PARTICLES = ConcurrentHashMap.newKeySet(); + private static final Set UNSUPPORTED_PARTICLES = ConcurrentHashMap.newKeySet(); + + private final Map players; + private final AtomicBoolean updateRequested; + private final AtomicBoolean tickRequested; + private final AtomicBoolean passQueued; + + public ModdedEngineEffects(Engine engine) { + super(engine, "FX"); + players = new HashMap<>(); + updateRequested = new AtomicBoolean(false); + tickRequested = new AtomicBoolean(false); + passQueued = new AtomicBoolean(false); + } + + @Override + public void updatePlayerMap() { + updateRequested.set(true); + queueMainPass(); + } + + @Override + public void tickRandomPlayer() { + tickRequested.set(true); + queueMainPass(); + } + + private void queueMainPass() { + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null || !passQueued.compareAndSet(false, true)) { + return; + } + scheduler.global(this::runQueuedPass); + } + + private void runQueuedPass() { + try { + boolean shouldUpdate = updateRequested.getAndSet(false); + boolean shouldTick = tickRequested.getAndSet(false); + if (!shouldUpdate && !shouldTick) { + return; + } + + ServerLevel level = resolveLevel(); + if (level == null) { + players.clear(); + return; + } + + if (shouldUpdate) { + syncPlayers(level); + } + if (shouldTick) { + tickPlayers(level); + } + } catch (Throwable error) { + IrisLogging.reportError(error); + } finally { + passQueued.set(false); + if (updateRequested.get() || tickRequested.get()) { + queueMainPass(); + } + } + } + + private ServerLevel resolveLevel() { + Engine engine = getEngine(); + if (engine.isClosed() || !engine.getWorld().hasPlatformWorld()) { + return null; + } + Object nativeWorld = engine.getWorld().platformWorld().nativeHandle(); + return nativeWorld instanceof ServerLevel ? (ServerLevel) nativeWorld : null; + } + + private void syncPlayers(ServerLevel level) { + List activePlayers = level.players(); + Set activeIds = new HashSet<>(Math.max(16, activePlayers.size() * 2)); + for (ServerPlayer player : activePlayers) { + UUID playerId = player.getUUID(); + activeIds.add(playerId); + PlayerState state = players.get(playerId); + if (state == null) { + players.put(playerId, new PlayerState(player)); + } else { + state.player(player); + } + } + + Iterator> iterator = players.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (!activeIds.contains(entry.getKey())) { + iterator.remove(); + } + } + } + + private void tickPlayers(ServerLevel level) { + if (players.isEmpty()) { + syncPlayers(level); + } else if (RNG.r.d() < PLAYER_REFRESH_CHANCE) { + syncPlayers(level); + return; + } + if (players.isEmpty()) { + return; + } + + List snapshot = new ArrayList<>(players.values()); + long started = System.nanoTime(); + int remaining = snapshot.size(); + while (remaining-- > 0 && System.nanoTime() - started < EFFECT_BUDGET_NANOS) { + PlayerState state = snapshot.get(RNG.r.nextInt(snapshot.size())); + try { + tickPlayer(level, state); + } catch (Throwable error) { + IrisLogging.reportError(error); + } + } + } + + private void tickPlayer(ServerLevel level, PlayerState state) { + ServerPlayer player = state.player(); + if (!player.isAlive() || player.isRemoved() || player.level() != level) { + return; + } + + samplePlayer(state); + if (!IrisSettings.get().getWorld().isEffectSystem()) { + return; + } + + IrisRegion region = state.region(); + if (region != null) { + applyEffects(level, player, region.getEffects()); + } + IrisBiome biome = state.biome(); + if (biome != null) { + applyEffects(level, player, biome.getEffects()); + } + } + + private void samplePlayer(PlayerState state) { + ServerPlayer player = state.player(); + double deltaX = player.getX() - state.lastX(); + double deltaY = player.getY() - state.lastY(); + double deltaZ = player.getZ() - state.lastZ(); + double distanceSquared = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ; + long now = System.currentTimeMillis(); + if (!needsSample(state.sampled(), now - state.lastSample(), distanceSquared)) { + return; + } + + state.sampled(true); + state.lastPosition(player.getX(), player.getY(), player.getZ()); + state.lastSample(now); + int blockX = floor(player.getX()); + int blockY = floor(player.getY()) - getEngine().getWorld().minHeight(); + int blockZ = floor(player.getZ()); + state.biome(getEngine().getBiome(blockX, blockY, blockZ)); + state.region(getEngine().getRegion(blockX, blockZ)); + } + + private void applyEffects(ServerLevel level, ServerPlayer player, Iterable effects) { + for (IrisEffect effect : effects) { + try { + applyEffect(level, player, effect); + } catch (Throwable error) { + IrisLogging.reportError(error); + } + } + } + + private void applyEffect(ServerLevel level, ServerPlayer player, IrisEffect effect) { + if (!effect.shouldApplyNow()) { + return; + } + + applySound(player, effect); + applyParticles(level, player, effect); + applyCommands(level, player, effect.getCommandRegistry()); + applyPotion(player, effect); + } + + private void applySound(ServerPlayer player, IrisEffect effect) { + String key = normalizeRegistryKey(effect.getSoundKey()); + if (key == null) { + return; + } + Identifier identifier = Identifier.tryParse(key); + Optional> sound = identifier == null + ? Optional.empty() + : BuiltInRegistries.SOUND_EVENT.get(identifier); + if (sound.isEmpty()) { + warnUnknown(UNKNOWN_SOUNDS, "sound", key); + return; + } + + int distance = effect.getSoundDistance(); + double x = player.getX() + RNG.r.i(-distance, distance); + double y = player.getY() + RNG.r.i(-distance, distance); + double z = player.getZ() + RNG.r.i(-distance, distance); + float volume = (float) effect.getVolume(); + float pitch = (float) RNG.r.d(effect.getMinPitch(), effect.getMaxPitch()); + player.connection.send(new ClientboundSoundPacket( + sound.get(), SoundSource.MASTER, x, y, z, volume, pitch, ThreadLocalRandom.current().nextLong())); + } + + private void applyParticles(ServerLevel level, ServerPlayer player, IrisEffect effect) { + String key = normalizeRegistryKey(effect.getParticleEffectKey()); + if (key == null) { + return; + } + Identifier identifier = Identifier.tryParse(key); + ParticleType particleType = identifier == null + ? null + : BuiltInRegistries.PARTICLE_TYPE.getValue(identifier); + if (particleType == null) { + warnUnknown(UNKNOWN_PARTICLES, "particle", key); + return; + } + if (!(particleType instanceof SimpleParticleType simpleParticle)) { + if (UNSUPPORTED_PARTICLES.add(key)) { + IrisLogging.warn("Particle type \"" + key + "\" requires particle data and cannot be used by an Iris effect without data."); + } + return; + } + + Vec3 direction = player.getLookAngle(); + double forward = RNG.r.i(effect.getParticleDistance()) + effect.getParticleAway(); + double sideways = RNG.r.d(-effect.getParticleDistanceWidth(), effect.getParticleDistanceWidth()); + double surfaceX = player.getX() + direction.x * forward + direction.z * sideways; + double surfaceZ = player.getZ() + direction.z * forward - direction.x * sideways; + int surfaceY = getEngine().getHeight(floor(surfaceX), floor(surfaceZ)); + double x = surfaceX + RNG.r.d(); + double y = surfaceY + 1.0D + level.getMinY() + RNG.r.i(effect.getParticleOffset()); + double z = surfaceZ + RNG.r.d(); + double altX = randomized(effect.getParticleAltX(), effect.isRandomAltX()); + double altY = randomized(effect.getParticleAltY(), effect.isRandomAltY()); + double altZ = randomized(effect.getParticleAltZ(), effect.isRandomAltZ()); + level.sendParticles(player, simpleParticle, false, false, + x, y, z, effect.getParticleCount(), altX, altY, altZ, effect.getExtra()); + } + + private void applyCommands(ServerLevel level, ServerPlayer player, IrisCommandRegistry registry) { + if (registry == null || registry.getRawCommands() == null || registry.getRawCommands().isEmpty()) { + return; + } + + ModdedPlatformWorld world = new ModdedPlatformWorld(level); + double x = commandCoordinate(player.getX(), registry.getCommandOffsetX(), registry.isCommandRandomAltX()); + double y = commandCoordinate(player.getY(), registry.getCommandOffsetY(), registry.isCommandRandomAltY()); + double z = commandCoordinate(player.getZ(), registry.getCommandOffsetZ(), registry.isCommandRandomAltZ()); + for (IrisCommand command : registry.getRawCommands()) { + command.run(world, floor(x), floor(y), floor(z)); + if (registry.isCommandAllRandomLocations()) { + x = commandCoordinate(player.getX(), registry.getCommandOffsetX(), registry.isCommandRandomAltX()); + y = commandCoordinate(player.getY(), registry.getCommandOffsetY(), registry.isCommandRandomAltY()); + z = commandCoordinate(player.getZ(), registry.getCommandOffsetZ(), registry.isCommandRandomAltZ()); + } + } + } + + private void applyPotion(ServerPlayer player, IrisEffect effect) { + int strength = effect.getPotionStrength(); + if (strength < 0) { + return; + } + + Holder type = resolvePotion(effect.getPotionEffect()); + MobEffectInstance current = player.getEffect(type); + if (current != null && !shouldReplacePotionEffect(current.getAmplifier(), strength)) { + return; + } + if (current != null) { + player.removeEffect(type); + } + + int minimum = Math.min(effect.getPotionTicksMin(), effect.getPotionTicksMax()); + int maximum = Math.max(effect.getPotionTicksMin(), effect.getPotionTicksMax()); + int duration = RNG.r.i(minimum, maximum); + player.addEffect(new MobEffectInstance(type, duration, strength, true, false, false)); + } + + private Holder resolvePotion(String rawKey) { + String key = normalizePotionEffectKey(rawKey); + Identifier identifier = Identifier.tryParse(key); + if (identifier != null) { + Optional> effect = BuiltInRegistries.MOB_EFFECT.get(identifier); + if (effect.isPresent()) { + return effect.get(); + } + } + if (UNKNOWN_POTIONS.add(key)) { + IrisLogging.warn("Unknown Potion Effect Type: \"" + rawKey + "\". Using LUCK instead."); + } + return MobEffects.LUCK; + } + + static String normalizeRegistryKey(String rawKey) { + if (rawKey == null || rawKey.isBlank()) { + return null; + } + String normalized = rawKey.trim().toLowerCase(Locale.ROOT).replace(' ', '_'); + return normalized.contains(":") ? normalized : "minecraft:" + normalized; + } + + static String normalizePotionEffectKey(String rawKey) { + String normalized = normalizeRegistryKey(rawKey == null || rawKey.isBlank() ? "luck" : rawKey); + int separator = normalized.indexOf(':'); + String namespace = normalized.substring(0, separator); + String path = normalized.substring(separator + 1); + String aliased = switch (path) { + case "slow" -> "slowness"; + case "fast_digging" -> "haste"; + case "slow_digging" -> "mining_fatigue"; + case "increase_damage" -> "strength"; + case "heal" -> "instant_health"; + case "harm" -> "instant_damage"; + case "jump" -> "jump_boost"; + case "confusion" -> "nausea"; + case "damage_resistance" -> "resistance"; + default -> path; + }; + return namespace + ":" + aliased; + } + + static boolean needsSample(boolean sampled, long elapsedMillis, double distanceSquared) { + return !sampled || elapsedMillis > SAMPLE_INTERVAL_MILLIS && distanceSquared > SAMPLE_DISTANCE_SQUARED; + } + + static boolean shouldReplacePotionEffect(int existingAmplifier, int configuredAmplifier) { + return existingAmplifier <= configuredAmplifier; + } + + private static int floor(double value) { + return (int) Math.floor(value); + } + + private static double randomized(double value, boolean random) { + return random ? RNG.r.d(-value, value) : value; + } + + private static double commandCoordinate(double base, double offset, boolean random) { + return base + randomized(offset, random); + } + + private static void warnUnknown(Set warned, String type, String key) { + if (warned.add(key)) { + IrisLogging.warn("Unknown " + type + " type: \"" + key + "\"."); + } + } + + private static final class PlayerState { + private ServerPlayer player; + private IrisBiome biome; + private IrisRegion region; + private double lastX; + private double lastY; + private double lastZ; + private long lastSample; + private boolean sampled; + + private PlayerState(ServerPlayer player) { + this.player = player; + lastX = player.getX(); + lastY = player.getY(); + lastZ = player.getZ(); + lastSample = -1L; + sampled = false; + } + + private ServerPlayer player() { + return player; + } + + private void player(ServerPlayer player) { + this.player = player; + } + + private IrisBiome biome() { + return biome; + } + + private void biome(IrisBiome biome) { + this.biome = biome; + } + + private IrisRegion region() { + return region; + } + + private void region(IrisRegion region) { + this.region = region; + } + + private double lastX() { + return lastX; + } + + private double lastY() { + return lastY; + } + + private double lastZ() { + return lastZ; + } + + private void lastPosition(double x, double y, double z) { + lastX = x; + lastY = y; + lastZ = z; + } + + private long lastSample() { + return lastSample; + } + + private void lastSample(long lastSample) { + this.lastSample = lastSample; + } + + private boolean sampled() { + return sampled; + } + + private void sampled(boolean sampled) { + this.sampled = sampled; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityAwareness.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityAwareness.java new file mode 100644 index 000000000..c72991497 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityAwareness.java @@ -0,0 +1,39 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.world.entity.Mob; + +import java.util.Set; + +public final class ModdedEntityAwareness { + private static final String UNAWARE_TAG = "iris_unaware"; + + private ModdedEntityAwareness() { + } + + public static void configure(Mob mob, boolean aware) { + if (aware) { + mob.removeTag(UNAWARE_TAG); + return; + } + if (!mob.entityTags().contains(UNAWARE_TAG) && !mob.addTag(UNAWARE_TAG)) { + IrisLogging.warn("Iris could not mark generated mob '" + mob.getStringUUID() + "' as unaware"); + } + } + + public static boolean isAware(Mob mob) { + return isAware(mob.entityTags()); + } + + static boolean isAware(Set tags) { + return !tags.contains(UNAWARE_TAG); + } + + static void configureTags(Set tags, boolean aware) { + if (aware) { + tags.remove(UNAWARE_TAG); + } else { + tags.add(UNAWARE_TAG); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityCommandRunner.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityCommandRunner.java new file mode 100644 index 000000000..f1a6709eb --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityCommandRunner.java @@ -0,0 +1,82 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.IrisCommand; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; + +final class ModdedEntityCommandRunner { + private ModdedEntityCommandRunner() { + } + + static void run(KList commands, ServerLevel level, int blockX, int blockY, int blockZ) { + if (commands.isEmpty()) { + return; + } + MinecraftServer server = level.getServer(); + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (server == null || scheduler == null) { + IrisLogging.error("Iris could not schedule entity commands because the modded server scheduler is unavailable."); + return; + } + + ModdedPlatformWorld world = new ModdedPlatformWorld(level); + for (IrisCommand command : commands) { + if (command == null || !command.isValid(world)) { + continue; + } + schedule(command, server, scheduler, blockX, blockY, blockZ); + } + } + + private static void schedule(IrisCommand command, MinecraftServer server, ModdedScheduler scheduler, int blockX, int blockY, int blockZ) { + int delay = clampDelay(command.getDelay(), 0); + int repeatDelay = clampDelay(command.getRepeatDelay(), 1); + for (String raw : command.getCommands()) { + String prepared = prepareCommand(raw, blockX, blockY, blockZ); + if (prepared == null) { + continue; + } + if (command.isRepeat()) { + scheduler.laterGlobal(() -> new RepeatingCommand(server, scheduler, prepared, repeatDelay).run(), delay); + } else { + scheduler.laterGlobal(() -> ModdedServerCommands.dispatch(server, prepared), delay); + } + } + } + + static String prepareCommand(String raw, int blockX, int blockY, int blockZ) { + if (raw == null || raw.isBlank()) { + return null; + } + return (raw.startsWith("/") ? raw.substring(1) : raw) + .replace("{x}", String.valueOf(blockX)) + .replace("{y}", String.valueOf(blockY)) + .replace("{z}", String.valueOf(blockZ)); + } + + static int clampDelay(long delay, int minimum) { + return (int) Math.max(minimum, Math.min(Integer.MAX_VALUE, delay)); + } + + private static final class RepeatingCommand implements Runnable { + private final MinecraftServer server; + private final ModdedScheduler scheduler; + private final String command; + private final int interval; + + private RepeatingCommand(MinecraftServer server, ModdedScheduler scheduler, String command, int interval) { + this.server = server; + this.scheduler = scheduler; + this.command = command; + this.interval = interval; + } + + @Override + public void run() { + ModdedServerCommands.dispatch(server, command); + scheduler.laterGlobal(this, interval); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityPersistence.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityPersistence.java new file mode 100644 index 000000000..362bed5e7 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityPersistence.java @@ -0,0 +1,43 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Mob; + +import java.util.Set; + +public final class ModdedEntityPersistence { + private static final String NON_PERSISTENT_TAG = "iris_non_persistent"; + + private ModdedEntityPersistence() { + } + + public static void configure(Entity entity, boolean persistent) { + if (persistent) { + entity.removeTag(NON_PERSISTENT_TAG); + if (entity instanceof Mob mob) { + mob.setPersistenceRequired(); + } + return; + } + if (!entity.entityTags().contains(NON_PERSISTENT_TAG) && !entity.addTag(NON_PERSISTENT_TAG)) { + IrisLogging.warn("Iris could not mark generated entity '" + entity.getStringUUID() + "' as non-persistent"); + } + } + + public static boolean shouldSave(Entity entity, boolean vanillaResult) { + return shouldSave(entity.entityTags(), vanillaResult); + } + + static boolean shouldSave(Set tags, boolean vanillaResult) { + return vanillaResult && !tags.contains(NON_PERSISTENT_TAG); + } + + static void configureTags(Set tags, boolean persistent) { + if (persistent) { + tags.remove(NON_PERSISTENT_TAG); + } else { + tags.add(NON_PERSISTENT_TAG); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java new file mode 100644 index 000000000..29a0403e0 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java @@ -0,0 +1,585 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.IrisAttributeModifier; +import art.arcane.iris.engine.object.IrisEntity; +import art.arcane.iris.engine.object.IrisEffect; +import art.arcane.iris.engine.object.IrisLoot; +import art.arcane.iris.modded.api.ModdedCustomContentRegistry; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.particles.SimpleParticleType; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.animal.panda.Panda; +import net.minecraft.world.entity.npc.villager.Villager; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; + +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +public final class ModdedEntitySpawner { + private static final int PASSENGER_RNG_BASE = 234858; + private static final int LEASH_RNG_SEED = 234548; + private static final int PLAYER_EFFECT_RADIUS = 32; + private static final int RISE_MAX_MOVES = 101; + private static final String COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx"; + private static final Set WARNED_TYPES = ConcurrentHashMap.newKeySet(); + private static final Set WARNED_ATTRIBUTES = ConcurrentHashMap.newKeySet(); + private static final Set WARNED_EFFECT_SOUNDS = ConcurrentHashMap.newKeySet(); + private static final Set WARNED_EFFECT_PARTICLES = ConcurrentHashMap.newKeySet(); + private static final Set WARNED_PERSISTENCE_TYPES = ConcurrentHashMap.newKeySet(); + + private ModdedEntitySpawner() { + } + + public static Entity spawn(Engine engine, IrisEntity irisEntity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) { + if (engine == null || irisEntity == null || level == null) { + return null; + } + if (!chunksSafe(level, blockX >> 4, blockZ >> 4)) { + return null; + } + + double x = blockX + 0.5; + double y = blockY + 0.5; + double z = blockZ + 0.5; + boolean riseEffectActive = irisEntity.isSpawnEffectRiseOutOfGround() && hasPlayersNearby(level, x, y, z); + int spawnBlockY = riseEffectActive ? blockY - 5 : blockY; + double spawnY = spawnBlockY + 0.5; + + Entity created = create(irisEntity, level, x, spawnY, z); + if (created == null) { + return null; + } + + if (irisEntity.isSpecialType() && !irisEntity.isApplySettingsToCustomMobAnyways()) { + return created; + } + + applyConfig(engine, irisEntity, created, level, blockX, spawnBlockY, blockZ, rng, riseEffectActive); + return created; + } + + private static Entity create(IrisEntity irisEntity, ServerLevel level, double x, double y, double z) { + if (irisEntity.isSpecialType()) { + return ModdedCustomContentRegistry.spawnMob(level, x, y, z, irisEntity.getSpecialType()); + } + + EntityType type = resolveType(irisEntity.getType()); + if (type == null) { + return null; + } + + Entity entity = type.spawn(level, BlockPos.containing(x, y, z), reasonFor(irisEntity.getReason())); + if (entity != null) { + entity.snapTo(x, y, z, 0F, 0F); + } + return entity; + } + + private static void applyConfig(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng, boolean riseEffectActive) { + String customName = irisEntity.getCustomName(); + if (customName != null) { + entity.setCustomName(Component.literal(colorize(customName))); + } + entity.setCustomNameVisible(irisEntity.isCustomNameVisible()); + entity.setGlowingTag(irisEntity.isGlowing()); + entity.setNoGravity(!irisEntity.isGravity()); + entity.setInvulnerable(irisEntity.isInvulnerable()); + entity.setSilent(irisEntity.isSilent()); + + boolean persistent = irisEntity.isKeepEntity() || forcePersist(); + applyPersistence(entity, persistent); + + applyPassengers(engine, irisEntity, entity, level, blockX, blockY, blockZ, rng); + + if (entity instanceof LivingEntity living) { + applyAttributes(irisEntity, living, level, rng); + } + + if (!irisEntity.getLoot().getTables().isEmpty()) { + ModdedDeathLoot.bind(engine, entity, irisEntity.getLoot().getTables(), blockX, blockY, blockZ, rng); + } + + if (entity instanceof Mob mob) { + mob.setNoAi(shouldDisableAi(irisEntity.isAi())); + ModdedEntityAwareness.configure(mob, irisEntity.isAware()); + mob.setCanPickUpLoot(irisEntity.isPickupItems()); + if (!irisEntity.isRemovable()) { + mob.setPersistenceRequired(); + } + if (irisEntity.getLeashHolder() != null) { + Entity holder = spawn(engine, irisEntity.getLeashHolder(), level, blockX, blockY, blockZ, rng.nextParallelRNG(LEASH_RNG_SEED)); + if (holder != null) { + mob.setLeashedTo(holder, true); + } + } + } + + if (entity instanceof LivingEntity equippable) { + applyEquipment(irisEntity, equippable, level, rng); + } + + if (irisEntity.isBaby()) { + applyBaby(entity); + } + + if (entity instanceof Panda panda) { + panda.setMainGene(gene(irisEntity.getPandaMainGene())); + panda.setHiddenGene(gene(irisEntity.getPandaHiddenGene())); + } + + if (entity instanceof Villager villager) { + ModdedEntityPersistence.configure(villager, true); + } + + applySpawnEffect(irisEntity.getSpawnEffect(), entity, level); + ModdedEntityCommandRunner.run(irisEntity.getRawCommands(), level, blockX, blockY, blockZ); + if (riseEffectActive && entity instanceof LivingEntity living) { + startRiseEffect(engine, level, entity, living); + } + } + + private static void applyPassengers(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) { + int index = 0; + for (IrisEntity passengerEntity : irisEntity.getPassengers()) { + Entity passenger = spawn(engine, passengerEntity, level, blockX, blockY, blockZ, rng.nextParallelRNG(PASSENGER_RNG_BASE + index++)); + if (passenger != null) { + passenger.startRiding(entity); + } + } + } + + private static void applyAttributes(IrisEntity irisEntity, LivingEntity living, ServerLevel level, RNG rng) { + KList modifiers = irisEntity.getAttributes(); + if (modifiers.isEmpty()) { + return; + } + + Registry registry = level.registryAccess().lookupOrThrow(Registries.ATTRIBUTE); + int index = 0; + for (IrisAttributeModifier modifier : modifiers) { + index++; + if (rng.nextDouble() >= modifier.getChance()) { + continue; + } + Holder holder = ModdedItemTranslator.resolveAttribute(registry, modifier.getAttribute()); + if (holder == null) { + if (WARNED_ATTRIBUTES.add(modifier.getAttribute())) { + IrisLogging.warn("Iris entity: unknown attribute '" + modifier.getAttribute() + "'"); + } + continue; + } + AttributeInstance instance = living.getAttributes().getInstance(holder); + if (instance == null) { + continue; + } + Identifier id = Identifier.tryParse("iris:" + normalizeName(modifier.getName()) + "_" + index); + if (id == null) { + continue; + } + instance.addOrReplacePermanentModifier(new AttributeModifier(id, modifier.getAmount(rng), ModdedItemTranslator.operationFor(modifier.getOperation()))); + } + } + + private static void applyEquipment(IrisEntity irisEntity, LivingEntity living, ServerLevel level, RNG rng) { + setSlot(living, EquipmentSlot.HEAD, irisEntity.getHelmet(), level, rng); + setSlot(living, EquipmentSlot.CHEST, irisEntity.getChestplate(), level, rng); + setSlot(living, EquipmentSlot.LEGS, irisEntity.getLeggings(), level, rng); + setSlot(living, EquipmentSlot.FEET, irisEntity.getBoots(), level, rng); + setSlot(living, EquipmentSlot.MAINHAND, irisEntity.getMainHand(), level, rng); + setSlot(living, EquipmentSlot.OFFHAND, irisEntity.getOffHand(), level, rng); + } + + private static void setSlot(LivingEntity living, EquipmentSlot slot, IrisLoot loot, ServerLevel level, RNG rng) { + if (loot == null || !LootResolver.oneIn(rng, loot.getRarity())) { + return; + } + ItemStack stack = ModdedItemTranslator.stack(loot, rng, level); + if (stack != null && !stack.isEmpty()) { + living.setItemSlot(slot, stack); + } + } + + private static void applyBaby(Entity entity) { + if (entity instanceof Mob mob) { + mob.setBaby(true); + } + } + + static boolean isAreaClearForSpawn(ServerLevel level, IrisEntity irisEntity, int blockX, int blockY, int blockZ) { + if (irisEntity.isSpecialType()) { + return true; + } + EntityType type = resolveType(irisEntity.getType()); + if (type == null) { + return true; + } + return isAreaClearForSpawn(blockX, blockY, blockZ, type.getWidth(), type.getHeight(), + position -> level.getBlockState(position).is(Blocks.AIR)); + } + + static boolean isAreaClearForSpawn(int blockX, int blockY, int blockZ, float width, float height, Predicate isAir) { + int radius = (int) (width / 2F); + int endY = blockY + (int) height; + for (int x = blockX - radius; x <= blockX + radius; x++) { + for (int y = blockY; y <= endY; y++) { + for (int z = blockZ - radius; z <= blockZ + radius; z++) { + if (!isAir.test(new BlockPos(x, y, z))) { + return false; + } + } + } + } + return true; + } + + static boolean chunksSafe(ServerLevel level, int chunkX, int chunkZ) { + return allNeighborChunksLoaded(chunkX, chunkZ, + (x, z) -> level.getChunkSource().getChunkNow(x, z) != null); + } + + static boolean allNeighborChunksLoaded(int chunkX, int chunkZ, ChunkLoadedLookup lookup) { + for (int x = chunkX - 1; x <= chunkX + 1; x++) { + for (int z = chunkZ - 1; z <= chunkZ + 1; z++) { + if (!lookup.loaded(x, z)) { + return false; + } + } + } + return true; + } + + static boolean shouldDisableAi(boolean ai) { + return !ai; + } + + private static boolean hasPlayersNearby(ServerLevel level, double x, double y, double z) { + AABB bounds = new AABB( + x - PLAYER_EFFECT_RADIUS, + y - PLAYER_EFFECT_RADIUS, + z - PLAYER_EFFECT_RADIUS, + x + PLAYER_EFFECT_RADIUS, + y + PLAYER_EFFECT_RADIUS, + z + PLAYER_EFFECT_RADIUS); + for (ServerPlayer player : level.players()) { + if (player.getBoundingBox().intersects(bounds)) { + return true; + } + } + return false; + } + + private static void applySpawnEffect(IrisEffect effect, Entity entity, ServerLevel level) { + if (effect == null || !effect.shouldApplyNow()) { + return; + } + + SoundEvent sound = resolveSound(effect.getSoundKey()); + if (sound != null) { + double soundX = entity.getX() + RNG.r.i(-effect.getSoundDistance(), effect.getSoundDistance()); + double soundY = entity.getY() + RNG.r.i(-effect.getSoundDistance(), effect.getSoundDistance()); + double soundZ = entity.getZ() + RNG.r.i(-effect.getSoundDistance(), effect.getSoundDistance()); + level.playSound(null, soundX, soundY, soundZ, sound, SoundSource.MASTER, + (float) effect.getVolume(), (float) RNG.r.d(effect.getMinPitch(), effect.getMaxPitch())); + } + + ParticleOptions particle = resolveParticle(effect.getParticleEffectKey()); + if (particle == null) { + return; + } + + double addition = RNG.r.d(); + double subtraction = RNG.r.d(); + double particleX = entity.getX() + addition - subtraction + RNG.r.d(); + double particleY = entity.getY() + 0.25 + addition - subtraction + RNG.r.i(effect.getParticleOffset()); + double particleZ = entity.getZ() + addition - subtraction + RNG.r.d(); + double altX = effect.isRandomAltX() ? RNG.r.d(-effect.getParticleAltX(), effect.getParticleAltX()) : effect.getParticleAltX(); + double altY = effect.isRandomAltY() ? RNG.r.d(-effect.getParticleAltY(), effect.getParticleAltY()) : effect.getParticleAltY(); + double altZ = effect.isRandomAltZ() ? RNG.r.d(-effect.getParticleAltZ(), effect.getParticleAltZ()) : effect.getParticleAltZ(); + level.sendParticles(particle, particleX, particleY, particleZ, effect.getParticleCount(), + altX, altY, altZ, effect.getExtra()); + } + + private static SoundEvent resolveSound(String key) { + Identifier id = identifierFor(key); + if (id == null || !BuiltInRegistries.SOUND_EVENT.containsKey(id)) { + if (key != null && WARNED_EFFECT_SOUNDS.add(key)) { + IrisLogging.warn("Iris entity effect: unknown sound '" + key + "'"); + } + return null; + } + return BuiltInRegistries.SOUND_EVENT.getValue(id); + } + + private static ParticleOptions resolveParticle(String key) { + Identifier id = identifierFor(key); + if (id == null || !BuiltInRegistries.PARTICLE_TYPE.containsKey(id)) { + if (key != null && WARNED_EFFECT_PARTICLES.add(key)) { + IrisLogging.warn("Iris entity effect: unknown particle '" + key + "'"); + } + return null; + } + ParticleType type = BuiltInRegistries.PARTICLE_TYPE.getValue(id); + if (type instanceof SimpleParticleType simple) { + return simple; + } + if (WARNED_EFFECT_PARTICLES.add(key)) { + IrisLogging.warn("Iris entity effect: particle '" + key + "' requires data that the effect does not define"); + } + return null; + } + + private static Identifier identifierFor(String key) { + if (key == null || key.isBlank()) { + return null; + } + String normalized = key.trim().toLowerCase(Locale.ROOT); + return Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized); + } + + private static void startRiseEffect(Engine engine, ServerLevel level, Entity entity, LivingEntity living) { + RiseTask task = new RiseTask(engine, level, entity, living); + task.start(); + } + + static EntityType resolveType(String key) { + if (key == null || key.isBlank()) { + return null; + } + String normalized = key.trim().toLowerCase(Locale.ROOT); + Identifier id = identifierFor(normalized); + if (id == null || !BuiltInRegistries.ENTITY_TYPE.containsKey(id)) { + if (WARNED_TYPES.add(normalized)) { + IrisLogging.warn("Iris entity: unknown entity type '" + key + "'; skipping spawn"); + } + return null; + } + return BuiltInRegistries.ENTITY_TYPE.getValue(id); + } + + static EntitySpawnReason reasonFor(String reason) { + if (reason == null || reason.isBlank()) { + return EntitySpawnReason.NATURAL; + } + String value = reason.trim().toUpperCase(Locale.ROOT); + return switch (value) { + case "NATURAL", "DEFAULT" -> EntitySpawnReason.NATURAL; + case "JOCKEY", "MOUNT" -> EntitySpawnReason.JOCKEY; + case "CHUNK_GEN" -> EntitySpawnReason.CHUNK_GENERATION; + case "SPAWNER" -> EntitySpawnReason.SPAWNER; + case "TRIAL_SPAWNER" -> EntitySpawnReason.TRIAL_SPAWNER; + case "EGG", "BUILD_SNOWMAN", "BUILD_IRONGOLEM", "BUILD_COPPERGOLEM", "BUILD_WITHER", + "SLIME_SPLIT", "SILVERFISH_BLOCK", "TRAP", "ENDER_PEARL", "EXPLOSION", + "ENCHANTMENT", "OMINOUS_ITEM_SPAWNER", "POTION_EFFECT", "REANIMATE" -> EntitySpawnReason.TRIGGERED; + case "SPAWNER_EGG" -> EntitySpawnReason.SPAWN_ITEM_USE; + case "LIGHTNING", "VILLAGE_INVASION", "RAID", "CUSTOM" -> EntitySpawnReason.EVENT; + case "VILLAGE_DEFENSE", "SPELL" -> EntitySpawnReason.MOB_SUMMONED; + case "BREEDING", "OCELOT_BABY", "DUPLICATION", "REHYDRATION" -> EntitySpawnReason.BREEDING; + case "REINFORCEMENTS" -> EntitySpawnReason.REINFORCEMENT; + case "NETHER_PORTAL" -> EntitySpawnReason.STRUCTURE; + case "DISPENSE_EGG" -> EntitySpawnReason.DISPENSER; + case "INFECTION", "CURED", "DROWNED", "SHEARED", "PIGLIN_ZOMBIFIED", "FROZEN", + "METAMORPHOSIS" -> EntitySpawnReason.CONVERSION; + case "SHOULDER_ENTITY", "BEEHIVE" -> EntitySpawnReason.LOAD; + case "PATROL" -> EntitySpawnReason.PATROL; + case "COMMAND" -> EntitySpawnReason.COMMAND; + case "BUCKET" -> EntitySpawnReason.BUCKET; + default -> EntitySpawnReason.NATURAL; + }; + } + + private static Panda.Gene gene(String value) { + if (value == null || value.isBlank()) { + return Panda.Gene.NORMAL; + } + try { + return Panda.Gene.valueOf(value.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + return Panda.Gene.NORMAL; + } + } + + private static boolean forcePersist() { + return IrisSettings.get().getWorld().isForcePersistEntities(); + } + + private static void applyPersistence(Entity entity, boolean persistent) { + ModdedEntityPersistence.configure(entity, persistent); + if (persistent && (!entity.getType().canSerialize() || !entity.shouldBeSaved())) { + String type = String.valueOf(BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType())); + if (WARNED_PERSISTENCE_TYPES.add(type)) { + IrisLogging.warn("Iris entity: vanilla cannot persist non-serializable entity type '" + type + "'"); + } + } + } + + private static String normalizeName(String name) { + String source = name == null || name.isBlank() ? "modifier" : name; + String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_"); + return normalized.isBlank() ? "modifier" : normalized; + } + + private static String colorize(String text) { + char[] chars = text.toCharArray(); + for (int i = 0; i < chars.length - 1; i++) { + if (chars[i] == '&' && COLOR_CODES.indexOf(chars[i + 1]) > -1) { + chars[i] = '§'; + chars[i + 1] = Character.toLowerCase(chars[i + 1]); + } + } + return new String(chars); + } + + @FunctionalInterface + interface ChunkLoadedLookup { + boolean loaded(int chunkX, int chunkZ); + } + + private static final class RiseTask implements Runnable { + private final Engine engine; + private final ServerLevel level; + private final Entity entity; + private final LivingEntity living; + private final double x; + private final double z; + private final boolean originalInvulnerable; + private final boolean originalNoPhysics; + private final int originalInvulnerableTime; + private final boolean originalNoAi; + private double y; + private int moves; + private boolean restored; + + private RiseTask(Engine engine, ServerLevel level, Entity entity, LivingEntity living) { + this.engine = engine; + this.level = level; + this.entity = entity; + this.living = living; + this.x = entity.getX(); + this.y = entity.getY(); + this.z = entity.getZ(); + this.originalInvulnerable = entity.isInvulnerable(); + this.originalNoPhysics = entity.noPhysics; + this.originalInvulnerableTime = entity.invulnerableTime; + this.originalNoAi = entity instanceof Mob mob && mob.isNoAi(); + } + + private void start() { + entity.setInvulnerable(true); + entity.noPhysics = true; + entity.invulnerableTime = 100_000; + if (entity instanceof Mob mob) { + mob.setNoAi(true); + } + run(); + } + + @Override + public void run() { + if (moves >= RISE_MAX_MOVES || !entity.isAlive() || engine.isClosed() || entity.level() != level) { + restore(); + return; + } + BlockPos current = entity.blockPosition(); + if (level.getChunkSource().getChunkNow(current.getX() >> 4, current.getZ() >> 4) == null) { + restore(); + return; + } + BlockPos eye = BlockPos.containing(entity.getX(), living.getEyeY(), entity.getZ()); + if (!ModdedBlockResolution.isSolid(level.getBlockState(current)) + && !ModdedBlockResolution.isSolid(level.getBlockState(eye))) { + restore(); + return; + } + + moves++; + y += 0.1; + entity.setPos(x, y, z); + emitEffects(); + + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + IrisLogging.error("Iris could not continue an entity rise effect because the modded scheduler is unavailable."); + restore(); + return; + } + scheduler.laterGlobal(this, 1); + } + + private void emitEffects() { + BlockPos source = BlockPos.containing(entity.getX(), living.getEyeY() - 2, entity.getZ()); + BlockState state = level.getBlockState(source); + ItemParticleOption item = new ItemParticleOption(ParticleTypes.ITEM, state.getBlock().asItem()); + level.sendParticles(item, entity.getX(), living.getEyeY(), entity.getZ(), 6, + 0.2, 0.4, 0.2, 0.06); + if (RNG.r.nextDouble() < 0.2) { + level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), SoundEvents.CHORUS_FLOWER_GROW, + SoundSource.MASTER, 0.8F, 0.1F); + } + } + + private void restore() { + if (restored) { + return; + } + restored = true; + entity.invulnerableTime = originalInvulnerableTime; + entity.noPhysics = originalNoPhysics; + entity.setInvulnerable(originalInvulnerable); + if (entity instanceof Mob mob) { + mob.setNoAi(originalNoAi); + } + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityType.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityType.java new file mode 100644 index 000000000..7d4c28b35 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntityType.java @@ -0,0 +1,64 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.PlatformEntityType; +import net.minecraft.world.entity.EntityType; + +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedEntityType implements PlatformEntityType { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + + private final EntityType type; + private final String key; + private final String namespace; + + private ModdedEntityType(EntityType type, String key) { + this.type = type; + this.key = key; + int colon = key.indexOf(':'); + this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft"; + } + + public static ModdedEntityType of(EntityType type, String key) { + return CACHE.computeIfAbsent(key, (String k) -> new ModdedEntityType(type, k)); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public String spawnCategory() { + return type.getCategory().getSerializedName().toLowerCase(Locale.ROOT); + } + + @Override + public Object nativeHandle() { + return type; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedForcedDatapack.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedForcedDatapack.java new file mode 100644 index 000000000..d1de90c32 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedForcedDatapack.java @@ -0,0 +1,746 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.core.nms.datapack.IDataFixer; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.core.pack.PackValidationResult; +import art.arcane.iris.core.pack.PackValidator; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionType; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KSet; +import net.minecraft.network.chat.Component; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.PathPackResources; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.server.packs.repository.RepositorySource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public final class ModdedForcedDatapack { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final String PACK_ID = "iris_worldgen"; + private static final String PACK_FOLDER = "iris"; + private static final String HASH_FILE_NAME = "packs.hash"; + // Bump this whenever the emitted datapack content changes for reasons the pack-directory hash cannot see. + // v2: custom biomes now inherit their vanilla derivative's biome tags, so every already-published pack has + // to regenerate once. + private static final String HASH_SALT = "iris-forced-datapack-v2"; + private static final long PACKS_HASH_TTL_NANOS = 2_000_000_000L; + private static final Object LOCK = new Object(); + private static final AtomicBoolean LOADED = new AtomicBoolean(false); + private static final AtomicBoolean STALE_SERVE_LOGGED = new AtomicBoolean(false); + private static volatile PublishedState published; + private static volatile HashMemo packsHashMemo; + + private ModdedForcedDatapack() { + } + + public static RepositorySource repositorySource() { + return (Consumer consumer) -> { + Pack pack = servePack(); + consumer.accept(pack); + LOADED.set(true); + }; + } + + /** + * Serves the published datapack without regenerating it whenever the installed packs still hash to what + * was generated last. That fast path is lock-free; everything else takes LOCK and rechecks, so a boot-time + * daemon regeneration and a loadPacks regeneration can never stage concurrently (publishDirectory moves the + * live directory aside, which would break a concurrent read). + * + *

A hash mismatch is never served: the HASH_SALT bump alone mismatches every install on its first boot + * after an upgrade, and serving that directory hands Create World a pack without the current biome tags. + * A published pack whose hash cannot be computed at all is still served, with one warning. + */ + private static Pack servePack() { + String currentHash = packsHashOrEmpty(); + PublishedState current = publishedState(); + if (current != null && !currentHash.isEmpty() && current.packsHash().equals(currentHash)) { + try { + return requireReadablePack(current.directory()); + } catch (RuntimeException unreadable) { + published = null; + LOGGER.error("Iris could not read the published forced datapack at {}; regenerating", + current.directory(), unreadable); + } + } + synchronized (LOCK) { + String hash = packsHashOrEmpty(); + PublishedState state = publishedState(); + String reason; + if (state == null) { + reason = "no published pack"; + } else if (!hash.isEmpty() && !state.packsHash().equals(hash)) { + reason = "stale cache (hash changed)"; + } else { + if (hash.isEmpty() && STALE_SERVE_LOGGED.compareAndSet(false, true)) { + LOGGER.warn("Iris cannot hash the installed packs; serving the last generated forced datapack from {} unverified", + state.directory()); + } + try { + return requireReadablePack(state.directory()); + } catch (RuntimeException unreadable) { + published = null; + LOGGER.error("Iris could not read the published forced datapack at {}; regenerating", + state.directory(), unreadable); + } + reason = "unreadable published pack"; + } + LOGGER.info("Iris forced datapack cache is unusable ({}); generating it once now", reason); + return buildPack(); + } + } + + public static void verifyInjected() { + if (LOADED.get()) { + return; + } + Path packsRoot = packsRoot(); + List packs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot.toFile()); + if (packs.isEmpty()) { + return; + } + LOGGER.error("==============================================================="); + LOGGER.error("Iris forced datapack '{}' was never loaded by this server.", PACK_ID); + LOGGER.error("{} installed pack(s) at {} contributed no dimension types or custom biomes.", packs.size(), packsRoot); + LOGGER.error("Datapack source injection failed for this loader (mixin/event not applied), so world creation will fail and restarting will not fix it."); + LOGGER.error("==============================================================="); + } + + public static Path datapackRoot() { + return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("generated").resolve("datapack"); + } + + private static Path packDirectory() { + return datapackRoot().resolve(PACK_FOLDER); + } + + private static Pack buildPack() { + try { + return requireReadablePack(regenerate()); + } catch (RuntimeException | Error generationFailure) { + Path lastKnownGood = packDirectory(); + if (Files.isRegularFile(lastKnownGood.resolve("pack.mcmeta"))) { + LOGGER.error("Iris kept the last known-good generated datapack after regeneration failed", + generationFailure); + return requireReadablePack(lastKnownGood); + } + throw generationFailure; + } + } + + private static Pack requireReadablePack(Path directory) { + PackLocationInfo location = new PackLocationInfo( + PACK_ID, + Component.literal(IrisLanguage.plain(RuntimeUiMessages.FORCED_DATAPACK_NAME)), + PackSource.BUILT_IN, + Optional.empty()); + PackSelectionConfig selection = new PackSelectionConfig(true, Pack.Position.TOP, true); + PathPackResources.PathResourcesSupplier supplier = new PathPackResources.PathResourcesSupplier(directory); + Pack pack = Pack.readMetaAndCreate(location, supplier, PackType.SERVER_DATA, selection); + if (pack == null) { + throw new IllegalStateException("Iris forced datapack at " + directory + + " produced no readable pack metadata"); + } + return pack; + } + + public static Path regenerate() { + synchronized (LOCK) { + try { + return write(); + } catch (Throwable e) { + LOGGER.error("Iris failed to generate the forced startup datapack", e); + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (e instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Iris failed to generate the forced startup datapack", e); + } + } + } + + /** + * Regenerates only when the installed packs no longer hash to the published datapack. Used by the boot + * trigger so a steady-state restart does not pay the full staging cost. + */ + public static boolean regenerateIfStale(String reason) { + synchronized (LOCK) { + String currentHash = packsHashOrEmpty(); + PublishedState state = publishedState(); + if (state != null && !currentHash.isEmpty() && state.packsHash().equals(currentHash)) { + LOGGER.debug("Iris forced datapack is current ({}); skipping regeneration", reason); + return false; + } + LOGGER.info("Iris regenerating the forced datapack ({})", reason); + regenerate(); + return true; + } + } + + /** + * Off-thread regeneration trigger. Call sites that run on the server thread must use this so a command + * or lifecycle hook never blocks on pack staging. + */ + public static void scheduleRegeneration(String reason) { + Runnable task = () -> { + try { + regenerateIfStale(reason); + } catch (Throwable failure) { + LOGGER.error("Iris forced datapack regeneration failed ({})", reason, failure); + } + }; + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler != null) { + scheduler.async(task); + return; + } + Thread thread = new Thread(task, "iris-modded-datapack-regen"); + thread.setDaemon(true); + thread.start(); + } + + private static Path write() throws IOException { + String packsHash = packsHash(); + Path datapackRoot = datapackRoot(); + Files.createDirectories(datapackRoot); + Path stagingDirectory = Files.createTempDirectory(datapackRoot, PACK_FOLDER + ".staging-"); + try { + writeStagedPack(stagingDirectory); + requireReadablePack(stagingDirectory); + publishDirectory(stagingDirectory, packDirectory()); + writePublishedHash(packsHash); + published = new PublishedState(packDirectory(), packsHash); + // Publish the hash this run was built from as the memo too: a memo captured before staging would + // otherwise mismatch what was just published and send the next serve straight back into buildPack. + packsHashMemo = new HashMemo(packsHash, System.nanoTime()); + STALE_SERVE_LOGGED.set(false); + return packDirectory(); + } catch (IOException | RuntimeException | Error failure) { + try { + clean(stagingDirectory); + } catch (Throwable cleanupError) { + failure.addSuppressed(cleanupError); + } + throw failure; + } + } + + private static PublishedState publishedState() { + PublishedState current = published; + if (current != null) { + return current; + } + Path directory = packDirectory(); + if (!Files.isRegularFile(directory.resolve("pack.mcmeta"))) { + return null; + } + PublishedState loaded = new PublishedState(directory, readPublishedHash()); + published = loaded; + return loaded; + } + + private static String readPublishedHash() { + Path hashFile = datapackRoot().resolve(HASH_FILE_NAME); + if (!Files.isRegularFile(hashFile)) { + return ""; + } + try { + return Files.readString(hashFile, StandardCharsets.UTF_8).trim(); + } catch (IOException unreadable) { + LOGGER.warn("Iris could not read the forced datapack hash at {}", hashFile, unreadable); + return ""; + } + } + + private static void writePublishedHash(String hash) throws IOException { + Path hashFile = datapackRoot().resolve(HASH_FILE_NAME); + Path temp = hashFile.resolveSibling(HASH_FILE_NAME + ".tmp-" + UUID.randomUUID()); + Files.writeString(temp, hash, StandardCharsets.UTF_8); + try { + Files.move(temp, hashFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException atomicUnsupported) { + Files.move(temp, hashFile, StandardCopyOption.REPLACE_EXISTING); + } + } + + /** + * Short-TTL memo: loadPacks runs on every PackRepository reload and the hash walks every installed pack + * file (thousands on a studio install), so back-to-back reloads must not re-walk the tree. The window is + * small enough that an operator dropping in a pack still gets picked up on the next reload. + */ + private static String packsHashOrEmpty() { + long now = System.nanoTime(); + HashMemo memo = packsHashMemo; + if (memo != null && now - memo.takenAtNanos() < PACKS_HASH_TTL_NANOS) { + return memo.hash(); + } + String hash; + try { + hash = packsHash(); + } catch (IOException | RuntimeException failure) { + LOGGER.warn("Iris could not hash the installed packs directory", failure); + hash = ""; + } + packsHashMemo = new HashMemo(hash, now); + return hash; + } + + /** + * Content hash over the installed packs: relative path, size and mtime of every regular file, plus the + * pack format and loader the generated datapack is shaped for. + */ + private static String packsHash() throws IOException { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException missing) { + throw new IllegalStateException("SHA-256 is unavailable", missing); + } + digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName() + + '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8)); + Path root = packsRoot(); + if (Files.isDirectory(root)) { + List entries = new ArrayList<>(); + Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) { + return !directory.equals(root) + && PackDirectoryResolver.isHiddenName(directory.getFileName().toString()) + ? FileVisitResult.SKIP_SUBTREE + : FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) { + if (attributes.isRegularFile()) { + entries.add(root.relativize(file).toString().replace('\\', '/') + + '|' + attributes.size() + '|' + attributes.lastModifiedTime().toMillis()); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException failure) { + entries.add(root.relativize(file).toString().replace('\\', '/') + "|unreadable"); + return FileVisitResult.CONTINUE; + } + }); + entries.sort(Comparator.naturalOrder()); + for (String entry : entries) { + digest.update(entry.getBytes(StandardCharsets.UTF_8)); + digest.update((byte) '\n'); + } + } + return HexFormat.of().formatHex(digest.digest()); + } + + private static void writeStagedPack(Path stagingDirectory) throws IOException { + Map> seenBiomes = new LinkedHashMap<>(); + IDataFixer fixer = DataVersion.getLatest().get(); + + int packCount = 0; + KList presetIds = new KList<>(); + File root = packsRoot().toFile(); + List packs = PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(root); + for (File pack : packs) { + if (stagePack(pack, fixer, stagingDirectory, seenBiomes, presetIds)) { + packCount++; + } + } + + writePackMeta(stagingDirectory); + // Forge only: FML has no block-drops event Iris can use, so drops are routed through a global loot + // modifier. NeoForge does not need one - IrisNeoForgeBootstrap listens to BlockDropsEvent and both + // replaces and appends drops there, so emitting a modifier would double-apply them. + if ("forge".equalsIgnoreCase(ModdedEngineBootstrap.loader().platformName())) { + writeForgeBlockLootModifier(stagingDirectory); + } + if (!presetIds.isEmpty()) { + writeWorldPresetTag(stagingDirectory, presetIds); + } + LOGGER.info("Iris forced startup datapack staged: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), stagingDirectory); + if (packCount == 0) { + LOGGER.warn("Iris installed NO worldgen packs into the forced datapack - custom biomes and their colors will NOT generate. Install a pack with /iris download pack=overworld, /iris download pack=underworld, or /iris download link=, then restart before creating an Iris world."); + } + } + + private static boolean stagePack(File sourcePack, IDataFixer fixer, Path stagingDirectory, + Map> seenBiomes, + KList presetIds) throws IOException { + PackValidationResult validation; + try { + validation = PackValidator.validateForDatapackBootstrap(sourcePack); + } catch (Throwable validationFailure) { + LOGGER.error("Iris excluded pack '{}' from Create World because validation failed", + sourcePack.getName(), validationFailure); + rethrowIfUnrecoverable(validationFailure); + return false; + } + if (!validation.isLoadable()) { + LOGGER.error("Iris excluded pack '{}' from Create World: {} blocking validation error(s); first error: {}", + sourcePack.getName(), validation.getBlockingErrors().size(), + validation.getBlockingErrors().getFirst()); + return false; + } + + Path packStagingDirectory = Files.createTempDirectory( + stagingDirectory.getParent(), PACK_FOLDER + ".pack-" + sourcePack.getName() + "-"); + Map> packBiomes = new LinkedHashMap<>(); + KList packPresetIds = new KList<>(); + KList packFolders = new KList<>(); + packFolders.add(packStagingDirectory.toFile()); + boolean installed; + try { + installed = installPack(sourcePack, fixer, packFolders, packBiomes, packPresetIds); + } catch (Throwable installationFailure) { + LOGGER.error("Iris excluded pack '{}' from Create World because datapack serialization failed", + sourcePack.getName(), installationFailure); + rethrowIfUnrecoverable(installationFailure); + installed = false; + } + + try { + if (!installed) { + return false; + } + mergeDirectory(packStagingDirectory, stagingDirectory); + mergeBiomes(seenBiomes, packBiomes); + presetIds.addAll(packPresetIds); + return true; + } finally { + try { + clean(packStagingDirectory); + } catch (Throwable cleanupFailure) { + LOGGER.warn("Iris could not remove temporary datapack staging for pack '{}'", + sourcePack.getName(), cleanupFailure); + } + } + } + + /** + * Per-pack staging isolates every failure so one broken pack cannot brick Create World for all of them. + * Only a VM-level failure (heap exhausted, native stack blown) is rethrown; LinkageError and + * ExceptionInInitializerError are exactly the per-pack failures that must stay contained. + */ + private static void rethrowIfUnrecoverable(Throwable failure) { + if (failure instanceof OutOfMemoryError outOfMemory) { + throw outOfMemory; + } + if (failure instanceof StackOverflowError stackOverflow) { + throw stackOverflow; + } + } + + private static void mergeDirectory(Path sourceDirectory, Path destinationDirectory) throws IOException { + List entries = new ArrayList<>(); + try (Stream walk = Files.walk(sourceDirectory)) { + walk.sorted(Comparator.comparingInt(Path::getNameCount)).forEach(entries::add); + } + for (Path source : entries) { + Path relative = sourceDirectory.relativize(source); + Path destination = destinationDirectory.resolve(relative); + if (Files.isDirectory(source)) { + Files.createDirectories(destination); + } else if (!Files.exists(destination)) { + Files.copy(source, destination); + } + } + } + + private static void mergeBiomes(Map> destination, + Map> source) { + for (Map.Entry> entry : source.entrySet()) { + destination.computeIfAbsent(entry.getKey(), ignored -> new KSet<>()) + .addAll(entry.getValue()); + } + } + + private static boolean installPack(File packFolder, IDataFixer fixer, KList folders, + Map> seenBiomes, + KList presetIds) throws IOException { + String packName = packFolder.getName(); + File dimensionsDirectory = new File(packFolder, "dimensions"); + if (!dimensionsDirectory.isDirectory()) { + return false; + } + File[] dimensionFiles = dimensionsDirectory.listFiles( + (File file) -> file.isFile() && file.getName().endsWith(".json")); + if (dimensionFiles == null) { + throw new IOException("Iris could not read dimensions for pack '" + packName + "'"); + } + if (dimensionFiles.length == 0) { + return false; + } + Arrays.sort(dimensionFiles, Comparator.comparing(File::getName)); + IrisData data = IrisData.get(packFolder); + for (File dimensionFile : dimensionFiles) { + String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length()); + IrisDimension dimension = data.getDimensionLoader().load(dimensionKey); + if (dimension == null) { + throw new IllegalStateException("Iris pack '" + packName + "' dimension '" + + dimensionKey + "' did not load while building the forced datapack"); + } + String biomePathPrefix = ModdedWorldgenIds.biomePathPrefix(packName, dimensionKey); + dimension.installBiomes(fixer, () -> data, folders, "irisworldgen", biomePathPrefix, + biomesForNamespace(seenBiomes, biomePathPrefix)); + dimension.installBiomes(fixer, () -> data, folders, + biomesForNamespace(seenBiomes, dimension.getLoadKey())); + writeDimensionType(folders, fixer, dimension, packName, dimensionKey); + String presetRef = ModdedWorldgenIds.presetRef(packName, dimensionKey); + writeWorldPreset(folders, packName, dimensionKey, presetRef); + presetIds.add(presetRef); + } + return true; + } + + static KSet biomesForNamespace(Map> biomes, String namespace) { + return biomes.computeIfAbsent(namespace, ignored -> new KSet<>()); + } + + static T requireRegisteredDimensionType(String typeRef, Optional registeredType, + String pack, String packDimensionKey) { + return registeredType.orElseThrow(() -> new IllegalStateException( + "Iris dimension type '" + typeRef + "' for pack '" + pack + "' dimension '" + + packDimensionKey + "' is not loaded. Restart the server so the forced Iris datapack registers it before creating the world." + + (LOADED.get() ? "" : " The forced Iris datapack has not been loaded by this server at all" + + " (datapack source injection failed; see the Iris boot ERROR), so a restart alone will not register it."))); + } + + private static void writeWorldPreset(KList folders, String packName, String dimensionKey, + String presetRef) throws IOException { + String dimensionRef = dimensionKey.equals(packName) ? packName : packName + ":" + dimensionKey; + String json = worldPresetJson(dimensionRef, + ModdedWorldgenIds.dimensionTypeRef(packName, dimensionKey)); + String presetPath = presetRef.substring(presetRef.indexOf(':') + 1); + for (File datapackRoot : folders) { + Path output = datapackRoot.toPath().resolve("data").resolve("irisworldgen") + .resolve("worldgen").resolve("world_preset").resolve(presetPath + ".json"); + Files.createDirectories(output.getParent()); + Files.writeString(output, json, StandardCharsets.UTF_8); + } + } + + private static String worldPresetJson(String dimensionRef, String dimensionTypeRef) { + return "{\n" + + " \"dimensions\": {\n" + + " \"minecraft:overworld\": {\n" + + " \"type\": \"" + dimensionTypeRef + "\",\n" + + " \"generator\": {\n" + + " \"type\": \"irisworldgen:iris\",\n" + + " \"biome_source\": {\n" + + " \"type\": \"minecraft:fixed\",\n" + + " \"biome\": \"minecraft:plains\"\n" + + " },\n" + + " \"dimension\": \"" + dimensionRef + "\"\n" + + " }\n" + + " },\n" + + " \"minecraft:the_nether\": {\n" + + " \"type\": \"minecraft:the_nether\",\n" + + " \"generator\": {\n" + + " \"type\": \"minecraft:noise\",\n" + + " \"settings\": \"minecraft:nether\",\n" + + " \"biome_source\": {\n" + + " \"type\": \"minecraft:multi_noise\",\n" + + " \"preset\": \"minecraft:nether\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"minecraft:the_end\": {\n" + + " \"type\": \"minecraft:the_end\",\n" + + " \"generator\": {\n" + + " \"type\": \"minecraft:noise\",\n" + + " \"settings\": \"minecraft:end\",\n" + + " \"biome_source\": {\n" + + " \"type\": \"minecraft:the_end\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n"; + } + + private static void writeWorldPresetTag(Path packDirectory, KList presetIds) throws IOException { + StringBuilder values = new StringBuilder(); + for (int i = 0; i < presetIds.size(); i++) { + if (i > 0) { + values.append(",\n"); + } + values.append(" \"").append(presetIds.get(i)).append("\""); + } + String json = "{\n" + + " \"replace\": false,\n" + + " \"values\": [\n" + + values + + "\n ]\n" + + "}\n"; + Path output = packDirectory.resolve("data").resolve("minecraft").resolve("tags").resolve("worldgen").resolve("world_preset").resolve("normal.json"); + Files.createDirectories(output.getParent()); + Files.writeString(output, json, StandardCharsets.UTF_8); + } + + static void writeDimensionType(KList folders, IDataFixer fixer, IrisDimension dimension, + String pack, String packDimensionKey) throws IOException { + IrisDimensionType type = dimension.getDimensionType(); + String json = type.toJson(fixer); + String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey); + String typePath = typeRef.substring(typeRef.indexOf(':') + 1); + for (File datapackRoot : folders) { + Path output = datapackRoot.toPath().resolve("data").resolve("irisworldgen") + .resolve("dimension_type").resolve(typePath + ".json"); + Files.createDirectories(output.getParent()); + Files.writeString(output, json, StandardCharsets.UTF_8); + // Load-bearing, not dead: worlds created before the scoped pack path reference + // irisworldgen: in their level.dat, and ModdedWorldEngines accepts that legacy + // key when validating the runtime dimension contract. Removing this emission unloads those worlds. + Path legacyOutput = datapackRoot.toPath().resolve("data").resolve("irisworldgen") + .resolve("dimension_type").resolve(dimension.getDimensionTypeKey() + ".json"); + if (!Files.exists(legacyOutput)) { + Files.createDirectories(legacyOutput.getParent()); + Files.writeString(legacyOutput, json, StandardCharsets.UTF_8); + } + } + } + + static void writeForgeBlockLootModifier(Path packDirectory) throws IOException { + Path list = packDirectory.resolve("data").resolve("forge").resolve("loot_modifiers").resolve("global_loot_modifiers.json"); + Files.createDirectories(list.getParent()); + Files.writeString(list, "{\n" + + " \"replace\": false,\n" + + " \"entries\": [\"irisworldgen:block_drops\"]\n" + + "}\n", StandardCharsets.UTF_8); + + Path modifier = packDirectory.resolve("data").resolve("irisworldgen").resolve("loot_modifiers").resolve("block_drops.json"); + Files.createDirectories(modifier.getParent()); + Files.writeString(modifier, "{\n" + + " \"type\": \"irisworldgen:block_drops\",\n" + + " \"conditions\": []\n" + + "}\n", StandardCharsets.UTF_8); + } + + private static void writePackMeta(Path packDirectory) throws IOException { + int packFormat = DataVersion.getLatest().getPackFormat(); + String json = "{\n" + + " \"pack\": {\n" + + " \"description\": \"Iris world generation biomes and dimension types for installed packs.\",\n" + + " \"pack_format\": " + packFormat + ",\n" + + " \"min_format\": " + packFormat + ",\n" + + " \"max_format\": " + packFormat + "\n" + + " }\n" + + "}\n"; + Files.writeString(packDirectory.resolve("pack.mcmeta"), json, StandardCharsets.UTF_8); + } + + private static int countBiomes(Map> biomes) { + int count = 0; + for (KSet values : biomes.values()) { + count += values.size(); + } + return count; + } + + private static void clean(Path packDirectory) throws IOException { + if (!Files.exists(packDirectory)) { + return; + } + List entries = new ArrayList<>(); + try (Stream walk = Files.walk(packDirectory)) { + walk.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).forEach(entries::add); + } + for (Path entry : entries) { + Files.deleteIfExists(entry); + } + } + + static void publishDirectory(Path stagingDirectory, Path publishedDirectory) throws IOException { + Path backupDirectory = publishedDirectory.resolveSibling( + publishedDirectory.getFileName() + ".backup-" + UUID.randomUUID()); + boolean hadPublishedDirectory = Files.exists(publishedDirectory); + if (hadPublishedDirectory) { + Files.move(publishedDirectory, backupDirectory, StandardCopyOption.ATOMIC_MOVE); + } + try { + Files.move(stagingDirectory, publishedDirectory, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException | RuntimeException | Error failure) { + if (hadPublishedDirectory) { + try { + Files.move(backupDirectory, publishedDirectory, StandardCopyOption.ATOMIC_MOVE); + } catch (Throwable rollbackError) { + failure.addSuppressed(rollbackError); + } + } + throw failure; + } + if (hadPublishedDirectory) { + try { + clean(backupDirectory); + } catch (Throwable cleanupError) { + LOGGER.warn("Iris published the forced datapack but could not remove backup {}", + backupDirectory, cleanupError); + } + } + } + + private static Path packsRoot() { + return IrisPlatforms.get().packsFolderNoCreate().toPath(); + } + + private record PublishedState(Path directory, String packsHash) { + } + + private record HashMemo(String hash, long takenAtNanos) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenPool.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenPool.java new file mode 100644 index 000000000..c8d612ed3 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenPool.java @@ -0,0 +1,360 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Owns the Iris generation pool and the single decision of whether this loader already runs chunk + * generation on its own worker threads. + * + *

The parallel-chunk-system probe is a two-stage check: {@link Class#forName} presence gates it + * (never a version string), then the mod's own configuration is read reflectively to confirm the + * feature is actually enabled. Any reflection failure resolves to "not parallel", which keeps + * generation on the Iris pool - the safe side, since an extra hop only costs throughput while a + * missing hop serializes generation on the loader's chunk threads. + */ +public final class ModdedGenPool { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final long SHUTDOWN_DRAIN_MILLIS = 2_000L; + private static final String[] C2ME_MARKERS = { + "com.ishland.c2me.base.ModProperties", + "com.ishland.c2me.base.common.config.C2MEConfig", + "com.ishland.c2me.opts.chunkio.ModProperties" + }; + private static final String C2ME_CONFIG = "com.ishland.c2me.base.common.config.C2MEConfig"; + private static final String[] C2ME_SECTIONS = {"asyncScheduling", "asyncSchedulingConfig", "threadedWorldGen"}; + private static final String[] C2ME_FLAGS = {"enabled", "isEnabled", "shouldEnable"}; + private static final String MOONRISE_MARKER = "ca.spottedleaf.moonrise.common.util.MoonriseCommon"; + private static final String[] MOONRISE_WORKER_COUNTS = {"getWorkerThreads", "workerThreads"}; + private static final String[] MOONRISE_POOLS = {"WORKER_POOL", "workerPool"}; + private static final String[] MOONRISE_POOL_COUNTS = {"getCoreThreads", "getThreadCount", "getThreads", "coreThreads", "threadCount"}; + + private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger(); + private static final ChunkSystem CHUNK_SYSTEM = detectChunkSystem(); + private static final AtomicReference GEN_POOL = new AtomicReference<>(createGenPool()); + + private ModdedGenPool() { + } + + /** + * True when the loader's chunk system already generates off the server thread, so Iris must not + * add its own pool hop. + */ + public static boolean parallelChunkSystem() { + return CHUNK_SYSTEM.parallel(); + } + + /** + * Terse description of the detected chunk system, for one-line diagnostics. + */ + public static String describeChunkSystem() { + return CHUNK_SYSTEM.description(); + } + + static ExecutorService pool() { + ExecutorService pool = GEN_POOL.get(); + if (pool != null && !pool.isShutdown()) { + return pool; + } + start(); + ExecutorService restarted = GEN_POOL.get(); + if (restarted == null) { + throw new RejectedExecutionException("Iris gen pool is shut down"); + } + return restarted; + } + + static void start() { + while (true) { + ExecutorService current = GEN_POOL.get(); + if (current != null && !current.isShutdown()) { + return; + } + ExecutorService created = createGenPool(); + if (GEN_POOL.compareAndSet(current, created)) { + return; + } + created.shutdownNow(); + } + } + + static void shutdown() { + ExecutorService pool = GEN_POOL.getAndSet(null); + if (pool == null) { + return; + } + pool.shutdown(); + try { + if (pool.awaitTermination(SHUTDOWN_DRAIN_MILLIS, TimeUnit.MILLISECONDS)) { + return; + } + LOGGER.debug("Iris gen pool did not drain in {}ms, forcing shutdown", SHUTDOWN_DRAIN_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + pool.shutdownNow(); + } + + private static ChunkSystem detectChunkSystem() { + ChunkSystem detected = probeC2ME(); + if (detected == null) { + detected = probeMoonrise(); + } + if (detected == null) { + detected = new ChunkSystem(false, "vanilla"); + } + LOGGER.info("Iris chunk system: {} (parallel={}, generation on {})", + detected.description(), + detected.parallel() ? "yes" : "no", + detected.parallel() ? "loader threads" : "Iris gen pool"); + return detected; + } + + private static ChunkSystem probeC2ME() { + if (!anyPresent(C2ME_MARKERS)) { + return null; + } + Class config = loadOrNull(C2ME_CONFIG); + if (config == null) { + return new ChunkSystem(false, "c2me present, config class missing"); + } + Boolean enabled = readSectionFlag(config, C2ME_SECTIONS, C2ME_FLAGS); + if (enabled == null) { + return new ChunkSystem(false, "c2me present, async scheduling unreadable"); + } + return new ChunkSystem(enabled, enabled ? "c2me async scheduling on" : "c2me async scheduling off"); + } + + private static ChunkSystem probeMoonrise() { + Class marker = loadOrNull(MOONRISE_MARKER); + if (marker == null) { + return null; + } + Integer workers = readMoonriseWorkers(marker); + if (workers == null) { + return new ChunkSystem(false, "moonrise present, worker pool unreadable"); + } + if (workers <= 0) { + return new ChunkSystem(false, "moonrise worker pool empty"); + } + return new ChunkSystem(true, "moonrise workers=" + workers); + } + + /** + * Reads {@code .

.} where the section may itself be the boolean. + * Returns null when nothing along the path is readable. + */ + private static Boolean readSectionFlag(Class configClass, String[] sections, String[] flags) { + for (String section : sections) { + Field field = staticFieldOrNull(configClass, section); + if (field == null) { + continue; + } + Object value; + try { + value = field.get(null); + } catch (Throwable e) { + LOGGER.debug("Iris chunk system probe could not read {}.{}: {}", configClass.getName(), section, e.toString()); + continue; + } + if (value instanceof Boolean flag) { + return flag; + } + if (value == null) { + continue; + } + Boolean nested = readBooleanMember(value, flags); + if (nested != null) { + return nested; + } + } + return null; + } + + private static Boolean readBooleanMember(Object owner, String[] names) { + for (String name : names) { + for (Class type = owner.getClass(); type != null && type != Object.class; type = type.getSuperclass()) { + Field field = declaredFieldOrNull(type, name); + if (field != null && (field.getType() == boolean.class || field.getType() == Boolean.class)) { + try { + if (field.get(owner) instanceof Boolean flag) { + return flag; + } + } catch (Throwable e) { + LOGGER.debug("Iris chunk system probe could not read {}.{}: {}", type.getName(), name, e.toString()); + } + } + Method method = declaredMethodOrNull(type, name); + if (method != null && (method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class)) { + try { + if (method.invoke(owner) instanceof Boolean flag) { + return flag; + } + } catch (Throwable e) { + LOGGER.debug("Iris chunk system probe could not call {}.{}(): {}", type.getName(), name, e.toString()); + } + } + } + } + return null; + } + + private static Integer readMoonriseWorkers(Class marker) { + for (String name : MOONRISE_WORKER_COUNTS) { + Integer direct = readIntMember(marker, null, name); + if (direct != null) { + return direct; + } + Field field = staticFieldOrNull(marker, name); + if (field != null) { + try { + Object value = field.get(null); + if (value instanceof Number number) { + return number.intValue(); + } + } catch (Throwable e) { + LOGGER.debug("Iris chunk system probe could not read {}.{}: {}", marker.getName(), name, e.toString()); + } + } + } + for (String poolName : MOONRISE_POOLS) { + Field field = staticFieldOrNull(marker, poolName); + if (field == null) { + continue; + } + Object pool; + try { + pool = field.get(null); + } catch (Throwable e) { + LOGGER.debug("Iris chunk system probe could not read {}.{}: {}", marker.getName(), poolName, e.toString()); + continue; + } + if (pool == null) { + continue; + } + for (String countName : MOONRISE_POOL_COUNTS) { + Integer count = readIntMember(pool.getClass(), pool, countName); + if (count != null) { + return count; + } + } + } + return null; + } + + private static Integer readIntMember(Class type, Object owner, String name) { + for (Class current = type; current != null && current != Object.class; current = current.getSuperclass()) { + Method method = declaredMethodOrNull(current, name); + if (method == null) { + continue; + } + try { + if (method.invoke(owner) instanceof Number number) { + return number.intValue(); + } + } catch (Throwable e) { + LOGGER.debug("Iris chunk system probe could not call {}.{}(): {}", current.getName(), name, e.toString()); + } + } + return null; + } + + private static Field staticFieldOrNull(Class type, String name) { + for (Class current = type; current != null && current != Object.class; current = current.getSuperclass()) { + Field field = declaredFieldOrNull(current, name); + if (field != null) { + return field; + } + } + return null; + } + + private static Field declaredFieldOrNull(Class type, String name) { + try { + Field field = type.getDeclaredField(name); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException e) { + return null; + } catch (Throwable e) { + LOGGER.debug("Iris chunk system probe could not access field {}.{}: {}", type.getName(), name, e.toString()); + return null; + } + } + + private static Method declaredMethodOrNull(Class type, String name) { + try { + Method method = type.getDeclaredMethod(name); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException e) { + return null; + } catch (Throwable e) { + LOGGER.debug("Iris chunk system probe could not access method {}.{}(): {}", type.getName(), name, e.toString()); + return null; + } + } + + private static boolean anyPresent(String[] markers) { + for (String marker : markers) { + if (loadOrNull(marker) != null) { + return true; + } + } + return false; + } + + private static Class loadOrNull(String name) { + try { + return Class.forName(name, false, ModdedGenPool.class.getClassLoader()); + } catch (Throwable e) { + LOGGER.debug("Iris chunk system probe: {} absent ({})", name, e.getClass().getSimpleName()); + return null; + } + } + + private static ExecutorService createGenPool() { + int threads = Math.max(2, Runtime.getRuntime().availableProcessors()); + ThreadPoolExecutor pool = new ThreadPoolExecutor( + threads, threads, 30L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + runnable -> { + Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet()); + thread.setDaemon(true); + return thread; + }); + pool.allowCoreThreadTimeOut(true); + return pool; + } + + private record ChunkSystem(boolean parallel, String description) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedHeightmaps.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedHeightmaps.java new file mode 100644 index 000000000..a9e4d1fbd --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedHeightmaps.java @@ -0,0 +1,22 @@ +package art.arcane.iris.modded; + +import net.minecraft.util.Mth; +import net.minecraft.util.SimpleBitStorage; + +import java.util.function.IntBinaryOperator; + +final class ModdedHeightmaps { + private ModdedHeightmaps() { + } + + static long[] terrainRawData(int height, IntBinaryOperator heightResolver) { + SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), 256); + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + int value = Mth.clamp(heightResolver.applyAsInt(x, z), 0, height); + storage.set(x + z * 16, value); + } + } + return storage.getRaw(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java new file mode 100644 index 000000000..b75edfd28 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java @@ -0,0 +1,435 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.GenerationSessionLease; +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.IrisImportedFeatureControl; +import art.arcane.iris.util.project.context.IrisContext; +import it.unimi.dsi.fastutil.ints.IntArraySet; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.FeatureSorter; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Native placed-feature passthrough for one Iris dimension, gated on {@code importedFeatures.enabled}. + * + *

This runs the FEATURES half of vanilla's decoration pass and nothing else. Iris places native structures + * itself, with its own vertical fitting and vegetation clearing, so calling {@code super.applyBiomeDecoration} + * would place every structure a second time. The feature half is reproduced here off the same decoration and + * feature seeds vanilla derives, so an imported feature lands where vanilla would have put it. + * + *

Threading: {@link #run} runs on the worldgen thread that is generating the chunk, never on + * {@link ModdedGenPool}. The FEATURES chunk step is not parallel-safe - it writes into the eight neighbouring + * chunks through {@code WorldGenLevel}, and vanilla and every threaded chunk system serialize it. Terrain is + * the only Iris step that may fan out. + * + *

Everything here is inert while the control is disabled: no table is built, no registry is walked, and + * {@link #generationSettings} answers exactly what vanilla's default getter answers. + */ +final class ModdedImportedFeatureStage { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final String CYCLE_MARKER = "Feature order cycle found"; + private static final long NO_GENERATION = Long.MIN_VALUE; + + private final IrisModdedBiomeSource biomeSource; + private final ReentrantLock buildLock = new ReentrantLock(); + private volatile IrisModdedChunkGenerator generator; + private volatile FeatureTable featureTable; + private volatile long inertGeneration = NO_GENERATION; + + ModdedImportedFeatureStage(IrisModdedBiomeSource biomeSource) { + this.biomeSource = biomeSource; + } + + void bind(IrisModdedChunkGenerator generator) { + this.generator = generator; + } + + /** + * Drops the feature table. Called from every repoint, hotload and unbind path so a table built for one + * pack can never serve another. + */ + void invalidate() { + featureTable = null; + inertGeneration = NO_GENERATION; + } + + boolean active() { + return featureTable != null; + } + + /** + * The generation-settings getter handed to {@code ChunkGenerator}'s two-argument constructor. Maps an Iris + * custom biome holder onto the generation settings of the vanilla biome its Iris biome derives from, so + * the per-step feature lists and {@code BiomeFilter}'s hasFeature gate both see real features for a biome + * whose datapack JSON declares none by design. Real registry biomes pass straight through. + * + *

With {@code importedFeatures} disabled there is no table and this is vanilla's default getter. + */ + BiomeGenerationSettings generationSettings(Holder biome) { + FeatureTable table = featureTable; + if (table == null) { + return biome.value().getGenerationSettings(); + } + return settingsFor(biome, table.derivatives()); + } + + /** + * Chunk-path prepare. The volatile fast path is unlocked, so a prepared stage costs two reads per chunk; + * the build itself is serialized because {@code applyBiomeDecoration} calls this from every worldgen + * thread, and two threads that both found the stage unprepared would each run {@code FeatureSorter}, whose + * cycle detection is the expensive part. Waiting here is safe: this caller holds no generator monitor. + */ + void prepare(Engine engine) { + prepare(engine, true); + } + + /** + * Bind and repoint prepare, which is what makes a feature-order cycle a single bind-time ERROR instead of + * a chunk-generation crash. Those callers hold the generator monitor and the build path can need it (the + * biome source may bind an engine while resolving), so this one never waits for another thread's build: it + * builds now or leaves it to the next chunk's prepare. + */ + void prepareWithoutWaiting(Engine engine) { + prepare(engine, false); + } + + private void prepare(Engine engine, boolean waitForBuild) { + if (engine == null || engine.isClosed() || engine.isClosing()) { + return; + } + long generation = biomeSource.packGeneration(); + if (settled(generation)) { + return; + } + if (waitForBuild) { + buildLock.lock(); + } else if (!buildLock.tryLock()) { + return; + } + try { + if (settled(generation)) { + return; + } + build(engine, generation); + } finally { + buildLock.unlock(); + } + } + + private boolean settled(long generation) { + FeatureTable current = featureTable; + if (current != null && current.generation() == generation) { + return true; + } + return inertGeneration == generation; + } + + private void build(Engine engine, long generation) { + IrisImportedFeatureControl control; + try { + control = NativeFeatureGenerationPolicy.control(engine); + } catch (RuntimeException error) { + LOGGER.error("Iris could not read importedFeatures for this dimension; features off: {}", + error.toString()); + markInert(generation); + return; + } + if (!control.shouldGenerateFeatures()) { + markInert(generation); + return; + } + FeatureTable built; + try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_imported_features"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + built = buildTable(engine, control, generation); + } catch (Throwable error) { + LOGGER.error("Iris importedFeatures is off for {}: feature table construction failed: {}", + dimensionKey(engine), error.toString()); + markInert(generation); + return; + } + if (built == null) { + markInert(generation); + return; + } + featureTable = built; + inertGeneration = NO_GENERATION; + // Arm the worldcheck log watch here, before any chunk decorates: arming from the first pass instead + // missed every far-chunk write the first chunk made. No-op unless -Diris.worldcheck is set. + WorldCheckFeaturePlacement.arm(); + LOGGER.info("Iris importedFeatures on for {}: {} biomes, {} steps, {} custom-biome derivative maps", + dimensionKey(engine), built.biomes().size(), built.steps().size(), + built.derivatives().size()); + } + + private void markInert(long generation) { + featureTable = null; + inertGeneration = generation; + } + + private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) { + // Registry-ordered biome list. FeatureSorter's cycle detection walks it, so an unordered list makes + // detection depend on JVM hash order and turns a real cycle into an intermittent one. + List> biomes = biomeSource.orderedPossibleBiomes(); + if (biomes.isEmpty()) { + LOGGER.error("Iris importedFeatures is on but {} exposes no biomes; features off", + dimensionKey(engine)); + return null; + } + Map> byKey = new HashMap<>(biomes.size()); + for (Holder biome : biomes) { + String key = holderKey(biome); + if (key != null) { + byKey.put(key, biome); + } + } + Map> derivatives = customBiomeDerivatives(engine, byKey); + List steps; + try { + // Same inputs as vanilla's own memo, built here so it can be keyed on the Iris pack generation and + // so the cycle failure lands at bind time. + steps = FeatureSorter.buildFeaturesPerStep(biomes, + (Holder biome) -> settingsFor(biome, derivatives).features(), true); + } catch (IllegalStateException error) { + String message = error.getMessage(); + if (message == null || !message.contains(CYCLE_MARKER)) { + throw error; + } + LOGGER.error("Iris importedFeatures is off for {}: the registered placed features cannot be ordered." + + " {}. Remove or reorder the conflicting content, or leave" + + " importedFeatures.enabled false.", + dimensionKey(engine), message); + return null; + } + boolean filtered = control.getDisabled() != null && !control.getDisabled().isEmpty(); + return new FeatureTable(generation, control, List.copyOf(biomes), Set.copyOf(biomes), + Map.copyOf(byKey), steps, Map.copyOf(derivatives), filtered); + } + + /** + * Maps every generated Iris custom biome key onto the registry holder of its Iris biome's vanilla + * derivative. The custom biome's own datapack JSON carries no features by design; this is the only place + * the vanilla feature set enters. + */ + private Map> customBiomeDerivatives(Engine engine, Map> byKey) { + Map> derivatives = new HashMap<>(); + for (IrisBiome irisBiome : engine.getAllBiomes()) { + if (!irisBiome.isCustom()) { + continue; + } + String derivativeKey = normalizeKey(irisBiome.getVanillaDerivativeKey()); + // Resolve from the registry, not only from this source's own biome set: a sea or shore biome's + // structure derivative is rewritten away from its vanilla derivative, so the derivative whose + // features we want is not always a biome this source can emit. + Holder derivative = byKey.get(derivativeKey); + if (derivative == null) { + derivative = biomeSource.registeredBiome(derivativeKey); + } + if (derivative == null) { + LOGGER.warn("Iris importedFeatures: vanilla derivative {} of biome {} is not registered;" + + " its custom biomes generate no imported features", + derivativeKey, irisBiome.getLoadKey()); + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + derivatives.put(ModdedWorldgenIds.biomeRef(engine, customBiome.getId()), derivative); + } + } + return derivatives; + } + + private static BiomeGenerationSettings settingsFor(Holder biome, + Map> derivatives) { + Holder mapped = derivatives.get(holderKey(biome)); + return mapped == null + ? biome.value().getGenerationSettings() + : mapped.value().getGenerationSettings(); + } + + /** + * Runs the vanilla placed-feature pass for one chunk, on the calling worldgen thread. A no-op while the + * control is disabled or the table degraded. + */ + void run(WorldGenLevel level, ChunkAccess chunk, Engine engine) { + FeatureTable table = featureTable; + if (table == null) { + WorldCheckFeaturePlacement.recordFeaturesOff(); + return; + } + if (table.generation() != biomeSource.packGeneration()) { + // A repoint landed between the prepare above and this chunk. Refuse stale content outright; the + // next chunk's prepare rebuilds against the new pack. + invalidate(); + return; + } + IrisModdedChunkGenerator owner = generator; + if (owner == null) { + return; + } + ChunkPos centerPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.of(centerPos, level.getMinSectionY()); + BlockPos origin = sectionPos.origin(); + Registry featureRegistry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + List steps = table.steps(); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); + Set> chunkBiomes = chunkBiomes(level, sectionPos, table); + + 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, + random, decorationSeed, origin, stepIndex); + } + } catch (Throwable error) { + WorldCheckFeaturePlacement.recordPlacementFailure(centerPos, error); + throw new IllegalStateException("Iris imported feature placement failed for chunk " + + centerPos.x() + "," + centerPos.z(), error); + } finally { + level.setCurrentlyGenerating(null); + } + WorldCheckFeaturePlacement.recordPlacementPass(); + } + + private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData, + Registry featureRegistry, Set> chunkBiomes, + IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed, + BlockPos origin, int stepIndex) { + IntSet stepFeatures = new IntArraySet(); + for (Holder biome : chunkBiomes) { + List> biomeFeatures = settingsFor(biome, table.derivatives()).features(); + if (stepIndex >= biomeFeatures.size()) { + continue; + } + for (Holder feature : biomeFeatures.get(stepIndex)) { + stepFeatures.add(stepData.indexMapping().applyAsInt(feature.value())); + } + } + if (stepFeatures.isEmpty()) { + return; + } + // Sorted global indices: identical ordering to vanilla, and each feature's seed comes from its own + // global index, so denying one feature never shifts another. + int[] featureIndices = stepFeatures.toIntArray(); + Arrays.sort(featureIndices); + for (int globalIndex : featureIndices) { + PlacedFeature feature = stepData.features().get(globalIndex); + if (table.filtered()) { + Identifier featureId = featureRegistry.getKey(feature); + if (featureId != null && !table.control().shouldGenerate(featureId.toString())) { + continue; + } + } + random.setFeatureSeed(decorationSeed, globalIndex, stepIndex); + level.setCurrentlyGenerating(() -> describeFeature(featureRegistry, feature)); + feature.placeWithBiomeCheck(level, owner, random, origin); + } + } + + private static String describeFeature(Registry registry, PlacedFeature feature) { + Identifier id = registry.getKey(feature); + return id == null ? feature.toString() : id.toString(); + } + + /** + * Biomes actually present in the 3x3 chunk neighbourhood, intersected with the dimension's biome set. Same + * shape as vanilla, which drops any section biome its biome source does not claim. Holders are canonicalised + * back onto the table's own holders so the index mapping cannot be handed a holder it never saw. + */ + private Set> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) { + List> 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); + } + }); + Set> present = new LinkedHashSet<>(); + for (Holder biome : collected) { + if (table.biomeSet().contains(biome)) { + present.add(biome); + continue; + } + String key = holderKey(biome); + Holder canonical = key == null ? null : table.byKey().get(key); + if (canonical != null) { + present.add(canonical); + } + } + return present; + } + + private static String dimensionKey(Engine engine) { + return engine.getDimension() == null ? "" : engine.getDimension().getLoadKey(); + } + + private static String holderKey(Holder holder) { + return holder.unwrapKey() + .map(key -> key.identifier().toString().toLowerCase(Locale.ROOT)) + .orElse(null); + } + + private static String normalizeKey(String key) { + Identifier identifier = key == null ? null : Identifier.tryParse(key); + return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT); + } + + private record FeatureTable(long generation, IrisImportedFeatureControl control, + List> biomes, Set> biomeSet, + Map> byKey, + List steps, + Map> derivatives, boolean filtered) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java new file mode 100644 index 000000000..31b2ef6d0 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java @@ -0,0 +1,100 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.LogLevel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ModdedIrisLog { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static volatile boolean DEBUG_SETTING_WARNING_LOGGED; + + private ModdedIrisLog() { + } + + public static void log(LogLevel level, String message) { + LogLevel target = level == null ? LogLevel.INFO : level; + switch (target) { + case DEBUG -> debug(message); + case WARN -> warn(message); + case ERROR -> error(message); + // INFO, NOTICE, and any level added later. This is a switch statement, so the compiler does not + // check it for exhaustiveness and a missing arm would drop the message silently. The modded + // loaders have one logger, so a lifecycle notice is already in the file the server writes; the + // level only differs on Bukkit, where INFO goes to a console sender instead. + default -> info(message); + } + } + + public static void debug(String message) { + if (!debugEnabled()) { + LOGGER.debug(clean(message)); + return; + } + + LOGGER.info("[Iris/DEBUG] " + clean(message)); + } + + public static void info(String message) { + LOGGER.info(clean(message)); + } + + public static void warn(String message) { + LOGGER.warn(clean(message)); + } + + public static void error(String message) { + LOGGER.error(clean(message)); + } + + public static void error(String message, Throwable error) { + if (error == null) { + error(message); + return; + } + + LOGGER.error(clean(message), error); + } + + public static String clean(String message) { + return IrisLogging.clean(message); + } + + private static boolean debugEnabled() { + try { + IrisSettings settings = IrisSettings.settings != null ? IrisSettings.settings : IrisSettings.get(); + return settings != null && settings.getGeneral() != null && settings.getGeneral().isDebug(); + } catch (Throwable error) { + warnDebugSetting(error); + return false; + } + } + + private static void warnDebugSetting(Throwable error) { + if (DEBUG_SETTING_WARNING_LOGGED) { + return; + } + + DEBUG_SETTING_WARNING_LOGGED = true; + LOGGER.warn("Iris debug logging setting could not be read", error); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisPayload.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisPayload.java new file mode 100644 index 000000000..85c43af8d --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisPayload.java @@ -0,0 +1,43 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.protocol.IrisProtocol; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; + +public record ModdedIrisPayload(byte[] data) implements CustomPacketPayload { + public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.parse(IrisProtocol.CHANNEL)); + public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(ModdedIrisPayload::write, ModdedIrisPayload::read); + + @Override + public CustomPacketPayload.Type type() { + return TYPE; + } + + private static ModdedIrisPayload read(RegistryFriendlyByteBuf buffer) { + return new ModdedIrisPayload(buffer.readByteArray(IrisProtocol.MAX_FRAME_BYTES)); + } + + private void write(RegistryFriendlyByteBuf buffer) { + buffer.writeByteArray(data); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java new file mode 100644 index 000000000..bf3ee022c --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java @@ -0,0 +1,63 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.splash.IrisSplashComposer; +import art.arcane.iris.core.splash.IrisSplashRenderer; +import art.arcane.iris.spi.IrisLogging; + +import java.io.File; + +public final class ModdedIrisSplash { + + private ModdedIrisSplash() { + } + + public static void print(ModdedLoader loader) { + printPacks(loader); + if (isLogoEnabled()) { + printLogo(loader); + } + } + + private static void printPacks(ModdedLoader loader) { + File packFolder = IrisPlatforms.get().packsFolderNoCreate(); + for (String line : IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)) { + IrisLogging.info(line); + } + } + + private static void printLogo(ModdedLoader loader) { + String serverLine = loader.platformName() + " / Minecraft " + loader.minecraftVersion(); + String[] splash = IrisSplashRenderer.renderPlain(); + String[] info = IrisSplashComposer.composeInfo(loader.modVersion(), serverLine, IrisSplashComposer.InfoStyle.PLAIN); + IrisLogging.info(IrisSplashComposer.compose(splash, info)); + } + + private static boolean isLogoEnabled() { + try { + return IrisSettings.get().getGeneral().isSplashLogoStartup(); + } catch (Throwable error) { + IrisLogging.warn("Iris splash setting could not be read: " + error.getClass().getSimpleName()); + return true; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItem.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItem.java new file mode 100644 index 000000000..b2f5d7414 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItem.java @@ -0,0 +1,58 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.PlatformItem; +import net.minecraft.world.item.Item; + +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedItem implements PlatformItem { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + + private final Item item; + private final String key; + private final String namespace; + + private ModdedItem(Item item, String key) { + this.item = item; + this.key = key; + int colon = key.indexOf(':'); + this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft"; + } + + public static ModdedItem of(Item item, String key) { + return CACHE.computeIfAbsent(key, (String k) -> new ModdedItem(item, k)); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public Object nativeHandle() { + return item; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItemTranslator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItemTranslator.java new file mode 100644 index 000000000..79149a98f --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItemTranslator.java @@ -0,0 +1,407 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.InventorySlotType; +import art.arcane.iris.engine.object.IrisAttributeModifier; +import art.arcane.iris.engine.object.IrisEnchantment; +import art.arcane.iris.engine.object.IrisLoot; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.math.RNG; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Unit; +import net.minecraft.world.entity.EquipmentSlotGroup; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.component.CustomModelData; +import net.minecraft.world.item.component.DyedItemColor; +import net.minecraft.world.item.component.ItemAttributeModifiers; +import net.minecraft.world.item.component.ItemLore; +import net.minecraft.world.item.component.TooltipDisplay; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.ItemEnchantments; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedItemTranslator { + private static final Set WARNED = ConcurrentHashMap.newKeySet(); + private static final String COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx"; + + private ModdedItemTranslator() { + } + + public static KList loot(IrisLootTable table, long lootSeed, InventorySlotType slot, ServerLevel level, int x, int y, int z) { + KList out = new KList<>(); + KList entries = table.getLoot(); + if (entries.isEmpty() || table.getMaxTries() <= 0) { + return out; + } + + RNG rng = LootResolver.tableRng(lootSeed, table, x, y, z); + int m = 0; + int c = 0; + int mx = Math.max(0, LootResolver.inclusive(rng, table.getMinPicked(), table.getMaxPicked())); + + while (m < mx && c++ < table.getMaxTries()) { + int entryIndex = rng.nextInt(entries.size()); + IrisLoot entry = entries.get(entryIndex); + if (entry == null || entry.getSlotTypes() != slot) { + continue; + } + ItemStack item = item(entry, table, rng, lootSeed, entryIndex, level, x, y, z); + if (item != null && !item.isEmpty()) { + out.add(item); + m++; + } + } + + return out; + } + + public static ItemStack stack(IrisLoot loot, RNG rng, ServerLevel level) { + try { + ItemStack stack = baseStack(loot, rng); + if (stack == null || stack.isEmpty()) { + return null; + } + applyComponents(loot, stack, rng, level); + applyCustomNbt(stack, loot.getCustomNbt()); + return stack; + } catch (Throwable e) { + IrisLogging.reportError(e); + return null; + } + } + + public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, long lootSeed, int entryIndex, ServerLevel level, int x, int y, int z) { + long combinedRarity = LootResolver.combinedRarity(loot.getRarity(), table.getRarity()); + if (!LootResolver.spatialOneIn(lootSeed, table, entryIndex, x, y, z, combinedRarity)) { + return null; + } + + try { + ItemStack stack = baseStack(loot, rng); + if (stack == null || stack.isEmpty()) { + return null; + } + applyComponents(loot, stack, rng, level); + applyCustomNbt(stack, loot.getCustomNbt()); + return stack; + } catch (Throwable e) { + IrisLogging.reportError(e); + return null; + } + } + + private static ItemStack baseStack(IrisLoot loot, RNG rng) { + String raw = loot.getTypeKey(); + if (raw == null || raw.isBlank()) { + return null; + } + + String key = raw.toLowerCase(Locale.ROOT); + Identifier id = Identifier.tryParse(key.contains(":") ? key : "minecraft:" + key); + if (id == null || !BuiltInRegistries.ITEM.containsKey(id)) { + warnOnce("item:" + key, "Iris loot: unknown item '" + raw + "'"); + return null; + } + + Item item = BuiltInRegistries.ITEM.getValue(id); + return new ItemStack(item, Math.max(1, LootResolver.inclusive(rng, loot.getMinAmount(), loot.getMaxAmount()))); + } + + private static void applyComponents(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) { + applyEnchantments(loot, stack, rng, level); + applyAttributes(loot, stack, rng, level); + + if (loot.isUnbreakable()) { + stack.set(DataComponents.UNBREAKABLE, Unit.INSTANCE); + } + + if (loot.getItemFlags().isNotEmpty()) { + applyItemFlags(loot, stack); + } + + if (loot.getCustomModel() != null) { + stack.set(DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(List.of(loot.getCustomModel().floatValue()), List.of(), List.of(), List.of())); + } + + if (stack.getMaxDamage() > 0) { + int max = stack.getMaxDamage(); + int damage = (int) Math.round(Math.max(0, Math.min(max, (1D - rng.d(loot.getMinDurability(), loot.getMaxDurability())) * max))); + stack.set(DataComponents.DAMAGE, damage); + } + + if (loot.getLeatherColor() != null) { + try { + int rgb = Integer.decode(loot.getLeatherColor()) & 0xFFFFFF; + stack.set(DataComponents.DYED_COLOR, new DyedItemColor(rgb)); + } catch (NumberFormatException e) { + warnOnce("leatherColor:" + loot.getLeatherColor(), "Iris loot: invalid leatherColor '" + loot.getLeatherColor() + "'"); + } + } + + String dye = loot.getDyeColorKey(); + if (dye != null) { + applyDyeColor(stack, dye); + } + + if (loot.getDisplayName() != null) { + stack.set(DataComponents.CUSTOM_NAME, Component.literal(colorize(loot.getDisplayName()))); + } + + applyLore(loot, stack); + } + + private static void applyEnchantments(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) { + if (loot.getEnchantments().isEmpty()) { + return; + } + + Registry registry = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT); + DataComponentType component = stack.is(Items.ENCHANTED_BOOK) ? DataComponents.STORED_ENCHANTMENTS : DataComponents.ENCHANTMENTS; + ItemEnchantments.Mutable mutable = new ItemEnchantments.Mutable(stack.getOrDefault(component, ItemEnchantments.EMPTY)); + boolean changed = false; + + for (IrisEnchantment enchantment : loot.getEnchantments()) { + String name = enchantment.getEnchantment(); + if (name == null || name.isBlank()) { + continue; + } + // Same normalization as IrisEnchantment.resolve() on Bukkit: trim, lowercase, spaces to underscores. + // Without it 'Fire Aspect' resolves on Bukkit and warns as unknown here, for the same pack. + String key = name.trim().toLowerCase(Locale.ROOT).replace(' ', '_'); + Identifier id = Identifier.tryParse(key.contains(":") ? key : "minecraft:" + key); + Optional> holder = id == null ? Optional.empty() : registry.get(id); + if (holder.isEmpty()) { + warnOnce("enchantment:" + key, "Iris loot: unknown enchantment '" + name + "'"); + continue; + } + if (rng.nextDouble() < enchantment.getChance()) { + mutable.set(holder.get(), enchantment.getLevel(rng)); + changed = true; + } + } + + if (changed) { + stack.set(component, mutable.toImmutable()); + } + } + + private static void applyAttributes(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) { + if (loot.getAttributes().isEmpty()) { + return; + } + + Registry registry = level.registryAccess().lookupOrThrow(Registries.ATTRIBUTE); + ItemAttributeModifiers.Builder builder = ItemAttributeModifiers.builder(); + boolean changed = false; + + for (IrisAttributeModifier attribute : loot.getAttributes()) { + if (rng.nextDouble() >= attribute.getChance()) { + continue; + } + double amount = attribute.getAmount(rng); + Holder holder = resolveAttribute(registry, attribute.getAttribute()); + if (holder == null) { + warnOnce("attribute:" + attribute.getAttribute(), "Iris loot: unknown attribute '" + attribute.getAttribute() + "'"); + continue; + } + Identifier modifierId = attributeModifierId(attribute.getName()); + if (modifierId == null) { + continue; + } + AttributeModifier modifier = new AttributeModifier(modifierId, amount, operationFor(attribute.getOperation())); + builder.add(holder, modifier, EquipmentSlotGroup.ANY); + changed = true; + } + + if (changed) { + stack.set(DataComponents.ATTRIBUTE_MODIFIERS, builder.build()); + } + } + + public static Holder resolveAttribute(Registry registry, String key) { + if (key == null || key.isBlank()) { + return null; + } + String value = key.trim().toLowerCase(Locale.ROOT); + Holder direct = lookupAttribute(registry, Identifier.tryParse(value.indexOf(':') >= 0 ? value : "minecraft:" + value)); + if (direct != null) { + return direct; + } + if (value.startsWith("generic_")) { + return lookupAttribute(registry, Identifier.tryParse("minecraft:" + value.substring("generic_".length()))); + } + if (value.startsWith("generic.")) { + return lookupAttribute(registry, Identifier.tryParse("minecraft:" + value.substring("generic.".length()))); + } + return null; + } + + private static Holder lookupAttribute(Registry registry, Identifier id) { + if (id == null) { + return null; + } + Optional> holder = registry.get(id); + return holder.isPresent() ? holder.get() : null; + } + + public static AttributeModifier.Operation operationFor(String operation) { + if (operation == null || operation.isBlank()) { + return AttributeModifier.Operation.ADD_VALUE; + } + return switch (operation.trim().toUpperCase(Locale.ROOT)) { + case "ADD_SCALAR", "ADD_MULTIPLIED_BASE" -> AttributeModifier.Operation.ADD_MULTIPLIED_BASE; + case "MULTIPLY_SCALAR_1", "ADD_MULTIPLIED_TOTAL" -> AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL; + default -> AttributeModifier.Operation.ADD_VALUE; + }; + } + + private static Identifier attributeModifierId(String name) { + String source = name == null || name.isBlank() ? "modifier" : name; + String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_"); + if (normalized.isBlank()) { + normalized = "modifier"; + } + return Identifier.tryParse("iris:" + normalized); + } + + private static void applyItemFlags(IrisLoot loot, ItemStack stack) { + TooltipDisplay display = stack.getOrDefault(DataComponents.TOOLTIP_DISPLAY, TooltipDisplay.DEFAULT); + boolean changed = false; + + for (String flag : loot.getItemFlags()) { + if (flag == null || flag.isBlank()) { + continue; + } + DataComponentType hidden = tooltipComponentFor(flag.trim().toUpperCase(Locale.ROOT)); + if (hidden == null) { + warnOnce("itemFlag:" + flag, "Iris loot: item flag '" + flag + "' has no modded tooltip equivalent; skipping"); + continue; + } + display = display.withHidden(hidden, true); + changed = true; + } + + if (changed) { + stack.set(DataComponents.TOOLTIP_DISPLAY, display); + } + } + + private static DataComponentType tooltipComponentFor(String flag) { + return switch (flag) { + case "HIDE_ENCHANTS" -> DataComponents.ENCHANTMENTS; + case "HIDE_STORED_ENCHANTS" -> DataComponents.STORED_ENCHANTMENTS; + case "HIDE_ATTRIBUTES" -> DataComponents.ATTRIBUTE_MODIFIERS; + case "HIDE_UNBREAKABLE" -> DataComponents.UNBREAKABLE; + case "HIDE_DESTROYS" -> DataComponents.CAN_BREAK; + case "HIDE_PLACED_ON" -> DataComponents.CAN_PLACE_ON; + case "HIDE_DYE" -> DataComponents.DYED_COLOR; + case "HIDE_ARMOR_TRIM" -> DataComponents.TRIM; + default -> null; + }; + } + + private static void applyDyeColor(ItemStack stack, String dye) { + DyeColor color = DyeColor.byName(dye.trim().toLowerCase(Locale.ROOT), null); + if (color == null) { + warnOnce("dyeColor:" + dye, "Iris loot: unknown dyeColor '" + dye + "'"); + return; + } + stack.set(DataComponents.BASE_COLOR, color); + } + + private static void applyLore(IrisLoot loot, ItemStack stack) { + if (loot.getLore().isEmpty()) { + return; + } + + List lines = new ArrayList<>(); + for (String line : loot.getLore()) { + String colored = colorize(line); + if (colored.length() > 24) { + for (String wrapped : Form.wrapWords(colored, 24).split("\\Q\n\\E")) { + lines.add(Component.literal(wrapped.trim())); + } + } else { + lines.add(Component.literal(colored)); + } + } + stack.set(DataComponents.LORE, new ItemLore(lines)); + } + + private static void applyCustomNbt(ItemStack stack, KMap customNbt) { + if (customNbt == null || customNbt.isEmpty()) { + return; + } + try { + CompoundTag tag = TagParser.parseCompoundFully(new JSONObject(customNbt).toString()); + tag.merge(stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag()); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); + } catch (CommandSyntaxException e) { + warnOnce("customNbt:" + e.getMessage(), "Iris loot: invalid customNbt: " + e.getMessage()); + } + } + + private static String colorize(String text) { + char[] chars = text.toCharArray(); + for (int i = 0; i < chars.length - 1; i++) { + if (chars[i] == '&' && COLOR_CODES.indexOf(chars[i + 1]) > -1) { + chars[i] = '§'; + chars[i + 1] = Character.toLowerCase(chars[i + 1]); + } + } + return new String(chars); + } + + private static void warnOnce(String key, String message) { + if (WARNED.add(key)) { + IrisLogging.warn(message); + } + } + +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLoader.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLoader.java new file mode 100644 index 000000000..03247425e --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLoader.java @@ -0,0 +1,50 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.state.BlockState; + +import java.io.File; +import java.nio.file.Path; + +public interface ModdedLoader { + String platformName(); + + String minecraftVersion(); + + String modVersion(); + + MinecraftServer currentServer(); + + void invalidateLevelCache(MinecraftServer server); + + boolean clientEnvironment(); + + Path configDir(); + + File modJar(); + + boolean hasTreeFellerPermission(ServerPlayer player); + + boolean canTreeFellerBreak(ServerLevel level, ServerPlayer player, BlockPos position, BlockState state); +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLootApplier.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLootApplier.java new file mode 100644 index 000000000..eab4ca87d --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLootApplier.java @@ -0,0 +1,337 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.framework.PlacedObject; +import art.arcane.iris.engine.object.IObjectLoot; +import art.arcane.iris.engine.object.InventorySlotType; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.engine.object.IrisObjectLoot; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisObjectVanillaLoot; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.Container; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.ChestType; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.LootTable; +import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +public final class ModdedLootApplier { + private ModdedLootApplier() { + } + + public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk mc) { + if (!isCanonicalContainer(pos, state)) { + return; + } + RNG rng = LootResolver.containerRng(engine.getSeedManager().getLoot(), pos.getX(), pos.getY(), pos.getZ()); + boolean nativeLootDefined = hasNativeLootTable(level, pos, state); + List sources = resolveSources(engine, level, rng, pos, state, mc, nativeLootDefined); + if (sources.isEmpty()) { + return; + } + + Container container = resolveContainer(level, pos, state); + if (container == null) { + IrisLogging.debug("Iris loot: no container at " + pos); + return; + } + + KList items = new KList<>(); + LootParams nativeParams = null; + for (LootSource source : sources) { + if (source instanceof IrisLootSource irisSource) { + IrisLootTable table = irisSource.table(); + if (table == null) { + continue; + } + items.addAll(ModdedItemTranslator.loot(table, engine.getSeedManager().getLoot(), InventorySlotType.STORAGE, level, pos.getX(), pos.getY(), pos.getZ())); + continue; + } + if (source instanceof NativeLootSource nativeSource) { + if (nativeParams == null) { + nativeParams = new LootParams.Builder(level) + .withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)) + .create(LootContextParamSets.CHEST); + } + items.addAll(nativeSource.table().getRandomItems(nativeParams, rng.nextLong())); + } + } + + fillContainer(container, items, rng); + } + + private static List resolveSources(Engine engine, ServerLevel level, RNG rng, BlockPos pos, BlockState state, MantleChunk mc, boolean nativeLootDefined) { + int rx = pos.getX(); + int rz = pos.getZ(); + int ry = pos.getY() - engine.getWorld().minHeight(); + List sources = new ArrayList<>(); + boolean objectLootDefined = nativeLootDefined; + + PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc); + if (po != null && po.getPlacement() != null && ModdedBlockResolution.isStorageChest(state)) { + objectLootDefined = objectLootDefined + || po.getPlacement().getLoot().isNotEmpty() + || po.getPlacement().getVanillaLoot().isNotEmpty(); + Candidate picked = pickPlacementTable(engine, level, po.getPlacement(), state, rng); + if (picked != null) { + sources.add(picked.source()); + if (po.getPlacement().isOverrideGlobalLoot()) { + return sources; + } + } + } + + LootResolver.resolveEnvironmentSources( + sources, + engine, + rng, + rx, + ry, + rz, + objectLootDefined, + IrisLootSource::new + ); + return sources; + } + + private static Candidate pickPlacementTable(Engine engine, ServerLevel level, IrisObjectPlacement placement, BlockState state, RNG rng) { + List exact = new ArrayList<>(); + List basic = new ArrayList<>(); + List global = new ArrayList<>(); + + for (IrisObjectLoot loot : placement.getLoot()) { + if (loot == null || loot.getWeight() <= 0) { + continue; + } + IrisLootTable table = engine.getData().getLootLoader().load(loot.getName()); + if (table == null) { + IrisLogging.warn("Couldn't find loot table " + loot.getName()); + continue; + } + bucket(engine, loot, new Candidate(new IrisLootSource(table), loot.getWeight()), state, exact, basic, global); + } + + for (IrisObjectVanillaLoot loot : placement.getVanillaLoot()) { + if (loot == null || loot.getWeight() <= 0) { + continue; + } + ResourceKey key = resolveNativeKey(loot.getName(), candidate -> hasNativeTable(level, candidate)); + if (key == null) { + IrisLogging.warn("Couldn't find vanilla loot table " + loot.getName()); + continue; + } + LootTable table = level.getServer().reloadableRegistries().lookup() + .lookupOrThrow(Registries.LOOT_TABLE) + .getOrThrow(key) + .value(); + bucket(engine, loot, new Candidate(new NativeLootSource(table), loot.getWeight()), state, exact, basic, global); + } + + List pool = !exact.isEmpty() ? exact : !basic.isEmpty() ? basic : global; + return LootResolver.pickWeighted(pool, Candidate::weight, rng); + } + + static ResourceKey resolveNativeKey(String name, Predicate> registryContains) { + Identifier id = name == null ? null : Identifier.tryParse(name); + if (id == null) { + return null; + } + ResourceKey key = ResourceKey.create(Registries.LOOT_TABLE, id); + return registryContains.test(key) ? key : null; + } + + static void fillContainer(Container container, List items, RNG rng) { + for (ItemStack item : items) { + addItem(container, item); + } + scramble(container, rng); + container.setChanged(); + } + + private static boolean hasNativeTable(ServerLevel level, ResourceKey key) { + return level.getServer().reloadableRegistries().lookup() + .lookup(Registries.LOOT_TABLE) + .flatMap(registry -> registry.get(key)) + .isPresent(); + } + + private static void bucket(Engine engine, IObjectLoot loot, Candidate candidate, BlockState state, List exact, List basic, List global) { + if (loot.getFilter().isEmpty()) { + global.add(candidate); + return; + } + + for (PlatformBlockState filterState : loot.getFilter(engine.getData())) { + BlockState filter = (BlockState) filterState.nativeHandle(); + if (loot.isExact() ? filter == state : filter.getBlock() == state.getBlock()) { + (loot.isExact() ? exact : basic).add(candidate); + return; + } + } + } + + static boolean isCanonicalContainer(BlockPos pos, BlockState state) { + if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) { + return true; + } + BlockPos connected = ChestBlock.getConnectedBlockPos(pos, state); + if (connected.equals(pos)) { + return true; + } + return isCanonicalPair(pos, connected); + } + + static boolean isCanonicalPair(BlockPos pos, BlockPos connected) { + return pos.getX() < connected.getX() + || pos.getX() == connected.getX() && pos.getZ() <= connected.getZ(); + } + + static boolean hasNativeLootTable(ServerLevel level, BlockPos pos, BlockState state) { + if (hasNativeLootTableAt(level, pos)) { + return true; + } + if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) { + return false; + } + BlockPos connected = ChestBlock.getConnectedBlockPos(pos, state); + return level.hasChunkAt(connected) && hasNativeLootTableAt(level, connected); + } + + private static boolean hasNativeLootTableAt(ServerLevel level, BlockPos pos) { + return hasNativeLootTable(level.getBlockEntity(pos)); + } + + static boolean hasNativeLootTable(BlockEntity blockEntity) { + return blockEntity instanceof RandomizableContainer container && hasNativeLootTable(container); + } + + static boolean hasNativeLootTable(RandomizableContainer container) { + return container != null && container.getLootTable() != null; + } + + private static Container resolveContainer(ServerLevel level, BlockPos pos, BlockState state) { + if (state.getBlock() instanceof ChestBlock chestBlock) { + Container combined = ChestBlock.getContainer(chestBlock, state, level, pos, true); + if (combined != null) { + return combined; + } + } + BlockEntity blockEntity = level.getBlockEntity(pos); + return blockEntity instanceof Container container ? container : null; + } + + private static void addItem(Container container, ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return; + } + for (int i = 0; i < container.getContainerSize() && !stack.isEmpty(); i++) { + ItemStack existing = container.getItem(i); + if (existing.isEmpty()) { + container.setItem(i, stack.split(container.getMaxStackSize(stack))); + continue; + } + if (ItemStack.isSameItemSameComponents(existing, stack)) { + int limit = container.getMaxStackSize(existing); + if (existing.getCount() < limit) { + int move = Math.min(stack.getCount(), limit - existing.getCount()); + existing.grow(move); + stack.shrink(move); + } + } + } + if (!stack.isEmpty()) { + IrisLogging.debug("Iris loot: container full, dropped " + stack.getCount() + "x " + stack.getItem()); + } + } + + private static void scramble(Container container, RNG rng) { + int size = container.getContainerSize(); + ItemStack[] items = new ItemStack[size]; + for (int i = 0; i < size; i++) { + items[i] = container.getItem(i); + } + boolean packedFull = false; + + splitting: + for (int i = 0; i < items.length; i++) { + ItemStack is = items[i]; + + if (!is.isEmpty() && is.getCount() > 1 && !packedFull) { + for (int j = 0; j < items.length; j++) { + if (items[j].isEmpty()) { + int take = rng.nextInt(is.getCount()); + take = take == 0 ? 1 : take; + is.setCount(is.getCount() - take); + items[j] = is.copyWithCount(take); + continue splitting; + } + } + + packedFull = true; + } + } + + for (int i = items.length; i > 1; i--) { + int j = rng.nextInt(i); + ItemStack tmp = items[i - 1]; + items[i - 1] = items[j]; + items[j] = tmp; + } + + for (int i = 0; i < size; i++) { + container.setItem(i, items[i]); + } + } + + private interface LootSource { + } + + private record IrisLootSource(IrisLootTable table) implements LootSource { + } + + private record NativeLootSource(LootTable table) implements LootSource { + } + + private record Candidate(LootSource source, int weight) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedMixinAudit.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedMixinAudit.java new file mode 100644 index 000000000..f88bdb479 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedMixinAudit.java @@ -0,0 +1,137 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +/** + * Boot audit for the Iris mixin configs. Mixin registration is per-loader (fabric.mod.json entry, NeoForge + * mods.toml [[mixins]] block, Forge shadowJar MixinConfigs manifest attribute) and a config that never gets + * registered fails silently: no crash, the hooks simply never exist, and entity persistence, custom mob loot + * and the Iris world-type labels quietly stop working. + * + *

Application is checked structurally: Mixin transfers an {@code @Inject} handler into the target class, + * so the handler's presence on the target proves the mixin applied. Mixin 0.8.7 renames the transferred + * method to {@code handler$$}, so the declared name is matched as an exact name or as a + * {@code $}-prefixed suffix. Client targets are resolved by name so this class stays free of client + * references and is safe on a dedicated server. + */ +public final class ModdedMixinAudit { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final AtomicBoolean AUDITED = new AtomicBoolean(false); + + private static final List EXPECTED = List.of( + new ExpectedMixin("EntityPersistenceMixin", "entity", + "net.minecraft.world.entity.Entity", "iris$applyGeneratedPersistence", + false, ModdedMixinFlags::entityPersistenceRan), + new ExpectedMixin("LivingEntityLootMixin", "entity", + "net.minecraft.world.entity.LivingEntity", "iris$replaceBaseLoot", + false, ModdedMixinFlags::livingEntityLootRan), + new ExpectedMixin("MobAwarenessMixin", "entity", + "net.minecraft.world.entity.Mob", "iris$tickUnawareMob", + false, ModdedMixinFlags::mobAwarenessRan), + new ExpectedMixin("StructureTemplatePaletteConcurrencyMixin", "common", + "net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate$Palette", + "iris$installConcurrentBlockCache", + false, ModdedMixinFlags::structureTemplatePaletteRan), + new ExpectedMixin("IrisWorldOpenFlowsMixin", "client", + "net.minecraft.client.gui.screens.worldselection.WorldOpenFlows", + "iris$openWorldCheckWorldStemCompatibility", + true, ModdedMixinFlags::worldOpenFlowsRan), + new ExpectedMixin("IrisWorldTypeEntryMixin", "client", + "net.minecraft.client.gui.screens.worldselection.WorldCreationUiState$WorldTypeEntry", + "iris$describePreset", + true, ModdedMixinFlags::worldTypeEntryRan)); + + private ModdedMixinAudit() { + } + + public static void runOnce() { + if (!AUDITED.compareAndSet(false, true)) { + return; + } + audit(ModdedEngineBootstrap.loader().platformName(), + ModdedEngineBootstrap.loader().clientEnvironment()); + } + + static void reset() { + AUDITED.set(false); + } + + static void audit(String platform, boolean clientEnvironment) { + List missing = new ArrayList<>(); + List applied = new ArrayList<>(); + for (ExpectedMixin expected : EXPECTED) { + if (expected.clientOnly() && !clientEnvironment) { + continue; + } + if (isApplied(expected)) { + applied.add(expected.mixinName() + (expected.ran().getAsBoolean() ? "" : " (not yet exercised)")); + } else { + missing.add(expected.config() + '/' + expected.mixinName() + " -> " + expected.targetClass()); + } + } + if (missing.isEmpty()) { + LOGGER.info("Iris mixin audit ok on {} ({} dist): {}", platform, + clientEnvironment ? "client" : "server", String.join(", ", applied)); + return; + } + LOGGER.error("==============================================================="); + LOGGER.error("Iris mixin audit FAILED on {} ({} dist): {} of {} expected mixin(s) were not applied.", + platform, clientEnvironment ? "client" : "server", missing.size(), + missing.size() + applied.size()); + for (String entry : missing) { + LOGGER.error(" missing: {}", entry); + } + LOGGER.error("The mixin config was not registered for this loader (fabric.mod.json mixins, neoforge.mods.toml [[mixins]], forge MixinConfigs manifest attribute)."); + LOGGER.error("Entity persistence, custom mob loot, parallel structure safety, or Iris world-type labels are disabled until this is fixed."); + LOGGER.error("==============================================================="); + } + + private static boolean isApplied(ExpectedMixin expected) { + try { + Class target = Class.forName(expected.targetClass(), false, + ModdedMixinAudit.class.getClassLoader()); + for (Method method : target.getDeclaredMethods()) { + // Mixin 0.8.7 renames applied @Inject handlers to handler$$, so an exact + // name match alone reports every applied mixin as missing. + String name = method.getName(); + if (name.equals(expected.handlerMethod()) + || name.endsWith('$' + expected.handlerMethod())) { + return true; + } + } + return false; + } catch (ClassNotFoundException | LinkageError unavailable) { + LOGGER.warn("Iris mixin audit could not inspect {}", expected.targetClass(), unavailable); + return true; + } + } + + private record ExpectedMixin(String mixinName, String config, String targetClass, String handlerMethod, + boolean clientOnly, BooleanSupplier ran) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedMixinFlags.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedMixinFlags.java new file mode 100644 index 000000000..3695d2080 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedMixinFlags.java @@ -0,0 +1,112 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +/** + * Plain flag holder every Iris mixin marks from its injected body. It is deliberately free of any + * net.minecraft, client or loader reference so the client-dist mixins in art.arcane.iris.client.mixin can + * write to it without dragging client types into modded-common. + * + *

These flags say "the injected code ran at least once", which is weaker than "the mixin was applied": + * a hook only fires when its target path executes. {@link ModdedMixinAudit} is what proves application at + * boot; these are the runtime confirmation reported alongside it. + */ +public final class ModdedMixinFlags { + private static volatile boolean entityPersistenceRan; + private static volatile boolean livingEntityLootRan; + private static volatile boolean mobAwarenessRan; + private static volatile boolean structureTemplatePaletteRan; + private static volatile boolean worldOpenFlowsRan; + private static volatile boolean worldTypeEntryRan; + + private ModdedMixinFlags() { + } + + // Read before write on every marker: the entity hooks run per entity save and per mob tick, and a + // volatile store is a cache-line invalidation on every core that reads the flag. The flags are one-way, + // so the guarded write costs one plain-ish read once set and races only ever re-store the same value. + public static void markEntityPersistence() { + if (!entityPersistenceRan) { + entityPersistenceRan = true; + } + } + + public static void markLivingEntityLoot() { + if (!livingEntityLootRan) { + livingEntityLootRan = true; + } + } + + public static void markMobAwareness() { + if (!mobAwarenessRan) { + mobAwarenessRan = true; + } + } + + public static void markStructureTemplatePalette() { + if (!structureTemplatePaletteRan) { + structureTemplatePaletteRan = true; + } + } + + public static void markWorldOpenFlows() { + if (!worldOpenFlowsRan) { + worldOpenFlowsRan = true; + } + } + + public static void markWorldTypeEntry() { + if (!worldTypeEntryRan) { + worldTypeEntryRan = true; + } + } + + public static boolean entityPersistenceRan() { + return entityPersistenceRan; + } + + public static boolean livingEntityLootRan() { + return livingEntityLootRan; + } + + public static boolean mobAwarenessRan() { + return mobAwarenessRan; + } + + public static boolean structureTemplatePaletteRan() { + return structureTemplatePaletteRan; + } + + public static boolean worldOpenFlowsRan() { + return worldOpenFlowsRan; + } + + public static boolean worldTypeEntryRan() { + return worldTypeEntryRan; + } + + static void reset() { + entityPersistenceRan = false; + livingEntityLootRan = false; + mobAwarenessRan = false; + structureTemplatePaletteRan = false; + worldOpenFlowsRan = false; + worldTypeEntryRan = false; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedModConfig.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedModConfig.java new file mode 100644 index 000000000..9387cad8a --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedModConfig.java @@ -0,0 +1,151 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ModdedModConfig { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Object LOCK = new Object(); + private static volatile ModdedModConfig instance; + + private final String defaultPack; + private final String primaryWorld; + private final boolean routePlayersToPrimaryWorld; + private final String mainWorldPack; + private final long mainWorldSeed; + private final boolean mainWorldAutoRestart; + + private ModdedModConfig(String defaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld, + String mainWorldPack, long mainWorldSeed, boolean mainWorldAutoRestart) { + this.defaultPack = defaultPack; + this.primaryWorld = primaryWorld == null ? "" : primaryWorld.trim(); + this.routePlayersToPrimaryWorld = routePlayersToPrimaryWorld; + this.mainWorldPack = mainWorldPack == null ? "" : mainWorldPack.trim(); + this.mainWorldSeed = mainWorldSeed; + this.mainWorldAutoRestart = mainWorldAutoRestart; + } + + public static ModdedModConfig get() { + ModdedModConfig bound = instance; + if (bound != null) { + return bound; + } + synchronized (LOCK) { + if (instance != null) { + return instance; + } + instance = load(); + return instance; + } + } + + public static void setPrimaryWorld(String dimensionId) { + synchronized (LOCK) { + ModdedModConfig current = get(); + ModdedModConfig updated = new ModdedModConfig(current.defaultPack, dimensionId, current.routePlayersToPrimaryWorld, + current.mainWorldPack, current.mainWorldSeed, current.mainWorldAutoRestart); + instance = updated; + write(configFile(), updated); + } + } + + public static void setMainWorld(String packRef, long seed) { + synchronized (LOCK) { + ModdedModConfig current = get(); + ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.primaryWorld, current.routePlayersToPrimaryWorld, + packRef == null ? "" : packRef.trim(), seed, current.mainWorldAutoRestart); + instance = updated; + write(configFile(), updated); + } + } + + public String defaultPack() { + return defaultPack; + } + + public String primaryWorld() { + return primaryWorld; + } + + public boolean routePlayersToPrimaryWorld() { + return routePlayersToPrimaryWorld; + } + + public String mainWorldPack() { + return mainWorldPack; + } + + public long mainWorldSeed() { + return mainWorldSeed; + } + + public boolean mainWorldAutoRestart() { + return mainWorldAutoRestart; + } + + private static Path configFile() { + return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("modded.json"); + } + + private static ModdedModConfig load() { + Path file = configFile(); + ModdedModConfig defaults = new ModdedModConfig("overworld", "", true, "", 0L, false); + if (!Files.isRegularFile(file)) { + write(file, defaults); + return defaults; + } + try { + JSONObject json = new JSONObject(Files.readString(file, StandardCharsets.UTF_8)); + return new ModdedModConfig( + json.optString("defaultPack", defaults.defaultPack), + json.optString("primaryWorld", defaults.primaryWorld), + json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld), + json.optString("mainWorldPack", defaults.mainWorldPack), + json.optLong("mainWorldSeed", defaults.mainWorldSeed), + json.optBoolean("mainWorldAutoRestart", defaults.mainWorldAutoRestart)); + } catch (RuntimeException | IOException e) { + LOGGER.error("Iris modded config at {} is invalid; using defaults", file, e); + return defaults; + } + } + + private static void write(Path file, ModdedModConfig config) { + JSONObject json = new JSONObject(); + json.put("defaultPack", config.defaultPack); + json.put("primaryWorld", config.primaryWorld); + json.put("routePlayersToPrimaryWorld", config.routePlayersToPrimaryWorld); + json.put("mainWorldPack", config.mainWorldPack); + json.put("mainWorldSeed", config.mainWorldSeed); + json.put("mainWorldAutoRestart", config.mainWorldAutoRestart); + try { + Files.createDirectories(file.getParent()); + Files.writeString(file, json.toString(4), StandardCharsets.UTF_8); + } catch (IOException e) { + LOGGER.error("Iris failed to write modded config at {}", file, e); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java new file mode 100644 index 000000000..e092de80d --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java @@ -0,0 +1,546 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +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.NativeStructureOwnershipRecord; +import art.arcane.iris.engine.framework.NativeStructureStartPlan; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.nativegen.NativeStructureGenerationException; +import art.arcane.iris.nativegen.NativeStructureLocatePersistence; +import art.arcane.iris.nativegen.NativeStructureLocateResults; +import art.arcane.iris.nativegen.NativeStructureOwnershipRecovery; +import art.arcane.iris.nativegen.NativeStructurePostProcessor; +import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope; +import art.arcane.iris.nativegen.NativeStructureSurfaceFitter; +import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator; +import art.arcane.iris.nativegen.NativeStructureVegetationClearer; +import art.arcane.iris.nativegen.NativeStructureVerticalPlacer; +import art.arcane.iris.nativegen.NativeStructureVanillaLocator; +import art.arcane.iris.nativegen.NativeStructureVolumeIndex; +import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.math.RNG; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +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.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.IntBinaryOperator; + +/** + * Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the + * {@link net.minecraft.world.level.chunk.ChunkGenerator} overrides because they issue {@code super} calls; + * everything they do beyond that lives here. + */ +final class ModdedNativeStructureStage { + private static final int WORLD_CHECK_SHIFT_RECORD_LIMIT = 4096; + private static final boolean WORLD_CHECK_ENABLED = Boolean.getBoolean("iris.worldcheck"); + + private final IrisModdedChunkGenerator generator; + private final ConcurrentHashMap worldCheckStructureShifts = new ConcurrentHashMap<>(); + private volatile StructureStepCache structureStepCache; + + ModdedNativeStructureStage(IrisModdedChunkGenerator generator) { + this.generator = generator; + } + + void installVolumeIndex(ServerLevel level, Engine engine) { + WeakReference levelReference = new WeakReference<>(level); + WeakReference generatorReference = new WeakReference<>(generator); + WeakReference biomeSourceReference = new WeakReference<>(generator.structureBiomeSource); + NativeStructureVolumeIndex.install(engine, new NativeStructureVolumeIndex.Context( + level.registryAccess(), + level.getServer().getStructureManager(), + level.dimension(), + LevelHeightAccessor.create(level.getMinY(), level.getHeight()), + generatorReference::get, + biomeSourceReference::get, + () -> { + ServerLevel active = levelReference.get(); + return active == null ? null : active.getChunkSource().getGeneratorState(); + })); + } + + Pair> findNearestIrisStructure(ServerLevel level, + HolderSet holders, + BlockPos pos, int radius, boolean findUnexplored, + Engine current, + NativeStructureVanillaLocator.Candidate nativeCandidate) { + Pair> nativeLocated = + nativeCandidate == null ? null : nativeCandidate.result(); + Runnable nativeReference = () -> { + if (nativeCandidate != null) { + nativeCandidate.reference(level.structureManager()); + } + }; + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List searches = new ArrayList<>(holders.size()); + NativeStructureLocatePersistence.ProbeBudget budget = NativeStructureLocatePersistence.probeBudget(); + for (Holder holder : holders) { + Identifier id = registry.getKey(holder.value()); + if (id == null) { + throw new IllegalStateException("Native structure locate received an unregistered structure holder"); + } + String structureId = id.toString(); + if (!IrisStructureLocator.hasNativePlacement(current, structureId)) { + continue; + } + NativeStructureLocatePersistence.Probe probe = NativeStructureLocatePersistence.probe( + level, holder.value(), findUnexplored, budget); + searches.add(new IrisNativeLocateSearch( + holder, structureId, NativeStructureLocatePersistence.search( + current, structureId, pos.getX(), pos.getZ(), radius, probe))); + } + searches.sort(Comparator.comparing(IrisNativeLocateSearch::structureId)); + for (int attempt = 0; attempt < NativeStructureLocatePersistence.MAX_SELECTED_CANDIDATE_RETRIES; attempt++) { + IrisNativeLocateSearch bestSearch = null; + IrisStructureLocator.LocateResult bestResult = null; + long bestDistance = Long.MAX_VALUE; + for (IrisNativeLocateSearch search : searches) { + IrisStructureLocator.LocateResult result = search.search().predict(); + if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { + throw new IllegalStateException("Iris structure locate reached its safety limit for " + + search.structureId() + " within " + radius + " placement rings"); + } + if (!result.found()) { + continue; + } + long dx = (long) result.originX() - pos.getX(); + long dz = (long) result.originZ() - pos.getZ(); + long distance = dx * dx + dz * dz; + if (distance < bestDistance) { + bestDistance = distance; + bestSearch = search; + bestResult = result; + } + } + if (bestSearch == null) { + return NativeStructureLocateResults.selectAndReference( + pos, null, () -> { }, nativeLocated, nativeReference); + } + Pair> predicted = Pair.of( + new BlockPos(bestResult.originX(), bestResult.baseY(), bestResult.originZ()), + bestSearch.holder()); + if (NativeStructureLocateResults.nearest(pos, predicted, nativeLocated) != predicted) { + return NativeStructureLocateResults.selectAndReference( + pos, predicted, () -> { }, nativeLocated, nativeReference); + } + NativeStructureLocatePersistence.VerifiedStart verified = + bestSearch.search().verify(bestResult); + if (verified == null) { + bestSearch.search().reject(bestResult); + continue; + } + BlockPos located = new BlockPos( + bestResult.originX(), verified.ownership().locatorY(), + bestResult.originZ()); + Pair> irisLocated = Pair.of(located, bestSearch.holder()); + IrisNativeLocateSearch selectedSearch = bestSearch; + NativeStructureLocatePersistence.VerifiedStart selectedStart = verified; + return NativeStructureLocateResults.selectAndReference( + pos, irisLocated, () -> selectedSearch.search().reference(selectedStart), + nativeLocated, nativeReference); + } + throw new IllegalStateException("Iris structure locate rejected too many selected candidates within " + + radius + " placement rings"); + } + + HolderSet filterReachableNativeStructures(ServerLevel level, HolderSet holders, + Engine current) { + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> kept = new ArrayList<>(holders.size()); + for (Holder holder : holders) { + Identifier id = registry.getKey(holder.value()); + if (id == null) { + throw new IllegalStateException("Native structure filtering received an unregistered structure holder"); + } + String key = id.toString(); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current, + key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step())); + if (!decision.generate() || !generator.structureBiomeSource.isStructureReachable(holder)) { + continue; + } + kept.add(holder); + } + return kept.size() == holders.size() ? holders : HolderSet.direct(kept); + } + + void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk, + Map previousStarts, + Map configuredStarts, + Engine current, + StructureTemplateManager templateManager) { + Registry registry = registryAccess.lookupOrThrow(Registries.STRUCTURE); + ChunkPos chunkPos = chunk.getPos(); + for (Map.Entry entry : chunk.getAllStarts().entrySet()) { + Structure structure = entry.getKey(); + StructureStart start = entry.getValue(); + if (!start.isValid() || previousStarts.get(structure) == start) { + continue; + } + if (configuredStarts.containsKey(structure)) { + recordWorldCheckStructureShift( + configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0); + 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()); + } + boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step()); + IrisNativeStructureDecision decision; + try { + decision = NativeStructureGenerationPolicy.resolve(current, + structureId, undergroundStep); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "policy resolution", structureId, chunkPos.x(), chunkPos.z(), error); + } + if (!decision.generate()) { + chunk.setStartForStructure(structure, StructureStart.INVALID_START); + continue; + } + int offsetY; + try { + offsetY = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, + structureId, + decision.yShift(), + generator.getSeaLevel(), + chunk.getMinY(), + chunk.getMinY() + chunk.getHeight(), + undergroundStep, + decision.preserveSourceY(), + decision.yBand(), + (x, z) -> current.getHeight(x, z, true) + current.getMinHeight()); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication( + start, structure, start.getReferences(), + NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()), + structureId); + chunk.setStartForStructure(structure, wrapped); + if (!wrapped.isValid()) { + continue; + } + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error); + } + recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY); + } + } + + void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) { + if (!structureManager.shouldGenerateStructures()) { + ChunkPos disabledChunk = chunk.getPos(); + throw new IllegalStateException("Iris cannot generate native structures in chunk " + + disabledChunk.x() + "," + disabledChunk.z() + + " because generate-structures=false disables them outside the pack. That flag is fixed when " + + "the world is created (server.properties generate-structures, or the Generate Structures " + + "toggle in singleplayer), so it cannot be changed for this world: create a new world with " + + "structures enabled, then deny families through importedStructures.disabled or complete keys " + + "through importedStructures.disabledExact"); + } + ChunkPos chunkPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY()); + BlockPos origin = sectionPos.origin(); + Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> byStep = structuresByStep(registry); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); + BoundingBox area = writableArea(chunk); + int steps = GenerationStep.Decoration.values().length; + Engine current = generator.engine(); + List placementGroups = new ArrayList<>(); + List heightmapStarts = new ArrayList<>(); + List vegetationTargets = new ArrayList<>(); + List terrainTargets = new ArrayList<>(); + for (int step = 0; step < steps; step++) { + int index = 0; + for (Structure structure : byStep.get(step)) { + Identifier id = registry.getKey(structure); + String structureId = id == null ? null : id.toString(); + if (structureId == null) { + throw NativeStructureGenerationException.failure( + "resolution", null, chunkPos.x(), chunkPos.z()); + } + try { + IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(current, + structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + List starts = structureManager.startsForStructure(sectionPos, structure); + List resolvedPlacements = new ArrayList<>(starts.size()); + for (StructureStart start : starts) { + NativeStructureOwnershipRecord ownership = + NativeStructureOwnershipRecovery.resolve( + current, world.getLevel(), structureId, structure, start); + IrisNativeStructureDecision decision = + ownership == null ? sourceDecision : ownership.restoredDecision(); + if (!decision.generate()) { + continue; + } + resolvedPlacements.add(new NativePlacement(start, decision)); + heightmapStarts.add(start); + terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget( + structureId, start, + NativeStructureTerrainIntegrator.resolveNativeTerrain( + start, decision.terrain()))); + vegetationTargets.add(start); + } + if (!resolvedPlacements.isEmpty()) { + placementGroups.add(new NativePlacementGroup( + structureId, index, step, List.copyOf(resolvedPlacements))); + } + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "resolution", structureId, chunkPos.x(), chunkPos.z(), error); + } + index++; + } + } + if (!placementGroups.isEmpty()) { + ServerLevel level = world.getLevel(); + visitExistingPois(chunk, (position, state) -> level.updatePOIOnBlockStateChange( + position, Blocks.AIR.defaultBlockState(), state)); + } + try { + int runtimeMinY = world.getMinY(); + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, chunkPos, heightmapStarts, + worldgenSurfaceHeight(current, runtimeMinY), + worldgenFloorHeight(current, runtimeMinY)); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "heightmap priming", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + try { + NativeStructureVegetationClearer.clearIntersectingVegetation( + world, chunk, area, vegetationTargets); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "vegetation cleanup", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan; + try { + vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures( + world, area, terrainTargets, + (x, z) -> current.getHeight(x, z, true) + current.getMinHeight()); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "terrain integration", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + try { + NativeStructurePostProcessor.prepareTerrain( + world, area, terrainTargets, this::resolvePaletteBlock); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "terrain preparation", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + for (NativePlacementGroup group : placementGroups) { + random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step()); + try { + for (NativePlacement placement : group.placements()) { + placeVanillaStructure(world, structureManager, random, area, chunkPos, + group.structureId(), placement.start(), placement.decision()); + } + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "placement", group.structureId(), chunkPos.x(), chunkPos.z(), error); + } + } + try { + NativeStructureSurfaceFitter.repairVacuumFoundations( + world, area, vacuumFoundationPlan); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "foundation repair", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + } + + static void visitExistingPois(ChunkAccess chunk, BiConsumer visitor) { + chunk.findBlocks(PoiTypes::hasPoi, visitor); + } + + private static String nativeStructureBatchContext(List placementGroups) { + if (placementGroups.isEmpty()) { + return ""; + } + StringBuilder context = new StringBuilder("["); + for (int i = 0; i < placementGroups.size(); i++) { + if (i > 0) { + context.append(", "); + } + context.append(placementGroups.get(i).structureId()); + } + return context.append(']').toString(); + } + + private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, + WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, + String structureId, StructureStart start, + IrisNativeStructureDecision decision) { + Engine current = generator.engine(); + int runtimeMinY = world.getMinY(); + WorldGenLevel boundedWorld = ModdedNativeStructureWorldgenAccess.create( + world, chunkPos, + worldgenSurfaceHeight(current, runtimeMinY), + worldgenFloorHeight(current, runtimeMinY)); + world.setCurrentlyGenerating(() -> "Iris native structure " + structureId); + try { + NativeStructurePostProcessor.place( + boundedWorld, structureManager, generator, random, area, chunkPos, + structureId, start, decision, this::resolvePaletteBlock, + (x, z) -> current.getHeight(x, z, true) + current.getMinHeight()); + } finally { + world.setCurrentlyGenerating(null); + } + } + + private List> structuresByStep(Registry registry) { + StructureStepCache cached = structureStepCache; + if (cached != null && cached.registry() == registry) { + return cached.structures(); + } + synchronized (generator) { + cached = structureStepCache; + if (cached != null && cached.registry() == registry) { + return cached.structures(); + } + int steps = GenerationStep.Decoration.values().length; + List> grouped = new ArrayList<>(steps); + for (int step = 0; step < steps; step++) { + grouped.add(new ArrayList<>()); + } + for (Structure structure : registry) { + grouped.get(structure.step().ordinal()).add(structure); + } + for (int step = 0; step < steps; step++) { + grouped.set(step, List.copyOf(grouped.get(step))); + } + List> resolved = List.copyOf(grouped); + structureStepCache = new StructureStepCache(registry, resolved); + return resolved; + } + } + + private void recordWorldCheckStructureShift(String structureId, ChunkPos startChunk, int offsetY) { + if (!WORLD_CHECK_ENABLED || structureId == null) { + return; + } + if (worldCheckStructureShifts.size() >= WORLD_CHECK_SHIFT_RECORD_LIMIT) { + worldCheckStructureShifts.clear(); + } + worldCheckStructureShifts.put(new NativeStructureStartKey(structureId, startChunk.pack()), offsetY); + } + + Integer worldCheckStructureShift(String structureId, ChunkPos startChunk) { + if (structureId == null || startChunk == null) { + return null; + } + return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack())); + } + + void clearWorldCheckStructureShifts() { + worldCheckStructureShifts.clear(); + } + + private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng, + int x, int y, int z) { + PlatformBlockState platformState = palette.get(rng, x, y, z, generator.engine().getData()); + if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) { + throw new IllegalStateException("Configured native structure palette did not resolve a Minecraft block at " + + x + "," + y + "," + z); + } + return blockState; + } + + private BoundingBox writableArea(ChunkAccess chunk) { + ChunkPos chunkPos = chunk.getPos(); + int minX = chunkPos.getMinBlockX(); + int minZ = chunkPos.getMinBlockZ(); + int minY = chunk.getMinY() + 1; + int maxY = chunk.getMinY() + chunk.getHeight() - 1; + return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15); + } + + private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) { + return (x, z) -> generationEngine.getHeight(x, z, false) + runtimeMinY + 1; + } + + private IntBinaryOperator worldgenFloorHeight(Engine generationEngine, int runtimeMinY) { + return (x, z) -> generationEngine.getHeight(x, z, true) + runtimeMinY + 1; + } + + private record NativeStructureStartKey(String structureId, long chunkPosition) { + } + + private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) { + } + + private record NativePlacementGroup(String structureId, int featureIndex, int step, + List placements) { + } + + private record IrisNativeLocateSearch(Holder holder, String structureId, + NativeStructureLocatePersistence.Search search) { + } + + private record StructureStepCache(Registry registry, List> structures) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccess.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccess.java new file mode 100644 index 000000000..9aa074711 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccess.java @@ -0,0 +1,558 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps; +import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.QuartPos; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.attribute.EnvironmentAttributeReader; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.EmptyLevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.LevelTickAccess; +import net.minecraft.world.ticks.ScheduledTick; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.IntBinaryOperator; +import java.util.function.Predicate; +import java.util.function.Supplier; + +final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel { + private static final int WRITE_RADIUS = 1; + + private final WorldGenLevel delegate; + private final ChunkPos generationCenter; + private final IntBinaryOperator surfaceFirstFreeY; + private final IntBinaryOperator floorFirstFreeY; + private final Holder fallbackBiome; + private final BiomeManager biomeManager; + private final AABB generationBounds; + private final LevelTickAccess blockTicks; + private final LevelTickAccess fluidTicks; + private final Long2LongOpenHashMap terrainHeights; + private final Long2ObjectOpenHashMap outsideChunks; + + private ModdedNativeStructureWorldgenAccess(WorldGenLevel delegate, Boundary boundary) { + this.delegate = Objects.requireNonNull(delegate, "Native structure world access requires a delegate"); + this.generationCenter = Objects.requireNonNull( + boundary.generationCenter(), "Native structure world access requires a generation center"); + this.surfaceFirstFreeY = Objects.requireNonNull( + boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver"); + this.floorFirstFreeY = Objects.requireNonNull( + boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver"); + BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel()); + this.fallbackBiome = delegate.getBiome(biomeSample); + this.biomeManager = delegate.getBiomeManager().withDifferentSource(this); + this.generationBounds = new AABB( + generationCenter.getMinBlockX() - 16, + delegate.getMinY(), + generationCenter.getMinBlockZ() - 16, + generationCenter.getMaxBlockX() + 17, + delegate.getMinY() + delegate.getHeight(), + generationCenter.getMaxBlockZ() + 17); + this.blockTicks = new BoundedTickAccess<>(delegate.getBlockTicks(), this::isWritable); + this.fluidTicks = new BoundedTickAccess<>(delegate.getFluidTicks(), this::isWritable); + this.terrainHeights = new Long2LongOpenHashMap(); + this.terrainHeights.defaultReturnValue(Long.MIN_VALUE); + this.outsideChunks = new Long2ObjectOpenHashMap<>(); + } + + static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter, + IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { + return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary( + generationCenter, surfaceFirstFreeY, floorFirstFreeY)); + } + + @Override + public long getSeed() { + return delegate.getSeed(); + } + + @Override + public boolean ensureCanWrite(BlockPos position) { + return isWritable(position) && delegate.ensureCanWrite(position); + } + + @Override + public void setCurrentlyGenerating(Supplier description) { + delegate.setCurrentlyGenerating(description); + } + + @Override + public ServerLevel getLevel() { + return delegate.getLevel(); + } + + @Override + public DifficultyInstance getCurrentDifficultyAt(BlockPos position) { + if (isReadable(position)) { + return delegate.getCurrentDifficultyAt(position); + } + return delegate.getCurrentDifficultyAt(generationCenter.getMiddleBlockPosition( + Mth.clamp(position.getY(), getMinY(), getMaxY() - 1))); + } + + @Override + public long nextSubTickCount() { + return delegate.nextSubTickCount(); + } + + @Override + public LevelTickAccess getBlockTicks() { + return blockTicks; + } + + @Override + public LevelTickAccess getFluidTicks() { + return fluidTicks; + } + + @Override + public LevelData getLevelData() { + return delegate.getLevelData(); + } + + @Override + public MinecraftServer getServer() { + return delegate.getServer(); + } + + @Override + public ChunkSource getChunkSource() { + return delegate.getChunkSource(); + } + + @Override + public RandomSource getRandom() { + return delegate.getRandom(); + } + + @Override + public void updateNeighborsAt(BlockPos position, Block block) { + if (isWritableNeighbourhood(position)) { + delegate.updateNeighborsAt(position, block); + } + } + + @Override + public void neighborShapeChanged(Direction direction, BlockPos position, + BlockPos neighbourPosition, BlockState neighbourState, + int updateFlags, int updateLimit) { + if (isWritable(position) && isWritable(neighbourPosition)) { + delegate.neighborShapeChanged( + direction, position, neighbourPosition, neighbourState, updateFlags, updateLimit); + } + } + + @Override + public void playSound(Entity source, BlockPos position, SoundEvent sound, + SoundSource soundSource, float volume, float pitch) { + if (isWritable(position)) { + delegate.playSound(source, position, sound, soundSource, volume, pitch); + } + } + + @Override + public void addParticle(ParticleOptions particle, double x, double y, double z, + double velocityX, double velocityY, double velocityZ) { + if (isWritable(BlockPos.containing(x, y, z))) { + delegate.addParticle(particle, x, y, z, velocityX, velocityY, velocityZ); + } + } + + @Override + public void levelEvent(Entity source, int eventId, BlockPos position, int data) { + if (isWritable(position)) { + delegate.levelEvent(source, eventId, position, data); + } + } + + @Override + public void gameEvent(Holder event, Vec3 position, GameEvent.Context context) { + if (isWritable(BlockPos.containing(position))) { + delegate.gameEvent(event, position, context); + } + } + + @Override + public ChunkAccess getChunk(int chunkX, int chunkZ) { + if (isInsideGenerationRegion(chunkX, chunkZ)) { + return delegate.getChunk(chunkX, chunkZ); + } + return outsideChunk(chunkX, chunkZ); + } + + @Override + public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) { + if (isInsideGenerationRegion(chunkX, chunkZ)) { + return delegate.getChunk(chunkX, chunkZ, status, create); + } + return create ? outsideChunk(chunkX, chunkZ) : null; + } + + @Override + public boolean hasChunk(int chunkX, int chunkZ) { + return isInsideGenerationRegion(chunkX, chunkZ) && delegate.hasChunk(chunkX, chunkZ); + } + + @Override + public int getHeight(Heightmap.Types type, int x, int z) { + if (isInsideGenerationRegion(x >> 4, z >> 4)) { + return delegate.getHeight(type, x, z); + } + return height(type, x, z); + } + + @Override + public int getSkyDarken() { + return delegate.getSkyDarken(); + } + + @Override + public BiomeManager getBiomeManager() { + return biomeManager; + } + + @Override + public Holder getNoiseBiome(int quartX, int quartY, int quartZ) { + return getUncachedNoiseBiome(quartX, quartY, quartZ); + } + + @Override + public Holder getUncachedNoiseBiome(int quartX, int quartY, int quartZ) { + if (isInsideGenerationRegion(QuartPos.toSection(quartX), QuartPos.toSection(quartZ))) { + return delegate.getUncachedNoiseBiome(quartX, quartY, quartZ); + } + return fallbackBiome; + } + + @Override + public boolean isClientSide() { + return delegate.isClientSide(); + } + + @Override + public int getSeaLevel() { + return delegate.getSeaLevel(); + } + + @Override + public DimensionType dimensionType() { + return delegate.dimensionType(); + } + + @Override + public int getMinY() { + return delegate.getMinY(); + } + + @Override + public int getHeight() { + return delegate.getHeight(); + } + + @Override + public RegistryAccess registryAccess() { + return delegate.registryAccess(); + } + + @Override + public FeatureFlagSet enabledFeatures() { + return delegate.enabledFeatures(); + } + + @Override + public EnvironmentAttributeReader environmentAttributes() { + return delegate.environmentAttributes(); + } + + @Override + public LevelLightEngine getLightEngine() { + return delegate.getLightEngine(); + } + + @Override + public int getBrightness(LightLayer layer, BlockPos position) { + if (isReadable(position)) { + return delegate.getBrightness(layer, position); + } + return layer == LightLayer.SKY && canSeeSky(position) ? 15 : 0; + } + + @Override + public int getRawBrightness(BlockPos position, int ambientDarkening) { + if (isReadable(position)) { + return delegate.getRawBrightness(position, ambientDarkening); + } + return Math.max(0, getBrightness(LightLayer.SKY, position) - ambientDarkening); + } + + @Override + public boolean canSeeSky(BlockPos position) { + if (isReadable(position)) { + return delegate.canSeeSky(position); + } + return position.getY() >= height(Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ()); + } + + @Override + public WorldBorder getWorldBorder() { + return delegate.getWorldBorder(); + } + + @Override + public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) { + if (isInsideGenerationRegion(chunkX, chunkZ)) { + return delegate.getChunkForCollisions(chunkX, chunkZ); + } + return outsideChunk(chunkX, chunkZ); + } + + @Override + public BlockEntity getBlockEntity(BlockPos position) { + return isReadable(position) ? delegate.getBlockEntity(position) : null; + } + + @Override + public Optional getBlockEntity( + BlockPos position, BlockEntityType type) { + return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty(); + } + + @Override + public BlockState getBlockState(BlockPos position) { + return isReadable(position) ? delegate.getBlockState(position) : terrainState(position); + } + + @Override + public FluidState getFluidState(BlockPos position) { + return isReadable(position) ? delegate.getFluidState(position) : terrainState(position).getFluidState(); + } + + @Override + public List getEntities(Entity source, AABB area, Predicate predicate) { + AABB boundedArea = boundedArea(area); + return boundedArea == null ? List.of() : delegate.getEntities(source, boundedArea, predicate); + } + + @Override + public List getEntities( + EntityTypeTest type, + AABB area, Predicate predicate) { + AABB boundedArea = boundedArea(area); + return boundedArea == null ? List.of() : delegate.getEntities(type, boundedArea, predicate); + } + + @Override + public List players() { + return delegate.players(); + } + + @Override + public boolean isStateAtPosition(BlockPos position, Predicate predicate) { + return predicate.test(getBlockState(position)); + } + + @Override + public boolean isFluidAtPosition(BlockPos position, Predicate predicate) { + return predicate.test(getFluidState(position)); + } + + @Override + public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) { + return position.atY(getHeight(type, position.getX(), position.getZ())); + } + + @Override + public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) { + return isWritable(position) && delegate.setBlock(position, state, updateFlags, updateLimit); + } + + @Override + public boolean removeBlock(BlockPos position, boolean move) { + return isWritable(position) && 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); + } + + @Override + public boolean addFreshEntity(Entity entity) { + return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity); + } + + + private boolean isReadable(BlockPos position) { + return isInsideGenerationRegion(position.getX() >> 4, position.getZ() >> 4) + && !isOutsideBuildHeight(position); + } + + private boolean isWritable(BlockPos position) { + return isReadable(position); + } + + private boolean isWritableNeighbourhood(BlockPos position) { + return isWritable(position) + && isWritable(position.north()) + && isWritable(position.south()) + && isWritable(position.east()) + && isWritable(position.west()) + && isWritable(position.above()) + && isWritable(position.below()); + } + + private AABB boundedArea(AABB area) { + double minX = Math.max(area.minX, generationBounds.minX); + double minY = Math.max(area.minY, generationBounds.minY); + double minZ = Math.max(area.minZ, generationBounds.minZ); + double maxX = Math.min(area.maxX, generationBounds.maxX); + double maxY = Math.min(area.maxY, generationBounds.maxY); + double maxZ = Math.min(area.maxZ, generationBounds.maxZ); + if (minX >= maxX || minY >= maxY || minZ >= maxZ) { + return null; + } + if (minX == area.minX && minY == area.minY && minZ == area.minZ + && maxX == area.maxX && maxY == area.maxY && maxZ == area.maxZ) { + return area; + } + return new AABB(minX, minY, minZ, maxX, maxY, maxZ); + } + + boolean isInsideGenerationRegion(int chunkX, int chunkZ) { + return Math.abs(chunkX - generationCenter.x()) <= WRITE_RADIUS + && Math.abs(chunkZ - generationCenter.z()) <= WRITE_RADIUS; + } + + @Override + public boolean isOutsideBuildHeight(BlockPos position) { + return position.getY() < getMinY() || position.getY() >= getMaxY(); + } + + private BlockState terrainState(BlockPos position) { + if (isOutsideBuildHeight(position)) { + return Blocks.VOID_AIR.defaultBlockState(); + } + long heights = heights(position.getX(), position.getZ()); + int surface = (int) (heights >> 32); + int floor = (int) heights; + if (position.getY() < floor) { + return Blocks.STONE.defaultBlockState(); + } + if (position.getY() < surface) { + return Blocks.WATER.defaultBlockState(); + } + return Blocks.AIR.defaultBlockState(); + } + + private int height(Heightmap.Types type, int x, int z) { + long heights = heights(x, z); + if (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG) { + return (int) heights; + } + return (int) (heights >> 32); + } + + private long heights(int x, int z) { + long key = ((long) x << 32) ^ (z & 0xffffffffL); + long cached = terrainHeights.get(key); + if (cached != Long.MIN_VALUE) { + return cached; + } + int floor = Mth.clamp(floorFirstFreeY.applyAsInt(x, z), getMinY(), getMaxY()); + int surface = Mth.clamp(surfaceFirstFreeY.applyAsInt(x, z), floor, getMaxY()); + long heights = ((long) surface << 32) | (floor & 0xffffffffL); + terrainHeights.put(key, heights); + return heights; + } + + private ChunkAccess outsideChunk(int chunkX, int chunkZ) { + long key = ChunkPos.pack(chunkX, chunkZ); + ChunkAccess cached = outsideChunks.get(key); + if (cached != null) { + return cached; + } + EmptyLevelChunk chunk = new EmptyLevelChunk(getLevel(), new ChunkPos(chunkX, chunkZ), fallbackBiome); + WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY, floorFirstFreeY); + outsideChunks.put(key, chunk); + return chunk; + } + + private record Boundary(ChunkPos generationCenter, + IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { + } + + private static final class BoundedTickAccess implements LevelTickAccess { + private final LevelTickAccess delegate; + private final Predicate writable; + + private BoundedTickAccess(LevelTickAccess delegate, Predicate writable) { + this.delegate = delegate; + this.writable = writable; + } + + @Override + public void schedule(ScheduledTick tick) { + if (writable.test(tick.pos())) { + delegate.schedule(tick); + } + } + + @Override + public boolean hasScheduledTick(BlockPos position, T type) { + return writable.test(position) && delegate.hasScheduledTick(position, type); + } + + @Override + public int count() { + return delegate.count(); + } + + @Override + public boolean willTickThisTick(BlockPos position, T type) { + return writable.test(position) && delegate.willTickThisTick(position, type); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedParityProbe.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedParityProbe.java new file mode 100644 index 000000000..d02e404ac --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedParityProbe.java @@ -0,0 +1,393 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisEngine; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; +import net.minecraft.server.MinecraftServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public final class ModdedParityProbe { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final String DIMENSION_KEY = "overworld"; + private static final long SEED = 1337L; + private static final int BIOME_STEP = 4; + private static final String DEFAULT_EXPECTED = "922bbcbe766d"; + private static final List REPORTED = Collections.synchronizedList(new ArrayList<>()); + + private ModdedParityProbe() { + } + + public static void schedule(String config) { + Thread thread = new Thread(() -> waitAndRun(config), "Iris Parity Probe"); + thread.setDaemon(true); + thread.start(); + } + + private static void waitAndRun(String config) { + long start = System.currentTimeMillis(); + MinecraftServer server = null; + while (System.currentTimeMillis() - start < 600000L) { + MinecraftServer candidate = ModdedEngineBootstrap.currentServer(); + if (candidate != null && candidate.isReady()) { + server = candidate; + break; + } + try { + Thread.sleep(250L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + + if (server == null) { + LOGGER.error("[parity] server did not become ready within 10 minutes"); + return; + } + + boolean match = false; + try { + match = run(server, config); + } catch (Throwable e) { + LOGGER.error("[parity] probe failed", e); + } + + LOGGER.info("[parity] shutting down dev server (result={})", match ? "MATCH" : "MISMATCH"); + server.halt(false); + } + + private static boolean run(MinecraftServer server, String config) throws Exception { + String packPath = config; + int radius = 8; + int lastColon = config.lastIndexOf(':'); + if (lastColon > 0) { + String tail = config.substring(lastColon + 1); + try { + radius = Integer.parseInt(tail); + packPath = config.substring(0, lastColon); + } catch (NumberFormatException ignored) { + } + } + + File packSource = new File(packPath); + if (!packSource.isDirectory()) { + LOGGER.error("[parity] pack folder not found: {}", packSource.getAbsolutePath()); + return false; + } + + ModdedEngineBootstrap.bind(); + ModdedPlatform.errorSink(REPORTED::add); + + File workRoot = Files.createTempDirectory("iris-parity").toFile(); + File pack = clonePack(packSource, workRoot); + LOGGER.info("[parity] pack: {}", packSource.getAbsolutePath()); + LOGGER.info("[parity] work copy: {}", pack.getAbsolutePath()); + LOGGER.info("[parity] radius: {} ({} chunks)", radius, (2 * radius + 1) * (2 * radius + 1)); + + IrisData data = IrisData.get(pack); + IrisDimension dimension = data.getDimensionLoader().load(DIMENSION_KEY); + if (dimension == null) { + LOGGER.error("[parity] dimension '{}' did not load from {}", DIMENSION_KEY, pack.getAbsolutePath()); + return false; + } + + IrisWorld world = IrisWorld.builder() + .platformIdentity("iris:parity") + .name("parity") + .seed(SEED) + .worldFolder(new File(workRoot, "world")) + .minHeight(dimension.getMinHeight()) + .maxHeight(dimension.getMaxHeight()) + .build(); + EngineTarget target = new EngineTarget(world, dimension, data); + Engine engine = new IrisEngine(target, IrisEngine.InitializationMode.RUNTIME); + + settle(); + int minY = dimension.getMinHeight(); + int maxY = dimension.getMaxHeight(); + int height = maxY - minY; + LOGGER.info("[parity] engine up: dim={} seed={} minY={} maxY={}", engine.getDimension().getLoadKey(), engine.getSeedManager().getSeed(), minY, maxY); + + Map goldenChunks = new HashMap<>(); + String goldenCombined = null; + int goldenRadius = -1; + String goldenPath = System.getProperty("iris.parity.golden"); + if (goldenPath != null) { + List goldenLines = Files.readAllLines(Path.of(goldenPath), StandardCharsets.UTF_8); + for (String line : goldenLines) { + if (line.startsWith("#")) { + int eq = line.indexOf('='); + if (eq > 0) { + String metaKey = line.substring(1, eq); + String metaValue = line.substring(eq + 1); + if (metaKey.equals("combined")) { + goldenCombined = metaValue; + } + if (metaKey.equals("radius")) { + goldenRadius = Integer.parseInt(metaValue.trim()); + } + } + } else if (!line.isBlank()) { + int second = line.indexOf(' ', line.indexOf(' ') + 1); + goldenChunks.put(line.substring(0, second), line); + } + } + LOGGER.info("[parity] golden: {} ({} chunks, combined={})", goldenPath, goldenChunks.size(), goldenCombined); + } + + PlatformBlockState airState = IrisPlatforms.get().registries().air(); + List targets = orderedTargets(0, 0, radius); + Map lines = new TreeMap<>(); + List mismatches = new ArrayList<>(); + int failed = 0; + + for (int[] at : targets) { + int cx = at[0]; + int cz = at[1]; + drainReported(); + ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, airState); + Hunk biomes = Hunk.newArrayHunk(16, height, 16); + List failures = new ArrayList<>(); + try { + engine.generate(cx << 4, cz << 4, blocks, biomes, false); + } catch (Throwable e) { + failures.add(e); + } + failures.addAll(drainReported()); + + if (!failures.isEmpty()) { + failed++; + LOGGER.error("[parity] chunk {},{} FAILED ({} error(s))", cx, cz, failures.size()); + for (Throwable failure : failures) { + LOGGER.error("[parity] chunk {},{} error", cx, cz, failure); + } + continue; + } + + String line = hashChunk(cx, cz, blocks, biomes, height); + lines.put(chunkKey(cx, cz), line); + + String key = cx + " " + cz; + String golden = goldenChunks.get(key); + if (golden != null && !golden.equals(line)) { + mismatches.add(key); + LOGGER.warn("[parity] chunk {} MISMATCH", key); + LOGGER.warn("[parity] golden: {}", golden); + LOGGER.warn("[parity] actual: {}", line); + if (mismatches.size() == 1) { + diffDeep(cx, cz, blocks, height, minY); + } + } + } + + List body = new ArrayList<>(lines.values()); + String combined = combinedHash(body); + String expected = goldenCombined != null && (goldenRadius == radius || goldenRadius < 0) + ? goldenCombined.substring(0, 12) + : DEFAULT_EXPECTED; + boolean combinedMatch = combined.startsWith(expected); + boolean chunkMatch = mismatches.isEmpty() && failed == 0; + boolean match = goldenChunks.isEmpty() ? combinedMatch : (chunkMatch && (radius != 8 || combinedMatch)); + + if (!goldenChunks.isEmpty()) { + LOGGER.info("[parity] per-chunk: {}/{} matched golden ({} failed)", body.size() - mismatches.size(), body.size(), failed); + } + LOGGER.info("[parity] combined={} expected={} {} ({}/{})", + combined.substring(0, 12), expected, match ? "MATCH" : "MISMATCH", body.size() - mismatches.size(), targets.size()); + return match; + } + + private static void diffDeep(int cx, int cz, ModdedBlockBuffer blocks, int height, int minY) { + String deepDir = System.getProperty("iris.parity.deep"); + if (deepDir == null) { + return; + } + try { + Path goldenDump = Path.of(deepDir, cx + "_" + cz + ".txt"); + if (!Files.exists(goldenDump)) { + LOGGER.warn("[parity] no deep dump for chunk {},{} at {}", cx, cz, goldenDump); + return; + } + List golden = Files.readAllLines(goldenDump, StandardCharsets.UTF_8); + List actual = new ArrayList<>(); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < height; y++) { + String state = blocks.get(x, y, z).key(); + if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) { + actual.add(x + " " + (y + minY) + " " + z + " " + state); + } + } + } + } + int shown = 0; + int max = Math.max(golden.size(), actual.size()); + for (int i = 0; i < max && shown < 20; i++) { + String g = i < golden.size() ? golden.get(i) : ""; + String a = i < actual.size() ? actual.get(i) : ""; + if (!g.equals(a)) { + LOGGER.warn("[parity] deep diff line {}: golden='{}' actual='{}'", i, g, a); + shown++; + } + } + File out = new File(IrisPlatforms.get().dataFolder("parity"), "deep-" + cx + "_" + cz + ".txt"); + Files.write(out.toPath(), actual, StandardCharsets.UTF_8); + LOGGER.warn("[parity] full actual dump: {}", out.getAbsolutePath()); + } catch (Throwable e) { + LOGGER.warn("[parity] deep diff failed", e); + } + } + + private static List orderedTargets(int centerX, int centerZ, int radius) { + List targets = new ArrayList<>(); + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + targets.add(new int[]{centerX + dx, centerZ + dz}); + } + } + targets.sort(Comparator.comparingInt((int[] t) -> { + int ox = t[0] - centerX; + int oz = t[1] - centerZ; + return ox * ox + oz * oz; + })); + return targets; + } + + private static String hashChunk(int chunkX, int chunkZ, ModdedBlockBuffer blocks, Hunk biomes, int height) { + MessageDigest blockDigest = sha256(); + MessageDigest biomeDigest = sha256(); + Map blockCache = new HashMap<>(); + Map biomeCache = new HashMap<>(); + byte[] plains = "minecraft:plains\n".getBytes(StandardCharsets.UTF_8); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < height; y++) { + PlatformBlockState state = blocks.get(x, y, z); + byte[] bytes = blockCache.computeIfAbsent(state, (PlatformBlockState s) -> (s.key() + "\n").getBytes(StandardCharsets.UTF_8)); + blockDigest.update(bytes); + } + } + } + + for (int x = 0; x < 16; x += BIOME_STEP) { + for (int z = 0; z < 16; z += BIOME_STEP) { + for (int y = 0; y < height; y += BIOME_STEP) { + PlatformBiome biome = biomes.get(x, y, z); + byte[] bytes = biome == null + ? plains + : biomeCache.computeIfAbsent(biome, (PlatformBiome b) -> (b.key() + "\n").getBytes(StandardCharsets.UTF_8)); + biomeDigest.update(bytes); + } + } + } + + return chunkX + " " + chunkZ + " " + + HexFormat.of().formatHex(blockDigest.digest()) + " " + + HexFormat.of().formatHex(biomeDigest.digest()); + } + + private static String combinedHash(List body) { + MessageDigest digest = sha256(); + for (String line : body) { + digest.update((line + "\n").getBytes(StandardCharsets.UTF_8)); + } + return HexFormat.of().formatHex(digest.digest()); + } + + private static long chunkKey(int x, int z) { + return (((long) x) << 32) ^ (z & 0xFFFFFFFFL); + } + + private static File clonePack(File source, File workRoot) throws Exception { + File destination = new File(workRoot, source.getName()); + Process clone = new ProcessBuilder("cp", "-Rc", source.getAbsolutePath(), destination.getAbsolutePath()) + .inheritIO() + .start(); + if (clone.waitFor() != 0) { + Process copy = new ProcessBuilder("cp", "-R", source.getAbsolutePath(), destination.getAbsolutePath()) + .inheritIO() + .start(); + if (copy.waitFor() != 0) { + throw new IllegalStateException("Failed to copy pack to " + destination.getAbsolutePath()); + } + } + return destination; + } + + private static void settle() throws InterruptedException { + long quietSince = System.currentTimeMillis(); + long start = quietSince; + while (System.currentTimeMillis() - start < 15000L) { + List batch = drainReported(); + if (batch.isEmpty()) { + if (System.currentTimeMillis() - quietSince >= 1500L) { + break; + } + } else { + for (Throwable error : batch) { + LOGGER.warn("[parity] engine-init reported error (non-fatal)", error); + } + quietSince = System.currentTimeMillis(); + } + Thread.sleep(50L); + } + } + + private static List drainReported() { + synchronized (REPORTED) { + List drained = new ArrayList<>(REPORTED); + REPORTED.clear(); + return drained; + } + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java new file mode 100644 index 000000000..fe2c9420b --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java @@ -0,0 +1,370 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.BuildConstants; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.LogLevel; +import art.arcane.iris.spi.PlatformBiomeWriter; +import art.arcane.iris.spi.PlatformEntityType; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.spi.PlatformScheduler; +import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.iris.spi.PlatformWorld; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +public final class ModdedPlatform implements IrisPlatform { + private static final int ERROR_SIGNATURE_BURST = 5; + private static final int ERROR_SIGNATURE_CAPACITY = 256; + private static final long ERROR_SUMMARY_INTERVAL_MILLIS = 300_000L; + private static final ConcurrentHashMap ERROR_THROTTLES = new ConcurrentHashMap<>(); + + private static volatile Consumer ERROR_SINK = null; + private static volatile Consumer CAPTURE_SINK = null; + + private final ModdedLoader loader; + private final ModdedRegistries registries; + private final ModdedScheduler scheduler; + private final ModdedStructureHooks structureHooks; + private final ModdedBiomeWriter biomeWriter; + + public ModdedPlatform(ModdedLoader loader) { + this.loader = loader; + this.registries = new ModdedRegistries(ModdedEngineBootstrap::currentServer); + this.scheduler = new ModdedScheduler(); + this.structureHooks = new ModdedStructureHooks(ModdedEngineBootstrap::currentServer); + this.biomeWriter = new ModdedBiomeWriter(ModdedEngineBootstrap::currentServer); + } + + public static void errorSink(Consumer sink) { + ERROR_SINK = sink; + } + + public static void captureSink(Consumer sink) { + CAPTURE_SINK = sink; + } + + public MinecraftServer server() { + return ModdedEngineBootstrap.currentServer(); + } + + public ModdedScheduler moddedScheduler() { + return scheduler; + } + + @Override + public String platformName() { + return loader.platformName(); + } + + @Override + public String minecraftVersion() { + return loader.minecraftVersion(); + } + + @Override + public PlatformRegistries registries() { + return registries; + } + + @Override + public PlatformScheduler scheduler() { + return scheduler; + } + + @Override + public PlatformStructureHooks structureHooks() { + return structureHooks; + } + + @Override + public PlatformBiomeWriter biomeWriter() { + return biomeWriter; + } + + @Override + public File dataFolder() { + File folder = loader.configDir().resolve("iris").toFile(); + folder.mkdirs(); + return folder; + } + + /** + * Modded packs live under config/irisworldgen/packs, not the config/iris data folder. The + * packsFolder overrides are the source of truth; the dataFolder/dataFile overrides below + * re-root any path whose FIRST segment is exactly "packs" so no call site can regress onto + * the empty config/iris/packs directory. Settings, worlds.json, parity/, cache/ and every + * other name stay under config/iris. + */ + @Override + public File packsFolder(String... sub) { + File folder = packsFolderNoCreate(sub); + folder.mkdirs(); + return folder; + } + + @Override + public File packsFolderNoCreate(String... sub) { + File root = loader.configDir().resolve("irisworldgen").resolve("packs").toFile(); + if (sub == null || sub.length == 0) { + return root; + } + return new File(root, String.join(File.separator, sub)); + } + + @Override + public File dataFolder(String... path) { + if (isPacksPath(path)) { + return packsFolder(stripPacksSegment(path)); + } + return IrisPlatform.super.dataFolder(path); + } + + @Override + public File dataFolderNoCreate(String... path) { + if (isPacksPath(path)) { + return packsFolderNoCreate(stripPacksSegment(path)); + } + return IrisPlatform.super.dataFolderNoCreate(path); + } + + @Override + public File dataFile(String... path) { + if (isPacksPath(path)) { + File file = packsFolderNoCreate(stripPacksSegment(path)); + file.getParentFile().mkdirs(); + return file; + } + File file = new File(dataFolder(), String.join(File.separator, path)); + file.getParentFile().mkdirs(); + return file; + } + + private static boolean isPacksPath(String... path) { + // Exact-segment match only: "packbenchmarks" and "packsx" must stay under config/iris. + return path != null && path.length > 0 && "packs".equals(path[0]); + } + + private static String[] stripPacksSegment(String... path) { + String[] sub = new String[path.length - 1]; + System.arraycopy(path, 1, sub, 0, sub.length); + return sub; + } + + @Override + public File pluginJar() { + File jar = loader.modJar(); + return jar != null ? jar : new File(dataFolder(), "iris-" + loader.platformName() + ".jar"); + } + + @Override + public int irisVersionNumber() { + return parseVersion(loader.modVersion()); + } + + @Override + public int minecraftVersionNumber() { + int fromLoader = parseVersion(loader.minecraftVersion()); + return fromLoader > 0 ? fromLoader : parseVersion(BuildConstants.MINECRAFT_VERSION); + } + + @Override + public void callEvent(Object event) { + } + + @Override + public void dispatchConsoleCommand(String command) { + ModdedServerCommands.dispatch(ModdedEngineBootstrap.currentServer(), command); + } + + @Override + public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) { + if (world == null || entityKey == null || !(world.nativeHandle() instanceof ServerLevel level)) { + return false; + } + PlatformEntityType resolved = registries.entity(entityKey); + if (resolved == null) { + return false; + } + EntityType type = (EntityType) resolved.nativeHandle(); + BlockPos pos = BlockPos.containing(x, y, z); + Entity entity = type.spawn(level, pos, EntitySpawnReason.COMMAND); + return entity != null; + } + + @Override + public void log(LogLevel level, String message) { + ModdedIrisLog.log(level, message); + } + + @Override + public void msg(String message) { + ModdedIrisLog.info(message); + } + + /** + * Throttled per exception signature. A single broken pack rule can fail on every generated chunk, and this + * feeds Sentry, so each distinct signature reports its first few occurrences and then only a periodic + * suppressed-count summary. + */ + @Override + public void reportError(Throwable error) { + reportThrottled("Iris reported error", error); + } + + /** + * Contextual reports run the same throttle and emit exactly one trace through the modded + * log; the SPI default's raw stderr copy would bypass the per-signature suppression. + */ + @Override + public void reportError(String context, Throwable error) { + reportThrottled(context == null || context.isBlank() ? "Iris reported error" : context, error); + } + + private void reportThrottled(String message, Throwable error) { + if (error == null) { + return; + } + if (!allowErrorReport(error)) { + return; + } + Consumer sink = ERROR_SINK; + if (sink != null) { + sink.accept(error); + return; + } + ModdedIrisLog.error(message, error); + Consumer capture = CAPTURE_SINK; + if (capture != null) { + try { + capture.accept(error); + } catch (Throwable captureFailure) { + ModdedIrisLog.error("Iris error-reporting sink failed", captureFailure); + } + } + } + + static void resetErrorThrottles() { + ERROR_THROTTLES.clear(); + } + + private static boolean allowErrorReport(Throwable error) { + String signature = errorSignature(error); + ErrorThrottle throttle = ERROR_THROTTLES.get(signature); + if (throttle == null) { + if (ERROR_THROTTLES.size() >= ERROR_SIGNATURE_CAPACITY) { + evictLeastRecentlySeen(); + } + throttle = ERROR_THROTTLES.computeIfAbsent(signature, ignored -> new ErrorThrottle()); + } + return throttle.allow(signature); + } + + /** + * At the cap a new signature used to report through unthrottled for the rest of the uptime, which is the + * failure mode the cap exists to prevent: one broken pack rule firing on every chunk with a per-chunk line + * number produces new signatures forever. Evict the entry nothing has hit for the longest instead. O(n) on + * an error path with n=256, and an evicted signature simply earns a fresh burst if it comes back. + */ + private static void evictLeastRecentlySeen() { + String oldest = null; + long oldestSeenAt = Long.MAX_VALUE; + for (Map.Entry entry : ERROR_THROTTLES.entrySet()) { + long seenAt = entry.getValue().lastSeenAt(); + if (seenAt < oldestSeenAt) { + oldestSeenAt = seenAt; + oldest = entry.getKey(); + } + } + if (oldest != null) { + ERROR_THROTTLES.remove(oldest); + } + } + + static String errorSignature(Throwable error) { + StackTraceElement[] trace = error.getStackTrace(); + String frame = trace.length == 0 + ? "" + : trace[0].getClassName() + '.' + trace[0].getMethodName() + ':' + trace[0].getLineNumber(); + return error.getClass().getName() + '@' + frame; + } + + private static final class ErrorThrottle { + private final AtomicLong reported = new AtomicLong(); + private final AtomicLong suppressed = new AtomicLong(); + private final AtomicLong nextSummaryAt = new AtomicLong(); + private final AtomicLong lastSeenAt = new AtomicLong(System.currentTimeMillis()); + + private long lastSeenAt() { + return lastSeenAt.get(); + } + + private boolean allow(String signature) { + long now = System.currentTimeMillis(); + lastSeenAt.set(now); + if (reported.get() < ERROR_SIGNATURE_BURST) { + reported.incrementAndGet(); + return true; + } + long total = suppressed.incrementAndGet(); + long due = nextSummaryAt.get(); + if (now >= due && nextSummaryAt.compareAndSet(due, now + ERROR_SUMMARY_INTERVAL_MILLIS)) { + ModdedIrisLog.warn("Iris suppressed " + total + " repeats of " + signature); + } + return false; + } + } + + private static int parseVersion(String raw) { + if (raw == null || raw.isBlank()) { + return -1; + } + int hyphen = raw.indexOf('-'); + String head = hyphen >= 0 ? raw.substring(0, hyphen) : raw; + StringBuilder digits = new StringBuilder(head.length()); + for (int i = 0; i < head.length(); i++) { + char ch = head.charAt(i); + if (ch >= '0' && ch <= '9') { + digits.append(ch); + } else if (ch != '.') { + break; + } + } + if (digits.isEmpty()) { + return -1; + } + try { + long value = Long.parseLong(digits.toString()); + return value > Integer.MAX_VALUE ? -1 : (int) value; + } catch (NumberFormatException error) { + return -1; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatformWorld.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatformWorld.java new file mode 100644 index 000000000..4c7ef31c3 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatformWorld.java @@ -0,0 +1,108 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformWorld; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.state.BlockState; + +public final class ModdedPlatformWorld implements PlatformWorld { + private final ServerLevel level; + + public ModdedPlatformWorld(ServerLevel level) { + this.level = level; + } + + @Override + public String name() { + return level.dimension().identifier().toString(); + } + + @Override + public long seed() { + return level.getSeed(); + } + + @Override + public int minHeight() { + return level.getMinY(); + } + + @Override + public int maxHeight() { + return exclusiveMaxHeight(level.getMinY(), level.getHeight()); + } + + @Override + public PlatformBlockState getBlock(int x, int y, int z) { + return ModdedBlockState.of(level.getBlockState(new BlockPos(x, y, z)), null); + } + + @Override + public void setBlock(int x, int y, int z, PlatformBlockState block, int flags) { + level.setBlock(new BlockPos(x, y, z), (BlockState) block.nativeHandle(), flags); + } + + @Override + public PlatformBiome getBiome(int x, int y, int z) { + Holder biome = level.getBiome(new BlockPos(x, y, z)); + String key = biome.unwrapKey() + .map(resourceKey -> resourceKey.identifier().toString()) + .orElse("minecraft:plains"); + return ModdedBiome.of(biome.value(), key); + } + + @Override + public boolean isChunkLoaded(int chunkX, int chunkZ) { + return level.getChunkSource().getChunkNow(chunkX, chunkZ) != null; + } + + @Override + public long getTime() { + return level.getDefaultClockTime(); + } + + @Override + public boolean isStorming() { + return level.isRaining(); + } + + @Override + public boolean isThundering() { + return level.isThundering(); + } + + @Override + public Object nativeHandle() { + return level; + } + + ServerLevel level() { + return level; + } + + static int exclusiveMaxHeight(int minHeight, int height) { + return minHeight + height; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPrimaryWorldRouter.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPrimaryWorldRouter.java new file mode 100644 index 000000000..3d73650a2 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPrimaryWorldRouter.java @@ -0,0 +1,103 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedPrimaryWorldRouter { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int TICK_INTERVAL = 20; + + private static final Set routed = ConcurrentHashMap.newKeySet(); + private static int tickCounter = 0; + + private ModdedPrimaryWorldRouter() { + } + + public static void clear() { + routed.clear(); + } + + /** + * Drops a disconnected player's routing mark. Without this the set grows with every unique player the + * server has ever seen, and a returning player is never routed again. + */ + public static void forget(UUID player) { + if (player != null) { + routed.remove(player); + } + } + + public static void tick(MinecraftServer server) { + if (server == null) { + return; + } + tickCounter++; + if (tickCounter < TICK_INTERVAL) { + return; + } + tickCounter = 0; + + ModdedModConfig config = ModdedModConfig.get(); + if (!config.routePlayersToPrimaryWorld()) { + return; + } + String primary = config.primaryWorld(); + if (primary.isBlank()) { + return; + } + + ServerLevel target = ModdedDimensionManager.level(server, primary); + if (target == null) { + return; + } + ServerLevel overworld = server.overworld(); + if (target == overworld) { + return; + } + + List players = new ArrayList<>(server.getPlayerList().getPlayers()); + for (ServerPlayer player : players) { + UUID id = player.getUUID(); + if (routed.contains(id)) { + continue; + } + if (player.level() != overworld) { + routed.add(id); + continue; + } + try { + ModdedDimensionManager.teleport(player, server, primary, player.getX(), Double.MIN_VALUE, player.getZ()); + routed.add(id); + } catch (Throwable e) { + LOGGER.error("Iris failed to route player {} to primary world '{}'", id, primary, e); + } + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolChannel.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolChannel.java new file mode 100644 index 000000000..f21db9250 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolChannel.java @@ -0,0 +1,27 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import net.minecraft.server.level.ServerPlayer; + +public interface ModdedProtocolChannel { + boolean canReceive(ServerPlayer player); + + void send(ServerPlayer player, ModdedIrisPayload payload); +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolHandler.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolHandler.java new file mode 100644 index 000000000..297b825cb --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolHandler.java @@ -0,0 +1,240 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.protocol.EngineResolver; +import art.arcane.iris.core.protocol.IrisProtocolServer; +import art.arcane.iris.core.protocol.IrisSession; +import art.arcane.iris.core.protocol.IrisSessionRegistry; +import art.arcane.iris.core.protocol.IrisVisionRequestService; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisProtocol; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedProtocolHandler { + private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN + | IrisProtocol.CAPABILITY_VISION + | IrisProtocol.CAPABILITY_CURSOR + | IrisProtocol.CAPABILITY_STUDIO; + private static final int DIMENSION_SYNC_INTERVAL_TICKS = 5; + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private static final ConcurrentHashMap SESSION_ENGINES = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap SESSION_LEVELS = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap SESSION_IDS = new ConcurrentHashMap<>(); + + private static volatile ModdedProtocolChannel channel; + private static volatile IrisSessionRegistry registry; + private static volatile IrisProtocolServer protocolServer; + private static volatile ModdedProtocolTransport transport; + private static volatile IrisVisionRequestService visionRequests; + private static int dimensionSyncTicks; + + private ModdedProtocolHandler() { + } + + public static void bindChannel(ModdedProtocolChannel boundChannel) { + channel = Objects.requireNonNull(boundChannel, "protocol channel"); + } + + public static void start(MinecraftServer server) { + ModdedProtocolChannel boundChannel = channel; + if (server == null || boundChannel == null) { + return; + } + SESSION_ENGINES.clear(); + SESSION_LEVELS.clear(); + SESSION_IDS.clear(); + dimensionSyncTicks = 0; + IrisSessionRegistry sessionRegistry = new IrisSessionRegistry(); + ModdedProtocolTransport serverTransport = new ModdedProtocolTransport(server, boundChannel); + IrisProtocolServer protocol = new IrisProtocolServer(sessionRegistry, SERVER_CAPABILITIES, brand(), true); + EngineResolver engineResolver = (String sessionId) -> { + Engine engine = SESSION_ENGINES.get(sessionId); + return engine == null || engine.isClosed() ? null : engine; + }; + protocol.setEngineResolver(engineResolver); + IrisVisionRequestService visionService = IrisVisionRequestService.create(engineResolver, sessionRegistry); + protocol.setVisionTileHandler(visionService); + registry = sessionRegistry; + transport = serverTransport; + protocolServer = protocol; + visionRequests = visionService; + IrisServices.register(IrisProtocolServer.class, protocol); + if (server.getPlayerList() == null) { + return; + } + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + sessionRegistry.register(new IrisSession(sessionId(player), serverTransport)); + } + } + + public static void stop() { + IrisServices.remove(IrisProtocolServer.class); + IrisSessionRegistry current = registry; + IrisVisionRequestService vision = visionRequests; + if (current != null) { + for (IrisSession session : current.all()) { + current.unregister(session.id()); + if (vision != null) { + vision.clearSession(session.id()); + } + } + } + SESSION_ENGINES.clear(); + SESSION_LEVELS.clear(); + SESSION_IDS.clear(); + dimensionSyncTicks = 0; + registry = null; + protocolServer = null; + transport = null; + visionRequests = null; + } + + public static void onPlayerJoin(ServerPlayer player) { + if (player == null) { + return; + } + String sessionId = sessionId(player); + ModdedStartup.warnPackFailuresTo(player); + IrisSessionRegistry current = registry; + ModdedProtocolTransport currentTransport = transport; + if (current == null || currentTransport == null) { + return; + } + current.register(new IrisSession(sessionId, currentTransport)); + } + + public static void onPlayerDisconnect(ServerPlayer player) { + if (player == null) { + return; + } + UUID id = player.getUUID(); + String sessionId = SESSION_IDS.remove(id); + if (sessionId == null) { + sessionId = id.toString(); + } + ModdedPrimaryWorldRouter.forget(id); + SESSION_ENGINES.remove(sessionId); + SESSION_LEVELS.remove(sessionId); + IrisSessionRegistry current = registry; + if (current != null) { + current.unregister(sessionId); + } + IrisVisionRequestService vision = visionRequests; + if (vision != null) { + vision.clearSession(sessionId); + } + } + + /** + * UUID.toString allocates a 36-char string every call; the dimension sync tick runs over every player four + * times a second, so the id is interned per player on first use and dropped on disconnect. + */ + private static String sessionId(ServerPlayer player) { + return SESSION_IDS.computeIfAbsent(player.getUUID(), UUID::toString); + } + + public static void onInbound(ServerPlayer player, byte[] frame) { + IrisProtocolServer current = protocolServer; + if (player == null || frame == null || current == null) { + return; + } + String sessionId = sessionId(player); + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + current.onClientFrame(sessionId, frame); + return; + } + scheduler.global(() -> current.onClientFrame(sessionId, frame)); + } + + public static void tickDimensionSync(MinecraftServer server) { + IrisSessionRegistry current = registry; + IrisProtocolServer protocol = protocolServer; + if (server == null || current == null || protocol == null) { + return; + } + dimensionSyncTicks++; + if (dimensionSyncTicks < DIMENSION_SYNC_INTERVAL_TICKS) { + return; + } + dimensionSyncTicks = 0; + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + String sessionId = sessionId(player); + IrisSession session = current.get(sessionId); + if (session == null || !session.isReady()) { + continue; + } + ServerLevel level = player.level(); + String levelId = level.dimension().identifier().toString(); + Engine cached = SESSION_ENGINES.get(sessionId); + if (levelId.equals(SESSION_LEVELS.get(sessionId)) && (cached == null || !cached.isClosed())) { + continue; + } + if (syncDimension(protocol, sessionId, level, levelId)) { + SESSION_LEVELS.put(sessionId, levelId); + } + } + } + + private static boolean syncDimension(IrisProtocolServer protocol, String sessionId, ServerLevel level, String levelId) { + ChunkGenerator generator = level.getChunkSource().getGenerator(); + // engineIfBound only, never commandEngine: the sync tick runs on the server thread and constructing an + // engine there stalls the tick for the whole pack load. An unbound generator simply retries next tick. + Engine engine = generator instanceof IrisModdedChunkGenerator irisGenerator ? resolveEngine(level, irisGenerator) : null; + if (generator instanceof IrisModdedChunkGenerator && engine == null) { + return false; + } + long seed = level.getSeed(); + if (engine != null) { + SESSION_ENGINES.put(sessionId, engine); + protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(), + seed, engine.getMinHeight(), engine.getMaxHeight(), true); + return true; + } + SESSION_ENGINES.remove(sessionId); + protocol.sendDimensionStatus(sessionId, levelId, "", seed, level.getMinY(), level.getMinY() + level.getHeight(), false); + return true; + } + + private static Engine resolveEngine(ServerLevel level, IrisModdedChunkGenerator generator) { + try { + Engine engine = generator.engineIfBound(); + return engine == null || engine.isClosed() ? null : engine; + } catch (Throwable failure) { + LOGGER.error("Iris dimension status engine lookup failed for {}", level.dimension().identifier(), failure); + return null; + } + } + + private static String brand() { + return "Iris " + ModdedEngineBootstrap.loader().modVersion() + " (" + ModdedEngineBootstrap.loader().platformName() + ")"; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolTransport.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolTransport.java new file mode 100644 index 000000000..a2daa7a5e --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolTransport.java @@ -0,0 +1,69 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.protocol.IrisServerTransport; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Objects; +import java.util.UUID; + +public final class ModdedProtocolTransport implements IrisServerTransport { + private final MinecraftServer server; + private final ModdedProtocolChannel channel; + + public ModdedProtocolTransport(MinecraftServer server, ModdedProtocolChannel channel) { + this.server = Objects.requireNonNull(server, "server"); + this.channel = Objects.requireNonNull(channel, "protocol channel"); + } + + @Override + public void sendToClient(String sessionId, byte[] frame) { + if (sessionId == null || frame == null) { + return; + } + UUID playerId = parseUuid(sessionId); + if (playerId == null) { + return; + } + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + return; + } + ModdedIrisPayload payload = new ModdedIrisPayload(frame); + scheduler.global(() -> deliver(playerId, payload)); + } + + private void deliver(UUID playerId, ModdedIrisPayload payload) { + ServerPlayer player = server.getPlayerList().getPlayer(playerId); + if (player == null || !channel.canReceive(player)) { + return; + } + channel.send(player, payload); + } + + private static UUID parseUuid(String sessionId) { + try { + return UUID.fromString(sessionId); + } catch (IllegalArgumentException invalid) { + return null; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRegistries.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRegistries.java new file mode 100644 index 000000000..19c269bd3 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRegistries.java @@ -0,0 +1,310 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.modded.api.ModdedCustomContentRegistry; +import art.arcane.iris.modded.api.ModdedDataType; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockProperty; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformEntityType; +import art.arcane.iris.spi.PlatformItem; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.volmlib.util.data.UnresolvedKeyLog; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.storage.loot.LootTable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + +public final class ModdedRegistries implements PlatformRegistries { + private static final UnresolvedKeyLog NOT_READY = new UnresolvedKeyLog("Iris modded registry reads before server ready", 60_000L); + + private final Supplier server; + + public ModdedRegistries(Supplier server) { + this.server = server; + } + + @Override + public PlatformBlockState block(String key) { + return ModdedBlockResolution.get(key); + } + + @Override + public PlatformBlockState blockOrNull(String key) { + return ModdedBlockResolution.getOrNull(key); + } + + @Override + public PlatformBlockState blockOrNull(String key, boolean warn) { + return ModdedBlockResolution.getOrNull(key, warn); + } + + @Override + public PlatformBlockState air() { + return ModdedBlockResolution.getAir(); + } + + @Override + public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) { + BlockState result = ModdedBlockResolution.toDeepSlateOre((BlockState) block.nativeHandle(), (BlockState) ore.nativeHandle()); + return ModdedBlockState.of(result, null); + } + + @Override + public PlatformBiome biome(String key) { + Identifier identifier = Identifier.tryParse(key); + if (identifier == null) { + return null; + } + Registry registry = biomeRegistry(); + if (registry == null) { + return null; + } + Biome biome = registry.getValue(identifier); + return biome == null ? null : ModdedBiome.of(biome, identifier.toString()); + } + + @Override + public PlatformItem item(String key) { + String normalized = key.trim().toLowerCase(Locale.ROOT).replace(' ', '_'); + Identifier identifier = Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized); + if (identifier == null || !BuiltInRegistries.ITEM.containsKey(identifier)) { + return null; + } + Item item = BuiltInRegistries.ITEM.getValue(identifier); + return ModdedItem.of(item, identifier.toString()); + } + + @Override + public PlatformEntityType entity(String key) { + Identifier identifier = Identifier.tryParse(key); + if (identifier == null || !BuiltInRegistries.ENTITY_TYPE.containsKey(identifier)) { + return null; + } + EntityType type = BuiltInRegistries.ENTITY_TYPE.getValue(identifier); + return ModdedEntityType.of(type, identifier.toString()); + } + + @Override + public List blockKeys() { + List keys = new ArrayList<>(); + for (Identifier identifier : BuiltInRegistries.BLOCK.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List biomeKeys() { + List keys = new ArrayList<>(); + Registry registry = biomeRegistry(); + if (registry == null) { + warnNotReady("biome"); + return keys; + } + for (Identifier identifier : registry.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List structureKeys() { + List keys = new ArrayList<>(); + MinecraftServer instance = server.get(); + if (instance == null) { + warnNotReady("structure"); + return keys; + } + for (Identifier identifier : instance.registryAccess().lookupOrThrow(Registries.STRUCTURE).keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List itemKeys() { + List keys = new ArrayList<>(); + for (Identifier identifier : BuiltInRegistries.ITEM.keySet()) { + keys.add(identifier.toString()); + } + keys.addAll(ModdedCustomContentRegistry.providerKeys(ModdedDataType.ITEM)); + return keys; + } + + @Override + public List specialEntityKeys() { + return ModdedCustomContentRegistry.providerKeys(ModdedDataType.ENTITY); + } + + @Override + public List entityKeys() { + List keys = new ArrayList<>(); + for (Identifier identifier : BuiltInRegistries.ENTITY_TYPE.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List blockTypeKeys() { + List keys = new ArrayList<>(); + for (Identifier identifier : BuiltInRegistries.BLOCK.keySet()) { + keys.add(identifier.toString()); + } + keys.addAll(customBlockKeys()); + return keys; + } + + @Override + public List enchantmentKeys() { + List keys = new ArrayList<>(); + Registry registry = enchantmentRegistry(); + if (registry == null) { + warnNotReady("enchantment"); + return keys; + } + for (Identifier identifier : registry.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List potionEffectKeys() { + List keys = new ArrayList<>(); + for (Identifier identifier : BuiltInRegistries.MOB_EFFECT.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List lootTableKeys() { + List keys = new ArrayList<>(); + MinecraftServer instance = server.get(); + if (instance == null) { + warnNotReady("loot table"); + return keys; + } + HolderLookup.RegistryLookup registry = instance.reloadableRegistries().lookup() + .lookupOrThrow(Registries.LOOT_TABLE); + registry.listElementIds().forEach(key -> keys.add(key.identifier().toString())); + return keys; + } + + @Override + public Map> blockStateProperties() { + Map> properties = new LinkedHashMap<>(); + // One List instance per identical property group. SchemaBuilder groups consecutive entries by list + // identity, so sharing collapses the emitted schema instead of writing a block-state object per block. + Map> shared = new LinkedHashMap<>(); + for (Block block : BuiltInRegistries.BLOCK) { + BlockState defaultState = block.defaultBlockState(); + List converted = new ArrayList<>(); + for (Property property : block.getStateDefinition().getProperties()) { + converted.add(convertProperty(property, defaultState)); + } + List group = shared.computeIfAbsent(groupSignature(converted), key -> List.copyOf(converted)); + properties.put(BuiltInRegistries.BLOCK.getKey(block).toString(), group); + } + List none = shared.computeIfAbsent(groupSignature(List.of()), key -> List.of()); + for (String key : customBlockKeys()) { + properties.putIfAbsent(key, none); + } + return properties; + } + + private static String groupSignature(List group) { + StringBuilder signature = new StringBuilder(group.size() * 24); + for (PlatformBlockProperty property : group) { + signature.append(property.name()).append(':').append(property.jsonType()).append('=') + .append(property.defaultValue()).append(property.allowedValues()).append(';'); + } + return signature.toString(); + } + + private static List customBlockKeys() { + List keys = new ArrayList<>(ModdedCustomContentRegistry.aliasBlockKeys()); + keys.addAll(ModdedCustomContentRegistry.providerKeys(ModdedDataType.BLOCK)); + return keys; + } + + private static void warnNotReady(String registryName) { + if (NOT_READY.firstOccurrence(registryName)) { + IrisLogging.warn("Iris registry read for '" + registryName + "' before the server is ready; returning empty"); + } + String summary = NOT_READY.pollSummary(); + if (summary != null) { + IrisLogging.warn(summary); + } + } + + private Registry biomeRegistry() { + MinecraftServer instance = server.get(); + if (instance == null) { + return null; + } + return instance.registryAccess().lookupOrThrow(Registries.BIOME); + } + + private Registry enchantmentRegistry() { + MinecraftServer instance = server.get(); + if (instance == null) { + return null; + } + return instance.registryAccess().lookupOrThrow(Registries.ENCHANTMENT); + } + + private static > PlatformBlockProperty convertProperty(Property property, BlockState defaultState) { + T defaultValue = defaultState.getValue(property); + Class valueClass = property.getValueClass(); + List allowedValues = new ArrayList<>(); + if (valueClass == Boolean.class || valueClass == Integer.class) { + for (T value : property.getPossibleValues()) { + allowedValues.add(value); + } + String jsonType = valueClass == Boolean.class ? "boolean" : "integer"; + return new PlatformBlockProperty(property.getName(), jsonType, defaultValue, List.copyOf(allowedValues), null); + } + for (T value : property.getPossibleValues()) { + allowedValues.add(property.getName(value)); + } + return new PlatformBlockProperty(property.getName(), "string", property.getName(defaultValue), List.copyOf(allowedValues), null); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRuntimeRegistry.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRuntimeRegistry.java new file mode 100644 index 000000000..f998a3680 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRuntimeRegistry.java @@ -0,0 +1,105 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.loader.IrisData; +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.util.common.data.DataProvider; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.dimension.DimensionType; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class ModdedRuntimeRegistry { + private ModdedRuntimeRegistry() { + } + + static void ensureDimensionType(Registry registry, + ResourceKey typeKey, String typeRef) { + if (registry.get(typeKey).isPresent()) { + return; + } + throw new IllegalStateException("Iris dimension type '" + typeRef + + "' is not registered. Minecraft freezes the dimension-type registry before Iris can add it," + + " so a pack installed after boot only takes effect on the next start." + + restartAdvice()); + } + + static void ensureCustomBiomes(RegistryAccess registryAccess, IrisDimension dimension, String pack) { + File packFolder = ModdedWorldEngines.packFolder(pack); + if (!packFolder.isDirectory()) { + return; + } + Registry registry = registryAccess.lookupOrThrow(Registries.BIOME); + IrisData data = IrisData.get(packFolder); + DataProvider provider = () -> data; + Set seen = new HashSet<>(); + List missing = new ArrayList<>(); + for (IrisBiome irisBiome : dimension.getAllBiomes(provider)) { + if (!irisBiome.isCustom()) { + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + String biomeId = customBiome.getId(); + if (!seen.add(biomeId)) { + continue; + } + String biomeRef = ModdedWorldgenIds.biomeRef(pack, dimension.getLoadKey(), biomeId); + ResourceKey biomeKey = ResourceKey.create(Registries.BIOME, Identifier.parse(biomeRef)); + if (registry.get(biomeKey).isEmpty()) { + missing.add(biomeRef); + } + } + } + if (!missing.isEmpty()) { + throw new IllegalStateException("Iris pack '" + pack + "' has " + missing.size() + + " custom biome(s) that are not registered. Pack '" + pack + + "' was installed after boot, and Minecraft freezes the biome registry before Iris can add them." + + restartAdvice() + " First missing entry: " + + missing.getFirst()); + } + } + + /** + * One complete instruction per environment. Singleplayer has no /iris world create step to follow the + * restart, so the client branch must not be suffixed with one. + */ + private static String restartAdvice() { + boolean client; + try { + client = ModdedEngineBootstrap.loader().clientEnvironment(); + } catch (RuntimeException unbound) { + client = false; + } + return client + ? " Quit to the title screen and re-create the world from the Iris world type." + : " Restart the server, then run /iris world create again."; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedScheduler.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedScheduler.java new file mode 100644 index 000000000..c3f46d4c6 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedScheduler.java @@ -0,0 +1,316 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.PlatformScheduler; +import art.arcane.iris.spi.PlatformWorld; +import net.minecraft.server.MinecraftServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public final class ModdedScheduler implements PlatformScheduler { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int ASYNC_MAX_THREADS = Math.max(4, Runtime.getRuntime().availableProcessors()); + private static final long ASYNC_KEEP_ALIVE_SECONDS = 30L; + private static final int ASYNC_BACKLOG_WARN = 8192; + private static final long ASYNC_BACKLOG_WARN_INTERVAL_MILLIS = 30000L; + private static final long DRAIN_BUDGET_NANOS = TimeUnit.MILLISECONDS.toNanos(5L); + private static final int DRAIN_TASK_CAP = 512; + + private static volatile Thread mainThread; + + private volatile ThreadPoolExecutor asyncExecutor; + private final ConcurrentLinkedQueue mainQueue = new ConcurrentLinkedQueue<>(); + private final PriorityBlockingQueue delayedQueue = new PriorityBlockingQueue<>(); + private final AtomicLong currentTick = new AtomicLong(); + private final AtomicLong delayedSequence = new AtomicLong(); + private final AtomicLong lastBacklogWarnAt = new AtomicLong(); + + public ModdedScheduler() { + this.asyncExecutor = createAsyncExecutor(); + } + + private static ThreadPoolExecutor createAsyncExecutor() { + // Unbounded queue: async work must never be executed inline on a tick thread (CallerRunsPolicy + // stalled the server tick under load). Core == max with core timeout keeps the pool elastic, + // which a LinkedBlockingQueue would otherwise pin to the core size. + BlockingQueue workQueue = new LinkedBlockingQueue<>(); + ThreadPoolExecutor executor = new ThreadPoolExecutor( + ASYNC_MAX_THREADS, + ASYNC_MAX_THREADS, + ASYNC_KEEP_ALIVE_SECONDS, + TimeUnit.SECONDS, + workQueue, + new AsyncThreadFactory(), + dropRejectedTask()); + executor.allowCoreThreadTimeOut(true); + return executor; + } + + private static RejectedExecutionHandler dropRejectedTask() { + return (Runnable task, ThreadPoolExecutor executor) -> { + if (task instanceof RejectionAwareTask rejectionAwareTask) { + rejectionAwareTask.reject(); + } + if (executor.isShutdown()) { + LOGGER.debug("Iris async task dropped: scheduler is shut down"); + return; + } + LOGGER.error("Iris async task rejected by the executor (queued={} active={})", + executor.getQueue().size(), executor.getActiveCount()); + }; + } + + public static void tick(MinecraftServer server) { + if (server == null) { + return; + } + Thread running = server.getRunningThread(); + if (mainThread != running) { + mainThread = running; + } + // First thing in the Iris tick body: keep the off-thread level snapshot current for levels registered + // outside ModdedServerAccess (vanilla boot, other mods) before the rest of the tick reads it. + ModdedServerLevels.refreshIfStale(server); + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + return; + } + scheduler.drain(); + } + + @Override + public void global(Runnable task) { + if (task == null) { + return; + } + if (onMainThread()) { + runGuarded(task); + return; + } + mainQueue.add(task); + } + + @Override + public void region(PlatformWorld world, int chunkX, int chunkZ, Runnable task) { + global(task); + } + + @Override + public void async(Runnable task) { + if (task == null) { + return; + } + ThreadPoolExecutor executor = asyncExecutor; + warnOnBacklog(executor); + executor.execute(() -> runGuarded(task)); + } + + public boolean asyncIfRunning(Runnable task, Runnable rejection) { + if (task == null) { + return false; + } + ThreadPoolExecutor executor = asyncExecutor; + RejectionAwareTask submittedTask = new RejectionAwareTask( + () -> runGuarded(task), + Objects.requireNonNull(rejection, "rejection") + ); + warnOnBacklog(executor); + try { + executor.execute(submittedTask); + } catch (RejectedExecutionException exception) { + submittedTask.reject(); + } + return !submittedTask.isRejected(); + } + + @Override + public void laterGlobal(Runnable task, int ticks) { + if (task == null) { + return; + } + if (ticks <= 0) { + global(task); + return; + } + delayedQueue.add(new DelayedTask(currentTick.get() + ticks, delayedSequence.getAndIncrement(), task)); + } + + @Override + public void laterRegion(PlatformWorld world, int chunkX, int chunkZ, Runnable task, int ticks) { + laterGlobal(task, ticks); + } + + public void reset() { + if (asyncExecutor.isShutdown()) { + asyncExecutor = createAsyncExecutor(); + } else { + List abandonedTasks = new ArrayList<>(); + asyncExecutor.getQueue().drainTo(abandonedTasks); + rejectAbandonedTasks(abandonedTasks); + } + mainQueue.clear(); + delayedQueue.clear(); + // reset() only runs from ModdedEngineBootstrap.start at SERVER_STARTING, which every loader + // fires on the server thread: capture it here instead of waiting for the first tick, so + // global() cannot mistake a boot-time server-thread call for an off-thread one and defer it. + mainThread = Thread.currentThread(); + } + + public void shutdown() { + List abandonedTasks = asyncExecutor.shutdownNow(); + rejectAbandonedTasks(abandonedTasks); + mainQueue.clear(); + delayedQueue.clear(); + mainThread = null; + // Shutdown stage that always runs (ModdedEngineBootstrap.stop): release the level snapshot with it. + ModdedServerLevels.forget(); + } + + private static void rejectAbandonedTasks(List abandonedTasks) { + for (Runnable abandonedTask : abandonedTasks) { + if (abandonedTask instanceof RejectionAwareTask rejectionAwareTask) { + rejectionAwareTask.reject(); + } + } + } + + private void warnOnBacklog(ThreadPoolExecutor executor) { + int queued = executor.getQueue().size(); + if (queued < ASYNC_BACKLOG_WARN) { + return; + } + long now = System.currentTimeMillis(); + long last = lastBacklogWarnAt.get(); + if (now - last < ASYNC_BACKLOG_WARN_INTERVAL_MILLIS || !lastBacklogWarnAt.compareAndSet(last, now)) { + return; + } + LOGGER.warn("Iris async backlog {} tasks (threads={}); async work is falling behind", queued, executor.getPoolSize()); + } + + private void drain() { + long tick = currentTick.incrementAndGet(); + promoteDelayed(tick); + long deadline = System.nanoTime() + DRAIN_BUDGET_NANOS; + int executed = 0; + Runnable task; + while ((task = mainQueue.poll()) != null) { + runGuarded(task); + executed++; + if (executed >= DRAIN_TASK_CAP || System.nanoTime() >= deadline) { + // Budget spent; the remainder stays queued in order and runs next tick. + break; + } + } + } + + /** + * Due-ordered promotion; only the tick thread polls, so the head is always the earliest due task. A task + * added while this runs is due at least one tick after the tick being promoted, so it sorts behind + * everything this pass drains and is picked up on a later tick instead of being skipped. No per-tick + * rebuild of the pending set. + */ + private void promoteDelayed(long tick) { + DelayedTask head; + while ((head = delayedQueue.peek()) != null && head.dueTick() <= tick) { + DelayedTask delayed = delayedQueue.poll(); + if (delayed == null) { + return; + } + mainQueue.add(delayed.task()); + } + } + + private boolean onMainThread() { + Thread main = mainThread; + return main != null && Thread.currentThread() == main; + } + + private void runGuarded(Runnable task) { + try { + task.run(); + } catch (Throwable error) { + LOGGER.error("Iris scheduled task failed", error); + } + } + + private record DelayedTask(long dueTick, long sequence, Runnable task) implements Comparable { + @Override + public int compareTo(DelayedTask other) { + int byTick = Long.compare(dueTick, other.dueTick); + return byTick != 0 ? byTick : Long.compare(sequence, other.sequence); + } + } + + private static final class AsyncThreadFactory implements ThreadFactory { + private final AtomicInteger counter = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "iris-modded-async-" + counter.getAndIncrement()); + thread.setDaemon(true); + return thread; + } + } + + private static final class RejectionAwareTask implements Runnable { + private final Runnable task; + private final Runnable rejection; + private final AtomicBoolean rejected; + + private RejectionAwareTask(Runnable task, Runnable rejection) { + this.task = task; + this.rejection = rejection; + rejected = new AtomicBoolean(); + } + + @Override + public void run() { + if (!rejected.get()) { + task.run(); + } + } + + private void reject() { + if (rejected.compareAndSet(false, true)) { + rejection.run(); + } + } + + private boolean isRejected() { + return rejected.get(); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSentry.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSentry.java new file mode 100644 index 000000000..fcb1b6aeb --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSentry.java @@ -0,0 +1,81 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.BuildConstants; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.json.JSONException; +import com.google.gson.JsonSyntaxException; +import io.sentry.Sentry; + +import java.util.concurrent.atomic.AtomicBoolean; + +public final class ModdedSentry { + private static final String DSN = "http://4cdbb9ac953306529947f4ca1e8e6b26@sentry.volmit.com:8080/2"; + private static final long FLUSH_TIMEOUT_MS = 2000L; + private static final AtomicBoolean STARTED = new AtomicBoolean(false); + + private ModdedSentry() { + } + + public static void start(ModdedLoader loader) { + IrisSettings.IrisSettingsSentry settings = IrisSettings.get().getSentry(); + if (settings.disableAutoReporting || Sentry.isEnabled() || Boolean.getBoolean("iris.suppressReporting")) { + return; + } + if (!STARTED.compareAndSet(false, true)) { + return; + } + IrisLogging.info("Enabling Sentry for anonymous error reporting. You can disable this in the settings."); + Sentry.init(options -> { + options.setDsn(DSN); + if (settings.debug) { + options.setDebug(true); + } + options.setAttachServerName(false); + options.setEnableUncaughtExceptionHandler(false); + options.setRelease(loader.modVersion()); + options.setEnvironment(BuildConstants.ENVIRONMENT); + options.setBeforeSend((event, hint) -> suppress(event.getThrowable()) ? null : event); + }); + Sentry.configureScope(scope -> { + scope.setTag("server", loader.platformName()); + scope.setTag("server.mc", loader.minecraftVersion()); + scope.setTag("iris.loader", loader.platformName()); + scope.setTag("iris.commit", BuildConstants.COMMIT); + }); + ModdedPlatform.captureSink(Sentry::captureException); + } + + public static void flush() { + if (Sentry.isEnabled()) { + Sentry.flush(FLUSH_TIMEOUT_MS); + Sentry.close(); + } + ModdedPlatform.captureSink(null); + STARTED.set(false); + } + + private static boolean suppress(Throwable error) { + return (error instanceof IllegalStateException state && "zip file closed".equals(state.getMessage())) + || error instanceof JSONException + || error instanceof JsonSyntaxException; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerAccess.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerAccess.java new file mode 100644 index 000000000..050a56369 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerAccess.java @@ -0,0 +1,41 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +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.storage.LevelStorageSource; + +import java.util.concurrent.Executor; + +public interface ModdedServerAccess { + Executor levelExecutor(MinecraftServer server); + + LevelStorageSource.LevelStorageAccess levelStorage(MinecraftServer server); + + ServerLevel putLevel(MinecraftServer server, ResourceKey key, ServerLevel level); + + ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey key, ServerLevel level); + + ServerLevel removeLevel(MinecraftServer server, ResourceKey key); + + boolean hasLevel(MinecraftServer server, ResourceKey key); +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerCommands.java new file mode 100644 index 000000000..800f4e396 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerCommands.java @@ -0,0 +1,41 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.server.MinecraftServer; + +public final class ModdedServerCommands { + private ModdedServerCommands() { + } + + public static boolean dispatch(MinecraftServer server, String command) { + if (server == null || command == null || command.isBlank()) { + return false; + } + String prepared = command.startsWith("/") ? command.substring(1) : command; + try { + server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), prepared); + return true; + } catch (Throwable error) { + IrisLogging.reportError(error); + return false; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerLevels.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerLevels.java new file mode 100644 index 000000000..576f2ce54 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerLevels.java @@ -0,0 +1,181 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +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.storage.LevelStorageSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ConcurrentModificationException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +public final class ModdedServerLevels implements ModdedServerAccess { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int CAPTURE_ATTEMPTS = 16; + private static volatile Snapshot snapshot; + + private final Consumer levelCacheInvalidator; + + public ModdedServerLevels(Consumer levelCacheInvalidator) { + this.levelCacheInvalidator = Objects.requireNonNull(levelCacheInvalidator); + } + + /** + * Immutable view of the loaded levels. {@code server.levels} is a plain map mutated on the server + * thread, so every off-server-thread reader must iterate this snapshot instead of + * {@code server.getAllLevels()} to avoid ConcurrentModificationException. + */ + public static List levels(MinecraftServer server) { + if (server == null) { + return List.of(); + } + Snapshot current = snapshot; + if (current != null && current.server() == server) { + return current.levels(); + } + Snapshot captured = capture(server); + return captured == null ? List.of() : captured.levels(); + } + + /** + * Keyed view of the loaded levels for off-server-thread lookups. Never used to gate injection: + * {@link #hasLevel} stays on the live map so a stale snapshot cannot mask a registered level. + */ + public static ServerLevel level(MinecraftServer server, ResourceKey key) { + if (server == null || key == null) { + return null; + } + Snapshot current = snapshot; + Snapshot resolved = current != null && current.server() == server ? current : capture(server); + return resolved == null ? null : resolved.byKey().get(key); + } + + /** + * Re-captures the snapshot when the live map no longer matches it. Server thread only; called once + * per tick so levels registered outside {@link ModdedServerAccess} (vanilla boot, other mods) are + * picked up without any reader touching the live map. + */ + public static void refreshIfStale(MinecraftServer server) { + if (server == null) { + return; + } + Snapshot current = snapshot; + if (current == null || current.server() != server) { + capture(server); + return; + } + List cached = current.levels(); + int index = 0; + for (ServerLevel level : server.getAllLevels()) { + if (index >= cached.size() || cached.get(index) != level) { + capture(server); + return; + } + index++; + } + if (index != cached.size()) { + capture(server); + } + } + + /** + * Drops the snapshot at shutdown so a stopped server (and its level graph) is not held alive until the + * next server publishes one. Integrated servers restart inside the same JVM. + */ + static void forget() { + snapshot = null; + } + + private static Snapshot capture(MinecraftServer server) { + for (int attempt = 0; attempt < CAPTURE_ATTEMPTS; attempt++) { + try { + LinkedHashMap, ServerLevel> byKey = new LinkedHashMap<>(); + for (ServerLevel level : server.getAllLevels()) { + byKey.put(level.dimension(), level); + } + Snapshot captured = new Snapshot(server, List.copyOf(byKey.values()), Map.copyOf(byKey)); + snapshot = captured; + return captured; + } catch (ConcurrentModificationException e) { + Thread.onSpinWait(); + } + } + LOGGER.error("Iris could not snapshot the level map after {} attempts; readers will see the previous snapshot", CAPTURE_ATTEMPTS); + Snapshot current = snapshot; + return current != null && current.server() == server ? current : null; + } + + @Override + public Executor levelExecutor(MinecraftServer server) { + return server.executor; + } + + @Override + public LevelStorageSource.LevelStorageAccess levelStorage(MinecraftServer server) { + return server.storageSource; + } + + @Override + public ServerLevel putLevel(MinecraftServer server, ResourceKey key, ServerLevel level) { + ServerLevel previous = server.levels.put(key, level); + if (previous != level) { + capture(server); + levelCacheInvalidator.accept(server); + } + return previous; + } + + @Override + public ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey key, ServerLevel level) { + ServerLevel previous = server.levels.putIfAbsent(key, level); + if (previous == null) { + capture(server); + levelCacheInvalidator.accept(server); + } + return previous; + } + + @Override + public ServerLevel removeLevel(MinecraftServer server, ResourceKey key) { + ServerLevel removed = server.levels.remove(key); + if (removed != null) { + capture(server); + levelCacheInvalidator.accept(server); + } + return removed; + } + + @Override + public boolean hasLevel(MinecraftServer server, ResourceKey key) { + return server.levels.containsKey(key); + } + + private record Snapshot(MinecraftServer server, List levels, + Map, ServerLevel> byKey) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServiceManager.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServiceManager.java new file mode 100644 index 000000000..835a612f3 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServiceManager.java @@ -0,0 +1,161 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.modded.service.ModdedService; +import art.arcane.iris.modded.service.ModdedTickableService; +import net.minecraft.server.MinecraftServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class ModdedServiceManager { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private final Map, ModdedService> services = new LinkedHashMap<>(); + private boolean enabled = false; + + public synchronized T register(Class type, T service) { + if (services.containsKey(type)) { + return type.cast(services.get(type)); + } + services.put(type, service); + if (enabled) { + try { + service.onEnable(); + } catch (Throwable failure) { + services.remove(type); + disableAfterFailure(service, failure); + throw serviceFailure(service, failure); + } + } + return service; + } + + public synchronized T service(Class type) { + ModdedService service = services.get(type); + return service == null ? null : type.cast(service); + } + + public synchronized void enableAll() { + if (enabled) { + return; + } + enabled = true; + ModdedService[] ordered = services.values().toArray(new ModdedService[0]); + for (int i = 0; i < ordered.length; i++) { + ModdedService service = ordered[i]; + try { + service.onEnable(); + } catch (Throwable failure) { + enabled = false; + for (int rollbackIndex = i; rollbackIndex >= 0; rollbackIndex--) { + disableAfterFailure(ordered[rollbackIndex], failure); + } + throw serviceFailure(service, failure); + } + } + } + + public synchronized void tick(MinecraftServer server) { + if (!enabled) { + return; + } + for (ModdedService service : services.values()) { + if (service instanceof ModdedTickableService tickable) { + tickService(tickable, server); + } + } + } + + /** + * Shutdown path: every service gets a disable attempt and nothing is rethrown. A throw here would abort the + * loader's remaining stop handlers, so failures are logged and the manager still ends up disabled. + */ + public synchronized void disableAll() { + if (!enabled) { + return; + } + Throwable failure = null; + int failed = 0; + ModdedService[] ordered = services.values().toArray(new ModdedService[0]); + for (int i = ordered.length - 1; i >= 0; i--) { + ModdedService service = ordered[i]; + try { + service.onDisable(); + } catch (Throwable serviceFailure) { + failed++; + LOGGER.error("Iris service onDisable failed for {}", service.getClass().getName(), serviceFailure); + if (failure == null) { + failure = serviceFailure; + } else if (serviceFailure != failure) { + failure.addSuppressed(serviceFailure); + } + } + } + enabled = false; + if (failure != null) { + LOGGER.error("Iris disabled all services with {} failure(s)", failed, failure); + } + } + + synchronized void rollback(Throwable failure) { + if (enabled) { + enabled = false; + ModdedService[] ordered = services.values().toArray(new ModdedService[0]); + for (int i = ordered.length - 1; i >= 0; i--) { + disableAfterFailure(ordered[i], failure); + } + } + services.clear(); + } + + private void tickService(ModdedTickableService service, MinecraftServer server) { + try { + service.onServerTick(server); + } catch (Throwable error) { + LOGGER.error("Iris service tick failed for {}", service.getClass().getName(), error); + } + } + + private void disableAfterFailure(ModdedService service, Throwable failure) { + try { + service.onDisable(); + } catch (Throwable cleanupError) { + if (cleanupError != failure) { + failure.addSuppressed(cleanupError); + } + LOGGER.error("Iris service rollback failed for {}", service.getClass().getName(), cleanupError); + } + } + + private RuntimeException serviceFailure(ModdedService service, Throwable failure) { + LOGGER.error("Iris service onEnable failed for {}", service.getClass().getName(), failure); + if (failure instanceof RuntimeException runtimeException) { + return runtimeException; + } + if (failure instanceof Error fatalError) { + throw fatalError; + } + return new IllegalStateException("Iris service failed to enable: " + service.getClass().getName(), failure); + } + +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSpawnTableMerger.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSpawnTableMerger.java new file mode 100644 index 000000000..f7fde8a5c --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSpawnTableMerger.java @@ -0,0 +1,124 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +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.util.project.context.IrisContext; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.resources.Identifier; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.MobSpawnSettings; + +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Vanilla-derivative spawn table state for {@link IrisModdedChunkGenerator}. Every mutator locks the + * generator monitor because the repoint/bind/reset paths already hold it while resetting this state. + */ +final class ModdedSpawnTableMerger { + private final IrisModdedChunkGenerator generator; + private final ConcurrentHashMap> vanillaSpawnBiomes = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> mergedSpawnTables = new ConcurrentHashMap<>(); + private volatile boolean vanillaSpawnBiomesInitialized; + + ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) { + this.generator = generator; + } + + void initializeVanillaSpawnBiomes(Registry registry) { + // Volatile fast path BEFORE the monitor: this runs per mob category per spawn attempt + // on the server thread, and the generator monitor is contended by binds/hotloads. + if (vanillaSpawnBiomesInitialized) { + return; + } + synchronized (generator) { + if (vanillaSpawnBiomesInitialized) { + return; + } + Engine current = generator.engineOrNull(); + if (current == null) { + return; + } + + try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT); + for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) { + if (irisBiome == null || !irisBiome.isCustom()) { + continue; + } + Holder vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey()); + if (vanillaHolder == null) { + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + Holder customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId()); + if (customHolder != null) { + vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder); + } + } + } + vanillaSpawnBiomesInitialized = true; + } + } + } + + Holder vanillaSpawnBiome(Biome biome) { + return vanillaSpawnBiomes.get(biome); + } + + WeightedList mergedSpawnTable( + Biome biome, MobCategory category, + WeightedList vanillaSpawns, + WeightedList explicitSpawns) { + SpawnTableKey key = new SpawnTableKey(biome, category); + return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns)); + } + + private Holder resolveBiomeHolder(Registry registry, String key) { + if (key == null || key.isBlank()) { + return null; + } + Identifier identifier = Identifier.tryParse(key); + if (identifier == null) { + return null; + } + Optional> reference = registry.get(identifier); + return reference.>map((Holder.Reference value) -> value).orElse(null); + } + + void resetVanillaSpawnBiomes() { + synchronized (generator) { + vanillaSpawnBiomes.clear(); + mergedSpawnTables.clear(); + vanillaSpawnBiomesInitialized = false; + } + } + + private record SpawnTableKey(Biome biome, MobCategory category) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java new file mode 100644 index 000000000..bc60b372f --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java @@ -0,0 +1,275 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.core.pack.PackDownloader; +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.modded.command.ModdedPackCommands; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +public final class ModdedStartup { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final AtomicBoolean PREPARED = new AtomicBoolean(false); + private static final AtomicBoolean STARTED = new AtomicBoolean(false); + + private ModdedStartup() { + } + + public static void reset() { + PREPARED.set(false); + STARTED.set(false); + } + + public static void prepareForStartup() { + if (!PREPARED.compareAndSet(false, true)) { + return; + } + reportLegacyPacksDirectory(); + validateAllPacks(); + } + + /** + * Older builds mkdir'd (and stale guidance sometimes populated) config/iris/packs, but modded + * packs live under config/irisworldgen/packs. Never auto-move user content: warn loudly when + * the legacy directory holds packs, and quietly remove it when it is empty. + */ + private static void reportLegacyPacksDirectory() { + try { + File legacy = ModdedEngineBootstrap.loader().configDir().resolve("iris").resolve("packs").toFile(); + if (!legacy.isDirectory()) { + return; + } + if (!PackDirectoryResolver.listVisiblePackDirectories(legacy).isEmpty()) { + File real = art.arcane.iris.spi.IrisPlatforms.get().packsFolderNoCreate(); + LOGGER.warn("Iris found packs under the legacy directory {} - modded packs load from {} only. Move them there.", + legacy.getAbsolutePath(), real.getAbsolutePath()); + return; + } + String[] entries = legacy.list(); + if (entries == null || entries.length == 0) { + legacy.delete(); + } + } catch (Throwable e) { + LOGGER.debug("Iris legacy packs directory check failed", e); + } + } + + /** + * Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler: + * ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this + * one-shot task, and the datapack must be regenerated before the level PackRepository reload if it can. + */ + public static void prefetchStartupDatapack() { + Thread thread = new Thread(ModdedStartup::refreshDatapack, "iris-modded-datapack-prefetch"); + thread.setDaemon(true); + thread.start(); + } + + private static void refreshDatapack() { + try { + ModdedForcedDatapack.regenerateIfStale("boot"); + } catch (Throwable failure) { + LOGGER.error("Iris could not refresh the forced datapack at boot", failure); + } + } + + public static void runOnce(MinecraftServer server) { + if (server == null || server.getPlayerList() == null) { + return; + } + prepareForStartup(); + if (!STARTED.compareAndSet(false, true)) { + return; + } + ModdedForcedDatapack.verifyInjected(); + ModdedMixinAudit.runOnce(); + reinjectPersistentDimensions(server); + + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + refreshDatapack(); + return; + } + scheduler.async(ModdedStartup::refreshDatapack); + } + + public static void validateAllPacks() { + File packsRoot = ModdedPackCommands.packsRoot(); + List packDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot); + PackValidationRegistry.clear(); + if (packDirs.isEmpty()) { + LOGGER.info("Iris found no packs to validate under {}; install one with /iris download pack=overworld, /iris download pack=underworld, or /iris download link=", + packsRoot.getAbsolutePath()); + return; + } + for (File packDir : packDirs) { + try { + PackValidationResult result = PackValidator.validate(packDir); + PackValidationRegistry.publish(result); + if (!result.isLoadable()) { + LOGGER.error("Iris pack '{}' FAILED validation with {} blocking error(s); world/studio creation will be refused. First error: {}", + result.getPackName(), result.getBlockingErrors().size(), + result.getBlockingErrors().getFirst()); + } else if (!result.getWarnings().isEmpty()) { + LOGGER.info("Iris pack '{}' validated ({} warning(s)).", result.getPackName(), result.getWarnings().size()); + for (String warning : result.getWarnings()) { + LOGGER.warn(" [{}] {}", result.getPackName(), warning); + } + } else { + LOGGER.info("Iris pack '{}' validated.", result.getPackName()); + } + } catch (Throwable e) { + LOGGER.error("Iris pack validation failed for '{}'", packDir.getName(), e); + String detail = e.getMessage(); + if (detail == null || detail.isBlank()) { + detail = e.getClass().getSimpleName(); + } + PackValidationRegistry.publish(new PackValidationResult( + packDir.getName(), + List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail), + List.of(), + System.currentTimeMillis())); + } + } + } + + public static PackValidationResult requirePackForWorldCreation(String pack) { + if (pack == null || pack.isBlank()) { + throw new IllegalArgumentException("Pack name is required for world creation"); + } + File packDir = PackDirectoryResolver.resolveExisting(ModdedPackCommands.packsRoot(), pack); + if (packDir == null) { + throw new BrokenPackException(pack, List.of( + "Pack folder does not exist under " + ModdedPackCommands.packsRoot().getAbsolutePath() + ".")); + } + PackValidationResult cached = PackValidationRegistry.get(pack); + if (cached != null && cached.getValidatedAtMillis() >= newestModificationMillis(packDir.toPath())) { + // prepareForStartup already validated this pack and nothing in it changed since; re-validating per + // persistent dimension at boot costs a full pack parse each time. + return PackValidationRegistry.requireLoadable(pack); + } + try { + PackValidationResult result = PackValidator.validate(packDir); + PackValidationRegistry.publish(result); + return PackValidationRegistry.requireLoadable(pack); + } catch (BrokenPackException e) { + throw e; + } catch (Throwable e) { + LOGGER.error("Iris required world-creation validation failed for '{}'", pack, e); + String detail = e.getMessage(); + if (detail == null || detail.isBlank()) { + detail = e.getClass().getSimpleName(); + } + PackValidationResult failure = new PackValidationResult( + pack, + List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail), + List.of(), + System.currentTimeMillis()); + PackValidationRegistry.publish(failure); + throw new BrokenPackException(pack, failure.getBlockingErrors()); + } + } + + private static void reinjectPersistentDimensions(MinecraftServer server) { + List dimensions = + ModdedDimensionRegistryStore.loadForStartup(server); + if (dimensions.isEmpty()) { + return; + } + int injected = 0; + int index = 0; + long startedAt = System.currentTimeMillis(); + for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) { + index++; + long dimensionStartedAt = System.currentTimeMillis(); + try { + ModdedDimensionManager.create(server, dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed()); + injected++; + LOGGER.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms", + index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(), + System.currentTimeMillis() - dimensionStartedAt); + } catch (Throwable e) { + LOGGER.error("Iris failed to re-inject persistent dimension '{}' (pack={} dim={} seed={})", dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed(), e); + if (e instanceof OutOfMemoryError outOfMemory) { + throw outOfMemory; + } + } + } + LOGGER.info("Iris re-injected {}/{} persistent dimension(s) at startup in {}ms", + injected, dimensions.size(), System.currentTimeMillis() - startedAt); + } + + private static long newestModificationMillis(Path root) { + long newest = 0L; + try (Stream walk = Files.walk(root)) { + for (Path path : (Iterable) walk::iterator) { + BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class); + long modified = attributes.lastModifiedTime().toMillis(); + if (modified > newest) { + newest = modified; + } + } + } catch (IOException | RuntimeException unreadable) { + LOGGER.debug("Iris could not stat {} for validation reuse; revalidating", root, unreadable); + return Long.MAX_VALUE; + } + return newest; + } + + /** + * SP-6: a pack excluded by validation is otherwise only visible in the console. Tell the operators who + * can actually act on it when they join. + */ + public static void warnPackFailuresTo(ServerPlayer player) { + if (player == null || !Commands.LEVEL_GAMEMASTERS.check(player.permissions())) { + return; + } + for (Map.Entry entry : PackValidationRegistry.snapshot().entrySet()) { + PackValidationResult result = entry.getValue(); + if (result == null || result.isLoadable()) { + continue; + } + String reason = result.getBlockingErrors().isEmpty() + ? "unknown validation failure" + : result.getBlockingErrors().getFirst(); + player.sendSystemMessage(Component.literal("Iris pack '" + entry.getKey() + + "' failed validation and cannot be used: " + reason)); + } + } + +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateMerger.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateMerger.java new file mode 100644 index 000000000..569c05362 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateMerger.java @@ -0,0 +1,75 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.BlockDataMergeSupport; +import art.arcane.iris.spi.PlatformBlockState; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; + +import java.util.Map; + +public final class ModdedStateMerger implements BlockDataMergeSupport.StateMerger { + @Override + public PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) { + ModdedBlockState fabricBase = (ModdedBlockState) base; + ModdedBlockState fabricUpdate = (ModdedBlockState) update; + ModdedBlockState metadataSource = fabricUpdate.isCustom() ? fabricUpdate : fabricBase; + + try { + return metadataSource.withHandle(mergeStates(fabricBase.handle(), fabricUpdate.handle(), fabricUpdate.parsedProperties()), null); + } catch (IllegalArgumentException e) { + ModdedBlockResolution.Parsed normalizedBase = ModdedBlockResolution.resolveGet(ModdedBlockState.serialize(fabricBase.handle())); + ModdedBlockResolution.Parsed normalizedUpdate = ModdedBlockResolution.resolveGet(ModdedBlockState.serialize(fabricUpdate.handle())); + + if (normalizedBase != null && normalizedUpdate != null) { + try { + return metadataSource.withHandle(mergeStates(normalizedBase.state(), normalizedUpdate.state(), normalizedUpdate.properties()), null); + } catch (IllegalArgumentException ignored) { + return metadataSource.withHandle(normalizedUpdate.state(), normalizedUpdate.properties()); + } + } + + if (normalizedUpdate != null) { + return metadataSource.withHandle(normalizedUpdate.state(), normalizedUpdate.properties()); + } + + return update; + } + } + + private static BlockState mergeStates(BlockState base, BlockState update, Map, Comparable> parsedProperties) { + if (parsedProperties == null) { + throw new IllegalArgumentException("Block data not created via string parsing"); + } + if (base.getBlock() != update.getBlock()) { + throw new IllegalArgumentException("States have different types (got " + update.getBlock() + ", expected " + base.getBlock() + ")"); + } + BlockState merged = base; + for (Map.Entry, Comparable> entry : parsedProperties.entrySet()) { + merged = apply(merged, entry.getKey(), entry.getValue()); + } + return merged; + } + + @SuppressWarnings("unchecked") + private static > BlockState apply(BlockState state, Property property, Comparable value) { + return state.setValue((Property) property, (T) value); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateRotator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateRotator.java new file mode 100644 index 000000000..c24db196a --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateRotator.java @@ -0,0 +1,365 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.IrisBlockVector; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.block.state.properties.RedstoneSide; +import net.minecraft.world.level.block.state.properties.RotationSegment; +import net.minecraft.world.level.block.state.properties.WallSide; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class ModdedStateRotator implements IrisObjectRotation.StateRotator { + private static final String[] FACE_NAMES = {"north", "east", "south", "west", "up", "down"}; + private static final String[] WALL_FACE_NAMES = {"north", "south", "east", "west"}; + private static final int[][] ROTATION_CYCLE_MODS = { + {0, 1}, {-1, 2}, {-1, 1}, {-2, 1}, {-1, 0}, {-2, -1}, {-1, -1}, {-1, -2}, + {0, -1}, {1, -2}, {1, -1}, {2, -1}, {1, 0}, {2, 1}, {1, 1}, {1, 2} + }; + + @Override + public PlatformBlockState rotate(IrisObjectRotation rotation, PlatformBlockState state, int spinxx, int spinyy, int spinzz) { + ModdedBlockState fabric = (ModdedBlockState) state; + BlockState d = fabric.handle(); + boolean deleted = false; + + try { + int spinx = (int) (90D * (Math.ceil(Math.abs((spinxx % 360D) / 90D)))); + int spiny = (int) (90D * (Math.ceil(Math.abs((spinyy % 360D) / 90D)))); + int spinz = (int) (90D * (Math.ceil(Math.abs((spinzz % 360D) / 90D)))); + + if (!rotation.canRotate()) { + return state; + } + + Property facing = findFacing(d); + Property rotationProperty = findRotation(d); + Property axis = findAxis(d); + Map multipleFacing = findMultipleFacing(d); + Map> wall = findWall(d); + Map> wire = findWire(d); + + if (facing != null) { + Direction f = (Direction) d.getValue(facing); + IrisBlockVector bv = new IrisBlockVector(f.getStepX(), f.getStepY(), f.getStepZ()); + bv = rotation.rotate(bv.clone(), spinx, spiny, spinz); + Direction t = faceFor(bv); + + if (facing.getPossibleValues().contains(t)) { + d = apply(d, facing, t); + } else if (!ModdedBlockResolution.isSolid(d.getBlock().defaultBlockState())) { + deleted = true; + } + } else if (rotationProperty != null) { + int segment = (Integer) d.getValue(rotationProperty); + int[] mods = ROTATION_CYCLE_MODS[segment]; + + IrisBlockVector bv = new IrisBlockVector(mods[0], 0, mods[1]); + bv = rotation.rotate(bv.clone(), spinx, spiny, spinz); + int[] hex = hexMods(bv); + + if (hex[0] == 0 && hex[1] == 0) { + throw new IllegalArgumentException("Invalid face, only horizontal face are allowed for this property!"); + } + double length = Math.sqrt((double) (hex[0] * hex[0] + hex[1] * hex[1])); + float angle = (float) -Math.toDegrees(Math.atan2(hex[0] / length, hex[1] / length)); + d = apply(d, rotationProperty, RotationSegment.convertToSegment(angle)); + } else if (axis != null) { + Direction.Axis current = (Direction.Axis) d.getValue(axis); + IrisBlockVector bv = axisFaceVector(current); + bv = rotation.rotate(bv.clone(), spinx, spiny, spinz); + Direction.Axis a = axisOf(bv); + + if (!a.equals(current) && axis.getPossibleValues().contains(a)) { + d = apply(d, axis, a); + } + } else if (multipleFacing.size() >= 2) { + List trueFaces = new ArrayList<>(); + for (Map.Entry entry : multipleFacing.entrySet()) { + if (d.getValue(entry.getValue())) { + trueFaces.add(entry.getKey()); + } + } + + List faces = new ArrayList<>(); + for (String name : trueFaces) { + IrisBlockVector bv = faceVector(name); + bv = rotation.rotate(bv.clone(), spinx, spiny, spinz); + String r = faceName(faceFor(bv)); + + if (multipleFacing.containsKey(r)) { + faces.add(r); + } + } + + for (String name : trueFaces) { + d = apply(d, multipleFacing.get(name), Boolean.FALSE); + } + + for (String name : faces) { + d = apply(d, multipleFacing.get(name), Boolean.TRUE); + } + } else if (wall.size() == 4) { + Map heights = new LinkedHashMap<>(); + + for (String name : WALL_FACE_NAMES) { + WallSide h = (WallSide) d.getValue(wall.get(name)); + IrisBlockVector bv = faceVector(name); + bv = rotation.rotate(bv.clone(), spinx, spiny, spinz); + String r = faceName(faceFor(bv)); + if (wall.containsKey(r)) { + heights.put(r, h); + } + } + + for (String name : WALL_FACE_NAMES) { + d = apply(d, wall.get(name), heights.getOrDefault(name, WallSide.NONE)); + } + } else if (wire.size() == 4) { + Map connections = new LinkedHashMap<>(); + + for (String name : WALL_FACE_NAMES) { + RedstoneSide connection = (RedstoneSide) d.getValue(wire.get(name)); + IrisBlockVector bv = faceVector(name); + bv = rotation.rotate(bv.clone(), spinx, spiny, spinz); + String r = faceName(faceFor(bv)); + if (wire.containsKey(r)) { + connections.put(r, connection); + } + } + + for (String name : WALL_FACE_NAMES) { + d = apply(d, wire.get(name), connections.getOrDefault(name, RedstoneSide.NONE)); + } + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + if (deleted) { + return null; + } + + return d == fabric.handle() ? state : fabric.withHandle(d, fabric.parsedProperties()); + } + + private static Property findFacing(BlockState state) { + for (Property property : state.getProperties()) { + if (property.getName().equals("facing") && property.getValueClass() == Direction.class) { + return property; + } + } + return null; + } + + private static Property findRotation(BlockState state) { + for (Property property : state.getProperties()) { + if (property.getName().equals("rotation") + && property instanceof IntegerProperty integer + && isFullRotationCycle(integer)) { + return property; + } + } + return null; + } + + private static boolean isFullRotationCycle(IntegerProperty property) { + List values = property.getPossibleValues(); + if (values.size() != ROTATION_CYCLE_MODS.length) { + return false; + } + for (int value : values) { + if (value < 0 || value >= ROTATION_CYCLE_MODS.length) { + return false; + } + } + return true; + } + + private static Property findAxis(BlockState state) { + for (Property property : state.getProperties()) { + if (property.getName().equals("axis") && property.getValueClass() == Direction.Axis.class) { + return property; + } + } + return null; + } + + private static Map findMultipleFacing(BlockState state) { + Map properties = new LinkedHashMap<>(); + for (String name : FACE_NAMES) { + for (Property property : state.getProperties()) { + if (property.getName().equals(name) && property instanceof BooleanProperty bool) { + properties.put(name, bool); + } + } + } + return properties; + } + + @SuppressWarnings("unchecked") + private static Map> findWall(BlockState state) { + Map> properties = new LinkedHashMap<>(); + for (String name : WALL_FACE_NAMES) { + for (Property property : state.getProperties()) { + if (property.getName().equals(name) && property.getValueClass() == WallSide.class) { + properties.put(name, (EnumProperty) property); + } + } + } + return properties; + } + + @SuppressWarnings("unchecked") + private static Map> findWire(BlockState state) { + Map> properties = new LinkedHashMap<>(); + for (String name : WALL_FACE_NAMES) { + for (Property property : state.getProperties()) { + if (property.getName().equals(name) && property.getValueClass() == RedstoneSide.class) { + properties.put(name, (EnumProperty) property); + } + } + } + return properties; + } + + private static Direction faceFor(IrisBlockVector v) { + int x = (int) Math.round(v.getX()); + int y = (int) Math.round(v.getY()); + int z = (int) Math.round(v.getZ()); + + if (x == 0 && z == -1) { + return Direction.NORTH; + } + + if (x == 0 && z == 1) { + return Direction.SOUTH; + } + + if (x == 1 && z == 0) { + return Direction.EAST; + } + + if (x == -1 && z == 0) { + return Direction.WEST; + } + + if (y > 0) { + return Direction.UP; + } + + if (y < 0) { + return Direction.DOWN; + } + + return Direction.SOUTH; + } + + private static int[] hexMods(IrisBlockVector v) { + int x = v.getBlockX(); + int y = v.getBlockY(); + int z = v.getBlockZ(); + + if (x == 0 && z == -1) return new int[]{0, -1}; + if (x == 1 && z == -2) return new int[]{1, -2}; + if (x == 1 && z == -1) return new int[]{1, -1}; + if (x == 2 && z == -1) return new int[]{2, -1}; + if (x == 1 && z == 0) return new int[]{1, 0}; + if (x == 2 && z == 1) return new int[]{2, 1}; + if (x == 1 && z == 1) return new int[]{1, 1}; + if (x == 1 && z == 2) return new int[]{1, 2}; + if (x == 0 && z == 1) return new int[]{0, 1}; + if (x == -1 && z == 2) return new int[]{-1, 2}; + if (x == -1 && z == 1) return new int[]{-1, 1}; + if (x == -2 && z == 1) return new int[]{-2, 1}; + if (x == -1 && z == 0) return new int[]{-1, 0}; + if (x == -2 && z == -1) return new int[]{-2, -1}; + if (x == -1 && z == -1) return new int[]{-1, -1}; + if (x == -1 && z == -2) return new int[]{-1, -2}; + + if (y > 0) { + return new int[]{0, 0}; + } + + if (y < 0) { + return new int[]{0, 0}; + } + + return new int[]{0, 1}; + } + + private static IrisBlockVector axisFaceVector(Direction.Axis axis) { + if (axis == Direction.Axis.X) { + return new IrisBlockVector(1, 0, 0); + } + if (axis == Direction.Axis.Y) { + return new IrisBlockVector(0, 1, 0); + } + return new IrisBlockVector(0, 0, 1); + } + + private static Direction.Axis axisOf(IrisBlockVector v) { + if (Math.abs(v.getBlockX()) > Math.max(Math.abs(v.getBlockY()), Math.abs(v.getBlockZ()))) { + return Direction.Axis.X; + } + + if (Math.abs(v.getBlockY()) > Math.max(Math.abs(v.getBlockX()), Math.abs(v.getBlockZ()))) { + return Direction.Axis.Y; + } + + if (Math.abs(v.getBlockZ()) > Math.max(Math.abs(v.getBlockX()), Math.abs(v.getBlockY()))) { + return Direction.Axis.Z; + } + + return Direction.Axis.Y; + } + + private static IrisBlockVector faceVector(String name) { + return switch (name) { + case "north" -> new IrisBlockVector(0, 0, -1); + case "south" -> new IrisBlockVector(0, 0, 1); + case "east" -> new IrisBlockVector(1, 0, 0); + case "west" -> new IrisBlockVector(-1, 0, 0); + case "up" -> new IrisBlockVector(0, 1, 0); + case "down" -> new IrisBlockVector(0, -1, 0); + default -> new IrisBlockVector(0, 0, 0); + }; + } + + private static String faceName(Direction direction) { + return direction.getSerializedName(); + } + + @SuppressWarnings("unchecked") + private static > BlockState apply(BlockState state, Property property, Object value) { + return state.setValue((Property) property, (T) value); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java new file mode 100644 index 000000000..a874ba674 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java @@ -0,0 +1,457 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.nativegen.NativeStructureGenerationException; +import art.arcane.iris.nativegen.NativeStructureGenerationKeys; +import art.arcane.iris.nativegen.NativeStructureFactory; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata; +import art.arcane.iris.spi.PlatformWorld; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +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.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature; +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.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +public final class ModdedStructureHooks implements PlatformStructureHooks { + /** + * Hard ceiling on the chunk grid a single capture placement may touch. placeChunks loads every chunk in + * the grid synchronously on the server thread, so a structure with a runaway bounding box (or maxSpan 0, + * which disables the span check entirely) would otherwise stall the server for thousands of chunk loads. + */ + static final int MAX_PLACEMENT_CHUNKS = 1024; + + private final Supplier server; + + public ModdedStructureHooks(Supplier server) { + this.server = Objects.requireNonNull(server); + } + + @Override + public List structureKeys() { + return registryKeys(Registries.STRUCTURE); + } + + @Override + public List jigsawStructureKeys() { + List keys = new ArrayList<>(); + MinecraftServer instance = requireServer("read registered jigsaw structures"); + Registry structures = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE); + for (Map.Entry, Structure> entry : structures.entrySet()) { + if (entry.getValue() instanceof JigsawStructure) { + keys.add(entry.getKey().identifier().toString()); + } + } + return keys; + } + + @Override + public List templatePoolKeys() { + return registryKeys(Registries.TEMPLATE_POOL); + } + + @Override + public JigsawSourceMetadata jigsawSourceMetadata(String structureKey) { + MinecraftServer instance = requireServer("resolve live jigsaw metadata for registered structure '" + + structureKey + "'"); + try { + Identifier identifier = Identifier.tryParse(structureKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered structure key: " + structureKey); + } + Registry registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Structure structure = registry.getValue(identifier); + if (structure == null) { + throw new IllegalArgumentException("Registered structure does not exist: " + structureKey); + } + if (!(structure instanceof JigsawStructure jigsaw)) { + throw new IllegalArgumentException("Registered structure is not a jigsaw: " + structureKey); + } + return NativeStructureFactory.sourceMetadata( + instance.registryAccess(), instance.getStructureManager(), jigsaw); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve live jigsaw metadata for registered structure '" + + structureKey + "' from the modded structure registry", error); + } + } + + @Override + public int templatePoolHorizontalSpan(String templatePoolKey) { + MinecraftServer instance = requireServer("resolve the live horizontal span for registered template pool '" + + templatePoolKey + "'"); + try { + return NativeStructureFactory.templatePoolHorizontalSpan( + instance.registryAccess(), instance.getStructureManager(), templatePoolKey); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve the live horizontal span for registered " + + "template pool '" + templatePoolKey + "' from the modded template-pool registry", error); + } + } + + @Override + public int jigsawStartPoolHorizontalSpan(String structureKey, String templatePoolKey) { + MinecraftServer instance = requireServer("resolve the effective start-pool span for registered jigsaw '" + + structureKey + "'"); + try { + Identifier identifier = Identifier.tryParse(structureKey); + if (identifier == null) { + throw new IllegalArgumentException("Invalid registered structure key: " + structureKey); + } + Structure structure = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE) + .getValue(identifier); + if (!(structure instanceof JigsawStructure jigsaw)) { + throw new IllegalArgumentException("Registered structure is not a jigsaw: " + structureKey); + } + return NativeStructureFactory.jigsawStartPoolHorizontalSpan( + instance.registryAccess(), instance.getStructureManager(), jigsaw, templatePoolKey); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve the effective start-pool span for registered " + + "jigsaw structure '" + structureKey + "' and pool '" + templatePoolKey + + "' from the modded registries", error); + } + } + + @Override + public List structureSetKeys() { + return registryKeys(Registries.STRUCTURE_SET); + } + + @Override + public List structureBiomeKeys(String structureKey) { + List keys = new ArrayList<>(); + MinecraftServer instance = requireServer("resolve biome keys for registered structure '" + structureKey + "'"); + try { + Identifier identifier = Identifier.tryParse(structureKey); + if (identifier == null) { + throw new IllegalArgumentException("Registered structure key is invalid: " + structureKey); + } + Registry registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Structure structure = registry.getValue(identifier); + if (structure == null) { + throw new IllegalArgumentException("Registered structure does not exist: " + structureKey); + } + boolean hasFilterEntries = false; + for (Holder holder : structure.biomes()) { + hasFilterEntries = true; + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + // An empty biome filter is legal datapack content (opt-in structures whose tags only + // reference absent modded biomes); the structure is unreachable, not an error state. + if (keys.isEmpty() && hasFilterEntries) { + throw new IllegalStateException("Registered structure '" + structureKey + + "' has biome filter entries but none resolve to registered biome keys"); + } + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve biome keys for registered structure '" + + structureKey + "' from the modded structure registry", error); + } + return keys; + } + + @Override + public List objectFeatureKeys() { + List keys = new ArrayList<>(); + try { + MinecraftServer instance = server.get(); + if (instance == null) { + return keys; + } + Registry> registry = instance.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE); + for (Identifier identifier : registry.keySet()) { + ConfiguredFeature configuredFeature = registry.getValue(identifier); + if (configuredFeature == null) { + continue; + } + String group = classifyFeature(configuredFeature.feature()); + if (group != null) { + keys.add(group + "|" + identifier); + } + } + } catch (Throwable error) { + IrisLogging.reportError(error); + } + return keys; + } + + @Override + public List reachableStructureKeys(PlatformWorld world) { + ServerLevel level = requireLevel(world, "resolve reachable structures"); + try { + BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource(); + Set possibleBiomes = possibleBiomeKeys(source); + return new ArrayList<>(NativeStructureGenerationKeys.reachable(level, possibleBiomes)); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve reachable structures for modded level '" + + level.dimension().identifier() + "'", error); + } + } + + @Override + public List possibleBiomeKeys(PlatformWorld world) { + List keys = new ArrayList<>(); + ServerLevel level = requireLevel(world, "resolve possible structure biome keys"); + try { + keys.addAll(possibleBiomeKeys(level.getChunkSource().getGenerator().getBiomeSource())); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to resolve possible structure biome keys for modded level '" + + level.dimension().identifier() + "'", error); + } + return keys; + } + + /** + * Swallow contract: a feature that refuses to place, or that throws while placing, is a normal outcome for + * the importer that drives this - it probes many keys against arbitrary terrain and treats false as "not + * here". So every Throwable is reported through IrisLogging and answered with false; placement never + * escalates to the caller. placeStructure below deliberately does the opposite (it rethrows with context) + * because a failed structure capture means the capture pass is broken, not that the site was unsuitable. + */ + @Override + public boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed) { + try { + ServerLevel level = level(world); + Identifier identifier = Identifier.tryParse(featureKey); + if (level == null || identifier == null) { + return false; + } + ChunkGenerator generator = level.getChunkSource().getGenerator(); + Registry> registry = level.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE); + ConfiguredFeature configuredFeature = registry.getValue(identifier); + if (configuredFeature == null) { + return false; + } + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed)); + return configuredFeature.place(level, generator, random, new BlockPos(x, y, z)); + } catch (Throwable error) { + IrisLogging.reportError(error); + return false; + } + } + + @Override + public int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) { + try { + ServerLevel level = level(world); + Identifier identifier = Identifier.tryParse(structureKey); + if (level == null || identifier == null) { + return null; + } + if (!level.getServer().isSameThread()) { + throw new IllegalStateException("Structure placement loads chunks synchronously and must run on the server thread, not " + + Thread.currentThread().getName()); + } + ChunkGenerator generator = level.getChunkSource().getGenerator(); + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Structure structure = registry.getValue(identifier); + if (structure == null) { + return null; + } + Holder holder = registry.wrapAsHolder(structure); + RandomState randomState = level.getChunkSource().randomState(); + StructureTemplateManager templateManager = level.getStructureManager(); + StructureManager structureManager = level.structureManager(); + BiomeSource biomeSource = generator.getBiomeSource(); + ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + StructureStart start = structure.generate( + holder, + level.dimension(), + level.registryAccess(), + generator, + biomeSource, + randomState, + templateManager, + seed, + chunkPos, + 0, + level, + biome -> true); + if (start == null || !start.isValid()) { + return null; + } + BoundingBox box = start.getBoundingBox(); + if (!isWithinSpan(box, maxSpan)) { + return null; + } + int placementChunks = chunkGridSize(box); + if (placementChunks > MAX_PLACEMENT_CHUNKS) { + IrisLogging.warn("Skipped structure capture for " + structureKey + " at " + chunkX + "," + chunkZ + + ": bounding box spans " + placementChunks + " chunks, cap is " + MAX_PLACEMENT_CHUNKS); + return null; + } + placeChunks(level, structureManager, generator, start, box, seed); + return bounds(box); + } catch (RuntimeException error) { + throw NativeStructureGenerationException.failure( + "capture placement", structureKey, chunkX, chunkZ, error); + } + } + + @Override + public boolean supportsStructurePlacement() { + return true; + } + + static String classifyFeature(Feature feature) { + if (feature instanceof TreeFeature) { + return "trees"; + } + if (feature instanceof FallenTreeFeature) { + return "fallen_trees"; + } + if (feature instanceof AbstractHugeMushroomFeature) { + return "mushrooms"; + } + return null; + } + + static boolean isWithinSpan(BoundingBox box, int maxSpan) { + return maxSpan <= 0 + || box.getXSpan() <= maxSpan && box.getYSpan() <= maxSpan && box.getZSpan() <= maxSpan; + } + + /** + * Number of chunks placeChunks would load for this bounding box. Computed in long arithmetic because a + * corrupt box can span the whole coordinate range and the product overflows int. + */ + static int chunkGridSize(BoundingBox box) { + long spanX = ((long) (box.maxX() >> 4)) - (box.minX() >> 4) + 1L; + long spanZ = ((long) (box.maxZ() >> 4)) - (box.minZ() >> 4) + 1L; + if (spanX <= 0L || spanZ <= 0L) { + return 0; + } + long total = spanX * spanZ; + return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) total; + } + + static int[] bounds(BoundingBox box) { + return new int[]{box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()}; + } + + private static Set possibleBiomeKeys(BiomeSource source) { + if (source == null) { + throw new IllegalStateException("Minecraft chunk generator has no biome source"); + } + Set keys = new LinkedHashSet<>(); + for (Holder holder : source.possibleBiomes()) { + Optional> key = holder.unwrapKey(); + if (key.isPresent()) { + keys.add(key.get().identifier().toString()); + } + } + if (keys.isEmpty()) { + throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes"); + } + return keys; + } + + private static ServerLevel level(PlatformWorld world) { + return world instanceof ModdedPlatformWorld moddedWorld ? moddedWorld.level() : null; + } + + private static void placeChunks(ServerLevel level, StructureManager structureManager, ChunkGenerator generator, + StructureStart start, BoundingBox box, long seed) { + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed)); + int minChunkX = box.minX() >> 4; + int maxChunkX = box.maxX() >> 4; + int minChunkZ = box.minZ() >> 4; + int maxChunkZ = box.maxZ() >> 4; + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + level.getChunk(chunkX, chunkZ); + ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + BoundingBox chunkBox = new BoundingBox( + chunkPos.getMinBlockX(), box.minY(), chunkPos.getMinBlockZ(), + chunkPos.getMaxBlockX(), box.maxY(), chunkPos.getMaxBlockZ()); + start.placeInChunk(level, structureManager, generator, random, chunkBox, chunkPos); + } + } + } + + private List registryKeys(ResourceKey> registryKey) { + List keys = new ArrayList<>(); + MinecraftServer instance = requireServer("read registry '" + registryKey.identifier() + "'"); + try { + Registry registry = instance.registryAccess().lookupOrThrow(registryKey); + for (Identifier identifier : registry.keySet()) { + keys.add(identifier.toString()); + } + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to read modded registry '" + + registryKey.identifier() + "'", error); + } + return keys; + } + + private MinecraftServer requireServer(String operation) { + MinecraftServer instance; + try { + instance = server.get(); + } catch (RuntimeException error) { + throw new IllegalStateException("Iris failed to access the Minecraft server to " + operation, error); + } + if (instance == null) { + throw new IllegalStateException("Iris cannot " + operation + " before the Minecraft server is available"); + } + return instance; + } + + private static ServerLevel requireLevel(PlatformWorld world, String operation) { + ServerLevel level = level(world); + if (level == null) { + throw new IllegalStateException("Iris cannot " + operation + + " without a bound modded ServerLevel"); + } + return level; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureSetFrequencyOverrides.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureSetFrequencyOverrides.java new file mode 100644 index 000000000..4e8dc72b0 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureSetFrequencyOverrides.java @@ -0,0 +1,285 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.NativeStructureFrequencyScale; +import art.arcane.iris.engine.object.IrisImportedStructureControl; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderOwner; +import net.minecraft.core.Vec3i; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +final class ModdedStructureSetFrequencyOverrides { + private ModdedStructureSetFrequencyOverrides() { + } + + static ChunkGeneratorStructureState apply( + ChunkGeneratorStructureState state, + IrisImportedStructureControl importedStructures + ) { + if (!importedStructures.hasFrequencyOverrides()) { + return state; + } + List> original = state.possibleStructureSets(); + List> scaled = scaleSets(original, importedStructures); + if (scaled == original) { + return state; + } + Field possibleSetsField = declaredField(ChunkGeneratorStructureState.class, List.class); + try { + possibleSetsField.set(state, scaled); + return state; + } catch (IllegalAccessException error) { + throw new IllegalStateException( + "Could not apply native structure-set frequency overrides", error); + } + } + + static List> scaleSets( + List> structureSets, + IrisImportedStructureControl importedStructures + ) { + Map> holdersByKey = new HashMap<>(); + for (Holder holder : structureSets) { + String key = structureSetKey(holder); + if (key != null) { + holdersByKey.putIfAbsent(key, holder); + } + } + Map, Holder> scaledByIdentity = new IdentityHashMap<>(); + List> scaledSets = new ArrayList<>(structureSets.size()); + boolean changed = false; + for (Holder holder : structureSets) { + Holder scaled = scaleHolder( + holder, importedStructures, holdersByKey, scaledByIdentity); + scaledSets.add(scaled); + changed |= scaled != holder; + } + return changed ? List.copyOf(scaledSets) : structureSets; + } + + private static Holder scaleHolder( + Holder holder, + IrisImportedStructureControl importedStructures, + Map> holdersByKey, + Map, Holder> scaledByIdentity + ) { + if (scaledByIdentity.containsKey(holder)) { + return scaledByIdentity.get(holder); + } + if (!dependsOnOverride(holder, importedStructures, holdersByKey)) { + scaledByIdentity.put(holder, holder); + return holder; + } + ResourceKey holderKey = holder.unwrapKey().orElseThrow(() -> + new IllegalStateException("An affected native structure-set exclusion graph has an unkeyed holder")); + ScaledStructureSetHolder scaledHolder = new ScaledStructureSetHolder(holderKey); + scaledByIdentity.put(holder, scaledHolder); + + StructureSet originalSet = holder.value(); + StructurePlacement originalPlacement = originalSet.placement(); + Optional originalZone = exclusionZone(originalPlacement); + Optional scaledZone = originalZone; + if (originalZone.isPresent()) { + Holder target = canonicalHolder( + originalZone.get().otherSet(), holdersByKey); + Holder scaledTarget = scaleHolder( + target, importedStructures, holdersByKey, scaledByIdentity); + if (scaledTarget != originalZone.get().otherSet()) { + scaledZone = Optional.of(new StructurePlacement.ExclusionZone( + scaledTarget, originalZone.get().chunkCount())); + } + } + double multiplier = importedStructures.frequencyMultiplier(structureSetKey(holder)); + StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone) + ? originalPlacement + : scalePlacement(originalPlacement, originalZone, scaledZone, multiplier); + scaledHolder.bind(new StructureSet(originalSet.structures(), scaledPlacement)); + return scaledHolder; + } + + private static boolean dependsOnOverride( + Holder holder, + IrisImportedStructureControl importedStructures, + Map> holdersByKey + ) { + Set> visited = Collections.newSetFromMap(new IdentityHashMap<>()); + Holder current = holder; + while (visited.add(current)) { + if (importedStructures.frequencyMultiplier(structureSetKey(current)) != 1D) { + return true; + } + Optional zone = + exclusionZone(current.value().placement()); + if (zone.isEmpty()) { + return false; + } + current = canonicalHolder(zone.get().otherSet(), holdersByKey); + } + return false; + } + + private static Holder canonicalHolder( + Holder holder, + Map> holdersByKey + ) { + String key = structureSetKey(holder); + return key == null ? holder : holdersByKey.getOrDefault(key, holder); + } + + private static StructurePlacement scalePlacement( + StructurePlacement placement, + Optional originalZone, + Optional scaledZone, + double multiplier + ) { + Vec3i locateOffset = (Vec3i) declaredFieldValue( + StructurePlacement.class, placement, Vec3i.class); + StructurePlacement.FrequencyReductionMethod reductionMethod = + (StructurePlacement.FrequencyReductionMethod) declaredFieldValue( + StructurePlacement.class, + placement, + StructurePlacement.FrequencyReductionMethod.class); + float frequency = (float) declaredFieldValue( + StructurePlacement.class, placement, float.class); + int salt = (int) declaredFieldValue( + StructurePlacement.class, placement, int.class); + + if (placement.getClass() == RandomSpreadStructurePlacement.class) { + RandomSpreadStructurePlacement randomSpread = + (RandomSpreadStructurePlacement) placement; + NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread( + frequency, randomSpread.spacing(), randomSpread.separation(), multiplier); + if (scale.frequency() == frequency + && scale.spacing() == randomSpread.spacing() + && scaledZone.equals(originalZone)) { + return placement; + } + return new RandomSpreadStructurePlacement( + locateOffset, + reductionMethod, + scale.frequency(), + salt, + scaledZone, + scale.spacing(), + randomSpread.separation(), + randomSpread.spreadType()); + } + if (placement.getClass() == ConcentricRingsStructurePlacement.class) { + ConcentricRingsStructurePlacement rings = + (ConcentricRingsStructurePlacement) placement; + float scaledFrequency = NativeStructureFrequencyScale.probability(frequency, multiplier); + if (scaledFrequency == frequency && scaledZone.equals(originalZone)) { + return placement; + } + return new ConcentricRingsStructurePlacement( + locateOffset, + reductionMethod, + scaledFrequency, + salt, + scaledZone, + rings.distance(), + rings.spread(), + rings.count(), + rings.preferredBiomes()); + } + throw new IllegalStateException("Unsupported native structure placement: " + + placement.getClass().getName()); + } + + private static Optional exclusionZone( + StructurePlacement placement + ) { + Object value = declaredFieldValue(StructurePlacement.class, placement, Optional.class); + if (value instanceof Optional optional && (optional.isEmpty() + || optional.get() instanceof StructurePlacement.ExclusionZone)) { + @SuppressWarnings("unchecked") + Optional resolved = + (Optional) optional; + return resolved; + } + throw new IllegalStateException("Could not read native structure exclusion zone from " + + placement.getClass().getName()); + } + + private static String structureSetKey(Holder holder) { + Optional> key = holder.unwrapKey(); + return key.map(resourceKey -> resourceKey.identifier().toString()).orElse(null); + } + + private static Object declaredFieldValue( + Class declaringType, + Object target, + Class fieldType + ) { + Field field = declaredField(declaringType, fieldType); + try { + return field.get(target); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Could not read " + fieldType.getName() + + " field on " + declaringType.getName(), error); + } + } + + private static Field declaredField(Class declaringType, Class fieldType) { + Field match = null; + for (Field field : declaringType.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != fieldType) { + continue; + } + if (match != null) { + throw new IllegalStateException("Ambiguous " + fieldType.getName() + " field on " + + declaringType.getName()); + } + match = field; + } + if (match == null || !match.trySetAccessible()) { + throw new IllegalStateException("Missing accessible " + fieldType.getName() + + " field on " + declaringType.getName()); + } + return match; + } + + private static final class ScaledStructureSetHolder extends Holder.Reference { + private ScaledStructureSetHolder(ResourceKey key) { + super(Type.STAND_ALONE, new HolderOwner<>() { + }, key, null); + } + + private void bind(StructureSet structureSet) { + bindValue(structureSet); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileData.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileData.java new file mode 100644 index 000000000..59612886f --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileData.java @@ -0,0 +1,418 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.data.UnresolvedKeyLog; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.Strictness; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CollectionTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BannerBlockEntity; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.minecraft.world.level.block.entity.SpawnerBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.storage.TagValueInput; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class ModdedTileData extends TileData { + public static final String NBT_PROPERTY = "nbt"; + static final String LEGACY_BANNER_COLOR_PROPERTY = "iris:legacy_banner_color"; + private static final int MAX_TAG_DEPTH = 64; + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create(); + private static final UnresolvedKeyLog SNBT_FALLBACK = new UnresolvedKeyLog("Iris tile capture SNBT fallback", 30_000L); + + private final byte[] raw; + private final KMap tileProperties; + private final String expectedBlockKey; + private final int legacyType; + private int hash; + + ModdedTileData(byte[] raw, KMap tileProperties, String expectedBlockKey, int legacyType) { + super(); + this.raw = raw; + this.tileProperties = tileProperties == null ? new KMap<>() : tileProperties; + this.expectedBlockKey = expectedBlockKey; + this.legacyType = legacyType; + } + + public static ModdedTileData capture(String blockKey, String snbt) throws IOException { + KMap properties = captureProperties(blockKey, snbt); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(bytes)) { + out.writeUTF(blockKey); + out.writeUTF(GSON.toJson(properties)); + } + return new ModdedTileData(bytes.toByteArray(), properties, normalizeBlockKey(blockKey), -1); + } + + /** + * Converts a captured tile to the generic map form the Bukkit side reads and writes, so an object captured on a + * mod loader still pastes on Bukkit, and keeps the original SNBT under {@value #NBT_PROPERTY} alongside it. + *

+ * Both forms are stored because the map form is lossy: {@link #fromTag(Tag, int, int)} collapses ByteArray, + * IntArray and LongArray tags to a plain List, and pasting that back produces a ListTag, which Minecraft rejects + * where it expects an array - a player head's {@code profile.id} (IntArray of 4) is the common case. Modded paste + * reads the SNBT first ({@link #payload()}), so a modded capture pastes byte-identical on a mod loader; Bukkit + * still reads the map form and accepts the array-shaped members degrading to lists, as it did before. + *

+ * The SNBT is dropped when the captured tag itself has a root member named {@code nbt}, since that member owns the + * map key; such a tile keeps the pre-existing lossy behaviour on both platforms. No vanilla block entity has one. + */ + @SuppressWarnings("unchecked") + private static KMap captureProperties(String blockKey, String snbt) { + KMap properties = new KMap<>(); + if (snbt != null && !snbt.isBlank()) { + try { + Object converted = fromTag(NbtUtils.snbtToStructure(snbt), 0, MAX_TAG_DEPTH); + if (converted instanceof KMap map) { + properties.putAll((KMap) map); + } + } catch (Throwable e) { + if (SNBT_FALLBACK.firstOccurrence(blockKey == null ? "" : blockKey)) { + IrisLogging.warn("Tile capture for '" + blockKey + "' kept SNBT form: " + e.getMessage()); + } + String summary = SNBT_FALLBACK.pollSummary(); + if (summary != null) { + IrisLogging.warn(summary); + } + } + } + if (snbt != null && !properties.containsKey(NBT_PROPERTY)) { + properties.put(NBT_PROPERTY, snbt); + } + return properties; + } + + public static ModdedTileData fromProperties(PlatformBlockState state, KMap properties) { + String blockKey = state.placementBaseState().key(); + int bracket = blockKey.indexOf('['); + if (bracket >= 0) { + blockKey = blockKey.substring(0, bracket); + } + KMap copied = properties == null ? new KMap<>() : properties.copy(); + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(bytes)) { + out.writeUTF(blockKey); + out.writeUTF(GSON.toJson(copied)); + } + return new ModdedTileData(bytes.toByteArray(), copied, normalizeBlockKey(blockKey), -1); + } catch (IOException e) { + throw new IllegalStateException("Failed to encode modded tile data for " + blockKey, e); + } + } + + public String snbt() { + Object value = tileProperties.get(NBT_PROPERTY); + return value == null ? null : value.toString(); + } + + public boolean apply(BlockEntity blockEntity, ServerLevel level) throws Exception { + CompoundTag payload = payload(); + if (payload == null) { + return false; + } + CompoundTag merged = blockEntity.saveWithoutMetadata(level.registryAccess()); + merged.merge(payload); + blockEntity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, level.registryAccess(), merged)); + blockEntity.setChanged(); + level.sendBlockUpdated(blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity.getBlockState(), Block.UPDATE_CLIENTS); + return true; + } + + /** + * The platform-neutral block key. The superclass reads its own {@code material} field, which a modded record + * never populates (it carries the key as {@link #expectedBlockKey} instead), so it must be answered here. + * Null for a legacy record, which identifies its target by block-entity type rather than by key - see + * {@link #isApplicable(BlockState, BlockEntity)}. + */ + @Override + public String getMaterialKey() { + return expectedBlockKey; + } + + public boolean isApplicable(BlockState state, BlockEntity blockEntity) { + if (expectedBlockKey != null) { + return expectedBlockKey.equals(normalizeBlockKey(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString())); + } + return switch (legacyType) { + case 0 -> blockEntity instanceof SignBlockEntity; + case 1 -> blockEntity instanceof SpawnerBlockEntity; + case 2 -> blockEntity instanceof BannerBlockEntity; + case 3 -> blockEntity instanceof RandomizableContainerBlockEntity; + default -> false; + }; + } + + CompoundTag payload() throws Exception { + String snbt = snbt(); + if (snbt != null && !snbt.isBlank()) { + return NbtUtils.snbtToStructure(snbt); + } + Tag converted = toTag(tileProperties); + return converted instanceof CompoundTag compound ? compound : null; + } + + public BlockState adjustBlockState(BlockState state) { + Object colorValue = tileProperties.get(LEGACY_BANNER_COLOR_PROPERTY); + if (colorValue == null) { + return state; + } + Identifier currentId = BuiltInRegistries.BLOCK.getKey(state.getBlock()); + String currentPath = currentId.getPath(); + if (!currentPath.endsWith("_banner")) { + return state; + } + String suffix = currentPath.endsWith("_wall_banner") ? "_wall_banner" : "_banner"; + Identifier targetId = Identifier.tryParse("minecraft:" + colorValue + suffix); + if (targetId == null || !BuiltInRegistries.BLOCK.containsKey(targetId)) { + return state; + } + BlockState adjusted = BuiltInRegistries.BLOCK.getValue(targetId).defaultBlockState(); + for (Property property : state.getProperties()) { + if (adjusted.hasProperty(property)) { + adjusted = copyProperty(adjusted, state, property); + } + } + return adjusted; + } + + private static Tag toTag(Object value) { + if (value == null) { + return null; + } + if (value instanceof Map map) { + CompoundTag compound = new CompoundTag(); + for (Map.Entry entry : map.entrySet()) { + if (LEGACY_BANNER_COLOR_PROPERTY.equals(String.valueOf(entry.getKey()))) { + continue; + } + Tag child = toTag(entry.getValue()); + if (child != null) { + compound.put(String.valueOf(entry.getKey()), child); + } + } + return compound; + } + if (value instanceof List values) { + ListTag list = new ListTag(); + for (Object entry : values) { + Tag child = toTag(entry); + if (child != null) { + list.add(child); + } + } + return list; + } + if (value instanceof Boolean bool) { + return ByteTag.valueOf(bool); + } + if (value instanceof Byte number) { + return ByteTag.valueOf(number); + } + if (value instanceof Short number) { + return ShortTag.valueOf(number); + } + if (value instanceof Integer number) { + return IntTag.valueOf(number); + } + if (value instanceof Long number) { + return LongTag.valueOf(number); + } + if (value instanceof Float number) { + return FloatTag.valueOf(number); + } + if (value instanceof Number number) { + return DoubleTag.valueOf(number.doubleValue()); + } + return StringTag.valueOf(String.valueOf(value)); + } + + /** + * Inverse of {@link #toTag(Object)}, matching the Bukkit NMS tile converter value-for-value so both platforms + * produce the same generic map for the same block entity. + */ + private static Object fromTag(Tag tag, int depth, int maxDepth) { + if (tag == null || depth > maxDepth) { + return null; + } + if (tag instanceof CompoundTag compound) { + KMap map = new KMap<>(); + for (String key : compound.keySet()) { + Tag child = compound.get(key); + if (child == null) { + continue; + } + Object value = fromTag(child, depth + 1, maxDepth); + if (value != null) { + map.put(key, value); + } + } + return map; + } + if (tag instanceof CollectionTag collection) { + List values = new ArrayList<>(); + for (Object entry : collection) { + if (entry instanceof Tag child) { + Object value = fromTag(child, depth + 1, maxDepth); + if (value != null) { + values.add(value); + } + } else if (entry != null) { + values.add(entry); + } + } + return values; + } + if (tag instanceof NumericTag numeric) { + return numeric.box(); + } + return tag.asString().orElse(null); + } + + private static > BlockState copyProperty(BlockState target, BlockState source, Property property) { + return target.setValue(property, source.getValue(property)); + } + + private static Object deepCopy(Object value) { + if (value instanceof Map map) { + KMap copy = new KMap<>(); + for (Map.Entry entry : map.entrySet()) { + copy.put(String.valueOf(entry.getKey()), deepCopy(entry.getValue())); + } + return copy; + } + if (value instanceof List values) { + List copy = new ArrayList<>(values.size()); + for (Object entry : values) { + copy.add(deepCopy(entry)); + } + return copy; + } + return value; + } + + static String normalizeBlockKey(String blockKey) { + if (blockKey == null || blockKey.isBlank()) { + return null; + } + String normalized = blockKey.trim().toLowerCase(java.util.Locale.ROOT); + int bracket = normalized.indexOf('['); + if (bracket >= 0) { + normalized = normalized.substring(0, bracket); + } + if (!normalized.contains(":")) { + normalized = "minecraft:" + normalized.replaceAll("\\s+", "_"); + } + Identifier identifier = Identifier.tryParse(normalized); + return identifier == null ? null : identifier.toString(); + } + + @Override + public KMap getProperties() { + return tileProperties; + } + + @Override + public void toBinary(DataOutputStream out) throws IOException { + out.write(raw); + } + + /** + * Identity over this record's own state. The superclass generates equals/hashCode from its + * {@code material} and {@code properties} fields, both of which stay null on a modded record, which would make + * every modded tile equal with a constant hash. Mantle tile sections are palette backed + * (16x16x16 = 4096 entries, so PaletteOrHunk picks a value-keyed DataContainer) and resolve palette ids through + * equals, so a collapsed identity writes the first tile's NBT into every other tile in the section. + *

+ * The serialized {@link #raw} form is the complete identity: it carries the block key and the property JSON for a + * modern record and the consumed bytes for a legacy one. Two logically identical tiles still share one palette + * entry, which is the intended dedup. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ModdedTileData other)) { + return false; + } + return legacyType == other.legacyType + && Objects.equals(expectedBlockKey, other.expectedBlockKey) + && Arrays.equals(raw, other.raw); + } + + @Override + public int hashCode() { + int cached = hash; + if (cached != 0) { + return cached; + } + int computed = 31 * (31 * Arrays.hashCode(raw) + Objects.hashCode(expectedBlockKey)) + legacyType; + if (computed == 0) { + computed = 1; + } + hash = computed; + return computed; + } + + @Override + public String toString() { + return (expectedBlockKey == null ? "legacy:" + legacyType : expectedBlockKey) + GSON.toJson(tileProperties); + } + + @SuppressWarnings("unchecked") + @Override + public TileData clone() { + return new ModdedTileData(raw == null ? null : raw.clone(), + (KMap) deepCopy(tileProperties), expectedBlockKey, legacyType); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileReader.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileReader.java new file mode 100644 index 000000000..b0a3e4915 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileReader.java @@ -0,0 +1,361 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.TileData; +import art.arcane.volmlib.util.collection.KMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.Strictness; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.block.entity.BannerPattern; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.function.Supplier; + +public final class ModdedTileReader implements TileData.TileReader { + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE).create(); + private static final int DYE_COLOR_COUNT = 16; + private static final Identifier DEFAULT_SPAWNER_ENTITY = Identifier.parse("minecraft:pig"); + private static final Identifier DEFAULT_BANNER_PATTERN = Identifier.parse("minecraft:base"); + private static final List LEGACY_BUKKIT_ENTITY_TYPES = createLegacyBukkitEntityTypes(); + private static final List PAPER_26_2_BANNER_PATTERNS = List.of( + Identifier.parse("minecraft:small_stripes"), + Identifier.parse("minecraft:stripe_right"), + Identifier.parse("minecraft:diagonal_left"), + Identifier.parse("minecraft:stripe_middle"), + Identifier.parse("minecraft:square_bottom_right"), + Identifier.parse("minecraft:half_horizontal"), + Identifier.parse("minecraft:skull"), + Identifier.parse("minecraft:flow"), + Identifier.parse("minecraft:rhombus"), + Identifier.parse("minecraft:border"), + Identifier.parse("minecraft:gradient_up"), + Identifier.parse("minecraft:guster"), + Identifier.parse("minecraft:square_top_left"), + Identifier.parse("minecraft:triangle_bottom"), + Identifier.parse("minecraft:triangles_bottom"), + Identifier.parse("minecraft:half_horizontal_bottom"), + Identifier.parse("minecraft:gradient"), + Identifier.parse("minecraft:triangle_top"), + Identifier.parse("minecraft:piglin"), + Identifier.parse("minecraft:stripe_center"), + Identifier.parse("minecraft:circle"), + Identifier.parse("minecraft:stripe_left"), + Identifier.parse("minecraft:stripe_bottom"), + Identifier.parse("minecraft:square_top_right"), + Identifier.parse("minecraft:curly_border"), + Identifier.parse("minecraft:creeper"), + Identifier.parse("minecraft:square_bottom_left"), + Identifier.parse("minecraft:triangles_top"), + Identifier.parse("minecraft:half_vertical"), + Identifier.parse("minecraft:mojang"), + Identifier.parse("minecraft:diagonal_right"), + Identifier.parse("minecraft:cross"), + Identifier.parse("minecraft:straight_cross"), + Identifier.parse("minecraft:bricks"), + Identifier.parse("minecraft:diagonal_up_left"), + Identifier.parse("minecraft:base"), + Identifier.parse("minecraft:flower"), + Identifier.parse("minecraft:stripe_downleft"), + Identifier.parse("minecraft:diagonal_up_right"), + Identifier.parse("minecraft:stripe_downright"), + Identifier.parse("minecraft:stripe_top"), + Identifier.parse("minecraft:globe"), + Identifier.parse("minecraft:half_vertical_right")); + + private final Supplier server; + + public ModdedTileReader(Supplier server) { + this.server = server; + } + + private static final class ReplayInputStream extends InputStream { + private final InputStream source; + private byte[] buffer = new byte[256]; + private int size = 0; + private int position = 0; + private int marked = 0; + + private ReplayInputStream(InputStream source) { + this.source = source; + } + + @Override + public int read() throws IOException { + if (position < size) { + int value = buffer[position] & 0xFF; + position++; + return value; + } + int value = source.read(); + if (value < 0) { + return value; + } + if (size == buffer.length) { + buffer = Arrays.copyOf(buffer, buffer.length * 2); + } + buffer[size] = (byte) value; + size++; + position++; + return value; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public synchronized void mark(int readLimit) { + marked = position; + } + + @Override + public synchronized void reset() { + position = marked; + } + + private void rewind() { + position = 0; + } + + private int position() { + return position; + } + + private byte[] consumed() { + return Arrays.copyOf(buffer, position); + } + } + + @Override + public TileData read(DataInputStream in) throws IOException { + if (!in.markSupported()) { + throw new IOException("Mark not supported"); + } + in.mark(Integer.MAX_VALUE); + ReplayInputStream replay = new ReplayInputStream(in); + DataInputStream din = new DataInputStream(replay); + try { + return parse(din, replay); + } finally { + in.reset(); + in.skipNBytes(replay.position()); + in.mark(0); + } + } + + private TileData parse(DataInputStream din, ReplayInputStream replay) throws IOException { + try { + String materialKey = din.readUTF(); + boolean materialMatched = matchMaterial(materialKey); + String json = din.readUTF(); + KMap properties = kmapFromJson(json); + if (!materialMatched) { + throw new NullPointerException("material is marked non-null but is null"); + } + if (properties == null) { + throw new NullPointerException("properties is marked non-null but is null"); + } + return new ModdedTileData(replay.consumed(), properties, + ModdedTileData.normalizeBlockKey(materialKey), -1); + } catch (Throwable e) { + replay.rewind(); + return parseLegacy(din, replay); + } + } + + @SuppressWarnings("unchecked") + private static KMap kmapFromJson(String json) { + return GSON.fromJson(json, KMap.class); + } + + private TileData parseLegacy(DataInputStream din, ReplayInputStream replay) throws IOException { + int id = din.readShort(); + String expectedBlockKey = null; + KMap properties = switch (id) { + case 0 -> readSign(din); + case 1 -> readSpawner(din, replay); + case 2 -> readBanner(din, replay); + case 3 -> { + expectedBlockKey = ModdedTileData.normalizeBlockKey(din.readUTF()); + yield readLootable(din); + } + default -> throw new IOException("Unknown tile type: " + id); + }; + return new ModdedTileData(replay.consumed(), properties, expectedBlockKey, id); + } + + private static KMap readSign(DataInputStream din) throws IOException { + List messages = List.of(din.readUTF(), din.readUTF(), din.readUTF(), din.readUTF()); + byte dye = din.readByte(); + if (dye < 0 || dye >= DYE_COLOR_COUNT) { + throw new ArrayIndexOutOfBoundsException("Index " + dye + " out of bounds for length " + DYE_COLOR_COUNT); + } + KMap text = new KMap<>(); + text.put("messages", messages); + text.put("color", DyeColor.byId(dye).getName()); + text.put("has_glowing_text", false); + KMap properties = new KMap<>(); + properties.put("front_text", text); + properties.put("back_text", text.copy()); + return properties; + } + + private static KMap readSpawner(DataInputStream din, ReplayInputStream replay) throws IOException { + Identifier entityId = null; + replay.mark(Integer.MAX_VALUE); + + try { + String keyString = din.readUTF(); + Identifier key = Identifier.tryParse(keyString); + if (key != null && BuiltInRegistries.ENTITY_TYPE.containsKey(key)) { + entityId = key; + } else { + replay.reset(); + } + } catch (Throwable ignored) { + replay.reset(); + } + + if (entityId == null) { + entityId = legacySpawnerEntityId(din.readShort()); + } + KMap entity = new KMap<>(); + entity.put("id", entityId.toString()); + KMap spawnData = new KMap<>(); + spawnData.put("entity", entity); + KMap properties = new KMap<>(); + properties.put("SpawnData", spawnData); + return properties; + } + + static Identifier legacySpawnerEntityId(int legacyOrdinal) { + if (legacyOrdinal < 0 || legacyOrdinal >= LEGACY_BUKKIT_ENTITY_TYPES.size()) { + return DEFAULT_SPAWNER_ENTITY; + } + return LEGACY_BUKKIT_ENTITY_TYPES.get(legacyOrdinal); + } + + private static List createLegacyBukkitEntityTypes() { + List entityTypes = new ArrayList<>(); + for (Identifier key : BuiltInRegistries.ENTITY_TYPE.keySet()) { + if (Identifier.DEFAULT_NAMESPACE.equals(key.getNamespace())) { + entityTypes.add(key); + } + } + entityTypes.sort(Identifier::compareTo); + return List.copyOf(entityTypes); + } + + private KMap readBanner(DataInputStream din, ReplayInputStream replay) throws IOException { + int baseColor = din.readUnsignedByte(); + int listSize = din.readUnsignedByte(); + replay.mark(Integer.MAX_VALUE); + + List layers = new ArrayList<>(listSize); + try { + for (int i = 0; i < listSize; i++) { + int color = din.readUnsignedByte(); + Identifier patternKey = Identifier.tryParse(din.readUTF()); + if (patternKey == null || !bannerPatternExists(patternKey)) { + throw new IOException("Unknown banner pattern key"); + } + layers.add(bannerLayer(patternKey, color)); + } + } catch (Throwable ignored) { + replay.reset(); + layers.clear(); + } + + if (layers.isEmpty() && listSize > 0) { + for (int i = 0; i < listSize; i++) { + int color = din.readUnsignedByte(); + int pattern = din.readUnsignedByte(); + layers.add(bannerLayer(legacyBannerPatternKey(pattern), color)); + } + } + KMap properties = new KMap<>(); + properties.put("patterns", layers); + properties.put(ModdedTileData.LEGACY_BANNER_COLOR_PROPERTY, DyeColor.byId(baseColor).getName()); + return properties; + } + + private static KMap bannerLayer(Identifier pattern, int color) { + KMap layer = new KMap<>(); + layer.put("pattern", pattern.toString()); + layer.put("color", DyeColor.byId(color).getName()); + return layer; + } + + static Identifier legacyBannerPatternKey(int legacyOrdinal) { + if (legacyOrdinal < 0 || legacyOrdinal >= PAPER_26_2_BANNER_PATTERNS.size()) { + return DEFAULT_BANNER_PATTERN; + } + return PAPER_26_2_BANNER_PATTERNS.get(legacyOrdinal); + } + + private boolean bannerPatternExists(Identifier key) { + MinecraftServer instance = server.get(); + if (instance == null) { + return true; + } + Registry registry = instance.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN); + return registry.containsKey(key); + } + + private static KMap readLootable(DataInputStream din) throws IOException { + String lootTable = din.readUTF(); + long seed = din.readLong(); + KMap properties = new KMap<>(); + if (!lootTable.isBlank()) { + properties.put("LootTable", lootTable); + properties.put("LootTableSeed", seed); + } + return properties; + } + + private static boolean matchMaterial(String name) { + String filtered = name.trim().toLowerCase(Locale.ROOT); + int bracket = filtered.indexOf('['); + if (bracket >= 0) { + filtered = filtered.substring(0, bracket); + } + if (!filtered.contains(":")) { + filtered = "minecraft:" + filtered.replaceAll("\\s+", "_"); + } + Identifier identifier = Identifier.tryParse(filtered); + if (identifier == null) { + return false; + } + return BuiltInRegistries.BLOCK.containsKey(identifier); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorkspaceGenerator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorkspaceGenerator.java new file mode 100644 index 000000000..9c96885b7 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorkspaceGenerator.java @@ -0,0 +1,134 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.project.SchemaBuilder; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.io.IOException; + +public final class ModdedWorkspaceGenerator { + private ModdedWorkspaceGenerator() { + } + + public static File writeWorkspace(IrisData data, File folder) throws IOException { + return writeWorkspace(data, folder, false); + } + + public static File writeWorkspace(IrisData data, File folder, boolean immediateSchemas) throws IOException { + JSONObject workspaceConfig = buildWorkspace(data, immediateSchemas); + File workspace = new File(folder, folder.getName() + ".code-workspace"); + IO.writeAll(workspace, workspaceConfig.toString(4)); + return workspace; + } + + public static JSONObject buildWorkspace(IrisData data) { + return buildWorkspace(data, false); + } + + private static JSONObject buildWorkspace(IrisData data, boolean immediateSchemas) { + JSONObject ws = new JSONObject(); + JSONArray folders = new JSONArray(); + JSONObject folder = new JSONObject(); + folder.put("path", "."); + folders.put(folder); + ws.put("folders", folders); + JSONObject settings = new JSONObject(); + settings.put("workbench.colorTheme", "Monokai"); + settings.put("workbench.preferredDarkColorTheme", "Solarized Dark"); + settings.put("workbench.tips.enabled", false); + settings.put("workbench.tree.indent", 24); + settings.put("files.autoSave", "onFocusChange"); + JSONObject json = new JSONObject(); + json.put("editor.autoIndent", "brackets"); + json.put("editor.acceptSuggestionOnEnter", "smart"); + json.put("editor.cursorSmoothCaretAnimation", true); + json.put("editor.dragAndDrop", false); + json.put("files.trimTrailingWhitespace", true); + json.put("diffEditor.ignoreTrimWhitespace", true); + json.put("files.trimFinalNewlines", true); + json.put("editor.suggest.showKeywords", false); + json.put("editor.suggest.showSnippets", false); + json.put("editor.suggest.showWords", false); + JSONObject quick = new JSONObject(); + quick.put("strings", true); + json.put("editor.quickSuggestions", quick); + json.put("editor.suggest.insertMode", "replace"); + settings.put("[json]", json); + settings.put("json.maxItemsComputed", 30000); + settings.put("json.schemas", buildSchemas(data, immediateSchemas)); + ws.put("settings", settings); + return ws; + } + + private static JSONArray buildSchemas(IrisData data, boolean immediateSchemas) { + JSONArray schemas = new JSONArray(); + + for (ResourceLoader loader : data.getLoaders().v()) { + if (loader.supportsSchemas()) { + schemas.put(immediateSchemas ? loader.buildSchemaImmediately() : loader.buildSchema()); + } + } + + for (Class snippetClass : data.resolveSnippets()) { + try { + String snipType = snippetClass.getDeclaredAnnotation(Snippet.class).value(); + JSONObject entry = new JSONObject(); + KList fileMatch = new KList<>(); + + for (int depth = 1; depth < 8; depth++) { + fileMatch.add("/snippet/" + snipType + Form.repeat("/*", depth) + ".json"); + } + + entry.put("fileMatch", new JSONArray(fileMatch.toArray())); + entry.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json"); + schemas.put(entry); + File schemaFile = new File(data.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json"); + if (immediateSchemas) { + IO.writeAll(schemaFile, new SchemaBuilder(snippetClass, data).construct().toString(4)); + } else { + J.attemptAsync(() -> { + try { + IO.writeAll(schemaFile, new SchemaBuilder(snippetClass, data).construct().toString(4)); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + }); + } + } catch (Throwable e) { + if (immediateSchemas) { + throw new IllegalStateException("Could not write snippet schema for " + snippetClass.getName(), e); + } + IrisLogging.reportError(e); + } + } + + return schemas; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java new file mode 100644 index 000000000..896402805 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java @@ -0,0 +1,319 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.modded.WorldCheckStructureAudit.NativeStructureGate; +import art.arcane.iris.modded.WorldCheckStructureAudit.PendingVillagePoi; +import art.arcane.iris.modded.WorldCheckStructureAudit.PoiAudit; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +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.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.Heightmap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +public final class ModdedWorldCheck { + private static final int EXIT_PASS = 0; + private static final int EXIT_FAILURE = 1; + private static final long SERVER_WAIT_TIMEOUT_MILLIS = 600000L; + private static final long SERVER_WAIT_INTERVAL_MILLIS = 250L; + private static final long SERVER_TASK_TIMEOUT_MILLIS = 900000L; + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + // halt, not exit: awaitStopAndExit already waited for MinecraftServer.halt(true), and exit() would run the + // shutdown hooks and block behind the server thread it just stopped, so a finished check could hang forever. + private static final ProcessExit PROCESS_EXIT = Runtime.getRuntime()::halt; + private static volatile MinecraftServer startedServer; + + private ModdedWorldCheck() { + } + + public static void schedule() { + coordinatorThread(() -> waitAndRun(PROCESS_EXIT)).start(); + } + + public static void serverStarted(MinecraftServer server) { + startedServer = server; + } + + public static void serverStopped(MinecraftServer server) { + if (startedServer == server) { + startedServer = null; + } + } + + static Thread coordinatorThread(Runnable coordinator) { + Thread thread = new Thread(coordinator, "Iris World Check"); + // Daemon: every wait below is bounded and the coordinator exits the process itself, so this thread + // must never be the reason a crashed dev server keeps the JVM alive. + thread.setDaemon(true); + return thread; + } + + private static void waitAndRun(ProcessExit processExit) { + long start = System.currentTimeMillis(); + MinecraftServer server = null; + int exitCode = EXIT_FAILURE; + AtomicBoolean stopRequested = new AtomicBoolean(false); + try { + while (System.currentTimeMillis() - start < SERVER_WAIT_TIMEOUT_MILLIS) { + MinecraftServer candidate = startedServer; + if (candidate != null) { + server = candidate; + break; + } + Thread.sleep(SERVER_WAIT_INTERVAL_MILLIS); + } + + if (server == null) { + LOGGER.error("[worldcheck] server did not finish starting within 10 minutes"); + return; + } + + MinecraftServer serverRef = server; + WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef)) + .get(SERVER_TASK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + exitCode = serverRef.submit(() -> runAndRequestStop( + () -> completeWorldCheck(preparation), + () -> { + stopRequested.set(true); + serverRef.halt(false); + } + )).get(SERVER_TASK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + LOGGER.error("[worldcheck] coordinator interrupted", e); + Thread.currentThread().interrupt(); + } catch (TimeoutException e) { + LOGGER.error("[worldcheck] server task did not finish within {}ms", SERVER_TASK_TIMEOUT_MILLIS); + } catch (Throwable e) { + LOGGER.error("[worldcheck] check failed", e); + } finally { + int resultCode = exitCode; + LOGGER.info("[worldcheck] shutting down dev server (result={})", resultCode == EXIT_PASS ? "PASS" : "FAIL"); + MinecraftServer serverRef = server; + if (serverRef != null && stopRequested.get()) { + awaitStopAndExit(() -> serverRef.halt(true), resultCode, processExit); + } else { + processExit.exit(EXIT_FAILURE); + } + } + } + + static int runAndRequestStop(BooleanSupplier check, Runnable requestStop) { + int exitCode = EXIT_FAILURE; + try { + exitCode = check.getAsBoolean() ? EXIT_PASS : EXIT_FAILURE; + } catch (Throwable e) { + LOGGER.error("[worldcheck] check failed", e); + } + try { + requestStop.run(); + } catch (Throwable e) { + LOGGER.error("[worldcheck] server stop request failed", e); + return EXIT_FAILURE; + } + return exitCode; + } + + static void awaitStopAndExit(Runnable awaitStop, int requestedExitCode, ProcessExit processExit) { + int exitCode = requestedExitCode; + boolean interrupted = Thread.interrupted(); + if (interrupted) { + exitCode = EXIT_FAILURE; + } + try { + awaitStop.run(); + } catch (Throwable e) { + exitCode = EXIT_FAILURE; + LOGGER.error("[worldcheck] waiting for server shutdown failed", e); + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + processExit.exit(exitCode); + } + + private static WorldCheckPreparation run(MinecraftServer server) { + ServerLevel level = targetLevel(server); + if (level == null) { + LOGGER.error("[worldcheck] no Iris dimension is loaded"); + return new WorldCheckPreparation(false, false, false, false, + new NativeStructureGate(false, 0, false, null)); + } + + String levelId = level.dimension().identifier().toString(); + String generatorClass = level.getChunkSource().getGenerator().getClass().getName(); + LOGGER.info("[worldcheck] {} generator: {}", levelId, generatorClass); + IrisModdedChunkGenerator generator = level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator iris + ? iris : null; + boolean irisGenerator = generator != null; + if (!irisGenerator) { + LOGGER.error("[worldcheck] {} is NOT using IrisModdedChunkGenerator", levelId); + } + boolean dimensionTypeOk = generator != null + && WorldCheckDimensionContract.checkDimensionType(level, generator); + + BlockPos spawn = level.getRespawnData().pos(); + LOGGER.info("[worldcheck] spawn: {} {} {} (minY={} height={})", spawn.getX(), spawn.getY(), spawn.getZ(), level.getMinY(), level.getHeight()); + + MessageDigest digest = WorldCheckPredicates.sha256(); + List samples = new ArrayList<>(); + Set surfaceKeys = new LinkedHashSet<>(); + for (int dx = 0; dx < 4; dx++) { + for (int dz = 0; dz < 4; dz++) { + int x = spawn.getX() + (dx - 2) * 16 + 8; + int z = spawn.getZ() + (dz - 2) * 16 + 8; + level.getChunk(x >> 4, z >> 4); + int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z); + BlockState surface = level.getBlockState(new BlockPos(x, y - 1, z)); + String key = BuiltInRegistries.BLOCK.getKey(surface.getBlock()).toString(); + String line = x + " " + (y - 1) + " " + z + " " + key; + samples.add(line); + surfaceKeys.add(key); + digest.update((line + "\n").getBytes(StandardCharsets.UTF_8)); + } + } + + for (int i = 0; i < Math.min(6, samples.size()); i++) { + LOGGER.info("[worldcheck] surface sample: {}", samples.get(i)); + } + LOGGER.info("[worldcheck] surface digest: {} ({} columns, {} distinct surface blocks: {})", + HexFormat.of().formatHex(digest.digest()).substring(0, 12), samples.size(), surfaceKeys.size(), surfaceKeys); + + ChunkAccess zeroChunk = level.getChunk(0, 0); + int nonEmptySections = 0; + for (LevelChunkSection section : zeroChunk.getSections()) { + if (!section.hasOnlyAir()) { + nonEmptySections++; + } + } + Set columnKeys = new LinkedHashSet<>(); + int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, 8, 8); + for (int y = level.getMinY(); y < surfaceY; y += 16) { + BlockState state = zeroChunk.getBlockState(new BlockPos(8, y, 8)); + if (!state.isAir()) { + columnKeys.add(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString()); + } + } + LOGGER.info("[worldcheck] chunk 0,0: {} non-empty sections of {}; column blocks at (8,*,8): {}", + nonEmptySections, zeroChunk.getSections().length, columnKeys); + + boolean sectionsOk = nonEmptySections >= 4; + boolean varietyOk = columnKeys.size() >= 2 || surfaceKeys.size() >= 2; + if (!sectionsOk) { + LOGGER.error("[worldcheck] chunk 0,0 looks empty/vanilla-flat ({} non-empty sections)", nonEmptySections); + } + if (!varietyOk) { + LOGGER.error("[worldcheck] generated terrain has no block variety (flat-world signature)"); + } + + boolean entityMixinsOk = WorldCheckDimensionContract.checkEntityMixins(level); + NativeStructureGate structureGate = generator == null + ? new NativeStructureGate(false, 0, false, null) + : WorldCheckStructureAudit.checkNativeStructures(level, generator, spawn); + boolean terrainOk = sectionsOk && varietyOk; + boolean nonStructurePass = irisGenerator && dimensionTypeOk && terrainOk && entityMixinsOk; + return new WorldCheckPreparation(nonStructurePass, terrainOk, dimensionTypeOk, + entityMixinsOk, structureGate); + } + + private static boolean completeWorldCheck(WorldCheckPreparation preparation) { + NativeStructureGate structureGate = preparation.structureGate(); + boolean poiOk = false; + PendingVillagePoi pendingPoi = structureGate.pendingPoi(); + if (pendingPoi != null) { + PoiAudit poi = WorldCheckStructureAudit.auditStructurePois(pendingPoi.level(), pendingPoi.start()); + poiOk = WorldCheckPredicates.villagePoiPass(poi.inBounds(), poi.outOfBounds()); + WorldCheckPredicates.qaEvent("village_poi_metric", "village", poiOk, + "inBounds=" + poi.inBounds() + ",outOfBounds=" + poi.outOfBounds()); + if (!poiOk) { + LOGGER.error("[worldcheck] village POI audit failed: inBounds={} outOfBounds={}", + poi.inBounds(), poi.outOfBounds()); + } + } else { + WorldCheckPredicates.qaEvent("village_poi_metric", "village", false, "skipped=structure"); + } + int passed = structureGate.nonVillagePassed() + + (structureGate.villagePassBeforePoi() && poiOk ? 1 : 0); + boolean structurePass = structureGate.passBeforePoi() && poiOk; + LOGGER.info("[worldcheck] native structure gate: {}/{} passed", passed, + WorldCheckStructureAudit.STRUCTURE_CHECKS.size()); + WorldCheckPredicates.qaEvent("structure_aggregate", "all", structurePass, + "passed=" + passed + ",total=" + WorldCheckStructureAudit.STRUCTURE_CHECKS.size()); + boolean pass = preparation.nonStructurePass() && structurePass; + LOGGER.info("[worldcheck] {}", pass ? "PASS" : "FAIL"); + WorldCheckPredicates.qaEvent("worldcheck_final", "all", pass, + "structures=" + WorldCheckStructureAudit.STRUCTURE_CHECKS.size() + + ",terrain=" + preparation.terrainOk() + + ",dimensionType=" + preparation.dimensionTypeOk() + + ",entityMixins=" + preparation.entityMixinsOk()); + return pass; + } + + private static ServerLevel targetLevel(MinecraftServer server) { + String target = System.getProperty("iris.worldcheck.dimension"); + if (target != null && !target.isBlank()) { + Identifier identifier = Identifier.tryParse(target.trim()); + ServerLevel requested = identifier == null + ? null + : ModdedServerLevels.level(server, ResourceKey.create(Registries.DIMENSION, identifier)); + if (requested != null) { + return requested; + } + LOGGER.error("[worldcheck] requested dimension '{}' is not loaded", target); + return null; + } + + for (ServerLevel level : ModdedServerLevels.levels(server)) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) { + return level; + } + } + return null; + } + + @FunctionalInterface + interface ProcessExit { + void exit(int status); + } + + private record WorldCheckPreparation(boolean nonStructurePass, boolean terrainOk, + boolean dimensionTypeOk, boolean entityMixinsOk, + NativeStructureGate structureGate) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java new file mode 100644 index 000000000..56e7935db --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java @@ -0,0 +1,232 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.engine.IrisEngine; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionRuntimeContract; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.modded.command.ModdedGuiHost; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.LevelResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedWorldEngines { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final ConcurrentHashMap ENGINES = new ConcurrentHashMap<>(); + + private ModdedWorldEngines() { + } + + public static Engine get(ServerLevel level, String pack, String dimensionKey, long seedOverride) { + Engine existing = ENGINES.get(level); + if (existing != null) { + return existing; + } + return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, pack, dimensionKey, seedOverride)); + } + + public static Collection activeEngines() { + return new ArrayList<>(ENGINES.values()); + } + + public static void evict(ServerLevel level) { + try { + evictOrThrow(level); + } catch (Throwable e) { + LOGGER.error("Iris engine evict close failed for {}", level.dimension().identifier(), e); + } + } + + static void evictOrThrow(ServerLevel level) { + Engine[] removed = new Engine[1]; + ENGINES.computeIfPresent(level, (ServerLevel ignored, Engine current) -> { + close(current); + removed[0] = current; + return null; + }); + if (removed[0] == null) { + return; + } + // The GUI host holds strong Engine/ServerLevel references with no other remove path. + ModdedGuiHost.unbind(removed[0]); + LOGGER.info("Iris engine evicted for {}", level.dimension().identifier()); + } + + static Engine prepareReplacement(ServerLevel level, String pack, String dimensionKey, long seedOverride) { + return create(level, pack, dimensionKey, seedOverride); + } + + static void installReplacement(ServerLevel level, Engine replacement) { + ServerLevel activeLevel = Objects.requireNonNull(level); + Engine activeReplacement = Objects.requireNonNull(replacement); + ENGINES.compute(activeLevel, (ServerLevel ignored, Engine current) -> { + if (current != null && current != activeReplacement) { + close(current); + ModdedGuiHost.unbind(current); + } + return activeReplacement; + }); + } + + static void closeUnregistered(Engine engine) { + close(engine); + ModdedGuiHost.unbind(engine); + } + + private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) { + ModdedEngineBootstrap.bind(); + File packDir = resolvePack(pack, dimensionKey); + PackValidationRegistry.requireLoadable(pack); + IrisData data = IrisData.openRuntime(packDir); + IrisDimension dimension = data.getDimensionLoader().load(dimensionKey); + if (dimension == null) { + LOGGER.error("Iris pack '{}' at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.", + pack, packDir.getAbsolutePath(), dimensionKey, dimensionKey); + throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + packDir.getAbsolutePath()); + } + + long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride; + validateDimensionContract(pack, dimensionKey, dimension, level); + File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile(); + IrisWorld world = IrisWorld.builder() + .platformIdentity(level.dimension().identifier().toString()) + .name(level.dimension().identifier().toString().replace(':', '_')) + .seed(seed) + .worldFolder(worldFolder) + .minHeight(dimension.getMinHeight()) + .maxHeight(dimension.getMaxHeight()) + .platformWorld(new ModdedPlatformWorld(level)) + .build(); + Engine engine = new IrisEngine( + new EngineTarget(world, dimension, data), + IrisEngine.InitializationMode.RUNTIME); + if (engine.isClosed() || engine.getComplex() == null) { + IllegalStateException failure = new IllegalStateException("Iris engine for " + + level.dimension().identifier() + " did not initialize a ready biome complex"); + try { + close(engine); + } catch (Throwable cleanupError) { + failure.addSuppressed(cleanupError); + } + throw failure; + } + + LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}", + level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight()); + return engine; + } + + private static void validateDimensionContract(String pack, String dimensionKey, + IrisDimension dimension, ServerLevel level) { + DimensionType actualType = level.dimensionType(); + String actualTypeKey = level.dimensionTypeRegistration().unwrapKey() + .map(key -> key.identifier().toString()) + .orElse(""); + String legacyTypeKey = "irisworldgen:" + dimension.getDimensionTypeKey(); + String expectedTypeKey = legacyTypeKey.equals(actualTypeKey) + ? legacyTypeKey + : ModdedWorldgenIds.dimensionTypeRef(pack, dimensionKey); + IrisDimensionRuntimeContract expected = new IrisDimensionRuntimeContract( + expectedTypeKey, + dimension.getMinHeight(), + dimension.getMaxHeight() - dimension.getMinHeight(), + dimension.getLogicalHeight()); + IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract( + actualTypeKey, + actualType.minY(), + actualType.height(), + actualType.logicalHeight()); + String runtimeName = "Modded level '" + level.dimension().identifier() + "'"; + expected.requireExact(runtimeName, actual); + expected.requireHeight(runtimeName, level.getMinY(), level.getHeight()); + } + + public static File packFolder(String pack) { + return IrisPlatforms.get().packsFolderNoCreate(pack); + } + + static File resolvePack(String pack, String dimensionKey) { + File packDir = packFolder(pack); + if (packDir.isDirectory()) { + return packDir; + } + + LOGGER.error("==============================================================="); + LOGGER.error("Iris pack '{}' is not installed.", pack); + LOGGER.error("Expected a pack folder at: {}", packDir.getAbsolutePath()); + LOGGER.error("Install an Iris pack there (the folder must contain dimensions/{}.json) and restart the server.", dimensionKey); + LOGGER.error("==============================================================="); + throw new IllegalStateException("Iris pack not installed: " + packDir.getAbsolutePath()); + } + + private static void close(Engine engine) { + if (engine != null && !engine.isClosed()) { + engine.close(); + } + } + + public static void shutdown() { + Throwable failure = null; + for (Map.Entry entry : new ArrayList<>(ENGINES.entrySet())) { + ServerLevel level = entry.getKey(); + Engine engine = entry.getValue(); + try { + // Latch the generator's unloading flag BEFORE closing (unbindEngine sets it, + // then evicts): chunk-system drain work running after this stage would + // otherwise see a closed engine and silently rebuild a fresh engine + Mantle + // that no teardown stage ever closes, writing plates after the final save. + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) { + generator.unbindEngine(level); + } else { + close(engine); + if (!ENGINES.remove(level, engine) && ENGINES.containsKey(level)) { + throw new IllegalStateException("Iris engine mapping changed during shutdown for " + + level.dimension().identifier()); + } + } + LOGGER.info("Iris engine closed for {}", level.dimension().identifier()); + } catch (Throwable e) { + LOGGER.error("Iris engine close failed for {}", level.dimension().identifier(), e); + if (failure == null) { + failure = e; + } else if (e != failure) { + failure.addSuppressed(e); + } + } + } + if (failure != null) { + throw new IllegalStateException("One or more Iris engines failed to close; failed mappings were retained", failure); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java new file mode 100644 index 000000000..36cad303c --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java @@ -0,0 +1,867 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineLifecycleTasks; +import art.arcane.iris.engine.framework.EngineWorldManager; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.IRare; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisEntity; +import art.arcane.iris.engine.object.IrisEntitySpawn; +import art.arcane.iris.engine.object.IrisMarker; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisSpawnGroup; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.engine.object.IrisSurface; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterMarker; +import art.arcane.volmlib.util.matter.slices.MarkerMatter; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.phys.AABB; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public final class ModdedWorldManager implements EngineWorldManager { + static final MantleFlag INITIAL_SPAWN_COMPLETION_FLAG = MantleFlag.INITIAL_SPAWNED_MARKER; + private static final int MAX_INITIAL_QUEUE = 8192; + private static final int MAX_INITIAL_DRAIN_PER_TICK = 8; + private static final int MAX_INITIAL_RECOVERY_PER_PASS = 128; + private static final int MANTLE_WARMUP_QUEUE_CAPACITY = 256; + private static final int AMBIENT_CHUNK_SAMPLE = 64; + private static final long INITIAL_RECOVERY_INTERVAL_MS = 1_000L; + + private final Engine engine; + private final InitialSpawnQueue initialSpawnQueue; + private final Set mantleWarmups; + private final ThreadPoolExecutor mantleWarmupExecutor; + private final long[] ambientChunkSample = new long[AMBIENT_CHUNK_SAMPLE]; + private int ambientChunkSampleSeen; + private long lastAmbientAt; + private long lastInitialRecoveryAt; + private boolean initialSpawnQueueClosed; + private boolean mantleWarmupExecutorStopped; + private boolean mantleWarmupsCleared; + private boolean spawnStateMissingLogged; + private volatile boolean closed; + private volatile boolean entityCountAvailable; + private volatile int cachedEntityCount; + private volatile int cachedConsideredChunks; + private volatile double cachedSaturation; + + public ModdedWorldManager(Engine engine) { + this.engine = engine; + this.initialSpawnQueue = new InitialSpawnQueue(MAX_INITIAL_QUEUE); + this.mantleWarmups = ConcurrentHashMap.newKeySet(); + BlockingQueue warmupQueue = new ArrayBlockingQueue<>(MANTLE_WARMUP_QUEUE_CAPACITY); + this.mantleWarmupExecutor = new ThreadPoolExecutor( + 1, + 1, + 30L, + TimeUnit.SECONDS, + warmupQueue, + runnable -> { + Thread thread = new Thread(runnable, "Iris Initial Spawn Mantle Warmup"); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }, + new ThreadPoolExecutor.AbortPolicy()); + this.mantleWarmupExecutor.allowCoreThreadTimeOut(true); + } + + public static void enqueueGenerated(Engine engine, int chunkX, int chunkZ) { + if (engine == null || engine.isClosed()) { + return; + } + EngineWorldManager worldManager = engine.getWorldManager(); + if (!(worldManager instanceof ModdedWorldManager moddedWorldManager)) { + return; + } + if (moddedWorldManager.closed || moddedWorldManager.isPregenActive()) { + // runServerTick skips the drain while a pregen targets this world, so every offer from the + // generation threads can only expire or overflow while contending for the queue monitor. + // recoverLoadedInitialSpawns re-offers the chunks that matter once the job ends. + return; + } + moddedWorldManager.initialSpawnQueue.offer(pack(chunkX, chunkZ)); + } + + public void serverTick(ServerLevel level) { + EngineLifecycleTasks.run(engine, "modded_world_manager_tick", () -> runServerTick(level)); + } + + private void runServerTick(ServerLevel level) { + if (closed || engine.isClosed() || engine.getMantle().getMantle().isClosed()) { + return; + } + if (!isEntitySpawningEnabledForCurrentWorld()) { + return; + } + if (isPregenActive()) { + return; + } + recoverLoadedInitialSpawns(level); + drainInitialSpawns(level); + ambientTick(level); + } + + private void recoverLoadedInitialSpawns(ServerLevel level) { + if (!markerSystemEnabled()) { + return; + } + long now = System.currentTimeMillis(); + if (now - lastInitialRecoveryAt < INITIAL_RECOVERY_INTERVAL_MS) { + return; + } + lastInitialRecoveryAt = now; + + Set candidates = new HashSet<>(); + for (ServerPlayer player : level.players()) { + int centerX = player.blockPosition().getX() >> 4; + int centerZ = player.blockPosition().getZ() >> 4; + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + candidates.add(pack(centerX + dx, centerZ + dz)); + } + } + } + candidates.addAll(level.getForceLoadedChunks()); + + Mantle mantle = engine.getMantle().getMantle(); + int recovered = 0; + for (long key : candidates) { + int chunkX = unpackX(key); + int chunkZ = unpackZ(key); + if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) { + continue; + } + if (mantle.isChunkLoaded(chunkX, chunkZ) && mantle.hasFlag(chunkX, chunkZ, INITIAL_SPAWN_COMPLETION_FLAG)) { + continue; + } + if (initialSpawnQueue.offer(key) && ++recovered >= MAX_INITIAL_RECOVERY_PER_PASS) { + return; + } + } + } + + private void drainInitialSpawns(ServerLevel level) { + if (initialSpawnQueue.isEmpty()) { + return; + } + if (!markerSystemEnabled()) { + initialSpawnQueue.clear(); + return; + } + + int budget = initialSpawnQueue.batchSize(MAX_INITIAL_DRAIN_PER_TICK); + while (budget-- > 0) { + Long key = initialSpawnQueue.poll(); + if (key == null) { + return; + } + int chunkX = unpackX(key); + int chunkZ = unpackZ(key); + boolean retry = false; + try { + if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) { + retry = true; + continue; + } + if (!initialSpawnChunk(level, chunkX, chunkZ)) { + retry = true; + warmupMantleChunkAsync(key, chunkX, chunkZ); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + retry = true; + } finally { + if (retry) { + initialSpawnQueue.retry(key); + } else { + initialSpawnQueue.complete(key); + } + } + } + } + + private boolean initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) { + if (!ModdedEntitySpawner.chunksSafe(level, chunkX, chunkZ)) { + return false; + } + Mantle mantle = engine.getMantle().getMantle(); + if (!mantle.isChunkLoaded(chunkX, chunkZ)) { + return false; + } + if (mantle.hasFlag(chunkX, chunkZ, INITIAL_SPAWN_COMPLETION_FLAG)) { + return true; + } + + MantleChunk chunk = mantle.getChunk(chunkX, chunkZ).use(); + try { + chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> { + spawnMarkerSpawners(level, chunkX, chunkZ, chunk, true); + scheduleInitialFollowUp(level, chunkX, chunkZ); + }); + } finally { + chunk.release(); + } + return true; + } + + private void scheduleInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) { + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + IrisLogging.error("Iris could not schedule the initial entity-spawn follow-up because the modded scheduler is unavailable."); + return; + } + scheduler.laterGlobal( + () -> EngineLifecycleTasks.run( + engine, + "modded_world_manager_initial_spawn_followup", + () -> runInitialFollowUp(level, chunkX, chunkZ)), + RNG.r.i(5, 200)); + } + + private void runInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) { + Mantle mantle = engine.getMantle().getMantle(); + if (closed || engine.isClosed() || mantle.isClosed() || !isEntitySpawningEnabledForCurrentWorld()) { + return; + } + if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null || !mantle.isChunkLoaded(chunkX, chunkZ)) { + return; + } + if (engine.getComplex() == null) { + return; + } + + MantleChunk chunk = mantle.getChunk(chunkX, chunkZ).use(); + try { + if (markerSystemEnabled()) { + spawnMarkerSpawners(level, chunkX, chunkZ, chunk, false); + } + if (ambientSystemEnabled()) { + spawnAmbient(level, chunkX, chunkZ, true); + } + } finally { + chunk.release(); + } + } + + private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) { + if (closed || !mantleWarmups.add(key)) { + return; + } + try { + mantleWarmupExecutor.execute(() -> { + if (!EngineLifecycleTasks.run( + engine, + "modded_world_manager_mantle_warmup", + () -> warmupMantleChunk(key, chunkX, chunkZ))) { + mantleWarmups.remove(key); + } + }); + } catch (RejectedExecutionException e) { + mantleWarmups.remove(key); + } + } + + private void warmupMantleChunk(long key, int chunkX, int chunkZ) { + try { + Mantle mantle = engine.getMantle().getMantle(); + if (!closed && !engine.isClosed() && !mantle.isClosed() && !mantle.isChunkLoaded(chunkX, chunkZ)) { + mantle.getChunk(chunkX, chunkZ); + } + } catch (Throwable e) { + if (!closed && !engine.isClosed()) { + IrisLogging.reportError(e); + } + } finally { + mantleWarmups.remove(key); + } + } + + private void ambientTick(ServerLevel level) { + long now = System.currentTimeMillis(); + long interval = IrisSettings.get().getWorld().getAsyncTickIntervalMS(); + if (now - lastAmbientAt < interval) { + return; + } + lastAmbientAt = now; + + if (!markerSystemEnabled() && !ambientSystemEnabled()) { + return; + } + if (level.players().isEmpty()) { + return; + } + + int loadedChunks = sampleLoadedChunks(level); + refreshEntityCount(level, loadedChunks); + if (!entityCountAvailable) { + return; + } + if (cachedSaturation > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) { + return; + } + + int sampled = Math.min(loadedChunks, ambientChunkSample.length); + if (sampled == 0) { + return; + } + + int spawnBuffer = RNG.r.i(2, 12); + while (spawnBuffer-- > 0) { + long key = ambientChunkSample[RNG.r.nextInt(sampled)]; + try { + ambientSpawnChunk(level, unpackX(key), unpackZ(key)); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + + private void ambientSpawnChunk(ServerLevel level, int chunkX, int chunkZ) { + if (!ModdedEntitySpawner.chunksSafe(level, chunkX, chunkZ)) { + return; + } + Mantle mantle = engine.getMantle().getMantle(); + if (!mantle.isChunkLoaded(chunkX, chunkZ)) { + return; + } + + MantleChunk chunk = mantle.getChunk(chunkX, chunkZ).use(); + try { + if (markerSystemEnabled()) { + spawnMarkerSpawners(level, chunkX, chunkZ, chunk, false); + } + if (ambientSystemEnabled()) { + spawnAmbient(level, chunkX, chunkZ, false); + } + } finally { + chunk.release(); + } + } + + private void spawnMarkerSpawners(ServerLevel level, int chunkX, int chunkZ, MantleChunk chunk, boolean initial) { + int minHeight = engine.getWorld().minHeight(); + KList obstructed = new KList<>(); + chunk.iterate(MatterMarker.class, (Integer x, Integer yf, Integer z, MatterMarker marker) -> { + String tag = marker.getTag(); + if (tag.equals("cave_floor") || tag.equals("cave_ceiling")) { + return; + } + IrisMarker resolved = engine.getData().getMarkerLoader().load(tag); + if (resolved == null) { + return; + } + + int worldX = (chunkX << 4) + (x & 15); + int worldZ = (chunkZ << 4) + (z & 15); + int worldY = yf + minHeight; + + if (resolved.isEmptyAbove() && aboveObstructed(level, worldX, worldY, worldZ)) { + obstructed.add(new IrisPosition(worldX, yf, worldZ)); + return; + } + + KList spawners = resolveMarkerSpawners(resolved); + if (spawners.isEmpty()) { + return; + } + IrisSpawner chosen = spawners.getRandom(); + if (chosen == null) { + return; + } + spawnFromSpawner(level, new IrisPosition(worldX, worldY, worldZ), chosen, initial); + }); + Mantle mantle = engine.getMantle().getMantle(); + for (IrisPosition position : obstructed) { + mantle.remove(position.getX(), position.getY(), position.getZ(), MatterMarker.class); + } + } + + private KList resolveMarkerSpawners(IrisMarker marker) { + KList spawners = new KList<>(); + for (String key : marker.getSpawners()) { + IrisSpawner spawner = engine.getData().getSpawnerLoader().load(key); + if (spawner == null) { + IrisLogging.error("Cannot load spawner: " + key + " for marker on " + engine.getName()); + continue; + } + spawner.setReferenceMarker(marker); + spawners.add(spawner); + } + return spawners; + } + + private void spawnFromSpawner(ServerLevel level, IrisPosition position, IrisSpawner spawner, boolean initial) { + KList spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns(); + if (spawns.isEmpty()) { + return; + } + for (IrisEntitySpawn entry : spawns) { + entry.setReferenceSpawner(spawner); + entry.setReferenceMarker(spawner.getReferenceMarker()); + } + IrisEntitySpawn chosen = rarityPick(spawns); + if (chosen == null) { + return; + } + + int chunkX = position.getX() >> 4; + int chunkZ = position.getZ() >> 4; + if (!canSpawn(spawner, chunkX, chunkZ)) { + return; + } + int spawned = spawnEntryAt(level, chosen, spawner, position); + if (spawned > 0) { + spawner.spawn(engine, chunkX, chunkZ); + } + } + + private void spawnAmbient(ServerLevel level, int chunkX, int chunkZ, boolean initial) { + IrisComplex complex = engine.getComplex(); + if (complex == null) { + return; + } + + int blockX = chunkX << 4; + int blockZ = chunkZ << 4; + IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ); + IrisRegion region = engine.getRegion(blockX, blockZ); + int chunkMobs = countChunkLivingEntities(level, chunkX, chunkZ); + + KList pool = new KList<>(); + collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(engine.getDimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial); + collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(region.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial); + collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial); + if (pool.isEmpty()) { + return; + } + + IrisEntitySpawn chosen = rarityPick(pool); + if (chosen == null || chosen.getReferenceSpawner() == null) { + return; + } + IrisSpawner spawner = chosen.getReferenceSpawner(); + if (!canSpawn(spawner, chunkX, chunkZ)) { + return; + } + int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ); + if (spawned > 0) { + spawner.spawn(engine, chunkX, chunkZ); + } + } + + private void collectSpawns(KList pool, KList spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, int chunkMobs, boolean initial) { + for (IrisSpawner spawner : spawners) { + if (spawner == null) { + continue; + } + if (spawner.getMaxEntitiesPerChunk() <= chunkMobs) { + continue; + } + if (biomeFilter != null && !spawner.isValid(biomeFilter)) { + continue; + } + if (!canSpawn(spawner, chunkX, chunkZ)) { + continue; + } + KList spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns(); + for (IrisEntitySpawn entry : spawns) { + entry.setReferenceSpawner(spawner); + entry.setReferenceMarker(spawner.getReferenceMarker()); + pool.add(entry); + } + } + } + + private int spawnEntry(ServerLevel level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ) { + IrisEntity irisEntity = entry.getRealEntity(engine); + if (irisEntity == null) { + return 0; + } + + int min = entry.getMinSpawns(); + int max = entry.getMaxSpawns(); + int count = LootResolver.inclusive(RNG.r, min, max); + if (count <= 0) { + return 0; + } + + RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity())); + IrisSpawnGroup group = spawner.getGroup(); + KList caveFloors = group == IrisSpawnGroup.CAVE + ? engine.getMantle().findMarkers(chunkX, chunkZ, MarkerMatter.CAVE_FLOOR) + : new KList<>(); + int spawned = 0; + for (int i = 0; i < count; i++) { + int worldX; + int worldY; + int worldZ; + if (group == IrisSpawnGroup.CAVE) { + if (caveFloors.isEmpty()) { + continue; + } + IrisPosition caveFloor = caveFloors.getRandom(RNG.r); + worldX = caveFloor.getX(); + worldY = caveFloor.getY() + 1; + worldZ = caveFloor.getZ(); + } else { + worldX = (chunkX << 4) + RNG.r.i(16); + worldZ = (chunkZ << 4) + RNG.r.i(16); + int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false); + int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true); + worldY = group == IrisSpawnGroup.NORMAL + ? surfaceY + 1 + : RNG.r.i(solidY + 1, surfaceY); + } + if (worldY <= level.getMinY() || worldY >= level.getMaxY()) { + continue; + } + // Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity). + if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) { + continue; + } + if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) { + continue; + } + if (!ModdedEntitySpawner.isAreaClearForSpawn(level, irisEntity, worldX, worldY, worldZ)) { + continue; + } + if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) { + spawned++; + } + } + return spawned; + } + + private int spawnEntryAt(ServerLevel level, IrisEntitySpawn entry, IrisSpawner spawner, IrisPosition position) { + IrisEntity irisEntity = entry.getRealEntity(engine); + if (irisEntity == null) { + return 0; + } + + int min = entry.getMinSpawns(); + int max = entry.getMaxSpawns(); + int count = LootResolver.inclusive(RNG.r, min, max); + if (count <= 0) { + return 0; + } + + exhaustMarker(spawner, position); + + RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity())); + int worldX = position.getX(); + int worldY = position.getY() + 1; + int worldZ = position.getZ(); + int spawned = 0; + for (int i = 0; i < count; i++) { + // Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity). + if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) { + continue; + } + if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) { + spawned++; + } + } + return spawned; + } + + private void exhaustMarker(IrisSpawner spawner, IrisPosition position) { + IrisMarker marker = spawner.getReferenceMarker(); + if (marker == null || !marker.shouldExhaust()) { + return; + } + engine.getMantle().getMantle().remove(position.getX(), position.getY() - engine.getWorld().minHeight(), position.getZ(), MatterMarker.class); + } + + private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ) { + return spawner.canSpawn(engine, chunkX, chunkZ); + } + + private boolean lightAllowed(IrisSpawner spawner, ServerLevel level, int worldX, int worldY, int worldZ) { + IrisRange range = spawner.getAllowedLightLevels(); + if (range.getMin() > 0 || range.getMax() < 15) { + return range.contains(level.getMaxLocalRawBrightness(new BlockPos(worldX, worldY, worldZ))); + } + return true; + } + + private boolean surfaceMatches(IrisSurface surface, ServerLevel level, int worldX, int worldY, int worldZ) { + BlockState below = level.getBlockState(new BlockPos(worldX, worldY - 1, worldZ)); + return matchesSurface(surface, below); + } + + private static boolean matchesSurface(IrisSurface surface, BlockState below) { + if (ModdedBlockResolution.isSolid(below)) { + return surface == IrisSurface.LAND || surface == IrisSurface.OVERWORLD + || (surface == IrisSurface.ANIMAL && isAnimalGround(below)); + } + if (below.is(Blocks.LAVA)) { + return surface == IrisSurface.LAVA; + } + if (ModdedBlockResolution.isWater(below) || ModdedBlockResolution.isWaterLogged(below) || isAquaticFoliage(below)) { + return surface == IrisSurface.WATER || surface == IrisSurface.OVERWORLD; + } + return false; + } + + private static boolean isAnimalGround(BlockState state) { + return state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.DIRT) || state.is(Blocks.DIRT_PATH) + || state.is(Blocks.COARSE_DIRT) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.PODZOL) + || state.is(Blocks.MYCELIUM) || state.is(Blocks.SNOW_BLOCK); + } + + private static boolean isAquaticFoliage(BlockState state) { + return state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS) + || state.is(Blocks.KELP) || state.is(Blocks.KELP_PLANT); + } + + private boolean aboveObstructed(ServerLevel level, int worldX, int worldY, int worldZ) { + return worldY + 2 >= level.getMaxY() + || ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ))) + || ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 2, worldZ))); + } + + private int countChunkLivingEntities(ServerLevel level, int chunkX, int chunkZ) { + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + AABB box = new AABB(baseX, level.getMinY(), baseZ, baseX + 16, level.getMaxY(), baseZ + 16); + return level.getEntities((Entity) null, box, + (Entity entity) -> isLivingEntityInChunk(entity, chunkX, chunkZ)).size(); + } + + private static boolean isLivingEntityInChunk(Entity entity, int chunkX, int chunkZ) { + if (!(entity instanceof LivingEntity)) { + return false; + } + BlockPos position = entity.blockPosition(); + return (position.getX() >> 4) == chunkX && (position.getZ() >> 4) == chunkZ; + } + + /** + * ServerChunkCache.tickChunks rebuilds NaturalSpawner.SpawnState every tick from every entity in the + * level, so read that instead of walking all entities again on an Iris timer. The count is the natural + * spawn cap population (non persistent mobs in loaded chunks, MISC excluded), which is exactly the + * population the ambient spawn gate throttles against. No state means the level has not ticked chunks + * yet, so hold spawning rather than guess. + */ + private void refreshEntityCount(ServerLevel level, int loadedChunks) { + cachedConsideredChunks = loadedChunks; + NaturalSpawner.SpawnState spawnState = level.getChunkSource().getLastSpawnState(); + if (spawnState == null) { + entityCountAvailable = false; + if (!spawnStateMissingLogged) { + spawnStateMissingLogged = true; + IrisLogging.warn("No spawn state for " + engine.getName() + " yet; ambient spawning held"); + } + return; + } + + Object2IntMap counts = spawnState.getMobCategoryCounts(); + int mobs = 0; + for (MobCategory category : counts.keySet()) { + mobs += counts.getInt(category); + } + + entityCountAvailable = true; + cachedEntityCount = mobs; + // Metric = natural-spawn-cap population over natural-spawn chunk count: numerator and denominator both + // come from MC's own spawn state, so the ratio is not diluted by chunks the spawner never counts. + cachedSaturation = mobs / (spawnState.getSpawnableChunkCount() + 1.0) * 1.28; + } + + /** + * Reservoir sample (algorithm R) of the ready to send chunks into a fixed buffer. ambientTick only picks + * up to 12 random chunks per pass, so materializing every loaded chunk position once per interval was + * pure garbage. Returns how many chunks the walk saw, which is the same considered-chunk count the old + * snapshot length reported. Server thread only, so the reservoir and its counter are plain fields. + */ + private int sampleLoadedChunks(ServerLevel level) { + ambientChunkSampleSeen = 0; + level.getChunkSource().chunkMap.forEachReadyToSendChunk((LevelChunk chunk) -> { + int index = ambientChunkSampleSeen++; + int capacity = ambientChunkSample.length; + int slot = reservoirSlot(index, capacity, index < capacity ? 0 : RNG.r.nextInt(index + 1)); + if (slot >= 0) { + ambientChunkSample[slot] = chunk.getPos().pack(); + } + }); + return ambientChunkSampleSeen; + } + + /** + * Algorithm R slot for the item at {@code index}: fill the reservoir first, then keep the item only when + * {@code roll} (uniform over 0..index) lands inside the reservoir. Negative means drop the item. + */ + static int reservoirSlot(int index, int capacity, int roll) { + if (index < capacity) { + return index; + } + return roll < capacity ? roll : -1; + } + + private boolean isPregenActive() { + PregeneratorJob job = PregeneratorJob.getInstance(); + return job != null && job.targetsWorldIdentity(engine.getWorld().identity()); + } + + private static boolean markerSystemEnabled() { + return IrisSettings.get().getWorld().isMarkerEntitySpawningSystem(); + } + + private static boolean ambientSystemEnabled() { + return IrisSettings.get().getWorld().isAmbientEntitySpawningSystem(); + } + + private boolean isEntitySpawningEnabledForCurrentWorld() { + return entitySpawningEnabled(engine.isStudio(), IrisSettings.get().getStudio().isEntitySpawning()); + } + + static boolean entitySpawningEnabled(boolean studio, boolean studioSetting) { + return !studio || studioSetting; + } + + private IrisEntitySpawn rarityPick(KList entries) { + KList weighted = IRare.expandWeighted(entries); + return weighted.isEmpty() ? entries.getRandom() : weighted.getRandom(); + } + + private static long pack(int x, int z) { + return (((long) x) & 0xFFFFFFFFL) | ((((long) z) & 0xFFFFFFFFL) << 32); + } + + private static int unpackX(long key) { + return (int) (key & 0xFFFFFFFFL); + } + + private static int unpackZ(long key) { + return (int) ((key >> 32) & 0xFFFFFFFFL); + } + + @Override + public synchronized void close() { + closed = true; + Throwable failure = null; + if (!initialSpawnQueueClosed) { + try { + initialSpawnQueue.close(); + initialSpawnQueueClosed = true; + } catch (Throwable e) { + failure = e; + } + } + if (!mantleWarmupExecutorStopped) { + try { + // Drain, don't interrupt: an interrupt inside a FileChannel plate read closes + // the channel and the read-failure fallback installs an empty plate that a + // later flush persists over real data. Queued tasks no-op on the closed flag, + // so the await only covers a single in-flight load; escalate on timeout only. + mantleWarmupExecutor.shutdown(); + if (!mantleWarmupExecutor.awaitTermination(5L, java.util.concurrent.TimeUnit.SECONDS)) { + IrisLogging.warn("Iris mantle warm-up did not stop before world manager close; forcing interrupt"); + mantleWarmupExecutor.shutdownNow(); + } + mantleWarmupExecutorStopped = true; + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + mantleWarmupExecutor.shutdownNow(); + mantleWarmupExecutorStopped = true; + } catch (Throwable e) { + failure = appendCloseFailure(failure, e); + } + } + if (!mantleWarmupsCleared) { + try { + mantleWarmups.clear(); + mantleWarmupsCleared = true; + } catch (Throwable e) { + failure = appendCloseFailure(failure, e); + } + } + if (failure != null) { + throw new IllegalStateException("Failed to completely stop the modded Iris world manager.", failure); + } + } + + @Override + public int getEntityCount() { + return cachedEntityCount; + } + + @Override + public int getChunkCount() { + return cachedConsideredChunks; + } + + @Override + public double getEntitySaturation() { + return cachedSaturation; + } + + @Override + public void onTick() { + } + + @Override + public void onSave() { + engine.getMantle().save(); + } + + private static Throwable appendCloseFailure(Throwable failure, Throwable next) { + if (failure == null) { + return next; + } + if (failure != next) { + failure.addSuppressed(next); + } + return failure; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldgenIds.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldgenIds.java new file mode 100644 index 000000000..64f99715c --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldgenIds.java @@ -0,0 +1,103 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; + +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +public final class ModdedWorldgenIds { + private static final String NAMESPACE = "irisworldgen"; + + private ModdedWorldgenIds() { + } + + public static String presetRef(String pack, String dimension) { + return NAMESPACE + ":" + scopedPath(pack, dimension) + "/preset"; + } + + public static String dimensionTypeRef(String pack, String dimension) { + return NAMESPACE + ":" + scopedPath(pack, dimension) + "/dimension_type"; + } + + public static String biomePathPrefix(String pack, String dimension) { + return scopedPath(pack, dimension) + "/biomes"; + } + + public static String biomeRef(String pack, String dimension, String biome) { + return NAMESPACE + ":" + biomePathPrefix(pack, dimension) + "/" + + biome.toLowerCase(Locale.ROOT); + } + + public static String biomeRef(Engine engine, String biome) { + return biomeRef(engine.getData().getDataFolder().getName(), + engine.getDimension().getLoadKey(), biome); + } + + public static String displayName(String presetPath) { + String[] parts = presetPath.split("/"); + if (parts.length != 5 || !"packs".equals(parts[0]) + || !"dimensions".equals(parts[2]) || !"preset".equals(parts[4])) { + return null; + } + String pack = decode(parts[1]); + String dimension = decode(parts[3]); + if (pack == null || dimension == null) { + return null; + } + String packLabel = title(pack); + return pack.equalsIgnoreCase(dimension) + ? "IRIS:" + packLabel + : "IRIS:" + packLabel + " / " + title(dimension); + } + + public static String generatorIdentity(String packDimension) { + int separator = packDimension.indexOf(':'); + String pack = separator >= 0 ? packDimension.substring(0, separator) : packDimension; + String dimension = separator >= 0 ? packDimension.substring(separator + 1) : packDimension; + return "iris:" + (pack.equalsIgnoreCase(dimension) + ? pack.toLowerCase(Locale.ROOT) + : pack.toLowerCase(Locale.ROOT) + "/" + dimension.toLowerCase(Locale.ROOT)); + } + + private static String scopedPath(String pack, String dimension) { + return "packs/" + encode(pack) + "/dimensions/" + encode(dimension); + } + + private static String encode(String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + StringBuilder encoded = new StringBuilder(bytes.length * 2); + for (byte current : bytes) { + encoded.append(Character.forDigit((current >>> 4) & 0xF, 16)); + encoded.append(Character.forDigit(current & 0xF, 16)); + } + return encoded.toString(); + } + + private static String decode(String value) { + if ((value.length() & 1) != 0) { + return null; + } + byte[] bytes = new byte[value.length() / 2]; + try { + for (int index = 0; index < bytes.length; index++) { + int high = Character.digit(value.charAt(index * 2), 16); + int low = Character.digit(value.charAt(index * 2 + 1), 16); + if (high < 0 || low < 0) { + return null; + } + bytes[index] = (byte) ((high << 4) | low); + } + return new String(bytes, StandardCharsets.UTF_8); + } catch (RuntimeException invalid) { + return null; + } + } + + private static String title(String value) { + String normalized = value.replace('_', ' ').replace('-', ' ').trim(); + if (normalized.isEmpty()) { + return value; + } + return Character.toUpperCase(normalized.charAt(0)) + normalized.substring(1); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/NativeSpawnTableMerger.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/NativeSpawnTableMerger.java new file mode 100644 index 000000000..2faa17e2b --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/NativeSpawnTableMerger.java @@ -0,0 +1,34 @@ +package art.arcane.iris.modded; + +import net.minecraft.util.random.Weighted; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.biome.MobSpawnSettings; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +final class NativeSpawnTableMerger { + private NativeSpawnTableMerger() { + } + + static WeightedList merge( + WeightedList vanillaSpawns, + WeightedList explicitSpawns) { + List> entries = new ArrayList<>( + vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size()); + Set> explicitTypes = new HashSet<>(); + for (Weighted entry : explicitSpawns.unwrap()) { + explicitTypes.add(entry.value().type()); + } + for (Weighted entry : vanillaSpawns.unwrap()) { + if (!explicitTypes.contains(entry.value().type())) { + entries.add(entry); + } + } + entries.addAll(explicitSpawns.unwrap()); + return WeightedList.of(entries); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java new file mode 100644 index 000000000..22af5f586 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java @@ -0,0 +1,114 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.volmlib.util.json.JSONObject; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.dimension.DimensionType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class WorldCheckDimensionContract { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private WorldCheckDimensionContract() { + } + + static boolean checkDimensionType(ServerLevel level, IrisModdedChunkGenerator generator) { + try { + IrisDimension dimension = generator.commandEngine().getDimension(); + DimensionContract expected = expectedDimensionContract(dimension); + DimensionContract actual = runtimeDimensionContract(level.dimensionType()); + boolean pass = matchesDimensionContract(level.getMinY(), level.getHeight(), expected, actual); + String detail = "expected=" + expected + ",actual=" + actual + + ",levelMinY=" + level.getMinY() + ",levelHeight=" + level.getHeight(); + WorldCheckPredicates.qaEvent("dimension_type", dimension.getLoadKey(), pass, detail); + if (!pass) { + LOGGER.error("[worldcheck] dimension type mismatch for {}: {}", dimension.getLoadKey(), detail); + } else { + LOGGER.info("[worldcheck] dimension type contract: {}", detail); + } + return pass; + } catch (Throwable error) { + LOGGER.error("[worldcheck] could not validate the Iris dimension type contract", error); + WorldCheckPredicates.qaEvent("dimension_type", generator.activeDimensionKey(), false, + "validationError=" + error.getClass().getSimpleName() + ":" + error.getMessage()); + return false; + } + } + + static DimensionContract expectedDimensionContract(IrisDimension dimension) { + JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get())); + return new DimensionContract( + json.getInt("min_y"), + json.getInt("height"), + json.getInt("logical_height"), + json.getDouble("coordinate_scale"), + (float) json.getDouble("ambient_light"), + json.getBoolean("has_skylight"), + json.getBoolean("has_ceiling"), + json.getBoolean("has_ender_dragon_fight"), + json.getInt("monster_spawn_block_light_limit")); + } + + static DimensionContract runtimeDimensionContract(DimensionType dimensionType) { + return new DimensionContract( + dimensionType.minY(), + dimensionType.height(), + dimensionType.logicalHeight(), + dimensionType.coordinateScale(), + dimensionType.ambientLight(), + dimensionType.hasSkyLight(), + dimensionType.hasCeiling(), + dimensionType.hasEnderDragonFight(), + dimensionType.monsterSpawnBlockLightLimit()); + } + + static boolean matchesDimensionContract(int levelMinY, int levelHeight, + DimensionContract expected, DimensionContract actual) { + return levelMinY == expected.minY() + && levelHeight == expected.height() + && actual.equals(expected); + } + + static boolean checkEntityMixins(ServerLevel level) { + ItemEntity item = new ItemEntity(level, 0D, level.getMinY(), 0D, Items.COBBLESTONE.getDefaultInstance()); + boolean vanillaSave = item.shouldBeSaved(); + ModdedEntityPersistence.configure(item, false); + boolean suppressed = !item.shouldBeSaved(); + ModdedEntityPersistence.configure(item, true); + boolean restored = item.shouldBeSaved(); + boolean pass = vanillaSave && suppressed && restored; + WorldCheckPredicates.qaEvent("entity_mixin", "persistence", pass, + "vanilla=" + vanillaSave + ",suppressed=" + suppressed + ",restored=" + restored); + if (!pass) { + LOGGER.error("[worldcheck] shared entity mixins are not active on this loader"); + } + return pass; + } + + record DimensionContract(int minY, int height, int logicalHeight, double coordinateScale, + float ambientLight, boolean hasSkyLight, boolean hasCeiling, + boolean hasEnderDragonFight, int monsterSpawnBlockLightLimit) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckFeaturePlacement.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckFeaturePlacement.java new file mode 100644 index 000000000..b19395227 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckFeaturePlacement.java @@ -0,0 +1,159 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import net.minecraft.world.level.ChunkPos; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.filter.AbstractFilter; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@code feature_placement} worldcheck gate for the imported-feature pass. + * + *

Vanilla does not throw when a feature writes into a chunk it is not allowed to touch; it logs + * {@code Detected setBlock in a far chunk} and drops the write. Reading back the world cannot distinguish that + * from a feature that legitimately placed nothing, so the gate watches the log instead. The second marker, + * {@code Requested chunk unavailable during world generation}, does throw, and is recorded from the feature + * pass directly. + * + *

Only armed under {@code -Diris.worldcheck}. Outside a gated run every entry point here is a single + * boolean read. + */ +final class WorldCheckFeaturePlacement { + private static final boolean ENABLED = Boolean.getBoolean("iris.worldcheck"); + private static final String FAR_CHUNK_MARKER = "Detected setBlock in a far chunk"; + private static final String UNAVAILABLE_CHUNK_MARKER = "Requested chunk unavailable during world generation"; + private static final List MARKERS = List.of(FAR_CHUNK_MARKER, UNAVAILABLE_CHUNK_MARKER); + private static final int REPORTED_SAMPLE_MAX = 5; + + private static final AtomicBoolean INSTALLED = new AtomicBoolean(); + private static final AtomicBoolean PASS_REPORTED = new AtomicBoolean(); + private static final AtomicBoolean SKIP_REPORTED = new AtomicBoolean(); + private static final AtomicInteger VIOLATIONS = new AtomicInteger(); + + private WorldCheckFeaturePlacement() { + } + + /** + * Arms the log watch. Called when the feature table is published, which is before any chunk decorates, so + * the watch is installed for the very first pass rather than from the second chunk onwards. The pass and + * failure recorders arm too, for a path that publishes a table without going through prepare. Idempotent + * and cheap when the gate is off. + */ + static void arm() { + if (!ENABLED || !INSTALLED.compareAndSet(false, true)) { + return; + } + try { + ((Logger) LogManager.getRootLogger()).addFilter(new PlacementWatch()); + } catch (Throwable error) { + WorldCheckPredicates.qaEvent("feature_placement", "all", false, + "skipped=log-watch-unavailable," + error.getClass().getSimpleName()); + } + } + + /** + * Emits the passing gate event once, after the first clean chunk. A later violation emits its own failing + * event, so a run that fails after passing still reports the failure. + */ + static void recordPlacementPass() { + if (!ENABLED) { + return; + } + arm(); + if (VIOLATIONS.get() == 0 && PASS_REPORTED.compareAndSet(false, true)) { + WorldCheckPredicates.qaEvent("feature_placement", "all", true, "violations=0"); + } + } + + /** + * Emits the not-asserted event once, so a gated run can tell "importedFeatures was off, nothing to check" + * apart from "checked and clean". + */ + static void recordFeaturesOff() { + if (!ENABLED) { + return; + } + if (SKIP_REPORTED.compareAndSet(false, true)) { + WorldCheckPredicates.qaEvent("feature_placement", "all", true, "skipped=importedFeatures-off"); + } + } + + static void recordPlacementFailure(ChunkPos chunkPos, Throwable error) { + if (!ENABLED) { + return; + } + arm(); + String message = messageChain(error); + String detail = message.contains(UNAVAILABLE_CHUNK_MARKER) + ? "unavailableChunk" + : "placementFailure"; + report(detail + ",chunk=" + chunkPos.x() + "," + chunkPos.z() + ",error=" + + error.getClass().getSimpleName()); + } + + private static void report(String detail) { + int count = VIOLATIONS.incrementAndGet(); + if (count <= REPORTED_SAMPLE_MAX) { + WorldCheckPredicates.qaEvent("feature_placement", "all", false, detail + ",violations=" + count); + } + } + + private static String messageChain(Throwable error) { + StringBuilder chain = new StringBuilder(); + Throwable current = error; + for (int depth = 0; current != null && depth < 16; depth++) { + if (current.getMessage() != null) { + chain.append(current.getMessage()).append('\n'); + } + current = current.getCause() == current ? null : current.getCause(); + } + return chain.toString(); + } + + private static final class PlacementWatch extends AbstractFilter { + private PlacementWatch() { + super(Filter.Result.NEUTRAL, Filter.Result.NEUTRAL); + } + + @Override + public Filter.Result filter(LogEvent event) { + if (event == null || event.getMessage() == null) { + return Filter.Result.NEUTRAL; + } + String message = event.getMessage().getFormattedMessage(); + if (message == null) { + return Filter.Result.NEUTRAL; + } + for (String marker : MARKERS) { + if (message.contains(marker)) { + report("logMarker=" + marker); + break; + } + } + return Filter.Result.NEUTRAL; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckMaterials.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckMaterials.java new file mode 100644 index 000000000..32fe1eccd --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckMaterials.java @@ -0,0 +1,130 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import net.minecraft.resources.Identifier; + +final class WorldCheckMaterials { + private WorldCheckMaterials() { + } + + static boolean isCharacteristicMaterial(String structureLabel, Identifier structureKey, Identifier blockKey) { + if (structureKey == null || blockKey == null || !blockKey.getNamespace().equals("minecraft")) { + return false; + } + String block = blockKey.getPath(); + return switch (structureLabel) { + case "stronghold" -> isStrongholdMaterial(block); + case "trial_chambers" -> isTrialChamberMaterial(block); + case "mansion" -> isWoodConstructionMaterial(block, "dark_oak") + || isWoodConstructionMaterial(block, "birch") + || isCobblestoneConstructionMaterial(block); + case "village" -> isVillageMaterial(structureKey.getPath(), block); + case "monument" -> isMonumentMaterial(block); + default -> false; + }; + } + + private static boolean isStrongholdMaterial(String block) { + return block.equals("stone_bricks") + || block.equals("cracked_stone_bricks") + || block.equals("mossy_stone_bricks") + || block.equals("infested_stone_bricks") + || block.equals("infested_cracked_stone_bricks") + || block.equals("infested_mossy_stone_bricks") + || block.equals("stone_brick_stairs") + || block.equals("stone_brick_slab") + || block.equals("stone_brick_wall"); + } + + private static boolean isTrialChamberMaterial(String block) { + return block.contains("tuff_brick") + || block.equals("polished_tuff") + || block.equals("chiseled_tuff") + || block.endsWith("copper_grate") + || block.equals("trial_spawner") + || block.equals("vault"); + } + + private static boolean isVillageMaterial(String structure, String block) { + if (isCobblestoneConstructionMaterial(block)) { + return true; + } + return switch (structure) { + case "village_plains" -> isWoodConstructionMaterial(block, "oak"); + case "village_desert" -> block.equals("cut_sandstone") + || block.equals("smooth_sandstone") + || block.equals("cut_sandstone_slab") + || block.equals("smooth_sandstone_slab") + || block.equals("smooth_sandstone_stairs") + || block.equals("sandstone_stairs") + || block.equals("sandstone_slab") + || block.equals("sandstone_wall"); + case "village_savanna" -> isWoodConstructionMaterial(block, "acacia"); + case "village_snowy", "village_taiga" -> isWoodConstructionMaterial(block, "spruce"); + default -> false; + }; + } + + private static boolean isWoodConstructionMaterial(String block, String wood) { + if (block.startsWith(wood)) { + int suffixOffset = wood.length(); + if (matchesSuffix(block, suffixOffset, "_planks") + || matchesSuffix(block, suffixOffset, "_stairs") + || matchesSuffix(block, suffixOffset, "_slab") + || matchesSuffix(block, suffixOffset, "_fence") + || matchesSuffix(block, suffixOffset, "_fence_gate") + || matchesSuffix(block, suffixOffset, "_door") + || matchesSuffix(block, suffixOffset, "_trapdoor")) { + return true; + } + } + int strippedOffset = "stripped_".length(); + if (!block.startsWith("stripped_") + || !block.regionMatches(strippedOffset, wood, 0, wood.length())) { + return false; + } + int suffixOffset = strippedOffset + wood.length(); + return matchesSuffix(block, suffixOffset, "_log") + || matchesSuffix(block, suffixOffset, "_wood"); + } + + private static boolean isCobblestoneConstructionMaterial(String block) { + return block.equals("cobblestone") + || block.equals("cobblestone_stairs") + || block.equals("cobblestone_slab") + || block.equals("cobblestone_wall") + || block.equals("mossy_cobblestone") + || block.equals("mossy_cobblestone_stairs") + || block.equals("mossy_cobblestone_slab") + || block.equals("mossy_cobblestone_wall"); + } + + private static boolean matchesSuffix(String value, int offset, String suffix) { + return value.length() == offset + suffix.length() + && value.regionMatches(offset, suffix, 0, suffix.length()); + } + + private static boolean isMonumentMaterial(String block) { + return block.equals("prismarine") + || block.equals("prismarine_bricks") + || block.equals("dark_prismarine") + || block.equals("sea_lantern"); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckPredicates.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckPredicates.java new file mode 100644 index 000000000..12acad76b --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckPredicates.java @@ -0,0 +1,130 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.StructureVerticalBounds; +import art.arcane.iris.modded.WorldCheckStructureAudit.StructureCheck; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +final class WorldCheckPredicates { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private WorldCheckPredicates() { + } + + static void emitSkipped(StructureCheck check, String reason, String... events) { + for (String event : events) { + qaEvent(event, check.label(), false, "skipped=" + reason); + } + } + + static void qaEvent(String event, String structure, boolean pass, String detail) { + LOGGER.info(qaEventJson(event, structure, pass, detail)); + } + + static String qaEventJson(String event, String structure, boolean pass, String detail) { + return "QA_EVT {\"event\":\"" + jsonEscape(event) + + "\",\"structure\":\"" + jsonEscape(structure) + + "\",\"pass\":" + pass + + ",\"detail\":\"" + jsonEscape(detail) + "\"}"; + } + + static String jsonEscape(String value) { + StringBuilder escaped = new StringBuilder(value.length() + 16); + for (int i = 0; i < value.length(); i++) { + char character = value.charAt(i); + switch (character) { + case '"' -> escaped.append("\\\""); + case '\\' -> escaped.append("\\\\"); + case '\b' -> escaped.append("\\b"); + case '\f' -> escaped.append("\\f"); + case '\n' -> escaped.append("\\n"); + case '\r' -> escaped.append("\\r"); + case '\t' -> escaped.append("\\t"); + default -> { + if (character < 32) { + escaped.append("\\u"); + String hex = Integer.toHexString(character); + escaped.append("0".repeat(4 - hex.length())).append(hex); + } else { + escaped.append(character); + } + } + } + } + return escaped.toString(); + } + + static boolean hasNativeStructureEvidence(boolean validStart, int references) { + return validStart || references > 0; + } + + static boolean hasCharacteristicMaterialEvidence(int blocks, int chunksWithMaterial, int scannedChunks) { + if (blocks <= 0 || chunksWithMaterial <= 0 || scannedChunks <= 0 + || chunksWithMaterial > scannedChunks) { + return false; + } + return scannedChunks == 1 || chunksWithMaterial > 1; + } + + static boolean verticalShiftMatches(int configuredShift, Integer appliedShift, int shiftedMinY, + int shiftedMaxY, int worldMinY, int worldMaxYExclusive) { + try { + if (appliedShift == null) { + return configuredShift == 0 && StructureVerticalBounds.clampOffset( + shiftedMinY, shiftedMaxY, 0, worldMinY, worldMaxYExclusive) == 0; + } + int originalMinY = Math.subtractExact(shiftedMinY, appliedShift); + int originalMaxY = Math.subtractExact(shiftedMaxY, appliedShift); + int expectedShift = StructureVerticalBounds.clampOffset( + originalMinY, originalMaxY, configuredShift, worldMinY, worldMaxYExclusive); + return appliedShift == expectedShift; + } catch (RuntimeException error) { + return false; + } + } + + static boolean mansionVegetationPass(int remainingVegetationBlocks) { + return remainingVegetationBlocks == 0; + } + + static boolean mansionVegetationAbovePiece(boolean vegetation, int blockY, int highestPieceY) { + return vegetation && blockY > highestPieceY; + } + + static boolean villageFoundationPass(int unsupportedColumns) { + return unsupportedColumns == 0; + } + + static boolean villagePoiPass(int inBounds, int outOfBounds) { + return inBounds > 0 && outOfBounds == 0; + } + + static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckStructureAudit.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckStructureAudit.java new file mode 100644 index 000000000..dcd7630e5 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckStructureAudit.java @@ -0,0 +1,783 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.nativegen.NativeStructureFoundationBuilder; +import art.arcane.iris.nativegen.NativeStructureVegetationClearer; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.IntBinaryOperator; + +final class WorldCheckStructureAudit { + static final List STRUCTURE_CHECKS = List.of( + new StructureCheck("stronghold", List.of("minecraft:stronghold"), 256), + new StructureCheck("trial_chambers", List.of("minecraft:trial_chambers"), 128), + new StructureCheck("mansion", List.of("minecraft:mansion"), 256), + new StructureCheck("village", List.of( + "minecraft:village_plains", + "minecraft:village_desert", + "minecraft:village_savanna", + "minecraft:village_snowy", + "minecraft:village_taiga"), 128), + new StructureCheck("monument", List.of("minecraft:monument"), 128) + ); + private static final int MAX_FOOTPRINT_CHUNKS = 96; + private static final int MAX_START_REFERENCE_CHUNKS = 16; + private static final int MAX_STRUCTURE_CANDIDATES = 1024; + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private WorldCheckStructureAudit() { + } + + static NativeStructureGate checkNativeStructures(ServerLevel level, + IrisModdedChunkGenerator generator, + BlockPos origin) { + boolean pass = true; + int nonVillagePassed = 0; + boolean villagePass = false; + PendingVillagePoi pendingPoi = null; + for (StructureCheck check : STRUCTURE_CHECKS) { + StructureCheckResult result = checkNativeStructure(level, generator, origin, check); + if (!result.pass()) { + pass = false; + } + if (check.label().equals("village")) { + villagePass = result.pass(); + pendingPoi = result.pendingPoi(); + } else if (result.pass()) { + nonVillagePassed++; + } + } + return new NativeStructureGate(pass, nonVillagePassed, villagePass, pendingPoi); + } + + private static StructureCheckResult checkNativeStructure(ServerLevel level, + IrisModdedChunkGenerator generator, + BlockPos origin, + StructureCheck check) { + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> registered = new ArrayList<>(check.registryKeys().size()); + LinkedHashSet registeredKeys = new LinkedHashSet<>(); + for (String key : check.registryKeys()) { + Identifier identifier = Identifier.tryParse(key); + if (identifier == null) { + continue; + } + Optional> resolved = registry.get(identifier); + if (resolved.isPresent()) { + registered.add(resolved.get()); + registeredKeys.add(identifier.toString()); + } + } + boolean registryOk = registered.size() == check.registryKeys().size(); + LOGGER.info("[worldcheck] {} registry: {}/{} resolved {}", check.label(), registered.size(), + check.registryKeys().size(), registeredKeys); + WorldCheckPredicates.qaEvent("structure_registry", check.label(), registryOk, + "resolved=" + registered.size() + ",expected=" + check.registryKeys().size() + + ",keys=" + String.join("|", registeredKeys)); + if (!registryOk) { + LOGGER.error("[worldcheck] {} registry resolution failed; expected {}", check.label(), check.registryKeys()); + WorldCheckPredicates.emitSkipped(check, "registry", "structure_reachability", "structure_locate", + "structure_start_reference", "structure_footprint", "structure_material", + "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + List> reachable = new ArrayList<>(registered.size()); + LinkedHashSet reachableKeys = new LinkedHashSet<>(); + for (Holder holder : registered) { + if (!generator.isNativeStructureReachable(holder)) { + continue; + } + reachable.add(holder); + Identifier key = registry.getKey(holder.value()); + if (key != null) { + reachableKeys.add(key.toString()); + } + } + boolean reachableOk = !reachable.isEmpty(); + LOGGER.info("[worldcheck] {} biome-reachable through Iris: {}", check.label(), reachableKeys); + WorldCheckPredicates.qaEvent("structure_reachability", check.label(), reachableOk, + "reachable=" + reachable.size() + ",registered=" + registered.size() + + ",keys=" + String.join("|", reachableKeys)); + if (!reachableOk) { + LOGGER.error("[worldcheck] {} cannot generate in any biome produced by this Iris pack", check.label()); + WorldCheckPredicates.emitSkipped(check, "reachability", "structure_locate", "structure_start_reference", + "structure_footprint", "structure_material", "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + long locateStart = System.nanoTime(); + Pair> found = findGeneratedStructureCandidate( + level, reachable, origin, check.locateRadius()); + long locateMillis = (System.nanoTime() - locateStart) / 1_000_000L; + Identifier foundKey = found == null ? null : registry.getKey(found.getSecond().value()); + boolean locateOk = found != null && foundKey != null && reachableKeys.contains(foundKey.toString()); + WorldCheckPredicates.qaEvent("structure_locate", check.label(), locateOk, + "method=placement_candidates,millis=" + locateMillis + ",radius=" + check.locateRadius() + + ",result=" + (foundKey == null ? "none" : foundKey)); + if (found == null) { + LOGGER.error("[worldcheck] {} native placement candidates produced no valid start within {} rings after {}ms", + check.label(), check.locateRadius(), locateMillis); + WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint", + "structure_material", "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + BlockPos position = found.getFirst(); + LOGGER.info("[worldcheck] {} generated candidate: {} {} {} in {}ms (radius={}, result={})", + check.label(), position.getX(), position.getY(), position.getZ(), locateMillis, + check.locateRadius(), foundKey); + if (!locateOk) { + LOGGER.error("[worldcheck] {} candidate scan returned unexpected structure {}", check.label(), foundKey); + WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint", + "structure_material", "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + int chunkX = position.getX() >> 4; + int chunkZ = position.getZ() >> 4; + ChunkAccess targetChunk = level.getChunk(chunkX, chunkZ); + Structure structure = found.getSecond().value(); + StructureStart start = resolveStructureStart(level, targetChunk, structure); + boolean validStart = start != null && start.isValid(); + int references = targetChunk.getReferencesForStructure(structure).size(); + boolean startReferenceOk = WorldCheckPredicates.hasNativeStructureEvidence(validStart, references); + LOGGER.info("[worldcheck] {} target chunk {},{}: valid start={}, references={}", + check.label(), chunkX, chunkZ, validStart, references); + WorldCheckPredicates.qaEvent("structure_start_reference", check.label(), startReferenceOk, + "chunk=" + chunkX + "," + chunkZ + ",validStart=" + validStart + + ",references=" + references); + if (!startReferenceOk || !validStart) { + LOGGER.error("[worldcheck] {} located at chunk {},{} but no resolvable valid start was generated", + check.label(), chunkX, chunkZ); + WorldCheckPredicates.emitSkipped(check, "start_reference", "structure_footprint", "structure_material", + "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve( + generator.commandEngine(), foundKey.toString(), + NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + Integer appliedShift = generator.worldCheckStructureShift(foundKey.toString(), start.getChunkPos()); + BoundingBox shiftedBounds = start.getBoundingBox(); + boolean verticalShiftOk = WorldCheckPredicates.verticalShiftMatches( + decision.yShift(), appliedShift, shiftedBounds.minY(), shiftedBounds.maxY(), + level.getMinY(), level.getMaxY()); + WorldCheckPredicates.qaEvent("structure_vertical_shift", check.label(), verticalShiftOk, + "configured=" + decision.yShift() + ",applied=" + + (appliedShift == null ? "unrecorded" : appliedShift)); + if (!verticalShiftOk) { + LOGGER.error("[worldcheck] {} expected vertical shift {} but generation recorded {}", + check.label(), decision.yShift(), appliedShift); + } + + FootprintAudit footprint = auditFootprint(level, structure, start, check, foundKey); + boolean footprintOk = footprint.inspectedChunks() > 0 + && footprint.evidenceChunks() == footprint.inspectedChunks() + && footprint.coveredPieces() == footprint.totalPieces(); + LOGGER.info("[worldcheck] {} footprint: chunks={}/{} evidence={} pieces={}/{}", + check.label(), footprint.inspectedChunks(), footprint.availableChunks(), + footprint.evidenceChunks(), footprint.coveredPieces(), footprint.totalPieces()); + WorldCheckPredicates.qaEvent("structure_footprint", check.label(), footprintOk, + "inspected=" + footprint.inspectedChunks() + ",available=" + footprint.availableChunks() + + ",evidence=" + footprint.evidenceChunks() + ",coveredPieces=" + + footprint.coveredPieces() + ",totalPieces=" + footprint.totalPieces()); + + boolean materialOk = WorldCheckPredicates.hasCharacteristicMaterialEvidence(footprint.characteristicBlocks(), + footprint.characteristicChunks(), footprint.materialScannedChunks()); + LOGGER.info("[worldcheck] {} material: blocks={} chunks={}/{}", + check.label(), footprint.characteristicBlocks(), footprint.characteristicChunks(), + footprint.materialScannedChunks()); + WorldCheckPredicates.qaEvent("structure_material", check.label(), materialOk, + "blocks=" + footprint.characteristicBlocks() + ",chunks=" + footprint.characteristicChunks() + + ",scanned=" + footprint.materialScannedChunks()); + + boolean vegetationOk = true; + if (check.label().equals("mansion")) { + boolean overlap = footprint.vegetationBlocks() > 0; + vegetationOk = WorldCheckPredicates.mansionVegetationPass(footprint.vegetationBlocks()); + LOGGER.info("[worldcheck] mansion vegetation metric: remaining log/leaf blocks={} columns={} overlap={}", + footprint.vegetationBlocks(), footprint.vegetationColumns(), overlap); + WorldCheckPredicates.qaEvent("mansion_vegetation_metric", check.label(), vegetationOk, + "remainingLogsOrLeaves=" + footprint.vegetationBlocks() + ",columns=" + + footprint.vegetationColumns() + ",overlap=" + overlap); + } + + boolean foundationOk = true; + PendingVillagePoi pendingPoi = null; + if (check.label().equals("village")) { + foundationOk = WorldCheckPredicates.villageFoundationPass(footprint.foundationGapColumns()); + LOGGER.info("[worldcheck] village foundation metric: bases={} cobblestone={} columns={} unsupported={}", + footprint.foundationBaseColumns(), footprint.foundationBlocks(), + footprint.foundationColumns(), footprint.foundationGapColumns()); + WorldCheckPredicates.qaEvent("village_foundation_metric", check.label(), foundationOk, + "bases=" + footprint.foundationBaseColumns() + ",cobblestoneBelowBase=" + + footprint.foundationBlocks() + ",columns=" + + footprint.foundationColumns() + ",unsupported=" + footprint.foundationGapColumns()); + pendingPoi = new PendingVillagePoi(level, start); + } + + boolean blockEntityOk = footprint.blockEntityStates() == footprint.blockEntitiesPresent(); + LOGGER.info("[worldcheck] {} block entities: state blocks={}, present={}, missing={}", + check.label(), footprint.blockEntityStates(), footprint.blockEntitiesPresent(), + footprint.blockEntityStates() - footprint.blockEntitiesPresent()); + WorldCheckPredicates.qaEvent("structure_block_entity", check.label(), blockEntityOk, + "states=" + footprint.blockEntityStates() + ",present=" + footprint.blockEntitiesPresent() + + ",missing=" + (footprint.blockEntityStates() - footprint.blockEntitiesPresent())); + if (!footprintOk) { + LOGGER.error("[worldcheck] {} structure footprint is incomplete", check.label()); + } + if (!materialOk) { + LOGGER.error("[worldcheck] {} has no distributed characteristic structure material", check.label()); + } + if (!blockEntityOk) { + LOGGER.error("[worldcheck] {} generated block-entity states without matching block entities", check.label()); + } + if (!vegetationOk) { + LOGGER.error("[worldcheck] mansion vegetation still intersects the generated structure footprint"); + } + if (!foundationOk) { + LOGGER.error("[worldcheck] village has unsupported foundation columns after stilt placement"); + } + boolean pass = verticalShiftOk && footprintOk && materialOk && blockEntityOk + && vegetationOk && foundationOk; + return new StructureCheckResult(pass, pass ? pendingPoi : null); + } + + private static StructureStart resolveStructureStart(ServerLevel level, ChunkAccess targetChunk, + Structure structure) { + StructureStart direct = targetChunk.getStartForStructure(structure); + if (direct != null && direct.isValid()) { + return direct; + } + int checked = 0; + for (long packed : targetChunk.getReferencesForStructure(structure)) { + if (checked++ >= MAX_START_REFERENCE_CHUNKS) { + break; + } + ChunkAccess referencedChunk = level.getChunk(ChunkPos.getX(packed), ChunkPos.getZ(packed)); + StructureStart referenced = referencedChunk.getStartForStructure(structure); + if (referenced != null && referenced.isValid()) { + return referenced; + } + } + return direct; + } + + private static Pair> findGeneratedStructureCandidate( + ServerLevel level, List> structures, BlockPos origin, int maxRadius) { + ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState(); + Set attempted = new LinkedHashSet<>(); + for (Holder structure : structures) { + for (StructurePlacement placement : state.getPlacementsForStructure(structure)) { + if (!(placement instanceof ConcentricRingsStructurePlacement rings)) { + continue; + } + List positions = state.getRingPositionsFor(rings); + if (positions == null) { + continue; + } + List sorted = new ArrayList<>(positions); + sorted.sort(Comparator.comparingLong(position -> distanceSquared(origin, position))); + for (ChunkPos position : sorted) { + Pair> found = inspectStructureCandidate( + level, structures, placement, position, attempted); + if (found != null) { + return found; + } + if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) { + return null; + } + } + } + } + + int originChunkX = origin.getX() >> 4; + int originChunkZ = origin.getZ() >> 4; + for (int radius = 0; radius <= maxRadius; radius++) { + for (Holder structure : structures) { + for (StructurePlacement placement : state.getPlacementsForStructure(structure)) { + if (!(placement instanceof RandomSpreadStructurePlacement randomSpread)) { + continue; + } + for (int x = -radius; x <= radius; x++) { + boolean xEdge = x == -radius || x == radius; + for (int z = -radius; z <= radius; z++) { + if (!xEdge && z != -radius && z != radius) { + continue; + } + int sectorX = originChunkX + randomSpread.spacing() * x; + int sectorZ = originChunkZ + randomSpread.spacing() * z; + ChunkPos candidate = randomSpread.getPotentialStructureChunk( + state.getLevelSeed(), sectorX, sectorZ); + if (!placement.isStructureChunk(state, candidate.x(), candidate.z())) { + continue; + } + Pair> found = inspectStructureCandidate( + level, structures, placement, candidate, attempted); + if (found != null) { + return found; + } + if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) { + return null; + } + } + } + } + } + } + return null; + } + + private static Pair> inspectStructureCandidate( + ServerLevel level, List> structures, StructurePlacement placement, + ChunkPos candidate, Set attempted) { + if (!attempted.add(candidate.pack())) { + return null; + } + ChunkAccess chunk = level.getChunk(candidate.x(), candidate.z()); + for (Holder structure : structures) { + StructureStart start = resolveStructureStart(level, chunk, structure.value()); + if (start == null || !start.isValid()) { + continue; + } + BlockPos locate = placement.getLocatePos(start.getChunkPos()); + BlockPos resolved = new BlockPos(locate.getX(), start.getBoundingBox().minY(), locate.getZ()); + return Pair.of(resolved, structure); + } + return null; + } + + private static long distanceSquared(BlockPos origin, ChunkPos position) { + long x = (long) position.getMinBlockX() - origin.getX(); + long z = (long) position.getMinBlockZ() - origin.getZ(); + return x * x + z * z; + } + + private static FootprintAudit auditFootprint(ServerLevel level, Structure structure, StructureStart start, + StructureCheck check, Identifier structureKey) { + List pieces = start.getPieces(); + BoundingBox bounds = start.getBoundingBox(); + int availableChunks = footprintChunkCount(bounds); + List selected = selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS); + int evidenceChunks = 0; + int blockEntityStates = 0; + int blockEntitiesPresent = 0; + int materialScannedChunks = 0; + int characteristicBlocks = 0; + int characteristicChunks = 0; + int vegetationBlocks = 0; + int vegetationColumns = 0; + int foundationBaseColumns = 0; + int foundationBlocks = 0; + int foundationColumns = 0; + int foundationGapColumns = 0; + BitSet visited = new BitSet(level.getHeight() << 8); + int[] maximumPieceY = new int[256]; + for (ChunkPos chunkPos : selected) { + ChunkAccess chunk = level.getChunk(chunkPos.x(), chunkPos.z()); + StructureStart localStart = chunk.getStartForStructure(structure); + boolean validStart = localStart != null && localStart.isValid(); + int references = chunk.getReferencesForStructure(structure).size(); + if (WorldCheckPredicates.hasNativeStructureEvidence(validStart, references)) { + evidenceChunks++; + } + BlockEntityAudit blockEntities = auditBlockEntities(level, chunk); + blockEntityStates += blockEntities.states(); + blockEntitiesPresent += blockEntities.present(); + StructureMaterialAudit material = auditStructureMaterial(level, chunk, start, check, + structureKey, visited, maximumPieceY); + if (material.scanned()) { + materialScannedChunks++; + } + characteristicBlocks += material.characteristicBlocks(); + if (material.characteristicBlocks() > 0) { + characteristicChunks++; + } + vegetationBlocks += material.vegetationBlocks(); + vegetationColumns += material.vegetationColumns(); + foundationBaseColumns += material.foundationBaseColumns(); + foundationBlocks += material.foundationBlocks(); + foundationColumns += material.foundationColumns(); + foundationGapColumns += material.foundationGapColumns(); + } + int coveredPieces = 0; + for (StructurePiece piece : pieces) { + boolean covered = false; + for (ChunkPos chunkPos : selected) { + if (intersectsChunk(piece.getBoundingBox(), chunkPos)) { + covered = true; + break; + } + } + if (covered) { + coveredPieces++; + } + } + return new FootprintAudit(selected.size(), availableChunks, evidenceChunks, + coveredPieces, pieces.size(), blockEntityStates, blockEntitiesPresent, + materialScannedChunks, characteristicBlocks, characteristicChunks, + vegetationBlocks, vegetationColumns, foundationBaseColumns, foundationBlocks, + foundationColumns, foundationGapColumns); + } + + private static StructureMaterialAudit auditStructureMaterial(ServerLevel level, ChunkAccess chunk, + StructureStart start, + StructureCheck check, + Identifier structureKey, + BitSet visited, + int[] maximumPieceY) { + List pieces = start.getPieces(); + visited.clear(); + Arrays.fill(maximumPieceY, Integer.MIN_VALUE); + int characteristicBlocks = 0; + int minimumWorldY = level.getMinY(); + int maximumWorldY = level.getMaxY() - 1; + int minimumChunkX = chunk.getPos().getMinBlockX(); + int maximumChunkX = chunk.getPos().getMaxBlockX(); + int minimumChunkZ = chunk.getPos().getMinBlockZ(); + int maximumChunkZ = chunk.getPos().getMaxBlockZ(); + boolean scanned = false; + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (StructurePiece piece : pieces) { + BoundingBox bounds = piece.getBoundingBox(); + int minimumX = Math.max(minimumChunkX, bounds.minX()); + int maximumX = Math.min(maximumChunkX, bounds.maxX()); + int minimumY = Math.max(minimumWorldY, bounds.minY()); + int maximumY = Math.min(maximumWorldY, bounds.maxY()); + int minimumZ = Math.max(minimumChunkZ, bounds.minZ()); + int maximumZ = Math.min(maximumChunkZ, bounds.maxZ()); + if (minimumX > maximumX || minimumY > maximumY || minimumZ > maximumZ) { + continue; + } + scanned = true; + for (int z = minimumZ; z <= maximumZ; z++) { + int localZ = z - minimumChunkZ; + for (int x = minimumX; x <= maximumX; x++) { + int column = (localZ << 4) | (x - minimumChunkX); + maximumPieceY[column] = Math.max(maximumPieceY[column], maximumY); + } + } + for (int y = minimumY; y <= maximumY; y++) { + int verticalIndex = (y - minimumWorldY) << 8; + for (int z = minimumZ; z <= maximumZ; z++) { + int localZ = z - minimumChunkZ; + for (int x = minimumX; x <= maximumX; x++) { + int column = (localZ << 4) | (x - minimumChunkX); + int index = verticalIndex | column; + if (visited.get(index)) { + continue; + } + visited.set(index); + BlockState state = chunk.getBlockState(position.set(x, y, z)); + Identifier blockKey = BuiltInRegistries.BLOCK.getKey(state.getBlock()); + if (WorldCheckMaterials.isCharacteristicMaterial(check.label(), structureKey, blockKey)) { + characteristicBlocks++; + } + } + } + } + } + + int vegetationBlocks = 0; + int vegetationColumns = 0; + if (check.label().equals("mansion")) { + for (int column = 0; column < maximumPieceY.length; column++) { + int highestPieceY = maximumPieceY[column]; + if (highestPieceY == Integer.MIN_VALUE || highestPieceY >= maximumWorldY) { + continue; + } + boolean vegetationColumn = false; + int x = minimumChunkX + (column & 15); + int z = minimumChunkZ + (column >> 4); + for (int y = highestPieceY + 1; y <= maximumWorldY; y++) { + BlockState state = chunk.getBlockState(position.set(x, y, z)); + boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + if (!WorldCheckPredicates.mansionVegetationAbovePiece(vegetation, y, highestPieceY)) { + continue; + } + vegetationBlocks++; + vegetationColumn = true; + } + if (vegetationColumn) { + vegetationColumns++; + } + } + } + + int foundationBaseColumns = 0; + int foundationBlocks = 0; + int foundationColumns = 0; + int foundationGapColumns = 0; + if (check.label().equals("village")) { + BoundingBox area = new BoundingBox( + minimumChunkX, minimumWorldY, minimumChunkZ, + maximumChunkX, maximumWorldY, maximumChunkZ); + ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator(); + if (!(chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator)) { + throw new IllegalStateException("Iris structure audit requires the Iris chunk generator"); + } + Engine engine = irisGenerator.commandEngine(); + IntBinaryOperator surfaceHeight = (x, z) -> + engine.getHeight(x, z, true) + engine.getMinHeight(); + NativeStructureFoundationBuilder.StiltSupportAudit foundation = + NativeStructureFoundationBuilder.auditStiltSupport( + level, area, start, Blocks.COBBLESTONE.defaultBlockState(), surfaceHeight); + foundationBaseColumns = foundation.baseColumns(); + foundationBlocks = foundation.stiltBlocks(); + foundationColumns = foundation.stiltColumns(); + foundationGapColumns = foundation.unsupportedColumns(); + } + + return new StructureMaterialAudit(scanned, characteristicBlocks, vegetationBlocks, + vegetationColumns, foundationBaseColumns, foundationBlocks, foundationColumns, + foundationGapColumns); + } + + private static List selectFootprintChunks(StructureStart start, int limit) { + LinkedHashSet selected = new LinkedHashSet<>(); + addBounded(selected, start.getChunkPos(), limit); + List pieceAnchors = new ArrayList<>(); + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + pieceAnchors.add(new ChunkPos((bounds.minX() + bounds.maxX()) >> 5, + (bounds.minZ() + bounds.maxZ()) >> 5)); + pieceAnchors.add(new ChunkPos(bounds.minX() >> 4, bounds.minZ() >> 4)); + pieceAnchors.add(new ChunkPos(bounds.maxX() >> 4, bounds.maxZ() >> 4)); + } + pieceAnchors.sort(Comparator.comparingInt((ChunkPos chunkPos) -> + chunkPos.distanceSquared(start.getChunkPos()))); + for (ChunkPos chunkPos : pieceAnchors) { + addBounded(selected, chunkPos, limit); + } + for (ChunkPos chunkPos : boundedFootprintChunks(start.getBoundingBox(), start.getChunkPos(), limit)) { + if (intersectsAnyPiece(start.getPieces(), chunkPos)) { + addBounded(selected, chunkPos, limit); + } + } + return List.copyOf(selected); + } + + static List boundedFootprintChunks(BoundingBox bounds, ChunkPos origin, int limit) { + if (limit <= 0) { + return List.of(); + } + int minChunkX = bounds.minX() >> 4; + int maxChunkX = bounds.maxX() >> 4; + int minChunkZ = bounds.minZ() >> 4; + int maxChunkZ = bounds.maxZ() >> 4; + long width = (long) maxChunkX - minChunkX + 1L; + long depth = (long) maxChunkZ - minChunkZ + 1L; + long total = width * depth; + LinkedHashSet chunks = new LinkedHashSet<>(); + addBounded(chunks, origin, limit); + if (total <= limit) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit); + } + } + return List.copyOf(chunks); + } + addBounded(chunks, new ChunkPos(minChunkX, minChunkZ), limit); + addBounded(chunks, new ChunkPos(maxChunkX, minChunkZ), limit); + addBounded(chunks, new ChunkPos(minChunkX, maxChunkZ), limit); + addBounded(chunks, new ChunkPos(maxChunkX, maxChunkZ), limit); + int samplesPerAxis = Math.max(2, (int) Math.floor(Math.sqrt(limit))); + for (int sampleZ = 0; sampleZ < samplesPerAxis; sampleZ++) { + int chunkZ = sampleCoordinate(minChunkZ, maxChunkZ, sampleZ, samplesPerAxis); + for (int sampleX = 0; sampleX < samplesPerAxis; sampleX++) { + int chunkX = sampleCoordinate(minChunkX, maxChunkX, sampleX, samplesPerAxis); + addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit); + } + } + return List.copyOf(chunks); + } + + private static int footprintChunkCount(BoundingBox bounds) { + long width = (long) (bounds.maxX() >> 4) - (bounds.minX() >> 4) + 1L; + long depth = (long) (bounds.maxZ() >> 4) - (bounds.minZ() >> 4) + 1L; + long total = width * depth; + return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) total; + } + + private static int sampleCoordinate(int minimum, int maximum, int index, int samples) { + if (samples <= 1 || minimum == maximum) { + return minimum; + } + double progress = (double) index / (double) (samples - 1); + return minimum + (int) Math.round((maximum - minimum) * progress); + } + + private static void addBounded(Set chunks, ChunkPos chunkPos, int limit) { + if (chunks.size() < limit) { + chunks.add(chunkPos); + } + } + + private static boolean intersectsAnyPiece(List pieces, ChunkPos chunkPos) { + for (StructurePiece piece : pieces) { + if (intersectsChunk(piece.getBoundingBox(), chunkPos)) { + return true; + } + } + return false; + } + + private static boolean intersectsChunk(BoundingBox bounds, ChunkPos chunkPos) { + return bounds.maxX() >= chunkPos.getMinBlockX() + && bounds.minX() <= chunkPos.getMaxBlockX() + && bounds.maxZ() >= chunkPos.getMinBlockZ() + && bounds.minZ() <= chunkPos.getMaxBlockZ(); + } + + private static BlockEntityAudit auditBlockEntities(ServerLevel level, ChunkAccess chunk) { + int states = 0; + int present = 0; + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + LevelChunkSection[] sections = chunk.getSections(); + for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + LevelChunkSection section = sections[sectionIndex]; + if (!section.maybeHas(BlockState::hasBlockEntity)) { + continue; + } + int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4; + for (int localY = 0; localY < 16; localY++) { + for (int localZ = 0; localZ < 16; localZ++) { + for (int localX = 0; localX < 16; localX++) { + BlockState state = section.getBlockState(localX, localY, localZ); + if (!state.hasBlockEntity()) { + continue; + } + states++; + position.set(chunk.getPos().getBlockX(localX), sectionMinY + localY, + chunk.getPos().getBlockZ(localZ)); + if (level.getBlockEntity(position) != null) { + present++; + } + } + } + } + } + return new BlockEntityAudit(states, present); + } + + static PoiAudit auditStructurePois(ServerLevel level, StructureStart start) { + int inBounds = 0; + int outOfBounds = 0; + PoiManager poiManager = level.getPoiManager(); + for (ChunkPos chunkPos : selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS)) { + List records = poiManager.getInChunk( + holder -> true, chunkPos, PoiManager.Occupancy.ANY).toList(); + for (PoiRecord record : records) { + BlockPos position = record.getPos(); + if (position.getY() < level.getMinY() || position.getY() >= level.getMaxY()) { + outOfBounds++; + continue; + } + if (insideAnyPiece(start.getPieces(), position)) { + inBounds++; + } + } + } + return new PoiAudit(inBounds, outOfBounds); + } + + private static boolean insideAnyPiece(List pieces, BlockPos position) { + for (StructurePiece piece : pieces) { + if (piece.getBoundingBox().isInside(position)) { + return true; + } + } + return false; + } + + record StructureCheck(String label, List registryKeys, int locateRadius) { + } + + record NativeStructureGate(boolean passBeforePoi, int nonVillagePassed, + boolean villagePassBeforePoi, PendingVillagePoi pendingPoi) { + } + + private record StructureCheckResult(boolean pass, PendingVillagePoi pendingPoi) { + } + + record PendingVillagePoi(ServerLevel level, StructureStart start) { + } + + private record FootprintAudit(int inspectedChunks, int availableChunks, int evidenceChunks, + int coveredPieces, int totalPieces, int blockEntityStates, + int blockEntitiesPresent, int materialScannedChunks, + int characteristicBlocks, int characteristicChunks, + int vegetationBlocks, int vegetationColumns, + int foundationBaseColumns, int foundationBlocks, int foundationColumns, + int foundationGapColumns) { + } + + private record BlockEntityAudit(int states, int present) { + } + + private record StructureMaterialAudit(boolean scanned, int characteristicBlocks, + int vegetationBlocks, int vegetationColumns, + int foundationBaseColumns, int foundationBlocks, + int foundationColumns, + int foundationGapColumns) { + } + + record PoiAudit(int inBounds, int outOfBounds) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/IrisModdedAPI.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/IrisModdedAPI.java new file mode 100644 index 000000000..43b4dcf47 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/IrisModdedAPI.java @@ -0,0 +1,233 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.api; + +import art.arcane.iris.core.tools.WorldMaintenance; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineLifecycleTasks; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.command.ModdedPregenJob; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkGenerator; + +/** + * Entry point for mods integrating with Iris on Fabric, Forge and NeoForge. + *

+ * Everything here is static and null-tolerant: a null or non-Iris {@link ServerLevel} produces false, null, or a + * no-op rather than an exception, so a caller never has to pre-check whether a level is generated by Iris. + *

+ * Threading. {@link #isIrisLevel(ServerLevel)}, {@link #isStudioLevel(ServerLevel)} and + * {@link #getEngine(ServerLevel)} read the level's chunk generator reference and are safe from any thread once + * the level is loaded. The mantle accessors are safe off the server thread but touch engine storage - see their + * own notes. {@link #pregenerate(ServerLevel, int)} and {@link #registerProvider(ModdedDataProvider)} mutate + * global state and belong on the server thread, during mod setup or from a command. + *

+ * Stability. This class and the {@code Modded*} types beside it are the intended integration surface. The + * types they expose from {@code art.arcane.iris.engine.*} and {@code art.arcane.iris.core.*} - notably + * {@link Engine} - are internal to Iris and change without a deprecation cycle. Treat {@link Engine} as an opaque + * token to hand back to Iris, and prefer the wrappers here over reaching into it. + * + * @see ModdedDataProvider for supplying custom blocks, items and entities to the generator + */ +public final class IrisModdedAPI { + private IrisModdedAPI() { + } + + /** + * Whether {@code level}'s chunk generator is an Iris generator. False for null and for every vanilla or + * third-party generated level. The cheapest available Iris check. + */ + public static boolean isIrisLevel(ServerLevel level) { + if (level == null) { + return false; + } + return level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator; + } + + /** + * Whether {@code level} is an Iris studio level - a throwaway world opened for pack authoring, which is + * deleted on shutdown. Persist nothing against one. False for null and for non-Iris levels. + */ + public static boolean isStudioLevel(ServerLevel level) { + Engine engine = getEngine(level); + return engine != null && engine.isStudio(); + } + + /** + * The Iris engine driving {@code level}, or null when the level is null, is not Iris-generated, or its engine + * is not currently available - during shutdown, or while the generator is still binding. + *

+ * Never cached: resolve per use. Reloading a pack or unloading the level replaces the engine, and a stale + * reference goes inert. {@link Engine} is internal to Iris; see the stability note on this class. + */ + public static Engine getEngine(ServerLevel level) { + if (level == null) { + return null; + } + ChunkGenerator generator = level.getChunkSource().getGenerator(); + if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) { + return null; + } + try { + return irisGenerator.commandEngine(); + } catch (Throwable error) { + return null; + } + } + + /** + * Starts a cached, asynchronous pregeneration of {@code radiusBlocks} around the world origin. + * Equivalent to {@code pregenerate(level, radiusBlocks, 0, 0, false, true)}. + */ + public static boolean pregenerate(ServerLevel level, int radiusBlocks) { + return pregenerate(level, radiusBlocks, 0, 0, false, true); + } + + /** + * Starts a pregeneration job over a square region. + *

+ * Returns as soon as the job is queued; progress is reported through Iris's own logging and boss bar, not to + * the caller. Only one job runs server-wide, so this returns false if one is already active. Call on the + * server thread. + * + * @param radiusBlocks half-extent of the square in blocks, measured from the centre + * @param sync write chunks synchronously; slower but avoids the async write queue + * @param cached reuse and update the on-disk pregeneration cache so an interrupted job resumes instead of + * regenerating + * @return false when {@code level} is not Iris-generated or another pregeneration job is already running + */ + public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean sync, boolean cached) { + Engine engine = getEngine(level); + if (engine == null) { + return false; + } + return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, sync, cached); + } + + /** + * Reads a mantle value of {@code type} at world coordinates. + *

+ * The mantle is Iris's own per-block sidecar storage, independent of chunk NBT, and it is how Iris carries + * data that must survive between generation stages. Coordinates are world-space: {@code y} is translated by + * the engine's minimum height internally. + *

+ * Returns null when the level is not Iris-generated, no mantle region exists for that column yet - reads + * never create or load one - or nothing of {@code type} is stored there. A {@code y} outside the engine's + * height range reads as null rather than throwing. + * + * A closing or closed engine refuses the operation quietly (null / no-op) instead of racing its shutdown. + */ + public static T getMantleData(ServerLevel level, int x, int y, int z, Class type) { + Engine engine = getEngine(level); + if (engine == null) { + return null; + } + // Lease-gated so the engine shutdown drain covers this public accessor; a mantle + // mid-close refuses the lease instead of racing the region flush. + return EngineLifecycleTasks.call(engine, "modded_api_mantle_get", + () -> engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type), null); + } + + /** + * Writes a mantle value at world coordinates, replacing any previous value of the same type there. + *

+ * Unlike {@link #getMantleData(ServerLevel, int, int, int, Class)}, a write creates the mantle region if it + * does not exist, which can touch disk - do not call it per block in a tick loop from the server thread. A + * null {@code data}, a non-Iris level, or a {@code y} outside the engine's height range is a silent no-op; + * remove values with {@link #deleteMantleData(ServerLevel, int, int, int, Class)}. + *

+ * Values written under a custom type are discarded when Iris trims a mantle region unless the type is + * declared with {@link #retainMantleDataForSlice(Class)}. + * + * A closing or closed engine refuses the operation quietly (null / no-op) instead of racing its shutdown. + */ + public static void setMantleData(ServerLevel level, int x, int y, int z, T data) { + Engine engine = getEngine(level); + if (engine == null || data == null) { + return; + } + EngineLifecycleTasks.run(engine, "modded_api_mantle_set", + () -> engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data)); + } + + /** + * Removes any mantle value of {@code type} at world coordinates. A non-Iris level or an out-of-range + * {@code y} is a silent no-op. Like a write, this creates the mantle region if it is absent. + * + * A closing or closed engine refuses the operation quietly (null / no-op) instead of racing its shutdown. + */ + public static void deleteMantleData(ServerLevel level, int x, int y, int z, Class type) { + Engine engine = getEngine(level); + if (engine == null) { + return; + } + EngineLifecycleTasks.run(engine, "modded_api_mantle_delete", + () -> engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type)); + } + + /** + * Declares that mantle slices of {@code sliceType} must be kept rather than discarded. + *

+ * Iris drops slices it does not need once a region's generation data has served its purpose - both the + * normal per-chunk trim and pregeneration's forced cleanup honor this registry. Any type a mod writes + * with {@link #setMantleData(ServerLevel, int, int, int, Object)} and expects to read back later must be + * declared here first. Registration is by canonical class name, process-wide across every Iris world, and + * cannot be undone - declare it once during mod setup. A null {@code sliceType} is ignored, and the + * block-state slice can never be retained. + *

+ * Retained data lives for the world's lifetime: it persists into the mantle region files and unloads + * with them, so region files grow with everything you retain. The mod owns the cleanup - call + * {@code deleteMantleData} when a value is no longer needed. + */ + public static void retainMantleDataForSlice(Class sliceType) { + if (sliceType == null) { + return; + } + WorldMaintenance.retainMantleDataForSlice(sliceType.getCanonicalName()); + } + + /** + * Registers a custom content provider imperatively, for mods that would rather call Iris than ship a + * {@link java.util.ServiceLoader} entry. + *

+ * Providers are keyed by {@link ModdedDataProvider#modId()}; a second registration under an id already present + * is logged and ignored. {@link ModdedDataProvider#init()} runs during this call, and a throwable it raises is + * logged rather than propagated. A null {@code provider} is ignored. + *

+ * Ordering matters: Iris only consults providers registered before a pack resolves the block in question, so + * register during mod setup. Registering after Iris's own {@link java.util.ServiceLoader} discovery is + * supported; registering after a world has generated is not - blocks already resolved are not revisited. + */ + public static void registerProvider(ModdedDataProvider provider) { + ModdedCustomContentRegistry.register(provider); + } + + /** + * Maps a custom {@code namespace:key} onto a fixed vanilla block state, for mods that only need a static alias + * and no provider class. + *

+ * {@code state} is a block state string in the same syntax packs use, for example + * {@code minecraft:oak_log[axis=y]}, and is parsed immediately: an unparseable state or an invalid identifier + * is logged and the registration is dropped, so a typo shows up at startup rather than as missing blocks. + * Aliases take precedence over provider lookups for the same key. Null arguments are ignored. + */ + public static void registerCustomBlockData(String namespace, String key, String state) { + ModdedCustomContentRegistry.registerCustomBlockData(namespace, key, state); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockData.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockData.java new file mode 100644 index 000000000..f0cc954ae --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockData.java @@ -0,0 +1,61 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.api; + +import net.minecraft.world.level.block.state.BlockState; + +import java.util.Objects; + +/** + * A provider's answer to a block lookup: the state to write, and whether the provider wants a second pass once the + * chunk is loaded. + *

+ * Immutable. Returned from {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)}; + * construct with {@link #direct(BlockState)} or {@link #deferred(BlockState)} rather than the canonical constructor. + * + * @param state the block state Iris writes. Never null + * @param deferredPlacement whether {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} + * should run for this position after the chunk is loaded + */ +public record ModdedBlockData(BlockState state, boolean deferredPlacement) { + /** + * @throws NullPointerException if {@code state} is null + */ + public ModdedBlockData { + Objects.requireNonNull(state); + } + + /** + * The state is final - Iris writes it during generation and does nothing further. + */ + public static ModdedBlockData direct(BlockState state) { + return new ModdedBlockData(state, false); + } + + /** + * {@code state} is a placeholder written during generation; the provider finishes the job in + * {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} once the chunk is loaded. Use + * when the real block needs a level - a block entity, neighbour state, or mod registries not available on a + * generation thread. Pick a placeholder with the same shape and occlusion as the final block so terrain around + * it generates correctly. + */ + public static ModdedBlockData deferred(BlockState state) { + return new ModdedBlockData(state, true); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockPlacementContext.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockPlacementContext.java new file mode 100644 index 000000000..dc5110b31 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockPlacementContext.java @@ -0,0 +1,64 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.api; + +import art.arcane.iris.engine.framework.Engine; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.Map; +import java.util.Objects; + +/** + * Everything a provider needs to finish a deferred block placement, handed to + * {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} on the server thread. + *

+ * Immutable, and constructed by Iris rather than by mods. {@code state} is defensively copied; {@code position} is + * already immutable. Because delivery is on the server thread with the chunk loaded, it is safe to write blocks, + * attach block entities and read neighbours from here. + * + * @param engine the Iris engine for this level. Internal Iris type - treat it as an opaque token + * @param level the level to write into. Never null + * @param position the block the placeholder was written at. Never null + * @param blockId the identifier the pack named, without state properties. Never null + * @param blockState the state currently at {@code position} - normally the placeholder returned as deferred, though + * another provider or a later generation stage may have replaced it. Never null + * @param state the {@code [prop=value]} pairs from the pack's key, possibly empty. Never null; unmodifiable + */ +public record ModdedBlockPlacementContext( + Engine engine, + ServerLevel level, + BlockPos position, + Identifier blockId, + Map state, + BlockState blockState) { + /** + * @throws NullPointerException if any component is null + */ + public ModdedBlockPlacementContext { + Objects.requireNonNull(engine); + Objects.requireNonNull(level); + Objects.requireNonNull(position); + Objects.requireNonNull(blockId); + state = Map.copyOf(state); + Objects.requireNonNull(blockState); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedCustomContentRegistry.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedCustomContentRegistry.java new file mode 100644 index 000000000..61e8a2890 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedCustomContentRegistry.java @@ -0,0 +1,451 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.api; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.modded.ModdedBlockResolution; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.state.BlockState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Registry of {@link ModdedDataProvider} instances and static block-data aliases, and the resolution path Iris + * itself calls into. + *

+ * Mods should go through {@link IrisModdedAPI#registerProvider(ModdedDataProvider)} and + * {@link IrisModdedAPI#registerCustomBlockData(String, String, String)} rather than calling this class directly; + * the resolution methods here are Iris internals and are public only because the adapter's generation code lives in + * another package. + *

+ * Threading. Mutation ({@link #register(ModdedDataProvider)}, + * {@link #registerCustomBlockData(String, String, String)}, {@link #discover()}) is serialized on the class + * monitor. Resolution ({@link #resolveBlock(String)}, {@link #spawnMob}, {@link #processBlockPlacement}) is lock + * free over a copy-on-write provider list and a concurrent alias map, so it runs on generation threads. Every + * resolution method catches provider throwables, logs them against the provider's mod id, and continues with the + * next provider. + */ +public final class ModdedCustomContentRegistry { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final List PROVIDERS = new CopyOnWriteArrayList<>(); + private static final Map CUSTOM_BLOCKS = new ConcurrentHashMap<>(); + private static volatile boolean scanned = false; + private static DiscoveryBatch discoveryBatch; + + private ModdedCustomContentRegistry() { + } + + /** + * Registers a static {@code namespace:key} to block-state alias. Invalid identifiers and unparseable states are + * logged and dropped; null arguments are ignored. See + * {@link IrisModdedAPI#registerCustomBlockData(String, String, String)}. + */ + public static synchronized void registerCustomBlockData(String namespace, String key, String state) { + if (namespace == null || key == null || state == null) { + return; + } + Identifier identifier = Identifier.tryParse(namespace + ":" + key); + if (identifier == null) { + LOGGER.warn("Iris custom block data registration rejected invalid id {}:{}", namespace, key); + return; + } + BlockState parsed; + try { + parsed = ModdedBlockResolution.strictParse(state).handle(); + } catch (Throwable error) { + LOGGER.error("Iris custom block data '{}:{}' has unparseable state '{}'", namespace, key, state, error); + return; + } + DiscoveryBatch activeBatch = discoveryBatch; + if (activeBatch == null) { + CUSTOM_BLOCKS.put(identifier.toString(), parsed); + } else { + activeBatch.customBlocks.put(identifier.toString(), parsed); + } + LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state); + } + + /** + * Registers a provider, rejecting a duplicate {@link ModdedDataProvider#modId()} with a warning and ignoring + * null. {@link ModdedDataProvider#init()} runs here; a throwable it raises is logged, not propagated. See + * {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}. + */ + public static synchronized void register(ModdedDataProvider provider) { + if (provider == null) { + return; + } + DiscoveryBatch activeBatch = discoveryBatch; + if (activeBatch != null) { + activeBatch.add(provider); + return; + } + for (ModdedDataProvider existing : PROVIDERS) { + if (existing.modId().equals(provider.modId())) { + LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", provider.modId()); + return; + } + } + PROVIDERS.add(provider); + try { + provider.init(); + } catch (Throwable error) { + LOGGER.error("Iris custom content provider '{}' failed to initialize", provider.modId(), error); + } + LOGGER.info("Iris registered custom content provider '{}'", provider.modId()); + } + + /** + * Runs {@link ServiceLoader} discovery for {@link ModdedDataProvider} against Iris's own class loader, once per + * process. Called during Iris mod initialization; a second call is a no-op that returns an inert handle. + *

+ * All-or-nothing: a provider whose {@link ModdedDataProvider#init()} throws aborts the pass, restores the + * previous provider and alias state, logs the failing provider's identity, and rethrows. + * + * @return a handle whose {@link Discovery#rollback()} undoes this pass, used by the bootstrap's rollback chain + */ + public static synchronized Discovery discover() { + if (scanned) { + return Discovery.unchanged(); + } + return discover(ServiceLoader.load( + ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader())); + } + + static synchronized Discovery discover(Iterable discoveredProviders) { + List previousProviders = List.copyOf(PROVIDERS); + Map previousCustomBlocks = new LinkedHashMap<>(CUSTOM_BLOCKS); + boolean previousDiscoveryComplete = scanned; + DiscoveryBatch batch = new DiscoveryBatch(previousProviders, previousCustomBlocks); + discoveryBatch = batch; + ModdedDataProvider failingProvider = null; + try { + for (ModdedDataProvider provider : discoveredProviders) { + failingProvider = provider; + batch.add(provider); + failingProvider = null; + } + PROVIDERS.addAll(batch.additions); + CUSTOM_BLOCKS.putAll(batch.customBlocks); + scanned = true; + for (ModdedDataProvider provider : batch.additions) { + LOGGER.info("Iris registered custom content provider '{}'", provider.modId()); + } + return new Discovery(previousProviders, previousCustomBlocks, + previousDiscoveryComplete, true); + } catch (Throwable failure) { + try { + restore(previousProviders, previousCustomBlocks, previousDiscoveryComplete); + } catch (Throwable rollbackFailure) { + if (rollbackFailure != failure) { + failure.addSuppressed(rollbackFailure); + } + } + LOGGER.warn("Iris custom content provider discovery failed at {}", + providerIdentity(failingProvider), failure); + if (failure instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (failure instanceof Error fatalError) { + throw fatalError; + } + throw new IllegalStateException("Iris custom content provider discovery failed", failure); + } finally { + discoveryBatch = null; + } + } + + private static String providerIdentity(ModdedDataProvider provider) { + if (provider == null) { + return "the provider service loader"; + } + String className = provider.getClass().getName(); + try { + String modId = provider.modId(); + return modId == null || modId.isBlank() ? className : "provider '" + modId + "' (" + className + ")"; + } catch (Throwable identityFailure) { + return className; + } + } + + static synchronized boolean hasProvider(String modId) { + for (ModdedDataProvider provider : PROVIDERS) { + if (Objects.equals(provider.modId(), modId)) { + return true; + } + } + return false; + } + + static boolean discoveryComplete() { + return scanned; + } + + /** + * Whether any provider or alias is registered. Iris checks this to skip custom resolution entirely on a server + * with no integrating mods. + */ + public static boolean hasProviders() { + return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty(); + } + + /** + * Every statically registered {@code namespace:key} block alias. Read-only snapshot for pack tooling + * (schema completion, key validation); not on the resolution path. + */ + public static List aliasBlockKeys() { + return List.copyOf(CUSTOM_BLOCKS.keySet()); + } + + /** + * Every key claimed by a registered provider for {@code type}, in registration order, deduplicated. Read-only + * snapshot for pack tooling; a provider that throws is logged against its mod id and skipped. + */ + public static List providerKeys(ModdedDataType type) { + if (type == null || PROVIDERS.isEmpty()) { + return List.of(); + } + List keys = new ArrayList<>(); + Set seen = new HashSet<>(); + for (ModdedDataProvider provider : PROVIDERS) { + Collection types; + try { + types = provider.getTypes(type); + } catch (Throwable error) { + LOGGER.error("Iris custom content provider '{}' failed listing {} types", provider.modId(), type, error); + continue; + } + if (types == null) { + continue; + } + for (Identifier identifier : types) { + if (identifier != null && seen.add(identifier.toString())) { + keys.add(identifier.toString()); + } + } + } + return List.copyOf(keys); + } + + /** + * Resolves a pack block key against aliases first, then each ready provider that claims it, in registration + * order. {@code key} may carry {@code [prop=value]} properties, which are parsed and passed along. Returns null + * when nothing claims it, which lets the caller fall back to air. Called from generation threads. + */ + public static ModdedBlockData resolveBlock(String key) { + if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) { + return null; + } + Identifier base = parseIdentifier(key); + if (base == null) { + return null; + } + BlockState custom = CUSTOM_BLOCKS.get(base.toString()); + if (custom != null) { + return ModdedBlockData.direct(custom); + } + if (PROVIDERS.isEmpty()) { + return null; + } + Map state = parseState(key); + for (ModdedDataProvider provider : PROVIDERS) { + if (!provider.isReady() || !provider.isValidProvider(base, ModdedDataType.BLOCK)) { + continue; + } + try { + ModdedBlockData resolved = provider.getBlockData(base, state); + if (resolved != null) { + return resolved; + } + } catch (Throwable error) { + LOGGER.error("Iris custom content provider '{}' failed resolving block {}", provider.modId(), key, error); + } + } + return null; + } + + /** + * Delivers a deferred placement to the first ready provider claiming {@code key}; later providers are not + * consulted for that position. An unparseable key or no matching provider is logged and skipped. Called on the + * server thread with the chunk loaded. + */ + public static void processBlockPlacement(Engine engine, ServerLevel level, BlockPos position, String key) { + Identifier base = parseIdentifier(key); + if (base == null) { + LOGGER.warn("Iris deferred custom block placement rejected invalid id {}", key); + return; + } + Map state = parseState(key); + for (ModdedDataProvider provider : PROVIDERS) { + if (!provider.isReady() || !provider.isValidProvider(base, ModdedDataType.BLOCK)) { + continue; + } + try { + provider.processBlockPlacement(new ModdedBlockPlacementContext( + engine, level, position.immutable(), base, state, level.getBlockState(position))); + } catch (Throwable error) { + LOGGER.error("Iris custom content provider '{}' failed post-placement for {} at {}", provider.modId(), key, position, error); + } + return; + } + LOGGER.warn("Iris deferred custom block placement has no provider for {}", key); + } + + /** + * Asks each ready provider claiming {@code key} to spawn a custom entity, returning the first non-null result. + * Null when no provider claims it or every attempt declined. Called on the server thread. + */ + public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) { + if (PROVIDERS.isEmpty() || level == null || key == null) { + return null; + } + Identifier base = parseIdentifier(key); + if (base == null) { + return null; + } + for (ModdedDataProvider provider : PROVIDERS) { + if (!provider.isReady() || !provider.isValidProvider(base, ModdedDataType.ENTITY)) { + continue; + } + try { + Entity entity = provider.spawnMob(level, x, y, z, base); + if (entity != null) { + return entity; + } + } catch (Throwable error) { + LOGGER.error("Iris custom content provider '{}' failed spawning mob {}", provider.modId(), key, error); + } + } + return null; + } + + private static Identifier parseIdentifier(String key) { + String trimmed = key.trim(); + int bracket = trimmed.indexOf('['); + String head = bracket < 0 ? trimmed : trimmed.substring(0, bracket); + return Identifier.tryParse(head); + } + + private static Map parseState(String key) { + Map state = new LinkedHashMap<>(); + int open = key.indexOf('['); + int close = key.indexOf(']'); + if (open < 0 || close < open) { + return state; + } + String body = key.substring(open + 1, close); + if (body.isEmpty()) { + return state; + } + for (String pair : body.split(",")) { + int eq = pair.indexOf('='); + if (eq <= 0) { + continue; + } + state.put(pair.substring(0, eq).trim(), pair.substring(eq + 1).trim()); + } + return state; + } + + private static synchronized void restore(List providers, + Map customBlocks, + boolean discoveryComplete) { + PROVIDERS.clear(); + PROVIDERS.addAll(providers); + CUSTOM_BLOCKS.clear(); + CUSTOM_BLOCKS.putAll(customBlocks); + scanned = discoveryComplete; + } + + /** + * Undo handle for one {@link #discover()} pass, so a failure later in Iris's bootstrap can restore the registry + * to its pre-discovery state. + */ + public static final class Discovery { + private final List providers; + private final Map customBlocks; + private final boolean discoveryComplete; + private boolean active; + + private Discovery(List providers, Map customBlocks, + boolean discoveryComplete, boolean active) { + this.providers = providers; + this.customBlocks = customBlocks; + this.discoveryComplete = discoveryComplete; + this.active = active; + } + + private static Discovery unchanged() { + return new Discovery(List.of(), Map.of(), true, false); + } + + /** + * Restores the providers and aliases captured before the pass. Idempotent; a no-op on a handle from a + * discovery that did not run. + */ + public synchronized void rollback() { + if (!active) { + return; + } + restore(providers, customBlocks, discoveryComplete); + active = false; + } + } + + private static final class DiscoveryBatch { + private final List providers; + private final List additions = new ArrayList<>(); + private final Map customBlocks; + + private DiscoveryBatch(List providers, Map customBlocks) { + this.providers = new ArrayList<>(providers); + this.customBlocks = new LinkedHashMap<>(customBlocks); + } + + private void add(ModdedDataProvider provider) { + ModdedDataProvider candidate = Objects.requireNonNull(provider, + "Iris discovered a null custom content provider"); + String modId = Objects.requireNonNull(candidate.modId(), + "Iris custom content provider returned a null mod id"); + for (ModdedDataProvider existing : providers) { + if (modId.equals(existing.modId())) { + LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", modId); + return; + } + } + providers.add(candidate); + additions.add(candidate); + candidate.init(); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataProvider.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataProvider.java new file mode 100644 index 000000000..da6a147df --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataProvider.java @@ -0,0 +1,115 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.api; + +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; + +import java.util.Collection; +import java.util.Map; + +/** + * Extension point letting a mod resolve its own blocks, items and entities for Iris packs, so a pack can name + * {@code yourmod:something} and have it placed. + *

+ * Discovered through {@link java.util.ServiceLoader} at Iris mod initialization, or registered imperatively with + * {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}. For ServiceLoader discovery, ship + * {@code META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider} listing the implementation's binary + * name; the class needs a public no-argument constructor. + *

+ * Threading. Implementations must be thread-safe. + * {@link #getBlockData(Identifier, Map)} is called from generation threads, potentially many at once, for every + * unresolved key a pack names - it must be fast and must not touch world state. + * {@link #processBlockPlacement(ModdedBlockPlacementContext)} and + * {@link #spawnMob(ServerLevel, double, double, double, Identifier)} are called on the server thread, where + * touching the level is safe. + *

+ * Failure handling. Iris catches throwables from every callback except {@link #init()} during + * ServiceLoader discovery, logs them against {@link #modId()}, and carries on with the remaining providers - one + * broken provider does not stop world generation. A throwable from {@link #init()} during discovery aborts + * discovery and rolls back every provider registered in that pass. + */ +public interface ModdedDataProvider { + /** + * The owning mod's id. Used as the provider's identity: duplicates are rejected, and it labels every log line + * Iris emits about this provider. Must be non-null and stable; returning null aborts discovery. + */ + String modId(); + + /** + * Whether this provider can answer lookups yet. Iris skips a provider that reports false rather than treating + * it as absent, so a provider whose registries populate late can gate itself instead of returning wrong + * answers. Defaults to true. + */ + default boolean isReady() { + return true; + } + + /** + * Every identifier this provider can supply for {@code type}. Used for command suggestion and pack tooling, not + * on the resolution path - {@link #isValidProvider(Identifier, ModdedDataType)} decides that. Return an empty + * collection rather than null. + */ + Collection getTypes(ModdedDataType type); + + /** + * Whether this provider claims {@code id} for {@code type}. Called before every resolution callback, on + * generation threads, so keep it to a set lookup. A cheap namespace check is usually enough. + */ + boolean isValidProvider(Identifier id, ModdedDataType type); + + /** + * Resolves a claimed block identifier into a concrete block state. + *

+ * {@code state} holds the {@code [prop=value]} pairs from the pack's key, already parsed and possibly empty; + * never null. Return null to decline, in which case Iris tries the next provider and finally falls back to air. + * Return {@link ModdedBlockData#deferred(net.minecraft.world.level.block.state.BlockState)} when the real block + * needs a loaded level - Iris then writes the placeholder state and calls + * {@link #processBlockPlacement(ModdedBlockPlacementContext)} later. Called from generation threads. + */ + default ModdedBlockData getBlockData(Identifier blockId, Map state) { + return null; + } + + /** + * Finishes a deferred placement once the chunk is loaded: swap in the real block, attach a block entity, seed + * NBT. + *

+ * Called on the server thread, once per deferred position, by the first provider that claims the identifier - + * later providers are not consulted for that position. Only fires for states returned as deferred. + */ + default void processBlockPlacement(ModdedBlockPlacementContext context) { + } + + /** + * Spawns a claimed custom entity at the given position. Return null to decline and let the next provider try. + * Called on the server thread from Iris's entity spawning. + */ + default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) { + return null; + } + + /** + * One-time setup, called by Iris immediately after this provider is accepted. A throwable raised here aborts + * ServiceLoader discovery; when registered imperatively it is logged and the provider stays registered. + */ + default void init() { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataType.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataType.java new file mode 100644 index 000000000..7a0677fb3 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataType.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.api; + +/** + * The kinds of custom content a {@link ModdedDataProvider} can claim. + *

+ * Constants may be added. Switch expressions over this enum need a {@code default} arm. + */ +public enum ModdedDataType { + /** Block states, resolved through {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)}. */ + BLOCK, + /** Item types, claimed for loot and pack tooling. */ + ITEM, + /** Entity types, spawned through {@link ModdedDataProvider#spawnMob(net.minecraft.server.level.ServerLevel, double, double, double, net.minecraft.resources.Identifier)}. */ + ENTITY +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java new file mode 100644 index 000000000..17cb8ded6 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java @@ -0,0 +1,546 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.IrisMessages; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.core.localization.PackDownloadMessages; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import art.arcane.iris.core.pack.PackDownloadExecution; +import art.arcane.iris.core.pack.PackDownloader; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedDimensionManager; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedForcedDatapack; +import art.arcane.iris.modded.ModdedLoader; +import art.arcane.iris.modded.ModdedScheduler; +import art.arcane.iris.modded.ModdedServerLevels; +import art.arcane.iris.modded.ModdedWorldgenIds; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KMap; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import art.arcane.volmlib.util.localization.MessageArgument; + +public final class IrisModdedCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L; + private static final Object DOWNLOAD_MONITOR = new Object(); + + static final SuggestionProvider PACK_NAMES = ModdedCommandSuggestions.PACK_NAMES; + private static PackDownloadExecution activeDownload; + private static boolean downloadAdmissionOpen; + + private IrisModdedCommands() { + } + + public static void register(CommandDispatcher dispatcher) { + LiteralCommandNode root = dispatcher.register(ModdedCommandTree.rootTree()); + dispatcher.register(Commands.literal("ir").redirect(root)); + dispatcher.register(Commands.literal("irs").redirect(root)); + IrisLogging.info("Iris /iris command tree registered"); + } + + public static void openDownloadAdmission() { + synchronized (DOWNLOAD_MONITOR) { + downloadAdmissionOpen = true; + } + } + + public static void shutdownDownloads() { + PackDownloadExecution execution; + synchronized (DOWNLOAD_MONITOR) { + downloadAdmissionOpen = false; + execution = activeDownload; + } + if (execution == null) { + return; + } + + execution.cancel(); + boolean interrupted = false; + boolean warned = false; + while (!execution.isComplete()) { + try { + if (!execution.await(DOWNLOAD_SHUTDOWN_POLL_SECONDS, TimeUnit.SECONDS) && !warned) { + warned = true; + LOGGER.warn(execution.isPublishing() + ? "Waiting for atomic pack publication to finish before Iris shutdown." + : "Waiting for the active pack download to cancel before Iris shutdown."); + } + } catch (InterruptedException exception) { + interrupted = true; + execution.cancel(); + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + static int tp(CommandSourceStack source, ServerLevel level, ServerPlayer target) { + ServerPlayer player = target != null ? target : source.getPlayer(); + if (player == null) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_PLAYER_IRIS_TP_DIMENSION_PLAYER)); + return 0; + } + if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()))); + return 0; + } + String dimensionId = level.dimension().identifier().toString(); + if (!ModdedDimensionManager.teleport(player, source.getServer(), dimensionId, 8.5D, Double.MIN_VALUE, 8.5D)) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED, MessageArgument.untrusted("dimensionId", dimensionId))); + return 0; + } + ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTING, MessageArgument.untrusted("value", player.getScoreboardName()), MessageArgument.untrusted("dimensionId", dimensionId))); + return 1; + } + + static int evacuate(CommandSourceStack source, ServerLevel target) { + MinecraftServer server = source.getServer(); + ServerLevel level = target != null ? target : source.getLevel(); + if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_2, MessageArgument.untrusted("value", level.dimension().identifier()))); + return 0; + } + ServerLevel fallback = server.overworld(); + if (fallback == level) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_EVACUATE_PRIMARY_WORLD_THERE_IS_NOWHERE_SEND_PLAYERS)); + return 0; + } + int count = ModdedDimensionManager.evacuate(server, level); + ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_EVACUATED_PLAYER_S_FROM, MessageArgument.untrusted("count", count), MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", fallback.dimension().identifier()))); + return 1; + } + + static int debug(CommandSourceStack source) { + boolean to = !IrisSettings.get().getGeneral().isDebug(); + IrisSettings.get().getGeneral().setDebug(to); + IrisSettings.get().forceSave(); + ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SET_DEBUG, MessageArgument.untrusted("to", to))); + return 1; + } + + static int reload(CommandSourceStack source) { + if (IrisSettings.settings != null) { + IrisSettings.invalidate(); + } + IrisSettings.get(); + // Forced-datapack regeneration trigger. Async: staging revalidates every pack and must never run on + // the server thread. + ModdedForcedDatapack.scheduleRegeneration("/iris reload"); + boolean localeLoaded = IrisLanguage.reload(); + if (localeLoaded) { + ok(source, IrisLanguage.plain( + IrisMessages.COMMAND_RELOAD_SUCCESS, + MessageArgument.trusted("locale", IrisLanguage.activeLocale()) + )); + return 1; + } + fail(source, IrisLanguage.plain( + IrisMessages.COMMAND_RELOAD_FAILED, + MessageArgument.untrusted("locale", IrisSettings.get().getGeneral().getLanguage()), + MessageArgument.trusted("activeLocale", IrisLanguage.activeLocale()) + )); + return 0; + } + + static int height(CommandSourceStack source) { + ServerLevel level = source.getLevel(); + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WORLD_HEIGHT_RANGE, + MessageArgument.trusted("minY", level.getMinY()), + MessageArgument.trusted("maxY", level.getMaxY()))); + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WORLD_HEIGHT_TOTAL, + MessageArgument.trusted("height", level.getHeight()))); + return 1; + } + + static int regen(CommandSourceStack source, int radius) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS)); + return 0; + } + ServerLevel level = source.getLevel(); + if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator irisGenerator)) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_4)); + return 0; + } + Engine engine = engineFor(level); + if (engine == null) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_5)); + return 0; + } + ModdedRegen.start(source, level, irisGenerator, engine, player, radius); + return 1; + } + + static int version(CommandSourceStack source) { + ModdedLoader loader = ModdedEngineBootstrap.loader(); + int engines = engineCount(source.getServer()); + ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IRIS_BY_VOLMIT_SOFTWARE_ON_MINECRAFT_IRIS_DIMENSION_S, MessageArgument.untrusted("value", loader.modVersion()), MessageArgument.untrusted("value2", loader.platformName()), MessageArgument.untrusted("value3", loader.minecraftVersion()), MessageArgument.untrusted("engines", engines))); + return 1; + } + + static int info(CommandSourceStack source, String filter) { + MinecraftServer server = source.getServer(); + // The seed is the one field in this listing that is not free to hand a plain player, and /iris worlds + // routes here too: emit it only for sources that pass the same gate /iris seed requires. + boolean showSeed = ModdedCommandTree.isGamemaster(source); + List lines = new ArrayList<>(); + int total = 0; + int iris = 0; + for (ServerLevel level : ModdedServerLevels.levels(server)) { + total++; + ChunkGenerator generator = level.getChunkSource().getGenerator(); + if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) { + continue; + } + iris++; + String dimensionId = level.dimension().identifier().toString(); + String irisIdentity = ModdedWorldgenIds.generatorIdentity(irisGenerator.dimensionKey()); + if (filter != null && !dimensionId.contains(filter) + && !irisIdentity.contains(filter) + && !irisGenerator.dimensionKey().contains(filter)) { + continue; + } + Engine engine = irisGenerator.engineIfBound(); + if (engine == null) { + lines.add(irisIdentity + ": pack=" + irisGenerator.dimensionKey() + + " world=" + dimensionId + " (engine not started yet)"); + continue; + } + String featureStatus = irisGenerator.importedFeaturesStatus(); + lines.add(irisIdentity + ": pack=" + engine.getDimension().getLoadKey() + + " world=" + dimensionId + + (showSeed ? " seed=" + level.getSeed() : "") + + " height=" + engine.getMinHeight() + ".." + engine.getMaxHeight() + + " generated=" + engine.getGenerated() + + (featureStatus == null ? "" : " importedFeatures=" + featureStatus) + + " data=" + engine.getData().getDataFolder().getName()); + } + ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_LOADED_DIMENSIONS_IRIS, MessageArgument.untrusted("total", total), MessageArgument.untrusted("iris", iris))); + if (lines.isEmpty()) { + if (filter == null) { + ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_NO_IRIS_DIMENSIONS_ARE_LOADED)); + } else { + ok(source, IrisLanguage.plain(RuntimeUiMessages.MODDED_NO_DIMENSION_MATCH, MessageArgument.untrusted("filter", filter))); + } + return 0; + } + for (String line : lines) { + ok(source, line); + } + return 1; + } + + static int seed(CommandSourceStack source) { + ServerLevel level = source.getLevel(); + Engine engine = engineFor(level); + if (engine == null) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_14)); + return 0; + } + ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_WORLD_SEED, MessageArgument.untrusted("value", level.getSeed()))); + ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_SEED_MIXED, MessageArgument.untrusted("value", engine.getSeedManager().getSeed()), MessageArgument.untrusted("value2", engine.getSeedManager().getFullMixedSeed()))); + return 1; + } + + static int goldenhash(CommandSourceStack source, int radius, int threads, ModdedGoldenHash.Mode mode) { + ServerLevel level = source.getLevel(); + Engine engine = engineFor(level); + if (engine == null) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_15)); + return 0; + } + ModdedGoldenHash.start(source, level, engine, radius, threads, mode); + return 1; + } + + static int download(CommandSourceStack source, String rawRequest) { + DownloadRequest request = parseDownloadRequest(rawRequest); + if (request == null) { + fail(source, "Use /iris download pack=overworld, /iris download pack=underworld, or /iris download link=."); + return 0; + } + String target = downloadDisplayTarget(request); + String downloadSource = request.pack() == null ? "direct ZIP URL" : "built-in beta release"; + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, + MessageArgument.untrusted("pack", target), + MessageArgument.untrusted("downloadSource", downloadSource))); + return 0; + } + PackDownloadExecution execution; + synchronized (DOWNLOAD_MONITOR) { + if (!downloadAdmissionOpen) { + fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, + MessageArgument.untrusted("pack", target), + MessageArgument.untrusted("downloadSource", downloadSource))); + return 0; + } + + LifecycleOperationCoordinator.Lease lease; + try { + lease = LifecycleOperationCoordinator.get().acquire( + LifecycleOperationCoordinator.Domain.PACK_MUTATION, + LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, + target + ); + } catch (LifecycleOperationCoordinator.BusyException error) { + fail(source, downloadBusyMessage(error.currentOperation())); + return 0; + } + + execution = new PackDownloadExecution( + lease, + cancellation -> executeDownload(source, request, target, downloadSource, cancellation) + ); + PackDownloadExecution trackedExecution = execution; + execution.onCompletion(() -> clearActiveDownload(trackedExecution)); + activeDownload = execution; + boolean accepted; + try { + accepted = scheduler.asyncIfRunning(execution, execution::cancel); + } catch (Throwable error) { + execution.cancel(); + LOGGER.error("Iris pack download dispatch failed for {}", target, error); + fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, + MessageArgument.untrusted("pack", target), + MessageArgument.untrusted("downloadSource", downloadSource))); + return 0; + } + if (!accepted) { + execution.cancel(); + LOGGER.error("Iris pack download dispatch rejected for {} because the scheduler is shut down", target); + fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, + MessageArgument.untrusted("pack", target), + MessageArgument.untrusted("downloadSource", downloadSource))); + return 0; + } + } + + ok(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_DOWNLOADING_IRISDIMENSIONS, + MessageArgument.untrusted("pack", target), + MessageArgument.untrusted("downloadSource", downloadSource))); + return 1; + } + + private static void executeDownload( + CommandSourceStack source, + DownloadRequest request, + String target, + String downloadSource, + PackDownloader.DownloadCancellation cancellation + ) throws PackDownloader.PackDownloadCancelledException { + File packs = ModdedPackCommands.packsRoot(); + try { + PackDownloader.PackInstallResult result = request.pack() == null + ? PackDownloader.downloadUrl( + packs, + request.url(), + false, + (String message) -> dispatchDownloadFeedback(source, () -> ok(source, message)), + cancellation + ) + : PackDownloader.downloadBuiltIn( + packs, + request.pack(), + false, + (String message) -> dispatchDownloadFeedback(source, () -> ok(source, message)), + cancellation + ); + String completionMessage = downloadCompletionMessage(result); + if (result != null) { + if (completionMessage != null) { + dispatchDownloadFeedback(source, () -> ok(source, completionMessage)); + } + return; + } + } catch (PackDownloader.PackDownloadCancelledException error) { + throw error; + } catch (PackDownloader.PackDownloadBusyException error) { + dispatchDownloadFeedback(source, () -> fail(source, error.getMessage())); + return; + } catch (IOException | RuntimeException error) { + LOGGER.error("Iris pack download failed for {}", target, error); + } + dispatchDownloadFeedback(source, () -> fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, + MessageArgument.untrusted("pack", target), + MessageArgument.untrusted("downloadSource", downloadSource)))); + } + + private static void dispatchDownloadFeedback(CommandSourceStack source, Runnable feedback) { + source.getServer().execute(feedback); + } + + static String downloadBusyMessage(LifecycleOperationCoordinator.ActiveOperation operation) { + if (operation.domain() == LifecycleOperationCoordinator.Domain.PACK_MUTATION + && operation.kind() == LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD) { + return IrisLanguage.plain(PackDownloadMessages.IN_PROGRESS); + } + return "Iris pack changes are busy with " + operation.kind().name().toLowerCase(Locale.ROOT) + + " for '" + operation.target() + "'. Try again when it completes."; + } + + static String downloadCompletionMessage(PackDownloader.PackInstallResult result) { + if (result == null || !result.changed()) { + return null; + } + return result.restartRequired() + ? "Pack installed on disk. Restart the server before using it." + : "Pack installed on disk."; + } + + static String downloadDisplayTarget(DownloadRequest request) { + return request.pack() == null + ? IrisLanguage.plain(PackDownloadMessages.PROGRESS_SOURCE_REMOTE) + : request.pack(); + } + + private static void clearActiveDownload(PackDownloadExecution execution) { + synchronized (DOWNLOAD_MONITOR) { + if (activeDownload == execution) { + activeDownload = null; + } + } + } + + static DownloadRequest parseDownloadRequest(String rawRequest) { + if (rawRequest == null || rawRequest.isBlank()) { + return null; + } + if (rawRequest.startsWith("pack=")) { + String pack = rawRequest.substring("pack=".length()).trim().toLowerCase(Locale.ROOT); + return PackDownloader.isBuiltInPack(pack) ? new DownloadRequest(pack, null) : null; + } + if (rawRequest.startsWith("link=")) { + String url = rawRequest.substring("link=".length()).trim(); + return PackDownloader.isDirectZipUrl(url) ? new DownloadRequest(null, url) : null; + } + return null; + } + + static int metrics(CommandSourceStack source) { + ServerLevel level = source.getLevel(); + Engine engine = engineFor(level); + if (engine == null) { + fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_16)); + return 0; + } + ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_GENERATED_CHUNK_S_S, MessageArgument.untrusted("value", engine.getGenerated()), MessageArgument.untrusted("value2", String.format("%.1f", engine.getGeneratedPerSecond())))); + KMap pulled = engine.getMetrics().pull(); + Map sorted = new TreeMap<>(pulled); + for (Map.Entry entry : sorted.entrySet()) { + if (entry.getValue() == null || entry.getValue() <= 0D) { + continue; + } + ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_MS, MessageArgument.untrusted("value", entry.getKey()), MessageArgument.untrusted("value2", String.format("%.2f", entry.getValue())))); + } + return 1; + } + + static int verifyStructures(CommandSourceStack source, String keyRaw) { + return ModdedLocateCommands.verifyStructures(source, keyRaw); + } + + static CompletableFuture suggestStructureKeys(CommandContext context, SuggestionsBuilder builder) { + return ModdedCommandSuggestions.suggestStructureKeys(context, builder); + } + + static void warnTabFailure(String suggestion, CommandSourceStack source, Throwable error) { + ModdedCommandSuggestions.warnTabFailure(suggestion, source, error); + } + + static Engine engineFor(ServerLevel level) { + ChunkGenerator generator = level.getChunkSource().getGenerator(); + if (generator instanceof IrisModdedChunkGenerator irisGenerator) { + try { + return irisGenerator.commandEngine(); + } catch (Throwable e) { + LOGGER.error("Iris engine lookup failed for {}", level.dimension().identifier(), e); + return null; + } + } + return null; + } + + private static int engineCount(MinecraftServer server) { + int count = 0; + for (ServerLevel level : ModdedServerLevels.levels(server)) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) { + count++; + } + } + return count; + } + + static void ok(CommandSourceStack source, String message) { + ModdedCommandFeedback.ok(source, message); + } + + static void ok(CommandSourceStack source, Component component) { + ModdedCommandFeedback.ok(source, component); + } + + static void fail(CommandSourceStack source, String message) { + ModdedCommandFeedback.fail(source, message); + } + + record DownloadRequest(String pack, String url) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java new file mode 100644 index 000000000..334835dfe --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java @@ -0,0 +1,205 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +final class ModdedCommandFeedback { + static final int IRIS = 0x1BB19E; + static final int HEADER_A = 0x34EB6B; + static final int HEADER_B = 0x32BFAD; + static final int BACK = 0x6FE98F; + static final int DARK_GREEN = 0x46826A; + static final int DESCRIPTION_ICON = 0x3FE05A; + static final int DESCRIPTION = 0x6AD97D; + static final int USAGE_ICON = 0xBBE03F; + static final int USAGE = 0xA8E0A2; + static final int EXAMPLE_ICON = 0xC2F7D2; + static final int PARAMETER = 0x5EF288; + static final int PARAMETER_ALT = 0x32BFAD; + static final int OPTIONAL = 0x4F4F4F; + static final int CATEGORY = 0x9DE5B6; + static final int REQUIRED = 0xDB4321; + static final int REQUIRED_TEXT = 0xFAA796; + static final int HOVER_TYPE = 0x8AD9AF; + static final int VALUE = 0xC2F7D2; + static final int PAGE_LINE_LENGTH = 75; + private static final long MESSAGE_SOUND_COOLDOWN_MS = 650L; + private static final long TAB_SOUND_COOLDOWN_MS = 175L; + private static final Map MESSAGE_SOUNDS = new ConcurrentHashMap<>(); + private static final Map TAB_SOUNDS = new ConcurrentHashMap<>(); + + private ModdedCommandFeedback() { + } + + static void ok(CommandSourceStack source, String message) { + source.sendSuccess(() -> Component.literal(message).withStyle(ChatFormatting.GREEN), false); + playSuccess(source); + } + + static void ok(CommandSourceStack source, Component component) { + source.sendSuccess(() -> component, false); + playSuccess(source); + } + + static void fail(CommandSourceStack source, String message) { + source.sendFailure(Component.literal(message).withStyle(ChatFormatting.RED)); + playFailure(source); + } + + static void send(CommandSourceStack source, Component component) { + source.sendSuccess(() -> component, false); + } + + static void clear(CommandSourceStack source) { + if (source.getPlayer() != null) { + send(source, Component.literal("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n")); + } + } + + static MutableComponent header(String title) { + MutableComponent header = Component.empty(); + header.append(text(" ".repeat(18), HEADER_A, false, true)); + header.append(text(" " + title + " ", IRIS, true, false)); + header.append(text(" ".repeat(18), HEADER_B, false, true)); + return header; + } + + static MutableComponent banner(String title) { + int pad = Math.max(1, 44 - (title.length() + 2) - 4); + MutableComponent banner = Component.empty(); + banner.append(gradientText("[" + " ".repeat(pad) + "(((", HEADER_A, HEADER_B, true)); + banner.append(Component.literal(" ")); + banner.append(gradientText(title, PARAMETER, PARAMETER_ALT, false)); + banner.append(Component.literal(" ")); + banner.append(gradientText(")))" + " ".repeat(pad) + "]", HEADER_B, HEADER_A, true)); + return banner; + } + + static MutableComponent gradientText(String value, int from, int to, boolean strikethrough) { + MutableComponent out = Component.empty(); + int steps = Math.max(1, value.length() - 1); + for (int index = 0; index < value.length(); index++) { + double t = index / (double) steps; + int red = (int) Math.round(((from >> 16) & 0xFF) + ((((to >> 16) & 0xFF) - ((from >> 16) & 0xFF)) * t)); + int green = (int) Math.round(((from >> 8) & 0xFF) + ((((to >> 8) & 0xFF) - ((from >> 8) & 0xFF)) * t)); + int blue = (int) Math.round((from & 0xFF) + (((to & 0xFF) - (from & 0xFF)) * t)); + out.append(text(String.valueOf(value.charAt(index)), (red << 16) | (green << 8) | blue, false, strikethrough)); + } + return out; + } + + static MutableComponent footer() { + return text(" ".repeat(PAGE_LINE_LENGTH), HEADER_B, false, true); + } + + static MutableComponent text(String value, int color) { + return text(value, color, false, false); + } + + static MutableComponent text(String value, int color, boolean bold, boolean strikethrough) { + return Component.literal(value).withStyle((Style style) -> { + Style next = style.withColor(TextColor.fromRgb(color)); + if (bold) { + next = next.withBold(true); + } + if (strikethrough) { + next = next.withStrikethrough(true); + } + return next; + }); + } + + static MutableComponent button(String label, String command, String hover, boolean runCommand) { + ClickEvent clickEvent = runCommand ? new ClickEvent.RunCommand(command) : new ClickEvent.SuggestCommand(command); + MutableComponent hoverText = text(hover, DESCRIPTION); + return text(label, PARAMETER_ALT, true, false).withStyle((Style style) -> style + .withClickEvent(clickEvent) + .withHoverEvent(new HoverEvent.ShowText(hoverText))); + } + + static MutableComponent progressBar(double percent, int width) { + double clamped = Math.max(0D, Math.min(100D, percent)); + int filled = (int) Math.round((clamped / 100D) * width); + MutableComponent bar = Component.empty(); + bar.append(text("[", DARK_GREEN)); + for (int i = 0; i < width; i++) { + bar.append(text(i < filled ? "|" : "·", i < filled ? PARAMETER : OPTIONAL)); + } + bar.append(text("]", DARK_GREEN)); + return bar; + } + + static void tab(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null || !claim(TAB_SOUNDS, player.getUUID(), TAB_SOUND_COOLDOWN_MS)) { + return; + } + + player.level().playSound(null, player.blockPosition(), SoundEvents.ITEM_FRAME_ROTATE_ITEM, SoundSource.PLAYERS, 0.25F, 1.7F); + } + + private static void playSuccess(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null || !claim(MESSAGE_SOUNDS, player.getUUID(), MESSAGE_SOUND_COOLDOWN_MS)) { + return; + } + + ServerLevel level = player.level(); + level.playSound(null, player.blockPosition(), SoundEvents.AMETHYST_CLUSTER_BREAK, SoundSource.PLAYERS, 0.77F, 1.65F); + level.playSound(null, player.blockPosition(), SoundEvents.RESPAWN_ANCHOR_CHARGE, SoundSource.PLAYERS, 0.125F, 2.99F); + } + + private static void playFailure(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null || !claim(MESSAGE_SOUNDS, player.getUUID(), MESSAGE_SOUND_COOLDOWN_MS)) { + return; + } + + ServerLevel level = player.level(); + level.playSound(null, player.blockPosition(), SoundEvents.AMETHYST_CLUSTER_BREAK, SoundSource.PLAYERS, 0.77F, 0.25F); + level.playSound(null, player.blockPosition(), SoundEvents.BEACON_DEACTIVATE, SoundSource.PLAYERS, 0.2F, 0.45F); + } + + private static boolean claim(Map sounds, UUID uuid, long cooldownMs) { + long now = System.currentTimeMillis(); + Long previous = sounds.get(uuid); + if (previous != null && now - previous.longValue() < cooldownMs) { + return false; + } + + sounds.put(uuid, now); + return true; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java new file mode 100644 index 000000000..254120a45 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java @@ -0,0 +1,560 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.IrisMessages; +import art.arcane.iris.core.localization.ModdedHelpMessages; +import art.arcane.volmlib.util.director.help.DirectorHelpMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.localization.TextKey; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static art.arcane.iris.modded.command.ModdedCommandFeedback.BACK; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.CATEGORY; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.DARK_GREEN; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.DESCRIPTION; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.DESCRIPTION_ICON; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.EXAMPLE_ICON; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.HOVER_TYPE; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.OPTIONAL; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.PARAMETER; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.PARAMETER_ALT; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.REQUIRED; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.REQUIRED_TEXT; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.USAGE; +import static art.arcane.iris.modded.command.ModdedCommandFeedback.USAGE_ICON; + +final class ModdedCommandHelp { + private static final int PAGE_SIZE = 17; + private static final int PAGE_BUTTON_WIDTH = 10; + private static final TextKey COMMAND_UNREGISTERED = TextKey.of( + "iris.modded.help.entry.command.unregistered", + "Print every structure excluded from goto completion and its exact reason to the server console"); + private static final Map> SECTIONS = new LinkedHashMap<>(); + + static { + SECTIONS.put("", List.of( + Entry.command("version", "", ModdedHelpMessages.COMMAND_VERSION_PRINT_VERSION_INFORMATION), + Entry.command("info", "[dimension]", ModdedHelpMessages.COMMAND_INFO_LIST_LOADED_IRIS_DIMENSIONS_AND_PACK_DETAILS), + Entry.command("what", "[here|biome|region|block|hand|markers]", ModdedHelpMessages.COMMAND_WHAT_INSPECT_THE_IRIS_BIOME_REGION_CAVE_BIOME_SURFACE_AND_CHUNK_AT_YOUR), + Entry.group("find", ModdedHelpMessages.GROUP_FIND_FIND_AND_TELEPORT_TO_IRIS_BIOMES_REGIONS_OBJECTS_IRIS_STRUCTURES_NATIVE_STRUCTURES, "goto"), + Entry.command("teleport", " [player]", ModdedHelpMessages.COMMAND_TP_TELEPORT_YOURSELF_OR_A_NAMED_PLAYER_INTO_A_LOADED_IRIS_DIMENSION, "tp"), + Entry.command("evacuate", "[dimension]", ModdedHelpMessages.COMMAND_EVACUATE_TELEPORT_EVERY_PLAYER_OUT_OF_AN_IRIS_DIMENSION_TO_THE_PRIMARY_WORLD), + Entry.command("seed", "", ModdedHelpMessages.COMMAND_SEED_PRINT_WORLD_AND_ENGINE_SEED_INFORMATION), + Entry.command("debug", "", ModdedHelpMessages.COMMAND_DEBUG_TOGGLE_IRIS_DEBUG_LOGGING_AND_SAVE_SETTINGS_JSON), + Entry.command("reload", "", ModdedHelpMessages.COMMAND_RELOAD_RELOAD_SETTINGS_JSON_ALSO_HOTLOADED_AUTOMATICALLY_EVERY_3S), + Entry.command("download", ">", ModdedHelpMessages.COMMAND_DOWNLOAD_DOWNLOAD_A_PACK_PROJECT, "dl"), + Entry.command("metrics", "", ModdedHelpMessages.COMMAND_METRICS_PRINT_GENERATION_METRICS_FOR_YOUR_CURRENT_IRIS_DIMENSION, "measure"), + Entry.command("regen", "[radius]", ModdedHelpMessages.COMMAND_REGEN_DELETE_AND_REGENERATE_NEARBY_CHUNKS_IN_PLACE, "rg"), + Entry.group("pregen", ModdedHelpMessages.GROUP_PREGEN_PREGENERATE_AN_IRIS_DIMENSION, "pregenerate"), + Entry.command("wand", "", ModdedHelpMessages.COMMAND_WAND_GET_AN_IRIS_OBJECT_WAND), + Entry.command("dust", "", ModdedHelpMessages.COMMAND_DUST_GET_DUST_THAT_REVEALS_OBJECT_PLACEMENTS, "d"), + Entry.group("object", ModdedHelpMessages.GROUP_OBJECT_OBJECT_WAND_SAVE_PASTE_ANALYZE_AND_UNDO_TOOLS, "o"), + Entry.group("edit", ModdedHelpMessages.GROUP_EDIT_OPEN_PACK_BIOME_REGION_AND_DIMENSION_JSON_FILES_IN_YOUR_DESKTOP_EDITOR), + Entry.command("create", " [pack|pack:dimensionKey] [seed]", ModdedHelpMessages.COMMAND_CREATE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_QUOTE_PACK_DIMENSIONKEY_TO_PICK, "c"), + Entry.command("height", "", ModdedHelpMessages.COMMAND_HEIGHT_PRINT_WORLD_HEIGHT), + Entry.command("worlds", "", ModdedHelpMessages.COMMAND_WORLDS_LIST_WORLD_ACCESS, "accesslist"), + Entry.group("studio", ModdedHelpMessages.GROUP_STUDIO_PACK_PROJECT_CREATION_PACKAGING_AND_REPORTS, "std", "s"), + Entry.group("pack", ModdedHelpMessages.GROUP_PACK_PACK_VALIDATION_AND_MAINTENANCE, "pk"), + Entry.group("world", ModdedHelpMessages.GROUP_WORLD_RUNTIME_IRIS_DIMENSION_CREATION_REMOVAL_AND_STATUS, "w"), + Entry.group("datapack", ModdedHelpMessages.GROUP_DATAPACK_WORLD_DATAPACK_INSTALL_AND_STATUS_HELPERS, "datapacks", "dp"), + Entry.group("structure", ModdedHelpMessages.GROUP_STRUCTURE_IRIS_STRUCTURE_INDEX_INFO_AND_PLACEMENT_TOOLS, "struct", "str"), + Entry.command("goldenhash", "[radius] [threads] [capture|verify]", ModdedHelpMessages.COMMAND_GOLDENHASH_GENERATE_DETERMINISTIC_BLOCK_HASHES_FOR_PARITY_TESTING, "gold"), + Entry.group("developer", ModdedHelpMessages.GROUP_DEVELOPER_DEVELOPER_DIAGNOSTICS_SENTRY_TEST_NETWORK_INTERFACES_REGION_FILE_SCAN, "dev") + )); + SECTIONS.put("what", List.of( + Entry.command("here", "", ModdedHelpMessages.COMMAND_WHAT_HERE_INSPECT_CURRENT_IRIS_CONTEXT), + Entry.command("biome", "", ModdedHelpMessages.COMMAND_WHAT_BIOME_INSPECT_CURRENT_BIOME), + Entry.command("region", "", ModdedHelpMessages.COMMAND_WHAT_REGION_INSPECT_CURRENT_REGION), + Entry.command("block", "", ModdedHelpMessages.COMMAND_WHAT_BLOCK_INSPECT_TARGET_BLOCK), + Entry.command("hand", "", ModdedHelpMessages.COMMAND_WHAT_HAND_INSPECT_HELD_ITEM), + Entry.command("markers", "", ModdedHelpMessages.COMMAND_WHAT_MARKERS_REVEAL_NEARBY_MARKERS) + )); + SECTIONS.put("find", List.of( + Entry.command("biome", "", ModdedHelpMessages.COMMAND_BIOME_FIND_AN_IRIS_BIOME), + Entry.command("region", "", ModdedHelpMessages.COMMAND_REGION_FIND_AN_IRIS_REGION), + Entry.command("object", "", ModdedHelpMessages.COMMAND_OBJECT_FIND_AN_OBJECT_PLACEMENT), + Entry.command("unregistered", "", COMMAND_UNREGISTERED), + Entry.command("structure", "", ModdedHelpMessages.COMMAND_STRUCTURE_FIND_AN_IRIS_PLACED_OR_NATIVE_DATAPACK_STRUCTURE), + Entry.command("poi", "", ModdedHelpMessages.COMMAND_POI_FIND_A_SUPPORTED_POINT_OF_INTEREST) + )); + SECTIONS.put("goto", SECTIONS.get("find")); + SECTIONS.put("edit", List.of( + Entry.command("biome", "[key]", ModdedHelpMessages.COMMAND_BIOME_OPEN_A_BIOME_JSON_IN_YOUR_DESKTOP_EDITOR_NO_KEY_OPENS_THE, "b"), + Entry.command("region", "[key]", ModdedHelpMessages.COMMAND_REGION_OPEN_A_REGION_JSON_IN_YOUR_DESKTOP_EDITOR_NO_KEY_OPENS_THE, "r"), + Entry.command("dimension", "", ModdedHelpMessages.COMMAND_DIMENSION_OPEN_THE_CURRENT_PACK_S_DIMENSION_JSON_IN_YOUR_DESKTOP_EDITOR, "d") + )); + SECTIONS.put("pregen", List.of( + Entry.command("start", " [dimension] [at] [x] [z] [gui] [sync] [nocache]", ModdedHelpMessages.COMMAND_START_START_PREGENERATION_RADIUS_IN_BLOCKS_RESUMABLE_CHECKPOINT_CACHE_ON_BY_DEFAULT_CENTER), + Entry.command("stop", "", ModdedHelpMessages.COMMAND_STOP_STOP_THE_ACTIVE_PREGENERATION_TASK, "x"), + Entry.command("pause", "", ModdedHelpMessages.COMMAND_PAUSE_PAUSE_OR_RESUME_PREGENERATION, "resume"), + Entry.command("status", "", ModdedHelpMessages.COMMAND_STATUS_SHOW_PREGENERATION_STATUS) + )); + SECTIONS.put("pregenerate", SECTIONS.get("pregen")); + SECTIONS.put("object", List.of( + Entry.command("wand", "", ModdedHelpMessages.COMMAND_WAND_GET_AN_IRIS_OBJECT_WAND_2), + Entry.command("dust", "", ModdedHelpMessages.COMMAND_DUST_GET_DUST_THAT_REVEALS_OBJECT_PLACEMENTS, "d"), + Entry.command("save", "[overwrite] ", ModdedHelpMessages.COMMAND_SAVE_SAVE_THE_SELECTED_WAND_VOLUME_AS_AN_OBJECT), + Entry.command("paste", "[at] [x] [y] [z] [rotate] [degrees] ", ModdedHelpMessages.COMMAND_PASTE_PASTE_AN_OBJECT_AT_YOUR_POSITION_OR_A_GIVEN_POSITION_OPTIONALLY_ROTATED), + Entry.command("expand", "[amount]", ModdedHelpMessages.COMMAND_EXPAND_EXPAND_THE_WAND_SELECTION_IN_YOUR_LOOKING_DIRECTION), + Entry.command("contract", "[amount]", ModdedHelpMessages.COMMAND_CONTRACT_CONTRACT_THE_WAND_SELECTION_IN_YOUR_LOOKING_DIRECTION, "-"), + Entry.command("shift", "[amount]", ModdedHelpMessages.COMMAND_SHIFT_SHIFT_THE_WAND_SELECTION_IN_YOUR_LOOKING_DIRECTION), + Entry.command("position1", "[look]", ModdedHelpMessages.COMMAND_POSITION1_SET_SELECTION_POINT_1, "p1"), + Entry.command("position2", "[look]", ModdedHelpMessages.COMMAND_POSITION2_SET_SELECTION_POINT_2, "p2"), + Entry.command("x+y", "", ModdedHelpMessages.COMMAND_X_Y_AUTOSELECT_UP_AND_OUT, "xpy"), + Entry.command("x&y", "", ModdedHelpMessages.COMMAND_X_Y_AUTOSELECT_UP_DOWN_AND_OUT, "xay"), + Entry.command("analyze", "", ModdedHelpMessages.COMMAND_ANALYZE_SHOW_OBJECT_COMPOSITION), + Entry.command("shrink", "", ModdedHelpMessages.COMMAND_SHRINK_SHRINK_AN_OBJECT_TO_ITS_MINIMUM_SIZE), + Entry.command("plausibilize", " [dryrun=true] [reach=N]", ModdedHelpMessages.COMMAND_PLAUSIBILIZE_GROW_BRANCHES_SO_TREE_LEAVES_SURVIVE_VANILLA_DECAY), + Entry.command("undo", "[amount]", ModdedHelpMessages.COMMAND_UNDO_UNDO_PASTED_OBJECTS, "u"), + Entry.command("we", "", ModdedHelpMessages.COMMAND_OBJECT_WE_BUKKIT_ONLY), + Entry.command("studio", "", ModdedHelpMessages.COMMAND_OBJECT_STUDIO_BUKKIT_ONLY), + Entry.command("convert", "", ModdedHelpMessages.COMMAND_OBJECT_CONVERT_BUKKIT_ONLY) + )); + SECTIONS.put("o", SECTIONS.get("object")); + SECTIONS.put("studio", List.of( + Entry.command("create", "[name] [template]", ModdedHelpMessages.COMMAND_CREATE_CREATE_A_NEW_PACK_PROJECT, "+"), + Entry.command("package", "[pack]", ModdedHelpMessages.COMMAND_PACKAGE_PACKAGE_A_DIMENSION_INTO_A_COMPRESSED_FORMAT, "pkg"), + Entry.command("version", "[pack]", ModdedHelpMessages.COMMAND_VERSION_PRINT_A_PACK_VERSION), + Entry.command("regions", "[radius]", ModdedHelpMessages.COMMAND_REGIONS_CALCULATE_NEARBY_REGION_DISTRIBUTION), + Entry.command("open", " [seed]", ModdedHelpMessages.COMMAND_OPEN_OPEN_A_TEMPORARY_STUDIO_DIMENSION_FOR_A_PACK, "o"), + Entry.command("close", "", ModdedHelpMessages.COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD, "x"), + Entry.command("tpstudio", "", ModdedHelpMessages.COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION, "stp"), + Entry.command("status", "", ModdedHelpMessages.COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK), + Entry.command("noise", "[generator] [seed]", ModdedHelpMessages.COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY, "nmap"), + Entry.command("map", "", ModdedHelpMessages.COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY, "render"), + Entry.command("vscode", "[pack]", ModdedHelpMessages.COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR, "vsc"), + Entry.command("update", "[pack]", ModdedHelpMessages.COMMAND_UPDATE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK), + Entry.command("importvanilla", "", ModdedHelpMessages.COMMAND_IMPORTVANILLA_EXPLAIN_VANILLA_IMPORT_WORKFLOW, "importv", "iv"), + Entry.command("loot", "", ModdedHelpMessages.COMMAND_STUDIO_LOOT_BUKKIT_ONLY), + Entry.command("profile", "", ModdedHelpMessages.COMMAND_STUDIO_PROFILE_BUKKIT_ONLY), + Entry.command("spawn", "", ModdedHelpMessages.COMMAND_STUDIO_SPAWN_BUKKIT_ONLY, "summon"), + Entry.command("objects", "", ModdedHelpMessages.COMMAND_STUDIO_OBJECTS_BUKKIT_ONLY, "find-objects") + )); + SECTIONS.put("std", SECTIONS.get("studio")); + SECTIONS.put("s", SECTIONS.get("studio")); + SECTIONS.put("pack", List.of( + Entry.command("validate", "[pack]", ModdedHelpMessages.COMMAND_VALIDATE_VALIDATE_A_PACK_OR_EVERY_PACK, "v"), + Entry.command("cleanup", " [apply]", ModdedHelpMessages.COMMAND_CLEANUP_PREVIEW_OR_QUARANTINE_UNUSED_RESOURCE_CANDIDATES, "c"), + Entry.command("restore", " [apply]", ModdedHelpMessages.COMMAND_RESTORE_PREVIEW_OR_RESTORE_THE_LATEST_QUARANTINE, "r"), + Entry.command("status", "[pack]", ModdedHelpMessages.COMMAND_STATUS_SHOW_CACHED_VALIDATION_STATUS, "s") + )); + SECTIONS.put("pk", SECTIONS.get("pack")); + SECTIONS.put("world", List.of( + Entry.command("enable", " [seed|random]", ModdedHelpMessages.COMMAND_ENABLE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_AT_RUNTIME_DOWNLOADS_THE_PACK, "create"), + Entry.command("replace-overworld", " [seed|random]", ModdedHelpMessages.COMMAND_REPLACE_OVERWORLD_INJECT_AN_IRIS_PRIMARY_WORLD_AND_ROUTE_PLAYERS_THERE_INSTEAD_OF_THE), + Entry.command("mainworld", " [seed|random]", ModdedHelpMessages.COMMAND_MAINWORLD_CONFIGURE_PRIMARY_WORLD_PRESET), + Entry.command("disable", "", ModdedHelpMessages.COMMAND_DISABLE_EVACUATE_AND_UNLOAD_AN_IRIS_DIMENSION_WORLD_DATA_ON_DISK_IS_KEPT), + Entry.command("delete", "", ModdedHelpMessages.COMMAND_DELETE_DISABLE_AN_IRIS_DIMENSION_AND_WIPE_ITS_CHUNK_AND_MANTLE_DATA_FROM, "remove", "rm"), + Entry.command("list", "", ModdedHelpMessages.COMMAND_LIST_LIST_LOADED_IRIS_DIMENSIONS, "ls"), + Entry.command("status", "", ModdedHelpMessages.COMMAND_STATUS_SHOW_LOADED_IRIS_DIMENSIONS_AND_THE_CONFIGURED_PRIMARY_WORLD) + )); + SECTIONS.put("w", SECTIONS.get("world")); + SECTIONS.put("datapack", List.of( + Entry.command("status", "", ModdedHelpMessages.COMMAND_STATUS_CHECK_LOADED_IRIS_DIMENSION_TYPE_OVERRIDES), + Entry.command("install", "", ModdedHelpMessages.COMMAND_INSTALL_INSTALL_DIMENSION_TYPE_OVERRIDES_FOR_LOADED_IRIS_DIMENSIONS), + Entry.command("list", "", ModdedHelpMessages.COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS, "ls"), + Entry.command("ingest", "", ModdedHelpMessages.COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW, "pull"), + Entry.command("remove", "", ModdedHelpMessages.COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW, "rm") + )); + SECTIONS.put("datapacks", SECTIONS.get("datapack")); + SECTIONS.put("dp", SECTIONS.get("datapack")); + SECTIONS.put("structure", List.of( + Entry.command("list", "", ModdedHelpMessages.COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON, "ls"), + Entry.command("info", "", ModdedHelpMessages.COMMAND_INFO_RESOLVE_AN_IRIS_STRUCTURE_GRAPH_AND_REPORT_BOUNDS), + Entry.command("place", "", ModdedHelpMessages.COMMAND_PLACE_ASSEMBLE_AND_PLACE_AN_IRIS_STRUCTURE_AT_YOUR_LOCATION, "p"), + Entry.command("import", "", ModdedHelpMessages.COMMAND_IMPORT_EXPLAIN_BUKKIT_STRUCTURE_IMPORT_WORKFLOW, "import-all", "reimport", "imp", "all"), + Entry.command("capture", "", ModdedHelpMessages.COMMAND_CAPTURE_EXPLAIN_BUKKIT_STRUCTURE_CAPTURE_WORKFLOW, "cap"), + Entry.command("verify", "[key]", ModdedHelpMessages.COMMAND_VERIFY_REPORT_NATIVE_AND_IRIS_STRUCTURE_REACHABILITY_IN_THE_CURRENT_DIMENSION, "locateall") + )); + SECTIONS.put("struct", SECTIONS.get("structure")); + SECTIONS.put("str", SECTIONS.get("structure")); + SECTIONS.put("developer", List.of( + Entry.command("sentry", "", ModdedHelpMessages.COMMAND_SENTRY_SEND_A_TEST_EXCEPTION_TO_THE_IRIS_ERROR_REPORTER), + Entry.command("network", "", ModdedHelpMessages.COMMAND_NETWORK_LIST_NETWORK_INTERFACES_AND_THEIR_ADDRESSES, "ip") + )); + SECTIONS.put("dev", SECTIONS.get("developer")); + } + + private ModdedCommandHelp() { + } + + static boolean documents(String section, String command) { + List entries = SECTIONS.get(section); + if (entries == null) { + return false; + } + for (Entry entry : entries) { + if (entry.name().equals(command)) { + return true; + } + for (String alias : entry.aliases()) { + if (alias.equals(command)) { + return true; + } + } + } + return false; + } + + static int send(CommandSourceStack source, String path) { + Request request = parse(path); + List entries = SECTIONS.get(request.section()); + if (entries == null) { + ModdedCommandFeedback.fail(source, IrisLanguage.plain( + IrisMessages.MODDED_HELP_UNKNOWN_SECTION, + MessageArgument.untrusted("section", request.section()) + )); + return 0; + } + + ModdedCommandFeedback.clear(source); + + if (source.getPlayer() == null) { + return sendConsole(source, request.section()); + } + + int totalPages = Math.max(1, (int) Math.ceil(entries.size() / (double) PAGE_SIZE)); + int page = Math.max(0, Math.min(request.page(), totalPages - 1)); + int from = page * PAGE_SIZE; + int to = Math.min(entries.size(), from + PAGE_SIZE); + + sendHeader(source, request.section(), page, totalPages); + if (!Commands.hasPermission(Commands.LEVEL_GAMEMASTERS).test(source)) { + ModdedCommandFeedback.send(source, opNotice()); + } + if (!request.section().isEmpty()) { + ModdedCommandFeedback.send(source, backButton(request.section())); + } + for (Entry entry : entries.subList(from, to)) { + ModdedCommandFeedback.send(source, line(request.section(), entry)); + } + ModdedCommandFeedback.ok(source, footer(request.section(), page, totalPages)); + return 1; + } + + private static int sendConsole(CommandSourceStack source, String section) { + sendHeader(source, section, 0, 1); + if (!Commands.hasPermission(Commands.LEVEL_GAMEMASTERS).test(source)) { + ModdedCommandFeedback.send(source, opNotice()); + } + for (String line : consoleLines(section)) { + ModdedCommandFeedback.send(source, Component.literal(line)); + } + return 1; + } + + static List consoleLines(String section) { + List entries = SECTIONS.get(section); + if (entries == null) { + return List.of(); + } + + List lines = new ArrayList<>(entries.size()); + for (Entry entry : entries) { + lines.add(consoleLine(section, entry)); + } + return lines; + } + + private static String consoleLine(String section, Entry entry) { + StringBuilder line = new StringBuilder(section.isEmpty() ? "/iris " : "/iris " + section + " "); + line.append(entry.name()); + if (!entry.usage().isBlank()) { + line.append(' ').append(entry.usage()); + } + if (entry.aliases().length > 0) { + line.append(" (").append(String.join(", ", entry.aliases())).append(')'); + } + if (entry.group()) { + line.append(" - ").append(IrisLanguage.plain(DirectorHelpMessages.CATEGORY)); + } + return line.append(" - ").append(IrisLanguage.plain(entry.description())).toString(); + } + + private static void sendHeader(CommandSourceStack source, String path, int page, int totalPages) { + String title = path.isEmpty() ? "/iris" : "/iris " + path; + if (totalPages > 1) { + title += " {" + (page + 1) + "/" + totalPages + "}"; + } + ModdedCommandFeedback.send(source, ModdedCommandFeedback.banner(title)); + } + + private static MutableComponent backButton(String path) { + String parent = parentPath(path); + String command = parent.isEmpty() ? "/iris" : "/iris help " + parent; + MutableComponent hover = Component.empty() + .append(text(IrisLanguage.plain( + IrisMessages.MODDED_HELP_BACK_HOVER, + MessageArgument.untrusted("parent", parent.isEmpty() ? "Iris" : parent) + ), DARK_GREEN)); + return text("〈 " + IrisLanguage.plain(DirectorHelpMessages.BACK), BACK).withStyle((style) -> style + .withClickEvent(new ClickEvent.RunCommand(command)) + .withHoverEvent(new HoverEvent.ShowText(hover))); + } + + private static MutableComponent line(String path, Entry entry) { + MutableComponent row = Component.empty(); + row.append(clickableCommand(path, entry)); + row.append(nodes(entry)); + return row; + } + + private static MutableComponent clickableCommand(String path, Entry entry) { + String parent = path.isEmpty() ? "/iris" : "/iris " + path; + String command = parent + " " + entry.name(); + String suggestion = entry.usage().isBlank() ? command : command + " " + entry.usage(); + ClickEvent clickEvent = entry.group() ? new ClickEvent.RunCommand(command) : new ClickEvent.SuggestCommand(suggestion); + MutableComponent hover = entryHover(entry, suggestion); + MutableComponent display = Component.empty(); + display.append(text("⇀", DARK_GREEN)); + display.append(Component.literal(" ")); + display.append(ModdedCommandFeedback.gradientText(entry.name(), PARAMETER, PARAMETER_ALT, false)); + return display.withStyle((style) -> style + .withClickEvent(clickEvent) + .withHoverEvent(new HoverEvent.ShowText(hover))); + } + + private static MutableComponent nodes(Entry entry) { + if (entry.group()) { + return text(" - " + IrisLanguage.plain(DirectorHelpMessages.CATEGORY), CATEGORY); + } + + List tokens = usageTokens(entry.usage()); + if (tokens.isEmpty()) { + return Component.empty(); + } + + MutableComponent nodes = Component.empty(); + for (String token : tokens) { + nodes.append(Component.literal(" ")); + nodes.append(parameter(token)); + } + return nodes; + } + + private static MutableComponent parameter(String token) { + String name = parameterName(token); + boolean required = token.startsWith("<"); + MutableComponent title = Component.empty(); + if (required) { + title.append(text("[", REQUIRED, true, false)); + title.append(text(name, PARAMETER, false, false)); + title.append(text("]", REQUIRED, true, false)); + } else { + title.append(text("⊰", OPTIONAL)); + title.append(text(name, PARAMETER)); + title.append(text("⊱", OPTIONAL)); + } + + MutableComponent hover = Component.empty(); + hover.append(text(name, PARAMETER)); + hover.append(Component.literal("\n")); + hover.append(text("✎ ", DESCRIPTION_ICON)); + hover.append(text(IrisLanguage.plain(IrisMessages.MODDED_HELP_COMMAND_PARAMETER), DESCRIPTION)); + hover.append(Component.literal("\n")); + if (required) { + hover.append(text("⚠ ", REQUIRED)); + hover.append(text(IrisLanguage.plain(DirectorHelpMessages.REQUIRED), REQUIRED_TEXT)); + } else { + hover.append(text("✔ ", DESCRIPTION_ICON)); + hover.append(text(IrisLanguage.plain(DirectorHelpMessages.OPTIONAL), USAGE)); + } + hover.append(Component.literal("\n")); + hover.append(text("✢ ", DARK_GREEN)); + hover.append(text(IrisLanguage.plain(IrisMessages.MODDED_HELP_BRIGADIER_TEXT), HOVER_TYPE)); + + return title.withStyle((style) -> style.withHoverEvent(new HoverEvent.ShowText(hover))); + } + + private static MutableComponent entryHover(Entry entry, String suggestion) { + MutableComponent hover = Component.empty(); + hover.append(text(names(entry), PARAMETER)); + hover.append(Component.literal("\n")); + hover.append(text("✎ ", DESCRIPTION_ICON)); + hover.append(text(IrisLanguage.plain(entry.description()), DESCRIPTION)); + hover.append(Component.literal("\n")); + hover.append(text("✒ ", USAGE_ICON)); + if (entry.group()) { + hover.append(text(IrisLanguage.plain(DirectorHelpMessages.COMMAND_GROUP), USAGE)); + } else if (entry.usage().isBlank()) { + hover.append(text(IrisLanguage.plain(DirectorHelpMessages.NO_PARAMETERS), USAGE)); + } else { + hover.append(text(IrisLanguage.plain(DirectorHelpMessages.PARAMETERS_HOVER), USAGE)); + hover.append(Component.literal("\n")); + hover.append(text("✦ ", EXAMPLE_ICON)); + hover.append(text(suggestion, PARAMETER)); + } + return hover; + } + + private static MutableComponent opNotice() { + MutableComponent notice = Component.empty(); + notice.append(text("⚠ ", REQUIRED)); + notice.append(text(IrisLanguage.plain(IrisMessages.MODDED_HELP_OPERATOR_NOTICE) + " ", REQUIRED_TEXT)); + notice.append(text(IrisLanguage.plain( + IrisMessages.MODDED_HELP_OPERATOR_INSTRUCTION, + MessageArgument.trusted("command", "/op ") + ), DESCRIPTION)); + return notice; + } + + private static MutableComponent footer(String section, int page, int totalPages) { + MutableComponent footer = Component.empty(); + int fill = ModdedCommandFeedback.PAGE_LINE_LENGTH; + boolean hasPrevious = page > 0; + boolean hasNext = page + 1 < totalPages; + + if (hasPrevious) { + fill -= PAGE_BUTTON_WIDTH; + footer.append(pageButton(section, page, false)); + footer.append(Component.literal(" ")); + } + if (hasNext) { + fill -= PAGE_BUTTON_WIDTH; + } + + footer.append(ModdedCommandFeedback.gradientText( + " ".repeat(fill), + ModdedCommandFeedback.HEADER_B, + ModdedCommandFeedback.HEADER_A, + true + )); + + if (hasNext) { + footer.append(Component.literal(" ")); + footer.append(pageButton(section, page + 2, true)); + } + return footer; + } + + private static MutableComponent pageButton(String section, int target, boolean next) { + String command = "/iris help " + (section.isEmpty() ? "" : section + " ") + target; + String pageWord = IrisLanguage.plain(DirectorHelpMessages.PAGE); + String label = next ? pageWord + " " + target + " ❭" : "〈 " + pageWord + " " + target; + MutableComponent hover = text(IrisLanguage.plain(next + ? DirectorHelpMessages.NEXT_PAGE + : DirectorHelpMessages.PREVIOUS_PAGE), DESCRIPTION); + return text(label, next ? PARAMETER_ALT : PARAMETER).withStyle((style) -> style + .withClickEvent(new ClickEvent.RunCommand(command)) + .withHoverEvent(new HoverEvent.ShowText(hover))); + } + + private static MutableComponent text(String value, int color) { + return ModdedCommandFeedback.text(value, color, false, false); + } + + private static MutableComponent text(String value, int color, boolean bold, boolean strikethrough) { + return ModdedCommandFeedback.text(value, color, bold, strikethrough); + } + + private static List usageTokens(String usage) { + if (usage == null || usage.isBlank()) { + return List.of(); + } + return List.of(usage.trim().split("\\s+")); + } + + private static String names(Entry entry) { + if (entry.aliases().length == 0) { + return entry.name(); + } + + List names = new ArrayList<>(entry.aliases().length + 1); + names.add(entry.name()); + for (String alias : entry.aliases()) { + names.add(alias); + } + return String.join(", ", names); + } + + private static String parameterName(String token) { + String name = token; + if ((name.startsWith("<") && name.endsWith(">")) || (name.startsWith("[") && name.endsWith("]"))) { + name = name.substring(1, name.length() - 1); + } + return name; + } + + private static String parentPath(String path) { + int lastSpace = path.lastIndexOf(' '); + if (lastSpace <= 0) { + return ""; + } + return path.substring(0, lastSpace); + } + + private static Request parse(String path) { + if (path == null) { + return new Request("", 0); + } + + String normalized = path.trim().toLowerCase(); + if (normalized.startsWith("/iris ")) { + normalized = normalized.substring(6).trim(); + } else if (normalized.equals("/iris")) { + normalized = ""; + } + + if (normalized.startsWith("help ")) { + normalized = normalized.substring(5).trim(); + } else if (normalized.equals("help")) { + normalized = ""; + } + + int page = 0; + String[] tokens = normalized.isEmpty() ? new String[0] : normalized.split("\\s+"); + int end = tokens.length; + if (end > 0 && tokens[end - 1].matches("\\d+")) { + try { + page = Math.max(0, Integer.parseInt(tokens[end - 1]) - 1); + } catch (NumberFormatException ignored) { + page = 0; + } + end--; + } + + String section = end > 0 ? tokens[0] : ""; + return new Request(section, page); + } + + private record Request(String section, int page) { + } + + private record Entry(String name, String usage, TextKey description, boolean group, String... aliases) { + static Entry command(String name, String usage, TextKey description, String... aliases) { + return new Entry(name, usage, description, false, aliases); + } + + static Entry group(String name, TextKey description, String... aliases) { + return new Entry(name, "", description, true, aliases); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java new file mode 100644 index 000000000..ed5200a3d --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java @@ -0,0 +1,277 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.pack.PackDirectoryResolver; +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.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedServerLevels; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.levelgen.structure.Structure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +final class ModdedCommandSuggestions { + static final SuggestionProvider BIOME_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder); + static final SuggestionProvider REGION_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder); + static final SuggestionProvider OBJECT_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder); + static final SuggestionProvider STRUCTURE_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder); + static final SuggestionProvider POI_TYPES = (CommandContext context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder); + static final SuggestionProvider PACK_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestPackNames(context, builder); + static final SuggestionProvider DIMENSION_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder); + + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int TAB_FAILURE_KEYS_MAX = 256; + private static final Set REPORTED_TAB_FAILURES = ConcurrentHashMap.newKeySet(); + private static final long PACK_NAME_CACHE_TTL_MS = 3_000L; + private static volatile Set cachedPackNames; + private static volatile long cachedPackNamesAt; + + private ModdedCommandSuggestions() { + } + + private static CompletableFuture suggestBiomeKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + if (engine != null) { + return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder); + } + } catch (Throwable e) { + warnTabFailure("biome keys", context.getSource(), e); + } + return builder.buildFuture(); + } + + private static CompletableFuture suggestRegionKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + if (engine != null) { + return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder); + } + } catch (Throwable e) { + warnTabFailure("region keys", context.getSource(), e); + } + return builder.buildFuture(); + } + + private static CompletableFuture suggestObjectKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + if (engine != null) { + return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder); + } + } catch (Throwable e) { + warnTabFailure("object keys", context.getSource(), e); + } + return builder.buildFuture(); + } + + static CompletableFuture suggestStructureKeys(CommandContext context, SuggestionsBuilder builder) { + CommandSourceStack source = context.getSource(); + ModdedCommandFeedback.tab(source); + try { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + return builder.buildFuture(); + } + boolean nativeGenerationEnabled = + source.getServer().getWorldGenSettings().options().generateStructures(); + Collection irisKeys = IrisStructureLocator.locatableEditableKeys(engine); + Set reachableNativeKeys = StructureReachability.reachableKeys(engine); + Registry registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); + List nativeKeys = new ArrayList<>(registry.keySet().size()); + Set registeredKeys = new HashSet<>(registry.keySet().size()); + for (Identifier identifier : registry.keySet()) { + String key = identifier.toString(); + registeredKeys.add(normalizeKey(key)); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false); + boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key); + boolean locatableNativePlacement = nativePlacement + && IrisStructureLocator.hasLocatableNativePlacement(engine, key); + boolean locatableEditableReplacement = !nativePlacement + && decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS + && IrisStructureLocator.hasLocatableEditablePlacement(engine, key); + if (isEligibleRegisteredStructure(decision, nativePlacement, + locatableNativePlacement, locatableEditableReplacement, + reachableNativeKeys.contains(normalizeKey(key)), nativeGenerationEnabled)) { + nativeKeys.add(key); + } + } + Collection unregisteredIrisKeys = eligibleUnregisteredEditableKeys( + irisKeys, registeredKeys, + (String candidate) -> IrisStructureLocator.hasNativePlacement(engine, candidate)); + return SharedSuggestionProvider.suggest( + combineStructureKeys(unregisteredIrisKeys, nativeKeys), builder); + } catch (Throwable e) { + warnTabFailure("structure keys", source, e); + } + return builder.buildFuture(); + } + + static boolean isEligibleRegisteredStructure(IrisNativeStructureDecision decision, + boolean nativePlacement, + boolean locatableNativePlacement, + boolean locatableEditableReplacement, + boolean reachable, + boolean nativeGenerationEnabled) { + if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + return locatableEditableReplacement + || nativeGenerationEnabled && nativePlacement && locatableNativePlacement; + } + return nativeGenerationEnabled && decision.generate() + && (reachable || nativePlacement && locatableNativePlacement); + } + + static void warnTabFailure(String suggestion, CommandSourceStack source, Throwable error) { + String origin = tabOrigin(source); + if (!REPORTED_TAB_FAILURES.add(suggestion + '|' + origin + '|' + error.getClass().getName())) { + return; + } + if (REPORTED_TAB_FAILURES.size() > TAB_FAILURE_KEYS_MAX) { + REPORTED_TAB_FAILURES.clear(); + } + LOGGER.warn("Iris tab-complete for {} in {} failed; suggestions will be empty", suggestion, origin, error); + } + + private static String tabOrigin(CommandSourceStack source) { + if (source == null) { + return ""; + } + try { + return source.getLevel().dimension().identifier().toString(); + } catch (Throwable originFailure) { + return ""; + } + } + + static List combineStructureKeys(Collection irisKeys, Collection nativeKeys) { + Map combined = new TreeMap<>(); + addStructureKeys(combined, irisKeys); + addStructureKeys(combined, nativeKeys); + return List.copyOf(combined.values()); + } + + static List eligibleUnregisteredEditableKeys( + Collection irisKeys, Set normalizedRegisteredKeys, + Predicate nativePlacement) { + List filtered = new ArrayList<>(irisKeys.size()); + for (String key : irisKeys) { + String normalizedKey = normalizeKey(key); + if (!normalizedKey.isEmpty() + && !normalizedRegisteredKeys.contains(normalizedKey) + && !nativePlacement.test(key)) { + filtered.add(key.trim()); + } + } + return List.copyOf(filtered); + } + + private static void addStructureKeys(Map combined, Collection keys) { + for (String key : keys) { + String normalizedKey = normalizeKey(key); + if (!normalizedKey.isEmpty()) { + combined.putIfAbsent(normalizedKey, key.trim()); + } + } + } + + static String normalizeKey(String key) { + return key == null ? "" : key.trim().toLowerCase(Locale.ROOT); + } + + private static CompletableFuture suggestPackNames(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + // Suggestion packets arrive per keystroke; a short-lived snapshot keeps the directory + // walk off the hot path without ever serving stale names for more than a few seconds. + long now = System.currentTimeMillis(); + Set cached = cachedPackNames; + if (cached != null && now - cachedPackNamesAt < PACK_NAME_CACHE_TTL_MS) { + return SharedSuggestionProvider.suggest(cached, builder); + } + Set names = new TreeSet<>(); + names.add("overworld"); + try { + File packs = IrisPlatforms.get().packsFolderNoCreate(); + for (File child : PackDirectoryResolver.listVisiblePackDirectories(packs)) { + String packName = child.getName(); + names.add(packName); + File dimensions = new File(child, "dimensions"); + File[] dimensionFiles = dimensions.listFiles( + (File directory, String name) -> name.endsWith(".json")); + if (dimensionFiles == null) { + continue; + } + for (File dimensionFile : dimensionFiles) { + String fileName = dimensionFile.getName(); + names.add(packName + ":" + fileName.substring(0, fileName.length() - 5)); + } + } + } catch (Throwable e) { + warnTabFailure("pack names", context.getSource(), e); + } + cachedPackNames = names; + cachedPackNamesAt = now; + return SharedSuggestionProvider.suggest(names, builder); + } + + private static CompletableFuture suggestDimensionNames(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + List names = new ArrayList<>(); + for (ServerLevel level : ModdedServerLevels.levels(context.getSource().getServer())) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) { + names.add(level.dimension().identifier().toString()); + } + } + return SharedSuggestionProvider.suggest(names, builder); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java new file mode 100644 index 000000000..b42598754 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java @@ -0,0 +1,349 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.LongArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.resources.Identifier; + +import java.util.List; +import java.util.function.Predicate; + +final class ModdedCommandTree { + private static final SuggestionProvider DOWNLOAD_SOURCES = + (context, builder) -> + SharedSuggestionProvider.suggest( + List.of("pack=overworld", "pack=underworld", "link="), + builder + ); + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + /** + * SP-4: read-only inspection must work for an unopped player in a no-cheats singleplayer world, where the + * what/height overlays are the only way to see what Iris generated. Everything that mutates a world, + * downloads, opens studio or starts a pregen stays on GATE, and so does the world seed: it is the one + * read-only value a plain player must not be handed, so /iris seed is gated and info/worlds omit the seed + * field for anyone who fails {@link #isGamemaster(CommandSourceStack)}. + */ + private static final Predicate READ_ONLY = Commands.hasPermission(Commands.LEVEL_ALL); + + private ModdedCommandTree() { + } + + /** + * Same gate the mutating subtrees use, for output that mixes gated and ungated fields in one command. + */ + static boolean isGamemaster(CommandSourceStack source) { + return GATE.test(source); + } + + static LiteralArgumentBuilder rootTree() { + LiteralArgumentBuilder root = Commands.literal("iris"); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "")); + root.then(helpTree()); + + root.then(Commands.literal("version").requires(READ_ONLY) + .executes((CommandContext context) -> IrisModdedCommands.version(context.getSource()))); + + root.then(Commands.literal("info").requires(READ_ONLY) + .executes((CommandContext context) -> IrisModdedCommands.info(context.getSource(), null)) + .then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES) + .executes((CommandContext context) -> IrisModdedCommands.info(context.getSource(), StringArgumentType.getString(context, "dimension"))))); + + // ModdedWhatCommands.tree() gates itself at LEVEL_GAMEMASTERS and keeps that gate: + // "what markers" drives lease-gated 9x9 mantle scans, and the Bukkit twin puts the + // whole tree behind an op-default permission. requires() would overwrite, not AND. + root.then(ModdedWhatCommands.tree()); + + root.then(teleportTree("teleport")); + root.then(teleportTree("tp")); + + root.then(Commands.literal("evacuate").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.evacuate(context.getSource(), null)) + .then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES) + .executes((CommandContext context) -> IrisModdedCommands.evacuate(context.getSource(), DimensionArgument.getDimension(context, "dimension"))))); + + root.then(Commands.literal("debug").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.debug(context.getSource()))); + + root.then(Commands.literal("reload").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.reload(context.getSource()))); + root.then(Commands.literal("height").requires(READ_ONLY) + .executes((CommandContext context) -> IrisModdedCommands.height(context.getSource()))); + root.then(Commands.literal("worlds").requires(READ_ONLY) + .executes((CommandContext context) -> IrisModdedCommands.info(context.getSource(), null))); + root.then(Commands.literal("accesslist").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.info(context.getSource(), null))); + + root.then(gotoTree("goto")); + root.then(gotoTree("find")); + + root.then(Commands.literal("seed").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.seed(context.getSource()))); + + root.then(goldenhashTree("goldenhash")); + root.then(goldenhashTree("gold")); + + root.then(downloadTree("download")); + root.then(downloadTree("dl")); + + root.then(metricsTree("metrics")); + root.then(metricsTree("measure")); + + root.then(regenTree("regen")); + root.then(regenTree("rg")); + + root.then(pregenTree("pregen")); + root.then(pregenTree("pregenerate")); + + root.then(Commands.literal("wand").requires(GATE) + .executes((CommandContext context) -> ModdedObjectCommands.giveWand(context.getSource()))); + root.then(Commands.literal("dust").requires(GATE) + .executes((CommandContext context) -> ModdedObjectCommands.giveDust(context.getSource()))); + root.then(Commands.literal("d").requires(GATE) + .executes((CommandContext context) -> ModdedObjectCommands.giveDust(context.getSource()))); + root.then(ModdedObjectCommands.tree("object")); + root.then(ModdedObjectCommands.tree("o")); + root.then(editTree()); + + root.then(createTree("create")); + root.then(createTree("c")); + + root.then(ModdedStudioCommands.tree("studio")); + root.then(ModdedStudioCommands.tree("std")); + root.then(ModdedStudioCommands.tree("s")); + root.then(ModdedPackCommands.tree("pack")); + root.then(ModdedPackCommands.tree("pk")); + root.then(ModdedWorldCommands.tree("world")); + root.then(ModdedWorldCommands.tree("w")); + root.then(ModdedDatapackCommands.tree("datapack")); + root.then(ModdedDatapackCommands.tree("datapacks")); + root.then(ModdedDatapackCommands.tree("dp")); + root.then(ModdedStructureCommands.tree("structure")); + root.then(ModdedStructureCommands.tree("struct")); + root.then(ModdedStructureCommands.tree("str")); + root.then(ModdedDeveloperCommands.tree("developer")); + root.then(ModdedDeveloperCommands.tree("dev")); + + return root; + } + + private static LiteralArgumentBuilder createTree(String name) { + return Commands.literal(name).requires(GATE) + .then(Commands.argument("name", StringArgumentType.word()) + .executes((CommandContext context) -> + ModdedWorldCommands.createWorld( + context.getSource(), + StringArgumentType.getString(context, "name"), + "overworld", + 1337L)) + .then(Commands.argument("pack", StringArgumentType.string()).suggests(ModdedCommandSuggestions.PACK_NAMES) + .executes((CommandContext context) -> ModdedWorldCommands.createWorld(context.getSource(), + StringArgumentType.getString(context, "name"), + StringArgumentType.getString(context, "pack"), + 1337L)) + .then(Commands.argument("seed", LongArgumentType.longArg()) + .executes((CommandContext context) -> ModdedWorldCommands.createWorld(context.getSource(), + StringArgumentType.getString(context, "name"), + StringArgumentType.getString(context, "pack"), + LongArgumentType.getLong(context, "seed")))))); + } + + private static LiteralArgumentBuilder teleportTree(String name) { + return Commands.literal(name).requires(GATE) + .then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES) + .executes((CommandContext context) -> + IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), null)) + .then(Commands.argument("player", EntityArgument.player()) + .executes((CommandContext context) -> + IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), + EntityArgument.getPlayer(context, "player"))))); + } + + private static LiteralArgumentBuilder helpTree() { + return Commands.literal("help").requires(READ_ONLY) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "")) + .then(Commands.argument("section", StringArgumentType.greedyString()) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), StringArgumentType.getString(context, "section")))); + } + + private static LiteralArgumentBuilder downloadTree(String name) { + return Commands.literal(name).requires(GATE) + .then(Commands.argument("source", StringArgumentType.greedyString()).suggests(DOWNLOAD_SOURCES) + .executes((CommandContext context) -> IrisModdedCommands.download( + context.getSource(), + StringArgumentType.getString(context, "source") + ))); + } + + private static LiteralArgumentBuilder metricsTree(String name) { + return Commands.literal(name).requires(READ_ONLY) + .executes((CommandContext context) -> IrisModdedCommands.metrics(context.getSource())); + } + + private static LiteralArgumentBuilder regenTree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.regen(context.getSource(), 0)) + .then(Commands.argument("radius", IntegerArgumentType.integer(0, 64)) + .executes((CommandContext context) -> IrisModdedCommands.regen(context.getSource(), IntegerArgumentType.getInteger(context, "radius")))); + } + + private static LiteralArgumentBuilder gotoTree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)) + .then(Commands.literal("biome") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS) + .executes((CommandContext context) -> ModdedLocateCommands.gotoBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("region") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS) + .executes((CommandContext context) -> ModdedLocateCommands.gotoRegion(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("object") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS) + .executes((CommandContext context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("unregistered") + .executes((CommandContext context) -> + ModdedUnregisteredStructures.print(context.getSource()))) + .then(Commands.literal("structure") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.STRUCTURE_KEYS) + .executes((CommandContext context) -> ModdedLocateCommands.gotoStructure(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("poi") + .then(Commands.argument("type", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.POI_TYPES) + .executes((CommandContext context) -> ModdedLocateCommands.gotoPoi(context.getSource(), StringArgumentType.getString(context, "type"))))); + } + + private static LiteralArgumentBuilder pregenTree(String name) { + RequiredArgumentBuilder radius = Commands.argument("radius", IntegerArgumentType.integer(1, 100000)) + .executes((CommandContext context) -> ModdedPregenCommands.pregenStart(context, false, false, false, false, false)); + attachPregenCenter(radius, false); + attachPregenFlags(radius, false, false, false, false, false); + RequiredArgumentBuilder dimension = Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES) + .executes((CommandContext context) -> ModdedPregenCommands.pregenStart(context, true, false, false, false, false)); + attachPregenCenter(dimension, true); + attachPregenFlags(dimension, true, false, false, false, false); + radius.then(dimension); + + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)) + .then(Commands.literal("start") + .then(radius)) + .then(Commands.literal("stop") + .executes((CommandContext context) -> ModdedPregenCommands.pregenStop(context.getSource()))) + .then(Commands.literal("x") + .executes((CommandContext context) -> ModdedPregenCommands.pregenStop(context.getSource()))) + .then(Commands.literal("pause") + .executes((CommandContext context) -> ModdedPregenCommands.pregenPause(context.getSource()))) + .then(Commands.literal("resume") + .executes((CommandContext context) -> ModdedPregenCommands.pregenPause(context.getSource()))) + .then(Commands.literal("status") + .executes((CommandContext context) -> ModdedPregenCommands.pregenStatus(context.getSource()))); + } + + private static void attachPregenCenter(ArgumentBuilder node, boolean withDimension) { + RequiredArgumentBuilder z = Commands.argument("z", IntegerArgumentType.integer()) + .executes((CommandContext context) -> ModdedPregenCommands.pregenStart(context, withDimension, true, false, false, false)); + attachPregenFlags(z, withDimension, true, false, false, false); + node.then(Commands.literal("at") + .then(Commands.argument("x", IntegerArgumentType.integer()) + .then(z))); + } + + private static void attachPregenFlags(ArgumentBuilder node, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) { + if (!gui) { + node.then(pregenFlagNode("gui", withDimension, withCenter, true, sync, nocache)); + } + if (!sync) { + node.then(pregenFlagNode("sync", withDimension, withCenter, gui, true, nocache)); + } + if (!nocache) { + node.then(pregenFlagNode("nocache", withDimension, withCenter, gui, sync, true)); + } + } + + private static LiteralArgumentBuilder pregenFlagNode(String name, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) { + LiteralArgumentBuilder flag = Commands.literal(name) + .executes((CommandContext context) -> ModdedPregenCommands.pregenStart(context, withDimension, withCenter, gui, sync, nocache)); + attachPregenFlags(flag, withDimension, withCenter, gui, sync, nocache); + return flag; + } + + private static LiteralArgumentBuilder goldenhashTree(String name) { + LiteralArgumentBuilder radiusAndThreads = Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO)); + attachModes(radiusAndThreads, (CommandContext context) -> 8, (CommandContext context) -> 8); + + com.mojang.brigadier.builder.RequiredArgumentBuilder radius = Commands.argument("radius", IntegerArgumentType.integer(0, 256)) + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 8, ModdedGoldenHash.Mode.AUTO)); + attachModes(radius, (CommandContext context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext context) -> 8); + + com.mojang.brigadier.builder.RequiredArgumentBuilder threads = Commands.argument("threads", IntegerArgumentType.integer(1, 64)) + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), IntegerArgumentType.getInteger(context, "threads"), ModdedGoldenHash.Mode.AUTO)); + attachModes(threads, (CommandContext context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext context) -> IntegerArgumentType.getInteger(context, "threads")); + + radius.then(threads); + radiusAndThreads.then(radius); + return radiusAndThreads; + } + + private interface IntExtractor { + int extract(CommandContext context); + } + + private static void attachModes(com.mojang.brigadier.builder.ArgumentBuilder node, IntExtractor radius, IntExtractor threads) { + node.then(Commands.literal("capture") + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.CAPTURE))); + node.then(Commands.literal("verify") + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.VERIFY))); + } + + private static LiteralArgumentBuilder editTree() { + return Commands.literal("edit").requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "edit")) + .then(Commands.literal("biome") + .executes((CommandContext context) -> ModdedEditCommands.editBiome(context.getSource(), null)) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS) + .executes((CommandContext context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("b") + .executes((CommandContext context) -> ModdedEditCommands.editBiome(context.getSource(), null)) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS) + .executes((CommandContext context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("region") + .executes((CommandContext context) -> ModdedEditCommands.editRegion(context.getSource(), null)) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS) + .executes((CommandContext context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("r") + .executes((CommandContext context) -> ModdedEditCommands.editRegion(context.getSource(), null)) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS) + .executes((CommandContext context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("dimension") + .executes((CommandContext context) -> ModdedEditCommands.editDimension(context.getSource()))) + .then(Commands.literal("d") + .executes((CommandContext context) -> ModdedEditCommands.editDimension(context.getSource()))); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java new file mode 100644 index 000000000..ad3b7d92f --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java @@ -0,0 +1,294 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedServerLevels; +import art.arcane.volmlib.util.collection.KList; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.LevelResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Predicate; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class ModdedDatapackCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + private static final String WORLD_PACK_NAME = "iris"; + + private ModdedDatapackCommands() { + } + + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); + + root.then(Commands.literal("status") + .executes((CommandContext context) -> status(context.getSource()))); + + root.then(Commands.literal("install") + .executes((CommandContext context) -> install(context.getSource()))); + + root.then(Commands.literal("list") + .executes((CommandContext context) -> list(context.getSource()))); + root.then(Commands.literal("ls") + .executes((CommandContext context) -> list(context.getSource()))); + + root.then(message("ingest", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions.")); + root.then(message("pull", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions.")); + + root.then(message("remove", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart.")); + root.then(message("rm", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart.")); + + return root; + } + + private static LiteralArgumentBuilder message(String name, String text) { + return Commands.literal(name) + .executes((CommandContext context) -> { + IrisModdedCommands.fail(context.getSource(), text); + return 0; + }) + .then(Commands.argument("args", StringArgumentType.greedyString()) + .executes((CommandContext context) -> { + IrisModdedCommands.fail(context.getSource(), text); + return 0; + })); + } + + private static File worldDatapacksFolder(MinecraftServer server) { + return server.getWorldPath(LevelResource.DATAPACK_DIR).toFile(); + } + + private static File overrideFile(MinecraftServer server, String dimensionKey) { + return new File(worldDatapacksFolder(server), WORLD_PACK_NAME + "/data/irisworldgen/dimension_type/" + IrisDimension.sanitizeDimensionTypeKeyValue(dimensionKey) + ".json"); + } + + private static int status(CommandSourceStack source) { + MinecraftServer server = source.getServer(); + int irisLevels = 0; + int mismatches = 0; + for (ServerLevel level : ModdedServerLevels.levels(server)) { + if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator irisGenerator)) { + continue; + } + irisLevels++; + String dimensionId = level.dimension().identifier().toString(); + String typeKey = level.dimensionTypeRegistration().unwrapKey() + .map((net.minecraft.resources.ResourceKey key) -> key.identifier().toString()) + .orElse("inline"); + DimensionType active = level.dimensionType(); + int activeMin = active.minY(); + int activeMax = active.minY() + active.height(); + + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null || engine.getDimension() == null) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_TYPE_ACTIVE_LOGICAL_PACK_ENGINE_NOT_STARTED_PACK_HEIGHTS_UNKNOWN, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("typeKey", typeKey), MessageArgument.untrusted("activeMin", activeMin), MessageArgument.untrusted("activeMax", activeMax), MessageArgument.untrusted("value", active.logicalHeight()), MessageArgument.untrusted("value2", irisGenerator.dimensionKey()))); + continue; + } + IrisDimension dimension = engine.getDimension(); + int packMin = dimension.getMinHeight(); + int packMax = dimension.getMaxHeight(); + int packLogical = dimension.getLogicalHeight(); + boolean matches = packMin == activeMin && packMax == activeMax && packLogical == active.logicalHeight(); + File override = overrideFile(server, irisGenerator.dimensionKey()); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_TYPE_ACTIVE_LOGICAL_PACK_WANTS_LOGICAL, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("typeKey", typeKey), MessageArgument.untrusted("activeMin", activeMin), MessageArgument.untrusted("activeMax", activeMax), MessageArgument.untrusted("value", active.logicalHeight()), MessageArgument.untrusted("value2", dimension.getLoadKey()), MessageArgument.untrusted("packMin", packMin), MessageArgument.untrusted("packMax", packMax), MessageArgument.untrusted("packLogical", packLogical), MessageArgument.trusted("value3", IrisLanguage.plain(matches ? RuntimeUiMessages.STATUS_MATCH : RuntimeUiMessages.STATUS_MISMATCH)), MessageArgument.trusted("value4", override.isFile() ? IrisLanguage.plain(RuntimeUiMessages.DATAPACK_OVERRIDE_SUFFIX) : ""))); + if (!matches) { + mismatches++; + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_WARNING_ACTIVE_DIMENSION_TYPE_DOES_NOT_MATCH_PACK_IRIS_WILL)); + } + } + if (irisLevels == 0) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_NO_IRIS_DIMENSIONS_ARE_LOADED)); + return 0; + } + if (mismatches == 0) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_ALL_IRIS_DIMENSION_S_MATCH_THEIR_PACK_HEIGHT_RANGES, MessageArgument.untrusted("irisLevels", irisLevels))); + } + return 1; + } + + private static int install(CommandSourceStack source) { + MinecraftServer server = source.getServer(); + List written = new ArrayList<>(); + for (ServerLevel level : ModdedServerLevels.levels(server)) { + if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator irisGenerator)) { + continue; + } + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null || engine.getDimension() == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_ENGINE_NOT_STARTED_CANNOT_DERIVE_ITS_DIMENSION_TYPE_YET, MessageArgument.untrusted("value", level.dimension().identifier()))); + continue; + } + IrisDimension dimension = engine.getDimension(); + String json; + try { + json = dimension.getDimensionType().toJson(DataVersion.getLatest().get()); + } catch (Throwable e) { + LOGGER.error("Iris dimension type generation failed for {}", dimension.getLoadKey(), e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_DIMENSION_TYPE_GENERATION_FAILED, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage())))); + continue; + } + File output = overrideFile(server, irisGenerator.dimensionKey()); + try { + output.getParentFile().mkdirs(); + Files.writeString(output.toPath(), json, StandardCharsets.UTF_8); + written.add(output.getPath()); + } catch (IOException e) { + LOGGER.error("Iris dimension type write failed for {}", output, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_FAILED_WRITE, MessageArgument.untrusted("output", output), MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + } + } + if (written.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_NO_IRIS_DIMENSIONS_WITH_RUNNING_ENGINES_FOUND_NOTHING_WAS_INSTALLED)); + return 0; + } + + File mcmeta = new File(worldDatapacksFolder(server), WORLD_PACK_NAME + "/pack.mcmeta"); + int packFormat = DataVersion.getLatest().getPackFormat(); + String meta = "{\n" + + " \"pack\": {\n" + + " \"description\": \"Iris dimension types derived from the installed Iris packs.\",\n" + + " \"pack_format\": " + packFormat + ",\n" + + " \"min_format\": " + packFormat + ",\n" + + " \"max_format\": " + packFormat + "\n" + + " }\n" + + "}\n"; + try { + mcmeta.getParentFile().mkdirs(); + Files.writeString(mcmeta.toPath(), meta, StandardCharsets.UTF_8); + written.add(mcmeta.getPath()); + } catch (IOException e) { + LOGGER.error("Iris pack.mcmeta write failed for {}", mcmeta, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_FAILED_WRITE_2, MessageArgument.untrusted("mcmeta", mcmeta), MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + return 0; + } + + for (String path : written) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_WROTE, MessageArgument.untrusted("path", path))); + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_WORLD_DATAPACK_DIMENSION_TYPE_OVERRIDES_INSTALLED_RESTART_SERVER_DIMENSION_TYPES, MessageArgument.untrusted("WORLDPACKNAME", WORLD_PACK_NAME))); + return 1; + } + + private static int list(CommandSourceStack source) { + MinecraftServer server = source.getServer(); + LinkedHashSet configured = new LinkedHashSet<>(); + File packsRoot = ModdedPackCommands.packsRoot(); + for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsRoot)) { + if (!new File(pack, "dimensions").isDirectory()) { + continue; + } + try { + IrisData data = IrisData.get(pack); + for (IrisDimension dimension : data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys())) { + if (dimension == null || dimension.getDatapackImports() == null) { + continue; + } + for (String url : dimension.getDatapackImports()) { + if (url != null && !url.isBlank()) { + configured.add(url.trim()); + } + } + } + } catch (Throwable e) { + LOGGER.error("Iris datapack import scan failed for pack {}", pack.getName(), e); + } + } + + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_CONFIGURED_DATAPACK_IMPORTS, MessageArgument.untrusted("value", configured.size()))); + for (String url : configured) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_MESSAGE, MessageArgument.untrusted("url", url))); + } + if (!configured.isEmpty()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_MODRINTH_INGEST_IS_BUKKIT_ONLY_INSTALL_THESE_MANUALLY_INTO_WORLD)); + } + + File datapacks = worldDatapacksFolder(server); + File[] installed = datapacks.isDirectory() + ? datapacks.listFiles(file -> file.isDirectory() || file.isFile() && file.getName().toLowerCase(Locale.ROOT).endsWith(".zip")) + : null; + Set availableIds = new HashSet<>(server.getPackRepository().getAvailableIds()); + Set selectedIds = new HashSet<>(server.getPackRepository().getSelectedIds()); + KList names = new KList<>(); + if (installed != null) { + for (File installedPack : installed) { + String name = installedPack.getName(); + String repositoryId = resolveRepositoryId(name, availableIds); + String state; + if (repositoryId == null) { + state = "unavailable"; + } else if (selectedIds.contains(repositoryId)) { + state = "enabled"; + } else { + state = "disabled"; + } + names.add(name + " [" + state + "]"); + } + } + Collections.sort(names); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_INSTALLED_WORLD_DATAPACKS, MessageArgument.untrusted("value", names.size()))); + for (String name : names) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_MESSAGE_2, MessageArgument.untrusted("name", name))); + } + return 1; + } + + private static String resolveRepositoryId(String filename, Set availableIds) { + String direct = "file/" + filename; + if (availableIds.contains(direct)) { + return direct; + } + for (String availableId : availableIds) { + if (availableId.equals(filename) || availableId.endsWith("/" + filename)) { + return availableId; + } + } + return null; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java new file mode 100644 index 000000000..d0ec4e459 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java @@ -0,0 +1,79 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.spi.IrisPlatforms; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.function.Predicate; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +final class ModdedDeveloperCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + + private ModdedDeveloperCommands() { + } + + static LiteralArgumentBuilder tree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "developer")) + .then(Commands.literal("sentry") + .executes((CommandContext context) -> sentry(context.getSource()))) + .then(Commands.literal("network") + .executes((CommandContext context) -> network(context.getSource()))) + .then(Commands.literal("ip") + .executes((CommandContext context) -> network(context.getSource()))); + } + + private static int sentry(CommandSourceStack source) { + IrisPlatforms.get().reportError(new Exception("This is an Iris Sentry test exception")); + ModdedCommandFeedback.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DEVELOPER_COMMANDS_DISPATCHED_TEST_EXCEPTION_IRIS_ERROR_REPORTER_SENTRY_IF_ENABLED)); + return 1; + } + + private static int network(CommandSourceStack source) { + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + for (NetworkInterface networkInterface : Collections.list(interfaces)) { + ModdedCommandFeedback.ok(source, networkInterface.getDisplayName()); + for (InetAddress address : Collections.list(networkInterface.getInetAddresses())) { + ModdedCommandFeedback.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DEVELOPER_COMMANDS_MESSAGE, MessageArgument.untrusted("value", address.getHostAddress()))); + } + } + return 1; + } catch (SocketException error) { + LOGGER.error("Iris developer network dump failed", error); + ModdedCommandFeedback.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DEVELOPER_COMMANDS_NETWORK_SCAN_FAILED, MessageArgument.untrusted("value", error.getClass().getSimpleName()))); + return 0; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java new file mode 100644 index 000000000..e1857c67f --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java @@ -0,0 +1,493 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +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.IrisRegion; +import art.arcane.iris.modded.ModdedBlockState; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedScheduler; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.localization.MessageArgument; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.level.biome.Biome; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +public final class ModdedDustRevealer { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int MAX_HITS = 2_048; + private static final int PARTICLE_BATCH_SIZE = 64; + private static final DustParticleOptions REVEAL_DUST = new DustParticleOptions(0xFFD24A, 1.2F); + private static final ConcurrentHashMap ACTIVE_RUNS = new ConcurrentHashMap<>(); + + private ModdedDustRevealer() { + } + + public static void reveal(ServerPlayer player, ServerLevel level, BlockPos pos) { + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + player.sendSystemMessage(Component.literal( + IrisLanguage.plain(RuntimeUiMessages.DUST_IRIS_WORLD_REQUIRED))); + return; + } + describe(player, level, engine, pos); + + int relativeY = pos.getY() - engine.getMinHeight(); + String key = safe( + "object lookup at " + coordinates(pos), + () -> engine.getObjectPlacementKey(pos.getX(), relativeY, pos.getZ())); + if (key == null) { + return; + } + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + player.sendSystemMessage(Component.literal( + IrisLanguage.plain(RuntimeUiMessages.DUST_REVEAL_FAILED))); + return; + } + level.playSound(null, pos, SoundEvents.LODESTONE_COMPASS_LOCK, + SoundSource.PLAYERS, 1.0F, 0.1F); + player.sendSystemMessage(Component.literal(IrisLanguage.plain( + RuntimeUiMessages.DUST_FOUND_OBJECT, + MessageArgument.untrusted("object", key) + ))); + + RevealRun run = new RevealRun( + player.getUUID(), + player, + level, + engine, + pos.immutable(), + key, + level.getMinY(), + level.getMaxY() + 1, + new AtomicBoolean()); + RevealRun previous = ACTIVE_RUNS.put(player.getUUID(), run); + if (previous != null) { + previous.cancelled().set(true); + } + scheduler.async(() -> discover(scheduler, run)); + } + + static void clear() { + for (RevealRun run : ACTIVE_RUNS.values()) { + run.cancelled().set(true); + } + ACTIVE_RUNS.clear(); + } + + private static void discover(ModdedScheduler scheduler, RevealRun run) { + try (GenerationSessionLease lease = run.engine().acquireGenerationLease("modded_dust_reveal"); + IrisContext.Scope ignored = IrisContext.open(run.engine(), lease.sessionId(), null)) { + List hits = collect(run); + if (!run.cancelled().get()) { + scheduler.global(() -> revealBatch(scheduler, run, hits, 0)); + } + } catch (GenerationSessionException error) { + revealFailure(scheduler, run, error); + } catch (Throwable error) { + revealFailure(scheduler, run, error); + } + } + + static List collect(RevealRun run) { + return collect( + run.origin(), + run.key(), + run.engine().getMinHeight(), + run.minY(), + run.maxYExclusive(), + run.cancelled(), + (int x, int relativeY, int z) -> + run.engine().getObjectPlacementKey(x, relativeY, z)); + } + + static List collect(BlockPos origin, String key, int engineMinY, + int minY, int maxYExclusive, AtomicBoolean cancelled, + ObjectPlacementLookup lookup) { + List hits = new ArrayList<>(); + Set visited = new HashSet<>(); + Deque frontier = new ArrayDeque<>(); + frontier.add(origin); + visited.add(origin); + while (!frontier.isEmpty() && hits.size() < MAX_HITS && !cancelled.get()) { + BlockPos current = frontier.poll(); + hits.add(current); + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dy == 0 && dz == 0) { + continue; + } + BlockPos next = current.offset(dx, dy, dz); + if (next.getY() < minY + || next.getY() >= maxYExclusive + || !visited.add(next)) { + continue; + } + String nextKey = lookup.at( + next.getX(), next.getY() - engineMinY, next.getZ()); + if (key.equals(nextKey)) { + frontier.add(next); + } + } + } + } + } + return List.copyOf(hits); + } + + private static void revealBatch(ModdedScheduler scheduler, RevealRun run, + List hits, int from) { + if (!active(run)) { + // Drop the registry entry on abort too, or the run record pins the player, level + // and engine until server stop. No-op if a newer run already replaced it. + ACTIVE_RUNS.remove(run.playerId(), run); + return; + } + int to = Math.min(hits.size(), from + PARTICLE_BATCH_SIZE); + for (int index = from; index < to; index++) { + BlockPos hit = hits.get(index); + run.level().sendParticles(run.player(), REVEAL_DUST, true, true, + hit.getX() + 0.5D, hit.getY() + 0.5D, hit.getZ() + 0.5D, + 3, 0.25D, 0.25D, 0.25D, 0.0D); + } + if (to > from) { + BlockPos soundAt = hits.get(from); + run.level().playSound(null, soundAt, SoundEvents.AMETHYST_BLOCK_CHIME, + SoundSource.PLAYERS, 0.5F, + ThreadLocalRandom.current().nextFloat(0.2F, 2.0F)); + } + if (to < hits.size()) { + scheduler.laterGlobal(() -> revealBatch(scheduler, run, hits, to), 1); + return; + } + ACTIVE_RUNS.remove(run.playerId(), run); + run.player().sendSystemMessage(Component.literal(IrisLanguage.plain( + hits.size() >= MAX_HITS + ? RuntimeUiMessages.DUST_REVEALED_CAPPED + : RuntimeUiMessages.DUST_REVEALED, + MessageArgument.trusted("count", hits.size()), + MessageArgument.untrusted("object", run.key()) + ))); + } + + private static boolean active(RevealRun run) { + return !run.cancelled().get() + && ACTIVE_RUNS.get(run.playerId()) == run + && !run.player().hasDisconnected() + && !run.player().isRemoved() + && run.player().level() == run.level() + && !run.engine().isClosing() + && !run.engine().isClosed(); + } + + private static void revealFailure(ModdedScheduler scheduler, RevealRun run, Throwable error) { + LOGGER.error("Iris dust reveal failed for {} at {}", run.key(), coordinates(run.origin()), error); + scheduler.global(() -> { + if (ACTIVE_RUNS.remove(run.playerId(), run)) { + run.player().sendSystemMessage(Component.literal( + IrisLanguage.plain(RuntimeUiMessages.DUST_REVEAL_FAILED))); + } + }); + } + + private static void describe(ServerPlayer player, ServerLevel level, Engine engine, BlockPos pos) { + int x = pos.getX(); + int y = pos.getY(); + int z = pos.getZ(); + int minHeight = engine.getMinHeight(); + int relativeY = y - minHeight; + Integer surfaceRelative = safe( + "surface height lookup at " + coordinates(pos), + () -> engine.getHeight(x, z, true)); + Integer surfaceY = surfaceRelative == null ? null : surfaceRelative + minHeight; + Integer offset = surfaceY == null ? null : y - surfaceY; + + String objectKey = safe( + "object lookup at " + coordinates(pos), + () -> engine.getObjectPlacementKey(x, relativeY, z)); + IrisBiome surfaceBiome = safe( + "surface biome lookup at " + x + ", " + z, + () -> engine.getSurfaceBiome(x, z)); + IrisBiome biomeHere = safe( + "biome lookup at " + coordinates(pos), + () -> engine.getBiome(x, relativeY, z)); + IrisBiome caveBiome = safe( + "cave biome lookup at " + coordinates(pos), + () -> engine.getCaveOrMantleBiome(x, relativeY, z)); + IrisRegion region = safe( + "region lookup at " + x + ", " + z, + () -> engine.getRegion(x, z)); + + List lines = new ArrayList<>(); + lines.add(new DustLine(IrisLanguage.plain( + RuntimeUiMessages.DUST_HEADER, + MessageArgument.trusted("x", x), + MessageArgument.trusted("y", y), + MessageArgument.trusted("z", z) + ), false)); + lines.add(new DustLine(IrisLanguage.plain( + RuntimeUiMessages.DUST_BLOCK, + MessageArgument.untrusted("block", ModdedBlockState.serialize(level.getBlockState(pos))) + ), false)); + if (offset != null && surfaceY != null) { + lines.add(new DustLine(positionLine(offset, surfaceY), true)); + lines.add(new DustLine( + placementLine(offset, surfaceRelative, relativeY, objectKey), true)); + } + lines.add(new DustLine(IrisLanguage.plain( + RuntimeUiMessages.DUST_OBJECT_AT_BLOCK, + MessageArgument.untrusted( + "object", + objectKey == null + ? IrisLanguage.plain(RuntimeUiMessages.DUST_NONE) + : objectKey) + ), false)); + if (objectKey == null) { + String columnObject = findColumnObject(engine, x, relativeY, z, minHeight); + lines.add(new DustLine(IrisLanguage.plain( + columnObject == null + ? RuntimeUiMessages.DUST_COLUMN_OBJECT_NONE + : RuntimeUiMessages.DUST_COLUMN_OBJECT, + MessageArgument.trusted( + columnObject == null ? "detail" : "object", + columnObject == null + ? IrisLanguage.plain(RuntimeUiMessages.DUST_COLUMN_NONE) + : columnObject) + ), false)); + } + if (surfaceBiome != null) { + lines.add(new DustLine(IrisLanguage.plain( + RuntimeUiMessages.DUST_SURFACE_BIOME_DETAIL, + MessageArgument.untrusted("biome", surfaceBiome.getLoadKey()), + MessageArgument.untrusted("derivative", surfaceBiome.getDerivativeKey()) + ), false)); + } + if (biomeHere != null + && (surfaceBiome == null + || !biomeHere.getLoadKey().equals(surfaceBiome.getLoadKey()))) { + lines.add(new DustLine(IrisLanguage.plain( + RuntimeUiMessages.DUST_BIOME_AT_Y, + MessageArgument.untrusted("biome", biomeHere.getLoadKey()) + ), false)); + } + if (caveBiome != null + && (surfaceBiome == null + || !caveBiome.getLoadKey().equals(surfaceBiome.getLoadKey()))) { + lines.add(new DustLine(IrisLanguage.plain( + RuntimeUiMessages.DUST_CAVE_BIOME, + MessageArgument.untrusted("biome", caveBiome.getLoadKey()) + ), false)); + } + NativeBiome nativeBiome = nativeBiome(level, pos); + lines.add(new DustLine(IrisLanguage.plain( + RuntimeUiMessages.DUST_SERVER_BIOME, + MessageArgument.untrusted("biome", nativeBiome.key()), + MessageArgument.trusted("id", nativeBiome.id()) + ), false)); + if (region != null) { + lines.add(new DustLine(IrisLanguage.plain( + RuntimeUiMessages.DUST_REGION, + MessageArgument.untrusted("region", region.getLoadKey()), + MessageArgument.untrusted("name", region.getName()) + ), false)); + } + Set objects = safe( + "chunk object lookup at " + (x >> 4) + ", " + (z >> 4), + () -> engine.getObjectsAt(x >> 4, z >> 4)); + if (objects != null && !objects.isEmpty()) { + lines.add(new DustLine(IrisLanguage.plain( + RuntimeUiMessages.DUST_OBJECTS_IN_CHUNK, + MessageArgument.untrusted("objects", objects.stream().sorted().toList()) + ), false)); + } + sendReport(player, lines); + } + + static String positionLine(int offset, int surfaceY) { + if (offset > 0) { + return IrisLanguage.plain( + RuntimeUiMessages.DUST_POSITION_ABOVE, + MessageArgument.trusted("offset", offset), + MessageArgument.trusted("surfaceY", surfaceY)); + } + if (offset < 0) { + return IrisLanguage.plain( + RuntimeUiMessages.DUST_POSITION_BELOW, + MessageArgument.trusted("offset", -offset), + MessageArgument.trusted("surfaceY", surfaceY)); + } + return IrisLanguage.plain( + RuntimeUiMessages.DUST_POSITION_AT, + MessageArgument.trusted("surfaceY", surfaceY)); + } + + static String placementLine(int offset, int surfaceRelative, + int relativeY, String objectKey) { + if (offset > 0) { + return objectKey == null + ? IrisLanguage.plain(RuntimeUiMessages.DUST_PLACED_BY_DECORATION_ABOVE) + : IrisLanguage.plain( + RuntimeUiMessages.DUST_PLACED_BY_OBJECT_ABOVE, + MessageArgument.untrusted("object", objectKey)); + } + if (objectKey != null) { + return IrisLanguage.plain( + RuntimeUiMessages.DUST_PLACED_BY_BURIED_OBJECT, + MessageArgument.untrusted("object", objectKey)); + } + return IrisLanguage.plain( + RuntimeUiMessages.DUST_PLACED_BY_TERRAIN, + MessageArgument.trusted("depth", Math.max(0, surfaceRelative - relativeY))); + } + + private static String findColumnObject(Engine engine, int x, int relativeY, + int z, int minHeight) { + int maxRelativeY = engine.getMaxHeight() - minHeight - 1; + try { + for (int dy = 1; dy <= 64; dy++) { + if (relativeY + dy <= maxRelativeY) { + String up = engine.getObjectPlacementKey(x, relativeY + dy, z); + if (up != null) { + return IrisLanguage.plain( + RuntimeUiMessages.DUST_COLUMN_ABOVE, + MessageArgument.untrusted("object", up), + MessageArgument.trusted("y", relativeY + dy + minHeight)); + } + } + if (relativeY - dy >= 0) { + String down = engine.getObjectPlacementKey(x, relativeY - dy, z); + if (down != null) { + return IrisLanguage.plain( + RuntimeUiMessages.DUST_COLUMN_BELOW, + MessageArgument.untrusted("object", down), + MessageArgument.trusted("y", relativeY - dy + minHeight)); + } + } + } + } catch (Throwable error) { + LOGGER.error("Iris dust column-object lookup failed at {}, {}, {}", + x, relativeY + minHeight, z, error); + } + return null; + } + + private static NativeBiome nativeBiome(ServerLevel level, BlockPos pos) { + Holder holder = level.getBiome(pos); + String key = holder.unwrapKey() + .map((ResourceKey resourceKey) -> resourceKey.identifier().toString()) + .orElse(IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED)); + Registry registry = level.registryAccess().lookupOrThrow(Registries.BIOME); + return new NativeBiome(key, registry.getId(holder.value())); + } + + private static void sendReport(ServerPlayer player, List lines) { + StringBuilder payload = new StringBuilder(); + for (int index = 0; index < lines.size(); index++) { + DustLine line = lines.get(index); + ChatFormatting color = index == 0 + ? ChatFormatting.GOLD + : line.emphasis() ? ChatFormatting.YELLOW : ChatFormatting.WHITE; + player.sendSystemMessage(Component.literal(line.text()).withStyle(color)); + if (index > 0) { + payload.append('\n'); + } + payload.append(line.text()); + } + MutableComponent hover = Component.literal( + IrisLanguage.plain(RuntimeUiMessages.DUST_COPY_HOVER)); + MutableComponent button = Component.literal( + IrisLanguage.plain(RuntimeUiMessages.DUST_COPY_BUTTON)) + .withStyle(ChatFormatting.GREEN) + .withStyle(style -> style + .withClickEvent(new ClickEvent.CopyToClipboard(payload.toString())) + .withHoverEvent(new HoverEvent.ShowText(hover))); + player.sendSystemMessage(button); + } + + private static T safe(String operation, Supplier supplier) { + try { + return supplier.get(); + } catch (Throwable error) { + LOGGER.error("Iris dust {} failed", operation, error); + return null; + } + } + + private static String coordinates(BlockPos pos) { + return pos.getX() + ", " + pos.getY() + ", " + pos.getZ(); + } + + private record DustLine(String text, boolean emphasis) { + } + + private record NativeBiome(String key, int id) { + } + + @FunctionalInterface + interface ObjectPlacementLookup { + String at(int x, int relativeY, int z); + } + + static record RevealRun( + UUID playerId, + ServerPlayer player, + ServerLevel level, + Engine engine, + BlockPos origin, + String key, + int minY, + int maxYExclusive, + AtomicBoolean cancelled + ) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java new file mode 100644 index 000000000..fbcfa1876 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java @@ -0,0 +1,133 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.gui.GuiHost; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.volmlib.util.localization.MessageArgument; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Desktop; +import java.io.File; + +final class ModdedEditCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private ModdedEditCommands() { + } + + static int editBiome(CommandSourceStack source, String key) { + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS)); + return 0; + } + IrisBiome biome; + if (key == null || key.isBlank()) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_BIOME_IRIS_EDIT_BIOME_KEY)); + return 0; + } + BlockPos pos = player.blockPosition(); + try { + biome = engine.getBiome(pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ()); + } catch (Throwable e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))); + return 0; + } + } else { + biome = engine.getData().getBiomeLoader().load(key.trim()); + if (biome == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME, MessageArgument.untrusted("key", key))); + return 0; + } + } + return openJson(source, biome); + } + + static int editRegion(CommandSourceStack source, String key) { + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_2)); + return 0; + } + IrisRegion region; + if (key == null || key.isBlank()) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_REGION_IRIS_EDIT_REGION_KEY)); + return 0; + } + BlockPos pos = player.blockPosition(); + try { + region = engine.getRegion(pos.getX(), pos.getZ()); + } catch (Throwable e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))); + return 0; + } + } else { + region = engine.getData().getRegionLoader().load(key.trim()); + if (region == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION, MessageArgument.untrusted("key", key))); + return 0; + } + } + return openJson(source, region); + } + + static int editDimension(CommandSourceStack source) { + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_3)); + return 0; + } + return openJson(source, engine.getDimension()); + } + + private static int openJson(CommandSourceStack source, IrisRegistrant registrant) { + if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_OPEN_FILES_HERE, MessageArgument.untrusted("value", ModdedGuiHost.guiUnavailableReason()))); + return 0; + } + if (registrant == null || registrant.getLoadFile() == null || !registrant.getLoadFile().isFile()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM)); + return 0; + } + File file = registrant.getLoadFile(); + try { + Desktop.getDesktop().open(file); + } catch (Throwable e) { + LOGGER.error("Iris edit failed to open {}", file, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_OPEN, MessageArgument.untrusted("value", file.getName()), MessageArgument.untrusted("value2", e.getClass().getSimpleName()))); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_OPENING_YOUR_EDITOR, MessageArgument.untrusted("value", registrant.getTypeName()), MessageArgument.untrusted("value2", file.getName()))); + return 1; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGoldenHash.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGoldenHash.java new file mode 100644 index 000000000..6b439b2ef --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGoldenHash.java @@ -0,0 +1,203 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.runtime.GoldenHashEngine; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.modded.ModdedBlockBuffer; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class ModdedGoldenHash { + public enum Mode { + AUTO, + CAPTURE, + VERIFY + } + + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final AtomicBoolean ACTIVE = new AtomicBoolean(false); + + private final CommandSourceStack source; + private final MinecraftServer server; + private final Engine engine; + private final GoldenHashEngine hashEngine; + + private ModdedGoldenHash(CommandSourceStack source, ServerLevel level, Engine engine, int radius, int threads, Mode mode) { + this.source = source; + this.server = source.getServer(); + this.engine = engine; + GoldenHashEngine.Mode engineMode = switch (mode) { + case AUTO -> GoldenHashEngine.Mode.AUTO; + case CAPTURE -> GoldenHashEngine.Mode.CAPTURE; + case VERIFY -> GoldenHashEngine.Mode.VERIFY; + }; + File goldenDir = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("golden").toFile(); + GoldenHashEngine.Request request = new GoldenHashEngine.Request( + engine.getWorld().name(), + engine.getSeedManager().getSeed(), + ModdedEngineBootstrap.loader().minecraftVersion(), + engine.getMinHeight(), + engine.getMaxHeight(), + 0, + 0, + radius, + threads, + engineMode, + true, + false, + GoldenHashEngine.FALLBACK_BIOME_KEY); + this.hashEngine = new GoldenHashEngine(engine, request, goldenDir, this::snapshot, feedback(), progress()); + } + + public static void start(CommandSourceStack source, ServerLevel level, Engine engine, int radius, int threads, Mode mode) { + if (!ACTIVE.compareAndSet(false, true)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_GOLDEN_HASH_GOLDENHASH_SCAN_IS_ALREADY_RUNNING)); + return; + } + ModdedGoldenHash scan = new ModdedGoldenHash(source, level, engine, radius, threads, mode); + int boundedRadius = Math.max(0, radius); + int chunks = (boundedRadius * 2 + 1) * (boundedRadius * 2 + 1); + scan.ok(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_STARTED, + MessageArgument.trusted("chunks", chunks), + MessageArgument.trusted("threads", Math.max(1, threads)), + MessageArgument.untrusted("mode", mode) + )); + LOGGER.info("goldenhash start: dim={} seed={} radius={} threads={} mode={} file={}", + engine.getDimension().getLoadKey(), engine.getSeedManager().getSeed(), boundedRadius, Math.max(1, threads), mode, scan.hashEngine.getGoldenFile().getName()); + Thread thread = new Thread(() -> { + try { + scan.hashEngine.run(); + } finally { + ACTIVE.set(false); + } + }, "Iris GoldenHash"); + thread.setDaemon(true); + thread.start(); + } + + private GoldenHashEngine.ChunkSnapshot snapshot(int chunkX, int chunkZ) throws Exception { + int minY = engine.getMinHeight(); + int height = engine.getMaxHeight() - minY; + PlatformBlockState air = IrisPlatforms.get().registries().air(); + ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air); + Hunk biomes = Hunk.newArrayHunk(16, height, 16); + engine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false); + return new GoldenHashEngine.ChunkSnapshot() { + @Override + public int minY() { + return minY; + } + + @Override + public int maxY() { + return minY + height; + } + + @Override + public PlatformBlockState block(int x, int y, int z) { + return blocks.get(x, y - minY, z); + } + + @Override + public PlatformBiome biome(int x, int y, int z) { + return biomes.get(x, y - minY, z); + } + }; + } + + private GoldenHashEngine.Feedback feedback() { + return new GoldenHashEngine.Feedback() { + @Override + public void ok(String message) { + ModdedGoldenHash.this.ok(message); + } + + @Override + public void warn(String message) { + ModdedGoldenHash.this.ok(message); + } + + @Override + public void fail(String message) { + ModdedGoldenHash.this.fail(message); + } + }; + } + + private GoldenHashEngine.Progress progress() { + return new GoldenHashEngine.Progress() { + private final AtomicInteger hashed = new AtomicInteger(); + + @Override + public void chunkDone(int chunkX, int chunkZ, boolean ok, int done, int total) { + if (!ok) { + return; + } + int doneCount = hashed.incrementAndGet(); + int stride = total <= 64 ? 1 : 32; + if (doneCount % stride == 0 || doneCount == total) { + ModdedGoldenHash.this.ok(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_CHUNK_HASHED, + MessageArgument.trusted("done", doneCount), + MessageArgument.trusted("total", total), + MessageArgument.trusted("x", chunkX), + MessageArgument.trusted("z", chunkZ) + )); + } + } + + @Override + public void chunkFailed(int chunkX, int chunkZ, Throwable error) { + LOGGER.error("goldenhash chunk {},{} failed", chunkX, chunkZ, error); + ModdedGoldenHash.this.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_CHUNK_FAILED, + MessageArgument.trusted("x", chunkX), + MessageArgument.trusted("z", chunkZ), + MessageArgument.untrusted("type", error.getClass().getSimpleName()) + )); + } + }; + } + + private void ok(String message) { + server.execute(() -> IrisModdedCommands.ok(source, message)); + } + + private void fail(String message) { + server.execute(() -> IrisModdedCommands.fail(source, message)); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGuiHost.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGuiHost.java new file mode 100644 index 000000000..3f2b22bd1 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGuiHost.java @@ -0,0 +1,120 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.gui.GuiHost; +import art.arcane.iris.core.gui.GuiOverlay; +import art.arcane.iris.engine.framework.Engine; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public final class ModdedGuiHost implements GuiHost.Provider { + private static final ModdedGuiHost INSTANCE = new ModdedGuiHost(); + + private final Map levels = new ConcurrentHashMap<>(); + private final Map openers = new ConcurrentHashMap<>(); + private volatile Engine active; + private volatile MinecraftServer server; + + private ModdedGuiHost() { + } + + public static void install() { + GuiHost.set(INSTANCE); + } + + public static void bindContext(MinecraftServer server, ServerLevel level, Engine engine, UUID opener) { + INSTANCE.server = server; + INSTANCE.active = engine; + INSTANCE.levels.put(engine, level); + if (opener == null) { + INSTANCE.openers.remove(engine); + } else { + INSTANCE.openers.put(engine, opener); + } + } + + /** + * Drops the GUI binding for an evicted engine. Without this the host pinned every + * GUI-bound Engine, its ServerLevel and transitively the MinecraftServer for the process + * lifetime — there was no remove path at all. + */ + public static void unbind(Engine engine) { + if (engine == null) { + return; + } + INSTANCE.levels.remove(engine); + INSTANCE.openers.remove(engine); + if (INSTANCE.active == engine) { + INSTANCE.active = null; + } + } + + public static void clear() { + INSTANCE.levels.clear(); + INSTANCE.openers.clear(); + INSTANCE.active = null; + INSTANCE.server = null; + } + + public static boolean isGuiLaunchable() { + return GuiHost.isAvailable() && IrisSettings.get().getGui().isUseServerLaunchedGuis(); + } + + public static String guiUnavailableReason() { + if (GuiHost.isDesktopSuppressed()) { + return "running inside a Minecraft client (desktop GUIs disabled to avoid a client crash)"; + } + if (!GuiHost.isAvailable()) { + return "headless JVM (no display)"; + } + return "gui.useServerLaunchedGuis=false in Iris settings"; + } + + @Override + public Engine findActiveEngine() { + Engine current = active; + if (current != null && !current.isClosed()) { + return current; + } + for (Map.Entry entry : levels.entrySet()) { + if (!entry.getKey().isClosed()) { + return entry.getKey(); + } + } + return null; + } + + @Override + public GuiOverlay overlayFor(Engine engine) { + if (engine == null) { + return null; + } + ServerLevel level = levels.get(engine); + if (level == null || server == null) { + return null; + } + return new ModdedVisionOverlay(server, level, engine, openers.get(engine)); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java new file mode 100644 index 000000000..2eaada62f --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java @@ -0,0 +1,607 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +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.Locator; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.framework.StructureReachability; +import art.arcane.iris.engine.framework.WrongEngineBroException; +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.modded.IrisModdedChunkGenerator; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.math.Position2; +import com.mojang.datafixers.util.Pair; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Relative; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.Structure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; + +final class ModdedLocateCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final long LOCATE_TIMEOUT_MS = 120000L; + private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100; + private static final ConcurrentHashMap> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>(); + + private ModdedLocateCommands() { + } + + static int gotoBiome(CommandSourceStack source, String key) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3)); + return 0; + } + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8)); + return 0; + } + IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim()); + if (biome == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key))); + return 0; + } + locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey()); + return 1; + } + + static int gotoRegion(CommandSourceStack source, String key) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_4)); + return 0; + } + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9)); + return 0; + } + IrisRegion region = engine.getData().getRegionLoader().load(key.trim()); + if (region == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key))); + return 0; + } + if (!engine.getDimension().getRegions().contains(region.getLoadKey())) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", region.getLoadKey()))); + return 0; + } + locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey()); + return 1; + } + + static int gotoObject(CommandSourceStack source, String keyRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_10)); + return 0; + } + String key = keyRaw.trim(); + if (!engine.hasObjectPlacement(key)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length))); + return 0; + } + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_OBJECT_KEY, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length))); + return 0; + } + locate(source, level, engine, player, Locator.object(key), "object " + key); + return 1; + } + + static int gotoStructure(CommandSourceStack source, String keyRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_11)); + return 0; + } + String key = keyRaw.trim(); + if (key.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NAME_IRIS_NATIVE_STRUCTURE_LOCATE)); + return 0; + } + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_5)); + return 0; + } + Optional resolved = resolveNativeStructure(source, level, engine, key); + if (resolved.isEmpty()) { + if (!IrisStructureLocator.hasNativePlacement(engine, key) + && IrisStructureLocator.hasLocatableEditablePlacement(engine, key)) { + locateIrisStructure(source, level, engine, player, key); + return 1; + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_USE_TAB_COMPLETION_CHOOSE_IRIS_PLACEMENT_REGISTERED_NATIVE, MessageArgument.untrusted("key", key))); + return 0; + } + NativeStructureTarget target = resolved.get(); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false); + boolean nativeGenerationEnabled = + source.getServer().getWorldGenSettings().options().generateStructures(); + boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, target.key()); + boolean locatableNativePlacement = nativePlacement + && IrisStructureLocator.hasLocatableNativePlacement(engine, target.key()); + boolean locatableEditableReplacement = !nativePlacement + && decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS + && IrisStructureLocator.hasLocatableEditablePlacement(engine, target.key()); + boolean reachable = StructureReachability.isReachable(engine, target.key()); + if (!ModdedCommandSuggestions.isEligibleRegisteredStructure( + decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement, + reachable, nativeGenerationEnabled)) { + IrisModdedCommands.fail(source, registeredStructureUnavailableMessage( + target.key(), target.availability(), decision, + nativePlacement, locatableNativePlacement, + nativeGenerationEnabled, reachable)); + return 0; + } + if (locatableEditableReplacement) { + locateIrisStructure(source, level, engine, player, target.key()); + return 1; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS))); + runNativeStructureLocate(source, level, player, target); + return 1; + } + + static String registeredStructureUnavailableMessage( + String key, NativeStructureAvailability nativeAvailability, + IrisNativeStructureDecision decision, + boolean nativePlacement, boolean locatableNativePlacement, + boolean nativeGenerationEnabled, boolean reachable) { + if (!decision.generate() + && decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + return NativeStructureGenerationPolicy.generationStatusMessage(key, decision.status()); + } + if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + if (nativePlacement && !nativeGenerationEnabled) { + return withInactiveNativePlacement( + nativeUnavailableMessage(key, NativeStructureAvailability.WORLD_DISABLED), + nativePlacement, locatableNativePlacement); + } + return "Iris replacement " + key + + " is configured, but every matching placement has non-positive density or a Y band " + + "outside this world's height range."; + } + if (!nativeGenerationEnabled) { + return withInactiveNativePlacement( + nativeUnavailableMessage(key, NativeStructureAvailability.WORLD_DISABLED), + nativePlacement, locatableNativePlacement); + } + NativeStructureAvailability availability = nativeAvailability; + if (availability == NativeStructureAvailability.AVAILABLE && !reachable) { + availability = NativeStructureAvailability.NO_PLACEMENT; + } + return withInactiveNativePlacement( + nativeUnavailableMessage(key, availability), nativePlacement, locatableNativePlacement); + } + + private static String withInactiveNativePlacement( + String reason, boolean nativePlacement, boolean locatableNativePlacement) { + if (!nativePlacement || locatableNativePlacement) { + return reason; + } + return reason + " A matching Iris nativeStructures placement is also configured, but has non-positive " + + "density or a Y band outside this world's height range."; + } + + private static void locateIrisStructure(CommandSourceStack source, ServerLevel level, Engine engine, + ServerPlayer player, String key) { + MinecraftServer server = source.getServer(); + int blockX = player.blockPosition().getX(); + int blockZ = player.blockPosition().getZ(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_IRIS_PLACED_STRUCTURE, MessageArgument.untrusted("key", key))); + Thread thread = new Thread(() -> { + try { + IrisStructureLocator.LocateResult result = + IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024); + if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key)))); + return; + } + if (!result.found()) { + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_IRIS_PLACED_STRUCTURE_WITHIN_1024_CHUNKS, MessageArgument.untrusted("key", key)))); + return; + } + int targetX = result.originX(); + int targetY = result.baseY() + 2; + int targetZ = result.originZ(); + server.execute(() -> teleportToStructure(source, level, player, targetX, targetY, targetZ, + "Iris-placed structure " + key)); + } catch (Throwable e) { + LOGGER.error("Iris structure locate failed for {}", key, e); + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())))); + } + }, "Iris Structure Locator"); + thread.setDaemon(true); + thread.start(); + } + + private static void runNativeStructureLocate(CommandSourceStack source, ServerLevel level, + ServerPlayer player, NativeStructureTarget target) { + MinecraftServer server = source.getServer(); + Runnable locateTask = () -> locateNativeStructure(source, level, player, target); + if (Thread.currentThread() == server.getRunningThread()) { + locateTask.run(); + return; + } + server.execute(locateTask); + } + + private static void locateNativeStructure(CommandSourceStack source, ServerLevel level, + ServerPlayer player, NativeStructureTarget target) { + try { + ChunkGenerator generator = level.getChunkSource().getGenerator(); + Pair> found = generator.findNearestMapStructure( + level, + HolderSet.direct(target.holder()), + player.blockPosition(), + NATIVE_STRUCTURE_LOCATE_RADIUS, + false); + if (found == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS))); + return; + } + BlockPos position = found.getFirst(); + int targetX = position.getX(); + int targetZ = position.getZ(); + level.getChunk(targetX >> 4, targetZ >> 4); + int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, targetX, targetZ) + 1; + int targetY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, surfaceY)); + teleportToStructure(source, level, player, targetX, targetY, targetZ, + "native structure " + target.key()); + } catch (Throwable e) { + LOGGER.error("Native structure locate failed for {}", target.key(), e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_NATIVE_STRUCTURE_FAILED, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("value2", e.getClass().getSimpleName()))); + } + } + + private static void teleportToStructure(CommandSourceStack source, ServerLevel level, ServerPlayer player, + int targetX, int targetY, int targetZ, String label) { + if (player.hasDisconnected() || player.isRemoved()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED)); + return; + } + if (player.level() != level) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN)); + return; + } + level.getChunk(targetX >> 4, targetZ >> 4); + int clampedY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, targetY)); + boolean teleported = player.teleportTo(level, targetX + 0.5D, clampedY, targetZ + 0.5D, + Set.of(), player.getYRot(), player.getXRot(), false); + if (!teleported) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ))); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ))); + } + + static int verifyStructures(CommandSourceStack source, String keyRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_12)); + return 0; + } + String key = keyRaw == null ? "" : keyRaw.trim(); + if (!key.isEmpty()) { + return verifyStructure(source, level, engine, key); + } + Registry registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); + int available = 0; + int disabled = 0; + int suppressed = 0; + int unreachableBiomes = 0; + int unsupported = 0; + for (Identifier identifier : registry.keySet()) { + Optional> holder = registry.get(identifier); + if (holder.isEmpty()) { + continue; + } + NativeStructureAvailability availability = nativeAvailability(source, level, engine, + identifier.toString(), holder.get()); + switch (availability) { + case AVAILABLE -> available++; + case WORLD_DISABLED, FILTERED -> disabled++; + case IRIS_SUPPRESSED -> suppressed++; + case EMPTY_BIOME_FILTER, BIOME_UNREACHABLE -> unreachableBiomes++; + case NO_PLACEMENT -> unsupported++; + } + } + int irisPlaced = IrisStructureLocator.placedKeys(engine).size(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_REACHABILITY_NATIVE_GENERATION_ELIGIBLE_IRIS_PLACED_NATIVE_DISABLED_NATIVE, MessageArgument.untrusted("available", available), MessageArgument.untrusted("irisPlaced", irisPlaced), MessageArgument.untrusted("disabled", disabled), MessageArgument.untrusted("suppressed", suppressed), MessageArgument.untrusted("unreachableBiomes", unreachableBiomes), MessageArgument.untrusted("unsupported", unsupported))); + return 1; + } + + private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) { + Optional target = resolveNativeStructure(source, level, engine, key); + if (target.isEmpty()) { + if (IrisStructureLocator.isPlaced(engine, key)) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_IRIS_PLACED_LOCATABLE_WITH_IRIS_GOTO_STRUCTURE, MessageArgument.untrusted("key", key), MessageArgument.untrusted("key2", key))); + return 1; + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_IT_IS_NEITHER_IRIS_PLACED_NOR_REGISTERED_BY, MessageArgument.untrusted("key", key))); + return 0; + } + NativeStructureTarget resolved = target.get(); + if (resolved.availability() == NativeStructureAvailability.IRIS_SUPPRESSED) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_EXPLICITLY_REPLACED_BY_IRIS_PLACEMENT_LOCATABLE_WITH_IRIS, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key()))); + return 1; + } + if (resolved.availability() != NativeStructureAvailability.AVAILABLE) { + IrisModdedCommands.fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability())); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NATIVE_STRUCTURE_IS_ENABLED_SUPPORTED_BY_THIS_DIMENSION_S_GENERATOR, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key()))); + return 1; + } + + private static Optional resolveNativeStructure(CommandSourceStack source, + ServerLevel level, + Engine engine, + String keyRaw) { + Identifier identifier = Identifier.tryParse(keyRaw); + if (identifier == null) { + return Optional.empty(); + } + Registry registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); + Optional> holder = registry.get(identifier); + if (holder.isEmpty()) { + return Optional.empty(); + } + String key = identifier.toString(); + NativeStructureAvailability availability = nativeAvailability(source, level, engine, key, holder.get()); + return Optional.of(new NativeStructureTarget(key, holder.get(), availability)); + } + + static NativeStructureAvailability nativeAvailability(CommandSourceStack source, ServerLevel level, + Engine engine, String key, + Holder.Reference holder) { + boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures(); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false); + boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK; + boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS; + boolean biomeFilterEmpty = holder.value().biomes().stream().findAny().isEmpty(); + ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator(); + boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator + && irisGenerator.isNativeStructureReachable(holder); + boolean hasPlacement = false; + if (worldEnabled && selected && !suppressed && !biomeFilterEmpty && biomeReachable) { + hasPlacement = !level.getChunkSource().getGeneratorState().getPlacementsForStructure(holder).isEmpty(); + } + return classifyNativeAvailability( + worldEnabled, selected, suppressed, biomeFilterEmpty, biomeReachable, hasPlacement); + } + + static NativeStructureAvailability classifyNativeAvailability(boolean worldEnabled, boolean selected, + boolean suppressed, boolean biomeFilterEmpty, + boolean biomeReachable, + boolean hasPlacement) { + if (!worldEnabled) { + return NativeStructureAvailability.WORLD_DISABLED; + } + if (!selected) { + return NativeStructureAvailability.FILTERED; + } + if (suppressed) { + return NativeStructureAvailability.IRIS_SUPPRESSED; + } + if (biomeFilterEmpty) { + return NativeStructureAvailability.EMPTY_BIOME_FILTER; + } + if (!biomeReachable) { + return NativeStructureAvailability.BIOME_UNREACHABLE; + } + if (!hasPlacement) { + return NativeStructureAvailability.NO_PLACEMENT; + } + return NativeStructureAvailability.AVAILABLE; + } + + static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) { + return switch (availability) { + case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located."; + case FILTERED -> NativeStructureGenerationPolicy.generationStatusMessage( + key, NativeStructureGenerationStatus.DISABLED_BY_PACK); + case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage( + key, NativeStructureGenerationStatus.REPLACED_BY_IRIS); + case EMPTY_BIOME_FILTER -> "Native structure " + key + + " has a biome tag or filter that resolves to zero registered biomes."; + case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack."; + case NO_PLACEMENT -> "Native structure " + key + + " is registered and biome-compatible, but has no active positive-weight, " + + "positive-frequency structure-set placement in this dimension's generator state."; + case AVAILABLE -> "Native structure " + key + " is available."; + }; + } + + static int gotoPoi(CommandSourceStack source, String typeRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_13)); + return 0; + } + String type = typeRaw.trim(); + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_POI_TYPE, MessageArgument.untrusted("type", type))); + return 0; + } + locate(source, level, engine, player, Locator.poi(type), "POI " + type); + return 1; + } + + private static void locate(CommandSourceStack source, ServerLevel level, Engine engine, ServerPlayer player, Locator locator, String label) { + MinecraftServer server = source.getServer(); + int chunkX = player.blockPosition().getX() >> 4; + int chunkZ = player.blockPosition().getZ() >> 4; + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING, MessageArgument.untrusted("label", label))); + CompletableFuture search; + try { + search = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> { + }); + } catch (WrongEngineBroException e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_THIS_WORLD_HAS_BEEN_CLOSED_REJOIN_DIMENSION_TRY_AGAIN)); + return; + } + UUID playerId = player.getUUID(); + CompletableFuture previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search); + if (previous != null && previous != search) { + previous.cancel(true); + } + search.whenComplete((Position2 at, Throwable error) -> completeLocate( + source, level, engine, player, label, server, playerId, search, at, error)); + } + + private static void completeLocate(CommandSourceStack source, ServerLevel level, Engine engine, + ServerPlayer player, String label, MinecraftServer server, UUID playerId, + CompletableFuture search, Position2 at, Throwable error) { + if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) { + return; + } + Throwable failure = unwrapCompletionFailure(error); + if (failure instanceof CancellationException) { + ACTIVE_LOCATE_REQUESTS.remove(playerId, search); + return; + } + if (failure != null) { + LOGGER.error("Iris locate failed for {}", label, failure); + server.execute(() -> { + if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED_2, MessageArgument.untrusted("failure", failure))); + } + }); + return; + } + if (at == null) { + server.execute(() -> { + if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_WITHIN_SEARCH_TIMEOUT, MessageArgument.untrusted("label", label))); + } + }); + return; + } + server.execute(() -> { + if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { + teleportToLocateResult(source, level, engine, player, label, at); + } + }); + } + + private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine, + ServerPlayer player, String label, Position2 at) { + // Same liveness guards the structure completion path has: the search can take up to + // two minutes, and the captured ServerPlayer may be gone or elsewhere by then. + if (player.hasDisconnected() || player.isRemoved()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED)); + return; + } + if (player.level() != level) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN)); + return; + } + int blockX = (at.getX() << 4) + 8; + int blockZ = (at.getZ() << 4) + 8; + try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2; + boolean teleported = player.teleportTo( + level, + blockX + 0.5D, + blockY, + blockZ + 0.5D, + Set.of(), + player.getYRot(), + player.getXRot(), + false); + if (!teleported) { + IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, + MessageArgument.untrusted("label", label), + MessageArgument.trusted("targetX", blockX), + MessageArgument.trusted("clampedY", blockY), + MessageArgument.trusted("targetZ", blockZ))); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ))); + } catch (GenerationSessionException e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label))); + } + } + + private static Throwable unwrapCompletionFailure(Throwable error) { + Throwable failure = error; + while ((failure instanceof CompletionException || failure instanceof ExecutionException) + && failure.getCause() != null) { + failure = failure.getCause(); + } + return failure; + } + + enum NativeStructureAvailability { + AVAILABLE, + WORLD_DISABLED, + FILTERED, + IRIS_SUPPRESSED, + EMPTY_BIOME_FILTER, + BIOME_UNREACHABLE, + NO_PLACEMENT + } + + private record NativeStructureTarget(String key, Holder.Reference holder, + NativeStructureAvailability availability) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java new file mode 100644 index 000000000..0e2607b62 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java @@ -0,0 +1,749 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.tools.TreePlausibilizeBatch; +import art.arcane.iris.core.tools.TreePlausibilizer; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.modded.ModdedBlockState; +import art.arcane.iris.modded.ModdedTileData; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.math.RNG; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.function.Predicate; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class ModdedObjectCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + private static final long MAX_SAVE_VOLUME = 500000L; + private static final long MAX_AUTOSELECT_VOLUME = 100000L; + private static final double TARGET_RANGE = 256.0D; + + private static final SuggestionProvider OBJECT_KEYS = (CommandContext context, SuggestionsBuilder builder) -> { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + if (engine != null) { + return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder); + } + } catch (Throwable e) { + IrisModdedCommands.warnTabFailure("object keys", context.getSource(), e); + } + return builder.buildFuture(); + }; + + private static final SuggestionProvider ROTATIONS = (CommandContext context, SuggestionsBuilder builder) -> + SharedSuggestionProvider.suggest(List.of("0", "90", "180", "270"), builder); + + private ModdedObjectCommands() { + } + + private enum ResizeOp { + EXPAND, + CONTRACT, + SHIFT + } + + public static LiteralArgumentBuilder tree(String name) { + ModdedObjectUndo.init(); + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); + + root.then(Commands.literal("wand") + .executes((CommandContext context) -> giveWand(context.getSource()))); + root.then(Commands.literal("dust") + .executes((CommandContext context) -> giveDust(context.getSource()))); + root.then(Commands.literal("d") + .executes((CommandContext context) -> giveDust(context.getSource()))); + + root.then(Commands.literal("save") + .then(Commands.literal("overwrite") + .then(Commands.argument("name", StringArgumentType.greedyString()) + .executes((CommandContext context) -> save(context.getSource(), StringArgumentType.getString(context, "name"), true)))) + .then(Commands.argument("name", StringArgumentType.greedyString()) + .executes((CommandContext context) -> save(context.getSource(), StringArgumentType.getString(context, "name"), false)))); + + root.then(pasteTree()); + + root.then(resizeTree("expand", ResizeOp.EXPAND)); + root.then(resizeTree("contract", ResizeOp.CONTRACT)); + root.then(resizeTree("-", ResizeOp.CONTRACT)); + root.then(resizeTree("shift", ResizeOp.SHIFT)); + + root.then(Commands.literal("xpy") + .executes((CommandContext context) -> autoSelect(context.getSource(), false))); + root.then(Commands.literal("x+y") + .executes((CommandContext context) -> autoSelect(context.getSource(), false))); + root.then(Commands.literal("xay") + .executes((CommandContext context) -> autoSelect(context.getSource(), true))); + root.then(Commands.literal("x&y") + .executes((CommandContext context) -> autoSelect(context.getSource(), true))); + + root.then(positionTree("position1", true)); + root.then(positionTree("p1", true)); + root.then(positionTree("position2", false)); + root.then(positionTree("p2", false)); + + root.then(Commands.literal("analyze") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS) + .executes((CommandContext context) -> analyze(context.getSource(), StringArgumentType.getString(context, "key"))))); + + root.then(Commands.literal("shrink") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS) + .executes((CommandContext context) -> shrink(context.getSource(), StringArgumentType.getString(context, "key"))))); + + root.then(Commands.literal("undo") + .executes((CommandContext context) -> undo(context.getSource(), 1)) + .then(Commands.argument("amount", IntegerArgumentType.integer(1, 32)) + .executes((CommandContext context) -> undo(context.getSource(), IntegerArgumentType.getInteger(context, "amount"))))); + root.then(Commands.literal("u") + .executes((CommandContext context) -> undo(context.getSource(), 1)) + .then(Commands.argument("amount", IntegerArgumentType.integer(1, 32)) + .executes((CommandContext context) -> undo(context.getSource(), IntegerArgumentType.getInteger(context, "amount"))))); + + root.then(bukkitOnly("we", "WorldEdit selection import requires the Bukkit plugin with WorldEdit installed.")); + root.then(bukkitOnly("studio", "The object studio world requires the Bukkit studio toolchain; it is not available on modded servers.")); + root.then(bukkitOnly("convert", "Schematic conversion (.schem -> .iob) requires the Bukkit plugin.")); + root.then(Commands.literal("plausibilize") + .then(Commands.argument("args", StringArgumentType.greedyString()).suggests(OBJECT_KEYS) + .executes((CommandContext context) -> plausibilize(context.getSource(), StringArgumentType.getString(context, "args"))))); + + return root; + } + + private static LiteralArgumentBuilder pasteTree() { + LiteralArgumentBuilder paste = Commands.literal("paste"); + paste.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS) + .executes((CommandContext context) -> paste(context.getSource(), StringArgumentType.getString(context, "key"), 0, null))); + paste.then(Commands.literal("rotate") + .then(Commands.argument("degrees", IntegerArgumentType.integer(-270, 270)).suggests(ROTATIONS) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS) + .executes((CommandContext context) -> paste(context.getSource(), StringArgumentType.getString(context, "key"), + IntegerArgumentType.getInteger(context, "degrees"), null))))); + paste.then(Commands.literal("at") + .then(Commands.argument("x", IntegerArgumentType.integer()) + .then(Commands.argument("y", IntegerArgumentType.integer()) + .then(Commands.argument("z", IntegerArgumentType.integer()) + .then(Commands.literal("rotate") + .then(Commands.argument("degrees", IntegerArgumentType.integer(-270, 270)).suggests(ROTATIONS) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS) + .executes((CommandContext context) -> paste(context.getSource(), StringArgumentType.getString(context, "key"), + IntegerArgumentType.getInteger(context, "degrees"), + new BlockPos(IntegerArgumentType.getInteger(context, "x"), IntegerArgumentType.getInteger(context, "y"), IntegerArgumentType.getInteger(context, "z"))))))) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS) + .executes((CommandContext context) -> paste(context.getSource(), StringArgumentType.getString(context, "key"), 0, + new BlockPos(IntegerArgumentType.getInteger(context, "x"), IntegerArgumentType.getInteger(context, "y"), IntegerArgumentType.getInteger(context, "z"))))))))); + return paste; + } + + private static LiteralArgumentBuilder resizeTree(String name, ResizeOp op) { + return Commands.literal(name) + .executes((CommandContext context) -> resize(context.getSource(), 1, op)) + .then(Commands.argument("amount", IntegerArgumentType.integer(1, 256)) + .executes((CommandContext context) -> resize(context.getSource(), IntegerArgumentType.getInteger(context, "amount"), op))); + } + + private static LiteralArgumentBuilder positionTree(String name, boolean first) { + return Commands.literal(name) + .executes((CommandContext context) -> position(context.getSource(), first, false)) + .then(Commands.literal("look") + .executes((CommandContext context) -> position(context.getSource(), first, true))); + } + + private static LiteralArgumentBuilder bukkitOnly(String name, String message) { + return Commands.literal(name) + .executes((CommandContext context) -> { + IrisModdedCommands.fail(context.getSource(), message); + return 0; + }); + } + + public static int giveWand(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_WAND_IS)); + return 0; + } + if (!player.getInventory().add(ModdedWandService.createWand())) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_YOUR_INVENTORY_IS_FULL)); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_POOF_GOOD_LUCK_BUILDING_LEFT_CLICK_CORNER_1_RIGHT_CLICK)); + return 1; + } + + static int giveDust(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_DUST_IS)); + return 0; + } + if (!player.getInventory().add(ModdedWandService.createDust())) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_YOUR_INVENTORY_IS_FULL_2)); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_RIGHT_CLICK_BLOCK_REVEAL_OBJECT_IT_BELONGS)); + return 1; + } + + private static int save(CommandSourceStack source, String nameRaw, boolean overwrite) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_SAVING_CAPTURES)); + return 0; + } + ServerLevel level = player.level(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_OBJECTS_SAVE_INTO)); + return 0; + } + if (!ModdedWandService.isHoldingWand(player)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_HOLD_YOUR_IRIS_WAND_IRIS_WAND)); + return 0; + } + ModdedWandService.Selection selection = ModdedWandService.selection(player); + if (selection == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_NO_AREA_SELECTED_LEFT_RIGHT_CLICK_BLOCKS_WITH_WAND_FIRST)); + return 0; + } + String name = nameRaw.trim().replace('\\', '/'); + if (name.isEmpty() || name.contains("..")) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_INVALID_OBJECT_NAME, MessageArgument.untrusted("nameRaw", nameRaw))); + return 0; + } + BlockPos min = selection.min(); + BlockPos max = selection.max(); + int w = max.getX() - min.getX() + 1; + int h = max.getY() - min.getY() + 1; + int d = max.getZ() - min.getZ() + 1; + long volume = (long) w * h * d; + if (volume > MAX_SAVE_VOLUME) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_SELECTION_TOO_LARGE_BLOCKS_MAX, MessageArgument.untrusted("volume", volume), MessageArgument.untrusted("MAXSAVEVOLUME", MAX_SAVE_VOLUME))); + return 0; + } + File file = new File(engine.getData().getDataFolder(), "objects" + File.separator + name.replace('/', File.separatorChar) + ".iob"); + // Atomic path claim ON the server thread: the async write below turned a plain + // exists() check into a TOCTOU where two rapid saves interleaved into one file. + File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + boolean claimed; + try { + claimed = file.createNewFile(); + } catch (IOException e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))); + return 0; + } + if (!claimed && !overwrite) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FILE_ALREADY_EXISTS_USE_IRIS_OBJECT_SAVE_OVERWRITE, MessageArgument.untrusted("name", name))); + return 0; + } + int[] tilesSkipped = {0}; + int[] tilesSaved = {0}; + // capture() must stay on the server thread (getBlockState/getBlockEntity are not + // async-safe), but the disk write of a local, unshared object is not tick work. + IrisObject object = capture(level, min, max, w, h, d, tilesSkipped, tilesSaved); + MinecraftServer server = source.getServer(); + boolean finalClaimed = claimed; + J.a(() -> { + try { + object.write(file); + } catch (IOException e) { + LOGGER.error("Iris object save failed for {}", file.getAbsolutePath(), e); + if (finalClaimed) { + // Never leave a 0-byte claim file permanently blocking non-overwrite saves. + file.delete(); + } + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))))); + return; + } + StringBuilder tileNote = new StringBuilder(); + if (tilesSaved[0] > 0) { + tileNote.append(" (").append(tilesSaved[0]).append(" tile entity state(s) captured"); + if (tilesSkipped[0] > 0) { + tileNote.append(", ").append(tilesSkipped[0]).append(" failed"); + } + tileNote.append(")"); + } else if (tilesSkipped[0] > 0) { + tileNote.append(" (").append(tilesSkipped[0]).append(" tile state(s) could not be captured)"); + } + server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_SAVED_OBJECTS_IOB_X_X_BLOCK_S, MessageArgument.untrusted("value", engine.getData().getDataFolder().getName()), MessageArgument.untrusted("name", name), MessageArgument.untrusted("w", w), MessageArgument.untrusted("h", h), MessageArgument.untrusted("d", d), MessageArgument.untrusted("value2", object.getBlocks().size()), MessageArgument.untrusted("tileNote", tileNote)))); + LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSaved={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSaved[0], tilesSkipped[0], file.getAbsolutePath()); + }); + return 1; + } + + private static IrisObject capture(ServerLevel level, BlockPos min, BlockPos max, int w, int h, int d, int[] tilesSkipped, int[] tilesSaved) { + IrisObject object = new IrisObject(w, h, d); + HolderLookup.Provider provider = level.registryAccess(); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + BlockState state = level.getBlockState(cursor.set(x, y, z)); + if (state.is(Blocks.AIR)) { + continue; + } + int ox = x - min.getX(); + int oy = y - min.getY(); + int oz = z - min.getZ(); + object.setUnsigned(ox, oy, oz, ModdedBlockState.of(state, null)); + if (state.hasBlockEntity()) { + TileData tile = captureTile(level, provider, cursor.immutable(), state); + if (tile != null) { + object.setUnsignedTile(ox, oy, oz, tile); + tilesSaved[0]++; + } else { + tilesSkipped[0]++; + } + } + } + } + } + return object; + } + + private static TileData captureTile(ServerLevel level, HolderLookup.Provider provider, BlockPos pos, BlockState state) { + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity == null) { + return null; + } + try { + CompoundTag tag = blockEntity.saveWithFullMetadata(provider); + String snbt = NbtUtils.structureToSnbt(tag); + String blockKey = BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(); + return ModdedTileData.capture(blockKey, snbt); + } catch (Throwable e) { + LOGGER.error("Iris tile capture failed at {} {} {}", pos.getX(), pos.getY(), pos.getZ(), e); + return null; + } + } + + private static int paste(CommandSourceStack source, String keyRaw, int rotation, BlockPos at) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + String key = keyRaw.trim(); + IrisObject object = null; + try { + object = IrisData.loadAnyObject(key, engine == null ? null : engine.getData()); + } catch (Throwable e) { + LOGGER.error("Iris object load failed for {}", key, e); + } + if (object == null || object.getBlocks().size() == 0) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_UNKNOWN_EMPTY_OBJECT, MessageArgument.untrusted("key", key))); + return 0; + } + + ServerPlayer player = source.getPlayer(); + BlockPos target = at; + if (target == null) { + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_CONSOLE_MUST_SPECIFY_COORDINATES_IRIS_OBJECT_PASTE_AT_X_Y, MessageArgument.untrusted("key", key))); + return 0; + } + HitResult hit = player.pick(TARGET_RANGE, 1.0F, false); + if (hit.getType() != HitResult.Type.BLOCK || !(hit instanceof BlockHitResult blockHit)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_YOU_ARE_NOT_LOOKING_AT_BLOCK_WITHIN_BLOCKS, MessageArgument.untrusted("value", (int) TARGET_RANGE))); + return 0; + } + target = blockHit.getBlockPos().above(); + } + + IrisObjectPlacement placement = new IrisObjectPlacement(); + placement.setRotation(IrisObjectRotation.of(0, rotation, 0)); + ModdedObjectPlacer placer = new ModdedObjectPlacer(level, engine); + try { + object.place(target.getX(), target.getY() + object.getCenter().getY(), target.getZ(), placer, placement, new RNG(), null); + } catch (Throwable e) { + LOGGER.error("Iris paste failed for {}", key, e); + ModdedObjectUndo.record(player == null ? ModdedObjectUndo.CONSOLE : player.getUUID(), level, placer.undoSnapshot()); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_PASTE_FAILED_PARTIAL_CHANGES_RECORDED_UNDO, MessageArgument.untrusted("value", e.getClass().getSimpleName()))); + return 0; + } + UUID owner = player == null ? ModdedObjectUndo.CONSOLE : player.getUUID(); + ModdedObjectUndo.record(owner, level, placer.undoSnapshot()); + String tileNote = tileNote(placer); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_PLACED_AT_ROT_WRITE_S_NON_AIR, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", target.getX()), MessageArgument.untrusted("value2", target.getY()), MessageArgument.untrusted("value3", target.getZ()), MessageArgument.untrusted("rotation", rotation), MessageArgument.untrusted("value4", placer.writes()), MessageArgument.untrusted("value5", placer.nonAirWrites()), MessageArgument.untrusted("tileNote", tileNote))); + LOGGER.info("Iris paste: {} at {},{},{} rot={} writes={} nonAir={} tilesRestored={} tilesSkipped={}", + key, target.getX(), target.getY(), target.getZ(), rotation, placer.writes(), placer.nonAirWrites(), placer.restoredTiles(), placer.skippedTiles()); + return placer.writes() > 0 ? 1 : 0; + } + + private static int resize(CommandSourceStack source, int amount, ResizeOp op) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS)); + return 0; + } + if (!ModdedWandService.isHoldingWand(player)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_HOLD_YOUR_IRIS_WAND_IRIS_WAND_2)); + return 0; + } + ModdedWandService.Selection selection = ModdedWandService.selection(player); + if (selection == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_NO_AREA_SELECTED)); + return 0; + } + Direction direction = Direction.getApproximateNearest(player.getLookAngle()); + int[] mins = {selection.min().getX(), selection.min().getY(), selection.min().getZ()}; + int[] maxs = {selection.max().getX(), selection.max().getY(), selection.max().getZ()}; + int axis = switch (direction.getAxis()) { + case X -> 0; + case Y -> 1; + case Z -> 2; + }; + int step = direction.getAxisDirection().getStep(); + switch (op) { + case EXPAND -> { + if (step > 0) { + maxs[axis] += amount; + } else { + mins[axis] -= amount; + } + } + case CONTRACT -> { + if (step > 0) { + maxs[axis] = Math.max(mins[axis], maxs[axis] - amount); + } else { + mins[axis] = Math.min(maxs[axis], mins[axis] + amount); + } + } + case SHIFT -> { + mins[axis] += step * amount; + maxs[axis] += step * amount; + } + } + BlockPos first = new BlockPos(mins[0], mins[1], mins[2]); + BlockPos second = new BlockPos(maxs[0], maxs[1], maxs[2]); + ModdedWandService.setSelection(player, first, second); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_MESSAGE, MessageArgument.untrusted("value", op.name().toLowerCase(Locale.ROOT)), MessageArgument.untrusted("amount", amount), MessageArgument.untrusted("value2", direction.getName()), MessageArgument.untrusted("value3", describe(first, second)))); + return 1; + } + + private static int position(CommandSourceStack source, boolean first, boolean look) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_2)); + return 0; + } + if (!ModdedWandService.isHoldingWand(player)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_READY_YOUR_WAND_IRIS_WAND)); + return 0; + } + BlockPos pos; + if (look) { + HitResult hit = player.pick(TARGET_RANGE, 1.0F, false); + if (hit.getType() != HitResult.Type.BLOCK || !(hit instanceof BlockHitResult blockHit)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_YOU_ARE_NOT_LOOKING_AT_BLOCK)); + return 0; + } + pos = blockHit.getBlockPos(); + } else { + pos = player.blockPosition().below(); + } + ModdedWandService.Selection selection = ModdedWandService.selection(player); + BlockPos other = selection == null ? null : (first ? selection.second() : selection.first()); + BlockPos fallback = other == null ? pos : other; + if (first) { + ModdedWandService.setSelection(player, pos, fallback); + } else { + ModdedWandService.setSelection(player, fallback, pos); + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_POSITION_SET, MessageArgument.untrusted("value", (first ? 1 : 2)), MessageArgument.untrusted("value2", pos.getX()), MessageArgument.untrusted("value3", pos.getY()), MessageArgument.untrusted("value4", pos.getZ()))); + return 1; + } + + private static int autoSelect(CommandSourceStack source, boolean down) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3)); + return 0; + } + if (!ModdedWandService.isHoldingWand(player)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_HOLD_YOUR_WAND)); + return 0; + } + ModdedWandService.Selection selection = ModdedWandService.selection(player); + if (selection == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_NO_AREA_SELECTED_2)); + return 0; + } + ServerLevel level = player.level(); + BlockPos min = selection.min(); + BlockPos max = selection.max(); + long volume = (long) (max.getX() - min.getX() + 1) * (max.getY() - min.getY() + 1) * (max.getZ() - min.getZ() + 1); + if (volume > MAX_AUTOSELECT_VOLUME) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_SELECTION_TOO_LARGE_AUTO_SELECT_BLOCKS_MAX, MessageArgument.untrusted("volume", volume), MessageArgument.untrusted("MAXAUTOSELECTVOLUME", MAX_AUTOSELECT_VOLUME))); + return 0; + } + int levelMinY = level.getMinY(); + int levelMaxY = levelMinY + level.getHeight() - 1; + + int topMinY = min.getY(); + int topMaxY = max.getY(); + while (topMaxY < levelMaxY && !boxOnlyAir(level, min.getX(), topMinY, min.getZ(), max.getX(), topMaxY, max.getZ())) { + topMinY++; + topMaxY++; + } + topMaxY--; + + int bottomY = min.getY(); + if (down) { + int lowMinY = min.getY(); + int lowMaxY = max.getY(); + while (lowMinY > levelMinY && !boxOnlyAir(level, min.getX(), lowMinY, min.getZ(), max.getX(), lowMaxY, max.getZ())) { + lowMinY--; + lowMaxY--; + } + bottomY = lowMinY + 1; + } + + int minX = min.getX(); + int maxX = max.getX(); + int minZ = min.getZ(); + int maxZ = max.getZ(); + while (minX < maxX && boxOnlyAir(level, minX, bottomY, minZ, minX, topMaxY, maxZ)) { + minX++; + } + while (maxX > minX && boxOnlyAir(level, maxX, bottomY, minZ, maxX, topMaxY, maxZ)) { + maxX--; + } + while (minZ < maxZ && boxOnlyAir(level, minX, bottomY, minZ, maxX, topMaxY, minZ)) { + minZ++; + } + while (maxZ > minZ && boxOnlyAir(level, minX, bottomY, maxZ, maxX, topMaxY, maxZ)) { + maxZ--; + } + + BlockPos first = new BlockPos(minX, bottomY, minZ); + BlockPos second = new BlockPos(maxX, topMaxY, maxZ); + ModdedWandService.setSelection(player, first, second); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_AUTO_SELECT_COMPLETE, MessageArgument.untrusted("value", describe(first, second)))); + return 1; + } + + private static boolean boxOnlyAir(ServerLevel level, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + if (!level.getBlockState(cursor.set(x, y, z)).isAir()) { + return false; + } + } + } + } + return true; + } + + private static int analyze(CommandSourceStack source, String keyRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + String key = keyRaw.trim(); + IrisObject object = null; + try { + object = IrisData.loadAnyObject(key, engine == null ? null : engine.getData()); + } catch (Throwable e) { + LOGGER.error("Iris object load failed for {}", key, e); + } + if (object == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_UNKNOWN_OBJECT, MessageArgument.untrusted("key", key))); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_OBJECT_SIZE, MessageArgument.untrusted("value", object.getW()), MessageArgument.untrusted("value2", object.getH()), MessageArgument.untrusted("value3", object.getD()))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_BLOCKS_USED, MessageArgument.untrusted("value", object.getBlocks().size()))); + Map counts = new HashMap<>(); + Iterator values = object.getBlocks().values(); + while (values.hasNext()) { + PlatformBlockState state = values.next(); + counts.merge(state.key(), 1, Integer::sum); + } + List> sorted = new ArrayList<>(counts.entrySet()); + sorted.sort(Comparator.comparingInt((Map.Entry entry) -> entry.getValue()).reversed()); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_BLOCKS_OBJECT)); + int shown = 0; + for (Map.Entry entry : sorted) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_MESSAGE_2, MessageArgument.untrusted("value", entry.getKey()), MessageArgument.untrusted("value2", entry.getValue()))); + shown++; + if (shown >= 10) { + int remaining = sorted.size() - shown; + if (remaining > 0) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_OTHER_BLOCK_STATE_S, MessageArgument.untrusted("remaining", remaining))); + } + break; + } + } + return 1; + } + + private static int shrink(CommandSourceStack source, String keyRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + String key = keyRaw.trim(); + IrisObject object = null; + try { + object = IrisData.loadAnyObject(key, engine == null ? null : engine.getData()); + } catch (Throwable e) { + LOGGER.error("Iris object load failed for {}", key, e); + } + if (object == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_UNKNOWN_OBJECT_2, MessageArgument.untrusted("key", key))); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_CURRENT_OBJECT_SIZE, MessageArgument.untrusted("value", object.getW()), MessageArgument.untrusted("value2", object.getH()), MessageArgument.untrusted("value3", object.getD()))); + object.shrinkwrap(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_NEW_OBJECT_SIZE, MessageArgument.untrusted("value", object.getW()), MessageArgument.untrusted("value2", object.getH()), MessageArgument.untrusted("value3", object.getD()))); + File file = object.getLoadFile(); + if (file == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_OBJECT_HAS_NO_LOAD_FILE_CANNOT_PERSIST_SHRINK)); + return 0; + } + try { + object.write(file); + } catch (IOException e) { + LOGGER.error("Iris object shrink save failed for {}", file.getAbsolutePath(), e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT_2, MessageArgument.untrusted("value", file.getName()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage())))); + return 0; + } + return 1; + } + + private static int plausibilize(CommandSourceStack source, String raw) { + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + IrisData data = engine == null ? null : engine.getData(); + boolean dryRun = false; + int reach = TreePlausibilizer.DEFAULT_REACH; + StringBuilder targetBuilder = new StringBuilder(); + for (String token : raw.trim().split("\\s+")) { + String lower = token.toLowerCase(Locale.ROOT); + if (lower.startsWith("dryrun=")) { + dryRun = Boolean.parseBoolean(lower.substring("dryrun=".length())); + continue; + } + if (lower.startsWith("reach=")) { + try { + reach = Integer.parseInt(lower.substring("reach=".length())); + } catch (NumberFormatException e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_INVALID_REACH, MessageArgument.untrusted("token", token))); + return 0; + } + continue; + } + if (!targetBuilder.isEmpty()) { + targetBuilder.append(' '); + } + targetBuilder.append(token); + } + String target = targetBuilder.toString(); + List targets = TreePlausibilizeBatch.resolve(target, data); + if (targets.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_NO_OBJECTS_MATCHED, MessageArgument.untrusted("target", target))); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain( + ModdedCommandMessages.MODDED_OBJECT_COMMANDS_PLAUSIBILIZE_REACH_QUEUED_OBJECT_S, + MessageArgument.trusted("reach", reach), + MessageArgument.trusted("value", dryRun ? IrisLanguage.plain(RuntimeUiMessages.TREE_DRY_SUFFIX) : ""), + MessageArgument.trusted("value2", targets.size()) + )); + boolean dry = dryRun; + int reachFinal = reach; + MinecraftServer server = source.getServer(); + J.a(() -> TreePlausibilizeBatch.run(targets, dry, reachFinal, data, (TreePlausibilizeBatch.Output output) -> + server.execute(() -> IrisModdedCommands.ok(source, output.text())))); + return 1; + } + + private static int undo(CommandSourceStack source, int amount) { + ServerPlayer player = source.getPlayer(); + UUID owner = player == null ? ModdedObjectUndo.CONSOLE : player.getUUID(); + int available = ModdedObjectUndo.size(owner); + if (available == 0) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_NOTHING_UNDO)); + return 0; + } + int reverted = ModdedObjectUndo.undo(owner, Math.min(amount, available)); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_REVERTED_PASTE_S, MessageArgument.untrusted("reverted", reverted))); + return 1; + } + + private static String describe(BlockPos first, BlockPos second) { + return "(" + first.getX() + "," + first.getY() + "," + first.getZ() + ") -> (" + + second.getX() + "," + second.getY() + "," + second.getZ() + ")"; + } + + static String tileNote(ModdedObjectPlacer placer) { + StringBuilder note = new StringBuilder(); + if (placer.restoredTiles() > 0) { + note.append(", ").append(placer.restoredTiles()).append(" tile entity state(s) restored"); + } + if (placer.skippedTiles() > 0) { + note.append(", ").append(placer.skippedTiles()).append(" tile state(s) skipped"); + } + return note.toString(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectPlacer.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectPlacer.java new file mode 100644 index 000000000..cf070912a --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectPlacer.java @@ -0,0 +1,220 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.modded.ModdedBlockResolution; +import art.arcane.iris.modded.ModdedBlockState; +import art.arcane.iris.modded.ModdedTileData; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.matter.Matter; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +final class ModdedObjectPlacer implements IObjectPlacer { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int DEFAULT_FLUID_HEIGHT = 63; + + private final ServerLevel level; + private final Engine engine; + private final Map undo = new HashMap<>(); + private int writes = 0; + private int nonAirWrites = 0; + private int skippedTiles = 0; + private int restoredTiles = 0; + + ModdedObjectPlacer(ServerLevel level, @Nullable Engine engine) { + this.level = level; + this.engine = engine; + } + + Map undoSnapshot() { + return undo; + } + + int writes() { + return writes; + } + + int nonAirWrites() { + return nonAirWrites; + } + + int skippedTiles() { + return skippedTiles; + } + + int restoredTiles() { + return restoredTiles; + } + + @Override + public int getHighest(int x, int z, IrisData data) { + return level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1; + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return level.getHeight(ignoreFluid ? Heightmap.Types.OCEAN_FLOOR : Heightmap.Types.MOTION_BLOCKING, x, z) - 1; + } + + @Override + public void set(int x, int y, int z, PlatformBlockState s) { + if (y <= level.getMinY() || y >= level.getMinY() + level.getHeight()) { + return; + } + BlockPos pos = new BlockPos(x, y, z); + BlockState current = level.getBlockState(pos); + if (current.is(Blocks.BEDROCK)) { + return; + } + BlockState target = (BlockState) s.nativeHandle(); + undo.putIfAbsent(pos, current); + level.setBlock(pos, target, Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE); + writes++; + if (!target.isAir()) { + nonAirWrites++; + } + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + return ModdedBlockState.of(level.getBlockState(new BlockPos(x, y, z)), null); + } + + @Override + public boolean isPreventingDecay() { + return false; + } + + /** + * Mantle Y is relative to the world minimum height while this placer works in absolute world Y, so shift + * before the lookup. Only answers from an already loaded mantle chunk: a hand placed object can sit + * anywhere, and loading a mantle chunk to answer a carve probe would generate terrain as a side effect. + */ + @Override + public boolean isCarved(int x, int y, int z) { + if (engine == null) { + return false; + } + Mantle mantle = engine.getMantle().getMantle(); + if (mantle.isClosed() || !mantle.isChunkLoaded(x >> 4, z >> 4)) { + return false; + } + return engine.getMantle().isCarved(x, y - engine.getWorld().minHeight(), z); + } + + @Override + public boolean isSolid(int x, int y, int z) { + return ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(x, y, z))); + } + + /** + * Engine height stream against the dimension fluid height, both engine relative, so no shift here. Needs a + * ready complex; the placer also runs from commands against levels that never bound an engine. + */ + @Override + public boolean isUnderwater(int x, int z) { + return engine != null && engine.getComplex() != null && engine.getMantle().isUnderwater(x, z); + } + + /** + * IrisDimension fluid height is engine relative while this placer works in absolute world Y, so shift it + * up by the engine minimum before handing it to object placement. + */ + @Override + public int getFluidHeight() { + return engine == null + ? DEFAULT_FLUID_HEIGHT + : engine.getMinHeight() + engine.getDimension().getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return false; + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + if (!(tile instanceof ModdedTileData moddedTile)) { + skippedTiles++; + return; + } + BlockPos pos = new BlockPos(xx, yy, zz); + BlockState state = level.getBlockState(pos); + BlockState adjusted = moddedTile.adjustBlockState(state); + if (adjusted != state) { + level.setBlock(pos, adjusted, Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE); + state = adjusted; + } + if (!state.hasBlockEntity()) { + skippedTiles++; + return; + } + try { + BlockEntity restored = level.getBlockEntity(pos); + if (restored == null && state.getBlock() instanceof EntityBlock entityBlock) { + restored = entityBlock.newBlockEntity(pos, state); + } + if (restored == null) { + skippedTiles++; + return; + } + level.setBlockEntity(restored); + if (!moddedTile.apply(restored, level)) { + skippedTiles++; + return; + } + restoredTiles++; + } catch (Throwable e) { + LOGGER.error("Iris tile restore failed at {} {} {}", xx, yy, zz, e); + skippedTiles++; + } + } + + @Override + public void setData(int xx, int yy, int zz, T data) { + } + + @Override + public T getData(int xx, int yy, int zz, Class t) { + return null; + } + + @Override + public Engine getEngine() { + return engine; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectUndo.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectUndo.java new file mode 100644 index 000000000..236f503ca --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectUndo.java @@ -0,0 +1,135 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class ModdedObjectUndo { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int MAX_ENTRIES_PER_OWNER = 32; + private static final ConcurrentHashMap> UNDOS = new ConcurrentHashMap<>(); + private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); + public static final UUID CONSOLE = new UUID(0L, 0L); + + private ModdedObjectUndo() { + } + + private record Entry(ServerLevel level, Map blocks) { + } + + public static void init() { + if (INITIALIZED.compareAndSet(false, true)) { + LOGGER.info("Iris object undo service ready (bounded to {} paste(s) per player)", MAX_ENTRIES_PER_OWNER); + } + } + + public static void record(UUID owner, ServerLevel level, Map oldBlocks) { + if (oldBlocks == null || oldBlocks.isEmpty()) { + return; + } + Deque queue = UNDOS.computeIfAbsent(owner, (UUID key) -> new ArrayDeque<>()); + synchronized (queue) { + queue.addLast(new Entry(level, oldBlocks)); + while (queue.size() > MAX_ENTRIES_PER_OWNER) { + queue.pollFirst(); + } + } + } + + public static int size(UUID owner) { + Deque queue = UNDOS.get(owner); + if (queue == null) { + return 0; + } + synchronized (queue) { + return queue.size(); + } + } + + public static int undo(UUID owner, int amount) { + Deque queue = UNDOS.get(owner); + if (queue == null) { + return 0; + } + int reverted = 0; + while (reverted < amount) { + Entry entry; + synchronized (queue) { + entry = queue.pollLast(); + } + if (entry == null) { + break; + } + // Identity check, not just null: a studio closed and reopened under the same + // dimension id must never have blocks replayed into the dead ServerLevel. + MinecraftServer server = entry.level().getServer(); + if (server == null || server.getLevel(entry.level().dimension()) != entry.level()) { + LOGGER.warn("Iris object undo: skipped a stale entry for removed dimension {}", + entry.level().dimension().identifier()); + continue; + } + int writes = 0; + for (Map.Entry block : entry.blocks().entrySet()) { + try { + entry.level().setBlock(block.getKey(), block.getValue(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE); + writes++; + } catch (Throwable e) { + LOGGER.error("Iris object undo: failed to revert a block at {}", block.getKey(), e); + } + } + LOGGER.info("Iris object undo: reverted {} block(s) in {}", writes, entry.level().dimension().identifier()); + reverted++; + } + return reverted; + } + + /** + * Drops every entry recorded against the given level. Called on dimension removal so a + * closed studio releases its block snapshots and the ServerLevel reference. + */ + public static void forget(ServerLevel level) { + if (level == null) { + return; + } + UNDOS.entrySet().removeIf((Map.Entry> ownerEntry) -> { + Deque queue = ownerEntry.getValue(); + synchronized (queue) { + queue.removeIf((Entry entry) -> entry.level() == level); + return queue.isEmpty(); + } + }); + } + + public static void clearAll() { + UNDOS.clear(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java new file mode 100644 index 000000000..614fa74de --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java @@ -0,0 +1,312 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.core.pack.PackResourceCleanup; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.core.pack.PackValidationResult; +import art.arcane.iris.core.pack.PackValidator; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class ModdedPackCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + + private ModdedPackCommands() { + } + + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); + + root.then(Commands.literal("validate") + .executes((CommandContext context) -> validate(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> validate(context.getSource(), StringArgumentType.getString(context, "pack"))))); + root.then(Commands.literal("v") + .executes((CommandContext context) -> validate(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> validate(context.getSource(), StringArgumentType.getString(context, "pack"))))); + + root.then(Commands.literal("cleanup") + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), false)) + .then(Commands.literal("apply") + .executes((CommandContext context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), true))))); + root.then(Commands.literal("c") + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), false)) + .then(Commands.literal("apply") + .executes((CommandContext context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), true))))); + + root.then(Commands.literal("restore") + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), false)) + .then(Commands.literal("apply") + .executes((CommandContext context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), true))))); + root.then(Commands.literal("r") + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), false)) + .then(Commands.literal("apply") + .executes((CommandContext context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), true))))); + + root.then(Commands.literal("status") + .executes((CommandContext context) -> status(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> status(context.getSource(), StringArgumentType.getString(context, "pack"))))); + root.then(Commands.literal("s") + .executes((CommandContext context) -> status(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> status(context.getSource(), StringArgumentType.getString(context, "pack"))))); + + return root; + } + + public static File packsRoot() { + return IrisPlatforms.get().packsFolderNoCreate(); + } + + private static int validate(CommandSourceStack source, String pack) { + File packsRoot = packsRoot(); + if (!packsRoot.isDirectory()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_PACKS_FOLDER_NOT_FOUND, MessageArgument.untrusted("value", packsRoot.getAbsolutePath()))); + return 0; + } + + List targets = new ArrayList<>(); + if (pack == null || pack.isBlank()) { + List dirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot); + if (dirs.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_NO_PACKS_VALIDATE_UNDER, MessageArgument.untrusted("value", packsRoot.getAbsolutePath()))); + return 0; + } + targets.addAll(dirs); + } else { + File target = PackDirectoryResolver.resolveExisting(packsRoot, pack); + if (target == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_PACK_NOT_FOUND_UNDER, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("value", packsRoot.getAbsolutePath()))); + return 0; + } + targets.add(target); + } + + MinecraftServer server = source.getServer(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_VALIDATING_PACK_S, MessageArgument.untrusted("value", targets.size()))); + Thread thread = new Thread(() -> { + int broken = 0; + for (File target : targets) { + try { + PackValidationResult result = PackValidator.validate(target); + PackValidationRegistry.publish(result); + if (!result.isLoadable()) { + broken++; + } + server.execute(() -> report(source, result)); + } catch (Throwable e) { + LOGGER.error("Iris pack validation failed for {}", target.getName(), e); + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_VALIDATION_FAILED, MessageArgument.untrusted("value", target.getName()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage()))))); + broken++; + } + } + int brokenTotal = broken; + server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_VALIDATION_COMPLETE_BROKEN_PACKS, MessageArgument.untrusted("brokenTotal", brokenTotal), MessageArgument.untrusted("value", targets.size())))); + }, "Iris Pack Validator"); + thread.setDaemon(true); + thread.start(); + return 1; + } + + private static int cleanup(CommandSourceStack source, String pack, boolean apply) { + File packFolder = PackDirectoryResolver.resolveExisting(packsRoot(), pack); + if (packFolder == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_PACK_NOT_FOUND_UNDER_2, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("value", packsRoot().getAbsolutePath()))); + return 0; + } + MinecraftServer server = source.getServer(); + Thread thread = new Thread(() -> { + if (apply) { + PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder); + server.execute(() -> reportCleanupApply(source, pack, result)); + } else { + PackResourceCleanup.Preview result = PackResourceCleanup.preview(packFolder); + server.execute(() -> reportCleanupPreview(source, pack, result)); + } + }, "Iris Pack Cleanup"); + thread.setDaemon(true); + thread.start(); + return 1; + } + + private static int restore(CommandSourceStack source, String pack, boolean apply) { + File packFolder = PackDirectoryResolver.resolveExisting(packsRoot(), pack); + if (packFolder == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_PACK_NOT_FOUND_UNDER_3, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("value", packsRoot().getAbsolutePath()))); + return 0; + } + MinecraftServer server = source.getServer(); + Thread thread = new Thread(() -> { + if (apply) { + PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder); + server.execute(() -> reportRestoreApply(source, pack, result)); + } else { + PackResourceCleanup.RestorePreview result = PackResourceCleanup.previewRestore(packFolder); + server.execute(() -> reportRestorePreview(source, pack, result)); + } + }, "Iris Pack Restore"); + thread.setDaemon(true); + thread.start(); + return 1; + } + + private static int status(CommandSourceStack source, String pack) { + if (pack == null || pack.isBlank()) { + Map snapshot = PackValidationRegistry.snapshot(); + if (snapshot.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST)); + return 0; + } + for (Map.Entry entry : snapshot.entrySet()) { + PackValidationResult result = entry.getValue(); + String tag = result.isLoadable() ? "OK" : "BROKEN"; + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_BLOCKING_WARNINGS, MessageArgument.untrusted("tag", tag), MessageArgument.untrusted("value", entry.getKey()), MessageArgument.untrusted("value2", result.getBlockingErrors().size()), MessageArgument.untrusted("value3", result.getWarnings().size()))); + } + return 1; + } + PackValidationResult result = PackValidationRegistry.get(pack); + if (result == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))); + return 0; + } + report(source, result); + return 1; + } + + private static void report(CommandSourceStack source, PackValidationResult result) { + if (result.isLoadable()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_PACK_IS_LOADABLE_WARNINGS, MessageArgument.untrusted("value", result.getPackName()), MessageArgument.untrusted("value2", result.getWarnings().size()))); + } else { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_PACK_IS_BROKEN, MessageArgument.untrusted("value", result.getPackName()))); + for (String reason : result.getBlockingErrors()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_MESSAGE, MessageArgument.untrusted("reason", reason))); + } + } + int warningMax = Math.min(10, result.getWarnings().size()); + for (int i = 0; i < warningMax; i++) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_MESSAGE_2, MessageArgument.untrusted("value", result.getWarnings().get(i)))); + } + if (result.getWarnings().size() > warningMax) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_MORE_WARNING_S, MessageArgument.untrusted("value", (result.getWarnings().size() - warningMax)))); + } + } + + private static void reportCleanupPreview(CommandSourceStack source, String pack, PackResourceCleanup.Preview result) { + if (!result.success()) { + IrisModdedCommands.fail(source, result.error()); + return; + } + if (!result.hasCandidates()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", pack))); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_CLEANUP_PREVIEW_PACK_CANDIDATE_S_NO_FILES_WERE_CHANGED, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("value", result.candidatePaths().size()))); + reportPaths(source, result.candidatePaths(), "candidate"); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_RUN_IRIS_PACK_CLEANUP_APPLY_QUARANTINE_AFTER_FRESH_SCAN, MessageArgument.untrusted("pack", pack))); + } + + private static void reportCleanupApply(CommandSourceStack source, String pack, PackResourceCleanup.ApplyResult result) { + if (!result.success()) { + IrisModdedCommands.fail(source, result.error()); + reportPaths(source, result.quarantinedPaths(), "still quarantined"); + return; + } + if (!result.changed()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_NO_CLEANUP_CANDIDATES_FOUND_PACK_2, MessageArgument.untrusted("pack", pack))); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("value", result.quarantinedPaths().size()), MessageArgument.untrusted("value2", result.quarantinePath()))); + reportPaths(source, result.quarantinedPaths(), "quarantined"); + } + + private static void reportRestorePreview(CommandSourceStack source, String pack, PackResourceCleanup.RestorePreview result) { + if (!result.success()) { + IrisModdedCommands.fail(source, result.error()); + return; + } + if (!result.hasFiles()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", pack))); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_RESTORE_PREVIEW_FILE_S_NO_FILES_WERE_CHANGED, MessageArgument.untrusted("value", result.dumpPath()), MessageArgument.untrusted("value2", result.filePaths().size()))); + reportPaths(source, result.filePaths(), "file"); + if (!result.conflicts().isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_RESTORE_IS_BLOCKED_BY_EXISTING_DESTINATION_S, MessageArgument.untrusted("value", result.conflicts().size()))); + reportPaths(source, result.conflicts(), "conflict"); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_RUN_IRIS_PACK_RESTORE_APPLY_RESTORE_AFTER_FRESH_CONFLICT_CHECK, MessageArgument.untrusted("pack", pack))); + } + + private static void reportRestoreApply(CommandSourceStack source, String pack, PackResourceCleanup.RestoreResult result) { + if (!result.conflicts().isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_RESTORE_REFUSED_BECAUSE_DESTINATION_S_ALREADY_EXIST, MessageArgument.untrusted("value", result.conflicts().size()))); + reportPaths(source, result.conflicts(), "conflict"); + return; + } + if (!result.success()) { + IrisModdedCommands.fail(source, result.error()); + return; + } + if (!result.changed()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_NOTHING_RESTORE_PACK_2, MessageArgument.untrusted("pack", pack))); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_RESTORED_FILE_S_FROM, MessageArgument.untrusted("value", result.restoredPaths().size()), MessageArgument.untrusted("value2", result.dumpPath()))); + reportPaths(source, result.restoredPaths(), "restored"); + } + + private static void reportPaths(CommandSourceStack source, List paths, String label) { + int max = Math.min(10, paths.size()); + for (int i = 0; i < max; i++) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_MESSAGE_3, MessageArgument.untrusted("label", label), MessageArgument.untrusted("value", paths.get(i)))); + } + if (paths.size() > max) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_MORE, MessageArgument.untrusted("value", (paths.size() - max)))); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenBossBar.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenBossBar.java new file mode 100644 index 000000000..37d2399c6 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenBossBar.java @@ -0,0 +1,147 @@ +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.core.protocol.IrisProtocolServer; +import art.arcane.iris.core.protocol.IrisSession; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisProtocol; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.localization.TextKey; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerBossEvent; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.BossEvent; + +import java.util.UUID; + +public final class ModdedPregenBossBar { + private static final UUID BAR_ID = UUID.fromString("14150000-1e15-4a2b-9c3d-1115e9a1cafe"); + private static final int UPDATE_INTERVAL_TICKS = 10; + + private static volatile ServerBossEvent bar; + private static volatile UUID viewer; + private static int sinceUpdate; + + private ModdedPregenBossBar() { + } + + public static synchronized void begin(ServerPlayer player) { + clear(); + if (player == null) { + return; + } + if (hasClientPregenHud(player)) { + return; + } + viewer = player.getUUID(); + bar = new ServerBossEvent( + BAR_ID, + Component.literal(IrisLanguage.plain(RuntimeUiMessages.PREGEN_STARTING)), + BossEvent.BossBarColor.GREEN, + BossEvent.BossBarOverlay.PROGRESS + ); + bar.setProgress(0.0F); + bar.addPlayer(player); + sinceUpdate = UPDATE_INTERVAL_TICKS; + } + + public static void tick(MinecraftServer server) { + ServerBossEvent active = bar; + if (active == null) { + return; + } + PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot(); + if (progress == null) { + clear(); + return; + } + if (++sinceUpdate < UPDATE_INTERVAL_TICKS) { + return; + } + sinceUpdate = 0; + reattach(server, active); + active.setProgress((float) clamp01(progress.percent() / 100.0D)); + active.setColor(progress.paused() ? BossEvent.BossBarColor.YELLOW : BossEvent.BossBarColor.GREEN); + active.setName(nameFor(progress)); + } + + private static void reattach(MinecraftServer server, ServerBossEvent active) { + UUID id = viewer; + if (id == null || server == null) { + return; + } + ServerPlayer player = server.getPlayerList().getPlayer(id); + if (player != null && !active.getPlayers().contains(player)) { + active.addPlayer(player); + } + } + + private static Component nameFor(PregeneratorJob.PregenProgress progress) { + TextKey message = progress.paused() + ? RuntimeUiMessages.PREGEN_BOSSBAR_PAUSED + : RuntimeUiMessages.PREGEN_BOSSBAR_RUNNING; + if (progress.paused()) { + return ModdedCommandFeedback.text(IrisLanguage.plain( + message, + MessageArgument.trusted("generated", Form.f(progress.generated())), + MessageArgument.trusted("total", Form.f(progress.totalChunks())), + MessageArgument.trusted("percent", String.format("%.1f", progress.percent())) + ), ModdedCommandFeedback.DARK_GREEN); + } + String eta = progress.eta() > 0L + ? IrisLanguage.plain( + RuntimeUiMessages.PREGEN_ETA_FRAGMENT, + MessageArgument.trusted("eta", Form.duration(progress.eta(), 1)) + ) + : ""; + String failed = progress.failed() > 0L + ? IrisLanguage.plain( + RuntimeUiMessages.PREGEN_FAILED_FRAGMENT, + MessageArgument.trusted("failed", Form.f(progress.failed())) + ) + : ""; + return ModdedCommandFeedback.text(IrisLanguage.plain( + message, + MessageArgument.trusted("generated", Form.f(progress.generated())), + MessageArgument.trusted("total", Form.f(progress.totalChunks())), + MessageArgument.trusted("percent", String.format("%.1f", progress.percent())), + MessageArgument.trusted("speed", Form.f((int) progress.chunksPerSecond())), + MessageArgument.trusted("eta", eta), + MessageArgument.trusted("failed", failed) + ), ModdedCommandFeedback.DARK_GREEN); + } + + private static boolean hasClientPregenHud(ServerPlayer player) { + IrisProtocolServer protocol = IrisServices.getOrNull(IrisProtocolServer.class); + if (protocol == null) { + return false; + } + IrisSession session = protocol.registry().get(player.getUUID().toString()); + return session != null && session.isReady() && session.hasCapability(IrisProtocol.CAPABILITY_PREGEN); + } + + private static double clamp01(double value) { + if (value < 0.0D) { + return 0.0D; + } + if (value > 1.0D) { + return 1.0D; + } + return value; + } + + public static synchronized void clear() { + ServerBossEvent existing = bar; + if (existing != null) { + existing.removeAllPlayers(); + existing.setVisible(false); + } + bar = null; + viewer = null; + sinceUpdate = 0; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java new file mode 100644 index 000000000..a8bbee792 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java @@ -0,0 +1,108 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.localization.MessageArgument; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; + +final class ModdedPregenCommands { + private ModdedPregenCommands() { + } + + static int pregenStart(CommandContext context, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) throws CommandSyntaxException { + CommandSourceStack source = context.getSource(); + int radius = IntegerArgumentType.getInteger(context, "radius"); + int centerX = withCenter ? IntegerArgumentType.getInteger(context, "x") : 0; + int centerZ = withCenter ? IntegerArgumentType.getInteger(context, "z") : 0; + ServerLevel level = withDimension ? DimensionArgument.getDimension(context, "dimension") : source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + if (withDimension) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_SEE_IRIS_INFO_LOADED_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()))); + } else { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CURRENT_DIMENSION_IS_NOT_GENERATED_BY_IRIS_NAME_ONE_EXPLICITLY, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("radius", radius))); + } + return 0; + } + boolean showGui = gui && ModdedGuiHost.isGuiLaunchable(); + boolean started; + try { + started = ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache); + } catch (IllegalArgumentException failure) { + IrisModdedCommands.fail(source, failure.getMessage()); + return 0; + } + if (!started) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS)); + return 0; + } + ModdedPregenBossBar.begin(source.getPlayer()); + String guiNote; + if (!gui) { + guiNote = ""; + } else if (showGui) { + guiNote = " A progress map window is opening on the server display."; + } else { + guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")"; + } + String modeNote = " Mode: " + (sync ? "sync" : "async") + (nocache ? ", cache disabled." : ", resumable (checkpoint cache)."); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", (radius * 2)), MessageArgument.untrusted("value3", (radius * 2)), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ), MessageArgument.untrusted("modeNote", modeNote), MessageArgument.untrusted("guiNote", guiNote))); + return 1; + } + + static int pregenStop(CommandSourceStack source) { + if (ModdedPregenJob.stop()) { + ModdedPregenBossBar.clear(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STOPPING_PREGENERATION_FINISHING_UP_CURRENT_REGION)); + return 1; + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_STOP)); + return 0; + } + + static int pregenPause(CommandSourceStack source) { + Boolean paused = ModdedPregenJob.pauseResume(); + if (paused == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_PAUSE_RESUME)); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_IS_NOW, MessageArgument.trusted("value", IrisLanguage.plain(paused.booleanValue() ? RuntimeUiMessages.STATUS_PAUSED_LOWER : RuntimeUiMessages.STATUS_RUNNING_LOWER)))); + return 1; + } + + static int pregenStatus(CommandSourceStack source) { + Component status = ModdedPregenJob.statusComponent(); + if (status == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK)); + return 0; + } + IrisModdedCommands.ok(source, status); + return 1; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java new file mode 100644 index 000000000..fc257cf35 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java @@ -0,0 +1,250 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.core.pregenerator.PregenPerformanceProfile; +import art.arcane.iris.core.pregenerator.PregenTask; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.core.pregenerator.cache.PregenCache; +import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.localization.MessageArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.LevelResource; + +import java.io.File; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; + +public final class ModdedPregenJob { + private static final long SHUTDOWN_TIMEOUT_MILLIS = 10_000L; + private static final AtomicReference ACTIVE = new AtomicReference<>(); + private static volatile String dimension = "?"; + + private ModdedPregenJob() { + } + + public static boolean start(MinecraftServer server, ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean gui, boolean sync, boolean cached) { + if (PregeneratorJob.getInstance() != null) { + return false; + } + + PregenTask task = PregenTask.builder() + .gui(gui) + .center(new Position2(centerBlockX, centerBlockZ)) + .radiusX(radiusBlocks) + .radiusZ(radiusBlocks) + .build(); + PregenPerformanceProfile.apply(engine); + ModdedPregenMethod moddedMethod = new ModdedPregenMethod(level, engine, sync); + PregeneratorMethod method = moddedMethod; + if (cached) { + method = new CachedPregenMethod(method, PregenCache.create(cacheDirectory(level)).sync(), task); + } + ActivePregen active = new ActivePregen(engine, moddedMethod); + ACTIVE.set(active); + dimension = level.dimension().identifier().toString(); + try { + PregeneratorJob job = new PregeneratorJob(task, method, engine); + job.whenDone(() -> { + if (!moddedMethod.hasPendingFinalSave()) { + ACTIVE.compareAndSet(active, null); + } + }); + return true; + } catch (Throwable failure) { + ACTIVE.compareAndSet(active, null); + throw propagate(failure); + } + } + + private static File cacheDirectory(ServerLevel level) { + File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile(); + return new File(worldFolder, "iris" + File.separator + "pregen"); + } + + public static boolean stop() { + return PregeneratorJob.shutdownInstance(); + } + + public static void shutdown() { + ActivePregen active = ACTIVE.get(); + shutdownAndSave(active, () -> PregeneratorJob.shutdownAndWait(SHUTDOWN_TIMEOUT_MILLIS)); + } + + public static boolean shutdownForWorld(String worldIdentity) { + PregeneratorJob job = PregeneratorJob.getInstance(); + if (job == null || !job.targetsWorldIdentity(worldIdentity)) { + return false; + } + ActivePregen active = matchingActive(worldIdentity); + return shutdownAndSave(active, () -> PregeneratorJob.shutdownInstanceForWorld(worldIdentity)); + } + + public static Boolean pauseResume() { + if (PregeneratorJob.getInstance() == null) { + return null; + } + PregeneratorJob.pauseResume(); + return PregeneratorJob.isPaused(); + } + + public static Component statusComponent() { + PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot(); + if (progress == null) { + return null; + } + + MutableComponent status = Component.empty(); + status.append(ModdedCommandFeedback.header(IrisLanguage.plain(RuntimeUiMessages.PREGEN_HEADER))); + status.append(Component.literal("\n")); + status.append(ModdedCommandFeedback.text(IrisLanguage.plain( + RuntimeUiMessages.PREGEN_STATUS_CONTEXT, + MessageArgument.untrusted("dimension", dimension), + MessageArgument.untrusted("method", progress.method()) + ), ModdedCommandFeedback.DARK_GREEN)); + status.append(Component.literal("\n")); + status.append(ModdedCommandFeedback.progressBar(progress.percent(), 32)); + status.append(ModdedCommandFeedback.text(" " + IrisLanguage.plain( + RuntimeUiMessages.PREGEN_STATUS_PROGRESS, + MessageArgument.trusted("percent", String.format("%.1f", progress.percent())) + ), ModdedCommandFeedback.USAGE)); + status.append(Component.literal("\n")); + status.append(ModdedCommandFeedback.text(chunksStatus(progress), ModdedCommandFeedback.DARK_GREEN)); + status.append(Component.literal("\n")); + status.append(ModdedCommandFeedback.text(IrisLanguage.plain( + progress.paused() + ? RuntimeUiMessages.PREGEN_STATUS_TIME_PAUSED + : RuntimeUiMessages.PREGEN_STATUS_TIME, + MessageArgument.trusted("eta", Form.duration(progress.eta(), 2)), + MessageArgument.trusted("elapsed", Form.duration(progress.elapsed(), 2)) + ), ModdedCommandFeedback.DARK_GREEN)); + status.append(Component.literal("\n")); + status.append(ModdedCommandFeedback.button( + IrisLanguage.plain(RuntimeUiMessages.PREGEN_PAUSE_BUTTON), + "/iris pregen pause", + IrisLanguage.plain(RuntimeUiMessages.PREGEN_PAUSE_HOVER), + true + )); + status.append(ModdedCommandFeedback.text(" ", ModdedCommandFeedback.OPTIONAL)); + status.append(ModdedCommandFeedback.button( + IrisLanguage.plain(RuntimeUiMessages.PREGEN_STOP_BUTTON), + "/iris pregen stop", + IrisLanguage.plain(RuntimeUiMessages.PREGEN_STOP_HOVER), + true + )); + status.append(Component.literal("\n")); + status.append(ModdedCommandFeedback.footer()); + return status; + } + + private static String chunksStatus(PregeneratorJob.PregenProgress progress) { + if (progress.failed() > 0) { + return IrisLanguage.plain( + RuntimeUiMessages.PREGEN_STATUS_CHUNKS_FAILED, + MessageArgument.trusted("generated", Form.f(progress.generated())), + MessageArgument.trusted("total", Form.f(progress.totalChunks())), + MessageArgument.trusted("overall", Form.f(progress.overallChunksPerSecond(), 1)), + MessageArgument.trusted("tenSecond", Form.f(progress.chunksPerSecond(), 1)), + MessageArgument.trusted("thirtySecond", Form.f(progress.thirtySecondChunksPerSecond(), 1)), + MessageArgument.trusted("sixtySecond", Form.f(progress.sixtySecondChunksPerSecond(), 1)), + MessageArgument.trusted("failed", Form.f(progress.failed())) + ); + } + return IrisLanguage.plain( + RuntimeUiMessages.PREGEN_STATUS_CHUNKS, + MessageArgument.trusted("generated", Form.f(progress.generated())), + MessageArgument.trusted("total", Form.f(progress.totalChunks())), + MessageArgument.trusted("overall", Form.f(progress.overallChunksPerSecond(), 1)), + MessageArgument.trusted("tenSecond", Form.f(progress.chunksPerSecond(), 1)), + MessageArgument.trusted("thirtySecond", Form.f(progress.thirtySecondChunksPerSecond(), 1)), + MessageArgument.trusted("sixtySecond", Form.f(progress.sixtySecondChunksPerSecond(), 1)) + ); + } + + private static ActivePregen matchingActive(String worldIdentity) { + ActivePregen active = ACTIVE.get(); + return active != null && active.targets(worldIdentity) ? active : null; + } + + private static boolean shutdownAndSave(ActivePregen active, BooleanSupplier shutdown) { + boolean deferred = active != null && active.method().deferFinalSaveToServerThread(); + boolean stopped = false; + boolean result = false; + Throwable failure = null; + try { + result = shutdown.getAsBoolean(); + stopped = result; + } catch (Throwable shutdownFailure) { + failure = shutdownFailure; + } + + if (deferred) { + try { + if (stopped) { + active.method().completeDeferredFinalSave(); + } else { + active.method().cancelDeferredFinalSave(); + } + } catch (Throwable saveFailure) { + if (failure == null) { + failure = saveFailure; + } else if (saveFailure != failure) { + failure.addSuppressed(saveFailure); + } + } + } + + if (failure == null && (stopped || PregeneratorJob.getInstance() == null)) { + ACTIVE.compareAndSet(active, null); + } + if (failure != null) { + throw propagate(failure); + } + return result; + } + + private static RuntimeException propagate(Throwable failure) { + if (failure instanceof RuntimeException runtimeException) { + return runtimeException; + } + if (failure instanceof Error fatalError) { + throw fatalError; + } + return new IllegalStateException("Iris modded pregenerator shutdown failed", failure); + } + + private record ActivePregen(Engine engine, ModdedPregenMethod method) { + private boolean targets(String worldIdentity) { + return engine != null + && engine.getWorld() != null + && Objects.equals(engine.getWorld().identity(), worldIdentity); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java new file mode 100644 index 000000000..2d3930a0c --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java @@ -0,0 +1,693 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.pregenerator.PregenListener; +import art.arcane.iris.core.pregenerator.PregenMantleBackpressure; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.modded.ModdedGenPool; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +public final class ModdedPregenMethod implements PregeneratorMethod { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final TicketType PREGEN_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING | TicketType.FLAG_KEEP_DIMENSION_ACTIVE); + private static final int ADAPTIVE_TIMEOUT_STEP = 3; + private static final long ADAPTIVE_RECOVERY_INTERVAL = 64L; + private static final long FINAL_SAVE_TIMEOUT_MILLIS = 10_000L; + private static final long FINAL_SAVE_POLL_MILLIS = 50L; + private static final AtomicBoolean SERVER_DEAD_LOGGED = new AtomicBoolean(); + + private final ServerLevel level; + private final Engine engine; + private final boolean sync; + private final int maxInFlight; + private final int minInFlight; + private final Semaphore semaphore; + private final Object permitMonitor = new Object(); + private final AtomicInteger inFlight = new AtomicInteger(); + private final AtomicInteger inFlightPeak = new AtomicInteger(); + private final AtomicInteger adaptiveLimit; + private final AtomicInteger timeoutStreak = new AtomicInteger(); + private final AtomicLong completed = new AtomicLong(); + private final AtomicBoolean finalSaveOnServerThread = new AtomicBoolean(false); + private final AtomicBoolean finalSaveDeferred = new AtomicBoolean(false); + private final AtomicBoolean finalSaveCompleted = new AtomicBoolean(false); + private final AtomicReference queuedFinalSave = new AtomicReference<>(); + private final AtomicBoolean stallHintLogged = new AtomicBoolean(false); + private final AtomicBoolean failureDetailLogged = new AtomicBoolean(false); + private final int timeoutSeconds; + private final PregenMantleBackpressure backpressure; + private final PauseWhenEmptyGuard pauseGuard; + + public ModdedPregenMethod(ServerLevel level, Engine engine) { + this(level, engine, false); + } + + public ModdedPregenMethod(ServerLevel level, Engine engine, boolean sync) { + this.level = level; + this.engine = engine; + this.sync = sync; + this.pauseGuard = new PauseWhenEmptyGuard(level.getServer()); + IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); + this.maxInFlight = Math.max(8, pregen.getModdedPregenInFlight()); + this.minInFlight = Math.max(4, Math.min(16, maxInFlight / 4)); + this.semaphore = new Semaphore(maxInFlight, true); + this.adaptiveLimit = new AtomicInteger(sync ? 1 : maxInFlight); + this.timeoutSeconds = Math.max(120, pregen.getChunkLoadTimeoutSeconds()); + this.backpressure = new PregenMantleBackpressure( + this::getMantle, + pregen.getEffectiveResidentTectonicPlates(engine.getHeight()), + pregen.getMantleBackpressureWaitMs(), + pregen.getMantleBackpressureTimeoutMs(), + () -> { + }, + () -> "dim=" + level.dimension().identifier()); + } + + @Override + public void init() { + pauseGuard.suspend(); + LOGGER.info("Iris modded pregen init: dim={} mode={} inFlightCap={} timeout={}s workerPool={} chunkSystem={}", + level.dimension().identifier(), + sync ? "sync" : "async", + sync ? 1 : maxInFlight, + timeoutSeconds, + describeWorkerPool(), + ModdedGenPool.describeChunkSystem()); + if (!sync && !ModdedGenPool.parallelChunkSystem()) { + LOGGER.info("Iris pregen note: this loader uses the vanilla main-thread chunk system, which caps pregen throughput. For Bukkit-level speed on Fabric install C2ME (Concurrent Chunk Management Engine); on servers use Paper."); + } + } + + @Override + public void close() { + try { + if (!sync) { + try { + semaphore.tryAcquire(maxInFlight, 5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + LOGGER.info("Iris modded pregen done: dim={} completed={} peakInFlight={} finalLimit={}", + level.dimension().identifier(), completed.get(), inFlightPeak.get(), adaptiveLimit.get()); + if (deferFinalSaveIfRequested()) { + return; + } + saveLevel(true); + } finally { + pauseGuard.restore(); + } + } + + @Override + public void save() { + saveLevel(false); + } + + boolean deferFinalSaveToServerThread() { + if (!level.getServer().isSameThread()) { + return false; + } + finalSaveOnServerThread.set(true); + return true; + } + + void completeDeferredFinalSave() { + requireServerThreadForFinalSave(); + cancelQueuedFinalSave(); + if (!finalSaveCompleted.get()) { + saveLevelOnServerThread(); + finalSaveCompleted.set(true); + } + finalSaveDeferred.set(false); + finalSaveOnServerThread.set(false); + } + + void cancelDeferredFinalSave() { + requireServerThreadForFinalSave(); + finalSaveOnServerThread.set(false); + if (finalSaveDeferred.get() || queuedFinalSave.get() != null) { + cancelQueuedFinalSave(); + if (!finalSaveCompleted.get()) { + saveLevelOnServerThread(); + finalSaveCompleted.set(true); + } + finalSaveDeferred.set(false); + } + } + + boolean hasPendingFinalSave() { + return finalSaveOnServerThread.get() + || finalSaveDeferred.get() + || queuedFinalSave.get() != null; + } + + private void saveLevel(boolean wait) { + if (wait) { + saveFinalLevel(); + return; + } + MinecraftServer server = level.getServer(); + if (server.isSameThread()) { + saveLevelOnServerThread(); + return; + } + server.execute(this::saveLevelOnServerThread); + } + + private void saveFinalLevel() { + MinecraftServer server = level.getServer(); + if (server.isSameThread()) { + saveLevelOnServerThread(); + finalSaveCompleted.set(true); + return; + } + if (deferFinalSaveIfRequested()) { + return; + } + + FinalSaveRequest request = new FinalSaveRequest(); + if (!queuedFinalSave.compareAndSet(null, request)) { + throw new IllegalStateException("Iris pregen final save is already queued for " + + level.dimension().identifier()); + } + try { + server.execute(() -> executeFinalSave(request)); + } catch (RuntimeException | Error failure) { + queuedFinalSave.compareAndSet(request, null); + request.fail(failure); + throw failure; + } + awaitFinalSave(request); + } + + private void executeFinalSave(FinalSaveRequest request) { + if (!request.claim()) { + return; + } + try { + saveLevelOnServerThread(); + finalSaveCompleted.set(true); + request.complete(); + } catch (RuntimeException | Error failure) { + request.fail(failure); + throw failure; + } finally { + queuedFinalSave.compareAndSet(request, null); + } + } + + private void awaitFinalSave(FinalSaveRequest request) { + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS); + while (true) { + if (deferFinalSaveIfRequested()) { + return; + } + long remainingNanos = deadline - System.nanoTime(); + if (remainingNanos <= 0L) { + LOGGER.warn("Iris pregen level save did not complete in time for {}", level.dimension().identifier()); + return; + } + long waitMillis = Math.max(1L, Math.min(FINAL_SAVE_POLL_MILLIS, + TimeUnit.NANOSECONDS.toMillis(remainingNanos))); + try { + request.completion().get(waitMillis, TimeUnit.MILLISECONDS); + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } catch (TimeoutException e) { + continue; + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + LOGGER.error("Iris pregen level save failed for {}", level.dimension().identifier(), cause); + throw new IllegalStateException("Iris pregen level save failed for " + + level.dimension().identifier(), cause); + } + } + } + + private boolean deferFinalSaveIfRequested() { + if (!finalSaveOnServerThread.get()) { + return false; + } + finalSaveDeferred.set(true); + if (finalSaveOnServerThread.get()) { + return true; + } + finalSaveDeferred.set(false); + return false; + } + + private void cancelQueuedFinalSave() { + FinalSaveRequest request = queuedFinalSave.getAndSet(null); + if (request != null) { + request.cancel(); + } + } + + private void saveLevelOnServerThread() { + level.save(null, false, false); + } + + private void requireServerThreadForFinalSave() { + if (!level.getServer().isSameThread()) { + throw new IllegalStateException("Iris pregen final save must run on the Minecraft server thread for " + + level.dimension().identifier()); + } + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return false; + } + + @Override + public String getMethod(int x, int z) { + return "Modded"; + } + + @Override + public boolean isAsyncChunkMode() { + return !sync; + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + backpressure.apply(); + if (sync) { + generateChunkSync(x, z, listener); + return; + } + generateChunkAsync(x, z, listener); + } + + private void generateChunkSync(int x, int z, PregenListener listener) { + listener.onChunkGenerating(x, z); + ChunkPos pos = new ChunkPos(x, z); + CompletableFuture loadFuture = CompletableFuture + .supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(PREGEN_TICKET, pos, 0), level.getServer()) + .thenCompose((CompletableFuture inner) -> inner); + markSubmitted(); + try { + Object result = loadFuture.get(timeoutSeconds, TimeUnit.SECONDS); + if (result instanceof ChunkResult chunkResult && !chunkResult.isSuccess()) { + LOGGER.warn("Iris pregen chunk {},{} returned no chunk: {}", x, z, chunkResult.getError()); + listener.onChunkFailed(x, z); + return; + } + markCompleted(); + listener.onChunkGenerated(x, z); + cleanupMantleChunk(x, z); + listener.onChunkCleaned(x, z); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (TimeoutException | ExecutionException e) { + if (e instanceof TimeoutException) { + noteStallHint(); + } + logChunkFailure(x, z, e); + listener.onChunkFailed(x, z); + } finally { + markFinished(); + level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0)); + } + } + + private void generateChunkAsync(int x, int z, PregenListener listener) { + // A stopped server executes submitted tasks inline and its chunk futures never complete; without + // this abort the pregen thread spins hot against the dead chunk source until the JVM dies. + if (level.getServer().isStopped() || !level.getServer().isRunning()) { + if (SERVER_DEAD_LOGGED.compareAndSet(false, true)) { + LOGGER.error("Iris pregen aborting: the server is no longer running (dim={})", level.dimension().identifier()); + } + listener.onChunkFailed(x, z); + ModdedPregenJob.stop(); + return; + } + listener.onChunkGenerating(x, z); + try { + synchronized (permitMonitor) { + while (inFlight.get() >= adaptiveLimit.get()) { + permitMonitor.wait(500L); + } + } + semaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + markSubmitted(); + + ChunkPos pos = new ChunkPos(x, z); + CompletableFuture loadFuture = CompletableFuture + .supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(PREGEN_TICKET, pos, 0), level.getServer()) + .thenCompose((CompletableFuture inner) -> inner); + + loadFuture.orTimeout(timeoutSeconds, TimeUnit.SECONDS).whenComplete((Object result, Throwable error) -> { + level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0)); + try { + if (error != null) { + if (unwrap(error) instanceof TimeoutException) { + onTimeout(); + } + logChunkFailure(x, z, error); + listener.onChunkFailed(x, z); + return; + } + if (result instanceof ChunkResult chunkResult && !chunkResult.isSuccess()) { + LOGGER.warn("Iris pregen chunk {},{} returned no chunk: {}", x, z, chunkResult.getError()); + listener.onChunkFailed(x, z); + return; + } + onSuccess(); + markCompleted(); + listener.onChunkGenerated(x, z); + cleanupMantleChunk(x, z); + listener.onChunkCleaned(x, z); + } finally { + markFinished(); + semaphore.release(); + } + }); + } + + private void markSubmitted() { + int current = inFlight.incrementAndGet(); + inFlightPeak.accumulateAndGet(current, Math::max); + } + + private void logChunkFailure(int x, int z, Throwable failure) { + Throwable cause = unwrap(failure); + if (failureDetailLogged.compareAndSet(false, true)) { + LOGGER.warn("Iris pregen chunk {},{} failed; first failure follows", x, z, cause); + return; + } + LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, cause.toString()); + } + + private void markFinished() { + inFlight.decrementAndGet(); + if (sync) { + return; + } + synchronized (permitMonitor) { + permitMonitor.notifyAll(); + } + } + + private void markCompleted() { + completed.incrementAndGet(); + } + + private void onTimeout() { + noteStallHint(); + if (timeoutStreak.incrementAndGet() % ADAPTIVE_TIMEOUT_STEP == 0) { + adjustAdaptiveLimit(-1); + } + } + + /** + * First timeout of a job explains itself if the server is able to stop ticking under us. Without + * this a paused server just produces a wall of identical chunk timeouts. + */ + private void noteStallHint() { + if (stallHintLogged.compareAndSet(false, true)) { + pauseGuard.logStallHint(); + } + } + + private void onSuccess() { + int streak = timeoutStreak.get(); + if (streak > 0) { + timeoutStreak.compareAndSet(streak, Math.max(0, streak - 2)); + return; + } + if ((completed.get() & (ADAPTIVE_RECOVERY_INTERVAL - 1L)) == 0L) { + adjustAdaptiveLimit(1); + } + } + + private void adjustAdaptiveLimit(int direction) { + while (true) { + int current = adaptiveLimit.get(); + int next; + if (direction < 0) { + next = Math.max(minInFlight, current - 1); + } else { + int deficit = maxInFlight - current; + int step = deficit > (maxInFlight / 2) ? Math.max(2, maxInFlight / 8) : 1; + next = Math.min(maxInFlight, current + step); + } + if (next == current) { + return; + } + if (adaptiveLimit.compareAndSet(current, next)) { + synchronized (permitMonitor) { + permitMonitor.notifyAll(); + } + return; + } + } + } + + private void cleanupMantleChunk(int x, int z) { + try { + engine.getMantle().forceCleanupChunk(x, z); + } catch (Throwable e) { + LOGGER.debug("Iris pregen mantle cleanup skipped for {},{}: {}", x, z, e.toString()); + } + } + + private String describeWorkerPool() { + Executor exec = level.getServer().executor; + if (exec == null) { + return "unknown"; + } + if (exec instanceof ThreadPoolExecutor tpe) { + return "ThreadPoolExecutor(core=" + tpe.getCorePoolSize() + ",max=" + tpe.getMaximumPoolSize() + ")"; + } + if (exec instanceof ForkJoinPool fjp) { + return "ForkJoinPool(parallelism=" + fjp.getParallelism() + ")"; + } + return exec.getClass().getSimpleName(); + } + + private static Throwable unwrap(Throwable error) { + return error != null && error.getCause() != null ? error.getCause() : error; + } + + @Override + public Mantle getMantle() { + return engine.getMantle().getMantle(); + } + + /** + * A dedicated server with {@code pause-when-empty-seconds > 0} returns from + * {@code MinecraftServer#tickServer} before {@code tickChildren} once it has been empty for that + * long (26.2 only keeps {@code tickConnection} plus the task/chunk-poll window alive). That + * freezes every per-tick Iris service - world manager, scheduler, protocol sync, pregen HUD - and + * the loader's own generation hooks for the whole job, and console pregen on a default + * server.properties is always empty. The guard zeroes the setting for the duration of the job + * through the vanilla public accessors + * ({@code DedicatedServer#pauseWhenEmptySeconds}/{@code #setPauseWhenEmptySeconds}, both widened + * to public by Mojang for the management API) and restores the previous value on completion or + * abort. No reflection and no access widener, identical on all three loaders. An integrated + * (singleplayer) server never pauses on empty - {@code MinecraftServer#pauseWhenEmptySeconds} + * returns 0 there - so it is skipped silently. + * + *

A crash or a kill during the job would otherwise leave the setting at 0 for the rest of the install, + * so suspending also arms a JVM shutdown hook that restores the previous value. The hook and + * {@link #restore()} share the same atomic, so whichever runs first wins and the other is a no-op; a + * normal restore also unregisters the hook. The setting is only ever restored in memory - nothing rewrites + * server.properties, so operator edits made during the job survive. + */ + private static final class PauseWhenEmptyGuard { + private static final int NOT_SUSPENDED = -1; + + private final MinecraftServer server; + private final AtomicInteger suspendedFrom = new AtomicInteger(NOT_SUSPENDED); + private final AtomicReference crashRestoreHook = new AtomicReference<>(); + + private PauseWhenEmptyGuard(MinecraftServer server) { + this.server = server; + } + + private void suspend() { + if (!(server instanceof DedicatedServer dedicated)) { + return; + } + int current; + try { + current = dedicated.pauseWhenEmptySeconds(); + } catch (Throwable e) { + LOGGER.warn("Iris pregen could not read pause-when-empty-seconds: {}", e.toString()); + return; + } + if (current <= 0) { + return; + } + try { + dedicated.setPauseWhenEmptySeconds(0); + } catch (Throwable e) { + refuse(current, e.toString()); + return; + } + int applied; + try { + applied = dedicated.pauseWhenEmptySeconds(); + } catch (Throwable e) { + refuse(current, e.toString()); + return; + } + if (applied != 0) { + refuse(current, "still " + applied + "s after the write"); + return; + } + suspendedFrom.set(current); + armCrashRestore(); + LOGGER.info("Iris pregen: suspending pause-when-empty (was {}s), restored when the job ends", current); + } + + private void restore() { + disarmCrashRestore(); + restoreOnce("restored"); + } + + private void restoreOnce(String what) { + int previous = suspendedFrom.getAndSet(NOT_SUSPENDED); + if (previous == NOT_SUSPENDED || !(server instanceof DedicatedServer dedicated)) { + return; + } + try { + dedicated.setPauseWhenEmptySeconds(previous); + LOGGER.info("Iris pregen: {} pause-when-empty ({}s)", what, previous); + } catch (Throwable e) { + LOGGER.error("Iris pregen could not restore pause-when-empty-seconds={}: {}. Set pause-when-empty-seconds={} in server.properties.", + previous, e.toString(), previous); + } + } + + private void armCrashRestore() { + Thread hook = new Thread(() -> restoreOnce("restored on shutdown"), "iris-pregen-pause-restore"); + if (!crashRestoreHook.compareAndSet(null, hook)) { + return; + } + try { + Runtime.getRuntime().addShutdownHook(hook); + } catch (IllegalStateException shuttingDown) { + crashRestoreHook.compareAndSet(hook, null); + } + } + + private void disarmCrashRestore() { + Thread hook = crashRestoreHook.getAndSet(null); + if (hook == null) { + return; + } + try { + Runtime.getRuntime().removeShutdownHook(hook); + } catch (IllegalStateException shuttingDown) { + // Already inside shutdown; the hook itself restores the value. + } + } + + /** + * True when the server can still stop ticking under a running job. + */ + private boolean pauseStillArmed() { + if (suspendedFrom.get() != NOT_SUSPENDED || !(server instanceof DedicatedServer dedicated)) { + return false; + } + try { + return dedicated.pauseWhenEmptySeconds() > 0 && server.getPlayerCount() == 0; + } catch (Throwable e) { + return false; + } + } + + private void logStallHint() { + if (!pauseStillArmed()) { + return; + } + LOGGER.error("Iris pregen is timing out on an empty server while pause-when-empty-seconds is active: the paused server stops ticking. Set pause-when-empty-seconds=0 in server.properties, or keep a player online while pregenerating."); + } + + private void refuse(int current, String reason) { + LOGGER.error("Iris pregen could not suspend pause-when-empty-seconds={} ({}). The server stops ticking once empty, which stalls pregen: set pause-when-empty-seconds=0 in server.properties, or keep a player online while pregenerating.", + current, reason); + } + } + + private static final class FinalSaveRequest { + private final CompletableFuture completion = new CompletableFuture<>(); + private final AtomicBoolean active = new AtomicBoolean(true); + + private boolean claim() { + return active.compareAndSet(true, false); + } + + private void complete() { + completion.complete(null); + } + + private void fail(Throwable failure) { + active.set(false); + completion.completeExceptionally(failure); + } + + private void cancel() { + active.set(false); + completion.complete(null); + } + + private CompletableFuture completion() { + return completion; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java new file mode 100644 index 000000000..8028d4d59 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java @@ -0,0 +1,308 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.tools.WorldMaintenance; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedBlockBuffer; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.service.ModdedChunkUpdateService; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.ChunkSpiral; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.math.M; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.AABB; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +public final class ModdedRegen { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int APPLY_AHEAD = 8; + private static final long CHUNK_SLOT_TIMEOUT_MILLIS = 120000L; + private static final long FINAL_APPLY_TIMEOUT_MILLIS = 300000L; + private static final AtomicBoolean ACTIVE = new AtomicBoolean(false); + + private final CommandSourceStack source; + private final MinecraftServer server; + private final ServerLevel level; + private final IrisModdedChunkGenerator generator; + private final Engine engine; + private final int centerX; + private final int centerZ; + private final int radius; + + private ModdedRegen(CommandSourceStack source, ServerLevel level, IrisModdedChunkGenerator generator, Engine engine, int centerX, int centerZ, int radius) { + this.source = source; + this.server = source.getServer(); + this.level = level; + this.generator = generator; + this.engine = engine; + this.centerX = centerX; + this.centerZ = centerZ; + this.radius = Math.max(0, radius); + } + + public static void start(CommandSourceStack source, ServerLevel level, IrisModdedChunkGenerator generator, Engine engine, ServerPlayer player, int radius) { + if (!ACTIVE.compareAndSet(false, true)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_REGEN_REGEN_IS_ALREADY_RUNNING)); + return; + } + int centerX = player.blockPosition().getX() >> 4; + int centerZ = player.blockPosition().getZ() >> 4; + ModdedRegen job = new ModdedRegen(source, level, generator, engine, centerX, centerZ, radius); + int chunks = (job.radius * 2 + 1) * (job.radius * 2 + 1); + job.ok("Regen started: " + chunks + " chunk(s) around " + centerX + "," + centerZ + ". Deleting and regenerating in place."); + LOGGER.info("Iris regen start: dim={} center={},{} radius={} chunks={}", + level.dimension().identifier(), centerX, centerZ, job.radius, chunks); + Thread thread = new Thread(job::run, "Iris Regenerate"); + thread.setDaemon(true); + thread.start(); + } + + private void run() { + long startedAt = M.ms(); + String worldIdentity = engine.getWorld() == null ? null : engine.getWorld().identity(); + WorldMaintenance.beginWorldMaintenance(worldIdentity, "regen"); + try { + resetMantleMargin(); + List targets = ChunkSpiral.centerOut(centerX, centerZ, radius); + int applied = regenerate(targets); + ok("Regen finished: " + applied + "/" + targets.size() + " chunk(s) in " + Form.duration(M.ms() - startedAt, 2)); + LOGGER.info("Iris regen done: {}/{} chunks in {}ms", applied, targets.size(), M.ms() - startedAt); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Throwable e) { + LOGGER.error("Iris regen failed", e); + fail("Regen failed: " + e); + } finally { + WorldMaintenance.endWorldMaintenance(worldIdentity, "regen"); + ACTIVE.set(false); + } + } + + private void resetMantleMargin() { + EngineMantle engineMantle = engine.getMantle(); + int margin = radius + Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1; + for (int dx = -margin; dx <= margin; dx++) { + for (int dz = -margin; dz <= margin; dz++) { + engineMantle.getMantle().deleteChunk(centerX + dx, centerZ + dz); + } + } + } + + private int regenerate(List targets) throws InterruptedException { + Semaphore inFlight = new Semaphore(APPLY_AHEAD); + CountDownLatch allApplied = new CountDownLatch(targets.size()); + AtomicBoolean aborted = new AtomicBoolean(false); + AtomicInteger completed = new AtomicInteger(); + AtomicInteger applied = new AtomicInteger(); + int total = targets.size(); + int stride = total <= 64 ? 1 : 32; + int height = engine.getMaxHeight() - engine.getMinHeight(); + PlatformBlockState air = IrisPlatforms.get().registries().air(); + + for (int[] target : targets) { + int chunkX = target[0]; + int chunkZ = target[1]; + if (!inFlight.tryAcquire(CHUNK_SLOT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + aborted.set(true); + LOGGER.error("Iris regen aborted: chunk {},{} waited {}ms for an apply slot ({}/{} done)", + chunkX, chunkZ, CHUNK_SLOT_TIMEOUT_MILLIS, completed.get(), total); + fail("Regen aborted: apply pipeline stalled at " + completed.get() + "/" + total + " chunk(s)"); + break; + } + MultiBurst.burst.lazy(() -> { + long chunkStart = M.ms(); + if (aborted.get()) { + completed.incrementAndGet(); + inFlight.release(); + allApplied.countDown(); + return; + } + ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air); + Hunk biomes = Hunk.newArrayHunk(16, height, 16); + try { + engine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false); + } catch (Throwable e) { + LOGGER.error("Iris regen chunk {},{} generation failed", chunkX, chunkZ, e); + fail("Chunk " + chunkX + "," + chunkZ + " generation FAILED: " + e.getClass().getSimpleName()); + completed.incrementAndGet(); + inFlight.release(); + allApplied.countDown(); + return; + } + server.execute(() -> { + boolean success = false; + try { + if (aborted.get()) { + return; + } + apply(chunkX, chunkZ, blocks, biomes); + success = true; + applied.incrementAndGet(); + } catch (Throwable e) { + LOGGER.error("Iris regen chunk {},{} apply failed", chunkX, chunkZ, e); + fail("Chunk " + chunkX + "," + chunkZ + " apply FAILED: " + e.getClass().getSimpleName()); + } finally { + int done = completed.incrementAndGet(); + if (success && (done % stride == 0 || done == total)) { + ok("Regen [" + done + "/" + total + "] chunk " + chunkX + "," + chunkZ + " in " + (M.ms() - chunkStart) + "ms"); + } + inFlight.release(); + allApplied.countDown(); + } + }); + }); + } + + if (aborted.get()) { + // Targets were never submitted, so the latch can no longer reach zero; in-flight tasks + // observe the abort flag and release themselves. + return applied.get(); + } + if (!allApplied.await(FINAL_APPLY_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + aborted.set(true); + long outstanding = allApplied.getCount(); + LOGGER.error("Iris regen aborted: {} of {} chunk(s) did not finish within {}ms", + outstanding, total, FINAL_APPLY_TIMEOUT_MILLIS); + fail("Regen aborted: " + outstanding + " of " + total + " chunk(s) never finished"); + } + return applied.get(); + } + + private void apply(int chunkX, int chunkZ, ModdedBlockBuffer blocks, Hunk biomes) { + LevelChunk chunk = level.getChunk(chunkX, chunkZ); + ChunkPos pos = chunk.getPos(); + discardEntities(pos); + for (BlockPos blockEntityPos : new ArrayList<>(chunk.getBlockEntities().keySet())) { + chunk.removeBlockEntity(blockEntityPos); + } + + int dimMinY = engine.getMinHeight(); + int height = engine.getMaxHeight() - dimMinY; + int baseX = pos.getMinBlockX(); + int baseZ = pos.getMinBlockZ(); + BlockState airState = Blocks.AIR.defaultBlockState(); + ThreadedLevelLightEngine lightEngine = (ThreadedLevelLightEngine) level.getChunkSource().getLightEngine(); + List lightChecks = new ArrayList<>(); + + for (int i = 0; i < chunk.getSectionsCount(); i++) { + LevelChunkSection section = chunk.getSection(i); + int sectionBaseY = chunk.getSectionYFromSectionIndex(i) << 4; + section.acquire(); + try { + for (int y = 0; y < 16; y++) { + int worldY = sectionBaseY + y; + int bufferY = worldY - dimMinY; + boolean inRange = bufferY >= 0 && bufferY < height; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + BlockState target = airState; + if (inRange && !blocks.isAir(x, bufferY, z)) { + target = (BlockState) blocks.getRaw(x, bufferY, z).nativeHandle(); + } + BlockState previous = section.setBlockState(x, y, z, target, false); + if (previous != target && (previous.getLightEmission() != target.getLightEmission() + || previous.getLightDampening() != target.getLightDampening() + || previous.propagatesSkylightDown() != target.propagatesSkylightDown())) { + lightChecks.add(new BlockPos(baseX + x, worldY, baseZ + z)); + } + if (target.hasBlockEntity() && target.getBlock() instanceof EntityBlock entityBlock) { + BlockEntity entity = entityBlock.newBlockEntity(new BlockPos(baseX + x, worldY, baseZ + z), target); + if (entity != null) { + chunk.setBlockEntity(entity); + } + } + } + } + } + } finally { + section.release(); + } + lightEngine.updateSectionStatus(SectionPos.of(pos, chunk.getSectionYFromSectionIndex(i)), section.hasOnlyAir()); + } + + ModdedChunkUpdateService updateService = ModdedEngineBootstrap.services().service(ModdedChunkUpdateService.class); + if (updateService == null) { + throw new IllegalStateException("Iris chunk update service is unavailable during regeneration"); + } + updateService.updateRegeneratedChunk(engine, level, chunkX, chunkZ); + Heightmap.primeHeightmaps(chunk, ChunkStatus.FULL.heightmapsAfter()); + chunk.fillBiomesFromNoise(generator.regenBiomeResolver(), level.getChunkSource().randomState().sampler()); + chunk.markUnsaved(); + + for (BlockPos check : lightChecks) { + lightEngine.checkBlock(check); + } + + for (ServerPlayer tracking : level.getChunkSource().chunkMap.getPlayers(pos, false)) { + tracking.connection.chunkSender.dropChunk(tracking, pos); + tracking.connection.chunkSender.markChunkPendingToSend(chunk); + } + } + + private void discardEntities(ChunkPos pos) { + AABB box = new AABB(pos.getMinBlockX(), level.getMinY(), pos.getMinBlockZ(), + pos.getMaxBlockX() + 1, level.getMaxY() + 1, pos.getMaxBlockZ() + 1); + for (Entity entity : level.getEntities((Entity) null, box, (Entity e) -> !(e instanceof ServerPlayer))) { + entity.discard(); + } + } + + private void ok(String message) { + server.execute(() -> IrisModdedCommands.ok(source, message)); + } + + private void fail(String message) { + server.execute(() -> IrisModdedCommands.fail(source, message)); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStructureCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStructureCommands.java new file mode 100644 index 000000000..603d894d9 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStructureCommands.java @@ -0,0 +1,238 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.structure.StructureIndexService; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.PlacedStructurePiece; +import art.arcane.iris.engine.framework.StructureAssembler; +import art.arcane.iris.engine.framework.structure.StructureAssemblyResult; +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.ObjectPlaceMode; +import art.arcane.volmlib.util.math.RNG; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class ModdedStructureCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + + private static final SuggestionProvider IRIS_STRUCTURE_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestIrisStructureKeys(context, builder); + private static final SuggestionProvider ALL_STRUCTURE_KEYS = (CommandContext context, SuggestionsBuilder builder) -> IrisModdedCommands.suggestStructureKeys(context, builder); + + private ModdedStructureCommands() { + } + + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); + + root.then(Commands.literal("list") + .executes((CommandContext context) -> list(context.getSource()))); + root.then(Commands.literal("ls") + .executes((CommandContext context) -> list(context.getSource()))); + + root.then(Commands.literal("info") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(IRIS_STRUCTURE_KEYS) + .executes((CommandContext context) -> info(context.getSource(), StringArgumentType.getString(context, "key"))))); + + root.then(Commands.literal("place") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(IRIS_STRUCTURE_KEYS) + .executes((CommandContext context) -> place(context.getSource(), StringArgumentType.getString(context, "key"))))); + root.then(Commands.literal("p") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(IRIS_STRUCTURE_KEYS) + .executes((CommandContext context) -> place(context.getSource(), StringArgumentType.getString(context, "key"))))); + + root.then(message("import", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("import-all", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("reimport", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("imp", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("all", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("capture", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding).")); + root.then(message("cap", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding).")); + root.then(verifyTree("verify")); + root.then(verifyTree("locateall")); + + return root; + } + + private static LiteralArgumentBuilder verifyTree(String name) { + return Commands.literal(name) + .executes((CommandContext context) -> IrisModdedCommands.verifyStructures(context.getSource(), null)) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ALL_STRUCTURE_KEYS) + .executes((CommandContext context) -> IrisModdedCommands.verifyStructures( + context.getSource(), StringArgumentType.getString(context, "key")))); + } + + private static LiteralArgumentBuilder message(String name, String text) { + return Commands.literal(name) + .executes((CommandContext context) -> { + IrisModdedCommands.fail(context.getSource(), text); + return 0; + }) + .then(Commands.argument("args", StringArgumentType.greedyString()) + .executes((CommandContext context) -> { + IrisModdedCommands.fail(context.getSource(), text); + return 0; + })); + } + + private static IrisData dataFor(CommandSourceStack source) { + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_RUN_THIS_FROM)); + return null; + } + return engine.getData(); + } + + private static int list(CommandSourceStack source) { + IrisData data = dataFor(source); + if (data == null) { + return 0; + } + File file = StructureIndexService.write(data); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_WROTE_STRUCTURE_INDEX, MessageArgument.untrusted("value", file.getPath()))); + return 1; + } + + private static int info(CommandSourceStack source, String keyRaw) { + IrisData data = dataFor(source); + if (data == null) { + return 0; + } + String key = keyRaw.trim(); + IrisStructure structure = data.load(IrisStructure.class, key, false); + if (structure == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_NO_IRIS_STRUCTURE_THIS_PACK, MessageArgument.untrusted("key", key))); + return 0; + } + StructureAssembler assembler = StructureAssembler.forData( + data, structure, new IrisPosition(0, 64, 0)); + StructureAssemblyResult assembly = assembler.assemble(new RNG(1234)); + List pieces = assembly.pieces(); + if (!assembly.hasOutput()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_STRUCTURE_ASSEMBLED_0_PIECES_CHECK_STARTPOOL, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", structure.getStartPool()))); + return 0; + } + int minX = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (PlacedStructurePiece piece : pieces) { + minX = Math.min(minX, piece.getMinX()); + minZ = Math.min(minZ, piece.getMinZ()); + maxX = Math.max(maxX, piece.getMaxX()); + maxZ = Math.max(maxZ, piece.getMaxZ()); + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_STRUCTURE_PIECES_FOOTPRINT_X_BLOCKS_SAMPLE_SEED_1234, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", pieces.size()), MessageArgument.untrusted("value2", (maxX - minX + 1)), MessageArgument.untrusted("value3", (maxZ - minZ + 1)))); + return 1; + } + + private static int place(CommandSourceStack source, String keyRaw) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_STRUCTURE_IS)); + return 0; + } + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + IrisData data = dataFor(source); + if (data == null) { + return 0; + } + String key = keyRaw.trim(); + IrisStructure structure = data.load(IrisStructure.class, key, false); + if (structure == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_NO_IRIS_STRUCTURE_THIS_PACK_2, MessageArgument.untrusted("key", key))); + return 0; + } + int originX = player.blockPosition().getX(); + int originY = player.blockPosition().getY(); + int originZ = player.blockPosition().getZ(); + StructureAssembler assembler = StructureAssembler.forData( + data, structure, new IrisPosition(originX, originY, originZ)); + RNG rng = new RNG((long) originX * 341873128712L + originZ); + StructureAssemblyResult assembly = assembler.assemble(rng); + List pieces = assembly.pieces(); + if (!assembly.hasOutput()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_STRUCTURE_ASSEMBLED_0_PIECES, MessageArgument.untrusted("key", key))); + return 0; + } + ModdedObjectPlacer placer = new ModdedObjectPlacer(level, engine); + UUID owner = player.getUUID(); + try { + for (PlacedStructurePiece piece : pieces) { + IrisObjectPlacement config = new IrisObjectPlacement(); + config.setMode(ObjectPlaceMode.STRUCTURE_PIECE); + config.setRotation(piece.getRotation()); + config.getPlace().add(piece.getObject().getLoadKey()); + if (!structure.getEdit().isEmpty()) { + config.setEdit(structure.getEdit()); + } + piece.getObject().place(piece.getX(), piece.getY(), piece.getZ(), placer, config, rng, null, null, data); + } + } catch (Throwable e) { + LOGGER.error("Iris structure place failed for {}", key, e); + ModdedObjectUndo.record(owner, level, placer.undoSnapshot()); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_PLACE_FAILED_PARTIAL_CHANGES_RECORDED_UNDO, MessageArgument.untrusted("value", e.getClass().getSimpleName()))); + return 0; + } + ModdedObjectUndo.record(owner, level, placer.undoSnapshot()); + String tileNote = ModdedObjectCommands.tileNote(placer); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STRUCTURE_COMMANDS_PLACED_PIECES_WRITE_S_AT_YOUR_LOCATION_IRIS_OBJECT_UNDO, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", pieces.size()), MessageArgument.untrusted("value2", placer.writes()), MessageArgument.untrusted("tileNote", tileNote))); + return 1; + } + + private static CompletableFuture suggestIrisStructureKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + if (engine != null && engine.getData().getStructureLoader() != null) { + return SharedSuggestionProvider.suggest(engine.getData().getStructureLoader().getPossibleKeys(), builder); + } + } catch (Throwable ignored) { + } + return builder.buildFuture(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java new file mode 100644 index 000000000..9657abbb2 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java @@ -0,0 +1,852 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.gui.GuiHost; +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.pack.StructurePackageClosure; +import art.arcane.iris.core.project.IrisProjectCopier; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeGeneratorLink; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisEntitySpawn; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.core.pack.PackExportClosure; +import art.arcane.iris.engine.object.IrisEntity; +import art.arcane.iris.engine.object.IrisMarker; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.modded.ModdedDimensionManager; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedWorkspaceGenerator; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.io.IO; +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 com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.LongArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Relative; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.zeroturnaround.zip.ZipUtil; + +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class ModdedStudioCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+"); + private static final Pattern STUDIO_ID_SANITIZER = Pattern.compile("[^a-z0-9_-]"); + private static final String STUDIO_NAMESPACE = "irisworldgen"; + private static final String STUDIO_PREFIX = "studio_"; + private static final String DEFAULT_TEMPLATE = "example"; + private static final UUID CONSOLE_OWNER = new UUID(0L, 0L); + private static final Map STUDIOS = new ConcurrentHashMap<>(); + private static final SuggestionProvider GENERATOR_KEYS = (CommandContext context, SuggestionsBuilder builder) -> { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + if (engine != null) { + return SharedSuggestionProvider.suggest(engine.getData().getGeneratorLoader().getPossibleKeys(), builder); + } + } catch (Throwable e) { + IrisModdedCommands.warnTabFailure("generator keys", context.getSource(), e); + } + return builder.buildFuture(); + }; + + private ModdedStudioCommands() { + } + + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); + + root.then(createTree("create")); + root.then(createTree("+")); + + root.then(packageTree("package")); + root.then(packageTree("pkg")); + + root.then(Commands.literal("version") + .executes((CommandContext context) -> version(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> version(context.getSource(), StringArgumentType.getString(context, "pack"))))); + + root.then(Commands.literal("regions") + .executes((CommandContext context) -> regions(context.getSource(), 500)) + .then(Commands.argument("radius", IntegerArgumentType.integer(8, 1000)) + .executes((CommandContext context) -> regions(context.getSource(), IntegerArgumentType.getInteger(context, "radius"))))); + + root.then(openTree("open")); + root.then(openTree("o")); + root.then(Commands.literal("close") + .executes((CommandContext context) -> close(context.getSource()))); + root.then(Commands.literal("x") + .executes((CommandContext context) -> close(context.getSource()))); + root.then(Commands.literal("tpstudio") + .executes((CommandContext context) -> tpStudio(context.getSource()))); + root.then(Commands.literal("stp") + .executes((CommandContext context) -> tpStudio(context.getSource()))); + root.then(Commands.literal("status") + .executes((CommandContext context) -> status(context.getSource()))); + root.then(workspaceTree("vscode", true)); + root.then(workspaceTree("vsc", true)); + root.then(workspaceTree("update", false)); + root.then(message("importvanilla", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("importv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("iv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(noiseTree("noise")); + root.then(noiseTree("nmap")); + root.then(mapTree("map")); + root.then(mapTree("render")); + root.then(message("loot", "Loot simulation opens a Bukkit chest inventory GUI; it is not available on modded servers.")); + root.then(message("profile", "Pack performance profiling is part of the Bukkit studio toolchain and is not ported to modded servers.")); + root.then(message("spawn", "Iris entity spawning uses the Bukkit entity pipeline and is not ported to modded servers.")); + root.then(message("summon", "Iris entity spawning uses the Bukkit entity pipeline and is not ported to modded servers.")); + root.then(message("objects", "The chunk object report reads Bukkit chunk data and is not ported to modded servers.")); + root.then(message("find-objects", "The chunk object report reads Bukkit chunk data and is not ported to modded servers.")); + + return root; + } + + private static LiteralArgumentBuilder createTree(String name) { + return Commands.literal(name) + .executes((CommandContext context) -> + create(context.getSource(), "studio", DEFAULT_TEMPLATE)) + .then(Commands.argument("name", StringArgumentType.word()) + .executes((CommandContext context) -> + create(context.getSource(), StringArgumentType.getString(context, "name"), DEFAULT_TEMPLATE)) + .then(Commands.argument("template", StringArgumentType.word()) + .suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> + create(context.getSource(), + StringArgumentType.getString(context, "name"), + StringArgumentType.getString(context, "template"))))); + } + + private static LiteralArgumentBuilder packageTree(String name) { + return Commands.literal(name) + .executes((CommandContext context) -> pkg(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> + pkg(context.getSource(), StringArgumentType.getString(context, "pack")))); + } + + public static void clear() { + STUDIOS.clear(); + } + + private static LiteralArgumentBuilder openTree(String name) { + return Commands.literal(name) + .executes((CommandContext context) -> openHelp(context.getSource())) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> open(context.getSource(), StringArgumentType.getString(context, "pack"), 1337L)) + .then(Commands.argument("seed", LongArgumentType.longArg()) + .executes((CommandContext context) -> open(context.getSource(), StringArgumentType.getString(context, "pack"), LongArgumentType.getLong(context, "seed"))))); + } + + private static int openHelp(CommandSourceStack source) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PROVIDE_DIMENSION_PACK_IRIS_STUDIO_OPEN_PACK_SEED)); + return 0; + } + + private static LiteralArgumentBuilder noiseTree(String name) { + return Commands.literal(name) + .executes((CommandContext context) -> noise(context.getSource(), null, 12345L)) + .then(Commands.argument("generator", StringArgumentType.word()).suggests(GENERATOR_KEYS) + .executes((CommandContext context) -> noise(context.getSource(), StringArgumentType.getString(context, "generator"), 12345L)) + .then(Commands.argument("seed", LongArgumentType.longArg()) + .executes((CommandContext context) -> noise(context.getSource(), StringArgumentType.getString(context, "generator"), LongArgumentType.getLong(context, "seed"))))); + } + + private static LiteralArgumentBuilder mapTree(String name) { + return Commands.literal(name) + .executes((CommandContext context) -> map(context.getSource())); + } + + private static int noise(CommandSourceStack source, String generatorKey, long seed) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) { + IrisModdedCommands.fail(source, guiUnavailableMessage()); + return 0; + } + if (engine != null) { + ServerPlayer player = source.getPlayer(); + ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID()); + } + if (generatorKey == null || generatorKey.isBlank()) { + NoiseExplorerGUI.launch(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_ON_SERVER_DISPLAY)); + return 1; + } + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_RUN_IRIS_STUDIO)); + return 0; + } + IrisGenerator generator = engine.getData().getGeneratorLoader().load(generatorKey.trim()); + if (generator == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_UNKNOWN_GENERATOR_PACK, MessageArgument.untrusted("generatorKey", generatorKey), MessageArgument.untrusted("value", engine.getDimension().getLoadKey()))); + return 0; + } + long mixedSeed = new RNG(seed).nextParallelRNG(3245).lmax(); + Supplier> supplier = () -> (Double x, Double z) -> generator.getHeight(x, z, mixedSeed); + NoiseExplorerGUI.launch(supplier, generatorKey.trim()); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED, MessageArgument.untrusted("value", generatorKey.trim()), MessageArgument.untrusted("seed", seed))); + return 1; + } + + private static int map(CommandSourceStack source) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_STAND_IRIS_STUDIO)); + return 0; + } + if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) { + IrisModdedCommands.fail(source, guiUnavailableMessage()); + return 0; + } + ServerPlayer player = source.getPlayer(); + ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID()); + VisionGUI.launch(engine); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.dimension().identifier()))); + return 1; + } + + private static LiteralArgumentBuilder workspaceTree(String name, boolean open) { + return Commands.literal(name) + .executes((CommandContext context) -> workspace(context.getSource(), null, open)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> workspace(context.getSource(), StringArgumentType.getString(context, "pack"), open))); + } + + private static int workspace(CommandSourceStack source, String pack, boolean open) { + File folder = resolvePack(source, pack); + if (folder == null) { + return 0; + } + File workspace; + try { + workspace = ModdedWorkspaceGenerator.writeWorkspace(IrisData.get(folder), folder, open); + } catch (Throwable e) { + LOGGER.error("Iris workspace write failed for {}", folder, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_FAILED_WRITE_WORKSPACE, MessageArgument.untrusted("value", folder.getAbsolutePath()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage())))); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_WORKSPACE_REGENERATED_WITH_JSON_SCHEMAS_AUTOCOMPLETE, MessageArgument.untrusted("value", workspace.getAbsolutePath()))); + if (!open) { + return 1; + } + if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) { + IrisModdedCommands.fail(source, guiUnavailableMessage()); + return 0; + } + try { + Desktop.getDesktop().open(workspace); + } catch (Throwable e) { + LOGGER.error("Iris workspace open failed for {}", workspace, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_COULD_NOT_OPEN, MessageArgument.untrusted("value", workspace.getName()), MessageArgument.untrusted("value2", e.getClass().getSimpleName()))); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_YOUR_EDITOR, MessageArgument.untrusted("value", workspace.getName()))); + return 1; + } + + private static String guiUnavailableMessage() { + if (GuiHost.isDesktopSuppressed()) { + return "Iris desktop GUIs are disabled in singleplayer/client to avoid crashing the game client; use the in-game map and chat output instead."; + } + if (!GuiHost.isAvailable()) { + return "This server has no display (headless JVM); the Iris desktop GUIs need an AWT-capable session."; + } + return "Server-launched GUIs are disabled (gui.useServerLaunchedGuis=false in Iris settings)."; + } + + private static LiteralArgumentBuilder message(String name, String text) { + return Commands.literal(name) + .executes((CommandContext context) -> { + IrisModdedCommands.fail(context.getSource(), text); + return 0; + }) + .then(Commands.argument("args", StringArgumentType.greedyString()) + .executes((CommandContext context) -> { + IrisModdedCommands.fail(context.getSource(), text); + return 0; + })); + } + + private static File resolvePack(CommandSourceStack source, String pack) { + String name = pack; + if (name == null || name.isBlank()) { + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null || engine.getDimension() == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_SPECIFY_PACK_NAME)); + return null; + } + name = engine.getDimension().getLoadKey(); + } + File folder = new File(ModdedPackCommands.packsRoot(), name); + if (!folder.isDirectory() || !new File(folder, "dimensions").isDirectory()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_NOT_FOUND_UNDER, MessageArgument.untrusted("name", name), MessageArgument.untrusted("value", ModdedPackCommands.packsRoot().getAbsolutePath()))); + return null; + } + return folder; + } + + private static String studioDimensionId(ServerPlayer player) { + String base = STUDIO_ID_SANITIZER.matcher(player.getScoreboardName().toLowerCase(Locale.ROOT)).replaceAll("_"); + if (base.isBlank()) { + base = player.getUUID().toString().replace("-", ""); + } + return STUDIO_NAMESPACE + ":" + STUDIO_PREFIX + base; + } + + private static String studioConsoleDimensionId() { + return STUDIO_NAMESPACE + ":" + STUDIO_PREFIX + "console"; + } + + private static int open(CommandSourceStack source, String pack, long seed) { + if (pack == null || pack.isBlank()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PROVIDE_DIMENSION_PACK_IRIS_STUDIO_OPEN_PACK_SEED_2)); + return 0; + } + ServerPlayer player = source.getPlayer(); + UUID owner = player == null ? CONSOLE_OWNER : player.getUUID(); + String dimensionId = player == null ? studioConsoleDimensionId() : studioDimensionId(player); + MinecraftServer server = source.getServer(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_STUDIO_SEED, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed))); + Thread thread = new Thread(() -> openAsync(source, server, owner, dimensionId, pack, seed), "Iris Studio Open"); + thread.setDaemon(true); + thread.start(); + return 1; + } + + private static void openAsync(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) { + try { + File packFolder = new File(ModdedPackCommands.packsRoot(), pack); + if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) { + server.execute(() -> IrisModdedCommands.fail(source, "Pack '" + pack + + "' is not installed. Use /iris download pack=overworld or pack=underworld, then restart.")); + return; + } + IrisData data = IrisData.get(packFolder); + IrisDimension dimension = data.getDimensionLoader().load(pack); + if (dimension == null) { + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_HAS_NO_DIMENSIONS_JSON, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)))); + return; + } + try { + ModdedWorkspaceGenerator.writeWorkspace(data, packFolder, true); + } catch (Throwable workspaceError) { + LOGGER.error("Iris workspace write failed for {}", packFolder, workspaceError); + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.MODDED_STUDIO_COMMANDS_FAILED_WRITE_WORKSPACE, + MessageArgument.untrusted("value", packFolder.getAbsolutePath()), + MessageArgument.untrusted("value2", String.valueOf(workspaceError.getMessage()))))); + } + server.execute(() -> { + if (owner.equals(CONSOLE_OWNER)) { + injectConsole(source, server, dimensionId, pack, seed); + } else { + injectAndTeleport(source, server, owner, dimensionId, pack, seed); + } + }); + } catch (Throwable e) { + LOGGER.error("Iris studio open failed for {}", pack, e); + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))))); + } + } + + private static void injectConsole(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, long seed) { + ModdedDimensionManager.Handle handle; + try { + handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed); + } catch (Throwable e) { + LOGGER.error("Iris console studio injection failed for {} ({})", dimensionId, pack, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e)))); + return; + } + STUDIOS.put(CONSOLE_OWNER, dimensionId); + ServerLevel studio = handle.level(); + int surface = studio.getMaxY(); + try { + Engine engine = IrisModdedCommands.engineFor(studio); + if (engine != null) { + surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2; + } + } catch (Throwable e) { + LOGGER.error("Iris console studio surface probe failed for {}", dimensionId, e); + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_CONSOLE_STUDIO_OPEN_NOW_RUNS_SEED_TRANSIENT_NOT_WRITTEN_IRIS, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_ENTER_IT_WITH_EXECUTE_RUN_TP_S_8_5_8, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("surface", surface))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PREGEN_IT_WITH_IRIS_PREGEN_START_RADIUS, MessageArgument.untrusted("dimensionId", dimensionId))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_REMOVE_IT_WITH_IRIS_STUDIO_CLOSE)); + } + + private static void injectAndTeleport(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) { + ServerPlayer player = server.getPlayerList().getPlayer(owner); + if (player == null) { + return; + } + ModdedDimensionManager.Handle handle; + try { + handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed); + } catch (Throwable e) { + LOGGER.error("Iris studio injection failed for {} ({})", dimensionId, pack, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED_2, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e)))); + return; + } + STUDIOS.put(owner, dimensionId); + ServerLevel studio = handle.level(); + int surface = studio.getMaxY(); + try { + Engine engine = IrisModdedCommands.engineFor(studio); + if (engine != null) { + surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2; + } + } catch (Throwable e) { + LOGGER.error("Iris studio surface probe failed for {}", dimensionId, e); + } + player.teleportTo(studio, 8.5D, surface, 8.5D, java.util.Set.of(), player.getYRot(), player.getXRot(), false); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed))); + } + + private static int close(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + MinecraftServer server = source.getServer(); + UUID owner = player == null ? CONSOLE_OWNER : player.getUUID(); + // Commit the ownership drop only after removal succeeds: dropping it first orphaned a + // still-registered studio that no command could ever remove again. + String dimensionId = STUDIOS.get(owner); + if (dimensionId == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN)); + return 0; + } + try { + ModdedDimensionManager.remove(server, dimensionId, true); + } catch (Throwable e) { + LOGGER.error("Iris studio close failed for {}", dimensionId, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e)))); + return 0; + } + STUDIOS.remove(owner, dimensionId); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED, MessageArgument.untrusted("dimensionId", dimensionId))); + return 1; + } + + private static int status(CommandSourceStack source) { + MinecraftServer server = source.getServer(); + List handles = ModdedDimensionManager.handles(); + List studios = new ArrayList<>(); + for (ModdedDimensionManager.Handle handle : handles) { + if (handle.dimensionId().startsWith(STUDIO_NAMESPACE + ":" + STUDIO_PREFIX) && ModdedDimensionManager.level(server, handle.dimensionId()) != null) { + studios.add(handle); + } + } + if (studios.isEmpty()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_NO_STUDIO_DIMENSIONS_ARE_CURRENTLY_OPEN)); + return 1; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_ACTIVE_STUDIO_DIMENSION_S, MessageArgument.untrusted("value", studios.size()))); + for (ModdedDimensionManager.Handle handle : studios) { + UUID owner = ownerOf(handle.dimensionId()); + String ownerName = owner == null ? "unclaimed" : ownerName(server, owner); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_SEED_OWNER, MessageArgument.untrusted("value", handle.dimensionId()), MessageArgument.untrusted("value2", handle.pack()), MessageArgument.untrusted("value3", handle.seed()), MessageArgument.untrusted("ownerName", ownerName))); + } + return 1; + } + + private static UUID ownerOf(String dimensionId) { + for (Map.Entry entry : STUDIOS.entrySet()) { + if (entry.getValue().equals(dimensionId)) { + return entry.getKey(); + } + } + return null; + } + + private static String ownerName(MinecraftServer server, UUID owner) { + if (owner.equals(CONSOLE_OWNER)) { + return "console"; + } + ServerPlayer player = server.getPlayerList().getPlayer(owner); + return player == null ? owner.toString() : player.getScoreboardName(); + } + + private static int tpStudio(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS)); + return 0; + } + MinecraftServer server = source.getServer(); + String dimensionId = STUDIOS.get(player.getUUID()); + if (dimensionId == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN_2)); + return 0; + } + ServerLevel studio = ModdedDimensionManager.level(server, dimensionId); + if (studio == null) { + STUDIOS.remove(player.getUUID()); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO)); + return 0; + } + int surface = studio.getMaxY(); + try { + Engine engine = IrisModdedCommands.engineFor(studio); + if (engine != null) { + surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2; + } + } catch (Throwable e) { + LOGGER.error("Iris tpstudio surface probe failed", e); + } + player.teleportTo(studio, 8.5D, surface, 8.5D, java.util.Set.of(), player.getYRot(), player.getXRot(), false); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TELEPORTED_YOUR_STUDIO, MessageArgument.untrusted("dimensionId", dimensionId))); + return 1; + } + + private static int version(CommandSourceStack source, String pack) { + File folder = resolvePack(source, pack); + if (folder == null) { + return 0; + } + IrisData data = IrisData.get(folder); + IrisDimension dimension = data.getDimensionLoader().load(folder.getName()); + if (dimension == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_HAS_NO_DIMENSIONS_JSON_2, MessageArgument.untrusted("value", folder.getName()), MessageArgument.untrusted("value2", folder.getName()))); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_HAS_VERSION, MessageArgument.untrusted("value", dimension.getName()), MessageArgument.untrusted("value2", dimension.getVersion()))); + return 1; + } + + private static int create(CommandSourceStack source, String nameRaw, String template) { + String name = nameRaw.toLowerCase(Locale.ROOT); + if (!PROJECT_NAME.matcher(name).matches()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_INVALID_PROJECT_NAME_ALLOWED_Z_0_9, MessageArgument.untrusted("nameRaw", nameRaw))); + return 0; + } + File packsRoot = ModdedPackCommands.packsRoot(); + File target = new File(packsRoot, name); + if (target.exists()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_ALREADY_EXISTS_AT, MessageArgument.untrusted("name", name), MessageArgument.untrusted("value", target.getAbsolutePath()))); + return 0; + } + MinecraftServer server = source.getServer(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_CREATING_PROJECT_FROM_TEMPLATE, MessageArgument.untrusted("name", name), MessageArgument.untrusted("template", template))); + Thread thread = new Thread(() -> { + try { + File templateFolder = new File(packsRoot, template); + if (!new File(templateFolder, "dimensions/" + template + ".json").isFile()) { + server.execute(() -> IrisModdedCommands.fail(source, "Template pack '" + template + + "' is not installed. Install its zip with /iris download link=, then restart.")); + return; + } + IrisProjectCopier.copyProject(templateFolder, target, template, name); + try { + ModdedWorkspaceGenerator.writeWorkspace(IrisData.get(target), target); + } catch (IOException e) { + LOGGER.error("Iris studio create workspace generation failed for {}", name, e); + } + server.execute(() -> { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_CREATED_PROJECT_AT, MessageArgument.untrusted("name", name), MessageArgument.untrusted("value", target.getAbsolutePath()))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_EDIT_DIMENSIONS_JSON_REST_PACK_VSCODE_WORKSPACE_WITH_JSON_SCHEMA, MessageArgument.untrusted("name", name))); + }); + } catch (Throwable e) { + LOGGER.error("Iris studio create failed for {}", name, e); + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PROJECT_CREATION_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))))); + } + }, "Iris Studio Create"); + thread.setDaemon(true); + thread.start(); + return 1; + } + + private static int pkg(CommandSourceStack source, String pack) { + File folder = resolvePack(source, pack); + if (folder == null) { + return 0; + } + MinecraftServer server = source.getServer(); + String dimKey = folder.getName(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACKAGING_DIMENSION, MessageArgument.untrusted("dimKey", dimKey))); + Thread thread = new Thread(() -> { + try { + File result = compilePackage(folder, dimKey); + server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACKAGE_COMPILED, MessageArgument.untrusted("value", result.getAbsolutePath())))); + } catch (Throwable e) { + LOGGER.error("Iris package failed for {}", dimKey, e); + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACKAGING_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))))); + } + }, "Iris Studio Package"); + thread.setDaemon(true); + thread.start(); + return 1; + } + + private static File compilePackage(File packFolder, String dimKey) throws IOException { + IrisData dm = IrisData.get(packFolder); + IrisDimension dimension = dm.getDimensionLoader().load(dimKey); + if (dimension == null) { + throw new IOException("Pack '" + dimKey + "' has no dimensions/" + dimKey + ".json"); + } + File exports = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("exports").toFile(); + File folder = new File(exports, dimension.getLoadKey()); + IO.delete(folder); + folder.mkdirs(); + + LinkedHashSet regionKeys = new LinkedHashSet<>(); + LinkedHashSet biomeKeys = new LinkedHashSet<>(); + LinkedHashSet entityKeys = new LinkedHashSet<>(); + LinkedHashSet spawnerKeys = new LinkedHashSet<>(); + LinkedHashSet generatorKeys = new LinkedHashSet<>(); + LinkedHashSet lootKeys = new LinkedHashSet<>(); + LinkedHashSet objectKeys = new LinkedHashSet<>(); + LinkedHashSet markerKeys = new LinkedHashSet<>(); + LinkedHashSet structureKeys = new LinkedHashSet<>(); + + regionKeys.addAll(dimension.getRegions()); + lootKeys.addAll(dimension.getLoot().getTables()); + spawnerKeys.addAll(dimension.getEntitySpawners()); + collectStructureKeys(structureKeys, dimension.getStructures()); + + for (String regionKey : regionKeys) { + IrisRegion region = dm.getRegionLoader().load(regionKey); + if (region == null) { + continue; + } + region.getAllBiomes(() -> dm).forEach((IrisBiome biome) -> { + if (biome != null && biome.getLoadKey() != null) { + biomeKeys.add(biome.getLoadKey()); + } + }); + lootKeys.addAll(region.getLoot().getTables()); + spawnerKeys.addAll(region.getEntitySpawners()); + collectStructureKeys(structureKeys, region.getStructures()); + objectKeys.addAll(PackExportClosure.collectObjectKeys(region.getObjects())); + markerKeys.addAll(PackExportClosure.collectMarkerKeys(region.getObjects())); + } + for (String biomeKey : biomeKeys) { + IrisBiome biome = dm.getBiomeLoader().load(biomeKey); + if (biome == null) { + continue; + } + biome.getGenerators().forEach((IrisBiomeGeneratorLink link) -> generatorKeys.add(link.getGenerator())); + lootKeys.addAll(biome.getLoot().getTables()); + spawnerKeys.addAll(biome.getEntitySpawners()); + collectStructureKeys(structureKeys, biome.getStructures()); + objectKeys.addAll(PackExportClosure.collectObjectKeys(biome.getObjects())); + markerKeys.addAll(PackExportClosure.collectMarkerKeys(biome.getObjects())); + } + for (String markerKey : markerKeys) { + IrisMarker marker = dm.getMarkerLoader().load(markerKey); + if (marker == null) { + continue; + } + spawnerKeys.addAll(marker.getSpawners()); + } + for (String spawnerKey : spawnerKeys) { + IrisSpawner spawner = dm.getSpawnerLoader().load(spawnerKey); + if (spawner == null) { + continue; + } + spawner.getSpawns().forEach((IrisEntitySpawn spawn) -> entityKeys.add(spawn.getEntity())); + spawner.getInitialSpawns().forEach((IrisEntitySpawn spawn) -> entityKeys.add(spawn.getEntity())); + } + for (String entityKey : entityKeys) { + IrisEntity entity = dm.getEntityLoader().load(entityKey); + if (entity == null) { + continue; + } + lootKeys.addAll(entity.getLoot().getTables()); + } + + StringBuilder hashes = new StringBuilder(); + StructurePackageClosure structureClosure = StructurePackageClosure.collect(packFolder, structureKeys); + if (!structureClosure.isValid()) { + throw new IOException("Structure package closure is invalid: " + String.join("; ", structureClosure.errors())); + } + hashes.append(structureClosure.writeTo(folder, true)); + for (String objectKey : objectKeys) { + try { + File objectFile = dm.getObjectLoader().findFile(objectKey); + IO.copyFile(objectFile, new File(folder, "objects/" + objectKey + ".iob")); + hashes.append(IO.hash(objectFile)); + } catch (Throwable e) { + LOGGER.error("Iris package failed to copy object {}", objectKey, e); + } + } + + hashes.append(copyJson(folder, "dimensions", dimension.getLoadKey(), dm.getDimensionLoader().findFile(dimension.getLoadKey()))); + for (String key : generatorKeys) { + hashes.append(copyJson(folder, "generators", key, dm.getGeneratorLoader().findFile(key))); + } + for (String key : regionKeys) { + hashes.append(copyJson(folder, "regions", key, dm.getRegionLoader().findFile(key))); + } + for (String key : dm.getBlockLoader().getPossibleKeys()) { + hashes.append(copyJson(folder, "blocks", key, dm.getBlockLoader().findFile(key))); + } + for (String key : biomeKeys) { + hashes.append(copyJson(folder, "biomes", key, dm.getBiomeLoader().findFile(key))); + } + for (String key : entityKeys) { + hashes.append(copyJson(folder, "entities", key, dm.getEntityLoader().findFile(key))); + } + for (String key : lootKeys) { + hashes.append(copyJson(folder, "loot", key, dm.getLootLoader().findFile(key))); + } + for (String key : spawnerKeys) { + hashes.append(copyJson(folder, "spawners", key, dm.getSpawnerLoader().findFile(key))); + } + for (String key : markerKeys) { + hashes.append(copyJson(folder, "markers", key, dm.getMarkerLoader().findFile(key))); + } + + JSONObject meta = new JSONObject(); + meta.put("hash", IO.hash(hashes.toString())); + meta.put("time", M.ms()); + meta.put("version", dimension.getVersion()); + IO.writeAll(new File(folder, "package.json"), meta.toString(0)); + + File output = new File(exports, dimension.getLoadKey() + ".iris"); + ZipUtil.pack(folder, output, 9); + IO.delete(folder); + return output; + } + + private static void collectStructureKeys(LinkedHashSet keys, List placements) { + if (placements == null) { + return; + } + for (IrisStructurePlacement placement : placements) { + if (placement == null || placement.getStructures() == null) { + continue; + } + for (String structureKey : placement.getStructures()) { + keys.add(structureKey); + } + } + } + + private static String copyJson(File folder, String category, String key, File file) { + if (file == null || !file.isFile()) { + return ""; + } + try { + String json = new JSONObject(IO.readAll(file)).toString(0); + IO.writeAll(new File(folder, category + "/" + key + ".json"), json); + return IO.hash(json); + } catch (Throwable e) { + LOGGER.error("Iris package failed to write {}/{}", category, key, e); + return ""; + } + } + + private static int regions(CommandSourceStack source, int radius) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_SAMPLING_IS)); + return 0; + } + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS)); + return 0; + } + MinecraftServer server = source.getServer(); + int blockX = player.blockPosition().getX(); + int blockZ = player.blockPosition().getZ(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_SAMPLING_REGION_DISTRIBUTION_X_CHUNKS_AROUND_YOU, MessageArgument.untrusted("value", (radius * 2)), MessageArgument.untrusted("value2", (radius * 2)))); + Thread thread = new Thread(() -> { + try { + int diameter = radius * 2; + int totalTasks = diameter * diameter; + KMap counts = new KMap<>(); + engine.getDimension().getRegions().forEach((String key) -> counts.put(key, new AtomicInteger(0))); + // finally-scoped: a throw mid-scan previously leaked the sampler's whole + // ForkJoinPool (close() is the only thing that shuts it down). + MultiBurst burst = new MultiBurst("Region Sampler"); + try { + BurstExecutor executor = burst.burst(totalTasks); + new Spiraler(diameter, diameter, (int x, int z) -> executor.queue(() -> { + IrisRegion region = engine.getRegion((x << 4) + 8, (z << 4) + 8); + counts.computeIfAbsent(region.getLoadKey(), (String key) -> new AtomicInteger(0)).incrementAndGet(); + })).setOffset(blockX >> 4, blockZ >> 4).drain(); + executor.complete(); + } finally { + burst.close(); + } + server.execute(() -> counts.forEach((String key, AtomicInteger count) -> { + IrisRegion region = engine.getData().getRegionLoader().load(key); + String rarity = region == null ? "?" : String.valueOf(region.getRarity()); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_RARITY, MessageArgument.untrusted("key", key), MessageArgument.untrusted("rarity", rarity), MessageArgument.untrusted("value", Form.f((double) count.get() / totalTasks * 100, 2)))); + })); + } catch (Throwable e) { + LOGGER.error("Iris region sampling failed", e); + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_REGION_SAMPLING_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())))); + } + }, "Iris Region Sampler"); + thread.setDaemon(true); + thread.start(); + return 1; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedUnregisteredStructures.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedUnregisteredStructures.java new file mode 100644 index 000000000..584caff0d --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedUnregisteredStructures.java @@ -0,0 +1,204 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +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.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.levelgen.structure.Structure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Predicate; + +final class ModdedUnregisteredStructures { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private ModdedUnregisteredStructures() { + } + + static int print(CommandSourceStack source) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, + "This dimension is not generated by Iris, so it has no Iris structure eligibility report."); + return 0; + } + try { + List excluded = collect(source, level, engine); + String dimension = level.dimension().identifier().toString(); + long unregistered = excluded.stream() + .filter((ExcludedStructure entry) -> entry.status() == ReportStatus.UNREGISTERED) + .count(); + long unplaced = excluded.stream() + .filter((ExcludedStructure entry) -> entry.status() == ReportStatus.UNPLACED) + .count(); + long hidden = excluded.size() - unregistered - unplaced; + LOGGER.info("[Iris goto unregistered] {} structure candidate(s) excluded from /iris goto structure in {}", + excluded.size(), dimension); + for (ExcludedStructure entry : excluded) { + LOGGER.info("[Iris goto unregistered] [{}] {} - {}", + entry.status().label(), entry.key(), entry.reason()); + } + LOGGER.info("[Iris goto unregistered] Inventory scope is the live registry, this pack's " + + "nativeStructures placements, and structureLoader editable resources. This is deterministic " + + "eligibility analysis and performs no chunk search; absent unmanaged datapack resources " + + "cannot be inferred after registry loading."); + IrisModdedCommands.ok(source, "Iris wrote " + excluded.size() + " structure candidate(s) for " + + dimension + " to the server console (" + hidden + " excluded, " + + unregistered + " unregistered, " + unplaced + " unplaced)."); + return 1; + } catch (Throwable error) { + LOGGER.error("Iris failed to build the excluded structure report for {}", + level.dimension().identifier(), error); + IrisModdedCommands.fail(source, + "Iris could not build the excluded structure report; see the server console."); + return 0; + } + } + + static List collect(CommandSourceStack source, ServerLevel level, Engine engine) { + boolean nativeGenerationEnabled = + source.getServer().getWorldGenSettings().options().generateStructures(); + Set reachableNativeKeys = StructureReachability.reachableKeys(engine); + Registry registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); + Map excluded = new TreeMap<>(); + Set registeredKeys = new HashSet<>(registry.keySet().size()); + Set eligibleRegisteredKeys = new HashSet<>(registry.keySet().size()); + for (Identifier identifier : registry.keySet()) { + String key = identifier.toString(); + String normalizedKey = ModdedCommandSuggestions.normalizeKey(key); + registeredKeys.add(normalizedKey); + Optional> holder = registry.get(identifier); + if (holder.isEmpty()) { + excluded.put(normalizedKey, new ExcludedStructure(ReportStatus.EXCLUDED, + key, "The live structure registry contains this key but has no bound structure holder.")); + continue; + } + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false); + boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key); + boolean locatableNativePlacement = nativePlacement + && IrisStructureLocator.hasLocatableNativePlacement(engine, key); + boolean locatableEditableReplacement = !nativePlacement + && decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS + && IrisStructureLocator.hasLocatableEditablePlacement(engine, key); + boolean reachable = reachableNativeKeys.contains(normalizedKey); + if (ModdedCommandSuggestions.isEligibleRegisteredStructure( + decision, nativePlacement, locatableNativePlacement, + locatableEditableReplacement, reachable, nativeGenerationEnabled)) { + eligibleRegisteredKeys.add(normalizedKey); + continue; + } + ModdedLocateCommands.NativeStructureAvailability availability = + ModdedLocateCommands.nativeAvailability(source, level, engine, key, holder.get()); + String reason = ModdedLocateCommands.registeredStructureUnavailableMessage( + key, availability, decision, nativePlacement, locatableNativePlacement, + nativeGenerationEnabled, reachable); + excluded.put(normalizedKey, new ExcludedStructure(ReportStatus.EXCLUDED, key, reason)); + } + List absentNativeKeys = missingConfiguredNativeKeys( + IrisStructureLocator.placedKeys(engine), registeredKeys, + (String key) -> IrisStructureLocator.hasNativePlacement(engine, key)); + for (String key : absentNativeKeys) { + String normalizedKey = ModdedCommandSuggestions.normalizeKey(key); + excluded.putIfAbsent(normalizedKey, new ExcludedStructure(ReportStatus.UNREGISTERED, key, + "Configured by an Iris nativeStructures placement, but absent from the live structure registry; " + + "install or enable the datapack or mod that provides this key.")); + } + if (engine.getData().getStructureLoader() != null) { + for (String key : engine.getData().getStructureLoader().getPossibleKeys()) { + String normalizedKey = ModdedCommandSuggestions.normalizeKey(key); + if (normalizedKey.isEmpty() || eligibleRegisteredKeys.contains(normalizedKey) + || excluded.containsKey(normalizedKey)) { + continue; + } + boolean registered = registeredKeys.contains(normalizedKey); + boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key); + boolean locatableEditable = IrisStructureLocator.hasLocatableEditablePlacement(engine, key); + if (!registered && !nativePlacement && locatableEditable) { + continue; + } + boolean configured = IrisStructureLocator.hasEditablePlacement(engine, key); + excluded.putIfAbsent(normalizedKey, new ExcludedStructure( + ReportStatus.UNPLACED, key.trim(), editableExclusionReason(configured))); + } + } + return List.copyOf(excluded.values()); + } + + static List missingConfiguredNativeKeys( + Collection placedKeys, Set normalizedRegisteredKeys, + Predicate nativePlacement) { + Map missing = new TreeMap<>(); + for (String key : placedKeys) { + String normalizedKey = ModdedCommandSuggestions.normalizeKey(key); + if (!normalizedKey.isEmpty() + && !normalizedRegisteredKeys.contains(normalizedKey) + && nativePlacement.test(key)) { + missing.putIfAbsent(normalizedKey, key.trim()); + } + } + return List.copyOf(missing.values()); + } + + static String editableExclusionReason(boolean configured) { + if (!configured) { + return "Editable Iris structure resource is loaded, but no biome, region, or dimension structure " + + "placement references it."; + } + return "Editable Iris structure resource is referenced by a placement, but every matching placement " + + "has non-positive density or a Y band outside this world's height range."; + } + + enum ReportStatus { + EXCLUDED("excluded"), + UNREGISTERED("unregistered"), + UNPLACED("unplaced"); + + private final String label; + + ReportStatus(String label) { + this.label = label; + } + + String label() { + return label; + } + } + + record ExcludedStructure(ReportStatus status, String key, String reason) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedVisionOverlay.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedVisionOverlay.java new file mode 100644 index 000000000..1b015b8b1 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedVisionOverlay.java @@ -0,0 +1,117 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.gui.GuiHost; +import art.arcane.iris.core.gui.GuiMarker; +import art.arcane.iris.core.gui.GuiOverlay; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.render.RenderType; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; + +import java.awt.Desktop; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +public final class ModdedVisionOverlay implements GuiOverlay { + private final MinecraftServer server; + private final ServerLevel level; + private final Engine engine; + private final UUID opener; + + public ModdedVisionOverlay(MinecraftServer server, ServerLevel level, Engine engine, UUID opener) { + this.server = server; + this.level = level; + this.engine = engine; + this.opener = opener; + } + + @Override + public List players() { + List markers = new ArrayList<>(); + for (ServerPlayer player : level.players()) { + Vec3 position = player.position(); + markers.add(GuiMarker.player(player.getScoreboardName(), position.x(), position.z())); + } + return markers; + } + + @Override + public void requestEntities(Consumer> sink) { + server.execute(() -> { + List markers = new ArrayList<>(); + for (Entity entity : level.getAllEntities()) { + if (entity instanceof ServerPlayer || !(entity instanceof LivingEntity living)) { + continue; + } + Vec3 position = living.position(); + markers.add(GuiMarker.entity(living.getType().toShortString(), position.x(), position.y(), position.z(), + living.getHealth(), living.getMaxHealth())); + } + sink.accept(markers); + }); + } + + @Override + public void teleport(double worldX, double worldZ) { + int blockX = (int) worldX; + int blockZ = (int) worldZ; + server.execute(() -> { + ServerPlayer player = opener == null ? null : server.getPlayerList().getPlayer(opener); + if (player == null) { + List players = level.players(); + if (players.isEmpty()) { + return; + } + player = players.get(0); + } + int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2; + int safeY = Math.max(surfaceY, level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ) + 1); + player.teleportTo(level, blockX + 0.5D, safeY, blockZ + 0.5D, java.util.Set.of(), player.getYRot(), player.getXRot(), false); + }); + } + + @Override + public String openInEditor(double worldX, double worldZ, RenderType type) { + if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) { + return null; + } + IrisComplex complex = engine.getComplex(); + File file = switch (type) { + case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT -> + complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode(); + case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode(); + case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode(); + case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode(); + case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode(); + default -> null; + }; + return file == null ? null : file.getName(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWandService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWandService.java new file mode 100644 index 000000000..a430491f1 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWandService.java @@ -0,0 +1,284 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Unit; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.component.ItemLore; +import net.minecraft.world.item.component.TooltipDisplay; +import net.minecraft.world.level.Level; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; + +public final class ModdedWandService { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final ConcurrentHashMap SELECTIONS = new ConcurrentHashMap<>(); + private static final String WAND_TAG = "iris_wand"; + private static final String DUST_TAG = "iris_dust"; + private static final int DRAW_INTERVAL_TICKS = 5; + private static final double DRAW_STEP = 0.5D; + private static final double DRAW_KEEP_CHANCE = 0.4D; + private static final int DRAW_POINT_CAP = 400; + private static final double DRAW_DISTANCE_SQUARED = 96.0D * 96.0D; + private static int tickCounter = 0; + + private ModdedWandService() { + } + + public record Selection(ResourceKey dimension, BlockPos first, BlockPos second) { + public boolean complete() { + return first != null && second != null; + } + + public BlockPos min() { + return new BlockPos(Math.min(first.getX(), second.getX()), Math.min(first.getY(), second.getY()), Math.min(first.getZ(), second.getZ())); + } + + public BlockPos max() { + return new BlockPos(Math.max(first.getX(), second.getX()), Math.max(first.getY(), second.getY()), Math.max(first.getZ(), second.getZ())); + } + } + + public static ItemStack createWand() { + ItemStack stack = new ItemStack(Items.BLAZE_ROD); + stack.set(DataComponents.CUSTOM_NAME, Component.literal(IrisLanguage.plain(RuntimeUiMessages.WAND_NAME)).withStyle(ChatFormatting.BOLD, ChatFormatting.GOLD)); + stack.set(DataComponents.LORE, new ItemLore(List.of( + Component.literal(IrisLanguage.plain(RuntimeUiMessages.WAND_LORE_FIRST)), + Component.literal(IrisLanguage.plain(RuntimeUiMessages.WAND_LORE_SECOND))))); + stack.set(DataComponents.UNBREAKABLE, Unit.INSTANCE); + stack.set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, Boolean.TRUE); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(flagTag(WAND_TAG))); + stack.set(DataComponents.TOOLTIP_DISPLAY, + TooltipDisplay.DEFAULT.withHidden(DataComponents.UNBREAKABLE, true)); + return stack; + } + + public static ItemStack createDust() { + ItemStack stack = new ItemStack(Items.GLOWSTONE_DUST); + stack.set(DataComponents.CUSTOM_NAME, Component.literal(IrisLanguage.plain(RuntimeUiMessages.DUST_NAME)).withStyle(ChatFormatting.BOLD, ChatFormatting.YELLOW)); + stack.set(DataComponents.LORE, new ItemLore(List.of( + Component.literal(IrisLanguage.plain(RuntimeUiMessages.DUST_LORE))))); + stack.set(DataComponents.UNBREAKABLE, Unit.INSTANCE); + stack.set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, Boolean.TRUE); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(flagTag(DUST_TAG))); + stack.set(DataComponents.TOOLTIP_DISPLAY, + TooltipDisplay.DEFAULT.withHidden(DataComponents.UNBREAKABLE, true)); + return stack; + } + + private static CompoundTag flagTag(String key) { + CompoundTag tag = new CompoundTag(); + tag.putBoolean(key, true); + return tag; + } + + public static boolean isWand(ItemStack stack) { + return hasFlag(stack, WAND_TAG); + } + + public static boolean isDust(ItemStack stack) { + return hasFlag(stack, DUST_TAG); + } + + private static boolean hasFlag(ItemStack stack, String key) { + if (stack == null || stack.isEmpty()) { + return false; + } + CustomData data = stack.get(DataComponents.CUSTOM_DATA); + return data != null && data.copyTag().getBooleanOr(key, false); + } + + public static boolean isHoldingWand(ServerPlayer player) { + return isWand(player.getMainHandItem()); + } + + public static boolean attackBlock(Player player, Level level, InteractionHand hand, BlockPos pos) { + if (hand != InteractionHand.MAIN_HAND || level.isClientSide() || !(player instanceof ServerPlayer serverPlayer) || !(level instanceof ServerLevel serverLevel)) { + return false; + } + if (isWand(serverPlayer.getMainHandItem())) { + setCorner(serverPlayer, serverLevel, pos, true); + return true; + } + return false; + } + + public static boolean useBlock(Player player, Level level, InteractionHand hand, BlockPos pos) { + if (hand != InteractionHand.MAIN_HAND || level.isClientSide() || !(player instanceof ServerPlayer serverPlayer) || !(level instanceof ServerLevel serverLevel)) { + return false; + } + ItemStack held = serverPlayer.getMainHandItem(); + if (isWand(held)) { + setCorner(serverPlayer, serverLevel, pos, false); + return true; + } + if (isDust(held)) { + serverLevel.playSound(null, pos, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 2.0F, 1.97F); + ModdedDustRevealer.reveal(serverPlayer, serverLevel, pos); + return true; + } + return false; + } + + private static void setCorner(ServerPlayer player, ServerLevel level, BlockPos pos, boolean first) { + ResourceKey dimension = level.dimension(); + BlockPos corner = pos.immutable(); + SELECTIONS.compute(player.getUUID(), (UUID uuid, Selection existing) -> { + BlockPos other = existing != null && existing.dimension().equals(dimension) ? (first ? existing.second() : existing.first()) : null; + return first ? new Selection(dimension, corner, other) : new Selection(dimension, other, corner); + }); + level.playSound(null, pos, SoundEvents.END_PORTAL_FRAME_FILL, SoundSource.PLAYERS, 1.0F, first ? 0.67F : 1.17F); + player.sendOverlayMessage(Component.literal(IrisLanguage.plain( + RuntimeUiMessages.WAND_POSITION_SET, + MessageArgument.trusted("position", first ? 1 : 2), + MessageArgument.trusted("x", corner.getX()), + MessageArgument.trusted("y", corner.getY()), + MessageArgument.trusted("z", corner.getZ()) + ))); + } + + public static Selection selection(ServerPlayer player) { + Selection selection = SELECTIONS.get(player.getUUID()); + if (selection == null || !selection.complete() || !selection.dimension().equals(player.level().dimension())) { + return null; + } + return selection; + } + + public static void setSelection(ServerPlayer player, BlockPos first, BlockPos second) { + SELECTIONS.put(player.getUUID(), new Selection(player.level().dimension(), first.immutable(), second.immutable())); + } + + public static void clearAll() { + SELECTIONS.clear(); + ModdedDustRevealer.clear(); + ModdedWhatCommands.clear(); + } + + public static void serverTick(MinecraftServer server) { + tickCounter++; + if (tickCounter % DRAW_INTERVAL_TICKS != 0) { + return; + } + try { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (!isHoldingWand(player)) { + continue; + } + Selection selection = selection(player); + if (selection == null) { + continue; + } + draw(player.level(), player, selection); + } + } catch (Throwable e) { + LOGGER.error("Iris wand selection draw failed", e); + } + } + + private static void draw(ServerLevel level, ServerPlayer player, Selection selection) { + BlockPos min = selection.min(); + BlockPos max = selection.max(); + double lowX = min.getX(); + double lowY = min.getY(); + double lowZ = min.getZ(); + double highX = max.getX() + 1; + double highY = max.getY() + 1; + double highZ = max.getZ() + 1; + double[][] edges = { + {lowX, lowY, lowZ, highX, lowY, lowZ}, + {lowX, lowY, lowZ, lowX, highY, lowZ}, + {lowX, lowY, lowZ, lowX, lowY, highZ}, + {highX, lowY, lowZ, highX, highY, lowZ}, + {highX, lowY, lowZ, highX, lowY, highZ}, + {lowX, highY, lowZ, highX, highY, lowZ}, + {lowX, highY, lowZ, lowX, highY, highZ}, + {lowX, lowY, highZ, highX, lowY, highZ}, + {lowX, lowY, highZ, lowX, highY, highZ}, + {highX, highY, lowZ, highX, highY, highZ}, + {lowX, highY, highZ, highX, highY, highZ}, + {highX, lowY, highZ, highX, highY, highZ} + }; + + ThreadLocalRandom random = ThreadLocalRandom.current(); + double px = player.getX(); + double py = player.getY(); + double pz = player.getZ(); + int sent = 0; + for (double[] edge : edges) { + double dx = edge[3] - edge[0]; + double dy = edge[4] - edge[1]; + double dz = edge[5] - edge[2]; + double length = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (length <= 0) { + continue; + } + double ux = dx / length; + double uy = dy / length; + double uz = dz / length; + for (double d = 0; d <= length; d += DRAW_STEP) { + if (random.nextDouble() > DRAW_KEEP_CHANCE) { + continue; + } + double x = edge[0] + ux * d; + double y = edge[1] + uy * d; + double z = edge[2] + uz * d; + double distX = x - px; + double distY = y - py; + double distZ = z - pz; + if (distX * distX + distY * distY + distZ * distZ > DRAW_DISTANCE_SQUARED) { + continue; + } + float hue = (float) (0.5F + (Math.sin((x + y + z + (player.tickCount / 2.0F)) / 20.0F) / 2.0D)); + Color color = Color.getHSBColor(hue, 1.0F, 1.0F); + DustParticleOptions options = new DustParticleOptions(color.getRGB() & 0xFFFFFF, 0.9F); + level.sendParticles(player, options, true, true, x, y, z, 1, 0.0D, 0.0D, 0.0D, 0.0D); + sent++; + if (sent >= DRAW_POINT_CAP) { + return; + } + } + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWhatCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWhatCommands.java new file mode 100644 index 000000000..b480fb44a --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWhatCommands.java @@ -0,0 +1,538 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.IrisMessages; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.core.localization.RuntimeUiMessages; +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.IrisPosition; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.modded.ModdedBlockState; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedScheduler; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.localization.TextKey; +import art.arcane.volmlib.util.matter.MatterMarker; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.SpawnerBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.storage.loot.LootTable; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; + +public final class ModdedWhatCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Predicate GATE = + Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + private static final SuggestionProvider MARKER_TYPES = + (CommandContext context, SuggestionsBuilder builder) -> + SharedSuggestionProvider.suggest( + List.of("cave_floor", "cave_ceiling", "object"), builder); + private static final DustParticleOptions MARKER_DUST = new DustParticleOptions(0x5A8CFF, 1.2F); + private static final int MAX_MARKERS = 8_192; + private static final int MARKER_BATCH_SIZE = 128; + private static final ConcurrentHashMap ACTIVE_MARKER_RUNS = new ConcurrentHashMap<>(); + + private ModdedWhatCommands() { + } + + public static LiteralArgumentBuilder tree() { + return Commands.literal("what").requires(GATE) + .executes((CommandContext context) -> + inspectHere(context.getSource())) + .then(Commands.literal("here") + .executes((CommandContext context) -> + inspectHere(context.getSource()))) + .then(Commands.literal("biome") + .executes((CommandContext context) -> + inspectBiome(context.getSource()))) + .then(Commands.literal("region") + .executes((CommandContext context) -> + inspectRegion(context.getSource()))) + .then(Commands.literal("block") + .executes((CommandContext context) -> + inspectBlock(context.getSource()))) + .then(Commands.literal("hand") + .executes((CommandContext context) -> + inspectHand(context.getSource()))) + .then(Commands.literal("markers") + .then(Commands.argument("marker", StringArgumentType.greedyString()) + .suggests(MARKER_TYPES) + .executes((CommandContext context) -> + inspectMarkers( + context.getSource(), + StringArgumentType.getString(context, "marker"))))); + } + + static void clear() { + for (MarkerRun run : ACTIVE_MARKER_RUNS.values()) { + run.cancelled().set(true); + } + ACTIVE_MARKER_RUNS.clear(); + } + + private static int inspectHere(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + return playerRequired(source, ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_2); + } + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_6)); + return 0; + } + BlockPos pos = player.blockPosition(); + int result = inspectBiome(source); + result &= inspectRegion(source); + int relativeY = pos.getY() - engine.getMinHeight(); + try { + IrisBiome cave = engine.getCaveOrMantleBiome(pos.getX(), relativeY, pos.getZ()); + IrisModdedCommands.ok(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_CAVE_BIOME, + MessageArgument.untrusted("value", cave == null + ? IrisLanguage.plain(RuntimeUiMessages.STATUS_NONE) + : cave.getLoadKey()))); + } catch (Throwable error) { + logLookupFailure(source, "cave biome", error, + ModdedCommandMessages.IRIS_MODDED_COMMANDS_CAVE_BIOME_LOOKUP_FAILED); + result = 0; + } + int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1; + BlockState surface = level.getBlockState(new BlockPos(pos.getX(), surfaceY, pos.getZ())); + IrisModdedCommands.ok(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_SURFACE_BLOCK_Y, + MessageArgument.untrusted("value", BuiltInRegistries.BLOCK.getKey(surface.getBlock())), + MessageArgument.trusted("value2", surfaceY))); + sendPosition(source, pos); + return result; + } + + private static int inspectBiome(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + return playerRequired(source, ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_2); + } + ServerLevel level = source.getLevel(); + BlockPos pos = player.blockPosition(); + NativeBiome nativeBiome = nativeBiome(level, pos); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_NON_IRIS_BIOME, + MessageArgument.untrusted("biome", nativeBiome.key()), + MessageArgument.trusted("id", nativeBiome.id()))); + return 1; + } + try { + IrisBiome biome = engine.getBiome( + pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ()); + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_IRIS_BIOME, + MessageArgument.untrusted("biome", biome.getLoadKey()), + MessageArgument.untrusted("name", biome.getName()))); + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_DERIVATIVE_BIOME, + MessageArgument.untrusted("biome", biome.getDerivativeKey()))); + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_NATIVE_BIOME, + MessageArgument.untrusted("biome", nativeBiome.key()), + MessageArgument.trusted("id", nativeBiome.id()))); + return 1; + } catch (Throwable error) { + logLookupFailure(source, "biome", error, + ModdedCommandMessages.IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED_2); + return 0; + } + } + + private static int inspectRegion(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + return playerRequired(source, ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_2); + } + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_6)); + return 0; + } + BlockPos pos = player.blockPosition(); + int centerX = (pos.getX() & ~15) + 8; + int centerZ = (pos.getZ() & ~15) + 8; + try { + IrisRegion region = engine.getRegion(centerX, centerZ); + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_IRIS_REGION, + MessageArgument.untrusted("region", region.getLoadKey()), + MessageArgument.untrusted("name", region.getName()))); + return 1; + } catch (Throwable error) { + logLookupFailure(source, "region", error, + ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED_2); + return 0; + } + } + + private static int inspectHand(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + return playerRequired(source, ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_IT_INSPECTS); + } + ItemStack stack = player.getMainHandItem(); + if (stack.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOUR_MAIN_HAND_IS_EMPTY)); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_MATERIAL, + MessageArgument.untrusted("material", BuiltInRegistries.ITEM.getKey(stack.getItem())))); + if (stack.getItem() instanceof BlockItem blockItem) { + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_FULL_STATE, + MessageArgument.untrusted( + "state", ModdedBlockState.serialize(blockItem.getBlock().defaultBlockState())))); + } + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_ITEM_COUNT, + MessageArgument.trusted("count", stack.getCount()))); + return 1; + } + + private static int inspectBlock(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + return playerRequired(source, ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_IT_INSPECTS_2); + } + HitResult hit = player.pick(128.0D, 1.0F, false); + if (!(hit instanceof BlockHitResult blockHit) || hit.getType() != HitResult.Type.BLOCK) { + IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_LOOK_AT_BLOCK_NOT_SKY)); + return 0; + } + ServerLevel level = source.getLevel(); + BlockPos pos = blockHit.getBlockPos(); + BlockState state = level.getBlockState(pos); + PlatformBlockState platform = ModdedBlockState.of(state, null); + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_MATERIAL, + MessageArgument.untrusted("material", BuiltInRegistries.BLOCK.getKey(state.getBlock())))); + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_FULL_STATE, + MessageArgument.untrusted("state", ModdedBlockState.serialize(state)))); + sendPosition(source, pos); + sendProperties(source, platform); + sendObject(source, level, pos); + sendBlockEntity(source, level, pos); + return 1; + } + + private static void sendPosition(CommandSourceStack source, BlockPos pos) { + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_POSITION, + MessageArgument.trusted("x", pos.getX()), + MessageArgument.trusted("y", pos.getY()), + MessageArgument.trusted("z", pos.getZ()), + MessageArgument.trusted("chunkX", pos.getX() >> 4), + MessageArgument.trusted("chunkZ", pos.getZ() >> 4))); + } + + private static void sendProperties(CommandSourceStack source, PlatformBlockState platform) { + List flags = propertyNames(platform); + if (flags.isEmpty()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(IrisMessages.MODDED_PROPERTIES_NONE)); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain( + IrisMessages.MODDED_PROPERTIES, + MessageArgument.untrusted("properties", String.join(", ", flags)))); + } + + static List propertyNames(PlatformBlockState platform) { + List flags = new ArrayList<>(); + addFlag(flags, platform.isSolid(), RuntimeUiMessages.WHAT_FLAG_SOLID); + addFlag(flags, platform.isFluid(), RuntimeUiMessages.WHAT_FLAG_FLUID); + addFlag(flags, platform.isWater(), RuntimeUiMessages.WHAT_FLAG_WATER); + addFlag(flags, platform.isWaterLogged(), RuntimeUiMessages.WHAT_FLAG_WATERLOGGED); + addFlag(flags, platform.isStorage(), RuntimeUiMessages.WHAT_FLAG_STORAGE); + addFlag(flags, platform.isLit(), RuntimeUiMessages.WHAT_FLAG_LIT); + addFlag(flags, platform.isFoliage(), RuntimeUiMessages.WHAT_FLAG_FOLIAGE); + addFlag(flags, platform.isFoliagePlantable(), RuntimeUiMessages.WHAT_FLAG_PLANTABLE_FOLIAGE); + addFlag(flags, platform.isDecorant(), RuntimeUiMessages.WHAT_FLAG_DECORANT); + addFlag(flags, platform.isOre(), RuntimeUiMessages.WHAT_FLAG_ORE); + addFlag(flags, platform.hasTileEntity(), RuntimeUiMessages.WHAT_FLAG_BLOCK_ENTITY); + return List.copyOf(flags); + } + + private static void addFlag(List flags, boolean enabled, + TextKey message) { + if (enabled) { + flags.add(IrisLanguage.plain(message)); + } + } + + private static void sendObject(CommandSourceStack source, ServerLevel level, BlockPos pos) { + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + return; + } + try { + String object = engine.getObjectPlacementKey( + pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ()); + if (object != null) { + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_OBJECT, + MessageArgument.untrusted("object", object))); + } + } catch (Throwable error) { + LOGGER.error("Iris object lookup failed for /iris what block at {}, {}, {}", + pos.getX(), pos.getY(), pos.getZ(), error); + } + } + + private static void sendBlockEntity(CommandSourceStack source, + ServerLevel level, BlockPos pos) { + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity == null) { + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_BLOCK_ENTITY, + MessageArgument.untrusted( + "type", BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(blockEntity.getType())))); + if (blockEntity instanceof RandomizableContainer container) { + ResourceKey lootTable = container.getLootTable(); + if (lootTable != null) { + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_LOOT_TABLE, + MessageArgument.untrusted("loot", lootTable.identifier()))); + } + } + if (blockEntity instanceof SpawnerBlockEntity) { + CompoundTag tag = blockEntity.saveWithoutMetadata(level.registryAccess()); + CompoundTag spawnData = tag.getCompound("SpawnData").orElse(null); + CompoundTag entity = spawnData == null ? null : spawnData.getCompound("entity").orElse(null); + String entityId = entity == null ? "" : entity.getStringOr("id", ""); + if (!entityId.isBlank()) { + IrisModdedCommands.ok(source, IrisLanguage.plain( + RuntimeUiMessages.WHAT_SPAWNER_ENTITY, + MessageArgument.untrusted("entity", entityId))); + } + } + } + + private static int inspectMarkers(CommandSourceStack source, String markerRaw) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + return playerRequired(source, ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_MARKERS_RENDER); + } + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_7)); + return 0; + } + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_MARKER_SCAN_FAILED, + MessageArgument.untrusted("value", "scheduler unavailable"))); + return 0; + } + String marker = markerRaw.trim(); + BlockPos origin = player.blockPosition(); + MarkerRun run = new MarkerRun( + player.getUUID(), player, level, engine, marker, + origin.getX() >> 4, origin.getZ() >> 4, new AtomicBoolean()); + MarkerRun previous = ACTIVE_MARKER_RUNS.put(player.getUUID(), run); + if (previous != null) { + previous.cancelled().set(true); + } + IrisModdedCommands.ok(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_SCANNING_MARKERS_AROUND_YOU, + MessageArgument.untrusted("marker", marker))); + scheduler.async(() -> scanMarkers(source, scheduler, run)); + return 1; + } + + private static void scanMarkers(CommandSourceStack source, + ModdedScheduler scheduler, MarkerRun run) { + List hits = new ArrayList<>(); + MatterMarker marker = new MatterMarker(run.marker()); + try (GenerationSessionLease lease = run.engine().acquireGenerationLease("modded_what_markers"); + IrisContext.Scope ignored = IrisContext.open(run.engine(), lease.sessionId(), null)) { + for (int chunkX = run.chunkX() - 4; chunkX <= run.chunkX() + 4; chunkX++) { + for (int chunkZ = run.chunkZ() - 4; chunkZ <= run.chunkZ() + 4; chunkZ++) { + if (run.cancelled().get()) { + return; + } + for (IrisPosition position : run.engine().getMantle().findMarkers(chunkX, chunkZ, marker)) { + hits.add(new BlockPos(position.getX(), position.getY(), position.getZ())); + if (hits.size() >= MAX_MARKERS) { + break; + } + } + if (hits.size() >= MAX_MARKERS) { + break; + } + } + if (hits.size() >= MAX_MARKERS) { + break; + } + } + scheduler.global(() -> renderMarkerBatch(source, scheduler, run, hits, 0)); + } catch (GenerationSessionException error) { + markerFailure(source, scheduler, run, error); + } catch (Throwable error) { + markerFailure(source, scheduler, run, error); + } + } + + private static void renderMarkerBatch(CommandSourceStack source, + ModdedScheduler scheduler, MarkerRun run, + List hits, int from) { + if (!active(run)) { + // Drop the registry entry on abort too, or the run record pins the player, level + // and engine until server stop. No-op if a newer run already replaced it. + ACTIVE_MARKER_RUNS.remove(run.playerId(), run); + return; + } + int to = Math.min(hits.size(), from + MARKER_BATCH_SIZE); + for (int index = from; index < to; index++) { + BlockPos hit = hits.get(index); + run.level().sendParticles(run.player(), MARKER_DUST, true, true, + hit.getX() + 0.5D, hit.getY() + 1.0D, hit.getZ() + 0.5D, + 3, 0.2D, 0.2D, 0.2D, 0.0D); + } + if (to < hits.size()) { + scheduler.laterGlobal(() -> renderMarkerBatch(source, scheduler, run, hits, to), 1); + return; + } + ACTIVE_MARKER_RUNS.remove(run.playerId(), run); + IrisModdedCommands.ok(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_NEARBY_MARKER_S, + MessageArgument.trusted("value", hits.size()), + MessageArgument.untrusted("marker", run.marker()))); + } + + private static boolean active(MarkerRun run) { + return !run.cancelled().get() + && ACTIVE_MARKER_RUNS.get(run.playerId()) == run + && !run.player().hasDisconnected() + && !run.player().isRemoved() + && run.player().level() == run.level() + && !run.engine().isClosing() + && !run.engine().isClosed(); + } + + private static void markerFailure(CommandSourceStack source, + ModdedScheduler scheduler, MarkerRun run, Throwable error) { + LOGGER.error("Iris marker scan failed for {}", run.marker(), error); + scheduler.global(() -> { + if (ACTIVE_MARKER_RUNS.remove(run.playerId(), run)) { + IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_MARKER_SCAN_FAILED, + MessageArgument.untrusted("value", error.getClass().getSimpleName()))); + } + }); + } + + private static NativeBiome nativeBiome(ServerLevel level, BlockPos pos) { + Holder holder = level.getBiome(pos); + String key = holder.unwrapKey() + .map((ResourceKey resourceKey) -> resourceKey.identifier().toString()) + .orElse(IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED)); + Registry registry = level.registryAccess().lookupOrThrow(Registries.BIOME); + return new NativeBiome(key, registry.getId(holder.value())); + } + + private static int playerRequired(CommandSourceStack source, + TextKey message) { + IrisModdedCommands.fail(source, IrisLanguage.plain(message)); + return 0; + } + + private static void logLookupFailure(CommandSourceStack source, + String operation, Throwable error, + TextKey message) { + LOGGER.error("Iris /what {} lookup failed in {}", operation, + source.getLevel().dimension().identifier(), error); + IrisModdedCommands.fail(source, IrisLanguage.plain( + message, + MessageArgument.untrusted("value", error.getClass().getSimpleName()))); + } + + private record NativeBiome(String key, int id) { + } + + private record MarkerRun( + UUID playerId, + ServerPlayer player, + ServerLevel level, + Engine engine, + String marker, + int chunkX, + int chunkZ, + AtomicBoolean cancelled + ) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java new file mode 100644 index 000000000..2150f9942 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java @@ -0,0 +1,479 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.MainWorldService; +import art.arcane.iris.modded.ModdedDimensionManager; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedModConfig; +import art.arcane.iris.modded.ModdedPrimaryWorldRouter; +import art.arcane.iris.modded.ModdedServerLevels; +import art.arcane.iris.modded.ModdedStartup; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.IdentifierArgument; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Predicate; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class ModdedWorldCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + private static final String DEFAULT_NAMESPACE = "irisworldgen"; + private static final long DEFAULT_SEED = 1337L; + private static final SuggestionProvider LOADED_DIMENSIONS = (CommandContext context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(loadedIrisDimensions(context.getSource().getServer()), builder); + + private ModdedWorldCommands() { + } + + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); + + root.then(Commands.literal("status") + .executes((CommandContext context) -> status(context.getSource()))); + root.then(Commands.literal("list") + .executes((CommandContext context) -> list(context.getSource()))); + root.then(Commands.literal("ls") + .executes((CommandContext context) -> list(context.getSource()))); + + root.then(enableTree("enable")); + root.then(enableTree("create")); + root.then(replaceOverworldTree()); + root.then(mainWorldTree()); + + root.then(disableTree()); + root.then(deleteTree("delete")); + root.then(deleteTree("remove")); + root.then(deleteTree("rm")); + + return root; + } + + private static LiteralArgumentBuilder enableTree(String name) { + return Commands.literal(name) + .then(Commands.argument("dimension", IdentifierArgument.id()) + .then(Commands.argument("pack", StringArgumentType.string()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> enable(context.getSource(), + dimensionArgument(context), + StringArgumentType.getString(context, "pack"), + null)) + .then(Commands.argument("seed", StringArgumentType.word()) + .executes((CommandContext context) -> enable(context.getSource(), + dimensionArgument(context), + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "seed")))))); + } + + private static LiteralArgumentBuilder disableTree() { + return Commands.literal("disable") + .then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS) + .executes((CommandContext context) -> disable(context.getSource(), dimensionArgument(context), false))); + } + + private static LiteralArgumentBuilder deleteTree(String name) { + return Commands.literal(name) + .then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS) + .executes((CommandContext context) -> disable(context.getSource(), dimensionArgument(context), true))); + } + + private static LiteralArgumentBuilder replaceOverworldTree() { + return Commands.literal("replace-overworld") + .then(Commands.argument("pack", StringArgumentType.string()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> replaceOverworld(context.getSource(), + StringArgumentType.getString(context, "pack"), + null)) + .then(Commands.argument("seed", StringArgumentType.word()) + .executes((CommandContext context) -> replaceOverworld(context.getSource(), + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "seed"))))); + } + + private static LiteralArgumentBuilder mainWorldTree() { + return Commands.literal("mainworld") + .then(Commands.literal("off") + .executes((CommandContext context) -> clearMainWorld(context.getSource()))) + .then(Commands.argument("pack", StringArgumentType.string()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> mainWorld(context.getSource(), + StringArgumentType.getString(context, "pack"), + null)) + .then(Commands.argument("seed", StringArgumentType.word()) + .executes((CommandContext context) -> mainWorld(context.getSource(), + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "seed"))))); + } + + public static int createWorld(CommandSourceStack source, String name, String pack, long seed) { + String[] packRef = parsePackRef(pack); + return enable(source, name, packRef[0], packRef[1], seed); + } + + private static int enable(CommandSourceStack source, String targetDimension, String packRaw, String seedRaw) { + Long seed = parseSeed(source, seedRaw); + if (seed == null) { + return 0; + } + String[] packRef = parsePackRef(packRaw); + return enable(source, targetDimension, packRef[0], packRef[1], seed); + } + + private static int enable(CommandSourceStack source, String targetDimension, String pack, String packDimension, long seed) { + MinecraftServer server = source.getServer(); + String dimensionId; + try { + dimensionId = normalizeDimensionId(targetDimension); + } catch (IllegalArgumentException e) { + IrisModdedCommands.fail(source, e.getMessage()); + return 0; + } + if (!validPackRef(source, pack, packDimension)) { + return 0; + } + File packFolder = new File(ModdedPackCommands.packsRoot(), pack); + if (packFolder.isDirectory()) { + return enableInstalled(source, server, dimensionId, pack, packDimension, seed); + } + IrisModdedCommands.fail(source, "Pack '" + pack + + "' is not installed. Use /iris download pack=overworld or pack=underworld, then restart."); + return 0; + } + + private static int enableInstalled(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, String packDimension, long seed) { + if (blockIfPackBroken(source, dimensionId, pack)) { + return 0; + } + if (!loadPackDimension(source, pack, packDimension)) { + return 0; + } + try { + ModdedDimensionManager.createPersistent(server, dimensionId, pack, packDimension, seed); + } catch (Throwable e) { + LOGGER.error("Iris world injection failed for {} (pack={} dim={})", dimensionId, pack, packDimension, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_WORLD, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e)))); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_CREATED_IRIS_WORLD_FROM_PACK_DIMENSION_SEED, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("packDimension", packDimension), MessageArgument.untrusted("seed", seed))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IT_IS_LIVE_NOW_RE_INJECTED_ON_EVERY_STARTUP_TELEPORT)); + return 1; + } + + private static int replaceOverworld(CommandSourceStack source, String packRaw, String seedRaw) { + MinecraftServer server = source.getServer(); + Long seed = parseSeed(source, seedRaw); + if (seed == null) { + return 0; + } + String[] packRef = parsePackRef(packRaw); + String pack = packRef[0]; + String packDimension = packRef[1]; + if (!validPackRef(source, pack, packDimension)) { + return 0; + } + String dimensionId = DEFAULT_NAMESPACE + ":primary"; + if (blockIfPackBroken(source, dimensionId, pack)) { + return 0; + } + if (!loadPackDimension(source, pack, packDimension)) { + return 0; + } + try { + ModdedDimensionManager.createPersistent(server, dimensionId, pack, packDimension, seed); + } catch (Throwable e) { + LOGGER.error("Iris primary world injection failed for {} (pack={} dim={})", dimensionId, pack, packDimension, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_PRIMARY_WORLD, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e)))); + return 0; + } + ModdedModConfig.setPrimaryWorld(dimensionId); + ModdedPrimaryWorldRouter.clear(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IRIS_PRIMARY_WORLD_SET_PACK_DIMENSION_SEED, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("packDimension", packDimension), MessageArgument.untrusted("seed", seed))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_VANILLA_OVERWORLD_GENERATOR_CANNOT_BE_HOT_SWAPPED_SO_THIS_DOES)); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_INSTEAD_IS_NOW_CONFIGURED_PRIMARY_WORLD_PLAYERS_VANILLA_OVERWORLD_ARE, MessageArgument.untrusted("dimensionId", dimensionId))); + return 1; + } + + private static int clearMainWorld(CommandSourceStack source) { + ModdedModConfig.setMainWorld("", 0L); + MainWorldService.clearOverride(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IRIS_MAIN_WORLD_OVERRIDE_CLEARED_OVERWORLD_KEEPS_ITS_CURRENT_GENERATOR)); + return 1; + } + + private static int mainWorld(CommandSourceStack source, String packRaw, String seedRaw) { + MinecraftServer server = source.getServer(); + long seed; + if (seedRaw == null || seedRaw.isBlank()) { + seed = 0L; + } else if (seedRaw.equalsIgnoreCase("random")) { + long rolled = ThreadLocalRandom.current().nextLong(); + seed = rolled == 0L ? 1L : rolled; + } else { + try { + seed = Long.parseLong(seedRaw.trim()); + } catch (NumberFormatException e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_INVALID_SEED_USE_NUMBER_RANDOM, MessageArgument.untrusted("seedRaw", seedRaw))); + return 0; + } + } + String[] packRef = parsePackRef(packRaw); + String pack = packRef[0]; + String packDimension = packRef[1]; + if (!validPackRef(source, pack, packDimension)) { + return 0; + } + File packFolder = new File(ModdedPackCommands.packsRoot(), pack); + if (packFolder.isDirectory()) { + return applyMainWorld(source, pack, packDimension, packRaw, seed); + } + IrisModdedCommands.fail(source, "Pack '" + pack + + "' is not installed. Use /iris download pack=overworld or pack=underworld, then restart."); + return 0; + } + + private static int applyMainWorld(CommandSourceStack source, String pack, String packDimension, String packRef, long seed) { + if (blockIfPackBroken(source, "the main world", pack)) { + return 0; + } + try { + if (!loadPackDimension(source, pack, packDimension)) { + return 0; + } + } catch (Throwable e) { + LOGGER.error("Iris main world pack load failed for {} (dim={})", pack, packDimension, e); + if (PackValidationRegistry.get(pack) == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, MessageArgument.untrusted("pack", pack))); + return 0; + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_VALIDATION_FAILED, + MessageArgument.untrusted("value", pack + ":" + packDimension), + MessageArgument.trusted("value2", e.getClass().getSimpleName() + IrisLanguage.errorDetail(e)))); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("pack", pack))); + return 0; + } + ModdedModConfig.setMainWorld(packRef, seed); + if (!MainWorldService.stage(packRef, seed)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FAILED_WRITE_SERVER_PROPERTIES_CHECK_FILE_PERMISSIONS_SET_LEVEL_TYPE)); + return 0; + } + String preset = MainWorldService.presetIdFor(packRef); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IRIS_MAIN_WORLD_SET_PRESET_SEED, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("preset", preset), MessageArgument.untrusted("value", seed == 0L ? IrisLanguage.plain(RuntimeUiMessages.STATUS_RANDOM) : Long.toString(seed)))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_SERVER_PROPERTIES_LEVEL_TYPE_IS_NOW_ON_NEXT_RESTART_OVERWORLD, MessageArgument.untrusted("preset", preset))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PLAYER_DATA_INVENTORIES_ADVANCEMENTS_STATS_IS_KEPT_EXISTING_TERRAIN_THOSE)); + if (ModdedModConfig.get().mainWorldAutoRestart()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_MAINWORLDAUTORESTART_IS_ENABLED_STOPPING_SERVER_NOW_SO_YOUR_RESTART_WRAPPER)); + source.getServer().halt(false); + } else { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_RESTART_SERVER_NOW_GENERATE_IT_SET_MAINWORLDAUTORESTART_TRUE_MODDED_JSON)); + } + return 1; + } + + private static boolean blockIfPackBroken(CommandSourceStack source, String dimensionId, String pack) { + try { + ModdedStartup.requirePackForWorldCreation(pack); + return false; + } catch (BrokenPackException e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_REFUSING_CREATE_WORLD_USING_PACK_BECAUSE_REQUIRED_VALIDATION_FAILED, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack))); + for (String reason : e.getReasons()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_MESSAGE, MessageArgument.untrusted("reason", reason))); + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("pack", pack))); + return true; + } + } + + private static boolean loadPackDimension(CommandSourceStack source, String pack, String packDimension) { + File packFolder = new File(ModdedPackCommands.packsRoot(), pack); + if (!packFolder.isDirectory()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_WAS_NOT_FOUND_UNDER, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("value", ModdedPackCommands.packsRoot().getAbsolutePath()))); + return false; + } + IrisData data = IrisData.get(packFolder); + IrisDimension dimension = data.getDimensionLoader().load(packDimension); + if (dimension == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_DOES_NOT_CONTAIN_DIMENSIONS_JSON, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("packDimension", packDimension))); + return false; + } + return true; + } + + private static String[] parsePackRef(String raw) { + String value = raw.trim(); + int colon = value.indexOf(':'); + if (colon >= 0) { + return new String[]{value.substring(0, colon), value.substring(colon + 1)}; + } + return new String[]{value, value}; + } + + private static boolean validPackRef(CommandSourceStack source, String pack, String packDimension) { + if (pack.matches("[A-Za-z0-9_.-]+") && !pack.contains("..") + && packDimension.matches("[A-Za-z0-9_/.-]+") && !packDimension.contains("..")) { + return true; + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_INVALID_PACK_REFERENCE_USE_PACK_PACK_DIMENSIONKEY, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("value", (pack.equals(packDimension) ? "" : ":" + packDimension)))); + return false; + } + + private static Long parseSeed(CommandSourceStack source, String seedRaw) { + if (seedRaw == null || seedRaw.isBlank()) { + return DEFAULT_SEED; + } + String value = seedRaw.trim(); + if (value.equalsIgnoreCase("random")) { + return ThreadLocalRandom.current().nextLong(); + } + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_INVALID_SEED_USE_NUMBER_RANDOM_2, MessageArgument.untrusted("seedRaw", seedRaw))); + return null; + } + } + + private static int disable(CommandSourceStack source, String targetDimension, boolean wipeStorage) { + MinecraftServer server = source.getServer(); + String dimensionId; + try { + dimensionId = normalizeDimensionId(targetDimension); + } catch (IllegalArgumentException e) { + IrisModdedCommands.fail(source, e.getMessage()); + return 0; + } + boolean removed; + try { + removed = ModdedDimensionManager.removePersistent(server, dimensionId, wipeStorage); + } catch (Throwable e) { + LOGGER.error("Iris world removal failed for {}", dimensionId, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FAILED_REMOVE_IRIS_WORLD, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e)))); + return 0; + } + if (dimensionId.equals(ModdedModConfig.get().primaryWorld())) { + ModdedModConfig.setPrimaryWorld(""); + ModdedPrimaryWorldRouter.clear(); + } + if (!removed) { + if (wipeStorage) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IRIS_WORLD_WAS_NOT_LOADED_CLEARED_ITS_PERSISTENT_REGISTRY_ENTRY, MessageArgument.untrusted("dimensionId", dimensionId))); + } else { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IRIS_WORLD_WAS_NOT_LOADED_CLEARED_ITS_PERSISTENT_REGISTRY_ENTRY_2, MessageArgument.untrusted("dimensionId", dimensionId))); + } + return 1; + } + if (wipeStorage) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_DELETED_IRIS_WORLD_EVACUATED_UNLOADED_CHUNK_MANTLE_DATA_WIPED_DROPPED, MessageArgument.untrusted("dimensionId", dimensionId))); + } else { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_DISABLED_IRIS_WORLD_EVACUATED_UNLOADED_DROPPED_FROM_STARTUP_REGISTRY, MessageArgument.untrusted("dimensionId", dimensionId))); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_WORLD_DATA_ON_DISK_IS_KEPT_RE_ENABLE_WITH_IRIS, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("dimensionId2", dimensionId))); + } + return 1; + } + + private static int status(CommandSourceStack source) { + MinecraftServer server = source.getServer(); + int loaded = 0; + for (ServerLevel level : ModdedServerLevels.levels(server)) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) { + loaded++; + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_LOADED_IRIS_LEVEL_PACK_DIMENSION, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", generator.activePack()), MessageArgument.untrusted("value3", generator.activeDimensionKey()))); + } + } + String primary = ModdedModConfig.get().primaryWorld(); + if (!primary.isBlank()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PRIMARY_WORLD, MessageArgument.untrusted("primary", primary), MessageArgument.trusted("value", IrisLanguage.plain(ModdedModConfig.get().routePlayersToPrimaryWorld() ? RuntimeUiMessages.PRIMARY_PLAYERS_ROUTED_SUFFIX : RuntimeUiMessages.PRIMARY_ROUTING_DISABLED_SUFFIX)))); + } + if (loaded == 0) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_NO_IRIS_DIMENSIONS_ARE_CURRENTLY_LOADED_CREATE_ONE_WITH_IRIS)); + } + return loaded > 0 ? 1 : 0; + } + + private static int list(CommandSourceStack source) { + List dimensions = loadedIrisDimensions(source.getServer()); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_LOADED_IRIS_DIMENSIONS, MessageArgument.untrusted("value", dimensions.size()))); + for (String dimension : dimensions) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_MESSAGE_2, MessageArgument.untrusted("dimension", dimension))); + } + if (dimensions.isEmpty()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_USE_IRIS_WORLD_CREATE_NAME_PACK_INJECT_ONE_WITHOUT_RESTARTING)); + } + return 1; + } + + private static List loadedIrisDimensions(MinecraftServer server) { + List dimensions = new ArrayList<>(); + for (ServerLevel level : ModdedServerLevels.levels(server)) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) { + dimensions.add(level.dimension().identifier().toString()); + } + } + return dimensions; + } + + private static String dimensionArgument(CommandContext context) { + for (ParsedCommandNode node : context.getNodes()) { + if ("dimension".equals(node.getNode().getName())) { + return node.getRange().get(context.getInput()); + } + } + return IdentifierArgument.getId(context, "dimension").toString(); + } + + private static String normalizeDimensionId(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("Missing dimension id."); + } + String normalized = value.trim().toLowerCase(Locale.ROOT); + String namespace = DEFAULT_NAMESPACE; + String path = normalized; + int colon = normalized.indexOf(':'); + if (colon >= 0) { + namespace = normalized.substring(0, colon); + path = normalized.substring(colon + 1); + } + if (!namespace.matches("[a-z0-9_.-]+") || !path.matches("[a-z0-9_./-]+") || path.startsWith("/") || path.endsWith("/") || path.contains("..")) { + throw new IllegalArgumentException("Invalid dimension id '" + value + "'. Use name or namespace:path."); + } + return namespace + ":" + path; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/EntityPersistenceMixin.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/EntityPersistenceMixin.java new file mode 100644 index 000000000..d3972a3fc --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/EntityPersistenceMixin.java @@ -0,0 +1,19 @@ +package art.arcane.iris.modded.mixin; + +import art.arcane.iris.modded.ModdedEntityPersistence; +import art.arcane.iris.modded.ModdedMixinFlags; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Entity.class) +public abstract class EntityPersistenceMixin { + @Inject(method = "shouldBeSaved", at = @At("RETURN"), cancellable = true) + private void iris$applyGeneratedPersistence(CallbackInfoReturnable info) { + ModdedMixinFlags.markEntityPersistence(); + Entity entity = (Entity) (Object) this; + info.setReturnValue(ModdedEntityPersistence.shouldSave(entity, info.getReturnValue())); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/LivingEntityLootMixin.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/LivingEntityLootMixin.java new file mode 100644 index 000000000..ac6f63701 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/LivingEntityLootMixin.java @@ -0,0 +1,22 @@ +package art.arcane.iris.modded.mixin; + +import art.arcane.iris.modded.ModdedDeathLoot; +import art.arcane.iris.modded.ModdedMixinFlags; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityLootMixin { + @Inject(method = "dropFromLootTable(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;Z)V", at = @At("HEAD"), cancellable = true) + private void iris$replaceBaseLoot(ServerLevel level, DamageSource damageSource, boolean playerKilled, CallbackInfo info) { + ModdedMixinFlags.markLivingEntityLoot(); + if (ModdedDeathLoot.replaceBaseLoot((LivingEntity) (Object) this)) { + info.cancel(); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/MobAwarenessMixin.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/MobAwarenessMixin.java new file mode 100644 index 000000000..e3e02f8d4 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/MobAwarenessMixin.java @@ -0,0 +1,41 @@ +package art.arcane.iris.modded.mixin; + +import art.arcane.iris.modded.ModdedEntityAwareness; +import art.arcane.iris.modded.ModdedMixinFlags; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.goal.FloatGoal; +import net.minecraft.world.entity.ai.goal.WrappedGoal; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Mob.class) +public abstract class MobAwarenessMixin { + @Inject( + method = "serverAiStep", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/world/entity/Mob;noActionTime:I", + opcode = Opcodes.PUTFIELD, + shift = At.Shift.AFTER), + cancellable = true) + private void iris$tickUnawareMob(CallbackInfo info) { + ModdedMixinFlags.markMobAwareness(); + Mob mob = (Mob) (Object) this; + if (ModdedEntityAwareness.isAware(mob)) { + return; + } + for (WrappedGoal wrappedGoal : mob.getGoalSelector().getAvailableGoals()) { + if (wrappedGoal.getGoal() instanceof FloatGoal floatGoal) { + if (floatGoal.canUse()) { + floatGoal.tick(); + } + mob.getJumpControl().tick(); + break; + } + } + info.cancel(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/StructureTemplatePaletteConcurrencyMixin.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/StructureTemplatePaletteConcurrencyMixin.java new file mode 100644 index 000000000..3c6213d7b --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/StructureTemplatePaletteConcurrencyMixin.java @@ -0,0 +1,31 @@ +package art.arcane.iris.modded.mixin; + +import art.arcane.iris.modded.ModdedMixinFlags; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Mixin(StructureTemplate.Palette.class) +public abstract class StructureTemplatePaletteConcurrencyMixin { + @Shadow + @Final + @Mutable + private Map> cache; + + @Inject(method = "(Ljava/util/List;)V", at = @At("RETURN")) + private void iris$installConcurrentBlockCache(List blocks, + CallbackInfo info) { + cache = new ConcurrentHashMap<>(); + ModdedMixinFlags.markStructureTemplatePalette(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java new file mode 100644 index 000000000..6e19788cf --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedChunkUpdateService.java @@ -0,0 +1,377 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.service; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedBlockResolution; +import art.arcane.iris.modded.ModdedLootApplier; +import art.arcane.iris.modded.ModdedServerLevels; +import art.arcane.iris.modded.ModdedTileData; +import art.arcane.iris.modded.api.ModdedCustomContentRegistry; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.project.matter.TileWrapper; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterUpdate; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +public final class ModdedChunkUpdateService implements ModdedTickableService { + private static final long PASS_PERIOD_MILLIS = 3_000L; + private static final int PLAYER_CHUNK_RADIUS = 1; + private static final int SILENT_FLAGS = Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_SKIP_ON_PLACE; + + private final Set warmupQueue = ConcurrentHashMap.newKeySet(); + private volatile ExecutorService warmupExecutor; + private long lastPassAt; + + @Override + public void onEnable() { + if (warmupExecutor != null) { + return; + } + warmupExecutor = Executors.newSingleThreadExecutor((Runnable runnable) -> { + Thread thread = new Thread(runnable, "Iris Mantle Warmup"); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + lastPassAt = 0L; + } + + @Override + public void onDisable() { + ExecutorService active = warmupExecutor; + warmupExecutor = null; + if (active != null) { + // NEVER interrupt first: an interrupt inside a FileChannel plate read closes the + // channel (ClosedByInterruptException) and the read-failure fallback installs an + // EMPTY plate that the very next shutdown stage flushes over real data. Queued + // warm-ups self-cancel (warmupExecutor is already null), so shutdown() + await + // only waits on the single in-flight load; escalate only on timeout. + active.shutdown(); + try { + if (!active.awaitTermination(5L, TimeUnit.SECONDS)) { + IrisLogging.warn("Iris mantle warm-up did not stop before engine close; forcing interrupt"); + active.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + warmupQueue.clear(); + } + + @Override + public void onServerTick(MinecraftServer server) { + long now = System.currentTimeMillis(); + if (now - lastPassAt < PASS_PERIOD_MILLIS) { + return; + } + lastPassAt = now; + + if (!IrisSettings.get().getWorld().isPostLoadBlockUpdates()) { + return; + } + + for (ServerLevel level : ModdedServerLevels.levels(server)) { + if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) { + continue; + } + Engine engine = generator.engineIfBound(); + if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) { + continue; + } + if (!hasUpdateTargets(!level.players().isEmpty(), !level.getForceLoadedChunks().isEmpty()) || isPregenActive(engine)) { + continue; + } + try { + updateNearPlayers(engine, level); + updateForcedChunks(engine, level); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + + static boolean hasUpdateTargets(boolean hasPlayers, boolean hasForcedChunks) { + return hasPlayers || hasForcedChunks; + } + + private boolean isPregenActive(Engine engine) { + PregeneratorJob job = PregeneratorJob.getInstance(); + return job != null && job.targetsWorldIdentity(engine.getWorld().identity()); + } + + private void updateNearPlayers(Engine engine, ServerLevel level) { + for (ServerPlayer player : level.players()) { + int centerX = player.blockPosition().getX() >> 4; + int centerZ = player.blockPosition().getZ() >> 4; + for (int dx = -PLAYER_CHUNK_RADIUS; dx <= PLAYER_CHUNK_RADIUS; dx++) { + for (int dz = -PLAYER_CHUNK_RADIUS; dz <= PLAYER_CHUNK_RADIUS; dz++) { + updateChunk(engine, level, centerX + dx, centerZ + dz); + } + } + } + } + + private void updateForcedChunks(Engine engine, ServerLevel level) { + for (long chunkKey : level.getForceLoadedChunks()) { + updateChunk(engine, level, ChunkPos.getX(chunkKey), ChunkPos.getZ(chunkKey)); + } + } + + public void updateRegeneratedChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) { + updateChunk(engine, level, chunkX, chunkZ, false); + } + + private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) { + updateChunk(engine, level, chunkX, chunkZ, true); + } + + private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ, boolean requireNeighbors) { + if (requireNeighbors) { + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + if (level.getChunkSource().getChunkNow(chunkX + x, chunkZ + z) == null) { + return; + } + } + } + } + + Mantle mantle = engine.getMantle().getMantle(); + if (!mantle.isChunkLoaded(chunkX, chunkZ)) { + warmupMantleChunk(mantle, chunkX, chunkZ); + return; + } + if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.ETCHED)) { + return; + } + + MantleChunk chunk = mantle.getChunk(chunkX, chunkZ).use(); + try { + chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> { + chunk.raiseFlagUnchecked(MantleFlag.TILE, () -> runTilePass(engine, level, chunkX, chunkZ, chunk)); + chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, () -> runCustomPass(engine, level, chunkX, chunkZ, chunk)); + chunk.raiseFlagUnchecked(MantleFlag.UPDATE, () -> runUpdatePass(engine, level, chunkX, chunkZ, chunk)); + }); + } finally { + chunk.release(); + } + } + + private void runTilePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk chunk) { + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + int minHeight = engine.getWorld().minHeight(); + materializeDeferredSlice(chunk, TileWrapper.class, () -> + chunk.iterate(TileWrapper.class, (Integer x, Integer yf, Integer z, TileWrapper v) -> { + int y = yf + minHeight; + if (y < level.getMinY() || y >= level.getMaxY()) { + return; + } + applyTile(level, baseX + (x & 15), y, baseZ + (z & 15), v.getData()); + })); + } + + private void runCustomPass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk chunk) { + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + int minHeight = engine.getWorld().minHeight(); + materializeDeferredSlice(chunk, Identifier.class, () -> + chunk.iterate(Identifier.class, (Integer x, Integer yf, Integer z, Identifier identifier) -> { + int y = yf + minHeight; + if (y < level.getMinY() || y >= level.getMaxY()) { + return; + } + BlockPos position = new BlockPos(baseX + (x & 15), y, baseZ + (z & 15)); + ModdedCustomContentRegistry.processBlockPlacement(engine, level, position, identifier.toString()); + })); + } + + static void materializeDeferredSlice(MantleChunk chunk, Class sliceType, Runnable materializer) { + materializer.run(); + chunk.deleteSlices(sliceType); + } + + private void applyTile(ServerLevel level, int x, int y, int z, TileData tile) { + if (!(tile instanceof ModdedTileData moddedTile)) { + return; + } + BlockPos pos = new BlockPos(x, y, z); + BlockState state = level.getBlockState(pos); + BlockState adjusted = moddedTile.adjustBlockState(state); + if (adjusted != state) { + level.setBlock(pos, adjusted, SILENT_FLAGS); + state = adjusted; + } + if (!state.hasBlockEntity() || !(state.getBlock() instanceof EntityBlock entityBlock)) { + return; + } + try { + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity == null) { + blockEntity = entityBlock.newBlockEntity(pos, state); + } + if (blockEntity == null) { + IrisLogging.warn("Iris could not create block entity at " + pos + " for " + state); + return; + } + if (!moddedTile.isApplicable(state, blockEntity)) { + return; + } + level.setBlockEntity(blockEntity); + if (!moddedTile.apply(blockEntity, level)) { + IrisLogging.warn("Iris tile payload was empty at " + pos + " for " + state); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private void warmupMantleChunk(Mantle mantle, int chunkX, int chunkZ) { + ExecutorService active = warmupExecutor; + if (active == null) { + return; + } + long key = Cache.key(chunkX, chunkZ); + if (!warmupQueue.add(key)) { + return; + } + try { + active.execute(() -> { + // Self-cancel on shutdown: warmupExecutor is nulled first in onDisable, so + // queued prefetches no-op instantly and only an in-flight load is awaited. + if (warmupExecutor == null || mantle.isClosed()) { + warmupQueue.remove(key); + return; + } + try { + mantle.getChunk(chunkX, chunkZ); + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + warmupQueue.remove(key); + } + }); + } catch (RejectedExecutionException rejected) { + warmupQueue.remove(key); + } + } + + private void runUpdatePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk chunk) { + PrecisionStopwatch stopwatch = PrecisionStopwatch.start(); + int minHeight = engine.getWorld().minHeight(); + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + int[][] grid = new int[16][16]; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + grid[x][z] = Integer.MIN_VALUE; + } + } + + chunk.iterate(MatterCavern.class, (Integer x, Integer yf, Integer z, MatterCavern v) -> { + int y = yf + minHeight; + if (y < level.getMinY() || y >= level.getMaxY()) { + return; + } + int lx = x & 15; + int lz = z & 15; + BlockPos pos = new BlockPos(baseX + lx, y, baseZ + lz); + if (!ModdedBlockResolution.isFluid(level.getBlockState(pos))) { + return; + } + boolean exposed = ModdedBlockResolution.isAir(level.getBlockState(pos.below())) + || ModdedBlockResolution.isAir(level.getBlockState(pos.west())) + || ModdedBlockResolution.isAir(level.getBlockState(pos.east())) + || ModdedBlockResolution.isAir(level.getBlockState(pos.south())) + || ModdedBlockResolution.isAir(level.getBlockState(pos.north())); + + if (exposed) { + grid[lx][lz] = Math.max(grid[lx][lz], y); + } + }); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (grid[x][z] == Integer.MIN_VALUE) { + continue; + } + update(engine, level, x, grid[x][z], z, baseX, baseZ, chunk); + } + } + + chunk.iterate(MatterUpdate.class, (Integer x, Integer yf, Integer z, MatterUpdate v) -> { + if (v != null && v.isUpdate()) { + update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk); + } + }); + chunk.deleteSlices(MatterUpdate.class); + engine.getMetrics().getUpdates().put(stopwatch.getMilliseconds()); + } + + private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk chunk) { + if (y < level.getMinY() || y >= level.getMaxY()) { + return; + } + BlockPos pos = new BlockPos(baseX + (x & 15), y, baseZ + (z & 15)); + BlockState state = level.getBlockState(pos); + engine.blockUpdatedMetric(); + if (ModdedBlockResolution.isStorage(state)) { + if (!ModdedBlockResolution.isStorageChest(state)) { + return; + } + try { + ModdedLootApplier.apply(engine, level, pos, state, chunk); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } else { + level.setBlock(pos, Blocks.AIR.defaultBlockState(), SILENT_FLAGS); + level.setBlock(pos, state, Block.UPDATE_ALL); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java new file mode 100644 index 000000000..af2ab2e4e --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java @@ -0,0 +1,229 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.service; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.service.EngineMaintenance; +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.modded.ModdedWorldEngines; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.project.context.IrisContext; +import net.minecraft.server.MinecraftServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +public final class ModdedEngineMaintenanceService implements ModdedTickableService { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final long MAINTENANCE_PERIOD_MILLIS = 2_000L; + private static final long SAVE_PERIOD_MILLIS = 60_000L; + private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L; + private static final long INTERRUPT_DRAIN_TIMEOUT_SECONDS = 5L; + + private final Set inFlight = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>())); + private final Map lastSavedAt = Collections.synchronizedMap(new IdentityHashMap<>()); + private volatile ExecutorService service; + private long lastMaintenanceAt; + + @Override + public void onEnable() { + ExecutorService current = service; + if (current != null) { + if (!current.isTerminated()) { + throw new IllegalStateException( + "Iris engine maintenance cannot restart while prior workers are still active"); + } + service = null; + } + + IrisSettings.IrisSettingsEngineSVC settings = IrisSettings.get().getPerformance().getEngineSVC(); + ThreadFactory factory = (settings.isUseVirtualThreads() + ? Thread.ofVirtual() + : Thread.ofPlatform().priority(settings.getPriority())) + .name("Iris EngineSVC-", 0) + .factory(); + service = Executors.newFixedThreadPool(EngineMaintenance.workerParallelism(), factory); + lastMaintenanceAt = 0L; + inFlight.clear(); + lastSavedAt.clear(); + } + + @Override + public void onDisable() { + ExecutorService active = service; + boolean drained = shutdownAndDrain(active); + if (drained) { + service = null; + inFlight.clear(); + } + lastSavedAt.clear(); + if (!drained) { + throw new IllegalStateException("Iris engine maintenance workers remained active during service shutdown"); + } + } + + @Override + public void onServerTick(MinecraftServer server) { + ExecutorService active = service; + if (active == null || active.isShutdown()) { + return; + } + + long now = System.currentTimeMillis(); + if (now - lastMaintenanceAt < MAINTENANCE_PERIOD_MILLIS) { + return; + } + lastMaintenanceAt = now; + + Collection engines = ModdedWorldEngines.activeEngines(); + reconcileSaveState(engines, now); + for (Engine engine : engines) { + if (!inFlight.add(engine)) { + continue; + } + try { + active.execute(() -> { + try { + maintain(engine, now); + } finally { + inFlight.remove(engine); + } + }); + } catch (RejectedExecutionException exception) { + inFlight.remove(engine); + if (active == service && !active.isShutdown()) { + LOGGER.error("Iris rejected engine maintenance for {}", engineName(engine), exception); + } + } + } + } + + private void maintain(Engine engine, long scheduledAt) { + if (engine == null || engine.isClosing() || engine.isClosed()) { + return; + } + try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_engine_maintenance"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + if (!EngineMaintenance.isAvailable(engine)) { + return; + } + + boolean pregeneratorTargetsWorld = EngineMaintenance.pregeneratorTargets(engine); + if (pregeneratorTargetsWorld) { + return; + } + + saveIfDue(engine, scheduledAt); + if (!EngineMaintenance.shouldRun(engine)) { + return; + } + + EngineMaintenance.Outcome outcome = EngineMaintenance.run(engine); + if (outcome.unloadedTectonicPlates() > 0) { + LOGGER.debug("Iris unloaded {} tectonic plates in {}ms for {}", + outcome.unloadedTectonicPlates(), outcome.unloadDurationMillis(), engineName(engine)); + } + } catch (GenerationSessionException exception) { + if (engine.isClosing() || exception.isExpectedTeardown()) { + return; + } + IrisLogging.reportError(exception); + LOGGER.error("Iris engine maintenance session failed for {}", engineName(engine), exception); + } catch (Throwable exception) { + if (EngineMaintenance.isMantleClosed(exception)) { + return; + } + IrisLogging.reportError(exception); + LOGGER.error("Iris engine maintenance failed for {}", engineName(engine), exception); + } + } + + private void saveIfDue(Engine engine, long scheduledAt) { + Long lastSave = lastSavedAt.get(engine); + if (lastSave == null || scheduledAt - lastSave < SAVE_PERIOD_MILLIS) { + return; + } + + try { + engine.save(); + lastSavedAt.put(engine, System.currentTimeMillis()); + } catch (Throwable exception) { + if (EngineMaintenance.isMantleClosed(exception)) { + return; + } + IrisLogging.reportError(exception); + LOGGER.error("Iris engine save failed for {}", engineName(engine), exception); + } + } + + private void reconcileSaveState(Collection engines, long now) { + Set activeEngines = Collections.newSetFromMap(new IdentityHashMap<>()); + activeEngines.addAll(engines); + synchronized (lastSavedAt) { + lastSavedAt.keySet().removeIf(engine -> !activeEngines.contains(engine)); + for (Engine engine : activeEngines) { + lastSavedAt.putIfAbsent(engine, now); + } + } + } + + private boolean shutdownAndDrain(ExecutorService active) { + if (active == null) { + return true; + } + + active.shutdown(); + try { + if (active.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + return true; + } + active.shutdownNow(); + if (active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + return true; + } + IllegalStateException failure = new IllegalStateException( + "Iris engine maintenance workers did not stop after shutdownNow"); + IrisLogging.reportError(failure); + LOGGER.error("Iris engine maintenance did not terminate; active engine lifecycle leases will block unsafe shutdown", failure); + return false; + } catch (InterruptedException exception) { + active.shutdownNow(); + Thread.currentThread().interrupt(); + IrisLogging.reportError(exception); + LOGGER.error("Interrupted while draining Iris engine maintenance", exception); + return false; + } + } + + private static String engineName(Engine engine) { + return engine == null || engine.getWorld() == null ? "" : engine.getWorld().name(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEntitySpawnService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEntitySpawnService.java new file mode 100644 index 000000000..8678b15a7 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEntitySpawnService.java @@ -0,0 +1,60 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.service; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineWorldManager; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedServerLevels; +import art.arcane.iris.modded.ModdedWorldManager; +import art.arcane.iris.spi.IrisLogging; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; + +public final class ModdedEntitySpawnService implements ModdedTickableService { + @Override + public void onEnable() { + } + + @Override + public void onDisable() { + } + + @Override + public void onServerTick(MinecraftServer server) { + for (ServerLevel level : ModdedServerLevels.levels(server)) { + if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) { + continue; + } + Engine engine = generator.engineIfBound(); + if (engine == null || engine.isClosed()) { + continue; + } + EngineWorldManager worldManager = engine.getWorldManager(); + if (!(worldManager instanceof ModdedWorldManager moddedWorldManager)) { + continue; + } + try { + moddedWorldManager.serverTick(level); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedLogFilterService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedLogFilterService.java new file mode 100644 index 000000000..9158218f0 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedLogFilterService.java @@ -0,0 +1,176 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.service; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.message.Message; + +import java.util.List; + +public final class ModdedLogFilterService implements ModdedService, Filter { + private static final String VANILLA_LOGGER_PREFIX = "net.minecraft"; + private static final List FILTERS = List.of( + "Ignoring heightmap data for chunk", + "Could not save data net.minecraft.world.entity.raid.PersistentRaid", + "UUID of added entity already exists"); + + private boolean installed = false; + + @Override + public void onEnable() { + if (installed) { + return; + } + ((Logger) LogManager.getRootLogger()).addFilter(this); + installed = true; + } + + @Override + public void onDisable() { + if (!installed) { + return; + } + ((Logger) LogManager.getRootLogger()).get().removeFilter(this); + installed = false; + } + + @Override + public void initialize() { + } + + @Override + public void start() { + } + + @Override + public void stop() { + } + + @Override + public boolean isStarted() { + return true; + } + + @Override + public boolean isStopped() { + return false; + } + + @Override + public State getState() { + return State.STARTED; + } + + @Override + public Filter.Result getOnMatch() { + return Result.NEUTRAL; + } + + @Override + public Filter.Result getOnMismatch() { + return Result.NEUTRAL; + } + + @Override + public Result filter(LogEvent event) { + return check(event.getLoggerName(), event.getMessage().getFormattedMessage()); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) { + return check(logger.getName(), String.valueOf(msg)); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) { + return check(logger.getName(), msg.getFormattedMessage()); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object... params) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8) { + return check(logger.getName(), message); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8, Object p9) { + return check(logger.getName(), message); + } + + private Result check(String loggerName, String message) { + if (loggerName == null || !loggerName.startsWith(VANILLA_LOGGER_PREFIX) || message == null) { + return Result.NEUTRAL; + } + for (String filter : FILTERS) { + if (message.contains(filter)) { + return Result.DENY; + } + } + return Result.NEUTRAL; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedPreservationService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedPreservationService.java new file mode 100644 index 000000000..7a0de0e40 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedPreservationService.java @@ -0,0 +1,124 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.service; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.iris.engine.framework.PreservationRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class ModdedPreservationService implements ModdedService, PreservationRegistry { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final long DEREFERENCE_INTERVAL_MILLIS = 60000L; + + private final List threads = new CopyOnWriteArrayList<>(); + private final List services = new CopyOnWriteArrayList<>(); + private final List> caches = new CopyOnWriteArrayList<>(); + private final AtomicBoolean running = new AtomicBoolean(false); + private Thread dereferencer; + + @Override + public void register(Thread thread) { + threads.add(thread); + } + + public void register(ExecutorService service) { + services.add(service); + } + + @Override + public void registerCache(MeteredCache cache) { + caches.add(new WeakReference<>(cache)); + } + + @Override + public void dereference() { + IrisData.dereference(); + threads.removeIf((Thread thread) -> !thread.isAlive()); + services.removeIf(ExecutorService::isShutdown); + caches.removeIf((WeakReference ref) -> { + MeteredCache cache = ref.get(); + return cache == null || cache.isClosed(); + }); + } + + @Override + public void onEnable() { + if (!running.compareAndSet(false, true)) { + return; + } + dereferencer = new Thread(this::loop, "iris-modded-preservation"); + dereferencer.setDaemon(true); + dereferencer.start(); + } + + @Override + public void onDisable() { + if (!running.compareAndSet(true, false)) { + return; + } + if (dereferencer != null) { + dereferencer.interrupt(); + dereferencer = null; + } + dereference(); + shutdownTrackedResources(); + } + + private void loop() { + while (running.get()) { + try { + Thread.sleep(DEREFERENCE_INTERVAL_MILLIS); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + return; + } + dereference(); + } + } + + private void shutdownTrackedResources() { + for (Thread thread : threads) { + if (!thread.isAlive()) { + continue; + } + try { + thread.interrupt(); + LOGGER.info("Iris preservation interrupted thread {}", thread.getName()); + } catch (Throwable error) { + LOGGER.error("Iris preservation failed to interrupt thread {}", thread.getName(), error); + } + } + for (ExecutorService service : services) { + try { + service.shutdownNow(); + LOGGER.info("Iris preservation shut down executor {}", service); + } catch (Throwable error) { + LOGGER.error("Iris preservation failed to shut down executor {}", service, error); + } + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedService.java new file mode 100644 index 000000000..06b92d5d1 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedService.java @@ -0,0 +1,25 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.service; + +public interface ModdedService { + void onEnable(); + + void onDisable(); +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedSettingsHotloadService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedSettingsHotloadService.java new file mode 100644 index 000000000..87a5747a8 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedSettingsHotloadService.java @@ -0,0 +1,141 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.service; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.hotload.ConfigHotloadEngine; +import net.minecraft.server.MinecraftServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public final class ModdedSettingsHotloadService implements ModdedTickableService { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final long POLL_PERIOD_MILLIS = 500L; + private static final long HOTLOAD_COOLDOWN_MILLIS = 3_000L; + private static final int MAX_SETTINGS_BYTES = 2 * 1024 * 1024; + + private ConfigHotloadEngine hotloadEngine; + private long lastPollAtNanos; + + @Override + public void onEnable() { + File settingsFile = settingsFile(); + hotloadEngine = new ConfigHotloadEngine( + this::isSettingsFile, + () -> List.of(settingsFile), + this::readSettings, + this::normalizeSettings + ); + hotloadEngine.configure( + POLL_PERIOD_MILLIS, + HOTLOAD_COOLDOWN_MILLIS, + List.of(settingsFile), + List.of() + ); + lastPollAtNanos = 0L; + } + + @Override + public void onDisable() { + ConfigHotloadEngine active = hotloadEngine; + hotloadEngine = null; + if (active != null) { + active.clear(); + } + } + + @Override + public void onServerTick(MinecraftServer server) { + ConfigHotloadEngine active = hotloadEngine; + if (active == null) { + return; + } + + long now = System.nanoTime(); + if (now - lastPollAtNanos < TimeUnit.MILLISECONDS.toNanos(POLL_PERIOD_MILLIS)) { + return; + } + lastPollAtNanos = now; + + try { + for (ConfigHotloadEngine.StableContentSnapshot snapshot : active.pollTouchedSnapshots()) { + if ("missing".equals(snapshot.signature())) { + active.processSnapshotChange(snapshot, ignored -> true, null); + LOGGER.warn("settings.json was removed; retaining the last valid runtime settings"); + continue; + } + active.processSnapshotChange(snapshot, this::reloadSettings, ignored -> LOGGER.info("Hotloaded settings.json")); + } + IrisLanguage.update(); + } catch (RuntimeException failure) { + LOGGER.error("Iris settings hotload watcher failed", failure); + } + } + + private boolean reloadSettings(ConfigHotloadEngine.StableContentSnapshot snapshot) { + try { + String content = snapshot.normalizedContent(); + if (content == null) { + return false; + } + return IrisSettings.applyHotloadSnapshot(content, IrisLanguage::reload); + } catch (RuntimeException failure) { + LOGGER.error("Iris settings hotload failed; keeping the previous runtime settings", failure); + return false; + } + } + + private boolean isSettingsFile(File file) { + return file != null && settingsFile().getAbsoluteFile().equals(file.getAbsoluteFile()); + } + + private String readSettings(File file) { + if (file == null || !file.isFile()) { + return null; + } + try (InputStream input = Files.newInputStream(file.toPath())) { + byte[] content = input.readNBytes(MAX_SETTINGS_BYTES + 1); + if (content.length > MAX_SETTINGS_BYTES) { + throw new IOException("Settings exceed " + MAX_SETTINGS_BYTES + " bytes: " + file); + } + return new String(content, StandardCharsets.UTF_8); + } catch (IOException failure) { + throw new UncheckedIOException("Failed to read Iris settings from " + file, failure); + } + } + + private String normalizeSettings(String content) { + return content == null ? null : content.replace("\r\n", "\n").trim(); + } + + private static File settingsFile() { + return IrisPlatforms.get().dataFile("settings.json"); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java new file mode 100644 index 000000000..3d0676d4c --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java @@ -0,0 +1,297 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.service; + +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.tools.WorldMaintenance; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EnginePlatformHooks; +import art.arcane.iris.engine.framework.NativeStructureVolume; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionRuntimeContract; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedDimensionManager; +import art.arcane.iris.modded.ModdedForcedDatapack; +import art.arcane.iris.modded.ModdedWorkspaceGenerator; +import art.arcane.iris.nativegen.NativeStructureVolumeIndex; +import art.arcane.iris.modded.command.ModdedPregenJob; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.io.ReactiveFolder; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import net.minecraft.server.MinecraftServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class ModdedStudioHotloadService implements ModdedTickableService, EnginePlatformHooks { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final String STUDIO_DIMENSION_PREFIX = "irisworldgen:studio_"; + private static final long POLL_MILLIS = 250L; + private static final long CHECK_LATCH_MILLIS = 1_000L; + private static final long RECENT_GENERATION_HOLDOFF_MILLIS = 2_000L; + + private final ConcurrentHashMap watches = new ConcurrentHashMap<>(); + private volatile ExecutorService executor; + private long lastPollAt; + + @Override + public void onEnable() { + if (executor != null) { + return; + } + executor = Executors.newSingleThreadExecutor((Runnable task) -> { + Thread thread = new Thread(task, "Iris Studio Hotload"); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + lastPollAt = 0L; + } + + @Override + public void onDisable() { + ExecutorService active = executor; + executor = null; + if (active != null) { + active.shutdownNow(); + } + for (Watch watch : watches.values()) { + watch.close(); + } + watches.clear(); + } + + @Override + public void refreshWorkspace(Engine engine) { + writeWorkspace(engine, "workspace refresh"); + } + + @Override + public KList nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) { + return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ); + } + + @Override + public void refreshDatapackWorkspace(Engine engine) { + IrisData data = engine.getData(); + KList dimensions = data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys()); + if (hasDatapackImports(dimensions)) { + writeWorkspace(engine, "datapack workspace refresh"); + } + } + + @Override + public void reloadDatapacks(Engine engine) { + ModdedForcedDatapack.regenerate(); + } + + @Override + public void validateDimensionHotload(Engine engine, IrisDimension replacement) { + IrisDimensionRuntimeContract.requireHotloadCompatible( + "Modded Studio level '" + engine.getWorld().identity() + "'", + engine.getDimension(), + replacement, + "irisworldgen"); + } + + @Override + public void onServerTick(MinecraftServer server) { + ExecutorService active = executor; + if (active == null) { + return; + } + long now = System.currentTimeMillis(); + if (now - lastPollAt < POLL_MILLIS) { + return; + } + lastPollAt = now; + List handles = ModdedDimensionManager.handles(); + Set seen = null; + for (ModdedDimensionManager.Handle handle : handles) { + String dimensionId = handle.dimensionId(); + if (!dimensionId.startsWith(STUDIO_DIMENSION_PREFIX)) { + continue; + } + IrisModdedChunkGenerator generator = handle.generator(); + if (generator == null) { + continue; + } + Engine engine = generator.engineIfBound(); + if (engine == null || engine.isClosed()) { + continue; + } + if (seen == null) { + seen = new HashSet<>(); + } + seen.add(dimensionId); + Watch watch = watches.computeIfAbsent(dimensionId, (String key) -> new Watch()); + if (throttled(generator, engine, now)) { + continue; + } + if (!watch.latch.flip()) { + continue; + } + if (!watch.busy.compareAndSet(false, true)) { + continue; + } + try { + active.execute(() -> { + try { + poll(dimensionId, watch, generator, engine); + } finally { + watch.busy.set(false); + } + }); + } catch (RejectedExecutionException rejected) { + watch.busy.set(false); + } + } + if (!watches.isEmpty()) { + Set keep = seen == null ? Set.of() : seen; + watches.entrySet().removeIf((Map.Entry entry) -> { + if (keep.contains(entry.getKey())) { + return false; + } + entry.getValue().close(); + return true; + }); + } + } + + @Override + public boolean isPregeneratorActive(Engine engine) { + IrisWorld world = engine.getWorld(); + if (world == null) { + return false; + } + PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); + return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(world.identity()); + } + + @Override + public void shutdownPregenerator(Engine engine) { + IrisWorld world = engine.getWorld(); + if (world != null) { + ModdedPregenJob.shutdownForWorld(world.identity()); + } + } + + @Override + public boolean shouldSkipMantleCleanup(Engine engine) { + IrisWorld world = engine.getWorld(); + return world != null + && WorldMaintenance.isWorldMaintenanceActive(world.identity()) + && !isPregeneratorActive(engine); + } + + private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) { + if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) { + return true; + } + return isPregeneratorActive(engine); + } + + private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) { + try { + if (watch.engine != engine) { + watch.close(); + watch.engine = engine; + watch.folder = new ReactiveFolder( + engine.getData().getDataFolder(), + (KList created, KList changed, KList deleted) -> hotload(dimensionId, generator, engine), + new KList<>(".iob", ".json"), + new KList<>(".iris"), + new KList<>()); + return; + } + ReactiveFolder folder = watch.folder; + if (folder != null) { + folder.check(); + } + } catch (Throwable e) { + LOGGER.error("Iris studio hotload check failed for {}", dimensionId, e); + } + } + + private void hotload(String dimensionId, IrisModdedChunkGenerator generator, Engine engine) { + if (engine.isClosed()) { + return; + } + long start = System.currentTimeMillis(); + try { + engine.hotloadSilently(); + generator.onHotload(); + LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start); + } catch (Throwable e) { + LOGGER.error("Iris studio hotload failed for {}", dimensionId, e); + throw new IllegalStateException("Iris studio hotload failed for " + dimensionId, e); + } + } + + static boolean hasDatapackImports(Iterable dimensions) { + if (dimensions == null) { + return false; + } + for (IrisDimension dimension : dimensions) { + if (dimension != null + && dimension.getDatapackImports() != null + && !dimension.getDatapackImports().isEmpty()) { + return true; + } + } + return false; + } + + private void writeWorkspace(Engine engine, String operation) { + try { + IrisData data = engine.getData(); + ModdedWorkspaceGenerator.writeWorkspace(data, data.getDataFolder()); + } catch (Throwable e) { + LOGGER.error("Iris {} failed for {}", operation, engine.getDimension().getLoadKey(), e); + } + } + + private static final class Watch { + private final ChronoLatch latch = new ChronoLatch(CHECK_LATCH_MILLIS, false); + private final AtomicBoolean busy = new AtomicBoolean(false); + private volatile Engine engine; + private volatile ReactiveFolder folder; + + private void close() { + ReactiveFolder active = folder; + folder = null; + if (active != null) { + active.clear(); + } + engine = null; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTickableService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTickableService.java new file mode 100644 index 000000000..b086e4fbd --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTickableService.java @@ -0,0 +1,25 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.service; + +import net.minecraft.server.MinecraftServer; + +public interface ModdedTickableService extends ModdedService { + void onServerTick(MinecraftServer server); +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTreeFellerPresentation.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTreeFellerPresentation.java new file mode 100644 index 000000000..3fe5cb669 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTreeFellerPresentation.java @@ -0,0 +1,227 @@ +package art.arcane.iris.modded.service; + +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedScheduler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.BlockState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +final class ModdedTreeFellerPresentation { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int MIN_BLOCKS_PER_PULSE = 4; + private static final int MAX_BLOCKS_PER_PULSE = 64; + private static final int TARGET_EROSION_PULSES = 60; + private static final int MAX_EFFECT_ORIGINS_PER_PULSE = 16; + + private final ServerPlayer player; + private final ServerLevel sourceLevel; + private final List pendingDrops = new ArrayList<>(); + private final AtomicBoolean effectFailureReported = new AtomicBoolean(); + private final AtomicBoolean deliveryFailureReported = new AtomicBoolean(); + private boolean flushScheduled; + private double fallbackX; + private double fallbackY; + private double fallbackZ; + + ModdedTreeFellerPresentation(ServerPlayer player, ServerLevel sourceLevel) { + this.player = player; + this.sourceLevel = sourceLevel; + this.fallbackX = player.getX(); + this.fallbackY = player.getY() + 0.15D; + this.fallbackZ = player.getZ(); + } + + static int blocksPerPulse(int blockCount) { + int requested = Math.max(1, (blockCount + TARGET_EROSION_PULSES - 1) / TARGET_EROSION_PULSES); + return Math.max(MIN_BLOCKS_PER_PULSE, Math.min(requested, MAX_BLOCKS_PER_PULSE)); + } + + static int effectStride(int blocksPerPulse) { + return Math.max( + 1, + (blocksPerPulse + MAX_EFFECT_ORIGINS_PER_PULSE - 1) / MAX_EFFECT_ORIGINS_PER_PULSE + ); + } + + static List consolidateDrops(Collection drops) { + List consolidated = new ArrayList<>(); + for (ItemStack drop : drops) { + mergeDrop(consolidated, drop); + } + return List.copyOf(consolidated); + } + + void activate(BlockPos position, BlockState state) { + try { + double x = position.getX() + 0.5D; + double y = position.getY() + 0.5D; + double z = position.getZ() + 0.5D; + sourceLevel.sendParticles(ParticleTypes.ENCHANT, x, y, z, 24, 0.45D, 0.45D, 0.45D, 0.18D); + sourceLevel.sendParticles(ParticleTypes.END_ROD, x, y, z, 8, 0.25D, 0.25D, 0.25D, 0.035D); + sourceLevel.sendParticles( + new BlockParticleOption(ParticleTypes.BLOCK, state), + x, + y, + z, + 8, + 0.25D, + 0.25D, + 0.25D, + 0.04D + ); + sourceLevel.playSound(null, position, SoundEvents.ENCHANTMENT_TABLE_USE, SoundSource.PLAYERS, 0.55F, 1.35F); + sourceLevel.playSound(null, position, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.4F, 0.8F); + } catch (Throwable error) { + reportEffectFailure(error); + } + } + + void erode(BlockPos position, BlockState state, int processed, int effectStride, float pitch) { + if (processed % effectStride != 0) { + return; + } + try { + double x = position.getX() + 0.5D; + double y = position.getY() + 0.5D; + double z = position.getZ() + 0.5D; + sourceLevel.sendParticles( + new BlockParticleOption(ParticleTypes.BLOCK, state), + x, + y, + z, + 5, + 0.3D, + 0.3D, + 0.3D, + 0.04D + ); + sourceLevel.sendParticles(ParticleTypes.ENCHANT, x, y, z, 3, 0.28D, 0.28D, 0.28D, 0.12D); + sourceLevel.playSound(null, position, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.22F, pitch); + } catch (Throwable error) { + reportEffectFailure(error); + } + } + + synchronized boolean route(Iterable drops) { + for (ItemStack drop : drops) { + if (drop != null && !drop.isEmpty()) { + pendingDrops.add(drop.copy()); + } + } + scheduleFlush(); + return true; + } + + synchronized void flush() { + flushScheduled = false; + if (pendingDrops.isEmpty()) { + return; + } + List drops = consolidateDrops(pendingDrops); + pendingDrops.clear(); + boolean atPlayer = !player.isRemoved() && player.level() == sourceLevel; + double x = atPlayer ? player.getX() : fallbackX; + double y = atPlayer ? player.getY() + 0.15D : fallbackY; + double z = atPlayer ? player.getZ() : fallbackZ; + if (atPlayer) { + fallbackX = x; + fallbackY = y; + fallbackZ = z; + } + int delivered = 0; + for (ItemStack drop : drops) { + try { + ItemEntity item = new ItemEntity(sourceLevel, x, y, z, drop, 0D, 0.08D, 0D); + item.setDefaultPickUpDelay(); + if (sourceLevel.addFreshEntity(item)) { + delivered++; + } else { + pendingDrops.add(drop.copy()); + } + } catch (Throwable error) { + pendingDrops.add(drop.copy()); + reportDeliveryFailure(error); + } + } + try { + int particles = Math.min(32, 6 + (delivered * 2)); + sourceLevel.sendParticles(ParticleTypes.ENCHANT, x, y + 0.35D, z, particles, 0.3D, 0.25D, 0.3D, 0.1D); + sourceLevel.playSound(null, x, y, z, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.28F, 1.75F); + } catch (Throwable error) { + reportEffectFailure(error); + } + if (!pendingDrops.isEmpty()) { + scheduleFlush(); + } + } + + synchronized void finish() { + flush(); + } + + private synchronized void scheduleFlush() { + if (pendingDrops.isEmpty() || flushScheduled) { + return; + } + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + flush(); + return; + } + flushScheduled = true; + scheduler.laterGlobal(this::flush, 1); + } + + private static void mergeDrop(List consolidated, ItemStack drop) { + if (drop == null || drop.isEmpty()) { + return; + } + ItemStack remaining = drop.copy(); + for (ItemStack existing : consolidated) { + if (!ItemStack.isSameItemSameComponents(existing, remaining)) { + continue; + } + int capacity = existing.getMaxStackSize() - existing.getCount(); + if (capacity <= 0) { + continue; + } + int moved = Math.min(capacity, remaining.getCount()); + existing.grow(moved); + remaining.shrink(moved); + if (remaining.isEmpty()) { + return; + } + } + while (!remaining.isEmpty()) { + int amount = Math.min(remaining.getCount(), remaining.getMaxStackSize()); + consolidated.add(remaining.copyWithCount(amount)); + remaining.shrink(amount); + } + } + + private void reportEffectFailure(Throwable error) { + if (effectFailureReported.compareAndSet(false, true)) { + LOGGER.error("Iris modded tree-feller presentation failed", error); + } + } + + private void reportDeliveryFailure(Throwable error) { + if (deliveryFailureReported.compareAndSet(false, true)) { + LOGGER.error("Iris modded tree-feller drop delivery failed", error); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTreeFellerService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTreeFellerService.java new file mode 100644 index 000000000..00ef52ec7 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedTreeFellerService.java @@ -0,0 +1,554 @@ +package art.arcane.iris.modded.service; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.service.tree.TreeDefinitionIndex; +import art.arcane.iris.core.service.tree.TreeMarkerTraversal; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.StructurePlacementMarker; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.modded.ModdedBlockBreakHandler; +import art.arcane.iris.modded.ModdedBlockState; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedScheduler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.ItemTags; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +public final class ModdedTreeFellerService implements ModdedTickableService { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final ThreadLocal BREAK_PROBE_DEPTH = ThreadLocal.withInitial(() -> 0); + + private final AtomicBoolean enabled = new AtomicBoolean(); + private final Set activeClaims = ConcurrentHashMap.newKeySet(); + private final Set activeRuns = ConcurrentHashMap.newKeySet(); + private final Map definitions = Collections.synchronizedMap(new WeakHashMap<>()); + + public static boolean isBreakProbe() { + return BREAK_PROBE_DEPTH.get() > 0; + } + + public static boolean runBreakProbe(BooleanSupplier probe) { + int depth = BREAK_PROBE_DEPTH.get(); + BREAK_PROBE_DEPTH.set(depth + 1); + try { + return probe.getAsBoolean(); + } finally { + if (depth == 0) { + BREAK_PROBE_DEPTH.remove(); + } else { + BREAK_PROBE_DEPTH.set(depth); + } + } + } + + @Override + public void onEnable() { + enabled.set(true); + } + + @Override + public void onDisable() { + enabled.set(false); + for (FellingRun run : List.copyOf(activeRuns)) { + finish(run); + } + activeRuns.clear(); + activeClaims.clear(); + definitions.clear(); + } + + @Override + public void onServerTick(MinecraftServer server) { + for (FellingRun run : List.copyOf(activeRuns)) { + if (!isRunControlActive(run)) { + finish(run); + } + } + } + + public PreparedOrigin prepare( + ServerLevel level, + ServerPlayer player, + BlockPos position, + BlockState state + ) { + IrisSettings.IrisSettingsTreeFeller settings = IrisSettings.get().getTreeFeller(); + if (!enabled.get() + || settings == null + || !settings.isEnabled() + || !player.gameMode().isSurvival() + || !player.isShiftKeyDown() + || !state.is(BlockTags.LOGS) + || !player.getInventory().getSelectedItem().is(ItemTags.AXES) + || !ModdedEngineBootstrap.loader().hasTreeFellerPermission(player)) { + return null; + } + Engine engine = ModdedBlockBreakHandler.engineFor(level); + if (engine == null || engine.isClosed()) { + return null; + } + int minimumY = level.getMinY(); + String marker = markerAt(engine, minimumY, position); + StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker); + if (decoded == null || decoded.structureAware()) { + return null; + } + TreeBlockMaterial expectedMaterial = materialAt(engine, minimumY, position); + if (expectedMaterial != null && !expectedMaterial.matches(ModdedBlockState.serialize(state))) { + return null; + } + if (expectedMaterial == null + && !decoded.objectKey().startsWith("trees/") + && !definitionIndex(engine).isTreeMarker(marker)) { + return null; + } + ItemStack tool = player.getInventory().getSelectedItem(); + return new PreparedOrigin( + this, + level, + player, + position.immutable(), + state, + engine, + marker, + minimumY, + level.getMaxY(), + player.getInventory().getSelectedSlot(), + tool.copy(), + settings.getDurabilityPreservationChance() + ); + } + + public OriginDropRoute completeOrigin(PreparedOrigin prepared) { + if (prepared == null + || prepared.owner() != this + || !enabled.get() + || prepared.engine().isClosed()) { + return null; + } + TreeClaim claim = new TreeClaim(prepared.level(), prepared.marker()); + if (!activeClaims.add(claim)) { + return null; + } + ModdedTreeFellerPresentation presentation = new ModdedTreeFellerPresentation( + prepared.player(), + prepared.level() + ); + FellingRun run = new FellingRun(claim, prepared, presentation); + if (!normalizeOriginTool(run)) { + activeClaims.remove(claim); + return null; + } + activeRuns.add(run); + presentation.activate(prepared.position(), prepared.state()); + OriginDropRoute route = presentation::route; + if (run.toolBroken) { + ModdedBlockBreakHandler.completeManagedBreak(prepared.level(), prepared.position()); + finish(run); + return route; + } + discover(run); + return route; + } + + private TreeDefinitionIndex definitionIndex(Engine engine) { + synchronized (definitions) { + return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build); + } + } + + private boolean normalizeOriginTool(FellingRun run) { + PreparedOrigin prepared = run.prepared; + ItemStack before = prepared.toolBefore(); + ItemStack current = prepared.player().getInventory().getItem(prepared.heldSlot()); + boolean currentMatches = !current.isEmpty() + && ItemStack.matchesIgnoringComponents( + before, + current, + (componentType) -> componentType == DataComponents.DAMAGE + ); + if (!before.isDamageableItem()) { + if (!currentMatches || !ItemStack.isSameItemSameComponents(before, current)) { + return false; + } + run.expectedTool = current.copy(); + return true; + } + + boolean preserve = ThreadLocalRandom.current().nextInt(100) < prepared.preservationChance(); + int desiredDamage = before.getDamageValue() + (preserve ? 0 : 1); + if (desiredDamage >= before.getMaxDamage()) { + if (!current.isEmpty() && !currentMatches) { + return false; + } + Item brokenItem = before.getItem(); + prepared.player().getInventory().setItem(prepared.heldSlot(), ItemStack.EMPTY); + if (!current.isEmpty()) { + prepared.player().onEquippedItemBroken(brokenItem, EquipmentSlot.MAINHAND); + } + prepared.player().inventoryMenu.sendAllDataToRemote(); + run.expectedTool = ItemStack.EMPTY; + run.toolBroken = true; + return true; + } + if (!current.isEmpty() && !currentMatches) { + return false; + } + ItemStack normalized = before.copy(); + normalized.setDamageValue(desiredDamage); + prepared.player().getInventory().setItem(prepared.heldSlot(), normalized); + prepared.player().inventoryMenu.sendAllDataToRemote(); + run.expectedTool = normalized.copy(); + return true; + } + + private void discover(FellingRun run) { + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + ModdedBlockBreakHandler.completeManagedBreak(run.prepared.level(), run.prepared.position()); + finish(run); + return; + } + scheduler.async(() -> { + TreeMarkerTraversal.Discovery discovery; + try { + PreparedOrigin prepared = run.prepared; + TreeMarkerTraversal.Position trigger = positionOf(prepared.position()); + discovery = TreeMarkerTraversal.discover( + trigger, + prepared.marker(), + prepared.minimumY(), + prepared.maximumY(), + (x, y, z) -> markerAt(prepared.engine(), prepared.minimumY(), x, y, z) + ); + } catch (Throwable error) { + LOGGER.error("Iris modded tree-feller discovery failed", error); + discovery = new TreeMarkerTraversal.Discovery(List.of(), false); + } + TreeMarkerTraversal.Discovery resolved = discovery; + scheduler.global(() -> beginErosion(run, resolved)); + }); + } + + private void beginErosion(FellingRun run, TreeMarkerTraversal.Discovery discovery) { + ModdedBlockBreakHandler.completeManagedBreak(run.prepared.level(), run.prepared.position()); + if (run.finished.get() || !discovery.complete()) { + finish(run); + return; + } + TreeMarkerTraversal.Position trigger = positionOf(run.prepared.position()); + run.work = discovery.members().stream() + .filter((position) -> !position.equals(trigger)) + .toList(); + if (run.work.isEmpty()) { + finish(run); + return; + } + run.blocksPerPulse = ModdedTreeFellerPresentation.blocksPerPulse(run.work.size()); + run.effectStride = ModdedTreeFellerPresentation.effectStride(run.blocksPerPulse); + scheduleNextPulse(run); + } + + private void scheduleNextPulse(FellingRun run) { + ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); + if (scheduler == null) { + finish(run); + return; + } + scheduler.laterGlobal(() -> processPulse(run), 1); + } + + private void processPulse(FellingRun run) { + if (run.finished.get() || !isRunControlActive(run)) { + finish(run); + return; + } + int processedThisPulse = 0; + while (processedThisPulse < run.blocksPerPulse && run.cursor < run.work.size()) { + if (!isRunControlActive(run)) { + finish(run); + return; + } + TreeMarkerTraversal.Position position = run.work.get(run.cursor++); + MemberResult result = processMember(run, position); + if (result == MemberResult.STOP) { + finish(run); + return; + } + processedThisPulse++; + } + run.presentation.flush(); + if (run.cursor >= run.work.size()) { + finish(run); + return; + } + scheduleNextPulse(run); + } + + private MemberResult processMember(FellingRun run, TreeMarkerTraversal.Position position) { + PreparedOrigin prepared = run.prepared; + BlockPos blockPosition = blockPosition(position); + ServerLevel level = prepared.level(); + if (!level.isLoaded(blockPosition)) { + return MemberResult.STOP; + } + BlockState state = level.getBlockState(blockPosition); + if (state.isAir()) { + ModdedBlockBreakHandler.completeManagedBreak(level, blockPosition); + return MemberResult.CONTINUE; + } + if (!prepared.marker().equals(markerAt(prepared.engine(), prepared.minimumY(), blockPosition))) { + return MemberResult.STOP; + } + TreeBlockMaterial expectedMaterial = materialAt(prepared.engine(), prepared.minimumY(), blockPosition); + if (expectedMaterial != null && !expectedMaterial.matches(ModdedBlockState.serialize(state))) { + ModdedBlockBreakHandler.completeManagedBreak(level, blockPosition); + return state.is(BlockTags.LOGS) ? MemberResult.STOP : MemberResult.CONTINUE; + } + boolean log = state.is(BlockTags.LOGS); + ServerPlayer player = prepared.player(); + if (!level.mayInteract(player, blockPosition) + || player.blockActionRestricted(level, blockPosition, player.gameMode()) + || !player.getInventory().getSelectedItem().canDestroyBlock( + state, + level, + blockPosition, + player + )) { + return log ? MemberResult.STOP : MemberResult.CONTINUE; + } + if (!ModdedEngineBootstrap.loader().canTreeFellerBreak( + level, + player, + blockPosition, + state + )) { + return log ? MemberResult.STOP : MemberResult.CONTINUE; + } + if (!state.equals(level.getBlockState(blockPosition)) + || !prepared.marker().equals(markerAt(prepared.engine(), prepared.minimumY(), blockPosition))) { + return MemberResult.STOP; + } + + ItemStack toolForDrops = prepared.player().getInventory().getSelectedItem().copy(); + BlockEntity blockEntity = state.hasBlockEntity() ? level.getBlockEntity(blockPosition) : null; + List vanillaDrops = Block.getDrops( + state, + level, + blockPosition, + blockEntity, + player, + toolForDrops + ); + ModdedBlockBreakHandler.Result customDrops = ModdedBlockBreakHandler.evaluateManagedDrops( + level, + blockPosition, + state + ); + ToolReservation reservation = log ? reserveToolDamage(run) : ToolReservation.free(toolForDrops); + if (reservation == null) { + return MemberResult.STOP; + } + if (!level.destroyBlock(blockPosition, false, player, 512)) { + refundToolDamage(run, reservation); + return MemberResult.STOP; + } + ModdedBlockBreakHandler.completeManagedBreak(level, blockPosition); + int processed = run.processed++; + float progress = run.work.size() <= 1 ? 1F : (float) processed / (float) (run.work.size() - 1); + float pitch = Math.min(1.95F, 0.65F + (progress * 1.25F)); + run.presentation.erode(blockPosition, state, processed, run.effectStride, pitch); + run.presentation.route(customDrops.combinedDrops(vanillaDrops)); + return reservation.broke() ? MemberResult.STOP : MemberResult.CONTINUE; + } + + private ToolReservation reserveToolDamage(FellingRun run) { + PreparedOrigin prepared = run.prepared; + ItemStack current = prepared.player().getInventory().getSelectedItem(); + if (!ItemStack.isSameItemSameComponents(current, run.expectedTool) || !current.is(ItemTags.AXES)) { + return null; + } + ItemStack before = current.copy(); + if (!current.isDamageableItem() + || ThreadLocalRandom.current().nextInt(100) < prepared.preservationChance()) { + return ToolReservation.free(before); + } + int nextDamage = current.getDamageValue() + 1; + if (nextDamage >= current.getMaxDamage()) { + Item brokenItem = current.getItem(); + prepared.player().getInventory().setSelectedItem(ItemStack.EMPTY); + prepared.player().onEquippedItemBroken(brokenItem, EquipmentSlot.MAINHAND); + prepared.player().inventoryMenu.sendAllDataToRemote(); + run.expectedTool = ItemStack.EMPTY; + return new ToolReservation(before, true, true); + } + current.setDamageValue(nextDamage); + prepared.player().getInventory().setSelectedItem(current); + prepared.player().inventoryMenu.sendAllDataToRemote(); + run.expectedTool = current.copy(); + return new ToolReservation(before, true, false); + } + + private void refundToolDamage(FellingRun run, ToolReservation reservation) { + if (!reservation.charged()) { + return; + } + PreparedOrigin prepared = run.prepared; + ItemStack current = prepared.player().getInventory().getSelectedItem(); + boolean expectedEmpty = run.expectedTool.isEmpty(); + if ((expectedEmpty && current.isEmpty()) + || (!expectedEmpty && ItemStack.isSameItemSameComponents(current, run.expectedTool))) { + ItemStack restored = reservation.before().copy(); + prepared.player().getInventory().setSelectedItem(restored); + prepared.player().inventoryMenu.sendAllDataToRemote(); + run.expectedTool = restored.copy(); + } + } + + private boolean isRunControlActive(FellingRun run) { + PreparedOrigin prepared = run.prepared; + ServerPlayer player = prepared.player(); + IrisSettings.IrisSettingsTreeFeller settings = IrisSettings.get().getTreeFeller(); + if (!enabled.get() + || settings == null + || !settings.isEnabled() + || run.finished.get() + || player.isRemoved() + || player.hasDisconnected() + || player.level() != prepared.level() + || !player.gameMode().isSurvival() + || !player.isShiftKeyDown() + || player.getInventory().getSelectedSlot() != prepared.heldSlot() + || run.expectedTool.isEmpty()) { + return false; + } + ItemStack current = player.getInventory().getSelectedItem(); + return current.is(ItemTags.AXES) && ItemStack.isSameItemSameComponents(current, run.expectedTool); + } + + private void finish(FellingRun run) { + if (run.finished.compareAndSet(false, true)) { + activeRuns.remove(run); + activeClaims.remove(run.claim); + ModdedBlockBreakHandler.completeManagedBreak(run.prepared.level(), run.prepared.position()); + run.presentation.finish(); + } + } + + private String markerAt(Engine engine, int minimumY, BlockPos position) { + return markerAt(engine, minimumY, position.getX(), position.getY(), position.getZ()); + } + + private String markerAt(Engine engine, int minimumY, int x, int y, int z) { + return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class); + } + + private TreeBlockMaterial materialAt(Engine engine, int minimumY, BlockPos position) { + return engine.getMantle().getMantle().get( + position.getX(), + position.getY() - minimumY, + position.getZ(), + TreeBlockMaterial.class + ); + } + + private TreeMarkerTraversal.Position positionOf(BlockPos position) { + return new TreeMarkerTraversal.Position(position.getX(), position.getY(), position.getZ()); + } + + private BlockPos blockPosition(TreeMarkerTraversal.Position position) { + return new BlockPos(position.x(), position.y(), position.z()); + } + + public record PreparedOrigin( + ModdedTreeFellerService owner, + ServerLevel level, + ServerPlayer player, + BlockPos position, + BlockState state, + Engine engine, + String marker, + int minimumY, + int maximumY, + int heldSlot, + ItemStack toolBefore, + int preservationChance + ) { + public PreparedOrigin { + Objects.requireNonNull(owner, "owner"); + Objects.requireNonNull(level, "level"); + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(position, "position"); + Objects.requireNonNull(state, "state"); + Objects.requireNonNull(engine, "engine"); + Objects.requireNonNull(marker, "marker"); + Objects.requireNonNull(toolBefore, "toolBefore"); + } + } + + @FunctionalInterface + public interface OriginDropRoute { + boolean route(Iterable drops); + } + + private enum MemberResult { + CONTINUE, + STOP + } + + private record TreeClaim(ServerLevel level, String marker) { + } + + private record ToolReservation(ItemStack before, boolean charged, boolean broke) { + private static ToolReservation free(ItemStack before) { + return new ToolReservation(before, false, false); + } + } + + private static final class FellingRun { + private final TreeClaim claim; + private final PreparedOrigin prepared; + private final ModdedTreeFellerPresentation presentation; + private final AtomicBoolean finished = new AtomicBoolean(); + private ItemStack expectedTool = ItemStack.EMPTY; + private List work = List.of(); + private int cursor; + private int processed; + private int blocksPerPulse = 1; + private int effectStride = 1; + private boolean toolBroken; + + private FellingRun( + TreeClaim claim, + PreparedOrigin prepared, + ModdedTreeFellerPresentation presentation + ) { + this.claim = claim; + this.prepared = prepared; + this.presentation = presentation; + } + } +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/icon.png b/adapters/modded-common/src/main/resources/assets/irisworldgen/icon.png new file mode 100644 index 000000000..aadf45975 Binary files /dev/null and b/adapters/modded-common/src/main/resources/assets/irisworldgen/icon.png differ diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/de_de.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/de_de.json new file mode 100644 index 000000000..a0d6d7ddf --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/de_de.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Vorgenerierungs-HUD umschalten", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/en_us.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/en_us.json new file mode 100644 index 000000000..59a31d305 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/en_us.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Toggle Pregen HUD", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/es_es.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/es_es.json new file mode 100644 index 000000000..c436c7fd4 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/es_es.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Alternar HUD de pregeneración", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/fi_fi.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/fi_fi.json new file mode 100644 index 000000000..b468c26e9 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/fi_fi.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Vaihda esigeneroinnin HUD", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/fr_fr.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/fr_fr.json new file mode 100644 index 000000000..ba54dc964 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/fr_fr.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Activer ou désactiver le HUD de prégénération", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/he_il.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/he_il.json new file mode 100644 index 000000000..542f08a02 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/he_il.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "הפעלה או כיבוי של תצוגת הקדם-יצירה", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/it_it.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/it_it.json new file mode 100644 index 000000000..7c5d4caa9 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/it_it.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Attiva o disattiva l'HUD di pregenerazione", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/ja_jp.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/ja_jp.json new file mode 100644 index 000000000..8efdde88c --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/ja_jp.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "事前生成 HUD の切り替え", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/ko_kr.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/ko_kr.json new file mode 100644 index 000000000..ae734e26b --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/ko_kr.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "사전 생성 HUD 전환", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/lt_lt.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/lt_lt.json new file mode 100644 index 000000000..741346668 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/lt_lt.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Perjungti išankstinio generavimo HUD", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/nl_nl.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/nl_nl.json new file mode 100644 index 000000000..f5ff6b361 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/nl_nl.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "HUD voor vooraf genereren in- of uitschakelen", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/pl_pl.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/pl_pl.json new file mode 100644 index 000000000..04cc94964 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/pl_pl.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Przełącz interfejs wstępnego generowania", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/pt_pt.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/pt_pt.json new file mode 100644 index 000000000..1ee6ca276 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/pt_pt.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Alternar HUD de pré-geração", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/ru_ru.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/ru_ru.json new file mode 100644 index 000000000..e5963a096 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/ru_ru.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Переключить HUD предварительной генерации", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/tr_tr.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/tr_tr.json new file mode 100644 index 000000000..bcb895cf3 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/tr_tr.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Ön oluşturma HUD'unu aç veya kapat", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/vi_vi.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/vi_vi.json new file mode 100644 index 000000000..919a5f8d9 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/vi_vi.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Bật hoặc tắt HUD tạo trước", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/zh_cn.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/zh_cn.json new file mode 100644 index 000000000..b0a7a16c3 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/zh_cn.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "切换预生成 HUD", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/zh_tw.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/zh_tw.json new file mode 100644 index 000000000..08194dbff --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/zh_tw.json @@ -0,0 +1,6 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "切換預先生成 HUD", + "key.irisworldgen.open_vision_map": "Open Iris Vision Map", + "key.irisworldgen.toggle_what_overlay": "Toggle Iris What Overlay" +} diff --git a/adapters/modded-common/src/main/resources/irisworldgen.client.mixins.json b/adapters/modded-common/src/main/resources/irisworldgen.client.mixins.json new file mode 100644 index 000000000..5140fdc9e --- /dev/null +++ b/adapters/modded-common/src/main/resources/irisworldgen.client.mixins.json @@ -0,0 +1,13 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "art.arcane.iris.client.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "IrisWorldOpenFlowsMixin", + "IrisWorldTypeEntryMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json b/adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json new file mode 100644 index 000000000..e7b5a28e2 --- /dev/null +++ b/adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "art.arcane.iris.modded.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "EntityPersistenceMixin", + "LivingEntityLootMixin", + "MobAwarenessMixin", + "StructureTemplatePaletteConcurrencyMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientCursorTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientCursorTest.java new file mode 100644 index 000000000..780d08012 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientCursorTest.java @@ -0,0 +1,53 @@ +package art.arcane.iris.client; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertEquals; + +public class IrisClientCursorTest { + @Test + public void refreshesAnUnchangedPositionAfterTheRefreshInterval() { + AtomicLong clock = new AtomicLong(500L); + AtomicInteger frames = new AtomicInteger(); + IrisClientCursor cursor = new IrisClientCursor(frame -> frames.incrementAndGet(), clock::get); + + cursor.requestFor(10, 20); + clock.addAndGet(1_999L); + cursor.requestFor(10, 20); + clock.incrementAndGet(); + cursor.requestFor(10, 20); + + assertEquals(2, frames.get()); + } + + @Test + public void throttlesRapidPositionChanges() { + AtomicLong clock = new AtomicLong(500L); + AtomicInteger frames = new AtomicInteger(); + IrisClientCursor cursor = new IrisClientCursor(frame -> frames.incrementAndGet(), clock::get); + + cursor.requestFor(10, 20); + clock.addAndGet(499L); + cursor.requestFor(11, 20); + clock.incrementAndGet(); + cursor.requestFor(11, 20); + + assertEquals(2, frames.get()); + } + + @Test + public void clearAllowsImmediateRequestAtTheSamePosition() { + AtomicLong clock = new AtomicLong(500L); + AtomicInteger frames = new AtomicInteger(); + IrisClientCursor cursor = new IrisClientCursor(frame -> frames.incrementAndGet(), clock::get); + + cursor.requestFor(10, 20); + cursor.clear(); + cursor.requestFor(10, 20); + + assertEquals(2, frames.get()); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientDimensionTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientDimensionTest.java new file mode 100644 index 000000000..bb51a28a6 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientDimensionTest.java @@ -0,0 +1,25 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisClientDimensionTest { + @Test + public void packSeedAndHeightChangesInvalidateDimensionCaches() { + IrisClientDimension dimension = new IrisClientDimension(); + IrisMessage.DimensionStatus initial = new IrisMessage.DimensionStatus( + "minecraft:overworld", "overworld", 1L, -64, 320, true); + + assertTrue(dimension.onDimensionStatus(initial)); + assertFalse(dimension.onDimensionStatus(initial)); + assertTrue(dimension.onDimensionStatus(new IrisMessage.DimensionStatus( + "minecraft:overworld", "other", 1L, -64, 320, true))); + assertTrue(dimension.onDimensionStatus(new IrisMessage.DimensionStatus( + "minecraft:overworld", "other", 2L, -64, 320, true))); + assertTrue(dimension.onDimensionStatus(new IrisMessage.DimensionStatus( + "minecraft:overworld", "other", 2L, 0, 256, true))); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientSessionTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientSessionTest.java new file mode 100644 index 000000000..d238a5c04 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientSessionTest.java @@ -0,0 +1,108 @@ +package art.arcane.iris.client; + +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisProtocol; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class IrisClientSessionTest { + @Test + public void retriesHelloAndEventuallyMarksUnsupported() { + AtomicLong clock = new AtomicLong(); + AtomicInteger frames = new AtomicInteger(); + IrisClientSession session = new IrisClientSession(clock::get); + session.bind(frame -> frames.incrementAndGet()); + + session.sendHello(); + assertEquals(1, frames.get()); + assertEquals(IrisClientSession.State.AWAITING_HELLO, session.state()); + + for (int attempt = 0; attempt < 5; attempt++) { + clock.addAndGet(2_000L); + session.tick(); + } + + assertEquals(5, frames.get()); + assertEquals(IrisClientSession.State.UNSUPPORTED, session.state()); + } + + @Test + public void matchingHelloCompletesRetryingSession() { + AtomicLong clock = new AtomicLong(); + IrisClientSession session = new IrisClientSession(clock::get); + session.bind(frame -> { + }); + session.sendHello(); + + session.onServerHello(new IrisMessage.ServerHello( + IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION, "Iris", true)); + + assertTrue(session.isReady()); + assertEquals("Iris", session.serverBrand()); + } + + @Test + public void incompatibleHelloIsRejected() { + IrisClientSession session = new IrisClientSession(); + session.onServerHello(new IrisMessage.ServerHello( + IrisProtocol.PROTOCOL_VERSION + 1, 0L, "Iris", true)); + + assertEquals(IrisClientSession.State.INCOMPATIBLE, session.state()); + assertEquals(IrisProtocol.PROTOCOL_VERSION + 1, session.serverProtocolVersion()); + assertEquals("Iris", session.serverBrand()); + } + + @Test + public void helloWithNoSinkStillRetriesAndResolvesToUnsupported() { + AtomicLong clock = new AtomicLong(); + IrisClientSession session = new IrisClientSession(clock::get); + + // No bind: the loader has not wired the channel yet. Without arming the retry clock on this path, + // nextHelloAt stayed at Long.MAX_VALUE, tick() returned immediately forever and the UI never left + // "connecting". + session.sendHello(); + assertEquals(IrisClientSession.State.AWAITING_HELLO, session.state()); + + for (int attempt = 0; attempt < 5; attempt++) { + clock.addAndGet(2_000L); + session.tick(); + } + + assertEquals(IrisClientSession.State.UNSUPPORTED, session.state()); + } + + @Test + public void sinkBoundLateStillCompletesTheHandshake() { + AtomicLong clock = new AtomicLong(); + AtomicInteger frames = new AtomicInteger(); + IrisClientSession session = new IrisClientSession(clock::get); + + session.sendHello(); + assertEquals(0, frames.get()); + + session.bind(frame -> frames.incrementAndGet()); + clock.addAndGet(2_000L); + session.tick(); + + assertEquals(1, frames.get()); + assertEquals(IrisClientSession.State.AWAITING_HELLO, session.state()); + } + + @Test + public void resetClearsTheServerVersion() { + IrisClientSession session = new IrisClientSession(); + session.onServerHello(new IrisMessage.ServerHello( + IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION, "Iris", true)); + assertTrue(session.isReady()); + + session.reset(); + + assertEquals(IrisClientSession.State.IDLE, session.state()); + assertEquals(0, session.serverProtocolVersion()); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientWireBoundsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientWireBoundsTest.java new file mode 100644 index 000000000..ed0af65d2 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisClientWireBoundsTest.java @@ -0,0 +1,160 @@ +package art.arcane.iris.client; + +import art.arcane.iris.core.protocol.IrisTileEncoder; +import art.arcane.iris.spi.protocol.IrisMessage; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Every structure here is filled from the wire, so its size is a dial the server holds. Caps mean a hostile or + * merely buggy server can waste bandwidth but not the client's heap. + */ +public class IrisClientWireBoundsTest { + @Test + public void markerTilesBeyondTheCapEvictTheOldest() { + IrisClientMarkers markers = new IrisClientMarkers(); + int overflow = IrisClientMarkers.MAX_TILES + 8; + for (int tileX = 0; tileX < overflow; tileX++) { + markers.onMarkers(new IrisMessage.VisionMarkers(tileX, 0, 0, + List.of(new IrisMessage.VisionMarkers.Marker(tileX, 0, 0, "m" + tileX)))); + } + + assertEquals(IrisClientMarkers.MAX_TILES, markers.trackedTiles()); + assertNull("the oldest marker tile must be gone", markers.forTile(new IrisTileKey(0, 0, 0))); + assertNotNull("the newest marker tile must be retained", markers.forTile(new IrisTileKey(overflow - 1, 0, 0))); + } + + @Test + public void markerTileIsReplacedNotAccumulated() { + IrisClientMarkers markers = new IrisClientMarkers(); + IrisTileKey key = new IrisTileKey(4, 5, 1); + markers.onMarkers(new IrisMessage.VisionMarkers(4, 5, 1, + List.of(new IrisMessage.VisionMarkers.Marker(1, 1, 0, "first"), + new IrisMessage.VisionMarkers.Marker(2, 2, 0, "second")))); + markers.onMarkers(new IrisMessage.VisionMarkers(4, 5, 1, + List.of(new IrisMessage.VisionMarkers.Marker(3, 3, 0, "third")))); + + assertEquals(1, markers.forTile(key).size()); + assertEquals(1, markers.trackedTiles()); + } + + @Test + public void pregenJobsBeyondTheCapEvictTheOldest() { + IrisClientPregenState pregen = new IrisClientPregenState(); + int overflow = IrisClientPregenState.MAX_JOBS + 5; + for (int jobId = 0; jobId < overflow; jobId++) { + pregen.onProgress(progress(jobId)); + } + + assertEquals(IrisClientPregenState.MAX_JOBS, pregen.trackedJobs()); + assertEquals(Long.valueOf(overflow - 1L), pregen.activeJobId()); + assertNotNull(pregen.active()); + } + + @Test + public void pregenPanelGoesStaleThenExpiresWithoutUpdates() { + AtomicLong clock = new AtomicLong(10_000L); + IrisClientPregenState pregen = new IrisClientPregenState(clock::get); + pregen.onProgress(progress(1L)); + + assertEquals(0L, pregen.activeAgeMillis()); + assertFalse(pregen.activeStale()); + assertFalse(pregen.activeExpired()); + + clock.addAndGet(IrisClientPregenState.STALE_AFTER_MILLIS); + assertTrue(pregen.activeStale()); + assertFalse(pregen.activeExpired()); + + clock.addAndGet(IrisClientPregenState.EXPIRE_AFTER_MILLIS); + assertTrue(pregen.activeExpired()); + + pregen.onProgress(progress(1L)); + assertFalse("a fresh frame must revive the panel", pregen.activeStale()); + } + + @Test + public void noActiveJobIsNeitherStaleNorExpired() { + AtomicLong clock = new AtomicLong(0L); + IrisClientPregenState pregen = new IrisClientPregenState(clock::get); + + assertEquals(-1L, pregen.activeAgeMillis()); + assertFalse(pregen.activeStale()); + assertFalse(pregen.activeExpired()); + + pregen.onProgress(progress(7L)); + pregen.onEnd(7L); + + assertNull(pregen.active()); + assertEquals(-1L, pregen.activeAgeMillis()); + assertFalse(pregen.activeExpired()); + } + + @Test + public void tileCacheKeepsTheDefaultBudgetForASmallViewport() { + assertEquals(IrisClientTileCache.DEFAULT_MAX_CACHED_TILES, retainedTiles(4)); + } + + @Test + public void tileCacheGrowsForALargeViewportButNotPastTheCeiling() { + assertEquals(IrisClientTileCache.ABSOLUTE_MAX_CACHED_TILES, + retainedTiles(IrisClientTileCache.ABSOLUTE_MAX_CACHED_TILES * 4)); + } + + @Test + public void malformedTileIsDroppedAndCountedWithoutPoisoningTheCache() { + IrisClientTileCache cache = new IrisClientTileCache(frame -> { + }, () -> 0L); + byte[] garbage = new byte[32]; + Arrays.fill(garbage, (byte) 0x7F); + + cache.onVisionTile(new IrisMessage.VisionTile(0, 0, 0, 1, 0, 1, garbage)); + + assertEquals(1L, cache.droppedMalformedCount()); + assertNull(cache.get(new IrisTileKey(0, 0, 0))); + + cache.onVisionTile(flatTile(0)); + assertNotNull("a bad frame must not block later tiles", cache.get(new IrisTileKey(0, 0, 0))); + } + + /** + * Capacity is not exposed, so it is measured the only way that matters: how many tiles survive. Tiles go in + * through the real wire path, so each one is a decoded image, not a stub. + */ + private static int retainedTiles(int viewportTiles) { + IrisClientTileCache cache = new IrisClientTileCache(frame -> { + }, () -> 0L); + cache.ensureCapacity(viewportTiles); + int expected = Math.max(IrisClientTileCache.DEFAULT_MAX_CACHED_TILES, + Math.min(IrisClientTileCache.ABSOLUTE_MAX_CACHED_TILES, viewportTiles)); + int inserted = expected + 8; + for (int tileX = 0; tileX < inserted; tileX++) { + cache.onVisionTile(flatTile(tileX)); + } + int retained = 0; + for (int tileX = 0; tileX < inserted; tileX++) { + if (cache.get(new IrisTileKey(tileX, 0, 0)) != null) { + retained++; + } + } + return retained; + } + + private static IrisMessage.VisionTile flatTile(int tileX) { + int[] pixels = new int[4]; + Arrays.fill(pixels, 0xFF102030); + return new IrisMessage.VisionTile(tileX, 0, 0, 1, 0, 1, IrisTileEncoder.encodePixels(pixels, 2, 2)); + } + + private static IrisMessage.PregenProgress progress(long jobId) { + return new IrisMessage.PregenProgress(jobId, 10L, 100L, 5.0D, 1000L, IrisMessage.PregenProgress.STATE_RUNNING); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisTileAssemblerAdversarialTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisTileAssemblerAdversarialTest.java new file mode 100644 index 000000000..f35327dcc --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisTileAssemblerAdversarialTest.java @@ -0,0 +1,193 @@ +package art.arcane.iris.client; + +import art.arcane.iris.core.protocol.IrisTileEncoder; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.ProtocolException; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.zip.Deflater; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +/** + * The tile stream is attacker-controlled: any server a player joins picks the sequence, chunk index, chunk + * count and payload. Structurally impossible headers must be dropped without allocating from them, a complete + * set that decodes to garbage must raise {@link ProtocolException} rather than return a half-built image, and a + * deflate stream that cannot make progress must not spin the render thread. + */ +public class IrisTileAssemblerAdversarialTest { + private static final int SIZE = IrisTileEncoder.TILE_PIXELS; + private static final byte[] PAYLOAD = {1, 2, 3, 4}; + + @Test + public void impossibleChunkHeadersAreDropped() throws ProtocolException { + IrisTileAssembler assembler = new IrisTileAssembler(); + assertNull("zero chunk count", assembler.add(tile(0, 0, 1, 0, 0, PAYLOAD))); + assertNull("negative chunk count", assembler.add(tile(0, 0, 1, 0, -1, PAYLOAD))); + assertNull("chunk count beyond the largest legal tile", + assembler.add(tile(0, 0, 1, 0, IrisTileAssembler.MAX_CHUNK_COUNT + 1, PAYLOAD))); + assertNull("index at count", assembler.add(tile(0, 0, 1, 2, 2, PAYLOAD))); + assertNull("index beyond count", assembler.add(tile(0, 0, 1, 9, 2, PAYLOAD))); + assertNull("negative index", assembler.add(tile(0, 0, 1, -1, 2, PAYLOAD))); + assertNull("missing payload", assembler.add(tile(0, 0, 1, 0, 1, null))); + } + + @Test + public void rejectedHeaderLeavesNoPartialBehind() throws ProtocolException { + IrisTileAssembler assembler = new IrisTileAssembler(); + assertNull(assembler.add(tile(0, 0, 1, 5, 2, PAYLOAD))); + + List good = IrisTileEncoder.splitIntoChunks(validBlob(), 0, 0, 0, 1); + assertNotNull("a rejected frame must not poison the tile slot", + IrisVisionTileRoundTripTest.assemble(assembler, good)); + } + + @Test + public void chunkCountFlipMidSetRestartsTheSet() throws ProtocolException { + IrisTileAssembler assembler = new IrisTileAssembler(); + // Two chunks promised at sequence 4, then the same sequence claims a single-chunk set. The stale + // half-set must be dropped rather than concatenated into the new one. + assertNull(assembler.add(tile(0, 0, 4, 0, 2, PAYLOAD))); + + List single = IrisTileEncoder.splitIntoChunks(validBlob(), 0, 0, 0, 4); + assertEquals("a flat tile must deflate into a single chunk", 1, single.size()); + assertNotNull(assembler.add(single.get(0))); + } + + @Test + public void staleSequenceChunksAreIgnoredAndTheNewerSetStillCompletes() throws ProtocolException { + IrisTileAssembler assembler = new IrisTileAssembler(); + byte[] blob = validBlob(); + byte[] head = Arrays.copyOfRange(blob, 0, blob.length / 2); + byte[] tail = Arrays.copyOfRange(blob, blob.length / 2, blob.length); + + assertNull(assembler.add(tile(0, 0, 9, 0, 2, head))); + assertNull("a chunk from an older sequence must not join the current set", + assembler.add(tile(0, 0, 8, 1, 2, tail))); + assertNotNull(assembler.add(tile(0, 0, 9, 1, 2, tail))); + } + + @Test + public void partialsBeyondTheCapAreEvicted() throws ProtocolException { + IrisTileAssembler assembler = new IrisTileAssembler(); + byte[] blob = validBlob(); + byte[] head = Arrays.copyOfRange(blob, 0, blob.length / 2); + byte[] tail = Arrays.copyOfRange(blob, blob.length / 2, blob.length); + + int overflow = IrisTileAssembler.MAX_PENDING_TILES + 1; + for (int tileX = 0; tileX < overflow; tileX++) { + assertNull(assembler.add(tile(tileX, 0, 1, 0, 2, head))); + } + // Tile 0 was the oldest partial and is gone, so its closing chunk opens a fresh incomplete set + // instead of finishing one. Retaining every partial forever would be the alternative. + assertNull("the oldest partial must have been evicted", assembler.add(tile(0, 0, 1, 1, 2, tail))); + assertNotNull("the newest partial must have survived", assembler.add(tile(overflow - 1, 0, 1, 1, 2, tail))); + } + + @Test + public void garbagePayloadRaisesProtocolException() { + IrisTileAssembler assembler = new IrisTileAssembler(); + byte[] garbage = new byte[64]; + Arrays.fill(garbage, (byte) 0x7F); + assertThrows(ProtocolException.class, () -> assembler.add(tile(0, 0, 1, 0, 1, garbage))); + } + + @Test + public void truncatedDeflateStreamRaisesProtocolExceptionInsteadOfSpinning() { + IrisTileAssembler assembler = new IrisTileAssembler(); + byte[] noisy = new byte[60_000]; + new Random(4242L).nextBytes(noisy); + byte[] full = deflate(noisy); + byte[] truncated = Arrays.copyOfRange(full, 0, full.length / 4); + // Zero-progress inflater: input exhausted, stream not finished. The old loop only broke out when + // finished/needsInput/needsDictionary happened to be set, so this shape spun forever on the render + // thread. + assertThrows(ProtocolException.class, () -> assembler.add(tile(0, 0, 1, 0, 1, truncated))); + } + + @Test + public void decompressionBombRaisesProtocolException() { + IrisTileAssembler assembler = new IrisTileAssembler(); + byte[] bomb = deflate(new byte[IrisTileCodec.MAX_DECODED_BYTES + 1024]); + assertThrows(ProtocolException.class, () -> assembler.add(tile(0, 0, 1, 0, 1, bomb))); + } + + @Test + public void impossibleHeaderFieldsRaiseProtocolException() { + IrisTileAssembler assembler = new IrisTileAssembler(); + assertThrows("zero width", ProtocolException.class, + () -> assembler.add(tile(0, 0, 1, 0, 1, deflate(header(0, SIZE, IrisTileCodec.MODE_RAW_RGB))))); + assertThrows("oversized height", ProtocolException.class, + () -> assembler.add(tile(1, 0, 1, 0, 1, deflate(header(SIZE, 4096, IrisTileCodec.MODE_RAW_RGB))))); + assertThrows("unknown pixel mode", ProtocolException.class, + () -> assembler.add(tile(2, 0, 1, 0, 1, deflate(header(SIZE, SIZE, 42))))); + } + + @Test + public void paletteIndexBeyondPaletteRaisesProtocolException() { + IrisTileAssembler assembler = new IrisTileAssembler(); + ByteArrayOutputStream raw = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(raw)) { + out.writeInt(2); + out.writeInt(2); + out.writeByte(IrisTileCodec.MODE_PALETTE); + out.writeInt(1); + out.writeByte(0); + out.writeByte(0); + out.writeByte(0); + for (int pixel = 0; pixel < 4; pixel++) { + out.writeByte(pixel == 3 ? 200 : 0); + } + } catch (IOException failure) { + throw new UncheckedIOException(failure); + } + byte[] blob = deflate(raw.toByteArray()); + assertThrows(ProtocolException.class, () -> assembler.add(tile(0, 0, 1, 0, 1, blob))); + } + + private static IrisMessage.VisionTile tile(int tileX, int tileZ, int sequence, int chunkIndex, int chunkCount, byte[] data) { + return new IrisMessage.VisionTile(tileX, tileZ, 0, sequence, chunkIndex, chunkCount, data); + } + + /** A real encoder output for a flat tile: one chunk, palette mode, decodes cleanly. */ + private static byte[] validBlob() { + int[] pixels = new int[SIZE * SIZE]; + Arrays.fill(pixels, 0xFF204060); + return IrisTileEncoder.encodePixels(pixels, SIZE, SIZE); + } + + private static byte[] header(int width, int height, int mode) { + ByteArrayOutputStream raw = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(raw)) { + out.writeInt(width); + out.writeInt(height); + out.writeByte(mode); + } catch (IOException failure) { + throw new UncheckedIOException(failure); + } + return raw.toByteArray(); + } + + private static byte[] deflate(byte[] input) { + Deflater deflater = new Deflater(Deflater.BEST_SPEED); + deflater.setInput(input); + deflater.finish(); + ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length / 2)); + byte[] buffer = new byte[8192]; + while (!deflater.finished()) { + out.write(buffer, 0, deflater.deflate(buffer)); + } + deflater.end(); + return out.toByteArray(); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisVisionTileRoundTripTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisVisionTileRoundTripTest.java new file mode 100644 index 000000000..338d74a45 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/client/IrisVisionTileRoundTripTest.java @@ -0,0 +1,149 @@ +package art.arcane.iris.client; + +import art.arcane.iris.core.protocol.IrisTileEncoder; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisMessageCodec; +import art.arcane.iris.spi.protocol.IrisProtocol; +import art.arcane.iris.spi.protocol.ProtocolException; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * End-to-end vision tile path: {@link IrisTileEncoder} on the server, the wire codec both ways, then + * {@link IrisTileAssembler} and {@link IrisTileCodec} on the client. Encoder and decoder live in different + * modules and share no code, so pixel equality is the only thing that proves they still agree - a palette + * write order change or a header field added on one side is otherwise silent. + */ +public class IrisVisionTileRoundTripTest { + private static final int SIZE = IrisTileEncoder.TILE_PIXELS; + private static final int OPAQUE = 0xFF000000; + + @Test + public void paletteTileRoundTripsToIdenticalPixels() throws ProtocolException { + int[] pixels = bandedPixels(16); + IrisTileImage decoded = roundTrip(pixels, 3, -4, 2, 7); + assertNotNull(decoded); + assertEquals(SIZE, decoded.width()); + assertEquals(SIZE, decoded.height()); + assertOpaqueRgbEquals(pixels, decoded.argb()); + } + + @Test + public void rawTileSplitsIntoChunksAndRoundTrips() throws ProtocolException { + int[] pixels = highEntropyPixels(); + byte[] blob = IrisTileEncoder.encodePixels(pixels, SIZE, SIZE); + List chunks = IrisTileEncoder.splitIntoChunks(blob, 0, 0, 0, 1); + assertTrue("a high-entropy tile must exceed one chunk to exercise reassembly", chunks.size() > 1); + for (IrisMessage.VisionTile chunk : chunks) { + assertTrue("chunk frame must fit the wire cap", + IrisMessageCodec.encode(chunk).length <= IrisProtocol.MAX_FRAME_BYTES); + } + + IrisTileImage decoded = assemble(new IrisTileAssembler(), overTheWire(chunks)); + assertNotNull(decoded); + assertOpaqueRgbEquals(pixels, decoded.argb()); + } + + @Test + public void chunksReassembleWhenDeliveredOutOfOrder() throws ProtocolException { + int[] pixels = highEntropyPixels(); + byte[] blob = IrisTileEncoder.encodePixels(pixels, SIZE, SIZE); + List chunks = new ArrayList<>(overTheWire(IrisTileEncoder.splitIntoChunks(blob, 5, 6, 1, 42))); + assertTrue(chunks.size() > 1); + Collections.reverse(chunks); + + IrisTileImage decoded = assemble(new IrisTileAssembler(), chunks); + assertNotNull(decoded); + assertOpaqueRgbEquals(pixels, decoded.argb()); + } + + @Test + public void incompleteChunkSetProducesNothing() throws ProtocolException { + int[] pixels = highEntropyPixels(); + byte[] blob = IrisTileEncoder.encodePixels(pixels, SIZE, SIZE); + List chunks = overTheWire(IrisTileEncoder.splitIntoChunks(blob, 1, 1, 0, 1)); + assertTrue(chunks.size() > 1); + + IrisTileAssembler assembler = new IrisTileAssembler(); + assertNull(assemble(assembler, chunks.subList(0, chunks.size() - 1))); + assertNotNull(assembler.add(chunks.get(chunks.size() - 1))); + } + + @Test + public void repeatedChunkDoesNotCompleteTheSetEarly() throws ProtocolException { + int[] pixels = highEntropyPixels(); + byte[] blob = IrisTileEncoder.encodePixels(pixels, SIZE, SIZE); + List chunks = overTheWire(IrisTileEncoder.splitIntoChunks(blob, 2, 2, 0, 1)); + assertTrue(chunks.size() > 1); + + IrisTileAssembler assembler = new IrisTileAssembler(); + for (int repeat = 0; repeat < chunks.size() + 2; repeat++) { + assertNull("a duplicated chunk must not count twice", assembler.add(chunks.get(0))); + } + } + + static IrisTileImage assemble(IrisTileAssembler assembler, List chunks) throws ProtocolException { + IrisTileImage assembled = null; + for (IrisMessage.VisionTile chunk : chunks) { + IrisTileImage produced = assembler.add(chunk); + if (produced != null) { + assembled = produced; + } + } + return assembled; + } + + /** Pushes each chunk through encode/decode so the test covers the wire form, not just the record. */ + private static List overTheWire(List chunks) { + List transported = new ArrayList<>(chunks.size()); + for (IrisMessage.VisionTile chunk : chunks) { + try { + transported.add((IrisMessage.VisionTile) IrisMessageCodec.decode(IrisMessageCodec.encode(chunk))); + } catch (ProtocolException rejected) { + throw new AssertionError("a chunk the encoder produced must survive the codec", rejected); + } + } + return transported; + } + + private static IrisTileImage roundTrip(int[] pixels, int tileX, int tileZ, int zoom, int sequence) throws ProtocolException { + byte[] blob = IrisTileEncoder.encodePixels(pixels, SIZE, SIZE); + return assemble(new IrisTileAssembler(), overTheWire(IrisTileEncoder.splitIntoChunks(blob, tileX, tileZ, zoom, sequence))); + } + + private static void assertOpaqueRgbEquals(int[] source, int[] decoded) { + assertEquals(source.length, decoded.length); + for (int index = 0; index < source.length; index++) { + assertEquals("pixel " + index, OPAQUE | source[index] & 0xFFFFFF, decoded[index]); + } + } + + /** Few enough distinct colours that the encoder picks palette mode. */ + private static int[] bandedPixels(int colors) { + int[] pixels = new int[SIZE * SIZE]; + for (int index = 0; index < pixels.length; index++) { + int band = index % colors; + pixels[index] = OPAQUE | band * 16 << 16 | band * 8 << 8 | band * 4; + } + return pixels; + } + + /** Enough distinct colours to force raw mode, and enough entropy that deflate cannot fold it into one chunk. */ + private static int[] highEntropyPixels() { + Random random = new Random(1337L); + int[] pixels = new int[SIZE * SIZE]; + for (int index = 0; index < pixels.length; index++) { + pixels[index] = OPAQUE | random.nextInt() & 0xFFFFFF; + } + return pixels; + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/InitialSpawnQueueTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/InitialSpawnQueueTest.java new file mode 100644 index 000000000..76aecdd4e --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/InitialSpawnQueueTest.java @@ -0,0 +1,160 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +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.AtomicLong; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class InitialSpawnQueueTest { + @Test + public void rejectsDuplicatesAndEnforcesCapacityWhileInFlight() { + InitialSpawnQueue queue = new InitialSpawnQueue(2); + + assertTrue(queue.offer(1L)); + assertFalse(queue.offer(1L)); + assertTrue(queue.offer(2L)); + assertEquals(Long.valueOf(1L), queue.poll()); + assertFalse(queue.offer(3L)); + queue.complete(1L); + assertTrue(queue.offer(3L)); + assertEquals(2, queue.size()); + } + + @Test + public void retryRotatesWithoutBlockingReadyEntries() { + InitialSpawnQueue queue = new InitialSpawnQueue(4); + queue.offer(1L); + queue.offer(2L); + queue.offer(3L); + + int budget = queue.batchSize(8); + Long unavailable = queue.poll(); + queue.retry(unavailable); + Long firstReady = queue.poll(); + queue.complete(firstReady); + Long secondReady = queue.poll(); + queue.complete(secondReady); + + assertEquals(3, budget); + assertEquals(Long.valueOf(1L), unavailable); + assertEquals(Long.valueOf(2L), firstReady); + assertEquals(Long.valueOf(3L), secondReady); + assertEquals(1, queue.batchSize(8)); + assertEquals(Long.valueOf(1L), queue.poll()); + } + + @Test + public void retryIsDeduplicated() { + InitialSpawnQueue queue = new InitialSpawnQueue(2); + queue.offer(7L); + Long key = queue.poll(); + + queue.retry(key); + queue.retry(key); + + assertEquals(1, queue.batchSize(8)); + assertEquals(Long.valueOf(7L), queue.poll()); + assertNull(queue.poll()); + } + + @Test + public void closeClearsAndRejectsFurtherWork() { + InitialSpawnQueue queue = new InitialSpawnQueue(2); + queue.offer(1L); + + queue.close(); + + assertTrue(queue.isEmpty()); + assertEquals(0, queue.size()); + assertFalse(queue.offer(2L)); + assertEquals(0, queue.batchSize(8)); + } + + @Test + public void clearAllowsLaterWork() { + InitialSpawnQueue queue = new InitialSpawnQueue(2); + queue.offer(1L); + + queue.clear(); + + assertTrue(queue.offer(2L)); + assertEquals(Long.valueOf(2L), queue.poll()); + } + + @Test + public void expiredEntriesReleaseCapacity() { + AtomicLong now = new AtomicLong(); + InitialSpawnQueue queue = new InitialSpawnQueue(2, 100L, now::get); + queue.offer(1L); + queue.offer(2L); + + now.set(100L); + + assertTrue(queue.offer(3L)); + assertEquals(1, queue.size()); + assertEquals(Long.valueOf(3L), queue.poll()); + } + + @Test + public void expireKeepsYoungerOffersWhenReleasingCapacity() { + AtomicLong now = new AtomicLong(); + InitialSpawnQueue queue = new InitialSpawnQueue(2, 100L, now::get); + queue.offer(1L); + now.set(60L); + queue.offer(2L); + + now.set(100L); + + assertTrue(queue.offer(3L)); + assertEquals(2, queue.size()); + assertEquals(Long.valueOf(2L), queue.poll()); + assertEquals(Long.valueOf(3L), queue.poll()); + assertNull(queue.poll()); + } + + @Test + public void expiredInFlightEntryCannotRetry() { + AtomicLong now = new AtomicLong(); + InitialSpawnQueue queue = new InitialSpawnQueue(2, 100L, now::get); + queue.offer(1L); + Long inFlight = queue.poll(); + + now.set(100L); + queue.retry(inFlight); + + assertEquals(0, queue.size()); + assertNull(queue.poll()); + } + + @Test + public void concurrentOffersRemainDeduplicatedAndBounded() throws Exception { + InitialSpawnQueue queue = new InitialSpawnQueue(256); + ExecutorService executor = Executors.newFixedThreadPool(8); + List> futures = new ArrayList<>(); + for (int thread = 0; thread < 8; thread++) { + futures.add(executor.submit(() -> { + for (long key = 0L; key < 512L; key++) { + queue.offer(key); + } + })); + } + for (Future future : futures) { + future.get(); + } + executor.shutdown(); + + assertTrue(executor.awaitTermination(10L, TimeUnit.SECONDS)); + assertEquals(256, queue.size()); + assertEquals(256, queue.batchSize(512)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModLanguageAssetsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModLanguageAssetsTest.java new file mode 100644 index 000000000..c11c983cd --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModLanguageAssetsTest.java @@ -0,0 +1,92 @@ +package art.arcane.iris.modded; + +import art.arcane.volmlib.util.localization.VolmitLocales; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.Test; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class IrisModLanguageAssetsTest { + private static final String ROOT = "assets/irisworldgen/lang/"; + private static final String TOGGLE_KEY = "key.irisworldgen.toggle_pregen_hud"; + + @Test + public void minecraftLanguageAssetsMatchSharedLocaleManifest() throws Exception { + JsonObject english = read("en_us"); + assertEquals(4, english.size()); + + for (String locale : VolmitLocales.nonEnglish()) { + String minecraftLocale = VolmitLocales.minecraftCode(locale); + JsonObject translated = read(minecraftLocale); + assertEquals(minecraftLocale, english.keySet(), translated.keySet()); + for (String key : english.keySet()) { + JsonElement value = translated.get(key); + assertTrue(minecraftLocale + ": " + key, value.isJsonPrimitive()); + assertTrue(minecraftLocale + ": " + key, value.getAsJsonPrimitive().isString()); + assertFalse(minecraftLocale + ": " + key, value.getAsString().isBlank()); + } + assertFalse( + minecraftLocale + " contains an English HUD label", + english.get(TOGGLE_KEY).getAsString().equals(translated.get(TOGGLE_KEY).getAsString()) + ); + } + } + + @Test + public void minecraftLanguageResourceSetMatchesSharedLocaleManifestAndEnglishBaseline() throws Exception { + Set expected = VolmitLocales.nonEnglish().stream() + .map(VolmitLocales::minecraftCode) + .map(locale -> locale + ".json") + .collect(Collectors.toCollection(LinkedHashSet::new)); + expected.add("en_us.json"); + + assertEquals(expected, resourceFiles()); + } + + private JsonObject read(String locale) throws Exception { + String resource = ROOT + locale + ".json"; + InputStream input = IrisModLanguageAssetsTest.class.getClassLoader().getResourceAsStream(resource); + assertNotNull("Missing mod language asset: " + resource, input); + try (InputStream stream = input; + InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { + JsonElement parsed = JsonParser.parseReader(reader); + assertTrue("Mod language asset is not an object: " + resource, parsed.isJsonObject()); + return parsed.getAsJsonObject(); + } + } + + private Set resourceFiles() throws Exception { + URL resource = IrisModLanguageAssetsTest.class.getClassLoader().getResource(ROOT); + Path directory; + if (resource != null && "file".equals(resource.getProtocol())) { + directory = Path.of(resource.toURI()); + } else { + String sources = System.getProperty("iris.moddedCommonSources"); + assertNotNull("Missing mod language resource directory and source root", sources); + directory = Path.of(sources).getParent() + .resolve("resources").resolve(ROOT); + } + try (Stream paths = Files.list(directory)) { + return paths + .filter(Files::isRegularFile) + .map(path -> path.getFileName().toString()) + .collect(Collectors.toUnmodifiableSet()); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedBlockEntityParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedBlockEntityParityTest.java new file mode 100644 index 000000000..5292c0c50 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedBlockEntityParityTest.java @@ -0,0 +1,152 @@ +package art.arcane.iris.modded; + +import art.arcane.volmlib.util.math.RNG; +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.RegistryAccess; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.Identifier; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.Container; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.BiomeSpecialEffects; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BannerBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.minecraft.world.level.block.entity.SpawnerBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.PalettedContainerFactory; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerRO; +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.storage.TagValueInput; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class IrisModdedBlockEntityParityTest { + private static RegistryAccess registries; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + if (!Items.DIAMOND.builtInRegistryHolder().areComponentsBound()) { + Items.DIAMOND.builtInRegistryHolder().bindComponents(DataComponentMap.EMPTY); + } + } + + @Test + public void generatedContainerGetsBlockEntityAndPersistsFilledLoot() { + ProtoChunk chunk = newChunk(); + BlockPos position = new BlockPos(3, 70, 5); + chunk.setBlockState(position, Blocks.CHEST.defaultBlockState(), 0); + + IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, position, Blocks.CHEST.defaultBlockState()); + + BlockEntity blockEntity = chunk.getBlockEntity(position); + assertNotNull(blockEntity); + assertTrue(blockEntity instanceof Container); + Container container = (Container) blockEntity; + ModdedLootApplier.fillContainer(container, List.of(new ItemStack(Items.DIAMOND)), new RNG(17L)); + assertEquals(1, countItem(container, Items.DIAMOND)); + CompoundTag saved = blockEntity.saveWithoutMetadata(registries); + ChestBlockEntity restored = new ChestBlockEntity(position, Blocks.CHEST.defaultBlockState()); + restored.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, saved)); + assertEquals(1, countItem(restored, Items.DIAMOND)); + } + + @Test + public void generatedSignSpawnerAndBannerGetNativeBlockEntities() { + ProtoChunk chunk = newChunk(); + BlockState signState = Blocks.OAK_SIGN.defaultBlockState(); + BlockState spawnerState = Blocks.SPAWNER.defaultBlockState(); + BlockState bannerState = BuiltInRegistries.BLOCK.getValue( + Identifier.parse("minecraft:white_banner")).defaultBlockState(); + BlockPos signPosition = new BlockPos(1, 70, 1); + BlockPos spawnerPosition = new BlockPos(2, 70, 1); + BlockPos bannerPosition = new BlockPos(3, 70, 1); + + chunk.setBlockState(signPosition, signState, 0); + chunk.setBlockState(spawnerPosition, spawnerState, 0); + chunk.setBlockState(bannerPosition, bannerState, 0); + IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, signPosition, signState); + IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, spawnerPosition, spawnerState); + IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, bannerPosition, bannerState); + + assertTrue(chunk.getBlockEntity(signPosition) instanceof SignBlockEntity); + assertTrue(chunk.getBlockEntity(spawnerPosition) instanceof SpawnerBlockEntity); + assertTrue(chunk.getBlockEntity(bannerPosition) instanceof BannerBlockEntity); + } + + private static ProtoChunk newChunk() { + return new ProtoChunk( + new ChunkPos(0, 0), + UpgradeData.EMPTY, + LevelHeightAccessor.create(-64, 384), + palettedContainerFactory(), + null); + } + + private static PalettedContainerFactory palettedContainerFactory() { + Strategy blockStrategy = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY); + Codec> blockCodec = PalettedContainer.codecRW( + BlockState.CODEC, blockStrategy, Blocks.AIR.defaultBlockState()); + Biome biome = new Biome.BiomeBuilder() + .hasPrecipitation(false) + .temperature(0.8F) + .downfall(0.4F) + .specialEffects(new BiomeSpecialEffects.Builder().waterColor(0x3F76E4).build()) + .mobSpawnSettings(MobSpawnSettings.EMPTY) + .generationSettings(BiomeGenerationSettings.EMPTY) + .build(); + Holder biomeHolder = Holder.direct(biome); + IdMapper> biomeIds = new IdMapper<>(1); + biomeIds.add(biomeHolder); + Strategy> biomeStrategy = Strategy.createForBiomes(biomeIds); + Codec>> biomeCodec = PalettedContainer.codecRO( + Biome.CODEC, biomeStrategy, biomeHolder); + return new PalettedContainerFactory( + blockStrategy, + Blocks.AIR.defaultBlockState(), + blockCodec, + biomeStrategy, + biomeHolder, + biomeCodec); + } + + private static int countItem(Container container, Item item) { + int count = 0; + for (int slot = 0; slot < container.getContainerSize(); slot++) { + ItemStack stack = container.getItem(slot); + if (stack.getItem() == item) { + count += stack.getCount(); + } + } + return count; + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java new file mode 100644 index 000000000..9ed4ebd45 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java @@ -0,0 +1,67 @@ +package art.arcane.iris.modded; + +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.random.Weighted; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.level.biome.MobSpawnSettings; +import org.junit.Test; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class IrisModdedChunkGeneratorSpawnTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void explicitSpawnsReplaceMatchingVanillaTypesAndPreserveOthers() { + MobSpawnSettings.SpawnerData zombie = new MobSpawnSettings.SpawnerData(EntityTypes.ZOMBIE, 1, 4); + MobSpawnSettings.SpawnerData vanillaSlime = new MobSpawnSettings.SpawnerData(EntityTypes.SLIME, 1, 1); + MobSpawnSettings.SpawnerData explicitSlime = new MobSpawnSettings.SpawnerData(EntityTypes.SLIME, 2, 5); + MobSpawnSettings.SpawnerData explicitCow = new MobSpawnSettings.SpawnerData(EntityTypes.COW, 2, 4); + WeightedList vanilla = WeightedList.of(List.of( + new Weighted<>(zombie, 100), + new Weighted<>(vanillaSlime, 1))); + WeightedList explicit = WeightedList.of(List.of( + new Weighted<>(explicitSlime, 7), + new Weighted<>(explicitCow, 3))); + + WeightedList merged = NativeSpawnTableMerger.merge(vanilla, explicit); + List> entries = merged.unwrap(); + + assertEquals(3, entries.size()); + assertEquals(EntityTypes.ZOMBIE, entries.get(0).value().type()); + assertEquals(100, entries.get(0).weight()); + assertEquals(explicitSlime, entries.get(1).value()); + assertEquals(7, entries.get(1).weight()); + assertEquals(explicitCow, entries.get(2).value()); + assertEquals(3, entries.get(2).weight()); + } + + @Test + public void vanillaChunkGenerationMobsUseTheVisibleBiomesVanillaDerivative() throws IOException { + Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"), + "art/arcane/iris/modded/IrisModdedChunkGenerator.java"); + String source = Files.readString(sourcePath); + int spawnStart = source.indexOf("public void spawnOriginalMobs"); + int spawnEnd = source.indexOf("@Override", spawnStart + 1); + String spawn = source.substring(spawnStart, spawnEnd); + + assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)")); + assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())")); + assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration(")); + assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())")); + assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()")); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedHeightmapParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedHeightmapParityTest.java new file mode 100644 index 000000000..cd6099ad1 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedHeightmapParityTest.java @@ -0,0 +1,33 @@ +package art.arcane.iris.modded; + +import net.minecraft.util.Mth; +import net.minecraft.util.SimpleBitStorage; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class IrisModdedHeightmapParityTest { + @Test + public void terrainHeightmapEncodesPaperRelativeSurfaceHeights() { + int height = 384; + long[] rawData = ModdedHeightmaps.terrainRawData(height, + (x, z) -> x + z * 16 + 1); + SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), 256, rawData); + + assertEquals(1, storage.get(0)); + assertEquals(16, storage.get(15)); + assertEquals(17, storage.get(16)); + assertEquals(256, storage.get(255)); + } + + @Test + public void terrainHeightmapClampsToDimensionStorageRange() { + int height = 384; + long[] rawData = ModdedHeightmaps.terrainRawData(height, + (x, z) -> x == 0 ? -20 : 900); + SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), 256, rawData); + + assertEquals(0, storage.get(0)); + assertEquals(height, storage.get(1)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedLocateRoutingTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedLocateRoutingTest.java new file mode 100644 index 000000000..144bd44be --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedLocateRoutingTest.java @@ -0,0 +1,25 @@ +package art.arcane.iris.modded; + +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 static org.junit.Assert.assertSame; + +public class IrisModdedLocateRoutingTest { + @Test + public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() { + BlockPos origin = BlockPos.ZERO; + Pair> irisNear = Pair.of(new BlockPos(4, 70, 0), null); + Pair> nativeFar = Pair.of(new BlockPos(8, 70, 0), null); + Pair> nativeNear = Pair.of(new BlockPos(2, 70, 0), null); + Pair> 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)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java new file mode 100644 index 000000000..6cf161e4a --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java @@ -0,0 +1,243 @@ +package art.arcane.iris.modded; + +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.IrisRange; +import art.arcane.iris.engine.object.InferredType; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.server.Bootstrap; +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.List; +import java.util.Set; +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 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; + +public class IrisModdedStructureParityTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void surfaceBiomeFastPathBeginsAboveTheCaveSwitch() { + assertFalse(IrisModdedBiomeSource.isGuaranteedSurfaceBiome(-2, -256)); + assertTrue(IrisModdedBiomeSource.isGuaranteedSurfaceBiome(-1, -256)); + assertFalse(IrisModdedBiomeSource.isGuaranteedSurfaceBiome(10, 0)); + assertTrue(IrisModdedBiomeSource.isGuaranteedSurfaceBiome(11, 0)); + } + + @Test + public void visibleCaveBiomeBeginsEightBlocksBelowTheSurface() { + assertFalse(IrisModdedBiomeSource.isUnderground(93, 100)); + assertTrue(IrisModdedBiomeSource.isUnderground(92, 100)); + assertTrue(IrisModdedBiomeSource.isUnderground(-20, 100)); + } + + @Test + public void monumentBiomeCubeUsesSurfaceBiomesAtShiftedSeaLevel() { + assertTrue(IrisModdedBiomeSource.isMonumentSurfaceBiomeQuery(50, 29, -256, 306)); + assertFalse(IrisModdedBiomeSource.isMonumentSurfaceBiomeQuery(51, 29, -256, 306)); + assertFalse(IrisModdedBiomeSource.isMonumentSurfaceBiomeQuery(50, 28, -256, 306)); + } + + @Test + public void strongholdRingSearchSamplesOneQuartColumnPerChunk() throws IOException { + assertEquals(4, IrisModdedBiomeSource.horizontalBiomeSearchQuartStep(0, 112)); + assertEquals(1, IrisModdedBiomeSource.horizontalBiomeSearchQuartStep(1, 112)); + assertEquals(1, IrisModdedBiomeSource.horizontalBiomeSearchQuartStep(0, 111)); + String source = Files.readString(Path.of( + System.getProperty("iris.moddedCommonSources"), + "art/arcane/iris/modded/IrisModdedBiomeSource.java")); + 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 spawnHeightMatchesPaperFixedSpawnClamp() { + assertEquals(96, ModdedDimensionMetadata.clampSpawnHeight(-64, 384)); + assertEquals(96, ModdedDimensionMetadata.clampSpawnHeight(0, 128)); + assertEquals(88, ModdedDimensionMetadata.clampSpawnHeight(80, 10)); + assertEquals(101, ModdedDimensionMetadata.clampSpawnHeight(100, 20)); + } + + @Test + public void freshAndStudioWorldsReconcileToOriginWhileCustomSpawnsRemain() { + assertTrue(ModdedEngineBootstrap.shouldReconcileSpawn(true, false, 120, -64)); + assertTrue(ModdedEngineBootstrap.shouldReconcileSpawn(false, true, 120, -64)); + assertTrue(ModdedEngineBootstrap.shouldReconcileSpawn(false, false, 0, 0)); + assertFalse(ModdedEngineBootstrap.shouldReconcileSpawn(false, false, 1, 0)); + assertFalse(ModdedEngineBootstrap.shouldReconcileSpawn(false, false, 0, -1)); + } + + @Test + public void reconciledSpawnUsesOriginAndClampedSurfaceHeight() { + assertEquals(new BlockPos(0, 73, 0), ModdedEngineBootstrap.reconciledSpawnPosition(73, -64, 384)); + assertEquals(new BlockPos(0, -63, 0), ModdedEngineBootstrap.reconciledSpawnPosition(-100, -64, 384)); + assertEquals(new BlockPos(0, 318, 0), ModdedEngineBootstrap.reconciledSpawnPosition(400, -64, 384)); + } + + @Test + public void biomeResolutionUsesRawPlatformSeedFormula() { + long worldSeed = 998877665544L; + int blockX = -124; + int blockY = 48; + int blockZ = 712; + long expected = worldSeed + ^ ((long) blockX * 341873128712L) + ^ ((long) blockY * 132897987541L) + ^ ((long) blockZ * 42317861L); + + assertEquals(expected, IrisModdedBiomeSource.biomeResolutionSeed(worldSeed, blockX, blockY, blockZ)); + } + + @Test + public void configuredBiomeKeysContainOnlyPackDerivativesAndCustomBiomes() { + IrisBiome ocean = new IrisBiome() + .setDerivative("minecraft:desert") + .setVanillaDerivative("minecraft:deep_ocean"); + IrisBiome custom = new IrisBiome() + .setDerivative("forest") + .setCustomDerivitives(new KList<>(new IrisBiomeCustom().setId("Aurora"))); + IrisBiome shore = new IrisBiome() + .setVanillaDerivative("minecraft:desert") + .setInferredType(InferredType.SHORE); + IrisBiome unsafeSea = new IrisBiome() + .setVanillaDerivative("minecraft:plains") + .setInferredType(InferredType.SEA); + + Set keys = ModdedDimensionMetadata.collectConfiguredBiomeKeys( + List.of(ocean, custom, shore, unsafeSea), "OverWorld"); + + assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "minecraft:beach", + "minecraft:the_void", "overworld:aurora"), keys); + assertFalse(keys.contains("minecraft:desert")); + assertFalse(keys.contains("minecraft:plains")); + } + + @Test + public void structureStateRejectsBiomesOutsideThePackContract() { + Set generated = Set.of("minecraft:deep_ocean", "minecraft:dark_forest"); + + assertTrue(IrisModdedBiomeSource.isGeneratedBiomeKey("minecraft:deep_ocean", generated)); + assertTrue(IrisModdedBiomeSource.isGeneratedBiomeKey("MINECRAFT:DARK_FOREST", generated)); + assertFalse(IrisModdedBiomeSource.isGeneratedBiomeKey("minecraft:desert", generated)); + assertFalse(IrisModdedBiomeSource.isGeneratedBiomeKey(null, generated)); + } + + @Test + public void structureBiomeContractRejectsAnEmptyConfiguredSet() { + IllegalStateException error = assertThrows(IllegalStateException.class, + () -> IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(Set.of())); + + assertEquals("Iris has no configured structure biomes", error.getMessage()); + } + + @Test + public void structureBiomeContractPreservesConfiguredKeysDuringBootstrap() { + Set configured = Set.of("minecraft:deep_ocean", "minecraft:dark_forest"); + + assertSame(configured, IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(configured)); + } + + @Test + public void configuredDimensionMetadataIsExactBeforeEngineBinding() { + IrisDimension dimension = new IrisDimension() + .setDimensionHeight(new IrisRange(-256, 512)) + .setFluidHeight(50); + dimension.setLoadKey("bootstrap_contract"); + + ModdedDimensionMetadata.DimensionMetadata metadata = + ModdedDimensionMetadata.dimensionMetadata(dimension); + + assertEquals(-256, metadata.minY()); + assertEquals(512, metadata.maxY()); + assertEquals(768, metadata.depth()); + assertEquals(50, metadata.seaLevel()); + } + + @Test + public void structureRingWorkersWaitWithoutBlockingLifecycleBinding() throws Exception { + ModdedEngineBinding binding = + new ModdedEngineBinding<>(5L, TimeUnit.SECONDS); + String exactEngine = "exact-engine"; + CountDownLatch workerStarted = new CountDownLatch(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future ringWorker = executor.submit(() -> { + workerStarted.countDown(); + return binding.await("overworld:overworld"); + }); + + assertTrue(workerStarted.await(1L, TimeUnit.SECONDS)); + assertFalse(ringWorker.isDone()); + binding.complete(exactEngine); + + assertSame(exactEngine, ringWorker.get(1L, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void structureRingBindingPropagatesBootstrapFailure() { + ModdedEngineBinding binding = + new ModdedEngineBinding<>(1L, TimeUnit.SECONDS); + IllegalArgumentException failure = new IllegalArgumentException("broken pack"); + binding.fail(failure); + + try { + binding.await("overworld:overworld"); + } catch (IllegalStateException error) { + assertSame(failure, error.getCause()); + return; + } + throw new AssertionError("Expected failed engine binding to propagate"); + } + + @Test + public void structureBiomeBootstrapAllowsOnlyPendingBindingsToUseMetadata() { + ModdedEngineBinding binding = + new ModdedEngineBinding<>(1L, TimeUnit.SECONDS); + + binding.throwIfFailed("overworld:overworld"); + } + + @Test + public void structureBiomeBootstrapPropagatesBindingFailure() { + ModdedEngineBinding binding = + new ModdedEngineBinding<>(1L, TimeUnit.SECONDS); + IllegalArgumentException failure = new IllegalArgumentException("broken pack"); + binding.fail(failure); + + IllegalStateException error = assertThrows(IllegalStateException.class, + () -> binding.throwIfFailed("overworld:overworld")); + + assertSame(failure, error.getCause()); + } + + @Test + public void initialEntitySpawnsUseThePaperCompletionMarker() { + assertSame(MantleFlag.INITIAL_SPAWNED_MARKER, ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG); + } + +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/MainWorldServiceInstanceRootTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/MainWorldServiceInstanceRootTest.java new file mode 100644 index 000000000..4c9c2d746 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/MainWorldServiceInstanceRootTest.java @@ -0,0 +1,94 @@ +package art.arcane.iris.modded; + +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.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class MainWorldServiceInstanceRootTest { + @Test + public void universeOptionIsReadInBothArgumentForms() { + assertEquals("worlds", MainWorldService.commandLineOption( + List.of("nogui", "--universe", "worlds"), "universe")); + assertEquals("worlds", MainWorldService.commandLineOption( + List.of("--universe=worlds", "nogui"), "universe")); + assertEquals("survival", MainWorldService.commandLineOption( + List.of("--universe", "worlds", "--world", "survival"), "world")); + } + + @Test + public void missingOrValuelessOptionsResolveToNull() { + assertNull(MainWorldService.commandLineOption(List.of("nogui"), "universe")); + assertNull(MainWorldService.commandLineOption(List.of(), "world")); + assertNull(MainWorldService.commandLineOption(List.of("nogui", "--world"), "world")); + } + + @Test + public void universeRootDefaultsToTheInstanceRootAndHonorsTheOption() throws IOException { + Path instanceRoot = Files.createTempDirectory("iris-instance"); + Path elsewhere = Files.createTempDirectory("iris-elsewhere"); + + assertEquals(instanceRoot, MainWorldService.universeRoot(instanceRoot, null)); + assertEquals(instanceRoot, MainWorldService.universeRoot(instanceRoot, "")); + assertEquals(instanceRoot.resolve("worlds"), MainWorldService.universeRoot(instanceRoot, "worlds")); + assertEquals(elsewhere, MainWorldService.universeRoot(instanceRoot, elsewhere.toString())); + } + + @Test + public void worldRootResolvesInsideTheUniverse() throws IOException { + Path universe = Files.createTempDirectory("iris-universe"); + Path world = Files.createDirectory(universe.resolve("survival")); + + assertEquals(world.toAbsolutePath().normalize(), + MainWorldService.resolveWorldRoot(universe, "survival")); + } + + @Test + public void worldRootRefusesToEscapeTheUniverse() throws IOException { + Path universe = Files.createTempDirectory("iris-universe"); + Files.createDirectory(universe.resolve("survival")); + + IOException escaped = assertThrows(IOException.class, + () -> MainWorldService.resolveWorldRoot(universe, "../survival")); + IOException empty = assertThrows(IOException.class, + () -> MainWorldService.resolveWorldRoot(universe, ".")); + + assertTrue(escaped.getMessage().contains("Unsafe world name")); + assertTrue(empty.getMessage().contains("Unsafe world name")); + } + + @Test + public void worldRootReportsMissingDirectoriesAsTheRecoverableCase() throws IOException { + Path universe = Files.createTempDirectory("iris-universe"); + + MainWorldService.MissingWorldRootException missingWorld = assertThrows( + MainWorldService.MissingWorldRootException.class, + () -> MainWorldService.resolveWorldRoot(universe, "survival")); + MainWorldService.MissingWorldRootException missingUniverse = assertThrows( + MainWorldService.MissingWorldRootException.class, + () -> MainWorldService.resolveWorldRoot(universe.resolve("absent"), "survival")); + + assertTrue(missingWorld.getMessage().contains("world directory does not exist")); + assertEquals(universe.resolve("survival").toAbsolutePath().normalize(), missingWorld.path()); + assertTrue(missingUniverse.getMessage().contains("universe directory does not exist")); + assertEquals(universe.resolve("absent"), missingUniverse.path()); + } + + @Test + public void unsafeWorldNameIsNotTreatedAsAMissingWorldRoot() throws IOException { + Path universe = Files.createTempDirectory("iris-universe"); + + IOException escaped = assertThrows(IOException.class, + () -> MainWorldService.resolveWorldRoot(universe, "../survival")); + + assertFalse(escaped instanceof MainWorldService.MissingWorldRootException); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBiomeWriterCacheTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBiomeWriterCacheTest.java new file mode 100644 index 000000000..c4ea59f7e --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBiomeWriterCacheTest.java @@ -0,0 +1,33 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.spi.PlatformBiome; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +public class ModdedBiomeWriterCacheTest { + @Test + public void unavailableServerFallsBackToBiomeIdZero() { + ModdedBiomeWriter writer = new ModdedBiomeWriter(() -> null); + + assertEquals(0, writer.biomeIdFor("minecraft:plains")); + assertEquals(0, writer.biomeIdFor("minecraft:plains")); + } + + @Test + public void unavailableServerYieldsAnEmptyMutableBiomeList() { + ModdedBiomeWriter writer = new ModdedBiomeWriter(() -> null); + + List first = writer.allBiomes(); + List second = writer.allBiomes(); + + assertTrue(first.isEmpty()); + assertNotSame("callers must never share the writer cache instance", first, second); + first.add(null); + assertTrue(second.isEmpty()); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockBreakHandlerTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockBreakHandlerTest.java new file mode 100644 index 000000000..36569bced --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockBreakHandlerTest.java @@ -0,0 +1,41 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.StairBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ModdedBlockBreakHandlerTest { + @Test + public void exactMatchingIncludesPropertiesWhileTypeMatchingDoesNot() { + BlockState north = Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING, Direction.NORTH); + BlockState east = Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING, Direction.EAST); + + assertTrue(ModdedBlockBreakHandler.matchesState(north, north, true)); + assertFalse(ModdedBlockBreakHandler.matchesState(north, east, true)); + assertTrue(ModdedBlockBreakHandler.matchesState(north, east, false)); + assertFalse(ModdedBlockBreakHandler.matchesState(north, Blocks.COBBLESTONE.defaultBlockState(), false)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java new file mode 100644 index 000000000..ce3613743 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java @@ -0,0 +1,63 @@ +package art.arcane.iris.modded; + +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.block.Blocks; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * The SPI splits block resolution into a null-returning lookup and an air-falling-back lookup. Modded used to + * collapse both onto air, so an unknown key produced no output at all. + */ +public class ModdedBlockResolutionContractTest { + private static final String UNKNOWN = "minecraft:definitely_not_a_real_block"; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void getOrNullReturnsNullForUnknownKey() { + assertNull(ModdedBlockResolution.getOrNull(UNKNOWN)); + assertNull(ModdedBlockResolution.getOrNull(UNKNOWN, true)); + } + + @Test + public void getFallsBackToAirForUnknownKey() { + ModdedBlockState state = ModdedBlockResolution.get(UNKNOWN); + assertNotNull(state); + assertEquals(Blocks.AIR, state.handle().getBlock()); + } + + @Test + public void getOrNullResolvesKnownKeyWithProperties() { + ModdedBlockState state = ModdedBlockResolution.getOrNull("minecraft:oak_log[axis=x]", true); + assertNotNull(state); + assertEquals(Blocks.OAK_LOG, state.handle().getBlock()); + } + + @Test + public void unknownPropertyFallsBackToDefaultState() { + ModdedBlockState state = ModdedBlockResolution.getOrNull("minecraft:oak_log[not_a_property=x]", true); + assertNotNull(state); + assertEquals(Blocks.OAK_LOG, state.handle().getBlock()); + } + + @Test + public void cactusPlacementRequiresNativeCactusSupport() { + assertTrue(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.CACTUS)); + assertTrue(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.SAND)); + assertTrue(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.RED_SAND)); + assertFalse(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.STONE)); + assertTrue(ModdedBlockResolution.isDecorant(Blocks.CACTUS.defaultBlockState())); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedClientPackageIsolationTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedClientPackageIsolationTest.java new file mode 100644 index 000000000..8b4801420 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedClientPackageIsolationTest.java @@ -0,0 +1,103 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Dist gate. The loader source sets fold modded-common, minecraft-common and client-common into one output, so + * nothing at compile time stops a server-side class from touching a client-only type. On a dedicated server + * that is a NoClassDefFoundError at the first call, usually deep inside worldgen. + * + *

The check is a constant-pool byte scan for the internal (slash) form of the forbidden packages. Slash form + * is the point: a real type reference - supertype, field or local descriptor, method owner, class literal - is + * always stored slash-separated, while a reflective lookup keeps the class name as a dotted string constant. + * {@link ModdedMixinAudit} depends on that distinction: it names client mixin targets as dotted strings on + * purpose so it can audit them from a dedicated server, and this gate must not flag it. + * + *

Same style as the core purity gate: read the bytes, do not load the class. Loading is what the gate is + * trying to prove is safe. + */ +public class ModdedClientPackageIsolationTest { + private static final String CLIENT_MINECRAFT = "net/minecraft/client/"; + private static final String CLIENT_IRIS = "art/arcane/iris/client/"; + private static final List GUARDED_PACKAGES = List.of( + "art/arcane/iris/modded", + "art/arcane/iris/nativegen"); + private static final int MINIMUM_SCANNED = 50; + + @Test + public void serverSidePackagesNeverReferenceClientOnlyTypes() throws IOException, URISyntaxException { + Path root = classesRoot(); + List violations = new ArrayList<>(); + int scanned = 0; + for (String guarded : GUARDED_PACKAGES) { + Path directory = root.resolve(guarded); + if (!Files.isDirectory(directory)) { + continue; + } + try (Stream walk = Files.walk(directory)) { + for (Path classFile : walk.filter(ModdedClientPackageIsolationTest::isClassFile).toList()) { + scanned++; + String bytecode = readAsLatin1(classFile); + String name = root.relativize(classFile).toString(); + if (bytecode.contains(CLIENT_MINECRAFT)) { + violations.add(name + " -> " + CLIENT_MINECRAFT); + } + if (bytecode.contains(CLIENT_IRIS)) { + violations.add(name + " -> " + CLIENT_IRIS); + } + } + } + } + assertTrue("guarded packages produced only " + scanned + " classes; the scan root is wrong", + scanned >= MINIMUM_SCANNED); + assertEquals("server-side classes referencing client-only types: " + violations, + List.of(), violations); + } + + /** + * Positive control. If the needle ever stops matching - a package rename, a scan that reads the wrong + * bytes - the gate above would pass silently forever. A known client-tainted class must still trip it. + */ + @Test + public void scanDetectsClientReferencesInAKnownClientClass() throws IOException, URISyntaxException { + Path visionScreen = classesRoot().resolve("art/arcane/iris/client/IrisVisionScreen.class"); + assertTrue("IrisVisionScreen.class missing from " + visionScreen, Files.isRegularFile(visionScreen)); + assertTrue("scan needle no longer matches a known client class", + readAsLatin1(visionScreen).contains(CLIENT_MINECRAFT)); + } + + private static boolean isClassFile(Path path) { + return Files.isRegularFile(path) && path.getFileName().toString().endsWith(".class"); + } + + private static String readAsLatin1(Path path) throws IOException { + return new String(Files.readAllBytes(path), StandardCharsets.ISO_8859_1); + } + + private static Path classesRoot() throws URISyntaxException { + String anchor = "/art/arcane/iris/modded/ModdedMixinFlags.class"; + URL located = ModdedClientPackageIsolationTest.class.getResource(anchor); + assertNotNull("compiled main classes are not on the test classpath as files", located); + assertEquals("expected a directory classpath entry, got " + located, "file", located.getProtocol()); + Path root = Path.of(located.toURI()); + for (int depth = 0; depth < 5; depth++) { + root = root.getParent(); + assertNotNull("walked past the classpath root resolving " + anchor, root); + } + return root; + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDeferredBlockParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDeferredBlockParityTest.java new file mode 100644 index 000000000..943706fe6 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDeferredBlockParityTest.java @@ -0,0 +1,76 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.modded.api.ModdedBlockData; +import art.arcane.iris.modded.api.ModdedCustomContentRegistry; +import art.arcane.iris.modded.api.ModdedDataProvider; +import art.arcane.iris.modded.api.ModdedDataType; +import art.arcane.iris.spi.PlatformBlockState; +import net.minecraft.SharedConstants; +import net.minecraft.resources.Identifier; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.StairBlock; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ModdedDeferredBlockParityTest { + private static final Identifier BLOCK_ID = Identifier.parse("iris_deferred_test:oak_stairs"); + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + ModdedCustomContentRegistry.register(new DeferredProvider()); + } + + @Test + public void deferredProviderStateCarriesPlacementMetadataThroughMutation() { + ModdedBlockState resolved = ModdedBlockResolution.getOrNull( + "iris_deferred_test:oak_stairs[facing=north,half=bottom]"); + + assertNotNull(resolved); + assertTrue(resolved.isCustom()); + assertEquals("iris_deferred_test:oak_stairs[facing=north,half=bottom]", resolved.deferredPlacementKey()); + assertEquals(Blocks.OAK_STAIRS, resolved.handle().getBlock()); + + PlatformBlockState mutated = resolved.withProperty("facing", "west"); + assertTrue(mutated.isCustom()); + assertEquals(resolved.deferredPlacementKey(), mutated.deferredPlacementKey()); + assertEquals("west", ((ModdedBlockState) mutated).handle().getValue(StairBlock.FACING).getName()); + + PlatformBlockState base = mutated.placementBaseState(); + assertFalse(base.isCustom()); + assertEquals(Blocks.OAK_STAIRS, ((ModdedBlockState) base).handle().getBlock()); + } + + private static final class DeferredProvider implements ModdedDataProvider { + @Override + public String modId() { + return "iris_deferred_test"; + } + + @Override + public Collection getTypes(ModdedDataType type) { + return type == ModdedDataType.BLOCK ? List.of(BLOCK_ID) : List.of(); + } + + @Override + public boolean isValidProvider(Identifier id, ModdedDataType type) { + return type == ModdedDataType.BLOCK && BLOCK_ID.equals(id); + } + + @Override + public ModdedBlockData getBlockData(Identifier blockId, Map state) { + return ModdedBlockData.deferred(Blocks.OAK_STAIRS.defaultBlockState()); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionRegistryStoreTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionRegistryStoreTest.java new file mode 100644 index 000000000..9e52f4bf7 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionRegistryStoreTest.java @@ -0,0 +1,140 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +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 ModdedDimensionRegistryStoreTest { + @Test + public void registryRoundTripsPersistentDimensions() throws IOException { + Path root = Files.createTempDirectory("iris-dimension-registry"); + Path file = root.resolve("iris-dimensions.json"); + try { + List expected = List.of( + new ModdedDimensionRegistryStore.PersistentDimension( + "iris:first", "overworld", "overworld", 42L), + new ModdedDimensionRegistryStore.PersistentDimension( + "iris:second", "other", "surface", -9L)); + + ModdedDimensionRegistryStore.write(file, expected); + + assertEquals(expected, ModdedDimensionRegistryStore.load(file)); + } finally { + Files.deleteIfExists(file); + Files.deleteIfExists(root); + } + } + + @Test + public void malformedEntryDoesNotDiscardHealthyEntries() throws IOException { + Path root = Files.createTempDirectory("iris-dimension-registry-partial"); + Path file = root.resolve("iris-dimensions.json"); + try { + Files.writeString(file, """ + { + "dimensions": [ + {"id":"iris:good","pack":"overworld","dimension":"overworld","seed":7}, + {"id":"iris:broken","pack":"overworld"} + ] + } + """, StandardCharsets.UTF_8); + + assertEquals(List.of(new ModdedDimensionRegistryStore.PersistentDimension( + "iris:good", "overworld", "overworld", 7L)), + ModdedDimensionRegistryStore.load(file)); + } finally { + Files.deleteIfExists(file); + Files.deleteIfExists(root); + } + } + + @Test + public void truncatedRegistryNeverBecomesAnEmptySuccessfulLoad() throws IOException { + Path root = Files.createTempDirectory("iris-dimension-registry-truncated"); + Path file = root.resolve("iris-dimensions.json"); + try { + Files.writeString(file, "{\"dimensions\":[", StandardCharsets.UTF_8); + + assertThrows(IllegalStateException.class, + () -> ModdedDimensionRegistryStore.load(file)); + } finally { + Files.deleteIfExists(file); + Files.deleteIfExists(root); + } + } + + @Test + public void missingDimensionsArrayNeverBecomesAnEmptySuccessfulLoad() throws IOException { + Path root = Files.createTempDirectory("iris-dimension-registry-missing-root"); + Path file = root.resolve("iris-dimensions.json"); + try { + Files.writeString(file, "{}", StandardCharsets.UTF_8); + + assertThrows(IllegalStateException.class, + () -> ModdedDimensionRegistryStore.load(file)); + } finally { + Files.deleteIfExists(file); + Files.deleteIfExists(root); + } + } + + @Test + public void startupLoadQuarantinesACorruptRegistryInsteadOfFailingBoot() throws IOException { + Path root = Files.createTempDirectory("iris-dimension-registry-corrupt-boot"); + Path file = root.resolve("iris-dimensions.json"); + try { + Files.writeString(file, "{\"dimensions\":[{\"id\":\"iris:lost\",", StandardCharsets.UTF_8); + + assertEquals(List.of(), ModdedDimensionRegistryStore.loadForStartup(file)); + assertFalse(Files.exists(file)); + + try (Stream entries = Files.list(root)) { + assertTrue(entries.anyMatch((Path entry) -> + entry.getFileName().toString().startsWith("iris-dimensions.json.broken-"))); + } + } finally { + deleteTree(root); + } + } + + @Test + public void startupLoadReturnsHealthyEntriesUntouched() throws IOException { + Path root = Files.createTempDirectory("iris-dimension-registry-healthy-boot"); + Path file = root.resolve("iris-dimensions.json"); + try { + List expected = List.of( + new ModdedDimensionRegistryStore.PersistentDimension( + "iris:first", "overworld", "overworld", 42L)); + ModdedDimensionRegistryStore.write(file, expected); + + assertEquals(expected, ModdedDimensionRegistryStore.loadForStartup(file)); + assertTrue(Files.exists(file)); + } finally { + deleteTree(root); + } + } + + private static void deleteTree(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + List entries; + try (Stream walk = Files.walk(root)) { + entries = walk.sorted(Comparator.reverseOrder()).toList(); + } + for (Path entry : entries) { + Files.deleteIfExists(entry); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java new file mode 100644 index 000000000..9bb4f97a4 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java @@ -0,0 +1,196 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.core.nms.datapack.IDataFixer; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionRuntimeContract; +import art.arcane.iris.engine.object.IrisDimensionTypeOptions; +import art.arcane.iris.engine.object.IrisEnvironment; +import art.arcane.iris.engine.object.IrisRange; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.json.JSONObject; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.FALSE; +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.TRUE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class ModdedDimensionTypeParityTest { + @Test + public void everyPackDimensionUsesItsExactTypeReference() { + IrisDimension overworld = dimension("overworld", IrisEnvironment.NORMAL, -64, 320, 320, new IrisDimensionTypeOptions()); + IrisDimension nether = dimension("nether", IrisEnvironment.NETHER, 0, 256, 256, new IrisDimensionTypeOptions()); + IrisDimension end = dimension("the_end", IrisEnvironment.THE_END, 0, 256, 256, new IrisDimensionTypeOptions()); + + assertEquals("irisworldgen:packs/6f766572776f726c64/dimensions/6f766572776f726c64/dimension_type", + ModdedWorldgenIds.dimensionTypeRef("overworld", overworld.getLoadKey())); + assertEquals("irisworldgen:packs/6e6574686572/dimensions/6e6574686572/dimension_type", + ModdedWorldgenIds.dimensionTypeRef("nether", nether.getLoadKey())); + assertEquals("irisworldgen:packs/7468655f656e64/dimensions/7468655f656e64/dimension_type", + ModdedWorldgenIds.dimensionTypeRef("the_end", end.getLoadKey())); + assertNotEquals(ModdedWorldgenIds.dimensionTypeRef("first", "overworld"), + ModdedWorldgenIds.dimensionTypeRef("second", "overworld")); + } + + @Test + public void writesExactOverworldNetherEndAndCustomContracts() throws IOException { + IrisDimensionTypeOptions customOptions = new IrisDimensionTypeOptions() + .coordinateScale(3.5D) + .ambientLight(0.4F) + .skylight(FALSE) + .ceiling(TRUE); + IrisDimension overworld = dimension("overworld", IrisEnvironment.NORMAL, -64, 320, 320, new IrisDimensionTypeOptions()); + IrisDimension nether = dimension("nether", IrisEnvironment.NETHER, 0, 256, 256, new IrisDimensionTypeOptions()); + IrisDimension end = dimension("the_end", IrisEnvironment.THE_END, 0, 256, 256, new IrisDimensionTypeOptions()); + IrisDimension custom = dimension("custom_contract", IrisEnvironment.CUSTOM, -128, 384, 384, customOptions); + List dimensions = List.of(overworld, nether, end, custom); + IDataFixer fixer = DataVersion.getLatest().get(); + Path packDirectory = Files.createTempDirectory("iris-dimension-contracts"); + KList roots = new KList<>(); + roots.add(packDirectory.toFile()); + try { + for (IrisDimension dimension : dimensions) { + ModdedForcedDatapack.writeDimensionType( + roots, fixer, dimension, "contracts", dimension.getLoadKey()); + Path output = typeFile(packDirectory, "contracts", dimension); + assertTrue(Files.isRegularFile(output)); + assertEquals(dimension.getDimensionType().toJson(fixer), + Files.readString(output, StandardCharsets.UTF_8)); + } + + JSONObject overworldJson = readType(packDirectory, "contracts", overworld); + JSONObject netherJson = readType(packDirectory, "contracts", nether); + JSONObject endJson = readType(packDirectory, "contracts", end); + JSONObject customJson = readType(packDirectory, "contracts", custom); + + assertTrue(overworldJson.getBoolean("has_skylight")); + assertFalse(overworldJson.getBoolean("has_ceiling")); + assertEquals(1D, overworldJson.getDouble("coordinate_scale"), 0D); + assertFalse(netherJson.getBoolean("has_skylight")); + assertTrue(netherJson.getBoolean("has_ceiling")); + assertEquals(8D, netherJson.getDouble("coordinate_scale"), 0D); + assertTrue(endJson.getBoolean("has_ender_dragon_fight")); + assertFalse(endJson.getBoolean("has_skylight")); + assertEquals(-128, customJson.getInt("min_y")); + assertEquals(512, customJson.getInt("height")); + assertEquals(384, customJson.getInt("logical_height")); + assertEquals(3.5D, customJson.getDouble("coordinate_scale"), 0D); + assertEquals(0.4D, customJson.getDouble("ambient_light"), 0.000001D); + assertFalse(customJson.getBoolean("has_skylight")); + assertTrue(customJson.getBoolean("has_ceiling")); + } finally { + deleteTree(packDirectory); + } + } + + @Test + public void missingExactDimensionTypeIsRejected() { + try { + ModdedForcedDatapack.requireRegisteredDimensionType( + "irisworldgen:overworld", Optional.empty(), "overworld", "overworld"); + fail("Missing dimension type must be rejected"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("irisworldgen:overworld")); + assertTrue(e.getMessage().contains("Restart the server")); + } + } + + @Test + public void loadedLevelMustMatchExactPackHeightRange() { + IrisDimension dimension = dimension("tall", IrisEnvironment.NORMAL, -128, 384, 384, new IrisDimensionTypeOptions()); + IrisDimensionRuntimeContract contract = IrisDimensionRuntimeContract.expected(dimension, "irisworldgen"); + + contract.requireHeight("test level", -128, 512); + + try { + contract.requireHeight("test level", -256, 768); + fail("Oversized fallback height must be rejected"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("terrain clipping is not allowed")); + } + } + + @Test + public void worldCheckRejectsFallbackHeightAndSemantics() { + IrisDimensionTypeOptions options = new IrisDimensionTypeOptions() + .coordinateScale(2.5D) + .ambientLight(0.3F) + .skylight(FALSE) + .ceiling(TRUE); + IrisDimension dimension = dimension("runtime_contract", IrisEnvironment.CUSTOM, + -128, 384, 384, options); + WorldCheckDimensionContract.DimensionContract expected = WorldCheckDimensionContract.expectedDimensionContract(dimension); + WorldCheckDimensionContract.DimensionContract fallback = new WorldCheckDimensionContract.DimensionContract( + -256, 768, 512, 1D, 0F, true, false, false, 0); + + assertTrue(WorldCheckDimensionContract.matchesDimensionContract(-128, 512, expected, expected)); + assertFalse(WorldCheckDimensionContract.matchesDimensionContract(-256, 768, expected, fallback)); + assertFalse(WorldCheckDimensionContract.matchesDimensionContract(-128, 512, expected, fallback)); + } + + private static IrisDimension dimension(String key, IrisEnvironment environment, int minY, int maxY, + int logicalHeight, IrisDimensionTypeOptions options) { + IrisDimension dimension = new IrisDimension(); + dimension.setLoadKey(key); + dimension.setEnvironment(environment); + dimension.setDimensionHeight(new IrisRange(minY, maxY)); + dimension.setLogicalHeight(logicalHeight); + dimension.setDimensionOptions(options); + return dimension; + } + + private static JSONObject readType(Path packDirectory, String pack, + IrisDimension dimension) throws IOException { + return new JSONObject(Files.readString( + typeFile(packDirectory, pack, dimension), StandardCharsets.UTF_8)); + } + + private static Path typeFile(Path packDirectory, String pack, IrisDimension dimension) { + String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, dimension.getLoadKey()); + return packDirectory.resolve("data/irisworldgen/dimension_type/") + .resolve(typeRef.substring(typeRef.indexOf(':') + 1) + ".json"); + } + + private static void deleteTree(Path root) throws IOException { + List paths = new ArrayList<>(); + try (Stream walk = Files.walk(root)) { + walk.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).forEach(paths::add); + } + for (Path path : paths) { + Files.deleteIfExists(path); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEngineEffectsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEngineEffectsTest.java new file mode 100644 index 000000000..c2da0c46b --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEngineEffectsTest.java @@ -0,0 +1,62 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class ModdedEngineEffectsTest { + @Test + public void normalizesRegistryKeys() { + assertNull(ModdedEngineEffects.normalizeRegistryKey(null)); + assertNull(ModdedEngineEffects.normalizeRegistryKey(" ")); + assertEquals("minecraft:block_amethyst_block_chime", + ModdedEngineEffects.normalizeRegistryKey("BLOCK AMETHYST BLOCK CHIME")); + assertEquals("example:custom_sound", + ModdedEngineEffects.normalizeRegistryKey("Example:Custom_Sound")); + } + + @Test + public void normalizesLegacyPotionAliases() { + assertEquals("minecraft:luck", ModdedEngineEffects.normalizePotionEffectKey(null)); + assertEquals("minecraft:slowness", ModdedEngineEffects.normalizePotionEffectKey("SLOW")); + assertEquals("minecraft:mining_fatigue", ModdedEngineEffects.normalizePotionEffectKey("SLOW_DIGGING")); + assertEquals("minecraft:instant_health", ModdedEngineEffects.normalizePotionEffectKey("minecraft:HEAL")); + assertEquals("example:resistance", ModdedEngineEffects.normalizePotionEffectKey("example:DAMAGE_RESISTANCE")); + } + + @Test + public void samplesInitiallyAndAfterMovingPastThreshold() { + assertTrue(ModdedEngineEffects.needsSample(false, 0L, 0.0D)); + assertFalse(ModdedEngineEffects.needsSample(true, 56L, 81.0D)); + assertFalse(ModdedEngineEffects.needsSample(true, 55L, 82.0D)); + assertTrue(ModdedEngineEffects.needsSample(true, 56L, 82.0D)); + } + + @Test + public void preservesOnlyStrictlyStrongerPotionEffects() { + assertFalse(ModdedEngineEffects.shouldReplacePotionEffect(3, 2)); + assertTrue(ModdedEngineEffects.shouldReplacePotionEffect(2, 2)); + assertTrue(ModdedEngineEffects.shouldReplacePotionEffect(1, 2)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntityAwarenessTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntityAwarenessTest.java new file mode 100644 index 000000000..547c020d9 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntityAwarenessTest.java @@ -0,0 +1,35 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ModdedEntityAwarenessTest { + @Test + public void unawareConfigurationAddsThePartialAiTag() { + Set tags = new HashSet<>(); + + ModdedEntityAwareness.configureTags(tags, false); + + assertFalse(ModdedEntityAwareness.isAware(tags)); + } + + @Test + public void awareConfigurationRemovesThePartialAiTag() { + Set tags = new HashSet<>(); + ModdedEntityAwareness.configureTags(tags, false); + + ModdedEntityAwareness.configureTags(tags, true); + + assertTrue(ModdedEntityAwareness.isAware(tags)); + } + + @Test + public void mobsAreAwareWithoutAnIrisTag() { + assertTrue(ModdedEntityAwareness.isAware(Set.of())); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntityCommandRunnerTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntityCommandRunnerTest.java new file mode 100644 index 000000000..7bf576c9e --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntityCommandRunnerTest.java @@ -0,0 +1,30 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ModdedEntityCommandRunnerTest { + @Test + public void commandPreparationRemovesOneLeadingSlashAndExpandsCoordinates() { + String command = ModdedEntityCommandRunner.prepareCommand( + "/summon pig {x} {y} {z} {x}", -12, 65, 99); + + assertEquals("summon pig -12 65 99 -12", command); + } + + @Test + public void blankCommandsAreIgnored() { + assertNull(ModdedEntityCommandRunner.prepareCommand(" ", 0, 0, 0)); + assertNull(ModdedEntityCommandRunner.prepareCommand(null, 0, 0, 0)); + } + + @Test + public void delaysMatchPaperClampingRules() { + assertEquals(0, ModdedEntityCommandRunner.clampDelay(-10L, 0)); + assertEquals(1, ModdedEntityCommandRunner.clampDelay(-10L, 1)); + assertEquals(73, ModdedEntityCommandRunner.clampDelay(73L, 0)); + assertEquals(Integer.MAX_VALUE, ModdedEntityCommandRunner.clampDelay(Long.MAX_VALUE, 0)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntityPersistenceTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntityPersistenceTest.java new file mode 100644 index 000000000..94b0dea05 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntityPersistenceTest.java @@ -0,0 +1,39 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ModdedEntityPersistenceTest { + @Test + public void generatedNonPersistentEntityIsExcludedFromVanillaSaves() { + Set tags = new HashSet<>(); + + ModdedEntityPersistence.configureTags(tags, false); + + assertFalse(ModdedEntityPersistence.shouldSave(tags, true)); + } + + @Test + public void positivePersistenceRemovesTheSaveExclusion() { + Set tags = new HashSet<>(); + ModdedEntityPersistence.configureTags(tags, false); + + ModdedEntityPersistence.configureTags(tags, true); + + assertTrue(ModdedEntityPersistence.shouldSave(tags, true)); + } + + @Test + public void interceptionNeverOverridesVanillaSaveRejection() { + Set tags = new HashSet<>(); + + ModdedEntityPersistence.configureTags(tags, true); + + assertFalse(ModdedEntityPersistence.shouldSave(tags, false)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntitySpawnerParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntitySpawnerParityTest.java new file mode 100644 index 000000000..9c5478b44 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedEntitySpawnerParityTest.java @@ -0,0 +1,165 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.IrisEffect; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.monster.Zoglin; +import net.minecraft.world.entity.monster.piglin.Piglin; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ModdedEntitySpawnerParityTest { + @Test + public void collisionCheckUsesPaperInclusiveIntegerDimensions() { + Set checked = new HashSet<>(); + + boolean clear = ModdedEntitySpawner.isAreaClearForSpawn( + 10, 64, 20, 0.6F, 1.95F, position -> { + checked.add(position); + return true; + }); + + assertTrue(clear); + assertEquals(Set.of(new BlockPos(10, 64, 20), new BlockPos(10, 65, 20)), checked); + } + + @Test + public void collisionCheckCoversWideEntityVolumeAndRejectsOneBlockedCell() { + AtomicInteger checked = new AtomicInteger(); + boolean fullyClear = ModdedEntitySpawner.isAreaClearForSpawn( + 0, 10, 0, 4F, 3.2F, position -> { + checked.incrementAndGet(); + return true; + }); + + assertTrue(fullyClear); + assertEquals(100, checked.get()); + + BlockPos obstruction = new BlockPos(2, 12, -2); + + boolean clear = ModdedEntitySpawner.isAreaClearForSpawn( + 0, 10, 0, 4F, 3.2F, position -> !position.equals(obstruction)); + + assertFalse(clear); + } + + @Test + public void spawnSafetyRequiresTheTargetChunkAndAllEightNeighbors() { + Set loaded = new HashSet<>(); + for (int x = 4; x <= 6; x++) { + for (int z = -3; z <= -1; z++) { + loaded.add(pack(x, z)); + } + } + + assertTrue(ModdedEntitySpawner.allNeighborChunksLoaded(5, -2, + (x, z) -> loaded.contains(pack(x, z)))); + + loaded.remove(pack(4, -3)); + + assertFalse(ModdedEntitySpawner.allNeighborChunksLoaded(5, -2, + (x, z) -> loaded.contains(pack(x, z)))); + } + + @Test + public void onlyAiFalseUsesVanillaNoAi() { + assertFalse(ModdedEntitySpawner.shouldDisableAi(true)); + assertTrue(ModdedEntitySpawner.shouldDisableAi(false)); + } + + @Test + public void piglinAndZoglinUseTheVirtualMobBabyContract() throws NoSuchMethodException { + assertTrue(Mob.class.isAssignableFrom(Piglin.class)); + assertTrue(Mob.class.isAssignableFrom(Zoglin.class)); + assertEquals(Piglin.class, Piglin.class.getMethod("setBaby", boolean.class).getDeclaringClass()); + assertEquals(Zoglin.class, Zoglin.class.getMethod("setBaby", boolean.class).getDeclaringClass()); + } + + @Test + public void everyAcceptedBukkitSpawnReasonHasAnExplicitNmsMapping() { + Map expected = Map.ofEntries( + Map.entry("NATURAL", EntitySpawnReason.NATURAL), + Map.entry("JOCKEY", EntitySpawnReason.JOCKEY), + Map.entry("CHUNK_GEN", EntitySpawnReason.CHUNK_GENERATION), + Map.entry("SPAWNER", EntitySpawnReason.SPAWNER), + Map.entry("TRIAL_SPAWNER", EntitySpawnReason.TRIAL_SPAWNER), + Map.entry("EGG", EntitySpawnReason.TRIGGERED), + Map.entry("SPAWNER_EGG", EntitySpawnReason.SPAWN_ITEM_USE), + Map.entry("LIGHTNING", EntitySpawnReason.EVENT), + Map.entry("BUILD_SNOWMAN", EntitySpawnReason.TRIGGERED), + Map.entry("BUILD_IRONGOLEM", EntitySpawnReason.TRIGGERED), + Map.entry("BUILD_COPPERGOLEM", EntitySpawnReason.TRIGGERED), + Map.entry("BUILD_WITHER", EntitySpawnReason.TRIGGERED), + Map.entry("VILLAGE_DEFENSE", EntitySpawnReason.MOB_SUMMONED), + Map.entry("VILLAGE_INVASION", EntitySpawnReason.EVENT), + Map.entry("BREEDING", EntitySpawnReason.BREEDING), + Map.entry("SLIME_SPLIT", EntitySpawnReason.TRIGGERED), + Map.entry("REINFORCEMENTS", EntitySpawnReason.REINFORCEMENT), + Map.entry("NETHER_PORTAL", EntitySpawnReason.STRUCTURE), + Map.entry("DISPENSE_EGG", EntitySpawnReason.DISPENSER), + Map.entry("INFECTION", EntitySpawnReason.CONVERSION), + Map.entry("CURED", EntitySpawnReason.CONVERSION), + Map.entry("OCELOT_BABY", EntitySpawnReason.BREEDING), + Map.entry("SILVERFISH_BLOCK", EntitySpawnReason.TRIGGERED), + Map.entry("MOUNT", EntitySpawnReason.JOCKEY), + Map.entry("TRAP", EntitySpawnReason.TRIGGERED), + Map.entry("ENDER_PEARL", EntitySpawnReason.TRIGGERED), + Map.entry("SHOULDER_ENTITY", EntitySpawnReason.LOAD), + Map.entry("DROWNED", EntitySpawnReason.CONVERSION), + Map.entry("SHEARED", EntitySpawnReason.CONVERSION), + Map.entry("EXPLOSION", EntitySpawnReason.TRIGGERED), + Map.entry("RAID", EntitySpawnReason.EVENT), + Map.entry("PATROL", EntitySpawnReason.PATROL), + Map.entry("BEEHIVE", EntitySpawnReason.LOAD), + Map.entry("PIGLIN_ZOMBIFIED", EntitySpawnReason.CONVERSION), + Map.entry("SPELL", EntitySpawnReason.MOB_SUMMONED), + Map.entry("FROZEN", EntitySpawnReason.CONVERSION), + Map.entry("METAMORPHOSIS", EntitySpawnReason.CONVERSION), + Map.entry("DUPLICATION", EntitySpawnReason.BREEDING), + Map.entry("COMMAND", EntitySpawnReason.COMMAND), + Map.entry("ENCHANTMENT", EntitySpawnReason.TRIGGERED), + Map.entry("OMINOUS_ITEM_SPAWNER", EntitySpawnReason.TRIGGERED), + Map.entry("BUCKET", EntitySpawnReason.BUCKET), + Map.entry("POTION_EFFECT", EntitySpawnReason.TRIGGERED), + Map.entry("REANIMATE", EntitySpawnReason.TRIGGERED), + Map.entry("REHYDRATION", EntitySpawnReason.BREEDING), + Map.entry("CUSTOM", EntitySpawnReason.EVENT), + Map.entry("DEFAULT", EntitySpawnReason.NATURAL)); + + assertEquals(47, expected.size()); + for (Map.Entry entry : expected.entrySet()) { + assertEquals(entry.getKey(), entry.getValue(), ModdedEntitySpawner.reasonFor(entry.getKey())); + assertEquals(entry.getKey(), entry.getValue(), ModdedEntitySpawner.reasonFor(entry.getKey().toLowerCase())); + } + } + + @Test + public void invalidConfiguredSpawnReasonIntentionallyDefaultsToNatural() { + assertEquals(EntitySpawnReason.NATURAL, ModdedEntitySpawner.reasonFor(null)); + assertEquals(EntitySpawnReason.NATURAL, ModdedEntitySpawner.reasonFor("")); + assertEquals(EntitySpawnReason.NATURAL, ModdedEntitySpawner.reasonFor("not_a_spawn_reason")); + } + + @Test + public void entityEffectExposesNeutralRegistryKeys() { + IrisEffect effect = new IrisEffect() + .setSound("minecraft:block.amethyst_block.chime") + .setParticleEffect("minecraft:flame"); + + assertEquals("minecraft:block.amethyst_block.chime", effect.getSoundKey()); + assertEquals("minecraft:flame", effect.getParticleEffectKey()); + } + + private static long pack(int x, int z) { + return (((long) x) & 0xFFFFFFFFL) | ((((long) z) & 0xFFFFFFFFL) << 32); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java new file mode 100644 index 000000000..6ac12df55 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java @@ -0,0 +1,168 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.volmlib.util.collection.KSet; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ModdedForcedDatapackTest { + @Test + public void packScopedIdsCannotCollideAndHaveReadableLabels() { + String first = ModdedWorldgenIds.presetRef("overworld", "overworld"); + String second = ModdedWorldgenIds.presetRef("other", "overworld"); + + assertFalse(first.equals(second)); + assertEquals("IRIS:Overworld", + ModdedWorldgenIds.displayName(first.substring(first.indexOf(':') + 1))); + assertEquals("IRIS:Other / Overworld", + ModdedWorldgenIds.displayName(second.substring(second.indexOf(':') + 1))); + assertEquals("iris:overworld", ModdedWorldgenIds.generatorIdentity("overworld")); + assertEquals("iris:other/overworld", + ModdedWorldgenIds.generatorIdentity("other:overworld")); + } + + @Test + public void scopesSharedCustomBiomeIdsByNamespace() { + Map> seenBiomes = new LinkedHashMap<>(); + + KSet firstNamespace = ModdedForcedDatapack.biomesForNamespace(seenBiomes, "first_dimension"); + KSet secondNamespace = ModdedForcedDatapack.biomesForNamespace(seenBiomes, "second_dimension"); + + assertTrue(firstNamespace.add("shared_biome")); + assertTrue(secondNamespace.add("shared_biome")); + assertFalse(firstNamespace.add("shared_biome")); + assertNotSame(firstNamespace, secondNamespace); + assertEquals(2, seenBiomes.size()); + } + + @Test + public void writesForgeBlockLootModifierListAndInstance() throws IOException { + Path packDirectory = Files.createTempDirectory("iris-forge-loot-modifier"); + try { + ModdedForcedDatapack.writeForgeBlockLootModifier(packDirectory); + + Path list = packDirectory.resolve("data/forge/loot_modifiers/global_loot_modifiers.json"); + Path modifier = packDirectory.resolve("data/irisworldgen/loot_modifiers/block_drops.json"); + assertTrue(Files.isRegularFile(list)); + assertTrue(Files.isRegularFile(modifier)); + assertEquals("{\n" + + " \"replace\": false,\n" + + " \"entries\": [\"irisworldgen:block_drops\"]\n" + + "}\n", Files.readString(list, StandardCharsets.UTF_8)); + assertEquals("{\n" + + " \"type\": \"irisworldgen:block_drops\",\n" + + " \"conditions\": []\n" + + "}\n", Files.readString(modifier, StandardCharsets.UTF_8)); + } finally { + deleteTree(packDirectory); + } + } + + @Test + public void publishesCompleteStagingDirectoryOverExistingPack() throws IOException { + Path root = Files.createTempDirectory("iris-forced-pack-publish"); + try { + Path published = Files.createDirectory(root.resolve("iris")); + Files.writeString(published.resolve("old.txt"), "old", StandardCharsets.UTF_8); + Path staging = Files.createDirectory(root.resolve("staging")); + Files.writeString(staging.resolve("new.txt"), "new", StandardCharsets.UTF_8); + + ModdedForcedDatapack.publishDirectory(staging, published); + + assertFalse(Files.exists(staging)); + assertFalse(Files.exists(published.resolve("old.txt"))); + assertEquals("new", Files.readString(published.resolve("new.txt"), StandardCharsets.UTF_8)); + assertEquals(List.of("iris"), directoryEntries(root)); + } finally { + deleteTree(root); + } + } + + @Test + public void publishesStagingDirectoryWhenNoPriorPackExists() throws IOException { + Path root = Files.createTempDirectory("iris-forced-pack-first-publish"); + try { + Path published = root.resolve("iris"); + Path staging = Files.createDirectory(root.resolve("staging")); + Files.writeString(staging.resolve("pack.mcmeta"), "first", StandardCharsets.UTF_8); + + ModdedForcedDatapack.publishDirectory(staging, published); + + assertEquals("first", + Files.readString(published.resolve("pack.mcmeta"), StandardCharsets.UTF_8)); + assertEquals(List.of("iris"), directoryEntries(root)); + } finally { + deleteTree(root); + } + } + + @Test + public void restoresPublishedPackWhenAtomicStagingMoveFails() throws IOException { + Path root = Files.createTempDirectory("iris-forced-pack-rollback"); + try { + Path published = Files.createDirectory(root.resolve("iris")); + Files.writeString(published.resolve("pack.mcmeta"), "known-good", StandardCharsets.UTF_8); + Path missingStaging = root.resolve("missing-staging"); + + assertThrows(IOException.class, + () -> ModdedForcedDatapack.publishDirectory(missingStaging, published)); + + assertEquals("known-good", + Files.readString(published.resolve("pack.mcmeta"), StandardCharsets.UTF_8)); + assertEquals(List.of("iris"), directoryEntries(root)); + } finally { + deleteTree(root); + } + } + + private List directoryEntries(Path root) throws IOException { + try (Stream entries = Files.list(root)) { + return entries.map((Path path) -> path.getFileName().toString()) + .sorted() + .toList(); + } + } + + private void deleteTree(Path root) throws IOException { + List paths = new ArrayList<>(); + try (Stream walk = Files.walk(root)) { + walk.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).forEach(paths::add); + } + for (Path path : paths) { + Files.deleteIfExists(path); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedGenerationLeaseContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedGenerationLeaseContractTest.java new file mode 100644 index 000000000..404788225 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedGenerationLeaseContractTest.java @@ -0,0 +1,231 @@ +package art.arcane.iris.modded; + +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 ModdedGenerationLeaseContractTest { + private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources"; + + @Test + public void biomeRuntimeReadsAreGenerationLeased() throws IOException { + String source = source("art/arcane/iris/modded/IrisModdedBiomeSource.java"); + + assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_biome\")")); + assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_visible_biome\")")); + assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_biomes_within\")")); + assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_reachability\")")); + assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_state_biome\")")); + assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_biome_keys\")")); + assertTrue(source.contains("engine == null || engine.isClosed()")); + assertTrue(source.contains("catch (GenerationSessionException e)")); + assertTrue(source.contains("e.isExpectedTeardown()")); + assertTrue(source.contains("Iris structure biome key lookup was rejected during an engine transition")); + assertTrue(source.contains("source.clearCache()")); + } + + @Test + public void terrainAndHeightQueriesShareGenerationSessions() throws IOException { + String source = source("art/arcane/iris/modded/IrisModdedChunkGenerator.java"); + + assertTrue(source.contains("generationEngine.acquireGenerationLease(\"modded_chunk_pipeline\")")); + assertTrue(source.contains("current.acquireGenerationLease(\"modded_base_height\")")); + assertTrue(source.contains("current.acquireGenerationLease(\"modded_base_column\")")); + assertTrue(source.contains("current.acquireGenerationLease(\"modded_configured_biome_keys\")")); + assertTrue(source.contains("generationEngine.isClosing() || e.isExpectedTeardown()")); + String terrain = method(source, "private ChunkAccess generateTerrain("); + int sessionFailure = terrain.indexOf("catch (GenerationSessionException e)"); + int generalFailure = terrain.indexOf("catch (Throwable e)", sessionFailure); + String sessionHandler = terrain.substring(sessionFailure, generalFailure); + assertTrue(sessionHandler.contains("throw new IllegalStateException")); + assertFalse(sessionHandler.contains("return chunk;")); + + String references = method(source, "public void createReferences("); + assertTrue(references.contains("requireGenerationLease(current, \"modded_create_references\")")); + assertTrue(references.contains("IrisContext.open(current, lease.sessionId(), null)")); + assertTrue(references.contains("NativeStructureReferenceRepair.createReferences(")); + assertFalse(references.contains("super.createReferences(level, structureManager, chunk);")); + } + + @Test + public void preservationDereferencesSharedPackData() throws IOException { + String source = source("art/arcane/iris/modded/service/ModdedPreservationService.java"); + int dereference = source.indexOf("public void dereference()"); + int irisData = source.indexOf("IrisData.dereference();", dereference); + int trackedResources = source.indexOf("threads.removeIf", dereference); + + assertTrue(dereference >= 0); + assertTrue(irisData > dereference); + assertTrue(trackedResources > irisData); + } + + @Test + public void hotloadShutdownOnlyTargetsItsOwnWorld() throws IOException { + String source = source("art/arcane/iris/modded/service/ModdedStudioHotloadService.java"); + int shutdown = source.indexOf("public void shutdownPregenerator(Engine engine)"); + int nextMethod = source.indexOf("private boolean throttled", shutdown); + String method = source.substring(shutdown, nextMethod); + + assertTrue(method.contains("ModdedPregenJob.shutdownForWorld(world.identity());")); + assertFalse(method.contains("PregeneratorJob.shutdownInstance();")); + } + + @Test + public void maintenanceOwnsOneGenerationSessionUntilWorkCompletes() throws IOException { + String source = source("art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java"); + String maintain = method(source, "private void maintain(Engine engine, long scheduledAt)"); + + assertTrue(maintain.contains("engine.acquireGenerationLease(\"modded_engine_maintenance\")")); + assertTrue(maintain.contains("IrisContext.open(engine, lease.sessionId(), null)")); + assertTrue(maintain.contains("exception.isExpectedTeardown()")); + assertTrue(source.contains("active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)")); + } + + @Test + public void packDownloadAdmissionPrecedesAsyncDispatch() throws IOException { + String source = source("art/arcane/iris/modded/command/IrisModdedCommands.java"); + String download = method(source, "static int download(CommandSourceStack source, String rawRequest)"); + int admission = download.indexOf("LifecycleOperationCoordinator.get().acquire("); + int executionTracking = download.indexOf("new PackDownloadExecution("); + int dispatch = download.indexOf("scheduler.asyncIfRunning(execution, execution::cancel)"); + + assertTrue(admission >= 0); + assertTrue(executionTracking > admission); + assertTrue(dispatch > admission); + assertTrue(download.contains("execution.cancel();")); + + String execution = method(source, "private static void executeDownload("); + assertTrue(execution.contains("PackDownloader.DownloadCancellation cancellation")); + assertTrue(execution.contains("catch (PackDownloader.PackDownloadCancelledException error)")); + assertTrue(execution.contains("dispatchDownloadFeedback(source,")); + assertFalse(execution.contains("scheduler.global(")); + assertFalse(execution.contains("lease.close();")); + + String feedback = method(source, "private static void dispatchDownloadFeedback("); + assertTrue(feedback.contains("source.getServer().execute(feedback);")); + } + + @Test + public void packDownloadsDrainBeforeModdedSchedulerShutdown() throws IOException { + String commands = source("art/arcane/iris/modded/command/IrisModdedCommands.java"); + String shutdownDownloads = method(commands, "public static void shutdownDownloads()"); + assertTrue(shutdownDownloads.contains("downloadAdmissionOpen = false;")); + assertTrue(shutdownDownloads.contains("execution.cancel();")); + assertTrue(shutdownDownloads.contains("execution.await(")); + + String bootstrap = source("art/arcane/iris/modded/ModdedEngineBootstrap.java"); + String stop = method(bootstrap, "public static void stop()"); + int downloads = stop.indexOf("IrisModdedCommands::shutdownDownloads"); + int scheduler = stop.indexOf("scheduler::shutdown"); + assertTrue(downloads >= 0); + assertTrue(scheduler > downloads); + } + + @Test + public void moddedSchedulerRejectsAndCancelsAbandonedDownloadSubmissions() throws IOException { + String scheduler = source("art/arcane/iris/modded/ModdedScheduler.java"); + String dispatch = method(scheduler, "public boolean asyncIfRunning(Runnable task, Runnable rejection)"); + assertTrue(dispatch.contains("RejectionAwareTask")); + assertTrue(dispatch.contains("Objects.requireNonNull(rejection")); + + String shutdown = method(scheduler, "public void shutdown()"); + assertTrue(shutdown.contains("asyncExecutor.shutdownNow()")); + assertTrue(shutdown.contains("rejectAbandonedTasks(abandonedTasks);")); + + String reset = method(scheduler, "public void reset()"); + assertTrue(reset.contains("asyncExecutor.getQueue().drainTo(abandonedTasks);")); + assertTrue(reset.contains("rejectAbandonedTasks(abandonedTasks);")); + + String rejection = method(scheduler, "private static void rejectAbandonedTasks("); + assertTrue(rejection.contains("rejectionAwareTask.reject();")); + } + + @Test + public void blockingPregenShutdownDefersItsFinalSaveToTheServerThread() throws IOException { + String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java"); + String shutdown = method(jobSource, "private static boolean shutdownAndSave("); + assertTrue(shutdown.contains("deferFinalSaveToServerThread()")); + assertTrue(shutdown.contains("completeDeferredFinalSave()")); + assertTrue(shutdown.contains("cancelDeferredFinalSave()")); + + String methodSource = source("art/arcane/iris/modded/command/ModdedPregenMethod.java"); + String close = method(methodSource, "public void close()"); + assertTrue(close.contains("deferFinalSaveIfRequested()")); + + String await = method(methodSource, "private void awaitFinalSave("); + assertTrue(await.contains("FINAL_SAVE_POLL_MILLIS")); + assertTrue(await.contains("deferFinalSaveIfRequested()")); + + String complete = method(methodSource, "void completeDeferredFinalSave()"); + int cancel = complete.indexOf("cancelQueuedFinalSave();"); + int directSave = complete.indexOf("saveLevelOnServerThread();"); + assertTrue(cancel >= 0); + assertTrue(directSave > cancel); + + String queuedSave = method(methodSource, "private void executeFinalSave("); + assertTrue(queuedSave.contains("if (!request.claim())")); + String cancelRequest = method(methodSource, "private void cancelQueuedFinalSave()"); + assertTrue(cancelRequest.contains("request.cancel();")); + String pending = method(methodSource, "boolean hasPendingFinalSave()"); + assertTrue(pending.contains("queuedFinalSave.get() != null")); + } + + @Test + public void firstPregenChunkFailureRetainsItsFullCause() throws IOException { + String methodSource = source("art/arcane/iris/modded/command/ModdedPregenMethod.java"); + String logFailure = method(methodSource, "private void logChunkFailure("); + + assertTrue(methodSource.contains("AtomicBoolean failureDetailLogged")); + assertTrue(logFailure.contains("unwrap(failure)")); + assertTrue(logFailure.contains("failureDetailLogged.compareAndSet(false, true)")); + assertTrue(logFailure.contains("x, z, cause);")); + assertTrue(logFailure.contains("cause.toString()")); + } + + @Test + public void invalidPregenBoundsFailBeforeRuntimeMutation() throws IOException { + String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java"); + String start = method(jobSource, "public static boolean start("); + int taskConstruction = start.indexOf("PregenTask task = PregenTask.builder()"); + int profileMutation = start.indexOf("PregenPerformanceProfile.apply(engine);"); + + assertTrue(taskConstruction >= 0); + assertTrue(profileMutation > taskConstruction); + + String commandSource = source("art/arcane/iris/modded/command/ModdedPregenCommands.java"); + String command = method(commandSource, "static int pregenStart("); + assertTrue(command.contains("catch (IllegalArgumentException failure)")); + assertTrue(command.contains("IrisModdedCommands.fail(source, failure.getMessage())")); + } + + private static String source(String relativePath) throws IOException { + String root = System.getProperty(SOURCE_ROOT_PROPERTY); + assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY, root != null && !root.isBlank()); + return Files.readString(Path.of(root, relativePath)); + } + + private static String method(String source, String signature) { + int start = source.indexOf(signature); + assertTrue("Missing source contract signature: " + signature, start >= 0); + int openBrace = source.indexOf('{', start); + assertTrue("Missing source contract method body: " + signature, openBrace >= 0); + int depth = 0; + for (int index = openBrace; index < source.length(); index++) { + char current = source.charAt(index); + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return source.substring(start, index + 1); + } + } + } + throw new IllegalArgumentException("Unclosed source contract method: " + signature); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedIrisLogLevelCoverageTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedIrisLogLevelCoverageTest.java new file mode 100644 index 000000000..19a317138 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedIrisLogLevelCoverageTest.java @@ -0,0 +1,27 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertTrue; + +/** + * The level router is a switch statement, which the compiler does not check for exhaustiveness. A level added + * to {@link art.arcane.iris.spi.LogLevel} with no arm here would compile and then drop every message at that + * level without a trace, so the fallback arm is what keeps a new level visible on the modded loaders. + */ +public class ModdedIrisLogLevelCoverageTest { + @Test + public void everyLogLevelReachesTheLoggerIncludingOnesAddedLater() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.moddedCommonSources"), + "art/arcane/iris/modded/ModdedIrisLog.java")); + int router = source.indexOf("public static void log(LogLevel level, String message)"); + assertTrue("log(LogLevel, String) not found", router >= 0); + + String body = source.substring(router, source.indexOf("public static void debug(", router)); + assertTrue(body, body.contains("default -> info(message);")); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLifecycleFailureContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLifecycleFailureContractTest.java new file mode 100644 index 000000000..d2fe583be --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLifecycleFailureContractTest.java @@ -0,0 +1,421 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ModdedLifecycleFailureContractTest { + private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources"; + + @Test + public void existingDimensionRepointBindsBeforeHandlePublication() throws IOException { + String managerSource = source("ModdedDimensionManager.java"); + String create = method(managerSource, "public static Handle create("); + int existingStart = requiredIndex(create, "if (existing != null"); + int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1); + int injectionStart = requiredIndex(create, "try {", presentStart); + + assertRepointBeforePublication(create.substring(existingStart, presentStart)); + assertRepointBeforePublication(create.substring(presentStart, injectionStart)); + + String generatorSource = source("IrisModdedChunkGenerator.java"); + String repointAndBind = method(generatorSource, "void repointAndBind("); + assertBefore(repointAndBind, "ModdedWorldEngines.prepareReplacement(", + "ModdedWorldEngines.installReplacement("); + assertBefore(repointAndBind, "ModdedWorldEngines.installReplacement(", "this.activePack = pack;"); + String failure = catchBlock(repointAndBind); + assertTrue(failure.contains("ModdedWorldEngines.closeUnregistered(replacement);")); + assertTrue(failure.contains("addSuppressed(")); + + String worldEnginesSource = source("ModdedWorldEngines.java"); + String installReplacement = method(worldEnginesSource, "static void installReplacement("); + assertTrue(installReplacement.contains("ENGINES.compute(")); + assertBefore(installReplacement, "close(current);", "return activeReplacement;"); + assertFalse(installReplacement.contains("catch (")); + } + + @Test + public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException { + String source = source("ModdedDimensionManager.java"); + String injection = method(source, "private static Handle inject("); + + assertBefore(injection, "generator.bindLevel(level);", "serverAccess.putLevelIfAbsent(server, key, level);"); + assertBefore(injection, "serverAccess.putLevelIfAbsent(server, key, level);", + "server.getPlayerList().addWorldborderListener(level);"); + assertFalse(injection.contains("serverAccess.putLevel(server, key, previous);")); + String failure = catchBlock(injection); + assertBefore(failure, "rollbackInjection(", "throw "); + + String rollback = method(source, "void rollbackInjection("); + assertBefore(rollback, "serverAccess.hasLevel(server, key)", "serverAccess.removeLevel(server, key);"); + assertTrue(rollback.contains("generator.unbindEngine(")); + assertTrue(rollback.contains("level.close();")); + assertTrue(rollback.contains("addSuppressed(")); + } + + @Test + public void persistentReinjectionQuarantinesBrokenEntriesAndContinues() throws IOException { + String source = source("ModdedStartup.java"); + String reinjection = method(source, "private static void reinjectPersistentDimensions("); + String failure = catchBlock(reinjection); + + assertTrue(failure.contains("LOGGER.error(")); + assertFalse(failure.contains("e.toString()")); + assertFalse(failure.contains("throw new IllegalStateException(")); + assertTrue(reinjection.contains("injected++;")); + } + + @Test + public void persistentReinjectionWaitsForPlayerServicesAndServerStarted() throws IOException { + String bootstrapSource = source("ModdedEngineBootstrap.java"); + String start = method(bootstrapSource, "public static void start("); + String serverAboutToStart = method(bootstrapSource, "public static void serverAboutToStart("); + String serverStarted = method(bootstrapSource, "public static void serverStarted("); + + assertFalse(start.contains("ModdedStartup.runOnce(server);")); + assertFalse(serverAboutToStart.contains("ModdedStartup.runOnce(server);")); + assertEquals(1, occurrences(serverStarted, "ModdedStartup.runOnce(server);")); + assertBefore(serverStarted, "bindWorldGenerators(server);", "ModdedStartup.runOnce(server);"); + assertBefore(serverStarted, "ModdedStartup.runOnce(server);", "reconcileSpawn(server);"); + + String startupSource = source("ModdedStartup.java"); + String runOnce = method(startupSource, "public static void runOnce("); + assertBefore(runOnce, "server.getPlayerList() == null", "STARTED.compareAndSet(false, true)"); + } + + @Test + public void dynamicLevelMutationsInvalidateLoaderTickCaches() throws IOException { + String levelsSource = source("ModdedServerLevels.java"); + String put = method(levelsSource, "public ServerLevel putLevel("); + String putIfAbsent = method(levelsSource, "public ServerLevel putLevelIfAbsent("); + String remove = method(levelsSource, "public ServerLevel removeLevel("); + + assertBefore(put, "server.levels.put(key, level);", "levelCacheInvalidator.accept(server);"); + assertTrue(put.contains("if (previous != level)")); + assertBefore(putIfAbsent, "server.levels.putIfAbsent(key, level);", "levelCacheInvalidator.accept(server);"); + assertTrue(putIfAbsent.contains("if (previous == null)")); + assertBefore(remove, "server.levels.remove(key);", "levelCacheInvalidator.accept(server);"); + assertTrue(remove.contains("if (removed != null)")); + + String loaderSource = source("ModdedLoader.java"); + assertTrue(loaderSource.contains("void invalidateLevelCache(MinecraftServer server);")); + String bootstrapSource = source("ModdedEngineBootstrap.java"); + assertTrue(bootstrapSource.contains("new ModdedServerLevels(boundLoader::invalidateLevelCache)")); + } + + @Test + public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException { + String source = source("ModdedDimensionManager.java"); + String loading = method(source, "private static IrisDimension loadPackDimension("); + + assertFalse(loading.contains("return null;")); + assertFalse(loading.contains("e.toString()")); + int catchIndex = loading.indexOf("catch ("); + if (catchIndex >= 0) { + String failure = blockAt(loading, requiredIndex(loading, "{", catchIndex)); + assertTrue(failure.contains("throw ")); + assertTrue(failure.contains(", e);") || failure.contains("throw e;")); + } + } + + @Test + public void productionPackResolutionNeverUsesTheParityProbeAsData() throws IOException { + String source = source("ModdedWorldEngines.java"); + String resolvePack = method(source, "static File resolvePack("); + + assertFalse(source.contains("System.getProperty(\"iris.parity\")")); + assertFalse(source.contains("parityPack")); + assertFalse(source.contains("falling back to parity pack")); + assertTrue(resolvePack.contains("throw new IllegalStateException(")); + } + + @Test + public void engineBootstrapResetsStartupExactlyOnceDuringStop() throws IOException { + String source = source("ModdedEngineBootstrap.java"); + String stop = method(source, "public static void stop("); + + assertEquals(1, occurrences(stop, "ModdedStartup::reset")); + } + + @Test + public void engineBootstrapAttemptsEveryShutdownStageAndAggregatesFailures() throws IOException { + String source = source("ModdedEngineBootstrap.java"); + String stop = method(source, "public static void stop("); + + assertBefore(stop, "\"services\"", "\"world engines\""); + assertBefore(stop, "\"world engines\"", "\"dimension manager\""); + assertBefore(stop, "\"server state\"", "if (failure != null)"); + assertFalse(stop.contains("throw")); + assertTrue(stop.contains("LOGGER.error(\"Iris modded shutdown completed with failures\", failure);")); + + String runStage = method(source, "private static Throwable runStopStage("); + assertTrue(runStage.contains("catch (Throwable stageFailure)")); + assertTrue(runStage.contains("failure.addSuppressed(stageFailure);")); + } + + @Test + public void worldShutdownRemovesOnlySuccessfullyClosedMappings() throws IOException { + String source = source("ModdedWorldEngines.java"); + String shutdown = method(source, "public static void shutdown()"); + + assertTrue(shutdown.contains("new ArrayList<>(ENGINES.entrySet())")); + assertFalse(shutdown.contains("ENGINES.clear()")); + // Bound generators must go through unbindEngine (latch unloading, then strict evict) so a + // late chunk-system drain cannot rebuild an engine nothing closes again. + assertTrue(shutdown.contains("generator.unbindEngine(level);")); + assertBefore(shutdown, "close(engine);", "ENGINES.remove(level, engine)"); + assertTrue(shutdown.contains("ENGINES.containsKey(level)")); + assertTrue(shutdown.contains("failure.addSuppressed(e);")); + assertTrue(shutdown.contains("failed mappings were retained")); + } + + @Test + public void engineEvictionRetainsTheMappingUntilCloseSucceeds() throws IOException { + String source = source("ModdedWorldEngines.java"); + String eviction = method(source, "static void evictOrThrow("); + + assertTrue(eviction.contains("ENGINES.computeIfPresent(")); + assertFalse(eviction.contains("ENGINES.remove(")); + assertBefore(eviction, "close(current);", "return null;"); + } + + @Test + public void generatorUnbindRetainsOwnershipWhenEvictionFails() throws IOException { + String source = source("IrisModdedChunkGenerator.java"); + String unbind = method(source, "synchronized void unbindEngine(ServerLevel level)"); + + assertFalse(unbind.contains("finally")); + assertBefore(unbind, "unloading = true;", "ModdedWorldEngines.evictOrThrow(level);"); + assertBefore(unbind, "ModdedWorldEngines.evictOrThrow(level);", "clearEngineBinding();"); + String binding = method(source, "private Engine bindEngine(ServerLevel level)"); + assertTrue(binding.contains("requireBindingAllowed();")); + assertTrue(binding.contains("requireCompletedShutdown(cached);")); + String bindLevel = method(source, "synchronized void bindLevel(ServerLevel level)"); + assertBefore(bindLevel, "requireCompletedShutdown(engine);", "unloading = false;"); + String boundEngine = method(source, "public Engine engineIfBound()"); + assertTrue(boundEngine.contains("unloading || current == null || current.isClosing() || current.isClosed()")); + } + + @Test + public void loaderUnloadSurfacesCloseFailureAndDynamicRemovalUsesStrictEviction() throws IOException { + String bootstrapSource = source("ModdedEngineBootstrap.java"); + String unload = method(bootstrapSource, "public static void levelUnloaded(ServerLevel level)"); + String failure = catchBlock(unload); + assertTrue(failure.contains("LOGGER.error(")); + assertTrue(failure.contains("throw ")); + + String managerSource = source("ModdedDimensionManager.java"); + String remove = method(managerSource, "public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage)"); + assertTrue(remove.contains("ModdedWorldEngines.evictOrThrow(level);")); + assertFalse(remove.contains("ModdedWorldEngines.evict(level);")); + assertTrue(remove.contains("generatorUnbound = true;")); + assertTrue(remove.contains("rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);")); + + String rollback = method(managerSource, "private static void rollbackRemoval("); + assertTrue(rollback.contains("serverAccess.hasLevel(server, key)")); + assertTrue(rollback.contains("generator.bindLevel(level);")); + assertTrue(rollback.contains("failure.addSuppressed(rollbackFailure);")); + assertTrue(rollback.contains("LOGGER.error(")); + } + + @Test + public void engineBootstrapRollsBackEveryPublishedBindingBeforeRethrowing() throws IOException { + String source = source("ModdedEngineBootstrap.java"); + String bind = method(source, "public static ModdedPlatform bind("); + String failure = catchBlock(bind); + + assertBefore(bind, "createdServices.enableAll();", "runtime = new BoundRuntime("); + assertTrue(bind.contains("rollback.add(() -> GuiHost.suppressDesktop(")); + assertTrue(bind.contains("rollback.add(() -> restorePlatform(")); + assertTrue(bind.contains("rollback.add(() -> ModdedDimensionManager.restoreAccess(")); + assertTrue(bind.contains("rollback.add(() -> IrisObjectRotation.restorePlatformRotator(")); + assertTrue(bind.contains("rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger(")); + assertTrue(bind.contains("rollback.add(() -> TileData.restorePlatformReader(")); + assertTrue(bind.contains("rollback.add(() -> TileData.restorePlatformFactory(")); + assertTrue(bind.contains("rollback.add(() -> GuiHost.set(")); + assertTrue(bind.contains("rollback.add(() -> DecoratorPlatformHooks.restore(")); + assertTrue(bind.contains("bindService(PreservationRegistry.class")); + assertTrue(bind.contains("bindService(EngineEffectsProvider.class")); + assertTrue(bind.contains("bindService(EnginePlatformHooks.class")); + assertTrue(bind.contains("bindService(EngineWorldManagerProvider.class")); + assertTrue(bind.contains("rollback.add(customContentDiscovery::rollback);")); + assertBefore(failure, "createdServices.rollback(failure);", "rollback.restore(failure);"); + assertBefore(failure, "rollback.restore(failure);", "throw "); + } + + private static void assertRepointBeforePublication(String branch) { + assertBefore(branch, "repointAndBind(", "new Handle("); + assertBefore(branch, "repointAndBind(", "HANDLES.put("); + assertBefore(branch, "repointAndBind(", "return "); + } + + private static void assertBefore(String source, String first, String second) { + int firstIndex = requiredIndex(source, first); + int secondIndex = requiredIndex(source, second); + assertTrue(first + " must occur before " + second, firstIndex < secondIndex); + } + + private static int requiredIndex(String source, String needle) { + return requiredIndex(source, needle, 0); + } + + private static int requiredIndex(String source, String needle, int start) { + int index = source.indexOf(needle, start); + assertTrue("Missing source contract token: " + needle, index >= 0); + return index; + } + + private static String source(String fileName) throws IOException { + String sourceRoot = System.getProperty(SOURCE_ROOT_PROPERTY); + assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY, + sourceRoot != null && !sourceRoot.isBlank()); + Path sourcePath = Path.of(sourceRoot, "art", "arcane", "iris", "modded", fileName); + return Files.readString(sourcePath); + } + + private static String method(String source, String signature) { + int start = requiredIndex(source, signature); + int openBrace = openingBrace(source, start); + return source.substring(start, matchingBrace(source, openBrace) + 1); + } + + /** + * Returns the block of the shallowest catch in the method, i.e. the one attached to the + * method's outermost try. Textual first-match would return a nested catch instead, and a + * nested catch handles a different failure than the one these contracts pin. + */ + private static String catchBlock(String source) { + int bodyStart = openingBrace(source, 0); + int depth = 0; + int bestDepth = Integer.MAX_VALUE; + int catchIndex = -1; + int i = bodyStart; + while (i < source.length()) { + int skipped = skipNonCode(source, i); + if (skipped > i) { + i = skipped; + continue; + } + char current = source.charAt(i); + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + break; + } + } else if (current == 'c' && depth < bestDepth && source.startsWith("catch (", i)) { + bestDepth = depth; + catchIndex = i; + } + i++; + } + assertTrue("Missing source contract token: catch (", catchIndex >= 0); + return blockAt(source, openingBrace(source, catchIndex)); + } + + private static String blockAt(String source, int openBrace) { + return source.substring(openBrace, matchingBrace(source, openBrace) + 1); + } + + private static int openingBrace(String source, int start) { + int i = start; + while (i < source.length()) { + int skipped = skipNonCode(source, i); + if (skipped > i) { + i = skipped; + continue; + } + if (source.charAt(i) == '{') { + return i; + } + i++; + } + throw new IllegalArgumentException("No source block after offset " + start); + } + + private static int matchingBrace(String source, int openBrace) { + int depth = 0; + int i = openBrace; + while (i < source.length()) { + int skipped = skipNonCode(source, i); + if (skipped > i) { + i = skipped; + continue; + } + char current = source.charAt(i); + if (current == '{') { + depth++; + } else if (current == '}') { + depth--; + if (depth == 0) { + return i; + } + } + i++; + } + throw new IllegalArgumentException("Unclosed source block"); + } + + /** + * Advances past a comment, string, char or text block starting at the index, otherwise returns + * the index unchanged. Comments must be skipped: an apostrophe in prose ("generator's") would + * otherwise open a char literal and swallow the rest of the file. + */ + private static int skipNonCode(String source, int index) { + char current = source.charAt(index); + if (current == '/' && index + 1 < source.length()) { + char next = source.charAt(index + 1); + if (next == '/') { + int end = source.indexOf('\n', index); + return end < 0 ? source.length() : end + 1; + } + if (next == '*') { + int end = source.indexOf("*/", index + 2); + return end < 0 ? source.length() : end + 2; + } + return index; + } + if (current == '"' && source.startsWith("\"\"\"", index)) { + int end = source.indexOf("\"\"\"", index + 3); + return end < 0 ? source.length() : end + 3; + } + if (current == '"' || current == '\'') { + return endOfLiteral(source, index, current); + } + return index; + } + + private static int endOfLiteral(String source, int index, char terminator) { + for (int i = index + 1; i < source.length(); i++) { + char current = source.charAt(i); + if (current == '\\') { + i++; + continue; + } + if (current == terminator) { + return i + 1; + } + } + return source.length(); + } + + private static int occurrences(String source, String needle) { + int count = 0; + int offset = 0; + while (true) { + int index = source.indexOf(needle, offset); + if (index < 0) { + return count; + } + count++; + offset = index + needle.length(); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLootApplierTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLootApplierTest.java new file mode 100644 index 000000000..07214edbf --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedLootApplierTest.java @@ -0,0 +1,208 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.IrisLootMode; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.storage.loot.LootTable; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.lang.reflect.Proxy; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class ModdedLootApplierTest { + @Test + public void addAppendsSourcesInOrder() { + List sources = new ArrayList<>(List.of("placement-native")); + + LootResolver.injectSources(sources, List.of("dimension-iris", "region-iris"), IrisLootMode.ADD, false); + + assertEquals(List.of("placement-native", "dimension-iris", "region-iris"), sources); + } + + @Test + public void clearRemovesEverythingAndContributesNothing() { + List sources = new ArrayList<>(List.of("placement-native", "dimension-iris")); + + LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false); + + assertEquals(List.of(), sources); + } + + @Test + public void replaceRemovesNativeAndIrisSourcesBeforeAdding() { + List sources = new ArrayList<>(List.of("placement-native", "dimension-iris")); + + LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.REPLACE, false); + + assertEquals(List.of("biome-iris"), sources); + } + + @Test + public void entityLootBindingTargetsOnlyTheBaseLootInjection() { + assertTrue(ModdedDeathLoot.hasBindingTag(Set.of("unrelated", "iris_loot|17|1|2|3|test"))); + assertFalse(ModdedDeathLoot.hasBindingTag(Set.of("unrelated", "iris_other"))); + } + + @Test + public void fallbackDoesNotOverrideExistingNativeOrIrisSources() { + List sources = new ArrayList<>(List.of("placement-native", "dimension-iris")); + + LootResolver.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, false); + + assertEquals(List.of("placement-native", "dimension-iris"), sources); + } + + @Test + public void fallbackAddsSourcesWhenPlacementIsEmpty() { + List sources = new ArrayList<>(); + + LootResolver.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, true); + + assertEquals(List.of("fallback-iris"), sources); + } + + @Test + public void zeroMultiplierRemovesNativeAndIrisSources() { + List sources = new ArrayList<>(List.of("placement-native", "dimension-iris")); + + LootResolver.scaleSources(sources, 0D, new RNG(17L)); + + assertTrue(sources.isEmpty()); + } + + @Test + public void unitMultiplierPreservesNativeAndIrisSources() { + List sources = new ArrayList<>(List.of("placement-native", "dimension-iris")); + + LootResolver.scaleSources(sources, 1D, new RNG(17L)); + + assertEquals(List.of("placement-native", "dimension-iris"), sources); + } + + @Test + public void doubleMultiplierScalesTheUnifiedSourceList() { + List sources = new ArrayList<>(List.of("placement-native", "dimension-iris")); + + LootResolver.scaleSources(sources, 2D, new RNG(17L)); + + assertEquals(4, sources.size()); + assertEquals("placement-native", sources.get(0)); + assertEquals("dimension-iris", sources.get(1)); + Set expectedSources = Set.of("placement-native", "dimension-iris"); + for (String source : sources) { + assertTrue(expectedSources.contains(source)); + } + } + + @Test + public void nativeKeyIsReturnedWhenTheLiveRegistryContainsIt() { + ResourceKey key = ModdedLootApplier.resolveNativeKey("minecraft:chests/simple_dungeon", candidate -> true); + + assertNotNull(key); + assertEquals(Identifier.parse("minecraft:chests/simple_dungeon"), key.identifier()); + } + + @Test + public void nativeKeyIsRejectedWhenTheLiveRegistryDoesNotContainIt() { + ResourceKey key = ModdedLootApplier.resolveNativeKey("minecraft:chests/missing", candidate -> false); + + assertNull(key); + } + + @Test + public void malformedNativeKeyIsRejectedBeforeRegistryLookup() { + AtomicBoolean registryConsulted = new AtomicBoolean(false); + + ResourceKey key = ModdedLootApplier.resolveNativeKey("not a valid identifier", candidate -> { + registryConsulted.set(true); + return true; + }); + + assertNull(key); + assertFalse(registryConsulted.get()); + } + + @Test + public void nativeOnlyFillMarksTheContainerChanged() { + TrackingContainer container = new TrackingContainer(); + + ModdedLootApplier.fillContainer(container, List.of(), new RNG(17L)); + + assertTrue(container.changed); + } + + @Test + public void doubleChestHasExactlyOneCanonicalHalf() { + BlockPos leftPos = new BlockPos(0, 64, 0); + BlockPos rightPos = new BlockPos(1, 64, 0); + + assertTrue(ModdedLootApplier.isCanonicalPair(leftPos, rightPos)); + assertFalse(ModdedLootApplier.isCanonicalPair(rightPos, leftPos)); + } + + @Test + public void nativeLootTableIsDetectedBeforeContainerAccess() { + ResourceKey key = ResourceKey.create( + Registries.LOOT_TABLE, + Identifier.parse("minecraft:chests/simple_dungeon") + ); + RandomizableContainer empty = containerWithLootTable(null); + RandomizableContainer nativeLoot = containerWithLootTable(key); + + assertFalse(ModdedLootApplier.hasNativeLootTable(empty)); + assertTrue(ModdedLootApplier.hasNativeLootTable(nativeLoot)); + } + + private RandomizableContainer containerWithLootTable(ResourceKey key) { + return (RandomizableContainer) Proxy.newProxyInstance( + RandomizableContainer.class.getClassLoader(), + new Class[]{RandomizableContainer.class}, + (proxy, method, args) -> { + if (method.getName().equals("getLootTable")) { + return key; + } + Class returnType = method.getReturnType(); + if (returnType == boolean.class) { + return false; + } + if (returnType == int.class) { + return 0; + } + if (returnType == long.class) { + return 0L; + } + return null; + } + ); + } + + private static final class TrackingContainer extends SimpleContainer { + private boolean changed; + + private TrackingContainer() { + super(0); + } + + @Override + public void setChanged() { + changed = true; + super.setChanged(); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructurePoiRegistrationTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructurePoiRegistrationTest.java new file mode 100644 index 000000000..6b371d9b3 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructurePoiRegistrationTest.java @@ -0,0 +1,169 @@ +package art.arcane.iris.modded; + +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.server.Bootstrap; +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.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.BiomeSpecialEffects; +import net.minecraft.world.level.biome.MobSpawnSettings; +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.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerFactory; +import net.minecraft.world.level.chunk.PalettedContainerRO; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.Strategy; +import net.minecraft.world.level.chunk.UpgradeData; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class ModdedNativeStructurePoiRegistrationTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void visitsOnlyExistingPoiBlocksBeforeNativePlacement() { + ProtoChunk chunk = newChunk(); + BlockPos poiPosition = new BlockPos(3, 70, 5); + BlockPos ordinaryPosition = new BlockPos(4, 70, 5); + BlockState poiState = Blocks.BARREL.defaultBlockState(); + chunk.setBlockState(poiPosition, poiState, 0); + chunk.setBlockState(ordinaryPosition, Blocks.STONE.defaultBlockState(), 0); + Map visited = new LinkedHashMap<>(); + + ModdedNativeStructureStage.visitExistingPois( + chunk, (position, state) -> visited.put(position.immutable(), state)); + + assertEquals(poiState, visited.get(poiPosition)); + assertFalse(visited.containsKey(ordinaryPosition)); + assertEquals(1, visited.size()); + } + + @Test + public void duplicatePrimingIsIdempotentForAnAlreadyRegisteredPoi() { + BlockPos position = new BlockPos(3, 70, 5); + Holder type = PoiTypes.forState( + Blocks.BARREL.defaultBlockState()).orElseThrow(); + PoiSection section = new PoiSection(() -> { }); + + assertNotNull(section.add(position, type)); + assertNull(section.add(position, type)); + assertEquals(type, section.getType(position).orElseThrow()); + + section.remove(position); + + assertTrue(section.getType(position).isEmpty()); + } + + @Test + public void worldgenPrimingQueuesBeforeTheLaterVanillaRemoval() { + ProtoChunk chunk = newChunk(); + BlockPos position = new BlockPos(3, 70, 5); + BlockState poiState = Blocks.BARREL.defaultBlockState(); + chunk.setBlockState(position, poiState, 0); + PoiSection section = new PoiSection(() -> { }); + Deque serverQueue = new ArrayDeque<>(); + AtomicInteger missingRemovals = new AtomicInteger(); + + ModdedNativeStructureStage.visitExistingPois(chunk, + (poiPosition, state) -> queuePoiTransition( + serverQueue, section, poiPosition, + Blocks.AIR.defaultBlockState(), state, missingRemovals)); + assertTrue(section.getType(position).isEmpty()); + queuePoiTransition(serverQueue, section, position, + poiState, Blocks.AIR.defaultBlockState(), missingRemovals); + + while (!serverQueue.isEmpty()) { + serverQueue.removeFirst().run(); + } + + assertEquals(0, missingRemovals.get()); + assertTrue(section.getType(position).isEmpty()); + } + + private static void queuePoiTransition(Deque serverQueue, + PoiSection section, + BlockPos position, + BlockState oldState, + BlockState newState, + AtomicInteger missingRemovals) { + Optional> oldType = PoiTypes.forState(oldState); + Optional> newType = PoiTypes.forState(newState); + if (Objects.equals(oldType, newType)) { + return; + } + BlockPos immutablePosition = position.immutable(); + oldType.ifPresent(type -> serverQueue.addLast(() -> { + if (section.getType(immutablePosition).isEmpty()) { + missingRemovals.incrementAndGet(); + return; + } + section.remove(immutablePosition); + })); + newType.ifPresent(type -> serverQueue.addLast( + () -> section.add(immutablePosition, type))); + } + + private static ProtoChunk newChunk() { + return new ProtoChunk( + new ChunkPos(0, 0), + UpgradeData.EMPTY, + LevelHeightAccessor.create(-64, 384), + palettedContainerFactory(), + null); + } + + private static PalettedContainerFactory palettedContainerFactory() { + Strategy blockStrategy = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY); + Codec> blockCodec = PalettedContainer.codecRW( + BlockState.CODEC, blockStrategy, Blocks.AIR.defaultBlockState()); + Biome biome = new Biome.BiomeBuilder() + .hasPrecipitation(false) + .temperature(0.8F) + .downfall(0.4F) + .specialEffects(new BiomeSpecialEffects.Builder().waterColor(0x3F76E4).build()) + .mobSpawnSettings(MobSpawnSettings.EMPTY) + .generationSettings(BiomeGenerationSettings.EMPTY) + .build(); + Holder biomeHolder = Holder.direct(biome); + IdMapper> biomeIds = new IdMapper<>(1); + biomeIds.add(biomeHolder); + Strategy> biomeStrategy = Strategy.createForBiomes(biomeIds); + Codec>> biomeCodec = PalettedContainer.codecRO( + Biome.CODEC, biomeStrategy, biomeHolder); + return new PalettedContainerFactory( + blockStrategy, + Blocks.AIR.defaultBlockState(), + blockCodec, + biomeStrategy, + biomeHolder, + biomeCodec); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java new file mode 100644 index 000000000..e231736e9 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java @@ -0,0 +1,323 @@ +package art.arcane.iris.modded; + +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.LevelTickAccess; +import net.minecraft.world.ticks.ScheduledTick; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +import 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.assertTrue; + +public class ModdedNativeStructureWorldgenAccessTest { + private static final int GENERATION_CENTER_X = 60; + private static final int GENERATION_CENTER_Z = 15; + private static final int SURFACE_FIRST_FREE_Y = 80; + private static final int FLOOR_FIRST_FREE_Y = 70; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void distanceTwoTerrainReadsNeverReachTheDelegate() { + RecordingDelegate recording = new RecordingDelegate(); + ModdedNativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + int x = generationCenter.getMiddleBlockX(); + int z = (generationCenter.z() + 2) << 4; + + assertSame(Blocks.STONE, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y - 1, z)).getBlock()); + assertSame(Blocks.WATER, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)).getBlock()); + assertSame(Blocks.AIR, access.getBlockState(new BlockPos(x, SURFACE_FIRST_FREE_Y, z)).getBlock()); + assertSame(Blocks.WATER.defaultBlockState().getFluidState(), + access.getFluidState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z))); + assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)); + assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)); + assertNull(access.getChunk(generationCenter.x(), generationCenter.z() + 2, ChunkStatus.FEATURES, false)); + List loadedOnly = access.getBlockStatesIfLoaded(new AABB( + x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList(); + List streamed = access.getBlockStates(new AABB( + x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList(); + + assertTrue(loadedOnly.isEmpty()); + assertEquals(1, streamed.size()); + assertSame(Blocks.WATER, streamed.getFirst().getBlock()); + assertEquals(0, recording.terrainReads); + assertEquals(0, recording.heightReads); + assertEquals(0, recording.chunkReads); + } + + @Test + public void distanceTwoMutationsAndSideEffectsNeverReachTheDelegate() { + RecordingDelegate recording = new RecordingDelegate(); + ModdedNativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + BlockPos position = new BlockPos( + generationCenter.getMiddleBlockX(), + FLOOR_FIRST_FREE_Y, + (generationCenter.z() + 2) << 4); + + assertFalse(access.ensureCanWrite(position)); + assertFalse(access.setBlock(position, Blocks.DIRT.defaultBlockState(), 2)); + assertFalse(access.removeBlock(position, false)); + assertFalse(access.destroyBlock(position, false)); + access.updateNeighborsAt(position, Blocks.DIRT); + access.neighborShapeChanged( + Direction.NORTH, position, position.north(), + Blocks.DIRT.defaultBlockState(), 2, 512); + access.levelEvent(null, 2001, position, 0); + access.gameEvent(null, Vec3.atCenterOf(position), null); + access.addParticle(null, position.getX(), position.getY(), position.getZ(), 0, 0, 0); + access.getBlockTicks().schedule(new ScheduledTick<>( + Blocks.DIRT, position, 1L, 0L)); + + assertEquals(0, recording.mutations); + assertEquals(0, recording.events); + assertEquals(0, recording.blockTicks.count()); + } + + @Test + public void radiusOneReadsWritesAndTicksRemainUnchanged() { + RecordingDelegate recording = new RecordingDelegate(); + ModdedNativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + BlockPos position = new BlockPos( + (generationCenter.x() + 1) << 4, + FLOOR_FIRST_FREE_Y, + (generationCenter.z() - 1) << 4); + + assertTrue(access.isInsideGenerationRegion( + generationCenter.x() + 1, generationCenter.z() - 1)); + assertSame(Blocks.DIRT, access.getBlockState(position).getBlock()); + assertEquals(91, access.getHeight( + Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ())); + assertTrue(access.ensureCanWrite(position)); + assertTrue(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2)); + assertTrue(access.removeBlock(position, false)); + assertTrue(access.destroyBlock(position, false)); + access.getBlockTicks().schedule(new ScheduledTick<>( + Blocks.DIRT, position, 1L, 0L)); + + assertEquals(1, recording.terrainReads); + assertEquals(1, recording.heightReads); + assertEquals(4, recording.mutations); + assertEquals(1, recording.blockTicks.count()); + } + + @Test + public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() { + RecordingDelegate recording = new RecordingDelegate(); + ModdedNativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + + assertNull(access.getChunk(generationCenter.getWorldPosition())); + + assertEquals(1, recording.statuslessChunkReads); + assertEquals(0, recording.statusChunkReads); + } + + @Test + public void entityQueriesRejectDisjointAreasAndClampOverlaps() { + RecordingDelegate recording = new RecordingDelegate(); + ModdedNativeStructureWorldgenAccess access = access(recording); + ChunkPos generationCenter = generationCenter(); + int safeMaxX = generationCenter.getMaxBlockX() + 17; + int safeMinZ = generationCenter.getMinBlockZ() - 16; + int safeMaxZ = generationCenter.getMaxBlockZ() + 17; + AABB disjoint = new AABB( + safeMaxX, -64, safeMinZ, + safeMaxX + 16, 320, safeMaxZ); + AABB overlap = new AABB( + safeMaxX - 8, -128, safeMinZ - 8, + safeMaxX + 16, 400, safeMaxZ + 8); + EntityTypeTest type = EntityTypeTest.forClass(Entity.class); + + assertTrue(access.getEntities((Entity) null, disjoint, entity -> true).isEmpty()); + assertTrue(access.getEntities(type, disjoint, entity -> true).isEmpty()); + access.getEntities((Entity) null, overlap, entity -> true); + access.getEntities(type, overlap, entity -> true); + + assertEquals(2, recording.entityAreas.size()); + for (AABB delegatedArea : recording.entityAreas) { + assertEquals(safeMaxX - 8, delegatedArea.minX, 0D); + assertEquals(-64, delegatedArea.minY, 0D); + assertEquals(safeMinZ, delegatedArea.minZ, 0D); + assertEquals(safeMaxX, delegatedArea.maxX, 0D); + assertEquals(320, delegatedArea.maxY, 0D); + assertEquals(safeMaxZ, delegatedArea.maxZ, 0D); + } + } + + private static ModdedNativeStructureWorldgenAccess access(RecordingDelegate recording) { + return ModdedNativeStructureWorldgenAccess.create( + recording.world(), generationCenter(), + (x, z) -> SURFACE_FIRST_FREE_Y, + (x, z) -> FLOOR_FIRST_FREE_Y); + } + + private static ChunkPos generationCenter() { + return new ChunkPos(GENERATION_CENTER_X, GENERATION_CENTER_Z); + } + + private static final class RecordingDelegate implements InvocationHandler { + private final Holder biome; + private final BiomeManager biomeManager; + private final RecordingTicks blockTicks; + private final RecordingTicks fluidTicks; + private final List entityAreas; + private int terrainReads; + private int heightReads; + private int chunkReads; + private int statuslessChunkReads; + private int statusChunkReads; + private int mutations; + private int events; + + private RecordingDelegate() { + this.biome = Holder.direct((Biome) null); + this.biomeManager = new BiomeManager((x, y, z) -> biome, 13L); + this.blockTicks = new RecordingTicks<>(); + this.fluidTicks = new RecordingTicks<>(); + this.entityAreas = new ArrayList<>(); + } + + private WorldGenLevel world() { + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), + new Class[]{WorldGenLevel.class}, this); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] arguments) { + String name = method.getName(); + if (name.equals("getSeaLevel")) { + return 63; + } + if (name.equals("getBiome")) { + return biome; + } + if (name.equals("getBiomeManager")) { + return biomeManager; + } + if (name.equals("getBlockTicks")) { + return blockTicks; + } + if (name.equals("getFluidTicks")) { + return fluidTicks; + } + if (name.equals("getMinY")) { + return -64; + } + if (name.equals("getHeight") && arguments == null) { + return 384; + } + if (name.equals("getHeight")) { + heightReads++; + return 91; + } + if (name.equals("getBlockState")) { + terrainReads++; + return Blocks.DIRT.defaultBlockState(); + } + if (name.equals("getFluidState")) { + terrainReads++; + return Blocks.WATER.defaultBlockState().getFluidState(); + } + if (name.equals("getChunk")) { + chunkReads++; + if (arguments.length == 2) { + statuslessChunkReads++; + } else { + statusChunkReads++; + } + return null; + } + if (name.equals("getChunkIfLoadedImmediately")) { + chunkReads++; + return null; + } + if (name.equals("getEntities")) { + entityAreas.add((AABB) arguments[1]); + return List.of(); + } + if (name.equals("ensureCanWrite") || name.equals("setBlock") + || name.equals("removeBlock") || name.equals("destroyBlock")) { + mutations++; + return true; + } + if (name.equals("updateNeighborsAt") || name.equals("neighborShapeChanged")) { + mutations++; + return null; + } + if (name.equals("levelEvent") || name.equals("gameEvent") || name.equals("addParticle")) { + events++; + return null; + } + if (name.equals("hashCode")) { + return System.identityHashCode(proxy); + } + if (name.equals("equals")) { + return proxy == arguments[0]; + } + if (name.equals("toString")) { + return "native-structure-worldgen-test-delegate"; + } + throw new UnsupportedOperationException(method.toString()); + } + } + + private static final class RecordingTicks implements LevelTickAccess { + private int count; + + @Override + public void schedule(ScheduledTick tick) { + count++; + } + + @Override + public boolean hasScheduledTick(BlockPos position, T type) { + return false; + } + + @Override + public int count() { + return count; + } + + @Override + public boolean willTickThisTick(BlockPos position, T type) { + return false; + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java new file mode 100644 index 000000000..fe42157df --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java @@ -0,0 +1,106 @@ +package art.arcane.iris.modded; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.state.BlockState; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; + +public class ModdedPlatformPathsTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private ModdedPlatform platform() { + Path configDir = temporaryFolder.getRoot().toPath(); + return new ModdedPlatform(new ModdedLoader() { + @Override + public String platformName() { + return "test"; + } + + @Override + public String minecraftVersion() { + return "26.2"; + } + + @Override + public String modVersion() { + return "0.0.0"; + } + + @Override + public MinecraftServer currentServer() { + return null; + } + + @Override + public void invalidateLevelCache(MinecraftServer server) { + } + + @Override + public boolean clientEnvironment() { + return false; + } + + @Override + public Path configDir() { + return configDir; + } + + @Override + public File modJar() { + return null; + } + + @Override + public boolean hasTreeFellerPermission(ServerPlayer player) { + return false; + } + + @Override + public boolean canTreeFellerBreak(ServerLevel level, ServerPlayer player, BlockPos position, BlockState state) { + return false; + } + }); + } + + @Test + public void packsResolveUnderIrisworldgen() { + ModdedPlatform platform = platform(); + File root = temporaryFolder.getRoot(); + File packsRoot = new File(new File(root, "irisworldgen"), "packs"); + + assertEquals(packsRoot, platform.packsFolder()); + assertEquals(packsRoot, platform.dataFolder("packs")); + assertEquals(new File(packsRoot, "overworld"), platform.dataFolderNoCreate("packs", "overworld")); + assertEquals(new File(packsRoot, "overworld" + File.separator + "dimensions" + File.separator + "overworld.json"), + platform.dataFile("packs", "overworld", "dimensions", "overworld.json")); + } + + @Test + public void everythingElseStaysUnderIris() { + ModdedPlatform platform = platform(); + File iris = new File(temporaryFolder.getRoot(), "iris"); + + assertEquals(iris, platform.dataFolder()); + assertEquals(new File(iris, "settings.json"), platform.dataFile("settings.json")); + assertEquals(new File(iris, "parity"), platform.dataFolder("parity")); + } + + @Test + public void packsMatchIsExactSegmentOnly() { + ModdedPlatform platform = platform(); + File iris = new File(temporaryFolder.getRoot(), "iris"); + + assertEquals(new File(iris, "packbenchmarks"), platform.dataFolder("packbenchmarks")); + assertEquals(new File(iris, "packsx"), platform.dataFolderNoCreate("packsx")); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedProtocolHandlerCapabilitiesTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedProtocolHandlerCapabilitiesTest.java new file mode 100644 index 000000000..8fbe3af81 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedProtocolHandlerCapabilitiesTest.java @@ -0,0 +1,21 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.spi.protocol.IrisProtocol; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.junit.Assert.assertEquals; + +public class ModdedProtocolHandlerCapabilitiesTest { + @Test + public void advertisesEveryServerFeatureThatSendsProtocolFrames() throws Exception { + Field field = ModdedProtocolHandler.class.getDeclaredField("SERVER_CAPABILITIES"); + field.setAccessible(true); + + assertEquals(IrisProtocol.CAPABILITY_PREGEN + | IrisProtocol.CAPABILITY_VISION + | IrisProtocol.CAPABILITY_CURSOR + | IrisProtocol.CAPABILITY_STUDIO, field.getLong(null)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedSchedulerDownloadCancellationTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedSchedulerDownloadCancellationTest.java new file mode 100644 index 000000000..cbf857231 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedSchedulerDownloadCancellationTest.java @@ -0,0 +1,110 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import art.arcane.iris.core.pack.PackDownloadExecution; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +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.assertTrue; + +public class ModdedSchedulerDownloadCancellationTest { + @Test + public void rejectedDownloadSubmissionReleasesItsLease() throws Exception { + ModdedScheduler scheduler = new ModdedScheduler(); + scheduler.shutdown(); + TestLease lease = new TestLease(); + AtomicBoolean ran = new AtomicBoolean(); + PackDownloadExecution execution = new PackDownloadExecution( + lease, + cancellation -> ran.set(true) + ); + + boolean accepted = scheduler.asyncIfRunning(execution, execution::cancel); + + assertFalse(accepted); + assertTrue(execution.await(1L, TimeUnit.SECONDS)); + assertFalse(ran.get()); + assertEquals(1, lease.closeCount()); + } + + @Test + public void schedulerShutdownCancelsQueuedDownloadAndReleasesItsLease() throws Exception { + ModdedScheduler scheduler = new ModdedScheduler(); + int workerCount = Math.max(4, Runtime.getRuntime().availableProcessors()); + CountDownLatch workersStarted = new CountDownLatch(workerCount); + CountDownLatch releaseWorkers = new CountDownLatch(1); + for (int index = 0; index < workerCount; index++) { + scheduler.async(() -> { + workersStarted.countDown(); + try { + releaseWorkers.await(); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + } + }); + } + assertTrue(workersStarted.await(10L, TimeUnit.SECONDS)); + + TestLease lease = new TestLease(); + AtomicBoolean ran = new AtomicBoolean(); + PackDownloadExecution execution = new PackDownloadExecution( + lease, + cancellation -> ran.set(true) + ); + assertTrue(scheduler.asyncIfRunning(execution, execution::cancel)); + + try { + scheduler.shutdown(); + + assertTrue(execution.await(5L, TimeUnit.SECONDS)); + assertFalse(ran.get()); + assertEquals(1, lease.closeCount()); + } finally { + releaseWorkers.countDown(); + scheduler.shutdown(); + } + } + + private static final class TestLease implements LifecycleOperationCoordinator.Lease { + private final LifecycleOperationCoordinator.ActiveOperation operation; + private final AtomicBoolean closed; + private final AtomicInteger closeCount; + + private TestLease() { + operation = new LifecycleOperationCoordinator.ActiveOperation( + 1L, + LifecycleOperationCoordinator.Domain.PACK_MUTATION, + LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, + "test" + ); + closed = new AtomicBoolean(); + closeCount = new AtomicInteger(); + } + + @Override + public LifecycleOperationCoordinator.ActiveOperation operation() { + return operation; + } + + @Override + public boolean isClosed() { + return closed.get(); + } + + @Override + public void close() { + closeCount.incrementAndGet(); + closed.set(true); + } + + private int closeCount() { + return closeCount.get(); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedServiceManagerTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedServiceManagerTest.java new file mode 100644 index 000000000..e4fa49efb --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedServiceManagerTest.java @@ -0,0 +1,120 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.modded.service.ModdedService; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; + +public class ModdedServiceManagerTest { + @Test + public void enablesAndDisablesEveryRegisteredService() { + ModdedServiceManager manager = new ModdedServiceManager(); + FirstService first = manager.register(FirstService.class, new FirstService()); + SecondService second = manager.register(SecondService.class, new SecondService(null, null)); + + manager.enableAll(); + manager.disableAll(); + + assertEquals(1, first.enableCount); + assertEquals(1, first.disableCount); + assertEquals(1, second.enableCount); + assertEquals(1, second.disableCount); + } + + @Test + public void rollsBackEnabledServicesAndPreservesCleanupFailures() { + ModdedServiceManager manager = new ModdedServiceManager(); + FirstService first = manager.register(FirstService.class, new FirstService()); + RuntimeException original = new RuntimeException("enable failed"); + RuntimeException cleanup = new RuntimeException("cleanup failed"); + SecondService second = manager.register( + SecondService.class, new SecondService(original, cleanup)); + + RuntimeException thrown = assertThrows(RuntimeException.class, manager::enableAll); + + assertSame(original, thrown); + assertEquals(1, first.enableCount); + assertEquals(1, first.disableCount); + assertEquals(1, second.enableCount); + assertEquals(1, second.disableCount); + assertEquals(1, thrown.getSuppressed().length); + assertSame(cleanup, thrown.getSuppressed()[0]); + + manager.rollback(thrown); + assertNull(manager.service(FirstService.class)); + assertNull(manager.service(SecondService.class)); + } + + @Test + public void disableAttemptsEveryServiceInReverseOrderAndNeverRethrows() { + ModdedServiceManager manager = new ModdedServiceManager(); + RuntimeException firstFailure = new RuntimeException("first disable failed"); + RuntimeException secondFailure = new RuntimeException("second disable failed"); + FirstService first = manager.register(FirstService.class, new FirstService()); + first.disableFailure = firstFailure; + SecondService second = manager.register( + SecondService.class, new SecondService(null, secondFailure)); + + manager.enableAll(); + manager.disableAll(); + + assertEquals(1, first.disableCount); + assertEquals(1, second.disableCount); + assertEquals(1, secondFailure.getSuppressed().length); + assertSame(firstFailure, secondFailure.getSuppressed()[0]); + + manager.disableAll(); + assertEquals(1, first.disableCount); + assertEquals(1, second.disableCount); + } + + private static final class FirstService implements ModdedService { + private RuntimeException disableFailure; + private int enableCount; + private int disableCount; + + @Override + public void onEnable() { + enableCount++; + } + + @Override + public void onDisable() { + disableCount++; + if (disableFailure != null) { + throw disableFailure; + } + } + } + + private static final class SecondService implements ModdedService { + private final RuntimeException enableFailure; + private final RuntimeException disableFailure; + private int enableCount; + private int disableCount; + + private SecondService(RuntimeException enableFailure, RuntimeException disableFailure) { + this.enableFailure = enableFailure; + this.disableFailure = disableFailure; + } + + @Override + public void onEnable() { + enableCount++; + if (enableFailure != null) { + throw enableFailure; + } + } + + @Override + public void onDisable() { + disableCount++; + if (disableFailure != null) { + throw disableFailure; + } + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedSpawnerRarityParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedSpawnerRarityParityTest.java new file mode 100644 index 000000000..95562cb3c --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedSpawnerRarityParityTest.java @@ -0,0 +1,75 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.IRare; +import art.arcane.iris.engine.object.IrisEntitySpawn; +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ModdedSpawnerRarityParityTest { + private static IrisEntitySpawn spawn(int rarity) { + IrisEntitySpawn spawn = new IrisEntitySpawn(); + spawn.setRarity(rarity); + return spawn; + } + + @Test + public void rarityIsAppliedOnceAsPoolWeightOnly() { + IrisEntitySpawn common = spawn(1); + IrisEntitySpawn rare = spawn(4); + + KList expanded = IRare.expandWeighted(List.of(common, rare)); + + // totalRarity 5 -> common appears 5/1 = 5 times, rare 5/4 = 1 time. + assertEquals(6, expanded.size()); + assertEquals(5, expanded.stream().filter(entry -> entry == common).count()); + assertEquals(1, expanded.stream().filter(entry -> entry == rare).count()); + } + + @Test + public void rarityZeroAndNegativeAreClampedToOne() { + assertEquals(1, IRare.get(spawn(0))); + assertEquals(1, IRare.get(spawn(-5))); + assertEquals(3, IRare.get(spawn(3))); + } + + @Test + public void perPositionSpawnLoopsDoNotRerollEntryRarity() throws Exception { + String source = Files.readString(Path.of(System.getProperty("iris.moddedCommonSources"), + "art/arcane/iris/modded/ModdedWorldManager.java")); + + for (String method : List.of("private int spawnEntry(", "private int spawnEntryAt(")) { + String body = methodBody(source, method); + assertFalse(method + " must not re-roll rarity per position; rarityPick already weighted the pool", + body.contains("getRarity()")); + assertTrue(method + " must keep the min/max spawn-count roll", + body.contains("LootResolver.inclusive(")); + } + } + + private static String methodBody(String source, String declaration) { + int start = source.indexOf(declaration); + assertTrue("ModdedWorldManager must declare " + declaration, start >= 0); + int open = source.indexOf('{', start); + int depth = 0; + for (int index = open; index < source.length(); index++) { + char character = source.charAt(index); + if (character == '{') { + depth++; + } else if (character == '}') { + depth--; + if (depth == 0) { + return source.substring(open + 1, index); + } + } + } + throw new AssertionError(declaration + " is not brace balanced"); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructureHooksTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructureHooksTest.java new file mode 100644 index 000000000..a50527929 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructureHooksTest.java @@ -0,0 +1,128 @@ +package art.arcane.iris.modded; + +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ModdedStructureHooksTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void objectFeaturesMatchPaperGroups() { + assertEquals("trees", ModdedStructureHooks.classifyFeature(Feature.TREE)); + assertEquals("fallen_trees", ModdedStructureHooks.classifyFeature(Feature.FALLEN_TREE)); + assertEquals("mushrooms", ModdedStructureHooks.classifyFeature(Feature.HUGE_BROWN_MUSHROOM)); + assertEquals("mushrooms", ModdedStructureHooks.classifyFeature(Feature.HUGE_RED_MUSHROOM)); + assertNull(ModdedStructureHooks.classifyFeature(Feature.ORE)); + } + + @Test + public void structureSpanGuardUsesInclusiveBoundingBoxDimensions() { + BoundingBox box = new BoundingBox(-16, -64, 32, 15, 63, 47); + + assertTrue(ModdedStructureHooks.isWithinSpan(box, 128)); + assertFalse(ModdedStructureHooks.isWithinSpan(box, 127)); + assertTrue(ModdedStructureHooks.isWithinSpan(box, 0)); + assertTrue(ModdedStructureHooks.isWithinSpan(box, -1)); + } + + @Test + public void chunkGridSizeCountsEveryChunkPlacementWouldLoad() { + assertEquals(1, ModdedStructureHooks.chunkGridSize(new BoundingBox(0, 0, 0, 15, 15, 15))); + assertEquals(4, ModdedStructureHooks.chunkGridSize(new BoundingBox(0, 0, 0, 16, 15, 16))); + assertEquals(9, ModdedStructureHooks.chunkGridSize(new BoundingBox(-16, 0, -16, 16, 15, 16))); + } + + @Test + public void chunkGridSizeSaturatesInsteadOfOverflowing() { + BoundingBox box = new BoundingBox( + Integer.MIN_VALUE / 2, 0, Integer.MIN_VALUE / 2, + Integer.MAX_VALUE / 2, 15, Integer.MAX_VALUE / 2); + + assertEquals(Integer.MAX_VALUE, ModdedStructureHooks.chunkGridSize(box)); + assertTrue(ModdedStructureHooks.chunkGridSize(box) > ModdedStructureHooks.MAX_PLACEMENT_CHUNKS); + } + + @Test + public void structurePlacementReturnsPaperOrderedBounds() { + BoundingBox box = new BoundingBox(-16, -64, 32, 15, 63, 47); + + assertArrayEquals(new int[]{-16, -64, 32, 15, 63, 47}, ModdedStructureHooks.bounds(box)); + } + + @Test + public void platformWorldMaximumHeightIsExclusive() { + assertEquals(320, ModdedPlatformWorld.exclusiveMaxHeight(-64, 384)); + assertEquals(384, ModdedPlatformWorld.exclusiveMaxHeight(0, 384)); + } + + @Test + public void structureRegistryHooksRejectUnavailableServer() { + ModdedStructureHooks hooks = new ModdedStructureHooks(() -> null); + + IllegalStateException structureKeys = assertThrows( + IllegalStateException.class, hooks::structureKeys); + IllegalStateException jigsawStructureKeys = assertThrows( + IllegalStateException.class, hooks::jigsawStructureKeys); + IllegalStateException templatePoolKeys = assertThrows( + IllegalStateException.class, hooks::templatePoolKeys); + IllegalStateException jigsawMetadata = assertThrows( + IllegalStateException.class, () -> hooks.jigsawSourceMetadata("minecraft:village")); + IllegalStateException templatePoolSpan = assertThrows( + IllegalStateException.class, + () -> hooks.templatePoolHorizontalSpan("minecraft:village/plains/town_centers")); + IllegalStateException structureSetKeys = assertThrows( + IllegalStateException.class, hooks::structureSetKeys); + IllegalStateException structureBiomeKeys = assertThrows( + IllegalStateException.class, () -> hooks.structureBiomeKeys("minecraft:village")); + + assertTrue(structureKeys.getMessage().contains("before the Minecraft server is available")); + assertTrue(jigsawStructureKeys.getMessage().contains("before the Minecraft server is available")); + assertTrue(templatePoolKeys.getMessage().contains("before the Minecraft server is available")); + assertTrue(jigsawMetadata.getMessage().contains("before the Minecraft server is available")); + assertTrue(templatePoolSpan.getMessage().contains("before the Minecraft server is available")); + assertTrue(structureSetKeys.getMessage().contains("before the Minecraft server is available")); + assertTrue(structureBiomeKeys.getMessage().contains("before the Minecraft server is available")); + } + + @Test + public void structureReachabilityHooksRejectUnavailableLevel() { + ModdedStructureHooks hooks = new ModdedStructureHooks(() -> null); + + IllegalStateException reachable = assertThrows( + IllegalStateException.class, () -> hooks.reachableStructureKeys(null)); + IllegalStateException possibleBiomes = assertThrows( + IllegalStateException.class, () -> hooks.possibleBiomeKeys(null)); + + assertTrue(reachable.getMessage().contains("without a bound modded ServerLevel")); + assertTrue(possibleBiomes.getMessage().contains("without a bound modded ServerLevel")); + } + + @Test + public void structureRegistryHooksPreserveServerSupplierFailure() { + IllegalStateException cause = new IllegalStateException("server supplier failed"); + ModdedStructureHooks hooks = new ModdedStructureHooks(() -> { + throw cause; + }); + + IllegalStateException failure = assertThrows( + IllegalStateException.class, hooks::structureKeys); + + assertTrue(failure.getMessage().contains("access the Minecraft server")); + assertEquals(cause, failure.getCause()); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructurePlacementFailureContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructurePlacementFailureContractTest.java new file mode 100644 index 000000000..0efa85911 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructurePlacementFailureContractTest.java @@ -0,0 +1,30 @@ +package art.arcane.iris.modded; + +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 ModdedStructurePlacementFailureContractTest { + @Test + public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException { + Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"), + "art/arcane/iris/modded/ModdedStructureHooks.java"); + String source = Files.readString(sourcePath); + 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 error)"); + + assertTrue(catchStart >= 0); + String failurePath = method.substring(catchStart); + assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure(")); + assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, error")); + assertFalse(failurePath.contains("return null;")); + assertFalse(failurePath.contains("catch (Throwable")); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructureSetFrequencyOverridesTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructureSetFrequencyOverridesTest.java new file mode 100644 index 000000000..a75a6f05a --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructureSetFrequencyOverridesTest.java @@ -0,0 +1,248 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.IrisImportedStructureControl; +import art.arcane.iris.engine.object.IrisStructureSetFrequencyOverride; +import art.arcane.volmlib.util.collection.KList; +import net.minecraft.SharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderOwner; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Vec3i; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; + +public class ModdedStructureSetFrequencyOverridesTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void createStateReplacementScalesOnlyTheExactSetAndRetainsEntries() throws Exception { + Holder complexes = structureSet( + "minecraft:nether_complexes", 27, 4, 30084232); + Holder fossils = structureSet( + "minecraft:nether_fossils", 2, 1, 14357921); + ChunkGeneratorStructureState state = state(List.of(complexes, fossils)); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("minecraft:nether_complexes") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + ChunkGeneratorStructureState returned = + ModdedStructureSetFrequencyOverrides.apply(state, control); + + assertSame(state, returned); + List> scaledSets = state.possibleStructureSets(); + RandomSpreadStructurePlacement scaledComplexes = + (RandomSpreadStructurePlacement) scaledSets.get(0).value().placement(); + assertEquals(26, scaledComplexes.spacing()); + assertEquals(4, scaledComplexes.separation()); + assertSame(complexes.value().structures(), scaledSets.get(0).value().structures()); + assertSame(fossils, scaledSets.get(1)); + } + + @Test + public void unrelatedCustomPlacementTypeRemainsUntouched() { + Holder custom = new BoundStructureSetHolder( + "example:custom", + new StructureSet(List.of(), new UnsupportedPlacement())); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("minecraft:nether_complexes") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + List> scaled = + ModdedStructureSetFrequencyOverrides.scaleSets(List.of(custom), control); + + assertSame(custom, scaled.getFirst()); + } + + @Test + public void unrelatedExclusionCycleRemainsUntouched() { + BoundStructureSetHolder first = new BoundStructureSetHolder("example:first"); + BoundStructureSetHolder second = new BoundStructureSetHolder("example:second"); + first.bind(new StructureSet(List.of(), placement(32, 8, 1, second))); + second.bind(new StructureSet(List.of(), placement(40, 10, 2, first))); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("minecraft:nether_complexes") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + List> scaled = ModdedStructureSetFrequencyOverrides.scaleSets( + List.of(first, second), control); + + assertSame(first, scaled.get(0)); + assertSame(second, scaled.get(1)); + } + + @Test + public void affectedExclusionCycleUsesBoundScaledHolders() { + BoundStructureSetHolder first = new BoundStructureSetHolder("example:first"); + BoundStructureSetHolder second = new BoundStructureSetHolder("example:second"); + first.bind(new StructureSet(List.of(), placement(32, 8, 1, second))); + second.bind(new StructureSet(List.of(), placement(40, 10, 2, first))); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("example:first") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + List> scaled = ModdedStructureSetFrequencyOverrides.scaleSets( + List.of(first, second), control); + + assertEquals(31, ((RandomSpreadStructurePlacement) + scaled.get(0).value().placement()).spacing()); + assertEquals(40, ((RandomSpreadStructurePlacement) + scaled.get(1).value().placement()).spacing()); + } + + @Test + public void affectedRandomSpreadSubclassFailsInsteadOfLosingSubtypeBehavior() { + Holder custom = new BoundStructureSetHolder( + "example:custom", + new StructureSet(List.of(), new CustomRandomSpreadPlacement())); + KList overrides = new KList<>(); + overrides.add(new IrisStructureSetFrequencyOverride() + .setStructureSet("example:custom") + .setMultiplier(1.1D)); + IrisImportedStructureControl control = new IrisImportedStructureControl() + .setFrequencyOverrides(overrides); + + assertThrows(IllegalStateException.class, () -> + ModdedStructureSetFrequencyOverrides.scaleSets(List.of(custom), control)); + } + + private static Holder structureSet( + String key, + int spacing, + int separation, + int salt + ) { + StructureSet value = new StructureSet( + List.of(), + new RandomSpreadStructurePlacement( + spacing, separation, RandomSpreadType.LINEAR, salt)); + return new BoundStructureSetHolder(key, value); + } + + private static RandomSpreadStructurePlacement placement( + int spacing, + int separation, + int salt, + Holder exclusionTarget + ) { + return new RandomSpreadStructurePlacement( + Vec3i.ZERO, + StructurePlacement.FrequencyReductionMethod.DEFAULT, + 1F, + salt, + Optional.of(new StructurePlacement.ExclusionZone(exclusionTarget, 1)), + spacing, + separation, + RandomSpreadType.LINEAR); + } + + private static ChunkGeneratorStructureState state( + List> sets + ) throws Exception { + Constructor constructor = + ChunkGeneratorStructureState.class.getDeclaredConstructor( + RandomState.class, + BiomeSource.class, + long.class, + long.class, + List.class); + constructor.setAccessible(true); + return constructor.newInstance( + null, + new EmptyBiomeSource(), + 1L, + 1L, + sets); + } + + private static final class BoundStructureSetHolder extends Holder.Reference { + private BoundStructureSetHolder(String key) { + this(key, null); + } + + private BoundStructureSetHolder(String key, StructureSet value) { + super(Type.STAND_ALONE, new HolderOwner<>() { + }, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), value); + } + + private void bind(StructureSet value) { + bindValue(value); + } + } + + private static final class EmptyBiomeSource extends BiomeSource { + @Override + public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { + return null; + } + + @Override + protected com.mojang.serialization.MapCodec codec() { + throw new UnsupportedOperationException(); + } + + @Override + protected java.util.stream.Stream> collectPossibleBiomes() { + return HolderSet.empty().stream(); + } + } + + private static final class UnsupportedPlacement extends StructurePlacement { + private UnsupportedPlacement() { + super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, Optional.empty()); + } + + @Override + protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) { + return false; + } + + @Override + public StructurePlacementType type() { + return null; + } + } + + private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement { + private CustomRandomSpreadPlacement() { + super(27, 4, RandomSpreadType.LINEAR, 30084232); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructureTemplateCacheTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructureTemplateCacheTest.java new file mode 100644 index 000000000..edbedc22d --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStructureTemplateCacheTest.java @@ -0,0 +1,77 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +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.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ModdedStructureTemplateCacheTest { + @Test + public void concurrentCacheOwnsDistinctAndSharedMappingsExactlyOnce() throws Exception { + ConcurrentHashMap cache = new ConcurrentHashMap<>(); + ExecutorService executor = Executors.newFixedThreadPool(32); + CountDownLatch start = new CountDownLatch(1); + AtomicInteger mappingCalls = new AtomicInteger(); + List> futures = new ArrayList<>(); + try { + for (int task = 0; task < 256; task++) { + int value = task; + String key = task % 2 == 0 ? "shared" : "distinct-" + task; + futures.add(executor.submit(() -> { + start.await(); + return cache.computeIfAbsent(key, ignored -> { + mappingCalls.incrementAndGet(); + Thread.yield(); + return value; + }); + })); + } + start.countDown(); + for (Future future : futures) { + assertNotNull(future.get(10, TimeUnit.SECONDS)); + } + } finally { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + + assertEquals(129, cache.size()); + assertEquals(129, mappingCalls.get()); + } + + @Test + public void requiredCommonMixinConfigRegistersPaletteConcurrencyFix() throws Exception { + InputStream resource = ModdedStructureTemplateCacheTest.class.getClassLoader() + .getResourceAsStream("irisworldgen.entity.mixins.json"); + assertNotNull(resource); + String config; + try (InputStream input = resource) { + config = new String(input.readAllBytes(), StandardCharsets.UTF_8); + } + assertTrue(config.contains("\"StructureTemplatePaletteConcurrencyMixin\"")); + + Path source = Path.of(System.getProperty("iris.moddedCommonSources")) + .resolve("art/arcane/iris/modded/mixin/StructureTemplatePaletteConcurrencyMixin.java"); + String mixin = Files.readString(source); + assertTrue(mixin.contains("@Mixin(StructureTemplate.Palette.class)")); + assertTrue(mixin.contains("@Shadow\n @Final\n @Mutable")); + assertTrue(mixin.contains("private Map> cache;")); + assertTrue(mixin.contains("@Inject(method = \"(Ljava/util/List;)V\", at = @At(\"RETURN\"))")); + assertTrue(mixin.contains("cache = new ConcurrentHashMap<>();")); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedTileDataIdentityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedTileDataIdentityTest.java new file mode 100644 index 000000000..030d4547d --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedTileDataIdentityTest.java @@ -0,0 +1,139 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.TileData; +import art.arcane.volmlib.util.collection.KMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.Bootstrap; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Mantle tile sections are palette backed and resolve palette ids through equals/hashCode, so two different tiles + * in one section must not share an identity - otherwise the first tile's NBT is written into every other tile. + * The superclass generates equals/hashCode from fields a modded record never populates, which is why this class + * answers identity itself. + */ +public class ModdedTileDataIdentityTest { + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + /** + * The generic map form the Bukkit side reads cannot express an NBT array: ByteArray, IntArray and LongArray all + * collapse to a plain List, and pasting that back produces a ListTag where Minecraft demands an array. A player + * head's {@code profile.id} is an IntArray of four, so a captured head pasted from the map form loses its skin. + * Capture therefore keeps the original SNBT alongside the map, and the modded paste path reads it first. + */ + @Test + public void captureKeepsSnbtSoArrayTypingSurvivesThePasteRoundTrip() throws Exception { + String snbt = "{profile:{id:[I;1,2,3,4],name:\"iris\"}}"; + + ModdedTileData captured = ModdedTileData.capture("minecraft:player_head", snbt); + + assertEquals(snbt, captured.snbt()); + assertEquals(snbt, captured.getProperties().get(ModdedTileData.NBT_PROPERTY)); + + // The Bukkit-readable map form is still written, and is still array-lossy - which is why the SNBT has to stay. + assertTrue(captured.getProperties().get("profile") instanceof KMap); + KMap profile = (KMap) captured.getProperties().get("profile"); + assertTrue(profile.get("id") instanceof List); + + CompoundTag payload = captured.payload(); + assertNotNull(payload); + Tag profileTag = payload.get("profile"); + assertTrue(profileTag instanceof CompoundTag); + Tag idTag = ((CompoundTag) profileTag).get("id"); + assertTrue("profile.id must paste back as an IntArrayTag, got " + + (idTag == null ? "null" : idTag.getClass().getSimpleName()), + idTag instanceof IntArrayTag); + assertArrayEquals(new int[]{1, 2, 3, 4}, ((IntArrayTag) idTag).getAsIntArray()); + } + + @Test + public void differentTilesAreNotEqualAndDoNotShareHashCode() { + ModdedTileData chest = tile("minecraft:chest", "Items", "diamond"); + ModdedTileData otherChest = tile("minecraft:chest", "Items", "emerald"); + ModdedTileData sign = tile("minecraft:oak_sign", "front_text", "hello"); + + assertNotEquals(chest, otherChest); + assertNotEquals(chest, sign); + // The inherited identity was a constant hash for every modded record; unequal objects are allowed to + // collide, so assert only that the hash actually varies with the record. + assertTrue(Set.of(chest.hashCode(), otherChest.hashCode(), sign.hashCode()).size() > 1); + } + + @Test + public void identicalTilesShareOnePaletteIdentity() { + ModdedTileData first = tile("minecraft:chest", "Items", "diamond"); + ModdedTileData second = tile("minecraft:chest", "Items", "diamond"); + + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + } + + @Test + public void materialKeyReportsTheBlockKeyInsteadOfNull() { + assertEquals("minecraft:chest", tile("minecraft:chest", "Items", "diamond").getMaterialKey()); + } + + @Test + public void legacyRecordHasNoBlockKey() { + ModdedTileData legacy = new ModdedTileData(new byte[]{0, 1}, new KMap<>(), null, 0); + assertNull(legacy.getMaterialKey()); + } + + @Test + public void cloneIsEqualButIndependentOfTheSource() { + ModdedTileData source = tile("minecraft:chest", "Items", "diamond"); + KMap nested = new KMap<>(); + nested.put("id", "minecraft:stone"); + source.getProperties().put("nested", nested); + source.getProperties().put("list", List.of("a", "b")); + + TileData copy = source.clone(); + + assertNotSame(source, copy); + assertNotSame(source.getProperties(), copy.getProperties()); + assertEquals(source.getProperties(), copy.getProperties()); + assertNotSame(source.getProperties().get("nested"), copy.getProperties().get("nested")); + assertTrue(copy.getProperties().get("list") instanceof List); + } + + private static ModdedTileData tile(String blockKey, String propertyKey, String propertyValue) { + KMap properties = new KMap<>(); + properties.put(propertyKey, propertyValue); + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(bytes)) { + out.writeUTF(blockKey); + out.writeUTF(GSON.toJson(properties)); + } + return new ModdedTileData(bytes.toByteArray(), properties, blockKey, -1); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedTileParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedTileParityTest.java new file mode 100644 index 000000000..d29756b04 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedTileParityTest.java @@ -0,0 +1,218 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.engine.object.TileData; +import art.arcane.volmlib.util.collection.KMap; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.Identifier; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BarrelBlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.minecraft.world.level.block.entity.SpawnerBlockEntity; +import net.minecraft.world.level.storage.TagValueInput; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ModdedTileParityTest { + private static RegistryAccess registries; + + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + TileData.bindPlatformReader(new ModdedTileReader(() -> null)); + TileData.bindPlatformFactory(ModdedTileData::fromProperties); + } + + @Test + public void legacySignPayloadLoadsIntoNativeSignBlockEntity() throws Exception { + ModdedTileData tile = legacyTile(out -> { + out.writeShort(0); + out.writeUTF("Iris"); + out.writeUTF("modded"); + out.writeUTF("tile"); + out.writeUTF("parity"); + out.writeByte(DyeColor.BLUE.getId()); + }); + SignBlockEntity sign = new SignBlockEntity(BlockPos.ZERO, Blocks.OAK_SIGN.defaultBlockState()); + + sign.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, tile.payload())); + + assertEquals("Iris", sign.getFrontText().getMessage(0, false).getString()); + assertEquals("parity", sign.getBackText().getMessage(3, false).getString()); + assertEquals(DyeColor.BLUE, sign.getFrontText().getColor()); + } + + @Test + public void legacySpawnerPayloadLoadsNamespacedEntity() throws Exception { + ModdedTileData tile = legacyTile(out -> { + out.writeShort(1); + out.writeUTF("minecraft:zombie"); + }); + SpawnerBlockEntity spawner = new SpawnerBlockEntity(BlockPos.ZERO, Blocks.SPAWNER.defaultBlockState()); + + spawner.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, tile.payload())); + CompoundTag saved = spawner.saveWithoutMetadata(registries); + + assertTrue(saved.toString().contains("minecraft:zombie")); + } + + @Test + public void legacySpawnerOrdinalUsesPaperEntityTypeOrdering() throws Exception { + ModdedTileData tile = legacyTile(out -> { + out.writeShort(1); + out.writeShort(28); + }); + SpawnerBlockEntity spawner = new SpawnerBlockEntity(BlockPos.ZERO, Blocks.SPAWNER.defaultBlockState()); + + spawner.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, tile.payload())); + CompoundTag saved = spawner.saveWithoutMetadata(registries); + + assertTrue(saved.toString().contains("minecraft:command_block_minecart")); + } + + @Test + public void invalidLegacySpawnerOrdinalFallsBackToPig() throws Exception { + ModdedTileData tile = legacyTile(out -> { + out.writeShort(1); + out.writeShort(-1); + }); + SpawnerBlockEntity spawner = new SpawnerBlockEntity(BlockPos.ZERO, Blocks.SPAWNER.defaultBlockState()); + + spawner.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, tile.payload())); + CompoundTag saved = spawner.saveWithoutMetadata(registries); + + assertTrue(saved.toString().contains("minecraft:pig")); + } + + @Test + public void legacyLootablePayloadLoadsTableAndSeed() throws Exception { + ModdedTileData tile = legacyTile(out -> { + out.writeShort(3); + out.writeUTF("minecraft:chest"); + out.writeUTF("minecraft:chests/simple_dungeon"); + out.writeLong(4123L); + }); + ChestBlockEntity chest = new ChestBlockEntity(BlockPos.ZERO, Blocks.CHEST.defaultBlockState()); + + chest.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, tile.payload())); + RandomizableContainer lootable = chest; + + assertNotNull(lootable.getLootTable()); + assertEquals("minecraft:chests/simple_dungeon", lootable.getLootTable().identifier().toString()); + assertEquals(4123L, lootable.getLootTableSeed()); + } + + @Test + public void legacyBannerBaseColorChangesTheGeneratedBlockState() throws Exception { + ModdedTileData tile = legacyTile(out -> { + out.writeShort(2); + out.writeByte(DyeColor.RED.getId()); + out.writeByte(0); + }); + + assertEquals( + BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:red_banner")), + tile.adjustBlockState(BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:white_banner")).defaultBlockState()).getBlock()); + assertEquals(Blocks.BARREL, tile.adjustBlockState(Blocks.BARREL.defaultBlockState()).getBlock()); + } + + @Test + public void paper26_2LegacyBannerFirstOrdinalIsSmallStripes() { + assertEquals( + Identifier.parse("minecraft:small_stripes"), + ModdedTileReader.legacyBannerPatternKey(0)); + } + + @Test + public void paper26_2LegacyBannerMiddleOrdinalIsStripeLeft() { + assertEquals( + Identifier.parse("minecraft:stripe_left"), + ModdedTileReader.legacyBannerPatternKey(21)); + } + + @Test + public void paper26_2LegacyBannerLastOrdinalIsHalfVerticalRight() { + assertEquals( + Identifier.parse("minecraft:half_vertical_right"), + ModdedTileReader.legacyBannerPatternKey(42)); + } + + @Test + public void invalidLegacyBannerOrdinalFallsBackToBase() { + assertEquals(Identifier.parse("minecraft:base"), ModdedTileReader.legacyBannerPatternKey(-1)); + assertEquals(Identifier.parse("minecraft:base"), ModdedTileReader.legacyBannerPatternKey(43)); + } + + @Test + public void modernPackTilePropertiesUseTheModdedFactoryAndLoadNatively() throws Exception { + KMap properties = new KMap<>(); + properties.put("LootTable", "minecraft:chests/abandoned_mineshaft"); + properties.put("LootTableSeed", 9124L); + TileData tile = TileData.of(ModdedBlockState.of(Blocks.CHEST.defaultBlockState(), null), properties); + + assertTrue(tile instanceof ModdedTileData); + ChestBlockEntity chest = new ChestBlockEntity(BlockPos.ZERO, Blocks.CHEST.defaultBlockState()); + chest.loadWithComponents(TagValueInput.create( + ProblemReporter.DISCARDING, registries, ((ModdedTileData) tile).payload())); + + assertNotNull(chest.getLootTable()); + assertEquals("minecraft:chests/abandoned_mineshaft", chest.getLootTable().identifier().toString()); + assertEquals(9124L, chest.getLootTableSeed()); + assertTrue(((ModdedTileData) tile).isApplicable(Blocks.CHEST.defaultBlockState(), chest)); + assertFalse(((ModdedTileData) tile).isApplicable( + Blocks.BARREL.defaultBlockState(), + new BarrelBlockEntity(BlockPos.ZERO, Blocks.BARREL.defaultBlockState()))); + } + + @Test + public void legacyTileFamiliesRejectUnrelatedBlockEntities() throws Exception { + ModdedTileData tile = legacyTile(out -> { + out.writeShort(0); + out.writeUTF("one"); + out.writeUTF("two"); + out.writeUTF("three"); + out.writeUTF("four"); + out.writeByte(DyeColor.BLACK.getId()); + }); + SignBlockEntity sign = new SignBlockEntity(BlockPos.ZERO, Blocks.OAK_SIGN.defaultBlockState()); + ChestBlockEntity chest = new ChestBlockEntity(BlockPos.ZERO, Blocks.CHEST.defaultBlockState()); + + assertTrue(tile.isApplicable(Blocks.OAK_SIGN.defaultBlockState(), sign)); + assertFalse(tile.isApplicable(Blocks.CHEST.defaultBlockState(), chest)); + } + + private static ModdedTileData legacyTile(TileWriter writer) throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(bytes)) { + writer.write(out); + } + TileData tile = TileData.read(new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))); + assertTrue(tile instanceof ModdedTileData); + return (ModdedTileData) tile; + } + + @FunctionalInterface + private interface TileWriter { + void write(DataOutputStream out) throws Exception; + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldCheckTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldCheckTest.java new file mode 100644 index 000000000..ddf92a2c2 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldCheckTest.java @@ -0,0 +1,301 @@ +package art.arcane.iris.modded; + +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ModdedWorldCheckTest { + @Test + public void coordinatorThreadIsDaemonSoItCannotWedgeTheJvm() { + Thread thread = ModdedWorldCheck.coordinatorThread(() -> { + }); + + assertTrue(thread.isDaemon()); + } + + @Test + public void poiAuditRunsInASecondServerTaskAfterVillageGeneration() throws IOException { + Path sourceRoot = Path.of(System.getProperty("iris.moddedCommonSources")); + String source = Files.readString(sourceRoot.resolve("art/arcane/iris/modded/ModdedWorldCheck.java")); + String auditSource = Files.readString( + sourceRoot.resolve("art/arcane/iris/modded/WorldCheckStructureAudit.java")); + int preparationSubmit = source.indexOf( + "WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef))"); + int completionSubmit = source.indexOf( + "exitCode = serverRef.submit(() -> runAndRequestStop(", preparationSubmit); + int completionMethod = source.indexOf("private static boolean completeWorldCheck"); + int deferredAudit = source.indexOf( + "PoiAudit poi = WorldCheckStructureAudit.auditStructurePois", completionMethod); + int structureMethod = auditSource.indexOf("private static StructureCheckResult checkNativeStructure"); + int structureMethodEnd = auditSource.indexOf("private static StructureStart resolveStructureStart", + structureMethod); + String structureSource = auditSource.substring(structureMethod, structureMethodEnd); + + assertTrue(preparationSubmit >= 0); + assertTrue(completionSubmit > preparationSubmit); + assertTrue(deferredAudit > completionMethod); + assertFalse(structureSource.contains("auditStructurePois")); + assertFalse(source.contains("prepareDeferredAudits")); + assertFalse(auditSource.contains("prepareDeferredAudits")); + } + + @Test + public void validStructureStartIsGenerationEvidence() { + assertTrue(WorldCheckPredicates.hasNativeStructureEvidence(true, 0)); + } + + @Test + public void structureReferenceIsGenerationEvidence() { + assertTrue(WorldCheckPredicates.hasNativeStructureEvidence(false, 1)); + } + + @Test + public void absentStartAndReferencesFailGenerationEvidence() { + assertFalse(WorldCheckPredicates.hasNativeStructureEvidence(false, 0)); + } + + @Test + public void characteristicMaterialsCoverEveryNativeStructureFamily() { + assertTrue(characteristic("stronghold", "minecraft:stronghold", "minecraft:cracked_stone_bricks")); + assertTrue(characteristic("trial_chambers", "minecraft:trial_chambers", "minecraft:oxidized_copper_grate")); + assertTrue(characteristic("mansion", "minecraft:mansion", "minecraft:dark_oak_planks")); + assertTrue(characteristic("mansion", "minecraft:mansion", "minecraft:birch_planks")); + assertTrue(characteristic("village", "minecraft:village_plains", "minecraft:oak_planks")); + assertTrue(characteristic("village", "minecraft:village_desert", "minecraft:cut_sandstone")); + assertTrue(characteristic("village", "minecraft:village_savanna", "minecraft:acacia_stairs")); + assertTrue(characteristic("village", "minecraft:village_snowy", "minecraft:spruce_planks")); + assertTrue(characteristic("village", "minecraft:village_snowy", "minecraft:stripped_spruce_log")); + assertTrue(characteristic("village", "minecraft:village_taiga", "minecraft:cobblestone")); + assertTrue(characteristic("monument", "minecraft:monument", "minecraft:dark_prismarine")); + } + + @Test + public void naturalTerrainAndWrongVillageWoodAreNotCharacteristic() { + assertFalse(characteristic("stronghold", "minecraft:stronghold", "minecraft:stone")); + assertFalse(characteristic("trial_chambers", "minecraft:trial_chambers", "minecraft:tuff")); + assertFalse(characteristic("mansion", "minecraft:mansion", "minecraft:dark_oak_leaves")); + assertFalse(characteristic("village", "minecraft:village_desert", "minecraft:sandstone")); + assertFalse(characteristic("village", "minecraft:village_savanna", "minecraft:oak_planks")); + assertFalse(characteristic("monument", "minecraft:monument", "minecraft:water")); + } + + @Test + public void materialEvidenceMustExist() { + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(0, 0, 1)); + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 0, 1)); + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 0)); + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 2, 1)); + } + + @Test + public void singleChunkStructureAcceptsMaterialInItsOnlyChunk() { + assertTrue(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 1)); + } + + @Test + public void multiChunkStructureRejectsMaterialConfinedToOneChunk() { + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 4)); + assertTrue(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 2, 4)); + } + + @Test + public void configuredVerticalShiftRequiresSafetyClampedGenerationEvidence() { + assertTrue(WorldCheckPredicates.verticalShiftMatches(0, null, -32, 20, -64, 320)); + assertFalse(WorldCheckPredicates.verticalShiftMatches(0, null, -112, -80, -64, 320)); + assertTrue(WorldCheckPredicates.verticalShiftMatches(0, 0, -32, 20, -64, 320)); + assertTrue(WorldCheckPredicates.verticalShiftMatches(0, 48, -64, -32, -64, 320)); + assertTrue(WorldCheckPredicates.verticalShiftMatches(-64, -64, -48, 4, -64, 320)); + assertTrue(WorldCheckPredicates.verticalShiftMatches(-64, -16, -64, -12, -64, 320)); + assertFalse(WorldCheckPredicates.verticalShiftMatches(-64, null, -48, 4, -64, 320)); + assertFalse(WorldCheckPredicates.verticalShiftMatches(-64, -15, -63, -11, -64, 320)); + assertFalse(WorldCheckPredicates.verticalShiftMatches(0, -1, -33, 19, -64, 320)); + } + + @Test + public void mansionVegetationGateRejectsRemainingLeaves() { + assertTrue(WorldCheckPredicates.mansionVegetationPass(0)); + assertFalse(WorldCheckPredicates.mansionVegetationPass(1)); + } + + @Test + public void mansionVegetationAuditIgnoresTemplateBlocksAndRejectsVegetationAbovePieces() { + assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(true, 80, 80)); + assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(true, 79, 80)); + assertTrue(WorldCheckPredicates.mansionVegetationAbovePiece(true, 81, 80)); + assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(false, 81, 80)); + } + + @Test + public void villageFoundationGateRejectsUnsupportedColumns() { + assertTrue(WorldCheckPredicates.villageFoundationPass(0)); + assertFalse(WorldCheckPredicates.villageFoundationPass(1)); + } + + @Test + public void villagePoiGateRequiresInBoundsPoiWithoutOutOfBoundsRecords() { + assertTrue(WorldCheckPredicates.villagePoiPass(1, 0)); + assertFalse(WorldCheckPredicates.villagePoiPass(0, 0)); + assertFalse(WorldCheckPredicates.villagePoiPass(1, 1)); + } + + @Test + public void smallStructureFootprintIncludesEveryChunk() { + BoundingBox bounds = new BoundingBox(-16, -20, -16, 31, 120, 31); + + List chunks = WorldCheckStructureAudit.boundedFootprintChunks(bounds, ChunkPos.ZERO, 96); + + assertEquals(9, chunks.size()); + assertTrue(chunks.contains(new ChunkPos(-1, -1))); + assertTrue(chunks.contains(new ChunkPos(1, 1))); + } + + @Test + public void largeStructureFootprintIsBoundedAndSamplesEdges() { + BoundingBox bounds = new BoundingBox(-512, -64, -512, 511, 300, 511); + + List chunks = WorldCheckStructureAudit.boundedFootprintChunks(bounds, ChunkPos.ZERO, 20); + + assertTrue(chunks.size() <= 20); + assertTrue(chunks.size() >= 16); + assertTrue(chunks.contains(ChunkPos.ZERO)); + assertTrue(chunks.contains(new ChunkPos(-32, -32))); + assertTrue(chunks.contains(new ChunkPos(31, 31))); + } + + @Test + public void qaEventsEscapeStructuredValues() { + String event = WorldCheckPredicates.qaEventJson("locate\"", "village\n", false, "x\\y\t"); + + assertEquals("QA_EVT {\"event\":\"locate\\\"\",\"structure\":\"village\\n\"," + + "\"pass\":false,\"detail\":\"x\\\\y\\t\"}", event); + } + + @Test + public void passRequestsStopAfterValidation() { + List events = new ArrayList<>(); + + int status = ModdedWorldCheck.runAndRequestStop( + () -> { + events.add("check"); + return true; + }, + () -> events.add("request-stop") + ); + + assertEquals(0, status); + assertEquals(List.of("check", "request-stop"), events); + } + + @Test + public void failureStillRequestsStop() { + List events = new ArrayList<>(); + + int status = ModdedWorldCheck.runAndRequestStop( + () -> { + events.add("check"); + return false; + }, + () -> events.add("request-stop") + ); + + assertEquals(1, status); + assertEquals(List.of("check", "request-stop"), events); + } + + @Test + public void thrownCheckStillRequestsStop() { + AtomicBoolean stopRequested = new AtomicBoolean(false); + + int status = ModdedWorldCheck.runAndRequestStop( + () -> { + throw new IllegalStateException("check failed"); + }, + () -> stopRequested.set(true) + ); + + assertEquals(1, status); + assertTrue(stopRequested.get()); + } + + @Test + public void stopRequestFailureForcesNonzeroResult() { + int status = ModdedWorldCheck.runAndRequestStop( + () -> true, + () -> { + throw new IllegalStateException("stop request failed"); + } + ); + + assertEquals(1, status); + } + + @Test + public void coordinatorAwaitsServerBeforeExit() { + List events = new ArrayList<>(); + + ModdedWorldCheck.awaitStopAndExit( + () -> events.add("await-stop"), + 0, + status -> events.add("exit:" + status) + ); + + assertEquals(List.of("await-stop", "exit:0"), events); + } + + @Test + public void shutdownWaitFailureForcesNonzeroExit() { + AtomicInteger status = new AtomicInteger(-1); + + ModdedWorldCheck.awaitStopAndExit( + () -> { + throw new IllegalStateException("shutdown wait failed"); + }, + 0, + status::set + ); + + assertEquals(1, status.get()); + } + + @Test + public void interruptionIsClearedForShutdownAndRestoredBeforeExit() { + AtomicBoolean interruptedDuringStop = new AtomicBoolean(true); + AtomicBoolean interruptedDuringExit = new AtomicBoolean(false); + AtomicInteger status = new AtomicInteger(-1); + Thread.currentThread().interrupt(); + try { + ModdedWorldCheck.awaitStopAndExit( + () -> interruptedDuringStop.set(Thread.currentThread().isInterrupted()), + 0, + exitStatus -> { + status.set(exitStatus); + interruptedDuringExit.set(Thread.currentThread().isInterrupted()); + } + ); + + assertFalse(interruptedDuringStop.get()); + assertTrue(interruptedDuringExit.get()); + assertEquals(1, status.get()); + } finally { + Thread.interrupted(); + } + } + + private static boolean characteristic(String structureLabel, String structureKey, String blockKey) { + return WorldCheckMaterials.isCharacteristicMaterial(structureLabel, + Identifier.parse(structureKey), Identifier.parse(blockKey)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldManagerParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldManagerParityTest.java new file mode 100644 index 000000000..1882c95ae --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldManagerParityTest.java @@ -0,0 +1,35 @@ +package art.arcane.iris.modded; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ModdedWorldManagerParityTest { + @Test + public void ambientChunkReservoirFillsBeforeSampling() { + assertEquals(0, ModdedWorldManager.reservoirSlot(0, 4, 0)); + assertEquals(3, ModdedWorldManager.reservoirSlot(3, 4, 0)); + } + + @Test + public void ambientChunkReservoirReplacesOnlyOnInRangeRolls() { + assertEquals(2, ModdedWorldManager.reservoirSlot(4, 4, 2)); + assertEquals(0, ModdedWorldManager.reservoirSlot(9, 4, 0)); + assertEquals(-1, ModdedWorldManager.reservoirSlot(4, 4, 4)); + assertEquals(-1, ModdedWorldManager.reservoirSlot(9, 4, 7)); + } + + @Test + public void normalWorldAlwaysAllowsEntitySpawning() { + assertTrue(ModdedWorldManager.entitySpawningEnabled(false, false)); + assertTrue(ModdedWorldManager.entitySpawningEnabled(false, true)); + } + + @Test + public void studioWorldRequiresItsEntitySpawningSetting() { + assertFalse(ModdedWorldManager.entitySpawningEnabled(true, false)); + assertTrue(ModdedWorldManager.entitySpawningEnabled(true, true)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java new file mode 100644 index 000000000..7ee5f5f67 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java @@ -0,0 +1,134 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.nativegen.NativeStructureGenerationException; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import org.junit.BeforeClass; +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.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class NativeStructureFailureContractTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException { + String source = moddedSource("IrisModdedChunkGenerator.java"); + int locateStart = source.indexOf("public Pair> findNearestMapStructure"); + int locateEnd = source.indexOf("public boolean isNativeStructureReachable", locateStart); + String locate = source.substring(locateStart, locateEnd); + String stage = moddedSource("ModdedNativeStructureStage.java"); + int filterStart = stage.indexOf("HolderSet filterReachableNativeStructures"); + int filterEnd = stage.indexOf("void adjustGeneratedStructures", filterStart); + String filter = stage.substring(filterStart, filterEnd); + + assertTrue(locate.contains("Engine current = engine();")); + assertFalse(locate.contains("catch (Throwable")); + assertFalse(locate.contains("return null;\n } catch")); + assertTrue(filter.contains("unregistered structure holder")); + assertFalse(filter.contains("catch (Throwable")); + assertFalse(filter.contains("failed closed")); + } + + @Test + public void globalStructureDisableRefusesGeneratorBinding() { + IrisModdedChunkGenerator.requireGlobalStructureGeneration(true, "overworld:overworld"); + + IllegalStateException error = assertThrows( + IllegalStateException.class, + () -> IrisModdedChunkGenerator.requireGlobalStructureGeneration( + false, "overworld:overworld")); + + assertTrue(error.getMessage().contains("overworld:overworld")); + assertTrue(error.getMessage().contains("generate-structures=false")); + assertTrue(error.getMessage().contains("importedStructures.disabled")); + assertTrue(error.getMessage().contains("importedStructures.disabledExact")); + } + + @Test + public void structureVegetationCleanupPrecedesTerrainPreparationAndPlacement() throws IOException { + String source = moddedSource("ModdedNativeStructureStage.java"); + int placementStart = source.indexOf("void placeVanillaStructures"); + int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart); + String placement = source.substring(placementStart, placementEnd); + + assertTrue(placement.contains("\"terrain integration\"")); + assertTrue(placement.contains("\"terrain preparation\"")); + assertTrue(placement.contains("\"foundation repair\"")); + assertFalse(placement.contains("\"terrain carving\"")); + assertTrue(placement.contains("visitExistingPois(chunk")); + assertTrue(placement.contains("level.updatePOIOnBlockStateChange(")); + assertTrue(placement.contains("Blocks.AIR.defaultBlockState(), state")); + assertTrue(placement.indexOf("visitExistingPois(chunk") + < placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")); + 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")); + } + + @Test + public void structurePlacementPrimesNeighbourWorldgenHeightmapsBeforeTerrainPreparation() throws IOException { + String source = moddedSource("ModdedNativeStructureStage.java"); + int placementStart = source.indexOf("void placeVanillaStructures"); + int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart); + String placement = source.substring(placementStart, placementEnd); + + assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;")); + assertTrue(placement.contains("WorldgenTerrainHeightmaps.primeStructurePlacement(")); + assertTrue(placement.contains("\"heightmap priming\"")); + assertTrue(placement.contains("heightmapStarts.add(start);")); + assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(") + < placement.indexOf("prepareSurfaceStructures")); + assertTrue(source.contains("generationEngine.getHeight(x, z, false) + runtimeMinY + 1")); + assertTrue(source.contains("generationEngine.getHeight(x, z, true) + runtimeMinY + 1")); + assertTrue(source.contains("int minY = chunk.getMinY() + 1;")); + } + + @Test + public void nativeStructurePostProcessingUsesBoundedWorldgenAccess() throws IOException { + String source = moddedSource("ModdedNativeStructureStage.java"); + int placementStart = source.indexOf("private void placeVanillaStructure"); + int placementEnd = source.indexOf("private List> structuresByStep", placementStart); + String placement = source.substring(placementStart, placementEnd); + + assertTrue(placement.contains("ModdedNativeStructureWorldgenAccess.create(")); + assertTrue(placement.contains("NativeStructurePostProcessor.place(\n boundedWorld")); + assertFalse(placement.contains("NativeStructurePostProcessor.place(world,")); + assertTrue(placement.contains("world.setCurrentlyGenerating(null);")); + } + + @Test + public void structureFailurePreservesPhaseIdentityChunkAndCause() { + IllegalArgumentException cause = new IllegalArgumentException("broken placement"); + NativeStructureGenerationException error = NativeStructureGenerationException.failure( + "placement", "minecraft:monument", 12, -8, cause); + + assertSame(cause, error.getCause()); + assertTrue(error.getMessage().contains("placement")); + assertTrue(error.getMessage().contains("minecraft:monument")); + assertTrue(error.getMessage().contains("12,-8")); + assertTrue(error.getMessage().contains("aborted")); + } + + private static String moddedSource(String fileName) throws IOException { + Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"), + "art", "arcane", "iris", "modded", fileName); + return Files.readString(sourcePath); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/SchemaBuilderPlatformIsolationTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/SchemaBuilderPlatformIsolationTest.java new file mode 100644 index 000000000..42d763ab5 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/SchemaBuilderPlatformIsolationTest.java @@ -0,0 +1,53 @@ +package art.arcane.iris.modded; + +import art.arcane.iris.core.project.SchemaBuilder; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class SchemaBuilderPlatformIsolationTest { + @Test + public void enumSchemaDoesNotResolveBukkitOnlyMethodSignatures() { + assertThrows(ClassNotFoundException.class, () -> Class.forName("org.bukkit.block.BlockFace")); + + JSONObject schema = new SchemaBuilder(DirectionModel.class, null).construct(); + JSONObject direction = schema.getJSONObject("properties").getJSONObject("direction"); + String definitionKey = direction.getString("$ref").substring("#/definitions/".length()); + JSONArray values = schema.getJSONObject("definitions").getJSONObject(definitionKey).getJSONArray("oneOf"); + List names = new ArrayList<>(values.length()); + for (int index = 0; index < values.length(); index++) { + names.add(values.getJSONObject(index).getString("const")); + } + + assertEquals(List.of( + "UP_POSITIVE_Y", + "DOWN_NEGATIVE_Y", + "NORTH_NEGATIVE_Z", + "SOUTH_POSITIVE_Z", + "EAST_POSITIVE_X", + "WEST_NEGATIVE_X"), names); + assertEquals("#/definitions/" + definitionKey, + schema.getJSONObject("properties").getJSONObject("directions") + .getJSONObject("items").getString("$ref")); + } + + @Desc("Direction model.") + public static class DirectionModel { + @Desc("Direction.") + private IrisDirection direction = IrisDirection.NORTH_NEGATIVE_Z; + + @Desc("Directions.") + @ArrayType(type = IrisDirection.class) + private KList directions = new KList<>(); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/api/ModdedCustomContentRegistryTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/api/ModdedCustomContentRegistryTest.java new file mode 100644 index 000000000..694276da5 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/api/ModdedCustomContentRegistryTest.java @@ -0,0 +1,84 @@ +package art.arcane.iris.modded.api; + +import net.minecraft.resources.Identifier; +import org.junit.Test; + +import java.util.Collection; +import java.util.List; + +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; + +public class ModdedCustomContentRegistryTest { + @Test + public void publishesProvidersOnlyAfterCompleteDiscoveryAndCanRollBack() { + String modId = "iris_discovery_success"; + TestProvider provider = new TestProvider(modId, null); + boolean previousDiscoveryComplete = ModdedCustomContentRegistry.discoveryComplete(); + + ModdedCustomContentRegistry.Discovery discovery = + ModdedCustomContentRegistry.discover(List.of(provider)); + try { + assertTrue(ModdedCustomContentRegistry.discoveryComplete()); + assertTrue(ModdedCustomContentRegistry.hasProvider(modId)); + } finally { + discovery.rollback(); + } + + assertEquals(previousDiscoveryComplete, ModdedCustomContentRegistry.discoveryComplete()); + assertFalse(ModdedCustomContentRegistry.hasProvider(modId)); + } + + @Test + public void failedDiscoveryPublishesNothingAndPreservesTheCause() { + String firstModId = "iris_discovery_staged"; + String failingModId = "iris_discovery_failure"; + RuntimeException original = new RuntimeException("provider init failed"); + boolean previousDiscoveryComplete = ModdedCustomContentRegistry.discoveryComplete(); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> ModdedCustomContentRegistry.discover(List.of( + new TestProvider(firstModId, null), + new TestProvider(failingModId, original)))); + + assertSame(original, thrown); + assertEquals(previousDiscoveryComplete, ModdedCustomContentRegistry.discoveryComplete()); + assertFalse(ModdedCustomContentRegistry.hasProvider(firstModId)); + assertFalse(ModdedCustomContentRegistry.hasProvider(failingModId)); + } + + private static final class TestProvider implements ModdedDataProvider { + private final String modId; + private final RuntimeException failure; + + private TestProvider(String modId, RuntimeException failure) { + this.modId = modId; + this.failure = failure; + } + + @Override + public String modId() { + return modId; + } + + @Override + public Collection getTypes(ModdedDataType type) { + return List.of(); + } + + @Override + public boolean isValidProvider(Identifier id, ModdedDataType type) { + return false; + } + + @Override + public void init() { + if (failure != null) { + throw failure; + } + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedCommandParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedCommandParityTest.java new file mode 100644 index 000000000..05c66d837 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedCommandParityTest.java @@ -0,0 +1,187 @@ +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.PackDownloadMessages; +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import art.arcane.iris.core.pack.PackDownloader; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.CommandNode; +import net.minecraft.SharedConstants; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.Bootstrap; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class IrisModdedCommandParityTest { + @Test + public void registersPluginParityWhatCommandsAndAliases() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + CommandDispatcher dispatcher = new CommandDispatcher<>(); + + IrisModdedCommands.register(dispatcher); + + CommandNode iris = child(dispatcher.getRoot(), "iris"); + CommandNode what = child(iris, "what"); + child(what, "here"); + child(what, "biome"); + child(what, "region"); + child(what, "block"); + child(what, "hand"); + child(what, "markers"); + child(iris, "dust"); + child(iris, "d"); + child(iris, "create"); + child(iris, "c"); + child(iris, "teleport"); + child(iris, "tp"); + child(iris, "height"); + child(iris, "worlds"); + child(iris, "accesslist"); + child(child(iris, "goto"), "unregistered"); + + CommandNode edit = child(iris, "edit"); + child(edit, "b"); + child(edit, "r"); + child(edit, "d"); + CommandNode studio = child(iris, "studio"); + child(studio, "package"); + child(studio, "pkg"); + + CommandNode download = child(iris, "download"); + CommandNode source = child(download, "source"); + assertTrue(source.getChildren().isEmpty()); + + assertSame(iris, child(dispatcher.getRoot(), "ir").getRedirect()); + assertSame(iris, child(dispatcher.getRoot(), "irs").getRedirect()); + } + + @Test + public void downloadRequestAcceptsOnlyBuiltInsAndZipLinks() { + IrisModdedCommands.DownloadRequest overworld = IrisModdedCommands.parseDownloadRequest("pack=overworld"); + IrisModdedCommands.DownloadRequest underworld = IrisModdedCommands.parseDownloadRequest("pack=UNDERWORLD"); + IrisModdedCommands.DownloadRequest link = IrisModdedCommands.parseDownloadRequest( + "link=https://packs.example.test/custom.zip?token=a=b" + ); + + assertNotNull(overworld); + assertEquals("overworld", overworld.pack()); + assertNotNull(underworld); + assertEquals("underworld", underworld.pack()); + assertNotNull(link); + assertEquals("https://packs.example.test/custom.zip?token=a=b", link.url()); + String displayTarget = IrisModdedCommands.downloadDisplayTarget(link); + assertEquals(IrisLanguage.plain(PackDownloadMessages.PROGRESS_SOURCE_REMOTE), displayTarget); + assertFalse(displayTarget.contains("token")); + assertNull(IrisModdedCommands.parseDownloadRequest("overworld")); + assertNull(IrisModdedCommands.parseDownloadRequest("pack=custom")); + assertNull(IrisModdedCommands.parseDownloadRequest("link=https://packs.example.test/custom.tar.gz")); + assertNull(IrisModdedCommands.parseDownloadRequest("pack=underworld branch=stable")); + assertNull(IrisModdedCommands.parseDownloadRequest("pack=underworld overwrite=true")); + } + + @Test + public void downloadCompletionMessageOnlyReportsActualPackChanges() { + assertEquals( + "Pack installed on disk. Restart the server before using it.", + IrisModdedCommands.downloadCompletionMessage( + new PackDownloader.PackInstallResult("overworld", true, true) + ) + ); + assertEquals( + "Pack installed on disk.", + IrisModdedCommands.downloadCompletionMessage( + new PackDownloader.PackInstallResult("overworld", true, false) + ) + ); + assertNull(IrisModdedCommands.downloadCompletionMessage( + new PackDownloader.PackInstallResult("overworld", false, false) + )); + assertNull(IrisModdedCommands.downloadCompletionMessage(null)); + } + + @Test + public void downloadBusyMessageDistinguishesPackDownloadsFromOtherLifecycleWork() { + LifecycleOperationCoordinator.ActiveOperation packDownload = new LifecycleOperationCoordinator.ActiveOperation( + 1L, + LifecycleOperationCoordinator.Domain.PACK_MUTATION, + LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, + "overworld" + ); + LifecycleOperationCoordinator.ActiveOperation worldCreation = new LifecycleOperationCoordinator.ActiveOperation( + 2L, + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_CREATE, + "iris_world" + ); + + assertEquals( + IrisLanguage.plain(PackDownloadMessages.IN_PROGRESS), + IrisModdedCommands.downloadBusyMessage(packDownload) + ); + assertEquals( + "Iris pack changes are busy with world_create for 'iris_world'. Try again when it completes.", + IrisModdedCommands.downloadBusyMessage(worldCreation) + ); + } + + @Test + public void helpDocumentsParityCommandsAndPlatformStubs() { + assertTrue(ModdedCommandHelp.documents("what", "here")); + assertTrue(ModdedCommandHelp.documents("what", "biome")); + assertTrue(ModdedCommandHelp.documents("what", "region")); + assertTrue(ModdedCommandHelp.documents("what", "block")); + assertTrue(ModdedCommandHelp.documents("what", "hand")); + assertTrue(ModdedCommandHelp.documents("what", "markers")); + assertTrue(ModdedCommandHelp.documents("", "dust")); + assertTrue(ModdedCommandHelp.documents("", "teleport")); + assertTrue(ModdedCommandHelp.documents("", "c")); + assertTrue(ModdedCommandHelp.documents("edit", "b")); + assertTrue(ModdedCommandHelp.documents("studio", "pkg")); + assertTrue(ModdedCommandHelp.documents("object", "we")); + assertTrue(ModdedCommandHelp.documents("world", "mainworld")); + assertTrue(ModdedCommandHelp.documents("goto", "unregistered")); + } + + @Test + public void categoryLiteralsFallBackToHelpWithoutArguments() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + CommandDispatcher dispatcher = new CommandDispatcher<>(); + + IrisModdedCommands.register(dispatcher); + + CommandNode iris = child(dispatcher.getRoot(), "iris"); + assertNotNull("iris", iris.getCommand()); + for (String category : new String[]{"help", "find", "goto", "pregen", "pregenerate", "object", "o", + "edit", "studio", "std", "s", "pack", "pk", "world", "w", "datapack", "datapacks", "dp", + "structure", "struct", "str", "developer", "dev"}) { + assertNotNull(category, child(iris, category).getCommand()); + } + } + + @Test + public void consoleHelpListsEveryEntryWithInlineUsageAndDescription() { + List root = ModdedCommandHelp.consoleLines(""); + + assertTrue(root.stream().anyMatch((String line) -> line.startsWith("/iris developer (dev) - "))); + assertTrue(root.stream().anyMatch((String line) -> line.startsWith("/iris teleport [player] (tp) - "))); + assertTrue(ModdedCommandHelp.consoleLines("pregen").stream().anyMatch((String line) -> + line.startsWith("/iris pregen start [dimension] [at] [x] [z] [gui] [sync] [nocache] - "))); + } + + private static CommandNode child( + CommandNode parent, String name) { + CommandNode child = parent.getChild(name); + assertNotNull(name, child); + return child; + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedStructureCommandTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedStructureCommandTest.java new file mode 100644 index 000000000..8e5a412b6 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedStructureCommandTest.java @@ -0,0 +1,337 @@ +package art.arcane.iris.modded.command; + +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import art.arcane.iris.nativegen.NativeStructureLocateResults; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class IrisModdedStructureCommandTest { + @Test + public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() { + BlockPos origin = BlockPos.ZERO; + Pair irisNear = Pair.of(new BlockPos(4, 70, 0), "iris"); + Pair nativeFar = Pair.of(new BlockPos(8, 70, 0), "native"); + AtomicInteger irisReferences = new AtomicInteger(); + AtomicInteger nativeReferences = new AtomicInteger(); + + Pair irisSelected = NativeStructureLocateResults.selectAndReference( + origin, + irisNear, () -> irisReferences.incrementAndGet(), + nativeFar, () -> nativeReferences.incrementAndGet()); + + assertSame(irisNear, irisSelected); + assertEquals(1, irisReferences.get()); + assertEquals(0, nativeReferences.get()); + + Pair nativeNear = Pair.of(new BlockPos(2, 70, 0), "native"); + Pair nativeSelected = NativeStructureLocateResults.selectAndReference( + origin, + irisNear, () -> irisReferences.incrementAndGet(), + nativeNear, () -> nativeReferences.incrementAndGet()); + + assertSame(nativeNear, nativeSelected); + assertEquals(1, irisReferences.get()); + assertEquals(1, nativeReferences.get()); + } + + @Test + public void gotoStructureSupportsIrisAndNativeRegistryTargets() throws IOException { + String source = source("ModdedLocateCommands.java"); + String suggestions = source("ModdedCommandSuggestions.java"); + + assertTrue(source.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)")); + assertTrue(source.contains("registry.get(identifier)")); + assertTrue(source.contains("getPlacementsForStructure(holder)")); + assertTrue(source.contains("generator.findNearestMapStructure(")); + assertTrue(source.contains("NATIVE_STRUCTURE_LOCATE_RADIUS = 100")); + assertTrue(source.contains("HolderSet.direct(target.holder())")); + assertFalse(source.contains("NativeStructureLocateCapability")); + assertTrue(source.contains("boolean teleported = player.teleportTo(")); + assertTrue(suggestions.contains("combineStructureKeys(unregisteredIrisKeys, nativeKeys)")); + assertTrue(source.contains("irisGenerator.isNativeStructureReachable(holder)")); + assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED")); + assertTrue(source.contains("IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS")); + assertTrue(source.contains("int targetX = result.originX()")); + assertTrue(source.contains("int targetY = result.baseY() + 2")); + assertTrue(source.contains("int targetZ = result.originZ()")); + assertFalse(source.contains("at[0] + 8")); + assertFalse(source.contains("at[2] + 8")); + } + + @Test + public void registeredStructureEligibilityMatchesGotoAndSuggestions() { + IrisNativeStructureDecision nativeDecision = decision(NativeStructureGenerationStatus.GENERATE_NATIVE); + IrisNativeStructureDecision replacementDecision = decision( + NativeStructureGenerationStatus.REPLACED_BY_IRIS); + IrisNativeStructureDecision disabledDecision = decision( + NativeStructureGenerationStatus.DISABLED_BY_PACK); + + assertTrue(ModdedCommandSuggestions.isEligibleRegisteredStructure( + nativeDecision, false, false, false, true, true)); + assertTrue(ModdedCommandSuggestions.isEligibleRegisteredStructure( + nativeDecision, true, true, false, false, true)); + assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure( + nativeDecision, true, false, false, false, true)); + assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure( + nativeDecision, true, true, false, true, false)); + assertTrue(ModdedCommandSuggestions.isEligibleRegisteredStructure( + replacementDecision, false, false, true, false, false)); + assertTrue(ModdedCommandSuggestions.isEligibleRegisteredStructure( + replacementDecision, true, true, false, false, true)); + assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure( + replacementDecision, true, true, false, false, false)); + assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure( + replacementDecision, false, false, false, true, true)); + assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure( + disabledDecision, true, true, true, true, true)); + } + + @Test + public void structureSuggestionsDedupeAndSortPlacedAndNativeKeys() { + List suggestions = ModdedCommandSuggestions.combineStructureKeys( + List.of("towns_and_towers:village_forest", "minecraft:village_plains", + "towns_and_towers:village_forest", "MINECRAFT:VILLAGE_PLAINS"), + List.of("minecraft:stronghold", "minecraft:village_plains")); + + assertEquals(List.of("minecraft:stronghold", "minecraft:village_plains", + "towns_and_towers:village_forest"), suggestions); + } + + @Test + public void structureSuggestionsCannotReintroduceRegistryOrNativePlacementCollisions() { + List suggestions = ModdedCommandSuggestions.eligibleUnregisteredEditableKeys( + List.of("iris:custom", "minecraft:disabled", "MINECRAFT:UNREACHABLE", + "iris:native_collision"), + Set.of("minecraft:disabled", "minecraft:unreachable"), + (String key) -> key.equalsIgnoreCase("iris:native_collision")); + + assertEquals(List.of("iris:custom"), suggestions); + } + + @Test + public void unregisteredReportFindsOnlyConfiguredNativeKeysAbsentFromRegistry() { + List missing = ModdedUnregisteredStructures.missingConfiguredNativeKeys( + List.of("iris:editable", "missing:native", "MISSING:NATIVE", "registered:native"), + Set.of("registered:native"), + (String key) -> key.equalsIgnoreCase("missing:native") + || key.equalsIgnoreCase("registered:native")); + + assertEquals(List.of("missing:native"), missing); + } + + @Test + public void unregisteredReportDistinguishesEveryNativeExclusionReason() { + assertEquals(ModdedLocateCommands.NativeStructureAvailability.WORLD_DISABLED, + ModdedLocateCommands.classifyNativeAvailability(false, true, false, false, true, true)); + assertEquals(ModdedLocateCommands.NativeStructureAvailability.FILTERED, + ModdedLocateCommands.classifyNativeAvailability(true, false, false, false, true, true)); + assertEquals(ModdedLocateCommands.NativeStructureAvailability.IRIS_SUPPRESSED, + ModdedLocateCommands.classifyNativeAvailability(true, true, true, false, true, true)); + assertEquals(ModdedLocateCommands.NativeStructureAvailability.EMPTY_BIOME_FILTER, + ModdedLocateCommands.classifyNativeAvailability(true, true, false, true, false, false)); + assertEquals(ModdedLocateCommands.NativeStructureAvailability.BIOME_UNREACHABLE, + ModdedLocateCommands.classifyNativeAvailability(true, true, false, false, false, false)); + assertEquals(ModdedLocateCommands.NativeStructureAvailability.NO_PLACEMENT, + ModdedLocateCommands.classifyNativeAvailability(true, true, false, false, true, false)); + assertEquals(ModdedLocateCommands.NativeStructureAvailability.AVAILABLE, + ModdedLocateCommands.classifyNativeAvailability(true, true, false, false, true, true)); + + assertTrue(ModdedLocateCommands.nativeUnavailableMessage( + "towns_and_towers:exclusive", + ModdedLocateCommands.NativeStructureAvailability.EMPTY_BIOME_FILTER) + .contains("resolves to zero registered biomes")); + assertTrue(ModdedLocateCommands.nativeUnavailableMessage( + "minecraft:village_plains", + ModdedLocateCommands.NativeStructureAvailability.NO_PLACEMENT) + .contains("no active positive-weight, positive-frequency structure-set placement")); + String combined = ModdedLocateCommands.registeredStructureUnavailableMessage( + "towns_and_towers:exclusive", + ModdedLocateCommands.NativeStructureAvailability.EMPTY_BIOME_FILTER, + decision(NativeStructureGenerationStatus.GENERATE_NATIVE), + true, false, true, false); + assertTrue(combined.contains("resolves to zero registered biomes")); + assertTrue(combined.contains("matching Iris nativeStructures placement is also configured")); + } + + @Test + public void unregisteredReportExplainsEditablePlacementState() { + assertTrue(ModdedUnregisteredStructures.editableExclusionReason(false) + .contains("no biome, region, or dimension structure placement")); + assertTrue(ModdedUnregisteredStructures.editableExclusionReason(true) + .contains("non-positive density or a Y band outside")); + } + + @Test + public void unregisteredCommandUsesGotoEligibilityAndPrintsToConsole() throws IOException { + String command = source("ModdedUnregisteredStructures.java"); + String tree = source("ModdedCommandTree.java"); + + assertTrue(tree.contains("Commands.literal(\"unregistered\")")); + assertTrue(tree.contains("ModdedUnregisteredStructures.print(context.getSource())")); + assertTrue(command.contains("ModdedCommandSuggestions.isEligibleRegisteredStructure(")); + assertTrue(command.contains("ModdedLocateCommands.registeredStructureUnavailableMessage(")); + assertTrue(command.contains("engine.getData().getStructureLoader().getPossibleKeys()")); + assertTrue(command.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)")); + assertTrue(command.contains("LOGGER.info(\"[Iris goto unregistered] [{}] {} - {}\"")); + assertTrue(command.contains("UNREGISTERED(\"unregistered\")")); + assertFalse(command.contains("DatapackIngestService")); + } + + @Test + public void structureSuggestionsUseActiveLevelAvailability() throws IOException { + String suggestions = source("ModdedCommandSuggestions.java"); + int methodStart = suggestions.indexOf("static CompletableFuture suggestStructureKeys("); + int methodEnd = suggestions.indexOf("static boolean isEligibleRegisteredStructure(", methodStart); + String method = suggestions.substring(methodStart, methodEnd); + + assertTrue(method.contains("ServerLevel level = source.getLevel()")); + assertTrue(method.contains("Engine engine = IrisModdedCommands.engineFor(level)")); + assertFalse(method.contains("IrisStructureLocator.locatableKeys(engine)")); + assertTrue(method.contains("IrisStructureLocator.locatableEditableKeys(engine)")); + assertTrue(method.contains("StructureReachability.reachableKeys(engine)")); + assertTrue(method.contains("NativeStructureGenerationPolicy.resolve(engine, key, false)")); + assertTrue(method.contains("IrisStructureLocator.hasLocatableNativePlacement(engine, key)")); + assertTrue(method.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)")); + assertTrue(method.contains("reachableNativeKeys.contains(normalizeKey(key))")); + assertTrue(method.contains("nativeGenerationEnabled")); + assertTrue(method.contains("eligibleUnregisteredEditableKeys(")); + assertTrue(method.contains("IrisStructureLocator.hasNativePlacement(engine, candidate)")); + assertFalse(method.contains("NativeStructureGenerationKeys.active(level)")); + assertFalse(method.contains("getPlacementsForStructure")); + } + + @Test + public void generatorLocateUsesEveryIrisPlacedNativeStructure() throws IOException { + String source = moddedSource("ModdedNativeStructureStage.java"); + int methodStart = source.indexOf("Pair> findNearestIrisStructure("); + int methodEnd = source.indexOf("HolderSet filterReachableNativeStructures(", methodStart); + String method = source.substring(methodStart, methodEnd); + int registryLookup = method.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)"); + int placedCheck = method.indexOf( + "if (!IrisStructureLocator.hasNativePlacement(current, structureId))"); + int irisLocate = method.indexOf("NativeStructureLocatePersistence.search(", placedCheck); + int nearestSelection = method.indexOf( + "NativeStructureLocateResults.nearest(pos, predicted, nativeLocated)", irisLocate); + int selectedVerification = method.indexOf("bestSearch.search().verify(bestResult)", nearestSelection); + int selectedReference = method.indexOf( + "selectedSearch.search().reference(selectedStart)", selectedVerification); + + assertTrue(registryLookup >= 0); + assertTrue(placedCheck > registryLookup); + assertTrue(irisLocate > placedCheck); + assertTrue(nearestSelection > irisLocate); + assertTrue(selectedVerification > nearestSelection); + assertTrue(selectedReference > selectedVerification); + assertTrue(method.contains("NativeStructureLocatePersistence.probe(")); + assertTrue(method.contains("findUnexplored")); + assertTrue(method.contains("LocateStatus.SEARCH_LIMIT_REACHED")); + assertTrue(method.contains("verified.ownership().locatorY()")); + assertTrue(method.contains("selectedSearch.search().reference(selectedStart)")); + assertTrue(method.contains("NativeStructureLocateResults.selectAndReference(")); + assertFalse(method.contains("NativeStructureLocateCapability")); + assertTrue(source.contains("structureBiomeSource.isStructureReachable(holder)")); + assertFalse(source.contains("isPaperUnavailable")); + String generator = moddedSource("IrisModdedChunkGenerator.java"); + assertTrue(generator.contains("NativeStructureVanillaLocator.predict(")); + assertFalse(generator.contains("super.findNearestMapStructure(level, reachable")); + } + + @Test + public void commandResolvesNativePolicyBeforeAnyVanillaAliasLookup() throws IOException { + String source = source("ModdedLocateCommands.java"); + int methodStart = source.indexOf("static int gotoStructure("); + int methodEnd = source.indexOf("private static void locateIrisStructure(", methodStart); + String method = source.substring(methodStart, methodEnd); + int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)"); + int editableIrisLookup = method.indexOf( + "IrisStructureLocator.hasLocatableEditablePlacement(engine, key)", nativeResolution); + int policyResolution = method.indexOf( + "NativeStructureGenerationPolicy.resolve(engine, target.key(), false)", editableIrisLookup); + int eligibilityCheck = method.indexOf( + "ModdedCommandSuggestions.isEligibleRegisteredStructure(", policyResolution); + int replacementLocate = method.indexOf( + "locateIrisStructure(source, level, engine, player, target.key())", eligibilityCheck); + int nativeLocate = method.indexOf("runNativeStructureLocate(source, level, player, target)", + replacementLocate); + + assertTrue(nativeResolution >= 0); + assertTrue(editableIrisLookup > nativeResolution); + assertTrue(policyResolution > editableIrisLookup); + assertTrue(eligibilityCheck > policyResolution); + assertTrue(replacementLocate > eligibilityCheck); + assertTrue(nativeLocate > replacementLocate); + assertTrue(method.contains("IrisStructureLocator.hasLocatableNativePlacement(engine, target.key())")); + assertTrue(method.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, target.key())")); + assertTrue(method.contains("!IrisStructureLocator.hasNativePlacement(engine, key)")); + assertTrue(method.contains("StructureReachability.isReachable(engine, target.key())")); + assertTrue(method.contains("getWorldGenSettings().options().generateStructures()")); + assertFalse(method.contains("IrisStructureLocator.isPlaced(engine, key)")); + } + + @Test + public void verifyResolvesRegisteredNativeBeforeGenericIrisAliases() throws IOException { + String source = source("ModdedLocateCommands.java"); + int methodStart = source.indexOf("private static int verifyStructure("); + int methodEnd = source.indexOf("private static Optional resolveNativeStructure(", + methodStart); + String method = source.substring(methodStart, methodEnd); + int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)"); + int genericIrisLookup = method.indexOf("IrisStructureLocator.isPlaced(engine, key)", nativeResolution); + + assertTrue(nativeResolution >= 0); + assertTrue(genericIrisLookup > nativeResolution); + assertTrue(method.contains("NativeStructureAvailability.IRIS_SUPPRESSED")); + } + + @Test + public void structureVerificationNoLongerClaimsNativeGenerationIsMissing() throws IOException { + String source = source("ModdedStructureCommands.java"); + + assertTrue(source.contains("verifyTree(\"verify\")")); + assertTrue(source.contains("verifyTree(\"locateall\")")); + assertTrue(source.contains("IrisModdedCommands.verifyStructures(")); + assertFalse(source.contains("Vanilla structure locate is meaningless here")); + } + + private String source(String fileName) throws IOException { + Path source = commonSourceRoot().resolve("art/arcane/iris/modded/command/") + .resolve(fileName) + .normalize(); + return Files.readString(source); + } + + private String moddedSource(String fileName) throws IOException { + Path source = commonSourceRoot().resolve("art/arcane/iris/modded/") + .resolve(fileName) + .normalize(); + return Files.readString(source); + } + + private Path commonSourceRoot() { + String configuredRoot = System.getProperty("iris.moddedCommonSources"); + if (configuredRoot != null && !configuredRoot.isBlank()) { + return Path.of(configuredRoot); + } + return Path.of(System.getProperty("user.dir")) + .resolve("../modded-common/src/main/java") + .normalize(); + } + + private IrisNativeStructureDecision decision(NativeStructureGenerationStatus status) { + return new IrisNativeStructureDecision(status, 0, null, false, null, null); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedDustRevealerTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedDustRevealerTest.java new file mode 100644 index 000000000..9ed81e8cc --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedDustRevealerTest.java @@ -0,0 +1,60 @@ +package art.arcane.iris.modded.command; + +import net.minecraft.core.BlockPos; +import org.junit.Test; + +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.assertTrue; + +public class ModdedDustRevealerTest { + @Test + public void placementClassificationCoversObjectDecorationAndTerrain() { + assertTrue(ModdedDustRevealer.placementLine(3, 10, 13, "tree") + .contains("object/stilt 'tree'")); + assertTrue(ModdedDustRevealer.placementLine(3, 10, 13, null) + .contains("decoration/object/stilt")); + assertTrue(ModdedDustRevealer.placementLine(0, 10, 10, "ore") + .contains("buried object 'ore'")); + assertTrue(ModdedDustRevealer.placementLine(-4, 10, 6, null) + .contains("depth 4")); + } + + @Test + public void revealTraversalUsesDiagonalAdjacencyButNotDisconnectedBlocks() { + Set object = Set.of( + new BlockPos(0, 0, 0), + new BlockPos(1, 1, 1), + new BlockPos(3, 3, 3)); + List hits = ModdedDustRevealer.collect( + new BlockPos(0, 0, 0), + "object", + -64, + -64, + 320, + new AtomicBoolean(), + (int x, int relativeY, int z) -> + object.contains(new BlockPos(x, relativeY - 64, z)) + ? "object" + : null); + + assertEquals(List.of(new BlockPos(0, 0, 0), new BlockPos(1, 1, 1)), hits); + } + + @Test + public void cancelledRevealDoesNoTraversal() { + List hits = ModdedDustRevealer.collect( + new BlockPos(0, 0, 0), + "object", + -64, + -64, + 320, + new AtomicBoolean(true), + (int x, int relativeY, int z) -> "object"); + + assertTrue(hits.isEmpty()); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedChunkUpdateServiceTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedChunkUpdateServiceTest.java new file mode 100644 index 000000000..2e4b82df6 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedChunkUpdateServiceTest.java @@ -0,0 +1,97 @@ +package art.arcane.iris.modded.service; + +import art.arcane.iris.util.project.matter.TileWrapper; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter; +import art.arcane.volmlib.util.mantle.runtime.MantleHooks; +import art.arcane.volmlib.util.matter.Matter; +import org.junit.Test; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +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 ModdedChunkUpdateServiceTest { + @Test + public void scansWhenPlayersArePresent() { + assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false)); + } + + @Test + public void scansHeadlessForceLoadedChunks() { + assertTrue(ModdedChunkUpdateService.hasUpdateTargets(false, true)); + } + + @Test + public void skipsLevelsWithoutPlayersOrForcedChunks() { + assertFalse(ModdedChunkUpdateService.hasUpdateTargets(false, false)); + } + + @Test + public void deferredSliceIsDeletedAfterMaterialization() { + RecordingMantleChunk chunk = new RecordingMantleChunk(); + + ModdedChunkUpdateService.materializeDeferredSlice( + chunk, + TileWrapper.class, + () -> chunk.record("materialize") + ); + + assertEquals(List.of("materialize", "delete:" + TileWrapper.class.getName()), + chunk.operations()); + } + + @Test + public void failedMaterializationRetainsDeferredSlice() { + RecordingMantleChunk chunk = new RecordingMantleChunk(); + + assertThrows(IllegalStateException.class, () -> + ModdedChunkUpdateService.materializeDeferredSlice( + chunk, + TileWrapper.class, + () -> { + chunk.record("materialize"); + throw new IllegalStateException("materialization failure"); + } + )); + + assertEquals(List.of("materialize"), chunk.operations()); + } + + private static final class RecordingMantleChunk extends MantleChunk { + private final ArrayList operations = new ArrayList<>(); + + private RecordingMantleChunk() { + super(1, 0, 0, emptyAdapter(), MantleHooks.NONE); + } + + @Override + public void deleteSlices(Class type) { + operations.add("delete:" + type.getName()); + } + + private void record(String operation) { + operations.add(operation); + } + + private List operations() { + return List.copyOf(operations); + } + } + + @SuppressWarnings("unchecked") + private static MantleDataAdapter emptyAdapter() { + return (MantleDataAdapter) Proxy.newProxyInstance( + MantleDataAdapter.class.getClassLoader(), + new Class[]{MantleDataAdapter.class}, + (proxy, method, arguments) -> { + throw new UnsupportedOperationException(method.getName()); + } + ); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedStudioHotloadServiceTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedStudioHotloadServiceTest.java new file mode 100644 index 000000000..298fdbf7a --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedStudioHotloadServiceTest.java @@ -0,0 +1,29 @@ +package art.arcane.iris.modded.service; + +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ModdedStudioHotloadServiceTest { + @Test + public void detectsDatapackImportsAcrossLoadedDimensions() { + IrisDimension empty = new IrisDimension(); + IrisDimension imported = new IrisDimension(); + imported.setDatapackImports(new KList().qadd("https://modrinth.com/datapack/example")); + + assertTrue(ModdedStudioHotloadService.hasDatapackImports(List.of(empty, imported))); + } + + @Test + public void rejectsMissingOrEmptyDatapackImports() { + IrisDimension empty = new IrisDimension(); + + assertFalse(ModdedStudioHotloadService.hasDatapackImports(null)); + assertFalse(ModdedStudioHotloadService.hasDatapackImports(List.of(empty))); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedTreeFellerPresentationTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedTreeFellerPresentationTest.java new file mode 100644 index 000000000..ecd4cf37a --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedTreeFellerPresentationTest.java @@ -0,0 +1,54 @@ +package art.arcane.iris.modded.service; + +import net.minecraft.SharedConstants; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponents; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ModdedTreeFellerPresentationTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + DataComponentMap stackComponents = DataComponentMap.builder() + .set(DataComponents.MAX_STACK_SIZE, 64) + .build(); + if (!Items.OAK_LOG.builtInRegistryHolder().areComponentsBound()) { + Items.OAK_LOG.builtInRegistryHolder().bindComponents(stackComponents); + } + if (!Items.BIRCH_LOG.builtInRegistryHolder().areComponentsBound()) { + Items.BIRCH_LOG.builtInRegistryHolder().bindComponents(stackComponents); + } + } + + @Test + public void pulseSizingKeepsErosionReadableAndBounded() { + assertEquals(4, ModdedTreeFellerPresentation.blocksPerPulse(1)); + assertEquals(4, ModdedTreeFellerPresentation.blocksPerPulse(240)); + assertEquals(10, ModdedTreeFellerPresentation.blocksPerPulse(600)); + assertEquals(64, ModdedTreeFellerPresentation.blocksPerPulse(100_000)); + assertEquals(4, ModdedTreeFellerPresentation.effectStride(64)); + } + + @Test + public void compatibleDropsConsolidateWithoutExceedingStackLimits() { + List drops = ModdedTreeFellerPresentation.consolidateDrops(List.of( + new ItemStack(Items.OAK_LOG, 48), + new ItemStack(Items.OAK_LOG, 48), + new ItemStack(Items.BIRCH_LOG, 3) + )); + + assertEquals(3, drops.size()); + assertEquals(64, drops.get(0).getCount()); + assertEquals(32, drops.get(1).getCount()); + assertEquals(3, drops.get(2).getCount()); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedTreeFellerServiceTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedTreeFellerServiceTest.java new file mode 100644 index 000000000..6b47113bb --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedTreeFellerServiceTest.java @@ -0,0 +1,37 @@ +package art.arcane.iris.modded.service; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class ModdedTreeFellerServiceTest { + @Test + public void syntheticBreakProbeDepthIsNestedAndRestored() { + assertFalse(ModdedTreeFellerService.isBreakProbe()); + + boolean accepted = ModdedTreeFellerService.runBreakProbe(() -> { + assertTrue(ModdedTreeFellerService.isBreakProbe()); + return ModdedTreeFellerService.runBreakProbe(() -> { + assertTrue(ModdedTreeFellerService.isBreakProbe()); + return true; + }); + }); + + assertTrue(accepted); + assertFalse(ModdedTreeFellerService.isBreakProbe()); + } + + @Test + public void syntheticBreakProbeDepthIsRestoredAfterFailure() { + try { + ModdedTreeFellerService.runBreakProbe(() -> { + throw new IllegalStateException("probe failure"); + }); + fail("Expected probe failure"); + } catch (IllegalStateException expected) { + assertFalse(ModdedTreeFellerService.isBreakProbe()); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/nativegen/NativeStructureReferenceRepairTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/nativegen/NativeStructureReferenceRepairTest.java new file mode 100644 index 000000000..421e59e32 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/nativegen/NativeStructureReferenceRepairTest.java @@ -0,0 +1,442 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisEngine; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.framework.NativeStructureOwnershipBundle; +import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; +import art.arcane.iris.engine.framework.NativeStructureOwnershipStore; +import art.arcane.iris.engine.framework.NativeStructureStartPlan; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisNativeStructure; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +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.engine.object.NativeStructureGenerationStatus; +import art.arcane.volmlib.util.collection.KList; +import com.github.benmanes.caffeine.cache.Cache; +import net.minecraft.SharedConstants; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class NativeStructureReferenceRepairTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void persistedEnvelopeRepairsReloadedMonumentReferenceOutsideLiveBounds() { + long seed = 119723L; + ChunkPos origin = new ChunkPos(-3, 6); + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + StructureStart generated = monumentStart(structure, origin, seed); + NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel( + generated, 0, 80, -64, 320); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(24); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + generated, + structure, + 0, + terrain); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + "minecraft:monument", + wrapped, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()), + NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain)); + PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad( + origin, seed, new PiecesContainer(wrapped.getPieces())); + StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces); + ChunkPos target = outsideLiveBounds(ownership, reloaded); + + assertNotNull(target); + assertTrue(NativeStructureReferenceRepair.requiresReference( + target, "minecraft:monument", reloaded, ownership)); + assertFalse(NativeStructureReferenceRepair.requiresReference( + target, "minecraft:monument", reloaded, null)); + } + + @Test + public void deniedNaturalStartsCannotEnterCollisionArbitration() { + IrisNativeStructureDecision enabled = new IrisNativeStructureDecision( + NativeStructureGenerationStatus.GENERATE_NATIVE, + 0, null, false, null, new IrisStructureTerrain()); + IrisNativeStructureDecision disabled = new IrisNativeStructureDecision( + NativeStructureGenerationStatus.DISABLED_BY_PACK, + 0, null, false, null, new IrisStructureTerrain()); + IrisNativeStructureDecision replaced = new IrisNativeStructureDecision( + NativeStructureGenerationStatus.REPLACED_BY_IRIS, + 0, null, false, null, new IrisStructureTerrain()); + + assertTrue(NativeStructureReferenceRepair.naturalDecisionAllows(enabled)); + assertFalse(NativeStructureReferenceRepair.naturalDecisionAllows(disabled)); + assertFalse(NativeStructureReferenceRepair.naturalDecisionAllows(replaced)); + } + + @Test + public void persistedManualOwnershipSurvivesDisabledNaturalPolicyDuringReferenceScans() + throws Exception { + String structureKey = "minecraft:monument"; + long seed = 582119L; + ChunkPos origin = new ChunkPos(2, -4); + OceanMonumentStructure structure = new OceanMonumentStructure( + new OceanMonumentStructure.StructureSettings(HolderSet.empty())); + StructureStart generated = monumentStart(structure, origin, seed); + NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel( + generated, 0, 80, -64, 320); + IrisStructureTerrain terrain = new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.FORCE_CARVE) + .setHorizontalPadding(24); + StructureStart start = NativeStructureReferenceEnvelope.wrap( + generated, structure, 0, terrain); + NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture( + structureKey, + start, + plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()), + NativeStructureReferenceEnvelope.referenceBounds(start, structure, terrain)); + IrisDimension dimension = new IrisDimension(); + dimension.getImportedStructures().getDisabled().add(structureKey); + IrisData data = allocateWithoutConstructor(IrisData.class); + Engine engine = engine(dimension, data); + IrisNativeStructureDecision currentDecision = NativeStructureGenerationPolicy.resolve( + engine, structureKey, false); + + assertFalse(currentDecision.generate()); + assertFalse(NativeStructureReferenceRepair.naturalDecisionAllows(currentDecision)); + + ProtoChunk originChunk = emptyChunk(origin); + originChunk.setStartForStructure(structure, start); + ProtoChunk emptyScannedChunk = emptyChunk(new ChunkPos(Integer.MIN_VALUE, Integer.MIN_VALUE)); + Registry registry = structureRegistry(structure, structureKey); + ServerLevel serverLevel = allocateWithoutConstructor(ServerLevel.class); + StructureManager structureManager = new StructureManager(null, null, null); + ChunkPos coveredTarget = new ChunkPos( + ownership.referenceMinChunkX(), ownership.referenceMinChunkZ()); + ChunkPos uncoveredTarget = outsideReferenceEnvelopeWithinScan(ownership); + + assertNotNull(uncoveredTarget); + + installOwnership(engine, ownership); + try { + WorldGenLevel level = worldGenLevel( + registry, serverLevel, originChunk, emptyScannedChunk); + ProtoChunk coveredChunk = emptyChunk(coveredTarget); + NativeStructureReferenceRepair.createReferences( + engine, level, structureManager, coveredChunk); + + assertSame(start, originChunk.getStartForStructure(structure)); + assertTrue(originChunk.getStartForStructure(structure).isValid()); + assertTrue(coveredChunk.getReferencesForStructure(structure).contains(origin.pack())); + + ProtoChunk uncoveredChunk = emptyChunk(uncoveredTarget); + NativeStructureReferenceRepair.createReferences( + engine, level, structureManager, uncoveredChunk); + + assertSame(start, originChunk.getStartForStructure(structure)); + assertTrue(originChunk.getStartForStructure(structure).isValid()); + assertFalse(uncoveredChunk.getReferencesForStructure(structure).contains(origin.pack())); + } finally { + NativeStructureOwnershipStore.close(engine); + } + } + + private static StructureStart monumentStart(OceanMonumentStructure structure, + ChunkPos origin, long seed) { + WorldgenRandom random = new WorldgenRandom( + new LegacyRandomSource(RandomSupport.generateUniqueSeed())); + random.setLargeFeatureSeed(seed, origin.x(), origin.z()); + Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random); + OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding( + random, + origin.getMinBlockX() - 29, + origin.getMinBlockZ() - 29, + orientation + ); + return new StructureStart( + structure, + origin, + 0, + new PiecesContainer(List.of(building)) + ); + } + + private static NativeStructureStartPlan plan(ChunkPos origin, int baseY) { + IrisNativeStructure source = new IrisNativeStructure() + .setStructure("minecraft:monument") + .setWeight(1); + IrisStructurePlacement placement = new IrisStructurePlacement() + .setPlacementId("monument-reference-test") + .setNativeStructures(new KList().qadd(source)); + return new NativeStructureStartPlan( + placement, + source, + origin.x(), + origin.z(), + baseY + ); + } + + private static ChunkPos outsideLiveBounds(NativeStructureOwnershipRecord ownership, + StructureStart start) { + for (int chunkX = ownership.referenceMinChunkX(); + chunkX <= ownership.referenceMaxChunkX(); chunkX++) { + for (int chunkZ = ownership.referenceMinChunkZ(); + chunkZ <= ownership.referenceMaxChunkZ(); chunkZ++) { + ChunkPos candidate = new ChunkPos(chunkX, chunkZ); + if (!start.getBoundingBox().intersects( + candidate.getMinBlockX(), candidate.getMinBlockZ(), + candidate.getMaxBlockX(), candidate.getMaxBlockZ())) { + return candidate; + } + } + } + return null; + } + + private static ChunkPos outsideReferenceEnvelopeWithinScan( + NativeStructureOwnershipRecord ownership) { + int originX = ownership.originChunkX(); + int originZ = ownership.originChunkZ(); + List candidates = List.of( + new ChunkPos(ownership.referenceMinChunkX() - 1, originZ), + new ChunkPos(ownership.referenceMaxChunkX() + 1, originZ), + new ChunkPos(originX, ownership.referenceMinChunkZ() - 1), + new ChunkPos(originX, ownership.referenceMaxChunkZ() + 1)); + for (ChunkPos candidate : candidates) { + if (!ownership.covers(candidate.x(), candidate.z()) + && Math.abs(candidate.x() - originX) + <= NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS + && Math.abs(candidate.z() - originZ) + <= NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS) { + return candidate; + } + } + return null; + } + + private static ProtoChunk emptyChunk(ChunkPos position) { + return new ProtoChunk( + position, + UpgradeData.EMPTY, + LevelHeightAccessor.create(0, 0), + null, + null); + } + + @SuppressWarnings("unchecked") + private static Registry structureRegistry( + Structure structure, String structureKey) { + Identifier identifier = Identifier.parse(structureKey); + InvocationHandler handler = (Object proxy, Method method, Object[] arguments) -> { + if (method.getName().equals("getKey")) { + return arguments != null && arguments.length == 1 + && arguments[0] == structure ? identifier : null; + } + return defaultProxyValue(proxy, method, arguments); + }; + return (Registry) Proxy.newProxyInstance( + Registry.class.getClassLoader(), + new Class[]{Registry.class}, + handler); + } + + private static WorldGenLevel worldGenLevel( + Registry registry, + ServerLevel serverLevel, + ChunkAccess originChunk, + ChunkAccess emptyChunk) { + RegistryAccess registryAccess = registryAccess(registry); + ChunkPos origin = originChunk.getPos(); + InvocationHandler handler = (Object proxy, Method method, Object[] arguments) -> { + return switch (method.getName()) { + case "registryAccess" -> registryAccess; + case "getLevel" -> serverLevel; + case "getChunk" -> arguments != null + && arguments.length >= 2 + && ((Integer) arguments[0]) == origin.x() + && ((Integer) arguments[1]) == origin.z() + ? originChunk : emptyChunk; + default -> defaultProxyValue(proxy, method, arguments); + }; + }; + return (WorldGenLevel) Proxy.newProxyInstance( + WorldGenLevel.class.getClassLoader(), + new Class[]{WorldGenLevel.class}, + handler); + } + + private static RegistryAccess registryAccess(Registry registry) { + InvocationHandler handler = (Object proxy, Method method, Object[] arguments) -> { + return switch (method.getName()) { + case "lookupOrThrow" -> registry; + case "lookup" -> Optional.of(registry); + case "registries" -> Stream.empty(); + default -> defaultProxyValue(proxy, method, arguments); + }; + }; + return (RegistryAccess) Proxy.newProxyInstance( + RegistryAccess.class.getClassLoader(), + new Class[]{RegistryAccess.class}, + handler); + } + + private static Engine engine(IrisDimension dimension, IrisData data) throws Exception { + TestEngine engine = allocateWithoutConstructor(TestEngine.class); + engine.dimension = dimension; + engine.data = data; + return engine; + } + + @SuppressWarnings("unchecked") + private static void installOwnership( + Engine engine, NativeStructureOwnershipRecord ownership) throws Exception { + NativeStructureOwnershipBundle bundle = + NativeStructureOwnershipBundle.empty().with(ownership); + Class storageType = Class.forName( + "art.arcane.iris.engine.framework.NativeStructureOwnershipStore$Storage"); + InvocationHandler storageHandler = + (Object proxy, Method method, Object[] arguments) -> { + return switch (method.getName()) { + case "read" -> arguments != null + && arguments.length == 2 + && ((Integer) arguments[0]) == ownership.originChunkX() + && ((Integer) arguments[1]) == ownership.originChunkZ() + ? bundle : null; + case "write", "remove" -> null; + default -> defaultProxyValue(proxy, method, arguments); + }; + }; + Object storage = Proxy.newProxyInstance( + storageType.getClassLoader(), + new Class[]{storageType}, + storageHandler); + Class stateType = Class.forName( + "art.arcane.iris.engine.framework.NativeStructureOwnershipStore$State"); + Constructor constructor = stateType.getDeclaredConstructor(Engine.class, storageType); + constructor.setAccessible(true); + Object state = constructor.newInstance(engine, storage); + Field statesField = NativeStructureOwnershipStore.class.getDeclaredField("STATES"); + statesField.setAccessible(true); + Cache states = (Cache) statesField.get(null); + states.put(engine, state); + } + + private static T allocateWithoutConstructor(Class type) throws Exception { + Class unsafeType = Class.forName("sun.misc.Unsafe"); + Field unsafeField = unsafeType.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + Object unsafe = unsafeField.get(null); + Method allocateInstance = unsafeType.getMethod("allocateInstance", Class.class); + return type.cast(allocateInstance.invoke(unsafe, type)); + } + + private static Object defaultProxyValue( + Object proxy, Method method, Object[] arguments) { + return switch (method.getName()) { + case "equals" -> arguments != null && arguments.length == 1 + && proxy == arguments[0]; + case "hashCode" -> System.identityHashCode(proxy); + case "toString" -> proxy.getClass().getInterfaces()[0].getSimpleName() + "Proxy"; + default -> primitiveDefault(method.getReturnType()); + }; + } + + private static Object primitiveDefault(Class returnType) { + if (!returnType.isPrimitive() || returnType == void.class) { + return null; + } + if (returnType == boolean.class) { + return false; + } + if (returnType == char.class) { + return '\0'; + } + if (returnType == byte.class) { + return (byte) 0; + } + if (returnType == short.class) { + return (short) 0; + } + if (returnType == int.class) { + return 0; + } + if (returnType == long.class) { + return 0L; + } + if (returnType == float.class) { + return 0.0F; + } + return 0.0D; + } + + private static final class TestEngine extends IrisEngine { + private IrisDimension dimension; + private IrisData data; + + private TestEngine() { + super(null, InitializationMode.RUNTIME); + } + + @Override + public IrisDimension getDimension() { + return dimension; + } + + @Override + public IrisData getData() { + return data; + } + + @Override + public boolean isClosing() { + return false; + } + + @Override + public boolean isClosed() { + return false; + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/nativegen/NativeStructureVacuumParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/nativegen/NativeStructureVacuumParityTest.java new file mode 100644 index 000000000..6d0d365e9 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/nativegen/NativeStructureVacuumParityTest.java @@ -0,0 +1,51 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.object.IrisStructureTerrain; +import art.arcane.iris.engine.object.IrisStructureTerrainMode; +import net.minecraft.SharedConstants; +import net.minecraft.core.HolderSet; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NativeStructureVacuumParityTest { + @BeforeClass + public static void bootstrapMinecraftRegistries() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void explicitVacuumUsesNonCarvingSurfaceFittingOnModdedLoaders() { + Structure structure = new DesertPyramidStructure( + new Structure.StructureSettings( + HolderSet.empty(), Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE)); + DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0); + StructureStart start = new StructureStart( + structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece))); + NativeStructureTerrainIntegrator.TerrainTarget target = + new NativeStructureTerrainIntegrator.TerrainTarget( + "test:vacuum", start, + new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM)); + + assertTrue(NativeStructureSurfaceFitter.requiresSurfaceTerrain(target)); + assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir( + start, target.terrain())); + } +} diff --git a/adapters/neoforge/build.gradle b/adapters/neoforge/build.gradle new file mode 100644 index 000000000..3177a6e89 --- /dev/null +++ b/adapters/neoforge/build.gradle @@ -0,0 +1,264 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.jvm.toolchain.JavaLanguageVersion + +plugins { + id 'java' + id 'net.neoforged.moddev' version '2.0.141' + alias(libs.plugins.shadow) +} + +Properties rootProperties = new Properties() +file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } +String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2')) +String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) +String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.2.0.59')) +String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')) +Closure irisArtifactName = { String platform, String targetVersion -> + return "Iris v${project.version} [${platform}] ${targetVersion}.jar" +} + +group = 'art.arcane' +version = irisVersion + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +sourceSets { + main { + java { + srcDir '../modded-common/src/main/java' + srcDir '../minecraft-common/src/main/java' + srcDir '../client-common/src/main/java' + } + resources { + srcDir '../modded-common/src/main/resources' + } + } + test { + java { + srcDir '../modded-common/src/test/java' + } + } +} + +repositories { + mavenCentral() + maven { + name = 'NeoForged' + url = uri('https://maven.neoforged.net/releases/') + } + maven { url = uri('https://repo.codemc.org/repository/maven-public/') } + maven { url = uri('https://jitpack.io') } + maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') } +} + +configurations { + bundle { + canBeConsumed = false + canBeResolved = true + } + devBundle { + canBeConsumed = false + canBeResolved = true + } +} + +configurations.named('bundle').configure { + exclude(group: 'com.google.code.gson') + exclude(group: 'com.google.guava') + exclude(group: 'commons-io') + exclude(group: 'org.apache.commons', module: 'commons-lang3') + exclude(group: 'it.unimi.dsi', module: 'fastutil') + exclude(group: 'org.slf4j') + exclude(group: 'org.apache.logging.log4j') + exclude(group: 'de.crazydev22.slimjar.helper') + exclude(group: 'de.crazydev22.slimjar') + exclude(group: 'io.github.slimjar') +} + +configurations.compileClasspath.extendsFrom(configurations.devBundle) +configurations.runtimeClasspath.extendsFrom(configurations.devBundle) +configurations.testCompileClasspath.extendsFrom(configurations.compileClasspath, configurations.devBundle) +configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath, configurations.devBundle) + +dependencies { + testImplementation('junit:junit:4.13.2') + testRuntimeOnly('org.junit.platform:junit-platform-launcher:6.1.2') + testRuntimeOnly('org.junit.vintage:junit-vintage-engine:6.1.2') + compileOnly('org.slf4j:slf4j-api:2.0.17') + compileOnly(libs.spigot) { + transitive = false + } + add('bundle', volmLibCoordinate) { + transitive = false + } + add('devBundle', volmLibCoordinate) { + transitive = false + } + add('bundle', libs.zip) { + transitive = false + } + add('devBundle', libs.zip) { + transitive = false + } + + List shared = [ + "art.arcane:core:${irisVersion}".toString(), + "art.arcane:spi:${irisVersion}".toString(), + libs.paralithic, + libs.lru, + libs.caffeine, + libs.dom4j, + libs.jaxen, + libs.sentry + ] + shared.each { Object notation -> + add('bundle', notation) + add('devBundle', notation) + } +} + +neoForge { + version = neoForgeVersion + + accessTransformers.from('src/main/resources/META-INF/accesstransformer.cfg') + + runs { + client { + client() + jvmArgument('-Xmx8G') + } + server { + server() + String parity = providers.gradleProperty('irisParity').getOrNull() + if (parity != null) { + systemProperty('iris.parity', parity) + } + String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull() + if (parityGolden != null) { + systemProperty('iris.parity.golden', parityGolden) + } + String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull() + if (parityDeep != null) { + systemProperty('iris.parity.deep', parityDeep) + } + String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull() + if (worldCheck != null) { + systemProperty('iris.worldcheck', worldCheck) + } + jvmArgument('-Xmx8G') + programArgument('nogui') + } + } + + mods { + irisworldgen { + sourceSet(sourceSets.main) + } + } + + unitTest { + enable() + testedMod = mods.irisworldgen + } +} + +tasks.named('compileJava', JavaCompile).configure { + options.encoding = 'UTF-8' + options.release.set(25) +} + +tasks.named('test').configure { + useJUnitPlatform() + systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath) +} + +processResources { + inputs.property('version', project.version) + inputs.property('minecraftVersion', minecraftVersion) + filesMatching('META-INF/neoforge.mods.toml') { + expand('version': project.version, 'minecraftVersion': minecraftVersion) + } +} + +tasks.named('shadowJar', ShadowJar).configure { + doFirst { + delete(fileTree(layout.buildDirectory.dir('libs')) { + include('Iris v* [NeoForge] *.jar') + include('iris-neoforge-*.jar') + }) + delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-neoforge.jar")) + } + archiveFileName.set(irisArtifactName('NeoForge', "${minecraftVersion}+${neoForgeVersion}")) + configurations = [project.configurations.named('bundle').get()] + from(sourceSets.main.output) + mergeServiceFiles() + exclude('META-INF/maven/**') + exclude('META-INF/proguard/**') + exclude('META-INF/*.SF') + exclude('META-INF/*.DSA') + exclude('META-INF/*.RSA') + // The only multi-release entry any bundled dependency contributes is an OSGi manifest. Keeping + // it forces `Multi-Release: true` onto the mod jar, which makes the loaders treat the whole + // archive as versioned and re-scan it per release directory for nothing. + exclude('META-INF/versions/**') + addMultiReleaseAttribute.set(false) + // Invalid package name on Java 9+ ('enum' is a keyword); a module-path scan chokes on it. + exclude('org/apache/commons/lang/enum/**') + // Compile-only annotation carriers. Shipping them duplicates packages other mods also ship. + exclude('org/jspecify/**') + exclude('com/google/errorprone/**') + exclude('com/google/j2objc/**') + exclude('org/checkerframework/**') + exclude('org/jetbrains/annotations/**') + exclude('org/intellij/lang/**') + // The Bukkit platform binding cannot load without org.bukkit on the classpath, which no mod + // loader has. Shipping it only adds dead classes that reference a missing package. + exclude('art/arcane/iris/platform/bukkit/**') + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm') + relocate('io.sentry', 'art.arcane.iris.shadow.sentry') + // Split-package guard. A co-installed mod that ships any of these at their original coordinates + // puts two copies of the same package on one module layer, which the loaders reject at boot + // (Terra ships paralithic; caffeine and dom4j are common in mod dependency trees). First-party + // art.arcane.volmlib is deliberately NOT relocated. + relocate('com.github.benmanes.caffeine', 'art.arcane.iris.shadow.caffeine') + relocate('com.googlecode.concurrentlinkedhashmap', 'art.arcane.iris.shadow.clhm') + relocate('com.dfsek.paralithic', 'art.arcane.iris.shadow.paralithic') + relocate('org.dom4j', 'art.arcane.iris.shadow.dom4j') + relocate('org.jaxen', 'art.arcane.iris.shadow.jaxen') + relocate('org.zeroturnaround.zip', 'art.arcane.iris.shadow.ztzip') +} + +// The thin `jar` output carries valid neoforge.mods.toml metadata but bundles zero dependencies. +// Installing it is an instant NoClassDefFoundError, and it sits in build/libs right next to the real +// artifact. shadowJar is the only shippable NeoForge jar. +tasks.named('jar').configure { + enabled = false +} + +tasks.named('assemble').configure { + dependsOn(tasks.named('shadowJar')) +} diff --git a/adapters/neoforge/gradle.properties b/adapters/neoforge/gradle.properties new file mode 100644 index 000000000..af258fbfc --- /dev/null +++ b/adapters/neoforge/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.caching=true +org.gradle.configuration-cache=false diff --git a/adapters/neoforge/settings.gradle b/adapters/neoforge/settings.gradle new file mode 100644 index 000000000..e8ebcffd5 --- /dev/null +++ b/adapters/neoforge/settings.gradle @@ -0,0 +1,52 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +pluginManagement { + repositories { + maven { + name = 'NeoForged' + url = uri('https://maven.neoforged.net/releases/') + } + gradlePluginPortal() + mavenCentral() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + +rootProject.name = 'iris-neoforge' + +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files('../../gradle/libs.versions.toml')) + } + } +} + +// Shared VolmLib source resolution; see gradle/volmlib-resolution.settings.gradle. +apply from: new File(settingsDir, '../../gradle/volmlib-resolution.settings.gradle').canonicalFile + +includeBuild('../..') { + dependencySubstitution { + substitute(module('art.arcane:core')).using(project(':core')) + substitute(module('art.arcane:spi')).using(project(':spi')) + } +} diff --git a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java new file mode 100644 index 000000000..687b40a91 --- /dev/null +++ b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java @@ -0,0 +1,184 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.neoforge; + +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedBlockBreakHandler; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedForcedDatapack; +import art.arcane.iris.modded.command.IrisModdedCommands; +import art.arcane.iris.modded.command.ModdedWandService; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.PackType; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.common.util.BlockSnapshot; +import net.neoforged.neoforge.event.AddPackFindersEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; +import net.neoforged.neoforge.event.level.BlockDropsEvent; +import net.neoforged.neoforge.event.level.BlockEvent; +import net.neoforged.neoforge.event.level.LevelEvent; +import net.neoforged.neoforge.event.level.block.BreakBlockEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.server.ServerStartingEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.registries.DeferredRegister; +import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; + +import java.util.List; + +@Mod("irisworldgen") +public final class IrisNeoForgeBootstrap { + public IrisNeoForgeBootstrap(IEventBus modBus) { + ModdedEngineBootstrap.bootCommon(new NeoForgeModdedLoader(), "NeoForge " + FMLLoader.getCurrent().getVersionInfo().neoForgeVersion(), () -> { + DeferredRegister> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen"); + chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC); + chunkGenerators.register(modBus); + }); + + modBus.addListener((AddPackFindersEvent event) -> { + if (event.getPackType() == PackType.SERVER_DATA) { + event.addRepositorySource(ModdedForcedDatapack.repositorySource()); + } + }); + + NeoForgeProtocolNetworking.register(modBus); + + if (FMLEnvironment.getDist() == Dist.CLIENT) { + IrisNeoForgeClient.init(modBus); + } + + NeoForge.EVENT_BUS.addListener((ServerAboutToStartEvent event) -> ModdedEngineBootstrap.serverAboutToStart(event.getServer())); + NeoForge.EVENT_BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer())); + NeoForge.EVENT_BUS.addListener((ServerStartedEvent event) -> ModdedEngineBootstrap.serverStarted(event.getServer())); + NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop()); + NeoForge.EVENT_BUS.addListener((LevelEvent.Load event) -> { + if (event.getLevel() instanceof ServerLevel level) { + ModdedEngineBootstrap.levelLoaded(level); + } + }); + NeoForge.EVENT_BUS.addListener((LevelEvent.Unload event) -> { + if (event.getLevel() instanceof ServerLevel level) { + ModdedEngineBootstrap.levelUnloaded(level); + } + }); + NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher())); + NeoForge.EVENT_BUS.addListener((PermissionGatherEvent.Nodes event) -> + event.addNodes(NeoForgeModdedLoader.TREE_FELLER_PERMISSION)); + // LOWEST so every other mod has already had its say about cancelling the break before Iris records + // provenance for it. + NeoForge.EVENT_BUS.addListener( + EventPriority.LOWEST, + false, + (BreakBlockEvent event) -> { + if (event.getLevel() instanceof ServerLevel level + && event.getPlayer() instanceof ServerPlayer player) { + ModdedBlockBreakHandler.prepare(level, player, event.getPos(), event.getState()); + } + } + ); + // Parity with the Fabric PlayerBlockBreakEvents.CANCELED hook: drop the prepared entry when the break + // never happens. It only observes cancellations from listeners registered before it at LOWEST; the + // 1-tick sweeper ModdedBlockBreakHandler.prepare schedules covers every other case. + NeoForge.EVENT_BUS.addListener( + EventPriority.LOWEST, + true, + (BreakBlockEvent event) -> { + if (event.isCanceled() && event.getLevel() instanceof ServerLevel level) { + ModdedBlockBreakHandler.cancel(level, event.getPos()); + } + } + ); + NeoForge.EVENT_BUS.addListener( + EventPriority.LOWEST, + false, + (BlockEvent.EntityPlaceEvent event) -> { + if (!(event instanceof BlockEvent.EntityMultiPlaceEvent) + && event.getLevel() instanceof ServerLevel level) { + ModdedBlockBreakHandler.clearPlacedProvenance(level, event.getPos()); + } + } + ); + NeoForge.EVENT_BUS.addListener( + EventPriority.LOWEST, + false, + (BlockEvent.EntityMultiPlaceEvent event) -> { + for (BlockSnapshot snapshot : event.getReplacedBlockSnapshots()) { + if (snapshot.getLevel() instanceof ServerLevel level) { + ModdedBlockBreakHandler.clearPlacedProvenance(level, snapshot.getPos()); + } + } + } + ); + NeoForge.EVENT_BUS.addListener((BlockDropsEvent event) -> { + if (!(event.getBreaker() instanceof Player)) { + return; + } + ServerLevel level = event.getLevel(); + ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.complete(level, event.getPos(), event.getState()); + List vanillaDrops = event.getDrops().stream() + .map(ItemEntity::getItem) + .toList(); + if (result.routeCombinedDrops(vanillaDrops)) { + event.getDrops().clear(); + return; + } + if (result.replaceVanillaDrops()) { + event.getDrops().clear(); + } + for (ItemStack stack : result.drops()) { + ItemEntity item = ModdedBlockBreakHandler.createDrop(level, event.getPos(), stack); + if (item != null) { + event.getDrops().add(item); + } + } + }); + NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.LeftClickBlock event) -> { + if (ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())) { + event.setCanceled(true); + } + }); + NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.RightClickBlock event) -> { + if (ModdedWandService.useBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())) { + event.setCanceled(true); + event.setCancellationResult(InteractionResult.SUCCESS); + } + }); + NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> { + ModdedEngineBootstrap.tick(event.getServer()); + ModdedWandService.serverTick(event.getServer()); + }); + } +} diff --git a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeClient.java b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeClient.java new file mode 100644 index 000000000..e0b5c5694 --- /dev/null +++ b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeClient.java @@ -0,0 +1,61 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.neoforge; + +import art.arcane.iris.client.IrisClient; +import art.arcane.iris.client.IrisClientHud; +import art.arcane.iris.client.IrisClientKeybinds; +import art.arcane.iris.modded.ModdedIrisPayload; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; + +public final class IrisNeoForgeClient { + private IrisNeoForgeClient() { + } + + public static void registerBidirectional(PayloadRegistrar registrar) { + registrar.playBidirectional(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC, + NeoForgeProtocolNetworking::onInbound, + (payload, context) -> context.enqueueWork(() -> IrisClient.onInbound(payload.data()))); + } + + public static void init(IEventBus modBus) { + IrisClient.bindSender(frame -> ClientPacketDistributor.sendToServer(new ModdedIrisPayload(frame))); + modBus.addListener((RegisterGuiLayersEvent event) -> + event.registerAboveAll(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics))); + modBus.addListener((RegisterKeyMappingsEvent event) -> { + event.register(IrisClientKeybinds.TOGGLE_HUD); + event.register(IrisClientKeybinds.OPEN_MAP); + event.register(IrisClientKeybinds.TOGGLE_WHAT); + }); + NeoForge.EVENT_BUS.addListener((ClientPlayerNetworkEvent.LoggingIn event) -> IrisClient.onWorldJoin()); + NeoForge.EVENT_BUS.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> IrisClient.onDisconnect()); + NeoForge.EVENT_BUS.addListener((ClientTickEvent.Post event) -> { + IrisClient.tick(); + IrisClientHud.tick(); + IrisClientKeybinds.pollToggle(); + }); + } +} diff --git a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeModdedLoader.java b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeModdedLoader.java new file mode 100644 index 000000000..7bf882e72 --- /dev/null +++ b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeModdedLoader.java @@ -0,0 +1,114 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.neoforge; + +import art.arcane.iris.modded.ModdedLoader; +import art.arcane.iris.modded.service.ModdedTreeFellerService; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.level.block.BreakBlockEvent; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.server.ServerLifecycleHooks; +import net.neoforged.neoforge.server.permission.PermissionAPI; +import net.neoforged.neoforge.server.permission.nodes.PermissionNode; +import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; +import net.neoforged.neoforgespi.language.IModFileInfo; + +import java.io.File; +import java.nio.file.Path; + +public final class NeoForgeModdedLoader implements ModdedLoader { + public static final PermissionNode TREE_FELLER_PERMISSION = new PermissionNode<>( + "irisworldgen", + "treefeller", + PermissionTypes.BOOLEAN, + (player, playerId, contexts) -> + player != null && Commands.LEVEL_GAMEMASTERS.check(player.permissions()) + ); + + @Override + public String platformName() { + return "neoforge"; + } + + @Override + public String minecraftVersion() { + return FMLLoader.getCurrent().getVersionInfo().mcVersion(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById("irisworldgen") + .map((net.neoforged.fml.ModContainer container) -> container.getModInfo().getVersion().toString()) + .orElse("unknown"); + } + + @Override + public MinecraftServer currentServer() { + return ServerLifecycleHooks.getCurrentServer(); + } + + @Override + public void invalidateLevelCache(MinecraftServer server) { + server.markWorldsDirty(); + } + + @Override + public boolean clientEnvironment() { + return FMLEnvironment.getDist().isClient(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public File modJar() { + IModFileInfo info = ModList.get().getModFileById("irisworldgen"); + return info == null ? null : info.getFile().getFilePath().toFile(); + } + + @Override + public boolean hasTreeFellerPermission(ServerPlayer player) { + return PermissionAPI.getPermission(player, TREE_FELLER_PERMISSION); + } + + @Override + public boolean canTreeFellerBreak( + ServerLevel level, + ServerPlayer player, + BlockPos position, + BlockState state + ) { + return ModdedTreeFellerService.runBreakProbe(() -> { + BreakBlockEvent event = new BreakBlockEvent(level, position, state, player); + NeoForge.EVENT_BUS.post(event); + return !event.isCanceled(); + }); + } +} diff --git a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeProtocolNetworking.java b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeProtocolNetworking.java new file mode 100644 index 000000000..dd6f9a764 --- /dev/null +++ b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeProtocolNetworking.java @@ -0,0 +1,82 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.neoforge; + +import art.arcane.iris.modded.ModdedIrisPayload; +import art.arcane.iris.modded.ModdedProtocolChannel; +import art.arcane.iris.modded.ModdedProtocolHandler; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.common.extensions.ICommonPacketListener; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; + +public final class NeoForgeProtocolNetworking { + private NeoForgeProtocolNetworking() { + } + + public static void register(IEventBus modBus) { + modBus.addListener((RegisterPayloadHandlersEvent event) -> onRegisterPayloads(event)); + NeoForge.EVENT_BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + ModdedProtocolHandler.onPlayerJoin(player); + } + }); + NeoForge.EVENT_BUS.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + ModdedProtocolHandler.onPlayerDisconnect(player); + } + }); + ModdedProtocolHandler.bindChannel(new NeoForgeProtocolChannel()); + } + + private static void onRegisterPayloads(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar("1").optional(); + if (FMLEnvironment.getDist() == Dist.CLIENT) { + IrisNeoForgeClient.registerBidirectional(registrar); + } else { + registrar.playBidirectional(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC, + NeoForgeProtocolNetworking::onInbound); + } + } + + static void onInbound(ModdedIrisPayload payload, IPayloadContext context) { + if (context.player() instanceof ServerPlayer player) { + ModdedProtocolHandler.onInbound(player, payload.data()); + } + } + + private static final class NeoForgeProtocolChannel implements ModdedProtocolChannel { + @Override + public boolean canReceive(ServerPlayer player) { + return ((ICommonPacketListener) player.connection).hasChannel(ModdedIrisPayload.TYPE); + } + + @Override + public void send(ServerPlayer player, ModdedIrisPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + } +} diff --git a/adapters/neoforge/src/main/resources/META-INF/accesstransformer.cfg b/adapters/neoforge/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 000000000..5a2faeeb3 --- /dev/null +++ b/adapters/neoforge/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,3 @@ +public net.minecraft.server.MinecraftServer levels +public net.minecraft.server.MinecraftServer executor +public net.minecraft.server.MinecraftServer storageSource diff --git a/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 000000000..cd81fa963 --- /dev/null +++ b/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,37 @@ +modLoader = "javafml" +loaderVersion = "[3,)" +license = "GPL-3.0" +issueTrackerURL = "https://github.com/VolmitSoftware/Iris/issues" + +[[mixins]] +config = "irisworldgen.entity.mixins.json" + +[[mixins]] +config = "irisworldgen.client.mixins.json" + +[[accessTransformers]] +file = "META-INF/accesstransformer.cfg" + +[[mods]] +modId = "irisworldgen" +version = "${version}" +displayName = "Iris" +description = "Iris World Generation Engine (NeoForge adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)" +authors = "Arcane Arts (Volmit Software)" +credits = "cyberpwn, NextdoorPsycho, Vatuu" +displayURL = "https://docs.volmit.com/iris/" +logoFile = "assets/irisworldgen/icon.png" + +[[dependencies.irisworldgen]] +modId = "neoforge" +type = "required" +versionRange = "[26.2,26.3)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.irisworldgen]] +modId = "minecraft" +type = "required" +versionRange = "[${minecraftVersion}]" +ordering = "NONE" +side = "BOTH" diff --git a/build-all.sh b/build-all.sh new file mode 100755 index 000000000..c827831de --- /dev/null +++ b/build-all.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +./gradlew buildAllToOut --no-parallel -PuseLocalVolmLib=false "$@" diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..9e2e3faed --- /dev/null +++ b/build.gradle @@ -0,0 +1,766 @@ +import de.undercouch.gradle.tasks.download.Download +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.jvm.tasks.Jar +import org.gradle.jvm.toolchain.JavaLanguageVersion + +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2021 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +buildscript { + repositories { + maven { + url = uri('https://jitpack.io') + } + } + dependencies { + classpath('com.github.VolmitSoftware:NMSTools:c88961416f') + } +} + +plugins { + id 'java' + id 'java-library' + alias(libs.plugins.download) +} + +group = 'art.arcane' +version = providers.gradleProperty('irisVersion').getOrElse('4.0.0-26.2') + +project(':core') { + group = 'art.arcane' + version = rootProject.version +} +String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.2') +// The Bukkit jar runs Paper 26.1.2 through 26.2 (compile-low pins); the loader jars stay 26.2-only. +String bukkitMinecraftRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.2') +String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.3') +String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.1.1') +String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.59') +String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') + .orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186') + .get() +String useLocalVolmLib = providers.gradleProperty('useLocalVolmLib').getOrElse('true') +String localVolmLibDirectory = providers.gradleProperty('localVolmLibDirectory').getOrNull() +Closure loaderDisplayVersion = { String loaderVersion -> + String coordinatePrefix = "${minecraftVersion}-" + if (loaderVersion.startsWith(coordinatePrefix)) { + return loaderVersion.substring(coordinatePrefix.length()) + } + + return loaderVersion +} +Closure irisArtifactName = { String platform, String targetVersion -> + return "Iris v${project.version} [${platform}] ${targetVersion}.jar" +} +String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange) +String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}") +String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}") +String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}") +apply plugin: ApiGenerator + +// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling +// Multiplexor workspace when it exists, with a repo-local fallback for fresh clones. +File multiplexorConsumerLocation = new File(rootDir, '../../[Minecraft Server]/consumers').canonicalFile +String defaultConsumerLocation = multiplexorConsumerLocation.isDirectory() + ? multiplexorConsumerLocation.absolutePath + : layout.buildDirectory.dir('consumers').get().asFile.absolutePath +String consumerLocation = providers.gradleProperty('location') + .getOrElse(defaultConsumerLocation) +String bukkitConsumerPath = "${consumerLocation}/plugin-consumers/dropins/plugins" +String fabricConsumerPath = "${consumerLocation}/fabric-mod-consumers/dropins/mods" +String forgeConsumerPath = "${consumerLocation}/forge-mod-consumers/dropins/mods" +String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropins/mods" + +def nmsBindings = [ + v26_2_R1: '26.1.2.build.74-stable', +] +Class nmsTypeClass = Class.forName('NMSBinding$Type') +nmsBindings.each { key, value -> + project(":adapters:bukkit:nms:${key}") { + apply plugin: JavaPlugin + + def nmsConfig = new Config() + nmsConfig.jvm = 25 + nmsConfig.version = value + nmsConfig.type = Enum.valueOf(nmsTypeClass, 'USER_DEV') + extensions.extraProperties.set('nms', nmsConfig) + plugins.apply(NMSBinding) + + sourceSets.main.java.srcDir(rootProject.file('adapters/minecraft-common/src/main/java')) + + dependencies { + compileOnly(project(':core')) + compileOnly(volmLibCoordinate) { + transitive = false + } + compileOnly(rootProject.libs.annotations) + compileOnly(rootProject.libs.byteBuddy.core) + testImplementation('junit:junit:4.13.2') + testImplementation(project(':core')) + testCompileOnly(volmLibCoordinate) { + transitive = false + } + testRuntimeOnly(project(':core')) + } + + tasks.withType(org.gradle.api.tasks.testing.Test).configureEach { + systemProperty('iris.nmsChunkGeneratorSource', + rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/IrisChunkGenerator.java").absolutePath) + systemProperty('iris.nmsBindingSource', + rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/NMSBinding.java").absolutePath) + systemProperty('iris.nativeStructurePostProcessorSource', + rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java').absolutePath) + systemProperty('iris.nativeStructureStartInjectorSource', + rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureStartInjector.java').absolutePath) + systemProperty('iris.nativeStructureGenerationKeysSource', + rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationKeys.java').absolutePath) + systemProperty('iris.nativeStructureVolumeIndexSource', + rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java').absolutePath) + systemProperty('iris.customBiomeSource', + rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/CustomBiomeSource.java").absolutePath) + systemProperty('iris.vanillaStructureBiomesSource', + rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/VanillaStructureBiomes.java").absolutePath) + systemProperty('iris.moddedStructureHooksSource', + rootProject.file('adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java').absolutePath) + } + } +} + +def included = configurations.create('included') +def jarJar = configurations.create('jarJar') +dependencies { + nmsBindings.keySet().each { key -> + add('included', project(path: ":adapters:bukkit:nms:${key}", configuration: 'runtimeElements')) + } + add('included', project(path: ':core', configuration: 'shadow')) + add('included', project(path: ':adapters:bukkit:plugin', configuration: 'runtimeElements')) + add('jarJar', project(':core:agent')) +} + +tasks.named('jar', Jar).configure { + inputs.files(included) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(jarJar, provider { included.resolve().collect { zipTree(it) } }) + doFirst { + delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar")) + } + archiveFileName.set(bukkitArtifactName) +} + +tasks.register('iris', Copy) { + group = 'iris' + dependsOn('jar') + from(layout.buildDirectory.file("libs/${bukkitArtifactName}")) + into(layout.buildDirectory.dir('consumer')) + doFirst { + delete(fileTree(layout.buildDirectory.dir('consumer').get()) { + include('Iris v* [CraftBukkit] *.jar') + }) + } +} + +tasks.register('buildSwiftSwamp') { + group = 'iris' + description = 'Builds every platform jar and drops them in the local BUILDS output directory.' + outputs.upToDateWhen { false } + dependsOn('iris', 'verifyModdedArtifacts') + doLast { + File swiftSwampOut = file('C://VolmitSoftware/BUILDS') + copy { + from(layout.buildDirectory.file("consumer/${bukkitArtifactName}")) + into(swiftSwampOut) + rename { String ignored -> 'Iris.jar' } + } + copy { + from(layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}")) + into(swiftSwampOut) + rename { String ignored -> 'Iris-Fabric.jar' } + } + copy { + from(layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}")) + into(swiftSwampOut) + rename { String ignored -> 'Iris-Forge.jar' } + } + copy { + from(layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}")) + into(swiftSwampOut) + rename { String ignored -> 'Iris-Neoforge.jar' } + } + println('') + println('=== Iris buildSwiftSwamp -> ' + swiftSwampOut + ' ===') + ['Iris.jar', 'Iris-Fabric.jar', 'Iris-Forge.jar', 'Iris-Neoforge.jar'].each { String jar -> + File produced = new File(swiftSwampOut, jar) + println(String.format(' %-24s %8d KB', jar, (long) (produced.length() / 1024))) + } + } +} + +tasks.register('buildBukkit', Copy) { + group = 'iris' + dependsOn('jar') + from(layout.buildDirectory.file("libs/${bukkitArtifactName}")) + into(layout.projectDirectory.dir('dist')) + doFirst { + delete(fileTree(layout.projectDirectory.dir('dist')) { + include('Iris v* [CraftBukkit] *.jar') + }) + delete(layout.projectDirectory.file("dist/Iris-${project.version}+mc${minecraftVersion}.jar")) + } +} + +// The three loader jars come out of nested standalone Gradle builds driven through Exec. Their own +// Gradle builds own the complete incremental model, so always launch them instead of allowing an +// incomplete outer input inventory to publish stale jars. +Closure resolveLocalVolmLibDirectory = { -> + String configured = localVolmLibDirectory != null && !localVolmLibDirectory.isBlank() + ? localVolmLibDirectory + : System.getenv('VOLMLIB_DIR') + if (configured != null && !configured.isBlank()) { + File configuredDirectory = new File(configured) + if (!configuredDirectory.isAbsolute()) { + configuredDirectory = new File(rootDir, configured) + } + return configuredDirectory.isDirectory() ? configuredDirectory : null + } + + File current = rootDir + while (current != null) { + File candidate = new File(current, 'VolmLib') + if (new File(candidate, 'settings.gradle').isFile() || new File(candidate, 'settings.gradle.kts').isFile()) { + return candidate + } + + current = current.parentFile + } + + return null +} + +List moddedSharedInputs = [] +[ + 'adapters/modded-common/src', + 'adapters/minecraft-common/src', + 'adapters/client-common/src', + 'core/src/main', + 'core/build.gradle', + 'spi/src/main', + 'spi/build.gradle', + 'build.gradle', + 'settings.gradle', + 'gradle.properties', + 'gradle/libs.versions.toml', + 'gradle/volmlib-resolution.settings.gradle' +].each { String path -> moddedSharedInputs.add(layout.projectDirectory.file(path).asFile) } +File localVolmLibCheckout = useLocalVolmLib.equalsIgnoreCase('true') ? resolveLocalVolmLibDirectory() : null +if (localVolmLibCheckout != null) { + moddedSharedInputs.add(new File(localVolmLibCheckout, 'shared/src/main')) +} + +Closure declareModdedJarIo = { Exec task, String adapter, String artifactName, Map versions -> + task.inputs.files(moddedSharedInputs).withPropertyName('moddedSharedSources') + task.inputs.files( + layout.projectDirectory.file("adapters/${adapter}/src").asFile, + layout.projectDirectory.file("adapters/${adapter}/build.gradle").asFile, + layout.projectDirectory.file("adapters/${adapter}/settings.gradle").asFile + ).withPropertyName('adapterSources') + versions.each { String key, String value -> task.inputs.property(key, value) } + task.inputs.property('useLocalVolmLib', useLocalVolmLib) + task.inputs.property('volmLibCoordinate', volmLibCoordinate) + task.inputs.property('localVolmLibCheckout', localVolmLibCheckout == null ? '' : localVolmLibCheckout.absolutePath) + task.outputs.file(layout.projectDirectory.file("adapters/${adapter}/build/libs/${artifactName}")) + task.outputs.upToDateWhen { false } + task.outputs.cacheIf { false } + return null +} + +tasks.register('fabricJar', Exec) { + group = 'iris' + workingDir = layout.projectDirectory.dir('adapters/fabric').asFile + String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew' + List command = [ + layout.projectDirectory.file(wrapperScript).asFile.absolutePath, + 'shadowJar', + '--console=plain', + "-PirisVersion=${project.version}", + "-PminecraftVersion=${minecraftVersion}", + "-PfabricLoaderVersion=${fabricLoaderVersion}", + "-PuseLocalVolmLib=${useLocalVolmLib}", + "-PvolmLibCoordinate=${volmLibCoordinate}" + ] + if (localVolmLibDirectory != null && !localVolmLibDirectory.isBlank()) { + command.add("-PlocalVolmLibDirectory=${localVolmLibDirectory}") + } + commandLine(command) + declareModdedJarIo(it, 'fabric', fabricArtifactName, [ + irisVersion: project.version.toString(), + minecraftVersion: minecraftVersion, + fabricLoaderVersion: fabricLoaderVersion + ]) +} + +tasks.register('buildFabric', Copy) { + group = 'iris' + dependsOn('fabricJar') + from(layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}")) + into(layout.projectDirectory.dir('dist')) + doFirst { + delete(fileTree(layout.projectDirectory.dir('dist')) { + include('Iris v* [Fabric] *.jar') + }) + delete(layout.projectDirectory.file("dist/Iris-${project.version}+mc${minecraftVersion}-fabric.jar")) + } +} + +tasks.register('forgeJar', Exec) { + group = 'iris' + workingDir = layout.projectDirectory.dir('adapters/forge').asFile + String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew' + List command = [ + layout.projectDirectory.file(wrapperScript).asFile.absolutePath, + 'shadowJar', + '--console=plain', + "-PirisVersion=${project.version}", + "-PminecraftVersion=${minecraftVersion}", + "-PforgeVersion=${forgeVersion}", + "-PuseLocalVolmLib=${useLocalVolmLib}", + "-PvolmLibCoordinate=${volmLibCoordinate}" + ] + if (localVolmLibDirectory != null && !localVolmLibDirectory.isBlank()) { + command.add("-PlocalVolmLibDirectory=${localVolmLibDirectory}") + } + commandLine(command) + declareModdedJarIo(it, 'forge', forgeArtifactName, [ + irisVersion: project.version.toString(), + minecraftVersion: minecraftVersion, + forgeVersion: forgeVersion + ]) +} + +tasks.register('buildForge', Copy) { + group = 'iris' + dependsOn('forgeJar') + from(layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}")) + into(layout.projectDirectory.dir('dist')) + doFirst { + delete(fileTree(layout.projectDirectory.dir('dist')) { + include('Iris v* [Forge] *.jar') + }) + delete(layout.projectDirectory.file("dist/Iris-${project.version}+mc${minecraftVersion}-forge.jar")) + } +} + +tasks.register('neoforgeJar', Exec) { + group = 'iris' + workingDir = layout.projectDirectory.dir('adapters/neoforge').asFile + String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew' + List command = [ + layout.projectDirectory.file(wrapperScript).asFile.absolutePath, + 'shadowJar', + '--console=plain', + "-PirisVersion=${project.version}", + "-PminecraftVersion=${minecraftVersion}", + "-PneoForgeVersion=${neoForgeVersion}", + "-PuseLocalVolmLib=${useLocalVolmLib}", + "-PvolmLibCoordinate=${volmLibCoordinate}" + ] + if (localVolmLibDirectory != null && !localVolmLibDirectory.isBlank()) { + command.add("-PlocalVolmLibDirectory=${localVolmLibDirectory}") + } + commandLine(command) + declareModdedJarIo(it, 'neoforge', neoForgeArtifactName, [ + irisVersion: project.version.toString(), + minecraftVersion: minecraftVersion, + neoForgeVersion: neoForgeVersion + ]) +} + +tasks.register('buildNeoforge', Copy) { + group = 'iris' + dependsOn('neoforgeJar') + from(layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}")) + into(layout.projectDirectory.dir('dist')) + doFirst { + delete(fileTree(layout.projectDirectory.dir('dist')) { + include('Iris v* [NeoForge] *.jar') + }) + delete(layout.projectDirectory.file("dist/Iris-${project.version}+mc${minecraftVersion}-neoforge.jar")) + } +} + +tasks.named('forgeJar').configure { + mustRunAfter(tasks.named('fabricJar')) +} + +tasks.named('fabricJar').configure { + mustRunAfter(tasks.named('jar')) + mustRunAfter(tasks.named('buildBukkit')) + mustRunAfter(project(':spi').tasks.named('jar')) +} + +tasks.named('neoforgeJar').configure { + mustRunAfter(tasks.named('forgeJar')) +} + +List commonModdedArtifactEntries = [ + 'art/arcane/iris/engine/IrisEngine.class', + 'art/arcane/iris/spi/IrisPlatform.class', + 'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class', + 'art/arcane/iris/util/common/misc/getHardware.class', + // Both shared mixin configs plus the lang file and mod icon every loader's metadata points at. + // A shadowJar exclude or a renamed resource that drops any of these is silent at build time and + // shows up as a non-applied mixin or a missing-translation report from a user. + 'irisworldgen.entity.mixins.json', + 'irisworldgen.client.mixins.json', + 'assets/irisworldgen/lang/en_us.json', + 'assets/irisworldgen/icon.png' +] + +// Frozen inventory of shipped classes that extend or implement an org.bukkit type. They come from +// core and VolmLib, sit on Bukkit-only code paths the mod loaders never resolve, and are inert dead +// weight rather than a boot hazard - excluding them means chasing the whole core reference graph. +// The verifier fails on anything NEW outside this set, which is the part that would be a regression. +// Shrinking the set (WP4 deletions, retypes) needs no edit here; growing it does. +Set knownBukkitSupertypeClasses = [ + 'art/arcane/iris/core/events/IrisEngineEvent.class', + 'art/arcane/iris/core/events/IrisLootEvent.class', + 'art/arcane/iris/core/events/IrisLootEvent$1.class', + 'art/arcane/iris/core/events/IrisLootEvent$2.class', + 'art/arcane/iris/core/link/data/ItemAdderDataProvider.class', + 'art/arcane/iris/core/link/data/MythicMobsDataProvider.class', + 'art/arcane/iris/core/link/MultiverseGuardListener.class', + 'art/arcane/iris/engine/framework/EngineAssignedWorldManager.class', + 'art/arcane/iris/engine/object/IrisEntity$BukkitOps$1.class', + 'art/arcane/iris/engine/object/LegacyTileData$SignTags$1.class', + 'art/arcane/iris/engine/platform/BukkitChunkGenerator.class', + 'art/arcane/iris/engine/platform/DummyBiomeProvider.class', + 'art/arcane/iris/engine/platform/DummyChunkGenerator.class', + 'art/arcane/iris/util/common/data/IrisCustomData.class', + 'art/arcane/iris/util/common/plugin/IrisService.class', + 'art/arcane/iris/util/common/plugin/VolmitPlugin.class', + 'art/arcane/iris/util/common/plugin/VolmitSender.class', + 'art/arcane/iris/util/common/plugin/chunk/ChunkTickets.class', + 'art/arcane/volmlib/util/board/BoardUpdateTask.class', + 'art/arcane/volmlib/util/bukkit/Events.class', + 'art/arcane/volmlib/util/data/Cuboid.class', + 'art/arcane/volmlib/util/data/InvertedBiomeGrid.class', + 'art/arcane/volmlib/util/data/IrisBiomeStorage.class', + 'art/arcane/volmlib/util/inventorygui/UIWindow.class' +] as Set + +tasks.register('verifyFabricArtifact') { + group = 'verification' + dependsOn('fabricJar') + File artifact = layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}").asFile + inputs.file(artifact) + doLast { + List requiredEntries = commonModdedArtifactEntries + [ + 'fabric.mod.json', + 'irisworldgen.mixins.json', + 'irisworldgen.accesswidener', + 'art/arcane/iris/fabric/IrisFabricBootstrap.class' + ] + ModdedArtifactVerifier.verify(artifact, requiredEntries, knownBukkitSupertypeClasses) + logger.lifecycle("Verified ${artifact.name} packaging, mixin compat, and nested jars") + } +} + +tasks.register('verifyForgeArtifact') { + group = 'verification' + dependsOn('forgeJar') + File artifact = layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}").asFile + inputs.file(artifact) + doLast { + List requiredEntries = commonModdedArtifactEntries + [ + 'META-INF/mods.toml', + 'META-INF/accesstransformer.cfg', + 'art/arcane/iris/forge/IrisForgeBootstrap.class' + ] + ModdedArtifactVerifier.verify(artifact, requiredEntries, knownBukkitSupertypeClasses) + logger.lifecycle("Verified ${artifact.name} packaging, mixin compat, and nested jars") + } +} + +tasks.register('verifyNeoforgeArtifact') { + group = 'verification' + dependsOn('neoforgeJar') + File artifact = layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}").asFile + inputs.file(artifact) + doLast { + List requiredEntries = commonModdedArtifactEntries + [ + 'META-INF/neoforge.mods.toml', + 'META-INF/accesstransformer.cfg', + 'art/arcane/iris/neoforge/IrisNeoForgeBootstrap.class' + ] + ModdedArtifactVerifier.verify(artifact, requiredEntries, knownBukkitSupertypeClasses) + logger.lifecycle("Verified ${artifact.name} packaging, mixin compat, and nested jars") + } +} + +tasks.register('verifyModdedArtifacts') { + group = 'verification' + dependsOn('verifyFabricArtifact', 'verifyForgeArtifact', 'verifyNeoforgeArtifact') +} + +// Entries the Bukkit artifact only ever reaches by name - NMS binding lookup, the SIMD kernels, the +// instrumentation agent, the JarScanner-driven command and service discovery, the Matter codec - plus +// the two plugin descriptors and the slimjar manifests. Nothing in the reference graph points at +// them, so a shadowJar exclude that drops one is silent until a server hits that path. +List requiredBukkitArtifactEntries = [ + 'plugin.yml', + 'paper-plugin.yml', + 'slimjar.dat', + 'slimjar-resolutions.dat', + 'agent.jar', + 'art/arcane/iris/Iris.class', + 'art/arcane/iris/IrisBootstrap.class', + 'art/arcane/iris/core/nms/INMS.class', + 'art/arcane/iris/core/nms/v26_2_R1/NMSBinding.class', + 'art/arcane/iris/core/nms/v1X/NMSBinding1X.class', + 'art/arcane/iris/engine/platform/PlatformChunkGenerator.class', + 'art/arcane/iris/core/lifecycle/WorldLifecycleStaging.class', + 'art/arcane/iris/util/simd/VectorSimdKernels.class', + 'art/arcane/iris/util/simd/VectorNoiseKernels2D.class', + 'art/arcane/iris/util/project/agent/Agent.class', + 'art/arcane/iris/util/common/misc/getHardware.class', + 'art/arcane/iris/util/caffeine/cache/Caffeine.class', + 'art/arcane/volmlib/util/io/JarScanner.class', + 'art/arcane/volmlib/util/director/runtime/DirectorRuntimeEngine.class', + 'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class', + 'art/arcane/volmlib/util/matter/Matter.class' +] + +tasks.register('verifyBukkitArtifact') { + group = 'verification' + dependsOn('jar') + File artifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile + inputs.file(artifact) + doLast { + BukkitArtifactVerifier.verify(artifact, requiredBukkitArtifactEntries, 17, 400, 16) + logger.lifecycle("Verified ${artifact.name} packaging and class reference graph") + } +} + +tasks.named('buildBukkit').configure { + dependsOn('verifyBukkitArtifact') +} + +tasks.named('iris').configure { + dependsOn('verifyBukkitArtifact') +} + +tasks.named('buildFabric').configure { + dependsOn('verifyFabricArtifact') +} + +tasks.named('buildForge').configure { + dependsOn('verifyForgeArtifact') +} + +tasks.named('buildNeoforge').configure { + dependsOn('verifyNeoforgeArtifact') +} + +tasks.register('buildAllToOut') { + group = 'iris' + dependsOn('buildBukkit', 'buildFabric', 'buildForge', 'buildNeoforge', ':spi:jar') + doLast { + File dist = layout.projectDirectory.dir('dist').asFile + File[] jars = dist.listFiles((java.io.FileFilter) { File f -> f.name.endsWith('.jar') }) + println('') + println('=== Iris buildAllToOut -> dist/ ===') + if (jars != null) { + jars.sort { it.name }.each { File f -> + println(String.format(' %-60s %8d KB', f.name, (long) (f.length() / 1024))) + } + } + } +} + +tasks.register('buildAll') { + group = 'iris' + outputs.upToDateWhen { false } + dependsOn('iris', 'verifyModdedArtifacts') + doLast { + delete( + file("${bukkitConsumerPath}/Iris-Fabric.jar"), + file("${bukkitConsumerPath}/Iris-Forge.jar"), + file("${bukkitConsumerPath}/Iris-Neoforge.jar") + ) + copy { + from(layout.buildDirectory.file("consumer/${bukkitArtifactName}")) + into(bukkitConsumerPath) + rename { String ignored -> 'Iris.jar' } + } + delete( + file("${fabricConsumerPath}/Iris.jar"), + file("${fabricConsumerPath}/Iris-Forge.jar"), + file("${fabricConsumerPath}/Iris-Neoforge.jar") + ) + copy { + from(layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}")) + into(fabricConsumerPath) + rename { String ignored -> 'Iris-Fabric.jar' } + } + delete( + file("${forgeConsumerPath}/Iris.jar"), + file("${forgeConsumerPath}/Iris-Fabric.jar"), + file("${forgeConsumerPath}/Iris-Neoforge.jar") + ) + copy { + from(layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}")) + into(forgeConsumerPath) + rename { String ignored -> 'Iris-Forge.jar' } + } + delete( + file("${neoForgeConsumerPath}/Iris.jar"), + file("${neoForgeConsumerPath}/Iris-Fabric.jar"), + file("${neoForgeConsumerPath}/Iris-Forge.jar") + ) + copy { + from(layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}")) + into(neoForgeConsumerPath) + rename { String ignored -> 'Iris-Neoforge.jar' } + } + println('') + println("=== Iris buildAll -> ${consumerLocation} ===") + println(" ${bukkitConsumerPath}/Iris.jar") + println(" ${fabricConsumerPath}/Iris-Fabric.jar") + println(" ${forgeConsumerPath}/Iris-Forge.jar") + println(" ${neoForgeConsumerPath}/Iris-Neoforge.jar") + } +} + +tasks.register('irisDev', Copy) { + group = 'iris' + from(project(':core').layout.buildDirectory.files('libs/core-javadoc.jar', 'libs/core-sources.jar')) + rename { String fileName -> fileName.replace('core', "Iris-${project.version}") } + into(layout.buildDirectory) + dependsOn(':core:sourcesJar') + dependsOn(':core:javadocJar') +} + +def cli = file('sentry-cli.exe') +tasks.register('downloadCli', Download) { + group = 'io.sentry' + src("https://release-registry.services.sentry.io/apps/sentry-cli/latest?response=download&arch=x86_64&platform=${System.getProperty('os.name')}&package=sentry-cli") + dest(cli) + + doLast { + cli.setExecutable(true) + } +} + +tasks.register('release') { + group = 'io.sentry' + dependsOn('downloadCli') + doLast { + String url = 'http://sentry.volmit.com:8080' + def authToken = project.findProperty('sentry.auth.token') ?: System.getenv('SENTRY_AUTH_TOKEN') + String org = 'sentry' + String projectName = 'iris' + runCommand(cli, '--url', url, '--auth-token', authToken, 'releases', 'new', '-o', org, '-p', projectName, version) + runCommand(cli, '--url', url, '--auth-token', authToken, 'releases', 'set-commits', '-o', org, '-p', projectName, version, '--auto', '--ignore-missing') + //exec(cli, "--url", url, "--auth-token", authToken, "releases", "finalize", "-o", org, "-p", projectName, version) + cli.delete() + } +} + +void runCommand(Object... command) { + Process process = new ProcessBuilder(command.collect { it.toString() }).start() + process.inputStream.readLines().each { println(it) } + process.errorStream.readLines().each { println(it) } + process.waitFor() +} + +configurations.configureEach { + resolutionStrategy.cacheChangingModulesFor(0, 'seconds') + resolutionStrategy.cacheDynamicVersionsFor(0, 'seconds') +} + +allprojects { + apply plugin: 'java' + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } + } + + repositories { + mavenLocal() + mavenCentral() + maven { url = uri('https://repo.papermc.io/repository/maven-public/') } + maven { url = uri('https://repo.codemc.org/repository/maven-public/') } + + maven { url = uri('https://jitpack.io') } // score + maven { url = uri('https://repo.auxilor.io/repository/maven-releases/') } // eco + maven { url = uri('https://repo.nexomc.com/releases/') } // nexo + maven { url = uri('https://maven.devs.beer/') } // itemsadder + maven { url = uri('https://repo.extendedclip.com/releases/') } // placeholderapi + maven { url = uri('https://mvn.lumine.io/repository/maven-public/') } // mythic + maven { url = uri('https://nexus.phoenixdevt.fr/repository/maven-public/') } //MMOItems + maven { url = uri('https://repo.onarandombox.com/content/groups/public/') } //Multiverse Core + maven { url = uri('https://repo.momirealms.net/releases/') } // CraftEngine + } + + /** + * We need parameter meta for the decree command system + */ + tasks.named('compileJava', JavaCompile).configure { + options.compilerArgs.add('-parameters') + options.encoding = 'UTF-8' + options.debugOptions.debugLevel = 'none' + options.release.set(25) + } + + tasks.named('javadoc').configure { + options.encoding = 'UTF-8' + options.quiet() + //options.addStringOption("Xdoclint:none") // TODO: Re-enable this + } + + tasks.register('sourcesJar', Jar) { + archiveClassifier.set('sources') + from(sourceSets.main.allSource) + } + + tasks.register('javadocJar', Jar) { + archiveClassifier.set('javadoc') + from(tasks.named('javadoc').map { it.destinationDir }) + } +} + +if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_25)) { + System.err.println() + System.err.println('=========================================================================================================') + System.err.println('You must run gradle on Java 25 or newer. You are using ' + JavaVersion.current()) + System.err.println() + System.err.println('=== For IDEs ===') + System.err.println('1. Configure the project for Java 25 toolchain') + System.err.println('2. Configure the bundled gradle to use Java 25+ in settings') + System.err.println() + System.err.println('=== For Command Line (gradlew) ===') + System.err.println('1. Install JDK 25 from https://adoptium.net/temurin/releases/?version=25') + System.err.println('2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-25') + System.err.println('3. Open a new command prompt window to get the new environment variables if need be.') + System.err.println('=========================================================================================================') + System.err.println() + System.exit(69) +} diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index 5d4d0496d..000000000 --- a/build.gradle.kts +++ /dev/null @@ -1,292 +0,0 @@ -import de.undercouch.gradle.tasks.download.Download -import org.gradle.jvm.toolchain.JavaLanguageVersion -import xyz.jpenilla.runpaper.task.RunServer -import kotlin.system.exitProcess - -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2021 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -buildscript { - repositories.maven("https://jitpack.io") - dependencies.classpath("com.github.VolmitSoftware:NMSTools:c88961416f") -} - -plugins { - java - `java-library` - alias(libs.plugins.download) - alias(libs.plugins.runPaper) -} - -group = "com.volmit" -version = "3.9.2-1.20.1-1.21.11" - -apply() - -// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED -// ======================== WINDOWS ============================= -registerCustomOutputTask("Cyberpwn", "C://Users/cyberpwn/Documents/development/server/plugins") -registerCustomOutputTask("Psycho", "C://Dan/MinecraftDevelopment/Server/plugins") -registerCustomOutputTask("ArcaneArts", "C://Users/arcane/Documents/development/server/plugins") -registerCustomOutputTask("Coco", "D://mcsm/plugins") -registerCustomOutputTask("Strange", "D://Servers/1.17 Test Server/plugins") -registerCustomOutputTask("Vatuu", "D://Minecraft/Servers/1.19.4/plugins") -registerCustomOutputTask("CrazyDev22", "C://Users/Julian/Desktop/server/plugins") -registerCustomOutputTask("PixelFury", "C://Users/repix/workplace/Iris/1.21.3 - Development-Public-v3/plugins") -registerCustomOutputTask("PixelFuryDev", "C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins") -// ========================== UNIX ============================== -registerCustomOutputTaskUnix("CyberpwnLT", "/Users/danielmills/development/server/plugins") -registerCustomOutputTaskUnix("PsychoLT", "/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/plugins") -registerCustomOutputTaskUnix("PixelMac", "/Users/test/Desktop/mcserver/plugins") -registerCustomOutputTaskUnix("CrazyDev22LT", "/home/julian/Desktop/server/plugins") -// ============================================================== - -val serverMinHeap = "10G" -val serverMaxHeap = "10G" -val additionalFlags = "-XX:+AlwaysPreTouch" -//Valid values are: none, truecolor, indexed256, indexed16, indexed8 -val color = "truecolor" -val errorReporting = "true" == findProperty("errorReporting") - -val nmsBindings = mapOf( - "v1_21_R7" to "1.21.11-R0.1-SNAPSHOT", - "v1_21_R6" to "1.21.10-R0.1-SNAPSHOT", - "v1_21_R5" to "1.21.8-R0.1-SNAPSHOT", - "v1_21_R4" to "1.21.5-R0.1-SNAPSHOT", - "v1_21_R3" to "1.21.4-R0.1-SNAPSHOT", - "v1_21_R2" to "1.21.3-R0.1-SNAPSHOT", - "v1_21_R1" to "1.21.1-R0.1-SNAPSHOT", - "v1_20_R4" to "1.20.6-R0.1-SNAPSHOT", - "v1_20_R3" to "1.20.4-R0.1-SNAPSHOT", - "v1_20_R2" to "1.20.2-R0.1-SNAPSHOT", - "v1_20_R1" to "1.20.1-R0.1-SNAPSHOT", -) -val jvmVersion = mapOf() -nmsBindings.forEach { (key, value) -> - project(":nms:$key") { - apply() - - nmsBinding { - jvm = jvmVersion.getOrDefault(key, 21) - version = value - type = NMSBinding.Type.DIRECT - } - - dependencies { - compileOnly(project(":core")) - compileOnly(rootProject.libs.annotations) - compileOnly(rootProject.libs.byteBuddy.core) - } - } - - tasks.register("runServer-$key") { - group = "servers" - minecraftVersion(value.split("-")[0]) - minHeapSize = serverMinHeap - maxHeapSize = serverMaxHeap - pluginJars(tasks.jar.flatMap { it.archiveFile }) - javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(jvmVersion.getOrDefault(key, 21))} - runDirectory.convention(layout.buildDirectory.dir("run/$key")) - systemProperty("disable.watchdog", "true") - systemProperty("net.kyori.ansi.colorLevel", color) - systemProperty("com.mojang.eula.agree", true) - systemProperty("iris.suppressReporting", !errorReporting) - jvmArgs("-javaagent:${project(":core:agent").tasks.jar.flatMap { it.archiveFile }.get().asFile.absolutePath}") - jvmArgs(additionalFlags.split(' ')) - } -} - -val included: Configuration by configurations.creating -val jarJar: Configuration by configurations.creating -dependencies { - for (key in nmsBindings.keys) { - included(project(":nms:$key", "reobf")) - } - included(project(":core", "shadow")) - jarJar(project(":core:agent")) -} - -tasks { - jar { - inputs.files(included) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(jarJar, provider { included.resolve().map(::zipTree) }) - archiveFileName.set("Iris-${project.version}.jar") - } - - register("iris") { - group = "iris" - dependsOn("jar") - from(layout.buildDirectory.file("libs/Iris-${project.version}.jar")) - into(layout.buildDirectory) - } - - register("irisDev") { - group = "iris" - from(project(":core").layout.buildDirectory.files("libs/core-javadoc.jar", "libs/core-sources.jar")) - rename { it.replace("core", "Iris-${project.version}") } - into(layout.buildDirectory) - dependsOn(":core:sourcesJar") - dependsOn(":core:javadocJar") - } - - val cli = file("sentry-cli.exe") - register("downloadCli") { - group = "io.sentry" - src("https://release-registry.services.sentry.io/apps/sentry-cli/latest?response=download&arch=x86_64&platform=${System.getProperty("os.name")}&package=sentry-cli") - dest(cli) - - doLast { - cli.setExecutable(true) - } - } - - register("release") { - group = "io.sentry" - dependsOn("downloadCli") - doLast { - val url = "http://sentry.volmit.com:8080" - val authToken = project.findProperty("sentry.auth.token") ?: System.getenv("SENTRY_AUTH_TOKEN") - val org = "sentry" - val projectName = "iris" - exec(cli, "--url", url , "--auth-token", authToken, "releases", "new", "-o", org, "-p", projectName, version) - exec(cli, "--url", url , "--auth-token", authToken, "releases", "set-commits", "-o", org, "-p", projectName, version, "--auto", "--ignore-missing") - //exec(cli, "--url", url, "--auth-token", authToken, "releases", "finalize", "-o", org, "-p", projectName, version) - cli.delete() - } - } -} - -fun exec(vararg command: Any) { - val p = ProcessBuilder(command.map { it.toString() }) - .start() - p.inputStream.reader().useLines { it.forEach(::println) } - p.errorStream.reader().useLines { it.forEach(::println) } - p.waitFor() -} - -configurations.configureEach { - resolutionStrategy.cacheChangingModulesFor(60, "minutes") - resolutionStrategy.cacheDynamicVersionsFor(60, "minutes") -} - -allprojects { - apply() - - java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) - } - } - - repositories { - mavenCentral() - maven("https://repo.papermc.io/repository/maven-public/") - maven("https://repo.codemc.org/repository/maven-public/") - - maven("https://jitpack.io") // EcoItems, score - maven("https://repo.nexomc.com/releases/") // nexo - maven("https://maven.devs.beer/") // itemsadder - maven("https://repo.extendedclip.com/releases/") // placeholderapi - maven("https://mvn.lumine.io/repository/maven-public/") // mythic - maven("https://nexus.phoenixdevt.fr/repository/maven-public/") //MMOItems - maven("https://repo.onarandombox.com/content/groups/public/") //Multiverse Core - maven("https://repo.momirealms.net/releases/") //CraftEngine - } - - dependencies { - // Provided or Classpath - compileOnly(rootProject.libs.lombok) - annotationProcessor(rootProject.libs.lombok) - } - - /** - * We need parameter meta for the decree command system - */ - tasks { - compileJava { - options.compilerArgs.add("-parameters") - options.encoding = "UTF-8" - } - - javadoc { - options.encoding = "UTF-8" - options.quiet() - //options.addStringOption("Xdoclint:none") // TODO: Re-enable this - } - - register("sourcesJar") { - archiveClassifier.set("sources") - from(sourceSets.main.map { it.allSource }) - } - - register("javadocJar") { - archiveClassifier.set("javadoc") - from(javadoc.map { it.destinationDir!! }) - } - } -} - -if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { - System.err.println() - System.err.println("=========================================================================================================") - System.err.println("You must run gradle on Java 21 or newer. You are using " + JavaVersion.current()) - System.err.println() - System.err.println("=== For IDEs ===") - System.err.println("1. Configure the project for Java 21 toolchain") - System.err.println("2. Configure the bundled gradle to use Java 21+ in settings") - System.err.println() - System.err.println("=== For Command Line (gradlew) ===") - System.err.println("1. Install JDK 21 from https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html") - System.err.println("2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-21.0.4") - System.err.println("3. Open a new command prompt window to get the new environment variables if need be.") - System.err.println("=========================================================================================================") - System.err.println() - exitProcess(69) -} - - -fun registerCustomOutputTask(name: String, path: String) { - if (!System.getProperty("os.name").lowercase().contains("windows")) { - return - } - - tasks.register("build$name") { - group = "development" - outputs.upToDateWhen { false } - dependsOn("iris") - from(layout.buildDirectory.file("Iris-${project.version}.jar")) - into(file(path)) - rename { "Iris.jar" } - } -} - -fun registerCustomOutputTaskUnix(name: String, path: String) { - if (System.getProperty("os.name").lowercase().contains("windows")) { - return - } - - tasks.register("build$name") { - group = "development" - outputs.upToDateWhen { false } - dependsOn("iris") - from(layout.buildDirectory.file("Iris-${project.version}.jar")) - into(file(path)) - rename { "Iris.jar" } - } -} diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 000000000..2046a409d --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,27 @@ +import org.gradle.jvm.toolchain.JavaLanguageVersion + +plugins { + id 'java' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +repositories { + mavenCentral() + gradlePluginPortal() + maven { + url = uri('https://jitpack.io') + } +} + +dependencies { + implementation(gradleApi()) + implementation('org.ow2.asm:asm:9.10.1') + implementation('com.github.VolmitSoftware:NMSTools:c88961416f') + implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21') + testImplementation('junit:junit:4.13.2') +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index 983ee94c1..000000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,32 +0,0 @@ -import org.gradle.jvm.toolchain.JavaLanguageVersion -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - kotlin("jvm") version embeddedKotlinVersion -} - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) - } -} - -kotlin { - jvmToolchain(21) - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } -} - -repositories { - mavenCentral() - gradlePluginPortal() - maven("https://jitpack.io") -} - -dependencies { - implementation("org.ow2.asm:asm:9.8") - implementation("com.github.VolmitSoftware:NMSTools:c88961416f") - implementation("io.papermc.paperweight:paperweight-userdev:2.0.0-beta.18") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") -} diff --git a/buildSrc/src/main/java/ApiGenerator.java b/buildSrc/src/main/java/ApiGenerator.java new file mode 100644 index 000000000..4a90df4e7 --- /dev/null +++ b/buildSrc/src/main/java/ApiGenerator.java @@ -0,0 +1,162 @@ +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.publish.PublishingExtension; +import org.gradle.api.publish.maven.MavenPublication; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.tasks.Jar; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; + +public class ApiGenerator implements Plugin { + @Override + public void apply(Project target) { + target.getPlugins().apply("maven-publish"); + TaskProvider task = target.getTasks().register("irisApi", GenerateApiTask.class); + + PublishingExtension publishing = target.getExtensions().findByType(PublishingExtension.class); + if (publishing == null) { + throw new GradleException("Publishing extension not found"); + } + + publishing.getRepositories().maven(repository -> { + repository.setName("deployDir"); + repository.setUrl(targetDirectory(target).toURI()); + }); + + publishing.getPublications().create("maven", MavenPublication.class, publication -> { + publication.setGroupId(target.getName()); + publication.setVersion(target.getVersion().toString()); + publication.artifact(task); + }); + } + + public static File targetDirectory(Project project) { + String dir = System.getenv("DEPLOY_DIR"); + if (dir == null) { + return project.getLayout().getBuildDirectory().dir("api").get().getAsFile(); + } + return new File(dir); + } +} + +abstract class GenerateApiTask extends DefaultTask { + private final File inputFile; + private final File outputFile; + + public GenerateApiTask() { + setGroup("iris"); + dependsOn("jar"); + finalizedBy("publishMavenPublicationToDeployDirRepository"); + doLast(task -> getLogger().lifecycle("The API is located at " + getOutputFile().getAbsolutePath())); + + TaskProvider jarTask = getProject().getTasks().named("jar", Jar.class); + this.inputFile = jarTask.get().getArchiveFile().get().getAsFile(); + this.outputFile = ApiGenerator.targetDirectory(getProject()).toPath().resolve(this.inputFile.getName()).toFile(); + } + + @InputFile + public File getInputFile() { + return inputFile; + } + + @OutputFile + public File getOutputFile() { + return outputFile; + } + + @TaskAction + public void generate() throws IOException { + File parent = outputFile.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + + try (JarFile jar = new JarFile(inputFile); + JarOutputStream out = new JarOutputStream(new FileOutputStream(outputFile))) { + jar.stream() + .parallel() + .filter(entry -> !entry.isDirectory()) + .filter(entry -> entry.getName().endsWith(".class")) + .forEach(entry -> writeStrippedClass(jar, out, entry)); + } + } + + private static void writeStrippedClass(JarFile jar, JarOutputStream out, JarEntry entry) { + byte[] bytes; + try (InputStream input = jar.getInputStream(entry)) { + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); + ClassVisitor visitor = new MethodClearingVisitor(writer); + ClassReader reader = new ClassReader(input); + reader.accept(visitor, 0); + bytes = writer.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + synchronized (out) { + try { + JarEntry outputEntry = new JarEntry(entry.getName()); + out.putNextEntry(outputEntry); + out.write(bytes); + out.closeEntry(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } +} + +class MethodClearingVisitor extends ClassVisitor { + public MethodClearingVisitor(ClassVisitor cv) { + super(Opcodes.ASM9, cv); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return new ExceptionThrowingMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions)); + } +} + +class ExceptionThrowingMethodVisitor extends MethodVisitor { + public ExceptionThrowingMethodVisitor(MethodVisitor mv) { + super(Opcodes.ASM9, mv); + } + + @Override + public void visitCode() { + if (mv == null) { + return; + } + + mv.visitCode(); + mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException"); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn("Only API"); + mv.visitMethodInsn( + Opcodes.INVOKESPECIAL, + "java/lang/IllegalStateException", + "", + "(Ljava/lang/String;)V", + false + ); + mv.visitInsn(Opcodes.ATHROW); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } +} diff --git a/buildSrc/src/main/java/BukkitArtifactVerifier.java b/buildSrc/src/main/java/BukkitArtifactVerifier.java new file mode 100644 index 000000000..bede3b3a6 --- /dev/null +++ b/buildSrc/src/main/java/BukkitArtifactVerifier.java @@ -0,0 +1,183 @@ +import org.gradle.api.GradleException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Packaging gate for the CraftBukkit artifact. Every shadowJar exclude is silent at build time, so + * this re-derives the class reference graph over the shipped jar and fails on anything the excludes + * left dangling, plus asserts the entries that are only ever reached by name. + */ +public final class BukkitArtifactVerifier { + // Packages the artifact is expected to ship in full. A CONSTANT_Class entry naming a missing + // class under one of these is a NoClassDefFoundError waiting for the right code path. + private static final List SHIPPED_PREFIXES = List.of( + "art/arcane/iris/", + "art/arcane/volmlib/", + "com/google/gson/", + "com/googlecode/concurrentlinkedhashmap/" + ); + // Relocation targets for the libraries slimjar downloads and relocates at runtime. Compiled + // references to them are correct and the classes are correctly absent from the jar. + private static final List RUNTIME_DOWNLOADED_PREFIXES = List.of( + "art/arcane/iris/util/paralithic/", + "art/arcane/iris/util/paper/", + "art/arcane/iris/util/kyori/", + "art/arcane/iris/util/metrics/", + "art/arcane/iris/util/sentry/", + "art/arcane/iris/util/maven/", + "art/arcane/iris/util/plexus/", + "art/arcane/iris/util/sisu/", + "art/arcane/iris/util/aether/", + "art/arcane/iris/util/guice/", + "art/arcane/iris/util/dom4j/", + "art/arcane/iris/util/jaxen/" + ); + private static final String CAFFEINE_CACHE_PACKAGE = "art/arcane/iris/util/caffeine/cache/"; + private static final String MATTER_SLICE_PACKAGE = "art/arcane/volmlib/util/matter/slices/"; + private static final String LANGUAGE_DIRECTORY = "languages/"; + + private BukkitArtifactVerifier() { + } + + public static void verify(File artifact, List requiredEntries, int minimumLocales, + int minimumCaffeineFactories, int minimumMatterSlices) { + if (!artifact.isFile()) { + throw new GradleException("Missing Bukkit Iris artifact: " + artifact.getAbsolutePath()); + } + + try (JarFile jar = new JarFile(artifact)) { + for (String requiredEntry : requiredEntries) { + if (jar.getJarEntry(requiredEntry) == null) { + throw new GradleException(artifact.getName() + " is missing " + requiredEntry + + ". If this run also requested a task whose name contains \"test\", :core:processResources" + + " was disabled for it and the artifact is incomplete by construction - build the jar in" + + " its own invocation."); + } + } + + Set shippedClasses = new LinkedHashSet<>(); + int locales = 0; + int caffeineFactories = 0; + int matterSlices = 0; + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + continue; + } + + String name = entry.getName(); + if (name.startsWith(LANGUAGE_DIRECTORY) && name.endsWith(".json")) { + locales++; + } + if (!name.endsWith(".class")) { + continue; + } + + String internalName = name.substring(0, name.length() - ".class".length()); + shippedClasses.add(internalName); + if (isGeneratedCaffeineFactory(internalName)) { + caffeineFactories++; + } + if (internalName.startsWith(MATTER_SLICE_PACKAGE)) { + matterSlices++; + } + } + + if (locales < minimumLocales) { + throw new GradleException(artifact.getName() + " ships " + locales + " locale files; expected at least " + + minimumLocales); + } + // Caffeine picks its cache and node implementation with MethodHandles.Lookup.findClass on a + // name built from the builder's feature flags. Nothing references these statically, so only a + // population check can tell that an exclude or minimize() ate them. + if (caffeineFactories < minimumCaffeineFactories) { + throw new GradleException(artifact.getName() + " ships " + caffeineFactories + + " generated Caffeine cache classes; expected at least " + minimumCaffeineFactories + + ". Caffeine resolves these by name and cannot survive static pruning"); + } + // Matter.read() resolves slice types from the canonical name stored in the payload. + if (matterSlices < minimumMatterSlices) { + throw new GradleException(artifact.getName() + " ships " + matterSlices + + " Matter slice types; expected at least " + minimumMatterSlices + + ". Matter payloads name these types directly"); + } + + TreeMap dangling = new TreeMap<>(); + entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.isDirectory() || !entry.getName().endsWith(".class")) { + continue; + } + + for (String reference : ClassReferences.read(readEntryBytes(jar, entry))) { + if (shippedClasses.contains(reference) + || !startsWithAny(reference, SHIPPED_PREFIXES) + || startsWithAny(reference, RUNTIME_DOWNLOADED_PREFIXES)) { + continue; + } + dangling.putIfAbsent(reference, entry.getName()); + } + } + + if (!dangling.isEmpty()) { + StringBuilder message = new StringBuilder(artifact.getName()) + .append(" references classes it does not ship. A shadowJar exclude removed a class that is") + .append(" still in use:"); + dangling.entrySet().stream().limit(12).forEach(missing -> message.append("\n ") + .append(missing.getKey()) + .append(" (referenced by ") + .append(missing.getValue()) + .append(')')); + if (dangling.size() > 12) { + message.append("\n ... and ").append(dangling.size() - 12).append(" more"); + } + throw new GradleException(message.toString()); + } + } catch (IOException e) { + throw new GradleException("Unable to verify Bukkit Iris artifact " + artifact.getAbsolutePath(), e); + } + } + + private static boolean isGeneratedCaffeineFactory(String internalName) { + if (!internalName.startsWith(CAFFEINE_CACHE_PACKAGE)) { + return false; + } + + String simpleName = internalName.substring(CAFFEINE_CACHE_PACKAGE.length()); + if (simpleName.isEmpty() || simpleName.indexOf('/') >= 0) { + return false; + } + for (int i = 0; i < simpleName.length(); i++) { + if (simpleName.charAt(i) < 'A' || simpleName.charAt(i) > 'Z') { + return false; + } + } + return true; + } + + private static byte[] readEntryBytes(JarFile jar, JarEntry entry) throws IOException { + try (InputStream input = jar.getInputStream(entry)) { + return input.readAllBytes(); + } + } + + private static boolean startsWithAny(String value, List prefixes) { + for (String prefix : prefixes) { + if (value.startsWith(prefix)) { + return true; + } + } + return false; + } +} diff --git a/buildSrc/src/main/java/ClassReferences.java b/buildSrc/src/main/java/ClassReferences.java new file mode 100644 index 000000000..75b3e1a18 --- /dev/null +++ b/buildSrc/src/main/java/ClassReferences.java @@ -0,0 +1,114 @@ +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Reads the {@code CONSTANT_Class} entries of a class file. That is exactly the set of types the + * class links against - supertypes, field and method owners, casts, catches, constant class + * literals. Annotation types live in the annotation attributes as plain UTF-8 descriptors and are + * deliberately not reported: the JVM skips an annotation whose type is absent. + */ +public final class ClassReferences { + private static final int CONSTANT_UTF8 = 1; + private static final int CONSTANT_INTEGER = 3; + private static final int CONSTANT_FLOAT = 4; + private static final int CONSTANT_LONG = 5; + private static final int CONSTANT_DOUBLE = 6; + private static final int CONSTANT_CLASS = 7; + private static final int CONSTANT_STRING = 8; + private static final int CONSTANT_FIELDREF = 9; + private static final int CONSTANT_METHODREF = 10; + private static final int CONSTANT_INTERFACE_METHODREF = 11; + private static final int CONSTANT_NAME_AND_TYPE = 12; + private static final int CONSTANT_METHOD_HANDLE = 15; + private static final int CONSTANT_METHOD_TYPE = 16; + private static final int CONSTANT_DYNAMIC = 17; + private static final int CONSTANT_INVOKE_DYNAMIC = 18; + private static final int CONSTANT_MODULE = 19; + private static final int CONSTANT_PACKAGE = 20; + private static final int CLASS_FILE_MAGIC = 0xCAFEBABE; + + private ClassReferences() { + } + + public static Set read(byte[] classFile) { + Set references = new LinkedHashSet<>(); + if (classFile.length < 10 || readInt(classFile, 0) != CLASS_FILE_MAGIC) { + return references; + } + + int constantCount = readUnsignedShort(classFile, 8); + String[] utf8 = new String[constantCount]; + List classNameIndexes = new ArrayList<>(); + int offset = 10; + int index = 1; + while (index < constantCount) { + int tag = classFile[offset] & 0xFF; + offset++; + switch (tag) { + case CONSTANT_UTF8 -> { + int length = readUnsignedShort(classFile, offset); + offset += 2; + utf8[index] = new String(classFile, offset, length, StandardCharsets.UTF_8); + offset += length; + } + case CONSTANT_CLASS -> { + classNameIndexes.add(readUnsignedShort(classFile, offset)); + offset += 2; + } + case CONSTANT_STRING, CONSTANT_METHOD_TYPE, CONSTANT_MODULE, CONSTANT_PACKAGE -> offset += 2; + case CONSTANT_METHOD_HANDLE -> offset += 3; + case CONSTANT_INTEGER, CONSTANT_FLOAT, CONSTANT_FIELDREF, CONSTANT_METHODREF, + CONSTANT_INTERFACE_METHODREF, CONSTANT_NAME_AND_TYPE, CONSTANT_DYNAMIC, + CONSTANT_INVOKE_DYNAMIC -> offset += 4; + case CONSTANT_LONG, CONSTANT_DOUBLE -> { + offset += 8; + index++; + } + default -> { + return references; + } + } + index++; + } + + for (int nameIndex : classNameIndexes) { + if (nameIndex <= 0 || nameIndex >= constantCount) { + continue; + } + String name = normalize(utf8[nameIndex]); + if (name != null) { + references.add(name); + } + } + return references; + } + + private static String normalize(String rawName) { + if (rawName == null || rawName.isEmpty()) { + return null; + } + + String name = rawName; + while (name.startsWith("[")) { + name = name.substring(1); + } + if (name.startsWith("L") && name.endsWith(";")) { + name = name.substring(1, name.length() - 1); + } + return name.length() > 1 ? name : null; + } + + private static int readUnsignedShort(byte[] data, int offset) { + return ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF); + } + + private static int readInt(byte[] data, int offset) { + return ((data[offset] & 0xFF) << 24) + | ((data[offset + 1] & 0xFF) << 16) + | ((data[offset + 2] & 0xFF) << 8) + | (data[offset + 3] & 0xFF); + } +} diff --git a/buildSrc/src/main/java/Config.java b/buildSrc/src/main/java/Config.java new file mode 100644 index 000000000..5c38dd6e4 --- /dev/null +++ b/buildSrc/src/main/java/Config.java @@ -0,0 +1,5 @@ +public class Config { + public int jvm = 25; + public NMSBinding.Type type = NMSBinding.Type.DIRECT; + public String version; +} diff --git a/buildSrc/src/main/java/ModdedArtifactVerifier.java b/buildSrc/src/main/java/ModdedArtifactVerifier.java new file mode 100644 index 000000000..30c4305db --- /dev/null +++ b/buildSrc/src/main/java/ModdedArtifactVerifier.java @@ -0,0 +1,396 @@ +import org.gradle.api.GradleException; +import org.objectweb.asm.ClassReader; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class ModdedArtifactVerifier { + private static final String CODEC_CLASS = "art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class"; + private static final List INTERNAL_LZ4_REFERENCES = List.of( + "net/jpountz/lz4/LZ4BlockInputStream", + "net/jpountz/lz4/LZ4BlockOutputStream" + ); + private static final String INTERNAL_OSHI_REFERENCE = "oshi/SystemInfo"; + private static final List BUNDLED_RUNTIME_PREFIXES = List.of( + "net/jpountz/", + "oshi/", + "com/sun/jna/", + "art/arcane/iris/shadow/jpountz/", + "art/arcane/iris/shadow/oshi/", + "art/arcane/iris/shadow/jna/" + ); + private static final List PRIVATE_REFERENCE_PREFIXES = List.of( + "art/arcane/iris/shadow/jpountz", + "art/arcane/iris/shadow/oshi", + "art/arcane/iris/shadow/jna" + ); + // ASM must always be relocated: the loaders put their own copy on the module layer and a second + // org.objectweb.asm package on the same layer is a boot failure. + private static final String ASM_PREFIX = "org/objectweb/asm/"; + // Only the Bukkit platform binding is allowed to sit on org.bukkit types, and the modded jars + // exclude it outright. Kept as an exemption so the same check can run over the Bukkit artifact. + private static final String BUKKIT_PLATFORM_PREFIX = "art/arcane/iris/platform/bukkit/"; + private static final String BUKKIT_TYPE_PREFIX = "org/bukkit/"; + private static final String FABRIC_METADATA = "fabric.mod.json"; + private static final String NEOFORGE_METADATA = "META-INF/neoforge.mods.toml"; + private static final String NESTED_JAR_DIRECTORY = "META-INF/jars/"; + private static final String MIXIN_CONFIGS_ATTRIBUTE = "MixinConfigs"; + // Forge 26.2 ships upstream Mixin 0.8.7, whose MixinEnvironment.CompatibilityLevel enum ends at + // JAVA_21. Anything higher is a hard MixinInitialisationError at Forge boot even though Fabric's + // fork accepts it, so the shared configs are capped here. + private static final int MAX_MIXIN_COMPATIBILITY_LEVEL = 21; + private static final Pattern TOML_CONFIG_ASSIGNMENT = Pattern.compile("(?m)^\\s*config\\s*=\\s*\"([^\"]+)\""); + private static final Pattern COMPATIBILITY_LEVEL = Pattern.compile("\"compatibilityLevel\"\\s*:\\s*\"([^\"]*)\""); + private static final Pattern COMPATIBILITY_LEVEL_VALUE = Pattern.compile("JAVA_(\\d+)"); + private static final Pattern QUOTED_JSON_FILE = Pattern.compile("\"([^\"]+\\.(?:json|jar))\""); + + private ModdedArtifactVerifier() { + } + + public static void verify(File artifact, List requiredEntries) { + verify(artifact, requiredEntries, Set.of()); + } + + /** + * @param bukkitSupertypeBaseline entry names (as they appear in the jar) that are known to + * extend or implement an {@code org.bukkit} type. New offenders + * outside this frozen set fail the build; entries that disappear + * from the artifact are not an error. + */ + public static void verify(File artifact, List requiredEntries, Set bukkitSupertypeBaseline) { + if (!artifact.isFile()) { + throw new GradleException("Missing modded Iris artifact: " + artifact.getAbsolutePath()); + } + + try (JarFile jar = new JarFile(artifact)) { + for (String requiredEntry : requiredEntries) { + if (jar.getJarEntry(requiredEntry) == null) { + throw new GradleException(artifact.getName() + " is missing " + requiredEntry); + } + } + + Set bundledRuntimeEntries = new LinkedHashSet<>(); + Set privateReferenceClasses = new LinkedHashSet<>(); + Set codecReferences = new LinkedHashSet<>(); + Set asmEntries = new LinkedHashSet<>(); + Set shippedNestedJars = new LinkedHashSet<>(); + Set bukkitSupertypeClasses = new LinkedHashSet<>(); + boolean oshiReference = false; + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + continue; + } + + String name = entry.getName(); + if (startsWithAny(name, BUNDLED_RUNTIME_PREFIXES)) { + bundledRuntimeEntries.add(name); + } + if (name.startsWith(ASM_PREFIX)) { + asmEntries.add(name); + } + if (name.startsWith(NESTED_JAR_DIRECTORY) && name.endsWith(".jar")) { + shippedNestedJars.add(name); + } + if (isNestedJar(name)) { + inspectNestedJar(jar, entry, bundledRuntimeEntries, privateReferenceClasses); + } + if (!name.endsWith(".class")) { + continue; + } + + byte[] classFile = readEntryBytes(jar, entry); + String bytecode = new String(classFile, StandardCharsets.ISO_8859_1); + if (containsAny(bytecode, PRIVATE_REFERENCE_PREFIXES)) { + privateReferenceClasses.add(name); + } + if (bytecode.contains(INTERNAL_OSHI_REFERENCE)) { + oshiReference = true; + } + if (name.equals(CODEC_CLASS)) { + for (String reference : INTERNAL_LZ4_REFERENCES) { + if (bytecode.contains(reference)) { + codecReferences.add(reference); + } + } + } + if (!name.startsWith(BUKKIT_PLATFORM_PREFIX) + && !bukkitSupertypeBaseline.contains(name) + && hasBukkitSupertype(classFile)) { + bukkitSupertypeClasses.add(name); + } + } + + if (!bundledRuntimeEntries.isEmpty()) { + throw new GradleException(artifact.getName() + " duplicates Minecraft runtime libraries: " + + preview(bundledRuntimeEntries)); + } + if (!privateReferenceClasses.isEmpty()) { + throw new GradleException(artifact.getName() + " contains unresolved Iris-private runtime references in: " + + preview(privateReferenceClasses)); + } + if (!codecReferences.containsAll(INTERNAL_LZ4_REFERENCES)) { + throw new GradleException(artifact.getName() + " does not link region storage to Minecraft's LZ4 runtime"); + } + if (!oshiReference) { + throw new GradleException(artifact.getName() + " does not link hardware diagnostics to Minecraft's OSHI runtime"); + } + if (!asmEntries.isEmpty()) { + throw new GradleException(artifact.getName() + " ships unrelocated ASM: " + preview(asmEntries)); + } + if (!bukkitSupertypeClasses.isEmpty()) { + throw new GradleException(artifact.getName() + " ships classes with an org.bukkit supertype outside " + + BUKKIT_PLATFORM_PREFIX + ": " + preview(bukkitSupertypeClasses)); + } + + verifyNestedJarDeclarations(artifact, jar, shippedNestedJars); + verifyMixinCompatibilityLevels(artifact, jar); + } catch (IOException e) { + throw new GradleException("Unable to verify modded Iris artifact " + artifact.getAbsolutePath(), e); + } + } + + private static void verifyNestedJarDeclarations(File artifact, JarFile jar, Set shippedNestedJars) + throws IOException { + String metadata = readEntryText(jar, FABRIC_METADATA); + if (metadata == null) { + if (!shippedNestedJars.isEmpty()) { + throw new GradleException(artifact.getName() + " ships nested jars but has no " + FABRIC_METADATA + + " to declare them: " + preview(shippedNestedJars)); + } + return; + } + + Set declared = collectQuotedFiles(extractJsonArray(metadata, "jars"), ".jar"); + Set undeclared = new LinkedHashSet<>(shippedNestedJars); + undeclared.removeAll(declared); + if (!undeclared.isEmpty()) { + throw new GradleException(artifact.getName() + " ships nested jars that " + FABRIC_METADATA + + " does not declare: " + preview(undeclared)); + } + + Set missing = new LinkedHashSet<>(declared); + missing.removeAll(shippedNestedJars); + if (!missing.isEmpty()) { + throw new GradleException(artifact.getName() + " declares nested jars in " + FABRIC_METADATA + + " that it does not ship: " + preview(missing)); + } + } + + private static void verifyMixinCompatibilityLevels(File artifact, JarFile jar) throws IOException { + Set configs = new LinkedHashSet<>(); + Manifest manifest = jar.getManifest(); + if (manifest != null) { + String declared = manifest.getMainAttributes().getValue(MIXIN_CONFIGS_ATTRIBUTE); + if (declared != null) { + for (String config : declared.split(",")) { + String trimmed = config.trim(); + if (!trimmed.isEmpty()) { + configs.add(trimmed); + } + } + } + } + + String neoforgeMetadata = readEntryText(jar, NEOFORGE_METADATA); + if (neoforgeMetadata != null) { + Matcher matcher = TOML_CONFIG_ASSIGNMENT.matcher(neoforgeMetadata); + while (matcher.find()) { + configs.add(matcher.group(1)); + } + } + + String fabricMetadata = readEntryText(jar, FABRIC_METADATA); + if (fabricMetadata != null) { + configs.addAll(collectQuotedFiles(extractJsonArray(fabricMetadata, "mixins"), ".json")); + } + + if (configs.isEmpty()) { + throw new GradleException(artifact.getName() + " registers no mixin configs; expected the " + + MIXIN_CONFIGS_ATTRIBUTE + " manifest attribute (Forge), [[mixins]] (NeoForge), or a mixins " + + "array (Fabric)"); + } + + for (String config : configs) { + String body = readEntryText(jar, config); + if (body == null) { + throw new GradleException(artifact.getName() + " registers mixin config " + config + + " which is not in the jar"); + } + + Matcher matcher = COMPATIBILITY_LEVEL.matcher(body); + if (!matcher.find()) { + continue; + } + + String level = matcher.group(1); + Matcher value = COMPATIBILITY_LEVEL_VALUE.matcher(level); + if (!value.matches()) { + throw new GradleException(artifact.getName() + " mixin config " + config + + " has unparseable compatibilityLevel " + level); + } + if (Integer.parseInt(value.group(1)) > MAX_MIXIN_COMPATIBILITY_LEVEL) { + throw new GradleException(artifact.getName() + " mixin config " + config + + " requests compatibilityLevel " + level + "; upstream Mixin 0.8.7 (Forge) tops out at JAVA_" + + MAX_MIXIN_COMPATIBILITY_LEVEL); + } + } + } + + private static boolean hasBukkitSupertype(byte[] classFile) { + ClassReader reader; + try { + reader = new ClassReader(classFile); + } catch (RuntimeException e) { + // Class file version newer than the bundled ASM. Nothing to assert about it. + return false; + } + + String superName = reader.getSuperName(); + if (superName != null && superName.startsWith(BUKKIT_TYPE_PREFIX)) { + return true; + } + for (String candidate : reader.getInterfaces()) { + if (candidate.startsWith(BUKKIT_TYPE_PREFIX)) { + return true; + } + } + return false; + } + + /** + * Returns the balanced {@code [...]} block that follows {@code "key"}, or an empty string. + */ + private static String extractJsonArray(String json, String key) { + int keyIndex = json.indexOf('"' + key + '"'); + if (keyIndex < 0) { + return ""; + } + + int open = json.indexOf('[', keyIndex); + if (open < 0) { + return ""; + } + + int depth = 0; + for (int i = open; i < json.length(); i++) { + char c = json.charAt(i); + if (c == '[') { + depth++; + } else if (c == ']') { + depth--; + if (depth == 0) { + return json.substring(open, i + 1); + } + } + } + return ""; + } + + private static Set collectQuotedFiles(String region, String suffix) { + Set found = new LinkedHashSet<>(); + Matcher matcher = QUOTED_JSON_FILE.matcher(region); + while (matcher.find()) { + String value = matcher.group(1); + if (value.endsWith(suffix)) { + found.add(value); + } + } + return found; + } + + private static String readEntryText(JarFile jar, String name) throws IOException { + JarEntry entry = jar.getJarEntry(name); + if (entry == null) { + return null; + } + return new String(readEntryBytes(jar, entry), StandardCharsets.UTF_8); + } + + private static byte[] readEntryBytes(JarFile jar, JarEntry entry) throws IOException { + try (InputStream input = jar.getInputStream(entry)) { + return input.readAllBytes(); + } + } + + private static boolean startsWithAny(String value, List prefixes) { + for (String prefix : prefixes) { + if (value.startsWith(prefix)) { + return true; + } + } + return false; + } + + private static boolean containsAny(String value, List fragments) { + for (String fragment : fragments) { + if (value.contains(fragment)) { + return true; + } + } + return false; + } + + private static void inspectNestedJar(JarFile jar, JarEntry nestedEntry, + Set bundledRuntimeEntries, + Set privateReferenceClasses) throws IOException { + String nestedName = nestedEntry.getName(); + if (isNamedRuntimeLibrary(nestedName)) { + bundledRuntimeEntries.add(nestedName); + } + + try (InputStream input = jar.getInputStream(nestedEntry); + JarInputStream nestedJar = new JarInputStream(input)) { + JarEntry entry; + while ((entry = nestedJar.getNextJarEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + + String name = entry.getName(); + String path = nestedName + "!/" + name; + if (startsWithAny(name, BUNDLED_RUNTIME_PREFIXES)) { + bundledRuntimeEntries.add(path); + } + if (name.endsWith(".class")) { + String bytecode = new String(nestedJar.readAllBytes(), StandardCharsets.ISO_8859_1); + if (containsAny(bytecode, PRIVATE_REFERENCE_PREFIXES)) { + privateReferenceClasses.add(path); + } + } + } + } + } + + private static boolean isNestedJar(String name) { + return name.endsWith(".jar") + && (name.startsWith(NESTED_JAR_DIRECTORY) || name.startsWith("META-INF/jarjar/")); + } + + private static boolean isNamedRuntimeLibrary(String name) { + if (!isNestedJar(name)) { + return false; + } + + String fileName = name.substring(name.lastIndexOf('/') + 1).toLowerCase(Locale.ROOT); + return fileName.startsWith("lz4") || fileName.startsWith("oshi") || fileName.startsWith("jna"); + } + + private static String preview(Set values) { + return String.join(", ", values.stream().limit(8).toList()); + } +} diff --git a/buildSrc/src/main/java/NMSBinding.java b/buildSrc/src/main/java/NMSBinding.java new file mode 100644 index 000000000..588b759c4 --- /dev/null +++ b/buildSrc/src/main/java/NMSBinding.java @@ -0,0 +1,285 @@ +import com.volmit.nmstools.NMSToolsExtension; +import com.volmit.nmstools.NMSToolsPlugin; +import io.papermc.paperweight.userdev.PaperweightUser; +import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension; +import io.papermc.paperweight.userdev.PaperweightUserExtension; +import io.papermc.paperweight.userdev.attribute.Obfuscation; +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.Named; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.attributes.Bundling; +import org.gradle.api.attributes.Category; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.attributes.Usage; +import org.gradle.api.file.FileTree; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.plugins.ExtraPropertiesExtension; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.work.DisableCachingByDefault; + +import javax.inject.Inject; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.papermc.paperweight.util.constants.ConstantsKt.REOBF_CONFIG; + +public class NMSBinding implements Plugin { + private static final String NEW_LINE = System.lineSeparator(); + private static final byte[] NEW_LINE_BYTES = NEW_LINE.getBytes(StandardCharsets.UTF_8); + + @Override + public void apply(Project target) { + ExtraPropertiesExtension extra = target.getExtensions().getExtraProperties(); + Object configValue = extra.has("nms") ? extra.get("nms") : null; + if (!(configValue instanceof Config)) { + throw new GradleException("No NMS binding configuration found"); + } + + Config config = (Config) configValue; + int jvm = config.jvm; + Type type = config.type; + + if (type == Type.USER_DEV) { + target.getPlugins().apply(PaperweightUser.class); + + PaperweightUserDependenciesExtension dependenciesExtension = + target.getDependencies().getExtensions().findByType(PaperweightUserDependenciesExtension.class); + if (dependenciesExtension != null) { + dependenciesExtension.paperDevBundle(config.version); + } + + JavaPluginExtension java = target.getExtensions().findByType(JavaPluginExtension.class); + if (java == null) { + throw new GradleException("Java plugin not found"); + } + + java.getToolchain().getLanguageVersion().set(JavaLanguageVersion.of(jvm)); + JavaToolchainService javaToolchains = target.getExtensions().getByType(JavaToolchainService.class); + target.getExtensions().configure(PaperweightUserExtension.class, + extension -> extension.getJavaLauncher().set(javaToolchains.launcherFor(java.getToolchain()))); + } else { + extra.set("nmsTools.useBuildTools", type == Type.BUILD_TOOLS); + target.getPlugins().apply(NMSToolsPlugin.class); + target.getExtensions().configure(NMSToolsExtension.class, extension -> { + extension.getJvm().set(jvm); + extension.getVersion().set(config.version); + }); + + } + + String outgoingArtifactTask = type == Type.USER_DEV ? "jar" : "remap"; + ObjectFactory objects = target.getObjects(); + Configuration reobfConfiguration = target.getConfigurations().findByName(REOBF_CONFIG); + if (reobfConfiguration == null) { + reobfConfiguration = target.getConfigurations().create(REOBF_CONFIG); + } + + target.getConfigurations().named(REOBF_CONFIG).configure(configuration -> { + configuration.setCanBeConsumed(true); + configuration.setCanBeResolved(false); + configuration.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, named(objects, Usage.class, Usage.JAVA_RUNTIME)); + configuration.getAttributes().attribute(Category.CATEGORY_ATTRIBUTE, named(objects, Category.class, Category.LIBRARY)); + configuration.getAttributes().attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, named(objects, LibraryElements.class, LibraryElements.JAR)); + configuration.getAttributes().attribute(Bundling.BUNDLING_ATTRIBUTE, named(objects, Bundling.class, Bundling.EXTERNAL)); + configuration.getAttributes().attribute(Obfuscation.Companion.getOBFUSCATION_ATTRIBUTE(), named(objects, Obfuscation.class, Obfuscation.OBFUSCATED)); + configuration.getOutgoing().artifact(target.getTasks().named(outgoingArtifactTask)); + }); + + int[] version = parseVersion(config.version); + int major = version[0]; + int minor = version[1]; + if (major <= 20 && minor <= 4) { + return; + } + + target.getTasks().register("convert", ConversionTask.class, type); + target.getTasks().named("compileJava").configure(task -> task.dependsOn("convert")); + target.getRootProject().getTasks() + .matching(task -> task.getName().equals("prepareKotlinBuildScriptModel")) + .configureEach(task -> task.dependsOn(target.getPath() + ":convert")); + } + + public static void nmsBinding(Project project, Action action) { + Config config = new Config(); + action.execute(config); + project.getExtensions().getExtraProperties().set("nms", config); + project.getPlugins().apply(NMSBinding.class); + } + + private static int[] parseVersion(String version) { + String trimmed = version; + int suffix = trimmed.indexOf('-'); + if (suffix >= 0) { + trimmed = trimmed.substring(0, suffix); + } + + String[] parts = trimmed.split("\\."); + if ("1".equals(parts[0])) { + return new int[]{Integer.parseInt(parts[1]), Integer.parseInt(parts[2])}; + } + return new int[]{Integer.parseInt(parts[0]), Integer.parseInt(parts[1])}; + } + + private static T named(ObjectFactory objects, Class type, String name) { + return objects.named(type, name); + } + + @DisableCachingByDefault + public abstract static class ConversionTask extends DefaultTask { + private final Pattern pattern; + private final String replacement; + + @Inject + public ConversionTask(Type type) { + setGroup("nms"); + getInputs().property("type", type); + + JavaPluginExtension java = getProject().getExtensions().findByType(JavaPluginExtension.class); + if (java == null) { + throw new GradleException("Java plugin not found"); + } + + Provider source = java.getSourceSets().named(SourceSet.MAIN_SOURCE_SET_NAME).map(SourceSet::getAllJava); + getInputs().files(source); + getOutputs().files(source); + + if (type == Type.USER_DEV) { + this.pattern = Pattern.compile("org\\.bukkit\\.craftbukkit\\." + getProject().getName()); + this.replacement = "org.bukkit.craftbukkit"; + } else { + this.pattern = Pattern.compile("org\\.bukkit\\.craftbukkit\\.(?!" + getProject().getName() + ")"); + this.replacement = "org.bukkit.craftbukkit." + getProject().getName() + "."; + } + } + + @TaskAction + public void process() { + ExecutorService executor = Executors.newFixedThreadPool(16); + try { + Set files = getInputs().getFiles().getFiles(); + List> futures = new ArrayList<>(files.size()); + for (File file : files) { + if (!file.getName().endsWith(".java")) { + continue; + } + futures.add(executor.submit(() -> processFile(file))); + } + + for (Future future : futures) { + future.get(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } finally { + executor.shutdown(); + } + } + + private void processFile(File file) { + List output = new ArrayList<>(); + boolean changed = false; + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("package") || line.isBlank()) { + output.add(line); + continue; + } + + if (!line.startsWith("import")) { + if (!changed) { + return; + } + + output.add(line); + continue; + } + + Matcher matcher = pattern.matcher(line); + if (!matcher.find()) { + output.add(line); + continue; + } + + output.add(matcher.replaceAll(replacement)); + changed = true; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (!changed) { + return; + } + + try { + if (hasTrailingNewLine(file)) { + output.add(""); + } + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + for (int i = 0; i < output.size(); i++) { + writer.append(output.get(i)); + if (i + 1 < output.size()) { + writer.append(NEW_LINE); + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private boolean hasTrailingNewLine(File file) throws IOException { + if (NEW_LINE_BYTES.length == 0) { + return false; + } + + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + if (raf.length() < NEW_LINE_BYTES.length) { + return false; + } + + byte[] bytes = new byte[NEW_LINE_BYTES.length]; + raf.seek(raf.length() - bytes.length); + raf.readFully(bytes); + return Arrays.equals(bytes, NEW_LINE_BYTES); + } + } + } + + public enum Type { + USER_DEV, + BUILD_TOOLS, + DIRECT + } +} diff --git a/buildSrc/src/main/kotlin/ApiGenerator.kt b/buildSrc/src/main/kotlin/ApiGenerator.kt deleted file mode 100644 index 530d4c6eb..000000000 --- a/buildSrc/src/main/kotlin/ApiGenerator.kt +++ /dev/null @@ -1,121 +0,0 @@ -import org.gradle.api.DefaultTask -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction -import org.gradle.jvm.tasks.Jar -import org.objectweb.asm.* -import java.io.File -import java.util.jar.JarFile -import java.util.jar.JarOutputStream - -class ApiGenerator : Plugin { - override fun apply(target: Project): Unit = with(target) { - plugins.apply("maven-publish") - val task = tasks.register("irisApi", GenerateApiTask::class.java) - extensions.findByType(PublishingExtension::class.java)!!.apply { - repositories.maven { - it.name = "deployDir" - it.url = targetDirectory.toURI() - } - - publications.create("maven", MavenPublication::class.java) { - it.groupId = name - it.version = version.toString() - it.artifact(task) - } - } - } -} - -abstract class GenerateApiTask : DefaultTask() { - init { - group = "iris" - dependsOn("jar") - finalizedBy("publishMavenPublicationToDeployDirRepository") - doLast { - logger.lifecycle("The API is located at ${outputFile.absolutePath}") - } - } - - @InputFile - val inputFile: File = project.tasks - .named("jar", Jar::class.java) - .get() - .archiveFile - .get() - .asFile - - @OutputFile - val outputFile: File = project.targetDirectory.resolve(inputFile.name) - - @TaskAction - fun generate() { - JarFile(inputFile).use { jar -> - JarOutputStream(outputFile.apply { parentFile?.mkdirs() }.outputStream()).use { out -> - jar.stream() - .parallel() - .filter { !it.isDirectory } - .filter { it.name.endsWith(".class") } - .forEach { - val bytes = jar.getInputStream(it).use { input -> - val writer = ClassWriter(ClassWriter.COMPUTE_MAXS) - val visitor = MethodClearingVisitor(writer) - ClassReader(input).accept(visitor, 0) - writer.toByteArray() - } - - synchronized(out) { - out.putNextEntry(it) - out.write(bytes) - out.closeEntry() - } - } - } - } - } -} - -val Project.targetDirectory: File get() { - val dir = System.getenv("DEPLOY_DIR") ?: return project.layout.buildDirectory.dir("api").get().asFile - return File(dir) -} - -private class MethodClearingVisitor( - cv: ClassVisitor -) : ClassVisitor(Opcodes.ASM9, cv) { - - override fun visitMethod( - access: Int, - name: String?, - descriptor: String?, - signature: String?, - exceptions: Array? - ) = ExceptionThrowingMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions)) -} - -private class ExceptionThrowingMethodVisitor( - mv: MethodVisitor -) : MethodVisitor(Opcodes.ASM9, mv) { - - override fun visitCode() { - if (mv == null) return - mv.visitCode() - - mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException") - mv.visitInsn(Opcodes.DUP) - mv.visitLdcInsn("Only API") - mv.visitMethodInsn( - Opcodes.INVOKESPECIAL, - "java/lang/IllegalStateException", - "", "(Ljava/lang/String;)V", false - ) - mv.visitInsn(Opcodes.ATHROW) - - mv.visitMaxs(0, 0) - mv.visitEnd() - } -} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/NMSBinding.kt b/buildSrc/src/main/kotlin/NMSBinding.kt deleted file mode 100644 index 937f98dde..000000000 --- a/buildSrc/src/main/kotlin/NMSBinding.kt +++ /dev/null @@ -1,182 +0,0 @@ -import NMSBinding.Type -import com.volmit.nmstools.NMSToolsExtension -import com.volmit.nmstools.NMSToolsPlugin -import io.papermc.paperweight.userdev.PaperweightUser -import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension -import io.papermc.paperweight.userdev.PaperweightUserExtension -import io.papermc.paperweight.userdev.attribute.Obfuscation -import io.papermc.paperweight.util.constants.REOBF_CONFIG -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.gradle.api.* -import org.gradle.api.attributes.Bundling -import org.gradle.api.attributes.Category -import org.gradle.api.attributes.LibraryElements -import org.gradle.api.attributes.Usage -import org.gradle.api.model.ObjectFactory -import org.gradle.api.plugins.JavaPluginExtension -import org.gradle.api.tasks.TaskAction -import org.gradle.internal.extensions.core.extra -import org.gradle.jvm.toolchain.JavaLanguageVersion -import org.gradle.jvm.toolchain.JavaToolchainService -import org.gradle.work.DisableCachingByDefault -import java.io.RandomAccessFile -import javax.inject.Inject - -class NMSBinding : Plugin { - override fun apply(target: Project): Unit = with(target) { - val config = extra["nms"] as? Config ?: throw GradleException("No NMS binding configuration found") - val jvm = config.jvm - val type = config.type - - if (type == Type.USER_DEV) { - plugins.apply(PaperweightUser::class.java) - dependencies.extensions.findByType(PaperweightUserDependenciesExtension::class.java) - ?.paperDevBundle(config.version) - - val java = extensions.findByType(JavaPluginExtension::class.java) ?: throw GradleException("Java plugin not found") - java.toolchain.languageVersion.set(JavaLanguageVersion.of(jvm)) - - val javaToolchains = project.extensions.getByType(JavaToolchainService::class.java) ?: throw GradleException("Java toolchain service not found") - extensions.configure(PaperweightUserExtension::class.java) { - it.javaLauncher.set(javaToolchains.launcherFor(java.toolchain)) - } - } else { - extra["nmsTools.useBuildTools"] = type == Type.BUILD_TOOLS - plugins.apply(NMSToolsPlugin::class.java) - extensions.configure(NMSToolsExtension::class.java) { - it.jvm.set(jvm) - it.version.set(config.version) - } - - configurations.register(REOBF_CONFIG) { conf -> - conf.isCanBeConsumed = true - conf.isCanBeResolved = false - conf.attributes { - it.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) - it.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) - it.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR)) - it.attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) - it.attribute(Obfuscation.OBFUSCATION_ATTRIBUTE, objects.named(Obfuscation.OBFUSCATED)) - } - conf.outgoing.artifact(tasks.named("remap")) - } - } - - val (major, minor) = config.version.parseVersion() - if (major <= 20 && minor <= 4) return@with - tasks.register("convert", ConversionTask::class.java, type) - tasks.named("compileJava") { it.dependsOn("convert") } - rootProject.tasks.named("prepareKotlinBuildScriptModel") { it.dependsOn("$path:convert") } - } - - @DisableCachingByDefault - abstract class ConversionTask @Inject constructor(type: Type) : DefaultTask() { - private val pattern: Regex - private val replacement: String - - init { - group = "nms" - inputs.property("type", type) - val java = project.extensions.findByType(JavaPluginExtension::class.java) ?: throw GradleException("Java plugin not found") - val source = java.sourceSets.named("main").map { it.allJava } - inputs.files(source) - outputs.files(source) - - if (type == Type.USER_DEV) { - pattern = "org\\.bukkit\\.craftbukkit\\.${project.name}".toRegex() - replacement = "org.bukkit.craftbukkit" - } else { - pattern = "org\\.bukkit\\.craftbukkit\\.(?!${project.name})".toRegex() - replacement = "org.bukkit.craftbukkit.${project.name}." - } - } - - @TaskAction - fun process() { - val dispatcher = Dispatchers.IO.limitedParallelism(16) - runBlocking { - for (file in inputs.files) { - if (file.extension !in listOf("java")) - continue - - launch(dispatcher) { - val output = ArrayList() - var changed = false - - file.bufferedReader().use { - for (line in it.lines()) { - if (line.startsWith("package") || line.isBlank()) { - output += line - continue - } - - if (!line.startsWith("import")) { - if (!changed) return@launch - else { - output += line - continue - } - } - - if (!line.contains(pattern)) { - output += line - continue - } - - output += line.replace(pattern, replacement) - changed = true - } - } - - if (changed) { - RandomAccessFile(file, "r").use { raf -> - val bytes = ByteArray(NEW_LINE_BYTES.size) - raf.seek(raf.length() - bytes.size) - raf.readFully(bytes) - if (bytes.contentEquals(NEW_LINE_BYTES)) - output += "" - } - - file.writer().use { - val iterator = output.iterator() - while (iterator.hasNext()) { - it.append(iterator.next()) - if (iterator.hasNext()) - it.append(NEW_LINE) - } - } - } - } - } - } - } - } - - enum class Type { - USER_DEV, - BUILD_TOOLS, - DIRECT, - } -} - -private val NEW_LINE = System.lineSeparator() -private val NEW_LINE_BYTES = NEW_LINE.encodeToByteArray() -private fun String.parseVersion() = substringBefore('-').split(".").let { - it[1].toInt() to it[2].toInt() -} - -class Config( - var jvm: Int = 21, - var type: Type = Type.DIRECT -) { - lateinit var version: String -} - -fun Project.nmsBinding(action: Config.() -> Unit) { - extra["nms"] = Config().apply(action) - plugins.apply(NMSBinding::class.java) -} - -private inline fun ObjectFactory.named(name: String): T = named(T::class.java, name) \ No newline at end of file diff --git a/buildSrc/src/test/java/BukkitArtifactVerifierTest.java b/buildSrc/src/test/java/BukkitArtifactVerifierTest.java new file mode 100644 index 000000000..bb25a5e39 --- /dev/null +++ b/buildSrc/src/test/java/BukkitArtifactVerifierTest.java @@ -0,0 +1,209 @@ +import org.gradle.api.GradleException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class BukkitArtifactVerifierTest { + private static final String PLUGIN_DESCRIPTOR = "plugin.yml"; + private static final String SLIMJAR_DEPENDENCIES = "slimjar.dat"; + private static final String SLIMJAR_RESOLUTIONS = "slimjar-resolutions.dat"; + private static final String NMS_BINDING = "art/arcane/iris/core/nms/v26_2_R1/NMSBinding"; + private static final List REQUIRED_ENTRIES = List.of( + PLUGIN_DESCRIPTOR, + SLIMJAR_DEPENDENCIES, + SLIMJAR_RESOLUTIONS, + NMS_BINDING + ".class" + ); + private static final int LOCALES = 2; + private static final int CAFFEINE_FACTORIES = 2; + private static final int MATTER_SLICES = 1; + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void acceptsCompleteArtifact() throws Exception { + File artifact = createArtifact(validEntries()); + + BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES); + } + + @Test + public void rejectsMissingRequiredEntry() throws Exception { + Map entries = validEntries(); + entries.remove(PLUGIN_DESCRIPTOR); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, + MATTER_SLICES)); + assertTrue(failure.getMessage().contains(PLUGIN_DESCRIPTOR)); + } + + @Test + public void rejectsMissingSlimJarDependencyManifest() throws Exception { + Map entries = validEntries(); + entries.remove(SLIMJAR_DEPENDENCIES); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, + MATTER_SLICES)); + assertTrue(failure.getMessage().contains(SLIMJAR_DEPENDENCIES)); + } + + @Test + public void rejectsMissingSlimJarResolutionManifest() throws Exception { + Map entries = validEntries(); + entries.remove(SLIMJAR_RESOLUTIONS); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, + MATTER_SLICES)); + assertTrue(failure.getMessage().contains(SLIMJAR_RESOLUTIONS)); + } + + @Test + public void rejectsExcludedClassThatIsStillReferenced() throws Exception { + Map entries = validEntries(); + entries.remove("art/arcane/volmlib/util/noise/CNG.class"); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, + MATTER_SLICES)); + assertTrue(failure.getMessage().contains("art/arcane/volmlib/util/noise/CNG")); + } + + @Test + public void acceptsReferenceToRuntimeDownloadedLibrary() throws Exception { + Map entries = validEntries(); + entries.put("art/arcane/iris/Consumer.class", + classReferencing("art/arcane/iris/Consumer", "art/arcane/iris/util/kyori/adventure/text/Component")); + File artifact = createArtifact(entries); + + BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES); + } + + @Test + public void rejectsStrippedCaffeineFactories() throws Exception { + Map entries = validEntries(); + entries.remove("art/arcane/iris/util/caffeine/cache/SSMS.class"); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, + MATTER_SLICES)); + assertTrue(failure.getMessage().contains("generated Caffeine cache classes")); + } + + @Test + public void rejectsDroppedLocale() throws Exception { + Map entries = validEntries(); + entries.remove("languages/de_DE.json"); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, + MATTER_SLICES)); + assertTrue(failure.getMessage().contains("locale files")); + } + + @Test + public void rejectsDroppedMatterSlice() throws Exception { + Map entries = validEntries(); + entries.remove("art/arcane/volmlib/util/matter/slices/BlockMatter.class"); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, + MATTER_SLICES)); + assertTrue(failure.getMessage().contains("Matter slice types")); + } + + @Test + public void ignoresAnnotationOnlyReferences() throws Exception { + Map entries = validEntries(); + entries.put("art/arcane/iris/Annotated.class", + classAnnotatedWith("art/arcane/iris/Annotated", "com/google/errorprone/annotations/CanIgnoreReturnValue")); + File artifact = createArtifact(entries); + + BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES); + } + + private Map validEntries() { + Map entries = new LinkedHashMap<>(); + entries.put(PLUGIN_DESCRIPTOR, "name: Iris\n".getBytes(StandardCharsets.UTF_8)); + entries.put(SLIMJAR_DEPENDENCIES, new byte[]{1}); + entries.put(SLIMJAR_RESOLUTIONS, new byte[]{1}); + entries.put("languages/de_DE.json", "{}".getBytes(StandardCharsets.UTF_8)); + entries.put("languages/fr_FR.json", "{}".getBytes(StandardCharsets.UTF_8)); + entries.put(NMS_BINDING + ".class", + classReferencing(NMS_BINDING, "art/arcane/volmlib/util/noise/CNG")); + entries.put("art/arcane/volmlib/util/noise/CNG.class", emptyClass("art/arcane/volmlib/util/noise/CNG")); + entries.put("art/arcane/volmlib/util/matter/slices/BlockMatter.class", + emptyClass("art/arcane/volmlib/util/matter/slices/BlockMatter")); + entries.put("art/arcane/iris/util/caffeine/cache/SSMS.class", + emptyClass("art/arcane/iris/util/caffeine/cache/SSMS")); + entries.put("art/arcane/iris/util/caffeine/cache/SSLMS.class", + emptyClass("art/arcane/iris/util/caffeine/cache/SSLMS")); + return entries; + } + + private static byte[] emptyClass(String internalName) { + ClassWriter writer = new ClassWriter(0); + writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null); + writer.visitEnd(); + return writer.toByteArray(); + } + + private static byte[] classReferencing(String internalName, String referenced) { + ClassWriter writer = new ClassWriter(0); + writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null); + MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "use", "()V", null, null); + method.visitCode(); + method.visitTypeInsn(Opcodes.NEW, referenced); + method.visitInsn(Opcodes.POP); + method.visitInsn(Opcodes.RETURN); + method.visitMaxs(1, 0); + method.visitEnd(); + writer.visitEnd(); + return writer.toByteArray(); + } + + private static byte[] classAnnotatedWith(String internalName, String annotation) { + ClassWriter writer = new ClassWriter(0); + writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null); + writer.visitAnnotation('L' + annotation + ';', true).visitEnd(); + writer.visitEnd(); + return writer.toByteArray(); + } + + private File createArtifact(Map entries) throws Exception { + File artifact = temporaryFolder.newFile("Iris-bukkit-" + System.nanoTime() + ".jar"); + try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(artifact))) { + for (Map.Entry entry : entries.entrySet()) { + jar.putNextEntry(new JarEntry(entry.getKey())); + jar.write(entry.getValue()); + jar.closeEntry(); + } + } + return artifact; + } +} diff --git a/buildSrc/src/test/java/ModdedArtifactVerifierTest.java b/buildSrc/src/test/java/ModdedArtifactVerifierTest.java new file mode 100644 index 000000000..42f4ec7ae --- /dev/null +++ b/buildSrc/src/test/java/ModdedArtifactVerifierTest.java @@ -0,0 +1,345 @@ +import org.gradle.api.GradleException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ModdedArtifactVerifierTest { + private static final String METADATA = "fabric.mod.json"; + private static final String CODEC_CLASS = "art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class"; + private static final String HARDWARE_CLASS = "art/arcane/iris/util/common/misc/getHardware.class"; + private static final String MIXIN_CONFIG = "irisworldgen.entity.mixins.json"; + private static final String CLIENT_MIXIN_CONFIG = "irisworldgen.client.mixins.json"; + private static final String NEOFORGE_METADATA = "META-INF/neoforge.mods.toml"; + private static final List REQUIRED_ENTRIES = List.of(METADATA, CODEC_CLASS, HARDWARE_CLASS); + private static final byte[] INTERNAL_CODEC = ( + "net/jpountz/lz4/LZ4BlockInputStream net/jpountz/lz4/LZ4BlockOutputStream" + ).getBytes(StandardCharsets.ISO_8859_1); + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void acceptsMinecraftRuntimeReferences() throws Exception { + Map entries = validEntries(); + File artifact = createArtifact(entries); + + ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES); + } + + @Test + public void rejectsRelocatedRuntimeReference() throws Exception { + Map entries = validEntries(); + entries.put("art/arcane/iris/Test.class", + "art/arcane/iris/shadow/jpountz/lz4/LZ4BlockInputStream" + .getBytes(StandardCharsets.ISO_8859_1)); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("Iris-private runtime references")); + } + + @Test + public void rejectsBundledMinecraftRuntimeLibrary() throws Exception { + Map entries = validEntries(); + entries.put("net/jpountz/lz4/LZ4BlockInputStream.class", new byte[]{0}); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries")); + } + + @Test + public void rejectsNestedMinecraftRuntimeLibrary() throws Exception { + Map entries = validEntries(); + entries.put("META-INF/jars/lz4-java.jar", new byte[]{0}); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries")); + } + + @Test + public void rejectsRenamedJarJarRuntimeLibrary() throws Exception { + Map entries = validEntries(); + entries.put("META-INF/jarjar/runtime-support.jar", createNestedArtifact(Map.of( + "com/sun/jna/Native.class", new byte[]{0} + ))); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries")); + } + + @Test + public void rejectsArtifactWithoutPlatformMetadata() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put(CODEC_CLASS, INTERNAL_CODEC); + entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1)); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("is missing " + METADATA)); + } + + @Test + public void rejectsUnrelocatedAsm() throws Exception { + Map entries = validEntries(); + entries.put("org/objectweb/asm/ClassReader.class", new byte[]{0}); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("ships unrelocated ASM")); + } + + @Test + public void rejectsNestedJarMissingFromMetadata() throws Exception { + Map entries = validEntries(); + entries.put("META-INF/jars/fabric-rendering-v1.jar", createNestedArtifact(Map.of( + "net/fabricmc/Placeholder.class", new byte[]{0} + ))); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("does not declare")); + } + + @Test + public void rejectsDeclaredNestedJarThatIsNotShipped() throws Exception { + Map entries = validEntries(); + entries.put(METADATA, fabricMetadata( + "[ { \"file\": \"META-INF/jars/fabric-api-base.jar\" } ]", + "[ \"" + MIXIN_CONFIG + "\" ]")); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("does not ship")); + } + + @Test + public void acceptsNestedJarDeclaredInMetadata() throws Exception { + Map entries = validEntries(); + entries.put(METADATA, fabricMetadata( + "[ { \"file\": \"META-INF/jars/fabric-api-base.jar\" } ]", + "[ \"" + MIXIN_CONFIG + "\" ]")); + entries.put("META-INF/jars/fabric-api-base.jar", createNestedArtifact(Map.of( + "net/fabricmc/Placeholder.class", new byte[]{0} + ))); + File artifact = createArtifact(entries); + + ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES); + } + + @Test + public void rejectsMixinCompatibilityLevelAboveJava21() throws Exception { + Map entries = validEntries(); + entries.put(MIXIN_CONFIG, mixinConfig("JAVA_25")); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("compatibilityLevel JAVA_25")); + } + + @Test + public void rejectsMixinCompatibilityLevelAboveJava21RegisteredByManifest() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put("META-INF/mods.toml", "modId = \"irisworldgen\"".getBytes(StandardCharsets.UTF_8)); + entries.put(CODEC_CLASS, INTERNAL_CODEC); + entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1)); + entries.put(CLIENT_MIXIN_CONFIG, mixinConfig("JAVA_25")); + File artifact = createArtifact(entries, manifestWithMixinConfigs(CLIENT_MIXIN_CONFIG)); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, List.of(CODEC_CLASS, HARDWARE_CLASS))); + assertTrue(failure.getMessage().contains("compatibilityLevel JAVA_25")); + } + + @Test + public void rejectsMixinCompatibilityLevelAboveJava21RegisteredByNeoforgeToml() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put(NEOFORGE_METADATA, ("[[mixins]]\nconfig = \"" + CLIENT_MIXIN_CONFIG + "\"\n") + .getBytes(StandardCharsets.UTF_8)); + entries.put(CODEC_CLASS, INTERNAL_CODEC); + entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1)); + entries.put(CLIENT_MIXIN_CONFIG, mixinConfig("JAVA_25")); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, List.of(CODEC_CLASS, HARDWARE_CLASS))); + assertTrue(failure.getMessage().contains("compatibilityLevel JAVA_25")); + } + + @Test + public void rejectsRegisteredMixinConfigThatIsNotShipped() throws Exception { + Map entries = validEntries(); + entries.remove(MIXIN_CONFIG); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("which is not in the jar")); + } + + @Test + public void rejectsArtifactWithoutMixinRegistration() throws Exception { + Map entries = validEntries(); + entries.put(METADATA, fabricMetadata("[ ]", "[ ]")); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("registers no mixin configs")); + } + + @Test + public void rejectsBukkitSupertypeOutsidePlatformPackage() throws Exception { + Map entries = validEntries(); + entries.put("art/arcane/iris/engine/platform/BukkitChunkGenerator.class", + classExtending("art/arcane/iris/engine/platform/BukkitChunkGenerator", + "org/bukkit/generator/ChunkGenerator")); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("org.bukkit supertype")); + } + + @Test + public void rejectsBukkitInterfaceOutsidePlatformPackage() throws Exception { + Map entries = validEntries(); + entries.put("art/arcane/iris/Listener.class", + classImplementing("art/arcane/iris/Listener", "org/bukkit/event/Listener")); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); + assertTrue(failure.getMessage().contains("org.bukkit supertype")); + } + + @Test + public void acceptsBukkitSupertypeInsidePlatformPackage() throws Exception { + Map entries = validEntries(); + entries.put("art/arcane/iris/platform/bukkit/BukkitWorld.class", + classImplementing("art/arcane/iris/platform/bukkit/BukkitWorld", "org/bukkit/event/Listener")); + File artifact = createArtifact(entries); + + ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES); + } + + @Test + public void acceptsBaselinedBukkitSupertype() throws Exception { + String entryName = "art/arcane/iris/engine/platform/BukkitChunkGenerator.class"; + Map entries = validEntries(); + entries.put(entryName, classExtending("art/arcane/iris/engine/platform/BukkitChunkGenerator", + "org/bukkit/generator/ChunkGenerator")); + File artifact = createArtifact(entries); + + ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, Set.of(entryName)); + } + + private Map validEntries() { + Map entries = new LinkedHashMap<>(); + entries.put(METADATA, fabricMetadata("[ ]", "[ \"" + MIXIN_CONFIG + "\" ]")); + entries.put(MIXIN_CONFIG, mixinConfig("JAVA_21")); + entries.put(CODEC_CLASS, INTERNAL_CODEC); + entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1)); + return entries; + } + + private byte[] fabricMetadata(String jarsArray, String mixinsArray) { + return ("{\n" + + " \"schemaVersion\": 1,\n" + + " \"id\": \"irisworldgen\",\n" + + " \"mixins\": " + mixinsArray + ",\n" + + " \"jars\": " + jarsArray + "\n" + + "}\n").getBytes(StandardCharsets.UTF_8); + } + + private byte[] mixinConfig(String compatibilityLevel) { + return ("{\n" + + " \"required\": true,\n" + + " \"minVersion\": \"0.8\",\n" + + " \"package\": \"art.arcane.iris.modded.mixin\",\n" + + " \"compatibilityLevel\": \"" + compatibilityLevel + "\"\n" + + "}\n").getBytes(StandardCharsets.UTF_8); + } + + private Manifest manifestWithMixinConfigs(String configs) { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("MixinConfigs", configs); + return manifest; + } + + private byte[] classExtending(String internalName, String superName) { + ClassWriter writer = new ClassWriter(0); + writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, superName, null); + writer.visitEnd(); + return writer.toByteArray(); + } + + private byte[] classImplementing(String internalName, String interfaceName) { + ClassWriter writer = new ClassWriter(0); + writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", + new String[]{interfaceName}); + writer.visitEnd(); + return writer.toByteArray(); + } + + private File createArtifact(Map entries) throws Exception { + return createArtifact(entries, null); + } + + private File createArtifact(Map entries, Manifest manifest) throws Exception { + File artifact = temporaryFolder.newFile("artifact-" + System.nanoTime() + ".jar"); + try (FileOutputStream file = new FileOutputStream(artifact); + JarOutputStream output = manifest == null + ? new JarOutputStream(file) + : new JarOutputStream(file, manifest)) { + for (Map.Entry entry : entries.entrySet()) { + output.putNextEntry(new JarEntry(entry.getKey())); + output.write(entry.getValue()); + output.closeEntry(); + } + } + return artifact; + } + + private byte[] createNestedArtifact(Map entries) throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (JarOutputStream output = new JarOutputStream(bytes)) { + for (Map.Entry entry : entries.entrySet()) { + output.putNextEntry(new JarEntry(entry.getKey())); + output.write(entry.getValue()); + output.closeEntry(); + } + } + return bytes.toByteArray(); + } +} diff --git a/core/agent/build.gradle b/core/agent/build.gradle new file mode 100644 index 000000000..baff85547 --- /dev/null +++ b/core/agent/build.gradle @@ -0,0 +1,14 @@ +import org.gradle.jvm.tasks.Jar + +plugins { + id 'java' +} + +tasks.named('jar', Jar).configure { + manifest.attributes( + 'Agent-Class': 'art.arcane.iris.util.project.agent.Installer', + 'Premain-Class': 'art.arcane.iris.util.project.agent.Installer', + 'Can-Redefine-Classes': true, + 'Can-Retransform-Classes': true + ) +} diff --git a/core/agent/build.gradle.kts b/core/agent/build.gradle.kts deleted file mode 100644 index a0d8024df..000000000 --- a/core/agent/build.gradle.kts +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - java -} - -tasks.jar { - manifest.attributes( - "Agent-Class" to "com.volmit.iris.util.agent.Installer", - "Premain-Class" to "com.volmit.iris.util.agent.Installer", - "Can-Redefine-Classes" to true, - "Can-Retransform-Classes" to true - ) -} \ No newline at end of file diff --git a/core/agent/src/main/java/com/volmit/iris/util/agent/Installer.java b/core/agent/src/main/java/art/arcane/iris/util/agent/Installer.java similarity index 95% rename from core/agent/src/main/java/com/volmit/iris/util/agent/Installer.java rename to core/agent/src/main/java/art/arcane/iris/util/agent/Installer.java index 3c68fd579..d88085758 100644 --- a/core/agent/src/main/java/com/volmit/iris/util/agent/Installer.java +++ b/core/agent/src/main/java/art/arcane/iris/util/agent/Installer.java @@ -1,4 +1,4 @@ -package com.volmit.iris.util.agent; +package art.arcane.iris.util.project.agent; import java.lang.instrument.Instrumentation; diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 000000000..855a3b7d9 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,390 @@ +import io.github.slimjar.resolver.data.Mirror +import org.gradle.api.Task +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.jvm.tasks.Jar +import org.gradle.jvm.toolchain.JavaLanguageVersion + +import java.net.URI +import java.nio.charset.StandardCharsets + +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2021 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +plugins { + id 'java' + id 'java-library' + alias(libs.plugins.shadow) + alias(libs.plugins.sentry) + alias(libs.plugins.slimjar) +} + +def lib = 'art.arcane.iris.util' +String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') + .orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186') + .get() +String sentryAuthToken = findProperty('sentry.auth.token') as String ?: System.getenv('SENTRY_AUTH_TOKEN') +boolean hasSentryAuthToken = sentryAuthToken != null && !sentryAuthToken.isBlank() + +/** + * Dependencies. + * + * Provided or classpath dependencies are not shaded and are available on the runtime classpath + * + * Shaded dependencies are not available at runtime, nor are they available on mvn central so they + * need to be shaded into the jar (increasing binary size) + * + * Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the + * plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare + * these dependencies if they are available on mvn central. + */ +dependencies { + api(project(':spi')) + + // Provided or Classpath + compileOnly(libs.lombok) + annotationProcessor(libs.lombok) + compileOnly(libs.paper.api) + compileOnly(libs.log4j.api) + compileOnly(libs.log4j.core) + compileOnly(libs.guava) + + // Third Party Integrations + compileOnly(libs.nexo) + compileOnly(libs.itemsadder) + compileOnly(libs.score) + compileOnly(libs.mmoitems) + compileOnly(libs.mythiclib) + compileOnly(libs.eco) + compileOnly(libs.mythic) + compileOnly(libs.mythicCrucible) + compileOnly(libs.kgenerators) { + transitive = false + } + compileOnly(libs.multiverseCore) + compileOnly(libs.craftengine.core) + compileOnly(libs.craftengine.bukkit) + + // Shaded + implementation('de.crazydev22.slimjar.helper:spigot:2.1.9') + implementation(volmLibCoordinate) { + transitive = false + } + implementation(libs.gson) + implementation(libs.lru) + implementation(libs.caffeine) + + // Dynamically Loaded + slim(libs.paralithic) + slim(libs.paperlib) + slim(libs.adventure.api) + slim(libs.adventure.minimessage) + slim(libs.adventure.platform) + slim(libs.bstats) + slim(libs.sentry) + + slim(libs.commons.io) + slim(libs.commons.lang3) + slim(libs.oshi) + slim(libs.lz4) + slim(libs.fastutil) + slim(libs.zip) { + exclude(group: 'org.slf4j', module: 'slf4j-api') + } + slim(libs.asm) + slim(libs.byteBuddy.core) + slim(libs.byteBuddy.agent) + slim(libs.dom4j) + slim(libs.jaxen) + + testImplementation('junit:junit:4.13.2') + testImplementation('org.mockito:mockito-core:5.23.0') + testImplementation(libs.paper.api) + testRuntimeOnly(libs.paper.api) + testImplementation(libs.multiverseCore) +} + +tasks.named('test').configure { + maxHeapSize = '1g' + systemProperty('iris.packBenchmarkingSource', file('src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java').absolutePath) + systemProperty('iris.irisToolbeltSource', file('src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java').absolutePath) + systemProperty('iris.irisCreatorSource', file('src/main/java/art/arcane/iris/core/tools/IrisCreator.java').absolutePath) + systemProperty('iris.bukkitChunkGeneratorSource', file('src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java').absolutePath) +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +sentry { + url = 'http://sentry.volmit.com:8080' + autoInstallation.enabled = false + includeSourceContext = true + + org = 'sentry' + projectName = 'iris' + authToken = sentryAuthToken +} + +slimJar { + mirrors = [ + new Mirror( + URI.create('https://maven-central.storage-download.googleapis.com/maven2').toURL(), + URI.create('https://repo.maven.apache.org/maven2/').toURL() + ) + ] + + relocate('com.dfsek.paralithic', "${lib}.paralithic") + relocate('io.papermc.lib', "${lib}.paper") + relocate('net.kyori', "${lib}.kyori") + relocate('org.bstats', "${lib}.metrics") + relocate('io.sentry', "${lib}.sentry") + relocate('org.apache.maven', "${lib}.maven") + relocate('org.codehaus.plexus', "${lib}.plexus") + relocate('org.eclipse.sisu', "${lib}.sisu") + relocate('org.eclipse.aether', "${lib}.aether") + relocate('com.google.inject', "${lib}.guice") + relocate('org.dom4j', "${lib}.dom4j") + relocate('org.jaxen', "${lib}.jaxen") + relocate('com.github.benmanes.caffeine', "${lib}.caffeine") +} + +def embeddedAgentJar = project(':core:agent').tasks.named('jar', Jar) +def templateSource = file('src/main/templates') +def templateDest = layout.buildDirectory.dir('generated/sources/templates') +def generateTemplates = tasks.register('generateTemplates', Copy) { + inputs.properties([ + environment: providers.provider { + if (project.hasProperty('release')) { + return 'production' + } + if (project.hasProperty('argghh')) { + return 'Argghh!' + } + return 'development' + }, + commit: providers.provider { + String commitId = null + Exception failure = null + try { + Process process = new ProcessBuilder('git', '-C', rootProject.projectDir.absolutePath, 'rev-parse', 'HEAD') + .redirectErrorStream(true) + .start() + String output = new String(process.inputStream.readAllBytes(), StandardCharsets.UTF_8).trim() + int exitCode = process.waitFor() + if (exitCode == 0) { + commitId = output + } else { + failure = new GradleException("git rev-parse exited with code ${exitCode}: ${output}") + } + } catch (Exception ex) { + failure = ex + } + + if (commitId != null && commitId.length() == 40) { + return commitId + } + + logger.error('Git commit hash not found', failure) + return 'unknown' + }, + minecraftVersion: providers.gradleProperty('minecraftVersion'), + ]) + + from(templateSource) + into(templateDest) + rename { String fileName -> "art/arcane/iris/${fileName}" } + expand(inputs.properties) +} + +tasks.named('compileJava', JavaCompile).configure { + /** + * We need parameter meta for the decree command system + */ + options.compilerArgs.add('-parameters') + options.compilerArgs.addAll(['--add-modules', 'jdk.incubator.vector']) + options.encoding = 'UTF-8' + options.debugOptions.debugLevel = 'none' + // lombok.config decides how equals/hashCode/toString are generated (fields, never getters), so editing it + // changes this module's bytecode. Gradle does not know that on its own and would serve a stale up-to-date + // build until something else in the module changed. + inputs.file(rootProject.file('lombok.config')) + .withPropertyName('lombokConfig') + .withPathSensitivity(PathSensitivity.NONE) +} + +tasks.named('test', Test).configure { + jvmArgs('--add-modules', 'jdk.incubator.vector') + classpath = files('src/main/resources') + classpath +} + +configurations.matching { it.name.startsWith('slim') }.all { } + +List requestedTasks = gradle.startParameter.taskNames +boolean runningOnlyTestTasks = !requestedTasks.isEmpty() + && requestedTasks.every { String taskName -> taskName.toLowerCase().contains('test') } +if (runningOnlyTestTasks) { + TaskProvider processResourcesTask = tasks.named('processResources') + tasks.named('classes').configure { Task classesTask -> + Set dependencies = new LinkedHashSet(classesTask.getDependsOn()) + dependencies.removeIf { Object dependency -> + if (dependency instanceof TaskProvider) { + return ((TaskProvider) dependency).name == processResourcesTask.name + } + if (dependency instanceof Task) { + return ((Task) dependency).name == processResourcesTask.name + } + String dependencyName = String.valueOf(dependency) + return dependencyName == 'processResources' || dependencyName.endsWith(':processResources') + } + classesTask.setDependsOn(dependencies) + } + processResourcesTask.configure { Task task -> + task.enabled = false + } +} + +// VolmLib packages that Iris forked into art.arcane.iris.util.project.* or replaced outright. Every +// class in each of these packages is unreachable from every other class shipped in the Bukkit jar, +// and none of them is a reflective target: Matter.read() resolves only matter/slices, JarScanner +// only walks Iris' own packages, and the remaining VolmLib Class.forName calls name Bukkit/NMS or +// runtime-downloaded types. Package granularity is deliberate - a partially used package stays whole. +// verifyBukkitArtifact re-derives the reference graph over the shipped jar and fails on any dangling +// reference, so an exclude that stops being dead is a build failure rather than a runtime crash. +List supersededVolmLibPackages = [ + 'art/arcane/volmlib/util/noise/**', + 'art/arcane/volmlib/util/stream/ProceduralStream.class', + 'art/arcane/volmlib/util/stream/BasicStream.class', + 'art/arcane/volmlib/util/stream/arithmetic/**', + 'art/arcane/volmlib/util/stream/convert/**', + 'art/arcane/volmlib/util/stream/interpolation/**', + 'art/arcane/volmlib/util/stream/sources/**', + 'art/arcane/volmlib/util/stream/utility/**', + 'art/arcane/volmlib/util/uniques/**', + 'art/arcane/volmlib/util/bukkit/json/**', + 'art/arcane/volmlib/util/bukkit/registry/**', + 'art/arcane/volmlib/util/director/visual/**', + 'art/arcane/volmlib/util/value/**', + 'art/arcane/volmlib/util/api/**', + 'art/arcane/volmlib/util/entity/**' +] + +// Annotation-only artifacts pulled in transitively by Gson and Caffeine. Their types appear solely +// in annotation attributes, which the JVM skips silently when the type is absent, so nothing loads +// or links against them at runtime. +List annotationOnlyArtifacts = [ + 'com/google/errorprone/**', + 'org/jspecify/**' +] + +// Publisher metadata for the shaded dependencies. Nothing reads these at runtime. +List dependencyBuildMetadata = [ + 'META-INF/maven/**', + 'META-INF/proguard/**', + 'META-INF/versions/*/OSGI-INF/**' +] + +tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).configure { + dependsOn(embeddedAgentJar) + mergeServiceFiles() + // minimize() stays off. Caffeine reaches its 520 generated cache and node classes through + // MethodHandles.Lookup.findClass on a computed name, and the VolmLib command, GUI and hotload + // surfaces are only referenced from :adapters:bukkit:plugin, which is merged into the artifact + // after this task runs. Both would be stripped as unreachable. Excluding them from minimization + // leaves it reaching ~25KB of Gson internals, which is not worth a mode that fails silently. + relocate('io.github.slimjar', "${lib}.slimjar") + exclude('modules/loader-agent.isolated-jar') + exclude(supersededVolmLibPackages) + exclude(annotationOnlyArtifacts) + exclude(dependencyBuildMetadata) + from(embeddedAgentJar.map { it.archiveFile }) { + rename { String ignored -> 'agent.jar' } + } +} + +tasks.named('sentryCollectSourcesJava').configure { + dependsOn(generateTemplates) +} + +tasks.named('generateSentryBundleIdJava').configure { + dependsOn(generateTemplates) +} + +tasks.matching { Task task -> + task.name.startsWith('sentry') || task.name.startsWith('generateSentry') +}.configureEach { + onlyIf { + hasSentryAuthToken + } +} + +rootProject.tasks.matching { + it.name == 'prepareKotlinBuildScriptModel' +}.configureEach { + dependsOn(generateTemplates) +} + +sourceSets { + main { + java { + srcDir(generateTemplates.map { it.outputs }) + } + } +} + +tasks.register('bukkitPurityRatchet') { + group = 'verification' + description = 'Fails if files outside core/purity-allowlist.txt import org.bukkit (the allowlist only shrinks).' + File allowlistFile = project.file('purity-allowlist.txt') + File sourceRoot = project.file('src/main/java') + inputs.file(allowlistFile) + inputs.dir(sourceRoot) + doLast { + Set allowed = allowlistFile.readLines().findAll { String line -> !line.isBlank() }.toSet() + List offenders = new ArrayList<>() + List coupled = new ArrayList<>() + sourceRoot.eachFileRecurse { File f -> + if (!f.name.endsWith('.java')) { + return + } + if (f.text.contains('org.bukkit')) { + String rel = sourceRoot.toPath().relativize(f.toPath()).toString() + coupled.add(rel) + if (!allowed.contains(rel)) { + offenders.add(rel) + } + } + } + if (!offenders.isEmpty()) { + throw new GradleException("Bukkit purity ratchet violated - new org.bukkit coupling in:\n " + offenders.join('\n ') + "\nDecouple these files or (only with strong justification) add them to core/purity-allowlist.txt") + } + int shrunk = allowed.size() - coupled.size() + if (shrunk > 0) { + println("Purity ratchet: " + coupled.size() + " coupled files (" + shrunk + " fewer than allowlist - consider tightening core/purity-allowlist.txt)") + } else { + println("Purity ratchet: " + coupled.size() + " coupled files (at allowlist ceiling)") + } + } +} + +tasks.named('check').configure { + dependsOn('bukkitPurityRatchet') +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts deleted file mode 100644 index 9f9e08c9a..000000000 --- a/core/build.gradle.kts +++ /dev/null @@ -1,234 +0,0 @@ -import io.github.slimjar.func.slimjarHelper -import io.github.slimjar.resolver.data.Mirror -import org.ajoberstar.grgit.Grgit -import org.gradle.jvm.toolchain.JavaLanguageVersion -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import java.net.URI - -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2021 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -plugins { - java - `java-library` - alias(libs.plugins.shadow) - alias(libs.plugins.sentry) - alias(libs.plugins.slimjar) - alias(libs.plugins.grgit) - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlin.lombok) -} - -val apiVersion = "1.19" -val main = "com.volmit.iris.Iris" -val lib = "com.volmit.iris.util" - -/** - * Dependencies. - * - * Provided or classpath dependencies are not shaded and are available on the runtime classpath - * - * Shaded dependencies are not available at runtime, nor are they available on mvn central so they - * need to be shaded into the jar (increasing binary size) - * - * Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the - * plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare - * these dependencies if they are available on mvn central. - */ -dependencies { - // Provided or Classpath - compileOnly(libs.spigot) - compileOnly(libs.log4j.api) - compileOnly(libs.log4j.core) - - // Third Party Integrations - compileOnly(libs.nexo) - compileOnly(libs.itemsadder) - compileOnly(libs.placeholderApi) - compileOnly(libs.score) - compileOnly(libs.mmoitems) - compileOnly(libs.ecoitems) - compileOnly(libs.mythic) - compileOnly(libs.mythicChrucible) - compileOnly(libs.kgenerators) { - isTransitive = false - } - compileOnly(libs.multiverseCore) - compileOnly(libs.craftengine.core) - compileOnly(libs.craftengine.bukkit) - //compileOnly(libs.sparrowNbt) - - // Shaded - implementation(slimjarHelper("spigot")) - - // Dynamically Loaded - slim(libs.paralithic) - slim(libs.paperlib) - slim(libs.adventure.api) - slim(libs.adventure.minimessage) - slim(libs.adventure.platform) - slim(libs.bstats) - slim(libs.sentry) - - slim(libs.commons.io) - slim(libs.commons.lang) - slim(libs.commons.lang3) - slim(libs.commons.math3) - slim(libs.oshi) - slim(libs.lz4) - slim(libs.fastutil) - slim(libs.lru) - slim(libs.zip) - slim(libs.gson) - slim(libs.asm) - slim(libs.caffeine) - slim(libs.byteBuddy.core) - slim(libs.byteBuddy.agent) - slim(libs.dom4j) - slim(libs.jaxen) - - // Script Engine - slim(libs.kotlin.stdlib) - slim(libs.kotlin.coroutines) - slim(libs.kotlin.scripting.common) - slim(libs.kotlin.scripting.jvm) - slim(libs.kotlin.scripting.jvm.host) - slim(libs.kotlin.scripting.dependencies.maven) { - constraints { - slim(libs.mavenCore) - } - } -} - -java { - disableAutoTargetJvm() - toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) - } -} - -kotlin { - jvmToolchain(21) - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } -} - -sentry { - url = "http://sentry.volmit.com:8080" - autoInstallation.enabled = false - includeSourceContext = true - - org = "sentry" - projectName = "iris" - authToken = findProperty("sentry.auth.token") as String? ?: System.getenv("SENTRY_AUTH_TOKEN") -} - -slimJar { - mirrors = listOf(Mirror( - URI.create("https://maven-central.storage-download.googleapis.com/maven2").toURL(), - URI.create("https://repo.maven.apache.org/maven2/").toURL() - )) - - relocate("com.dfsek.paralithic", "$lib.paralithic") - relocate("io.papermc.lib", "$lib.paper") - relocate("net.kyori", "$lib.kyori") - relocate("org.bstats", "$lib.metrics") - relocate("io.sentry", "$lib.sentry") - relocate("org.apache.maven", "$lib.maven") - relocate("org.codehaus.plexus", "$lib.plexus") - relocate("org.eclipse.sisu", "$lib.sisu") - relocate("org.eclipse.aether", "$lib.aether") - relocate("com.google.inject", "$lib.guice") - relocate("org.dom4j", "$lib.dom4j") - relocate("org.jaxen", "$lib.jaxen") - relocate("com.github.benmanes.caffeine", "$lib.caffeine") -} - -tasks { - /** - * We need parameter meta for the decree command system - */ - compileJava { - options.compilerArgs.add("-parameters") - options.encoding = "UTF-8" - } - - /** - * Expand properties into plugin yml - */ - processResources { - inputs.properties( - "name" to rootProject.name, - "version" to rootProject.version, - "apiVersion" to apiVersion, - "main" to main, - ) - filesMatching("**/plugin.yml") { - expand(inputs.properties) - } - } - - shadowJar { - mergeServiceFiles() - //minimize() - relocate("io.github.slimjar", "$lib.slimjar") - exclude("modules/loader-agent.isolated-jar") - } - - sentryCollectSourcesJava { - dependsOn(generateTemplates) - } -} - -val templateSource = file("src/main/templates") -val templateDest = layout.buildDirectory.dir("generated/sources/templates")!! -val generateTemplates = tasks.register("generateTemplates") { - inputs.properties( - "environment" to when { - project.hasProperty("release") -> "production" - project.hasProperty("argghh") -> "Argghh!" - else -> "development" - }, - "commit" to provider { - val res = runCatching { project.extensions.getByType().head().id } - res.getOrDefault("") - .takeIf { it.length == 40 } ?: run { - this.logger.error("Git commit hash not found", res.exceptionOrNull()) - "unknown" - } - }, - ) - - from(templateSource) - into(templateDest) - rename { "com/volmit/iris/$it" } - expand(inputs.properties) -} - -tasks.generateSentryBundleIdJava { - dependsOn(generateTemplates) -} - -rootProject.tasks.named("prepareKotlinBuildScriptModel") { - dependsOn(generateTemplates) -} - -sourceSets.main { - java.srcDir(generateTemplates.map { it.outputs }) -} diff --git a/core/plugins/Iris/cache/instance b/core/plugins/Iris/cache/instance new file mode 100644 index 000000000..38fa149ac --- /dev/null +++ b/core/plugins/Iris/cache/instance @@ -0,0 +1 @@ +931318337 \ No newline at end of file diff --git a/core/purity-allowlist.txt b/core/purity-allowlist.txt new file mode 100644 index 000000000..31a622766 --- /dev/null +++ b/core/purity-allowlist.txt @@ -0,0 +1,191 @@ +art/arcane/iris/core/IrisRuntimeSchedulerMode.java +art/arcane/iris/core/IrisWorldStorage.java +art/arcane/iris/core/IrisWorlds.java +art/arcane/iris/core/ServerConfigurator.java +art/arcane/iris/core/WorldCreatorCompat.java +art/arcane/iris/core/WorldRemovalPathPolicy.java +art/arcane/iris/core/datapack/DatapackIngestService.java +art/arcane/iris/core/edit/BlockSignal.java +art/arcane/iris/core/edit/DustRevealer.java +art/arcane/iris/core/events/IrisEngineEvent.java +art/arcane/iris/core/events/IrisEngineHotloadEvent.java +art/arcane/iris/core/events/IrisLootEvent.java +art/arcane/iris/core/lifecycle/BukkitWorldConfiguration.java +art/arcane/iris/core/lifecycle/BukkitPublicBackend.java +art/arcane/iris/core/lifecycle/CapabilitySnapshot.java +art/arcane/iris/core/lifecycle/IrisWorldRemovalService.java +art/arcane/iris/core/lifecycle/PaperLibBootstrap.java +art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java +art/arcane/iris/core/lifecycle/WorldLifecycleBackend.java +art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java +art/arcane/iris/core/lifecycle/WorldLifecycleService.java +art/arcane/iris/core/lifecycle/WorldLifecycleStaging.java +art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java +art/arcane/iris/core/lifecycle/WorldsProviderBackend.java +art/arcane/iris/core/link/ExternalDataProvider.java +art/arcane/iris/core/link/MultiverseCoreLink.java +art/arcane/iris/core/link/MultiverseGuardListener.java +art/arcane/iris/core/link/WorldEditLink.java +art/arcane/iris/core/link/data/CraftEngineDataProvider.java +art/arcane/iris/core/link/data/EcoItemsDataProvider.java +art/arcane/iris/core/link/data/ExecutableItemsDataProvider.java +art/arcane/iris/core/link/data/ItemAdderDataProvider.java +art/arcane/iris/core/link/data/KGeneratorsDataProvider.java +art/arcane/iris/core/link/data/MMOItemsDataProvider.java +art/arcane/iris/core/link/data/MythicCrucibleDataProvider.java +art/arcane/iris/core/link/data/MythicMobsDataProvider.java +art/arcane/iris/core/link/data/NexoDataProvider.java +art/arcane/iris/core/nms/INMS.java +art/arcane/iris/core/nms/INMSBinding.java +art/arcane/iris/core/nms/MinecraftVersion.java +art/arcane/iris/core/nms/v1X/NMSBinding1X.java +art/arcane/iris/core/pack/IrisPack.java +art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java +art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +art/arcane/iris/core/pregenerator/methods/HybridPregenMethod.java +art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java +art/arcane/iris/core/project/IrisProject.java +art/arcane/iris/core/project/StudioOpenProgressReporter.java +art/arcane/iris/core/runtime/BukkitPublicRuntimeControlBackend.java +art/arcane/iris/core/runtime/ChunkClearer.java +art/arcane/iris/core/runtime/ChunkJobReporter.java +art/arcane/iris/core/runtime/GoldenHashScanner.java +art/arcane/iris/core/runtime/InPlaceChunkRegenerator.java +art/arcane/iris/core/runtime/PaperLikeRuntimeControlBackend.java +art/arcane/iris/core/runtime/StudioOpenCoordinator.java +art/arcane/iris/core/runtime/WorldRuntimeControlBackend.java +art/arcane/iris/core/runtime/WorldRuntimeControlService.java +art/arcane/iris/core/safeguard/Mode.java +art/arcane/iris/core/safeguard/task/Tasks.java +art/arcane/iris/core/service/BoardSVC.java +art/arcane/iris/core/service/EntityRiseSVC.java +art/arcane/iris/core/service/ExternalDataSVC.java +art/arcane/iris/core/service/GlobalCacheSVC.java +art/arcane/iris/core/service/JigsawStudioMarkerParser.java +art/arcane/iris/core/service/JigsawStudioMenuController.java +art/arcane/iris/core/service/JigsawStudioPreviewRenderer.java +art/arcane/iris/core/service/JigsawStudioService.java +art/arcane/iris/core/service/JigsawStudioToolCodec.java +art/arcane/iris/core/service/MultiverseSVC.java +art/arcane/iris/core/service/ObjectSVC.java +art/arcane/iris/core/service/ObjectStudioSaveService.java +art/arcane/iris/core/service/PackDownloadProgressReporter.java +art/arcane/iris/core/service/StudioSVC.java +art/arcane/iris/core/service/TreeSVC.java +art/arcane/iris/core/structure/BulkStructureImporter.java +art/arcane/iris/core/structure/FeatureImporter.java +art/arcane/iris/core/structure/StructureCaptureImporter.java +art/arcane/iris/core/structure/StructureImporter.java +art/arcane/iris/core/structure/VillageImporter.java +art/arcane/iris/core/tools/IrisConverter.java +art/arcane/iris/core/tools/IrisCreator.java +art/arcane/iris/core/tools/IrisPackBenchmarking.java +art/arcane/iris/core/tools/IrisReflectiveAPI.java +art/arcane/iris/core/tools/IrisToolbelt.java +art/arcane/iris/core/tools/IrisWorldCreator.java +art/arcane/iris/core/tools/WorldCreationProgressReporter.java +art/arcane/iris/engine/IrisEngineEffects.java +art/arcane/iris/engine/IrisWorldManager.java +art/arcane/iris/engine/MarkerSpawnScanner.java +art/arcane/iris/engine/WorldBlockDropRouter.java +art/arcane/iris/engine/WorldChunkMaintenance.java +art/arcane/iris/engine/WorldEntitySpawner.java +art/arcane/iris/engine/WorldTeleportWarmup.java +art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java +art/arcane/iris/engine/data/chunk/TerrainChunk.java +art/arcane/iris/engine/decorator/DecoratorCore.java +art/arcane/iris/engine/framework/BukkitEngineWorldManager.java +art/arcane/iris/engine/framework/EngineAssignedWorldManager.java +art/arcane/iris/engine/framework/EnginePlayer.java +art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java +art/arcane/iris/engine/mantle/MantleWriter.java +art/arcane/iris/engine/object/BlockDataMergeSupport.java +art/arcane/iris/engine/object/IPostBlockAccess.java +art/arcane/iris/engine/object/IrisAttributeModifier.java +art/arcane/iris/engine/object/IrisBiome.java +art/arcane/iris/engine/object/IrisBiomeCustomParticle.java +art/arcane/iris/engine/object/IrisBiomeDerivatives.java +art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java +art/arcane/iris/engine/object/IrisBlockDrops.java +art/arcane/iris/engine/object/IrisCommandRegistry.java +art/arcane/iris/engine/object/IrisCompat.java +art/arcane/iris/engine/object/IrisCompatabilityBlockFilter.java +art/arcane/iris/engine/object/IrisCompatabilityItemFilter.java +art/arcane/iris/engine/object/IrisDirection.java +art/arcane/iris/engine/object/IrisEffect.java +art/arcane/iris/engine/object/IrisEnchantment.java +art/arcane/iris/engine/object/IrisEntity.java +art/arcane/iris/engine/object/IrisEntitySpawn.java +art/arcane/iris/engine/object/IrisLoot.java +art/arcane/iris/engine/object/IrisLootTable.java +art/arcane/iris/engine/object/IrisObject.java +art/arcane/iris/engine/object/IrisObjectPlacement.java +art/arcane/iris/engine/object/IrisObjectRotation.java +art/arcane/iris/engine/object/IrisPotionEffect.java +art/arcane/iris/engine/object/IrisSurface.java +art/arcane/iris/engine/object/IrisTree.java +art/arcane/iris/engine/object/IrisVanillaLootTable.java +art/arcane/iris/engine/object/LegacyTileData.java +art/arcane/iris/engine/object/PotionEffectTypes.java +art/arcane/iris/engine/object/TileData.java +art/arcane/iris/engine/platform/BukkitChunkGenerator.java +art/arcane/iris/engine/platform/DummyBiomeProvider.java +art/arcane/iris/engine/platform/DummyChunkGenerator.java +art/arcane/iris/engine/platform/EngineBukkitOps.java +art/arcane/iris/engine/platform/studio/generators/JigsawStudioGenerator.java +art/arcane/iris/engine/platform/studio/generators/ObjectStudioGenerator.java +art/arcane/iris/platform/bukkit/BukkitBiome.java +art/arcane/iris/platform/bukkit/BukkitBlockResolution.java +art/arcane/iris/platform/bukkit/BukkitBlockState.java +art/arcane/iris/platform/bukkit/BukkitEntityType.java +art/arcane/iris/platform/bukkit/BukkitEnvironment.java +art/arcane/iris/platform/bukkit/BukkitItem.java +art/arcane/iris/platform/bukkit/BukkitPlatform.java +art/arcane/iris/platform/bukkit/BukkitRegistries.java +art/arcane/iris/platform/bukkit/BukkitScheduler.java +art/arcane/iris/platform/bukkit/BukkitWorld.java +art/arcane/iris/platform/bukkit/BukkitWorldBinding.java +art/arcane/iris/util/common/data/IrisCustomData.java +art/arcane/iris/util/common/data/registry/Attributes.java +art/arcane/iris/util/common/data/registry/KeyedRegistry.java +art/arcane/iris/util/common/data/registry/Materials.java +art/arcane/iris/util/common/data/registry/Particles.java +art/arcane/iris/util/common/data/registry/RegistryTypeAdapter.java +art/arcane/iris/util/common/data/registry/RegistryUtil.java +art/arcane/iris/util/common/director/DirectorExecutor.java +art/arcane/iris/util/common/director/context/VectorContextHandler.java +art/arcane/iris/util/common/director/context/WorldContextHandler.java +art/arcane/iris/util/common/director/handlers/BlockVectorHandler.java +art/arcane/iris/util/common/director/handlers/PlayerHandler.java +art/arcane/iris/util/common/director/handlers/VectorHandler.java +art/arcane/iris/util/common/director/handlers/WorldHandler.java +art/arcane/iris/util/common/director/specialhandlers/NullablePlayerHandler.java +art/arcane/iris/util/common/format/C.java +art/arcane/iris/util/common/inventorygui/UIStaticDecorator.java +art/arcane/iris/util/common/inventorygui/Window.java +art/arcane/iris/util/common/inventorygui/WindowResolution.java +art/arcane/iris/util/common/math/AxisAlignedBB.java +art/arcane/iris/util/common/math/Direction.java +art/arcane/iris/util/common/math/VectorMath.java +art/arcane/iris/util/common/misc/Bindings.java +art/arcane/iris/util/common/nbt/mca/Chunk.java +art/arcane/iris/util/common/nbt/mca/NBTWorld.java +art/arcane/iris/util/common/plugin/Chunks.java +art/arcane/iris/util/common/plugin/IrisService.java +art/arcane/iris/util/common/plugin/VolmitPlugin.java +art/arcane/iris/util/common/plugin/VolmitSender.java +art/arcane/iris/util/common/plugin/chunk/ChunkTickets.java +art/arcane/iris/util/common/plugin/chunk/TicketHolder.java +art/arcane/iris/util/common/reflect/KeyedType.java +art/arcane/iris/util/common/scheduling/J.java +art/arcane/iris/util/project/hunk/Hunk.java +art/arcane/iris/util/project/hunk/view/ChunkBiomeHunkView.java +art/arcane/iris/util/project/hunk/view/ChunkDataHunkHolder.java +art/arcane/iris/util/project/hunk/view/ChunkDataHunkView.java +art/arcane/iris/util/project/hunk/view/ChunkHunkView.java +art/arcane/iris/util/project/matter/IrisMatterSupport.java +art/arcane/iris/util/project/matter/WorldMatter.java +art/arcane/iris/util/project/matter/slices/EntityMatter.java +art/arcane/iris/util/project/matter/slices/TileMatter.java +art/arcane/iris/util/project/sentry/Attachments.java +art/arcane/iris/util/project/sentry/ServerID.java diff --git a/core/src/main/java/art/arcane/iris/core/DatapackInstallResult.java b/core/src/main/java/art/arcane/iris/core/DatapackInstallResult.java new file mode 100644 index 000000000..42ca4bbba --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/DatapackInstallResult.java @@ -0,0 +1,44 @@ +package art.arcane.iris.core; + +import java.util.Objects; + +public record DatapackInstallResult(Status status) { + public DatapackInstallResult { + Objects.requireNonNull(status, "status"); + } + + public static DatapackInstallResult failedResult() { + return new DatapackInstallResult(Status.FAILED); + } + + public static DatapackInstallResult unchangedResult() { + return new DatapackInstallResult(Status.UNCHANGED); + } + + public static DatapackInstallResult readyResult() { + return new DatapackInstallResult(Status.READY); + } + + public static DatapackInstallResult restartRequiredResult() { + return new DatapackInstallResult(Status.RESTART_REQUIRED); + } + + public boolean succeeded() { + return status != Status.FAILED; + } + + public boolean changed() { + return status == Status.READY || status == Status.RESTART_REQUIRED; + } + + public boolean restartRequired() { + return status == Status.RESTART_REQUIRED; + } + + public enum Status { + FAILED, + UNCHANGED, + READY, + RESTART_REQUIRED + } +} diff --git a/core/src/main/java/art/arcane/iris/core/ExactWorldSlotPathPolicy.java b/core/src/main/java/art/arcane/iris/core/ExactWorldSlotPathPolicy.java new file mode 100644 index 000000000..bf1c7a374 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/ExactWorldSlotPathPolicy.java @@ -0,0 +1,213 @@ +package art.arcane.iris.core; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Objects; +import java.util.regex.Pattern; + +public final class ExactWorldSlotPathPolicy { + private static final Pattern SAFE_IRIS_KEY = Pattern.compile("^[a-z0-9_-]+$"); + + private ExactWorldSlotPathPolicy() { + } + + public static Target resolve(Path levelRoot, WorldSlotKey worldKey) { + WorldSlotKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey"); + Path canonicalLevelRoot = canonicalLevelRoot(levelRoot); + SlotKind slotKind = classify(requiredWorldKey); + Path dimensionsRoot = canonicalLevelRoot.resolve("dimensions"); + Path namespaceRoot = dimensionsRoot.resolve(requiredWorldKey.namespace()); + Path worldDirectory = namespaceRoot.resolve(requiredWorldKey.key()).normalize(); + if (!Objects.equals(worldDirectory.getParent(), namespaceRoot)) { + throw new Rejection( + RejectionReason.PATH_TRAVERSAL, + "World key escapes its exact dimension namespace: " + requiredWorldKey + ); + } + + requireDirectoryOrAbsent(dimensionsRoot, "dimension storage"); + requireDirectoryOrAbsent(namespaceRoot, "dimension namespace"); + requireDirectoryOrAbsent(worldDirectory, "world slot"); + return new Target(requiredWorldKey, slotKind, canonicalLevelRoot, namespaceRoot, worldDirectory); + } + + public static Target validate(Path levelRoot, WorldSlotKey worldKey, Path candidate) { + Path requiredCandidate = Objects.requireNonNull(candidate, "candidate"); + rejectTraversal(requiredCandidate, "World candidate"); + Target target = resolve(levelRoot, worldKey); + Path normalizedCandidate = requiredCandidate.toAbsolutePath().normalize(); + if (!normalizedCandidate.equals(target.worldDirectory())) { + throw new Rejection( + RejectionReason.PATH_MISMATCH, + "World candidate is not the exact expected dimension slot." + ); + } + requireDirectoryOrAbsent(normalizedCandidate, "world slot"); + return target; + } + + private static Path canonicalLevelRoot(Path levelRoot) { + Path requiredLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot"); + rejectTraversal(requiredLevelRoot, "Level root"); + Path normalizedLevelRoot = requiredLevelRoot.toAbsolutePath().normalize(); + if (normalizedLevelRoot.getParent() == null) { + throw new Rejection(RejectionReason.UNSAFE_ENTRY, "A filesystem root cannot be a level root."); + } + requireDirectory(normalizedLevelRoot, "level root", true); + try { + return normalizedLevelRoot.toRealPath(); + } catch (IOException exception) { + throw new Rejection( + RejectionReason.UNSAFE_ENTRY, + "Could not canonicalize the level root: " + normalizedLevelRoot, + exception + ); + } + } + + private static SlotKind classify(WorldSlotKey worldKey) { + if ("iris".equals(worldKey.namespace())) { + if (!SAFE_IRIS_KEY.matcher(worldKey.key()).matches()) { + throw new Rejection( + RejectionReason.INVALID_IRIS_KEY, + "Iris world keys must be safe single path segments." + ); + } + return SlotKind.IRIS_MANAGED; + } + if (!"minecraft".equals(worldKey.namespace())) { + throw new Rejection( + RejectionReason.FOREIGN_NAMESPACE, + "Only Iris-managed and exact vanilla dimension slots can be replaced." + ); + } + return switch (worldKey.key()) { + case "overworld" -> SlotKind.VANILLA_OVERWORLD; + case "the_nether" -> SlotKind.VANILLA_NETHER; + case "the_end" -> SlotKind.VANILLA_END; + default -> throw new Rejection( + RejectionReason.UNSUPPORTED_MINECRAFT_SLOT, + "Only minecraft:overworld, minecraft:the_nether, and minecraft:the_end can be replaced." + ); + }; + } + + private static void rejectTraversal(Path path, String label) { + for (Path component : path) { + if ("..".equals(component.toString())) { + throw new Rejection( + RejectionReason.PATH_TRAVERSAL, + label + " contains path traversal." + ); + } + } + } + + private static void requireDirectoryOrAbsent(Path path, String label) { + if (Files.isSymbolicLink(path)) { + throw new Rejection( + RejectionReason.SYMBOLIC_LINK, + "The " + label + " is a symbolic link: " + path + ); + } + requireDirectory(path, label, false); + } + + private static void requireDirectory(Path path, String label, boolean required) { + if (Files.isSymbolicLink(path)) { + throw new Rejection( + RejectionReason.SYMBOLIC_LINK, + "The " + label + " is a symbolic link: " + path + ); + } + BasicFileAttributes attributes; + try { + attributes = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + } catch (NoSuchFileException exception) { + if (!required) { + return; + } + throw new Rejection( + RejectionReason.MISSING_LEVEL_ROOT, + "The level root does not exist: " + path, + exception + ); + } catch (IOException exception) { + throw new Rejection( + RejectionReason.UNSAFE_ENTRY, + "Could not inspect the " + label + ": " + path, + exception + ); + } + if (!attributes.isDirectory()) { + throw new Rejection( + RejectionReason.UNSAFE_ENTRY, + "The " + label + " is not a directory: " + path + ); + } + } + + public record Target( + WorldSlotKey worldKey, + SlotKind slotKind, + Path levelRoot, + Path namespaceRoot, + Path worldDirectory + ) { + public Target { + Objects.requireNonNull(worldKey, "worldKey"); + Objects.requireNonNull(slotKind, "slotKind"); + levelRoot = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize(); + namespaceRoot = Objects.requireNonNull(namespaceRoot, "namespaceRoot").toAbsolutePath().normalize(); + worldDirectory = Objects.requireNonNull(worldDirectory, "worldDirectory").toAbsolutePath().normalize(); + SlotKind expectedSlotKind = classify(worldKey); + Path expectedNamespaceRoot = levelRoot.resolve("dimensions").resolve(worldKey.namespace()); + Path expectedWorldDirectory = expectedNamespaceRoot.resolve(worldKey.key()); + if (slotKind != expectedSlotKind + || !namespaceRoot.equals(expectedNamespaceRoot) + || !worldDirectory.equals(expectedWorldDirectory)) { + throw new IllegalArgumentException("World slot paths do not form an exact dimension hierarchy."); + } + } + } + + public enum SlotKind { + IRIS_MANAGED, + VANILLA_OVERWORLD, + VANILLA_NETHER, + VANILLA_END + } + + public enum RejectionReason { + INVALID_IRIS_KEY, + FOREIGN_NAMESPACE, + UNSUPPORTED_MINECRAFT_SLOT, + MISSING_LEVEL_ROOT, + SYMBOLIC_LINK, + UNSAFE_ENTRY, + PATH_TRAVERSAL, + PATH_MISMATCH + } + + public static final class Rejection extends IllegalArgumentException { + private final RejectionReason reason; + + private Rejection(RejectionReason reason, String message) { + super(message); + this.reason = Objects.requireNonNull(reason, "reason"); + } + + private Rejection(RejectionReason reason, String message, Throwable cause) { + super(message, cause); + this.reason = Objects.requireNonNull(reason, "reason"); + } + + public RejectionReason reason() { + return reason; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java b/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java new file mode 100644 index 000000000..feade6483 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java @@ -0,0 +1,710 @@ +package art.arcane.iris.core; + +import art.arcane.iris.BuildConstants; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.core.nms.datapack.IDataFixer; +import art.arcane.iris.core.pack.AtomicDirectoryPublisher; +import art.arcane.iris.core.pack.PackDirectoryResolver; +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.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KSet; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public final class IrisDatapackCompiler { + private static final int INPUT_FINGERPRINT_SCHEMA = 2; + private static final int INPUT_BUFFER_BYTES = 64 * 1024; + private static final int WORLD_PACK_SCAN_DEPTH = 8; + private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); + private static final List INPUT_DIRECTORIES = List.of("dimensions", "biomes", "snippet"); + private static final String FLAT_VOID_LEVEL_STEM = """ + { + "type": "%s", + "generator": { + "type": "minecraft:flat", + "settings": { + "biome": "minecraft:the_void", + "features": false, + "lakes": false, + "layers": [ + { + "block": "minecraft:air", + "height": 1 + } + ], + "structure_overrides": [] + } + } + } + """; + + private IrisDatapackCompiler() { + } + + public static List collectPackRoots(Path dataDirectory, Path serverRoot) throws IOException { + return collectPackRoots(dataDirectory, serverRoot, true); + } + + public static List collectCompilerInputRoots(Path dataDirectory, Path serverRoot) throws IOException { + return collectPackRoots(dataDirectory, serverRoot, false); + } + + private static List collectPackRoots( + Path dataDirectory, + Path serverRoot, + boolean validateWholePack + ) throws IOException { + LinkedHashMap roots = new LinkedHashMap<>(); + collectInstalledPackRoots(dataDirectory.resolve("packs"), roots, validateWholePack); + collectWorldPackRoots(serverRoot.resolve("dimensions"), roots, validateWholePack); + return new ArrayList<>(roots.values()); + } + + public static String computeInputFingerprint( + List packRoots, + List bindings, + IDataFixer fixer, + boolean adjustVanillaHeight + ) throws IOException { + Objects.requireNonNull(fixer, "fixer"); + return computeInputFingerprint( + packRoots, + bindings, + adjustVanillaHeight, + compilerIdentity(fixer)); + } + + static String computeInputFingerprint( + List packRoots, + List bindings, + boolean adjustVanillaHeight, + String compilerIdentity + ) throws IOException { + Objects.requireNonNull(packRoots, "packRoots"); + Objects.requireNonNull(compilerIdentity, "compilerIdentity"); + List normalizedBindings = normalizeBindings(bindings); + MessageDigest digest = sha256(); + updateDigestString(digest, "iris-datapack-compiler-input"); + updateDigestInt(digest, INPUT_FINGERPRINT_SCHEMA); + updateDigestString(digest, compilerIdentity); + digest.update((byte) (adjustVanillaHeight ? 1 : 0)); + updateDigestInt(digest, normalizedBindings.size()); + for (IrisGeneratorBinding binding : normalizedBindings) { + updateDigestString(digest, binding.worldKey().toString()); + updateDigestString(digest, binding.dimension()); + } + updateDigestInt(digest, packRoots.size()); + + for (int index = 0; index < packRoots.size(); index++) { + File packRoot = Objects.requireNonNull(packRoots.get(index), "pack root"); + Path normalizedRoot = packRoot.toPath().toAbsolutePath().normalize(); + if (!Files.isDirectory(normalizedRoot)) { + throw new IOException("Iris datapack compiler input root is missing or unsafe: " + normalizedRoot); + } + Path realRoot = normalizedRoot.toRealPath(); + updateDigestInt(digest, index); + updateDigestString(digest, normalizedRoot.toString()); + updateDigestString(digest, realRoot.toString()); + boolean active = hasDimensions(normalizedRoot); + digest.update((byte) (active ? 1 : 0)); + if (!active) { + continue; + } + + List entries = collectCompilerInputEntries(normalizedRoot); + updateDigestInt(digest, entries.size()); + byte[] buffer = new byte[INPUT_BUFFER_BYTES]; + for (CompilerInputEntry entry : entries) { + updateDigestString(digest, entry.relativePath()); + updateDigestLong(digest, entry.size()); + long readBytes = 0L; + try (InputStream input = Files.newInputStream( + entry.source(), + StandardOpenOption.READ, + LinkOption.NOFOLLOW_LINKS)) { + int read; + while ((read = input.read(buffer)) >= 0) { + if (read > 0) { + digest.update(buffer, 0, read); + readBytes += read; + } + } + } + if (readBytes != entry.size()) { + throw new IOException("Iris datapack compiler input changed while hashing: " + entry.source()); + } + } + } + return HexFormat.of().formatHex(digest.digest()); + } + + public static Map computeRegistryRequirements( + List packRoots, + IDataFixer fixer + ) throws IOException { + Objects.requireNonNull(packRoots, "packRoots"); + Objects.requireNonNull(fixer, "fixer"); + LinkedHashMap requirements = new LinkedHashMap<>(); + Set installedBiomeKeys = new LinkedHashSet<>(); + for (File packRoot : packRoots) { + PackDirectoryResolver.requireSafePackTree(packRoot); + if (!hasDimensions(packRoot.toPath())) { + continue; + } + IrisData data = IrisData.openDatapackCompiler(packRoot); + try { + ResourceLoader loader = data.getDimensionLoader(); + String[] possibleKeys = loader.getPossibleKeys(); + if (possibleKeys == null || possibleKeys.length == 0) { + throw new IOException("Iris pack has no dimension definitions: " + packRoot); + } + for (String possibleKey : possibleKeys) { + IrisDimension dimension = loader.load(possibleKey); + if (dimension == null) { + throw new IOException("Unable to load Iris dimension '" + possibleKey + "' from " + packRoot); + } + collectRegistryRequirements( + requirements, + installedBiomeKeys, + dimension, + fixer); + } + } finally { + data.close(); + } + } + return Map.copyOf(requirements); + } + + public static Map computeRegistryRequirements( + IrisDimension dimension, + IDataFixer fixer + ) throws IOException { + IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension"); + IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer"); + LinkedHashMap requirements = new LinkedHashMap<>(); + collectRegistryRequirements( + requirements, + new LinkedHashSet<>(), + requiredDimension, + requiredFixer); + return Map.copyOf(requirements); + } + + private static void collectRegistryRequirements( + Map requirements, + Set installedBiomeKeys, + IrisDimension dimension, + IDataFixer fixer + ) { + requirements.put( + "dimension_type/iris:" + dimension.getDimensionTypeKey(), + fingerprintContent(dimension.getDimensionType().toJson(fixer))); + + String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT); + for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) { + if (biome == null || !biome.isCustom()) { + continue; + } + String derivativeKey = biome.getVanillaDerivativeKey(); + for (IrisBiomeCustom customBiome : biome.getCustomDerivitives()) { + if (customBiome == null) { + continue; + } + String biomeKey = namespace + ":" + customBiome.getId(); + if (!installedBiomeKeys.add(biomeKey)) { + continue; + } + requirements.put( + "worldgen/biome/" + biomeKey, + fingerprintContent(customBiome.generateJson(fixer))); + TreeSet tags = new TreeSet<>(); + for (String tag : customBiome.getEffectiveTags(derivativeKey)) { + String normalizedTag = normalizeRegistryKey(tag); + if (normalizedTag != null) { + tags.add(normalizedTag); + } + } + requirements.put( + "worldgen/biome_tags/" + biomeKey, + fingerprintContent(String.join("\n", tags))); + } + } + } + + private static String normalizeRegistryKey(String key) { + if (key == null || key.isBlank()) { + return null; + } + String normalized = key.trim().toLowerCase(Locale.ROOT); + if (normalized.indexOf(':') < 0) { + normalized = "minecraft:" + normalized; + } + return REGISTRY_KEY_PATTERN.matcher(normalized).matches() ? normalized : null; + } + + private static String fingerprintContent(String content) { + MessageDigest digest = sha256(); + updateDigestString(digest, Objects.requireNonNull(content, "registry content")); + return HexFormat.of().formatHex(digest.digest()); + } + + public static String compilerIdentity(IDataFixer fixer) { + IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer"); + return String.join( + "|", + Integer.toString(INPUT_FINGERPRINT_SCHEMA), + BuildConstants.COMMIT, + BuildConstants.MINECRAFT_VERSION, + requiredFixer.getClass().getName(), + Integer.toString(DataVersion.minSupportedPackFormat()), + Integer.toString(DataVersion.getLatest().getPackFormat())); + } + + public static CompilationResult compile( + List packRoots, + KList datapackRoots, + List bindings, + IDataFixer fixer, + boolean adjustVanillaHeight + ) throws IOException { + Objects.requireNonNull(packRoots, "packRoots"); + Objects.requireNonNull(datapackRoots, "datapackRoots"); + Objects.requireNonNull(fixer, "fixer"); + List normalizedBindings = normalizeBindings(bindings); + if (datapackRoots.isEmpty()) { + throw new IOException("No Iris datapack output roots were provided"); + } + + resetOutputRoots(datapackRoots); + IrisDimension.clearGeneratedBiomeTags(datapackRoots); + + DimensionHeight height = new DimensionHeight(fixer); + Map> biomes = new LinkedHashMap<>(); + Map> dimensions = new LinkedHashMap<>(); + int packCount = 0; + int dimensionCount = 0; + for (File packRoot : packRoots) { + PackDirectoryResolver.requireSafePackTree(packRoot); + if (!hasDimensions(packRoot.toPath())) { + continue; + } + IrisData data = IrisData.openDatapackCompiler(packRoot); + try { + ResourceLoader loader = data.getDimensionLoader(); + String[] possibleKeys = loader.getPossibleKeys(); + if (possibleKeys == null || possibleKeys.length == 0) { + throw new IOException("Iris pack has no dimension definitions: " + packRoot); + } + + int installedDimensions = 0; + for (String possibleKey : possibleKeys) { + IrisDimension dimension = loader.load(possibleKey); + if (dimension == null) { + throw new IOException("Unable to load Iris dimension '" + possibleKey + "' from " + packRoot); + } + IrisLogging.debug(" Compiling Dimension " + dimension.getLoadFile().getPath()); + height.merge(dimension); + dimensions.computeIfAbsent(dimension.getLoadKey(), ignored -> new ArrayList<>()) + .add(new DimensionCandidate( + dimension, + packRoot.toPath().toAbsolutePath().normalize(), + dimension.getDimensionType().toJson(fixer) + )); + KSet seenBiomes = biomes.computeIfAbsent(dimension.getLoadKey(), ignored -> new KSet<>()); + dimension.installBiomes(fixer, dimension::getLoader, datapackRoots, seenBiomes); + dimension.installDimensionType(fixer, datapackRoots); + installedDimensions++; + dimensionCount++; + } + if (installedDimensions > 0) { + packCount++; + } + } finally { + data.close(); + } + } + + IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight); + installLevelStemBindings(normalizedBindings, dimensions, datapackRoots); + validateOutputs(datapackRoots, dimensionCount, normalizedBindings.size()); + return new CompilationResult(packCount, dimensionCount, countBiomes(biomes)); + } + + private static void installLevelStemBindings( + List bindings, + Map> dimensions, + Collection datapackRoots + ) throws IOException { + for (IrisGeneratorBinding binding : bindings) { + DimensionCandidate selected = resolveDimension(binding, dimensions.get(binding.dimension())); + String typeKey = "iris:" + selected.dimension().getDimensionTypeKey(); + String levelStem = FLAT_VOID_LEVEL_STEM.formatted(typeKey); + for (File datapackRoot : datapackRoots) { + Path output = datapackRoot.toPath() + .toAbsolutePath() + .normalize() + .resolve("data/iris/dimension") + .resolve(binding.worldKey().key() + ".json"); + Files.createDirectories(output.getParent()); + Files.writeString( + output, + levelStem, + StandardCharsets.UTF_8, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE + ); + } + } + } + + private static DimensionCandidate resolveDimension( + IrisGeneratorBinding binding, + List candidates + ) throws IOException { + if (candidates == null || candidates.isEmpty()) { + throw new IOException("Iris world " + binding.worldKey() + " selects missing dimension \"" + + binding.dimension() + "\"."); + } + DimensionCandidate selected = candidates.getFirst(); + for (int index = 1; index < candidates.size(); index++) { + DimensionCandidate candidate = candidates.get(index); + if (!selected.dimensionTypeJson().equals(candidate.dimensionTypeJson())) { + throw new IOException("Iris world " + binding.worldKey() + " selects ambiguous dimension \"" + + binding.dimension() + "\" from " + selected.packRoot() + " and " + + candidate.packRoot() + "."); + } + } + return selected; + } + + private static List normalizeBindings(List bindings) + throws IOException { + List requiredBindings = List.copyOf( + Objects.requireNonNull(bindings, "bindings") + ); + Map byWorld = new LinkedHashMap<>(); + for (IrisGeneratorBinding binding : requiredBindings) { + IrisGeneratorBinding requiredBinding = Objects.requireNonNull(binding, "binding"); + IrisGeneratorBinding previous = byWorld.putIfAbsent( + requiredBinding.worldKey(), + requiredBinding + ); + if (previous != null) { + throw new IOException("Multiple Iris LevelStem bindings target " + + requiredBinding.worldKey() + "."); + } + } + ArrayList normalized = new ArrayList<>(byWorld.values()); + normalized.sort(Comparator.comparing(binding -> binding.worldKey().toString())); + return List.copyOf(normalized); + } + + private static void collectInstalledPackRoots( + Path packsRoot, + Map roots, + boolean validateWholePack + ) throws IOException { + List candidates = PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(packsRoot.toFile()); + for (File candidate : candidates) { + addPackRoot(candidate.toPath(), roots, validateWholePack); + } + } + + private static void collectWorldPackRoots( + Path dimensionsRoot, + Map roots, + boolean validateWholePack + ) throws IOException { + if (Files.isSymbolicLink(dimensionsRoot) + || !Files.isDirectory(dimensionsRoot, LinkOption.NOFOLLOW_LINKS)) { + return; + } + List namespaces = visibleDirectories(dimensionsRoot); + List candidates = new ArrayList<>(); + for (Path namespace : namespaces) { + collectNamespaceWorldPackRoots(namespace, candidates); + } + candidates.sort(Comparator.comparing(Path::toString)); + for (Path candidate : candidates) { + addPackRoot(candidate, roots, validateWholePack); + } + } + + private static void collectNamespaceWorldPackRoots( + Path namespace, + List candidates + ) throws IOException { + Files.walkFileTree(namespace, Set.of(), WORLD_PACK_SCAN_DEPTH, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory( + Path directory, + BasicFileAttributes attributes + ) { + if (!directory.equals(namespace) + && PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) { + return FileVisitResult.SKIP_SUBTREE; + } + Path irisRoot = directory.resolve("iris"); + Path candidate = irisRoot.resolve("pack"); + if (!Files.isSymbolicLink(irisRoot) + && !Files.isSymbolicLink(candidate) + && Files.isDirectory(candidate, LinkOption.NOFOLLOW_LINKS) + && hasDimensions(candidate)) { + candidates.add(candidate); + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + }); + } + + private static List visibleDirectories(Path root) throws IOException { + try (Stream entries = Files.list(root)) { + return entries + .filter(entry -> !PackDirectoryResolver.isHiddenName(entry.getFileName().toString())) + .filter(entry -> !Files.isSymbolicLink(entry)) + .filter(entry -> Files.isDirectory(entry, LinkOption.NOFOLLOW_LINKS)) + .sorted(Comparator.comparing(Path::toString)) + .toList(); + } + } + + private static void addPackRoot( + Path root, + Map roots, + boolean validateWholePack + ) throws IOException { + if (!hasDimensions(root)) { + return; + } + Path normalized = root.toAbsolutePath().normalize(); + if (!Files.isDirectory(normalized)) { + return; + } + Path identity = normalized.toRealPath(); + if (!Files.isDirectory(identity, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (validateWholePack) { + PackDirectoryResolver.requireSafePackTree(normalized.toFile()); + } + roots.putIfAbsent(identity, normalized.toFile()); + } + + private static boolean hasDimensions(Path root) { + Path dimensions = root.resolve("dimensions"); + if (Files.isSymbolicLink(dimensions) + || !Files.isDirectory(dimensions, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + try (Stream stream = Files.list(dimensions)) { + return stream.anyMatch(path -> !Files.isSymbolicLink(path) + && Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) + && path.getFileName().toString().endsWith(".json")); + } catch (IOException e) { + return false; + } + } + + private static List collectCompilerInputEntries(Path packRoot) throws IOException { + List entries = new ArrayList<>(); + for (String directoryName : INPUT_DIRECTORIES) { + Path directory = packRoot.resolve(directoryName); + if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + if (Files.isSymbolicLink(directory) + || !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Iris datapack compiler input is missing or unsafe: " + directory); + } + Files.walkFileTree(directory, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path child, BasicFileAttributes attributes) throws IOException { + if (attributes.isSymbolicLink() || Files.isSymbolicLink(child)) { + throw new IOException("Iris datapack compiler input contains a symbolic link: " + child); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { + if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) { + throw new IOException("Iris datapack compiler input contains a symbolic link: " + file); + } + if (!attributes.isRegularFile()) { + throw new IOException("Iris datapack compiler input contains an unsupported entry: " + file); + } + if (file.getFileName().toString().endsWith(".json")) { + String relativePath = packRoot.relativize(file).toString().replace(File.separatorChar, '/'); + entries.add(new CompilerInputEntry(file, relativePath, attributes.size())); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException { + throw new IOException("Unable to inspect Iris datapack compiler input: " + file, failure); + } + }); + } + entries.sort(Comparator.comparing(CompilerInputEntry::relativePath)); + return entries; + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 not available", exception); + } + } + + private static void updateDigestString(MessageDigest digest, String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + updateDigestInt(digest, bytes.length); + digest.update(bytes); + } + + private static void updateDigestInt(MessageDigest digest, int value) { + for (int shift = Integer.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) { + digest.update((byte) (value >>> shift)); + } + } + + private static void updateDigestLong(MessageDigest digest, long value) { + for (int shift = Long.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) { + digest.update((byte) (value >>> shift)); + } + } + + private static void resetOutputRoots(Collection datapackRoots) throws IOException { + for (File datapackRoot : datapackRoots) { + Path root = datapackRoot.toPath().toAbsolutePath().normalize(); + if (Files.isSymbolicLink(root)) { + throw new IOException("Iris datapack output root is a symbolic link: " + root); + } + if (Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Iris datapack output root is not a directory: " + root); + } + AtomicDirectoryPublisher.deleteTree(root); + } + Files.createDirectories(root); + } + } + + private static void validateOutputs( + Collection datapackRoots, + int dimensionCount, + int levelStemCount + ) throws IOException { + for (File datapackRoot : datapackRoots) { + Path root = datapackRoot.toPath(); + if (!Files.isRegularFile(root.resolve("pack.mcmeta"))) { + throw new IOException("Iris datapack metadata was not generated at " + root); + } + if (dimensionCount > 0 && !Files.isDirectory(root.resolve("data/iris/dimension_type"))) { + throw new IOException("Iris dimension types were not generated at " + root); + } + if (levelStemCount > 0 && !Files.isDirectory(root.resolve("data/iris/dimension"))) { + throw new IOException("Iris LevelStem bindings were not generated at " + root); + } + } + } + + private static int countBiomes(Map> biomes) { + int count = 0; + for (KSet values : biomes.values()) { + count += values.size(); + } + return count; + } + + public record CompilationResult(int packCount, int dimensionCount, int biomeCount) { + } + + private record CompilerInputEntry(Path source, String relativePath, long size) { + } + + private record DimensionCandidate( + IrisDimension dimension, + Path packRoot, + String dimensionTypeJson + ) { + } + + public static final class DimensionHeight { + private final IDataFixer fixer; + private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3]; + + public DimensionHeight(IDataFixer fixer) { + this.fixer = Objects.requireNonNull(fixer, "fixer"); + for (int index = 0; index < dimensions.length; index++) { + dimensions[index] = new AtomicIntegerArray(new int[]{ + Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE + }); + } + } + + public void merge(IrisDimension dimension) { + AtomicIntegerArray values = dimensions[dimension.getBaseDimension().ordinal()]; + values.updateAndGet(0, current -> Math.min(current, dimension.getMinHeight())); + values.updateAndGet(1, current -> Math.max(current, dimension.getMaxHeight())); + values.updateAndGet(2, current -> Math.max(current, dimension.getLogicalHeight())); + } + + public String[] jsonStrings() { + IDataFixer.Dimension[] types = IDataFixer.Dimension.values(); + String[] output = new String[types.length]; + for (int index = 0; index < types.length; index++) { + output[index] = jsonString(types[index]); + } + return output; + } + + private String jsonString(IDataFixer.Dimension dimension) { + AtomicIntegerArray values = dimensions[dimension.ordinal()]; + int minY = values.get(0); + int maxY = values.get(1); + int logicalHeight = values.get(2); + if (minY == Integer.MAX_VALUE || maxY == Integer.MIN_VALUE || logicalHeight == Integer.MIN_VALUE) { + return null; + } + return fixer.createDimension(dimension, minY, maxY - minY, logicalHeight, null).toString(4); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisPaperLikeBackendMode.java b/core/src/main/java/art/arcane/iris/core/IrisPaperLikeBackendMode.java new file mode 100644 index 000000000..9b068a863 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisPaperLikeBackendMode.java @@ -0,0 +1,7 @@ +package art.arcane.iris.core; + +public enum IrisPaperLikeBackendMode { + AUTO, + TICKET, + SERVICE +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisRuntimeSchedulerMode.java b/core/src/main/java/art/arcane/iris/core/IrisRuntimeSchedulerMode.java new file mode 100644 index 000000000..21ca8ae7e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisRuntimeSchedulerMode.java @@ -0,0 +1,76 @@ +package art.arcane.iris.core; + +import art.arcane.volmlib.util.scheduling.FoliaScheduler; +import org.bukkit.Bukkit; +import org.bukkit.Server; + +import java.util.Locale; + +public enum IrisRuntimeSchedulerMode { + AUTO, + PAPER_LIKE, + FOLIA; + + public static IrisRuntimeSchedulerMode resolve(IrisSettings.IrisSettingsPregen pregen) { + Server server = Bukkit.getServer(); + boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server); + if (regionizedRuntime) { + return FOLIA; + } + + IrisRuntimeSchedulerMode configuredMode = pregen == null ? null : pregen.getRuntimeSchedulerMode(); + if (configuredMode != null && configuredMode != AUTO) { + if (configuredMode == FOLIA) { + return PAPER_LIKE; + } + return configuredMode; + } + + String bukkitName = Bukkit.getName(); + String bukkitVersion = Bukkit.getVersion(); + String serverClassName = server == null ? "" : server.getClass().getName(); + if (containsIgnoreCase(bukkitName, "folia") + || containsIgnoreCase(bukkitVersion, "folia") + || containsIgnoreCase(serverClassName, "folia")) { + return FOLIA; + } + + if (containsIgnoreCase(bukkitName, "purpur") + || containsIgnoreCase(bukkitVersion, "purpur") + || containsIgnoreCase(serverClassName, "purpur") + || containsIgnoreCase(bukkitName, "canvas") + || containsIgnoreCase(bukkitVersion, "canvas") + || containsIgnoreCase(serverClassName, "canvas") + || containsIgnoreCase(bukkitName, "paper") + || containsIgnoreCase(bukkitVersion, "paper") + || containsIgnoreCase(serverClassName, "paper") + || containsIgnoreCase(bukkitName, "pufferfish") + || containsIgnoreCase(bukkitVersion, "pufferfish") + || containsIgnoreCase(serverClassName, "pufferfish") + || containsIgnoreCase(bukkitName, "leaf") + || containsIgnoreCase(bukkitVersion, "leaf") + || containsIgnoreCase(serverClassName, "leaf") + || containsIgnoreCase(bukkitName, "spigot") + || containsIgnoreCase(bukkitVersion, "spigot") + || containsIgnoreCase(serverClassName, "spigot") + || containsIgnoreCase(bukkitName, "craftbukkit") + || containsIgnoreCase(bukkitVersion, "craftbukkit") + || containsIgnoreCase(serverClassName, "craftbukkit")) { + return PAPER_LIKE; + } + + if (regionizedRuntime) { + return FOLIA; + } + + return PAPER_LIKE; + } + + private static boolean containsIgnoreCase(String value, String contains) { + if (value == null || contains == null || contains.isEmpty()) { + return false; + } + + return value.toLowerCase(Locale.ROOT).contains(contains.toLowerCase(Locale.ROOT)); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java new file mode 100644 index 000000000..721bd7f09 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -0,0 +1,420 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import com.google.gson.Gson; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONException; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.iris.util.common.misc.getHardware; +import art.arcane.iris.util.common.plugin.VolmitSender; +import lombok.Data; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; +import java.util.function.Predicate; + +@Data +public class IrisSettings { + private static final Object SETTINGS_LOCK = new Object(); + private static final IrisSettings BOOTSTRAP_DEFAULTS = new IrisSettings(); + public static volatile IrisSettings settings; + private IrisSettingsGeneral general = new IrisSettingsGeneral(); + private IrisSettingsWorld world = new IrisSettingsWorld(); + private IrisSettingsGUI gui = new IrisSettingsGUI(); + private IrisSettingsAutoconfiguration autoConfiguration = new IrisSettingsAutoconfiguration(); + private IrisSettingsGenerator generator = new IrisSettingsGenerator(); + private transient IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency(); + private IrisSettingsStudio studio = new IrisSettingsStudio(); + private IrisSettingsPerformance performance = new IrisSettingsPerformance(); + private IrisSettingsPregen pregen = new IrisSettingsPregen(); + private IrisSettingsSentry sentry = new IrisSettingsSentry(); + private IrisSettingsTreeFeller treeFeller = new IrisSettingsTreeFeller(); + + public static int getThreadCount(int c) { + return Math.max(switch (c) { + case -1, -2, -4 -> Runtime.getRuntime().availableProcessors() / -c; + default -> Math.max(c, 2); + }, 1); + } + + public static IrisSettings get() { + IrisSettings current = settings; + + if (current != null) { + return current; + } + + if (Thread.holdsLock(SETTINGS_LOCK)) { + // read() logs and does IO, and the logging path calls back into get(). + // Serve defaults instead of recursing into another disk read. + return BOOTSTRAP_DEFAULTS; + } + + synchronized (SETTINGS_LOCK) { + current = settings; + + if (current != null) { + return current; + } + + current = read(); + settings = current; + return current; + } + } + + private static IrisSettings read() { + IrisSettings loaded = new IrisSettings(); + File s = IrisPlatforms.get().dataFile("settings.json"); + + if (!s.exists()) { + try { + IO.writeAll(s, new JSONObject(new Gson().toJson(loaded)).toString(4)); + } catch (JSONException | IOException e) { + e.printStackTrace(); + IrisLogging.reportError(e); + } + + return loaded; + } + + try { + String ss = IO.readAll(s); + IrisSettings parsed = new Gson().fromJson(ss, IrisSettings.class); + + if (parsed != null) { + loaded = parsed; + } + + migrateLegacyKeys(loaded, ss); + + try { + IO.writeAll(s, new JSONObject(new Gson().toJson(loaded)).toString(4)); + } catch (IOException e) { + e.printStackTrace(); + } + } catch (Throwable ee) { + // IrisLogging.reportError(ee); causes a self-reference & stackoverflow + IrisLogging.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage()); + } + + return loaded; + } + + private static void migrateLegacyKeys(IrisSettings target, String rawJson) { + JSONObject root = new JSONObject(rawJson); + JSONObject worldObject = root.optJSONObject("world"); + if (worldObject == null || !worldObject.has("anbientEntitySpawningSystem")) { + return; + } + + target.getWorld().setAmbientEntitySpawningSystem(worldObject.optBoolean("anbientEntitySpawningSystem", target.getWorld().isAmbientEntitySpawningSystem())); + IrisLogging.info("Migrated legacy settings key world.anbientEntitySpawningSystem -> world.ambientEntitySpawningSystem"); + } + + public static void invalidate() { + synchronized (SETTINGS_LOCK) { + settings = null; + } + } + + public static IrisSettings installHotloadSnapshot(String rawJson) { + IrisSettings parsed = parseHotloadSnapshot(rawJson); + synchronized (SETTINGS_LOCK) { + settings = parsed; + } + return parsed; + } + + public static boolean applyHotloadSnapshot(String rawJson, Predicate candidateActivation) { + Objects.requireNonNull(candidateActivation, "candidateActivation"); + IrisSettings parsed = parseHotloadSnapshot(rawJson); + if (!candidateActivation.test(parsed)) { + return false; + } + synchronized (SETTINGS_LOCK) { + settings = parsed; + } + return true; + } + + public static IrisSettings parseHotloadSnapshot(String rawJson) { + if (rawJson == null || rawJson.isBlank()) { + throw new IllegalArgumentException("Iris settings snapshot is empty"); + } + + IrisSettings parsed; + try { + parsed = new Gson().fromJson(rawJson, IrisSettings.class); + if (parsed == null) { + throw new IllegalArgumentException("Iris settings snapshot did not contain an object"); + } + migrateLegacyKeys(parsed, rawJson); + } catch (RuntimeException failure) { + throw new IllegalArgumentException("Iris settings snapshot is invalid", failure); + } + + parsed.fillMissingSections(); + return parsed; + } + + public void forceSave() { + File s = IrisPlatforms.get().dataFile("settings.json"); + + try { + IO.writeAll(s, new JSONObject(new Gson().toJson(this)).toString(4)); + } catch (JSONException | IOException e) { + e.printStackTrace(); + IrisLogging.reportError(e); + } + } + + private void fillMissingSections() { + general = general == null ? new IrisSettingsGeneral() : general; + world = world == null ? new IrisSettingsWorld() : world; + gui = gui == null ? new IrisSettingsGUI() : gui; + autoConfiguration = autoConfiguration == null ? new IrisSettingsAutoconfiguration() : autoConfiguration; + generator = generator == null ? new IrisSettingsGenerator() : generator; + concurrency = concurrency == null ? new IrisSettingsConcurrency() : concurrency; + studio = studio == null ? new IrisSettingsStudio() : studio; + performance = performance == null ? new IrisSettingsPerformance() : performance; + pregen = pregen == null ? new IrisSettingsPregen() : pregen; + sentry = sentry == null ? new IrisSettingsSentry() : sentry; + treeFeller = treeFeller == null ? new IrisSettingsTreeFeller() : treeFeller; + } + + @Data + public static class IrisSettingsAutoconfiguration { + public boolean configureSpigotTimeoutTime = true; + public boolean configurePaperWatchdogDelay = true; + } + + @Data + public static class IrisSettingsWorld { + public boolean postLoadBlockUpdates = true; + public boolean forcePersistEntities = true; + public boolean ambientEntitySpawningSystem = true; + public long asyncTickIntervalMS = 700; + public double targetSpawnEntitiesPerChunk = 0.95; + public boolean markerEntitySpawningSystem = true; + public boolean effectSystem = true; + public boolean worldEditWandCUI = true; + public boolean globalPregenCache = false; + } + + @Data + public static class IrisSettingsConcurrency { + public int getParallelism() { + return Math.max(2, Runtime.getRuntime().availableProcessors()); + } + + public int getIoParallelism() { + return Math.max(2, Runtime.getRuntime().availableProcessors() / 2); + } + + public int getWorldGenThreads() { + return Math.max(2, Runtime.getRuntime().availableProcessors()); + } + } + + @Data + public static class IrisSettingsPregen { + private static final int REFERENCE_WORLD_HEIGHT = 384; + private static final int MIN_RESIDENT_TECTONIC_PLATES = 16; + private static final double MANTLE_HEAP_FRACTION = 0.6D; + private static final int REFERENCE_PLATE_MEGABYTES = 48; + public IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO; + public IrisPaperLikeBackendMode paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO; + public int chunkLoadTimeoutSeconds = 15; + public int timeoutWarnIntervalMs = 500; + public int saveIntervalMs = 30_000; + public int maxResidentTectonicPlates = 96; + public int mantleBackpressureWaitMs = 25; + public int mantleBackpressureTimeoutMs = 60_000; + public int moddedPregenInFlight = 0; + + public int getChunkLoadTimeoutSeconds() { + return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120)); + } + + public int getModdedPregenInFlight() { + if (moddedPregenInFlight > 0) { + return Math.min(512, moddedPregenInFlight); + } + + int cpu = Math.max(1, Runtime.getRuntime().availableProcessors()); + return Math.max(16, Math.min(48, cpu * 2)); + } + + public int getMaxResidentTectonicPlates() { + return Math.max(16, maxResidentTectonicPlates); + } + + public int getEffectiveResidentTectonicPlates(int worldHeight) { + int baseCap = getMaxResidentTectonicPlates(); + int normalizedHeight = Math.max(1, worldHeight); + int heightScaledCap = (int) Math.round((double) baseCap * REFERENCE_WORLD_HEIGHT / (double) normalizedHeight); + long maxHeapMegabytes = getHardware.getProcessMemory(); + double plateMegabytes = (double) REFERENCE_PLATE_MEGABYTES * (double) normalizedHeight / (double) REFERENCE_WORLD_HEIGHT; + int byteBudgetCap = (int) Math.floor(MANTLE_HEAP_FRACTION * (double) maxHeapMegabytes / plateMegabytes); + int effective = Math.min(heightScaledCap, byteBudgetCap); + return Math.max(MIN_RESIDENT_TECTONIC_PLATES, Math.min(baseCap, effective)); + } + + public int getMantleBackpressureWaitMs() { + return Math.max(5, Math.min(mantleBackpressureWaitMs, 1_000)); + } + + public int getMantleBackpressureTimeoutMs() { + return Math.max(5_000, Math.min(mantleBackpressureTimeoutMs, 600_000)); + } + + public int getTimeoutWarnIntervalMs() { + return Math.max(timeoutWarnIntervalMs, 250); + } + + public IrisPaperLikeBackendMode getPaperLikeBackendMode() { + if (paperLikeBackendMode == null) { + return IrisPaperLikeBackendMode.AUTO; + } + + return paperLikeBackendMode; + } + + public int getSaveIntervalMs() { + return Math.max(5_000, Math.min(saveIntervalMs, 900_000)); + } + } + + @Data + public static class IrisSettingsPerformance { + private IrisSettingsEngineSVC engineSVC = new IrisSettingsEngineSVC(); + public boolean trimMantleInStudio = false; + public int mantleKeepAlive = 30; + public int noiseCacheSize = 1_024; + public int resourceLoaderCacheSize = 1_024; + public int objectLoaderCacheSize = 4_096; + public int mantleCleanupDelay = 200; + public boolean simdKernels = true; + } + + @Data + public static class IrisSettingsGeneral { + public String language = "en_US"; + public boolean commandSounds = true; + public boolean debug = false; + public boolean dumpMantleOnError = false; + public boolean disableNMS = false; + public boolean pluginMetrics = true; + public boolean splashLogoStartup = true; + public boolean useConsoleCustomColors = true; + public boolean useCustomColorsIngame = true; + /** + * Boss bar progress loaders for jobs, studio opens, world creation, chunk jobs and pack + * downloads. Turning this off keeps the action bar progress line; only the bar goes away. + */ + public boolean progressBossBar = true; + public boolean adjustVanillaHeight = false; + public boolean autoIngestDatapacks = true; + /** + * Converting every registered datapack structure into editable Iris resources writes + * thousands of objects/pools/pieces into the pack folder. Native generation and + * nativeStructures placements never need those copies, so this stays opt-in; run + * /iris structure import <dimension> when you actually want editable copies. + */ + public boolean autoImportDatapackStructures = false; + /** Unresolved pack content keys and bad block-state properties become blocking pack errors. -Diris.strictContent overrides. */ + public boolean strictContentKeys = false; + public int spinh = -20; + public int spins = 7; + public int spinb = 8; + + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean canUseCustomColors(VolmitSender volmitSender) { + return volmitSender.isPlayer() ? useCustomColorsIngame : useConsoleCustomColors; + } + } + + @Data + public static class IrisSettingsSentry { + public boolean includeServerId = true; + public boolean disableAutoReporting = false; + public boolean debug = false; + } + + @Data + public static class IrisSettingsGUI { + public boolean useServerLaunchedGuis = true; + public boolean maximumPregenGuiFPS = false; + public boolean colorMode = true; + } + + @Data + public static class IrisSettingsGenerator { + public String defaultWorldType = "overworld"; + public boolean preventLeafDecay = true; + } + + @Data + public static class IrisSettingsTreeFeller { + public boolean enabled = false; + public int durabilityPreservationChance = 0; + + public int getDurabilityPreservationChance() { + return Math.max(0, Math.min(durabilityPreservationChance, 100)); + } + } + + @Data + public static class IrisSettingsStudio { + public boolean openVSCode = true; + public boolean disableTimeAndWeather = true; + public boolean entitySpawning = true; + public boolean autoStartDefaultStudio = false; + } + + @Data + public static class IrisSettingsEngineSVC { + public boolean useVirtualThreads = true; + public boolean forceMulticoreWrite = false; + public int priority = Thread.NORM_PRIORITY; + public int parallelism = -1; + + public int getPriority() { + return Math.max(Math.min(priority, Thread.MAX_PRIORITY), Thread.MIN_PRIORITY); + } + + public int getParallelism() { + int processors = Math.max(1, Runtime.getRuntime().availableProcessors()); + if (parallelism > 0) { + int maximumParallelism = processors > Integer.MAX_VALUE / 2 + ? Integer.MAX_VALUE + : processors * 2; + return Math.min(parallelism, maximumParallelism); + } + + return Math.max(1, (int) Math.ceil(Math.sqrt(processors))); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java b/core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java new file mode 100644 index 000000000..cfd8536ae --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java @@ -0,0 +1,164 @@ +package art.arcane.iris.core; + +import java.util.List; +import java.util.Optional; + +public final class IrisStartupValidation { + private static volatile Snapshot snapshot = Snapshot.disabled(); + + private IrisStartupValidation() { + } + + public static synchronized void begin() { + snapshot = new Snapshot(true, ValidationState.PENDING, ValidationState.PENDING, List.of(), List.of()); + } + + public static synchronized void disable() { + snapshot = Snapshot.disabled(); + } + + public static synchronized void beginDatapackValidation() { + if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) { + return; + } + snapshot = new Snapshot(true, ValidationState.PENDING, snapshot.packs(), List.of(), snapshot.packFailures()); + } + + public static synchronized void markDatapacksReady() { + if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) { + return; + } + snapshot = new Snapshot(true, ValidationState.READY, snapshot.packs(), List.of(), snapshot.packFailures()); + } + + public static synchronized void markDatapacksInvalid(String failure) { + if (!snapshot.enforced()) { + return; + } + snapshot = new Snapshot( + true, + ValidationState.INVALID, + snapshot.packs(), + List.of(normalizeFailure(failure, "External datapack validation failed.")), + snapshot.packFailures()); + } + + public static synchronized void requireRestart(String reason) { + if (!snapshot.enforced()) { + return; + } + snapshot = new Snapshot( + true, + ValidationState.RESTART_REQUIRED, + snapshot.packs(), + List.of(normalizeFailure(reason, "A restart is required to load validated datapacks.")), + snapshot.packFailures()); + } + + public static synchronized void markPacksReady() { + if (!snapshot.enforced()) { + return; + } + snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.READY, snapshot.datapackFailures(), List.of()); + } + + public static synchronized void markPacksInvalid(List failures) { + if (!snapshot.enforced()) { + return; + } + List normalized = failures == null || failures.isEmpty() + ? List.of("Iris dimension-pack validation failed.") + : failures.stream() + .map(failure -> normalizeFailure(failure, "Iris dimension-pack validation failed.")) + .toList(); + snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.INVALID, + snapshot.datapackFailures(), normalized); + } + + public static boolean isReady() { + return isReady(snapshot); + } + + public static Optional denialReason() { + Snapshot current = snapshot; + if (!current.enforced() || isReady(current)) { + return Optional.empty(); + } + if (current.datapacks() == ValidationState.RESTART_REQUIRED) { + return Optional.of(firstFailure(current.datapackFailures(), + "Iris installed validated datapacks and requires a restart before the server is safe.")); + } + if (current.datapacks() == ValidationState.INVALID) { + return Optional.of(firstFailure(current.datapackFailures(), + "Iris external datapack validation failed.")); + } + if (current.datapacks() == ValidationState.PENDING) { + return Optional.of("Iris is still validating external datapacks."); + } + if (current.packs() == ValidationState.INVALID) { + return Optional.of(firstFailure(current.packFailures(), + "Iris dimension-pack validation failed.")); + } + return Optional.of("Iris is still validating dimension packs."); + } + + public static void requireWorldCreationReady() { + Optional denial = denialReason(); + if (denial.isPresent()) { + throw new IllegalStateException("Iris world creation is locked: " + denial.get()); + } + } + + public static void requireWorldReplacementStagingReady() { + Snapshot current = snapshot; + if (current.enforced() + && current.datapacks() == ValidationState.RESTART_REQUIRED + && current.packs() == ValidationState.READY) { + return; + } + requireWorldCreationReady(); + } + + static Snapshot snapshot() { + return snapshot; + } + + private static String normalizeFailure(String failure, String fallback) { + return failure == null || failure.isBlank() ? fallback : failure.trim(); + } + + private static String firstFailure(List failures, String fallback) { + return failures == null || failures.isEmpty() ? fallback : failures.getFirst(); + } + + private static boolean isReady(Snapshot current) { + return !current.enforced() + || current.datapacks() == ValidationState.READY + && current.packs() == ValidationState.READY; + } + + enum ValidationState { + PENDING, + READY, + INVALID, + RESTART_REQUIRED, + DISABLED + } + + record Snapshot( + boolean enforced, + ValidationState datapacks, + ValidationState packs, + List datapackFailures, + List packFailures + ) { + Snapshot { + datapackFailures = List.copyOf(datapackFailures); + packFailures = List.copyOf(packFailures); + } + + private static Snapshot disabled() { + return new Snapshot(false, ValidationState.DISABLED, ValidationState.DISABLED, List.of(), List.of()); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java b/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java new file mode 100644 index 000000000..b6afd8c50 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java @@ -0,0 +1,537 @@ +package art.arcane.iris.core; + +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.generator.WorldInfo; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Stream; + +public final class IrisWorldStorage { + private static final String IRIS_NAMESPACE = "iris"; + private static final String DEFAULT_LEVEL_NAME = "world"; + + /** + * Server#getLevelDirectory is Paper-API-only. Once a call throws NoSuchMethodError (plain + * Spigot/CraftBukkit) this flips and every later call goes straight to the fallback. + */ + private static volatile boolean levelDirectoryUnavailable; + private static volatile String cachedFallbackLevelName; + + private IrisWorldStorage() { + } + + public static File levelRoot() { + if (!levelDirectoryUnavailable) { + try { + return Bukkit.getServer().getLevelDirectory().toAbsolutePath().normalize().toFile(); + } catch (NoSuchMethodError e) { + levelDirectoryUnavailable = true; + } + } + return new File(Bukkit.getWorldContainer(), fallbackLevelName()).getAbsoluteFile(); + } + + private static String fallbackLevelName() { + String cached = cachedFallbackLevelName; + if (cached == null) { + cached = levelNameFromProperties(new File("server.properties")); + cachedFallbackLevelName = cached; + } + return cached; + } + + public static String configuredLevelName() { + return levelNameFromProperties(new File("server.properties")); + } + + static String levelNameFromProperties(File serverProperties) { + Properties properties = new Properties(); + if (Objects.requireNonNull(serverProperties, "serverProperties").isFile()) { + try (InputStream in = new FileInputStream(serverProperties)) { + properties.load(in); + } catch (IOException ignored) { + // Unreadable server.properties: fall through to the default level name. + } + } + return levelNameFromProperties(properties); + } + + static String levelNameFromProperties(Properties properties) { + String levelName = Objects.requireNonNull(properties, "properties").getProperty("level-name", DEFAULT_LEVEL_NAME).trim(); + return levelName.isEmpty() ? DEFAULT_LEVEL_NAME : levelName; + } + + public static File levelRoot(File dimensionRoot) { + Path current = Objects.requireNonNull(dimensionRoot, "dimensionRoot").toPath().toAbsolutePath().normalize(); + while (current != null) { + Path fileName = current.getFileName(); + if (fileName != null && "dimensions".equals(fileName.toString())) { + Path parent = current.getParent(); + if (parent != null) { + return parent.toFile(); + } + break; + } + current = current.getParent(); + } + return dimensionRoot.getAbsoluteFile(); + } + + public static NamespacedKey keyFromName(String worldName) { + return keyFromName(worldName, levelRoot().getName()); + } + + public static NamespacedKey managedKeyFromName(String worldName) { + String requestedName = Objects.requireNonNull(worldName, "worldName").trim(); + if (requestedName.contains(":")) { + return managedKeyFromName(requestedName, DEFAULT_LEVEL_NAME); + } + return managedKeyFromName(requestedName, levelRoot().getName()); + } + + public static NamespacedKey managedKeyFromName(String worldName, String levelName) { + String requestedName = Objects.requireNonNull(worldName, "worldName").trim(); + if (requestedName.isEmpty()) { + throw new IllegalArgumentException("World name cannot be empty."); + } + if (requestedName.contains("/") || requestedName.contains("\\") || requestedName.contains("..")) { + throw new IllegalArgumentException("World name must be a safe single path segment."); + } + + NamespacedKey key; + if (requestedName.contains(":")) { + key = NamespacedKey.fromString(requestedName.toLowerCase(Locale.ENGLISH)); + if (key == null) { + throw new IllegalArgumentException("World identifier is invalid: " + requestedName); + } + } else { + // "iris worlds" and Multiverse print the Bukkit startup name (_iris_), so + // accept that form here instead of turning it into iris:_iris_. + key = keyFromConfiguredWorldName(requestedName, levelName); + } + + if (!IRIS_NAMESPACE.equals(key.getNamespace()) || !key.getKey().matches("[a-z0-9_-]+")) { + throw new IllegalArgumentException("Only Iris-managed dimension worlds can be changed."); + } + return key; + } + + public static NamespacedKey replacementKeyFromName(String requestedName, String levelName) { + String requested = Objects.requireNonNull(requestedName, "requestedName").trim(); + String configuredLevelName = Objects.requireNonNull(levelName, "levelName").trim(); + if (requested.isEmpty()) { + throw new IllegalArgumentException("World name cannot be empty."); + } + if (configuredLevelName.isEmpty()) { + throw new IllegalArgumentException("Configured level name cannot be empty."); + } + if (requested.contains("/") || requested.contains("\\") || requested.contains("..")) { + throw new IllegalArgumentException("World name must be a safe single path segment."); + } + + String normalized = requested.toLowerCase(Locale.ENGLISH); + if (normalized.contains(":")) { + int separator = normalized.indexOf(':'); + if (separator == 0 + || separator == normalized.length() - 1 + || separator != normalized.lastIndexOf(':')) { + throw new IllegalArgumentException("World identifier is invalid: " + requestedName); + } + NamespacedKey explicit = NamespacedKey.fromString(normalized); + if (explicit == null) { + throw new IllegalArgumentException("World identifier is invalid: " + requestedName); + } + return explicit; + } + String configuredIrisPrefix = configuredLevelName + "_" + IRIS_NAMESPACE + "_"; + if (requested.startsWith(configuredIrisPrefix)) { + return keyFromConfiguredWorldName(requested, configuredLevelName); + } + if (requested.equalsIgnoreCase(configuredLevelName)) { + return NamespacedKey.minecraft("overworld"); + } + if (requested.equalsIgnoreCase(configuredLevelName + "_nether")) { + return NamespacedKey.minecraft("the_nether"); + } + if (requested.equalsIgnoreCase(configuredLevelName + "_the_end")) { + return NamespacedKey.minecraft("the_end"); + } + return switch (normalized) { + case "main", "overworld" -> NamespacedKey.minecraft("overworld"); + case "nether", "the_nether" -> NamespacedKey.minecraft("the_nether"); + case "end", "the_end" -> NamespacedKey.minecraft("the_end"); + default -> new NamespacedKey(IRIS_NAMESPACE, normalized.replace(' ', '_')); + }; + } + + static NamespacedKey keyFromName(String worldName, String levelName) { + String name = Objects.requireNonNull(worldName, "worldName").trim(); + String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException("World name cannot be empty."); + } + if (name.equals(mainLevelName)) { + return NamespacedKey.minecraft("overworld"); + } + if (name.equals(mainLevelName + "_nether")) { + return NamespacedKey.minecraft("the_nether"); + } + if (name.equals(mainLevelName + "_the_end")) { + return NamespacedKey.minecraft("the_end"); + } + + String key = name.toLowerCase(Locale.ENGLISH).replace(' ', '_'); + return new NamespacedKey(IRIS_NAMESPACE, key); + } + + public static NamespacedKey keyFromConfiguredWorldName(String configuredWorldName, String levelName) { + String name = Objects.requireNonNull(configuredWorldName, "configuredWorldName").trim(); + String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException("Configured world name cannot be empty."); + } + if (mainLevelName.isEmpty()) { + throw new IllegalArgumentException("Level name cannot be empty."); + } + + String irisPrefix = mainLevelName + "_" + IRIS_NAMESPACE + "_"; + if (name.startsWith(irisPrefix)) { + return managedKeyFromName(IRIS_NAMESPACE + ":" + name.substring(irisPrefix.length()), mainLevelName); + } + return keyFromName(name, mainLevelName); + } + + public static String configuredWorldName(NamespacedKey key, String levelName) { + NamespacedKey worldKey = Objects.requireNonNull(key, "key"); + String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim(); + if (mainLevelName.isEmpty()) { + throw new IllegalArgumentException("Level name cannot be empty."); + } + if (NamespacedKey.minecraft("overworld").equals(worldKey)) { + return mainLevelName; + } + if (NamespacedKey.minecraft("the_nether").equals(worldKey)) { + return mainLevelName + "_nether"; + } + if (NamespacedKey.minecraft("the_end").equals(worldKey)) { + return mainLevelName + "_the_end"; + } + if (!IRIS_NAMESPACE.equals(worldKey.getNamespace()) || !worldKey.getKey().matches("[a-z0-9_-]+")) { + throw new IllegalArgumentException("Only safe Iris-managed world keys have a Bukkit startup name."); + } + return mainLevelName + "_" + worldKey.getNamespace() + "_" + worldKey.getKey(); + } + + public static String configuredWorldName(WorldSlotKey key, String levelName) { + WorldSlotKey worldKey = Objects.requireNonNull(key, "key"); + NamespacedKey namespacedKey = NamespacedKey.fromString(worldKey.toString()); + if (namespacedKey == null) { + throw new IllegalArgumentException("World key is invalid: " + worldKey); + } + return configuredWorldName(namespacedKey, levelName); + } + + public static String logicalName(WorldInfo world) { + return logicalName(WorldIdentity.key(world)); + } + + public static String logicalName(NamespacedKey key) { + NamespacedKey worldKey = Objects.requireNonNull(key, "key"); + if (IRIS_NAMESPACE.equals(worldKey.getNamespace())) { + return worldKey.getKey(); + } + return logicalName(worldKey, levelRoot().getName()); + } + + static String logicalName(NamespacedKey key, String levelName) { + NamespacedKey worldKey = Objects.requireNonNull(key, "key"); + String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim(); + if (NamespacedKey.minecraft("overworld").equals(worldKey)) { + return mainLevelName; + } + if (NamespacedKey.minecraft("the_nether").equals(worldKey)) { + return mainLevelName + "_nether"; + } + if (NamespacedKey.minecraft("the_end").equals(worldKey)) { + return mainLevelName + "_the_end"; + } + return worldKey.getKey(); + } + + public static File dimensionRoot(String worldName) { + return dimensionRoot(keyFromName(worldName)); + } + + public static File dimensionRoot(WorldInfo world) { + NamespacedKey key = WorldIdentity.key(world); + Optional loadedWorld = WorldIdentity.resolve(key); + if (loadedWorld.isPresent()) { + return loadedWorld.get().getWorldFolder().getAbsoluteFile(); + } + return dimensionRoot(key); + } + + public static File dimensionRoot(NamespacedKey key) { + return dimensionRoot(levelRoot(), key); + } + + public static File requireSafeManagedDimensionRoot(NamespacedKey key) { + return requireSafeManagedDimensionRoot(levelRoot(), key); + } + + public static File requireSafePersistentDimensionRoot(NamespacedKey key) { + return requireSafePersistentDimensionRoot(levelRoot(), key); + } + + static File requireSafePersistentDimensionRoot(File levelRoot, NamespacedKey key) { + NamespacedKey worldKey = Objects.requireNonNull(key, "key"); + WorldSlotKey slotKey = new WorldSlotKey(worldKey.getNamespace(), worldKey.getKey()); + return ExactWorldSlotPathPolicy.resolve( + Objects.requireNonNull(levelRoot, "levelRoot").toPath(), + slotKey + ).worldDirectory().toFile(); + } + + public static File requireSafeManagedDimensionRoot(File levelRoot, NamespacedKey key) { + NamespacedKey worldKey = Objects.requireNonNull(key, "key"); + if (!IRIS_NAMESPACE.equals(worldKey.getNamespace()) || !worldKey.getKey().matches("[a-z0-9_-]+")) { + throw new IllegalArgumentException("Only safe Iris-managed dimension worlds can be changed."); + } + + Path root = Objects.requireNonNull(levelRoot, "levelRoot").toPath().toAbsolutePath().normalize(); + Path dimensions = root.resolve("dimensions"); + Path namespace = dimensions.resolve(IRIS_NAMESPACE); + Path target = namespace.resolve(worldKey.getKey()).normalize(); + if (!Objects.equals(target.getParent(), namespace)) { + throw new IllegalArgumentException("World target escapes the Iris namespace root."); + } + for (Path path : new Path[]{dimensions, namespace, target}) { + if (Files.isSymbolicLink(path)) { + throw new IllegalArgumentException("World storage path contains a symbolic link: " + path); + } + } + return target.toFile(); + } + + /** + * True when the level root holds at least one Iris-managed world directory. Used by the failure paths + * that have to decide whether letting the server continue would put vanilla terrain into Iris storage. + */ + public static boolean hasManagedWorldStorage(File levelRoot) { + if (levelRoot == null) { + return false; + } + Path namespace = levelRoot.toPath() + .toAbsolutePath() + .normalize() + .resolve("dimensions") + .resolve(IRIS_NAMESPACE); + if (!Files.isDirectory(namespace, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + try (Stream entries = Files.list(namespace)) { + return entries.anyMatch(entry -> Files.isDirectory(entry, LinkOption.NOFOLLOW_LINKS)); + } catch (IOException unreadable) { + return false; + } + } + + public static boolean isExistingManagedDimensionRoot(File levelRoot, NamespacedKey key) { + try { + Path target = requireSafeManagedDimensionRoot(levelRoot, key).toPath(); + return Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS); + } catch (IllegalArgumentException exception) { + return false; + } + } + + public static File dimensionRoot(File levelRoot, NamespacedKey key) { + Path dimensionsRoot = Objects.requireNonNull(levelRoot, "levelRoot") + .toPath() + .toAbsolutePath() + .normalize() + .resolve("dimensions"); + NamespacedKey worldKey = Objects.requireNonNull(key, "key"); + Path namespaceRoot = dimensionsRoot.resolve(worldKey.getNamespace()).normalize(); + Path dimensionRoot = namespaceRoot.resolve(worldKey.getKey()).normalize(); + if (!dimensionRoot.startsWith(namespaceRoot)) { + throw new IllegalArgumentException("World key escapes its namespace storage root: " + worldKey); + } + return dimensionRoot.toFile(); + } + + public static Optional keyFromDimensionRoot(File levelRoot, File dimensionRoot) { + Path dimensionsPath = Objects.requireNonNull(levelRoot, "levelRoot") + .toPath() + .toAbsolutePath() + .normalize() + .resolve("dimensions"); + Path worldPath = Objects.requireNonNull(dimensionRoot, "dimensionRoot") + .toPath() + .toAbsolutePath() + .normalize(); + if (!worldPath.startsWith(dimensionsPath)) { + return Optional.empty(); + } + + Path relative = dimensionsPath.relativize(worldPath); + if (relative.getNameCount() < 2) { + return Optional.empty(); + } + + String namespace = relative.getName(0).toString(); + StringBuilder key = new StringBuilder(); + for (int i = 1; i < relative.getNameCount(); i++) { + if (!key.isEmpty()) { + key.append('/'); + } + key.append(relative.getName(i)); + } + + return Optional.ofNullable(NamespacedKey.fromString(namespace + ":" + key)); + } + + public static File packRoot(NamespacedKey key) { + return new File(dimensionRoot(key), "iris/pack"); + } + + public static File requireFrozenDimensionRoot( + File worldContainer, + File levelRoot, + String bukkitWorldName, + NamespacedKey key + ) { + NamespacedKey worldKey = Objects.requireNonNull(key, "key"); + return frozenDimensionRoot(worldContainer, levelRoot, bukkitWorldName, worldKey) + .orElseThrow(() -> new IllegalStateException( + "Frozen Iris world storage is missing for " + worldKey + ".")); + } + + public static Optional frozenDimensionRoot( + File worldContainer, + File levelRoot, + String bukkitWorldName, + NamespacedKey key + ) { + File requiredLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot").getAbsoluteFile(); + NamespacedKey worldKey = Objects.requireNonNull(key, "key"); + String requiredWorldName = Objects.requireNonNull(bukkitWorldName, "bukkitWorldName").trim(); + if (requiredWorldName.isEmpty()) { + throw new IllegalArgumentException("Bukkit world name cannot be empty."); + } + + File directRoot = dimensionRoot(requiredLevelRoot, worldKey); + boolean directExists = isExistingSafeDimensionRoot(requiredLevelRoot.toPath(), directRoot.toPath()); + if (!IRIS_NAMESPACE.equals(worldKey.getNamespace())) { + return directExists ? Optional.of(directRoot) : Optional.empty(); + } + + String configuredName = configuredWorldName(worldKey, requiredLevelRoot.getName()); + if (!configuredName.equals(requiredWorldName)) { + return directExists ? Optional.of(directRoot) : Optional.empty(); + } + + File configuredRoot = configuredDimensionRoot(worldContainer, requiredLevelRoot, worldKey); + boolean configuredExists = isExistingSafeDimensionRoot( + Objects.requireNonNull(worldContainer, "worldContainer").toPath(), + configuredRoot.toPath() + ); + if (directExists == configuredExists) { + if (directExists) { + throw new IllegalStateException("Frozen Iris world storage is ambiguous for " + worldKey + "."); + } + return Optional.empty(); + } + return Optional.of(directExists ? directRoot : configuredRoot); + } + + public static File configuredLevelRoot(File worldContainer, File levelRoot, NamespacedKey key) { + Path container = Objects.requireNonNull(worldContainer, "worldContainer") + .toPath() + .toAbsolutePath() + .normalize(); + String configuredName = configuredWorldName( + Objects.requireNonNull(key, "key"), + Objects.requireNonNull(levelRoot, "levelRoot").getName() + ); + Path configuredLevelRoot = container.resolve(configuredName).normalize(); + if (!Objects.equals(configuredLevelRoot.getParent(), container)) { + throw new IllegalStateException("Configured Bukkit world storage escapes the world container."); + } + isExistingSafeDimensionRoot(container, configuredLevelRoot.resolve("dimensions")); + return configuredLevelRoot.toFile(); + } + + public static File configuredDimensionRoot(File worldContainer, File levelRoot, NamespacedKey key) { + File configuredLevelRoot = configuredLevelRoot(worldContainer, levelRoot, key); + File configuredRoot = dimensionRoot(configuredLevelRoot, key); + isExistingSafeDimensionRoot( + Objects.requireNonNull(worldContainer, "worldContainer").toPath(), + configuredRoot.toPath() + ); + return configuredRoot; + } + + public static File requireFrozenPackRoot(File dimensionRoot) { + Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot") + .toPath() + .toAbsolutePath() + .normalize(); + Path irisRoot = root.resolve("iris"); + Path packRoot = irisRoot.resolve("pack"); + for (Path path : new Path[]{root, irisRoot, packRoot}) { + if (Files.isSymbolicLink(path)) { + throw new IllegalStateException("Frozen Iris pack path contains a symbolic link: " + path); + } + if (Files.exists(path, LinkOption.NOFOLLOW_LINKS) + && !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IllegalStateException("Frozen Iris pack path is not a directory: " + path); + } + } + if (!Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS)) { + throw new IllegalStateException("Frozen Iris pack snapshot is missing: " + packRoot); + } + return packRoot.toFile(); + } + + private static boolean isExistingSafeDimensionRoot(Path storageRoot, Path dimensionRoot) { + Path root = storageRoot.toAbsolutePath().normalize(); + Path target = dimensionRoot.toAbsolutePath().normalize(); + if (Files.isSymbolicLink(root)) { + throw new IllegalStateException("Iris world storage root is a symbolic link: " + root); + } + if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { + throw new IllegalStateException("Iris world storage root is not a directory: " + root); + } + if (!target.startsWith(root) || Objects.equals(target, root)) { + throw new IllegalStateException("Iris world storage escapes its expected root: " + target); + } + + Path relative = root.relativize(target); + Path current = root; + for (Path segment : relative) { + current = current.resolve(segment); + if (Files.isSymbolicLink(current)) { + throw new IllegalStateException("Iris world storage contains a symbolic link: " + current); + } + if (Files.exists(current, LinkOption.NOFOLLOW_LINKS) + && !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS)) { + throw new IllegalStateException("Iris world storage path is not a directory: " + current); + } + } + return Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisWorlds.java b/core/src/main/java/art/arcane/iris/core/IrisWorlds.java new file mode 100644 index 000000000..35815be4d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisWorlds.java @@ -0,0 +1,407 @@ +package art.arcane.iris.core; + +import art.arcane.iris.core.lifecycle.MissingWorldStorageLog; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackDownloader; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.util.common.misc.ServerProperties; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.io.IO; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +public class IrisWorlds { + private static final String IRIS_GENERATOR_NAME = "iris"; + private static final String IRIS_GENERATOR_PREFIX = "iris:"; + private static final String IRIS_NAMESPACE = "iris"; + private static final AtomicCache cache = new AtomicCache<>(); + private static final Set UNUSABLE_STORAGE_REPORTED = ConcurrentHashMap.newKeySet(); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Type TYPE = TypeToken.getParameterized(KMap.class, String.class, String.class).getType(); + private final Path levelRoot; + private final Path registryFile; + private final KMap worlds; + private volatile boolean dirty = false; + + private IrisWorlds(Path levelRoot, KMap worlds) { + this.levelRoot = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize(); + registryFile = registryFile(this.levelRoot); + this.worlds = new KMap<>(); + // One unreadable entry drops itself. This runs behind a static cache whose callers all dereference + // the result immediately, so anything thrown here stops Iris from enabling for every world. + worlds.forEach((identity, type) -> { + try { + this.worlds.put(WorldIdentity.parse(identity).toString(), type); + } catch (IllegalArgumentException unreadable) { + IrisLogging.warn("Dropping unreadable worlds.json entry %s: %s", identity, unreadable.getMessage()); + dirty = true; + } + }); + readBukkitWorlds(this.levelRoot).forEach((name, type) -> put0( + IrisWorldStorage.keyFromConfiguredWorldName(name, this.levelRoot.getFileName().toString()).toString(), + type)); + save(); + } + + /** + * The world registry, never null. + *

+ * A failure here used to be swallowed into a null return, and every caller dereferences the result + * immediately, so a single unusable world folder turned into an NPE that stopped Iris from enabling - + * and a server whose Iris worlds then generated vanilla terrain over their own region files. Per-world + * storage problems are isolated in {@link #clean()} and {@link #loadDimension(String, String)}; anything + * that gets past those is a real failure and is raised, not hidden. + */ + public static IrisWorlds get() { + return cache.aquireOnceOrThrow(() -> { + Path levelRoot = IrisWorldStorage.levelRoot().toPath().toAbsolutePath().normalize(); + File file = registryFile(levelRoot).toFile(); + if (!file.exists()) { + return new IrisWorlds(levelRoot, new KMap<>()); + } + + try { + String json = IO.readAll(file); + KMap worlds = GSON.fromJson(json, TYPE); + return new IrisWorlds(levelRoot, Objects.requireNonNullElseGet(worlds, KMap::new)); + } catch (Throwable e) { + IrisLogging.error("Failed to read worlds.json for level root " + levelRoot + + "; rebuilding it from bukkit.yml."); + IrisLogging.reportError(e); + } + + return new IrisWorlds(levelRoot, new KMap<>()); + }); + } + + public synchronized void put(String identity, String type) { + String canonicalIdentity = WorldIdentity.parse(identity).toString(); + String requiredType = Objects.requireNonNull(type, "type"); + String previous = worlds.put(canonicalIdentity, requiredType); + if (requiredType.equals(previous)) { + return; + } + dirty = true; + try { + saveOrThrow(); + } catch (IOException e) { + if (previous == null) { + worlds.remove(canonicalIdentity); + } else { + worlds.put(canonicalIdentity, previous); + } + dirty = true; + throw new UncheckedIOException("Failed to persist Iris world registry entry for " + canonicalIdentity, e); + } + } + + public synchronized boolean remove(String identity) { + String canonicalIdentity = WorldIdentity.parse(identity).toString(); + String previous = worlds.remove(canonicalIdentity); + if (previous == null) { + return false; + } + dirty = true; + try { + saveOrThrow(); + return true; + } catch (IOException e) { + worlds.put(canonicalIdentity, previous); + dirty = true; + throw new UncheckedIOException("Failed to remove Iris world registry entry for " + canonicalIdentity, e); + } + } + + private void put0(String identity, String type) { + String canonicalIdentity = WorldIdentity.parse(identity).toString(); + String old = worlds.put(canonicalIdentity, type); + if (!type.equals(old)) + dirty = true; + } + + public synchronized KMap getWorlds() { + clean(); + KMap result = new KMap<>(); + readBukkitWorlds(levelRoot).forEach((name, type) -> result.put( + IrisWorldStorage.keyFromConfiguredWorldName(name, levelRoot.getFileName().toString()).toString(), + type)); + return result.put(worlds); + } + + public Stream getPacks() { + return getDimensions() + .map(IrisDimension::getLoader) + .filter(Objects::nonNull); + } + + /** + * The dimension one registered world generates from, or null when its pack snapshot cannot supply it. + * Same resolution {@link #getDimensions()} uses, for callers that hold a single world identity. + */ + public IrisDimension getDimension(String worldIdentity, String id) { + return loadDimension(worldIdentity, id); + } + + public Stream getDimensions() { + return getWorlds() + .entrySet() + .stream() + .map(entry -> loadDimension(entry.getKey(), entry.getValue())) + .filter(Objects::nonNull); + } + + /** + * Drops registry entries whose pack snapshot no longer backs them. + *

+ * A world whose storage is present but unusable is kept and reported instead of dropped: the entry is + * what lets {@code /iris remove} find the world, and dropping it would not stop the server from loading + * the folder anyway. It is excluded from {@link #getDimensions()} so one broken world cannot fail the + * whole registry - that failure used to propagate out of the constructor and null out {@link #get()}. + */ + public synchronized void clean() { + boolean removed = worlds.entrySet().removeIf(entry -> { + try { + Optional packRoot = packRoot(entry.getKey()); + return packRoot.isEmpty() + || !new File(packRoot.get(), "dimensions/" + entry.getValue() + ".json").exists(); + } catch (IllegalArgumentException e) { + return true; + } catch (IllegalStateException e) { + warnUnusableStorage(entry.getKey(), e); + return false; + } + }); + dirty = dirty || removed; + } + + public synchronized void save() { + try { + saveOrThrow(); + } catch (IOException e) { + IrisLogging.error("Failed to save worlds.json!"); + IrisLogging.reportError(e); + } + } + + private void saveOrThrow() throws IOException { + clean(); + if (!dirty) { + return; + } + + Path target = registryFile; + Path parent = target.getParent(); + if (parent == null) { + throw new IOException("worlds.json target has no parent: " + target); + } + Files.createDirectories(parent); + Path staged = Files.createTempFile(parent, ".iris-worlds-", ".json"); + try { + Files.writeString(staged, GSON.toJson(worlds, TYPE), StandardCharsets.UTF_8); + try (FileChannel channel = FileChannel.open(staged, StandardOpenOption.WRITE)) { + channel.force(true); + } + try { + Files.move(staged, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException exception) { + Files.move(staged, target, StandardCopyOption.REPLACE_EXISTING); + } + dirty = false; + } finally { + Files.deleteIfExists(staged); + } + } + + public static Long readBukkitWorldSeed(String world) { + YamlConfiguration bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML); + ConfigurationSection worlds = bukkit.getConfigurationSection("worlds"); + if (worlds == null || !worlds.contains(world + ".seed")) { + return null; + } + + return worlds.getLong(world + ".seed"); + } + + public static KMap readBukkitWorlds() { + return readBukkitWorlds(IrisWorldStorage.levelRoot().toPath()); + } + + static Path registryFile(Path levelRoot) { + return Objects.requireNonNull(levelRoot, "levelRoot") + .toAbsolutePath() + .normalize() + .resolve("iris") + .resolve("worlds.json"); + } + + static KMap filterBukkitWorldsByStorage( + Path levelRoot, + Map configuredWorlds + ) { + Path root = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize(); + Path levelNamePath = root.getFileName(); + if (levelNamePath == null) { + throw new IllegalArgumentException("Selected level root has no name: " + root); + } + + KMap result = new KMap<>(); + for (Map.Entry entry : Objects.requireNonNull(configuredWorlds, "configuredWorlds").entrySet()) { + String configuredWorldName = entry.getKey(); + NamespacedKey worldKey; + try { + worldKey = IrisWorldStorage.keyFromConfiguredWorldName( + configuredWorldName, + levelNamePath.toString()); + if (!IrisWorldStorage.configuredWorldName(worldKey, levelNamePath.toString()) + .equals(configuredWorldName)) { + continue; + } + } catch (IllegalArgumentException notAnIrisWorldName) { + // A bukkit.yml name that has no Iris identity is somebody else's world, never this + // registry's, and must not stop the whole registry from being built. + continue; + } + Path worldContainer = root.getParent(); + if (worldContainer == null) { + throw new IllegalArgumentException("Selected level root has no world container: " + root); + } + boolean storagePresent; + try { + storagePresent = IrisWorldStorage.frozenDimensionRoot( + worldContainer.toFile(), + root.toFile(), + configuredWorldName, + worldKey + ).isPresent(); + } catch (IllegalStateException unusableStorage) { + storagePresent = false; + } + if (storagePresent) { + result.put(configuredWorldName, entry.getValue()); + continue; + } + // Vanilla slots are absent on every server that never created them; only an Iris world that + // bukkit.yml still points at is an orphan worth reporting. + if (IRIS_NAMESPACE.equals(worldKey.getNamespace())) { + MissingWorldStorageLog.warnOnce( + configuredWorldName, + root.resolve("dimensions").resolve(IRIS_NAMESPACE).resolve(worldKey.getKey())); + } + } + return result; + } + + private static KMap readBukkitWorlds(Path levelRoot) { + YamlConfiguration bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML); + ConfigurationSection worlds = bukkit.getConfigurationSection("worlds"); + if (worlds == null) return new KMap<>(); + + KMap result = new KMap<>(); + String defaultWorldType = IrisSettings.get().getGenerator().getDefaultWorldType(); + for (String world : worlds.getKeys(false)) { + String loadKey = generatorLoadKey(worlds.getString(world + ".generator"), defaultWorldType); + if (loadKey == null) continue; + + result.put(world, loadKey); + } + + return filterBukkitWorldsByStorage(levelRoot, result); + } + + /** + * Maps a bukkit.yml generator string onto the Iris pack it selects, or null when the string is + * not Iris. Bukkit matches generator plugin names case-insensitively, so the prefix does too. + */ + static String generatorLoadKey(String generator, String defaultWorldType) { + if (generator == null) return null; + if (generator.equalsIgnoreCase(IRIS_GENERATOR_NAME)) return defaultWorldType; + + int prefixLength = IRIS_GENERATOR_PREFIX.length(); + if (generator.length() > prefixLength + && generator.regionMatches(true, 0, IRIS_GENERATOR_PREFIX, 0, prefixLength)) { + return generator.substring(prefixLength); + } + return null; + } + + private Optional packRoot(String worldIdentity) { + NamespacedKey worldKey = WorldIdentity.parse(worldIdentity); + Path worldContainer = levelRoot.getParent(); + if (worldContainer == null) { + throw new IllegalStateException("Selected level root has no world container: " + levelRoot); + } + String configuredWorldName = IrisWorldStorage.configuredWorldName( + worldKey, + levelRoot.getFileName().toString() + ); + Optional dimensionRoot = IrisWorldStorage.frozenDimensionRoot( + worldContainer.toFile(), + levelRoot.toFile(), + configuredWorldName, + worldKey + ); + return dimensionRoot.map(IrisWorldStorage::requireFrozenPackRoot); + } + + private static void warnUnusableStorage(String worldIdentity, IllegalStateException failure) { + if (!UNUSABLE_STORAGE_REPORTED.add(worldIdentity)) { + return; + } + IrisLogging.error("Iris world %s has unusable world storage and is excluded: %s", + worldIdentity, failure.getMessage()); + IrisLogging.error("Restore its iris/pack snapshot, or drop the world with /iris remove world=%s delete=true.", + worldIdentity); + } + + private IrisDimension loadDimension(String worldIdentity, String id) { + File pack; + try { + pack = packRoot(worldIdentity).orElse(null); + } catch (IllegalStateException unusableStorage) { + warnUnusableStorage(worldIdentity, unusableStorage); + return null; + } + IrisDimension dimension = pack == null ? null : IrisData.get(pack).getDimensionLoader().load(id); + if (dimension == null) { + dimension = IrisData.loadAnyDimension(id, null); + } + if (dimension == null) { + File packsRoot = IrisPlatforms.get().packsFolderNoCreate(); + if (PackDownloader.isPackPresent(packsRoot, id)) { + IrisLogging.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath() + + " but its dimension failed to load; not redownloading. Fix or delete the pack folder."); + return null; + } + IrisLogging.warn("Unable to find dimension type " + id + ". Install it with " + + PackDownloader.downloadCommandFor(id) + " and restart the server."); + } + return dimension; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java new file mode 100644 index 000000000..b6e4ef496 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -0,0 +1,1111 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.datapack.DatapackIngestService; +import art.arcane.iris.core.datapack.DatapackIngestService.ReapplyOutcome; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding; +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.core.nms.datapack.IDataFixer; +import art.arcane.iris.core.pack.AtomicDirectoryPublisher; +import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.misc.ServerProperties; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import lombok.NonNull; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.volmlib.util.localization.MessageArgument; +public class ServerConfigurator { + private static final Object DATAPACK_INSTALL_LOCK = new Object(); + private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace"; + private static final String COMPILER_INPUT_FINGERPRINT_CACHE = "datapack-compiler-input-fingerprint"; + private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024; + private static volatile boolean loadedDatapackRuntimeReady; + private static volatile String loadedDatapackCompilerInputFingerprint = ""; + private static volatile Map loadedDatapackRegistryRequirements = Map.of(); + private static volatile long loadedDatapackRuntimeGeneration; + private static volatile boolean loadedDatapackRestartRequired; + + public static void configure() { + synchronized (DATAPACK_INSTALL_LOCK) { + invalidateLoadedDatapackRuntime(); + loadedDatapackCompilerInputFingerprint = ""; + loadedDatapackRegistryRequirements = Map.of(); + loadedDatapackRestartRequired = false; + } + IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration(); + if (s.isConfigureSpigotTimeoutTime()) { + J.attempt(ServerConfigurator::increaseKeepAliveSpigot); + } + + if (s.isConfigurePaperWatchdogDelay()) { + J.attempt(ServerConfigurator::increasePaperWatchdog); + } + + if (DefaultPackBootstrapProvisioner.wasProvisionedThisStartup()) { + loadedDatapackRuntimeReady = !IrisSettings.get().getGeneral().adjustVanillaHeight + && pinLoadedDatapackCompilerInputs( + DefaultPackBootstrapProvisioner.compilerInputFingerprintThisStartup()) + && pinLoadedDatapackRegistryRequirements(); + IrisLogging.info("Paper loaded the Iris datapack during bootstrap; skipping the legacy startup install."); + } else { + DatapackInstallResult result = installDataPacks(true); + loadedDatapackRuntimeReady = result.succeeded() + && !result.restartRequired() + && pinLoadedDatapackCompilerInputs() + && pinLoadedDatapackRegistryRequirements(); + if (result.restartRequired()) { + IrisLogging.warn("Iris datapack changes require another server restart before worlds can use them."); + } + } + } + + public static boolean isLoadedDatapackRuntimeReady(IrisDimension dimension) { + IrisDimension requiredDimension = Objects.requireNonNull(dimension, "Iris dimension"); + if (!loadedDatapackRuntimeReady + || loadedDatapackRestartRequired + || !BukkitPlatform.hasPlugin() + || !BukkitPlatform.plugin().isEnabled()) { + return false; + } + try { + if (!INMS.get().supportsIrisWorldGeneration() + || INMS.get().missingDimensionTypes(requiredDimension.getDimensionTypeKey())) { + return false; + } + Map requiredRegistryEntries = + IrisDatapackCompiler.computeRegistryRequirements(requiredDimension, resolveDataFixer()); + return loadedRegistrySatisfies( + loadedDatapackRegistryRequirements, + requiredRegistryEntries); + } catch (IOException | RuntimeException exception) { + IrisLogging.reportError("Unable to verify loaded Iris datapack registry requirements.", exception); + return false; + } + } + + public static LoadedDatapackRuntimeInvalidation invalidateLoadedDatapackRuntime() { + synchronized (DATAPACK_INSTALL_LOCK) { + boolean wasReady = loadedDatapackRuntimeReady; + String fingerprint = loadedDatapackCompilerInputFingerprint; + loadedDatapackRuntimeReady = false; + loadedDatapackRuntimeGeneration++; + return new LoadedDatapackRuntimeInvalidation( + loadedDatapackRuntimeGeneration, + wasReady, + fingerprint); + } + } + + public static void requireDatapackRestart() { + synchronized (DATAPACK_INSTALL_LOCK) { + invalidateLoadedDatapackRuntime(); + loadedDatapackRestartRequired = true; + } + IrisStartupValidation.requireRestart( + "Iris datapack changes require a restart before player admission or world creation."); + } + + public static void restoreLoadedDatapackRuntimeIfUnchanged( + LoadedDatapackRuntimeInvalidation invalidation + ) { + if (invalidation == null + || !invalidation.wasReady() + || invalidation.fingerprint().isBlank()) { + return; + } + String currentFingerprint; + try { + currentFingerprint = computeCurrentDatapackCompilerInputFingerprint(resolveDataFixer()); + } catch (IOException | RuntimeException exception) { + IrisLogging.reportError("Unable to restore loaded Iris datapack runtime readiness.", exception); + return; + } + synchronized (DATAPACK_INSTALL_LOCK) { + if (loadedDatapackRuntimeGeneration != invalidation.generation() + || loadedDatapackRuntimeReady + || loadedDatapackRestartRequired + || !reusableRuntimeFingerprint(invalidation.fingerprint(), currentFingerprint)) { + return; + } + loadedDatapackCompilerInputFingerprint = currentFingerprint; + loadedDatapackRuntimeReady = true; + } + } + + private static void increaseKeepAliveSpigot() throws IOException, InvalidConfigurationException { + File spigotConfig = new File("spigot.yml"); + FileConfiguration f = new YamlConfiguration(); + f.load(spigotConfig); + long tt = f.getLong("settings.timeout-time"); + + long spigotTimeout = TimeUnit.MINUTES.toSeconds(5); + + if (tt < spigotTimeout) { + IrisLogging.warn("Updating spigot.yml timeout-time: " + tt + " -> " + spigotTimeout + " (5 minutes)"); + IrisLogging.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value."); + f.set("settings.timeout-time", spigotTimeout); + f.save(spigotConfig); + } + } + + private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException { + File spigotConfig = new File("config/paper-global.yml"); + FileConfiguration f = new YamlConfiguration(); + f.load(spigotConfig); + long tt = f.getLong("watchdog.early-warning-delay"); + + long watchdog = TimeUnit.MINUTES.toMillis(3); + if (tt < watchdog) { + IrisLogging.warn("Updating paper.yml watchdog early-warning-delay: " + tt + " -> " + watchdog + " (3 minutes)"); + IrisLogging.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value."); + f.set("watchdog.early-warning-delay", watchdog); + f.save(spigotConfig); + } + } + + public static KList getDatapacksFolder() { + return new KList().qadd(new File(IrisWorldStorage.levelRoot(), "datapacks")); + } + + public static KList getIrisDatapackRoots() { + KList roots = new KList<>(); + for (File datapacksFolder : getDatapacksFolder()) { + roots.add(new File(datapacksFolder, "iris")); + } + return roots; + } + + public static DatapackInstallResult installDataPacks(boolean fullInstall) { + return installDataPacks(resolveDataFixer(), fullInstall); + } + + public static DatapackInstallResult installDataPacks(IDataFixer fixer, boolean fullInstall) { + synchronized (DATAPACK_INSTALL_LOCK) { + return installDataPacksLocked(fixer, fullInstall); + } + } + + private static DatapackInstallResult installDataPacksLocked(IDataFixer fixer, boolean fullInstall) { + if (fixer == null) { + IrisLogging.error("Unable to install datapacks, fixer is null!"); + return DatapackInstallResult.failedResult(); + } + KList datapacksFolders = getDatapacksFolder(); + ReapplyOutcome reapply = DatapackIngestService.reapplyFromStaging(datapacksFolders); + if (!reapply.succeeded()) { + return DatapackInstallResult.failedResult(); + } + return compileDataPacksLocked(fixer, fullInstall, reapply); + } + + private static DatapackInstallResult compileDataPacksLocked( + IDataFixer fixer, + boolean fullInstall, + ReapplyOutcome reapply + ) { + if (!Objects.requireNonNull(reapply, "External datapack reapply outcome").succeeded()) { + return DatapackInstallResult.failedResult(); + } + if (fixer == null) { + IrisLogging.error("Unable to install datapacks, fixer is null!"); + return DatapackInstallResult.failedResult(); + } + if (fullInstall) { + IrisLogging.info("Checking Data Packs..."); + } else { + IrisLogging.debug("Checking Data Packs..."); + } + List packRoots; + List bindings; + try { + packRoots = collectCompilerPackRoots(); + bindings = collectConfiguredLevelStemBindings(); + } catch (IOException exception) { + IrisLogging.reportError("Unable to resolve Iris datapack compiler inputs.", exception); + return DatapackInstallResult.failedResult(); + } + + KList liveRoots = getIrisDatapackRoots(); + KList stagedRoots = new KList<>(); + List stagedPaths = new ArrayList<>(liveRoots.size()); + List publications = new ArrayList<>(liveRoots.size()); + try { + for (File liveRoot : liveRoots) { + Path target = liveRoot.toPath().toAbsolutePath().normalize(); + Path parent = target.getParent(); + if (parent == null) { + throw new IOException("Iris datapack root has no parent: " + target); + } + Files.createDirectories(parent); + Path staged = parent.resolve(".iris-compile-" + UUID.randomUUID()); + Files.createDirectories(staged); + stagedPaths.add(staged); + stagedRoots.add(staged.toFile()); + } + IrisDatapackCompiler.compile( + packRoots, + stagedRoots, + bindings, + fixer, + IrisSettings.get().getGeneral().adjustVanillaHeight + ); + for (int i = 0; i < liveRoots.size(); i++) { + publications.add(AtomicDirectoryPublisher.publish( + stagedRoots.get(i).toPath(), + liveRoots.get(i).toPath() + )); + } + for (AtomicDirectoryPublisher.Publication publication : publications) { + publication.commit(); + try { + publication.cleanupBackup(); + } catch (IOException cleanupFailure) { + IrisLogging.warn("Iris datapack was committed but its backup could not be removed: " + + cleanupFailure.getMessage()); + } + } + } catch (IOException | RuntimeException e) { + closePublications(publications, e); + IrisLogging.reportError("Unable to compile Iris datapacks", e); + return DatapackInstallResult.failedResult(); + } finally { + for (Path stagedPath : stagedPaths) { + try { + AtomicDirectoryPublisher.deleteTree(stagedPath); + } catch (IOException cleanupFailure) { + IrisLogging.warn("Failed to clean Iris datapack compilation stage " + stagedPath + ": " + + cleanupFailure.getMessage()); + } + } + } + if (fullInstall) { + IrisLogging.info("Data Packs Setup!"); + } else { + IrisLogging.debug("Data Packs Setup!"); + } + + boolean verifiedRestartRequired = fullInstall && verifyDataPacksPost(); + boolean restartRequired = fullInstall && (reapply.changed() || verifiedRestartRequired); + return restartRequired + ? DatapackInstallResult.restartRequiredResult() + : DatapackInstallResult.readyResult(); + } + + private static IDataFixer resolveDataFixer() { + IDataFixer fixer = DataVersion.getDefault(); + if (fixer != null) { + return fixer; + } + DataVersion fallback = DataVersion.getLatest(); + IrisLogging.warn("Primary datapack fixer was null, forcing latest fixer: " + fallback.getVersion()); + return fallback.get(); + } + + private static void closePublications(List publications, Throwable failure) { + for (int i = publications.size() - 1; i >= 0; i--) { + try { + publications.get(i).close(); + } catch (IOException rollbackFailure) { + failure.addSuppressed(rollbackFailure); + } + } + } + + public static DatapackInstallResult installDataPacksIfChanged(boolean fullInstall) { + return installDataPacksIfChanged(fullInstall, null); + } + + public static DatapackInstallResult installDataPacksIfChanged( + boolean fullInstall, + BiConsumer timingConsumer + ) { + synchronized (DATAPACK_INSTALL_LOCK) { + long totalStart = System.nanoTime(); + File cacheFile = new File( + IrisPlatforms.get().dataFolder("cache"), + COMPILER_INPUT_FINGERPRINT_CACHE); + String cached = readCompilerInputFingerprintCache(cacheFile.toPath()); + long recoveryStart = System.nanoTime(); + ReapplyOutcome reapply = DatapackIngestService.reapplyFromStaging(getDatapacksFolder()); + reportTiming(timingConsumer, "datapack_external_recovery", recoveryStart); + if (!reapply.succeeded()) { + reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart); + return DatapackInstallResult.failedResult(); + } + if (fullInstall && loadedDatapackRestartRequired) { + reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart); + return DatapackInstallResult.restartRequiredResult(); + } + String current; + long fingerprintStart = System.nanoTime(); + try { + current = restoredCompilerInputFingerprint(); + if (current.isBlank()) { + current = computeCurrentDatapackCompilerInputFingerprint(resolveDataFixer()); + } + } catch (IOException | RuntimeException exception) { + reportTiming(timingConsumer, "datapack_compiler_input_fingerprint", fingerprintStart); + reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart); + IrisLogging.reportError("Unable to fingerprint Iris datapack compiler inputs safely", exception); + return DatapackInstallResult.failedResult(); + } + reportTiming(timingConsumer, "datapack_compiler_input_fingerprint", fingerprintStart); + boolean loadedCompilerInputsChanged = !loadedDatapackCompilerInputFingerprint.isBlank() + && !reusableRuntimeFingerprint( + loadedDatapackCompilerInputFingerprint, + current); + if (!current.isEmpty() && current.equals(cached)) { + IrisLogging.debug("Data packs unchanged, skipping install."); + DatapackInstallResult result = fullInstall && loadedCompilerInputsChanged + ? DatapackInstallResult.restartRequiredResult() + : resultForUnchangedFingerprint(fullInstall, reapply); + if (result.restartRequired()) { + requireDatapackRestart(); + } + reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart); + return result; + } + long compileStart = System.nanoTime(); + DatapackInstallResult result = compileDataPacksLocked( + resolveDataFixer(), + fullInstall, + reapply); + if (fullInstall && loadedCompilerInputsChanged && result.succeeded()) { + result = DatapackInstallResult.restartRequiredResult(); + } + if (result.restartRequired()) { + requireDatapackRestart(); + } + reportTiming(timingConsumer, "datapack_compile_publish", compileStart); + if (result.succeeded() && !result.restartRequired()) { + writeCompilerInputFingerprintCache(cacheFile.toPath(), current); + } + reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart); + return result; + } + } + + private static List collectCompilerPackRoots() throws IOException { + return IrisDatapackCompiler.collectPackRoots( + IrisPlatforms.get().dataFolder().toPath(), + IrisWorldStorage.levelRoot().toPath()); + } + + private static List collectConfiguredLevelStemBindings() throws IOException { + File levelRoot = IrisWorldStorage.levelRoot(); + String levelId = levelRoot.getName(); + if (levelId.isBlank()) { + throw new IOException("Configured level root has no Paper startup level id: " + levelRoot); + } + return BukkitWorldConfiguration.readIrisGeneratorBindings( + ServerProperties.BUKKIT_YML, + levelId, + levelRoot.toPath() + ); + } + + private static String computeCurrentDatapackCompilerInputFingerprint(IDataFixer fixer) throws IOException { + return IrisDatapackCompiler.computeInputFingerprint( + IrisDatapackCompiler.collectCompilerInputRoots( + IrisPlatforms.get().dataFolder().toPath(), + IrisWorldStorage.levelRoot().toPath()), + collectConfiguredLevelStemBindings(), + Objects.requireNonNull(fixer, "Datapack fixer"), + IrisSettings.get().getGeneral().adjustVanillaHeight); + } + + static String restoredCompilerInputFingerprint() { + if (!loadedDatapackRuntimeReady || loadedDatapackRestartRequired) { + return ""; + } + return Objects.requireNonNullElse(loadedDatapackCompilerInputFingerprint, ""); + } + + private static boolean pinLoadedDatapackCompilerInputs() { + return pinLoadedDatapackCompilerInputs(null); + } + + private static boolean pinLoadedDatapackCompilerInputs(String expectedFingerprint) { + if (loadedDatapackRestartRequired) { + return false; + } + try { + String fingerprint = computeCurrentDatapackCompilerInputFingerprint(resolveDataFixer()); + if (fingerprint.isBlank() + || expectedFingerprint != null + && !reusableRuntimeFingerprint(expectedFingerprint, fingerprint)) { + return false; + } + loadedDatapackCompilerInputFingerprint = fingerprint; + File cacheFile = new File( + IrisPlatforms.get().dataFolder("cache"), + COMPILER_INPUT_FINGERPRINT_CACHE); + writeCompilerInputFingerprintCache(cacheFile.toPath(), fingerprint); + return true; + } catch (IOException | RuntimeException exception) { + loadedDatapackCompilerInputFingerprint = ""; + IrisLogging.reportError("Unable to pin loaded Iris datapack compiler inputs.", exception); + return false; + } + } + + private static boolean pinLoadedDatapackRegistryRequirements() { + if (loadedDatapackRestartRequired) { + return false; + } + try { + loadedDatapackRegistryRequirements = IrisDatapackCompiler.computeRegistryRequirements( + collectCompilerPackRoots(), + resolveDataFixer()); + return true; + } catch (IOException | RuntimeException exception) { + loadedDatapackRegistryRequirements = Map.of(); + IrisLogging.reportError("Unable to pin loaded Iris datapack registry requirements.", exception); + return false; + } + } + + static boolean loadedRegistrySatisfies( + Map loadedRequirements, + Map requiredEntries + ) { + if (loadedRequirements == null || requiredEntries == null || requiredEntries.isEmpty()) { + return false; + } + for (Map.Entry entry : requiredEntries.entrySet()) { + if (!entry.getValue().equals(loadedRequirements.get(entry.getKey()))) { + return false; + } + } + return true; + } + + static boolean reusableRuntimeFingerprint(String loadedFingerprint, String currentFingerprint) { + return loadedFingerprint != null + && !loadedFingerprint.isBlank() + && loadedFingerprint.equals(currentFingerprint); + } + + private static void reportTiming( + BiConsumer timingConsumer, + String phase, + long startedAtNanos + ) { + if (timingConsumer == null) { + return; + } + try { + timingConsumer.accept(phase, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos)); + } catch (Throwable exception) { + IrisLogging.reportError("Datapack timing consumer failed during phase \"" + phase + "\".", exception); + } + } + + static DatapackInstallResult resultForUnchangedFingerprint( + boolean fullInstall, + ReapplyOutcome reapply + ) { + if (!Objects.requireNonNull(reapply, "External datapack reapply outcome").succeeded()) { + return DatapackInstallResult.failedResult(); + } + if (!reapply.changed()) { + return DatapackInstallResult.unchangedResult(); + } + return fullInstall + ? DatapackInstallResult.restartRequiredResult() + : DatapackInstallResult.readyResult(); + } + + static PackFingerprint resolvePostRecoveryPackFingerprint( + File packsDir, + String cachedMetadata, + String cachedContent, + ReapplyOutcome reapply + ) { + if (Objects.requireNonNull(reapply, "External datapack reapply outcome").changed()) { + return resolvePackFingerprint(packsDir, "", ""); + } + return resolvePackFingerprint(packsDir, cachedMetadata, cachedContent); + } + + static PackFingerprint resolvePackFingerprint(File packsDir, String cachedMetadata, String cachedContent) { + String metadata = computePackMetadataDigest(packsDir); + if (!metadata.isEmpty() + && metadata.equals(cachedMetadata) + && cachedContent != null + && !cachedContent.isEmpty()) { + return new PackFingerprint(metadata, cachedContent); + } + return new PackFingerprint(metadata, computePackFingerprint(packsDir)); + } + + public static String computePackMetadataDigest(File packsDir) { + Path root = resolveFingerprintRoot(packsDir); + if (root == null) { + return ""; + } + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + List entries = collectFingerprintEntries(root.toRealPath()); + entries.sort(Comparator.comparing(FingerprintEntry::relativePath)); + for (FingerprintEntry entry : entries) { + byte[] relativePath = entry.relativePath().getBytes(StandardCharsets.UTF_8); + updateDigestInt(digest, relativePath.length); + digest.update(relativePath); + updateDigestLong(digest, entry.size()); + updateDigestLong(digest, entry.lastModifiedMillis()); + } + return HexFormat.of().formatHex(digest.digest()); + } catch (IOException exception) { + throw new UncheckedIOException("Unable to fingerprint Iris packs at " + root, exception); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 not available", e); + } + } + + public static String computePackFingerprint(File packsDir) { + return computePackContentSnapshot(packsDir).content(); + } + + public static PackContentSnapshot computePackContentSnapshot(File packsDir) { + Path root = resolveFingerprintRoot(packsDir); + if (root == null) { + return new PackContentSnapshot("", Map.of()); + } + try { + Path resolvedRoot = root.toRealPath(); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + Map packDigests = new LinkedHashMap<>(); + List entries = collectFingerprintEntries(resolvedRoot); + entries.sort(Comparator.comparing(FingerprintEntry::relativePath)); + byte[] buffer = new byte[FINGERPRINT_BUFFER_BYTES]; + for (FingerprintEntry entry : entries) { + MessageDigest packDigest = entry.packName() == null + ? null + : packDigests.computeIfAbsent(entry.packName(), ignored -> newSha256Digest()); + updateFingerprintEntry(digest, entry.relativePath(), entry.size()); + if (packDigest != null) { + updateFingerprintEntry(packDigest, entry.packRelativePath(), entry.size()); + } + long readBytes = 0L; + try (InputStream input = Files.newInputStream( + entry.source(), + StandardOpenOption.READ, + LinkOption.NOFOLLOW_LINKS)) { + int read; + while ((read = input.read(buffer)) >= 0) { + if (read > 0) { + digest.update(buffer, 0, read); + if (packDigest != null) { + packDigest.update(buffer, 0, read); + } + readBytes += read; + } + } + } + if (readBytes != entry.size()) { + throw new IOException("Iris pack changed while fingerprinting: " + entry.source()); + } + } + Map packContents = new LinkedHashMap<>(); + for (Map.Entry entry : packDigests.entrySet()) { + packContents.put(entry.getKey(), HexFormat.of().formatHex(entry.getValue().digest())); + } + return new PackContentSnapshot( + HexFormat.of().formatHex(digest.digest()), + Map.copyOf(packContents)); + } catch (IOException exception) { + throw new UncheckedIOException("Unable to fingerprint Iris packs at " + root, exception); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 not available", e); + } + } + + public static String computePackTreeFingerprint(File packDir) { + Path root = resolveFingerprintRoot(packDir); + if (root == null) { + return ""; + } + try { + List entries = new ArrayList<>(); + collectFingerprintTree(root.toRealPath(), "", null, entries); + entries.sort(Comparator.comparing(FingerprintEntry::relativePath)); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[FINGERPRINT_BUFFER_BYTES]; + for (FingerprintEntry entry : entries) { + updateFingerprintEntry(digest, entry.relativePath(), entry.size()); + long readBytes = 0L; + try (InputStream input = Files.newInputStream( + entry.source(), + StandardOpenOption.READ, + LinkOption.NOFOLLOW_LINKS)) { + int read; + while ((read = input.read(buffer)) >= 0) { + if (read > 0) { + digest.update(buffer, 0, read); + readBytes += read; + } + } + } + if (readBytes != entry.size()) { + throw new IOException("Iris pack changed while fingerprinting: " + entry.source()); + } + } + return HexFormat.of().formatHex(digest.digest()); + } catch (IOException exception) { + throw new UncheckedIOException("Unable to fingerprint Iris pack at " + root, exception); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 not available", exception); + } + } + + private static MessageDigest newSha256Digest() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 not available", exception); + } + } + + private static void updateFingerprintEntry(MessageDigest digest, String relativePath, long size) { + byte[] relativeBytes = relativePath.getBytes(StandardCharsets.UTF_8); + updateDigestInt(digest, relativeBytes.length); + digest.update(relativeBytes); + updateDigestLong(digest, size); + } + + private static Path resolveFingerprintRoot(File packsDir) { + if (packsDir == null) { + return null; + } + Path root = packsDir.toPath().toAbsolutePath().normalize(); + if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + return null; + } + if (!Files.isDirectory(root)) { + if (Files.isSymbolicLink(root)) { + throw new IllegalArgumentException("Iris packs root target is missing or unsafe: " + root); + } + return null; + } + return root; + } + + private static FingerprintCache readFingerprintCache(Path cacheFile) { + if (!Files.isRegularFile(cacheFile)) { + return new FingerprintCache("", ""); + } + try { + List lines = Files.readAllLines(cacheFile, StandardCharsets.UTF_8); + String content = lines.isEmpty() ? "" : lines.getFirst().trim(); + String metadata = lines.size() > 1 ? lines.get(1).trim() : ""; + return new FingerprintCache(content, metadata); + } catch (IOException e) { + return new FingerprintCache("", ""); + } + } + + private static String readCompilerInputFingerprintCache(Path cacheFile) { + if (!Files.isRegularFile(cacheFile)) { + return ""; + } + try { + return Files.readString(cacheFile, StandardCharsets.UTF_8).trim(); + } catch (IOException exception) { + return ""; + } + } + + private static void writeFingerprintCache(Path cacheFile, PackFingerprint fingerprint) { + try { + writeFingerprintAtomic(cacheFile, fingerprint.content() + "\n" + fingerprint.metadata()); + } catch (IOException e) { + IrisLogging.warn("Failed to write datapack fingerprint cache: " + e.getMessage()); + } + } + + private static void writeCompilerInputFingerprintCache(Path cacheFile, String fingerprint) { + try { + writeFingerprintAtomic(cacheFile, fingerprint); + } catch (IOException exception) { + IrisLogging.warn("Failed to write datapack compiler-input fingerprint cache: " + + exception.getMessage()); + } + } + + private static void writeFingerprintAtomic(Path target, String fingerprint) throws IOException { + Path absoluteTarget = target.toAbsolutePath().normalize(); + Path parent = absoluteTarget.getParent(); + if (parent == null) { + throw new IOException("Datapack fingerprint target has no parent: " + absoluteTarget); + } + Files.createDirectories(parent); + Path staged = Files.createTempFile(parent, ".datapack-fingerprint-", ".tmp"); + try { + Files.writeString(staged, fingerprint, StandardCharsets.UTF_8); + try (FileChannel channel = FileChannel.open(staged, StandardOpenOption.WRITE)) { + channel.force(true); + } + try { + Files.move(staged, absoluteTarget, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException exception) { + Files.move(staged, absoluteTarget, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(staged); + } + } + + private static List collectFingerprintEntries(Path root) throws IOException { + List entries = new ArrayList<>(); + try (Stream children = Files.list(root)) { + for (Path child : children.toList()) { + String childName = child.getFileName().toString(); + if (PackDirectoryResolver.isHiddenName(childName) || isGeneratedPackFile(childName)) { + continue; + } + if (Files.isSymbolicLink(child)) { + if (!Files.isDirectory(child)) { + throw new IOException("Iris pack fingerprint rejected symbolic link: " + child); + } + PackDirectoryResolver.requireSafePackTree(child.toFile()); + collectFingerprintTree(child.toRealPath(), childName, childName, entries); + } else if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) { + collectFingerprintTree(child, childName, childName, entries); + } else if (Files.isRegularFile(child, LinkOption.NOFOLLOW_LINKS)) { + BasicFileAttributes attributes = Files.readAttributes( + child, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + entries.add(new FingerprintEntry( + child, + childName, + null, + null, + attributes.size(), + attributes.lastModifiedTime().toMillis())); + } else { + throw new IOException("Iris pack fingerprint rejected unsupported entry: " + child); + } + } + } + return entries; + } + + private static void collectFingerprintTree( + Path treeRoot, + String logicalRoot, + String packName, + List entries + ) throws IOException { + Files.walkFileTree(treeRoot, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory( + Path directory, + BasicFileAttributes attributes + ) { + if (!directory.equals(treeRoot) + && treeRoot.relativize(directory).getNameCount() == 1 + && PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { + String fileName = file.getFileName().toString(); + if ((treeRoot.relativize(file).getNameCount() == 1 + && PackDirectoryResolver.isHiddenName(fileName)) + || isGeneratedPackFile(fileName)) { + return FileVisitResult.CONTINUE; + } + if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) { + throw new IOException("Iris pack fingerprint rejected symbolic link: " + file); + } + if (!attributes.isRegularFile()) { + throw new IOException("Iris pack fingerprint rejected unsupported entry: " + file); + } + String relative = treeRoot.relativize(file).toString().replace(File.separatorChar, '/'); + String logicalRelative = logicalRoot.isEmpty() ? relative : logicalRoot + "/" + relative; + entries.add(new FingerprintEntry( + file, + logicalRelative, + packName, + packName == null ? null : relative, + attributes.size(), + attributes.lastModifiedTime().toMillis())); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException { + throw new IOException("Unable to inspect Iris pack entry: " + file, failure); + } + }); + } + + private static boolean isGeneratedPackFile(String name) { + return name != null && name.endsWith(CODE_WORKSPACE_SUFFIX); + } + + private record FingerprintEntry( + Path source, + String relativePath, + String packName, + String packRelativePath, + long size, + long lastModifiedMillis + ) { + } + + record PackFingerprint(String metadata, String content) { + } + + public record PackContentSnapshot(String content, Map packContents) { + public PackContentSnapshot { + content = Objects.requireNonNullElse(content, ""); + packContents = Map.copyOf(Objects.requireNonNullElse(packContents, Map.of())); + } + } + + private record FingerprintCache(String content, String metadata) { + } + + private static void updateDigestInt(MessageDigest digest, int value) { + for (int shift = Integer.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) { + digest.update((byte) (value >>> shift)); + } + } + + private static void updateDigestLong(MessageDigest digest, long value) { + for (int shift = Long.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) { + digest.update((byte) (value >>> shift)); + } + } + + public static File resolveDatapacksFolder(File worldFolder) { + File rootFolder = resolveWorldRootFolder(worldFolder); + return new File(rootFolder, "datapacks"); + } + + static File resolveWorldRootFolder(File worldFolder) { + if (worldFolder == null) { + return IrisWorldStorage.levelRoot(); + } + return IrisWorldStorage.levelRoot(worldFolder); + } + + private static boolean verifyDataPacksPost() { + try (Stream stream = allPacks()) { + boolean bad = stream + .map(data -> { + IrisLogging.debug("Checking Pack: " + data.getDataFolder().getPath()); + ResourceLoader loader = data.getDimensionLoader(); + return loader.loadAll(loader.getPossibleKeys()) + .stream() + .filter(Objects::nonNull) + .map(ServerConfigurator::verifyDataPackInstalled) + .toList() + .contains(false); + }) + .toList() + .contains(true); + if (!bad) { + return false; + } + } + + + if (INMS.get().supportsDataPacks()) { + // Three sentences, no rules: a separator carries the record's severity too, so a box drawn + // out of equals signs became three more [SEVERE] lines saying nothing. + IrisLogging.error(C.ITALIC + "You need to restart your server to properly generate custom biomes."); + IrisLogging.error(C.ITALIC + "By continuing, Iris will use backup biomes in place of the custom biomes."); + IrisLogging.error(C.UNDERLINE + "Restart the server before generating."); + + for (Player i : Bukkit.getOnlinePlayers()) { + if (i.isOp() || i.hasPermission("iris.all")) { + VolmitSender sender = new VolmitSender(i, BukkitPlatform.volmitPlugin().getTag("WARNING")); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.SERVER_CONFIGURATOR_THERE_ARE_SOME_IRIS_PACKS_THAT_HAVE_CUSTOM_BIOMES_THEM)); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.SERVER_CONFIGURATOR_YOU_NEED_RESTART_YOUR_SERVER_USE_THESE_PACKS)); + } + } + + } + return true; + } + + public static void restart() { + restart("New data pack entries have been installed in Iris."); + } + + public static void restart(String reason) { + requireDatapackRestart(); + LifecycleOperationCoordinator.get().quiesceForRestart(() -> J.s(() -> { + IrisLogging.warn(reason + " Restarting server to restore a safe lifecycle boundary."); + J.s(() -> { + IrisLogging.warn("Looks like the restart command didn't work. Stopping the server instead!"); + Bukkit.shutdown(); + }, 100); + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "restart"); + })); + } + + public static boolean verifyDataPackInstalled(IrisDimension dimension) { + KSet keys = new KSet<>(); + boolean warn = false; + + for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) { + if (i.isCustom()) { + for (IrisBiomeCustom j : i.getCustomDerivitives()) { + keys.add(dimension.getLoadKey() + ":" + j.getId()); + } + } + } + String key = getWorld(dimension.getLoader()); + if (key == null) key = dimension.getLoadKey(); + else key += "/" + dimension.getLoadKey(); + + if (!INMS.get().supportsDataPacks()) { + if (!keys.isEmpty()) { + IrisLogging.warn("==================================================================================="); + IrisLogging.warn("Pack " + key + " has " + keys.size() + " custom biome(s). "); + IrisLogging.warn("Your server version does not yet support datapacks for iris."); + IrisLogging.warn("The world will generate these biomes as backup biomes."); + IrisLogging.warn("===================================================================================="); + } + + return true; + } + + for (String i : keys) { + Object o = INMS.get().getCustomBiomeBaseFor(i); + + if (o == null) { + IrisLogging.warn("The Biome " + i + " is not registered on the server."); + warn = true; + } + } + + if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) { + IrisLogging.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server."); + warn = true; + } + + if (warn) { + IrisLogging.error("The Pack " + key + " is INCAPABLE of generating custom biomes"); + IrisLogging.error("If not done automatically, restart your server before generating with this pack!"); + } + + return !warn; + } + + public static Stream allPacks() { + Stream locals = PackDirectoryResolver.listVisiblePackDirectories( + IrisPlatforms.get().packsFolder() + ).stream(); + return Stream.concat(locals + .filter(base -> { + File[] content = new File(base, "dimensions").listFiles(); + return content != null && content.length > 0; + }) + .map(IrisData::get), IrisWorlds.get().getPacks()); + } + + @Nullable + public static String getWorld(@NonNull IrisData data) { + Path packPath = data.getDataFolder().toPath().toAbsolutePath().normalize(); + Path irisPath = packPath.getParent(); + if (irisPath == null || !"pack".equals(packPath.getFileName().toString()) || !"iris".equals(irisPath.getFileName().toString())) { + return null; + } + + Path dimensionPath = irisPath.getParent(); + if (dimensionPath == null) { + return null; + } + File dimensionRoot = dimensionPath.toFile(); + NamespacedKey key = IrisWorldStorage.keyFromDimensionRoot(IrisWorldStorage.levelRoot(), dimensionRoot).orElse(null); + return key == null ? null : key.toString(); + } + + public record LoadedDatapackRuntimeInvalidation( + long generation, + boolean wasReady, + String fingerprint + ) { + public LoadedDatapackRuntimeInvalidation { + fingerprint = Objects.requireNonNullElse(fingerprint, ""); + } + } + +} diff --git a/core/src/main/java/art/arcane/iris/core/SnapshotDirectoryTreeDeleter.java b/core/src/main/java/art/arcane/iris/core/SnapshotDirectoryTreeDeleter.java new file mode 100644 index 000000000..1284a370e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/SnapshotDirectoryTreeDeleter.java @@ -0,0 +1,91 @@ +package art.arcane.iris.core; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class SnapshotDirectoryTreeDeleter { + private SnapshotDirectoryTreeDeleter() { + } + + public static void delete(Path target) throws IOException { + Path root = Objects.requireNonNull(target, "target").toAbsolutePath().normalize(); + if (root.getParent() == null) { + throw new IOException("Refusing to delete a filesystem root: " + root); + } + if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + return; + } + requireDirectory(root); + deleteDirectory(root); + } + + private static void deleteDirectory(Path directory) throws IOException { + requireDirectory(directory); + List entries = snapshot(directory); + for (SnapshotEntry entry : entries) { + BasicFileAttributes current = requireSafeEntry(entry.path()); + if (entry.directory() != current.isDirectory() + || !sameFile(entry.fileKey(), current.fileKey())) { + throw new IOException("Directory entry changed during deletion: " + entry.path()); + } + if (entry.directory()) { + deleteDirectory(entry.path()); + } else { + Files.delete(entry.path()); + } + } + Files.delete(directory); + } + + private static List snapshot(Path directory) throws IOException { + ArrayList entries = new ArrayList<>(); + try (DirectoryStream children = Files.newDirectoryStream(directory)) { + for (Path child : children) { + Path normalized = child.toAbsolutePath().normalize(); + if (!Objects.equals(normalized.getParent(), directory)) { + throw new IOException("Directory entry escapes its parent: " + child); + } + BasicFileAttributes attributes = requireSafeEntry(normalized); + entries.add(new SnapshotEntry(normalized, attributes.isDirectory(), attributes.fileKey())); + } + } + return List.copyOf(entries); + } + + private static BasicFileAttributes requireDirectory(Path directory) throws IOException { + BasicFileAttributes attributes = requireSafeEntry(directory); + if (!attributes.isDirectory()) { + throw new IOException("Deletion target is not a directory: " + directory); + } + return attributes; + } + + private static BasicFileAttributes requireSafeEntry(Path entry) throws IOException { + BasicFileAttributes attributes = Files.readAttributes( + entry, + BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS + ); + if (attributes.isSymbolicLink()) { + throw new IOException("Deletion target contains a symbolic link: " + entry); + } + if (!attributes.isDirectory() && !attributes.isRegularFile()) { + throw new IOException("Deletion target contains an unsafe filesystem entry: " + entry); + } + return attributes; + } + + private static boolean sameFile(Object expected, Object actual) { + return expected == null || actual == null || expected.equals(actual); + } + + private record SnapshotEntry(Path path, boolean directory, Object fileKey) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/WorldCreatorCompat.java b/core/src/main/java/art/arcane/iris/core/WorldCreatorCompat.java new file mode 100644 index 000000000..8cdb5594b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/WorldCreatorCompat.java @@ -0,0 +1,143 @@ +package art.arcane.iris.core; + +import art.arcane.iris.spi.IrisLogging; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.WorldCreator; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.Objects; + +/** + * WorldCreator.ofKey and WorldCreator#key are Paper-API-only. Once a call throws + * NoSuchMethodError (plain Spigot/CraftBukkit) this flips and every later call goes + * straight to the fallback. The fallback derives names/keys through IrisWorldStorage's + * current configured-name mapping so persistent Spigot worlds round-trip without changing + * their startup directory. + * + *

Paper names a keyed creator {@code _} and refuses a creator built from a name + * and a key together, but it names that same world {@code __} when it loads + * it out of {@code /dimensions//} on every later boot. A persistent world + * therefore has to be born under the startup name, which is what {@link #ofPersistentKey} does.

+ */ +public final class WorldCreatorCompat { + private static volatile boolean keyedCreatorsUnavailable; + private static volatile boolean creatorNamesUnwritable; + private static volatile Field creatorNameField; + + private WorldCreatorCompat() { + } + + public static WorldCreator ofKey(NamespacedKey worldKey) { + WorldCreator keyedCreator = keyedCreator(worldKey); + if (keyedCreator != null) { + return keyedCreator; + } + return new WorldCreator(IrisWorldStorage.logicalName(worldKey)); + } + + public static WorldCreator ofKey(NamespacedKey worldKey, String worldName) { + String name = requireWorldName(worldName); + WorldCreator keyedCreator = keyedCreator(worldKey); + if (keyedCreator == null) { + return new WorldCreator(name); + } + renameCreator(keyedCreator, name); + return keyedCreator; + } + + public static WorldCreator ofPersistentKey(NamespacedKey worldKey) { + return ofKey(worldKey, persistentWorldName(worldKey, IrisWorldStorage.levelRoot().getName())); + } + + public static File persistentDimensionRoot(NamespacedKey worldKey) { + if (keyedCreator(worldKey) != null) { + return IrisWorldStorage.requireSafePersistentDimensionRoot(worldKey); + } + return IrisWorldStorage.configuredDimensionRoot( + Bukkit.getWorldContainer(), + IrisWorldStorage.levelRoot(), + worldKey + ); + } + + public static File persistentLevelRoot(NamespacedKey worldKey) { + if (keyedCreator(worldKey) != null) { + return persistentDimensionRoot(worldKey); + } + return IrisWorldStorage.configuredLevelRoot( + Bukkit.getWorldContainer(), + IrisWorldStorage.levelRoot(), + worldKey + ); + } + + public static NamespacedKey keyOf(WorldCreator creator) { + if (!keyedCreatorsUnavailable) { + try { + return creator.key(); + } catch (NoSuchMethodError e) { + keyedCreatorsUnavailable = true; + } + } + return IrisWorldStorage.keyFromConfiguredWorldName( + creator.name(), + IrisWorldStorage.levelRoot().getName() + ); + } + + static String persistentWorldName(NamespacedKey worldKey, String levelName) { + return IrisWorldStorage.configuredWorldName(worldKey, levelName); + } + + private static WorldCreator keyedCreator(NamespacedKey worldKey) { + if (keyedCreatorsUnavailable) { + return null; + } + try { + return WorldCreator.ofKey(worldKey); + } catch (NoSuchMethodError e) { + keyedCreatorsUnavailable = true; + return null; + } + } + + /** + * WorldCreator rejects a name and a key passed together and derives the name from the key, so the + * only way to hand a keyed world its startup name at creation is to overwrite the creator's name. + * If that ever stops working the keyed creator is still correct except for the name, which is what + * shipped before, so the world is created rather than refused. + */ + private static void renameCreator(WorldCreator creator, String worldName) { + if (worldName.equals(creator.name()) || creatorNamesUnwritable) { + return; + } + try { + creatorNameField().set(creator, worldName); + } catch (ReflectiveOperationException | RuntimeException | Error e) { + creatorNamesUnwritable = true; + IrisLogging.warn("WorldCreator name is not writable; world %s is created as %s and renamed on the next boot.", + worldName, creator.name()); + } + } + + private static Field creatorNameField() throws ReflectiveOperationException { + Field cached = creatorNameField; + if (cached != null) { + return cached; + } + Field name = WorldCreator.class.getDeclaredField("name"); + name.setAccessible(true); + creatorNameField = name; + return name; + } + + private static String requireWorldName(String worldName) { + String name = Objects.requireNonNull(worldName, "worldName").trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException("World name cannot be empty."); + } + return name; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/WorldRemovalPathPolicy.java b/core/src/main/java/art/arcane/iris/core/WorldRemovalPathPolicy.java new file mode 100644 index 000000000..570e674f0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/WorldRemovalPathPolicy.java @@ -0,0 +1,236 @@ +package art.arcane.iris.core; + +import org.bukkit.NamespacedKey; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +public final class WorldRemovalPathPolicy { + private WorldRemovalPathPolicy() { + } + + public static Target resolve(String identifier, String configuredMainWorld, Path levelRoot) { + return resolve(identifier, configuredMainWorld, List.of(configuredMainWorld), levelRoot); + } + + public static Target resolve( + String identifier, + String currentMainWorld, + Collection protectedWorldNames, + Path levelRoot + ) { + String requestedIdentifier = requireIdentifier(identifier); + String mainWorld = requireIdentifier(currentMainWorld); + for (String protectedWorldName : Objects.requireNonNull(protectedWorldNames, "protectedWorldNames")) { + if (protectedWorldName != null + && !protectedWorldName.isBlank() + && requestedIdentifier.equalsIgnoreCase(protectedWorldName.trim())) { + throw new Rejection(RejectionReason.CONFIGURED_MAIN_WORLD, + "A current or configured main world cannot be removed."); + } + } + if (requestedIdentifier.toLowerCase(Locale.ENGLISH).startsWith(NamespacedKey.MINECRAFT + ":")) { + throw new Rejection(RejectionReason.MINECRAFT_NAMESPACE, + "Minecraft namespace worlds cannot be removed."); + } + + NamespacedKey worldKey; + try { + worldKey = IrisWorldStorage.managedKeyFromName(requestedIdentifier, mainWorld); + } catch (IllegalArgumentException failure) { + throw classifyIdentifierFailure(requestedIdentifier, failure); + } + + Path normalizedLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize(); + if (Files.isSymbolicLink(normalizedLevelRoot)) { + throw new Rejection(RejectionReason.SYMBOLIC_LINK, + "World storage path contains a symbolic link: " + normalizedLevelRoot); + } + StorageLayout storageLayout; + try { + storageLayout = resolveStorageLayout(normalizedLevelRoot, worldKey); + } catch (RuntimeException failure) { + throw classifyStorageFailure(normalizedLevelRoot, worldKey, failure); + } + validateStoragePath(normalizedLevelRoot, worldKey, storageLayout.dimensionDirectory()); + validateStorageRoot(normalizedLevelRoot, worldKey, storageLayout.storageDirectory()); + return new Target( + requestedIdentifier, + worldKey, + IrisWorldStorage.logicalName(worldKey, mainWorld), + normalizedLevelRoot, + storageLayout.dimensionDirectory(), + storageLayout.storageDirectory() + ); + } + + public static void validateStoragePath(Path levelRoot, NamespacedKey worldKey, Path candidate) { + Path normalizedLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize(); + if (Files.isSymbolicLink(normalizedLevelRoot)) { + throw new Rejection(RejectionReason.SYMBOLIC_LINK, + "World storage path contains a symbolic link: " + normalizedLevelRoot); + } + Path expected; + try { + expected = resolveStorageLayout( + normalizedLevelRoot, + Objects.requireNonNull(worldKey, "worldKey") + ).dimensionDirectory(); + } catch (RuntimeException failure) { + throw classifyStorageFailure(normalizedLevelRoot, worldKey, failure); + } + Path normalizedCandidate = Objects.requireNonNull(candidate, "candidate").toAbsolutePath().normalize(); + if (!normalizedCandidate.equals(expected)) { + throw new Rejection(RejectionReason.OUTSIDE_STORAGE_ROOT, + "The world directory is outside its exact Iris dimension storage root."); + } + } + + public static void validateStorageRoot(Path levelRoot, NamespacedKey worldKey, Path candidate) { + Path normalizedLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize(); + if (Files.isSymbolicLink(normalizedLevelRoot)) { + throw new Rejection(RejectionReason.SYMBOLIC_LINK, + "World storage path contains a symbolic link: " + normalizedLevelRoot); + } + Path expected; + try { + expected = resolveStorageLayout( + normalizedLevelRoot, + Objects.requireNonNull(worldKey, "worldKey") + ).storageDirectory(); + } catch (RuntimeException failure) { + throw classifyStorageFailure(normalizedLevelRoot, worldKey, failure); + } + Path normalizedCandidate = Objects.requireNonNull(candidate, "candidate").toAbsolutePath().normalize(); + if (!normalizedCandidate.equals(expected)) { + throw new Rejection(RejectionReason.OUTSIDE_STORAGE_ROOT, + "The world storage directory is outside its exact current platform root."); + } + } + + private static Rejection classifyIdentifierFailure(String identifier, IllegalArgumentException failure) { + NamespacedKey parsed = identifier.contains(":") + ? NamespacedKey.fromString(identifier.toLowerCase(Locale.ENGLISH)) + : null; + RejectionReason reason = parsed != null && !"iris".equals(parsed.getNamespace()) + ? RejectionReason.NOT_IRIS_NAMESPACE + : RejectionReason.INVALID_IDENTIFIER; + return new Rejection(reason, failure.getMessage(), failure); + } + + private static Rejection classifyStorageFailure( + Path levelRoot, + NamespacedKey worldKey, + RuntimeException failure + ) { + Path dimensions = levelRoot.resolve("dimensions"); + Path namespace = dimensions.resolve(worldKey.getNamespace()); + Path target = namespace.resolve(worldKey.getKey()); + String failureMessage = String.valueOf(failure.getMessage()).toLowerCase(Locale.ENGLISH); + RejectionReason reason = failureMessage.contains("symbolic link") + || Files.isSymbolicLink(dimensions) + || Files.isSymbolicLink(namespace) + || Files.isSymbolicLink(target) + ? RejectionReason.SYMBOLIC_LINK + : RejectionReason.OUTSIDE_STORAGE_ROOT; + return new Rejection(reason, failure.getMessage(), failure); + } + + private static StorageLayout resolveStorageLayout(Path levelRoot, NamespacedKey worldKey) { + Path worldContainer = levelRoot.getParent(); + if (worldContainer == null) { + throw new IllegalArgumentException("Selected level root has no world container: " + levelRoot); + } + String configuredWorldName = IrisWorldStorage.configuredWorldName( + worldKey, + levelRoot.getFileName().toString() + ); + Path directDimension = IrisWorldStorage.requireSafeManagedDimensionRoot( + levelRoot.toFile(), + worldKey + ).toPath().toAbsolutePath().normalize(); + Path dimensionDirectory = IrisWorldStorage.frozenDimensionRoot( + worldContainer.toFile(), + levelRoot.toFile(), + configuredWorldName, + worldKey + ).map(file -> file.toPath().toAbsolutePath().normalize()).orElse(directDimension); + if (dimensionDirectory.equals(directDimension)) { + return new StorageLayout(directDimension, directDimension); + } + + Path configuredDimension = IrisWorldStorage.configuredDimensionRoot( + worldContainer.toFile(), + levelRoot.toFile(), + worldKey + ).toPath().toAbsolutePath().normalize(); + if (!dimensionDirectory.equals(configuredDimension)) { + throw new IllegalStateException("Iris world storage does not match the current platform layout."); + } + Path configuredLevel = IrisWorldStorage.configuredLevelRoot( + worldContainer.toFile(), + levelRoot.toFile(), + worldKey + ).toPath().toAbsolutePath().normalize(); + return new StorageLayout(configuredDimension, configuredLevel); + } + + private static String requireIdentifier(String identifier) { + if (identifier == null || identifier.isBlank()) { + throw new Rejection(RejectionReason.INVALID_IDENTIFIER, "The world identifier cannot be empty."); + } + return identifier.trim(); + } + + public record Target( + String requestedIdentifier, + NamespacedKey worldKey, + String logicalName, + Path levelRoot, + Path worldDirectory, + Path storageDirectory + ) { + public Target { + Objects.requireNonNull(requestedIdentifier, "requestedIdentifier"); + Objects.requireNonNull(worldKey, "worldKey"); + Objects.requireNonNull(logicalName, "logicalName"); + Objects.requireNonNull(levelRoot, "levelRoot"); + Objects.requireNonNull(worldDirectory, "worldDirectory"); + Objects.requireNonNull(storageDirectory, "storageDirectory"); + } + } + + private record StorageLayout(Path dimensionDirectory, Path storageDirectory) { + } + + public enum RejectionReason { + INVALID_IDENTIFIER, + CONFIGURED_MAIN_WORLD, + MINECRAFT_NAMESPACE, + NOT_IRIS_NAMESPACE, + OUTSIDE_STORAGE_ROOT, + SYMBOLIC_LINK + } + + public static final class Rejection extends IllegalArgumentException { + private final RejectionReason reason; + + private Rejection(RejectionReason reason, String message) { + super(message); + this.reason = Objects.requireNonNull(reason, "reason"); + } + + private Rejection(RejectionReason reason, String message, Throwable cause) { + super(message, cause); + this.reason = Objects.requireNonNull(reason, "reason"); + } + + public RejectionReason reason() { + return reason; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/WorldSlotKey.java b/core/src/main/java/art/arcane/iris/core/WorldSlotKey.java new file mode 100644 index 000000000..37f194afb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/WorldSlotKey.java @@ -0,0 +1,41 @@ +package art.arcane.iris.core; + +import java.util.Objects; +import java.util.regex.Pattern; + +public record WorldSlotKey(String namespace, String key) { + private static final Pattern KEY_PATTERN = Pattern.compile("^[a-z0-9/._-]+$"); + private static final Pattern NAMESPACE_PATTERN = Pattern.compile("^[a-z0-9._-]+$"); + + public WorldSlotKey { + namespace = Objects.requireNonNull(namespace, "namespace"); + key = Objects.requireNonNull(key, "key"); + if (!NAMESPACE_PATTERN.matcher(namespace).matches()) { + throw new IllegalArgumentException("World slot namespace is invalid: " + namespace); + } + if (!KEY_PATTERN.matcher(key).matches()) { + throw new IllegalArgumentException("World slot key is invalid: " + key); + } + } + + public static WorldSlotKey parse(String value) { + String requiredValue = Objects.requireNonNull(value, "value"); + int separator = requiredValue.indexOf(':'); + if (separator <= 0 || separator != requiredValue.lastIndexOf(':') || separator == requiredValue.length() - 1) { + throw new IllegalArgumentException("World slot key must use namespace:key syntax."); + } + return new WorldSlotKey( + requiredValue.substring(0, separator), + requiredValue.substring(separator + 1) + ); + } + + public static WorldSlotKey minecraft(String key) { + return new WorldSlotKey("minecraft", key); + } + + @Override + public String toString() { + return namespace + ":" + key; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java new file mode 100644 index 000000000..e785084d8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java @@ -0,0 +1,6242 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.datapack; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.IrisStartupValidation; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.datapack.ModrinthResolver.ResolvedDatapack; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.MinecraftVersion; +import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.project.IrisCodeWorkspace; +import art.arcane.iris.core.structure.BulkStructureImporter; +import art.arcane.iris.core.structure.StructureImporter; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureSource; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisImportedStructureControl; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.io.ZipUtils; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.Bukkit; +import org.bukkit.Server; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileStore; +import java.nio.file.FileSystems; +import java.nio.file.LinkOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Stream; + +public final class DatapackIngestService { + private static final String USER_AGENT = "VolmitSoftware/Iris (datapack-ingest)"; + private static final String FINDER_METADATA = ".DS_Store"; + private static final String OVERRIDES_STRIPPED_MARKER = ".iris-overrides-stripped"; + private static final String OWNERSHIP_MARKER = ".iris-managed.json"; + private static final String TRANSACTION_DIRECTORY = ".iris-datapack-transactions"; + private static final String TRANSACTION_JOURNAL = "journal.json"; + private static final String TRANSACTION_JOURNAL_NEXT = "journal.next.json"; + private static final String STARTUP_VALIDATION_CACHE = "startup-validation.json"; + private static final int OWNERSHIP_SCHEMA = 1; + private static final int TRANSACTION_SCHEMA = 2; + private static final int STARTUP_VALIDATION_SCHEMA = 1; + private static final int STRUCTURE_IMPORT_FORMAT_REVISION = 3; + private static final int MAX_REDIRECTS = 5; + private static final int MAX_ARCHIVE_ENTRIES = 100_000; + private static final int MAX_CACHE_FILES = 32; + private static final int MAX_MANAGED_PATHS = MAX_ARCHIVE_ENTRIES + 16; + private static final long MAX_DOWNLOAD_BYTES = 256L * 1024L * 1024L; + private static final long MAX_EXPANDED_BYTES = 1024L * 1024L * 1024L; + private static final long MAX_ENTRY_BYTES = 256L * 1024L * 1024L; + private static final long MAX_CACHE_BYTES = 1024L * 1024L * 1024L; + private static final long MAX_MANIFEST_BYTES = 16L * 1024L * 1024L; + private static final long MAX_TRANSACTION_JOURNAL_BYTES = 4L * 1024L * 1024L; + private static final long MAX_METADATA_BYTES = 1024L * 1024L; + private static final long MAX_OWNERSHIP_BYTES = 1024L * 1024L; + private static final int MAX_TRANSACTION_COUNT = 1_024; + private static final int MAX_SCRATCH_DELETE_ATTEMPTS = 3; + private static final int WINDOWS_LEGACY_PATH_LIMIT = 247; + private static final int HASH_BUFFER_BYTES = 64 * 1024; + private static final Set RESERVED_IDS = Set.of("iris"); + private static final ReentrantLock TRANSACTION_LOCK = new ReentrantLock(); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static volatile StartupValidationCache activeStartupValidation; + + private DatapackIngestService() { + } + + public static Report ingestAll(VolmitSender sender, boolean restart) { + return ingest(sender, collectConfiguredImports(), restart); + } + + public static void autoIngestOnStartup() { + StartupValidationOutcome outcome = validateOnStartup(); + if (outcome == StartupValidationOutcome.READY) { + runPostStartupTasks(); + } + } + + public static StartupValidationOutcome validateOnStartup() { + activeStartupValidation = null; + IrisStartupValidation.beginDatapackValidation(); + KList configured = collectConfiguredImports(); + List urls = configured.stream().sorted().toList(); + boolean autoIngest = IrisSettings.get().getGeneral().autoIngestDatapacks; + boolean stripOverrides = resolveStripOverrides(); + String mcVersion = serverMcVersion(); + int irisVersion = IrisPlatforms.get().irisVersionNumber(); + File root = IrisPlatforms.get().dataFolder("datapacks"); + KList worldFolders = ServerConfigurator.getDatapacksFolder(); + Path cacheFile = new File(root, STARTUP_VALIDATION_CACHE).toPath(); + + try { + StartupValidationCache cached = readStartupValidationCache(cacheFile); + if (startupValidationContextMatches( + cached, mcVersion, irisVersion, autoIngest, stripOverrides, urls)) { + String localFingerprint = startupValidationFingerprint(root, worldFolders); + if (startupValidationCacheMatches( + cached, + mcVersion, + irisVersion, + autoIngest, + stripOverrides, + urls, + localFingerprint)) { + activeStartupValidation = cached; + IrisLogging.info("External datapacks match the persisted startup validation; remote resolution and full revalidation were skipped."); + IrisStartupValidation.markDatapacksReady(); + return StartupValidationOutcome.READY; + } + } + } catch (IOException | RuntimeException exception) { + IrisLogging.warn("Persisted external datapack validation could not be reused: " + + failureMessage(exception)); + } + + if (autoIngest && !configured.isEmpty()) { + IrisLogging.info("Validating " + configured.size() + + " configured external datapack import(s) before player admission..."); + Report report = ingest(null, configured, true, true); + if (!report.getFailed().isEmpty()) { + String failure = report.getFailed().getFirst(); + IrisStartupValidation.markDatapacksInvalid(failure); + return StartupValidationOutcome.FAILED; + } + StartupValidationOutcome outcome = report.changed() + ? StartupValidationOutcome.RESTART_REQUIRED + : StartupValidationOutcome.READY; + activeStartupValidation = cacheStartupValidation(root, worldFolders, cacheFile, mcVersion, irisVersion, + autoIngest, stripOverrides, urls); + if (outcome == StartupValidationOutcome.RESTART_REQUIRED) { + IrisStartupValidation.requireRestart( + "Iris installed updated external datapacks; restart must complete before player admission or world creation."); + } else { + IrisStartupValidation.markDatapacksReady(); + } + return outcome; + } + + ReapplyOutcome reapply = reapplyFromStaging(worldFolders); + if (!reapply.succeeded()) { + String failure = reapply.failure() + .map(DatapackIngestService::failureMessage) + .orElse("External datapack recovery failed."); + IrisStartupValidation.markDatapacksInvalid(failure); + return StartupValidationOutcome.FAILED; + } + activeStartupValidation = cacheStartupValidation(root, worldFolders, cacheFile, mcVersion, irisVersion, + autoIngest, stripOverrides, urls); + if (reapply.changed()) { + IrisStartupValidation.requireRestart( + "Iris repaired external datapack files; restart must complete before player admission or world creation."); + return StartupValidationOutcome.RESTART_REQUIRED; + } + IrisStartupValidation.markDatapacksReady(); + return StartupValidationOutcome.READY; + } + + public static void runPostStartupTasks() { + refreshWorkspaces(); + boolean maintenanceChanged = autoImportDatapackStructures(); + refreshStartupValidationAfterMaintenance(maintenanceChanged); + } + + private static StartupValidationCache cacheStartupValidation( + File root, + KList worldFolders, + Path cacheFile, + String mcVersion, + int irisVersion, + boolean autoIngest, + boolean stripOverrides, + List urls + ) { + try { + StartupValidationCache cache = createStartupValidationCache( + mcVersion, + irisVersion, + autoIngest, + stripOverrides, + urls, + startupValidationFingerprint(root, worldFolders)); + writeStartupValidationCache(cacheFile, cache); + return cache; + } catch (IOException | RuntimeException exception) { + IrisLogging.warn("Could not persist external datapack startup validation: " + + failureMessage(exception)); + return null; + } + } + + private static void refreshStartupValidationAfterMaintenance(boolean maintenanceChanged) { + if (!maintenanceChanged) { + return; + } + StartupValidationCache validated = activeStartupValidation; + if (validated == null || !IrisStartupValidation.isReady()) { + return; + } + KList configured = collectConfiguredImports(); + List urls = configured.stream().sorted().toList(); + boolean autoIngest = IrisSettings.get().getGeneral().autoIngestDatapacks; + boolean stripOverrides = resolveStripOverrides(); + String mcVersion = serverMcVersion(); + int irisVersion = IrisPlatforms.get().irisVersionNumber(); + if (!startupValidationContextMatches( + validated, mcVersion, irisVersion, autoIngest, stripOverrides, urls)) { + return; + } + File root = IrisPlatforms.get().dataFolder("datapacks"); + KList worldFolders = ServerConfigurator.getDatapacksFolder(); + Path cacheFile = new File(root, STARTUP_VALIDATION_CACHE).toPath(); + TRANSACTION_LOCK.lock(); + try { + recoverTransactions(root, worldFolders); + StartupValidationCache refreshed = refreshStartupValidationCache( + validated, root, worldFolders); + writeStartupValidationCache(cacheFile, refreshed); + activeStartupValidation = refreshed; + } catch (IOException | RuntimeException exception) { + IrisLogging.warn("Could not refresh external datapack validation after startup maintenance: " + + failureMessage(exception)); + } finally { + TRANSACTION_LOCK.unlock(); + } + } + + static StartupValidationCache refreshStartupValidationCache( + StartupValidationCache validated, + File root, + KList worldFolders + ) throws IOException { + Objects.requireNonNull(validated, "Validated external datapack startup state"); + return createStartupValidationCache( + validated.minecraftVersion, + validated.irisVersion, + validated.autoIngest, + validated.stripOverrides, + validated.urls, + startupValidationFingerprint(root, worldFolders)); + } + + private static StartupValidationCache createStartupValidationCache( + String mcVersion, + int irisVersion, + boolean autoIngest, + boolean stripOverrides, + List urls, + String localFingerprint + ) { + StartupValidationCache cache = new StartupValidationCache(); + cache.schemaVersion = STARTUP_VALIDATION_SCHEMA; + cache.minecraftVersion = Objects.requireNonNullElse(mcVersion, ""); + cache.irisVersion = irisVersion; + cache.autoIngest = autoIngest; + cache.stripOverrides = stripOverrides; + cache.urls = List.copyOf(urls); + cache.localFingerprint = localFingerprint; + return cache; + } + + static boolean startupValidationContextMatches( + StartupValidationCache cache, + String mcVersion, + int irisVersion, + boolean autoIngest, + boolean stripOverrides, + List urls + ) { + return cache != null + && cache.schemaVersion == STARTUP_VALIDATION_SCHEMA + && Objects.equals(cache.minecraftVersion, Objects.requireNonNullElse(mcVersion, "")) + && cache.irisVersion == irisVersion + && cache.autoIngest == autoIngest + && cache.stripOverrides == stripOverrides + && Objects.equals(cache.urls, urls); + } + + static boolean startupValidationCacheMatches( + StartupValidationCache cache, + String mcVersion, + int irisVersion, + boolean autoIngest, + boolean stripOverrides, + List urls, + String localFingerprint + ) { + return startupValidationContextMatches( + cache, mcVersion, irisVersion, autoIngest, stripOverrides, urls) + && localFingerprint != null && !localFingerprint.isBlank() + && Objects.equals(cache.localFingerprint, localFingerprint); + } + + static StartupValidationCache readStartupValidationCache(Path cacheFile) { + if (cacheFile == null + || Files.isSymbolicLink(cacheFile) + || !Files.isRegularFile(cacheFile, LinkOption.NOFOLLOW_LINKS)) { + return null; + } + try { + if (Files.size(cacheFile) > MAX_METADATA_BYTES) { + return null; + } + StartupValidationCache cache = GSON.fromJson( + readBoundedUtf8(cacheFile, MAX_METADATA_BYTES, "External datapack startup validation"), + StartupValidationCache.class); + if (cache == null || cache.urls == null) { + return null; + } + List sortedUrls = cache.urls.stream() + .filter(Objects::nonNull) + .sorted() + .toList(); + if (sortedUrls.size() != cache.urls.size() + || new HashSet<>(sortedUrls).size() != sortedUrls.size() + || !sortedUrls.equals(cache.urls)) { + return null; + } + cache.urls = sortedUrls; + return cache; + } catch (IOException | RuntimeException exception) { + return null; + } + } + + private static void writeStartupValidationCache(Path cacheFile, StartupValidationCache cache) throws IOException { + Path absolute = cacheFile.toAbsolutePath().normalize(); + Path parent = Objects.requireNonNull(absolute.getParent(), "External datapack startup validation parent"); + Files.createDirectories(parent); + Path staged = Files.createTempFile(parent, ".startup-validation-", ".tmp"); + try { + byte[] content = GSON.toJson(cache).getBytes(StandardCharsets.UTF_8); + if (content.length > MAX_METADATA_BYTES) { + throw new IOException("External datapack startup validation exceeds " + MAX_METADATA_BYTES + " bytes"); + } + Files.write(staged, content, StandardOpenOption.TRUNCATE_EXISTING); + forceFile(staged); + try { + Files.move(staged, absolute, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException exception) { + Files.move(staged, absolute, StandardCopyOption.REPLACE_EXISTING); + } + forceDirectoryIfSupported(parent); + } finally { + Files.deleteIfExists(staged); + } + } + + static String startupValidationFingerprint(File root, KList worldFolders) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + Path manifestPath = new File(root, "manifest.json").toPath(); + updateFingerprintValue(digest, "manifest"); + if (Files.exists(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + if (Files.isSymbolicLink(manifestPath) + || !Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid datapack manifest path " + manifestPath); + } + byte[] manifest = readBoundedBytes( + manifestPath, MAX_MANIFEST_BYTES, "Datapack manifest fingerprint"); + updateDigestLong(digest, manifest.length); + digest.update(manifest); + } else if (Files.notExists(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + updateDigestLong(digest, -1L); + } else { + throw new IOException("Cannot determine datapack manifest state at " + manifestPath); + } + + Manifest manifest = readCommittedManifest(root); + updateDirectoryFingerprint(digest, "staging", new File(root, "staging")); + updateDirectoryFingerprint(digest, "transactions", new File(root, TRANSACTION_DIRECTORY)); + updateDirectoryFingerprint( + digest, + "storage-install-scratch", + installScratchRoot(new File(root, "staging"))); + + List entries = new ArrayList<>(manifest.entries); + entries.sort(Comparator.comparing(entry -> entry.id)); + List targets = new ArrayList<>(worldFolders == null ? List.of() : worldFolders); + targets.sort(Comparator.comparing(file -> file.toPath().toAbsolutePath().normalize().toString())); + for (File worldFolder : targets) { + String worldIdentity = worldFolder.toPath().toAbsolutePath().normalize().toString(); + updateFingerprintValue(digest, "world:" + worldIdentity); + updateDirectoryFingerprint( + digest, + "world-install-scratch:" + worldIdentity, + installScratchRoot(worldFolder)); + for (Entry entry : entries) { + updateDirectoryFingerprint( + digest, + "world-pack:" + worldIdentity + ":" + entry.id, + new File(worldFolder, entry.id)); + } + } + return hex(digest.digest()); + } catch (NoSuchAlgorithmException exception) { + throw new IOException("SHA-256 algorithm unavailable", exception); + } + } + + private static void updateDirectoryFingerprint( + MessageDigest digest, + String identity, + File directory + ) throws IOException { + updateFingerprintValue(digest, identity); + Path path = directory.toPath(); + if (Files.notExists(path, LinkOption.NOFOLLOW_LINKS)) { + updateFingerprintValue(digest, "missing"); + return; + } + if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS) + || Files.isSymbolicLink(path) + || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid external datapack validation directory " + path); + } + updateFingerprintValue(digest, directoryHash(directory)); + Path ownership = new File(directory, OWNERSHIP_MARKER).toPath(); + if (Files.isRegularFile(ownership, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(ownership)) { + byte[] marker = readBoundedBytes( + ownership, MAX_OWNERSHIP_BYTES, "External datapack ownership fingerprint"); + updateDigestLong(digest, marker.length); + digest.update(marker); + } else { + updateDigestLong(digest, -1L); + } + } + + private static void updateFingerprintValue(MessageDigest digest, String value) { + byte[] bytes = Objects.requireNonNullElse(value, "").getBytes(StandardCharsets.UTF_8); + updateDigestInt(digest, bytes.length); + digest.update(bytes); + } + + private static String failureMessage(Throwable exception) { + if (exception == null) { + return "unknown failure"; + } + String message = exception.getMessage(); + return message == null || message.isBlank() ? exception.getClass().getSimpleName() : message; + } + + public static void refreshWorkspaces() { + try (Stream stream = ServerConfigurator.allPacks()) { + stream.forEach(DatapackIngestService::refreshWorkspace); + } + } + + public static void refreshWorkspace(IrisData data) { + if (data == null || !hasImports(data)) { + return; + } + try { + new IrisCodeWorkspace(new IrisProject(data.getDataFolder())).updateWorkspace(); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + public static Report ingest(VolmitSender sender, KList urls, boolean restart) { + return ingest(sender, urls, restart, false); + } + + /** + * Only startup validation owns the global player-admission gate (gateAdmission=true). The + * runtime admin command must never flip it: a transient download failure there would lock + * every login until restart, and the PENDING window would close logins for the whole + * (potentially very slow) download. World-creation safety on the ungated path is carried + * by the loaded-runtime invalidation plus requireDatapackRestart when files change. + */ + private static Report ingest(VolmitSender sender, KList urls, boolean restart, boolean gateAdmission) { + if (gateAdmission) { + IrisStartupValidation.beginDatapackValidation(); + } + ServerConfigurator.LoadedDatapackRuntimeInvalidation invalidation = + ServerConfigurator.invalidateLoadedDatapackRuntime(); + Report report; + boolean settled = false; + TRANSACTION_LOCK.lock(); + try { + report = ingestLocked(sender, urls, restart); + settled = true; + } finally { + TRANSACTION_LOCK.unlock(); + if (!settled && gateAdmission) { + // Never strand the admission gate at PENDING on an unexpected throw. + IrisStartupValidation.markDatapacksInvalid( + "External datapack ingest failed unexpectedly; check the log above."); + } + } + if (!report.changed() && report.getFailed().isEmpty()) { + ServerConfigurator.restoreLoadedDatapackRuntimeIfUnchanged(invalidation); + } else if (report.changed()) { + if (restart) { + ServerConfigurator.restart(); + } else { + ServerConfigurator.requireDatapackRestart(); + } + } + if (gateAdmission) { + if (!report.getFailed().isEmpty()) { + IrisStartupValidation.markDatapacksInvalid(report.getFailed().getFirst()); + } else if (!report.changed()) { + IrisStartupValidation.markDatapacksReady(); + } + } + return report; + } + + private static Report ingestLocked(VolmitSender sender, KList urls, boolean restart) { + Report report = new Report(); + if (urls == null || urls.isEmpty()) { + message(sender, C.YELLOW + "No datapackImports configured in any loaded pack. Add Modrinth URLs to a dimension's 'datapackImports' list, then run /iris datapack ingest."); + return report; + } + + File root = IrisPlatforms.get().dataFolder("datapacks"); + File cacheDir = new File(root, "cache"); + File stagingDir = new File(root, "staging"); + try { + ensureScratchDirectory(cacheDir, "datapack download cache"); + ensureScratchDirectory(stagingDir, "datapack staging"); + } catch (IOException e) { + report.failed.add("local storage - " + e.getMessage()); + message(sender, C.RED + "Datapack ingest failed: " + e.getMessage()); + IrisLogging.reportError(e); + return report; + } + + KList worldFolders = ServerConfigurator.getDatapacksFolder(); + String mcVersion = serverMcVersion(); + try { + recoverTransactions(root, worldFolders); + } catch (IOException e) { + report.failed.add("transaction recovery - " + e.getMessage()); + message(sender, C.RED + "Datapack ingest blocked by incomplete transaction recovery: " + e.getMessage()); + IrisLogging.reportError(e); + return report; + } + Manifest manifest = readManifest(root); + boolean stripOverrides = resolveStripOverrides(); + List installs = new ArrayList<>(); + + message(sender, C.GRAY + "Ingesting " + C.WHITE + urls.size() + C.GRAY + " datapack import(s)" + (mcVersion == null ? "" : " for MC " + mcVersion) + (stripOverrides ? C.GRAY + " (datapackOverrides=false: minecraft-namespaced structure overrides will be stripped)" : "") + "..."); + + for (String url : urls) { + try { + ingestSingle( + sender, + url, + mcVersion, + cacheDir, + stagingDir, + worldFolders, + manifest, + report, + stripOverrides, + installs + ); + } catch (Exception e) { + report.failed.add(url + " - " + e.getMessage()); + message(sender, C.RED + " Failed: " + C.WHITE + url + C.RED + " - " + e.getMessage()); + IrisLogging.reportError(e); + } + } + + diagnoseConflicts(sender, manifest); + ManifestWrite manifestWrite = null; + boolean manifestDurabilityConfirmed = false; + try { + for (InstallExecution install : installs) { + verifyInstallExecution(install); + } + manifestWrite = prepareManifestWrite(root, manifest); + manifestWrite.publish(); + manifestDurabilityConfirmed = true; + } catch (IOException manifestFailure) { + if (manifestWrite == null || !manifestWrite.published()) { + rollbackInstallExecutions(installs, manifestFailure); + report.failed.add("manifest - " + manifestFailure.getMessage()); + report.updated.clear(); + report.upToDate.clear(); + report.requiresRestart = false; + message(sender, C.RED + "Datapack ingest rolled back before the manifest commit: " + + manifestFailure.getMessage()); + IrisLogging.reportError(manifestFailure); + return report; + } + IrisLogging.reportError("Datapack manifest was published but durability confirmation failed; " + + "transaction backups are retained for restart recovery.", manifestFailure); + } finally { + if (manifestWrite != null) { + try { + manifestWrite.discard(); + } catch (IOException cleanupFailure) { + IrisLogging.reportError("Datapack manifest staging cleanup failed.", cleanupFailure); + } + } + } + if (manifestDurabilityConfirmed) { + for (InstallExecution install : installs) { + try { + finishInstallExecution(install); + } catch (IOException cleanupFailure) { + IrisLogging.reportError("Datapack install committed but transaction cleanup requires restart recovery.", + cleanupFailure); + } + } + } + pruneCache(cacheDir); + message(sender, C.GREEN + "Datapack ingest complete: " + C.WHITE + report.updated.size() + C.GREEN + " updated, " + C.WHITE + report.upToDate.size() + C.GREEN + " up to date, " + C.WHITE + report.failed.size() + C.GREEN + " failed."); + + if (report.changed()) { + message(sender, C.YELLOW + "New datapack structures were installed. A server restart is required for them to register and generate."); + message(sender, C.GRAY + "After the restart they generate natively only in Iris dimensions that declare their source URL - no import needed. To get editable Iris copies (jigsaw pools, pieces & objects written into the pack) run /iris structure import , or set general.autoImportDatapackStructures=true to do it on every ingest. Place any registered key directly with a 'structures' placement using nativeStructures."); + message(sender, C.GRAY + "Datapacks replace matching vanilla structure keys by default. Set 'importedStructures.datapackOverrides' to false to keep minecraft-namespaced structure definitions untouched; deny non-minecraft datapack and mod structure families with importedStructures.disabled or complete keys with importedStructures.disabledExact."); + if (!restart) { + message(sender, C.GRAY + "Run with restart=true to restart now, or restart manually. After restart, run /iris structure list to see the new keys."); + } + } + + return report; + } + + public static ReapplyOutcome reapplyFromStaging(KList worldFolders) { + ServerConfigurator.LoadedDatapackRuntimeInvalidation invalidation = + ServerConfigurator.invalidateLoadedDatapackRuntime(); + ReapplyOutcome outcome; + TRANSACTION_LOCK.lock(); + try { + outcome = reapplyFromStagingLocked(worldFolders); + } finally { + TRANSACTION_LOCK.unlock(); + } + if (outcome.succeeded() && !outcome.changed()) { + ServerConfigurator.restoreLoadedDatapackRuntimeIfUnchanged(invalidation); + } else if (outcome.changed()) { + ServerConfigurator.requireDatapackRestart(); + } else { + IrisStartupValidation.markDatapacksInvalid(outcome.failure() + .map(DatapackIngestService::failureMessage) + .orElse("External datapack recovery failed.")); + } + return outcome; + } + + private static ReapplyOutcome reapplyFromStagingLocked(KList worldFolders) { + File root = IrisPlatforms.get().dataFolder("datapacks"); + ReapplyOutcome recovery = recoverBeforeReapplyOutcome(root, worldFolders); + if (!recovery.succeeded()) { + return reportReapplyFailure(recovery); + } + File stagingDir = IrisPlatforms.get().dataFolderNoCreate("datapacks", "staging"); + ReapplyOutcome repair = reapplyStagingRootOutcome( + root, + stagingDir, + worldFolders, + resolveStripOverrides()); + if (!repair.succeeded()) { + return reportReapplyFailure(repair); + } + return ReapplyOutcome.success(recovery.recovered(), repair.repaired()); + } + + static boolean reapplyStagingRoot( + File root, + File stagingDir, + KList worldFolders, + boolean stripOverrides + ) { + return reportReapplyFailure(reapplyStagingRootOutcome( + root, + stagingDir, + worldFolders, + stripOverrides)).succeeded(); + } + + static ReapplyOutcome reapplyStagingRootOutcome( + File root, + File stagingDir, + KList worldFolders, + boolean stripOverrides + ) { + Manifest manifest = readManifest(root); + if (stagingDir == null + || !Files.exists(stagingDir.toPath(), LinkOption.NOFOLLOW_LINKS)) { + if (manifest.entries.isEmpty()) { + return ReapplyOutcome.success(false, false); + } + File missing = stagingDir == null ? new File(root, "staging") : stagingDir; + return ReapplyOutcome.failed(new IOException( + "Managed datapack staging is missing at " + missing.getPath())); + } + if (Files.isSymbolicLink(stagingDir.toPath()) + || !Files.isDirectory(stagingDir.toPath(), LinkOption.NOFOLLOW_LINKS)) { + return ReapplyOutcome.failed(new IOException( + "Managed datapack staging is not a safe directory at " + stagingDir.getPath())); + } + return reapplyStagedDirectoriesOutcome( + root, stagingDir, worldFolders, stripOverrides, manifest); + } + + static boolean reapplyStagedDirectories( + File root, + File stagingDir, + KList worldFolders, + boolean stripOverrides + ) { + if (Files.isSymbolicLink(stagingDir.toPath()) + || !Files.isDirectory(stagingDir.toPath(), LinkOption.NOFOLLOW_LINKS)) { + return reportReapplyFailure(ReapplyOutcome.failed(new IOException( + "Managed datapack staging is not a safe directory at " + + stagingDir.getPath()))).succeeded(); + } + return reportReapplyFailure(reapplyStagedDirectoriesOutcome( + root, + stagingDir, + worldFolders, + stripOverrides, + readManifest(root))).succeeded(); + } + + private static ReapplyOutcome reapplyStagedDirectoriesOutcome( + File root, + File stagingDir, + KList worldFolders, + boolean stripOverrides, + Manifest manifest + ) { + File[] staged = stagingDir.listFiles(File::isDirectory); + if (staged == null) { + return ReapplyOutcome.failed(new IOException( + "Unable to enumerate managed datapack staging at " + stagingDir.getPath())); + } + boolean repaired = false; + IOException failure = null; + for (Entry entry : manifest.entries) { + File stagedDir = new File(stagingDir, entry.id); + if (isRecordedUnchangedInstall(stagedDir, worldFolders, entry, stripOverrides)) { + continue; + } + if (!isUsableStaging(stagedDir, entry)) { + forgetInstallMetadata(entry); + failure = appendFailure(failure, new IOException( + "Managed datapack staging is unusable for '" + entry.id + + "' at " + stagedDir.getPath())); + continue; + } + try { + InstallResult result = install(stagedDir, worldFolders, entry, stripOverrides); + if (result.changed()) { + repaired = true; + IrisLogging.warn("Repaired installed datapack '" + entry.id + + "' from Iris staging before datapack compilation."); + } + recordInstallMetadata(stagedDir, worldFolders, entry); + } catch (IOException e) { + forgetInstallMetadata(entry); + failure = appendFailure(failure, e); + } + } + writeManifest(root, manifest); + return failure == null + ? ReapplyOutcome.success(false, repaired) + : ReapplyOutcome.failed(failure); + } + + private static IOException appendFailure(IOException current, IOException additional) { + if (current == null) { + return additional; + } + current.addSuppressed(additional); + return current; + } + + private static ReapplyOutcome reportReapplyFailure(ReapplyOutcome outcome) { + if (!outcome.succeeded()) { + IrisLogging.reportError( + "External datapack recovery or staging repair failed.", + outcome.failure().orElseThrow()); + } + return outcome; + } + + private static boolean isRecordedUnchangedInstall( + File stagedDir, + KList worldFolders, + Entry entry, + boolean stripOverrides + ) { + if (entry.stagingMetadata == null || entry.stagingMetadata.isBlank() + || entry.installMetadata == null || entry.installMetadata.size() != worldFolders.size()) { + return false; + } + try { + if (!isRecordedManagedDirectory(stagedDir, entry) + || !entry.stagingMetadata.equals(metadataDigest(stagedDir))) { + return false; + } + for (File worldFolder : worldFolders) { + File target = new File(worldFolder, entry.id); + if (!isRecordedManagedDirectory(target, entry) + || new File(target, OVERRIDES_STRIPPED_MARKER).isFile() != stripOverrides) { + return false; + } + String recorded = entry.installMetadata.get(installMetadataKey(target)); + if (recorded == null || !recorded.equals(metadataDigest(target))) { + return false; + } + } + return true; + } catch (IOException e) { + IrisLogging.debug("Managed datapack '" + entry.id + + "' requires full verification: " + e.getMessage()); + return false; + } + } + + private static boolean isRecordedManagedDirectory(File directory, Entry entry) throws IOException { + Path path = directory.toPath(); + if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(path)) { + return false; + } + if (!new File(directory, "pack.mcmeta").isFile()) { + return false; + } + Ownership ownership = readOwnershipOrNull(directory); + return ownership != null + && ownershipSourceMatches(ownership, entry) + && Objects.equals(ownership.versionId, entry.versionId) + && Objects.equals(ownership.versionNumber, entry.versionNumber) + && Objects.equals(ownership.sha1, entry.sha1); + } + + private static void recordInstallMetadata(File stagedDir, KList worldFolders, Entry entry) { + try { + Map recorded = new HashMap<>(); + for (File worldFolder : worldFolders) { + File target = new File(worldFolder, entry.id); + recorded.put(installMetadataKey(target), metadataDigest(target)); + } + entry.stagingMetadata = metadataDigest(stagedDir); + entry.installMetadata = recorded; + } catch (IOException e) { + IrisLogging.debug("Unable to record managed datapack metadata for '" + entry.id + + "': " + e.getMessage()); + forgetInstallMetadata(entry); + } + } + + private static void forgetInstallMetadata(Entry entry) { + entry.stagingMetadata = ""; + entry.installMetadata = new HashMap<>(); + } + + private static String installMetadataKey(File target) { + return target.toPath().toAbsolutePath().normalize().toString(); + } + + private static String metadataDigest(File root) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + Path rootPath = root.toPath().toAbsolutePath().normalize(); + List entries = new ArrayList<>(); + try (Stream paths = Files.walk(rootPath)) { + Iterator iterator = paths.iterator(); + int pathCount = 0; + while (iterator.hasNext()) { + Path path = iterator.next(); + if (path.equals(rootPath) || isFinderMetadata(path)) { + continue; + } + pathCount++; + if (pathCount > MAX_MANAGED_PATHS) { + throw new IOException("Datapack contains more than " + MAX_MANAGED_PATHS + " paths"); + } + if (Files.isSymbolicLink(path)) { + throw new IOException("Datapack contains a symbolic link: " + path); + } + String relativePath = rootPath.relativize(path).toString(); + entries.add(new MetadataEntry(path, relativePath)); + } + } + entries.sort(Comparator.comparing(MetadataEntry::relativePath)); + for (MetadataEntry entry : entries) { + String relative = entry.relativePath().replace(File.separatorChar, '/'); + byte[] relativeBytes = relative.getBytes(StandardCharsets.UTF_8); + BasicFileAttributes attributes = Files.readAttributes( + entry.path(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (!attributes.isDirectory() && !attributes.isRegularFile()) { + throw new IOException("Datapack contains an unsupported filesystem entry: " + entry.path()); + } + digest.update((byte) (attributes.isDirectory() ? 1 : 2)); + updateDigestInt(digest, relativeBytes.length); + digest.update(relativeBytes); + if (!attributes.isDirectory()) { + updateDigestLong(digest, attributes.size()); + updateDigestLong(digest, attributes.lastModifiedTime().toMillis()); + } + } + return hex(digest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 algorithm unavailable", e); + } + } + + static boolean recoverBeforeReapply(File root, List worldFolders) { + return reportReapplyFailure(recoverBeforeReapplyOutcome(root, worldFolders)).succeeded(); + } + + private static ReapplyOutcome recoverBeforeReapplyOutcome(File root, List worldFolders) { + try { + return ReapplyOutcome.success(recoverTransactions(root, worldFolders), false); + } catch (IOException e) { + return ReapplyOutcome.failed(e); + } + } + + enum RemoveOutcome { + REMOVED, + REJECTED, + FAILED_AFTER_MUTATION + } + + public static boolean remove(VolmitSender sender, String id) { + ServerConfigurator.LoadedDatapackRuntimeInvalidation invalidation = + ServerConfigurator.invalidateLoadedDatapackRuntime(); + RemoveOutcome outcome; + TRANSACTION_LOCK.lock(); + try { + outcome = removeOutcomeLocked(sender, id); + } finally { + TRANSACTION_LOCK.unlock(); + } + if (outcome == RemoveOutcome.REMOVED) { + ServerConfigurator.requireDatapackRestart(); + } else if (outcome == RemoveOutcome.REJECTED) { + // Rejected before any mutation: nothing on disk changed, so the loaded runtime is + // still valid. Leaving it invalidated forced a full datapack reinstall on every + // later world creation for the rest of the session. + ServerConfigurator.restoreLoadedDatapackRuntimeIfUnchanged(invalidation); + } + return outcome == RemoveOutcome.REMOVED; + } + + private static RemoveOutcome removeOutcomeLocked(VolmitSender sender, String id) { + File root = IrisPlatforms.get().dataFolder("datapacks"); + return removeOutcomeLocked(sender, id, root, ServerConfigurator.getDatapacksFolder()); + } + + static boolean removeLocked(VolmitSender sender, String id, File root, List worldFolders) { + return removeOutcomeLocked(sender, id, root, worldFolders) == RemoveOutcome.REMOVED; + } + + static RemoveOutcome removeOutcomeLocked(VolmitSender sender, String id, File root, List worldFolders) { + String requested = id == null ? "" : id.trim().toLowerCase(Locale.ROOT); + String cleaned = sanitizeId(id); + if (requested.isBlank() || !requested.equals(cleaned) || RESERVED_IDS.contains(cleaned)) { + message(sender, C.RED + "Invalid Iris-managed datapack id '" + requested + "'. Run /iris datapack list and use the exact listed id."); + return RemoveOutcome.REJECTED; + } + try { + recoverTransactions(root, worldFolders); + } catch (IOException e) { + message(sender, C.RED + "Datapack removal blocked by incomplete transaction recovery: " + e.getMessage()); + IrisLogging.reportError(e); + return RemoveOutcome.REJECTED; + } + Manifest manifest = readManifest(root); + Entry ownedEntry = manifest.findById(cleaned); + if (ownedEntry == null) { + message(sender, C.YELLOW + "No Iris-managed datapack named '" + cleaned + "'. Unmanaged world datapacks are never removed by Iris."); + return RemoveOutcome.REJECTED; + } + + List targets; + try { + targets = preflightRemovalTargets(root, worldFolders, ownedEntry); + } catch (IOException e) { + message(sender, C.RED + "Refused to remove datapack '" + cleaned + "': " + e.getMessage()); + IrisLogging.reportError(e); + return RemoveOutcome.REJECTED; + } + + EditableImportRemoval editableRemoval = null; + DirectoryRemoval directoryRemoval = null; + ManifestWrite manifestWrite = null; + DatapackCoordinator coordinator = null; + try { + editableRemoval = prepareEntryImportRemoval(ownedEntry, manifest.entries); + directoryRemoval = prepareOwnedDirectories(targets); + if (!manifest.removeById(cleaned)) { + throw new IOException("Datapack manifest entry disappeared during removal"); + } + manifestWrite = prepareManifestWrite(root, manifest); + coordinator = createRemovalCoordinator(root, ownedEntry, directoryRemoval, editableRemoval); + coordinator.phase(CoordinatorPhase.PUBLISHING); + directoryRemoval.prepare(); + coordinator.phase(CoordinatorPhase.PUBLISHED); + manifestWrite.publish(); + } catch (IOException | RuntimeException removalFailure) { + boolean manifestCommitted = manifestWrite != null && manifestWrite.published(); + if (manifestCommitted) { + finishCommittedRemoval(cleaned, manifestWrite, directoryRemoval, editableRemoval, coordinator, + removalFailure); + message(sender, C.GREEN + "Removed datapack '" + C.WHITE + cleaned + C.GREEN + + "'. Restart for it to stop generating, and delete its URL from the pack's datapackImports to keep it gone."); + return RemoveOutcome.REMOVED; + } + boolean restored = rollbackRemoval(manifestWrite, directoryRemoval, editableRemoval, removalFailure); + if (restored && coordinator != null) { + try { + coordinator.finish(); + } catch (IOException cleanupFailure) { + removalFailure.addSuppressed(cleanupFailure); + } + } + message(sender, C.RED + "Failed to remove datapack '" + cleaned + + "'; Iris attempted to restore every prior location: " + removalFailure.getMessage()); + IrisLogging.reportError(removalFailure); + // A mutation was attempted; even a successful rollback is not provably identical + // (the fingerprint does not cover datapacks/), so stay conservatively invalidated. + return RemoveOutcome.FAILED_AFTER_MUTATION; + } + finishCommittedRemoval(cleaned, manifestWrite, directoryRemoval, editableRemoval, coordinator, null); + message(sender, C.GREEN + "Removed datapack '" + C.WHITE + cleaned + C.GREEN + + "'. Restart for it to stop generating, and delete its URL from the pack's datapackImports to keep it gone."); + return RemoveOutcome.REMOVED; + } + + private static void finishCommittedRemoval( + String id, + ManifestWrite manifestWrite, + DirectoryRemoval directoryRemoval, + EditableImportRemoval editableRemoval, + DatapackCoordinator coordinator, + Throwable priorFailure + ) { + if (priorFailure != null) { + IOException recoveryFailure = new IOException( + "Datapack manifest committed before publication durability was confirmed", + priorFailure + ); + if (editableRemoval != null) { + try { + editableRemoval.leaveForRecovery(); + } catch (IOException releaseFailure) { + recoveryFailure.addSuppressed(releaseFailure); + } + } + try { + manifestWrite.discard(); + } catch (IOException cleanupFailure) { + recoveryFailure.addSuppressed(cleanupFailure); + } + IrisLogging.reportError("Datapack '" + id + + "' was removed but transaction cleanup requires restart recovery.", recoveryFailure); + return; + } + + IOException failure = null; + if (coordinator != null) { + try { + coordinator.phase(CoordinatorPhase.COMMITTED); + } catch (IOException phaseFailure) { + failure = appendIOException(failure, phaseFailure); + if (editableRemoval != null) { + try { + editableRemoval.leaveForRecovery(); + } catch (IOException releaseFailure) { + failure = appendIOException(failure, releaseFailure); + } + } + try { + manifestWrite.discard(); + } catch (IOException cleanupFailure) { + failure = appendIOException(failure, cleanupFailure); + } + IrisLogging.reportError("Datapack '" + id + + "' was removed but transaction cleanup requires restart recovery.", failure); + return; + } + } + if (editableRemoval != null) { + try { + editableRemoval.markCommitted(); + editableRemoval.finishCommit(); + } catch (IOException | RuntimeException cleanupFailure) { + failure = appendIOException(failure, cleanupFailure); + try { + editableRemoval.leaveForRecovery(); + } catch (IOException releaseFailure) { + failure = appendIOException(failure, releaseFailure); + } + } + } + if (directoryRemoval != null) { + try { + directoryRemoval.finishCommit(); + } catch (IOException | RuntimeException cleanupFailure) { + failure = appendIOException(failure, cleanupFailure); + } + } + try { + manifestWrite.discard(); + } catch (IOException cleanupFailure) { + failure = appendIOException(failure, cleanupFailure); + } + if (failure == null && coordinator != null) { + try { + coordinator.finish(); + } catch (IOException cleanupFailure) { + failure = cleanupFailure; + } + } + if (failure != null) { + IrisLogging.reportError("Datapack '" + id + + "' was removed but transaction cleanup requires restart recovery.", failure); + } + } + + private static IOException appendIOException(IOException current, Throwable failure) { + IOException next = failure instanceof IOException ioFailure + ? ioFailure : new IOException("Datapack transaction participant failed", failure); + if (current == null) { + return next; + } + current.addSuppressed(next); + return current; + } + + private static List preflightRemovalTargets(File root, List worldFolders, Entry entry) throws IOException { + List targets = new ArrayList<>(); + Set seen = new HashSet<>(); + File staging = new File(root, "staging"); + verifyDirectoryContainerIfPresent(staging, "datapack staging"); + File stagedDir = new File(staging, entry.id); + verifyAndCollectRemovalTarget(stagedDir, entry, targets, seen); + for (File worldFolder : worldFolders) { + verifyDirectoryContainerIfPresent(worldFolder, "world datapacks"); + verifyAndCollectRemovalTarget(new File(worldFolder, entry.id), entry, targets, seen); + } + return targets; + } + + private static void verifyAndCollectRemovalTarget( + File target, + Entry entry, + List targets, + Set seen + ) throws IOException { + verifyOwnedDirectoryIfPresent(target, entry); + Path normalized = target.toPath().toAbsolutePath().normalize(); + if (pathExists(normalized, "datapack removal target")) { + Path identity = normalized.toRealPath(); + if (!seen.add(identity)) { + throw new IOException("Aliased datapack removal target " + normalized); + } + targets.add(target); + } + } + + private static EditableImportRemoval prepareEntryImportRemoval( + Entry entry, + List manifestEntries + ) throws IOException { + List prepared = new ArrayList<>(); + Set targetIdSet = new TreeSet<>(entry.importedBundles.keySet()); + targetIdSet.addAll(entry.importedTargets.keySet()); + targetIdSet.addAll(entry.importAttempts.keySet()); + List targetIds = new ArrayList<>(targetIdSet); + targetIds.sort(String::compareTo); + try { + for (String targetId : targetIds) { + Set retainedKeys = invalidateRetainedImportClaims( + entry, targetId, manifestEntries); + File dataFolder = new File(targetId); + if (!dataFolder.isDirectory()) { + continue; + } + StructureTransactionWriter writer = new StructureTransactionWriter(dataFolder.toPath()); + List removals = ownedImportRemovals( + writer, + entry, + targetId, + retainedKeys + ); + StructureTransactionWriter.PreparedRemoval removal = + writer.prepareMatchingOwnedRemovals(removals); + prepared.add(new PreparedEditableImport(dataFolder, removal)); + } + return new EditableImportRemoval(prepared); + } catch (IOException | RuntimeException preparationFailure) { + IOException failure = preparationFailure instanceof IOException ioFailure + ? ioFailure + : new IOException("Failed preparing editable datapack import cleanup", preparationFailure); + for (int i = prepared.size() - 1; i >= 0; i--) { + try { + prepared.get(i).removal().rollback(); + } catch (IOException rollbackFailure) { + failure.addSuppressed(rollbackFailure); + } + } + throw failure; + } + } + + private static List ownedImportRemovals( + StructureTransactionWriter writer, + Entry removingEntry, + String targetId, + Set retainedKeys + ) throws IOException { + List removals = new ArrayList<>(); + Map> removingClaims = importBundleClaims(removingEntry, targetId); + for (Map.Entry> bundle : removingClaims.entrySet()) { + try { + StructureKey targetKey = StructureKey.parse(bundle.getKey()); + if (retainedKeys.contains(bundle.getKey())) { + continue; + } + Optional ownedSource = writer.ownedSource(targetKey); + if (ownedSource.isEmpty() || !sourceClaimsContain(bundle.getValue(), ownedSource.get())) { + continue; + } + removals.add(StructureTransactionWriter.OwnedRemoval.managedDatapack( + targetKey, + ownedSource.get().kind(), + ownedSource.get().key() + )); + } catch (RuntimeException e) { + throw new IOException("Invalid editable structure import inventory entry '" + + bundle.getKey() + "' -> '" + bundle.getValue() + "'", e); + } + } + return List.copyOf(removals); + } + + private static Set invalidateRetainedImportClaims( + Entry removingEntry, + String targetId, + List manifestEntries + ) { + Set removingKeys = importBundleClaims(removingEntry, targetId).keySet(); + Set retainedKeys = new TreeSet<>(); + for (Entry candidate : manifestEntries) { + if (candidate == removingEntry) { + continue; + } + Set candidateKeys = importBundleClaims(candidate, targetId).keySet(); + boolean candidateRetained = false; + for (String candidateKey : candidateKeys) { + if (removingKeys.contains(candidateKey)) { + retainedKeys.add(candidateKey); + candidateRetained = true; + } + } + if (candidateRetained) { + candidate.importedTargets.remove(targetId); + candidate.importAttempts.remove(targetId); + candidate.structuresImported = false; + } + } + return Set.copyOf(retainedKeys); + } + + private static Map> importBundleClaims(Entry entry, String targetId) { + Map> claims = new TreeMap<>(); + addImportBundleClaims(claims, entry.importedBundles.getOrDefault(targetId, Map.of())); + if (entry.importedBundles.containsKey(targetId) || entry.importedTargets.containsKey(targetId)) { + addImportBundleClaims(claims, importBundleInventory(entry)); + } + return claims; + } + + private static void addImportBundleClaims( + Map> claims, + Map inventory + ) { + for (Map.Entry bundle : inventory.entrySet()) { + claims.computeIfAbsent(bundle.getKey(), ignored -> new TreeSet<>()).add(bundle.getValue()); + } + } + + private static boolean sourceClaimsContain(Set claims, StructureSource source) throws IOException { + for (String claimedKey : claims) { + try { + StructureKey sourceKey = StructureKey.parse(claimedKey); + StructureSource.Kind sourceKind = sourceKey.namespace().equals("minecraft") + ? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK; + if (source.kind() == sourceKind && source.key().equals(sourceKey)) { + return true; + } + } catch (RuntimeException e) { + throw new IOException("Invalid editable structure source key '" + claimedKey + "'", e); + } + } + return false; + } + + private static DirectoryRemoval prepareOwnedDirectories(List targets) throws IOException { + List planned = new ArrayList<>(); + for (File target : targets) { + File parent = target.getParentFile(); + File backupRoot = new File(parent.getParentFile() == null ? parent : parent.getParentFile(), ".iris-datapack-remove"); + File backup = new File(backupRoot, target.getName() + "-" + UUID.randomUUID()); + ensureScratchDirectory(backupRoot, "datapack removal backup"); + validateInstallTree(target, parent, "Datapack removal target"); + planned.add(new DirectoryMove( + target, + backup, + directoryHash(target), + ownershipMarkerFingerprint(target), + directoryIdentity(target), + realDirectoryPath(parent, "datapack target root"), + realDirectoryPath(backupRoot, "datapack removal scratch root"), + directoryIdentity(parent), + directoryIdentity(backupRoot) + )); + } + return new DirectoryRemoval(planned); + } + + private static boolean rollbackRemoval( + ManifestWrite manifestWrite, + DirectoryRemoval directoryRemoval, + EditableImportRemoval editableRemoval, + Throwable removalFailure + ) { + if (manifestWrite != null) { + try { + manifestWrite.discard(); + } catch (IOException discardFailure) { + removalFailure.addSuppressed(discardFailure); + } + } + if (directoryRemoval != null) { + try { + directoryRemoval.rollback(); + } catch (IOException rollbackFailure) { + removalFailure.addSuppressed(rollbackFailure); + } + } + if (editableRemoval != null) { + try { + editableRemoval.rollback(); + } catch (IOException rollbackFailure) { + removalFailure.addSuppressed(rollbackFailure); + } + } + return removalFailure.getSuppressed().length == 0; + } + + private static void verifyOwnedDirectoryIfPresent(File directory, Entry entry) throws IOException { + if (!Files.exists(directory.toPath(), LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (Files.isSymbolicLink(directory.toPath()) + || !Files.isDirectory(directory.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Refusing non-directory or symbolic-link target " + directory.getPath()); + } + Ownership ownership = readOwnership(directory); + if (!ownershipSourceMatches(ownership, entry)) { + throw new IOException("Ownership marker at " + directory.getPath() + " belongs to '" + ownership.id + "'"); + } + removeFinderMetadata(directory); + if (!Objects.equals(ownership.contentHash, directoryHash(directory))) { + throw new IOException("Refusing to remove modified or corrupt Iris-managed datapack " + directory.getPath()); + } + } + + public static KList collectConfiguredImports() { + LinkedHashSet urls = new LinkedHashSet<>(); + try (Stream stream = ServerConfigurator.allPacks()) { + stream.forEach(data -> collectImports(data, urls)); + } + KList result = new KList<>(); + result.addAll(urls); + return result; + } + + public static List installed() { + TRANSACTION_LOCK.lock(); + try { + File root = IrisPlatforms.get().dataFolder("datapacks"); + try { + recoverTransactions(root, ServerConfigurator.getDatapacksFolder()); + } catch (IOException e) { + IrisLogging.reportError("Could not recover datapack transactions before listing installed packs.", e); + return List.of(); + } + return List.copyOf(readManifest(root).entries); + } finally { + TRANSACTION_LOCK.unlock(); + } + } + + public static List installedStructureScopeResources() throws IOException { + TRANSACTION_LOCK.lock(); + try { + File root = IrisPlatforms.get().dataFolder("datapacks"); + Manifest manifest = readManifest(root); + KList datapackFolders = ServerConfigurator.getDatapacksFolder(); + List resources = new ArrayList<>(); + for (Entry entry : manifest.entries) { + boolean found = false; + for (File datapackFolder : datapackFolders) { + File installedDirectory = new File(datapackFolder, entry.id); + if (!Files.exists(installedDirectory.toPath(), LinkOption.NOFOLLOW_LINKS)) { + continue; + } + resources.add(scanInstalledStructureScope(installedDirectory, entry)); + found = true; + } + if (!found) { + throw new IOException("Missing installed Iris-managed datapack '" + entry.id + "'"); + } + } + return List.copyOf(resources); + } finally { + TRANSACTION_LOCK.unlock(); + } + } + + static StructureScopeResources scanInstalledStructureScope(File directory, Entry entry) throws IOException { + Path path = directory.toPath(); + if (Files.isSymbolicLink(path) + || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid installed Iris-managed datapack directory " + directory.getPath()); + } + validatePackMetadata(directory); + rejectSymbolicLinks(directory); + Ownership ownership = readOwnership(directory); + if (!ownershipSourceMatches(ownership, entry)) { + throw new IOException("Installed datapack ownership mismatch at " + directory.getPath()); + } + if (!Objects.equals(ownership.contentHash, directoryHash(directory))) { + throw new IOException("Installed Iris-managed datapack is modified or corrupt at " + + directory.getPath()); + } + PackResources resources = scanPackResources(directory); + return new StructureScopeResources( + entry.url, + resources.structureKeys(), + resources.structureSetKeys()); + } + + private static void ingestSingle( + VolmitSender sender, + String url, + String mcVersion, + File cacheDir, + File stagingDir, + KList worldFolders, + Manifest manifest, + Report report, + boolean stripOverrides, + List installs + ) throws IOException { + ResolvedDatapack resolved = ModrinthResolver.resolve(url, mcVersion); + Entry existing = manifest.find(url); + String id = existing == null ? deriveId(resolved) : existing.id; + if (RESERVED_IDS.contains(id)) { + throw new IOException("Datapack id '" + id + "' is reserved by Iris"); + } + Entry idCollision = manifest.findById(id); + if (idCollision != null && !Objects.equals(idCollision.url, url)) { + throw new IOException("Datapack id '" + id + "' is already owned by " + idCollision.url); + } + File stagedDir = new File(stagingDir, id); + boolean stageUsable = existing != null && isUsableStaging(stagedDir, existing); + boolean recoveringManagedStaging = existing != null && !stageUsable; + boolean sameVersion = !resolved.isDirect() + && existing != null + && Objects.equals(existing.versionId, resolved.getVersionId()) + && (resolved.getSha1() == null || Objects.equals(existing.sha1, resolved.getSha1())) + && stageUsable; + + if (sameVersion) { + InstallExecution execution = prepareInstallExecution( + stagedDir, worldFolders, existing, stripOverrides, cacheDir.getParentFile()); + installs.add(execution); + InstallResult installResult = execution.result(); + recordInstallResult( + sender, report, stagedDir, worldFolders, existing, installResult, resolved.getVersionNumber()); + return; + } + + message(sender, C.GRAY + " Checking " + C.WHITE + id + C.GRAY + " " + safe(resolved.getVersionNumber()) + "..."); + File zip = new File(cacheDir, id + "-" + safeFile(resolved.getVersionId()) + ".zip"); + DownloadResult download = download( + resolved.getDownloadUrl(), + zip, + resolved.isDirect() && stageUsable ? existing.etag : null, + resolved.isDirect() && stageUsable ? existing.lastModified : null + ); + if (download.notModified()) { + if (!stageUsable) { + throw new IOException("Remote returned not-modified but Iris staging is missing or corrupt for " + id); + } + Entry updated = copyEntry(existing); + updated.etag = download.etag(); + updated.lastModified = download.lastModified(); + InstallExecution execution = prepareInstallExecution( + stagedDir, worldFolders, updated, stripOverrides, cacheDir.getParentFile()); + installs.add(execution); + InstallResult installResult = execution.result(); + manifest.put(updated); + recordInstallResult( + sender, report, stagedDir, worldFolders, updated, installResult, resolved.getVersionNumber()); + return; + } + + String checksum = sha1(zip); + if (resolved.getSha1() != null && !resolved.getSha1().isBlank() && !resolved.getSha1().equalsIgnoreCase(checksum)) { + IO.delete(zip); + throw new IOException("Checksum mismatch for " + id + " (expected " + resolved.getSha1() + ", got " + checksum + ")"); + } + + if (resolved.isDirect() && existing != null && Objects.equals(existing.sha1, checksum) && stageUsable) { + Entry updated = copyEntry(existing); + updated.etag = download.etag(); + updated.lastModified = download.lastModified(); + updated.installedEpoch = System.currentTimeMillis(); + InstallExecution execution = prepareInstallExecution( + stagedDir, worldFolders, updated, stripOverrides, cacheDir.getParentFile()); + installs.add(execution); + InstallResult installResult = execution.result(); + writeOwnership(stagedDir, updated); + manifest.put(updated); + recordInstallResult( + sender, report, stagedDir, worldFolders, updated, installResult, resolved.getVersionNumber()); + return; + } + + Entry entry = existing != null ? copyEntry(existing) : new Entry(); + entry.url = url; + entry.id = id; + entry.versionId = resolved.getVersionId(); + entry.versionNumber = resolved.getVersionNumber(); + entry.sha1 = checksum; + entry.filename = resolved.getFileName(); + entry.etag = download.etag(); + entry.lastModified = download.lastModified(); + entry.installedEpoch = System.currentTimeMillis(); + entry.structuresImported = false; + File extractedDir = extractArchive(zip, stagingDir, entry); + InstallExecution execution; + try { + VerifiedStagingInstall verifiedStagingInstall = + authorizeVerifiedStagingInstall( + cacheDir.getParentFile(), stagingDir, extractedDir, entry); + execution = prepareInstallExecution( + extractedDir, worldFolders, entry, stripOverrides, cacheDir.getParentFile(), + verifiedStagingInstall); + } finally { + cleanupExtractedStaging(extractedDir); + } + installs.add(execution); + InstallResult installResult = execution.result(); + recordInstallMetadata(stagedDir, worldFolders, entry); + manifest.put(entry); + + report.updated.add(id + " (" + safe(resolved.getVersionNumber()) + ")"); + if (freshInstallRequiresRestart(installResult.changed(), recoveringManagedStaging)) { + report.requiresRestart = true; + } + message(sender, C.GREEN + " Installed " + C.WHITE + id + C.GREEN + " " + safe(resolved.getVersionNumber())); + } + + static boolean freshInstallRequiresRestart(boolean contentChanged, boolean recoveringManagedStaging) { + return contentChanged || recoveringManagedStaging; + } + + private static File extractArchive(File zip, File stagingRoot, Entry entry) throws IOException { + ensureScratchDirectory(stagingRoot, "datapack staging"); + File pending = new File(stagingRoot, ".pending-" + entry.id + "-" + UUID.randomUUID()); + try { + if (!pending.mkdirs() && !pending.isDirectory()) { + throw new IOException("Couldn't create datapack extraction directory " + pending.getPath()); + } + ZipUtils.unzipFile(zip, pending, MAX_ARCHIVE_ENTRIES, MAX_EXPANDED_BYTES, MAX_ENTRY_BYTES); + flattenIfWrapped(pending); + validatePackMetadata(pending); + PackResources resources = scanPackResources(pending); + entry.structureKeys = resources.structureKeys; + entry.templateKeys = resources.templateKeys; + writeOwnership(pending, entry); + return pending; + } catch (UncheckedIOException e) { + IOException failure = e.getCause(); + cleanupFailedExtraction(pending, failure); + throw failure; + } catch (IOException | RuntimeException e) { + cleanupFailedExtraction(pending, e); + throw e; + } + } + + private static void cleanupFailedExtraction(File pending, Throwable failure) { + try { + deleteInstallScratch(pending, "failed datapack extraction"); + } catch (IOException cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + } + + private static void cleanupExtractedStaging(File extractedDir) { + try { + deleteInstallScratch(extractedDir, "extracted datapack staging"); + } catch (IOException e) { + IrisLogging.warn("Preserving extracted datapack staging for restart cleanup: " + + extractedDir.getPath() + " (" + e.getMessage() + ")"); + } + } + + static VerifiedStagingInstall authorizeVerifiedStagingInstall( + File root, + File stagingRoot, + File verifiedSource, + Entry entry + ) throws IOException { + Path normalizedRoot = requireDirectoryIdentity(root, "datapack storage root"); + Path normalizedStagingRoot = requireDirectoryIdentity(stagingRoot, "datapack staging root"); + requireNoSymbolicLinkComponents(normalizedRoot, "datapack storage root"); + requireNoSymbolicLinkComponents(normalizedStagingRoot, "datapack staging root"); + Path expectedStagingRoot = normalizedRoot.resolve("staging").normalize(); + if (!normalizedStagingRoot.equals(expectedStagingRoot) + || !Files.isSameFile(normalizedStagingRoot, expectedStagingRoot)) { + throw new IOException("Verified datapack staging root is not Iris's canonical staging directory"); + } + Path normalizedSource = requireDirectoryIdentity(verifiedSource, "verified datapack extraction"); + requireNoSymbolicLinkComponents(normalizedSource, "verified datapack extraction"); + if (!Objects.equals(normalizedSource.getParent(), normalizedStagingRoot) + || !Files.isSameFile(normalizedSource.getParent(), normalizedStagingRoot)) { + throw new IOException("Verified datapack extraction is outside Iris's canonical staging directory"); + } + verifyPendingExtractionName(normalizedSource.getFileName().toString(), entry.id); + validateManagedDirectory(verifiedSource, entry.id); + validateScratchTree(normalizedSource); + if (!sameScratchVolume(normalizedSource, normalizedStagingRoot)) { + throw new IOException("Verified datapack extraction crosses a filesystem boundary"); + } + String desiredHash = directoryHash(verifiedSource); + Ownership ownership = readOwnership(verifiedSource); + if (!ownershipMetadataMatches(ownership, entry, desiredHash)) { + throw new IOException("Verified datapack extraction does not match the resolved archive metadata"); + } + Manifest committedManifest = readCommittedManifest(root); + Entry committed = committedManifest.findById(entry.id); + boolean legacyReplacementAuthorized = committed != null + && Objects.equals(committed.id, entry.id) + && Objects.equals(committed.url, entry.url); + LegacyStagingSnapshot legacyStagingSnapshot = captureLegacyStagingSnapshot( + normalizedStagingRoot, entry.id, legacyReplacementAuthorized); + return new VerifiedStagingInstall( + normalizedRoot, + normalizedStagingRoot, + normalizedSource, + entry, + desiredHash, + committed, + legacyReplacementAuthorized, + legacyStagingSnapshot + ); + } + + private static LegacyStagingSnapshot captureLegacyStagingSnapshot( + Path normalizedStagingRoot, + String id, + boolean legacyReplacementAuthorized + ) throws IOException { + if (!legacyReplacementAuthorized) { + return null; + } + Path target = normalizedStagingRoot.resolve(id).normalize(); + if (!pathExists(target, "legacy datapack staging target")) { + return null; + } + if (Files.isSymbolicLink(target) + || !Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid legacy datapack staging target " + target); + } + validateInstallTree(target.toFile(), normalizedStagingRoot.toFile(), "Legacy datapack staging"); + String markerHash = ownershipMarkerFingerprint(target.toFile()); + if (!"absent".equals(markerHash)) { + return null; + } + return new LegacyStagingSnapshot( + target, + target.toRealPath(), + directoryIdentity(target.toFile()), + directoryHash(target.toFile()), + markerHash + ); + } + + private static Path requireDirectoryIdentity(File directory, String purpose) throws IOException { + Path normalized = directory.toPath().toAbsolutePath().normalize(); + BasicFileAttributes attributes = Files.readAttributes( + normalized, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (!isSupportedScratchDirectory(attributes)) { + throw new IOException("Invalid " + purpose + " " + normalized); + } + normalized.toRealPath(); + return normalized; + } + + private static void requireNoSymbolicLinkComponents(Path normalized, String purpose) throws IOException { + Path current = normalized.getRoot(); + for (Path component : normalized) { + current = current == null ? component : current.resolve(component); + BasicFileAttributes attributes = Files.readAttributes( + current, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (attributes.isSymbolicLink()) { + throw new IOException("Refusing symbolic-link component in " + purpose + " " + normalized); + } + if (attributes.isOther()) { + throw new IOException("Refusing special filesystem component in " + purpose + " " + normalized); + } + if (!isSupportedScratchDirectory(attributes)) { + throw new IOException("Refusing unsupported component in " + purpose + " " + normalized); + } + } + } + + private static void verifyPendingExtractionName(String name, String id) throws IOException { + String prefix = ".pending-" + id + "-"; + if (!name.startsWith(prefix)) { + throw new IOException("Verified datapack extraction has an invalid staging name " + name); + } + try { + UUID.fromString(name.substring(prefix.length())); + } catch (IllegalArgumentException e) { + throw new IOException("Verified datapack extraction has an invalid staging identity " + name, e); + } + } + + private static void validateInstallParticipants( + List worldFolders, + VerifiedStagingInstall verifiedStagingInstall, + Entry entry + ) throws IOException { + List roots = new ArrayList<>(worldFolders); + if (verifiedStagingInstall != null) { + roots.add(verifiedStagingInstall.stagingRoot().toFile()); + } + Set normalizedTargets = new HashSet<>(); + Set realRoots = new HashSet<>(); + Set targetIdentities = new HashSet<>(); + List existingTargets = new ArrayList<>(); + for (File folder : roots) { + ensureInstallTargetRoot(folder); + Path normalizedRoot = folder.toPath().toAbsolutePath().normalize(); + Path realRoot = normalizedRoot.toRealPath(); + Path normalizedTarget = normalizedRoot.resolve(entry.id).normalize(); + if (!realRoots.add(realRoot) || !normalizedTargets.add(normalizedTarget)) { + throw new IOException("Duplicate datapack install target " + normalizedTarget); + } + Path targetIdentity = realRoot.resolve(entry.id).normalize(); + if (Files.exists(normalizedTarget, LinkOption.NOFOLLOW_LINKS)) { + if (Files.isSymbolicLink(normalizedTarget) + || !Files.isDirectory(normalizedTarget, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Refusing invalid datapack install target " + normalizedTarget); + } + targetIdentity = normalizedTarget.toRealPath(); + for (Path existingTarget : existingTargets) { + if (Files.isSameFile(normalizedTarget, existingTarget)) { + throw new IOException("Aliased datapack install target " + normalizedTarget); + } + } + existingTargets.add(normalizedTarget); + } + if (!targetIdentities.add(targetIdentity)) { + throw new IOException("Aliased datapack install target " + normalizedTarget); + } + } + if (verifiedStagingInstall != null) { + verifiedStagingInstall.verifyStagingRoot(); + } + } + + private static void ensureInstallTargetRoot(File directory) throws IOException { + if (!Files.exists(directory.toPath(), LinkOption.NOFOLLOW_LINKS)) { + Files.createDirectories(directory.toPath()); + } + requireDirectoryIdentity(directory, "datapack install root"); + } + + static void recordInstallResult( + VolmitSender sender, + Report report, + File stagedDir, + KList worldFolders, + Entry entry, + InstallResult result, + String versionNumber + ) { + recordInstallMetadata(stagedDir, worldFolders, entry); + if (result.changed()) { + report.updated.add(entry.id + " (" + safe(versionNumber) + ")"); + report.requiresRestart = true; + message(sender, C.GREEN + " Repaired " + C.WHITE + entry.id + C.GREEN + " " + safe(versionNumber)); + return; + } + report.upToDate.add(entry.id + " (" + safe(versionNumber) + ")"); + message(sender, C.GRAY + " Up to date: " + C.WHITE + entry.id + C.GRAY + " " + safe(versionNumber)); + } + + private static void collectImports(IrisData data, LinkedHashSet urls) { + if (data == null || data.getDimensionLoader() == null) { + return; + } + for (IrisDimension dimension : data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys())) { + if (dimension == null) { + continue; + } + KList imports = dimension.getDatapackImports(); + if (imports == null) { + continue; + } + for (String url : imports) { + if (url != null && !url.isBlank()) { + urls.add(url.trim()); + } + } + } + } + + private static Set configuredImports(IrisData data) { + LinkedHashSet urls = new LinkedHashSet<>(); + collectImports(data, urls); + return urls; + } + + private static boolean hasImports(IrisData data) { + if (data.getDimensionLoader() == null) { + return false; + } + for (IrisDimension dimension : data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys())) { + if (dimension == null) { + continue; + } + KList imports = dimension.getDatapackImports(); + if (imports != null && !imports.isEmpty()) { + return true; + } + } + return false; + } + + static InstallResult install(File stagedDir, KList worldFolders, Entry entry, boolean stripOverrides) throws IOException { + File root = inferDatapackRoot(stagedDir); + recoverTransactions(root, worldFolders); + InstallExecution execution = prepareInstallExecution( + stagedDir, + worldFolders, + entry, + stripOverrides, + root + ); + try { + verifyInstallExecution(execution); + } catch (IOException failure) { + rollbackInstallExecutions(List.of(execution), failure); + throw failure; + } + finishInstallExecution(execution); + return execution.result(); + } + + static InstallExecution prepareInstallExecution( + File stagedDir, + KList worldFolders, + Entry entry, + boolean stripOverrides, + File root + ) throws IOException { + return prepareInstallExecution( + stagedDir, worldFolders, entry, stripOverrides, root, null); + } + + static InstallExecution prepareInstallExecution( + File stagedDir, + KList worldFolders, + Entry entry, + boolean stripOverrides, + File root, + VerifiedStagingInstall verifiedStagingInstall + ) throws IOException { + validateManagedDirectory(stagedDir, entry.id); + Ownership stagedOwnership = readOwnership(stagedDir); + String stagedHash = directoryHash(stagedDir); + if (!ownershipMetadataMatches(stagedOwnership, entry, stagedHash)) { + throw new IOException("Iris datapack staging does not match the committed manifest entry for " + entry.id); + } + validateInstallParticipants(worldFolders, verifiedStagingInstall, entry); + List plans = new ArrayList<>(); + try { + for (File worldFolder : worldFolders) { + plans.add(prepareInstall( + stagedDir, worldFolder, entry, stagedHash, stripOverrides, verifiedStagingInstall)); + } + if (verifiedStagingInstall != null) { + plans.add(prepareInstall( + stagedDir, + verifiedStagingInstall.stagingRoot().toFile(), + entry, + stagedHash, + false, + verifiedStagingInstall)); + } + } catch (IOException | RuntimeException preparationFailure) { + for (InstallPlan plan : plans) { + try { + cleanupInstallPlan(plan, false); + } catch (IOException cleanupFailure) { + preparationFailure.addSuppressed(cleanupFailure); + } + } + if (preparationFailure instanceof UncheckedIOException unchecked) { + throw unchecked.getCause(); + } + if (preparationFailure instanceof IOException ioFailure) { + throw ioFailure; + } + throw preparationFailure; + } + + boolean changed = false; + List publishPlans = new ArrayList<>(); + List unchangedPlans = new ArrayList<>(); + try { + for (InstallPlan plan : plans) { + changed |= plan.contentChanged(); + if (plan.publishRequired()) { + publishPlans.add(plan); + } else { + unchangedPlans.add(plan); + cleanupInstallPlan(plan, true); + } + } + } catch (IOException cleanupFailure) { + for (InstallPlan plan : plans) { + try { + cleanupInstallPlan(plan, false); + } catch (IOException additionalFailure) { + cleanupFailure.addSuppressed(additionalFailure); + } + } + throw cleanupFailure; + } + if (publishPlans.isEmpty()) { + return new InstallExecution( + new InstallResult(changed), + null, + stagedDir, + entry, + stagedHash, + verifiedStagingInstall == null, + publishPlans, + unchangedPlans); + } + + Manifest committedManifest = readCommittedManifest(root); + boolean manifestAlreadyMatched = manifestEntryMatches(committedManifest.findById(entry.id), entry); + DatapackCoordinator coordinator; + try { + coordinator = createInstallCoordinator(root, entry, publishPlans, manifestAlreadyMatched); + } catch (IOException | RuntimeException coordinatorFailure) { + for (InstallPlan plan : publishPlans) { + try { + cleanupInstallPlan(plan, false); + } catch (IOException cleanupFailure) { + coordinatorFailure.addSuppressed(cleanupFailure); + } + } + if (coordinatorFailure instanceof IOException ioFailure) { + throw ioFailure; + } + throw coordinatorFailure; + } + + try { + coordinator.phase(CoordinatorPhase.PUBLISHING); + for (InstallPlan plan : publishPlans) { + publishInstallPlan(plan); + } + coordinator.phase(CoordinatorPhase.PUBLISHED); + } catch (IOException | RuntimeException publishFailure) { + try { + resolveCoordinatorDirectories(coordinator.journal, false); + coordinator.finish(); + } catch (IOException rollbackFailure) { + publishFailure.addSuppressed(rollbackFailure); + } + if (publishFailure instanceof IOException ioFailure) { + throw ioFailure; + } + throw publishFailure; + } + return new InstallExecution( + new InstallResult(changed), + coordinator, + stagedDir, + entry, + stagedHash, + verifiedStagingInstall == null, + publishPlans, + unchangedPlans); + } + + static void verifyInstallExecution(InstallExecution execution) throws IOException { + if (execution.verified()) { + return; + } + if (execution.verifyStagedSource()) { + validateManagedDirectory(execution.stagedDir(), execution.entry().id); + Ownership stagedOwnership = readOwnership(execution.stagedDir()); + String stagedHash = directoryHash(execution.stagedDir()); + if (!Objects.equals(stagedHash, execution.stagedHash()) + || !ownershipMetadataMatches(stagedOwnership, execution.entry(), stagedHash)) { + throw new IOException("Iris datapack staging changed before installation commit for " + + execution.entry().id); + } + } + for (InstallPlan plan : execution.publishedPlans()) { + verifyDesiredInstallSnapshot(plan.target(), plan, "published datapack target"); + } + for (InstallPlan plan : execution.unchangedPlans()) { + verifyOriginalInstallSnapshot(plan.target(), plan, "unchanged datapack target"); + } + execution.markVerified(); + } + + static void finishInstallExecution(InstallExecution execution) throws IOException { + if (!execution.verified()) { + throw new IOException("Datapack install cannot commit before final verification"); + } + if (execution.coordinator() == null) { + return; + } + execution.coordinator().phase(CoordinatorPhase.COMMITTED); + resolveCoordinatorDirectories(execution.coordinator().journal, true); + execution.coordinator().finish(); + } + + static void rollbackInstallExecutions(List executions, Throwable failure) { + List reversed = new ArrayList<>(executions); + Collections.reverse(reversed); + for (InstallExecution execution : reversed) { + if (execution.coordinator() == null) { + continue; + } + try { + resolveCoordinatorDirectories(execution.coordinator().journal, false); + execution.coordinator().finish(); + } catch (IOException rollbackFailure) { + failure.addSuppressed(rollbackFailure); + } + } + } + + private static File inferDatapackRoot(File stagedDir) { + File parent = stagedDir.getParentFile(); + if (parent != null && "staging".equals(parent.getName()) && parent.getParentFile() != null) { + return parent.getParentFile(); + } + return parent == null ? stagedDir : parent; + } + + private static boolean manifestEntryMatches(Entry committed, Entry desired) { + return committed != null + && Objects.equals(committed.id, desired.id) + && Objects.equals(committed.url, desired.url) + && Objects.equals(committed.versionId, desired.versionId) + && Objects.equals(committed.versionNumber, desired.versionNumber) + && Objects.equals(committed.sha1, desired.sha1); + } + + static String directoryIdentity(File directory) throws IOException { + BasicFileAttributes attributes = Files.readAttributes( + directory.toPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (!attributes.isDirectory() || attributes.isSymbolicLink()) { + throw new IOException("Invalid datapack directory identity at " + directory.getPath()); + } + Object fileKey = attributes.fileKey(); + if (fileKey != null) { + return "key:" + fileKey; + } + // Windows exposes no inode-equivalent for a directory, and returning nothing here fails + // the identity gate closed, which disables legacy staging replacement on that platform + // outright. Creation time is the one stable discriminator the filesystem still offers: a + // directory swapped in at the same path brings its own, so a swap is still caught. It is + // a weaker guarantee than an inode, so it is only ever the fallback. + return "created:" + attributes.creationTime(); + } + + private static boolean pathExists(Path path, String purpose) throws IOException { + if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + return true; + } + if (Files.notExists(path, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + throw new IOException("Cannot determine " + purpose + " state at " + path); + } + + private static void validateInstallTree(File directory, File storeAnchor, String purpose) throws IOException { + validateScratchTree(directory.toPath()); + if (!sameScratchVolume(directory.toPath(), storeAnchor.toPath())) { + throw new IOException(purpose + " crosses a filesystem boundary at " + directory.getPath()); + } + } + + static InstallPlan prepareInstall( + File stagedDir, + File worldFolder, + Entry entry, + String stagedHash, + boolean stripOverrides, + VerifiedStagingInstall verifiedStagingInstall + ) throws IOException { + ensureInstallTargetRoot(worldFolder); + File target = new File(worldFolder, entry.id); + InstallPlan unchanged = tryPrepareUnchangedManagedInstall( + stagedDir, + worldFolder, + target, + entry, + stagedHash, + stripOverrides, + verifiedStagingInstall); + if (unchanged != null) { + return unchanged; + } + boolean canonicalStagingInstall = verifiedStagingInstall != null + && verifiedStagingInstall.isCanonicalInstall(worldFolder, target); + boolean legacyReplacementAuthorized = canonicalStagingInstall + && verifiedStagingInstall.consume(stagedDir, worldFolder, target, entry, stagedHash); + VerifiedStagingInstall legacyWorldAuthorization = null; + File pendingRoot = installScratchRoot(worldFolder); + File pending = new File(pendingRoot, entry.id + "-" + UUID.randomUUID()); + File backup = new File(pendingRoot, entry.id + "-backup-" + UUID.randomUUID()); + try { + ensureScratchDirectory(pendingRoot, "datapack install staging"); + String targetRootIdentity = realDirectoryPath(worldFolder, "datapack target root"); + String scratchRootIdentity = realDirectoryPath(pendingRoot, "datapack install scratch root"); + String targetRootFileIdentity = directoryIdentity(worldFolder); + String scratchRootFileIdentity = directoryIdentity(pendingRoot); + IO.copyDirectory(stagedDir.toPath(), pending.toPath()); + Files.deleteIfExists(new File(pending, OWNERSHIP_MARKER).toPath()); + String copiedHash = directoryHash(pending); + if (!Objects.equals(stagedHash, copiedHash)) { + throw new IOException("Datapack staging changed or copied incompletely while preparing " + entry.id); + } + boolean removedOverrideMarker = Files.deleteIfExists( + new File(pending, OVERRIDES_STRIPPED_MARKER).toPath()); + if (stripOverrides) { + stripVanillaStructureOverrides(pending); + writeMarker(new File(pending, OVERRIDES_STRIPPED_MARKER)); + } + validatePackMetadata(pending); + if (!stripOverrides && !removedOverrideMarker) { + writeOwnership(pending, entry, copiedHash); + } else { + writeOwnership(pending, entry); + } + validateInstallTree(pending, worldFolder, "Prepared datapack install"); + Ownership desiredOwnership = readOwnership(pending); + String desiredHash = desiredOwnership.contentHash; + String desiredMarkerHash = ownershipMarkerFingerprint(pending); + String desiredIdentity = directoryIdentity(pending); + boolean hadTarget = pathExists(target.toPath(), "datapack install target"); + String originalHash = ""; + String originalMarkerHash = "absent"; + String originalIdentity = ""; + boolean contentChanged = !hadTarget; + boolean publishRequired = !hadTarget; + if (hadTarget) { + if (Files.isSymbolicLink(target.toPath()) + || !Files.isDirectory(target.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Refusing to replace non-directory or symbolic-link datapack " + target.getPath()); + } + validateInstallTree(target, worldFolder, "Existing datapack install"); + Ownership ownership = readOwnershipOrNull(target); + if (ownership != null) { + removeFinderMetadata(target); + } + String currentHash = directoryHash(target); + originalHash = currentHash; + originalMarkerHash = ownershipMarkerFingerprint(target); + originalIdentity = directoryIdentity(target); + if (ownership == null) { + boolean differingLegacyTarget = !Objects.equals(currentHash, desiredHash); + if (differingLegacyTarget && !legacyReplacementAuthorized + && verifiedStagingInstall != null + && verifiedStagingInstall.authorizeLegacyWorldReplacement( + stagedDir, + worldFolder, + target, + entry, + stagedHash, + currentHash, + originalMarkerHash)) { + legacyReplacementAuthorized = true; + legacyWorldAuthorization = verifiedStagingInstall; + } + if (differingLegacyTarget && !legacyReplacementAuthorized) { + throw new IOException("Refusing to replace unmanaged datapack at " + target.getPath()); + } + if (differingLegacyTarget && (originalIdentity.isEmpty() + || targetRootFileIdentity.isEmpty() + || scratchRootFileIdentity.isEmpty() + || verifiedStagingInstall == null + || !verifiedStagingInstall.hasStablePathIdentities())) { + throw new IOException("Cannot safely identify legacy datapack staging at " + target.getPath()); + } + publishRequired = true; + } else { + if (!entry.id.equals(ownership.id)) { + throw new IOException("Datapack ownership mismatch at " + target.getPath()); + } + publishRequired = !Objects.equals(ownership.contentHash, currentHash) + || !ownershipMetadataMatches(ownership, entry, desiredHash); + } + contentChanged = !Objects.equals(currentHash, desiredHash); + publishRequired |= contentChanged; + if (ownership != null && !Objects.equals(ownership.contentHash, currentHash)) { + IrisLogging.warn("Repairing modified or corrupt Iris-managed datapack at " + target.getPath()); + } + } + return new InstallPlan( + target, + pending, + backup, + pendingRoot, + hadTarget, + publishRequired, + contentChanged, + originalHash, + desiredHash, + originalMarkerHash, + desiredMarkerHash, + originalIdentity, + desiredIdentity, + targetRootIdentity, + scratchRootIdentity, + targetRootFileIdentity, + scratchRootFileIdentity, + entry.id, + entry.url, + legacyWorldAuthorization + ); + } catch (UncheckedIOException e) { + IOException cause = e.getCause(); + cleanupPreparedInstall(pending, pendingRoot, cause); + throw cause; + } catch (IOException e) { + cleanupPreparedInstall(pending, pendingRoot, e); + throw e; + } catch (RuntimeException e) { + cleanupPreparedInstall(pending, pendingRoot, e); + throw e; + } + } + + private static InstallPlan tryPrepareUnchangedManagedInstall( + File stagedDir, + File worldFolder, + File target, + Entry entry, + String stagedHash, + boolean stripOverrides, + VerifiedStagingInstall verifiedStagingInstall + ) throws IOException { + if (verifiedStagingInstall != null + || stripOverrides + || pathExists(new File(stagedDir, OVERRIDES_STRIPPED_MARKER).toPath(), + "staged datapack override marker") + || !pathExists(target.toPath(), "datapack install target")) { + return null; + } + if (Files.isSymbolicLink(target.toPath()) + || !Files.isDirectory(target.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Refusing to replace non-directory or symbolic-link datapack " + target.getPath()); + } + Ownership ownership = readOwnershipOrNull(target); + if (ownership == null) { + return null; + } + if (!entry.id.equals(ownership.id)) { + throw new IOException("Datapack ownership mismatch at " + target.getPath()); + } + if (!ownershipMetadataMatches(ownership, entry, stagedHash)) { + return null; + } + validateInstallTree(target, worldFolder, "Existing datapack install"); + removeFinderMetadata(target); + String currentHash = directoryHash(target); + if (!Objects.equals(currentHash, stagedHash)) { + return null; + } + String markerHash = ownershipMarkerFingerprint(target); + String identity = directoryIdentity(target); + File pendingRoot = installScratchRoot(worldFolder); + return new InstallPlan( + target, + new File(pendingRoot, entry.id + "-" + UUID.randomUUID()), + new File(pendingRoot, entry.id + "-backup-" + UUID.randomUUID()), + pendingRoot, + true, + false, + false, + currentHash, + currentHash, + markerHash, + markerHash, + identity, + identity, + realDirectoryPath(worldFolder, "datapack target root"), + "", + directoryIdentity(worldFolder), + "", + entry.id, + entry.url, + null + ); + } + + private static File installScratchRoot(File targetFolder) { + File parent = targetFolder.getParentFile(); + return new File(parent == null ? targetFolder : parent, ".iris-datapack-install"); + } + + private static void cleanupPreparedInstall(File pending, File pendingRoot, Throwable failure) { + try { + deleteInstallScratch(pending, "prepared datapack install"); + } catch (IOException cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + pendingRoot.delete(); + } + + private static boolean ownershipMetadataMatches(Ownership ownership, Entry entry, String contentHash) { + return ownership.schemaVersion == OWNERSHIP_SCHEMA + && Objects.equals(ownership.id, entry.id) + && Objects.equals(ownership.url, entry.url) + && Objects.equals(ownership.versionId, entry.versionId) + && Objects.equals(ownership.versionNumber, entry.versionNumber) + && Objects.equals(ownership.sha1, entry.sha1) + && Objects.equals(ownership.contentHash, contentHash) + && copyList(ownership.structureKeys).equals(copyList(entry.structureKeys)) + && copyList(ownership.templateKeys).equals(copyList(entry.templateKeys)); + } + + private static boolean ownershipSourceMatches(Ownership ownership, Entry entry) { + return ownership.schemaVersion == OWNERSHIP_SCHEMA + && Objects.equals(ownership.id, entry.id) + && Objects.equals(ownership.url, entry.url); + } + + static void publishInstallPlan(InstallPlan plan) throws IOException { + verifyDirectoryContainerIdentity( + plan.target().getParentFile(), plan.targetRootIdentity(), + plan.targetRootFileIdentity(), "datapack target root"); + verifyDirectoryContainerIdentity( + plan.pendingRoot(), plan.scratchRootIdentity(), + plan.scratchRootFileIdentity(), "datapack install scratch root"); + verifyDesiredInstallSnapshot(plan.pending(), plan, "prepared datapack install"); + if (plan.hadTarget()) { + verifyOriginalInstallSnapshot(plan.target(), plan, "original datapack target"); + } else if (pathExists(plan.target().toPath(), "new datapack target")) { + throw new IOException("Datapack install target was concurrently created at " + plan.target().getPath()); + } + if (plan.legacyWorldAuthorization() != null) { + plan.legacyWorldAuthorization().verifyLegacyWorldSnapshot(); + } + try { + if (plan.hadTarget()) { + moveNew(plan.target().toPath(), plan.backup().toPath()); + verifyOriginalInstallSnapshot(plan.backup(), plan, "datapack install backup"); + if (pathExists(plan.target().toPath(), "moved datapack target")) { + throw new IOException("Datapack install target reappeared after backup at " + plan.target().getPath()); + } + forceInstallMoveDirectories(plan); + } + moveNew(plan.pending().toPath(), plan.target().toPath()); + verifyDesiredInstallSnapshot(plan.target(), plan, "installed datapack target"); + forceInstallMoveDirectories(plan); + } catch (IOException publishFailure) { + if (plan.hadTarget() + && Files.exists(plan.backup().toPath(), LinkOption.NOFOLLOW_LINKS) + && Files.exists(plan.pending().toPath(), LinkOption.NOFOLLOW_LINKS) + && Files.notExists(plan.target().toPath(), LinkOption.NOFOLLOW_LINKS)) { + try { + verifyOriginalInstallSnapshot(plan.backup(), plan, "datapack install backup"); + moveNew(plan.backup().toPath(), plan.target().toPath()); + forceInstallMoveDirectories(plan); + } catch (IOException restoreFailure) { + publishFailure.addSuppressed(restoreFailure); + } + } + throw publishFailure; + } + } + + private static void forceInstallMoveDirectories(InstallPlan plan) throws IOException { + forceDirectoryIfSupported(plan.target().getParentFile().toPath()); + forceDirectoryIfSupported(plan.pendingRoot().toPath()); + } + + private static void verifyDirectoryContainerIdentity( + File directory, + String expectedRealPath, + String expectedFileIdentity, + String purpose + ) throws IOException { + Path normalized = directory.toPath().toAbsolutePath().normalize(); + if (Files.isSymbolicLink(normalized) + || !Files.isDirectory(normalized, LinkOption.NOFOLLOW_LINKS) + || !Objects.equals(normalized.toRealPath(), Path.of(expectedRealPath)) + || (!expectedFileIdentity.isEmpty() + && !Objects.equals(directoryIdentity(directory), expectedFileIdentity))) { + throw new IOException("Changed or unsafe " + purpose + " " + normalized); + } + } + + private static void verifyOriginalInstallSnapshot( + File directory, + InstallPlan plan, + String purpose + ) throws IOException { + verifyDirectorySnapshot( + directory, + plan.target().getParentFile(), + plan.originalHash(), + plan.originalMarkerHash(), + plan.originalIdentity(), + purpose + ); + } + + private static void verifyDesiredInstallSnapshot( + File directory, + InstallPlan plan, + String purpose + ) throws IOException { + verifyDirectorySnapshot( + directory, + plan.target().getParentFile(), + plan.desiredHash(), + plan.desiredMarkerHash(), + plan.desiredIdentity(), + purpose + ); + Ownership ownership = readOwnership(directory); + if (!Objects.equals(ownership.id, plan.id()) + || !Objects.equals(ownership.url, plan.url()) + || !Objects.equals(ownership.contentHash, plan.desiredHash())) { + throw new IOException("Datapack ownership changed in " + purpose + " at " + directory.getPath()); + } + } + + private static void verifyDirectorySnapshot( + File directory, + File storeAnchor, + String expectedHash, + String expectedMarkerHash, + String expectedIdentity, + String purpose + ) throws IOException { + if (!pathExists(directory.toPath(), purpose) + || Files.isSymbolicLink(directory.toPath()) + || !Files.isDirectory(directory.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Missing or unsafe " + purpose + " at " + directory.getPath()); + } + validateInstallTree(directory, storeAnchor, purpose); + if (Files.exists(new File(directory, OWNERSHIP_MARKER).toPath(), LinkOption.NOFOLLOW_LINKS)) { + removeFinderMetadata(directory); + } + if (!expectedIdentity.isEmpty() + && !Objects.equals(directoryIdentity(directory), expectedIdentity)) { + throw new IOException("Datapack directory identity changed in " + purpose + " at " + directory.getPath()); + } + if (!Objects.equals(expectedHash, directoryHash(directory)) + || !Objects.equals(expectedMarkerHash, ownershipMarkerFingerprint(directory))) { + throw new IOException("Datapack content changed in " + purpose + " at " + directory.getPath()); + } + } + + private static void cleanupInstallPlan(InstallPlan plan, boolean committed) throws IOException { + deleteInstallScratch(plan.pending, "datapack install pending directory"); + if (committed) { + deleteInstallScratch(plan.backup, "datapack install backup"); + } + if (Files.exists(plan.backup.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Preserving prior datapack backup after incomplete install: " + + plan.backup.getPath()); + } + plan.pendingRoot.delete(); + } + + static void deleteInstallScratch(File scratch, String purpose) throws IOException { + Path scratchPath = scratch.toPath(); + File parent = Objects.requireNonNull(scratch.getParentFile(), "datapack scratch parent"); + for (int attempt = 0; attempt < MAX_SCRATCH_DELETE_ATTEMPTS; attempt++) { + if (Files.notExists(scratchPath, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (!Files.exists(scratchPath, LinkOption.NOFOLLOW_LINKS) + || Files.isSymbolicLink(scratchPath) + || !Files.isDirectory(scratchPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Refusing to remove unsafe " + purpose + " " + scratch.getPath()); + } + validateInstallTree(scratch, parent, purpose); + removeFinderMetadata(scratch); + IO.delete(scratch); + if (Files.notExists(scratchPath, LinkOption.NOFOLLOW_LINKS)) { + return; + } + } + throw new IOException("Could not remove " + purpose + " " + scratch.getPath()); + } + + private static boolean resolveStripOverrides() { + try (Stream stream = ServerConfigurator.allPacks()) { + return stream.anyMatch(DatapackIngestService::packDisablesOverrides); + } + } + + private static boolean packDisablesOverrides(IrisData data) { + if (data == null || data.getDimensionLoader() == null) { + return false; + } + for (IrisDimension dimension : data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys())) { + if (dimension == null) { + continue; + } + IrisImportedStructureControl control = dimension.getImportedStructures(); + if (control != null && !control.isDatapackOverrides()) { + return true; + } + } + return false; + } + + private static void stripVanillaStructureOverrides(File datapackRoot) throws IOException { + stripVanillaStructureOverrides(datapackRoot, IO::delete); + } + + static void stripVanillaStructureOverrides( + File datapackRoot, + DirectoryDeleter deleter + ) throws IOException { + File minecraftData = new File(new File(datapackRoot, "data"), "minecraft"); + if (!minecraftData.isDirectory()) { + return; + } + String[] relativeTrees = { + "worldgen" + File.separator + "structure_set", + "worldgen" + File.separator + "structure", + "worldgen" + File.separator + "template_pool", + "structure" + }; + for (String tree : relativeTrees) { + File dir = new File(minecraftData, tree); + if (dir.exists()) { + deleter.delete(dir); + if (Files.exists(dir.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Could not remove vanilla structure override tree " + dir.getPath()); + } + } + } + } + + private static void writeMarker(File marker) throws IOException { + Files.writeString(marker.toPath(), "stripped", StandardCharsets.UTF_8); + } + + static void validatePackMetadata(File datapackRoot) throws IOException { + File metadata = new File(datapackRoot, "pack.mcmeta"); + if (!metadata.isFile() || Files.isSymbolicLink(metadata.toPath())) { + throw new IOException("Datapack is missing a regular pack.mcmeta"); + } + if (metadata.length() > MAX_METADATA_BYTES) { + throw new IOException("Datapack pack.mcmeta exceeds 1 MiB"); + } + JsonElement parsed; + try { + parsed = JsonParser.parseString(readBoundedUtf8( + metadata.toPath(), MAX_METADATA_BYTES, "Datapack pack.mcmeta")); + } catch (RuntimeException e) { + throw new IOException("Datapack pack.mcmeta is not valid JSON", e); + } + if (!parsed.isJsonObject()) { + throw new IOException("Datapack pack.mcmeta root must be an object"); + } + JsonObject root = parsed.getAsJsonObject(); + if (!root.has("pack") || !root.get("pack").isJsonObject()) { + throw new IOException("Datapack pack.mcmeta must contain a pack object"); + } + JsonObject pack = root.getAsJsonObject("pack"); + boolean hasPackFormat = pack.has("pack_format") && pack.get("pack_format").isJsonPrimitive() + && pack.getAsJsonPrimitive("pack_format").isNumber(); + boolean hasRange = validFormatValue(pack.get("min_format")) && validFormatValue(pack.get("max_format")); + if (!hasPackFormat && !hasRange) { + throw new IOException("Datapack pack.mcmeta must declare numeric pack_format or valid min_format/max_format"); + } + if (!pack.has("description") || pack.get("description").isJsonNull()) { + throw new IOException("Datapack pack.mcmeta must declare a description"); + } + File data = new File(datapackRoot, "data"); + if (data.exists() && (!data.isDirectory() || Files.isSymbolicLink(data.toPath()))) { + throw new IOException("Datapack data path is not a regular directory"); + } + } + + private static boolean validFormatValue(JsonElement value) { + if (value == null || value.isJsonNull()) { + return false; + } + if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isNumber()) { + return true; + } + if (!value.isJsonArray() || value.getAsJsonArray().size() != 2) { + return false; + } + for (JsonElement component : value.getAsJsonArray()) { + if (!component.isJsonPrimitive() || !component.getAsJsonPrimitive().isNumber()) { + return false; + } + } + return true; + } + + static void writeOwnership(File directory, Entry entry) throws IOException { + writeOwnership(directory, entry, directoryHash(directory)); + } + + private static void writeOwnership(File directory, Entry entry, String contentHash) throws IOException { + if (!isValidManagedId(entry.id) || entry.url == null || entry.url.isBlank()) { + throw new IOException("Invalid Iris datapack ownership identity for " + directory.getPath()); + } + if (contentHash == null || contentHash.isBlank()) { + throw new IOException("Missing Iris datapack ownership content hash for " + directory.getPath()); + } + Ownership ownership = new Ownership( + OWNERSHIP_SCHEMA, + entry.id, + entry.url, + entry.versionId, + entry.versionNumber, + entry.sha1, + contentHash, + copyList(entry.structureKeys), + copyList(entry.templateKeys) + ); + Path marker = new File(directory, OWNERSHIP_MARKER).toPath(); + Path temporary = Files.createTempFile(directory.toPath(), OWNERSHIP_MARKER, ".tmp"); + try { + Files.writeString( + temporary, + GSON.toJson(ownership), + StandardCharsets.UTF_8, + StandardOpenOption.TRUNCATE_EXISTING + ); + move(temporary, marker); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static Ownership readOwnership(File directory) throws IOException { + Ownership ownership = readOwnershipOrNull(directory); + if (ownership == null) { + throw new IOException("Missing Iris datapack ownership marker in " + directory.getPath()); + } + return ownership; + } + + private static Ownership readOwnershipOrNull(File directory) throws IOException { + File marker = new File(directory, OWNERSHIP_MARKER); + Path markerPath = marker.toPath(); + if (Files.notExists(markerPath, LinkOption.NOFOLLOW_LINKS)) { + return null; + } + if (!Files.exists(markerPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Cannot determine Iris datapack ownership marker state in " + directory.getPath()); + } + if (Files.isSymbolicLink(markerPath) + || !Files.isRegularFile(markerPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid Iris datapack ownership marker in " + directory.getPath()); + } + if (marker.length() > MAX_OWNERSHIP_BYTES) { + throw new IOException("Oversized Iris datapack ownership marker in " + directory.getPath()); + } + try { + Ownership ownership = GSON.fromJson(readBoundedUtf8( + marker.toPath(), MAX_OWNERSHIP_BYTES, "Iris datapack ownership marker"), Ownership.class); + if (ownership == null || ownership.schemaVersion != OWNERSHIP_SCHEMA + || !isValidManagedId(ownership.id) || ownership.url == null || ownership.url.isBlank() + || ownership.contentHash == null || ownership.contentHash.isBlank()) { + throw new IOException("Invalid Iris datapack ownership marker in " + directory.getPath()); + } + return ownership; + } catch (RuntimeException e) { + throw new IOException("Invalid Iris datapack ownership marker in " + directory.getPath(), e); + } + } + + static String ownershipMarkerFingerprint(File directory) throws IOException { + Path marker = new File(directory, OWNERSHIP_MARKER).toPath(); + if (Files.notExists(marker, LinkOption.NOFOLLOW_LINKS)) { + return "absent"; + } + if (!Files.exists(marker, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Cannot determine Iris datapack ownership marker state in " + directory.getPath()); + } + if (Files.isSymbolicLink(marker) + || !Files.isRegularFile(marker, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid Iris datapack ownership marker in " + directory.getPath()); + } + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return "sha256:" + hex(digest.digest(readBoundedBytes( + marker, MAX_OWNERSHIP_BYTES, "Iris datapack ownership marker"))); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 algorithm unavailable", e); + } + } + + static boolean isUsableStaging(File stagedDir, Entry entry) { + return inspectUsableStaging(stagedDir, entry).usable(); + } + + private static StagingInspection inspectUsableStaging(File stagedDir, Entry entry) { + try { + validateManagedDirectory(stagedDir, entry.id); + Ownership ownership = readOwnership(stagedDir); + String contentHash = directoryHash(stagedDir); + if (!ownershipSourceMatches(ownership, entry) + || !Objects.equals(ownership.versionId, entry.versionId) + || !Objects.equals(ownership.versionNumber, entry.versionNumber) + || !Objects.equals(ownership.sha1, entry.sha1) + || !Objects.equals(ownership.contentHash, contentHash)) { + return new StagingInspection(false, false, false); + } + PackResources resources = scanPackResources(stagedDir); + List previousStructureKeys = copyList(entry.structureKeys); + List previousTemplateKeys = copyList(entry.templateKeys); + boolean ownershipCorrected = false; + if (!copyList(resources.structureKeys).equals(copyList(ownership.structureKeys)) + || !copyList(resources.templateKeys).equals(copyList(ownership.templateKeys))) { + Entry corrected = copyEntry(entry); + corrected.structureKeys = resources.structureKeys; + corrected.templateKeys = resources.templateKeys; + writeOwnership(stagedDir, corrected); + ownershipCorrected = true; + } + entry.structureKeys = resources.structureKeys; + entry.templateKeys = resources.templateKeys; + boolean manifestChanged = !previousStructureKeys.equals(copyList(entry.structureKeys)) + || !previousTemplateKeys.equals(copyList(entry.templateKeys)); + return new StagingInspection(true, ownershipCorrected, manifestChanged); + } catch (IOException e) { + IrisLogging.warn("Ignoring unusable Iris datapack staging at " + stagedDir.getPath() + ": " + e.getMessage()); + return new StagingInspection(false, false, false); + } + } + + private static void validateManagedDirectory(File directory, String id) throws IOException { + Path path = directory.toPath(); + if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS) || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Missing datapack directory " + directory.getPath()); + } + if (Files.isSymbolicLink(path)) { + throw new IOException("Refusing symbolic-link datapack directory " + directory.getPath()); + } + validatePackMetadata(directory); + rejectSymbolicLinks(directory); + Ownership ownership = readOwnershipOrNull(directory); + if (ownership == null) { + throw new IOException("Datapack is not Iris-managed: " + directory.getPath()); + } + if (!id.equals(ownership.id)) { + throw new IOException("Datapack ownership mismatch at " + directory.getPath()); + } + removeFinderMetadata(directory); + } + + private static void rejectSymbolicLinks(File root) throws IOException { + try (Stream paths = Files.walk(root.toPath())) { + Path symbolicLink = paths.filter(Files::isSymbolicLink).findFirst().orElse(null); + if (symbolicLink != null) { + throw new IOException("Datapack contains a symbolic link: " + symbolicLink); + } + } + } + + private static boolean sameDatapackVolume( + Path root, + FileStore rootStore, + Path entry + ) throws IOException { + return sameScratchVolume(root, rootStore, entry, Files.getFileStore(entry)); + } + + private static String directoryHash(File root) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + Path rootPath = root.toPath().toAbsolutePath().normalize(); + Path rootMarker = rootPath.resolve(OWNERSHIP_MARKER); + FileStore rootStore = Files.getFileStore(rootPath); + List entries = new ArrayList<>(); + try (Stream paths = Files.walk(rootPath)) { + Iterator iterator = paths.iterator(); + int pathCount = 0; + while (iterator.hasNext()) { + Path path = iterator.next(); + if (path.equals(rootPath) || path.equals(rootMarker)) { + continue; + } + if (isFinderMetadata(path)) { + if (Files.isSymbolicLink(path) + || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Suspicious Finder metadata in datapack: " + path); + } + continue; + } + pathCount++; + if (pathCount > MAX_MANAGED_PATHS) { + throw new IOException("Datapack contains more than " + MAX_MANAGED_PATHS + " paths"); + } + if (Files.isSymbolicLink(path)) { + throw new IOException("Datapack contains a symbolic link: " + path); + } + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) + && !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Datapack contains an unsupported filesystem entry: " + path); + } + entries.add(path); + } + } + entries.sort(Comparator.comparing(path -> rootPath.relativize(path).toString())); + byte[] buffer = new byte[HASH_BUFFER_BYTES]; + long totalBytes = 0; + for (Path entry : entries) { + String relative = rootPath.relativize(entry).toString().replace(File.separatorChar, '/'); + byte[] relativeBytes = relative.getBytes(StandardCharsets.UTF_8); + BasicFileAttributes attributes = Files.readAttributes( + entry, + BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS + ); + if (attributes.isSymbolicLink() + || attributes.isOther() + || !attributes.isDirectory() && !attributes.isRegularFile()) { + throw new IOException("Datapack entry changed while hashing: " + relative); + } + if (!sameScratchVolume(rootPath, rootStore, entry, Files.getFileStore(entry))) { + throw new IOException("Datapack entry crosses a filesystem boundary: " + entry); + } + boolean directory = attributes.isDirectory(); + digest.update((byte) (directory ? 1 : 2)); + updateDigestInt(digest, relativeBytes.length); + digest.update(relativeBytes); + if (!directory) { + long expectedBytes = attributes.size(); + if (expectedBytes > MAX_ENTRY_BYTES) { + throw new IOException("Datapack file exceeds " + MAX_ENTRY_BYTES + " bytes: " + relative); + } + totalBytes += expectedBytes; + if (totalBytes > MAX_EXPANDED_BYTES) { + throw new IOException("Datapack contents exceed " + MAX_EXPANDED_BYTES + " bytes"); + } + updateDigestLong(digest, expectedBytes); + long entryBytes = 0; + try (InputStream input = Files.newInputStream( + entry, + StandardOpenOption.READ, + LinkOption.NOFOLLOW_LINKS + )) { + int length; + while ((length = input.read(buffer)) > 0) { + entryBytes += length; + if (entryBytes > expectedBytes) { + throw new IOException("Datapack file changed while hashing: " + relative); + } + digest.update(buffer, 0, length); + } + } + if (entryBytes != expectedBytes) { + throw new IOException("Datapack file changed while hashing: " + relative); + } + } + } + return hex(digest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 algorithm unavailable", e); + } + } + + private static void removeFinderMetadata(File root) throws IOException { + List entries; + try (Stream paths = Files.walk(root.toPath())) { + entries = paths.limit(MAX_MANAGED_PATHS + 1L).toList(); + } + if (entries.size() > MAX_MANAGED_PATHS) { + throw new IOException("Datapack contains more than " + MAX_MANAGED_PATHS + " paths"); + } + for (Path path : entries) { + if (!isFinderMetadata(path)) { + continue; + } + if (Files.isSymbolicLink(path) + || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Suspicious Finder metadata in datapack: " + path); + } + Files.delete(path); + } + } + + private static boolean isFinderMetadata(Path path) { + Path fileName = path.getFileName(); + return fileName != null && FINDER_METADATA.equals(fileName.toString()); + } + + private static PackResources scanPackResources(File root) throws IOException { + TreeSet structureKeys = new TreeSet<>(); + TreeSet structureSetKeys = new TreeSet<>(); + TreeSet templateKeys = new TreeSet<>(); + Path dataRoot = new File(root, "data").toPath(); + if (!Files.isDirectory(dataRoot, LinkOption.NOFOLLOW_LINKS)) { + return new PackResources(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); + } + try (Stream paths = Files.walk(dataRoot)) { + for (Path path : paths.filter(file -> Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)).toList()) { + Path relative = dataRoot.relativize(path); + if (relative.getNameCount() < 3) { + continue; + } + String namespace = relative.getName(0).toString().toLowerCase(Locale.ROOT); + String normalized = relative.subpath(1, relative.getNameCount()).toString().replace(File.separatorChar, '/'); + addResourceKey(structureKeys, namespace, normalized, "worldgen/structure/", ".json"); + addResourceKey(structureKeys, namespace, normalized, "worldgen/structures/", ".json"); + addResourceKey(structureSetKeys, namespace, normalized, "worldgen/structure_set/", ".json"); + addResourceKey(structureSetKeys, namespace, normalized, "worldgen/structure_sets/", ".json"); + addResourceKey(templateKeys, namespace, normalized, "structure/", ".nbt"); + addResourceKey(templateKeys, namespace, normalized, "structures/", ".nbt"); + } + } + return new PackResources( + new ArrayList<>(structureKeys), + new ArrayList<>(structureSetKeys), + new ArrayList<>(templateKeys)); + } + + private static void addResourceKey(Set keys, String namespace, String path, String prefix, String suffix) { + if (!path.startsWith(prefix) || !path.endsWith(suffix)) { + return; + } + String resourcePath = path.substring(prefix.length(), path.length() - suffix.length()); + if (!resourcePath.isBlank()) { + keys.add(namespace + ":" + resourcePath); + } + } + + static boolean deleteOwnedDirectory(File directory, String id) throws IOException { + Path path = directory.toPath(); + if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + if (Files.isSymbolicLink(path) || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Refusing non-directory or symbolic-link target " + directory.getPath()); + } + Ownership ownership = readOwnership(directory); + if (!id.equals(ownership.id)) { + throw new IOException("Ownership marker belongs to '" + ownership.id + "'"); + } + removeFinderMetadata(directory); + if (!Objects.equals(ownership.contentHash, directoryHash(directory))) { + throw new IOException("Refusing to delete modified or corrupt Iris-managed datapack " + directory.getPath()); + } + File parent = Objects.requireNonNull(directory.getParentFile(), "managed datapack parent"); + validateInstallTree(directory, parent, "Managed datapack deletion"); + IO.delete(directory); + if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Could not delete " + directory.getPath()); + } + return true; + } + + private static boolean autoImportDatapackStructures() { + TRANSACTION_LOCK.lock(); + try { + return autoImportDatapackStructuresLocked(); + } finally { + TRANSACTION_LOCK.unlock(); + } + } + + private static boolean autoImportDatapackStructuresLocked() { + boolean autoImportEnabled = IrisSettings.get().getGeneral().autoImportDatapackStructures; + File root = IrisPlatforms.get().dataFolder("datapacks"); + boolean recovered; + try { + recovered = recoverTransactions(root, ServerConfigurator.getDatapacksFolder()); + } catch (IOException e) { + IrisLogging.reportError("Automatic datapack structure import blocked by incomplete transaction recovery.", e); + return false; + } + Manifest manifest = readManifest(root); + if (manifest.entries.isEmpty()) { + return recovered; + } + Map manifestEntriesByUrl = new HashMap<>(); + for (Entry entry : manifest.entries) { + if (entry.url != null) { + manifestEntriesByUrl.put(entry.url, entry); + } + } + + List packs; + try (Stream stream = ServerConfigurator.allPacks()) { + packs = stream.filter(Objects::nonNull).toList(); + } + if (!autoImportEnabled && !hasRemovedImportState(packs, manifest.entries)) { + return recovered; + } + + Map entriesByUrl = new HashMap<>(); + File stagingRoot = new File(root, "staging"); + boolean stagingStateChanged = false; + boolean manifestStagingMetadataChanged = false; + for (Entry entry : manifest.entries) { + if (entry.url == null) { + continue; + } + StagingInspection inspection = inspectUsableStaging(new File(stagingRoot, entry.id), entry); + stagingStateChanged |= inspection.ownershipCorrected(); + manifestStagingMetadataChanged |= inspection.manifestChanged(); + if (inspection.usable()) { + entriesByUrl.put(entry.url, entry); + } + } + + int attemptedPacks = 0; + int completedPacks = 0; + int cleanupTargets = 0; + Set completedUrls = new HashSet<>(); + Set failedUrls = new HashSet<>(); + for (IrisData data : packs) { + Set configured = configuredImports(data); + String targetId = data.getDataFolder().toPath().toAbsolutePath().normalize().toString(); + for (Entry entry : manifest.entries) { + if (!configured.contains(entry.url) && hasImportState(entry, targetId)) { + cleanupTargets++; + } + } + if (!cleanupRemovedImports(data, targetId, configured, manifest.entries, manifestEntriesByUrl)) { + for (Entry entry : manifest.entries) { + if (!configured.contains(entry.url) && hasImportState(entry, targetId)) { + failedUrls.add(entry.url); + } + } + } + if (!autoImportEnabled) { + continue; + } + + Set pendingUrls = new HashSet<>(); + for (String url : configured) { + Entry entry = entriesByUrl.get(url); + if (entry != null && importPending(entry, targetId)) { + pendingUrls.add(url); + } + } + if (pendingUrls.isEmpty()) { + continue; + } + for (String pendingUrl : pendingUrls) { + Entry entry = entriesByUrl.get(pendingUrl); + prepareImportRecoveryInventory(entry, targetId); + } + try { + writeManifestChecked(root, manifest); + } catch (IOException e) { + failedUrls.addAll(pendingUrls); + IrisLogging.reportError("Datapack structure import for pack '" + + data.getDataFolder().getPath() + + "' was blocked because its recovery inventory could not be persisted.", e); + continue; + } + Set structureKeys = new TreeSet<>(); + Set templateKeys = new TreeSet<>(); + for (Entry entry : manifest.entries) { + if (!configured.contains(entry.url)) { + continue; + } + structureKeys.addAll(copyList(entry.structureKeys)); + templateKeys.addAll(copyList(entry.templateKeys)); + } + attemptedPacks++; + try { + BulkStructureImporter.Report report = BulkStructureImporter.importManagedDatapackStructures( + data, + StructureImporter.Mode.OVERWRITE, + BukkitPlatform.console(), + structureKeys, + templateKeys + ); + Set successfulPendingUrls = new HashSet<>(pendingUrls); + Set incompleteUrls = new HashSet<>(); + for (String pendingUrl : pendingUrls) { + Entry entry = entriesByUrl.get(pendingUrl); + Map desired = importBundleInventory(entry); + if (!report.successfulBundles().entrySet().containsAll(desired.entrySet())) { + successfulPendingUrls.remove(pendingUrl); + incompleteUrls.add(pendingUrl); + failedUrls.add(pendingUrl); + } + } + if (report.failed() > 0 && incompleteUrls.isEmpty()) { + successfulPendingUrls.clear(); + incompleteUrls.addAll(pendingUrls); + failedUrls.addAll(pendingUrls); + } + if (!incompleteUrls.isEmpty()) { + boolean reconciled = reconcileFailedImportInventories( + root, manifest, data, targetId, incompleteUrls, entriesByUrl); + if (report.retryRequired() || !reconciled) { + IrisLogging.error("Datapack structure import for pack '%s' reported %d incomplete source(s) and remains pending because a retryable runtime failure occurred.", + data.getDataFolder().getPath(), incompleteUrls.size()); + } else if (recordDeterministicImportAttempts( + root, manifest, targetId, incompleteUrls, entriesByUrl)) { + IrisLogging.warn("Datapack structure import for pack '" + + data.getDataFolder().getPath() + "' left " + incompleteUrls.size() + + " source(s) incomplete after deterministic validation failures. Iris will retain the partial editable imports without retrying until the datapack source, importer format, or target pack changes."); + } + } + Map sharedBundles = desiredBundles(configured, entriesByUrl); + boolean packCompleted = false; + for (String pendingUrl : successfulPendingUrls) { + Entry entry = entriesByUrl.get(pendingUrl); + Map desired = importBundleInventory(entry); + Map previous = entry.importedBundles.getOrDefault(targetId, Map.of()); + Map stale = new TreeMap<>(previous); + stale.keySet().removeAll(desired.keySet()); + stale.keySet().removeAll(sharedBundles.keySet()); + Map remaining = cleanupImportedBundles(data, stale); + if (!remaining.isEmpty()) { + failedUrls.add(pendingUrl); + continue; + } + entry.importedBundles.put(targetId, desired); + recordSuccessfulImport(entry, targetId); + completedUrls.add(pendingUrl); + packCompleted = true; + } + if (packCompleted) { + completedPacks++; + } + } catch (RuntimeException e) { + failedUrls.addAll(pendingUrls); + IrisLogging.reportError("Datapack structure import failed for pack '" + + data.getDataFolder().getPath() + "'; the manifest remains pending for retry.", e); + reconcileFailedImportInventories(root, manifest, data, targetId, pendingUrls, entriesByUrl); + } + } + + for (Entry entry : manifest.entries) { + if (failedUrls.contains(entry.url)) { + entry.structuresImported = false; + } else if (completedUrls.contains(entry.url)) { + entry.structuresImported = true; + } + } + if (attemptedPacks == 0 && cleanupTargets == 0) { + if (manifestStagingMetadataChanged) { + writeManifest(root, manifest); + } + return recovered || stagingStateChanged || manifestStagingMetadataChanged; + } + writeManifest(root, manifest); + if (attemptedPacks == 0) { + IrisLogging.info("Datapack editable-import cleanup reconciled " + cleanupTargets + " removed source target(s)."); + return true; + } + IrisLogging.info("Datapack structure import refreshed " + completedUrls.size() + " source(s) across " + + completedPacks + "/" + attemptedPacks + + " pack(s). Reference the imported keys from a 'structures' placement to position them manually."); + return true; + } + + private static boolean hasRemovedImportState(List packs, List entries) { + for (IrisData data : packs) { + Set configured = configuredImports(data); + String targetId = data.getDataFolder().toPath().toAbsolutePath().normalize().toString(); + for (Entry entry : entries) { + if (!configured.contains(entry.url) && hasImportState(entry, targetId)) { + return true; + } + } + } + return false; + } + + static String importRevision(Entry entry) { + return importRevision(entry, STRUCTURE_IMPORT_FORMAT_REVISION); + } + + static String importRevision(Entry entry, int importerFormatRevision) { + return "v" + importerFormatRevision + ":" + safe(entry.versionId) + ":" + safe(entry.sha1); + } + + static boolean importPending(Entry entry, String targetId) { + String revision = importRevision(entry); + return !revision.equals(entry.importedTargets.get(targetId)) + && !revision.equals(entry.importAttempts.get(targetId)); + } + + static void recordDeterministicImportAttempt(Entry entry, String targetId) { + entry.importedTargets.remove(targetId); + entry.importAttempts.put(targetId, importRevision(entry)); + entry.structuresImported = false; + } + + static void recordSuccessfulImport(Entry entry, String targetId) { + entry.importedTargets.put(targetId, importRevision(entry)); + entry.importAttempts.remove(targetId); + } + + static void prepareImportRecoveryInventory(Entry entry, String targetId) { + Map desired = importBundleInventory(entry); + Map recovery = new TreeMap<>(desired); + recovery.putAll(entry.importedBundles.getOrDefault(targetId, Map.of())); + entry.importedBundles.put(targetId, recovery); + entry.importedTargets.remove(targetId); + entry.importAttempts.remove(targetId); + entry.structuresImported = false; + } + + private static boolean reconcileFailedImportInventories( + File root, + Manifest manifest, + IrisData data, + String targetId, + Set pendingUrls, + Map entriesByUrl + ) { + boolean reconciled = true; + for (String pendingUrl : pendingUrls) { + Entry entry = entriesByUrl.get(pendingUrl); + try { + reconcileFailedImportInventory(data, entry, targetId); + } catch (IOException | RuntimeException e) { + reconciled = false; + IrisLogging.reportError("Could not reconcile partial editable structure imports for '" + + pendingUrl + "' in pack '" + data.getDataFolder().getPath() + + "'; the conservative recovery inventory remains pending.", e); + } + } + try { + writeManifestChecked(root, manifest); + } catch (IOException e) { + reconciled = false; + IrisLogging.reportError("Could not persist reconciled partial editable structure imports for pack '" + + data.getDataFolder().getPath() + "'; the earlier recovery inventory remains durable.", e); + } + return reconciled; + } + + private static boolean recordDeterministicImportAttempts( + File root, + Manifest manifest, + String targetId, + Set incompleteUrls, + Map entriesByUrl + ) { + for (String incompleteUrl : incompleteUrls) { + recordDeterministicImportAttempt(entriesByUrl.get(incompleteUrl), targetId); + } + try { + writeManifestChecked(root, manifest); + return true; + } catch (IOException e) { + for (String incompleteUrl : incompleteUrls) { + entriesByUrl.get(incompleteUrl).importAttempts.remove(targetId); + } + IrisLogging.reportError("Could not persist deterministic editable structure import attempts; the sources remain pending for retry.", e); + return false; + } + } + + private static void reconcileFailedImportInventory( + IrisData data, + Entry entry, + String targetId + ) throws IOException { + Map> claims = importBundleClaims(entry, targetId); + Map reconciled = new TreeMap<>(); + StructureTransactionWriter writer = new StructureTransactionWriter(data.getDataFolder().toPath()); + for (Map.Entry> bundle : claims.entrySet()) { + StructureKey targetKey; + try { + targetKey = StructureKey.parse(bundle.getKey()); + } catch (RuntimeException e) { + throw new IOException("Invalid editable structure target key '" + bundle.getKey() + "'", e); + } + Optional source = writer.ownedSource(targetKey); + if (source.isPresent() && sourceClaimsContain(bundle.getValue(), source.get())) { + reconciled.put(bundle.getKey(), source.get().key().value()); + } + } + if (reconciled.isEmpty()) { + entry.importedBundles.remove(targetId); + } else { + entry.importedBundles.put(targetId, reconciled); + } + entry.importedTargets.remove(targetId); + entry.importAttempts.remove(targetId); + entry.structuresImported = false; + } + + static boolean cleanupRemovedImports( + IrisData data, + String targetId, + Set configured, + List entries, + Map entriesByUrl + ) { + boolean successful = true; + Map retainedBundles = desiredBundles(configured, entriesByUrl); + for (Entry entry : entries) { + if (configured.contains(entry.url)) { + continue; + } + Map inventory = entry.importedBundles.get(targetId); + if (inventory == null && !hasImportState(entry, targetId)) { + continue; + } + Map resolvedInventory = inventory == null ? Map.of() : inventory; + for (Entry retainedEntry : entries) { + if (configured.contains(retainedEntry.url)) { + retainedEntry.importedTargets.remove(targetId); + retainedEntry.importAttempts.remove(targetId); + retainedEntry.structuresImported = false; + } + } + Map removable = new TreeMap<>(resolvedInventory); + removable.keySet().removeAll(retainedBundles.keySet()); + Map remaining = cleanupImportedBundles(data, removable); + if (!remaining.isEmpty()) { + Map retained = new TreeMap<>(); + for (Map.Entry bundle : resolvedInventory.entrySet()) { + if (retainedBundles.containsKey(bundle.getKey()) || remaining.containsKey(bundle.getKey())) { + retained.put(bundle.getKey(), bundle.getValue()); + } + } + entry.importedBundles.put(targetId, retained); + entry.importedTargets.remove(targetId); + entry.importAttempts.remove(targetId); + entry.structuresImported = false; + successful = false; + continue; + } + entry.importedBundles.remove(targetId); + entry.importedTargets.remove(targetId); + entry.importAttempts.remove(targetId); + } + return successful; + } + + private static boolean hasImportState(Entry entry, String targetId) { + return entry.importedBundles.containsKey(targetId) + || entry.importedTargets.containsKey(targetId) + || entry.importAttempts.containsKey(targetId); + } + + private static Map cleanupImportedBundles(IrisData data, Map inventory) { + Map remaining = new TreeMap<>(); + StructureTransactionWriter writer = new StructureTransactionWriter(data.getDataFolder().toPath()); + boolean removed = false; + for (Map.Entry bundle : inventory.entrySet()) { + StructureKey sourceKey; + try { + sourceKey = StructureKey.parse(bundle.getValue()); + StructureSource.Kind sourceKind = sourceKey.namespace().equals("minecraft") + ? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK; + removed |= writer.removeManagedDatapackOwned( + StructureKey.parse(bundle.getKey()), + sourceKind, + sourceKey + ); + } catch (IOException | RuntimeException e) { + remaining.put(bundle.getKey(), bundle.getValue()); + IrisLogging.reportError("Preserving imported structure bundle '" + bundle.getKey() + + "' in pack '" + data.getDataFolder().getPath() + + "' because ownership-safe cleanup failed.", e); + } + } + if (removed) { + data.invalidateStructureResources(); + } + return remaining; + } + + private static Map desiredBundles(Set configured, Map entriesByUrl) { + Map bundles = new TreeMap<>(); + for (String url : configured) { + Entry entry = entriesByUrl.get(url); + if (entry != null) { + bundles.putAll(importBundleInventory(entry)); + } + } + return bundles; + } + + static Map importBundleInventory(Entry entry) { + Map bundles = new TreeMap<>(); + for (String structureKey : copyList(entry.structureKeys)) { + bundles.put("iris:" + StructureImporter.deriveName(structureKey), structureKey); + } + for (String templateKey : copyList(entry.templateKeys)) { + bundles.put("iris:" + BulkStructureImporter.templateNameFor(templateKey), templateKey); + } + return bundles; + } + + private static void flattenIfWrapped(File dir) throws IOException { + if (new File(dir, "pack.mcmeta").isFile()) { + return; + } + File[] children = dir.listFiles(); + if (children == null) { + return; + } + File singleDir = null; + int dirCount = 0; + int fileCount = 0; + for (File child : children) { + if (child.isDirectory()) { + dirCount++; + singleDir = child; + } else { + fileCount++; + } + } + if (dirCount != 1 || fileCount != 0 || singleDir == null || !new File(singleDir, "pack.mcmeta").isFile()) { + return; + } + File[] inner = singleDir.listFiles(); + if (inner != null) { + for (File item : inner) { + File moved = new File(dir, item.getName()); + if (item.renameTo(moved)) { + continue; + } + if (item.isDirectory()) { + IO.copyDirectory(item.toPath(), moved.toPath()); + } else { + Files.copy(item.toPath(), moved.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + } + deleteInstallScratch(singleDir, "wrapped datapack extraction"); + } + + static DownloadResult download(String url, File dest, String etag, String lastModified) throws IOException { + URI current; + try { + current = new URI(url); + } catch (URISyntaxException e) { + throw new IOException("Invalid datapack URL " + url, e); + } + for (int attempt = 0; attempt < MAX_REDIRECTS; attempt++) { + if (!"http".equalsIgnoreCase(current.getScheme()) && !"https".equalsIgnoreCase(current.getScheme())) { + throw new IOException("Datapack URL must use HTTP or HTTPS: " + current); + } + URL target = current.toURL(); + HttpURLConnection connection = (HttpURLConnection) target.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", USER_AGENT); + if (etag != null && !etag.isBlank()) { + connection.setRequestProperty("If-None-Match", etag); + } + if (lastModified != null && !lastModified.isBlank()) { + connection.setRequestProperty("If-Modified-Since", lastModified); + } + connection.setConnectTimeout(20000); + connection.setReadTimeout(60000); + connection.setInstanceFollowRedirects(false); + try { + int code = connection.getResponseCode(); + if (code == HttpURLConnection.HTTP_NOT_MODIFIED) { + String responseEtag = headerOrFallback(connection, "ETag", etag); + String responseLastModified = headerOrFallback(connection, "Last-Modified", lastModified); + return new DownloadResult(true, responseEtag, responseLastModified); + } + if (code / 100 == 3) { + String location = connection.getHeaderField("Location"); + if (location == null || location.isBlank()) { + throw new IOException("Redirect without a location header from " + current); + } + try { + current = current.resolve(new URI(location)); + } catch (URISyntaxException e) { + throw new IOException("Invalid redirect location from " + current + ": " + location, e); + } + continue; + } + if (code != 200) { + throw new IOException("HTTP " + code + " downloading " + current); + } + long declaredLength = connection.getContentLengthLong(); + if (declaredLength > MAX_DOWNLOAD_BYTES) { + throw new IOException("Datapack download exceeds " + MAX_DOWNLOAD_BYTES + " bytes"); + } + + File parent = dest.getParentFile(); + Path parentPath = parent == null ? Path.of(".").toAbsolutePath().normalize() : parent.toPath(); + ensureScratchDirectory(parentPath.toFile(), "datapack download cache"); + Path temporary = Files.createTempFile(parentPath, dest.getName() + "-", ".part"); + try { + long downloaded = 0; + String responseEtag = connection.getHeaderField("ETag"); + String responseLastModified = connection.getHeaderField("Last-Modified"); + try (InputStream in = connection.getInputStream(); + OutputStream out = Files.newOutputStream(temporary)) { + byte[] buffer = new byte[8192]; + int length; + while ((length = in.read(buffer)) > 0) { + downloaded += length; + if (downloaded > MAX_DOWNLOAD_BYTES) { + throw new IOException("Datapack download exceeds " + MAX_DOWNLOAD_BYTES + " bytes"); + } + out.write(buffer, 0, length); + } + } + move(temporary, dest.toPath()); + return new DownloadResult(false, responseEtag, responseLastModified); + } finally { + Files.deleteIfExists(temporary); + } + } finally { + connection.disconnect(); + } + } + throw new IOException("Too many redirects downloading " + url); + } + + private static String headerOrFallback(HttpURLConnection connection, String name, String fallback) { + String value = connection.getHeaderField(name); + return value == null || value.isBlank() ? fallback : value; + } + + private static String sha1(File file) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + try (InputStream in = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; + int length; + while ((length = in.read(buffer)) > 0) { + digest.update(buffer, 0, length); + } + } + byte[] hash = digest.digest(); + StringBuilder builder = new StringBuilder(hash.length * 2); + for (byte value : hash) { + builder.append(String.format("%02x", value)); + } + return builder.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-1 algorithm unavailable", e); + } + } + + private static String deriveId(ResolvedDatapack resolved) { + String base = resolved.getProjectSlug(); + if (base == null || base.isBlank()) { + base = resolved.getFileName(); + int dot = base.lastIndexOf('.'); + if (dot > 0) { + base = base.substring(0, dot); + } + } + String id = sanitizeId(base); + if (resolved.isDirect()) { + return id + "-" + ModrinthResolver.directIdentity(resolved.getDownloadUrl()); + } + return id; + } + + private static String sanitizeId(String value) { + if (value == null) { + return "datapack"; + } + String lower = value.toLowerCase(Locale.ROOT).trim(); + StringBuilder builder = new StringBuilder(lower.length()); + for (int i = 0; i < lower.length(); i++) { + char c = lower.charAt(i); + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.') { + builder.append(c); + } else if (c == ' ' || c == '/' || c == '\\') { + builder.append('-'); + } + } + String cleaned = builder.toString().replaceAll("-+", "-"); + cleaned = cleaned.replaceAll("^[-_.]+", "").replaceAll("[-_.]+$", ""); + return cleaned.isBlank() ? "datapack" : cleaned; + } + + private static boolean isValidManagedId(String value) { + return value != null && !value.isBlank() && value.equals(sanitizeId(value)) + && !RESERVED_IDS.contains(value); + } + + private static String serverMcVersion() { + return serverMcVersion(Bukkit.getServer()); + } + + static String serverMcVersion(Server server) { + MinecraftVersion detected = MinecraftVersion.detect(server); + return detected == null ? null : detected.value(); + } + + private static String safe(String value) { + return value == null || value.isBlank() ? "?" : value; + } + + private static String safeFile(String value) { + return value == null || value.isBlank() ? "unknown" : value.replaceAll("[^a-zA-Z0-9._-]", "_"); + } + + private static void diagnoseConflicts(VolmitSender sender, Manifest manifest) { + Map> owners = new TreeMap<>(); + for (Entry entry : manifest.entries) { + for (String key : copyList(entry.structureKeys)) { + owners.computeIfAbsent("structure " + key, ignored -> new ArrayList<>()).add(entry.id); + } + for (String key : copyList(entry.templateKeys)) { + owners.computeIfAbsent("template " + key, ignored -> new ArrayList<>()).add(entry.id); + } + } + int conflicts = 0; + for (Map.Entry> resource : owners.entrySet()) { + if (resource.getValue().size() < 2) { + continue; + } + conflicts++; + if (conflicts <= 20) { + message(sender, C.YELLOW + " Registry conflict: " + C.WHITE + resource.getKey() + + C.YELLOW + " is supplied by " + String.join(", ", resource.getValue())); + } + } + if (conflicts > 0) { + message(sender, C.YELLOW + "Detected " + conflicts + " external datapack registry conflict(s). Minecraft's enabled-pack order in level.dat determines precedence; datapackImports order does not."); + } + if (conflicts > 20) { + message(sender, C.GRAY + " " + (conflicts - 20) + " additional conflict(s) omitted."); + } + } + + private static void pruneCache(File cacheDir) { + File[] files = cacheDir.listFiles(File::isFile); + if (files == null) { + return; + } + List archives = new ArrayList<>(); + long totalBytes = 0; + for (File file : files) { + if (file.getName().endsWith(".part")) { + IO.delete(file); + continue; + } + if (file.getName().endsWith(".zip")) { + archives.add(file); + totalBytes += Math.max(0, file.length()); + } + } + archives.sort(Comparator.comparingLong(File::lastModified)); + int remaining = archives.size(); + for (File archive : archives) { + if (remaining <= MAX_CACHE_FILES && totalBytes <= MAX_CACHE_BYTES) { + break; + } + long size = Math.max(0, archive.length()); + IO.delete(archive); + if (!archive.exists()) { + remaining--; + totalBytes -= size; + } + } + } + + private static List copyList(List values) { + return values == null ? List.of() : List.copyOf(values); + } + + static Entry copyEntry(Entry source) { + Entry resolved = Objects.requireNonNull(source, "Datapack manifest entry must not be null"); + Entry copy = new Entry(); + copy.url = resolved.url; + copy.id = resolved.id; + copy.versionId = resolved.versionId; + copy.versionNumber = resolved.versionNumber; + copy.sha1 = resolved.sha1; + copy.filename = resolved.filename; + copy.etag = resolved.etag; + copy.lastModified = resolved.lastModified; + copy.installedEpoch = resolved.installedEpoch; + copy.structuresImported = resolved.structuresImported; + copy.stagingMetadata = resolved.stagingMetadata; + copy.structureKeys = new ArrayList<>(copyList(resolved.structureKeys)); + copy.templateKeys = new ArrayList<>(copyList(resolved.templateKeys)); + copy.installMetadata = new HashMap<>(Objects.requireNonNullElseGet( + resolved.installMetadata, Map::of)); + copy.importedTargets = new HashMap<>(Objects.requireNonNullElseGet( + resolved.importedTargets, Map::of)); + copy.importAttempts = new HashMap<>(Objects.requireNonNullElseGet( + resolved.importAttempts, Map::of)); + copy.importedBundles = new HashMap<>(); + if (resolved.importedBundles != null) { + for (Map.Entry> bundle : resolved.importedBundles.entrySet()) { + copy.importedBundles.put(bundle.getKey(), new HashMap<>(bundle.getValue())); + } + } + return copy; + } + + private static String hex(byte[] hash) { + StringBuilder builder = new StringBuilder(hash.length * 2); + for (byte value : hash) { + builder.append(String.format("%02x", value)); + } + return builder.toString(); + } + + private static void updateDigestInt(MessageDigest digest, int value) { + digest.update((byte) (value >>> 24)); + digest.update((byte) (value >>> 16)); + digest.update((byte) (value >>> 8)); + digest.update((byte) value); + } + + private static void updateDigestLong(MessageDigest digest, long value) { + digest.update((byte) (value >>> 56)); + digest.update((byte) (value >>> 48)); + digest.update((byte) (value >>> 40)); + digest.update((byte) (value >>> 32)); + digest.update((byte) (value >>> 24)); + digest.update((byte) (value >>> 16)); + digest.update((byte) (value >>> 8)); + digest.update((byte) value); + } + + private static void message(VolmitSender sender, String text) { + if (sender != null) { + sender.sendMessage(text); + return; + } + IrisLogging.info(text); + } + + private static Manifest readManifest(File root) { + File file = new File(root, "manifest.json"); + Manifest manifest = null; + boolean recoverFromStaging = false; + Path path = file.toPath(); + if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + manifest = new Manifest(); + } else if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + IrisLogging.error("Unreadable datapack manifest " + file.getPath() + + "; moving the non-regular path aside before recovering Iris-managed staging"); + recoverFromStaging = quarantine(path); + } else { + try { + String json = readBoundedUtf8(path, MAX_MANIFEST_BYTES, "Datapack manifest"); + manifest = GSON.fromJson(json, Manifest.class); + if (manifest == null) { + throw new IOException("Datapack manifest is empty"); + } + } catch (Exception e) { + IrisLogging.reportError("Unreadable datapack manifest " + file.getPath() + + "; moving it aside before recovering Iris-managed staging", e); + recoverFromStaging = quarantine(file.toPath()); + } + } + if (manifest == null) { + manifest = new Manifest(); + } + normalizeManifest(manifest); + if (recoverFromStaging) { + recoverManifestFromStaging(root, manifest); + } + return manifest; + } + + private static boolean quarantine(Path file) { + try { + move(file, file.resolveSibling(file.getFileName().toString() + ".corrupt-" + System.currentTimeMillis())); + return true; + } catch (IOException e) { + IrisLogging.reportError("Failed to move aside corrupt datapack manifest " + file, e); + return false; + } + } + + private static void normalizeManifest(Manifest manifest) { + if (manifest.entries == null) { + manifest.entries = new ArrayList<>(); + return; + } + List normalized = new ArrayList<>(); + Set urls = new HashSet<>(); + Set ids = new HashSet<>(); + for (Entry entry : manifest.entries) { + if (entry == null || entry.url == null || entry.url.isBlank() || entry.id == null || entry.id.isBlank()) { + continue; + } + entry.url = entry.url.trim(); + if (!isValidManagedId(entry.id)) { + IrisLogging.warn("Ignoring datapack manifest entry with invalid id '" + entry.id + "'"); + continue; + } + entry.structureKeys = normalizeKeys(entry.structureKeys); + entry.templateKeys = normalizeKeys(entry.templateKeys); + entry.stagingMetadata = entry.stagingMetadata == null ? "" : entry.stagingMetadata.trim(); + entry.installMetadata = normalizeImportedTargets(entry.installMetadata); + entry.importedTargets = normalizeImportedTargets(entry.importedTargets); + entry.importAttempts = normalizeImportedTargets(entry.importAttempts); + entry.importedBundles = normalizeImportedBundles(entry.importedBundles); + if (!urls.add(entry.url) || !ids.add(entry.id)) { + IrisLogging.warn("Ignoring duplicate datapack manifest entry for id '" + entry.id + "' and url " + entry.url); + continue; + } + normalized.add(entry); + } + manifest.entries = normalized; + } + + private static void recoverManifestFromStaging(File root, Manifest manifest) { + File staging = new File(root, "staging"); + File[] directories = staging.listFiles(File::isDirectory); + if (directories == null) { + return; + } + for (File directory : directories) { + try { + Ownership ownership = readOwnershipOrNull(directory); + if (ownership == null || !directory.getName().equals(ownership.id) + || manifest.find(ownership.url) != null || manifest.findById(ownership.id) != null) { + continue; + } + validateManagedDirectory(directory, ownership.id); + if (!Objects.equals(ownership.contentHash, directoryHash(directory))) { + IrisLogging.warn("Ignoring corrupt Iris-managed datapack staging at " + directory.getPath()); + continue; + } + Entry recovered = ownership.toEntry(); + recovered.installedEpoch = directory.lastModified(); + manifest.put(recovered); + IrisLogging.warn("Recovered Iris-managed datapack manifest entry '" + recovered.id + "' from staging."); + } catch (IOException e) { + IrisLogging.warn("Ignoring orphan datapack staging at " + directory.getPath() + ": " + e.getMessage()); + } + } + } + + private static List normalizeKeys(List keys) { + TreeSet normalized = new TreeSet<>(); + if (keys != null) { + for (String key : keys) { + if (key != null && !key.isBlank()) { + normalized.add(key.trim().toLowerCase(Locale.ROOT)); + } + } + } + return new ArrayList<>(normalized); + } + + private static Map normalizeImportedTargets(Map targets) { + Map normalized = new HashMap<>(); + if (targets == null) { + return normalized; + } + for (Map.Entry target : targets.entrySet()) { + if (target.getKey() != null && !target.getKey().isBlank() + && target.getValue() != null && !target.getValue().isBlank()) { + normalized.put(target.getKey(), target.getValue()); + } + } + return normalized; + } + + private static Map> normalizeImportedBundles( + Map> targets + ) { + Map> normalized = new HashMap<>(); + if (targets == null) { + return normalized; + } + for (Map.Entry> target : targets.entrySet()) { + if (target.getKey() == null || target.getKey().isBlank() || target.getValue() == null) { + continue; + } + Map bundles = new TreeMap<>(); + for (Map.Entry bundle : target.getValue().entrySet()) { + if (bundle.getKey() != null && !bundle.getKey().isBlank() + && bundle.getValue() != null && !bundle.getValue().isBlank()) { + bundles.put(bundle.getKey(), bundle.getValue()); + } + } + normalized.put(target.getKey(), bundles); + } + return normalized; + } + + private static void writeManifest(File root, Manifest manifest) { + try { + writeManifestChecked(root, manifest); + } catch (IOException e) { + IrisLogging.reportError("Failed to write datapack manifest " + + new File(root, "manifest.json").toPath(), e); + } + } + + private static void writeManifestChecked(File root, Manifest manifest) throws IOException { + try (ManifestWrite write = prepareManifestWrite(root, manifest)) { + write.publish(); + } + } + + private static ManifestWrite prepareManifestWrite(File root, Manifest manifest) throws IOException { + Path file = new File(root, "manifest.json").toPath(); + Path parent = file.getParent(); + Files.createDirectories(parent); + Path temp = Files.createTempFile(parent, "manifest", ".json.tmp"); + Path rollback = null; + try { + byte[] content = GSON.toJson(manifest).getBytes(StandardCharsets.UTF_8); + if (content.length > MAX_MANIFEST_BYTES) { + throw new IOException("Datapack manifest exceeds " + MAX_MANIFEST_BYTES + " bytes"); + } + Files.write(temp, content, StandardOpenOption.TRUNCATE_EXISTING); + forceFile(temp); + if (Files.exists(file, LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(file)) { + throw new IOException("Datapack manifest is not a regular file: " + file); + } + rollback = Files.createTempFile(parent, "manifest-rollback", ".json.tmp"); + Files.copy(file, rollback, StandardCopyOption.REPLACE_EXISTING); + forceFile(rollback); + } + return new ManifestWrite(temp, file, rollback); + } catch (IOException | RuntimeException e) { + try { + Files.deleteIfExists(temp); + } catch (IOException cleanupFailure) { + e.addSuppressed(cleanupFailure); + } + if (rollback != null) { + try { + Files.deleteIfExists(rollback); + } catch (IOException cleanupFailure) { + e.addSuppressed(cleanupFailure); + } + } + throw e; + } + } + + private static void move(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } + + private static void moveNew(Path source, Path target) throws IOException { + if (pathExists(target, "move target")) { + throw new IOException("Refusing to replace concurrently-created path " + target); + } + Files.move(source, target); + } + + private static void ensureScratchDirectory(File directory, String purpose) throws IOException { + Path path = directory.toPath(); + if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + if (Files.isSymbolicLink(path) || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Refusing invalid " + purpose + " directory " + directory.getPath()); + } + return; + } + Files.createDirectories(path); + if (Files.isSymbolicLink(path) || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Could not create a safe " + purpose + " directory " + directory.getPath()); + } + } + + private static void verifyDirectoryContainerIfPresent(File directory, String purpose) throws IOException { + Path path = directory.toPath(); + if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (Files.isSymbolicLink(path) || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Refusing invalid " + purpose + " directory " + directory.getPath()); + } + } + + private static DatapackCoordinator createInstallCoordinator( + File root, + Entry entry, + List plans, + boolean manifestAlreadyMatched + ) throws IOException { + CoordinatorJournal journal = newCoordinatorJournal( + CoordinatorOperation.INSTALL, + entry, + manifestAlreadyMatched + ); + for (InstallPlan plan : plans) { + if (!plan.publishRequired()) { + continue; + } + CoordinatorDirectory directory = new CoordinatorDirectory(); + directory.target = normalizedPath(plan.target()); + directory.pending = normalizedPath(plan.pending()); + directory.backup = normalizedPath(plan.backup()); + directory.hadTarget = plan.hadTarget(); + directory.originalHash = plan.originalHash(); + directory.desiredHash = plan.desiredHash(); + directory.originalMarkerHash = plan.originalMarkerHash(); + directory.desiredMarkerHash = plan.desiredMarkerHash(); + directory.originalIdentity = plan.originalIdentity(); + directory.desiredIdentity = plan.desiredIdentity(); + directory.targetRoot = plan.targetRootIdentity(); + directory.scratchRoot = plan.scratchRootIdentity(); + directory.targetRootIdentity = plan.targetRootFileIdentity(); + directory.scratchRootIdentity = plan.scratchRootFileIdentity(); + journal.directories.add(directory); + } + return createCoordinator(root, journal); + } + + private static DatapackCoordinator createRemovalCoordinator( + File root, + Entry entry, + DirectoryRemoval directoryRemoval, + EditableImportRemoval editableRemoval + ) throws IOException { + CoordinatorJournal journal = newCoordinatorJournal(CoordinatorOperation.REMOVE, entry, true); + for (DirectoryMove move : directoryRemoval.moves()) { + CoordinatorDirectory directory = new CoordinatorDirectory(); + directory.target = normalizedPath(move.target()); + directory.pending = ""; + directory.backup = normalizedPath(move.backup()); + directory.hadTarget = true; + directory.originalHash = move.originalHash(); + directory.desiredHash = ""; + directory.originalMarkerHash = move.originalMarkerHash(); + directory.desiredMarkerHash = ""; + directory.originalIdentity = move.originalIdentity(); + directory.desiredIdentity = ""; + directory.targetRoot = move.targetRootIdentity(); + directory.scratchRoot = move.scratchRootIdentity(); + directory.targetRootIdentity = move.targetRootFileIdentity(); + directory.scratchRootIdentity = move.scratchRootFileIdentity(); + journal.directories.add(directory); + } + for (StructureTransactionWriter.PreparedRemovalToken token : editableRemoval.recoveryTokens()) { + CoordinatorEditable editable = new CoordinatorEditable(); + editable.packRoot = token.packRoot().toString(); + editable.transactionId = token.transactionId().toString(); + editable.claimId = UUID.randomUUID().toString(); + journal.editables.add(editable); + } + Path transactionRoot = coordinatorTransactionRoot(root, journal); + try { + editableRemoval.claimRecoveryOwners(transactionRoot, journal); + writeCoordinatorJournal(transactionRoot, journal); + return new DatapackCoordinator(transactionRoot, journal); + } catch (IOException | RuntimeException creationFailure) { + try { + deleteInstallScratch(transactionRoot.toFile(), "incomplete datapack transaction"); + } catch (IOException cleanupFailure) { + creationFailure.addSuppressed(cleanupFailure); + } + if (creationFailure instanceof IOException ioFailure) { + throw ioFailure; + } + throw creationFailure; + } + } + + private static CoordinatorJournal newCoordinatorJournal( + CoordinatorOperation operation, + Entry entry, + boolean manifestAlreadyMatched + ) { + CoordinatorJournal journal = new CoordinatorJournal(); + journal.schemaVersion = TRANSACTION_SCHEMA; + journal.transactionId = UUID.randomUUID().toString(); + journal.operation = operation; + journal.phase = CoordinatorPhase.PREPARED; + journal.id = entry.id; + journal.url = entry.url; + journal.versionId = entry.versionId; + journal.versionNumber = entry.versionNumber; + journal.sha1 = entry.sha1; + journal.manifestAlreadyMatched = manifestAlreadyMatched; + return journal; + } + + private static DatapackCoordinator createCoordinator(File root, CoordinatorJournal journal) throws IOException { + Path transactionRoot = coordinatorTransactionRoot(root, journal); + writeCoordinatorJournal(transactionRoot, journal); + return new DatapackCoordinator(transactionRoot, journal); + } + + private static Path coordinatorTransactionRoot(File root, CoordinatorJournal journal) throws IOException { + File transactionDirectory = new File(root, TRANSACTION_DIRECTORY); + ensureScratchDirectory(transactionDirectory, "datapack transaction"); + Path transactionRoot = new File(transactionDirectory, journal.transactionId).toPath().toAbsolutePath().normalize(); + if (Files.exists(transactionRoot, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Datapack transaction id already exists: " + journal.transactionId); + } + return transactionRoot; + } + + private static String normalizedPath(File file) { + return file.toPath().toAbsolutePath().normalize().toString(); + } + + private static String realDirectoryPath(File directory, String purpose) throws IOException { + Path path = directory.toPath().toAbsolutePath().normalize(); + if (Files.isSymbolicLink(path) || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid " + purpose + " " + path); + } + return path.toRealPath().toString(); + } + + static boolean recoverTransactions(File root, List worldFolders) throws IOException { + boolean changed = recoverStagingScratch(new File(root, "staging")); + File transactionDirectory = new File(root, TRANSACTION_DIRECTORY); + Path transactionPath = transactionDirectory.toPath(); + if (!Files.exists(transactionPath, LinkOption.NOFOLLOW_LINKS)) { + return recoverInstallScratch(root, worldFolders) | changed; + } + verifyDirectoryContainerIfPresent(transactionDirectory, "datapack transaction"); + Manifest committedManifest = readCommittedManifest(root); + List transactionRoots; + try (Stream paths = Files.list(transactionPath)) { + transactionRoots = paths.limit(MAX_TRANSACTION_COUNT + 2L).sorted().toList(); + } + if (transactionRoots.size() == MAX_TRANSACTION_COUNT + 2) { + throw new IOException("Datapack transaction directory contains too many entries"); + } + int transactionCount = 0; + for (Path transactionRoot : transactionRoots) { + if (isHarmlessRecoveryArtifact(transactionRoot)) { + continue; + } + transactionCount++; + if (transactionCount > MAX_TRANSACTION_COUNT) { + throw new IOException("Datapack transaction count exceeds " + MAX_TRANSACTION_COUNT); + } + } + for (Path transactionRoot : transactionRoots) { + if (isHarmlessRecoveryArtifact(transactionRoot)) { + changed |= Files.deleteIfExists(transactionRoot); + continue; + } + recoverTransaction(root, worldFolders, committedManifest, transactionPath, transactionRoot); + changed = true; + } + changed |= transactionDirectory.delete(); + return recoverInstallScratch(root, worldFolders) | changed; + } + + private static boolean recoverInstallScratch(File root, List worldFolders) throws IOException { + Set scratchRoots = new TreeSet<>(); + scratchRoots.add(installScratchRoot(new File(root, "staging")).toPath().toAbsolutePath().normalize()); + for (File worldFolder : worldFolders) { + scratchRoots.add(installScratchRoot(worldFolder).toPath().toAbsolutePath().normalize()); + } + boolean changed = false; + for (Path scratchRoot : scratchRoots) { + changed |= recoverInstallScratchRoot(scratchRoot); + } + return changed; + } + + private static boolean recoverInstallScratchRoot(Path scratchRoot) throws IOException { + if (!Files.exists(scratchRoot, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + verifyDirectoryContainerIfPresent(scratchRoot.toFile(), "datapack install scratch"); + List children; + try (Stream paths = Files.list(scratchRoot)) { + children = paths.limit(MAX_MANAGED_PATHS + 2L).sorted().toList(); + } + int managedEntries = 0; + for (Path child : children) { + if (!isHarmlessRecoveryArtifact(child)) { + managedEntries++; + } + if (managedEntries > MAX_MANAGED_PATHS) { + throw new IOException("Datapack install scratch contains too many entries"); + } + } + + List pending = new ArrayList<>(); + List backups = new ArrayList<>(); + boolean changed = false; + for (Path child : children) { + if (isHarmlessRecoveryArtifact(child)) { + changed |= Files.deleteIfExists(child); + continue; + } + StagingScratch scratch = parseInstallScratch(scratchRoot, child); + if (scratch == null) { + throw new IOException("Unexpected datapack install scratch artifact " + child); + } + if (Files.isSymbolicLink(child) || !Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid datapack install scratch artifact " + child); + } + validateScratchTree(child); + if (scratch.kind() == StagingScratchKind.BACKUP) { + backups.add(scratch); + } else { + pending.add(scratch); + } + } + if (!backups.isEmpty()) { + throw new IOException("Preserving unjournaled datapack install backup " + + backups.getFirst().path()); + } + for (StagingScratch scratch : pending) { + deleteInstallScratch(scratch.path().toFile(), "orphan datapack install pending directory"); + changed = true; + } + changed |= scratchRoot.toFile().delete(); + return changed; + } + + private static StagingScratch parseInstallScratch(Path scratchRoot, Path child) throws IOException { + Path normalized = child.toAbsolutePath().normalize(); + if (!Objects.equals(normalized.getParent(), scratchRoot)) { + throw new IOException("Datapack install scratch artifact escapes its root: " + child); + } + String name = normalized.getFileName().toString(); + int uuidStart = name.length() - 36; + if (uuidStart <= 1 || name.charAt(uuidStart - 1) != '-') { + return null; + } + try { + UUID.fromString(name.substring(uuidStart)); + } catch (IllegalArgumentException e) { + return null; + } + String stem = name.substring(0, uuidStart - 1); + StagingScratchKind kind = stem.endsWith("-backup") + ? StagingScratchKind.BACKUP : StagingScratchKind.PENDING; + String id = kind == StagingScratchKind.BACKUP + ? stem.substring(0, stem.length() - "-backup".length()) : stem; + if (id.isBlank() || !id.equals(sanitizeId(id)) || RESERVED_IDS.contains(id)) { + return null; + } + return new StagingScratch(kind, id, normalized); + } + + private static boolean recoverStagingScratch(File stagingDirectory) throws IOException { + Path stagingRoot = stagingDirectory.toPath().toAbsolutePath().normalize(); + if (!Files.exists(stagingRoot, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + verifyDirectoryContainerIfPresent(stagingDirectory, "datapack staging"); + List children; + try (Stream paths = Files.list(stagingRoot)) { + children = paths.limit(MAX_MANAGED_PATHS + 1L).sorted().toList(); + } + if (children.size() > MAX_MANAGED_PATHS) { + throw new IOException("Datapack staging contains too many entries"); + } + + List pending = new ArrayList<>(); + Map> backups = new TreeMap<>(); + for (Path child : children) { + StagingScratch scratch = parseStagingScratch(stagingRoot, child); + if (scratch == null) { + continue; + } + if (Files.isSymbolicLink(child) || !Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid datapack staging scratch artifact " + child); + } + if (scratch.kind() == StagingScratchKind.PENDING) { + validateScratchTree(child); + pending.add(scratch); + } else { + backups.computeIfAbsent(scratch.id(), ignored -> new ArrayList<>()).add(scratch); + } + } + + for (Map.Entry> entry : backups.entrySet()) { + if (entry.getValue().size() != 1) { + throw new IOException("Ambiguous datapack staging backups for '" + entry.getKey() + "'"); + } + StagingScratch backup = entry.getValue().getFirst(); + Ownership backupOwnership = verifyManagedScratchDirectory(backup.path().toFile(), backup.id()); + Path target = stagingRoot.resolve(backup.id()).normalize(); + if (!Objects.equals(target.getParent(), stagingRoot)) { + throw new IOException("Datapack staging backup target escapes its root"); + } + if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { + Ownership targetOwnership = verifyManagedScratchDirectory(target.toFile(), backup.id()); + if (!Objects.equals(backupOwnership.url, targetOwnership.url)) { + throw new IOException("Datapack staging backup source does not match its target: " + target); + } + } + } + + boolean changed = false; + for (List matches : backups.values()) { + StagingScratch backup = matches.getFirst(); + Path target = stagingRoot.resolve(backup.id()).normalize(); + if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { + deleteVerifiedDirectory(backup.path().toFile()); + } else { + moveNew(backup.path(), target); + } + changed = true; + } + for (StagingScratch scratch : pending) { + deleteVerifiedDirectory(scratch.path().toFile()); + changed = true; + } + forceDirectoryIfSupported(stagingRoot); + return changed; + } + + private static StagingScratch parseStagingScratch(Path stagingRoot, Path child) throws IOException { + Path normalized = child.toAbsolutePath().normalize(); + if (!Objects.equals(normalized.getParent(), stagingRoot)) { + throw new IOException("Datapack staging artifact escapes its root: " + child); + } + String name = normalized.getFileName().toString(); + StagingScratchKind kind; + String prefix; + if (name.startsWith(".pending-")) { + kind = StagingScratchKind.PENDING; + prefix = ".pending-"; + } else if (name.startsWith(".backup-")) { + kind = StagingScratchKind.BACKUP; + prefix = ".backup-"; + } else { + return null; + } + int uuidStart = name.length() - 36; + if (uuidStart <= prefix.length() || name.charAt(uuidStart - 1) != '-') { + return null; + } + String id = name.substring(prefix.length(), uuidStart - 1); + if (!id.equals(sanitizeId(id)) || RESERVED_IDS.contains(id)) { + return null; + } + try { + UUID.fromString(name.substring(uuidStart)); + } catch (IllegalArgumentException e) { + return null; + } + return new StagingScratch(kind, id, normalized); + } + + private static void validateScratchTree(Path root) throws IOException { + FileStore rootStore = Files.getFileStore(root); + int[] pathCount = new int[]{0}; + Files.walkFileTree(root, new SimpleFileVisitor<>() { + private FileVisitResult inspect(Path entry, BasicFileAttributes attributes) throws IOException { + pathCount[0]++; + if (pathCount[0] > MAX_MANAGED_PATHS) { + throw new IOException("Datapack scratch contains too many paths: " + root); + } + if (attributes.isSymbolicLink() + || attributes.isOther() + || (!attributes.isDirectory() && !attributes.isRegularFile())) { + throw new IOException("Datapack scratch contains an unsupported file: " + entry); + } + if (!sameDatapackVolume(root, rootStore, entry)) { + throw new IOException("Datapack scratch crosses a filesystem boundary: " + entry); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory( + Path directory, + BasicFileAttributes attributes + ) throws IOException { + return inspect(directory, attributes); + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { + return inspect(file, attributes); + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException { + throw new IOException("Unable to inspect datapack scratch entry: " + file, failure); + } + }); + } + + private static boolean sameScratchVolume(Path first, Path second) throws IOException { + return sameScratchVolume(first, Files.getFileStore(first), second, Files.getFileStore(second)); + } + + static boolean sameScratchVolume( + Path first, + FileStore firstStore, + Path second, + FileStore secondStore + ) { + if (Objects.equals(firstStore, secondStore)) { + return true; + } + if (!isDefaultWindowsPath(first) || !isDefaultWindowsPath(second)) { + return false; + } + Path firstAbsolute = first.toAbsolutePath().normalize(); + Path secondAbsolute = second.toAbsolutePath().normalize(); + if ((firstAbsolute.toString().length() > WINDOWS_LEGACY_PATH_LIMIT) + == (secondAbsolute.toString().length() > WINDOWS_LEGACY_PATH_LIMIT)) { + return false; + } + Path firstRoot = firstAbsolute.getRoot(); + Path secondRoot = secondAbsolute.getRoot(); + if (firstRoot == null || secondRoot == null) { + return false; + } + return sameWindowsVolume( + firstStore, firstRoot.toString(), secondStore, secondRoot.toString()); + } + + static boolean sameWindowsVolume( + FileStore firstStore, + String firstRoot, + FileStore secondStore, + String secondRoot + ) { + if (firstRoot == null || secondRoot == null || !firstRoot.equalsIgnoreCase(secondRoot)) { + return false; + } + try { + Object firstSerial = firstStore.getAttribute("volume:vsn"); + Object secondSerial = secondStore.getAttribute("volume:vsn"); + return firstSerial != null && firstSerial.equals(secondSerial); + } catch (IOException | RuntimeException exception) { + return false; + } + } + + static boolean isSupportedScratchDirectory(BasicFileAttributes attributes) { + return attributes != null + && attributes.isDirectory() + && !attributes.isSymbolicLink() + && !attributes.isOther(); + } + + private static boolean isDefaultWindowsPath(Path path) { + return File.separatorChar == '\\' + && path.getFileSystem().equals(FileSystems.getDefault()); + } + + private static Ownership verifyManagedScratchDirectory(File directory, String id) throws IOException { + validateManagedDirectory(directory, id); + Ownership ownership = readOwnership(directory); + if (!Objects.equals(ownership.contentHash, directoryHash(directory))) { + throw new IOException("Datapack staging backup is modified or corrupt: " + directory.getPath()); + } + return ownership; + } + + private static boolean isHarmlessRecoveryArtifact(Path path) throws IOException { + if (!isFinderMetadata(path)) { + return false; + } + if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Suspicious datapack recovery artifact " + path); + } + return true; + } + + private static void recoverTransaction( + File root, + List worldFolders, + Manifest committedManifest, + Path transactionDirectory, + Path transactionRoot + ) throws IOException { + Path normalizedRoot = transactionRoot.toAbsolutePath().normalize(); + if (!Objects.equals(normalizedRoot.getParent(), transactionDirectory.toAbsolutePath().normalize()) + || Files.isSymbolicLink(normalizedRoot) + || !Files.isDirectory(normalizedRoot, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid datapack transaction directory " + transactionRoot); + } + try { + UUID.fromString(normalizedRoot.getFileName().toString()); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid datapack transaction directory name " + transactionRoot, e); + } + CoordinatorJournal journal = readCoordinatorJournal(normalizedRoot); + if (journal == null) { + deleteCoordinatorTransaction(normalizedRoot); + return; + } + validateCoordinatorJournal(root, worldFolders, committedManifest, normalizedRoot, journal); + boolean commit = coordinatorCommitDecision(committedManifest, journal); + resolveCoordinatorDirectories(journal, commit); + resolveCoordinatorEditables(journal, normalizedRoot, commit); + deleteCoordinatorTransaction(normalizedRoot); + } + + private static Manifest readCommittedManifest(File root) throws IOException { + Path manifestPath = new File(root, "manifest.json").toPath(); + if (!Files.exists(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + return new Manifest(); + } + if (Files.isSymbolicLink(manifestPath) + || !Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Datapack manifest is not a regular file: " + manifestPath); + } + Manifest manifest; + try { + manifest = GSON.fromJson( + readBoundedUtf8(manifestPath, MAX_MANIFEST_BYTES, "Datapack manifest"), + Manifest.class + ); + } catch (RuntimeException e) { + throw new IOException("Invalid datapack manifest " + manifestPath, e); + } + if (manifest == null) { + throw new IOException("Empty datapack manifest " + manifestPath); + } + normalizeManifest(manifest); + return manifest; + } + + private static CoordinatorJournal readCoordinatorJournal(Path transactionRoot) throws IOException { + Path committed = transactionRoot.resolve(TRANSACTION_JOURNAL); + Path next = transactionRoot.resolve(TRANSACTION_JOURNAL_NEXT); + if (Files.exists(committed, LinkOption.NOFOLLOW_LINKS)) { + return parseCoordinatorJournal(committed); + } + if (!Files.exists(next, LinkOption.NOFOLLOW_LINKS)) { + try (Stream entries = Files.list(transactionRoot)) { + if (entries.findAny().isEmpty()) { + return null; + } + } + throw new IOException("Missing datapack transaction journal " + committed); + } + try { + return parseCoordinatorJournal(next); + } catch (IOException firstWriteFailure) { + try (Stream entries = Files.list(transactionRoot)) { + List contents = entries.limit(2).toList(); + if (contents.size() == 1 && Objects.equals(contents.getFirst(), next) + && !Files.isSymbolicLink(next)) { + return null; + } + } + throw firstWriteFailure; + } + } + + private static CoordinatorJournal parseCoordinatorJournal(Path journalPath) throws IOException { + if (Files.isSymbolicLink(journalPath) + || !Files.isRegularFile(journalPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid datapack transaction journal " + journalPath); + } + try { + CoordinatorJournal journal = GSON.fromJson( + readBoundedUtf8( + journalPath, + MAX_TRANSACTION_JOURNAL_BYTES, + "Datapack transaction journal" + ), + CoordinatorJournal.class + ); + if (journal == null) { + throw new IOException("Empty datapack transaction journal " + journalPath); + } + return journal; + } catch (RuntimeException e) { + throw new IOException("Invalid datapack transaction journal " + journalPath, e); + } + } + + private static void validateCoordinatorJournal( + File root, + List worldFolders, + Manifest committedManifest, + Path transactionRoot, + CoordinatorJournal journal + ) throws IOException { + if (journal.schemaVersion != TRANSACTION_SCHEMA || journal.transactionId == null + || journal.operation == null || journal.phase == null || journal.id == null + || journal.url == null || journal.directories == null || journal.editables == null) { + throw new IOException("Incomplete datapack transaction journal at " + transactionRoot); + } + UUID transactionId; + try { + transactionId = UUID.fromString(journal.transactionId); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid datapack transaction id " + journal.transactionId, e); + } + if (!transactionId.toString().equals(transactionRoot.getFileName().toString())) { + throw new IOException("Datapack transaction id does not match its directory"); + } + if (!journal.id.equals(sanitizeId(journal.id)) || RESERVED_IDS.contains(journal.id)) { + throw new IOException("Invalid datapack transaction entry id " + journal.id); + } + if (journal.directories.size() > worldFolders.size() + 1 || journal.editables.size() > 10_000) { + throw new IOException("Datapack transaction journal contains too many participants"); + } + if (journal.operation == CoordinatorOperation.INSTALL && !journal.editables.isEmpty()) { + throw new IOException("Datapack install transaction unexpectedly contains editable participants"); + } + + Set allowedTargets = new HashSet<>(); + allowedTargets.add(new File(new File(root, "staging"), journal.id).toPath().toAbsolutePath().normalize()); + for (File worldFolder : worldFolders) { + allowedTargets.add(new File(worldFolder, journal.id).toPath().toAbsolutePath().normalize()); + } + Set seenTargets = new HashSet<>(); + Set seenTargetIdentities = new HashSet<>(); + for (CoordinatorDirectory directory : journal.directories) { + validateCoordinatorDirectory( + directory, journal.operation, allowedTargets, seenTargets, seenTargetIdentities); + } + Set seenEditables = new HashSet<>(); + Set allowedEditableRoots = journal.editables.isEmpty() + ? Set.of() : authoritativeEditableRoots(committedManifest, journal); + for (CoordinatorEditable editable : journal.editables) { + if (editable == null || editable.packRoot == null || editable.transactionId == null + || editable.claimId == null + || !seenEditables.add(editable.packRoot)) { + throw new IOException("Invalid duplicate editable participant in datapack transaction"); + } + Path packRoot = coordinatorPath(editable.packRoot, "editable pack root"); + Path realPackRoot = Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS) + ? packRoot.toRealPath() : packRoot; + if (Files.isSymbolicLink(packRoot) || !Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS) + || !packRoot.equals(realPackRoot) || !allowedEditableRoots.contains(realPackRoot)) { + throw new IOException("Invalid editable pack root in datapack transaction: " + packRoot); + } + try { + UUID.fromString(editable.transactionId); + UUID.fromString(editable.claimId); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid editable transaction recovery identity", e); + } + StructureTransactionWriter writer = new StructureTransactionWriter(packRoot); + boolean recoveryDataPresent = writer.verifyRecoveryOwner( + new StructureTransactionWriter.PreparedRemovalToken( + packRoot, + UUID.fromString(editable.transactionId) + ), + new StructureTransactionWriter.RecoveryOwner( + transactionRoot, + UUID.fromString(journal.transactionId), + UUID.fromString(editable.claimId) + ), + committedManifest.findById(journal.id) == null + && journal.phase == CoordinatorPhase.PUBLISHED + ); + if (!recoveryDataPresent && journal.phase != CoordinatorPhase.COMMITTED) { + throw new IOException("Editable structure recovery data disappeared before commit"); + } + } + } + + private static Set authoritativeEditableRoots( + Manifest committedManifest, + CoordinatorJournal journal + ) throws IOException { + Set roots = new HashSet<>(); + Entry committed = committedManifest.findById(journal.id); + if (committed != null) { + if (!Objects.equals(committed.url, journal.url)) { + throw new IOException("Datapack transaction conflicts with the committed editable pack owner"); + } + addExistingPackRoots(roots, committed.importedTargets.keySet()); + addExistingPackRoots(roots, committed.importAttempts.keySet()); + addExistingPackRoots(roots, committed.importedBundles.keySet()); + return roots; + } + if (journal.operation != CoordinatorOperation.REMOVE || !journal.phase.published()) { + throw new IOException("Datapack transaction has no committed editable pack authority"); + } + for (CoordinatorEditable editable : journal.editables) { + Path packRoot = coordinatorPath(editable.packRoot, "editable pack root"); + if (Files.isSymbolicLink(packRoot) || !Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid editable pack root in datapack transaction: " + packRoot); + } + Path realPackRoot = packRoot.toRealPath(); + if (!packRoot.equals(realPackRoot)) { + throw new IOException("Editable pack root changed before datapack recovery: " + packRoot); + } + roots.add(realPackRoot); + } + return roots; + } + + private static void addExistingPackRoots(Set roots, Set paths) throws IOException { + for (String path : paths) { + try { + Path root = Path.of(path).toAbsolutePath().normalize(); + if (Files.isDirectory(root)) { + roots.add(root.toRealPath()); + } + } catch (RuntimeException e) { + throw new IOException("Invalid editable pack root in the datapack manifest: " + path, e); + } + } + } + + private static void validateCoordinatorDirectory( + CoordinatorDirectory directory, + CoordinatorOperation operation, + Set allowedTargets, + Set seenTargets, + Set seenTargetIdentities + ) throws IOException { + if (directory == null || directory.target == null || directory.backup == null + || directory.originalHash == null || directory.desiredHash == null + || directory.originalMarkerHash == null || directory.desiredMarkerHash == null + || directory.originalIdentity == null || directory.desiredIdentity == null + || directory.targetRoot == null || directory.scratchRoot == null + || directory.targetRootIdentity == null || directory.scratchRootIdentity == null) { + throw new IOException("Incomplete directory participant in datapack transaction"); + } + Path target = coordinatorPath(directory.target, "target"); + if (!allowedTargets.contains(target) || !seenTargets.add(target)) { + throw new IOException("Datapack transaction target is outside its configured roots: " + target); + } + File targetFile = target.toFile(); + File targetParent = targetFile.getParentFile(); + Path targetRootIdentity = coordinatorPath(directory.targetRoot, "target root"); + if (!seenTargetIdentities.add(targetRootIdentity.resolve(target.getFileName()).normalize())) { + throw new IOException("Aliased datapack transaction target " + target); + } + validateRecoveryContainer( + targetParent.toPath(), + targetRootIdentity, + true, + "datapack target root" + ); + verifyDirectoryContainerIdentity( + targetParent, directory.targetRoot, directory.targetRootIdentity, "datapack target root"); + File scratchRoot = new File( + targetParent.getParentFile() == null ? targetParent : targetParent.getParentFile(), + operation == CoordinatorOperation.INSTALL ? ".iris-datapack-install" : ".iris-datapack-remove" + ); + Path expectedScratch = scratchRoot.toPath().toAbsolutePath().normalize(); + Path backup = coordinatorPath(directory.backup, "backup"); + if (!Objects.equals(backup.getParent(), expectedScratch) || Files.isSymbolicLink(backup)) { + throw new IOException("Invalid datapack transaction backup path " + backup); + } + Path pending = null; + if (operation == CoordinatorOperation.INSTALL) { + if (directory.pending == null || directory.pending.isBlank()) { + throw new IOException("Install transaction is missing its pending directory"); + } + pending = coordinatorPath(directory.pending, "pending directory"); + if (!Objects.equals(pending.getParent(), expectedScratch) || Files.isSymbolicLink(pending)) { + throw new IOException("Invalid datapack transaction pending path " + pending); + } + } else if (directory.pending != null && !directory.pending.isBlank()) { + throw new IOException("Removal transaction unexpectedly contains a pending directory"); + } + boolean scratchRequired = Files.exists(backup, LinkOption.NOFOLLOW_LINKS) + || pending != null && Files.exists(pending, LinkOption.NOFOLLOW_LINKS); + validateRecoveryContainer( + expectedScratch, + coordinatorPath(directory.scratchRoot, "scratch root"), + scratchRequired, + "datapack transaction scratch root" + ); + if (scratchRequired) { + verifyDirectoryContainerIdentity( + expectedScratch.toFile(), directory.scratchRoot, + directory.scratchRootIdentity, "datapack transaction scratch root"); + } + } + + private static void validateRecoveryContainer( + Path container, + Path expectedRealPath, + boolean required, + String purpose + ) throws IOException { + Path normalized = container.toAbsolutePath().normalize(); + Path normalizedExpected = expectedRealPath.toAbsolutePath().normalize(); + if (!Files.exists(normalized, LinkOption.NOFOLLOW_LINKS)) { + if (required) { + throw new IOException("Missing " + purpose + " " + normalized); + } + return; + } + if (Files.isSymbolicLink(normalized) + || !Files.isDirectory(normalized, LinkOption.NOFOLLOW_LINKS) + || !Objects.equals(normalized.toRealPath(), normalizedExpected)) { + throw new IOException("Changed or unsafe " + purpose + " " + normalized); + } + } + + private static boolean coordinatorCommitDecision(Manifest manifest, CoordinatorJournal journal) throws IOException { + Entry committed = manifest.findById(journal.id); + if (journal.operation == CoordinatorOperation.REMOVE) { + if (committed == null) { + if (!journal.phase.published()) { + throw new IOException("Datapack removal manifest changed before every participant was published"); + } + return true; + } + if (!Objects.equals(committed.url, journal.url) || journal.phase == CoordinatorPhase.COMMITTED) { + throw new IOException("Datapack removal journal conflicts with the committed manifest"); + } + return false; + } + + boolean matches = committed != null + && Objects.equals(committed.url, journal.url) + && Objects.equals(committed.versionId, journal.versionId) + && Objects.equals(committed.versionNumber, journal.versionNumber) + && Objects.equals(committed.sha1, journal.sha1); + if (journal.manifestAlreadyMatched) { + if (!matches) { + throw new IOException("Committed datapack changed while an install transaction was incomplete"); + } + return journal.phase.published(); + } + if (matches) { + if (!journal.phase.published()) { + throw new IOException("Datapack install manifest changed before every target was published"); + } + return true; + } + if (journal.phase == CoordinatorPhase.COMMITTED) { + throw new IOException("Committed datapack install journal conflicts with the manifest"); + } + return false; + } + + private static void resolveCoordinatorDirectories(CoordinatorJournal journal, boolean commit) throws IOException { + List directories = new ArrayList<>(journal.directories); + if (!commit) { + Collections.reverse(directories); + } + IOException failure = null; + for (CoordinatorDirectory directory : directories) { + try { + if (journal.operation == CoordinatorOperation.INSTALL) { + resolveInstallDirectory(journal, directory, commit); + } else { + resolveRemovalDirectory(directory, commit); + } + } catch (IOException | RuntimeException e) { + IOException participantFailure = e instanceof IOException ioFailure + ? ioFailure : new IOException("Failed resolving datapack directory participant", e); + if (failure == null) { + failure = participantFailure; + } else { + failure.addSuppressed(participantFailure); + } + } + } + if (failure != null) { + throw failure; + } + } + + private static void resolveInstallDirectory( + CoordinatorJournal journal, + CoordinatorDirectory directory, + boolean commit + ) throws IOException { + File target = new File(directory.target); + File pending = new File(directory.pending); + File backup = new File(directory.backup); + File targetRoot = new File(directory.targetRoot); + if (commit) { + verifyDesiredDirectory( + target, targetRoot, journal, directory.desiredHash, + directory.desiredMarkerHash, directory.desiredIdentity); + deleteOriginalBackupIfPresent( + backup, targetRoot, directory.originalHash, + directory.originalMarkerHash, directory.originalIdentity); + deleteDesiredDirectoryIfPresent( + pending, targetRoot, journal, directory.desiredHash, + directory.desiredMarkerHash, directory.desiredIdentity); + cleanupScratchParent(backup); + return; + } + + if (directory.hadTarget) { + if (Files.exists(backup.toPath(), LinkOption.NOFOLLOW_LINKS)) { + verifyDirectorySnapshot( + backup, targetRoot, directory.originalHash, + directory.originalMarkerHash, directory.originalIdentity, + "datapack install backup"); + deleteDesiredDirectoryIfPresent( + target, targetRoot, journal, directory.desiredHash, + directory.desiredMarkerHash, directory.desiredIdentity); + moveNew(backup.toPath(), target.toPath()); + forceDirectoryIfSupported(target.getParentFile().toPath()); + forceDirectoryIfSupported(backup.getParentFile().toPath()); + } else { + verifyDirectorySnapshot( + target, targetRoot, directory.originalHash, + directory.originalMarkerHash, directory.originalIdentity, + "original datapack target"); + } + } else { + if (Files.exists(backup.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Unexpected backup for newly-installed datapack " + backup.getPath()); + } + deleteDesiredDirectoryIfPresent( + target, targetRoot, journal, directory.desiredHash, + directory.desiredMarkerHash, directory.desiredIdentity); + } + deleteDesiredDirectoryIfPresent( + pending, targetRoot, journal, directory.desiredHash, + directory.desiredMarkerHash, directory.desiredIdentity); + cleanupScratchParent(backup); + } + + private static void resolveRemovalDirectory(CoordinatorDirectory directory, boolean commit) throws IOException { + File target = new File(directory.target); + File backup = new File(directory.backup); + File targetRoot = new File(directory.targetRoot); + if (commit) { + if (Files.exists(target.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Removed datapack target reappeared before transaction cleanup: " + target.getPath()); + } + deleteOriginalBackupIfPresent( + backup, targetRoot, directory.originalHash, + directory.originalMarkerHash, directory.originalIdentity); + cleanupScratchParent(backup); + return; + } + if (Files.exists(backup.toPath(), LinkOption.NOFOLLOW_LINKS)) { + verifyDirectorySnapshot( + backup, targetRoot, directory.originalHash, + directory.originalMarkerHash, directory.originalIdentity, + "datapack removal backup"); + if (Files.exists(target.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Datapack removal target was concurrently recreated: " + target.getPath()); + } + moveNew(backup.toPath(), target.toPath()); + forceDirectoryIfSupported(target.getParentFile().toPath()); + forceDirectoryIfSupported(backup.getParentFile().toPath()); + } else { + verifyDirectorySnapshot( + target, targetRoot, directory.originalHash, + directory.originalMarkerHash, directory.originalIdentity, + "original datapack target"); + } + cleanupScratchParent(backup); + } + + private static void resolveCoordinatorEditables( + CoordinatorJournal journal, + Path transactionRoot, + boolean commit + ) throws IOException { + IOException failure = null; + for (CoordinatorEditable editable : journal.editables) { + try { + Path packRoot = coordinatorPath(editable.packRoot, "editable pack root"); + StructureTransactionWriter writer = new StructureTransactionWriter(packRoot); + writer.resolvePreparedRemoval( + new StructureTransactionWriter.PreparedRemovalToken( + packRoot, + UUID.fromString(editable.transactionId) + ), + new StructureTransactionWriter.RecoveryOwner( + transactionRoot, + UUID.fromString(journal.transactionId), + UUID.fromString(editable.claimId) + ), + commit + ); + IrisData.invalidateLoadedStructureResources(packRoot.toFile()); + } catch (IOException | RuntimeException e) { + IOException participantFailure = e instanceof IOException ioFailure + ? ioFailure : new IOException("Failed resolving editable structure participant", e); + if (failure == null) { + failure = participantFailure; + } else { + failure.addSuppressed(participantFailure); + } + } + } + if (failure != null) { + throw failure; + } + } + + private static void verifyDesiredDirectory( + File directory, + File targetRoot, + CoordinatorJournal journal, + String expectedHash, + String expectedMarkerHash, + String expectedIdentity + ) throws IOException { + verifyDirectorySnapshot( + directory, targetRoot, expectedHash, expectedMarkerHash, + expectedIdentity, "installed datapack target"); + Ownership ownership = readOwnership(directory); + if (!Objects.equals(ownership.id, journal.id) || !Objects.equals(ownership.url, journal.url) + || !Objects.equals(ownership.contentHash, expectedHash)) { + throw new IOException("Installed datapack ownership does not match its transaction at " + directory.getPath()); + } + } + + private static void deleteDesiredDirectoryIfPresent( + File directory, + File targetRoot, + CoordinatorJournal journal, + String expectedHash, + String expectedMarkerHash, + String expectedIdentity + ) throws IOException { + if (!Files.exists(directory.toPath(), LinkOption.NOFOLLOW_LINKS)) { + return; + } + verifyDesiredDirectory( + directory, targetRoot, journal, expectedHash, + expectedMarkerHash, expectedIdentity); + deleteVerifiedDirectory(directory); + } + + private static void deleteOriginalBackupIfPresent( + File backup, + File targetRoot, + String expectedHash, + String expectedMarkerHash, + String expectedIdentity + ) throws IOException { + if (!Files.exists(backup.toPath(), LinkOption.NOFOLLOW_LINKS)) { + return; + } + verifyDirectorySnapshot( + backup, targetRoot, expectedHash, expectedMarkerHash, + expectedIdentity, "datapack transaction backup"); + deleteVerifiedDirectory(backup); + } + + private static void deleteVerifiedDirectory(File directory) throws IOException { + File parent = Objects.requireNonNull(directory.getParentFile(), "datapack transaction parent"); + validateInstallTree(directory, parent, "Datapack transaction directory"); + IO.delete(directory); + if (Files.exists(directory.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Could not delete datapack transaction directory " + directory.getPath()); + } + } + + private static void cleanupScratchParent(File participant) { + File parent = participant.getParentFile(); + if (parent != null) { + parent.delete(); + } + } + + private static void deleteCoordinatorTransaction(Path transactionRoot) throws IOException { + deleteInstallScratch(transactionRoot.toFile(), "completed datapack transaction"); + forceDirectoryIfSupported(Objects.requireNonNull(transactionRoot.getParent(), "transaction parent")); + } + + private static void writeCoordinatorJournal(Path transactionRoot, CoordinatorJournal journal) throws IOException { + byte[] content = GSON.toJson(journal).getBytes(StandardCharsets.UTF_8); + if (content.length > MAX_TRANSACTION_JOURNAL_BYTES) { + throw new IOException("Datapack transaction journal exceeds " + MAX_TRANSACTION_JOURNAL_BYTES + " bytes"); + } + Files.createDirectories(transactionRoot); + Path next = transactionRoot.resolve(TRANSACTION_JOURNAL_NEXT); + if (Files.isSymbolicLink(next)) { + throw new IOException("Datapack transaction journal cannot be a symbolic link: " + next); + } + Files.deleteIfExists(next); + Files.write(next, content, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); + forceFile(next); + move(next, transactionRoot.resolve(TRANSACTION_JOURNAL)); + forceDirectoryIfSupported(transactionRoot); + forceDirectoryIfSupported(Objects.requireNonNull(transactionRoot.getParent(), "transaction parent")); + } + + private static String readBoundedUtf8(Path path, long maxBytes, String purpose) throws IOException { + return new String(readBoundedBytes(path, maxBytes, purpose), StandardCharsets.UTF_8); + } + + private static byte[] readBoundedBytes(Path path, long maxBytes, String purpose) throws IOException { + int readLimit = Math.toIntExact(maxBytes + 1); + byte[] content; + try (InputStream input = Files.newInputStream( + path, + StandardOpenOption.READ, + LinkOption.NOFOLLOW_LINKS + )) { + content = input.readNBytes(readLimit); + } + if (content.length > maxBytes) { + throw new IOException(purpose + " exceeds " + maxBytes + " bytes"); + } + return content; + } + + private static Path coordinatorPath(String value, String purpose) throws IOException { + try { + return Path.of(value).toAbsolutePath().normalize(); + } catch (RuntimeException e) { + throw new IOException("Invalid datapack transaction " + purpose + " path", e); + } + } + + private static void forceFile(Path file) throws IOException { + try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) { + channel.force(true); + } + } + + private static void forceDirectoryIfSupported(Path directory) throws IOException { + if (!Files.getFileStore(directory).supportsFileAttributeView("posix")) { + return; + } + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + private enum CoordinatorOperation { + INSTALL, + REMOVE + } + + private enum StagingScratchKind { + PENDING, + BACKUP + } + + private enum CoordinatorPhase { + PREPARED, + PUBLISHING, + PUBLISHED, + COMMITTED; + + private boolean published() { + return this == PUBLISHED || this == COMMITTED; + } + } + + private static final class CoordinatorJournal { + private int schemaVersion; + private String transactionId; + private CoordinatorOperation operation; + private CoordinatorPhase phase; + private String id; + private String url; + private String versionId; + private String versionNumber; + private String sha1; + private boolean manifestAlreadyMatched; + private List directories = new ArrayList<>(); + private List editables = new ArrayList<>(); + } + + private static final class CoordinatorDirectory { + private String target; + private String pending; + private String backup; + private boolean hadTarget; + private String originalHash; + private String desiredHash; + private String originalMarkerHash; + private String desiredMarkerHash; + private String originalIdentity; + private String desiredIdentity; + private String targetRoot; + private String scratchRoot; + private String targetRootIdentity; + private String scratchRootIdentity; + } + + private static final class CoordinatorEditable { + private String packRoot; + private String transactionId; + private String claimId; + } + + private static final class DatapackCoordinator { + private final Path transactionRoot; + private final CoordinatorJournal journal; + + private DatapackCoordinator(Path transactionRoot, CoordinatorJournal journal) { + this.transactionRoot = transactionRoot; + this.journal = journal; + } + + private void phase(CoordinatorPhase phase) throws IOException { + journal.phase = phase; + writeCoordinatorJournal(transactionRoot, journal); + } + + private void finish() throws IOException { + deleteCoordinatorTransaction(transactionRoot); + } + } + + static final class VerifiedStagingInstall { + private final Path normalizedRoot; + private final Path realRoot; + private final String rootIdentity; + private final Path normalizedStagingRoot; + private final Path realStagingRoot; + private final String stagingRootIdentity; + private final Path normalizedSource; + private final Path realSource; + private final String sourceIdentity; + private final String id; + private final String url; + private final String versionId; + private final String versionNumber; + private final String sha1; + private final String desiredHash; + private final Entry committedEntrySnapshot; + private final boolean legacyReplacementAuthorized; + private final LegacyStagingSnapshot legacyStagingSnapshot; + private boolean consumed; + + private VerifiedStagingInstall( + Path normalizedRoot, + Path normalizedStagingRoot, + Path normalizedSource, + Entry entry, + String desiredHash, + Entry committedEntrySnapshot, + boolean legacyReplacementAuthorized, + LegacyStagingSnapshot legacyStagingSnapshot + ) throws IOException { + this.normalizedRoot = normalizedRoot; + this.realRoot = normalizedRoot.toRealPath(); + this.rootIdentity = directoryIdentity(normalizedRoot.toFile()); + this.normalizedStagingRoot = normalizedStagingRoot; + this.realStagingRoot = normalizedStagingRoot.toRealPath(); + this.stagingRootIdentity = directoryIdentity(normalizedStagingRoot.toFile()); + this.normalizedSource = normalizedSource; + this.realSource = normalizedSource.toRealPath(); + this.sourceIdentity = directoryIdentity(normalizedSource.toFile()); + this.id = entry.id; + this.url = entry.url; + this.versionId = entry.versionId; + this.versionNumber = entry.versionNumber; + this.sha1 = entry.sha1; + this.desiredHash = desiredHash; + this.committedEntrySnapshot = committedEntrySnapshot == null ? null : copyEntry(committedEntrySnapshot); + this.legacyReplacementAuthorized = legacyReplacementAuthorized; + this.legacyStagingSnapshot = legacyStagingSnapshot; + } + + private Path stagingRoot() { + return normalizedStagingRoot; + } + + private boolean hasStablePathIdentities() { + return !rootIdentity.isEmpty() + && !stagingRootIdentity.isEmpty() + && !sourceIdentity.isEmpty(); + } + + private boolean isCanonicalInstall(File installRoot, File target) { + Path suppliedRoot = installRoot.toPath().toAbsolutePath().normalize(); + Path suppliedTarget = target.toPath().toAbsolutePath().normalize(); + return suppliedRoot.equals(normalizedStagingRoot) + && suppliedTarget.equals(normalizedStagingRoot.resolve(id).normalize()); + } + + private void verifyStagingRoot() throws IOException { + verifyPathIdentity(normalizedRoot, realRoot, rootIdentity, "datapack storage root"); + verifyPathIdentity( + normalizedStagingRoot, realStagingRoot, stagingRootIdentity, "datapack staging root"); + if (!Objects.equals(normalizedStagingRoot.getParent(), normalizedRoot) + || !Files.isSameFile(normalizedStagingRoot.getParent(), normalizedRoot)) { + throw new IOException("Verified datapack staging root changed identity"); + } + } + + private boolean consume( + File source, + File installRoot, + File target, + Entry entry, + String stagedHash + ) throws IOException { + if (consumed) { + throw new IOException("Verified datapack staging authorization was already consumed"); + } + consumed = true; + Path suppliedRoot = installRoot.toPath().toAbsolutePath().normalize(); + Path suppliedTarget = target.toPath().toAbsolutePath().normalize(); + if (!suppliedRoot.equals(normalizedStagingRoot) + || !suppliedTarget.equals(normalizedStagingRoot.resolve(id).normalize())) { + throw new IOException("Verified datapack staging authorization was paired with a different path"); + } + verifyAuthority(source, entry, stagedHash); + return legacyReplacementAuthorized; + } + + private boolean authorizeLegacyWorldReplacement( + File source, + File installRoot, + File target, + Entry entry, + String stagedHash, + String currentHash, + String currentMarkerHash + ) throws IOException { + if (legacyStagingSnapshot == null + || !"absent".equals(currentMarkerHash) + || !Objects.equals(currentHash, legacyStagingSnapshot.contentHash()) + || legacyStagingSnapshot.targetIdentity().isEmpty()) { + return false; + } + Path suppliedRoot = installRoot.toPath().toAbsolutePath().normalize(); + Path suppliedTarget = target.toPath().toAbsolutePath().normalize(); + if (!Objects.equals(suppliedTarget.getParent(), suppliedRoot) + || !Objects.equals(suppliedTarget.getFileName().toString(), id) + || Files.isSameFile(suppliedRoot, normalizedStagingRoot) + || Files.isSameFile(suppliedTarget, legacyStagingSnapshot.normalizedTarget())) { + return false; + } + verifyAuthority(source, entry, stagedHash); + verifyLegacyWorldSnapshot(); + return true; + } + + private void verifyAuthority(File source, Entry entry, String stagedHash) throws IOException { + verifyAuthorityState(); + Path suppliedSource = source.toPath().toAbsolutePath().normalize(); + if (!suppliedSource.equals(normalizedSource)) { + throw new IOException("Verified datapack staging authorization was paired with a different path"); + } + if (!Objects.equals(entry.id, id) + || !Objects.equals(entry.url, url) + || !Objects.equals(entry.versionId, versionId) + || !Objects.equals(entry.versionNumber, versionNumber) + || !Objects.equals(entry.sha1, sha1) + || !Objects.equals(stagedHash, desiredHash)) { + throw new IOException("Verified datapack staging authorization was paired with different metadata"); + } + } + + private void verifyAuthorityState() throws IOException { + verifyStagingRoot(); + verifyPathIdentity(normalizedSource, realSource, sourceIdentity, "verified datapack extraction"); + Manifest committedManifest = readCommittedManifest(normalizedRoot.toFile()); + Entry committed = committedManifest.findById(id); + if (!manifestEntrySnapshotMatches(committed, committedEntrySnapshot)) { + throw new IOException("Committed datapack staging authority changed before installation"); + } + if (committedEntrySnapshot != null && !legacyReplacementAuthorized) { + throw new IOException("Committed datapack staging authority conflicts with " + id); + } + } + + private static boolean manifestEntrySnapshotMatches(Entry current, Entry expected) { + if (current == null || expected == null) { + return current == expected; + } + return Objects.equals(current.id, expected.id) + && Objects.equals(current.url, expected.url) + && Objects.equals(current.versionId, expected.versionId) + && Objects.equals(current.versionNumber, expected.versionNumber) + && Objects.equals(current.sha1, expected.sha1) + && Objects.equals(current.filename, expected.filename) + && Objects.equals(current.etag, expected.etag) + && Objects.equals(current.lastModified, expected.lastModified) + && current.installedEpoch == expected.installedEpoch + && current.structuresImported == expected.structuresImported + && copyList(current.structureKeys).equals(copyList(expected.structureKeys)) + && copyList(current.templateKeys).equals(copyList(expected.templateKeys)) + && Objects.equals(current.importedTargets, expected.importedTargets) + && Objects.equals(current.importAttempts, expected.importAttempts) + && Objects.equals(current.importedBundles, expected.importedBundles); + } + + private void verifyLegacyWorldSnapshot() throws IOException { + verifyAuthorityState(); + if (legacyStagingSnapshot == null || legacyStagingSnapshot.targetIdentity().isEmpty()) { + throw new IOException("Missing stable canonical legacy datapack staging snapshot for " + id); + } + if (!Objects.equals(legacyStagingSnapshot.normalizedTarget().getParent(), normalizedStagingRoot) + || !Files.isSameFile( + legacyStagingSnapshot.normalizedTarget().getParent(), normalizedStagingRoot) + || !sameScratchVolume( + legacyStagingSnapshot.normalizedTarget(), normalizedStagingRoot)) { + throw new IOException("Changed or unsafe canonical legacy datapack staging target for " + id); + } + verifyDirectorySnapshot( + legacyStagingSnapshot.normalizedTarget().toFile(), + normalizedStagingRoot.toFile(), + legacyStagingSnapshot.contentHash(), + legacyStagingSnapshot.markerHash(), + legacyStagingSnapshot.targetIdentity(), + "canonical legacy datapack staging target" + ); + if (!Objects.equals( + legacyStagingSnapshot.normalizedTarget().toRealPath(), + legacyStagingSnapshot.realTarget())) { + throw new IOException("Changed canonical legacy datapack staging target identity for " + id); + } + } + + private static void verifyPathIdentity( + Path normalized, + Path expectedReal, + String expectedIdentity, + String purpose + ) throws IOException { + if (Files.isSymbolicLink(normalized) + || !Files.isDirectory(normalized, LinkOption.NOFOLLOW_LINKS) + || !Objects.equals(normalized.toRealPath(), expectedReal) + || (!expectedIdentity.isEmpty() + && !Objects.equals(directoryIdentity(normalized.toFile()), expectedIdentity))) { + throw new IOException("Changed or unsafe " + purpose + " " + normalized); + } + } + } + + public static final class Report { + private final KList updated = new KList<>(); + private final KList upToDate = new KList<>(); + private final KList failed = new KList<>(); + private boolean requiresRestart; + + public boolean changed() { + return requiresRestart; + } + + public KList getUpdated() { + return updated; + } + + public KList getUpToDate() { + return upToDate; + } + + public KList getFailed() { + return failed; + } + } + + public static final class Entry { + public String url; + public String id; + public String versionId; + public String versionNumber; + public String sha1; + public String filename; + public String etag; + public String lastModified; + public long installedEpoch; + public boolean structuresImported; + public String stagingMetadata = ""; + public List structureKeys = new ArrayList<>(); + public List templateKeys = new ArrayList<>(); + public Map installMetadata = new HashMap<>(); + public Map importedTargets = new HashMap<>(); + public Map importAttempts = new HashMap<>(); + public Map> importedBundles = new HashMap<>(); + } + + static final class StartupValidationCache { + int schemaVersion; + String minecraftVersion; + int irisVersion; + boolean autoIngest; + boolean stripOverrides; + List urls; + String localFingerprint; + } + + public enum StartupValidationOutcome { + READY, + RESTART_REQUIRED, + FAILED + } + + private static final class Manifest { + private List entries = new ArrayList<>(); + + private Entry find(String url) { + for (Entry entry : entries) { + if (entry.url != null && entry.url.equals(url)) { + return entry; + } + } + return null; + } + + private Entry findById(String id) { + for (Entry entry : entries) { + if (entry.id != null && entry.id.equals(id)) { + return entry; + } + } + return null; + } + + private void put(Entry entry) { + for (int i = 0; i < entries.size(); i++) { + Entry current = entries.get(i); + if (current.url != null && current.url.equals(entry.url)) { + entries.set(i, entry); + return; + } + } + entries.add(entry); + } + + private boolean removeById(String id) { + return entries.removeIf(entry -> id.equals(entry.id)); + } + } + + static record DownloadResult(boolean notModified, String etag, String lastModified) { + } + + static record InstallResult(boolean changed) { + } + + public record ReapplyOutcome( + ReapplyStatus status, + Optional failure + ) { + public ReapplyOutcome { + status = Objects.requireNonNull(status, "External datapack reapply status"); + failure = Objects.requireNonNull(failure, "External datapack reapply failure"); + if (status == ReapplyStatus.FAILED && failure.isEmpty()) { + throw new IllegalArgumentException("Failed external datapack reapply requires a cause"); + } + if (status != ReapplyStatus.FAILED && failure.isPresent()) { + throw new IllegalArgumentException("Successful external datapack reapply cannot carry a cause"); + } + } + + public static ReapplyOutcome success(boolean recovered, boolean repaired) { + ReapplyStatus status; + if (recovered && repaired) { + status = ReapplyStatus.RECOVERED_AND_REPAIRED; + } else if (recovered) { + status = ReapplyStatus.RECOVERED; + } else if (repaired) { + status = ReapplyStatus.REPAIRED; + } else { + status = ReapplyStatus.UNCHANGED; + } + return new ReapplyOutcome(status, Optional.empty()); + } + + public static ReapplyOutcome failed(Throwable failure) { + return new ReapplyOutcome( + ReapplyStatus.FAILED, + Optional.of(Objects.requireNonNull(failure, "External datapack reapply failure cause"))); + } + + public boolean succeeded() { + return status != ReapplyStatus.FAILED; + } + + public boolean changed() { + return recovered() || repaired(); + } + + public boolean recovered() { + return status == ReapplyStatus.RECOVERED + || status == ReapplyStatus.RECOVERED_AND_REPAIRED; + } + + public boolean repaired() { + return status == ReapplyStatus.REPAIRED + || status == ReapplyStatus.RECOVERED_AND_REPAIRED; + } + } + + public enum ReapplyStatus { + UNCHANGED, + RECOVERED, + REPAIRED, + RECOVERED_AND_REPAIRED, + FAILED + } + + static final class InstallExecution { + private final InstallResult result; + private final DatapackCoordinator coordinator; + private final File stagedDir; + private final Entry entry; + private final String stagedHash; + private final boolean verifyStagedSource; + private final List publishedPlans; + private final List unchangedPlans; + private boolean verified; + + private InstallExecution( + InstallResult result, + DatapackCoordinator coordinator, + File stagedDir, + Entry entry, + String stagedHash, + boolean verifyStagedSource, + List publishedPlans, + List unchangedPlans + ) { + this.result = result; + this.coordinator = coordinator; + this.stagedDir = stagedDir; + this.entry = copyEntry(entry); + this.stagedHash = stagedHash; + this.verifyStagedSource = verifyStagedSource; + this.publishedPlans = List.copyOf(publishedPlans); + this.unchangedPlans = List.copyOf(unchangedPlans); + } + + InstallResult result() { + return result; + } + + private DatapackCoordinator coordinator() { + return coordinator; + } + + private File stagedDir() { + return stagedDir; + } + + private Entry entry() { + return entry; + } + + private String stagedHash() { + return stagedHash; + } + + private boolean verifyStagedSource() { + return verifyStagedSource; + } + + private List publishedPlans() { + return publishedPlans; + } + + private List unchangedPlans() { + return unchangedPlans; + } + + private boolean verified() { + return verified; + } + + private void markVerified() { + verified = true; + } + } + + private record PackResources( + List structureKeys, + List structureSetKeys, + List templateKeys + ) { + } + + private record StagingInspection( + boolean usable, + boolean ownershipCorrected, + boolean manifestChanged + ) { + } + + public record StructureScopeResources( + String source, + List structureKeys, + List structureSetKeys + ) { + } + + private static final class EditableImportRemoval { + private final List prepared; + private boolean closed; + + private EditableImportRemoval(List prepared) { + this.prepared = List.copyOf(prepared); + } + + private List recoveryTokens() { + List tokens = new ArrayList<>(); + for (PreparedEditableImport editableImport : prepared) { + editableImport.removal().recoveryToken().ifPresent(tokens::add); + } + return List.copyOf(tokens); + } + + private void claimRecoveryOwners(Path transactionRoot, CoordinatorJournal journal) throws IOException { + int ownerIndex = 0; + for (PreparedEditableImport editableImport : prepared) { + Optional token = + editableImport.removal().recoveryToken(); + if (token.isEmpty()) { + continue; + } + if (ownerIndex >= journal.editables.size()) { + throw new IOException("Missing datapack coordinator claim for editable structure removal"); + } + CoordinatorEditable editable = journal.editables.get(ownerIndex++); + StructureTransactionWriter.PreparedRemovalToken recoveryToken = token.get(); + if (!Objects.equals(recoveryToken.packRoot().toString(), editable.packRoot) + || !Objects.equals(recoveryToken.transactionId().toString(), editable.transactionId)) { + throw new IOException("Datapack coordinator editable claim order changed during preparation"); + } + editableImport.removal().claimRecoveryOwner( + new StructureTransactionWriter.RecoveryOwner( + transactionRoot, + UUID.fromString(journal.transactionId), + UUID.fromString(editable.claimId) + ) + ); + } + if (ownerIndex != journal.editables.size()) { + throw new IOException("Unexpected datapack coordinator editable recovery claim"); + } + } + + private void markCommitted() throws IOException { + if (closed) { + throw new IllegalStateException("Editable import removal transaction is closed"); + } + for (PreparedEditableImport editableImport : prepared) { + editableImport.removal().markCommitted(); + } + } + + private void finishCommit() throws IOException { + if (closed) { + return; + } + IOException failure = null; + for (PreparedEditableImport editableImport : prepared) { + try { + editableImport.removal().finishCommit(); + } catch (IOException | RuntimeException cleanupFailure) { + IOException transactionFailure = cleanupFailure instanceof IOException ioFailure + ? ioFailure + : new IOException("Failed finalizing editable import removal", cleanupFailure); + if (failure == null) { + failure = transactionFailure; + } else { + failure.addSuppressed(transactionFailure); + } + } + if (editableImport.removal().changed()) { + try { + IrisData.getLoaded(editableImport.dataFolder()) + .ifPresent(IrisData::invalidateStructureResources); + } catch (RuntimeException invalidationFailure) { + IOException transactionFailure = new IOException( + "Failed invalidating editable import structure resources", + invalidationFailure + ); + if (failure == null) { + failure = transactionFailure; + } else { + failure.addSuppressed(transactionFailure); + } + } + } + } + closed = true; + if (failure != null) { + throw failure; + } + } + + private void rollback() throws IOException { + if (closed) { + return; + } + IOException failure = null; + for (int i = prepared.size() - 1; i >= 0; i--) { + try { + prepared.get(i).removal().rollback(); + } catch (IOException rollbackFailure) { + if (failure == null) { + failure = rollbackFailure; + } else { + failure.addSuppressed(rollbackFailure); + } + } + } + closed = true; + if (failure != null) { + throw failure; + } + } + + private void leaveForRecovery() throws IOException { + if (closed) { + return; + } + IOException failure = null; + for (PreparedEditableImport editableImport : prepared) { + try { + editableImport.removal().leaveForRecovery(); + } catch (IOException releaseFailure) { + if (failure == null) { + failure = releaseFailure; + } else { + failure.addSuppressed(releaseFailure); + } + } + } + closed = true; + if (failure != null) { + throw failure; + } + } + } + + private static final class DirectoryRemoval { + private final List moved; + private int prepared; + private boolean closed; + + private DirectoryRemoval(List moved) { + this.moved = List.copyOf(moved); + } + + private List moves() { + return moved; + } + + private void prepare() throws IOException { + if (closed || prepared > 0) { + throw new IllegalStateException("Datapack directory removal transaction is not new"); + } + try { + for (DirectoryMove directoryMove : moved) { + verifyDirectoryContainerIdentity( + directoryMove.target().getParentFile(), + directoryMove.targetRootIdentity(), + directoryMove.targetRootFileIdentity(), + "datapack removal target root" + ); + verifyDirectoryContainerIdentity( + directoryMove.backup().getParentFile(), + directoryMove.scratchRootIdentity(), + directoryMove.scratchRootFileIdentity(), + "datapack removal scratch root" + ); + verifyDirectorySnapshot( + directoryMove.target(), + new File(directoryMove.targetRootIdentity()), + directoryMove.originalHash(), + directoryMove.originalMarkerHash(), + directoryMove.originalIdentity(), + "datapack removal target" + ); + moveNew(directoryMove.target().toPath(), directoryMove.backup().toPath()); + prepared++; + forceDirectoryIfSupported(directoryMove.target().getParentFile().toPath()); + forceDirectoryIfSupported(directoryMove.backup().getParentFile().toPath()); + verifyDirectorySnapshot( + directoryMove.backup(), + new File(directoryMove.targetRootIdentity()), + directoryMove.originalHash(), + directoryMove.originalMarkerHash(), + directoryMove.originalIdentity(), + "datapack removal backup" + ); + } + } catch (IOException removalFailure) { + try { + rollback(); + } catch (IOException restoreFailure) { + removalFailure.addSuppressed(restoreFailure); + } + throw removalFailure; + } + } + + private void rollback() throws IOException { + if (closed) { + return; + } + IOException failure = null; + for (int i = prepared - 1; i >= 0; i--) { + DirectoryMove directoryMove = moved.get(i); + if (!Files.exists(directoryMove.backup().toPath(), LinkOption.NOFOLLOW_LINKS)) { + continue; + } + try { + verifyDirectoryContainerIdentity( + directoryMove.target().getParentFile(), + directoryMove.targetRootIdentity(), + directoryMove.targetRootFileIdentity(), + "datapack removal target root" + ); + verifyDirectoryContainerIdentity( + directoryMove.backup().getParentFile(), + directoryMove.scratchRootIdentity(), + directoryMove.scratchRootFileIdentity(), + "datapack removal scratch root" + ); + verifyDirectorySnapshot( + directoryMove.backup(), + new File(directoryMove.targetRootIdentity()), + directoryMove.originalHash(), + directoryMove.originalMarkerHash(), + directoryMove.originalIdentity(), + "datapack removal backup" + ); + if (pathExists(directoryMove.target().toPath(), "datapack removal target")) { + throw new IOException("Datapack removal target was concurrently recreated at " + + directoryMove.target().getPath()); + } + DatapackIngestService.moveNew( + directoryMove.backup().toPath(), + directoryMove.target().toPath() + ); + forceDirectoryIfSupported(directoryMove.target().getParentFile().toPath()); + forceDirectoryIfSupported(directoryMove.backup().getParentFile().toPath()); + } catch (IOException restoreFailure) { + if (failure == null) { + failure = restoreFailure; + } else { + failure.addSuppressed(restoreFailure); + } + } + } + for (int i = 0; i < prepared; i++) { + DirectoryMove directoryMove = moved.get(i); + directoryMove.backup().getParentFile().delete(); + } + closed = true; + if (failure != null) { + throw failure; + } + } + + private void finishCommit() throws IOException { + if (closed) { + return; + } + IOException failure = null; + for (int i = 0; i < prepared; i++) { + DirectoryMove directoryMove = moved.get(i); + try { + verifyDirectoryContainerIdentity( + directoryMove.backup().getParentFile(), + directoryMove.scratchRootIdentity(), + directoryMove.scratchRootFileIdentity(), + "datapack removal scratch root" + ); + deleteOriginalBackupIfPresent( + directoryMove.backup(), + new File(directoryMove.targetRootIdentity()), + directoryMove.originalHash(), + directoryMove.originalMarkerHash(), + directoryMove.originalIdentity() + ); + } catch (IOException cleanupFailure) { + failure = appendIOException(failure, cleanupFailure); + } + directoryMove.backup().getParentFile().delete(); + } + closed = true; + if (failure != null) { + throw failure; + } + } + } + + private static final class ManifestWrite implements AutoCloseable { + private final Path staged; + private final Path target; + private final Path rollback; + private boolean published; + + private ManifestWrite(Path staged, Path target, Path rollback) { + this.staged = staged; + this.target = target; + this.rollback = rollback; + } + + private void publish() throws IOException { + if (published) { + throw new IllegalStateException("Datapack manifest write was already published"); + } + try { + move(staged, target); + published = true; + } catch (IOException publishFailure) { + try { + restoreOriginal(); + } catch (IOException restoreFailure) { + publishFailure.addSuppressed(restoreFailure); + } + throw publishFailure; + } + forceDirectoryIfSupported(Objects.requireNonNull(target.getParent(), "manifest parent")); + } + + private boolean published() { + return published; + } + + private void discard() throws IOException { + IOException failure = null; + try { + Files.deleteIfExists(staged); + } catch (IOException cleanupFailure) { + failure = cleanupFailure; + } + if (rollback != null) { + try { + Files.deleteIfExists(rollback); + } catch (IOException cleanupFailure) { + if (failure == null) { + failure = cleanupFailure; + } else { + failure.addSuppressed(cleanupFailure); + } + } + } + if (failure != null) { + throw failure; + } + } + + @Override + public void close() throws IOException { + discard(); + } + + private void restoreOriginal() throws IOException { + if (rollback == null) { + Files.deleteIfExists(target); + } else { + move(rollback, target); + } + forceDirectoryIfSupported(Objects.requireNonNull(target.getParent(), "manifest parent")); + } + } + + private record PreparedEditableImport( + File dataFolder, + StructureTransactionWriter.PreparedRemoval removal + ) { + } + + private record MetadataEntry(Path path, String relativePath) { + } + + private record DirectoryMove( + File target, + File backup, + String originalHash, + String originalMarkerHash, + String originalIdentity, + String targetRootIdentity, + String scratchRootIdentity, + String targetRootFileIdentity, + String scratchRootFileIdentity + ) { + } + + record InstallPlan( + File target, + File pending, + File backup, + File pendingRoot, + boolean hadTarget, + boolean publishRequired, + boolean contentChanged, + String originalHash, + String desiredHash, + String originalMarkerHash, + String desiredMarkerHash, + String originalIdentity, + String desiredIdentity, + String targetRootIdentity, + String scratchRootIdentity, + String targetRootFileIdentity, + String scratchRootFileIdentity, + String id, + String url, + VerifiedStagingInstall legacyWorldAuthorization + ) { + } + + private record LegacyStagingSnapshot( + Path normalizedTarget, + Path realTarget, + String targetIdentity, + String contentHash, + String markerHash + ) { + } + + private record StagingScratch(StagingScratchKind kind, String id, Path path) { + } + + @FunctionalInterface + interface DirectoryDeleter { + void delete(File directory); + } + + private static final class Ownership { + private final int schemaVersion; + private final String id; + private final String url; + private final String versionId; + private final String versionNumber; + private final String sha1; + private final String contentHash; + private final List structureKeys; + private final List templateKeys; + + private Ownership( + int schemaVersion, + String id, + String url, + String versionId, + String versionNumber, + String sha1, + String contentHash, + List structureKeys, + List templateKeys + ) { + this.schemaVersion = schemaVersion; + this.id = id; + this.url = url; + this.versionId = versionId; + this.versionNumber = versionNumber; + this.sha1 = sha1; + this.contentHash = contentHash; + this.structureKeys = List.copyOf(structureKeys); + this.templateKeys = List.copyOf(templateKeys); + } + + private Entry toEntry() { + Entry entry = new Entry(); + entry.id = id; + entry.url = url; + entry.versionId = versionId; + entry.versionNumber = versionNumber; + entry.sha1 = sha1; + entry.structureKeys = normalizeKeys(structureKeys); + entry.templateKeys = normalizeKeys(templateKeys); + return entry; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/datapack/DatapackStructureScopeIndex.java b/core/src/main/java/art/arcane/iris/core/datapack/DatapackStructureScopeIndex.java new file mode 100644 index 000000000..43a3b379d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/datapack/DatapackStructureScopeIndex.java @@ -0,0 +1,131 @@ +package art.arcane.iris.core.datapack; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +public final class DatapackStructureScopeIndex { + private final Map> sourcesByStructure; + private final Map> sourcesByStructureSet; + + private DatapackStructureScopeIndex( + Map> sourcesByStructure, + Map> sourcesByStructureSet + ) { + this.sourcesByStructure = sourcesByStructure; + this.sourcesByStructureSet = sourcesByStructureSet; + } + + public static DatapackStructureScopeIndex create( + List resources + ) { + Map> mutableSourcesByStructure = new HashMap<>(); + Map> mutableSourcesBySet = new HashMap<>(); + if (resources != null) { + for (DatapackIngestService.StructureScopeResources resource : resources) { + if (resource == null) { + continue; + } + String source = normalizeSource(resource.source()); + if (source.isEmpty()) { + continue; + } + addOwnership(mutableSourcesByStructure, resource.structureKeys(), source); + addOwnership(mutableSourcesBySet, resource.structureSetKeys(), source); + } + } + + return new DatapackStructureScopeIndex( + immutableOwnership(mutableSourcesByStructure), + immutableOwnership(mutableSourcesBySet)); + } + + public Set declaredSources(Iterable datapackImports) { + if (datapackImports == null) { + return Set.of(); + } + Set declared = new HashSet<>(); + for (String source : datapackImports) { + String normalized = normalizeSource(source); + if (!normalized.isEmpty()) { + declared.add(normalized); + } + } + return declared.isEmpty() ? Set.of() : Set.copyOf(declared); + } + + public boolean allowsStructureSet(String structureSetKey, Set declaredSources) { + return allows(sourcesByStructureSet, structureSetKey, declaredSources); + } + + public boolean allowsStructure(String structureKey, Set declaredSources) { + return allows(sourcesByStructure, structureKey, declaredSources); + } + + public boolean isManagedStructureSet(String structureSetKey) { + return sourcesByStructureSet.containsKey(normalizeKey(structureSetKey)); + } + + public boolean isManagedStructure(String structureKey) { + return sourcesByStructure.containsKey(normalizeKey(structureKey)); + } + + public int managedStructureCount() { + return sourcesByStructure.size(); + } + + public int managedStructureSetCount() { + return sourcesByStructureSet.size(); + } + + public boolean isEmpty() { + return sourcesByStructure.isEmpty() && sourcesByStructureSet.isEmpty(); + } + + private static void addOwnership( + Map> ownership, + List keys, + String source + ) { + if (keys == null) { + return; + } + for (String key : keys) { + String normalizedKey = normalizeKey(key); + if (!normalizedKey.isEmpty()) { + ownership.computeIfAbsent(normalizedKey, ignored -> new HashSet<>()).add(source); + } + } + } + + private static Map> immutableOwnership(Map> ownership) { + Map> immutable = new HashMap<>(ownership.size()); + for (Map.Entry> entry : ownership.entrySet()) { + immutable.put(entry.getKey(), Set.copyOf(entry.getValue())); + } + return Map.copyOf(immutable); + } + + private static boolean allows( + Map> ownership, + String key, + Set declaredSources + ) { + Set owners = ownership.get(normalizeKey(key)); + if (owners == null) { + return true; + } + return declaredSources != null && declaredSources.containsAll(owners); + } + + private static String normalizeSource(String source) { + return source == null ? "" : source.trim(); + } + + private static String normalizeKey(String key) { + return key == null ? "" : key.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/datapack/ModrinthResolver.java b/core/src/main/java/art/arcane/iris/core/datapack/ModrinthResolver.java new file mode 100644 index 000000000..55c102f4e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/datapack/ModrinthResolver.java @@ -0,0 +1,459 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.datapack; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public final class ModrinthResolver { + private static final String API = "https://api.modrinth.com/v2"; + private static final String USER_AGENT = "VolmitSoftware/Iris (datapack-ingest)"; + private static final String DATAPACK_LOADER = "datapack"; + private static final int MAX_API_RESPONSE_CHARS = 8 * 1024 * 1024; + + private ModrinthResolver() { + } + + public static ResolvedDatapack resolve(String rawUrl, String serverMcVersion) throws IOException { + String url = rawUrl == null ? "" : rawUrl.trim(); + if (url.isEmpty()) { + throw new IOException("Empty datapack url"); + } + + ModrinthRef ref = parse(url); + if (ref == null) { + return directResolve(url); + } + + JsonArray versions = getJsonArray(API + "/project/" + ref.slug + "/version"); + if (versions == null || versions.isEmpty()) { + throw new IOException("Modrinth project '" + ref.slug + "' returned no versions"); + } + + JsonObject version = ref.versionToken == null + ? selectLatestDatapackVersion(versions, serverMcVersion) + : selectVersionByToken(versions, ref.versionToken); + if (version == null) { + String detail = ref.versionToken == null + ? (serverMcVersion == null || serverMcVersion.isBlank() + ? "no datapack-loader version" + : "no datapack-loader version compatible with Minecraft " + serverMcVersion) + : "no version matching '" + ref.versionToken + "'"; + throw new IOException("Modrinth project '" + ref.slug + "' has " + detail); + } + if (!isDatapack(version)) { + throw new IOException("Modrinth version for '" + ref.slug + "' is not published for the datapack loader"); + } + if (serverMcVersion != null && !serverMcVersion.isBlank() && !gameVersionsContains(version, serverMcVersion)) { + throw new IOException("Modrinth version for '" + ref.slug + "' is not compatible with Minecraft " + serverMcVersion); + } + + JsonObject file = selectFile(version); + if (file == null) { + throw new IOException("Modrinth version for '" + ref.slug + "' has no downloadable file"); + } + + return toResolved(version, file, ref.slug); + } + + private static ModrinthRef parse(String url) { + URI uri; + try { + uri = new URI(url); + } catch (URISyntaxException e) { + return null; + } + String host = uri.getHost(); + if (host == null || !(host.equalsIgnoreCase("modrinth.com") || host.equalsIgnoreCase("www.modrinth.com"))) { + return null; + } + + List parts = new ArrayList<>(); + for (String segment : uri.getPath().split("/")) { + if (!segment.isBlank()) { + parts.add(segment); + } + } + if (parts.size() < 2) { + return null; + } + + String slug = parts.get(1); + String token = null; + for (int i = 2; i + 1 < parts.size(); i++) { + if (parts.get(i).equalsIgnoreCase("version")) { + token = parts.get(i + 1); + break; + } + } + return new ModrinthRef(slug, token); + } + + private static JsonObject selectVersionByToken(JsonArray versions, String token) { + JsonObject datapackMatch = null; + JsonObject anyMatch = null; + String normalizedToken = normalizeVersion(token); + + for (JsonElement element : versions) { + if (!element.isJsonObject()) { + continue; + } + JsonObject version = element.getAsJsonObject(); + String id = optString(version, "id"); + String number = optString(version, "version_number"); + boolean matches = token.equals(id) + || token.equalsIgnoreCase(number) + || normalizedToken.equals(normalizeVersion(number)); + if (!matches) { + continue; + } + if (anyMatch == null) { + anyMatch = version; + } + if (isDatapack(version)) { + datapackMatch = version; + break; + } + } + + return datapackMatch != null ? datapackMatch : anyMatch; + } + + static JsonObject selectLatestDatapackVersion(JsonArray versions, String serverMcVersion) { + for (JsonElement element : versions) { + if (!element.isJsonObject()) { + continue; + } + JsonObject version = element.getAsJsonObject(); + if (!isDatapack(version)) { + continue; + } + if (serverMcVersion == null || serverMcVersion.isBlank() || gameVersionsContains(version, serverMcVersion)) { + return version; + } + } + return null; + } + + static JsonObject selectFile(JsonObject version) { + JsonArray files = optArray(version, "files"); + if (files == null || files.isEmpty()) { + return null; + } + + JsonObject primaryZip = null; + JsonObject anyZip = null; + JsonObject primary = null; + JsonObject first = null; + for (JsonElement element : files) { + if (!element.isJsonObject()) { + continue; + } + JsonObject file = element.getAsJsonObject(); + if (first == null) { + first = file; + } + boolean isPrimary = optBoolean(file, "primary"); + boolean isZip = optString(file, "filename").toLowerCase(Locale.ROOT).endsWith(".zip"); + if (isPrimary && primary == null) { + primary = file; + } + if (isZip && anyZip == null) { + anyZip = file; + } + if (isPrimary && isZip && primaryZip == null) { + primaryZip = file; + } + } + + if (primaryZip != null) { + return primaryZip; + } + if (anyZip != null) { + return anyZip; + } + if (primary != null) { + return primary; + } + return first; + } + + private static ResolvedDatapack toResolved(JsonObject version, JsonObject file, String slug) throws IOException { + String downloadUrl = optString(file, "url"); + String filename = optString(file, "filename"); + if (downloadUrl.isBlank() || filename.isBlank()) { + throw new IOException("Modrinth version for '" + slug + "' has an invalid downloadable file"); + } + String sha1 = null; + if (file.has("hashes") && file.get("hashes").isJsonObject()) { + sha1 = optString(file.getAsJsonObject("hashes"), "sha1"); + if (!sha1.isBlank() && !sha1.matches("(?i)[0-9a-f]{40}")) { + throw new IOException("Modrinth version for '" + slug + "' has an invalid SHA-1 checksum"); + } + } + return new ResolvedDatapack(downloadUrl, filename, sha1, optString(version, "id"), optString(version, "version_number"), slug, false); + } + + private static ResolvedDatapack directResolve(String url) { + String filename = url; + int slash = filename.lastIndexOf('/'); + if (slash >= 0) { + filename = filename.substring(slash + 1); + } + int query = filename.indexOf('?'); + if (query >= 0) { + filename = filename.substring(0, query); + } + if (filename.isBlank()) { + filename = "datapack.zip"; + } + String identity = directIdentity(url); + return new ResolvedDatapack(url, filename, null, "direct-" + identity, "direct", null, true); + } + + static String directIdentity(String url) { + String normalized = url; + try { + URI parsed = new URI(url).normalize(); + normalized = new URI( + parsed.getScheme() == null ? null : parsed.getScheme().toLowerCase(Locale.ROOT), + parsed.getUserInfo(), + parsed.getHost() == null ? null : parsed.getHost().toLowerCase(Locale.ROOT), + parsed.getPort(), + parsed.getPath(), + parsed.getQuery(), + null + ).toASCIIString(); + } catch (URISyntaxException ignored) { + } + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(normalized.getBytes(StandardCharsets.UTF_8)); + StringBuilder builder = new StringBuilder(16); + for (int i = 0; i < 8; i++) { + builder.append(String.format("%02x", hash[i])); + } + return builder.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 algorithm unavailable", e); + } + } + + private static boolean isDatapack(JsonObject version) { + JsonArray loaders = optArray(version, "loaders"); + if (loaders == null) { + return false; + } + for (JsonElement loader : loaders) { + if (isString(loader) && DATAPACK_LOADER.equalsIgnoreCase(loader.getAsString())) { + return true; + } + } + return false; + } + + private static boolean gameVersionsContains(JsonObject version, String mc) { + JsonArray gameVersions = optArray(version, "game_versions"); + if (gameVersions == null) { + return false; + } + for (JsonElement gv : gameVersions) { + if (isString(gv) && mc.equalsIgnoreCase(gv.getAsString())) { + return true; + } + } + return false; + } + + private static String normalizeVersion(String value) { + if (value == null) { + return ""; + } + String normalized = value.trim().toLowerCase(Locale.ROOT); + if (normalized.startsWith("v")) { + normalized = normalized.substring(1); + } + return normalized; + } + + private static String optString(JsonObject object, String key) { + if (object == null || !object.has(key) || !isString(object.get(key))) { + return ""; + } + return object.get(key).getAsString(); + } + + private static JsonArray optArray(JsonObject object, String key) { + if (object == null || !object.has(key) || !object.get(key).isJsonArray()) { + return null; + } + return object.getAsJsonArray(key); + } + + private static boolean optBoolean(JsonObject object, String key) { + if (object == null || !object.has(key) || !object.get(key).isJsonPrimitive() + || !object.get(key).getAsJsonPrimitive().isBoolean()) { + return false; + } + return object.get(key).getAsBoolean(); + } + + private static boolean isString(JsonElement element) { + return element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString(); + } + + private static JsonArray getJsonArray(String url) throws IOException { + String body = httpGet(url); + try { + JsonElement parsed = JsonParser.parseString(body); + if (!parsed.isJsonArray()) { + throw new IOException("Unexpected response from " + url); + } + return parsed.getAsJsonArray(); + } catch (RuntimeException e) { + throw new IOException("Invalid JSON response from " + url, e); + } + } + + private static String httpGet(String url) throws IOException { + URL target = URI.create(url).toURL(); + HttpURLConnection connection = (HttpURLConnection) target.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setRequestProperty("Accept", "application/json"); + connection.setConnectTimeout(15000); + connection.setReadTimeout(20000); + connection.setInstanceFollowRedirects(true); + + // Everything past openConnection under one finally: a timeout/DNS/TLS throw inside + // getResponseCode abandoned the connection undisconnected. + try { + int code = connection.getResponseCode(); + if (code != 200) { + throw new IOException("HTTP " + code + " from " + url); + } + + try (InputStream input = connection.getInputStream(); + InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) { + return readBoundedApiResponse(reader, url, MAX_API_RESPONSE_CHARS); + } + } finally { + connection.disconnect(); + } + } + + static String readBoundedApiResponse(Reader reader, String source, int maxChars) throws IOException { + if (maxChars < 1) { + throw new IllegalArgumentException("Response character limit must be positive"); + } + int bufferSize = maxChars < 8192 ? maxChars + 1 : 8192; + char[] buffer = new char[bufferSize]; + StringBuilder builder = new StringBuilder(Math.min(maxChars, 8192)); + while (true) { + int remaining = maxChars - builder.length(); + int requested = remaining >= buffer.length ? buffer.length : remaining + 1; + int length = reader.read(buffer, 0, requested); + if (length == -1) { + return builder.toString(); + } + if (length == 0) { + continue; + } + if (length > remaining) { + throw new IOException("Oversized response from " + source); + } + builder.append(buffer, 0, length); + } + } + + private static final class ModrinthRef { + private final String slug; + private final String versionToken; + + private ModrinthRef(String slug, String versionToken) { + this.slug = slug; + this.versionToken = versionToken; + } + } + + public static final class ResolvedDatapack { + private final String downloadUrl; + private final String fileName; + private final String sha1; + private final String versionId; + private final String versionNumber; + private final String projectSlug; + private final boolean direct; + + public ResolvedDatapack(String downloadUrl, String fileName, String sha1, String versionId, String versionNumber, String projectSlug, boolean direct) { + this.downloadUrl = downloadUrl; + this.fileName = fileName; + this.sha1 = sha1; + this.versionId = versionId; + this.versionNumber = versionNumber; + this.projectSlug = projectSlug; + this.direct = direct; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public String getFileName() { + return fileName; + } + + public String getSha1() { + return sha1; + } + + public String getVersionId() { + return versionId; + } + + public String getVersionNumber() { + return versionNumber; + } + + public String getProjectSlug() { + return projectSlug; + } + + public boolean isDirect() { + return direct; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/edit/BlockSignal.java b/core/src/main/java/art/arcane/iris/core/edit/BlockSignal.java new file mode 100644 index 000000000..19c104f41 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/edit/BlockSignal.java @@ -0,0 +1,162 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.edit; + +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.SR; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.FallingBlock; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.concurrent.atomic.AtomicInteger; + +@SuppressWarnings("InstantiationOfUtilityClass") +public class BlockSignal { + public static final AtomicInteger active = new AtomicInteger(0); + + public BlockSignal(Block block, int ticks) { + // Count only after the entity actually exists, and release unconditionally: a refused + // Folia schedule or a spawn failure must not inflate the throttle counter forever. + Location tg = block.getLocation().clone().add(0.5, 0, 0.5); + FallingBlock e; + try { + e = block.getWorld().spawnFallingBlock(tg, block.getBlockData()); + } catch (Throwable spawnFailure) { + sendBlockRefresh(block); + throw spawnFailure; + } + active.incrementAndGet(); + e.setGravity(false); + e.setInvulnerable(true); + e.setGlowing(true); + e.setDropItem(false); + e.setHurtEntities(false); + e.setSilent(true); + e.setTicksLived(1); + e.setVelocity(new Vector(0, 0, 0)); + Location blockLocation = block.getLocation(); + Runnable removeTask = () -> { + if (!J.runEntity(e, e::remove) && !e.isDead()) { + e.remove(); + } + active.decrementAndGet(); + sendBlockRefresh(block); + }; + boolean scheduled = J.runAt(blockLocation, removeTask, ticks); + if (!scheduled && !J.isFolia()) { + J.s(removeTask, ticks); + } else if (!scheduled) { + removeTask.run(); + } + } + + public static void of(Block block, int ticks) { + if (block == null) { + return; + } + + of(block.getWorld(), block.getX(), block.getY(), block.getZ(), ticks); + } + + public static void of(Block block) { + of(block, 100); + } + + public static void of(World world, int x, int y, int z, int ticks) { + if (world == null) { + return; + } + + Location location = new Location(world, x, y, z); + Runnable createTask = () -> new BlockSignal(world.getBlockAt(x, y, z), ticks); + if (!J.runAt(location, createTask)) { + if (!J.isFolia()) { + J.s(createTask); + } + } + } + + public static void of(World world, int x, int y, int z) { + of(world, x, y, z, 100); + } + + public static Runnable forever(Block block) { + Location tg = block.getLocation().clone().add(0.5, 0, 0.5).clone(); + FallingBlock e = block.getWorld().spawnFallingBlock(tg.clone(), block.getBlockData()); + e.setGravity(false); + e.setInvulnerable(true); + e.setGlowing(true); + e.teleport(tg.clone()); + e.setDropItem(false); + e.setHurtEntities(false); + e.setSilent(true); + e.setTicksLived(1); + e.setVelocity(new Vector(0, 0, 0)); + + new SR(20) { + @Override + public void run() { + if (!J.runEntity(e, () -> { + if (e.isDead()) { + cancel(); + return; + } + + e.setTicksLived(1); + e.teleport(tg.clone()); + e.setVelocity(new Vector(0, 0, 0)); + }, 0, this::cancel)) { + cancel(); + } + } + }; + + return () -> { + if (!J.runEntity(e, e::remove) && !e.isDead()) { + e.remove(); + } + Location blockLocation = block.getLocation(); + Runnable refreshTask = () -> sendBlockRefresh(block); + if (!J.runAt(blockLocation, refreshTask)) { + refreshTask.run(); + } + }; + } + + private static void sendBlockRefresh(Block block) { + if (block == null) { + return; + } + + Location location = block.getLocation(); + BlockData blockData = block.getBlockData(); + for (Player player : Bukkit.getOnlinePlayers()) { + if (!player.getWorld().equals(location.getWorld())) { + continue; + } + + J.runEntity(player, () -> player.sendBlockChange(location, blockData)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java b/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java new file mode 100644 index 000000000..f3b1998ef --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java @@ -0,0 +1,365 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.edit; + +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +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.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Location; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.Block; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; + +public class DustRevealer { + // Matches the modded twin (ModdedDustRevealer) so both platforms truncate identically. + private static final int MAX_HITS = 2_048; + private static final int PARTICLE_BATCH_SIZE = 64; + + /** + * Bounded, single-threaded flood fill over the object's placement key, followed by batched + * particle playback on the owning threads. The previous shape recursively spawned one + * scheduler task + one burst task per matched block, all mutating one unsynchronized + * ArrayList and sleep-spinning MultiBurst workers — unbounded fan-out on large objects. + */ + private static void reveal(Engine engine, World world, BlockPosition origin, String key) { + KList hits = new KList<>(); + Set visited = new HashSet<>(); + ArrayDeque frontier = new ArrayDeque<>(); + visited.add(origin); + frontier.add(origin); + hits.add(origin); + int minY = world.getMinHeight(); + int maxY = world.getMaxHeight(); + + try { + search: + while (!frontier.isEmpty()) { + BlockPosition at = frontier.poll(); + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dy == 0 && dz == 0) { + continue; + } + BlockPosition next = new BlockPosition(at.getX() + dx, at.getY() + dy, at.getZ() + dz); + if (next.getY() < minY || next.getY() >= maxY || !visited.add(next)) { + continue; + } + if (key.equals(engine.getObjectPlacementKey(next.getX(), next.getY() - minY, next.getZ()))) { + hits.add(next); + frontier.add(next); + if (hits.size() >= MAX_HITS) { + break search; + } + } + } + } + } + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + revealBatch(world, hits, 0); + } + + private static void revealBatch(World world, KList hits, int from) { + if (from >= hits.size()) { + return; + } + int to = Math.min(from + PARTICLE_BATCH_SIZE, hits.size()); + Runnable batch = () -> { + for (int i = from; i < to; i++) { + BlockPosition p = hits.get(i); + BlockSignal.of(world, p.getX(), p.getY(), p.getZ(), 10); + } + BlockPosition first = hits.get(from); + if (M.r(0.25)) { + world.playSound(first.toBlock(world).getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1f, RNG.r.f(0.2f, 2f)); + } + revealBatch(world, hits, to); + }; + Location at = hits.get(from).toBlock(world).getLocation(); + if (!J.runAt(at, batch, 1) && !J.isFolia()) { + J.s(batch, 1); + } + } + + public static void spawn(Block block, VolmitSender sender) { + World world = block.getWorld(); + PlatformChunkGenerator generator = IrisToolbelt.access(world); + if (generator == null) { + return; + } + Engine access = generator.getEngine(); + + if (access != null) { + describe(access, world, block, sender); + + String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ()); + if (a != null) { + world.playSound(block.getLocation(), Sound.ITEM_LODESTONE_COMPASS_LOCK, 1f, 0.1f); + + sender.sendMessage(IrisLanguage.text( + RuntimeUiMessages.DUST_FOUND_OBJECT, + MessageArgument.untrusted("object", a) + )); + J.a(() -> reveal(access, world, new BlockPosition(block.getX(), block.getY(), block.getZ()), a)); + } + } + } + + private static void describe(Engine engine, World world, Block block, VolmitSender sender) { + int x = block.getX(); + int y = block.getY(); + int z = block.getZ(); + int minHeight = world.getMinHeight(); + int relativeY = y - minHeight; + int surfaceRelative = engine.getHeight(x, z, true); + int surfaceY = surfaceRelative + minHeight; + int offset = y - surfaceY; + + String objectKey = safe(() -> engine.getObjectPlacementKey(x, relativeY, z)); + IrisBiome surfaceBiome = safe(() -> engine.getSurfaceBiome(x, z)); + IrisBiome biomeHere = safe(() -> engine.getBiome(x, relativeY, z)); + IrisBiome caveBiome = safe(() -> engine.getCaveOrMantleBiome(x, relativeY, z)); + IrisRegion region = safe(() -> engine.getRegion(x, z)); + + KList lines = new KList<>(); + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_HEADER, + MessageArgument.trusted("x", x), + MessageArgument.trusted("y", y), + MessageArgument.trusted("z", z) + ), false)); + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_BLOCK, + MessageArgument.untrusted("block", block.getType().name()) + ), false)); + if (offset > 0) { + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_POSITION_ABOVE, + MessageArgument.trusted("offset", offset), + MessageArgument.trusted("surfaceY", surfaceY) + ), true)); + } else if (offset < 0) { + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_POSITION_BELOW, + MessageArgument.trusted("offset", -offset), + MessageArgument.trusted("surfaceY", surfaceY) + ), true)); + } else { + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_POSITION_AT, + MessageArgument.trusted("surfaceY", surfaceY) + ), true)); + } + + String placedBy; + if (offset > 0) { + placedBy = objectKey != null + ? IrisLanguage.text( + RuntimeUiMessages.DUST_PLACED_BY_OBJECT_ABOVE, + MessageArgument.untrusted("object", objectKey) + ) + : IrisLanguage.text(RuntimeUiMessages.DUST_PLACED_BY_DECORATION_ABOVE); + } else if (objectKey != null) { + placedBy = IrisLanguage.text( + RuntimeUiMessages.DUST_PLACED_BY_BURIED_OBJECT, + MessageArgument.untrusted("object", objectKey) + ); + } else { + placedBy = IrisLanguage.text( + RuntimeUiMessages.DUST_PLACED_BY_TERRAIN, + MessageArgument.trusted("depth", Math.max(0, surfaceRelative - relativeY)) + ); + } + lines.add(new DustLine(placedBy, true)); + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_OBJECT_AT_BLOCK, + MessageArgument.untrusted( + "object", + objectKey == null ? IrisLanguage.text(RuntimeUiMessages.DUST_NONE) : objectKey + ) + ), false)); + if (objectKey == null) { + String columnObject = findColumnObject(engine, x, relativeY, z, minHeight); + lines.add(new DustLine(IrisLanguage.text( + columnObject == null + ? RuntimeUiMessages.DUST_COLUMN_OBJECT_NONE + : RuntimeUiMessages.DUST_COLUMN_OBJECT, + MessageArgument.trusted( + columnObject == null ? "detail" : "object", + columnObject == null ? IrisLanguage.text(RuntimeUiMessages.DUST_COLUMN_NONE) : columnObject + ) + ), false)); + } + + if (surfaceBiome != null) { + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_SURFACE_BIOME_DETAIL, + MessageArgument.untrusted("biome", surfaceBiome.getLoadKey()), + MessageArgument.untrusted("derivative", biomeKey(surfaceBiome.getDerivative())) + ), false)); + } + if (biomeHere != null && (surfaceBiome == null || !biomeHere.getLoadKey().equals(surfaceBiome.getLoadKey()))) { + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_BIOME_AT_Y, + MessageArgument.untrusted("biome", biomeHere.getLoadKey()) + ), false)); + } + if (caveBiome != null && (surfaceBiome == null || !caveBiome.getLoadKey().equals(surfaceBiome.getLoadKey()))) { + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_CAVE_BIOME, + MessageArgument.untrusted("biome", caveBiome.getLoadKey()) + ), false)); + } + + try { + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_SERVER_BIOME, + MessageArgument.untrusted("biome", INMS.get().getTrueBiomeBaseKey(block.getLocation())), + MessageArgument.trusted("id", INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(block.getLocation()))) + ), false)); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + if (region != null) { + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_REGION, + MessageArgument.untrusted("region", region.getLoadKey()), + MessageArgument.untrusted("name", region.getName()) + ), false)); + } + + Set objects = safe(() -> engine.getObjectsAt(x >> 4, z >> 4)); + if (objects != null && !objects.isEmpty()) { + lines.add(new DustLine(IrisLanguage.text( + RuntimeUiMessages.DUST_OBJECTS_IN_CHUNK, + MessageArgument.untrusted("objects", objects) + ), false)); + } + + StringBuilder payload = new StringBuilder(); + sender.sendMessage(C.IRIS + lines.get(0).text()); + payload.append(lines.get(0).text()); + for (int i = 1; i < lines.size(); i++) { + DustLine line = lines.get(i); + sender.sendMessage((line.emphasis() ? C.YELLOW : C.WHITE) + line.text()); + payload.append('\n').append(line.text()); + } + sendCopyButton(sender, payload.toString()); + } + + private static String findColumnObject(Engine engine, int x, int relativeY, int z, int minHeight) { + for (int dy = 1; dy <= 64; dy++) { + if (relativeY + dy >= 0) { + try { + String up = engine.getObjectPlacementKey(x, relativeY + dy, z); + if (up != null) { + return IrisLanguage.text( + RuntimeUiMessages.DUST_COLUMN_ABOVE, + MessageArgument.untrusted("object", up), + MessageArgument.trusted("y", relativeY + dy + minHeight) + ); + } + } catch (Throwable ignored) { + } + } + if (relativeY - dy >= 0) { + try { + String down = engine.getObjectPlacementKey(x, relativeY - dy, z); + if (down != null) { + return IrisLanguage.text( + RuntimeUiMessages.DUST_COLUMN_BELOW, + MessageArgument.untrusted("object", down), + MessageArgument.trusted("y", relativeY - dy + minHeight) + ); + } + } catch (Throwable ignored) { + } + } + } + return null; + } + + private static String biomeKey(Biome biome) { + if (biome == null) { + return IrisLanguage.text(RuntimeUiMessages.DUST_NONE); + } + try { + return biome.getKey().getKey(); + } catch (Throwable ignored) { + return biome.toString(); + } + } + + private static T safe(Supplier supplier) { + try { + return supplier.get(); + } catch (Throwable e) { + IrisLogging.reportError(e); + return null; + } + } + + private static void sendCopyButton(VolmitSender sender, String payload) { + if (!sender.isPlayer()) { + return; + } + try { + Component button = Component.text(IrisLanguage.text(RuntimeUiMessages.DUST_COPY_BUTTON)) + .color(NamedTextColor.GREEN) + .clickEvent(ClickEvent.copyToClipboard(payload)) + .hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.text(RuntimeUiMessages.DUST_COPY_HOVER)))); + sender.sendComponent(button); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private record DustLine(String text, boolean emphasis) { + } +} diff --git a/core/src/main/java/com/volmit/iris/core/events/IrisEngineEvent.java b/core/src/main/java/art/arcane/iris/core/events/IrisEngineEvent.java similarity index 94% rename from core/src/main/java/com/volmit/iris/core/events/IrisEngineEvent.java rename to core/src/main/java/art/arcane/iris/core/events/IrisEngineEvent.java index 5651823c7..47b6d6500 100644 --- a/core/src/main/java/com/volmit/iris/core/events/IrisEngineEvent.java +++ b/core/src/main/java/art/arcane/iris/core/events/IrisEngineEvent.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.events; +package art.arcane.iris.core.events; -import com.volmit.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.Engine; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/com/volmit/iris/core/events/IrisEngineHotloadEvent.java b/core/src/main/java/art/arcane/iris/core/events/IrisEngineHotloadEvent.java similarity index 93% rename from core/src/main/java/com/volmit/iris/core/events/IrisEngineHotloadEvent.java rename to core/src/main/java/art/arcane/iris/core/events/IrisEngineHotloadEvent.java index 7b3afe4c1..d47d4aebf 100644 --- a/core/src/main/java/com/volmit/iris/core/events/IrisEngineHotloadEvent.java +++ b/core/src/main/java/art/arcane/iris/core/events/IrisEngineHotloadEvent.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.events; +package art.arcane.iris.core.events; -import com.volmit.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.Engine; import org.bukkit.event.HandlerList; public class IrisEngineHotloadEvent extends IrisEngineEvent { diff --git a/core/src/main/java/art/arcane/iris/core/events/IrisLootEvent.java b/core/src/main/java/art/arcane/iris/core/events/IrisLootEvent.java new file mode 100644 index 000000000..ece0a171f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/events/IrisLootEvent.java @@ -0,0 +1,145 @@ +package art.arcane.iris.core.events; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.InventorySlotType; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.scheduling.J; +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.world.LootGenerateEvent; + +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.loot.LootContext; +import org.bukkit.loot.LootTable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Random; + +@Getter +public class IrisLootEvent extends Event { + private static final HandlerList handlers = new HandlerList(); + private static final LootTable EMPTY = new LootTable() { + @NotNull + @Override + public NamespacedKey getKey() { + return new NamespacedKey(BukkitPlatform.plugin(), "empty"); + } + + @NotNull + @Override + public Collection populateLoot(@Nullable Random random, @NotNull LootContext context) { + return List.of(); + } + + @Override + public void fillInventory(@NotNull Inventory inventory, @Nullable Random random, @NotNull LootContext context) { + } + }; + + private final Engine engine; + private final Block block; + private final InventorySlotType slot; + private final KList tables; + + /** + * Constructor for IrisLootEvent with mode selection. + * + * @param engine The engine instance. + * @param block The block associated with the event. + * @param slot The inventory slot type. + * @param tables The list of IrisLootTables. (mutable*) + */ + public IrisLootEvent(Engine engine, Block block, InventorySlotType slot, KList tables) { + this.engine = engine; + this.block = block; + this.slot = slot; + this.tables = tables; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + /** + * Required method to get the HandlerList for this event. + * + * @return The HandlerList. + */ + public static HandlerList getHandlerList() { + return handlers; + } + + /** + * Triggers the corresponding Bukkit loot event. + * This method integrates your custom IrisLootTables with Bukkit's LootGenerateEvent, + * allowing other plugins to modify or cancel the loot generation. + * + * @return true when the event was canceled + */ + public static boolean callLootEvent(KList loot, Inventory inv, World world, int x, int y, int z) { + InventoryHolder holder = inv.getHolder(); + Location loc = new Location(world, x, y, z); + if (holder == null) { + holder = new InventoryHolder() { + @NotNull + @Override + public Inventory getInventory() { + return inv; + } + }; + } + + LootContext context = new LootContext.Builder(loc).build(); + LootGenerateEvent event = new LootGenerateEvent(world, null, holder, EMPTY, context, loot, true); + if (!Bukkit.isPrimaryThread()) { + IrisLogging.warn("LootGenerateEvent was not called on the main thread, please report this issue."); + Thread.dumpStack(); + J.sfut(() -> { + try { + Bukkit.getPluginManager().callEvent(event); + } catch (Throwable e) { + IrisLogging.reportError("LootGenerateEvent dispatch failed at " + + world.getName() + " [" + x + "," + y + "," + z + "].", e); + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (e instanceof Error error) { + throw error; + } + throw new IllegalStateException(e); + } + }).join(); + } else { + try { + Bukkit.getPluginManager().callEvent(event); + } catch (Throwable e) { + IrisLogging.reportError("LootGenerateEvent dispatch failed at " + + world.getName() + " [" + x + "," + y + "," + z + "].", e); + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (e instanceof Error error) { + throw error; + } + throw new IllegalStateException(e); + } + } + + return event.isCancelled(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/gui/GuiHost.java b/core/src/main/java/art/arcane/iris/core/gui/GuiHost.java new file mode 100644 index 000000000..ec4517cc4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/gui/GuiHost.java @@ -0,0 +1,139 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.engine.framework.Engine; + +import javax.swing.JFrame; +import java.awt.Desktop; +import java.awt.GraphicsEnvironment; +import java.awt.desktop.QuitResponse; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class GuiHost { + private static final AtomicBoolean DESKTOP_QUIT_GUARD_INSTALLED = new AtomicBoolean(false); + private static volatile Provider provider = new Provider() { + }; + private static volatile boolean desktopSuppressed = false; + + private GuiHost() { + } + + public static void suppressDesktop(boolean suppress) { + desktopSuppressed = suppress; + } + + public static boolean isDesktopSuppressed() { + return desktopSuppressed; + } + + public interface Provider { + default Engine findActiveEngine() { + return null; + } + + default void registerHotloadHook(Runnable onHotload) { + } + + default void unregisterHotloadHook(Runnable onHotload) { + } + + default GuiOverlay overlayFor(Engine engine) { + return null; + } + } + + public static void set(Provider boundProvider) { + if (boundProvider != null) { + provider = boundProvider; + } + } + + public static Provider get() { + return provider; + } + + public static boolean isAvailable() { + return !desktopSuppressed && !GraphicsEnvironment.isHeadless(); + } + + public static void prepareFrame(JFrame frame) { + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + prepareServerDesktop(); + } + + private static void prepareServerDesktop() { + if (!isAvailable() + || !System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("mac") + || !DESKTOP_QUIT_GUARD_INSTALLED.compareAndSet(false, true)) { + return; + } + + try { + if (!Desktop.isDesktopSupported()) { + return; + } + Desktop desktop = Desktop.getDesktop(); + if (!desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) { + return; + } + desktop.setQuitHandler((event, response) -> cancelDesktopQuit(response)); + } catch (Throwable error) { + IrisLogging.reportError(error); + IrisLogging.info("Unable to install the Iris desktop quit guard; use the server stop command instead of macOS Quit"); + } + } + + static void cancelDesktopQuit(QuitResponse response) { + response.cancelQuit(); + } + + /** + * Outcome of a server triggered desktop gui launch request. + */ + public enum ServerGuiLaunch { + /** + * The gui was asked for and a display environment exists. + */ + OPEN, + /** + * The gui was not asked for, or server launched guis are turned off in settings. + */ + DISABLED, + /** + * The gui was asked for but the jvm is headless or the desktop is suppressed. + */ + UNAVAILABLE + } + + /** + * Decides whether a server triggered job may open a desktop gui. Callers must not attempt an + * awt launch on anything other than {@link ServerGuiLaunch#OPEN}, since awt throws on a headless jvm. + */ + public static ServerGuiLaunch serverGuiLaunch(boolean requested) { + if (!requested || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) { + return ServerGuiLaunch.DISABLED; + } + + return isAvailable() ? ServerGuiLaunch.OPEN : ServerGuiLaunch.UNAVAILABLE; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/gui/GuiMarker.java b/core/src/main/java/art/arcane/iris/core/gui/GuiMarker.java new file mode 100644 index 000000000..58173f942 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/gui/GuiMarker.java @@ -0,0 +1,29 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +public record GuiMarker(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) { + public static GuiMarker player(String name, double worldX, double worldZ) { + return new GuiMarker(name, worldX, 0, worldZ, 0, 0); + } + + public static GuiMarker entity(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) { + return new GuiMarker(label, worldX, worldY, worldZ, health, maxHealth); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/gui/GuiOverlay.java b/core/src/main/java/art/arcane/iris/core/gui/GuiOverlay.java new file mode 100644 index 000000000..b329e0272 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/gui/GuiOverlay.java @@ -0,0 +1,34 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +import art.arcane.iris.engine.framework.render.RenderType; + +import java.util.List; +import java.util.function.Consumer; + +public interface GuiOverlay { + List players(); + + void requestEntities(Consumer> sink); + + void teleport(double worldX, double worldZ); + + String openInEditor(double worldX, double worldZ, RenderType type); +} diff --git a/core/src/main/java/art/arcane/iris/core/gui/NoiseExplorerGUI.java b/core/src/main/java/art/arcane/iris/core/gui/NoiseExplorerGUI.java new file mode 100644 index 000000000..f8d6075c4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/gui/NoiseExplorerGUI.java @@ -0,0 +1,612 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +import art.arcane.iris.core.localization.DesktopUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.math.RollingSequence; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import javax.swing.BorderFactory; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.JFrame; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public class NoiseExplorerGUI extends JPanel implements MouseWheelListener { + + private static final long serialVersionUID = 2094606939770332040L; + private static final Color BG = new Color(24, 24, 28); + private static final Color SIDEBAR_BG = new Color(20, 20, 24); + private static final Color SIDEBAR_SELECTED = new Color(40, 50, 70); + private static final Color SIDEBAR_ITEM_COLOR = new Color(170, 170, 185); + private static final Color SEARCH_BG = new Color(30, 30, 38); + private static final Color SEARCH_FG = new Color(180, 180, 190); + private static final Color STATUS_BG = new Color(32, 32, 38, 230); + private static final Color STATUS_TEXT = new Color(180, 180, 190); + private static final Color ACCENT = new Color(90, 140, 255); + private static final Color SEPARATOR = new Color(40, 40, 50); + private static final Font STATUS_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 12); + private static final Font SIDEBAR_HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11); + private static final Font SIDEBAR_ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + private static final Font SEARCH_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 13); + private static final int SIDEBAR_WIDTH = 240; + private static final int[] HSB_LUT = new int[256]; + + private static final NoiseCategory[] CATEGORY_ORDER = { + NoiseCategory.PACK_GENERATORS, NoiseCategory.SIMPLEX, NoiseCategory.PERLIN, + NoiseCategory.CELLULAR, NoiseCategory.IRIS, NoiseCategory.CLOVER, NoiseCategory.HEXAGON, + NoiseCategory.VASCULAR, NoiseCategory.GLOBE, NoiseCategory.CUBIC, NoiseCategory.FRACTAL, + NoiseCategory.STATIC, NoiseCategory.NOWHERE, NoiseCategory.SIERPINSKI, NoiseCategory.UTILITY, + NoiseCategory.OTHER + }; + + static { + for (int i = 0; i < 256; i++) { + float n = i / 255f; + HSB_LUT[i] = Color.HSBtoRGB(0.666f - n * 0.666f, 1f, 1f - n * 0.8f); + } + } + + private final RollingSequence fpsHistory = new RollingSequence(60); + private final boolean colorMode = IrisSettings.get().getGui().colorMode; + private final MultiBurst gx = MultiBurst.burst; + private final Runnable hotloadHook = this::refreshGenerator; + private double scale = 1; + private double animScale = 10; + private double ox = 0; + private double oz = 0; + private double animOx = 0; + private double animOz = 0; + private double lastMouseX = Double.MAX_VALUE; + private double lastMouseZ = Double.MAX_VALUE; + private double time = 0; + private double animTime = 0; + private int imgWidth = 0; + private int imgHeight = 0; + private BufferedImage img; + private CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong())); + private Function2 generator; + private Supplier> loader; + private String currentName = "STATIC"; + + public NoiseExplorerGUI() { + GuiHost.get().registerHotloadHook(hotloadHook); + setBackground(BG); + addMouseWheelListener(this); + addMouseMotionListener(new MouseMotionListener() { + @Override + public void mouseMoved(MouseEvent e) { + Point cp = e.getPoint(); + lastMouseX = cp.getX(); + lastMouseZ = cp.getY(); + } + + @Override + public void mouseDragged(MouseEvent e) { + Point cp = e.getPoint(); + ox += (lastMouseX - cp.getX()) * scale; + oz += (lastMouseZ - cp.getY()) * scale; + lastMouseX = cp.getX(); + lastMouseZ = cp.getY(); + } + }); + } + + public static void launch() { + Engine engine = GuiHost.get().findActiveEngine(); + EventQueue.invokeLater(() -> { + NoiseExplorerGUI nv = new NoiseExplorerGUI(); + buildFrame(IrisLanguage.plain(DesktopUiMessages.NOISE_TITLE), nv, engine, null, null); + }); + } + + public static void launch(Supplier> gen, String genName) { + Engine engine = GuiHost.get().findActiveEngine(); + EventQueue.invokeLater(() -> { + NoiseExplorerGUI nv = new NoiseExplorerGUI(); + nv.loader = gen; + nv.generator = gen.get(); + nv.currentName = genName; + buildFrame(IrisLanguage.plain(DesktopUiMessages.NOISE_TITLE_GENERATOR, MessageArgument.untrusted("generator", genName)), nv, engine, gen, genName); + }); + } + + private static JFrame buildFrame(String title, NoiseExplorerGUI nv, Engine engine, + Supplier> customGen, String customName) { + JFrame frame = new JFrame(title); + GuiHost.prepareFrame(frame); + frame.getContentPane().setBackground(BG); + frame.setLayout(new BorderLayout()); + + JPanel sidebar = buildSidebar(nv, engine, customGen, customName); + frame.add(sidebar, BorderLayout.WEST); + frame.add(nv, BorderLayout.CENTER); + + frame.setSize(1440, 820); + frame.setMinimumSize(new Dimension(640, 480)); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + GuiHost.get().unregisterHotloadHook(nv.hotloadHook); + } + }); + return frame; + } + + private static JPanel buildSidebar(NoiseExplorerGUI nv, Engine engine, + Supplier> customGen, String customName) { + JPanel sidebar = new JPanel(new BorderLayout()); + sidebar.setPreferredSize(new Dimension(SIDEBAR_WIDTH, 0)); + sidebar.setBackground(SIDEBAR_BG); + sidebar.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, SEPARATOR)); + + JTextField search = new JTextField(); + search.setBackground(SEARCH_BG); + search.setForeground(SEARCH_FG); + search.setCaretColor(SEARCH_FG); + search.setFont(SEARCH_FONT); + search.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, SEPARATOR), + BorderFactory.createEmptyBorder(8, 10, 8, 10) + )); + search.putClientProperty("JTextField.placeholderText", IrisLanguage.plain(DesktopUiMessages.NOISE_SEARCH)); + + LinkedHashMap> categories = buildCategoryMap(nv, engine, customGen, customName); + DefaultListModel model = new DefaultListModel<>(); + populateModel(model, categories, ""); + + JList list = new JList<>(model); + list.setBackground(SIDEBAR_BG); + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + list.setCellRenderer(new SidebarCellRenderer()); + list.setFixedCellHeight(-1); + + list.addListSelectionListener(e -> { + if (e.getValueIsAdjusting()) return; + ListItem selected = list.getSelectedValue(); + if (selected != null && !selected.header && selected.action != null) { + selected.action.run(); + } + }); + + search.getDocument().addDocumentListener(new DocumentListener() { + private void filter() { + String text = search.getText().trim(); + populateModel(model, categories, text); + } + + public void insertUpdate(DocumentEvent e) { filter(); } + public void removeUpdate(DocumentEvent e) { filter(); } + public void changedUpdate(DocumentEvent e) { filter(); } + }); + + JScrollPane scrollPane = new JScrollPane(list); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); + scrollPane.getVerticalScrollBar().setUnitIncrement(16); + scrollPane.getVerticalScrollBar().setBackground(SIDEBAR_BG); + + sidebar.add(search, BorderLayout.NORTH); + sidebar.add(scrollPane, BorderLayout.CENTER); + return sidebar; + } + + private static void populateModel(DefaultListModel model, LinkedHashMap> categories, String filter) { + model.clear(); + String lower = filter.toLowerCase(); + for (Map.Entry> entry : categories.entrySet()) { + List matching = new ArrayList<>(); + for (ListItem item : entry.getValue()) { + if (lower.isEmpty() || item.text.toLowerCase().contains(lower) || item.rawName.toLowerCase().contains(lower)) { + matching.add(item); + } + } + if (!matching.isEmpty()) { + model.addElement(new ListItem(entry.getKey(), entry.getKey(), true, null)); + for (ListItem item : matching) { + model.addElement(item); + } + } + } + } + + private static LinkedHashMap> buildCategoryMap(NoiseExplorerGUI nv, Engine engine, + Supplier> customGen, String customName) { + LinkedHashMap> categories = new LinkedHashMap<>(); + + if (customGen != null && customName != null) { + List custom = new ArrayList<>(); + custom.add(new ListItem(customName, customName, false, () -> { + nv.generator = customGen.get(); + nv.loader = customGen; + nv.currentName = customName; + })); + categories.put(IrisLanguage.plain(DesktopUiMessages.NOISE_CATEGORY_CUSTOM), custom); + } + + Map> styleGroups = new LinkedHashMap<>(); + for (NoiseStyle style : NoiseStyle.values()) { + NoiseCategory cat = categorize(style); + styleGroups.computeIfAbsent(cat, k -> new ArrayList<>()).add(style); + } + + if (engine != null && !engine.isClosed()) { + List genItems = new ArrayList<>(); + try { + IrisData data = engine.getData(); + String[] keys = data.getGeneratorLoader().getPossibleKeys(); + Arrays.sort(keys); + for (String key : keys) { + IrisGenerator gen = data.getGeneratorLoader().load(key); + if (gen != null) { + long seed = new RNG(12345).nextParallelRNG(3245).lmax(); + genItems.add(new ListItem(formatName(key), key, false, () -> { + nv.generator = (x, z) -> gen.getHeight(x, z, seed); + nv.loader = null; + nv.currentName = key; + })); + } + } + } catch (Throwable ignored) {} + if (!genItems.isEmpty()) { + categories.put(categoryLabel(NoiseCategory.PACK_GENERATORS), genItems); + } + } + + for (NoiseCategory cat : CATEGORY_ORDER) { + if (cat == NoiseCategory.PACK_GENERATORS) continue; + List styles = styleGroups.get(cat); + if (styles != null && !styles.isEmpty()) { + List items = new ArrayList<>(); + for (NoiseStyle style : styles) { + items.add(new ListItem(formatName(style.name()), style.name(), false, () -> { + nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax())); + nv.generator = null; + nv.loader = null; + nv.currentName = style.name(); + })); + } + categories.put(categoryLabel(cat), items); + } + } + + for (Map.Entry> entry : styleGroups.entrySet()) { + String category = categoryLabel(entry.getKey()); + if (!categories.containsKey(category)) { + List items = new ArrayList<>(); + for (NoiseStyle style : entry.getValue()) { + items.add(new ListItem(formatName(style.name()), style.name(), false, () -> { + nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax())); + nv.generator = null; + nv.loader = null; + nv.currentName = style.name(); + })); + } + categories.put(category, items); + } + } + + return categories; + } + + private static NoiseCategory categorize(NoiseStyle style) { + String n = style.name(); + if (n.startsWith("STATIC")) return NoiseCategory.STATIC; + if (n.startsWith("IRIS")) return NoiseCategory.IRIS; + if (n.startsWith("CLOVER")) return NoiseCategory.CLOVER; + if (n.startsWith("VASCULAR")) return NoiseCategory.VASCULAR; + if (n.equals("FLAT")) return NoiseCategory.UTILITY; + if (n.startsWith("CELLULAR")) return NoiseCategory.CELLULAR; + if (n.startsWith("HEX") || n.equals("HEXAGON")) return NoiseCategory.HEXAGON; + if (n.startsWith("SIERPINSKI")) return NoiseCategory.SIERPINSKI; + if (n.startsWith("NOWHERE")) return NoiseCategory.NOWHERE; + if (n.startsWith("GLOB")) return NoiseCategory.GLOBE; + if (n.startsWith("PERLIN")) return NoiseCategory.PERLIN; + if (n.startsWith("CUBIC") || (n.startsWith("FRACTAL") && n.contains("CUBIC"))) return NoiseCategory.CUBIC; + if (n.contains("SIMPLEX") && !n.startsWith("FRACTAL")) return NoiseCategory.SIMPLEX; + if (n.startsWith("FRACTAL")) return NoiseCategory.FRACTAL; + return NoiseCategory.OTHER; + } + + private static String categoryLabel(NoiseCategory category) { + return IrisLanguage.plain(switch (category) { + case PACK_GENERATORS -> DesktopUiMessages.NOISE_CATEGORY_PACK_GENERATORS; + case SIMPLEX -> DesktopUiMessages.NOISE_CATEGORY_SIMPLEX; + case PERLIN -> DesktopUiMessages.NOISE_CATEGORY_PERLIN; + case CELLULAR -> DesktopUiMessages.NOISE_CATEGORY_CELLULAR; + case IRIS -> DesktopUiMessages.NOISE_CATEGORY_IRIS; + case CLOVER -> DesktopUiMessages.NOISE_CATEGORY_CLOVER; + case HEXAGON -> DesktopUiMessages.NOISE_CATEGORY_HEXAGON; + case VASCULAR -> DesktopUiMessages.NOISE_CATEGORY_VASCULAR; + case GLOBE -> DesktopUiMessages.NOISE_CATEGORY_GLOBE; + case CUBIC -> DesktopUiMessages.NOISE_CATEGORY_CUBIC; + case FRACTAL -> DesktopUiMessages.NOISE_CATEGORY_FRACTAL; + case STATIC -> DesktopUiMessages.NOISE_CATEGORY_STATIC; + case NOWHERE -> DesktopUiMessages.NOISE_CATEGORY_NOWHERE; + case SIERPINSKI -> DesktopUiMessages.NOISE_CATEGORY_SIERPINSKI; + case UTILITY -> DesktopUiMessages.NOISE_CATEGORY_UTILITY; + case OTHER -> DesktopUiMessages.NOISE_CATEGORY_OTHER; + }); + } + + private static String formatName(String enumName) { + String lower = enumName.toLowerCase().replace('_', ' '); + return Character.toUpperCase(lower.charAt(0)) + lower.substring(1); + } + + private void refreshGenerator() { + if (generator != null && loader != null) { + generator = loader.get(); + } + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + int notches = e.getWheelRotation(); + if (e.isControlDown()) { + time = time + ((0.0025 * time) * notches); + return; + } + scale = scale + ((0.044 * scale) * notches); + scale = Math.max(scale, 0.00001); + } + + private double lerp(double current, double target, double speed) { + double diff = target - current; + if (Math.abs(diff) < 0.001) return target; + return current + diff * speed; + } + + @Override + public void paint(Graphics g) { + animScale = lerp(animScale, scale, 0.16); + animTime = lerp(animTime, time, 0.29); + animOx = lerp(animOx, ox, 0.16); + animOz = lerp(animOz, oz, 0.16); + + PrecisionStopwatch p = PrecisionStopwatch.start(); + + if (g instanceof Graphics2D gg) { + gg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + gg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + int pw = getWidth(); + int ph = getHeight(); + + if (pw != imgWidth || ph != imgHeight || img == null) { + imgWidth = pw; + imgHeight = ph; + img = null; + } + + int accuracy = M.clip((fpsHistory.getAverage() / 14D), 1D, 64D).intValue(); + int rw = Math.max(1, pw / accuracy); + int rh = Math.max(1, ph / accuracy); + + if (img == null || img.getWidth() != rw || img.getHeight() != rh) { + img = new BufferedImage(rw, rh, BufferedImage.TYPE_INT_RGB); + } + + int[] pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData(); + + BurstExecutor burst = gx.burst(rw); + for (int x = 0; x < rw; x++) { + int xx = x; + burst.queue(() -> { + for (int z = 0; z < rh; z++) { + double worldX = (xx * accuracy * animScale) + animOx; + double worldZ = (z * accuracy * animScale) + animOz; + double n = generator != null + ? generator.apply(worldX, worldZ) + : cng.noise(worldX, worldZ); + n = Math.max(0, Math.min(1, n)); + + int rgb; + if (colorMode) { + rgb = HSB_LUT[(int) (n * 255)]; + } else { + int v = (int) (n * 255); + rgb = (v << 16) | (v << 8) | v; + } + pixels[z * rw + xx] = rgb; + } + }); + } + burst.complete(); + + gg.setColor(BG); + gg.fillRect(0, 0, pw, ph); + gg.drawImage(img, 0, 0, pw, ph, null); + + renderStatusBar(gg, pw, ph, p.getMilliseconds()); + renderCrosshair(gg, pw, ph); + } + + p.end(); + time += 1D; + fpsHistory.put(p.getMilliseconds()); + + if (!isVisible() || !getParent().isVisible()) { + return; + } + + long sleepMs = Math.max(1, 16 - (long) p.getMilliseconds()); + // Pace on a worker, not the EDT: a sleep queued on the event thread blocks painting + // and input for the whole frame budget. repaint() marshals itself back. + J.a(() -> { + J.sleep(sleepMs); + repaint(); + }); + } + + private void renderCrosshair(Graphics2D g, int w, int h) { + int cx = w / 2; + int cy = h / 2; + g.setColor(new Color(255, 255, 255, 40)); + g.drawLine(cx - 8, cy, cx + 8, cy); + g.drawLine(cx, cy - 8, cx, cy + 8); + } + + private void renderStatusBar(Graphics2D g, int w, int h, double frameMs) { + int barHeight = 28; + int y = h - barHeight; + + g.setColor(STATUS_BG); + g.fillRect(0, y, w, barHeight); + g.setColor(new Color(50, 50, 60)); + g.drawLine(0, y, w, y); + + g.setFont(STATUS_FONT); + g.setColor(STATUS_TEXT); + + double worldX = (w / 2.0 * animScale) + animOx; + double worldZ = (h / 2.0 * animScale) + animOz; + double noiseVal = generator != null + ? generator.apply(worldX, worldZ) + : cng.noise(worldX, worldZ); + noiseVal = Math.max(0, Math.min(1, noiseVal)); + + int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0; + + String status = IrisLanguage.plain( + DesktopUiMessages.NOISE_STATUS, + MessageArgument.untrusted("name", currentName), + MessageArgument.trusted("x", String.format("%.1f", worldX)), + MessageArgument.trusted("z", String.format("%.1f", worldZ)), + MessageArgument.trusted("zoom", String.format("%.4f", animScale)), + MessageArgument.trusted("value", String.format("%.4f", noiseVal)), + MessageArgument.trusted("fps", fps) + ); + g.drawString(status, 8, y + 18); + + int barW = 60; + int barX = w - barW - 12; + int barY = y + 6; + int barH = barHeight - 12; + g.setColor(new Color(40, 40, 48)); + g.fillRoundRect(barX, barY, barW, barH, 4, 4); + int fillW = (int) (noiseVal * (barW - 2)); + g.setColor(ACCENT); + g.fillRoundRect(barX + 1, barY + 1, fillW, barH - 2, 3, 3); + } + + private static final class ListItem { + final String text; + final String rawName; + final boolean header; + final Runnable action; + + ListItem(String text, String rawName, boolean header, Runnable action) { + this.text = text; + this.rawName = rawName; + this.header = header; + this.action = action; + } + + @Override + public String toString() { + return text; + } + } + + private static final class SidebarCellRenderer extends DefaultListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean selected, boolean focus) { + ListItem item = (ListItem) value; + super.getListCellRendererComponent(list, item.text, index, !item.header && selected, false); + setOpaque(true); + if (item.header) { + setFont(SIDEBAR_HEADER_FONT); + setForeground(ACCENT); + setBackground(SIDEBAR_BG); + setBorder(BorderFactory.createEmptyBorder(10, 10, 4, 10)); + } else { + setFont(SIDEBAR_ITEM_FONT); + setForeground(selected ? Color.WHITE : SIDEBAR_ITEM_COLOR); + setBackground(selected ? SIDEBAR_SELECTED : SIDEBAR_BG); + setBorder(BorderFactory.createEmptyBorder(3, 20, 3, 10)); + } + return this; + } + } + + private enum NoiseCategory { + PACK_GENERATORS, + SIMPLEX, + PERLIN, + CELLULAR, + IRIS, + CLOVER, + HEXAGON, + VASCULAR, + GLOBE, + CUBIC, + FRACTAL, + STATIC, + NOWHERE, + SIERPINSKI, + UTILITY, + OTHER + } +} diff --git a/core/src/main/java/art/arcane/iris/core/gui/PregenRenderSource.java b/core/src/main/java/art/arcane/iris/core/gui/PregenRenderSource.java new file mode 100644 index 000000000..638b16373 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/gui/PregenRenderSource.java @@ -0,0 +1,31 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +import art.arcane.volmlib.util.math.Position2; + +public interface PregenRenderSource { + Position2 min(); + + Position2 max(); + + String[] progress(); + + boolean paused(); +} diff --git a/core/src/main/java/art/arcane/iris/core/gui/PregenRenderer.java b/core/src/main/java/art/arcane/iris/core/gui/PregenRenderer.java new file mode 100644 index 000000000..d241c32da --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/gui/PregenRenderer.java @@ -0,0 +1,205 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +import art.arcane.iris.core.localization.DesktopUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; +import art.arcane.volmlib.util.math.M; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Frame; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.util.concurrent.locks.ReentrantLock; + +public final class PregenRenderer extends JPanel implements KeyListener { + private static final long serialVersionUID = 2094606939770332040L; + + private static final int MAX_PENDING_DRAWS = 16_384; + private Long2ObjectLinkedOpenHashMap pending = new Long2ObjectLinkedOpenHashMap<>(); + private final ReentrantLock lock = new ReentrantLock(); + private final int res = 512; + private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB); + private final Graphics2D imageGraphics = image.createGraphics(); + private final PregenRenderSource source; + private final Runnable onPause; + private final Timer repaintTimer; + private volatile boolean renderingEnabled; + private boolean disposed; + private volatile JFrame frame; + + private PregenRenderer(PregenRenderSource source, Runnable onPause) { + this.source = source; + this.onPause = onPause; + repaintTimer = new Timer(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250, event -> repaint()); + } + + public static PregenRenderer open(String title, PregenRenderSource source, Runnable onPause) { + PregenRenderer renderer = new PregenRenderer(source, onPause); + JFrame frame = new JFrame(title); + GuiHost.prepareFrame(frame); + renderer.frame = frame; + renderer.renderingEnabled = true; + frame.addKeyListener(renderer); + frame.add(renderer); + frame.setSize(1000, 1000); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent event) { + renderer.disposeRenderer(); + } + }); + frame.addWindowStateListener(event -> renderer.renderingEnabled = (event.getNewState() & Frame.ICONIFIED) == 0); + frame.setVisible(true); + renderer.repaintTimer.start(); + return renderer; + } + + public void submit(int x, int z, Color color) { + if (!renderingEnabled) { + return; + } + + long key = ((long) x << 32) ^ (z & 0xffffffffL); + lock.lock(); + try { + pending.putAndMoveToFirst(key, color); + if (pending.size() > MAX_PENDING_DRAWS) { + pending.removeLast(); + } + } finally { + lock.unlock(); + } + } + + public boolean isVisibleFrame() { + return renderingEnabled; + } + + public void close() { + if (!SwingUtilities.isEventDispatchThread()) { + SwingUtilities.invokeLater(this::close); + return; + } + JFrame activeFrame = frame; + if (activeFrame != null) { + activeFrame.dispose(); + } else { + disposeRenderer(); + } + } + + private void disposeRenderer() { + if (disposed) { + return; + } + disposed = true; + renderingEnabled = false; + frame = null; + repaintTimer.stop(); + lock.lock(); + try { + pending.clear(); + } finally { + lock.unlock(); + } + imageGraphics.dispose(); + } + + @Override + public void paint(Graphics gx) { + Graphics2D g = (Graphics2D) gx; + Long2ObjectLinkedOpenHashMap batch; + lock.lock(); + try { + batch = pending; + pending = new Long2ObjectLinkedOpenHashMap<>(); + } finally { + lock.unlock(); + } + for (Long2ObjectMap.Entry entry : batch.long2ObjectEntrySet()) { + try { + long key = entry.getLongKey(); + draw((int) (key >> 32), (int) key, entry.getValue(), imageGraphics); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true); + g.setColor(Color.WHITE); + g.setFont(new Font("Hevetica", Font.BOLD, 13)); + String[] prog = source.progress(); + int h = g.getFontMetrics().getHeight() + 5; + int hh = 20; + + if (source.paused()) { + g.drawString(IrisLanguage.plain(DesktopUiMessages.PREGEN_PAUSED), 20, hh += h); + g.drawString(IrisLanguage.plain(DesktopUiMessages.PREGEN_RESUME_HINT), 20, hh += h); + } else { + for (String i : prog) { + g.drawString(i, 20, hh += h); + } + g.drawString(IrisLanguage.plain(DesktopUiMessages.PREGEN_PAUSE_HINT), 20, hh += h); + } + + } + + private void draw(int chunkX, int chunkZ, Color c, Graphics2D bg) { + double pw = M.lerpInverse(source.min().getX(), source.max().getX(), chunkX); + double ph = M.lerpInverse(source.min().getZ(), source.max().getZ(), chunkZ); + double pwa = M.lerpInverse(source.min().getX(), source.max().getX(), chunkX + 1); + double pha = M.lerpInverse(source.min().getZ(), source.max().getZ(), chunkZ + 1); + int x = (int) M.lerp(0, res, pw); + int z = (int) M.lerp(0, res, ph); + int xa = (int) M.lerp(0, res, pwa); + int za = (int) M.lerp(0, res, pha); + bg.setColor(c); + bg.fillRect(x, z, xa - x, za - z); + } + + @Override + public void keyTyped(KeyEvent e) { + } + + @Override + public void keyPressed(KeyEvent e) { + } + + @Override + public void keyReleased(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_P && onPause != null) { + onPause.run(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java new file mode 100644 index 000000000..4fbef3eb4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java @@ -0,0 +1,586 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +import art.arcane.iris.core.localization.DesktopUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.core.protocol.IrisProtocolServer; +import art.arcane.iris.core.pregenerator.IrisPregenerator; +import art.arcane.iris.core.pregenerator.PregenApiPhase; +import art.arcane.iris.core.pregenerator.PregenApiSink; +import art.arcane.iris.core.pregenerator.PregenListener; +import art.arcane.iris.core.pregenerator.PregenPhaseTracker; +import art.arcane.iris.core.pregenerator.PregenRates; +import art.arcane.iris.core.pregenerator.PregenTask; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.format.MemoryMonitor; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.iris.util.common.scheduling.J; + +import java.awt.Color; +import java.awt.EventQueue; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +public class PregeneratorJob implements PregenListener, PregenRenderSource { + // Must exceed the worker's own worst-case teardown budget (AsyncPregenMethod close: + // 60s permit drain + 120s flush + plate reclaim), or a routine drain trips the deadline + // and aborts the engine shutdown sequence mid-teardown. + private static final long WORLD_SHUTDOWN_TIMEOUT_MILLIS = 200_000L; + private static final Color COLOR_EXISTS = parseColor("#4d7d5b"); + private static final Color COLOR_BLACK = parseColor("#4d7d5b"); + private static final Color COLOR_MANTLE = parseColor("#3c2773"); + private static final Color COLOR_GENERATING = parseColor("#66967f"); + private static final Color COLOR_NETWORK = parseColor("#a863c2"); + private static final Color COLOR_NETWORK_GENERATING = parseColor("#836b8c"); + private static final Color COLOR_GENERATED = parseColor("#65c295"); + private static final Color COLOR_CLEANED = parseColor("#34eb93"); + private static final AtomicReference instance = new AtomicReference<>(); + private final MemoryMonitor monitor; + private final PregenTask task; + private final AtomicBoolean saving; + private final List> onProgress = new CopyOnWriteArrayList<>(); + private final List whenDone = new CopyOnWriteArrayList<>(); + private final IrisPregenerator pregenerator; + private final Position2 min; + private final Position2 max; + private final Engine engine; + private final ExecutorService service; + private final Thread worker; + private final PregenPhaseTracker apiPhases = new PregenPhaseTracker(); + private PregenRenderer renderer; + private String[] info; + private volatile double lastChunksPerSecond = 0D; + private volatile double lastOverallChunksPerSecond = 0D; + private volatile double lastThirtySecondChunksPerSecond = 0D; + private volatile double lastSixtySecondChunksPerSecond = 0D; + private volatile long lastChunksRemaining = 0L; + private volatile long lastGenerated = 0L; + private volatile long lastTotalChunks = 0L; + private volatile long lastEta = 0L; + private volatile long lastElapsed = 0L; + private volatile String lastMethod = IrisLanguage.plain(DesktopUiMessages.PREGEN_METHOD_PENDING); + + public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) { + this.engine = engine; + monitor = new MemoryMonitor(50); + saving = new AtomicBoolean(false); + info = new String[]{IrisLanguage.plain(DesktopUiMessages.PREGEN_INITIALIZING)}; + this.task = task; + this.pregenerator = new IrisPregenerator(task, method, this); + max = new Position2(Integer.MIN_VALUE, Integer.MIN_VALUE); + min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE); + service = new ThreadPoolExecutor( + 1, + 1, + 0L, + TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(1_024), + runnable -> { + Thread thread = new Thread(runnable, "Iris Pregen Renderer"); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + thread.setUncaughtExceptionHandler((activeThread, error) -> IrisLogging.reportError(error)); + return thread; + }, + new ThreadPoolExecutor.DiscardOldestPolicy()); + + switch (GuiHost.serverGuiLaunch(task.isGui())) { + case OPEN -> open(); + case UNAVAILABLE -> IrisLogging.info("Pregen GUI unavailable (headless), continuing"); + case DISABLED -> { + } + } + + worker = new Thread(() -> { + J.sleep(1000); + computeBounds(); + this.pregenerator.start(); + }, "Iris Pregenerator"); + worker.setPriority(Thread.MIN_PRIORITY); + worker.setDaemon(true); + worker.setUncaughtExceptionHandler((thread, ex) -> IrisLogging.reportError(ex)); + + // Publish into the static only after every field is assigned (the volatile swap is + // what makes them visible to metrics/shutdown readers), and start the worker after + // publication so it also sees a complete object. + // CAS-or-throw: updateAndGet must be side-effect free (it can re-apply on + // contention), and silently killing the previous job overlapped its 60s+120s + // teardown with the new job's generation. Replacement goes through + // shutdownAndWait first, matching the modded adapter's rejection contract. + if (!instance.compareAndSet(null, this)) { + monitor.close(); + service.shutdown(); + throw new IllegalStateException("An Iris pregeneration job is already running; stop it first."); + } + try { + worker.start(); + } catch (Throwable startFailure) { + // Un-publish: a worker that never started can never run onClose(), so nothing + // else would ever clear this instance via the normal path. + instance.compareAndSet(this, null); + monitor.close(); + service.shutdown(); + throw startFailure; + } + } + + private void computeBounds() { + task.iterateAllChunks((xx, zz) -> { + min.setX(Math.min(xx, min.getX())); + min.setZ(Math.min(zz, min.getZ())); + max.setX(Math.max(xx, max.getX())); + max.setZ(Math.max(zz, max.getZ())); + }); + } + + public static boolean shutdownInstance() { + PregeneratorJob inst = instance.get(); + if (inst == null) { + return false; + } + + if (!inst.worker.isAlive() && inst.worker.getState() != Thread.State.NEW) { + // The worker died without running onClose (early abort); clear the phantom job so + // it stops suppressing entity spawns and blocking future pregens. A NEW worker is + // a job mid-construction, not a dead one. + instance.compareAndSet(inst, null); + return false; + } + + J.a(() -> { + inst.pregenerator.close(); + inst.worker.interrupt(); + }); + return true; + } + + public static boolean shutdownInstanceForWorld(String worldIdentity) { + PregeneratorJob inst = instance.get(); + if (inst == null || !inst.targetsWorldIdentity(worldIdentity)) { + return false; + } + + return shutdownAndWait(inst, WORLD_SHUTDOWN_TIMEOUT_MILLIS); + } + + public static boolean shutdownAndWait(long timeoutMs) { + PregeneratorJob inst = instance.get(); + if (inst == null) { + return false; + } + + return shutdownAndWait(inst, timeoutMs); + } + + private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs) { + inst.pregenerator.close(); + inst.worker.interrupt(); + try { + inst.worker.join(Math.max(1L, timeoutMs)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while stopping the Iris pregenerator.", e); + } + if (inst.worker.isAlive()) { + throw new IllegalStateException("Timed out while stopping the Iris pregenerator after " + + Math.max(1L, timeoutMs) + "ms."); + } + instance.compareAndSet(inst, null); + return true; + } + + public static PregeneratorJob getInstance() { + return instance.get(); + } + + public static boolean pauseResume() { + PregeneratorJob inst = instance.get(); + if (inst == null) { + return false; + } + + if (isPaused()) { + inst.pregenerator.resume(); + } else { + inst.pregenerator.pause(); + } + return true; + } + + public static boolean isPaused() { + PregeneratorJob inst = instance.get(); + if (inst == null) { + return true; + } + + return inst.paused(); + } + + public static double chunksPerSecond() { + PregeneratorJob inst = instance.get(); + return inst == null ? 0D : Math.max(0D, inst.lastChunksPerSecond); + } + + public static long chunksRemaining() { + PregeneratorJob inst = instance.get(); + return inst == null ? -1L : Math.max(0L, inst.lastChunksRemaining); + } + + public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond, + double overallChunksPerSecond, double thirtySecondChunksPerSecond, + double sixtySecondChunksPerSecond, long chunksRemaining, long eta, long elapsed, + String method, boolean paused, long failed, String worldName, String worldIdentity) { + } + + public static PregenProgress progressSnapshot() { + PregeneratorJob inst = instance.get(); + return inst == null ? null : inst.snapshot(); + } + + public PregenProgress snapshot() { + double percent = lastTotalChunks <= 0 ? 0D : ((double) lastGenerated / (double) lastTotalChunks) * 100D; + return new PregenProgress( + percent, + lastGenerated, + lastTotalChunks, + Math.max(0D, lastChunksPerSecond), + Math.max(0D, lastOverallChunksPerSecond), + Math.max(0D, lastThirtySecondChunksPerSecond), + Math.max(0D, lastSixtySecondChunksPerSecond), + Math.max(0L, lastChunksRemaining), + lastEta, + lastElapsed, + lastMethod, + paused(), + pregenerator.getFailedChunks(), + worldName(), + worldIdentity()); + } + + public String worldName() { + if (engine == null || engine.getWorld() == null) { + return null; + } + + return engine.getWorld().name(); + } + + public String worldIdentity() { + if (engine == null || engine.getWorld() == null) { + return null; + } + return engine.getWorld().identity(); + } + + public boolean targetsWorldIdentity(String worldIdentity) { + if (worldIdentity == null || engine == null || engine.getWorld() == null) { + return false; + } + + return worldIdentity.equals(engine.getWorld().identity()); + } + + private static Color parseColor(String c) { + String v = (c.startsWith("#") ? c : "#" + c).trim(); + try { + return Color.decode(v); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Error Parsing 'color', (" + c + ")"); + } + + return Color.RED; + } + + public Mantle getMantle() { + return pregenerator.getMantle(); + } + + public PregeneratorJob onProgress(Consumer c) { + onProgress.add(c); + return this; + } + + public PregeneratorJob whenDone(Runnable r) { + whenDone.add(r); + return this; + } + + public void drawRegion(int x, int z, Color color) { + J.a(() -> task.iterateChunks(x, z, (xx, zz) -> { + draw(xx, zz, color); + J.sleep(3); + })); + } + + public void draw(int x, int z, Color color) { + try { + PregenRenderer activeRenderer = renderer; + if (activeRenderer != null && activeRenderer.isVisibleFrame()) { + activeRenderer.submit(x, z, color); + } + } catch (Throwable error) { + IrisLogging.reportError(error); + IrisLogging.error("Failed to draw pregen"); + } + } + + public void stop() { + J.a(() -> { + pregenerator.close(); + worker.interrupt(); + close(); + instance.compareAndSet(this, null); + }); + } + + public void close() { + try { + monitor.close(); + PregenRenderer activeRenderer = renderer; + if (activeRenderer != null) { + activeRenderer.close(); + } + } catch (Throwable error) { + IrisLogging.reportError(error); + IrisLogging.error("Error closing pregen gui"); + } + } + + public void open() { + EventQueue.invokeLater(() -> { + try { + renderer = PregenRenderer.open(IrisLanguage.plain(DesktopUiMessages.PREGEN_TITLE), this, PregeneratorJob::pauseResume); + } catch (Throwable error) { + IrisLogging.reportError(error); + IrisLogging.error("Error opening pregen gui"); + } + }); + } + + @Override + public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) { + PregenRates rateSnapshot = pregenerator.getRates(); + lastChunksPerSecond = chunksPerSecond; + lastOverallChunksPerSecond = rateSnapshot.overall(); + lastThirtySecondChunksPerSecond = rateSnapshot.thirtySecond(); + lastSixtySecondChunksPerSecond = rateSnapshot.sixtySecond(); + lastChunksRemaining = chunksRemaining; + lastGenerated = generated; + lastTotalChunks = totalChunks; + lastEta = eta; + lastElapsed = elapsed; + lastMethod = method; + + info = new String[]{ + IrisLanguage.plain( + paused() ? DesktopUiMessages.PREGEN_PROGRESS_PAUSED + : saving.getAndSet(false) ? DesktopUiMessages.PREGEN_PROGRESS_SAVING : DesktopUiMessages.PREGEN_PROGRESS_GENERATING, + MessageArgument.trusted("generated", Form.f(generated)), + MessageArgument.trusted("total", Form.f(totalChunks)), + MessageArgument.trusted("percent", Form.pc(percent, 0)) + ), + IrisLanguage.plain( + cached ? DesktopUiMessages.PREGEN_SPEED_CACHED : DesktopUiMessages.PREGEN_SPEED, + MessageArgument.trusted("overall", Form.f(rateSnapshot.overall(), 1)), + MessageArgument.trusted("tenSecond", Form.f(rateSnapshot.tenSecond(), 1)), + MessageArgument.trusted("thirtySecond", Form.f(rateSnapshot.thirtySecond(), 1)), + MessageArgument.trusted("sixtySecond", Form.f(rateSnapshot.sixtySecond(), 1)) + ), + IrisLanguage.plain( + DesktopUiMessages.PREGEN_TIME, + MessageArgument.trusted("remaining", Form.duration(eta, 2)), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 2)) + ), + IrisLanguage.plain(DesktopUiMessages.PREGEN_METHOD, MessageArgument.untrusted("method", String.valueOf(method))), + IrisLanguage.plain( + DesktopUiMessages.PREGEN_MEMORY, + MessageArgument.trusted("used", Form.memSize(monitor.getUsedBytes(), 2)), + MessageArgument.trusted("usage", Form.pc(monitor.getUsagePercent(), 0)), + MessageArgument.trusted("pressure", Form.memSize(monitor.getPressure(), 0)) + ) + }; + + for (Consumer i : onProgress) { + i.accept(percent); + } + + dispatchApiPhases(apiPhases.onTick(paused())); + } + + private void dispatchApiPhases(List phases) { + if (phases.isEmpty()) { + return; + } + + PregenApiSink sink = IrisServices.getOrNull(PregenApiSink.class); + if (sink == null) { + return; + } + + PregenProgress progress = snapshot(); + for (PregenApiPhase phase : phases) { + try { + sink.pregen(phase, progress); + } catch (Throwable error) { + IrisLogging.reportError("Iris pregeneration API dispatch failed for phase " + phase + ".", error); + } + } + } + + private boolean reachedTotal() { + return lastTotalChunks > 0L && lastGenerated >= lastTotalChunks; + } + + @Override + public void onChunkGenerating(int x, int z) { + draw(x, z, COLOR_GENERATING); + } + + @Override + public void onChunkGenerated(int x, int z, boolean cached) { + if (renderer == null || !renderer.isVisibleFrame()) return; + if (service.isShutdown()) { + return; + } + service.execute(() -> { + if (engine != null) { + draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8)); + return; + } + + draw(x, z, COLOR_GENERATED); + }); + } + + @Override + public void onRegionGenerated(int x, int z) { + // No forced System.gc() here: a wall-clock full STW collection mid-generation stalled + // everything; MantleHeapPressure's 96% panic reclaim already owns heap pressure. + broadcastRegionDelta(x, z, IrisMessage.PregenRegionDelta.STATE_DONE); + } + + private void broadcastRegionDelta(int regionX, int regionZ, int state) { + IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class); + if (protocolServer == null) { + return; + } + protocolServer.broadcastPregenRegionDelta(pregenerator.getJobId(), regionX, regionZ, state); + } + + @Override + public void onRegionGenerating(int x, int z) { + broadcastRegionDelta(x, z, IrisMessage.PregenRegionDelta.STATE_GENERATING); + } + + @Override + public void onChunkCleaned(int x, int z) { + //draw(x, z, COLOR_CLEANED); + } + + @Override + public void onRegionSkipped(int x, int z) { + + } + + @Override + public void onNetworkStarted(int x, int z) { + drawRegion(x, z, COLOR_NETWORK); + } + + @Override + public void onNetworkFailed(int x, int z) { + + } + + @Override + public void onNetworkReclaim(int revert) { + + } + + @Override + public void onNetworkGeneratedChunk(int x, int z) { + draw(x, z, COLOR_NETWORK_GENERATING); + } + + @Override + public void onNetworkDownloaded(int x, int z) { + drawRegion(x, z, COLOR_NETWORK); + } + + @Override + public void onClose() { + dispatchApiPhases(apiPhases.onClose(reachedTotal())); + close(); + instance.compareAndSet(this, null); + whenDone.forEach(Runnable::run); + service.shutdownNow(); + } + + @Override + public void onSaving() { + saving.set(true); + dispatchApiPhases(apiPhases.onSaving()); + } + + @Override + public void onChunkExistsInRegionGen(int x, int z) { + if (engine != null) { + draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8)); + return; + } + + draw(x, z, COLOR_EXISTS); + } + + @Override + public Position2 max() { + return max; + } + + @Override + public Position2 min() { + return min; + } + + @Override + public boolean paused() { + return pregenerator.paused(); + } + + @Override + public String[] progress() { + return info; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java b/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java new file mode 100644 index 000000000..6aac2efa7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java @@ -0,0 +1,988 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.gui; + +import art.arcane.iris.core.localization.DesktopUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.engine.framework.render.IrisRenderer; +import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RollingSequence; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.O; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSeparator; +import javax.swing.JToggleButton; +import javax.swing.SwingConstants; +import javax.swing.event.MouseInputListener; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener { + private static final long serialVersionUID = 2094606939770332040L; + + private static final Color BG = new Color(18, 18, 22); + private static final Color CARD_BG = new Color(28, 28, 36, 220); + private static final Color CARD_BORDER = new Color(60, 60, 75, 180); + private static final Color TEXT_PRIMARY = new Color(220, 220, 230); + private static final Color TEXT_SECONDARY = new Color(140, 140, 155); + private static final Color ACCENT = new Color(90, 140, 255); + private static final Color PLAYER_COLOR = new Color(80, 200, 120); + private static final Color MOB_COLOR = new Color(220, 80, 80); + private static final Color STATUS_BG = new Color(24, 24, 30, 240); + private static final Color GRID_COLOR = new Color(255, 255, 255, 12); + private static final Font FONT_STATUS = new Font(Font.MONOSPACED, Font.PLAIN, 12); + private static final Font FONT_CARD_TITLE = new Font(Font.SANS_SERIF, Font.BOLD, 13); + private static final Font FONT_CARD_BODY = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + private static final Font FONT_HELP_KEY = new Font(Font.MONOSPACED, Font.BOLD, 12); + private static final Font FONT_NOTIFICATION = new Font(Font.SANS_SERIF, Font.BOLD, 14); + private static final int CARD_RADIUS = 8; + private static final int CARD_PAD = 12; + private static final int STATUS_HEIGHT = 26; + + private final KList lastEntities = new KList<>(); + private final KMap notifications = new KMap<>(); + private final ChronoLatch centities = new ChronoLatch(1000); + private final RollingSequence rs = new RollingSequence(512); + private final O m = new O<>(); + private final KMap positions = new KMap<>(); + private final KMap fastpositions = new KMap<>(); + private final KSet working = new KSet<>(); + private final KSet workingfast = new KSet<>(); + + private RenderType currentType = RenderType.BIOME; + private boolean help = true; + private boolean helpIgnored = false; + private boolean shift = false; + private boolean debug = false; + private boolean control = false; + private boolean eco = false; + private boolean lowtile = false; + private boolean follow = false; + private boolean alt = false; + private boolean grid = false; + private GuiMarker followMarker = null; + private IrisRenderer renderer; + private GuiOverlay overlay; + private double velocity = 0; + private int lowq = 12; + private double scale = 128; + private double mscale = 4D; + private int w = 0; + private int h = 0; + private double lx = 0; + private double lz = 0; + private double ox = 0; + private double oz = 0; + private double hx = 0; + private double hz = 0; + private double oxp = 0; + private double ozp = 0; + private Engine engine; + private int tid = 0; + private Map modeButtons; + + private final ExecutorService e = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> { + tid++; + Thread t = new Thread(r); + t.setName("Iris HD Renderer " + tid); + t.setPriority(Thread.MIN_PRIORITY); + t.setUncaughtExceptionHandler((et, ex) -> ex.printStackTrace()); + return t; + }); + + private final ExecutorService eh = Executors.newFixedThreadPool(3, r -> { + tid++; + Thread t = new Thread(r); + t.setName("Iris Renderer " + tid); + t.setPriority(Thread.NORM_PRIORITY); + t.setUncaughtExceptionHandler((et, ex) -> ex.printStackTrace()); + return t; + }); + + public VisionGUI(JFrame frame) { + PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class); + if (preservation != null) { + preservation.register(e); + preservation.register(eh); + } + m.set(8); + rs.put(1); + setBackground(BG); + addMouseWheelListener(this); + addMouseMotionListener(this); + addMouseListener(this); + frame.addKeyListener(this); + J.a(() -> { + J.sleep(10000); + if (!helpIgnored && help) { + help = false; + } + }); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent windowEvent) { + e.shutdownNow(); + eh.shutdownNow(); + } + }); + } + + public static void launch(Engine g) { + EventQueue.invokeLater(() -> createAndShowGUI(g)); + } + + private static void createAndShowGUI(Engine r) { + JFrame frame = new JFrame(IrisLanguage.plain(DesktopUiMessages.VISION_TITLE)); + GuiHost.prepareFrame(frame); + VisionGUI nv = new VisionGUI(frame); + nv.engine = r; + nv.overlay = GuiHost.get().overlayFor(r); + nv.renderer = new IrisRenderer(r); + frame.getContentPane().setBackground(BG); + frame.setLayout(new BorderLayout()); + frame.add(buildToolbar(nv), BorderLayout.NORTH); + frame.add(nv, BorderLayout.CENTER); + frame.setSize(1440, 820); + frame.setMinimumSize(new Dimension(640, 480)); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + private static JPanel buildToolbar(VisionGUI nv) { + JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 3, 3)); + toolbar.setBackground(new Color(22, 22, 28)); + toolbar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(45, 45, 55))); + + JLabel modeLabel = new JLabel(IrisLanguage.plain(DesktopUiMessages.VISION_VIEW)); + modeLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 11)); + modeLabel.setForeground(TEXT_SECONDARY); + modeLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 2)); + toolbar.add(modeLabel); + + ButtonGroup modeGroup = new ButtonGroup(); + Map modeButtons = new LinkedHashMap<>(); + for (RenderType type : RenderType.values()) { + JToggleButton btn = createToolbarToggle(modeName(type), type == nv.currentType); + btn.addActionListener(e -> { + nv.setRenderType(type); + for (Map.Entry entry : modeButtons.entrySet()) { + entry.getValue().setSelected(entry.getKey() == type); + } + }); + modeGroup.add(btn); + modeButtons.put(type, btn); + toolbar.add(btn); + } + nv.modeButtons = modeButtons; + + toolbar.add(createToolbarSeparator()); + + JToggleButton gridBtn = createToolbarToggle(IrisLanguage.plain(DesktopUiMessages.VISION_GRID), nv.grid); + gridBtn.addActionListener(e -> { nv.toggleGrid(); gridBtn.setSelected(nv.grid); }); + toolbar.add(gridBtn); + + JToggleButton followBtn = createToolbarToggle(IrisLanguage.plain(DesktopUiMessages.VISION_FOLLOW), nv.follow); + followBtn.addActionListener(e -> { nv.toggleFollow(); followBtn.setSelected(nv.follow); }); + toolbar.add(followBtn); + + JToggleButton qualityBtn = createToolbarToggle(IrisLanguage.plain(DesktopUiMessages.VISION_LOW_QUALITY_SHORT), nv.lowtile); + qualityBtn.addActionListener(e -> { nv.toggleQuality(); qualityBtn.setSelected(nv.lowtile); }); + toolbar.add(qualityBtn); + + return toolbar; + } + + private static JToggleButton createToolbarToggle(String text, boolean selected) { + JToggleButton btn = new JToggleButton(text, selected); + btn.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 11)); + btn.setFocusable(false); + btn.setForeground(new Color(170, 170, 185)); + btn.setBackground(new Color(32, 32, 40)); + btn.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(50, 50, 60)), + BorderFactory.createEmptyBorder(3, 8, 3, 8) + )); + btn.setOpaque(true); + btn.addChangeListener(e -> { + if (btn.isSelected()) { + btn.setBackground(new Color(50, 60, 85)); + btn.setForeground(Color.WHITE); + } else { + btn.setBackground(new Color(32, 32, 40)); + btn.setForeground(new Color(170, 170, 185)); + } + }); + return btn; + } + + private static JSeparator createToolbarSeparator() { + JSeparator sep = new JSeparator(SwingConstants.VERTICAL); + sep.setPreferredSize(new Dimension(1, 24)); + sep.setForeground(new Color(50, 50, 60)); + sep.setBackground(new Color(22, 22, 28)); + return sep; + } + + public boolean updateEngine() { + if (engine.isClosed() || engine.isClosing() || engine.getComplex() == null) { + try { + Engine reacquired = GuiHost.get().findActiveEngine(); + if (reacquired != null && !reacquired.isClosed()) { + engine = reacquired; + overlay = GuiHost.get().overlayFor(reacquired); + renderer = new IrisRenderer(reacquired); + return true; + } + } catch (Throwable ignored) { + } + } + return false; + } + + @Override + public void mouseMoved(MouseEvent e) { + Point cp = e.getPoint(); + lx = cp.getX(); + lz = cp.getY(); + } + + @Override + public void mouseDragged(MouseEvent e) { + Point cp = e.getPoint(); + ox += (lx - cp.getX()) * scale; + oz += (lz - cp.getY()) * scale; + lx = cp.getX(); + lz = cp.getY(); + } + + public void notify(String s) { + notifications.put(s, M.ms() + 2500); + } + + @Override + public void keyTyped(KeyEvent e) { + } + + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = true; + if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = true; + if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = true; + if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = true; helpIgnored = true; } + if (e.getKeyCode() == KeyEvent.VK_ALT) alt = true; + } + + @Override + public void keyReleased(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = false; + if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = false; + if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = false; + if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = false; helpIgnored = true; } + if (e.getKeyCode() == KeyEvent.VK_ALT) alt = false; + + if (e.getKeyCode() == KeyEvent.VK_F) { toggleFollow(); return; } + if (e.getKeyCode() == KeyEvent.VK_R) { dump(); notify(IrisLanguage.plain(DesktopUiMessages.VISION_REFRESHING)); return; } + if (e.getKeyCode() == KeyEvent.VK_P) { toggleQuality(); return; } + if (e.getKeyCode() == KeyEvent.VK_E) { eco = !eco; dump(); notify(IrisLanguage.plain(DesktopUiMessages.VISION_FPS, MessageArgument.trusted("fps", eco ? 30 : 60))); return; } + if (e.getKeyCode() == KeyEvent.VK_G) { toggleGrid(); return; } + + if (e.getKeyCode() == KeyEvent.VK_EQUALS) { + mscale = mscale + ((0.044 * mscale) * -3); + mscale = Math.max(mscale, 0.00001); + dump(); + return; + } + if (e.getKeyCode() == KeyEvent.VK_MINUS) { + mscale = mscale + ((0.044 * mscale) * 3); + mscale = Math.max(mscale, 0.00001); + dump(); + return; + } + if (e.getKeyCode() == KeyEvent.VK_BACK_SLASH) { + mscale = 1D; + dump(); + notify(IrisLanguage.plain(DesktopUiMessages.VISION_ZOOM_RESET)); + return; + } + + int currentMode = currentType.ordinal(); + for (RenderType i : RenderType.values()) { + if (e.getKeyChar() == String.valueOf(i.ordinal() + 1).charAt(0)) { + if (i.ordinal() != currentMode) { + setRenderType(i); + syncModeButtons(); + return; + } + } + } + + if (e.getKeyCode() == KeyEvent.VK_M) { + setRenderType(RenderType.values()[(currentMode + 1) % RenderType.values().length]); + syncModeButtons(); + } + } + + private static String modeName(RenderType type) { + return IrisLanguage.plain(switch (type) { + case BIOME -> DesktopUiMessages.VISION_MODE_BIOME; + case BIOME_LAND -> DesktopUiMessages.VISION_MODE_BIOME_LAND; + case BIOME_SEA -> DesktopUiMessages.VISION_MODE_BIOME_SEA; + case REGION -> DesktopUiMessages.VISION_MODE_REGION; + case CAVE_LAND -> DesktopUiMessages.VISION_MODE_CAVE_LAND; + case HEIGHT -> DesktopUiMessages.VISION_MODE_HEIGHT; + case OBJECT_LOAD -> DesktopUiMessages.VISION_MODE_OBJECT_LOAD; + case DECORATOR_LOAD -> DesktopUiMessages.VISION_MODE_DECORATOR_LOAD; + case CONTINENT -> DesktopUiMessages.VISION_MODE_CONTINENT; + case LAYER_LOAD -> DesktopUiMessages.VISION_MODE_LAYER_LOAD; + }); + } + + void setRenderType(RenderType type) { + currentType = type; + dump(); + notify(modeName(type)); + } + + void toggleGrid() { + grid = !grid; + notify(IrisLanguage.plain(grid ? DesktopUiMessages.VISION_GRID_ENABLED : DesktopUiMessages.VISION_GRID_DISABLED)); + } + + void toggleFollow() { + follow = !follow; + if (followMarker != null && follow) { + notify(IrisLanguage.plain(DesktopUiMessages.VISION_FOLLOWING, MessageArgument.untrusted("player", followMarker.label()))); + } else if (follow) { + notify(IrisLanguage.plain(DesktopUiMessages.VISION_NO_PLAYER)); + follow = false; + } else { + notify(IrisLanguage.plain(DesktopUiMessages.VISION_FOLLOW_DISABLED)); + } + } + + void toggleQuality() { + lowtile = !lowtile; + dump(); + notify(IrisLanguage.plain(lowtile ? DesktopUiMessages.VISION_LOW_QUALITY : DesktopUiMessages.VISION_HIGH_QUALITY)); + } + + private void syncModeButtons() { + if (modeButtons == null) return; + for (Map.Entry entry : modeButtons.entrySet()) { + entry.getValue().setSelected(entry.getKey() == currentType); + } + } + + private void dump() { + positions.clear(); + fastpositions.clear(); + } + + public BufferedImage getTile(KSet fg, int div, int x, int z, O m) { + BlockPosition key = new BlockPosition((int) mscale, Math.floorDiv(x, div), Math.floorDiv(z, div)); + fg.add(key); + + if (positions.containsKey(key)) { + return positions.get(key); + } + + if (fastpositions.containsKey(key)) { + if (!working.contains(key) && working.size() < 9) { + m.set(m.get() - 1); + if (m.get() >= 0 && velocity < 50) { + working.add(key); + double mk = mscale; + double mkd = scale; + e.submit(() -> { + // finally: a render throw is swallowed by the discarded Future, and a + // leaked key permanently blocks the admission gate (working < 9). + try { + PrecisionStopwatch ps = PrecisionStopwatch.start(); + BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / (lowtile ? 3 : 1), currentType); + rs.put(ps.getMilliseconds()); + if (mk == mscale && mkd == scale) { + positions.put(key, b); + } + } catch (Throwable ex) { + IrisLogging.debug("Vision tile render failed: " + ex.getClass().getSimpleName() + ": " + ex.getMessage()); + } finally { + working.remove(key); + } + }); + } + } + return fastpositions.get(key); + } + + if (workingfast.contains(key) || workingfast.size() > Runtime.getRuntime().availableProcessors()) { + return null; + } + + workingfast.add(key); + double mk = mscale; + double mkd = scale; + eh.submit(() -> { + try { + PrecisionStopwatch ps = PrecisionStopwatch.start(); + BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / lowq, currentType); + rs.put(ps.getMilliseconds()); + if (mk == mscale && mkd == scale) { + fastpositions.put(key, b); + } + } catch (Throwable ex) { + IrisLogging.debug("Vision tile render failed: " + ex.getClass().getSimpleName() + ": " + ex.getMessage()); + } finally { + workingfast.remove(key); + } + }); + return null; + } + + private double getWorldX(double screenX) { + return (mscale * screenX) + ((oxp / scale) * mscale); + } + + private double getWorldZ(double screenZ) { + return (mscale * screenZ) + ((ozp / scale) * mscale); + } + + private double getScreenX(double x) { + return (x / mscale) - (oxp / scale); + } + + private double getScreenZ(double z) { + return (z / mscale) - (ozp / scale); + } + + private double lerp(double current, double target, double speed) { + double diff = target - current; + if (Math.abs(diff) < 0.5) return target; + return current + diff * speed; + } + + @Override + public void paint(Graphics gx) { + // Cover the closing window too: releaseRuntime nulls the complex BEFORE closed flips, + // and a paint NPE in that window killed the self-driven repaint loop forever. + if ((engine.isClosed() || engine.isClosing() || engine.getComplex() == null) && !updateEngine()) { + EventQueue.invokeLater(() -> { + try { setVisible(false); } catch (Throwable ignored) { } + }); + return; + } + double frameMs = 0; + try { + frameMs = paintBody(gx); + } catch (Throwable e) { + IrisLogging.debug("Vision paint failed: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } finally { + // The repaint loop is entirely self-driven from here; it must survive any render + // exception or the window freezes on a stale frame permanently. Frame-time + // compensated so a slow frame does not stack a full sleep on top of itself. + long sleepMs = Math.max(1, (eco ? 32 : 16) - (long) frameMs); + J.a(() -> { + J.sleep(sleepMs); + repaint(); + }); + } + } + + private double paintBody(Graphics gx) { + + velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36; + oxp = lerp(oxp, ox, 0.36); + ozp = lerp(ozp, oz, 0.36); + hx = lerp(hx, lx, 0.36); + hz = lerp(hz, lz, 0.36); + + if (centities.flip() && overlay != null) { + overlay.requestEntities((List next) -> { + synchronized (lastEntities) { + lastEntities.clear(); + if (next != null) { + lastEntities.addAll(next); + } + } + }); + } + + lowq = Math.max(Math.min((int) M.lerp(8, 28, velocity / 1000D), 28), 8); + PrecisionStopwatch p = PrecisionStopwatch.start(); + Graphics2D g = (Graphics2D) gx; + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + w = getWidth(); + h = getHeight(); + double vscale = scale; + scale = w / 12D; + + if (scale != vscale) { + positions.clear(); + } + + KSet gg = new KSet<>(); + int iscale = (int) scale; + g.setColor(BG); + g.fillRect(0, 0, w, h); + double offsetX = oxp / scale; + double offsetZ = ozp / scale; + m.set(3); + + for (int r = 0; r < Math.max(w, h); r += iscale) { + for (int i = -iscale; i < w + iscale; i += iscale) { + for (int j = -iscale; j < h + iscale; j += iscale) { + int a = i - (w / 2); + int b = j - (h / 2); + if (a * a + b * b <= r * r) { + int tx = (int) (Math.floor((offsetX + i) / iscale) * iscale); + int tz = (int) (Math.floor((offsetZ + j) / iscale) * iscale); + BufferedImage t = getTile(gg, iscale, tx, tz, m); + + if (t != null) { + int rx = Math.floorMod((int) Math.floor(offsetX), iscale); + int rz = Math.floorMod((int) Math.floor(offsetZ), iscale); + g.drawImage(t, i - rx, j - rz, iscale, iscale, null); + } + } + } + } + } + + if (grid) { + renderGrid(g, iscale, offsetX, offsetZ); + } + + p.end(); + + for (BlockPosition i : positions.k()) { + if (!gg.contains(i)) { + positions.remove(i); + } + } + + // Bounded, not pruned-to-frame: the low-quality cache is the pan placeholder, but + // unbounded growth while panning leaked one BufferedImage per visited tile forever. + if (fastpositions.size() > 4096) { + for (BlockPosition i : fastpositions.k()) { + if (!gg.contains(i)) { + fastpositions.remove(i); + } + } + } + + handleFollow(); + renderOverlays(g, p.getMilliseconds()); + return p.getMilliseconds(); + } + + private void renderGrid(Graphics2D g, int tileSize, double offsetX, double offsetZ) { + g.setColor(GRID_COLOR); + int rx = Math.floorMod((int) Math.floor(offsetX), tileSize); + int rz = Math.floorMod((int) Math.floor(offsetZ), tileSize); + for (int i = -tileSize; i < w + tileSize; i += tileSize) { + g.drawLine(i - rx, 0, i - rx, h); + } + for (int j = -tileSize; j < h + tileSize; j += tileSize) { + g.drawLine(0, j - rz, w, j - rz); + } + } + + private void handleFollow() { + if (follow && followMarker != null) { + animateTo(followMarker.worldX(), followMarker.worldZ()); + } + } + + private void renderOverlays(Graphics2D g, double frameMs) { + renderEntities(g); + + if (help) { + renderOverlayHelp(g); + } else if (debug) { + renderOverlayDebug(g); + } + + renderStatusBar(g, frameMs); + renderHoverOverlay(g, shift); + + if (!notifications.isEmpty()) { + renderNotification(g); + } + } + + private void renderStatusBar(Graphics2D g, double frameMs) { + int y = h - STATUS_HEIGHT; + g.setColor(STATUS_BG); + g.fillRect(0, y, w, STATUS_HEIGHT); + g.setColor(CARD_BORDER); + g.drawLine(0, y, w, y); + + g.setFont(FONT_STATUS); + g.setColor(TEXT_SECONDARY); + + double wx = getWorldX(w / 2.0); + double wz = getWorldZ(h / 2.0); + int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0; + + String left = IrisLanguage.plain( + DesktopUiMessages.VISION_STATUS_LEFT, + MessageArgument.trusted("mode", modeName(currentType)), + MessageArgument.trusted("bpp", Form.f(mscale, 1)), + MessageArgument.trusted("width", Form.f((int) (mscale * w))), + MessageArgument.trusted("height", Form.f((int) (mscale * h))) + ); + g.drawString(left, 8, y + 17); + + String right = IrisLanguage.plain( + DesktopUiMessages.VISION_STATUS_RIGHT, + MessageArgument.trusted("x", Form.f((int) wx)), + MessageArgument.trusted("z", Form.f((int) wz)), + MessageArgument.trusted("fps", fps) + ); + int rw = g.getFontMetrics().stringWidth(right); + g.drawString(right, w - rw - 8, y + 17); + + g.setColor(ACCENT); + g.fillRect(0, y + 1, 3, STATUS_HEIGHT - 1); + } + + private void renderEntities(Graphics2D g) { + GuiMarker firstPlayer = null; + + List players = overlay == null ? List.of() : overlay.players(); + if (players != null) { + for (GuiMarker i : players) { + firstPlayer = i; + renderPlayerMarker(g, i.worldX(), i.worldZ(), i.label()); + } + } + + synchronized (lastEntities) { + double dist = Double.MAX_VALUE; + GuiMarker nearest = null; + + for (GuiMarker i : lastEntities) { + renderMobMarker(g, i.worldX(), i.worldZ()); + if (shift) { + double dx = i.worldX() - getWorldX(hx); + double dz = i.worldZ() - getWorldZ(hz); + double d = dx * dx + dz * dz; + if (d < dist) { + dist = d; + nearest = i; + } + } + } + + if (nearest != null && shift) { + double sx = getScreenX(nearest.worldX()); + double sz = getScreenZ(nearest.worldZ()); + g.setColor(MOB_COLOR); + g.fillOval((int) sx - 6, (int) sz - 6, 12, 12); + g.setColor(new Color(220, 80, 80, 60)); + g.fillOval((int) sx - 10, (int) sz - 10, 20, 20); + + KList k = new KList<>(); + k.add(nearest.label()); + k.add(IrisLanguage.plain( + DesktopUiMessages.VISION_ENTITY_POSITION, + MessageArgument.trusted("x", (int) nearest.worldX()), + MessageArgument.trusted("y", (int) nearest.worldY()), + MessageArgument.trusted("z", (int) nearest.worldZ()) + )); + if (nearest.maxHealth() > 0) { + k.add(IrisLanguage.plain( + DesktopUiMessages.VISION_ENTITY_HEALTH, + MessageArgument.trusted("health", Form.f(nearest.health(), 1)), + MessageArgument.trusted("maximum", Form.f(nearest.maxHealth(), 1)) + )); + } + drawCard(w - CARD_PAD, CARD_PAD, 1, 0, g, k); + } + } + + followMarker = firstPlayer; + } + + private void renderPlayerMarker(Graphics2D g, double x, double z, String name) { + int sx = (int) getScreenX(x); + int sz = (int) getScreenZ(z); + g.setColor(new Color(80, 200, 120, 40)); + g.fillOval(sx - 12, sz - 12, 24, 24); + g.setColor(PLAYER_COLOR); + g.fillOval(sx - 5, sz - 5, 10, 10); + g.setColor(new Color(40, 160, 80)); + g.drawOval(sx - 5, sz - 5, 10, 10); + + g.setFont(FONT_CARD_BODY); + g.setColor(TEXT_PRIMARY); + int nw = g.getFontMetrics().stringWidth(name); + g.drawString(name, sx - nw / 2, sz - 14); + } + + private void renderMobMarker(Graphics2D g, double x, double z) { + int sx = (int) getScreenX(x); + int sz = (int) getScreenZ(z); + g.setColor(MOB_COLOR); + g.fillRect(sx - 2, sz - 2, 4, 4); + } + + private void animateTo(double wx, double wz) { + double cx = getWorldX(getWidth() / 2.0); + double cz = getWorldZ(getHeight() / 2.0); + ox += ((wx - cx) / mscale) * scale; + oz += ((wz - cz) / mscale) * scale; + } + + private void renderHoverOverlay(Graphics2D g, boolean detailed) { + IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)); + IrisRegion region = engine.getComplex().getRegionStream().get(getWorldX(hx), getWorldZ(hz)); + KList l = new KList<>(); + l.add(biome.getName()); + l.add(region.getName()); + l.add(IrisLanguage.plain(DesktopUiMessages.VISION_BLOCK_POSITION, MessageArgument.trusted("x", (int) getWorldX(hx)), MessageArgument.trusted("z", (int) getWorldZ(hz)))); + if (detailed) { + l.add(IrisLanguage.plain(DesktopUiMessages.VISION_CHUNK_POSITION, MessageArgument.trusted("x", (int) getWorldX(hx) >> 4), MessageArgument.trusted("z", (int) getWorldZ(hz) >> 4))); + l.add(IrisLanguage.plain(DesktopUiMessages.VISION_REGION_POSITION, MessageArgument.trusted("x", (int) getWorldX(hx) >> 9), MessageArgument.trusted("z", (int) getWorldZ(hz) >> 9))); + l.add(IrisLanguage.plain(DesktopUiMessages.VISION_BIOME_KEY, MessageArgument.untrusted("key", biome.getLoadKey()))); + l.add(IrisLanguage.plain(DesktopUiMessages.VISION_BIOME_FILE, MessageArgument.untrusted("file", String.valueOf(biome.getLoadFile())))); + } + drawCard((float) hx + 16, (float) hz, 0, 0, g, l); + } + + private void renderOverlayDebug(Graphics2D g) { + KList l = new KList<>(); + l.add(IrisLanguage.plain(DesktopUiMessages.VISION_VELOCITY, MessageArgument.trusted("velocity", (int) velocity))); + l.add(IrisLanguage.plain(DesktopUiMessages.VISION_TILES, MessageArgument.trusted("high", positions.size()), MessageArgument.trusted("low", fastpositions.size()))); + l.add(IrisLanguage.plain(DesktopUiMessages.VISION_WORKERS, MessageArgument.trusted("high", working.size()), MessageArgument.trusted("low", workingfast.size()))); + l.add(IrisLanguage.plain(DesktopUiMessages.VISION_CENTER, MessageArgument.trusted("x", Form.f((int) getWorldX(getWidth() / 2.0))), MessageArgument.trusted("z", Form.f((int) getWorldZ(getHeight() / 2.0))))); + drawCard(CARD_PAD, h - STATUS_HEIGHT - CARD_PAD, 0, 1, g, l); + } + + private void renderOverlayHelp(Graphics2D g) { + KList keys = new KList<>(); + KList descs = new KList<>(); + keys.add("/"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_TOGGLE)); + keys.add("R"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_REFRESH)); + keys.add("F"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_FOLLOW)); + keys.add("+/-"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_ZOOM)); + keys.add("\\"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_RESET_ZOOM)); + keys.add("M"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_CYCLE_MODE)); + keys.add("P"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_QUALITY)); + keys.add("E"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_FPS)); + keys.add("G"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_GRID)); + + int ff = 0; + for (RenderType i : RenderType.values()) { + ff++; + keys.add(String.valueOf(ff)); + descs.add(modeName(i)); + } + + keys.add("Shift"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_BIOME)); + keys.add("Ctrl+Click"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_TELEPORT)); + keys.add("Alt+Click"); descs.add(IrisLanguage.plain(DesktopUiMessages.VISION_HELP_EDITOR)); + + int maxKeyW = 0; + g.setFont(FONT_HELP_KEY); + for (String k : keys) { + maxKeyW = Math.max(maxKeyW, g.getFontMetrics().stringWidth(k)); + } + + int lineH = 20; + int totalH = keys.size() * lineH + CARD_PAD * 2 + 4; + int totalW = maxKeyW + 180 + CARD_PAD * 2; + + drawCardBackground(g, CARD_PAD, CARD_PAD, totalW, totalH); + + for (int i = 0; i < keys.size(); i++) { + int y = CARD_PAD + 16 + i * lineH; + + g.setFont(FONT_HELP_KEY); + g.setColor(ACCENT); + g.drawString(keys.get(i), CARD_PAD * 2, y); + + g.setFont(FONT_CARD_BODY); + g.setColor(TEXT_SECONDARY); + g.drawString(descs.get(i), CARD_PAD * 2 + maxKeyW + 16, y); + } + } + + private void renderNotification(Graphics2D g) { + int y = h - STATUS_HEIGHT - 50; + g.setFont(FONT_NOTIFICATION); + + KList active = new KList<>(); + for (String i : notifications.k()) { + if (M.ms() > notifications.get(i)) { + notifications.remove(i); + } else { + active.add(i); + } + } + + if (active.isEmpty()) return; + + String text = String.join(" | ", active); + int tw = g.getFontMetrics().stringWidth(text); + int th = g.getFontMetrics().getHeight(); + int px = (w - tw) / 2 - 16; + int py = y - th / 2 - 8; + int bw = tw + 32; + int bh = th + 16; + + drawCardBackground(g, px, py, bw, bh); + g.setColor(TEXT_PRIMARY); + g.drawString(text, px + 16, py + th + 4); + } + + private void drawCardBackground(Graphics2D g, int x, int y, int w, int h) { + RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, w, h, CARD_RADIUS, CARD_RADIUS); + g.setColor(CARD_BG); + g.fill(rect); + g.setColor(CARD_BORDER); + g.draw(rect); + } + + private void drawCard(float x, float y, double pushX, double pushZ, Graphics2D g, KList text) { + g.setFont(FONT_CARD_BODY); + int lineH = g.getFontMetrics().getHeight(); + int cardW = 0; + for (String i : text) { + cardW = Math.max(cardW, g.getFontMetrics().stringWidth(i)); + } + cardW += CARD_PAD * 2; + int cardH = text.size() * lineH + CARD_PAD * 2 - 4; + + int cx = (int) (x - cardW * pushX); + int cy = (int) (y - cardH * pushZ); + + drawCardBackground(g, cx, cy, cardW, cardH); + + int ty = cy + CARD_PAD + lineH - 4; + for (int i = 0; i < text.size(); i++) { + g.setColor(i == 0 ? TEXT_PRIMARY : TEXT_SECONDARY); + g.setFont(i == 0 ? FONT_CARD_TITLE : FONT_CARD_BODY); + g.drawString(text.get(i), cx + CARD_PAD, ty + i * lineH); + } + } + + public void mouseWheelMoved(MouseWheelEvent e) { + int notches = e.getWheelRotation(); + if (e.isControlDown()) return; + + double m0 = mscale; + double m1 = m0 + ((0.25 * m0) * notches); + m1 = Math.max(m1, 0.00001); + if (m1 == m0) return; + + positions.clear(); + fastpositions.clear(); + + Point p = e.getPoint(); + double sx = p.getX(); + double sz = p.getY(); + + double newOxp = scale * ((m0 / m1) * (sx + (oxp / scale)) - sx); + double newOzp = scale * ((m0 / m1) * (sz + (ozp / scale)) - sz); + + mscale = m1; + oxp = newOxp; + ozp = newOzp; + ox = oxp; + oz = ozp; + } + + @Override + public void mouseClicked(MouseEvent e) { + if (control) teleport(); + else if (alt) open(); + } + + @Override public void mousePressed(MouseEvent e) { } + @Override public void mouseReleased(MouseEvent e) { } + @Override public void mouseEntered(MouseEvent e) { } + @Override public void mouseExited(MouseEvent e) { } + + private void open() { + if (overlay == null) { + return; + } + String opened = overlay.openInEditor(getWorldX(hx), getWorldZ(hz), currentType); + if (opened != null) { + notify(IrisLanguage.plain(DesktopUiMessages.VISION_OPENED, MessageArgument.untrusted("target", opened))); + } + } + + private void teleport() { + if (overlay == null) { + notify(IrisLanguage.plain(DesktopUiMessages.VISION_NO_PLAYER)); + return; + } + int xx = (int) getWorldX(hx); + int zz = (int) getWorldZ(hz); + overlay.teleport(xx, zz); + notify(IrisLanguage.plain(DesktopUiMessages.VISION_TELEPORTING, MessageArgument.trusted("x", xx), MessageArgument.trusted("z", zz))); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java new file mode 100644 index 000000000..291842f50 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java @@ -0,0 +1,56 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.World; +import org.bukkit.WorldCreator; + +import java.util.concurrent.CompletableFuture; + +final class BukkitPublicBackend implements WorldLifecycleBackend { + private final CapabilitySnapshot capabilities; + + BukkitPublicBackend(CapabilitySnapshot capabilities) { + this.capabilities = capabilities; + } + + @Override + public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) { + return true; + } + + @Override + public CompletableFuture create(WorldLifecycleRequest request) { + World existing = WorldIdentity.resolve(request.worldKey()).orElse(null); + if (existing != null) { + return CompletableFuture.completedFuture(existing); + } + + WorldCreator creator = request.toWorldCreator(); + if (request.generator() != null) { + WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider()); + WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator()); + } + + INMS.get().ensureServerLevelInjection(); + + try { + World world = creator.createWorld(); + return CompletableFuture.completedFuture(world); + } catch (Throwable e) { + return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e)); + } finally { + WorldLifecycleStaging.clearAll(request.worldName()); + } + } + + @Override + public CompletableFuture unloadAsync(World world, boolean save) { + return WorldLifecycleSupport.unloadWorldAsync(capabilities, world, save); + } + + @Override + public String backendName() { + return "bukkit_public"; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitStartupPaths.java b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitStartupPaths.java new file mode 100644 index 000000000..462c3d870 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitStartupPaths.java @@ -0,0 +1,370 @@ +package art.arcane.iris.core.lifecycle; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; + +public final class BukkitStartupPaths { + private static final String DEFAULT_BUKKIT_CONFIGURATION = "bukkit.yml"; + private static final String DEFAULT_LEVEL_NAME = "world"; + private static final String DEFAULT_SERVER_PROPERTIES = "server.properties"; + private static final String DEFAULT_WORLD_CONTAINER = "."; + + private final Path bukkitConfiguration; + private final Path levelRoot; + private final String levelName; + private final Path serverProperties; + private final Path serverRoot; + private final Path worldContainer; + + private BukkitStartupPaths( + Path serverRoot, + Path serverProperties, + Path bukkitConfiguration, + Path worldContainer, + String levelName, + Path levelRoot + ) { + this.serverRoot = Objects.requireNonNull(serverRoot, "serverRoot"); + this.serverProperties = Objects.requireNonNull(serverProperties, "serverProperties"); + this.bukkitConfiguration = Objects.requireNonNull(bukkitConfiguration, "bukkitConfiguration"); + this.worldContainer = Objects.requireNonNull(worldContainer, "worldContainer"); + this.levelName = Objects.requireNonNull(levelName, "levelName"); + this.levelRoot = Objects.requireNonNull(levelRoot, "levelRoot"); + } + + public static BukkitStartupPaths resolveCurrent() throws IOException { + return resolve(Path.of(""), currentArguments()); + } + + public static BukkitStartupPaths resolve(Path serverWorkingDirectory) throws IOException { + return resolve(serverWorkingDirectory, currentArguments()); + } + + public static BukkitStartupPaths resolve(Path serverWorkingDirectory, String[] processArguments) + throws IOException { + Path serverRoot = canonicalDirectory( + Objects.requireNonNull(serverWorkingDirectory, "serverWorkingDirectory").toAbsolutePath().normalize(), + "Server working directory" + ); + StartupArguments arguments = parseArguments(processArguments); + Path serverProperties = resolveAgainstServerRoot( + serverRoot, + arguments.serverProperties(), + DEFAULT_SERVER_PROPERTIES, + "server properties" + ); + Path bukkitConfiguration = resolveAgainstServerRoot( + serverRoot, + arguments.bukkitConfiguration(), + DEFAULT_BUKKIT_CONFIGURATION, + "Bukkit configuration" + ); + String levelName = effectiveLevelName(serverProperties, arguments.levelName()); + Path worldContainer = canonicalPotentialDirectory( + resolveAgainstServerRoot( + serverRoot, + arguments.worldContainer() == null + ? BukkitWorldConfiguration.readWorldContainer(bukkitConfiguration.toFile()) + : arguments.worldContainer(), + DEFAULT_WORLD_CONTAINER, + "world container" + ), + "Configured world container" + ); + Path configuredLevel = parsePath(levelName, "level name"); + Path levelRoot = canonicalPotentialDirectory( + configuredLevel.isAbsolute() + ? configuredLevel + : worldContainer.resolve(configuredLevel), + "Configured level root" + ); + return new BukkitStartupPaths( + serverRoot, + serverProperties, + bukkitConfiguration, + worldContainer, + levelName, + levelRoot + ); + } + + public Path bukkitConfiguration() { + return bukkitConfiguration; + } + + public Path levelRoot() { + return levelRoot; + } + + public String levelName() { + return levelName; + } + + public Path serverProperties() { + return serverProperties; + } + + public Path serverRoot() { + return serverRoot; + } + + public Path worldContainer() { + return worldContainer; + } + + private static String[] currentArguments() { + return applicationArguments(ProcessHandle.current().info().arguments().orElse(new String[0])); + } + + static String[] applicationArguments(String[] processArguments) { + String[] arguments = Objects.requireNonNull(processArguments, "processArguments"); + for (int index = 0; index < arguments.length; index++) { + if (arguments[index].equals("-jar") + || arguments[index].equals("-m") + || arguments[index].equals("--module")) { + int applicationStart = Math.min(arguments.length, index + 2); + return Arrays.copyOfRange(arguments, applicationStart, arguments.length); + } + } + Set optionsWithValues = Set.of( + "-cp", + "-classpath", + "--class-path", + "-p", + "--module-path", + "--upgrade-module-path", + "--add-modules", + "--enable-native-access", + "--limit-modules", + "--add-exports", + "--add-opens", + "--add-reads", + "--patch-module", + "--source" + ); + for (int index = 0; index < arguments.length; index++) { + String argument = arguments[index]; + if (optionsWithValues.contains(argument)) { + index++; + continue; + } + if (argument.startsWith("-") || argument.startsWith("@")) { + continue; + } + return Arrays.copyOfRange(arguments, index + 1, arguments.length); + } + return new String[0]; + } + + private static StartupArguments parseArguments(String[] processArguments) throws IOException { + String[] arguments = Objects.requireNonNull(processArguments, "processArguments"); + String serverProperties = null; + String bukkitConfiguration = null; + String levelName = null; + String worldContainer = null; + for (int index = 0; index < arguments.length; index++) { + String argument = Objects.requireNonNull(arguments[index], "process argument"); + if (argument.equals("--")) { + break; + } + ParsedArgument parsed = parseArgument( + argument, + index + 1 < arguments.length ? arguments[index + 1] : null + ); + if (parsed == null) { + continue; + } + switch (parsed.kind()) { + case SERVER_PROPERTIES -> serverProperties = parsed.value(); + case BUKKIT_CONFIGURATION -> bukkitConfiguration = parsed.value(); + case LEVEL_NAME -> levelName = parsed.value(); + case WORLD_CONTAINER -> worldContainer = parsed.value(); + } + if (parsed.consumedFollowing()) { + index++; + } + } + return new StartupArguments(serverProperties, bukkitConfiguration, levelName, worldContainer); + } + + private static ParsedArgument parseArgument(String argument, String following) throws IOException { + ParsedArgument parsed = parseArgument( + argument, + following, + ArgumentKind.SERVER_PROPERTIES, + "-c", + "--config" + ); + if (parsed != null) { + return parsed; + } + parsed = parseArgument( + argument, + following, + ArgumentKind.BUKKIT_CONFIGURATION, + "-b", + "--bukkit-settings" + ); + if (parsed != null) { + return parsed; + } + parsed = parseArgument( + argument, + following, + ArgumentKind.WORLD_CONTAINER, + "-W", + "--world-dir", + "--universe", + "--world-container" + ); + if (parsed != null) { + return parsed; + } + return parseArgument( + argument, + following, + ArgumentKind.LEVEL_NAME, + "-w", + "--world", + "--level-name" + ); + } + + private static ParsedArgument parseArgument( + String argument, + String following, + ArgumentKind kind, + String... keys + ) throws IOException { + for (String key : keys) { + if (argument.equals(key)) { + if (following == null || following.isBlank()) { + throw new IOException("Startup argument " + key + " requires a value"); + } + return new ParsedArgument(kind, following, true); + } + if (key.length() == 2 && argument.startsWith(key) && argument.length() > key.length()) { + String value = argument.substring(key.length()); + if (value.startsWith("=")) { + value = value.substring(1); + } + if (value.isBlank()) { + throw new IOException("Startup argument " + key + " requires a value"); + } + return new ParsedArgument(kind, value, false); + } + String prefix = key + "="; + if (argument.startsWith(prefix)) { + String value = argument.substring(prefix.length()); + if (value.isBlank()) { + throw new IOException("Startup argument " + key + " requires a value"); + } + return new ParsedArgument(kind, value, false); + } + } + return null; + } + + private static Path resolveAgainstServerRoot( + Path serverRoot, + String configuredValue, + String defaultValue, + String label + ) throws IOException { + String value = configuredValue == null ? defaultValue : configuredValue; + Path configured = parsePath(value, label); + return configured.isAbsolute() + ? configured.normalize() + : serverRoot.resolve(configured).normalize(); + } + + private static Path parsePath(String value, String label) throws IOException { + if (value == null || value.isBlank()) { + throw new IOException("Configured " + label + " is empty"); + } + try { + return Path.of(value); + } catch (InvalidPathException exception) { + throw new IOException("Configured " + label + " is invalid", exception); + } + } + + private static String effectiveLevelName(Path serverProperties, String argumentOverride) throws IOException { + if (argumentOverride != null) { + return requireValue(argumentOverride, "level name"); + } + if (!Files.exists(serverProperties, LinkOption.NOFOLLOW_LINKS)) { + return DEFAULT_LEVEL_NAME; + } + if (!Files.isRegularFile(serverProperties)) { + throw new IOException("Configured server properties is not a regular file: " + serverProperties); + } + Properties properties = new Properties(); + try (InputStream input = Files.newInputStream(serverProperties)) { + properties.load(input); + } + return requireValue(properties.getProperty("level-name", DEFAULT_LEVEL_NAME), "level name"); + } + + private static String requireValue(String value, String label) throws IOException { + if (value == null || value.isBlank()) { + throw new IOException("Configured " + label + " is empty"); + } + return value; + } + + private static Path canonicalDirectory(Path path, String label) throws IOException { + Path canonical = path.toRealPath(); + if (!Files.isDirectory(canonical)) { + throw new IOException(label + " is not a directory: " + path); + } + return canonical; + } + + private static Path canonicalPotentialDirectory(Path path, String label) throws IOException { + Path normalized = path.toAbsolutePath().normalize(); + ArrayList missing = new ArrayList<>(); + Path existing = normalized; + while (!Files.exists(existing, LinkOption.NOFOLLOW_LINKS)) { + Path fileName = existing.getFileName(); + Path parent = existing.getParent(); + if (fileName == null || parent == null) { + throw new IOException(label + " has no existing filesystem ancestor: " + normalized); + } + missing.add(fileName); + existing = parent; + } + Path canonical = canonicalDirectory(existing, label); + for (int index = missing.size() - 1; index >= 0; index--) { + canonical = canonical.resolve(missing.get(index)); + } + return canonical.normalize(); + } + + private enum ArgumentKind { + SERVER_PROPERTIES, + BUKKIT_CONFIGURATION, + LEVEL_NAME, + WORLD_CONTAINER + } + + private record ParsedArgument(ArgumentKind kind, String value, boolean consumedFollowing) { + } + + private record StartupArguments( + String serverProperties, + String bukkitConfiguration, + String levelName, + String worldContainer + ) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitWorldConfiguration.java b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitWorldConfiguration.java new file mode 100644 index 000000000..573c08f83 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitWorldConfiguration.java @@ -0,0 +1,819 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.WorldSlotKey; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public final class BukkitWorldConfiguration { + private static final String DEFAULT_WORLD_CONTAINER = "."; + private static final Object MUTATION_LOCK = new Object(); + + private BukkitWorldConfiguration() { + } + + public static Registration register(File configurationFile, String worldName, String dimension, Long seed) throws IOException { + Objects.requireNonNull(configurationFile, "configurationFile"); + String requiredWorldName = requireWorldName(worldName); + String requiredDimension = requireName(dimension, "Dimension"); + synchronized (MUTATION_LOCK) { + YamlConfiguration configuration = load(configurationFile); + ConfigurationSection worlds = configuration.getConfigurationSection("worlds"); + if (worlds == null) { + worlds = configuration.createSection("worlds"); + } + + ConfigurationSection existing = worlds.getConfigurationSection(requiredWorldName); + String generator = "Iris:" + requiredDimension; + if (existing != null) { + String existingGenerator = existing.getString("generator"); + Long existingSeed = existing.contains("seed") ? existing.getLong("seed") : null; + if (!generator.equals(existingGenerator) || !Objects.equals(seed, existingSeed)) { + throw new IOException("bukkit.yml already contains a different definition for world \"" + + requiredWorldName + "\"."); + } + return Registration.UNCHANGED; + } + + ConfigurationSection created = worlds.createSection(requiredWorldName); + created.set("generator", generator); + if (seed != null) { + created.set("seed", seed); + } + saveAtomic(configurationFile.toPath(), configuration); + return Registration.CREATED; + } + } + + public static WorldGeneratorSnapshot snapshot(File configurationFile, String worldName) throws IOException { + Objects.requireNonNull(configurationFile, "configurationFile"); + String requiredWorldName = requireWorldName(worldName); + synchronized (MUTATION_LOCK) { + return snapshot(load(configurationFile), requiredWorldName); + } + } + + public static String readWorldContainer(File configurationFile) throws IOException { + File requiredConfigurationFile = Objects.requireNonNull(configurationFile, "configurationFile"); + Path configurationPath = requiredConfigurationFile.toPath(); + if (!Files.exists(configurationPath, LinkOption.NOFOLLOW_LINKS)) { + return DEFAULT_WORLD_CONTAINER; + } + if (!Files.isRegularFile(configurationPath)) { + throw new IOException("Configured Bukkit configuration is not a regular file: " + configurationPath); + } + synchronized (MUTATION_LOCK) { + YamlConfiguration configuration = load(requiredConfigurationFile); + Object configured = configuration.get("settings.world-container"); + if (configured == null) { + return DEFAULT_WORLD_CONTAINER; + } + if (!(configured instanceof String value)) { + throw new IOException("Bukkit settings.world-container must be a path string"); + } + if (value.isBlank()) { + throw new IOException("Configured world container is empty"); + } + return value; + } + } + + public static List readIrisGeneratorBindings( + File configurationFile, + String levelName, + Path levelRoot + ) throws IOException { + File requiredConfigurationFile = Objects.requireNonNull(configurationFile, "configurationFile"); + String requiredLevelName = requireName(levelName, "Level name"); + Path requiredLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot") + .toAbsolutePath() + .normalize(); + Path configurationPath = requiredConfigurationFile.toPath(); + if (!Files.exists(configurationPath, LinkOption.NOFOLLOW_LINKS)) { + return List.of(); + } + synchronized (MUTATION_LOCK) { + Map bindings = new LinkedHashMap<>(); + for (IrisWorldCandidate candidate : readIrisWorldCandidates( + load(requiredConfigurationFile), + requiredLevelName + )) { + String configuredName = candidate.configuredWorldName(); + Path worldContainer = requiredLevelRoot.getParent(); + if (worldContainer == null) { + throw new IOException("Selected level root has no world container: " + requiredLevelRoot); + } + boolean storagePresent; + try { + storagePresent = IrisWorldStorage.frozenDimensionRoot( + worldContainer.toFile(), + requiredLevelRoot.toFile(), + configuredName, + candidate.worldKey() + ).isPresent(); + } catch (IllegalStateException failure) { + storagePresent = false; + } + if (!storagePresent) { + MissingWorldStorageLog.warnOnce( + configuredName, + expectedDimensionRoot(requiredLevelRoot, candidate.worldKey())); + continue; + } + String dimension = selectedIrisDimension(candidate.configuredGenerator(), configuredName); + WorldSlotKey worldKey = new WorldSlotKey( + candidate.worldKey().getNamespace(), + candidate.worldKey().getKey() + ); + IrisGeneratorBinding binding = new IrisGeneratorBinding(configuredName, worldKey, dimension); + IrisGeneratorBinding previous = bindings.putIfAbsent(worldKey, binding); + if (previous != null) { + throw new IOException("bukkit.yml maps both \"" + previous.configuredWorldName() + + "\" and \"" + configuredName + "\" to " + worldKey + "."); + } + } + return List.copyOf(bindings.values()); + } + } + + /** + * Classifies the world storage behind every Iris entry in bukkit.yml. + *

+ * {@link #readIrisGeneratorBindings} answers "which worlds can Iris bind", which deliberately drops + * anything without storage. Startup needs the opposite view: a world whose folder is on disk but whose + * frozen pack snapshot is gone still gets loaded by the server, and if Iris cannot supply a generator + * for it the server writes vanilla terrain into Iris region files. That state has to be found before + * any level loads, so it is reported here rather than discovered at world-generation time. + */ + public static List auditIrisWorldStorage( + File configurationFile, + String levelName, + Path levelRoot + ) throws IOException { + File requiredConfigurationFile = Objects.requireNonNull(configurationFile, "configurationFile"); + String requiredLevelName = requireName(levelName, "Level name"); + Path requiredLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot") + .toAbsolutePath() + .normalize(); + Path configurationPath = requiredConfigurationFile.toPath(); + if (!Files.exists(configurationPath, LinkOption.NOFOLLOW_LINKS)) { + return List.of(); + } + Path worldContainer = requiredLevelRoot.getParent(); + if (worldContainer == null) { + throw new IOException("Selected level root has no world container: " + requiredLevelRoot); + } + synchronized (MUTATION_LOCK) { + List entries = new ArrayList<>(); + for (IrisWorldCandidate candidate : readIrisWorldCandidates( + load(requiredConfigurationFile), + requiredLevelName + )) { + entries.add(classifyStorage(worldContainer, requiredLevelRoot, candidate)); + } + return List.copyOf(entries); + } + } + + private static IrisWorldStorageEntry classifyStorage( + Path worldContainer, + Path levelRoot, + IrisWorldCandidate candidate + ) { + String configuredName = candidate.configuredWorldName(); + NamespacedKey worldKey = candidate.worldKey(); + WorldSlotKey slotKey = new WorldSlotKey(worldKey.getNamespace(), worldKey.getKey()); + Path expectedRoot = expectedDimensionRoot(levelRoot, worldKey); + File dimensionRoot; + try { + dimensionRoot = IrisWorldStorage.frozenDimensionRoot( + worldContainer.toFile(), + levelRoot.toFile(), + configuredName, + worldKey + ).orElse(null); + } catch (IllegalStateException unusable) { + // The server only loads a level it can see as a directory, so an entry with no directory at + // either candidate path is an orphan, not a world that is about to generate. + return hasDimensionDirectory(worldContainer, levelRoot, configuredName, worldKey) + ? new IrisWorldStorageEntry(configuredName, slotKey, expectedRoot, + IrisWorldStorageState.UNUSABLE, unusable.getMessage()) + : new IrisWorldStorageEntry(configuredName, slotKey, expectedRoot, + IrisWorldStorageState.MISSING, null); + } + if (dimensionRoot == null) { + return new IrisWorldStorageEntry(configuredName, slotKey, expectedRoot, + IrisWorldStorageState.MISSING, null); + } + Path resolvedRoot = dimensionRoot.toPath().toAbsolutePath().normalize(); + try { + IrisWorldStorage.requireFrozenPackRoot(dimensionRoot); + } catch (IllegalStateException unusablePack) { + // Only a folder that owns something can be corrupted by a vanilla generator writing into it. + return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot, + hasWorldData(resolvedRoot) + ? IrisWorldStorageState.UNUSABLE + : IrisWorldStorageState.EMPTY, + unusablePack.getMessage()); + } + return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot, + IrisWorldStorageState.PRESENT, null); + } + + /** + * True when the world folder holds something a vanilla generator could overwrite: an {@code iris/} + * directory marking it as an Iris world whose pack snapshot broke, or stored chunk data. + *

+ * Deleting a loaded world's folder and letting the server save the level back recreates a small + * {@code data/} skeleton with no regions and no {@code iris/} directory. That husk owns nothing, so + * refusing to boot over it only strands the server behind a manual delete. + */ + private static boolean hasWorldData(Path worldRoot) { + if (Files.isDirectory(worldRoot.resolve("iris"), LinkOption.NOFOLLOW_LINKS)) { + return true; + } + for (String directory : new String[]{"region", "entities", "poi", "mantle"}) { + if (hasEntries(worldRoot.resolve(directory))) { + return true; + } + } + return false; + } + + private static boolean hasEntries(Path directory) { + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + try (Stream entries = Files.list(directory)) { + return entries.findAny().isPresent(); + } catch (IOException unreadable) { + // An unreadable directory is not proof that there is nothing to lose. + return true; + } + } + + private static boolean hasDimensionDirectory( + Path worldContainer, + Path levelRoot, + String configuredWorldName, + NamespacedKey worldKey + ) { + if (occupiesDimensionPath(levelRoot, expectedDimensionRoot(levelRoot, worldKey))) { + return true; + } + Path configuredLevelRoot = worldContainer.resolve(configuredWorldName).normalize(); + return occupiesDimensionPath(configuredLevelRoot, expectedDimensionRoot(configuredLevelRoot, worldKey)); + } + + /** + * True when something the server could load sits at the dimension path: a real directory, or a symbolic + * link on any segment below the level root. + *

+ * {@link IrisWorldStorage} refuses to resolve through a link, so a linked path is storage Iris cannot use, + * not storage that is not there. Calling it missing tells the operator to restore a folder that is already + * present and leaves whatever the link points at open to a vanilla generator. + */ + private static boolean occupiesDimensionPath(Path levelRoot, Path dimensionRoot) { + Path root = levelRoot.toAbsolutePath().normalize(); + Path target = dimensionRoot.toAbsolutePath().normalize(); + if (!target.startsWith(root) || target.equals(root)) { + return Files.isSymbolicLink(target) || Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS); + } + + Path current = root; + for (Path segment : root.relativize(target)) { + current = current.resolve(segment); + if (Files.isSymbolicLink(current)) { + return true; + } + } + return Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS); + } + + private static Path expectedDimensionRoot(Path levelRoot, NamespacedKey worldKey) { + return levelRoot.toAbsolutePath() + .normalize() + .resolve("dimensions") + .resolve(worldKey.getNamespace()) + .resolve(worldKey.getKey()) + .normalize(); + } + + /** + * Every canonical Iris entry in bukkit.yml, in configured-name order, before any storage is inspected. + * Malformed entries throw exactly as they always did; entries that are not Iris', or whose name is not + * the canonical startup name for their key, are dropped. + */ + private static List readIrisWorldCandidates( + YamlConfiguration configuration, + String levelName + ) throws IOException { + Object rawWorlds = configuration.get("worlds"); + ConfigurationSection worlds = configuration.getConfigurationSection("worlds"); + if (rawWorlds != null && worlds == null) { + throw new IOException("bukkit.yml worlds entry is not a section."); + } + if (worlds == null) { + return List.of(); + } + + List configuredNames = new ArrayList<>(worlds.getKeys(false)); + configuredNames.sort(Comparator.naturalOrder()); + List candidates = new ArrayList<>(configuredNames.size()); + for (String configuredName : configuredNames) { + Object rawWorld = worlds.get(configuredName); + ConfigurationSection world = worlds.getConfigurationSection(configuredName); + if (rawWorld != null && world == null) { + throw new IOException("bukkit.yml world entry \"" + configuredName + "\" is not a section."); + } + if (world == null || !world.getKeys(false).contains("generator")) { + continue; + } + Object rawGenerator = world.get("generator"); + if (!(rawGenerator instanceof String generator)) { + throw new IOException("bukkit.yml generator for world \"" + configuredName + + "\" is not a string."); + } + String configuredGenerator = generator.trim(); + if (!configuredGenerator.equalsIgnoreCase("Iris") + && !configuredGenerator.regionMatches(true, 0, "Iris:", 0, 5)) { + continue; + } + + NamespacedKey namespacedKey; + try { + namespacedKey = IrisWorldStorage.keyFromConfiguredWorldName(configuredName, levelName); + } catch (IllegalArgumentException failure) { + throw new IOException("bukkit.yml contains an invalid Iris world name \"" + + configuredName + "\".", failure); + } + if (!"iris".equals(namespacedKey.getNamespace())) { + continue; + } + if (!configuredName.equals(IrisWorldStorage.configuredWorldName(namespacedKey, levelName))) { + continue; + } + candidates.add(new IrisWorldCandidate(configuredName, namespacedKey, configuredGenerator)); + } + return candidates; + } + + public static GeneratorReplacement replaceIfMatching( + File configurationFile, + String worldName, + WorldGeneratorSnapshot expected, + String dimension, + Long seed + ) throws IOException { + Objects.requireNonNull(configurationFile, "configurationFile"); + String requiredWorldName = requireWorldName(worldName); + WorldGeneratorSnapshot requiredExpected = Objects.requireNonNull(expected, "expected"); + String requiredDimension = requireName(dimension, "Dimension"); + WorldGeneratorSnapshot replacement = WorldGeneratorSnapshot.configured(requiredDimension, seed); + synchronized (MUTATION_LOCK) { + YamlConfiguration configuration = load(configurationFile); + WorldGeneratorSnapshot current = snapshot(configuration, requiredWorldName); + if (!current.matchesGeneratorAndSeed(requiredExpected)) { + return new GeneratorReplacement(false, current, replacement); + } + apply(configuration, requiredWorldName, replacement); + saveAtomic(configurationFile.toPath(), configuration, true); + return new GeneratorReplacement(true, current, replacement); + } + } + + public static boolean restoreIfMatching( + File configurationFile, + String worldName, + WorldGeneratorSnapshot expectedCurrent, + WorldGeneratorSnapshot restoration + ) throws IOException { + Objects.requireNonNull(configurationFile, "configurationFile"); + String requiredWorldName = requireWorldName(worldName); + WorldGeneratorSnapshot requiredExpected = Objects.requireNonNull(expectedCurrent, "expectedCurrent"); + WorldGeneratorSnapshot requiredRestoration = Objects.requireNonNull(restoration, "restoration"); + synchronized (MUTATION_LOCK) { + YamlConfiguration configuration = load(configurationFile); + WorldGeneratorSnapshot current = snapshot(configuration, requiredWorldName); + if (!current.matchesGeneratorAndSeed(requiredExpected)) { + return false; + } + apply(configuration, requiredWorldName, requiredRestoration); + saveAtomic(configurationFile.toPath(), configuration, true); + return true; + } + } + + public static boolean remove(File configurationFile, String worldName) throws IOException { + Objects.requireNonNull(configurationFile, "configurationFile"); + String requiredWorldName = requireWorldName(worldName); + synchronized (MUTATION_LOCK) { + YamlConfiguration configuration = load(configurationFile); + ConfigurationSection worlds = configuration.getConfigurationSection("worlds"); + if (worlds == null || worlds.get(requiredWorldName) == null) { + return false; + } + + worlds.set(requiredWorldName, null); + if (worlds.getKeys(false).isEmpty()) { + configuration.set("worlds", null); + } + saveAtomic(configurationFile.toPath(), configuration); + return true; + } + } + + public static boolean removeIfMatching( + File configurationFile, + String worldName, + String dimension, + Long seed + ) throws IOException { + Objects.requireNonNull(configurationFile, "configurationFile"); + String requiredWorldName = requireWorldName(worldName); + String requiredDimension = requireName(dimension, "Dimension"); + synchronized (MUTATION_LOCK) { + YamlConfiguration configuration = load(configurationFile); + ConfigurationSection worlds = configuration.getConfigurationSection("worlds"); + if (worlds == null) { + return false; + } + + ConfigurationSection existing = worlds.getConfigurationSection(requiredWorldName); + if (existing == null) { + return false; + } + String expectedGenerator = "Iris:" + requiredDimension; + String actualGenerator = existing.getString("generator"); + Long actualSeed = existing.contains("seed") ? existing.getLong("seed") : null; + if (!expectedGenerator.equals(actualGenerator) || !Objects.equals(seed, actualSeed)) { + return false; + } + + worlds.set(requiredWorldName, null); + if (worlds.getKeys(false).isEmpty()) { + configuration.set("worlds", null); + } + saveAtomic(configurationFile.toPath(), configuration); + return true; + } + } + + public static int removeMatching(File configurationFile, Predicate matcher) throws IOException { + Objects.requireNonNull(configurationFile, "configurationFile"); + Predicate requiredMatcher = Objects.requireNonNull(matcher, "matcher"); + synchronized (MUTATION_LOCK) { + YamlConfiguration configuration = load(configurationFile); + ConfigurationSection worlds = configuration.getConfigurationSection("worlds"); + if (worlds == null) { + return 0; + } + + int removed = 0; + for (String worldName : new ArrayList<>(worlds.getKeys(false))) { + if (!requiredMatcher.test(worldName)) { + continue; + } + worlds.set(worldName, null); + removed++; + } + if (removed == 0) { + return 0; + } + if (worlds.getKeys(false).isEmpty()) { + configuration.set("worlds", null); + } + saveAtomic(configurationFile.toPath(), configuration); + return removed; + } + } + + static void saveAtomic(Path target, YamlConfiguration configuration) throws IOException { + saveAtomic(target, configuration, false); + } + + private static void saveAtomic( + Path target, + YamlConfiguration configuration, + boolean requireAtomicReplacement + ) throws IOException { + Path absoluteTarget = target.toAbsolutePath().normalize(); + requireRegularConfigurationFile(absoluteTarget); + Path parent = absoluteTarget.getParent(); + if (parent == null) { + throw new IOException("bukkit.yml target has no parent: " + absoluteTarget); + } + Files.createDirectories(parent); + Path staged = Files.createTempFile(parent, ".bukkit-worlds-", ".yml"); + try { + configuration.save(staged.toFile()); + try (FileChannel channel = FileChannel.open(staged, StandardOpenOption.WRITE)) { + channel.force(true); + } + requireRegularConfigurationFile(absoluteTarget); + try { + Files.move(staged, absoluteTarget, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException exception) { + if (requireAtomicReplacement) { + throw new IOException( + "Exact world replacement requires atomic bukkit.yml publication on this filesystem.", + exception + ); + } + Files.move(staged, absoluteTarget, StandardCopyOption.REPLACE_EXISTING); + } + // bukkit.yml is the commit record the bootstrap reconciles against; its rename + // needs the same directory-durability barrier the journal and world moves get. + if (requireAtomicReplacement) { + DirectoryDurability.forceDirectoryRequired(parent); + } else { + DirectoryDurability.forceDirectoryAfterCommit(parent, "A bukkit.yml world configuration change"); + } + } finally { + Files.deleteIfExists(staged); + } + } + + private static YamlConfiguration load(File configurationFile) throws IOException { + requireRegularConfigurationFile(configurationFile.toPath()); + YamlConfiguration configuration = new YamlConfiguration(); + try { + configuration.load(configurationFile); + return configuration; + } catch (InvalidConfigurationException exception) { + throw new IOException("bukkit.yml is invalid and was not changed.", exception); + } + } + + private static void requireRegularConfigurationFile(Path configurationFile) throws IOException { + Path path = configurationFile.toAbsolutePath().normalize(); + BasicFileAttributes attributes = Files.readAttributes( + path, + BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS + ); + if (attributes.isSymbolicLink() || !attributes.isRegularFile()) { + throw new IOException("bukkit.yml must be an existing regular file and was not changed: " + path); + } + } + + private static WorldGeneratorSnapshot snapshot( + YamlConfiguration configuration, + String worldName + ) throws IOException { + Object rawWorlds = configuration.get("worlds"); + ConfigurationSection worlds = configuration.getConfigurationSection("worlds"); + if (rawWorlds != null && worlds == null) { + throw new IOException("bukkit.yml worlds entry is not a section and was not changed."); + } + if (worlds == null) { + return WorldGeneratorSnapshot.absent(); + } + + Object rawWorld = worlds.get(worldName); + ConfigurationSection world = worlds.getConfigurationSection(worldName); + if (rawWorld != null && world == null) { + throw new IOException("bukkit.yml world entry \"" + worldName + "\" is not a section and was not changed."); + } + if (world == null) { + return WorldGeneratorSnapshot.absentWorld(true); + } + + boolean generatorPresent = world.getKeys(false).contains("generator"); + String generator = null; + if (generatorPresent) { + Object rawGenerator = world.get("generator"); + if (!(rawGenerator instanceof String generatorValue)) { + throw new IOException("bukkit.yml generator for world \"" + worldName + + "\" is not a string and was not changed."); + } + generator = generatorValue; + } + + boolean seedPresent = world.getKeys(false).contains("seed"); + Long seed = null; + if (seedPresent) { + Object rawSeed = world.get("seed"); + if (!(rawSeed instanceof Byte + || rawSeed instanceof Short + || rawSeed instanceof Integer + || rawSeed instanceof Long)) { + throw new IOException("bukkit.yml seed for world \"" + worldName + + "\" is not an integer and was not changed."); + } + seed = ((Number) rawSeed).longValue(); + } + + return new WorldGeneratorSnapshot( + true, + true, + generatorPresent, + generator, + seedPresent, + seed + ); + } + + private static void apply( + YamlConfiguration configuration, + String worldName, + WorldGeneratorSnapshot snapshot + ) throws IOException { + ConfigurationSection worlds = configuration.getConfigurationSection("worlds"); + if (worlds == null) { + Object rawWorlds = configuration.get("worlds"); + if (rawWorlds != null) { + throw new IOException("bukkit.yml worlds entry is not a section and was not changed."); + } + worlds = configuration.createSection("worlds"); + } + + ConfigurationSection world = worlds.getConfigurationSection(worldName); + if (world == null) { + Object rawWorld = worlds.get(worldName); + if (rawWorld != null) { + throw new IOException("bukkit.yml world entry \"" + worldName + + "\" is not a section and was not changed."); + } + world = worlds.createSection(worldName); + } + + world.set("generator", snapshot.generatorPresent() ? snapshot.generator() : null); + world.set("seed", snapshot.seedPresent() ? snapshot.seed() : null); + if (!snapshot.worldSectionPresent() && world.getKeys(false).isEmpty()) { + worlds.set(worldName, null); + } + if (!snapshot.worldsSectionPresent() && worlds.getKeys(false).isEmpty()) { + configuration.set("worlds", null); + } + } + + private static String requireWorldName(String value) { + String worldName = requireName(value, "World name"); + if (!worldName.matches("[a-z0-9_-]+")) { + throw new IllegalArgumentException("World name must contain only lowercase letters, numbers, underscores, or hyphens."); + } + return worldName; + } + + private static String requireName(String value, String label) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(label + " cannot be empty."); + } + return value.trim(); + } + + private static String selectedIrisDimension(String configuredGenerator, String worldName) throws IOException { + if (configuredGenerator.equalsIgnoreCase("Iris")) { + throw new IOException("bukkit.yml Iris generator for custom world \"" + worldName + + "\" must select a dimension with Iris:."); + } + String dimension = configuredGenerator.substring(5).trim(); + if (dimension.isEmpty()) { + throw new IOException("bukkit.yml Iris generator for world \"" + worldName + + "\" does not select a dimension."); + } + return dimension; + } + + public enum Registration { + CREATED, + UNCHANGED + } + + public enum IrisWorldStorageState { + /** + * Storage resolves and carries a usable frozen pack snapshot. + */ + PRESENT, + /** + * No world directory at all. The server never enumerates the level, so nothing generates; the + * bukkit.yml and Multiverse entries are left alone so putting the folder back restores the world. + */ + MISSING, + /** + * A world directory the server will load that holds no pack snapshot and no world data - the + * skeleton the server writes back when it saves a level whose folder was deleted underneath it. + * There is nothing to overwrite, so it is reported and the server starts. + */ + EMPTY, + /** + * A world directory the server will load, whose frozen pack snapshot cannot be used. Iris cannot + * generate it and nothing else may. + */ + UNUSABLE + } + + public record IrisWorldStorageEntry( + String configuredWorldName, + WorldSlotKey worldKey, + Path storagePath, + IrisWorldStorageState state, + String detail + ) { + public IrisWorldStorageEntry { + configuredWorldName = requireName(configuredWorldName, "Configured world name"); + Objects.requireNonNull(worldKey, "worldKey"); + Objects.requireNonNull(storagePath, "storagePath"); + Objects.requireNonNull(state, "state"); + } + } + + private record IrisWorldCandidate( + String configuredWorldName, + NamespacedKey worldKey, + String configuredGenerator + ) { + } + + public record IrisGeneratorBinding( + String configuredWorldName, + WorldSlotKey worldKey, + String dimension + ) { + public IrisGeneratorBinding { + configuredWorldName = requireName(configuredWorldName, "Configured world name"); + WorldSlotKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey"); + if (!"iris".equals(requiredWorldKey.namespace()) + || !requiredWorldKey.key().matches("[a-z0-9_-]+")) { + throw new IllegalArgumentException("Only safe Iris-managed keys can have LevelStem bindings."); + } + dimension = requireName(dimension, "Iris dimension"); + } + } + + public record WorldGeneratorSnapshot( + boolean worldsSectionPresent, + boolean worldSectionPresent, + boolean generatorPresent, + String generator, + boolean seedPresent, + Long seed + ) { + public WorldGeneratorSnapshot { + if (worldSectionPresent && !worldsSectionPresent) { + throw new IllegalArgumentException("A world section requires a worlds section."); + } + if (!worldSectionPresent && (generatorPresent || seedPresent)) { + throw new IllegalArgumentException("Generator and seed values require a world section."); + } + if (generatorPresent != (generator != null)) { + throw new IllegalArgumentException("Generator presence and value must agree."); + } + if (seedPresent != (seed != null)) { + throw new IllegalArgumentException("Seed presence and value must agree."); + } + } + + private static WorldGeneratorSnapshot absent() { + return new WorldGeneratorSnapshot(false, false, false, null, false, null); + } + + private static WorldGeneratorSnapshot absentWorld(boolean worldsSectionPresent) { + return new WorldGeneratorSnapshot(worldsSectionPresent, false, false, null, false, null); + } + + private static WorldGeneratorSnapshot configured(String dimension, Long seed) { + return new WorldGeneratorSnapshot(true, true, true, "Iris:" + dimension, seed != null, seed); + } + + public boolean matchesGeneratorAndSeed(WorldGeneratorSnapshot other) { + return generatorPresent == other.generatorPresent + && Objects.equals(generator, other.generator) + && seedPresent == other.seedPresent + && Objects.equals(seed, other.seed); + } + } + + public record GeneratorReplacement( + boolean applied, + WorldGeneratorSnapshot observed, + WorldGeneratorSnapshot replacement + ) { + public GeneratorReplacement { + Objects.requireNonNull(observed, "observed"); + Objects.requireNonNull(replacement, "replacement"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilityResolution.java b/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilityResolution.java new file mode 100644 index 000000000..909eac039 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilityResolution.java @@ -0,0 +1,235 @@ +package art.arcane.iris.core.lifecycle; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.function.Predicate; + +final class CapabilityResolution { + private CapabilityResolution() { + } + + static Method resolveCreateLevelMethod(Class owner) throws NoSuchMethodException { + Method current = resolveMethod(owner, "createLevel", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 3 + && "LevelStem".equals(params[0].getSimpleName()) + && "WorldLoadingInfoAndData".equals(params[1].getSimpleName()) + && "WorldDataAndGenSettings".equals(params[2].getSimpleName()); + }); + if (current != null) { + return current; + } + + Method legacy = resolveMethod(owner, "createLevel", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 4 + && "LevelStem".equals(params[0].getSimpleName()) + && "WorldLoadingInfo".equals(params[1].getSimpleName()) + && "LevelStorageAccess".equals(params[2].getSimpleName()) + && "PrimaryLevelData".equals(params[3].getSimpleName()); + }); + if (legacy != null) { + return legacy; + } + + throw new NoSuchMethodException(owner.getName() + "#createLevel"); + } + + static Method resolveLevelStorageAccessMethod(Class owner) throws NoSuchMethodException { + Method exactValidate = resolveMethod(owner, "validateAndCreateAccess", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 2 + && String.class.equals(params[0]) + && "ResourceKey".equals(params[1].getSimpleName()) + && "LevelStorageAccess".equals(method.getReturnType().getSimpleName()); + }); + if (exactValidate != null) { + return exactValidate; + } + + Method oneArgValidate = resolveMethod(owner, "validateAndCreateAccess", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 1 + && String.class.equals(params[0]) + && "LevelStorageAccess".equals(method.getReturnType().getSimpleName()); + }); + if (oneArgValidate != null) { + return oneArgValidate; + } + + Method exactCreate = resolveMethod(owner, "createAccess", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 2 + && String.class.equals(params[0]) + && "ResourceKey".equals(params[1].getSimpleName()) + && "LevelStorageAccess".equals(method.getReturnType().getSimpleName()); + }); + if (exactCreate != null) { + return exactCreate; + } + + Method oneArgCreate = resolveMethod(owner, "createAccess", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 1 + && String.class.equals(params[0]) + && "LevelStorageAccess".equals(method.getReturnType().getSimpleName()); + }); + if (oneArgCreate != null) { + return oneArgCreate; + } + + throw new NoSuchMethodException(owner.getName() + "#validateAndCreateAccess/createAccess"); + } + + static Method resolvePaperWorldDataMethod(Class owner) throws NoSuchMethodException { + Method current = resolveMethod(owner, "loadWorldData", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 3 + && "MinecraftServer".equals(params[0].getSimpleName()) + && "ResourceKey".equals(params[1].getSimpleName()) + && String.class.equals(params[2]) + && "LoadedWorldData".equals(method.getReturnType().getSimpleName()); + }); + if (current != null) { + return current; + } + + Method legacy = resolveMethod(owner, "getLevelData", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 1 && "LevelStorageAccess".equals(params[0].getSimpleName()); + }); + if (legacy != null) { + return legacy; + } + + throw new NoSuchMethodException(owner.getName() + "#loadWorldData/getLevelData"); + } + + static Constructor resolveWorldLoadingInfoConstructor(Class owner) throws NoSuchMethodException { + Constructor current = resolveConstructor(owner, constructor -> { + Class[] params = constructor.getParameterTypes(); + return params.length == 4 + && "Environment".equals(params[0].getSimpleName()) + && "ResourceKey".equals(params[1].getSimpleName()) + && "ResourceKey".equals(params[2].getSimpleName()) + && boolean.class.equals(params[3]); + }); + if (current != null) { + return current; + } + + Constructor legacy = resolveConstructor(owner, constructor -> { + Class[] params = constructor.getParameterTypes(); + return params.length == 5 + && int.class.equals(params[0]) + && String.class.equals(params[1]) + && String.class.equals(params[2]) + && "ResourceKey".equals(params[3].getSimpleName()) + && boolean.class.equals(params[4]); + }); + if (legacy != null) { + return legacy; + } + + throw new NoSuchMethodException(owner.getName() + "#"); + } + + static Constructor resolveWorldLoadingInfoAndDataConstructor(Class owner) throws NoSuchMethodException { + Constructor constructor = resolveConstructor(owner, candidate -> { + Class[] params = candidate.getParameterTypes(); + return params.length == 2 + && "WorldLoadingInfo".equals(params[0].getSimpleName()) + && "LoadedWorldData".equals(params[1].getSimpleName()); + }); + if (constructor == null) { + throw new NoSuchMethodException(owner.getName() + "#"); + } + return constructor; + } + + static Method resolveCreateNewWorldDataMethod(Class owner) throws NoSuchMethodException { + Method method = resolveMethod(owner, "createNewWorldData", candidate -> { + Class[] params = candidate.getParameterTypes(); + return params.length == 5 + && "DedicatedServerSettings".equals(params[0].getSimpleName()) + && "DataLoadContext".equals(params[1].getSimpleName()) + && "Registry".equals(params[2].getSimpleName()) + && boolean.class.equals(params[3]) + && boolean.class.equals(params[4]); + }); + if (method == null) { + throw new NoSuchMethodException(owner.getName() + "#createNewWorldData"); + } + return method; + } + + static Method resolveServerRegistryAccessMethod(Class owner) throws NoSuchMethodException { + Method method = resolveMethod(owner, "registryAccess", candidate -> candidate.getParameterCount() == 0 + && !void.class.equals(candidate.getReturnType())); + if (method == null) { + throw new NoSuchMethodException(owner.getName() + "#registryAccess"); + } + return method; + } + + static Method resolveMethod(Class owner, String name, Predicate predicate) { + Method selected = scanMethods(owner.getMethods(), name, predicate); + if (selected != null) { + return selected; + } + + Class current = owner; + while (current != null) { + selected = scanMethods(current.getDeclaredMethods(), name, predicate); + if (selected != null) { + selected.setAccessible(true); + return selected; + } + current = current.getSuperclass(); + } + + return null; + } + + static Field resolveField(Class owner, String name) throws NoSuchFieldException { + Class current = owner; + while (current != null) { + try { + Field field = current.getDeclaredField(name); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException ignored) { + current = current.getSuperclass(); + } + } + throw new NoSuchFieldException(owner.getName() + "#" + name); + } + + private static Method scanMethods(Method[] methods, String name, Predicate predicate) { + for (Method method : methods) { + if (!method.getName().equals(name)) { + continue; + } + if (predicate.test(method)) { + return method; + } + } + return null; + } + + private static Constructor resolveConstructor(Class owner, Predicate> predicate) { + for (Constructor constructor : owner.getConstructors()) { + if (predicate.test(constructor)) { + return constructor; + } + } + for (Constructor constructor : owner.getDeclaredConstructors()) { + if (predicate.test(constructor)) { + constructor.setAccessible(true); + return constructor; + } + } + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilitySnapshot.java b/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilitySnapshot.java new file mode 100644 index 000000000..6edba406a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilitySnapshot.java @@ -0,0 +1,612 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.FoliaScheduler; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.plugin.RegisteredServiceProvider; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Locale; + +public final class CapabilitySnapshot { + public enum PaperLikeFlavor { + CURRENT_INFO_AND_DATA, + LEGACY_STORAGE_ACCESS, + UNSUPPORTED + } + + private final ServerFamily serverFamily; + private final boolean regionizedRuntime; + private final Object worldsProvider; + private final Class worldsLevelStemClass; + private final Class worldsGeneratorTypeClass; + private final String worldsProviderResolution; + private final Object bukkitServer; + private final Object minecraftServer; + private final Method createLevelMethod; + private final PaperLikeFlavor paperLikeFlavor; + private final Class paperWorldLoaderClass; + private final Method paperWorldDataMethod; + private final Constructor worldLoadingInfoConstructor; + private final Constructor worldLoadingInfoAndDataConstructor; + private final Method createNewWorldDataMethod; + private final Method levelStorageAccessMethod; + private final Field worldLoaderContextField; + private final Method serverRegistryAccessMethod; + private final Field settingsField; + private final Field optionsField; + private final Method isDemoMethod; + private final Method unloadWorldAsyncMethod; + private final Method chunkAtAsyncMethod; + private final Method removeLevelMethod; + private final String paperLikeResolution; + + private CapabilitySnapshot( + ServerFamily serverFamily, + boolean regionizedRuntime, + Object worldsProvider, + Class worldsLevelStemClass, + Class worldsGeneratorTypeClass, + String worldsProviderResolution, + Object bukkitServer, + Object minecraftServer, + Method createLevelMethod, + PaperLikeFlavor paperLikeFlavor, + Class paperWorldLoaderClass, + Method paperWorldDataMethod, + Constructor worldLoadingInfoConstructor, + Constructor worldLoadingInfoAndDataConstructor, + Method createNewWorldDataMethod, + Method levelStorageAccessMethod, + Field worldLoaderContextField, + Method serverRegistryAccessMethod, + Field settingsField, + Field optionsField, + Method isDemoMethod, + Method unloadWorldAsyncMethod, + Method chunkAtAsyncMethod, + Method removeLevelMethod, + String paperLikeResolution + ) { + this.serverFamily = serverFamily; + this.regionizedRuntime = regionizedRuntime; + this.worldsProvider = worldsProvider; + this.worldsLevelStemClass = worldsLevelStemClass; + this.worldsGeneratorTypeClass = worldsGeneratorTypeClass; + this.worldsProviderResolution = worldsProviderResolution; + this.bukkitServer = bukkitServer; + this.minecraftServer = minecraftServer; + this.createLevelMethod = createLevelMethod; + this.paperLikeFlavor = paperLikeFlavor; + this.paperWorldLoaderClass = paperWorldLoaderClass; + this.paperWorldDataMethod = paperWorldDataMethod; + this.worldLoadingInfoConstructor = worldLoadingInfoConstructor; + this.worldLoadingInfoAndDataConstructor = worldLoadingInfoAndDataConstructor; + this.createNewWorldDataMethod = createNewWorldDataMethod; + this.levelStorageAccessMethod = levelStorageAccessMethod; + this.worldLoaderContextField = worldLoaderContextField; + this.serverRegistryAccessMethod = serverRegistryAccessMethod; + this.settingsField = settingsField; + this.optionsField = optionsField; + this.isDemoMethod = isDemoMethod; + this.unloadWorldAsyncMethod = unloadWorldAsyncMethod; + this.chunkAtAsyncMethod = chunkAtAsyncMethod; + this.removeLevelMethod = removeLevelMethod; + this.paperLikeResolution = paperLikeResolution; + } + + public static CapabilitySnapshot probe() { + Server server = Bukkit.getServer(); + Object bukkitServer = server; + boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server); + ServerFamily serverFamily = detectServerFamily(server, regionizedRuntime); + + Object worldsProvider = null; + Class worldsLevelStemClass = null; + Class worldsGeneratorTypeClass = null; + String worldsProviderResolution = "inactive"; + try { + Object[] worldsProviderData = resolveWorldsProvider(); + worldsProvider = worldsProviderData[0]; + worldsLevelStemClass = (Class) worldsProviderData[1]; + worldsGeneratorTypeClass = (Class) worldsProviderData[2]; + worldsProviderResolution = (String) worldsProviderData[3]; + } catch (Throwable e) { + worldsProviderResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage()); + } + + Object minecraftServer = null; + Method createLevelMethod = null; + PaperLikeFlavor paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED; + Class paperWorldLoaderClass = null; + Method paperWorldDataMethod = null; + Constructor worldLoadingInfoConstructor = null; + Constructor worldLoadingInfoAndDataConstructor = null; + Method createNewWorldDataMethod = null; + Method levelStorageAccessMethod = null; + Field worldLoaderContextField = null; + Method serverRegistryAccessMethod = null; + Field settingsField = null; + Field optionsField = null; + Method isDemoMethod = null; + Method removeLevelMethod = null; + String paperLikeResolution = "inactive"; + + try { + if (bukkitServer != null) { + Method getServerMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "getServer", method -> method.getParameterCount() == 0); + if (getServerMethod != null) { + minecraftServer = getServerMethod.invoke(bukkitServer); + } + } + + if (minecraftServer != null) { + Class minecraftServerClass = Class.forName("net.minecraft.server.MinecraftServer"); + if (!minecraftServerClass.isInstance(minecraftServer)) { + throw new IllegalStateException("resolved server is not a MinecraftServer: " + minecraftServer.getClass().getName()); + } + + createLevelMethod = CapabilityResolution.resolveCreateLevelMethod(minecraftServer.getClass()); + removeLevelMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "removeLevel", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 1 && "ServerLevel".equals(params[0].getSimpleName()); + }); + worldLoaderContextField = CapabilityResolution.resolveField(minecraftServer.getClass(), "worldLoaderContext"); + serverRegistryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(minecraftServer.getClass()); + settingsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "settings"); + optionsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "options"); + isDemoMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "isDemo", method -> method.getParameterCount() == 0 && boolean.class.equals(method.getReturnType())); + + Class mainClass = Class.forName("net.minecraft.server.Main"); + createNewWorldDataMethod = CapabilityResolution.resolveCreateNewWorldDataMethod(mainClass); + + Class paperLoaderCandidate = Class.forName("io.papermc.paper.world.PaperWorldLoader"); + paperWorldLoaderClass = paperLoaderCandidate; + paperWorldDataMethod = CapabilityResolution.resolvePaperWorldDataMethod(paperLoaderCandidate); + Class worldLoadingInfoClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfo"); + worldLoadingInfoConstructor = CapabilityResolution.resolveWorldLoadingInfoConstructor(worldLoadingInfoClass); + + if (createLevelMethod.getParameterCount() == 3) { + Class worldLoadingInfoAndDataClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfoAndData"); + worldLoadingInfoAndDataConstructor = CapabilityResolution.resolveWorldLoadingInfoAndDataConstructor(worldLoadingInfoAndDataClass); + paperLikeFlavor = PaperLikeFlavor.CURRENT_INFO_AND_DATA; + } else { + Class levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource"); + levelStorageAccessMethod = CapabilityResolution.resolveLevelStorageAccessMethod(levelStorageSourceClass); + paperLikeFlavor = PaperLikeFlavor.LEGACY_STORAGE_ACCESS; + } + + paperLikeResolution = "available(flavor=" + paperLikeFlavor.name().toLowerCase(Locale.ROOT) + + ", createLevel=" + createLevelMethod.toGenericString() + ")"; + } + } catch (Throwable e) { + paperLikeResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage()); + createLevelMethod = null; + paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED; + paperWorldLoaderClass = null; + paperWorldDataMethod = null; + worldLoadingInfoConstructor = null; + worldLoadingInfoAndDataConstructor = null; + createNewWorldDataMethod = null; + levelStorageAccessMethod = null; + worldLoaderContextField = null; + serverRegistryAccessMethod = null; + settingsField = null; + optionsField = null; + isDemoMethod = null; + removeLevelMethod = null; + } + + Method unloadWorldAsyncMethod = null; + try { + if (bukkitServer != null) { + unloadWorldAsyncMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "unloadWorldAsync", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 3 + && World.class.equals(params[0]) + && boolean.class.equals(params[1]) + && "Consumer".equals(params[2].getSimpleName()); + }); + } + } catch (Throwable ignored) { + unloadWorldAsyncMethod = null; + } + + Method chunkAtAsyncMethod = null; + try { + chunkAtAsyncMethod = CapabilityResolution.resolveMethod(World.class, "getChunkAtAsync", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 3 + && int.class.equals(params[0]) + && int.class.equals(params[1]) + && boolean.class.equals(params[2]); + }); + } catch (Throwable ignored) { + chunkAtAsyncMethod = null; + } + + return new CapabilitySnapshot( + serverFamily, + regionizedRuntime, + worldsProvider, + worldsLevelStemClass, + worldsGeneratorTypeClass, + worldsProviderResolution, + bukkitServer, + minecraftServer, + createLevelMethod, + paperLikeFlavor, + paperWorldLoaderClass, + paperWorldDataMethod, + worldLoadingInfoConstructor, + worldLoadingInfoAndDataConstructor, + createNewWorldDataMethod, + levelStorageAccessMethod, + worldLoaderContextField, + serverRegistryAccessMethod, + settingsField, + optionsField, + isDemoMethod, + unloadWorldAsyncMethod, + chunkAtAsyncMethod, + removeLevelMethod, + paperLikeResolution + ); + } + + public static CapabilitySnapshot forTesting(ServerFamily serverFamily, boolean regionizedRuntime, boolean worldsProviderHealthy, boolean paperLikeRuntimeHealthy) { + Object minecraftServer = paperLikeRuntimeHealthy ? new TestingPaperLikeServer("datapack-registry", "server-registry") : null; + Method createLevelMethod = null; + Field worldLoaderContextField = null; + Method serverRegistryAccessMethod = null; + try { + createLevelMethod = paperLikeRuntimeHealthy + ? TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class) + : null; + worldLoaderContextField = paperLikeRuntimeHealthy + ? CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext") + : null; + serverRegistryAccessMethod = paperLikeRuntimeHealthy + ? CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class) + : null; + } catch (NoSuchMethodException | NoSuchFieldException e) { + throw new IllegalStateException(e); + } + return new CapabilitySnapshot( + serverFamily, + regionizedRuntime, + worldsProviderHealthy ? new Object() : null, + worldsProviderHealthy ? Object.class : null, + worldsProviderHealthy ? Object.class : null, + worldsProviderHealthy ? "test-provider" : "inactive", + null, + minecraftServer, + createLevelMethod, + paperLikeRuntimeHealthy ? PaperLikeFlavor.CURRENT_INFO_AND_DATA : PaperLikeFlavor.UNSUPPORTED, + null, + null, + null, + null, + null, + null, + worldLoaderContextField, + serverRegistryAccessMethod, + null, + null, + null, + null, + null, + null, + paperLikeRuntimeHealthy ? "available(test)" : "unsupported(test)" + ); + } + + public static CapabilitySnapshot forTestingRuntimeRegistries(ServerFamily serverFamily, boolean regionizedRuntime, Object datapackDimensions, Object serverRegistryAccess) { + TestingPaperLikeServer minecraftServer = new TestingPaperLikeServer(datapackDimensions, serverRegistryAccess); + Method createLevelMethod; + Field worldLoaderContextField; + Method registryAccessMethod; + try { + createLevelMethod = TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class); + worldLoaderContextField = CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext"); + registryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class); + } catch (NoSuchMethodException | NoSuchFieldException e) { + throw new IllegalStateException(e); + } + return new CapabilitySnapshot( + serverFamily, + regionizedRuntime, + null, + null, + null, + "inactive", + null, + minecraftServer, + createLevelMethod, + PaperLikeFlavor.CURRENT_INFO_AND_DATA, + null, + null, + null, + null, + null, + null, + worldLoaderContextField, + registryAccessMethod, + null, + null, + null, + null, + null, + null, + "available(test-runtime-registries)" + ); + } + + public ServerFamily serverFamily() { + return serverFamily; + } + + public boolean regionizedRuntime() { + return regionizedRuntime; + } + + public Object worldsProvider() { + return worldsProvider; + } + + public Class worldsLevelStemClass() { + return worldsLevelStemClass; + } + + public Class worldsGeneratorTypeClass() { + return worldsGeneratorTypeClass; + } + + public Object bukkitServer() { + return bukkitServer; + } + + public Object minecraftServer() { + return minecraftServer; + } + + public Method createLevelMethod() { + return createLevelMethod; + } + + public PaperLikeFlavor paperLikeFlavor() { + return paperLikeFlavor; + } + + public Class paperWorldLoaderClass() { + return paperWorldLoaderClass; + } + + public Method paperWorldDataMethod() { + return paperWorldDataMethod; + } + + public Constructor worldLoadingInfoConstructor() { + return worldLoadingInfoConstructor; + } + + public Constructor worldLoadingInfoAndDataConstructor() { + return worldLoadingInfoAndDataConstructor; + } + + public Method createNewWorldDataMethod() { + return createNewWorldDataMethod; + } + + public Method levelStorageAccessMethod() { + return levelStorageAccessMethod; + } + + public Field worldLoaderContextField() { + return worldLoaderContextField; + } + + public Method serverRegistryAccessMethod() { + return serverRegistryAccessMethod; + } + + public Field settingsField() { + return settingsField; + } + + public Field optionsField() { + return optionsField; + } + + public Method isDemoMethod() { + return isDemoMethod; + } + + public Method unloadWorldAsyncMethod() { + return unloadWorldAsyncMethod; + } + + public Method chunkAtAsyncMethod() { + return chunkAtAsyncMethod; + } + + public Method removeLevelMethod() { + return removeLevelMethod; + } + + public boolean hasWorldsProvider() { + return worldsProvider != null && worldsLevelStemClass != null && worldsGeneratorTypeClass != null; + } + + public boolean hasPaperLikeRuntime() { + return minecraftServer != null + && createLevelMethod != null + && serverRegistryAccessMethod != null + && paperLikeFlavor != PaperLikeFlavor.UNSUPPORTED; + } + + public String worldsProviderResolution() { + return worldsProviderResolution; + } + + public String paperLikeResolution() { + return paperLikeResolution; + } + + public String describe() { + return "family=" + serverFamily.id() + + ", regionizedRuntime=" + regionizedRuntime + + ", worldsProvider=" + worldsProviderResolution + + ", paperLike=" + paperLikeResolution + + ", serverRegistryAccess=" + (serverRegistryAccessMethod != null) + + ", unloadAsync=" + (unloadWorldAsyncMethod != null) + + ", chunkAsync=" + (chunkAtAsyncMethod != null); + } + + private static ServerFamily detectServerFamily(Server server, boolean regionizedRuntime) { + String bukkitName = server == null ? "" : server.getName(); + String bukkitVersion = server == null ? "" : server.getVersion(); + String serverClassName = server == null ? "" : server.getClass().getName(); + boolean canvasRuntime = hasCanvasRuntime(); + + if (containsIgnoreCase(bukkitName, "folia") + || containsIgnoreCase(bukkitVersion, "folia") + || containsIgnoreCase(serverClassName, "folia")) { + return ServerFamily.FOLIA; + } + + if (canvasRuntime + || containsIgnoreCase(bukkitName, "canvas") + || containsIgnoreCase(bukkitVersion, "canvas") + || containsIgnoreCase(serverClassName, "canvas")) { + return regionizedRuntime ? ServerFamily.CANVAS : ServerFamily.CANVAS; + } + + if (containsIgnoreCase(bukkitName, "purpur") + || containsIgnoreCase(bukkitVersion, "purpur") + || containsIgnoreCase(serverClassName, "purpur")) { + return ServerFamily.PURPUR; + } + + if (containsIgnoreCase(bukkitName, "paper") + || containsIgnoreCase(bukkitVersion, "paper") + || containsIgnoreCase(serverClassName, "paper") + || containsIgnoreCase(bukkitName, "pufferfish") + || containsIgnoreCase(bukkitVersion, "pufferfish") + || containsIgnoreCase(serverClassName, "pufferfish")) { + return ServerFamily.PAPER; + } + + if (containsIgnoreCase(bukkitName, "spigot") + || containsIgnoreCase(bukkitVersion, "spigot") + || containsIgnoreCase(serverClassName, "spigot")) { + return ServerFamily.SPIGOT; + } + + if (containsIgnoreCase(bukkitName, "craftbukkit") + || containsIgnoreCase(bukkitVersion, "craftbukkit") + || containsIgnoreCase(serverClassName, "craftbukkit") + || containsIgnoreCase(bukkitName, "bukkit") + || containsIgnoreCase(bukkitVersion, "bukkit")) { + return ServerFamily.BUKKIT; + } + + if (regionizedRuntime || J.isFolia()) { + return ServerFamily.FOLIA; + } + + return ServerFamily.UNKNOWN; + } + + private static boolean hasCanvasRuntime() { + try { + Class.forName("io.canvasmc.canvas.region.WorldRegionizer"); + return true; + } catch (Throwable ignored) { + return false; + } + } + + private static boolean containsIgnoreCase(String value, String needle) { + if (value == null || needle == null || needle.isEmpty()) { + return false; + } + return value.toLowerCase(Locale.ROOT).contains(needle.toLowerCase(Locale.ROOT)); + } + + private static Object[] resolveWorldsProvider() throws Throwable { + try { + Class worldsProviderClass = Class.forName("net.thenextlvl.worlds.api.WorldsProvider"); + Class levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem"); + Class generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType"); + Object provider = Bukkit.getServicesManager().load(worldsProviderClass); + String resolution = provider == null ? "inactive(service not registered)" : "active(service=" + provider.getClass().getName() + ")"; + return new Object[]{provider, levelStemClass, generatorTypeClass, resolution}; + } catch (Throwable ignored) { + } + + Collection> knownServices = Bukkit.getServicesManager().getKnownServices(); + for (Class serviceClass : knownServices) { + if (!"net.thenextlvl.worlds.api.WorldsProvider".equals(serviceClass.getName())) { + continue; + } + + RegisteredServiceProvider registration = Bukkit.getServicesManager().getRegistration(serviceClass); + if (registration == null) { + continue; + } + + Object provider = registration.getProvider(); + ClassLoader loader = serviceClass.getClassLoader(); + if (loader == null && provider != null) { + loader = provider.getClass().getClassLoader(); + } + if (loader == null) { + continue; + } + + Class levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem", false, loader); + Class generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType", false, loader); + return new Object[]{provider, levelStemClass, generatorTypeClass, "active(service-scan=" + provider.getClass().getName() + ")"}; + } + + return new Object[]{null, null, null, "inactive(service scan found nothing)"}; + } + + private static final class TestingPaperLikeServer { + private final TestingWorldLoaderContext worldLoaderContext; + private final Object registryAccess; + + private TestingPaperLikeServer(Object datapackDimensions, Object registryAccess) { + this.worldLoaderContext = new TestingWorldLoaderContext(datapackDimensions); + this.registryAccess = registryAccess; + } + + @SuppressWarnings("unused") + private void createLevel(Object levelStem, Object worldLoadingInfoAndData, Object worldDataAndGenSettings) { + } + + @SuppressWarnings("unused") + private Object registryAccess() { + return registryAccess; + } + } + + private static final class TestingWorldLoaderContext { + private final Object datapackDimensions; + + private TestingWorldLoaderContext(Object datapackDimensions) { + this.datapackDimensions = datapackDimensions; + } + + @SuppressWarnings("unused") + private Object datapackDimensions() { + return datapackDimensions; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/DirectoryDurability.java b/core/src/main/java/art/arcane/iris/core/lifecycle/DirectoryDurability.java new file mode 100644 index 000000000..8d645c115 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/DirectoryDurability.java @@ -0,0 +1,42 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.spi.IrisLogging; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +/** + * Directory-level durability barrier shared by every atomic publication in the + * world-replacement protocol (journal writes, world-directory moves, bukkit.yml + * saves). A rename is only durable once its parent directory has been fsynced; + * skipped on Windows, where directory handles cannot be forced. + */ +final class DirectoryDurability { + private DirectoryDurability() { + } + + static void forceDirectoryRequired(Path directory) throws IOException { + if (File.separatorChar == '\\') { + return; + } + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } catch (UnsupportedOperationException failure) { + throw new IOException("Directory durability sync is unavailable for " + directory + ".", failure); + } + } + + static void forceDirectoryAfterCommit(Path directory, String context) { + try { + forceDirectoryRequired(directory); + } catch (IOException failure) { + IrisLogging.reportError( + context + " completed, but its parent directory could not be durability-synced.", + failure + ); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/HuskWorldQuarantine.java b/core/src/main/java/art/arcane/iris/core/lifecycle/HuskWorldQuarantine.java new file mode 100644 index 000000000..cec987525 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/HuskWorldQuarantine.java @@ -0,0 +1,252 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.WorldSlotKey; + +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * Moves a hot-deleted Iris world's husk out of the dimensions tree before any level is created. + *

+ * Deleting a loaded world's folder and letting the server save the level back leaves a directory holding + * nothing but the server's own {@code data/} skeleton: no frozen pack snapshot, no regions, no mantle. The + * server still enumerates it, and both outcomes stop startup - Iris refuses to boot over a directory it + * cannot generate, and excluding it from the dimension registry instead trips Paper's interactive + * world-migration gate, which consumes no console input and hangs the boot forever. + *

+ * The husk is moved, never deleted: it is recoverable, and the move alone is enough to let the server start. + * The test is deliberately narrow. Anything with chunk data, an {@code iris/} directory holding anything of + * Iris' own, a symbolic link anywhere on the path, or a directory that cannot be read is left exactly where it + * is, for {@link BukkitWorldConfiguration#auditIrisWorldStorage} to fail closed on. + */ +public final class HuskWorldQuarantine { + private static final String IRIS_NAMESPACE = "iris"; + private static final Pattern MANAGED_KEY = Pattern.compile("[a-z0-9_-]+"); + private static final String[] CHUNK_DATA_DIRECTORIES = {"region", "entities", "poi", "mantle"}; + private static final Set OS_METADATA_FILES = Set.of(".DS_Store", "Thumbs.db", "desktop.ini"); + private static final String APPLE_DOUBLE_PREFIX = "._"; + private static final DateTimeFormatter STAMP = DateTimeFormatter + .ofPattern("yyyyMMdd'T'HHmmss") + .withZone(ZoneOffset.UTC); + private static final int MAX_DESTINATION_ATTEMPTS = 64; + + private HuskWorldQuarantine() { + } + + public static List quarantineWorthlessHusks(Path levelRoot, Consumer warn) { + return quarantineWorthlessHusks(levelRoot, warn, Instant.now()); + } + + static List quarantineWorthlessHusks(Path levelRoot, Consumer warn, Instant at) { + Path root = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize(); + Consumer log = Objects.requireNonNull(warn, "warn"); + Instant stamp = Objects.requireNonNull(at, "at"); + String levelName = root.getFileName() == null ? null : root.getFileName().toString(); + if (levelName == null || levelName.isBlank()) { + return List.of(); + } + Path namespace = root.resolve("dimensions").resolve(IRIS_NAMESPACE); + if (Files.isSymbolicLink(namespace) || !Files.isDirectory(namespace, LinkOption.NOFOLLOW_LINKS)) { + return List.of(); + } + + List candidates; + try (Stream entries = Files.list(namespace)) { + candidates = entries.sorted().toList(); + } catch (IOException unreadable) { + // An unreadable namespace is not proof that there is nothing in it to lose. + return List.of(); + } + + List quarantined = new ArrayList<>(); + for (Path candidate : candidates) { + String key = candidate.getFileName().toString(); + if (!MANAGED_KEY.matcher(key).matches() || !isWorthlessHusk(candidate)) { + continue; + } + String worldName; + try { + worldName = IrisWorldStorage.configuredWorldName( + new WorldSlotKey(IRIS_NAMESPACE, key), + levelName); + } catch (RuntimeException notAManagedWorld) { + continue; + } + Path destination; + try { + destination = move(root, key, candidate, stamp); + } catch (IOException failure) { + log.accept("Iris husk world " + worldName + " at " + candidate + + " could not be moved aside (" + describe(failure) + + "); startup will stop until it is moved or deleted by hand."); + continue; + } + quarantined.add(new Quarantine(key, candidate, destination)); + log.accept("Iris husk world " + worldName + ": no pack snapshot and no world data; moved " + + candidate + " -> " + destination + + ". Drop the entry with /iris remove world=" + worldName + " delete=true."); + } + return List.copyOf(quarantined); + } + + /** + * True when nothing in this directory can be lost: no frozen pack snapshot, nothing of Iris' own under + * {@code iris/}, and no stored chunk data. Every uncertain answer - a symbolic link, an unreadable + * directory, an entry that is not the type it should be - is false, because uncertainty is not proof of + * worthlessness. + */ + private static boolean isWorthlessHusk(Path dimensionRoot) { + if (!isReadableDirectory(dimensionRoot)) { + return false; + } + if (holdsIrisContent(dimensionRoot.resolve("iris"))) { + return false; + } + for (String directory : CHUNK_DATA_DIRECTORIES) { + if (hasEntries(dimensionRoot.resolve(directory))) { + return false; + } + } + return true; + } + + /** + * True when the {@code iris/} entry holds anything of Iris' own. + *

+ * The desktop file managers recreate a directory to hold their own metadata while a delete is still in + * flight - on macOS a world folder can come back holding nothing but {@code iris/.DS_Store} - and a folder + * that holds only that is exactly as worthless as one with no {@code iris/} entry at all. Only files are + * ever treated as metadata: a directory, a symbolic link, an unreadable entry and anything that is not on + * the list all count as content. + */ + private static boolean holdsIrisContent(Path irisRoot) { + if (Files.isSymbolicLink(irisRoot)) { + return true; + } + if (!Files.exists(irisRoot, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + if (!Files.isDirectory(irisRoot, LinkOption.NOFOLLOW_LINKS)) { + return true; + } + try (Stream entries = Files.list(irisRoot)) { + return entries.anyMatch(entry -> !isOperatingSystemMetadata(entry)); + } catch (IOException unreadable) { + return true; + } + } + + private static boolean isOperatingSystemMetadata(Path entry) { + if (!Files.isRegularFile(entry, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + Path name = entry.getFileName(); + if (name == null) { + return false; + } + String fileName = name.toString(); + return OS_METADATA_FILES.contains(fileName) || fileName.startsWith(APPLE_DOUBLE_PREFIX); + } + + /** + * True when the path is a real directory that can actually be listed. The permission bits alone are not + * the answer: what matters is whether Iris can see everything in it before deciding it holds nothing. + */ + private static boolean isReadableDirectory(Path directory) { + if (Files.isSymbolicLink(directory) || !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + try (Stream entries = Files.list(directory)) { + entries.findAny(); + return true; + } catch (IOException unreadable) { + return false; + } + } + + private static boolean hasEntries(Path directory) { + if (Files.isSymbolicLink(directory)) { + return true; + } + if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS) + && !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + return true; + } + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + try (Stream entries = Files.list(directory)) { + return entries.findAny().isPresent(); + } catch (IOException unreadable) { + return true; + } + } + + private static Path move(Path levelRoot, String key, Path husk, Instant at) throws IOException { + Path huskRoot = requireDirectoryPath(levelRoot, levelRoot.resolve(IRIS_NAMESPACE).resolve("husks")); + String base = key + "-" + STAMP.format(at); + for (int attempt = 1; attempt <= MAX_DESTINATION_ATTEMPTS; attempt++) { + Path destination = huskRoot.resolve(attempt == 1 ? base : base + "-" + attempt); + if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + try { + Files.move(husk, destination, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException notAtomic) { + Files.move(husk, destination); + } + DirectoryDurability.forceDirectoryAfterCommit(huskRoot, "An Iris husk world quarantine"); + return destination; + } + throw new IOException("No free husk destination under " + huskRoot + " for " + key); + } + + /** + * Creates the quarantine directory, refusing when any segment below the level root is a symbolic link so + * a hostile or accidental link cannot redirect a world folder out of the level root. Segments above the + * level root are the server's own installation path and are not Iris' to judge. + */ + private static Path requireDirectoryPath(Path levelRoot, Path directory) throws IOException { + Path current = levelRoot; + for (Path segment : levelRoot.relativize(directory)) { + current = current.resolve(segment); + if (Files.isSymbolicLink(current)) { + throw new IOException("Husk quarantine path contains a symbolic link: " + current); + } + if (Files.exists(current, LinkOption.NOFOLLOW_LINKS) + && !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Husk quarantine path is not a directory: " + current); + } + } + Files.createDirectories(directory); + return directory; + } + + private static String describe(Throwable failure) { + String message = failure.getMessage(); + return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message; + } + + public record Quarantine(String worldKey, Path husk, Path destination) { + public Quarantine { + Objects.requireNonNull(worldKey, "worldKey"); + Objects.requireNonNull(husk, "husk"); + Objects.requireNonNull(destination, "destination"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/IrisWorldRemovalService.java b/core/src/main/java/art/arcane/iris/core/lifecycle/IrisWorldRemovalService.java new file mode 100644 index 000000000..ace825999 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/IrisWorldRemovalService.java @@ -0,0 +1,1179 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.IrisWorlds; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.SnapshotDirectoryTreeDeleter; +import art.arcane.iris.core.WorldRemovalPathPolicy; +import art.arcane.iris.core.link.MultiverseCoreLink; +import art.arcane.iris.core.runtime.WorldDeletionQueue; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.misc.ServerProperties; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; + +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public final class IrisWorldRemovalService { + private static final long PHASE_TIMEOUT_SECONDS = 120L; + private static final IrisWorldRemovalService INSTANCE = new IrisWorldRemovalService( + LifecycleOperationCoordinator.get(), + new BukkitRemovalBackend(ForkJoinPool.commonPool()) + ); + + private final LifecycleOperationCoordinator coordinator; + private final RemovalBackend backend; + private final long phaseTimeoutMillis; + private final Consumer restartRequester; + + IrisWorldRemovalService(LifecycleOperationCoordinator coordinator, RemovalBackend backend) { + this(coordinator, new ServiceOptions( + backend, + TimeUnit.SECONDS.toMillis(PHASE_TIMEOUT_SECONDS), + ServerConfigurator::restart + )); + } + + IrisWorldRemovalService(LifecycleOperationCoordinator coordinator, ServiceOptions options) { + this.coordinator = Objects.requireNonNull(coordinator, "coordinator"); + ServiceOptions requiredOptions = Objects.requireNonNull(options, "options"); + backend = requiredOptions.backend(); + phaseTimeoutMillis = requiredOptions.phaseTimeoutMillis(); + restartRequester = requiredOptions.restartRequester(); + } + + public static IrisWorldRemovalService get() { + return INSTANCE; + } + + public CompletableFuture remove(String worldIdentifier, boolean deleteFiles) { + String operationTarget = normalizeOperationTarget(worldIdentifier); + if (operationTarget == null) { + return CompletableFuture.completedFuture(RemovalResult.failure( + RemovalStatus.INVALID_IDENTIFIER, + worldIdentifier, + null, + new IllegalArgumentException("World identifier cannot be empty.") + )); + } + + LifecycleOperationCoordinator.Lease lease; + try { + lease = coordinator.acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_REMOVE, + operationTarget + ); + } catch (LifecycleOperationCoordinator.BusyException exception) { + return CompletableFuture.completedFuture(RemovalResult.busy(worldIdentifier, exception.currentOperation())); + } catch (Throwable failure) { + return CompletableFuture.completedFuture(RemovalResult.failure( + RemovalStatus.INTERNAL_FAILURE, + worldIdentifier, + null, + failure + )); + } + + CompletableFuture operation; + try { + WorldRemovalPathPolicy.Target target = backend.resolveTarget(operationTarget); + operation = execute(target, deleteFiles, new AtomicBoolean(false)); + } catch (WorldRemovalPathPolicy.Rejection rejection) { + operation = CompletableFuture.completedFuture(RemovalResult.failure( + mapRejection(rejection.reason()), + worldIdentifier, + null, + rejection + )); + } catch (Throwable failure) { + operation = CompletableFuture.completedFuture(RemovalResult.failure( + RemovalStatus.RESOLUTION_FAILED, + worldIdentifier, + null, + failure + )); + } + + CompletableFuture result = new CompletableFuture<>(); + operation.whenComplete((removalResult, throwable) -> completeAndRelease( + result, + lease, + worldIdentifier, + removalResult, + throwable + )); + return result; + } + + private CompletableFuture execute( + WorldRemovalPathPolicy.Target target, + boolean deleteFiles, + AtomicBoolean terminal + ) { + return phase(RemovalStatus.RESOLUTION_FAILED, () -> backend.resolve(target), terminal) + .thenCompose(resolvedWorld -> validateResolvedWorld(resolvedWorld)) + .thenCompose(resolvedWorld -> runWithMaintenance(resolvedWorld, deleteFiles, terminal)) + .exceptionally(throwable -> failureResult(target, throwable)); + } + + private CompletableFuture runWithMaintenance( + ResolvedWorld resolved, + boolean deleteFiles, + AtomicBoolean terminal + ) { + CompletableFuture operation = phase( + RemovalStatus.RESOLUTION_FAILED, + () -> backend.beginMaintenance(resolved).thenApply(ignored -> resolved), + terminal + ) + .thenCompose(activeWorld -> phase( + RemovalStatus.TELEPORT_FAILED, + () -> backend.evacuatePlayers(activeWorld) + .thenApply(ignored -> activeWorld), + terminal + )) + .thenCompose(activeWorld -> phase( + RemovalStatus.UNLOAD_FAILED, + () -> backend.unloadWorld(activeWorld) + .thenApply(unloaded -> requireUnloaded(activeWorld, unloaded)), + terminal + )) + .thenCompose(activeWorld -> phase( + RemovalStatus.GENERATOR_CLOSE_FAILED, + () -> backend.closeGenerator(activeWorld) + .thenApply(ignored -> activeWorld), + terminal + )) + .thenCompose(activeWorld -> phase( + RemovalStatus.CONFIGURATION_FAILED, + () -> backend.unregister(activeWorld.target(), terminal::get), + terminal + ).thenCompose(configuration -> { + if (configuration.failure() != null) { + return CompletableFuture.completedFuture(RemovalResult.partialFailure( + RemovalStatus.CONFIGURATION_FAILED, + activeWorld.target(), + null, + configuration.changed(), + false, + configuration.failure() + )); + } + return unregisterRegistryAndFinish( + activeWorld, + configuration.changed(), + deleteFiles, + terminal + ); + })); + + return operation.handle((result, throwable) -> backend.endMaintenance(resolved) + .handle((ignored, cleanupFailure) -> { + if (cleanupFailure != null) { + Throwable unwrappedCleanup = unwrap(cleanupFailure); + if (throwable != null) { + Throwable original = unwrap(throwable); + original.addSuppressed(unwrappedCleanup); + throw new CompletionException(original); + } + throw new CompletionException(unwrappedCleanup); + } + if (throwable != null) { + throw new CompletionException(unwrap(throwable)); + } + return result; + })).thenCompose(result -> result); + } + + private CompletableFuture validateResolvedWorld(ResolvedWorld resolvedWorld) { + if (!resolvedWorld.managed()) { + return CompletableFuture.failedFuture(new RemovalFailure( + RemovalStatus.NOT_FOUND, + new IllegalStateException("No Iris-managed world was found for " + + resolvedWorld.target().worldKey() + ".") + )); + } + if (resolvedWorld.loadedWorld() != null && resolvedWorld.generator() == null) { + return CompletableFuture.failedFuture(new RemovalFailure( + RemovalStatus.NOT_IRIS_WORLD, + new IllegalStateException("Loaded world " + resolvedWorld.target().worldKey() + + " is not using an Iris generator.") + )); + } + if (resolvedWorld.conflictingConfiguration()) { + return CompletableFuture.failedFuture(new RemovalFailure( + RemovalStatus.NOT_IRIS_WORLD, + new IllegalStateException("bukkit.yml assigns a non-Iris generator to " + + resolvedWorld.target().logicalName() + ".") + )); + } + return CompletableFuture.completedFuture(resolvedWorld); + } + + private CompletableFuture unregisterRegistryAndFinish( + ResolvedWorld resolvedWorld, + boolean configurationChanged, + boolean deleteFiles, + AtomicBoolean terminal + ) { + return phase( + RemovalStatus.REGISTRY_FAILED, + () -> backend.unregisterRegistry(resolvedWorld.target(), terminal::get), + terminal + ).handle((registryChanged, throwable) -> { + if (throwable == null) { + return finishRemoval( + resolvedWorld, + configurationChanged, + registryChanged, + deleteFiles, + terminal + ); + } + + Throwable failure = unwrap(throwable); + RemovalStatus status = RemovalStatus.INTERNAL_FAILURE; + Throwable cause = failure; + if (failure instanceof RemovalFailure removalFailure) { + status = removalFailure.status(); + cause = removalFailure.getCause(); + } + return CompletableFuture.completedFuture(RemovalResult.partialFailure( + status, + resolvedWorld.target(), + null, + configurationChanged, + false, + cause + )); + }).thenCompose(result -> result); + } + + private ResolvedWorld requireUnloaded(ResolvedWorld resolvedWorld, Boolean unloaded) { + if (!Boolean.TRUE.equals(unloaded)) { + throw new RemovalFailure( + RemovalStatus.UNLOAD_FAILED, + new IllegalStateException("World lifecycle backend refused to unload " + + resolvedWorld.target().worldKey() + ".") + ); + } + return resolvedWorld; + } + + private CompletableFuture finishRemoval( + ResolvedWorld resolvedWorld, + boolean configurationChanged, + boolean registryChanged, + boolean deleteFiles, + AtomicBoolean terminal + ) { + WorldRemovalPathPolicy.Target target = resolvedWorld.target(); + if (!deleteFiles) { + return CompletableFuture.completedFuture(RemovalResult.success( + RemovalStatus.UNREGISTERED, + target, + null, + configurationChanged, + registryChanged + )); + } + + return phase( + RemovalStatus.QUARANTINE_FAILED, + () -> backend.delete(target, terminal::get), + terminal + ) + .handle((disposition, throwable) -> { + if (throwable == null) { + return RemovalResult.success( + disposition.queued() ? RemovalStatus.DELETE_QUEUED : RemovalStatus.DELETED, + target, + disposition.quarantineDirectory(), + configurationChanged, + registryChanged + ); + } + + Throwable failure = unwrap(throwable); + if (failure instanceof RemovalFailure removalFailure) { + return RemovalResult.partialFailure( + removalFailure.status(), + target, + removalFailure.quarantineDirectory(), + configurationChanged, + registryChanged, + removalFailure.getCause() + ); + } + return RemovalResult.partialFailure( + RemovalStatus.INTERNAL_FAILURE, + target, + null, + configurationChanged, + registryChanged, + failure + ); + }); + } + + private CompletableFuture phase( + RemovalStatus failureStatus, + Supplier> operation, + AtomicBoolean terminal + ) { + if (terminal.get()) { + return CompletableFuture.failedFuture(wrapFailure( + failureStatus, + new TimeoutException("World removal already crossed its terminal timeout boundary.") + )); + } + + CompletableFuture future; + try { + future = Objects.requireNonNull(operation.get(), "Removal phase returned no completion future."); + } catch (Throwable failure) { + return CompletableFuture.failedFuture(wrapFailure(failureStatus, failure)); + } + CompletableFuture guarded = new CompletableFuture<>(); + AtomicBoolean settled = new AtomicBoolean(false); + future.whenComplete((value, throwable) -> { + if (!settled.compareAndSet(false, true)) { + return; + } + if (throwable == null) { + guarded.complete(value); + } else { + guarded.completeExceptionally(unwrap(throwable)); + } + }); + CompletableFuture.delayedExecutor(phaseTimeoutMillis, TimeUnit.MILLISECONDS).execute(() -> { + if (!settled.compareAndSet(false, true)) { + return; + } + + terminal.set(true); + String phaseName = failureStatus.name().toLowerCase(); + String restartReason = "World removal timed out during " + phaseName + "."; + TimeoutException timeout = new TimeoutException("World removal phase " + + phaseName + " did not settle within " + phaseTimeoutMillis + " milliseconds."); + try { + restartRequester.accept(restartReason); + } catch (Throwable restartFailure) { + timeout.addSuppressed(restartFailure); + } + if (coordinator.active(LifecycleOperationCoordinator.Domain.SERVER_LIFECYCLE).isEmpty()) { + coordinator.quiesceForRestart(() -> IrisLogging.error( + restartReason + " Automatic restart dispatch failed; restart the server manually.")); + } + try { + future.cancel(false); + } catch (Throwable cancellationFailure) { + timeout.addSuppressed(cancellationFailure); + } + guarded.completeExceptionally(timeout); + }); + return guarded.handle((value, throwable) -> { + if (throwable != null) { + throw wrapFailure(failureStatus, throwable); + } + return value; + }); + } + + private RemovalResult failureResult(WorldRemovalPathPolicy.Target target, Throwable throwable) { + Throwable failure = unwrap(throwable); + if (failure instanceof RemovalFailure removalFailure) { + return RemovalResult.failure( + removalFailure.status(), + target.requestedIdentifier(), + target, + removalFailure.getCause() + ); + } + return RemovalResult.failure( + RemovalStatus.INTERNAL_FAILURE, + target.requestedIdentifier(), + target, + failure + ); + } + + private RemovalFailure wrapFailure(RemovalStatus status, Throwable throwable) { + Throwable failure = unwrap(throwable); + if (failure instanceof RemovalFailure removalFailure) { + return removalFailure; + } + if (failure instanceof WorldRemovalPathPolicy.Rejection rejection) { + return new RemovalFailure(mapRejection(rejection.reason()), rejection); + } + return new RemovalFailure(status, failure); + } + + private void completeAndRelease( + CompletableFuture destination, + LifecycleOperationCoordinator.Lease lease, + String worldIdentifier, + RemovalResult removalResult, + Throwable throwable + ) { + RemovalResult completedResult = removalResult; + if (throwable != null) { + completedResult = RemovalResult.failure( + RemovalStatus.INTERNAL_FAILURE, + worldIdentifier, + null, + unwrap(throwable) + ); + } + try { + lease.close(); + } catch (Throwable releaseFailure) { + Throwable existingFailure = completedResult == null ? null : completedResult.failure(); + if (existingFailure != null) { + releaseFailure.addSuppressed(existingFailure); + } + completedResult = RemovalResult.failure( + RemovalStatus.INTERNAL_FAILURE, + worldIdentifier, + completedResult == null ? null : completedResult.target(), + releaseFailure + ); + } + destination.complete(completedResult); + } + + private static String normalizeOperationTarget(String worldIdentifier) { + if (worldIdentifier == null || worldIdentifier.isBlank()) { + return null; + } + return worldIdentifier.trim(); + } + + private static RemovalStatus mapRejection(WorldRemovalPathPolicy.RejectionReason reason) { + return switch (reason) { + case INVALID_IDENTIFIER -> RemovalStatus.INVALID_IDENTIFIER; + case CONFIGURED_MAIN_WORLD, MINECRAFT_NAMESPACE -> RemovalStatus.PROTECTED_WORLD; + case NOT_IRIS_NAMESPACE -> RemovalStatus.NOT_IRIS_WORLD; + case OUTSIDE_STORAGE_ROOT, SYMBOLIC_LINK -> RemovalStatus.UNSAFE_PATH; + }; + } + + private static Throwable unwrap(Throwable throwable) { + Throwable current = throwable; + while (!(current instanceof RemovalFailure) + && (current instanceof CompletionException || current instanceof ExecutionException) + && current.getCause() != null) { + current = current.getCause(); + } + return current; + } + + record ServiceOptions( + RemovalBackend backend, + long phaseTimeoutMillis, + Consumer restartRequester + ) { + ServiceOptions { + Objects.requireNonNull(backend, "backend"); + if (phaseTimeoutMillis < 1L) { + throw new IllegalArgumentException("phaseTimeoutMillis must be positive"); + } + Objects.requireNonNull(restartRequester, "restartRequester"); + } + } + + interface RemovalBackend { + WorldRemovalPathPolicy.Target resolveTarget(String identifier); + + CompletableFuture resolve(WorldRemovalPathPolicy.Target target); + + CompletableFuture evacuatePlayers(ResolvedWorld resolvedWorld); + + CompletableFuture beginMaintenance(ResolvedWorld resolvedWorld); + + CompletableFuture endMaintenance(ResolvedWorld resolvedWorld); + + CompletableFuture closeGenerator(ResolvedWorld resolvedWorld); + + CompletableFuture unloadWorld(ResolvedWorld resolvedWorld); + + CompletableFuture unregister( + WorldRemovalPathPolicy.Target target, + BooleanSupplier terminal + ); + + CompletableFuture unregisterRegistry( + WorldRemovalPathPolicy.Target target, + BooleanSupplier terminal + ); + + CompletableFuture delete( + WorldRemovalPathPolicy.Target target, + BooleanSupplier terminal + ); + } + + record ResolvedWorld( + WorldRemovalPathPolicy.Target target, + World loadedWorld, + PlatformChunkGenerator generator, + boolean configurationManaged, + boolean conflictingConfiguration, + boolean registryManaged, + boolean directoryPresent + ) { + ResolvedWorld { + Objects.requireNonNull(target, "target"); + } + + boolean managed() { + return loadedWorld != null || configurationManaged || registryManaged || directoryPresent; + } + } + + record DeleteDisposition(boolean queued, Path quarantineDirectory) { + } + + record ConfigurationDisposition(boolean changed, Throwable failure) { + } + + public enum RemovalStatus { + UNREGISTERED, + DELETED, + DELETE_QUEUED, + BUSY, + INVALID_IDENTIFIER, + PROTECTED_WORLD, + NOT_IRIS_WORLD, + UNSAFE_PATH, + NOT_FOUND, + RESOLUTION_FAILED, + TELEPORT_FAILED, + GENERATOR_CLOSE_FAILED, + UNLOAD_FAILED, + CONFIGURATION_FAILED, + REGISTRY_FAILED, + QUARANTINE_FAILED, + DELETE_FAILED, + INTERNAL_FAILURE + } + + public record RemovalResult( + RemovalStatus status, + String requestedIdentifier, + WorldRemovalPathPolicy.Target target, + Path quarantineDirectory, + boolean configurationChanged, + boolean registryChanged, + LifecycleOperationCoordinator.ActiveOperation blockingOperation, + Throwable failure + ) { + public RemovalResult { + Objects.requireNonNull(status, "status"); + } + + static RemovalResult success( + RemovalStatus status, + WorldRemovalPathPolicy.Target target, + Path quarantineDirectory, + boolean configurationChanged, + boolean registryChanged + ) { + if (status != RemovalStatus.UNREGISTERED + && status != RemovalStatus.DELETED + && status != RemovalStatus.DELETE_QUEUED) { + throw new IllegalArgumentException("Not a successful removal status: " + status); + } + return new RemovalResult( + status, + target.requestedIdentifier(), + target, + quarantineDirectory, + configurationChanged, + registryChanged, + null, + null + ); + } + + static RemovalResult busy( + String requestedIdentifier, + LifecycleOperationCoordinator.ActiveOperation blockingOperation + ) { + return new RemovalResult( + RemovalStatus.BUSY, + requestedIdentifier, + null, + null, + false, + false, + Objects.requireNonNull(blockingOperation, "blockingOperation"), + null + ); + } + + static RemovalResult failure( + RemovalStatus status, + String requestedIdentifier, + WorldRemovalPathPolicy.Target target, + Throwable failure + ) { + return new RemovalResult( + status, + requestedIdentifier, + target, + null, + false, + false, + null, + Objects.requireNonNull(failure, "failure") + ); + } + + public boolean succeeded() { + return status == RemovalStatus.UNREGISTERED + || status == RemovalStatus.DELETED + || status == RemovalStatus.DELETE_QUEUED; + } + + public boolean deletionDeferred() { + return status == RemovalStatus.DELETE_QUEUED; + } + + public boolean busy() { + return status == RemovalStatus.BUSY; + } + + static RemovalResult partialFailure( + RemovalStatus status, + WorldRemovalPathPolicy.Target target, + Path quarantineDirectory, + boolean configurationChanged, + boolean registryChanged, + Throwable failure + ) { + return new RemovalResult( + status, + target.requestedIdentifier(), + target, + quarantineDirectory, + configurationChanged, + registryChanged, + null, + Objects.requireNonNull(failure, "failure") + ); + } + } + + static String bukkitConfigurationWorldName(WorldRemovalPathPolicy.Target target) { + WorldRemovalPathPolicy.Target requiredTarget = Objects.requireNonNull(target, "target"); + Path levelName = requiredTarget.levelRoot().getFileName(); + if (levelName == null || levelName.toString().isBlank()) { + throw new IllegalArgumentException("Level root must have a Bukkit startup name."); + } + return IrisWorldStorage.configuredWorldName(requiredTarget.worldKey(), levelName.toString()); + } + + static String bukkitGenerator( + YamlConfiguration configuration, + WorldRemovalPathPolicy.Target target + ) { + ConfigurationSection worlds = Objects.requireNonNull(configuration, "configuration") + .getConfigurationSection("worlds"); + if (worlds == null) { + return null; + } + ConfigurationSection world = worlds.getConfigurationSection(bukkitConfigurationWorldName(target)); + return world == null ? null : world.getString("generator"); + } + + private static final class RemovalFailure extends CompletionException { + private final RemovalStatus status; + private final Path quarantineDirectory; + + private RemovalFailure(RemovalStatus status, Throwable cause) { + this(status, cause, null); + } + + private RemovalFailure(RemovalStatus status, Throwable cause, Path quarantineDirectory) { + super(Objects.requireNonNull(cause, "cause")); + this.status = Objects.requireNonNull(status, "status"); + this.quarantineDirectory = quarantineDirectory; + } + + private RemovalStatus status() { + return status; + } + + private Path quarantineDirectory() { + return quarantineDirectory; + } + } + + private static final class BukkitRemovalBackend implements RemovalBackend { + private final Executor ioExecutor; + + private BukkitRemovalBackend(Executor ioExecutor) { + this.ioExecutor = Objects.requireNonNull(ioExecutor, "ioExecutor"); + } + + @Override + public WorldRemovalPathPolicy.Target resolveTarget(String identifier) { + String currentMainWorld = IrisWorldStorage.levelRoot().getName(); + String configuredMainWorld = IrisWorldStorage.configuredLevelName(); + return WorldRemovalPathPolicy.resolve( + identifier, + currentMainWorld, + List.of(currentMainWorld, configuredMainWorld), + IrisWorldStorage.levelRoot().toPath() + ); + } + + @Override + public CompletableFuture resolve(WorldRemovalPathPolicy.Target target) { + CompletableFuture runtimeInspection = onGlobal(() -> inspectRuntime(target)); + CompletableFuture diskInspection = CompletableFuture.supplyAsync( + () -> inspectDisk(target), + ioExecutor + ); + return runtimeInspection.thenCombine(diskInspection, (runtime, disk) -> new ResolvedWorld( + target, + runtime.world(), + runtime.generator(), + disk.configurationManaged(), + disk.conflictingConfiguration(), + runtime.registryManaged(), + disk.directoryPresent() + )); + } + + @Override + public CompletableFuture beginMaintenance(ResolvedWorld resolvedWorld) { + World world = resolvedWorld.loadedWorld(); + if (world != null) { + IrisToolbelt.beginWorldMaintenance(world, "world-remove", true); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture endMaintenance(ResolvedWorld resolvedWorld) { + World world = resolvedWorld.loadedWorld(); + if (world != null) { + IrisToolbelt.endWorldMaintenance(world, "world-remove", true); + } + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture evacuatePlayers(ResolvedWorld resolvedWorld) { + World world = resolvedWorld.loadedWorld(); + if (world == null) { + return CompletableFuture.completedFuture(null); + } + + return onGlobal(() -> createEvacuationPlan(world)).thenCompose(plan -> { + if (plan.players().isEmpty()) { + return CompletableFuture.completedFuture(null); + } + if (plan.destination() == null) { + return CompletableFuture.failedFuture(new IllegalStateException( + "No destination world is available for " + plan.players().size() + " player(s)." + )); + } + + List> teleports = new ArrayList<>(plan.players().size()); + for (Player player : plan.players()) { + teleports.add(teleport(player, world, plan.destination())); + } + return CompletableFuture.allOf(teleports.toArray(CompletableFuture[]::new)); + }); + } + + @Override + public CompletableFuture closeGenerator(ResolvedWorld resolvedWorld) { + PlatformChunkGenerator generator = resolvedWorld.generator(); + if (generator == null) { + return CompletableFuture.completedFuture(null); + } + CompletableFuture closeFuture = generator.closeAsync(); + if (closeFuture == null) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Iris generator returned no close completion future." + )); + } + return closeFuture; + } + + @Override + public CompletableFuture unloadWorld(ResolvedWorld resolvedWorld) { + World world = resolvedWorld.loadedWorld(); + if (world == null) { + return CompletableFuture.completedFuture(true); + } + return WorldLifecycleService.get().unloadAsync(world, true); + } + + @Override + public CompletableFuture unregister( + WorldRemovalPathPolicy.Target target, + BooleanSupplier terminal + ) { + return onGlobal(() -> { + requireNotTerminal(terminal, "Multiverse unregistration"); + return IrisServices.get(MultiverseCoreLink.class).removeFromConfig( + bukkitConfigurationWorldName(target) + ); + }).thenCompose(multiverseChanged -> { + if (terminal.getAsBoolean()) { + return CompletableFuture.failedFuture(new IllegalStateException( + "World removal stopped before Bukkit configuration unregistration.")); + } + return CompletableFuture.supplyAsync(() -> { + requireNotTerminal(terminal, "Bukkit configuration unregistration"); + try { + boolean bukkitChanged = BukkitWorldConfiguration.remove( + ServerProperties.BUKKIT_YML, + bukkitConfigurationWorldName(target) + ); + return new ConfigurationDisposition(multiverseChanged || bukkitChanged, null); + } catch (Throwable failure) { + return new ConfigurationDisposition(multiverseChanged, unwrap(failure)); + } + }, ioExecutor); + }); + } + + @Override + public CompletableFuture unregisterRegistry( + WorldRemovalPathPolicy.Target target, + BooleanSupplier terminal + ) { + return CompletableFuture.supplyAsync( + () -> { + requireNotTerminal(terminal, "Iris registry unregistration"); + return IrisWorlds.get().remove(target.worldKey().toString()); + }, + ioExecutor + ); + } + + @Override + public CompletableFuture delete( + WorldRemovalPathPolicy.Target target, + BooleanSupplier terminal + ) { + Path quarantine = target.levelRoot() + .resolve("dimensions/iris/.iris-delete-" + UUID.randomUUID()) + .toAbsolutePath() + .normalize(); + return CompletableFuture.supplyAsync( + () -> { + requireNotTerminal(terminal, "deletion intent"); + return queueDeletionIntent(target, quarantine); + }, + ioExecutor + ) + .thenCompose(queuedQuarantine -> { + if (queuedQuarantine == null) { + return CompletableFuture.completedFuture(null); + } + if (terminal.getAsBoolean()) { + return CompletableFuture.failedFuture(new IllegalStateException( + "World removal stopped after recording its deletion intent.")); + } + return onGlobal(() -> { + requireNotTerminal(terminal, "world quarantine"); + return quarantineWorld(target, queuedQuarantine); + }); + }) + .thenCompose(quarantined -> { + if (quarantined == null) { + return CompletableFuture.completedFuture(new DeleteDisposition(false, null)); + } + if (terminal.getAsBoolean()) { + return CompletableFuture.failedFuture(new IllegalStateException( + "World removal stopped after quarantining its directory.")); + } + return CompletableFuture.supplyAsync(() -> { + requireNotTerminal(terminal, "quarantine deletion"); + return deleteQuarantine(quarantined); + }, ioExecutor); + }); + } + + private static void requireNotTerminal(BooleanSupplier terminal, String stage) { + if (terminal.getAsBoolean()) { + throw new IllegalStateException("World removal stopped before " + stage + "."); + } + } + + private BukkitInspection inspectRuntime(WorldRemovalPathPolicy.Target target) { + World world = WorldIdentity.resolve(target.worldKey()).orElse(null); + PlatformChunkGenerator generator = null; + if (world != null) { + Path loadedDirectory = world.getWorldFolder().toPath().toAbsolutePath().normalize(); + WorldRemovalPathPolicy.validateStoragePath(target.levelRoot(), target.worldKey(), loadedDirectory); + generator = IrisToolbelt.access(world); + } + boolean registryManaged = IrisWorlds.get().getWorlds().containsKey(target.worldKey().toString()); + return new BukkitInspection(world, generator, registryManaged); + } + + private DiskInspection inspectDisk(WorldRemovalPathPolicy.Target target) { + WorldRemovalPathPolicy.validateStoragePath( + target.levelRoot(), + target.worldKey(), + target.worldDirectory() + ); + WorldRemovalPathPolicy.validateStorageRoot( + target.levelRoot(), + target.worldKey(), + target.storageDirectory() + ); + boolean directoryPresent = Files.isDirectory(target.storageDirectory(), LinkOption.NOFOLLOW_LINKS); + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML); + String generator = bukkitGenerator(configuration, target); + boolean configurationManaged = generator != null + && (generator.equalsIgnoreCase("Iris") || generator.regionMatches(true, 0, "Iris:", 0, 5)); + boolean conflictingConfiguration = generator != null && !configurationManaged; + return new DiskInspection(configurationManaged, conflictingConfiguration, directoryPresent); + } + + private EvacuationPlan createEvacuationPlan(World source) { + List players = List.copyOf(source.getPlayers()); + Location destination = null; + for (World candidate : Bukkit.getWorlds()) { + if (!WorldIdentity.key(candidate).equals(WorldIdentity.key(source))) { + destination = candidate.getSpawnLocation().clone(); + break; + } + } + return new EvacuationPlan(players, destination); + } + + private CompletableFuture teleport(Player player, World source, Location destination) { + CompletableFuture result = new CompletableFuture<>(); + Runnable operation = () -> { + try { + if (!WorldIdentity.key(player.getWorld()).equals(WorldIdentity.key(source))) { + result.complete(null); + return; + } + CompletableFuture teleportFuture = BukkitPlatform.teleportAsync(player, destination); + if (teleportFuture == null) { + result.completeExceptionally(new IllegalStateException( + "No teleport completion future was returned for " + player.getName() + "." + )); + return; + } + teleportFuture.whenComplete((success, throwable) -> { + if (throwable != null) { + result.completeExceptionally(throwable); + } else if (!Boolean.TRUE.equals(success)) { + result.completeExceptionally(new IllegalStateException( + "Player evacuation was refused for " + player.getName() + "." + )); + } else { + result.complete(null); + } + }); + } catch (Throwable failure) { + result.completeExceptionally(failure); + } + }; + boolean scheduled = J.runEntity(player, operation, 0, () -> result.completeExceptionally( + new IllegalStateException("Player retired before evacuation: " + player.getName() + ".") + )); + if (!scheduled) { + result.completeExceptionally(new IllegalStateException( + "Failed to schedule evacuation for " + player.getName() + "." + )); + } + return result; + } + + private Path queueDeletionIntent(WorldRemovalPathPolicy.Target target, Path quarantine) { + WorldRemovalPathPolicy.validateStoragePath( + target.levelRoot(), + target.worldKey(), + target.worldDirectory() + ); + WorldRemovalPathPolicy.validateStorageRoot( + target.levelRoot(), + target.worldKey(), + target.storageDirectory() + ); + Path storageDirectory = target.storageDirectory(); + if (!Files.exists(storageDirectory, LinkOption.NOFOLLOW_LINKS)) { + return null; + } + if (!Files.isDirectory(storageDirectory, LinkOption.NOFOLLOW_LINKS)) { + throw new RemovalFailure( + RemovalStatus.QUARANTINE_FAILED, + new IOException("Iris world target is not a directory: " + storageDirectory) + ); + } + + requireSafeQuarantineParent(target, quarantine); + + WorldDeletionQueue deletionQueue = IrisServices.getOrNull(WorldDeletionQueue.class); + if (deletionQueue == null) { + throw new RemovalFailure( + RemovalStatus.QUARANTINE_FAILED, + new IOException("World deletion queue is unavailable; no files were moved.") + ); + } + try { + deletionQueue.queueExactForStartupDeletion(List.of(quarantine.getFileName().toString())); + return quarantine; + } catch (IOException failure) { + throw new RemovalFailure(RemovalStatus.QUARANTINE_FAILED, failure); + } + } + + private Path quarantineWorld(WorldRemovalPathPolicy.Target target, Path quarantine) { + if (WorldIdentity.resolve(target.worldKey()).isPresent()) { + throw new RemovalFailure( + RemovalStatus.QUARANTINE_FAILED, + new IllegalStateException("World became loaded again before quarantine: " + + target.worldKey() + ".") + ); + } + WorldRemovalPathPolicy.validateStoragePath( + target.levelRoot(), + target.worldKey(), + target.worldDirectory() + ); + WorldRemovalPathPolicy.validateStorageRoot( + target.levelRoot(), + target.worldKey(), + target.storageDirectory() + ); + Path storageDirectory = target.storageDirectory(); + if (!Files.exists(storageDirectory, LinkOption.NOFOLLOW_LINKS)) { + return null; + } + if (!Files.isDirectory(storageDirectory, LinkOption.NOFOLLOW_LINKS)) { + throw new RemovalFailure( + RemovalStatus.QUARANTINE_FAILED, + new IOException("Iris world target is not a directory: " + storageDirectory) + ); + } + try { + try { + Files.move(storageDirectory, quarantine, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + Files.move(storageDirectory, quarantine); + } + return quarantine; + } catch (IOException failure) { + throw new RemovalFailure(RemovalStatus.QUARANTINE_FAILED, failure); + } + } + + private static void requireSafeQuarantineParent( + WorldRemovalPathPolicy.Target target, + Path quarantine + ) { + Path expectedParent = target.levelRoot().resolve("dimensions/iris").toAbsolutePath().normalize(); + if (!Objects.equals(quarantine.getParent(), expectedParent)) { + throw new RemovalFailure( + RemovalStatus.QUARANTINE_FAILED, + new IOException("Iris quarantine path is outside its exact namespace root: " + quarantine) + ); + } + try { + Path current = target.levelRoot().toAbsolutePath().normalize(); + for (Path segment : target.levelRoot().relativize(expectedParent)) { + current = current.resolve(segment); + if (Files.isSymbolicLink(current)) { + throw new IOException("Iris quarantine storage contains a symbolic link: " + current); + } + } + Files.createDirectories(expectedParent); + } catch (IOException failure) { + throw new RemovalFailure(RemovalStatus.QUARANTINE_FAILED, failure); + } + } + + private DeleteDisposition deleteQuarantine(Path quarantine) { + try { + SnapshotDirectoryTreeDeleter.delete(quarantine); + return new DeleteDisposition(false, quarantine); + } catch (Throwable deletionFailure) { + IrisLogging.reportError( + "Immediate Iris world deletion failed; startup cleanup remains queued for " + quarantine + ".", + deletionFailure + ); + return new DeleteDisposition(true, quarantine); + } + } + + private static CompletableFuture onGlobal(Supplier supplier) { + CompletableFuture result = new CompletableFuture<>(); + boolean scheduled = J.runGlobal(() -> { + try { + result.complete(supplier.get()); + } catch (Throwable failure) { + result.completeExceptionally(failure); + } + }); + if (!scheduled) { + result.completeExceptionally(new IllegalStateException("Failed to schedule world removal on the global thread.")); + } + return result; + } + + private record BukkitInspection( + World world, + PlatformChunkGenerator generator, + boolean registryManaged + ) { + } + + private record DiskInspection( + boolean configurationManaged, + boolean conflictingConfiguration, + boolean directoryPresent + ) { + } + + private record EvacuationPlan(List players, Location destination) { + private EvacuationPlan { + Objects.requireNonNull(players, "players"); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/LifecycleOperationCoordinator.java b/core/src/main/java/art/arcane/iris/core/lifecycle/LifecycleOperationCoordinator.java new file mode 100644 index 000000000..406a45581 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/LifecycleOperationCoordinator.java @@ -0,0 +1,275 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.spi.IrisLogging; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class LifecycleOperationCoordinator { + private static final LifecycleOperationCoordinator INSTANCE = new LifecycleOperationCoordinator(); + + private final Object monitor; + private final ArrayDeque idleCallbacks; + private ActiveOperation mutationOperation; + private ActiveOperation restartOperation; + private Runnable pendingRestartCallback; + private long nextOperationId; + + LifecycleOperationCoordinator() { + monitor = new Object(); + idleCallbacks = new ArrayDeque<>(); + nextOperationId = 1L; + } + + public static LifecycleOperationCoordinator get() { + return INSTANCE; + } + + public Lease acquire(Domain domain, OperationKind kind, String target) { + Objects.requireNonNull(domain, "domain"); + Objects.requireNonNull(kind, "kind"); + String normalizedTarget = Objects.requireNonNull(target, "target").trim(); + if (normalizedTarget.isEmpty()) { + throw new IllegalArgumentException("target must not be blank"); + } + + synchronized (monitor) { + if (domain == Domain.SERVER_LIFECYCLE) { + throw new IllegalArgumentException("SERVER_LIFECYCLE is reserved for terminal operations"); + } + + ActiveOperation currentOperation = restartOperation == null ? mutationOperation : restartOperation; + if (currentOperation != null) { + throw new BusyException(currentOperation); + } + + ActiveOperation operation = new ActiveOperation(nextOperationId++, domain, kind, normalizedTarget); + mutationOperation = operation; + return new LeaseImpl(this, operation); + } + } + + public boolean quiesceForRestart(Runnable callback) { + Runnable restartCallback = Objects.requireNonNull(callback, "callback"); + ActiveOperation operation; + boolean dispatch; + synchronized (monitor) { + if (restartOperation != null) { + return false; + } + + operation = new ActiveOperation( + nextOperationId++, + Domain.SERVER_LIFECYCLE, + OperationKind.SERVER_RESTART, + "server"); + restartOperation = operation; + dispatch = mutationOperation == null; + if (!dispatch) { + pendingRestartCallback = restartCallback; + return true; + } + } + + runRestartCallback(restartCallback, operation); + return true; + } + + public Optional active(Domain domain) { + Objects.requireNonNull(domain, "domain"); + synchronized (monitor) { + if (domain == Domain.SERVER_LIFECYCLE) { + return Optional.ofNullable(restartOperation); + } + if (mutationOperation != null && mutationOperation.domain() == domain) { + return Optional.of(mutationOperation); + } + return Optional.empty(); + } + } + + public Map snapshot() { + synchronized (monitor) { + EnumMap operations = new EnumMap<>(Domain.class); + if (mutationOperation != null) { + operations.put(mutationOperation.domain(), mutationOperation); + } + if (restartOperation != null) { + operations.put(Domain.SERVER_LIFECYCLE, restartOperation); + } + return Collections.unmodifiableMap(operations); + } + } + + public boolean isIdle() { + synchronized (monitor) { + return mutationOperation == null && restartOperation == null; + } + } + + public void whenIdle(Runnable callback) { + Runnable idleCallback = Objects.requireNonNull(callback, "callback"); + synchronized (monitor) { + if (mutationOperation != null || restartOperation != null) { + idleCallbacks.addLast(idleCallback); + return; + } + runIdleCallback(idleCallback); + } + } + + private void release(ActiveOperation operation) { + Runnable restartCallback = null; + ActiveOperation terminalOperation = null; + synchronized (monitor) { + if (!operation.equals(mutationOperation)) { + throw new IllegalStateException("Lifecycle operation lease is not active: " + operation.id()); + } + + mutationOperation = null; + if (restartOperation != null && pendingRestartCallback != null) { + terminalOperation = restartOperation; + restartCallback = pendingRestartCallback; + pendingRestartCallback = null; + } else { + runIdleCallbacks(); + } + } + + if (restartCallback != null) { + runRestartCallback(restartCallback, terminalOperation); + } + } + + private void runIdleCallbacks() { + while (mutationOperation == null && restartOperation == null && !idleCallbacks.isEmpty()) { + runIdleCallback(idleCallbacks.removeFirst()); + } + } + + private void runIdleCallback(Runnable callback) { + try { + callback.run(); + } catch (Throwable failure) { + IrisLogging.reportError("Lifecycle idle callback failed.", failure); + } + } + + private void runRestartCallback(Runnable callback, ActiveOperation operation) { + try { + callback.run(); + } catch (Throwable failure) { + IrisLogging.reportError("Lifecycle restart dispatch failed for operation " + operation.id() + ".", failure); + } + } + + public enum Domain { + WORLD_MUTATION, + PACK_MUTATION, + SERVER_LIFECYCLE + } + + public enum OperationKind { + WORLD_CREATE, + WORLD_LOAD, + WORLD_UNLOAD, + WORLD_REMOVE, + WORLD_REPLACE, + STUDIO_OPEN, + STUDIO_CLOSE, + PACK_CREATE, + PACK_DOWNLOAD, + PACK_PUBLISH, + DATAPACK_COMPILE, + SERVER_RESTART + } + + public record ActiveOperation(long id, Domain domain, OperationKind kind, String target) { + public ActiveOperation { + if (id < 1L) { + throw new IllegalArgumentException("id must be positive"); + } + Objects.requireNonNull(domain, "domain"); + Objects.requireNonNull(kind, "kind"); + target = Objects.requireNonNull(target, "target").trim(); + if (target.isEmpty()) { + throw new IllegalArgumentException("target must not be blank"); + } + } + } + + public static final class BusyException extends IllegalStateException { + private final ActiveOperation currentOperation; + + private BusyException(ActiveOperation currentOperation) { + super("Lifecycle domain " + currentOperation.domain() + + " is busy with " + currentOperation.kind() + + " target=" + currentOperation.target() + + " id=" + currentOperation.id()); + this.currentOperation = currentOperation; + } + + public ActiveOperation currentOperation() { + return currentOperation; + } + + public long operationId() { + return currentOperation.id(); + } + + public Domain domain() { + return currentOperation.domain(); + } + + public OperationKind operationKind() { + return currentOperation.kind(); + } + + public String target() { + return currentOperation.target(); + } + } + + public interface Lease extends AutoCloseable { + ActiveOperation operation(); + + boolean isClosed(); + + @Override + void close(); + } + + private static final class LeaseImpl implements Lease { + private final LifecycleOperationCoordinator coordinator; + private final ActiveOperation operation; + private final AtomicBoolean closed; + + private LeaseImpl(LifecycleOperationCoordinator coordinator, ActiveOperation operation) { + this.coordinator = coordinator; + this.operation = operation; + closed = new AtomicBoolean(); + } + + @Override + public ActiveOperation operation() { + return operation; + } + + @Override + public boolean isClosed() { + return closed.get(); + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + coordinator.release(operation); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/ManagedWorldLoader.java b/core/src/main/java/art/arcane/iris/core/lifecycle/ManagedWorldLoader.java new file mode 100644 index 000000000..c559ef23e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/ManagedWorldLoader.java @@ -0,0 +1,38 @@ +package art.arcane.iris.core.lifecycle; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * Loads a persistent Iris world back into a running server. + *

+ * Iris creates its worlds from a keyed WorldCreator so the level lands in + * {@code /dimensions/iris/} and carries the environment the pack's dimension declares. + * Nothing else can: a plain name-keyed creator builds a fresh vanilla world beside the level root, and a + * custom-dimension world reports {@code CUSTOM} after its first restart, which CraftServer refuses outright. + * Any integration that wants an Iris world loaded has to come through here. + *

+ * The implementation is installed by the platform adapter; core resolves it through + * {@link art.arcane.iris.spi.IrisServices} and degrades when nothing is registered. + */ +public interface ManagedWorldLoader { + /** + * Loads the world registered under {@code configuredWorldName} - the Bukkit startup name, not an alias. + * Never throws: a refusal is reported through the returned outcome. + */ + CompletableFuture load(String configuredWorldName); + + record ManagedWorldLoad(boolean loaded, String message) { + public ManagedWorldLoad { + Objects.requireNonNull(message, "message"); + } + + public static ManagedWorldLoad loaded(String message) { + return new ManagedWorldLoad(true, message); + } + + public static ManagedWorldLoad failed(String message) { + return new ManagedWorldLoad(false, message); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/MissingWorldStorageLog.java b/core/src/main/java/art/arcane/iris/core/lifecycle/MissingWorldStorageLog.java new file mode 100644 index 000000000..ffe5a4688 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/MissingWorldStorageLog.java @@ -0,0 +1,118 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * One warning per orphaned Iris world per boot. + *

+ * The bootstrap storage audit, the bukkit.yml binding read and the worlds.json registry read all notice the + * same orphan, and the registry read runs on every {@code getWorlds()}, so the warning is deduplicated by + * configured world name instead of being emitted per code path. + *

+ * Paper runs the plugin bootstrap before any plugin logger exists, so a warning raised there reaches the + * console and the runtime log but never the instance's {@code logs/latest.log}, which is the only log most + * operators read. Warnings raised while no platform is bound are held and re-emitted once by + * {@link #replayToPlatformLog()}. + *

+ * Nothing here prunes configuration. A world folder can be missing because a drive is unmounted or a backup + * restore is in flight, and the surviving bukkit.yml and Multiverse entries are exactly what makes putting + * the folder back a complete recovery. + */ +public final class MissingWorldStorageLog { + private static final Set WARNED = ConcurrentHashMap.newKeySet(); + private static final List PENDING = new ArrayList<>(); + private static final Object PENDING_LOCK = new Object(); + + private MissingWorldStorageLog() { + } + + public static void warnOnce(String configuredWorldName, Path expectedStorage) { + String worldName = Objects.requireNonNull(configuredWorldName, "configuredWorldName"); + Path storage = Objects.requireNonNull(expectedStorage, "expectedStorage"); + if (!WARNED.add(worldName)) { + return; + } + emit(new String[]{ + IrisLogging.format("Iris world %s is configured but has no world storage at %s; it will not load.", + worldName, storage), + IrisLogging.format("Restore that folder, or drop the world with /iris remove world=%s delete=true.", + worldName) + }); + } + + /** + * Reports a world folder that survives with no pack snapshot and no world data - the skeleton the server + * writes back when it saves a level whose folder was deleted underneath it. There is nothing in it to + * lose, so it is reported instead of stopping startup, and the remedy is a delete rather than a restore. + */ + public static void warnEmptyOnce(String configuredWorldName, Path storage) { + String worldName = Objects.requireNonNull(configuredWorldName, "configuredWorldName"); + Path worldStorage = Objects.requireNonNull(storage, "storage"); + if (!WARNED.add(worldName)) { + return; + } + emit(new String[]{ + IrisLogging.format("Iris world %s has an empty world folder at %s: no pack snapshot, no region" + + " data; it will generate as vanilla terrain.", worldName, worldStorage), + IrisLogging.format("Delete that folder, or drop the world with /iris remove world=%s delete=true.", + worldName) + }); + } + + public static boolean hasWarned(String configuredWorldName) { + return WARNED.contains(Objects.requireNonNull(configuredWorldName, "configuredWorldName")); + } + + /** + * Re-emits every warning raised before a platform was bound, exactly once. Safe to call more than once + * and safe to call when nothing was held. + */ + public static void replayToPlatformLog() { + List held; + synchronized (PENDING_LOCK) { + if (PENDING.isEmpty()) { + return; + } + held = List.copyOf(PENDING); + PENDING.clear(); + } + for (String[] lines : held) { + write(lines); + } + } + + /** + * Visible for tests: the deduplication set is per JVM, not per server boot. + */ + public static void reset() { + WARNED.clear(); + synchronized (PENDING_LOCK) { + PENDING.clear(); + } + } + + private static void emit(String[] lines) { + write(lines); + if (IrisPlatforms.isBound()) { + return; + } + synchronized (PENDING_LOCK) { + PENDING.add(lines); + } + } + + private static void write(String[] lines) { + for (String line : lines) { + // Pre-formatted: passing no arguments keeps a path containing '%' out of the format engine. + IrisLogging.warn(line); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLibBootstrap.java b/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLibBootstrap.java new file mode 100644 index 000000000..c836774e6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLibBootstrap.java @@ -0,0 +1,72 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.spi.IrisLogging; +import io.papermc.lib.PaperLib; +import io.papermc.lib.environments.PaperEnvironment; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.event.player.PlayerTeleportEvent; + +public final class PaperLibBootstrap { + private PaperLibBootstrap() { + } + + public static void install() { + if (PaperLib.getEnvironment().getMinecraftVersion() > 0) { + return; + } + + String bukkitVersion = Bukkit.getBukkitVersion(); + if (!isModernVersionScheme(bukkitVersion)) { + return; + } + + boolean hasAsyncTeleport = hasMethod(Entity.class, "teleportAsync", Location.class, PlayerTeleportEvent.TeleportCause.class); + boolean hasAsyncChunks = hasMethod(World.class, "getChunkAtAsync", int.class, int.class, boolean.class); + if (!hasAsyncTeleport || !hasAsyncChunks) { + return; + } + + PaperLib.setCustomEnvironment(new ModernPaperEnvironment()); + IrisLogging.debug("Installed forced-modern Paper environment for MC " + bukkitVersion + "; bundled PaperLib predates the two-digit version scheme"); + } + + static boolean isModernVersionScheme(String bukkitVersion) { + if (bukkitVersion == null || bukkitVersion.isEmpty()) { + return false; + } + + int end = 0; + while (end < bukkitVersion.length() && Character.isDigit(bukkitVersion.charAt(end))) { + end++; + } + + if (end == 0) { + return false; + } + + try { + return Integer.parseInt(bukkitVersion.substring(0, end)) > 1; + } catch (NumberFormatException e) { + return false; + } + } + + private static boolean hasMethod(Class owner, String name, Class... parameterTypes) { + try { + owner.getMethod(name, parameterTypes); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + private static final class ModernPaperEnvironment extends PaperEnvironment { + @Override + public boolean isVersion(int minor, int patch) { + return true; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java new file mode 100644 index 000000000..ffbc61a3d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java @@ -0,0 +1,92 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.World; + +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +final class PaperLikeRuntimeBackend implements WorldLifecycleBackend { + private final CapabilitySnapshot capabilities; + + PaperLikeRuntimeBackend(CapabilitySnapshot capabilities) { + this.capabilities = capabilities; + } + + @Override + public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) { + if (!capabilities.serverFamily().isPaperLike() || !capabilities.hasPaperLikeRuntime()) { + return false; + } + + if (request.studio()) { + return true; + } + + return capabilities.serverFamily() == ServerFamily.FOLIA || capabilities.regionizedRuntime(); + } + + @Override + public CompletableFuture create(WorldLifecycleRequest request) { + Object legacyStorageAccess = null; + try { + World existing = WorldIdentity.resolve(request.worldKey()).orElse(null); + if (existing != null) { + return CompletableFuture.completedFuture(existing); + } + + if (request.generator() == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Runtime world creation requires a non-null chunk generator.")); + } + + WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider()); + WorldLifecycleSupport.stageRuntimeConfiguration(request.worldName()); + + IrisLogging.debug("WorldLifecycle runtime LevelStem: world=" + request.worldName() + + ", backend=paper_like_runtime, flavor=" + capabilities.paperLikeFlavor().name().toLowerCase(Locale.ROOT) + + ", registrySource=" + WorldLifecycleSupport.runtimeLevelStemRegistrySource(request)); + Object levelStem = WorldLifecycleSupport.resolveRuntimeLevelStem(capabilities, request); + Object stemKey = WorldLifecycleSupport.createRuntimeLevelStemKey(request.worldKey()); + + if (capabilities.paperLikeFlavor() == CapabilitySnapshot.PaperLikeFlavor.CURRENT_INFO_AND_DATA) { + Object dimensionKey = WorldLifecycleSupport.createDimensionKey(stemKey); + Object loadedWorldData = capabilities.paperWorldDataMethod().invoke(null, capabilities.minecraftServer(), dimensionKey, request.worldName()); + Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, !request.studio()); + Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData); + Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName()); + if (!WorldLifecycleSupport.hasExistingWorldData(request.worldKey())) { + worldDataAndGenSettings = WorldLifecycleSupport.applySeedToWorldDataAndGenSettings(worldDataAndGenSettings, request.seed()); + } + capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings); + } else { + legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities); + Object primaryLevelData = WorldLifecycleSupport.createLegacyPrimaryLevelData(capabilities, legacyStorageAccess, request.worldName()); + Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(0, request.worldName(), request.environment().name().toLowerCase(Locale.ROOT), stemKey, !request.studio()); + capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfo, legacyStorageAccess, primaryLevelData); + } + + World loadedWorld = WorldIdentity.resolve(request.worldKey()).orElse(null); + if (loadedWorld == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Paper-like runtime backend did not load world \"" + request.worldName() + "\".")); + } + + return CompletableFuture.completedFuture(loadedWorld); + } catch (Throwable e) { + return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e)); + } finally { + WorldLifecycleStaging.clearGenerator(request.worldName()); + WorldLifecycleSupport.closeLevelStorageAccess(legacyStorageAccess); + } + } + + @Override + public CompletableFuture unloadAsync(World world, boolean save) { + return WorldLifecycleSupport.unloadWorldAsync(capabilities, world, save); + } + + @Override + public String backendName() { + return "paper_like_runtime"; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/ServerFamily.java b/core/src/main/java/art/arcane/iris/core/lifecycle/ServerFamily.java new file mode 100644 index 000000000..0bfaf801f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/ServerFamily.java @@ -0,0 +1,21 @@ +package art.arcane.iris.core.lifecycle; + +import java.util.Locale; + +public enum ServerFamily { + BUKKIT, + SPIGOT, + PAPER, + PURPUR, + FOLIA, + CANVAS, + UNKNOWN; + + public boolean isPaperLike() { + return this == PAPER || this == PURPUR || this == FOLIA || this == CANVAS; + } + + public String id() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/VanishedWorldStorage.java b/core/src/main/java/art/arcane/iris/core/lifecycle/VanishedWorldStorage.java new file mode 100644 index 000000000..2f048e9fe --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/VanishedWorldStorage.java @@ -0,0 +1,81 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.spi.IrisLogging; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Detects an Iris world folder that was deleted out from under a live engine. + *

+ * Every persistence path resolves its target from a world folder captured when the engine bound, and the + * writes create their own parents. After an external delete that turns a save into a rebuild: a directory + * tree with engine data and server {@code .dat} files but no pack snapshot and no regions, which the next + * boot reads as an owned-but-broken world. Persistence stops instead, once the folder is gone. + *

+ * The verdict latches. A {@code save-all} after the delete writes the level's own {@code data/*.dat} files + * back, which recreates the world folder, and a plain "is it a directory" check would then let Iris resume + * writing into a tree that no longer holds the world. It also accepts a directory Iris established under the + * folder: once Iris has written {@code iris/engine-data} there, that directory disappearing is a delete + * whether or not the server has already put the folder back, which is what makes the next boot's storage + * classification the same on every run instead of depending on save ordering. + */ +public final class VanishedWorldStorage { + private static final Set VANISHED = ConcurrentHashMap.newKeySet(); + + private VanishedWorldStorage() { + } + + /** + * True when the world folder is no longer a directory, or was already found gone earlier in this JVM. + */ + public static boolean vanished(File worldFolder) { + return vanished(worldFolder, null); + } + + /** + * True when the world folder is gone, or when {@code establishedTree} - a directory Iris is known to have + * created under it already - is gone. Reports the first time it sees each folder. + * + * @param establishedTree a directory Iris has already written, or null when it has written none yet + */ + public static boolean vanished(File worldFolder, File establishedTree) { + if (worldFolder == null) { + return false; + } + Path path = worldFolder.toPath().toAbsolutePath().normalize(); + if (VANISHED.contains(path.toString())) { + return true; + } + if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + return report(path); + } + if (establishedTree == null) { + return false; + } + Path tree = establishedTree.toPath().toAbsolutePath().normalize(); + if (Files.isDirectory(tree, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + return report(path); + } + + /** + * Visible for tests: the report set is per JVM, not per server boot. + */ + public static void reset() { + VANISHED.clear(); + } + + private static boolean report(Path path) { + if (VANISHED.add(path.toString())) { + IrisLogging.error("Iris world storage is gone at %s; that world is no longer being written.", path); + IrisLogging.error("Unload or remove the world, or restore the folder and restart the server."); + } + return true; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleBackend.java new file mode 100644 index 000000000..9b3d39124 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleBackend.java @@ -0,0 +1,15 @@ +package art.arcane.iris.core.lifecycle; + +import org.bukkit.World; + +import java.util.concurrent.CompletableFuture; + +public interface WorldLifecycleBackend { + boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities); + + CompletableFuture create(WorldLifecycleRequest request); + + CompletableFuture unloadAsync(World world, boolean save); + + String backendName(); +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleCaller.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleCaller.java new file mode 100644 index 000000000..b295e0a93 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleCaller.java @@ -0,0 +1,7 @@ +package art.arcane.iris.core.lifecycle; + +public enum WorldLifecycleCaller { + STUDIO, + CREATE, + BENCHMARK +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java new file mode 100644 index 000000000..d1594d03f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java @@ -0,0 +1,55 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.WorldCreatorCompat; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.WorldType; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; + +public record WorldLifecycleRequest( + String worldName, + NamespacedKey worldKey, + World.Environment environment, + ChunkGenerator generator, + BiomeProvider biomeProvider, + WorldType worldType, + boolean generateStructures, + boolean hardcore, + long seed, + boolean studio, + boolean benchmark, + WorldLifecycleCaller callerKind +) { + public static WorldLifecycleRequest fromCreator(WorldCreator creator, boolean studio, boolean benchmark, WorldLifecycleCaller callerKind) { + return new WorldLifecycleRequest( + creator.name(), + WorldCreatorCompat.keyOf(creator), + creator.environment(), + creator.generator(), + creator.biomeProvider(), + creator.type(), + creator.generateStructures(), + creator.hardcore(), + creator.seed(), + studio, + benchmark, + callerKind + ); + } + + public WorldCreator toWorldCreator() { + WorldCreator creator = WorldCreatorCompat.ofKey(worldKey, worldName) + .environment(environment) + .generateStructures(generateStructures) + .hardcore(hardcore) + .type(worldType) + .seed(seed) + .generator(generator); + if (biomeProvider != null) { + creator.biomeProvider(biomeProvider); + } + return creator; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java new file mode 100644 index 000000000..6a4add872 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java @@ -0,0 +1,246 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.IrisStartupValidation; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.NamespacedKey; +import org.bukkit.World; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public final class WorldLifecycleService { + private static final long UNLOAD_TIMEOUT_SECONDS = 120L; + private static volatile WorldLifecycleService instance; + + private final CapabilitySnapshot capabilities; + private final WorldLifecycleBackend worldsProviderBackend; + private final WorldLifecycleBackend paperLikeRuntimeBackend; + private final WorldLifecycleBackend bukkitPublicBackend; + private final List backends; + private final Map worldBackendByKey; + + public WorldLifecycleService(CapabilitySnapshot capabilities) { + this( + capabilities, + new WorldsProviderBackend(capabilities), + new PaperLikeRuntimeBackend(capabilities), + new BukkitPublicBackend(capabilities) + ); + } + + WorldLifecycleService( + CapabilitySnapshot capabilities, + WorldLifecycleBackend worldsProviderBackend, + WorldLifecycleBackend paperLikeRuntimeBackend, + WorldLifecycleBackend bukkitPublicBackend + ) { + this.capabilities = Objects.requireNonNull(capabilities, "capabilities"); + this.worldsProviderBackend = Objects.requireNonNull(worldsProviderBackend, "worldsProviderBackend"); + this.paperLikeRuntimeBackend = Objects.requireNonNull(paperLikeRuntimeBackend, "paperLikeRuntimeBackend"); + this.bukkitPublicBackend = Objects.requireNonNull(bukkitPublicBackend, "bukkitPublicBackend"); + this.backends = List.of(worldsProviderBackend, paperLikeRuntimeBackend, bukkitPublicBackend); + this.worldBackendByKey = new ConcurrentHashMap<>(); + } + + public static WorldLifecycleService get() { + WorldLifecycleService current = instance; + if (current != null) { + return current; + } + + synchronized (WorldLifecycleService.class) { + if (instance != null) { + return instance; + } + + CapabilitySnapshot capabilities = CapabilitySnapshot.probe(); + instance = new WorldLifecycleService(capabilities); + IrisLogging.info("WorldLifecycle capabilities: %s", capabilities.describe()); + return instance; + } + } + + public CapabilitySnapshot capabilities() { + return capabilities; + } + + public CompletableFuture create(WorldLifecycleRequest request) { + WorldLifecycleBackend backend; + try { + IrisStartupValidation.requireWorldCreationReady(); + backend = selectCreateBackend(request); + } catch (Throwable e) { + IrisLogging.reportError("WorldLifecycle create backend selection failed for world=\"" + request.worldName() + + "\", caller=" + request.callerKind().name().toLowerCase() + ".", e); + return CompletableFuture.failedFuture(e); + } + IrisLogging.info("WorldLifecycle create: world=%s, caller=%s, backend=%s", + request.worldName(), + request.callerKind().name().toLowerCase(), + backend.backendName()); + return backend.create(request).whenComplete((world, throwable) -> { + if (throwable != null) { + Throwable cause = WorldLifecycleSupport.unwrap(throwable); + IrisLogging.reportError("WorldLifecycle create failed: world=\"" + request.worldName() + + "\", caller=" + request.callerKind().name().toLowerCase() + + ", backend=" + backend.backendName() + + ", family=" + capabilities.serverFamily().id() + ".", cause); + return; + } + if (world != null) { + worldBackendByKey.put(WorldIdentity.serialize(world), backend.backendName()); + } + }); + } + + public World createBlocking(WorldLifecycleRequest request) { + try { + return create(request).join(); + } catch (CompletionException e) { + throw new IllegalStateException(WorldLifecycleSupport.unwrap(e)); + } + } + + public CompletableFuture unloadAsync(World world, boolean save) { + World requiredWorld = Objects.requireNonNull(world, "world"); + String worldIdentity = WorldIdentity.serialize(requiredWorld); + String worldName = requiredWorld.getName(); + WorldLifecycleBackend backend = selectUnloadBackend(worldIdentity); + IrisLogging.info("WorldLifecycle unload: world=%s, backend=%s", + worldName, + backend.backendName()); + + WorldUnloadBoundaryRegistry.Boundary rawBoundary; + try { + rawBoundary = WorldUnloadBoundaryRegistry.begin(worldIdentity); + } catch (Throwable e) { + return CompletableFuture.failedFuture(e); + } + + CompletableFuture unloadFuture; + try { + unloadFuture = backend.unloadAsync(requiredWorld, save); + if (unloadFuture == null) { + throw new IllegalStateException("World lifecycle backend returned no unload completion future."); + } + } catch (Throwable e) { + unloadFuture = CompletableFuture.failedFuture(e); + } + unloadFuture.whenComplete((unloaded, throwable) -> + WorldUnloadBoundaryRegistry.complete(rawBoundary, unloaded, throwable)); + + CompletableFuture guardedFuture = guardUnloadCompletion(worldName, unloadFuture); + return guardedFuture.whenComplete((unloaded, throwable) -> { + if (throwable != null) { + Throwable cause = WorldLifecycleSupport.unwrap(throwable); + IrisLogging.reportError("WorldLifecycle unload failed: world=\"" + worldName + + "\", backend=" + backend.backendName() + + ", family=" + capabilities.serverFamily().id() + ".", cause); + return; + } + if (Boolean.TRUE.equals(unloaded)) { + worldBackendByKey.remove(worldIdentity, backend.backendName()); + } + }); + } + + private CompletableFuture guardUnloadCompletion( + String worldName, + CompletableFuture unloadFuture + ) { + CompletableFuture guarded = new CompletableFuture<>(); + unloadFuture.whenComplete((unloaded, throwable) -> { + if (throwable == null) { + guarded.complete(Boolean.TRUE.equals(unloaded)); + } else { + guarded.completeExceptionally(WorldLifecycleSupport.unwrap(throwable)); + } + }); + if (!guarded.isDone()) { + CompletableFuture.delayedExecutor(UNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).execute(() -> { + TimeoutException timeout = new TimeoutException( + "World unload did not settle within " + UNLOAD_TIMEOUT_SECONDS + " seconds for \"" + + worldName + "\"."); + if (!guarded.completeExceptionally(timeout) || IrisToolbelt.isServerStopping()) { + return; + } + ServerConfigurator.restart("World unload timed out for \"" + worldName + "\"."); + }); + } + return guarded; + } + + public boolean unload(World world, boolean save) { + if (J.isPrimaryThread() || (J.isFolia() && WorldLifecycleSupport.isGlobalTickThread())) { + throw new IllegalStateException("WorldLifecycle unload cannot block the primary/global tick thread; use unloadAsync instead."); + } + + try { + return Boolean.TRUE.equals(unloadAsync(world, save).join()); + } catch (CompletionException e) { + Throwable cause = WorldLifecycleSupport.unwrap(e); + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (cause instanceof Error error) { + throw error; + } + throw new IllegalStateException(cause); + } + } + + public String backendNameForWorld(NamespacedKey worldKey) { + return selectUnloadBackend(worldKey.toString()).backendName(); + } + + WorldLifecycleBackend selectCreateBackend(WorldLifecycleRequest request) { + if (worldsProviderBackend.supports(request, capabilities)) { + return worldsProviderBackend; + } + + if (request.studio() && capabilities.serverFamily().isPaperLike()) { + if (!paperLikeRuntimeBackend.supports(request, capabilities)) { + throw new IllegalStateException("World lifecycle backend paper_like_runtime is unavailable for studio create on " + + capabilities.serverFamily().id() + ": " + capabilities.paperLikeResolution()); + } + return paperLikeRuntimeBackend; + } + + for (WorldLifecycleBackend backend : backends) { + if (backend.supports(request, capabilities)) { + return backend; + } + } + + throw new IllegalStateException("No world lifecycle backend supports request for \"" + request.worldName() + "\"."); + } + + WorldLifecycleBackend selectUnloadBackend(String worldIdentity) { + String backendName = worldBackendByKey.get(worldIdentity); + if (backendName == null) { + return bukkitPublicBackend; + } + + for (WorldLifecycleBackend backend : backends) { + if (backend.backendName().equals(backendName)) { + return backend; + } + } + + return bukkitPublicBackend; + } + + void rememberBackend(NamespacedKey worldKey, String backendName) { + worldBackendByKey.put(worldKey.toString(), backendName); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleStaging.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleStaging.java new file mode 100644 index 000000000..858fdbd2b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleStaging.java @@ -0,0 +1,65 @@ +package art.arcane.iris.core.lifecycle; + +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class WorldLifecycleStaging { + private static final Map stagedGenerators = new ConcurrentHashMap<>(); + private static final Map stagedBiomeProviders = new ConcurrentHashMap<>(); + private static final Map stagedStemGenerators = new ConcurrentHashMap<>(); + + private WorldLifecycleStaging() { + } + + public static void stageGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator, @Nullable BiomeProvider biomeProvider) { + stagedGenerators.put(worldName, generator); + if (biomeProvider != null) { + stagedBiomeProviders.put(worldName, biomeProvider); + } else { + stagedBiomeProviders.remove(worldName); + } + } + + public static void stageStemGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator) { + stagedStemGenerators.put(worldName, generator); + } + + @Nullable + public static ChunkGenerator consumeGenerator(@NotNull String worldName) { + return stagedGenerators.remove(worldName); + } + + @Nullable + public static BiomeProvider consumeBiomeProvider(@NotNull String worldName) { + return stagedBiomeProviders.remove(worldName); + } + + @Nullable + public static ChunkGenerator consumeStemGenerator(@NotNull String worldName) { + return stagedStemGenerators.remove(worldName); + } + + @Nullable + public static ChunkGenerator peekStemGenerator(@NotNull String worldName) { + return stagedStemGenerators.get(worldName); + } + + public static void clearGenerator(@NotNull String worldName) { + stagedGenerators.remove(worldName); + stagedBiomeProviders.remove(worldName); + } + + public static void clearStem(@NotNull String worldName) { + stagedStemGenerators.remove(worldName); + } + + public static void clearAll(@NotNull String worldName) { + clearGenerator(worldName); + clearStem(worldName); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java new file mode 100644 index 000000000..33bc592ac --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java @@ -0,0 +1,683 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.INMSBinding; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.scheduling.FoliaScheduler; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.event.world.WorldUnloadEvent; +import org.bukkit.generator.ChunkGenerator; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +final class WorldLifecycleSupport { + private WorldLifecycleSupport() { + } + + static Throwable unwrap(Throwable throwable) { + if (throwable instanceof InvocationTargetException invocationTargetException && invocationTargetException.getCause() != null) { + return unwrap(invocationTargetException.getCause()); + } + if (throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null) { + return unwrap(completionException.getCause()); + } + if (throwable instanceof ExecutionException executionException && executionException.getCause() != null) { + return unwrap(executionException.getCause()); + } + return throwable; + } + + static Object invoke(Method method, Object target, Object... args) throws ReflectiveOperationException { + return method.invoke(target, args); + } + + static Object invokeNamed(Object target, String methodName, Class[] parameterTypes, Object... args) throws ReflectiveOperationException { + Method method = target.getClass().getMethod(methodName, parameterTypes); + return method.invoke(target, args); + } + + static Object read(Field field, Object target) throws IllegalAccessException { + return field.get(target); + } + + static void stageRuntimeConfiguration(String worldName) throws ReflectiveOperationException { + Object bukkitServer = Bukkit.getServer(); + if (bukkitServer == null) { + throw new IllegalStateException("Bukkit server is unavailable."); + } + + Field configurationField = CapabilityResolution.resolveField(bukkitServer.getClass(), "configuration"); + Object rawConfiguration = configurationField.get(bukkitServer); + if (!(rawConfiguration instanceof YamlConfiguration configuration)) { + throw new IllegalStateException("CraftServer configuration field is unavailable."); + } + + ConfigurationSection worldsSection = configuration.getConfigurationSection("worlds"); + if (worldsSection == null) { + worldsSection = configuration.createSection("worlds"); + } + + ConfigurationSection worldSection = worldsSection.getConfigurationSection(worldName); + if (worldSection == null) { + worldSection = worldsSection.createSection(worldName); + } + + worldSection.set("generator", "Iris:runtime"); + } + + static Object getRuntimeDatapackDimensions(CapabilitySnapshot capabilities) throws ReflectiveOperationException { + Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer()); + Method datapackDimensionsMethod = CapabilityResolution.resolveMethod(worldLoaderContext.getClass(), "datapackDimensions", method -> method.getParameterCount() == 0); + if (datapackDimensionsMethod == null) { + throw new IllegalStateException("DataLoadContext does not expose datapackDimensions()."); + } + Object datapackDimensions = datapackDimensionsMethod.invoke(worldLoaderContext); + if (datapackDimensions == null) { + throw new IllegalStateException("DataLoadContext.datapackDimensions() returned null."); + } + return datapackDimensions; + } + + static Object getRuntimeServerRegistryAccess(CapabilitySnapshot capabilities) throws ReflectiveOperationException { + Method registryAccessMethod = capabilities.serverRegistryAccessMethod(); + if (registryAccessMethod == null) { + throw new IllegalStateException("MinecraftServer does not expose registryAccess()."); + } + Object registryAccess = registryAccessMethod.invoke(capabilities.minecraftServer()); + if (registryAccess == null) { + throw new IllegalStateException("MinecraftServer.registryAccess() returned null."); + } + return registryAccess; + } + + static Object getRuntimeLevelStemRegistry(CapabilitySnapshot capabilities) throws ReflectiveOperationException { + Object datapackDimensions = getRuntimeDatapackDimensions(capabilities); + Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries") + .getField("LEVEL_STEM") + .get(null); + Method lookupMethod = CapabilityResolution.resolveMethod(datapackDimensions.getClass(), "lookupOrThrow", method -> method.getParameterCount() == 1); + if (lookupMethod == null) { + throw new IllegalStateException("Registry access does not expose lookupOrThrow(...)."); + } + return lookupMethod.invoke(datapackDimensions, levelStemRegistryKey); + } + + static Object createRuntimeLevelStemKey(NamespacedKey worldKey) throws ReflectiveOperationException { + Identifier identifier = new Identifier(worldKey.getNamespace(), worldKey.getKey()); + Object rawIdentifier = Class.forName("net.minecraft.resources.Identifier") + .getMethod("fromNamespaceAndPath", String.class, String.class) + .invoke(null, identifier.namespace(), identifier.key()); + Object registryKey = Class.forName("net.minecraft.core.registries.Registries") + .getField("LEVEL_STEM") + .get(null); + Method createMethod = Class.forName("net.minecraft.resources.ResourceKey") + .getMethod("create", registryKey.getClass(), rawIdentifier.getClass()); + return createMethod.invoke(null, registryKey, rawIdentifier); + } + + static Object createDimensionKey(Object stemKey) throws ReflectiveOperationException { + Class resourceKeyClass = Class.forName("net.minecraft.resources.ResourceKey"); + Method identifierMethod = CapabilityResolution.resolveMethod(resourceKeyClass, "identifier", method -> method.getParameterCount() == 0); + Object identifier = identifierMethod.invoke(stemKey); + Object dimensionRegistryKey = Class.forName("net.minecraft.core.registries.Registries") + .getField("DIMENSION") + .get(null); + Method createMethod = resourceKeyClass.getMethod("create", dimensionRegistryKey.getClass(), identifier.getClass()); + return createMethod.invoke(null, dimensionRegistryKey, identifier); + } + + static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request) throws ReflectiveOperationException { + return resolveRuntimeLevelStem(capabilities, request, INMS.get()); + } + + static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request, INMSBinding binding) throws ReflectiveOperationException { + ChunkGenerator generator = request.generator(); + if (generator instanceof PlatformChunkGenerator) { + Object registryAccess = getRuntimeServerRegistryAccess(capabilities); + try { + Object levelStem = binding.createRuntimeLevelStem(registryAccess, generator); + if (levelStem == null) { + throw new IllegalStateException("Iris NMS binding returned null runtime LevelStem."); + } + return levelStem; + } catch (Throwable e) { + throw new IllegalStateException("Failed to create runtime LevelStem from full server registry access for world \"" + request.worldName() + "\".", unwrap(e)); + } + } + + try { + Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities); + Object overworldKey = Class.forName("net.minecraft.world.level.dimension.LevelStem") + .getField("OVERWORLD") + .get(null); + Method getValueMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "getValue", method -> method.getParameterCount() == 1); + if (getValueMethod != null) { + Object resolved = getValueMethod.invoke(levelStemRegistry, overworldKey); + if (resolved != null) { + return resolved; + } + } + + Method getMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "get", method -> method.getParameterCount() == 1); + if (getMethod == null) { + throw new IllegalStateException("Unable to resolve OVERWORLD LevelStem from registry."); + } + Object raw = getMethod.invoke(levelStemRegistry, overworldKey); + return extractRegistryValue(raw); + } catch (Throwable e) { + throw new IllegalStateException("Failed to resolve fallback OVERWORLD LevelStem from datapack registry access for world \"" + request.worldName() + "\".", unwrap(e)); + } + } + + static String runtimeLevelStemRegistrySource(WorldLifecycleRequest request) { + if (request.generator() instanceof PlatformChunkGenerator) { + return "full_server_registry"; + } + return "datapack_level_stem_registry"; + } + + static Object extractRegistryValue(Object raw) throws ReflectiveOperationException { + if (raw == null) { + return null; + } + if (raw instanceof Optional optional) { + Object nested = optional.orElse(null); + if (nested == null) { + return null; + } + return extractRegistryValue(nested); + } + Method valueMethod = CapabilityResolution.resolveMethod(raw.getClass(), "value", method -> method.getParameterCount() == 0); + if (valueMethod != null) { + return valueMethod.invoke(raw); + } + return raw; + } + + static void applyWorldDataNameAndModInfo(CapabilitySnapshot capabilities, Object worldDataAndGenSettings, String worldName) throws ReflectiveOperationException { + Method dataMethod = CapabilityResolution.resolveMethod(worldDataAndGenSettings.getClass(), "data", method -> method.getParameterCount() == 0); + if (dataMethod == null) { + return; + } + + Object worldData = dataMethod.invoke(worldDataAndGenSettings); + if (worldData == null) { + return; + } + + Method checkNameMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "checkName", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 1 && String.class.equals(params[0]); + }); + if (checkNameMethod != null) { + checkNameMethod.invoke(worldData, worldName); + } + + Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0); + Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0); + if (getModdedStatusMethod == null || getServerModNameMethod == null) { + return; + } + + Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer()); + Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0); + Method setModdedInfoMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "setModdedInfo", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]); + }); + if (shouldReportAsModifiedMethod == null || setModdedInfoMethod == null) { + return; + } + + boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck)); + String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer()); + setModdedInfoMethod.invoke(worldData, modName, modified); + } + + static boolean hasExistingWorldData(NamespacedKey worldKey) { + File worldFolder = IrisWorldStorage.dimensionRoot(worldKey); + return new File(worldFolder, "region").exists() + || new File(worldFolder, "entities").exists() + || new File(worldFolder, "poi").exists() + || new File(worldFolder, "data/paper/metadata.dat").exists() + || new File(worldFolder, "paper-world.yml").exists(); + } + + static Object applySeedToWorldDataAndGenSettings(Object worldDataAndGenSettings, long seed) throws ReflectiveOperationException { + Method genSettingsMethod = CapabilityResolution.resolveMethod(worldDataAndGenSettings.getClass(), "genSettings", method -> method.getParameterCount() == 0); + Method dataMethod = CapabilityResolution.resolveMethod(worldDataAndGenSettings.getClass(), "data", method -> method.getParameterCount() == 0); + if (genSettingsMethod == null || dataMethod == null) { + throw new IllegalStateException("WorldDataAndGenSettings does not expose data()/genSettings()."); + } + + Object genSettings = genSettingsMethod.invoke(worldDataAndGenSettings); + Method optionsMethod = CapabilityResolution.resolveMethod(genSettings.getClass(), "options", method -> method.getParameterCount() == 0); + Method dimensionsMethod = CapabilityResolution.resolveMethod(genSettings.getClass(), "dimensions", method -> method.getParameterCount() == 0); + if (optionsMethod == null || dimensionsMethod == null) { + throw new IllegalStateException("WorldGenSettings does not expose options()/dimensions()."); + } + + Object options = optionsMethod.invoke(genSettings); + Method seedMethod = CapabilityResolution.resolveMethod(options.getClass(), "seed", method -> method.getParameterCount() == 0 && long.class.equals(method.getReturnType())); + Method withSeedMethod = CapabilityResolution.resolveMethod(options.getClass(), "withSeed", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 1 && OptionalLong.class.equals(params[0]); + }); + if (seedMethod == null || withSeedMethod == null) { + throw new IllegalStateException("WorldOptions does not expose seed()/withSeed(OptionalLong)."); + } + + long currentSeed = (long) seedMethod.invoke(options); + if (currentSeed == seed) { + return worldDataAndGenSettings; + } + + Object newOptions = withSeedMethod.invoke(options, OptionalLong.of(seed)); + Object newGenSettings = construct(genSettings.getClass(), newOptions, dimensionsMethod.invoke(genSettings)); + return construct(worldDataAndGenSettings.getClass(), dataMethod.invoke(worldDataAndGenSettings), newGenSettings); + } + + private static Object construct(Class type, Object first, Object second) throws ReflectiveOperationException { + for (Constructor constructor : type.getDeclaredConstructors()) { + Class[] params = constructor.getParameterTypes(); + if (params.length != 2) { + continue; + } + + if ((first == null || params[0].isInstance(first)) && (second == null || params[1].isInstance(second))) { + constructor.setAccessible(true); + return constructor.newInstance(first, second); + } + } + + throw new IllegalStateException("No compatible two-argument constructor on " + type.getName()); + } + + static Object createCurrentWorldDataAndSettings(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException { + Object settings = read(capabilities.settingsField(), capabilities.minecraftServer()); + Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer()); + Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities); + boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer())); + Object options = read(capabilities.optionsField(), capabilities.minecraftServer()); + Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 1 && String.class.equals(params[0]); + }); + boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest")); + Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest); + Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0); + if (cookieMethod == null) { + throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie()."); + } + Object worldDataAndGenSettings = cookieMethod.invoke(dataLoadOutput); + applyWorldDataNameAndModInfo(capabilities, worldDataAndGenSettings, worldName); + return worldDataAndGenSettings; + } + + static Object createLegacyPrimaryLevelData(CapabilitySnapshot capabilities, Object levelStorageAccess, String worldName) throws ReflectiveOperationException { + Object levelDataResult = capabilities.paperWorldDataMethod().invoke(null, levelStorageAccess); + Method fatalErrorMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "fatalError", method -> method.getParameterCount() == 0); + Method dataTagMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "dataTag", method -> method.getParameterCount() == 0); + if (fatalErrorMethod != null && Boolean.TRUE.equals(fatalErrorMethod.invoke(levelDataResult))) { + throw new IllegalStateException("Paper runtime world-data helper reported a fatal error for \"" + worldName + "\"."); + } + if (dataTagMethod != null && dataTagMethod.invoke(levelDataResult) != null) { + throw new IllegalStateException("Runtime world \"" + worldName + "\" already contains level data."); + } + + Object settings = read(capabilities.settingsField(), capabilities.minecraftServer()); + Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer()); + Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities); + boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer())); + Object options = read(capabilities.optionsField(), capabilities.minecraftServer()); + Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 1 && String.class.equals(params[0]); + }); + boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest")); + Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest); + Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0); + if (cookieMethod == null) { + throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie()."); + } + Object primaryLevelData = cookieMethod.invoke(dataLoadOutput); + + Method checkNameMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "checkName", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 1 && String.class.equals(params[0]); + }); + if (checkNameMethod != null) { + checkNameMethod.invoke(primaryLevelData, worldName); + } + + Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0); + Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0); + if (getModdedStatusMethod != null && getServerModNameMethod != null) { + Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer()); + Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0); + Method setModdedInfoMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "setModdedInfo", method -> { + Class[] params = method.getParameterTypes(); + return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]); + }); + if (shouldReportAsModifiedMethod != null && setModdedInfoMethod != null) { + boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck)); + String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer()); + setModdedInfoMethod.invoke(primaryLevelData, modName, modified); + } + } + + return primaryLevelData; + } + + static Object createLegacyStorageAccess(CapabilitySnapshot capabilities) throws ReflectiveOperationException { + Class levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource"); + Method createDefaultMethod = levelStorageSourceClass.getMethod("createDefault", Path.class); + File levelRoot = IrisWorldStorage.levelRoot(); + Object levelStorageSource = createDefaultMethod.invoke(null, levelRoot.getParentFile().toPath()); + Method storageAccessMethod = capabilities.levelStorageAccessMethod(); + if (storageAccessMethod.getParameterCount() == 1) { + return storageAccessMethod.invoke(levelStorageSource, levelRoot.getName()); + } + Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem") + .getField("OVERWORLD") + .get(null); + return storageAccessMethod.invoke(levelStorageSource, levelRoot.getName(), overworldStemKey); + } + + static void closeLevelStorageAccess(Object levelStorageAccess) { + if (levelStorageAccess == null) { + return; + } + try { + Method closeMethod = levelStorageAccess.getClass().getMethod("close"); + closeMethod.invoke(levelStorageAccess); + } catch (Throwable ignored) { + } + } + + static CompletableFuture unloadWorldAsync(CapabilitySnapshot capabilities, World world, boolean save) { + if (world == null) { + return CompletableFuture.completedFuture(false); + } + + CompletableFuture result = new CompletableFuture<>(); + Runnable invokeTask = () -> beginUnload(capabilities, world, save, result); + boolean folia = J.isFolia(); + if ((!folia && J.isPrimaryThread()) || (folia && isGlobalTickThread())) { + invokeTask.run(); + return result; + } + + CompletableFuture scheduled = runGlobalAsync(invokeTask); + scheduled.whenComplete((unused, throwable) -> { + if (throwable != null) { + result.completeExceptionally(unwrap(throwable)); + } + }); + return result; + } + + private static void beginUnload( + CapabilitySnapshot capabilities, + World world, + boolean save, + CompletableFuture result + ) { + CompletableFuture operation; + try { + operation = unloadWorldViaAsyncApi(capabilities, world, save); + if (operation == null) { + operation = unloadWorldWithoutAsyncApi(capabilities, world, save); + } + } catch (Throwable e) { + result.completeExceptionally(unwrap(e)); + return; + } + + operation.whenComplete((unloaded, throwable) -> { + if (throwable == null) { + result.complete(Boolean.TRUE.equals(unloaded)); + } else { + result.completeExceptionally(unwrap(throwable)); + } + }); + } + + private static CompletableFuture unloadWorldWithoutAsyncApi( + CapabilitySnapshot capabilities, + World world, + boolean save + ) { + String worldName = world.getName(); + try { + try { + return CompletableFuture.completedFuture(Bukkit.unloadWorld(world, save)); + } catch (UnsupportedOperationException unsupported) { + if (capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null) { + return CompletableFuture.failedFuture(unsupported); + } + } + + if (!announceManualWorldUnload(world)) { + return CompletableFuture.completedFuture(false); + } + + if (save) { + world.save(); + } + Method getHandleMethod = world.getClass().getMethod("getHandle"); + Object serverLevel = getHandleMethod.invoke(world); + CompletableFuture operation = closeServerLevelAsync(world, serverLevel) + .thenCompose(unused -> detachServerLevelAsync(capabilities, serverLevel, world)) + .thenApply(unused -> WorldIdentity.resolve(WorldIdentity.key(world)).isEmpty()); + return contextualizeUnloadFailure(worldName, operation); + } catch (Throwable e) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Failed to unload world \"" + worldName + "\" through the selected world lifecycle backend.", + unwrap(e) + )); + } + } + + static boolean announceManualWorldUnload(World world) { + WorldUnloadEvent unloadEvent = new WorldUnloadEvent(world); + Bukkit.getPluginManager().callEvent(unloadEvent); + return !unloadEvent.isCancelled(); + } + + private static CompletableFuture unloadWorldViaAsyncApi(CapabilitySnapshot capabilities, World world, boolean save) { + if (capabilities.unloadWorldAsyncMethod() == null || capabilities.bukkitServer() == null) { + return null; + } + + return invokeAsyncUnload( + capabilities.bukkitServer(), + capabilities.unloadWorldAsyncMethod(), + world, + save + ); + } + + static CompletableFuture invokeAsyncUnload( + Object bukkitServer, + Method unloadWorldAsyncMethod, + World world, + boolean save + ) { + CompletableFuture callbackFuture = new CompletableFuture<>(); + Consumer callback = unloaded -> callbackFuture.complete(Boolean.TRUE.equals(unloaded)); + try { + unloadWorldAsyncMethod.invoke(bukkitServer, world, save, callback); + } catch (Throwable e) { + callbackFuture.completeExceptionally(unwrap(e)); + } + return callbackFuture; + } + + private static CompletableFuture contextualizeUnloadFailure( + String worldName, + CompletableFuture operation + ) { + CompletableFuture result = new CompletableFuture<>(); + operation.whenComplete((unloaded, throwable) -> { + if (throwable == null) { + result.complete(Boolean.TRUE.equals(unloaded)); + } else { + result.completeExceptionally(new IllegalStateException( + "Failed to unload world \"" + worldName + "\" through the selected world lifecycle backend.", + unwrap(throwable) + )); + } + }); + return result; + } + + private static CompletableFuture closeServerLevelAsync(World world, Object serverLevel) { + Method closeMethod; + try { + closeMethod = CapabilityResolution.resolveMethod( + serverLevel.getClass(), + "close", + method -> method.getParameterCount() == 0 + ); + } catch (Throwable e) { + return CompletableFuture.failedFuture(unwrap(e)); + } + if (closeMethod == null) { + return CompletableFuture.completedFuture(null); + } + + if (!J.isFolia()) { + try { + closeMethod.invoke(serverLevel); + return CompletableFuture.completedFuture(null); + } catch (Throwable e) { + return CompletableFuture.failedFuture(unwrap(e)); + } + } + + Location spawn = world.getSpawnLocation(); + int chunkX = spawn == null ? 0 : spawn.getBlockX() >> 4; + int chunkZ = spawn == null ? 0 : spawn.getBlockZ() >> 4; + CompletableFuture closeFuture = new CompletableFuture<>(); + boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> { + try { + closeMethod.invoke(serverLevel); + closeFuture.complete(null); + } catch (Throwable e) { + closeFuture.completeExceptionally(unwrap(e)); + } + }); + if (!scheduled) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Failed to schedule region close task for world \"" + world.getName() + "\"." + )); + } + return closeFuture.orTimeout(90L, TimeUnit.SECONDS); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void removeWorldFromCraftServerMap(World world) throws ReflectiveOperationException { + Object bukkitServer = Bukkit.getServer(); + if (bukkitServer == null) { + return; + } + + Field worldsField = CapabilityResolution.resolveField(bukkitServer.getClass(), "worlds"); + Object rawWorlds = worldsField.get(bukkitServer); + if (rawWorlds instanceof Map map) { + boolean removed = map.values().removeIf(candidate -> candidate == world); + if (!removed) { + throw new IllegalStateException( + "CraftServer world registry did not contain \"" + world.getName() + "\"."); + } + } + } + + private static CompletableFuture detachServerLevelAsync( + CapabilitySnapshot capabilities, + Object serverLevel, + World world + ) { + Runnable detachTask = () -> { + try { + capabilities.removeLevelMethod().invoke(capabilities.minecraftServer(), serverLevel); + removeWorldFromCraftServerMap(world); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }; + + if (!J.isFolia() || isGlobalTickThread()) { + try { + detachTask.run(); + return CompletableFuture.completedFuture(null); + } catch (Throwable e) { + return CompletableFuture.failedFuture(unwrap(e)); + } + } + + CompletableFuture detachFuture = runGlobalAsync(detachTask); + return detachFuture.orTimeout(15L, TimeUnit.SECONDS); + } + + private static CompletableFuture runGlobalAsync(Runnable task) { + if (!J.isFolia()) { + return J.sfut(task); + } + + CompletableFuture result = new CompletableFuture<>(); + Runnable settlement = () -> { + try { + task.run(); + result.complete(null); + } catch (Throwable e) { + result.completeExceptionally(unwrap(e)); + } + }; + + try { + if (!FoliaScheduler.runGlobal(BukkitPlatform.plugin(), settlement)) { + result.completeExceptionally(new IllegalStateException("Failed to schedule global world lifecycle task.")); + } + } catch (Throwable e) { + result.completeExceptionally(unwrap(e)); + } + return result; + } + + static boolean isGlobalTickThread() { + Object server = Bukkit.getServer(); + if (server == null) { + return false; + } + try { + Method method = server.getClass().getMethod("isGlobalTickThread"); + return Boolean.TRUE.equals(method.invoke(server)); + } catch (Throwable ignored) { + return false; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementBootstrap.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementBootstrap.java new file mode 100644 index 000000000..5b36bcf6e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementBootstrap.java @@ -0,0 +1,279 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.ExactWorldSlotPathPolicy; +import art.arcane.iris.core.WorldSlotKey; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot; +import art.arcane.iris.core.lifecycle.WorldReplacementFilesystem.ReplacementPaths; +import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase; +import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Transaction; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public final class WorldReplacementBootstrap { + private WorldReplacementBootstrap() { + } + + public static ReconcileResult reconcile( + Path dataDirectory, + Path levelRoot, + Path bukkitConfiguration, + Consumer feedback + ) throws IOException { + Path requiredDataDirectory = Objects.requireNonNull(dataDirectory, "dataDirectory") + .toAbsolutePath() + .normalize(); + Path requiredLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot") + .toAbsolutePath() + .normalize(); + File requiredBukkitConfiguration = Objects.requireNonNull(bukkitConfiguration, "bukkitConfiguration") + .toAbsolutePath() + .normalize() + .toFile(); + Consumer requiredFeedback = Objects.requireNonNull(feedback, "feedback"); + List transactions = WorldReplacementJournal.load(requiredDataDirectory, requiredLevelRoot); + int published = 0; + int rolledBack = 0; + int retained = 0; + int skipped = 0; + for (Transaction transaction : transactions) { + // A journal recorded against another level root or logical world name is not this + // boot's transaction; skip it with a pointer instead of aborting the whole startup. + if (!WorldReplacementJournal.appliesTo(transaction, requiredLevelRoot)) { + skipped++; + requiredFeedback.accept("Skipping pending world replacement " + transaction.id() + + " for " + transaction.worldKey() + ": it was staged against level root " + + transaction.levelRoot() + " but this server now uses " + requiredLevelRoot + + ". Remove " + requiredDataDirectory.resolve(WorldReplacementJournal.DIRECTORY_NAME) + .resolve(transaction.id() + ".properties") + + " or restore the previous level-name to resolve it."); + continue; + } + ReconcileAction action = reconcileTransaction( + requiredDataDirectory, + requiredLevelRoot, + requiredBukkitConfiguration, + transaction, + requiredFeedback + ); + switch (action) { + case PUBLISHED -> published++; + case ROLLED_BACK -> rolledBack++; + case RETAINED -> retained++; + } + } + return new ReconcileResult(transactions.size(), published, rolledBack, retained, skipped); + } + + public static WorldGeneratorSnapshot replacementSnapshot(Transaction transaction) { + Transaction requiredTransaction = Objects.requireNonNull(transaction, "transaction"); + return new WorldGeneratorSnapshot( + true, + true, + true, + "Iris:" + requiredTransaction.dimension(), + true, + requiredTransaction.seed() + ); + } + + private static ReconcileAction reconcileTransaction( + Path dataDirectory, + Path levelRoot, + File bukkitConfiguration, + Transaction transaction, + Consumer feedback + ) throws IOException { + ExactWorldSlotPathPolicy.Target target = WorldReplacementJournal.resolveTarget(transaction, levelRoot); + ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id()); + WorldGeneratorSnapshot current = BukkitWorldConfiguration.snapshot( + bukkitConfiguration, + transaction.worldName() + ); + WorldGeneratorSnapshot replacement = replacementSnapshot(transaction); + if (transaction.phase() == Phase.CLEANUP_PENDING) { + if (!current.matchesGeneratorAndSeed(replacement)) { + throw conflict(transaction, "A verified replacement no longer matches bukkit.yml."); + } + WorldReplacementFilesystem.validateCommittedTarget(paths, transaction.packFingerprint()); + return ReconcileAction.RETAINED; + } + if (transaction.phase() == Phase.ROLLBACK_CLEANUP) { + if (!current.matchesGeneratorAndSeed(transaction.originalConfiguration())) { + throw conflict(transaction, "Rollback cleanup no longer matches the retained bukkit.yml state."); + } + WorldReplacementFilesystem.finishPreparedRollback(paths, transaction.originalTargetPresent()); + WorldReplacementJournal.delete(dataDirectory, transaction.id()); + feedback.accept("Restored the retained world for " + transaction.worldKey() + "."); + return ReconcileAction.ROLLED_BACK; + } + if (transaction.phase() == Phase.ROLLBACK_PENDING) { + rollback(dataDirectory, bukkitConfiguration, transaction, paths, current, replacement); + feedback.accept("Restored the retained world for " + transaction.worldKey() + "."); + return ReconcileAction.ROLLED_BACK; + } + Transaction active = transaction; + if (active.phase() == Phase.PREPARED) { + if (current.matchesGeneratorAndSeed(active.originalConfiguration())) { + rollback(dataDirectory, bukkitConfiguration, active, paths, current, replacement); + feedback.accept("Cancelled incomplete Iris world replacement for " + active.worldKey() + "."); + return ReconcileAction.ROLLED_BACK; + } + if (!current.matchesGeneratorAndSeed(replacement)) { + throw conflict(active, "bukkit.yml matches neither the replacement nor its retained original state."); + } + active = active.withPhase(Phase.ARMED); + WorldReplacementJournal.write(dataDirectory, active); + } + if (active.phase() == Phase.ARMED) { + if (!current.matchesGeneratorAndSeed(replacement)) { + if (current.matchesGeneratorAndSeed(active.originalConfiguration())) { + rollback(dataDirectory, bukkitConfiguration, active, paths, current, replacement); + feedback.accept("Cancelled Iris world replacement for " + active.worldKey() + "."); + return ReconcileAction.ROLLED_BACK; + } + throw conflict(active, "bukkit.yml matches neither the replacement nor its retained original state."); + } + try { + refreshOverworldEntryGuard(levelRoot, active, paths); + WorldReplacementFilesystem.publish( + paths, + active.originalTargetPresent(), + active.packFingerprint() + ); + } catch (IOException publishFailure) { + throw new IOException("Pending replacement for " + active.worldKey() + + " cannot be published: " + publishFailure.getMessage() + + " To recover, restore the original generator entry for \"" + active.worldName() + + "\" in bukkit.yml (the next boot rolls the replacement back), or delete the journal at " + + dataDirectory.resolve(WorldReplacementJournal.DIRECTORY_NAME).resolve(active.id() + ".properties") + + ".", publishFailure); + } + active = active.withPhase(Phase.PUBLISHED); + WorldReplacementJournal.write(dataDirectory, active); + feedback.accept("Published Iris world replacement for " + active.worldKey() + + "; waiting for runtime verification."); + return ReconcileAction.PUBLISHED; + } + if (active.phase() == Phase.PUBLISHED) { + if (!current.matchesGeneratorAndSeed(replacement)) { + if (current.matchesGeneratorAndSeed(active.originalConfiguration())) { + rollback(dataDirectory, bukkitConfiguration, active, paths, current, replacement); + feedback.accept("Rolled back Iris world replacement for " + active.worldKey() + "."); + return ReconcileAction.ROLLED_BACK; + } + throw conflict(active, "bukkit.yml matches neither the replacement nor its retained original state."); + } + refreshOverworldEntryGuard(levelRoot, active, paths); + WorldReplacementFilesystem.publish( + paths, + active.originalTargetPresent(), + active.packFingerprint() + ); + return ReconcileAction.RETAINED; + } + throw conflict(active, "Replacement journal reached an unsupported bootstrap phase."); + } + + private static void refreshOverworldEntryGuard( + Path levelRoot, + Transaction transaction, + ReplacementPaths paths + ) throws IOException { + if (!WorldSlotKey.minecraft("overworld").equals(transaction.worldKey())) { + return; + } + Path replacementWorld = Files.isDirectory(paths.stage(), LinkOption.NOFOLLOW_LINKS) + ? paths.stage() + : paths.target(); + WorldReplacementEntryGuard.refreshPlayers(levelRoot, replacementWorld, transaction.id()); + } + + private static void rollback( + Path dataDirectory, + File bukkitConfiguration, + Transaction transaction, + ReplacementPaths paths, + WorldGeneratorSnapshot current, + WorldGeneratorSnapshot replacement + ) throws IOException { + if (!current.matchesGeneratorAndSeed(replacement) + && !current.matchesGeneratorAndSeed(transaction.originalConfiguration())) { + throw conflict(transaction, "bukkit.yml conflicts with the pending world rollback."); + } + Transaction rollback = transaction.phase() == Phase.ROLLBACK_PENDING + ? transaction + : transaction.withPhase(Phase.ROLLBACK_PENDING); + if (transaction.phase() != Phase.ROLLBACK_PENDING) { + WorldReplacementJournal.write(dataDirectory, rollback); + } + WorldReplacementFilesystem.prepareRollback(paths, rollback.originalTargetPresent()); + if (current.matchesGeneratorAndSeed(replacement)) { + boolean restored; + try { + restored = BukkitWorldConfiguration.restoreIfMatching( + bukkitConfiguration, + rollback.worldName(), + replacement, + rollback.originalConfiguration() + ); + } catch (IOException failure) { + republishAfterConfigurationFailure(rollback, paths, failure); + throw failure; + } + if (!restored) { + WorldGeneratorSnapshot observed = BukkitWorldConfiguration.snapshot( + bukkitConfiguration, + rollback.worldName() + ); + if (!observed.matchesGeneratorAndSeed(rollback.originalConfiguration())) { + IOException failure = conflict( + rollback, + "bukkit.yml changed during startup rollback." + ); + republishAfterConfigurationFailure(rollback, paths, failure); + throw failure; + } + } + } + Transaction cleanup = rollback.withPhase(Phase.ROLLBACK_CLEANUP); + WorldReplacementJournal.write(dataDirectory, cleanup); + WorldReplacementFilesystem.finishPreparedRollback(paths, cleanup.originalTargetPresent()); + WorldReplacementJournal.delete(dataDirectory, cleanup.id()); + } + + private static void republishAfterConfigurationFailure( + Transaction transaction, + ReplacementPaths paths, + IOException failure + ) { + try { + WorldReplacementFilesystem.publish( + paths, + transaction.originalTargetPresent(), + transaction.packFingerprint() + ); + } catch (Throwable republishFailure) { + failure.addSuppressed(republishFailure); + } + } + + private static IOException conflict(Transaction transaction, String detail) { + return new IOException("Pending replacement for " + transaction.worldKey() + " is blocked: " + detail); + } + + public record ReconcileResult(int transactions, int published, int rolledBack, int retained, int skipped) { + } + + private enum ReconcileAction { + PUBLISHED, + ROLLED_BACK, + RETAINED + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementBootstrapMarker.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementBootstrapMarker.java new file mode 100644 index 000000000..543c8e637 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementBootstrapMarker.java @@ -0,0 +1,16 @@ +package art.arcane.iris.core.lifecycle; + +public final class WorldReplacementBootstrapMarker { + private static volatile boolean bootstrappedThisProcess; + + private WorldReplacementBootstrapMarker() { + } + + public static boolean wasBootstrappedThisProcess() { + return bootstrappedThisProcess; + } + + public static void markBootstrapped() { + bootstrappedThisProcess = true; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementEntryGuard.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementEntryGuard.java new file mode 100644 index 000000000..95d4dc67e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementEntryGuard.java @@ -0,0 +1,231 @@ +package art.arcane.iris.core.lifecycle; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class WorldReplacementEntryGuard { + public static final String MARKER_NAME = "replacement-entry.properties"; + + private static final Pattern PLAYER_DATA_NAME = Pattern.compile( + "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\\.dat$"); + + private WorldReplacementEntryGuard() { + } + + public static Entry stage(Path levelRoot, Path stagedWorld, UUID transactionId) throws IOException { + Path requiredLevelRoot = normalize(levelRoot, "levelRoot"); + Path requiredStagedWorld = normalize(stagedWorld, "stagedWorld"); + Entry entry = new Entry( + Objects.requireNonNull(transactionId, "transactionId"), + discoverPlayers(requiredLevelRoot) + ); + write(requiredStagedWorld, entry); + return entry; + } + + public static Entry refreshPlayers(Path levelRoot, Path worldDirectory, UUID transactionId) throws IOException { + Path requiredLevelRoot = normalize(levelRoot, "levelRoot"); + Path requiredWorldDirectory = normalize(worldDirectory, "worldDirectory"); + UUID requiredTransactionId = Objects.requireNonNull(transactionId, "transactionId"); + Optional loaded = load(requiredWorldDirectory); + if (loaded.isEmpty()) { + throw new IOException("The staged Overworld replacement is missing its entry marker."); + } + Entry current = loaded.get(); + if (!current.transactionId().equals(requiredTransactionId)) { + throw new IOException("Replacement entry marker belongs to another transaction."); + } + HashSet pendingPlayers = new HashSet<>(current.pendingPlayers()); + pendingPlayers.addAll(discoverPlayers(requiredLevelRoot)); + Entry refreshed = new Entry(requiredTransactionId, pendingPlayers); + write(requiredWorldDirectory, refreshed); + return refreshed; + } + + public static Optional load(Path worldDirectory) throws IOException { + Path marker = marker(worldDirectory); + if (!Files.exists(marker, LinkOption.NOFOLLOW_LINKS)) { + return Optional.empty(); + } + if (Files.isSymbolicLink(marker) || !Files.isRegularFile(marker, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Replacement entry marker is unsafe: " + marker); + } + Properties properties = new Properties(); + try (InputStream input = Files.newInputStream(marker)) { + properties.load(input); + } + UUID transactionId; + try { + transactionId = UUID.fromString(required(properties, "transaction")); + } catch (IllegalArgumentException failure) { + throw new IOException("Replacement entry marker contains an invalid transaction id.", failure); + } + HashSet pendingPlayers = new HashSet<>(); + String encodedPlayers = properties.getProperty("pendingPlayers", "").trim(); + if (!encodedPlayers.isEmpty()) { + for (String encodedPlayer : encodedPlayers.split(",", -1)) { + try { + if (!pendingPlayers.add(UUID.fromString(encodedPlayer))) { + throw new IOException("Replacement entry marker contains a duplicate player id."); + } + } catch (IllegalArgumentException failure) { + throw new IOException("Replacement entry marker contains an invalid player id.", failure); + } + } + } + return Optional.of(new Entry(transactionId, pendingPlayers)); + } + + public static Optional completePlayer( + Path worldDirectory, + UUID transactionId, + UUID playerId + ) throws IOException { + Path requiredWorldDirectory = normalize(worldDirectory, "worldDirectory"); + UUID requiredTransactionId = Objects.requireNonNull(transactionId, "transactionId"); + UUID requiredPlayerId = Objects.requireNonNull(playerId, "playerId"); + Optional loaded = load(requiredWorldDirectory); + if (loaded.isEmpty()) { + return Optional.empty(); + } + Entry current = loaded.get(); + if (!current.transactionId().equals(requiredTransactionId)) { + throw new IOException("Replacement entry marker belongs to another transaction."); + } + HashSet remaining = new HashSet<>(current.pendingPlayers()); + remaining.remove(requiredPlayerId); + Entry updated = new Entry(requiredTransactionId, remaining); + write(requiredWorldDirectory, updated); + return Optional.of(updated); + } + + public static boolean retireIfEmpty(Path worldDirectory, UUID transactionId) throws IOException { + Path requiredWorldDirectory = normalize(worldDirectory, "worldDirectory"); + Optional loaded = load(requiredWorldDirectory); + if (loaded.isEmpty()) { + return true; + } + Entry current = loaded.get(); + if (!current.transactionId().equals(Objects.requireNonNull(transactionId, "transactionId"))) { + throw new IOException("Replacement entry marker belongs to another transaction."); + } + if (!current.pendingPlayers().isEmpty()) { + return false; + } + Path marker = marker(requiredWorldDirectory); + Files.delete(marker); + DirectoryDurability.forceDirectoryAfterCommit(marker.getParent(), "A replacement entry marker retirement"); + return true; + } + + private static Set discoverPlayers(Path levelRoot) throws IOException { + Path playerData = levelRoot.resolve("players/data"); + if (!Files.exists(playerData, LinkOption.NOFOLLOW_LINKS)) { + return Set.of(); + } + if (Files.isSymbolicLink(playerData) || !Files.isDirectory(playerData, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Player data storage is unsafe: " + playerData); + } + HashSet players = new HashSet<>(); + try (DirectoryStream files = Files.newDirectoryStream(playerData)) { + for (Path file : files) { + Matcher matcher = PLAYER_DATA_NAME.matcher(file.getFileName().toString()); + if (!matcher.matches()) { + continue; + } + if (Files.isSymbolicLink(file) || !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Player data entry is unsafe: " + file); + } + players.add(UUID.fromString(matcher.group(1))); + } + } + return Set.copyOf(players); + } + + private static void write(Path worldDirectory, Entry entry) throws IOException { + Path marker = marker(worldDirectory); + Path parent = marker.getParent(); + if (Files.isSymbolicLink(parent)) { + throw new IOException("Replacement entry storage is unsafe: " + parent); + } + Files.createDirectories(parent); + if (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Replacement entry storage is not a directory: " + parent); + } + Properties properties = new Properties(); + properties.setProperty("transaction", entry.transactionId().toString()); + ArrayList orderedPlayers = new ArrayList<>(entry.pendingPlayers()); + orderedPlayers.sort(Comparator.comparing(UUID::toString)); + properties.setProperty( + "pendingPlayers", + String.join(",", orderedPlayers.stream().map(UUID::toString).toList()) + ); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + properties.store(output, null); + Path staged = Files.createTempFile(parent, ".replacement-entry-", ".tmp"); + try { + try (FileChannel channel = FileChannel.open( + staged, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE + )) { + ByteBuffer buffer = ByteBuffer.wrap(output.toByteArray()); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move(staged, marker, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException failure) { + throw new IOException("Replacement entry marker requires atomic publication.", failure); + } + DirectoryDurability.forceDirectoryAfterCommit(parent, "A replacement entry marker change"); + } finally { + Files.deleteIfExists(staged); + } + } + + private static Path marker(Path worldDirectory) { + return normalize(worldDirectory, "worldDirectory").resolve("iris").resolve(MARKER_NAME); + } + + private static Path normalize(Path path, String name) { + return Objects.requireNonNull(path, name).toAbsolutePath().normalize(); + } + + private static String required(Properties properties, String key) throws IOException { + String value = properties.getProperty(key); + if (value == null || value.isBlank()) { + throw new IOException("Replacement entry marker is missing " + key + "."); + } + return value.trim(); + } + + public record Entry(UUID transactionId, Set pendingPlayers) { + public Entry { + Objects.requireNonNull(transactionId, "transactionId"); + pendingPlayers = Set.copyOf(Objects.requireNonNull(pendingPlayers, "pendingPlayers")); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementFilesystem.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementFilesystem.java new file mode 100644 index 000000000..c0711aa6c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementFilesystem.java @@ -0,0 +1,493 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.ExactWorldSlotPathPolicy; +import art.arcane.iris.core.SnapshotDirectoryTreeDeleter; +import art.arcane.iris.core.pack.PackDirectoryResolver; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public final class WorldReplacementFilesystem { + private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace"; + private static final String MACOS_FINDER_METADATA_FILE = ".DS_Store"; + private static final List PAPER_WORLD_METADATA = List.of( + Path.of("data/paper/metadata.dat"), + Path.of("data/paper/level_overrides.dat"), + Path.of("data/minecraft/world_gen_settings.dat") + ); + private static final Pattern STAGE_NAME = Pattern.compile( + "^\\.iris-replace-[a-z0-9_-]+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.stage$"); + private static final Pattern BACKUP_NAME = Pattern.compile( + "^\\.iris-replace-[a-z0-9_-]+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.backup$"); + + private WorldReplacementFilesystem() { + } + + public static ReplacementPaths paths(ExactWorldSlotPathPolicy.Target target, UUID id) { + ExactWorldSlotPathPolicy.Target requiredTarget = Objects.requireNonNull(target, "target"); + UUID requiredId = Objects.requireNonNull(id, "id"); + String artifactBase = ".iris-replace-" + requiredTarget.worldKey().key() + "-" + requiredId; + return new ReplacementPaths( + requiredTarget.worldDirectory(), + requiredTarget.namespaceRoot().resolve(artifactBase + ".stage"), + requiredTarget.namespaceRoot().resolve(artifactBase + ".backup") + ); + } + + public static void requireExistingTarget(ReplacementPaths paths) throws IOException { + ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths"); + State state = inspect(requiredPaths); + if (!state.targetPresent()) { + throw new IOException("/iris replace requires an existing exact world slot; use /iris create for a new world."); + } + if (state.stagePresent() || state.backupPresent()) { + throw new IOException("A replacement artifact already exists for this transaction."); + } + requireCurrentPaperWorld(requiredPaths.target()); + } + + public static void requireCurrentPaperWorld(Path retainedWorld) throws IOException { + try { + requireDirectory(retainedWorld, "retained world"); + requireDirectory(retainedWorld.resolve("data"), "retained world data"); + requireDirectory(retainedWorld.resolve("data/paper"), "retained Paper data"); + requireDirectory(retainedWorld.resolve("data/minecraft"), "retained Minecraft data"); + for (Path relative : PAPER_WORLD_METADATA) { + BasicFileAttributes sourceAttributes = requireSafeEntry(retainedWorld.resolve(relative)); + if (!sourceAttributes.isRegularFile()) { + throw new IOException("Retained Paper world metadata is not a regular file: " + relative); + } + } + } catch (NoSuchFileException e) { + throw new IOException("World slot " + retainedWorld.getFileName() + + " is missing Paper world metadata (" + e.getFile() + + "); load the world once on this server before replacing it.", e); + } + } + + public static void publish( + ReplacementPaths paths, + boolean originalTargetPresent, + String expectedPackFingerprint + ) throws IOException { + ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths"); + String expectedFingerprint = requireFingerprint(expectedPackFingerprint); + State state = inspect(requiredPaths); + if (state.stagePresent()) { + requireSafeTree(requiredPaths.stage(), "replacement stage"); + requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint); + if (originalTargetPresent) { + Path retainedWorld = state.targetPresent() + ? requiredPaths.target() + : requiredPaths.backup(); + preservePaperWorldMetadata(retainedWorld, requiredPaths.stage()); + } + if (state.targetPresent()) { + if (state.backupPresent()) { + throw new IOException("Replacement target, stage, and backup are all present."); + } + if (!originalTargetPresent) { + throw new IOException("Replacement target appeared after an absent target was staged."); + } + move(requiredPaths.target(), requiredPaths.backup()); + state = inspect(requiredPaths); + } + if (state.targetPresent()) { + throw new IOException("Replacement target is still present after backup publication."); + } + if (originalTargetPresent != state.backupPresent()) { + throw new IOException("Replacement backup state does not match the original target state."); + } + requireSafeTree(requiredPaths.stage(), "replacement stage"); + requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint); + move(requiredPaths.stage(), requiredPaths.target()); + state = inspect(requiredPaths); + } + + if (!state.targetPresent() || state.stagePresent()) { + throw new IOException("Replacement publication did not produce one exact target directory."); + } + if (originalTargetPresent != state.backupPresent()) { + throw new IOException("Replacement publication lost or unexpectedly created its backup."); + } + requireSafeTree(requiredPaths.target(), "replacement target"); + requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint); + if (originalTargetPresent) { + preservePaperWorldMetadata(requiredPaths.backup(), requiredPaths.target()); + requireSafeTree(requiredPaths.target(), "replacement target"); + requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint); + } + } + + public static void rollback(ReplacementPaths paths, boolean originalTargetPresent) throws IOException { + prepareRollback(paths, originalTargetPresent); + discardStage(paths); + } + + public static void prepareRollback(ReplacementPaths paths, boolean originalTargetPresent) throws IOException { + ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths"); + State state = inspect(requiredPaths); + if (originalTargetPresent) { + if (state.backupPresent()) { + if (state.targetPresent()) { + if (state.stagePresent()) { + throw new IOException("Rollback cannot quarantine two replacement directories."); + } + move(requiredPaths.target(), requiredPaths.stage()); + } + move(requiredPaths.backup(), requiredPaths.target()); + state = inspect(requiredPaths); + } + if (!state.targetPresent() || !state.stagePresent() || state.backupPresent()) { + throw new IOException("Rollback could not restore the original world target."); + } + } else { + if (state.backupPresent()) { + throw new IOException("An originally absent world acquired an unexpected backup."); + } + if (state.targetPresent()) { + if (state.stagePresent()) { + throw new IOException("Rollback cannot quarantine two replacement directories."); + } + move(requiredPaths.target(), requiredPaths.stage()); + state = inspect(requiredPaths); + } + if (state.targetPresent()) { + throw new IOException("Rollback could not remove the replacement target."); + } + if (!state.stagePresent()) { + throw new IOException("Rollback lost the quarantined replacement target."); + } + } + } + + public static void finishPreparedRollback(ReplacementPaths paths, boolean originalTargetPresent) + throws IOException { + ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths"); + State state = inspect(requiredPaths); + if (state.backupPresent()) { + throw new IOException("Rollback cleanup cannot continue while a retained backup is still published."); + } + if (originalTargetPresent != state.targetPresent()) { + throw new IOException("Rollback cleanup target state does not match the retained world state."); + } + discardStage(requiredPaths); + } + + public static void discardStage(ReplacementPaths paths) throws IOException { + ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths"); + State state = inspect(requiredPaths); + if (state.backupPresent()) { + throw new IOException("Cannot discard a replacement stage after a backup was published."); + } + if (state.stagePresent()) { + SnapshotDirectoryTreeDeleter.delete(requiredPaths.stage()); + } + } + + public static void cleanupBackup(ReplacementPaths paths) throws IOException { + ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths"); + State state = inspect(requiredPaths); + if (!state.targetPresent() || state.stagePresent()) { + throw new IOException("Cannot clean a replacement backup before publication is complete."); + } + if (state.backupPresent()) { + SnapshotDirectoryTreeDeleter.delete(requiredPaths.backup()); + } + } + + public static void validateCommittedTarget(ReplacementPaths paths, String expectedPackFingerprint) + throws IOException { + ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths"); + String expectedFingerprint = requireFingerprint(expectedPackFingerprint); + State state = inspect(requiredPaths); + if (!state.targetPresent() || state.stagePresent()) { + throw new IOException("Committed replacement storage does not contain one exact target directory."); + } + requireSafeTree(requiredPaths.target(), "replacement target"); + requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint); + } + + public static void validatePublishedTarget( + ReplacementPaths paths, + boolean originalTargetPresent, + String expectedPackFingerprint + ) throws IOException { + ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths"); + String expectedFingerprint = requireFingerprint(expectedPackFingerprint); + State state = inspect(requiredPaths); + if (!state.targetPresent() || state.stagePresent() + || originalTargetPresent != state.backupPresent()) { + throw new IOException("Published replacement storage does not match its retained-world contract."); + } + requireSafeTree(requiredPaths.target(), "replacement target"); + requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint); + } + + public static String fingerprintPack(Path packRoot) throws IOException { + Path root = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize(); + requireDirectory(root, "pack root"); + MessageDigest digest = sha256(); + List entries; + try (Stream stream = Files.walk(root)) { + entries = stream + .filter(path -> !path.equals(root)) + .map(path -> fingerprintEntry(root, path)) + .sorted(Comparator.comparing(FingerprintEntry::relativeString)) + .toList(); + } + String separator = root.getFileSystem().getSeparator(); + byte[] buffer = new byte[8192]; + for (FingerprintEntry entry : entries) { + BasicFileAttributes attributes = requireSafeEntry(entry.path()); + Path relative = entry.relative(); + if (isGeneratedPackMetadata(relative, attributes)) { + continue; + } + update(digest, entry.relativeString().replace(separator, "/")); + digest.update((byte) (attributes.isDirectory() ? 1 : 0)); + if (!attributes.isRegularFile()) { + continue; + } + update(digest, Long.toString(attributes.size())); + try (InputStream input = Files.newInputStream(entry.path())) { + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + } + } + return HexFormat.of().formatHex(digest.digest()); + } + + private static FingerprintEntry fingerprintEntry(Path root, Path path) { + Path relative = root.relativize(path); + return new FingerprintEntry(path, relative, relative.toString()); + } + + private static State inspect(ReplacementPaths paths) throws IOException { + return new State( + directoryPresent(paths.target(), "replacement target"), + directoryPresent(paths.stage(), "replacement stage"), + directoryPresent(paths.backup(), "replacement backup") + ); + } + + private static boolean directoryPresent(Path path, String label) throws IOException { + if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + requireDirectory(path, label); + return true; + } + + private static void requireFingerprint(Path packRoot, String expected) throws IOException { + String actual = fingerprintPack(packRoot); + if (!actual.equals(expected)) { + throw new IOException("Staged world pack fingerprint changed before publication."); + } + } + + private static String requireFingerprint(String value) { + String fingerprint = Objects.requireNonNull(value, "expectedPackFingerprint"); + if (!fingerprint.matches("[0-9a-f]{64}")) { + throw new IllegalArgumentException("Pack fingerprint must be lowercase SHA-256."); + } + return fingerprint; + } + + private static boolean isGeneratedPackMetadata(Path relative, BasicFileAttributes attributes) { + String fileName = relative.getFileName().toString(); + if (PackDirectoryResolver.isHiddenName(relative.getName(0).toString())) { + return true; + } + return attributes.isRegularFile() + && (MACOS_FINDER_METADATA_FILE.equals(fileName) || fileName.endsWith(CODE_WORKSPACE_SUFFIX)); + } + + private static BasicFileAttributes requireSafeEntry(Path path) throws IOException { + BasicFileAttributes attributes = Files.readAttributes( + path, + BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS + ); + if (attributes.isSymbolicLink()) { + throw new IOException("Replacement storage contains a symbolic link: " + path); + } + if (!attributes.isDirectory() && !attributes.isRegularFile()) { + throw new IOException("Replacement storage contains an unsafe entry: " + path); + } + return attributes; + } + + private static void requireDirectory(Path path, String label) throws IOException { + BasicFileAttributes attributes = requireSafeEntry(path); + if (!attributes.isDirectory()) { + throw new IOException("The " + label + " is not a directory: " + path); + } + } + + private static void requireSafeTree(Path root, String label) throws IOException { + requireDirectory(root, label); + List entries; + try (Stream stream = Files.walk(root)) { + entries = stream.sorted().toList(); + } + for (Path entry : entries) { + requireSafeEntry(entry); + } + } + + private static void preservePaperWorldMetadata(Path retainedWorld, Path replacementWorld) throws IOException { + requireCurrentPaperWorld(retainedWorld); + requireDirectory(replacementWorld, "replacement world"); + ensureDirectory(replacementWorld.resolve("data"), "replacement world data"); + ensureDirectory(replacementWorld.resolve("data/paper"), "replacement Paper data"); + ensureDirectory(replacementWorld.resolve("data/minecraft"), "replacement Minecraft data"); + for (Path relative : PAPER_WORLD_METADATA) { + Path destination = replacementWorld.resolve(relative); + if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) { + BasicFileAttributes destinationAttributes = requireSafeEntry(destination); + if (!destinationAttributes.isRegularFile()) { + throw new IOException("Replacement Paper world metadata is not a regular file: " + relative); + } + continue; + } + copyMetadataFile(retainedWorld.resolve(relative), destination); + } + } + + private static void ensureDirectory(Path directory, String label) throws IOException { + if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + requireDirectory(directory, label); + return; + } + Files.createDirectory(directory); + forceDirectoryRequired(directory.getParent()); + } + + private static void copyMetadataFile(Path source, Path destination) throws IOException { + Path temporary = destination.resolveSibling("." + destination.getFileName() + ".iris-replace.tmp"); + if (Files.exists(temporary, LinkOption.NOFOLLOW_LINKS)) { + BasicFileAttributes attributes = requireSafeEntry(temporary); + if (!attributes.isRegularFile()) { + throw new IOException("Replacement metadata staging path is unsafe: " + temporary); + } + Files.delete(temporary); + } + try { + Files.copy(source, temporary, StandardCopyOption.COPY_ATTRIBUTES); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE)) { + channel.force(true); + } + try { + Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException exception) { + throw new IOException( + "Exact world replacement requires atomic Paper metadata publication on this filesystem.", + exception + ); + } + forceDirectoryRequired(destination.getParent()); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static void update(MessageDigest digest, String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + digest.update((byte) (bytes.length >>> 24)); + digest.update((byte) (bytes.length >>> 16)); + digest.update((byte) (bytes.length >>> 8)); + digest.update((byte) bytes.length); + digest.update(bytes); + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable.", exception); + } + } + + private static void move(Path source, Path target) throws IOException { + forceDirectoryRequired(source.getParent()); + try { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException exception) { + throw new IOException( + "Exact world replacement requires atomic directory moves on this filesystem.", + exception + ); + } + forceDirectoryAfterCommit(source.getParent()); + } + + private static void forceDirectoryRequired(Path directory) throws IOException { + DirectoryDurability.forceDirectoryRequired(directory); + } + + private static void forceDirectoryAfterCommit(Path directory) { + DirectoryDurability.forceDirectoryAfterCommit(directory, "A world-replacement move"); + } + + public record ReplacementPaths(Path target, Path stage, Path backup) { + public ReplacementPaths { + target = normalize(target, "target"); + stage = normalize(stage, "stage"); + backup = normalize(backup, "backup"); + Path parent = target.getParent(); + if (parent == null || !parent.equals(stage.getParent()) || !parent.equals(backup.getParent())) { + throw new IllegalArgumentException("Replacement paths must have one exact parent."); + } + if (target.equals(stage) || target.equals(backup) || stage.equals(backup)) { + throw new IllegalArgumentException("Replacement paths must be distinct."); + } + if (!STAGE_NAME.matcher(stage.getFileName().toString()).matches() + || !BACKUP_NAME.matcher(backup.getFileName().toString()).matches()) { + throw new IllegalArgumentException("Replacement artifact names are invalid."); + } + String stageStem = stage.getFileName().toString().replaceFirst("\\.stage$", ""); + String backupStem = backup.getFileName().toString().replaceFirst("\\.backup$", ""); + if (!stageStem.equals(backupStem)) { + throw new IllegalArgumentException("Replacement stage and backup do not belong to one transaction."); + } + } + + private static Path normalize(Path path, String label) { + Path required = Objects.requireNonNull(path, label).toAbsolutePath(); + for (Path component : required) { + if ("..".equals(component.toString())) { + throw new IllegalArgumentException("Replacement " + label + " contains path traversal."); + } + } + return required.normalize(); + } + } + + private record State(boolean targetPresent, boolean stagePresent, boolean backupPresent) { + } + + private record FingerprintEntry(Path path, Path relative, String relativeString) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementJournal.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementJournal.java new file mode 100644 index 000000000..4541b20cb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementJournal.java @@ -0,0 +1,420 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.ExactWorldSlotPathPolicy; +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.WorldSlotKey; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; + +public final class WorldReplacementJournal { + public static final String DIRECTORY_NAME = "pending-world-replacements"; + + private static final String JOURNAL_SUFFIX = ".properties"; + + private WorldReplacementJournal() { + } + + public static List load(Path dataDirectory, Path currentLevelRoot) throws IOException { + Path directory = directory(dataDirectory, false); + if (directory == null) { + return List.of(); + } + ArrayList transactions = new ArrayList<>(); + try (DirectoryStream files = Files.newDirectoryStream(directory, "*" + JOURNAL_SUFFIX)) { + for (Path file : files) { + if (Files.isSymbolicLink(file) || !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Replacement journal entry is unsafe: " + file); + } + try { + transactions.add(read(file)); + } catch (IOException failure) { + throw new IOException("Invalid replacement journal " + file + ": " + failure.getMessage(), failure); + } + } + } + transactions.sort(Comparator.comparing(transaction -> transaction.id().toString())); + // Only transactions that target the current level root compete for a slot; a stale + // journal recorded against another level root must not collide with a live one. + Set worldKeys = new HashSet<>(); + for (Transaction transaction : transactions) { + if (appliesTo(transaction, currentLevelRoot) && !worldKeys.add(transaction.worldKey())) { + throw new IOException("Multiple replacement journals target " + transaction.worldKey() + "."); + } + } + return List.copyOf(transactions); + } + + /** + * Whether this journal entry targets the current level root and logical world name. A + * mismatch means the operator changed level-name or world container after the replacement + * was staged; such entries are skipped at bootstrap instead of aborting the whole boot. + */ + public static boolean appliesTo(Transaction transaction, Path currentLevelRoot) { + Transaction requiredTransaction = Objects.requireNonNull(transaction, "transaction"); + ExactWorldSlotPathPolicy.Target target; + try { + target = ExactWorldSlotPathPolicy.resolve(currentLevelRoot, requiredTransaction.worldKey()); + } catch (RuntimeException failure) { + return false; + } + if (!target.levelRoot().equals(requiredTransaction.levelRoot())) { + return false; + } + String expectedWorldName; + try { + expectedWorldName = logicalWorldName(target.levelRoot(), requiredTransaction.worldKey()); + } catch (IllegalArgumentException failure) { + return false; + } + return expectedWorldName.equals(requiredTransaction.worldName()); + } + + public static void write(Path dataDirectory, Transaction transaction) throws IOException { + Transaction requiredTransaction = Objects.requireNonNull(transaction, "transaction"); + Path directory = Objects.requireNonNull(directory(dataDirectory, true)); + Path target = directory.resolve(requiredTransaction.id() + JOURNAL_SUFFIX); + Properties properties = new Properties(); + properties.setProperty("id", requiredTransaction.id().toString()); + properties.setProperty("worldKey", requiredTransaction.worldKey().toString()); + properties.setProperty("worldName", requiredTransaction.worldName()); + properties.setProperty("levelRoot", requiredTransaction.levelRoot().toString()); + properties.setProperty("dimension", requiredTransaction.dimension()); + properties.setProperty("seed", Long.toString(requiredTransaction.seed())); + properties.setProperty("packFingerprint", requiredTransaction.packFingerprint()); + properties.setProperty( + "originalTargetPresent", + Boolean.toString(requiredTransaction.originalTargetPresent()) + ); + properties.setProperty("phase", requiredTransaction.phase().name()); + writeSnapshot(properties, "original.", requiredTransaction.originalConfiguration()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + properties.store(output, null); + writeAtomic(target, output.toByteArray()); + } + + public static void delete(Path dataDirectory, UUID id) throws IOException { + Path directory = directory(dataDirectory, false); + if (directory == null) { + return; + } + forceDirectoryRequired(directory); + Files.deleteIfExists(directory.resolve(Objects.requireNonNull(id, "id") + JOURNAL_SUFFIX)); + forceDirectoryAfterCommit(directory); + } + + public static ExactWorldSlotPathPolicy.Target resolveTarget(Transaction transaction, Path currentLevelRoot) + throws IOException { + Transaction requiredTransaction = Objects.requireNonNull(transaction, "transaction"); + ExactWorldSlotPathPolicy.Target target = ExactWorldSlotPathPolicy.resolve( + currentLevelRoot, + requiredTransaction.worldKey() + ); + if (!target.levelRoot().equals(requiredTransaction.levelRoot())) { + throw new IOException("The configured level root changed after the world replacement was staged."); + } + String expectedWorldName; + try { + expectedWorldName = logicalWorldName(target.levelRoot(), requiredTransaction.worldKey()); + } catch (IllegalArgumentException failure) { + throw new IOException("The replacement journal targets an ambiguous logical world name.", failure); + } + if (!expectedWorldName.equals(requiredTransaction.worldName())) { + throw new IOException("The logical world name changed after the world replacement was staged."); + } + return target; + } + + public static String logicalWorldName(Path levelRoot, WorldSlotKey worldKey) { + Path requiredLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize(); + WorldSlotKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey"); + Path fileName = requiredLevelRoot.getFileName(); + if (fileName == null || fileName.toString().isBlank()) { + throw new IllegalArgumentException("Level root must have a logical world name."); + } + String levelName = fileName.toString(); + if ("iris".equals(requiredWorldKey.namespace())) { + return IrisWorldStorage.configuredWorldName(requiredWorldKey, levelName); + } + if (WorldSlotKey.minecraft("overworld").equals(requiredWorldKey)) { + return levelName; + } + if (WorldSlotKey.minecraft("the_nether").equals(requiredWorldKey)) { + return levelName + "_nether"; + } + if (WorldSlotKey.minecraft("the_end").equals(requiredWorldKey)) { + return levelName + "_the_end"; + } + throw new IllegalArgumentException("World key is not an exact replaceable world slot: " + requiredWorldKey); + } + + private static Transaction read(Path file) throws IOException { + Properties properties = new Properties(); + try (InputStream input = Files.newInputStream(file)) { + properties.load(input); + } + UUID id; + try { + id = UUID.fromString(required(properties, "id")); + } catch (IllegalArgumentException failure) { + throw new IOException("Replacement journal contains an invalid transaction id.", failure); + } + if (!file.getFileName().toString().equals(id + JOURNAL_SUFFIX)) { + throw new IOException("Replacement journal filename does not match its transaction id."); + } + WorldSlotKey worldKey; + try { + worldKey = WorldSlotKey.parse(required(properties, "worldKey")); + } catch (IllegalArgumentException failure) { + throw new IOException("Replacement journal contains an invalid world key.", failure); + } + String worldName = exact(properties, "worldName"); + Path recordedLevelRoot = recordedLevelRoot(properties); + String dimension = required(properties, "dimension"); + if (!safeDimension(dimension)) { + throw new IOException("Replacement journal contains an invalid dimension key."); + } + long seed = parseLong(properties, "seed"); + String packFingerprint = required(properties, "packFingerprint"); + if (!packFingerprint.matches("[0-9a-f]{64}")) { + throw new IOException("Replacement journal contains an invalid pack fingerprint."); + } + WorldGeneratorSnapshot original = readSnapshot(properties, "original."); + boolean originalTargetPresent = parseBoolean(properties, "originalTargetPresent"); + Phase phase; + try { + phase = Phase.valueOf(required(properties, "phase")); + } catch (IllegalArgumentException failure) { + throw new IOException("Replacement journal contains an invalid phase.", failure); + } + Transaction transaction = new Transaction( + id, + worldKey, + worldName, + recordedLevelRoot, + dimension, + seed, + packFingerprint, + original, + originalTargetPresent, + phase + ); + return transaction; + } + + private static Path recordedLevelRoot(Properties properties) throws IOException { + String value = exact(properties, "levelRoot"); + Path parsed; + try { + parsed = Path.of(value); + } catch (RuntimeException failure) { + throw new IOException("Replacement journal contains an invalid level root.", failure); + } + if (!parsed.isAbsolute() || !parsed.normalize().equals(parsed)) { + throw new IOException("Replacement journal level root must be an absolute normalized path."); + } + return parsed; + } + + private static Path directory(Path dataDirectory, boolean create) throws IOException { + Path dataRoot = Objects.requireNonNull(dataDirectory, "dataDirectory").toAbsolutePath().normalize(); + Path directory = dataRoot.resolve(DIRECTORY_NAME); + if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + if (!create) { + return null; + } + Files.createDirectories(directory); + } + if (Files.isSymbolicLink(directory) || !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Replacement journal storage is unsafe: " + directory); + } + return directory; + } + + private static void writeSnapshot(Properties properties, String prefix, WorldGeneratorSnapshot snapshot) { + properties.setProperty(prefix + "worldsSectionPresent", Boolean.toString(snapshot.worldsSectionPresent())); + properties.setProperty(prefix + "worldSectionPresent", Boolean.toString(snapshot.worldSectionPresent())); + properties.setProperty(prefix + "generatorPresent", Boolean.toString(snapshot.generatorPresent())); + if (snapshot.generatorPresent()) { + properties.setProperty(prefix + "generator", snapshot.generator()); + } + properties.setProperty(prefix + "seedPresent", Boolean.toString(snapshot.seedPresent())); + if (snapshot.seedPresent()) { + properties.setProperty(prefix + "seed", Long.toString(snapshot.seed())); + } + } + + private static WorldGeneratorSnapshot readSnapshot(Properties properties, String prefix) throws IOException { + boolean worldsPresent = parseBoolean(properties, prefix + "worldsSectionPresent"); + boolean worldPresent = parseBoolean(properties, prefix + "worldSectionPresent"); + boolean generatorPresent = parseBoolean(properties, prefix + "generatorPresent"); + String generator = generatorPresent ? exact(properties, prefix + "generator") : null; + boolean seedPresent = parseBoolean(properties, prefix + "seedPresent"); + Long seed = seedPresent ? parseLong(properties, prefix + "seed") : null; + try { + return new WorldGeneratorSnapshot( + worldsPresent, + worldPresent, + generatorPresent, + generator, + seedPresent, + seed + ); + } catch (IllegalArgumentException failure) { + throw new IOException("Replacement journal contains an invalid configuration snapshot.", failure); + } + } + + private static boolean safeDimension(String value) { + if (value.isEmpty() || value.length() > 256 || value.startsWith(".") || value.contains("..")) { + return false; + } + String[] segments = value.split("/", -1); + if (segments.length > 16) { + return false; + } + for (String segment : segments) { + if (segment.isEmpty() || !segment.matches("[A-Za-z0-9_-]+")) { + return false; + } + } + return true; + } + + private static String exact(Properties properties, String key) throws IOException { + String value = properties.getProperty(key); + if (value == null) { + throw new IOException("Replacement journal is missing " + key + "."); + } + return value; + } + + private static String required(Properties properties, String key) throws IOException { + String value = exact(properties, key); + if (value.isBlank()) { + throw new IOException("Replacement journal is missing " + key + "."); + } + return value.trim(); + } + + private static boolean parseBoolean(Properties properties, String key) throws IOException { + String value = required(properties, key); + if (!"true".equals(value) && !"false".equals(value)) { + throw new IOException("Replacement journal contains an invalid boolean for " + key + "."); + } + return Boolean.parseBoolean(value); + } + + private static long parseLong(Properties properties, String key) throws IOException { + try { + return Long.parseLong(required(properties, key)); + } catch (NumberFormatException failure) { + throw new IOException("Replacement journal contains an invalid integer for " + key + ".", failure); + } + } + + private static void writeAtomic(Path target, byte[] content) throws IOException { + Path parent = Objects.requireNonNull(target.getParent(), "journal parent"); + Path temporary = parent.resolve("." + target.getFileName() + ".tmp-" + UUID.randomUUID()); + try { + try (FileChannel channel = FileChannel.open( + temporary, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE + )) { + ByteBuffer buffer = ByteBuffer.wrap(content); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + forceDirectoryRequired(parent); + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException failure) { + throw new IOException( + "Exact world replacement requires atomic journal publication on this filesystem.", + failure + ); + } + forceDirectoryAfterCommit(parent); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static void forceDirectoryRequired(Path directory) throws IOException { + DirectoryDurability.forceDirectoryRequired(directory); + } + + private static void forceDirectoryAfterCommit(Path directory) { + DirectoryDurability.forceDirectoryAfterCommit(directory, "A world-replacement journal change"); + } + + public record Transaction( + UUID id, + WorldSlotKey worldKey, + String worldName, + Path levelRoot, + String dimension, + long seed, + String packFingerprint, + WorldGeneratorSnapshot originalConfiguration, + boolean originalTargetPresent, + Phase phase + ) { + public Transaction { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(worldKey, "worldKey"); + Objects.requireNonNull(worldName, "worldName"); + levelRoot = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize(); + Objects.requireNonNull(dimension, "dimension"); + Objects.requireNonNull(packFingerprint, "packFingerprint"); + Objects.requireNonNull(originalConfiguration, "originalConfiguration"); + Objects.requireNonNull(phase, "phase"); + } + + public Transaction withPhase(Phase nextPhase) { + return new Transaction( + id, + worldKey, + worldName, + levelRoot, + dimension, + seed, + packFingerprint, + originalConfiguration, + originalTargetPresent, + nextPhase + ); + } + } + + public enum Phase { + PREPARED, + ARMED, + PUBLISHED, + ROLLBACK_PENDING, + ROLLBACK_CLEANUP, + CLEANUP_PENDING + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementSeed.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementSeed.java new file mode 100644 index 000000000..930508816 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldReplacementSeed.java @@ -0,0 +1,162 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.volmlib.util.nbt.io.NBTUtil; +import art.arcane.volmlib.util.nbt.io.NamedTag; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.tag.LongTag; +import art.arcane.volmlib.util.nbt.tag.Tag; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Objects; +import java.util.OptionalLong; + +public final class WorldReplacementSeed { + private static final Path WORLD_GEN_SETTINGS = Path.of("data/minecraft/world_gen_settings.dat"); + + private WorldReplacementSeed() { + } + + public static long readAuthoritativeSeed(Path worldDirectory) throws IOException { + Path requiredWorldDirectory = Objects.requireNonNull(worldDirectory, "worldDirectory") + .toAbsolutePath() + .normalize(); + Path settings = requiredWorldDirectory.resolve(WORLD_GEN_SETTINGS); + NamedTag namedTag = readSettings(settings); + return requireData(namedTag, settings).getLongTag("seed").asLong(); + } + + public static long stageAuthoritativeSeed( + Path sourceWorldDirectory, + Path stagedWorldDirectory, + OptionalLong requestedSeed + ) throws IOException { + Path sourceWorld = Objects.requireNonNull(sourceWorldDirectory, "sourceWorldDirectory") + .toAbsolutePath() + .normalize(); + Path stagedWorld = Objects.requireNonNull(stagedWorldDirectory, "stagedWorldDirectory") + .toAbsolutePath() + .normalize(); + OptionalLong requiredRequestedSeed = Objects.requireNonNull(requestedSeed, "requestedSeed"); + Path source = sourceWorld.resolve(WORLD_GEN_SETTINGS); + NamedTag namedTag = readSettings(source); + CompoundTag data = requireData(namedTag, source); + long retainedSeed = data.getLongTag("seed").asLong(); + long effectiveSeed = requiredRequestedSeed.orElse(retainedSeed); + writeSettings(stagedWorld, namedTag, data, effectiveSeed); + return effectiveSeed; + } + + public static void copyWithAuthoritativeSeed( + Path sourceWorldDirectory, + Path targetWorldDirectory, + long seed + ) throws IOException { + Path sourceWorld = Objects.requireNonNull(sourceWorldDirectory, "sourceWorldDirectory") + .toAbsolutePath() + .normalize(); + Path targetWorld = Objects.requireNonNull(targetWorldDirectory, "targetWorldDirectory") + .toAbsolutePath() + .normalize(); + Path source = sourceWorld.resolve(WORLD_GEN_SETTINGS); + NamedTag namedTag = readSettings(source); + CompoundTag data = requireData(namedTag, source); + writeSettings(targetWorld, namedTag, data, seed); + } + + private static void writeSettings( + Path targetWorld, + NamedTag namedTag, + CompoundTag data, + long seed + ) throws IOException { + Path target = targetWorld.resolve(WORLD_GEN_SETTINGS); + data.putLong("seed", seed); + if (Files.exists(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) { + throw new IOException("Staged Paper world generation settings already exist: " + target); + } + Path parent = target.getParent(); + if (parent == null) { + throw new IOException("Staged Paper world generation settings have no parent: " + target); + } + Files.createDirectories(parent); + Path staged = Files.createTempFile(parent, ".world-gen-settings-", ".dat"); + try { + NBTUtil.write(namedTag, staged.toFile(), true); + try (FileChannel channel = FileChannel.open(staged, StandardOpenOption.WRITE)) { + channel.force(true); + } + try { + Files.move(staged, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException exception) { + Files.move(staged, target); + } + forceSettingsHierarchy(targetWorld, parent); + } finally { + Files.deleteIfExists(staged); + } + + long writtenSeed = readAuthoritativeSeed(targetWorld); + if (writtenSeed != seed) { + throw new IOException("Staged Paper world generation settings did not retain the requested seed."); + } + } + + private static void forceSettingsHierarchy(Path targetWorld, Path settingsParent) throws IOException { + Path directory = settingsParent; + while (directory != null && directory.startsWith(targetWorld)) { + DirectoryDurability.forceDirectoryRequired(directory); + if (directory.equals(targetWorld)) { + return; + } + directory = directory.getParent(); + } + throw new IOException("Staged Paper world generation settings escaped their target directory."); + } + + private static NamedTag readSettings(Path settings) throws IOException { + BasicFileAttributes attributes = Files.readAttributes( + settings, + BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS + ); + if (attributes.isSymbolicLink() || !attributes.isRegularFile()) { + throw new IOException("Paper world generation settings are not a regular file: " + settings); + } + + try { + return NBTUtil.read(settings.toFile()); + } catch (IOException failure) { + throw new IOException("Could not read Paper world generation settings: " + settings, failure); + } + } + + private static CompoundTag requireData(NamedTag namedTag, Path settings) throws IOException { + Tag rootTag = namedTag.getTag(); + if (!(rootTag instanceof CompoundTag root)) { + throw new IOException("Paper world generation settings must have a compound root: " + settings); + } + Tag dataTag = root.get("data"); + if (dataTag == null) { + throw new IOException("Paper world generation settings are missing data: " + settings); + } + if (!(dataTag instanceof CompoundTag data)) { + throw new IOException("Paper world generation settings data must be a compound tag: " + settings); + } + Tag seedTag = data.get("seed"); + if (seedTag == null) { + throw new IOException("Paper world generation settings are missing data.seed: " + settings); + } + if (!(seedTag instanceof LongTag seed)) { + throw new IOException("Paper world generation settings data.seed must be a long tag: " + settings); + } + return data; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldUnloadBoundaryRegistry.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldUnloadBoundaryRegistry.java new file mode 100644 index 000000000..546ca92ce --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldUnloadBoundaryRegistry.java @@ -0,0 +1,41 @@ +package art.arcane.iris.core.lifecycle; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; + +public final class WorldUnloadBoundaryRegistry { + private static final ConcurrentHashMap ACTIVE = new ConcurrentHashMap<>(); + + private WorldUnloadBoundaryRegistry() { + } + + static Boundary begin(String worldIdentity) { + String requiredIdentity = Objects.requireNonNull(worldIdentity, "world identity"); + Boundary boundary = new Boundary(requiredIdentity, new CompletableFuture<>()); + Boundary existing = ACTIVE.putIfAbsent(requiredIdentity, boundary); + if (existing != null) { + throw new IllegalStateException("World unload is already active for " + requiredIdentity + "."); + } + return boundary; + } + + public static CompletionStage claim(String worldIdentity) { + Boundary boundary = ACTIVE.remove(Objects.requireNonNull(worldIdentity, "world identity")); + return boundary == null ? null : boundary.completion(); + } + + static void complete(Boundary boundary, Boolean unloaded, Throwable failure) { + Objects.requireNonNull(boundary, "world unload boundary"); + ACTIVE.remove(boundary.worldIdentity(), boundary); + if (failure == null) { + boundary.completion().complete(Boolean.TRUE.equals(unloaded)); + return; + } + boundary.completion().completeExceptionally(WorldLifecycleSupport.unwrap(failure)); + } + + record Boundary(String worldIdentity, CompletableFuture completion) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java new file mode 100644 index 000000000..f6ee2d31d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java @@ -0,0 +1,88 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.IrisWorldStorage; +import org.bukkit.World; +import org.bukkit.WorldType; + +import java.nio.file.Path; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +final class WorldsProviderBackend implements WorldLifecycleBackend { + private final CapabilitySnapshot capabilities; + + WorldsProviderBackend(CapabilitySnapshot capabilities) { + this.capabilities = capabilities; + } + + @Override + public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) { + return request.studio() && capabilities.hasWorldsProvider(); + } + + @Override + @SuppressWarnings("unchecked") + public CompletableFuture create(WorldLifecycleRequest request) { + try { + Path worldPath = IrisWorldStorage.dimensionRoot(request.worldKey()).toPath(); + Object builder = WorldLifecycleSupport.invokeNamed(capabilities.worldsProvider(), "levelBuilder", new Class[]{Path.class}, worldPath); + builder = WorldLifecycleSupport.invokeNamed(builder, "name", new Class[]{String.class}, request.worldName()); + builder = WorldLifecycleSupport.invokeNamed(builder, "seed", new Class[]{long.class}, request.seed()); + builder = WorldLifecycleSupport.invokeNamed(builder, "levelStem", new Class[]{capabilities.worldsLevelStemClass()}, resolveLevelStem(request.environment())); + builder = WorldLifecycleSupport.invokeNamed(builder, "chunkGenerator", new Class[]{org.bukkit.generator.ChunkGenerator.class}, request.generator()); + builder = WorldLifecycleSupport.invokeNamed(builder, "biomeProvider", new Class[]{org.bukkit.generator.BiomeProvider.class}, request.biomeProvider()); + builder = WorldLifecycleSupport.invokeNamed(builder, "generatorType", new Class[]{capabilities.worldsGeneratorTypeClass()}, resolveGeneratorType(request.worldType())); + builder = WorldLifecycleSupport.invokeNamed(builder, "structures", new Class[]{boolean.class}, request.generateStructures()); + builder = WorldLifecycleSupport.invokeNamed(builder, "hardcore", new Class[]{boolean.class}, request.hardcore()); + Object levelBuilder = WorldLifecycleSupport.invokeNamed(builder, "build", new Class[0]); + Object async = WorldLifecycleSupport.invokeNamed(levelBuilder, "createAsync", new Class[0]); + if (async instanceof CompletableFuture future) { + return future.thenApply(world -> (World) world); + } + return CompletableFuture.failedFuture(new IllegalStateException("Worlds provider createAsync did not return CompletableFuture.")); + } catch (Throwable e) { + return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e)); + } + } + + @Override + public CompletableFuture unloadAsync(World world, boolean save) { + return WorldLifecycleSupport.unloadWorldAsync(capabilities, world, save); + } + + @Override + public String backendName() { + return "worlds_provider"; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private Object resolveLevelStem(World.Environment environment) { + String key; + if (environment == World.Environment.NETHER) { + key = "NETHER"; + } else if (environment == World.Environment.THE_END) { + key = "END"; + } else { + key = "OVERWORLD"; + } + Class enumClass = capabilities.worldsLevelStemClass().asSubclass(Enum.class); + return Enum.valueOf(enumClass, key); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private Object resolveGeneratorType(WorldType worldType) { + String typeName = worldType == null ? "NORMAL" : worldType.getName(); + String key; + if ("FLAT".equalsIgnoreCase(typeName)) { + key = "FLAT"; + } else if ("AMPLIFIED".equalsIgnoreCase(typeName)) { + key = "AMPLIFIED"; + } else if ("LARGE_BIOMES".equalsIgnoreCase(typeName) || "LARGEBIOMES".equalsIgnoreCase(typeName)) { + key = "LARGE_BIOMES"; + } else { + key = "NORMAL"; + } + Class enumClass = capabilities.worldsGeneratorTypeClass().asSubclass(Enum.class); + return Enum.valueOf(enumClass, key.toUpperCase(Locale.ROOT)); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/link/ExternalDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/ExternalDataProvider.java similarity index 91% rename from core/src/main/java/com/volmit/iris/core/link/ExternalDataProvider.java rename to core/src/main/java/art/arcane/iris/core/link/ExternalDataProvider.java index 2ec93447e..9b4269a3b 100644 --- a/core/src/main/java/com/volmit/iris/core/link/ExternalDataProvider.java +++ b/core/src/main/java/art/arcane/iris/core/link/ExternalDataProvider.java @@ -1,14 +1,14 @@ -package com.volmit.iris.core.link; +package art.arcane.iris.core.link; -import com.volmit.iris.core.link.data.DataType; -import com.volmit.iris.core.nms.container.BiomeColor; -import com.volmit.iris.core.nms.container.BlockProperty; -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.math.RNG; +import art.arcane.iris.core.link.data.DataType; +import art.arcane.iris.core.nms.container.BiomeColor; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.data.IrisCustomData; +import art.arcane.volmlib.util.math.RNG; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -18,7 +18,6 @@ import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.data.BlockData; import org.bukkit.entity.Entity; -import org.bukkit.event.Listener; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; @@ -31,7 +30,7 @@ import java.util.MissingResourceException; @Getter @RequiredArgsConstructor -public abstract class ExternalDataProvider implements Listener { +public abstract class ExternalDataProvider { @NonNull private final String pluginId; diff --git a/core/src/main/java/art/arcane/iris/core/link/Identifier.java b/core/src/main/java/art/arcane/iris/core/link/Identifier.java new file mode 100644 index 000000000..65ea5ca6a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/Identifier.java @@ -0,0 +1,18 @@ +package art.arcane.iris.core.link; + +public record Identifier(String namespace, String key) { + private static final String DEFAULT_NAMESPACE = "minecraft"; + + public static Identifier fromString(String id) { + String[] strings = id.split(":", 2); + if (strings.length == 1) { + return new Identifier(DEFAULT_NAMESPACE, strings[0]); + } + return new Identifier(strings[0], strings[1]); + } + + @Override + public String toString() { + return namespace + ":" + key; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/link/MultiverseCoreLink.java b/core/src/main/java/art/arcane/iris/core/link/MultiverseCoreLink.java new file mode 100644 index 000000000..dab62c6d6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/MultiverseCoreLink.java @@ -0,0 +1,638 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.link; + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.IrisWorlds; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.platform.bukkit.BukkitEnvironment; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.mvplugins.multiverse.core.MultiverseCoreApi; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.core.utils.result.FailureReason; +import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; +import org.mvplugins.multiverse.core.world.MultiverseWorld; +import org.mvplugins.multiverse.core.world.WorldManager; +import org.mvplugins.multiverse.core.world.options.ImportWorldOptions; +import org.mvplugins.multiverse.core.world.options.LoadWorldOptions; +import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions; +import org.mvplugins.multiverse.core.world.reasons.LoadFailureReason; +import org.mvplugins.multiverse.core.world.reasons.RemoveFailureReason; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Multiverse indexes its world store by NamespacedKey and by the Bukkit world name it saw when the + * world was imported, and records that name as {@code legacy-world-name}. Iris creates and registers + * its worlds under the Paper startup name ({@code __}), so the name Multiverse + * sees at import is already the name it will see again after every restart, and it is the name every + * Iris unregistration uses. + *

+ * Every entry point here degrades instead of throwing when Multiverse is absent, not yet loaded, or + * has moved the internals this link reflects into. A server without Multiverse must behave exactly as + * it did before this link existed. + */ +public class MultiverseCoreLink { + public static final String MULTIVERSE_PLUGIN = "Multiverse-Core"; + private static final String GENERATOR_PREFIX = "Iris:"; + /** + * Reflection into Multiverse internals is warned about once. A server that keeps logging the same + * broken accessor for every world tells the admin nothing the first line did not. + */ + private static volatile boolean worldConfigUnwritable; + + /** + * Best-effort scrub for worlds Iris never registered (studio worlds). Multiverse not knowing the + * world is the normal case here, so a miss is not worth a warning. + */ + public boolean removeIfPresent(World world) { + String worldName = Objects.requireNonNull(world, "world").getName(); + WorldManager manager = worldManager(); + if (manager == null) { + return false; + } + try { + MultiverseWorld multiverseWorld = resolve(manager, worldName); + return multiverseWorld != null && remove(manager, multiverseWorld, worldName); + } catch (Throwable failure) { + // Studio teardown is not allowed to fail over a Multiverse bookkeeping entry. + IrisLogging.warn("Multiverse refused to drop \"%s\": %s", worldName, describe(failure)); + return false; + } + } + + /** + * Authoritative unregistration for a world Iris registered. A miss leaves a ghost entry behind in + * Multiverse's worlds.yml, so it is reported. A refusal from Multiverse itself is raised, because + * the caller is a removal the admin asked for and a silent success would delete the world files + * while Multiverse kept pointing at them. + */ + public boolean removeFromConfig(String configuredWorldName) { + WorldManager manager = worldManager(); + if (manager == null) { + IrisLogging.debug("Multiverse is not available; skipped unregistering \"" + + configuredWorldName + "\"."); + return false; + } + String worldName = requireWorldName(configuredWorldName); + MultiverseWorld multiverseWorld; + try { + multiverseWorld = resolve(manager, worldName); + } catch (Throwable failure) { + // Only a refusal Multiverse states is worth stopping a removal for; an unusable link is not. + IrisLogging.warn("Could not look %s up in Multiverse: %s; its worlds.yml entry was left as-is.", + worldName, describe(failure)); + return false; + } + if (multiverseWorld == null) { + IrisLogging.warn("Multiverse has no world registered as %s; its worlds.yml entry was left as-is.", + worldName); + return false; + } + return remove(manager, multiverseWorld, worldName); + } + + /** + * Registers a freshly created world with Multiverse. Never throws: a Multiverse failure here used + * to unwind {@code IrisCreator}, whose rollback deletes the world storage that was just built. + */ + public void updateWorld(World bukkitWorld, String configuredWorldName, String pack) { + try { + World world = Objects.requireNonNull(bukkitWorld, "bukkitWorld"); + String worldName = requireWorldName(configuredWorldName); + WorldManager manager = worldManager(); + if (manager == null) { + return; + } + if (!worldName.equals(world.getName())) { + // Multiverse records the live name as legacy-world-name. A live name that is not the + // startup name makes it re-import the world next boot and collide with its own config key. + IrisLogging.info("World %s is live as %s; Multiverse will record the live name.", + worldName, world.getName()); + } + String generator = GENERATOR_PREFIX + Objects.requireNonNull(pack, "pack"); + MultiverseWorld multiverseWorld = manager.getWorld(world) + .orElse(() -> manager.getWorld(worldName)) + .getOrElse(() -> { + // Import through the live world so Multiverse binds its own config key to the + // key Paper gave the world and records the startup name it was created under. + ImportWorldOptions options = ImportWorldOptions.worldName(world.getName()) + .generator(generator) + .environment(world.getEnvironment()) + .useSpawnAdjust(false); + return manager.importWorld(options).get(); + }); + + applyIrisWorldSettings(multiverseWorld, generator); + manager.saveWorldsConfig().get(); + } catch (Throwable failure) { + IrisLogging.warn("Multiverse registration failed for %s: %s; the world is unaffected.", + String.valueOf(configuredWorldName), describe(failure)); + } + } + + /** + * Re-imposes Iris' intended Multiverse state on every world Iris owns. + *

+ * Iris registers worlds it creates, but worlds reconciled out of bukkit.yml at boot are registered + * by Multiverse's own auto-import instead, which defaults {@code auto-load} and {@code adjust-spawn} + * on. Auto-load makes Multiverse try to load an Iris world every boot and log a SEVERE when Paper + * has already loaded it, and adjust-spawn hands Multiverse the authority to relocate and persist a + * spawn Iris chose. + *

+ * The stored environment is corrected here too. Multiverse refuses to adopt a world whose live + * environment differs from the one it stored, and an Iris world's live environment is not stable across a + * restart: it is whatever the pack declares in the session the world was created or loaded in, and + * {@code CUSTOM} once the server enumerates the level from {@code /dimensions/iris/} on + * the next boot. A runtime {@code /mv load} therefore leaves Multiverse holding the pack's environment, + * and this pass is what moves it back to the live one before adoption is attempted. + *

+ * This is also where a world Iris already loaded is offered to Multiverse, which is what makes the + * running world visible to every Multiverse command that needs a loaded one. + * + * @return the number of worlds whose Multiverse entry was corrected; adoption is reported separately + * because it changes Multiverse's registry rather than the world's entry + */ + public int reconcileOwnedWorlds() { + WorldManager manager = worldManager(); + if (manager == null) { + return 0; + } + Map owned; + String levelName; + try { + levelName = IrisWorldStorage.levelRoot().getName(); + owned = IrisWorlds.get().getWorlds(); + } catch (Throwable failure) { + IrisLogging.warn("Could not read the Iris world registry for Multiverse reconciliation: %s", + describe(failure)); + return 0; + } + + int corrected = 0; + int adopted = 0; + for (Map.Entry entry : owned.entrySet()) { + NamespacedKey worldKey; + String worldName; + try { + worldKey = WorldIdentity.parse(entry.getKey()); + worldName = IrisWorldStorage.configuredWorldName(worldKey, levelName); + } catch (Throwable ignored) { + // Not a world with a Bukkit startup name; Multiverse cannot be holding an entry for it. + continue; + } + try { + MultiverseWorld multiverseWorld = resolve(manager, worldName, levelName); + if (multiverseWorld == null) { + continue; + } + warnOnStaleRecordedName(multiverseWorld, worldName); + boolean changed = applyIrisWorldEnvironment( + multiverseWorld, + ownedWorldEnvironment(worldKey, entry.getValue())); + changed = applyIrisWorldSettings(multiverseWorld, GENERATOR_PREFIX + entry.getValue()) || changed; + if (changed) { + corrected++; + } + if (adoptLoadedWorld(manager, multiverseWorld, worldKey)) { + adopted++; + } + } catch (Throwable failure) { + IrisLogging.warn("Could not correct the Multiverse entry for %s: %s", + worldName, describe(failure)); + } + } + if (corrected > 0) { + try { + manager.saveWorldsConfig().get(); + } catch (Throwable failure) { + IrisLogging.warn("Could not save worlds.yml after Multiverse reconciliation: %s", + describe(failure)); + } + IrisLogging.info("Corrected %d Multiverse world entr%s.", corrected, corrected == 1 ? "y" : "ies"); + } + if (adopted > 0) { + IrisLogging.notice("Adopted %d live Iris world%s into Multiverse.", adopted, adopted == 1 ? "" : "s"); + } + return corrected; + } + + /** + * Points Multiverse's stored environment at the one Iris is about to create the world with, so the + * adoption Multiverse runs from its own {@code WorldLoadEvent} handler does not fail on the mismatch. + *

+ * Multiverse compares the stored environment to the live world's before it will take a world, and it + * rewrites the stored value from the live world every time it binds one. An Iris world reports the + * environment its pack declares in the session it was created and {@code CUSTOM} on every later boot, so + * the stored value is only ever right for one of the two - and it is this call, plus the boot-time pass + * in {@link #reconcileOwnedWorlds()}, that moves it between them. + */ + public void prepareOwnedWorldLoad(String configuredWorldName) { + try { + WorldManager manager = worldManager(); + if (manager == null) { + return; + } + String worldName = requireWorldName(configuredWorldName); + String levelName = IrisWorldStorage.levelRoot().getName(); + NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(worldName, levelName); + MultiverseWorld multiverseWorld = resolve(manager, worldName, levelName); + if (multiverseWorld == null) { + return; + } + if (applyIrisWorldEnvironment( + multiverseWorld, + ownedWorldEnvironment(worldKey, ownedWorldPack(worldKey)))) { + manager.saveWorldsConfig().get(); + } + } catch (Throwable failure) { + IrisLogging.warn("Could not prepare the Multiverse entry for %s: %s", + String.valueOf(configuredWorldName), describe(failure)); + } + } + + /** + * Offers a world Iris has just loaded to Multiverse, importing it when Multiverse has no entry at all. + * + * @return true when Multiverse now tracks the world as loaded + */ + public boolean adoptOwnedWorld(String configuredWorldName) { + try { + WorldManager manager = worldManager(); + if (manager == null) { + return false; + } + String worldName = requireWorldName(configuredWorldName); + String levelName = IrisWorldStorage.levelRoot().getName(); + NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(worldName, levelName); + World live = WorldIdentity.resolve(worldKey).orElse(null); + if (live == null) { + return false; + } + String pack = ownedWorldPack(worldKey); + MultiverseWorld multiverseWorld = resolve(manager, worldName, levelName); + if (multiverseWorld == null) { + if (pack == null) { + return false; + } + updateWorld(live, worldName, pack); + return true; + } + boolean changed = applyIrisWorldEnvironment(multiverseWorld, live.getEnvironment()); + changed = applyIrisWorldSettings(multiverseWorld, pack == null ? null : GENERATOR_PREFIX + pack) + || changed; + boolean adopted = adoptLoadedWorld(manager, multiverseWorld, worldKey); + if (changed || adopted) { + manager.saveWorldsConfig().get(); + } + return adopted; + } catch (Throwable failure) { + IrisLogging.warn("Could not hand %s to Multiverse: %s", + String.valueOf(configuredWorldName), describe(failure)); + return false; + } + } + + private static String ownedWorldPack(NamespacedKey worldKey) { + try { + return IrisWorlds.get().getWorlds().get(worldKey.toString()); + } catch (Throwable unreadable) { + IrisLogging.debug("Could not read the Iris world registry for " + worldKey + ": " + + describe(unreadable)); + return null; + } + } + + /** + * Binds a live Iris world into Multiverse's loaded registry. + *

+ * Iris turns {@code auto-load} off and loads its worlds out of bukkit.yml before Multiverse enables, so + * Multiverse never adopts them: {@code mv list} calls a running world UNLOADED and every command that + * needs a loaded world - {@code mv info}, {@code mv gamerule} - refuses it. + *

+ * Multiverse's {@code loadWorld} short-circuits to {@code Bukkit.getWorld()} when that + * world already exists and binds it without going near a WorldCreator. That short circuit is the whole + * safety property here: without a live world under the name Multiverse recorded, the same call would + * build one over Iris storage instead, so the world is looked up and identified first and adoption is + * skipped whenever it is not the target. + * + * @return true when Multiverse took the world + */ + boolean adoptLoadedWorld(WorldManager manager, MultiverseWorld multiverseWorld, NamespacedKey worldKey) { + if (manager.isLoadedWorld(multiverseWorld)) { + return false; + } + World live = Bukkit.getWorld(multiverseWorld.getName()); + if (live == null || !worldKey.equals(WorldIdentity.key(live))) { + return false; + } + Attempt adoption = + manager.loadWorld(LoadWorldOptions.world(multiverseWorld)); + if (adoption.isFailure()) { + IrisLogging.warn("Multiverse would not adopt live world %s: %s", + multiverseWorld.getName(), describeFailure(adoption)); + return false; + } + return true; + } + + /** + * The Bukkit environment an Iris-owned world is or will be live with, or null when it cannot be + * determined. + *

+ * A live world already carries it, and it is the value Multiverse compares its own against before it will + * adopt the world, so it wins whenever there is one. With no live world the pack's dimension answers it - + * the same {@code BukkitEnvironment.from(dimension.getEnvironment())} Iris' own load path passes to the + * WorldCreator - so a world about to be loaded gets the environment it is about to have, not a guess at + * what Multiverse wants to hear. + */ + World.Environment ownedWorldEnvironment(NamespacedKey worldKey, String pack) { + World live = WorldIdentity.resolve(worldKey).orElse(null); + if (live != null) { + return live.getEnvironment(); + } + try { + IrisDimension dimension = IrisWorlds.get().getDimension(worldKey.toString(), pack); + return dimension == null ? null : BukkitEnvironment.from(dimension.getEnvironment()); + } catch (Throwable unreadable) { + IrisLogging.debug("Could not read the Iris dimension for " + worldKey + ": " + describe(unreadable)); + return null; + } + } + + /** + * Corrects the environment Multiverse stored for an Iris world. + *

+ * Multiverse compares the stored value to the live world's and refuses to adopt the world when they + * differ, and it rewrites the stored value from the live world every time it binds one. An Iris world's + * live environment flips between the pack's own and {@code CUSTOM} across a restart, so this is the only + * thing that keeps the two in step; without it {@code mv list} reports a running world as unloaded and + * every command that needs a loaded one refuses it. + * + * @return true when something was changed + */ + boolean applyIrisWorldEnvironment(MultiverseWorld multiverseWorld, World.Environment environment) { + if (environment == null || environment == multiverseWorld.getEnvironment()) { + return false; + } + return setWorldConfigValue(multiverseWorld, "setEnvironment", World.Environment.class, environment); + } + + /** + * Re-imposes Iris ownership on one Multiverse entry. Iris drives loading through bukkit.yml and owns + * the spawn, so Multiverse must neither load the world nor move its spawn. + * + * @return true when something was changed + */ + boolean applyIrisWorldSettings(MultiverseWorld multiverseWorld, String generator) { + boolean changed = false; + if (multiverseWorld.isAutoLoad()) { + multiverseWorld.setAutoLoad(false); + changed = true; + } + if (multiverseWorld.getAdjustSpawn()) { + multiverseWorld.setAdjustSpawn(false); + changed = true; + } + if (generator != null && !generator.equals(multiverseWorld.getGenerator())) { + changed = setWorldConfigValue(multiverseWorld, "setGenerator", String.class, generator) || changed; + } + return changed; + } + + /** + * Multiverse tracks the world name it saw at import and indexes its store by it. Iris cannot rewrite + * that name without desynchronising Multiverse's own name index, so a mismatch is reported instead. + */ + void warnOnStaleRecordedName(MultiverseWorld multiverseWorld, String configuredWorldName) { + String recorded = multiverseWorld.getName(); + if (configuredWorldName.equals(recorded)) { + return; + } + IrisLogging.warn("Multiverse records %s as %s; remove that worlds.yml entry so Iris can re-register it.", + configuredWorldName, recorded); + } + + /** + * Reports whether a Bukkit world name names a persistent world Iris owns on disk. Never throws, so a + * name Iris has no opinion about simply answers false. + */ + public static boolean isIrisOwnedWorldName(String worldName, File levelRoot) { + if (worldName == null || levelRoot == null) { + return false; + } + String levelName = levelRoot.getName(); + NamespacedKey key; + try { + key = IrisWorldStorage.managedKeyFromName(worldName.trim(), levelName); + } catch (RuntimeException notAnIrisName) { + return false; + } + return IrisWorldStorage.isExistingManagedDimensionRoot(levelRoot, key); + } + + private boolean remove(WorldManager manager, MultiverseWorld multiverseWorld, String worldName) { + Attempt removal = manager.removeWorld(removalOptions(multiverseWorld)); + if (removal.isFailure()) { + throw new IllegalStateException("Multiverse refused to remove world \"" + worldName + "\": " + + describeFailure(removal)); + } + manager.saveWorldsConfig().get(); + return true; + } + + /** + * Removal options for one Multiverse world. + *

+ * Iris evacuates, unloads and closes a world before it unregisters it, but Multiverse still holds it in + * its loaded registry and its unload-before-remove dereferences the Bukkit world that is already gone, + * failing the whole removal. Multiverse only owns the unload while the Bukkit world is genuinely live. + */ + static RemoveWorldOptions removalOptions(MultiverseWorld multiverseWorld) { + RemoveWorldOptions options = RemoveWorldOptions.world(multiverseWorld); + if (hasLiveBukkitWorld(multiverseWorld)) { + return options; + } + return options.unloadBukkitWorld(false).saveBukkitWorld(false); + } + + private static boolean hasLiveBukkitWorld(MultiverseWorld multiverseWorld) { + LoadedMultiverseWorld loaded = multiverseWorld.asLoadedWorld().getOrNull(); + return loaded != null && loaded.getBukkitWorld().getOrNull() != null; + } + + MultiverseWorld resolve(WorldManager manager, String worldName) { + return resolve(manager, worldName, IrisWorldStorage.levelRoot().getName()); + } + + /** + * The Multiverse entry for one Iris world, under whichever of its names Multiverse currently holds. + *

+ * A name other than the one asked for only identifies the same world when Multiverse keys its entry by + * the Iris world key, so an entry sitting on the runtime name under some other key is never returned: + * a removal must not delete a world that is not its target. + */ + MultiverseWorld resolve(WorldManager manager, String worldName, String levelName) { + NamespacedKey expectedKey = managedKeyOrNull(worldName, levelName); + for (String candidate : lookupNames(worldName, levelName)) { + MultiverseWorld multiverseWorld = manager.getWorld(candidate).getOrElse((MultiverseWorld) null); + if (multiverseWorld == null) { + continue; + } + if (candidate.equals(worldName) || expectedKey != null && expectedKey.equals(multiverseWorld.getKey())) { + return multiverseWorld; + } + } + return null; + } + + /** + * Every name one Iris world can be registered under in Multiverse: the Bukkit startup name Iris + * registers, the Multiverse config key, and the {@code _} runtime name a world gets + * when Multiverse re-creates it from a keyed WorldCreator. Without all three an entry Multiverse + * reloaded under a different form can never be removed. + */ + static List lookupNames(String worldName, String levelName) { + List candidates = new ArrayList<>(3); + candidates.add(worldName); + NamespacedKey key = managedKeyOrNull(worldName, levelName); + if (key != null && IrisWorldStorage.configuredWorldName(key, levelName).equals(worldName)) { + candidates.add(key.toString()); + candidates.add(key.getNamespace() + "_" + key.getKey()); + } + return List.copyOf(candidates); + } + + private static NamespacedKey managedKeyOrNull(String worldName, String levelName) { + try { + return IrisWorldStorage.managedKeyFromName(worldName, levelName); + } catch (IllegalArgumentException notAnIrisName) { + // Not an Iris startup name; the plain name is the only identity Multiverse can have. + return null; + } + } + + /** + * {@code WorldConfig} and its setters are package-private in Multiverse, so its stored values can only + * be corrected reflectively. A Multiverse release that renames either must degrade to leaving the value + * alone rather than taking down whatever asked for the write. + */ + private static boolean setWorldConfigValue( + MultiverseWorld world, + String setter, + Class parameterType, + Object value + ) { + if (worldConfigUnwritable) { + return false; + } + try { + Field field = MultiverseWorld.class.getDeclaredField("worldConfig"); + field.setAccessible(true); + + Object config = field.get(world); + Method method = config.getClass().getDeclaredMethod(setter, parameterType); + method.setAccessible(true); + method.invoke(config, value); + return true; + } catch (ReflectiveOperationException | RuntimeException | Error failure) { + worldConfigUnwritable = true; + // A probe on a private Multiverse field. It is latched, the fallback is Multiverse's own + // handling of the setting, and nothing an operator can do changes the answer. + IrisLogging.info("Multiverse WorldConfig.%s is not reachable; Iris world settings are left to Multiverse.", + setter); + return false; + } + } + + /** + * A Multiverse failure reason as text. {@code getFailureMessage} is a Multiverse {@code Message}, whose + * rendering goes through a locale manager that is not installed in every teardown order, so the reason + * name is the fallback rather than an object identity string. + */ + private static String describeFailure(Attempt attempt) { + Message message = attempt.getFailureMessage(); + if (message != null) { + try { + String formatted = message.formatted(); + if (formatted != null && !formatted.isBlank()) { + return formatted; + } + } catch (RuntimeException | Error unrenderable) { + // Fall through to the reason name. + } + } + FailureReason reason = attempt.getFailureReason(); + return reason == null ? "no reason given" : reason.toString(); + } + + private static String requireWorldName(String worldName) { + String name = Objects.requireNonNull(worldName, "worldName").trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException("World name cannot be empty."); + } + return name; + } + + private static String describe(Throwable failure) { + String message = failure.getMessage(); + return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message; + } + + /** + * The world manager, or null when Multiverse cannot serve one. Plugin enablement alone is not enough: + * the API singleton is installed during Multiverse's own enable, and Iris is asked for generators + * before that finishes. + */ + WorldManager worldManager() { + if (!isActive()) { + return null; + } + try { + MultiverseCoreApi api = MultiverseCoreApi.get(); + return api.getWorldManager(); + } catch (RuntimeException | Error failure) { + IrisLogging.debug("Multiverse world manager is unavailable: " + describe(failure)); + return null; + } + } + + public boolean isActive() { + if (!Bukkit.getPluginManager().isPluginEnabled(MULTIVERSE_PLUGIN)) { + return false; + } + try { + return MultiverseCoreApi.isLoaded(); + } catch (Error missingApi) { + // Multiverse older than 5.1 has no readiness probe; enablement is the only signal there is. + return true; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/link/MultiverseGuardListener.java b/core/src/main/java/art/arcane/iris/core/link/MultiverseGuardListener.java new file mode 100644 index 000000000..a77a1851f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/MultiverseGuardListener.java @@ -0,0 +1,487 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.link; + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.lifecycle.ManagedWorldLoader; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.server.ServerCommandEvent; +import org.mvplugins.multiverse.core.event.world.MVWorldDeleteEvent; +import org.mvplugins.multiverse.core.event.world.MVWorldImportedEvent; +import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; +import org.mvplugins.multiverse.core.world.MultiverseWorld; +import org.mvplugins.multiverse.core.world.WorldManager; + +import java.lang.ref.WeakReference; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.CompletionException; + +/** + * Keeps Multiverse's destructive world commands away from Iris storage. + *

+ * An Iris world folder is not a vanilla world folder: {@code /dimensions/iris/} also holds + * {@code iris/pack}, the frozen pack snapshot the world is generated from, and {@code iris/engine-data}. + * Multiverse deletes a world by recursively deleting its Bukkit world folder, and its {@code --keep-files} + * only matches file basenames, so a pack directory can never survive. The delete leaves the world, the + * pack and the Multiverse entry gone while bukkit.yml and Iris' worlds.json still point at them, and a + * half-deleted tree reads back as an owned-but-broken world. + *

+ * This listener is only registered while Multiverse is enabled. Every handler here names a Multiverse + * type, so registering it without Multiverse on the classpath would fail at handler resolution. + */ +public final class MultiverseGuardListener implements Listener { + private static final String IRIS_REMOVE_HINT = "Use /iris remove world=%s delete=true instead."; + private static final String MULTIVERSE_LOAD_PERMISSION = "multiverse.core.load"; + private final MultiverseCoreLink link; + /** + * Multiverse world events carry no command sender, and its own cancellation message is the generic + * failure string, so the sender who typed the command has to be remembered across the dispatch. The + * capture is consumed on first read so a later API-driven delete cannot message an unrelated player. + */ + private volatile WeakReference multiverseCommandSender; + + public MultiverseGuardListener(MultiverseCoreLink link) { + this.link = Objects.requireNonNull(link, "link"); + } + + /** + * Cancels {@code mv delete} and {@code mv regen} for Iris worlds. + *

+ * Both commands funnel through {@code WorldManager#doDeleteWorld}, which fires this event after + * resolving the world folder but before it unloads the world or touches disk, so a cancel here is + * free of side effects. Regen is refused with delete because the event cannot tell them apart, and + * because a regen Iris did not drive would have Multiverse re-create the world through its own + * {@code CreateWorldOptions} rather than through Iris storage. + */ + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onMultiverseWorldDelete(MVWorldDeleteEvent event) { + MultiverseWorld multiverseWorld = event.getWorld(); + String worldName = multiverseWorld.getName(); + if (!isIrisWorld(multiverseWorld)) { + return; + } + event.setCancelled(true); + String irisName = irisCommandName(multiverseWorld, worldName); + refuse(consumeCommandSender(), + "Multiverse cannot delete or regenerate Iris world " + worldName + + "; it would destroy the world-local pack snapshot.", + String.format(IRIS_REMOVE_HINT, irisName)); + } + + /** + * Multiverse auto-imports every world it finds that is not already in worlds.yml, using its own + * defaults, so an Iris world reconciled out of bukkit.yml arrives with {@code auto-load} and + * {@code adjust-spawn} on. Iris drives its own loading and owns the spawn, so both are turned back off. + */ + @EventHandler(priority = EventPriority.MONITOR) + public void onMultiverseWorldImported(MVWorldImportedEvent event) { + MultiverseWorld multiverseWorld = event.getWorld(); + if (!isIrisWorld(multiverseWorld)) { + return; + } + try { + if (!link.applyIrisWorldSettings(multiverseWorld, null)) { + return; + } + WorldManager manager = link.worldManager(); + if (manager != null) { + manager.saveWorldsConfig().get(); + } + IrisLogging.debug("Reset Multiverse auto-load and adjust-spawn for " + multiverseWorld.getName()); + } catch (Throwable failure) { + IrisLogging.warn("Could not apply Iris settings to the Multiverse import of %s: %s", + multiverseWorld.getName(), failure.getClass().getSimpleName()); + } + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onPlayerCommand(PlayerCommandPreprocessEvent event) { + if (inspectCommand(event.getPlayer(), event.getMessage())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onServerCommand(ServerCommandEvent event) { + if (inspectCommand(event.getSender(), event.getCommand())) { + event.setCancelled(true); + } + } + + /** + * Records the sender of a Multiverse command and vetoes a clone that reads from, or writes over, Iris + * storage. + *

+ * Multiverse fires no cancellable event for a clone: it copies the source world folder first and only + * then imports the copy, and the import of an Iris copy always fails. That leaves an orphaned copy of + * the world - frozen pack included - under {@code /dimensions/minecraft/}, which + * Multiverse then advertises as importable, so a later plain {@code mv import} would build a vanilla + * world on top of Iris region files. The command is the only place left to stop it. + *

+ * The destination is checked as well. Multiverse decides a name is free with its own + * {@code hasWorldFolder} check, which resolves the vanilla {@code /} path and never + * looks in the Iris dimension namespace, so a clone can be aimed straight at a live Iris world's slot. + * + * @return true when the command must be cancelled + */ + private boolean inspectCommand(CommandSender sender, String commandLine) { + if (multiverseCommandRoot(commandLine) == null) { + return false; + } + multiverseCommandSender = new WeakReference<>(sender); + String load = multiverseLoad(commandLine); + if (load != null) { + return interceptLoad(sender, load); + } + CloneCommand clone = multiverseClone(commandLine); + if (clone == null) { + return false; + } + if (isIrisWorld(clone.source())) { + refuse(sender, + "Multiverse cannot clone Iris world " + clone.source() + + "; its pack snapshot is world-local and the copy is not a loadable world.", + "Create another Iris world with /iris create type= instead."); + return true; + } + if (clone.destination() != null && isIrisWorld(clone.destination())) { + refuse(sender, + "Multiverse cannot clone onto Iris world " + clone.destination() + + "; that name already owns Iris world storage.", + "Choose another destination name, or free it with /iris remove world=" + + clone.destination() + " delete=true."); + return true; + } + return false; + } + + /** + * Takes over {@code /mv load} for an Iris world Iris can load itself. + *

+ * Multiverse only adopts a world it finds already in Bukkit; otherwise it rebuilds one from a + * WorldCreator carrying the environment it stored. Neither half of that works for an Iris world. The + * stored environment is written from the live world every time Multiverse binds it, and a custom + * dimension reports {@code NORMAL} in the session it was created and {@code CUSTOM} on every later boot, + * so {@code CraftServer.createWorld} refuses it with "Illegal dimension (CUSTOM)". Writing {@code normal} + * instead only trades that for the other failure: Multiverse would build a vanilla world beside the level + * root under the Iris world's name, and the next boot's adoption would fail on the mismatch. + *

+ * So the load is handed to Iris, which creates the world from a keyed creator at + * {@code /dimensions/iris/} with the pack's own environment, and the result is offered + * back to Multiverse. With no loader installed the command is refused rather than passed through, because + * passing it through is the failure this exists to prevent. + * + * @return true when the command must be cancelled + */ + private boolean interceptLoad(CommandSender sender, String worldNameOrAlias) { + String worldName = irisWorldName(worldNameOrAlias); + if (worldName == null || Bukkit.getWorld(worldName) != null) { + // A live world is adopted by Multiverse without a WorldCreator; that path already works. + return false; + } + if (sender != null && !sender.hasPermission(MULTIVERSE_LOAD_PERMISSION)) { + // Taking the command over must never load a world for someone Multiverse would have refused. + return false; + } + ManagedWorldLoader loader = IrisServices.getOrNull(ManagedWorldLoader.class); + if (loader == null) { + refuse(sender, + "Iris owns loading for " + worldName + "; Multiverse would build a vanilla world in its place.", + "Load it with /iris load " + worldName + ", or restart the server."); + return true; + } + link.prepareOwnedWorldLoad(worldName); + IrisLogging.info("Multiverse load of %s handed to Iris.", worldName); + WeakReference target = new WeakReference<>(sender); + try { + // The load settles off the main thread, and handing the result to Multiverse fires a Bukkit + // event, so the whole tail runs back on the main thread exactly as /iris load's does. + loader.load(worldName).whenComplete((result, failure) -> + J.s(() -> settleLoad(target.get(), worldName, result, failure))); + } catch (Throwable failure) { + refuse(sender, "Iris could not load " + worldName + ": " + describe(failure), + "Load it with /iris load " + worldName + ", or restart the server."); + } + return true; + } + + void settleLoad( + CommandSender sender, + String worldName, + ManagedWorldLoader.ManagedWorldLoad result, + Throwable failure + ) { + if (failure != null || result == null || !result.loaded()) { + String detail = failure != null + ? describe(failure) + : result == null ? "the load completed without a result" : result.message(); + refuse(sender, "Iris could not load " + worldName + ": " + detail, + "Check the Iris log, then retry with /iris load " + worldName + "."); + return; + } + boolean adopted = link.adoptOwnedWorld(worldName); + IrisLogging.info("Iris loaded %s; multiverse adopted=%s.", worldName, adopted); + announce(sender, "Iris loaded " + worldName + "."); + } + + /** + * The Bukkit startup name of the Iris world a Multiverse world argument names, or null when it names no + * Iris world. Multiverse accepts its own alias, so the argument is resolved through Multiverse first and + * the name Multiverse recorded is what Iris is asked for. + */ + private String irisWorldName(String worldNameOrAlias) { + if (isIrisOwnedName(worldNameOrAlias)) { + return startupName(worldNameOrAlias); + } + WorldManager manager = link.worldManager(); + if (manager == null) { + return null; + } + MultiverseWorld multiverseWorld = manager.getWorldByNameOrAlias(worldNameOrAlias) + .getOrElse((MultiverseWorld) null); + if (multiverseWorld == null) { + return null; + } + String recorded = multiverseWorld.getName(); + return isIrisOwnedName(recorded) ? startupName(recorded) : null; + } + + /** + * The Bukkit startup name for an Iris world named in any of the forms Iris answers to - the startup name + * itself, the bare key, or {@code iris:} - so the live-world check and the loader both see the one + * name the world is actually registered under. + */ + private static String startupName(String worldName) { + try { + String levelName = IrisWorldStorage.levelRoot().getName(); + return IrisWorldStorage.configuredWorldName( + IrisWorldStorage.managedKeyFromName(worldName, levelName), + levelName); + } catch (Throwable notAnIrisName) { + return worldName; + } + } + + /** + * The world argument of a Multiverse load command, or null when the line is not one. Syntax is + * {@code /mv load [flags]} with the legacy alias {@code /mvload}. + */ + static String multiverseLoad(String commandLine) { + String root = multiverseCommandRoot(commandLine); + if (root == null) { + return null; + } + String line = commandLine.trim(); + if (line.startsWith("/")) { + line = line.substring(1); + } + String[] parts = line.split("\\s+"); + if ("mv".equals(root)) { + return parts.length >= 3 && "load".equalsIgnoreCase(parts[1]) ? worldArgument(parts, 2) : null; + } + if ("mvload".equals(root)) { + return parts.length >= 2 ? worldArgument(parts, 1) : null; + } + return null; + } + + /** + * The Multiverse command root, or null when the line is not one. Multiverse registers {@code /mv} plus + * per-command legacy aliases; anything Iris does not recognise is left alone, which is the behaviour a + * server without this listener already has. + */ + static String multiverseCommandRoot(String commandLine) { + if (commandLine == null) { + return null; + } + String line = commandLine.trim(); + if (line.startsWith("/")) { + line = line.substring(1); + } + int firstSpace = line.indexOf(' '); + String root = (firstSpace < 0 ? line : line.substring(0, firstSpace)).toLowerCase(Locale.ENGLISH); + // Bukkit always accepts the "plugin:command" form as well. + int namespace = root.lastIndexOf(':'); + if (namespace >= 0) { + root = root.substring(namespace + 1); + } + // Every Multiverse alias starts with "mv". Matching the prefix rather than an enumeration keeps a + // new legacy alias from silently escaping the guard; a false match only stores a sender reference + // that nothing but a Multiverse world event ever reads. + return root.startsWith("mv") ? root : null; + } + + /** + * The source and destination world arguments of a Multiverse clone command, or null when the line is + * not one. Syntax is {@code /mv clone [flags]} with the legacy aliases + * {@code /mvcl} and {@code /mvclone}. The destination is null when the admin has not typed it yet. + */ + static CloneCommand multiverseClone(String commandLine) { + String root = multiverseCommandRoot(commandLine); + if (root == null) { + return null; + } + String line = commandLine.trim(); + if (line.startsWith("/")) { + line = line.substring(1); + } + String[] parts = line.split("\\s+"); + int worldIndex; + if ("mv".equals(root)) { + if (parts.length < 3 || !"clone".equalsIgnoreCase(parts[1])) { + return null; + } + worldIndex = 2; + } else if ("mvcl".equals(root) || "mvclone".equals(root)) { + if (parts.length < 2) { + return null; + } + worldIndex = 1; + } else { + return null; + } + String source = worldArgument(parts, worldIndex); + if (source == null) { + return null; + } + return new CloneCommand(source, worldArgument(parts, worldIndex + 1)); + } + + /** + * A world-name argument, or null when it is absent or a flag. Multiverse quotes names containing + * spaces; the guard only needs the leading token, so the opening quote is dropped and the rest kept. + */ + private static String worldArgument(String[] parts, int index) { + if (index >= parts.length) { + return null; + } + String world = parts[index]; + if (world.startsWith("-")) { + return null; + } + if (world.startsWith("\"")) { + world = world.substring(1); + } + return world.isEmpty() ? null : world; + } + + record CloneCommand(String source, String destination) { + CloneCommand { + Objects.requireNonNull(source, "source"); + } + } + + private boolean isIrisWorld(MultiverseWorld multiverseWorld) { + LoadedMultiverseWorld loaded = multiverseWorld.asLoadedWorld().getOrNull(); + World bukkitWorld = loaded == null ? null : loaded.getBukkitWorld().getOrNull(); + if (bukkitWorld != null && IrisToolbelt.isIrisWorld(bukkitWorld)) { + return true; + } + return isIrisOwnedName(multiverseWorld.getName()); + } + + private boolean isIrisWorld(String worldNameOrAlias) { + World live = Bukkit.getWorld(worldNameOrAlias); + if (live != null && IrisToolbelt.isIrisWorld(live)) { + return true; + } + WorldManager manager = link.worldManager(); + if (manager != null) { + MultiverseWorld multiverseWorld = manager.getWorldByNameOrAlias(worldNameOrAlias) + .getOrElse((MultiverseWorld) null); + if (multiverseWorld != null) { + return isIrisWorld(multiverseWorld); + } + } + return isIrisOwnedName(worldNameOrAlias); + } + + private static boolean isIrisOwnedName(String worldName) { + try { + return MultiverseCoreLink.isIrisOwnedWorldName(worldName, IrisWorldStorage.levelRoot()); + } catch (Throwable unavailable) { + return false; + } + } + + /** + * The name {@code /iris remove} accepts. Multiverse reports the name it recorded at import, which is + * the Bukkit startup name for every world Iris registered. + */ + private static String irisCommandName(MultiverseWorld multiverseWorld, String fallback) { + LoadedMultiverseWorld loaded = multiverseWorld.asLoadedWorld().getOrNull(); + World bukkitWorld = loaded == null ? null : loaded.getBukkitWorld().getOrNull(); + return bukkitWorld == null ? fallback : bukkitWorld.getName(); + } + + private CommandSender consumeCommandSender() { + WeakReference captured = multiverseCommandSender; + multiverseCommandSender = null; + return captured == null ? null : captured.get(); + } + + /** + * Multiverse reports a cancelled destructive command as its generic failure string, so the refusal has + * to reach the sender from here or the admin only sees "something went wrong". + */ + private static void refuse(CommandSender sender, String reason, String remedy) { + // The refusal is the guard working, and the admin who typed the command is told directly. This is + // the console's copy of the same sentence, not a warning about the state of the server. + IrisLogging.info("%s %s", reason, remedy); + announce(sender, reason, remedy); + } + + private static void announce(CommandSender sender, String... lines) { + if (sender == null || sender instanceof ConsoleCommandSender) { + // The console already saw it through the Iris log. + return; + } + try { + VolmitSender target = new VolmitSender(sender); + for (String line : lines) { + target.sendMessage(line); + } + } catch (Throwable ignored) { + for (String line : lines) { + sender.sendMessage(line); + } + } + } + + private static String describe(Throwable failure) { + Throwable cause = failure instanceof CompletionException && failure.getCause() != null + ? failure.getCause() + : failure; + String message = cause.getMessage(); + return message == null || message.isBlank() ? cause.getClass().getSimpleName() : message; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/link/WorldEditLink.java b/core/src/main/java/art/arcane/iris/core/link/WorldEditLink.java new file mode 100644 index 000000000..f9c3deee8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/WorldEditLink.java @@ -0,0 +1,84 @@ +package art.arcane.iris.core.link; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.volmlib.util.data.Cuboid; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.lang.reflect.InvocationTargetException; +import java.util.function.BooleanSupplier; + +public class WorldEditLink { + private static final AtomicCache active = new AtomicCache<>(); + private static final ChronoLatch errorThrottle = new ChronoLatch(60_000); + + public static Cuboid getSelection(Player p) { + if (!hasWorldEdit()) + return null; + + try { + Object instance = Class.forName("com.sk89q.worldedit.WorldEdit").getDeclaredMethod("getInstance").invoke(null); + Object sessionManager = instance.getClass().getDeclaredMethod("getSessionManager").invoke(instance); + Class bukkitAdapter = Class.forName("com.sk89q.worldedit.bukkit.BukkitAdapter"); + Object world = bukkitAdapter.getDeclaredMethod("adapt", World.class).invoke(null, p.getWorld()); + Object player = bukkitAdapter.getDeclaredMethod("adapt", Player.class).invoke(null, p); + Object localSession = sessionManager.getClass().getDeclaredMethod("getIfPresent", Class.forName("com.sk89q.worldedit.session.SessionOwner")).invoke(sessionManager, player); + if (localSession == null) return null; + + Object region = null; + try { + region = localSession.getClass().getDeclaredMethod("getSelection", Class.forName("com.sk89q.worldedit.world.World")).invoke(localSession, world); + } catch (InvocationTargetException ignored) {} + if (region == null) return null; + + Object min = region.getClass().getDeclaredMethod("getMinimumPoint").invoke(region); + Object max = region.getClass().getDeclaredMethod("getMaximumPoint").invoke(region); + return new Cuboid(p.getWorld(), + (int) min.getClass().getDeclaredMethod("x").invoke(min), + (int) min.getClass().getDeclaredMethod("y").invoke(min), + (int) min.getClass().getDeclaredMethod("z").invoke(min), + (int) min.getClass().getDeclaredMethod("x").invoke(max), + (int) min.getClass().getDeclaredMethod("y").invoke(max), + (int) min.getClass().getDeclaredMethod("z").invoke(max) + ); + } catch (Throwable e) { + if (errorThrottle.flip()) { + IrisLogging.error("Could not get selection"); + e.printStackTrace(); + IrisLogging.reportError(e); + } + invalidate(); + } + return null; + } + + public static boolean hasWorldEdit() { + return hasWorldEdit(() -> Bukkit.getPluginManager().isPluginEnabled("WorldEdit")); + } + + static boolean hasWorldEdit(BooleanSupplier detector) { + if (Boolean.TRUE.equals(active.getIfPresent())) { + return true; + } + + boolean present; + try { + present = detector.getAsBoolean(); + } catch (Throwable ignored) { + return false; + } + + if (present) { + active.aquire(() -> Boolean.TRUE); + } + + return present; + } + + static void invalidate() { + active.reset(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/link/data/CraftEngineDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/CraftEngineDataProvider.java new file mode 100644 index 000000000..4882e9866 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/data/CraftEngineDataProvider.java @@ -0,0 +1,186 @@ +package art.arcane.iris.core.link.data; + +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.common.data.IrisCustomData; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.math.RNG; +import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks; +import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture; +import net.momirealms.craftengine.bukkit.api.CraftEngineItems; +import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition; +import net.momirealms.craftengine.core.block.BlockDefinition; +import net.momirealms.craftengine.core.block.ImmutableBlockState; +import net.momirealms.craftengine.core.block.property.BooleanProperty; +import net.momirealms.craftengine.core.block.property.IntegerProperty; +import net.momirealms.craftengine.core.block.property.Property; +import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition; +import net.momirealms.craftengine.core.util.Key; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.function.Function; +import java.util.stream.Stream; + +public class CraftEngineDataProvider extends ExternalDataProvider { + private static final BlockProperty[] FURNITURE_PROPERTIES = new BlockProperty[]{ + BlockProperty.ofBoolean("randomYaw", false), + BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true), + BlockProperty.ofBoolean("randomPitch", false), + BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true), + }; + + public CraftEngineDataProvider() { + super("CraftEngine"); + } + + @Override + public void init() { + } + + @Override + public @NotNull List getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException { + Key key = Key.of(blockId.namespace(), blockId.key()); + BlockDefinition block = CraftEngineBlocks.byId(key); + if (block != null) { + return block.properties().stream().map(CraftEngineDataProvider::convert).toList(); + } + + FurnitureDefinition furniture = CraftEngineFurniture.byId(key); + if (furniture != null) { + BlockProperty[] properties = Arrays.copyOf(FURNITURE_PROPERTIES, 5); + properties[4] = new BlockProperty( + "variant", + String.class, + furniture.anyVariantName(), + furniture.variants().keySet(), + Function.identity() + ); + return List.of(properties); + } + + throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); + } + + @Override + public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) throws MissingResourceException { + BukkitItemDefinition item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key())); + if (item == null) { + throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); + } + + return item.buildBukkitItem(); + } + + @Override + public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap state) throws MissingResourceException { + Key key = Key.of(blockId.namespace(), blockId.key()); + if (CraftEngineBlocks.byId(key) == null && CraftEngineFurniture.byId(key) == null) { + throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); + } + + return IrisCustomData.of(BukkitBlockResolution.getAir(), ExternalDataSVC.buildState(blockId, state)); + } + + @Override + public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) { + art.arcane.iris.core.nms.container.Pair> statePair = ExternalDataSVC.parseState(blockId); + Identifier baseBlockId = statePair.getA(); + KMap state = statePair.getB(); + Key key = Key.of(baseBlockId.namespace(), baseBlockId.key()); + + BlockDefinition customBlock = CraftEngineBlocks.byId(key); + if (customBlock != null) { + ImmutableBlockState blockState = customBlock.defaultState(); + + for (Map.Entry entry : state.entrySet()) { + Property property = customBlock.getProperty(entry.getKey()); + if (property == null) { + continue; + } + + Comparable tag = property.optional(entry.getValue()).orElse(null); + if (tag == null) { + continue; + } + + blockState = ImmutableBlockState.with(blockState, property, tag); + } + + CraftEngineBlocks.place(block.getLocation(), blockState, false); + return; + } + + FurnitureDefinition furniture = CraftEngineFurniture.byId(key); + if (furniture == null) { + return; + } + + Location location = parseYawAndPitch(engine, block, state); + String variant = state.getOrDefault("variant", furniture.anyVariantName()); + CraftEngineFurniture.place(location, furniture, variant, false); + } + + private static Location parseYawAndPitch(@NotNull Engine engine, @NotNull Block block, @NotNull Map state) { + Location location = block.getLocation(); + long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY(); + RNG rng = new RNG(seed); + + if ("true".equals(state.get("randomYaw"))) { + location.setYaw(rng.f(0, 360)); + } else if (state.containsKey("yaw")) { + location.setYaw(Float.parseFloat(state.get("yaw"))); + } + + if ("true".equals(state.get("randomPitch"))) { + location.setPitch(rng.f(0, 360)); + } else if (state.containsKey("pitch")) { + location.setPitch(Float.parseFloat(state.get("pitch"))); + } + + return location; + } + + @Override + public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { + Stream keys = switch (dataType) { + case ENTITY -> Stream.empty(); + case ITEM -> CraftEngineItems.loadedItems().keySet().stream(); + case BLOCK -> Stream.concat(CraftEngineBlocks.loadedBlocks().keySet().stream(), + CraftEngineFurniture.loadedFurniture().keySet().stream()); + }; + return keys.map(key -> new Identifier(key.namespace(), key.value())).toList(); + } + + @Override + public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { + Key key = Key.of(id.namespace(), id.key()); + return switch (dataType) { + case ENTITY -> false; + case ITEM -> CraftEngineItems.byId(key) != null; + case BLOCK -> CraftEngineBlocks.byId(key) != null || CraftEngineFurniture.byId(key) != null; + }; + } + + private static > BlockProperty convert(Property raw) { + return switch (raw) { + case BooleanProperty property -> BlockProperty.ofBoolean(property.name(), property.defaultValue()); + case IntegerProperty property -> BlockProperty.ofLong(property.name(), property.defaultValue(), property.min, property.max, false, false); + default -> new BlockProperty(raw.name(), raw.valueClass(), raw.defaultValue(), raw.possibleValues(), raw::valueName); + }; + } +} diff --git a/core/src/main/java/com/volmit/iris/core/link/data/DataType.java b/core/src/main/java/art/arcane/iris/core/link/data/DataType.java similarity index 86% rename from core/src/main/java/com/volmit/iris/core/link/data/DataType.java rename to core/src/main/java/art/arcane/iris/core/link/data/DataType.java index 65edfc2a3..fc0e90b86 100644 --- a/core/src/main/java/com/volmit/iris/core/link/data/DataType.java +++ b/core/src/main/java/art/arcane/iris/core/link/data/DataType.java @@ -1,7 +1,7 @@ -package com.volmit.iris.core.link.data; +package art.arcane.iris.core.link.data; -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; import java.util.MissingResourceException; import java.util.function.BiPredicate; diff --git a/core/src/main/java/art/arcane/iris/core/link/data/EcoItemsDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/EcoItemsDataProvider.java new file mode 100644 index 000000000..5beb4cbf9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/data/EcoItemsDataProvider.java @@ -0,0 +1,52 @@ +package art.arcane.iris.core.link.data; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.volmlib.util.collection.KMap; +import com.willfp.eco.core.items.CustomItem; +import com.willfp.eco.core.items.Items; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; +import java.util.MissingResourceException; + +public class EcoItemsDataProvider extends ExternalDataProvider { + public EcoItemsDataProvider() { + super("EcoItems"); + } + + @Override + public void init() { + IrisLogging.info("Setting up EcoItems Link..."); + } + + @NotNull + @Override + public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) throws MissingResourceException { + ItemStack item = Items.lookup(itemId.namespace() + ":" + itemId.key()).getItem(); + if (item.getType().isAir()) { + throw new MissingResourceException("Failed to find Item!", itemId.namespace(), itemId.key()); + } + return item; + } + + @Override + public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { + if (dataType != DataType.ITEM) return List.of(); + return Items.getCustomItems() + .stream() + .map(CustomItem::getKey) + .filter(key -> key.getNamespace().equalsIgnoreCase("ecoitems")) + .map(key -> new Identifier(key.getNamespace(), key.getKey())) + .filter(dataType.asPredicate(this)) + .toList(); + } + + @Override + public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { + return id.namespace().equalsIgnoreCase("ecoitems") && dataType == DataType.ITEM; + } +} diff --git a/core/src/main/java/com/volmit/iris/core/link/data/ExecutableItemsDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/ExecutableItemsDataProvider.java similarity index 84% rename from core/src/main/java/com/volmit/iris/core/link/data/ExecutableItemsDataProvider.java rename to core/src/main/java/art/arcane/iris/core/link/data/ExecutableItemsDataProvider.java index 88a2dc943..2264488c5 100644 --- a/core/src/main/java/com/volmit/iris/core/link/data/ExecutableItemsDataProvider.java +++ b/core/src/main/java/art/arcane/iris/core/link/data/ExecutableItemsDataProvider.java @@ -1,10 +1,10 @@ -package com.volmit.iris.core.link.data; +package art.arcane.iris.core.link.data; import com.ssomar.score.api.executableitems.ExecutableItemsAPI; -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.util.collection.KMap; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.volmlib.util.collection.KMap; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -20,7 +20,7 @@ public class ExecutableItemsDataProvider extends ExternalDataProvider { @Override public void init() { - Iris.info("Setting up ExecutableItems Link..."); + IrisLogging.info("Setting up ExecutableItems Link..."); } @NotNull diff --git a/core/src/main/java/com/volmit/iris/core/link/data/ItemAdderDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/ItemAdderDataProvider.java similarity index 83% rename from core/src/main/java/com/volmit/iris/core/link/data/ItemAdderDataProvider.java rename to core/src/main/java/art/arcane/iris/core/link/data/ItemAdderDataProvider.java index 0f4495972..1910670cd 100644 --- a/core/src/main/java/com/volmit/iris/core/link/data/ItemAdderDataProvider.java +++ b/core/src/main/java/art/arcane/iris/core/link/data/ItemAdderDataProvider.java @@ -1,17 +1,18 @@ -package com.volmit.iris.core.link.data; +package art.arcane.iris.core.link.data; -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.IrisCustomData; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.data.IrisCustomData; import dev.lone.itemsadder.api.CustomBlock; import dev.lone.itemsadder.api.CustomStack; import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -21,7 +22,7 @@ import java.util.MissingResourceException; import java.util.Set; import java.util.stream.Collectors; -public class ItemAdderDataProvider extends ExternalDataProvider { +public class ItemAdderDataProvider extends ExternalDataProvider implements Listener { private volatile Set itemNamespaces = Set.of(); private volatile Set blockNamespaces = Set.of(); @@ -88,15 +89,15 @@ public class ItemAdderDataProvider extends ExternalDataProvider { updateNamespaces(DataType.ITEM); updateNamespaces(DataType.BLOCK); } catch (Throwable e) { - Iris.warn("Failed to update ItemAdder namespaces: " + e.getMessage()); + IrisLogging.warn("Failed to update ItemAdder namespaces: " + e.getMessage()); } } private void updateNamespaces(DataType dataType) { - var namespaces = getTypes(dataType).stream().map(Identifier::namespace).collect(Collectors.toSet()); + Set namespaces = getTypes(dataType).stream().map(Identifier::namespace).collect(Collectors.toSet()); if (dataType == DataType.ITEM) itemNamespaces = namespaces; else blockNamespaces = namespaces; - Iris.debug("Updated ItemAdder namespaces: " + dataType + " - " + namespaces); + IrisLogging.debug("Updated ItemAdder namespaces: " + dataType + " - " + namespaces); } @Override diff --git a/core/src/main/java/art/arcane/iris/core/link/data/KGeneratorsDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/KGeneratorsDataProvider.java new file mode 100644 index 000000000..ec4846a25 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/data/KGeneratorsDataProvider.java @@ -0,0 +1,77 @@ +package art.arcane.iris.core.link.data; + +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.data.IrisCustomData; +import me.kryniowesegryderiusz.kgenerators.Main; +import me.kryniowesegryderiusz.kgenerators.api.KGeneratorsAPI; +import me.kryniowesegryderiusz.kgenerators.api.interfaces.IGeneratorLocation; +import me.kryniowesegryderiusz.kgenerators.generators.generator.objects.Generator; +import me.kryniowesegryderiusz.kgenerators.generators.locations.objects.GeneratorLocation; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; +import java.util.MissingResourceException; + +public class KGeneratorsDataProvider extends ExternalDataProvider { + public KGeneratorsDataProvider() { + super("KGenerators"); + } + + @Override + public void init() { + + } + + @Override + public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap state) throws MissingResourceException { + if (Main.getGenerators().get(blockId.key()) == null) throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); + return IrisCustomData.of(Material.STRUCTURE_VOID.createBlockData(), ExternalDataSVC.buildState(blockId, state)); + } + + @Override + public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) throws MissingResourceException { + Generator generator = Main.getGenerators().get(itemId.key()); + if (generator == null) throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); + return generator.getGeneratorItem(); + } + + @Override + public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) { + if (block.getType() != Material.STRUCTURE_VOID) return; + IGeneratorLocation existing = KGeneratorsAPI.getLoadedGeneratorLocation(block.getLocation()); + if (existing != null) return; + block.setBlockData(BukkitBlockResolution.getAir(), false); + Generator generator = Main.getGenerators().get(blockId.key()); + if (generator == null) return; + GeneratorLocation location = new GeneratorLocation(-1, generator, block.getLocation(), Main.getPlacedGenerators().getChunkInfo(block.getChunk()), null, null); + Main.getDatabases().getDb().saveGenerator(location); + Main.getPlacedGenerators().addLoaded(location); + Main.getSchedules().schedule(location, true); + } + + @Override + public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { + if (dataType == DataType.ENTITY) return List.of(); + return Main.getGenerators().getAll().stream() + .map(gen -> new Identifier("kgenerators", gen.getId())) + .filter(dataType.asPredicate(this)) + .toList(); + } + + @Override + public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { + if (dataType == DataType.ENTITY) return false; + return "kgenerators".equalsIgnoreCase(id.namespace()); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/link/data/MMOItemsDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/MMOItemsDataProvider.java similarity index 91% rename from core/src/main/java/com/volmit/iris/core/link/data/MMOItemsDataProvider.java rename to core/src/main/java/art/arcane/iris/core/link/data/MMOItemsDataProvider.java index 0bf3fc490..99f517df3 100644 --- a/core/src/main/java/com/volmit/iris/core/link/data/MMOItemsDataProvider.java +++ b/core/src/main/java/art/arcane/iris/core/link/data/MMOItemsDataProvider.java @@ -1,12 +1,13 @@ -package com.volmit.iris.core.link.data; +package art.arcane.iris.core.link.data; -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.scheduling.J; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.scheduling.J; import net.Indyuce.mmoitems.MMOItems; import net.Indyuce.mmoitems.api.ItemTier; +import net.Indyuce.mmoitems.api.Type; import net.Indyuce.mmoitems.api.block.CustomBlock; import org.bukkit.Bukkit; import org.bukkit.block.data.BlockData; @@ -28,7 +29,7 @@ public class MMOItemsDataProvider extends ExternalDataProvider { @Override public void init() { - Iris.info("Setting up MMOItems Link..."); + IrisLogging.info("Setting up MMOItems Link..."); } @NotNull @@ -52,7 +53,7 @@ public class MMOItemsDataProvider extends ExternalDataProvider { CompletableFuture future = new CompletableFuture<>(); Runnable run = () -> { try { - var type = api().getTypes().get(parts[1]); + Type type = api().getTypes().get(parts[1]); int level = -1; ItemTier tier = null; diff --git a/core/src/main/java/com/volmit/iris/core/link/data/MythicCrucibleDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/MythicCrucibleDataProvider.java similarity index 79% rename from core/src/main/java/com/volmit/iris/core/link/data/MythicCrucibleDataProvider.java rename to core/src/main/java/art/arcane/iris/core/link/data/MythicCrucibleDataProvider.java index b264e144a..4b5317d10 100644 --- a/core/src/main/java/com/volmit/iris/core/link/data/MythicCrucibleDataProvider.java +++ b/core/src/main/java/art/arcane/iris/core/link/data/MythicCrucibleDataProvider.java @@ -16,19 +16,21 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.link.data; +package art.arcane.iris.core.link.data; -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.nms.container.BiomeColor; -import com.volmit.iris.core.nms.container.BlockProperty; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.IrisCustomData; +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.container.BiomeColor; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.data.IrisCustomData; import io.lumine.mythic.bukkit.BukkitAdapter; import io.lumine.mythic.bukkit.utils.serialize.Chroma; import io.lumine.mythiccrucible.MythicCrucible; @@ -37,10 +39,12 @@ import io.lumine.mythiccrucible.items.ItemManager; import io.lumine.mythiccrucible.items.blocks.CustomBlockItemContext; import io.lumine.mythiccrucible.items.furniture.FurnitureItemContext; import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; import org.bukkit.block.data.BlockData; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import java.awt.Color; import java.util.Collection; import java.util.List; import java.util.MissingResourceException; @@ -56,12 +60,8 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider { @Override public void init() { - Iris.info("Setting up MythicCrucible Link..."); - try { - this.itemManager = MythicCrucible.inst().getItemManager(); - } catch (Exception e) { - Iris.error("Failed to set up MythicCrucible Link: Unable to fetch MythicCrucible instance!"); - } + IrisLogging.info("Setting up MythicCrucible Link..."); + itemManager = MythicCrucible.inst().getItemManager(); } @NotNull @@ -72,7 +72,7 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider { CustomBlockItemContext blockItemContext = crucibleItem.getBlockData(); FurnitureItemContext furnitureItemContext = crucibleItem.getFurnitureData(); if (furnitureItemContext != null) { - return IrisCustomData.of(B.getAir(), ExternalDataSVC.buildState(blockId, state)); + return IrisCustomData.of(BukkitBlockResolution.getAir(), ExternalDataSVC.buildState(blockId, state)); } else if (blockItemContext != null) { return blockItemContext.getBlockData(); } @@ -113,8 +113,8 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider { @Override public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) { - var parsedState = ExternalDataSVC.parseState(blockId); - var state = parsedState.getB(); + Pair> parsedState = ExternalDataSVC.parseState(blockId); + KMap state = parsedState.getB(); blockId = parsedState.getA(); Optional item = itemManager.getItem(blockId.key()); @@ -122,14 +122,14 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider { FurnitureItemContext furniture = item.get().getFurnitureData(); if (furniture == null) return; - var pair = parseYawAndFace(engine, block, state); + Pair pair = parseYawAndFace(engine, block, state); BiomeColor type = null; Chroma color = null; try { type = BiomeColor.valueOf(state.get("matchBiome").toUpperCase()); } catch (NullPointerException | IllegalArgumentException ignored) {} if (type != null) { - var biomeColor = INMS.get().getBiomeColor(block.getLocation(), type); + Color biomeColor = INMS.get().getBiomeColor(block.getLocation(), type); if (biomeColor == null) return; color = Chroma.of(biomeColor.getRGB()); } diff --git a/core/src/main/java/art/arcane/iris/core/link/data/MythicMobsDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/MythicMobsDataProvider.java new file mode 100644 index 000000000..7ff2bc134 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/data/MythicMobsDataProvider.java @@ -0,0 +1,141 @@ +package art.arcane.iris.core.link.data; + +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import io.lumine.mythic.api.adapters.AbstractLocation; +import io.lumine.mythic.api.config.MythicLineConfig; +import io.lumine.mythic.api.mobs.MythicMob; +import io.lumine.mythic.api.mobs.entities.SpawnReason; +import io.lumine.mythic.api.skills.conditions.ILocationCondition; +import io.lumine.mythic.bukkit.BukkitAdapter; +import io.lumine.mythic.bukkit.MythicBukkit; +import io.lumine.mythic.bukkit.adapters.BukkitWorld; +import io.lumine.mythic.bukkit.events.MythicConditionLoadEvent; +import io.lumine.mythic.core.mobs.ActiveMob; +import io.lumine.mythic.core.mobs.MobExecutor; +import io.lumine.mythic.core.mobs.MobStack; +import io.lumine.mythic.core.skills.SkillCondition; +import io.lumine.mythic.core.utils.annotations.MythicCondition; +import io.lumine.mythic.core.utils.annotations.MythicField; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.MissingResourceException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +public class MythicMobsDataProvider extends ExternalDataProvider implements Listener { + public MythicMobsDataProvider() { + super("MythicMobs"); + } + + @Override + public void init() { + } + + @Override + public @Nullable Entity spawnMob(@NotNull Location location, @NotNull Identifier entityId) throws MissingResourceException { + ActiveMob activeMob = spawnMob(BukkitAdapter.adapt(location), entityId); + return activeMob == null ? null : activeMob.getEntity().getBukkitEntity(); + } + + private ActiveMob spawnMob(AbstractLocation location, Identifier entityId) throws MissingResourceException { + MobExecutor manager = MythicBukkit.inst().getMobManager(); + MythicMob mythicMob = manager.getMythicMob(entityId.key()).orElse(null); + if (mythicMob == null) { + MobStack stack = manager.getMythicMobStack(entityId.key()); + if (stack == null) throw new MissingResourceException("Failed to find Mob!", entityId.namespace(), entityId.key()); + return stack.spawn(location, 1d, SpawnReason.OTHER, null); + } + return mythicMob.spawn(location, 1d, SpawnReason.OTHER, null, null); + } + + @Override + public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { + if (dataType != DataType.ENTITY) return List.of(); + MobExecutor manager = MythicBukkit.inst().getMobManager(); + return Stream.concat(manager.getMobNames().stream(), + manager.getMobStacks() + .stream() + .map(MobStack::getName) + ) + .distinct() + .map(name -> new Identifier("mythicmobs", name)) + .toList(); + } + + @Override + public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { + return id.namespace().equalsIgnoreCase("mythicmobs") && dataType == DataType.ENTITY; + } + + @EventHandler + public void on(MythicConditionLoadEvent event) { + switch (event.getConditionName()) { + case "irisbiome" -> event.register(new IrisBiomeCondition(event.getConditionName(), event.getConfig())); + case "irisregion" -> event.register(new IrisRegionCondition(event.getConditionName(), event.getConfig())); + } + } + + @MythicCondition(author = "CrazyDev22", name = "irisbiome", description = "Tests if the target is within the given list of biomes") + public static class IrisBiomeCondition extends SkillCondition implements ILocationCondition { + @MythicField(name = "biome", aliases = {"b"}, description = "A list of biomes to check") + private Set biomes = ConcurrentHashMap.newKeySet(); + @MythicField(name = "surface", aliases = {"s"}, description = "If the biome check should only be performed on the surface") + private boolean surface; + + public IrisBiomeCondition(String line, MythicLineConfig mlc) { + super(line); + String b = mlc.getString(new String[]{"biome", "b"}, ""); + biomes.addAll(Arrays.asList(b.split(","))); + surface = mlc.getBoolean(new String[]{"surface", "s"}, false); + } + + @Override + public boolean check(AbstractLocation target) { + PlatformChunkGenerator access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld()); + if (access == null) return false; + Engine engine = access.getEngine(); + if (engine == null) return false; + IrisBiome biome = surface ? + engine.getSurfaceBiome(target.getBlockX(), target.getBlockZ()) : + engine.getBiomeOrMantle(target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ()); + return biomes.contains(biome.getLoadKey()); + } + } + + @MythicCondition(author = "CrazyDev22", name = "irisregion", description = "Tests if the target is within the given list of biomes") + public static class IrisRegionCondition extends SkillCondition implements ILocationCondition { + @MythicField(name = "region", aliases = {"r"}, description = "A list of regions to check") + private Set regions = ConcurrentHashMap.newKeySet(); + + public IrisRegionCondition(String line, MythicLineConfig mlc) { + super(line); + String b = mlc.getString(new String[]{"region", "r"}, ""); + regions.addAll(Arrays.asList(b.split(","))); + } + + @Override + public boolean check(AbstractLocation target) { + PlatformChunkGenerator access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld()); + if (access == null) return false; + Engine engine = access.getEngine(); + if (engine == null) return false; + IrisRegion region = engine.getRegion(target.getBlockX(), target.getBlockZ()); + return regions.contains(region.getLoadKey()); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/link/data/NexoDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/NexoDataProvider.java new file mode 100644 index 000000000..c65c7bd60 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/data/NexoDataProvider.java @@ -0,0 +1,145 @@ +package art.arcane.iris.core.link.data; + +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import com.nexomc.nexo.api.NexoBlocks; +import com.nexomc.nexo.api.NexoFurniture; +import com.nexomc.nexo.api.NexoItems; +import com.nexomc.nexo.items.ItemBuilder; +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.container.BiomeColor; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.data.IrisCustomData; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.ItemDisplay; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.LeatherArmorMeta; +import org.bukkit.inventory.meta.MapMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.PotionMeta; +import org.jetbrains.annotations.NotNull; + +import java.awt.Color; +import java.util.Collection; +import java.util.List; +import java.util.MissingResourceException; + +import static org.bukkit.Color.fromARGB; + +public class NexoDataProvider extends ExternalDataProvider { + public NexoDataProvider() { + super("Nexo"); + } + + @Override + public void init() { + } + + @NotNull + @Override + public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap state) throws MissingResourceException { + if (!NexoItems.exists(blockId.key())) { + throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); + } + + Identifier blockState = ExternalDataSVC.buildState(blockId, state); + if (NexoBlocks.isCustomBlock(blockId.key())) { + BlockData data = NexoBlocks.blockData(blockId.key()); + if (data == null) + throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); + return IrisCustomData.of(data, blockState); + } else if (NexoFurniture.isFurniture(blockId.key())) { + return IrisCustomData.of(BukkitBlockResolution.getAir(), blockState); + } + + throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); + } + + @Override + public @NotNull List getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException { + if (!NexoItems.exists(blockId.key())) { + throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); + } + + return NexoFurniture.isFurniture(blockId.key()) ? YAW_FACE_BIOME_PROPERTIES : List.of(); + } + + @NotNull + @Override + public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) throws MissingResourceException { + ItemBuilder builder = NexoItems.itemFromId(itemId.key()); + if (builder == null) { + throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); + } + try { + return builder.build(); + } catch (Exception e) { + e.printStackTrace(); + throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); + } + } + + @Override + public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) { + Pair> statePair = ExternalDataSVC.parseState(blockId); + KMap state = statePair.getB(); + blockId = statePair.getA(); + + if (NexoBlocks.isCustomBlock(blockId.key())) { + NexoBlocks.place(blockId.key(), block.getLocation()); + return; + } + + if (!NexoFurniture.isFurniture(blockId.key())) + return; + + Pair pair = parseYawAndFace(engine, block, state); + ItemDisplay display = NexoFurniture.place(blockId.key(), block.getLocation(), pair.getA(), pair.getB()); + if (display == null) return; + ItemStack itemStack = display.getItemStack(); + if (itemStack == null) return; + + BiomeColor type = null; + try { + type = BiomeColor.valueOf(state.get("matchBiome").toUpperCase()); + } catch (NullPointerException | IllegalArgumentException ignored) {} + + if (type != null) { + Color biomeColor = INMS.get().getBiomeColor(block.getLocation(), type); + if (biomeColor == null) return; + ItemMeta meta = itemStack.getItemMeta(); + switch (meta) { + case LeatherArmorMeta armor -> armor.setColor(fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue())); + case PotionMeta potion -> potion.setColor(fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue())); + case MapMeta map -> map.setColor(fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue())); + case null, default -> {} + } + itemStack.setItemMeta(meta); + } + display.setItemStack(itemStack); + } + + @Override + public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { + if (dataType == DataType.ENTITY) return List.of(); + return NexoItems.itemNames() + .stream() + .map(i -> new Identifier("nexo", i)) + .filter(dataType.asPredicate(this)) + .toList(); + } + + @Override + public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { + if (dataType == DataType.ENTITY) return false; + return "nexo".equalsIgnoreCase(id.namespace()); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java new file mode 100644 index 000000000..b6c9d630e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java @@ -0,0 +1,177 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.loader; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.object.IrisImage; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.data.KCache; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +public class ImageResourceLoader extends ResourceLoader { + public ImageResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName, Options options) { + super(root, idm, folderName, resourceTypeName, IrisImage.class, options); + loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize()); + } + + public boolean supportsSchemas() { + return false; + } + + public long getSize() { + return loadCache.getSize(); + } + + public long getTotalStorage() { + return getSize(); + } + + protected IrisImage loadFile(File j, String name) { + try { + PrecisionStopwatch p = PrecisionStopwatch.start(); + BufferedImage bu = ImageIO.read(j); + + if (bu == null) { + IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + " (unsupported or corrupt image)"); + return null; + } + + IrisImage img = new IrisImage(bu); + img.setLoadFile(j); + img.setLoader(manager); + img.setLoadKey(name); + logLoad(j, img); + tlt.addAndGet(p.getMilliseconds()); + return img; + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + ": " + e.getMessage()); + return null; + } + } + + void getPNGFiles(File directory, String prefix, Set m, HashSet visitedDirectories) { + if (directory == null || !directory.exists()) { + return; + } + + if (directory.isDirectory()) { + String canonicalDirectory = toCanonicalPath(directory); + if (canonicalDirectory != null && !visitedDirectories.add(canonicalDirectory)) { + return; + } + } + + File[] listedFiles = directory.listFiles(); + if (listedFiles == null) { + return; + } + + for (File file : listedFiles) { + if (file.isFile() && file.getName().endsWith(".png")) { + m.add(prefix + file.getName().replaceAll("\\Q.png\\E", "")); + } else if (file.isDirectory()) { + getPNGFiles(file, prefix + file.getName() + "/", m, visitedDirectories); + } + } + } + + + public String[] getPossibleKeys() { + if (possibleKeys != null) { + return possibleKeys; + } + + IrisLogging.debug("Building " + resourceTypeName + " Possibility Lists"); + KSet m = new KSet<>(); + HashSet visitedDirectories = new HashSet<>(); + + for (File i : getFolders()) { + getPNGFiles(i, "", m, visitedDirectories); + } + + KList v = new KList<>(m); + possibleKeys = v.toArray(new String[0]); + return possibleKeys; + } + + private String toCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException ignored) { + return null; + } + } + + public File findFile(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null")) { + IrisLogging.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + + File file = resolveFile(name, ".png"); + + if (file != null) { + return file; + } + + IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); + + return null; + } + + public IrisImage load(String name) { + return load(name, true); + } + + private IrisImage loadRaw(String name) { + File file = resolveFile(name, ".png"); + + if (file != null) { + return loadFile(file, name); + } + + IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); + + return null; + } + + public IrisImage load(String name, boolean warn) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null") && warn) { + IrisLogging.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + return loadCache.get(name); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java new file mode 100644 index 000000000..9cfe48269 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java @@ -0,0 +1,771 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.loader; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.Strictness; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.IrisStructureLocator; +import art.arcane.iris.engine.framework.structure.StructureGraphCatalog; +import art.arcane.iris.core.structure.authoring.StructureRecoveryResult; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBlockData; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisEntity; +import art.arcane.iris.engine.object.IrisExpression; +import art.arcane.iris.engine.object.IrisObjectScale; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.engine.object.IrisImage; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.engine.object.IrisMarker; +import art.arcane.iris.engine.object.IrisMod; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.matter.IrisMatterObject; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.mantle.flag.MantleFlagAdapter; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.reflect.KeyedType; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.iris.util.project.context.IrisContext; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Data +public class IrisData implements ExclusionStrategy, TypeAdapterFactory { + private static final Map dataLoaders = new ConcurrentHashMap<>(); + // Loaders (cached or detached) that currently have registered engines; see hasActiveEngines. + // Identity-keyed on purpose: Lombok's @Data hashCode over this class's mutable loader state + // drifts while an engine runs, so a hashing set would silently fail removal and pin every + // engine-hosting IrisData (and its pack graph) for the JVM lifetime. + private static final Set ENGINE_HOLDERS = + Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>())); + private final File dataFolder; + private final int id; + private final boolean datapackCompiler; + private volatile boolean closed = false; + private ResourceLoader biomeLoader; + private ResourceLoader lootLoader; + private ResourceLoader regionLoader; + private ResourceLoader dimensionLoader; + private ResourceLoader generatorLoader; + private ResourceLoader entityLoader; + private ResourceLoader markerLoader; + private ResourceLoader spawnerLoader; + private ResourceLoader modLoader; + private ResourceLoader blockLoader; + private ResourceLoader expressionLoader; + private ResourceLoader objectLoader; + private ResourceLoader matterLoader; + private ResourceLoader imageLoader; + private ResourceLoader structureLoader; + private ResourceLoader jigsawPoolLoader; + private ResourceLoader jigsawPieceLoader; + private KMap> possibleSnippets; + private Gson gson; + private Gson snippetLoader; + private GsonBuilder builder; + private volatile KMap, ResourceLoader> loaders = new KMap<>(); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final transient List engines = new ArrayList<>(); + + private IrisData(File dataFolder) { + this(dataFolder, false); + } + + private IrisData(File dataFolder, boolean datapackCompiler) { + this.dataFolder = dataFolder; + this.id = RNG.r.imax(); + this.datapackCompiler = datapackCompiler; + if (datapackCompiler) { + hotloadedDatapackCompiler(); + } else { + hotloaded(); + } + } + + public static IrisData get(File dataFolder) { + return dataLoaders.computeIfAbsent(dataFolder, IrisData::new); + } + + public static IrisData openRuntime(File dataFolder) { + return new IrisData(dataFolder); + } + + public static IrisData openDatapackCompiler(File dataFolder) { + return new IrisData(dataFolder, true); + } + + public static Optional getLoaded(File dataFolder) { + return Optional.ofNullable(dataLoaders.get(dataFolder)); + } + + public static boolean invalidateLoadedStructureResources(File dataFolder) { + Path requested = dataFolderIdentity(Objects.requireNonNull( + dataFolder, + "Iris data folder to invalidate")); + boolean invalidated = false; + for (Map.Entry entry : dataLoaders.entrySet()) { + Path loaded = dataFolderIdentity(entry.getKey()); + if (!loaded.equals(requested)) { + continue; + } + entry.getValue().invalidateStructureResources(); + invalidated = true; + } + return invalidated; + } + + public static void dereference() { + dataLoaders.values().forEach(IrisData::cleanupEngine); + } + + public static int cacheSize() { + int m = 0; + for (IrisData i : dataLoaders.values()) { + for (ResourceLoader j : i.getLoaders().values()) { + m += j.getLoadCache().getSize(); + } + } + + return m; + } + + public static IrisObject loadAnyObject(String key, @Nullable IrisData nearest) { + return loadAny(IrisObject.class, key, nearest); + } + + public static IrisMatterObject loadAnyMatter(String key, @Nullable IrisData nearest) { + return loadAny(IrisMatterObject.class, key, nearest); + } + + public static IrisBiome loadAnyBiome(String key, @Nullable IrisData nearest) { + return loadAny(IrisBiome.class, key, nearest); + } + + public static IrisExpression loadAnyExpression(String key, @Nullable IrisData nearest) { + return loadAny(IrisExpression.class, key, nearest); + } + + public static IrisMod loadAnyMod(String key, @Nullable IrisData nearest) { + return loadAny(IrisMod.class, key, nearest); + } + + public static IrisEntity loadAnyEntity(String key, @Nullable IrisData nearest) { + return loadAny(IrisEntity.class, key, nearest); + } + + public static IrisLootTable loadAnyLootTable(String key, @Nullable IrisData nearest) { + return loadAny(IrisLootTable.class, key, nearest); + } + + public static IrisBlockData loadAnyBlock(String key, @Nullable IrisData nearest) { + return loadAny(IrisBlockData.class, key, nearest); + } + + public static IrisSpawner loadAnySpaner(String key, @Nullable IrisData nearest) { + return loadAny(IrisSpawner.class, key, nearest); + } + + public static IrisRegion loadAnyRegion(String key, @Nullable IrisData nearest) { + return loadAny(IrisRegion.class, key, nearest); + } + + public static IrisMarker loadAnyMarker(String key, @Nullable IrisData nearest) { + return loadAny(IrisMarker.class, key, nearest); + } + + public static IrisImage loadAnyImage(String key, @Nullable IrisData nearest) { + return loadAny(IrisImage.class, key, nearest); + } + + public static IrisDimension loadAnyDimension(String key, @Nullable IrisData nearest) { + return loadAny(IrisDimension.class, key, nearest); + } + + public static IrisGenerator loadAnyGenerator(String key, @Nullable IrisData nearest) { + return loadAny(IrisGenerator.class, key, nearest); + } + + public static IrisJigsawPool loadAnyJigsawPool(String key, @Nullable IrisData nearest) { + return loadAny(IrisJigsawPool.class, key, nearest); + } + + public static IrisJigsawPiece loadAnyJigsawPiece(String key, @Nullable IrisData nearest) { + return loadAny(IrisJigsawPiece.class, key, nearest); + } + + public static T loadAny(Class type, String key, @Nullable IrisData nearest) { + try { + if (nearest != null) { + T t = nearest.load(type, key, false); + if (t != null) { + return t; + } + } + + for (File i : PackDirectoryResolver.listVisiblePackDirectories( + IrisPlatforms.get().packsFolder())) { + IrisData dm = get(i); + if (dm == nearest) continue; + T t = dm.load(type, key, false); + + if (t != null) { + return t; + } + } + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + + return null; + } + + public T load(Class type, String key, boolean warn) { + var loader = getLoader(type); + if (loader == null) return null; + return loader.load(key, warn); + } + + @SuppressWarnings("unchecked") + public ResourceLoader getLoader(Class type) { + return (ResourceLoader) loaders.get(type); + } + + public ResourceLoader getTypedLoaderFor(File f) { + String[] k = f.getPath().split("\\Q" + File.separator + "\\E"); + + for (String i : k) { + for (ResourceLoader j : loaders.values()) { + if (j.getFolderName().equals(i)) { + return j; + } + } + } + + return null; + } + + public void cleanupEngine() { + int removed; + synchronized (engines) { + int previousSize = engines.size(); + removeClosedEngines(); + removed = previousSize - engines.size(); + } + if (removed > 0) { + IrisLogging.debug("Dereferenced " + removed + " Data registration(s) " + getId() + " " + getDataFolder()); + } + } + + public Engine getEngine() { + IrisContext context = IrisContext.get(); + if (context != null) { + Engine contextEngine = context.getEngine(); + if (!contextEngine.isClosed() && contextEngine.getData() == this) { + return contextEngine; + } + } + + synchronized (engines) { + removeClosedEngines(); + return engines.size() == 1 ? engines.get(0) : null; + } + } + + public List getEngines() { + synchronized (engines) { + removeClosedEngines(); + return List.copyOf(engines); + } + } + + /** + * True when any loader — including detached {@link #openRuntime(File)} instances, which + * never enter the dataLoaders cache — has a live engine reading the given pack folder. + * The dataLoaders cache cannot answer this question: engines only ever attach to + * openRuntime instances. + */ + public static boolean hasActiveEngines(File dataFolder) { + Path target = dataFolder.toPath().toAbsolutePath().normalize(); + IrisData[] holders; + synchronized (ENGINE_HOLDERS) { + holders = ENGINE_HOLDERS.toArray(new IrisData[0]); + } + for (IrisData data : holders) { + if (data.getEngines().isEmpty()) { + ENGINE_HOLDERS.remove(data); + continue; + } + if (data.getDataFolder().toPath().toAbsolutePath().normalize().equals(target)) { + return true; + } + } + return false; + } + + public void registerEngine(Engine engine) { + Objects.requireNonNull(engine, "engine"); + synchronized (engines) { + for (Engine registeredEngine : engines) { + if (registeredEngine == engine) { + return; + } + } + engines.add(engine); + } + ENGINE_HOLDERS.add(this); + } + + public void unregisterEngine(Engine engine) { + if (engine == null) { + return; + } + synchronized (engines) { + Iterator iterator = engines.iterator(); + while (iterator.hasNext()) { + if (iterator.next() == engine) { + iterator.remove(); + break; + } + } + if (engines.isEmpty()) { + ENGINE_HOLDERS.remove(this); + } + } + } + + private void removeClosedEngines() { + Iterator iterator = engines.iterator(); + while (iterator.hasNext()) { + if (iterator.next().isClosed()) { + iterator.remove(); + } + } + } + + public void preprocessObject(IrisRegistrant t) { + } + + public void close() { + closed = true; + dump(); + synchronized (engines) { + engines.clear(); + } + ENGINE_HOLDERS.remove(this); + dataLoaders.remove(dataFolder, this); + } + + public IrisData copy() { + return IrisData.get(dataFolder); + } + + private ResourceLoader registerLoader(Class registrant, KMap, ResourceLoader> target) { + try { + IrisRegistrant rr = registrant.getConstructor().newInstance(); + ResourceLoader r = null; + ResourceLoader.Options options = datapackCompiler + ? ResourceLoader.Options.datapackCompiler() + : ResourceLoader.Options.runtime(); + if (registrant.equals(IrisObject.class)) { + r = (ResourceLoader) new ObjectResourceLoader(dataFolder, this, rr.getFolderName(), + rr.getTypeName(), options); + } else if (registrant.equals(IrisMatterObject.class)) { + r = (ResourceLoader) new MatterObjectResourceLoader(dataFolder, this, rr.getFolderName(), + rr.getTypeName(), options); + } else if (registrant.equals(IrisImage.class)) { + r = (ResourceLoader) new ImageResourceLoader(dataFolder, this, rr.getFolderName(), + rr.getTypeName(), options); + } else { + J.attempt(() -> registrant.getConstructor().newInstance().registerTypeAdapters(builder)); + r = new ResourceLoader<>(dataFolder, this, rr.getFolderName(), rr.getTypeName(), registrant, options); + } + + target.put(registrant, r); + + return r; + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + IrisLogging.error("Failed to create loader! " + registrant.getCanonicalName()); + } + + return null; + } + + public synchronized void hotloaded() { + StructureGraphCatalog.invalidate(this); + IrisObjectScale.invalidate(this); + closed = false; + possibleSnippets = new KMap<>(); + builder = new GsonBuilder() + .addDeserializationExclusionStrategy(this) + .addSerializationExclusionStrategy(this) + .setStrictness(Strictness.LENIENT) + .registerTypeAdapterFactory(this) + .registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter()) + .setPrettyPrinting(); + KMap, ResourceLoader> replacement = new KMap<>(); + File packs = dataFolder; + packs.mkdirs(); + recoverStructureTransactions(); + this.lootLoader = registerLoader(IrisLootTable.class, replacement); + this.spawnerLoader = registerLoader(IrisSpawner.class, replacement); + this.entityLoader = registerLoader(IrisEntity.class, replacement); + this.regionLoader = registerLoader(IrisRegion.class, replacement); + this.biomeLoader = registerLoader(IrisBiome.class, replacement); + this.modLoader = registerLoader(IrisMod.class, replacement); + this.dimensionLoader = registerLoader(IrisDimension.class, replacement); + this.generatorLoader = registerLoader(IrisGenerator.class, replacement); + this.markerLoader = registerLoader(IrisMarker.class, replacement); + this.blockLoader = registerLoader(IrisBlockData.class, replacement); + this.expressionLoader = registerLoader(IrisExpression.class, replacement); + this.objectLoader = registerLoader(IrisObject.class, replacement); + this.imageLoader = registerLoader(IrisImage.class, replacement); + this.matterLoader = registerLoader(IrisMatterObject.class, replacement); + this.structureLoader = registerLoader(IrisStructure.class, replacement); + this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class, replacement); + this.jigsawPieceLoader = registerLoader(IrisJigsawPiece.class, replacement); + builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter); + + gson = builder.create(); + loaders = replacement; + + for (Engine engine : getEngines()) { + engine.hotload(); + } + } + + private void hotloadedDatapackCompiler() { + closed = false; + possibleSnippets = new KMap<>(); + builder = new GsonBuilder() + .addDeserializationExclusionStrategy(this) + .addSerializationExclusionStrategy(this) + .setStrictness(Strictness.LENIENT) + .registerTypeAdapterFactory(this) + .registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter()) + .setPrettyPrinting(); + KMap, ResourceLoader> replacement = new KMap<>(); + dataFolder.mkdirs(); + recoverStructureTransactions(); + biomeLoader = registerLoader(IrisBiome.class, replacement); + dimensionLoader = registerLoader(IrisDimension.class, replacement); + builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter); + gson = builder.create(); + loaders = replacement; + if (biomeLoader == null || dimensionLoader == null) { + throw new IllegalStateException("Unable to initialize Iris datapack compiler loaders for " + dataFolder); + } + } + + public void dump() { + StructureGraphCatalog.invalidate(this); + IrisObjectScale.invalidate(this); + for (ResourceLoader i : loaders.values()) { + i.clearCache(); + } + } + + public synchronized void invalidateStructureResources() { + StructureGraphCatalog.invalidate(this); + invalidateLoader(objectLoader); + invalidateLoader(structureLoader); + invalidateLoader(jigsawPoolLoader); + invalidateLoader(jigsawPieceLoader); + for (Engine engine : getEngines()) { + IrisStructureLocator.invalidate(engine); + } + } + + private void recoverStructureTransactions() { + StructureRecoveryResult recovery = new StructureTransactionWriter(dataFolder.toPath()) + .recoverIncompleteTransactions(); + if (recovery.successful()) { + if (recovery.recoveredTransactions() > 0) { + IrisLogging.warn("Recovered " + recovery.recoveredTransactions() + + " interrupted structure authoring transaction(s) in " + dataFolder); + } + return; + } + IllegalStateException failure = new IllegalStateException( + "Unable to recover interrupted structure authoring transactions in " + dataFolder); + for (StructureRecoveryResult.Failure recoveryFailure : recovery.failures()) { + failure.addSuppressed(new IOException( + "Recovery failed for " + recoveryFailure.transactionRoot(), + recoveryFailure.cause() + )); + } + IrisLogging.reportError(failure); + failure.printStackTrace(); + throw failure; + } + + public void clearLists() { + for (ResourceLoader i : loaders.values()) { + i.clearList(); + } + possibleSnippets.clear(); + } + + private void invalidateLoader(ResourceLoader loader) { + if (loader == null) { + return; + } + loader.clearCache(); + loader.clearList(); + } + + private static Path dataFolderIdentity(File dataFolder) { + Path normalized = dataFolder.toPath().toAbsolutePath().normalize(); + try { + return Files.exists(normalized) ? normalized.toRealPath() : normalized; + } catch (IOException exception) { + IrisLogging.debug("Unable to resolve Iris data folder identity for " + + normalized + "; using its normalized path: " + exception.getMessage()); + return normalized; + } + } + + public Set> resolveSnippets() { + var result = new HashSet>(); + var processed = new HashSet>(); + + var queue = new LinkedList>(loaders.keySet()); + while (!queue.isEmpty()) { + var type = queue.poll(); + if (shouldSkipClass(type) || !processed.add(type)) + continue; + if (type.isAnnotationPresent(Snippet.class)) + result.add(type); + + try { + for (var field : type.getDeclaredFields()) { + if (shouldSkipField(new FieldAttributes(field))) + continue; + + queue.add(field.getType()); + } + } catch (Throwable ignored) { + } + } + + return result; + } + + public String toLoadKey(File f) { + if (f.getPath().startsWith(getDataFolder().getPath())) { + String[] full = f.getPath().split("\\Q" + File.separator + "\\E"); + String[] df = getDataFolder().getPath().split("\\Q" + File.separator + "\\E"); + StringBuilder g = new StringBuilder(); + boolean m = true; + for (int i = 0; i < full.length; i++) { + if (i >= df.length) { + if (m) { + m = false; + continue; + } + + g.append("/").append(full[i]); + } + } + + return g.substring(1).split("\\Q.\\E")[0]; + } else { + IrisLogging.error("Forign file from loader " + f.getPath() + " (loader realm: " + getDataFolder().getPath() + ")"); + } + + IrisLogging.error("Failed to load " + f.getPath() + " (loader realm: " + getDataFolder().getPath() + ")"); + + return null; + } + + @Override + public boolean shouldSkipField(FieldAttributes f) { + return false; + } + + @Override + public boolean shouldSkipClass(Class c) { + if (c.equals(AtomicCache.class)) { + return true; + } else return c.equals(ChronoLatch.class); + } + + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (!typeToken.getRawType().isAnnotationPresent(Snippet.class)) { + return null; + } + + String snippetType = typeToken.getRawType().getDeclaredAnnotation(Snippet.class).value(); + String snippedBase = "snippet/" + snippetType + "/"; + + return new TypeAdapter<>() { + @Override + public void write(JsonWriter jsonWriter, T t) throws IOException { + gson.getDelegateAdapter(IrisData.this, typeToken).write(jsonWriter, t); + } + + @Override + public T read(JsonReader reader) throws IOException { + TypeAdapter adapter = gson.getDelegateAdapter(IrisData.this, typeToken); + + if (reader.peek().equals(JsonToken.STRING)) { + String r = reader.nextString(); + if (!r.startsWith("snippet/")) + return null; + if (!r.startsWith(snippedBase)) + r = snippedBase + r.substring(8); + + File f = new File(getDataFolder(), r + ".json"); + if (f.exists()) { + try (JsonReader snippetReader = new JsonReader(new FileReader(f))){ + return adapter.read(snippetReader); + } catch (Throwable e) { + IrisLogging.error("Couldn't read snippet " + r + " in " + reader.getPath() + " (" + e.getMessage() + ")"); + } + } else { + IrisLogging.error("Couldn't find snippet " + r + " in " + reader.getPath()); + } + + return null; + } + + try { + return adapter.read(reader); + } catch (Throwable e) { + IrisLogging.error("Failed to read " + typeToken.getRawType().getCanonicalName() + "... faking objects a little to load the file at least."); + IrisLogging.reportError(e); + try { + return (T) typeToken.getRawType().getConstructor().newInstance(); + } catch (Throwable ignored) { + + } + } + return null; + } + }; + } + + public KList getPossibleSnippets(String f) { + return possibleSnippets.computeIfAbsent(f, (k) -> { + KList l = new KList<>(); + + File snippetFolder = new File(getDataFolder(), "snippet/" + f); + if (!snippetFolder.exists()) return l; + + String absPath = snippetFolder.getAbsolutePath(); + try (var stream = Files.walk(snippetFolder.toPath())) { + stream.filter(Files::isRegularFile) + .map(Path::toAbsolutePath) + .map(Path::toString) + .filter(s -> s.endsWith(".json")) + .map(s -> s.substring(absPath.length() + 1)) + .map(s -> s.replace("\\", "/")) + .map(s -> s.split("\\Q.\\E")[0]) + .forEach(s -> l.add("snippet/" + s)); + } catch (Throwable e) { + e.printStackTrace(); + } + + return l; + }); + } + + public boolean isClosed() { + return closed; + } + + public void savePrefetch(Engine engine) { + BurstExecutor b = MultiBurst.ioBurst.burst(loaders.size()); + + for (ResourceLoader i : loaders.values()) { + b.queue(() -> { + try { + i.saveFirstAccess(engine); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + b.complete(); + IrisLogging.info("Saved Prefetch Cache to speed up future world startups"); + } + + public void loadPrefetch(Engine engine) { + for (ResourceLoader loader : loaders.values()) { + try { + loader.loadFirstAccess(engine); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } + IrisLogging.debug("Loaded Prefetch Cache to reduce generation disk use."); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/loader/IrisRegistrant.java b/core/src/main/java/art/arcane/iris/core/loader/IrisRegistrant.java new file mode 100644 index 000000000..35aac5081 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/loader/IrisRegistrant.java @@ -0,0 +1,55 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.loader; + +import com.google.gson.GsonBuilder; +import art.arcane.iris.spi.IrisLogging; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.awt.Desktop; +import java.io.File; + +@Data +public abstract class IrisRegistrant { + @EqualsAndHashCode.Exclude + private transient IrisData loader; + + private transient String loadKey; + + private transient File loadFile; + + public abstract String getFolderName(); + + public abstract String getTypeName(); + + public void registerTypeAdapters(GsonBuilder builder) { + + } + + public File openInVSCode() { + try { + Desktop.getDesktop().open(getLoadFile()); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + return getLoadFile(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/loader/JsonSchemaValidator.java b/core/src/main/java/art/arcane/iris/core/loader/JsonSchemaValidator.java new file mode 100644 index 000000000..8857a35fa --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/loader/JsonSchemaValidator.java @@ -0,0 +1,203 @@ +package art.arcane.iris.core.loader; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.json.JSONObject; +import com.google.gson.annotations.SerializedName; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class JsonSchemaValidator { + private static final ConcurrentHashMap, Set> FIELD_CACHE = new ConcurrentHashMap<>(); + private static final int SUGGESTION_MAX_DISTANCE = 4; + + private JsonSchemaValidator() { + } + + static void validateTopLevelKeys(JSONObject parsed, String rawText, File file, String resourceTypeName, Class objectClass) { + if (parsed == null || objectClass == null) { + return; + } + Set known = FIELD_CACHE.computeIfAbsent(objectClass, JsonSchemaValidator::collectFieldNames); + for (String key : parsed.keySet()) { + if (known.contains(key)) { + continue; + } + reportUnknownKey(key, rawText, file, resourceTypeName, known); + } + } + + static void reportLoadFailure(File file, String rawText, String resourceTypeName, Throwable error) { + String message = error.getMessage(); + if (message == null || message.isBlank()) { + message = error.getClass().getSimpleName(); + } + int line = extractLineFromMessage(message); + String location = file.getPath(); + if (line > 0) { + location = location + ":" + line; + } + StringBuilder out = new StringBuilder(); + out.append("Couldn't load ").append(resourceTypeName) + .append(C.RED).append(" in ").append(C.WHITE).append(location).append(C.RED) + .append(" -> ").append(message); + String snippet = buildSnippet(rawText, line); + if (snippet != null) { + out.append('\n').append(snippet); + } + IrisLogging.warn(out.toString()); + } + + private static void reportUnknownKey(String key, String rawText, File file, String resourceTypeName, Set known) { + int line = findLineForKey(rawText, key); + String suggestion = closestMatch(key, known); + StringBuilder out = new StringBuilder(); + out.append("Unknown ").append(resourceTypeName).append(" field ") + .append(C.WHITE).append('"').append(key).append('"').append(C.YELLOW) + .append(" in ").append(C.WHITE).append(file.getPath()); + if (line > 0) { + out.append(":").append(line); + } + out.append(C.YELLOW).append(" (Gson will silently ignore this)"); + if (suggestion != null) { + out.append(". Did you mean ").append(C.WHITE).append('"').append(suggestion).append('"').append(C.YELLOW).append("?"); + } + String snippet = buildSnippet(rawText, line); + if (snippet != null) { + out.append('\n').append(snippet); + } + IrisLogging.warn(out.toString()); + } + + private static Set collectFieldNames(Class cls) { + Set names = new LinkedHashSet<>(); + Class c = cls; + while (c != null && c != Object.class) { + for (Field field : c.getDeclaredFields()) { + int mods = field.getModifiers(); + if (Modifier.isStatic(mods) || Modifier.isTransient(mods)) { + continue; + } + if (field.isSynthetic()) { + continue; + } + SerializedName serialized = field.getAnnotation(SerializedName.class); + if (serialized != null) { + names.add(serialized.value()); + Collections.addAll(names, serialized.alternate()); + } else { + names.add(field.getName()); + } + } + c = c.getSuperclass(); + } + return Collections.unmodifiableSet(names); + } + + private static int findLineForKey(String rawText, String key) { + if (rawText == null || key == null) { + return -1; + } + Pattern pattern = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:"); + Matcher matcher = pattern.matcher(rawText); + if (!matcher.find()) { + return -1; + } + int index = matcher.start(); + int line = 1; + for (int i = 0; i < index; i++) { + if (rawText.charAt(i) == '\n') { + line++; + } + } + return line; + } + + private static int extractLineFromMessage(String message) { + if (message == null) { + return -1; + } + Matcher m = Pattern.compile("line\\s+(\\d+)").matcher(message); + if (m.find()) { + try { + return Integer.parseInt(m.group(1)); + } catch (NumberFormatException ignored) { + } + } + return -1; + } + + private static String buildSnippet(String rawText, int line) { + if (rawText == null || line <= 0) { + return null; + } + String[] lines = rawText.split("\n", -1); + if (line > lines.length) { + return null; + } + int from = Math.max(0, line - 2); + int to = Math.min(lines.length, line + 1); + StringBuilder out = new StringBuilder(); + int width = String.valueOf(to).length(); + for (int i = from; i < to; i++) { + int n = i + 1; + boolean focus = n == line; + out.append(focus ? C.RED + "> " : C.GRAY + " "); + out.append(String.format("%" + width + "d", n)).append(" | "); + String content = lines[i]; + if (content.length() > 200) { + content = content.substring(0, 200) + "..."; + } + out.append(content); + if (i < to - 1) { + out.append('\n'); + } + } + return out.toString(); + } + + private static String closestMatch(String key, Set known) { + String lowerKey = key.toLowerCase(); + String best = null; + int bestDistance = Integer.MAX_VALUE; + for (String candidate : known) { + int d = levenshtein(lowerKey, candidate.toLowerCase()); + if (d < bestDistance) { + bestDistance = d; + best = candidate; + } + } + if (best == null) { + return null; + } + int threshold = Math.min(SUGGESTION_MAX_DISTANCE, Math.max(1, key.length() / 2)); + return bestDistance <= threshold ? best : null; + } + + private static int levenshtein(String a, String b) { + int[] prev = new int[b.length() + 1]; + int[] curr = new int[b.length() + 1]; + for (int j = 0; j <= b.length(); j++) { + prev[j] = j; + } + for (int i = 1; i <= a.length(); i++) { + curr[0] = i; + for (int j = 1; j <= b.length(); j++) { + int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1; + curr[j] = Math.min(Math.min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + int[] tmp = prev; + prev = curr; + curr = tmp; + } + return prev[b.length()]; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java new file mode 100644 index 000000000..25a2c14f3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java @@ -0,0 +1,166 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.loader; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.object.matter.IrisMatterObject; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.data.KCache; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; + +public class MatterObjectResourceLoader extends ResourceLoader { + public MatterObjectResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName, Options options) { + super(root, idm, folderName, resourceTypeName, IrisMatterObject.class, options); + loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize()); + } + + public boolean supportsSchemas() { + return false; + } + + public long getSize() { + return loadCache.getSize(); + } + + public long getTotalStorage() { + return getSize(); + } + + protected IrisMatterObject loadFile(File j, String name) { + try { + PrecisionStopwatch p = PrecisionStopwatch.start(); + IrisMatterObject t = IrisMatterObject.from(j, manager); + t.setLoadKey(name); + t.setLoader(manager); + t.setLoadFile(j); + logLoad(j, t); + tlt.addAndGet(p.getMilliseconds()); + return t; + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + ": " + e.getMessage()); + return null; + } + } + + private void findMatFiles(File dir, String prefix, KSet m, HashSet visitedDirectories) { + if (dir == null || !dir.exists()) { + return; + } + + if (dir.isDirectory()) { + String canonicalDirectory = toCanonicalPath(dir); + if (canonicalDirectory != null && !visitedDirectories.add(canonicalDirectory)) { + return; + } + } + + File[] listedFiles = dir.listFiles(); + if (listedFiles == null) { + return; + } + + for (File file : listedFiles) { + if (file.isFile() && file.getName().endsWith(".mat")) { + m.add(prefix + file.getName().replaceAll("\\Q.mat\\E", "")); + } else if (file.isDirectory()) { + findMatFiles(file, prefix + file.getName() + "/", m, visitedDirectories); + } + } + } + + public String[] getPossibleKeys() { + if (possibleKeys != null) { + return possibleKeys; + } + + IrisLogging.debug("Building " + resourceTypeName + " Possibility Lists"); + KSet m = new KSet<>(); + HashSet visitedDirectories = new HashSet<>(); + + for (File folder : getFolders()) { + findMatFiles(folder, "", m, visitedDirectories); + } + + KList v = new KList<>(m); + possibleKeys = v.toArray(new String[0]); + return possibleKeys; + } + + private String toCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException ignored) { + return null; + } + } + + public File findFile(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null")) { + IrisLogging.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + + File file = resolveFile(name, ".mat"); + + if (file != null) { + return file; + } + + IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); + + return null; + } + + public IrisMatterObject load(String name) { + return load(name, true); + } + + private IrisMatterObject loadRaw(String name) { + File file = resolveFile(name, ".mat"); + + if (file != null) { + return loadFile(file, name); + } + + IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); + + return null; + } + + public IrisMatterObject load(String name, boolean warn) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null") && warn) { + IrisLogging.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + return loadCache.get(name); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java new file mode 100644 index 000000000..9f7fc902e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java @@ -0,0 +1,168 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.loader; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.data.KCache; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; + +public class ObjectResourceLoader extends ResourceLoader { + public ObjectResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName, Options options) { + super(root, idm, folderName, resourceTypeName, IrisObject.class, options); + loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize()); + } + + public boolean supportsSchemas() { + return false; + } + + public long getSize() { + return loadCache.getSize(); + } + + public long getTotalStorage() { + return getSize(); + } + + protected IrisObject loadFile(File j, String name) { + try { + PrecisionStopwatch p = PrecisionStopwatch.start(); + IrisObject t = new IrisObject(0, 0, 0); + t.setLoadKey(name); + t.setLoader(manager); + t.setLoadFile(j); + t.read(j); + logLoad(j, t); + tlt.addAndGet(p.getMilliseconds()); + return t; + } catch (Throwable e) { + IrisLogging.reportError(e); + String message = e.getMessage(); + String reason = e.getClass().getSimpleName(); + if (message != null && !message.isBlank()) { + reason = reason + ": " + message; + } + IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + " (" + reason + ")"); + return null; + } + } + + public String[] getPossibleKeys() { + if (possibleKeys != null) { + return possibleKeys; + } + IrisLogging.debug("Building " + resourceTypeName + " Possibility Lists"); + KSet m = new KSet<>(); + HashSet visitedDirectories = new HashSet<>(); + for (File i : getFolders()) { + m.addAll(getFiles(i, ".iob", true, visitedDirectories)); + } + possibleKeys = m.toArray(new String[0]); + return possibleKeys; + } + + private KList getFiles(File dir, String ext, boolean skipDirName, HashSet visitedDirectories) { + KList paths = new KList<>(); + if (dir == null || !dir.exists()) { + return paths; + } + + if (dir.isDirectory()) { + String canonicalDirectory = toCanonicalPath(dir); + if (canonicalDirectory != null && !visitedDirectories.add(canonicalDirectory)) { + return paths; + } + } + + File[] listedFiles = dir.listFiles(); + if (listedFiles == null) { + return paths; + } + + String name = skipDirName ? "" : dir.getName() + "/"; + for (File f : listedFiles) { + if (f.isFile() && f.getName().endsWith(ext)) { + paths.add(name + f.getName().replaceAll("\\Q" + ext + "\\E", "")); + } else if (f.isDirectory()) { + getFiles(f, ext, false, visitedDirectories).forEach(e -> paths.add(name + e)); + } + } + return paths; + } + + private String toCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException ignored) { + return null; + } + } + + public File findFile(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null")) { + IrisLogging.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + + File file = resolveFile(name, ".iob"); + + if (file != null) { + return file; + } + + IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); + + return null; + } + + public IrisObject load(String name) { + return load(name, true); + } + + private IrisObject loadRaw(String name) { + File file = resolveFile(name, ".iob"); + return file == null ? null : loadFile(file, name); + } + + public IrisObject load(String name, boolean warn) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null") && warn) { + IrisLogging.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + IrisObject result = loadCache.get(name); + if (result == null && warn) { + IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); + } + return result; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java new file mode 100644 index 000000000..dabd48102 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java @@ -0,0 +1,798 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.loader; + +import com.google.common.util.concurrent.AtomicDouble; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.project.SchemaBuilder; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.data.KCache; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.CustomOutputStream; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +@Data +@EqualsAndHashCode(exclude = "manager") +@ToString(exclude = "manager") +public class ResourceLoader implements MeteredCache { + public static final AtomicDouble tlt = new AtomicDouble(0); + private static final int CACHE_SIZE = 100000; + private static final int PREFETCH_MAGIC = 0x49504632; + private static final int PREFETCH_FORMAT_VERSION = 1; + private static final int PREFETCH_ENTRY_LIMIT = 1_024; + private static final int PREFETCH_KEY_LENGTH_LIMIT = 1_024; + private static final long PREFETCH_FILE_SIZE_LIMIT = 4L * 1_024L * 1_024L; + private static volatile ExecutorService schemaBuildExecutor; + private static final Set schemaBuildQueue = ConcurrentHashMap.newKeySet(); + protected final AtomicCache> folderCache; + protected volatile KSet firstAccess; + protected File root; + protected String folderName; + protected String resourceTypeName; + protected KCache loadCache; + protected Class objectClass; + protected String cname; + protected String[] possibleKeys = null; + protected IrisData manager; + protected AtomicInteger loads; + protected ChronoLatch sec; + private volatile boolean firstAccessOverflowed; + private final Options options; + + public ResourceLoader( + File root, + IrisData manager, + String folderName, + String resourceTypeName, + Class objectClass, + Options options + ) { + this.options = Objects.requireNonNull(options, "options"); + this.manager = manager; + firstAccess = new KSet<>(); + firstAccessOverflowed = false; + folderCache = new AtomicCache<>(); + sec = new ChronoLatch(5000); + loads = new AtomicInteger(); + this.objectClass = objectClass; + cname = objectClass.getCanonicalName(); + this.resourceTypeName = resourceTypeName; + this.root = root; + this.folderName = folderName; + loadCache = new KCache<>(this::loadRaw, options.cacheSize()); + IrisLogging.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> created in " + C.RED + "IDM/" + manager.getId() + C.LIGHT_PURPLE + " on " + C.GRAY + manager.getDataFolder().getPath()); + if (options.registerPreservation()) { + IrisServices.get(PreservationRegistry.class).registerCache(this); + } + } + + /** + * Lazily (re)creates the schema-build pool. PreservationSVC shutdownNow()s every + * registered executor on plugin disable; a static final pool registered once was dead for + * the rest of the JVM after a hot reload, breaking every studio workspace refresh. Each + * created pool is registered with the CURRENT cycle's preservation registry. + */ + private static synchronized ExecutorService schemaBuildExecutor() { + ExecutorService current = schemaBuildExecutor; + if (current == null || current.isShutdown()) { + schemaBuildQueue.clear(); + current = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "Iris-Schema-Builder"); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + schemaBuildExecutor = current; + PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class); + if (preservation != null) { + preservation.register(current); + } + } + return current; + } + + public JSONObject buildSchema() { + return buildSchema(false); + } + + public JSONObject buildSchemaImmediately() { + return buildSchema(true); + } + + private JSONObject buildSchema(boolean immediate) { + IrisLogging.debug("Building Schema " + objectClass.getSimpleName() + " " + root.getPath()); + JSONObject o = new JSONObject(); + KList fm = new KList<>(); + + for (int g = 1; g < 8; g++) { + fm.add("/" + folderName + Form.repeat("/*", g) + ".json"); + } + + o.put("fileMatch", new JSONArray(fm.toArray())); + o.put("url", "./.iris/schema/" + getFolderName() + "-schema.json"); + File a = new File(getManager().getDataFolder(), ".iris/schema/" + getFolderName() + "-schema.json"); + if (immediate) { + try { + IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4)); + } catch (IOException e) { + throw new IllegalStateException("Could not write schema " + a.getAbsolutePath(), e); + } + return o; + } + + String schemaPath = a.getAbsolutePath(); + if (schemaBuildQueue.add(schemaPath)) { + try { + schemaBuildExecutor().execute(() -> { + try { + IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4)); + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + schemaBuildQueue.remove(schemaPath); + } + }); + } catch (RejectedExecutionException rejected) { + // Never let a dead pool leak the queue entry or escape into workspace + // generation (which would delete the user's .code-workspace on the way out). + schemaBuildQueue.remove(schemaPath); + IrisLogging.warn("Schema build skipped (executor unavailable): " + schemaPath); + } + } + + return o; + } + + public File findFile(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null")) { + IrisLogging.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + + File file = resolveFile(name, ".json"); + + if (file != null) { + return file; + } + + IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); + + return null; + } + + /** + * Resolves a resource file by key. An exact name + extension hit always wins; + * only then is the dotted-prefix scan used (so plains.json beats plains.disabled.json). + */ + protected File resolveFile(String name, String extension) { + return resolveFile(name, extension, getFolders(name)); + } + + protected File resolveFile(String name, String extension, KList folders) { + if (folders == null) { + return null; + } + + for (File folder : folders) { + File exact = new File(folder, name + extension); + + if (exact.isFile()) { + return exact; + } + + File[] listed = folder.listFiles(); + + if (listed == null) { + continue; + } + + KList matches = new KList<>(); + + for (File candidate : listed) { + if (candidate.isFile() && candidate.getName().endsWith(extension) && candidate.getName().split("\\Q.\\E")[0].equals(name)) { + matches.add(candidate); + } + } + + if (matches.isEmpty()) { + continue; + } + + if (matches.size() > 1) { + matches.sort(Comparator.comparing(File::getName)); + IrisLogging.warn("Ambiguous " + resourceTypeName + " " + name + " in " + folder.getPath() + ": " + + matches.stream().map(File::getName).collect(Collectors.joining(", ")) + + " (using " + matches.get(0).getName() + ")"); + } + + return matches.get(0); + } + + return null; + } + + protected static String describeName(String name) { + if (name == null) return ""; + if (name.isEmpty()) return ""; + if (name.equals("null")) return "\"null\" (literal string)"; + return "\"" + name + "\""; + } + + protected static String callerHint() { + StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + return walker.walk(frames -> frames + .filter(f -> { + String cn = f.getClassName(); + return !cn.startsWith("art.arcane.iris.core.loader.") + && !cn.startsWith("art.arcane.volmlib.util.data.") + && !cn.startsWith("com.github.benmanes.caffeine."); + }) + .limit(3) + .map(f -> f.getClassName().substring(f.getClassName().lastIndexOf('.') + 1) + + "." + f.getMethodName() + ":" + f.getLineNumber()) + .reduce((a, b) -> a + " <- " + b) + .orElse("")); + } + + public void logLoad(File path, T t) { + loads.getAndIncrement(); + + if (loads.get() == 1) { + sec.flip(); + } + + if (sec.flip()) { + Runnable summary = () -> { + IrisLogging.debug("Loaded " + C.WHITE + loads.get() + " " + resourceTypeName + (loads.get() == 1 ? "" : "s") + C.GRAY + " (" + Form.f(getLoadCache().getSize()) + " " + resourceTypeName + (loadCache.getSize() == 1 ? "" : "s") + " Loaded)"); + loads.set(0); + }; + if (options.synchronousReporting()) { + summary.run(); + } else { + J.a(summary); + } + } + + IrisLogging.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> iload " + C.YELLOW + t.getLoadKey() + C.LIGHT_PURPLE + " in " + C.GRAY + t.getLoadFile().getPath() + C.LIGHT_PURPLE + " TLT: " + C.RED + Form.duration(tlt.get(), 2)); + } + + public void failLoad(File path, Throwable e) { + failLoad(path, null, e); + } + + public void failLoad(File path, String rawText, Throwable e) { + Runnable report = () -> JsonSchemaValidator.reportLoadFailure(path, rawText, resourceTypeName, e); + if (options.synchronousReporting()) { + report.run(); + } else { + J.a(report); + } + } + + private KList matchAllFiles(File root, Predicate f) { + KList files = new KList<>(); + HashSet visitedDirectories = new HashSet<>(); + matchFiles(root, files, f, visitedDirectories); + return files; + } + + private void matchFiles(File at, KList files, Predicate f, HashSet visitedDirectories) { + if (at == null || !at.exists()) { + return; + } + + if (at.isDirectory()) { + String canonicalPath = toCanonicalPath(at); + if (canonicalPath != null && !visitedDirectories.add(canonicalPath)) { + return; + } + + File[] listedFiles = at.listFiles(); + if (listedFiles == null) { + return; + } + + for (File listedFile : listedFiles) { + matchFiles(listedFile, files, f, visitedDirectories); + } + return; + } + + if (f.test(at)) { + files.add(at); + } + } + + private String toCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException ignored) { + return null; + } + } + + public String[] getPossibleKeys() { + if (possibleKeys != null) { + return possibleKeys; + } + + KList files = getFolders(); + + if (files == null) { + possibleKeys = new String[0]; + return possibleKeys; + } + + HashSet m = new HashSet<>(); + for (File i : files) { + for (File j : matchAllFiles(i, (f) -> f.getName().endsWith(".json"))) { + m.add(i.toURI().relativize(j.toURI()).getPath().replaceAll("\\Q.json\\E", "")); + } + } + + KList v = new KList<>(m); + possibleKeys = v.toArray(new String[0]); + return possibleKeys; + } + + public long count() { + return loadCache.getSize(); + } + + protected T loadFile(File j, String name) { + String rawText = null; + try { + PrecisionStopwatch p = PrecisionStopwatch.start(); + rawText = IO.readAll(j); + JSONObject parsed = new JSONObject(rawText); + JsonSchemaValidator.validateTopLevelKeys(parsed, rawText, j, resourceTypeName, objectClass); + T t = getManager().getGson() + .fromJson(preprocess(parsed).toString(0), objectClass); + t.setLoadKey(name); + t.setLoadFile(j); + t.setLoader(manager); + getManager().preprocessObject(t); + logLoad(j, t); + tlt.addAndGet(p.getMilliseconds()); + return t; + } catch (Throwable e) { + IrisLogging.reportError(e); + failLoad(j, rawText, e); + return null; + } + } + + protected JSONObject preprocess(JSONObject j) { + return j; + } + + public Stream streamAll() { + return streamAll(Arrays.stream(getPossibleKeys())); + } + + public Stream streamAll(Stream s) { + return s.map(this::load); + } + + public KList loadAll(KList s) { + KList m = new KList<>(); + + for (String i : s) { + T t = load(i); + + if (t != null) { + m.add(t); + } + } + + return m; + } + + public KList loadAllParallel(KList s) { + KList m = new KList<>(); + BurstExecutor burst = MultiBurst.ioBurst.burst(s.size()); + + for (String i : s) { + burst.queue(() -> { + T t = load(i); + if (t == null) + return; + + synchronized (m) { + m.add(t); + } + }); + } + + burst.complete(); + return m; + } + + public KList loadAll(KList s, Consumer postLoad) { + KList m = new KList<>(); + + for (String i : s) { + T t = load(i); + + if (t != null) { + m.add(t); + postLoad.accept(t); + } + } + + return m; + } + + public KList loadAll(String[] s) { + KList m = new KList<>(); + + for (String i : s) { + T t = load(i); + + if (t != null) { + m.add(t); + } + } + + return m; + } + + public T load(String name) { + return load(name, true); + } + + private T loadRaw(String name) { + File file = resolveFile(name, ".json"); + return file == null ? null : loadFile(file, name); + } + + public T load(String name, boolean warn) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null") && warn) { + IrisLogging.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + + KSet set = firstAccess; + // contains-first: CHM.add takes the bin monitor even when the key is present, and + // every generation thread hammers the same few keys through this line. + if (set != null && !set.contains(name)) { + if (set.size() < prefetchEntryLimit()) { + set.add(name); + } else { + firstAccessOverflowed = true; + } + } + return loadCache.get(name); + } + + private File prefetchFile(Engine engine) { + String dimensionIdentity = digestIdentity( + root.toPath().toAbsolutePath().normalize().toString(), + Long.toString(engine.getSeedManager().getSeed()), + Integer.toString(engine.getDimension().getVersion()), + Objects.requireNonNullElse(engine.getDimension().getLoadKey(), "")); + String loaderIdentity = digestIdentity(getFolderName()); + return IrisPlatforms.get().dataFile( + "prefetch", + "v2", + dimensionIdentity, + loaderIdentity + ".ipfch"); + } + + public void loadFirstAccess(Engine engine) throws IOException { + File file = prefetchFile(engine); + + if (!file.exists()) { + return; + } + + if (file.length() > PREFETCH_FILE_SIZE_LIMIT) { + discardPrefetch(file, "file exceeds " + PREFETCH_FILE_SIZE_LIMIT + " bytes"); + return; + } + + KList entries = new KList<>(); + + try (FileInputStream fin = new FileInputStream(file); + GZIPInputStream gzi = new GZIPInputStream(fin); + DataInputStream din = new DataInputStream(gzi)) { + int magic = din.readInt(); + int version = din.readInt(); + int count = din.readInt(); + + if (magic != PREFETCH_MAGIC || version != PREFETCH_FORMAT_VERSION) { + throw new IOException("unsupported prefetch format"); + } + if (count < 0 || count > prefetchEntryLimit()) { + throw new IOException("bad prefetch count " + count); + } + for (int index = 0; index < count; index++) { + String key = din.readUTF(); + if (key.isBlank() || key.length() > PREFETCH_KEY_LENGTH_LIMIT) { + throw new IOException("invalid prefetch key length " + key.length()); + } + entries.add(key); + } + } catch (IOException e) { + discardPrefetch(file, e.getMessage()); + return; + } + + IrisLogging.info("Loading " + entries.size() + " prefetch " + getFolderName()); + firstAccess = null; + for (String entry : entries) { + load(entry); + } + } + + public void saveFirstAccess(Engine engine) throws IOException { + KSet set = firstAccess; + if (set == null) return; + KList snapshot = new KList<>(set); + File file = prefetchFile(engine); + if (firstAccessOverflowed || snapshot.size() > prefetchEntryLimit()) { + Files.deleteIfExists(file.toPath()); + firstAccess = null; + return; + } + for (String key : snapshot) { + if (key == null || key.isBlank() || key.length() > PREFETCH_KEY_LENGTH_LIMIT) { + Files.deleteIfExists(file.toPath()); + firstAccess = null; + return; + } + } + File parent = file.getParentFile(); + + if (parent == null) { + throw new IOException("Prefetch path has no parent: " + file.getPath()); + } + + if (!parent.isDirectory() && !parent.mkdirs() && !parent.isDirectory()) { + throw new IOException("Couldn't create prefetch folder " + parent.getPath()); + } + + File temp = File.createTempFile(file.getName(), ".tmp", parent); + + try { + try (FileOutputStream fos = new FileOutputStream(temp); + GZIPOutputStream gzo = new CustomOutputStream(fos, 9); + DataOutputStream dos = new DataOutputStream(gzo)) { + dos.writeInt(PREFETCH_MAGIC); + dos.writeInt(PREFETCH_FORMAT_VERSION); + dos.writeInt(snapshot.size()); + + for (String key : snapshot) { + dos.writeUTF(key); + } + } + + try { + Files.move(temp.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(temp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(temp.toPath()); + } + + firstAccess = null; + } + + private int prefetchEntryLimit() { + return (int) Math.min(loadCache.getMaxSize(), PREFETCH_ENTRY_LIMIT); + } + + private void discardPrefetch(File file, String reason) { + IrisLogging.warn("Discarding corrupt prefetch " + file.getPath() + ": " + reason); + if (!file.delete()) { + IrisLogging.warn("Couldn't delete corrupt prefetch " + file.getPath()); + } + } + + private static String digestIdentity(String... components) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + for (String component : components) { + byte[] value = Objects.requireNonNullElse(component, "").getBytes(StandardCharsets.UTF_8); + digest.update((byte) (value.length >>> 24)); + digest.update((byte) (value.length >>> 16)); + digest.update((byte) (value.length >>> 8)); + digest.update((byte) value.length); + digest.update(value); + } + return HexFormat.of().formatHex(digest.digest()); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + public KList getFolders() { + return folderCache.aquire(() -> { + KList fc = new KList<>(); + + File[] files = root.listFiles(); + if (files == null) { + throw new IllegalStateException("Failed to list files in " + root); + } + + for (File i : files) { + if (i.isDirectory()) { + if (i.getName().equals(folderName)) { + fc.add(i); + break; + } + } + } + return fc; + }); + } + + public KList getFolders(String rc) { + KList folders = getFolders().copy(); + + if (rc.contains(":")) { + for (File i : folders.copy()) { + if (!rc.startsWith(i.getName() + ":")) { + folders.remove(i); + } + } + } + + return folders; + } + + public void clearCache() { + possibleKeys = null; + loadCache.invalidate(); + folderCache.reset(); + } + + public File fileFor(T b) { + return resolveFile(b.getLoadKey(), ".json", getFolders()); + } + + public boolean isLoaded(String next) { + return loadCache.contains(next); + } + + public void clearList() { + folderCache.reset(); + possibleKeys = null; + } + + public KList getPossibleKeys(String arg) { + KList f = new KList<>(); + + for (String i : getPossibleKeys()) { + if (i.equalsIgnoreCase(arg) || i.toLowerCase(Locale.ROOT).startsWith(arg.toLowerCase(Locale.ROOT)) || i.toLowerCase(Locale.ROOT).contains(arg.toLowerCase(Locale.ROOT)) || arg.toLowerCase(Locale.ROOT).contains(i.toLowerCase(Locale.ROOT))) { + f.add(i); + } + } + + return f; + } + + public boolean supportsSchemas() { + return true; + } + + public void clean() { + + } + + public long getSize() { + return loadCache.getSize(); + } + + @Override + public KCache getRawCache() { + return loadCache; + } + + @Override + public long getMaxSize() { + return loadCache.getMaxSize(); + } + + @Override + public boolean isClosed() { + return getManager().isClosed(); + } + + public long getTotalStorage() { + return getSize(); + } + + public record Options(int cacheSize, boolean registerPreservation, boolean synchronousReporting) { + public Options { + if (cacheSize < 1) { + throw new IllegalArgumentException("Resource loader cache size must be positive"); + } + } + + public static Options runtime() { + return new Options( + IrisSettings.get().getPerformance().getResourceLoaderCacheSize(), + true, + false + ); + } + + public static Options datapackCompiler() { + return new Options(CACHE_SIZE, false, true); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessages.java b/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessages.java new file mode 100644 index 000000000..176c79262 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessages.java @@ -0,0 +1,365 @@ +package art.arcane.iris.core.localization; + +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; + +public final class BukkitCommandMessages { + public static final TextKey COMMAND_DATAPACK_STARTING_DATAPACK_INGEST = TextKey.of( + "iris.bukkit.commanddatapack.starting_datapack_ingest", + C.GRAY + "Starting datapack ingest..." + ); + public static final TextKey COMMAND_DATAPACK_CONFIGURED_DATAPACK_IMPORTS = TextKey.of( + "iris.bukkit.commanddatapack.configured_datapack_imports", + C.GREEN + "Configured datapack imports: " + C.WHITE + "{value}" + ); + public static final TextKey COMMAND_DATAPACK_MESSAGE = TextKey.of( + "iris.bukkit.commanddatapack.message", + C.GRAY + " - " + C.WHITE + "{url}" + ); + public static final TextKey COMMAND_DATAPACK_INSTALLED_DATAPACKS = TextKey.of( + "iris.bukkit.commanddatapack.installed_datapacks", + C.GREEN + "Installed datapacks: " + C.WHITE + "{value}" + ); + public static final TextKey COMMAND_DATAPACK_MESSAGE_2 = TextKey.of( + "iris.bukkit.commanddatapack.message_2", + C.GRAY + " - " + C.WHITE + "{value}" + C.GRAY + " " + "{value2}" + ); + public static final TextKey COMMAND_DATAPACK_ADD_MODRINTH_URLS_DIMENSION_S_DATAPACKIMPORTS_LIST_THEN_RUN_IRIS = TextKey.of( + "iris.bukkit.commanddatapack.add_modrinth_urls_dimension_s_datapackimports_list_then_run_iris", + C.YELLOW + "Add Modrinth URLs to a dimension's 'datapackImports' list, then run /iris datapack ingest." + ); + public static final TextKey COMMAND_DEVELOPER_GENHASH_STARTED_CHUNKS = TextKey.of( + "iris.bukkit.commanddeveloper.genhash_started_chunks", + C.GREEN + "genhash started: " + "{value}" + " chunks..." + ); + public static final TextKey COMMAND_DEVELOPER_GENHASH_FAILED_AT_CHUNK = TextKey.of( + "iris.bukkit.commanddeveloper.genhash_failed_at_chunk", + C.RED + "genhash failed at chunk " + "{rx}" + "," + "{rz}" + ": " + "{value}" + ); + public static final TextKey COMMAND_DEVELOPER_GENHASH_GLOBAL_CHUNKS_SOLID = TextKey.of( + "iris.bukkit.commanddeveloper.genhash_global_chunks_solid", + C.GREEN + "genhash global=" + C.GOLD + "{value}" + C.GREEN + " chunks=" + "{value2}" + " solid=" + "{solidBlocks}" + " in " + "{value3}" + ); + public static final TextKey COMMAND_FIND_NOT_IRIS_WORLD = TextKey.of( + "iris.bukkit.commandfind.not_iris_world", + C.GOLD + "Not in an Iris World!" + ); + public static final TextKey COMMAND_FIND_UNKNOWN_STRUCTURE = TextKey.of( + "iris.bukkit.commandfind.unknown_structure", + C.RED + "Unknown structure: " + "{structureKey}" + ); + public static final TextKey COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE = TextKey.of( + "iris.bukkit.commandfind.run_this_game_teleport_structure", + C.GOLD + "Run this in-game to teleport to a structure." + ); + public static final TextKey COMMAND_FIND_LOCATING = TextKey.of( + "iris.bukkit.commandfind.locating", + C.GRAY + "Locating " + "{structureKey}" + "..." + ); + public static final TextKey COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE_2 = TextKey.of( + "iris.bukkit.commandfind.run_this_game_teleport_structure_2", + C.GOLD + "Run this in-game to teleport to a structure." + ); + public static final TextKey COMMAND_FIND_LOCATING_2 = TextKey.of( + "iris.bukkit.commandfind.locating_2", + C.GRAY + "Locating " + "{structure}" + "..." + ); + public static final TextKey COMMAND_FIND_TELEPORTED = TextKey.of( + "iris.bukkit.commandfind.teleported", + C.GREEN + "Teleported to " + "{structure}" + " @ " + "{value}" + ", " + "{y}" + ", " + "{value2}" + ); + public static final TextKey COMMAND_IRIS_SUCCESSFULLY_REMOVED_WORLD_FOLDER = TextKey.of( + "iris.bukkit.commandiris.successfully_removed_world_folder", + C.GREEN + "Successfully removed world folder" + ); + public static final TextKey COMMAND_IRIS_SUCCESSFULLY_REMOVED_WORLD_FOLDER_2 = TextKey.of( + "iris.bukkit.commandiris.successfully_removed_world_folder_2", + C.GREEN + "Successfully removed world folder" + ); + public static final TextKey COMMAND_IRIS_FAILED_REMOVE_WORLD_FOLDER = TextKey.of( + "iris.bukkit.commandiris.failed_remove_world_folder", + C.RED + "Failed to remove world folder" + ); + public static final TextKey COMMAND_OBJECT_NO_PACKS_WITH_OBJECTS_WERE_FOUND_ON_THIS_SERVER = TextKey.of( + "iris.bukkit.commandobject.no_packs_with_objects_were_found_on_this_server", + C.RED + "No packs with objects were found on this server." + ); + public static final TextKey COMMAND_OBJECT_NO_OBJECTS_PLACE_ACROSS_SELECTED_PACK_S = TextKey.of( + "iris.bukkit.commandobject.no_objects_place_across_selected_pack_s", + C.RED + "No objects to place across the selected pack(s)." + ); + public static final TextKey COMMAND_OBJECT_OPENING_OBJECT_STUDIO_OBJECTS = TextKey.of( + "iris.bukkit.commandobject.opening_object_studio_objects", + C.GREEN + "Opening Object Studio for " + "{scope}" + " (" + "{totalObjects}" + " objects)" + ); + public static final TextKey COMMAND_OBJECT_FAILED_OPEN_OBJECT_STUDIO = TextKey.of( + "iris.bukkit.commandobject.failed_open_object_studio", + C.RED + "Failed to open object studio: " + "{value}" + ); + public static final TextKey COMMAND_PACK_YOU_MUST_SPECIFY_PACK_NAME = TextKey.of( + "iris.bukkit.commandpack.you_must_specify_pack_name", + C.RED + "You must specify a pack name." + ); + public static final TextKey COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS = TextKey.of( + "iris.bukkit.commandpack.pack_not_found_under_packs", + C.RED + "Pack '" + "{pack}" + "' not found under packs/." + ); + public static final TextKey COMMAND_PACK_MESSAGE = TextKey.of( + "iris.bukkit.commandpack.message", + C.GRAY + " - " + "{label}" + ": " + "{value}" + ); + public static final TextKey COMMAND_PACK_MORE = TextKey.of( + "iris.bukkit.commandpack.more", + C.GRAY + " ... and " + "{value}" + " more." + ); + public static final TextKey COMMAND_STRUCTURE_VERIFYING_STRUCTURES_FROM_WITHIN_CHUNKS = TextKey.of( + "iris.bukkit.commandstructure.verifying_structures_from_within_chunks", + C.GREEN + "Verifying structures in " + C.WHITE + "{value}" + C.GREEN + " from " + "{value2}" + "," + "{value3}" + " within " + "{searchRadius}" + " chunks..." + ); + public static final TextKey COMMAND_STUDIO_IMPORTING_VANILLA_CONTENT_INTO = TextKey.of( + "iris.bukkit.commandstudio.importing_vanilla_content_into", + C.GREEN + "Importing vanilla content into " + C.WHITE + "{value}" + C.GREEN + "..." + ); + public static final TextKey COMMAND_STUDIO_IMPORTVANILLA_COMPLETE_OBJECTS_STRUCTURES_WRITTEN_FAILED = TextKey.of( + "iris.bukkit.commandstudio.importvanilla_complete_objects_structures_written_failed", + C.GREEN + "importvanilla complete: " + C.WHITE + "{imported}" + C.GREEN + " objects/structures written, " + C.WHITE + "{failed}" + C.GREEN + " failed." + ); + public static final TextKey COMMAND_STUDIO_TREES_OBJECTS_ARE_UNDER_OBJECTS_VANILLA_REFERENCE_THEM_FROM_BIOME = TextKey.of( + "iris.bukkit.commandstudio.trees_objects_are_under_objects_vanilla_reference_them_from_biome", + C.GRAY + "Trees/objects are under objects/vanilla/...; reference them from biome object placements." + ); + public static final TextKey COMMAND_STUDIO_NO_OPEN_STUDIO_PROJECTS = TextKey.of( + "iris.bukkit.commandstudio.no_open_studio_projects", + C.RED + "No open studio projects." + ); + public static final TextKey COMMAND_STUDIO_CLOSING_STUDIO = TextKey.of( + "iris.bukkit.commandstudio.closing_studio", + C.YELLOW + "Closing studio..." + ); + public static final TextKey COMMAND_STUDIO_STUDIO_CLOSE_FAILED = TextKey.of( + "iris.bukkit.commandstudio.studio_close_failed", + C.RED + "Studio close failed: " + "{value}" + ); + public static final TextKey COMMAND_STUDIO_STUDIO_CLOSE_FAILED_2 = TextKey.of( + "iris.bukkit.commandstudio.studio_close_failed_2", + C.RED + "Studio close failed: " + "{value}" + ); + public static final TextKey COMMAND_STUDIO_STUDIO_CLOSED_REMAINING_WORLD_FAMILY_CLEANUP_WAS_QUEUED_STARTUP_FALLBACK = TextKey.of( + "iris.bukkit.commandstudio.studio_closed_remaining_world_family_cleanup_was_queued_startup_fallback", + C.YELLOW + "Studio closed. Remaining world-family cleanup was queued for startup fallback." + ); + public static final TextKey COMMAND_STUDIO_STUDIO_CLOSED = TextKey.of( + "iris.bukkit.commandstudio.studio_closed", + C.GREEN + "Studio closed." + ); + public static final TextKey COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD_TOGGLE_DEBUG_SCOREBOARD = TextKey.of( + "iris.bukkit.commandstudio.you_must_be_studio_world_toggle_debug_scoreboard", + C.RED + "You must be in a Studio world to toggle the debug scoreboard." + ); + public static final TextKey COMMAND_STUDIO_STUDIO_DEBUG_SCOREBOARD = TextKey.of( + "iris.bukkit.commandstudio.studio_debug_scoreboard", + "{value}" + "Studio debug scoreboard " + "{value2}" + ); + public static final TextKey COMMAND_STUDIO_COULD_NOT_UPDATE_STUDIO_DEBUG_SCOREBOARD_RIGHT_NOW = TextKey.of( + "iris.bukkit.commandstudio.could_not_update_studio_debug_scoreboard_right_now", + C.RED + "Could not update the Studio debug scoreboard right now." + ); + public static final TextKey COMMAND_STUDIO_OPENED_INVENTORY = TextKey.of( + "iris.bukkit.commandstudio.opened_inventory", + C.GREEN + "Opened inventory!" + ); + public static final TextKey COMMAND_STUDIO_GENERATING_DATA = TextKey.of( + "iris.bukkit.commandstudio.generating_data", + C.GRAY + "Generating data..." + ); + public static final TextKey COMMAND_STUDIO_DONE = TextKey.of( + "iris.bukkit.commandstudio.done", + C.GREEN + "Done!" + ); + public static final TextKey COMMAND_STUDIO_MESSAGE = TextKey.of( + "iris.bukkit.commandstudio.message", + C.GREEN + "{k}" + ": " + "{value}" + " / " + "{value2}" + "%" + ); + public static final TextKey COMMAND_S_V_C_YOU_LACK_PERMISSION = TextKey.of( + "iris.bukkit.commandsvc.you_lack_permission", + "You lack the Permission '" + "{ROOTPERMISSION}" + "'" + ); + public static final TextKey IRIS_ENGINE_STATUS_MESSAGE = TextKey.of( + "iris.bukkit.irisenginestatus.message", + C.DARK_PURPLE + "-------------------------" + ); + public static final TextKey IRIS_ENGINE_STATUS_STATUS = TextKey.of( + "iris.bukkit.irisenginestatus.status", + C.DARK_PURPLE + "Status:" + ); + public static final TextKey IRIS_ENGINE_STATUS_SERVICE = TextKey.of( + "iris.bukkit.irisenginestatus.service", + C.DARK_PURPLE + "- Service: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_METRICS = TextKey.of( + "iris.bukkit.irisenginestatus.metrics", + C.DARK_PURPLE + "- Metrics: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_MAINTENANCE_PERIOD = TextKey.of( + "iris.bukkit.irisenginestatus.maintenance_period", + C.DARK_PURPLE + "- Maintenance Period: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_WORKER_PARALLELISM = TextKey.of( + "iris.bukkit.irisenginestatus.worker_parallelism", + C.DARK_PURPLE + "- Worker Parallelism: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_ACTIVE_WORLD_TASKS = TextKey.of( + "iris.bukkit.irisenginestatus.active_world_tasks", + C.DARK_PURPLE + "- Active World Tasks: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_TECTONIC_PLATES = TextKey.of( + "iris.bukkit.irisenginestatus.tectonic_plates", + C.DARK_PURPLE + "Tectonic Plates:" + ); + public static final TextKey IRIS_ENGINE_STATUS_CONFIGURED_RETENTION = TextKey.of( + "iris.bukkit.irisenginestatus.configured_retention", + C.DARK_PURPLE + "- Configured Retention: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_HEAP_USAGE = TextKey.of( + "iris.bukkit.irisenginestatus.heap_usage", + C.DARK_PURPLE + "- Heap Usage: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_RESIDENT = TextKey.of( + "iris.bukkit.irisenginestatus.resident", + C.DARK_PURPLE + "- Resident: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_QUEUED = TextKey.of( + "iris.bukkit.irisenginestatus.queued", + C.DARK_PURPLE + "- Queued: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_AVERAGE_IDLE_DURATION = TextKey.of( + "iris.bukkit.irisenginestatus.average_idle_duration", + C.DARK_PURPLE + "- Average Idle Duration: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_MAX_IDLE_DURATION = TextKey.of( + "iris.bukkit.irisenginestatus.max_idle_duration", + C.DARK_PURPLE + "- Max Idle Duration: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_MIN_IDLE_DURATION = TextKey.of( + "iris.bukkit.irisenginestatus.min_idle_duration", + C.DARK_PURPLE + "- Min Idle Duration: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_CACHES = TextKey.of( + "iris.bukkit.irisenginestatus.caches", + C.DARK_PURPLE + "Caches:" + ); + public static final TextKey IRIS_ENGINE_STATUS_RESOURCE = TextKey.of( + "iris.bukkit.irisenginestatus.resource", + C.DARK_PURPLE + "- Resource: " + C.LIGHT_PURPLE + "{value}" + " (" + "{value2}" + ")" + ); + public static final TextKey IRIS_ENGINE_STATUS_2D_STREAM = TextKey.of( + "iris.bukkit.irisenginestatus.2d_stream", + C.DARK_PURPLE + "- 2D Stream: " + C.LIGHT_PURPLE + "{value}" + " (" + "{value2}" + ")" + ); + public static final TextKey IRIS_ENGINE_STATUS_3D_STREAM = TextKey.of( + "iris.bukkit.irisenginestatus.3d_stream", + C.DARK_PURPLE + "- 3D Stream: " + C.LIGHT_PURPLE + "{value}" + " (" + "{value2}" + ")" + ); + public static final TextKey IRIS_ENGINE_STATUS_OTHER = TextKey.of( + "iris.bukkit.irisenginestatus.other", + C.DARK_PURPLE + "- Other: " + C.LIGHT_PURPLE + "{value}" + " (" + "{value2}" + ")" + ); + public static final TextKey IRIS_ENGINE_STATUS_OTHER_2 = TextKey.of( + "iris.bukkit.irisenginestatus.other_2", + C.DARK_PURPLE + "Other:" + ); + public static final TextKey IRIS_ENGINE_STATUS_IRIS_WORLDS = TextKey.of( + "iris.bukkit.irisenginestatus.iris_worlds", + C.DARK_PURPLE + "- Iris Worlds: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_LOADED_CHUNKS = TextKey.of( + "iris.bukkit.irisenginestatus.loaded_chunks", + C.DARK_PURPLE + "- Loaded Chunks: " + C.LIGHT_PURPLE + "{value}" + ); + public static final TextKey IRIS_ENGINE_STATUS_MESSAGE_2 = TextKey.of( + "iris.bukkit.irisenginestatus.message_2", + C.DARK_PURPLE + "-------------------------" + ); + + private static final List KEYS = List.of( + COMMAND_DATAPACK_STARTING_DATAPACK_INGEST, + COMMAND_DATAPACK_CONFIGURED_DATAPACK_IMPORTS, + COMMAND_DATAPACK_MESSAGE, + COMMAND_DATAPACK_INSTALLED_DATAPACKS, + COMMAND_DATAPACK_MESSAGE_2, + COMMAND_DATAPACK_ADD_MODRINTH_URLS_DIMENSION_S_DATAPACKIMPORTS_LIST_THEN_RUN_IRIS, + COMMAND_DEVELOPER_GENHASH_STARTED_CHUNKS, + COMMAND_DEVELOPER_GENHASH_FAILED_AT_CHUNK, + COMMAND_DEVELOPER_GENHASH_GLOBAL_CHUNKS_SOLID, + COMMAND_FIND_NOT_IRIS_WORLD, + COMMAND_FIND_UNKNOWN_STRUCTURE, + COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE, + COMMAND_FIND_LOCATING, + COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE_2, + COMMAND_FIND_LOCATING_2, + COMMAND_FIND_TELEPORTED, + COMMAND_IRIS_SUCCESSFULLY_REMOVED_WORLD_FOLDER, + COMMAND_IRIS_SUCCESSFULLY_REMOVED_WORLD_FOLDER_2, + COMMAND_IRIS_FAILED_REMOVE_WORLD_FOLDER, + COMMAND_OBJECT_NO_PACKS_WITH_OBJECTS_WERE_FOUND_ON_THIS_SERVER, + COMMAND_OBJECT_NO_OBJECTS_PLACE_ACROSS_SELECTED_PACK_S, + COMMAND_OBJECT_OPENING_OBJECT_STUDIO_OBJECTS, + COMMAND_OBJECT_FAILED_OPEN_OBJECT_STUDIO, + COMMAND_PACK_YOU_MUST_SPECIFY_PACK_NAME, + COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS, + COMMAND_PACK_MESSAGE, + COMMAND_PACK_MORE, + COMMAND_STRUCTURE_VERIFYING_STRUCTURES_FROM_WITHIN_CHUNKS, + COMMAND_STUDIO_IMPORTING_VANILLA_CONTENT_INTO, + COMMAND_STUDIO_IMPORTVANILLA_COMPLETE_OBJECTS_STRUCTURES_WRITTEN_FAILED, + COMMAND_STUDIO_TREES_OBJECTS_ARE_UNDER_OBJECTS_VANILLA_REFERENCE_THEM_FROM_BIOME, + COMMAND_STUDIO_NO_OPEN_STUDIO_PROJECTS, + COMMAND_STUDIO_CLOSING_STUDIO, + COMMAND_STUDIO_STUDIO_CLOSE_FAILED, + COMMAND_STUDIO_STUDIO_CLOSE_FAILED_2, + COMMAND_STUDIO_STUDIO_CLOSED_REMAINING_WORLD_FAMILY_CLEANUP_WAS_QUEUED_STARTUP_FALLBACK, + COMMAND_STUDIO_STUDIO_CLOSED, + COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD_TOGGLE_DEBUG_SCOREBOARD, + COMMAND_STUDIO_STUDIO_DEBUG_SCOREBOARD, + COMMAND_STUDIO_COULD_NOT_UPDATE_STUDIO_DEBUG_SCOREBOARD_RIGHT_NOW, + COMMAND_STUDIO_OPENED_INVENTORY, + COMMAND_STUDIO_GENERATING_DATA, + COMMAND_STUDIO_DONE, + COMMAND_STUDIO_MESSAGE, + COMMAND_S_V_C_YOU_LACK_PERMISSION, + IRIS_ENGINE_STATUS_MESSAGE, + IRIS_ENGINE_STATUS_STATUS, + IRIS_ENGINE_STATUS_SERVICE, + IRIS_ENGINE_STATUS_METRICS, + IRIS_ENGINE_STATUS_MAINTENANCE_PERIOD, + IRIS_ENGINE_STATUS_WORKER_PARALLELISM, + IRIS_ENGINE_STATUS_ACTIVE_WORLD_TASKS, + IRIS_ENGINE_STATUS_TECTONIC_PLATES, + IRIS_ENGINE_STATUS_CONFIGURED_RETENTION, + IRIS_ENGINE_STATUS_HEAP_USAGE, + IRIS_ENGINE_STATUS_RESIDENT, + IRIS_ENGINE_STATUS_QUEUED, + IRIS_ENGINE_STATUS_AVERAGE_IDLE_DURATION, + IRIS_ENGINE_STATUS_MAX_IDLE_DURATION, + IRIS_ENGINE_STATUS_MIN_IDLE_DURATION, + IRIS_ENGINE_STATUS_CACHES, + IRIS_ENGINE_STATUS_RESOURCE, + IRIS_ENGINE_STATUS_2D_STREAM, + IRIS_ENGINE_STATUS_3D_STREAM, + IRIS_ENGINE_STATUS_OTHER, + IRIS_ENGINE_STATUS_OTHER_2, + IRIS_ENGINE_STATUS_IRIS_WORLDS, + IRIS_ENGINE_STATUS_LOADED_CHUNKS, + IRIS_ENGINE_STATUS_MESSAGE_2 + ); + + private BukkitCommandMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessagesExtended.java b/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessagesExtended.java new file mode 100644 index 000000000..356d1ee3d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessagesExtended.java @@ -0,0 +1,1045 @@ +package art.arcane.iris.core.localization; + +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; + +public final class BukkitCommandMessagesExtended { + public static final TextKey COMMAND_DEVELOPER_WORLD_IS_NULL = TextKey.of( + "iris.bukkit.commanddeveloper.world_is_null", + C.RED + "World is null." + ); + public static final TextKey COMMAND_DEVELOPER_TARGET_IRIS_WORLD_BEFORE_READING_MANTLE_DUMP = TextKey.of( + "iris.bukkit.commanddeveloper.target_iris_world_before_reading_mantle_dump", + C.RED + "Target an Iris world before reading a mantle dump." + ); + public static final TextKey COMMAND_DEVELOPER_UPGRADING = TextKey.of( + "iris.bukkit.commanddeveloper.upgrading", + C.GREEN + "Upgrading to " + "{value}" + "..." + ); + public static final TextKey COMMAND_DEVELOPER_DONE_UPGRADING_YOU_CAN_NOW_UPDATE_YOUR_SERVER_VERSION = TextKey.of( + "iris.bukkit.commanddeveloper.done_upgrading_you_can_now_update_your_server_version", + C.GREEN + "Done upgrading! You can now update your server version to " + "{value}" + ); + public static final TextKey COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER = TextKey.of( + "iris.bukkit.commanddeveloper.radius_must_be_0_greater", + C.RED + "Radius must be 0 or greater." + ); + public static final TextKey COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD = TextKey.of( + "iris.bukkit.commanddeveloper.this_is_not_iris_world", + C.RED + "This is not an Iris world." + ); + public static final TextKey COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL = TextKey.of( + "iris.bukkit.commanddeveloper.engine_access_this_world_is_null", + C.RED + "The engine access for this world is null." + ); + public static final TextKey COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR = TextKey.of( + "iris.bukkit.commanddeveloper.delete_started_chunk_s_around_clearing_blocks_air", + C.GREEN + "Delete started: " + C.GOLD + "{chunks}" + C.GREEN + " chunk(s) around " + C.GOLD + "{centerX}" + "," + "{centerZ}" + C.GREEN + ". Clearing blocks to air." + ); + public static final TextKey COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_2 = TextKey.of( + "iris.bukkit.commanddeveloper.radius_must_be_0_greater_2", + C.RED + "Radius must be 0 or greater." + ); + public static final TextKey COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN = TextKey.of( + "iris.bukkit.commanddeveloper.you_must_be_iris_world_use_regen", + C.RED + "You must be in an Iris world to use regen." + ); + public static final TextKey COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST = TextKey.of( + "iris.bukkit.commanddeveloper.engine_access_this_world_is_null_generate_nearby_chunks_first", + C.RED + "The engine access for this world is null. Generate nearby chunks first." + ); + public static final TextKey COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE = TextKey.of( + "iris.bukkit.commanddeveloper.regen_started_chunk_s_around_deleting_regenerating_place", + C.GREEN + "Regen started: " + C.GOLD + "{chunks}" + C.GREEN + " chunk(s) around " + C.GOLD + "{centerX}" + "," + "{centerZ}" + C.GREEN + ". Deleting and regenerating in place." + ); + public static final TextKey COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_3 = TextKey.of( + "iris.bukkit.commanddeveloper.radius_must_be_0_greater_3", + C.RED + "Radius must be 0 or greater." + ); + public static final TextKey COMMAND_DEVELOPER_TARGET_MUST_BE_IRIS_WORLD = TextKey.of( + "iris.bukkit.commanddeveloper.target_must_be_iris_world", + C.RED + "Target must be an Iris world." + ); + public static final TextKey COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_2 = TextKey.of( + "iris.bukkit.commanddeveloper.engine_access_this_world_is_null_2", + C.RED + "The engine access for this world is null." + ); + public static final TextKey COMMAND_DEVELOPER_GOLDENHASH_STARTED_CHUNK_S_AROUND_BUFFERS_WORLD_UNTOUCHED = TextKey.of( + "iris.bukkit.commanddeveloper.goldenhash_started_chunk_s_around_buffers_world_untouched", + C.GREEN + "GoldenHash started: " + C.GOLD + "{chunks}" + C.GREEN + " chunk(s) around " + C.GOLD + "{centerX}" + "," + "{centerZ}" + C.GREEN + " in buffers (world untouched)." + ); + public static final TextKey COMMAND_EDIT_PLAYERS_ONLY = TextKey.of( + "iris.bukkit.commandedit.players_only", + C.RED + "Players only!" + ); + public static final TextKey COMMAND_EDIT_NO_STUDIO_WORLD_IS_OPEN = TextKey.of( + "iris.bukkit.commandedit.no_studio_world_is_open", + C.RED + "No studio world is open!" + ); + public static final TextKey COMMAND_EDIT_YOU_MUST_BE_STUDIO_WORLD = TextKey.of( + "iris.bukkit.commandedit.you_must_be_studio_world", + C.RED + "You must be in a studio world!" + ); + public static final TextKey COMMAND_EDIT_CANNOT_OPEN_FILES_HEADLESS_ENVIRONMENTS = TextKey.of( + "iris.bukkit.commandedit.cannot_open_files_headless_environments", + C.RED + "Cannot open files in headless environments!" + ); + public static final TextKey COMMAND_EDIT_DESKTOP_IS_NOT_SUPPORTED_BY_THIS_ENVIRONMENT = TextKey.of( + "iris.bukkit.commandedit.desktop_is_not_supported_by_this_environment", + C.RED + "Desktop is not supported by this environment!" + ); + public static final TextKey COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM = TextKey.of( + "iris.bukkit.commandedit.cannot_find_file_perhaps_it_was_not_loaded_directly_from", + C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?" + ); + public static final TextKey COMMAND_EDIT_OPENING_VSCODE = TextKey.of( + "iris.bukkit.commandedit.opening_vscode", + C.GREEN + "Opening " + "{value}" + " " + "{value2}" + " in VSCode! " + ); + public static final TextKey COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST = TextKey.of( + "iris.bukkit.commandedit.cant_find_file_registrant_does_not_exist", + C.RED + "Cant find the file. Or registrant does not exist" + ); + public static final TextKey COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_2 = TextKey.of( + "iris.bukkit.commandedit.cannot_find_file_perhaps_it_was_not_loaded_directly_from_2", + C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?" + ); + public static final TextKey COMMAND_EDIT_OPENING_VSCODE_2 = TextKey.of( + "iris.bukkit.commandedit.opening_vscode_2", + C.GREEN + "Opening " + "{value}" + " " + "{value2}" + " in VSCode! " + ); + public static final TextKey COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_2 = TextKey.of( + "iris.bukkit.commandedit.cant_find_file_registrant_does_not_exist_2", + C.RED + "Cant find the file. Or registrant does not exist" + ); + public static final TextKey COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_3 = TextKey.of( + "iris.bukkit.commandedit.cannot_find_file_perhaps_it_was_not_loaded_directly_from_3", + C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?" + ); + public static final TextKey COMMAND_EDIT_OPENING_VSCODE_3 = TextKey.of( + "iris.bukkit.commandedit.opening_vscode_3", + C.GREEN + "Opening " + "{value}" + " " + "{value2}" + " in VSCode! " + ); + public static final TextKey COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_3 = TextKey.of( + "iris.bukkit.commandedit.cant_find_file_registrant_does_not_exist_3", + C.RED + "Cant find the file. Or registrant does not exist" + ); + public static final TextKey COMMAND_FIND_NOT_IRIS_WORLD = TextKey.of( + "iris.bukkit.commandfind.not_iris_world_5", + C.GOLD + "Not in an Iris World!" + ); + public static final TextKey COMMAND_FIND_NOT_IRIS_WORLD_2 = TextKey.of( + "iris.bukkit.commandfind.not_iris_world_2", + C.GOLD + "Not in an Iris World!" + ); + public static final TextKey COMMAND_FIND_NOT_IRIS_WORLD_3 = TextKey.of( + "iris.bukkit.commandfind.not_iris_world_3", + C.GOLD + "Not in an Iris World!" + ); + public static final TextKey COMMAND_FIND_NOT_IRIS_WORLD_4 = TextKey.of( + "iris.bukkit.commandfind.not_iris_world_4", + C.GOLD + "Not in an Iris World!" + ); + public static final TextKey COMMAND_FIND_OBJECT_STUDIO_TELEPORTING = TextKey.of( + "iris.bukkit.commandfind.object_studio_teleporting", + C.GREEN + "Object Studio: teleporting to " + "{object}" + ); + public static final TextKey COMMAND_FIND_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS = TextKey.of( + "iris.bukkit.commandfind.is_not_configured_any_region_biome_object_placements", + C.RED + "{object}" + " is not configured in any region/biome object placements." + ); + public static final TextKey COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_IRIS_CREATING_WORLDS_AS_IRIS = TextKey.of( + "iris.bukkit.commandiris.you_cannot_use_world_name_iris_creating_worlds_as_iris", + C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds." + ); + public static final TextKey COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD = TextKey.of( + "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead", + C.RED + "May we suggest the name \"IrisWorld\" instead?" + ); + public static final TextKey COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_BENCHMARK_CREATING_WORLDS_AS_IRIS = TextKey.of( + "iris.bukkit.commandiris.you_cannot_use_world_name_benchmark_creating_worlds_as_iris", + C.RED + "You cannot use the world name \"benchmark\" for creating worlds as Iris uses this directory for Benchmarking Packs." + ); + public static final TextKey COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD_2 = TextKey.of( + "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2", + C.RED + "May we suggest the name \"IrisWorld\" instead?" + ); + public static final TextKey COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS = TextKey.of( + "iris.bukkit.commandiris.that_folder_already_exists", + C.RED + "That folder already exists!" + ); + public static final TextKey COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND = TextKey.of( + "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend", + C.YELLOW + "Try one of: overworld, vanilla, flat, theend" + ); + public static final TextKey COMMAND_IRIS_DIMENSION_NOT_FOUND = TextKey.of( + "iris.bukkit.commandiris.dimension_not_found", + C.RED + "Could not find dimension " + C.WHITE + "{dimension}" + C.RED + "." + ); + public static final TextKey COMMAND_IRIS_INSTALL_PACK_AND_RESTART = TextKey.of( + "iris.bukkit.commandiris.install_pack_and_restart", + C.YELLOW + "Install it with " + C.AQUA + "{command}" + C.YELLOW + " and restart the server." + ); + public static final TextKey COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD = TextKey.of( + "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load", + C.GREEN + "World staging completed. Iris is restarting the server to generate/load \"" + "{worldName}" + "\"." + ); + public static final TextKey COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS = TextKey.of( + "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details", + C.RED + "Exception raised during creation. See the console for more details." + ); + public static final TextKey COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD = TextKey.of( + "iris.bukkit.commandiris.successfully_created_your_world", + C.GREEN + "Successfully created your world!" + ); + public static final TextKey COMMAND_IRIS_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA = TextKey.of( + "iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia", + C.YELLOW + "Runtime world creation is disabled on Folia." + ); + public static final TextKey COMMAND_IRIS_PREPARING_WORLD_FILES_BUKKIT_YML_NEXT_STARTUP = TextKey.of( + "iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup", + C.YELLOW + "Preparing world files and bukkit.yml for next startup..." + ); + public static final TextKey COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION = TextKey.of( + "iris.bukkit.commandiris.failed_stage_world_files_dimension", + C.RED + "Failed to stage world files for dimension \"" + "{value}" + "\"." + ); + public static final TextKey COMMAND_IRIS_STAGED_IRIS_WORLD_WITH_GENERATOR_IRIS_SEED = TextKey.of( + "iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed", + C.GREEN + "Staged Iris world \"" + "{name}" + "\" with generator Iris:" + "{value}" + " and seed " + "{seed}" + "." + ); + public static final TextKey COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML = TextKey.of( + "iris.bukkit.commandiris.failed_update_bukkit_yml", + C.RED + "Failed to update bukkit.yml: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST = TextKey.of( + "iris.bukkit.commandiris.specified_player_does_not_exist", + C.RED + "The specified player does not exist." + ); + public static final TextKey COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE = TextKey.of( + "iris.bukkit.commandiris.iris_v_by_volmit_software", + C.GREEN + "Iris v" + "{value}" + " by Volmit Software" + ); + public static final TextKey COMMAND_IRIS_TO = TextKey.of( + "iris.bukkit.commandiris.to", + C.GREEN + "" + "{value}" + " to " + "{value2}" + ); + public static final TextKey COMMAND_IRIS_TOTAL_HEIGHT = TextKey.of( + "iris.bukkit.commandiris.total_height", + C.GREEN + "Total Height: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_IRIS_WORLDS = TextKey.of( + "iris.bukkit.commandiris.iris_worlds", + C.BLUE + "Iris Worlds: " + ); + public static final TextKey COMMAND_IRIS_MESSAGE = TextKey.of( + "iris.bukkit.commandiris.message", + C.IRIS + "- " + "{value}" + ); + public static final TextKey COMMAND_IRIS_BUKKIT_WORLDS = TextKey.of( + "iris.bukkit.commandiris.bukkit_worlds", + C.GOLD + "Bukkit Worlds: " + ); + public static final TextKey COMMAND_IRIS_MESSAGE_2 = TextKey.of( + "iris.bukkit.commandiris.message_2", + C.GRAY + "- " + "{value}" + ); + public static final TextKey COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS = TextKey.of( + "iris.bukkit.commandiris.this_is_not_iris_world_iris_worlds", + C.RED + "This is not an Iris world. Iris worlds: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_REMOVING_WORLD = TextKey.of( + "iris.bukkit.commandiris.removing_world", + C.GREEN + "Removing world: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_FAILED_EVACUATE_WORLD = TextKey.of( + "iris.bukkit.commandiris.failed_evacuate_world", + C.RED + "Failed to evacuate world: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_FAILED_UNLOAD_WORLD = TextKey.of( + "iris.bukkit.commandiris.failed_unload_world", + C.RED + "Failed to unload world: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_SUCCESSFULLY_REMOVED_FROM_BUKKIT_YML = TextKey.of( + "iris.bukkit.commandiris.successfully_removed_from_bukkit_yml", + C.GREEN + "Successfully removed " + "{value}" + " from bukkit.yml" + ); + public static final TextKey COMMAND_IRIS_LOOKS_LIKE_WORLD_WAS_ALREADY_REMOVED_FROM_BUKKIT_YML = TextKey.of( + "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml", + C.YELLOW + "Looks like the world was already removed from bukkit.yml" + ); + public static final TextKey COMMAND_IRIS_FAILED_SAVE_BUKKIT_YML_BECAUSE = TextKey.of( + "iris.bukkit.commandiris.failed_save_bukkit_yml_because", + C.RED + "Failed to save bukkit.yml because of " + "{value}" + ); + public static final TextKey COMMAND_IRIS_SET_DEBUG = TextKey.of( + "iris.bukkit.commandiris.set_debug", + C.GREEN + "Set debug to: " + "{to}" + ); + public static final TextKey COMMAND_IRIS_YOU_MUST_BE_IRIS_WORLD = TextKey.of( + "iris.bukkit.commandiris.you_must_be_iris_world", + C.RED + "You must be in an Iris world" + ); + public static final TextKey COMMAND_IRIS_SENDING_METRICS = TextKey.of( + "iris.bukkit.commandiris.sending_metrics", + C.GREEN + "Sending metrics..." + ); + public static final TextKey COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_2 = TextKey.of( + "iris.bukkit.commandiris.this_is_not_iris_world_iris_worlds_2", + C.RED + "This is not an Iris world. Iris worlds: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_UNLOADING_WORLD = TextKey.of( + "iris.bukkit.commandiris.unloading_world", + C.GREEN + "Unloading world: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_WORLD_UNLOADED_SUCCESSFULLY = TextKey.of( + "iris.bukkit.commandiris.world_unloaded_successfully", + C.GREEN + "World unloaded successfully." + ); + public static final TextKey COMMAND_IRIS_FAILED_UNLOAD_WORLD_2 = TextKey.of( + "iris.bukkit.commandiris.failed_unload_world_2", + C.RED + "Failed to unload the world." + ); + public static final TextKey COMMAND_IRIS_FAILED_UNLOAD_WORLD_3 = TextKey.of( + "iris.bukkit.commandiris.failed_unload_world_3", + C.RED + "Failed to unload the world: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_DOESNT_EXIST_ON_SERVER = TextKey.of( + "iris.bukkit.commandiris.doesnt_exist_on_server", + C.YELLOW + "{logicalWorldName}" + " Doesnt exist on the server." + ); + public static final TextKey COMMAND_IRIS_GENERATOR = TextKey.of( + "iris.bukkit.commandiris.generator", + C.BLUE + "Generator: " + "{dimension}" + ); + public static final TextKey COMMAND_IRIS_IS_NOT_IRIS_WORLD = TextKey.of( + "iris.bukkit.commandiris.is_not_iris_world", + C.GOLD + "{logicalWorldName}" + " is not an iris world." + ); + public static final TextKey COMMAND_IRIS_COULD_NOT_DETERMINE_IRIS_DIMENSION = TextKey.of( + "iris.bukkit.commandiris.could_not_determine_iris_dimension", + C.RED + "Could not determine Iris dimension for " + "{logicalWorldName}" + "." + ); + public static final TextKey COMMAND_IRIS_LOADING_WORLD = TextKey.of( + "iris.bukkit.commandiris.loading_world", + C.GREEN + "Loading world: " + "{logicalWorldName}" + ); + public static final TextKey COMMAND_IRIS_FOLIA_CANNOT_LOAD_NEW_WORLDS_AT_RUNTIME_RESTART_SERVER_LOAD = TextKey.of( + "iris.bukkit.commandiris.folia_cannot_load_new_worlds_at_runtime_restart_server_load", + C.YELLOW + "Folia cannot load new worlds at runtime. Restart the server to load \"" + "{logicalWorldName}" + "\"." + ); + public static final TextKey COMMAND_IRIS_LOADED_SUCCESSFULLY = TextKey.of( + "iris.bukkit.commandiris.loaded_successfully", + C.GREEN + "{logicalWorldName}" + " loaded successfully." + ); + public static final TextKey COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_3 = TextKey.of( + "iris.bukkit.commandiris.this_is_not_iris_world_iris_worlds_3", + C.RED + "This is not an Iris world. Iris worlds: " + "{value}" + ); + public static final TextKey COMMAND_IRIS_EVACUATING_WORLD = TextKey.of( + "iris.bukkit.commandiris.evacuating_world", + C.GREEN + "Evacuating world" + "{value}" + ); + public static final TextKey COMMAND_OBJECT_OBJECT_SIZE = TextKey.of( + "iris.bukkit.commandobject.object_size", + "Object Size: " + "{value}" + " * " + "{value2}" + " * " + "{value3}" + "" + ); + public static final TextKey COMMAND_OBJECT_BLOCKS_USED = TextKey.of( + "iris.bukkit.commandobject.blocks_used", + "Blocks Used: " + "{value}" + ); + public static final TextKey COMMAND_OBJECT_BLOCKS_OBJECT = TextKey.of( + "iris.bukkit.commandobject.blocks_object", + "== Blocks in object ==" + ); + public static final TextKey COMMAND_OBJECT_OTHER_BLOCK_TYPES = TextKey.of( + "iris.bukkit.commandobject.other_block_types", + " + " + "{value}" + " other block types" + ); + public static final TextKey COMMAND_OBJECT_CURRENT_OBJECT_SIZE = TextKey.of( + "iris.bukkit.commandobject.current_object_size", + "Current Object Size: " + "{value}" + " * " + "{value2}" + " * " + "{value3}" + ); + public static final TextKey COMMAND_OBJECT_NEW_OBJECT_SIZE = TextKey.of( + "iris.bukkit.commandobject.new_object_size", + "New Object Size: " + "{value}" + " * " + "{value2}" + " * " + "{value3}" + ); + public static final TextKey COMMAND_OBJECT_FAILED_SAVE_OBJECT = TextKey.of( + "iris.bukkit.commandobject.failed_save_object", + "Failed to save object " + "{value}" + ": " + "{value2}" + ); + public static final TextKey COMMAND_OBJECT_NO_OBJECTS_MATCHED = TextKey.of( + "iris.bukkit.commandobject.no_objects_matched", + C.RED + "No objects matched: " + "{target}" + ); + public static final TextKey COMMAND_OBJECT_PLAUSIBILIZE_REACH_QUEUED_OBJECT_S = TextKey.of( + "iris.bukkit.commandobject.plausibilize_reach_queued_object_s", + C.IRIS + "Plausibilize [reach=" + "{reach}" + "{value}" + "] queued " + "{value2}" + " object(s)" + ); + public static final TextKey COMMAND_OBJECT_HOLD_YOUR_WAND = TextKey.of( + "iris.bukkit.commandobject.hold_your_wand", + "Hold your wand." + ); + public static final TextKey COMMAND_OBJECT_NO_AREA_SELECTED = TextKey.of( + "iris.bukkit.commandobject.no_area_selected", + "No area selected." + ); + public static final TextKey COMMAND_OBJECT_READY_YOUR_WAND = TextKey.of( + "iris.bukkit.commandobject.ready_your_wand", + "Ready your Wand." + ); + public static final TextKey COMMAND_OBJECT_READY_YOUR_WAND_2 = TextKey.of( + "iris.bukkit.commandobject.ready_your_wand_2", + "Ready your Wand." + ); + public static final TextKey COMMAND_OBJECT_INDICATED_SCALE_EXCEEDS_MAXIMUM_DOWNSCALED_MAXIMUM = TextKey.of( + "iris.bukkit.commandobject.indicated_scale_exceeds_maximum_downscaled_maximum", + C.YELLOW + "Indicated scale exceeds maximum. Downscaled to maximum: " + "{maxScale}" + ); + public static final TextKey COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB = TextKey.of( + "iris.bukkit.commandobject.updated_wand_objects_iob", + "Updated wand for " + "objects/" + "{value}" + ".iob " + ); + public static final TextKey COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB = TextKey.of( + "iris.bukkit.commandobject.given_new_wand_objects_iob", + "Given new wand for " + "objects/" + "{value}" + ".iob " + ); + public static final TextKey COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2 = TextKey.of( + "iris.bukkit.commandobject.updated_wand_objects_iob_2", + "Updated wand for " + "objects/" + "{value}" + ".iob " + ); + public static final TextKey COMMAND_OBJECT_PLACED = TextKey.of( + "iris.bukkit.commandobject.placed", + C.IRIS + "Placed " + "{object}" + ); + public static final TextKey COMMAND_OBJECT_YOU_NEED_HOLD_YOUR_WAND = TextKey.of( + "iris.bukkit.commandobject.you_need_hold_your_wand", + C.YELLOW + "You need to hold your wand!" + ); + public static final TextKey COMMAND_OBJECT_FILE_ALREADY_EXISTS_SET_OVERWRITE_TRUE_OVERWRITE_IT = TextKey.of( + "iris.bukkit.commandobject.file_already_exists_set_overwrite_true_overwrite_it", + C.RED + "File already exists. Set overwrite=true to overwrite it." + ); + public static final TextKey COMMAND_OBJECT_FAILED_SAVE_OBJECT_BECAUSE_IOEXCEPTION = TextKey.of( + "iris.bukkit.commandobject.failed_save_object_because_ioexception", + C.RED + "Failed to save object because of an IOException: " + "{value}" + ); + public static final TextKey COMMAND_OBJECT_SUCCESSFULLY_OBJECT_SAVED_OBJECTS = TextKey.of( + "iris.bukkit.commandobject.successfully_object_saved_objects", + C.GREEN + "Successfully object to saved: " + "{value}" + "/objects/" + "{name}" + ); + public static final TextKey COMMAND_OBJECT_HOLD_YOUR_WAND_2 = TextKey.of( + "iris.bukkit.commandobject.hold_your_wand_2", + "Hold your wand." + ); + public static final TextKey COMMAND_OBJECT_NO_AREA_SELECTED_2 = TextKey.of( + "iris.bukkit.commandobject.no_area_selected_2", + "No area selected." + ); + public static final TextKey COMMAND_OBJECT_REVERTED_PASTES = TextKey.of( + "iris.bukkit.commandobject.reverted_pastes", + C.BLUE + "Reverted " + "{actualReverts}" + C.BLUE + " pastes!" + ); + public static final TextKey COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW = TextKey.of( + "iris.bukkit.commandobject.you_can_t_get_worldedit_selection_without_worldedit_you_know", + C.RED + "You can't get a WorldEdit selection without WorldEdit, you know." + ); + public static final TextKey COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD = TextKey.of( + "iris.bukkit.commandobject.you_don_t_have_worldedit_selection_this_world", + C.RED + "You don't have a WorldEdit selection in this world." + ); + public static final TextKey COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT = TextKey.of( + "iris.bukkit.commandobject.fresh_wand_with_your_current_worldedit_selection_on_it", + C.GREEN + "A fresh wand with your current WorldEdit selection on it!" + ); + public static final TextKey COMMAND_OBJECT_POOF_GOOD_LUCK_BUILDING = TextKey.of( + "iris.bukkit.commandobject.poof_good_luck_building", + C.GREEN + "Poof! Good luck building!" + ); + public static final TextKey COMMAND_OBJECT_HOLD_YOUR_WAND_3 = TextKey.of( + "iris.bukkit.commandobject.hold_your_wand_3", + C.YELLOW + "Hold your wand!" + ); + public static final TextKey COMMAND_OBJECT_NO_AREA_SELECTED_3 = TextKey.of( + "iris.bukkit.commandobject.no_area_selected_3", + "No area selected." + ); + public static final TextKey COMMAND_OBJECT_AUTO_SELECT_COMPLETE = TextKey.of( + "iris.bukkit.commandobject.auto_select_complete", + C.GREEN + "Auto-select complete!" + ); + public static final TextKey COMMAND_OBJECT_HOLD_YOUR_WAND_4 = TextKey.of( + "iris.bukkit.commandobject.hold_your_wand_4", + C.YELLOW + "Hold your wand!" + ); + public static final TextKey COMMAND_OBJECT_NO_AREA_SELECTED_4 = TextKey.of( + "iris.bukkit.commandobject.no_area_selected_4", + "No area selected." + ); + public static final TextKey COMMAND_OBJECT_AUTO_SELECT_COMPLETE_2 = TextKey.of( + "iris.bukkit.commandobject.auto_select_complete_2", + C.GREEN + "Auto-select complete!" + ); + public static final TextKey COMMAND_PREGEN_PREGEN_RADIUS_MUST_BE_GREATER_THAN_ZERO_BLOCKS = TextKey.of( + "iris.bukkit.commandpregen.pregen_radius_must_be_greater_than_zero_blocks", + C.RED + "Pregen radius must be greater than zero blocks." + ); + public static final TextKey COMMAND_STUDIO_REGIONS_RADIUS_OUT_OF_RANGE = TextKey.of( + "iris.bukkit.commandstudio.regions_radius_out_of_range", + C.RED + "Radius must be between 1 and 2048 chunks." + ); + public static final TextKey COMMAND_STUDIO_REGIONS_SCAN_FAILED = TextKey.of( + "iris.bukkit.commandstudio.regions_scan_failed", + C.RED + "Region scan failed: {value}" + ); + public static final TextKey COMMAND_PREGEN_STRICT_SERIAL_PREGENERATION_REQUIRES_PAPER_PAPER_COMPATIBLE_SERVER = TextKey.of( + "iris.bukkit.commandpregen.strict_serial_pregeneration_requires_paper_paper_compatible_server", + C.RED + "Strict serial pregeneration requires Paper or a Paper-compatible server." + ); + public static final TextKey COMMAND_PREGEN_ENGINE_ACCESS_THIS_WORLD_IS_NULL = TextKey.of( + "iris.bukkit.commandpregen.engine_access_this_world_is_null", + C.RED + "The engine access for this world is null!" + ); + public static final TextKey COMMAND_PREGEN_PLEASE_MAKE_SURE_WORLD_IS_LOADED_ENGINE_IS_INITIALIZED_GENERATE = TextKey.of( + "iris.bukkit.commandpregen.please_make_sure_world_is_loaded_engine_is_initialized_generate", + C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example." + ); + public static final TextKey COMMAND_PREGEN_FAILED_START_PREGENERATION_SEE_CONSOLE_DETAILS = TextKey.of( + "iris.bukkit.commandpregen.failed_start_pregeneration_see_console_details", + C.RED + "Failed to start pregeneration. See console for details." + ); + public static final TextKey COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_STOP = TextKey.of( + "iris.bukkit.commandpregen.no_active_pregeneration_tasks_stop", + C.YELLOW + "No active pregeneration tasks to stop" + ); + public static final TextKey COMMAND_PREGEN_PAUSED_UNPAUSED_PREGENERATION_TASK_NOW = TextKey.of( + "iris.bukkit.commandpregen.paused_unpaused_pregeneration_task_now", + C.GREEN + "Paused/unpaused pregeneration task, now: " + "{value}" + "." + ); + public static final TextKey COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_PAUSE_UNPAUSE = TextKey.of( + "iris.bukkit.commandpregen.no_active_pregeneration_tasks_pause_unpause", + C.YELLOW + "No active pregeneration tasks to pause/unpause." + ); + public static final TextKey COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASK = TextKey.of( + "iris.bukkit.commandpregen.no_active_pregeneration_task", + C.YELLOW + "No active pregeneration task." + ); + public static final TextKey COMMAND_PREGEN_PREGEN = TextKey.of( + "iris.bukkit.commandpregen.pregen", + C.GREEN + "Pregen " + C.GOLD + "{world}" + C.GREEN + ": " + C.GOLD + "{value}" + "/" + "{value2}" + C.GREEN + " (" + C.GOLD + "{value3}" + "%" + C.GREEN + ")" + "{value4}" + ); + public static final TextKey COMMAND_PREGEN_SPEED_S_ETA_ELAPSED_METHOD = TextKey.of( + "iris.bukkit.commandpregen.speed_s_eta_elapsed_method", + C.GREEN + "Rates: " + C.GOLD + "overall {overall}/s, 10s {tenSecond}/s, 30s {thirtySecond}/s, 60s {sixtySecond}/s" + + C.GREEN + " ETA: " + C.GOLD + "{eta}" + C.GREEN + " Elapsed: " + C.GOLD + "{elapsed}" + + C.GREEN + " Method: " + C.GOLD + "{method}" + "{failures}" + ); + public static final TextKey COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION = TextKey.of( + "iris.bukkit.commandstructure.could_not_resolve_pack_dimension", + C.RED + "Could not resolve the pack for dimension " + "{value}" + ); + public static final TextKey COMMAND_STRUCTURE_WROTE_STRUCTURE_INDEX = TextKey.of( + "iris.bukkit.commandstructure.wrote_structure_index", + C.GREEN + "Wrote structure index: " + C.WHITE + "{value}" + ); + public static final TextKey COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_2 = TextKey.of( + "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_2", + C.RED + "Could not resolve the pack for dimension " + "{value}" + ); + public static final TextKey COMMAND_STRUCTURE_IMPORTING_ALL_VANILLA_DATAPACK_STRUCTURES_INTO_OVERWRITE = TextKey.of( + "iris.bukkit.commandstructure.importing_all_vanilla_datapack_structures_into_overwrite", + C.GREEN + "Importing all vanilla & datapack structures into " + C.WHITE + "{value}" + C.GREEN + " (overwrite)..." + ); + public static final TextKey COMMAND_STRUCTURE_IMPORT_COMPLETE_STRUCTURES_OBJECTS_WRITTEN_FAILED = TextKey.of( + "iris.bukkit.commandstructure.import_complete_structures_objects_written_failed", + C.GREEN + "Import complete: " + C.WHITE + "{imported}" + C.GREEN + " structures/objects written, " + C.WHITE + "{failed}" + C.GREEN + " failed." + ); + public static final TextKey COMMAND_STRUCTURE_REFERENCE_THEM_FROM_BIOME_REGION_DIMENSION_STRUCTURES_LIST_RUN_IRIS = TextKey.of( + "iris.bukkit.commandstructure.reference_them_from_biome_region_dimension_structures_list_run_iris", + C.GRAY + "Reference them from a biome/region/dimension 'structures' list, or run /iris structure list " + "{value}" + " to refresh the index. Regenerate chunks for changes to take effect." + ); + public static final TextKey COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_3 = TextKey.of( + "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_3", + C.RED + "Could not resolve the pack for dimension " + "{value}" + ); + public static final TextKey COMMAND_STRUCTURE_CAPTURED_STRUCTURES_PLACE_THEM_FROM_STRUCTURES_LIST_REGENERATE_CHUNKS_DELETE = TextKey.of( + "iris.bukkit.commandstructure.captured_structures_place_them_from_structures_list_regenerate_chunks_delete", + C.GRAY + "Captured " + "{value}" + " structures. Place them from a 'structures' list and regenerate chunks. Delete a structures/*.json to re-capture it." + ); + public static final TextKey COMMAND_STRUCTURE_NO_LOADED_IRIS_WORLD_FOUND_JOIN_CREATE_ONE_FIRST_SEARCH = TextKey.of( + "iris.bukkit.commandstructure.no_loaded_iris_world_found_join_create_one_first_search", + C.RED + "No loaded Iris world found for " + "{value}" + ". Join or create one first (the search runs against a live world)." + ); + public static final TextKey COMMAND_STRUCTURE_SELECTED_IRIS_WORLD_HAS_NO_ACTIVE_GENERATOR_ENGINE = TextKey.of( + "iris.bukkit.commandstructure.selected_iris_world_has_no_active_generator_engine", + C.RED + "The selected Iris world has no active generator engine." + ); + public static final TextKey COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_4 = TextKey.of( + "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_4", + C.RED + "Could not resolve the pack for dimension " + "{value}" + ); + public static final TextKey COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK = TextKey.of( + "iris.bukkit.commandstructure.no_iris_structure_this_pack", + C.RED + "No iris structure '" + "{structure}" + "' in this pack" + ); + public static final TextKey COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES_CHECK_STARTPOOL = TextKey.of( + "iris.bukkit.commandstructure.structure_assembled_0_pieces_check_startpool", + C.RED + "Structure '" + "{structure}" + "' assembled 0 pieces (check startPool '" + "{value}" + "')" + ); + public static final TextKey COMMAND_STRUCTURE_STRUCTURE_PIECES_FOOTPRINT_X_BLOCKS_SAMPLE_SEED_1234 = TextKey.of( + "iris.bukkit.commandstructure.structure_pieces_footprint_x_blocks_sample_seed_1234", + C.GREEN + "Structure '" + "{structure}" + "': " + C.WHITE + "{value}" + C.GREEN + " pieces, footprint " + C.WHITE + "{value2}" + "x" + "{value3}" + C.GREEN + " blocks (sample seed 1234)" + ); + public static final TextKey COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_5 = TextKey.of( + "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5", + C.RED + "Could not resolve the pack for dimension " + "{value}" + ); + public static final TextKey COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK_2 = TextKey.of( + "iris.bukkit.commandstructure.no_iris_structure_this_pack_2", + C.RED + "No iris structure '" + "{structure}" + "' in this pack" + ); + public static final TextKey COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES = TextKey.of( + "iris.bukkit.commandstructure.structure_assembled_0_pieces", + C.RED + "Structure '" + "{structure}" + "' assembled 0 pieces" + ); + public static final TextKey COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION = TextKey.of( + "iris.bukkit.commandstructure.placed_pieces_at_your_location", + C.GREEN + "Placed '" + "{structure}" + "' (" + "{value}" + " pieces, " + + "{value2}" + " block changes) at your location." + ); + public static final TextKey COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS = TextKey.of( + "iris.bukkit.commandstructure.placement_changed_no_blocks", + C.RED + "Structure '" + "{structure}" + "' assembled " + "{value}" + + " pieces but changed 0 blocks at your location. Check that the selected variants contain " + + "non-air blocks and that the placement is above the world's minimum height." + ); + public static final TextKey COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED = TextKey.of( + "iris.bukkit.commandstudio.opening_studio_pack_seed", + C.GREEN + "Opening studio for the \"" + "{value}" + "\" pack (seed: " + "{seed}" + ")" + ); + public static final TextKey COMMAND_STUDIO_PROVIDE_DIMENSION_PACK_IRIS_STD_IMPORTVANILLA_PACK_DIMENSION = TextKey.of( + "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension", + C.RED + "Provide a dimension pack: /iris std importvanilla pack=" + ); + public static final TextKey COMMAND_STUDIO_COULD_NOT_RESOLVE_PACK_DIMENSION = TextKey.of( + "iris.bukkit.commandstudio.could_not_resolve_pack_dimension", + C.RED + "Could not resolve the pack for dimension " + "{value}" + ); + public static final TextKey COMMAND_STUDIO_OPENING_VSCODE_PACK = TextKey.of( + "iris.bukkit.commandstudio.opening_vscode_pack", + C.GREEN + "Opening VSCode for the \"" + "{value}" + "\" pack" + ); + public static final TextKey COMMAND_STUDIO_PACK_HAS_VERSION = TextKey.of( + "iris.bukkit.commandstudio.pack_has_version", + C.GREEN + "The \"" + "{value}" + "\" pack has version: " + "{value2}" + ); + public static final TextKey COMMAND_STUDIO_OPENING_NOISE_EXPLORER = TextKey.of( + "iris.bukkit.commandstudio.opening_noise_explorer", + C.GREEN + "Opening Noise Explorer!" + ); + public static final TextKey COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE = TextKey.of( + "iris.bukkit.commandstudio.cannot_add_items_virtual_inventory_because", + C.RED + "Cannot add items to virtual inventory because of: " + "{value}" + ); + public static final TextKey COMMAND_STUDIO_OPENING_INVENTORY_NOW = TextKey.of( + "iris.bukkit.commandstudio.opening_inventory_now", + C.GREEN + "Opening inventory now!" + ); + public static final TextKey COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD = TextKey.of( + "iris.bukkit.commandstudio.only_works_iris_world", + C.RED + "Only works in an Iris world!" + ); + public static final TextKey COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD = TextKey.of( + "iris.bukkit.commandstudio.you_need_be_specify_iris_generated_world", + C.RED + "You need to be in or specify an Iris-generated world!" + ); + public static final TextKey COMMAND_STUDIO_OPENING_MAP = TextKey.of( + "iris.bukkit.commandstudio.opening_map", + C.GREEN + "Opening map!" + ); + public static final TextKey COMMAND_STUDIO_CALCULATING_PERFORMANCE_METRICS_NOISE_GENERATORS = TextKey.of( + "iris.bukkit.commandstudio.calculating_performance_metrics_noise_generators", + "Calculating Performance Metrics for Noise generators" + ); + public static final TextKey COMMAND_STUDIO_CALCULATING_INTERPOLATOR_TIMINGS = TextKey.of( + "iris.bukkit.commandstudio.calculating_interpolator_timings", + "Calculating Interpolator Timings..." + ); + public static final TextKey COMMAND_STUDIO_PROCESSING_GENERATOR_SCORES = TextKey.of( + "iris.bukkit.commandstudio.processing_generator_scores", + "Processing Generator Scores: " + ); + public static final TextKey COMMAND_STUDIO_SCORE = TextKey.of( + "iris.bukkit.commandstudio.score", + "Score: " + "{value}" + ); + public static final TextKey COMMAND_STUDIO_DONE = TextKey.of( + "iris.bukkit.commandstudio.done_with_result", + C.GREEN + "Done! " + "{value}" + ); + public static final TextKey COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN = TextKey.of( + "iris.bukkit.commandstudio.you_have_be_iris_world_spawn_entities_properly_trying_spawn", + C.RED + "You have to be in an Iris world to spawn entities properly. Trying to spawn the best we can do." + ); + public static final TextKey COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN = TextKey.of( + "iris.bukkit.commandstudio.no_studio_world_is_open", + C.RED + "No studio world is open!" + ); + public static final TextKey COMMAND_STUDIO_YOU_ARE_ALREADY_STUDIO_WORLD = TextKey.of( + "iris.bukkit.commandstudio.you_are_already_studio_world", + C.RED + "You are already in a studio world!" + ); + public static final TextKey COMMAND_STUDIO_SENDING_YOU_STUDIO_WORLD = TextKey.of( + "iris.bukkit.commandstudio.sending_you_studio_world", + C.GREEN + "Sending you to the studio world!" + ); + public static final TextKey COMMAND_STUDIO_UPDATING_CODE_WORKSPACE = TextKey.of( + "iris.bukkit.commandstudio.updating_code_workspace", + C.GOLD + "Updating Code Workspace for " + "{value}" + "..." + ); + public static final TextKey COMMAND_STUDIO_UPDATED_CODE_WORKSPACE = TextKey.of( + "iris.bukkit.commandstudio.updated_code_workspace", + C.GREEN + "Updated Code Workspace for " + "{value}" + ); + public static final TextKey COMMAND_STUDIO_INVALID_PROJECT_TRY_DELETING_CODE_WORKSPACE_FILE_TRY_AGAIN = TextKey.of( + "iris.bukkit.commandstudio.invalid_project_try_deleting_code_workspace_file_try_again", + C.RED + "Invalid project: " + "{value}" + ". Try deleting the code-workspace file and try again." + ); + public static final TextKey COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD = TextKey.of( + "iris.bukkit.commandstudio.you_must_be_iris_world", + C.RED + "You must be in an Iris world" + ); + public static final TextKey COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD_2 = TextKey.of( + "iris.bukkit.commandstudio.you_must_be_iris_world_2", + "You must be in an iris world." + ); + public static final TextKey COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS = TextKey.of( + "iris.bukkit.commandstudio.capturing_igendata_from_nearby_chunks", + "Capturing IGenData from " + "{value}" + " nearby chunks." + ); + public static final TextKey COMMAND_STUDIO_REPORTED = TextKey.of( + "iris.bukkit.commandstudio.reported", + "Reported to: " + "{value}" + ); + public static final TextKey COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS = TextKey.of( + "iris.bukkit.commandstudio.you_must_have_server_launched_guis_enabled_settings", + C.RED + "You must have server launched GUIs enabled in the settings!" + ); + public static final TextKey COMMAND_STUDIO_PLAYERS_ONLY = TextKey.of( + "iris.bukkit.commandstudio.players_only", + C.RED + "Players only!" + ); + public static final TextKey COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN_2 = TextKey.of( + "iris.bukkit.commandstudio.no_studio_world_is_open_2", + C.RED + "No studio world is open!" + ); + public static final TextKey COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD = TextKey.of( + "iris.bukkit.commandstudio.you_must_be_studio_world", + C.RED + "You must be in a studio world!" + ); + public static final TextKey COMMAND_WHAT_MATERIAL = TextKey.of( + "iris.bukkit.commandwhat.material", + "Material: " + C.GREEN + "{value}" + ); + public static final TextKey COMMAND_WHAT_FULL = TextKey.of( + "iris.bukkit.commandwhat.full", + "Full: " + C.WHITE + "{value}" + ); + public static final TextKey COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM = TextKey.of( + "iris.bukkit.commandwhat.please_hold_block_item", + "Please hold a block/item" + ); + public static final TextKey COMMAND_WHAT_MATERIAL_2 = TextKey.of( + "iris.bukkit.commandwhat.material_2", + "Material: " + C.GREEN + "{value}" + ); + public static final TextKey COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM_2 = TextKey.of( + "iris.bukkit.commandwhat.please_hold_block_item_2", + "Please hold a block/item" + ); + public static final TextKey COMMAND_WHAT_IBIOME = TextKey.of( + "iris.bukkit.commandwhat.ibiome", + "IBiome: " + "{value}" + " (" + "{value2}" + ")" + ); + public static final TextKey COMMAND_WHAT_NON_IRIS_BIOME = TextKey.of( + "iris.bukkit.commandwhat.non_iris_biome", + "Non-Iris Biome: " + "{value}" + ); + public static final TextKey COMMAND_WHAT_DATA_PACK_BIOME_ID = TextKey.of( + "iris.bukkit.commandwhat.data_pack_biome_id", + "Data Pack Biome: " + "{value}" + " (ID: " + "{value2}" + ")" + ); + public static final TextKey COMMAND_WHAT_IREGION = TextKey.of( + "iris.bukkit.commandwhat.iregion", + "IRegion: " + "{value}" + " (" + "{value2}" + ")" + ); + public static final TextKey COMMAND_WHAT_IRIS_WORLDS_ONLY = TextKey.of( + "iris.bukkit.commandwhat.iris_worlds_only", + C.IRIS + "Iris worlds only." + ); + public static final TextKey COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY = TextKey.of( + "iris.bukkit.commandwhat.please_look_at_any_block_not_at_sky", + "Please look at any block, not at the sky" + ); + public static final TextKey COMMAND_WHAT_MATERIAL_3 = TextKey.of( + "iris.bukkit.commandwhat.material_3", + "Material: " + C.GREEN + "{value}" + ); + public static final TextKey COMMAND_WHAT_FULL_2 = TextKey.of( + "iris.bukkit.commandwhat.full_2", + "Full: " + C.WHITE + "{value}" + ); + public static final TextKey COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE = TextKey.of( + "iris.bukkit.commandwhat.storage_block_loot_capable", + C.YELLOW + "* Storage Block (Loot Capable)" + ); + public static final TextKey COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE = TextKey.of( + "iris.bukkit.commandwhat.lit_block_light_capable", + C.YELLOW + "* Lit Block (Light Capable)" + ); + public static final TextKey COMMAND_WHAT_FOLIAGE_BLOCK = TextKey.of( + "iris.bukkit.commandwhat.foliage_block", + C.YELLOW + "* Foliage Block" + ); + public static final TextKey COMMAND_WHAT_DECORANT_BLOCK = TextKey.of( + "iris.bukkit.commandwhat.decorant_block", + C.YELLOW + "* Decorant Block" + ); + public static final TextKey COMMAND_WHAT_FLUID_BLOCK = TextKey.of( + "iris.bukkit.commandwhat.fluid_block", + C.YELLOW + "* Fluid Block" + ); + public static final TextKey COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK = TextKey.of( + "iris.bukkit.commandwhat.plantable_foliage_block", + C.YELLOW + "* Plantable Foliage Block" + ); + public static final TextKey COMMAND_WHAT_SOLID_BLOCK = TextKey.of( + "iris.bukkit.commandwhat.solid_block", + C.YELLOW + "* Solid Block" + ); + public static final TextKey COMMAND_WHAT_FOUND_NEARBY_MARKERS = TextKey.of( + "iris.bukkit.commandwhat.found_nearby_markers", + "Found " + "{value}" + " Nearby Markers (" + "{marker}" + ")" + ); + public static final TextKey COMMAND_WHAT_IRIS_WORLDS_ONLY_2 = TextKey.of( + "iris.bukkit.commandwhat.iris_worlds_only_2", + C.IRIS + "Iris worlds only." + ); + + private static final List KEYS = List.of( + COMMAND_DEVELOPER_WORLD_IS_NULL, + COMMAND_DEVELOPER_TARGET_IRIS_WORLD_BEFORE_READING_MANTLE_DUMP, + COMMAND_DEVELOPER_UPGRADING, + COMMAND_DEVELOPER_DONE_UPGRADING_YOU_CAN_NOW_UPDATE_YOUR_SERVER_VERSION, + COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER, + COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD, + COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL, + COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, + COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_2, + COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN, + COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST, + COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, + COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_3, + COMMAND_DEVELOPER_TARGET_MUST_BE_IRIS_WORLD, + COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_2, + COMMAND_DEVELOPER_GOLDENHASH_STARTED_CHUNK_S_AROUND_BUFFERS_WORLD_UNTOUCHED, + COMMAND_EDIT_PLAYERS_ONLY, + COMMAND_EDIT_NO_STUDIO_WORLD_IS_OPEN, + COMMAND_EDIT_YOU_MUST_BE_STUDIO_WORLD, + COMMAND_EDIT_CANNOT_OPEN_FILES_HEADLESS_ENVIRONMENTS, + COMMAND_EDIT_DESKTOP_IS_NOT_SUPPORTED_BY_THIS_ENVIRONMENT, + COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM, + COMMAND_EDIT_OPENING_VSCODE, + COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST, + COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_2, + COMMAND_EDIT_OPENING_VSCODE_2, + COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_2, + COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_3, + COMMAND_EDIT_OPENING_VSCODE_3, + COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_3, + COMMAND_FIND_NOT_IRIS_WORLD, + COMMAND_FIND_NOT_IRIS_WORLD_2, + COMMAND_FIND_NOT_IRIS_WORLD_3, + COMMAND_FIND_NOT_IRIS_WORLD_4, + COMMAND_FIND_OBJECT_STUDIO_TELEPORTING, + COMMAND_FIND_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS, + COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_IRIS_CREATING_WORLDS_AS_IRIS, + COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD, + COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_BENCHMARK_CREATING_WORLDS_AS_IRIS, + COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD_2, + COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS, + COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND, + COMMAND_IRIS_DIMENSION_NOT_FOUND, + COMMAND_IRIS_INSTALL_PACK_AND_RESTART, + COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD, + COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS, + COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD, + COMMAND_IRIS_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA, + COMMAND_IRIS_PREPARING_WORLD_FILES_BUKKIT_YML_NEXT_STARTUP, + COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION, + COMMAND_IRIS_STAGED_IRIS_WORLD_WITH_GENERATOR_IRIS_SEED, + COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML, + COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST, + COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE, + COMMAND_IRIS_TO, + COMMAND_IRIS_TOTAL_HEIGHT, + COMMAND_IRIS_IRIS_WORLDS, + COMMAND_IRIS_MESSAGE, + COMMAND_IRIS_BUKKIT_WORLDS, + COMMAND_IRIS_MESSAGE_2, + COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS, + COMMAND_IRIS_REMOVING_WORLD, + COMMAND_IRIS_FAILED_EVACUATE_WORLD, + COMMAND_IRIS_FAILED_UNLOAD_WORLD, + COMMAND_IRIS_SUCCESSFULLY_REMOVED_FROM_BUKKIT_YML, + COMMAND_IRIS_LOOKS_LIKE_WORLD_WAS_ALREADY_REMOVED_FROM_BUKKIT_YML, + COMMAND_IRIS_FAILED_SAVE_BUKKIT_YML_BECAUSE, + COMMAND_IRIS_SET_DEBUG, + COMMAND_IRIS_YOU_MUST_BE_IRIS_WORLD, + COMMAND_IRIS_SENDING_METRICS, + COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_2, + COMMAND_IRIS_UNLOADING_WORLD, + COMMAND_IRIS_WORLD_UNLOADED_SUCCESSFULLY, + COMMAND_IRIS_FAILED_UNLOAD_WORLD_2, + COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, + COMMAND_IRIS_DOESNT_EXIST_ON_SERVER, + COMMAND_IRIS_GENERATOR, + COMMAND_IRIS_IS_NOT_IRIS_WORLD, + COMMAND_IRIS_COULD_NOT_DETERMINE_IRIS_DIMENSION, + COMMAND_IRIS_LOADING_WORLD, + COMMAND_IRIS_FOLIA_CANNOT_LOAD_NEW_WORLDS_AT_RUNTIME_RESTART_SERVER_LOAD, + COMMAND_IRIS_LOADED_SUCCESSFULLY, + COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_3, + COMMAND_IRIS_EVACUATING_WORLD, + COMMAND_OBJECT_OBJECT_SIZE, + COMMAND_OBJECT_BLOCKS_USED, + COMMAND_OBJECT_BLOCKS_OBJECT, + COMMAND_OBJECT_OTHER_BLOCK_TYPES, + COMMAND_OBJECT_CURRENT_OBJECT_SIZE, + COMMAND_OBJECT_NEW_OBJECT_SIZE, + COMMAND_OBJECT_FAILED_SAVE_OBJECT, + COMMAND_OBJECT_NO_OBJECTS_MATCHED, + COMMAND_OBJECT_PLAUSIBILIZE_REACH_QUEUED_OBJECT_S, + COMMAND_OBJECT_HOLD_YOUR_WAND, + COMMAND_OBJECT_NO_AREA_SELECTED, + COMMAND_OBJECT_READY_YOUR_WAND, + COMMAND_OBJECT_READY_YOUR_WAND_2, + COMMAND_OBJECT_INDICATED_SCALE_EXCEEDS_MAXIMUM_DOWNSCALED_MAXIMUM, + COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, + COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, + COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, + COMMAND_OBJECT_PLACED, + COMMAND_OBJECT_YOU_NEED_HOLD_YOUR_WAND, + COMMAND_OBJECT_FILE_ALREADY_EXISTS_SET_OVERWRITE_TRUE_OVERWRITE_IT, + COMMAND_OBJECT_FAILED_SAVE_OBJECT_BECAUSE_IOEXCEPTION, + COMMAND_OBJECT_SUCCESSFULLY_OBJECT_SAVED_OBJECTS, + COMMAND_OBJECT_HOLD_YOUR_WAND_2, + COMMAND_OBJECT_NO_AREA_SELECTED_2, + COMMAND_OBJECT_REVERTED_PASTES, + COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW, + COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD, + COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT, + COMMAND_OBJECT_POOF_GOOD_LUCK_BUILDING, + COMMAND_OBJECT_HOLD_YOUR_WAND_3, + COMMAND_OBJECT_NO_AREA_SELECTED_3, + COMMAND_OBJECT_AUTO_SELECT_COMPLETE, + COMMAND_OBJECT_HOLD_YOUR_WAND_4, + COMMAND_OBJECT_NO_AREA_SELECTED_4, + COMMAND_OBJECT_AUTO_SELECT_COMPLETE_2, + COMMAND_PREGEN_PREGEN_RADIUS_MUST_BE_GREATER_THAN_ZERO_BLOCKS, + COMMAND_STUDIO_REGIONS_RADIUS_OUT_OF_RANGE, + COMMAND_STUDIO_REGIONS_SCAN_FAILED, + COMMAND_PREGEN_STRICT_SERIAL_PREGENERATION_REQUIRES_PAPER_PAPER_COMPATIBLE_SERVER, + COMMAND_PREGEN_ENGINE_ACCESS_THIS_WORLD_IS_NULL, + COMMAND_PREGEN_PLEASE_MAKE_SURE_WORLD_IS_LOADED_ENGINE_IS_INITIALIZED_GENERATE, + COMMAND_PREGEN_FAILED_START_PREGENERATION_SEE_CONSOLE_DETAILS, + COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_STOP, + COMMAND_PREGEN_PAUSED_UNPAUSED_PREGENERATION_TASK_NOW, + COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_PAUSE_UNPAUSE, + COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASK, + COMMAND_PREGEN_PREGEN, + COMMAND_PREGEN_SPEED_S_ETA_ELAPSED_METHOD, + COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION, + COMMAND_STRUCTURE_WROTE_STRUCTURE_INDEX, + COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_2, + COMMAND_STRUCTURE_IMPORTING_ALL_VANILLA_DATAPACK_STRUCTURES_INTO_OVERWRITE, + COMMAND_STRUCTURE_IMPORT_COMPLETE_STRUCTURES_OBJECTS_WRITTEN_FAILED, + COMMAND_STRUCTURE_REFERENCE_THEM_FROM_BIOME_REGION_DIMENSION_STRUCTURES_LIST_RUN_IRIS, + COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_3, + COMMAND_STRUCTURE_CAPTURED_STRUCTURES_PLACE_THEM_FROM_STRUCTURES_LIST_REGENERATE_CHUNKS_DELETE, + COMMAND_STRUCTURE_NO_LOADED_IRIS_WORLD_FOUND_JOIN_CREATE_ONE_FIRST_SEARCH, + COMMAND_STRUCTURE_SELECTED_IRIS_WORLD_HAS_NO_ACTIVE_GENERATOR_ENGINE, + COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_4, + COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK, + COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES_CHECK_STARTPOOL, + COMMAND_STRUCTURE_STRUCTURE_PIECES_FOOTPRINT_X_BLOCKS_SAMPLE_SEED_1234, + COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_5, + COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK_2, + COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES, + COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION, + COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS, + COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED, + COMMAND_STUDIO_PROVIDE_DIMENSION_PACK_IRIS_STD_IMPORTVANILLA_PACK_DIMENSION, + COMMAND_STUDIO_COULD_NOT_RESOLVE_PACK_DIMENSION, + COMMAND_STUDIO_OPENING_VSCODE_PACK, + COMMAND_STUDIO_PACK_HAS_VERSION, + COMMAND_STUDIO_OPENING_NOISE_EXPLORER, + COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE, + COMMAND_STUDIO_OPENING_INVENTORY_NOW, + COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD, + COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD, + COMMAND_STUDIO_OPENING_MAP, + COMMAND_STUDIO_CALCULATING_PERFORMANCE_METRICS_NOISE_GENERATORS, + COMMAND_STUDIO_CALCULATING_INTERPOLATOR_TIMINGS, + COMMAND_STUDIO_PROCESSING_GENERATOR_SCORES, + COMMAND_STUDIO_SCORE, + COMMAND_STUDIO_DONE, + COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN, + COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN, + COMMAND_STUDIO_YOU_ARE_ALREADY_STUDIO_WORLD, + COMMAND_STUDIO_SENDING_YOU_STUDIO_WORLD, + COMMAND_STUDIO_UPDATING_CODE_WORKSPACE, + COMMAND_STUDIO_UPDATED_CODE_WORKSPACE, + COMMAND_STUDIO_INVALID_PROJECT_TRY_DELETING_CODE_WORKSPACE_FILE_TRY_AGAIN, + COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD, + COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD_2, + COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, + COMMAND_STUDIO_REPORTED, + COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS, + COMMAND_STUDIO_PLAYERS_ONLY, + COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN_2, + COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD, + COMMAND_WHAT_MATERIAL, + COMMAND_WHAT_FULL, + COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM, + COMMAND_WHAT_MATERIAL_2, + COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM_2, + COMMAND_WHAT_IBIOME, + COMMAND_WHAT_NON_IRIS_BIOME, + COMMAND_WHAT_DATA_PACK_BIOME_ID, + COMMAND_WHAT_IREGION, + COMMAND_WHAT_IRIS_WORLDS_ONLY, + COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY, + COMMAND_WHAT_MATERIAL_3, + COMMAND_WHAT_FULL_2, + COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE, + COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE, + COMMAND_WHAT_FOLIAGE_BLOCK, + COMMAND_WHAT_DECORANT_BLOCK, + COMMAND_WHAT_FLUID_BLOCK, + COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK, + COMMAND_WHAT_SOLID_BLOCK, + COMMAND_WHAT_FOUND_NEARBY_MARKERS, + COMMAND_WHAT_IRIS_WORLDS_ONLY_2 + ); + + private BukkitCommandMessagesExtended() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java b/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java new file mode 100644 index 000000000..bb5cbdace --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java @@ -0,0 +1,770 @@ +package art.arcane.iris.core.localization; + +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.localization.LinesKey; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.PluralKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; +import java.util.Map; + +public final class BukkitRuntimeMessages { + public static final TextKey COMMAND_PACK_PACKS_FOLDER_NOT_FOUND = TextKey.of( + "iris.bukkit.runtime.commandpack.packs_folder_not_found", + C.RED + "packs/ folder not found." + ); + public static final TextKey COMMAND_PACK_NO_PACKS_VALIDATE = TextKey.of( + "iris.bukkit.runtime.commandpack.no_packs_validate", + C.YELLOW + "No packs to validate." + ); + public static final TextKey COMMAND_PACK_VALIDATION_COMPLETE_BROKEN_PACKS = TextKey.of( + "iris.bukkit.runtime.commandpack.validation_complete_broken_packs", + C.GREEN + "Validation complete. Broken packs: " + "{broken}" + "/" + "{value}" + ); + public static final TextKey COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS = TextKey.of( + "iris.bukkit.runtime.commandpack.pack_not_found_under_packs", + C.RED + "Pack '" + "{pack}" + "' not found under packs/." + ); + public static final TextKey COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK = TextKey.of( + "iris.bukkit.runtime.commandpack.no_cleanup_candidates_found_pack", + C.GREEN + "No cleanup candidates found for pack '" + "{pack}" + "'." + ); + public static final TextKey COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER = TextKey.of( + "iris.bukkit.runtime.commandpack.quarantined_cleanup_candidate_s_under", + C.GREEN + "Quarantined " + "{size}" + " cleanup candidate(s) under " + "{quarantinePath}" + "." + ); + public static final TextKey COMMAND_PACK_CLEANUP_MODE_MUST_BE_PREVIEW_APPLY = TextKey.of( + "iris.bukkit.runtime.commandpack.cleanup_mode_must_be_preview_apply", + C.RED + "Cleanup mode must be preview or apply." + ); + public static final TextKey COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK_2 = TextKey.of( + "iris.bukkit.runtime.commandpack.no_cleanup_candidates_found_pack_2", + C.GREEN + "No cleanup candidates found for pack '" + "{pack}" + "'." + ); + public static final TextKey COMMAND_PACK_CLEANUP_PREVIEW_PACK_CANDIDATE_S_NO_FILES_WERE_CHANGED = TextKey.of( + "iris.bukkit.runtime.commandpack.cleanup_preview_pack_candidate_s_no_files_were_changed", + C.YELLOW + "Cleanup preview for pack '" + "{pack}" + "': " + "{size}" + " candidate(s). No files were changed." + ); + public static final TextKey COMMAND_PACK_RUN_IRIS_PACK_CLEANUP_MODE_APPLY_QUARANTINE_THESE_CANDIDATES_AFTER_FRESH_SCAN = TextKey.of( + "iris.bukkit.runtime.commandpack.run_iris_pack_cleanup_mode_apply_quarantine_these_candidates_after_fresh_scan", + C.GRAY + "Run /iris pack cleanup " + "{pack}" + " mode=apply to quarantine these candidates after a fresh scan." + ); + public static final TextKey COMMAND_PACK_RESTORE_REFUSED_BECAUSE_DESTINATION_S_ALREADY_EXIST = TextKey.of( + "iris.bukkit.runtime.commandpack.restore_refused_because_destination_s_already_exist", + C.RED + "Restore refused because " + "{size}" + " destination(s) already exist." + ); + public static final TextKey COMMAND_PACK_NOTHING_RESTORE_PACK = TextKey.of( + "iris.bukkit.runtime.commandpack.nothing_restore_pack", + C.YELLOW + "Nothing to restore for pack '" + "{pack}" + "'." + ); + public static final TextKey COMMAND_PACK_RESTORED_FILE_S_FROM = TextKey.of( + "iris.bukkit.runtime.commandpack.restored_file_s_from", + C.GREEN + "Restored " + "{size}" + " file(s) from " + "{dumpPath}" + "." + ); + public static final TextKey COMMAND_PACK_RESTORE_MODE_MUST_BE_PREVIEW_APPLY = TextKey.of( + "iris.bukkit.runtime.commandpack.restore_mode_must_be_preview_apply", + C.RED + "Restore mode must be preview or apply." + ); + public static final TextKey COMMAND_PACK_NOTHING_RESTORE_PACK_2 = TextKey.of( + "iris.bukkit.runtime.commandpack.nothing_restore_pack_2", + C.YELLOW + "Nothing to restore for pack '" + "{pack}" + "'." + ); + public static final TextKey COMMAND_PACK_RESTORE_PREVIEW_FILE_S_NO_FILES_WERE_CHANGED = TextKey.of( + "iris.bukkit.runtime.commandpack.restore_preview_file_s_no_files_were_changed", + C.YELLOW + "Restore preview for " + "{dumpPath}" + ": " + "{size}" + " file(s). No files were changed." + ); + public static final TextKey COMMAND_PACK_RESTORE_IS_BLOCKED_BY_EXISTING_DESTINATION_S = TextKey.of( + "iris.bukkit.runtime.commandpack.restore_is_blocked_by_existing_destination_s", + C.RED + "Restore is blocked by " + "{size}" + " existing destination(s)." + ); + public static final TextKey COMMAND_PACK_RUN_IRIS_PACK_RESTORE_MODE_APPLY_RESTORE_AFTER_FRESH_CONFLICT_CHECK = TextKey.of( + "iris.bukkit.runtime.commandpack.run_iris_pack_restore_mode_apply_restore_after_fresh_conflict_check", + C.GRAY + "Run /iris pack restore " + "{pack}" + " mode=apply to restore after a fresh conflict check." + ); + public static final TextKey COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST = TextKey.of( + "iris.bukkit.runtime.commandpack.no_validation_results_recorded_run_iris_pack_validate_first", + C.YELLOW + "No validation results recorded. Run /iris pack validate first." + ); + public static final TextKey COMMAND_PACK_STATUS_OK = TextKey.of( + "iris.bukkit.runtime.commandpack.status.ok", + C.GREEN + "OK" + C.RESET + " {pack}" + C.GRAY + " (blocking={blocking}, warnings={warnings})" + ); + public static final TextKey COMMAND_PACK_STATUS_BROKEN = TextKey.of( + "iris.bukkit.runtime.commandpack.status.broken", + C.RED + "BROKEN" + C.RESET + " {pack}" + C.GRAY + " (blocking={blocking}, warnings={warnings})" + ); + public static final TextKey COMMAND_PACK_CLEANUP_FAILED = TextKey.of( + "iris.bukkit.runtime.commandpack.cleanup_failed", + C.RED + "Cleanup failed: {error}" + ); + public static final TextKey COMMAND_PACK_RESTORE_FAILED = TextKey.of( + "iris.bukkit.runtime.commandpack.restore_failed", + C.RED + "Restore failed: {error}" + ); + public static final TextKey COMMAND_PACK_PATH_STILL_QUARANTINED = TextKey.of( + "iris.bukkit.runtime.commandpack.path.still_quarantined", + "still quarantined" + ); + public static final TextKey COMMAND_PACK_PATH_QUARANTINED = TextKey.of( + "iris.bukkit.runtime.commandpack.path.quarantined", + "quarantined" + ); + public static final TextKey COMMAND_PACK_PATH_CANDIDATE = TextKey.of( + "iris.bukkit.runtime.commandpack.path.candidate", + "candidate" + ); + public static final TextKey COMMAND_PACK_PATH_CONFLICT = TextKey.of( + "iris.bukkit.runtime.commandpack.path.conflict", + "conflict" + ); + public static final TextKey COMMAND_PACK_PATH_RESTORED = TextKey.of( + "iris.bukkit.runtime.commandpack.path.restored", + "restored" + ); + public static final TextKey COMMAND_PACK_PATH_FILE = TextKey.of( + "iris.bukkit.runtime.commandpack.path.file", + "file" + ); + public static final TextKey COMMAND_PACK_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE = TextKey.of( + "iris.bukkit.runtime.commandpack.no_validation_result_run_iris_pack_validate", + C.YELLOW + "No validation result for '" + "{pack}" + "'. Run /iris pack validate " + "{pack2}" + "." + ); + public static final TextKey COMMAND_PACK_VALIDATION_FAILED = TextKey.of( + "iris.bukkit.runtime.commandpack.validation_failed", + C.RED + "Validation of '" + "{name}" + "' failed: " + "{error}" + ); + public static final TextKey COMMAND_PACK_PACK_IS_LOADABLE_WARNINGS = TextKey.of( + "iris.bukkit.runtime.commandpack.pack_is_loadable_warnings", + C.GREEN + "Pack '" + "{packName}" + "' is loadable." + C.GRAY + " (warnings=" + "{size}" + ")" + ); + public static final TextKey COMMAND_PACK_PACK_IS_BROKEN = TextKey.of( + "iris.bukkit.runtime.commandpack.pack_is_broken", + C.RED + "Pack '" + "{packName}" + "' is BROKEN:" + ); + public static final TextKey COMMAND_PACK_MESSAGE = TextKey.of( + "iris.bukkit.runtime.commandpack.message", + C.RED + " - " + "{reason}" + ); + public static final TextKey COMMAND_PACK_MESSAGE_2 = TextKey.of( + "iris.bukkit.runtime.commandpack.message_2", + C.YELLOW + " ! " + "{value}" + ); + public static final PluralKey COMMAND_PACK_MORE_WARNING_S = PluralKey.of( + "iris.bukkit.runtime.commandpack.more_warning_s", + "count", + Map.of( + "one", C.GRAY + " ... and {count} more warning.", + "other", C.GRAY + " ... and {count} more warnings." + ) + ); + public static final LinesKey COMMAND_DEVELOPER_UPDATE_WORLD_WARNING = LinesKey.of( + "iris.bukkit.runtime.commanddeveloper.update_world_warning", + C.RED + "You should always make a backup before using this", + C.YELLOW + "Issues caused by this can be, but are not limited to:", + C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)", + C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks", + C.YELLOW + " - Structures not spawning again when regenerating", + C.YELLOW + " - Caves not lining up", + C.YELLOW + " - Terrain layers not lining up", + C.RED + "Now that you are aware of the risks, and have made a back-up:", + C.RED + "/iris developer update-world {world} {pack} confirm=true" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.importing_vanilla_datapack_structures_mode_includenonjigsaw", + C.GREEN + "Importing " + C.WHITE + "{total}" + C.GREEN + " vanilla & datapack structures (mode=" + "{mode}" + ", includeNonJigsaw=" + "{includeNonJigsaw}" + ")..." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_invalid_key", + C.RED + "[fail] " + "{keyString}" + ": invalid key" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_JIGSAW = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.jigsaw", + C.GRAY + "[jigsaw] " + "{keyString}" + " -> " + "{name}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_SINGLE = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.single", + C.GRAY + "[single] " + "{keyString}" + " -> " + "{name}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_SKIP_NO_SINGLE_TEMPLATE_NBT_VANILLA_BUILDS_THIS_CODE_FROM_SEPARATE_PIECE = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.skip_no_single_template_nbt_vanilla_builds_this_code_from_separate_piece", + C.YELLOW + "[skip] " + "{keyString}" + ": no single-template NBT - vanilla builds this in code or from separate piece templates (imported via the templates pass); nothing to import as one structure." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail", + C.RED + "[fail] " + "{keyString}" + ": " + "{message}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_2 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_2", + C.RED + "[fail] " + "{keyString}" + ": " + "{message}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_3 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_3", + C.RED + "[fail] " + "{keyString}" + ": " + "{error}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_BULK_IMPORT_COMPLETE_IMPORTED_SKIPPED_FAILED_TOTAL = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.bulk_import_complete_imported_skipped_failed_total", + C.GREEN + "Bulk import complete: " + C.WHITE + "{imported}" + C.GREEN + " imported, " + C.WHITE + "{skipped}" + C.GREEN + " skipped, " + C.WHITE + "{failed}" + C.GREEN + " failed (" + C.WHITE + "{total}" + C.GREEN + " total)." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_BUILDING_SINGLE_TEMPLATE_STRUCTURES_FROM_IMPORTED_PIECES_ONE_VARIANT_PLACED_PER_GENERATION = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.building_single_template_structures_from_imported_pieces_one_variant_placed_per_generation", + C.GREEN + "Building single-template structures from imported pieces (one variant placed per generation)..." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_GROUP_VARIANTS = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.group_variants", + C.GRAY + "[group] " + "{value}" + " -> " + "{value2}" + " (" + "{blocks}" + " variants)" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_SKIP = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.skip", + C.YELLOW + "[skip] " + "{value}" + ": " + "{message}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_4 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_4", + C.RED + "[fail] " + "{value}" + ": " + "{error}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_SINGLE_TEMPLATE_STRUCTURES_BUILT_SKIPPED_FAILED = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.single_template_structures_built_skipped_failed", + C.GREEN + "Single-template structures: " + C.WHITE + "{imported}" + C.GREEN + " built, " + C.WHITE + "{skipped}" + C.GREEN + " skipped, " + C.WHITE + "{failed}" + C.GREEN + " failed." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAILED_ENUMERATE_STRUCTURE_TEMPLATES_VIA_SERVER_RESOURCEMANAGER = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.failed_enumerate_structure_templates_via_server_resourcemanager", + C.RED + "Failed to enumerate structure templates via the server ResourceManager: " + "{e}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_NO_STRUCTURE_TEMPLATES_WERE_FOUND_UNDER_STRUCTURE_RESOURCE_PATH = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.no_structure_templates_were_found_under_structure_resource_path", + C.YELLOW + "No structure templates were found under the 'structure' resource path." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTING_STRUCTURE_TEMPLATES_MODE = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.importing_structure_templates_mode", + C.GREEN + "Importing " + C.WHITE + "{total}" + C.GREEN + " structure templates (mode=" + "{mode}" + ")..." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY_2 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_invalid_key_2", + C.RED + "[fail] " + "{keyString}" + ": invalid key" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_5 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_5", + C.RED + "[fail] " + "{keyString}" + ": " + "{message}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_6 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_6", + C.RED + "[fail] " + "{keyString}" + ": " + "{error}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTED_SKIPPED_FAILED = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.imported_skipped_failed", + C.GRAY + "..." + "{processed}" + "/" + "{total}" + " (" + "{imported}" + " imported, " + "{skipped}" + " skipped, " + "{failed}" + " failed)" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_TEMPLATE_IMPORT_COMPLETE_IMPORTED_SKIPPED_FAILED_TOTAL = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.template_import_complete_imported_skipped_failed_total", + C.GREEN + "Template import complete: " + C.WHITE + "{imported}" + C.GREEN + " imported, " + C.WHITE + "{skipped}" + C.GREEN + " skipped, " + C.WHITE + "{failed}" + C.GREEN + " failed (" + C.WHITE + "{total}" + C.GREEN + " total)." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_NO_DATAPACK_NON_MINECRAFT_STRUCTURES_ARE_REGISTERED_INGEST_DATAPACK_RESTART_FIRST_THEN = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.no_datapack_non_minecraft_structures_are_registered_ingest_datapack_restart_first_then", + C.YELLOW + "No datapack (non-minecraft) structures are registered. Ingest a datapack and restart first, then run this again." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTING_DATAPACK_STRUCTURES_MODE = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.importing_datapack_structures_mode", + C.GREEN + "Importing " + C.WHITE + "{total}" + C.GREEN + " datapack structures (mode=" + "{mode}" + ")..." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY_3 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_invalid_key_3", + C.RED + "[fail] " + "{keyString}" + ": invalid key" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_JIGSAW_2 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.jigsaw_2", + C.GRAY + "[jigsaw] " + "{keyString}" + " -> " + "{name}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_SINGLE_2 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.single_2", + C.GRAY + "[single] " + "{keyString}" + " -> " + "{name}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_7 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_7", + C.RED + "[fail] " + "{keyString}" + ": " + "{message}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_8 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_8", + C.RED + "[fail] " + "{keyString}" + ": " + "{message}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_9 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_9", + C.RED + "[fail] " + "{keyString}" + ": " + "{error}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTING_DATAPACK_STRUCTURE_TEMPLATES = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.importing_datapack_structure_templates", + C.GREEN + "Importing " + C.WHITE + "{size}" + C.GREEN + " datapack structure templates..." + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_10 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_10", + C.RED + "[fail] " + "{keyString}" + ": " + "{message}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_FAIL_11 = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.fail_11", + C.RED + "[fail] " + "{keyString}" + ": " + "{error}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_COULD_NOT_ENUMERATE_DATAPACK_TEMPLATES_VIA_SERVER_RESOURCEMANAGER = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.could_not_enumerate_datapack_templates_via_server_resourcemanager", + C.YELLOW + "Could not enumerate datapack templates via the server ResourceManager: " + "{error}" + ); + public static final TextKey BULK_STRUCTURE_IMPORTER_DATAPACK_STRUCTURE_IMPORT_COMPLETE_IMPORTED_SKIPPED_FAILED = TextKey.of( + "iris.bukkit.runtime.bulkstructureimporter.datapack_structure_import_complete_imported_skipped_failed", + C.GREEN + "Datapack structure import complete: " + C.WHITE + "{imported}" + C.GREEN + " imported, " + C.WHITE + "{skipped}" + C.GREEN + " skipped, " + C.WHITE + "{failed}" + C.GREEN + " failed." + ); + public static final TextKey STRUCTURE_CAPTURE_IMPORTER_STRUCTURE_CAPTURE_IS_NOT_SUPPORTED_BY_ACTIVE_NMS_BINDING_SKIPPING_CAPTURE_PASS = TextKey.of( + "iris.bukkit.runtime.structurecaptureimporter.structure_capture_is_not_supported_by_active_nms_binding_skipping_capture_pass", + C.YELLOW + "Structure capture is not supported by the active NMS binding; skipping the capture pass." + ); + public static final TextKey STRUCTURE_CAPTURE_IMPORTER_NO_CODE_GENERATED_STRUCTURES_LEFT_CAPTURE_EVERYTHING_IS_ALREADY_IMPORTED_AS_STRUCTURE = TextKey.of( + "iris.bukkit.runtime.structurecaptureimporter.no_code_generated_structures_left_capture_everything_is_already_imported_as_structure", + C.GRAY + "No code-generated structures left to capture (everything is already imported as a structure)." + ); + public static final TextKey STRUCTURE_CAPTURE_IMPORTER_CAPTURING_CODE_GENERATED_STRUCTURES_NO_NBT_TEMPLATE_INTO_SCRATCH_WORLD_SKIPPING_ANY = TextKey.of( + "iris.bukkit.runtime.structurecaptureimporter.capturing_code_generated_structures_no_nbt_template_into_scratch_world_skipping_any", + C.GREEN + "Capturing " + C.WHITE + "{total}" + C.GREEN + " code-generated structures (no NBT template) into a scratch world (skipping any wider/taller than " + "{MAXSPAN}" + " blocks)..." + ); + public static final TextKey STRUCTURE_CAPTURE_IMPORTER_SKIP_DID_NOT_PLACE_CAPTURABLE_STRUCTURE_HERE_TOO_LARGE_WRONG_DIMENSION_NO = TextKey.of( + "iris.bukkit.runtime.structurecaptureimporter.skip_did_not_place_capturable_structure_here_too_large_wrong_dimension_no", + C.YELLOW + "[skip] " + "{key}" + ": did not place a capturable structure here (too large, wrong dimension, or no valid placement in a flat world). Stays vanilla-generated." + ); + public static final TextKey STRUCTURE_CAPTURE_IMPORTER_FAIL = TextKey.of( + "iris.bukkit.runtime.structurecaptureimporter.fail", + C.RED + "[fail] " + "{key}" + ": " + "{message}" + ); + public static final TextKey STRUCTURE_CAPTURE_IMPORTER_CAPTURE_OBJECTS_IOB_X_X = TextKey.of( + "iris.bukkit.runtime.structurecaptureimporter.capture_objects_iob_x_x", + C.GRAY + "[capture] " + "{key}" + " -> objects/" + "{name}" + ".iob (" + "{w}" + "x" + "{h}" + "x" + "{d}" + ")" + ); + public static final TextKey STRUCTURE_CAPTURE_IMPORTER_FAIL_2 = TextKey.of( + "iris.bukkit.runtime.structurecaptureimporter.fail_2", + C.RED + "[fail] " + "{key}" + ": " + "{error}" + ); + public static final TextKey STRUCTURE_CAPTURE_IMPORTER_CAPTURED_SKIPPED_FAILED = TextKey.of( + "iris.bukkit.runtime.structurecaptureimporter.captured_skipped_failed", + C.GRAY + "..." + "{processed}" + "/" + "{total}" + " (" + "{imported}" + " captured, " + "{skipped}" + " skipped, " + "{failed}" + " failed)" + ); + public static final TextKey STRUCTURE_CAPTURE_IMPORTER_STRUCTURE_CAPTURE_COMPLETE_CAPTURED_SKIPPED_FAILED_TOTAL = TextKey.of( + "iris.bukkit.runtime.structurecaptureimporter.structure_capture_complete_captured_skipped_failed_total", + C.GREEN + "Structure capture complete: " + C.WHITE + "{imported}" + C.GREEN + " captured, " + C.WHITE + "{skipped}" + C.GREEN + " skipped, " + C.WHITE + "{failed}" + C.GREEN + " failed (" + C.WHITE + "{total}" + C.GREEN + " total)." + ); + public static final TextKey FEATURE_IMPORTER_NO_VANILLA_TREE_OBJECT_FEATURES_ARE_EXPOSED_BY_ACTIVE_NMS_BINDING_IMPORTING = TextKey.of( + "iris.bukkit.runtime.featureimporter.no_vanilla_tree_object_features_are_exposed_by_active_nms_binding_importing", + C.YELLOW + "No vanilla tree/object features are exposed by the active NMS binding (importing structures only)." + ); + public static final TextKey FEATURE_IMPORTER_IMPORTING_VANILLA_TREE_OBJECT_FEATURES_VARIANTS_EACH_INTO_SCRATCH_WORLD = TextKey.of( + "iris.bukkit.runtime.featureimporter.importing_vanilla_tree_object_features_variants_each_into_scratch_world", + C.GREEN + "Importing " + C.WHITE + "{total}" + C.GREEN + " vanilla tree/object features (" + C.WHITE + "{wantVariants}" + C.GREEN + " variants each) into a scratch world..." + ); + public static final TextKey FEATURE_IMPORTER_OBJ_OBJECTS_VANILLA = TextKey.of( + "iris.bukkit.runtime.featureimporter.obj_objects_vanilla", + C.GRAY + "[obj] " + "{key}" + " -> objects/vanilla/" + "{group}" + "/" + "{safeName}" + " (" + "{written}" + ")" + ); + public static final TextKey FEATURE_IMPORTER_SKIP_FEATURE_PLACED_NOTHING_AFTER_RETRIES = TextKey.of( + "iris.bukkit.runtime.featureimporter.skip_feature_placed_nothing_after_retries", + C.YELLOW + "[skip] " + "{key}" + ": feature placed nothing after retries." + ); + public static final TextKey FEATURE_IMPORTER_FAIL = TextKey.of( + "iris.bukkit.runtime.featureimporter.fail", + C.RED + "[fail] " + "{key}" + ": " + "{error}" + ); + public static final TextKey FEATURE_IMPORTER_IMPORTED_SKIPPED_FAILED = TextKey.of( + "iris.bukkit.runtime.featureimporter.imported_skipped_failed", + C.GRAY + "..." + "{processed}" + "/" + "{total}" + " (" + "{imported}" + " imported, " + "{skipped}" + " skipped, " + "{failed}" + " failed)" + ); + public static final TextKey FEATURE_IMPORTER_FEATURE_IMPORT_COMPLETE_FEATURES_WRITTEN_SKIPPED_FAILED_TOTAL = TextKey.of( + "iris.bukkit.runtime.featureimporter.feature_import_complete_features_written_skipped_failed_total", + C.GREEN + "Feature import complete: " + C.WHITE + "{imported}" + C.GREEN + " features written, " + C.WHITE + "{skipped}" + C.GREEN + " skipped, " + C.WHITE + "{failed}" + C.GREEN + " failed (" + C.WHITE + "{total}" + C.GREEN + " total)." + ); + public static final TextKey FEATURE_IMPORTER_COULD_NOT_CREATE_SCRATCH_WORLD_FEATURE_IMPORT_SKIPPING_TREE_OBJECT_PASS = TextKey.of( + "iris.bukkit.runtime.featureimporter.could_not_create_scratch_world_feature_import_skipping_tree_object_pass", + C.RED + "Could not create the scratch world for feature import (" + "{error}" + "); skipping the tree/object pass." + ); + public static final TextKey IRIS_CONVERTER_NO_SCHEMATIC_FILES_CONVERT_FOUND = TextKey.of( + "iris.bukkit.runtime.irisconverter.no_schematic_files_convert_found", + "No schematic files to convert found in " + "{path}" + ); + public static final TextKey IRIS_CONVERTER_CONVERTED = TextKey.of( + "iris.bukkit.runtime.irisconverter.converted", + C.IRIS + "Converted " + "{name}" + " -> " + "{value}" + " in " + "{value2}" + ); + public static final TextKey IRIS_CONVERTER_CONVERTED_2 = TextKey.of( + "iris.bukkit.runtime.irisconverter.converted_2", + C.IRIS + "Converted " + "{name}" + " -> " + "{value}" + ); + public static final TextKey IRIS_CONVERTER_FAILED_SAVE = TextKey.of( + "iris.bukkit.runtime.irisconverter.failed_save", + C.RED + "Failed to save: " + "{name}" + ); + public static final TextKey IRIS_CONVERTER_FAILED_CONVERT = TextKey.of( + "iris.bukkit.runtime.irisconverter.failed_convert", + C.RED + "Failed to convert: " + "{name}" + ); + public static final TextKey IRIS_CONVERTER_CONVERTED_3 = TextKey.of( + "iris.bukkit.runtime.irisconverter.converted_3", + C.GRAY + "Converted: " + "{get}" + " in " + "{value}" + ); + public static final TextKey IRIS_CONVERTER_SOME_SCHEMATICS_FAILED_CONVERT_CHECK_CONSOLE_DETAILS = TextKey.of( + "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details", + C.RED + "Some schematics failed to convert. Check the console for details." + ); + public static final TextKey STUDIO_S_V_C_INSTALLING_PACKAGE = TextKey.of( + "iris.bukkit.runtime.studiosvc.installing_package", + C.GOLD + "World pack " + C.AQUA + "{name}" + ":" + "{loadKey}" + C.GRAY + " | " + C.WHITE + "Publishing snapshot" + ); + public static final TextKey STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD = TextKey.of( + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread", + C.RED + "Iris refused to copy the world pack on the Bukkit primary thread." + ); + public static final TextKey STUDIO_S_V_C_PACK_INSTALL_FAILED = TextKey.of( + "iris.bukkit.runtime.studiosvc.pack_install_failed", + C.RED + "Failed to install world pack " + C.WHITE + "{dimension}" + C.RED + ": {error}" + ); + public static final TextKey STUDIO_S_V_C_LOOKING_PACKAGE = TextKey.of( + "iris.bukkit.runtime.studiosvc.looking_package", + "Looking for Package: " + "{type}" + ); + public static final TextKey STUDIO_S_V_C_FOUND_IRIS_FOLDER = TextKey.of( + "iris.bukkit.runtime.studiosvc.found_iris_folder", + "Found " + "{type}" + ".iris in " + "{WORKSPACENAME}" + " folder" + ); + public static final TextKey STUDIO_S_V_C_FOUND_DIMENSION_FOLDER_REPACKAGING = TextKey.of( + "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging", + "Found " + "{type}" + " dimension in " + "{WORKSPACENAME}" + " folder. Repackaging" + ); + public static final TextKey STUDIO_S_V_C_CAN_T_FIND_DIMENSIONS_FOLDER_THIS_PACK_FAILED = TextKey.of( + "iris.bukkit.runtime.studiosvc.can_t_find_dimensions_folder_this_pack_failed", + "Can't find the " + "{name}" + " in the dimensions folder of this pack! Failed!" + ); + public static final TextKey STUDIO_S_V_C_CAN_T_LOAD_DIMENSION_FAILED = TextKey.of( + "iris.bukkit.runtime.studiosvc.can_t_load_dimension_failed", + "Can't load the dimension! Failed!" + ); + public static final TextKey STUDIO_S_V_C_TYPE_INSTALLED = TextKey.of( + "iris.bukkit.runtime.studiosvc.type_installed", + "{name}" + " type installed. " + ); + public static final TextKey STUDIO_S_V_C_FAILED_OPEN_STUDIO_WORLD = TextKey.of( + "iris.bukkit.runtime.studiosvc.failed_open_studio_world", + "Failed to open studio world: " + "{error}" + ); + public static final TextKey STUDIO_S_V_C_CANNOT_OPEN_STUDIO_PACK_HAS_BLOCKING_ERRORS = TextKey.of( + "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors", + "Cannot open studio '" + "{dimm}" + "' - pack has blocking errors:" + ); + public static final TextKey STUDIO_S_V_C_MESSAGE = TextKey.of( + "iris.bukkit.runtime.studiosvc.message", + " - " + "{reason}" + ); + public static final TextKey STUDIO_S_V_C_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE = TextKey.of( + "iris.bukkit.runtime.studiosvc.fix_pack_run_iris_pack_validate_revalidate", + "Fix the pack and run /iris pack validate " + "{dimm}" + " to revalidate." + ); + public static final TextKey STUDIO_S_V_C_FAILED_CLOSE_EXISTING_STUDIO_PROJECT = TextKey.of( + "iris.bukkit.runtime.studiosvc.failed_close_existing_studio_project", + "Failed to close the existing studio project: " + "{error}" + ); + public static final TextKey STUDIO_S_V_C_FAILED_CLOSE_EXISTING_STUDIO_PROJECT_2 = TextKey.of( + "iris.bukkit.runtime.studiosvc.failed_close_existing_studio_project_2", + "Failed to close the existing studio project: " + "{error}" + ); + public static final TextKey STUDIO_S_V_C_FAILED_OPEN_STUDIO_WORLD_2 = TextKey.of( + "iris.bukkit.runtime.studiosvc.failed_open_studio_world_2", + "Failed to open studio world: " + "{error}" + ); + public static final TextKey STUDIO_S_V_C_COULDN_T_FIND_PACK_CREATE_NEW_DIMENSION_FROM = TextKey.of( + "iris.bukkit.runtime.studiosvc.couldn_t_find_pack_create_new_dimension_from", + "Couldn't find the pack to create a new dimension from." + ); + public static final TextKey STUDIO_S_V_C_MISSING_IMPORTED_DIMENSION_FILE = TextKey.of( + "iris.bukkit.runtime.studiosvc.missing_imported_dimension_file", + "Missing Imported Dimension File" + ); + public static final TextKey STUDIO_S_V_C_IMPORTING_INTO_NEW_PROJECT = TextKey.of( + "iris.bukkit.runtime.studiosvc.importing_into_new_project", + "Importing " + "{downloadable}" + " into new Project " + "{s}" + ); + public static final TextKey OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CELL_UNDER_CLICK_X_Z = TextKey.of( + "iris.bukkit.runtime.objectstudiosaveservice.object_studio_no_cell_under_click_x_z", + C.GRAY + "Object Studio: no cell under click (x=" + "{x}" + " z=" + "{z}" + ")." + ); + public static final TextKey OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVING_X_X = TextKey.of( + "iris.bukkit.runtime.objectstudiosaveservice.object_studio_saving_x_x", + C.AQUA + "Object Studio: saving " + C.WHITE + "{pack}" + "/" + "{key}" + C.GRAY + " (" + "{w}" + "x" + "{h}" + "x" + "{d}" + ")" + ); + public static final TextKey OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CHANGES = TextKey.of( + "iris.bukkit.runtime.objectstudiosaveservice.object_studio_no_changes", + C.GRAY + "Object Studio: no changes for " + "{pack}" + "/" + "{key}" + "." + ); + public static final TextKey OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_EMPTY_CELL_NOTHING_WRITE = TextKey.of( + "iris.bukkit.runtime.objectstudiosaveservice.object_studio_empty_cell_nothing_write", + C.GRAY + "Object Studio: empty cell " + "{pack}" + "/" + "{key}" + " (nothing to write)." + ); + public static final TextKey OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_TARGET_FILE = TextKey.of( + "iris.bukkit.runtime.objectstudiosaveservice.object_studio_no_target_file", + C.RED + "Object Studio: no target file for " + "{pack}" + "/" + "{key}" + "." + ); + public static final TextKey OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVED = TextKey.of( + "iris.bukkit.runtime.objectstudiosaveservice.object_studio_saved", + C.GREEN + "Object Studio: saved " + C.WHITE + "{pack}" + "/" + "{key}" + ); + public static final TextKey OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVE_FAILED = TextKey.of( + "iris.bukkit.runtime.objectstudiosaveservice.object_studio_save_failed", + C.RED + "Object Studio: save failed for " + "{pack}" + "/" + "{key}" + " (" + "{error}" + ")" + ); + public static final TextKey IRIS_PROJECT_COULD_NOT_LOAD_DIMENSION = TextKey.of( + "iris.bukkit.runtime.irisproject.could_not_load_dimension", + "Could not load dimension \"" + "{value}" + "\"" + ); + public static final TextKey IRIS_PROJECT_COULD_NOT_GET_DIMENSION_LOADER = TextKey.of( + "iris.bukkit.runtime.irisproject.could_not_get_dimension_loader", + "Could not get dimension loader" + ); + public static final TextKey IRIS_PROJECT_STUDIO_OPEN_FAILED = TextKey.of( + "iris.bukkit.runtime.irisproject.studio_open_failed", + C.RED + "Studio open failed: " + "{error}" + ); + public static final TextKey IRIS_PROJECT_STUDIO_OPEN_FAILED_2 = TextKey.of( + "iris.bukkit.runtime.irisproject.studio_open_failed_2", + C.RED + "Studio open failed." + ); + public static final TextKey IRIS_PROJECT_STUDIO_READY = TextKey.of( + "iris.bukkit.runtime.irisproject.studio_ready", + C.GREEN + "Studio ready " + C.GRAY + "(" + "{value}" + ")" + ); + public static final TextKey IRIS_PROJECT_STUDIO = TextKey.of( + "iris.bukkit.runtime.irisproject.studio", + C.GOLD + "Studio " + C.AQUA + "{bar}" + " " + C.YELLOW + "{percent}" + "%" + C.GRAY + " " + "{currentStage}" + C.DARK_GRAY + " (" + "{value}" + ")" + ); + public static final TextKey IRIS_PROJECT_SERIALIZING_OBJECTS = TextKey.of( + "iris.bukkit.runtime.irisproject.serializing_objects", + "Serializing Objects" + ); + public static final TextKey IRIS_PROJECT_WROTE_ANOTHER_OBJECTS = TextKey.of( + "iris.bukkit.runtime.irisproject.wrote_another_objects", + "Wrote another " + "{g}" + " Objects" + ); + public static final TextKey IRIS_PROJECT_PACKAGE_COMPILED = TextKey.of( + "iris.bukkit.runtime.irisproject.package_compiled", + "Package Compiled!" + ); + public static final TextKey IRIS_PROJECT_FAILED = TextKey.of( + "iris.bukkit.runtime.irisproject.failed", + "Failed!" + ); + public static final TextKey IRIS_ENGINE_TOTAL = TextKey.of( + "iris.bukkit.runtime.irisengine.total", + "Total: " + C.BOLD + C.WHITE + "{value}" + ); + public static final TextKey IRIS_ENGINE_ENGINE = TextKey.of( + "iris.bukkit.runtime.irisengine.engine", + " Engine " + C.UNDERLINE + C.GREEN + "{i}" + C.RESET + ": " + C.BOLD + C.WHITE + "{value}" + ); + public static final TextKey IRIS_ENGINE_DETAILS = TextKey.of( + "iris.bukkit.runtime.irisengine.details", + "Details: " + ); + public static final TextKey IRIS_ENGINE_MESSAGE = TextKey.of( + "iris.bukkit.runtime.irisengine.message", + " " + "{befb}" + "{num}" + "{afb}" + ": " + C.BOLD + C.WHITE + "{value}" + ); + public static final TextKey ENGINE_BUKKIT_OPS_IS_NOT_DEFINED_DIMENSION = TextKey.of( + "iris.bukkit.runtime.enginebukkitops.is_not_defined_dimension", + C.RED + "{name}" + " is not defined in the dimension!" + ); + public static final TextKey ENGINE_BUKKIT_OPS_COULD_NOT_FIND_WITHIN_SEARCH_RANGE = TextKey.of( + "iris.bukkit.runtime.enginebukkitops.could_not_find_within_search_range", + C.RED + "Could not find " + "{message}" + " within search range." + ); + public static final TextKey ENGINE_BUKKIT_OPS_TELEPORTING = TextKey.of( + "iris.bukkit.runtime.enginebukkitops.teleporting", + C.GREEN + "Teleporting to " + "{message}" + "..." + ); + public static final TextKey ENGINE_BUKKIT_OPS_AT = TextKey.of( + "iris.bukkit.runtime.enginebukkitops.at", + C.GREEN + "{message}" + " at: " + "{blockX}" + " " + "{blockY}" + " " + "{blockZ}" + ); + public static final TextKey IRIS_TOOLBELT_YOU_HAVE_BEEN_EVACUATED_FROM_THIS_WORLD = TextKey.of( + "iris.bukkit.runtime.iristoolbelt.you_have_been_evacuated_from_this_world", + "You have been evacuated from this world." + ); + public static final TextKey IRIS_TOOLBELT_YOU_HAVE_BEEN_EVACUATED_FROM_THIS_WORLD_2 = TextKey.of( + "iris.bukkit.runtime.iristoolbelt.you_have_been_evacuated_from_this_world_2", + "You have been evacuated from this world. " + "{m}" + ); + public static final TextKey SERVER_CONFIGURATOR_THERE_ARE_SOME_IRIS_PACKS_THAT_HAVE_CUSTOM_BIOMES_THEM = TextKey.of( + "iris.bukkit.runtime.serverconfigurator.there_are_some_iris_packs_that_have_custom_biomes_them", + "There are some Iris Packs that have custom biomes in them" + ); + public static final TextKey SERVER_CONFIGURATOR_YOU_NEED_RESTART_YOUR_SERVER_USE_THESE_PACKS = TextKey.of( + "iris.bukkit.runtime.serverconfigurator.you_need_restart_your_server_use_these_packs", + "You need to restart your server to use these packs." + ); + public static final TextKey VIRTUAL_COMMAND_MESSAGE = TextKey.of( + "iris.bukkit.runtime.virtualcommand.message", + "- " + C.WHITE + "{i}" + ); + public static final TextKey VIRTUAL_COMMAND_INSUFFICIENT_PERMISSIONS = TextKey.of( + "iris.bukkit.runtime.virtualcommand.insufficient_permissions", + "Insufficient Permissions" + ); + public static final TextKey MORTAR_COMMAND_PARAMETERS_IGNORED = TextKey.of( + "iris.bukkit.runtime.mortarcommand.parameters_ignored", + "Parameters Ignored: " + "{m}" + ); + + private static final List KEYS = List.of( + COMMAND_PACK_PACKS_FOLDER_NOT_FOUND, + COMMAND_PACK_NO_PACKS_VALIDATE, + COMMAND_PACK_VALIDATION_COMPLETE_BROKEN_PACKS, + COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS, + COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, + COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, + COMMAND_PACK_CLEANUP_MODE_MUST_BE_PREVIEW_APPLY, + COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK_2, + COMMAND_PACK_CLEANUP_PREVIEW_PACK_CANDIDATE_S_NO_FILES_WERE_CHANGED, + COMMAND_PACK_RUN_IRIS_PACK_CLEANUP_MODE_APPLY_QUARANTINE_THESE_CANDIDATES_AFTER_FRESH_SCAN, + COMMAND_PACK_RESTORE_REFUSED_BECAUSE_DESTINATION_S_ALREADY_EXIST, + COMMAND_PACK_NOTHING_RESTORE_PACK, + COMMAND_PACK_RESTORED_FILE_S_FROM, + COMMAND_PACK_RESTORE_MODE_MUST_BE_PREVIEW_APPLY, + COMMAND_PACK_NOTHING_RESTORE_PACK_2, + COMMAND_PACK_RESTORE_PREVIEW_FILE_S_NO_FILES_WERE_CHANGED, + COMMAND_PACK_RESTORE_IS_BLOCKED_BY_EXISTING_DESTINATION_S, + COMMAND_PACK_RUN_IRIS_PACK_RESTORE_MODE_APPLY_RESTORE_AFTER_FRESH_CONFLICT_CHECK, + COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST, + COMMAND_PACK_STATUS_OK, + COMMAND_PACK_STATUS_BROKEN, + COMMAND_PACK_CLEANUP_FAILED, + COMMAND_PACK_RESTORE_FAILED, + COMMAND_PACK_PATH_STILL_QUARANTINED, + COMMAND_PACK_PATH_QUARANTINED, + COMMAND_PACK_PATH_CANDIDATE, + COMMAND_PACK_PATH_CONFLICT, + COMMAND_PACK_PATH_RESTORED, + COMMAND_PACK_PATH_FILE, + COMMAND_PACK_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE, + COMMAND_PACK_VALIDATION_FAILED, + COMMAND_PACK_PACK_IS_LOADABLE_WARNINGS, + COMMAND_PACK_PACK_IS_BROKEN, + COMMAND_PACK_MESSAGE, + COMMAND_PACK_MESSAGE_2, + COMMAND_PACK_MORE_WARNING_S, + COMMAND_DEVELOPER_UPDATE_WORLD_WARNING, + BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW, + BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY, + BULK_STRUCTURE_IMPORTER_JIGSAW, + BULK_STRUCTURE_IMPORTER_SINGLE, + BULK_STRUCTURE_IMPORTER_SKIP_NO_SINGLE_TEMPLATE_NBT_VANILLA_BUILDS_THIS_CODE_FROM_SEPARATE_PIECE, + BULK_STRUCTURE_IMPORTER_FAIL, + BULK_STRUCTURE_IMPORTER_FAIL_2, + BULK_STRUCTURE_IMPORTER_FAIL_3, + BULK_STRUCTURE_IMPORTER_BULK_IMPORT_COMPLETE_IMPORTED_SKIPPED_FAILED_TOTAL, + BULK_STRUCTURE_IMPORTER_BUILDING_SINGLE_TEMPLATE_STRUCTURES_FROM_IMPORTED_PIECES_ONE_VARIANT_PLACED_PER_GENERATION, + BULK_STRUCTURE_IMPORTER_GROUP_VARIANTS, + BULK_STRUCTURE_IMPORTER_SKIP, + BULK_STRUCTURE_IMPORTER_FAIL_4, + BULK_STRUCTURE_IMPORTER_SINGLE_TEMPLATE_STRUCTURES_BUILT_SKIPPED_FAILED, + BULK_STRUCTURE_IMPORTER_FAILED_ENUMERATE_STRUCTURE_TEMPLATES_VIA_SERVER_RESOURCEMANAGER, + BULK_STRUCTURE_IMPORTER_NO_STRUCTURE_TEMPLATES_WERE_FOUND_UNDER_STRUCTURE_RESOURCE_PATH, + BULK_STRUCTURE_IMPORTER_IMPORTING_STRUCTURE_TEMPLATES_MODE, + BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY_2, + BULK_STRUCTURE_IMPORTER_FAIL_5, + BULK_STRUCTURE_IMPORTER_FAIL_6, + BULK_STRUCTURE_IMPORTER_IMPORTED_SKIPPED_FAILED, + BULK_STRUCTURE_IMPORTER_TEMPLATE_IMPORT_COMPLETE_IMPORTED_SKIPPED_FAILED_TOTAL, + BULK_STRUCTURE_IMPORTER_NO_DATAPACK_NON_MINECRAFT_STRUCTURES_ARE_REGISTERED_INGEST_DATAPACK_RESTART_FIRST_THEN, + BULK_STRUCTURE_IMPORTER_IMPORTING_DATAPACK_STRUCTURES_MODE, + BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY_3, + BULK_STRUCTURE_IMPORTER_JIGSAW_2, + BULK_STRUCTURE_IMPORTER_SINGLE_2, + BULK_STRUCTURE_IMPORTER_FAIL_7, + BULK_STRUCTURE_IMPORTER_FAIL_8, + BULK_STRUCTURE_IMPORTER_FAIL_9, + BULK_STRUCTURE_IMPORTER_IMPORTING_DATAPACK_STRUCTURE_TEMPLATES, + BULK_STRUCTURE_IMPORTER_FAIL_10, + BULK_STRUCTURE_IMPORTER_FAIL_11, + BULK_STRUCTURE_IMPORTER_COULD_NOT_ENUMERATE_DATAPACK_TEMPLATES_VIA_SERVER_RESOURCEMANAGER, + BULK_STRUCTURE_IMPORTER_DATAPACK_STRUCTURE_IMPORT_COMPLETE_IMPORTED_SKIPPED_FAILED, + STRUCTURE_CAPTURE_IMPORTER_STRUCTURE_CAPTURE_IS_NOT_SUPPORTED_BY_ACTIVE_NMS_BINDING_SKIPPING_CAPTURE_PASS, + STRUCTURE_CAPTURE_IMPORTER_NO_CODE_GENERATED_STRUCTURES_LEFT_CAPTURE_EVERYTHING_IS_ALREADY_IMPORTED_AS_STRUCTURE, + STRUCTURE_CAPTURE_IMPORTER_CAPTURING_CODE_GENERATED_STRUCTURES_NO_NBT_TEMPLATE_INTO_SCRATCH_WORLD_SKIPPING_ANY, + STRUCTURE_CAPTURE_IMPORTER_SKIP_DID_NOT_PLACE_CAPTURABLE_STRUCTURE_HERE_TOO_LARGE_WRONG_DIMENSION_NO, + STRUCTURE_CAPTURE_IMPORTER_FAIL, + STRUCTURE_CAPTURE_IMPORTER_CAPTURE_OBJECTS_IOB_X_X, + STRUCTURE_CAPTURE_IMPORTER_FAIL_2, + STRUCTURE_CAPTURE_IMPORTER_CAPTURED_SKIPPED_FAILED, + STRUCTURE_CAPTURE_IMPORTER_STRUCTURE_CAPTURE_COMPLETE_CAPTURED_SKIPPED_FAILED_TOTAL, + FEATURE_IMPORTER_NO_VANILLA_TREE_OBJECT_FEATURES_ARE_EXPOSED_BY_ACTIVE_NMS_BINDING_IMPORTING, + FEATURE_IMPORTER_IMPORTING_VANILLA_TREE_OBJECT_FEATURES_VARIANTS_EACH_INTO_SCRATCH_WORLD, + FEATURE_IMPORTER_OBJ_OBJECTS_VANILLA, + FEATURE_IMPORTER_SKIP_FEATURE_PLACED_NOTHING_AFTER_RETRIES, + FEATURE_IMPORTER_FAIL, + FEATURE_IMPORTER_IMPORTED_SKIPPED_FAILED, + FEATURE_IMPORTER_FEATURE_IMPORT_COMPLETE_FEATURES_WRITTEN_SKIPPED_FAILED_TOTAL, + FEATURE_IMPORTER_COULD_NOT_CREATE_SCRATCH_WORLD_FEATURE_IMPORT_SKIPPING_TREE_OBJECT_PASS, + IRIS_CONVERTER_NO_SCHEMATIC_FILES_CONVERT_FOUND, + IRIS_CONVERTER_CONVERTED, + IRIS_CONVERTER_CONVERTED_2, + IRIS_CONVERTER_FAILED_SAVE, + IRIS_CONVERTER_FAILED_CONVERT, + IRIS_CONVERTER_CONVERTED_3, + IRIS_CONVERTER_SOME_SCHEMATICS_FAILED_CONVERT_CHECK_CONSOLE_DETAILS, + STUDIO_S_V_C_INSTALLING_PACKAGE, + STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD, + STUDIO_S_V_C_PACK_INSTALL_FAILED, + STUDIO_S_V_C_LOOKING_PACKAGE, + STUDIO_S_V_C_FOUND_IRIS_FOLDER, + STUDIO_S_V_C_FOUND_DIMENSION_FOLDER_REPACKAGING, + STUDIO_S_V_C_CAN_T_FIND_DIMENSIONS_FOLDER_THIS_PACK_FAILED, + STUDIO_S_V_C_CAN_T_LOAD_DIMENSION_FAILED, + STUDIO_S_V_C_TYPE_INSTALLED, + STUDIO_S_V_C_FAILED_OPEN_STUDIO_WORLD, + STUDIO_S_V_C_CANNOT_OPEN_STUDIO_PACK_HAS_BLOCKING_ERRORS, + STUDIO_S_V_C_MESSAGE, + STUDIO_S_V_C_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, + STUDIO_S_V_C_FAILED_CLOSE_EXISTING_STUDIO_PROJECT, + STUDIO_S_V_C_FAILED_CLOSE_EXISTING_STUDIO_PROJECT_2, + STUDIO_S_V_C_FAILED_OPEN_STUDIO_WORLD_2, + STUDIO_S_V_C_COULDN_T_FIND_PACK_CREATE_NEW_DIMENSION_FROM, + STUDIO_S_V_C_MISSING_IMPORTED_DIMENSION_FILE, + STUDIO_S_V_C_IMPORTING_INTO_NEW_PROJECT, + OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CELL_UNDER_CLICK_X_Z, + OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVING_X_X, + OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CHANGES, + OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_EMPTY_CELL_NOTHING_WRITE, + OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_TARGET_FILE, + OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVED, + OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVE_FAILED, + IRIS_PROJECT_COULD_NOT_LOAD_DIMENSION, + IRIS_PROJECT_COULD_NOT_GET_DIMENSION_LOADER, + IRIS_PROJECT_STUDIO_OPEN_FAILED, + IRIS_PROJECT_STUDIO_OPEN_FAILED_2, + IRIS_PROJECT_STUDIO_READY, + IRIS_PROJECT_STUDIO, + IRIS_PROJECT_SERIALIZING_OBJECTS, + IRIS_PROJECT_WROTE_ANOTHER_OBJECTS, + IRIS_PROJECT_PACKAGE_COMPILED, + IRIS_PROJECT_FAILED, + IRIS_ENGINE_TOTAL, + IRIS_ENGINE_ENGINE, + IRIS_ENGINE_DETAILS, + IRIS_ENGINE_MESSAGE, + ENGINE_BUKKIT_OPS_IS_NOT_DEFINED_DIMENSION, + ENGINE_BUKKIT_OPS_COULD_NOT_FIND_WITHIN_SEARCH_RANGE, + ENGINE_BUKKIT_OPS_TELEPORTING, + ENGINE_BUKKIT_OPS_AT, + IRIS_TOOLBELT_YOU_HAVE_BEEN_EVACUATED_FROM_THIS_WORLD, + IRIS_TOOLBELT_YOU_HAVE_BEEN_EVACUATED_FROM_THIS_WORLD_2, + SERVER_CONFIGURATOR_THERE_ARE_SOME_IRIS_PACKS_THAT_HAVE_CUSTOM_BIOMES_THEM, + SERVER_CONFIGURATOR_YOU_NEED_RESTART_YOUR_SERVER_USE_THESE_PACKS, + VIRTUAL_COMMAND_MESSAGE, + VIRTUAL_COMMAND_INSUFFICIENT_PERMISSIONS, + MORTAR_COMMAND_PARAMETERS_IGNORED + ); + + private BukkitRuntimeMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/BukkitUiMessages.java b/core/src/main/java/art/arcane/iris/core/localization/BukkitUiMessages.java new file mode 100644 index 000000000..e9b070109 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/BukkitUiMessages.java @@ -0,0 +1,70 @@ +package art.arcane.iris.core.localization; + +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; + +public final class BukkitUiMessages { + public static final TextKey SCOREBOARD_TITLE = TextKey.of( + "iris.bukkit.scoreboard.title", + C.GREEN + "Iris" + ); + public static final TextKey SCOREBOARD_SPEED = TextKey.of( + "iris.bukkit.scoreboard.speed", + C.GREEN + "Speed" + C.GRAY + ": {speed}/s {duration}" + ); + public static final TextKey SCOREBOARD_CACHE = TextKey.of( + "iris.bukkit.scoreboard.cache", + C.AQUA + "Cache" + C.GRAY + ": {count}" + ); + public static final TextKey SCOREBOARD_MANTLE = TextKey.of( + "iris.bukkit.scoreboard.mantle", + C.AQUA + "Mantle" + C.GRAY + ": {count}" + ); + public static final TextKey SCOREBOARD_CARVING = TextKey.of( + "iris.bukkit.scoreboard.carving", + C.LIGHT_PURPLE + "Carving" + C.GRAY + ": {state}" + ); + public static final TextKey SCOREBOARD_REGION = TextKey.of( + "iris.bukkit.scoreboard.region", + C.AQUA + "Region" + C.GRAY + ": {region}" + ); + public static final TextKey SCOREBOARD_BIOME = TextKey.of( + "iris.bukkit.scoreboard.biome", + C.AQUA + "Biome" + C.GRAY + ": {biome}" + ); + public static final TextKey SCOREBOARD_HEIGHT = TextKey.of( + "iris.bukkit.scoreboard.height", + C.AQUA + "Height" + C.GRAY + ": {height}" + ); + public static final TextKey SCOREBOARD_SLOPE = TextKey.of( + "iris.bukkit.scoreboard.slope", + C.AQUA + "Slope" + C.GRAY + ": {slope}" + ); + public static final TextKey SCOREBOARD_BLOCK_UPDATES = TextKey.of( + "iris.bukkit.scoreboard.block_updates", + C.AQUA + "BUD/s" + C.GRAY + ": {updates}" + ); + + private static final List KEYS = List.of( + SCOREBOARD_TITLE, + SCOREBOARD_SPEED, + SCOREBOARD_CACHE, + SCOREBOARD_MANTLE, + SCOREBOARD_CARVING, + SCOREBOARD_REGION, + SCOREBOARD_BIOME, + SCOREBOARD_HEIGHT, + SCOREBOARD_SLOPE, + SCOREBOARD_BLOCK_UPDATES + ); + + private BukkitUiMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/ClientUiMessages.java b/core/src/main/java/art/arcane/iris/core/localization/ClientUiMessages.java new file mode 100644 index 000000000..518461e8d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/ClientUiMessages.java @@ -0,0 +1,126 @@ +package art.arcane.iris.core.localization; + +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.PluralKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; +import java.util.Map; + +public final class ClientUiMessages { + public static final TextKey VISION_TITLE = TextKey.of( + "iris.client.vision.title", + "Iris Vision" + ); + public static final TextKey VISION_CONNECTING = TextKey.of( + "iris.client.vision.connecting", + "Connecting to Iris server..." + ); + public static final TextKey VISION_NOT_CONNECTED = TextKey.of( + "iris.client.vision.not_connected", + "not connected" + ); + public static final TextKey VISION_NOT_IRIS_WORLD = TextKey.of( + "iris.client.vision.not_iris_world", + "Not an Iris world" + ); + public static final TextKey VISION_SERVER_WITHOUT_IRIS = TextKey.of( + "iris.client.vision.server_without_iris", + "This server does not run Iris" + ); + public static final TextKey VISION_VERSION_MISMATCH = TextKey.of( + "iris.client.vision.version_mismatch", + "Iris version mismatch between client and server" + ); + public static final TextKey VISION_NO_DIMENSION_DATA = TextKey.of( + "iris.client.vision.no_dimension_data", + "no dimension data" + ); + public static final TextKey VISION_HEADER_DETAIL = TextKey.of( + "iris.client.vision.header_detail", + "{status} zoom {zoom} x{x} z{z}" + ); + public static final TextKey VISION_FOOTER_HINT = TextKey.of( + "iris.client.vision.footer_hint", + "Drag to pan Scroll to zoom Esc to close" + ); + public static final TextKey VISION_DIMENSION_PACK = TextKey.of( + "iris.client.vision.dimension_pack", + "{dimension} pack {pack}" + ); + public static final TextKey CREATE_STRUCTURES_REQUIRED_TITLE = TextKey.of( + "iris.client.create.structures_required_title", + "Iris requires Generate Structures" + ); + public static final TextKey CREATE_STRUCTURES_REQUIRED_BODY = TextKey.of( + "iris.client.create.structures_required_body", + "Iris places its own structures through the structure generation step, and refuses to load a world that was created with Generate Structures off. Turn Generate Structures back on, or choose a different world type." + ); + public static final TextKey TOAST_STUDIO_HOTLOAD = TextKey.of("iris.client.toast.studio_hotload", "Studio Hotload"); + public static final PluralKey TOAST_CHANGED_FILES = PluralKey.of( + "iris.client.toast.changed_files", + "count", + Map.of( + "one", "{count} file", + "other", "{count} files" + ) + ); + public static final TextKey TOAST_RELOAD_FAILED = TextKey.of("iris.client.toast.reload_failed", "reload failed"); + public static final TextKey TOAST_RELOADED = TextKey.of("iris.client.toast.reloaded", "reloaded"); + public static final TextKey TOAST_PACK_FAILED = TextKey.of("iris.client.toast.pack_failed", "{pack} failed"); + public static final TextKey WHAT_TITLE = TextKey.of("iris.client.what.title", "Iris What"); + public static final TextKey WHAT_QUERYING = TextKey.of("iris.client.what.querying", "Querying {x}, {z}..."); + public static final TextKey WHAT_BIOME = TextKey.of("iris.client.what.biome", "Biome: {biome}"); + public static final TextKey WHAT_REGION = TextKey.of("iris.client.what.region", "Region: {region}"); + public static final TextKey WHAT_CAVE = TextKey.of("iris.client.what.cave", "Cave: {cave}"); + public static final TextKey WHAT_HEIGHT = TextKey.of("iris.client.what.height", "Height: {height} ({x}, {z})"); + public static final TextKey PREGEN_STATS = TextKey.of("iris.client.pregen.stats", "{done} / {total} ({percent}%)"); + public static final TextKey PREGEN_PAUSED = TextKey.of("iris.client.pregen.paused", "PAUSED"); + public static final TextKey PREGEN_STALE = TextKey.of("iris.client.pregen.stale", "no updates for {seconds}s"); + public static final TextKey PREGEN_RATE = TextKey.of("iris.client.pregen.rate", "{rate}/s"); + public static final TextKey PREGEN_RATE_ETA = TextKey.of("iris.client.pregen.rate_eta", "{rate}/s ETA {eta}"); + public static final TextKey DURATION_HOURS_MINUTES = TextKey.of("iris.client.duration.hours_minutes", "{hours}h {minutes}m"); + public static final TextKey DURATION_MINUTES_SECONDS = TextKey.of("iris.client.duration.minutes_seconds", "{minutes}m {seconds}s"); + public static final TextKey DURATION_SECONDS = TextKey.of("iris.client.duration.seconds", "{seconds}s"); + + private static final List KEYS = List.of( + VISION_TITLE, + VISION_CONNECTING, + VISION_NOT_CONNECTED, + VISION_NOT_IRIS_WORLD, + VISION_SERVER_WITHOUT_IRIS, + VISION_VERSION_MISMATCH, + VISION_NO_DIMENSION_DATA, + VISION_HEADER_DETAIL, + VISION_FOOTER_HINT, + VISION_DIMENSION_PACK, + CREATE_STRUCTURES_REQUIRED_TITLE, + CREATE_STRUCTURES_REQUIRED_BODY, + TOAST_STUDIO_HOTLOAD, + TOAST_CHANGED_FILES, + TOAST_RELOAD_FAILED, + TOAST_RELOADED, + TOAST_PACK_FAILED, + WHAT_TITLE, + WHAT_QUERYING, + WHAT_BIOME, + WHAT_REGION, + WHAT_CAVE, + WHAT_HEIGHT, + PREGEN_STATS, + PREGEN_PAUSED, + PREGEN_STALE, + PREGEN_RATE, + PREGEN_RATE_ETA, + DURATION_HOURS_MINUTES, + DURATION_MINUTES_SECONDS, + DURATION_SECONDS + ); + + private ClientUiMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/DesktopUiMessages.java b/core/src/main/java/art/arcane/iris/core/localization/DesktopUiMessages.java new file mode 100644 index 000000000..af17ffe7e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/DesktopUiMessages.java @@ -0,0 +1,127 @@ +package art.arcane.iris.core.localization; + +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; + +public final class DesktopUiMessages { + public static final TextKey VISION_TITLE = TextKey.of("iris.desktop.vision.title", "Iris Vision"); + public static final TextKey VISION_VIEW = TextKey.of("iris.desktop.vision.view", "View:"); + public static final TextKey VISION_GRID = TextKey.of("iris.desktop.vision.grid", "Grid"); + public static final TextKey VISION_FOLLOW = TextKey.of("iris.desktop.vision.follow", "Follow"); + public static final TextKey VISION_LOW_QUALITY_SHORT = TextKey.of("iris.desktop.vision.low_quality_short", "LQ"); + public static final TextKey VISION_REFRESHING = TextKey.of("iris.desktop.vision.refreshing", "Refreshing"); + public static final TextKey VISION_FPS = TextKey.of("iris.desktop.vision.fps", "{fps} FPS"); + public static final TextKey VISION_ZOOM_RESET = TextKey.of("iris.desktop.vision.zoom_reset", "Zoom reset"); + public static final TextKey VISION_GRID_ENABLED = TextKey.of("iris.desktop.vision.grid_enabled", "Grid enabled"); + public static final TextKey VISION_GRID_DISABLED = TextKey.of("iris.desktop.vision.grid_disabled", "Grid disabled"); + public static final TextKey VISION_FOLLOWING = TextKey.of("iris.desktop.vision.following", "Following {player}"); + public static final TextKey VISION_NO_PLAYER = TextKey.of("iris.desktop.vision.no_player", "No player in world"); + public static final TextKey VISION_FOLLOW_DISABLED = TextKey.of("iris.desktop.vision.follow_disabled", "Follow disabled"); + public static final TextKey VISION_LOW_QUALITY = TextKey.of("iris.desktop.vision.low_quality", "Low quality"); + public static final TextKey VISION_HIGH_QUALITY = TextKey.of("iris.desktop.vision.high_quality", "High quality"); + public static final TextKey VISION_STATUS_LEFT = TextKey.of("iris.desktop.vision.status_left", "{mode} | {bpp} bpp | {width} x {height} blocks"); + public static final TextKey VISION_STATUS_RIGHT = TextKey.of("iris.desktop.vision.status_right", "X: {x} Z: {z} | {fps} FPS"); + public static final TextKey VISION_ENTITY_POSITION = TextKey.of("iris.desktop.vision.entity_position", "Position: {x}, {y}, {z}"); + public static final TextKey VISION_ENTITY_HEALTH = TextKey.of("iris.desktop.vision.entity_health", "Health: {health} / {maximum}"); + public static final TextKey VISION_BLOCK_POSITION = TextKey.of("iris.desktop.vision.block_position", "Block {x}, {z}"); + public static final TextKey VISION_CHUNK_POSITION = TextKey.of("iris.desktop.vision.chunk_position", "Chunk {x}, {z}"); + public static final TextKey VISION_REGION_POSITION = TextKey.of("iris.desktop.vision.region_position", "Region {x}, {z}"); + public static final TextKey VISION_BIOME_KEY = TextKey.of("iris.desktop.vision.biome_key", "Key: {key}"); + public static final TextKey VISION_BIOME_FILE = TextKey.of("iris.desktop.vision.biome_file", "File: {file}"); + public static final TextKey VISION_VELOCITY = TextKey.of("iris.desktop.vision.velocity", "Velocity: {velocity}"); + public static final TextKey VISION_TILES = TextKey.of("iris.desktop.vision.tiles", "Tiles: {high} HD / {low} LQ"); + public static final TextKey VISION_WORKERS = TextKey.of("iris.desktop.vision.workers", "Workers: {high} HD / {low} LQ"); + public static final TextKey VISION_CENTER = TextKey.of("iris.desktop.vision.center", "Center: {x}, {z}"); + public static final TextKey VISION_HELP_TOGGLE = TextKey.of("iris.desktop.vision.help.toggle", "Toggle help"); + public static final TextKey VISION_HELP_REFRESH = TextKey.of("iris.desktop.vision.help.refresh", "Refresh tiles"); + public static final TextKey VISION_HELP_FOLLOW = TextKey.of("iris.desktop.vision.help.follow", "Follow player"); + public static final TextKey VISION_HELP_ZOOM = TextKey.of("iris.desktop.vision.help.zoom", "Zoom in/out"); + public static final TextKey VISION_HELP_RESET_ZOOM = TextKey.of("iris.desktop.vision.help.reset_zoom", "Reset zoom"); + public static final TextKey VISION_HELP_CYCLE_MODE = TextKey.of("iris.desktop.vision.help.cycle_mode", "Cycle render mode"); + public static final TextKey VISION_HELP_QUALITY = TextKey.of("iris.desktop.vision.help.quality", "Toggle tile quality"); + public static final TextKey VISION_HELP_FPS = TextKey.of("iris.desktop.vision.help.fps", "Toggle 30/60 FPS"); + public static final TextKey VISION_HELP_GRID = TextKey.of("iris.desktop.vision.help.grid", "Toggle grid"); + public static final TextKey VISION_HELP_BIOME = TextKey.of("iris.desktop.vision.help.biome", "Detailed biome info"); + public static final TextKey VISION_HELP_TELEPORT = TextKey.of("iris.desktop.vision.help.teleport", "Teleport to cursor"); + public static final TextKey VISION_HELP_EDITOR = TextKey.of("iris.desktop.vision.help.editor", "Open biome in editor"); + public static final TextKey VISION_OPENED = TextKey.of("iris.desktop.vision.opened", "Opened {target}"); + public static final TextKey VISION_TELEPORTING = TextKey.of("iris.desktop.vision.teleporting", "Teleporting to {x}, {z}"); + public static final TextKey VISION_MODE_BIOME = TextKey.of("iris.desktop.vision.mode.biome", "Biome"); + public static final TextKey VISION_MODE_BIOME_LAND = TextKey.of("iris.desktop.vision.mode.biome_land", "Biome land"); + public static final TextKey VISION_MODE_BIOME_SEA = TextKey.of("iris.desktop.vision.mode.biome_sea", "Biome sea"); + public static final TextKey VISION_MODE_REGION = TextKey.of("iris.desktop.vision.mode.region", "Region"); + public static final TextKey VISION_MODE_CAVE_LAND = TextKey.of("iris.desktop.vision.mode.cave_land", "Cave land"); + public static final TextKey VISION_MODE_HEIGHT = TextKey.of("iris.desktop.vision.mode.height", "Height"); + public static final TextKey VISION_MODE_OBJECT_LOAD = TextKey.of("iris.desktop.vision.mode.object_load", "Object load"); + public static final TextKey VISION_MODE_DECORATOR_LOAD = TextKey.of("iris.desktop.vision.mode.decorator_load", "Decorator load"); + public static final TextKey VISION_MODE_CONTINENT = TextKey.of("iris.desktop.vision.mode.continent", "Continent"); + public static final TextKey VISION_MODE_LAYER_LOAD = TextKey.of("iris.desktop.vision.mode.layer_load", "Layer load"); + public static final TextKey NOISE_TITLE = TextKey.of("iris.desktop.noise.title", "Noise Explorer"); + public static final TextKey NOISE_TITLE_GENERATOR = TextKey.of("iris.desktop.noise.title_generator", "Noise Explorer: {generator}"); + public static final TextKey NOISE_SEARCH = TextKey.of("iris.desktop.noise.search", "Search..."); + public static final TextKey NOISE_STATUS = TextKey.of("iris.desktop.noise.status", "{name} | X: {x} Z: {z} | Zoom: {zoom} | Value: {value} | {fps} FPS"); + public static final TextKey NOISE_CATEGORY_CUSTOM = TextKey.of("iris.desktop.noise.category.custom", "Custom"); + public static final TextKey NOISE_CATEGORY_PACK_GENERATORS = TextKey.of("iris.desktop.noise.category.pack_generators", "Pack Generators"); + public static final TextKey NOISE_CATEGORY_SIMPLEX = TextKey.of("iris.desktop.noise.category.simplex", "Simplex"); + public static final TextKey NOISE_CATEGORY_PERLIN = TextKey.of("iris.desktop.noise.category.perlin", "Perlin"); + public static final TextKey NOISE_CATEGORY_CELLULAR = TextKey.of("iris.desktop.noise.category.cellular", "Cellular"); + public static final TextKey NOISE_CATEGORY_IRIS = TextKey.of("iris.desktop.noise.category.iris", "Iris"); + public static final TextKey NOISE_CATEGORY_CLOVER = TextKey.of("iris.desktop.noise.category.clover", "Clover"); + public static final TextKey NOISE_CATEGORY_HEXAGON = TextKey.of("iris.desktop.noise.category.hexagon", "Hexagon"); + public static final TextKey NOISE_CATEGORY_VASCULAR = TextKey.of("iris.desktop.noise.category.vascular", "Vascular"); + public static final TextKey NOISE_CATEGORY_GLOBE = TextKey.of("iris.desktop.noise.category.globe", "Globe"); + public static final TextKey NOISE_CATEGORY_CUBIC = TextKey.of("iris.desktop.noise.category.cubic", "Cubic"); + public static final TextKey NOISE_CATEGORY_FRACTAL = TextKey.of("iris.desktop.noise.category.fractal", "Fractal"); + public static final TextKey NOISE_CATEGORY_STATIC = TextKey.of("iris.desktop.noise.category.static", "Static"); + public static final TextKey NOISE_CATEGORY_NOWHERE = TextKey.of("iris.desktop.noise.category.nowhere", "Nowhere"); + public static final TextKey NOISE_CATEGORY_SIERPINSKI = TextKey.of("iris.desktop.noise.category.sierpinski", "Sierpinski"); + public static final TextKey NOISE_CATEGORY_UTILITY = TextKey.of("iris.desktop.noise.category.utility", "Utility"); + public static final TextKey NOISE_CATEGORY_OTHER = TextKey.of("iris.desktop.noise.category.other", "Other"); + public static final TextKey PREGEN_INITIALIZING = TextKey.of("iris.desktop.pregen.initializing", "Initializing..."); + public static final TextKey PREGEN_TITLE = TextKey.of("iris.desktop.pregen.title", "Pregen View"); + public static final TextKey PREGEN_METHOD_PENDING = TextKey.of("iris.desktop.pregen.method_pending", "Pending"); + public static final TextKey PREGEN_PAUSED = TextKey.of("iris.desktop.pregen.paused", "PAUSED"); + public static final TextKey PREGEN_RESUME_HINT = TextKey.of("iris.desktop.pregen.resume_hint", "Press P to resume"); + public static final TextKey PREGEN_PAUSE_HINT = TextKey.of("iris.desktop.pregen.pause_hint", "Press P to pause"); + public static final TextKey PREGEN_PROGRESS_PAUSED = TextKey.of("iris.desktop.pregen.progress_paused", "PAUSED {generated} of {total} ({percent} complete)"); + public static final TextKey PREGEN_PROGRESS_SAVING = TextKey.of("iris.desktop.pregen.progress_saving", "Saving... {generated} of {total} ({percent} complete)"); + public static final TextKey PREGEN_PROGRESS_GENERATING = TextKey.of("iris.desktop.pregen.progress_generating", "Generating {generated} of {total} ({percent} complete)"); + public static final TextKey PREGEN_SPEED = TextKey.of("iris.desktop.pregen.speed", "Speed: overall {overall}, 10s {tenSecond}, 30s {thirtySecond}, 60s {sixtySecond} chunks/s"); + public static final TextKey PREGEN_SPEED_CACHED = TextKey.of("iris.desktop.pregen.speed_cached", "Speed (cached): overall {overall}, 10s {tenSecond}, 30s {thirtySecond}, 60s {sixtySecond} chunks/s"); + public static final TextKey PREGEN_TIME = TextKey.of("iris.desktop.pregen.time", "{remaining} remaining ({elapsed} elapsed)"); + public static final TextKey PREGEN_METHOD = TextKey.of("iris.desktop.pregen.method", "Generation method: {method}"); + public static final TextKey PREGEN_MEMORY = TextKey.of("iris.desktop.pregen.memory", "Memory: {used} ({usage}) Pressure: {pressure}/s"); + + private static final List KEYS = List.of( + VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW, VISION_LOW_QUALITY_SHORT, + VISION_REFRESHING, VISION_FPS, VISION_ZOOM_RESET, VISION_GRID_ENABLED, VISION_GRID_DISABLED, + VISION_FOLLOWING, VISION_NO_PLAYER, VISION_FOLLOW_DISABLED, VISION_LOW_QUALITY, VISION_HIGH_QUALITY, + VISION_STATUS_LEFT, VISION_STATUS_RIGHT, VISION_ENTITY_POSITION, VISION_ENTITY_HEALTH, + VISION_BLOCK_POSITION, VISION_CHUNK_POSITION, VISION_REGION_POSITION, VISION_BIOME_KEY, + VISION_BIOME_FILE, VISION_VELOCITY, VISION_TILES, VISION_WORKERS, VISION_CENTER, + VISION_HELP_TOGGLE, VISION_HELP_REFRESH, VISION_HELP_FOLLOW, VISION_HELP_ZOOM, + VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_QUALITY, VISION_HELP_FPS, + VISION_HELP_GRID, VISION_HELP_BIOME, VISION_HELP_TELEPORT, VISION_HELP_EDITOR, VISION_OPENED, + VISION_TELEPORTING, VISION_MODE_BIOME, VISION_MODE_BIOME_LAND, VISION_MODE_BIOME_SEA, + VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD, + VISION_MODE_DECORATOR_LOAD, VISION_MODE_CONTINENT, VISION_MODE_LAYER_LOAD, NOISE_TITLE, + NOISE_TITLE_GENERATOR, NOISE_SEARCH, NOISE_STATUS, NOISE_CATEGORY_CUSTOM, + NOISE_CATEGORY_PACK_GENERATORS, NOISE_CATEGORY_SIMPLEX, NOISE_CATEGORY_PERLIN, + NOISE_CATEGORY_CELLULAR, NOISE_CATEGORY_IRIS, NOISE_CATEGORY_CLOVER, NOISE_CATEGORY_HEXAGON, + NOISE_CATEGORY_VASCULAR, NOISE_CATEGORY_GLOBE, NOISE_CATEGORY_CUBIC, NOISE_CATEGORY_FRACTAL, + NOISE_CATEGORY_STATIC, NOISE_CATEGORY_NOWHERE, NOISE_CATEGORY_SIERPINSKI, + NOISE_CATEGORY_UTILITY, NOISE_CATEGORY_OTHER, PREGEN_INITIALIZING, PREGEN_TITLE, + PREGEN_METHOD_PENDING, PREGEN_PAUSED, PREGEN_RESUME_HINT, PREGEN_PAUSE_HINT, + PREGEN_PROGRESS_PAUSED, PREGEN_PROGRESS_SAVING, PREGEN_PROGRESS_GENERATING, PREGEN_SPEED, + PREGEN_SPEED_CACHED, PREGEN_TIME, PREGEN_METHOD, PREGEN_MEMORY + ); + + private DesktopUiMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/DirectorCommandMessages.java b/core/src/main/java/art/arcane/iris/core/localization/DirectorCommandMessages.java new file mode 100644 index 000000000..f2f4986df --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/DirectorCommandMessages.java @@ -0,0 +1,1094 @@ +package art.arcane.iris.core.localization; + +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; + +public final class DirectorCommandMessages { + public static final TextKey COMMAND_DATAPACK_DIRECTOR_DOWNLOAD_MANAGE_EXTERNAL_DATAPACK_IMPORTS_MODRINTH = TextKey.of( + "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth", + "Download & manage external datapack imports (Modrinth)" + ); + public static final TextKey COMMAND_DATAPACK_DIRECTOR_DOWNLOAD_UPDATE_EVERY_DATAPACK_LISTED_PACK_DIMENSION_S_DATAPACKIMPORTS_INSTALL_IT_INTO = TextKey.of( + "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", + "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla" + ); + public static final TextKey COMMAND_DATAPACK_PARAM_RESTART_SERVER_WHEN_NEW_DATAPACKS_ARE_INSTALLED_REQUIRED_NEW_STRUCTURES_REGISTER_GENERATE = TextKey.of( + "iris.director.commanddatapack.param.restart_server_when_new_datapacks_are_installed_required_new_structures_register_generate", + "Restart the server when new datapacks are installed (required for new structures to register and generate)" + ); + public static final TextKey COMMAND_DATAPACK_DIRECTOR_LIST_CONFIGURED_DATAPACK_IMPORTS_THEIR_INSTALLED_VERSIONS = TextKey.of( + "iris.director.commanddatapack.director.list_configured_datapack_imports_their_installed_versions", + "List configured datapack imports and their installed versions" + ); + public static final TextKey COMMAND_DATAPACK_DIRECTOR_REMOVE_INSTALLED_DATAPACK_BY_ID_ALSO_DELETE_ITS_URL_FROM_DATAPACKIMPORTS_KEEP = TextKey.of( + "iris.director.commanddatapack.director.remove_installed_datapack_by_id_also_delete_its_url_from_datapackimports_keep", + "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)" + ); + public static final TextKey COMMAND_DATAPACK_PARAM_DATAPACK_ID_FOLDER_NAME_SHOWN_BY_IRIS_DATAPACK_LIST = TextKey.of( + "iris.director.commanddatapack.param.datapack_id_folder_name_shown_by_iris_datapack_list", + "The datapack id (folder name) shown by /iris datapack list" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER = TextKey.of( + "iris.director.commanddeveloper.director.iris_world_manager", + "Iris World Manager" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_GET_LOADED_TECTONICPLATES_COUNT = TextKey.of( + "iris.director.commanddeveloper.director.get_loaded_tectonicplates_count", + "Get Loaded TectonicPlates Count" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_SEND_TEST_EXCEPTION_SENTRY = TextKey.of( + "iris.director.commanddeveloper.director.send_test_exception_sentry", + "Send a test exception to sentry" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_HASH_GENERATED_BLOCK_OUTPUT_FIXED_AREA_DETERMINISM_IDENTITY_TESTING = TextKey.of( + "iris.director.commanddeveloper.director.hash_generated_block_output_fixed_area_determinism_identity_testing", + "Hash generated block output of a fixed area for determinism/identity testing" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_HASH = TextKey.of( + "iris.director.commanddeveloper.param.world_hash", + "The world to hash" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER = TextKey.of( + "iris.director.commanddeveloper.param.radius_chunks_around_center", + "Radius in chunks around the center" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X = TextKey.of( + "iris.director.commanddeveloper.param.center_chunk_x", + "Center chunk X" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_Z = TextKey.of( + "iris.director.commanddeveloper.param.center_chunk_z", + "Center chunk Z" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_UPDATE_PACK_WORLD_UNSAFE = TextKey.of( + "iris.director.commanddeveloper.director.update_pack_world_unsafe", + "Update the pack of a world (UNSAFE!)" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_UPDATE = TextKey.of( + "iris.director.commanddeveloper.param.world_update", + "The world to update" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_PACK_INSTALL_INTO_WORLD = TextKey.of( + "iris.director.commanddeveloper.param.pack_install_into_world", + "The pack to install into the world" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_MAKE_SURE_MAKE_BACKUP_READ_WARNINGS_FIRST = TextKey.of( + "iris.director.commanddeveloper.param.make_sure_make_backup_read_warnings_first", + "Make sure to make a backup & read the warnings first!" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_TEST = TextKey.of( + "iris.director.commanddeveloper.director.test", + "Test" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_DUMP_WHOLE_TECTONIC_PLATE_INSTEAD_SINGLE_SECTION = TextKey.of( + "iris.director.commanddeveloper.param.dump_whole_tectonic_plate_instead_single_section", + "Dump the whole tectonic plate instead of a single section" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_DUMP_FILE_ID_UNDER_PLUGINS_IRIS_DUMP_PV_ID = TextKey.of( + "iris.director.commanddeveloper.param.dump_file_id_under_plugins_iris_dump_pv_id", + "The dump file id under plugins/Iris/dump (pv..*)" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_TEST_2 = TextKey.of( + "iris.director.commanddeveloper.director.test_2", + "Test" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_PACK_BENCH = TextKey.of( + "iris.director.commanddeveloper.param.pack_bench", + "The pack to bench" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_RADIUS_REGIONS = TextKey.of( + "iris.director.commanddeveloper.param.radius_regions", + "Radius in regions" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_OPEN_GUI_WHILE_BENCHMARKING = TextKey.of( + "iris.director.commanddeveloper.param.open_gui_while_benchmarking", + "Open GUI while benchmarking" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_UPGRADE_ANOTHER_MINECRAFT_VERSION = TextKey.of( + "iris.director.commanddeveloper.director.upgrade_another_minecraft_version", + "Upgrade to another Minecraft version" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_VERSION_UPGRADE = TextKey.of( + "iris.director.commanddeveloper.param.version_upgrade", + "The version to upgrade to" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_TEST_3 = TextKey.of( + "iris.director.commanddeveloper.director.test_3", + "test" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_FOLDER_SCAN_MCA_REGION_FILES = TextKey.of( + "iris.director.commanddeveloper.param.world_folder_scan_mca_region_files", + "The world folder to scan for .mca region files" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_DELETE_NEARBY_CHUNK_BLOCKS_REGEN_TESTING = TextKey.of( + "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", + "Delete nearby chunk blocks for regen testing" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_YOUR_CURRENT_CHUNK = TextKey.of( + "iris.director.commanddeveloper.param.radius_chunks_around_your_current_chunk", + "Radius in chunks around your current chunk" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_TEST_4 = TextKey.of( + "iris.director.commanddeveloper.director.test_4", + "Test" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_DELETE_REGENERATE_NEARBY_CHUNKS_PLACE_USING_IRIS_GENERATION = TextKey.of( + "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", + "Delete and regenerate nearby chunks in place using Iris generation" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_RADIUS_NEARBY_CHUNKS = TextKey.of( + "iris.director.commanddeveloper.param.radius_nearby_chunks", + "The radius of nearby chunks" + ); + public static final TextKey COMMAND_DEVELOPER_DIRECTOR_GENERATE_CHUNKS_INTO_BUFFERS_NO_WORLD_WRITES_HASH_BLOCKS_BIOMES_CAPTURES_GOLDEN = TextKey.of( + "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", + "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Deletes the world's entire mantle - use on disposable test worlds." + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_SCAN = TextKey.of( + "iris.director.commanddeveloper.param.world_scan", + "The world to scan" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER_2 = TextKey.of( + "iris.director.commanddeveloper.param.radius_chunks_around_center_2", + "Radius in chunks around the center" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X_2 = TextKey.of( + "iris.director.commanddeveloper.param.center_chunk_x_2", + "Center chunk X" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_Z_2 = TextKey.of( + "iris.director.commanddeveloper.param.center_chunk_z_2", + "Center chunk Z" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_DELETE_MANTLE_DATA_SCAN_AREA_FIRST_FULL_REGENERATION_FROM_SCRATCH = TextKey.of( + "iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch", + "Delete the world's entire mantle folder first for full regeneration from scratch" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_CONCURRENT_CHUNK_GENERATIONS_1_STRICTLY_SERIAL_ORDER_DEPENDENCE_TESTING = TextKey.of( + "iris.director.commanddeveloper.param.concurrent_chunk_generations_1_strictly_serial_order_dependence_testing", + "Concurrent chunk generations; 1 = strictly serial for order-dependence testing" + ); + public static final TextKey COMMAND_DEVELOPER_PARAM_ALSO_DUMP_FULL_PER_CHUNK_NON_AIR_BLOCKSTATES_OFFLINE_DIFFING = TextKey.of( + "iris.director.commanddeveloper.param.also_dump_full_per_chunk_non_air_blockstates_offline_diffing", + "Also dump full per-chunk non-air blockstates for offline diffing" + ); + public static final TextKey COMMAND_EDIT_DIRECTOR_EDIT_SOMETHING = TextKey.of( + "iris.director.commandedit.director.edit_something", + "Edit something" + ); + public static final TextKey COMMAND_EDIT_DIRECTOR_EDIT_BIOME_YOU_SPECIFIED = TextKey.of( + "iris.director.commandedit.director.edit_biome_you_specified", + "Edit the biome you specified" + ); + public static final TextKey COMMAND_EDIT_PARAM_BIOME_EDIT = TextKey.of( + "iris.director.commandedit.param.biome_edit", + "The biome to edit" + ); + public static final TextKey COMMAND_EDIT_DIRECTOR_EDIT_REGION_YOU_SPECIFIED = TextKey.of( + "iris.director.commandedit.director.edit_region_you_specified", + "Edit the region you specified" + ); + public static final TextKey COMMAND_EDIT_PARAM_REGION_EDIT = TextKey.of( + "iris.director.commandedit.param.region_edit", + "The region to edit" + ); + public static final TextKey COMMAND_EDIT_DIRECTOR_EDIT_DIMENSION_YOU_SPECIFIED = TextKey.of( + "iris.director.commandedit.director.edit_dimension_you_specified", + "Edit the dimension you specified" + ); + public static final TextKey COMMAND_EDIT_PARAM_DIMENSION_EDIT = TextKey.of( + "iris.director.commandedit.param.dimension_edit", + "The dimension to edit" + ); + public static final TextKey COMMAND_FIND_DIRECTOR_IRIS_FIND_COMMANDS = TextKey.of( + "iris.director.commandfind.director.iris_find_commands", + "Iris Find commands" + ); + public static final TextKey COMMAND_FIND_DIRECTOR_FIND_BIOME = TextKey.of( + "iris.director.commandfind.director.find_biome", + "Find a biome" + ); + public static final TextKey COMMAND_FIND_PARAM_BIOME_LOOK = TextKey.of( + "iris.director.commandfind.param.biome_look", + "The biome to look for" + ); + public static final TextKey COMMAND_FIND_PARAM_SHOULD_YOU_BE_TELEPORTED = TextKey.of( + "iris.director.commandfind.param.should_you_be_teleported", + "Should you be teleported" + ); + public static final TextKey COMMAND_FIND_DIRECTOR_FIND_REGION = TextKey.of( + "iris.director.commandfind.director.find_region", + "Find a region" + ); + public static final TextKey COMMAND_FIND_PARAM_REGION_LOOK = TextKey.of( + "iris.director.commandfind.param.region_look", + "The region to look for" + ); + public static final TextKey COMMAND_FIND_PARAM_SHOULD_YOU_BE_TELEPORTED_2 = TextKey.of( + "iris.director.commandfind.param.should_you_be_teleported_2", + "Should you be teleported" + ); + public static final TextKey COMMAND_FIND_DIRECTOR_FIND_POINT_INTEREST = TextKey.of( + "iris.director.commandfind.director.find_point_interest", + "Find a point of interest." + ); + public static final TextKey COMMAND_FIND_PARAM_TYPE_POI_LOOK = TextKey.of( + "iris.director.commandfind.param.type_poi_look", + "The type of PoI to look for." + ); + public static final TextKey COMMAND_FIND_PARAM_SHOULD_YOU_BE_TELEPORTED_3 = TextKey.of( + "iris.director.commandfind.param.should_you_be_teleported_3", + "Should you be teleported" + ); + public static final TextKey COMMAND_FIND_DIRECTOR_FIND_STRUCTURE_VANILLA_KEY_LIKE_MINECRAFT_VILLAGE_PLAINS_MINECRAFT_STRONGHOLD_IMPORTED_IRIS = TextKey.of( + "iris.director.commandfind.director.find_structure_vanilla_key_like_minecraft_village_plains_minecraft_stronghold_imported_iris", + "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)" + ); + public static final TextKey COMMAND_FIND_PARAM_STRUCTURE_LOOK_E_G_MINECRAFT_VILLAGE_PLAINS_MINECRAFT_STRONGHOLD_MINECRAFT_ANCIENT_CITY = TextKey.of( + "iris.director.commandfind.param.structure_look_e_g_minecraft_village_plains_minecraft_stronghold_minecraft_ancient_city", + "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)" + ); + public static final TextKey COMMAND_FIND_DIRECTOR_FIND_OBJECT = TextKey.of( + "iris.director.commandfind.director.find_object", + "Find an object" + ); + public static final TextKey COMMAND_FIND_PARAM_OBJECT_LOOK = TextKey.of( + "iris.director.commandfind.param.object_look", + "The object to look for" + ); + public static final TextKey COMMAND_FIND_PARAM_SHOULD_YOU_BE_TELEPORTED_4 = TextKey.of( + "iris.director.commandfind.param.should_you_be_teleported_4", + "Should you be teleported" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_BASIC_COMMAND = TextKey.of( + "iris.director.commandiris.director.basic_command", + "Basic Command" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_CREATE_NEW_WORLD = TextKey.of( + "iris.director.commandiris.director.create_new_world", + "Create a new world" + ); + public static final TextKey COMMAND_IRIS_PARAM_NAME_WORLD_CREATE = TextKey.of( + "iris.director.commandiris.param.name_world_create", + "The name of the world to create" + ); + public static final TextKey COMMAND_IRIS_PARAM_DIMENSION_PACK_CREATE_WORLD_WITH = TextKey.of( + "iris.director.commandiris.param.dimension_pack_create_world_with", + "The dimension/pack to create the world with" + ); + public static final TextKey COMMAND_IRIS_PARAM_SEED_GENERATE_WORLD_WITH = TextKey.of( + "iris.director.commandiris.param.seed_generate_world_with", + "The seed to generate the world with" + ); + public static final TextKey COMMAND_IRIS_PARAM_REPLACE_EXACT_EXISTING_WORLD_SLOT_NEXT_RESTART = TextKey.of( + "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart", + "Replace the exact existing world slot on the next restart" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_TELEPORT_ANOTHER_WORLD = TextKey.of( + "iris.director.commandiris.director.teleport_another_world", + "Teleport to another world" + ); + public static final TextKey COMMAND_IRIS_PARAM_WORLD_TELEPORT = TextKey.of( + "iris.director.commandiris.param.world_teleport", + "World to teleport to" + ); + public static final TextKey COMMAND_IRIS_PARAM_PLAYER_TELEPORT = TextKey.of( + "iris.director.commandiris.param.player_teleport", + "Player to teleport" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_PRINT_VERSION_INFORMATION = TextKey.of( + "iris.director.commandiris.director.print_version_information", + "Print version information" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_BENCHMARK_PACK = TextKey.of( + "iris.director.commandiris.director.benchmark_pack", + "Benchmark a pack" + ); + public static final TextKey COMMAND_IRIS_PARAM_DIMENSION_BENCHMARK = TextKey.of( + "iris.director.commandiris.param.dimension_benchmark", + "Dimension to benchmark" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_PRINT_WORLD_HEIGHT_INFORMATION = TextKey.of( + "iris.director.commandiris.director.print_world_height_information", + "Print world height information" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_CHECK_ACCESS_ALL_WORLDS = TextKey.of( + "iris.director.commandiris.director.check_access_all_worlds", + "Check access of all worlds." + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_REMOVE_IRIS_WORLD = TextKey.of( + "iris.director.commandiris.director.remove_iris_world", + "Remove an Iris world" + ); + public static final TextKey COMMAND_IRIS_PARAM_WORLD_REMOVE = TextKey.of( + "iris.director.commandiris.param.world_remove", + "The world to remove" + ); + public static final TextKey COMMAND_IRIS_PARAM_WHETHER_ALSO_REMOVE_FOLDER_IF_SET_FALSE_JUST_DOES_NOT_LOAD_WORLD = TextKey.of( + "iris.director.commandiris.param.whether_also_remove_folder_if_set_false_just_does_not_load_world", + "Whether to also remove the folder (if set to false, just does not load the world)" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_TOGGLE_DEBUG = TextKey.of( + "iris.director.commandiris.director.toggle_debug", + "Toggle debug" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_DOWNLOAD_PROJECT = TextKey.of( + "iris.director.commandiris.director.download_project", + "Download a project." + ); + public static final TextKey COMMAND_IRIS_PARAM_PACK_DOWNLOAD = TextKey.of( + "iris.director.commandiris.param.pack_download", + "The pack to download" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_GET_METRICS_YOUR_WORLD = TextKey.of( + "iris.director.commandiris.director.get_metrics_your_world", + "Get metrics for your world" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_RELOAD_CONFIGURATION_FILE_THIS_IS_ALSO_DONE_AUTOMATICALLY = TextKey.of( + "iris.director.commandiris.director.reload_configuration_file_this_is_also_done_automatically", + "Reload configuration file (this is also done automatically)" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_UNLOAD_IRIS_WORLD = TextKey.of( + "iris.director.commandiris.director.unload_iris_world", + "Unload an Iris World" + ); + public static final TextKey COMMAND_IRIS_PARAM_WORLD_UNLOAD = TextKey.of( + "iris.director.commandiris.param.world_unload", + "The world to unload" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_LOAD_IRIS_WORLD = TextKey.of( + "iris.director.commandiris.director.load_iris_world", + "Load an Iris World" + ); + public static final TextKey COMMAND_IRIS_PARAM_NAME_WORLD_LOAD = TextKey.of( + "iris.director.commandiris.param.name_world_load", + "The name of the world to load" + ); + public static final TextKey COMMAND_IRIS_DIRECTOR_EVACUATE_IRIS_WORLD = TextKey.of( + "iris.director.commandiris.director.evacuate_iris_world", + "Evacuate an iris world" + ); + public static final TextKey COMMAND_IRIS_PARAM_EVACUATE_WORLD = TextKey.of( + "iris.director.commandiris.param.evacuate_world", + "Evacuate the world" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_IRIS_OBJECT_MANIPULATION = TextKey.of( + "iris.director.commandobject.director.iris_object_manipulation", + "Iris object manipulation" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_OPEN_OBJECT_STUDIO_WORLD_GRID_EVERY_OBJECT_DIMENSION_OPTIONAL_DEFAULTS_ALL_PACKS = TextKey.of( + "iris.director.commandobject.director.open_object_studio_world_grid_every_object_dimension_optional_defaults_all_packs", + "Open an object studio world (grid of every object; dimension optional, defaults to all packs)" + ); + public static final TextKey COMMAND_OBJECT_PARAM_OPTIONAL_DIMENSION_WHOSE_OBJECT_PACK_LAY_OUT_OMIT_AGGREGATE_OBJECTS_FROM_EVERY = TextKey.of( + "iris.director.commandobject.param.optional_dimension_whose_object_pack_lay_out_omit_aggregate_objects_from_every", + "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack" + ); + public static final TextKey COMMAND_OBJECT_PARAM_SEED_GENERATE_STUDIO_WITH = TextKey.of( + "iris.director.commandobject.param.seed_generate_studio_with", + "The seed to generate the studio with" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_CHECK_COMPOSITION_OBJECT = TextKey.of( + "iris.director.commandobject.director.check_composition_object", + "Check the composition of an object" + ); + public static final TextKey COMMAND_OBJECT_PARAM_OBJECT_ANALYZE = TextKey.of( + "iris.director.commandobject.param.object_analyze", + "The object to analyze" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_SHRINK_OBJECT_ITS_MINIMUM_SIZE = TextKey.of( + "iris.director.commandobject.director.shrink_object_its_minimum_size", + "Shrink an object to its minimum size" + ); + public static final TextKey COMMAND_OBJECT_PARAM_OBJECT_SHRINK = TextKey.of( + "iris.director.commandobject.param.object_shrink", + "The object to shrink" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_GROW_ORGANIC_BRANCHES_THROUGH_CANOPY_SO_EVERY_LEAF_SURVIVES_VANILLA_DECAY = TextKey.of( + "iris.director.commandobject.director.grow_organic_branches_through_canopy_so_every_leaf_survives_vanilla_decay", + "Grow organic branches through the canopy so every leaf survives vanilla decay" + ); + public static final TextKey COMMAND_OBJECT_PARAM_OBJECT_KEY_PREFIX_TREES_FILESYSTEM_PATH = TextKey.of( + "iris.director.commandobject.param.object_key_prefix_trees_filesystem_path", + "Object key, prefix (trees/), or filesystem path" + ); + public static final TextKey COMMAND_OBJECT_PARAM_DRYRUN_TRUE_ANALYZES_ONLY_WRITES_NOTHING = TextKey.of( + "iris.director.commandobject.param.dryrun_true_analyzes_only_writes_nothing", + "dryrun=true analyzes only, writes nothing" + ); + public static final TextKey COMMAND_OBJECT_PARAM_REACH_N_MAX_BRANCH_LENGTH_BLOCKS_FROM_EXISTING_WOOD_FARTHER_LEAF_CLUSTERS = TextKey.of( + "iris.director.commandobject.param.reach_n_max_branch_length_blocks_from_existing_wood_farther_leaf_clusters", + "reach=N max branch length in blocks from existing wood; farther leaf clusters are pinned persistent instead. reach=0 grows unlimited" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_CONVERT_SCHEM_FILES_CONVERT_FOLDER_IOB_FILES = TextKey.of( + "iris.director.commandobject.director.convert_schem_files_convert_folder_iob_files", + "Convert .schem files in the 'convert' folder to .iob files." + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_GET_POWDER_THAT_REVEALS_OBJECTS = TextKey.of( + "iris.director.commandobject.director.get_powder_that_reveals_objects", + "Get a powder that reveals objects" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_CONTRACT_SELECTION_BASED_ON_YOUR_LOOKING_DIRECTION = TextKey.of( + "iris.director.commandobject.director.contract_selection_based_on_your_looking_direction", + "Contract a selection based on your looking direction" + ); + public static final TextKey COMMAND_OBJECT_PARAM_AMOUNT_INSET_BY = TextKey.of( + "iris.director.commandobject.param.amount_inset_by", + "The amount to inset by" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_SET_POINT_1_LOOK = TextKey.of( + "iris.director.commandobject.director.set_point_1_look", + "Set point 1 to look" + ); + public static final TextKey COMMAND_OBJECT_PARAM_WHETHER_USE_YOUR_CURRENT_POSITION_WHERE_YOU_LOOK = TextKey.of( + "iris.director.commandobject.param.whether_use_your_current_position_where_you_look", + "Whether to use your current position, or where you look" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_SET_POINT_2_LOOK = TextKey.of( + "iris.director.commandobject.director.set_point_2_look", + "Set point 2 to look" + ); + public static final TextKey COMMAND_OBJECT_PARAM_WHETHER_USE_YOUR_CURRENT_POSITION_WHERE_YOU_LOOK_2 = TextKey.of( + "iris.director.commandobject.param.whether_use_your_current_position_where_you_look_2", + "Whether to use your current position, or where you look" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_PASTE_OBJECT = TextKey.of( + "iris.director.commandobject.director.paste_object", + "Paste an object" + ); + public static final TextKey COMMAND_OBJECT_PARAM_OBJECT_PASTE = TextKey.of( + "iris.director.commandobject.param.object_paste", + "The object to paste" + ); + public static final TextKey COMMAND_OBJECT_PARAM_WHETHER_NOT_EDIT_OBJECT_NEED_HOLD_WAND = TextKey.of( + "iris.director.commandobject.param.whether_not_edit_object_need_hold_wand", + "Whether or not to edit the object (need to hold wand)" + ); + public static final TextKey COMMAND_OBJECT_PARAM_AMOUNT_DEGREES_ROTATE_BY = TextKey.of( + "iris.director.commandobject.param.amount_degrees_rotate_by", + "The amount of degrees to rotate by" + ); + public static final TextKey COMMAND_OBJECT_PARAM_FACTOR_BY_WHICH_SCALE_OBJECT_PLACEMENT = TextKey.of( + "iris.director.commandobject.param.factor_by_which_scale_object_placement", + "The factor by which to scale the object placement" + ); + public static final TextKey COMMAND_OBJECT_PARAM_SCALE_INTERPOLATOR_USE = TextKey.of( + "iris.director.commandobject.param.scale_interpolator_use", + "The scale interpolator to use" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_SAVE_OBJECT = TextKey.of( + "iris.director.commandobject.director.save_object", + "Save an object" + ); + public static final TextKey COMMAND_OBJECT_PARAM_DIMENSION_STORE_OBJECT = TextKey.of( + "iris.director.commandobject.param.dimension_store_object", + "The dimension to store the object in" + ); + public static final TextKey COMMAND_OBJECT_PARAM_FILE_STORE_IT_CAN_USE_SUBFOLDERS = TextKey.of( + "iris.director.commandobject.param.file_store_it_can_use_subfolders", + "The file to store it in, can use / for subfolders" + ); + public static final TextKey COMMAND_OBJECT_PARAM_OVERWRITE_EXISTING_OBJECT_FILES = TextKey.of( + "iris.director.commandobject.param.overwrite_existing_object_files", + "Overwrite existing object files" + ); + public static final TextKey COMMAND_OBJECT_PARAM_USE_LEGACY_TILESTATE_SERIALIZATION_IF_POSSIBLE = TextKey.of( + "iris.director.commandobject.param.use_legacy_tilestate_serialization_if_possible", + "Use legacy TileState serialization if possible" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_SHIFT_SELECTION_YOUR_LOOKING_DIRECTION = TextKey.of( + "iris.director.commandobject.director.shift_selection_your_looking_direction", + "Shift a selection in your looking direction" + ); + public static final TextKey COMMAND_OBJECT_PARAM_AMOUNT_SHIFT_BY = TextKey.of( + "iris.director.commandobject.param.amount_shift_by", + "The amount to shift by" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_UNDO_NUMBER_PASTES = TextKey.of( + "iris.director.commandobject.director.undo_number_pastes", + "Undo a number of pastes" + ); + public static final TextKey COMMAND_OBJECT_PARAM_AMOUNT_PASTES_UNDO = TextKey.of( + "iris.director.commandobject.param.amount_pastes_undo", + "The amount of pastes to undo" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_GETS_OBJECT_WAND_GRABS_CURRENT_WORLDEDIT_SELECTION = TextKey.of( + "iris.director.commandobject.director.gets_object_wand_grabs_current_worldedit_selection", + "Gets an object wand and grabs the current WorldEdit selection." + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_GET_OBJECT_WAND = TextKey.of( + "iris.director.commandobject.director.get_object_wand", + "Get an object wand" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_AUTOSELECT_UP_DOWN_OUT = TextKey.of( + "iris.director.commandobject.director.autoselect_up_down_out", + "Autoselect up, down & out" + ); + public static final TextKey COMMAND_OBJECT_DIRECTOR_AUTOSELECT_UP_OUT = TextKey.of( + "iris.director.commandobject.director.autoselect_up_out", + "Autoselect up & out" + ); + public static final TextKey COMMAND_PACK_DIRECTOR_PACK_VALIDATION_MAINTENANCE = TextKey.of( + "iris.director.commandpack.director.pack_validation_maintenance", + "Pack validation and maintenance" + ); + public static final TextKey COMMAND_PACK_DIRECTOR_VALIDATE_PACK_ALL_PACKS_RE_PUBLISH_RESULTS = TextKey.of( + "iris.director.commandpack.director.validate_pack_all_packs_re_publish_results", + "Validate a pack (or all packs) and re-publish results" + ); + public static final TextKey COMMAND_PACK_PARAM_PACK_FOLDER_NAME_VALIDATE_LEAVE_EMPTY_ALL = TextKey.of( + "iris.director.commandpack.param.pack_folder_name_validate_leave_empty_all", + "The pack folder name to validate (leave empty for all)" + ); + public static final TextKey COMMAND_PACK_DIRECTOR_PREVIEW_APPLY_UNUSED_RESOURCE_CLEANUP = TextKey.of( + "iris.director.commandpack.director.preview_apply_unused_resource_cleanup", + "Preview or apply unused-resource cleanup" + ); + public static final TextKey COMMAND_PACK_PARAM_PACK_FOLDER_NAME_CLEAN = TextKey.of( + "iris.director.commandpack.param.pack_folder_name_clean", + "The pack folder name to clean" + ); + public static final TextKey COMMAND_PACK_PARAM_PREVIEW_APPLY = TextKey.of( + "iris.director.commandpack.param.preview_apply", + "preview or apply" + ); + public static final TextKey COMMAND_PACK_DIRECTOR_PREVIEW_APPLY_RESTORATION_LATEST_QUARANTINE = TextKey.of( + "iris.director.commandpack.director.preview_apply_restoration_latest_quarantine", + "Preview or apply restoration of the latest quarantine" + ); + public static final TextKey COMMAND_PACK_PARAM_PACK_FOLDER_NAME_RESTORE = TextKey.of( + "iris.director.commandpack.param.pack_folder_name_restore", + "The pack folder name to restore" + ); + public static final TextKey COMMAND_PACK_PARAM_PREVIEW_APPLY_2 = TextKey.of( + "iris.director.commandpack.param.preview_apply_2", + "preview or apply" + ); + public static final TextKey COMMAND_PACK_DIRECTOR_SHOW_CACHED_VALIDATION_STATUS_PACK = TextKey.of( + "iris.director.commandpack.director.show_cached_validation_status_pack", + "Show cached validation status for a pack" + ); + public static final TextKey COMMAND_PACK_PARAM_PACK_FOLDER_NAME = TextKey.of( + "iris.director.commandpack.param.pack_folder_name", + "The pack folder name" + ); + public static final TextKey COMMAND_PREGEN_DIRECTOR_PREGENERATE_YOUR_IRIS_WORLDS = TextKey.of( + "iris.director.commandpregen.director.pregenerate_your_iris_worlds", + "Pregenerate your Iris worlds!" + ); + public static final TextKey COMMAND_PREGEN_DIRECTOR_PREGENERATE_WORLD = TextKey.of( + "iris.director.commandpregen.director.pregenerate_world", + "Pregenerate a world" + ); + public static final TextKey COMMAND_PREGEN_PARAM_RADIUS_PREGEN_BLOCKS = TextKey.of( + "iris.director.commandpregen.param.radius_pregen_blocks", + "The radius of the pregen in blocks" + ); + public static final TextKey COMMAND_PREGEN_PARAM_WORLD_PREGEN = TextKey.of( + "iris.director.commandpregen.param.world_pregen", + "The world to pregen" + ); + public static final TextKey COMMAND_PREGEN_PARAM_CENTER_LOCATION_PREGEN_USE_ME_YOUR_CURRENT_LOCATION = TextKey.of( + "iris.director.commandpregen.param.center_location_pregen_use_me_your_current_location", + "The center location of the pregen. Use \"me\" for your current location" + ); + public static final TextKey COMMAND_PREGEN_PARAM_OPEN_IRIS_PREGEN_GUI = TextKey.of( + "iris.director.commandpregen.param.open_iris_pregen_gui", + "Open the Iris pregen gui" + ); + public static final TextKey COMMAND_PREGEN_PARAM_GENERATE_ONLY_ONE_CHUNK_AT_TIME = TextKey.of( + "iris.director.commandpregen.param.generate_only_one_chunk_at_time", + "Generate only one chunk at a time" + ); + public static final TextKey COMMAND_PREGEN_DIRECTOR_STOP_ACTIVE_PREGENERATION_TASK = TextKey.of( + "iris.director.commandpregen.director.stop_active_pregeneration_task", + "Stop the active pregeneration task" + ); + public static final TextKey COMMAND_PREGEN_DIRECTOR_PAUSE_CONTINUE_ACTIVE_PREGENERATION_TASK = TextKey.of( + "iris.director.commandpregen.director.pause_continue_active_pregeneration_task", + "Pause / continue the active pregeneration task" + ); + public static final TextKey COMMAND_PREGEN_DIRECTOR_SHOW_ACTIVE_PREGENERATION_STATUS = TextKey.of( + "iris.director.commandpregen.director.show_active_pregeneration_status", + "Show the active pregeneration status" + ); + public static final TextKey COMMAND_STRUCTURE_DIRECTOR_IRIS_STRUCTURE_TOOLS_INDEX_IMPORT_INFO = TextKey.of( + "iris.director.commandstructure.director.iris_structure_tools_index_import_info", + "Iris structure tools (index, import, info)" + ); + public static final TextKey COMMAND_STRUCTURE_DIRECTOR_REGENERATE_STRUCTURE_INDEX_JSON_LISTING_ALL_VANILLA_DATAPACK_IRIS_STRUCTURES = TextKey.of( + "iris.director.commandstructure.director.regenerate_structure_index_json_listing_all_vanilla_datapack_iris_structures", + "Regenerate structure-index.json listing all vanilla, datapack & iris structures" + ); + public static final TextKey COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_INDEX = TextKey.of( + "iris.director.commandstructure.param.dimension_whose_pack_index", + "The dimension whose pack to index" + ); + public static final TextKey COMMAND_STRUCTURE_DIRECTOR_IMPORT_EVERY_STRUCTURE_VANILLA_INGESTED_DATAPACKS_INTO_THIS_PACK_AS_EDITABLE_IRIS = TextKey.of( + "iris.director.commandstructure.director.import_every_structure_vanilla_ingested_datapacks_into_this_pack_as_editable_iris", + "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place." + ); + public static final TextKey COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_IMPORT_INTO = TextKey.of( + "iris.director.commandstructure.param.dimension_whose_pack_import_into", + "The dimension whose pack to import into" + ); + public static final TextKey COMMAND_STRUCTURE_DIRECTOR_CAPTURE_CODE_GENERATED_STRUCTURES_THAT_HAVE_NO_NBT_TEMPLATE_SWAMP_HUTS_IGLOOS = TextKey.of( + "iris.director.commandstructure.director.capture_code_generated_structures_that_have_no_nbt_template_swamp_huts_igloos", + "Capture code-generated structures that have no NBT template (swamp huts, igloos, etc.) into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. Skips structures that already import as a structure, structures wider/taller than the capture cap (strongholds, mansions, monuments stay vanilla), and anything that will not generate in a flat overworld. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. Runs automatically as the last pass of /iris structure import." + ); + public static final TextKey COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_CAPTURE_INTO = TextKey.of( + "iris.director.commandstructure.param.dimension_whose_pack_capture_into", + "The dimension whose pack to capture into" + ); + public static final TextKey COMMAND_STRUCTURE_DIRECTOR_VERIFY_NATIVE_STRUCTURE_ELIGIBILITY_LOCATE_IRIS_PLACED_STRUCTURES_WITHOUT_RUNNING_BLOCKING_NATIVE = TextKey.of( + "iris.director.commandstructure.director.verify_native_structure_eligibility_locate_iris_placed_structures_without_running_blocking_native", + "Verify native structure eligibility and locate Iris-placed structures without running blocking native searches." + ); + public static final TextKey COMMAND_STRUCTURE_PARAM_DIMENSION_VERIFY = TextKey.of( + "iris.director.commandstructure.param.dimension_verify", + "The dimension to verify" + ); + public static final TextKey COMMAND_STRUCTURE_PARAM_SEARCH_RADIUS_CHUNKS_AROUND_ORIGIN_LARGER_IS_MUCH_SLOWER = TextKey.of( + "iris.director.commandstructure.param.search_radius_chunks_around_origin_larger_is_much_slower", + "Search radius in chunks around the origin (larger is much slower)" + ); + public static final TextKey COMMAND_STRUCTURE_DIRECTOR_RESOLVE_IRIS_STRUCTURE_S_JIGSAW_GRAPH_REPORT_PIECE_COUNT_BOUNDS = TextKey.of( + "iris.director.commandstructure.director.resolve_iris_structure_s_jigsaw_graph_report_piece_count_bounds", + "Resolve an iris structure's jigsaw graph and report piece count & bounds" + ); + public static final TextKey COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_HOLDS_STRUCTURE = TextKey.of( + "iris.director.commandstructure.param.dimension_whose_pack_holds_structure", + "The dimension whose pack holds the structure" + ); + public static final TextKey COMMAND_STRUCTURE_PARAM_IRIS_STRUCTURE_KEY_INSPECT = TextKey.of( + "iris.director.commandstructure.param.iris_structure_key_inspect", + "The iris structure key to inspect" + ); + public static final TextKey COMMAND_STRUCTURE_DIRECTOR_ASSEMBLE_PLACE_IRIS_STRUCTURE_AT_YOUR_LOCATION_STUDIO_TESTING = TextKey.of( + "iris.director.commandstructure.director.assemble_place_iris_structure_at_your_location_studio_testing", + "Assemble and place an iris structure at your location (studio testing)" + ); + public static final TextKey COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_HOLDS_STRUCTURE_2 = TextKey.of( + "iris.director.commandstructure.param.dimension_whose_pack_holds_structure_2", + "The dimension whose pack holds the structure" + ); + public static final TextKey COMMAND_STRUCTURE_PARAM_IRIS_STRUCTURE_KEY_PLACE = TextKey.of( + "iris.director.commandstructure.param.iris_structure_key_place", + "The iris structure key to place" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_STUDIO_COMMANDS = TextKey.of( + "iris.director.commandstudio.director.studio_commands", + "Studio Commands" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_OPEN_NEW_STUDIO_WORLD = TextKey.of( + "iris.director.commandstudio.director.open_new_studio_world", + "Open a new studio world" + ); + public static final TextKey COMMAND_STUDIO_PARAM_DIMENSION_PACK_OPEN_STUDIO = TextKey.of( + "iris.director.commandstudio.param.dimension_pack_open_studio", + "The dimension pack to open a studio for" + ); + public static final TextKey COMMAND_STUDIO_PARAM_SEED_GENERATE_STUDIO_WITH = TextKey.of( + "iris.director.commandstudio.param.seed_generate_studio_with", + "The seed to generate the studio with" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_IMPORT_VANILLA_TREES_MUSHROOMS_OBJECTS_STRUCTURES_JIGSAW_FROM_SERVER_INTO_PACK_S = TextKey.of( + "iris.director.commandstudio.director.import_vanilla_trees_mushrooms_objects_structures_jigsaw_from_server_into_pack_s", + "Import vanilla trees, mushrooms & objects (and structures/jigsaw) from the server into a pack's objects/vanilla folder" + ); + public static final TextKey COMMAND_STUDIO_PARAM_DIMENSION_PACK_IMPORT_VANILLA_CONTENT_INTO = TextKey.of( + "iris.director.commandstudio.param.dimension_pack_import_vanilla_content_into", + "The dimension pack to import vanilla content into" + ); + public static final TextKey COMMAND_STUDIO_PARAM_HOW_MANY_VARIANTS_CAPTURE_PER_TREE_OBJECT_FEATURE = TextKey.of( + "iris.director.commandstudio.param.how_many_variants_capture_per_tree_object_feature", + "How many variants to capture per tree/object feature" + ); + public static final TextKey COMMAND_STUDIO_PARAM_ALSO_IMPORT_VANILLA_DATAPACK_STRUCTURES_JIGSAW_INTO_PACK = TextKey.of( + "iris.director.commandstudio.param.also_import_vanilla_datapack_structures_jigsaw_into_pack", + "Also import vanilla & datapack structures/jigsaw into the pack" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_OPEN_VSCODE_DIMENSION = TextKey.of( + "iris.director.commandstudio.director.open_vscode_dimension", + "Open VSCode for a dimension" + ); + public static final TextKey COMMAND_STUDIO_PARAM_DIMENSION_OPEN_VSCODE = TextKey.of( + "iris.director.commandstudio.param.dimension_open_vscode", + "The dimension to open VSCode for" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_CLOSE_OPEN_STUDIO_PROJECT = TextKey.of( + "iris.director.commandstudio.director.close_open_studio_project", + "Close an open studio project" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_TOGGLE_YOUR_STUDIO_DEBUG_SCOREBOARD = TextKey.of( + "iris.director.commandstudio.director.toggle_your_studio_debug_scoreboard", + "Toggle your Studio debug scoreboard" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_CREATE_NEW_STUDIO_PROJECT = TextKey.of( + "iris.director.commandstudio.director.create_new_studio_project", + "Create a new studio project" + ); + public static final TextKey COMMAND_STUDIO_PARAM_NAME_THIS_NEW_IRIS_PROJECT = TextKey.of( + "iris.director.commandstudio.param.name_this_new_iris_project", + "The name of this new Iris Project." + ); + public static final TextKey COMMAND_STUDIO_PARAM_COPY_CONTENTS_EXISTING_PROJECT_YOUR_PACKS_FOLDER_USE_IT_AS_TEMPLATE_THIS = TextKey.of( + "iris.director.commandstudio.param.copy_contents_existing_project_your_packs_folder_use_it_as_template_this", + "Copy the contents of an existing project in your packs folder and use it as a template in this new project." + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_GET_VERSION_PACK = TextKey.of( + "iris.director.commandstudio.director.get_version_pack", + "Get the version of a pack" + ); + public static final TextKey COMMAND_STUDIO_PARAM_DIMENSION_GET_VERSION = TextKey.of( + "iris.director.commandstudio.param.dimension_get_version", + "The dimension get the version of" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_OPEN_NOISE_EXPLORER_EXTERNAL_GUI = TextKey.of( + "iris.director.commandstudio.director.open_noise_explorer_external_gui", + "Open the noise explorer (External GUI)" + ); + public static final TextKey COMMAND_STUDIO_PARAM_OPTIONAL_PACK_GENERATOR_PREVIEW = TextKey.of( + "iris.director.commandstudio.param.optional_pack_generator_preview", + "Optional pack generator to preview" + ); + public static final TextKey COMMAND_STUDIO_PARAM_SEED_PREVIEW_GENERATOR_WITH = TextKey.of( + "iris.director.commandstudio.param.seed_preview_generator_with", + "The seed to preview the generator with" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_SHOW_LOOT_IF_CHEST_WERE_RIGHT_HERE = TextKey.of( + "iris.director.commandstudio.director.show_loot_if_chest_were_right_here", + "Show loot if a chest were right here" + ); + public static final TextKey COMMAND_STUDIO_PARAM_FAST_INSERTION_ITEMS_VIRTUAL_INVENTORY_MAY_CAUSE_PERFORMANCE_DROP = TextKey.of( + "iris.director.commandstudio.param.fast_insertion_items_virtual_inventory_may_cause_performance_drop", + "Fast insertion of items in virtual inventory (may cause performance drop)" + ); + public static final TextKey COMMAND_STUDIO_PARAM_WHETHER_NOT_APPEND_INVENTORY_CURRENTLY_OPEN_IF_FALSE_CLEARS_OPENED_INVENTORY = TextKey.of( + "iris.director.commandstudio.param.whether_not_append_inventory_currently_open_if_false_clears_opened_inventory", + "Whether or not to append to the inventory currently open (if false, clears opened inventory)" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_CALCULATE_CHANCE_EACH_REGION_GENERATE = TextKey.of( + "iris.director.commandstudio.director.calculate_chance_each_region_generate", + "Calculate the chance for each region to generate" + ); + public static final TextKey COMMAND_STUDIO_PARAM_RADIUS_CHUNKS = TextKey.of( + "iris.director.commandstudio.param.radius_chunks", + "The radius in chunks" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_RENDER_WORLD_MAP_EXTERNAL_GUI = TextKey.of( + "iris.director.commandstudio.director.render_world_map_external_gui", + "Render a world map (External GUI)" + ); + public static final TextKey COMMAND_STUDIO_PARAM_WORLD_OPEN_GENERATOR = TextKey.of( + "iris.director.commandstudio.param.world_open_generator", + "The world to open the generator for" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_PACKAGE_DIMENSION_INTO_COMPRESSED_FORMAT = TextKey.of( + "iris.director.commandstudio.director.package_dimension_into_compressed_format", + "Package a dimension into a compressed format" + ); + public static final TextKey COMMAND_STUDIO_PARAM_DIMENSION_PACK_COMPRESS = TextKey.of( + "iris.director.commandstudio.param.dimension_pack_compress", + "The dimension pack to compress" + ); + public static final TextKey COMMAND_STUDIO_PARAM_WHETHER_NOT_OBFUSCATE_PACK = TextKey.of( + "iris.director.commandstudio.param.whether_not_obfuscate_pack", + "Whether or not to obfuscate the pack" + ); + public static final TextKey COMMAND_STUDIO_PARAM_WHETHER_NOT_MINIFY_PACK = TextKey.of( + "iris.director.commandstudio.param.whether_not_minify_pack", + "Whether or not to minify the pack" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_PROFILES_PERFORMANCE_DIMENSION = TextKey.of( + "iris.director.commandstudio.director.profiles_performance_dimension", + "Profiles the performance of a dimension" + ); + public static final TextKey COMMAND_STUDIO_PARAM_DIMENSION_PROFILE = TextKey.of( + "iris.director.commandstudio.param.dimension_profile", + "The dimension to profile" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_SPAWN_IRIS_ENTITY = TextKey.of( + "iris.director.commandstudio.director.spawn_iris_entity", + "Spawn an Iris entity" + ); + public static final TextKey COMMAND_STUDIO_PARAM_ENTITY_SPAWN = TextKey.of( + "iris.director.commandstudio.param.entity_spawn", + "The entity to spawn" + ); + public static final TextKey COMMAND_STUDIO_PARAM_LOCATION_SPAWN_ENTITY_AT = TextKey.of( + "iris.director.commandstudio.param.location_spawn_entity_at", + "The location to spawn the entity at" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_TELEPORT_ACTIVE_STUDIO_WORLD = TextKey.of( + "iris.director.commandstudio.director.teleport_active_studio_world", + "Teleport to the active studio world" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_UPDATE_YOUR_DIMENSION_PROJECTS_VSCODE_WORKSPACE = TextKey.of( + "iris.director.commandstudio.director.update_your_dimension_projects_vscode_workspace", + "Update your dimension projects VSCode workspace" + ); + public static final TextKey COMMAND_STUDIO_PARAM_DIMENSION_UPDATE_WORKSPACE = TextKey.of( + "iris.director.commandstudio.param.dimension_update_workspace", + "The dimension to update the workspace of" + ); + public static final TextKey COMMAND_STUDIO_DIRECTOR_CAPTURE_IGENDATA_CHUNK_REPORT_NEARBY_CHUNKS = TextKey.of( + "iris.director.commandstudio.director.capture_igendata_chunk_report_nearby_chunks", + "Capture an IGenData chunk report for nearby chunks" + ); + public static final TextKey COMMAND_WHAT_DIRECTOR_IRIS_WHAT = TextKey.of( + "iris.director.commandwhat.director.iris_what", + "Iris What?" + ); + public static final TextKey COMMAND_WHAT_DIRECTOR_WHAT_IS_MY_HAND = TextKey.of( + "iris.director.commandwhat.director.what_is_my_hand", + "What is in my hand?" + ); + public static final TextKey COMMAND_WHAT_DIRECTOR_WHAT_BIOME_AM_I = TextKey.of( + "iris.director.commandwhat.director.what_biome_am_i", + "What biome am i in?" + ); + public static final TextKey COMMAND_WHAT_DIRECTOR_WHAT_REGION_AM_I = TextKey.of( + "iris.director.commandwhat.director.what_region_am_i", + "What region am i in?" + ); + public static final TextKey COMMAND_WHAT_DIRECTOR_WHAT_BLOCK_AM_I_LOOKING_AT = TextKey.of( + "iris.director.commandwhat.director.what_block_am_i_looking_at", + "What block am i looking at?" + ); + public static final TextKey COMMAND_WHAT_DIRECTOR_SHOW_MARKERS_CHUNK = TextKey.of( + "iris.director.commandwhat.director.show_markers_chunk", + "Show markers in chunk" + ); + public static final TextKey COMMAND_WHAT_PARAM_MARKER_NAME_SUCH_AS_CAVE_FLOOR_CAVE_CEILING = TextKey.of( + "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling", + "Marker name such as cave_floor or cave_ceiling" + ); + + private static final List KEYS = List.of( + COMMAND_DATAPACK_DIRECTOR_DOWNLOAD_MANAGE_EXTERNAL_DATAPACK_IMPORTS_MODRINTH, + COMMAND_DATAPACK_DIRECTOR_DOWNLOAD_UPDATE_EVERY_DATAPACK_LISTED_PACK_DIMENSION_S_DATAPACKIMPORTS_INSTALL_IT_INTO, + COMMAND_DATAPACK_PARAM_RESTART_SERVER_WHEN_NEW_DATAPACKS_ARE_INSTALLED_REQUIRED_NEW_STRUCTURES_REGISTER_GENERATE, + COMMAND_DATAPACK_DIRECTOR_LIST_CONFIGURED_DATAPACK_IMPORTS_THEIR_INSTALLED_VERSIONS, + COMMAND_DATAPACK_DIRECTOR_REMOVE_INSTALLED_DATAPACK_BY_ID_ALSO_DELETE_ITS_URL_FROM_DATAPACKIMPORTS_KEEP, + COMMAND_DATAPACK_PARAM_DATAPACK_ID_FOLDER_NAME_SHOWN_BY_IRIS_DATAPACK_LIST, + COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER, + COMMAND_DEVELOPER_DIRECTOR_GET_LOADED_TECTONICPLATES_COUNT, + COMMAND_DEVELOPER_DIRECTOR_SEND_TEST_EXCEPTION_SENTRY, + COMMAND_DEVELOPER_DIRECTOR_HASH_GENERATED_BLOCK_OUTPUT_FIXED_AREA_DETERMINISM_IDENTITY_TESTING, + COMMAND_DEVELOPER_PARAM_WORLD_HASH, + COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER, + COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X, + COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_Z, + COMMAND_DEVELOPER_DIRECTOR_UPDATE_PACK_WORLD_UNSAFE, + COMMAND_DEVELOPER_PARAM_WORLD_UPDATE, + COMMAND_DEVELOPER_PARAM_PACK_INSTALL_INTO_WORLD, + COMMAND_DEVELOPER_PARAM_MAKE_SURE_MAKE_BACKUP_READ_WARNINGS_FIRST, + COMMAND_DEVELOPER_DIRECTOR_TEST, + COMMAND_DEVELOPER_PARAM_DUMP_WHOLE_TECTONIC_PLATE_INSTEAD_SINGLE_SECTION, + COMMAND_DEVELOPER_PARAM_DUMP_FILE_ID_UNDER_PLUGINS_IRIS_DUMP_PV_ID, + COMMAND_DEVELOPER_DIRECTOR_TEST_2, + COMMAND_DEVELOPER_PARAM_PACK_BENCH, + COMMAND_DEVELOPER_PARAM_RADIUS_REGIONS, + COMMAND_DEVELOPER_PARAM_OPEN_GUI_WHILE_BENCHMARKING, + COMMAND_DEVELOPER_DIRECTOR_UPGRADE_ANOTHER_MINECRAFT_VERSION, + COMMAND_DEVELOPER_PARAM_VERSION_UPGRADE, + COMMAND_DEVELOPER_DIRECTOR_TEST_3, + COMMAND_DEVELOPER_PARAM_WORLD_FOLDER_SCAN_MCA_REGION_FILES, + COMMAND_DEVELOPER_DIRECTOR_DELETE_NEARBY_CHUNK_BLOCKS_REGEN_TESTING, + COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_YOUR_CURRENT_CHUNK, + COMMAND_DEVELOPER_DIRECTOR_TEST_4, + COMMAND_DEVELOPER_DIRECTOR_DELETE_REGENERATE_NEARBY_CHUNKS_PLACE_USING_IRIS_GENERATION, + COMMAND_DEVELOPER_PARAM_RADIUS_NEARBY_CHUNKS, + COMMAND_DEVELOPER_DIRECTOR_GENERATE_CHUNKS_INTO_BUFFERS_NO_WORLD_WRITES_HASH_BLOCKS_BIOMES_CAPTURES_GOLDEN, + COMMAND_DEVELOPER_PARAM_WORLD_SCAN, + COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER_2, + COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X_2, + COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_Z_2, + COMMAND_DEVELOPER_PARAM_DELETE_MANTLE_DATA_SCAN_AREA_FIRST_FULL_REGENERATION_FROM_SCRATCH, + COMMAND_DEVELOPER_PARAM_CONCURRENT_CHUNK_GENERATIONS_1_STRICTLY_SERIAL_ORDER_DEPENDENCE_TESTING, + COMMAND_DEVELOPER_PARAM_ALSO_DUMP_FULL_PER_CHUNK_NON_AIR_BLOCKSTATES_OFFLINE_DIFFING, + COMMAND_EDIT_DIRECTOR_EDIT_SOMETHING, + COMMAND_EDIT_DIRECTOR_EDIT_BIOME_YOU_SPECIFIED, + COMMAND_EDIT_PARAM_BIOME_EDIT, + COMMAND_EDIT_DIRECTOR_EDIT_REGION_YOU_SPECIFIED, + COMMAND_EDIT_PARAM_REGION_EDIT, + COMMAND_EDIT_DIRECTOR_EDIT_DIMENSION_YOU_SPECIFIED, + COMMAND_EDIT_PARAM_DIMENSION_EDIT, + COMMAND_FIND_DIRECTOR_IRIS_FIND_COMMANDS, + COMMAND_FIND_DIRECTOR_FIND_BIOME, + COMMAND_FIND_PARAM_BIOME_LOOK, + COMMAND_FIND_PARAM_SHOULD_YOU_BE_TELEPORTED, + COMMAND_FIND_DIRECTOR_FIND_REGION, + COMMAND_FIND_PARAM_REGION_LOOK, + COMMAND_FIND_PARAM_SHOULD_YOU_BE_TELEPORTED_2, + COMMAND_FIND_DIRECTOR_FIND_POINT_INTEREST, + COMMAND_FIND_PARAM_TYPE_POI_LOOK, + COMMAND_FIND_PARAM_SHOULD_YOU_BE_TELEPORTED_3, + COMMAND_FIND_DIRECTOR_FIND_STRUCTURE_VANILLA_KEY_LIKE_MINECRAFT_VILLAGE_PLAINS_MINECRAFT_STRONGHOLD_IMPORTED_IRIS, + COMMAND_FIND_PARAM_STRUCTURE_LOOK_E_G_MINECRAFT_VILLAGE_PLAINS_MINECRAFT_STRONGHOLD_MINECRAFT_ANCIENT_CITY, + COMMAND_FIND_DIRECTOR_FIND_OBJECT, + COMMAND_FIND_PARAM_OBJECT_LOOK, + COMMAND_FIND_PARAM_SHOULD_YOU_BE_TELEPORTED_4, + COMMAND_IRIS_DIRECTOR_BASIC_COMMAND, + COMMAND_IRIS_DIRECTOR_CREATE_NEW_WORLD, + COMMAND_IRIS_PARAM_NAME_WORLD_CREATE, + COMMAND_IRIS_PARAM_DIMENSION_PACK_CREATE_WORLD_WITH, + COMMAND_IRIS_PARAM_SEED_GENERATE_WORLD_WITH, + COMMAND_IRIS_PARAM_REPLACE_EXACT_EXISTING_WORLD_SLOT_NEXT_RESTART, + COMMAND_IRIS_DIRECTOR_TELEPORT_ANOTHER_WORLD, + COMMAND_IRIS_PARAM_WORLD_TELEPORT, + COMMAND_IRIS_PARAM_PLAYER_TELEPORT, + COMMAND_IRIS_DIRECTOR_PRINT_VERSION_INFORMATION, + COMMAND_IRIS_DIRECTOR_BENCHMARK_PACK, + COMMAND_IRIS_PARAM_DIMENSION_BENCHMARK, + COMMAND_IRIS_DIRECTOR_PRINT_WORLD_HEIGHT_INFORMATION, + COMMAND_IRIS_DIRECTOR_CHECK_ACCESS_ALL_WORLDS, + COMMAND_IRIS_DIRECTOR_REMOVE_IRIS_WORLD, + COMMAND_IRIS_PARAM_WORLD_REMOVE, + COMMAND_IRIS_PARAM_WHETHER_ALSO_REMOVE_FOLDER_IF_SET_FALSE_JUST_DOES_NOT_LOAD_WORLD, + COMMAND_IRIS_DIRECTOR_TOGGLE_DEBUG, + COMMAND_IRIS_DIRECTOR_DOWNLOAD_PROJECT, + COMMAND_IRIS_PARAM_PACK_DOWNLOAD, + COMMAND_IRIS_DIRECTOR_GET_METRICS_YOUR_WORLD, + COMMAND_IRIS_DIRECTOR_RELOAD_CONFIGURATION_FILE_THIS_IS_ALSO_DONE_AUTOMATICALLY, + COMMAND_IRIS_DIRECTOR_UNLOAD_IRIS_WORLD, + COMMAND_IRIS_PARAM_WORLD_UNLOAD, + COMMAND_IRIS_DIRECTOR_LOAD_IRIS_WORLD, + COMMAND_IRIS_PARAM_NAME_WORLD_LOAD, + COMMAND_IRIS_DIRECTOR_EVACUATE_IRIS_WORLD, + COMMAND_IRIS_PARAM_EVACUATE_WORLD, + COMMAND_OBJECT_DIRECTOR_IRIS_OBJECT_MANIPULATION, + COMMAND_OBJECT_DIRECTOR_OPEN_OBJECT_STUDIO_WORLD_GRID_EVERY_OBJECT_DIMENSION_OPTIONAL_DEFAULTS_ALL_PACKS, + COMMAND_OBJECT_PARAM_OPTIONAL_DIMENSION_WHOSE_OBJECT_PACK_LAY_OUT_OMIT_AGGREGATE_OBJECTS_FROM_EVERY, + COMMAND_OBJECT_PARAM_SEED_GENERATE_STUDIO_WITH, + COMMAND_OBJECT_DIRECTOR_CHECK_COMPOSITION_OBJECT, + COMMAND_OBJECT_PARAM_OBJECT_ANALYZE, + COMMAND_OBJECT_DIRECTOR_SHRINK_OBJECT_ITS_MINIMUM_SIZE, + COMMAND_OBJECT_PARAM_OBJECT_SHRINK, + COMMAND_OBJECT_DIRECTOR_GROW_ORGANIC_BRANCHES_THROUGH_CANOPY_SO_EVERY_LEAF_SURVIVES_VANILLA_DECAY, + COMMAND_OBJECT_PARAM_OBJECT_KEY_PREFIX_TREES_FILESYSTEM_PATH, + COMMAND_OBJECT_PARAM_DRYRUN_TRUE_ANALYZES_ONLY_WRITES_NOTHING, + COMMAND_OBJECT_PARAM_REACH_N_MAX_BRANCH_LENGTH_BLOCKS_FROM_EXISTING_WOOD_FARTHER_LEAF_CLUSTERS, + COMMAND_OBJECT_DIRECTOR_CONVERT_SCHEM_FILES_CONVERT_FOLDER_IOB_FILES, + COMMAND_OBJECT_DIRECTOR_GET_POWDER_THAT_REVEALS_OBJECTS, + COMMAND_OBJECT_DIRECTOR_CONTRACT_SELECTION_BASED_ON_YOUR_LOOKING_DIRECTION, + COMMAND_OBJECT_PARAM_AMOUNT_INSET_BY, + COMMAND_OBJECT_DIRECTOR_SET_POINT_1_LOOK, + COMMAND_OBJECT_PARAM_WHETHER_USE_YOUR_CURRENT_POSITION_WHERE_YOU_LOOK, + COMMAND_OBJECT_DIRECTOR_SET_POINT_2_LOOK, + COMMAND_OBJECT_PARAM_WHETHER_USE_YOUR_CURRENT_POSITION_WHERE_YOU_LOOK_2, + COMMAND_OBJECT_DIRECTOR_PASTE_OBJECT, + COMMAND_OBJECT_PARAM_OBJECT_PASTE, + COMMAND_OBJECT_PARAM_WHETHER_NOT_EDIT_OBJECT_NEED_HOLD_WAND, + COMMAND_OBJECT_PARAM_AMOUNT_DEGREES_ROTATE_BY, + COMMAND_OBJECT_PARAM_FACTOR_BY_WHICH_SCALE_OBJECT_PLACEMENT, + COMMAND_OBJECT_PARAM_SCALE_INTERPOLATOR_USE, + COMMAND_OBJECT_DIRECTOR_SAVE_OBJECT, + COMMAND_OBJECT_PARAM_DIMENSION_STORE_OBJECT, + COMMAND_OBJECT_PARAM_FILE_STORE_IT_CAN_USE_SUBFOLDERS, + COMMAND_OBJECT_PARAM_OVERWRITE_EXISTING_OBJECT_FILES, + COMMAND_OBJECT_PARAM_USE_LEGACY_TILESTATE_SERIALIZATION_IF_POSSIBLE, + COMMAND_OBJECT_DIRECTOR_SHIFT_SELECTION_YOUR_LOOKING_DIRECTION, + COMMAND_OBJECT_PARAM_AMOUNT_SHIFT_BY, + COMMAND_OBJECT_DIRECTOR_UNDO_NUMBER_PASTES, + COMMAND_OBJECT_PARAM_AMOUNT_PASTES_UNDO, + COMMAND_OBJECT_DIRECTOR_GETS_OBJECT_WAND_GRABS_CURRENT_WORLDEDIT_SELECTION, + COMMAND_OBJECT_DIRECTOR_GET_OBJECT_WAND, + COMMAND_OBJECT_DIRECTOR_AUTOSELECT_UP_DOWN_OUT, + COMMAND_OBJECT_DIRECTOR_AUTOSELECT_UP_OUT, + COMMAND_PACK_DIRECTOR_PACK_VALIDATION_MAINTENANCE, + COMMAND_PACK_DIRECTOR_VALIDATE_PACK_ALL_PACKS_RE_PUBLISH_RESULTS, + COMMAND_PACK_PARAM_PACK_FOLDER_NAME_VALIDATE_LEAVE_EMPTY_ALL, + COMMAND_PACK_DIRECTOR_PREVIEW_APPLY_UNUSED_RESOURCE_CLEANUP, + COMMAND_PACK_PARAM_PACK_FOLDER_NAME_CLEAN, + COMMAND_PACK_PARAM_PREVIEW_APPLY, + COMMAND_PACK_DIRECTOR_PREVIEW_APPLY_RESTORATION_LATEST_QUARANTINE, + COMMAND_PACK_PARAM_PACK_FOLDER_NAME_RESTORE, + COMMAND_PACK_PARAM_PREVIEW_APPLY_2, + COMMAND_PACK_DIRECTOR_SHOW_CACHED_VALIDATION_STATUS_PACK, + COMMAND_PACK_PARAM_PACK_FOLDER_NAME, + COMMAND_PREGEN_DIRECTOR_PREGENERATE_YOUR_IRIS_WORLDS, + COMMAND_PREGEN_DIRECTOR_PREGENERATE_WORLD, + COMMAND_PREGEN_PARAM_RADIUS_PREGEN_BLOCKS, + COMMAND_PREGEN_PARAM_WORLD_PREGEN, + COMMAND_PREGEN_PARAM_CENTER_LOCATION_PREGEN_USE_ME_YOUR_CURRENT_LOCATION, + COMMAND_PREGEN_PARAM_OPEN_IRIS_PREGEN_GUI, + COMMAND_PREGEN_PARAM_GENERATE_ONLY_ONE_CHUNK_AT_TIME, + COMMAND_PREGEN_DIRECTOR_STOP_ACTIVE_PREGENERATION_TASK, + COMMAND_PREGEN_DIRECTOR_PAUSE_CONTINUE_ACTIVE_PREGENERATION_TASK, + COMMAND_PREGEN_DIRECTOR_SHOW_ACTIVE_PREGENERATION_STATUS, + COMMAND_STRUCTURE_DIRECTOR_IRIS_STRUCTURE_TOOLS_INDEX_IMPORT_INFO, + COMMAND_STRUCTURE_DIRECTOR_REGENERATE_STRUCTURE_INDEX_JSON_LISTING_ALL_VANILLA_DATAPACK_IRIS_STRUCTURES, + COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_INDEX, + COMMAND_STRUCTURE_DIRECTOR_IMPORT_EVERY_STRUCTURE_VANILLA_INGESTED_DATAPACKS_INTO_THIS_PACK_AS_EDITABLE_IRIS, + COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_IMPORT_INTO, + COMMAND_STRUCTURE_DIRECTOR_CAPTURE_CODE_GENERATED_STRUCTURES_THAT_HAVE_NO_NBT_TEMPLATE_SWAMP_HUTS_IGLOOS, + COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_CAPTURE_INTO, + COMMAND_STRUCTURE_DIRECTOR_VERIFY_NATIVE_STRUCTURE_ELIGIBILITY_LOCATE_IRIS_PLACED_STRUCTURES_WITHOUT_RUNNING_BLOCKING_NATIVE, + COMMAND_STRUCTURE_PARAM_DIMENSION_VERIFY, + COMMAND_STRUCTURE_PARAM_SEARCH_RADIUS_CHUNKS_AROUND_ORIGIN_LARGER_IS_MUCH_SLOWER, + COMMAND_STRUCTURE_DIRECTOR_RESOLVE_IRIS_STRUCTURE_S_JIGSAW_GRAPH_REPORT_PIECE_COUNT_BOUNDS, + COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_HOLDS_STRUCTURE, + COMMAND_STRUCTURE_PARAM_IRIS_STRUCTURE_KEY_INSPECT, + COMMAND_STRUCTURE_DIRECTOR_ASSEMBLE_PLACE_IRIS_STRUCTURE_AT_YOUR_LOCATION_STUDIO_TESTING, + COMMAND_STRUCTURE_PARAM_DIMENSION_WHOSE_PACK_HOLDS_STRUCTURE_2, + COMMAND_STRUCTURE_PARAM_IRIS_STRUCTURE_KEY_PLACE, + COMMAND_STUDIO_DIRECTOR_STUDIO_COMMANDS, + COMMAND_STUDIO_DIRECTOR_OPEN_NEW_STUDIO_WORLD, + COMMAND_STUDIO_PARAM_DIMENSION_PACK_OPEN_STUDIO, + COMMAND_STUDIO_PARAM_SEED_GENERATE_STUDIO_WITH, + COMMAND_STUDIO_DIRECTOR_IMPORT_VANILLA_TREES_MUSHROOMS_OBJECTS_STRUCTURES_JIGSAW_FROM_SERVER_INTO_PACK_S, + COMMAND_STUDIO_PARAM_DIMENSION_PACK_IMPORT_VANILLA_CONTENT_INTO, + COMMAND_STUDIO_PARAM_HOW_MANY_VARIANTS_CAPTURE_PER_TREE_OBJECT_FEATURE, + COMMAND_STUDIO_PARAM_ALSO_IMPORT_VANILLA_DATAPACK_STRUCTURES_JIGSAW_INTO_PACK, + COMMAND_STUDIO_DIRECTOR_OPEN_VSCODE_DIMENSION, + COMMAND_STUDIO_PARAM_DIMENSION_OPEN_VSCODE, + COMMAND_STUDIO_DIRECTOR_CLOSE_OPEN_STUDIO_PROJECT, + COMMAND_STUDIO_DIRECTOR_TOGGLE_YOUR_STUDIO_DEBUG_SCOREBOARD, + COMMAND_STUDIO_DIRECTOR_CREATE_NEW_STUDIO_PROJECT, + COMMAND_STUDIO_PARAM_NAME_THIS_NEW_IRIS_PROJECT, + COMMAND_STUDIO_PARAM_COPY_CONTENTS_EXISTING_PROJECT_YOUR_PACKS_FOLDER_USE_IT_AS_TEMPLATE_THIS, + COMMAND_STUDIO_DIRECTOR_GET_VERSION_PACK, + COMMAND_STUDIO_PARAM_DIMENSION_GET_VERSION, + COMMAND_STUDIO_DIRECTOR_OPEN_NOISE_EXPLORER_EXTERNAL_GUI, + COMMAND_STUDIO_PARAM_OPTIONAL_PACK_GENERATOR_PREVIEW, + COMMAND_STUDIO_PARAM_SEED_PREVIEW_GENERATOR_WITH, + COMMAND_STUDIO_DIRECTOR_SHOW_LOOT_IF_CHEST_WERE_RIGHT_HERE, + COMMAND_STUDIO_PARAM_FAST_INSERTION_ITEMS_VIRTUAL_INVENTORY_MAY_CAUSE_PERFORMANCE_DROP, + COMMAND_STUDIO_PARAM_WHETHER_NOT_APPEND_INVENTORY_CURRENTLY_OPEN_IF_FALSE_CLEARS_OPENED_INVENTORY, + COMMAND_STUDIO_DIRECTOR_CALCULATE_CHANCE_EACH_REGION_GENERATE, + COMMAND_STUDIO_PARAM_RADIUS_CHUNKS, + COMMAND_STUDIO_DIRECTOR_RENDER_WORLD_MAP_EXTERNAL_GUI, + COMMAND_STUDIO_PARAM_WORLD_OPEN_GENERATOR, + COMMAND_STUDIO_DIRECTOR_PACKAGE_DIMENSION_INTO_COMPRESSED_FORMAT, + COMMAND_STUDIO_PARAM_DIMENSION_PACK_COMPRESS, + COMMAND_STUDIO_PARAM_WHETHER_NOT_OBFUSCATE_PACK, + COMMAND_STUDIO_PARAM_WHETHER_NOT_MINIFY_PACK, + COMMAND_STUDIO_DIRECTOR_PROFILES_PERFORMANCE_DIMENSION, + COMMAND_STUDIO_PARAM_DIMENSION_PROFILE, + COMMAND_STUDIO_DIRECTOR_SPAWN_IRIS_ENTITY, + COMMAND_STUDIO_PARAM_ENTITY_SPAWN, + COMMAND_STUDIO_PARAM_LOCATION_SPAWN_ENTITY_AT, + COMMAND_STUDIO_DIRECTOR_TELEPORT_ACTIVE_STUDIO_WORLD, + COMMAND_STUDIO_DIRECTOR_UPDATE_YOUR_DIMENSION_PROJECTS_VSCODE_WORKSPACE, + COMMAND_STUDIO_PARAM_DIMENSION_UPDATE_WORKSPACE, + COMMAND_STUDIO_DIRECTOR_CAPTURE_IGENDATA_CHUNK_REPORT_NEARBY_CHUNKS, + COMMAND_WHAT_DIRECTOR_IRIS_WHAT, + COMMAND_WHAT_DIRECTOR_WHAT_IS_MY_HAND, + COMMAND_WHAT_DIRECTOR_WHAT_BIOME_AM_I, + COMMAND_WHAT_DIRECTOR_WHAT_REGION_AM_I, + COMMAND_WHAT_DIRECTOR_WHAT_BLOCK_AM_I_LOOKING_AT, + COMMAND_WHAT_DIRECTOR_SHOW_MARKERS_CHUNK, + COMMAND_WHAT_PARAM_MARKER_NAME_SUCH_AS_CAVE_FLOOR_CAVE_CEILING + ); + + private DirectorCommandMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/IrisLanguage.java b/core/src/main/java/art/arcane/iris/core/localization/IrisLanguage.java new file mode 100644 index 000000000..a06c0ed81 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/IrisLanguage.java @@ -0,0 +1,645 @@ +package art.arcane.iris.core.localization; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.director.DirectorTextResolver; +import art.arcane.volmlib.util.localization.LinesKey; +import art.arcane.volmlib.util.localization.LocaleOverlay; +import art.arcane.volmlib.util.localization.LocalizationCandidate; +import art.arcane.volmlib.util.localization.LocalizationIssue; +import art.arcane.volmlib.util.localization.LocalizationManager; +import art.arcane.volmlib.util.localization.LocalizationReloadResult; +import art.arcane.volmlib.util.localization.LocalizationSnapshot; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.localization.MessageArgumentKind; +import art.arcane.volmlib.util.localization.MessageArgs; +import art.arcane.volmlib.util.localization.MessageCatalog; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.PluralKey; +import art.arcane.volmlib.util.localization.PluralSelector; +import art.arcane.volmlib.util.localization.ResolvedLines; +import art.arcane.volmlib.util.localization.ResolvedText; +import art.arcane.volmlib.util.localization.TextKey; +import art.arcane.volmlib.util.localization.VolmitLocales; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; + +public final class IrisLanguage { + private static final long MAX_LOCALE_BYTES = 2L * 1024L * 1024L; + private static final long HOTLOAD_CONTENT_STABILITY_NANOS = TimeUnit.MILLISECONDS.toNanos(250L); + private static final long HOTLOAD_DELETION_GRACE_NANOS = TimeUnit.SECONDS.toNanos(3L); + private static final long HOTLOAD_COOLDOWN_NANOS = TimeUnit.SECONDS.toNanos(3L); + private static final int MAX_REPORTED_ISSUES = 12; + private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+"); + private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]"); + private static final MessageCatalog CATALOG = IrisMessages.catalog(); + private static final LocalizationManager MANAGER = new LocalizationManager( + LocalizationCandidate.english(CATALOG, PluralSelector.oneOther()) + ); + /** + * Memoized argument-free {@link #plain(MessageKey)} results. HUD and overlay code calls it several times + * per frame for fixed labels; resolve plus placeholder render plus colour clean is not free at 60fps. + * Keyed by message id and pinned to the snapshot it was resolved against, so a locale reload publishes a + * new snapshot and the whole memo is discarded. Bounded by the catalog size. + */ + private static final AtomicReference PLAIN_MEMO = new AtomicReference<>(null); + private static final LocaleHotloadGate HOTLOAD_GATE = new LocaleHotloadGate( + new LocaleHotloadGate.Timing( + HOTLOAD_CONTENT_STABILITY_NANOS, + HOTLOAD_DELETION_GRACE_NANOS, + HOTLOAD_COOLDOWN_NANOS + ) + ); + + private static volatile File dataFolder; + private static volatile String lastCaptureFailureKey; + private static volatile String lastInvalidHotloadLocale; + private static volatile String activeLocale = CATALOG.englishLocale(); + + private IrisLanguage() { + } + + public static boolean initialize() { + if (!IrisPlatforms.isBound()) { + return false; + } + return reload(IrisPlatforms.get().dataFolder(), configuredLocale()); + } + + public static synchronized boolean reload() { + File root = dataFolder; + if (root == null && IrisPlatforms.isBound()) { + root = IrisPlatforms.get().dataFolder(); + } + if (root == null) { + return false; + } + return reload(root, configuredLocale()); + } + + public static synchronized boolean reload(IrisSettings candidate) { + IrisSettings resolvedCandidate = Objects.requireNonNull(candidate, "Candidate settings cannot be null"); + File root = dataFolder; + if (root == null && IrisPlatforms.isBound()) { + root = IrisPlatforms.get().dataFolder(); + } + if (root == null) { + return false; + } + IrisSettings.IrisSettingsGeneral general = resolvedCandidate.getGeneral(); + String locale = general == null ? CATALOG.englishLocale() : general.getLanguage(); + return reload(root, locale); + } + + public static synchronized boolean reload(File root, String locale) { + File resolvedRoot = root == null ? null : root.getAbsoluteFile(); + if (resolvedRoot == null) { + throw new IllegalArgumentException("Iris locale data folder cannot be null"); + } + String requestedLocale; + try { + requestedLocale = normalizeLocale(locale); + } catch (RuntimeException exception) { + dataFolder = resolvedRoot; + HOTLOAD_GATE.reset(null); + IrisLogging.error("Rejected locale setting '" + locale + "'; continuing with " + activeLocale + "."); + IrisLogging.reportError(exception); + exception.printStackTrace(); + return false; + } + + File override = overrideFile(resolvedRoot, requestedLocale); + SnapshotCapture capture = captureForReload(override, requestedLocale); + LocalizationReloadResult result; + if (capture.failure() == null) { + result = MANAGER.reload(() -> loadCandidate(resolvedRoot, requestedLocale, capture.snapshot())); + } else { + result = MANAGER.reload(() -> { + throw capture.failure(); + }); + } + dataFolder = resolvedRoot; + HOTLOAD_GATE.reset(capture.snapshot()); + lastCaptureFailureKey = null; + if (!result.applied()) { + reportRejectedReload(requestedLocale, result); + return false; + } + + activeLocale = requestedLocale; + int warnings = result.validation().warnings().size(); + IrisLogging.info("Loaded locale " + requestedLocale + " with " + warnings + " fallback " + + (warnings == 1 ? "entry" : "entries") + "."); + return true; + } + + public static synchronized boolean update() { + File root = dataFolder; + if (root == null) { + return initialize(); + } + String locale; + try { + locale = normalizeLocale(configuredLocale()); + } catch (RuntimeException exception) { + String invalidLocale = configuredLocale(); + if (Objects.equals(lastInvalidHotloadLocale, invalidLocale)) { + return false; + } + lastInvalidHotloadLocale = invalidLocale; + return reload(root, invalidLocale); + } + lastInvalidHotloadLocale = null; + File expected = overrideFile(root, locale); + LocaleHotloadSnapshot snapshot; + try { + snapshot = captureHotloadSnapshot(expected, locale); + } catch (IOException | RuntimeException failure) { + HOTLOAD_GATE.unavailable(); + reportCaptureFailure(expected, failure); + return false; + } + if (snapshot == null) { + HOTLOAD_GATE.unavailable(); + return true; + } + lastCaptureFailureKey = null; + + LocaleHotloadGate.Attempt attempt = HOTLOAD_GATE.observe(snapshot, System.nanoTime()); + if (attempt == null) { + return true; + } + + LocalizationReloadResult result = MANAGER.reload(() -> loadCandidate(root, locale, attempt.snapshot())); + boolean applied = result.applied(); + HOTLOAD_GATE.complete(attempt, System.nanoTime(), applied); + if (!applied) { + reportRejectedReload(locale, result); + return false; + } + + activeLocale = locale; + int warnings = result.validation().warnings().size(); + IrisLogging.info("Loaded locale " + locale + " with " + warnings + " fallback " + + (warnings == 1 ? "entry" : "entries") + "."); + return true; + } + + public static String activeLocale() { + return activeLocale; + } + + public static File overrideFolder() { + File root = dataFolder; + if (root == null && IrisPlatforms.isBound()) { + root = IrisPlatforms.get().dataFolder(); + } + if (root == null) { + return null; + } + return new File(root, "languages/overrides"); + } + + public static String text(MessageKey key, MessageArgument... arguments) { + MessageArgs.Builder builder = MessageArgs.builder(); + for (MessageArgument argument : arguments) { + builder.add(argument); + } + return text(key, builder.build()); + } + + public static String text(MessageKey key) { + return text(key, MessageArgs.empty()); + } + + public static String text(MessageKey key, MessageArgs arguments) { + return render(resolve(key, arguments)); + } + + public static String plain(MessageKey key, MessageArgument... arguments) { + MessageArgs.Builder builder = MessageArgs.builder(); + for (MessageArgument argument : arguments) { + builder.add(argument); + } + return plain(key, builder.build()); + } + + public static String plain(MessageKey key) { + LocalizationSnapshot snapshot = MANAGER.snapshot(); + PlainMemo memo = PLAIN_MEMO.get(); + if (memo == null || memo.snapshot() != snapshot) { + memo = new PlainMemo(snapshot, new ConcurrentHashMap<>()); + PLAIN_MEMO.set(memo); + } + String cached = memo.values().get(key.id()); + if (cached != null) { + return cached; + } + String resolved = plain(key, MessageArgs.empty()); + memo.values().put(key.id(), resolved); + return resolved; + } + + public static String plain(MessageKey key, MessageArgs arguments) { + String rendered = render(resolve(key, arguments)); + return IrisLogging.clean(rendered); + } + + public static String errorDetail(Throwable throwable) { + String message = throwable == null ? null : throwable.getMessage(); + if (message == null || message.isBlank()) { + return ""; + } + return plain(RuntimeUiMessages.ERROR_DETAIL_SUFFIX, MessageArgument.untrusted("error", message)); + } + + public static DirectorTextResolver directorResolver() { + return (key, arguments) -> { + MessageKey definition = CATALOG.key(key.id()); + if (!(definition instanceof TextKey textKey)) { + return DirectorTextResolver.ENGLISH.resolve(key, arguments); + } + return plain(textKey, arguments); + }; + } + + public static MessageCatalog catalog() { + return CATALOG; + } + + private static ResolvedText resolve(MessageKey key, MessageArgs arguments) { + if (key instanceof TextKey textKey) { + return MANAGER.snapshot().resolve(textKey, arguments); + } + if (key instanceof PluralKey pluralKey) { + return MANAGER.snapshot().resolve(pluralKey, arguments); + } + if (key instanceof LinesKey linesKey) { + ResolvedLines lines = MANAGER.snapshot().resolve(linesKey, arguments); + return new ResolvedText(lines.key(), lines.locale(), String.join("\n", lines.lines()), lines.arguments()); + } + throw new IllegalArgumentException("Unsupported Iris message key: " + key.id()); + } + + private static LocalizationCandidate loadCandidate( + File root, + String locale, + LocaleHotloadSnapshot snapshot + ) throws Exception { + File folder = new File(root, "languages/overrides"); + Files.createDirectories(folder.toPath()); + List overlays = new ArrayList<>(2); + if (!snapshot.missing()) { + overlays.add(parseOverlay(snapshot.file().getPath(), locale, snapshot.content())); + } + + if (!CATALOG.englishLocale().equals(locale)) { + LocaleOverlay bundled = loadBundledOverlay(locale); + if (bundled != null) { + overlays.add(bundled); + } + } + return new LocalizationCandidate(CATALOG, overlays, PluralSelector.oneOther()); + } + + static LocaleOverlay loadBundledOverlay(String locale) throws Exception { + String normalizedLocale = normalizeLocale(locale); + if (CATALOG.englishLocale().equals(normalizedLocale)) { + return null; + } + String resourceName = "/languages/" + normalizedLocale + ".json"; + InputStream input = IrisLanguage.class.getResourceAsStream(resourceName); + if (input == null) { + if (VolmitLocales.isBundled(normalizedLocale)) { + throw new IllegalStateException("Missing bundled Iris locale resource: " + resourceName); + } + return null; + } + try (InputStream stream = input) { + byte[] bytes = stream.readNBytes((int) MAX_LOCALE_BYTES + 1); + if (bytes.length > MAX_LOCALE_BYTES) { + throw new IllegalArgumentException("Bundled locale is too large: " + resourceName); + } + return parseOverlay("bundled:" + resourceName, normalizedLocale, new String(bytes, StandardCharsets.UTF_8)); + } + } + + private static LocaleOverlay parseOverlay(String source, String locale, String raw) { + JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw); + if (!parsed.isJsonObject()) { + throw new IllegalArgumentException("Locale source is not a JSON object: " + source); + } + JsonObject root = parsed.getAsJsonObject(); + for (String key : root.keySet()) { + if (!key.equals("locale") && !key.equals("messages")) { + throw new IllegalArgumentException("Unknown locale root key: " + key); + } + } + if (root.has("locale")) { + JsonElement declaredLocale = root.get("locale"); + if (!declaredLocale.isJsonPrimitive() || !locale.equals(normalizeLocale(declaredLocale.getAsString()))) { + throw new IllegalArgumentException("Locale source declares a different locale than its file: " + source); + } + } + LocaleOverlay.Builder builder = LocaleOverlay.builder(source, locale); + if (!root.has("messages")) { + return builder.build(); + } + JsonElement messages = root.get("messages"); + if (!messages.isJsonObject()) { + throw new IllegalArgumentException("Locale messages must be a JSON object: " + source); + } + appendMessages(builder, messages.getAsJsonObject(), ""); + return builder.build(); + } + + private static void appendMessages(LocaleOverlay.Builder builder, JsonObject object, String prefix) { + for (Map.Entry entry : object.entrySet()) { + String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey(); + JsonElement value = entry.getValue(); + if (value == null || value.isJsonNull()) { + throw new IllegalArgumentException("Locale value cannot be null: " + key); + } + MessageKey definition = CATALOG.key(key); + if (value.isJsonObject() && definition instanceof PluralKey) { + builder.plural(key, readPlural(key, value.getAsJsonObject())); + } else if (value.isJsonObject()) { + appendMessages(builder, value.getAsJsonObject(), key); + } else if (value.isJsonArray()) { + builder.lines(key, readLines(key, value.getAsJsonArray())); + } else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) { + builder.text(key, value.getAsString()); + } else { + throw new IllegalArgumentException("Locale value must be text, lines, or plural forms: " + key); + } + } + } + + private static List readLines(String key, JsonArray array) { + List lines = new ArrayList<>(array.size()); + for (JsonElement value : array) { + if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { + throw new IllegalArgumentException("Locale line must be text: " + key); + } + lines.add(value.getAsString()); + } + return lines; + } + + private static Map readPlural(String key, JsonObject object) { + Map forms = new LinkedHashMap<>(); + for (Map.Entry entry : object.entrySet()) { + JsonElement value = entry.getValue(); + if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { + throw new IllegalArgumentException("Locale plural form must be text: " + key + "." + entry.getKey()); + } + forms.put(entry.getKey(), value.getAsString()); + } + return forms; + } + + private static String render(ResolvedText resolved) { + String prepared = resolved.template(); + List replacements = new ArrayList<>(resolved.arguments().size()); + int index = 0; + for (MessageArgument argument : resolved.arguments().arguments().values()) { + String token = "\uE000" + index + "\uE001"; + prepared = prepared.replace("{" + argument.name() + "}", token); + replacements.add(new RenderedArgument(token, argument)); + index++; + } + + String rendered = translateColors(prepared); + StringBuilder output = new StringBuilder(rendered.length()); + int cursor = 0; + while (cursor < rendered.length()) { + if (rendered.charAt(cursor) != '\uE000') { + output.append(rendered.charAt(cursor)); + cursor++; + continue; + } + int end = rendered.indexOf('\uE001', cursor + 1); + int replacementIndex = end < 0 ? -1 : parseReplacementIndex(rendered, cursor + 1, end); + if (replacementIndex < 0 || replacementIndex >= replacements.size()) { + output.append(rendered.charAt(cursor)); + cursor++; + continue; + } + RenderedArgument replacement = replacements.get(replacementIndex); + if (replacement.token().length() != end - cursor + 1 + || !rendered.regionMatches(cursor, replacement.token(), 0, replacement.token().length())) { + output.append(rendered.charAt(cursor)); + cursor++; + continue; + } + MessageArgument argument = replacement.argument(); + String value = String.valueOf(argument.value()); + output.append(argument.kind() == MessageArgumentKind.TRUSTED + ? translateColors(value) + : escapeUntrusted(value)); + cursor = end + 1; + } + return output.toString(); + } + + private static int parseReplacementIndex(String value, int start, int end) { + if (start >= end) { + return -1; + } + int result = 0; + for (int index = start; index < end; index++) { + char character = value.charAt(index); + if (character < '0' || character > '9') { + return -1; + } + int digit = character - '0'; + if (result > (Integer.MAX_VALUE - digit) / 10) { + return -1; + } + result = result * 10 + digit; + } + return result; + } + + private static String translateColors(String value) { + if (value == null || value.isEmpty()) { + return ""; + } + char[] characters = value.toCharArray(); + for (int index = 0; index < characters.length - 1; index++) { + if (characters[index] == '&' && isColorCode(characters[index + 1])) { + characters[index] = '\u00a7'; + characters[index + 1] = Character.toLowerCase(characters[index + 1]); + } + } + return new String(characters); + } + + private static boolean isColorCode(char value) { + char lowered = Character.toLowerCase(value); + return lowered >= '0' && lowered <= '9' || lowered >= 'a' && lowered <= 'f' + || lowered >= 'k' && lowered <= 'o' || lowered == 'r' || lowered == 'x'; + } + + private static String escapeUntrusted(String value) { + return LEGACY_COLOR.matcher(value).replaceAll("") + .replace("&", "&") + .replace("<", "‹") + .replace(">", "›"); + } + + private static String configuredLocale() { + IrisSettings.IrisSettingsGeneral general = IrisSettings.get().getGeneral(); + return general == null ? CATALOG.englishLocale() : general.getLanguage(); + } + + private static String normalizeLocale(String locale) { + String value = locale == null || locale.isBlank() ? CATALOG.englishLocale() : locale.trim(); + if (!LOCALE_NAME.matcher(value).matches()) { + throw new IllegalArgumentException("Invalid locale name: " + value); + } + return value; + } + + private static File overrideFile(File root, String locale) { + return new File(new File(root, "languages/overrides"), normalizeLocale(locale) + ".json").getAbsoluteFile(); + } + + static LocaleHotloadSnapshot captureHotloadSnapshot(File file, String locale) throws IOException { + File resolvedFile = Objects.requireNonNull(file, "Locale override file cannot be null").getAbsoluteFile(); + String resolvedLocale = normalizeLocale(locale); + BasicFileAttributes before; + try { + before = Files.readAttributes(resolvedFile.toPath(), BasicFileAttributes.class); + } catch (NoSuchFileException failure) { + return LocaleHotloadSnapshot.missing(resolvedFile, resolvedLocale); + } + if (!before.isRegularFile()) { + throw new IllegalArgumentException("Locale override is not a regular file: " + resolvedFile.getPath()); + } + if (before.size() > MAX_LOCALE_BYTES) { + throw new IllegalArgumentException("Locale override is too large: " + resolvedFile.getPath()); + } + + byte[] bytes; + try (InputStream input = Files.newInputStream(resolvedFile.toPath())) { + bytes = input.readNBytes((int) MAX_LOCALE_BYTES + 1); + } catch (NoSuchFileException failure) { + return null; + } + if (bytes.length > MAX_LOCALE_BYTES) { + throw new IllegalArgumentException("Locale override is too large: " + resolvedFile.getPath()); + } + + BasicFileAttributes after; + try { + after = Files.readAttributes(resolvedFile.toPath(), BasicFileAttributes.class); + } catch (NoSuchFileException failure) { + return null; + } + if (!sameIdentity(before, after) || bytes.length != after.size()) { + return null; + } + + String content; + try { + content = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes)).toString(); + } catch (CharacterCodingException failure) { + throw new IOException("Locale override is not valid UTF-8: " + resolvedFile.getPath(), failure); + } + return LocaleHotloadSnapshot.present(resolvedFile, resolvedLocale, content, sha256(bytes)); + } + + private static SnapshotCapture captureForReload(File file, String locale) { + try { + LocaleHotloadSnapshot snapshot = captureHotloadSnapshot(file, locale); + if (snapshot == null) { + return new SnapshotCapture( + null, + new IOException("Locale override changed while being read: " + file.getPath()) + ); + } + return new SnapshotCapture(snapshot, null); + } catch (Exception failure) { + return new SnapshotCapture(null, failure); + } + } + + private static boolean sameIdentity(BasicFileAttributes before, BasicFileAttributes after) { + return before.isRegularFile() == after.isRegularFile() + && before.size() == after.size() + && before.lastModifiedTime().equals(after.lastModifiedTime()) + && Objects.equals(before.fileKey(), after.fileKey()); + } + + private static String sha256(byte[] content) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return HexFormat.of().formatHex(digest.digest(content)); + } catch (NoSuchAlgorithmException failure) { + throw new IllegalStateException("SHA-256 is unavailable", failure); + } + } + + private static void reportCaptureFailure(File file, Exception failure) { + String detail = failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage(); + String failureKey = file.getAbsolutePath() + ":" + failure.getClass().getName() + ":" + detail; + if (Objects.equals(failureKey, lastCaptureFailureKey)) { + return; + } + lastCaptureFailureKey = failureKey; + IrisLogging.error("Failed to capture stable locale override " + file.getPath() + ": " + detail); + IrisLogging.reportError(failure); + failure.printStackTrace(); + } + + private static void reportRejectedReload(String locale, LocalizationReloadResult result) { + IrisLogging.error("Rejected locale reload for " + locale + "; continuing with " + activeLocale + "."); + List issues = result.validation().errors(); + for (int index = 0; index < Math.min(issues.size(), MAX_REPORTED_ISSUES); index++) { + LocalizationIssue issue = issues.get(index); + IrisLogging.error(issue.source() + " [" + issue.key() + "]: " + issue.detail()); + } + if (issues.size() > MAX_REPORTED_ISSUES) { + IrisLogging.error((issues.size() - MAX_REPORTED_ISSUES) + " additional locale errors were omitted."); + } + if (result.failure() != null) { + IrisLogging.reportError(result.failure()); + result.failure().printStackTrace(); + } + } + + private record RenderedArgument(String token, MessageArgument argument) { + } + + private record PlainMemo(LocalizationSnapshot snapshot, Map values) { + } + + private record SnapshotCapture(LocaleHotloadSnapshot snapshot, Exception failure) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/IrisMessages.java b/core/src/main/java/art/arcane/iris/core/localization/IrisMessages.java new file mode 100644 index 000000000..71561b14b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/IrisMessages.java @@ -0,0 +1,116 @@ +package art.arcane.iris.core.localization; + +import art.arcane.volmlib.util.director.DirectorMessages; +import art.arcane.volmlib.util.localization.MessageCatalog; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; +import art.arcane.volmlib.util.localization.VolmitLocales; + +import java.util.List; + +public final class IrisMessages { + public static final TextKey COMMAND_PERMISSION_DENIED = TextKey.of( + "iris.command.permission_denied", + "You lack the permission '{permission}'" + ); + public static final TextKey COMMAND_UNKNOWN = TextKey.of( + "iris.command.unknown", + "Unknown Iris command" + ); + public static final TextKey COMMAND_PLAYER_ONLY = TextKey.of( + "iris.command.player_only", + "This command can only be used by players." + ); + public static final TextKey COMMAND_IRIS_WORLD_REQUIRED = TextKey.of( + "iris.command.iris_world_required", + "This dimension is not generated by Iris." + ); + public static final TextKey COMMAND_RELOAD_SUCCESS = TextKey.of( + "iris.command.reload.success", + "Hotloaded settings and locale {locale}." + ); + public static final TextKey COMMAND_RELOAD_FAILED = TextKey.of( + "iris.command.reload.failed", + "Settings were reloaded, but locale {locale} was rejected; continuing with {activeLocale}." + ); + public static final TextKey MODDED_HELP_UNKNOWN_SECTION = TextKey.of( + "iris.modded.help.unknown_section", + "Unknown Iris help section: {section}" + ); + public static final TextKey MODDED_HELP_BACK_HOVER = TextKey.of( + "iris.modded.help.back_hover", + "Return to {parent}." + ); + public static final TextKey MODDED_HELP_COMMAND_PARAMETER = TextKey.of( + "iris.modded.help.command_parameter", + "Command parameter" + ); + public static final TextKey MODDED_HELP_BRIGADIER_TEXT = TextKey.of( + "iris.modded.help.brigadier_text", + "This parameter is read as text by Brigadier." + ); + public static final TextKey MODDED_HELP_OPERATOR_NOTICE = TextKey.of( + "iris.modded.help.operator_notice", + "Iris commands need operator permission (level 2)." + ); + public static final TextKey MODDED_HELP_OPERATOR_INSTRUCTION = TextKey.of( + "iris.modded.help.operator_instruction", + "Run {command} from the console (or enable cheats in singleplayer); until then these commands will not run or tab-complete." + ); + public static final TextKey MODDED_PROPERTIES_NONE = TextKey.of( + "iris.modded.command.properties.none", + "Properties: (none)" + ); + public static final TextKey MODDED_PROPERTIES = TextKey.of( + "iris.modded.command.properties.list", + "Properties: {properties}" + ); + + private static final List RUNTIME_KEYS = List.of( + COMMAND_PERMISSION_DENIED, + COMMAND_UNKNOWN, + COMMAND_PLAYER_ONLY, + COMMAND_IRIS_WORLD_REQUIRED, + COMMAND_RELOAD_SUCCESS, + COMMAND_RELOAD_FAILED, + MODDED_HELP_UNKNOWN_SECTION, + MODDED_HELP_BACK_HOVER, + MODDED_HELP_COMMAND_PARAMETER, + MODDED_HELP_BRIGADIER_TEXT, + MODDED_HELP_OPERATOR_NOTICE, + MODDED_HELP_OPERATOR_INSTRUCTION, + MODDED_PROPERTIES_NONE, + MODDED_PROPERTIES + ); + private static final MessageCatalog CATALOG = createCatalog(); + + private IrisMessages() { + } + + public static MessageCatalog catalog() { + return CATALOG; + } + + public static MessageKey require(String id) { + return CATALOG.require(id); + } + + private static MessageCatalog createCatalog() { + MessageCatalog.Builder builder = MessageCatalog.builder(VolmitLocales.ENGLISH); + builder.addAll(DirectorMessages.keys()); + builder.addAll(RUNTIME_KEYS); + builder.addAll(BukkitCommandMessages.keys()); + builder.addAll(BukkitCommandMessagesExtended.keys()); + builder.addAll(ModdedCommandMessages.keys()); + builder.addAll(DirectorCommandMessages.keys()); + builder.addAll(ModdedHelpMessages.keys()); + builder.addAll(RuntimeUiMessages.keys()); + builder.addAll(BukkitRuntimeMessages.keys()); + builder.addAll(RuntimeProgressMessages.keys()); + builder.addAll(PackDownloadMessages.keys()); + builder.addAll(ClientUiMessages.keys()); + builder.addAll(BukkitUiMessages.keys()); + builder.addAll(DesktopUiMessages.keys()); + return builder.build(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/LocaleHotloadGate.java b/core/src/main/java/art/arcane/iris/core/localization/LocaleHotloadGate.java new file mode 100644 index 000000000..08d47d675 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/LocaleHotloadGate.java @@ -0,0 +1,100 @@ +package art.arcane.iris.core.localization; + +import java.util.Objects; + +final class LocaleHotloadGate { + private final Timing timing; + + private long generation; + private LocaleHotloadSnapshot baseline; + private LocaleHotloadSnapshot observed; + private long observedSinceNanos; + private LocaleHotloadSnapshot pending; + private Attempt inFlight; + private long lastCompletedAtNanos; + private boolean hasCompleted; + + LocaleHotloadGate(Timing timing) { + this.timing = Objects.requireNonNull(timing, "Locale hotload timing cannot be null"); + } + + synchronized Attempt observe(LocaleHotloadSnapshot snapshot, long nowNanos) { + LocaleHotloadSnapshot current = Objects.requireNonNull(snapshot, "Locale hotload snapshot cannot be null"); + if (!current.equals(observed)) { + observed = current; + observedSinceNanos = nowNanos; + return null; + } + + long stabilityNanos = current.missing() + ? timing.deletionGraceNanos() + : timing.contentStabilityNanos(); + if (!elapsed(nowNanos, observedSinceNanos, stabilityNanos)) { + return null; + } + + pending = current.equals(baseline) ? null : current; + if (pending == null || inFlight != null || !pending.equals(observed)) { + return null; + } + if (hasCompleted && !elapsed(nowNanos, lastCompletedAtNanos, timing.cooldownNanos())) { + return null; + } + + Attempt attempt = new Attempt(generation, pending); + inFlight = attempt; + return attempt; + } + + synchronized void unavailable() { + observed = null; + observedSinceNanos = 0L; + } + + synchronized void complete(Attempt attempt, long nowNanos, boolean applied) { + if (attempt == null || inFlight == null || !inFlight.equals(attempt) || attempt.generation() != generation) { + return; + } + + inFlight = null; + lastCompletedAtNanos = nowNanos; + hasCompleted = true; + if (!applied) { + return; + } + + baseline = attempt.snapshot(); + if (attempt.snapshot().equals(pending)) { + pending = null; + } + } + + synchronized void reset(LocaleHotloadSnapshot snapshot) { + generation++; + baseline = snapshot; + observed = snapshot; + observedSinceNanos = 0L; + pending = null; + inFlight = null; + lastCompletedAtNanos = 0L; + hasCompleted = false; + } + + private boolean elapsed(long nowNanos, long startNanos, long durationNanos) { + return nowNanos - startNanos >= durationNanos; + } + + record Timing(long contentStabilityNanos, long deletionGraceNanos, long cooldownNanos) { + Timing { + if (contentStabilityNanos < 0L || deletionGraceNanos < 0L || cooldownNanos < 0L) { + throw new IllegalArgumentException("Locale hotload timing cannot be negative"); + } + } + } + + record Attempt(long generation, LocaleHotloadSnapshot snapshot) { + Attempt { + snapshot = Objects.requireNonNull(snapshot, "Locale hotload attempt snapshot cannot be null"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/LocaleHotloadSnapshot.java b/core/src/main/java/art/arcane/iris/core/localization/LocaleHotloadSnapshot.java new file mode 100644 index 000000000..39b424b81 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/LocaleHotloadSnapshot.java @@ -0,0 +1,29 @@ +package art.arcane.iris.core.localization; + +import java.io.File; +import java.util.Objects; + +record LocaleHotloadSnapshot(File file, String locale, String content, String sha256) { + LocaleHotloadSnapshot { + file = Objects.requireNonNull(file, "Locale override file cannot be null").getAbsoluteFile(); + locale = Objects.requireNonNull(locale, "Locale cannot be null"); + sha256 = Objects.requireNonNull(sha256, "Locale content hash cannot be null"); + } + + static LocaleHotloadSnapshot missing(File file, String locale) { + return new LocaleHotloadSnapshot(file, locale, null, "missing"); + } + + static LocaleHotloadSnapshot present(File file, String locale, String content, String sha256) { + return new LocaleHotloadSnapshot( + file, + locale, + Objects.requireNonNull(content, "Locale content cannot be null"), + sha256 + ); + } + + boolean missing() { + return content == null; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java b/core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java new file mode 100644 index 000000000..5ad8c3a2e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java @@ -0,0 +1,1575 @@ +package art.arcane.iris.core.localization; + +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; + +public final class ModdedCommandMessages { + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_BIOME_IRIS_EDIT_BIOME_KEY = TextKey.of( + "iris.modded.irismoddedcommands.console_must_name_biome_iris_edit_biome_key", + "Console must name a biome: /iris edit biome " + ); + public static final TextKey IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED = TextKey.of( + "iris.modded.irismoddedcommands.biome_lookup_failed", + "Biome lookup failed: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_UNKNOWN_BIOME = TextKey.of( + "iris.modded.irismoddedcommands.unknown_biome", + "Unknown biome: " + "{key}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_2 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_2", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_REGION_IRIS_EDIT_REGION_KEY = TextKey.of( + "iris.modded.irismoddedcommands.console_must_name_region_iris_edit_region_key", + "Console must name a region: /iris edit region " + ); + public static final TextKey IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED = TextKey.of( + "iris.modded.irismoddedcommands.region_lookup_failed", + "Region lookup failed: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_UNKNOWN_REGION = TextKey.of( + "iris.modded.irismoddedcommands.unknown_region", + "Unknown region: " + "{key}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_3 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_3", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_CANNOT_OPEN_FILES_HERE = TextKey.of( + "iris.modded.irismoddedcommands.cannot_open_files_here", + "Cannot open files here: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM = TextKey.of( + "iris.modded.irismoddedcommands.cannot_find_file_perhaps_it_was_not_loaded_directly_from", + "Cannot find the file; perhaps it was not loaded directly from a file?" + ); + public static final TextKey IRIS_MODDED_COMMANDS_COULD_NOT_OPEN = TextKey.of( + "iris.modded.irismoddedcommands.could_not_open", + "Could not open " + "{value}" + ": " + "{value2}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_OPENING_YOUR_EDITOR = TextKey.of( + "iris.modded.irismoddedcommands.opening_your_editor", + "Opening " + "{value}" + " " + "{value2}" + " in your editor." + ); + public static final TextKey IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_PLAYER_IRIS_TP_DIMENSION_PLAYER = TextKey.of( + "iris.modded.irismoddedcommands.console_must_name_player_iris_tp_dimension_player", + "Console must name a player: /iris tp " + ); + public static final TextKey IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS = TextKey.of( + "iris.modded.irismoddedcommands.is_not_generated_by_iris", + "{value}" + " is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED = TextKey.of( + "iris.modded.irismoddedcommands.teleport_failed_dimension_is_not_loaded", + "Teleport failed: dimension " + "{dimensionId}" + " is not loaded." + ); + public static final TextKey IRIS_MODDED_COMMANDS_TELEPORTING = TextKey.of( + "iris.modded.irismoddedcommands.teleporting", + "Teleporting " + "{value}" + " to " + "{dimensionId}" + "..." + ); + public static final TextKey IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_2 = TextKey.of( + "iris.modded.irismoddedcommands.is_not_generated_by_iris_2", + "{value}" + " is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_CANNOT_EVACUATE_PRIMARY_WORLD_THERE_IS_NOWHERE_SEND_PLAYERS = TextKey.of( + "iris.modded.irismoddedcommands.cannot_evacuate_primary_world_there_is_nowhere_send_players", + "Cannot evacuate the primary world; there is nowhere to send players." + ); + public static final TextKey IRIS_MODDED_COMMANDS_EVACUATED_PLAYER_S_FROM = TextKey.of( + "iris.modded.irismoddedcommands.evacuated_player_s_from", + "Evacuated " + "{count}" + " player(s) from " + "{value}" + " to " + "{value2}" + "." + ); + public static final TextKey IRIS_MODDED_COMMANDS_SET_DEBUG = TextKey.of( + "iris.modded.irismoddedcommands.set_debug", + "Set debug to: " + "{to}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_HOTLOADED_SETTINGS = TextKey.of( + "iris.modded.irismoddedcommands.hotloaded_settings", + "Hotloaded settings" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_IT_INSPECTS = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players_it_inspects", + "This command can only be used by players (it inspects your held item)." + ); + public static final TextKey IRIS_MODDED_COMMANDS_YOUR_MAIN_HAND_IS_EMPTY = TextKey.of( + "iris.modded.irismoddedcommands.your_main_hand_is_empty", + "Your main hand is empty." + ); + public static final TextKey IRIS_MODDED_COMMANDS_HAND_X = TextKey.of( + "iris.modded.irismoddedcommands.hand_x", + "Hand: " + "{value}" + " x" + "{value2}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players", + "This command can only be used by players." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_4 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_4", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_5 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_5", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_SEE_IRIS_INFO_LOADED_IRIS = TextKey.of( + "iris.modded.irismoddedcommands.is_not_generated_by_iris_see_iris_info_loaded_iris", + "{value}" + " is not generated by Iris; see /iris info for loaded Iris dimensions." + ); + public static final TextKey IRIS_MODDED_COMMANDS_CURRENT_DIMENSION_IS_NOT_GENERATED_BY_IRIS_NAME_ONE_EXPLICITLY = TextKey.of( + "iris.modded.irismoddedcommands.current_dimension_is_not_generated_by_iris_name_one_explicitly", + "The current dimension (" + "{value}" + ") is not generated by Iris. Name one explicitly: /iris pregen start " + "{radius}" + " ; see /iris info for loaded Iris dimensions." + ); + public static final TextKey IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS = TextKey.of( + "iris.modded.irismoddedcommands.pregeneration_task_is_already_running_stop_it_first_with_iris", + "A pregeneration task is already running. Stop it first with /iris pregen stop." + ); + public static final TextKey IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS = TextKey.of( + "iris.modded.irismoddedcommands.pregen_started_by_blocks_from_progress_logs_console_see_iris", + "Pregen started in " + "{value}" + " of " + "{value2}" + " by " + "{value3}" + " blocks from " + "{centerX}" + "," + "{centerZ}" + "." + "{modeNote}" + " Progress logs to console; see /iris pregen status." + "{guiNote}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_STOPPING_PREGENERATION_FINISHING_UP_CURRENT_REGION = TextKey.of( + "iris.modded.irismoddedcommands.stopping_pregeneration_finishing_up_current_region", + "Stopping pregeneration; finishing up the current region..." + ); + public static final TextKey IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_STOP = TextKey.of( + "iris.modded.irismoddedcommands.no_active_pregeneration_task_stop", + "No active pregeneration task to stop." + ); + public static final TextKey IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_PAUSE_RESUME = TextKey.of( + "iris.modded.irismoddedcommands.no_active_pregeneration_task_pause_resume", + "No active pregeneration task to pause/resume." + ); + public static final TextKey IRIS_MODDED_COMMANDS_PREGENERATION_IS_NOW = TextKey.of( + "iris.modded.irismoddedcommands.pregeneration_is_now", + "Pregeneration is now " + "{value}" + "." + ); + public static final TextKey IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK = TextKey.of( + "iris.modded.irismoddedcommands.no_active_pregeneration_task", + "No active pregeneration task." + ); + public static final TextKey IRIS_MODDED_COMMANDS_IRIS_BY_VOLMIT_SOFTWARE_ON_MINECRAFT_IRIS_DIMENSION_S = TextKey.of( + "iris.modded.irismoddedcommands.iris_by_volmit_software_on_minecraft_iris_dimension_s", + "Iris " + "{value}" + " by Volmit Software on " + "{value2}" + " (Minecraft " + "{value3}" + "), " + "{engines}" + " Iris dimension(s)" + ); + public static final TextKey IRIS_MODDED_COMMANDS_LOADED_DIMENSIONS_IRIS = TextKey.of( + "iris.modded.irismoddedcommands.loaded_dimensions_iris", + "Loaded dimensions: " + "{total}" + " (" + "{iris}" + " Iris)" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_2 = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players_2", + "This command can only be used by players." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_6 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_6", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_BIOME = TextKey.of( + "iris.modded.irismoddedcommands.biome", + "Biome: " + "{value}" + " (" + "{value2}" + ")" + ); + public static final TextKey IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED_2 = TextKey.of( + "iris.modded.irismoddedcommands.biome_lookup_failed_2", + "Biome lookup failed: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_REGION = TextKey.of( + "iris.modded.irismoddedcommands.region", + "Region: " + "{value}" + " (" + "{value2}" + ")" + ); + public static final TextKey IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED_2 = TextKey.of( + "iris.modded.irismoddedcommands.region_lookup_failed_2", + "Region lookup failed: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_CAVE_BIOME = TextKey.of( + "iris.modded.irismoddedcommands.cave_biome", + "Cave biome: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_CAVE_BIOME_LOOKUP_FAILED = TextKey.of( + "iris.modded.irismoddedcommands.cave_biome_lookup_failed", + "Cave biome lookup failed: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_SURFACE_BLOCK_Y = TextKey.of( + "iris.modded.irismoddedcommands.surface_block_y", + "Surface block: " + "{value}" + " (y=" + "{value2}" + ")" + ); + public static final TextKey IRIS_MODDED_COMMANDS_POSITION_CHUNK = TextKey.of( + "iris.modded.irismoddedcommands.position_chunk", + "Position: " + "{value}" + " " + "{value2}" + " " + "{value3}" + " (chunk " + "{value4}" + "," + "{value5}" + ")" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_IT_INSPECTS_2 = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players_it_inspects_2", + "This command can only be used by players (it inspects the block you are looking at)." + ); + public static final TextKey IRIS_MODDED_COMMANDS_LOOK_AT_BLOCK_NOT_SKY = TextKey.of( + "iris.modded.irismoddedcommands.look_at_block_not_sky", + "Look at a block, not the sky." + ); + public static final TextKey IRIS_MODDED_COMMANDS_BLOCK_Y = TextKey.of( + "iris.modded.irismoddedcommands.block_y", + "Block: " + "{value}" + " (y=" + "{value2}" + ")" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_MARKERS_RENDER = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players_markers_render", + "This command can only be used by players (markers render as particles around you)." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_7 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_7", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_SCANNING_MARKERS_AROUND_YOU = TextKey.of( + "iris.modded.irismoddedcommands.scanning_markers_around_you", + "Scanning for '" + "{marker}" + "' markers around you..." + ); + public static final TextKey IRIS_MODDED_COMMANDS_MARKER_SCAN_FAILED = TextKey.of( + "iris.modded.irismoddedcommands.marker_scan_failed", + "Marker scan failed: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_FOUND_NEARBY_MARKER_S = TextKey.of( + "iris.modded.irismoddedcommands.found_nearby_marker_s", + "Found " + "{value}" + " nearby marker(s) (" + "{marker}" + ")" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3 = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players_3", + "This command can only be used by players." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_8", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2 = TextKey.of( + "iris.modded.irismoddedcommands.unknown_biome_2", + "Unknown biome: " + "{key}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_4 = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players_4", + "This command can only be used by players." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_9", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2 = TextKey.of( + "iris.modded.irismoddedcommands.unknown_region_2", + "Unknown region: " + "{key}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION = TextKey.of( + "iris.modded.irismoddedcommands.is_not_defined_dimension", + "{value}" + " is not defined in the dimension!" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_10 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_10", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS = TextKey.of( + "iris.modded.irismoddedcommands.is_not_configured_any_region_biome_object_placements_object_keys", + "{key}" + " is not configured in any region/biome object placements (" + "{value}" + " object keys loaded)." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_OBJECT_KEY = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players_object_key", + "This command can only be used by players. (object key '" + "{key}" + "' resolved against " + "{value}" + " loaded object keys)" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_11 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_11", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_NAME_IRIS_NATIVE_STRUCTURE_LOCATE = TextKey.of( + "iris.modded.irismoddedcommands.name_iris_native_structure_locate", + "Name an Iris or native structure to locate." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_5 = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players_5", + "This command can only be used by players." + ); + public static final TextKey IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_USE_TAB_COMPLETION_CHOOSE_IRIS_PLACEMENT_REGISTERED_NATIVE = TextKey.of( + "iris.modded.irismoddedcommands.unknown_structure_use_tab_completion_choose_iris_placement_registered_native", + "Unknown structure '" + "{key}" + "'. Use tab completion to choose an Iris placement or a registered native/datapack structure." + ); + public static final TextKey IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS = TextKey.of( + "iris.modded.irismoddedcommands.searching_native_structure_within_chunks", + "Searching for native structure " + "{value}" + " within " + "{NATIVESTRUCTURELOCATERADIUS}" + " chunks..." + ); + public static final TextKey IRIS_MODDED_COMMANDS_SEARCHING_IRIS_PLACED_STRUCTURE = TextKey.of( + "iris.modded.irismoddedcommands.searching_iris_placed_structure", + "Searching for Iris-placed structure " + "{key}" + "..." + ); + public static final TextKey IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS = TextKey.of( + "iris.modded.irismoddedcommands.unable_locate_iris_placed_structure_density_search_safety_limit_was", + "Unable to locate Iris-placed structure " + "{key}" + ": the density search safety limit was reached before the full 1024-chunk radius was searched." + ); + public static final TextKey IRIS_MODDED_COMMANDS_COULD_NOT_FIND_IRIS_PLACED_STRUCTURE_WITHIN_1024_CHUNKS = TextKey.of( + "iris.modded.irismoddedcommands.could_not_find_iris_placed_structure_within_1024_chunks", + "Could not find Iris-placed structure " + "{key}" + " within 1024 chunks." + ); + public static final TextKey IRIS_MODDED_COMMANDS_SEARCH_FAILED = TextKey.of( + "iris.modded.irismoddedcommands.search_failed", + "Search failed: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_COULD_NOT_FIND_NATIVE_STRUCTURE_WITHIN_CHUNKS = TextKey.of( + "iris.modded.irismoddedcommands.could_not_find_native_structure_within_chunks", + "Could not find native structure " + "{value}" + " within " + "{NATIVESTRUCTURELOCATERADIUS}" + " chunks." + ); + public static final TextKey IRIS_MODDED_COMMANDS_SEARCH_NATIVE_STRUCTURE_FAILED = TextKey.of( + "iris.modded.irismoddedcommands.search_native_structure_failed", + "Search for native structure " + "{value}" + " failed: " + "{value2}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED = TextKey.of( + "iris.modded.irismoddedcommands.player_disconnected_before_structure_search_completed", + "The player disconnected before the structure search completed." + ); + public static final TextKey IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN = TextKey.of( + "iris.modded.irismoddedcommands.you_changed_dimensions_before_structure_search_completed_run_command_again", + "You changed dimensions before the structure search completed; run the command again." + ); + public static final TextKey IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED = TextKey.of( + "iris.modded.irismoddedcommands.found_at_but_teleportation_failed", + "Found " + "{label}" + " at " + "{targetX}" + " " + "{clampedY}" + " " + "{targetZ}" + ", but teleportation failed." + ); + public static final TextKey IRIS_MODDED_COMMANDS_TELEPORTED_AT = TextKey.of( + "iris.modded.irismoddedcommands.teleported_at", + "Teleported to " + "{label}" + " at " + "{targetX}" + " " + "{clampedY}" + " " + "{targetZ}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_12 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_12", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_STRUCTURE_REACHABILITY_NATIVE_GENERATION_ELIGIBLE_IRIS_PLACED_NATIVE_DISABLED_NATIVE = TextKey.of( + "iris.modded.irismoddedcommands.structure_reachability_native_generation_eligible_iris_placed_native_disabled_native", + "Structure reachability: " + "{available}" + " native generation-eligible, " + "{irisPlaced}" + " Iris-placed, " + "{disabled}" + " native disabled, " + "{suppressed}" + " native replaced by Iris placements, " + "{unreachableBiomes}" + " native excluded by this pack's biomes, and " + "{unsupported}" + " registered native structures unsupported in this dimension. Use /iris structure verify for one structure." + ); + public static final TextKey IRIS_MODDED_COMMANDS_STRUCTURE_IS_IRIS_PLACED_LOCATABLE_WITH_IRIS_GOTO_STRUCTURE = TextKey.of( + "iris.modded.irismoddedcommands.structure_is_iris_placed_locatable_with_iris_goto_structure", + "Structure " + "{key}" + " is Iris-placed and locatable with /iris goto structure " + "{key2}" + "." + ); + public static final TextKey IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_IT_IS_NEITHER_IRIS_PLACED_NOR_REGISTERED_BY = TextKey.of( + "iris.modded.irismoddedcommands.unknown_structure_it_is_neither_iris_placed_nor_registered_by", + "Unknown structure '" + "{key}" + "'. It is neither Iris-placed nor registered by vanilla or a datapack." + ); + public static final TextKey IRIS_MODDED_COMMANDS_STRUCTURE_IS_EXPLICITLY_REPLACED_BY_IRIS_PLACEMENT_LOCATABLE_WITH_IRIS = TextKey.of( + "iris.modded.irismoddedcommands.structure_is_explicitly_replaced_by_iris_placement_locatable_with_iris", + "Structure " + "{value}" + " is explicitly replaced by an Iris placement and locatable with /iris goto structure " + "{value2}" + "." + ); + public static final TextKey IRIS_MODDED_COMMANDS_NATIVE_STRUCTURE_IS_ENABLED_SUPPORTED_BY_THIS_DIMENSION_S_GENERATOR = TextKey.of( + "iris.modded.irismoddedcommands.native_structure_is_enabled_supported_by_this_dimension_s_generator", + "Native structure " + "{value}" + " is enabled, supported by this dimension's generator state, and locatable with /iris goto structure " + "{value2}" + "." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_13 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_13", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_POI_TYPE = TextKey.of( + "iris.modded.irismoddedcommands.this_command_can_only_be_used_by_players_poi_type", + "This command can only be used by players. (POI type '" + "{type}" + "' accepted)" + ); + public static final TextKey IRIS_MODDED_COMMANDS_SEARCHING = TextKey.of( + "iris.modded.irismoddedcommands.searching", + "Searching for " + "{label}" + "..." + ); + public static final TextKey IRIS_MODDED_COMMANDS_ENGINE_THIS_WORLD_HAS_BEEN_CLOSED_REJOIN_DIMENSION_TRY_AGAIN = TextKey.of( + "iris.modded.irismoddedcommands.engine_this_world_has_been_closed_rejoin_dimension_try_again", + "The engine for this world has been closed; rejoin the dimension and try again." + ); + public static final TextKey IRIS_MODDED_COMMANDS_SEARCH_FAILED_2 = TextKey.of( + "iris.modded.irismoddedcommands.search_failed_2", + "Search failed: " + "{failure}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_COULD_NOT_FIND_WITHIN_SEARCH_TIMEOUT = TextKey.of( + "iris.modded.irismoddedcommands.could_not_find_within_search_timeout", + "Could not find " + "{label}" + " within the search timeout." + ); + public static final TextKey IRIS_MODDED_COMMANDS_TELEPORTED_AT_2 = TextKey.of( + "iris.modded.irismoddedcommands.teleported_at_2", + "Teleported to " + "{label}" + " at " + "{blockX}" + " " + "{blockY}" + " " + "{blockZ}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN = TextKey.of( + "iris.modded.irismoddedcommands.engine_changed_while_locating_try_again", + "The engine changed while locating " + "{label}" + "; try again." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_14 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_14", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_WORLD_SEED = TextKey.of( + "iris.modded.irismoddedcommands.world_seed", + "World seed: " + "{value}" + ); + public static final TextKey IRIS_MODDED_COMMANDS_ENGINE_SEED_MIXED = TextKey.of( + "iris.modded.irismoddedcommands.engine_seed_mixed", + "Engine seed: " + "{value}" + " (mixed: " + "{value2}" + ")" + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_15 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_15", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_DOWNLOADING_IRISDIMENSIONS = TextKey.of( + "iris.modded.irismoddedcommands.downloading_irisdimensions", + "Downloading IrisDimensions/" + "{pack}" + " (" + "{downloadSource}" + ")..." + ); + public static final TextKey IRIS_MODDED_COMMANDS_PACK_INSTALLED_ITS_EXACT_DIMENSION_TYPES_CUSTOM_BIOMES_JOIN_FORCED = TextKey.of( + "iris.modded.irismoddedcommands.pack_installed_its_exact_dimension_types_custom_biomes_join_forced", + "Pack '" + "{pack}" + "' installed. Its exact dimension types and custom biomes join the forced Iris datapack on the next server restart; restart before creating worlds from this pack." + ); + public static final TextKey IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE = TextKey.of( + "iris.modded.irismoddedcommands.pack_download_failed_see_console", + "Pack download failed for " + "{pack}" + " (" + "{downloadSource}" + "; see console)." + ); + public static final TextKey IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_16 = TextKey.of( + "iris.modded.irismoddedcommands.this_dimension_is_not_generated_by_iris_16", + "This dimension is not generated by Iris." + ); + public static final TextKey IRIS_MODDED_COMMANDS_GENERATED_CHUNK_S_S = TextKey.of( + "iris.modded.irismoddedcommands.generated_chunk_s_s", + "Generated: " + "{value}" + " chunk(s), " + "{value2}" + "/s" + ); + public static final TextKey IRIS_MODDED_COMMANDS_MS = TextKey.of( + "iris.modded.irismoddedcommands.ms", + " " + "{value}" + ": " + "{value2}" + "ms" + ); + public static final TextKey MODDED_COMMAND_HELP_UNKNOWN_IRIS_HELP_SECTION = TextKey.of( + "iris.modded.moddedcommandhelp.unknown_iris_help_section", + "Unknown Iris help section: " + "{normalized}" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_TYPE_ACTIVE_LOGICAL_PACK_ENGINE_NOT_STARTED_PACK_HEIGHTS_UNKNOWN = TextKey.of( + "iris.modded.moddeddatapackcommands.type_active_logical_pack_engine_not_started_pack_heights_unknown", + "{dimensionId}" + ": type=" + "{typeKey}" + " active=" + "{activeMin}" + ".." + "{activeMax}" + " logical=" + "{value}" + " (pack=" + "{value2}" + ", engine not started; pack heights unknown)" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_TYPE_ACTIVE_LOGICAL_PACK_WANTS_LOGICAL = TextKey.of( + "iris.modded.moddeddatapackcommands.type_active_logical_pack_wants_logical", + "{dimensionId}" + ": type=" + "{typeKey}" + " active=" + "{activeMin}" + ".." + "{activeMax}" + " logical=" + "{value}" + " | pack '" + "{value2}" + "' wants " + "{packMin}" + ".." + "{packMax}" + " logical=" + "{packLogical}" + " | " + "{value3}" + "{value4}" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_WARNING_ACTIVE_DIMENSION_TYPE_DOES_NOT_MATCH_PACK_IRIS_WILL = TextKey.of( + "iris.modded.moddeddatapackcommands.warning_active_dimension_type_does_not_match_pack_iris_will", + " WARNING: the active dimension type does not match the pack. Iris will refuse to start this world engine instead of clipping terrain. Run /iris world enable for new worlds, or /iris datapack install for already-loaded Iris dimensions, then restart." + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_NO_IRIS_DIMENSIONS_ARE_LOADED = TextKey.of( + "iris.modded.moddeddatapackcommands.no_iris_dimensions_are_loaded", + "No Iris dimensions are loaded." + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_ALL_IRIS_DIMENSION_S_MATCH_THEIR_PACK_HEIGHT_RANGES = TextKey.of( + "iris.modded.moddeddatapackcommands.all_iris_dimension_s_match_their_pack_height_ranges", + "All " + "{irisLevels}" + " Iris dimension(s) match their pack height ranges." + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_ENGINE_NOT_STARTED_CANNOT_DERIVE_ITS_DIMENSION_TYPE_YET = TextKey.of( + "iris.modded.moddeddatapackcommands.engine_not_started_cannot_derive_its_dimension_type_yet", + "{value}" + ": engine not started; cannot derive its dimension type yet." + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_DIMENSION_TYPE_GENERATION_FAILED = TextKey.of( + "iris.modded.moddeddatapackcommands.dimension_type_generation_failed", + "{value}" + ": dimension type generation failed: " + "{value2}" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_FAILED_WRITE = TextKey.of( + "iris.modded.moddeddatapackcommands.failed_write", + "Failed to write " + "{output}" + ": " + "{value}" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_NO_IRIS_DIMENSIONS_WITH_RUNNING_ENGINES_FOUND_NOTHING_WAS_INSTALLED = TextKey.of( + "iris.modded.moddeddatapackcommands.no_iris_dimensions_with_running_engines_found_nothing_was_installed", + "No Iris dimensions with running engines found; nothing was installed." + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_FAILED_WRITE_2 = TextKey.of( + "iris.modded.moddeddatapackcommands.failed_write_2", + "Failed to write " + "{mcmeta}" + ": " + "{value}" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_WROTE = TextKey.of( + "iris.modded.moddeddatapackcommands.wrote", + "Wrote " + "{path}" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_WORLD_DATAPACK_DIMENSION_TYPE_OVERRIDES_INSTALLED_RESTART_SERVER_DIMENSION_TYPES = TextKey.of( + "iris.modded.moddeddatapackcommands.world_datapack_dimension_type_overrides_installed_restart_server_dimension_types", + "World datapack '" + "{WORLDPACKNAME}" + "' dimension type overrides installed. Restart the server for the dimension types to apply." + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_CONFIGURED_DATAPACK_IMPORTS = TextKey.of( + "iris.modded.moddeddatapackcommands.configured_datapack_imports", + "Configured datapack imports: " + "{value}" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_MESSAGE = TextKey.of( + "iris.modded.moddeddatapackcommands.message", + " - " + "{url}" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_MODRINTH_INGEST_IS_BUKKIT_ONLY_INSTALL_THESE_MANUALLY_INTO_WORLD = TextKey.of( + "iris.modded.moddeddatapackcommands.modrinth_ingest_is_bukkit_only_install_these_manually_into_world", + "Modrinth ingest is Bukkit-only; install these manually into world/datapacks if needed." + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_INSTALLED_WORLD_DATAPACKS = TextKey.of( + "iris.modded.moddeddatapackcommands.installed_world_datapacks", + "Installed world datapacks: " + "{value}" + ); + public static final TextKey MODDED_DATAPACK_COMMANDS_MESSAGE_2 = TextKey.of( + "iris.modded.moddeddatapackcommands.message_2", + " - " + "{name}" + ); + public static final TextKey MODDED_DEVELOPER_COMMANDS_DISPATCHED_TEST_EXCEPTION_IRIS_ERROR_REPORTER_SENTRY_IF_ENABLED = TextKey.of( + "iris.modded.moddeddevelopercommands.dispatched_test_exception_iris_error_reporter_sentry_if_enabled", + "Dispatched a test exception to the Iris error reporter (Sentry if enabled)." + ); + public static final TextKey MODDED_DEVELOPER_COMMANDS_MESSAGE = TextKey.of( + "iris.modded.moddeddevelopercommands.message", + " " + "{value}" + ); + public static final TextKey MODDED_DEVELOPER_COMMANDS_NETWORK_SCAN_FAILED = TextKey.of( + "iris.modded.moddeddevelopercommands.network_scan_failed", + "Network scan failed: " + "{value}" + ); + public static final TextKey MODDED_GOLDEN_HASH_GOLDENHASH_SCAN_IS_ALREADY_RUNNING = TextKey.of( + "iris.modded.moddedgoldenhash.goldenhash_scan_is_already_running", + "A goldenhash scan is already running." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_WAND_IS = TextKey.of( + "iris.modded.moddedobjectcommands.this_command_can_only_be_used_by_players_wand_is", + "This command can only be used by players. (the wand is a physical item)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_YOUR_INVENTORY_IS_FULL = TextKey.of( + "iris.modded.moddedobjectcommands.your_inventory_is_full", + "Your inventory is full." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_POOF_GOOD_LUCK_BUILDING_LEFT_CLICK_CORNER_1_RIGHT_CLICK = TextKey.of( + "iris.modded.moddedobjectcommands.poof_good_luck_building_left_click_corner_1_right_click", + "Poof! Good luck building! (left click = corner 1, right click = corner 2)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_DUST_IS = TextKey.of( + "iris.modded.moddedobjectcommands.this_command_can_only_be_used_by_players_dust_is", + "This command can only be used by players. (the dust is a physical item)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_YOUR_INVENTORY_IS_FULL_2 = TextKey.of( + "iris.modded.moddedobjectcommands.your_inventory_is_full_2", + "Your inventory is full." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_RIGHT_CLICK_BLOCK_REVEAL_OBJECT_IT_BELONGS = TextKey.of( + "iris.modded.moddedobjectcommands.right_click_block_reveal_object_it_belongs", + "Right click a block to reveal the object it belongs to." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_SAVING_CAPTURES = TextKey.of( + "iris.modded.moddedobjectcommands.this_command_can_only_be_used_by_players_saving_captures", + "This command can only be used by players. (saving captures your wand selection)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_OBJECTS_SAVE_INTO = TextKey.of( + "iris.modded.moddedobjectcommands.this_dimension_is_not_generated_by_iris_objects_save_into", + "This dimension is not generated by Iris; objects save into the dimension's pack." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_HOLD_YOUR_IRIS_WAND_IRIS_WAND = TextKey.of( + "iris.modded.moddedobjectcommands.hold_your_iris_wand_iris_wand", + "Hold your Iris wand. (/iris wand)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_NO_AREA_SELECTED_LEFT_RIGHT_CLICK_BLOCKS_WITH_WAND_FIRST = TextKey.of( + "iris.modded.moddedobjectcommands.no_area_selected_left_right_click_blocks_with_wand_first", + "No area selected. Left/right click blocks with the wand first." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_INVALID_OBJECT_NAME = TextKey.of( + "iris.modded.moddedobjectcommands.invalid_object_name", + "Invalid object name: " + "{nameRaw}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_SELECTION_TOO_LARGE_BLOCKS_MAX = TextKey.of( + "iris.modded.moddedobjectcommands.selection_too_large_blocks_max", + "Selection too large: " + "{volume}" + " blocks (max " + "{MAXSAVEVOLUME}" + ")." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_FILE_ALREADY_EXISTS_USE_IRIS_OBJECT_SAVE_OVERWRITE = TextKey.of( + "iris.modded.moddedobjectcommands.file_already_exists_use_iris_object_save_overwrite", + "File already exists. Use /iris object save overwrite " + "{name}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT = TextKey.of( + "iris.modded.moddedobjectcommands.failed_save_object", + "Failed to save object: " + "{value}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_SAVED_OBJECTS_IOB_X_X_BLOCK_S = TextKey.of( + "iris.modded.moddedobjectcommands.saved_objects_iob_x_x_block_s", + "Saved " + "{value}" + "/objects/" + "{name}" + ".iob: " + "{w}" + "x" + "{h}" + "x" + "{d}" + ", " + "{value2}" + " block(s)" + "{tileNote}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_UNKNOWN_EMPTY_OBJECT = TextKey.of( + "iris.modded.moddedobjectcommands.unknown_empty_object", + "Unknown or empty object: " + "{key}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_CONSOLE_MUST_SPECIFY_COORDINATES_IRIS_OBJECT_PASTE_AT_X_Y = TextKey.of( + "iris.modded.moddedobjectcommands.console_must_specify_coordinates_iris_object_paste_at_x_y", + "Console must specify coordinates: /iris object paste at [rotate ] " + "{key}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_YOU_ARE_NOT_LOOKING_AT_BLOCK_WITHIN_BLOCKS = TextKey.of( + "iris.modded.moddedobjectcommands.you_are_not_looking_at_block_within_blocks", + "You are not looking at a block within " + "{value}" + " blocks." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_PASTE_FAILED_PARTIAL_CHANGES_RECORDED_UNDO = TextKey.of( + "iris.modded.moddedobjectcommands.paste_failed_partial_changes_recorded_undo", + "Paste failed: " + "{value}" + " (partial changes recorded for undo)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_PLACED_AT_ROT_WRITE_S_NON_AIR = TextKey.of( + "iris.modded.moddedobjectcommands.placed_at_rot_write_s_non_air", + "Placed " + "{key}" + " at " + "{value}" + " " + "{value2}" + " " + "{value3}" + " rot=" + "{rotation}" + " (" + "{value4}" + " write(s), " + "{value5}" + " non-air" + "{tileNote}" + ")" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS = TextKey.of( + "iris.modded.moddedobjectcommands.this_command_can_only_be_used_by_players", + "This command can only be used by players." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_HOLD_YOUR_IRIS_WAND_IRIS_WAND_2 = TextKey.of( + "iris.modded.moddedobjectcommands.hold_your_iris_wand_iris_wand_2", + "Hold your Iris wand. (/iris wand)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_NO_AREA_SELECTED = TextKey.of( + "iris.modded.moddedobjectcommands.no_area_selected", + "No area selected." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_MESSAGE = TextKey.of( + "iris.modded.moddedobjectcommands.message", + "{value}" + " " + "{amount}" + " " + "{value2}" + ": " + "{value3}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_2 = TextKey.of( + "iris.modded.moddedobjectcommands.this_command_can_only_be_used_by_players_2", + "This command can only be used by players." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_READY_YOUR_WAND_IRIS_WAND = TextKey.of( + "iris.modded.moddedobjectcommands.ready_your_wand_iris_wand", + "Ready your wand. (/iris wand)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_YOU_ARE_NOT_LOOKING_AT_BLOCK = TextKey.of( + "iris.modded.moddedobjectcommands.you_are_not_looking_at_block", + "You are not looking at a block." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_POSITION_SET = TextKey.of( + "iris.modded.moddedobjectcommands.position_set", + "Position " + "{value}" + " set to " + "{value2}" + " " + "{value3}" + " " + "{value4}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3 = TextKey.of( + "iris.modded.moddedobjectcommands.this_command_can_only_be_used_by_players_3", + "This command can only be used by players." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_HOLD_YOUR_WAND = TextKey.of( + "iris.modded.moddedobjectcommands.hold_your_wand", + "Hold your wand!" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_NO_AREA_SELECTED_2 = TextKey.of( + "iris.modded.moddedobjectcommands.no_area_selected_2", + "No area selected." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_SELECTION_TOO_LARGE_AUTO_SELECT_BLOCKS_MAX = TextKey.of( + "iris.modded.moddedobjectcommands.selection_too_large_auto_select_blocks_max", + "Selection too large for auto-select: " + "{volume}" + " blocks (max " + "{MAXAUTOSELECTVOLUME}" + ")." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_AUTO_SELECT_COMPLETE = TextKey.of( + "iris.modded.moddedobjectcommands.auto_select_complete", + "Auto-select complete: " + "{value}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_UNKNOWN_OBJECT = TextKey.of( + "iris.modded.moddedobjectcommands.unknown_object", + "Unknown object: " + "{key}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_OBJECT_SIZE = TextKey.of( + "iris.modded.moddedobjectcommands.object_size", + "Object Size: " + "{value}" + " * " + "{value2}" + " * " + "{value3}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_BLOCKS_USED = TextKey.of( + "iris.modded.moddedobjectcommands.blocks_used", + "Blocks Used: " + "{value}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_BLOCKS_OBJECT = TextKey.of( + "iris.modded.moddedobjectcommands.blocks_object", + "== Blocks in object ==" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_MESSAGE_2 = TextKey.of( + "iris.modded.moddedobjectcommands.message_2", + " - " + "{value}" + " * " + "{value2}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_OTHER_BLOCK_STATE_S = TextKey.of( + "iris.modded.moddedobjectcommands.other_block_state_s", + " + " + "{remaining}" + " other block state(s)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_UNKNOWN_OBJECT_2 = TextKey.of( + "iris.modded.moddedobjectcommands.unknown_object_2", + "Unknown object: " + "{key}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_CURRENT_OBJECT_SIZE = TextKey.of( + "iris.modded.moddedobjectcommands.current_object_size", + "Current Object Size: " + "{value}" + " * " + "{value2}" + " * " + "{value3}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_NEW_OBJECT_SIZE = TextKey.of( + "iris.modded.moddedobjectcommands.new_object_size", + "New Object Size: " + "{value}" + " * " + "{value2}" + " * " + "{value3}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_OBJECT_HAS_NO_LOAD_FILE_CANNOT_PERSIST_SHRINK = TextKey.of( + "iris.modded.moddedobjectcommands.object_has_no_load_file_cannot_persist_shrink", + "Object has no load file; cannot persist the shrink." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT_2 = TextKey.of( + "iris.modded.moddedobjectcommands.failed_save_object_2", + "Failed to save object " + "{value}" + ": " + "{value2}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_INVALID_REACH = TextKey.of( + "iris.modded.moddedobjectcommands.invalid_reach", + "Invalid reach: " + "{token}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_NO_OBJECTS_MATCHED = TextKey.of( + "iris.modded.moddedobjectcommands.no_objects_matched", + "No objects matched: " + "{target}" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_PLAUSIBILIZE_REACH_QUEUED_OBJECT_S = TextKey.of( + "iris.modded.moddedobjectcommands.plausibilize_reach_queued_object_s", + "Plausibilize [reach=" + "{reach}" + "{value}" + "] queued " + "{value2}" + " object(s)" + ); + public static final TextKey MODDED_OBJECT_COMMANDS_NOTHING_UNDO = TextKey.of( + "iris.modded.moddedobjectcommands.nothing_undo", + "Nothing to undo." + ); + public static final TextKey MODDED_OBJECT_COMMANDS_REVERTED_PASTE_S = TextKey.of( + "iris.modded.moddedobjectcommands.reverted_paste_s", + "Reverted " + "{reverted}" + " paste(s)!" + ); + public static final TextKey MODDED_PACK_COMMANDS_PACKS_FOLDER_NOT_FOUND = TextKey.of( + "iris.modded.moddedpackcommands.packs_folder_not_found", + "Packs folder not found: " + "{value}" + ); + public static final TextKey MODDED_PACK_COMMANDS_NO_PACKS_VALIDATE_UNDER = TextKey.of( + "iris.modded.moddedpackcommands.no_packs_validate_under", + "No packs to validate under " + "{value}" + ); + public static final TextKey MODDED_PACK_COMMANDS_PACK_NOT_FOUND_UNDER = TextKey.of( + "iris.modded.moddedpackcommands.pack_not_found_under", + "Pack '" + "{pack}" + "' not found under " + "{value}" + ); + public static final TextKey MODDED_PACK_COMMANDS_VALIDATING_PACK_S = TextKey.of( + "iris.modded.moddedpackcommands.validating_pack_s", + "Validating " + "{value}" + " pack(s)..." + ); + public static final TextKey MODDED_PACK_COMMANDS_VALIDATION_FAILED = TextKey.of( + "iris.modded.moddedpackcommands.validation_failed", + "Validation of '" + "{value}" + "' failed: " + "{value2}" + ); + public static final TextKey MODDED_PACK_COMMANDS_VALIDATION_COMPLETE_BROKEN_PACKS = TextKey.of( + "iris.modded.moddedpackcommands.validation_complete_broken_packs", + "Validation complete. Broken packs: " + "{brokenTotal}" + "/" + "{value}" + ); + public static final TextKey MODDED_PACK_COMMANDS_PACK_NOT_FOUND_UNDER_2 = TextKey.of( + "iris.modded.moddedpackcommands.pack_not_found_under_2", + "Pack '" + "{pack}" + "' not found under " + "{value}" + ); + public static final TextKey MODDED_PACK_COMMANDS_PACK_NOT_FOUND_UNDER_3 = TextKey.of( + "iris.modded.moddedpackcommands.pack_not_found_under_3", + "Pack '" + "{pack}" + "' not found under " + "{value}" + ); + public static final TextKey MODDED_PACK_COMMANDS_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST = TextKey.of( + "iris.modded.moddedpackcommands.no_validation_results_recorded_run_iris_pack_validate_first", + "No validation results recorded. Run /iris pack validate first." + ); + public static final TextKey MODDED_PACK_COMMANDS_BLOCKING_WARNINGS = TextKey.of( + "iris.modded.moddedpackcommands.blocking_warnings", + "{tag}" + " " + "{value}" + " (blocking=" + "{value2}" + ", warnings=" + "{value3}" + ")" + ); + public static final TextKey MODDED_PACK_COMMANDS_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE = TextKey.of( + "iris.modded.moddedpackcommands.no_validation_result_run_iris_pack_validate", + "No validation result for '" + "{pack}" + "'. Run /iris pack validate " + "{pack2}" + "." + ); + public static final TextKey MODDED_PACK_COMMANDS_PACK_IS_LOADABLE_WARNINGS = TextKey.of( + "iris.modded.moddedpackcommands.pack_is_loadable_warnings", + "Pack '" + "{value}" + "' is loadable." + " (warnings=" + "{value2}" + ")" + ); + public static final TextKey MODDED_PACK_COMMANDS_PACK_IS_BROKEN = TextKey.of( + "iris.modded.moddedpackcommands.pack_is_broken", + "Pack '" + "{value}" + "' is BROKEN:" + ); + public static final TextKey MODDED_PACK_COMMANDS_MESSAGE = TextKey.of( + "iris.modded.moddedpackcommands.message", + " - " + "{reason}" + ); + public static final TextKey MODDED_PACK_COMMANDS_MESSAGE_2 = TextKey.of( + "iris.modded.moddedpackcommands.message_2", + " ! " + "{value}" + ); + public static final TextKey MODDED_PACK_COMMANDS_MORE_WARNING_S = TextKey.of( + "iris.modded.moddedpackcommands.more_warning_s", + " ... and " + "{value}" + " more warning(s)." + ); + public static final TextKey MODDED_PACK_COMMANDS_NO_CLEANUP_CANDIDATES_FOUND_PACK = TextKey.of( + "iris.modded.moddedpackcommands.no_cleanup_candidates_found_pack", + "No cleanup candidates found for pack '" + "{pack}" + "'." + ); + public static final TextKey MODDED_PACK_COMMANDS_CLEANUP_PREVIEW_PACK_CANDIDATE_S_NO_FILES_WERE_CHANGED = TextKey.of( + "iris.modded.moddedpackcommands.cleanup_preview_pack_candidate_s_no_files_were_changed", + "Cleanup preview for pack '" + "{pack}" + "': " + "{value}" + " candidate(s). No files were changed." + ); + public static final TextKey MODDED_PACK_COMMANDS_RUN_IRIS_PACK_CLEANUP_APPLY_QUARANTINE_AFTER_FRESH_SCAN = TextKey.of( + "iris.modded.moddedpackcommands.run_iris_pack_cleanup_apply_quarantine_after_fresh_scan", + "Run /iris pack cleanup " + "{pack}" + " apply to quarantine after a fresh scan." + ); + public static final TextKey MODDED_PACK_COMMANDS_NO_CLEANUP_CANDIDATES_FOUND_PACK_2 = TextKey.of( + "iris.modded.moddedpackcommands.no_cleanup_candidates_found_pack_2", + "No cleanup candidates found for pack '" + "{pack}" + "'." + ); + public static final TextKey MODDED_PACK_COMMANDS_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER = TextKey.of( + "iris.modded.moddedpackcommands.quarantined_cleanup_candidate_s_under", + "Quarantined " + "{value}" + " cleanup candidate(s) under " + "{value2}" + "." + ); + public static final TextKey MODDED_PACK_COMMANDS_NOTHING_RESTORE_PACK = TextKey.of( + "iris.modded.moddedpackcommands.nothing_restore_pack", + "Nothing to restore for pack '" + "{pack}" + "'." + ); + public static final TextKey MODDED_PACK_COMMANDS_RESTORE_PREVIEW_FILE_S_NO_FILES_WERE_CHANGED = TextKey.of( + "iris.modded.moddedpackcommands.restore_preview_file_s_no_files_were_changed", + "Restore preview for " + "{value}" + ": " + "{value2}" + " file(s). No files were changed." + ); + public static final TextKey MODDED_PACK_COMMANDS_RESTORE_IS_BLOCKED_BY_EXISTING_DESTINATION_S = TextKey.of( + "iris.modded.moddedpackcommands.restore_is_blocked_by_existing_destination_s", + "Restore is blocked by " + "{value}" + " existing destination(s)." + ); + public static final TextKey MODDED_PACK_COMMANDS_RUN_IRIS_PACK_RESTORE_APPLY_RESTORE_AFTER_FRESH_CONFLICT_CHECK = TextKey.of( + "iris.modded.moddedpackcommands.run_iris_pack_restore_apply_restore_after_fresh_conflict_check", + "Run /iris pack restore " + "{pack}" + " apply to restore after a fresh conflict check." + ); + public static final TextKey MODDED_PACK_COMMANDS_RESTORE_REFUSED_BECAUSE_DESTINATION_S_ALREADY_EXIST = TextKey.of( + "iris.modded.moddedpackcommands.restore_refused_because_destination_s_already_exist", + "Restore refused because " + "{value}" + " destination(s) already exist." + ); + public static final TextKey MODDED_PACK_COMMANDS_NOTHING_RESTORE_PACK_2 = TextKey.of( + "iris.modded.moddedpackcommands.nothing_restore_pack_2", + "Nothing to restore for pack '" + "{pack}" + "'." + ); + public static final TextKey MODDED_PACK_COMMANDS_RESTORED_FILE_S_FROM = TextKey.of( + "iris.modded.moddedpackcommands.restored_file_s_from", + "Restored " + "{value}" + " file(s) from " + "{value2}" + "." + ); + public static final TextKey MODDED_PACK_COMMANDS_MESSAGE_3 = TextKey.of( + "iris.modded.moddedpackcommands.message_3", + " - " + "{label}" + ": " + "{value}" + ); + public static final TextKey MODDED_PACK_COMMANDS_MORE = TextKey.of( + "iris.modded.moddedpackcommands.more", + " ... and " + "{value}" + " more." + ); + public static final TextKey MODDED_REGEN_REGEN_IS_ALREADY_RUNNING = TextKey.of( + "iris.modded.moddedregen.regen_is_already_running", + "A regen is already running." + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_RUN_THIS_FROM = TextKey.of( + "iris.modded.moddedstructurecommands.this_dimension_is_not_generated_by_iris_run_this_from", + "This dimension is not generated by Iris. Run this from an Iris dimension so the pack can be resolved." + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_WROTE_STRUCTURE_INDEX = TextKey.of( + "iris.modded.moddedstructurecommands.wrote_structure_index", + "Wrote structure index: " + "{value}" + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_NO_IRIS_STRUCTURE_THIS_PACK = TextKey.of( + "iris.modded.moddedstructurecommands.no_iris_structure_this_pack", + "No iris structure '" + "{key}" + "' in this pack" + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_STRUCTURE_ASSEMBLED_0_PIECES_CHECK_STARTPOOL = TextKey.of( + "iris.modded.moddedstructurecommands.structure_assembled_0_pieces_check_startpool", + "Structure '" + "{key}" + "' assembled 0 pieces (check startPool '" + "{value}" + "')" + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_STRUCTURE_PIECES_FOOTPRINT_X_BLOCKS_SAMPLE_SEED_1234 = TextKey.of( + "iris.modded.moddedstructurecommands.structure_pieces_footprint_x_blocks_sample_seed_1234", + "Structure '" + "{key}" + "': " + "{value}" + " pieces, footprint " + "{value2}" + "x" + "{value3}" + " blocks (sample seed 1234)" + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_STRUCTURE_IS = TextKey.of( + "iris.modded.moddedstructurecommands.this_command_can_only_be_used_by_players_structure_is", + "This command can only be used by players (the structure is assembled at your position)." + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_NO_IRIS_STRUCTURE_THIS_PACK_2 = TextKey.of( + "iris.modded.moddedstructurecommands.no_iris_structure_this_pack_2", + "No iris structure '" + "{key}" + "' in this pack" + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_STRUCTURE_ASSEMBLED_0_PIECES = TextKey.of( + "iris.modded.moddedstructurecommands.structure_assembled_0_pieces", + "Structure '" + "{key}" + "' assembled 0 pieces" + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_PLACE_FAILED_PARTIAL_CHANGES_RECORDED_UNDO = TextKey.of( + "iris.modded.moddedstructurecommands.place_failed_partial_changes_recorded_undo", + "Place failed: " + "{value}" + " (partial changes recorded for undo)" + ); + public static final TextKey MODDED_STRUCTURE_COMMANDS_PLACED_PIECES_WRITE_S_AT_YOUR_LOCATION_IRIS_OBJECT_UNDO = TextKey.of( + "iris.modded.moddedstructurecommands.placed_pieces_write_s_at_your_location_iris_object_undo", + "Placed '" + "{key}" + "' (" + "{value}" + " pieces, " + "{value2}" + " write(s)" + "{tileNote}" + ") at your location. /iris object undo to revert." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PROVIDE_DIMENSION_PACK_IRIS_STUDIO_OPEN_PACK_SEED = TextKey.of( + "iris.modded.moddedstudiocommands.provide_dimension_pack_iris_studio_open_pack_seed", + "Provide a dimension pack: /iris studio open [seed]" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_ON_SERVER_DISPLAY = TextKey.of( + "iris.modded.moddedstudiocommands.opening_noise_explorer_on_server_display", + "Opening the Noise Explorer on the server display." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_RUN_IRIS_STUDIO = TextKey.of( + "iris.modded.moddedstudiocommands.this_dimension_is_not_generated_by_iris_run_iris_studio", + "This dimension is not generated by Iris; run /iris studio noise from an Iris dimension to resolve generators, or omit the generator name." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_UNKNOWN_GENERATOR_PACK = TextKey.of( + "iris.modded.moddedstudiocommands.unknown_generator_pack", + "Unknown generator '" + "{generatorKey}" + "' in pack " + "{value}" + "." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED = TextKey.of( + "iris.modded.moddedstudiocommands.opening_noise_explorer_generator_seed", + "Opening the Noise Explorer for generator '" + "{value}" + "' (seed " + "{seed}" + ")." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_STAND_IRIS_STUDIO = TextKey.of( + "iris.modded.moddedstudiocommands.this_dimension_is_not_generated_by_iris_stand_iris_studio", + "This dimension is not generated by Iris; stand in an Iris (or studio) dimension and run /iris studio map." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY = TextKey.of( + "iris.modded.moddedstudiocommands.opening_vision_map_on_server_display", + "Opening the Vision map for " + "{value}" + " on the server display." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_FAILED_WRITE_WORKSPACE = TextKey.of( + "iris.modded.moddedstudiocommands.failed_write_workspace", + "Failed to write workspace for " + "{value}" + ": " + "{value2}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_WORKSPACE_REGENERATED_WITH_JSON_SCHEMAS_AUTOCOMPLETE = TextKey.of( + "iris.modded.moddedstudiocommands.workspace_regenerated_with_json_schemas_autocomplete", + "Workspace regenerated: " + "{value}" + " with JSON schemas for autocomplete." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_COULD_NOT_OPEN = TextKey.of( + "iris.modded.moddedstudiocommands.could_not_open", + "Could not open " + "{value}" + ": " + "{value2}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_OPENING_YOUR_EDITOR = TextKey.of( + "iris.modded.moddedstudiocommands.opening_your_editor", + "Opening " + "{value}" + " in your editor." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_SPECIFY_PACK_NAME = TextKey.of( + "iris.modded.moddedstudiocommands.this_dimension_is_not_generated_by_iris_specify_pack_name", + "This dimension is not generated by Iris; specify a pack name explicitly." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACK_NOT_FOUND_UNDER = TextKey.of( + "iris.modded.moddedstudiocommands.pack_not_found_under", + "Pack '" + "{name}" + "' not found under " + "{value}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PROVIDE_DIMENSION_PACK_IRIS_STUDIO_OPEN_PACK_SEED_2 = TextKey.of( + "iris.modded.moddedstudiocommands.provide_dimension_pack_iris_studio_open_pack_seed_2", + "Provide a dimension pack: /iris studio open [seed]" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_OPENING_STUDIO_SEED = TextKey.of( + "iris.modded.moddedstudiocommands.opening_studio_seed", + "Opening studio for '" + "{pack}" + "' (seed " + "{seed}" + ")..." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACK_MISSING_DOWNLOADING_IRISDIMENSIONS = TextKey.of( + "iris.modded.moddedstudiocommands.pack_missing_downloading_irisdimensions", + "Pack '" + "{pack}" + "' missing; downloading IrisDimensions/" + "{pack2}" + "..." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_TRY_IRIS_DOWNLOAD = TextKey.of( + "iris.modded.moddedstudiocommands.pack_could_not_be_downloaded_check_name_try_iris_download", + "Pack '" + "{pack}" + "' could not be downloaded; check the name and try /iris download " + "{pack2}" + "." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACK_HAS_NO_DIMENSIONS_JSON = TextKey.of( + "iris.modded.moddedstudiocommands.pack_has_no_dimensions_json", + "Pack '" + "{pack}" + "' has no dimensions/" + "{pack2}" + ".json" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_OPEN_FAILED = TextKey.of( + "iris.modded.moddedstudiocommands.studio_open_failed", + "Studio open failed: " + "{value}" + "{errorMessage}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED = TextKey.of( + "iris.modded.moddedstudiocommands.studio_injection_failed", + "Studio injection failed: " + "{value}" + "{errorMessage}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_CONSOLE_STUDIO_OPEN_NOW_RUNS_SEED_TRANSIENT_NOT_WRITTEN_IRIS = TextKey.of( + "iris.modded.moddedstudiocommands.console_studio_open_now_runs_seed_transient_not_written_iris", + "Console studio open: " + "{dimensionId}" + " now runs '" + "{pack}" + "' seed " + "{seed}" + " (transient; not written to iris-dimensions.json and cleared on restart)." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_ENTER_IT_WITH_EXECUTE_RUN_TP_S_8_5_8 = TextKey.of( + "iris.modded.moddedstudiocommands.enter_it_with_execute_run_tp_s_8_5_8", + "Enter it with: /execute in " + "{dimensionId}" + " run tp @s 8.5 " + "{surface}" + " 8.5" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PREGEN_IT_WITH_IRIS_PREGEN_START_RADIUS = TextKey.of( + "iris.modded.moddedstudiocommands.pregen_it_with_iris_pregen_start_radius", + "Pregen it with: /iris pregen start " + "{dimensionId}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_REMOVE_IT_WITH_IRIS_STUDIO_CLOSE = TextKey.of( + "iris.modded.moddedstudiocommands.remove_it_with_iris_studio_close", + "Remove it with: /iris studio close" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED_2 = TextKey.of( + "iris.modded.moddedstudiocommands.studio_injection_failed_2", + "Studio injection failed: " + "{value}" + "{errorMessage}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN = TextKey.of( + "iris.modded.moddedstudiocommands.studio_open_now_runs_seed_use_iris_studio_close_when", + "Studio open: " + "{dimensionId}" + " now runs '" + "{pack}" + "' seed " + "{seed}" + ". Use /iris studio close when done." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN = TextKey.of( + "iris.modded.moddedstudiocommands.you_do_not_have_open_studio_use_iris_studio_open", + "You do not have an open studio. Use /iris studio open first." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED = TextKey.of( + "iris.modded.moddedstudiocommands.studio_close_failed", + "Studio close failed: " + "{value}" + "{errorMessage}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED = TextKey.of( + "iris.modded.moddedstudiocommands.studio_closed_was_evacuated_unloaded_its_region_data_deleted", + "Studio closed: " + "{dimensionId}" + " was evacuated, unloaded and its region data deleted." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_NO_STUDIO_DIMENSIONS_ARE_CURRENTLY_OPEN = TextKey.of( + "iris.modded.moddedstudiocommands.no_studio_dimensions_are_currently_open", + "No studio dimensions are currently open." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_ACTIVE_STUDIO_DIMENSION_S = TextKey.of( + "iris.modded.moddedstudiocommands.active_studio_dimension_s", + "Active studio dimension(s): " + "{value}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACK_SEED_OWNER = TextKey.of( + "iris.modded.moddedstudiocommands.pack_seed_owner", + " " + "{value}" + ": pack '" + "{value2}" + "' seed " + "{value3}" + " owner " + "{ownerName}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS = TextKey.of( + "iris.modded.moddedstudiocommands.this_command_can_only_be_used_by_players", + "This command can only be used by players." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN_2 = TextKey.of( + "iris.modded.moddedstudiocommands.you_do_not_have_open_studio_use_iris_studio_open_2", + "You do not have an open studio. Use /iris studio open first." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO = TextKey.of( + "iris.modded.moddedstudiocommands.your_studio_dimension_is_no_longer_loaded_use_iris_studio", + "Your studio dimension is no longer loaded. Use /iris studio open again." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_TELEPORTED_YOUR_STUDIO = TextKey.of( + "iris.modded.moddedstudiocommands.teleported_your_studio", + "Teleported to your studio (" + "{dimensionId}" + ")." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACK_HAS_NO_DIMENSIONS_JSON_2 = TextKey.of( + "iris.modded.moddedstudiocommands.pack_has_no_dimensions_json_2", + "Pack '" + "{value}" + "' has no dimensions/" + "{value2}" + ".json" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACK_HAS_VERSION = TextKey.of( + "iris.modded.moddedstudiocommands.pack_has_version", + "The \"" + "{value}" + "\" pack has version: " + "{value2}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_INVALID_PROJECT_NAME_ALLOWED_Z_0_9 = TextKey.of( + "iris.modded.moddedstudiocommands.invalid_project_name_allowed_z_0_9", + "Invalid project name '" + "{nameRaw}" + "' (allowed: a-z, 0-9, _ and -)" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACK_ALREADY_EXISTS_AT = TextKey.of( + "iris.modded.moddedstudiocommands.pack_already_exists_at", + "Pack '" + "{name}" + "' already exists at " + "{value}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_CREATING_PROJECT_FROM_TEMPLATE = TextKey.of( + "iris.modded.moddedstudiocommands.creating_project_from_template", + "Creating project '" + "{name}" + "' from template '" + "{template}" + "'..." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_TEMPLATE_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS = TextKey.of( + "iris.modded.moddedstudiocommands.template_is_not_installed_downloading_irisdimensions", + "Template '" + "{template}" + "' is not installed; downloading IrisDimensions/" + "{template2}" + "..." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_TEMPLATE_COULD_NOT_BE_DOWNLOADED_INSTALL_PACK_WITH_DIMENSIONS_JSON = TextKey.of( + "iris.modded.moddedstudiocommands.template_could_not_be_downloaded_install_pack_with_dimensions_json", + "Template '" + "{template}" + "' could not be downloaded; install a pack with dimensions/" + "{template2}" + ".json first." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_CREATED_PROJECT_AT = TextKey.of( + "iris.modded.moddedstudiocommands.created_project_at", + "Created project '" + "{name}" + "' at " + "{value}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_EDIT_DIMENSIONS_JSON_REST_PACK_VSCODE_WORKSPACE_WITH_JSON_SCHEMA = TextKey.of( + "iris.modded.moddedstudiocommands.edit_dimensions_json_rest_pack_vscode_workspace_with_json_schema", + "Edit dimensions/" + "{name}" + ".json and the rest of the pack. A VSCode workspace with JSON schema autocomplete was generated; open it or rerun /iris studio vscode." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PROJECT_CREATION_FAILED = TextKey.of( + "iris.modded.moddedstudiocommands.project_creation_failed", + "Project creation failed: " + "{value}" + "{errorMessage}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACKAGING_DIMENSION = TextKey.of( + "iris.modded.moddedstudiocommands.packaging_dimension", + "Packaging dimension '" + "{dimKey}" + "'..." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACKAGE_COMPILED = TextKey.of( + "iris.modded.moddedstudiocommands.package_compiled", + "Package compiled: " + "{value}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_PACKAGING_FAILED = TextKey.of( + "iris.modded.moddedstudiocommands.packaging_failed", + "Packaging failed: " + "{value}" + "{errorMessage}" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_SAMPLING_IS = TextKey.of( + "iris.modded.moddedstudiocommands.this_command_can_only_be_used_by_players_sampling_is", + "This command can only be used by players (sampling is centered on your position)." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS = TextKey.of( + "iris.modded.moddedstudiocommands.this_dimension_is_not_generated_by_iris", + "This dimension is not generated by Iris." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_SAMPLING_REGION_DISTRIBUTION_X_CHUNKS_AROUND_YOU = TextKey.of( + "iris.modded.moddedstudiocommands.sampling_region_distribution_x_chunks_around_you", + "Sampling region distribution in " + "{value}" + "x" + "{value2}" + " chunks around you..." + ); + public static final TextKey MODDED_STUDIO_COMMANDS_RARITY = TextKey.of( + "iris.modded.moddedstudiocommands.rarity", + "{key}" + ": rarity=" + "{rarity}" + " / " + "{value}" + "%" + ); + public static final TextKey MODDED_STUDIO_COMMANDS_REGION_SAMPLING_FAILED = TextKey.of( + "iris.modded.moddedstudiocommands.region_sampling_failed", + "Region sampling failed: " + "{value}" + ); + public static final TextKey MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS = TextKey.of( + "iris.modded.moddedworldcommands.pack_is_not_installed_downloading_irisdimensions", + "Pack '" + "{pack}" + "' is not installed; downloading IrisDimensions/" + "{pack2}" + "..." + ); + public static final TextKey MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH = TextKey.of( + "iris.modded.moddedworldcommands.pack_could_not_be_downloaded_check_name_install_it_with", + "Pack '" + "{pack}" + "' could not be downloaded; check the name or install it with /iris download " + "{pack2}" + "." + ); + public static final TextKey MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_WORLD = TextKey.of( + "iris.modded.moddedworldcommands.failed_inject_iris_world", + "Failed to inject Iris world '" + "{dimensionId}" + "': " + "{value}" + "{errorMessage}" + ); + public static final TextKey MODDED_WORLD_COMMANDS_CREATED_IRIS_WORLD_FROM_PACK_DIMENSION_SEED = TextKey.of( + "iris.modded.moddedworldcommands.created_iris_world_from_pack_dimension_seed", + "Created Iris world " + "{dimensionId}" + " from pack '" + "{pack}" + "' dimension '" + "{packDimension}" + "' (seed " + "{seed}" + ")." + ); + public static final TextKey MODDED_WORLD_COMMANDS_IT_IS_LIVE_NOW_RE_INJECTED_ON_EVERY_STARTUP_TELEPORT = TextKey.of( + "iris.modded.moddedworldcommands.it_is_live_now_re_injected_on_every_startup_teleport", + "It is live now and re-injected on every startup. Teleport in with /iris world status or a portal; no restart required." + ); + public static final TextKey MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_PRIMARY_WORLD = TextKey.of( + "iris.modded.moddedworldcommands.failed_inject_iris_primary_world", + "Failed to inject Iris primary world: " + "{value}" + "{errorMessage}" + ); + public static final TextKey MODDED_WORLD_COMMANDS_IRIS_PRIMARY_WORLD_SET_PACK_DIMENSION_SEED = TextKey.of( + "iris.modded.moddedworldcommands.iris_primary_world_set_pack_dimension_seed", + "Iris primary world set to " + "{dimensionId}" + " (pack '" + "{pack}" + "' dimension '" + "{packDimension}" + "' seed " + "{seed}" + ")." + ); + public static final TextKey MODDED_WORLD_COMMANDS_VANILLA_OVERWORLD_GENERATOR_CANNOT_BE_HOT_SWAPPED_SO_THIS_DOES = TextKey.of( + "iris.modded.moddedworldcommands.vanilla_overworld_generator_cannot_be_hot_swapped_so_this_does", + "The vanilla overworld generator cannot be hot-swapped, so this does NOT regenerate the existing overworld." + ); + public static final TextKey MODDED_WORLD_COMMANDS_INSTEAD_IS_NOW_CONFIGURED_PRIMARY_WORLD_PLAYERS_VANILLA_OVERWORLD_ARE = TextKey.of( + "iris.modded.moddedworldcommands.instead_is_now_configured_primary_world_players_vanilla_overworld_are", + "Instead, " + "{dimensionId}" + " is now the configured primary world: players in the vanilla overworld are routed there on join, and it re-injects on every startup." + ); + public static final TextKey MODDED_WORLD_COMMANDS_IRIS_MAIN_WORLD_OVERRIDE_CLEARED_OVERWORLD_KEEPS_ITS_CURRENT_GENERATOR = TextKey.of( + "iris.modded.moddedworldcommands.iris_main_world_override_cleared_overworld_keeps_its_current_generator", + "Iris main world override cleared. The overworld keeps its current generator; edit server.properties level-type and restart to change it back." + ); + public static final TextKey MODDED_WORLD_COMMANDS_INVALID_SEED_USE_NUMBER_RANDOM = TextKey.of( + "iris.modded.moddedworldcommands.invalid_seed_use_number_random", + "Invalid seed '" + "{seedRaw}" + "'. Use a number or 'random'." + ); + public static final TextKey MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS_2 = TextKey.of( + "iris.modded.moddedworldcommands.pack_is_not_installed_downloading_irisdimensions_2", + "Pack '" + "{pack}" + "' is not installed; downloading IrisDimensions/" + "{pack2}" + "..." + ); + public static final TextKey MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH_2 = TextKey.of( + "iris.modded.moddedworldcommands.pack_could_not_be_downloaded_check_name_install_it_with_2", + "Pack '" + "{pack}" + "' could not be downloaded; check the name or install it with /iris download " + "{pack2}" + "." + ); + public static final TextKey MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND = TextKey.of( + "iris.modded.moddedworldcommands.pack_is_not_ready_yet_still_loading_validating_try_command", + "Pack '" + "{pack}" + "' is not ready yet (still loading or validating). Try the command again in a moment." + ); + public static final TextKey MODDED_WORLD_COMMANDS_FAILED_WRITE_SERVER_PROPERTIES_CHECK_FILE_PERMISSIONS_SET_LEVEL_TYPE = TextKey.of( + "iris.modded.moddedworldcommands.failed_write_server_properties_check_file_permissions_set_level_type", + "Could not update server.properties (missing at the server working directory, or unwritable); see the server log, or set level-type manually." + ); + public static final TextKey MODDED_WORLD_COMMANDS_IRIS_MAIN_WORLD_SET_PRESET_SEED = TextKey.of( + "iris.modded.moddedworldcommands.iris_main_world_set_preset_seed", + "Iris main world set to '" + "{pack}" + "' (preset " + "{preset}" + ", seed " + "{value}" + ")." + ); + public static final TextKey MODDED_WORLD_COMMANDS_SERVER_PROPERTIES_LEVEL_TYPE_IS_NOW_ON_NEXT_RESTART_OVERWORLD = TextKey.of( + "iris.modded.moddedworldcommands.server_properties_level_type_is_now_on_next_restart_overworld", + "server.properties level-type is now " + "{preset}" + ". On the next restart the overworld, nether, and end regenerate as this Iris world." + ); + public static final TextKey MODDED_WORLD_COMMANDS_PLAYER_DATA_INVENTORIES_ADVANCEMENTS_STATS_IS_KEPT_EXISTING_TERRAIN_THOSE = TextKey.of( + "iris.modded.moddedworldcommands.player_data_inventories_advancements_stats_is_kept_existing_terrain_those", + "Player data (inventories, advancements, stats) is kept; existing terrain in those dimensions is replaced. This applies once." + ); + public static final TextKey MODDED_WORLD_COMMANDS_MAINWORLDAUTORESTART_IS_ENABLED_STOPPING_SERVER_NOW_SO_YOUR_RESTART_WRAPPER = TextKey.of( + "iris.modded.moddedworldcommands.mainworldautorestart_is_enabled_stopping_server_now_so_your_restart_wrapper", + "mainWorldAutoRestart is enabled - stopping the server now so your restart wrapper brings it back on the new main world." + ); + public static final TextKey MODDED_WORLD_COMMANDS_RESTART_SERVER_NOW_GENERATE_IT_SET_MAINWORLDAUTORESTART_TRUE_MODDED_JSON = TextKey.of( + "iris.modded.moddedworldcommands.restart_server_now_generate_it_set_mainworldautorestart_true_modded_json", + "Restart the server now to generate it. (Set mainWorldAutoRestart=true in modded.json to have Iris stop the server for you.)" + ); + public static final TextKey MODDED_WORLD_COMMANDS_REFUSING_CREATE_WORLD_USING_PACK_BECAUSE_REQUIRED_VALIDATION_FAILED = TextKey.of( + "iris.modded.moddedworldcommands.refusing_create_world_using_pack_because_required_validation_failed", + "Refusing to create world '" + "{dimensionId}" + "' using pack '" + "{pack}" + "' because required validation failed:" + ); + public static final TextKey MODDED_WORLD_COMMANDS_MESSAGE = TextKey.of( + "iris.modded.moddedworldcommands.message", + " - " + "{reason}" + ); + public static final TextKey MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE = TextKey.of( + "iris.modded.moddedworldcommands.fix_pack_run_iris_pack_validate_revalidate", + "Fix the pack and run /iris pack validate " + "{pack}" + " to revalidate." + ); + public static final TextKey MODDED_WORLD_COMMANDS_PACK_WAS_NOT_FOUND_UNDER = TextKey.of( + "iris.modded.moddedworldcommands.pack_was_not_found_under", + "Pack '" + "{pack}" + "' was not found under " + "{value}" + ); + public static final TextKey MODDED_WORLD_COMMANDS_PACK_DOES_NOT_CONTAIN_DIMENSIONS_JSON = TextKey.of( + "iris.modded.moddedworldcommands.pack_does_not_contain_dimensions_json", + "Pack '" + "{pack}" + "' does not contain dimensions/" + "{packDimension}" + ".json" + ); + public static final TextKey MODDED_WORLD_COMMANDS_INVALID_PACK_REFERENCE_USE_PACK_PACK_DIMENSIONKEY = TextKey.of( + "iris.modded.moddedworldcommands.invalid_pack_reference_use_pack_pack_dimensionkey", + "Invalid pack reference '" + "{pack}" + "{value}" + "'. Use pack or pack:dimensionKey." + ); + public static final TextKey MODDED_WORLD_COMMANDS_INVALID_SEED_USE_NUMBER_RANDOM_2 = TextKey.of( + "iris.modded.moddedworldcommands.invalid_seed_use_number_random_2", + "Invalid seed '" + "{seedRaw}" + "'. Use a number or 'random'." + ); + public static final TextKey MODDED_WORLD_COMMANDS_FAILED_REMOVE_IRIS_WORLD = TextKey.of( + "iris.modded.moddedworldcommands.failed_remove_iris_world", + "Failed to remove Iris world '" + "{dimensionId}" + "': " + "{value}" + "{errorMessage}" + ); + public static final TextKey MODDED_WORLD_COMMANDS_IRIS_WORLD_WAS_NOT_LOADED_CLEARED_ITS_PERSISTENT_REGISTRY_ENTRY = TextKey.of( + "iris.modded.moddedworldcommands.iris_world_was_not_loaded_cleared_its_persistent_registry_entry", + "Iris world '" + "{dimensionId}" + "' was not loaded; cleared its persistent registry entry and wiped its stored chunk/mantle data." + ); + public static final TextKey MODDED_WORLD_COMMANDS_IRIS_WORLD_WAS_NOT_LOADED_CLEARED_ITS_PERSISTENT_REGISTRY_ENTRY_2 = TextKey.of( + "iris.modded.moddedworldcommands.iris_world_was_not_loaded_cleared_its_persistent_registry_entry_2", + "Iris world '" + "{dimensionId}" + "' was not loaded; cleared its persistent registry entry. World data on disk is kept." + ); + public static final TextKey MODDED_WORLD_COMMANDS_DELETED_IRIS_WORLD_EVACUATED_UNLOADED_CHUNK_MANTLE_DATA_WIPED_DROPPED = TextKey.of( + "iris.modded.moddedworldcommands.deleted_iris_world_evacuated_unloaded_chunk_mantle_data_wiped_dropped", + "Deleted Iris world '" + "{dimensionId}" + "': evacuated, unloaded, chunk/mantle data wiped, and dropped from the startup registry." + ); + public static final TextKey MODDED_WORLD_COMMANDS_DISABLED_IRIS_WORLD_EVACUATED_UNLOADED_DROPPED_FROM_STARTUP_REGISTRY = TextKey.of( + "iris.modded.moddedworldcommands.disabled_iris_world_evacuated_unloaded_dropped_from_startup_registry", + "Disabled Iris world '" + "{dimensionId}" + "': evacuated, unloaded, and dropped from the startup registry." + ); + public static final TextKey MODDED_WORLD_COMMANDS_WORLD_DATA_ON_DISK_IS_KEPT_RE_ENABLE_WITH_IRIS = TextKey.of( + "iris.modded.moddedworldcommands.world_data_on_disk_is_kept_re_enable_with_iris", + "World data on disk is kept; re-enable with /iris world enable " + "{dimensionId}" + " or delete it with /iris world delete " + "{dimensionId2}" + "." + ); + public static final TextKey MODDED_WORLD_COMMANDS_LOADED_IRIS_LEVEL_PACK_DIMENSION = TextKey.of( + "iris.modded.moddedworldcommands.loaded_iris_level_pack_dimension", + "Loaded Iris level: " + "{value}" + " -> pack '" + "{value2}" + "' dimension '" + "{value3}" + "'" + ); + public static final TextKey MODDED_WORLD_COMMANDS_PRIMARY_WORLD = TextKey.of( + "iris.modded.moddedworldcommands.primary_world", + "Primary world: " + "{primary}" + "{value}" + ); + public static final TextKey MODDED_WORLD_COMMANDS_NO_IRIS_DIMENSIONS_ARE_CURRENTLY_LOADED_CREATE_ONE_WITH_IRIS = TextKey.of( + "iris.modded.moddedworldcommands.no_iris_dimensions_are_currently_loaded_create_one_with_iris", + "No Iris dimensions are currently loaded. Create one with /iris world create ." + ); + public static final TextKey MODDED_WORLD_COMMANDS_LOADED_IRIS_DIMENSIONS = TextKey.of( + "iris.modded.moddedworldcommands.loaded_iris_dimensions", + "Loaded Iris dimensions: " + "{value}" + ); + public static final TextKey MODDED_WORLD_COMMANDS_MESSAGE_2 = TextKey.of( + "iris.modded.moddedworldcommands.message_2", + " - " + "{dimension}" + ); + public static final TextKey MODDED_WORLD_COMMANDS_USE_IRIS_WORLD_CREATE_NAME_PACK_INJECT_ONE_WITHOUT_RESTARTING = TextKey.of( + "iris.modded.moddedworldcommands.use_iris_world_create_name_pack_inject_one_without_restarting", + "Use /iris world create to inject one without restarting." + ); + + private static final List KEYS = List.of( + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS, + IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_BIOME_IRIS_EDIT_BIOME_KEY, + IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED, + IRIS_MODDED_COMMANDS_UNKNOWN_BIOME, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_2, + IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_REGION_IRIS_EDIT_REGION_KEY, + IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, + IRIS_MODDED_COMMANDS_UNKNOWN_REGION, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_3, + IRIS_MODDED_COMMANDS_CANNOT_OPEN_FILES_HERE, + IRIS_MODDED_COMMANDS_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM, + IRIS_MODDED_COMMANDS_COULD_NOT_OPEN, + IRIS_MODDED_COMMANDS_OPENING_YOUR_EDITOR, + IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_PLAYER_IRIS_TP_DIMENSION_PLAYER, + IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS, + IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED, + IRIS_MODDED_COMMANDS_TELEPORTING, + IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_2, + IRIS_MODDED_COMMANDS_CANNOT_EVACUATE_PRIMARY_WORLD_THERE_IS_NOWHERE_SEND_PLAYERS, + IRIS_MODDED_COMMANDS_EVACUATED_PLAYER_S_FROM, + IRIS_MODDED_COMMANDS_SET_DEBUG, + IRIS_MODDED_COMMANDS_HOTLOADED_SETTINGS, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_IT_INSPECTS, + IRIS_MODDED_COMMANDS_YOUR_MAIN_HAND_IS_EMPTY, + IRIS_MODDED_COMMANDS_HAND_X, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_4, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_5, + IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_SEE_IRIS_INFO_LOADED_IRIS, + IRIS_MODDED_COMMANDS_CURRENT_DIMENSION_IS_NOT_GENERATED_BY_IRIS_NAME_ONE_EXPLICITLY, + IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS, + IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS, + IRIS_MODDED_COMMANDS_STOPPING_PREGENERATION_FINISHING_UP_CURRENT_REGION, + IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_STOP, + IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_PAUSE_RESUME, + IRIS_MODDED_COMMANDS_PREGENERATION_IS_NOW, + IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK, + IRIS_MODDED_COMMANDS_IRIS_BY_VOLMIT_SOFTWARE_ON_MINECRAFT_IRIS_DIMENSION_S, + IRIS_MODDED_COMMANDS_LOADED_DIMENSIONS_IRIS, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_2, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_6, + IRIS_MODDED_COMMANDS_BIOME, + IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED_2, + IRIS_MODDED_COMMANDS_REGION, + IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED_2, + IRIS_MODDED_COMMANDS_CAVE_BIOME, + IRIS_MODDED_COMMANDS_CAVE_BIOME_LOOKUP_FAILED, + IRIS_MODDED_COMMANDS_SURFACE_BLOCK_Y, + IRIS_MODDED_COMMANDS_POSITION_CHUNK, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_IT_INSPECTS_2, + IRIS_MODDED_COMMANDS_LOOK_AT_BLOCK_NOT_SKY, + IRIS_MODDED_COMMANDS_BLOCK_Y, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_MARKERS_RENDER, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_7, + IRIS_MODDED_COMMANDS_SCANNING_MARKERS_AROUND_YOU, + IRIS_MODDED_COMMANDS_MARKER_SCAN_FAILED, + IRIS_MODDED_COMMANDS_FOUND_NEARBY_MARKER_S, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8, + IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_4, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9, + IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, + IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_10, + IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_OBJECT_KEY, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_11, + IRIS_MODDED_COMMANDS_NAME_IRIS_NATIVE_STRUCTURE_LOCATE, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_5, + IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_USE_TAB_COMPLETION_CHOOSE_IRIS_PLACEMENT_REGISTERED_NATIVE, + IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, + IRIS_MODDED_COMMANDS_SEARCHING_IRIS_PLACED_STRUCTURE, + IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, + IRIS_MODDED_COMMANDS_COULD_NOT_FIND_IRIS_PLACED_STRUCTURE_WITHIN_1024_CHUNKS, + IRIS_MODDED_COMMANDS_SEARCH_FAILED, + IRIS_MODDED_COMMANDS_COULD_NOT_FIND_NATIVE_STRUCTURE_WITHIN_CHUNKS, + IRIS_MODDED_COMMANDS_SEARCH_NATIVE_STRUCTURE_FAILED, + IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED, + IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN, + IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, + IRIS_MODDED_COMMANDS_TELEPORTED_AT, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_12, + IRIS_MODDED_COMMANDS_STRUCTURE_REACHABILITY_NATIVE_GENERATION_ELIGIBLE_IRIS_PLACED_NATIVE_DISABLED_NATIVE, + IRIS_MODDED_COMMANDS_STRUCTURE_IS_IRIS_PLACED_LOCATABLE_WITH_IRIS_GOTO_STRUCTURE, + IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_IT_IS_NEITHER_IRIS_PLACED_NOR_REGISTERED_BY, + IRIS_MODDED_COMMANDS_STRUCTURE_IS_EXPLICITLY_REPLACED_BY_IRIS_PLACEMENT_LOCATABLE_WITH_IRIS, + IRIS_MODDED_COMMANDS_NATIVE_STRUCTURE_IS_ENABLED_SUPPORTED_BY_THIS_DIMENSION_S_GENERATOR, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_13, + IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_POI_TYPE, + IRIS_MODDED_COMMANDS_SEARCHING, + IRIS_MODDED_COMMANDS_ENGINE_THIS_WORLD_HAS_BEEN_CLOSED_REJOIN_DIMENSION_TRY_AGAIN, + IRIS_MODDED_COMMANDS_SEARCH_FAILED_2, + IRIS_MODDED_COMMANDS_COULD_NOT_FIND_WITHIN_SEARCH_TIMEOUT, + IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, + IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_14, + IRIS_MODDED_COMMANDS_WORLD_SEED, + IRIS_MODDED_COMMANDS_ENGINE_SEED_MIXED, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_15, + IRIS_MODDED_COMMANDS_DOWNLOADING_IRISDIMENSIONS, + IRIS_MODDED_COMMANDS_PACK_INSTALLED_ITS_EXACT_DIMENSION_TYPES_CUSTOM_BIOMES_JOIN_FORCED, + IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, + IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_16, + IRIS_MODDED_COMMANDS_GENERATED_CHUNK_S_S, + IRIS_MODDED_COMMANDS_MS, + MODDED_COMMAND_HELP_UNKNOWN_IRIS_HELP_SECTION, + MODDED_DATAPACK_COMMANDS_TYPE_ACTIVE_LOGICAL_PACK_ENGINE_NOT_STARTED_PACK_HEIGHTS_UNKNOWN, + MODDED_DATAPACK_COMMANDS_TYPE_ACTIVE_LOGICAL_PACK_WANTS_LOGICAL, + MODDED_DATAPACK_COMMANDS_WARNING_ACTIVE_DIMENSION_TYPE_DOES_NOT_MATCH_PACK_IRIS_WILL, + MODDED_DATAPACK_COMMANDS_NO_IRIS_DIMENSIONS_ARE_LOADED, + MODDED_DATAPACK_COMMANDS_ALL_IRIS_DIMENSION_S_MATCH_THEIR_PACK_HEIGHT_RANGES, + MODDED_DATAPACK_COMMANDS_ENGINE_NOT_STARTED_CANNOT_DERIVE_ITS_DIMENSION_TYPE_YET, + MODDED_DATAPACK_COMMANDS_DIMENSION_TYPE_GENERATION_FAILED, + MODDED_DATAPACK_COMMANDS_FAILED_WRITE, + MODDED_DATAPACK_COMMANDS_NO_IRIS_DIMENSIONS_WITH_RUNNING_ENGINES_FOUND_NOTHING_WAS_INSTALLED, + MODDED_DATAPACK_COMMANDS_FAILED_WRITE_2, + MODDED_DATAPACK_COMMANDS_WROTE, + MODDED_DATAPACK_COMMANDS_WORLD_DATAPACK_DIMENSION_TYPE_OVERRIDES_INSTALLED_RESTART_SERVER_DIMENSION_TYPES, + MODDED_DATAPACK_COMMANDS_CONFIGURED_DATAPACK_IMPORTS, + MODDED_DATAPACK_COMMANDS_MESSAGE, + MODDED_DATAPACK_COMMANDS_MODRINTH_INGEST_IS_BUKKIT_ONLY_INSTALL_THESE_MANUALLY_INTO_WORLD, + MODDED_DATAPACK_COMMANDS_INSTALLED_WORLD_DATAPACKS, + MODDED_DATAPACK_COMMANDS_MESSAGE_2, + MODDED_DEVELOPER_COMMANDS_DISPATCHED_TEST_EXCEPTION_IRIS_ERROR_REPORTER_SENTRY_IF_ENABLED, + MODDED_DEVELOPER_COMMANDS_MESSAGE, + MODDED_DEVELOPER_COMMANDS_NETWORK_SCAN_FAILED, + MODDED_GOLDEN_HASH_GOLDENHASH_SCAN_IS_ALREADY_RUNNING, + MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_WAND_IS, + MODDED_OBJECT_COMMANDS_YOUR_INVENTORY_IS_FULL, + MODDED_OBJECT_COMMANDS_POOF_GOOD_LUCK_BUILDING_LEFT_CLICK_CORNER_1_RIGHT_CLICK, + MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_DUST_IS, + MODDED_OBJECT_COMMANDS_YOUR_INVENTORY_IS_FULL_2, + MODDED_OBJECT_COMMANDS_RIGHT_CLICK_BLOCK_REVEAL_OBJECT_IT_BELONGS, + MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_SAVING_CAPTURES, + MODDED_OBJECT_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_OBJECTS_SAVE_INTO, + MODDED_OBJECT_COMMANDS_HOLD_YOUR_IRIS_WAND_IRIS_WAND, + MODDED_OBJECT_COMMANDS_NO_AREA_SELECTED_LEFT_RIGHT_CLICK_BLOCKS_WITH_WAND_FIRST, + MODDED_OBJECT_COMMANDS_INVALID_OBJECT_NAME, + MODDED_OBJECT_COMMANDS_SELECTION_TOO_LARGE_BLOCKS_MAX, + MODDED_OBJECT_COMMANDS_FILE_ALREADY_EXISTS_USE_IRIS_OBJECT_SAVE_OVERWRITE, + MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, + MODDED_OBJECT_COMMANDS_SAVED_OBJECTS_IOB_X_X_BLOCK_S, + MODDED_OBJECT_COMMANDS_UNKNOWN_EMPTY_OBJECT, + MODDED_OBJECT_COMMANDS_CONSOLE_MUST_SPECIFY_COORDINATES_IRIS_OBJECT_PASTE_AT_X_Y, + MODDED_OBJECT_COMMANDS_YOU_ARE_NOT_LOOKING_AT_BLOCK_WITHIN_BLOCKS, + MODDED_OBJECT_COMMANDS_PASTE_FAILED_PARTIAL_CHANGES_RECORDED_UNDO, + MODDED_OBJECT_COMMANDS_PLACED_AT_ROT_WRITE_S_NON_AIR, + MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS, + MODDED_OBJECT_COMMANDS_HOLD_YOUR_IRIS_WAND_IRIS_WAND_2, + MODDED_OBJECT_COMMANDS_NO_AREA_SELECTED, + MODDED_OBJECT_COMMANDS_MESSAGE, + MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_2, + MODDED_OBJECT_COMMANDS_READY_YOUR_WAND_IRIS_WAND, + MODDED_OBJECT_COMMANDS_YOU_ARE_NOT_LOOKING_AT_BLOCK, + MODDED_OBJECT_COMMANDS_POSITION_SET, + MODDED_OBJECT_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3, + MODDED_OBJECT_COMMANDS_HOLD_YOUR_WAND, + MODDED_OBJECT_COMMANDS_NO_AREA_SELECTED_2, + MODDED_OBJECT_COMMANDS_SELECTION_TOO_LARGE_AUTO_SELECT_BLOCKS_MAX, + MODDED_OBJECT_COMMANDS_AUTO_SELECT_COMPLETE, + MODDED_OBJECT_COMMANDS_UNKNOWN_OBJECT, + MODDED_OBJECT_COMMANDS_OBJECT_SIZE, + MODDED_OBJECT_COMMANDS_BLOCKS_USED, + MODDED_OBJECT_COMMANDS_BLOCKS_OBJECT, + MODDED_OBJECT_COMMANDS_MESSAGE_2, + MODDED_OBJECT_COMMANDS_OTHER_BLOCK_STATE_S, + MODDED_OBJECT_COMMANDS_UNKNOWN_OBJECT_2, + MODDED_OBJECT_COMMANDS_CURRENT_OBJECT_SIZE, + MODDED_OBJECT_COMMANDS_NEW_OBJECT_SIZE, + MODDED_OBJECT_COMMANDS_OBJECT_HAS_NO_LOAD_FILE_CANNOT_PERSIST_SHRINK, + MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT_2, + MODDED_OBJECT_COMMANDS_INVALID_REACH, + MODDED_OBJECT_COMMANDS_NO_OBJECTS_MATCHED, + MODDED_OBJECT_COMMANDS_PLAUSIBILIZE_REACH_QUEUED_OBJECT_S, + MODDED_OBJECT_COMMANDS_NOTHING_UNDO, + MODDED_OBJECT_COMMANDS_REVERTED_PASTE_S, + MODDED_PACK_COMMANDS_PACKS_FOLDER_NOT_FOUND, + MODDED_PACK_COMMANDS_NO_PACKS_VALIDATE_UNDER, + MODDED_PACK_COMMANDS_PACK_NOT_FOUND_UNDER, + MODDED_PACK_COMMANDS_VALIDATING_PACK_S, + MODDED_PACK_COMMANDS_VALIDATION_FAILED, + MODDED_PACK_COMMANDS_VALIDATION_COMPLETE_BROKEN_PACKS, + MODDED_PACK_COMMANDS_PACK_NOT_FOUND_UNDER_2, + MODDED_PACK_COMMANDS_PACK_NOT_FOUND_UNDER_3, + MODDED_PACK_COMMANDS_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST, + MODDED_PACK_COMMANDS_BLOCKING_WARNINGS, + MODDED_PACK_COMMANDS_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE, + MODDED_PACK_COMMANDS_PACK_IS_LOADABLE_WARNINGS, + MODDED_PACK_COMMANDS_PACK_IS_BROKEN, + MODDED_PACK_COMMANDS_MESSAGE, + MODDED_PACK_COMMANDS_MESSAGE_2, + MODDED_PACK_COMMANDS_MORE_WARNING_S, + MODDED_PACK_COMMANDS_NO_CLEANUP_CANDIDATES_FOUND_PACK, + MODDED_PACK_COMMANDS_CLEANUP_PREVIEW_PACK_CANDIDATE_S_NO_FILES_WERE_CHANGED, + MODDED_PACK_COMMANDS_RUN_IRIS_PACK_CLEANUP_APPLY_QUARANTINE_AFTER_FRESH_SCAN, + MODDED_PACK_COMMANDS_NO_CLEANUP_CANDIDATES_FOUND_PACK_2, + MODDED_PACK_COMMANDS_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, + MODDED_PACK_COMMANDS_NOTHING_RESTORE_PACK, + MODDED_PACK_COMMANDS_RESTORE_PREVIEW_FILE_S_NO_FILES_WERE_CHANGED, + MODDED_PACK_COMMANDS_RESTORE_IS_BLOCKED_BY_EXISTING_DESTINATION_S, + MODDED_PACK_COMMANDS_RUN_IRIS_PACK_RESTORE_APPLY_RESTORE_AFTER_FRESH_CONFLICT_CHECK, + MODDED_PACK_COMMANDS_RESTORE_REFUSED_BECAUSE_DESTINATION_S_ALREADY_EXIST, + MODDED_PACK_COMMANDS_NOTHING_RESTORE_PACK_2, + MODDED_PACK_COMMANDS_RESTORED_FILE_S_FROM, + MODDED_PACK_COMMANDS_MESSAGE_3, + MODDED_PACK_COMMANDS_MORE, + MODDED_REGEN_REGEN_IS_ALREADY_RUNNING, + MODDED_STRUCTURE_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_RUN_THIS_FROM, + MODDED_STRUCTURE_COMMANDS_WROTE_STRUCTURE_INDEX, + MODDED_STRUCTURE_COMMANDS_NO_IRIS_STRUCTURE_THIS_PACK, + MODDED_STRUCTURE_COMMANDS_STRUCTURE_ASSEMBLED_0_PIECES_CHECK_STARTPOOL, + MODDED_STRUCTURE_COMMANDS_STRUCTURE_PIECES_FOOTPRINT_X_BLOCKS_SAMPLE_SEED_1234, + MODDED_STRUCTURE_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_STRUCTURE_IS, + MODDED_STRUCTURE_COMMANDS_NO_IRIS_STRUCTURE_THIS_PACK_2, + MODDED_STRUCTURE_COMMANDS_STRUCTURE_ASSEMBLED_0_PIECES, + MODDED_STRUCTURE_COMMANDS_PLACE_FAILED_PARTIAL_CHANGES_RECORDED_UNDO, + MODDED_STRUCTURE_COMMANDS_PLACED_PIECES_WRITE_S_AT_YOUR_LOCATION_IRIS_OBJECT_UNDO, + MODDED_STUDIO_COMMANDS_PROVIDE_DIMENSION_PACK_IRIS_STUDIO_OPEN_PACK_SEED, + MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_ON_SERVER_DISPLAY, + MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_RUN_IRIS_STUDIO, + MODDED_STUDIO_COMMANDS_UNKNOWN_GENERATOR_PACK, + MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED, + MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_STAND_IRIS_STUDIO, + MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, + MODDED_STUDIO_COMMANDS_FAILED_WRITE_WORKSPACE, + MODDED_STUDIO_COMMANDS_WORKSPACE_REGENERATED_WITH_JSON_SCHEMAS_AUTOCOMPLETE, + MODDED_STUDIO_COMMANDS_COULD_NOT_OPEN, + MODDED_STUDIO_COMMANDS_OPENING_YOUR_EDITOR, + MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_SPECIFY_PACK_NAME, + MODDED_STUDIO_COMMANDS_PACK_NOT_FOUND_UNDER, + MODDED_STUDIO_COMMANDS_PROVIDE_DIMENSION_PACK_IRIS_STUDIO_OPEN_PACK_SEED_2, + MODDED_STUDIO_COMMANDS_OPENING_STUDIO_SEED, + MODDED_STUDIO_COMMANDS_PACK_MISSING_DOWNLOADING_IRISDIMENSIONS, + MODDED_STUDIO_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_TRY_IRIS_DOWNLOAD, + MODDED_STUDIO_COMMANDS_PACK_HAS_NO_DIMENSIONS_JSON, + MODDED_STUDIO_COMMANDS_STUDIO_OPEN_FAILED, + MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED, + MODDED_STUDIO_COMMANDS_CONSOLE_STUDIO_OPEN_NOW_RUNS_SEED_TRANSIENT_NOT_WRITTEN_IRIS, + MODDED_STUDIO_COMMANDS_ENTER_IT_WITH_EXECUTE_RUN_TP_S_8_5_8, + MODDED_STUDIO_COMMANDS_PREGEN_IT_WITH_IRIS_PREGEN_START_RADIUS, + MODDED_STUDIO_COMMANDS_REMOVE_IT_WITH_IRIS_STUDIO_CLOSE, + MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED_2, + MODDED_STUDIO_COMMANDS_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN, + MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN, + MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED, + MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED, + MODDED_STUDIO_COMMANDS_NO_STUDIO_DIMENSIONS_ARE_CURRENTLY_OPEN, + MODDED_STUDIO_COMMANDS_ACTIVE_STUDIO_DIMENSION_S, + MODDED_STUDIO_COMMANDS_PACK_SEED_OWNER, + MODDED_STUDIO_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS, + MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN_2, + MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO, + MODDED_STUDIO_COMMANDS_TELEPORTED_YOUR_STUDIO, + MODDED_STUDIO_COMMANDS_PACK_HAS_NO_DIMENSIONS_JSON_2, + MODDED_STUDIO_COMMANDS_PACK_HAS_VERSION, + MODDED_STUDIO_COMMANDS_INVALID_PROJECT_NAME_ALLOWED_Z_0_9, + MODDED_STUDIO_COMMANDS_PACK_ALREADY_EXISTS_AT, + MODDED_STUDIO_COMMANDS_CREATING_PROJECT_FROM_TEMPLATE, + MODDED_STUDIO_COMMANDS_TEMPLATE_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, + MODDED_STUDIO_COMMANDS_TEMPLATE_COULD_NOT_BE_DOWNLOADED_INSTALL_PACK_WITH_DIMENSIONS_JSON, + MODDED_STUDIO_COMMANDS_CREATED_PROJECT_AT, + MODDED_STUDIO_COMMANDS_EDIT_DIMENSIONS_JSON_REST_PACK_VSCODE_WORKSPACE_WITH_JSON_SCHEMA, + MODDED_STUDIO_COMMANDS_PROJECT_CREATION_FAILED, + MODDED_STUDIO_COMMANDS_PACKAGING_DIMENSION, + MODDED_STUDIO_COMMANDS_PACKAGE_COMPILED, + MODDED_STUDIO_COMMANDS_PACKAGING_FAILED, + MODDED_STUDIO_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_SAMPLING_IS, + MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS, + MODDED_STUDIO_COMMANDS_SAMPLING_REGION_DISTRIBUTION_X_CHUNKS_AROUND_YOU, + MODDED_STUDIO_COMMANDS_RARITY, + MODDED_STUDIO_COMMANDS_REGION_SAMPLING_FAILED, + MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, + MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH, + MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_WORLD, + MODDED_WORLD_COMMANDS_CREATED_IRIS_WORLD_FROM_PACK_DIMENSION_SEED, + MODDED_WORLD_COMMANDS_IT_IS_LIVE_NOW_RE_INJECTED_ON_EVERY_STARTUP_TELEPORT, + MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_PRIMARY_WORLD, + MODDED_WORLD_COMMANDS_IRIS_PRIMARY_WORLD_SET_PACK_DIMENSION_SEED, + MODDED_WORLD_COMMANDS_VANILLA_OVERWORLD_GENERATOR_CANNOT_BE_HOT_SWAPPED_SO_THIS_DOES, + MODDED_WORLD_COMMANDS_INSTEAD_IS_NOW_CONFIGURED_PRIMARY_WORLD_PLAYERS_VANILLA_OVERWORLD_ARE, + MODDED_WORLD_COMMANDS_IRIS_MAIN_WORLD_OVERRIDE_CLEARED_OVERWORLD_KEEPS_ITS_CURRENT_GENERATOR, + MODDED_WORLD_COMMANDS_INVALID_SEED_USE_NUMBER_RANDOM, + MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS_2, + MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH_2, + MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, + MODDED_WORLD_COMMANDS_FAILED_WRITE_SERVER_PROPERTIES_CHECK_FILE_PERMISSIONS_SET_LEVEL_TYPE, + MODDED_WORLD_COMMANDS_IRIS_MAIN_WORLD_SET_PRESET_SEED, + MODDED_WORLD_COMMANDS_SERVER_PROPERTIES_LEVEL_TYPE_IS_NOW_ON_NEXT_RESTART_OVERWORLD, + MODDED_WORLD_COMMANDS_PLAYER_DATA_INVENTORIES_ADVANCEMENTS_STATS_IS_KEPT_EXISTING_TERRAIN_THOSE, + MODDED_WORLD_COMMANDS_MAINWORLDAUTORESTART_IS_ENABLED_STOPPING_SERVER_NOW_SO_YOUR_RESTART_WRAPPER, + MODDED_WORLD_COMMANDS_RESTART_SERVER_NOW_GENERATE_IT_SET_MAINWORLDAUTORESTART_TRUE_MODDED_JSON, + MODDED_WORLD_COMMANDS_REFUSING_CREATE_WORLD_USING_PACK_BECAUSE_REQUIRED_VALIDATION_FAILED, + MODDED_WORLD_COMMANDS_MESSAGE, + MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, + MODDED_WORLD_COMMANDS_PACK_WAS_NOT_FOUND_UNDER, + MODDED_WORLD_COMMANDS_PACK_DOES_NOT_CONTAIN_DIMENSIONS_JSON, + MODDED_WORLD_COMMANDS_INVALID_PACK_REFERENCE_USE_PACK_PACK_DIMENSIONKEY, + MODDED_WORLD_COMMANDS_INVALID_SEED_USE_NUMBER_RANDOM_2, + MODDED_WORLD_COMMANDS_FAILED_REMOVE_IRIS_WORLD, + MODDED_WORLD_COMMANDS_IRIS_WORLD_WAS_NOT_LOADED_CLEARED_ITS_PERSISTENT_REGISTRY_ENTRY, + MODDED_WORLD_COMMANDS_IRIS_WORLD_WAS_NOT_LOADED_CLEARED_ITS_PERSISTENT_REGISTRY_ENTRY_2, + MODDED_WORLD_COMMANDS_DELETED_IRIS_WORLD_EVACUATED_UNLOADED_CHUNK_MANTLE_DATA_WIPED_DROPPED, + MODDED_WORLD_COMMANDS_DISABLED_IRIS_WORLD_EVACUATED_UNLOADED_DROPPED_FROM_STARTUP_REGISTRY, + MODDED_WORLD_COMMANDS_WORLD_DATA_ON_DISK_IS_KEPT_RE_ENABLE_WITH_IRIS, + MODDED_WORLD_COMMANDS_LOADED_IRIS_LEVEL_PACK_DIMENSION, + MODDED_WORLD_COMMANDS_PRIMARY_WORLD, + MODDED_WORLD_COMMANDS_NO_IRIS_DIMENSIONS_ARE_CURRENTLY_LOADED_CREATE_ONE_WITH_IRIS, + MODDED_WORLD_COMMANDS_LOADED_IRIS_DIMENSIONS, + MODDED_WORLD_COMMANDS_MESSAGE_2, + MODDED_WORLD_COMMANDS_USE_IRIS_WORLD_CREATE_NAME_PACK_INJECT_ONE_WITHOUT_RESTARTING + ); + + private ModdedCommandMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java b/core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java new file mode 100644 index 000000000..6b2843015 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java @@ -0,0 +1,534 @@ +package art.arcane.iris.core.localization; + +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; + +public final class ModdedHelpMessages { + public static final TextKey COMMAND_VERSION_PRINT_VERSION_INFORMATION = TextKey.of( + "iris.modded.help.entry.command.version", + "Print version information" + ); + public static final TextKey COMMAND_INFO_LIST_LOADED_IRIS_DIMENSIONS_AND_PACK_DETAILS = TextKey.of( + "iris.modded.help.entry.command.info", + "List loaded Iris dimensions and pack details" + ); + public static final TextKey COMMAND_WHAT_INSPECT_THE_IRIS_BIOME_REGION_CAVE_BIOME_SURFACE_AND_CHUNK_AT_YOUR = TextKey.of( + "iris.modded.help.entry.command.what", + "Inspect the Iris biome, region, cave biome, surface and chunk at your position, the block you look at, your held item, or nearby markers" + ); + public static final TextKey GROUP_FIND_FIND_AND_TELEPORT_TO_IRIS_BIOMES_REGIONS_OBJECTS_IRIS_STRUCTURES_NATIVE_STRUCTURES = TextKey.of( + "iris.modded.help.entry.group.find", + "Find and teleport to Iris biomes, regions, objects, Iris structures, native structures and points of interest" + ); + public static final TextKey COMMAND_TP_TELEPORT_YOURSELF_OR_A_NAMED_PLAYER_INTO_A_LOADED_IRIS_DIMENSION = TextKey.of( + "iris.modded.help.entry.command.tp", + "Teleport yourself or a named player into a loaded Iris dimension" + ); + public static final TextKey COMMAND_EVACUATE_TELEPORT_EVERY_PLAYER_OUT_OF_AN_IRIS_DIMENSION_TO_THE_PRIMARY_WORLD = TextKey.of( + "iris.modded.help.entry.command.evacuate", + "Teleport every player out of an Iris dimension to the primary world spawn" + ); + public static final TextKey COMMAND_SEED_PRINT_WORLD_AND_ENGINE_SEED_INFORMATION = TextKey.of( + "iris.modded.help.entry.command.seed", + "Print world and engine seed information" + ); + public static final TextKey COMMAND_DEBUG_TOGGLE_IRIS_DEBUG_LOGGING_AND_SAVE_SETTINGS_JSON = TextKey.of( + "iris.modded.help.entry.command.debug", + "Toggle Iris debug logging and save settings.json" + ); + public static final TextKey COMMAND_RELOAD_RELOAD_SETTINGS_JSON_ALSO_HOTLOADED_AUTOMATICALLY_EVERY_3S = TextKey.of( + "iris.modded.help.entry.command.reload", + "Reload settings.json (also hotloaded automatically every 3s)" + ); + public static final TextKey COMMAND_DOWNLOAD_DOWNLOAD_A_PACK_PROJECT = TextKey.of( + "iris.modded.help.entry.command.download", + "Download a pack project" + ); + public static final TextKey COMMAND_METRICS_PRINT_GENERATION_METRICS_FOR_YOUR_CURRENT_IRIS_DIMENSION = TextKey.of( + "iris.modded.help.entry.command.metrics", + "Print generation metrics for your current Iris dimension" + ); + public static final TextKey COMMAND_REGEN_DELETE_AND_REGENERATE_NEARBY_CHUNKS_IN_PLACE = TextKey.of( + "iris.modded.help.entry.command.regen", + "Delete and regenerate nearby chunks in place" + ); + public static final TextKey GROUP_PREGEN_PREGENERATE_AN_IRIS_DIMENSION = TextKey.of( + "iris.modded.help.entry.group.pregen", + "Pregenerate an Iris dimension" + ); + public static final TextKey COMMAND_WAND_GET_AN_IRIS_OBJECT_WAND = TextKey.of( + "iris.modded.help.entry.command.wand", + "Get an Iris object wand" + ); + public static final TextKey GROUP_OBJECT_OBJECT_WAND_SAVE_PASTE_ANALYZE_AND_UNDO_TOOLS = TextKey.of( + "iris.modded.help.entry.group.object", + "Object wand, save, paste, analyze and undo tools" + ); + public static final TextKey GROUP_EDIT_OPEN_PACK_BIOME_REGION_AND_DIMENSION_JSON_FILES_IN_YOUR_DESKTOP_EDITOR = TextKey.of( + "iris.modded.help.entry.group.edit", + "Open pack biome, region and dimension json files in your desktop editor" + ); + public static final TextKey COMMAND_CREATE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_QUOTE_PACK_DIMENSIONKEY_TO_PICK = TextKey.of( + "iris.modded.help.entry.command.create", + "Create and inject a persistent Iris dimension; quote pack:dimensionKey to pick a specific pack dimension" + ); + public static final TextKey GROUP_STUDIO_PACK_PROJECT_CREATION_PACKAGING_AND_REPORTS = TextKey.of( + "iris.modded.help.entry.group.studio", + "Pack project creation, packaging and reports" + ); + public static final TextKey GROUP_PACK_PACK_VALIDATION_AND_MAINTENANCE = TextKey.of( + "iris.modded.help.entry.group.pack", + "Pack validation and maintenance" + ); + public static final TextKey GROUP_WORLD_RUNTIME_IRIS_DIMENSION_CREATION_REMOVAL_AND_STATUS = TextKey.of( + "iris.modded.help.entry.group.world", + "Runtime Iris dimension creation, removal and status" + ); + public static final TextKey GROUP_DATAPACK_WORLD_DATAPACK_INSTALL_AND_STATUS_HELPERS = TextKey.of( + "iris.modded.help.entry.group.datapack", + "World datapack install and status helpers" + ); + public static final TextKey GROUP_STRUCTURE_IRIS_STRUCTURE_INDEX_INFO_AND_PLACEMENT_TOOLS = TextKey.of( + "iris.modded.help.entry.group.structure", + "Iris structure index, info and placement tools" + ); + public static final TextKey COMMAND_GOLDENHASH_GENERATE_DETERMINISTIC_BLOCK_HASHES_FOR_PARITY_TESTING = TextKey.of( + "iris.modded.help.entry.command.goldenhash", + "Generate deterministic block hashes for parity testing" + ); + public static final TextKey GROUP_DEVELOPER_DEVELOPER_DIAGNOSTICS_SENTRY_TEST_NETWORK_INTERFACES_REGION_FILE_SCAN = TextKey.of( + "iris.modded.help.entry.group.developer", + "Developer diagnostics: Sentry test, network interfaces, region-file scan" + ); + public static final TextKey COMMAND_BIOME_FIND_AN_IRIS_BIOME = TextKey.of( + "iris.modded.help.entry.command.biome", + "Find an Iris biome" + ); + public static final TextKey COMMAND_REGION_FIND_AN_IRIS_REGION = TextKey.of( + "iris.modded.help.entry.command.region", + "Find an Iris region" + ); + public static final TextKey COMMAND_OBJECT_FIND_AN_OBJECT_PLACEMENT = TextKey.of( + "iris.modded.help.entry.command.object", + "Find an object placement" + ); + public static final TextKey COMMAND_STRUCTURE_FIND_AN_IRIS_PLACED_OR_NATIVE_DATAPACK_STRUCTURE = TextKey.of( + "iris.modded.help.entry.command.structure", + "Find an Iris-placed or native/datapack structure" + ); + public static final TextKey COMMAND_POI_FIND_A_SUPPORTED_POINT_OF_INTEREST = TextKey.of( + "iris.modded.help.entry.command.poi", + "Find a supported point of interest" + ); + public static final TextKey COMMAND_BIOME_OPEN_A_BIOME_JSON_IN_YOUR_DESKTOP_EDITOR_NO_KEY_OPENS_THE = TextKey.of( + "iris.modded.help.entry.command.biome_2", + "Open a biome json in your desktop editor; no key opens the biome at your position" + ); + public static final TextKey COMMAND_REGION_OPEN_A_REGION_JSON_IN_YOUR_DESKTOP_EDITOR_NO_KEY_OPENS_THE = TextKey.of( + "iris.modded.help.entry.command.region_2", + "Open a region json in your desktop editor; no key opens the region at your position" + ); + public static final TextKey COMMAND_DIMENSION_OPEN_THE_CURRENT_PACK_S_DIMENSION_JSON_IN_YOUR_DESKTOP_EDITOR = TextKey.of( + "iris.modded.help.entry.command.dimension", + "Open the current pack's dimension json in your desktop editor" + ); + public static final TextKey COMMAND_START_START_PREGENERATION_RADIUS_IN_BLOCKS_RESUMABLE_CHECKPOINT_CACHE_ON_BY_DEFAULT_CENTER = TextKey.of( + "iris.modded.help.entry.command.start", + "Start pregeneration; radius in blocks, resumable checkpoint cache on by default, center via 'at ', flags compose in any order" + ); + public static final TextKey COMMAND_STOP_STOP_THE_ACTIVE_PREGENERATION_TASK = TextKey.of( + "iris.modded.help.entry.command.stop", + "Stop the active pregeneration task" + ); + public static final TextKey COMMAND_PAUSE_PAUSE_OR_RESUME_PREGENERATION = TextKey.of( + "iris.modded.help.entry.command.pause", + "Pause or resume pregeneration" + ); + public static final TextKey COMMAND_STATUS_SHOW_PREGENERATION_STATUS = TextKey.of( + "iris.modded.help.entry.command.status", + "Show pregeneration status" + ); + public static final TextKey COMMAND_WAND_GET_AN_IRIS_OBJECT_WAND_2 = TextKey.of( + "iris.modded.help.entry.command.wand_3", + "Get an Iris object wand" + ); + public static final TextKey COMMAND_DUST_GET_DUST_THAT_REVEALS_OBJECT_PLACEMENTS = TextKey.of( + "iris.modded.help.entry.command.dust", + "Get dust that reveals object placements" + ); + public static final TextKey COMMAND_SAVE_SAVE_THE_SELECTED_WAND_VOLUME_AS_AN_OBJECT = TextKey.of( + "iris.modded.help.entry.command.save", + "Save the selected wand volume as an object" + ); + public static final TextKey COMMAND_PASTE_PASTE_AN_OBJECT_AT_YOUR_POSITION_OR_A_GIVEN_POSITION_OPTIONALLY_ROTATED = TextKey.of( + "iris.modded.help.entry.command.paste", + "Paste an object at your position or a given position, optionally rotated" + ); + public static final TextKey COMMAND_EXPAND_EXPAND_THE_WAND_SELECTION_IN_YOUR_LOOKING_DIRECTION = TextKey.of( + "iris.modded.help.entry.command.expand", + "Expand the wand selection in your looking direction" + ); + public static final TextKey COMMAND_CONTRACT_CONTRACT_THE_WAND_SELECTION_IN_YOUR_LOOKING_DIRECTION = TextKey.of( + "iris.modded.help.entry.command.contract", + "Contract the wand selection in your looking direction" + ); + public static final TextKey COMMAND_SHIFT_SHIFT_THE_WAND_SELECTION_IN_YOUR_LOOKING_DIRECTION = TextKey.of( + "iris.modded.help.entry.command.shift", + "Shift the wand selection in your looking direction" + ); + public static final TextKey COMMAND_POSITION1_SET_SELECTION_POINT_1 = TextKey.of( + "iris.modded.help.entry.command.position1", + "Set selection point 1" + ); + public static final TextKey COMMAND_POSITION2_SET_SELECTION_POINT_2 = TextKey.of( + "iris.modded.help.entry.command.position2", + "Set selection point 2" + ); + public static final TextKey COMMAND_X_Y_AUTOSELECT_UP_AND_OUT = TextKey.of( + "iris.modded.help.entry.command.x_y", + "Autoselect up and out" + ); + public static final TextKey COMMAND_X_Y_AUTOSELECT_UP_DOWN_AND_OUT = TextKey.of( + "iris.modded.help.entry.command.x_y_2", + "Autoselect up, down and out" + ); + public static final TextKey COMMAND_ANALYZE_SHOW_OBJECT_COMPOSITION = TextKey.of( + "iris.modded.help.entry.command.analyze", + "Show object composition" + ); + public static final TextKey COMMAND_SHRINK_SHRINK_AN_OBJECT_TO_ITS_MINIMUM_SIZE = TextKey.of( + "iris.modded.help.entry.command.shrink", + "Shrink an object to its minimum size" + ); + public static final TextKey COMMAND_PLAUSIBILIZE_GROW_BRANCHES_SO_TREE_LEAVES_SURVIVE_VANILLA_DECAY = TextKey.of( + "iris.modded.help.entry.command.plausibilize", + "Grow branches so tree leaves survive vanilla decay" + ); + public static final TextKey COMMAND_UNDO_UNDO_PASTED_OBJECTS = TextKey.of( + "iris.modded.help.entry.command.undo", + "Undo pasted objects" + ); + public static final TextKey COMMAND_CREATE_CREATE_A_NEW_PACK_PROJECT = TextKey.of( + "iris.modded.help.entry.command.create_2", + "Create a new pack project" + ); + public static final TextKey COMMAND_PACKAGE_PACKAGE_A_DIMENSION_INTO_A_COMPRESSED_FORMAT = TextKey.of( + "iris.modded.help.entry.command.package", + "Package a dimension into a compressed format" + ); + public static final TextKey COMMAND_VERSION_PRINT_A_PACK_VERSION = TextKey.of( + "iris.modded.help.entry.command.version_2", + "Print a pack version" + ); + public static final TextKey COMMAND_REGIONS_CALCULATE_NEARBY_REGION_DISTRIBUTION = TextKey.of( + "iris.modded.help.entry.command.regions", + "Calculate nearby region distribution" + ); + public static final TextKey COMMAND_OPEN_OPEN_A_TEMPORARY_STUDIO_DIMENSION_FOR_A_PACK = TextKey.of( + "iris.modded.help.entry.command.open", + "Open a temporary studio dimension for a pack" + ); + public static final TextKey COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD = TextKey.of( + "iris.modded.help.entry.command.close", + "Close the open studio dimension and discard its world" + ); + public static final TextKey COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION = TextKey.of( + "iris.modded.help.entry.command.tpstudio", + "Teleport into the open studio dimension" + ); + public static final TextKey COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK = TextKey.of( + "iris.modded.help.entry.command.status_studio", + "Show the open studio dimension and its pack" + ); + public static final TextKey COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY = TextKey.of( + "iris.modded.help.entry.command.noise", + "Open the Noise Explorer GUI on the server display" + ); + public static final TextKey COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY = TextKey.of( + "iris.modded.help.entry.command.map", + "Open the Vision map GUI on the server display" + ); + public static final TextKey COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR = TextKey.of( + "iris.modded.help.entry.command.vscode", + "Regenerate the .code-workspace for a pack and open it in your desktop editor" + ); + public static final TextKey COMMAND_UPDATE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK = TextKey.of( + "iris.modded.help.entry.command.update", + "Regenerate the .code-workspace for a pack" + ); + public static final TextKey COMMAND_IMPORTVANILLA_EXPLAIN_VANILLA_IMPORT_WORKFLOW = TextKey.of( + "iris.modded.help.entry.command.importvanilla", + "Explain vanilla import workflow" + ); + public static final TextKey COMMAND_VALIDATE_VALIDATE_A_PACK_OR_EVERY_PACK = TextKey.of( + "iris.modded.help.entry.command.validate", + "Validate a pack or every pack" + ); + public static final TextKey COMMAND_CLEANUP_PREVIEW_OR_QUARANTINE_UNUSED_RESOURCE_CANDIDATES = TextKey.of( + "iris.modded.help.entry.command.cleanup", + "Preview or quarantine unused-resource candidates" + ); + public static final TextKey COMMAND_RESTORE_PREVIEW_OR_RESTORE_THE_LATEST_QUARANTINE = TextKey.of( + "iris.modded.help.entry.command.restore", + "Preview or restore the latest quarantine" + ); + public static final TextKey COMMAND_STATUS_SHOW_CACHED_VALIDATION_STATUS = TextKey.of( + "iris.modded.help.entry.command.status_validation", + "Show cached validation status" + ); + public static final TextKey COMMAND_ENABLE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_AT_RUNTIME_DOWNLOADS_THE_PACK = TextKey.of( + "iris.modded.help.entry.command.enable", + "Create and inject a persistent Iris dimension at runtime; refuses if the pack is missing, quote pack:dimensionKey to pick a specific pack dimension" + ); + public static final TextKey COMMAND_REPLACE_OVERWORLD_INJECT_AN_IRIS_PRIMARY_WORLD_AND_ROUTE_PLAYERS_THERE_INSTEAD_OF_THE = TextKey.of( + "iris.modded.help.entry.command.replace_overworld", + "Inject an Iris primary world and route players there instead of the vanilla overworld" + ); + public static final TextKey COMMAND_DISABLE_EVACUATE_AND_UNLOAD_AN_IRIS_DIMENSION_WORLD_DATA_ON_DISK_IS_KEPT = TextKey.of( + "iris.modded.help.entry.command.disable", + "Evacuate and unload an Iris dimension; world data on disk is kept for re-enabling" + ); + public static final TextKey COMMAND_DELETE_DISABLE_AN_IRIS_DIMENSION_AND_WIPE_ITS_CHUNK_AND_MANTLE_DATA_FROM = TextKey.of( + "iris.modded.help.entry.command.delete", + "Disable an Iris dimension and wipe its chunk and mantle data from disk" + ); + public static final TextKey COMMAND_LIST_LIST_LOADED_IRIS_DIMENSIONS = TextKey.of( + "iris.modded.help.entry.command.list", + "List loaded Iris dimensions" + ); + public static final TextKey COMMAND_STATUS_SHOW_LOADED_IRIS_DIMENSIONS_AND_THE_CONFIGURED_PRIMARY_WORLD = TextKey.of( + "iris.modded.help.entry.command.status_world", + "Show loaded Iris dimensions and the configured primary world" + ); + public static final TextKey COMMAND_STATUS_CHECK_LOADED_IRIS_DIMENSION_TYPE_OVERRIDES = TextKey.of( + "iris.modded.help.entry.command.status_datapack", + "Check loaded Iris dimension type overrides" + ); + public static final TextKey COMMAND_INSTALL_INSTALL_DIMENSION_TYPE_OVERRIDES_FOR_LOADED_IRIS_DIMENSIONS = TextKey.of( + "iris.modded.help.entry.command.install", + "Install dimension type overrides for loaded Iris dimensions" + ); + public static final TextKey COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS = TextKey.of( + "iris.modded.help.entry.command.list_datapacks", + "List configured and installed datapacks" + ); + public static final TextKey COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW = TextKey.of( + "iris.modded.help.entry.command.ingest", + "Explain Bukkit datapack ingest workflow" + ); + public static final TextKey COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW = TextKey.of( + "iris.modded.help.entry.command.remove", + "Explain datapack removal workflow" + ); + public static final TextKey COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON = TextKey.of( + "iris.modded.help.entry.command.list_structures", + "Regenerate structure-index.json" + ); + public static final TextKey COMMAND_INFO_RESOLVE_AN_IRIS_STRUCTURE_GRAPH_AND_REPORT_BOUNDS = TextKey.of( + "iris.modded.help.entry.command.info_2", + "Resolve an Iris structure graph and report bounds" + ); + public static final TextKey COMMAND_PLACE_ASSEMBLE_AND_PLACE_AN_IRIS_STRUCTURE_AT_YOUR_LOCATION = TextKey.of( + "iris.modded.help.entry.command.place", + "Assemble and place an Iris structure at your location" + ); + public static final TextKey COMMAND_IMPORT_EXPLAIN_BUKKIT_STRUCTURE_IMPORT_WORKFLOW = TextKey.of( + "iris.modded.help.entry.command.import", + "Explain Bukkit structure import workflow" + ); + public static final TextKey COMMAND_CAPTURE_EXPLAIN_BUKKIT_STRUCTURE_CAPTURE_WORKFLOW = TextKey.of( + "iris.modded.help.entry.command.capture", + "Explain Bukkit structure capture workflow" + ); + public static final TextKey COMMAND_VERIFY_REPORT_NATIVE_AND_IRIS_STRUCTURE_REACHABILITY_IN_THE_CURRENT_DIMENSION = TextKey.of( + "iris.modded.help.entry.command.verify", + "Report native and Iris structure reachability in the current dimension" + ); + public static final TextKey COMMAND_SENTRY_SEND_A_TEST_EXCEPTION_TO_THE_IRIS_ERROR_REPORTER = TextKey.of( + "iris.modded.help.entry.command.sentry", + "Send a test exception to the Iris error reporter" + ); + public static final TextKey COMMAND_NETWORK_LIST_NETWORK_INTERFACES_AND_THEIR_ADDRESSES = TextKey.of( + "iris.modded.help.entry.command.network", + "List network interfaces and their addresses" + ); + public static final TextKey COMMAND_WHAT_HERE_INSPECT_CURRENT_IRIS_CONTEXT = TextKey.of( + "iris.modded.help.entry.command.what.here", + "Inspect the Iris biome, region, cave biome, surface and chunk at your position" + ); + public static final TextKey COMMAND_WHAT_BIOME_INSPECT_CURRENT_BIOME = TextKey.of( + "iris.modded.help.entry.command.what.biome", + "Inspect the Iris biome, configured derivative and registered native biome" + ); + public static final TextKey COMMAND_WHAT_REGION_INSPECT_CURRENT_REGION = TextKey.of( + "iris.modded.help.entry.command.what.region", + "Inspect the Iris region at your current chunk" + ); + public static final TextKey COMMAND_WHAT_BLOCK_INSPECT_TARGET_BLOCK = TextKey.of( + "iris.modded.help.entry.command.what.block", + "Inspect the targeted block state, properties, object and block entity" + ); + public static final TextKey COMMAND_WHAT_HAND_INSPECT_HELD_ITEM = TextKey.of( + "iris.modded.help.entry.command.what.hand", + "Inspect the held item and its default block state" + ); + public static final TextKey COMMAND_WHAT_MARKERS_REVEAL_NEARBY_MARKERS = TextKey.of( + "iris.modded.help.entry.command.what.markers", + "Reveal nearby Iris Mantle markers with particles" + ); + public static final TextKey COMMAND_HEIGHT_PRINT_WORLD_HEIGHT = TextKey.of( + "iris.modded.help.entry.command.height", + "Print the current dimension height range" + ); + public static final TextKey COMMAND_WORLDS_LIST_WORLD_ACCESS = TextKey.of( + "iris.modded.help.entry.command.worlds", + "List loaded dimensions and identify the Iris-generated dimensions" + ); + public static final TextKey COMMAND_MAINWORLD_CONFIGURE_PRIMARY_WORLD_PRESET = TextKey.of( + "iris.modded.help.entry.command.mainworld", + "Configure or clear the Iris primary-world preset used on restart" + ); + public static final TextKey COMMAND_OBJECT_WE_BUKKIT_ONLY = TextKey.of( + "iris.modded.help.entry.command.object.we", + "Explain why WorldEdit selection import requires the Bukkit plugin" + ); + public static final TextKey COMMAND_OBJECT_STUDIO_BUKKIT_ONLY = TextKey.of( + "iris.modded.help.entry.command.object.studio", + "Explain why the object studio world requires the Bukkit toolchain" + ); + public static final TextKey COMMAND_OBJECT_CONVERT_BUKKIT_ONLY = TextKey.of( + "iris.modded.help.entry.command.object.convert", + "Explain why schematic conversion requires the Bukkit plugin" + ); + public static final TextKey COMMAND_STUDIO_LOOT_BUKKIT_ONLY = TextKey.of( + "iris.modded.help.entry.command.studio.loot", + "Explain why the Bukkit loot simulation GUI is unavailable" + ); + public static final TextKey COMMAND_STUDIO_PROFILE_BUKKIT_ONLY = TextKey.of( + "iris.modded.help.entry.command.studio.profile", + "Explain why Bukkit pack profiling is unavailable" + ); + public static final TextKey COMMAND_STUDIO_SPAWN_BUKKIT_ONLY = TextKey.of( + "iris.modded.help.entry.command.studio.spawn", + "Explain why Bukkit Iris entity spawning is unavailable" + ); + public static final TextKey COMMAND_STUDIO_OBJECTS_BUKKIT_ONLY = TextKey.of( + "iris.modded.help.entry.command.studio.objects", + "Explain why the Bukkit chunk object report is unavailable" + ); + + private static final List KEYS = List.of( + COMMAND_VERSION_PRINT_VERSION_INFORMATION, + COMMAND_INFO_LIST_LOADED_IRIS_DIMENSIONS_AND_PACK_DETAILS, + COMMAND_WHAT_INSPECT_THE_IRIS_BIOME_REGION_CAVE_BIOME_SURFACE_AND_CHUNK_AT_YOUR, + GROUP_FIND_FIND_AND_TELEPORT_TO_IRIS_BIOMES_REGIONS_OBJECTS_IRIS_STRUCTURES_NATIVE_STRUCTURES, + COMMAND_TP_TELEPORT_YOURSELF_OR_A_NAMED_PLAYER_INTO_A_LOADED_IRIS_DIMENSION, + COMMAND_EVACUATE_TELEPORT_EVERY_PLAYER_OUT_OF_AN_IRIS_DIMENSION_TO_THE_PRIMARY_WORLD, + COMMAND_SEED_PRINT_WORLD_AND_ENGINE_SEED_INFORMATION, + COMMAND_DEBUG_TOGGLE_IRIS_DEBUG_LOGGING_AND_SAVE_SETTINGS_JSON, + COMMAND_RELOAD_RELOAD_SETTINGS_JSON_ALSO_HOTLOADED_AUTOMATICALLY_EVERY_3S, + COMMAND_DOWNLOAD_DOWNLOAD_A_PACK_PROJECT, + COMMAND_METRICS_PRINT_GENERATION_METRICS_FOR_YOUR_CURRENT_IRIS_DIMENSION, + COMMAND_REGEN_DELETE_AND_REGENERATE_NEARBY_CHUNKS_IN_PLACE, + GROUP_PREGEN_PREGENERATE_AN_IRIS_DIMENSION, + COMMAND_WAND_GET_AN_IRIS_OBJECT_WAND, + GROUP_OBJECT_OBJECT_WAND_SAVE_PASTE_ANALYZE_AND_UNDO_TOOLS, + GROUP_EDIT_OPEN_PACK_BIOME_REGION_AND_DIMENSION_JSON_FILES_IN_YOUR_DESKTOP_EDITOR, + COMMAND_CREATE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_QUOTE_PACK_DIMENSIONKEY_TO_PICK, + GROUP_STUDIO_PACK_PROJECT_CREATION_PACKAGING_AND_REPORTS, + GROUP_PACK_PACK_VALIDATION_AND_MAINTENANCE, + GROUP_WORLD_RUNTIME_IRIS_DIMENSION_CREATION_REMOVAL_AND_STATUS, + GROUP_DATAPACK_WORLD_DATAPACK_INSTALL_AND_STATUS_HELPERS, + GROUP_STRUCTURE_IRIS_STRUCTURE_INDEX_INFO_AND_PLACEMENT_TOOLS, + COMMAND_GOLDENHASH_GENERATE_DETERMINISTIC_BLOCK_HASHES_FOR_PARITY_TESTING, + GROUP_DEVELOPER_DEVELOPER_DIAGNOSTICS_SENTRY_TEST_NETWORK_INTERFACES_REGION_FILE_SCAN, + COMMAND_BIOME_FIND_AN_IRIS_BIOME, + COMMAND_REGION_FIND_AN_IRIS_REGION, + COMMAND_OBJECT_FIND_AN_OBJECT_PLACEMENT, + COMMAND_STRUCTURE_FIND_AN_IRIS_PLACED_OR_NATIVE_DATAPACK_STRUCTURE, + COMMAND_POI_FIND_A_SUPPORTED_POINT_OF_INTEREST, + COMMAND_BIOME_OPEN_A_BIOME_JSON_IN_YOUR_DESKTOP_EDITOR_NO_KEY_OPENS_THE, + COMMAND_REGION_OPEN_A_REGION_JSON_IN_YOUR_DESKTOP_EDITOR_NO_KEY_OPENS_THE, + COMMAND_DIMENSION_OPEN_THE_CURRENT_PACK_S_DIMENSION_JSON_IN_YOUR_DESKTOP_EDITOR, + COMMAND_START_START_PREGENERATION_RADIUS_IN_BLOCKS_RESUMABLE_CHECKPOINT_CACHE_ON_BY_DEFAULT_CENTER, + COMMAND_STOP_STOP_THE_ACTIVE_PREGENERATION_TASK, + COMMAND_PAUSE_PAUSE_OR_RESUME_PREGENERATION, + COMMAND_STATUS_SHOW_PREGENERATION_STATUS, + COMMAND_WAND_GET_AN_IRIS_OBJECT_WAND_2, + COMMAND_DUST_GET_DUST_THAT_REVEALS_OBJECT_PLACEMENTS, + COMMAND_SAVE_SAVE_THE_SELECTED_WAND_VOLUME_AS_AN_OBJECT, + COMMAND_PASTE_PASTE_AN_OBJECT_AT_YOUR_POSITION_OR_A_GIVEN_POSITION_OPTIONALLY_ROTATED, + COMMAND_EXPAND_EXPAND_THE_WAND_SELECTION_IN_YOUR_LOOKING_DIRECTION, + COMMAND_CONTRACT_CONTRACT_THE_WAND_SELECTION_IN_YOUR_LOOKING_DIRECTION, + COMMAND_SHIFT_SHIFT_THE_WAND_SELECTION_IN_YOUR_LOOKING_DIRECTION, + COMMAND_POSITION1_SET_SELECTION_POINT_1, + COMMAND_POSITION2_SET_SELECTION_POINT_2, + COMMAND_X_Y_AUTOSELECT_UP_AND_OUT, + COMMAND_X_Y_AUTOSELECT_UP_DOWN_AND_OUT, + COMMAND_ANALYZE_SHOW_OBJECT_COMPOSITION, + COMMAND_SHRINK_SHRINK_AN_OBJECT_TO_ITS_MINIMUM_SIZE, + COMMAND_PLAUSIBILIZE_GROW_BRANCHES_SO_TREE_LEAVES_SURVIVE_VANILLA_DECAY, + COMMAND_UNDO_UNDO_PASTED_OBJECTS, + COMMAND_CREATE_CREATE_A_NEW_PACK_PROJECT, + COMMAND_PACKAGE_PACKAGE_A_DIMENSION_INTO_A_COMPRESSED_FORMAT, + COMMAND_VERSION_PRINT_A_PACK_VERSION, + COMMAND_REGIONS_CALCULATE_NEARBY_REGION_DISTRIBUTION, + COMMAND_OPEN_OPEN_A_TEMPORARY_STUDIO_DIMENSION_FOR_A_PACK, + COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD, + COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION, + COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK, + COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY, + COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY, + COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR, + COMMAND_UPDATE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK, + COMMAND_IMPORTVANILLA_EXPLAIN_VANILLA_IMPORT_WORKFLOW, + COMMAND_VALIDATE_VALIDATE_A_PACK_OR_EVERY_PACK, + COMMAND_CLEANUP_PREVIEW_OR_QUARANTINE_UNUSED_RESOURCE_CANDIDATES, + COMMAND_RESTORE_PREVIEW_OR_RESTORE_THE_LATEST_QUARANTINE, + COMMAND_STATUS_SHOW_CACHED_VALIDATION_STATUS, + COMMAND_ENABLE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_AT_RUNTIME_DOWNLOADS_THE_PACK, + COMMAND_REPLACE_OVERWORLD_INJECT_AN_IRIS_PRIMARY_WORLD_AND_ROUTE_PLAYERS_THERE_INSTEAD_OF_THE, + COMMAND_DISABLE_EVACUATE_AND_UNLOAD_AN_IRIS_DIMENSION_WORLD_DATA_ON_DISK_IS_KEPT, + COMMAND_DELETE_DISABLE_AN_IRIS_DIMENSION_AND_WIPE_ITS_CHUNK_AND_MANTLE_DATA_FROM, + COMMAND_LIST_LIST_LOADED_IRIS_DIMENSIONS, + COMMAND_STATUS_SHOW_LOADED_IRIS_DIMENSIONS_AND_THE_CONFIGURED_PRIMARY_WORLD, + COMMAND_STATUS_CHECK_LOADED_IRIS_DIMENSION_TYPE_OVERRIDES, + COMMAND_INSTALL_INSTALL_DIMENSION_TYPE_OVERRIDES_FOR_LOADED_IRIS_DIMENSIONS, + COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS, + COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW, + COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW, + COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON, + COMMAND_INFO_RESOLVE_AN_IRIS_STRUCTURE_GRAPH_AND_REPORT_BOUNDS, + COMMAND_PLACE_ASSEMBLE_AND_PLACE_AN_IRIS_STRUCTURE_AT_YOUR_LOCATION, + COMMAND_IMPORT_EXPLAIN_BUKKIT_STRUCTURE_IMPORT_WORKFLOW, + COMMAND_CAPTURE_EXPLAIN_BUKKIT_STRUCTURE_CAPTURE_WORKFLOW, + COMMAND_VERIFY_REPORT_NATIVE_AND_IRIS_STRUCTURE_REACHABILITY_IN_THE_CURRENT_DIMENSION, + COMMAND_SENTRY_SEND_A_TEST_EXCEPTION_TO_THE_IRIS_ERROR_REPORTER, + COMMAND_NETWORK_LIST_NETWORK_INTERFACES_AND_THEIR_ADDRESSES, + COMMAND_WHAT_HERE_INSPECT_CURRENT_IRIS_CONTEXT, + COMMAND_WHAT_BIOME_INSPECT_CURRENT_BIOME, + COMMAND_WHAT_REGION_INSPECT_CURRENT_REGION, + COMMAND_WHAT_BLOCK_INSPECT_TARGET_BLOCK, + COMMAND_WHAT_HAND_INSPECT_HELD_ITEM, + COMMAND_WHAT_MARKERS_REVEAL_NEARBY_MARKERS, + COMMAND_HEIGHT_PRINT_WORLD_HEIGHT, + COMMAND_WORLDS_LIST_WORLD_ACCESS, + COMMAND_MAINWORLD_CONFIGURE_PRIMARY_WORLD_PRESET, + COMMAND_OBJECT_WE_BUKKIT_ONLY, + COMMAND_OBJECT_STUDIO_BUKKIT_ONLY, + COMMAND_OBJECT_CONVERT_BUKKIT_ONLY, + COMMAND_STUDIO_LOOT_BUKKIT_ONLY, + COMMAND_STUDIO_PROFILE_BUKKIT_ONLY, + COMMAND_STUDIO_SPAWN_BUKKIT_ONLY, + COMMAND_STUDIO_OBJECTS_BUKKIT_ONLY + ); + + private ModdedHelpMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/PackDownloadMessages.java b/core/src/main/java/art/arcane/iris/core/localization/PackDownloadMessages.java new file mode 100644 index 000000000..ab6d985a3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/PackDownloadMessages.java @@ -0,0 +1,243 @@ +package art.arcane.iris.core.localization; + +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.localization.LinesKey; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.PluralKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; +import java.util.Map; + +public final class PackDownloadMessages { + public static final TextKey PROGRESS_START = TextKey.of( + "iris.runtime.pack_download.progress.start", + C.IRIS + "Iris " + C.GOLD + "PACK DOWNLOAD" + C.DARK_GRAY + " | " + C.WHITE + "{source}" + ); + public static final TextKey PROGRESS_PHASE = TextKey.of( + "iris.runtime.pack_download.progress.phase", + C.IRIS + "Iris " + C.AQUA + "{phase}" + C.DARK_GRAY + " | " + C.GRAY + "{source}" + ); + public static final TextKey PROGRESS_DETERMINATE = TextKey.of( + "iris.runtime.pack_download.progress.determinate", + "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.DARK_GRAY + " | " + + C.WHITE + "{transferred}" + C.GRAY + "/" + C.WHITE + "{total}" + + C.DARK_GRAY + " | " + C.AQUA + "{rate}/s" + ); + public static final TextKey PROGRESS_INDETERMINATE = TextKey.of( + "iris.runtime.pack_download.progress.indeterminate", + "{bar}" + C.GRAY + " " + C.AQUA + "{phase}" + C.DARK_GRAY + " | " + + C.WHITE + "{transferred}" + C.DARK_GRAY + " | " + C.AQUA + "{rate}/s" + ); + public static final TextKey PROGRESS_DETAIL = TextKey.of( + "iris.runtime.pack_download.progress.detail", + C.DARK_GRAY + " - " + C.GRAY + "{detail}" + ); + public static final TextKey PROGRESS_COMPLETE = TextKey.of( + "iris.runtime.pack_download.progress.complete", + C.GREEN + "Iris pack '{pack}' installed" + C.DARK_GRAY + " | " + + C.WHITE + "{transferred}" + C.GRAY + " in " + C.WHITE + "{elapsed}" + ); + public static final TextKey PROGRESS_UNCHANGED = TextKey.of( + "iris.runtime.pack_download.progress.unchanged", + C.YELLOW + "Iris pack '{pack}' is already installed." + ); + public static final TextKey PROGRESS_FAILED = TextKey.of( + "iris.runtime.pack_download.progress.failed", + C.RED + "Iris pack download failed." + C.GRAY + " Review the download details above and retry." + ); + public static final TextKey PROGRESS_FAILED_DETAIL = TextKey.of( + "iris.runtime.pack_download.progress.failed_detail", + C.RED + "Iris pack download failed." + C.GRAY + " {error}" + ); + public static final TextKey PROGRESS_CANCELLED = TextKey.of( + "iris.runtime.pack_download.progress.cancelled", + C.YELLOW + "Iris pack download cancelled before publication." + ); + public static final TextKey PROGRESS_RESTART = TextKey.of( + "iris.runtime.pack_download.progress.restart", + C.GOLD + "Restart required" + C.DARK_GRAY + " | " + + C.GRAY + "Restart the server before creating or replacing a world with this pack." + ); + public static final TextKey PROGRESS_PHASE_CONNECTING = TextKey.of( + "iris.runtime.pack_download.progress.phase.connecting", + "Connecting" + ); + public static final TextKey PROGRESS_PHASE_DOWNLOADING = TextKey.of( + "iris.runtime.pack_download.progress.phase.downloading", + "Downloading" + ); + public static final TextKey PROGRESS_PHASE_UNPACKING = TextKey.of( + "iris.runtime.pack_download.progress.phase.unpacking", + "Unpacking" + ); + public static final TextKey PROGRESS_PHASE_VALIDATING = TextKey.of( + "iris.runtime.pack_download.progress.phase.validating", + "Validating" + ); + public static final TextKey PROGRESS_PHASE_PUBLISHING = TextKey.of( + "iris.runtime.pack_download.progress.phase.publishing", + "Publishing" + ); + public static final TextKey PROGRESS_SOURCE_REMOTE = TextKey.of( + "iris.runtime.pack_download.progress.source.remote", + "Remote ZIP" + ); + public static final TextKey INVALID_SOURCE = TextKey.of( + "iris.runtime.pack_download.invalid_source", + C.RED + "Choose exactly one source: /iris download pack=overworld, " + + "/iris download pack=underworld, or /iris download link=‹zip-url›." + ); + public static final TextKey INVALID_URL = TextKey.of( + "iris.runtime.pack_download.invalid_url", + C.RED + "Iris requires a valid HTTP or HTTPS .zip URL." + ); + public static final TextKey INVALID_BUILT_IN = TextKey.of( + "iris.runtime.pack_download.invalid_built_in", + C.RED + "Iris only provides built-in downloads for 'overworld' and 'underworld'." + ); + public static final TextKey SHUTTING_DOWN = TextKey.of( + "iris.runtime.pack_download.shutting_down", + C.YELLOW + "Iris is shutting down and is not accepting pack downloads." + ); + public static final TextKey DOWNLOADING = TextKey.of( + "iris.runtime.pack_download.downloading", + "Downloading {url}" + ); + public static final TextKey FAILED_TO_FIND = TextKey.of( + "iris.runtime.pack_download.failed_to_find", + "Failed to find pack at {url}" + ); + public static final TextKey UNPACKING = TextKey.of( + "iris.runtime.pack_download.unpacking", + "Unpacking {repository}" + ); + public static final LinesKey UNPACK_FAILED = LinesKey.of( + "iris.runtime.pack_download.unpack_failed", + "Issue when unpacking. Please check/do the following:", + "1. Do you have a functioning internet connection?", + "2. Did the download corrupt?", + "3. Try deleting the */plugins/iris/packs folder and re-download.", + "4. Download the pack from the GitHub repo: https://github.com/IrisDimensions/overworld", + "5. Contact support (if all other options do not help)" + ); + public static final TextKey NO_EXTRACTED_FILES = TextKey.of( + "iris.runtime.pack_download.no_extracted_files", + "No files were extracted from the zip file." + ); + public static final TextKey HOME_DIRECTORY_ERROR = TextKey.of( + "iris.runtime.pack_download.home_directory_error", + "Error when finding home directory. Are there any non-text characters in the file name?" + ); + public static final TextKey INVALID_ARCHIVE_FORMAT = TextKey.of( + "iris.runtime.pack_download.invalid_archive_format", + "Invalid format. Missing root folder or too many folders!" + ); + public static final TextKey NO_DIMENSION_FILE = TextKey.of( + "iris.runtime.pack_download.no_dimension_file", + "No dimension file found in the extracted zip file." + ); + public static final TextKey CHECK_GITHUB = TextKey.of( + "iris.runtime.pack_download.check_github", + "Check that it is present on GitHub and report this to staff!" + ); + public static final TextKey INVALID_DIMENSION = TextKey.of( + "iris.runtime.pack_download.invalid_dimension", + "Invalid dimension folder under dimensions/." + ); + public static final TextKey IMPORTING = TextKey.of( + "iris.runtime.pack_download.importing", + "Importing {name} ({key})" + ); + public static final TextKey DIMENSION_KEY_CONFLICT = TextKey.of( + "iris.runtime.pack_download.dimension_key_conflict", + "Another dimension in the packs folder is already using the key {key}. Import failed!" + ); + public static final TextKey PACK_KEY_CONFLICT = TextKey.of( + "iris.runtime.pack_download.pack_key_conflict", + "Another pack is using the key {key}. Import failed!" + ); + public static final TextKey ACQUIRED = TextKey.of( + "iris.runtime.pack_download.acquired", + "Successfully acquired {name}." + ); + public static final TextKey ALREADY_INSTALLED = TextKey.of( + "iris.runtime.pack_download.already_installed", + "Pack {key} is already installed, skipping download." + ); + public static final TextKey IN_PROGRESS = TextKey.of( + "iris.runtime.pack_download.in_progress", + "Another Iris pack download is already in progress. Wait for it to finish before retrying." + ); + public static final TextKey VALIDATION_FAILED = TextKey.of( + "iris.runtime.pack_download.validation_failed", + "Pack '{pack}' failed validation; world and Studio creation will be refused. Reasons:" + ); + public static final TextKey VALIDATION_REASON = TextKey.of( + "iris.runtime.pack_download.validation_reason", + " - {reason}" + ); + public static final PluralKey VALIDATED_WITH_WARNINGS = PluralKey.of( + "iris.runtime.pack_download.validated_with_warnings", + "count", + Map.of( + "one", "Pack '{pack}' validated with {count} warning.", + "other", "Pack '{pack}' validated with {count} warnings." + ) + ); + public static final TextKey VALIDATED = TextKey.of( + "iris.runtime.pack_download.validated", + "Pack '{pack}' validated." + ); + + private static final List KEYS = List.of( + PROGRESS_START, + PROGRESS_PHASE, + PROGRESS_DETERMINATE, + PROGRESS_INDETERMINATE, + PROGRESS_DETAIL, + PROGRESS_COMPLETE, + PROGRESS_UNCHANGED, + PROGRESS_FAILED, + PROGRESS_FAILED_DETAIL, + PROGRESS_CANCELLED, + PROGRESS_RESTART, + PROGRESS_PHASE_CONNECTING, + PROGRESS_PHASE_DOWNLOADING, + PROGRESS_PHASE_UNPACKING, + PROGRESS_PHASE_VALIDATING, + PROGRESS_PHASE_PUBLISHING, + PROGRESS_SOURCE_REMOTE, + INVALID_SOURCE, + INVALID_URL, + INVALID_BUILT_IN, + SHUTTING_DOWN, + DOWNLOADING, + FAILED_TO_FIND, + UNPACKING, + UNPACK_FAILED, + NO_EXTRACTED_FILES, + HOME_DIRECTORY_ERROR, + INVALID_ARCHIVE_FORMAT, + NO_DIMENSION_FILE, + CHECK_GITHUB, + INVALID_DIMENSION, + IMPORTING, + DIMENSION_KEY_CONFLICT, + PACK_KEY_CONFLICT, + ACQUIRED, + ALREADY_INSTALLED, + IN_PROGRESS, + VALIDATION_FAILED, + VALIDATION_REASON, + VALIDATED_WITH_WARNINGS, + VALIDATED + ); + + private PackDownloadMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/RuntimeProgressMessages.java b/core/src/main/java/art/arcane/iris/core/localization/RuntimeProgressMessages.java new file mode 100644 index 000000000..c88443071 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/RuntimeProgressMessages.java @@ -0,0 +1,209 @@ +package art.arcane.iris.core.localization; + +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.PluralKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; +import java.util.Map; + +public final class RuntimeProgressMessages { + public static final TextKey STUDIO_OPENING = TextKey.of("iris.runtime.studio.opening", C.GOLD + "Studio " + C.AQUA + "OPENING"); + public static final TextKey STUDIO_OPENING_PROGRESS = TextKey.of("iris.runtime.studio.opening_progress", C.GOLD + "Studio " + C.AQUA + "OPENING" + C.GRAY + " {percent}%"); + public static final TextKey STUDIO_FAILED_PROGRESS = TextKey.of("iris.runtime.studio.failed_progress", C.GOLD + "Studio " + C.RED + "FAILED" + C.GRAY + " {percent}%"); + public static final TextKey STUDIO_READY_PROGRESS = TextKey.of("iris.runtime.studio.ready_progress", C.GOLD + "Studio " + C.GREEN + "READY" + C.GRAY + " 100%"); + public static final TextKey STUDIO_ACTION_FAILED = TextKey.of("iris.runtime.studio.action.failed", "{bar}" + C.GRAY + " " + C.RED + "FAILED" + C.GRAY + " | " + C.WHITE + "{stage}"); + public static final TextKey STUDIO_ACTION_READY = TextKey.of("iris.runtime.studio.action.ready", "{bar}" + C.GRAY + " " + C.GREEN + "100%" + C.GRAY + " | " + C.GREEN + "Studio ready" + C.DARK_GRAY + " {elapsed}"); + public static final TextKey STUDIO_ACTION_PROGRESS = TextKey.of("iris.runtime.studio.action.progress", "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.GRAY + " | " + C.WHITE + "{stage}" + C.DARK_GRAY + " {elapsed}"); + public static final TextKey STUDIO_CONSOLE_PROGRESS = TextKey.of("iris.runtime.studio.console.progress", C.GOLD + "Studio " + C.AQUA + "{bar}" + C.YELLOW + " {percent}%" + C.GRAY + " {stage}" + C.DARK_GRAY + " ({elapsed})"); + public static final TextKey STUDIO_STAGE_INITIALIZING = TextKey.of("iris.runtime.studio.stage.initializing", "Initializing"); + public static final TextKey STUDIO_STAGE_QUEUED = TextKey.of("iris.runtime.studio.stage.queued", "Queued"); + public static final TextKey STUDIO_STAGE_RESOLVE_DIMENSION = TextKey.of("iris.runtime.studio.stage.resolve_dimension", "Resolving dimension"); + public static final TextKey STUDIO_STAGE_PREPARE_WORLD_PACK = TextKey.of("iris.runtime.studio.stage.prepare_world_pack", "Preparing world pack"); + public static final TextKey STUDIO_STAGE_INSTALL_DATAPACKS = TextKey.of("iris.runtime.studio.stage.install_datapacks", "Installing datapacks"); + public static final TextKey STUDIO_STAGE_CREATE_WORLD = TextKey.of("iris.runtime.studio.stage.create_world", "Creating world"); + public static final TextKey STUDIO_STAGE_APPLY_WORLD_RULES = TextKey.of("iris.runtime.studio.stage.apply_world_rules", "Applying world rules"); + public static final TextKey STUDIO_STAGE_PREPARE_GENERATOR = TextKey.of("iris.runtime.studio.stage.prepare_generator", "Preparing generator"); + public static final TextKey STUDIO_STAGE_REQUEST_ENTRY_CHUNK = TextKey.of("iris.runtime.studio.stage.request_entry_chunk", "Loading entry chunk"); + public static final TextKey STUDIO_STAGE_RESOLVE_SAFE_ENTRY = TextKey.of("iris.runtime.studio.stage.resolve_safe_entry", "Finding safe spawn"); + public static final TextKey STUDIO_STAGE_TELEPORT_PLAYER = TextKey.of("iris.runtime.studio.stage.teleport_player", "Teleporting"); + public static final TextKey STUDIO_STAGE_FINALIZE_OPEN = TextKey.of("iris.runtime.studio.stage.finalize_open", "Finalizing"); + public static final TextKey STUDIO_STAGE_CLEANUP = TextKey.of("iris.runtime.studio.stage.cleanup", "Cleaning up"); + public static final TextKey WORLD_CREATE_TELEPORT_FAILED = TextKey.of("iris.runtime.world_create.teleport_failed", C.YELLOW + "The world was created, but automatic teleport failed. Try /iris teleport world={world}"); + public static final TextKey WORLD_CREATE_BOSSBAR_WORKING = TextKey.of("iris.runtime.world_create.bossbar.working", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.WHITE + "Starting"); + public static final TextKey WORLD_CREATE_BOSSBAR_PROGRESS = TextKey.of("iris.runtime.world_create.bossbar.progress", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.YELLOW + "{percent}% " + C.WHITE + "{stage}"); + public static final TextKey WORLD_CREATE_BOSSBAR_FAILED = TextKey.of("iris.runtime.world_create.bossbar.failed", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.RED + "FAILED " + C.DARK_GRAY + "{percent}%"); + public static final TextKey WORLD_CREATE_BOSSBAR_READY = TextKey.of("iris.runtime.world_create.bossbar.ready", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.GREEN + "READY 100%"); + public static final TextKey WORLD_CREATE_LIFECYCLE_ACTION = TextKey.of("iris.runtime.world_create.lifecycle.action", "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.GRAY + " | " + C.WHITE + "{stage}{detail}" + C.DARK_GRAY + " {elapsed}"); + public static final TextKey WORLD_CREATE_LIFECYCLE_ACTION_FAILED = TextKey.of("iris.runtime.world_create.lifecycle.action.failed", "{bar}" + C.GRAY + " " + C.RED + "FAILED" + C.GRAY + " | " + C.WHITE + "{stage}{detail}" + C.DARK_GRAY + " {elapsed}"); + public static final TextKey WORLD_CREATE_LIFECYCLE_ACTION_READY = TextKey.of("iris.runtime.world_create.lifecycle.action.ready", "{bar}" + C.GRAY + " " + C.GREEN + "100%" + C.GRAY + " | " + C.GREEN + "World ready" + C.DARK_GRAY + " {elapsed}"); + public static final TextKey WORLD_CREATE_LIFECYCLE_CONSOLE = TextKey.of("iris.runtime.world_create.lifecycle.console", C.GOLD + "World " + C.AQUA + "{world} {bar} " + C.YELLOW + "{percent}%" + C.GRAY + " {stage}{detail}" + C.DARK_GRAY + " ({elapsed})"); + public static final TextKey WORLD_CREATE_LIFECYCLE_CONSOLE_FAILED = TextKey.of("iris.runtime.world_create.lifecycle.console.failed", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.RED + "creation failed" + C.DARK_GRAY + " after {elapsed}"); + public static final TextKey WORLD_CREATE_LIFECYCLE_CONSOLE_READY = TextKey.of("iris.runtime.world_create.lifecycle.console.ready", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.GREEN + "ready" + C.DARK_GRAY + " in {elapsed}"); + public static final TextKey WORLD_CREATE_STAGE_INITIALIZING = TextKey.of("iris.runtime.world_create.stage.initializing", "Initializing"); + public static final TextKey WORLD_CREATE_STAGE_RESOLVE_DIMENSION = TextKey.of("iris.runtime.world_create.stage.resolve_dimension", "Resolving dimension"); + public static final TextKey WORLD_CREATE_STAGE_VALIDATE_PACK = TextKey.of("iris.runtime.world_create.stage.validate_pack", "Validating pack"); + public static final TextKey WORLD_CREATE_STAGE_PREPARE_WORLD_PACK = TextKey.of("iris.runtime.world_create.stage.prepare_world_pack", "Preparing world pack"); + public static final TextKey WORLD_CREATE_STAGE_INSTALL_DATAPACKS = TextKey.of("iris.runtime.world_create.stage.install_datapacks", "Installing datapacks"); + public static final TextKey WORLD_CREATE_STAGE_PREPARE_GENERATOR = TextKey.of("iris.runtime.world_create.stage.prepare_generator", "Preparing generator"); + public static final TextKey WORLD_CREATE_STAGE_CREATE_WORLD = TextKey.of("iris.runtime.world_create.stage.create_world", "Generating spawn"); + public static final TextKey WORLD_CREATE_STAGE_REGISTER_WORLD = TextKey.of("iris.runtime.world_create.stage.register_world", "Registering world"); + public static final TextKey WORLD_CREATE_STAGE_TELEPORT_PLAYER = TextKey.of("iris.runtime.world_create.stage.teleport_player", "Finding safe entry"); + public static final TextKey WORLD_CREATE_STAGE_PREGENERATE = TextKey.of("iris.runtime.world_create.stage.pregenerate", "Pregenerating"); + public static final TextKey WORLD_CREATE_STAGE_FINALIZE = TextKey.of("iris.runtime.world_create.stage.finalize", "Finalizing"); + public static final TextKey WORLD_CREATE_STAGE_COMPLETE = TextKey.of("iris.runtime.world_create.stage.complete", "World ready"); + public static final TextKey WORLD_PREGEN_ACTION = TextKey.of("iris.runtime.world_create.pregen.action", "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.GRAY + " | " + C.WHITE + "Pregenerating"); + public static final TextKey WORLD_PREGEN_CONSOLE = TextKey.of("iris.runtime.world_create.pregen.console", C.GOLD + "Pregenerating " + C.YELLOW + "{percent}%"); + public static final TextKey CHUNK_TITLE_REGEN = TextKey.of("iris.runtime.chunk_job.title.regen", "Regen"); + public static final TextKey CHUNK_TITLE_DELETE = TextKey.of("iris.runtime.chunk_job.title.delete", "Delete"); + public static final TextKey CHUNK_TITLE_GOLDEN_HASH = TextKey.of("iris.runtime.chunk_job.title.golden_hash", "GoldenHash"); + public static final TextKey CHUNK_STAGE_PREPARING = TextKey.of("iris.runtime.chunk_job.stage.preparing", "Preparing"); + public static final TextKey CHUNK_STAGE_CLEARING = TextKey.of("iris.runtime.chunk_job.stage.clearing", "Clearing"); + public static final TextKey CHUNK_STAGE_RESETTING_MANTLE = TextKey.of("iris.runtime.chunk_job.stage.resetting_mantle", "Resetting mantle"); + public static final TextKey CHUNK_STAGE_REGENERATING = TextKey.of("iris.runtime.chunk_job.stage.regenerating", "Regenerating"); + public static final TextKey CHUNK_STAGE_GENERATING = TextKey.of("iris.runtime.chunk_job.stage.generating", "Generating"); + public static final TextKey CHUNK_STAGE_COMPARING = TextKey.of("iris.runtime.chunk_job.stage.comparing", "Comparing"); + public static final TextKey CHUNK_STAGE_DIAGNOSING = TextKey.of("iris.runtime.chunk_job.stage.diagnosing", "Diagnosing"); + public static final TextKey CHUNK_BOSSBAR_WORKING = TextKey.of("iris.runtime.chunk_job.bossbar.working", C.GOLD + "{title} " + C.AQUA + "WORKING"); + public static final TextKey CHUNK_BOSSBAR_PROGRESS = TextKey.of("iris.runtime.chunk_job.bossbar.progress", C.GOLD + "{title} " + C.AQUA + "{stage}" + C.GRAY + " " + C.YELLOW + "{percent}%"); + public static final TextKey CHUNK_ACTION_PROGRESS = TextKey.of("iris.runtime.chunk_job.action.progress", "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.GRAY + " | " + C.WHITE + "{stage} {applied}/{total}"); + public static final TextKey CHUNK_SUMMARY = TextKey.of("iris.runtime.chunk_job.summary", "{applied}/{total} chunks in {elapsed}"); + public static final TextKey CHUNK_SUMMARY_FAILED = TextKey.of("iris.runtime.chunk_job.summary.failed", "{applied}/{total} chunks in {elapsed} ({failures} failed)"); + public static final TextKey CHUNK_BOSSBAR_DONE = TextKey.of("iris.runtime.chunk_job.bossbar.done", C.GOLD + "{title} " + C.GREEN + "DONE" + C.GRAY + " " + C.YELLOW + "{summary}"); + public static final TextKey CHUNK_BOSSBAR_FAILED = TextKey.of("iris.runtime.chunk_job.bossbar.failed", C.GOLD + "{title} " + C.RED + "FAILED" + C.GRAY + " " + C.YELLOW + "{summary}"); + public static final TextKey CHUNK_ACTION_DONE = TextKey.of("iris.runtime.chunk_job.action.done", "{bar}" + C.GRAY + " " + C.GREEN + "Done" + C.GRAY + " | " + C.WHITE + "{summary}"); + public static final TextKey CHUNK_ACTION_FAILED = TextKey.of("iris.runtime.chunk_job.action.failed", "{bar}" + C.GRAY + " " + C.RED + "Failed" + C.GRAY + " | " + C.WHITE + "{summary}"); + public static final TextKey CHUNK_COMPLETE = TextKey.of("iris.runtime.chunk_job.complete", C.GREEN + "{title} complete: {summary}"); + public static final TextKey CHUNK_FAILED = TextKey.of("iris.runtime.chunk_job.failed", C.RED + "{title} finished with errors: {summary}"); + public static final TextKey GOLDEN_NO_CAPTURE = TextKey.of("iris.runtime.golden.no_capture", "No golden capture at {path}; run a capture first."); + public static final PluralKey GOLDEN_ABORTED = PluralKey.of( + "iris.runtime.golden.aborted", + "failed", + Map.of( + "one", "GoldenHash aborted: {failed} chunk failed to generate. No golden file written.", + "other", "GoldenHash aborted: {failed} chunks failed to generate. No golden file written." + ) + ); + public static final TextKey GOLDEN_FAILED = TextKey.of("iris.runtime.golden.failed", "GoldenHash failed: {error}"); + public static final TextKey GOLDEN_MANTLE_RESET = TextKey.of("iris.runtime.golden.mantle_reset", "Mantle reset ({path})"); + public static final TextKey GOLDEN_MANTLE_RESET_FAILED = TextKey.of("iris.runtime.golden.mantle_reset_failed", "Mantle reset failed ({type}); continuing with existing mantle state."); + public static final TextKey GOLDEN_CAPTURED = TextKey.of("iris.runtime.golden.captured", "Golden captured: {chunks} chunks combined={hash}"); + public static final TextKey GOLDEN_WRONG_WORLD = TextKey.of("iris.runtime.golden.wrong_world", "Golden file is for dim={goldenDimension} seed={goldenSeed} but this world is dim={dimension} seed={seed}. Aborting."); + public static final TextKey GOLDEN_VERSION_WARNING = TextKey.of("iris.runtime.golden.version_warning", "Golden was captured on mc={goldenVersion}, running mc={version}. Diffs may be version-induced."); + public static final TextKey GOLDEN_MATCH = TextKey.of("iris.runtime.golden.match", "GOLDEN MATCH: {current}/{golden} chunks, combined={hash}"); + public static final TextKey GOLDEN_MISMATCH = TextKey.of("iris.runtime.golden.mismatch", "GOLDEN MISMATCH: {mismatches}/{chunks} chunks differ."); + public static final TextKey GOLDEN_MISMATCH_CHUNK = TextKey.of("iris.runtime.golden.mismatch_chunk", " chunk {chunk}"); + public static final TextKey GOLDEN_MISSING_IN_GOLDEN = TextKey.of("iris.runtime.golden.missing_in_golden", "{chunk} (missing in golden)"); + public static final TextKey GOLDEN_MISMATCH_MORE = TextKey.of("iris.runtime.golden.mismatch_more", " ... and {count} more"); + public static final TextKey GOLDEN_CURRENT_WRITTEN = TextKey.of("iris.runtime.golden.current_written", "Current hashes written to {file}"); + public static final TextKey GOLDEN_DIAG_STABLE = TextKey.of("iris.runtime.golden.diag.stable", "Repeat-gen STABLE, mantle-reset {mantleStatus} -> {file}"); + public static final TextKey GOLDEN_DIAG_UNSTABLE = TextKey.of("iris.runtime.golden.diag.unstable", "Repeat-gen UNSTABLE ({diffs}+ block diffs), mantle-reset {mantleStatus} -> {file}"); + public static final TextKey GOLDEN_MANTLE_STABLE = TextKey.of("iris.runtime.golden.diag.mantle_stable", "STABLE (mantle rebuild reproduces scan output)"); + public static final TextKey GOLDEN_MANTLE_DIVERGED = TextKey.of("iris.runtime.golden.diag.mantle_diverged", "DIVERGED ({diffs}+ diffs - mantle build is state/order dependent)"); + public static final TextKey GOLDEN_MANTLE_SKIPPED = TextKey.of("iris.runtime.golden.diag.mantle_skipped", "SKIPPED ({type})"); + public static final TextKey GOLDEN_DIAG_FAILED = TextKey.of("iris.runtime.golden.diag.failed", "Diagnosis failed: {error}"); + public static final TextKey GOLDEN_STARTED = TextKey.of("iris.runtime.golden.started", "GoldenHash started: {chunks} chunks around 0,0 in buffers (world untouched), threads={threads} mode={mode}"); + public static final TextKey GOLDEN_CHUNK_HASHED = TextKey.of("iris.runtime.golden.chunk_hashed", "[{done}/{total}] chunk {x},{z} hashed"); + public static final TextKey GOLDEN_CHUNK_FAILED = TextKey.of("iris.runtime.golden.chunk_failed", "Chunk {x},{z} FAILED: {type}"); + + private static final List KEYS = List.of( + STUDIO_OPENING, + STUDIO_OPENING_PROGRESS, + STUDIO_FAILED_PROGRESS, + STUDIO_READY_PROGRESS, + STUDIO_ACTION_FAILED, + STUDIO_ACTION_READY, + STUDIO_ACTION_PROGRESS, + STUDIO_CONSOLE_PROGRESS, + STUDIO_STAGE_INITIALIZING, + STUDIO_STAGE_QUEUED, + STUDIO_STAGE_RESOLVE_DIMENSION, + STUDIO_STAGE_PREPARE_WORLD_PACK, + STUDIO_STAGE_INSTALL_DATAPACKS, + STUDIO_STAGE_CREATE_WORLD, + STUDIO_STAGE_APPLY_WORLD_RULES, + STUDIO_STAGE_PREPARE_GENERATOR, + STUDIO_STAGE_REQUEST_ENTRY_CHUNK, + STUDIO_STAGE_RESOLVE_SAFE_ENTRY, + STUDIO_STAGE_TELEPORT_PLAYER, + STUDIO_STAGE_FINALIZE_OPEN, + STUDIO_STAGE_CLEANUP, + WORLD_CREATE_TELEPORT_FAILED, + WORLD_CREATE_BOSSBAR_WORKING, + WORLD_CREATE_BOSSBAR_PROGRESS, + WORLD_CREATE_BOSSBAR_FAILED, + WORLD_CREATE_BOSSBAR_READY, + WORLD_CREATE_LIFECYCLE_ACTION, + WORLD_CREATE_LIFECYCLE_ACTION_FAILED, + WORLD_CREATE_LIFECYCLE_ACTION_READY, + WORLD_CREATE_LIFECYCLE_CONSOLE, + WORLD_CREATE_LIFECYCLE_CONSOLE_FAILED, + WORLD_CREATE_LIFECYCLE_CONSOLE_READY, + WORLD_CREATE_STAGE_INITIALIZING, + WORLD_CREATE_STAGE_RESOLVE_DIMENSION, + WORLD_CREATE_STAGE_VALIDATE_PACK, + WORLD_CREATE_STAGE_PREPARE_WORLD_PACK, + WORLD_CREATE_STAGE_INSTALL_DATAPACKS, + WORLD_CREATE_STAGE_PREPARE_GENERATOR, + WORLD_CREATE_STAGE_CREATE_WORLD, + WORLD_CREATE_STAGE_REGISTER_WORLD, + WORLD_CREATE_STAGE_TELEPORT_PLAYER, + WORLD_CREATE_STAGE_PREGENERATE, + WORLD_CREATE_STAGE_FINALIZE, + WORLD_CREATE_STAGE_COMPLETE, + WORLD_PREGEN_ACTION, + WORLD_PREGEN_CONSOLE, + CHUNK_TITLE_REGEN, + CHUNK_TITLE_DELETE, + CHUNK_TITLE_GOLDEN_HASH, + CHUNK_STAGE_PREPARING, + CHUNK_STAGE_CLEARING, + CHUNK_STAGE_RESETTING_MANTLE, + CHUNK_STAGE_REGENERATING, + CHUNK_STAGE_GENERATING, + CHUNK_STAGE_COMPARING, + CHUNK_STAGE_DIAGNOSING, + CHUNK_BOSSBAR_WORKING, + CHUNK_BOSSBAR_PROGRESS, + CHUNK_ACTION_PROGRESS, + CHUNK_SUMMARY, + CHUNK_SUMMARY_FAILED, + CHUNK_BOSSBAR_DONE, + CHUNK_BOSSBAR_FAILED, + CHUNK_ACTION_DONE, + CHUNK_ACTION_FAILED, + CHUNK_COMPLETE, + CHUNK_FAILED, + GOLDEN_NO_CAPTURE, + GOLDEN_ABORTED, + GOLDEN_FAILED, + GOLDEN_MANTLE_RESET, + GOLDEN_MANTLE_RESET_FAILED, + GOLDEN_CAPTURED, + GOLDEN_WRONG_WORLD, + GOLDEN_VERSION_WARNING, + GOLDEN_MATCH, + GOLDEN_MISMATCH, + GOLDEN_MISMATCH_CHUNK, + GOLDEN_MISSING_IN_GOLDEN, + GOLDEN_MISMATCH_MORE, + GOLDEN_CURRENT_WRITTEN, + GOLDEN_DIAG_STABLE, + GOLDEN_DIAG_UNSTABLE, + GOLDEN_MANTLE_STABLE, + GOLDEN_MANTLE_DIVERGED, + GOLDEN_MANTLE_SKIPPED, + GOLDEN_DIAG_FAILED, + GOLDEN_STARTED, + GOLDEN_CHUNK_HASHED, + GOLDEN_CHUNK_FAILED + ); + + private RuntimeProgressMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/RuntimeUiMessages.java b/core/src/main/java/art/arcane/iris/core/localization/RuntimeUiMessages.java new file mode 100644 index 000000000..093469a40 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/RuntimeUiMessages.java @@ -0,0 +1,275 @@ +package art.arcane.iris.core.localization; + +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; + +public final class RuntimeUiMessages { + public static final TextKey FINDING_REGIONS = TextKey.of("iris.runtime.progress.finding_regions", "Finding regions"); + public static final TextKey CONVERTING = TextKey.of("iris.runtime.progress.converting", "Converting"); + public static final TextKey STATUS_RUNNING = TextKey.of("iris.runtime.status.running", "Running"); + public static final TextKey STATUS_STOPPED = TextKey.of("iris.runtime.status.stopped", "Stopped"); + public static final TextKey STATUS_PAUSED = TextKey.of("iris.runtime.status.paused", "Paused"); + public static final TextKey STATUS_PAUSED_LOWER = TextKey.of("iris.runtime.status.paused_lower", "paused"); + public static final TextKey STATUS_RUNNING_LOWER = TextKey.of("iris.runtime.status.running_lower", "running"); + public static final TextKey STATUS_ENABLED = TextKey.of("iris.runtime.status.enabled", "enabled."); + public static final TextKey STATUS_DISABLED = TextKey.of("iris.runtime.status.disabled", "disabled."); + public static final TextKey STATUS_UNREGISTERED = TextKey.of("iris.runtime.status.unregistered", "unregistered"); + public static final TextKey STATUS_NONE = TextKey.of("iris.runtime.status.none", "none"); + public static final TextKey STATUS_RANDOM = TextKey.of("iris.runtime.status.random", "random"); + public static final TextKey STATUS_MATCH = TextKey.of("iris.runtime.status.match", "MATCH"); + public static final TextKey STATUS_MISMATCH = TextKey.of("iris.runtime.status.mismatch", "MISMATCH"); + public static final TextKey STATUS_TRUE = TextKey.of("iris.runtime.status.true", "true"); + public static final TextKey STATUS_FALSE = TextKey.of("iris.runtime.status.false", "false"); + public static final TextKey ERROR_DETAIL_SUFFIX = TextKey.of("iris.runtime.error.detail_suffix", " - {error}"); + public static final TextKey DATAPACK_OVERRIDE_SUFFIX = TextKey.of("iris.runtime.datapack.override_suffix", " (world datapack override installed)"); + public static final TextKey PRIMARY_PLAYERS_ROUTED_SUFFIX = TextKey.of("iris.runtime.primary_world.players_routed_suffix", " (players routed there)"); + public static final TextKey PRIMARY_ROUTING_DISABLED_SUFFIX = TextKey.of("iris.runtime.primary_world.routing_disabled_suffix", " (routing disabled)"); + public static final TextKey MODDED_NO_DIMENSION_MATCH = TextKey.of("iris.runtime.modded.no_dimension_match", "No Iris dimension matches '{filter}'."); + public static final TextKey TELEPORTED_TO_WORLD = TextKey.of("iris.runtime.teleport.world", "You have been teleported to {world}."); + public static final TextKey ENGINE_HOTLOADED = TextKey.of("iris.runtime.engine.hotloaded", "Engine Hotloaded"); + public static final TextKey JOB_COMPLETED = TextKey.of("iris.runtime.job.completed", "Completed {job} in {duration}"); + public static final TextKey JOB_SCANNING_SELECTION = TextKey.of("iris.runtime.job.scanning_selection", "Scanning Selection"); + public static final TextKey JOB_LOADING_CHUNKS = TextKey.of("iris.runtime.job.loading_chunks", "Loading Chunks"); + public static final TextKey JOB_SEARCHED_CHUNKS = TextKey.of("iris.runtime.job.searched_chunks", "Searched {chunks} Chunks"); + public static final TextKey JOB_COMPILE = TextKey.of("iris.runtime.job.compile", "Compile"); + public static final TextKey JOB_SAVING_OBJECT = TextKey.of("iris.runtime.job.saving_object", "Saving Object"); + public static final TextKey JOB_DOWNLOADING = TextKey.of("iris.runtime.job.downloading", "Downloading"); + public static final TextKey JOB_EXTRACTING = TextKey.of("iris.runtime.job.extracting", "Extracting"); + public static final TextKey JOB_INSTALLING = TextKey.of("iris.runtime.job.installing", "Installing"); + public static final TextKey COMPILE_IOB_EMPTY = TextKey.of("iris.runtime.compile.iob_empty", "- IOB {file} has 0 blocks!"); + public static final TextKey COMPILE_IOB_EMPTY_HOVER = TextKey.of("iris.runtime.compile.iob_empty_hover", "Error:\n{path}"); + public static final TextKey COMPILE_IOB_NOT_3D = TextKey.of("iris.runtime.compile.iob_not_3d", "- IOB {file} is not 3D!"); + public static final TextKey COMPILE_IOB_NOT_3D_HOVER = TextKey.of("iris.runtime.compile.iob_not_3d_hover", "Error:\n{path}\nThe width, height, or depth is zero (bad format)"); + public static final TextKey COMPILE_JSON_ERROR = TextKey.of("iris.runtime.compile.json_error", "- JSON Error {file}"); + public static final TextKey COMPILE_JSON_ERROR_HOVER = TextKey.of("iris.runtime.compile.json_error_hover", "Error:\n{path}\n{error}"); + public static final TextKey COMPILE_LOADER_NOT_FOUND = TextKey.of("iris.runtime.compile.loader_not_found", "Can't find loader for {path}"); + public static final TextKey FORCED_DATAPACK_NAME = TextKey.of("iris.runtime.datapack.name", "Iris World Generation"); + public static final TextKey PACK_ALREADY_EXISTS = TextKey.of("iris.runtime.pack.already_exists", "Pack already exists!"); + public static final TextKey TREE_DRY_SUFFIX = TextKey.of("iris.runtime.tree_plausibilize.dry_suffix", ", DRY"); + public static final TextKey TREE_SKIP_LOAD = TextKey.of("iris.runtime.tree_plausibilize.skip_load", "skip {object}: failed to load"); + public static final TextKey TREE_RESULT = TextKey.of("iris.runtime.tree_plausibilize.result", "{object}: +{wood} wood ({branches} branches), {converted} leaves->wood, ~{distances} distances"); + public static final TextKey TREE_RESULT_PINNED = TextKey.of("iris.runtime.tree_plausibilize.result_pinned", "{object}: +{wood} wood ({branches} branches), {converted} leaves->wood, ~{distances} distances, !{pinned} pinned"); + public static final TextKey TREE_PROGRESS = TextKey.of("iris.runtime.tree_plausibilize.progress", "[{current}/{total}]"); + public static final TextKey TREE_FAILED = TextKey.of("iris.runtime.tree_plausibilize.failed", "fail {object}: {type}: {error}"); + public static final TextKey TREE_DONE = TextKey.of("iris.runtime.tree_plausibilize.done", "Done: {processed} processed, {changed} changed, {skipped} skipped, {failed} failed"); + public static final TextKey TREE_DONE_DRY = TextKey.of("iris.runtime.tree_plausibilize.done_dry", "Done: {processed} processed, {changed} changed, {skipped} skipped, {failed} failed (dry run, nothing written)"); + public static final TextKey TREE_TOTALS = TextKey.of("iris.runtime.tree_plausibilize.totals", "Totals: +{wood} wood ({branches} branches), {converted} leaves->wood, ~{distances} distances, !{pinned} pinned, unreachable {before} -> {after}"); + public static final TextKey WAND_NAME = TextKey.of("iris.runtime.item.wand.name", "Wand of Iris"); + public static final TextKey WAND_LORE_FIRST = TextKey.of("iris.runtime.item.wand.lore.first", "Left click a block to set the first corner"); + public static final TextKey WAND_LORE_SECOND = TextKey.of("iris.runtime.item.wand.lore.second", "Right click a block to set the second corner"); + public static final TextKey DUST_NAME = TextKey.of("iris.runtime.item.dust.name", "Dust of Revealing"); + public static final TextKey DUST_LORE = TextKey.of("iris.runtime.item.dust.lore", "Right click a block to reveal its placement structure!"); + public static final TextKey WAND_POSITION_SET = TextKey.of("iris.runtime.wand.position_set", "Position {position} set to {x}, {y}, {z}"); + public static final TextKey DUST_IRIS_WORLD_REQUIRED = TextKey.of("iris.runtime.dust.iris_world_required", "This dimension is not generated by Iris."); + public static final TextKey DUST_FOUND_OBJECT = TextKey.of("iris.runtime.dust.found_object", "Found object {object}"); + public static final TextKey DUST_REVEALED = TextKey.of("iris.runtime.dust.revealed", "Revealed {count} block(s) of {object}"); + public static final TextKey DUST_REVEALED_CAPPED = TextKey.of("iris.runtime.dust.revealed_capped", "Revealed {count} block(s) of {object} (capped)"); + public static final TextKey DUST_HEADER = TextKey.of("iris.runtime.dust.header", "--- Iris Dust @ {x}, {y}, {z} ---"); + public static final TextKey DUST_BLOCK = TextKey.of("iris.runtime.dust.block", "Block: {block}"); + public static final TextKey DUST_POSITION_ABOVE = TextKey.of("iris.runtime.dust.position.above", "Position: +{offset} ABOVE surface (surface Y={surfaceY})"); + public static final TextKey DUST_POSITION_BELOW = TextKey.of("iris.runtime.dust.position.below", "Position: {offset} below surface (surface Y={surfaceY})"); + public static final TextKey DUST_POSITION_AT = TextKey.of("iris.runtime.dust.position.at", "Position: at surface (Y={surfaceY})"); + public static final TextKey DUST_OBJECT_AT_BLOCK = TextKey.of("iris.runtime.dust.object_at_block", "Object @block: {object}"); + public static final TextKey DUST_NONE = TextKey.of("iris.runtime.dust.none", "none"); + public static final TextKey DUST_PLACED_BY_OBJECT_ABOVE = TextKey.of("iris.runtime.dust.placed_by.object_above", "Placed by: object/stilt '{object}' (above surface)"); + public static final TextKey DUST_PLACED_BY_DECORATION_ABOVE = TextKey.of("iris.runtime.dust.placed_by.decoration_above", "Placed by: decoration/object/stilt (above surface)"); + public static final TextKey DUST_PLACED_BY_BURIED_OBJECT = TextKey.of("iris.runtime.dust.placed_by.buried_object", "Placed by: buried object '{object}'"); + public static final TextKey DUST_PLACED_BY_TERRAIN = TextKey.of("iris.runtime.dust.placed_by.terrain", "Placed by: terrain layer (depth {depth} below surface)"); + public static final TextKey DUST_COLUMN_OBJECT = TextKey.of("iris.runtime.dust.column_object", "Column object: {object} -> this block is likely that object's stilt"); + public static final TextKey DUST_COLUMN_OBJECT_NONE = TextKey.of("iris.runtime.dust.column_object_none", "Column object: {detail}"); + public static final TextKey DUST_COLUMN_NONE = TextKey.of("iris.runtime.dust.column_none", "none within 64 (decorator or terrain, NOT an object stilt)"); + public static final TextKey DUST_COLUMN_ABOVE = TextKey.of("iris.runtime.dust.column.above", "{object} @Y={y} (above)"); + public static final TextKey DUST_COLUMN_BELOW = TextKey.of("iris.runtime.dust.column.below", "{object} @Y={y} (below)"); + public static final TextKey DUST_SURFACE_BIOME = TextKey.of("iris.runtime.dust.surface_biome", "Surface biome: {biome}"); + public static final TextKey DUST_SURFACE_BIOME_DETAIL = TextKey.of("iris.runtime.dust.surface_biome_detail", "Surface biome: {biome} ({derivative})"); + public static final TextKey DUST_BIOME_AT_Y = TextKey.of("iris.runtime.dust.biome_at_y", "Biome @Y: {biome}"); + public static final TextKey DUST_CAVE_BIOME = TextKey.of("iris.runtime.dust.cave_biome", "Cave/Mantle biome: {biome}"); + public static final TextKey DUST_SERVER_BIOME = TextKey.of("iris.runtime.dust.server_biome", "Server biome: {biome} (ID: {id})"); + public static final TextKey DUST_REGION = TextKey.of("iris.runtime.dust.region", "Region: {region} ({name})"); + public static final TextKey DUST_OBJECTS_IN_CHUNK = TextKey.of("iris.runtime.dust.objects_in_chunk", "Objects in chunk: {objects}"); + public static final TextKey DUST_COPY_BUTTON = TextKey.of("iris.runtime.dust.copy_button", "[Click to copy these stats]"); + public static final TextKey DUST_COPY_HOVER = TextKey.of("iris.runtime.dust.copy_hover", "Copy block stats to clipboard"); + public static final TextKey DUST_REVEAL_FAILED = TextKey.of("iris.runtime.dust.reveal_failed", "Object reveal failed; see the console for details."); + public static final TextKey WHAT_MATERIAL = TextKey.of("iris.runtime.what.material", "Material: {material}"); + public static final TextKey WHAT_FULL_STATE = TextKey.of("iris.runtime.what.full_state", "Full: {state}"); + public static final TextKey WHAT_ITEM_COUNT = TextKey.of("iris.runtime.what.item_count", "Count: {count}"); + public static final TextKey WHAT_IRIS_BIOME = TextKey.of("iris.runtime.what.iris_biome", "Iris biome: {biome} ({name})"); + public static final TextKey WHAT_DERIVATIVE_BIOME = TextKey.of("iris.runtime.what.derivative_biome", "Derivative biome: {biome}"); + public static final TextKey WHAT_NATIVE_BIOME = TextKey.of("iris.runtime.what.native_biome", "Registered biome: {biome} (ID: {id})"); + public static final TextKey WHAT_NON_IRIS_BIOME = TextKey.of("iris.runtime.what.non_iris_biome", "Non-Iris biome: {biome} (ID: {id})"); + public static final TextKey WHAT_IRIS_REGION = TextKey.of("iris.runtime.what.iris_region", "Iris region: {region} ({name})"); + public static final TextKey WHAT_POSITION = TextKey.of("iris.runtime.what.position", "Position: {x}, {y}, {z} (chunk {chunkX}, {chunkZ})"); + public static final TextKey WHAT_OBJECT = TextKey.of("iris.runtime.what.object", "Iris object: {object}"); + public static final TextKey WHAT_BLOCK_ENTITY = TextKey.of("iris.runtime.what.block_entity", "Block entity: {type}"); + public static final TextKey WHAT_LOOT_TABLE = TextKey.of("iris.runtime.what.loot_table", "Loot table: {loot}"); + public static final TextKey WHAT_SPAWNER_ENTITY = TextKey.of("iris.runtime.what.spawner_entity", "Spawner entity: {entity}"); + public static final TextKey WHAT_FLAG_SOLID = TextKey.of("iris.runtime.what.flag.solid", "solid"); + public static final TextKey WHAT_FLAG_FLUID = TextKey.of("iris.runtime.what.flag.fluid", "fluid"); + public static final TextKey WHAT_FLAG_WATER = TextKey.of("iris.runtime.what.flag.water", "water"); + public static final TextKey WHAT_FLAG_WATERLOGGED = TextKey.of("iris.runtime.what.flag.waterlogged", "waterlogged"); + public static final TextKey WHAT_FLAG_STORAGE = TextKey.of("iris.runtime.what.flag.storage", "storage (loot capable)"); + public static final TextKey WHAT_FLAG_LIT = TextKey.of("iris.runtime.what.flag.lit", "lit"); + public static final TextKey WHAT_FLAG_FOLIAGE = TextKey.of("iris.runtime.what.flag.foliage", "foliage"); + public static final TextKey WHAT_FLAG_PLANTABLE_FOLIAGE = TextKey.of("iris.runtime.what.flag.plantable_foliage", "plantable foliage"); + public static final TextKey WHAT_FLAG_DECORANT = TextKey.of("iris.runtime.what.flag.decorant", "decorant"); + public static final TextKey WHAT_FLAG_ORE = TextKey.of("iris.runtime.what.flag.ore", "ore"); + public static final TextKey WHAT_FLAG_BLOCK_ENTITY = TextKey.of("iris.runtime.what.flag.block_entity", "block entity"); + public static final TextKey WORLD_HEIGHT_RANGE = TextKey.of("iris.runtime.world.height_range", "World height: {minY} to {maxY}"); + public static final TextKey WORLD_HEIGHT_TOTAL = TextKey.of("iris.runtime.world.height_total", "Total height: {height}"); + public static final TextKey PREGEN_STARTING = TextKey.of("iris.runtime.pregen.starting", "Iris Pregen starting..."); + public static final TextKey PREGEN_HEADER = TextKey.of("iris.runtime.pregen.header", "Iris Pregen"); + public static final TextKey PREGEN_BOSSBAR_PAUSED = TextKey.of("iris.runtime.pregen.bossbar.paused", "Iris Pregen {generated}/{total} {percent}% PAUSED"); + public static final TextKey PREGEN_BOSSBAR_RUNNING = TextKey.of("iris.runtime.pregen.bossbar.running", "Iris Pregen {generated}/{total} {percent}% {speed}/s{eta}{failed}"); + public static final TextKey PREGEN_ETA_FRAGMENT = TextKey.of("iris.runtime.pregen.eta_fragment", " ETA {eta}"); + public static final TextKey PREGEN_FAILED_FRAGMENT = TextKey.of("iris.runtime.pregen.failed_fragment", " failed {failed}"); + public static final TextKey PREGEN_STATUS_CONTEXT = TextKey.of("iris.runtime.pregen.status.context", "Dimension {dimension} · Method {method}"); + public static final TextKey PREGEN_STATUS_PROGRESS = TextKey.of("iris.runtime.pregen.status.progress", "{percent}%"); + public static final TextKey PREGEN_STATUS_CHUNKS = TextKey.of("iris.runtime.pregen.status.chunks", "Chunks {generated}/{total} · Rates overall {overall}/s, 10s {tenSecond}/s, 30s {thirtySecond}/s, 60s {sixtySecond}/s"); + public static final TextKey PREGEN_STATUS_CHUNKS_FAILED = TextKey.of("iris.runtime.pregen.status.chunks_failed", "Chunks {generated}/{total} · Rates overall {overall}/s, 10s {tenSecond}/s, 30s {thirtySecond}/s, 60s {sixtySecond}/s · Failed {failed}"); + public static final TextKey PREGEN_STATUS_TIME = TextKey.of("iris.runtime.pregen.status.time", "ETA {eta} · Elapsed {elapsed}"); + public static final TextKey PREGEN_STATUS_TIME_PAUSED = TextKey.of("iris.runtime.pregen.status.time_paused", "ETA {eta} · Elapsed {elapsed} · PAUSED"); + public static final TextKey PREGEN_PAUSE_BUTTON = TextKey.of("iris.runtime.pregen.button.pause", "Pause/Resume"); + public static final TextKey PREGEN_PAUSE_HOVER = TextKey.of("iris.runtime.pregen.button.pause.hover", "Toggle pregeneration pause state"); + public static final TextKey PREGEN_STOP_BUTTON = TextKey.of("iris.runtime.pregen.button.stop", "Stop"); + public static final TextKey PREGEN_STOP_HOVER = TextKey.of("iris.runtime.pregen.button.stop.hover", "Finish the current region and stop pregeneration"); + + private static final List KEYS = List.of( + FINDING_REGIONS, + CONVERTING, + STATUS_RUNNING, + STATUS_STOPPED, + STATUS_PAUSED, + STATUS_PAUSED_LOWER, + STATUS_RUNNING_LOWER, + STATUS_ENABLED, + STATUS_DISABLED, + STATUS_UNREGISTERED, + STATUS_NONE, + STATUS_RANDOM, + STATUS_MATCH, + STATUS_MISMATCH, + STATUS_TRUE, + STATUS_FALSE, + ERROR_DETAIL_SUFFIX, + DATAPACK_OVERRIDE_SUFFIX, + PRIMARY_PLAYERS_ROUTED_SUFFIX, + PRIMARY_ROUTING_DISABLED_SUFFIX, + MODDED_NO_DIMENSION_MATCH, + TELEPORTED_TO_WORLD, + ENGINE_HOTLOADED, + JOB_COMPLETED, + JOB_SCANNING_SELECTION, + JOB_LOADING_CHUNKS, + JOB_SEARCHED_CHUNKS, + JOB_COMPILE, + JOB_SAVING_OBJECT, + JOB_DOWNLOADING, + JOB_EXTRACTING, + JOB_INSTALLING, + COMPILE_IOB_EMPTY, + COMPILE_IOB_EMPTY_HOVER, + COMPILE_IOB_NOT_3D, + COMPILE_IOB_NOT_3D_HOVER, + COMPILE_JSON_ERROR, + COMPILE_JSON_ERROR_HOVER, + COMPILE_LOADER_NOT_FOUND, + FORCED_DATAPACK_NAME, + PACK_ALREADY_EXISTS, + TREE_DRY_SUFFIX, + TREE_SKIP_LOAD, + TREE_RESULT, + TREE_RESULT_PINNED, + TREE_PROGRESS, + TREE_FAILED, + TREE_DONE, + TREE_DONE_DRY, + TREE_TOTALS, + WAND_NAME, + WAND_LORE_FIRST, + WAND_LORE_SECOND, + DUST_NAME, + DUST_LORE, + WAND_POSITION_SET, + DUST_IRIS_WORLD_REQUIRED, + DUST_FOUND_OBJECT, + DUST_REVEALED, + DUST_REVEALED_CAPPED, + DUST_HEADER, + DUST_BLOCK, + DUST_POSITION_ABOVE, + DUST_POSITION_BELOW, + DUST_POSITION_AT, + DUST_OBJECT_AT_BLOCK, + DUST_NONE, + DUST_PLACED_BY_OBJECT_ABOVE, + DUST_PLACED_BY_DECORATION_ABOVE, + DUST_PLACED_BY_BURIED_OBJECT, + DUST_PLACED_BY_TERRAIN, + DUST_COLUMN_OBJECT, + DUST_COLUMN_OBJECT_NONE, + DUST_COLUMN_NONE, + DUST_COLUMN_ABOVE, + DUST_COLUMN_BELOW, + DUST_SURFACE_BIOME, + DUST_SURFACE_BIOME_DETAIL, + DUST_BIOME_AT_Y, + DUST_CAVE_BIOME, + DUST_SERVER_BIOME, + DUST_REGION, + DUST_OBJECTS_IN_CHUNK, + DUST_COPY_BUTTON, + DUST_COPY_HOVER, + DUST_REVEAL_FAILED, + WHAT_MATERIAL, + WHAT_FULL_STATE, + WHAT_ITEM_COUNT, + WHAT_IRIS_BIOME, + WHAT_DERIVATIVE_BIOME, + WHAT_NATIVE_BIOME, + WHAT_NON_IRIS_BIOME, + WHAT_IRIS_REGION, + WHAT_POSITION, + WHAT_OBJECT, + WHAT_BLOCK_ENTITY, + WHAT_LOOT_TABLE, + WHAT_SPAWNER_ENTITY, + WHAT_FLAG_SOLID, + WHAT_FLAG_FLUID, + WHAT_FLAG_WATER, + WHAT_FLAG_WATERLOGGED, + WHAT_FLAG_STORAGE, + WHAT_FLAG_LIT, + WHAT_FLAG_FOLIAGE, + WHAT_FLAG_PLANTABLE_FOLIAGE, + WHAT_FLAG_DECORANT, + WHAT_FLAG_ORE, + WHAT_FLAG_BLOCK_ENTITY, + WORLD_HEIGHT_RANGE, + WORLD_HEIGHT_TOTAL, + PREGEN_STARTING, + PREGEN_HEADER, + PREGEN_BOSSBAR_PAUSED, + PREGEN_BOSSBAR_RUNNING, + PREGEN_ETA_FRAGMENT, + PREGEN_FAILED_FRAGMENT, + PREGEN_STATUS_CONTEXT, + PREGEN_STATUS_PROGRESS, + PREGEN_STATUS_CHUNKS, + PREGEN_STATUS_CHUNKS_FAILED, + PREGEN_STATUS_TIME, + PREGEN_STATUS_TIME_PAUSED, + PREGEN_PAUSE_BUTTON, + PREGEN_PAUSE_HOVER, + PREGEN_STOP_BUTTON, + PREGEN_STOP_HOVER + ); + + private RuntimeUiMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/DatapackStructureScopeResult.java b/core/src/main/java/art/arcane/iris/core/nms/DatapackStructureScopeResult.java new file mode 100644 index 000000000..005d72752 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/DatapackStructureScopeResult.java @@ -0,0 +1,7 @@ +package art.arcane.iris.core.nms; + +public record DatapackStructureScopeResult( + int retainedManagedSets, + int excludedManagedSets +) { +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMS.java b/core/src/main/java/art/arcane/iris/core/nms/INMS.java new file mode 100644 index 000000000..15a627d02 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/INMS.java @@ -0,0 +1,118 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.nms; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.nms.v1X.NMSBinding1X; +import org.bukkit.Bukkit; + +public class INMS { + //@done + private static final INMSBinding binding; + private static final Throwable bindFailure; + + // A throwing field initializer would leave this class permanently erroneous — every + // later touch raises NoClassDefFoundError and buries the real "unsupported Minecraft + // version" message. Capture the failure so callers can report the actual cause. + static { + INMSBinding bound = null; + Throwable failure = null; + try { + bound = bind(); + } catch (Throwable t) { + failure = t; + } + binding = bound; + bindFailure = failure; + } + + public static boolean isBound() { + return binding != null; + } + + public static Throwable bindFailure() { + return bindFailure; + } + + public static INMSBinding get() { + if (binding == null) { + throw new IllegalStateException("Iris NMS binding is unavailable: " + + (bindFailure == null ? "unknown failure" : bindFailure.getMessage()), bindFailure); + } + return binding; + } + + public static String getNMSTag() { + if (IrisSettings.get().getGeneral().isDisableNMS()) { + return "BUKKIT"; + } + + return NmsBindingSelector.select(requireMinecraftVersion()); + } + + private static INMSBinding bind() { + boolean disableNms = IrisSettings.get().getGeneral().isDisableNMS(); + if (disableNms) { + IrisLogging.info("Craftbukkit BUKKIT <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound"); + IrisLogging.warn("NMS support is disabled. Iris world creation is unavailable until general.disableNMS=false."); + return new NMSBinding1X(); + } + return bindExact(getNMSTag()); + } + + private static MinecraftVersion requireMinecraftVersion() { + try { + MinecraftVersion detected = MinecraftVersion.detect(Bukkit.getServer()); + if (detected == null) { + throw new IllegalStateException("Iris could not determine the exact Minecraft server version"); + } + return detected; + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Failed to determine server minecraft version!"); + e.printStackTrace(); + if (e instanceof IllegalStateException illegalStateException) { + throw illegalStateException; + } + throw new IllegalStateException("Iris could not determine the exact Minecraft server version", e); + } + } + + private static INMSBinding bindExact(String code) { + IrisLogging.info("Locating exact NMS Binding for " + code); + try { + Class clazz = Class.forName("art.arcane.iris.core.nms." + code + ".NMSBinding"); + Object candidate = clazz.getConstructor().newInstance(); + if (candidate instanceof INMSBinding binding) { + IrisLogging.info("Craftbukkit " + code + " <-> " + candidate.getClass().getSimpleName() + " Successfully Bound"); + return binding; + } + throw new IllegalStateException("Exact NMS binding class for " + code + + " does not implement " + INMSBinding.class.getName()); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + if (e instanceof IllegalStateException illegalStateException) { + throw illegalStateException; + } + throw new IllegalStateException("Failed to bind exact NMS revision " + code, e); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java new file mode 100644 index 000000000..f88f93ff3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java @@ -0,0 +1,302 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.nms; + +import art.arcane.iris.core.datapack.DatapackStructureScopeIndex; +import art.arcane.iris.engine.object.IrisImportedStructureControl; +import art.arcane.iris.core.lifecycle.WorldLifecycleCaller; +import art.arcane.iris.core.lifecycle.WorldLifecycleRequest; +import art.arcane.iris.core.lifecycle.WorldLifecycleService; +import art.arcane.iris.core.nms.container.BiomeColor; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.nms.datapack.DataVersion; +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.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +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.nbt.mca.palette.MCABiomeContainer; +import art.arcane.volmlib.util.nbt.mca.palette.MCAPaletteAccess; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.block.Biome; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.inventory.ItemStack; + +import java.awt.Color; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public interface INMSBinding { + boolean hasTile(Material material); + + boolean hasTile(Location l); + + KMap serializeTile(Location location); + + void deserializeTile(KMap s, Location newPosition); + + CompoundTag serializeEntity(Entity location); + + Entity deserializeEntity(CompoundTag s, Location newPosition); + + boolean supportsCustomHeight(); + + Object getBiomeBaseFromId(int id); + + int getMinHeight(World world); + + boolean supportsCustomBiomes(); + + boolean supportsIrisWorldGeneration(); + + int getTrueBiomeBaseId(Object biomeBase); + + Object getTrueBiomeBase(Location location); + + String getTrueBiomeBaseKey(Location location); + + Object getCustomBiomeBaseFor(String mckey); + + Object getCustomBiomeBaseHolderFor(String mckey); + + int getBiomeBaseIdForKey(String key); + + String getKeyForBiomeBase(Object biomeBase); + + Object getBiomeBase(World world, Biome biome); + + Object getBiomeBase(Object registry, Biome biome); + + KList getBiomes(); + + KList getStructureKeys(); + + default KList getJigsawStructureKeys() { + return new KList<>(); + } + + default KList getTemplatePoolKeys() { + return new KList<>(); + } + + default JigsawSourceMetadata getJigsawSourceMetadata(String structureKey) { + throw new UnsupportedOperationException("The active NMS binding does not expose registered jigsaw metadata"); + } + + default int getTemplatePoolHorizontalSpan(String templatePoolKey) { + throw new UnsupportedOperationException("The active NMS binding does not expose registered template pool spans"); + } + + default int getJigsawStartPoolHorizontalSpan(String structureKey, String templatePoolKey) { + return getTemplatePoolHorizontalSpan(templatePoolKey); + } + + KList getStructureSetKeys(); + + KList getReachableStructureKeys(World world); + + KList getStructureBiomeKeys(String structureKey); + + KList getPossibleBiomeKeys(World world); + + default KList getObjectFeatureKeys() { + return new KList<>(); + } + + default boolean placeFeature(World world, int x, int y, int z, String featureKey, long seed) { + throw new UnsupportedOperationException("The active NMS binding does not support feature placement."); + } + + default int[] placeStructure(World world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) { + throw new UnsupportedOperationException("The active NMS binding does not support structure placement."); + } + + default boolean supportsStructureCapture() { + return false; + } + + /** + * World-space piece bounds of every native structure that will generate inside the given XZ rect. Bindings + * without native structure support answer with no volumes, which leaves the object veto inert. + */ + default KList nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) { + return NativeStructureVolume.NONE; + } + + int getBiomeId(Biome biome); + + MCABiomeContainer newBiomeContainer(int min, int max, int[] data); + + MCABiomeContainer newBiomeContainer(int min, int max); + + default World createWorld(WorldCreator c) { + WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(c, false, false, WorldLifecycleCaller.CREATE); + return createWorld(c, request); + } + + default CompletableFuture createWorldAsync(WorldCreator c) { + WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(c, false, false, WorldLifecycleCaller.CREATE); + return createWorldAsync(c, request); + } + + default World createWorld(WorldCreator c, WorldLifecycleRequest request) { + validateDimensionTypes(c); + return WorldLifecycleService.get().createBlocking(request); + } + + default CompletableFuture createWorldAsync(WorldCreator c, WorldLifecycleRequest request) { + try { + validateDimensionTypes(c); + return WorldLifecycleService.get().create(request); + } catch (Throwable e) { + return CompletableFuture.failedFuture(e); + } + } + + default Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) { + throw new UnsupportedOperationException("Active NMS binding does not support runtime LevelStem creation."); + } + + default void ensureServerLevelInjection() { + } + + int countCustomBiomes(); + + default boolean supportsDataPacks() { + return false; + } + + MCAPaletteAccess createPalette(); + + default boolean applyChunkBlocks(Chunk chunk, TerrainChunk data) { + return false; + } + + default boolean applyChunkDataBlocks(ChunkGenerator.ChunkData chunkData, Hunk data) { + return false; + } + + default boolean clearChunkBlocks(Chunk chunk) { + return false; + } + + default boolean forceEvictChunk(World world, int chunkX, int chunkZ) { + return false; + } + + default boolean saveAndUnloadChunk(World world, int x, int z) { + return false; + } + + default void flushChunkIO(World world) { + } + + void injectBiomesFromMantle(Chunk e, Mantle mantle); + + ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException; + + void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException; + + DatapackStructureScopeResult scopeDatapackStructures( + World world, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + IrisImportedStructureControl importedStructures + ) throws NoSuchFieldException, IllegalAccessException; + + void completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException; + + void abandonStudioStructureBootstrap(World world); + + Vector3d getBoundingbox(org.bukkit.entity.EntityType entity); + + String getEntitySpawnCategory(String key); + + Entity spawnEntity(Location location, EntityType type, CreatureSpawnEvent.SpawnReason reason); + + Color getBiomeColor(Location location, BiomeColor type); + + default DataVersion getDataVersion() { + return DataVersion.V26_2; + } + + default int getSpawnChunkCount(World world) { + return 441; + } + + boolean missingDimensionTypes(String... keys); + + default boolean injectBukkit() { + return true; + } + + /** + * Removes any instrumentation installed by {@link #injectBukkit()}. Must be idempotent; + * called on plugin disable and pre-unload so the transformer cannot outlive the plugin. + */ + default void uninjectBukkit() { + } + + default void writeCurrentPaperWorldData( + Path sourceWorldDirectory, + Path targetWorldDirectory, + long seed + ) throws IOException { + throw new UnsupportedOperationException("The active NMS binding does not support current Paper world data staging."); + } + + default boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) { + return true; + } + + KMap> getBlockProperties(); + + private void validateDimensionTypes(WorldCreator c) { + if (!(c.generator() instanceof PlatformChunkGenerator generator)) { + return; + } + if (!supportsIrisWorldGeneration()) { + throw new IllegalStateException("Iris world '" + c.name() + "' cannot be created with limited NMS binding " + + getClass().getSimpleName() + + "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)"); + } + if (missingDimensionTypes(generator.getTarget().getDimension().getDimensionTypeKey())) { + throw new IllegalStateException("Missing dimension types to create world"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/MinecraftVersion.java b/core/src/main/java/art/arcane/iris/core/nms/MinecraftVersion.java new file mode 100644 index 000000000..dde236c23 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/MinecraftVersion.java @@ -0,0 +1,146 @@ +package art.arcane.iris.core.nms; + +import org.bukkit.Server; + +import java.lang.reflect.Method; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class MinecraftVersion { + private static final Pattern DECORATED_VERSION_PATTERN = Pattern.compile("\\(MC: ([0-9]+(?:\\.[0-9]+){0,2})\\)"); + + private final String value; + private final int major; + private final int minor; + private final int patch; + + private MinecraftVersion(String value, int major, int minor, int patch) { + this.value = value; + this.major = major; + this.minor = minor; + this.patch = patch; + } + + public static MinecraftVersion detect(Server server) { + if (server == null) { + return null; + } + + MinecraftVersion runtimeVersion = fromRuntimeMinecraftVersion(server); + if (runtimeVersion != null) { + return runtimeVersion; + } + + MinecraftVersion decoratedVersion = fromDecoratedVersion(server.getVersion()); + if (decoratedVersion != null) { + return decoratedVersion; + } + + return fromBukkitVersion(server.getBukkitVersion()); + } + + static MinecraftVersion fromRuntimeMinecraftVersion(Server server) { + try { + Method method = server.getClass().getMethod("getMinecraftVersion"); + Object value = method.invoke(server); + if (value instanceof String version) { + return fromVersionToken(version); + } + } catch (ReflectiveOperationException ignored) { + return null; + } + + return null; + } + + static MinecraftVersion fromDecoratedVersion(String input) { + if (input == null || input.isBlank()) { + return null; + } + + Matcher matcher = DECORATED_VERSION_PATTERN.matcher(input); + if (!matcher.find()) { + return null; + } + + return fromVersionToken(matcher.group(1)); + } + + static MinecraftVersion fromBukkitVersion(String input) { + if (input == null || input.isBlank()) { + return null; + } + + String versionToken = input.split("-", 2)[0].trim(); + return fromVersionToken(versionToken); + } + + private static MinecraftVersion fromVersionToken(String input) { + if (input == null || input.isBlank()) { + return null; + } + + String[] parts = input.split("\\."); + if (parts.length < 2 || parts.length > 3) { + return null; + } + for (String part : parts) { + if (part.isEmpty() || !part.chars().allMatch(Character::isDigit)) { + return null; + } + } + + try { + if ("1".equals(parts[0])) { + int major = Integer.parseInt(parts[1]); + int minor = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + return new MinecraftVersion(input, major, minor, 0); + } + + int major = Integer.parseInt(parts[0]); + int minor = Integer.parseInt(parts[1]); + int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + return new MinecraftVersion(input, major, minor, patch); + } catch (NumberFormatException ignored) { + return null; + } + } + + public String value() { + return value; + } + + public int major() { + return major; + } + + public int minor() { + return minor; + } + + public int patch() { + return patch; + } + + public boolean isAtLeast(int major, int minor) { + return this.major > major || (this.major == major && this.minor >= minor); + } + + public boolean isAtLeast(int major, int minor, int patch) { + if (this.major != major) { + return this.major > major; + } + if (this.minor != minor) { + return this.minor > minor; + } + return this.patch >= patch; + } + + public boolean isSameRelease(int major, int minor, int patch) { + return this.major == major && this.minor == minor && this.patch == patch; + } + + public boolean isNewerThan(int major, int minor) { + return this.major > major || (this.major == major && this.minor > minor); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/NmsBindingSelector.java b/core/src/main/java/art/arcane/iris/core/nms/NmsBindingSelector.java new file mode 100644 index 000000000..797936ce1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/NmsBindingSelector.java @@ -0,0 +1,20 @@ +package art.arcane.iris.core.nms; + +final class NmsBindingSelector { + private static final String SUPPORTED_VERSIONS = "26.1.2 or 26.2"; + private static final String SUPPORTED_TAG = "v26_2_R1"; + + private NmsBindingSelector() { + } + + static String select(MinecraftVersion version) { + if (version == null) { + throw new IllegalStateException("Iris requires an exact Minecraft version before selecting NMS"); + } + if (!version.isSameRelease(26, 2, 0) && !version.isSameRelease(26, 1, 2)) { + throw new IllegalStateException("Iris requires Minecraft " + SUPPORTED_VERSIONS + + ". Detected server version: " + version.value()); + } + return SUPPORTED_TAG; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/ServerShutdownBoundary.java b/core/src/main/java/art/arcane/iris/core/nms/ServerShutdownBoundary.java new file mode 100644 index 000000000..0d3acadb5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/ServerShutdownBoundary.java @@ -0,0 +1,59 @@ +package art.arcane.iris.core.nms; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; + +public final class ServerShutdownBoundary { + private static final long MAX_JOIN_SLICE_MILLIS = 1000L; + + private ServerShutdownBoundary() { + } + + public static boolean await( + BooleanSupplier boundaryReached, + Thread serverThread, + long timeout, + TimeUnit unit + ) { + BooleanSupplier reached = Objects.requireNonNull(boundaryReached, "Server shutdown boundary"); + Thread activeServerThread = Objects.requireNonNull(serverThread, "Server thread"); + TimeUnit activeUnit = Objects.requireNonNull(unit, "Server shutdown timeout unit"); + if (reached.getAsBoolean()) { + return true; + } + if (activeServerThread == Thread.currentThread()) { + return false; + } + + long timeoutNanos = Math.max(0L, activeUnit.toNanos(timeout)); + long started = System.nanoTime(); + boolean interrupted = false; + while (!reached.getAsBoolean()) { + long remaining = timeoutNanos - (System.nanoTime() - started); + if (remaining <= 0L || !activeServerThread.isAlive()) { + restoreInterrupt(interrupted); + return reached.getAsBoolean(); + } + + long joinMillis = Math.max( + 1L, + Math.min(MAX_JOIN_SLICE_MILLIS, TimeUnit.NANOSECONDS.toMillis(remaining)) + ); + try { + activeServerThread.join(joinMillis); + } catch (InterruptedException e) { + interrupted = true; + } + } + + restoreInterrupt(interrupted); + return true; + } + + private static void restoreInterrupt(boolean interrupted) { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/container/BiomeColor.java b/core/src/main/java/art/arcane/iris/core/nms/container/BiomeColor.java new file mode 100644 index 000000000..02d7cbcf7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/container/BiomeColor.java @@ -0,0 +1,10 @@ +package art.arcane.iris.core.nms.container; + +public enum BiomeColor { + FOG, + WATER, + WATER_FOG, + SKY, + FOLIAGE, + GRASS +} diff --git a/core/src/main/java/com/volmit/iris/core/nms/container/BlockPos.java b/core/src/main/java/art/arcane/iris/core/nms/container/BlockPos.java similarity index 83% rename from core/src/main/java/com/volmit/iris/core/nms/container/BlockPos.java rename to core/src/main/java/art/arcane/iris/core/nms/container/BlockPos.java index 80a496be7..09c475378 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/container/BlockPos.java +++ b/core/src/main/java/art/arcane/iris/core/nms/container/BlockPos.java @@ -1,4 +1,4 @@ -package com.volmit.iris.core.nms.container; +package art.arcane.iris.core.nms.container; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/com/volmit/iris/core/nms/container/BlockProperty.java b/core/src/main/java/art/arcane/iris/core/nms/container/BlockProperty.java similarity index 92% rename from core/src/main/java/com/volmit/iris/core/nms/container/BlockProperty.java rename to core/src/main/java/art/arcane/iris/core/nms/container/BlockProperty.java index 9ab777ce6..3a9d01425 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/container/BlockProperty.java +++ b/core/src/main/java/art/arcane/iris/core/nms/container/BlockProperty.java @@ -1,10 +1,16 @@ -package com.volmit.iris.core.nms.container; +package art.arcane.iris.core.nms.container; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; import java.util.function.Function; public class BlockProperty { @@ -62,7 +68,7 @@ public class BlockProperty { max, exclusiveMin, exclusiveMax, - (l) -> Long.toString(l) + value -> Long.toString(value) ); } @@ -135,8 +141,10 @@ public class BlockProperty { } private static class BoundedLong extends BlockProperty { - private final long min, max; - private final boolean exclusiveMin, exclusiveMax; + private final long min; + private final long max; + private final boolean exclusiveMin; + private final boolean exclusiveMax; public BoundedLong( String name, diff --git a/core/src/main/java/com/volmit/iris/core/nms/container/Pair.java b/core/src/main/java/art/arcane/iris/core/nms/container/Pair.java similarity index 81% rename from core/src/main/java/com/volmit/iris/core/nms/container/Pair.java rename to core/src/main/java/art/arcane/iris/core/nms/container/Pair.java index 140bf0200..4a253acb8 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/container/Pair.java +++ b/core/src/main/java/art/arcane/iris/core/nms/container/Pair.java @@ -1,4 +1,4 @@ -package com.volmit.iris.core.nms.container; +package art.arcane.iris.core.nms.container; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java new file mode 100644 index 000000000..cd8a964c7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java @@ -0,0 +1,66 @@ +package art.arcane.iris.core.nms.datapack; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217; +import art.arcane.volmlib.util.collection.KMap; +import lombok.AccessLevel; +import lombok.Getter; + +import java.util.function.Supplier; + +//https://minecraft.wiki/w/Pack_format +@Getter +public enum DataVersion { + UNSUPPORTED("0.0.0", 0, () -> null), + V26_1_2("26.1.2", 101, DataFixerV1217::new), + V26_2("26.2", 107, DataFixerV1217::new); + private static final KMap cache = new KMap<>(); + @Getter(AccessLevel.NONE) + private final Supplier constructor; + private final String version; + private final int packFormat; + + DataVersion(String version, int packFormat, Supplier constructor) { + this.constructor = constructor; + this.packFormat = packFormat; + this.version = version; + } + + public IDataFixer get() { + return cache.computeIfAbsent(this, k -> constructor.get()); + } + + public static IDataFixer getDefault() { + DataVersion version = INMS.get().getDataVersion(); + if (version == null || version == UNSUPPORTED) { + DataVersion fallback = getLatest(); + IrisLogging.warn("Unsupported datapack version mapping detected, falling back to latest fixer: " + fallback.getVersion()); + return fallback.get(); + } + + IDataFixer fixer = version.get(); + if (fixer == null) { + DataVersion fallback = getLatest(); + IrisLogging.warn("Null datapack fixer for " + version.getVersion() + ", falling back to latest fixer: " + fallback.getVersion()); + return fallback.get(); + } + + return fixer; + } + + public static DataVersion getLatest() { + return values()[values().length - 1]; + } + + public static int minSupportedPackFormat() { + int minimum = Integer.MAX_VALUE; + for (DataVersion version : values()) { + if (version == UNSUPPORTED) { + continue; + } + minimum = Math.min(minimum, version.packFormat); + } + return minimum; + } +} diff --git a/core/src/main/java/com/volmit/iris/core/nms/datapack/IDataFixer.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/IDataFixer.java similarity index 78% rename from core/src/main/java/com/volmit/iris/core/nms/datapack/IDataFixer.java rename to core/src/main/java/art/arcane/iris/core/nms/datapack/IDataFixer.java index 0e8a706a3..f58638b05 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/datapack/IDataFixer.java +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/IDataFixer.java @@ -1,8 +1,8 @@ -package com.volmit.iris.core.nms.datapack; +package art.arcane.iris.core.nms.datapack; -import com.volmit.iris.engine.object.IrisBiomeCustom; -import com.volmit.iris.engine.object.IrisDimensionTypeOptions; -import com.volmit.iris.util.json.JSONObject; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.engine.object.IrisDimensionTypeOptions; +import art.arcane.volmlib.util.json.JSONObject; import org.jetbrains.annotations.Nullable; public interface IDataFixer { diff --git a/core/src/main/java/com/volmit/iris/core/nms/datapack/v1192/DataFixerV1192.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1192/DataFixerV1192.java similarity index 88% rename from core/src/main/java/com/volmit/iris/core/nms/datapack/v1192/DataFixerV1192.java rename to core/src/main/java/art/arcane/iris/core/nms/datapack/v1192/DataFixerV1192.java index a9bb59e0e..8de733b3e 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/datapack/v1192/DataFixerV1192.java +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1192/DataFixerV1192.java @@ -1,13 +1,14 @@ -package com.volmit.iris.core.nms.datapack.v1192; +package art.arcane.iris.core.nms.datapack.v1192; -import com.volmit.iris.core.nms.datapack.IDataFixer; -import com.volmit.iris.engine.object.IrisDimensionTypeOptions; -import com.volmit.iris.util.json.JSONObject; +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.FALSE; +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.TRUE; +import art.arcane.iris.core.nms.datapack.IDataFixer; +import art.arcane.iris.engine.object.IrisDimensionTypeOptions; +import art.arcane.volmlib.util.json.JSONObject; import org.jetbrains.annotations.Nullable; import java.util.Map; -import static com.volmit.iris.engine.object.IrisDimensionTypeOptions.TriState.*; public class DataFixerV1192 implements IDataFixer { private static final Map OPTIONS = Map.of( diff --git a/core/src/main/java/art/arcane/iris/core/nms/datapack/v1206/DataFixerV1206.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1206/DataFixerV1206.java new file mode 100644 index 000000000..bbd2e6ce2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1206/DataFixerV1206.java @@ -0,0 +1,67 @@ +package art.arcane.iris.core.nms.datapack.v1206; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.nms.datapack.v1192.DataFixerV1192; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.engine.object.IrisBiomeCustomSpawn; +import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.util.Locale; + +public class DataFixerV1206 extends DataFixerV1192 { + @Override + public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) { + int spawnRarity = biome.getSpawnRarity(); + if (spawnRarity > 0) { + json.put("creature_spawn_probability", Math.min(spawnRarity/20d, 0.9999999)); + } else { + json.remove("creature_spawn_probability"); + } + + KList spawns = biome.getSpawns(); + if (spawns != null && spawns.isNotEmpty()) { + JSONObject spawners = new JSONObject(); + KMap groups = new KMap<>(); + + for (IrisBiomeCustomSpawn i : spawns) { + if (i == null) { + continue; + } + String key = i.getTypeKey(); + if (key == null) { + IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + biome.getId()); + continue; + } + IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup(); + JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray()); + JSONObject o = new JSONObject(); + o.put("type", key); + o.put("weight", i.getWeight()); + o.put("minCount", i.getMinCount()); + o.put("maxCount", i.getMaxCount()); + g.put(o); + } + + for (IrisBiomeCustomSpawnType i : groups.k()) { + spawners.put(i.name().toLowerCase(Locale.ROOT), groups.get(i)); + } + + json.put("spawners", spawners); + } + return json; + } + + @Override + public void fixDimension(Dimension dimension, JSONObject json) { + super.fixDimension(dimension, json); + if (!(json.get("monster_spawn_light_level") instanceof JSONObject lightLevel)) + return; + var value = (JSONObject) lightLevel.remove("value"); + lightLevel.put("max_inclusive", value.get("max_inclusive")); + lightLevel.put("min_inclusive", value.get("min_inclusive")); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/datapack/v1213/DataFixerV1213.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1213/DataFixerV1213.java new file mode 100644 index 000000000..e79fa19e1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1213/DataFixerV1213.java @@ -0,0 +1,16 @@ +package art.arcane.iris.core.nms.datapack.v1213; + +import art.arcane.iris.core.nms.datapack.v1206.DataFixerV1206; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +public class DataFixerV1213 extends DataFixerV1206 { + + @Override + public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) { + json = super.fixCustomBiome(biome, json); + json.put("carvers", new JSONArray()); + return json; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/datapack/v1217/DataFixerV1217.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1217/DataFixerV1217.java new file mode 100644 index 000000000..2065c6f42 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1217/DataFixerV1217.java @@ -0,0 +1,199 @@ +package art.arcane.iris.core.nms.datapack.v1217; + +import art.arcane.iris.core.nms.datapack.v1213.DataFixerV1213; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.util.Map; + +public class DataFixerV1217 extends DataFixerV1213 { + private static final Map DIMENSIONS = Map.of( + Dimension.OVERWORLD, """ + { + "ambient_light": 0.0, + "default_clock": "minecraft:overworld", + "has_ender_dragon_fight": false, + "attributes": { + "minecraft:audio/ambient_sounds": { + "mood": { + "block_search_extent": 8, + "offset": 2.0, + "sound": "minecraft:ambient.cave", + "tick_delay": 6000 + } + }, + "minecraft:audio/background_music": { + "creative": { + "max_delay": 24000, + "min_delay": 12000, + "sound": "minecraft:music.creative" + }, + "default": { + "max_delay": 24000, + "min_delay": 12000, + "sound": "minecraft:music.game" + } + }, + "minecraft:visual/ambient_light_color": "#0a0a0a", + "minecraft:visual/cloud_color": "#ccffffff", + "minecraft:visual/fog_color": "#c0d8ff", + "minecraft:visual/sky_color": "#78a7ff" + }, + "timelines": "#minecraft:in_overworld" + }""", + Dimension.NETHER, """ + { + "ambient_light": 0.1, + "has_fixed_time": true, + "has_ender_dragon_fight": false, + "attributes": { + "minecraft:gameplay/sky_light_level": 4.0, + "minecraft:gameplay/snow_golem_melts": true, + "minecraft:visual/ambient_light_color": "#302821", + "minecraft:visual/fog_end_distance": 96.0, + "minecraft:visual/fog_start_distance": 10.0, + "minecraft:visual/sky_light_color": "#7a7aff", + "minecraft:visual/sky_light_factor": 0.0 + }, + "cardinal_light": "nether", + "skybox": "none", + "timelines": "#minecraft:in_nether" + }""", + Dimension.END, """ + { + "ambient_light": 0.25, + "default_clock": "minecraft:the_end", + "has_fixed_time": true, + "has_ender_dragon_fight": true, + "attributes": { + "minecraft:audio/ambient_sounds": { + "mood": { + "block_search_extent": 8, + "offset": 2.0, + "sound": "minecraft:ambient.cave", + "tick_delay": 6000 + } + }, + "minecraft:audio/background_music": { + "default": { + "max_delay": 24000, + "min_delay": 6000, + "replace_current_music": true, + "sound": "minecraft:music.end" + } + }, + "minecraft:visual/ambient_light_color": "#3f473f", + "minecraft:visual/fog_color": "#181318", + "minecraft:visual/sky_color": "#000000", + "minecraft:visual/sky_light_color": "#e580ff", + "minecraft:visual/sky_light_factor": 0.0 + }, + "skybox": "end", + "timelines": "#minecraft:in_end" + }""" + ); + + @Override + public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) { + JSONObject fixed = super.fixCustomBiome(biome, json); + JSONObject effects = fixed.getJSONObject("effects"); + JSONObject attributes = fixed.optJSONObject("attributes"); + if (attributes == null) { + attributes = new JSONObject(); + fixed.put("attributes", attributes); + } + + moveAttribute(effects, attributes, "sky_color", "minecraft:visual/sky_color"); + moveAttribute(effects, attributes, "fog_color", "minecraft:visual/fog_color"); + moveAttribute(effects, attributes, "water_fog_color", "minecraft:visual/water_fog_color"); + + JSONObject particle = effects.optJSONObject("particle"); + if (particle != null) { + JSONObject ambientParticle = new JSONObject(); + ambientParticle.put("particle", particle.remove("options")); + ambientParticle.put("probability", particle.remove("probability")); + attributes.put("minecraft:visual/ambient_particles", new JSONArray().put(ambientParticle)); + effects.remove("particle"); + } + + return fixed; + } + + @Override + public void fixDimension(Dimension dimension, JSONObject json) { + super.fixDimension(dimension, json); + + JSONObject attributes = new JSONObject(); + if ((Boolean) json.remove("ultrawarm")) { + attributes.put("minecraft:gameplay/water_evaporates", true); + attributes.put("minecraft:gameplay/fast_lava", true); + attributes.put("minecraft:gameplay/snow_golem_melts", true); + attributes.put("minecraft:visual/default_dripstone_particle", new JSONObject() + .put("type", "minecraft:dripping_dripstone_lava")); + } + + if ((Boolean) json.remove("bed_works")) { + attributes.put("minecraft:gameplay/bed_rule", new JSONObject() + .put("can_set_spawn", "always") + .put("can_sleep", "when_dark") + .put("error_message", new JSONObject() + .put("translate", "block.minecraft.bed.no_sleep"))); + } else { + attributes.put("minecraft:gameplay/bed_rule", new JSONObject() + .put("can_set_spawn", "never") + .put("can_sleep", "never") + .put("explodes", true)); + } + + attributes.put("minecraft:gameplay/respawn_anchor_works", json.remove("respawn_anchor_works")); + attributes.put("minecraft:gameplay/piglins_zombify", !(Boolean) json.remove("piglin_safe")); + attributes.put("minecraft:gameplay/can_start_raid", json.remove("has_raids")); + + Object cloudHeight = json.remove("cloud_height"); + if (cloudHeight != null) { + attributes.put("minecraft:visual/cloud_height", cloudHeight); + } + + boolean natural = (Boolean) json.remove("natural"); + attributes.put("minecraft:gameplay/nether_portal_spawns_piglin", natural); + if (natural != (dimension == Dimension.OVERWORLD)) { + attributes.put("minecraft:gameplay/eyeblossom_open", natural); + attributes.put("minecraft:gameplay/creaking_active", natural); + } + + //json.put("has_fixed_time", json.remove("fixed_time") != null); //TODO investigate + json.put("attributes", attributes); + + json.remove("effects"); + JSONObject defaults = new JSONObject(DIMENSIONS.get(dimension)); + merge(json, defaults); + + Object ambientLight = json.opt("ambient_light"); + if (ambientLight instanceof Number number && number.doubleValue() >= 1D) { + json.getJSONObject("attributes").put("minecraft:visual/ambient_light_color", "#ffffff"); + } + } + + private void moveAttribute(JSONObject source, JSONObject target, String sourceKey, String targetKey) { + Object value = source.remove(sourceKey); + if (value != null) { + target.put(targetKey, value); + } + } + + private void merge(JSONObject base, JSONObject override) { + for (String key : override.keySet()) { + switch (base.opt(key)) { + case null -> base.put(key, override.opt(key)); + case JSONObject base1 when override.opt(key) instanceof JSONObject override1 -> merge(base1, override1); + case JSONArray base1 when override.opt(key) instanceof JSONArray override1 -> { + for (Object o : override1) { + base1.put(o); + } + } + default -> {} + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java new file mode 100644 index 000000000..020988ad7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java @@ -0,0 +1,320 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.nms.v1X; + +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.spi.IrisLogging; +import art.arcane.iris.core.nms.INMSBinding; +import art.arcane.iris.core.nms.container.BiomeColor; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +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.nbt.mca.palette.MCABiomeContainer; +import art.arcane.volmlib.util.nbt.mca.palette.MCAPaletteAccess; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.inventory.ItemStack; + +import java.awt.Color; +import java.util.List; +import java.util.Set; +import java.util.stream.StreamSupport; + +public class NMSBinding1X implements INMSBinding { + private static final boolean supportsCustomHeight = testCustomHeight(); + + @SuppressWarnings("ConstantConditions") + private static boolean testCustomHeight() { + try { + if (World.class.getDeclaredMethod("getMaxHeight") != null && World.class.getDeclaredMethod("getMinHeight") != null) + ; + { + return true; + } + } catch (Throwable ignored) { + + } + + return false; + } + + @Override + public boolean hasTile(Material material) { + return false; + } + + @Override + public boolean hasTile(Location l) { + return false; + } + + @Override + public KMap serializeTile(Location location) { + return null; + } + + @Override + public void deserializeTile(KMap s, Location newPosition) { + + } + + + @Override + public void injectBiomesFromMantle(Chunk e, Mantle mantle) { + + } + + @Override + public ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException { + return itemStack; + } + + @Override + public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException { + throw new IllegalStateException("Iris world generation requires the supported NMS binding; " + + "general.disableNMS=true cannot create or initialize an Iris world"); + } + + @Override + public DatapackStructureScopeResult scopeDatapackStructures( + World world, + DatapackStructureScopeIndex scopeIndex, + Set declaredSources, + IrisImportedStructureControl importedStructures + ) { + throw new IllegalStateException("Iris-managed datapack structure isolation requires the supported NMS binding"); + } + + @Override + public void completeStudioStructureBootstrap(World world) { + } + + @Override + public void abandonStudioStructureBootstrap(World world) { + } + + public Vector3d getBoundingbox() { + return null; + } + + @Override + public Entity spawnEntity(Location location, EntityType type, CreatureSpawnEvent.SpawnReason reason) { + return location.getWorld().spawnEntity(location, type); + } + + @Override + public Color getBiomeColor(Location location, BiomeColor type) { + return Color.GREEN; + } + + @Override + public boolean missingDimensionTypes(String... keys) { + return false; + } + + @Override + public KMap> getBlockProperties() { + KMap> map = new KMap<>(); + for (Material m : Material.values()) { + if (m.isBlock()) map.put(m, List.of()); + } + return map; + } + + @Override + public CompoundTag serializeEntity(Entity location) { + return null; + } + + @Override + public Entity deserializeEntity(CompoundTag s, Location newPosition) { + return null; + } + + @Override + public boolean supportsCustomHeight() { + return supportsCustomHeight; + } + + @Override + public Object getBiomeBaseFromId(int id) { + return null; + } + + @Override + public int getMinHeight(World world) { + return supportsCustomHeight ? world.getMinHeight() : 0; + } + + @Override + public boolean supportsCustomBiomes() { + return false; + } + + @Override + public boolean supportsIrisWorldGeneration() { + return false; + } + + @Override + public int getTrueBiomeBaseId(Object biomeBase) { + return 0; + } + + @Override + public Object getTrueBiomeBase(Location location) { + return null; + } + + @Override + public String getTrueBiomeBaseKey(Location location) { + return null; + } + + @Override + public Object getCustomBiomeBaseFor(String mckey) { + return null; + } + + @Override + public Object getCustomBiomeBaseHolderFor(String mckey) { + return null; + } + + @Override + public int getBiomeBaseIdForKey(String key) { + return 0; + } + + @Override + public String getKeyForBiomeBase(Object biomeBase) { + return null; + } + + public Object getBiomeBase(World world, Biome biome) { + return null; + } + + @Override + public Object getBiomeBase(Object registry, Biome biome) { + return null; + } + + @Override + public KList getBiomes() { + KList biomes = new KList<>(); + for (Biome biome : Registry.BIOME) { + biomes.add(biome); + } + return biomes; + } + + @Override + public KList getStructureKeys() { + throw unsupportedStructureHook("read registered structure keys"); + } + + @Override + public KList getStructureSetKeys() { + throw unsupportedStructureHook("read registered structure-set keys"); + } + + @Override + public KList getReachableStructureKeys(World world) { + throw unsupportedStructureHook("resolve reachable structures"); + } + + @Override + public KList getStructureBiomeKeys(String structureKey) { + throw unsupportedStructureHook("resolve structure biome keys"); + } + + @Override + public KList getPossibleBiomeKeys(World world) { + throw unsupportedStructureHook("resolve possible biome keys"); + } + + @Override + public DataVersion getDataVersion() { + return DataVersion.UNSUPPORTED; + } + + @Override + public int getBiomeId(Biome biome) { + List biomes = StreamSupport.stream(Registry.BIOME.spliterator(), false).toList(); + int index = biomes.indexOf(biome); + return Math.max(index, 0); + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max) { + IrisLogging.error("Cannot use the custom biome data! Iris is incapable of using MCA generation on this version of minecraft!"); + + return null; + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max, int[] v) { + IrisLogging.error("Cannot use the custom biome data! Iris is incapable of using MCA generation on this version of minecraft!"); + + return null; + } + + @Override + public int countCustomBiomes() { + return 0; + } + + @Override + public Vector3d getBoundingbox(org.bukkit.entity.EntityType entity) { + return null; + } + + @Override + public String getEntitySpawnCategory(String key) { + return null; + } + + @Override + public MCAPaletteAccess createPalette() { + IrisLogging.error("Cannot use the global data palette! Iris is incapable of using MCA generation on this version of minecraft!"); + return null; + } + + private IllegalStateException unsupportedStructureHook(String operation) { + return new IllegalStateException("Iris cannot " + operation + " with limited NMS binding " + + getClass().getSimpleName() + + "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)"); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/AtomicDirectoryPublisher.java b/core/src/main/java/art/arcane/iris/core/pack/AtomicDirectoryPublisher.java new file mode 100644 index 000000000..f9237e78e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/AtomicDirectoryPublisher.java @@ -0,0 +1,130 @@ +package art.arcane.iris.core.pack; + +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Comparator; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Stream; + +public final class AtomicDirectoryPublisher { + private AtomicDirectoryPublisher() { + } + + public static Publication publish(Path stagedDirectory, Path targetDirectory) throws IOException { + Path staged = Objects.requireNonNull(stagedDirectory, "stagedDirectory").toAbsolutePath().normalize(); + Path target = Objects.requireNonNull(targetDirectory, "targetDirectory").toAbsolutePath().normalize(); + if (!Files.isDirectory(staged) || Files.isSymbolicLink(staged)) { + throw new IOException("Staged directory is missing or unsafe: " + staged); + } + if (!Objects.equals(staged.getParent(), target.getParent())) { + throw new IOException("Staged and target directories must have the same parent."); + } + + Path backup = null; + if (Files.exists(target) || Files.isSymbolicLink(target)) { + backup = target.resolveSibling("." + target.getFileName() + ".backup-" + UUID.randomUUID()); + move(target, backup); + } + try { + move(staged, target); + return new Publication(target, backup); + } catch (IOException failure) { + if (backup != null && (Files.exists(backup) || Files.isSymbolicLink(backup))) { + try { + move(backup, target); + } catch (IOException rollbackFailure) { + failure.addSuppressed(rollbackFailure); + } + } + throw failure; + } + } + + public static Publication publishAbsent(Path stagedDirectory, Path targetDirectory) throws IOException { + Path staged = Objects.requireNonNull(stagedDirectory, "stagedDirectory").toAbsolutePath().normalize(); + Path target = Objects.requireNonNull(targetDirectory, "targetDirectory").toAbsolutePath().normalize(); + if (!Files.isDirectory(staged) || Files.isSymbolicLink(staged)) { + throw new IOException("Staged directory is missing or unsafe: " + staged); + } + if (!Objects.equals(staged.getParent(), target.getParent())) { + throw new IOException("Staged and target directories must have the same parent."); + } + if (Files.exists(target) || Files.isSymbolicLink(target)) { + throw new FileAlreadyExistsException("Publication target already exists: " + target); + } + move(staged, target); + return new Publication(target, null); + } + + private static void move(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException exception) { + Files.move(source, target); + } + } + + public static final class Publication implements AutoCloseable { + private final Path target; + private final Path backup; + private boolean committed; + private boolean closed; + + private Publication(Path target, Path backup) { + this.target = target; + this.backup = backup; + } + + public synchronized void commit() { + if (closed) { + throw new IllegalStateException("Directory publication is already closed."); + } + committed = true; + } + + public synchronized void cleanupBackup() throws IOException { + if (!committed) { + throw new IllegalStateException("Directory publication is not committed."); + } + if (backup != null) { + deleteTree(backup); + } + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (committed) { + return; + } + + deleteTree(target); + if (backup != null && (Files.exists(backup) || Files.isSymbolicLink(backup))) { + move(backup, target); + } + } + } + + public static void deleteTree(Path path) throws IOException { + if (!Files.exists(path) && !Files.isSymbolicLink(path)) { + return; + } + if (Files.isSymbolicLink(path) || !Files.isDirectory(path)) { + Files.delete(path); + return; + } + try (Stream stream = Files.walk(path)) { + for (Path entry : stream.sorted(Comparator.reverseOrder()).toList()) { + Files.deleteIfExists(entry); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/BrokenPackException.java b/core/src/main/java/art/arcane/iris/core/pack/BrokenPackException.java new file mode 100644 index 000000000..898aed374 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/BrokenPackException.java @@ -0,0 +1,56 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class BrokenPackException extends RuntimeException { + private final String packName; + private final List reasons; + + public BrokenPackException(String packName, List reasons) { + super(buildMessage(packName, reasons)); + this.packName = packName; + this.reasons = reasons == null ? new ArrayList<>() : new ArrayList<>(reasons); + } + + public String getPackName() { + return packName; + } + + public List getReasons() { + return Collections.unmodifiableList(reasons); + } + + private static String buildMessage(String packName, List reasons) { + StringBuilder sb = new StringBuilder(); + sb.append("Iris pack '").append(packName).append("' is broken and cannot be used for world or studio creation."); + if (reasons != null) { + for (String reason : reasons) { + if (reason == null || reason.isBlank()) { + continue; + } + sb.append(System.lineSeparator()).append(" - ").append(reason); + } + } + return sb.toString(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java b/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java new file mode 100644 index 000000000..7d34f66c9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java @@ -0,0 +1,629 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.object.IrisObjectIO; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockProperty; +import art.arcane.iris.spi.PlatformNumericRange; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +public final class ContentKeyValidator { + private static final int MAX_SUGGESTION_SCANS = 4096; + private static final int MAX_OBJECT_PALETTE_SCANS = 20_000; + private static final String DEFAULT_NAMESPACE = "minecraft"; + private static final String STRICT_PROPERTY = "iris.strictContent"; + private static final AtomicBoolean WARNED_EMPTY_REGISTRIES = new AtomicBoolean(); + + private ContentKeyValidator() { + } + + public enum ContentRegistry { + BLOCK, + ITEM, + ENTITY; + + public String label() { + return name().toLowerCase(Locale.ROOT); + } + } + + public record ContentKeyError(String key, ContentRegistry registry, boolean namespaceLoaded, String suggestion) { + public String message() { + StringBuilder sb = new StringBuilder(64); + sb.append("Unknown ").append(registry.label()).append(" key '").append(key).append('\''); + sb.append(" (missing from the ").append(registry.label()).append(" registry"); + if (!namespaceLoaded) { + sb.append("; namespace '").append(namespaceOf(key)).append("' is not loaded on this server"); + } + sb.append(')'); + if (suggestion != null) { + sb.append(" - did you mean '").append(suggestion).append("'?"); + } + return sb.toString(); + } + } + + public static List validate(PlatformRegistries registries, + Collection referencedBlocks, + Collection referencedItems, + Collection referencedEntities) { + if (registries == null) { + return List.of(); + } + + List blockKeys = normalizeKeyList(registries.blockKeys()); + List itemKeys = normalizeKeyList(registries.itemKeys()); + List entityKeys = normalizeKeyList(registries.entityKeys()); + + Set knownBlocks = new HashSet<>(blockKeys); + Set knownItems = new HashSet<>(itemKeys); + Set knownEntities = new HashSet<>(entityKeys); + + Set loadedNamespaces = new HashSet<>(); + addNamespaces(blockKeys, loadedNamespaces); + addNamespaces(itemKeys, loadedNamespaces); + addNamespaces(entityKeys, loadedNamespaces); + + Map errors = new LinkedHashMap<>(); + validateCategory(referencedBlocks, ContentRegistry.BLOCK, knownBlocks, blockKeys, loadedNamespaces, errors); + validateCategory(referencedItems, ContentRegistry.ITEM, knownItems, itemKeys, loadedNamespaces, errors); + validateCategory(referencedEntities, ContentRegistry.ENTITY, knownEntities, entityKeys, loadedNamespaces, errors); + return List.copyOf(errors.values()); + } + + /** + * Validates the {@code [prop=value,...]} section of every referenced block state against the platform's declared + * properties for that block. + *

+ * Only blocks the platform declares at least one property for are checked: a block key that is absent from + * {@link PlatformRegistries#blockStateProperties()}, or present with an empty list, carries no property knowledge + * (pack-registered blocks and mod provider blocks land there) and a typo cannot be told from a valid custom + * property. + */ + public static List validateBlockStateProperties(PlatformRegistries registries, + Collection referencedBlockStates) { + if (registries == null || referencedBlockStates == null || referencedBlockStates.isEmpty()) { + return List.of(); + } + Map> declared = registries.blockStateProperties(); + if (declared == null || declared.isEmpty()) { + return List.of(); + } + + List messages = new ArrayList<>(); + Set seen = new HashSet<>(); + for (String raw : referencedBlockStates) { + String properties = propertySectionOf(raw); + if (properties == null || properties.isBlank()) { + continue; + } + String base = normalizeKey(raw); + if (base == null) { + continue; + } + List known = declared.get(base); + if (known == null || known.isEmpty()) { + continue; + } + for (String pair : properties.split(",")) { + int equals = pair.indexOf('='); + if (equals <= 0) { + continue; + } + String name = pair.substring(0, equals).trim(); + String value = pair.substring(equals + 1).trim(); + if (name.isEmpty()) { + continue; + } + String message = describeProperty(base, name, value, known); + if (message != null && seen.add(message)) { + messages.add(message); + } + } + } + return List.copyOf(messages); + } + + private static String describeProperty(String base, String name, String value, List known) { + PlatformBlockProperty match = null; + List names = new ArrayList<>(known.size()); + for (PlatformBlockProperty property : known) { + names.add(property.name()); + if (property.name().equalsIgnoreCase(name)) { + match = property; + } + } + + if (match == null) { + String suggestion = nearestKey(name, names); + StringBuilder sb = new StringBuilder(96); + sb.append("Block '").append(base).append("' has no property '").append(name).append('\''); + if (suggestion != null) { + sb.append(" - did you mean '").append(suggestion).append("'?"); + } else { + sb.append(" (has: ").append(String.join(", ", names)).append(')'); + } + return sb.toString(); + } + + List allowed = match.allowedValues(); + if (allowed == null || allowed.isEmpty()) { + // The adapters disagree on how a numeric property describes itself: modded enumerates every legal + // integer into allowedValues, Bukkit leaves that empty and publishes bounds instead. Validating the + // range here is what makes 'minecraft:water[level=99]' report the same thing on both platforms. + return describeNumericRange(base, match, value); + } + List allowedText = new ArrayList<>(allowed.size()); + for (Object candidate : allowed) { + String text = String.valueOf(candidate); + allowedText.add(text); + if (text.equalsIgnoreCase(value)) { + return null; + } + } + return "Block '" + base + "' property '" + match.name() + "' does not accept '" + value + + "' (allowed: " + String.join(", ", allowedText) + ")"; + } + + /** + * Range check for a property that declares bounds but cannot enumerate its values. A value that is not a number + * at all is reported too - a numeric property never accepts one. + */ + private static String describeNumericRange(String base, PlatformBlockProperty match, String value) { + if (!match.hasNumericRange()) { + return null; + } + PlatformNumericRange range = match.numericRange(); + String bounds = "expected " + + (range.exclusiveMinimum() ? "greater than " : "at least ") + describeBound(range.minimum()) + + " and " + (range.exclusiveMaximum() ? "less than " : "at most ") + describeBound(range.maximum()); + + double parsed; + try { + parsed = Double.parseDouble(value); + } catch (NumberFormatException e) { + return "Block '" + base + "' property '" + match.name() + "' is numeric and does not accept '" + value + + "' (" + bounds + ")"; + } + + boolean belowMinimum = range.exclusiveMinimum() ? parsed <= range.minimum() : parsed < range.minimum(); + boolean aboveMaximum = range.exclusiveMaximum() ? parsed >= range.maximum() : parsed > range.maximum(); + if (!belowMinimum && !aboveMaximum) { + return null; + } + return "Block '" + base + "' property '" + match.name() + "' does not accept '" + value + "' (" + bounds + ")"; + } + + private static String describeBound(double bound) { + return bound == Math.rint(bound) && !Double.isInfinite(bound) + ? String.valueOf((long) bound) + : String.valueOf(bound); + } + + static String propertySectionOf(String raw) { + if (raw == null) { + return null; + } + int open = raw.indexOf('['); + if (open < 0) { + return null; + } + int close = raw.indexOf(']', open); + return close < 0 ? raw.substring(open + 1) : raw.substring(open + 1, close); + } + + /** + * Whether unresolved pack content keys are blocking errors instead of warnings. Enabled by + * {@code -Diris.strictContent} or {@code general.strictContentKeys} in settings.json; the system property wins. + */ + public static boolean strictContent() { + String property = System.getProperty(STRICT_PROPERTY); + if (property != null) { + return property.isEmpty() || Boolean.parseBoolean(property); + } + try { + return IrisPlatforms.isBound() && IrisSettings.get().getGeneral().isStrictContentKeys(); + } catch (Throwable e) { + return false; + } + } + + static String namespaceOf(String key) { + int colon = key.indexOf(':'); + return colon < 0 ? DEFAULT_NAMESPACE : key.substring(0, colon); + } + + private static void validateCategory(Collection referenced, + ContentRegistry registry, + Set known, + List candidatePool, + Set loadedNamespaces, + Map errors) { + if (referenced == null || referenced.isEmpty()) { + return; + } + for (String raw : referenced) { + String normalized = normalizeKey(raw); + if (normalized == null || known.contains(normalized)) { + continue; + } + String dedupKey = registry.name() + '|' + normalized; + if (errors.containsKey(dedupKey)) { + continue; + } + boolean namespaceLoaded = loadedNamespaces.contains(namespaceOf(normalized)); + String suggestion = nearestKey(normalized, candidatePool); + errors.put(dedupKey, new ContentKeyError(normalized, registry, namespaceLoaded, suggestion)); + } + } + + private static List normalizeKeyList(List keys) { + if (keys == null || keys.isEmpty()) { + return List.of(); + } + List out = new ArrayList<>(keys.size()); + for (String key : keys) { + if (key == null || key.isEmpty()) { + continue; + } + String value = key.trim().toLowerCase(Locale.ROOT); + if (!value.isEmpty()) { + out.add(value.indexOf(':') < 0 ? DEFAULT_NAMESPACE + ':' + value : value); + } + } + return out; + } + + private static void addNamespaces(List keys, Set namespaces) { + for (String key : keys) { + namespaces.add(namespaceOf(key)); + } + } + + private static String normalizeKey(String raw) { + if (raw == null) { + return null; + } + String value = raw.trim().toLowerCase(Locale.ROOT); + int bracket = value.indexOf('['); + if (bracket >= 0) { + value = value.substring(0, bracket).trim(); + } + if (value.isEmpty()) { + return null; + } + return value.indexOf(':') < 0 ? DEFAULT_NAMESPACE + ':' + value : value; + } + + private static String nearestKey(String key, List candidatePool) { + String path = pathOf(key); + int maxDistance = Math.max(2, path.length() / 3); + int bestDistance = Integer.MAX_VALUE; + String best = null; + int scans = 0; + + for (String candidate : candidatePool) { + String candidatePath = pathOf(candidate); + if (Math.abs(candidatePath.length() - path.length()) > maxDistance) { + continue; + } + if (scans++ >= MAX_SUGGESTION_SCANS) { + break; + } + int distance = boundedLevenshtein(path, candidatePath, maxDistance); + if (distance < bestDistance) { + bestDistance = distance; + best = candidate; + if (distance <= 1) { + break; + } + } + } + + return bestDistance <= maxDistance ? best : null; + } + + private static String pathOf(String key) { + int colon = key.indexOf(':'); + return colon < 0 ? key : key.substring(colon + 1); + } + + private static int boundedLevenshtein(String a, String b, int max) { + int la = a.length(); + int lb = b.length(); + if (Math.abs(la - lb) > max) { + return max + 1; + } + int[] prev = new int[lb + 1]; + int[] curr = new int[lb + 1]; + for (int j = 0; j <= lb; j++) { + prev[j] = j; + } + for (int i = 1; i <= la; i++) { + curr[0] = i; + int rowMin = curr[0]; + char ca = a.charAt(i - 1); + for (int j = 1; j <= lb; j++) { + int cost = ca == b.charAt(j - 1) ? 0 : 1; + curr[j] = Math.min(Math.min(prev[j] + 1, curr[j - 1] + 1), prev[j - 1] + cost); + if (curr[j] < rowMin) { + rowMin = curr[j]; + } + } + if (rowMin > max) { + return max + 1; + } + int[] tmp = prev; + prev = curr; + curr = tmp; + } + return prev[lb]; + } + + /** + * Unresolved-key and block-property issues for a pack, split by how hard they are allowed to bite. + * + * @param strict issues the caller may promote to blocking errors under {@link #strictContent()} + * @param advisory issues that stay warnings whatever the mode - see {@link #collectContentKeyIssues(File)} + */ + record ContentKeyIssues(List strict, List advisory) { + static ContentKeyIssues none() { + return new ContentKeyIssues(List.of(), List.of()); + } + } + + /** + * Collects unresolved-key and block-property issues for a pack. Messages come back in report order; the caller + * decides whether the {@link ContentKeyIssues#strict()} half is warnings or blocking errors (see + * {@link #strictContent()}). + *

+ * Findings whose only source is an object palette are {@link ContentKeyIssues#advisory()} and never blocking. A + * {@code .iob} palette is a build artifact, not authored text: it carries whatever key the block had when the + * object was saved, so a decade-old community object legitimately names {@code minecraft:grass} or + * {@code minecraft:grass_path}. Those resolve at generation time through the Bukkit compatibility rewrite table + * and are already reported as warnings; refusing to load the pack over them would reject most of the object + * library over keys the author cannot edit and the engine handles. + */ + static ContentKeyIssues collectContentKeyIssues(File packFolder) { + List strict = new ArrayList<>(); + List advisory = new ArrayList<>(); + try { + if (!IrisPlatforms.isBound()) { + return ContentKeyIssues.none(); + } + PlatformRegistries registries = IrisPlatforms.get().registries(); + if (registries == null) { + return ContentKeyIssues.none(); + } + List blockKeys = registries.blockKeys(); + List itemKeys = registries.itemKeys(); + List entityKeys = registries.entityKeys(); + if (blockKeys == null || blockKeys.isEmpty() || itemKeys == null || itemKeys.isEmpty() || entityKeys == null || entityKeys.isEmpty()) { + if (WARNED_EMPTY_REGISTRIES.compareAndSet(false, true)) { + IrisLogging.warn("Content-key validation skipped: platform registries are empty (blocks=" + + size(blockKeys) + " items=" + size(itemKeys) + " entities=" + size(entityKeys) + ")"); + } + return ContentKeyIssues.none(); + } + + ReferencedContentKeys referenced = collectReferencedContentKeys(packFolder); + List errors = ContentKeyValidator.validate( + registries, referenced.blocks(), referenced.items(), referenced.entities()); + for (ContentKeyValidator.ContentKeyError error : errors) { + if (referenced.paletteOnlyBlocks().contains(error.key())) { + advisory.add(error.message()); + } else { + strict.add(error.message()); + } + } + strict.addAll(validateBlockStateProperties(registries, referenced.blockStates())); + } catch (Throwable e) { + IrisLogging.reportError("PackValidator content-key validation failed for pack '" + packFolder.getName() + "'", e); + } + return new ContentKeyIssues(List.copyOf(strict), List.copyOf(advisory)); + } + + private static int size(List keys) { + return keys == null ? 0 : keys.size(); + } + + private static ReferencedContentKeys collectReferencedContentKeys(File packFolder) { + Set blocks = new HashSet<>(); + Set items = new HashSet<>(); + Set entities = new HashSet<>(); + Set blockStates = new HashSet<>(); + Set customBlocks = deriveRegistrantKeys(new File(packFolder, "blocks")); + + try (Stream stream = Files.walk(packFolder.toPath())) { + List files = stream.filter(Files::isRegularFile) + .filter(PackValidationIo::isScannableJsonPath) + .toList(); + for (Path path : files) { + String relative = packFolder.toPath().relativize(path).toString().replace(File.separatorChar, '/'); + boolean inLoot = relative.startsWith("loot/"); + boolean inEntities = relative.startsWith("entities/"); + JSONObject json; + try { + json = new JSONObject(Files.readString(path, StandardCharsets.UTF_8)); + } catch (Throwable ignored) { + continue; + } + collectFromNode(json, blocks, inLoot ? items : null, inEntities ? entities : null, customBlocks, blockStates); + } + } catch (Throwable e) { + IrisLogging.reportError("PackValidator failed to walk pack for content-key extraction", e); + } + + Set jsonBlocks = Set.copyOf(blocks); + collectObjectPaletteKeys(new File(packFolder, PackValidator.OBJECTS_FOLDER), blocks, blockStates, customBlocks); + + // Keys the JSON scan never saw came only out of an .iob palette. Those stay advisory - the pack author has + // no text to fix. + Set paletteOnly = new HashSet<>(blocks); + paletteOnly.removeAll(jsonBlocks); + + return new ReferencedContentKeys(blocks, items, entities, blockStates, paletteOnly); + } + + /** + * Adds the V2 {@code .iob} palette keys under {@code objects/} to the referenced block keys. Object palettes are + * the largest source of block keys in a pack and are invisible to the JSON scan. + */ + private static void collectObjectPaletteKeys(File objectsFolder, Set blocks, Set blockStates, Set customBlocks) { + if (!objectsFolder.isDirectory()) { + return; + } + int scanned = 0; + try (Stream stream = Files.walk(objectsFolder.toPath())) { + List files = stream.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".iob")) + .toList(); + for (Path path : files) { + if (scanned++ >= MAX_OBJECT_PALETTE_SCANS) { + IrisLogging.debug("Content-key validation stopped object palette scan at " + MAX_OBJECT_PALETTE_SCANS + + " objects (" + files.size() + " present)"); + break; + } + for (String key : IrisObjectIO.readPaletteKeys(path.toFile())) { + addBlockRef(key, blocks, customBlocks, blockStates); + } + } + } catch (Throwable e) { + IrisLogging.reportError("PackValidator failed to scan object palettes for content-key extraction", e); + } + } + + private static void collectFromNode(Object node, Set blocks, Set items, Set entities, Set customBlocks, Set blockStates) { + if (node instanceof JSONObject obj) { + for (String key : obj.keySet()) { + Object value = obj.get(key); + if (value instanceof String str) { + if ("block".equals(key)) { + addBlockRef(str, blocks, customBlocks, blockStates); + } else if (items != null && "type".equals(key)) { + addSimpleRef(str, items); + } else if (entities != null && "type".equals(key)) { + addSimpleRef(str, entities); + } + } else { + collectFromNode(value, blocks, items, entities, customBlocks, blockStates); + } + } + } else if (node instanceof JSONArray arr) { + for (int i = 0; i < arr.length(); i++) { + collectFromNode(arr.get(i), blocks, items, entities, customBlocks, blockStates); + } + } + } + + private static void addBlockRef(String raw, Set blocks, Set customBlocks, Set blockStates) { + String value = raw.trim().toLowerCase(Locale.ROOT); + int bracket = value.indexOf('['); + String base = bracket >= 0 ? value.substring(0, bracket).trim() : value; + if (base.isEmpty() || customBlocks.contains(base)) { + return; + } + blocks.add(base); + if (bracket >= 0 && blockStates != null) { + blockStates.add(value); + } + } + + private static void addSimpleRef(String raw, Set target) { + String value = raw.trim().toLowerCase(Locale.ROOT); + if (!value.isEmpty()) { + target.add(value); + } + } + + private static Set deriveRegistrantKeys(File folder) { + Set keys = new HashSet<>(); + if (!folder.isDirectory()) { + return keys; + } + for (File file : PackValidationIo.listJsonRecursive(folder)) { + String key = PackValidationIo.deriveKey(folder, file); + if (key != null && !key.isBlank()) { + keys.add(key.toLowerCase(Locale.ROOT)); + } + } + return keys; + } + + static Set deriveRegistrantKeysExact(File folder) { + Set keys = new HashSet<>(); + if (!folder.isDirectory()) { + return keys; + } + for (File file : PackValidationIo.listJsonRecursive(folder)) { + String key = PackValidationIo.deriveKey(folder, file); + if (key != null && !key.isBlank()) { + keys.add(key); + } + } + return keys; + } + + static Set deriveObjectKeysExact(File folder) { + Set keys = new HashSet<>(); + if (!folder.isDirectory()) { + return keys; + } + try (Stream stream = Files.walk(folder.toPath())) { + stream.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".iob")) + .forEach(path -> { + Path relative = folder.toPath().relativize(path); + String key = relative.toString().replace(File.separatorChar, '/'); + keys.add(key.substring(0, key.length() - ".iob".length())); + }); + } catch (IOException ignored) { + } + return keys; + } + + /** + * @param paletteOnlyBlocks the subset of {@code blocks} that no pack JSON names - contributed purely by an + * object palette, so findings about them cannot be blocking + */ + private record ReferencedContentKeys(Set blocks, Set items, Set entities, + Set blockStates, Set paletteOnlyBlocks) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/DefaultPackBootstrapProvisioner.java b/core/src/main/java/art/arcane/iris/core/pack/DefaultPackBootstrapProvisioner.java new file mode 100644 index 000000000..6251b1bbd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/DefaultPackBootstrapProvisioner.java @@ -0,0 +1,980 @@ +package art.arcane.iris.core.pack; + +import art.arcane.iris.core.IrisDatapackCompiler; +import art.arcane.iris.core.lifecycle.BukkitStartupPaths; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding; +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.core.nms.datapack.IDataFixer; +import art.arcane.volmlib.util.collection.KList; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public final class DefaultPackBootstrapProvisioner { + private static final List DEFAULT_PACKS = List.of(); + private static final String WORLD_DATAPACK_DIRECTORY = "iris"; + private static final int MARKER_SCHEMA = 6; + private static final int MAX_ARCHIVE_ENTRIES = 100_000; + private static final long MAX_ARCHIVE_BYTES = 512L * 1024L * 1024L; + private static final long MAX_EXPANDED_BYTES = 2L * 1024L * 1024L * 1024L; + private static final AtomicBoolean PROVISIONED_THIS_STARTUP = new AtomicBoolean(false); + private static final AtomicReference PROVISIONED_COMPILER_INPUT_FINGERPRINT = + new AtomicReference<>(""); + + private DefaultPackBootstrapProvisioner() { + } + + static List defaultPacks() { + return DEFAULT_PACKS; + } + + public static ProvisionResult provision(Path dataDirectory, Consumer feedback) throws IOException { + return provision(dataDirectory, feedback, BukkitStartupPaths.resolveCurrent()); + } + + public static ProvisionResult provision( + Path dataDirectory, + Consumer feedback, + BukkitStartupPaths startupPaths + ) throws IOException { + Objects.requireNonNull(dataDirectory, "dataDirectory"); + Objects.requireNonNull(feedback, "feedback"); + BukkitStartupPaths requiredStartupPaths = Objects.requireNonNull(startupPaths, "startupPaths"); + Path levelRoot = requiredStartupPaths.levelRoot(); + List bindings = BukkitWorldConfiguration.readIrisGeneratorBindings( + requiredStartupPaths.bukkitConfiguration().toFile(), + levelId(levelRoot), + requiredStartupPaths.levelRoot() + ); + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(15)) + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); + ProvisionOptions options = new ProvisionOptions( + DEFAULT_PACKS, + bindings, + client, + Clock.systemUTC(), + Duration.ofMinutes(30), + Duration.ofSeconds(45), + 3, + Duration.ofMillis(250), + MAX_ARCHIVE_BYTES, + levelRoot + ); + return provision(dataDirectory, feedback, options); + } + + public static boolean isProvisioned(Path dataDirectory) { + if (dataDirectory == null) { + return false; + } + try { + BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent(); + List bindings = BukkitWorldConfiguration.readIrisGeneratorBindings( + startupPaths.bukkitConfiguration().toFile(), + levelId(startupPaths.levelRoot()), + startupPaths.levelRoot() + ); + return isProvisioned(dataDirectory, startupPaths.levelRoot(), DEFAULT_PACKS, bindings); + } catch (IOException | RuntimeException exception) { + return false; + } + } + + static boolean isProvisioned( + Path dataDirectory, + Path levelRoot, + List requiredPacks, + List bindings + ) { + try { + Path normalizedData = dataDirectory.toAbsolutePath().normalize(); + Path normalizedLevel = levelRoot.toAbsolutePath().normalize(); + Path bootstrapRoot = normalizedData.resolve("bootstrap"); + Path datapackRoot = worldDatapackRoot(normalizedLevel); + Path markerFile = bootstrapRoot.resolve("provisioned.properties"); + if (!Files.isRegularFile(markerFile) || !isDatapackRoot(datapackRoot)) { + return false; + } + Properties marker = loadProperties(markerFile); + if (!Integer.toString(MARKER_SCHEMA).equals(marker.getProperty("schema"))) { + return false; + } + IDataFixer fixer = DataVersion.getLatest().get(); + if (fixer == null + || !IrisDatapackCompiler.compilerIdentity(fixer) + .equals(marker.getProperty("compilerIdentity"))) { + return false; + } + for (PackSpec spec : requiredPacks) { + Path packRoot = normalizedData.resolve("packs").resolve(spec.key()); + if (!isPackRoot(packRoot, spec) + || !spec.source().toString().equals(marker.getProperty(markerKey(spec, "source"))) + || !spec.requiredDimension().equals(marker.getProperty(markerKey(spec, "requiredDimension"))) + || !directoryFingerprint(packRoot).equals(marker.getProperty(markerKey(spec, "fingerprint")))) { + return false; + } + } + List packRoots = IrisDatapackCompiler.collectPackRoots( + normalizedData, + normalizedLevel + ); + return directoryFingerprint(datapackRoot).equals(marker.getProperty("datapackFingerprint")) + && datapackRoot.toString().equals(marker.getProperty("datapackPath")) + && packRootsFingerprint(packRoots).equals(marker.getProperty("aggregateFingerprint")) + && IrisDatapackCompiler.computeInputFingerprint( + packRoots, + bindings, + fixer, + false + ).equals(marker.getProperty("compilerInputFingerprint")); + } catch (IOException | RuntimeException exception) { + return false; + } + } + + public static boolean wasProvisionedThisStartup() { + return PROVISIONED_THIS_STARTUP.get(); + } + + public static String compilerInputFingerprintThisStartup() { + return PROVISIONED_COMPILER_INPUT_FINGERPRINT.get(); + } + + static ProvisionResult provision(Path dataDirectory, Consumer feedback, ProvisionOptions options) throws IOException { + PROVISIONED_THIS_STARTUP.set(false); + PROVISIONED_COMPILER_INPUT_FINGERPRINT.set(""); + Path normalizedData = dataDirectory.toAbsolutePath().normalize(); + Path packsRoot = normalizedData.resolve("packs"); + Path bootstrapRoot = normalizedData.resolve("bootstrap"); + Path legacyDatapackRoot = bootstrapRoot.resolve("datapack"); + Path datapacksRoot = options.levelRoot().toAbsolutePath().normalize().resolve("datapacks"); + Path datapackRoot = datapacksRoot.resolve(WORLD_DATAPACK_DIRECTORY); + Path markerFile = bootstrapRoot.resolve("provisioned.properties"); + Path cacheRoot = normalizedData.resolve("cache/bootstrap"); + Files.createDirectories(packsRoot); + Files.createDirectories(bootstrapRoot); + Files.createDirectories(cacheRoot); + Files.createDirectories(datapacksRoot); + + Properties previousMarker = Files.isRegularFile(markerFile) ? loadProperties(markerFile) : new Properties(); + boolean existingDatapack = isDatapackRoot(datapackRoot); + List plans = new ArrayList<>(options.packs().size()); + for (PackSpec spec : options.packs()) { + Path packRoot = packsRoot.resolve(spec.key()).normalize(); + if (!Objects.equals(packRoot.getParent(), packsRoot)) { + throw new IOException("Bootstrap pack target escapes the packs directory: " + packRoot); + } + boolean existingPack = isPackRoot(packRoot, spec); + String currentFingerprint = existingPack ? directoryFingerprint(packRoot) : ""; + boolean markerOwned = "true".equals(markerProperty(previousMarker, spec, "managed")); + boolean unchangedManagedPack = markerOwned + && currentFingerprint.equals(markerProperty(previousMarker, spec, "fingerprint")); + boolean managed = !existingPack || unchangedManagedPack; + if (Files.isSymbolicLink(packRoot)) { + managed = false; + } + String retainedSourceSha = markerProperty(previousMarker, spec, "sourceSha256"); + Archive archive = managed + ? acquireArchive(cacheRoot, previousMarker, spec, feedback, options) + : new Archive(null, retainedSourceSha == null || retainedSourceSha.isBlank() + ? currentFingerprint + : retainedSourceSha); + boolean replacePack = !existingPack || managed + && (!archive.sha256().equals(markerProperty(previousMarker, spec, "sourceSha256")) + || !currentFingerprint.equals(markerProperty(previousMarker, spec, "fingerprint"))); + plans.add(new PackPlan(spec, packRoot, existingPack, managed, archive, replacePack)); + } + + Map stagedPacks = new LinkedHashMap<>(); + List extractionRoots = new ArrayList<>(); + Path compileContainer = null; + Path datapackBackup = null; + boolean datapackReplaced = false; + List packPublications = new ArrayList<>(); + boolean committed = false; + try { + for (PackPlan plan : plans) { + if (!plan.replace()) { + continue; + } + Path extractionRoot = cacheRoot.resolve(".extract-" + plan.spec().key() + "-" + UUID.randomUUID()); + extractionRoots.add(extractionRoot); + Files.createDirectories(extractionRoot); + Path extractedPack = extractArchive(plan.archive().path(), extractionRoot, plan.spec()); + Path stagedPack = packsRoot.resolve("." + plan.spec().key() + "-stage-" + UUID.randomUUID()); + copyDirectory(extractedPack, stagedPack); + validatePackRoot(stagedPack, plan.spec()); + stagedPacks.put(plan, stagedPack); + } + for (Map.Entry entry : stagedPacks.entrySet()) { + Path backup = replaceWithBackup(entry.getValue(), entry.getKey().root()); + packPublications.add(new PackPublication(entry.getKey().root(), backup)); + } + + List packRoots = IrisDatapackCompiler.collectPackRoots(normalizedData, options.levelRoot()); + IDataFixer fixer = DataVersion.getLatest().get(); + if (fixer == null) { + throw new IOException("Latest Iris datapack fixer is unavailable during bootstrap"); + } + String compilerIdentity = IrisDatapackCompiler.compilerIdentity(fixer); + String aggregateFingerprint = packRootsFingerprint(packRoots); + String compilerInputFingerprint = IrisDatapackCompiler.computeInputFingerprint( + packRoots, + options.bindings(), + fixer, + false + ); + boolean anyPackReplaced = !packPublications.isEmpty(); + boolean rebuildDatapack = anyPackReplaced + || !existingDatapack + || !aggregateFingerprint.equals(previousMarker.getProperty("aggregateFingerprint")) + || !compilerInputFingerprint.equals(previousMarker.getProperty("compilerInputFingerprint")) + || !compilerIdentity.equals(previousMarker.getProperty("compilerIdentity")) + || !datapackRoot.toString().equals(previousMarker.getProperty("datapackPath")) + || !directoryFingerprint(datapackRoot).equals(previousMarker.getProperty("datapackFingerprint")); + if (rebuildDatapack) { + compileContainer = datapacksRoot.resolve("." + WORLD_DATAPACK_DIRECTORY + "-stage-" + UUID.randomUUID()); + Files.createDirectories(compileContainer); + KList outputFolders = new KList().qadd(compileContainer.toFile()); + IrisDatapackCompiler.compile(packRoots, outputFolders, options.bindings(), fixer, false); + if (!isDatapackRoot(compileContainer, !packRoots.isEmpty())) { + throw new IOException("Canonical Iris datapack compiler produced incomplete output at " + compileContainer); + } + datapackBackup = replaceWithBackup(compileContainer, datapackRoot); + compileContainer = null; + datapackReplaced = true; + } + + for (PackPlan plan : plans) { + validatePackRoot(plan.root(), plan.spec()); + } + List finalPackRoots = IrisDatapackCompiler.collectPackRoots( + normalizedData, + options.levelRoot()); + if (!isDatapackRoot(datapackRoot, !finalPackRoots.isEmpty())) { + throw new IOException("Bootstrap datapack output is incomplete at " + datapackRoot); + } + String finalAggregateFingerprint = packRootsFingerprint(finalPackRoots); + String finalCompilerInputFingerprint = IrisDatapackCompiler.computeInputFingerprint( + finalPackRoots, + options.bindings(), + fixer, + false); + String finalDatapackFingerprint = directoryFingerprint(datapackRoot); + Properties marker = new Properties(); + marker.setProperty("schema", Integer.toString(MARKER_SCHEMA)); + for (PackPlan plan : plans) { + marker.setProperty(markerKey(plan.spec(), "source"), plan.spec().source().toString()); + marker.setProperty(markerKey(plan.spec(), "sourceSha256"), plan.archive().sha256()); + marker.setProperty(markerKey(plan.spec(), "managed"), Boolean.toString(plan.managed())); + marker.setProperty(markerKey(plan.spec(), "fingerprint"), directoryFingerprint(plan.root())); + marker.setProperty(markerKey(plan.spec(), "requiredDimension"), plan.spec().requiredDimension()); + } + marker.setProperty("aggregateFingerprint", finalAggregateFingerprint); + marker.setProperty("compilerIdentity", compilerIdentity); + marker.setProperty("compilerInputFingerprint", finalCompilerInputFingerprint); + marker.setProperty("datapackFingerprint", finalDatapackFingerprint); + marker.setProperty("datapackPath", datapackRoot.toString()); + marker.setProperty("completedAt", Long.toString(options.clock().millis())); + storePropertiesAtomic(markerFile, marker); + committed = true; + PROVISIONED_COMPILER_INPUT_FINGERPRINT.set(finalCompilerInputFingerprint); + PROVISIONED_THIS_STARTUP.set(true); + for (PackPublication publication : packPublications) { + deleteQuietly(publication.backup(), feedback); + } + deleteQuietly(datapackBackup, feedback); + deleteQuietly(legacyDatapackRoot, feedback); + + boolean everyPackMissing = plans.stream().noneMatch(PackPlan::existed); + ProvisionStatus status; + if (everyPackMissing && !existingDatapack) { + status = ProvisionStatus.INSTALLED; + } else if (anyPackReplaced || rebuildDatapack) { + status = ProvisionStatus.UPDATED; + } else { + status = ProvisionStatus.UNCHANGED; + } + Map provisionedPacks = new LinkedHashMap<>(); + for (PackPlan plan : plans) { + provisionedPacks.put(plan.spec().key(), plan.root()); + } + return new ProvisionResult(provisionedPacks, datapackRoot, status); + } catch (IOException failure) { + if (!committed) { + IOException rollbackFailure = rollback( + packPublications, + datapackRoot, + datapackBackup, + datapackReplaced + ); + if (rollbackFailure != null) { + failure.addSuppressed(rollbackFailure); + } + } + throw failure; + } catch (RuntimeException | LinkageError failure) { + if (!committed) { + IOException rollbackFailure = rollback( + packPublications, + datapackRoot, + datapackBackup, + datapackReplaced + ); + if (rollbackFailure != null) { + failure.addSuppressed(rollbackFailure); + } + } + throw new IOException("Iris bootstrap provisioning failed", failure); + } finally { + for (Path stagedPack : stagedPacks.values()) { + deleteQuietly(stagedPack, feedback); + } + for (Path extractionRoot : extractionRoots) { + deleteQuietly(extractionRoot, feedback); + } + deleteQuietly(compileContainer, feedback); + } + } + + private static Archive acquireArchive( + Path cacheRoot, + Properties marker, + PackSpec spec, + Consumer feedback, + ProvisionOptions options + ) throws IOException { + Path archivePath = cacheRoot.resolve("default-" + spec.key() + ".zip"); + Path metadataPath = cacheRoot.resolve("default-" + spec.key() + ".properties"); + Properties metadata = Files.isRegularFile(metadataPath) ? loadProperties(metadataPath) : new Properties(); + boolean validCache = false; + if (Files.isRegularFile(archivePath)) { + try { + validCache = validateArchive(archivePath, spec); + } catch (IOException exception) { + feedback.accept("Cached Iris " + spec.key() + " beta archive is invalid; downloading a replacement."); + } + } + String cachedSource = metadata.getProperty("source", ""); + boolean cacheMatchesSource = validCache && (spec.source().toString().equals(cachedSource) + || cachedSource.isBlank() && "overworld".equals(spec.key())); + long fetchedAt = parseLong(metadata.getProperty("fetchedAt"), 0L); + boolean fresh = cacheMatchesSource + && options.clock().millis() - fetchedAt < options.refreshInterval().toMillis(); + if (fresh) { + if (cachedSource.isBlank()) { + metadata.setProperty("source", spec.source().toString()); + storePropertiesAtomic(metadataPath, metadata); + } + return new Archive(archivePath, sha256(archivePath)); + } + + IOException networkFailure = null; + for (int attempt = 1; attempt <= options.attempts(); attempt++) { + try { + HttpRequest.Builder request = HttpRequest.newBuilder(spec.source()) + .timeout(options.requestTimeout()) + .header("Accept", "application/octet-stream") + .header("User-Agent", "Iris-Bootstrap") + .GET(); + if (cacheMatchesSource) { + String etag = metadata.getProperty("etag"); + String lastModified = metadata.getProperty("lastModified"); + if (etag != null && !etag.isBlank()) { + request.header("If-None-Match", etag); + } + if (lastModified != null && !lastModified.isBlank()) { + request.header("If-Modified-Since", lastModified); + } + } + HttpResponse response = options.client().send(request.build(), HttpResponse.BodyHandlers.ofInputStream()); + int status = response.statusCode(); + if (status == 304 && cacheMatchesSource) { + close(response.body()); + metadata.setProperty("source", spec.source().toString()); + metadata.setProperty("fetchedAt", Long.toString(options.clock().millis())); + storePropertiesAtomic(metadataPath, metadata); + return new Archive(archivePath, sha256(archivePath)); + } + if (status == 200) { + Path temporary = cacheRoot.resolve(".download-" + UUID.randomUUID() + ".zip"); + try { + try (InputStream input = response.body(); OutputStream output = Files.newOutputStream(temporary)) { + copyLimited(input, output, options.maxArchiveBytes(), spec); + } + if (!validateArchive(temporary, spec)) { + throw new IOException("Downloaded Iris " + spec.key() + " beta archive is invalid"); + } + move(temporary, archivePath, true); + } finally { + Files.deleteIfExists(temporary); + } + Properties updated = new Properties(); + response.headers().firstValue("etag").ifPresent(value -> updated.setProperty("etag", value)); + response.headers().firstValue("last-modified").ifPresent(value -> updated.setProperty("lastModified", value)); + updated.setProperty("source", spec.source().toString()); + updated.setProperty("fetchedAt", Long.toString(options.clock().millis())); + updated.setProperty("sha256", sha256(archivePath)); + storePropertiesAtomic(metadataPath, updated); + feedback.accept("Downloaded the Iris " + spec.key() + " beta archive."); + return new Archive(archivePath, updated.getProperty("sha256")); + } + close(response.body()); + IOException statusFailure = new IOException("Iris " + spec.key() + " beta download returned HTTP " + status); + if (!retryableStatus(status)) { + networkFailure = statusFailure; + break; + } + networkFailure = statusFailure; + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IOException("Iris " + spec.key() + " beta download was interrupted", exception); + } catch (IOException exception) { + networkFailure = exception; + } + if (attempt < options.attempts()) { + pause(options.retryDelay().multipliedBy(attempt)); + } + } + + if (cacheMatchesSource) { + feedback.accept("Iris " + spec.key() + " beta download failed; using the validated cached archive."); + return new Archive(archivePath, sha256(archivePath)); + } + if (isManagedPackOutputUsable(marker, cacheRoot.getParent().getParent(), spec)) { + String sourceSha = markerProperty(marker, spec, "sourceSha256"); + return new Archive(null, sourceSha); + } + throw networkFailure == null + ? new IOException("Iris " + spec.key() + " beta archive is unavailable and no valid cache exists") + : new IOException("Iris " + spec.key() + " beta archive is unavailable and no valid cache exists", networkFailure); + } + + private static boolean isManagedPackOutputUsable(Properties marker, Path dataDirectory, PackSpec spec) { + String sourceSha = markerProperty(marker, spec, "sourceSha256"); + String expectedFingerprint = markerProperty(marker, spec, "fingerprint"); + if (sourceSha == null || sourceSha.isBlank() || expectedFingerprint == null || expectedFingerprint.isBlank()) { + return false; + } + Path packRoot = dataDirectory.toAbsolutePath().normalize().resolve("packs").resolve(spec.key()); + try { + return isPackRoot(packRoot, spec) && directoryFingerprint(packRoot).equals(expectedFingerprint); + } catch (IOException | RuntimeException exception) { + return false; + } + } + + public static Path resolveLevelRoot(Path serverRoot) throws IOException { + return BukkitStartupPaths.resolve(serverRoot).levelRoot(); + } + + private static boolean validateArchive(Path archive, PackSpec spec) throws IOException { + int entries = 0; + long expanded = 0L; + boolean dimensionFound = false; + Set paths = new HashSet<>(); + try (InputStream input = Files.newInputStream(archive); ZipInputStream zip = new ZipInputStream(input)) { + ZipEntry entry; + while ((entry = zip.getNextEntry()) != null) { + entries++; + if (entries > MAX_ARCHIVE_ENTRIES) { + throw new IOException("Iris " + spec.key() + " beta archive contains too many entries"); + } + String name = normalizedZipEntry(entry.getName()); + if (!paths.add(name)) { + throw new IOException("Iris " + spec.key() + " beta archive contains duplicate path " + name); + } + String requiredDimension = "dimensions/" + spec.requiredDimension() + ".json"; + if (name.equals(requiredDimension) || name.endsWith("/" + requiredDimension)) { + dimensionFound = true; + } + if (!entry.isDirectory()) { + byte[] buffer = new byte[8192]; + int read; + while ((read = zip.read(buffer)) >= 0) { + expanded += read; + if (expanded > MAX_EXPANDED_BYTES) { + throw new IOException("Iris " + spec.key() + " beta archive expands beyond the safety limit"); + } + } + } + zip.closeEntry(); + } + } + return entries > 0 && dimensionFound; + } + + private static Path extractArchive(Path archive, Path extractionRoot, PackSpec spec) throws IOException { + if (archive == null) { + throw new IOException("Cached Iris " + spec.key() + " beta archive is unavailable for required pack rebuild"); + } + long expanded = 0L; + int entries = 0; + try (InputStream input = Files.newInputStream(archive); ZipInputStream zip = new ZipInputStream(input)) { + ZipEntry entry; + while ((entry = zip.getNextEntry()) != null) { + entries++; + if (entries > MAX_ARCHIVE_ENTRIES) { + throw new IOException("Iris " + spec.key() + " beta archive contains too many entries"); + } + String name = normalizedZipEntry(entry.getName()); + Path output = extractionRoot.resolve(name).normalize(); + if (!output.startsWith(extractionRoot)) { + throw new IOException("Unsafe path in Iris " + spec.key() + " beta archive: " + entry.getName()); + } + if (entry.isDirectory()) { + Files.createDirectories(output); + } else { + Files.createDirectories(output.getParent()); + try (OutputStream file = Files.newOutputStream(output)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = zip.read(buffer)) >= 0) { + expanded += read; + if (expanded > MAX_EXPANDED_BYTES) { + throw new IOException("Iris " + spec.key() + " beta archive expands beyond the safety limit"); + } + file.write(buffer, 0, read); + } + } + } + zip.closeEntry(); + } + } + if (isPackRoot(extractionRoot, spec)) { + return extractionRoot; + } + List candidates = new ArrayList<>(); + try (DirectoryStream children = Files.newDirectoryStream(extractionRoot)) { + for (Path child : children) { + if (Files.isDirectory(child) && isPackRoot(child, spec)) { + candidates.add(child); + } + } + } + if (candidates.size() != 1) { + throw new IOException("Iris " + spec.key() + " beta archive has an invalid root layout"); + } + return candidates.getFirst(); + } + + private static String normalizedZipEntry(String raw) throws IOException { + if (raw == null || raw.isBlank() || raw.indexOf('\0') >= 0 || raw.startsWith("/") || raw.startsWith("\\")) { + throw new IOException("Invalid path in Iris bootstrap pack archive"); + } + String normalized = raw.replace('\\', '/'); + Path path = Path.of(normalized).normalize(); + if (path.isAbsolute() || path.startsWith("..") || normalized.matches("^[A-Za-z]:.*")) { + throw new IOException("Unsafe path in Iris bootstrap pack archive: " + raw); + } + return path.toString().replace('\\', '/'); + } + + private static boolean isPackRoot(Path path, PackSpec spec) { + return path != null + && Files.isRegularFile(path.resolve("dimensions").resolve(spec.requiredDimension() + ".json")); + } + + private static void validatePackRoot(Path path, PackSpec spec) throws IOException { + if (!isPackRoot(path, spec)) { + throw new IOException("Iris " + spec.key() + " beta pack is missing dimensions/" + + spec.requiredDimension() + ".json at " + path); + } + } + + private static boolean isDatapackRoot(Path path) { + return isDatapackRoot(path, false); + } + + private static boolean isDatapackRoot(Path path, boolean requiresGeneratedTypes) { + return path != null + && Files.isRegularFile(path.resolve("pack.mcmeta")) + && (!requiresGeneratedTypes || Files.isDirectory(path.resolve("data/iris/dimension_type"))); + } + + static Path worldDatapackRoot(Path levelRoot) { + return levelRoot.toAbsolutePath().normalize().resolve("datapacks").resolve(WORLD_DATAPACK_DIRECTORY); + } + + private static String packRootsFingerprint(List packRoots) throws IOException { + MessageDigest digest = digest(); + List roots = packRoots.stream() + .map(File::toPath) + .map(path -> path.toAbsolutePath().normalize()) + .distinct() + .sorted(Comparator.comparing(Path::toString)) + .toList(); + for (Path root : roots) { + digest.update(root.toString().getBytes(StandardCharsets.UTF_8)); + digest.update(directoryFingerprint(root).getBytes(StandardCharsets.UTF_8)); + } + return HexFormat.of().formatHex(digest.digest()); + } + + private static String directoryFingerprint(Path root) throws IOException { + if (!Files.isDirectory(root)) { + return ""; + } + Path scanRoot = root.toRealPath(); + MessageDigest digest = digest(); + try (Stream stream = Files.walk(scanRoot)) { + List files = stream.filter(Files::isRegularFile) + .sorted(Comparator.comparing(path -> scanRoot.relativize(path).toString())) + .toList(); + byte[] buffer = new byte[8192]; + for (Path file : files) { + String relative = scanRoot.relativize(file).toString().replace('\\', '/'); + digest.update(relative.getBytes(StandardCharsets.UTF_8)); + try (InputStream input = Files.newInputStream(file)) { + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + } + } + } + return HexFormat.of().formatHex(digest.digest()); + } + + private static String sha256(Path file) throws IOException { + MessageDigest digest = digest(); + byte[] buffer = new byte[8192]; + try (InputStream input = Files.newInputStream(file)) { + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + } + return HexFormat.of().formatHex(digest.digest()); + } + + private static MessageDigest digest() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + private static Path replaceWithBackup(Path staged, Path target) throws IOException { + Path backup = target.resolveSibling("." + target.getFileName() + "-backup-" + UUID.randomUUID()); + if (Files.exists(target)) { + move(target, backup, false); + } else { + backup = null; + } + try { + move(staged, target, false); + return backup; + } catch (IOException failure) { + if (backup != null && Files.exists(backup)) { + move(backup, target, false); + } + throw failure; + } + } + + private static IOException rollback( + List packPublications, + Path datapackRoot, + Path datapackBackup, + boolean datapackReplaced + ) { + IOException failure = null; + try { + restore(datapackRoot, datapackBackup, datapackReplaced); + } catch (IOException exception) { + failure = exception; + } + for (int index = packPublications.size() - 1; index >= 0; index--) { + PackPublication publication = packPublications.get(index); + try { + restore(publication.root(), publication.backup(), true); + } catch (IOException exception) { + if (failure == null) { + failure = exception; + } else { + failure.addSuppressed(exception); + } + } + } + return failure; + } + + private static void restore(Path target, Path backup, boolean replaced) throws IOException { + if (!replaced) { + return; + } + delete(target); + if (backup != null && Files.exists(backup)) { + move(backup, target, false); + } + } + + private static void copyDirectory(Path source, Path target) throws IOException { + try (Stream stream = Files.walk(source)) { + for (Path entry : stream.toList()) { + Path relative = source.relativize(entry); + Path output = target.resolve(relative); + if (Files.isDirectory(entry)) { + Files.createDirectories(output); + } else if (Files.isRegularFile(entry)) { + Files.createDirectories(output.getParent()); + Files.copy(entry, output, StandardCopyOption.COPY_ATTRIBUTES); + } + } + } + } + + private static void move(Path source, Path target, boolean replace) throws IOException { + if (source == null) { + return; + } + Files.createDirectories(target.getParent()); + try { + if (replace) { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } else { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); + } + } catch (AtomicMoveNotSupportedException exception) { + if (replace) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } else { + Files.move(source, target); + } + } + } + + private static void delete(Path path) throws IOException { + if (path == null || !Files.exists(path)) { + return; + } + try (Stream stream = Files.walk(path)) { + List entries = stream.sorted(Comparator.reverseOrder()).toList(); + for (Path entry : entries) { + Files.deleteIfExists(entry); + } + } + } + + private static String levelId(Path levelRoot) throws IOException { + Path fileName = Objects.requireNonNull(levelRoot, "levelRoot").getFileName(); + if (fileName == null || fileName.toString().isBlank()) { + throw new IOException("Configured level root has no Paper startup level id: " + levelRoot); + } + return fileName.toString(); + } + + private static void deleteQuietly(Path path, Consumer feedback) { + try { + delete(path); + } catch (IOException exception) { + feedback.accept("Unable to clean temporary Iris bootstrap path " + path + ": " + exception.getMessage()); + } + } + + private static void copyLimited( + InputStream input, + OutputStream output, + long limit, + PackSpec spec + ) throws IOException { + byte[] buffer = new byte[8192]; + long total = 0L; + int read; + while ((read = input.read(buffer)) >= 0) { + total += read; + if (total > limit) { + throw new IOException("Iris " + spec.key() + " beta archive exceeds the download size limit"); + } + output.write(buffer, 0, read); + } + } + + private static Properties loadProperties(Path path) throws IOException { + Properties properties = new Properties(); + try (InputStream input = Files.newInputStream(path)) { + properties.load(input); + } + return properties; + } + + private static void storePropertiesAtomic(Path path, Properties properties) throws IOException { + Files.createDirectories(path.getParent()); + Path temporary = Files.createTempFile(path.getParent(), path.getFileName().toString(), ".tmp"); + try { + try (OutputStream output = Files.newOutputStream(temporary)) { + properties.store(output, null); + } + move(temporary, path, true); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static boolean retryableStatus(int status) { + return status == 408 || status == 425 || status == 429 || status >= 500; + } + + private static void pause(Duration duration) throws IOException { + if (duration.isZero() || duration.isNegative()) { + return; + } + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IOException("Iris bootstrap pack retry wait was interrupted", exception); + } + } + + private static long parseLong(String value, long fallback) { + if (value == null) { + return fallback; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException exception) { + return fallback; + } + } + + private static String markerKey(PackSpec spec, String property) { + return "pack." + spec.key() + "." + property; + } + + private static String markerProperty(Properties marker, PackSpec spec, String property) { + String current = marker.getProperty(markerKey(spec, property)); + if (current != null || !"overworld".equals(spec.key())) { + return current; + } + return switch (property) { + case "source" -> marker.getProperty("source"); + case "sourceSha256" -> marker.getProperty("sourceSha256"); + case "managed" -> marker.getProperty("managedDefault"); + case "fingerprint" -> marker.getProperty("defaultPackFingerprint"); + default -> null; + }; + } + + private static void close(InputStream input) { + try { + input.close(); + } catch (IOException ignored) { + } + } + + public enum ProvisionStatus { + INSTALLED, + UPDATED, + UNCHANGED + } + + public record ProvisionResult(Map packRoots, Path datapackRoot, ProvisionStatus status) { + public ProvisionResult { + packRoots = Map.copyOf(Objects.requireNonNull(packRoots, "packRoots")); + Objects.requireNonNull(datapackRoot, "datapackRoot"); + Objects.requireNonNull(status, "status"); + } + } + + record ProvisionOptions( + List packs, + List bindings, + HttpClient client, + Clock clock, + Duration refreshInterval, + Duration requestTimeout, + int attempts, + Duration retryDelay, + long maxArchiveBytes, + Path levelRoot + ) { + ProvisionOptions { + packs = List.copyOf(Objects.requireNonNull(packs, "packs")); + bindings = List.copyOf(Objects.requireNonNull(bindings, "bindings")); + Objects.requireNonNull(client, "client"); + Objects.requireNonNull(clock, "clock"); + Objects.requireNonNull(refreshInterval, "refreshInterval"); + Objects.requireNonNull(requestTimeout, "requestTimeout"); + Objects.requireNonNull(retryDelay, "retryDelay"); + Objects.requireNonNull(levelRoot, "levelRoot"); + if (attempts < 1 || maxArchiveBytes < 1L) { + throw new IllegalArgumentException("Invalid bootstrap provisioning options"); + } + Set keys = new HashSet<>(); + for (PackSpec pack : packs) { + if (!keys.add(pack.key())) { + throw new IllegalArgumentException("Duplicate bootstrap pack key '" + pack.key() + "'"); + } + } + } + } + + record PackSpec(String key, URI source, String requiredDimension) { + PackSpec { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(requiredDimension, "requiredDimension"); + if (!key.matches("[a-z0-9_-]+") || !requiredDimension.matches("[a-z0-9_/-]+")) { + throw new IllegalArgumentException("Invalid bootstrap pack specification for '" + key + "'"); + } + } + } + + private record PackPlan( + PackSpec spec, + Path root, + boolean existed, + boolean managed, + Archive archive, + boolean replace + ) { + } + + private record PackPublication(Path root, Path backup) { + } + + private record Archive(Path path, String sha256) { + private Archive { + Objects.requireNonNull(sha256, "sha256"); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/core/pack/IrisPack.java b/core/src/main/java/art/arcane/iris/core/pack/IrisPack.java similarity index 89% rename from core/src/main/java/com/volmit/iris/core/pack/IrisPack.java rename to core/src/main/java/art/arcane/iris/core/pack/IrisPack.java index f67fb507e..875b33f69 100644 --- a/core/src/main/java/com/volmit/iris/core/pack/IrisPack.java +++ b/core/src/main/java/art/arcane/iris/core/pack/IrisPack.java @@ -16,22 +16,23 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.pack; +package art.arcane.iris.core.pack; -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.ResourceLoader; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.engine.object.IrisWorld; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.exceptions.IrisException; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.exceptions.IrisException; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import lombok.Data; import org.bukkit.World; import org.zeroturnaround.zip.commons.FileUtils; @@ -153,7 +154,7 @@ public class IrisPack { * @return the file path */ public static File packsPack(String name) { - return Iris.instance.getDataFolderNoCreate(StudioSVC.WORKSPACE_NAME, name); + return IrisPlatforms.get().packsFolderNoCreate(name); } private static KList collectFiles(File f, String fileExtension) { @@ -208,20 +209,20 @@ public class IrisPack { try { PrecisionStopwatch p = PrecisionStopwatch.start(); - Iris.debug("Building Workspace: " + ws.getPath()); + IrisLogging.debug("Building Workspace: " + ws.getPath()); JSONObject j = generateWorkspaceConfig(); IO.writeAll(ws, j.toString(4)); p.end(); - Iris.debug("Building Workspace: " + ws.getPath() + " took " + Form.duration(p.getMilliseconds(), 2)); + IrisLogging.debug("Building Workspace: " + ws.getPath() + " took " + Form.duration(p.getMilliseconds(), 2)); return true; } catch (Throwable e) { - Iris.reportError(e); - Iris.warn("Pack invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!"); + IrisLogging.reportError(e); + IrisLogging.warn("Pack invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!"); ws.delete(); try { IO.writeAll(ws, generateWorkspaceConfig()); } catch (IOException e1) { - Iris.reportError(e1); + IrisLogging.reportError(e1); e1.printStackTrace(); } } @@ -265,7 +266,7 @@ public class IrisPack { try { FileUtils.copyDirectory(getFolder(), folder); } catch (IOException e) { - Iris.reportError(e); + IrisLogging.reportError(e); } return new IrisPack(folder); @@ -288,7 +289,7 @@ public class IrisPack { try { FileUtils.copyDirectory(getFolder(), newPack); } catch (IOException e) { - Iris.reportError(e); + IrisLogging.reportError(e); } IrisData data = IrisData.get(newPack); diff --git a/core/src/main/java/art/arcane/iris/core/pack/IrisPackRepository.java b/core/src/main/java/art/arcane/iris/core/pack/IrisPackRepository.java new file mode 100644 index 000000000..37b1b00e7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/IrisPackRepository.java @@ -0,0 +1,136 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.volmlib.util.format.Form; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.jobs.DownloadJob; +import art.arcane.iris.util.common.scheduling.jobs.JobCollection; +import art.arcane.iris.util.common.scheduling.jobs.SingleJob; +import lombok.Builder; +import lombok.Data; +import org.zeroturnaround.zip.ZipUtil; +import org.zeroturnaround.zip.commons.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.UUID; + +@Data +@Builder +public class IrisPackRepository { + private static final String DEFAULT_BRANCH = "stable"; + + @Builder.Default + private String user = "IrisDimensions"; + + @Builder.Default + private String repo = "overworld"; + + @Builder.Default + private String branch = DEFAULT_BRANCH; + + @Builder.Default + private String tag = ""; + + /** + * + */ + public static IrisPackRepository from(String g) { + // https://github.com/IrisDimensions/overworld + if (g.startsWith("https://github.com/")) { + String sub = g.split("\\Qgithub.com/\\E")[1]; + IrisPackRepository r = IrisPackRepository.builder() + .user(sub.split("\\Q/\\E")[0]) + .repo(sub.split("\\Q/\\E")[1]).build(); + + if (g.contains("/tree/")) { + r.setBranch(g.split("/tree/")[1]); + } + + return r; + } else if (g.contains("/")) { + String[] f = g.split("\\Q/\\E"); + + if (f.length == 1) { + return from(g); + } else if (f.length == 2) { + return IrisPackRepository.builder() + .user(f[0]) + .repo(f[1]) + .build(); + } else if (f.length >= 3) { + IrisPackRepository r = IrisPackRepository.builder() + .user(f[0]) + .repo(f[1]) + .build(); + + if (f[2].startsWith("#")) { + r.setTag(f[2].substring(1)); + } else { + r.setBranch(f[2]); + } + + return r; + } + } else { + return IrisPackRepository.builder() + .user("IrisDimensions") + .repo(g) + .branch(DEFAULT_BRANCH) + .build(); + } + + return null; + } + + public String toURL() { + if (!tag.trim().isEmpty()) { + return "https://codeload.github.com/" + user + "/" + repo + "/zip/refs/tags/" + tag; + } + + return "https://codeload.github.com/" + user + "/" + repo + "/zip/refs/heads/" + branch; + } + + public void install(VolmitSender sender, Runnable whenComplete) throws MalformedURLException { + File pack = IrisPlatforms.get().packsFolderNoCreate(getRepo()); + + if (!pack.exists()) { + File dl = new File(IrisPlatforms.get().dataFolder("cache", "temp"), "dltk-" + UUID.randomUUID() + ".zip"); + File work = new File(IrisPlatforms.get().dataFolder("cache", "temp"), "extk-" + UUID.randomUUID()); + new JobCollection(Form.capitalize(getRepo()), + new DownloadJob(toURL(), pack), + new SingleJob(IrisLanguage.text(RuntimeUiMessages.JOB_EXTRACTING), () -> ZipUtil.unpack(dl, work)), + new SingleJob(IrisLanguage.text(RuntimeUiMessages.JOB_INSTALLING), () -> { + try { + FileUtils.copyDirectory(work.listFiles()[0], pack); + } catch (IOException e) { + e.printStackTrace(); + } + })).execute(sender, whenComplete); + } else { + sender.sendMessage(IrisLanguage.text(RuntimeUiMessages.PACK_ALREADY_EXISTS)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java new file mode 100644 index 000000000..17297aa22 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java @@ -0,0 +1,142 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Validates biome layer stacks. caveCeilingLayers reuses the height generators built from layers, + * so a biome with more ceiling entries than surface entries has no generator for the extras; the + * engine skips them, and this validator surfaces the mistake to the author at validate time. + */ +final class PackBiomeLayerValidator { + private static final int DEFAULT_LAYER_COUNT = 1; + + private PackBiomeLayerValidator() { + } + + static List validateCeilingLayerCounts(File biomesFolder) { + List blockingErrors = new ArrayList<>(); + if (biomesFolder == null || !biomesFolder.isDirectory()) { + return blockingErrors; + } + + List biomeFiles = PackValidationIo.listJsonRecursive(biomesFolder); + biomeFiles.sort(Comparator.comparing(File::getPath)); + for (File biomeFile : biomeFiles) { + String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile); + JSONObject biome; + try { + biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + // Invalid JSON is reported by the graph validators; layer counts have nothing to add. + continue; + } + + Integer layers = arrayLength(biome, "layers", biomeKey, DEFAULT_LAYER_COUNT, blockingErrors); + Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, 0, blockingErrors); + if (layers == null || ceiling == null) { + continue; + } + + if (ceiling > layers) { + blockingErrors.add("Biome '" + biomeKey + "' declares " + ceiling + " caveCeilingLayers but only " + + layers + " layers. caveCeilingLayers reuses the layers height generators and must not have more entries."); + } + } + return blockingErrors; + } + + static List validateDecoratorPalettes(File biomesFolder, File decoratorSnippetsFolder) { + List blockingErrors = new ArrayList<>(); + if (biomesFolder != null && biomesFolder.isDirectory()) { + List biomeFiles = PackValidationIo.listJsonRecursive(biomesFolder); + biomeFiles.sort(Comparator.comparing(File::getPath)); + for (File biomeFile : biomeFiles) { + String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile); + JSONObject biome = PackValidationIo.readJson(biomeFile); + if (biome == null || !biome.has("decorators") || biome.isNull("decorators")) { + continue; + } + JSONArray decorators = biome.optJSONArray("decorators"); + if (decorators == null) { + blockingErrors.add("Biome '" + biomeKey + "' decorators must be an array."); + continue; + } + validateDecoratorArray(decorators, "Biome '" + biomeKey + "' decorators", blockingErrors); + } + } + + if (decoratorSnippetsFolder != null && decoratorSnippetsFolder.isDirectory()) { + List snippetFiles = PackValidationIo.listJsonRecursive(decoratorSnippetsFolder); + snippetFiles.sort(Comparator.comparing(File::getPath)); + for (File snippetFile : snippetFiles) { + String snippetKey = PackValidationIo.deriveKey(decoratorSnippetsFolder, snippetFile); + JSONObject snippet = PackValidationIo.readJson(snippetFile); + if (snippet != null) { + validateDecoratorPalette(snippet, "Decorator snippet '" + snippetKey + "'", blockingErrors); + } + } + } + return blockingErrors; + } + + private static void validateDecoratorArray(JSONArray decorators, String path, List blockingErrors) { + for (int index = 0; index < decorators.length(); index++) { + Object rawDecorator = decorators.opt(index); + if (rawDecorator instanceof String) { + continue; + } + if (!(rawDecorator instanceof JSONObject decorator)) { + blockingErrors.add(path + "[" + index + "] must be an object or snippet reference."); + continue; + } + validateDecoratorPalette(decorator, path + "[" + index + "]", blockingErrors); + } + } + + private static void validateDecoratorPalette(JSONObject decorator, String path, List blockingErrors) { + JSONArray palette = decorator.optJSONArray("palette"); + if (palette == null || palette.length() == 0) { + blockingErrors.add(path + " must declare a non-empty palette."); + } + } + + private static Integer arrayLength(JSONObject biome, String field, String biomeKey, int defaultCount, + List blockingErrors) { + if (!biome.has(field) || biome.isNull(field)) { + return defaultCount; + } + + JSONArray array = biome.optJSONArray(field); + if (array == null) { + blockingErrors.add("Biome '" + biomeKey + "' " + field + " must be an array."); + return null; + } + return array.length(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackCaveProfileValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackCaveProfileValidator.java new file mode 100644 index 000000000..d7d631b22 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackCaveProfileValidator.java @@ -0,0 +1,98 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +final class PackCaveProfileValidator { + private static final String CAVE_PROFILE_SNIPPET_FOLDER = "snippet/cave-profile"; + private static final List LEGACY_FIELDS = List.of( + new LegacyField("allowWater", "allowFluid"), + new LegacyField("waterMinDepthBelowSurface", "fluidMinDepthBelowSurface"), + new LegacyField("waterRequiresFloor", "fluidRequiresFloor") + ); + + private PackCaveProfileValidator() { + } + + static List validateLegacyFields(File packFolder) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + JSONObject caveProfile = resource.optJSONObject("caveProfile"); + if (caveProfile == null) { + continue; + } + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + validateFields(resourceType + " '" + resourceKey + "' caveProfile.", "caveProfile.", caveProfile, + blockingErrors); + } + } + + File snippetFolder = new File(packFolder, CAVE_PROFILE_SNIPPET_FOLDER); + if (!snippetFolder.isDirectory()) { + return blockingErrors; + } + List snippetFiles = PackValidationIo.listJsonRecursive(snippetFolder); + snippetFiles.sort(Comparator.comparing(File::getPath)); + for (File snippetFile : snippetFiles) { + JSONObject caveProfile = PackValidationIo.readJson(snippetFile); + if (caveProfile == null) { + continue; + } + String snippetKey = PackValidationIo.deriveKey(snippetFolder, snippetFile); + validateFields("Cave-profile snippet '" + snippetKey + "' ", "", caveProfile, blockingErrors); + } + return blockingErrors; + } + + private static void validateFields(String location, String replacementPrefix, JSONObject caveProfile, + List blockingErrors) { + for (LegacyField field : LEGACY_FIELDS) { + if (caveProfile.has(field.oldName())) { + blockingErrors.add(location + field.oldName() + " was removed; use " + replacementPrefix + + field.newName() + + ". Cave aquifers use the dimension fluidPalette, which defaults to water."); + } + } + } + + private record LegacyField(String oldName, String newName) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java new file mode 100644 index 000000000..6066363b3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java @@ -0,0 +1,311 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.engine.object.IrisDimensionType; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +final class PackDimensionValidator { + private PackDimensionValidator() { + } + + static void validateDimensions(File packFolder, File[] dimensionFiles, List blockingErrors, List warnings) { + File regionsFolder = new File(packFolder, "regions"); + File biomesFolder = new File(packFolder, "biomes"); + + for (File dimFile : dimensionFiles) { + String dimensionKey = PackValidationIo.stripExtension(dimFile.getName()); + JSONObject dimJson; + try { + dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Dimension '" + dimensionKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors, warnings); + validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors); + + JSONArray regionsArray = dimJson.optJSONArray("regions"); + if (regionsArray == null || regionsArray.length() == 0) { + blockingErrors.add("Dimension '" + dimensionKey + "' declares no regions."); + continue; + } + + int resolvedRegions = 0; + for (int i = 0; i < regionsArray.length(); i++) { + String regionKey = regionsArray.optString(i, null); + if (regionKey == null || regionKey.isBlank()) { + warnings.add("Dimension '" + dimensionKey + "' has a blank region entry at index " + i + "."); + continue; + } + File regionFile = new File(regionsFolder, regionKey + ".json"); + if (!regionFile.isFile()) { + blockingErrors.add("Dimension '" + dimensionKey + "' references missing region '" + regionKey + "'."); + continue; + } + + JSONObject regionJson; + try { + regionJson = new JSONObject(Files.readString(regionFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Region '" + regionKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + int anyBiome = countBiomeRefs(regionJson, "landBiomes", biomesFolder, regionKey, warnings) + + countBiomeRefs(regionJson, "seaBiomes", biomesFolder, regionKey, warnings) + + countBiomeRefs(regionJson, "shoreBiomes", biomesFolder, regionKey, warnings) + + countBiomeRefs(regionJson, "caveBiomes", biomesFolder, regionKey, warnings); + if (anyBiome == 0) { + blockingErrors.add("Region '" + regionKey + "' has no resolvable biomes."); + } + resolvedRegions++; + } + + if (resolvedRegions == 0) { + blockingErrors.add("Dimension '" + dimensionKey + "' has no resolvable regions."); + } + } + } + + static void validateImportedStructurePolicy(String dimensionKey, JSONObject dimension, + List blockingErrors, List warnings) { + if (!dimension.has("importedStructures")) { + return; + } + if (dimension.isNull("importedStructures")) { + blockingErrors.add("Dimension '" + dimensionKey + "' importedStructures must be an object."); + return; + } + JSONObject policy = dimension.optJSONObject("importedStructures"); + if (policy == null) { + blockingErrors.add("Dimension '" + dimensionKey + "' importedStructures must be an object."); + return; + } + if (policy.has("mode")) { + blockingErrors.add("Dimension '" + dimensionKey + + "' importedStructures.mode is not supported. Native structures are enabled by default; deny families in importedStructures.disabled or complete keys in importedStructures.disabledExact."); + } + if (policy.has("enabled")) { + blockingErrors.add("Dimension '" + dimensionKey + + "' importedStructures.enabled is not supported. Native structures are enabled by default; deny families in importedStructures.disabled or complete keys in importedStructures.disabledExact."); + } + validateStructureKeyList(dimensionKey, policy, "disabled", blockingErrors); + validateStructureKeyList(dimensionKey, policy, "disabledExact", blockingErrors); + validateFrequencyOverrides(dimensionKey, policy, blockingErrors); + JSONArray adjustments = policy.optJSONArray("adjustments"); + if (adjustments == null) { + if (policy.has("adjustments")) { + blockingErrors.add("Dimension '" + dimensionKey + + "' importedStructures.adjustments must be an array."); + } + return; + } + for (int index = 0; index < adjustments.length(); index++) { + JSONObject adjustment = adjustments.optJSONObject(index); + if (adjustment == null) { + blockingErrors.add("Dimension '" + dimensionKey + + "' importedStructures.adjustments has a non-object entry at index " + index + "."); + continue; + } + if (adjustment.has("clearVegetation")) { + warnings.add("Dimension '" + dimensionKey + "' importedStructures.adjustments[" + index + + "].clearVegetation was removed and is ignored. Vegetation is always cleared inside structure piece envelopes."); + } + validateStructureKeyList(dimensionKey, adjustment, "match", blockingErrors); + validateAdjustmentYBand(dimensionKey, adjustment, index, blockingErrors); + PackStructurePlacementValidator.validateNativeTerrain("Dimension '" + dimensionKey + + "' importedStructures.adjustments[" + index + "]", adjustment, blockingErrors); + } + } + + private static void validateFrequencyOverrides(String dimensionKey, JSONObject policy, + List blockingErrors) { + if (!policy.has("frequencyOverrides")) { + return; + } + JSONArray overrides = policy.optJSONArray("frequencyOverrides"); + if (overrides == null) { + blockingErrors.add("Dimension '" + dimensionKey + + "' importedStructures.frequencyOverrides must be an array."); + return; + } + for (int index = 0; index < overrides.length(); index++) { + JSONObject override = overrides.optJSONObject(index); + String path = "Dimension '" + dimensionKey + + "' importedStructures.frequencyOverrides[" + index + "]"; + if (override == null) { + blockingErrors.add(path + " must be an object."); + continue; + } + Object rawKey = override.opt("structureSet"); + if (!(rawKey instanceof String key) + || key.isBlank() + || !PackValidator.RESOURCE_KEY_PATTERN.matcher(key.trim()).matches()) { + blockingErrors.add(path + ".structureSet must be a namespaced registry key."); + } + PackJsonFieldChecks.validateOptionalDoubleRange( + path, override, "multiplier", 0.01D, 16D, blockingErrors); + } + } + + private static void validateAdjustmentYBand(String dimensionKey, JSONObject adjustment, int index, + List blockingErrors) { + if (!adjustment.has("yBand") || adjustment.opt("yBand") == JSONObject.NULL) { + return; + } + String path = "Dimension '" + dimensionKey + + "' importedStructures.adjustments[" + index + "].yBand"; + JSONObject band = adjustment.optJSONObject("yBand"); + if (band == null) { + blockingErrors.add(path + " must be an object."); + return; + } + PackJsonFieldChecks.validateOptionalIntegerRange(path, band, "min", -4064, 4064, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, band, "max", -4064, 4064, blockingErrors); + } + + private static void validateStructureKeyList(String dimensionKey, JSONObject owner, String field, + List blockingErrors) { + if (!owner.has(field)) { + return; + } + JSONArray keys = owner.optJSONArray(field); + if (keys == null) { + blockingErrors.add("Dimension '" + dimensionKey + "' structure policy field '" + + field + "' must be an array."); + return; + } + for (int index = 0; index < keys.length(); index++) { + Object value = keys.opt(index); + if (!(value instanceof String key) || key.isBlank()) { + blockingErrors.add("Dimension '" + dimensionKey + "' structure policy field '" + + field + "' has a blank or non-string entry at index " + index + "."); + } + } + } + + private static int countBiomeRefs(JSONObject regionJson, String field, File biomesFolder, String regionKey, List warnings) { + JSONArray arr = regionJson.optJSONArray(field); + if (arr == null) { + return 0; + } + int resolved = 0; + for (int i = 0; i < arr.length(); i++) { + String biomeKey = arr.optString(i, null); + if (biomeKey == null || biomeKey.isBlank()) { + continue; + } + File biomeFile = new File(biomesFolder, biomeKey + ".json"); + if (!biomeFile.isFile()) { + warnings.add("Region '" + regionKey + "' references missing biome '" + biomeKey + "' in " + field + "."); + continue; + } + resolved++; + } + return resolved; + } + + /** + * Mirrors the IrisDimensionType constructor checks so a pack that would throw at world creation + * fails validation instead. Defaults must match the POJOs exactly: dimensionHeight absent means + * the IrisDimension field initializer (-64..320); present-but-partial means the IrisRange field + * initializers (min 16, max 32); logicalHeight absent means 256. + */ + static void validateDimensionHeights(File packFolder, String dimensionKey, JSONObject dimJson, List blockingErrors) { + JSONObject range = resolveDimensionHeight(packFolder, dimJson); + if (range == null && dimJson.has("dimensionHeight") && !dimJson.isNull("dimensionHeight")) { + if (!(dimJson.opt("dimensionHeight") instanceof String)) { + blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight must be an object or a range snippet reference."); + } + // Unresolvable snippet references are reported by the content-key machinery. + return; + } + + int minY; + int maxY; + if (range == null) { + minY = -64; + maxY = 320; + } else { + minY = (int) range.optDouble("min", 16D); + maxY = (int) range.optDouble("max", 32D); + } + int height = maxY - minY; + int logicalHeight = dimJson.optInt("logicalHeight", 256); + + if (height < IrisDimensionType.MIN_HEIGHT || height > IrisDimensionType.MAX_HEIGHT) { + blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight span (max - min) is " + height + + "; it must be between " + IrisDimensionType.MIN_HEIGHT + " and " + IrisDimensionType.MAX_HEIGHT + "."); + } else if ((height & (IrisDimensionType.HEIGHT_STEP - 1)) != 0) { + blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight span (max - min) is " + height + + "; it must be a multiple of " + IrisDimensionType.HEIGHT_STEP + "."); + } + if (minY < IrisDimensionType.MIN_MIN_Y || minY > IrisDimensionType.MAX_MIN_Y) { + blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight.min is " + minY + + "; it must be between " + IrisDimensionType.MIN_MIN_Y + " and " + IrisDimensionType.MAX_MIN_Y + "."); + } else if ((minY & (IrisDimensionType.HEIGHT_STEP - 1)) != 0) { + blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight.min is " + minY + + "; it must be a multiple of " + IrisDimensionType.HEIGHT_STEP + "."); + } + if (logicalHeight < 0) { + blockingErrors.add("Dimension '" + dimensionKey + "' logicalHeight is " + logicalHeight + "; it cannot be negative."); + } else if (logicalHeight > height) { + blockingErrors.add("Dimension '" + dimensionKey + "' logicalHeight is " + logicalHeight + + "; it cannot be greater than the dimension height of " + height + "."); + } + } + + private static JSONObject resolveDimensionHeight(File packFolder, JSONObject dimJson) { + if (!dimJson.has("dimensionHeight") || dimJson.isNull("dimensionHeight")) { + return null; + } + + JSONObject inline = dimJson.optJSONObject("dimensionHeight"); + if (inline != null) { + return inline; + } + + String reference = dimJson.optString("dimensionHeight", null); + if (reference == null || !reference.startsWith("snippet/")) { + return null; + } + // Mirror IrisData's snippet adapter: canonical snippet/range/... is used verbatim; any other + // snippet/... reference is re-rooted under this field's snippet folder. + if (!reference.startsWith("snippet/range/")) { + reference = "snippet/range/" + reference.substring("snippet/".length()); + } + File snippet = new File(packFolder, reference + ".json"); + if (!snippet.isFile()) { + return null; + } + try { + return new JSONObject(Files.readString(snippet.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + return null; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackDirectoryResolver.java b/core/src/main/java/art/arcane/iris/core/pack/PackDirectoryResolver.java new file mode 100644 index 000000000..9a2a1f0e3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackDirectoryResolver.java @@ -0,0 +1,127 @@ +package art.arcane.iris.core.pack; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +public final class PackDirectoryResolver { + private PackDirectoryResolver() { + } + + public static File resolveExisting(File packsRoot, String packName) { + if (packsRoot == null || packName == null || packName.isBlank()) { + return null; + } + Path root = packsRoot.toPath().toAbsolutePath().normalize(); + Path candidate = root.resolve(packName).normalize(); + if (!root.equals(candidate.getParent())) { + return null; + } + if (!isVisiblePackDirectory(candidate.toFile())) { + return null; + } + return candidate.toFile(); + } + + public static List listVisiblePackDirectories(File packsRoot) { + try { + return listVisiblePackDirectoriesOrThrow(packsRoot); + } catch (IOException exception) { + return List.of(); + } + } + + public static List listVisiblePackDirectoriesOrThrow(File packsRoot) throws IOException { + if (packsRoot == null) { + return List.of(); + } + Path root = packsRoot.toPath().toAbsolutePath().normalize(); + if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + return List.of(); + } + if (!Files.isDirectory(root)) { + throw new IOException("Pack workspace is missing or unsafe: " + root); + } + try (Stream stream = Files.list(root)) { + return stream + .map(Path::toFile) + .filter(PackDirectoryResolver::isVisiblePackDirectory) + .sorted(Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER) + .thenComparing(File::getName)) + .toList(); + } + } + + public static boolean isVisiblePackDirectory(File candidate) { + if (candidate == null || isHiddenName(candidate.getName())) { + return false; + } + Path path = candidate.toPath(); + return Files.isDirectory(path); + } + + public static void requireSafePackTree(File candidate) throws IOException { + if (!isVisiblePackDirectory(candidate)) { + throw new IOException("Pack directory is missing or unsafe: " + candidate); + } + Path root = candidate.toPath().toAbsolutePath().normalize().toRealPath(); + if (Files.isSymbolicLink(root) || !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Pack directory target is missing or unsafe: " + candidate); + } + Files.walkFileTree(root, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) { + if (!directory.equals(root) && isHiddenName(directory.getFileName().toString())) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { + if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) { + throw new IOException("Pack directory contains a symbolic link: " + file); + } + if (!attributes.isRegularFile()) { + throw new IOException("Pack directory contains an unsupported entry: " + file); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException { + throw new IOException("Unable to inspect pack entry: " + file, failure); + } + }); + } + + public static boolean containsHiddenPathSegment(Path root, Path candidate) { + if (root == null || candidate == null) { + return true; + } + Path normalizedRoot = root.toAbsolutePath().normalize(); + Path normalizedCandidate = candidate.toAbsolutePath().normalize(); + if (!normalizedCandidate.startsWith(normalizedRoot)) { + return true; + } + Path relative = normalizedRoot.relativize(normalizedCandidate); + for (Path segment : relative) { + if (isHiddenName(segment.toString())) { + return true; + } + } + return false; + } + + public static boolean isHiddenName(String name) { + return name != null && name.startsWith("."); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackDownloadExecution.java b/core/src/main/java/art/arcane/iris/core/pack/PackDownloadExecution.java new file mode 100644 index 000000000..a0829eb3b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackDownloadExecution.java @@ -0,0 +1,144 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import art.arcane.iris.spi.IrisLogging; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class PackDownloadExecution implements Runnable { + private final Object monitor = new Object(); + private final LifecycleOperationCoordinator.Lease lease; + private final Work work; + private final PackDownloader.DownloadCancellation cancellation; + private final CompletableFuture completion; + private final AtomicBoolean finished; + private Future future; + private boolean cancellationRequested; + private boolean started; + + public PackDownloadExecution(LifecycleOperationCoordinator.Lease lease, Work work) { + this.lease = Objects.requireNonNull(lease, "lease"); + this.work = Objects.requireNonNull(work, "work"); + cancellation = new PackDownloader.DownloadCancellation(); + completion = new CompletableFuture<>(); + finished = new AtomicBoolean(); + } + + public void bind(Future submittedFuture) { + Future acceptedFuture = Objects.requireNonNull(submittedFuture, "submittedFuture"); + boolean cancelBeforeStart; + synchronized (monitor) { + future = acceptedFuture; + cancelBeforeStart = cancellationRequested && !started; + } + if (cancelBeforeStart) { + acceptedFuture.cancel(false); + finish(); + } + } + + public void onCompletion(Runnable callback) { + Runnable completionCallback = Objects.requireNonNull(callback, "callback"); + completion.whenComplete((ignored, failure) -> completionCallback.run()); + } + + public void cancel() { + Future submittedFuture; + boolean cancelBeforeStart; + synchronized (monitor) { + cancellationRequested = true; + submittedFuture = future; + cancelBeforeStart = !started; + } + cancellation.cancel(); + if (cancelBeforeStart) { + if (submittedFuture != null) { + submittedFuture.cancel(false); + } + finish(); + } + } + + public boolean await(long timeout, TimeUnit unit) throws InterruptedException { + try { + completion.get(timeout, unit); + return true; + } catch (TimeoutException exception) { + return false; + } catch (ExecutionException exception) { + return true; + } + } + + public boolean isPublishing() { + return cancellation.isPublishing(); + } + + public boolean isComplete() { + return completion.isDone(); + } + + @Override + public void run() { + synchronized (monitor) { + if (cancellationRequested) { + finish(); + return; + } + started = true; + } + + try { + cancellation.attachCurrentThread(); + work.run(cancellation); + } catch (PackDownloader.PackDownloadCancelledException ignored) { + } catch (Throwable failure) { + IrisLogging.reportError("Pack download worker failed.", failure); + } finally { + cancellation.complete(); + finish(); + } + } + + private void finish() { + if (!finished.compareAndSet(false, true)) { + return; + } + try { + lease.close(); + } catch (Throwable failure) { + IrisLogging.reportError("Failed to release the pack download lifecycle lease.", failure); + } finally { + completion.complete(null); + } + } + + @FunctionalInterface + public interface Work { + void run(PackDownloader.DownloadCancellation cancellation) throws Exception; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java b/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java new file mode 100644 index 000000000..18e574f50 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java @@ -0,0 +1,1063 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.PackDownloadMessages; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.util.common.misc.WebCache; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.localization.MessageArgument; +import org.zeroturnaround.zip.commons.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +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; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public final class PackDownloader { + private static final String DEFAULT_OVERWORLD_PACK = "overworld"; + private static final String DEFAULT_OVERWORLD_URL = + "https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip"; + private static final String UNDERWORLD_PACK = "underworld"; + private static final String UNDERWORLD_URL = + "https://github.com/IrisDimensions/underworld/releases/download/beta/underworld.zip"; + private static final List BUILT_IN_PACK_KEYS = List.of(DEFAULT_OVERWORLD_PACK, UNDERWORLD_PACK); + private static final Map BUILT_IN_PACK_URLS = Map.of( + DEFAULT_OVERWORLD_PACK, DEFAULT_OVERWORLD_URL, + UNDERWORLD_PACK, UNDERWORLD_URL + ); + private static final Pattern PACK_KEY = Pattern.compile("[a-z0-9_-]+"); + private static final ArchiveLimits ARCHIVE_LIMITS = new ArchiveLimits( + 512L * 1024L * 1024L, + 100_000, + 2L * 1024L * 1024L * 1024L, + 256L * 1024L * 1024L + ); + private static final ConcurrentHashMap DOWNLOAD_LOCKS = new ConcurrentHashMap<>(); + private static final AtomicBoolean DOWNLOAD_ACTIVE = new AtomicBoolean(); + private static final DownloadProgressListener NO_DOWNLOAD_PROGRESS = progress -> { + }; + + private PackDownloader() { + } + + public static boolean isDefaultOverworld(String pack) { + return DEFAULT_OVERWORLD_PACK.equals(pack); + } + + public static boolean isBuiltInPack(String pack) { + return pack != null && BUILT_IN_PACK_URLS.containsKey(pack); + } + + public static List builtInPacks() { + return BUILT_IN_PACK_KEYS; + } + + public static String downloadCommandFor(String pack) { + return isBuiltInPack(pack) + ? "/iris download pack=" + pack + : "/iris download link="; + } + + public static boolean isDirectZipUrl(String value) { + if (value == null || value.isBlank()) { + return false; + } + try { + URI uri = URI.create(value.trim()); + String scheme = uri.getScheme(); + String path = uri.getPath(); + return ("https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)) + && uri.getHost() != null + && !uri.getHost().isBlank() + && path != null + && path.toLowerCase(Locale.ROOT).endsWith(".zip"); + } catch (IllegalArgumentException exception) { + return false; + } + } + + public static String defaultOverworldPack() { + return DEFAULT_OVERWORLD_PACK; + } + + /** + * Whether a pack folder for {@code key} already exists with at least one dimension file. + * Presence is judged on disk, not on loadability: a pack that exists but fails to parse must + * surface as an error, never as a redownload. A folder without any dimensions/*.json is a + * partial import (an interrupted copy) and counts as absent so it can be replaced. + */ + public static boolean isPackPresent(File packsFolder, String key) { + if (packsFolder == null || !isSafePackKey(key)) { + return false; + } + Path packsRoot = packsFolder.toPath().toAbsolutePath().normalize(); + File resolvedPack = PackDirectoryResolver.resolveExisting(packsFolder, key); + if (resolvedPack == null) { + return false; + } + try { + PackDirectoryResolver.requireSafePackTree(resolvedPack); + } catch (IOException exception) { + return false; + } + Path pack = resolvedPack.toPath().toAbsolutePath().normalize(); + Path dimensions = pack.resolve("dimensions"); + if (!Objects.equals(pack.getParent(), packsRoot) + || Files.isSymbolicLink(dimensions) + || !Files.isDirectory(pack) + || !Files.isDirectory(dimensions, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + try (Stream entries = Files.list(dimensions)) { + return entries.anyMatch(path -> path.getFileName().toString().endsWith(".json") + && !Files.isSymbolicLink(path) + && Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)); + } catch (IOException exception) { + return false; + } + } + + public static boolean isBuiltInPackPresent(File packsFolder, String key) { + if (!isBuiltInPack(key) || !isPackPresent(packsFolder, key)) { + return false; + } + File resolvedPack = PackDirectoryResolver.resolveExisting(packsFolder, key); + if (resolvedPack == null) { + return false; + } + Path primaryDimension = resolvedPack.toPath().toAbsolutePath().normalize() + .resolve("dimensions") + .resolve(key + ".json"); + return !Files.isSymbolicLink(primaryDimension) + && Files.isRegularFile(primaryDimension, LinkOption.NOFOLLOW_LINKS); + } + + public static PackInstallResult downloadDefaultOverworld(File packsFolder, boolean forceOverwrite, Consumer feedback) throws IOException { + return downloadBuiltIn(packsFolder, DEFAULT_OVERWORLD_PACK, forceOverwrite, feedback); + } + + public static PackInstallResult downloadDefaultOverworld(File packsFolder, boolean forceOverwrite, + Consumer feedback, + DownloadProgressListener progressListener) throws IOException { + return downloadBuiltIn(packsFolder, DEFAULT_OVERWORLD_PACK, forceOverwrite, feedback, progressListener); + } + + public static PackInstallResult downloadBuiltIn(File packsFolder, String pack, boolean forceOverwrite, + Consumer feedback) throws IOException { + return downloadBuiltIn( + packsFolder, + pack, + forceOverwrite, + feedback, + new DownloadCancellation(), + NO_DOWNLOAD_PROGRESS + ); + } + + public static PackInstallResult downloadBuiltIn(File packsFolder, String pack, boolean forceOverwrite, + Consumer feedback, + DownloadProgressListener progressListener) throws IOException { + return downloadBuiltIn( + packsFolder, + pack, + forceOverwrite, + feedback, + new DownloadCancellation(), + progressListener + ); + } + + public static PackInstallResult downloadBuiltIn(File packsFolder, String pack, boolean forceOverwrite, + Consumer feedback, DownloadCancellation cancellation) throws IOException { + return downloadBuiltIn( + packsFolder, + pack, + forceOverwrite, + feedback, + cancellation, + NO_DOWNLOAD_PROGRESS + ); + } + + public static PackInstallResult downloadBuiltIn(File packsFolder, String pack, boolean forceOverwrite, + Consumer feedback, DownloadCancellation cancellation, + DownloadProgressListener progressListener) throws IOException { + String url = pack == null ? null : BUILT_IN_PACK_URLS.get(pack); + if (url == null) { + throw new IllegalArgumentException("Pack '" + pack + "' is not a built-in Iris download"); + } + return downloadArchive( + packsFolder, + url, + forceOverwrite, + pack, + feedback, + cancellation, + progressListener + ); + } + + public static PackInstallResult downloadUrl(File packsFolder, String url, boolean forceOverwrite, + Consumer feedback) throws IOException { + return downloadUrl( + packsFolder, + url, + forceOverwrite, + feedback, + new DownloadCancellation(), + NO_DOWNLOAD_PROGRESS + ); + } + + public static PackInstallResult downloadUrl(File packsFolder, String url, boolean forceOverwrite, + Consumer feedback, + DownloadProgressListener progressListener) throws IOException { + return downloadUrl( + packsFolder, + url, + forceOverwrite, + feedback, + new DownloadCancellation(), + progressListener + ); + } + + public static PackInstallResult downloadUrl(File packsFolder, String url, boolean forceOverwrite, + Consumer feedback, DownloadCancellation cancellation) throws IOException { + return downloadUrl( + packsFolder, + url, + forceOverwrite, + feedback, + cancellation, + NO_DOWNLOAD_PROGRESS + ); + } + + public static PackInstallResult downloadUrl(File packsFolder, String url, boolean forceOverwrite, + Consumer feedback, DownloadCancellation cancellation, + DownloadProgressListener progressListener) throws IOException { + if (!isDirectZipUrl(url)) { + throw new IllegalArgumentException("Pack URL must be an HTTP or HTTPS .zip link"); + } + return downloadArchive( + packsFolder, + url.trim(), + forceOverwrite, + null, + feedback, + cancellation, + progressListener + ); + } + + private static PackInstallResult downloadArchive(File packsFolder, String url, boolean forceOverwrite, + String expectedKey, Consumer feedback, + DownloadCancellation cancellation, + DownloadProgressListener progressListener) throws IOException { + Objects.requireNonNull(packsFolder, "packsFolder"); + DownloadCancellation control = Objects.requireNonNull(cancellation, "cancellation"); + Consumer output = feedback == null ? ignored -> { + } : feedback; + DownloadProgressListener progress = progressListener == null ? NO_DOWNLOAD_PROGRESS : progressListener; + if (expectedKey != null && !expectedKey.isBlank() && !isSafePackKey(expectedKey)) { + throw new IllegalArgumentException("Invalid expected pack key '" + expectedKey + "'"); + } + if (!DOWNLOAD_ACTIVE.compareAndSet(false, true)) { + throw new PackDownloadBusyException(); + } + try { + control.attachCurrentThread(); + control.checkpoint(); + String lockKey = expectedKey != null && !expectedKey.isBlank() + ? "key:" + expectedKey + : "url:" + IO.hash(url); + return withDownloadLock(lockKey, () -> { + control.checkpoint(); + boolean present = isBuiltInPack(expectedKey) + ? isBuiltInPackPresent(packsFolder, expectedKey) + : isPackPresent(packsFolder, expectedKey); + if (!forceOverwrite && present) { + sendFeedback(output, IrisLanguage.plain(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", expectedKey))); + return new PackInstallResult(expectedKey, false, false); + } + return downloadLocked( + packsFolder, + url, + forceOverwrite, + expectedKey, + lockKey, + output, + control, + progress + ); + }); + } finally { + control.complete(); + DOWNLOAD_ACTIVE.set(false); + } + } + + private static PackInstallResult downloadLocked(File packsFolder, String url, boolean forceOverwrite, + String expectedKey, String heldLockKey, Consumer feedback, + DownloadCancellation cancellation, + DownloadProgressListener progressListener) throws IOException { + cancellation.checkpoint(); + String source = expectedKey == null || expectedKey.isBlank() + ? IrisLanguage.plain(PackDownloadMessages.PROGRESS_SOURCE_REMOTE) + : expectedKey; + sendProgress(progressListener, DownloadProgress.phase(DownloadPhase.CONNECTING)); + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.DOWNLOADING, + MessageArgument.untrusted("url", source) + ) + " "); + File zip = WebCache.getNonCachedFile( + "pack-archive", + url, + ARCHIVE_LIMITS.maxArchiveBytes(), + transfer -> sendProgress(progressListener, DownloadProgress.transfer(transfer)) + ); + cancellation.checkpoint(); + File temp = WebCache.getTemp(); + File work = new File(temp, "dl-" + UUID.randomUUID()); + + try { + if (zip == null || !zip.exists()) { + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.FAILED_TO_FIND, + MessageArgument.untrusted("url", source) + )); + return null; + } + sendProgress(progressListener, DownloadProgress.phase(DownloadPhase.UNPACKING)); + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.UNPACKING, + MessageArgument.untrusted("repository", source) + )); + try { + unpackArchive(zip.toPath(), work.toPath(), ARCHIVE_LIMITS, cancellation); + } catch (IOException exception) { + if (exception instanceof PackDownloadCancelledException) { + throw exception; + } + IrisLogging.reportError(exception); + sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.UNPACK_FAILED)); + return null; + } + cancellation.checkpoint(); + File[] zipFiles = work.listFiles(); + if (zipFiles == null) { + sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.NO_EXTRACTED_FILES)); + return null; + } + File directory = findExtractedPackDirectory(work, zipFiles); + if (directory == null) { + sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.INVALID_ARCHIVE_FORMAT)); + return null; + } + return installExtractedPack( + packsFolder, + directory, + forceOverwrite, + expectedKey, + heldLockKey, + feedback, + cancellation, + progressListener + ); + } finally { + deleteDirectory(work); + } + } + + private static File findExtractedPackDirectory(File work, File[] extractedFiles) { + if (new File(work, "dimensions").isDirectory()) { + return work; + } + File candidate = null; + for (File extracted : extractedFiles) { + if (!extracted.isDirectory() || !new File(extracted, "dimensions").isDirectory()) { + continue; + } + if (candidate != null) { + return work; + } + candidate = extracted; + } + if (candidate != null) { + return candidate; + } + return extractedFiles.length == 1 && extractedFiles[0].isDirectory() ? extractedFiles[0] : work; + } + + static PackInstallResult installExtractedPack(File packsFolder, File extractedPack, boolean forceOverwrite, + String expectedKey, Consumer feedback) throws IOException { + Objects.requireNonNull(packsFolder, "packsFolder"); + Objects.requireNonNull(extractedPack, "extractedPack"); + Consumer output = feedback == null ? ignored -> { + } : feedback; + return installExtractedPack( + packsFolder, + extractedPack, + forceOverwrite, + expectedKey, + null, + output, + null, + NO_DOWNLOAD_PROGRESS + ); + } + + private static PackInstallResult installExtractedPack(File packsFolder, File extractedPack, boolean forceOverwrite, + String expectedKey, String heldLockKey, Consumer feedback, + DownloadCancellation cancellation, + DownloadProgressListener progressListener) throws IOException { + if (expectedKey != null && !expectedKey.isBlank() && !isSafePackKey(expectedKey)) { + throw new IllegalArgumentException("Invalid expected pack key '" + expectedKey + "'"); + } + Path packsRoot = packsFolder.toPath().toAbsolutePath().normalize(); + Files.createDirectories(packsRoot); + Path staging = packsRoot.resolve(".iris-import-" + UUID.randomUUID()); + try { + checkpoint(cancellation); + if (cancellation == null) { + FileUtils.copyDirectory(extractedPack, staging.toFile()); + } else { + copyDirectory(extractedPack.toPath(), staging, cancellation); + } + checkpoint(cancellation); + sendProgress(progressListener, DownloadProgress.phase(DownloadPhase.VALIDATING)); + PreparedPack prepared = prepareStagedPack(staging.toFile(), expectedKey, feedback); + if (prepared == null) { + return null; + } + checkpoint(cancellation); + String destinationLockKey = "key:" + prepared.key(); + if (destinationLockKey.equals(heldLockKey)) { + return publishPreparedPack( + packsFolder, + packsRoot, + staging, + prepared, + forceOverwrite, + feedback, + cancellation, + progressListener + ); + } + return withDownloadLock(destinationLockKey, + () -> publishPreparedPack( + packsFolder, + packsRoot, + staging, + prepared, + forceOverwrite, + feedback, + cancellation, + progressListener + )); + } finally { + deleteDirectory(staging.toFile()); + } + } + + private static PreparedPack prepareStagedPack(File staging, String expectedKey, + Consumer feedback) throws IOException { + IrisData data = IrisData.openDatapackCompiler(staging); + String key; + String name; + try { + String[] dimensions = data.getDimensionLoader().getPossibleKeys(); + if (dimensions == null || dimensions.length == 0) { + sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.NO_DIMENSION_FILE)); + sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.CHECK_GITHUB)); + return null; + } + String selectedDimension = selectDimensionKey(dimensions, expectedKey, feedback); + if (selectedDimension == null) { + return null; + } + IrisDimension dimension = data.getDimensionLoader().load(selectedDimension); + if (dimension == null) { + sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.INVALID_DIMENSION)); + return null; + } + key = dimension.getLoadKey(); + name = dimension.getName(); + } finally { + data.close(); + } + if (!isSafePackKey(key)) { + throw new IOException("Downloaded pack has unsafe dimension key '" + key + "'"); + } + if (expectedKey != null && !expectedKey.isBlank() && !expectedKey.equals(key)) { + throw new IOException("Downloaded pack key '" + key + "' does not match requested key '" + expectedKey + "'"); + } + + PackValidationResult stagedValidation; + try { + stagedValidation = PackValidator.validateForDatapackBootstrap(staging); + } catch (RuntimeException exception) { + throw new IOException("Pack validation failed before publication for '" + key + "'", exception); + } + PackValidationResult validation = new PackValidationResult( + key, + stagedValidation.getBlockingErrors(), + stagedValidation.getWarnings(), + stagedValidation.getValidatedAtMillis() + ); + if (!validation.isLoadable()) { + sendValidationFeedback(validation, feedback); + return null; + } + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.IMPORTING, + MessageArgument.untrusted("name", name), + MessageArgument.untrusted("key", key) + )); + return new PreparedPack(key, name, validation); + } + + private static String selectDimensionKey(String[] dimensions, String expectedKey, + Consumer feedback) throws IOException { + if (expectedKey == null || expectedKey.isBlank()) { + return Arrays.stream(dimensions) + .min((left, right) -> { + int lengthComparison = Integer.compare(left.length(), right.length()); + return lengthComparison == 0 ? left.compareTo(right) : lengthComparison; + }) + .orElseThrow(() -> new IOException("Downloaded pack contains no dimensions")); + } + + int matches = 0; + for (String dimension : dimensions) { + if (expectedKey.equals(dimension)) { + matches++; + } + } + if (matches != 1) { + throw new IOException("Downloaded pack dimensions " + Arrays.toString(dimensions) + + " do not contain exactly one requested key '" + expectedKey + "'"); + } + return expectedKey; + } + + private static PackInstallResult publishPreparedPack(File packsFolder, Path packsRoot, Path staging, PreparedPack prepared, + boolean forceOverwrite, Consumer feedback, + DownloadCancellation cancellation, + DownloadProgressListener progressListener) throws IOException { + sendProgress(progressListener, DownloadProgress.phase(DownloadPhase.PUBLISHING)); + Path target = packsRoot.resolve(prepared.key()).normalize(); + if (!Objects.equals(target.getParent(), packsRoot)) { + throw new IOException("Pack target escapes the packs folder: " + target); + } + if (Files.isSymbolicLink(target)) { + sendFeedback(feedback, "Pack '" + prepared.key() + "' is a symbolic-link source and cannot be replaced by Iris."); + return null; + } + + Path conflictingPack = findConflictingPack(packsRoot, staging, target, prepared.key()); + if (conflictingPack != null) { + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.DIMENSION_KEY_CONFLICT, + MessageArgument.untrusted("key", prepared.key()) + )); + return null; + } + boolean present = isBuiltInPack(prepared.key()) + ? isBuiltInPackPresent(packsRoot.toFile(), prepared.key()) + : isPackPresent(packsRoot.toFile(), prepared.key()); + if (!forceOverwrite && present) { + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.PACK_KEY_CONFLICT, + MessageArgument.untrusted("key", prepared.key()) + )); + return null; + } + if (!forceOverwrite && Files.exists(target) && !present) { + IrisLogging.warn("Replacing partial pack folder " + target + + " (required primary dimension is missing)."); + } + + // A registered loader alone is not "active": startup validation registers a loader for + // every visible pack (permanently), which made force-updating any installed pack + // impossible. Live engines only ever attach to detached openRuntime loaders, so the + // gate must go through the engine index, not the cached loader's own engine list. + // Stale cached registrations are closed so the swap cannot race a loader holding the + // old tree. + if (IrisData.hasActiveEngines(target.toFile())) { + sendFeedback( + feedback, + "Pack '" + prepared.key() + "' is active and cannot be replaced safely. Unload its worlds before retrying." + ); + return null; + } + if (cancellation != null) { + cancellation.beginPublication(); + } + IrisData.getLoaded(new File(packsFolder, prepared.key())).ifPresent(IrisData::close); + IrisData.getLoaded(target.toFile()).ifPresent(IrisData::close); + try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staging, target)) { + publication.commit(); + try { + publication.cleanupBackup(); + } catch (IOException exception) { + IrisLogging.reportError( + "Pack '" + prepared.key() + "' was published, but its transaction backup could not be cleaned.", + exception + ); + } + } + PackValidationRegistry.publish(prepared.validation()); + sendValidationFeedback(prepared.validation(), feedback); + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.ACQUIRED, + MessageArgument.untrusted("name", prepared.name()) + )); + sendProgress(progressListener, DownloadProgress.terminal()); + return new PackInstallResult(prepared.key(), true, true); + } + + private static Path findConflictingPack(Path packsRoot, Path staging, Path target, String key) throws IOException { + Set roots = new LinkedHashSet<>(); + roots.add(packsRoot); + if (IrisPlatforms.isBound()) { + roots.add(IrisPlatforms.get().packsFolder().toPath().toAbsolutePath().normalize()); + } + for (Path root : roots) { + if (!Files.isDirectory(root)) { + continue; + } + try (Stream entries = Files.list(root)) { + List candidates = entries.toList(); + for (Path candidate : candidates) { + Path normalized = candidate.toAbsolutePath().normalize(); + String candidateName = candidate.getFileName().toString(); + if (normalized.equals(staging) + || normalized.equals(target) + || PackDirectoryResolver.isHiddenName(candidateName) + || !PackDirectoryResolver.isVisiblePackDirectory(candidate.toFile())) { + continue; + } + Path dimension = candidate.resolve("dimensions").resolve(key + ".json"); + if (Files.isRegularFile(dimension)) { + return normalized; + } + } + } + } + return null; + } + + private static boolean isSafePackKey(String key) { + return key != null && PACK_KEY.matcher(key).matches(); + } + + private static PackInstallResult withDownloadLock(String key, DownloadOperation operation) throws IOException { + DownloadLock lock = DOWNLOAD_LOCKS.compute(key, (ignored, existing) -> { + DownloadLock selected = existing == null ? new DownloadLock() : existing; + selected.references++; + return selected; + }); + try { + synchronized (lock) { + return operation.run(); + } + } finally { + DOWNLOAD_LOCKS.computeIfPresent(key, (ignored, existing) -> { + if (existing != lock) { + return existing; + } + existing.references--; + return existing.references == 0 ? null : existing; + }); + } + } + + static int downloadLockCount() { + return DOWNLOAD_LOCKS.size(); + } + + static void unpackArchive(Path archive, Path destination, ArchiveLimits limits) throws IOException { + unpackArchive(archive, destination, limits, null); + } + + static void unpackArchive(Path archive, Path destination, ArchiveLimits limits, + DownloadCancellation cancellation) throws IOException { + Path source = Objects.requireNonNull(archive, "archive").toAbsolutePath().normalize(); + Path root = Objects.requireNonNull(destination, "destination").toAbsolutePath().normalize(); + ArchiveLimits safety = Objects.requireNonNull(limits, "limits"); + checkpoint(cancellation); + if (Files.isSymbolicLink(source) || !Files.isRegularFile(source, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Pack archive is missing or unsafe: " + source); + } + if (Files.size(source) > safety.maxArchiveBytes()) { + throw new IOException("Pack archive exceeds the compressed size limit."); + } + if (Files.exists(root, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Pack extraction target is not a directory: " + root); + } + Files.createDirectories(root); + if (Files.isSymbolicLink(root)) { + throw new IOException("Pack extraction target is unsafe: " + root); + } + + int entryCount = 0; + long expandedBytes = 0L; + Set paths = new HashSet<>(); + try (InputStream input = Files.newInputStream(source); ZipInputStream zip = new ZipInputStream(input)) { + ZipEntry entry; + while ((entry = zip.getNextEntry()) != null) { + checkpoint(cancellation); + entryCount++; + if (entryCount > safety.maxEntries()) { + throw new IOException("Pack archive contains too many entries."); + } + String normalizedName = normalizeArchiveEntry(entry.getName()); + String collisionKey = normalizedName.toLowerCase(Locale.ROOT); + if (!paths.add(collisionKey)) { + throw new IOException("Pack archive contains a duplicate path: " + normalizedName); + } + Path output = root.resolve(normalizedName).normalize(); + if (!output.startsWith(root)) { + throw new IOException("Pack archive entry escapes extraction: " + entry.getName()); + } + if (entry.isDirectory()) { + Files.createDirectories(output); + zip.closeEntry(); + continue; + } + long declaredSize = entry.getSize(); + if (declaredSize > safety.maxEntryBytes()) { + throw new IOException("Pack archive entry exceeds the file size limit: " + normalizedName); + } + Files.createDirectories(output.getParent()); + long entryBytes = 0L; + try (OutputStream file = Files.newOutputStream(output, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = zip.read(buffer)) != -1) { + checkpoint(cancellation); + if (read == 0) { + continue; + } + entryBytes += read; + expandedBytes += read; + if (entryBytes > safety.maxEntryBytes()) { + throw new IOException("Pack archive entry exceeds the file size limit: " + normalizedName); + } + if (expandedBytes > safety.maxExpandedBytes()) { + throw new IOException("Pack archive expands beyond the safety limit."); + } + file.write(buffer, 0, read); + } + } + zip.closeEntry(); + } + } + if (entryCount == 0) { + throw new IOException("Pack archive is empty."); + } + } + + private static void copyDirectory(Path source, Path destination, + DownloadCancellation cancellation) throws IOException { + Path sourceRoot = source.toAbsolutePath().normalize(); + Path destinationRoot = destination.toAbsolutePath().normalize(); + Files.walkFileTree(sourceRoot, new SimpleFileVisitor() { + private final byte[] buffer = new byte[8192]; + + @Override + public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) throws IOException { + cancellation.checkpoint(); + Files.createDirectories(destinationRoot.resolve(sourceRoot.relativize(directory))); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { + cancellation.checkpoint(); + if (Files.isSymbolicLink(file)) { + throw new IOException("Downloaded pack contains an unsafe symbolic link: " + file); + } + Path target = destinationRoot.resolve(sourceRoot.relativize(file)); + try (InputStream input = Files.newInputStream(file); + OutputStream output = Files.newOutputStream( + target, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE + )) { + int read; + while ((read = input.read(buffer)) != -1) { + cancellation.checkpoint(); + if (read > 0) { + output.write(buffer, 0, read); + } + } + } + return FileVisitResult.CONTINUE; + } + }); + } + + private static void checkpoint(DownloadCancellation cancellation) throws PackDownloadCancelledException { + if (cancellation != null) { + cancellation.checkpoint(); + } + } + + private static String normalizeArchiveEntry(String rawName) throws IOException { + if (rawName == null || rawName.isBlank() || rawName.indexOf('\0') >= 0 + || rawName.startsWith("/") || rawName.startsWith("\\")) { + throw new IOException("Pack archive contains an invalid path."); + } + String slashNormalized = rawName.replace('\\', '/'); + if (slashNormalized.matches("^[A-Za-z]:.*")) { + throw new IOException("Pack archive contains an unsafe path: " + rawName); + } + Path normalized = Path.of(slashNormalized).normalize(); + String result = normalized.toString().replace('\\', '/'); + if (normalized.isAbsolute() || normalized.startsWith("..") || result.isBlank() || ".".equals(result)) { + throw new IOException("Pack archive contains an unsafe path: " + rawName); + } + return result; + } + + private static void deleteDirectory(File directory) { + try { + AtomicDirectoryPublisher.deleteTree(directory.toPath()); + } catch (IOException exception) { + IrisLogging.reportError("Failed to clean temporary pack directory '" + directory.getPath() + "'", exception); + } + } + + private static void sendFeedback(Consumer feedback, String message) { + try { + feedback.accept(message); + } catch (RuntimeException exception) { + IrisLogging.reportError("Pack download feedback delivery failed", exception); + } + } + + private static void sendProgress(DownloadProgressListener listener, DownloadProgress progress) { + try { + listener.onProgress(progress); + } catch (RuntimeException exception) { + IrisLogging.reportError("Pack download progress delivery failed", exception); + } + } + + private static void sendValidationFeedback(PackValidationResult result, Consumer feedback) { + if (!result.isLoadable()) { + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.VALIDATION_FAILED, + MessageArgument.untrusted("pack", result.getPackName()) + )); + for (String reason : result.getBlockingErrors()) { + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.VALIDATION_REASON, + MessageArgument.untrusted("reason", reason) + )); + } + return; + } + if (!result.getWarnings().isEmpty()) { + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.VALIDATED_WITH_WARNINGS, + MessageArgument.untrusted("pack", result.getPackName()), + MessageArgument.trusted("count", result.getWarnings().size()) + )); + return; + } + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.VALIDATED, + MessageArgument.untrusted("pack", result.getPackName()) + )); + } + + static String builtInPackUrl(String pack) { + return BUILT_IN_PACK_URLS.get(pack); + } + + private record PreparedPack(String key, String name, PackValidationResult validation) { + } + + public record PackInstallResult(String key, boolean changed, boolean restartRequired) { + } + + public record DownloadProgress(DownloadPhase phase, long transferredBytes, long totalBytes, + long elapsedMillis, boolean complete) { + private static DownloadProgress phase(DownloadPhase phase) { + return new DownloadProgress(phase, 0L, -1L, 0L, false); + } + + private static DownloadProgress transfer(WebCache.TransferProgress transfer) { + return new DownloadProgress( + DownloadPhase.DOWNLOADING, + transfer.transferredBytes(), + transfer.contentLength(), + transfer.elapsedMillis(), + false + ); + } + + private static DownloadProgress terminal() { + return new DownloadProgress(DownloadPhase.PUBLISHING, 0L, -1L, 0L, true); + } + } + + public enum DownloadPhase { + CONNECTING, + DOWNLOADING, + UNPACKING, + VALIDATING, + PUBLISHING + } + + @FunctionalInterface + public interface DownloadProgressListener { + void onProgress(DownloadProgress progress); + } + + public static final class PackDownloadBusyException extends IOException { + public PackDownloadBusyException() { + super(IrisLanguage.plain(PackDownloadMessages.IN_PROGRESS)); + } + } + + public static final class PackDownloadCancelledException extends InterruptedIOException { + private PackDownloadCancelledException() { + super("Pack download cancelled."); + } + } + + public static final class DownloadCancellation { + private final Object monitor = new Object(); + private boolean cancelled; + private boolean publishing; + private Thread worker; + + public void cancel() { + Thread interruptTarget; + synchronized (monitor) { + cancelled = true; + interruptTarget = publishing ? null : worker; + } + if (interruptTarget != null) { + interruptTarget.interrupt(); + } + } + + public boolean isPublishing() { + synchronized (monitor) { + return publishing; + } + } + + void attachCurrentThread() throws PackDownloadCancelledException { + synchronized (monitor) { + Thread current = Thread.currentThread(); + if (worker != null && worker != current) { + throw new IllegalStateException("Pack download cancellation is already attached to another thread."); + } + worker = current; + checkCancelled(current); + } + } + + void checkpoint() throws PackDownloadCancelledException { + synchronized (monitor) { + checkCancelled(Thread.currentThread()); + } + } + + void beginPublication() throws PackDownloadCancelledException { + synchronized (monitor) { + checkCancelled(Thread.currentThread()); + publishing = true; + } + } + + void complete() { + boolean clearInterrupt; + synchronized (monitor) { + clearInterrupt = cancelled && worker == Thread.currentThread(); + publishing = false; + worker = null; + } + if (clearInterrupt) { + Thread.interrupted(); + } + } + + private void checkCancelled(Thread current) throws PackDownloadCancelledException { + if (cancelled || current.isInterrupted()) { + throw new PackDownloadCancelledException(); + } + } + } + + record ArchiveLimits(long maxArchiveBytes, int maxEntries, long maxExpandedBytes, long maxEntryBytes) { + ArchiveLimits { + if (maxArchiveBytes < 1L || maxEntries < 1 || maxExpandedBytes < 1L || maxEntryBytes < 1L) { + throw new IllegalArgumentException("Archive limits must be positive."); + } + } + } + + @FunctionalInterface + private interface DownloadOperation { + PackInstallResult run() throws IOException; + } + + private static final class DownloadLock { + private int references; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackExportClosure.java b/core/src/main/java/art/arcane/iris/core/pack/PackExportClosure.java new file mode 100644 index 000000000..e4ea58f3e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackExportClosure.java @@ -0,0 +1,72 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.engine.object.IrisObjectMarker; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.volmlib.util.collection.KSet; + +/** + * Shared key collection for the pack packagers. Both the Bukkit re-serializing compiler and the + * modded verbatim-copy compiler must export the complete ambient-spawning graph: object placements + * carry markers, markers carry spawners, spawners carry entities, entities carry loot. These + * helpers keep the two packagers walking placements identically. + */ +public final class PackExportClosure { + private PackExportClosure() { + } + + public static KSet collectMarkerKeys(Iterable placements) { + KSet markerKeys = new KSet<>(); + if (placements == null) { + return markerKeys; + } + for (IrisObjectPlacement placement : placements) { + if (placement == null || placement.getMarkers() == null) { + continue; + } + for (IrisObjectMarker marker : placement.getMarkers()) { + if (marker == null || marker.getMarker() == null || marker.getMarker().isBlank()) { + continue; + } + markerKeys.add(marker.getMarker()); + } + } + return markerKeys; + } + + public static KSet collectObjectKeys(Iterable placements) { + KSet objectKeys = new KSet<>(); + if (placements == null) { + return objectKeys; + } + for (IrisObjectPlacement placement : placements) { + if (placement == null || placement.getPlace() == null) { + continue; + } + for (String key : placement.getPlace()) { + if (key == null || key.isBlank()) { + continue; + } + objectKeys.add(key); + } + } + return objectKeys; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackGeneratorDuplicateValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackGeneratorDuplicateValidator.java new file mode 100644 index 000000000..0e18fe1d8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackGeneratorDuplicateValidator.java @@ -0,0 +1,165 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Warns when two generator files with identical content are both referenced by biomes. IrisGenerator's + * equals ignores the load key and IrisComplex buckets generators into a HashSet, so only one of a + * content-identical pair survives; biomes linked to the losing key resolve 0/0 height bounds and + * contribute a flat band at fluid height. + * + *

The fingerprint is a conservative approximation of IrisGenerator.equals built from raw JSON + * (sorted keys, canonical number formatting). It can miss a collision where one file spells out a + * value the other leaves defaulted, and it ignores fields the POJO does not declare - acceptable + * for a warning aimed at the copy-the-file-and-forget-the-seed mistake. + */ +final class PackGeneratorDuplicateValidator { + private static final String GENERATOR_SNIPPET_FOLDER = "snippet/generator-layer/"; + + private PackGeneratorDuplicateValidator() { + } + + static List validateDuplicateGenerators(File packFolder) { + List warnings = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return warnings; + } + File generatorsFolder = new File(packFolder, "generators"); + if (!generatorsFolder.isDirectory()) { + return warnings; + } + + Set referenced = collectReferencedGeneratorKeys(packFolder); + Map> byFingerprint = new TreeMap<>(); + List generatorFiles = PackValidationIo.listJsonRecursive(generatorsFolder); + generatorFiles.sort(Comparator.comparing(File::getPath)); + for (File generatorFile : generatorFiles) { + String key = PackValidationIo.deriveKey(generatorsFolder, generatorFile); + String fingerprint; + try { + fingerprint = fingerprint(new JSONObject(Files.readString(generatorFile.toPath(), StandardCharsets.UTF_8))); + } catch (Throwable e) { + continue; + } + byFingerprint.computeIfAbsent(fingerprint, ignored -> new ArrayList<>()).add(key); + } + + for (List group : byFingerprint.values()) { + List referencedKeys = group.stream().filter(referenced::contains).sorted().toList(); + if (referencedKeys.size() < 2) { + continue; + } + warnings.add("Generators " + String.join(", ", referencedKeys) + + " have identical content and are both referenced by biomes. Iris buckets generators by value" + + " (IrisGenerator equals ignores the load key), so only one survives and biomes referencing the" + + " others get a zero height band. Give each generator a distinct value (for example a different" + + " seed) or point every biome at a single key."); + } + return warnings; + } + + private static Set collectReferencedGeneratorKeys(File packFolder) { + Set referenced = new HashSet<>(); + File biomesFolder = new File(packFolder, "biomes"); + if (!biomesFolder.isDirectory()) { + return referenced; + } + for (File biomeFile : PackValidationIo.listJsonRecursive(biomesFolder)) { + JSONObject biome; + try { + biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + continue; + } + JSONArray links = biome.optJSONArray("generators"); + if (links == null) { + continue; + } + for (int i = 0; i < links.length(); i++) { + JSONObject link = links.optJSONObject(i); + if (link == null) { + String reference = links.optString(i, null); + link = resolveGeneratorLayerSnippet(packFolder, reference); + if (link == null) { + continue; + } + } + referenced.add(link.optString("generator", "default")); + } + } + return referenced; + } + + private static JSONObject resolveGeneratorLayerSnippet(File packFolder, String reference) { + if (reference == null || !reference.startsWith("snippet/")) { + return null; + } + String resolved = reference.startsWith(GENERATOR_SNIPPET_FOLDER) + ? reference + : GENERATOR_SNIPPET_FOLDER + reference.substring("snippet/".length()); + File snippet = new File(packFolder, resolved + ".json"); + if (!snippet.isFile()) { + return null; + } + try { + return new JSONObject(Files.readString(snippet.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + return null; + } + } + + private static String fingerprint(Object value) { + if (value instanceof JSONObject object) { + Map sorted = new TreeMap<>(); + for (String key : object.keySet()) { + sorted.put(key, fingerprint(object.get(key))); + } + StringBuilder builder = new StringBuilder("{"); + sorted.forEach((key, child) -> builder.append(key).append('=').append(child).append(';')); + return builder.append('}').toString(); + } + if (value instanceof JSONArray array) { + StringBuilder builder = new StringBuilder("["); + for (int i = 0; i < array.length(); i++) { + builder.append(fingerprint(array.get(i))).append(';'); + } + return builder.append(']').toString(); + } + if (value instanceof Number number) { + // Gson deserializes 1 and 1.0 into the same field value; fingerprint them alike. + return Double.toString(number.doubleValue()); + } + return String.valueOf(value); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackJsonFieldChecks.java b/core/src/main/java/art/arcane/iris/core/pack/PackJsonFieldChecks.java new file mode 100644 index 000000000..c397a5890 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackJsonFieldChecks.java @@ -0,0 +1,101 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONObject; + +import java.util.List; +import java.util.Set; + +final class PackJsonFieldChecks { + private PackJsonFieldChecks() { + } + + static void validateOptionalBoolean(String path, JSONObject object, String field, + List blockingErrors) { + if (!object.has(field) || object.opt(field) == JSONObject.NULL) { + return; + } + if (!(object.opt(field) instanceof Boolean)) { + blockingErrors.add(path + "." + field + " must be a boolean."); + } + } + + static void validateOptionalResourceKey(String path, JSONObject object, String field, + boolean allowNone, List blockingErrors) { + if (!object.has(field)) { + return; + } + Object rawValue = object.opt(field); + if (!(rawValue instanceof String value)) { + blockingErrors.add(path + "." + field + " must be a string."); + return; + } + String normalized = value.trim(); + if (normalized.isEmpty() || allowNone && "NONE".equalsIgnoreCase(normalized)) { + return; + } + if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(normalized).matches()) { + blockingErrors.add(path + "." + field + " must be a namespaced registry key."); + } + } + + static void validateOptionalIntegerRange(String path, JSONObject object, String field, + int minimum, int maximum, + List blockingErrors) { + if (!object.has(field) || object.opt(field) == JSONObject.NULL) { + return; + } + Integer value = PackLootValidator.lootInteger(object, field, minimum, path, blockingErrors); + PackLootValidator.requireMinimum(path + "." + field, value, minimum, blockingErrors); + PackLootValidator.requireMaximum(path + "." + field, value, maximum, blockingErrors); + } + + static void validateOptionalDoubleRange(String path, JSONObject object, String field, + double minimum, double maximum, + List blockingErrors) { + if (!object.has(field) || object.opt(field) == JSONObject.NULL) { + return; + } + String fieldPath = path + "." + field; + Object rawValue = object.opt(field); + if (!(rawValue instanceof Number number) || !Double.isFinite(number.doubleValue())) { + blockingErrors.add(fieldPath + " must be a number."); + return; + } + double value = number.doubleValue(); + if (value < minimum) { + blockingErrors.add(fieldPath + " must be at least " + minimum + "."); + } + if (value > maximum) { + blockingErrors.add(fieldPath + " must be at most " + maximum + "."); + } + } + + static void validateOptionalEnum(String path, JSONObject object, String field, + Set values, List blockingErrors) { + if (!object.has(field)) { + return; + } + Object rawValue = object.opt(field); + if (!(rawValue instanceof String value) || !values.contains(value)) { + blockingErrors.add(path + "." + field + " must be one of " + values + "."); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackLootValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackLootValidator.java new file mode 100644 index 000000000..102eca458 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackLootValidator.java @@ -0,0 +1,255 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.engine.object.IrisLoot; +import art.arcane.iris.engine.object.IrisLootReference; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +final class PackLootValidator { + private PackLootValidator() { + } + + record LootGraphIssues(List errors, List warnings) { + LootGraphIssues { + errors = List.copyOf(errors); + warnings = List.copyOf(warnings); + } + } + + static LootGraphIssues validateLootGraph(File packFolder) { + List blockingErrors = new ArrayList<>(); + List warnings = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return new LootGraphIssues(blockingErrors, warnings); + } + + File lootFolder = new File(packFolder, PackValidator.LOOT_FOLDER); + Set lootKeys = ContentKeyValidator.deriveRegistrantKeysExact(lootFolder); + if (lootFolder.isDirectory()) { + List lootFiles = PackValidationIo.listJsonRecursive(lootFolder); + lootFiles.sort(Comparator.comparing(File::getPath)); + for (File lootFile : lootFiles) { + String lootKey = PackValidationIo.deriveKey(lootFolder, lootFile); + JSONObject table = PackStructurePlacementValidator.readGraphJson( + lootFile, "Loot table", lootKey, blockingErrors); + if (table != null) { + validateLootTable(lootKey, table, blockingErrors); + } + } + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null || !resource.has("loot")) { + continue; + } + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + validateLootReference(resourceType, resourceKey, resource.opt("loot"), lootKeys, blockingErrors, warnings); + } + } + return new LootGraphIssues(blockingErrors, warnings); + } + + private static void validateLootTable(String lootKey, JSONObject table, List blockingErrors) { + String path = "Loot table '" + lootKey + "'"; + Integer rarity = lootInteger(table, "rarity", 1, path, blockingErrors); + Integer minimumPicked = lootInteger(table, "minPicked", 1, path, blockingErrors); + Integer maximumPicked = lootInteger(table, "maxPicked", 5, path, blockingErrors); + Integer maximumTries = lootInteger(table, "maxTries", 10, path, blockingErrors); + requireMinimum(path + ".rarity", rarity, 1, blockingErrors); + requireMinimum(path + ".minPicked", minimumPicked, 0, blockingErrors); + requireMinimum(path + ".maxPicked", maximumPicked, 1, blockingErrors); + requireMinimum(path + ".maxTries", maximumTries, 1, blockingErrors); + requireMaximum(path + ".minPicked", minimumPicked, IrisLootTable.MAX_PICKED, blockingErrors); + requireMaximum(path + ".maxPicked", maximumPicked, IrisLootTable.MAX_PICKED, blockingErrors); + requireMaximum(path + ".maxTries", maximumTries, IrisLootTable.MAX_TRIES, blockingErrors); + requireOrdered(path + ".minPicked", minimumPicked, path + ".maxPicked", maximumPicked, blockingErrors); + + JSONArray entries = table.optJSONArray("loot"); + if (entries == null || entries.length() == 0) { + blockingErrors.add(path + ".loot must be a non-empty array."); + return; + } + for (int entryIndex = 0; entryIndex < entries.length(); entryIndex++) { + JSONObject entry = entries.optJSONObject(entryIndex); + String entryPath = path + ".loot[" + entryIndex + "]"; + if (entry == null) { + blockingErrors.add(entryPath + " must be an object."); + continue; + } + String type = entry.optString("type", "").trim(); + if (type.isEmpty()) { + blockingErrors.add(entryPath + ".type must not be blank."); + } + Integer entryRarity = lootInteger(entry, "rarity", 1, entryPath, blockingErrors); + Integer minimumAmount = lootInteger(entry, "minAmount", 1, entryPath, blockingErrors); + Integer maximumAmount = lootInteger(entry, "maxAmount", 1, entryPath, blockingErrors); + requireMinimum(entryPath + ".rarity", entryRarity, 1, blockingErrors); + requireMinimum(entryPath + ".minAmount", minimumAmount, 1, blockingErrors); + requireMinimum(entryPath + ".maxAmount", maximumAmount, 1, blockingErrors); + requireMaximum(entryPath + ".minAmount", minimumAmount, IrisLoot.MAX_AMOUNT, blockingErrors); + requireMaximum(entryPath + ".maxAmount", maximumAmount, IrisLoot.MAX_AMOUNT, blockingErrors); + requireOrdered(entryPath + ".minAmount", minimumAmount, + entryPath + ".maxAmount", maximumAmount, blockingErrors); + validateLootEnchantments(entryPath, entry.opt("enchantments"), blockingErrors); + } + } + + private static void validateLootEnchantments(String entryPath, Object rawEnchantments, + List blockingErrors) { + if (rawEnchantments == null || rawEnchantments == JSONObject.NULL) { + return; + } + if (!(rawEnchantments instanceof JSONArray enchantments)) { + blockingErrors.add(entryPath + ".enchantments must be an array."); + return; + } + for (int enchantmentIndex = 0; enchantmentIndex < enchantments.length(); enchantmentIndex++) { + JSONObject enchantment = enchantments.optJSONObject(enchantmentIndex); + String enchantmentPath = entryPath + ".enchantments[" + enchantmentIndex + "]"; + if (enchantment == null) { + blockingErrors.add(enchantmentPath + " must be an object."); + continue; + } + if (enchantment.optString("enchantment", "").isBlank()) { + blockingErrors.add(enchantmentPath + ".enchantment must not be blank."); + } + Integer minimumLevel = lootInteger(enchantment, "minLevel", 1, enchantmentPath, blockingErrors); + Integer maximumLevel = lootInteger(enchantment, "maxLevel", 1, enchantmentPath, blockingErrors); + requireMinimum(enchantmentPath + ".minLevel", minimumLevel, 1, blockingErrors); + requireMinimum(enchantmentPath + ".maxLevel", maximumLevel, 1, blockingErrors); + requireOrdered(enchantmentPath + ".minLevel", minimumLevel, + enchantmentPath + ".maxLevel", maximumLevel, blockingErrors); + if (enchantment.has("chance")) { + Object rawChance = enchantment.opt("chance"); + if (!(rawChance instanceof Number number) + || !Double.isFinite(number.doubleValue()) + || number.doubleValue() < 0D + || number.doubleValue() > 1D) { + blockingErrors.add(enchantmentPath + ".chance must be a finite number from 0 to 1."); + } + } + } + } + + private static void validateLootReference(String resourceType, String resourceKey, Object rawLoot, + Set lootKeys, List blockingErrors, List warnings) { + String path = resourceType + " '" + resourceKey + "'.loot"; + if (!(rawLoot instanceof JSONObject reference)) { + blockingErrors.add(path + " must be an object."); + return; + } + boolean clearMode = false; + if (reference.has("mode")) { + Object rawMode = reference.opt("mode"); + if (!(rawMode instanceof String mode) + || !Set.of("ADD", "CLEAR", "REPLACE", "FALLBACK").contains(mode)) { + blockingErrors.add(path + ".mode must be ADD, CLEAR, REPLACE, or FALLBACK."); + } else { + clearMode = "CLEAR".equals(mode); + } + } + if (reference.has("multiplier")) { + Object rawMultiplier = reference.opt("multiplier"); + if (!(rawMultiplier instanceof Number multiplier) + || !Double.isFinite(multiplier.doubleValue()) + || multiplier.doubleValue() < 0D + || multiplier.doubleValue() > IrisLootReference.MAX_MULTIPLIER) { + blockingErrors.add(path + ".multiplier must be a finite number from 0 to " + + (int) IrisLootReference.MAX_MULTIPLIER + "."); + } + } + if (!reference.has("tables")) { + return; + } + JSONArray tables = reference.optJSONArray("tables"); + if (tables == null) { + blockingErrors.add(path + ".tables must be an array."); + return; + } + if (clearMode && tables.length() > 0) { + warnings.add(path + " uses mode CLEAR and lists " + tables.length() + + " table(s); CLEAR clears parent tables and contributes no tables of its own, so these entries are dead. Use REPLACE to substitute them."); + } + for (int tableIndex = 0; tableIndex < tables.length(); tableIndex++) { + Object rawTableKey = tables.opt(tableIndex); + if (!(rawTableKey instanceof String tableKey) || tableKey.isBlank()) { + blockingErrors.add(path + ".tables[" + tableIndex + "] must name a loot table."); + } else if (!lootKeys.contains(tableKey)) { + blockingErrors.add(path + ".tables[" + tableIndex + + "] references missing loot table '" + tableKey + "'."); + } + } + } + + static Integer lootInteger(JSONObject object, String field, int defaultValue, + String path, List blockingErrors) { + if (!object.has(field)) { + return defaultValue; + } + Object rawValue = object.opt(field); + if (!(rawValue instanceof Number number) + || !Double.isFinite(number.doubleValue()) + || number.doubleValue() != Math.rint(number.doubleValue()) + || number.longValue() < Integer.MIN_VALUE + || number.longValue() > Integer.MAX_VALUE) { + blockingErrors.add(path + "." + field + " must be an integer."); + return null; + } + return number.intValue(); + } + + static void requireMinimum(String fieldPath, Integer value, int minimum, + List blockingErrors) { + if (value != null && value < minimum) { + blockingErrors.add(fieldPath + " must be at least " + minimum + "."); + } + } + + static void requireMaximum(String fieldPath, Integer value, int maximum, + List blockingErrors) { + if (value != null && value > maximum) { + blockingErrors.add(fieldPath + " must be at most " + maximum + "."); + } + } + + static void requireOrdered(String minimumPath, Integer minimum, String maximumPath, + Integer maximum, List blockingErrors) { + if (minimum != null && maximum != null && minimum > maximum) { + blockingErrors.add(minimumPath + " must not exceed " + maximumPath + "."); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackNativeStructureValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackNativeStructureValidator.java new file mode 100644 index 000000000..85dcc62c2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackNativeStructureValidator.java @@ -0,0 +1,341 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class PackNativeStructureValidator { + private PackNativeStructureValidator() { + } + + static List validateNativeStructureReplacements( + File packFolder, + Set replacementOutputStructures, + Map> sampledVerticalEnvelopes + ) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + Set viableStructures = replacementOutputStructures == null + ? Set.of() : replacementOutputStructures; + Map> verticalEnvelopes = + sampledVerticalEnvelopes == null ? Map.of() : sampledVerticalEnvelopes; + File structuresFolder = new File(packFolder, PackValidator.STRUCTURES_FOLDER); + Map structures = new HashMap<>(); + for (File structureFile : PackValidationIo.listJsonRecursive(structuresFolder)) { + JSONObject structure = PackValidationIo.readJson(structureFile); + if (structure != null) { + structures.put(PackValidationIo.deriveKey(structuresFolder, structureFile), structure); + } + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + JSONArray placements = resource.optJSONArray("structures"); + if (placements == null) { + continue; + } + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { + JSONObject placement = placements.optJSONObject(placementIndex); + if (placement == null || !placement.has("nativeSuppression")) { + continue; + } + Object rawSuppression = placement.opt("nativeSuppression"); + if (!(rawSuppression instanceof String suppression)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "].nativeSuppression must be NONE or REPLACE_SOURCE."); + continue; + } + if ("NONE".equals(suppression)) { + continue; + } + if (!"REPLACE_SOURCE".equals(suppression)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "].nativeSuppression has unsupported value '" + + suppression + "'. Use NONE or REPLACE_SOURCE."); + continue; + } + if (!PackValidator.DIMENSIONS_FOLDER.equals(folderName)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "] requests REPLACE_SOURCE, but native replacement is only" + + " valid on dimension-level placements."); + continue; + } + JSONArray references = placement.optJSONArray("structures"); + JSONArray nativeStructures = placement.optJSONArray("nativeStructures"); + if (nativeStructures != null && nativeStructures.length() > 0) { + continue; + } + if (references == null || references.length() == 0) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "] requests REPLACE_SOURCE without any structure backend."); + continue; + } + for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) { + Object rawReference = references.opt(referenceIndex); + if (!(rawReference instanceof String structureKey) || structureKey.isBlank()) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "].structures[" + referenceIndex + + "] must name an Iris structure for REPLACE_SOURCE."); + continue; + } + JSONObject structure = structures.get(structureKey); + if (structure == null) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "] cannot REPLACE_SOURCE with missing or invalid structure '" + + structureKey + "'."); + continue; + } + String vanillaSource = structure.optString("vanillaSource", "").trim(); + if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(vanillaSource).matches()) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "] requests REPLACE_SOURCE for structure '" + structureKey + + "', but its vanillaSource is not a valid namespaced registry key."); + } + if (!viableStructures.contains(structureKey)) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "] requests REPLACE_SOURCE for structure '" + structureKey + + "', but that structure is not runtime-viable. Native generation will not" + + " be used as a fallback."); + continue; + } + validateReplacementVerticalEnvelope( + resourceKey, + resource, + placementIndex, + placement, + structureKey, + structure, + verticalEnvelopes.get(structureKey), + blockingErrors); + } + } + } + } + return blockingErrors; + } + + private static void validateReplacementVerticalEnvelope( + String dimensionKey, + JSONObject dimension, + int placementIndex, + JSONObject placement, + String structureKey, + JSONObject structure, + List sampledVerticalEnvelopes, + List blockingErrors + ) { + String context = "Dimension '" + dimensionKey + "' structures[" + placementIndex + + "] REPLACE_SOURCE structure '" + structureKey + "'"; + if (sampledVerticalEnvelopes == null || sampledVerticalEnvelopes.isEmpty()) { + blockingErrors.add(context + " has no sampled vertical envelope. Native generation will not" + + " be used as a fallback."); + return; + } + + DimensionVerticalBounds worldBounds = resolveDimensionVerticalBounds(dimension, context, blockingErrors); + PlacementVerticalBounds placementBounds = resolvePlacementVerticalBounds(placement, context, blockingErrors); + ObjectPlaceMode placeMode = resolvePlaceMode(structure, context, blockingErrors); + if (worldBounds == null || placementBounds == null || placeMode == null) { + return; + } + + for (StructureGraphPackValidator.SampledVerticalEnvelope sampled : sampledVerticalEnvelopes) { + boolean exactY = placementBounds.underground() + || sampled.pieceCount() > 1 + || placeMode == ObjectPlaceMode.STRUCTURE_PIECE + || placeMode == ObjectPlaceMode.FLOATING; + if (!exactY) { + continue; + } + + long minimumYOffset = sampled.minimumYOffset(); + long maximumYOffset = sampled.maximumYOffset(); + boolean surfaceAligned = !placementBounds.underground() + && sampled.pieceCount() > 1 + && placeMode != ObjectPlaceMode.STRUCTURE_PIECE + && placeMode != ObjectPlaceMode.FLOATING; + if (surfaceAligned) { + maximumYOffset -= minimumYOffset; + minimumYOffset = 0L; + } + + boolean fitsConfiguredRange; + if (placementBounds.underground()) { + long minimumAnchor = Math.max( + Math.max(placementBounds.minimumY(), worldBounds.minimumY()), + worldBounds.minimumY() - minimumYOffset); + long maximumAnchor = Math.min( + Math.min(placementBounds.maximumY(), worldBounds.maximumY()), + worldBounds.maximumY() - maximumYOffset); + fitsConfiguredRange = minimumAnchor <= maximumAnchor; + } else { + long minimumTerrainY = Math.max(placementBounds.minimumY(), worldBounds.minimumY()); + long maximumTerrainY = Math.min(placementBounds.maximumY(), worldBounds.maximumY()); + fitsConfiguredRange = minimumTerrainY <= maximumTerrainY + && minimumTerrainY + minimumYOffset >= worldBounds.minimumY() + && maximumTerrainY + maximumYOffset <= worldBounds.maximumY(); + } + if (fitsConfiguredRange) { + continue; + } + + String alignment = surfaceAligned ? "surface-aligned " : ""; + blockingErrors.add(context + " sampled seed " + sampled.seed() + " has an " + alignment + + "exact-Y piece envelope " + minimumYOffset + ".." + maximumYOffset + + " relative to its anchor, which cannot fit placement band " + + placementBounds.minimumY() + ".." + placementBounds.maximumY() + + " inside writable world " + worldBounds.minimumY() + ".." + worldBounds.maximumY() + + ". Native generation will not be used as a fallback."); + return; + } + } + + private static DimensionVerticalBounds resolveDimensionVerticalBounds( + JSONObject dimension, + String context, + List blockingErrors + ) { + long dimensionMinimum = -64L; + long dimensionMaximum = 320L; + if (dimension.has("dimensionHeight")) { + JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight"); + if (dimensionHeight == null) { + blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight" + + " must be an object."); + return null; + } + Long configuredMinimum = integralJsonNumber(dimensionHeight, "min", 16L); + Long configuredMaximum = integralJsonNumber(dimensionHeight, "max", 32L); + if (configuredMinimum == null || configuredMaximum == null) { + blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight" + + " min and max must be finite integer values."); + return null; + } + dimensionMinimum = configuredMinimum; + dimensionMaximum = configuredMaximum; + } + + long writableMinimum = dimensionMinimum + 1L; + long writableMaximum = dimensionMaximum - 1L; + if (writableMinimum > writableMaximum) { + blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight " + + dimensionMinimum + ".." + dimensionMaximum + " has no writable structure range."); + return null; + } + return new DimensionVerticalBounds(writableMinimum, writableMaximum); + } + + private static PlacementVerticalBounds resolvePlacementVerticalBounds( + JSONObject placement, + String context, + List blockingErrors + ) { + boolean underground = false; + if (placement.has("underground")) { + Object rawUnderground = placement.opt("underground"); + if (!(rawUnderground instanceof Boolean configuredUnderground)) { + blockingErrors.add(context + " cannot validate its vertical envelope because underground" + + " must be true or false."); + return null; + } + underground = configuredUnderground; + } + Long configuredMinimum = integralJsonNumber(placement, "minHeight", -2032L); + Long configuredMaximum = integralJsonNumber(placement, "maxHeight", 2032L); + if (configuredMinimum == null || configuredMaximum == null) { + blockingErrors.add(context + " cannot validate its vertical envelope because minHeight and" + + " maxHeight must be finite integer values."); + return null; + } + long minimumY = underground + ? Math.min(configuredMinimum, configuredMaximum) : configuredMinimum; + long maximumY = underground + ? Math.max(configuredMinimum, configuredMaximum) : configuredMaximum; + return new PlacementVerticalBounds(minimumY, maximumY, underground); + } + + private static ObjectPlaceMode resolvePlaceMode( + JSONObject structure, + String context, + List blockingErrors + ) { + if (!structure.has("placeMode")) { + return ObjectPlaceMode.STRUCTURE_PIECE; + } + Object rawPlaceMode = structure.opt("placeMode"); + if (!(rawPlaceMode instanceof String placeModeName)) { + blockingErrors.add(context + " cannot validate its vertical envelope because placeMode must name" + + " an Iris object place mode."); + return null; + } + try { + return ObjectPlaceMode.valueOf(placeModeName); + } catch (IllegalArgumentException exception) { + blockingErrors.add(context + " cannot validate its vertical envelope because placeMode '" + + placeModeName + "' is not supported."); + return null; + } + } + + private static Long integralJsonNumber(JSONObject owner, String field, long defaultValue) { + if (!owner.has(field)) { + return defaultValue; + } + Object rawValue = owner.opt(field); + if (!(rawValue instanceof Number number)) { + return null; + } + double value = number.doubleValue(); + if (!Double.isFinite(value) || value != Math.rint(value) + || value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { + return null; + } + return (long) value; + } + + private record DimensionVerticalBounds(long minimumY, long maximumY) { + } + + private record PlacementVerticalBounds(long minimumY, long maximumY, boolean underground) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackObjectSurfaceValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackObjectSurfaceValidator.java new file mode 100644 index 000000000..7b7e8c8a6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackObjectSurfaceValidator.java @@ -0,0 +1,232 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +final class PackObjectSurfaceValidator { + private PackObjectSurfaceValidator() { + } + + static Set collectPlacedStructureKeys(File packFolder) { + Set structureKeys = new LinkedHashSet<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return structureKeys; + } + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + for (File resourceFile : PackValidationIo.listJsonRecursive(resourceFolder)) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + JSONArray placements = resource.optJSONArray("structures"); + if (placements == null) { + continue; + } + for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { + JSONObject placement = placements.optJSONObject(placementIndex); + if (placement == null) { + continue; + } + JSONArray references = placement.optJSONArray("structures"); + if (references == null) { + continue; + } + for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) { + Object rawReference = references.opt(referenceIndex); + if (rawReference instanceof String structureKey && !structureKey.isBlank()) { + structureKeys.add(structureKey); + } + } + } + } + } + return Set.copyOf(structureKeys); + } + + static List validateUnsupportedStructureTransforms(File packFolder) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + for (File resourceFile : resourceFiles) { + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + JSONObject resource = PackStructurePlacementValidator.readGraphJson( + resourceFile, resourceType, resourceKey, blockingErrors); + if (resource == null) { + continue; + } + + JSONArray placements = resource.optJSONArray("structures"); + if (placements == null) { + continue; + } + for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { + JSONObject placement = placements.optJSONObject(placementIndex); + if (placement == null) { + continue; + } + for (String field : PackValidator.UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS) { + if (placement.has(field)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + placementIndex + + "] declares unsupported field '" + field + + "'. Structure placement transforms are not supported; remove the field."); + } + } + } + } + } + return blockingErrors; + } + + static List validateStructureGraph(File packFolder) { + return validateStructureGraph(packFolder, true); + } + + static List validateStructureGraph(File packFolder, boolean validateLiveRegistries) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + File structuresFolder = new File(packFolder, PackValidator.STRUCTURES_FOLDER); + File poolsFolder = new File(packFolder, PackValidator.JIGSAW_POOLS_FOLDER); + File piecesFolder = new File(packFolder, PackValidator.JIGSAW_PIECES_FOLDER); + File objectsFolder = new File(packFolder, PackValidator.OBJECTS_FOLDER); + Set structureKeys = ContentKeyValidator.deriveRegistrantKeysExact(structuresFolder); + Set poolKeys = ContentKeyValidator.deriveRegistrantKeysExact(poolsFolder); + Set pieceKeys = ContentKeyValidator.deriveRegistrantKeysExact(piecesFolder); + Set objectKeys = ContentKeyValidator.deriveObjectKeysExact(objectsFolder); + + PackStructurePlacementValidator.validateStructurePlacements( + packFolder, structureKeys, validateLiveRegistries, blockingErrors); + PackStructurePlacementValidator.validateStructureStartPools(structuresFolder, poolKeys, blockingErrors); + PackStructurePlacementValidator.validateJigsawPools(poolsFolder, poolKeys, pieceKeys, blockingErrors); + PackStructurePlacementValidator.validateJigsawPieces(piecesFolder, poolKeys, objectKeys, blockingErrors); + return blockingErrors; + } + + static List validateRemovedWorldgenFields(File packFolder) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + for (String field : PackValidator.REMOVED_WORLDGEN_FIELDS) { + if (resource.has(field)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' declares removed field '" + + field + "'. Remove it because fluid-body generation is not supported."); + } + } + } + } + return blockingErrors; + } + + static List validateObjectSurfaceSupport(File packFolder) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + String path = resourceType + " '" + PackValidationIo.deriveKey(resourceFolder, resourceFile) + "'"; + if ("dimensions".equals(folderName)) { + PackJsonFieldChecks.validateOptionalIntegerRange( + path, resource, "objectSurfaceSupportBuffer", 0, 16, blockingErrors); + PackJsonFieldChecks.validateOptionalBoolean( + path, resource, "requireObjectSurfaceSupport", blockingErrors); + } + validateObjectPlacementSurfaceSupport(path, resource.optJSONArray("objects"), blockingErrors); + } + } + return blockingErrors; + } + + private static void validateObjectPlacementSurfaceSupport(String path, JSONArray placements, + List blockingErrors) { + if (placements == null) { + return; + } + for (int i = 0; i < placements.length(); i++) { + JSONObject placement = placements.optJSONObject(i); + if (placement == null) { + continue; + } + String placementPath = path + ".objects[" + i + "]"; + if (placement.has("surfaceOpeningClearance")) { + blockingErrors.add(placementPath + " declares removed field 'surfaceOpeningClearance'. " + + "Use surfaceSupportBuffer instead."); + } + PackJsonFieldChecks.validateOptionalIntegerRange( + placementPath, placement, "surfaceSupportBuffer", 0, 16, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + placementPath, placement, "surfaceSupportDepth", 1, 16, blockingErrors); + PackJsonFieldChecks.validateOptionalBoolean( + placementPath, placement, "requireSurfaceSupport", blockingErrors); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackResourceCleanup.java b/core/src/main/java/art/arcane/iris/core/pack/PackResourceCleanup.java new file mode 100644 index 000000000..b352c1275 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackResourceCleanup.java @@ -0,0 +1,620 @@ +package art.arcane.iris.core.pack; + +import art.arcane.iris.spi.IrisLogging; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +public final class PackResourceCleanup { + private static final String TRASH_ROOT = ".iris-trash"; + private static final List MANAGED_RESOURCE_FOLDERS = List.of( + "biomes", + "regions", + "entities", + "spawners", + "loot", + "generators", + "expressions", + "markers", + "blocks", + "mods" + ); + private static final List CORPUS_EXCLUSIONS = List.of( + TRASH_ROOT, + "datapack-imports", + "externaldatapacks", + "internaldatapacks", + "datapacks", + "cache", + "objects", + ".iris" + ); + private static final DateTimeFormatter TRASH_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss-SSS"); + private static final Map PACK_LOCKS = new ConcurrentHashMap<>(); + + private PackResourceCleanup() { + } + + public static Preview preview(File packFolder) { + Path lockPath = lockPath(packFolder); + if (lockPath == null) { + return new Preview(List.of(), "Pack folder is required."); + } + synchronized (packLock(lockPath)) { + try { + Path packRoot = requirePackRoot(packFolder); + CleanupScan scan = scanCleanup(packRoot); + return new Preview(scan.paths(), null); + } catch (IOException | RuntimeException e) { + return new Preview(List.of(), errorMessage("Unable to scan pack resources", e)); + } + } + } + + public static ApplyResult apply(File packFolder) { + Path lockPath = lockPath(packFolder); + if (lockPath == null) { + return new ApplyResult(null, List.of(), "Pack folder is required."); + } + synchronized (packLock(lockPath)) { + try { + Path packRoot = requirePackRoot(packFolder); + CleanupScan scan = scanCleanup(packRoot); + if (scan.paths().isEmpty()) { + return new ApplyResult(null, List.of(), null); + } + return quarantine(packRoot, scan.paths()); + } catch (IOException | RuntimeException e) { + return new ApplyResult(null, List.of(), errorMessage("Unable to quarantine pack resources", e)); + } + } + } + + public static RestorePreview previewRestore(File packFolder) { + Path lockPath = lockPath(packFolder); + if (lockPath == null) { + return new RestorePreview(null, List.of(), List.of(), "Pack folder is required."); + } + synchronized (packLock(lockPath)) { + try { + Path packRoot = requirePackRoot(packFolder); + RestoreScan scan = scanRestore(packRoot); + return new RestorePreview(scan.dumpPath(), scan.paths(), scan.conflicts(), null); + } catch (IOException | RuntimeException e) { + return new RestorePreview(null, List.of(), List.of(), errorMessage("Unable to scan quarantined resources", e)); + } + } + } + + public static RestoreResult restoreLatest(File packFolder) { + Path lockPath = lockPath(packFolder); + if (lockPath == null) { + return new RestoreResult(null, List.of(), List.of(), "Pack folder is required."); + } + synchronized (packLock(lockPath)) { + try { + Path packRoot = requirePackRoot(packFolder); + RestoreScan scan = scanRestore(packRoot); + if (scan.dump() == null) { + return new RestoreResult(null, List.of(), List.of(), null); + } + if (!scan.conflicts().isEmpty()) { + return new RestoreResult(scan.dumpPath(), List.of(), scan.conflicts(), null); + } + return restore(packRoot, scan); + } catch (IOException | RuntimeException e) { + return new RestoreResult(null, List.of(), List.of(), errorMessage("Unable to restore quarantined resources", e)); + } + } + } + + private static ApplyResult quarantine(Path packRoot, List paths) throws IOException { + Path dump = createUniqueDump(packRoot); + List moved = new ArrayList<>(paths.size()); + try { + for (String path : paths) { + Path source = resolveContained(packRoot, path); + requireRegularFile(source, "Cleanup candidate"); + Path destination = resolveContained(dump, path); + if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) { + throw new FileAlreadyExistsException(destination.toString()); + } + Files.createDirectories(destination.getParent()); + Files.move(source, destination); + moved.add(new Transfer(path, source, destination)); + } + return new ApplyResult(relativePath(packRoot, dump), paths, null); + } catch (IOException | RuntimeException e) { + List remaining = rollbackMoves(moved); + deleteEmptyDirectories(dump); + deleteIfEmpty(dump.getParent()); + String rollbackState = remaining.isEmpty() + ? "Quarantine failed and was rolled back" + : "Quarantine failed and rollback left " + remaining.size() + " file(s) quarantined"; + IrisLogging.reportError(rollbackState + " for pack " + packRoot.getFileName(), e); + return new ApplyResult(relativePath(packRoot, dump), remaining, errorMessage(rollbackState, e)); + } + } + + private static RestoreResult restore(Path packRoot, RestoreScan scan) throws IOException { + List copiedDestinations = new ArrayList<>(scan.transfers().size()); + List createdDirectories = new ArrayList<>(); + try { + for (Transfer transfer : scan.transfers()) { + createDirectories(packRoot, transfer.destination().getParent(), createdDirectories); + Files.copy(transfer.source(), transfer.destination(), StandardCopyOption.COPY_ATTRIBUTES); + copiedDestinations.add(transfer.destination()); + } + } catch (IOException | RuntimeException e) { + List rollbackRemainders = rollbackCopies(packRoot, copiedDestinations, createdDirectories); + String rollbackState = rollbackRemainders.isEmpty() + ? "Restore copy failed and was rolled back" + : "Restore copy failed and rollback left " + rollbackRemainders.size() + " destination(s)"; + IrisLogging.reportError(rollbackState + " for pack " + packRoot.getFileName(), e); + return new RestoreResult(scan.dumpPath(), rollbackRemainders, List.of(), errorMessage(rollbackState, e)); + } + + List removedSources = new ArrayList<>(scan.transfers().size()); + for (Transfer transfer : scan.transfers()) { + try { + Files.delete(transfer.source()); + removedSources.add(transfer); + } catch (IOException | RuntimeException e) { + return rollbackSourceRemoval(packRoot, scan, copiedDestinations, createdDirectories, removedSources, e); + } + } + deleteEmptyDirectories(scan.dump()); + deleteIfEmpty(scan.dump().getParent()); + return new RestoreResult(scan.dumpPath(), scan.paths(), List.of(), null); + } + + private static CleanupScan scanCleanup(Path packRoot) throws IOException { + List corpusFiles = listTree(packRoot); + StringBuilder corpus = new StringBuilder(1 << 16); + for (Path path : corpusFiles) { + if (!isScannableJson(packRoot, path)) { + continue; + } + requireRegularFile(path, "Corpus file"); + corpus.append(Files.readString(path, StandardCharsets.UTF_8)).append('\n'); + } + if (corpus.isEmpty()) { + return new CleanupScan(List.of()); + } + String corpusText = corpus.toString(); + + List candidates = new ArrayList<>(); + for (String folderName : MANAGED_RESOURCE_FOLDERS) { + Path resourceFolder = resolveContained(packRoot, folderName); + if (!Files.exists(resourceFolder, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + requireDirectory(resourceFolder, "Managed resource folder"); + for (Path resource : listTree(resourceFolder)) { + if (!isJsonFile(resource)) { + continue; + } + requireRegularFile(resource, "Managed resource"); + String key = stripJsonExtension(relativePath(resourceFolder, resource)); + if (!key.isBlank() && !isReferenced(corpusText, key)) { + candidates.add(relativePath(packRoot, resource)); + } + } + } + candidates.sort(String::compareTo); + return new CleanupScan(List.copyOf(candidates)); + } + + private static RestoreScan scanRestore(Path packRoot) throws IOException { + Path trashRoot = resolveContained(packRoot, TRASH_ROOT); + if (!Files.exists(trashRoot, LinkOption.NOFOLLOW_LINKS)) { + return RestoreScan.empty(); + } + requireDirectory(trashRoot, "Quarantine root"); + List dumps; + try (Stream stream = Files.list(trashRoot)) { + dumps = stream.sorted(Comparator.comparing(path -> path.getFileName().toString())).toList(); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + List directories = new ArrayList<>(); + for (Path dump : dumps) { + if (Files.isSymbolicLink(dump)) { + throw new IOException("Quarantine entry is a symbolic link: " + dump); + } + if (Files.isDirectory(dump, LinkOption.NOFOLLOW_LINKS)) { + directories.add(dump); + } + } + if (directories.isEmpty()) { + return RestoreScan.empty(); + } + + Path latest = directories.get(directories.size() - 1); + String dumpPath = relativePath(packRoot, latest); + List transfers = new ArrayList<>(); + List conflicts = new ArrayList<>(); + for (Path source : listTree(latest)) { + if (Files.isDirectory(source, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + requireRegularFile(source, "Quarantined resource"); + String relative = relativePath(latest, source); + Path destination = resolveContained(packRoot, relative); + if (destination.startsWith(trashRoot)) { + throw new IOException("Quarantined resource resolves inside the quarantine root: " + relative); + } + if (hasDestinationConflict(packRoot, destination)) { + conflicts.add(relative); + } + transfers.add(new Transfer(relative, source, destination)); + } + transfers.sort(Comparator.comparing(Transfer::path)); + conflicts.sort(String::compareTo); + List paths = transfers.stream().map(Transfer::path).toList(); + return new RestoreScan(latest, dumpPath, List.copyOf(transfers), List.copyOf(paths), List.copyOf(conflicts)); + } + + private static List listTree(Path root) throws IOException { + requireDirectory(root, "Scan root"); + try (Stream stream = Files.walk(root)) { + List paths = stream.sorted(Comparator.comparing(path -> relativePath(root, path))).toList(); + for (Path path : paths) { + if (!path.equals(root) && Files.isSymbolicLink(path)) { + throw new IOException("Symbolic links are not supported during pack cleanup: " + path); + } + } + return paths; + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + + private static Path createUniqueDump(Path packRoot) throws IOException { + Path trashRoot = resolveContained(packRoot, TRASH_ROOT); + if (Files.exists(trashRoot, LinkOption.NOFOLLOW_LINKS)) { + requireDirectory(trashRoot, "Quarantine root"); + } else { + Files.createDirectory(trashRoot); + } + String baseName = LocalDateTime.now().format(TRASH_STAMP); + for (int attempt = 0; attempt < 10_000; attempt++) { + String name = attempt == 0 ? baseName : baseName + '-' + String.format(Locale.ROOT, "%04d", attempt); + Path dump = resolveContained(trashRoot, name); + try { + return Files.createDirectory(dump); + } catch (FileAlreadyExistsException ignored) { + } + } + throw new IOException("Unable to allocate a unique quarantine directory."); + } + + private static List rollbackMoves(List moved) { + for (int i = moved.size() - 1; i >= 0; i--) { + Transfer transfer = moved.get(i); + try { + if (!Files.exists(transfer.source(), LinkOption.NOFOLLOW_LINKS) + && Files.exists(transfer.destination(), LinkOption.NOFOLLOW_LINKS)) { + Files.move(transfer.destination(), transfer.source()); + } + } catch (IOException | RuntimeException e) { + IrisLogging.reportError("Failed to roll back quarantined resource '" + transfer.path() + "'", e); + } + } + List remaining = new ArrayList<>(); + for (Transfer transfer : moved) { + if (Files.exists(transfer.destination(), LinkOption.NOFOLLOW_LINKS)) { + remaining.add(transfer.path()); + } + } + remaining.sort(String::compareTo); + return List.copyOf(remaining); + } + + private static RestoreResult rollbackSourceRemoval(Path packRoot, + RestoreScan scan, + List copiedDestinations, + List createdDirectories, + List removedSources, + Throwable failure) { + List sourceRollbackFailures = new ArrayList<>(); + for (int i = removedSources.size() - 1; i >= 0; i--) { + Transfer transfer = removedSources.get(i); + try { + Files.copy(transfer.destination(), transfer.source(), StandardCopyOption.COPY_ATTRIBUTES); + } catch (IOException | RuntimeException e) { + sourceRollbackFailures.add(transfer.path()); + IrisLogging.reportError("Failed to restore quarantined source '" + transfer.path() + "' during rollback", e); + } + } + List removableDestinations = new ArrayList<>(); + for (Path destination : copiedDestinations) { + String path = relativePath(packRoot, destination); + Path source = scan.dump().resolve(path).normalize(); + if (Files.exists(source, LinkOption.NOFOLLOW_LINKS)) { + removableDestinations.add(destination); + } + } + List destinationRemainders = rollbackCopies(packRoot, removableDestinations, createdDirectories); + List remainders = new ArrayList<>(sourceRollbackFailures.size() + destinationRemainders.size()); + remainders.addAll(sourceRollbackFailures); + for (String destinationRemainder : destinationRemainders) { + if (!remainders.contains(destinationRemainder)) { + remainders.add(destinationRemainder); + } + } + remainders.sort(String::compareTo); + String rollbackState = remainders.isEmpty() + ? "Restore source removal failed and was rolled back" + : "Restore source removal failed and rollback left " + remainders.size() + " destination(s)"; + IrisLogging.reportError(rollbackState + " for pack " + packRoot.getFileName(), failure); + return new RestoreResult(scan.dumpPath(), remainders, List.of(), errorMessage(rollbackState, failure)); + } + + private static List rollbackCopies(Path packRoot, + List destinations, + List createdDirectories) { + for (int i = destinations.size() - 1; i >= 0; i--) { + try { + Files.deleteIfExists(destinations.get(i)); + } catch (IOException | RuntimeException e) { + IrisLogging.reportError("Failed to roll back restored resource '" + relativePath(packRoot, destinations.get(i)) + "'", e); + } + } + for (int i = createdDirectories.size() - 1; i >= 0; i--) { + deleteIfEmpty(createdDirectories.get(i)); + } + List remaining = new ArrayList<>(); + for (Path destination : destinations) { + if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) { + remaining.add(relativePath(packRoot, destination)); + } + } + remaining.sort(String::compareTo); + return List.copyOf(remaining); + } + + private static void createDirectories(Path packRoot, Path target, List createdDirectories) throws IOException { + Path contained = requireContained(packRoot, target); + List missing = new ArrayList<>(); + Path current = contained; + while (!current.equals(packRoot) && !Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { + missing.add(current); + current = current.getParent(); + } + if (!Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(current)) { + throw new IOException("Restore parent is not a safe directory: " + current); + } + for (int i = missing.size() - 1; i >= 0; i--) { + Path directory = missing.get(i); + Files.createDirectory(directory); + createdDirectories.add(directory); + } + } + + private static boolean hasDestinationConflict(Path packRoot, Path destination) throws IOException { + if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) { + return true; + } + Path parent = destination.getParent(); + while (parent != null && !parent.equals(packRoot)) { + if (Files.exists(parent, LinkOption.NOFOLLOW_LINKS) + && (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(parent))) { + return true; + } + parent = parent.getParent(); + } + return parent == null; + } + + private static void deleteEmptyDirectories(Path root) { + if (root == null || !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { + return; + } + try (Stream stream = Files.walk(root)) { + List directories = stream.filter(path -> Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) + .sorted(Comparator.reverseOrder()) + .toList(); + for (Path directory : directories) { + deleteIfEmpty(directory); + } + } catch (IOException | RuntimeException ignored) { + } + } + + private static void deleteIfEmpty(Path directory) { + if (directory == null || !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + return; + } + try (Stream stream = Files.list(directory)) { + if (stream.findAny().isEmpty()) { + Files.deleteIfExists(directory); + } + } catch (IOException | RuntimeException ignored) { + } + } + + private static boolean isScannableJson(Path packRoot, Path path) { + if (!isJsonFile(path)) { + return false; + } + Path relative = packRoot.relativize(path); + for (Path segment : relative) { + if (CORPUS_EXCLUSIONS.contains(segment.toString())) { + return false; + } + } + return true; + } + + private static boolean isJsonFile(Path path) { + return path.getFileName() != null && path.getFileName().toString().endsWith(".json") + && Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS); + } + + private static boolean isReferenced(String corpus, String key) { + if (corpus.contains("\"" + key + "\"")) { + return true; + } + int slash = key.indexOf('/'); + if (slash <= 0) { + return false; + } + String tail = key.substring(slash + 1); + return !tail.isBlank() && corpus.contains("\"" + tail + "\""); + } + + private static String stripJsonExtension(String path) { + return path.substring(0, path.length() - ".json".length()); + } + + private static Path requirePackRoot(File packFolder) throws IOException { + if (packFolder == null) { + throw new IOException("Pack folder is required."); + } + Path root = packFolder.toPath().toAbsolutePath().normalize(); + requireDirectory(root, "Pack folder"); + return root; + } + + private static void requireDirectory(Path path, String label) throws IOException { + if (Files.isSymbolicLink(path) || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException(label + " is not a safe directory: " + path); + } + } + + private static void requireRegularFile(Path path, String label) throws IOException { + if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException(label + " is not a safe regular file: " + path); + } + } + + private static Path resolveContained(Path root, String relative) throws IOException { + return requireContained(root, root.resolve(relative).normalize()); + } + + private static Path requireContained(Path root, Path path) throws IOException { + Path normalizedRoot = root.toAbsolutePath().normalize(); + Path normalizedPath = path.toAbsolutePath().normalize(); + if (!normalizedPath.startsWith(normalizedRoot)) { + throw new IOException("Path escapes pack boundary: " + path); + } + return normalizedPath; + } + + private static String relativePath(Path root, Path path) { + return root.relativize(path).toString().replace(File.separatorChar, '/'); + } + + private static Path lockPath(File packFolder) { + return packFolder == null ? null : packFolder.toPath().toAbsolutePath().normalize(); + } + + private static Object packLock(Path path) { + return PACK_LOCKS.computeIfAbsent(path, ignored -> new Object()); + } + + private static String errorMessage(String prefix, Throwable error) { + String message = error.getMessage(); + return prefix + (message == null || message.isBlank() ? "." : ": " + message); + } + + public record Preview(List candidatePaths, String error) { + public Preview { + candidatePaths = candidatePaths == null ? List.of() : List.copyOf(candidatePaths); + } + + public boolean success() { + return error == null; + } + + public boolean hasCandidates() { + return !candidatePaths.isEmpty(); + } + } + + public record ApplyResult(String quarantinePath, List quarantinedPaths, String error) { + public ApplyResult { + quarantinedPaths = quarantinedPaths == null ? List.of() : List.copyOf(quarantinedPaths); + } + + public boolean success() { + return error == null; + } + + public boolean changed() { + return !quarantinedPaths.isEmpty(); + } + } + + public record RestorePreview(String dumpPath, List filePaths, List conflicts, String error) { + public RestorePreview { + filePaths = filePaths == null ? List.of() : List.copyOf(filePaths); + conflicts = conflicts == null ? List.of() : List.copyOf(conflicts); + } + + public boolean success() { + return error == null; + } + + public boolean hasFiles() { + return !filePaths.isEmpty(); + } + + public boolean canRestore() { + return success() && hasFiles() && conflicts.isEmpty(); + } + + public boolean hasConflicts() { + return !conflicts.isEmpty(); + } + } + + public record RestoreResult(String dumpPath, List restoredPaths, List conflicts, String error) { + public RestoreResult { + restoredPaths = restoredPaths == null ? List.of() : List.copyOf(restoredPaths); + conflicts = conflicts == null ? List.of() : List.copyOf(conflicts); + } + + public boolean success() { + return error == null && conflicts.isEmpty(); + } + + public boolean changed() { + return !restoredPaths.isEmpty(); + } + + public boolean hasConflicts() { + return !conflicts.isEmpty(); + } + } + + private record CleanupScan(List paths) { + } + + private record RestoreScan(Path dump, String dumpPath, List transfers, List paths, + List conflicts) { + private static RestoreScan empty() { + return new RestoreScan(null, null, List.of(), List.of(), List.of()); + } + } + + private record Transfer(String path, Path source, Path destination) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackSpawnValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackSpawnValidator.java new file mode 100644 index 000000000..7e2d1f470 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackSpawnValidator.java @@ -0,0 +1,343 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformEntityType; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +final class PackSpawnValidator { + private PackSpawnValidator() { + } + + static List validateSpawnerEntityReferences(File spawnersFolder, File entitiesFolder) { + List blockingErrors = new ArrayList<>(); + if (spawnersFolder == null || !spawnersFolder.isDirectory()) { + return blockingErrors; + } + + Path entityRoot = entitiesFolder.toPath().toAbsolutePath().normalize(); + Set validEntityFiles = new HashSet<>(); + Map invalidEntityFiles = new HashMap<>(); + List spawnerFiles = PackValidationIo.listJsonRecursive(spawnersFolder); + spawnerFiles.sort(Comparator.comparing(File::getPath)); + for (File spawnerFile : spawnerFiles) { + String spawnerKey = PackValidationIo.deriveKey(spawnersFolder, spawnerFile); + JSONObject spawner; + try { + spawner = new JSONObject(Files.readString(spawnerFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Spawner '" + spawnerKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + validateSpawnerSpawnEntries(spawnerKey, spawner, "spawns", entityRoot, + validEntityFiles, invalidEntityFiles, blockingErrors); + validateSpawnerSpawnEntries(spawnerKey, spawner, "initialSpawns", entityRoot, + validEntityFiles, invalidEntityFiles, blockingErrors); + } + return blockingErrors; + } + + private static void validateSpawnerSpawnEntries(String spawnerKey, + JSONObject spawner, + String field, + Path entityRoot, + Set validEntityFiles, + Map invalidEntityFiles, + List blockingErrors) { + if (!spawner.has(field)) { + return; + } + JSONArray entries = spawner.optJSONArray(field); + if (entries == null) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " must be an array."); + return; + } + + for (int index = 0; index < entries.length(); index++) { + JSONObject entry = entries.optJSONObject(index); + if (entry == null) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + + " has a non-object entry at index " + index + "."); + continue; + } + if (!entry.has("entity") || entry.isNull("entity")) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + + " has an entry without an entity reference at index " + index + "."); + continue; + } + + Object rawEntity = entry.get("entity"); + if (!(rawEntity instanceof String entityKey)) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + + " entity reference at index " + index + " must be a string."); + continue; + } + if (entityKey.isBlank()) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + + " has a blank entity reference at index " + index + "."); + continue; + } + + Path entityFile; + try { + if (entityKey.indexOf('\\') >= 0) { + throw new IllegalArgumentException("backslash path separators are not portable"); + } + entityFile = entityRoot.resolve(entityKey + ".json").normalize(); + } catch (RuntimeException e) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " has invalid entity reference '" + entityKey + "': " + e.getMessage()); + continue; + } + if (!entityFile.startsWith(entityRoot)) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " has unsafe entity reference '" + entityKey + "'."); + continue; + } + if (!Files.isRegularFile(entityFile)) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " references missing entity '" + entityKey + "'."); + continue; + } + + String invalidJson = invalidEntityFiles.get(entityFile); + if (invalidJson != null) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " references malformed entity '" + entityKey + "': " + invalidJson); + continue; + } + if (validEntityFiles.contains(entityFile)) { + continue; + } + + try { + new JSONObject(Files.readString(entityFile, StandardCharsets.UTF_8)); + validEntityFiles.add(entityFile); + } catch (Throwable e) { + String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); + invalidEntityFiles.put(entityFile, message); + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " references malformed entity '" + entityKey + "': " + message); + } + } + } + + static List validateCustomBiomeSpawns(File biomesFolder, Function categoryResolver) { + List blockingErrors = new ArrayList<>(); + if (biomesFolder == null || !biomesFolder.isDirectory()) { + return blockingErrors; + } + + List biomeFiles = PackValidationIo.listJsonRecursive(biomesFolder); + biomeFiles.sort(Comparator.comparing(File::getPath)); + for (File biomeFile : biomeFiles) { + String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile); + JSONObject biome; + try { + biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Biome '" + biomeKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + JSONArray derivatives = biome.optJSONArray("customDerivitives"); + if (derivatives == null) { + if (biome.has("customDerivitives") && !biome.isNull("customDerivitives")) { + blockingErrors.add("Biome '" + biomeKey + "' customDerivitives must be an array."); + } + continue; + } + for (int derivativeIndex = 0; derivativeIndex < derivatives.length(); derivativeIndex++) { + JSONObject derivative = derivatives.optJSONObject(derivativeIndex); + if (derivative == null) { + blockingErrors.add("Biome '" + biomeKey + "' has a non-object custom derivative at index " + derivativeIndex + "."); + continue; + } + validateCustomBiomeDerivativeTags(biomeKey, derivative, derivativeIndex, blockingErrors); + validateCustomBiomeDerivativeSpawns( + biomeKey, derivative, derivativeIndex, categoryResolver, blockingErrors); + } + } + return blockingErrors; + } + + private static void validateCustomBiomeDerivativeTags(String biomeKey, + JSONObject derivative, + int derivativeIndex, + List blockingErrors) { + if (!derivative.has("tags") || derivative.isNull("tags")) { + return; + } + String derivativeId = derivative.optString("id", "#" + derivativeIndex); + JSONArray tags = derivative.optJSONArray("tags"); + if (tags == null) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' tags must be an array."); + return; + } + for (int tagIndex = 0; tagIndex < tags.length(); tagIndex++) { + Object rawTag = tags.opt(tagIndex); + if (!(rawTag instanceof String tag)) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' has a non-string tag at index " + tagIndex + "."); + continue; + } + String normalized = tag.trim().toLowerCase(Locale.ROOT); + if (normalized.indexOf(':') < 0) { + normalized = "minecraft:" + normalized; + } + if (!isSafeResourceKey(normalized)) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' has invalid tag '" + tag + "'."); + } + } + } + + private static boolean isSafeResourceKey(String key) { + if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(key).matches()) { + return false; + } + int separator = key.indexOf(':'); + String[] segments = key.substring(separator + 1).split("/"); + for (String segment : segments) { + if (segment.equals("..")) { + return false; + } + } + return true; + } + + private static void validateCustomBiomeDerivativeSpawns(String biomeKey, + JSONObject derivative, + int derivativeIndex, + Function categoryResolver, + List blockingErrors) { + JSONArray spawns = derivative.optJSONArray("spawns"); + if (spawns == null) { + if (derivative.has("spawns") && !derivative.isNull("spawns")) { + String derivativeId = derivative.optString("id", "#" + derivativeIndex); + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawns must be an array."); + } + return; + } + String derivativeId = derivative.optString("id", "#" + derivativeIndex); + for (int spawnIndex = 0; spawnIndex < spawns.length(); spawnIndex++) { + JSONObject spawn = spawns.optJSONObject(spawnIndex); + if (spawn == null) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' has a non-object spawn at index " + spawnIndex + "."); + continue; + } + + String type = spawn.optString("type", "").trim().toLowerCase(Locale.ROOT); + if (type.isEmpty()) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' has a spawn without an entity type at index " + spawnIndex + "."); + continue; + } + String typeKey = type.indexOf(':') >= 0 ? type : "minecraft:" + type; + SpawnCategoryResolution resolution; + try { + resolution = categoryResolver == null ? null : categoryResolver.apply(typeKey); + } catch (Throwable e) { + IrisLogging.reportError("PackValidator failed to resolve spawn category for '" + typeKey + "'", e); + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn category lookup failed for '" + typeKey + "': " + e.getMessage()); + continue; + } + if (resolution != null && !resolution.entityKnown()) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn references unknown entity type '" + typeKey + "'."); + continue; + } + String expectedGroup = resolution == null ? null : resolution.category(); + String group = spawn.optString("group", "").trim(); + if (group.isEmpty()) { + if (expectedGroup != null && !expectedGroup.isBlank() + && !IrisBiomeCustomSpawnType.MISC.name().equalsIgnoreCase(expectedGroup)) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn '" + typeKey + "' must declare group '" + + expectedGroup.toUpperCase(Locale.ROOT) + "'."); + } + continue; + } + + IrisBiomeCustomSpawnType configuredGroup; + try { + configuredGroup = IrisBiomeCustomSpawnType.valueOf(group); + } catch (IllegalArgumentException e) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn '" + typeKey + "' declares unknown group '" + group + "'."); + continue; + } + + if (expectedGroup != null && !expectedGroup.isBlank() + && !configuredGroup.name().equalsIgnoreCase(expectedGroup)) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn '" + typeKey + "' declares group '" + configuredGroup.name() + + "' but the live entity registry requires '" + expectedGroup.toUpperCase(Locale.ROOT) + "'."); + } + } + } + + static SpawnCategoryResolution resolveEntitySpawnCategory(String typeKey) { + if (!IrisPlatforms.isBound()) { + return null; + } + PlatformRegistries registries = IrisPlatforms.get().registries(); + if (registries == null) { + return null; + } + PlatformEntityType entityType = registries.entity(typeKey); + return entityType == null + ? SpawnCategoryResolution.unknown() + : SpawnCategoryResolution.known(entityType.spawnCategory()); + } + + record SpawnCategoryResolution(boolean entityKnown, String category) { + static SpawnCategoryResolution unknown() { + return new SpawnCategoryResolution(false, null); + } + + static SpawnCategoryResolution known(String category) { + return new SpawnCategoryResolution(true, category); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java new file mode 100644 index 000000000..4d95b5e4a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java @@ -0,0 +1,844 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +final class PackStructurePlacementValidator { + private static final Set TERRAIN_ENVELOPE_MODES = Set.of( + "BORE", "FORCE_CARVE", "VACUUM", "ENCASE"); + + private PackStructurePlacementValidator() { + } + + static void validateStructurePlacements(File packFolder, + Set structureKeys, + boolean validateLiveRegistries, + List blockingErrors) { + RegistrySnapshot registries = registrySnapshot(validateLiveRegistries, blockingErrors); + if (registries == null) { + return; + } + Map placementIds = new HashMap<>(); + Map anonymousGridIdentities = new HashMap<>(); + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = structureHostType(folderName); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + if (PackValidator.DIMENSIONS_FOLDER.equals(folderName)) { + validateImportedStructureAdjustmentEnvelopes( + resourceKey, resource, registries, blockingErrors); + } + Object rawPlacements = resource.opt("structures"); + JSONArray placements = resource.optJSONArray("structures"); + if (placements == null) { + if (resource.has("structures") && rawPlacements != JSONObject.NULL) { + blockingErrors.add(resourceType + " '" + resourceKey + "'.structures must be an array."); + } + continue; + } + for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { + JSONObject placement = placements.optJSONObject(placementIndex); + String placementPath = resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "]"; + if (placement == null) { + blockingErrors.add(placementPath + " must be an object."); + continue; + } + validatePlacementConfiguration(placementPath, placement, blockingErrors); + validatePlacementIdentity( + placementPath, placement, placementIds, anonymousGridIdentities, blockingErrors); + JSONArray references = placement.optJSONArray("structures"); + JSONArray nativeStructures = placement.optJSONArray("nativeStructures"); + if (placement.has("structures") && placement.opt("structures") != JSONObject.NULL + && references == null) { + blockingErrors.add(placementPath + ".structures must be an array."); + } + if (placement.has("nativeStructures") && placement.opt("nativeStructures") != JSONObject.NULL + && nativeStructures == null) { + blockingErrors.add(placementPath + ".nativeStructures must be an array."); + } + boolean hasIrisStructures = references != null && references.length() > 0; + boolean hasNativeStructures = nativeStructures != null && nativeStructures.length() > 0; + if (hasIrisStructures == hasNativeStructures) { + blockingErrors.add(placementPath + + " must declare exactly one non-empty backend: structures or nativeStructures."); + continue; + } + if (hasNativeStructures) { + validateNativeStructures( + placementPath, nativeStructures, + registries.structures(), registries.jigsaws(), + registries.pools(), registries.jigsawMetadataResolver(), + registries.hooks(), blockingErrors); + continue; + } + validateEditableStructureTerrain(placementPath, placement, blockingErrors); + Set editableStructureKeys = new HashSet<>(); + for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) { + Object rawReference = references.opt(referenceIndex); + if (!(rawReference instanceof String structureKey) || structureKey.isBlank()) { + blockingErrors.add(placementPath + ".structures[" + referenceIndex + + "] must name a non-blank Iris structure."); + continue; + } + String normalizedStructureKey = structureKey.trim().toLowerCase(Locale.ROOT); + if (!editableStructureKeys.add(normalizedStructureKey)) { + blockingErrors.add(placementPath + ".structures[" + referenceIndex + + "] duplicates Iris structure '" + structureKey + "'."); + continue; + } + if (!structureKeys.contains(structureKey)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "].structures[" + referenceIndex + + "] references missing structure '" + structureKey + "'."); + } + } + } + } + } + } + + private static RegistrySnapshot registrySnapshot(boolean validateLiveRegistries, + List blockingErrors) { + if (!validateLiveRegistries || !IrisPlatforms.isBound()) { + return new RegistrySnapshot(Set.of(), Set.of(), Set.of(), null, null); + } + try { + PlatformStructureHooks hooks = IrisPlatforms.get().structureHooks(); + if (hooks == null) { + throw new IllegalStateException("The active platform did not provide structure registry hooks"); + } + Set structures = normalizeRegistryKeys( + hooks.structureKeys(), "structure"); + Set jigsaws = normalizeRegistryKeys( + hooks.jigsawStructureKeys(), "jigsaw structure"); + Set pools = normalizeRegistryKeys( + hooks.templatePoolKeys(), "template pool"); + if (structures.isEmpty()) { + throw new IllegalStateException("The active structure registry is empty"); + } + if (jigsaws.isEmpty()) { + throw new IllegalStateException("The active jigsaw structure registry is empty"); + } + if (pools.isEmpty()) { + throw new IllegalStateException("The active template pool registry is empty"); + } + return new RegistrySnapshot( + structures, jigsaws, pools, new JigsawMetadataResolver(hooks), hooks); + } catch (RuntimeException | LinkageError e) { + IrisLogging.reportError("Could not read the live structure registries during pack validation", e); + blockingErrors.add("Could not validate native structure references against the live registries: " + + failureMessage(e) + "."); + return null; + } + } + + private static Set normalizeRegistryKeys(List registered, String registryName) { + if (registered == null) { + throw new IllegalStateException("The active " + registryName + " registry returned null"); + } + Set keys = new HashSet<>(); + for (String key : registered) { + if (key != null && !key.isBlank()) { + keys.add(key.toLowerCase(Locale.ROOT)); + } + } + return Set.copyOf(keys); + } + + private static String failureMessage(Throwable throwable) { + String message = throwable.getMessage(); + return message == null || message.isBlank() ? throwable.getClass().getSimpleName() : message; + } + + private static void validatePlacementConfiguration(String path, JSONObject placement, + List blockingErrors) { + PackJsonFieldChecks.validateOptionalEnum(path, placement, "distribution", + Set.of("RANDOM_SPREAD", "DENSITY", "CONCENTRIC_RINGS"), blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, placement, "spacing", 1, 4096, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, placement, "separation", 0, Integer.MAX_VALUE, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, placement, "salt", Integer.MIN_VALUE, Integer.MAX_VALUE, blockingErrors); + PackJsonFieldChecks.validateOptionalDoubleRange(path, placement, "density", 0D, 1D, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, placement, "ringCount", 1, Integer.MAX_VALUE, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, placement, "ringDistance", 1, Integer.MAX_VALUE, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, placement, "ringSpread", 1, Integer.MAX_VALUE, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, placement, "minHeight", Integer.MIN_VALUE, Integer.MAX_VALUE, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, placement, "maxHeight", Integer.MIN_VALUE, Integer.MAX_VALUE, blockingErrors); + PackJsonFieldChecks.validateOptionalBoolean(path, placement, "underground", blockingErrors); + PackJsonFieldChecks.validateOptionalBoolean(path, placement, "underwater", blockingErrors); + PackJsonFieldChecks.validateOptionalEnum(path, placement, "nativeSuppression", + Set.of("NONE", "REPLACE_SOURCE"), blockingErrors); + + Integer spacing = integerValue(placement, "spacing", 32); + Integer separation = integerValue(placement, "separation", 8); + if (spacing != null && separation != null && separation >= spacing) { + blockingErrors.add(path + ".separation must be smaller than spacing."); + } + Integer minimumHeight = integerValue(placement, "minHeight", -2032); + Integer maximumHeight = integerValue(placement, "maxHeight", 2032); + if (minimumHeight != null && maximumHeight != null && minimumHeight > maximumHeight) { + blockingErrors.add(path + " has an inverted height band: minHeight must not exceed maxHeight."); + } + Integer ringCount = integerValue(placement, "ringCount", 128); + Integer ringDistance = integerValue(placement, "ringDistance", 32); + Integer ringSpread = integerValue(placement, "ringSpread", 3); + if (ringCount != null && ringDistance != null && ringSpread != null + && ringCount > 0 && ringDistance > 0 && ringSpread > 0) { + long ringRadius = (long) Math.ceilDiv(ringCount, ringSpread) * ringDistance; + if (ringRadius > Integer.MAX_VALUE) { + blockingErrors.add(path + " concentric ring radius exceeds the supported chunk coordinate range."); + } + } + + if (placement.has("placementId")) { + Object rawPlacementId = placement.opt("placementId"); + if (!(rawPlacementId instanceof String placementId)) { + blockingErrors.add(path + ".placementId must be a string."); + } else if (!placementId.isEmpty() && placementId.isBlank()) { + blockingErrors.add(path + ".placementId must be omitted or non-blank."); + } else if (!placementId.equals(placementId.trim())) { + blockingErrors.add(path + ".placementId must not contain leading or trailing whitespace."); + } + } + validateStilt(path, placement, blockingErrors); + validateNativeTerrain(path, placement, blockingErrors); + } + + private static void validatePlacementIdentity(String path, JSONObject placement, + Map placementIds, + Map anonymousGridIdentities, + List blockingErrors) { + Object rawPlacementId = placement.opt("placementId"); + if (rawPlacementId instanceof String placementId && !placementId.isBlank()) { + String normalizedId = placementId.trim(); + String existing = placementIds.putIfAbsent(normalizedId, path); + if (existing != null) { + blockingErrors.add(path + ".placementId duplicates '" + normalizedId + + "' already declared by " + existing + "."); + } + return; + } + String gridIdentity = anonymousGridIdentity(placement); + String existing = anonymousGridIdentities.putIfAbsent(gridIdentity, path); + if (existing != null) { + blockingErrors.add(path + " duplicates an anonymous placement grid already declared by " + + existing + "; give distinct placements unique placementId values."); + } + } + + private static String anonymousGridIdentity(JSONObject placement) { + StringBuilder identity = new StringBuilder(); + appendIdentity(identity, placement.optString("distribution", "RANDOM_SPREAD")); + appendIdentity(identity, integerValue(placement, "salt", 165745296)); + appendIdentity(identity, integerValue(placement, "spacing", 32)); + appendIdentity(identity, integerValue(placement, "separation", 8)); + Object rawDensity = placement.has("density") ? placement.opt("density") : 0.02D; + appendIdentity(identity, rawDensity instanceof Number number + ? Double.doubleToLongBits(number.doubleValue()) : rawDensity); + appendIdentity(identity, integerValue(placement, "ringCount", 128)); + appendIdentity(identity, integerValue(placement, "ringDistance", 32)); + appendIdentity(identity, integerValue(placement, "ringSpread", 3)); + appendIdentity(identity, integerValue(placement, "minHeight", -2032)); + appendIdentity(identity, integerValue(placement, "maxHeight", 2032)); + appendIdentity(identity, placement.opt("underground") instanceof Boolean underground && underground); + appendIdentity(identity, placement.opt("underwater") instanceof Boolean underwater && underwater); + JSONArray structures = placement.optJSONArray("structures"); + JSONArray nativeStructures = placement.optJSONArray("nativeStructures"); + if (structures != null && structures.length() > 0) { + appendIdentity(identity, "iris"); + List orderedStructures = new ArrayList<>(structures.length()); + for (int index = 0; index < structures.length(); index++) { + orderedStructures.add(String.valueOf(structures.opt(index))); + } + orderedStructures.sort(String::compareTo); + for (String structure : orderedStructures) { + appendIdentity(identity, structure); + } + } else { + appendIdentity(identity, "native"); + if (nativeStructures != null) { + List orderedSources = new ArrayList<>(nativeStructures.length()); + for (int index = 0; index < nativeStructures.length(); index++) { + JSONObject source = nativeStructures.optJSONObject(index); + String sourceKey = source == null ? "null" : source.optString("structure", ""); + Integer weight = source == null ? null : integerValue(source, "weight", 1); + orderedSources.add(sourceKey.length() + ":" + sourceKey + ":" + weight); + } + orderedSources.sort(String::compareTo); + for (String source : orderedSources) { + appendIdentity(identity, source); + } + } + } + return identity.toString(); + } + + private static void appendIdentity(StringBuilder identity, Object value) { + String text = String.valueOf(value); + identity.append(text.length()).append(':').append(text).append('|'); + } + + private static void validateStilt(String path, JSONObject placement, List blockingErrors) { + if (!placement.has("stilt") || placement.opt("stilt") == JSONObject.NULL) { + return; + } + JSONObject stilt = placement.optJSONObject("stilt"); + if (stilt == null) { + blockingErrors.add(path + ".stilt must be an object."); + return; + } + PackJsonFieldChecks.validateOptionalIntegerRange( + path + ".stilt", stilt, "maxDepth", 1, 4064, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path + ".stilt", stilt, "spacing", 1, 64, blockingErrors); + PackJsonFieldChecks.validateOptionalBoolean( + path + ".stilt", stilt, "supportNonOccluding", blockingErrors); + if (stilt.has("palette") && stilt.opt("palette") != JSONObject.NULL + && stilt.optJSONObject("palette") == null) { + blockingErrors.add(path + ".stilt.palette must be an object."); + } + } + + private static Integer integerValue(JSONObject object, String field, int defaultValue) { + if (!object.has(field)) { + return defaultValue; + } + Object rawValue = object.opt(field); + if (!(rawValue instanceof Number number)) { + return null; + } + double doubleValue = number.doubleValue(); + if (!Double.isFinite(doubleValue) || Math.rint(doubleValue) != doubleValue + || doubleValue < Integer.MIN_VALUE || doubleValue > Integer.MAX_VALUE) { + return null; + } + return number.intValue(); + } + + private static void validateNativeStructures(String placementPath, + JSONArray nativeStructures, + Set registeredStructures, + Set registeredJigsaws, + Set registeredPools, + JigsawMetadataResolver jigsawMetadataResolver, + PlatformStructureHooks hooks, + List blockingErrors) { + Set sourceKeys = new HashSet<>(); + long totalWeight = 0L; + for (int sourceIndex = 0; sourceIndex < nativeStructures.length(); sourceIndex++) { + String sourcePath = placementPath + ".nativeStructures[" + sourceIndex + "]"; + JSONObject source = nativeStructures.optJSONObject(sourceIndex); + if (source == null) { + blockingErrors.add(sourcePath + " must be an object."); + continue; + } + String structureKey = source.optString("structure", "").trim(); + if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(structureKey).matches()) { + blockingErrors.add(sourcePath + ".structure must be a namespaced registry key."); + } else if (!registeredStructures.isEmpty() + && !registeredStructures.contains(structureKey.toLowerCase(Locale.ROOT))) { + blockingErrors.add(sourcePath + ".structure '" + structureKey + + "' is not a registered structure."); + } + if (!structureKey.isEmpty() && !sourceKeys.add(structureKey.toLowerCase(Locale.ROOT))) { + blockingErrors.add(sourcePath + ".structure duplicates native source '" + structureKey + "'."); + } + Integer weight = PackLootValidator.lootInteger(source, "weight", 1, sourcePath, blockingErrors); + PackLootValidator.requireMinimum(sourcePath + ".weight", weight, 1, blockingErrors); + if (weight != null && weight > 0) { + totalWeight += weight; + } + String normalizedStructureKey = structureKey.toLowerCase(Locale.ROOT); + boolean registeredJigsaw = registeredJigsaws.contains(normalizedStructureKey); + JSONObject jigsaw = source.optJSONObject("jigsaw"); + if (source.has("jigsaw") && source.opt("jigsaw") != JSONObject.NULL && jigsaw == null) { + blockingErrors.add(sourcePath + ".jigsaw must be an object."); + } else if (jigsaw != null) { + if (!registeredJigsaws.isEmpty() && !registeredJigsaw) { + blockingErrors.add(sourcePath + + ".jigsaw requires a registered jigsaw structure."); + } + validateJigsawAssembly( + sourcePath + ".jigsaw", jigsaw, registeredPools, blockingErrors); + } + JigsawSourceMetadata sourceMetadata = registeredJigsaw + ? resolveJigsawMetadata(sourcePath, normalizedStructureKey, + jigsawMetadataResolver, blockingErrors) + : null; + if (registeredJigsaw && sourceMetadata == null) { + continue; + } + if (registeredJigsaw || jigsaw != null) { + validateReferenceEnvelope( + sourcePath, normalizedStructureKey, jigsaw, + sourceMetadata, hooks, blockingErrors); + } + } + if (totalWeight > Integer.MAX_VALUE) { + blockingErrors.add(placementPath + ".nativeStructures total weight exceeds " + + Integer.MAX_VALUE + "."); + } + } + + private static void validateReferenceEnvelope(String sourcePath, String structureKey, + JSONObject jigsaw, JigsawSourceMetadata sourceMetadata, + PlatformStructureHooks hooks, + List blockingErrors) { + boolean overriddenDistance = jigsaw != null && jigsaw.has("maxDistanceHorizontal"); + Integer maximumDistance = overriddenDistance + ? integerValue(jigsaw, "maxDistanceHorizontal", -1) + : sourceMetadata == null ? null : sourceMetadata.maxDistanceHorizontal(); + if (maximumDistance == null || maximumDistance < 0) { + return; + } + Integer startElementSpan = effectiveStartElementSpan( + sourcePath, structureKey, jigsaw, sourceMetadata, hooks, blockingErrors); + if (startElementSpan == null) { + return; + } + long assemblySpan = Math.max(maximumDistance, startElementSpan); + if (assemblySpan > 128L) { + String distancePath = overriddenDistance + ? ".jigsaw.maxDistanceHorizontal" : " registered jigsaw source max-distance"; + blockingErrors.add(sourcePath + distancePath + " and its actual structure content" + + " (" + startElementSpan + "-block maximum start element and " + + maximumDistance + "-block maximum assembly distance) must not exceed Minecraft's" + + " 128-block (8-chunk) structure reference range."); + } + } + + private static Integer effectiveStartElementSpan(String sourcePath, String structureKey, + JSONObject jigsaw, + JigsawSourceMetadata sourceMetadata, + PlatformStructureHooks hooks, + List blockingErrors) { + int sourceSpan = sourceMetadata == null ? 0 : sourceMetadata.maxStartElementHorizontalSpan(); + if (jigsaw == null || !jigsaw.has("startPool")) { + return sourceSpan; + } + String startPool = jigsaw.optString("startPool", "").trim(); + if (startPool.isEmpty() || hooks == null) { + return sourceSpan; + } + try { + int resolvedSpan = hooks.jigsawStartPoolHorizontalSpan(structureKey, startPool); + if (resolvedSpan < 0) { + throw new IllegalStateException("negative horizontal span " + resolvedSpan); + } + return resolvedSpan; + } catch (RuntimeException | LinkageError error) { + IrisLogging.reportError("Could not resolve the effective native jigsaw start-pool span", error); + blockingErrors.add(sourcePath + ".jigsaw.startPool '" + startPool + + "' could not resolve a bounded live horizontal span: " + + failureMessage(error) + "."); + return null; + } + } + + private static void validateImportedStructureAdjustmentEnvelopes( + String dimensionKey, JSONObject dimension, RegistrySnapshot registries, + List blockingErrors) { + JSONObject policy = dimension.optJSONObject("importedStructures"); + if (policy == null) { + return; + } + JSONArray adjustments = policy.optJSONArray("adjustments"); + if (adjustments == null) { + return; + } + List orderedStructureKeys = new ArrayList<>(registries.structures()); + orderedStructureKeys.sort(String::compareTo); + Map effective = new HashMap<>(); + for (int adjustmentIndex = 0; adjustmentIndex < adjustments.length(); adjustmentIndex++) { + JSONObject adjustment = adjustments.optJSONObject(adjustmentIndex); + if (adjustment == null || adjustment.optJSONObject("terrain") == null) { + continue; + } + JSONArray matches = adjustment.optJSONArray("match"); + if (matches == null || matches.length() == 0) { + continue; + } + String path = "Dimension '" + dimensionKey + + "' importedStructures.adjustments[" + adjustmentIndex + "]"; + for (String structureKey : orderedStructureKeys) { + if (matchesStructure(matches, structureKey)) { + effective.put(structureKey, new EffectiveTerrainAdjustment(path, adjustment)); + } + } + } + for (String structureKey : orderedStructureKeys) { + EffectiveTerrainAdjustment adjustment = effective.get(structureKey); + if (adjustment == null) { + continue; + } + JSONObject terrain = adjustment.adjustment().optJSONObject("terrain"); + if (terrain == null || !TERRAIN_ENVELOPE_MODES.contains( + terrain.optString("mode", "SOURCE").toUpperCase(Locale.ROOT))) { + continue; + } + String sourcePath = adjustment.path() + " matched registered structure '" + + structureKey + "'"; + if (registries.jigsaws().contains(structureKey)) { + JigsawSourceMetadata metadata = resolveJigsawMetadata( + sourcePath, structureKey, + registries.jigsawMetadataResolver(), blockingErrors); + if (metadata == null) { + continue; + } + validateReferenceEnvelope( + sourcePath, structureKey, null, metadata, + registries.hooks(), blockingErrors); + } + } + } + + private static boolean matchesStructure(JSONArray matches, String structureKey) { + for (int matchIndex = 0; matchIndex < matches.length(); matchIndex++) { + Object rawPattern = matches.opt(matchIndex); + if (rawPattern instanceof String pattern + && matchesStructurePattern(pattern, structureKey)) { + return true; + } + } + return false; + } + + private static JigsawSourceMetadata resolveJigsawMetadata( + String sourcePath, String structureKey, + JigsawMetadataResolver resolver, List blockingErrors) { + if (resolver == null) { + return null; + } + MetadataResolution resolution = resolver.resolve(structureKey); + if (resolution.metadata() != null) { + return resolution.metadata(); + } + blockingErrors.add(sourcePath + " could not resolve live metadata for registered jigsaw source '" + + structureKey + "': " + resolution.failure() + "."); + return null; + } + + private static boolean matchesStructurePattern(String pattern, String structureKey) { + String normalizedPattern = pattern == null ? "" : pattern.trim().toLowerCase(Locale.ROOT); + String normalizedKey = structureKey == null ? "" : structureKey.trim().toLowerCase(Locale.ROOT); + if (normalizedPattern.isEmpty() || !normalizedKey.startsWith(normalizedPattern)) { + return false; + } + if (normalizedKey.length() == normalizedPattern.length()) { + return true; + } + char patternEnd = normalizedPattern.charAt(normalizedPattern.length() - 1); + if (patternEnd == ':' || patternEnd == '/' || patternEnd == '_') { + return true; + } + char boundary = normalizedKey.charAt(normalizedPattern.length()); + return boundary == '/' || boundary == '_'; + } + + private static void validateJigsawAssembly(String path, JSONObject assembly, + Set registeredPools, + List blockingErrors) { + PackJsonFieldChecks.validateOptionalResourceKey(path, assembly, "startPool", false, blockingErrors); + String startPool = assembly.optString("startPool", "").trim(); + if (!startPool.isEmpty() && !registeredPools.isEmpty() + && !registeredPools.contains(startPool.toLowerCase(Locale.ROOT))) { + blockingErrors.add(path + ".startPool '" + startPool + + "' is not a registered template pool."); + } + PackJsonFieldChecks.validateOptionalResourceKey(path, assembly, "startJigsawName", true, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, assembly, "maxDepth", 0, 20, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, assembly, "maxDistanceHorizontal", 1, 128, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, assembly, "maxDistanceVertical", 1, 4064, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, assembly, "dimensionPaddingBottom", 0, Integer.MAX_VALUE, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, assembly, "dimensionPaddingTop", 0, Integer.MAX_VALUE, blockingErrors); + if (assembly.has("useExpansionHack") + && !(assembly.opt("useExpansionHack") instanceof Boolean)) { + blockingErrors.add(path + ".useExpansionHack must be a boolean."); + } + PackJsonFieldChecks.validateOptionalEnum(path, assembly, "projectStartToHeightmap", + Set.of("SOURCE", "NONE", "WORLD_SURFACE_WG", "WORLD_SURFACE", + "OCEAN_FLOOR_WG", "OCEAN_FLOOR", "MOTION_BLOCKING", + "MOTION_BLOCKING_NO_LEAVES"), blockingErrors); + PackJsonFieldChecks.validateOptionalEnum(path, assembly, "liquidSettings", + Set.of("SOURCE", "IGNORE_WATERLOGGING", "APPLY_WATERLOGGING"), blockingErrors); + } + + static void validateNativeTerrain(String path, JSONObject placement, + List blockingErrors) { + JSONObject terrain = placement.optJSONObject("terrain"); + if (placement.has("terrain") && placement.opt("terrain") != JSONObject.NULL && terrain == null) { + blockingErrors.add(path + ".terrain must be an object."); + return; + } + if (terrain == null) { + return; + } + PackJsonFieldChecks.validateOptionalEnum(path + ".terrain", terrain, "mode", + Set.of("SOURCE", "PRESERVE", "BORE", "FORCE_CARVE", "VACUUM", "ENCASE"), blockingErrors); + PackJsonFieldChecks.validateOptionalEnum(path + ".terrain", terrain, "shape", + Set.of("BOX", "ROUNDED", "ERODED"), blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path + ".terrain", terrain, + "horizontalPadding", 0, 128, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path + ".terrain", terrain, + "ceilingPadding", 0, 128, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path + ".terrain", terrain, + "floorPadding", 0, 64, blockingErrors); + PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain, + "erosionStrength", 0D, 1D, blockingErrors); + PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain, + "erosionFrequency", 0.001D, 1D, blockingErrors); + PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain, + "lobeFrequency", 0D, 1D, blockingErrors); + PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain, + "lobeStrength", 0D, 1D, blockingErrors); + if (terrain.has("encasePalette") && terrain.opt("encasePalette") != JSONObject.NULL + && terrain.optJSONObject("encasePalette") == null) { + blockingErrors.add(path + ".terrain.encasePalette must be an object."); + } + } + + private static void validateEditableStructureTerrain( + String path, JSONObject placement, List blockingErrors) { + JSONObject terrain = placement.optJSONObject("terrain"); + if (terrain == null) { + return; + } + String mode = terrain.optString("mode", "SOURCE"); + if ("VACUUM".equals(mode) || "ENCASE".equals(mode)) { + blockingErrors.add(path + ".terrain.mode " + mode + + " cannot target editable Iris structures; use nativeStructures or " + + "importedStructures.adjustments for native terrain preparation."); + } + } + + static void validateStructureStartPools(File structuresFolder, + Set poolKeys, + List blockingErrors) { + if (!structuresFolder.isDirectory()) { + return; + } + List structureFiles = PackValidationIo.listJsonRecursive(structuresFolder); + structureFiles.sort(Comparator.comparing(File::getPath)); + for (File structureFile : structureFiles) { + String structureKey = PackValidationIo.deriveKey(structuresFolder, structureFile); + JSONObject structure = readGraphJson(structureFile, "Structure", structureKey, blockingErrors); + if (structure == null || isLegacyStructureIndex(structureKey, structure)) { + continue; + } + String startPool = structure.optString("startPool", "").trim(); + if (startPool.isEmpty()) { + blockingErrors.add("Structure '" + structureKey + "' does not declare a startPool."); + } else if (!poolKeys.contains(startPool)) { + blockingErrors.add("Structure '" + structureKey + "' references missing start pool '" + + startPool + "'."); + } + } + } + + static void validateJigsawPools(File poolsFolder, + Set poolKeys, + Set pieceKeys, + List blockingErrors) { + if (!poolsFolder.isDirectory()) { + return; + } + List poolFiles = PackValidationIo.listJsonRecursive(poolsFolder); + poolFiles.sort(Comparator.comparing(File::getPath)); + for (File poolFile : poolFiles) { + String poolKey = PackValidationIo.deriveKey(poolsFolder, poolFile); + JSONObject pool = readGraphJson(poolFile, "Jigsaw pool", poolKey, blockingErrors); + if (pool == null) { + continue; + } + JSONArray entries = pool.optJSONArray("pieces"); + if (entries != null) { + for (int entryIndex = 0; entryIndex < entries.length(); entryIndex++) { + JSONObject entry = entries.optJSONObject(entryIndex); + if (entry == null) { + continue; + } + String pieceKey = entry.optString("piece", "").trim(); + if (!pieceKey.isEmpty() && !pieceKeys.contains(pieceKey)) { + blockingErrors.add("Jigsaw pool '" + poolKey + "' pieces[" + entryIndex + + "] references missing piece '" + pieceKey + "'."); + } + } + } + String fallback = pool.optString("fallback", "").trim(); + if (!fallback.isEmpty() && !poolKeys.contains(fallback)) { + blockingErrors.add("Jigsaw pool '" + poolKey + "' references missing fallback pool '" + + fallback + "'."); + } + } + } + + static void validateJigsawPieces(File piecesFolder, + Set poolKeys, + Set objectKeys, + List blockingErrors) { + if (!piecesFolder.isDirectory()) { + return; + } + List pieceFiles = PackValidationIo.listJsonRecursive(piecesFolder); + pieceFiles.sort(Comparator.comparing(File::getPath)); + for (File pieceFile : pieceFiles) { + String pieceKey = PackValidationIo.deriveKey(piecesFolder, pieceFile); + JSONObject piece = readGraphJson(pieceFile, "Jigsaw piece", pieceKey, blockingErrors); + if (piece == null) { + continue; + } + String objectKey = piece.optString("object", "").trim(); + if (objectKey.isEmpty()) { + blockingErrors.add("Jigsaw piece '" + pieceKey + "' does not declare an object."); + } else if (!objectKeys.contains(objectKey)) { + blockingErrors.add("Jigsaw piece '" + pieceKey + "' references missing object '" + + objectKey + "'."); + } + JSONArray connectors = piece.optJSONArray("connectors"); + if (connectors == null) { + continue; + } + for (int connectorIndex = 0; connectorIndex < connectors.length(); connectorIndex++) { + JSONObject connector = connectors.optJSONObject(connectorIndex); + if (connector == null) { + continue; + } + String poolKey = connector.optString("pool", "").trim(); + if (!poolKey.isEmpty() && !poolKeys.contains(poolKey)) { + blockingErrors.add("Jigsaw piece '" + pieceKey + "' connectors[" + connectorIndex + + "] references missing pool '" + poolKey + "'."); + } + } + } + } + + static JSONObject readGraphJson(File file, + String resourceType, + String resourceKey, + List blockingErrors) { + try { + return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } catch (IOException | RuntimeException e) { + String reason = e.getMessage(); + if (reason == null || reason.isBlank()) { + reason = e.getClass().getSimpleName(); + } + blockingErrors.add(resourceType + " '" + resourceKey + "' has invalid JSON: " + reason); + return null; + } + } + + private static boolean isLegacyStructureIndex(String structureKey, JSONObject structure) { + return "structure-index".equals(structureKey) + && structure.has("counts") + && structure.has("structureSets") + && structure.has("iris"); + } + + static String structureHostType(String folderName) { + return switch (folderName) { + case "dimensions" -> "Dimension"; + case "regions" -> "Region"; + case "biomes" -> "Biome"; + default -> "Resource"; + }; + } + + private record RegistrySnapshot(Set structures, Set jigsaws, Set pools, + JigsawMetadataResolver jigsawMetadataResolver, + PlatformStructureHooks hooks) { + } + + private static final class JigsawMetadataResolver { + private final PlatformStructureHooks hooks; + private final Map resolutions = new HashMap<>(); + + private JigsawMetadataResolver(PlatformStructureHooks hooks) { + this.hooks = hooks; + } + + private MetadataResolution resolve(String structureKey) { + MetadataResolution cached = resolutions.get(structureKey); + if (cached != null) { + return cached; + } + MetadataResolution resolved; + try { + JigsawSourceMetadata metadata = hooks.jigsawSourceMetadata(structureKey); + if (metadata == null) { + throw new IllegalStateException( + "The active structure registry returned null jigsaw metadata for '" + + structureKey + "'"); + } + resolved = new MetadataResolution(metadata, null); + } catch (RuntimeException | LinkageError error) { + IrisLogging.reportError( + "Could not resolve live jigsaw metadata for registered structure '" + + structureKey + "' during pack validation", error); + resolved = new MetadataResolution(null, failureMessage(error)); + } + resolutions.put(structureKey, resolved); + return resolved; + } + } + + private record MetadataResolution(JigsawSourceMetadata metadata, String failure) { + } + + private record EffectiveTerrainAdjustment(String path, JSONObject adjustment) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackStyledRangeDefaultValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackStyledRangeDefaultValidator.java new file mode 100644 index 000000000..bbfa1abfb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackStyledRangeDefaultValidator.java @@ -0,0 +1,154 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +/** + * Catches the shared IrisStyledRange class default (min 16, max 32) leaking into pack content. + * The POJO cannot distinguish an empty object from an explicit 16/32 after Gson runs, so the raw + * JSON is the only place this mistake is still visible: an objects[].densityStyle of {} places + * 16-32 objects per chunk, and a caveProfile.densityThreshold of {} hollows the whole cave range. + */ +final class PackStyledRangeDefaultValidator { + private static final String STYLE_RANGE_SNIPPET_FOLDER = "snippet/style-range/"; + + record Validation(List errors, List warnings) { + Validation { + errors = List.copyOf(errors); + warnings = List.copyOf(warnings); + } + } + + private PackStyledRangeDefaultValidator() { + } + + static Validation validate(File packFolder) { + List errors = new ArrayList<>(); + List warnings = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return new Validation(errors, warnings); + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String type = hostType(folderName); + for (File resourceFile : resourceFiles) { + String key = PackValidationIo.deriveKey(resourceFolder, resourceFile); + JSONObject json; + try { + json = new JSONObject(Files.readString(resourceFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + continue; + } + + validateObjectPlacements(packFolder, type, key, json, errors, warnings); + validateCaveProfileThreshold(packFolder, type, key, json.optJSONObject("caveProfile"), errors, warnings); + } + } + return new Validation(errors, warnings); + } + + private static void validateObjectPlacements(File packFolder, String type, String key, JSONObject json, + List errors, List warnings) { + JSONArray objects = json.optJSONArray("objects"); + if (objects == null) { + return; + } + for (int i = 0; i < objects.length(); i++) { + JSONObject placement = objects.optJSONObject(i); + if (placement == null || !placement.has("densityStyle") || placement.isNull("densityStyle")) { + continue; + } + checkRange(packFolder, placement.opt("densityStyle"), + type + " '" + key + "' objects[" + i + "].densityStyle", + "16-32 objects per chunk", "remove densityStyle to use the scalar density field", + errors, warnings); + } + } + + private static void validateCaveProfileThreshold(File packFolder, String type, String key, JSONObject caveProfile, + List errors, List warnings) { + if (caveProfile == null || !caveProfile.has("densityThreshold") || caveProfile.isNull("densityThreshold")) { + return; + } + checkRange(packFolder, caveProfile.opt("densityThreshold"), + type + " '" + key + "' caveProfile.densityThreshold", + "a carve threshold of 16-32, hollowing the entire vertical range", + "remove densityThreshold to keep the cave profile default", + errors, warnings); + } + + private static void checkRange(File packFolder, Object raw, String path, String consequence, String removal, + List errors, List warnings) { + JSONObject range = null; + String via = ""; + if (raw instanceof JSONObject inline) { + range = inline; + } else if (raw instanceof String reference && reference.startsWith("snippet/")) { + String resolved = reference.startsWith(STYLE_RANGE_SNIPPET_FOLDER) + ? reference + : STYLE_RANGE_SNIPPET_FOLDER + reference.substring("snippet/".length()); + File snippet = new File(packFolder, resolved + ".json"); + if (!snippet.isFile()) { + return; // Missing snippets are reported by the content-key machinery. + } + try { + range = new JSONObject(Files.readString(snippet.toPath(), StandardCharsets.UTF_8)); + via = " (via snippet '" + resolved + "')"; + } catch (Throwable e) { + return; + } + } + if (range == null) { + return; + } + + boolean hasMin = range.has("min") && !range.isNull("min"); + boolean hasMax = range.has("max") && !range.isNull("max"); + if (!hasMin && !hasMax) { + errors.add(path + via + " omits both min and max, which resolves to the shared IrisStyledRange default of " + + consequence + ". Set min and max explicitly, or " + removal + "."); + } else if (!hasMin || !hasMax) { + String missing = hasMin ? "max" : "min"; + String defaultValue = hasMin ? "32" : "16"; + warnings.add(path + via + " omits " + missing + ", which falls back to the shared IrisStyledRange default of " + + defaultValue + ". Set it explicitly if that is not intended."); + } + } + + private static String hostType(String folderName) { + String singular = folderName.endsWith("s") ? folderName.substring(0, folderName.length() - 1) : folderName; + return singular.substring(0, 1).toUpperCase(Locale.ROOT) + singular.substring(1); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidationCache.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidationCache.java new file mode 100644 index 000000000..522196acc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidationCache.java @@ -0,0 +1,201 @@ +package art.arcane.iris.core.pack; + +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.spi.PlatformStructureHooks; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public final class PackValidationCache { + private static final int SCHEMA_VERSION = 2; + private static final long MAX_CACHE_BYTES = 16L * 1024L * 1024L; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private PackValidationCache() { + } + + public static String contentFingerprint(File packsRoot) { + return ServerConfigurator.computePackFingerprint(packsRoot); + } + + public static String contextFingerprint() { + if (!IrisPlatforms.isBound()) { + return ""; + } + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + IrisPlatform platform = IrisPlatforms.get(); + update(digest, platform.platformName()); + update(digest, platform.minecraftVersion()); + update(digest, Integer.toString(platform.irisVersionNumber())); + update(digest, Boolean.toString(ContentKeyValidator.strictContent())); + + PlatformRegistries registries = Objects.requireNonNull( + platform.registries(), "Pack validation platform registries"); + updateSorted(digest, registries.blockKeys()); + updateSorted(digest, registries.biomeKeys()); + updateSorted(digest, registries.itemKeys()); + updateSorted(digest, registries.entityKeys()); + + PlatformStructureHooks hooks = Objects.requireNonNull( + platform.structureHooks(), "Pack validation structure hooks"); + updateSorted(digest, hooks.structureKeys()); + updateSorted(digest, hooks.jigsawStructureKeys()); + updateSorted(digest, hooks.templatePoolKeys()); + updateSorted(digest, hooks.structureSetKeys()); + updateSorted(digest, hooks.objectFeatureKeys()); + return HexFormat.of().formatHex(digest.digest()); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 is unavailable", exception); + } + } + + public static Optional> load( + Path cacheFile, + String contentFingerprint, + String contextFingerprint, + List expectedPackNames + ) { + if (cacheFile == null + || contentFingerprint == null || contentFingerprint.isBlank() + || contextFingerprint == null || contextFingerprint.isBlank() + || !Files.isRegularFile(cacheFile, LinkOption.NOFOLLOW_LINKS) + || Files.isSymbolicLink(cacheFile)) { + return Optional.empty(); + } + try { + if (Files.size(cacheFile) > MAX_CACHE_BYTES) { + return Optional.empty(); + } + CacheState state = GSON.fromJson(Files.readString(cacheFile, StandardCharsets.UTF_8), CacheState.class); + if (state == null + || state.schemaVersion != SCHEMA_VERSION + || !contentFingerprint.equals(state.contentFingerprint) + || !contextFingerprint.equals(state.contextFingerprint) + || state.results == null) { + return Optional.empty(); + } + Set expected = new HashSet<>(expectedPackNames == null ? List.of() : expectedPackNames); + List results = new ArrayList<>(state.results.size()); + Set actual = new HashSet<>(); + for (CachedResult cached : state.results) { + if (cached == null || cached.packName == null || cached.packName.isBlank() + || cached.blockingErrors == null || cached.warnings == null + || !actual.add(cached.packName)) { + return Optional.empty(); + } + results.add(new PackValidationResult( + cached.packName, + cached.blockingErrors, + cached.warnings, + cached.validatedAtMillis)); + } + if (!expected.equals(actual)) { + return Optional.empty(); + } + results.sort(Comparator.comparing(PackValidationResult::getPackName)); + return Optional.of(List.copyOf(results)); + } catch (IOException | RuntimeException exception) { + return Optional.empty(); + } + } + + public static void save( + Path cacheFile, + String contentFingerprint, + String contextFingerprint, + List results + ) throws IOException { + if (cacheFile == null || contentFingerprint == null || contentFingerprint.isBlank() + || contextFingerprint == null || contextFingerprint.isBlank()) { + return; + } + List sorted = new ArrayList<>(results == null ? List.of() : results); + sorted.sort(Comparator.comparing(PackValidationResult::getPackName)); + CacheState state = new CacheState(); + state.schemaVersion = SCHEMA_VERSION; + state.contentFingerprint = contentFingerprint; + state.contextFingerprint = contextFingerprint; + state.results = new ArrayList<>(sorted.size()); + for (PackValidationResult result : sorted) { + CachedResult cached = new CachedResult(); + cached.packName = result.getPackName(); + cached.blockingErrors = List.copyOf(result.getBlockingErrors()); + cached.warnings = List.copyOf(result.getWarnings()); + cached.validatedAtMillis = result.getValidatedAtMillis(); + state.results.add(cached); + } + + Path absolute = cacheFile.toAbsolutePath().normalize(); + Path parent = Objects.requireNonNull(absolute.getParent(), "Pack validation cache parent"); + Files.createDirectories(parent); + Path staged = Files.createTempFile(parent, ".pack-validation-", ".tmp"); + try { + byte[] content = GSON.toJson(state).getBytes(StandardCharsets.UTF_8); + if (content.length > MAX_CACHE_BYTES) { + throw new IOException("Pack validation cache exceeds " + MAX_CACHE_BYTES + " bytes"); + } + Files.write(staged, content); + try { + Files.move(staged, absolute, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException exception) { + Files.move(staged, absolute, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(staged); + } + } + + private static void updateSorted(MessageDigest digest, List values) { + List sorted = new ArrayList<>(Objects.requireNonNull(values, "Pack validation registry keys")); + sorted.sort(String::compareTo); + update(digest, Integer.toString(sorted.size())); + for (String value : sorted) { + update(digest, value); + } + } + + private static void update(MessageDigest digest, String value) { + byte[] bytes = Objects.requireNonNullElse(value, "").getBytes(StandardCharsets.UTF_8); + digest.update((byte) (bytes.length >>> 24)); + digest.update((byte) (bytes.length >>> 16)); + digest.update((byte) (bytes.length >>> 8)); + digest.update((byte) bytes.length); + digest.update(bytes); + } + + private static final class CacheState { + private int schemaVersion; + private String contentFingerprint; + private String contextFingerprint; + private List results; + } + + private static final class CachedResult { + private String packName; + private List blockingErrors; + private List warnings; + private long validatedAtMillis; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidationIo.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidationIo.java new file mode 100644 index 000000000..8e6c73a01 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidationIo.java @@ -0,0 +1,127 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public final class PackValidationIo { + private PackValidationIo() { + } + + static JSONObject readJson(File file) { + try { + return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } catch (IOException | RuntimeException parseFailure) { + // Every folder scanned through readJson is also scanned by a readGraphJson pass + // (validateUnsupportedStructureTransforms / validateStructureStartPools), which records the + // parse failure as a blocking error. Reporting here too would duplicate it per scan. + return null; + } + } + + static boolean isScannableJsonPath(Path path) { + String name = path.getFileName().toString(); + if (!name.endsWith(".json")) { + return false; + } + String str = path.toString().replace(File.separatorChar, '/'); + if (str.contains("/" + PackValidator.TRASH_ROOT + "/")) { + return false; + } + if (str.contains("/" + PackValidator.DATAPACK_IMPORTS + "/")) { + return false; + } + if (str.contains("/" + PackValidator.EXTERNAL_DATAPACKS + "/")) { + return false; + } + if (str.contains("/" + PackValidator.INTERNAL_DATAPACKS + "/")) { + return false; + } + if (str.contains("/" + PackValidator.DATAPACKS_FOLDER + "/")) { + return false; + } + if (str.contains("/" + PackValidator.CACHE_FOLDER + "/")) { + return false; + } + if (str.contains("/" + PackValidator.OBJECTS_FOLDER + "/")) { + return false; + } + if (str.contains("/.iris/")) { + return false; + } + return true; + } + + static List listJsonRecursive(File root) { + List out = new ArrayList<>(); + try (Stream stream = Files.walk(root.toPath())) { + stream.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().endsWith(".json")) + .forEach(p -> out.add(p.toFile())); + } catch (Throwable ignored) { + } + return out; + } + + static String deriveKey(File resourceFolder, File resourceFile) { + Path relative = resourceFolder.toPath().relativize(resourceFile.toPath()); + String str = relative.toString().replace(File.separatorChar, '/'); + if (!str.endsWith(".json")) { + return null; + } + return str.substring(0, str.length() - ".json".length()); + } + + static String stripExtension(String name) { + int dot = name.lastIndexOf('.'); + return dot <= 0 ? name : name.substring(0, dot); + } + + public static Set listReferencedKeysFromCorpus(String corpus) { + Set keys = new HashSet<>(); + if (corpus == null) { + return keys; + } + int i = 0; + while (i < corpus.length()) { + int start = corpus.indexOf('"', i); + if (start < 0) { + break; + } + int end = corpus.indexOf('"', start + 1); + if (end < 0) { + break; + } + keys.add(corpus.substring(start + 1, end)); + i = end + 1; + } + return keys; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidationRegistry.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidationRegistry.java new file mode 100644 index 000000000..03369ea53 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidationRegistry.java @@ -0,0 +1,353 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +public final class PackValidationRegistry { + private static final Map RESULTS = new ConcurrentHashMap<>(); + private static final Map ROOT_STATES = new ConcurrentHashMap<>(); + + private PackValidationRegistry() { + } + + public static void publish(PackValidationResult result) { + if (result == null || result.getPackName() == null || result.getPackName().isBlank()) { + return; + } + RESULTS.put(result.getPackName(), result); + } + + public static void publish(Path packRoot, PackValidationResult result) { + if (packRoot == null || result == null) { + return; + } + publish(normalize(packRoot), new RootValidation(result, "")); + } + + public static void publish(Path packRoot, PackValidationResult result, String contentFingerprint) { + if (packRoot == null || result == null || contentFingerprint == null || contentFingerprint.isBlank()) { + return; + } + publish(normalize(packRoot), new RootValidation(result, contentFingerprint)); + } + + public static PackValidationResult publishMatchingCopy( + Path sourceRoot, + Path targetRoot, + String copiedContentFingerprint + ) { + if (sourceRoot == null || targetRoot == null + || copiedContentFingerprint == null || copiedContentFingerprint.isBlank()) { + return null; + } + RootValidation sourceValidation = matchingValidation(sourceRoot, copiedContentFingerprint); + if (sourceValidation == null) { + return null; + } + publish(normalize(targetRoot), sourceValidation); + return sourceValidation.result(); + } + + public static RootMutation beginRootMutation(Path packRoot) { + Path normalizedRoot = normalize(Objects.requireNonNull(packRoot, "Pack root")); + AtomicReference mutation = new AtomicReference<>(); + ROOT_STATES.compute(normalizedRoot, (path, current) -> { + if (current != null && current.mutating()) { + throw new IllegalStateException("Iris pack validation is already mutating " + normalizedRoot); + } + long generation = nextGeneration(current); + mutation.set(new RootMutation(normalizedRoot, generation)); + return new RootState(generation, true, null); + }); + return mutation.get(); + } + + public static ValidationTicket tryBeginValidation(Path packRoot) { + Path normalizedRoot = normalize(Objects.requireNonNull(packRoot, "Pack root")); + AtomicReference ticket = new AtomicReference<>(); + ROOT_STATES.compute(normalizedRoot, (path, current) -> { + RootState state = current == null ? new RootState(0L, false, null) : current; + if (!state.mutating()) { + ticket.set(new ValidationTicket(normalizedRoot, state.generation())); + } + return state; + }); + return ticket.get(); + } + + public static boolean publishIfCurrent(ValidationTicket ticket, PackValidationResult result) { + if (ticket == null || result == null) { + return false; + } + AtomicBoolean published = new AtomicBoolean(); + ROOT_STATES.compute(ticket.packRoot, (path, current) -> { + if (current == null + || current.mutating() + || current.generation() != ticket.generation) { + return current; + } + published.set(true); + return new RootState( + current.generation(), + false, + new RootValidation(result, "")); + }); + return published.get(); + } + + public static PackValidationResult get(String packName) { + if (packName == null || packName.isBlank()) { + return null; + } + return RESULTS.get(packName); + } + + public static PackValidationResult get(Path packRoot) { + if (packRoot == null) { + return null; + } + RootState state = ROOT_STATES.get(normalize(packRoot)); + return state == null || state.mutating() || state.validation() == null + ? null + : state.validation().result(); + } + + public static PackValidationResult requireLoadable(String packName) { + if (packName == null || packName.isBlank()) { + throw new IllegalArgumentException("Pack name is required for validation"); + } + PackValidationResult result = get(packName); + if (result == null) { + throw new BrokenPackException(packName, List.of( + "Required pack validation has not completed. World creation fails closed until validation succeeds.")); + } + if (!result.isLoadable()) { + throw new BrokenPackException(packName, result.getBlockingErrors()); + } + return result; + } + + public static PackValidationResult requireLoadable(Path packRoot) { + if (packRoot == null) { + throw new IllegalArgumentException("Pack root is required for validation"); + } + Path normalizedRoot = normalize(packRoot); + PackValidationResult result = get(normalizedRoot); + if (result == null) { + throw new BrokenPackException(normalizedRoot.toString(), List.of( + "Required pack validation has not completed. World creation fails closed until validation succeeds.")); + } + if (!result.isLoadable()) { + throw new BrokenPackException(normalizedRoot.toString(), result.getBlockingErrors()); + } + return result; + } + + public static boolean isBroken(String packName) { + PackValidationResult result = get(packName); + return result != null && !result.isLoadable(); + } + + public static boolean isBroken(Path packRoot) { + PackValidationResult result = get(packRoot); + return result != null && !result.isLoadable(); + } + + public static Map snapshot() { + return Collections.unmodifiableMap(RESULTS); + } + + public static void remove(String packName) { + if (packName == null || packName.isBlank()) { + return; + } + RESULTS.remove(packName); + } + + public static void remove(Path packRoot) { + if (packRoot == null) { + return; + } + Path normalizedRoot = normalize(packRoot); + ROOT_STATES.compute(normalizedRoot, (path, current) -> { + if (current != null && current.mutating()) { + return current; + } + return new RootState(nextGeneration(current), false, null); + }); + } + + public static void clear() { + RESULTS.clear(); + ROOT_STATES.clear(); + } + + private static Path normalize(Path packRoot) { + Path normalizedRoot = packRoot.toAbsolutePath().normalize(); + try { + Path existing = normalizedRoot; + List missingNames = new ArrayList<>(); + while (existing != null && !Files.exists(existing, LinkOption.NOFOLLOW_LINKS)) { + Path name = existing.getFileName(); + if (name != null) { + missingNames.add(name); + } + existing = existing.getParent(); + } + if (existing == null) { + return normalizedRoot; + } + Path resolved = existing.toRealPath(); + for (int index = missingNames.size() - 1; index >= 0; index--) { + resolved = resolved.resolve(missingNames.get(index)); + } + return resolved.normalize(); + } catch (IOException exception) { + throw new IllegalArgumentException("Unable to resolve Iris pack root: " + normalizedRoot, exception); + } + } + + private static void publish(Path normalizedRoot, RootValidation validation) { + ROOT_STATES.compute(normalizedRoot, (path, current) -> { + if (current != null && current.mutating()) { + throw new IllegalStateException("Iris pack validation is mutating " + normalizedRoot); + } + return new RootState(nextGeneration(current), false, validation); + }); + } + + private static RootValidation matchingValidation(Path sourceRoot, String copiedContentFingerprint) { + Path normalizedSource = normalize(sourceRoot); + RootState sourceState = ROOT_STATES.get(normalizedSource); + if (sourceState == null + || sourceState.mutating() + || sourceState.validation() == null + || !copiedContentFingerprint.equals(sourceState.validation().contentFingerprint())) { + return null; + } + return sourceState.validation(); + } + + private static long nextGeneration(RootState current) { + return current == null ? 1L : Math.incrementExact(current.generation()); + } + + private static void closeMutation(Path packRoot, long generation) { + ROOT_STATES.computeIfPresent(packRoot, (path, current) -> { + if (!current.mutating() || current.generation() != generation) { + return current; + } + return new RootState(generation, false, null); + }); + } + + public static final class RootMutation implements AutoCloseable { + private final Path packRoot; + private final long generation; + private RootValidation pendingValidation; + private boolean closed; + + private RootMutation(Path packRoot, long generation) { + this.packRoot = packRoot; + this.generation = generation; + } + + public synchronized PackValidationResult stageMatchingCopy( + Path sourceRoot, + String copiedContentFingerprint + ) { + requireOpen(); + RootValidation matching = matchingValidation(sourceRoot, copiedContentFingerprint); + if (matching == null) { + return null; + } + pendingValidation = matching; + return matching.result(); + } + + public synchronized void stage(PackValidationResult result) { + requireOpen(); + pendingValidation = new RootValidation( + Objects.requireNonNull(result, "Pack validation result"), + ""); + } + + public synchronized void commit() { + requireOpen(); + if (pendingValidation == null) { + throw new IllegalStateException("No pack validation is staged for " + packRoot); + } + AtomicBoolean published = new AtomicBoolean(); + ROOT_STATES.computeIfPresent(packRoot, (path, current) -> { + if (!current.mutating() || current.generation() != generation) { + return current; + } + published.set(true); + return new RootState(generation, false, pendingValidation); + }); + if (!published.get()) { + throw new IllegalStateException("Iris pack validation mutation lost ownership of " + packRoot); + } + closed = true; + } + + @Override + public synchronized void close() { + if (closed) { + return; + } + closed = true; + closeMutation(packRoot, generation); + } + + private void requireOpen() { + if (closed) { + throw new IllegalStateException("Iris pack validation mutation is already closed for " + packRoot); + } + } + } + + public static final class ValidationTicket { + private final Path packRoot; + private final long generation; + + private ValidationTicket(Path packRoot, long generation) { + this.packRoot = packRoot; + this.generation = generation; + } + } + + private record RootValidation(PackValidationResult result, String contentFingerprint) { + } + + private record RootState(long generation, boolean mutating, RootValidation validation) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java new file mode 100644 index 000000000..b00e2e938 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java @@ -0,0 +1,60 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PackValidationResult { + private final String packName; + private final List blockingErrors; + private final List warnings; + private final long validatedAtMillis; + + public PackValidationResult(String packName, + List blockingErrors, + List warnings, + long validatedAtMillis) { + this.packName = packName; + this.blockingErrors = blockingErrors == null ? new ArrayList<>() : new ArrayList<>(blockingErrors); + this.warnings = warnings == null ? new ArrayList<>() : new ArrayList<>(warnings); + this.validatedAtMillis = validatedAtMillis; + } + + public String getPackName() { + return packName; + } + + public boolean isLoadable() { + return blockingErrors.isEmpty(); + } + + public List getBlockingErrors() { + return Collections.unmodifiableList(blockingErrors); + } + + public List getWarnings() { + return Collections.unmodifiableList(warnings); + } + + public long getValidatedAtMillis() { + return validatedAtMillis; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java new file mode 100644 index 000000000..a66070d44 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java @@ -0,0 +1,125 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public final class PackValidator { + static final String TRASH_ROOT = ".iris-trash"; + static final String DATAPACK_IMPORTS = "datapack-imports"; + static final String EXTERNAL_DATAPACKS = "externaldatapacks"; + static final String INTERNAL_DATAPACKS = "internaldatapacks"; + static final String DATAPACKS_FOLDER = "datapacks"; + static final String CACHE_FOLDER = "cache"; + static final String OBJECTS_FOLDER = "objects"; + static final String LOOT_FOLDER = "loot"; + static final String DIMENSIONS_FOLDER = "dimensions"; + static final String STRUCTURES_FOLDER = "structures"; + static final String JIGSAW_POOLS_FOLDER = "jigsaw-pools"; + static final String JIGSAW_PIECES_FOLDER = "jigsaw-pieces"; + static final List STRUCTURE_HOST_FOLDERS = List.of(DIMENSIONS_FOLDER, "regions", "biomes"); + static final List REMOVED_WORLDGEN_FIELDS = List.of("fluidBodies"); + static final List UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS = List.of("rotation", "translate", "scale"); + static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); + + private PackValidator() { + } + + public static PackValidationResult validate(File packFolder) { + return validate(packFolder, true); + } + + public static PackValidationResult validateForDatapackBootstrap(File packFolder) { + return validate(packFolder, false); + } + + private static PackValidationResult validate(File packFolder, boolean validateLiveRegistries) { + String packName = packFolder == null ? "" : packFolder.getName(); + List blockingErrors = new ArrayList<>(); + List warnings = new ArrayList<>(); + long validatedAt = System.currentTimeMillis(); + + if (packFolder == null || !packFolder.isDirectory()) { + blockingErrors.add("Pack folder does not exist or is not a directory."); + return new PackValidationResult(packName, blockingErrors, warnings, validatedAt); + } + + File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER); + if (!dimensionsFolder.isDirectory()) { + blockingErrors.add("Missing dimensions/ folder."); + return new PackValidationResult(packName, blockingErrors, warnings, validatedAt); + } + + File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json")); + if (dimensionFiles == null || dimensionFiles.length == 0) { + blockingErrors.add("No dimension JSON files under dimensions/."); + return new PackValidationResult(packName, blockingErrors, warnings, validatedAt); + } + + PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings); + blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder)); + PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder); + addDistinct(blockingErrors, lootIssues.errors()); + addDistinct(warnings, lootIssues.warnings()); + blockingErrors.addAll(PackObjectSurfaceValidator.validateRemovedWorldgenFields(packFolder)); + blockingErrors.addAll(PackObjectSurfaceValidator.validateObjectSurfaceSupport(packFolder)); + blockingErrors.addAll(PackObjectSurfaceValidator.validateUnsupportedStructureTransforms(packFolder)); + blockingErrors.addAll(PackObjectSurfaceValidator.validateStructureGraph( + packFolder, validateLiveRegistries)); + StructureGraphPackValidator.Validation compiledStructures = + StructureGraphPackValidator.validate( + packFolder.toPath(), PackObjectSurfaceValidator.collectPlacedStructureKeys(packFolder)); + addDistinct(blockingErrors, compiledStructures.errors()); + addDistinct(warnings, compiledStructures.warnings()); + blockingErrors.addAll(PackNativeStructureValidator.validateNativeStructureReplacements( + packFolder, + compiledStructures.replacementOutputStructures(), + compiledStructures.sampledVerticalEnvelopes())); + blockingErrors.addAll(PackSpawnValidator.validateSpawnerEntityReferences( + new File(packFolder, "spawners"), new File(packFolder, "entities"))); + blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns( + new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory)); + blockingErrors.addAll(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(packFolder, "biomes"))); + blockingErrors.addAll(PackBiomeLayerValidator.validateDecoratorPalettes( + new File(packFolder, "biomes"), new File(packFolder, "snippet/decorator"))); + PackStyledRangeDefaultValidator.Validation styledRanges = PackStyledRangeDefaultValidator.validate(packFolder); + addDistinct(blockingErrors, styledRanges.errors()); + addDistinct(warnings, styledRanges.warnings()); + addDistinct(warnings, PackGeneratorDuplicateValidator.validateDuplicateGenerators(packFolder)); + + // Strict content mode promotes unresolved keys and bad block properties from advisory to blocking. Palette + // -sourced findings are exempt and stay warnings - see ContentKeyValidator.collectContentKeyIssues. + ContentKeyValidator.ContentKeyIssues contentKeys = ContentKeyValidator.collectContentKeyIssues(packFolder); + addDistinct(ContentKeyValidator.strictContent() ? blockingErrors : warnings, contentKeys.strict()); + addDistinct(warnings, contentKeys.advisory()); + + return new PackValidationResult(packName, blockingErrors, warnings, validatedAt); + } + + private static void addDistinct(List destination, List additions) { + for (String addition : additions) { + if (!destination.contains(addition)) { + destination.add(addition); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/StructureGraphPackValidator.java b/core/src/main/java/art/arcane/iris/core/pack/StructureGraphPackValidator.java new file mode 100644 index 000000000..7c5fe8692 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/StructureGraphPackValidator.java @@ -0,0 +1,351 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.engine.framework.PlacedStructurePiece; +import art.arcane.iris.engine.framework.StructureAssembler; +import art.arcane.iris.engine.framework.structure.IrisObjectFrameReader; +import art.arcane.iris.engine.framework.structure.StructureAssemblyResult; +import art.arcane.iris.engine.framework.structure.StructureGraphCompilation; +import art.arcane.iris.engine.framework.structure.StructureGraphCompiler; +import art.arcane.iris.engine.framework.structure.StructureGraphDiagnostic; +import art.arcane.iris.engine.framework.structure.StructureGraphAssemblySample; +import art.arcane.iris.engine.framework.structure.StructureGraphResolver; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.volmlib.util.math.RNG; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +final class StructureGraphPackValidator { + private static final Gson GSON = new GsonBuilder().create(); + private static final List GEOMETRY_SAMPLE_SEEDS = List.of( + 0L, 1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L, 55L, 89L, 144L, 233L, 377L, 610L, 987L); + private static final int GEOMETRY_SAMPLE_ORIGIN_Y = 64; + + private StructureGraphPackValidator() { + } + + static Validation validate(Path packRoot) { + return validate(packRoot, null); + } + + static Validation validate(Path packRoot, Set activeStructureKeys) { + Path normalizedRoot = packRoot.toAbsolutePath().normalize(); + Path structuresRoot = normalizedRoot.resolve("structures"); + Set errors = new LinkedHashSet<>(); + Set warnings = new LinkedHashSet<>(); + Set replacementOutputStructures = new LinkedHashSet<>(); + Map> sampledVerticalEnvelopes = new LinkedHashMap<>(); + FileResolver resolver = new FileResolver(normalizedRoot); + for (Path structureFile : listJsonFiles(structuresRoot)) { + String structureKey = resourceKey(structuresRoot, structureFile, ".json"); + if (structureKey.equals("structure-index")) { + continue; + } + IrisStructure structure = readJson(structureFile, IrisStructure.class); + if (structure == null) { + continue; + } + structure.setLoadKey(structureKey); + if (activeStructureKeys != null && !activeStructureKeys.contains(structureKey)) { + continue; + } + StructureGraphCompilation compilation; + try { + compilation = StructureGraphCompiler.compile(structure, resolver); + } catch (MalformedObjectResourceException e) { + errors.add(e.getMessage()); + continue; + } + for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) { + if (diagnostic.severity() == StructureGraphDiagnostic.Severity.ERROR) { + errors.add(diagnostic.message()); + } else { + warnings.add(diagnostic.message()); + } + } + if (!compilation.isAssemblyViable()) { + errors.add("Structure '" + structureKey + + "' does not produce a complete deterministic assembly and will not generate" + + failedSampleSummary(compilation) + "."); + continue; + } + RuntimeGeometryValidation geometry = validateRuntimeGeometry(compilation); + if (geometry.failures().isEmpty()) { + sampledVerticalEnvelopes.put(structureKey, geometry.sampledVerticalEnvelopes()); + if (compilation.guaranteesAssemblyOutput() && geometry.outputForEverySample()) { + replacementOutputStructures.add(structureKey); + } + } else { + errors.add("Structure '" + structureKey + + "' fails sampled collision- and radius-aware assembly: " + + String.join("; ", geometry.failures()) + "."); + } + } + return new Validation( + List.copyOf(errors), List.copyOf(warnings), Set.copyOf(replacementOutputStructures), + immutableEnvelopes(sampledVerticalEnvelopes)); + } + + private static RuntimeGeometryValidation validateRuntimeGeometry( + StructureGraphCompilation compilation + ) { + List failures = new ArrayList<>(); + List sampledVerticalEnvelopes = new ArrayList<>(); + boolean outputForEverySample = true; + for (long seed : GEOMETRY_SAMPLE_SEEDS) { + try { + StructureAssembler assembler = StructureAssembler.forCompilation( + compilation, new IrisPosition(0, GEOMETRY_SAMPLE_ORIGIN_Y, 0)); + StructureAssemblyResult result = assembler.assemble(new RNG(seed)); + if (result.status().isFailure()) { + failures.add("seed " + seed + " returned " + result.status() + ": " + result.detail()); + } else if (!result.hasOutput()) { + outputForEverySample = false; + } else { + sampledVerticalEnvelopes.add(sampleVerticalEnvelope(seed, result.pieces())); + } + } catch (RuntimeException e) { + failures.add("seed " + seed + " threw " + e.getClass().getSimpleName() + + ": " + failureMessage(e)); + } + } + return new RuntimeGeometryValidation( + List.copyOf(failures), outputForEverySample, List.copyOf(sampledVerticalEnvelopes)); + } + + private static SampledVerticalEnvelope sampleVerticalEnvelope( + long seed, + List pieces + ) { + int minimumY = Integer.MAX_VALUE; + int maximumY = Integer.MIN_VALUE; + for (PlacedStructurePiece piece : pieces) { + minimumY = Math.min(minimumY, piece.getMinY()); + maximumY = Math.max(maximumY, piece.getMaxY()); + } + return new SampledVerticalEnvelope( + seed, + pieces.size(), + Math.subtractExact(minimumY, GEOMETRY_SAMPLE_ORIGIN_Y), + Math.subtractExact(maximumY, GEOMETRY_SAMPLE_ORIGIN_Y)); + } + + private static Map> immutableEnvelopes( + Map> sampledVerticalEnvelopes + ) { + Map> immutable = new LinkedHashMap<>(); + for (Map.Entry> entry : sampledVerticalEnvelopes.entrySet()) { + immutable.put(entry.getKey(), List.copyOf(entry.getValue())); + } + return Map.copyOf(immutable); + } + + private static String failureMessage(RuntimeException exception) { + return exception.getMessage() == null || exception.getMessage().isBlank() + ? "no failure detail" : exception.getMessage(); + } + + private static String failedSampleSummary(StructureGraphCompilation compilation) { + List failures = new ArrayList<>(); + for (StructureGraphAssemblySample sample : compilation.getAssemblySamples()) { + if (sample.outcome().isComplete()) { + continue; + } + failures.add("seed " + sample.seed() + " placed " + sample.outcome().pieceKeys().size() + + " piece(s), status=" + sample.outcome().status() + + ": " + sample.outcome().detail()); + if (failures.size() == 3) { + break; + } + } + return failures.isEmpty() ? "" : " (" + String.join("; ", failures) + ")"; + } + + private static List listJsonFiles(Path root) { + if (!Files.isDirectory(root)) { + return List.of(); + } + try (Stream paths = Files.walk(root)) { + return paths.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".json")) + .sorted() + .toList(); + } catch (IOException e) { + return List.of(); + } + } + + private static String resourceKey(Path root, Path file, String extension) { + String relative = root.relativize(file).toString().replace(file.getFileSystem().getSeparator(), "/"); + return relative.substring(0, relative.length() - extension.length()); + } + + private static T readJson(Path file, Class type) { + try { + return GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), type); + } catch (IOException | RuntimeException e) { + return null; + } + } + + record Validation( + List errors, + List warnings, + Set replacementOutputStructures, + Map> sampledVerticalEnvelopes + ) { + Validation { + errors = List.copyOf(errors); + warnings = List.copyOf(warnings); + replacementOutputStructures = Set.copyOf(replacementOutputStructures); + sampledVerticalEnvelopes = immutableEnvelopes(sampledVerticalEnvelopes); + } + } + + record SampledVerticalEnvelope(long seed, int pieceCount, int minimumYOffset, int maximumYOffset) { + SampledVerticalEnvelope { + if (pieceCount <= 0) { + throw new IllegalArgumentException("Sampled vertical envelope must contain at least one piece"); + } + if (maximumYOffset < minimumYOffset) { + throw new IllegalArgumentException("Sampled vertical envelope maximum Y cannot be below minimum Y"); + } + } + } + + private record RuntimeGeometryValidation( + List failures, + boolean outputForEverySample, + List sampledVerticalEnvelopes + ) { + } + + private static final class FileResolver implements StructureGraphResolver { + private final Path packRoot; + private final Map pools = new HashMap<>(); + private final Map pieces = new HashMap<>(); + private final Map objects = new HashMap<>(); + private final Set missingPools = new LinkedHashSet<>(); + private final Set missingPieces = new LinkedHashSet<>(); + private final Set missingObjects = new LinkedHashSet<>(); + + private FileResolver(Path packRoot) { + this.packRoot = packRoot; + } + + @Override + public IrisJigsawPool loadPool(String key) { + if (pools.containsKey(key)) { + return pools.get(key); + } + if (missingPools.contains(key)) { + return null; + } + Path file = resolve("jigsaw-pools", key, ".json"); + IrisJigsawPool pool = file == null ? null : readJson(file, IrisJigsawPool.class); + if (pool == null) { + missingPools.add(key); + return null; + } + pools.put(key, pool); + return pool; + } + + @Override + public IrisJigsawPiece loadPiece(String key) { + if (pieces.containsKey(key)) { + return pieces.get(key); + } + if (missingPieces.contains(key)) { + return null; + } + Path file = resolve("jigsaw-pieces", key, ".json"); + IrisJigsawPiece piece = file == null ? null : readJson(file, IrisJigsawPiece.class); + if (piece == null) { + missingPieces.add(key); + return null; + } + pieces.put(key, piece); + return piece; + } + + @Override + public IrisObject loadObject(String key) { + if (objects.containsKey(key)) { + return objects.get(key); + } + if (missingObjects.contains(key)) { + return null; + } + Path file = resolve("objects", key, ".iob"); + IrisObject object = file == null ? null : readObjectBounds(file); + if (object == null) { + missingObjects.add(key); + return null; + } + objects.put(key, object); + return object; + } + + private Path resolve(String folder, String key, String extension) { + if (key == null || key.isBlank() || key.indexOf('\\') >= 0 || key.indexOf(':') >= 0) { + return null; + } + Path root = packRoot.resolve(folder).normalize(); + Path file = root.resolve(key + extension).normalize(); + if (!file.startsWith(root) || !Files.isRegularFile(file)) { + return null; + } + return file; + } + + private IrisObject readObjectBounds(Path file) { + try (BufferedInputStream input = new BufferedInputStream(Files.newInputStream(file))) { + return IrisObjectFrameReader.readBounds(input, file.toString()); + } catch (IOException e) { + throw new MalformedObjectResourceException(e.getMessage(), e); + } + } + } + + private static final class MalformedObjectResourceException extends RuntimeException { + private MalformedObjectResourceException(String message, IOException cause) { + super(message, cause); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/StructurePackageClosure.java b/core/src/main/java/art/arcane/iris/core/pack/StructurePackageClosure.java new file mode 100644 index 000000000..08526c246 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/StructurePackageClosure.java @@ -0,0 +1,605 @@ +package art.arcane.iris.core.pack; + +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +public final class StructurePackageClosure { + private static final String STRUCTURES = "structures"; + private static final String POOLS = "jigsaw-pools"; + private static final String PIECES = "jigsaw-pieces"; + private static final String OBJECTS = "objects"; + private static final String LOOT = "loot"; + + private final Path sourceRoot; + private final Set structures; + private final Set pools; + private final Set pieces; + private final Set objects; + private final Set loot; + private final List errors; + + private StructurePackageClosure(Path sourceRoot, MutableClosure closure) { + this.sourceRoot = sourceRoot; + this.structures = Collections.unmodifiableSet(new LinkedHashSet<>(closure.structures)); + this.pools = Collections.unmodifiableSet(new LinkedHashSet<>(closure.pools)); + this.pieces = Collections.unmodifiableSet(new LinkedHashSet<>(closure.pieces)); + this.objects = Collections.unmodifiableSet(new LinkedHashSet<>(closure.objects)); + this.loot = Collections.unmodifiableSet(new LinkedHashSet<>(closure.loot)); + this.errors = Collections.unmodifiableList(new ArrayList<>(closure.errors)); + } + + public static StructurePackageClosure collect(File sourceRoot, Collection rootStructures) { + return collect(sourceRoot, rootStructures, null); + } + + public static StructurePackageClosure collect( + File sourceRoot, + Collection rootStructures, + Limits limits + ) { + Path normalizedRoot = sourceRoot.toPath().toAbsolutePath().normalize(); + MutableClosure closure = new MutableClosure(limits); + if (!Files.isDirectory(normalizedRoot)) { + closure.errors.add("Structure package source is not a directory: " + normalizedRoot); + return new StructurePackageClosure(normalizedRoot, closure); + } + + if (rootStructures != null) { + for (String structureKey : rootStructures) { + enqueueKey(closure.structureQueue, structureKey, "structure", closure.errors); + } + } + + collectStructures(normalizedRoot, closure); + collectPools(normalizedRoot, closure); + collectPieces(normalizedRoot, closure); + validateBinaryResources(normalizedRoot, closure); + validatePortableCollisions(closure.structures, STRUCTURES, closure.errors); + validatePortableCollisions(closure.pools, POOLS, closure.errors); + validatePortableCollisions(closure.pieces, PIECES, closure.errors); + validatePortableCollisions(closure.objects, OBJECTS, closure.errors); + validatePortableCollisions(closure.loot, LOOT, closure.errors); + return new StructurePackageClosure(normalizedRoot, closure); + } + + public boolean isValid() { + return errors.isEmpty(); + } + + public Set structures() { + return structures; + } + + public Set pools() { + return pools; + } + + public Set pieces() { + return pieces; + } + + public Set objects() { + return objects; + } + + public Set loot() { + return loot; + } + + public List errors() { + return errors; + } + + public String writeTo(File targetRoot, boolean minify) throws IOException { + if (!isValid()) { + throw new IOException("Cannot package an invalid structure graph: " + String.join("; ", errors)); + } + + Path normalizedTarget = targetRoot.toPath().toAbsolutePath().normalize(); + prepareTargetRoot(normalizedTarget); + StringBuilder hashes = new StringBuilder(); + copyJsonResources(normalizedTarget, STRUCTURES, structures, minify, hashes); + copyJsonResources(normalizedTarget, POOLS, pools, minify, hashes); + copyJsonResources(normalizedTarget, PIECES, pieces, minify, hashes); + copyJsonResources(normalizedTarget, LOOT, loot, minify, hashes); + copyBinaryResources(normalizedTarget, OBJECTS, objects, hashes); + return IO.hash(hashes.toString()); + } + + private static void collectStructures(Path sourceRoot, MutableClosure closure) { + while (!closure.structureQueue.isEmpty()) { + String structureKey = closure.structureQueue.removeFirst(); + if (!closure.structures.add(structureKey)) { + continue; + } + + JSONObject structure = readJson(sourceRoot, STRUCTURES, structureKey, closure.errors, closure.limits); + if (structure == null) { + continue; + } + String startPool = requiredString(structure, "startPool", "Structure '" + structureKey + "'", closure.errors); + if (startPool != null) { + enqueueKey(closure.poolQueue, startPool, + "start pool for structure '" + structureKey + "'", closure.errors); + } + JSONArray loot = optionalArray(structure, "loot", "Structure '" + structureKey + "'", closure.errors); + addArrayKeys(loot, closure.loot, "loot table", closure.errors); + } + } + + private static void collectPools(Path sourceRoot, MutableClosure closure) { + while (!closure.poolQueue.isEmpty()) { + String poolKey = closure.poolQueue.removeFirst(); + if (!closure.pools.add(poolKey)) { + continue; + } + + JSONObject pool = readJson(sourceRoot, POOLS, poolKey, closure.errors, closure.limits); + if (pool == null) { + continue; + } + JSONArray entries = requiredArray(pool, "pieces", "Jigsaw pool '" + poolKey + "'", closure.errors); + if (entries != null) { + for (int index = 0; index < entries.length(); index++) { + JSONObject entry = entries.optJSONObject(index); + if (entry == null) { + closure.errors.add("Jigsaw pool '" + poolKey + "' has a non-object piece entry at index " + index + "."); + continue; + } + if (isEmptyEntry(entry, poolKey, index, closure.errors)) { + continue; + } + String pieceKey = requiredString(entry, "piece", + "Piece entry " + index + " in jigsaw pool '" + poolKey + "'", closure.errors); + if (pieceKey != null) { + enqueueKey(closure.pieceQueue, pieceKey, + "piece in pool '" + poolKey + "'", closure.errors); + } + } + } + String fallback = optionalString(pool, "fallback", "Jigsaw pool '" + poolKey + "'", closure.errors); + enqueueOptionalKey(closure.poolQueue, fallback, + "fallback pool for '" + poolKey + "'", closure.errors); + } + } + + private static void collectPieces(Path sourceRoot, MutableClosure closure) { + while (!closure.pieceQueue.isEmpty()) { + String pieceKey = closure.pieceQueue.removeFirst(); + if (!closure.pieces.add(pieceKey)) { + continue; + } + + JSONObject piece = readJson(sourceRoot, PIECES, pieceKey, closure.errors, closure.limits); + if (piece == null) { + continue; + } + String objectKey = requiredString(piece, "object", "Jigsaw piece '" + pieceKey + "'", closure.errors); + if (objectKey != null) { + addRequiredKey(closure.objects, objectKey, + "object for piece '" + pieceKey + "'", closure.errors); + } + JSONArray connectors = optionalArray(piece, "connectors", "Jigsaw piece '" + pieceKey + "'", closure.errors); + if (connectors == null) { + continue; + } + for (int index = 0; index < connectors.length(); index++) { + JSONObject connector = connectors.optJSONObject(index); + if (connector == null) { + closure.errors.add("Jigsaw piece '" + pieceKey + "' has a non-object connector at index " + index + "."); + continue; + } + String poolKey = requiredString(connector, "pool", + "Connector " + index + " in jigsaw piece '" + pieceKey + "'", closure.errors); + if (poolKey != null) { + enqueueKey(closure.poolQueue, poolKey, + "connector pool for piece '" + pieceKey + "'", closure.errors); + } + } + collectPools(sourceRoot, closure); + } + } + + private static void validateBinaryResources(Path sourceRoot, MutableClosure closure) { + for (String objectKey : closure.objects) { + resolveExisting(sourceRoot, OBJECTS, objectKey, ".iob", closure.errors); + } + for (String lootKey : closure.loot) { + resolveExisting(sourceRoot, LOOT, lootKey, ".json", closure.errors); + } + } + + private static JSONObject readJson( + Path sourceRoot, + String folder, + String key, + List errors, + Limits limits + ) { + Path file = resolveExisting(sourceRoot, folder, key, ".json", errors); + if (file == null) { + return null; + } + try { + String content = limits == null + ? Files.readString(file, StandardCharsets.UTF_8) + : new String(readBounded(file, limits.maxJsonBytes()), StandardCharsets.UTF_8); + return new JSONObject(content); + } catch (IOException | RuntimeException e) { + errors.add("Invalid " + folder + " resource '" + key + "': " + describe(e)); + return null; + } + } + + private static byte[] readBounded(Path file, int maximumBytes) throws IOException { + try (InputStream input = Files.newInputStream(file)) { + byte[] content = input.readNBytes(maximumBytes + 1); + if (content.length > maximumBytes) { + throw new IOException("JSON resource exceeds " + maximumBytes + " bytes"); + } + return content; + } + } + + private static Path resolveExisting(Path sourceRoot, String folder, String key, String extension, + List errors) { + Path file = resolveResource(sourceRoot, folder, key, extension, errors); + if (file != null && !Files.isRegularFile(file)) { + errors.add("Missing " + folder + " resource '" + key + "'."); + return null; + } + if (file != null && !isContainedSourceResource(sourceRoot, folder, key, extension, file, errors)) { + return null; + } + return file; + } + + private static Path resolveResource(Path root, String folder, String key, String extension, List errors) { + if (!validKey(key)) { + errors.add("Invalid " + folder + " resource key '" + key + "'."); + return null; + } + Path folderRoot = root.resolve(folder).normalize(); + Path file = folderRoot.resolve(key + extension).normalize(); + if (!file.startsWith(folderRoot)) { + errors.add("Resource key escapes " + folder + ": '" + key + "'."); + return null; + } + return file; + } + + private void copyJsonResources(Path targetRoot, String folder, Set keys, boolean minify, + StringBuilder hashes) throws IOException { + for (String key : keys) { + Path source = resolveExistingOrThrow(sourceRoot, folder, key, ".json"); + Path target = prepareTargetResource(targetRoot, folder, key, ".json"); + String json = new JSONObject(Files.readString(source, StandardCharsets.UTF_8)).toString(minify ? 0 : 4); + Files.writeString(target, json, StandardCharsets.UTF_8); + hashes.append(IO.hash(json)); + } + } + + private void copyBinaryResources(Path targetRoot, String folder, Set keys, + StringBuilder hashes) throws IOException { + for (String key : keys) { + Path source = resolveExistingOrThrow(sourceRoot, folder, key, ".iob"); + Path target = prepareTargetResource(targetRoot, folder, key, ".iob"); + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); + hashes.append(IO.hash(source.toFile())); + } + } + + private static Path resolveExistingOrThrow(Path root, String folder, String key, String extension) throws IOException { + List errors = new ArrayList<>(); + Path file = resolveExisting(root, folder, key, extension, errors); + if (file == null) { + throw new IOException(String.join("; ", errors)); + } + return file; + } + + private static Path resolveResourceOrThrow(Path root, String folder, String key, String extension) throws IOException { + List errors = new ArrayList<>(); + Path file = resolveResource(root, folder, key, extension, errors); + if (file == null) { + throw new IOException(String.join("; ", errors)); + } + return file; + } + + private static void prepareTargetRoot(Path targetRoot) throws IOException { + if (Files.exists(targetRoot, LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isDirectory(targetRoot)) { + throw new IOException("Structure package target is not a directory: " + targetRoot); + } + return; + } + Files.createDirectories(targetRoot); + } + + private static Path prepareTargetResource(Path targetRoot, String folder, String key, String extension) throws IOException { + Path target = resolveResourceOrThrow(targetRoot, folder, key, extension); + Path parent = target.getParent(); + Path current = targetRoot; + Path relativeParent = targetRoot.relativize(parent); + for (Path segment : relativeParent) { + Path next = current.resolve(segment); + if (Files.exists(next, LinkOption.NOFOLLOW_LINKS)) { + if (Files.isSymbolicLink(next) || !Files.isDirectory(next, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Structure package target contains a non-directory or symbolic link: " + next); + } + } else { + Files.createDirectory(next); + } + current = next; + } + if (Files.isSymbolicLink(target)) { + throw new IOException("Structure package target is a symbolic link: " + target); + } + return target; + } + + private static boolean isContainedSourceResource(Path sourceRoot, String folder, String key, String extension, + Path file, List errors) { + try { + Path realSourceRoot = sourceRoot.toRealPath(); + Path realFile = file.toRealPath(); + if (!realFile.startsWith(realSourceRoot)) { + errors.add("Resource escapes the structure package through a symbolic link: " + + folder + "/" + key + extension + "."); + return false; + } + if (!hasExactSourceCase(sourceRoot, file)) { + errors.add("Resource path casing does not match its reference: " + + folder + "/" + key + extension + "."); + return false; + } + return true; + } catch (IOException exception) { + errors.add("Cannot verify " + folder + " resource '" + key + "': " + describe(exception)); + return false; + } + } + + private static boolean hasExactSourceCase(Path sourceRoot, Path file) throws IOException { + Path current = sourceRoot; + for (Path segment : sourceRoot.relativize(file)) { + Path exactChild = findExactChild(current, segment.toString()); + if (exactChild == null) { + return false; + } + current = exactChild; + } + return true; + } + + private static Path findExactChild(Path folder, String name) throws IOException { + try (DirectoryStream children = Files.newDirectoryStream(folder)) { + for (Path child : children) { + if (child.getFileName().toString().equals(name)) { + return child; + } + } + } + return null; + } + + private static void addArrayKeys(JSONArray values, Set target, String kind, List errors) { + if (values == null) { + return; + } + for (int index = 0; index < values.length(); index++) { + Object raw = values.opt(index); + if (!(raw instanceof String value)) { + errors.add("Invalid " + kind + " reference at index " + index + "."); + continue; + } + addRequiredKey(target, value, kind, errors); + } + } + + private static void addRequiredKey(Set target, String key, String kind, List errors) { + String canonical = canonicalKey(key); + if (canonical == null) { + errors.add("Missing or invalid " + kind + "."); + return; + } + target.add(canonical); + } + + private static void enqueueKey(Deque queue, String key, String kind, List errors) { + String canonical = canonicalKey(key); + if (canonical == null) { + errors.add("Missing or invalid " + kind + "."); + return; + } + queue.addLast(canonical); + } + + private static void enqueueOptionalKey(Deque queue, String key, String kind, List errors) { + if (key == null || key.isBlank()) { + return; + } + enqueueKey(queue, key, kind, errors); + } + + private static String canonicalKey(String key) { + if (key == null || !key.equals(key.trim())) { + return null; + } + return validKey(key) ? key : null; + } + + private static boolean validKey(String key) { + if (key == null || key.isBlank() || !key.equals(key.trim())) { + return false; + } + try { + StructureResourceBundle.validateRelativePath(key); + return true; + } catch (IllegalArgumentException exception) { + return false; + } + } + + private static String requiredString(JSONObject object, String field, String resource, List errors) { + Object value = object.opt(field); + if (value instanceof String stringValue) { + return stringValue; + } + errors.add(resource + " requires string field '" + field + "'."); + return null; + } + + private static String optionalString(JSONObject object, String field, String resource, List errors) { + if (!object.has(field)) { + return null; + } + return requiredString(object, field, resource, errors); + } + + private static JSONArray requiredArray(JSONObject object, String field, String resource, List errors) { + Object value = object.opt(field); + if (value instanceof JSONArray arrayValue) { + return arrayValue; + } + errors.add(resource + " requires array field '" + field + "'."); + return null; + } + + private static JSONArray optionalArray(JSONObject object, String field, String resource, List errors) { + if (!object.has(field)) { + return null; + } + return requiredArray(object, field, resource, errors); + } + + private static boolean isEmptyEntry(JSONObject entry, String poolKey, int index, List errors) { + if (!entry.has("empty")) { + return false; + } + Object emptyValue = entry.opt("empty"); + if (!(emptyValue instanceof Boolean isEmpty)) { + errors.add("Piece entry " + index + " in jigsaw pool '" + poolKey + "' requires boolean field 'empty'."); + return false; + } + if (!isEmpty) { + return false; + } + if (!entry.has("piece")) { + return true; + } + Object pieceValue = entry.opt("piece"); + if (!(pieceValue instanceof String piece)) { + errors.add("Empty piece entry " + index + " in jigsaw pool '" + poolKey + + "' requires string field 'piece' when defined."); + } else if (!piece.isBlank()) { + errors.add("Empty piece entry " + index + " in jigsaw pool '" + poolKey + + "' cannot define non-empty field 'piece'."); + } + return true; + } + + private static void validatePortableCollisions(Set keys, String folder, List errors) { + Map portableKeys = new HashMap<>(); + for (String key : keys) { + String portableKey = key.toLowerCase(Locale.ROOT); + String previous = portableKeys.putIfAbsent(portableKey, key); + if (previous != null && !previous.equals(key)) { + errors.add("Case-insensitive " + folder + " resource collision between '" + + previous + "' and '" + key + "'."); + } + } + } + + private static String describe(Exception exception) { + String message = exception.getMessage(); + return message == null || message.isBlank() ? exception.getClass().getSimpleName() : message; + } + + public record Limits(int maxResources, int maxJsonBytes) { + public Limits { + if (maxResources < 1 || maxResources > 100_000) { + throw new IllegalArgumentException("Structure closure resource limit must be between 1 and 100000"); + } + if (maxJsonBytes < 1 || maxJsonBytes > 64 * 1024 * 1024) { + throw new IllegalArgumentException("Structure closure JSON limit must be between 1 and 67108864 bytes"); + } + } + } + + private static final class MutableClosure { + private final Limits limits; + private final Set structures; + private final Set pools; + private final Set pieces; + private final Set objects; + private final Set loot; + private final List errors = new ArrayList<>(); + private final Deque structureQueue = new ArrayDeque<>(); + private final Deque poolQueue = new ArrayDeque<>(); + private final Deque pieceQueue = new ArrayDeque<>(); + private int resources; + private boolean resourceLimitReported; + + private MutableClosure(Limits limits) { + this.limits = limits; + structures = new BudgetedSet(this); + pools = new BudgetedSet(this); + pieces = new BudgetedSet(this); + objects = new BudgetedSet(this); + loot = new BudgetedSet(this); + } + + private boolean reserveResource() { + if (limits == null || resources < limits.maxResources()) { + resources++; + return true; + } + if (!resourceLimitReported) { + errors.add("Structure closure exceeds " + limits.maxResources() + " resources."); + resourceLimitReported = true; + } + return false; + } + } + + private static final class BudgetedSet extends LinkedHashSet { + private final MutableClosure closure; + + private BudgetedSet(MutableClosure closure) { + this.closure = closure; + } + + @Override + public boolean add(String value) { + if (contains(value) || !closure.reserveResource()) { + return false; + } + return super.add(value); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java new file mode 100644 index 000000000..109eea2d5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java @@ -0,0 +1,558 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.core.protocol.IrisProtocolServer; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.tools.IrisPackBenchmarking; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.Looper; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + + +public class IrisPregenerator { + private static final double INVALID = 9223372036854775807d; + private static final AtomicLong JOB_SEQUENCE = new AtomicLong(0L); + private final long jobId; + private final PregenTask task; + private final PregeneratorMethod generator; + private final PregenListener listener; + private final Looper ticker; + private final AtomicBoolean paused; + private final AtomicBoolean shutdown; + private final PregenRateTracker rateTracker; + private final KList chunksPerSecondHistory; + private final AtomicLong generated; + private final AtomicLong cached; + private final AtomicLong lastCachedAtMillis; + private final AtomicLong historyLastGenerated; + private final AtomicLong historyLastCached; + private final AtomicLong historyLastMillis; + private final AtomicLong totalChunks; + private final AtomicLong startTime; + private final AtomicReference currentGeneratorMethod; + private final KSet generatedRegions; + private final KSet retry; + private final KSet net; + private final ChronoLatch cl; + private final ChronoLatch saveLatch; + private final ChronoLatch heapReclaimLatch; + private final AtomicLong failed; + private final IrisPackBenchmarking benchmarking; + private volatile PregenRates rates; + + public IrisPregenerator(PregenTask task, PregeneratorMethod generator, PregenListener listener) { + this.jobId = JOB_SEQUENCE.incrementAndGet(); + benchmarking = IrisPackBenchmarking.getInstance(); + this.listener = listenify(listener); + cl = new ChronoLatch(30000, false); + saveLatch = new ChronoLatch(IrisSettings.get().getPregen().getSaveIntervalMs()); + heapReclaimLatch = new ChronoLatch(1000); + failed = new AtomicLong(0); + generatedRegions = new KSet<>(); + this.shutdown = new AtomicBoolean(false); + this.paused = new AtomicBoolean(false); + this.task = task; + this.generator = generator; + retry = new KSet<>(); + net = new KSet<>(); + currentGeneratorMethod = new AtomicReference<>("Void"); + chunksPerSecondHistory = new KList<>(); + generated = new AtomicLong(0); + cached = new AtomicLong(); + lastCachedAtMillis = new AtomicLong(0L); + historyLastGenerated = new AtomicLong(0L); + historyLastCached = new AtomicLong(0L); + historyLastMillis = new AtomicLong(M.ms()); + totalChunks = new AtomicLong(0); + startTime = new AtomicLong(M.ms()); + rateTracker = new PregenRateTracker(startTime.get(), 0L); + rates = PregenRates.ZERO; + ticker = new Looper() { + @Override + protected long loop() { + publishProgress(M.ms(), cl.flip()); + return 1000; + } + }; + } + + private long computeETA() { + long gen = generated.get(); + long total = totalChunks.get(); + long remaining = total - gen; + double cps = gen > 1024 ? rates.overall() : rates.tenSecond(); + double d = cps > 0D ? (remaining / cps) * 1000D : 0D; + return Double.isFinite(d) && d != INVALID ? (long) d : 0; + } + + private void publishProgress(long now, boolean logProgress) { + long generatedCount = generated.get(); + long total = totalChunks.get(); + rates = rateTracker.sample(generatedCount, now); + sampleHistory(now, generatedCount, cached.get()); + long eta = computeETA(); + long lastCachedAt = lastCachedAtMillis.get(); + boolean cachedProgress = lastCachedAt > 0L && now - lastCachedAt <= 10_000L; + double chunksPerMinute = rates.sixtySecond() * 60D; + double percentage = total > 0L ? (double) generatedCount / (double) total : 0D; + + listener.onTick( + rates.tenSecond(), + chunksPerMinute, + chunksPerMinute / 1024D, + percentage, + generatedCount, + total, + Math.max(0L, total - generatedCount), + eta, + now - startTime.get(), + currentGeneratorMethod.get(), + cachedProgress); + + IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class); + if (protocolServer != null) { + protocolServer.broadcastPregenProgress( + jobId, + generatedCount, + total, + rates.tenSecond(), + eta, + paused.get() ? IrisMessage.PregenProgress.STATE_PAUSED : IrisMessage.PregenProgress.STATE_RUNNING); + } + + if (logProgress) { + IrisLogging.info("%s: %s of %s (%.0f%%), rates overall=%s 10s=%s 30s=%s 60s=%s chunks/s%s, ETA: %s", + benchmarking != null ? "Benchmarking" : "Pregen", + Form.f(generatedCount), + Form.f(total), + percentage * 100D, + formatRate(rates.overall()), + formatRate(rates.tenSecond()), + formatRate(rates.thirtySecond()), + formatRate(rates.sixtySecond()), + cachedProgress ? " (cached)" : "", + Form.duration(eta, 2)); + } + } + + private void sampleHistory(long now, long generatedCount, long cachedCount) { + long previousTime = historyLastMillis.getAndSet(now); + long previousGenerated = historyLastGenerated.getAndSet(generatedCount); + long previousCached = historyLastCached.getAndSet(cachedCount); + long elapsed = now - previousTime; + long completed = generatedCount - previousGenerated - (cachedCount - previousCached); + if (elapsed <= 0L || completed <= 0L) { + return; + } + + int chunksPerSecond = (int) Math.round((double) completed * 1_000D / (double) elapsed); + synchronized (chunksPerSecondHistory) { + chunksPerSecondHistory.add(chunksPerSecond); + } + } + + private static String formatRate(double rate) { + return Form.f(rate, 1); + } + + + public long getJobId() { + return jobId; + } + + public void close() { + shutdown.set(true); + } + + public void start() { + PrecisionStopwatch p = PrecisionStopwatch.start(); + boolean completed = false; + // Everything runs inside the try: an early throw from init/checkRegions must still + // reach shutdown(), or the ticker/monitor threads leak and listener.onClose never + // fires — leaving a phantom job that suppresses spawns and blocks future pregens. + try { + init(); + task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet()); + long startedAt = M.ms(); + startTime.set(startedAt); + rateTracker.reset(startedAt, generated.get()); + historyLastGenerated.set(generated.get()); + historyLastCached.set(cached.get()); + historyLastMillis.set(startedAt); + ticker.start(); + checkRegions(); + int[] regionBounds = task.regionBounds(); + generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]); + task.iterateRegions((x, z) -> visitRegion(x, z, true)); + task.iterateRegions((x, z) -> visitRegion(x, z, false)); + completed = true; + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Pregen aborted after " + Form.duration((long) p.getMilliseconds()) + " due to " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } finally { + publishProgress(M.ms(), false); + shutdown(); + } + if (completed && allVisitsComplete()) { + logSuccessfulCompletion(p); + } else { + logIncompleteCompletion(p); + } + if (benchmarking != null) { + benchmarking.finishedBenchmark(snapshotChunksPerSecondHistory(), rates.overall()); + } + } + + private KList snapshotChunksPerSecondHistory() { + synchronized (chunksPerSecondHistory) { + return new KList<>(chunksPerSecondHistory); + } + } + + private boolean allVisitsComplete() { + long total = totalChunks.get(); + return total > 0 && generated.get() + failed.get() >= total; + } + + private void logSuccessfulCompletion(PrecisionStopwatch stopwatch) { + long failedCount = failed.get(); + if (failedCount > 0) { + IrisLogging.warn("Pregen finished with " + Form.f(failedCount) + " failed chunk(s); failures are not cached, rerun to fill them"); + } + IrisLogging.notice("Pregen finished: generated=" + Form.f(generated.get()) + + " total=" + Form.f(totalChunks.get()) + + " failed=" + Form.f(failedCount) + + " duration=" + Form.duration((long) stopwatch.getMilliseconds()) + + formatRateSummary()); + } + + private void logIncompleteCompletion(PrecisionStopwatch stopwatch) { + long generatedCount = generated.get(); + long failedCount = failed.get(); + long total = totalChunks.get(); + long remaining = Math.max(0L, total - generatedCount - failedCount); + String status = shutdown.get() ? "Pregen cancelled" : "Pregen stopped before completion"; + IrisLogging.info(status + ": generated=" + Form.f(generatedCount) + + " total=" + Form.f(total) + + " failed=" + Form.f(failedCount) + + " remaining=" + Form.f(remaining) + + " duration=" + Form.duration((long) stopwatch.getMilliseconds()) + + formatRateSummary()); + } + + private String formatRateSummary() { + return " rates[overall=" + formatRate(rates.overall()) + + ", 10s=" + formatRate(rates.tenSecond()) + + ", 30s=" + formatRate(rates.thirtySecond()) + + ", 60s=" + formatRate(rates.sixtySecond()) + + "] chunks/s"; + } + + private void checkRegions() { + task.iterateRegions(this::checkRegion); + } + + private void init() { + generator.init(); + } + + private void shutdown() { + shutdownStep("saving", listener::onSaving); + shutdownStep("generator", generator::close); + shutdownStep("ticker", ticker::interrupt); + shutdownStep("listener", listener::onClose); + shutdownStep("protocol", () -> { + IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class); + if (protocolServer != null) { + long total = totalChunks.get(); + protocolServer.pregenEnd(jobId, total > 0 && generated.get() >= total); + } + }); + shutdownStep("mantle", () -> { + Mantle mantle = getMantle(); + if (mantle != null) { + reclaimTectonicPlates(mantle); + } + }); + } + + private void shutdownStep(String step, Runnable action) { + try { + action.run(); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Pregen shutdown step " + step + " failed: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + + private void reclaimTectonicPlates(Mantle mantle) { + int loadedBefore = mantle.getLoadedRegionCount(); + if (loadedBefore <= 0) { + return; + } + + int previousLoaded = loadedBefore; + int stagnantRounds = 0; + for (int attempt = 0; attempt < 24 && stagnantRounds < 4; attempt++) { + mantle.trim(0, 0); + mantle.unloadTectonicPlate(0); + int loaded = mantle.getLoadedRegionCount(); + if (loaded <= 0) { + break; + } + + if (loaded >= previousLoaded) { + stagnantRounds++; + } else { + stagnantRounds = 0; + } + + previousLoaded = loaded; + J.sleep(150); + } + + int loadedAfter = mantle.getLoadedRegionCount(); + if (loadedAfter < loadedBefore) { + IrisLogging.info("Pregen reclaimed " + (loadedBefore - loadedAfter) + " tectonic plate(s) from the mantle"); + } + } + + private void visitRegion(int x, int z, boolean regions) { + while (paused.get() && !shutdown.get()) { + J.sleep(50); + } + + if (shutdown.get()) { + listener.onRegionSkipped(x, z); + return; + } + + Position2 pos = new Position2(x, z); + + if (generatedRegions.contains(pos)) { + return; + } + + currentGeneratorMethod.set(generator.getMethod(x, z)); + boolean hit = false; + if (generator.supportsRegions(x, z, listener) && regions) { + hit = true; + listener.onRegionGenerating(x, z); + generator.generateRegion(x, z, listener); + } else if (!regions) { + hit = true; + listener.onRegionGenerating(x, z); + task.iterateChunks(x, z, (xx, zz) -> { + while ((paused.get() || MantleHeapPressure.overHighWater()) && !shutdown.get()) { + if (!paused.get()) { + reclaimHeapPressure(); + } + J.sleep(50); + } + + if (shutdown.get()) { + return; + } + + generator.generateChunk(xx, zz, listener); + }); + } + + if (hit) { + // Exactly once per visited region, on BOTH branches: a cache-completed region that + // never releases its sentinel wedges neighbor eviction for the whole resume + // frontier (allNeighborsDrained can never pass around it). + generator.onRegionSubmitted(x, z); + listener.onRegionGenerated(x, z); + + if (saveLatch.flip()) { + listener.onSaving(); + generator.save(); + try { + Mantle mantle = getMantle(); + if (mantle != null) { + mantle.trim(0, 0); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + generatedRegions.add(pos); + // No checkRegions() here: re-spiraling every region after every completed region + // was O(regions^2) dead work on the submitter thread (result discarded, and the + // only side effect was CachedPregenMethod faulting plates against eviction). + } + } + + private void reclaimHeapPressure() { + if (!heapReclaimLatch.flip()) { + return; + } + + try { + Mantle mantle = getMantle(); + if (mantle != null) { + mantle.trim(0, 0); + mantle.unloadTectonicPlate(0); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + MantleHeapPressure.requestPanicReclaim(); + } + + private void checkRegion(int x, int z) { + if (generatedRegions.contains(new Position2(x, z))) { + return; + } + + generator.supportsRegions(x, z, listener); + } + + public long getFailedChunks() { + return failed.get(); + } + + public PregenRates getRates() { + return rates; + } + + public void pause() { + paused.set(true); + } + + public void resume() { + paused.set(false); + } + + private PregenListener listenify(PregenListener listener) { + return new PregenListener() { + @Override + public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) { + listener.onTick(chunksPerSecond, chunksPerMinute, regionsPerMinute, percent, generated, totalChunks, chunksRemaining, eta, elapsed, method, cached); + } + + @Override + public void onChunkGenerating(int x, int z) { + listener.onChunkGenerating(x, z); + } + + @Override + public void onChunkGenerated(int x, int z, boolean c) { + listener.onChunkGenerated(x, z, c); + generated.addAndGet(1); + if (c) { + cached.addAndGet(1); + lastCachedAtMillis.set(M.ms()); + } + } + + @Override + public void onChunkFailed(int x, int z) { + failed.addAndGet(1); + listener.onChunkFailed(x, z); + } + + @Override + public void onRegionGenerated(int x, int z) { + listener.onRegionGenerated(x, z); + } + + @Override + public void onRegionGenerating(int x, int z) { + listener.onRegionGenerating(x, z); + } + + @Override + public void onChunkCleaned(int x, int z) { + listener.onChunkCleaned(x, z); + } + + @Override + public void onRegionSkipped(int x, int z) { + listener.onRegionSkipped(x, z); + } + + @Override + public void onNetworkStarted(int x, int z) { + net.add(new Position2(x, z)); + } + + @Override + public void onNetworkFailed(int x, int z) { + retry.add(new Position2(x, z)); + } + + @Override + public void onNetworkReclaim(int revert) { + generated.addAndGet(-revert); + } + + @Override + public void onNetworkGeneratedChunk(int x, int z) { + generated.addAndGet(1); + } + + @Override + public void onNetworkDownloaded(int x, int z) { + net.remove(new Position2(x, z)); + } + + @Override + public void onClose() { + listener.onClose(); + } + + @Override + public void onSaving() { + listener.onSaving(); + } + + @Override + public void onChunkExistsInRegionGen(int x, int z) { + listener.onChunkExistsInRegionGen(x, z); + } + }; + } + + public boolean paused() { + return paused.get(); + } + + public Mantle getMantle() { + return generator.getMantle(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/MantleHeapPressure.java b/core/src/main/java/art/arcane/iris/core/pregenerator/MantleHeapPressure.java new file mode 100644 index 000000000..03ea5d2eb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/MantleHeapPressure.java @@ -0,0 +1,286 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +import art.arcane.iris.spi.IrisLogging; + +import javax.management.MBeanServer; +import javax.management.ObjectName; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryUsage; +import java.util.function.BiConsumer; +import java.util.function.DoubleConsumer; +import java.util.function.LongSupplier; + +public final class MantleHeapPressure { + private static final double HIGH_WATER = 0.92D; + private static final double LOW_WATER = 0.82D; + private static final long MAXIMUM_HYSTERESIS_MS = 60_000L; + private static final ObjectName DIAGNOSTIC_COMMAND = diagnosticCommandName(); + private static final PanicGcReclaimer PANIC_RECLAIMER = new PanicGcReclaimer( + new PanicGcPolicy(10_000L, 60_000L, 60_000L, 15 * 60_000L), + new PanicGcActions( + System::currentTimeMillis, + System::gc, + () -> invokeHotSpotDiagnosticGc(ManagementFactory.getPlatformMBeanServer()), + // An escalation step in the reclaim policy, rate limited by that policy rather than by + // anything the operator did. What is actionable is the wait budget being exceeded. + (double fraction) -> IrisLogging.info( + "Iris heap remained at %.1f%% after normal panic reclaim; invoking the current JVM's diagnostic full GC to keep generation live.", + fraction * 100.0D), + (String context, Throwable failure) -> IrisLogging.reportError(context, failure))); + private static final HeapPressureGate PRESSURE_GATE = new HeapPressureGate( + HIGH_WATER, + LOW_WATER, + MAXIMUM_HYSTERESIS_MS, + System::currentTimeMillis, + PANIC_RECLAIMER::resetEpisode); + + private MantleHeapPressure() { + } + + public static double usedFraction() { + MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); + long max = heap.getMax(); + if (max <= 0L) { + Runtime runtime = Runtime.getRuntime(); + long runtimeMax = runtime.maxMemory(); + if (runtimeMax <= 0L) { + return 0.0D; + } + long used = runtime.totalMemory() - runtime.freeMemory(); + return (double) used / (double) runtimeMax; + } + return (double) heap.getUsed() / (double) max; + } + + public static boolean overHighWater() { + return PRESSURE_GATE.update(usedFraction()); + } + + public static double reclaimUrgency(double fraction) { + if (!Double.isFinite(fraction) || fraction <= LOW_WATER) { + return 0D; + } + if (fraction >= HIGH_WATER) { + return 1D; + } + return (fraction - LOW_WATER) / (HIGH_WATER - LOW_WATER); + } + + public static void requestPanicReclaim() { + PANIC_RECLAIMER.request(usedFraction()); + } + + static void invokeHotSpotDiagnosticGc(MBeanServer server) throws Exception { + if (!server.isRegistered(DIAGNOSTIC_COMMAND)) { + throw new UnsupportedOperationException("HotSpot DiagnosticCommand MBean is not registered on this JVM"); + } + server.invoke( + DIAGNOSTIC_COMMAND, + "gcRun", + new Object[0], + new String[0]); + } + + private static ObjectName diagnosticCommandName() { + try { + return new ObjectName("com.sun.management:type=DiagnosticCommand"); + } catch (Exception failure) { + throw new ExceptionInInitializerError(failure); + } + } + + record PanicGcPolicy( + long diagnosticDelayMs, + long diagnosticCooldownMs, + long initialFailureBackoffMs, + long maximumFailureBackoffMs + ) { + PanicGcPolicy { + if (diagnosticDelayMs < 0L + || diagnosticCooldownMs < 0L + || initialFailureBackoffMs < 0L + || maximumFailureBackoffMs < initialFailureBackoffMs) { + throw new IllegalArgumentException("Invalid panic GC timing policy"); + } + } + } + + record PanicGcActions( + LongSupplier clock, + Runnable explicitGc, + DiagnosticGc diagnosticGc, + DoubleConsumer diagnosticStart, + BiConsumer failureSink + ) { + } + + @FunctionalInterface + interface DiagnosticGc { + void run() throws Exception; + } + + static final class HeapPressureGate { + private static final long NOT_BELOW_HIGH_WATER = Long.MIN_VALUE; + + private final double highWater; + private final double lowWater; + private final long maximumHysteresisMs; + private final LongSupplier clock; + private final Runnable releaseAction; + private boolean engaged; + private long belowHighWaterSince; + + HeapPressureGate(double highWater, double lowWater, long maximumHysteresisMs, LongSupplier clock, Runnable releaseAction) { + if (!Double.isFinite(highWater) + || !Double.isFinite(lowWater) + || lowWater < 0.0D + || highWater <= lowWater + || maximumHysteresisMs < 0L) { + throw new IllegalArgumentException("Invalid heap pressure hysteresis policy"); + } + this.highWater = highWater; + this.lowWater = lowWater; + this.maximumHysteresisMs = maximumHysteresisMs; + this.clock = clock; + this.releaseAction = releaseAction; + this.belowHighWaterSince = NOT_BELOW_HIGH_WATER; + } + + synchronized boolean update(double fraction) { + if (!Double.isFinite(fraction)) { + return engaged; + } + if (!engaged) { + if (fraction >= highWater) { + engaged = true; + belowHighWaterSince = NOT_BELOW_HIGH_WATER; + } + return engaged; + } + if (fraction <= lowWater) { + release(); + return false; + } + if (fraction >= highWater) { + belowHighWaterSince = NOT_BELOW_HIGH_WATER; + return true; + } + + long now = clock.getAsLong(); + if (belowHighWaterSince == NOT_BELOW_HIGH_WATER) { + belowHighWaterSince = now; + return true; + } + if (elapsed(now, belowHighWaterSince) < maximumHysteresisMs) { + return true; + } + + release(); + return false; + } + + private void release() { + engaged = false; + belowHighWaterSince = NOT_BELOW_HIGH_WATER; + releaseAction.run(); + } + + private static long elapsed(long now, long then) { + return now >= then ? now - then : Long.MAX_VALUE; + } + } + + static final class PanicGcReclaimer { + private final PanicGcPolicy policy; + private final PanicGcActions actions; + private boolean panicEpisode; + private long explicitAttemptAt; + private long nextDiagnosticAllowedAt; + private long failureBackoffMs; + + PanicGcReclaimer(PanicGcPolicy policy, PanicGcActions actions) { + this.policy = policy; + this.actions = actions; + this.failureBackoffMs = policy.initialFailureBackoffMs(); + } + + synchronized void request(double fraction) { + if (!Double.isFinite(fraction) || fraction <= LOW_WATER) { + return; + } + + long now = actions.clock().getAsLong(); + if (!panicEpisode) { + if (fraction < HIGH_WATER) { + return; + } + beginEpisode(now); + return; + } + if (elapsed(now, explicitAttemptAt) < policy.diagnosticDelayMs() + || now < nextDiagnosticAllowedAt) { + return; + } + + actions.diagnosticStart().accept(fraction); + try { + actions.diagnosticGc().run(); + failureBackoffMs = policy.initialFailureBackoffMs(); + nextDiagnosticAllowedAt = deadline(now, policy.diagnosticCooldownMs()); + } catch (Exception failure) { + actions.failureSink().accept( + "Iris could not invoke the current JVM's DiagnosticCommand GC after normal panic reclaim was ineffective; generation remains pressure-limited to avoid an OOM.", + failure); + nextDiagnosticAllowedAt = deadline(now, failureBackoffMs); + failureBackoffMs = Math.min(policy.maximumFailureBackoffMs(), doubled(failureBackoffMs)); + } + } + + synchronized void resetEpisode() { + panicEpisode = false; + explicitAttemptAt = 0L; + } + + private void beginEpisode(long now) { + panicEpisode = true; + explicitAttemptAt = now; + try { + actions.explicitGc().run(); + } catch (RuntimeException failure) { + actions.failureSink().accept( + "Iris normal panic heap reclaim failed; the diagnostic fallback will be attempted if pressure remains critical.", + failure); + } + } + + private static long elapsed(long now, long then) { + return now >= then ? now - then : Long.MAX_VALUE; + } + + private static long deadline(long now, long delay) { + return delay > Long.MAX_VALUE - now ? Long.MAX_VALUE : now + delay; + } + + private static long doubled(long value) { + return value > Long.MAX_VALUE / 2L ? Long.MAX_VALUE : value * 2L; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiPhase.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiPhase.java new file mode 100644 index 000000000..a2c1ffa68 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiPhase.java @@ -0,0 +1,29 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +public enum PregenApiPhase { + STARTED, + TICK, + PAUSED, + RESUMED, + SAVING, + COMPLETED, + CANCELLED +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiSink.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiSink.java new file mode 100644 index 000000000..8c844553e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiSink.java @@ -0,0 +1,25 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +import art.arcane.iris.core.gui.PregeneratorJob; + +public interface PregenApiSink { + void pregen(PregenApiPhase phase, PregeneratorJob.PregenProgress progress); +} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenListener.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenListener.java similarity index 94% rename from core/src/main/java/com/volmit/iris/core/pregenerator/PregenListener.java rename to core/src/main/java/art/arcane/iris/core/pregenerator/PregenListener.java index 6f5d83194..d41c34530 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenListener.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenListener.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.pregenerator; +package art.arcane.iris.core.pregenerator; public interface PregenListener { void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached); @@ -29,6 +29,9 @@ public interface PregenListener { void onChunkGenerated(int x, int z, boolean cached); + default void onChunkFailed(int x, int z) { + } + void onRegionGenerated(int x, int z); void onRegionGenerating(int x, int z); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java new file mode 100644 index 000000000..39152f161 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java @@ -0,0 +1,185 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.math.M; + +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +public final class PregenMantleBackpressure { + private final Supplier mantleSupplier; + private final int maxResidentTectonicPlates; + private final int waitMs; + private final long timeoutMs; + private final Runnable onBudgetTimeout; + private final Supplier diagnostics; + private final BooleanSupplier cancelled; + + public PregenMantleBackpressure(Supplier mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier diagnostics) { + this(mantleSupplier, maxResidentTectonicPlates, waitMs, timeoutMs, onBudgetTimeout, diagnostics, () -> false); + } + + public PregenMantleBackpressure(Supplier mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier diagnostics, BooleanSupplier cancelled) { + this.mantleSupplier = mantleSupplier; + this.maxResidentTectonicPlates = maxResidentTectonicPlates; + this.waitMs = waitMs; + this.timeoutMs = timeoutMs; + this.onBudgetTimeout = onBudgetTimeout; + this.diagnostics = diagnostics; + this.cancelled = cancelled; + } + + public void apply() { + enforceMantleBudget(); + awaitHeapHeadroom(); + } + + public void enforceMantleBudget() { + int cap = maxResidentTectonicPlates; + if (cap <= 0) { + return; + } + + Mantle mantle = resolveMantle(); + if (mantle == null) { + return; + } + + int hardCap = cap * 2; + if (mantle.getLoadedRegionCount() <= hardCap) { + return; + } + + long waitStart = M.ms(); + long lastLog = 0L; + while (mantle.getLoadedRegionCount() > hardCap) { + if (isCancelled()) { + return; + } + + int freed; + int resident; + try { + mantle.trim(0L, 0); + freed = mantle.unloadTectonicPlate(0); + resident = mantle.getLoadedRegionCount(); + } catch (Throwable e) { + IrisLogging.reportError(e); + break; + } + if (resident <= hardCap) { + break; + } + + long elapsed = M.ms() - waitStart; + if (elapsed >= timeoutMs) { + IrisLogging.warn("Pregen mantle backpressure exceeded " + timeoutMs + "ms with " + resident + + " tectonic plates resident (hard cap " + hardCap + "); proceeding to avoid deadlock. " + + "Raise pregen.maxResidentTectonicPlates if this persists. " + diagnostics.get()); + onBudgetTimeout.run(); + return; + } + + long logNow = M.ms(); + if (logNow - lastLog >= 5_000L) { + lastLog = logNow; + // Pausing to stay under the plate cap is the design working, and it is reported every five + // seconds for the whole duration of a large pregen. Only exceeding the budget is a warning. + IrisLogging.info("Pregen mantle backpressure: " + resident + " tectonic plates resident (hard cap " + hardCap + + "), freed " + freed + " last pass, waited " + elapsed + "ms."); + } + + try { + Thread.sleep(waitMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + + public void awaitHeapHeadroom() { + Mantle mantle = resolveMantle(); + long waitStart = M.ms(); + long lastLog = 0L; + while (MantleHeapPressure.overHighWater()) { + if (isCancelled()) { + return; + } + + try { + if (mantle != null && mantle.getLoadedRegionCount() > maxResidentTectonicPlates) { + mantle.trim(0L, 0); + mantle.unloadTectonicPlate(0); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + MantleHeapPressure.requestPanicReclaim(); + + long elapsed = M.ms() - waitStart; + if (elapsed >= timeoutMs) { + IrisLogging.warn("Pregen heap pressure wait exceeded " + timeoutMs + "ms at " + + Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; proceeding to avoid deadlock. " + + diagnostics.get()); + onBudgetTimeout.run(); + return; + } + + long logNow = M.ms(); + if (logNow - lastLog >= 5_000L) { + lastLog = logNow; + IrisLogging.info("Pregen heap pressure: pausing generation at " + + Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; evicting tectonic plates and waiting for headroom" + + (mantle != null ? " (" + mantle.getLoadedRegionCount() + " plates resident)" : "") + "."); + } + + try { + Thread.sleep(waitMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + + private boolean isCancelled() { + if (Thread.currentThread().isInterrupted()) { + return true; + } + + try { + return cancelled.getAsBoolean(); + } catch (Throwable ignored) { + return false; + } + } + + private Mantle resolveMantle() { + try { + return mantleSupplier.get(); + } catch (Throwable ignored) { + return null; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfile.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfile.java new file mode 100644 index 000000000..a16cea5c2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPerformanceProfile.java @@ -0,0 +1,105 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.utility.CachedStream2D; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class PregenPerformanceProfile { + private static final long HOTLOAD_ACQUISITION_TIMEOUT_SECONDS = 30L; + private static final AtomicBoolean JVM_HINT_LOGGED = new AtomicBoolean(false); + + private PregenPerformanceProfile() { + } + + public static boolean apply() { + IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance(); + int previousNoiseCacheSize = performance.getNoiseCacheSize(); + int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096); + boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast"); + boolean changed = false; + + if (targetNoiseCacheSize != previousNoiseCacheSize) { + performance.setNoiseCacheSize(targetNoiseCacheSize); + changed = true; + } + + if (!fastCacheEnabledBefore) { + System.setProperty("iris.cache.fast", "true"); + changed = true; + } + + if (JVM_HINT_LOGGED.compareAndSet(false, true) && !fastCacheEnabledBefore) { + IrisLogging.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true"); + } + + return changed; + } + + public static void apply(Engine engine) { + apply(); + if (requiresNoiseCacheRefresh(engine)) { + engine.hotloadComplex(); + logApplied(); + } + } + + public static void applyToGenerator(PlatformChunkGenerator generator) { + apply(); + Engine engine = generator == null ? null : generator.getEngine(); + if (requiresNoiseCacheRefresh(engine)) { + generator.hotloadComplexAsync(HOTLOAD_ACQUISITION_TIMEOUT_SECONDS, TimeUnit.SECONDS).join(); + logApplied(); + } + } + + private static boolean requiresNoiseCacheRefresh(Engine engine) { + if (engine == null) { + return false; + } + IrisComplex complex = engine.getComplex(); + if (complex == null) { + return true; + } + ProceduralStream heightStream = complex.getHeightStream(); + if (!(heightStream instanceof MeteredCache cache)) { + return true; + } + ProceduralStream caveBiomeStream = complex.getCaveBiomeStream(); + if (!(caveBiomeStream instanceof CachedStream2D cachedStream) || !cachedStream.usesFastCache()) { + return true; + } + long configuredMaxSize = (long) IrisSettings.get().getPerformance().getNoiseCacheSize() * 256L; + return cache.getMaxSize() != configuredMaxSize; + } + + private static void logApplied() { + IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast")); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPhaseTracker.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPhaseTracker.java new file mode 100644 index 000000000..df68dbae7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenPhaseTracker.java @@ -0,0 +1,80 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public final class PregenPhaseTracker { + private final AtomicReference state = new AtomicReference<>(new State(false, false, false, false)); + + public List onTick(boolean pausedNow) { + while (true) { + State current = state.get(); + if (current.finished()) { + return List.of(); + } + + State next = new State(true, pausedNow, false, false); + List phases; + if (!current.started()) { + phases = List.of(PregenApiPhase.STARTED, PregenApiPhase.TICK); + } else if (pausedNow != current.paused()) { + phases = List.of(pausedNow ? PregenApiPhase.PAUSED : PregenApiPhase.RESUMED, PregenApiPhase.TICK); + } else { + phases = List.of(PregenApiPhase.TICK); + } + + if (state.compareAndSet(current, next)) { + return phases; + } + } + } + + public List onSaving() { + while (true) { + State current = state.get(); + if (current.finished() || current.saving()) { + return List.of(); + } + + State next = new State(current.started(), current.paused(), true, false); + if (state.compareAndSet(current, next)) { + return List.of(PregenApiPhase.SAVING); + } + } + } + + public List onClose(boolean completed) { + while (true) { + State current = state.get(); + if (current.finished()) { + return List.of(); + } + + State next = new State(current.started(), current.paused(), current.saving(), true); + if (state.compareAndSet(current, next)) { + return List.of(completed ? PregenApiPhase.COMPLETED : PregenApiPhase.CANCELLED); + } + } + } + + private record State(boolean started, boolean paused, boolean saving, boolean finished) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenRateTracker.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenRateTracker.java new file mode 100644 index 000000000..25970000e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenRateTracker.java @@ -0,0 +1,100 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +final class PregenRateTracker { + private static final int CAPACITY = 64; + private static final long TEN_SECONDS_MILLIS = 10_000L; + private static final long THIRTY_SECONDS_MILLIS = 30_000L; + private static final long SIXTY_SECONDS_MILLIS = 60_000L; + + private final long[] timestamps = new long[CAPACITY]; + private final long[] completed = new long[CAPACITY]; + private long startedAtMillis; + private long startedCompleted; + private int next; + private int size; + + PregenRateTracker(long startedAtMillis, long startedCompleted) { + reset(startedAtMillis, startedCompleted); + } + + synchronized void reset(long atMillis, long completedCount) { + startedAtMillis = atMillis; + startedCompleted = completedCount; + next = 0; + size = 0; + append(atMillis, completedCount); + } + + synchronized PregenRates sample(long completedCount, long atMillis) { + int latestIndex = latestIndex(); + long normalizedTime = Math.max(atMillis, timestamps[latestIndex]); + if (normalizedTime == timestamps[latestIndex]) { + completed[latestIndex] = completedCount; + } else { + append(normalizedTime, completedCount); + } + + return new PregenRates( + rate(startedCompleted, startedAtMillis, completedCount, normalizedTime), + windowRate(completedCount, normalizedTime, TEN_SECONDS_MILLIS), + windowRate(completedCount, normalizedTime, THIRTY_SECONDS_MILLIS), + windowRate(completedCount, normalizedTime, SIXTY_SECONDS_MILLIS)); + } + + private double windowRate(long completedCount, long atMillis, long windowMillis) { + long cutoff = atMillis - windowMillis; + int selectedIndex = oldestIndex(); + for (int offset = 0; offset < size; offset++) { + int index = (oldestIndex() + offset) % CAPACITY; + if (timestamps[index] > cutoff) { + break; + } + selectedIndex = index; + } + return rate(completed[selectedIndex], timestamps[selectedIndex], completedCount, atMillis); + } + + private static double rate(long fromCompleted, long fromMillis, long toCompleted, long toMillis) { + long elapsedMillis = toMillis - fromMillis; + if (elapsedMillis <= 0L) { + return 0D; + } + long delta = Math.max(0L, toCompleted - fromCompleted); + return (double) delta * 1_000D / (double) elapsedMillis; + } + + private void append(long atMillis, long completedCount) { + timestamps[next] = atMillis; + completed[next] = completedCount; + next = (next + 1) % CAPACITY; + if (size < CAPACITY) { + size++; + } + } + + private int latestIndex() { + return (next - 1 + CAPACITY) % CAPACITY; + } + + private int oldestIndex() { + return (next - size + CAPACITY) % CAPACITY; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenRates.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenRates.java new file mode 100644 index 000000000..9be5906b2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenRates.java @@ -0,0 +1,28 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +public record PregenRates( + double overall, + double tenSecond, + double thirtySecond, + double sixtySecond +) { + public static final PregenRates ZERO = new PregenRates(0D, 0D, 0D, 0D); +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java new file mode 100644 index 000000000..9fb9926b7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java @@ -0,0 +1,307 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator; + +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.Spiraled; +import art.arcane.volmlib.util.math.Spiraler; +import lombok.Builder; +import lombok.Data; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Builder +@Data +public class PregenTask { + static final int MAX_WORLD_BLOCK = 29_999_984; + /** + * Saturation limits for block bounds. The full int range is safe downstream: the widest derived value is + * regionToChunk(blockToRegionFloor(MAX_BLOCK)) + 31 shifted back to blocks, which lands inside int. + */ + static final int MIN_BLOCK = Integer.MIN_VALUE; + static final int MAX_BLOCK = Integer.MAX_VALUE; + /** + * Widest region span a pregen may cover on one axis: the Minecraft world limit of +/- 30,000,000 blocks, + * which is 58594 regions each way. Clamping alone is not enough - a saturated bound spans 8.4 million + * regions per axis, and the spiral over that is ~7e13 iterations, which never finishes and looks like a + * hang. A request past the world limit is a bad request, so it fails at construction. + */ + static final int MAX_REGION_SPAN = 117_189; + private static final int MAX_CACHED_ORDERS = 512; + private static final LinkedHashMap ORDERS = new LinkedHashMap<>(64, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_CACHED_ORDERS; + } + }; + + @Builder.Default + private final boolean gui = false; + @Builder.Default + private final Position2 center = new Position2(0, 0); + @Builder.Default + private final int radiusX = 1; + @Builder.Default + private final int radiusZ = 1; + + private final Bounds bounds = new Bounds(); + + protected PregenTask(boolean gui, Position2 center, int radiusX, int radiusZ) { + if (radiusX <= 0 || radiusZ <= 0) { + throw new IllegalArgumentException("Pregen radii must be greater than zero blocks."); + } + requireWithinWorld(center, radiusX, radiusZ); + + this.gui = gui; + this.center = new ProxiedPos(center); + this.radiusX = radiusX; + this.radiusZ = radiusZ; + bounds.update(); + } + + private static void requireWithinWorld(Position2 center, int radiusX, int radiusZ) { + long minX = (long) center.getX() - radiusX; + long maxX = (long) center.getX() + radiusX; + long minZ = (long) center.getZ() - radiusZ; + long maxZ = (long) center.getZ() + radiusZ; + if (minX < -MAX_WORLD_BLOCK || maxX > MAX_WORLD_BLOCK + || minZ < -MAX_WORLD_BLOCK || maxZ > MAX_WORLD_BLOCK) { + throw new IllegalArgumentException("Pregen area exceeds Minecraft's coordinate limit of +/-" + + MAX_WORLD_BLOCK + " blocks: center " + center.getX() + "," + center.getZ() + + " radius " + radiusX + "x" + radiusZ + "."); + } + } + + public static void iterateRegion(int xr, int zr, Spiraled s, Position2 pull) { + iterateRegion(xr, zr, s, pull.getX(), pull.getZ()); + } + + public static void iterateRegion(int xr, int zr, Spiraled s, int pullX, int pullZ) { + for (int packed : orderForPull(pullX, pullZ)) { + s.on(PowerOfTwoCoordinates.unpackLocal32X(packed) + PowerOfTwoCoordinates.regionToChunk(xr), PowerOfTwoCoordinates.unpackLocal32Z(packed) + PowerOfTwoCoordinates.regionToChunk(zr)); + } + } + + public static void iterateRegion(int xr, int zr, Spiraled s) { + iterateRegion(xr, zr, s, -PowerOfTwoCoordinates.regionToChunk(xr), -PowerOfTwoCoordinates.regionToChunk(zr)); + } + + private static int[] orderForPull(int pullX, int pullZ) { + long key = orderKey(pullX, pullZ); + synchronized (ORDERS) { + int[] cached = ORDERS.get(key); + if (cached != null) { + return cached; + } + } + + int[] computed = computeOrder(key); + synchronized (ORDERS) { + ORDERS.put(key, computed); + } + return computed; + } + + private static int[] computeOrder(long key) { + int pullX = (int) (key >> 32); + int pullZ = (int) key; + Position2 pull = new Position2(pullX, pullZ); + KList p = new KList<>(); + new Spiraler(33, 33, (x, z) -> { + int xx = (x + 15); + int zz = (z + 15); + if (xx < 0 || xx > 31 || zz < 0 || zz > 31) { + return; + } + + p.add(new Position2(xx, zz)); + }).drain(); + p.sort(Comparator.comparing((i) -> i.distance(pull))); + + int[] packed = new int[p.size()]; + for (int index = 0; index < p.size(); index++) { + Position2 position = p.get(index); + packed[index] = PowerOfTwoCoordinates.packLocal32(position.getX(), position.getZ()); + } + + return packed; + } + + private static long orderKey(int pullX, int pullZ) { + long high = (long) pullX << 32; + long low = pullZ & 0xFFFFFFFFL; + return high | low; + } + + public void iterateRegions(Spiraled s) { + Bound bound = bounds.region(); + new Spiraler(bound.sizeX, bound.sizeZ, ((x, z) -> { + if (bound.check(x, z)) s.on(x, z); + })).setOffset(PowerOfTwoCoordinates.blockToRegionFloor(center.getX()), PowerOfTwoCoordinates.blockToRegionFloor(center.getZ())).drain(); + } + + public void iterateChunks(int rX, int rZ, Spiraled s) { + Bound bound = bounds.chunk(); + iterateRegion(rX, rZ, ((x, z) -> { + if (bound.check(x, z)) s.on(x, z); + })); + } + + public int[] regionBounds() { + Bound bound = bounds.region(); + return new int[]{bound.minX(), bound.minZ(), bound.maxX(), bound.maxZ()}; + } + + @FunctionalInterface + public interface InterleavedChunkConsumer { + boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion); + } + + public void iterateAllChunks(Spiraled s) { + iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s))); + } + + public void iterateAllChunksInterleaved(InterleavedChunkConsumer consumer) { + List regions = new ArrayList<>(); + iterateRegions((rX, rZ) -> regions.add(new int[]{rX, rZ})); + + List> regionChunks = new ArrayList<>(); + for (int[] region : regions) { + List chunks = new ArrayList<>(); + iterateChunks(region[0], region[1], (cx, cz) -> chunks.add(new int[]{region[0], region[1], cx, cz})); + if (!chunks.isEmpty()) { + regionChunks.add(chunks); + } + } + + int[] indices = new int[regionChunks.size()]; + boolean anyRemaining = true; + while (anyRemaining) { + anyRemaining = false; + for (int r = 0; r < regionChunks.size(); r++) { + List chunks = regionChunks.get(r); + int idx = indices[r]; + if (idx >= chunks.size()) { + continue; + } + anyRemaining = true; + int[] entry = chunks.get(idx); + boolean first = idx == 0; + boolean last = idx == chunks.size() - 1; + indices[r]++; + if (!consumer.on(entry[0], entry[1], entry[2], entry[3], first, last)) { + return; + } + } + } + } + + private class Bounds { + private Bound chunk = null; + private Bound region = null; + + /** + * Saturating block bounds. center +/- radius is int arithmetic that wraps for far-out centers or huge + * radii, and a wrapped bound silently inverts min/max so every check() fails and the job pregenerates + * nothing. Clamp in long space instead. + */ + public void update() { + int maxX = clampBlock((long) center.getX() + radiusX); + int maxZ = clampBlock((long) center.getZ() + radiusZ); + int minX = clampBlock((long) center.getX() - radiusX); + int minZ = clampBlock((long) center.getZ() - radiusZ); + + chunk = new Bound( + PowerOfTwoCoordinates.blockToChunkFloor(minX), + PowerOfTwoCoordinates.blockToChunkFloor(minZ), + PowerOfTwoCoordinates.ceilDivPow2(maxX, PowerOfTwoCoordinates.CHUNK_BITS), + PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.CHUNK_BITS) + ); + region = new Bound( + PowerOfTwoCoordinates.blockToRegionFloor(minX), + PowerOfTwoCoordinates.blockToRegionFloor(minZ), + PowerOfTwoCoordinates.ceilDivPow2(maxX, PowerOfTwoCoordinates.REGION_BITS), + PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.REGION_BITS) + ); + requireSaneSpan(region); + } + + /** + * A clamped bound is ordered but can still be absurd. Refuse it here instead of handing the spiral a + * span no run could ever finish. + */ + private void requireSaneSpan(Bound region) { + if (region.sizeX() > MAX_REGION_SPAN || region.sizeZ() > MAX_REGION_SPAN) { + throw new IllegalArgumentException("Pregen area is larger than a Minecraft world: center " + + center.getX() + "," + center.getZ() + " radius " + radiusX + "x" + radiusZ + + " blocks spans " + region.sizeX() + "x" + region.sizeZ() + " regions, limit " + + MAX_REGION_SPAN + "."); + } + } + + public Bound chunk() { + if (chunk == null) update(); + return chunk; + } + + public Bound region() { + if (region == null) update(); + return region; + } + } + + static int clampBlock(long block) { + if (block > MAX_BLOCK) { + return MAX_BLOCK; + } + return block < MIN_BLOCK ? MIN_BLOCK : (int) block; + } + + private record Bound(int minX, int minZ, int maxX, int maxZ, int sizeX, int sizeZ) { + private Bound(int minX, int minZ, int maxX, int maxZ) { + this(minX, minZ, maxX, maxZ, maxX - minX + 1, maxZ - minZ + 1); + } + + boolean check(int x, int z) { + return x >= minX && x <= maxX && z >= minZ && z <= maxZ; + } + } + + private static class ProxiedPos extends Position2 { + public ProxiedPos(Position2 p) { + super(p.getX(), p.getZ()); + } + + @Override + public void setX(int x) { + throw new IllegalStateException("This Position2 may not be modified"); + } + + @Override + public void setZ(int z) { + throw new IllegalStateException("This Position2 may not be modified"); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/PregeneratorMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregeneratorMethod.java similarity index 87% rename from core/src/main/java/com/volmit/iris/core/pregenerator/PregeneratorMethod.java rename to core/src/main/java/art/arcane/iris/core/pregenerator/PregeneratorMethod.java index 282f2d2c1..e6ec6ad6f 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/PregeneratorMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregeneratorMethod.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.pregenerator; +package art.arcane.iris.core.pregenerator; -import com.volmit.iris.util.mantle.Mantle; +import art.arcane.volmlib.util.mantle.runtime.Mantle; /** * Represents something that is capable of generating in chunks or regions, or both @@ -58,6 +58,10 @@ public interface PregeneratorMethod { */ String getMethod(int x, int z); + default boolean isAsyncChunkMode() { + return false; + } + /** * Called to generate a region. Execute sync, if multicore internally, wait * for the task to complete @@ -76,5 +80,11 @@ public interface PregeneratorMethod { */ void generateChunk(int x, int z, PregenListener listener); + default void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + } + + default void onRegionSubmitted(int regionX, int regionZ) { + } + Mantle getMantle(); } diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCache.java b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCache.java similarity index 88% rename from core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCache.java rename to core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCache.java index e085975c2..9170ffd2a 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCache.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCache.java @@ -1,7 +1,7 @@ -package com.volmit.iris.core.pregenerator.cache; +package art.arcane.iris.core.pregenerator.cache; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.documentation.RegionCoordinates; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.documentation.RegionCoordinates; import java.io.File; diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java new file mode 100644 index 000000000..f89b9ec23 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java @@ -0,0 +1,429 @@ +package art.arcane.iris.core.pregenerator.cache; + +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.volmlib.util.data.Varint; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.documentation.RegionCoordinates; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import net.jpountz.lz4.LZ4BlockInputStream; +import net.jpountz.lz4.LZ4BlockOutputStream; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +public class PregenCacheImpl implements PregenCache { + // Daemon threads: this static pool is never shut down, and non-daemon threads here block JVM exit + // (test workers and integrated servers) the moment this class is initialized. + private static final ExecutorService DISPATCHER = Executors.newFixedThreadPool(4, runnable -> { + Thread thread = new Thread(runnable, "Iris Pregen Cache Dispatcher"); + thread.setDaemon(true); + return thread; + }); + private static final AtomicBoolean DISPATCHER_REGISTERED = new AtomicBoolean(); + private static final short SIZE = 1024; + + private final File directory; + private final int maxSize; + private final AtomicBoolean missingStorageReported = new AtomicBoolean(); + private final Long2ObjectLinkedOpenHashMap cache = new Long2ObjectLinkedOpenHashMap<>(); + + public PregenCacheImpl(File directory, int maxSize) { + this.directory = directory; + this.maxSize = maxSize; + PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class); + if (preservation != null && DISPATCHER_REGISTERED.compareAndSet(false, true)) { + preservation.register(DISPATCHER); + } + } + + @Override + @ChunkCoordinates + public boolean isChunkCached(int x, int z) { + return getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 10), PowerOfTwoCoordinates.floorDivPow2(z, 10)).isCached( + PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS), + PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(z, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS), + region -> region.isCached( + PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS), + PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS) + ) + ); + } + + @Override + @RegionCoordinates + public boolean isRegionCached(int x, int z) { + return getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.floorDivPow2(z, 5)).isCached( + PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS), + PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS), + Region::isCached + ); + } + + @Override + @ChunkCoordinates + public void cacheChunk(int x, int z) { + getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 10), PowerOfTwoCoordinates.floorDivPow2(z, 10)).cache( + PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS), + PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(z, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS), + region -> region.cache( + PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS), + PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS) + ) + ); + } + + @Override + @RegionCoordinates + public void cacheRegion(int x, int z) { + getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.floorDivPow2(z, 5)).cache( + PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS), + PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS), + Region::cache + ); + } + + @Override + public void write() { + if (cache.isEmpty()) { + return; + } + + List> futures = new ArrayList<>(cache.size()); + for (Plate plate : cache.values()) { + if (!plate.dirty) { + continue; + } + futures.add(CompletableFuture.runAsync(() -> writePlate(plate), DISPATCHER)); + } + + for (CompletableFuture future : futures) { + future.join(); + } + } + + @Override + public void trim(long unloadDuration) { + if (cache.isEmpty()) { + return; + } + + long threshold = System.currentTimeMillis() - unloadDuration; + List> futures = new ArrayList<>(cache.size()); + Iterator iterator = cache.values().iterator(); + while (iterator.hasNext()) { + Plate plate = iterator.next(); + if (plate.lastAccess < threshold) { + iterator.remove(); + } + futures.add(CompletableFuture.runAsync(() -> writePlate(plate), DISPATCHER)); + } + + for (CompletableFuture future : futures) { + future.join(); + } + } + + private Plate getPlate(int x, int z) { + long key = key(x, z); + Plate plate = cache.getAndMoveToFirst(key); + if (plate != null) { + return plate; + } + + Plate loaded = readPlate(x, z); + cache.putAndMoveToFirst(key, loaded); + + if (cache.size() > maxSize) { + List> futures = new ArrayList<>(cache.size() - maxSize); + while (cache.size() > maxSize) { + Plate evicted = cache.removeLast(); + futures.add(CompletableFuture.runAsync(() -> writePlate(evicted), DISPATCHER)); + } + for (CompletableFuture future : futures) { + future.join(); + } + } + + return loaded; + } + + private Plate readPlate(int x, int z) { + File file = fileForPlate(x, z); + if (!file.exists()) { + return new Plate(x, z); + } + + try (DataInputStream input = new DataInputStream(new LZ4BlockInputStream(new FileInputStream(file)))) { + return readPlate(x, z, input); + } catch (IOException e) { + IrisLogging.error("Failed to read pregen cache " + file); + e.printStackTrace(); + IrisLogging.reportError(e); + } + + return new Plate(x, z); + } + + private void writePlate(Plate plate) { + if (!plate.dirty) { + return; + } + if (!prepareCacheDirectory()) { + return; + } + + File file = null; + try { + file = fileForPlate(plate.x, plate.z); + IO.write(file, output -> new DataOutputStream(new LZ4BlockOutputStream(output)), plate::write); + plate.dirty = false; + } catch (Throwable e) { + IrisLogging.error("Failed to write pregen cache " + (file != null ? file : "c." + plate.x + "." + plate.z)); + e.printStackTrace(); + IrisLogging.reportError(e); + } + } + + /** + * Creates the cache directory under an existing parent only. This is a cache: when the storage it lives + * in has been deleted the plate is dropped rather than rebuilt, so a trim cannot resurrect a world tree. + */ + private boolean prepareCacheDirectory() { + if (directory.isDirectory()) { + return true; + } + File parent = directory.getParentFile(); + if (parent == null || !parent.isDirectory()) { + if (missingStorageReported.compareAndSet(false, true)) { + IrisLogging.warn("Pregen cache storage is gone at %s; cached plates are being dropped.", + directory.getAbsolutePath()); + } + return false; + } + if (!directory.mkdirs() && !directory.isDirectory()) { + throw new IllegalStateException("Cannot create directory: " + directory.getAbsolutePath()); + } + return true; + } + + private File fileForPlate(int x, int z) { + return new File(directory, "c." + x + "." + z + ".lz4b"); + } + + private static long key(int x, int z) { + return (((long) x) << 32) ^ (z & 0xffffffffL); + } + + private interface RegionPredicate { + boolean test(Region region); + } + + private enum CacheResult { + NOOP, + SET, + COMPLETED + } + + private interface RegionCacheOp { + CacheResult apply(Region region); + } + + private static class Plate { + private final int x; + private final int z; + private short count; + private Region[] regions; + private boolean dirty; + private long lastAccess; + + private Plate(int x, int z) { + this(x, z, (short) 0, new Region[1024]); + } + + private Plate(int x, int z, short count, Region[] regions) { + this.x = x; + this.z = z; + this.count = count; + this.regions = regions; + this.lastAccess = System.currentTimeMillis(); + } + + private boolean cache(int x, int z, RegionCacheOp op) { + lastAccess = System.currentTimeMillis(); + if (count == SIZE) { + return false; + } + + int index = PowerOfTwoCoordinates.packLocal32(x, z); + Region region = regions[index]; + if (region == null) { + region = new Region(); + regions[index] = region; + } + + CacheResult result = op.apply(region); + if (result == CacheResult.NOOP) { + return false; + } + + dirty = true; + if (result != CacheResult.COMPLETED) { + return false; + } + + count++; + if (count == SIZE) { + regions = null; + } + return true; + } + + private boolean isCached(int x, int z, RegionPredicate predicate) { + lastAccess = System.currentTimeMillis(); + if (count == SIZE) { + return true; + } + + Region region = regions[PowerOfTwoCoordinates.packLocal32(x, z)]; + if (region == null) { + return false; + } + return predicate.test(region); + } + + private void write(DataOutput output) throws IOException { + Varint.writeSignedVarInt(count, output); + if (regions == null) { + return; + } + + for (Region region : regions) { + output.writeBoolean(region == null); + if (region != null) { + region.write(output); + } + } + } + } + + private static class Region { + private short count; + private long[] words; + + private Region() { + this((short) 0, new long[64]); + } + + private Region(short count, long[] words) { + this.count = count; + this.words = words; + } + + private CacheResult cache() { + if (count == SIZE) { + return CacheResult.NOOP; + } + count = SIZE; + words = null; + return CacheResult.COMPLETED; + } + + private CacheResult cache(int x, int z) { + if (count == SIZE) { + return CacheResult.NOOP; + } + + long[] value = words; + if (value == null) { + return CacheResult.NOOP; + } + + int index = PowerOfTwoCoordinates.packLocal32(x, z); + int wordIndex = index >> 6; + long bit = 1L << (index & 63); + boolean current = (value[wordIndex] & bit) != 0L; + if (current) { + return CacheResult.NOOP; + } + + count++; + if (count == SIZE) { + words = null; + return CacheResult.COMPLETED; + } + + value[wordIndex] = value[wordIndex] | bit; + return CacheResult.SET; + } + + private boolean isCached() { + return count == SIZE; + } + + private boolean isCached(int x, int z) { + int index = PowerOfTwoCoordinates.packLocal32(x, z); + if (count == SIZE) { + return true; + } + return (words[index >> 6] & (1L << (index & 63))) != 0L; + } + + private void write(DataOutput output) throws IOException { + Varint.writeSignedVarInt(count, output); + if (words == null) { + return; + } + + for (long word : words) { + Varint.writeUnsignedVarLong(word, output); + } + } + } + + private static Plate readPlate(int x, int z, DataInput input) throws IOException { + int count = Varint.readSignedVarInt(input); + if (count == 1024) { + return new Plate(x, z, SIZE, null); + } + + Region[] regions = new Region[1024]; + for (int i = 0; i < regions.length; i++) { + boolean isNull = input.readBoolean(); + if (!isNull) { + regions[i] = readRegion(input); + } + } + + return new Plate(x, z, (short) count, regions); + } + + private static Region readRegion(DataInput input) throws IOException { + int count = Varint.readSignedVarInt(input); + if (count == 1024) { + return new Region(SIZE, null); + } + + long[] words = new long[64]; + for (int i = 0; i < words.length; i++) { + words[i] = Varint.readUnsignedVarLong(input); + } + + return new Region((short) count, words); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/SynchronizedCache.java b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/SynchronizedCache.java similarity index 95% rename from core/src/main/java/com/volmit/iris/core/pregenerator/cache/SynchronizedCache.java rename to core/src/main/java/art/arcane/iris/core/pregenerator/cache/SynchronizedCache.java index 18efefa43..2605ada76 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/SynchronizedCache.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/SynchronizedCache.java @@ -1,4 +1,4 @@ -package com.volmit.iris.core.pregenerator.cache; +package art.arcane.iris.core.pregenerator.cache; record SynchronizedCache(PregenCache cache) implements PregenCache { @Override diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java new file mode 100644 index 000000000..b87a366bc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java @@ -0,0 +1,99 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator.methods; + +import art.arcane.iris.core.pregenerator.PregenListener; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import io.papermc.lib.PaperLib; +import org.bukkit.World; + +public class AsyncOrMedievalPregenMethod implements PregeneratorMethod { + private final PregeneratorMethod method; + + public AsyncOrMedievalPregenMethod(World world, int threads) { + if (PaperLib.isPaper()) { + method = new AsyncPregenMethod(world, threads); + } else { + method = new MedievalPregenMethod(world); + } + } + + private AsyncOrMedievalPregenMethod(PregeneratorMethod method) { + this.method = method; + } + + public static AsyncOrMedievalPregenMethod strictSerial(World world) { + if (!PaperLib.isPaper()) { + throw new UnsupportedOperationException("Strict serial pregeneration requires Paper or a Paper-compatible server."); + } + + return new AsyncOrMedievalPregenMethod(AsyncPregenMethod.strictSerial(world)); + } + + @Override + public void init() { + method.init(); + } + + @Override + public void close() { + method.close(); + } + + @Override + public void save() { + method.save(); + } + + @Override + public String getMethod(int x, int z) { + return method.getMethod(x, z); + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return false; + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + method.generateChunk(x, z, listener); + } + + @Override + public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ); + } + + @Override + public void onRegionSubmitted(int regionX, int regionZ) { + method.onRegionSubmitted(regionX, regionZ); + } + + @Override + public Mantle getMantle() { + return method.getMantle(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java new file mode 100644 index 000000000..a56aa24a7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -0,0 +1,1104 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator.methods; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisPaperLikeBackendMode; +import art.arcane.iris.core.IrisRuntimeSchedulerMode; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.pregenerator.PregenListener; +import art.arcane.iris.core.pregenerator.PregenMantleBackpressure; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.math.M; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.scheduling.J; +import io.papermc.lib.PaperLib; +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class AsyncPregenMethod implements PregeneratorMethod { + // THREAD_COUNT records the pool's original size while boosted; BOOST_HOLDERS counts live + // jobs holding the boost. Pairing them per instance stops a dying job (whose close can + // land minutes after its replacement started) from shrinking the pool under the live job. + private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); + private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger(); + private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3; + private static final int ADAPTIVE_RECOVERY_INTERVAL = 8; + private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L; + private static final long FLUSH_TIMEOUT_SECONDS = 120L; + private final World world; + private final IrisRuntimeSchedulerMode runtimeSchedulerMode; + private final IrisPaperLikeBackendMode paperLikeBackendMode; + private final boolean foliaRuntime; + private final String backendMode; + private final int workerPoolThreads; + private final int runtimeCpuThreads; + private final int effectiveWorkerThreads; + private final int recommendedRuntimeConcurrencyCap; + private final Method directChunkAtAsyncUrgentMethod; + private final Method directChunkAtAsyncMethod; + private final String chunkAccessMode; + private final ChunkRequestExecutor executor; + private final Executor slowRequestExecutor; + private final Semaphore semaphore; + private final int threads; + private final int slowRequestWarningSeconds; + private final int slowRequestWarnIntervalMs; + private final boolean urgent; + private final ConcurrentHashMap regionPending; + private final ConcurrentHashMap> regionChunks; + private final KSet drainedRegions; + private final KSet evictedRegions; + private volatile int evictionWindowRegions; + private volatile int boundsMinRegionX; + private volatile int boundsMinRegionZ; + private volatile int boundsMaxRegionX; + private volatile int boundsMaxRegionZ; + private final AtomicInteger adaptiveInFlightLimit; + private final int adaptiveMinInFlightLimit; + private final AtomicInteger slowRequestStreak = new AtomicInteger(); + private final AtomicLong lastSlowRequestLogAt = new AtomicLong(0L); + private final AtomicLong lastFailedReleaseLogAt = new AtomicLong(0L); + private final AtomicInteger suppressedSlowRequestLogs = new AtomicInteger(); + private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L); + private final AtomicInteger inFlight = new AtomicInteger(); + private final AtomicLong submitted = new AtomicLong(); + private final AtomicLong completed = new AtomicLong(); + private final AtomicLong failed = new AtomicLong(); + private final AtomicLong lastProgressAt = new AtomicLong(M.ms()); + private final AtomicBoolean closing = new AtomicBoolean(); + private final AtomicBoolean holdsWorkerBoost = new AtomicBoolean(); + private final Object permitMonitor = new Object(); + private volatile Engine metricsEngine; + private volatile Mantle cachedMantle; + private final PregenMantleBackpressure backpressure; + + public AsyncPregenMethod(World world, int unusedThreads) { + this(world, false); + } + + private AsyncPregenMethod(World world, boolean strictSerial) { + if (!PaperLib.isPaper()) { + throw new UnsupportedOperationException("Cannot use PaperAsync on non paper!"); + } + + this.world = world; + IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); + this.runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(pregen); + this.foliaRuntime = runtimeSchedulerMode == IrisRuntimeSchedulerMode.FOLIA; + ChunkAsyncMethodSelection chunkAsyncMethodSelection = resolveChunkAsyncMethodSelection(world); + this.directChunkAtAsyncUrgentMethod = chunkAsyncMethodSelection.urgentMethod(); + this.directChunkAtAsyncMethod = chunkAsyncMethodSelection.standardMethod(); + this.chunkAccessMode = chunkAsyncMethodSelection.mode(); + int detectedWorkerPoolThreads = resolveWorkerPoolThreads(); + int detectedCpuThreads = Math.max(1, Runtime.getRuntime().availableProcessors()); + int configuredWorldGenThreads = Math.max(1, IrisSettings.get().getConcurrency().getWorldGenThreads()); + int workerThreadsForCap = foliaRuntime + ? resolveFoliaConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, configuredWorldGenThreads) + : resolvePaperLikeConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, configuredWorldGenThreads); + if (foliaRuntime) { + this.paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO; + this.backendMode = "folia-region"; + this.executor = new FoliaRegionExecutor(); + } else { + this.paperLikeBackendMode = resolvePaperLikeBackendMode(pregen); + if (paperLikeBackendMode == IrisPaperLikeBackendMode.SERVICE) { + this.executor = new ServiceExecutor(); + this.backendMode = "paper-service"; + } else { + this.executor = new TicketExecutor(); + this.backendMode = "paper-ticket"; + } + } + int configuredThreads = foliaRuntime + ? computeFoliaRecommendedCap(workerThreadsForCap) + : computePaperLikeRecommendedCap(workerThreadsForCap); + this.threads = selectConcurrencyCap(configuredThreads, strictSerial); + this.workerPoolThreads = detectedWorkerPoolThreads; + this.runtimeCpuThreads = detectedCpuThreads; + this.effectiveWorkerThreads = workerThreadsForCap; + this.recommendedRuntimeConcurrencyCap = configuredThreads; + this.semaphore = new Semaphore(this.threads, true); + this.slowRequestWarningSeconds = pregen.getChunkLoadTimeoutSeconds(); + this.slowRequestExecutor = CompletableFuture.delayedExecutor(this.slowRequestWarningSeconds, TimeUnit.SECONDS); + this.slowRequestWarnIntervalMs = pregen.getTimeoutWarnIntervalMs(); + this.urgent = false; + this.regionPending = new ConcurrentHashMap<>(); + this.regionChunks = new ConcurrentHashMap<>(); + this.drainedRegions = new KSet<>(); + this.evictedRegions = new KSet<>(); + this.evictionWindowRegions = -1; + this.boundsMinRegionX = Integer.MIN_VALUE; + this.boundsMinRegionZ = Integer.MIN_VALUE; + this.boundsMaxRegionX = Integer.MAX_VALUE; + this.boundsMaxRegionZ = Integer.MAX_VALUE; + this.adaptiveInFlightLimit = new AtomicInteger(this.threads); + this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4))); + int pregenWorldHeight = world.getMaxHeight() - world.getMinHeight(); + this.backpressure = new PregenMantleBackpressure( + this::resolveMantle, + pregen.getEffectiveResidentTectonicPlates(pregenWorldHeight), + pregen.getMantleBackpressureWaitMs(), + pregen.getMantleBackpressureTimeoutMs(), + this::lowerAdaptiveInFlightLimit, + this::metricsSnapshot, + this::isCancelled); + } + + public static AsyncPregenMethod strictSerial(World world) { + return new AsyncPregenMethod(world, true); + } + + private IrisPaperLikeBackendMode resolvePaperLikeBackendMode(IrisSettings.IrisSettingsPregen pregen) { + IrisPaperLikeBackendMode configuredMode = pregen.getPaperLikeBackendMode(); + if (configuredMode != IrisPaperLikeBackendMode.AUTO) { + return configuredMode; + } + + return IrisPaperLikeBackendMode.TICKET; + } + + private int resolveWorkerPoolThreads() { + try { + Class moonriseCommonClass = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon"); + java.lang.reflect.Field workerPoolField = moonriseCommonClass.getDeclaredField("WORKER_POOL"); + Object workerPool = workerPoolField.get(null); + Object coreThreads = workerPool.getClass().getDeclaredMethod("getCoreThreads").invoke(workerPool); + if (coreThreads instanceof Thread[] threadsArray) { + return threadsArray.length; + } + } catch (Throwable ignored) { + } + + return -1; + } + + private static long rkey(int rx, int rz) { + return (((long) rx) << 32) | (rz & 0xFFFFFFFFL); + } + + private int evictionWindow() { + int cached = evictionWindowRegions; + if (cached > 0) { + return cached; + } + + Engine engine = resolveMetricsEngine(); + if (engine == null) { + return 2; + } + + try { + int radius = engine.getMantle().getRadius(); + int resolved = radius > 0 ? Math.max(1, (int) Math.ceil(radius / 32.0)) : 2; + evictionWindowRegions = resolved; + return resolved; + } catch (Throwable ignored) { + return 2; + } + } + + private void onChunkCompleted(int x, int z, Chunk chunk) { + if (chunk == null) { + return; + } + + try { + long rk = rkey(x >> 5, z >> 5); + regionChunks.computeIfAbsent(rk, k -> new ConcurrentLinkedQueue<>()).add(chunk); + AtomicInteger pending = regionPending.get(rk); + if (pending != null && pending.decrementAndGet() == 0) { + onRegionDrained(rk); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private void onChunkFailedToLoad(int x, int z) { + try { + long rk = rkey(x >> 5, z >> 5); + AtomicInteger pending = regionPending.get(rk); + if (pending != null && pending.decrementAndGet() == 0) { + onRegionDrained(rk); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + long now = M.ms(); + long last = lastFailedReleaseLogAt.get(); + if (now - last >= slowRequestWarnIntervalMs && lastFailedReleaseLogAt.compareAndSet(last, now)) { + IrisLogging.warn("Released region slot for failed chunk at " + x + "," + z + ". " + metricsSnapshot()); + } + } + + @Override + public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + boundsMinRegionX = minRegionX; + boundsMinRegionZ = minRegionZ; + boundsMaxRegionX = maxRegionX; + boundsMaxRegionZ = maxRegionZ; + } + + private boolean inBounds(int rx, int rz) { + return rx >= boundsMinRegionX && rx <= boundsMaxRegionX && rz >= boundsMinRegionZ && rz <= boundsMaxRegionZ; + } + + @Override + public void onRegionSubmitted(int regionX, int regionZ) { + try { + long rk = rkey(regionX, regionZ); + AtomicInteger pending = regionPending.get(rk); + if (pending == null || pending.decrementAndGet() == 0) { + onRegionDrained(rk); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private void onRegionDrained(long rk) { + if (!drainedRegions.add(rk)) { + return; + } + + int w = evictionWindow(); + int rx = (int) (rk >> 32); + int rz = (int) rk; + for (int dx = -w; dx <= w; dx++) { + for (int dz = -w; dz <= w; dz++) { + int cx = rx + dx; + int cz = rz + dz; + long candidate = rkey(cx, cz); + if (evictedRegions.contains(candidate)) { + continue; + } + if (!drainedRegions.contains(candidate)) { + continue; + } + if (allNeighborsDrained(cx, cz, w)) { + evictRegion(candidate); + } + } + } + } + + private boolean allNeighborsDrained(int rx, int rz, int w) { + for (int dx = -w; dx <= w; dx++) { + for (int dz = -w; dz <= w; dz++) { + int nx = rx + dx; + int nz = rz + dz; + if (!inBounds(nx, nz)) { + continue; + } + + if (!drainedRegions.contains(rkey(nx, nz))) { + return false; + } + } + } + + return true; + } + + private void evictRegion(long c) { + if (!evictedRegions.add(c)) { + return; + } + + regionPending.remove(c); + Queue chunks = regionChunks.remove(c); + if (chunks == null || chunks.isEmpty()) { + return; + } + + try { + if (foliaRuntime) { + Chunk anchor = null; + for (Chunk chunk : chunks) { + if (chunk == null) { + continue; + } + + if (anchor == null) { + anchor = chunk; + } + + int cx = chunk.getX(); + int cz = chunk.getZ(); + if (!J.runRegion(world, cx, cz, () -> unloadChunkSafely(cx, cz))) { + unloadChunkSafely(cx, cz); + } + } + + if (anchor != null) { + int ax = anchor.getX(); + int az = anchor.getZ(); + if (!J.runRegion(world, ax, az, () -> INMS.get().flushChunkIO(world))) { + INMS.get().flushChunkIO(world); + } + } + return; + } + + J.s(() -> { + for (Chunk chunk : chunks) { + if (chunk != null) { + unloadChunkSafely(chunk.getX(), chunk.getZ()); + } + } + + INMS.get().flushChunkIO(world); + }); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private void unloadChunkSafely(int cx, int cz) { + try { + world.removePluginChunkTicket(cx, cz, BukkitPlatform.plugin()); + } catch (Throwable ignored) { + } + + try { + if (!INMS.get().saveAndUnloadChunk(world, cx, cz)) { + world.unloadChunk(cx, cz, true); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private void flushAllRemainingChunks() { + List keys = new ArrayList<>(regionChunks.keySet()); + + if (foliaRuntime) { + for (Long rk : keys) { + evictRegion(rk); + } + return; + } + + CompletableFuture flush = J.sfut(() -> { + for (Long rk : keys) { + if (!evictedRegions.add(rk)) { + continue; + } + + regionPending.remove(rk); + Queue chunks = regionChunks.remove(rk); + if (chunks == null) { + continue; + } + + for (Chunk chunk : chunks) { + if (chunk != null) { + unloadChunkSafely(chunk.getX(), chunk.getZ()); + } + } + } + + world.save(); + INMS.get().flushChunkIO(world); + }); + + try { + flush.get(FLUSH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + IrisLogging.warn("Interrupted while flushing pregen chunks for " + world.getName() + "."); + } catch (TimeoutException e) { + IrisLogging.warn("Pregen chunk flush for " + world.getName() + " did not finish in " + FLUSH_TIMEOUT_SECONDS + + "s, continuing shutdown. " + keys.size() + " region(s) queued."); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) { + try { + IrisLogging.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot()); + + IrisLogging.reportError(throwable); + } catch (Throwable e) { + e.printStackTrace(); + } + + return null; + } + + private void completeChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) { + boolean success = false; + try { + if (throwable != null) { + onChunkFutureFailure(x, z, throwable); + onChunkFailedToLoad(x, z); + listener.onChunkFailed(x, z); + } else if (chunk == null) { + onChunkFailedToLoad(x, z); + listener.onChunkFailed(x, z); + } else { + listener.onChunkGenerated(x, z); + cleanupMantleChunk(x, z); + listener.onChunkCleaned(x, z); + onChunkCompleted(x, z, chunk); + success = true; + } + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } finally { + try { + markFinished(success); + } finally { + semaphore.release(); + } + } + } + + private void onSlowRequest(int x, int z) { + if (closing.get()) { + return; + } + + int streak = slowRequestStreak.incrementAndGet(); + if (streak % ADAPTIVE_SLOW_REQUEST_STEP == 0) { + lowerAdaptiveInFlightLimit(); + } + + long now = M.ms(); + long last = lastSlowRequestLogAt.get(); + if (now - last < slowRequestWarnIntervalMs || !lastSlowRequestLogAt.compareAndSet(last, now)) { + suppressedSlowRequestLogs.incrementAndGet(); + return; + } + + int suppressed = suppressedSlowRequestLogs.getAndSet(0); + String suppressedText = suppressed <= 0 ? "" : " suppressed=" + suppressed; + // Slow chunk loads are what the adaptive in flight limit exists to absorb, and this line is already + // interval throttled with a suppression count. It reports the throttle working, not a fault. + IrisLogging.info("Async pregen chunk load at " + x + "," + z + + " is still pending after " + slowRequestWarningSeconds + "s." + + " adaptiveLimit=" + adaptiveInFlightLimit.get() + + suppressedText + " " + metricsSnapshot()); + } + + private void onSuccess() { + int streak = slowRequestStreak.get(); + if (streak > 0) { + int newStreak = Math.max(0, streak - 2); + slowRequestStreak.compareAndSet(streak, newStreak); + if (newStreak > 0) { + return; + } + } + + if ((completed.get() & (ADAPTIVE_RECOVERY_INTERVAL - 1)) == 0L) { + raiseAdaptiveInFlightLimit(); + } + } + + private void lowerAdaptiveInFlightLimit() { + while (true) { + int current = adaptiveInFlightLimit.get(); + if (current <= adaptiveMinInFlightLimit) { + return; + } + + int next = Math.max(adaptiveMinInFlightLimit, current - 1); + if (adaptiveInFlightLimit.compareAndSet(current, next)) { + logAdaptiveLimit("decrease", next); + notifyPermitWaiters(); + return; + } + } + } + + private void raiseAdaptiveInFlightLimit() { + while (true) { + int current = adaptiveInFlightLimit.get(); + if (current >= threads) { + return; + } + + int deficit = threads - current; + int step = deficit > (threads / 2) ? Math.max(2, threads / 8) : 1; + int next = Math.min(threads, current + step); + if (adaptiveInFlightLimit.compareAndSet(current, next)) { + logAdaptiveLimit("increase", next); + notifyPermitWaiters(); + return; + } + } + } + + private void logAdaptiveLimit(String mode, int value) { + long now = M.ms(); + long last = lastAdaptiveLogAt.get(); + if (now - last < 5000L) { + return; + } + + if (lastAdaptiveLogAt.compareAndSet(last, now)) { + IrisLogging.info("Async pregen adaptive limit " + mode + " -> " + value + " " + metricsSnapshot()); + } + } + + static int computePaperLikeRecommendedCap(int workerThreads) { + int normalizedWorkers = Math.max(1, workerThreads); + int recommendedCap = normalizedWorkers * 8; + if (recommendedCap < 16) { + return 16; + } + + if (recommendedCap > 128) { + return 128; + } + + return recommendedCap; + } + + static int selectConcurrencyCap(int recommendedCap, boolean strictSerial) { + return strictSerial ? 1 : Math.max(1, recommendedCap); + } + + static int resolvePaperLikeConcurrencyWorkerThreads(int detectedWorkerPoolThreads, int detectedCpuThreads, int configuredWorldGenThreads) { + int provisionedWorkerThreads = Math.max(1, configuredWorldGenThreads); + if (detectedWorkerPoolThreads > 0) { + return Math.max(detectedWorkerPoolThreads, provisionedWorkerThreads); + } + + return Math.max(provisionedWorkerThreads, detectedCpuThreads); + } + + static CompletableFuture observeSlowRequest(CompletableFuture future, Executor executor, Runnable onSlowRequest) { + executor.execute(() -> { + if (!future.isDone()) { + onSlowRequest.run(); + } + }); + return future; + } + + static int computeFoliaRecommendedCap(int workerThreads) { + int normalizedWorkers = Math.max(1, workerThreads); + int recommendedCap = normalizedWorkers * 8; + if (recommendedCap < 64) { + return 64; + } + + if (recommendedCap > 192) { + return 192; + } + + return recommendedCap; + } + + static int resolveFoliaConcurrencyWorkerThreads(int detectedWorkerPoolThreads, int detectedCpuThreads, int configuredWorldGenThreads) { + return Math.max(detectedCpuThreads, Math.max(configuredWorldGenThreads, Math.max(1, detectedWorkerPoolThreads))); + } + + private String metricsSnapshot() { + long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get()); + return "world=" + world.getName() + + " permits=" + semaphore.availablePermits() + "/" + threads + + " adaptiveLimit=" + adaptiveInFlightLimit.get() + + " inFlight=" + inFlight.get() + + " submitted=" + submitted.get() + + " completed=" + completed.get() + + " failed=" + failed.get() + + " stalledForMs=" + stalledFor; + } + + private void markSubmitted() { + submitted.incrementAndGet(); + inFlight.incrementAndGet(); + } + + private void markFinished(boolean success) { + if (success) { + completed.incrementAndGet(); + onSuccess(); + } else { + failed.incrementAndGet(); + } + + lastProgressAt.set(M.ms()); + int after = inFlight.decrementAndGet(); + if (after < 0) { + inFlight.compareAndSet(after, 0); + } + notifyPermitWaiters(); + } + + private void notifyPermitWaiters() { + synchronized (permitMonitor) { + permitMonitor.notifyAll(); + } + } + + private void recordAdaptiveWait(long waitedMs) { + Engine engine = resolveMetricsEngine(); + if (engine != null) { + engine.getMetrics().getPregenWaitAdaptive().put(waitedMs); + } + } + + private void recordPermitWait(long waitedMs) { + Engine engine = resolveMetricsEngine(); + if (engine != null) { + engine.getMetrics().getPregenWaitPermit().put(waitedMs); + } + } + + private void cleanupMantleChunk(int x, int z) { + Engine engine = resolveMetricsEngine(); + if (engine != null) { + try { + engine.getMantle().forceCleanupChunk(x, z); + } catch (Throwable ignored) { + } + } + } + + private Engine resolveMetricsEngine() { + Engine cachedEngine = metricsEngine; + if (cachedEngine != null) { + return cachedEngine; + } + + if (!IrisToolbelt.isIrisWorld(world)) { + return null; + } + + try { + Engine resolvedEngine = IrisToolbelt.access(world).getEngine(); + if (resolvedEngine != null) { + metricsEngine = resolvedEngine; + } + return resolvedEngine; + } catch (Throwable ignored) { + return null; + } + } + + private Mantle resolveMantle() { + Mantle cached = cachedMantle; + if (cached != null) { + return cached; + } + + Mantle resolved = getMantle(); + if (resolved != null) { + cachedMantle = resolved; + } + return resolved; + } + + @Override + public void init() { + IrisLogging.info("Async pregen init: world=" + world.getName() + + ", mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT) + + ", backend=" + backendMode + + ", chunkAccess=" + chunkAccessMode + + ", threads=" + threads + + ", adaptiveLimit=" + adaptiveInFlightLimit.get() + + ", workerPoolThreads=" + workerPoolThreads + + ", cpuThreads=" + runtimeCpuThreads + + ", effectiveWorkerThreads=" + effectiveWorkerThreads + + ", recommendedCap=" + recommendedRuntimeConcurrencyCap + + ", urgent=" + urgent + + ", slowWarning=" + slowRequestWarningSeconds + "s"); + if (workerPoolThreads > 0 && holdsWorkerBoost.compareAndSet(false, true)) { + acquireWorkerThreadBoost(); + } + } + + @Override + public String getMethod(int x, int z) { + return "Async"; + } + + @Override + public boolean isAsyncChunkMode() { + return true; + } + + @Override + public void close() { + closing.set(true); + notifyPermitWaiters(); + + // A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk. + boolean interrupted = Thread.interrupted(); + try { + boolean drained = false; + try { + drained = semaphore.tryAcquire(threads, CLOSE_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + interrupted = true; + } + + if (!drained) { + IrisLogging.warn("Async pregen close did not drain in " + CLOSE_DRAIN_TIMEOUT_SECONDS + + "s, continuing degraded. " + metricsSnapshot()); + } + + flushAllRemainingChunks(); + executor.shutdown(); + if (holdsWorkerBoost.compareAndSet(true, false)) { + releaseWorkerThreadBoost(); + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + private boolean isCancelled() { + return closing.get() || Thread.currentThread().isInterrupted(); + } + + @Override + public void save() { + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return false; + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + listener.onChunkGenerating(x, z); + backpressure.enforceMantleBudget(); + backpressure.awaitHeapHeadroom(); + if (isCancelled()) { + return; + } + + try { + long waitStart = M.ms(); + synchronized (permitMonitor) { + while (inFlight.get() >= adaptiveInFlightLimit.get()) { + if (isCancelled()) { + return; + } + + permitMonitor.wait(500L); + } + } + long adaptiveWait = Math.max(0L, M.ms() - waitStart); + if (adaptiveWait > 0L) { + recordAdaptiveWait(adaptiveWait); + } + + long permitWaitStart = M.ms(); + while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) { + if (isCancelled()) { + return; + } + } + long permitWait = Math.max(0L, M.ms() - permitWaitStart); + if (permitWait > 0L) { + recordPermitWait(permitWait); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + regionPending.computeIfAbsent(rkey(x >> 5, z >> 5), k -> new AtomicInteger(1)).incrementAndGet(); + markSubmitted(); + executor.generate(x, z, listener); + } + + private CompletableFuture requestChunkAsync(int x, int z) { + Throwable failure = null; + + if (directChunkAtAsyncUrgentMethod != null) { + try { + return invokeChunkFuture(directChunkAtAsyncUrgentMethod, x, z, true, urgent); + } catch (Throwable e) { + failure = e; + } + } + + if (directChunkAtAsyncMethod != null) { + try { + return invokeChunkFuture(directChunkAtAsyncMethod, x, z, true, urgent); + } catch (Throwable e) { + if (failure == null) { + failure = e; + } + } + } + + try { + CompletableFuture future = PaperLib.getChunkAtAsync(world, x, z, true, urgent); + if (future != null) { + return future; + } + } catch (Throwable e) { + if (failure == null) { + failure = e; + } + } + + if (failure == null) { + failure = new IllegalStateException("Chunk async access returned no future."); + } + + return CompletableFuture.failedFuture(new IllegalStateException("Failed to request async chunk " + x + "," + z + " in world " + world.getName(), failure)); + } + + private CompletableFuture requestMonitoredChunkAsync(int x, int z) { + return observeSlowRequest(requestChunkAsync(x, z), slowRequestExecutor, () -> onSlowRequest(x, z)); + } + + @SuppressWarnings("unchecked") + private CompletableFuture invokeChunkFuture(Method method, int x, int z, boolean generate, boolean urgentRequest) throws Throwable { + Object result; + try { + if (method.getParameterCount() == 4) { + result = method.invoke(world, x, z, generate, urgentRequest); + } else { + result = method.invoke(world, x, z, generate); + } + } catch (InvocationTargetException e) { + throw e.getCause() == null ? e : e.getCause(); + } + + if (result instanceof CompletableFuture) { + return (CompletableFuture) result; + } + + throw new IllegalStateException("Chunk async method returned a non-future result."); + } + + private static ChunkAsyncMethodSelection resolveChunkAsyncMethodSelection(World world) { + if (world == null) { + return new ChunkAsyncMethodSelection(null, null, "paperlib"); + } + + Class worldClass = world.getClass(); + Method urgentMethod = resolveChunkAsyncMethod(worldClass, int.class, int.class, boolean.class, boolean.class); + Method standardMethod = resolveChunkAsyncMethod(worldClass, int.class, int.class, boolean.class); + if (urgentMethod != null) { + return new ChunkAsyncMethodSelection(urgentMethod, standardMethod, "world#getChunkAtAsync(int,int,boolean,boolean)"); + } + if (standardMethod != null) { + return new ChunkAsyncMethodSelection(null, standardMethod, "world#getChunkAtAsync(int,int,boolean)"); + } + return new ChunkAsyncMethodSelection(null, null, "paperlib"); + } + + private static Method resolveChunkAsyncMethod(Class worldClass, Class... parameterTypes) { + try { + return worldClass.getMethod("getChunkAtAsync", parameterTypes); + } catch (NoSuchMethodException ignored) { + } + + try { + return World.class.getMethod("getChunkAtAsync", parameterTypes); + } catch (NoSuchMethodException ignored) { + } + + return null; + } + + @Override + public Mantle getMantle() { + if (IrisToolbelt.isIrisWorld(world)) { + return IrisToolbelt.access(world).getEngine().getMantle().getMantle(); + } + + return null; + } + + public static void acquireWorkerThreadBoost() { + if (BOOST_HOLDERS.getAndIncrement() > 0) { + return; + } + increaseWorkerThreads(); + } + + public static void releaseWorkerThreadBoost() { + if (BOOST_HOLDERS.decrementAndGet() > 0) { + return; + } + resetWorkerThreads(); + } + + private static void increaseWorkerThreads() { + THREAD_COUNT.updateAndGet(i -> { + if (i > 0) { + return i; + } + + int adjusted = IrisSettings.get().getConcurrency().getWorldGenThreads(); + try { + Field field = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon").getDeclaredField("WORKER_POOL"); + Object pool = field.get(null); + int threads = ((Thread[]) pool.getClass().getDeclaredMethod("getCoreThreads").invoke(pool)).length; + if (threads >= adjusted) { + return 0; + } + + pool.getClass().getDeclaredMethod("adjustThreadCount", int.class).invoke(pool, adjusted); + return threads; + } catch (Throwable e) { + IrisLogging.warn("Failed to increase worker threads, if you are on paper or a fork of it please increase it manually to " + adjusted); + IrisLogging.warn("For more information see https://docs.papermc.io/paper/reference/global-configuration#chunk_system_worker_threads"); + if (e instanceof InvocationTargetException) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + return 0; + }); + } + + private static void resetWorkerThreads() { + THREAD_COUNT.updateAndGet(i -> { + if (i == 0) { + return 0; + } + + try { + Field field = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon").getDeclaredField("WORKER_POOL"); + Object pool = field.get(null); + Method method = pool.getClass().getDeclaredMethod("adjustThreadCount", int.class); + method.invoke(pool, i); + return 0; + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Failed to reset worker threads"); + e.printStackTrace(); + } + return i; + }); + } + + private interface ChunkRequestExecutor { + void generate(int x, int z, PregenListener listener); + default void shutdown() {} + } + + private class FoliaRegionExecutor implements ChunkRequestExecutor { + @Override + public void generate(int x, int z, PregenListener listener) { + try { + requestMonitoredChunkAsync(x, z) + .whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable)); + return; + } catch (Throwable ignored) { + } + + Runnable regionTask = () -> { + try { + requestMonitoredChunkAsync(x, z) + .whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable)); + } catch (Throwable e) { + completeChunk(x, z, listener, null, e); + } + }; + + if (!J.runRegion(world, x, z, regionTask)) { + completeChunk(x, z, listener, null, + new IllegalStateException("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot())); + } + } + } + + private class ServiceExecutor implements ChunkRequestExecutor { + private final ExecutorService service = new MultiBurst("Iris Async Pregen"); + + public void generate(int x, int z, PregenListener listener) { + try { + service.submit(() -> { + try { + Chunk i = requestMonitoredChunkAsync(x, z).get(); + completeChunk(x, z, listener, i, null); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + completeChunk(x, z, listener, null, e); + } catch (Throwable e) { + completeChunk(x, z, listener, null, e); + } + }); + } catch (Throwable e) { + completeChunk(x, z, listener, null, e); + } + } + + @Override + public void shutdown() { + service.shutdown(); + } + } + + private class TicketExecutor implements ChunkRequestExecutor { + @Override + public void generate(int x, int z, PregenListener listener) { + try { + requestMonitoredChunkAsync(x, z) + .whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable)); + } catch (Throwable e) { + completeChunk(x, z, listener, null, e); + } + } + } + + private record ChunkAsyncMethodSelection(Method urgentMethod, Method standardMethod, String mode) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java new file mode 100644 index 000000000..6fe0f860e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java @@ -0,0 +1,187 @@ +package art.arcane.iris.core.pregenerator.methods; + +import art.arcane.iris.core.pregenerator.PregenListener; +import art.arcane.iris.core.pregenerator.PregenTask; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.core.pregenerator.cache.PregenCache; +import art.arcane.volmlib.util.mantle.runtime.Mantle; + +public class CachedPregenMethod implements PregeneratorMethod { + private final PregeneratorMethod method; + private final PregenCache cache; + private final PregenTask task; + private volatile PregenListener wrappedSource; + private volatile PregenListener wrappedListener; + + public CachedPregenMethod(PregeneratorMethod method, PregenCache cache, PregenTask task) { + this.method = method; + this.cache = cache.sync(); + this.task = task; + } + + @Override + public void init() { + method.init(); + } + + @Override + public void close() { + cache.write(); + method.close(); + cache.write(); + } + + @Override + public void save() { + method.save(); + cache.write(); + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return cache.isRegionCached(x, z) || method.supportsRegions(x, z, listener); + } + + @Override + public String getMethod(int x, int z) { + if (cache.isRegionCached(x, z)) { + return "Cached"; + } + return method.getMethod(x, z); + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + if (cache.isRegionCached(x, z)) { + listener.onRegionGenerated(x, z); + task.iterateChunks(x, z, (cX, cZ) -> { + listener.onChunkGenerated(cX, cZ, true); + listener.onChunkCleaned(cX, cZ); + }); + return; + } + method.generateRegion(x, z, listener); + cache.cacheRegion(x, z); + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + if (cache.isChunkCached(x, z)) { + listener.onChunkGenerated(x, z, true); + listener.onChunkCleaned(x, z); + return; + } + method.generateChunk(x, z, cachingListener(listener)); + } + + private PregenListener cachingListener(PregenListener listener) { + PregenListener source = wrappedSource; + PregenListener wrapped = wrappedListener; + if (source == listener && wrapped != null) { + return wrapped; + } + + PregenListener created = new PregenListener() { + @Override + public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) { + listener.onTick(chunksPerSecond, chunksPerMinute, regionsPerMinute, percent, generated, totalChunks, chunksRemaining, eta, elapsed, method, cached); + } + + @Override + public void onChunkGenerating(int x, int z) { + listener.onChunkGenerating(x, z); + } + + @Override + public void onChunkGenerated(int x, int z, boolean cachedChunk) { + if (!cachedChunk) { + cache.cacheChunk(x, z); + } + listener.onChunkGenerated(x, z, cachedChunk); + } + + @Override + public void onChunkFailed(int x, int z) { + listener.onChunkFailed(x, z); + } + + @Override + public void onRegionGenerated(int x, int z) { + listener.onRegionGenerated(x, z); + } + + @Override + public void onRegionGenerating(int x, int z) { + listener.onRegionGenerating(x, z); + } + + @Override + public void onChunkCleaned(int x, int z) { + listener.onChunkCleaned(x, z); + } + + @Override + public void onRegionSkipped(int x, int z) { + listener.onRegionSkipped(x, z); + } + + @Override + public void onNetworkStarted(int x, int z) { + listener.onNetworkStarted(x, z); + } + + @Override + public void onNetworkFailed(int x, int z) { + listener.onNetworkFailed(x, z); + } + + @Override + public void onNetworkReclaim(int revert) { + listener.onNetworkReclaim(revert); + } + + @Override + public void onNetworkGeneratedChunk(int x, int z) { + listener.onNetworkGeneratedChunk(x, z); + } + + @Override + public void onNetworkDownloaded(int x, int z) { + listener.onNetworkDownloaded(x, z); + } + + @Override + public void onClose() { + listener.onClose(); + } + + @Override + public void onSaving() { + listener.onSaving(); + } + + @Override + public void onChunkExistsInRegionGen(int x, int z) { + listener.onChunkExistsInRegionGen(x, z); + } + }; + wrappedSource = listener; + wrappedListener = created; + return created; + } + + @Override + public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ); + } + + @Override + public void onRegionSubmitted(int regionX, int regionZ) { + method.onRegionSubmitted(regionX, regionZ); + } + + @Override + public Mantle getMantle() { + return method.getMantle(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/HybridPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/HybridPregenMethod.java new file mode 100644 index 000000000..7e902edc4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/HybridPregenMethod.java @@ -0,0 +1,92 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator.methods; + +import art.arcane.iris.core.pregenerator.PregenListener; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import org.bukkit.World; + +public class HybridPregenMethod implements PregeneratorMethod { + private final PregeneratorMethod inWorld; + private final World world; + + public HybridPregenMethod(World world, int threads) { + this(world, new AsyncOrMedievalPregenMethod(world, threads)); + } + + private HybridPregenMethod(World world, PregeneratorMethod inWorld) { + this.world = world; + this.inWorld = inWorld; + } + + public static HybridPregenMethod strictSerial(World world) { + return new HybridPregenMethod(world, AsyncOrMedievalPregenMethod.strictSerial(world)); + } + + @Override + public String getMethod(int x, int z) { + return "Hybrid<" + inWorld.getMethod(x, z) + ">"; + } + + @Override + public void init() { + inWorld.init(); + } + + @Override + public void close() { + inWorld.close(); + } + + @Override + public void save() { + inWorld.save(); + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return inWorld.supportsRegions(x, z, listener); + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + inWorld.generateRegion(x, z, listener); + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + inWorld.generateChunk(x, z, listener); + } + + @Override + public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + inWorld.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ); + } + + @Override + public void onRegionSubmitted(int regionX, int regionZ) { + inWorld.onRegionSubmitted(regionX, regionZ); + } + + @Override + public Mantle getMantle() { + return inWorld.getMantle(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java new file mode 100644 index 000000000..b2560d3d1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java @@ -0,0 +1,347 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator.methods; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.pregenerator.PregenListener; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.math.M; +import art.arcane.iris.util.common.scheduling.J; +import io.papermc.lib.PaperLib; +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public class MedievalPregenMethod implements PregeneratorMethod { + private static final long CHUNK_WAIT_TIMEOUT_SECONDS = 60L; + private static final long UNLOAD_TIMEOUT_SECONDS = 120L; + private final World world; + private final KList> futures; + private final Map lastUse; + private final int maxFutures; + private final AtomicBoolean directAsyncDisabled; + private final AtomicBoolean prefetchDisabled; + private final ExecutorService prefetchPool; + private volatile Engine cachedEngine; + private volatile boolean engineResolutionAttempted; + + public MedievalPregenMethod(World world) { + this.world = world; + futures = new KList<>(); + this.lastUse = new ConcurrentHashMap<>(); + int configuredThreads = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()); + this.maxFutures = J.isFolia() ? Math.max(2, Math.min(64, configuredThreads)) : Math.max(16, Math.min(128, configuredThreads * 4)); + this.directAsyncDisabled = new AtomicBoolean(false); + this.prefetchDisabled = new AtomicBoolean(J.isFolia()); + int prefetchThreads = J.isFolia() ? 0 : Math.max(2, Math.min(16, configuredThreads)); + this.prefetchPool = prefetchThreads > 0 ? newPrefetchPool(prefetchThreads) : null; + PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class); + if (preservation != null && prefetchPool != null) { + preservation.register(prefetchPool); + } + } + + private static ExecutorService newPrefetchPool(int threads) { + AtomicInteger counter = new AtomicInteger(); + return Executors.newFixedThreadPool(threads, r -> { + Thread t = new Thread(r, "Iris Medieval Prefetch " + counter.incrementAndGet()); + t.setDaemon(true); + return t; + }); + } + + private Engine resolveEngine() { + Engine cached = cachedEngine; + if (cached != null) { + return cached; + } + if (engineResolutionAttempted) { + return null; + } + engineResolutionAttempted = true; + try { + if (!IrisToolbelt.isIrisWorld(world)) { + return null; + } + cached = IrisToolbelt.access(world).getEngine(); + if (cached != null) { + cachedEngine = cached; + } + } catch (Throwable ignored) { + } + return cached; + } + + private void waitForChunks() { + for (CompletableFuture i : futures) { + try { + i.get(CHUNK_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (TimeoutException e) { + IrisLogging.warn("Medieval pregen chunk did not finish in " + CHUNK_WAIT_TIMEOUT_SECONDS + "s, abandoning it."); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + futures.clear(); + } + + private void unloadAndSaveAllChunks(boolean saveWorld) { + if (J.isFolia()) { + lastUse.clear(); + return; + } + + CompletableFuture unload = J.sfut(() -> { + if (world == null) { + IrisLogging.warn("World was null somehow..."); + return; + } + + for (Chunk i : new ArrayList<>(lastUse.keySet())) { + Long lastUseTime = lastUse.get(i); + if (lastUseTime != null && M.ms() - lastUseTime >= 10) { + i.unload(); + lastUse.remove(i); + } + } + if (saveWorld) { + world.save(); + } + }); + + try { + unload.get(UNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (TimeoutException e) { + IrisLogging.warn("Medieval pregen chunk unload did not finish in " + UNLOAD_TIMEOUT_SECONDS + "s, continuing."); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + @Override + public void init() { + unloadAndSaveAllChunks(false); + } + + @Override + public void close() { + // A stop request interrupts the pregen worker; shield the drain and save so chunks still hit disk. + boolean interrupted = Thread.interrupted(); + try { + waitForChunks(); + if (prefetchPool != null) { + prefetchPool.shutdownNow(); + } + unloadAndSaveAllChunks(true); + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + @Override + public void save() { + unloadAndSaveAllChunks(false); + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return false; + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public String getMethod(int x, int z) { + return "Medieval"; + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + if (futures.size() >= maxFutures) { + waitForChunks(); + } + + listener.onChunkGenerating(x, z); + // Single choke point for failure accounting: without onChunkFailed a lossy run can + // never satisfy allVisitsComplete, so a finished pregen reports as aborted forever. + CompletableFuture chunkFuture = J.isFolia() + ? PaperLib.getChunkAtAsync(world, x, z, true).thenAccept(c -> { + if (c != null) { + lastUse.put(c, M.ms()); + } + listener.onChunkGenerated(x, z); + try { + listener.onChunkCleaned(x, z); + } catch (Throwable e) { + // Already counted as generated; a throw here must not also count the + // chunk failed through the whenComplete choke point below. + IrisLogging.reportError(e); + } + }) + : scheduleChunkLoad(x, z, listener); + futures.add(chunkFuture.whenComplete((r, err) -> { + if (err != null) { + listener.onChunkFailed(x, z); + } + })); + } + + private CompletableFuture scheduleChunkLoad(int x, int z, PregenListener listener) { + if (prefetchDisabled.get() || prefetchPool == null) { + return runChunkLoad(x, z, listener); + } + + Engine engine = resolveEngine(); + if (engine == null) { + return runChunkLoad(x, z, listener); + } + + CompletableFuture aggregate = new CompletableFuture<>(); + try { + prefetchPool.submit(() -> { + try { + try { + prefetchMantle(engine, x, z); + } catch (Throwable e) { + if (prefetchDisabled.compareAndSet(false, true)) { + IrisLogging.warn("Mantle prefetch failed at chunk " + x + "," + z + "; disabling prefetch for this pregen."); + IrisLogging.reportError(e); + } + } + + CompletableFuture chunkFuture = runChunkLoad(x, z, listener); + chunkFuture.whenComplete((r, err) -> { + if (err != null) { + aggregate.completeExceptionally(err); + } else { + aggregate.complete(null); + } + }); + } catch (Throwable e) { + aggregate.completeExceptionally(e); + } + }); + } catch (Throwable rejected) { + if (prefetchDisabled.compareAndSet(false, true)) { + IrisLogging.warn("Mantle prefetch pool rejected work; disabling prefetch."); + } + return runChunkLoad(x, z, listener); + } + return aggregate; + } + + private void prefetchMantle(Engine engine, int chunkX, int chunkZ) { + if (engine == null || engine.isClosing()) { + return; + } + if (!engine.getDimension().isUseMantle()) { + return; + } + + ChunkContext context = new ChunkContext(chunkX, chunkZ, engine.getComplex()); + engine.generateMatter(chunkX, chunkZ, true, context); + } + + private CompletableFuture runChunkLoad(int x, int z, PregenListener listener) { + return directAsyncDisabled.get() ? generateChunkSync(x, z, listener) : generateChunkDirectAsync(x, z, listener); + } + + private CompletableFuture generateChunkDirectAsync(int x, int z, PregenListener listener) { + CompletableFuture future = new CompletableFuture<>(); + + J.a(() -> { + try { + loadChunk(x, z, listener); + future.complete(null); + } catch (Throwable error) { + if (directAsyncDisabled.compareAndSet(false, true)) { + IrisLogging.warn("Direct async Spigot pregen chunk load failed at " + x + "," + z + "; falling back to sync chunk loads."); + IrisLogging.reportError(error); + } + + // Chain instead of blocking: parking this MultiBurst worker on a 60s get() + // pinned a shared pool thread per failing chunk. + generateChunkSync(x, z, listener).whenComplete((r, fallbackError) -> { + if (fallbackError != null) { + future.completeExceptionally(fallbackError); + } else { + future.complete(null); + } + }); + } + }); + + return future; + } + + private CompletableFuture generateChunkSync(int x, int z, PregenListener listener) { + return J.sfut(() -> loadChunk(x, z, listener)); + } + + private void loadChunk(int x, int z, PregenListener listener) { + Chunk chunk = world.getChunkAt(x, z); + lastUse.put(chunk, M.ms()); + listener.onChunkGenerated(x, z); + try { + listener.onChunkCleaned(x, z); + } catch (Throwable e) { + // The chunk already counted as generated; a throw here must not also count it + // as failed through the whenComplete choke point. + IrisLogging.reportError(e); + } + } + + @Override + public Mantle getMantle() { + if (IrisToolbelt.isIrisWorld(world)) { + return IrisToolbelt.access(world).getEngine().getMantle().getMantle(); + } + + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisCodeWorkspace.java b/core/src/main/java/art/arcane/iris/core/project/IrisCodeWorkspace.java new file mode 100644 index 000000000..f2527319a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/IrisCodeWorkspace.java @@ -0,0 +1,306 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import org.dom4j.Document; +import org.dom4j.Element; + +import java.awt.Desktop; +import java.awt.GraphicsEnvironment; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@SuppressWarnings("ALL") +public class IrisCodeWorkspace { + private final IrisProject project; + + public IrisCodeWorkspace(IrisProject project) { + this.project = project; + } + + public void openVSCode(VolmitSender sender) { + J.attemptAsync(this::prepareAndOpenVSCode); + } + + void prepareAndOpenVSCode() { + boolean updated = updateWorkspace(true); + File workspace = getCodeWorkspaceFile(); + if (!workspace.isFile()) { + IrisLogging.warn("Could not create the code workspace for project " + project.getName() + " at " + workspace.getAbsolutePath() + "."); + return; + } + if (!updated) { + IrisLogging.warn("Could not refresh every schema for " + workspace.getAbsolutePath() + "; the editor will not be opened with stale autocomplete data."); + return; + } + if (!IrisSettings.get().getStudio().isOpenVSCode() || GraphicsEnvironment.isHeadless()) { + return; + } + + IrisLogging.msg("Opening VSCode. You may see the output from VSCode."); + IrisLogging.msg("VSCode output always starts with: '(node:#####) electron'"); + Thread launcherThread = new Thread(() -> { + try { + Desktop.getDesktop().open(workspace); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + }, "Iris-VSCode-Launcher"); + launcherThread.setDaemon(true); + launcherThread.start(); + } + + public File getCodeWorkspaceFile() { + return new File(project.getPath(), project.getName() + ".code-workspace"); + } + + public boolean updateWorkspace() { + return updateWorkspace(false); + } + + private boolean updateWorkspace(boolean immediateSchemas) { + project.getPath().mkdirs(); + File ws = getCodeWorkspaceFile(); + + // Render before touching the file: a config-generation failure has nothing to do with + // the existing workspace file, and deleting it before a rethrowing rebuild silently + // destroyed the author's workspace on every boot. + String rendered; + try { + rendered = createCodeWorkspaceConfig(immediateSchemas).toString(4); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Could not generate the code workspace config for " + ws.getAbsolutePath() + "; leaving the existing workspace file untouched."); + return false; + } + + try { + writeIfChanged(ws, rendered); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Project invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!"); + ws.delete(); + try { + IO.writeAll(ws, rendered); + } catch (Throwable e1) { + IrisLogging.reportError(e1); + e1.printStackTrace(); + } + } + + return false; + } + + public JSONObject createCodeWorkspaceConfig() { + return createCodeWorkspaceConfig(false); + } + + private static void writeIfChanged(File target, String rendered) throws IOException { + if (target.isFile() && (rendered + "\n").equals(IO.readAll(target))) { + return; + } + IO.writeAll(target, rendered); + } + + private JSONObject createCodeWorkspaceConfig(boolean immediateSchemas) { + JSONObject ws = new JSONObject(); + JSONArray folders = new JSONArray(); + JSONObject folder = new JSONObject(); + folder.put("path", "."); + folders.put(folder); + ws.put("folders", folders); + JSONObject settings = new JSONObject(); + settings.put("workbench.colorTheme", "Monokai"); + settings.put("workbench.preferredDarkColorTheme", "Solarized Dark"); + settings.put("workbench.tips.enabled", false); + settings.put("workbench.tree.indent", 24); + settings.put("files.autoSave", "onFocusChange"); + JSONObject jc = new JSONObject(); + jc.put("editor.autoIndent", "brackets"); + jc.put("editor.acceptSuggestionOnEnter", "smart"); + jc.put("editor.cursorSmoothCaretAnimation", true); + jc.put("editor.dragAndDrop", false); + jc.put("files.trimTrailingWhitespace", true); + jc.put("diffEditor.ignoreTrimWhitespace", true); + jc.put("files.trimFinalNewlines", true); + jc.put("editor.suggest.showKeywords", false); + jc.put("editor.suggest.showSnippets", false); + jc.put("editor.suggest.showWords", false); + JSONObject st = new JSONObject(); + st.put("strings", true); + jc.put("editor.quickSuggestions", st); + jc.put("editor.suggest.insertMode", "replace"); + settings.put("[json]", jc); + settings.put("json.maxItemsComputed", 30000); + JSONArray schemas = new JSONArray(); + List schemaEntries = new ArrayList<>(); + IrisData dm = IrisData.get(project.getPath()); + for (ResourceLoader resourceLoader : dm.getLoaders().v()) { + if (resourceLoader.supportsSchemas()) { + schemaEntries.add(immediateSchemas + ? resourceLoader.buildSchemaImmediately() + : resourceLoader.buildSchema()); + } + } + + for (Class snippetClass : sortedSnippets(dm.resolveSnippets())) { + try { + String snipType = snippetClass.getDeclaredAnnotation(Snippet.class).value(); + JSONObject schemaEntry = new JSONObject(); + KList fileMatches = new KList<>(); + + for (int depth = 1; depth < 8; depth++) { + fileMatches.add("/snippet/" + snipType + Form.repeat("/*", depth) + ".json"); + } + + schemaEntry.put("fileMatch", new JSONArray(fileMatches.toArray())); + schemaEntry.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json"); + schemaEntries.add(schemaEntry); + File schemaFile = new File(dm.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json"); + if (immediateSchemas) { + IO.writeAll(schemaFile, new SchemaBuilder(snippetClass, dm).construct().toString(4)); + } else { + IrisData snippetData = dm; + J.attemptAsync(() -> { + try { + IO.writeAll(schemaFile, new SchemaBuilder(snippetClass, snippetData).construct().toString(4)); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + }); + } + } catch (Throwable e) { + if (immediateSchemas) { + throw new IllegalStateException("Could not write snippet schema for " + snippetClass.getName(), e); + } + IrisLogging.reportError(e); + } + } + + schemaEntries.sort(Comparator.comparing(entry -> entry.getString("url"))); + for (JSONObject entry : schemaEntries) { + schemas.put(entry); + } + + settings.put("json.schemas", schemas); + ws.put("settings", settings); + + try { + updateIntelliJSchemaMappings(schemas); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Could not update IntelliJ schema mappings for " + project.getPath().getAbsolutePath() + "; VSCode workspace generation will continue."); + } + + return ws; + } + + private void updateIntelliJSchemaMappings(JSONArray schemas) { + File schemasFile = new File(project.getPath(), ".idea" + File.separator + "jsonSchemas.xml"); + Document doc = IO.read(schemasFile); + Element mappings = (Element) doc.selectSingleNode("//component[@name='JsonSchemaMappingsProjectConfiguration']"); + if (mappings == null) { + mappings = doc.getRootElement() + .addElement("component") + .addAttribute("name", "JsonSchemaMappingsProjectConfiguration"); + } + + Element state = (Element) mappings.selectSingleNode("state"); + if (state == null) state = mappings.addElement("state"); + + Element map = (Element) state.selectSingleNode("map"); + if (map == null) map = state.addElement("map"); + KMap schemaMap = new KMap<>(); + schemas.forEach(element -> { + if (!(element instanceof JSONObject obj)) + return; + + String url = obj.getString("url"); + String dir = obj.getJSONArray("fileMatch").getString(0); + schemaMap.put(url, dir.substring(1, dir.indexOf("/*"))); + }); + + map.selectNodes("entry/value/SchemaInfo/option[@name='relativePathToSchema']") + .stream() + .map(node -> node.valueOf("@value")) + .forEach(schemaMap::remove); + + Element ideaSchemas = map; + schemaMap.forEach((url, dir) -> { + String generatedName = UUID.randomUUID().toString(); + + Element info = ideaSchemas.addElement("entry") + .addAttribute("key", generatedName) + .addElement("value") + .addElement("SchemaInfo"); + info.addElement("option") + .addAttribute("name", "generatedName") + .addAttribute("value", generatedName); + info.addElement("option") + .addAttribute("name", "name") + .addAttribute("value", dir); + info.addElement("option") + .addAttribute("name", "relativePathToSchema") + .addAttribute("value", url); + + + Element item = info.addElement("option") + .addAttribute("name", "patterns") + .addElement("list") + .addElement("Item"); + item.addElement("option") + .addAttribute("name", "directory") + .addAttribute("value", "true"); + item.addElement("option") + .addAttribute("name", "path") + .addAttribute("value", dir); + item.addElement("option") + .addAttribute("name", "mappingKind") + .addAttribute("value", "Directory"); + }); + if (!schemaMap.isEmpty()) { + IO.write(schemasFile, doc); + } + } + + private static List> sortedSnippets(Set> snippets) { + List> sorted = new ArrayList<>(snippets); + sorted.sort(Comparator.comparing(Class::getName)); + return sorted; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisPackageCompiler.java b/core/src/main/java/art/arcane/iris/core/project/IrisPackageCompiler.java new file mode 100644 index 000000000..2011305f6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/IrisPackageCompiler.java @@ -0,0 +1,312 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import com.google.gson.Gson; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.pack.StructurePackageClosure; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBlockData; +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.IrisLootTable; +import art.arcane.iris.core.pack.PackExportClosure; +import art.arcane.iris.engine.object.IrisMarker; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.volmlib.util.scheduling.O; +import org.zeroturnaround.zip.ZipUtil; + +import java.io.File; +import java.io.IOException; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; + +@SuppressWarnings("ALL") +public class IrisPackageCompiler { + private final IrisProject project; + + public IrisPackageCompiler(IrisProject project) { + this.project = project; + } + + public File compilePackage(VolmitSender sender, boolean obfuscate, boolean minify) { + // Detached loader on purpose: obfuscation rewrites the placement 'place' lists on the + // loaded resources, which must never leak into the shared cached pack state — a second + // export against a mutated cache produced archives with an empty objects/ directory. + IrisData dm = IrisData.openRuntime(project.getPath()); + try { + return compilePackage(dm, sender, obfuscate, minify); + } finally { + dm.close(); + } + } + + private static void addIfPresent(KSet set, T value) { + if (value != null) { + set.add(value); + } + } + + private File compilePackage(IrisData dm, VolmitSender sender, boolean obfuscate, boolean minify) { + String dimm = project.getName(); + IrisDimension dimension = dm.getDimensionLoader().load(dimm); + File folder = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey()); + IO.delete(folder); + if (folder.exists()) { + throw new IllegalStateException("Failed to clear structure package staging folder " + folder.getAbsolutePath()); + } + if (!folder.mkdirs() && !folder.isDirectory()) { + throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath()); + } + IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : "")); + KSet regions = new KSet<>(); + KSet biomes = new KSet<>(); + KSet entities = new KSet<>(); + KSet spawners = new KSet<>(); + KSet generators = new KSet<>(); + KSet loot = new KSet<>(); + KSet blocks = new KSet<>(); + + // KSet is ConcurrentHashMap-backed: both add(null) and remove(null) throw NPE, so a + // failed loader lookup must be filtered at the add site, never stripped afterwards. + for (String i : dm.getBlockLoader().getPossibleKeys()) { + addIfPresent(blocks, dm.getBlockLoader().load(i)); + } + + dimension.getRegions().forEach((i) -> addIfPresent(regions, dm.getRegionLoader().load(i))); + dimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))); + regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm))); + regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)))); + regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)))); + dimension.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))); + biomes.forEach((i) -> i.getGenerators().forEach((j) -> addIfPresent(generators, j.getCachedGenerator(() -> dm)))); + biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)))); + biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)))); + KList allPlacements = new KList<>(); + regions.forEach((r) -> allPlacements.addAll(r.getObjects())); + biomes.forEach((i) -> allPlacements.addAll(i.getObjects())); + KSet markers = new KSet<>(); + for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) { + IrisMarker marker = dm.getMarkerLoader().load(markerKey); + if (marker == null) { + continue; + } + markers.add(marker); + marker.getSpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))); + } + collectSpawnerEntityKeys(spawners).forEach((i) -> addIfPresent(entities, dm.getEntityLoader().load(i))); + entities.forEach((e) -> e.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)))); + Set structureKeys = new LinkedHashSet<>(); + collectStructureKeys(structureKeys, dimension.getStructures()); + regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures())); + biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures())); + KMap renameObjects = new KMap<>(); + String a; + StringBuilder b = new StringBuilder(); + StringBuilder c = new StringBuilder(); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_SERIALIZING_OBJECTS)); + + for (IrisObjectPlacement j : allPlacements) { + b.append(j.hashCode()); + KList newNames = new KList<>(); + + for (String k : j.getPlace()) { + if (renameObjects.containsKey(k)) { + newNames.add(renameObjects.get(k)); + continue; + } + + String name = !obfuscate ? k : UUID.randomUUID().toString().replaceAll("-", ""); + b.append(name); + newNames.add(name); + renameObjects.put(k, name); + } + + j.setPlace(newNames); + } + + KMap> lookupObjects = renameObjects.flip(); + StringBuilder gb = new StringBuilder(); + ChronoLatch cl = new ChronoLatch(1000); + O ggg = new O<>(); + ggg.set(0); + O missingObjects = new O<>(); + missingObjects.set(0); + allPlacements.forEach((j) -> j.getPlace().forEach((k) -> + { + try { + KList sources = lookupObjects.get(k); + File f = sources == null || sources.isEmpty() ? null : dm.getObjectLoader().findFile(sources.get(0)); + if (f == null) { + missingObjects.set(missingObjects.get() + 1); + IrisLogging.error("Missing object for placement key " + k); + return; + } + IO.copyFile(f, new File(folder, "objects/" + k + ".iob")); + gb.append(IO.hash(f)); + ggg.set(ggg.get() + 1); + + if (cl.flip()) { + int g = ggg.get(); + ggg.set(0); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_WROTE_ANOTHER_OBJECTS, MessageArgument.untrusted("g", String.valueOf(g)))); + } + } catch (Throwable e) { + missingObjects.set(missingObjects.get() + 1); + IrisLogging.reportError(e); + } + })); + if (missingObjects.get() > 0) { + // A package missing objects must fail loudly, not ship as a clean "compiled". + throw new IllegalStateException(missingObjects.get() + + " object(s) could not be exported for pack '" + dimm + "'; package compile aborted."); + } + + b.append(IO.hash(gb.toString())); + c.append(IO.hash(b.toString())); + b = new StringBuilder(); + + IrisLogging.info("Writing Dimensional Scaffold"); + + try { + StructurePackageClosure structureClosure = StructurePackageClosure.collect(project.getPath(), structureKeys); + if (!structureClosure.isValid()) { + throw new IOException("Structure package closure is invalid: " + String.join("; ", structureClosure.errors())); + } + b.append(structureClosure.writeTo(folder, minify)); + a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + + for (IrisGenerator i : generators) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "generators/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + c.append(IO.hash(b.toString())); + b = new StringBuilder(); + + for (IrisRegion i : regions) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "regions/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + for (IrisBlockData i : blocks) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "blocks/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + for (IrisBiome i : biomes) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "biomes/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + for (IrisEntity i : entities) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "entities/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + for (IrisLootTable i : loot) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "loot/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + // Sorted so package.json.hash stays stable across runs. + for (IrisSpawner i : spawners.stream().sorted(Comparator.comparing(IrisSpawner::getLoadKey)).toList()) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "spawners/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + for (IrisMarker i : markers.stream().sorted(Comparator.comparing(IrisMarker::getLoadKey)).toList()) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "markers/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + c.append(IO.hash(b.toString())); + String finalHash = IO.hash(c.toString()); + JSONObject meta = new JSONObject(); + meta.put("hash", finalHash); + meta.put("time", M.ms()); + meta.put("version", dimension.getVersion()); + IO.writeAll(new File(folder, "package.json"), meta.toString(minify ? 0 : 4)); + File p = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey() + ".iris"); + IrisLogging.info("Compressing Package"); + ZipUtil.pack(folder, p, 9); + IO.delete(folder); + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_PACKAGE_COMPILED)); + return p; + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_FAILED)); + return null; + } + + static KSet collectSpawnerEntityKeys(KSet spawners) { + KSet entityKeys = new KSet<>(); + for (IrisSpawner spawner : spawners) { + spawner.getSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity())); + spawner.getInitialSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity())); + } + return entityKeys; + } + + private static void collectStructureKeys(Set keys, KList placements) { + if (placements == null) { + return; + } + for (IrisStructurePlacement placement : placements) { + if (placement == null || placement.getStructures() == null) { + continue; + } + for (String structureKey : placement.getStructures()) { + keys.add(structureKey); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java new file mode 100644 index 000000000..a07a9b713 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java @@ -0,0 +1,162 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.runtime.StudioOpenCoordinator; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.exceptions.IrisException; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import lombok.Data; +import org.bukkit.GameMode; +import org.bukkit.World; + +import java.io.File; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.volmlib.util.localization.MessageArgument; +@SuppressWarnings("ALL") +@Data +public class IrisProject { + private File path; + private String name; + private PlatformChunkGenerator activeProvider; + + public IrisProject(File path) { + this.path = path; + this.name = path.getName(); + } + + public boolean isOpen() { + return activeProvider != null; + } + + public KList collectFiles(File f, String fileExtension) { + KList l = new KList<>(); + + if (f.isDirectory()) { + for (File i : f.listFiles()) { + l.addAll(collectFiles(i, fileExtension)); + } + } else if (f.getName().endsWith("." + fileExtension)) { + l.add(f); + } + + return l; + } + + public KList collectFiles(String json) { + return collectFiles(path, json); + } + + public void open(VolmitSender sender) throws IrisException { + open(sender, 1337, StudioOpenCoordinator.StudioOpenKind.STANDARD, (w) -> + { + }); + } + + public CompletableFuture open( + VolmitSender sender, + long seed, + StudioOpenCoordinator.StudioOpenKind openKind, + Consumer onDone + ) throws IrisException { + if (isOpen()) { + return close().thenCompose(ignored -> openInternal(sender, seed, openKind, onDone)); + } + + return openInternal(sender, seed, openKind, onDone); + } + + private CompletableFuture openInternal( + VolmitSender sender, + long seed, + StudioOpenCoordinator.StudioOpenKind openKind, + Consumer onDone + ) { + AtomicReference stage = new AtomicReference<>("Queued"); + AtomicReference progress = new AtomicReference<>(0.01D); + AtomicBoolean complete = new AtomicBoolean(false); + AtomicBoolean failed = new AtomicBoolean(false); + CompletableFuture future = StudioOpenCoordinator.get().open( + StudioOpenCoordinator.StudioOpenRequest.studioProject( + this, + sender, + seed, + openKind, + update -> { + if (update.stage() != null && !update.stage().isBlank()) { + stage.set(update.stage()); + } + progress.set(Math.max(0D, Math.min(0.99D, update.progress()))); + }, + onDone + ) + ); + StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed); + future.whenComplete((result, throwable) -> { + World maintenanceWorld = null; + boolean maintenanceActive = false; + try { + if (throwable != null) { + failed.set(true); + Throwable error = throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null + ? completionException.getCause() + : throwable; + IrisLogging.reportError("Studio open failed for project \"" + getName() + "\".", error); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED, MessageArgument.untrusted("error", String.valueOf(error.getMessage())))); + return; + } + + if (sender.isPlayer() && sender.player() != null) { + J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.CREATIVE)); + } + activeProvider = IrisToolbelt.access(result.world()); + maintenanceWorld = result.world(); + if (maintenanceWorld != null) { + IrisToolbelt.beginWorldMaintenance(maintenanceWorld, "studio-open"); + maintenanceActive = true; + } + } finally { + if (maintenanceActive && maintenanceWorld != null) { + World worldToRelease = maintenanceWorld; + J.a(() -> IrisToolbelt.endWorldMaintenance(worldToRelease, "studio-open"), 300); + } + complete.set(true); + } + }); + return future; + } + + public CompletableFuture close() { + if (activeProvider == null) { + return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null)); + } + + return StudioOpenCoordinator.get().closeProject(this); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProjectCopier.java b/core/src/main/java/art/arcane/iris/core/project/IrisProjectCopier.java new file mode 100644 index 000000000..7c292dd68 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProjectCopier.java @@ -0,0 +1,230 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Comparator; +import java.util.Objects; +import java.util.stream.Stream; + +public final class IrisProjectCopier { + private IrisProjectCopier() { + } + + public static void copyProject(File sourcePack, File targetWorkspace, String sourceKey, String targetKey) throws IOException { + copyProject(sourcePack, targetWorkspace, sourceKey, targetKey, (source, target) -> { + }); + } + + static void copyProject( + File sourcePack, + File targetWorkspace, + String sourceKey, + String targetKey, + CopyHook copyHook + ) throws IOException { + String validatedSourceKey = requireSafeKey(sourceKey, "source"); + String validatedTargetKey = requireSafeKey(targetKey, "target"); + Path source = requireSafeSource( + Objects.requireNonNull(sourcePack, "sourcePack").toPath().toAbsolutePath().normalize(), + validatedSourceKey + ); + Path workspace = requireTargetWorkspace(targetWorkspace); + Path target = workspace.resolve(validatedTargetKey).normalize(); + requireAvailableTarget(target, workspace, validatedTargetKey); + + Path stage = Files.createTempDirectory(workspace, "." + validatedTargetKey + ".importing-"); + boolean published = false; + Throwable operationFailure = null; + try { + copyTree(source, stage, Objects.requireNonNull(copyHook, "copyHook")); + transformDimension(stage, validatedSourceKey, validatedTargetKey); + requireAvailableTarget(target, workspace, validatedTargetKey); + publish(stage, target); + published = true; + } catch (IOException | RuntimeException | Error e) { + operationFailure = e; + throw e; + } finally { + if (!published) { + try { + deleteTree(stage); + } catch (IOException cleanupFailure) { + if (operationFailure != null) { + operationFailure.addSuppressed(cleanupFailure); + } else { + throw cleanupFailure; + } + } + } + } + } + + private static String requireSafeKey(String value, String purpose) throws IOException { + if (value == null || value.isBlank()) { + throw new IOException("Project " + purpose + " key cannot be empty."); + } + + Path key; + try { + key = Path.of(value); + } catch (RuntimeException e) { + throw new IOException("Invalid project " + purpose + " key: " + value, e); + } + if (key.isAbsolute() + || key.getNameCount() != 1 + || !key.normalize().equals(key) + || ".".equals(value) + || "..".equals(value) + || value.indexOf('/') >= 0 + || value.indexOf('\\') >= 0) { + throw new IOException("Invalid project " + purpose + " key: " + value); + } + return value; + } + + private static Path requireTargetWorkspace(File targetWorkspace) throws IOException { + Path workspace = Objects.requireNonNull(targetWorkspace, "targetWorkspace").toPath().toAbsolutePath().normalize(); + if (Files.isSymbolicLink(workspace) + || !Files.isDirectory(workspace, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Project workspace is missing or unsafe: " + workspace); + } + return workspace; + } + + private static Path requireSafeSource(Path source, String sourceKey) throws IOException { + if (!Files.isDirectory(source)) { + throw new IOException("Source project is missing or unsafe: " + source); + } + PackDirectoryResolver.requireSafePackTree(source.toFile()); + Path resolvedSource = source.toRealPath(); + + Path dimension = resolvedSource.resolve("dimensions").resolve(sourceKey + ".json").normalize(); + if (!dimension.startsWith(resolvedSource) + || Files.isSymbolicLink(dimension) + || !Files.isRegularFile(dimension, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Source project dimension is missing or unsafe: " + dimension); + } + return resolvedSource; + } + + private static void requireAvailableTarget(Path target, Path workspace, String targetKey) throws IOException { + if (!workspace.equals(target.getParent()) || !targetKey.equals(target.getFileName().toString())) { + throw new IOException("Target project must be a direct child of the workspace: " + target); + } + if (Files.exists(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) { + throw new FileAlreadyExistsException(target.toString()); + } + } + + private static void copyTree(Path source, Path stage, CopyHook copyHook) throws IOException { + try (Stream walk = Files.walk(source)) { + for (Path path : walk.sorted(Comparator.naturalOrder()).toList()) { + Path relative = source.relativize(path); + if (relative.toString().isEmpty() || shouldSkip(relative)) { + continue; + } + if (Files.isSymbolicLink(path)) { + throw new IOException("Source project contains a symbolic link: " + path); + } + + Path destination = stage.resolve(relative).normalize(); + if (!destination.startsWith(stage)) { + throw new IOException("Source project entry escapes staging: " + relative); + } + copyHook.beforeCopy(path, destination); + if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + Files.createDirectories(destination); + } else if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + Files.createDirectories(destination.getParent()); + Files.copy(path, destination); + } else { + throw new IOException("Source project contains an unsupported entry: " + path); + } + } + } + } + + private static boolean shouldSkip(Path relative) { + return ".git".equals(relative.getName(0).toString()) + || relative.getFileName().toString().endsWith(".code-workspace"); + } + + private static void transformDimension(Path stage, String sourceKey, String targetKey) throws IOException { + Path oldDimension = stage.resolve("dimensions").resolve(sourceKey + ".json"); + Path newDimension = stage.resolve("dimensions").resolve(targetKey + ".json"); + if (!oldDimension.equals(newDimension)) { + Files.move(oldDimension, newDimension); + } + + JSONObject json = new JSONObject(IO.readAll(newDimension.toFile())); + if (json.has("name")) { + json.put("name", Form.capitalizeWords(targetKey.replace('-', ' '))); + IO.writeAll(newDimension.toFile(), json.toString(4)); + } + } + + private static void publish(Path stage, Path target) throws IOException { + try { + Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(stage, target); + } + } + + private static void deleteTree(Path root) throws IOException { + if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + return; + } + try (Stream walk = Files.walk(root)) { + IOException failure = null; + for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + if (failure != null) { + throw failure; + } + } + } + + @FunctionalInterface + interface CopyHook { + void beforeCopy(Path source, Path target) throws IOException; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java new file mode 100644 index 000000000..870139495 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java @@ -0,0 +1,1015 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockProperty; +import art.arcane.iris.spi.PlatformNumericRange; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.structure.StructureSchemaKeys; +import art.arcane.iris.engine.framework.ListFunction; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBiome; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.RegistryListEnchantment; +import art.arcane.iris.engine.object.annotations.RegistryListEntityType; +import art.arcane.iris.engine.object.annotations.RegistryListFont; +import art.arcane.iris.engine.object.annotations.RegistryListFunction; +import art.arcane.iris.engine.object.annotations.RegistryListItemType; +import art.arcane.iris.engine.object.annotations.RegistryListNativeJigsawPool; +import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.RegistryListSpecialEntity; +import art.arcane.iris.engine.object.annotations.RegistryListStructure; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructureSet; +import art.arcane.iris.engine.object.annotations.RegistryMapBlockState; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import org.jetbrains.annotations.NotNull; + +import java.awt.GraphicsEnvironment; +import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +public class SchemaBuilder { + private static final String SYMBOL_LIMIT__N = "*"; + private static final String SYMBOL_TYPE__N = ""; + private static final String MINECRAFT_NAMESPACE = "minecraft:"; + /** Namespaced key or family/namespace prefix: "minecraft:village_plains", "minecraft:village", "nova_structures:". */ + private static final String VANILLA_STRUCTURE_PREFIX_PATTERN = "^[a-z0-9_.-]+:[a-z0-9_./-]*$"; + private static volatile JSONArray fontTypes; + private final KMap definitions; + private final Class root; + private final KList warnings; + private final IrisData data; + private JSONArray potionTypes; + private JSONArray enchantTypes; + + public SchemaBuilder(Class root, IrisData data) { + this.data = data; + warnings = new KList<>(); + this.definitions = new KMap<>(); + this.root = root; + } + + public JSONObject construct() { + JSONObject schema = new JSONObject(); + schema.put("$schema", "http://json-schema.org/draft-07/schema#"); + schema.put("$id", "https://volmit.com/iris-schema/" + root.getSimpleName().toLowerCase() + ".json"); + + JSONObject props = buildProperties(root); + + for (String i : props.keySet()) { + if (!schema.has(i)) { + schema.put(i, props.get(i)); + } + } + + JSONObject defs = new JSONObject(); + + for (Map.Entry entry : definitions.entrySet()) { + defs.put(entry.getKey(), entry.getValue()); + } + + schema.put("definitions", defs); + + for (String i : warnings) { + IrisLogging.warn(root.getSimpleName() + ": " + i); + } + + return schema; + } + + /** + * Font families are only used for schema completion. Enumerating them touches AWT, which can fail outright on a + * headless or mod-loader JVM - a failure degrades to no completion, never to a broken schema. + */ + private static JSONArray fontTypes() { + JSONArray cached = fontTypes; + if (cached != null) { + return cached; + } + JSONArray built; + try { + built = new JSONArray(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()); + } catch (Throwable e) { + IrisLogging.debug("Schema font family enumeration unavailable: " + e.getMessage()); + built = new JSONArray(); + } + fontTypes = built; + return built; + } + + private JSONArray potionTypes() { + if (potionTypes == null) { + potionTypes = registryKeyForms(IrisPlatforms.get().registries().potionEffectKeys(), true); + } + return potionTypes; + } + + private JSONArray enchantTypes() { + if (enchantTypes == null) { + enchantTypes = registryKeyForms(IrisPlatforms.get().registries().enchantmentKeys(), false); + } + return enchantTypes; + } + + /** + * Emits the full namespaced key for every registry entry, so mod and datapack content is addressable + * unambiguously, plus the legacy short form for the vanilla namespace so existing packs stay valid. + */ + private static JSONArray registryKeyForms(List keys, boolean upperCaseLegacy) { + JSONArray a = new JSONArray(); + Set seen = new LinkedHashSet<>(); + if (keys != null) { + for (String key : keys) { + if (key == null || key.isBlank()) { + continue; + } + seen.add(key); + if (key.startsWith(MINECRAFT_NAMESPACE) || key.indexOf(':') < 0) { + String path = stripNamespace(key); + seen.add(upperCaseLegacy ? path.toUpperCase(Locale.ROOT).replace(' ', '_') : path); + } + } + } + for (String key : seen) { + a.put(key); + } + return a; + } + + /** + * Biome derivatives resolve through NamespacedKey.fromString, which accepts a full key or a bare vanilla path, + * so both forms are offered. + */ + private JSONArray biomeTypes() { + return registryKeyForms(IrisPlatforms.get().registries().biomeKeys(), false); + } + + private JSONArray entityTypes() { + return keysAsArray(IrisPlatforms.get().registries().entityKeys()); + } + + private JSONArray specialEntityTypes() { + return keysAsArray(IrisPlatforms.get().registries().specialEntityKeys()); + } + + private JSONArray vanillaStructures() { + return keysAsArray(IrisPlatforms.get().registries().structureKeys()); + } + + private JSONArray vanillaStructureSets() { + if (IrisPlatforms.get().structureHooks() == null) { + return new JSONArray(); + } + List keys = IrisPlatforms.get().structureHooks().structureSetKeys(); + return keysAsArray(keys == null ? List.of() : keys); + } + + private JSONArray nativeJigsawPools() { + return keysAsArray(templatePoolKeys()); + } + + private static JSONArray keysAsArray(List keys) { + JSONArray a = new JSONArray(); + if (keys != null) { + for (String key : keys) { + if (key != null && !key.isBlank()) { + a.put(key); + } + } + } + return a; + } + + /** + * Registers a registry-backed enum definition under {@code definitionKey} and points {@code target} at it. + *

+ * An empty key list means the registry has nothing to offer yet - the server is still booting, a modded registry + * has not been frozen, or the host simply does not expose that catalog. Emitting {@code "enum": []} in that case + * writes a schema that rejects every value the author could possibly type, turning a missing autocomplete list + * into a pack that reads as broken in the editor. The reference is omitted instead, leaving the field + * unconstrained, and the values are only computed when the definition does not exist yet. + */ + private void putRegistryEnumRef(JSONObject target, String definitionKey, Supplier values) { + if (!definitions.containsKey(definitionKey)) { + JSONArray built = values.get(); + if (built == null || built.length() == 0) { + IrisLogging.debug("Schema enum '" + definitionKey + "' omitted: the registry returned no keys"); + return; + } + JSONObject definition = new JSONObject(); + definition.put("enum", built); + definitions.put(definitionKey, definition); + } + target.put("$ref", "#/definitions/" + definitionKey); + } + + /** + * {@link #putRegistryEnumRef(JSONObject, String, Supplier)} for a list-typed property. When the enum is omitted no + * {@code items} schema is written at all, which is valid and simply means "any element". + */ + private void putRegistryEnumItems(JSONObject prop, String definitionKey, Supplier values) { + JSONObject items = new JSONObject(); + putRegistryEnumRef(items, definitionKey, values); + if (items.has("$ref")) { + prop.put("items", items); + } + } + + /** + * A registry enum that ALSO accepts family/namespace prefixes ("minecraft:village", + * "nova_structures:") — the runtime prefix-matching contract of importedStructures.disabled and + * adjustments[].match. Emitted as anyOf(enum, pattern) so autocomplete still offers registered + * keys while prefix entries validate instead of being rejected. + */ + private void putRegistryEnumOrPrefixRef(JSONObject target, String definitionKey, + String enumDefinitionKey, Supplier values, + String pattern) { + if (!definitions.containsKey(definitionKey)) { + JSONArray anyOf = new JSONArray(); + JSONObject enumRef = new JSONObject(); + try { + putRegistryEnumRef(enumRef, enumDefinitionKey, values); + } catch (RuntimeException e) { + IrisLogging.debug("Schema enum '" + enumDefinitionKey + "' unavailable (" + + e.getMessage() + "); emitting prefix pattern only"); + } + if (enumRef.has("$ref")) { + anyOf.put(enumRef); + } + JSONObject prefix = new JSONObject(); + prefix.put("type", "string"); + prefix.put("pattern", pattern); + anyOf.put(prefix); + JSONObject definition = new JSONObject(); + definition.put("anyOf", anyOf); + definitions.put(definitionKey, definition); + } + target.put("$ref", "#/definitions/" + definitionKey); + } + + private void putRegistryEnumOrPrefixItems(JSONObject prop, String definitionKey, + String enumDefinitionKey, Supplier values, + String pattern) { + JSONObject items = new JSONObject(); + putRegistryEnumOrPrefixRef(items, definitionKey, enumDefinitionKey, values, pattern); + prop.put("items", items); + } + + private JSONArray itemTypes() { + JSONArray a = new JSONArray(); + for (String key : IrisPlatforms.get().registries().itemKeys()) { + a.put(key.startsWith(MINECRAFT_NAMESPACE) ? key.substring(MINECRAFT_NAMESPACE.length()) : key); + } + return a; + } + + private JSONArray blockTypes() { + JSONArray a = new JSONArray(); + for (String i : data.getBlockLoader().getPossibleKeys()) { + a.put(i); + } + for (String i : IrisPlatforms.get().registries().blockTypeKeys()) { + a.put(i); + } + return a; + } + + private static String stripNamespace(String key) { + int idx = key.indexOf(':'); + return idx < 0 ? key : key.substring(idx + 1); + } + + private JSONObject buildProperties(Class c) { + JSONObject o = new JSONObject(); + JSONObject properties = new JSONObject(); + String desc = getDescription(c); + o.put("description", desc); + o.put("x-intellij-html-description", desc.replace("\n", "
")); + o.put("type", getType(c)); + JSONArray required = new JSONArray(); + JSONArray extended = new JSONArray(); + + Class parent = c.getSuperclass(); + while (parent != null && IrisRegistrant.class.isAssignableFrom(parent)) { + buildProperties(properties, required, extended, parent); + parent = parent.getSuperclass(); + } + + buildProperties(properties, required, extended, c); + + if (required.length() > 0) { + o.put("required", required); + } + if (extended.length() > 0) { + o.put("allOf", extended); + } + + o.put("properties", properties); + + + return buildSnippet(o, c); + } + + private void buildProperties(JSONObject properties, JSONArray required, JSONArray extended, Class c) { + for (Field k : c.getDeclaredFields()) { + if (Modifier.isStatic(k.getModifiers()) || Modifier.isFinal(k.getModifiers()) || Modifier.isTransient(k.getModifiers())) { + continue; + } + + try { + k.setAccessible(true); + } catch (InaccessibleObjectException e) { + continue; + } + + JSONObject property = buildProperty(k, c); + + if (Boolean.TRUE == property.remove("!top")) { + extended.put(property); + continue; + } + + if (Boolean.TRUE == property.remove("!required")) { + required.put(k.getName()); + } + + properties.put(k.getName(), property); + } + } + + private JSONObject buildProperty(Field k, Class cl) { + JSONObject prop = new JSONObject(); + String type = getType(k.getType()); + KList description = new KList<>(); + prop.put("!required", k.isAnnotationPresent(Required.class)); + prop.put("type", type); + String fancyType = "Unknown Type"; + + switch (type) { + case "boolean" -> fancyType = "Boolean"; + case "integer" -> { + fancyType = "Integer"; + if (k.isAnnotationPresent(MinNumber.class)) { + int min = (int) k.getDeclaredAnnotation(MinNumber.class).value(); + prop.put("minimum", min); + description.add(SYMBOL_LIMIT__N + " Minimum allowed is " + min); + } + if (k.isAnnotationPresent(MaxNumber.class)) { + int max = (int) k.getDeclaredAnnotation(MaxNumber.class).value(); + prop.put("maximum", max); + description.add(SYMBOL_LIMIT__N + " Maximum allowed is " + max); + } + } + case "number" -> { + fancyType = "Number"; + if (k.isAnnotationPresent(MinNumber.class)) { + double min = k.getDeclaredAnnotation(MinNumber.class).value(); + prop.put("minimum", min); + description.add(SYMBOL_LIMIT__N + " Minimum allowed is " + min); + } + if (k.isAnnotationPresent(MaxNumber.class)) { + double max = k.getDeclaredAnnotation(MaxNumber.class).value(); + prop.put("maximum", max); + description.add(SYMBOL_LIMIT__N + " Maximum allowed is " + max); + } + } + case "string" -> { + fancyType = "Text"; + if (k.isAnnotationPresent(MinNumber.class)) { + int min = (int) k.getDeclaredAnnotation(MinNumber.class).value(); + prop.put("minLength", min); + description.add(SYMBOL_LIMIT__N + " Minimum Length allowed is " + min); + } + if (k.isAnnotationPresent(MaxNumber.class)) { + int max = (int) k.getDeclaredAnnotation(MaxNumber.class).value(); + prop.put("maxLength", max); + description.add(SYMBOL_LIMIT__N + " Maximum Length allowed is " + max); + } + + + if (k.isAnnotationPresent(RegistryListResource.class)) { + RegistryListResource rr = k.getDeclaredAnnotation(RegistryListResource.class); + ResourceLoader loader = data.getLoaders().get(rr.value()); + + if (loader != null) { + String key = "erz" + loader.getFolderName(); + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", new JSONArray(loader.getPossibleKeys())); + definitions.put(key, j); + } + + fancyType = "Iris " + loader.getResourceTypeName(); + prop.put("$ref", "#/definitions/" + key); + description.add(SYMBOL_TYPE__N + " Must be a valid " + loader.getFolderName() + " (use ctrl+space for auto complete!)"); + } else { + IrisLogging.error("Cannot find Registry Loader for type " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName()); + } + } else if (k.isAnnotationPresent(RegistryListStructure.class)) { + String key = "enum-iris-structure-placement"; + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", new JSONArray(StructureSchemaKeys.collect( + Arrays.asList(data.getStructureLoader().getPossibleKeys()), + Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0]))); + definitions.put(key, j); + } + + fancyType = "Structure"; + prop.put("$ref", "#/definitions/" + key); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)"); + + } else if (k.isAnnotationPresent(RegistryListBlockType.class)) { + fancyType = "Block Type"; + putRegistryEnumRef(prop, "enum-block-type", this::blockTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)"); + + } else if (k.isAnnotationPresent(RegistryListNativeJigsawPool.class)) { + fancyType = "Native Jigsaw Pool"; + putRegistryEnumRef(prop, "enum-native-jigsaw-pool", this::nativeJigsawPools); + description.add(SYMBOL_TYPE__N + " Must be a registered vanilla, datapack, or modded template pool key (use ctrl+space for auto complete!)"); + + } else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) { + fancyType = "Vanilla Structure"; + if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) { + putRegistryEnumOrPrefixRef(prop, "enum-vanilla-structure-or-prefix", + "enum-vanilla-structure", this::vanillaStructures, VANILLA_STRUCTURE_PREFIX_PATTERN); + description.add(SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)"); + } else { + putRegistryEnumRef(prop, "enum-vanilla-structure", this::vanillaStructures); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)"); + } + + } else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) { + fancyType = "Vanilla Structure Set"; + putRegistryEnumRef(prop, "enum-vanilla-structure-set", this::vanillaStructureSets); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure SET key (use ctrl+space for auto complete!)"); + + } else if (k.isAnnotationPresent(RegistryListItemType.class)) { + fancyType = "Item Type"; + putRegistryEnumRef(prop, "enum-item-type", this::itemTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)"); + + } else if (k.isAnnotationPresent(RegistryListEntityType.class)) { + fancyType = "Entity Type"; + putRegistryEnumRef(prop, "enum-entity-type", this::entityTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Entity Type (use ctrl+space for auto complete!)"); + + } else if (k.isAnnotationPresent(RegistryListBiome.class)) { + fancyType = "Biome Type"; + putRegistryEnumRef(prop, "enum-biome-type", this::biomeTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or mod biome key (use ctrl+space for auto complete!)"); + + } else if (k.isAnnotationPresent(RegistryListSpecialEntity.class)) { + fancyType = "Custom Mob Type"; + putRegistryEnumRef(prop, "enum-reg-specialentity", this::specialEntityTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Custom Mob Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListFont.class)) { + fancyType = "Font Family"; + putRegistryEnumRef(prop, "enum-font", SchemaBuilder::fontTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Font Family (use ctrl+space for auto complete!)"); + + } else if (k.isAnnotationPresent(RegistryListEnchantment.class)) { + fancyType = "Enchantment Type"; + putRegistryEnumRef(prop, "enum-enchantment", this::enchantTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListPotionEffect.class)) { + fancyType = "Potion Effect Type"; + putRegistryEnumRef(prop, "enum-potion-effect-type", this::potionTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListFunction.class)) { + Class>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value(); + try { + ListFunction> instance = functionClass.getDeclaredConstructor().newInstance(); + String key = instance.key(); + fancyType = instance.fancyName(); + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", instance.apply(data)); + definitions.put(key, j); + } + + prop.put("$ref", "#/definitions/" + key); + description.add(SYMBOL_TYPE__N + " Must be a valid " + fancyType + " (use ctrl+space for auto complete!)"); + } catch (Throwable e) { + IrisLogging.error("Could not execute apply method in " + functionClass.getName()); + } + } else if (SchemaKeyedTypes.isKeyed(k.getType())) { + fancyType = addEnum(k.getType(), prop, description, SchemaKeyedTypes.values(k.getType()), Function.identity()); + } else if (k.getType().isEnum()) { + fancyType = addEnum(k.getType(), prop, description, enumNames(k.getType()), Function.identity()); + } + } + case "object" -> { + if (k.isAnnotationPresent(RegistryMapBlockState.class)) { + String blockType = k.getDeclaredAnnotation(RegistryMapBlockState.class).value(); + fancyType = "Block State"; + prop.put("!top", true); + JSONArray any = new JSONArray(); + prop.put("anyOf", any); + + for (BlockStateGroup group : reconstructBlockStateGroups()) { + List blocks = group.blocks(); + if (blocks.isEmpty()) { + continue; + } + + String raw = blocks.get(0).replace(':', '_'); + String enumKey = "enum-block-state-" + raw; + String propertiesKey = "obj-block-state-" + raw; + + any.put(new JSONObject() + .put("if", new JSONObject() + .put("properties", new JSONObject() + .put(blockType, new JSONObject() + .put("type", "string") + .put("$ref", "#/definitions/" + enumKey)))) + .put("then", new JSONObject() + .put("properties", new JSONObject() + .put(k.getName(), new JSONObject() + .put("type", "object") + .put("$ref", "#/definitions/" + propertiesKey)))) + .put("else", false)); + + if (!definitions.containsKey(enumKey)) { + JSONArray filters = new JSONArray(); + blocks.forEach(filters::put); + + definitions.put(enumKey, new JSONObject() + .put("type", "string") + .put("enum", filters)); + } + + if (!definitions.containsKey(propertiesKey)) { + JSONObject props = new JSONObject(); + for (PlatformBlockProperty property : group.properties()) { + props.put(property.name(), buildBlockPropertyJson(property)); + } + + definitions.put(propertiesKey, new JSONObject() + .put("type", "object") + .put("properties", props)); + } + } + } else { + fancyType = k.getType().getSimpleName().replaceAll("\\QIris\\E", "") + " (Object)"; + String key = "obj-" + k.getType().getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase(); + if (!definitions.containsKey(key)) { + definitions.put(key, new JSONObject()); + definitions.put(key, buildProperties(k.getType())); + } + prop.put("$ref", "#/definitions/" + key); + } + } + case "array" -> { + fancyType = "List of Something...?"; + ArrayType t = k.getDeclaredAnnotation(ArrayType.class); + if (t != null) { + if (t.min() > 0) { + prop.put("minItems", t.min()); + if (t.min() == 1) { + description.add(SYMBOL_LIMIT__N + " At least one entry must be defined, or just remove this list."); + } else { + description.add(SYMBOL_LIMIT__N + " Requires at least " + t.min() + " entries."); + } + } + + String arrayType = getType(t.type()); + + switch (arrayType) { + case "integer" -> fancyType = "List of Integers"; + case "number" -> fancyType = "List of Numbers"; + case "object" -> { + fancyType = "List of " + t.type().getSimpleName().replaceAll("\\QIris\\E", "") + "s (Objects)"; + String key = "obj-" + t.type().getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase(); + if (!definitions.containsKey(key)) { + definitions.put(key, new JSONObject()); + definitions.put(key, buildProperties(t.type())); + } + JSONObject items = new JSONObject(); + items.put("$ref", "#/definitions/" + key); + prop.put("items", items); + } + case "string" -> { + fancyType = "List of Text"; + + if (k.isAnnotationPresent(RegistryListResource.class)) { + RegistryListResource rr = k.getDeclaredAnnotation(RegistryListResource.class); + ResourceLoader loader = data.getLoaders().get(rr.value()); + + if (loader != null) { + fancyType = "List<" + loader.getResourceTypeName() + ">"; + String key = "erz" + loader.getFolderName(); + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", new JSONArray(loader.getPossibleKeys())); + definitions.put(key, j); + } + + JSONObject items = new JSONObject(); + items.put("$ref", "#/definitions/" + key); + prop.put("items", items); + description.add(SYMBOL_TYPE__N + " Must be a valid " + loader.getResourceTypeName() + " (use ctrl+space for auto complete!)"); + } else { + IrisLogging.error("Cannot find Registry Loader for type (list schema) " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName()); + } + } else if (k.isAnnotationPresent(RegistryListStructure.class)) { + fancyType = "List"; + String key = "enum-iris-structure-placement"; + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", new JSONArray(StructureSchemaKeys.collect( + Arrays.asList(data.getStructureLoader().getPossibleKeys()), + Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0]))); + definitions.put(key, j); + } + + JSONObject items = new JSONObject(); + items.put("$ref", "#/definitions/" + key); + prop.put("items", items); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListNativeJigsawPool.class)) { + fancyType = "List"; + putRegistryEnumItems(prop, "enum-native-jigsaw-pool", this::nativeJigsawPools); + description.add(SYMBOL_TYPE__N + " Must be a registered vanilla, datapack, or modded template pool key (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) { + fancyType = "List"; + if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) { + putRegistryEnumOrPrefixItems(prop, "enum-vanilla-structure-or-prefix", + "enum-vanilla-structure", this::vanillaStructures, VANILLA_STRUCTURE_PREFIX_PATTERN); + description.add(SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)"); + } else { + putRegistryEnumItems(prop, "enum-vanilla-structure", this::vanillaStructures); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)"); + } + } else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) { + fancyType = "List"; + putRegistryEnumItems(prop, "enum-vanilla-structure-set", this::vanillaStructureSets); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure set key (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListBlockType.class)) { + fancyType = "List of Block Types"; + putRegistryEnumItems(prop, "enum-block-type", this::blockTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListItemType.class)) { + fancyType = "List of Item Types"; + putRegistryEnumItems(prop, "enum-item-type", this::itemTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListEntityType.class)) { + fancyType = "List of Entity Types"; + putRegistryEnumItems(prop, "enum-entity-type", this::entityTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Entity Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListBiome.class)) { + fancyType = "List of Biome Types"; + putRegistryEnumItems(prop, "enum-biome-type", this::biomeTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or mod biome key (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListFont.class)) { + fancyType = "List of Font Families"; + putRegistryEnumItems(prop, "enum-font", SchemaBuilder::fontTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Font Family (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListEnchantment.class)) { + fancyType = "List of Enchantment Types"; + putRegistryEnumItems(prop, "enum-enchantment", this::enchantTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListPotionEffect.class)) { + fancyType = "List of Potion Effect Types"; + putRegistryEnumItems(prop, "enum-potion-effect-type", this::potionTypes); + description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListFunction.class)) { + Class>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value(); + try { + ListFunction> instance = functionClass.getDeclaredConstructor().newInstance(); + String key = instance.key(); + fancyType = instance.fancyName(); + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", instance.apply(data)); + definitions.put(key, j); + } + + JSONObject items = new JSONObject(); + items.put("$ref", "#/definitions/" + key); + prop.put("items", items); + description.add(SYMBOL_TYPE__N + " Must be a valid " + fancyType + " (use ctrl+space for auto complete!)"); + } catch (Throwable e) { + IrisLogging.error("Could not execute apply method in " + functionClass.getName()); + } + } else if (SchemaKeyedTypes.isKeyed(t.type())) { + fancyType = addEnumList(prop, description, t, SchemaKeyedTypes.values(t.type()), Function.identity()); + } else if (t.type().isEnum()) { + fancyType = addEnumList(prop, description, t, enumNames(t.type()), Function.identity()); + } + } + } + } else { + warnings.add("Undefined array type for field " + k.getName() + " (" + k.getType().getSimpleName() + ") in class " + cl.getSimpleName()); + } + } + default -> + warnings.add("Unexpected Schema Type: " + type + " for field " + k.getName() + " (" + k.getType().getSimpleName() + ") in class " + cl.getSimpleName()); + } + + KList d = new KList<>(); + d.add("" + k.getName() + ""); + d.add(getFieldDescription(k) + "


"); + d.add("" + fancyType + ""); + String typeDesc = getDescription(k.getType()); + boolean present = !typeDesc.isBlank(); + if (present) d.add(typeDesc); + + Snippet snippet = k.getType().getDeclaredAnnotation(Snippet.class); + if (snippet == null) { + ArrayType array = k.getType().getDeclaredAnnotation(ArrayType.class); + if (array != null) { + snippet = array.type().getDeclaredAnnotation(Snippet.class); + } + } + + if (snippet != null) { + String sm = snippet.value(); + if (present) d.add(" "); + d.add("You can instead specify \"snippet/" + sm + "/some-name.json\" to use a snippet file instead of specifying it here."); + present = false; + } + + try { + k.setAccessible(true); + Object value = k.get(cl.getDeclaredConstructor().newInstance()); + + if (value != null) { + if (present) d.add(" "); + if (value instanceof List) { + d.add(SYMBOL_LIMIT__N + " Default Value is an empty list"); + } else if (!k.getType().isPrimitive() && !(value instanceof Number) && !(value instanceof String) && !(value instanceof Enum) && !SchemaKeyedTypes.isKeyed(k.getType())) { + d.add(SYMBOL_LIMIT__N + " Default Value is a default object (create this object to see default properties)"); + } else { + d.add(SYMBOL_LIMIT__N + " Default Value is " + value); + } + } + } catch (Throwable ignored) { + + } + + description.forEach((g) -> d.add(g.trim())); + String desc = d.toString("\n") + .replace("
", "\n") + .replace("", "") + .replace("", ""); + String hDesc = d.toString("
"); + prop.put("type", type); + prop.put("description", desc); + prop.put("x-intellij-html-description", hDesc); + return buildSnippet(prop, k.getType()); + } + + private JSONObject buildSnippet(JSONObject prop, Class type) { + Snippet snippet = type.getDeclaredAnnotation(Snippet.class); + if (snippet == null) return prop; + + JSONObject anyOf = new JSONObject(); + JSONArray arr = new JSONArray(); + JSONObject str = new JSONObject(); + str.put("type", "string"); + String key = "enum-snippet-" + snippet.value(); + str.put("$ref", "#/definitions/" + key); + + if (!definitions.containsKey(key)) { + JSONObject enumObj = new JSONObject(); + JSONArray snl = new JSONArray(); + data.getPossibleSnippets(snippet.value()).forEach(snl::put); + enumObj.put("enum", snl); + JSONObject patternObj = new JSONObject(); + patternObj.put("type", "string"); + patternObj.put("pattern", "^snippet/" + snippet.value() + "/"); + JSONArray snippetAlt = new JSONArray(); + snippetAlt.put(enumObj); + snippetAlt.put(patternObj); + JSONObject j = new JSONObject(); + j.put("anyOf", snippetAlt); + definitions.put(key, j); + } + + arr.put(prop); + arr.put(str); + str.put("description", prop.getString("description")); + str.put("x-intellij-html-description", prop.getString("x-intellij-html-description")); + anyOf.put("anyOf", arr); + anyOf.put("description", prop.getString("description")); + anyOf.put("x-intellij-html-description", prop.getString("x-intellij-html-description")); + anyOf.put("!required", type.isAnnotationPresent(Required.class)); + + return anyOf; + } + + @NotNull + private String addEnumList(JSONObject prop, KList description, ArrayType t, T[] values, Function function) { + JSONObject items = new JSONObject(); + String s = addEnum(t.type(), items, description, values, function); + prop.put("items", items); + + return "List of " + s + "s"; + } + + private static String[] enumNames(Class enumType) { + try { + Object[] constants = enumType.getEnumConstants(); + String[] names = new String[constants.length]; + for (int index = 0; index < constants.length; index++) { + names[index] = ((Enum) constants[index]).name(); + } + return names; + } catch (LinkageError error) { + return Arrays.stream(enumType.getDeclaredFields()) + .filter(Field::isEnumConstant) + .map(Field::getName) + .toArray(String[]::new); + } + } + + @NotNull + private String addEnum(Class type, JSONObject prop, KList description, T[] values, Function function) { + JSONArray a = new JSONArray(); + boolean advanced = type.isAnnotationPresent(Desc.class); + for (T gg : values) { + if (advanced) { + try { + JSONObject j = new JSONObject(); + String name = function.apply(gg); + j.put("const", name); + Desc dd = type.getField(name).getAnnotation(Desc.class); + String desc = dd == null ? ("No Description for " + name) : dd.value(); + j.put("description", desc); + j.put("x-intellij-html-description", desc.replace("\n", "
")); + a.put(j); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } else { + a.put(function.apply(gg)); + } + } + + String key = (advanced ? "oneof-" : "") + "enum-" + type.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase(); + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put(advanced ? "oneOf" : "enum", a); + definitions.put(key, j); + } + + prop.put("$ref", "#/definitions/" + key); + description.add(SYMBOL_TYPE__N + " Must be a valid " + type.getSimpleName().replaceAll("\\QIris\\E", "") + " (use ctrl+space for auto complete!)"); + return type.getSimpleName().replaceAll("\\QIris\\E", ""); + } + + private JSONObject buildBlockPropertyJson(PlatformBlockProperty property) { + JSONObject json = new JSONObject(); + json.put("type", property.jsonType()); + json.put("default", property.defaultValue()); + List allowed = property.allowedValues(); + if (!allowed.isEmpty()) { + json.put("enum", new JSONArray(allowed)); + } + if (property.hasNumericRange()) { + PlatformNumericRange range = property.numericRange(); + if ("integer".equals(property.jsonType())) { + json.put("minimum", (long) range.minimum()); + json.put("maximum", (long) range.maximum()); + } else { + json.put("minimum", range.minimum()); + json.put("maximum", range.maximum()); + } + json.put("exclusiveMinimum", range.exclusiveMinimum()); + json.put("exclusiveMaximum", range.exclusiveMaximum()); + } + return json; + } + + private List reconstructBlockStateGroups() { + List groups = new ArrayList<>(); + List currentProperties = null; + List currentBlocks = null; + for (Map.Entry> entry : IrisPlatforms.get().registries().blockStateProperties().entrySet()) { + List value = entry.getValue(); + if (currentProperties != null && value == currentProperties) { + currentBlocks.add(entry.getKey()); + } else { + currentBlocks = new ArrayList<>(); + currentBlocks.add(entry.getKey()); + currentProperties = value; + groups.add(new BlockStateGroup(currentBlocks, value)); + } + } + return groups; + } + + private List templatePoolKeys() { + if (IrisPlatforms.get().structureHooks() == null) { + return List.of(); + } + List keys = IrisPlatforms.get().structureHooks().templatePoolKeys(); + return keys == null ? List.of() : keys; + } + + private String getType(Class c) { + if (c.equals(int.class) || c.equals(Integer.class) || c.equals(long.class) || c.equals(Long.class)) { + return "integer"; + } + + if (c.equals(float.class) || c.equals(double.class) || c.equals(Float.class) || c.equals(Double.class)) { + return "number"; + } + + if (c.equals(boolean.class) || c.equals(Boolean.class)) { + return "boolean"; + } + + if (c.equals(String.class) || c.isEnum() || SchemaKeyedTypes.isKeyed(c)) { + return "string"; + } + + if (c.equals(KList.class)) { + return "array"; + } + + if (c.equals(KMap.class)) { + return "object"; + } + + if (!c.isAnnotationPresent(Desc.class) && c.getCanonicalName().startsWith("art.arcane.iris.")) { + warnings.addIfMissing("Unsupported Type: " + c.getCanonicalName() + " Did you forget @Desc?"); + } + + return "object"; + } + + private String getFieldDescription(Field r) { + + if (r.isAnnotationPresent(Desc.class)) { + return r.getDeclaredAnnotation(Desc.class).value(); + } + + warnings.addIfMissing("Missing @Desc on field " + r.getName() + " (" + r.getType() + ") in " + r.getDeclaringClass().getCanonicalName()); + return "No Field Description"; + } + + private String getDescription(Class r) { + if (r.isAnnotationPresent(Desc.class)) { + return r.getDeclaredAnnotation(Desc.class).value(); + } + + if (!r.isPrimitive() && !r.equals(KList.class) && !r.equals(KMap.class) && r.getCanonicalName().startsWith("art.arcane.")) { + warnings.addIfMissing("Missing @Desc on " + r.getSimpleName() + " in " + (r.getDeclaringClass() != null ? r.getDeclaringClass().getCanonicalName() : " NOSRC")); + } + return ""; + } + + private record BlockStateGroup(List blocks, List properties) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaKeyedTypes.java b/core/src/main/java/art/arcane/iris/core/project/SchemaKeyedTypes.java new file mode 100644 index 000000000..6026c96e1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/SchemaKeyedTypes.java @@ -0,0 +1,34 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.util.common.reflect.KeyedType; + +final class SchemaKeyedTypes { + private SchemaKeyedTypes() { + } + + static boolean isKeyed(Class type) { + return KeyedType.isKeyed(type); + } + + static String[] values(Class type) { + return KeyedType.values(type); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java b/core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java new file mode 100644 index 000000000..0e8f13e75 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java @@ -0,0 +1,200 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.localization.MessageArgument; +import org.bukkit.Bukkit; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +@SuppressWarnings("ALL") +final class StudioOpenProgressReporter { + private static final int STUDIO_PROGRESS_BAR_WIDTH = 44; + + private StudioOpenProgressReporter() { + } + + static void startStudioOpenReporter(VolmitSender sender, AtomicReference stage, AtomicReference progress, AtomicBoolean complete, AtomicBoolean failed) { + AtomicLong nextConsoleUpdate = new AtomicLong(0L); + AtomicLong startMs = new AtomicLong(System.currentTimeMillis()); + AtomicInteger taskId = new AtomicInteger(-1); + org.bukkit.boss.BossBar bossBar; + + if (sender.isPlayer() && sender.player() != null + && IrisSettings.get().getGeneral().isProgressBossBar()) { + bossBar = Bukkit.createBossBar( + IrisLanguage.text(RuntimeProgressMessages.STUDIO_OPENING), + org.bukkit.boss.BarColor.BLUE, + org.bukkit.boss.BarStyle.SEGMENTED_20 + ); + bossBar.setProgress(0.0D); + bossBar.addPlayer(sender.player()); + bossBar.setVisible(true); + } else { + bossBar = null; + } + + int scheduledTaskId = J.ar(() -> { + double currentProgress = Math.max(0D, Math.min(0.99D, progress.get())); + String currentStage = describeStage(stage.get()); + int percent = (int) Math.round(currentProgress * 100.0D); + long elapsed = System.currentTimeMillis() - startMs.get(); + + if (complete.get()) { + J.car(taskId.get()); + + if (failed.get()) { + if (bossBar != null) { + bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress))); + bossBar.setColor(org.bukkit.boss.BarColor.RED); + bossBar.setTitle(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_FAILED_PROGRESS, + MessageArgument.trusted("percent", percent) + )); + J.a(() -> { + bossBar.removeAll(); + bossBar.setVisible(false); + }, 60); + } + if (sender.isPlayer()) { + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_FAILED, + MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)), + MessageArgument.trusted("stage", currentStage) + )); + } else { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED_2)); + } + } else { + if (bossBar != null) { + bossBar.setProgress(1.0D); + bossBar.setColor(org.bukkit.boss.BarColor.GREEN); + bossBar.setTitle(IrisLanguage.text(RuntimeProgressMessages.STUDIO_READY_PROGRESS)); + J.a(() -> { + bossBar.removeAll(); + bossBar.setVisible(false); + }, 60); + } + if (sender.isPlayer()) { + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_READY, + MessageArgument.trusted("bar", buildStudioProgressBar(1.0D)), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + )); + } else { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_READY, MessageArgument.untrusted("value", String.valueOf(Form.duration(elapsed, 1))))); + } + } + return; + } + + if (sender.isPlayer() && sender.player() != null) { + if (bossBar != null) { + bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress))); + bossBar.setTitle(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_OPENING_PROGRESS, + MessageArgument.trusted("percent", percent) + )); + } + + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_PROGRESS, + MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + )); + } else { + long now = System.currentTimeMillis(); + long nextUpdate = nextConsoleUpdate.get(); + if (now >= nextUpdate) { + String bar = buildStudioConsoleBar(currentProgress); + sender.sendMessage(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_CONSOLE_PROGRESS, + MessageArgument.trusted("bar", bar), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + )); + nextConsoleUpdate.set(now + 1500L); + } + } + }, 3); + + taskId.set(scheduledTaskId); + if (complete.get()) { + J.car(taskId.get()); + } + } + + private static String buildStudioProgressBar(double progress) { + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * STUDIO_PROGRESS_BAR_WIDTH); + StringBuilder bar = new StringBuilder(STUDIO_PROGRESS_BAR_WIDTH * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int i = 0; i < STUDIO_PROGRESS_BAR_WIDTH; i++) { + bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } + bar.append(C.DARK_GRAY).append("]"); + return bar.toString(); + } + + private static String buildStudioConsoleBar(double progress) { + int width = 20; + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width); + StringBuilder bar = new StringBuilder(); + bar.append("["); + for (int i = 0; i < width; i++) { + bar.append(i < filled ? "#" : "-"); + } + bar.append("]"); + return bar.toString(); + } + + private static String describeStage(String stage) { + if (stage == null || stage.isBlank()) { + return IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INITIALIZING); + } + return switch (stage) { + case "Queued" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_QUEUED); + case "resolve_dimension" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_DIMENSION); + case "prepare_world_pack" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_WORLD_PACK); + case "install_datapacks" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INSTALL_DATAPACKS); + case "create_world" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CREATE_WORLD); + case "apply_world_rules" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_APPLY_WORLD_RULES); + case "prepare_generator" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_GENERATOR); + case "request_entry_chunk" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_REQUEST_ENTRY_CHUNK); + case "resolve_safe_entry" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_SAFE_ENTRY); + case "teleport_player" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_TELEPORT_PLAYER); + case "finalize_open" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_FINALIZE_OPEN); + case "cleanup" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CLEANUP); + default -> Form.capitalizeWords(stage.replace('_', ' ')); + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/EngineResolver.java b/core/src/main/java/art/arcane/iris/core/protocol/EngineResolver.java new file mode 100644 index 000000000..0b073c6db --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/EngineResolver.java @@ -0,0 +1,26 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; + +@FunctionalInterface +public interface EngineResolver { + Engine resolve(String sessionId); +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisCursorResolver.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisCursorResolver.java new file mode 100644 index 000000000..57e26b722 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisCursorResolver.java @@ -0,0 +1,45 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.spi.protocol.IrisMessage; + +public final class IrisCursorResolver { + private IrisCursorResolver() { + } + + public static IrisMessage.CursorInfo resolve(Engine engine, int blockX, int blockZ) { + IrisBiome surfaceBiome = engine.getSurfaceBiome(blockX, blockZ); + IrisRegion region = engine.getRegion(blockX, blockZ); + IrisBiome caveBiome = engine.getCaveBiome(blockX, blockZ); + int height = engine.getMinHeight() + engine.getHeight(blockX, blockZ); + String biomeKey = keyOf(surfaceBiome == null ? null : surfaceBiome.getLoadKey()); + String regionKey = keyOf(region == null ? null : region.getLoadKey()); + String caveBiomeKey = keyOf(caveBiome == null ? null : caveBiome.getLoadKey()); + String dimensionKey = keyOf(engine.getDimension().getLoadKey()); + return new IrisMessage.CursorInfo(blockX, blockZ, biomeKey, regionKey, caveBiomeKey, height, dimensionKey); + } + + private static String keyOf(String value) { + return value == null ? "" : value; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisProtocolServer.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisProtocolServer.java new file mode 100644 index 000000000..04e1f74f7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisProtocolServer.java @@ -0,0 +1,376 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisMessageCodec; +import art.arcane.iris.spi.protocol.IrisProtocol; +import art.arcane.iris.spi.protocol.ProtocolException; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongSupplier; + +public final class IrisProtocolServer { + private final IrisSessionRegistry registry; + private final long serverCapabilities; + private final String serverBrand; + private final boolean irisActive; + private final LongSupplier clock; + private final AtomicLong droppedBeforeHello; + private final AtomicLong rateLimitedFrames; + private final AtomicLong rejectedFrames; + private final AtomicLong versionMismatches; + private final AtomicLong capabilityRejected; + private final AtomicLong noEngineDrops; + private final AtomicLong cursorInfoServed; + private final AtomicLong cursorRateLimited; + private final AtomicLong cursorOutOfBounds; + private final AtomicLong visionTileForwarded; + private final AtomicLong visionRateLimited; + private final AtomicLong pregenRegionDeltasBroadcast; + private final AtomicLong studioHotloadsBroadcast; + private final AtomicLong toastsBroadcast; + private final AtomicLong toastsSent; + private volatile EngineResolver engineResolver; + private volatile VisionTileRequestHandler visionTileHandler; + + public IrisProtocolServer(IrisSessionRegistry registry, long serverCapabilities, String serverBrand, boolean irisActive) { + this(registry, serverCapabilities, serverBrand, irisActive, System::currentTimeMillis); + } + + IrisProtocolServer(IrisSessionRegistry registry, long serverCapabilities, String serverBrand, boolean irisActive, LongSupplier clock) { + this.registry = Objects.requireNonNull(registry, "session registry"); + this.serverCapabilities = serverCapabilities; + this.serverBrand = Objects.requireNonNull(serverBrand, "server brand"); + this.irisActive = irisActive; + this.clock = Objects.requireNonNull(clock, "clock"); + this.droppedBeforeHello = new AtomicLong(0L); + this.rateLimitedFrames = new AtomicLong(0L); + this.rejectedFrames = new AtomicLong(0L); + this.versionMismatches = new AtomicLong(0L); + this.capabilityRejected = new AtomicLong(0L); + this.noEngineDrops = new AtomicLong(0L); + this.cursorInfoServed = new AtomicLong(0L); + this.cursorRateLimited = new AtomicLong(0L); + this.cursorOutOfBounds = new AtomicLong(0L); + this.visionTileForwarded = new AtomicLong(0L); + this.visionRateLimited = new AtomicLong(0L); + this.pregenRegionDeltasBroadcast = new AtomicLong(0L); + this.studioHotloadsBroadcast = new AtomicLong(0L); + this.toastsBroadcast = new AtomicLong(0L); + this.toastsSent = new AtomicLong(0L); + } + + public IrisSessionRegistry registry() { + return registry; + } + + public void setEngineResolver(EngineResolver engineResolver) { + this.engineResolver = engineResolver; + } + + public void setVisionTileHandler(VisionTileRequestHandler visionTileHandler) { + this.visionTileHandler = visionTileHandler; + } + + public void onClientFrame(String sessionId, byte[] frame) { + IrisSession session = registry.get(sessionId); + if (session == null) { + return; + } + if (!session.allowInbound(clock.getAsLong())) { + rateLimitedFrames.incrementAndGet(); + return; + } + IrisMessage message; + try { + message = IrisMessageCodec.decode(frame); + } catch (ProtocolException rejected) { + rejectedFrames.incrementAndGet(); + return; + } + if (message == null) { + return; + } + dispatch(session, message); + } + + public void broadcastPregenProgress(long jobId, long chunksDone, long chunksTotal, double chunksPerSecond, long etaMillis, int state) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_PREGEN)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.PregenProgress(jobId, chunksDone, chunksTotal, chunksPerSecond, etaMillis, state)); + } + session.sendRaw(frame); + } + } + + public void pregenEnd(long jobId, boolean completed) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_PREGEN)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.PregenEnd(jobId, completed)); + } + session.sendRaw(frame); + } + } + + public void broadcastDimensionStatus(String dimensionKey, String packKey, long seed, int minY, int maxY, boolean irisWorld) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady()) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.DimensionStatus(dimensionKey, packKey, seed, minY, maxY, irisWorld)); + } + session.sendRaw(frame); + } + } + + public void sendDimensionStatus(String sessionId, String dimensionKey, String packKey, long seed, int minY, int maxY, boolean irisWorld) { + IrisSession session = registry.get(sessionId); + if (session == null || !session.isReady()) { + return; + } + session.send(new IrisMessage.DimensionStatus(dimensionKey, packKey, seed, minY, maxY, irisWorld)); + } + + public void broadcastPregenRegionDelta(long jobId, int regionX, int regionZ, int state) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_PREGEN)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.PregenRegionDelta(jobId, regionX, regionZ, state)); + } + session.sendRaw(frame); + } + if (frame != null) { + pregenRegionDeltasBroadcast.incrementAndGet(); + } + } + + public void broadcastStudioHotload(String packKey, int changedFiles, boolean failed, String message) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_STUDIO)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.StudioHotload(packKey, changedFiles, failed, message)); + } + session.sendRaw(frame); + } + if (frame != null) { + studioHotloadsBroadcast.incrementAndGet(); + } + } + + public void broadcastToast(int kind, String title, String body) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_STUDIO)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.Toast(kind, title, body)); + } + session.sendRaw(frame); + } + if (frame != null) { + toastsBroadcast.incrementAndGet(); + } + } + + public void sendToast(String sessionId, int kind, String title, String body) { + IrisSession session = registry.get(sessionId); + if (session == null || !session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_STUDIO)) { + return; + } + session.send(new IrisMessage.Toast(kind, title, body)); + toastsSent.incrementAndGet(); + } + + public long droppedBeforeHelloCount() { + return droppedBeforeHello.get(); + } + + public long rateLimitedFrameCount() { + return rateLimitedFrames.get(); + } + + public long rejectedFrameCount() { + return rejectedFrames.get(); + } + + public long versionMismatchCount() { + return versionMismatches.get(); + } + + public long capabilityRejectedCount() { + return capabilityRejected.get(); + } + + public long noEngineDropCount() { + return noEngineDrops.get(); + } + + public long cursorInfoServedCount() { + return cursorInfoServed.get(); + } + + public long cursorRateLimitedCount() { + return cursorRateLimited.get(); + } + + public long cursorOutOfBoundsCount() { + return cursorOutOfBounds.get(); + } + + public long visionTileForwardedCount() { + return visionTileForwarded.get(); + } + + public long visionRateLimitedCount() { + return visionRateLimited.get(); + } + + public long pregenRegionDeltasBroadcastCount() { + return pregenRegionDeltasBroadcast.get(); + } + + public long studioHotloadsBroadcastCount() { + return studioHotloadsBroadcast.get(); + } + + public long toastsBroadcastCount() { + return toastsBroadcast.get(); + } + + public long toastsSentCount() { + return toastsSent.get(); + } + + private void dispatch(IrisSession session, IrisMessage message) { + if (message instanceof IrisMessage.ClientHello clientHello) { + onClientHello(session, clientHello); + return; + } + if (session.state() == IrisSession.State.AWAITING_HELLO) { + droppedBeforeHello.incrementAndGet(); + return; + } + switch (message) { + case IrisMessage.CursorInfoRequest cursorInfoRequest -> onCursorInfoRequest(session, cursorInfoRequest); + case IrisMessage.VisionTileRequest visionTileRequest -> onVisionTileRequest(session, visionTileRequest); + default -> { + } + } + } + + private void onClientHello(IrisSession session, IrisMessage.ClientHello clientHello) { + if (clientHello.protocolVersion() != IrisProtocol.PROTOCOL_VERSION) { + versionMismatches.incrementAndGet(); + // Answer anyway with our version so the client lands in INCOMPATIBLE instead of retrying until it + // gives up and reports "server does not run Iris". The session stays AWAITING_HELLO and every + // later frame from it is dropped by dispatch. + session.send(new IrisMessage.ServerHello(IrisProtocol.PROTOCOL_VERSION, serverCapabilities, serverBrand, irisActive)); + return; + } + session.markReady(clientHello.protocolVersion(), clientHello.capabilities()); + session.send(new IrisMessage.ServerHello(IrisProtocol.PROTOCOL_VERSION, serverCapabilities, serverBrand, irisActive)); + } + + private void onCursorInfoRequest(IrisSession session, IrisMessage.CursorInfoRequest request) { + if (!session.hasCapability(IrisProtocol.CAPABILITY_CURSOR)) { + capabilityRejected.incrementAndGet(); + return; + } + if (outOfWorldBounds(request.blockX()) || outOfWorldBounds(request.blockZ())) { + cursorOutOfBounds.incrementAndGet(); + return; + } + if (!session.allowCursorInfo(clock.getAsLong())) { + cursorRateLimited.incrementAndGet(); + return; + } + EngineResolver resolver = engineResolver; + if (resolver == null) { + noEngineDrops.incrementAndGet(); + return; + } + Engine engine = resolver.resolve(session.id()); + if (engine == null) { + noEngineDrops.incrementAndGet(); + return; + } + session.send(IrisCursorResolver.resolve(engine, request.blockX(), request.blockZ())); + cursorInfoServed.incrementAndGet(); + } + + private void onVisionTileRequest(IrisSession session, IrisMessage.VisionTileRequest request) { + if (!session.hasCapability(IrisProtocol.CAPABILITY_VISION)) { + capabilityRejected.incrementAndGet(); + return; + } + if (!session.allowVisionTile(clock.getAsLong())) { + visionRateLimited.incrementAndGet(); + return; + } + VisionTileRequestHandler handler = visionTileHandler; + if (handler == null) { + noEngineDrops.incrementAndGet(); + return; + } + handler.handle(session.id(), request.tileX(), request.tileZ(), request.zoomLevel()); + visionTileForwarded.incrementAndGet(); + } + + private static boolean outOfWorldBounds(int coordinate) { + return coordinate > IrisProtocol.MAX_QUERY_BLOCK_COORDINATE || coordinate < -IrisProtocol.MAX_QUERY_BLOCK_COORDINATE; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisServerTransport.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisServerTransport.java new file mode 100644 index 000000000..e0c8794c6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisServerTransport.java @@ -0,0 +1,23 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.protocol; + +public interface IrisServerTransport { + void sendToClient(String sessionId, byte[] frame); +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisSession.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisSession.java new file mode 100644 index 000000000..abdd499da --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisSession.java @@ -0,0 +1,137 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisMessageCodec; +import art.arcane.iris.spi.protocol.IrisProtocol; + +import java.util.Objects; + +public final class IrisSession { + private final String id; + private final IrisServerTransport transport; + private volatile State state; + private volatile long capabilities; + private volatile int remoteProtocolVersion; + private long inboundWindowStartMillis; + private int inboundFramesInWindow; + private long visionTileWindowStartMillis; + private int visionTileRequestsInWindow; + private long cursorInfoWindowStartMillis; + private int cursorInfoRequestsInWindow; + + public IrisSession(String id, IrisServerTransport transport) { + this.id = Objects.requireNonNull(id, "session id"); + this.transport = Objects.requireNonNull(transport, "session transport"); + this.state = State.AWAITING_HELLO; + this.capabilities = 0L; + this.remoteProtocolVersion = 0; + this.inboundWindowStartMillis = 0L; + this.inboundFramesInWindow = 0; + this.visionTileWindowStartMillis = 0L; + this.visionTileRequestsInWindow = 0; + this.cursorInfoWindowStartMillis = 0L; + this.cursorInfoRequestsInWindow = 0; + } + + public String id() { + return id; + } + + public State state() { + return state; + } + + public long capabilities() { + return capabilities; + } + + public int remoteProtocolVersion() { + return remoteProtocolVersion; + } + + public boolean isReady() { + return state == State.READY; + } + + public boolean hasCapability(long capabilityMask) { + return (capabilities & capabilityMask) != 0L; + } + + public void markReady(int protocolVersion, long negotiatedCapabilities) { + this.remoteProtocolVersion = protocolVersion; + this.capabilities = negotiatedCapabilities; + this.state = State.READY; + } + + public synchronized boolean allowInbound(long nowMillis) { + if (nowMillis - inboundWindowStartMillis >= 1000L) { + inboundWindowStartMillis = nowMillis; + inboundFramesInWindow = 0; + } + if (inboundFramesInWindow >= IrisProtocol.MAX_INBOUND_FRAMES_PER_SECOND) { + return false; + } + inboundFramesInWindow++; + return true; + } + + public synchronized boolean allowVisionTile(long nowMillis) { + if (nowMillis - visionTileWindowStartMillis >= 1000L) { + visionTileWindowStartMillis = nowMillis; + visionTileRequestsInWindow = 0; + } + if (visionTileRequestsInWindow >= IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND) { + return false; + } + visionTileRequestsInWindow++; + return true; + } + + /** + * Cursor lookups get their own second-window budget instead of sharing + * {@link #allowInbound(long)}: a client is free to spend its whole frame budget on cursors otherwise, and + * each lookup costs three engine column resolves. + */ + public synchronized boolean allowCursorInfo(long nowMillis) { + if (nowMillis - cursorInfoWindowStartMillis >= 1000L) { + cursorInfoWindowStartMillis = nowMillis; + cursorInfoRequestsInWindow = 0; + } + if (cursorInfoRequestsInWindow >= IrisProtocol.MAX_CURSOR_INFO_REQUESTS_PER_SECOND) { + return false; + } + cursorInfoRequestsInWindow++; + return true; + } + + public void send(IrisMessage message) { + sendRaw(IrisMessageCodec.encode(message)); + } + + public void sendRaw(byte[] frame) { + transport.sendToClient(id, frame); + } + + public enum State { + AWAITING_HELLO, + READY + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisSessionRegistry.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisSessionRegistry.java new file mode 100644 index 000000000..07a800f18 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisSessionRegistry.java @@ -0,0 +1,53 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.protocol; + +import java.util.concurrent.ConcurrentHashMap; + +public final class IrisSessionRegistry { + private final ConcurrentHashMap sessions; + + public IrisSessionRegistry() { + this.sessions = new ConcurrentHashMap<>(); + } + + public void register(IrisSession session) { + sessions.put(session.id(), session); + } + + public IrisSession unregister(String sessionId) { + return sessions.remove(sessionId); + } + + public IrisSession get(String sessionId) { + return sessions.get(sessionId); + } + + public boolean isEmpty() { + return sessions.isEmpty(); + } + + public int size() { + return sessions.size(); + } + + public Iterable all() { + return sessions.values(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisTileEncoder.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisTileEncoder.java new file mode 100644 index 000000000..c0c45bdef --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisTileEncoder.java @@ -0,0 +1,138 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.render.IrisRenderer; +import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisProtocol; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.Deflater; + +public final class IrisTileEncoder { + public static final int TILE_PIXELS = 128; + public static final int MAX_ZOOM_LEVEL = 8; + public static final int MODE_RAW_RGB = 0; + public static final int MODE_PALETTE = 1; + private static final int MAX_PALETTE_COLORS = 256; + private static final int RGB_MASK = 0xFFFFFF; + + private IrisTileEncoder() { + } + + public static List encode(Engine engine, int tileX, int tileZ, int zoomLevel, int sequence) { + int zoom = clampZoom(zoomLevel); + int blocksPerPixel = 1 << zoom; + double size = (double) TILE_PIXELS * blocksPerPixel; + double sx = (double) tileX * size; + double sz = (double) tileZ * size; + BufferedImage image = new IrisRenderer(engine).render(sx, sz, size, TILE_PIXELS, RenderType.BIOME); + int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + byte[] blob = encodePixels(pixels, TILE_PIXELS, TILE_PIXELS); + return splitIntoChunks(blob, tileX, tileZ, zoom, sequence); + } + + public static byte[] encodePixels(int[] pixels, int width, int height) { + Map palette = buildPalette(pixels); + ByteArrayOutputStream raw = new ByteArrayOutputStream(pixels.length); + try (DataOutputStream out = new DataOutputStream(raw)) { + out.writeInt(width); + out.writeInt(height); + if (palette != null) { + out.writeByte(MODE_PALETTE); + out.writeInt(palette.size()); + for (Integer color : palette.keySet()) { + out.writeByte(color >> 16 & 0xFF); + out.writeByte(color >> 8 & 0xFF); + out.writeByte(color & 0xFF); + } + for (int pixel : pixels) { + out.writeByte(palette.get(pixel & RGB_MASK)); + } + } else { + out.writeByte(MODE_RAW_RGB); + for (int pixel : pixels) { + out.writeByte(pixel >> 16 & 0xFF); + out.writeByte(pixel >> 8 & 0xFF); + out.writeByte(pixel & 0xFF); + } + } + } catch (IOException failure) { + throw new UncheckedIOException("vision tile pixel encode failed", failure); + } + return deflate(raw.toByteArray()); + } + + public static List splitIntoChunks(byte[] blob, int tileX, int tileZ, int zoomLevel, int sequence) { + int chunkBytes = IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES; + int chunkCount = Math.max(1, (blob.length + chunkBytes - 1) / chunkBytes); + List chunks = new ArrayList<>(chunkCount); + for (int index = 0; index < chunkCount; index++) { + int start = index * chunkBytes; + int end = Math.min(start + chunkBytes, blob.length); + byte[] slice = new byte[end - start]; + System.arraycopy(blob, start, slice, 0, slice.length); + chunks.add(new IrisMessage.VisionTile(tileX, tileZ, zoomLevel, sequence, index, chunkCount, slice)); + } + return chunks; + } + + private static Map buildPalette(int[] pixels) { + LinkedHashMap palette = new LinkedHashMap<>(); + for (int pixel : pixels) { + int color = pixel & RGB_MASK; + if (!palette.containsKey(color)) { + if (palette.size() >= MAX_PALETTE_COLORS) { + return null; + } + palette.put(color, palette.size()); + } + } + return palette; + } + + private static byte[] deflate(byte[] input) { + Deflater deflater = new Deflater(Deflater.BEST_SPEED); + deflater.setInput(input); + deflater.finish(); + ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length / 2)); + byte[] buffer = new byte[8192]; + while (!deflater.finished()) { + int produced = deflater.deflate(buffer); + out.write(buffer, 0, produced); + } + deflater.end(); + return out.toByteArray(); + } + + private static int clampZoom(int zoomLevel) { + return Math.max(0, Math.min(zoomLevel, MAX_ZOOM_LEVEL)); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisVisionRequestService.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisVisionRequestService.java new file mode 100644 index 000000000..f74b4928b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisVisionRequestService.java @@ -0,0 +1,194 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisProtocol; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public final class IrisVisionRequestService implements VisionTileRequestHandler { + private static final int DEFAULT_MAX_PENDING = 64; + private static final long SHED_LOG_INTERVAL_MILLIS = 60_000L; + private static final int SEQUENCE_WRAP_GUARD = Integer.MAX_VALUE - 1024; + + private final EngineResolver engineResolver; + private final IrisSessionRegistry registry; + private final Executor executor; + private final int maxPending; + private final ArrayDeque pending; + /** + * One counter per session, not one per (session, tile, zoom). The client only ever compares sequences + * within a single tile key, so a session-wide monotonic counter satisfies the "newer wins" contract in + * IrisTileAssembler while keeping this map bounded by the player count instead of by how far players pan. + */ + private final ConcurrentHashMap sequences; + private final AtomicLong nextShedLogAt; + private final AtomicLong droppedSaturated; + private final AtomicLong droppedNoEngine; + private final AtomicLong droppedNoSession; + private final AtomicLong tilesEncoded; + + IrisVisionRequestService(EngineResolver engineResolver, IrisSessionRegistry registry, Executor executor, int maxPending) { + this.engineResolver = Objects.requireNonNull(engineResolver, "engine resolver"); + this.registry = Objects.requireNonNull(registry, "session registry"); + this.executor = Objects.requireNonNull(executor, "executor"); + this.maxPending = Math.max(1, maxPending); + this.pending = new ArrayDeque<>(); + this.sequences = new ConcurrentHashMap<>(); + this.nextShedLogAt = new AtomicLong(0L); + this.droppedSaturated = new AtomicLong(0L); + this.droppedNoEngine = new AtomicLong(0L); + this.droppedNoSession = new AtomicLong(0L); + this.tilesEncoded = new AtomicLong(0L); + } + + public static IrisVisionRequestService create(EngineResolver engineResolver, IrisSessionRegistry registry) { + AtomicInteger threadIndex = new AtomicInteger(0); + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), runnable -> { + Thread thread = new Thread(runnable); + thread.setName("Iris Vision Tile " + threadIndex.incrementAndGet()); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + executor.allowCoreThreadTimeOut(true); + PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class); + if (preservation != null) { + preservation.register(executor); + } + return new IrisVisionRequestService(engineResolver, registry, executor, DEFAULT_MAX_PENDING); + } + + @Override + public void handle(String sessionId, int tileX, int tileZ, int zoomLevel) { + PendingRequest request = new PendingRequest(sessionId, tileX, tileZ, zoomLevel); + int shed = 0; + synchronized (pending) { + while (pending.size() >= maxPending) { + pending.pollFirst(); + shed++; + } + pending.addLast(request); + } + if (shed > 0) { + droppedSaturated.addAndGet(shed); + logShed(); + } + executor.execute(this::drainOne); + } + + /** + * Drops the retained sequence counter and every queued tile request for a session. Called when a session + * disconnects or unregisters so neither structure grows with the player count over a server's uptime. + */ + public void clearSession(String sessionId) { + if (sessionId == null || sessionId.isEmpty()) { + return; + } + sequences.remove(sessionId); + synchronized (pending) { + pending.removeIf((PendingRequest request) -> sessionId.equals(request.sessionId())); + } + } + + public long droppedSaturatedCount() { + return droppedSaturated.get(); + } + + public long droppedNoEngineCount() { + return droppedNoEngine.get(); + } + + public long droppedNoSessionCount() { + return droppedNoSession.get(); + } + + public long tilesEncodedCount() { + return tilesEncoded.get(); + } + + public int pendingSize() { + synchronized (pending) { + return pending.size(); + } + } + + private void drainOne() { + PendingRequest request; + synchronized (pending) { + request = pending.pollFirst(); + } + if (request == null) { + return; + } + process(request); + } + + private void process(PendingRequest request) { + IrisSession session = registry.get(request.sessionId()); + if (session == null || !session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_VISION)) { + droppedNoSession.incrementAndGet(); + return; + } + Engine engine = engineResolver.resolve(request.sessionId()); + if (engine == null) { + droppedNoEngine.incrementAndGet(); + return; + } + int sequence = nextSequence(request); + List chunks = IrisTileEncoder.encode(engine, request.tileX(), request.tileZ(), request.zoomLevel(), sequence); + for (IrisMessage.VisionTile chunk : chunks) { + session.send(chunk); + } + tilesEncoded.incrementAndGet(); + } + + private int nextSequence(PendingRequest request) { + return sequences.merge( + request.sessionId(), + 1, + (Integer current, Integer step) -> current >= SEQUENCE_WRAP_GUARD ? 1 : current + step); + } + + private void logShed() { + long now = System.currentTimeMillis(); + long due = nextShedLogAt.get(); + if (now < due || !nextShedLogAt.compareAndSet(due, now + SHED_LOG_INTERVAL_MILLIS)) { + return; + } + IrisLogging.warn("vision: request queue saturated at " + maxPending + ", shed " + droppedSaturated.get() + " total"); + } + + private record PendingRequest(String sessionId, int tileX, int tileZ, int zoomLevel) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/VisionTileRequestHandler.java b/core/src/main/java/art/arcane/iris/core/protocol/VisionTileRequestHandler.java new file mode 100644 index 000000000..ccfdb755b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/VisionTileRequestHandler.java @@ -0,0 +1,24 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.protocol; + +@FunctionalInterface +public interface VisionTileRequestHandler { + void handle(String sessionId, int tileX, int tileZ, int zoomLevel); +} diff --git a/core/src/main/java/com/volmit/iris/core/report/Report.java b/core/src/main/java/art/arcane/iris/core/report/Report.java similarity index 97% rename from core/src/main/java/com/volmit/iris/core/report/Report.java rename to core/src/main/java/art/arcane/iris/core/report/Report.java index 67f665fee..f97de5995 100644 --- a/core/src/main/java/com/volmit/iris/core/report/Report.java +++ b/core/src/main/java/art/arcane/iris/core/report/Report.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.report; +package art.arcane.iris.core.report; import lombok.Builder; import lombok.Data; diff --git a/core/src/main/java/com/volmit/iris/core/report/ReportType.java b/core/src/main/java/art/arcane/iris/core/report/ReportType.java similarity index 95% rename from core/src/main/java/com/volmit/iris/core/report/ReportType.java rename to core/src/main/java/art/arcane/iris/core/report/ReportType.java index 5976010a0..8fd19b339 100644 --- a/core/src/main/java/com/volmit/iris/core/report/ReportType.java +++ b/core/src/main/java/art/arcane/iris/core/report/ReportType.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.report; +package art.arcane.iris.core.report; public enum ReportType { ERROR, diff --git a/core/src/main/java/art/arcane/iris/core/runtime/BukkitPublicRuntimeControlBackend.java b/core/src/main/java/art/arcane/iris/core/runtime/BukkitPublicRuntimeControlBackend.java new file mode 100644 index 000000000..ddada2db9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/BukkitPublicRuntimeControlBackend.java @@ -0,0 +1,77 @@ +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.lifecycle.CapabilitySnapshot; +import io.papermc.lib.PaperLib; +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; + +final class BukkitPublicRuntimeControlBackend implements WorldRuntimeControlBackend { + private final CapabilitySnapshot capabilities; + + BukkitPublicRuntimeControlBackend(CapabilitySnapshot capabilities) { + this.capabilities = capabilities; + } + + @Override + public String backendName() { + return "bukkit_public_runtime"; + } + + @Override + public String describeCapabilities() { + String chunkAsync = capabilities.chunkAtAsyncMethod() != null ? "world#getChunkAtAsync" : "paperlib"; + return "time=bukkit_world#setTime, chunkAsync=" + chunkAsync + ", teleport=entity_scheduler"; + } + + @Override + public OptionalLong readDayTime(World world) { + if (world == null) { + return OptionalLong.empty(); + } + + return OptionalLong.of(world.getTime()); + } + + @Override + public boolean writeDayTime(World world, long dayTime) { + if (world == null) { + return false; + } + + world.setTime(dayTime); + return true; + } + + @Override + public void syncTime(World world) { + } + + @Override + public CompletableFuture requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) { + if (world == null) { + return CompletableFuture.failedFuture(new IllegalStateException("World is null.")); + } + + if (capabilities.chunkAtAsyncMethod() != null) { + try { + Object result = capabilities.chunkAtAsyncMethod().invoke(world, chunkX, chunkZ, generate); + if (result instanceof CompletableFuture) { + @SuppressWarnings("unchecked") + CompletableFuture future = (CompletableFuture) result; + return future; + } + } catch (Throwable ignored) { + } + } + + CompletableFuture future = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate); + if (future == null) { + return CompletableFuture.failedFuture(new IllegalStateException("PaperLib did not return a chunk future.")); + } + + return future; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/ChunkClearer.java b/core/src/main/java/art/arcane/iris/core/runtime/ChunkClearer.java new file mode 100644 index 000000000..e2e3c15e0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/ChunkClearer.java @@ -0,0 +1,144 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; + +public final class ChunkClearer { + private static final int APPLY_AHEAD = 8; + + private final World world; + private final Engine engine; + private final int centerChunkX; + private final int centerChunkZ; + private final int radius; + private final ChunkJobReporter reporter; + + public ChunkClearer(World world, Engine engine, VolmitSender sender, int centerChunkX, int centerChunkZ, int radius) { + this.world = world; + this.engine = engine; + this.centerChunkX = centerChunkX; + this.centerChunkZ = centerChunkZ; + this.radius = Math.max(0, radius); + this.reporter = new ChunkJobReporter(sender, IrisLanguage.text(RuntimeProgressMessages.CHUNK_TITLE_DELETE), world); + } + + public void start() { + reporter.start(); + Thread thread = new Thread(this::run, "Iris Delete Chunks"); + thread.setDaemon(true); + thread.start(); + } + + private void run() { + boolean error = false; + try { + List targets = ChunkJobReporter.orderedTargets(centerChunkX, centerChunkZ, radius); + reporter.setTotal(targets.size()); + reporter.setStage(IrisLanguage.text(RuntimeProgressMessages.CHUNK_STAGE_CLEARING)); + clear(targets); + } catch (Throwable e) { + IrisLogging.reportError(e); + error = true; + } finally { + reporter.finish(error); + } + } + + private void clear(List targets) throws InterruptedException { + Mantle mantle = engine.getMantle().getMantle(); + Semaphore inFlight = new Semaphore(APPLY_AHEAD); + CountDownLatch allCleared = new CountDownLatch(targets.size()); + + for (int[] target : targets) { + int chunkX = target[0]; + int chunkZ = target[1]; + + inFlight.acquire(); + boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> { + boolean ok = false; + try { + clearChunk(chunkX, chunkZ, mantle); + ok = true; + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + reporter.countApplied(ok); + inFlight.release(); + allCleared.countDown(); + } + }); + + if (!scheduled) { + IrisLogging.warn("Delete could not schedule chunk clear at " + chunkX + "," + chunkZ + " in " + world.getName() + "."); + reporter.countApplied(false); + inFlight.release(); + allCleared.countDown(); + } + } + + allCleared.await(); + } + + private void clearChunk(int chunkX, int chunkZ, Mantle mantle) { + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + for (Entity entity : chunk.getEntities()) { + if (!(entity instanceof Player)) { + entity.remove(); + } + } + + if (!INMS.get().clearChunkBlocks(chunk)) { + clearBlocks(chunk, chunk.getChunkSnapshot(true, false, false), world.getMinHeight(), world.getMaxHeight()); + } + + mantle.deleteChunk(chunkX, chunkZ); + world.refreshChunk(chunkX, chunkZ); + } + + static void clearBlocks(Chunk chunk, ChunkSnapshot snapshot, int minHeight, int maxHeight) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int top = Math.min(maxHeight - 1, snapshot.getHighestBlockYAt(x, z) + 1); + for (int y = minHeight; y <= top; y++) { + if (snapshot.getBlockType(x, y, z) != Material.AIR) { + chunk.getBlock(x, y, z).setType(Material.AIR, false); + } + } + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/ChunkJobReporter.java b/core/src/main/java/art/arcane/iris/core/runtime/ChunkJobReporter.java new file mode 100644 index 000000000..49884c194 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/ChunkJobReporter.java @@ -0,0 +1,218 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.math.ChunkSpiral; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.localization.MessageArgument; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +public final class ChunkJobReporter { + private static final int PROGRESS_BAR_WIDTH = 24; + private static final int REPORT_INTERVAL_TICKS = 5; + private static final int FINISH_LINGER_TICKS = 60; + + private final VolmitSender sender; + private final String title; + private final String worldName; + + private final AtomicReference stage = new AtomicReference<>( + IrisLanguage.text(RuntimeProgressMessages.CHUNK_STAGE_PREPARING) + ); + private final AtomicReference progress = new AtomicReference<>(0.0D); + private final AtomicBoolean complete = new AtomicBoolean(false); + private final AtomicBoolean failed = new AtomicBoolean(false); + private final AtomicInteger applied = new AtomicInteger(0); + private final AtomicInteger failures = new AtomicInteger(0); + private volatile int total = 0; + private volatile long startMs = 0L; + + public ChunkJobReporter(VolmitSender sender, String title, World world) { + this.sender = sender; + this.title = title; + this.worldName = world.getName(); + } + + public static List orderedTargets(int centerChunkX, int centerChunkZ, int radius) { + return ChunkSpiral.centerOut(centerChunkX, centerChunkZ, radius); + } + + public void start() { + startMs = System.currentTimeMillis(); + startReporter(); + } + + public void setStage(String value) { + stage.set(value); + } + + public void setTotal(int value) { + total = value; + } + + public void countApplied(boolean ok) { + if (ok) { + applied.incrementAndGet(); + } else { + failures.incrementAndGet(); + } + + int finished = applied.get() + failures.get(); + if (total > 0) { + progress.set(Math.min(0.999D, (double) finished / total)); + } + } + + public void finish(boolean error) { + progress.set(1.0D); + failed.set(error || (applied.get() == 0 && total > 0)); + complete.set(true); + } + + public int applied() { + return applied.get(); + } + + public int failures() { + return failures.get(); + } + + private void startReporter() { + boolean showBossBar = sender.isPlayer() && sender.player() != null + && IrisSettings.get().getGeneral().isProgressBossBar(); + BossBar bossBar = showBossBar + ? Bukkit.createBossBar(IrisLanguage.text( + RuntimeProgressMessages.CHUNK_BOSSBAR_WORKING, + MessageArgument.trusted("title", title) + ), BarColor.BLUE, BarStyle.SEGMENTED_20) + : null; + if (bossBar != null) { + bossBar.setProgress(0.0D); + bossBar.addPlayer(sender.player()); + bossBar.setVisible(true); + } + AtomicInteger taskId = new AtomicInteger(-1); + taskId.set(J.ar(() -> { + double currentProgress = Math.max(0.0D, Math.min(1.0D, progress.get())); + int percent = (int) Math.round(currentProgress * 100.0D); + long elapsed = System.currentTimeMillis() - startMs; + + if (complete.get()) { + J.car(taskId.get()); + finishReporter(bossBar, elapsed); + return; + } + + if (bossBar != null) { + bossBar.setProgress(Math.min(1.0D, currentProgress)); + bossBar.setTitle(IrisLanguage.text( + RuntimeProgressMessages.CHUNK_BOSSBAR_PROGRESS, + MessageArgument.trusted("title", title), + MessageArgument.trusted("stage", stage.get()), + MessageArgument.trusted("percent", percent) + )); + } + if (sender.isPlayer()) { + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.CHUNK_ACTION_PROGRESS, + MessageArgument.trusted("bar", progressBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", stage.get()), + MessageArgument.trusted("applied", applied.get()), + MessageArgument.trusted("total", total <= 0 ? "?" : total) + )); + } + }, REPORT_INTERVAL_TICKS)); + if (complete.get()) { + J.car(taskId.get()); + } + } + + private void finishReporter(BossBar bossBar, long elapsed) { + boolean ok = !failed.get(); + String summary = failures.get() > 0 + ? IrisLanguage.text( + RuntimeProgressMessages.CHUNK_SUMMARY_FAILED, + MessageArgument.trusted("applied", applied.get()), + MessageArgument.trusted("total", total), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)), + MessageArgument.trusted("failures", failures.get()) + ) + : IrisLanguage.text( + RuntimeProgressMessages.CHUNK_SUMMARY, + MessageArgument.trusted("applied", applied.get()), + MessageArgument.trusted("total", total), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + ); + + if (bossBar != null) { + bossBar.setProgress(1.0D); + bossBar.setColor(ok ? BarColor.GREEN : BarColor.RED); + bossBar.setTitle(IrisLanguage.text( + ok ? RuntimeProgressMessages.CHUNK_BOSSBAR_DONE : RuntimeProgressMessages.CHUNK_BOSSBAR_FAILED, + MessageArgument.trusted("title", title), + MessageArgument.trusted("summary", summary) + )); + J.a(() -> { + bossBar.removeAll(); + bossBar.setVisible(false); + }, FINISH_LINGER_TICKS); + } + + if (sender.isPlayer()) { + sender.sendAction(IrisLanguage.text( + ok ? RuntimeProgressMessages.CHUNK_ACTION_DONE : RuntimeProgressMessages.CHUNK_ACTION_FAILED, + MessageArgument.trusted("bar", progressBar(1.0D)), + MessageArgument.trusted("summary", summary) + )); + } + sender.sendMessage(IrisLanguage.text( + ok ? RuntimeProgressMessages.CHUNK_COMPLETE : RuntimeProgressMessages.CHUNK_FAILED, + MessageArgument.trusted("title", title), + MessageArgument.trusted("summary", summary) + )); + IrisLogging.info(title + " done: world=" + worldName + " " + summary); + } + + private String progressBar(double value) { + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, value)) * PROGRESS_BAR_WIDTH); + StringBuilder bar = new StringBuilder(C.DARK_GRAY + "["); + for (int i = 0; i < PROGRESS_BAR_WIDTH; i++) { + bar.append(i < filled ? C.GREEN + "|" : C.DARK_GRAY + "|"); + } + bar.append(C.DARK_GRAY).append("]"); + return bar.toString(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashEngine.java b/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashEngine.java new file mode 100644 index 000000000..d540de662 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashEngine.java @@ -0,0 +1,565 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.ChunkSpiral; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.mantle.runtime.Mantle; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; + +public final class GoldenHashEngine { + public static final String FALLBACK_BIOME_KEY = "minecraft:plains"; + + public enum Mode { + AUTO, + CAPTURE, + VERIFY + } + + public interface ChunkSnapshot { + int minY(); + + int maxY(); + + PlatformBlockState block(int x, int y, int z); + + PlatformBiome biome(int x, int y, int z); + } + + public interface ChunkSource { + ChunkSnapshot generate(int chunkX, int chunkZ) throws Exception; + } + + public interface Feedback { + void ok(String message); + + void warn(String message); + + void fail(String message); + } + + public interface Progress { + default void stage(String stage) { + } + + default void total(int total) { + } + + default void chunkDone(int chunkX, int chunkZ, boolean ok, int done, int total) { + } + + default void chunkFailed(int chunkX, int chunkZ, Throwable error) { + } + } + + public record Request(String worldName, long seed, String mcVersion, int minY, int maxY, int centerChunkX, + int centerChunkZ, int radius, int threads, Mode mode, boolean resetMantle, boolean deep, + String nullBiomeKey) { + } + + private static final AtomicInteger ACTIVE_SCANS = new AtomicInteger(0); + private static final String FORMAT = "iris-goldenhash v1"; + private static final int BIOME_STEP = 4; + private static final int MAX_REPORTED_MISMATCHES = 10; + + private final Engine engine; + private final Request request; + private final ChunkSource source; + private final Feedback feedback; + private final Progress progress; + private final int radius; + private final int threads; + private final File goldenFile; + + public GoldenHashEngine(Engine engine, Request request, File goldenDir, ChunkSource source, Feedback feedback, Progress progress) { + this.engine = engine; + this.request = request; + this.source = source; + this.feedback = feedback; + this.progress = progress; + this.radius = Math.max(0, request.radius()); + this.threads = Math.max(1, request.threads()); + goldenDir.mkdirs(); + this.goldenFile = new File(goldenDir, engine.getDimension().getLoadKey() + + "-s" + request.seed() + + "-c" + request.centerChunkX() + "x" + request.centerChunkZ() + + "-r" + this.radius + ".hashes"); + } + + public static boolean isActive() { + return ACTIVE_SCANS.get() > 0; + } + + public File getGoldenFile() { + return goldenFile; + } + + public boolean run() { + ACTIVE_SCANS.incrementAndGet(); + try { + boolean exists = goldenFile.exists(); + if (request.mode() == Mode.VERIFY && !exists) { + feedback.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_NO_CAPTURE, + MessageArgument.untrusted("path", goldenFile.getAbsolutePath()) + )); + return false; + } + + if (request.resetMantle()) { + progress.stage(IrisLanguage.plain(RuntimeProgressMessages.CHUNK_STAGE_RESETTING_MANTLE)); + if (!resetMantleFull()) { + // A partial reset silently invalidates the capture; never scan over it. + return false; + } + } + + List targets = ChunkSpiral.centerOut(request.centerChunkX(), request.centerChunkZ(), radius); + progress.total(targets.size()); + progress.stage(IrisLanguage.plain(RuntimeProgressMessages.CHUNK_STAGE_GENERATING)); + Map lines = scan(targets); + + if (lines.size() != targets.size()) { + feedback.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_ABORTED, + MessageArgument.trusted("failed", targets.size() - lines.size()) + )); + return false; + } + + progress.stage(IrisLanguage.plain(RuntimeProgressMessages.CHUNK_STAGE_COMPARING)); + if (request.mode() == Mode.CAPTURE || (request.mode() == Mode.AUTO && !exists)) { + capture(lines); + return true; + } + + return verify(lines); + } catch (Throwable e) { + IrisLogging.reportError(e); + feedback.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_FAILED, + MessageArgument.untrusted("error", String.valueOf(e)) + )); + return false; + } finally { + ACTIVE_SCANS.decrementAndGet(); + } + } + + private boolean resetMantleFull() { + Mantle mantle = engine.getMantle().getMantle(); + try { + // resetStorage invalidates the IOWorker channel cache before unlinking; deleting + // behind cached channels sent every post-reset plate write to an unlinked inode. + mantle.resetStorage(); + feedback.ok(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_MANTLE_RESET, + MessageArgument.untrusted("path", mantle.getDataFolder().getAbsolutePath()) + )); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + feedback.warn(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_MANTLE_RESET_FAILED, + MessageArgument.untrusted("type", e.getClass().getSimpleName()) + )); + return false; + } + } + + private Map scan(List targets) throws InterruptedException { + Map lines = new ConcurrentHashMap<>(); + Semaphore inFlight = new Semaphore(threads); + CountDownLatch done = new CountDownLatch(targets.size()); + AtomicInteger completed = new AtomicInteger(); + int total = targets.size(); + + for (int[] target : targets) { + int chunkX = target[0]; + int chunkZ = target[1]; + + inFlight.acquire(); + MultiBurst.burst.lazy(() -> { + boolean ok = false; + try { + ChunkSnapshot snapshot = source.generate(chunkX, chunkZ); + lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, snapshot)); + if (request.deep()) { + writeDeepDump(chunkX, chunkZ, snapshot); + } + ok = true; + } catch (Throwable e) { + IrisLogging.reportError(e); + progress.chunkFailed(chunkX, chunkZ, e); + } finally { + progress.chunkDone(chunkX, chunkZ, ok, completed.incrementAndGet(), total); + inFlight.release(); + done.countDown(); + } + }); + } + + done.await(); + return lines; + } + + private String hashChunk(int chunkX, int chunkZ, ChunkSnapshot snapshot) { + MessageDigest blockDigest = sha256(); + MessageDigest biomeDigest = sha256(); + int minY = snapshot.minY(); + int maxY = snapshot.maxY(); + IdentityHashMap blockCache = new IdentityHashMap<>(); + Map biomeCache = new HashMap<>(); + byte[] nullBiome = (request.nullBiomeKey() + "\n").getBytes(StandardCharsets.UTF_8); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = minY; y < maxY; y++) { + PlatformBlockState data = snapshot.block(x, y, z); + byte[] bytes = blockCache.computeIfAbsent(data, (PlatformBlockState d) -> (d.key() + "\n").getBytes(StandardCharsets.UTF_8)); + blockDigest.update(bytes); + } + } + } + + for (int x = 0; x < 16; x += BIOME_STEP) { + for (int z = 0; z < 16; z += BIOME_STEP) { + for (int y = minY; y < maxY; y += BIOME_STEP) { + PlatformBiome biome = snapshot.biome(x, y, z); + byte[] bytes = biome == null + ? nullBiome + : biomeCache.computeIfAbsent(biome, (PlatformBiome b) -> (b.key() + "\n").getBytes(StandardCharsets.UTF_8)); + biomeDigest.update(bytes); + } + } + } + + return chunkX + " " + chunkZ + " " + + HexFormat.of().formatHex(blockDigest.digest()) + " " + + HexFormat.of().formatHex(biomeDigest.digest()); + } + + private void capture(Map lines) throws IOException { + List body = orderedBody(lines); + String combined = combinedHash(body); + List out = new ArrayList<>(); + out.add("#" + FORMAT); + out.add("#world=" + request.worldName()); + out.add("#dim=" + engine.getDimension().getLoadKey()); + out.add("#seed=" + request.seed()); + out.add("#mc=" + request.mcVersion()); + out.add("#minY=" + request.minY() + " maxY=" + request.maxY()); + out.add("#center=" + request.centerChunkX() + "," + request.centerChunkZ()); + out.add("#radius=" + radius); + out.addAll(body); + out.add("#combined=" + combined); + Files.write(goldenFile.toPath(), out, StandardCharsets.UTF_8); + + feedback.ok(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_CAPTURED, + MessageArgument.trusted("chunks", body.size()), + MessageArgument.trusted("hash", shortHash(combined)) + )); + feedback.ok(goldenFile.getAbsolutePath()); + IrisLogging.info("goldenhash captured: " + goldenFile.getAbsolutePath() + " combined=" + combined); + } + + private boolean verify(Map lines) throws IOException { + List existing = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8); + Map meta = new HashMap<>(); + Map goldenChunks = new HashMap<>(); + for (String line : existing) { + if (line.startsWith("#")) { + int eq = line.indexOf('='); + if (eq > 0) { + meta.put(line.substring(1, eq), line.substring(eq + 1)); + } + } else if (!line.isBlank()) { + int second = line.indexOf(' ', line.indexOf(' ') + 1); + goldenChunks.put(line.substring(0, second), line); + } + } + + String expectedSeed = String.valueOf(request.seed()); + String expectedDim = engine.getDimension().getLoadKey(); + if (!expectedSeed.equals(meta.get("seed")) || !expectedDim.equals(meta.get("dim"))) { + feedback.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_WRONG_WORLD, + MessageArgument.untrusted("goldenDimension", String.valueOf(meta.get("dim"))), + MessageArgument.untrusted("goldenSeed", String.valueOf(meta.get("seed"))), + MessageArgument.untrusted("dimension", expectedDim), + MessageArgument.untrusted("seed", expectedSeed) + )); + return false; + } + if (!request.mcVersion().equals(meta.get("mc"))) { + feedback.warn(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_VERSION_WARNING, + MessageArgument.untrusted("goldenVersion", String.valueOf(meta.get("mc"))), + MessageArgument.untrusted("version", request.mcVersion()) + )); + } + + List body = orderedBody(lines); + List mismatches = new ArrayList<>(); + // Display strings and diagnosis input are different shapes: diagnose() parses a bare + // " " key, and feeding it a localized sentence aborted the diagnosis whenever + // the FIRST mismatch was a chunk missing from the golden capture. + String firstMismatchKey = null; + for (String line : body) { + int second = line.indexOf(' ', line.indexOf(' ') + 1); + String key = line.substring(0, second); + String golden = goldenChunks.get(key); + if (!line.equals(golden)) { + if (firstMismatchKey == null) { + firstMismatchKey = key; + } + mismatches.add(golden == null + ? IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_MISSING_IN_GOLDEN, + MessageArgument.untrusted("chunk", key) + ) + : key); + } + } + + String combined = combinedHash(body); + if (mismatches.isEmpty()) { + feedback.ok(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_MATCH, + MessageArgument.trusted("current", body.size()), + MessageArgument.trusted("golden", goldenChunks.size()), + MessageArgument.trusted("hash", shortHash(combined)) + )); + IrisLogging.info("goldenhash MATCH: " + goldenFile.getName() + " combined=" + combined); + return true; + } + + feedback.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_MISMATCH, + MessageArgument.trusted("mismatches", mismatches.size()), + MessageArgument.trusted("chunks", body.size()) + )); + for (int i = 0; i < Math.min(MAX_REPORTED_MISMATCHES, mismatches.size()); i++) { + feedback.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_MISMATCH_CHUNK, + MessageArgument.untrusted("chunk", mismatches.get(i)) + )); + } + if (mismatches.size() > MAX_REPORTED_MISMATCHES) { + feedback.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_MISMATCH_MORE, + MessageArgument.trusted("count", mismatches.size() - MAX_REPORTED_MISMATCHES) + )); + } + + File current = new File(goldenFile.getParentFile(), goldenFile.getName() + ".new"); + List out = new ArrayList<>(body); + out.add("#combined=" + combined); + Files.write(current.toPath(), out, StandardCharsets.UTF_8); + feedback.warn(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_CURRENT_WRITTEN, + MessageArgument.untrusted("file", current.getName()) + )); + IrisLogging.info("goldenhash MISMATCH: " + mismatches.size() + "/" + body.size() + " -> " + current.getAbsolutePath()); + + progress.stage(IrisLanguage.plain(RuntimeProgressMessages.CHUNK_STAGE_DIAGNOSING)); + diagnose(firstMismatchKey); + return false; + } + + private void diagnose(String mismatchKey) { + try { + String[] parts = mismatchKey.trim().split(" "); + int chunkX = Integer.parseInt(parts[0]); + int chunkZ = Integer.parseInt(parts[1]); + + ChunkSnapshot first = source.generate(chunkX, chunkZ); + ChunkSnapshot second = source.generate(chunkX, chunkZ); + + int minY = first.minY(); + int maxY = first.maxY(); + List diffs = new ArrayList<>(); + for (int x = 0; x < 16 && diffs.size() < 50; x++) { + for (int z = 0; z < 16 && diffs.size() < 50; z++) { + for (int y = minY; y < maxY && diffs.size() < 50; y++) { + String a = first.block(x, y, z).key(); + String b = second.block(x, y, z).key(); + if (!a.equals(b)) { + diffs.add(x + " " + y + " " + z + " | " + a + " | " + b); + } + } + } + } + + List mantleDiffs = new ArrayList<>(); + String mantleStatus; + boolean mantleStable = false; + try { + EngineMantle engineMantle = engine.getMantle(); + int margin = Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1; + for (int dx = -margin; dx <= margin; dx++) { + for (int dz = -margin; dz <= margin; dz++) { + engineMantle.getMantle().deleteChunk(chunkX + dx, chunkZ + dz); + } + } + ChunkSnapshot reset = source.generate(chunkX, chunkZ); + for (int x = 0; x < 16 && mantleDiffs.size() < 80; x++) { + for (int z = 0; z < 16 && mantleDiffs.size() < 80; z++) { + for (int y = minY; y < maxY && mantleDiffs.size() < 80; y++) { + String a = first.block(x, y, z).key(); + String b = reset.block(x, y, z).key(); + if (!a.equals(b)) { + mantleDiffs.add(x + " " + y + " " + z + " | scan: " + a + " | mantle-reset: " + b); + } + } + } + } + mantleStable = mantleDiffs.isEmpty(); + mantleStatus = mantleStable + ? IrisLanguage.plain(RuntimeProgressMessages.GOLDEN_MANTLE_STABLE) + : IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_MANTLE_DIVERGED, + MessageArgument.trusted("diffs", mantleDiffs.size()) + ); + } catch (Throwable t) { + mantleDiffs.clear(); + mantleStatus = IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_MANTLE_SKIPPED, + MessageArgument.untrusted("type", t.getClass().getSimpleName()) + ); + } + + List report = new ArrayList<>(); + report.add("#goldenhash diagnosis chunk=" + chunkX + "," + chunkZ); + report.add("#repeat-generation: " + (diffs.isEmpty() ? "STABLE (nondeterminism is order/state-dependent, not per-call)" : "UNSTABLE (" + diffs.size() + "+ diffs between two back-to-back generations)")); + report.addAll(diffs); + report.add("#mantle-reset regeneration: " + mantleStatus); + report.addAll(mantleDiffs); + report.add("#full non-air dump of generation 1 follows (x y z state)"); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = minY; y < maxY; y++) { + String state = first.block(x, y, z).key(); + if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) { + report.add(x + " " + y + " " + z + " " + state); + } + } + } + } + + File diag = new File(goldenFile.getParentFile(), goldenFile.getName() + ".diag-c" + chunkX + "x" + chunkZ + ".txt"); + Files.write(diag.toPath(), report, StandardCharsets.UTF_8); + if (diffs.isEmpty() && mantleStable) { + feedback.warn(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_DIAG_STABLE, + MessageArgument.trusted("mantleStatus", mantleStatus), + MessageArgument.untrusted("file", diag.getName()) + )); + } else { + feedback.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_DIAG_UNSTABLE, + MessageArgument.trusted("diffs", diffs.size()), + MessageArgument.trusted("mantleStatus", mantleStatus), + MessageArgument.untrusted("file", diag.getName()) + )); + } + IrisLogging.info("goldenhash diag: chunk=" + chunkX + "," + chunkZ + " repeatStable=" + diffs.isEmpty() + " -> " + diag.getAbsolutePath()); + } catch (Throwable e) { + IrisLogging.reportError(e); + feedback.fail(IrisLanguage.plain( + RuntimeProgressMessages.GOLDEN_DIAG_FAILED, + MessageArgument.untrusted("error", String.valueOf(e.getMessage())) + )); + } + } + + private void writeDeepDump(int chunkX, int chunkZ, ChunkSnapshot snapshot) throws IOException { + File dir = new File(goldenFile.getParentFile(), goldenFile.getName() + (goldenFile.exists() ? ".deep-verify" : ".deep")); + dir.mkdirs(); + int minY = snapshot.minY(); + int maxY = snapshot.maxY(); + List out = new ArrayList<>(); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = minY; y < maxY; y++) { + String state = snapshot.block(x, y, z).key(); + if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) { + out.add(x + " " + y + " " + z + " " + state); + } + } + } + } + Files.write(new File(dir, chunkX + "_" + chunkZ + ".txt").toPath(), out, StandardCharsets.UTF_8); + } + + private List orderedBody(Map lines) { + Map sorted = new TreeMap<>(lines); + return new ArrayList<>(sorted.values()); + } + + private String combinedHash(List body) { + MessageDigest digest = sha256(); + for (String line : body) { + digest.update((line + "\n").getBytes(StandardCharsets.UTF_8)); + } + return HexFormat.of().formatHex(digest.digest()); + } + + private static String shortHash(String hex) { + return hex.substring(0, 12); + } + + private static long chunkKey(int x, int z) { + return (((long) x) << 32) ^ (z & 0xFFFFFFFFL); + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java b/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java new file mode 100644 index 000000000..ed815ee9a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java @@ -0,0 +1,145 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import org.bukkit.Bukkit; +import org.bukkit.World; + +public final class GoldenHashScanner { + private final World world; + private final Engine engine; + private final VolmitSender sender; + private final ChunkJobReporter reporter; + private final GoldenHashEngine hashEngine; + + public GoldenHashScanner(World world, Engine engine, VolmitSender sender, int centerChunkX, int centerChunkZ, int radius, boolean resetMantle, int threads, boolean deep) { + this.world = world; + this.engine = engine; + this.sender = sender; + this.reporter = new ChunkJobReporter(sender, IrisLanguage.text(RuntimeProgressMessages.CHUNK_TITLE_GOLDEN_HASH), world); + GoldenHashEngine.Request request = new GoldenHashEngine.Request( + world.getName(), + world.getSeed(), + Bukkit.getBukkitVersion(), + world.getMinHeight(), + world.getMaxHeight(), + centerChunkX, + centerChunkZ, + radius, + threads, + GoldenHashEngine.Mode.AUTO, + resetMantle, + deep, + GoldenHashEngine.FALLBACK_BIOME_KEY); + this.hashEngine = new GoldenHashEngine(engine, request, IrisPlatforms.get().dataFolder("golden"), this::snapshot, feedback(), progress()); + } + + public static boolean isScanActive() { + return GoldenHashEngine.isActive(); + } + + public void start() { + reporter.start(); + Thread thread = new Thread(this::run, "Iris GoldenHash"); + thread.setDaemon(true); + thread.start(); + } + + private void run() { + boolean ok = false; + try { + ok = hashEngine.run(); + } finally { + reporter.finish(!ok); + } + } + + private GoldenHashEngine.ChunkSnapshot snapshot(int chunkX, int chunkZ) throws Exception { + TerrainChunk buffer = TerrainChunk.create(world); + engine.generate(chunkX << 4, chunkZ << 4, buffer, false); + return new GoldenHashEngine.ChunkSnapshot() { + @Override + public int minY() { + return buffer.getMinHeight(); + } + + @Override + public int maxY() { + return buffer.getMaxHeight(); + } + + @Override + public PlatformBlockState block(int x, int y, int z) { + return buffer.getBlockData(x, y, z); + } + + @Override + public PlatformBiome biome(int x, int y, int z) { + return buffer.getBiome(x, y, z); + } + }; + } + + private GoldenHashEngine.Feedback feedback() { + return new GoldenHashEngine.Feedback() { + @Override + public void ok(String message) { + sender.sendMessage(C.GREEN + message); + } + + @Override + public void warn(String message) { + sender.sendMessage(C.YELLOW + message); + } + + @Override + public void fail(String message) { + sender.sendMessage(C.RED + message); + } + }; + } + + private GoldenHashEngine.Progress progress() { + return new GoldenHashEngine.Progress() { + @Override + public void stage(String stage) { + reporter.setStage(stage); + } + + @Override + public void total(int total) { + reporter.setTotal(total); + } + + @Override + public void chunkDone(int chunkX, int chunkZ, boolean ok, int done, int total) { + reporter.countApplied(ok); + } + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/InPlaceChunkRegenerator.java b/core/src/main/java/art/arcane/iris/core/runtime/InPlaceChunkRegenerator.java new file mode 100644 index 000000000..bc0ebabb1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/InPlaceChunkRegenerator.java @@ -0,0 +1,234 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.spi.PlatformBiome; +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.mantle.runtime.Mantle; +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.generator.ChunkGenerator.ChunkData; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public final class InPlaceChunkRegenerator { + private static final int BIOME_STEP = 4; + private static final int APPLY_AHEAD = 8; + + private final World world; + private final Engine engine; + private final int centerChunkX; + private final int centerChunkZ; + private final int radius; + private final ChunkJobReporter reporter; + + public InPlaceChunkRegenerator(World world, Engine engine, VolmitSender sender, int centerChunkX, int centerChunkZ, int radius) { + this.world = world; + this.engine = engine; + this.centerChunkX = centerChunkX; + this.centerChunkZ = centerChunkZ; + this.radius = Math.max(0, radius); + this.reporter = new ChunkJobReporter(sender, IrisLanguage.text(RuntimeProgressMessages.CHUNK_TITLE_REGEN), world); + } + + public void start() { + reporter.start(); + Thread thread = new Thread(this::run, "Iris Regenerate"); + thread.setDaemon(true); + thread.start(); + } + + private void run() { + boolean error = false; + try { + reporter.setStage(IrisLanguage.text(RuntimeProgressMessages.CHUNK_STAGE_RESETTING_MANTLE)); + resetMantleMargin(); + + List targets = ChunkJobReporter.orderedTargets(centerChunkX, centerChunkZ, radius); + reporter.setTotal(targets.size()); + reporter.setStage(IrisLanguage.text(RuntimeProgressMessages.CHUNK_STAGE_REGENERATING)); + regenerate(targets); + } catch (Throwable e) { + IrisLogging.reportError(e); + error = true; + } finally { + reporter.finish(error); + } + } + + private void resetMantleMargin() { + EngineMantle engineMantle = engine.getMantle(); + Mantle mantle = engineMantle.getMantle(); + int margin = radius + Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1; + for (int dx = -margin; dx <= margin; dx++) { + for (int dz = -margin; dz <= margin; dz++) { + mantle.deleteChunk(centerChunkX + dx, centerChunkZ + dz); + } + } + } + + private void regenerate(List targets) throws InterruptedException { + Semaphore inFlight = new Semaphore(APPLY_AHEAD); + CountDownLatch allApplied = new CountDownLatch(targets.size()); + + for (int[] target : targets) { + int chunkX = target[0]; + int chunkZ = target[1]; + + inFlight.acquire(); + MultiBurst.burst.lazy(() -> { + // Settle exactly once no matter which path fails: an unguarded throw here + // (TerrainChunk.create, runRegion) parked the regen thread on the latch + // forever and stranded the player's boss bar. CAS is required — on Folia's + // owned-region path runRegion runs the apply inline before returning. + AtomicBoolean settled = new AtomicBoolean(); + Consumer settle = ok -> { + if (settled.compareAndSet(false, true)) { + reporter.countApplied(ok); + inFlight.release(); + allApplied.countDown(); + } + }; + try { + TerrainChunk buffer = TerrainChunk.create(world); + engine.generate(chunkX << 4, chunkZ << 4, buffer, false); + + boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> { + boolean ok = false; + try { + applyToLiveChunk(chunkX, chunkZ, buffer); + ok = true; + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + settle.accept(ok); + } + }); + + if (!scheduled) { + IrisLogging.warn("Regen could not schedule chunk apply at " + chunkX + "," + chunkZ + " in " + world.getName() + "."); + settle.accept(false); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + settle.accept(false); + } + }); + } + + allApplied.await(); + } + + private void applyToLiveChunk(int chunkX, int chunkZ, TerrainChunk buffer) { + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + for (Entity entity : chunk.getEntities()) { + if (!(entity instanceof Player)) { + entity.remove(); + } + } + + int minHeight = world.getMinHeight(); + int maxHeight = world.getMaxHeight(); + if (!INMS.get().applyChunkBlocks(chunk, buffer)) { + applyBlockDiffs(chunk, chunk.getChunkSnapshot(false, false, false), buffer.getChunkData(), minHeight, maxHeight); + } + + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + for (int x = 0; x < 16; x += BIOME_STEP) { + for (int z = 0; z < 16; z += BIOME_STEP) { + for (int y = minHeight; y < maxHeight; y += BIOME_STEP) { + PlatformBiome biome = buffer.getBiome(x, y, z); + if (biome != null) { + Biome bukkitBiome = (Biome) biome.nativeHandle(); + if (world.getBiome(baseX + x, y, baseZ + z) != bukkitBiome) { + world.setBiome(baseX + x, y, baseZ + z, bukkitBiome); + } + } + } + } + } + + EngineBukkitOps.updateChunk(engine, chunk); + world.refreshChunk(chunkX, chunkZ); + } + + public static void applyBlockDiffs( + Chunk chunk, + ChunkData generated, + int minHeight, + int maxHeight + ) { + applyBlockDiffs( + chunk, + chunk.getChunkSnapshot(false, false, false), + generated, + minHeight, + maxHeight); + } + + public static void applyBlockDiffs( + Chunk chunk, + ChunkSnapshot snapshot, + ChunkData generated, + int minHeight, + int maxHeight + ) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = minHeight; y < maxHeight; y++) { + Material target = generated.getType(x, y, z); + if (snapshot.getBlockType(x, y, z) != target) { + chunk.getBlock(x, y, z).setBlockData(generated.getBlockData(x, y, z), false); + continue; + } + + if (target == Material.AIR) { + continue; + } + + BlockData data = generated.getBlockData(x, y, z); + if (!data.equals(snapshot.getBlockData(x, y, z))) { + chunk.getBlock(x, y, z).setBlockData(data, false); + } + } + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioActivation.java b/core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioActivation.java new file mode 100644 index 000000000..c28475569 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioActivation.java @@ -0,0 +1,67 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.loader.IrisData; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class ObjectStudioActivation { + private static final Set ACTIVE = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Map> SOURCES = new ConcurrentHashMap<>(); + + private ObjectStudioActivation() { + } + + public static void activate(String packKey) { + if (packKey == null) return; + ACTIVE.add(normalize(packKey)); + } + + public static void deactivate(String packKey) { + if (packKey == null) return; + String norm = normalize(packKey); + ACTIVE.remove(norm); + SOURCES.remove(norm); + } + + public static boolean isActive(String packKey) { + if (packKey == null) return false; + return ACTIVE.contains(normalize(packKey)); + } + + public static void setSources(String packKey, Map sources) { + if (packKey == null || sources == null || sources.isEmpty()) return; + SOURCES.put(normalize(packKey), new LinkedHashMap<>(sources)); + } + + public static Map getSources(String packKey) { + if (packKey == null) return null; + return SOURCES.get(normalize(packKey)); + } + + private static String normalize(String key) { + return key.toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioLayout.java b/core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioLayout.java new file mode 100644 index 000000000..f3669c505 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioLayout.java @@ -0,0 +1,258 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.iris.util.common.math.IrisBlockVector; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class ObjectStudioLayout { + public static final int FLOOR_Y = 64; + private static final int DEFAULT_ROW_WIDTH_CAP = 160; + + private final int padding; + private final int rowWidthCap; + private final List cells; + private final Map byKey; + + private ObjectStudioLayout(int padding, int rowWidthCap, List cells) { + this.padding = padding; + this.rowWidthCap = rowWidthCap; + this.cells = Collections.unmodifiableList(cells); + Map index = new LinkedHashMap<>(); + for (GridCell cell : cells) { + index.putIfAbsent(cell.key(), cell); + index.putIfAbsent(cell.pack() + "/" + cell.key(), cell); + } + this.byKey = Collections.unmodifiableMap(index); + } + + public static ObjectStudioLayout build(IrisData data, int padding) { + Map sources = new LinkedHashMap<>(); + sources.put(data.getDataFolder().getName(), data); + return build(sources, padding, DEFAULT_ROW_WIDTH_CAP); + } + + public static ObjectStudioLayout build(Map sources, int padding) { + return build(sources, padding, DEFAULT_ROW_WIDTH_CAP); + } + + public static ObjectStudioLayout build(Map sources, int padding, int rowWidthCap) { + List entries = new ArrayList<>(); + for (Map.Entry entry : sources.entrySet()) { + String packName = entry.getKey(); + IrisData data = entry.getValue(); + String[] possible = data.getObjectLoader().getPossibleKeys(); + if (possible == null) continue; + List sorted = new ArrayList<>(Arrays.asList(possible)); + Collections.sort(sorted); + for (String key : sorted) { + entries.add(new PackEntry(packName, data, key)); + } + } + entries.sort((a, b) -> { + int c = a.pack.compareTo(b.pack); + if (c != 0) return c; + return a.key.compareTo(b.key); + }); + + List packed = new ArrayList<>(entries.size()); + int cursorX = 0; + int rowZ = 0; + int rowMaxDepth = 0; + + for (PackEntry entry : entries) { + File file = entry.data.getObjectLoader().findFile(entry.key); + if (file == null) { + continue; + } + IrisBlockVector size; + try { + size = IrisObject.sampleSize(file); + } catch (Throwable e) { + IrisLogging.reportError(e); + continue; + } + + int w = Math.max(1, size.getBlockX()); + int h = Math.max(1, size.getBlockY()); + int d = Math.max(1, size.getBlockZ()); + + int cellWidth = w + padding * 2; + int cellDepth = d + padding * 2; + + if (cursorX > 0 && cursorX + cellWidth > rowWidthCap) { + rowZ += rowMaxDepth; + cursorX = 0; + rowMaxDepth = 0; + } + + int originX = cursorX + padding; + int originZ = rowZ + padding; + int originY = FLOOR_Y + 1; + + packed.add(new GridCell(entry.pack, entry.key, originX, originY, originZ, w, h, d)); + + cursorX += cellWidth; + rowMaxDepth = Math.max(rowMaxDepth, cellDepth); + } + + return new ObjectStudioLayout(padding, rowWidthCap, packed); + } + + public static ObjectStudioLayout load(File file, Map sources, int padding) { + if (file == null || !file.exists()) { + return null; + } + try { + String raw = Files.readString(file.toPath()); + JSONObject root = new JSONObject(raw); + int storedPadding = root.optInt("padding", padding); + int storedCap = root.optInt("rowWidthCap", DEFAULT_ROW_WIDTH_CAP); + JSONArray arr = root.getJSONArray("cells"); + + List stored = new ArrayList<>(); + Set storedIds = new HashSet<>(); + for (int i = 0; i < arr.length(); i++) { + JSONObject c = arr.getJSONObject(i); + String pack = c.optString("pack", null); + if (pack == null || pack.isEmpty()) { + return null; + } + GridCell cell = new GridCell( + pack, + c.getString("key"), + c.getInt("x"), + c.getInt("y"), + c.getInt("z"), + c.getInt("w"), + c.getInt("h"), + c.getInt("d") + ); + stored.add(cell); + storedIds.add(cell.pack() + "/" + cell.key()); + } + + Set liveIds = new HashSet<>(); + for (Map.Entry entry : sources.entrySet()) { + String[] live = entry.getValue().getObjectLoader().getPossibleKeys(); + if (live == null) continue; + for (String k : live) { + liveIds.add(entry.getKey() + "/" + k); + } + } + + if (liveIds.size() == storedIds.size() && liveIds.containsAll(storedIds)) { + return new ObjectStudioLayout(storedPadding, storedCap, stored); + } + return null; + } catch (Throwable e) { + IrisLogging.reportError(e); + return null; + } + } + + public void save(File file) { + if (file == null) { + return; + } + try { + if (file.getParentFile() != null) { + file.getParentFile().mkdirs(); + } + JSONObject root = new JSONObject(); + root.put("padding", padding); + root.put("rowWidthCap", rowWidthCap); + JSONArray arr = new JSONArray(); + for (GridCell cell : cells) { + JSONObject c = new JSONObject(); + c.put("pack", cell.pack()); + c.put("key", cell.key()); + c.put("x", cell.originX()); + c.put("y", cell.originY()); + c.put("z", cell.originZ()); + c.put("w", cell.w()); + c.put("h", cell.h()); + c.put("d", cell.d()); + arr.put(c); + } + root.put("cells", arr); + Files.writeString(file.toPath(), root.toString(2)); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + public int padding() { + return padding; + } + + public List cells() { + return cells; + } + + public GridCell findAt(int worldX, int worldZ) { + for (GridCell cell : cells) { + if (worldX >= cell.originX() && worldX < cell.originX() + cell.w() + && worldZ >= cell.originZ() && worldZ < cell.originZ() + cell.d()) { + return cell; + } + } + return null; + } + + public GridCell get(String key) { + return byKey.get(key); + } + + private record PackEntry(String pack, IrisData data, String key) { + } + + public record GridCell(String pack, String key, int originX, int originY, int originZ, int w, int h, int d) { + public int chunkMinX() { + return originX >> 4; + } + + public int chunkMaxX() { + return (originX + w - 1) >> 4; + } + + public int chunkMinZ() { + return originZ >> 4; + } + + public int chunkMaxZ() { + return (originZ + d - 1) >> 4; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/PaperLikeRuntimeControlBackend.java b/core/src/main/java/art/arcane/iris/core/runtime/PaperLikeRuntimeControlBackend.java new file mode 100644 index 000000000..377113068 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/PaperLikeRuntimeControlBackend.java @@ -0,0 +1,319 @@ +package art.arcane.iris.core.runtime; + +import art.arcane.iris.core.lifecycle.CapabilitySnapshot; +import io.papermc.lib.PaperLib; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.lang.reflect.Method; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +final class PaperLikeRuntimeControlBackend implements WorldRuntimeControlBackend { + private final CapabilitySnapshot capabilities; + private final AtomicReference timeAccessStrategy; + + PaperLikeRuntimeControlBackend(CapabilitySnapshot capabilities) { + this.capabilities = capabilities; + this.timeAccessStrategy = new AtomicReference<>(); + } + + @Override + public String backendName() { + return "paper_like_runtime"; + } + + @Override + public String describeCapabilities() { + TimeAccessStrategy strategy = timeAccessStrategy.get(); + String timeAccess = strategy == null ? "deferred" : strategy.description(); + String chunkAsync = capabilities.chunkAtAsyncMethod() != null ? "world#getChunkAtAsync" : "paperlib"; + return "time=" + timeAccess + ", chunkAsync=" + chunkAsync + ", teleport=entity_scheduler"; + } + + @Override + public OptionalLong readDayTime(World world) { + if (world == null) { + return OptionalLong.empty(); + } + + TimeAccessStrategy strategy = resolveTimeAccessStrategy(world); + if (strategy == null) { + return OptionalLong.empty(); + } + + try { + Object handle = strategy.handleMethod().invoke(world); + if (handle == null) { + return OptionalLong.empty(); + } + + Object value = strategy.readMethod().invoke(strategy.readOwner(handle), strategy.readArguments(handle)); + if (value instanceof Long longValue) { + return OptionalLong.of(longValue.longValue()); + } + if (value instanceof Number number) { + return OptionalLong.of(number.longValue()); + } + } catch (Throwable ignored) { + } + + return OptionalLong.empty(); + } + + @Override + public boolean writeDayTime(World world, long dayTime) throws ReflectiveOperationException { + if (world == null) { + return false; + } + + TimeAccessStrategy strategy = resolveTimeAccessStrategy(world); + if (strategy == null || !strategy.writable()) { + return false; + } + + Object handle = strategy.handleMethod().invoke(world); + if (handle == null) { + return false; + } + + Object writeOwner = strategy.writeOwner(handle); + if (writeOwner == null) { + return false; + } + + strategy.writeMethod().invoke(writeOwner, dayTime); + return true; + } + + @Override + public void syncTime(World world) { + TimeAccessStrategy strategy = timeAccessStrategy.get(); + if (strategy == null || strategy.syncMethod() == null) { + return; + } + + try { + Object craftServer = Bukkit.getServer(); + if (craftServer == null) { + return; + } + + Object serverHandle = strategy.serverHandleMethod() == null ? null : strategy.serverHandleMethod().invoke(craftServer); + if (serverHandle == null) { + return; + } + + strategy.syncMethod().invoke(serverHandle); + } catch (Throwable ignored) { + } + } + + @Override + public CompletableFuture requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) { + if (world == null) { + return CompletableFuture.failedFuture(new IllegalStateException("World is null.")); + } + + if (capabilities.chunkAtAsyncMethod() != null) { + try { + Object result = capabilities.chunkAtAsyncMethod().invoke(world, chunkX, chunkZ, generate); + if (result instanceof CompletableFuture) { + @SuppressWarnings("unchecked") + CompletableFuture future = (CompletableFuture) result; + return future; + } + } catch (Throwable e) { + return CompletableFuture.failedFuture(e); + } + } + + CompletableFuture future = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate); + if (future == null) { + return CompletableFuture.failedFuture(new IllegalStateException("PaperLib did not return a chunk future.")); + } + + return future; + } + + private TimeAccessStrategy resolveTimeAccessStrategy(World world) { + TimeAccessStrategy current = timeAccessStrategy.get(); + if (current != null) { + return current; + } + + synchronized (timeAccessStrategy) { + current = timeAccessStrategy.get(); + if (current != null) { + return current; + } + + TimeAccessStrategy resolved = probeTimeAccessStrategy(world); + timeAccessStrategy.set(resolved); + return resolved; + } + } + + private TimeAccessStrategy probeTimeAccessStrategy(World world) { + if (world == null) { + return TimeAccessStrategy.unsupported(); + } + + try { + Method handleMethod = resolveZeroArgMethod(world.getClass(), "getHandle"); + if (handleMethod == null) { + return TimeAccessStrategy.unsupported(); + } + + Object handle = handleMethod.invoke(world); + if (handle == null) { + return TimeAccessStrategy.unsupported(); + } + + Method readMethod = resolveZeroArgMethod(handle.getClass(), "getDayTime"); + Method writeMethod = resolveLongArgMethod(handle.getClass(), "setDayTime"); + if (readMethod != null && writeMethod != null) { + return TimeAccessStrategy.forHandle(handleMethod, readMethod, writeMethod, "runtime_handle#setDayTime"); + } + + Method levelDataMethod = resolveZeroArgMethod(handle.getClass(), "serverLevelData"); + if (levelDataMethod == null) { + levelDataMethod = resolveZeroArgMethod(handle.getClass(), "getLevelData"); + } + if (levelDataMethod != null) { + Object levelData = levelDataMethod.invoke(handle); + if (levelData != null) { + Method levelDataReadMethod = resolveZeroArgMethod(levelData.getClass(), "getDayTime"); + Method levelDataWriteMethod = resolveLongArgMethod(levelData.getClass(), "setDayTime"); + if (levelDataReadMethod != null && levelDataWriteMethod != null) { + return TimeAccessStrategy.forLevelData(handleMethod, levelDataMethod, levelDataReadMethod, levelDataWriteMethod, "world_data#setDayTime"); + } + } + } + + return TimeAccessStrategy.unsupported(handleMethod); + } catch (Throwable ignored) { + return TimeAccessStrategy.unsupported(); + } + } + + private static Method resolveZeroArgMethod(Class type, String name) { + Class current = type; + while (current != null) { + try { + Method method = current.getDeclaredMethod(name); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException ignored) { + current = current.getSuperclass(); + } + } + + return null; + } + + private static Method resolveLongArgMethod(Class type, String name) { + Class current = type; + while (current != null) { + try { + Method method = current.getDeclaredMethod(name, long.class); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException ignored) { + current = current.getSuperclass(); + } + } + + return null; + } + + private record TimeAccessStrategy( + Method handleMethod, + Method levelDataMethod, + Method readMethod, + Method writeMethod, + Method serverHandleMethod, + Method syncMethod, + String description + ) { + static TimeAccessStrategy forHandle(Method handleMethod, Method readMethod, Method writeMethod, String description) { + Method serverHandleMethod = resolveCraftServerMethod("getHandle"); + Method syncMethod = resolveServerMethod(serverHandleMethod, "forceTimeSynchronization"); + return new TimeAccessStrategy(handleMethod, null, readMethod, writeMethod, serverHandleMethod, syncMethod, description); + } + + static TimeAccessStrategy forLevelData(Method handleMethod, Method levelDataMethod, Method readMethod, Method writeMethod, String description) { + Method serverHandleMethod = resolveCraftServerMethod("getHandle"); + Method syncMethod = resolveServerMethod(serverHandleMethod, "forceTimeSynchronization"); + return new TimeAccessStrategy(handleMethod, levelDataMethod, readMethod, writeMethod, serverHandleMethod, syncMethod, description); + } + + static TimeAccessStrategy unsupported() { + return new TimeAccessStrategy(null, null, null, null, null, null, "unsupported"); + } + + static TimeAccessStrategy unsupported(Method handleMethod) { + return new TimeAccessStrategy(handleMethod, null, null, null, null, null, "unsupported"); + } + + boolean writable() { + return handleMethod != null && readMethod != null && writeMethod != null; + } + + Object readOwner(Object handle) throws ReflectiveOperationException { + if (levelDataMethod == null) { + return handle; + } + + return levelDataMethod.invoke(handle); + } + + Object[] readArguments(Object handle) { + return new Object[0]; + } + + Object writeOwner(Object handle) throws ReflectiveOperationException { + if (levelDataMethod == null) { + return handle; + } + + return levelDataMethod.invoke(handle); + } + + private static Method resolveCraftServerMethod(String name) { + try { + Method method = Bukkit.getServer().getClass().getMethod(name); + method.setAccessible(true); + return method; + } catch (Throwable ignored) { + return null; + } + } + + private static Method resolveServerMethod(Method serverHandleMethod, String name) { + if (serverHandleMethod == null) { + return null; + } + + try { + Object craftServer = Bukkit.getServer(); + if (craftServer == null) { + return null; + } + + Object serverHandle = serverHandleMethod.invoke(craftServer); + if (serverHandle == null) { + return null; + } + + Method method = serverHandle.getClass().getMethod(name); + method.setAccessible(true); + return method; + } catch (Throwable ignored) { + return null; + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java new file mode 100644 index 000000000..fdc1351ab --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java @@ -0,0 +1,1115 @@ +package art.arcane.iris.core.runtime; + +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import art.arcane.iris.core.link.MultiverseCoreLink; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.core.lifecycle.WorldLifecycleService; +import art.arcane.iris.core.pack.AtomicDirectoryPublisher; +import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.project.IrisCodeWorkspace; +import art.arcane.iris.core.tools.IrisCreator; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.exceptions.IrisException; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.io.File; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public final class StudioOpenCoordinator { + private static final long STUDIO_CLOSE_TIMEOUT_SECONDS = 120L; + private static final long STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS = 30L; + private static final long STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS = 30L; + private static final long STUDIO_ENTRY_CLEANUP_BOUNDARY_SECONDS = 120L; + private static volatile StudioOpenCoordinator instance; + private final EntryLoadRegistry entryLoads; + + private StudioOpenCoordinator() { + entryLoads = new EntryLoadRegistry(); + } + + public static StudioOpenCoordinator get() { + StudioOpenCoordinator current = instance; + if (current != null) { + return current; + } + + synchronized (StudioOpenCoordinator.class) { + if (instance != null) { + return instance; + } + + instance = new StudioOpenCoordinator(); + return instance; + } + } + + public CompletableFuture open(StudioOpenRequest request) { + CompletableFuture future = new CompletableFuture<>(); + J.aBukkit(() -> executeOpen(request, future)); + return future; + } + + public CompletableFuture closeProject(IrisProject project) { + if (project == null) { + return CompletableFuture.completedFuture(new StudioCloseResult(null, true, true, false, null)); + } + + PlatformChunkGenerator provider = project.getActiveProvider(); + if (provider == null) { + return CompletableFuture.completedFuture(new StudioCloseResult(null, true, true, false, null)); + } + + World world = BukkitWorldBinding.world(provider.getTarget().getWorld()); + String worldName = world == null + ? IrisWorldStorage.logicalName(WorldIdentity.parse(provider.getTarget().getWorld().identity())) + : IrisWorldStorage.logicalName(world); + return closeWorldCoordinated(provider, worldName, world, true, project); + } + + private void executeOpen(StudioOpenRequest request, CompletableFuture future) { + World world = null; + PlatformChunkGenerator provider = null; + CompletableFuture entryLoadFuture = null; + try { + long openStart = System.nanoTime(); + long t = openStart; + entryLoads.rejectNewOpen(); + updateStage(request, "resolve_dimension", 0.04D); + if (IrisToolbelt.getDimension(request.dimensionKey()) == null) { + throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + "."); + } + + updateStage(request, "prepare_world_pack", 0.10D); + cleanupStaleTransientWorlds(request.worldName()); + t = logStudioPhase(request, "resolve_dimension_and_cleanup", t, openStart); + + updateStage(request, "install_datapacks", 0.18D); + IrisCreator creator = IrisToolbelt.createWorld() + .seed(request.seed()) + .sender(request.sender()) + .studio(true) + .name(request.worldName()) + .dimension(request.dimensionKey()) + .datapackPreparation(request.openKind().datapackPreparation()) + .studioProgressConsumer((progress, stage) -> updateStage(request, mapCreatorStage(stage), progress)) + .studioTimingConsumer((phase, duration) -> logMeasuredStudioPhase(request, phase, duration)); + world = creator.create(); + t = logStudioPhase(request, "create_world_total", t, openStart); + provider = IrisToolbelt.access(world); + if (provider == null) { + throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\"."); + } + + updateStage(request, "apply_world_rules", 0.72D); + final World rulesWorld = world; + CompletableFuture rulesApplied = + J.sfut(() -> WorldRuntimeControlService.get().applyStudioWorldRules(rulesWorld)); + if (rulesApplied != null) { + rulesApplied.get(15L, TimeUnit.SECONDS); + } + t = logStudioPhase(request, "apply_world_rules", t, openStart); + + updateStage(request, "prepare_generator", 0.78D); + if (request.openKind().prepareGeneratorState()) { + WorldRuntimeControlService.get().prepareGenerator(world); + } + t = logStudioPhase(request, "prepare_generator", t, openStart); + + Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world); + if (entryAnchor == null) { + throw new IllegalStateException("Studio entry anchor could not be resolved."); + } + t = logStudioPhase(request, "resolve_entry_anchor", t, openStart); + + updateStage(request, "load_entry_chunk", 0.80D); + int entryChunkX = entryAnchor.getBlockX() >> 4; + int entryChunkZ = entryAnchor.getBlockZ() >> 4; + try { + entryLoadFuture = loadEntryChunk(world, entryChunkX, entryChunkZ); + entryLoads.register(request.worldName(), entryLoadFuture); + entryLoadFuture.get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (TimeoutException e) { + throw new IllegalStateException("Studio entry chunk did not load in time at " + + entryChunkX + "," + entryChunkZ + " — chunk system may be stalled.", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Studio entry chunk load was interrupted at " + + entryChunkX + "," + entryChunkZ + ".", e); + } + t = logStudioPhase(request, "load_entry_chunk", t, openStart); + + updateStage(request, "resolve_safe_entry", 0.84D); + Location safeEntry; + try { + safeEntry = WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor) + .get(5L, TimeUnit.SECONDS); + } catch (TimeoutException e) { + throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled."); + } + if (safeEntry == null) { + throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\"."); + } + t = logStudioPhase(request, "resolve_safe_entry", t, openStart); + + if (request.openKind().teleportThroughStandardEntry() + && request.playerName() != null + && !request.playerName().isBlank()) { + updateStage(request, "teleport_player", 0.96D); + Player player = resolvePlayer(request.playerName()); + if (player == null) { + throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online."); + } + + Boolean teleported; + try { + teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(60L, TimeUnit.SECONDS); + } catch (TimeoutException e) { + throw new IllegalStateException("Studio teleport timed out — destination region may still be generating."); + } + if (!Boolean.TRUE.equals(teleported)) { + throw new IllegalStateException("Studio teleport did not complete successfully."); + } + t = logStudioPhase(request, "teleport_standard_entry", t, openStart); + } + + endStudioEntryBootstrap(world, provider); + + updateStage(request, "finalize_open", 1.00D); + if (request.project() != null) { + request.project().setActiveProvider(provider); + } + if (request.openKind().openWorkspace() && request.project() != null) { + new IrisCodeWorkspace(request.project()).openVSCode(request.sender()); + } + runOpenFinalizer(request.onDone(), world); + t = logStudioPhase(request, "finalize_open", t, openStart); + + IrisLogging.info("Studio open: " + world.getName() + " ready in " + + elapsedMillis(openStart) + "ms"); + entryLoads.release(request.worldName(), entryLoadFuture); + future.complete(new StudioOpenResult(world, safeEntry)); + } catch (Throwable e) { + abandonStudioEntryBootstrap(world, e); + IrisLogging.reportError("Studio open failed for world \"" + request.worldName() + "\".", e); + if (!request.retainOnFailure()) { + updateStage(request, "cleanup", 1.00D); + if (LifecycleOperationCoordinator.get() + .active(LifecycleOperationCoordinator.Domain.SERVER_LIFECYCLE) + .isPresent()) { + deferFailedOpenCleanupToRestart( + provider, + request.worldName(), + world, + entryLoadFuture); + } else if (requiresDeferredEntryCleanup(entryLoadFuture)) { + deferFailedOpenCleanup( + entryLoadFuture, + provider, + request.worldName(), + world, + request.project()); + } else { + try { + CompletableFuture cleanup = cleanupFailedOpen( + provider, + request.worldName(), + world, + request.project()); + entryLoads.releaseAfterSuccessfulCompletion( + request.worldName(), + entryLoadFuture, + cleanup); + cleanup.get(45L, TimeUnit.SECONDS); + } catch (Throwable cleanupError) { + IrisLogging.reportError("Studio cleanup failed for world \"" + + request.worldName() + "\".", unwrapFailure(cleanupError)); + } + } + } + future.completeExceptionally(e); + } + } + + private void deferFailedOpenCleanupToRestart( + PlatformChunkGenerator provider, + String worldName, + World world, + CompletableFuture entryLoadFuture + ) { + if (provider != null || world != null || transientWorldStorageExists(worldName)) { + queueStartupCleanup( + worldName, + new IllegalStateException("Studio cleanup deferred across the queued server restart.")); + } + entryLoads.release(worldName, entryLoadFuture); + } + + private boolean transientWorldStorageExists(String worldName) { + if (worldName == null || worldName.isBlank()) { + return false; + } + try { + File root = IrisWorldStorage.requireSafeManagedDimensionRoot( + IrisWorldStorage.managedKeyFromName(worldName)); + return Files.exists(root.toPath(), LinkOption.NOFOLLOW_LINKS); + } catch (RuntimeException failure) { + return true; + } + } + + private long logStudioPhase(StudioOpenRequest request, String phase, long t, long openStart) { + long now = System.nanoTime(); + IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms cumulative=%dms", + request.worldName(), + request.openKind().name().toLowerCase(Locale.ROOT), + phase, + TimeUnit.NANOSECONDS.toMillis(now - t), + TimeUnit.NANOSECONDS.toMillis(now - openStart)); + return now; + } + + private void runOpenFinalizer(Consumer finalizer, World world) + throws InterruptedException, ExecutionException, TimeoutException { + if (finalizer == null) { + return; + } + if (J.isPrimaryThread()) { + finalizer.accept(world); + return; + } + + CompletableFuture completion = J.sfut(() -> finalizer.accept(world)); + completion.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + private long elapsedMillis(long startedAtNanos) { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos); + } + + private void logMeasuredStudioPhase(StudioOpenRequest request, String phase, long duration) { + IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms", + request.worldName(), + request.openKind().name().toLowerCase(Locale.ROOT), + phase, + duration); + } + + private CompletableFuture loadEntryChunk(World world, int chunkX, int chunkZ) { + if (!J.isFolia()) { + return loadEntryChunkAsync(world, chunkX, chunkZ); + } + return scheduleEntryChunkRetention(world, chunkX, chunkZ); + } + + private CompletableFuture loadEntryChunkAsync(World world, int chunkX, int chunkZ) { + CompletableFuture requested; + try { + requested = WorldRuntimeControlService.get().requestChunkAsync( + world, + chunkX, + chunkZ, + true, + true); + } catch (Throwable throwable) { + return CompletableFuture.failedFuture(throwable); + } + if (requested == null) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Entry-chunk async request did not return a future at " + chunkX + "," + chunkZ + ".")); + } + return requested.thenCompose(chunk -> { + if (chunk == null) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Entry-chunk async request returned no chunk at " + chunkX + "," + chunkZ + ".")); + } + return scheduleEntryChunkRetention(world, chunkX, chunkZ); + }); + } + + private CompletableFuture scheduleEntryChunkRetention(World world, int chunkX, int chunkZ) { + CompletableFuture loaded = new CompletableFuture<>(); + try { + J.s(() -> retainAndConfirmEntryChunk(world, chunkX, chunkZ) + .whenComplete((ignored, throwable) -> complete(loaded, throwable))); + } catch (Throwable throwable) { + loaded.completeExceptionally(throwable); + } + return loaded; + } + + private CompletableFuture retainAndConfirmEntryChunk(World world, int chunkX, int chunkZ) { + CompletableFuture confirmed = new CompletableFuture<>(); + try { + world.addPluginChunkTicket( + chunkX, + chunkZ, + BukkitPlatform.plugin()); + } catch (Throwable throwable) { + confirmed.completeExceptionally(throwable); + return confirmed; + } + if (!J.runRegion(world, chunkX, chunkZ, () -> confirmed.complete(null))) { + confirmed.completeExceptionally(new IllegalStateException( + "Failed to confirm entry-chunk region at " + chunkX + "," + chunkZ + ".")); + } + return confirmed; + } + + private void complete(CompletableFuture target, Throwable throwable) { + if (throwable == null) { + target.complete(null); + return; + } + target.completeExceptionally(throwable); + } + + private void endStudioEntryBootstrap(World world, PlatformChunkGenerator provider) { + if (!(provider instanceof BukkitChunkGenerator bukkitGenerator)) { + throw new IllegalStateException("Studio runtime provider cannot finish its entry bootstrap."); + } + AtomicBoolean activationClaim = new AtomicBoolean(true); + CompletableFuture activation = J.sfut(() -> { + if (!activationClaim.compareAndSet(true, false)) { + INMS.get().abandonStudioStructureBootstrap(world); + return; + } + try { + INMS.get().completeStudioStructureBootstrap(world); + bukkitGenerator.endStudioEntryBootstrap(); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Studio native structure state could not be activated after entry bootstrap.", e); + } + }); + try { + activation.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + activationClaim.compareAndSet(true, false); + Thread.currentThread().interrupt(); + throw new IllegalStateException("Studio native structure activation was interrupted.", e); + } catch (ExecutionException e) { + activationClaim.compareAndSet(true, false); + throw new IllegalStateException("Studio native structure activation did not complete.", + unwrapFailure(e)); + } catch (TimeoutException e) { + if (!activationClaim.compareAndSet(true, false)) { + try { + activation.get(5L, TimeUnit.SECONDS); + return; + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IllegalStateException( + "Studio native structure activation was interrupted.", interrupted); + } catch (ExecutionException | TimeoutException settlementFailure) { + throw new IllegalStateException( + "Studio native structure activation did not settle after claiming completion.", + unwrapFailure(settlementFailure)); + } + } + throw new IllegalStateException("Studio native structure activation did not complete.", e); + } + } + + private void abandonStudioEntryBootstrap(World world, Throwable failure) { + if (world == null) { + return; + } + if (J.isPrimaryThread()) { + try { + INMS.get().abandonStudioStructureBootstrap(world); + } catch (Throwable abandonmentFailure) { + failure.addSuppressed(abandonmentFailure); + } + return; + } + CompletableFuture abandonment = J.sfut( + () -> INMS.get().abandonStudioStructureBootstrap(world)); + try { + abandonment.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + failure.addSuppressed(new IllegalStateException( + "Studio native structure abandonment was interrupted.", e)); + } catch (ExecutionException | TimeoutException e) { + failure.addSuppressed(new IllegalStateException( + "Studio native structure abandonment did not complete.", unwrapFailure(e))); + } + } + + private void deferFailedOpenCleanup( + CompletableFuture entryLoadFuture, + PlatformChunkGenerator provider, + String worldName, + World world, + IrisProject project + ) { + CompletableFuture cleanup = deferCleanupUntilEntrySettlement( + entryLoadFuture, + () -> cleanupFailedOpen(provider, worldName, world, project)); + entryLoads.releaseAfterSuccessfulCompletion(worldName, entryLoadFuture, cleanup); + cleanup.whenComplete((ignored, cleanupFailure) -> { + if (cleanupFailure != null) { + IrisLogging.reportError("Deferred Studio cleanup failed for world \"" + + worldName + "\".", unwrapFailure(cleanupFailure)); + } + }); + observeDeferredEntryCleanupBoundary(entryLoadFuture, worldName); + } + + static CompletableFuture deferCleanupUntilEntrySettlement( + CompletableFuture entryLoadFuture, + Supplier> cleanup + ) { + Objects.requireNonNull(entryLoadFuture, "Studio entry-load future"); + Objects.requireNonNull(cleanup, "Studio cleanup"); + return entryLoadFuture.handle((ignored, entryFailure) -> null) + .thenCompose(ignored -> invokePhase(cleanup)); + } + + static boolean requiresDeferredEntryCleanup(CompletableFuture entryLoadFuture) { + return entryLoadFuture != null && !entryLoadFuture.isDone(); + } + + private CompletableFuture cleanupFailedOpen( + PlatformChunkGenerator provider, + String worldName, + World world, + IrisProject project + ) { + return closeWorldCoordinated(provider, worldName, world, true, project) + .thenCompose(result -> result.failureCause() == null + ? CompletableFuture.completedFuture(null) + : CompletableFuture.failedFuture(result.failureCause())); + } + + private void observeDeferredEntryCleanupBoundary( + CompletableFuture entryLoadFuture, + String worldName + ) { + CompletableFuture.delayedExecutor( + STUDIO_ENTRY_CLEANUP_BOUNDARY_SECONDS, + TimeUnit.SECONDS).execute(() -> { + if (entryLoadFuture.isDone()) { + return; + } + TimeoutException timeout = new TimeoutException( + "Studio entry generation remained active for " + + STUDIO_ENTRY_CLEANUP_BOUNDARY_SECONDS + + " seconds after the open timeout for \"" + worldName + "\"."); + boolean queued = queueStartupCleanup(worldName, timeout); + String recovery = queued + ? " The transient world is queued for deletion at the next clean startup." + : " The transient world could not be queued for startup deletion."; + IrisLogging.reportError( + "Studio world \"" + worldName + + "\" remains loaded because its entry generation is still active;" + + " Iris did not unload or close its generator." + + recovery, + timeout); + }); + } + + private CompletableFuture closeWorldCoordinated( + PlatformChunkGenerator provider, + String worldName, + World world, + boolean deleteFolder, + IrisProject project + ) { + String operationTarget = worldName == null || worldName.isBlank() ? "unknown-studio-world" : worldName; + LifecycleOperationCoordinator.Lease lease; + try { + lease = LifecycleOperationCoordinator.get().acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.STUDIO_CLOSE, + operationTarget + ); + } catch (Throwable failure) { + boolean queued = deleteFolder && queueStartupCleanup(worldName, failure); + return CompletableFuture.completedFuture(new StudioCloseResult( + worldName, + false, + false, + queued, + failure + )); + } + + CompletableFuture closeFuture; + try { + closeFuture = closeWorldReserved(provider, worldName, world, deleteFolder, project); + } catch (Throwable failure) { + boolean queued = deleteFolder && queueStartupCleanup(worldName, failure); + closeFuture = CompletableFuture.completedFuture(new StudioCloseResult( + worldName, + false, + false, + queued, + failure + )); + } + return closeFuture.whenComplete((result, throwable) -> lease.close()); + } + + private CompletableFuture closeWorldReserved( + PlatformChunkGenerator provider, + String worldName, + World world, + boolean deleteFolder, + IrisProject project + ) { + AtomicBoolean unloadConfirmed = new AtomicBoolean(false); + AtomicBoolean folderDeleted = new AtomicBoolean(!deleteFolder); + AtomicBoolean terminalTimeout = new AtomicBoolean(false); + if (world != null) { + IrisToolbelt.beginWorldMaintenance(world, "studio-close", true); + } + + CompletableFuture sequence = sequenceStudioClose( + () -> evacuateWorldFamily(worldName, world), + () -> unloadWorldFamily(worldName, world).thenRun(() -> { + if (terminalTimeout.get()) { + throw new CompletionException(new TimeoutException( + "Studio close stopped after its terminal timeout.")); + } + unloadConfirmed.set(true); + if (project != null) { + project.setActiveProvider(null); + } + }), + () -> provider == null ? CompletableFuture.completedFuture(null) : provider.closeAsync(), + () -> deleteFolder + ? deleteWorldFamily(worldName).thenRun(() -> folderDeleted.set(true)) + : CompletableFuture.completedFuture(null), + terminalTimeout::get + ); + CompletableFuture operation = guardCloseCompletion( + sequence, + terminalTimeout, + worldName) + .thenApply(ignored -> new StudioCloseResult( + worldName, + true, + folderDeleted.get(), + false, + null + )) + .exceptionally(throwable -> { + Throwable failure = unwrapFailure(throwable); + boolean queued = deleteFolder && queueStartupCleanup(worldName, failure); + return new StudioCloseResult( + worldName, + unloadConfirmed.get(), + folderDeleted.get(), + queued, + failure + ); + }); + return operation.whenComplete((result, throwable) -> { + if (world != null) { + IrisToolbelt.endWorldMaintenance(world, "studio-close", true); + } + }); + } + + static CompletableFuture sequenceStudioClose( + Supplier> evacuate, + Supplier> unload, + Supplier> closeGenerator, + Supplier> deleteFolders + ) { + return sequenceStudioClose(evacuate, unload, closeGenerator, deleteFolders, () -> false); + } + + static CompletableFuture sequenceStudioClose( + Supplier> evacuate, + Supplier> unload, + Supplier> closeGenerator, + Supplier> deleteFolders, + BooleanSupplier terminalTimeout + ) { + return invokePhase(evacuate) + .thenCompose(ignored -> invokePhaseUnlessTimedOut(unload, terminalTimeout)) + .thenCompose(ignored -> invokePhaseUnlessTimedOut(closeGenerator, terminalTimeout)) + .thenCompose(ignored -> invokePhaseUnlessTimedOut(deleteFolders, terminalTimeout)); + } + + private static CompletableFuture invokePhase(Supplier> phase) { + try { + CompletableFuture future = phase.get(); + if (future == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Studio close phase returned no completion future.")); + } + return future; + } catch (Throwable failure) { + return CompletableFuture.failedFuture(failure); + } + } + + private static CompletableFuture invokePhaseUnlessTimedOut( + Supplier> phase, + BooleanSupplier terminalTimeout + ) { + if (terminalTimeout.getAsBoolean()) { + return CompletableFuture.failedFuture(new TimeoutException( + "Studio close stopped after its terminal timeout.")); + } + return invokePhase(phase); + } + + private CompletableFuture guardCloseCompletion( + CompletableFuture source, + AtomicBoolean terminalTimeout, + String worldName + ) { + CompletableFuture guarded = new CompletableFuture<>(); + AtomicBoolean settled = new AtomicBoolean(false); + source.whenComplete((ignored, throwable) -> { + if (!settled.compareAndSet(false, true)) { + return; + } + if (throwable == null) { + guarded.complete(null); + } else { + guarded.completeExceptionally(throwable); + } + }); + CompletableFuture.delayedExecutor(STUDIO_CLOSE_TIMEOUT_SECONDS, TimeUnit.SECONDS).execute(() -> { + if (!settled.compareAndSet(false, true)) { + return; + } + terminalTimeout.set(true); + TimeoutException timeout = new TimeoutException( + "Studio close did not settle within " + STUDIO_CLOSE_TIMEOUT_SECONDS + + " seconds for \"" + worldName + "\"."); + ServerConfigurator.restart("Studio close timed out for \"" + worldName + "\"."); + guarded.completeExceptionally(timeout); + }); + return guarded; + } + + private CompletableFuture evacuateWorldFamily(String worldName, World primaryWorld) { + List loadedWorlds = loadedWorldFamily(worldName, primaryWorld); + if (loadedWorlds.isEmpty()) { + return CompletableFuture.completedFuture(null); + } + + ArrayList> evacuations = new ArrayList<>(loadedWorlds.size()); + for (World loadedWorld : loadedWorlds) { + CompletableFuture evacuation = J.sfut(() -> IrisToolbelt.evacuateAsync(loadedWorld)) + .thenCompose(evacuationFuture -> evacuationFuture) + .thenCompose(evacuated -> Boolean.TRUE.equals(evacuated) + ? CompletableFuture.completedFuture(null) + : CompletableFuture.failedFuture(new IllegalStateException( + "Studio player evacuation failed for \"" + loadedWorld.getName() + "\"."))); + evacuations.add(evacuation); + } + return CompletableFuture.allOf(evacuations.toArray(CompletableFuture[]::new)); + } + + private CompletableFuture unloadWorldFamily(String worldName, World primaryWorld) { + List loadedWorlds = loadedWorldFamily(worldName, primaryWorld); + ArrayList> unloads = new ArrayList<>(loadedWorlds.size()); + for (World loadedWorld : loadedWorlds) { + CompletableFuture unload = J.sfut(() -> + IrisServices.get(MultiverseCoreLink.class) + .removeIfPresent(loadedWorld)) + .thenCompose(ignored -> WorldLifecycleService.get().unloadAsync(loadedWorld, false)); + unloads.add(unload); + } + + return CompletableFuture.allOf(unloads.toArray(CompletableFuture[]::new)).thenApply(ignored -> { + for (CompletableFuture unload : unloads) { + if (!Boolean.TRUE.equals(unload.join())) { + throw new CompletionException(new IllegalStateException( + "Studio world family unload returned false for \"" + worldName + "\".")); + } + } + if (isWorldFamilyLoaded(worldName)) { + throw new CompletionException(new IllegalStateException( + "Studio world family remained loaded after confirmed unload for \"" + worldName + "\".")); + } + return null; + }); + } + + private List loadedWorldFamily(String worldName, World primaryWorld) { + LinkedHashSet worlds = new LinkedHashSet<>(); + if (primaryWorld != null) { + worlds.add(primaryWorld); + } + if (worldName == null || worldName.isBlank()) { + return List.copyOf(worlds); + } + for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { + WorldIdentity.resolve(IrisWorldStorage.keyFromName(familyWorldName)).ifPresent(worlds::add); + } + return List.copyOf(worlds); + } + + private CompletableFuture deleteWorldFamily(String worldName) { + if (worldName == null || worldName.isBlank()) { + return CompletableFuture.completedFuture(null); + } + if (isWorldFamilyLoaded(worldName)) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Refusing to delete a loaded studio world family for \"" + worldName + "\".")); + } + + return CompletableFuture.runAsync(() -> { + for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { + try { + if (isWorldFamilyLoaded(worldName)) { + throw new IOException("Studio world family became loaded before deletion for \"" + + worldName + "\"."); + } + File folder = IrisWorldStorage.requireSafeManagedDimensionRoot( + IrisWorldStorage.managedKeyFromName(familyWorldName)); + AtomicDirectoryPublisher.deleteTree(folder.toPath()); + } catch (IOException | IllegalArgumentException failure) { + throw new CompletionException(failure); + } + } + }); + } + + private boolean queueStartupCleanup(String worldName, Throwable failure) { + if (worldName == null || worldName.isBlank()) { + return false; + } + try { + IrisServices.get(WorldDeletionQueue.class).queueFamilyForStartupDeletion(Collections.singleton(worldName)); + return true; + } catch (Throwable queueFailure) { + if (failure != null) { + failure.addSuppressed(queueFailure); + } + IrisLogging.reportError("Failed to queue deferred deletion for world \"" + worldName + "\".", queueFailure); + return false; + } + } + + private void cleanupStaleTransientWorlds(String worldName) { + LinkedHashSet staleWorldNames = collectSafeTransientWorldNames(); + String requestedBaseName = TransientWorldCleanupSupport.transientStudioBaseWorldName(worldName); + if (requestedBaseName != null) { + staleWorldNames.add(requestedBaseName); + } + + for (String staleWorldName : staleWorldNames) { + if (entryLoads.isFenced(staleWorldName)) { + IrisLogging.warn("Skipping stale Studio cleanup for \"" + staleWorldName + + "\" because its open or deferred cleanup is still active."); + continue; + } + try { + StudioCloseResult cleanupResult = closeWorldCoordinated( + null, + staleWorldName, + null, + true, + null + ).get(30L, TimeUnit.SECONDS); + if (cleanupResult.failureCause() != null) { + IrisLogging.reportError("Stale studio world cleanup failed for \"" + staleWorldName + "\".", cleanupResult.failureCause()); + } + } catch (Throwable failure) { + IrisLogging.reportError("Stale studio world cleanup failed for \"" + staleWorldName + "\".", unwrapFailure(failure)); + } + } + } + + private LinkedHashSet collectSafeTransientWorldNames() { + LinkedHashSet worldNames = new LinkedHashSet<>(); + Path irisNamespace = IrisWorldStorage.levelRoot() + .toPath() + .toAbsolutePath() + .normalize() + .resolve("dimensions") + .resolve("iris"); + if (!Files.exists(irisNamespace, LinkOption.NOFOLLOW_LINKS)) { + return worldNames; + } + if (Files.isSymbolicLink(irisNamespace) || !Files.isDirectory(irisNamespace, LinkOption.NOFOLLOW_LINKS)) { + IrisLogging.warn("Skipping stale studio cleanup because Iris dimension storage is unsafe: " + irisNamespace); + return worldNames; + } + + try (DirectoryStream children = Files.newDirectoryStream(irisNamespace)) { + for (Path child : children) { + if (Files.isSymbolicLink(child) || !Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + String transientName = TransientWorldCleanupSupport.transientStudioBaseWorldName( + child.getFileName().toString()); + if (transientName != null) { + worldNames.add(transientName); + } + } + } catch (IOException failure) { + IrisLogging.reportError("Failed to inspect stale studio worlds in \"" + irisNamespace + "\".", failure); + } + return worldNames; + } + + private void updateStage(StudioOpenRequest request, String stage, double progress) { + if (request.progressConsumer() != null) { + request.progressConsumer().accept(new StudioOpenProgress(progress, stage)); + } + } + + private String mapCreatorStage(String stage) { + if (stage == null || stage.isBlank()) { + return "create_world"; + } + + String normalized = stage.trim().toLowerCase(); + return switch (normalized) { + case "resolve_dimension", "resolving dimension" -> "resolve_dimension"; + case "prepare_world_pack", "preparing world pack" -> "prepare_world_pack"; + case "install_datapacks", "installing datapacks", "datapacks ready" -> "install_datapacks"; + case "create_world", "creating world", "world created" -> "create_world"; + default -> normalized.replace(' ', '_'); + }; + } + + private Throwable unwrapFailure(Throwable throwable) { + Throwable cursor = throwable; + while (cursor instanceof CompletionException || cursor instanceof ExecutionException) { + if (cursor.getCause() == null) { + break; + } + + cursor = cursor.getCause(); + } + + return cursor; + } + + private Player resolvePlayer(String playerName) { + Player exact = Bukkit.getPlayerExact(playerName); + if (exact != null) { + return exact; + } + + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getName().equalsIgnoreCase(playerName)) { + return player; + } + } + + return null; + } + + private boolean isWorldFamilyLoaded(String worldName) { + if (worldName == null || worldName.isBlank()) { + return false; + } + + for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { + if (WorldIdentity.resolve(IrisWorldStorage.keyFromName(familyWorldName)).isPresent()) { + return true; + } + } + + return false; + } + + public record StudioOpenRequest( + String dimensionKey, + IrisProject project, + VolmitSender sender, + long seed, + String worldName, + String playerName, + StudioOpenKind openKind, + boolean retainOnFailure, + Consumer progressConsumer, + Consumer onDone + ) { + public StudioOpenRequest { + openKind = Objects.requireNonNull(openKind, "Studio open kind"); + } + + public static StudioOpenRequest studioProject( + IrisProject project, + VolmitSender sender, + long seed, + StudioOpenKind openKind, + Consumer progressConsumer, + Consumer onDone + ) { + String playerName = sender != null && sender.isPlayer() && sender.player() != null ? sender.player().getName() : null; + return new StudioOpenRequest( + project.getName(), + project, + sender, + seed, + "iris-" + UUID.randomUUID(), + playerName, + openKind, + false, + progressConsumer, + onDone + ); + } + } + + public enum StudioOpenKind { + STANDARD( + true, + true, + IrisCreator.DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY), + JIGSAW( + false, + false, + IrisCreator.DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY); + + private final boolean teleportThroughStandardEntry; + private final boolean openWorkspace; + private final IrisCreator.DatapackPreparation datapackPreparation; + + StudioOpenKind( + boolean teleportThroughStandardEntry, + boolean openWorkspace, + IrisCreator.DatapackPreparation datapackPreparation + ) { + this.teleportThroughStandardEntry = teleportThroughStandardEntry; + this.openWorkspace = openWorkspace; + this.datapackPreparation = Objects.requireNonNull( + datapackPreparation, + "Studio datapack preparation"); + } + + public boolean teleportThroughStandardEntry() { + return teleportThroughStandardEntry; + } + + public boolean openWorkspace() { + return openWorkspace; + } + + public boolean prepareGeneratorState() { + return this == STANDARD; + } + + public IrisCreator.DatapackPreparation datapackPreparation() { + return datapackPreparation; + } + } + + public record StudioOpenProgress(double progress, String stage) { + } + + public record StudioOpenResult(World world, Location entryLocation) { + } + + public record StudioCloseResult( + String worldName, + boolean unloadCompletedLive, + boolean folderDeletionCompletedLive, + boolean startupCleanupQueued, + Throwable failureCause + ) { + public boolean successful() { + return failureCause == null; + } + } + + static final class EntryLoadRegistry { + private final ConcurrentHashMap> entryLoads; + + EntryLoadRegistry() { + entryLoads = new ConcurrentHashMap<>(); + } + + void register(String worldName, CompletableFuture entryLoadFuture) { + Objects.requireNonNull(worldName, "Studio world name"); + Objects.requireNonNull(entryLoadFuture, "Studio entry-load future"); + CompletableFuture existing = entryLoads.putIfAbsent(worldName, entryLoadFuture); + if (existing != null) { + throw new IllegalStateException("Studio open or deferred cleanup is already active for \"" + + worldName + "\"."); + } + } + + void release( + String worldName, + CompletableFuture entryLoadFuture + ) { + if (worldName == null || entryLoadFuture == null) { + return; + } + entryLoads.remove(worldName, entryLoadFuture); + } + + void releaseAfterSuccessfulCompletion( + String worldName, + CompletableFuture entryLoadFuture, + CompletableFuture completion + ) { + Objects.requireNonNull(completion, "Studio lifecycle completion"); + completion.whenComplete((ignored, failure) -> { + if (failure == null) { + release(worldName, entryLoadFuture); + } + }); + } + + void rejectNewOpen() { + ArrayList activeWorlds = new ArrayList<>(); + for (Map.Entry> entry : entryLoads.entrySet()) { + activeWorlds.add(entry.getKey()); + } + if (activeWorlds.isEmpty()) { + return; + } + Collections.sort(activeWorlds); + throw new IllegalStateException("A previous Studio open or deferred cleanup is still active for " + + String.join(", ", activeWorlds) + + ". Wait for it to settle before opening another Studio; a reported cleanup failure" + + " requires the queued clean restart."); + } + + boolean isFenced(String worldName) { + return entryLoads.containsKey(worldName); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupport.java b/core/src/main/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupport.java new file mode 100644 index 000000000..3ac0d6680 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupport.java @@ -0,0 +1,98 @@ +package art.arcane.iris.core.runtime; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +public final class TransientWorldCleanupSupport { + private static final Pattern TRANSIENT_STUDIO_WORLD_PATTERN = Pattern.compile("^iris-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE); + + private TransientWorldCleanupSupport() { + } + + public static boolean isTransientStudioWorldName(String worldName) { + return transientStudioBaseWorldName(worldName) != null; + } + + public static String transientStudioBaseWorldName(String worldName) { + if (worldName == null || worldName.isBlank()) { + return null; + } + + String candidate = worldName.trim(); + if (candidate.endsWith("_nether")) { + candidate = candidate.substring(0, candidate.length() - "_nether".length()); + } else if (candidate.endsWith("_the_end")) { + candidate = candidate.substring(0, candidate.length() - "_the_end".length()); + } + + if (!TRANSIENT_STUDIO_WORLD_PATTERN.matcher(candidate).matches()) { + return null; + } + + return candidate; + } + + public static List worldFamilyNames(String worldName) { + ArrayList names = new ArrayList<>(); + String normalized = normalizeWorldName(worldName); + if (normalized == null) { + return names; + } + + names.add(normalized); + names.add(normalized + "_nether"); + names.add(normalized + "_the_end"); + return names; + } + + public static LinkedHashSet collectTransientStudioWorldNames(File levelRoot) { + LinkedHashSet names = new LinkedHashSet<>(); + if (levelRoot == null) { + return names; + } + + File namespacesRoot = new File(levelRoot, "dimensions"); + File[] namespaces = namespacesRoot.listFiles(File::isDirectory); + if (namespaces == null) { + return names; + } + + for (File namespace : namespaces) { + collectTransientStudioWorldNames(namespace, names); + } + return names; + } + + private static void collectTransientStudioWorldNames(File folder, LinkedHashSet names) { + File[] children = folder.listFiles(File::isDirectory); + if (children == null) { + return; + } + + for (File child : children) { + String baseName = transientStudioBaseWorldName(child.getName()); + if (baseName != null) { + names.add(baseName); + continue; + } + collectTransientStudioWorldNames(child, names); + } + } + + private static String normalizeWorldName(String worldName) { + if (worldName == null) { + return null; + } + + String normalized = worldName.trim(); + if (normalized.isEmpty()) { + return null; + } + + return normalized.toLowerCase(Locale.ROOT).equals(normalized) ? normalized : normalized; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/WorldDeletionQueue.java b/core/src/main/java/art/arcane/iris/core/runtime/WorldDeletionQueue.java new file mode 100644 index 000000000..8cd10327a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/WorldDeletionQueue.java @@ -0,0 +1,31 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.runtime; + +import java.io.IOException; +import java.util.Collection; + +/** + * Queues world folders for deletion at next startup. + */ +public interface WorldDeletionQueue { + int queueExactForStartupDeletion(Collection worldNames) throws IOException; + + int queueFamilyForStartupDeletion(Collection worldNames) throws IOException; +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlBackend.java b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlBackend.java new file mode 100644 index 000000000..3e662cb68 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlBackend.java @@ -0,0 +1,57 @@ +package art.arcane.iris.core.runtime; + +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.lang.reflect.Method; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; + +interface WorldRuntimeControlBackend { + String backendName(); + + String describeCapabilities(); + + OptionalLong readDayTime(World world); + + boolean writeDayTime(World world, long dayTime) throws ReflectiveOperationException; + + void syncTime(World world); + + CompletableFuture requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate); + + default CompletableFuture requestChunkAsync( + World world, + int chunkX, + int chunkZ, + boolean generate, + boolean urgent + ) { + if (!urgent) { + return requestChunkAsync(world, chunkX, chunkZ, generate); + } + if (world == null) { + return CompletableFuture.failedFuture(new IllegalStateException("World is null.")); + } + try { + Method method = World.class.getMethod( + "getChunkAtAsync", + int.class, + int.class, + boolean.class, + boolean.class); + Object result = method.invoke(world, chunkX, chunkZ, generate, true); + if (result instanceof CompletableFuture future) { + @SuppressWarnings("unchecked") + CompletableFuture chunkFuture = (CompletableFuture) future; + return chunkFuture; + } + return CompletableFuture.failedFuture( + new IllegalStateException("Paper World#getChunkAtAsync returned a non-future result.")); + } catch (NoSuchMethodException exception) { + return requestChunkAsync(world, chunkX, chunkZ, generate); + } catch (Throwable exception) { + return CompletableFuture.failedFuture(exception); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java new file mode 100644 index 000000000..cc624419d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java @@ -0,0 +1,717 @@ +package art.arcane.iris.core.runtime; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.lifecycle.CapabilitySnapshot; +import art.arcane.iris.core.lifecycle.ServerFamily; +import art.arcane.iris.core.lifecycle.WorldLifecycleService; +import art.arcane.iris.core.service.BoardSVC; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.common.scheduling.J; +import io.papermc.lib.PaperLib; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.GameRule; +import org.bukkit.HeightMap; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Waterlogged; +import org.bukkit.entity.Player; +import org.bukkit.event.world.TimeSkipEvent; +import org.bukkit.plugin.PluginManager; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.VoxelShape; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public final class WorldRuntimeControlService { + private static final int MAX_SAFE_ENTRY_HORIZONTAL_RADIUS = 15; + private static final int MAX_SAFE_ENTRY_VERTICAL_SEARCH = 64; + private static final double BLOCK_CENTER = 0.5D; + private static final double COLLISION_EPSILON = 0.000001D; + private static final Set UNSAFE_ENTRY_MATERIALS = Set.of( + Material.CACTUS, + Material.CAMPFIRE, + Material.COBWEB, + Material.END_GATEWAY, + Material.END_PORTAL, + Material.FIRE, + Material.MAGMA_BLOCK, + Material.NETHER_PORTAL, + Material.POINTED_DRIPSTONE, + Material.POWDER_SNOW, + Material.SOUL_CAMPFIRE, + Material.SOUL_FIRE, + Material.SWEET_BERRY_BUSH, + Material.WITHER_ROSE + ); + private static volatile WorldRuntimeControlService instance; + + private final CapabilitySnapshot capabilities; + private final WorldRuntimeControlBackend backend; + private final String capabilityDescription; + + private WorldRuntimeControlService(CapabilitySnapshot capabilities) { + this.capabilities = capabilities; + this.backend = selectBackend(capabilities); + this.capabilityDescription = "family=" + capabilities.serverFamily().id() + + ", backend=" + backend.backendName() + + ", " + backend.describeCapabilities(); + } + + public static WorldRuntimeControlService get() { + WorldRuntimeControlService current = instance; + if (current != null) { + return current; + } + + synchronized (WorldRuntimeControlService.class) { + if (instance != null) { + return instance; + } + + CapabilitySnapshot capabilities = WorldLifecycleService.get().capabilities(); + instance = new WorldRuntimeControlService(capabilities); + IrisLogging.info("WorldRuntimeControl capabilities: %s", instance.capabilityDescription); + return instance; + } + } + + public String backendName() { + return backend.backendName(); + } + + public String capabilityDescription() { + return capabilityDescription; + } + + public OptionalLong readDayTime(World world) { + return backend.readDayTime(world); + } + + public boolean applyStudioWorldRules(World world) { + if (world == null) { + return false; + } + + IrisServices.get(art.arcane.iris.core.link.MultiverseCoreLink.class).removeIfPresent(world); + setIntGameRule(world, 0, "SPAWN_CHUNK_RADIUS", "spawnChunkRadius"); + enableStudioEntitySpawning(world); + if (!IrisSettings.get().getStudio().isDisableTimeAndWeather()) { + return true; + } + + setBooleanGameRule(world, false, "ADVANCE_WEATHER", "DO_WEATHER_CYCLE", "WEATHER_CYCLE", "doWeatherCycle", "weatherCycle"); + setBooleanGameRule(world, false, "ADVANCE_TIME", "DO_DAYLIGHT_CYCLE", "DAYLIGHT_CYCLE", "doDaylightCycle", "daylightCycle"); + applyNoonTimeLock(world); + return true; + } + + public boolean applyObjectStudioWorldRules(World world) { + if (world == null) { + return false; + } + + applyStudioWorldRules(world); + + setBooleanGameRule(world, false, "DO_FIRE_TICK", "doFireTick"); + setBooleanGameRule(world, false, "DO_MOB_LOOT", "doMobLoot"); + setBooleanGameRule(world, true, "DO_IMMEDIATE_RESPAWN", "doImmediateRespawn"); + setBooleanGameRule(world, false, "FALL_DAMAGE", "fallDamage"); + setBooleanGameRule(world, false, "FIRE_DAMAGE", "fireDamage"); + setBooleanGameRule(world, false, "DROWNING_DAMAGE", "drowningDamage"); + setBooleanGameRule(world, false, "FREEZE_DAMAGE", "freezeDamage"); + setBooleanGameRule(world, false, "MOB_GRIEFING", "mobGriefing"); + setBooleanGameRule(world, false, "DO_TILE_DROPS", "doTileDrops"); + setBooleanGameRule(world, true, "KEEP_INVENTORY", "keepInventory"); + setIntGameRule(world, 0, "RANDOM_TICK_SPEED", "randomTickSpeed"); + setIntGameRule(world, 0, "SPAWN_RADIUS", "spawnRadius"); + setIntGameRule(world, 0, "MAX_ENTITY_CRAMMING", "maxEntityCramming"); + applyNoonTimeLock(world); + return true; + } + + static void enableStudioEntitySpawning(World world) { + setBooleanGameRule(world, true, "DO_MOB_SPAWNING", "doMobSpawning"); + setBooleanGameRule(world, true, "DO_TRADER_SPAWNING", "doTraderSpawning"); + setBooleanGameRule(world, true, "DO_PATROL_SPAWNING", "doPatrolSpawning"); + setBooleanGameRule(world, true, "DO_INSOMNIA", "doInsomnia"); + setBooleanGameRule(world, true, "DO_WARDEN_SPAWNING", "doWardenSpawning"); + } + + public boolean applyNoonTimeLock(World world) { + if (world == null) { + return false; + } + + if (!hasMutableClock(world)) { + return false; + } + + OptionalLong currentTime = readDayTime(world); + if (currentTime.isEmpty()) { + return false; + } + + long skipAmount = (6000L - currentTime.getAsLong()) % 24000L; + if (skipAmount < 0L) { + skipAmount += 24000L; + } + + long effectiveSkip = skipAmount; + if (TIME_SKIP_EVENT_AVAILABLE) { + long fired = fireTimeSkipEvent(world, skipAmount); + if (fired == TIME_SKIP_CANCELLED) { + return false; + } + effectiveSkip = fired; + } + + try { + boolean written = backend.writeDayTime(world, currentTime.getAsLong() + effectiveSkip); + if (!written) { + return false; + } + backend.syncTime(world); + return true; + } catch (Throwable e) { + IrisLogging.debug("Runtime time lock skipped for world \"" + world.getName() + "\": " + e.getMessage()); + return false; + } + } + + private static final long TIME_SKIP_CANCELLED = Long.MIN_VALUE; + private static final boolean TIME_SKIP_EVENT_AVAILABLE = probeTimeSkipEvent(); + + private static boolean probeTimeSkipEvent() { + try { + Class.forName("org.bukkit.event.world.TimeSkipEvent$SkipReason"); + return true; + } catch (Throwable e) { + return false; + } + } + + private long fireTimeSkipEvent(World world, long skipAmount) { + try { + TimeSkipEvent event = new TimeSkipEvent(world, TimeSkipEvent.SkipReason.CUSTOM, skipAmount); + PluginManager pluginManager = Bukkit.getPluginManager(); + if (pluginManager != null) { + pluginManager.callEvent(event); + } + if (event.isCancelled()) { + return TIME_SKIP_CANCELLED; + } + return event.getSkipAmount(); + } catch (Throwable e) { + return skipAmount; + } + } + + public CompletableFuture requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) { + return backend.requestChunkAsync(world, chunkX, chunkZ, generate); + } + + public CompletableFuture requestChunkAsync( + World world, + int chunkX, + int chunkZ, + boolean generate, + boolean urgent + ) { + return backend.requestChunkAsync(world, chunkX, chunkZ, generate, urgent); + } + + public void prepareGenerator(World world) { + if (world == null) { + return; + } + + try { + art.arcane.iris.engine.platform.PlatformChunkGenerator provider = art.arcane.iris.core.tools.IrisToolbelt.access(world); + if (provider == null) { + return; + } + + art.arcane.iris.engine.framework.Engine engine = provider.getEngine(); + if (engine == null) { + return; + } + + engine.getMantle().getComponents(); + engine.getMantle().getRealRadius(); + } catch (Throwable e) { + IrisLogging.reportError("Failed to prepare generator state for world \"" + world.getName() + "\".", e); + } + } + + public Location resolveEntryAnchor(World world) { + if (world == null) { + return null; + } + + PlatformChunkGenerator provider = IrisToolbelt.access(world); + return resolveEntryAnchor(world, provider); + } + + static Location resolveEntryAnchor(World world, PlatformChunkGenerator provider) { + if (world == null) { + return null; + } + + if (provider != null && provider.isStudio() && provider instanceof BukkitChunkGenerator bukkitProvider) { + Location initialSpawn = bukkitProvider.getInitialSpawnLocation(world); + if (initialSpawn != null) { + return initialSpawn.clone(); + } + } + + Location spawnLocation = world.getSpawnLocation(); + if (spawnLocation != null) { + return spawnLocation.clone(); + } + + int minY = world.getMinHeight() + 1; + int y = Math.max(minY, 96); + return new Location(world, 0.5D, y, 0.5D); + } + + public CompletableFuture resolveSafeEntry(World world, Location source) { + if (world == null || source == null) { + return CompletableFuture.completedFuture(null); + } + + int chunkX = source.getBlockX() >> 4; + int chunkZ = source.getBlockZ() >> 4; + CompletableFuture future = new CompletableFuture<>(); + boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> { + try { + future.complete(findTopSafeLocation(world, source)); + } catch (Throwable t) { + future.completeExceptionally(t); + } + }); + if (!scheduled) { + future.completeExceptionally(new IllegalStateException( + "Failed to schedule safe-entry resolve for " + world.getName() + "@" + chunkX + "," + chunkZ + ".")); + } + return future; + } + + public CompletableFuture teleport(Player player, Location location) { + if (player == null || location == null) { + return CompletableFuture.completedFuture(false); + } + + CompletableFuture future = new CompletableFuture<>(); + boolean scheduled = J.runEntity(player, () -> { + try { + CompletableFuture teleportFuture = PaperLib.teleportAsync(player, location); + if (teleportFuture == null) { + future.complete(false); + return; + } + + teleportFuture.whenComplete((success, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + return; + } + + if (Boolean.TRUE.equals(success)) { + J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player)); + future.complete(true); + return; + } + + future.complete(false); + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + }); + if (!scheduled) { + return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule teleport for " + player.getName() + ".")); + } + + return future; + } + + public boolean hasMutableClock(World world) { + try { + Object handle = invokeNoArg(world, "getHandle"); + if (handle == null) { + return false; + } + + Object dimensionTypeHolder = invokeNoArg(handle, "dimensionTypeRegistration"); + Object dimensionType = unwrapDimensionType(dimensionTypeHolder); + if (dimensionType == null) { + return false; + } + + return !dimensionTypeHasFixedTime(dimensionType); + } catch (Throwable e) { + return false; + } + } + + private static WorldRuntimeControlBackend selectBackend(CapabilitySnapshot capabilities) { + ServerFamily family = capabilities.serverFamily(); + if (family.isPaperLike()) { + return new PaperLikeRuntimeControlBackend(capabilities); + } + + return new BukkitPublicRuntimeControlBackend(capabilities); + } + + static Location findTopSafeLocation(World world, Location source) { + int sourceX = source.getBlockX(); + int sourceZ = source.getBlockZ(); + float yaw = source.getYaw(); + float pitch = source.getPitch(); + int chunkX = sourceX >> 4; + int chunkZ = sourceZ >> 4; + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return null; + } + + int minimumFloorY = world.getMinHeight(); + int maximumFloorY = world.getMaxHeight() - 3; + if (minimumFloorY > maximumFloorY) { + return null; + } + + int minimumX = chunkX << 4; + int minimumZ = chunkZ << 4; + int maximumX = minimumX + 15; + int maximumZ = minimumZ + 15; + for (int radius = 0; radius <= MAX_SAFE_ENTRY_HORIZONTAL_RADIUS; radius++) { + for (int offsetX = -radius; offsetX <= radius; offsetX++) { + for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) { + if (Math.max(Math.abs(offsetX), Math.abs(offsetZ)) != radius) { + continue; + } + + int x = sourceX + offsetX; + int z = sourceZ + offsetZ; + if (x < minimumX || x > maximumX || z < minimumZ || z > maximumZ) { + continue; + } + + Location safeLocation = findSafeLocationInColumn( + world, + x, + z, + minimumFloorY, + maximumFloorY, + yaw, + pitch + ); + if (safeLocation != null) { + return safeLocation; + } + } + } + } + + return null; + } + + private static Location findSafeLocationInColumn( + World world, + int x, + int z, + int minimumFloorY, + int maximumFloorY, + float yaw, + float pitch + ) { + int highestY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES); + int startingFloorY = Math.max(minimumFloorY, Math.min(maximumFloorY, highestY)); + int lowestFloorY = Math.max(minimumFloorY, startingFloorY - MAX_SAFE_ENTRY_VERTICAL_SEARCH + 1); + for (int floorY = startingFloorY; floorY >= lowestFloorY; floorY--) { + Block floor = world.getBlockAt(x, floorY, z); + if (!isSafeFloor(floor)) { + continue; + } + + Block feet = world.getBlockAt(x, floorY + 1, z); + Block head = world.getBlockAt(x, floorY + 2, z); + if (isClearEntryBlock(feet) && isClearEntryBlock(head)) { + return new Location(world, x + BLOCK_CENTER, floorY + 1D, z + BLOCK_CENTER, yaw, pitch); + } + } + + return null; + } + + private static boolean isSafeFloor(Block block) { + Material material = block.getType(); + if (material == null + || isAir(material) + || material.name().endsWith("_LEAVES") + || UNSAFE_ENTRY_MATERIALS.contains(material) + || block.isLiquid() + || block.isPassable() + || isWaterlogged(block)) { + return false; + } + + VoxelShape collisionShape = block.getCollisionShape(); + if (collisionShape == null) { + return false; + } + + for (BoundingBox boundingBox : collisionShape.getBoundingBoxes()) { + if (boundingBox.getMinX() <= BLOCK_CENTER + && boundingBox.getMaxX() >= BLOCK_CENTER + && boundingBox.getMinZ() <= BLOCK_CENTER + && boundingBox.getMaxZ() >= BLOCK_CENTER + && boundingBox.getMaxY() > COLLISION_EPSILON + && boundingBox.getMaxY() <= 1D + COLLISION_EPSILON) { + return true; + } + } + + return false; + } + + private static boolean isClearEntryBlock(Block block) { + Material material = block.getType(); + if (material == null + || block.isLiquid() + || isWaterlogged(block) + || UNSAFE_ENTRY_MATERIALS.contains(material) + || !block.isPassable()) { + return false; + } + + VoxelShape collisionShape = block.getCollisionShape(); + return collisionShape != null && collisionShape.getBoundingBoxes().isEmpty(); + } + + private static boolean isAir(Material material) { + return material == Material.AIR || material == Material.CAVE_AIR || material == Material.VOID_AIR; + } + + private static boolean isWaterlogged(Block block) { + BlockData blockData = block.getBlockData(); + return blockData instanceof Waterlogged waterlogged && waterlogged.isWaterlogged(); + } + + @SuppressWarnings("unchecked") + private static void setBooleanGameRule(World world, boolean value, String... names) { + GameRule gameRule = resolveBooleanGameRule(world, names); + if (gameRule != null) { + world.setGameRule(gameRule, value); + } + } + + @SuppressWarnings("unchecked") + private static void setIntGameRule(World world, int value, String... names) { + GameRule gameRule = resolveIntGameRule(world, names); + if (gameRule != null) { + world.setGameRule(gameRule, value); + } + } + + @SuppressWarnings("unchecked") + private static GameRule resolveIntGameRule(World world, String... names) { + if (world == null || names == null || names.length == 0) { + return null; + } + + Set candidates = buildRuleNameCandidates(names); + for (String name : candidates) { + if (name == null || name.isBlank()) { + continue; + } + + try { + Field field = GameRule.class.getField(name); + Object value = field.get(null); + if (value instanceof GameRule gameRule && Integer.class.equals(gameRule.getType())) { + return (GameRule) gameRule; + } + } catch (Throwable ignored) { + } + + try { + GameRule byName = GameRule.getByName(name); + if (byName != null && Integer.class.equals(byName.getType())) { + return (GameRule) byName; + } + } catch (Throwable ignored) { + } + } + + String[] availableRules = world.getGameRules(); + if (availableRules == null || availableRules.length == 0) { + return null; + } + + Set normalizedCandidates = new LinkedHashSet<>(); + for (String candidate : candidates) { + if (candidate != null && !candidate.isBlank()) { + normalizedCandidates.add(normalizeRuleName(candidate)); + } + } + + for (String availableRule : availableRules) { + String normalizedAvailable = normalizeRuleName(availableRule); + if (!normalizedCandidates.contains(normalizedAvailable)) { + continue; + } + + try { + GameRule byName = GameRule.getByName(availableRule); + if (byName != null && Integer.class.equals(byName.getType())) { + return (GameRule) byName; + } + } catch (Throwable ignored) { + } + } + + return null; + } + + @SuppressWarnings("unchecked") + private static GameRule resolveBooleanGameRule(World world, String... names) { + if (world == null || names == null || names.length == 0) { + return null; + } + + Set candidates = buildRuleNameCandidates(names); + for (String name : candidates) { + if (name == null || name.isBlank()) { + continue; + } + + try { + Field field = GameRule.class.getField(name); + Object value = field.get(null); + if (value instanceof GameRule gameRule && Boolean.class.equals(gameRule.getType())) { + return (GameRule) gameRule; + } + } catch (Throwable ignored) { + } + + try { + GameRule byName = GameRule.getByName(name); + if (byName != null && Boolean.class.equals(byName.getType())) { + return (GameRule) byName; + } + } catch (Throwable ignored) { + } + } + + String[] availableRules = world.getGameRules(); + if (availableRules == null || availableRules.length == 0) { + return null; + } + + Set normalizedCandidates = new LinkedHashSet<>(); + for (String candidate : candidates) { + if (candidate != null && !candidate.isBlank()) { + normalizedCandidates.add(normalizeRuleName(candidate)); + } + } + + for (String availableRule : availableRules) { + String normalizedAvailable = normalizeRuleName(availableRule); + if (!normalizedCandidates.contains(normalizedAvailable)) { + continue; + } + + try { + GameRule byName = GameRule.getByName(availableRule); + if (byName != null && Boolean.class.equals(byName.getType())) { + return (GameRule) byName; + } + } catch (Throwable ignored) { + } + } + + return null; + } + + private static Set buildRuleNameCandidates(String... names) { + Set candidates = new LinkedHashSet<>(); + for (String name : names) { + if (name == null || name.isBlank()) { + continue; + } + + candidates.add(name); + candidates.add(name.toUpperCase()); + candidates.add(name.toLowerCase()); + } + + return candidates; + } + + private static String normalizeRuleName(String name) { + if (name == null) { + return ""; + } + + StringBuilder builder = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char current = name.charAt(i); + if (Character.isLetterOrDigit(current)) { + builder.append(Character.toLowerCase(current)); + } + } + return builder.toString(); + } + + private static boolean dimensionTypeHasFixedTime(Object dimensionType) throws ReflectiveOperationException { + Object fixedTimeFlag; + try { + fixedTimeFlag = invokeNoArg(dimensionType, "hasFixedTime"); + } catch (NoSuchMethodException ignored) { + Object fixedTime = invokeNoArg(dimensionType, "fixedTime"); + if (fixedTime instanceof OptionalLong optionalLong) { + return optionalLong.isPresent(); + } + if (fixedTime instanceof Optional optional) { + return optional.isPresent(); + } + return false; + } + + return fixedTimeFlag instanceof Boolean && (Boolean) fixedTimeFlag; + } + + private static Object unwrapDimensionType(Object dimensionTypeHolder) throws ReflectiveOperationException { + if (dimensionTypeHolder == null) { + return null; + } + + Class holderClass = dimensionTypeHolder.getClass(); + if (holderClass.getName().startsWith("net.minecraft.world.level.dimension.")) { + return dimensionTypeHolder; + } + + Method valueMethod = holderClass.getMethod("value"); + return valueMethod.invoke(dimensionTypeHolder); + } + + private static Object invokeNoArg(Object instance, String methodName) throws ReflectiveOperationException { + Method method = instance.getClass().getMethod(methodName); + return method.invoke(instance); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarArchetype.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarArchetype.java new file mode 100644 index 000000000..e54c875ca --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarArchetype.java @@ -0,0 +1,78 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype; + +import java.util.Objects; + +public enum JigsawPlanarArchetype { + BLANK("workcell/blank", JigsawPlanarTopology.BLANK), + END("workcell/end", JigsawPlanarTopology.NORTH_END), + STRAIGHT("workcell/straight", JigsawPlanarTopology.NORTH_SOUTH_STRAIGHT), + CORNER("workcell/corner", JigsawPlanarTopology.NORTH_EAST_CORNER), + TEE("workcell/tee", JigsawPlanarTopology.NORTH_EAST_WEST_TEE), + CROSS("workcell/cross", JigsawPlanarTopology.CROSS); + + private final String stableId; + private final JigsawPlanarTopology canonicalTopology; + + JigsawPlanarArchetype(String stableId, JigsawPlanarTopology canonicalTopology) { + this.stableId = stableId; + this.canonicalTopology = canonicalTopology; + } + + public static JigsawPlanarArchetype fromTopology(JigsawPlanarTopology topology) { + JigsawPlanarTopology source = Objects.requireNonNull(topology, "Planar topology"); + return switch (source.kind()) { + case BLANK -> BLANK; + case END -> END; + case STRAIGHT -> STRAIGHT; + case CORNER -> CORNER; + case TEE -> TEE; + case CROSS -> CROSS; + }; + } + + public static JigsawPlanarArchetype fromModel(IrisJigsawWorkcellArchetype archetype) { + return valueOf(Objects.requireNonNull(archetype, "Planar workcell archetype").name()); + } + + public String stableId() { + return stableId; + } + + public JigsawPlanarTopology canonicalTopology() { + return canonicalTopology; + } + + public IrisJigsawWorkcellArchetype modelArchetype() { + return IrisJigsawWorkcellArchetype.valueOf(name()); + } + + public String displayName() { + return switch (this) { + case BLANK -> "Blank"; + case END -> "End Cap"; + case STRAIGHT -> "Hallway"; + case CORNER -> "L Junction"; + case TEE -> "T Junction"; + case CROSS -> "Cross Junction"; + }; + } + + public int sourceToCanonicalQuarterTurns(JigsawPlanarTopology sourceTopology) { + JigsawPlanarTopology source = Objects.requireNonNull(sourceTopology, "Source planar topology"); + if (fromTopology(source) != this) { + throw new IllegalArgumentException("Topology " + source + " does not belong to archetype " + this); + } + for (int quarterTurns = 0; quarterTurns < 4; quarterTurns++) { + if (source.rotateClockwise(quarterTurns) == canonicalTopology) { + return quarterTurns; + } + } + throw new IllegalStateException("No canonical rotation exists for topology " + source); + } + + public int canonicalToSourceQuarterTurns(JigsawPlanarTopology sourceTopology) { + return Math.floorMod(-sourceToCanonicalQuarterTurns(sourceTopology), 4); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarDirection.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarDirection.java new file mode 100644 index 000000000..0bbb80bc0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarDirection.java @@ -0,0 +1,38 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.engine.object.IrisDirection; + +public enum JigsawPlanarDirection { + NORTH(1), + EAST(2), + SOUTH(4), + WEST(8); + + private final int bit; + + JigsawPlanarDirection(int bit) { + this.bit = bit; + } + + public int bit() { + return bit; + } + + public IrisDirection irisDirection() { + return switch (this) { + case NORTH -> IrisDirection.NORTH_NEGATIVE_Z; + case EAST -> IrisDirection.EAST_POSITIVE_X; + case SOUTH -> IrisDirection.SOUTH_POSITIVE_Z; + case WEST -> IrisDirection.WEST_NEGATIVE_X; + }; + } + + public JigsawPlanarDirection rotateClockwise(int quarterTurns) { + int normalizedTurns = Math.floorMod(quarterTurns, values().length); + return values()[(ordinal() + normalizedTurns) % values().length]; + } + + public JigsawPlanarDirection opposite() { + return rotateClockwise(2); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarTopology.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarTopology.java new file mode 100644 index 000000000..8f59b28e5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarTopology.java @@ -0,0 +1,100 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +public enum JigsawPlanarTopology { + BLANK(0), + NORTH_END(1), + EAST_END(2), + NORTH_EAST_CORNER(3), + SOUTH_END(4), + NORTH_SOUTH_STRAIGHT(5), + EAST_SOUTH_CORNER(6), + NORTH_EAST_SOUTH_TEE(7), + WEST_END(8), + NORTH_WEST_CORNER(9), + EAST_WEST_STRAIGHT(10), + NORTH_EAST_WEST_TEE(11), + SOUTH_WEST_CORNER(12), + NORTH_SOUTH_WEST_TEE(13), + EAST_SOUTH_WEST_TEE(14), + CROSS(15); + + private static final JigsawPlanarTopology[] BY_MASK = buildMaskIndex(); + + private final int mask; + private final JigsawPlanarTopologyKind kind; + private final Set directions; + + JigsawPlanarTopology(int mask) { + this.mask = mask; + this.kind = resolveKind(mask); + this.directions = Collections.unmodifiableSet(resolveDirections(mask)); + } + + public static JigsawPlanarTopology fromMask(int mask) { + if (mask < 0 || mask >= BY_MASK.length) { + throw new IllegalArgumentException("Planar topology mask must be between 0 and 15"); + } + return BY_MASK[mask]; + } + + public int mask() { + return mask; + } + + public JigsawPlanarTopologyKind kind() { + return kind; + } + + public Set directions() { + return directions; + } + + public boolean connects(JigsawPlanarDirection direction) { + return directions.contains(direction); + } + + public JigsawPlanarTopology rotateClockwise(int quarterTurns) { + int normalizedTurns = Math.floorMod(quarterTurns, 4); + if (normalizedTurns == 0 || mask == 0 || mask == 15) { + return this; + } + int rotatedMask = ((mask << normalizedTurns) | (mask >>> (4 - normalizedTurns))) & 15; + return fromMask(rotatedMask); + } + + private static JigsawPlanarTopology[] buildMaskIndex() { + JigsawPlanarTopology[] index = new JigsawPlanarTopology[16]; + for (JigsawPlanarTopology topology : values()) { + index[topology.mask] = topology; + } + return index; + } + + private static EnumSet resolveDirections(int mask) { + EnumSet result = EnumSet.noneOf(JigsawPlanarDirection.class); + for (JigsawPlanarDirection direction : JigsawPlanarDirection.values()) { + if ((mask & direction.bit()) != 0) { + result.add(direction); + } + } + return result; + } + + private static JigsawPlanarTopologyKind resolveKind(int mask) { + int connectionCount = Integer.bitCount(mask); + return switch (connectionCount) { + case 0 -> JigsawPlanarTopologyKind.BLANK; + case 1 -> JigsawPlanarTopologyKind.END; + case 2 -> mask == 5 || mask == 10 + ? JigsawPlanarTopologyKind.STRAIGHT + : JigsawPlanarTopologyKind.CORNER; + case 3 -> JigsawPlanarTopologyKind.TEE; + case 4 -> JigsawPlanarTopologyKind.CROSS; + default -> throw new IllegalArgumentException("Invalid planar topology mask " + mask); + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarTopologyKind.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarTopologyKind.java new file mode 100644 index 000000000..ecf50678d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawPlanarTopologyKind.java @@ -0,0 +1,10 @@ +package art.arcane.iris.core.runtime.jigsaw; + +public enum JigsawPlanarTopologyKind { + BLANK, + END, + STRAIGHT, + CORNER, + TEE, + CROSS +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioActivation.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioActivation.java new file mode 100644 index 000000000..34132e38e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioActivation.java @@ -0,0 +1,409 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.core.loader.IrisData; + +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +public final class JigsawStudioActivation { + private static final Map ACTIVE = new ConcurrentHashMap<>(); + private static final AtomicReference OPENING_OWNER = new AtomicReference<>(); + private static final AtomicReference STAGED = new AtomicReference<>(); + + private JigsawStudioActivation() { + } + + public static Request activate( + String packKey, + String structureKey, + JigsawStudioMode mode, + JigsawStudioCompatibilityTarget compatibilityTarget, + JigsawStudioCellDimensions cellDimensions, + IrisData source + ) { + JigsawStudioLayout layout = JigsawStudioLayout.create( + mode, + cellDimensions, + JigsawStudioVariantCatalog.empty()); + return activate( + packKey, + structureKey, + mode, + compatibilityTarget, + cellDimensions, + source, + layout, + null + ); + } + + public static Request activate( + String packKey, + String structureKey, + JigsawStudioMode mode, + JigsawStudioCompatibilityTarget compatibilityTarget, + JigsawStudioCellDimensions cellDimensions, + IrisData source, + JigsawStudioLayout initialLayout + ) { + return activate( + packKey, + structureKey, + mode, + compatibilityTarget, + cellDimensions, + source, + initialLayout, + null + ); + } + + public static synchronized Request activate( + String packKey, + String structureKey, + JigsawStudioMode mode, + JigsawStudioCompatibilityTarget compatibilityTarget, + JigsawStudioCellDimensions cellDimensions, + IrisData source, + JigsawStudioLayout initialLayout, + UUID ownerId + ) { + if (STAGED.get() != null) { + throw new IllegalStateException("A Jigsaw Studio replacement is already staged"); + } + if (ownerId != null && !ownerId.equals(OPENING_OWNER.get())) { + throw new IllegalStateException("Jigsaw Studio activation does not own the active opening lease"); + } + UUID activeOwnerId = activeOwnerId(); + if (ownerId != null && activeOwnerId != null && !ownerId.equals(activeOwnerId)) { + throw new IllegalStateException("Jigsaw Studio is owned by another player session"); + } + ActiveRequest activeRequest = createActiveRequest( + packKey, + structureKey, + mode, + compatibilityTarget, + cellDimensions, + source, + initialLayout, + ownerId); + ACTIVE.put(normalize(packKey), activeRequest); + return activeRequest.request(); + } + + public static synchronized StagedActivation stage( + String packKey, + String structureKey, + JigsawStudioMode mode, + JigsawStudioCompatibilityTarget compatibilityTarget, + JigsawStudioCellDimensions cellDimensions, + IrisData source, + JigsawStudioLayout initialLayout, + UUID ownerId, + UUID preservedRequestId + ) { + UUID openingOwner = OPENING_OWNER.get(); + UUID activeOwner = activeOwnerId(); + UUID requestedOwner = Objects.requireNonNull(ownerId, "Jigsaw Studio staged owner ID"); + if (!requestedOwner.equals(openingOwner)) { + throw new IllegalStateException("Jigsaw Studio staging does not own the active opening lease"); + } + if (activeOwner != null && !requestedOwner.equals(activeOwner)) { + throw new IllegalStateException("Jigsaw Studio is owned by another player session"); + } + if (STAGED.get() != null) { + throw new IllegalStateException("A Jigsaw Studio replacement is already staged"); + } + ActiveRequest preserved = preservedRequestId == null ? null : active(preservedRequestId); + if (preservedRequestId != null && preserved == null) { + throw new IllegalStateException("The Jigsaw Studio being replaced is no longer active"); + } + if (activeOwner != null && preserved == null) { + throw new IllegalStateException("The active Jigsaw Studio must be preserved during replacement"); + } + if (preserved != null && preserved.request().ownerId() != null + && !requestedOwner.equals(preserved.request().ownerId())) { + throw new IllegalStateException("The Jigsaw Studio being replaced belongs to another player session"); + } + ActiveRequest candidate = createActiveRequest( + packKey, + structureKey, + mode, + compatibilityTarget, + cellDimensions, + source, + initialLayout, + requestedOwner); + StagedActivation staged = new StagedActivation( + UUID.randomUUID(), candidate.request(), candidate.session()); + STAGED.set(new StagedState(staged, candidate, preserved, false)); + return staged; + } + + public static synchronized boolean beginStagedGeneration(StagedActivation expected) { + StagedState state = stagedState(expected); + if (state == null || state.generationVisible()) { + return false; + } + STAGED.set(new StagedState(state.staged(), state.candidate(), state.preserved(), true)); + return true; + } + + public static synchronized boolean commit(StagedActivation expected) { + StagedState state = stagedState(expected); + if (state == null || !state.generationVisible()) { + return false; + } + if (state.preserved() != null) { + ACTIVE.remove(normalize(state.preserved().request().packKey()), state.preserved()); + } + ACTIVE.put(normalize(state.candidate().request().packKey()), state.candidate()); + STAGED.set(null); + return true; + } + + public static synchronized boolean rollback(StagedActivation expected) { + StagedState state = stagedState(expected); + if (state == null) { + return false; + } + STAGED.set(null); + return true; + } + + public static Request getGeneratorRequest(String packKey) { + StagedState state = STAGED.get(); + if (state != null && state.generationVisible() + && normalize(state.candidate().request().packKey()).equals(normalize(packKey))) { + return state.candidate().request(); + } + return getRequest(packKey); + } + + public static JigsawStudioSession getGeneratorSession(String packKey) { + StagedState state = STAGED.get(); + if (state != null && state.generationVisible() + && normalize(state.candidate().request().packKey()).equals(normalize(packKey))) { + return state.candidate().session(); + } + return getSession(packKey); + } + + public static boolean tryBeginOpen(UUID ownerId) { + UUID requestedOwner = Objects.requireNonNull(ownerId, "Jigsaw Studio opening owner ID"); + UUID activeOwnerId = activeOwnerId(); + if (activeOwnerId != null && !requestedOwner.equals(activeOwnerId)) { + return false; + } + return OPENING_OWNER.compareAndSet(null, requestedOwner); + } + + public static void finishOpen(UUID ownerId) { + if (ownerId != null) { + OPENING_OWNER.compareAndSet(ownerId, null); + } + } + + public static UUID openingOwnerId() { + return OPENING_OWNER.get(); + } + + public static UUID activeOwnerId() { + for (ActiveRequest activeRequest : ACTIVE.values()) { + UUID ownerId = activeRequest.request().ownerId(); + if (ownerId != null) { + return ownerId; + } + } + return null; + } + + public static void deactivate(String packKey) { + if (packKey != null) { + ACTIVE.remove(normalize(packKey)); + StagedState state = STAGED.get(); + if (state != null && (normalize(state.candidate().request().packKey()).equals(normalize(packKey)) + || state.preserved() != null && normalize(state.preserved().request().packKey()) + .equals(normalize(packKey)))) { + STAGED.compareAndSet(state, null); + } + } + } + + public static boolean deactivate(String packKey, UUID requestId) { + if (packKey == null || requestId == null) { + return false; + } + StagedState staged = STAGED.get(); + if (staged != null && staged.preserved() != null + && requestId.equals(staged.preserved().request().requestId()) + && normalize(packKey).equals(normalize(staged.preserved().request().packKey()))) { + return false; + } + AtomicBoolean removed = new AtomicBoolean(false); + ACTIVE.computeIfPresent(normalize(packKey), (key, activeRequest) -> { + if (!requestId.equals(activeRequest.request().requestId())) { + return activeRequest; + } + removed.set(true); + return null; + }); + return removed.get(); + } + + public static boolean isActive(String packKey) { + return getRequest(packKey) != null; + } + + public static Request getRequest(String packKey) { + ActiveRequest activeRequest = active(packKey); + return activeRequest == null ? null : activeRequest.request(); + } + + public static JigsawStudioSession getSession(String packKey) { + ActiveRequest activeRequest = active(packKey); + return activeRequest == null ? null : activeRequest.session(); + } + + public static JigsawStudioLayout getLayout(String packKey) { + JigsawStudioSession session = getSession(packKey); + return session == null ? null : session.layout(); + } + + public static Request getRequest(UUID requestId) { + ActiveRequest activeRequest = active(requestId); + return activeRequest == null ? null : activeRequest.request(); + } + + public static JigsawStudioSession getSession(UUID requestId) { + ActiveRequest activeRequest = active(requestId); + return activeRequest == null ? null : activeRequest.session(); + } + + private static ActiveRequest active(String packKey) { + if (packKey == null) { + return null; + } + return ACTIVE.get(normalize(packKey)); + } + + private static ActiveRequest active(UUID requestId) { + if (requestId == null) { + return null; + } + for (ActiveRequest activeRequest : ACTIVE.values()) { + if (requestId.equals(activeRequest.request().requestId())) { + return activeRequest; + } + } + return null; + } + + private static ActiveRequest createActiveRequest( + String packKey, + String structureKey, + JigsawStudioMode mode, + JigsawStudioCompatibilityTarget compatibilityTarget, + JigsawStudioCellDimensions cellDimensions, + IrisData source, + JigsawStudioLayout initialLayout, + UUID ownerId + ) { + Request request = new Request( + UUID.randomUUID(), + packKey, + structureKey, + mode, + compatibilityTarget, + cellDimensions, + source, + ownerId); + JigsawStudioLayout layout = Objects.requireNonNull(initialLayout, "Initial Jigsaw Studio layout"); + if (layout.mode() != mode) { + throw new IllegalArgumentException("Initial Jigsaw Studio layout mode does not match the request"); + } + if (!layout.cellDimensions().equals(cellDimensions)) { + throw new IllegalArgumentException("Initial Jigsaw Studio layout cell dimensions do not match the request"); + } + JigsawStudioSession session = new JigsawStudioSession( + request.requestId(), + request.packKey(), + request.structureKey(), + layout); + return new ActiveRequest(request, session); + } + + private static StagedState stagedState(StagedActivation expected) { + StagedActivation staged = Objects.requireNonNull(expected, "Staged Jigsaw Studio activation"); + StagedState state = STAGED.get(); + return state != null && state.staged().stageId().equals(staged.stageId()) ? state : null; + } + + private static String normalize(String key) { + return key.trim().toLowerCase(Locale.ROOT); + } + + private record ActiveRequest(Request request, JigsawStudioSession session) { + } + + private record StagedState( + StagedActivation staged, + ActiveRequest candidate, + ActiveRequest preserved, + boolean generationVisible + ) { + } + + public record StagedActivation( + UUID stageId, + Request request, + JigsawStudioSession session + ) { + public StagedActivation { + Objects.requireNonNull(stageId, "Jigsaw Studio stage ID"); + Objects.requireNonNull(request, "Jigsaw Studio staged request"); + Objects.requireNonNull(session, "Jigsaw Studio staged session"); + if (!request.requestId().equals(session.sessionId())) { + throw new IllegalArgumentException("Staged Jigsaw Studio request and session IDs do not match"); + } + } + } + + public record Request( + UUID requestId, + String packKey, + String structureKey, + JigsawStudioMode mode, + JigsawStudioCompatibilityTarget compatibilityTarget, + JigsawStudioCellDimensions cellDimensions, + IrisData source, + UUID ownerId + ) { + public Request { + requestId = Objects.requireNonNull(requestId, "Jigsaw Studio request ID"); + packKey = requireKey(packKey, "pack"); + structureKey = requireKey(structureKey, "structure"); + mode = Objects.requireNonNull(mode, "Jigsaw Studio mode"); + compatibilityTarget = Objects.requireNonNull( + compatibilityTarget, + "Jigsaw Studio compatibility target" + ); + cellDimensions = Objects.requireNonNull(cellDimensions, "Jigsaw Studio cell dimensions"); + source = Objects.requireNonNull(source, "Jigsaw Studio source data"); + } + + private static String requireKey(String value, String name) { + Objects.requireNonNull(value, "Jigsaw Studio " + name + " key"); + String normalized = value.trim(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio " + name + " key cannot be blank"); + } + return normalized; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioAuthoringAccess.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioAuthoringAccess.java new file mode 100644 index 000000000..207c43d57 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioAuthoringAccess.java @@ -0,0 +1,31 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; + +import java.io.IOException; +import java.util.Objects; + +public final class JigsawStudioAuthoringAccess { + private JigsawStudioAuthoringAccess() { + } + + public static boolean isEditable(StructureOwnershipManifest manifest) { + StructureOwnershipManifest ownership = Objects.requireNonNull( + manifest, + "Jigsaw Studio ownership manifest"); + return ownership.provenance().origin() != StructureOwnershipManifest.Origin.MANAGED_DATAPACK; + } + + public static StructureOwnershipManifest requireEditable( + StructureOwnershipManifest manifest + ) throws IOException { + StructureOwnershipManifest ownership = Objects.requireNonNull( + manifest, + "Jigsaw Studio ownership manifest"); + if (!isEditable(ownership)) { + throw new IOException("This graph is read-only because it is managed by datapack ingest; " + + "adopt or clone it before making authoring changes."); + } + return ownership; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBay.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBay.java new file mode 100644 index 000000000..e239d46a5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBay.java @@ -0,0 +1,62 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.Objects; +import java.util.Optional; + +public record JigsawStudioBay( + String stableId, + JigsawStudioBayKind kind, + Optional workcellSpec, + String authorDisplayName, + JigsawStudioBounds bounds +) { + public JigsawStudioBay { + stableId = requireStableId(stableId); + kind = Objects.requireNonNull(kind, "Jigsaw Studio bay kind"); + workcellSpec = Objects.requireNonNull(workcellSpec, "Jigsaw Studio workcell specification"); + authorDisplayName = authorDisplayName == null ? "" : authorDisplayName.trim(); + bounds = Objects.requireNonNull(bounds, "Jigsaw Studio bay bounds"); + if (kind == JigsawStudioBayKind.PLANAR_WORKCELL && workcellSpec.isEmpty()) { + throw new IllegalArgumentException("Planar Jigsaw Studio workcells require an archetype"); + } + if (kind == JigsawStudioBayKind.SPATIAL_WORKCELL && workcellSpec.isPresent()) { + throw new IllegalArgumentException("Spatial Jigsaw Studio workcells cannot declare an archetype"); + } + if (workcellSpec.isPresent() && !workcellSpec.get().dimensions().equals(bounds.dimensions())) { + throw new IllegalArgumentException("Jigsaw Studio workcell specification dimensions do not match bounds"); + } + } + + public Optional archetype() { + return workcellSpec.map(JigsawStudioWorkcellSpec::archetype); + } + + public boolean enabled() { + return workcellSpec.map(JigsawStudioWorkcellSpec::enabled).orElse(true); + } + + public String canonicalDisplayName() { + return archetype().map(JigsawPlanarArchetype::displayName).orElse("Spatial"); + } + + public String displayName() { + return authorDisplayName.isEmpty() ? canonicalDisplayName() : authorDisplayName; + } + + public JigsawStudioCellDimensions capacity() { + return bounds.dimensions(); + } + + public Optional topology() { + return archetype().map(JigsawPlanarArchetype::canonicalTopology); + } + + private static String requireStableId(String value) { + Objects.requireNonNull(value, "Jigsaw Studio bay stable ID"); + String normalized = value.trim(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio bay stable ID cannot be blank"); + } + return normalized; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBayKind.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBayKind.java new file mode 100644 index 000000000..9a19e0647 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBayKind.java @@ -0,0 +1,6 @@ +package art.arcane.iris.core.runtime.jigsaw; + +public enum JigsawStudioBayKind { + PLANAR_WORKCELL, + SPATIAL_WORKCELL +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBounds.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBounds.java new file mode 100644 index 000000000..7578ee6c8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioBounds.java @@ -0,0 +1,37 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.Objects; + +public record JigsawStudioBounds( + int originX, + int originY, + int originZ, + JigsawStudioCellDimensions dimensions +) { + public JigsawStudioBounds { + dimensions = Objects.requireNonNull(dimensions, "Jigsaw Studio bounds dimensions"); + } + + public int maxX() { + return originX + dimensions.width() - 1; + } + + public int maxY() { + return originY + dimensions.height() - 1; + } + + public int maxZ() { + return originZ + dimensions.depth() - 1; + } + + public boolean contains(int worldX, int worldY, int worldZ) { + return worldX >= originX && worldX <= maxX() + && worldY >= originY && worldY <= maxY() + && worldZ >= originZ && worldZ <= maxZ(); + } + + public boolean intersectsHorizontal(int minX, int minZ, int maxX, int maxZ) { + return this.maxX() >= minX && originX <= maxX + && this.maxZ() >= minZ && originZ <= maxZ; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioCellDimensions.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioCellDimensions.java new file mode 100644 index 000000000..342f0e2b6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioCellDimensions.java @@ -0,0 +1,30 @@ +package art.arcane.iris.core.runtime.jigsaw; + +public record JigsawStudioCellDimensions(int width, int height, int depth) { + public static final int MAX_HORIZONTAL_AXIS = 128; + public static final int MAX_HEIGHT = 192; + public static final long MAX_VOLUME = 2_097_152L; + + public JigsawStudioCellDimensions { + if (width < 1 || height < 1 || depth < 1) { + throw new IllegalArgumentException("Jigsaw Studio cell dimensions must be positive"); + } + if (width > MAX_HORIZONTAL_AXIS || depth > MAX_HORIZONTAL_AXIS) { + throw new IllegalArgumentException("Jigsaw Studio cell width and depth cannot exceed " + + MAX_HORIZONTAL_AXIS + " blocks"); + } + if (height > MAX_HEIGHT) { + throw new IllegalArgumentException("Jigsaw Studio cell height cannot exceed " + + MAX_HEIGHT + " blocks"); + } + long volume = (long) width * height * depth; + if (volume > MAX_VOLUME) { + throw new IllegalArgumentException("Jigsaw Studio cell volume cannot exceed " + + MAX_VOLUME + " blocks"); + } + } + + public long volume() { + return (long) width * height * depth; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioCompatibilityTarget.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioCompatibilityTarget.java new file mode 100644 index 000000000..e2262ebca --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioCompatibilityTarget.java @@ -0,0 +1,6 @@ +package art.arcane.iris.core.runtime.jigsaw; + +public enum JigsawStudioCompatibilityTarget { + IRIS_EXTENDED, + VANILLA_PORTABLE +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioControlPosition.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioControlPosition.java new file mode 100644 index 000000000..25b709f54 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioControlPosition.java @@ -0,0 +1,4 @@ +package art.arcane.iris.core.runtime.jigsaw; + +public record JigsawStudioControlPosition(int worldX, int worldY, int worldZ) { +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioGraphEditor.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioGraphEditor.java new file mode 100644 index 000000000..4e363afcc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioGraphEditor.java @@ -0,0 +1,1888 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.authoring.StructureWriteOptions; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import art.arcane.iris.engine.framework.structure.PlanarJigsawWorkcellResolver; +import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawMode; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.JigsawJoint; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.IrisBlockVector; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public final class JigsawStudioGraphEditor { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private JigsawStudioGraphEditor() { + } + + public static StructureWriteResult createPiece( + Path packRoot, + String structureKey, + String poolKey, + String pieceKey, + int weight, + JigsawStudioCellDimensions dimensions, + JigsawPlanarTopology topology + ) throws IOException { + if (weight < 1) { + throw new IllegalArgumentException("Jigsaw piece weight must be positive"); + } + String normalizedPiece = JigsawStudioProjectCreator.Options.requireResourceKey(pieceKey); + String normalizedPool = JigsawStudioProjectCreator.Options.requireResourceKey(poolKey); + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String poolResource = "jigsaw-pools/" + normalizedPool + ".json"; + if (!graph.manifest().resourceHashes().containsKey(poolResource)) { + throw new IOException("The target pool is not owned by this jigsaw project: " + poolKey); + } + String pieceResource = "jigsaw-pieces/" + normalizedPiece + ".json"; + String objectResource = "objects/" + normalizedPiece + ".iob"; + if (graph.manifest().resourceHashes().containsKey(pieceResource) + || graph.manifest().resourceHashes().containsKey(objectResource)) { + throw new IOException("The jigsaw project already owns piece or object '" + normalizedPiece + "'"); + } + + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + for (Map.Entry resource : graph.manifest().resourceHashes().entrySet()) { + Path resourcePath = resolveOwnedResource(graph.root(), resource.getKey()); + byte[] content = Files.readAllBytes(resourcePath); + if (resource.getKey().equals(poolResource)) { + content = addPoolEntry(content, normalizedPiece, weight, resourcePath); + } + bundle.resource(resource.getKey(), content); + } + IrisJigsawPiece piece = new IrisJigsawPiece() + .setObject(normalizedPiece) + .setRotatable(true); + if (topology != null) { + IrisPosition cellSize = new IrisPosition( + dimensions.width(), dimensions.height(), dimensions.depth()); + for (JigsawPlanarDirection planarDirection : topology.directions()) { + IrisDirection direction = planarDirection.irisDirection(); + piece.getConnectors().add(new IrisJigsawConnector() + .setPosition(IrisJigsawConnector.canonicalPlanarPosition(cellSize, direction)) + .setDirection(direction) + .setTop(IrisDirection.UP_POSITIVE_Y) + .setPool(normalizedPool) + .setName("iris:planar") + .setTargetName("iris:planar") + .setJoint(JigsawJoint.ALIGNED) + .setFinalState("minecraft:structure_void")); + } + } + IrisObject object = new IrisObject(dimensions.width(), dimensions.height(), dimensions.depth()); + bundle.textResource(pieceResource, GSON.toJson(piece) + "\n"); + bundle.resource(objectResource, JigsawStudioProjectCreator.serialize(object)); + return write(graph, bundle.build()); + } + + public static StructureWriteResult duplicatePiece( + Path packRoot, + String structureKey, + String sourcePieceKey, + String targetPieceKey + ) throws IOException { + return createVariantFromPiece( + packRoot, + structureKey, + sourcePieceKey, + targetPieceKey, + VariantObjectMode.COPY_SOURCE); + } + + public static StructureWriteResult createBlankVariant( + Path packRoot, + String structureKey, + String sourcePieceKey, + String targetPieceKey + ) throws IOException { + return createVariantFromPiece( + packRoot, + structureKey, + sourcePieceKey, + targetPieceKey, + VariantObjectMode.EMPTY_SOURCE_SIZE); + } + + public static VariantFamilyCreation duplicateActiveFamily( + Path packRoot, + String structureKey, + Map sourcePieceKeysByWorkcell, + String newThemeKey + ) throws IOException { + Map requestedSources = Map.copyOf(Objects.requireNonNull( + sourcePieceKeysByWorkcell, + "Jigsaw Studio theme-set source pieces")); + String themeKey = JigsawStudioProjectCreator.Options.requireResourceKey(newThemeKey); + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String structureResource = "structures/" + graph.manifest().structure().path() + ".json"; + if (!graph.manifest().resourceHashes().containsKey(structureResource)) { + throw new IOException("The owned graph manifest does not include " + structureResource); + } + IrisStructure structure = readStructure( + resolveOwnedResource(graph.root(), structureResource), + structureResource); + Map expectedSources = expectedThemeSetSources( + structure, + requestedSources.keySet()); + requireExactThemeSetSources(requestedSources, expectedSources.keySet()); + + Map resources = readOwnedResources(graph); + resources.put( + structureResource, + appendThemeSet(resources.get(structureResource), themeKey, structureResource)); + Map newPieceKeysByWorkcell = new LinkedHashMap<>(); + Map newPieceKeysBySource = new LinkedHashMap<>(); + for (Map.Entry expected : expectedSources.entrySet()) { + String stableId = expected.getKey(); + String sourcePieceKey = JigsawStudioProjectCreator.Options.requireResourceKey( + requestedSources.get(stableId)); + if (newPieceKeysBySource.containsKey(sourcePieceKey)) { + throw new IOException("Theme-set source piece '" + sourcePieceKey + + "' was selected for more than one workcell"); + } + String sourcePieceResource = "jigsaw-pieces/" + sourcePieceKey + ".json"; + if (!graph.manifest().resourceHashes().containsKey(sourcePieceResource)) { + throw new IOException("Theme-set source piece '" + sourcePieceKey + + "' is not owned by this jigsaw project"); + } + IrisJigsawPiece sourcePiece = readPiece( + resolveOwnedResource(graph.root(), sourcePieceResource), + sourcePieceResource); + JigsawPlanarArchetype expectedArchetype = expected.getValue(); + if (expectedArchetype != null + && IrisJigsawWorkcellArchetype.fromPiece(sourcePiece) + != expectedArchetype.modelArchetype()) { + throw new IOException("Theme-set source piece '" + sourcePieceKey + + "' does not belong to " + stableId); + } + if (sourcePiece.getObject() == null || sourcePiece.getObject().isBlank()) { + throw new IOException("Theme-set source piece '" + sourcePieceKey + + "' does not declare an object"); + } + String sourceObjectKey = JigsawStudioProjectCreator.Options.requireResourceKey(sourcePiece.getObject()); + String sourceObjectResource = "objects/" + sourceObjectKey + ".iob"; + if (!graph.manifest().resourceHashes().containsKey(sourceObjectResource)) { + throw new IOException("Theme-set source object '" + sourceObjectKey + + "' is not owned by this jigsaw project"); + } + String variantFolder = expectedArchetype == null + ? expectedSources.size() == 1 + ? "spatial" + : "spatial/" + sourcePieceKey + : expectedArchetype.name().toLowerCase(Locale.ROOT); + String targetPieceKey = graph.manifest().structure().path() + + "/variants/" + variantFolder + "/" + themeKey; + String targetPieceResource = "jigsaw-pieces/" + targetPieceKey + ".json"; + String targetObjectResource = "objects/" + targetPieceKey + ".iob"; + requireAvailableThemeSetTarget(graph, targetPieceResource); + requireAvailableThemeSetTarget(graph, targetObjectResource); + resources.put( + targetPieceResource, + duplicatePieceForTheme( + resources.get(sourcePieceResource), + targetPieceKey, + themeKey, + sourcePieceResource)); + resources.put(targetObjectResource, resources.get(sourceObjectResource).clone()); + newPieceKeysByWorkcell.put(stableId, targetPieceKey); + newPieceKeysBySource.put(sourcePieceKey, targetPieceKey); + } + + for (Map.Entry resource : new ArrayList<>(resources.entrySet())) { + String relativePath = resource.getKey(); + if (!relativePath.startsWith("jigsaw-pools/") || !relativePath.endsWith(".json")) { + continue; + } + resources.put( + relativePath, + duplicatePoolMemberships( + resource.getValue(), + newPieceKeysBySource, + relativePath).content()); + } + + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + for (Map.Entry resource : resources.entrySet()) { + bundle.resource(resource.getKey(), resource.getValue()); + } + StructureWriteResult result = write(graph, bundle.build()); + return new VariantFamilyCreation(newPieceKeysByWorkcell, result); + } + + public static String nextVariantKey( + Path packRoot, + String structureKey, + String archetypeKey + ) throws IOException { + String normalizedStructure = JigsawStudioProjectCreator.Options.requireResourceKey(structureKey); + String normalizedArchetype = JigsawStudioProjectCreator.Options.requireResourceKey(archetypeKey); + OwnedGraph graph = loadOwnedGraph(packRoot, normalizedStructure); + String prefix = normalizedStructure + "/variants/" + normalizedArchetype + "/variant-"; + for (int index = 1; index <= 100_000; index++) { + String candidate = prefix + index; + if (!graph.manifest().resourceHashes().containsKey("jigsaw-pieces/" + candidate + ".json") + && !graph.manifest().resourceHashes().containsKey("objects/" + candidate + ".iob")) { + return candidate; + } + } + throw new IOException("No deterministic variant key remains below " + prefix); + } + + public static List ownedPoolKeys(Path packRoot, String structureKey) throws IOException { + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + List pools = new ArrayList<>(); + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + if (!relativePath.startsWith("jigsaw-pools/") || !relativePath.endsWith(".json")) { + continue; + } + pools.add(relativePath.substring("jigsaw-pools/".length(), relativePath.length() - ".json".length())); + } + pools.sort(Comparator.naturalOrder()); + return List.copyOf(pools); + } + + public static boolean ownsPiece( + Path packRoot, + String structureKey, + String pieceKey, + String objectKey + ) throws IOException { + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String normalizedPiece = JigsawStudioProjectCreator.Options.requireResourceKey(pieceKey); + String normalizedObject = JigsawStudioProjectCreator.Options.requireResourceKey(objectKey); + return graph.manifest().resourceHashes().containsKey("jigsaw-pieces/" + normalizedPiece + ".json") + && graph.manifest().resourceHashes().containsKey("objects/" + normalizedObject + ".iob"); + } + + public static StructureWriteResult createPool( + Path packRoot, + String structureKey, + String poolKey, + String fallbackPoolKey + ) throws IOException { + String normalizedPool = JigsawStudioProjectCreator.Options.requireResourceKey(poolKey); + String fallback = fallbackPoolKey == null ? "" : fallbackPoolKey.trim(); + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String poolResource = "jigsaw-pools/" + normalizedPool + ".json"; + if (graph.manifest().resourceHashes().containsKey(poolResource)) { + throw new IOException("The jigsaw project already owns pool '" + normalizedPool + "'"); + } + if (!fallback.isBlank()) { + String fallbackResource = "jigsaw-pools/" + + JigsawStudioProjectCreator.Options.requireResourceKey(fallback) + ".json"; + if (!graph.manifest().resourceHashes().containsKey(fallbackResource)) { + throw new IOException("Fallback pool '" + fallback + "' is not owned by this jigsaw project"); + } + } + + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + bundle.resource(relativePath, Files.readAllBytes(resolveOwnedResource(graph.root(), relativePath))); + } + IrisJigsawPool pool = new IrisJigsawPool().setFallback(fallback); + bundle.textResource(poolResource, GSON.toJson(pool) + "\n"); + return write(graph, bundle.build()); + } + + public static StructureWriteResult updateRotatable( + Path packRoot, + String structureKey, + String pieceKey, + boolean rotatable + ) throws IOException { + String normalizedPiece = JigsawStudioProjectCreator.Options.requireResourceKey(pieceKey); + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String pieceResource = "jigsaw-pieces/" + normalizedPiece + ".json"; + if (!graph.manifest().resourceHashes().containsKey(pieceResource)) { + throw new IOException("Piece '" + normalizedPiece + "' is not owned by this jigsaw project"); + } + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + byte[] content = Files.readAllBytes(resolveOwnedResource(graph.root(), relativePath)); + if (relativePath.equals(pieceResource)) { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw piece is not a JSON object: " + pieceResource); + } + JsonObject piece = parsed.getAsJsonObject(); + piece.addProperty("rotatable", rotatable); + content = (GSON.toJson(piece) + "\n").getBytes(StandardCharsets.UTF_8); + } + bundle.resource(relativePath, content); + } + return write(graph, bundle.build()); + } + + public static StructureWriteResult updatePieceThemes( + Path packRoot, + String structureKey, + String pieceKey, + List themes + ) throws IOException { + List normalizedThemes = normalizeThemes(themes); + return updateOwnedPiece( + packRoot, + structureKey, + pieceKey, + (piece, pieceResource) -> { + JsonArray values = new JsonArray(); + for (String theme : normalizedThemes) { + values.add(theme); + } + piece.add("themes", values); + }); + } + + public static StructureWriteResult updatePieceDisplayName( + Path packRoot, + String structureKey, + String pieceKey, + String displayName + ) throws IOException { + String normalizedName = normalizeDisplayName(displayName); + return updateOwnedPiece( + packRoot, + structureKey, + pieceKey, + (piece, pieceResource) -> { + if (normalizedName.isEmpty()) { + piece.remove("displayName"); + } else { + piece.addProperty("displayName", normalizedName); + } + }); + } + + public static StructureWriteResult updatePieceRules( + Path packRoot, + String structureKey, + String pieceKey, + JigsawStudioPieceRules rules + ) throws IOException { + JigsawStudioPieceRules normalizedRules = Objects.requireNonNull( + rules, + "Jigsaw Studio piece rules"); + return updateOwnedPiece( + packRoot, + structureKey, + pieceKey, + (piece, pieceResource) -> { + JsonObject values = new JsonObject(); + values.addProperty("minimumDepth", normalizedRules.minimumDepth()); + values.addProperty("maximumDepth", normalizedRules.maximumDepth()); + values.addProperty("minimumPlacements", normalizedRules.minimumPlacements()); + values.addProperty("maximumPlacements", normalizedRules.maximumPlacements()); + values.addProperty("terminal", normalizedRules.terminal()); + piece.add("rules", values); + }); + } + + public static PieceDeletionResult deletePieceVariant( + Path packRoot, + String structureKey, + String pieceKey + ) throws IOException { + String normalizedPiece = JigsawStudioProjectCreator.Options.requireResourceKey(pieceKey); + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String pieceResource = "jigsaw-pieces/" + normalizedPiece + ".json"; + if (!graph.manifest().resourceHashes().containsKey(pieceResource)) { + throw new IOException("Piece '" + normalizedPiece + "' is not owned by this jigsaw project"); + } + IrisJigsawPiece targetPiece = readPiece( + resolveOwnedResource(graph.root(), pieceResource), + pieceResource); + if (targetPiece.getObject() == null || targetPiece.getObject().isBlank()) { + throw new IOException("Piece '" + normalizedPiece + "' does not declare an object"); + } + String objectKey = JigsawStudioProjectCreator.Options.requireResourceKey(targetPiece.getObject()); + String objectResource = "objects/" + objectKey + ".iob"; + boolean removeObject = graph.manifest().resourceHashes().containsKey(objectResource) + && !otherOwnedPieceReferencesObject(graph, normalizedPiece, objectKey); + requireDeletableArchetype(graph, normalizedPiece, targetPiece); + + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + int removedMemberships = 0; + int changedPools = 0; + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + if (relativePath.equals(pieceResource) || removeObject && relativePath.equals(objectResource)) { + continue; + } + byte[] content = Files.readAllBytes(resolveOwnedResource(graph.root(), relativePath)); + if (relativePath.startsWith("jigsaw-pools/") && relativePath.endsWith(".json")) { + PoolEntryRemoval removal = removePoolEntries(content, normalizedPiece, relativePath); + content = removal.content(); + removedMemberships += removal.removedEntries(); + if (removal.removedEntries() > 0) { + changedPools++; + } + } + bundle.resource(relativePath, content); + } + StructureResourceBundle updatedBundle = bundle.build(); + try { + StructureResourceBundleGraphCompiler.requireViable(updatedBundle); + } catch (RuntimeException exception) { + throw new IOException("Deleting piece '" + normalizedPiece + + "' would make the jigsaw graph non-viable: " + failureMessage(exception), exception); + } + StructureWriteResult result = writeCompiled(graph, updatedBundle); + return new PieceDeletionResult( + result, + removedMemberships, + changedPools, + 1, + removeObject ? 1 : 0); + } + + public static VariantResizeResult resizePieceObject( + Path packRoot, + String structureKey, + String pieceKey, + JigsawStudioCellDimensions dimensions + ) throws IOException { + String normalizedPiece = JigsawStudioProjectCreator.Options.requireResourceKey(pieceKey); + JigsawStudioCellDimensions targetDimensions = Objects.requireNonNull( + dimensions, + "Jigsaw Studio variant dimensions"); + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String pieceResource = "jigsaw-pieces/" + normalizedPiece + ".json"; + if (!graph.manifest().resourceHashes().containsKey(pieceResource)) { + throw new IOException("Piece '" + normalizedPiece + "' is not owned by this jigsaw project"); + } + IrisJigsawPiece piece = readPiece(resolveOwnedResource(graph.root(), pieceResource), pieceResource); + if (piece.getObject() == null || piece.getObject().isBlank()) { + throw new IOException("Piece '" + normalizedPiece + "' does not declare an object"); + } + String objectKey = JigsawStudioProjectCreator.Options.requireResourceKey(piece.getObject()); + String objectResource = "objects/" + objectKey + ".iob"; + if (!graph.manifest().resourceHashes().containsKey(objectResource)) { + throw new IOException("Object '" + objectKey + "' is not owned by this jigsaw project"); + } + requireExclusiveObjectReference(graph, normalizedPiece, objectKey); + Path objectPath = resolveOwnedResource(graph.root(), objectResource); + IrisObject source = new IrisObject(); + source.read(objectPath.toFile()); + String structureResource = "structures/" + + JigsawStudioProjectCreator.Options.requireResourceKey(structureKey) + ".json"; + IrisStructure structure = readStructure( + resolveOwnedResource(graph.root(), structureResource), + structureResource); + IrisObject resizedObject; + JigsawStudioCellDimensions sourceDimensions = new JigsawStudioCellDimensions( + source.getW(), + source.getH(), + source.getD()); + JigsawStudioCellDimensions previousDimensions; + int relocatedConnectors = 0; + if (structure.resolvedMode() == IrisJigsawMode.PLANAR_JIGSAW) { + IrisJigsawWorkcellArchetype archetype = IrisJigsawWorkcellArchetype.fromPiece(piece); + JigsawPlanarArchetype planarArchetype = JigsawPlanarArchetype.fromModel(archetype); + int quarterTurns = archetype.sourceToCanonicalQuarterTurns(piece); + previousDimensions = canonicalDimensions(sourceDimensions, quarterTurns); + PlanarJigsawWorkcellResolver.ResolvedWorkcell workcell = + PlanarJigsawWorkcellResolver.resolve(structure).get(archetype); + if (workcell == null || !workcell.contains(dimensionsPosition(targetDimensions))) { + throw new IOException("Variant '" + normalizedPiece + "' size " + + describeDimensions(targetDimensions) + " exceeds the " + + planarArchetype.displayName() + " workcell capacity " + + describeDimensions(new JigsawStudioCellDimensions( + workcell == null ? 1 : workcell.width(), + workcell == null ? 1 : workcell.height(), + workcell == null ? 1 : workcell.depth())) + + "; increase that workcell capacity first"); + } + PlanarPieceObjectResize resized = resizePlanarPieceObject( + source, + piece, + planarArchetype, + targetDimensions, + normalizedPiece); + resizedObject = resized.object(); + relocatedConnectors = resized.relocatedConnectors(); + } else { + previousDimensions = sourceDimensions; + requireConnectorsInside(piece, targetDimensions, normalizedPiece); + resizedObject = resizeObject(source, targetDimensions, normalizedPiece); + } + + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + byte[] content; + if (relativePath.equals(objectResource)) { + content = JigsawStudioProjectCreator.serialize(resizedObject); + } else if (relativePath.equals(pieceResource)) { + content = (GSON.toJson(piece) + "\n").getBytes(StandardCharsets.UTF_8); + } else { + content = Files.readAllBytes(resolveOwnedResource(graph.root(), relativePath)); + } + bundle.resource(relativePath, content); + } + StructureWriteResult writeResult = write(graph, bundle.build()); + return new VariantResizeResult( + writeResult, + previousDimensions, + targetDimensions, + relocatedConnectors); + } + + public static WorkcellCapacityResult updatePlanarWorkcellCapacity( + Path packRoot, + String structureKey, + JigsawPlanarArchetype archetype, + JigsawStudioCellDimensions dimensions + ) throws IOException { + JigsawPlanarArchetype targetArchetype = Objects.requireNonNull( + archetype, + "Planar Jigsaw Studio archetype"); + JigsawStudioCellDimensions targetDimensions = Objects.requireNonNull( + dimensions, + "Planar Jigsaw Studio workcell dimensions"); + if (targetDimensions.width() < 3 || targetDimensions.depth() < 3) { + throw new IllegalArgumentException( + "Planar Jigsaw Studio workcell width and depth must each be at least 3 blocks"); + } + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String structureResource = "structures/" + graph.manifest().structure().path() + ".json"; + if (!graph.manifest().resourceHashes().containsKey(structureResource)) { + throw new IOException("The owned graph manifest does not include " + structureResource); + } + IrisStructure structure = readStructure( + resolveOwnedResource(graph.root(), structureResource), + structureResource); + if (structure.resolvedMode() != IrisJigsawMode.PLANAR_JIGSAW) { + throw new IOException("Workcell resizing requires a planar Jigsaw Studio structure"); + } + + Map resources = readOwnedResources(graph); + int checkedVariants = 0; + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + if (!relativePath.startsWith("jigsaw-pieces/") || !relativePath.endsWith(".json")) { + continue; + } + IrisJigsawPiece piece = readPiece(resolveOwnedResource(graph.root(), relativePath), relativePath); + if (IrisJigsawWorkcellArchetype.fromPiece(piece) != targetArchetype.modelArchetype()) { + continue; + } + checkedVariants++; + } + + Path structurePath = resolveOwnedResource(graph.root(), structureResource); + resources.put( + structureResource, + JigsawStudioStructureEditor.updateWorkcell( + resources.get(structureResource), + targetArchetype, + targetDimensions, + null, + null, + structurePath)); + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + for (Map.Entry resource : resources.entrySet()) { + bundle.resource(resource.getKey(), resource.getValue()); + } + StructureWriteResult writeResult; + try { + writeResult = write(graph, bundle.build()); + } catch (RuntimeException exception) { + throw new IOException("Workcell capacity " + describeDimensions(targetDimensions) + + " cannot contain every " + targetArchetype.displayName() + + " variant: " + failureMessage(exception), exception); + } + return new WorkcellCapacityResult(writeResult, checkedVariants); + } + + public static StructureWriteResult updateConnectorChannel( + Path packRoot, + String structureKey, + String pieceKey, + IrisPosition position, + String channel + ) throws IOException { + String normalizedPiece = JigsawStudioProjectCreator.Options.requireResourceKey(pieceKey); + IrisPosition connectorPosition = Objects.requireNonNull(position, "Jigsaw connector position"); + String normalizedChannel = normalizeChannel(channel); + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String pieceResource = "jigsaw-pieces/" + normalizedPiece + ".json"; + if (!graph.manifest().resourceHashes().containsKey(pieceResource)) { + throw new IOException("Piece '" + normalizedPiece + "' is not owned by this jigsaw project"); + } + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + boolean found = false; + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + byte[] content = Files.readAllBytes(resolveOwnedResource(graph.root(), relativePath)); + if (relativePath.equals(pieceResource)) { + IrisJigsawPiece piece; + try { + piece = GSON.fromJson(new String(content, StandardCharsets.UTF_8), IrisJigsawPiece.class); + } catch (RuntimeException exception) { + throw new IOException("Jigsaw piece is not valid JSON: " + pieceResource, exception); + } + if (piece == null) { + throw new IOException("Jigsaw piece is empty: " + pieceResource); + } + for (IrisJigsawConnector connector : piece.getConnectors()) { + if (connectorPosition.equals(connector.getPosition())) { + connector.setChannel(normalizedChannel); + found = true; + } + } + content = (GSON.toJson(piece) + "\n").getBytes(StandardCharsets.UTF_8); + } + bundle.resource(relativePath, content); + } + if (!found) { + throw new IOException("Piece '" + normalizedPiece + "' has no connector at " + + connectorPosition.getX() + "," + connectorPosition.getY() + "," + + connectorPosition.getZ()); + } + return write(graph, bundle.build()); + } + + private static OwnedGraph loadOwnedGraph(Path packRoot, String structureKey) throws IOException { + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + StructureKey ownershipKey = StructureKey.parse(structureKey, "iris"); + StructureTransactionWriter writer = new StructureTransactionWriter(root); + Path manifestPath = writer.ownershipManifestPath(ownershipKey); + if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("This graph is read-only because it is not Studio-owned; create a new Jigsaw Studio project before editing pieces"); + } + byte[] manifestContent = Files.readAllBytes(manifestPath); + StructureOwnershipManifest manifest = JigsawStudioAuthoringAccess.requireEditable( + StructureOwnershipManifest.fromJson(manifestContent)); + return new OwnedGraph(root, writer, manifest, StructureHash.sha256(manifestContent)); + } + + private static Map expectedThemeSetSources( + IrisStructure structure, + Set requestedStableIds + ) throws IOException { + Map expected = new LinkedHashMap<>(); + if (structure.resolvedMode() == IrisJigsawMode.SPATIAL_JIGSAW) { + if (requestedStableIds.isEmpty()) { + throw new IOException("A spatial theme set requires at least one workcell source"); + } + List stableIds = new ArrayList<>(requestedStableIds); + stableIds.sort(Comparator.naturalOrder()); + for (String stableId : stableIds) { + if (!stableId.equals(JigsawStudioLayout.SPATIAL_WORKCELL_ID) + && !stableId.startsWith(JigsawStudioLayout.SPATIAL_WORKCELL_ID + "/")) { + throw new IOException("Invalid spatial workcell source '" + stableId + "'"); + } + expected.put(stableId, null); + } + return expected; + } + Map workcells; + try { + workcells = PlanarJigsawWorkcellResolver.resolve(structure); + } catch (IllegalArgumentException exception) { + throw new IOException("Planar workcell configuration is invalid: " + + exception.getMessage(), exception); + } + for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) { + PlanarJigsawWorkcellResolver.ResolvedWorkcell workcell = workcells.get(archetype.modelArchetype()); + if (workcell != null && workcell.enabled()) { + expected.put(archetype.stableId(), archetype); + } + } + if (expected.isEmpty()) { + throw new IOException("A planar theme set requires at least one enabled workcell"); + } + return expected; + } + + private static void requireExactThemeSetSources( + Map requestedSources, + Set expectedStableIds + ) throws IOException { + Set missing = new LinkedHashSet<>(expectedStableIds); + missing.removeAll(requestedSources.keySet()); + Set unexpected = new LinkedHashSet<>(requestedSources.keySet()); + unexpected.removeAll(expectedStableIds); + if (!missing.isEmpty() || !unexpected.isEmpty()) { + throw new IOException("Theme-set sources must match enabled workcells exactly; missing=" + + missing + ", unexpected=" + unexpected); + } + } + + private static Map readOwnedResources(OwnedGraph graph) throws IOException { + Map resources = new LinkedHashMap<>(); + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + resources.put( + relativePath, + Files.readAllBytes(resolveOwnedResource(graph.root(), relativePath))); + } + return resources; + } + + private static byte[] appendThemeSet( + byte[] content, + String themeKey, + String structureResource + ) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw structure is not a JSON object: " + structureResource); + } + JsonObject structure = parsed.getAsJsonObject(); + JsonArray themeSets; + if (!structure.has("themeSets")) { + themeSets = new JsonArray(); + structure.add("themeSets", themeSets); + } else if (!structure.get("themeSets").isJsonArray()) { + throw new IOException("Jigsaw structure has an invalid themeSets value: " + structureResource); + } else { + themeSets = structure.getAsJsonArray("themeSets"); + } + for (JsonElement element : themeSets) { + if (element.isJsonObject() + && element.getAsJsonObject().has("key") + && themeKey.equals(element.getAsJsonObject().get("key").getAsString())) { + throw new IOException("Jigsaw structure already declares theme '" + themeKey + "'"); + } + } + JsonObject theme = new JsonObject(); + theme.addProperty("key", themeKey); + theme.addProperty("weight", 1); + themeSets.add(theme); + return (GSON.toJson(structure) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private static StructureWriteResult createVariantFromPiece( + Path packRoot, + String structureKey, + String sourcePieceKey, + String targetPieceKey, + VariantObjectMode objectMode + ) throws IOException { + String normalizedSource = JigsawStudioProjectCreator.Options.requireResourceKey(sourcePieceKey); + String normalizedTarget = JigsawStudioProjectCreator.Options.requireResourceKey(targetPieceKey); + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String sourcePieceResource = "jigsaw-pieces/" + normalizedSource + ".json"; + String targetPieceResource = "jigsaw-pieces/" + normalizedTarget + ".json"; + String targetObjectResource = "objects/" + normalizedTarget + ".iob"; + if (!graph.manifest().resourceHashes().containsKey(sourcePieceResource)) { + throw new IOException("Source piece '" + normalizedSource + "' is not owned by this project"); + } + requireAvailableVariantTarget(graph, targetPieceResource); + requireAvailableVariantTarget(graph, targetObjectResource); + + Path sourcePiecePath = resolveOwnedResource(graph.root(), sourcePieceResource); + byte[] sourcePieceContent = Files.readAllBytes(sourcePiecePath); + IrisJigsawPiece sourcePiece = readPiece(sourcePiecePath, sourcePieceResource); + if (sourcePiece.getObject() == null || sourcePiece.getObject().isBlank()) { + throw new IOException("Source piece '" + normalizedSource + "' does not declare an object"); + } + String sourceObjectKey = JigsawStudioProjectCreator.Options.requireResourceKey(sourcePiece.getObject()); + String sourceObjectResource = "objects/" + sourceObjectKey + ".iob"; + if (!graph.manifest().resourceHashes().containsKey(sourceObjectResource)) { + throw new IOException("Source object '" + sourceObjectKey + "' is not owned by this project"); + } + Path sourceObjectPath = resolveOwnedResource(graph.root(), sourceObjectResource); + byte[] targetObjectContent; + if (objectMode == VariantObjectMode.COPY_SOURCE) { + targetObjectContent = Files.readAllBytes(sourceObjectPath); + } else { + IrisBlockVector sourceSize = IrisObject.sampleSize(sourceObjectPath.toFile()); + targetObjectContent = JigsawStudioProjectCreator.serialize(new IrisObject( + sourceSize.getBlockX(), + sourceSize.getBlockY(), + sourceSize.getBlockZ())); + } + + Map targetPieceKeysBySource = Map.of(normalizedSource, normalizedTarget); + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + int duplicatedMemberships = 0; + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + byte[] content = Files.readAllBytes(resolveOwnedResource(graph.root(), relativePath)); + if (relativePath.startsWith("jigsaw-pools/") && relativePath.endsWith(".json")) { + PoolMembershipDuplication duplication = duplicatePoolMemberships( + content, + targetPieceKeysBySource, + relativePath); + content = duplication.content(); + duplicatedMemberships += duplication.duplicatedEntries(); + } + bundle.resource(relativePath, content); + } + if (duplicatedMemberships == 0) { + throw new IOException("Source piece '" + normalizedSource + + "' has no owned pool membership to copy; select an owned pool explicitly"); + } + bundle.resource( + targetPieceResource, + duplicatePieceForVariant(sourcePieceContent, normalizedTarget, sourcePieceResource)); + bundle.resource(targetObjectResource, targetObjectContent); + return write(graph, bundle.build()); + } + + private static void requireAvailableVariantTarget( + OwnedGraph graph, + String relativePath + ) throws IOException { + StructureResourceBundle.validateRelativePath(relativePath); + Path target = graph.root().resolve(relativePath).normalize(); + if (!target.startsWith(graph.root())) { + throw new IOException("Variant target escapes the pack root: " + relativePath); + } + if (graph.manifest().resourceHashes().containsKey(relativePath) + || Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Variant target already exists: " + relativePath); + } + } + + private static void requireAvailableThemeSetTarget( + OwnedGraph graph, + String relativePath + ) throws IOException { + StructureResourceBundle.validateRelativePath(relativePath); + Path target = graph.root().resolve(relativePath).normalize(); + if (!target.startsWith(graph.root())) { + throw new IOException("Theme-set target escapes the pack root: " + relativePath); + } + if (graph.manifest().resourceHashes().containsKey(relativePath) + || Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Theme-set target already exists: " + relativePath); + } + } + + private static byte[] duplicatePieceForTheme( + byte[] content, + String targetPieceKey, + String themeKey, + String sourcePieceResource + ) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw piece is not a JSON object: " + sourcePieceResource); + } + JsonObject piece = parsed.getAsJsonObject().deepCopy(); + piece.addProperty("object", targetPieceKey); + JsonArray themes = new JsonArray(); + themes.add(themeKey); + piece.add("themes", themes); + return (GSON.toJson(piece) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private static byte[] duplicatePieceForVariant( + byte[] content, + String targetPieceKey, + String sourcePieceResource + ) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw piece is not a JSON object: " + sourcePieceResource); + } + JsonObject piece = parsed.getAsJsonObject().deepCopy(); + piece.addProperty("object", targetPieceKey); + return (GSON.toJson(piece) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private static PoolMembershipDuplication duplicatePoolMemberships( + byte[] content, + Map targetPieceKeysBySource, + String poolResource + ) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw pool is not a JSON object: " + poolResource); + } + JsonObject pool = parsed.getAsJsonObject(); + JsonArray pieces = pool.getAsJsonArray("pieces"); + if (pieces == null) { + throw new IOException("Jigsaw pool does not declare a pieces array: " + poolResource); + } + JsonArray expanded = new JsonArray(); + int duplicatedEntries = 0; + for (JsonElement element : pieces) { + expanded.add(element.deepCopy()); + if (!element.isJsonObject() || !element.getAsJsonObject().has("piece")) { + continue; + } + String sourcePieceKey = element.getAsJsonObject().get("piece").getAsString(); + String targetPieceKey = targetPieceKeysBySource.get(sourcePieceKey); + if (targetPieceKey == null) { + continue; + } + JsonObject duplicate = element.getAsJsonObject().deepCopy(); + duplicate.addProperty("piece", targetPieceKey); + expanded.add(duplicate); + duplicatedEntries++; + } + if (duplicatedEntries == 0) { + return new PoolMembershipDuplication(content, 0); + } + pool.add("pieces", expanded); + return new PoolMembershipDuplication( + (GSON.toJson(pool) + "\n").getBytes(StandardCharsets.UTF_8), + duplicatedEntries); + } + + private static StructureWriteResult updateOwnedPiece( + Path packRoot, + String structureKey, + String pieceKey, + PieceContentEditor editor + ) throws IOException { + String normalizedPiece = JigsawStudioProjectCreator.Options.requireResourceKey(pieceKey); + OwnedGraph graph = loadOwnedGraph(packRoot, structureKey); + String pieceResource = "jigsaw-pieces/" + normalizedPiece + ".json"; + if (!graph.manifest().resourceHashes().containsKey(pieceResource)) { + throw new IOException("Piece '" + normalizedPiece + "' is not owned by this jigsaw project"); + } + StructureResourceBundle.Builder bundle = graph.bundleBuilder(); + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + byte[] content = Files.readAllBytes(resolveOwnedResource(graph.root(), relativePath)); + if (relativePath.equals(pieceResource)) { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw piece is not a JSON object: " + pieceResource); + } + JsonObject piece = parsed.getAsJsonObject(); + editor.edit(piece, pieceResource); + content = (GSON.toJson(piece) + "\n").getBytes(StandardCharsets.UTF_8); + } + bundle.resource(relativePath, content); + } + return write(graph, bundle.build()); + } + + private static List normalizeThemes(List themes) { + Objects.requireNonNull(themes, "Jigsaw Studio piece themes"); + Set normalized = new LinkedHashSet<>(); + for (String theme : themes) { + Objects.requireNonNull(theme, "Jigsaw Studio piece theme"); + String key = theme.trim(); + if (key.isEmpty() || !key.equals(theme)) { + throw new IllegalArgumentException( + "Jigsaw Studio piece themes must be non-blank and whitespace-normalized"); + } + if (!normalized.add(key)) { + throw new IllegalArgumentException("Duplicate Jigsaw Studio piece theme '" + key + "'"); + } + } + return List.copyOf(normalized); + } + + public static String normalizeDisplayName(String displayName) { + String normalized = displayName == null ? "" : displayName.trim(); + if (normalized.codePointCount(0, normalized.length()) > 64) { + throw new IllegalArgumentException("Jigsaw Studio display names cannot exceed 64 visible characters"); + } + for (int index = 0; index < normalized.length(); ) { + int codePoint = normalized.codePointAt(index); + if (Character.isISOControl(codePoint) || codePoint == '§') { + throw new IllegalArgumentException( + "Jigsaw Studio display names cannot contain control or formatting characters"); + } + index += Character.charCount(codePoint); + } + return normalized; + } + + private static boolean otherOwnedPieceReferencesObject( + OwnedGraph graph, + String targetPieceKey, + String objectKey + ) throws IOException { + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + if (!relativePath.startsWith("jigsaw-pieces/") || !relativePath.endsWith(".json")) { + continue; + } + String pieceKey = resourceKey(relativePath, "jigsaw-pieces/", ".json"); + if (pieceKey.equals(targetPieceKey)) { + continue; + } + IrisJigsawPiece piece = readPiece(resolveOwnedResource(graph.root(), relativePath), relativePath); + if (objectKey.equals(piece.getObject())) { + return true; + } + } + return false; + } + + private static void requireDeletableArchetype( + OwnedGraph graph, + String targetPieceKey, + IrisJigsawPiece targetPiece + ) throws IOException { + String structureResource = "structures/" + graph.manifest().structure().path() + ".json"; + if (!graph.manifest().resourceHashes().containsKey(structureResource)) { + throw new IOException("The owned graph manifest does not include " + structureResource); + } + IrisStructure structure = readStructure( + resolveOwnedResource(graph.root(), structureResource), + structureResource); + if (structure.resolvedMode() == IrisJigsawMode.SPATIAL_JIGSAW) { + if (countOtherOwnedPieces(graph, targetPieceKey, null) == 0) { + throw new IOException("Cannot delete the final spatial jigsaw variant"); + } + return; + } + IrisJigsawWorkcellArchetype archetype = IrisJigsawWorkcellArchetype.fromPiece(targetPiece); + Map workcells; + try { + workcells = PlanarJigsawWorkcellResolver.resolve(structure); + } catch (IllegalArgumentException exception) { + throw new IOException("Planar workcell configuration is invalid: " + + exception.getMessage(), exception); + } + PlanarJigsawWorkcellResolver.ResolvedWorkcell workcell = workcells.get(archetype); + if (workcell != null && workcell.enabled() + && countOtherOwnedPieces(graph, targetPieceKey, archetype) == 0) { + throw new IOException("Cannot delete the final variant for enabled planar workcell " + + JigsawPlanarArchetype.fromModel(archetype).stableId()); + } + } + + private static int countOtherOwnedPieces( + OwnedGraph graph, + String targetPieceKey, + IrisJigsawWorkcellArchetype archetype + ) throws IOException { + int count = 0; + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + if (!relativePath.startsWith("jigsaw-pieces/") || !relativePath.endsWith(".json")) { + continue; + } + String pieceKey = resourceKey(relativePath, "jigsaw-pieces/", ".json"); + if (pieceKey.equals(targetPieceKey)) { + continue; + } + if (archetype == null) { + count++; + continue; + } + IrisJigsawPiece piece = readPiece(resolveOwnedResource(graph.root(), relativePath), relativePath); + if (IrisJigsawWorkcellArchetype.fromPiece(piece) == archetype) { + count++; + } + } + return count; + } + + private static PoolEntryRemoval removePoolEntries( + byte[] content, + String pieceKey, + String poolResource + ) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw pool is not a JSON object: " + poolResource); + } + JsonObject pool = parsed.getAsJsonObject(); + JsonArray pieces = pool.getAsJsonArray("pieces"); + if (pieces == null) { + throw new IOException("Jigsaw pool does not declare a pieces array: " + poolResource); + } + int removed = 0; + for (int index = pieces.size() - 1; index >= 0; index--) { + JsonElement entry = pieces.get(index); + if (entry.isJsonObject() + && entry.getAsJsonObject().has("piece") + && pieceKey.equals(entry.getAsJsonObject().get("piece").getAsString())) { + pieces.remove(index); + removed++; + } + } + if (removed == 0) { + return new PoolEntryRemoval(content, 0); + } + return new PoolEntryRemoval( + (GSON.toJson(pool) + "\n").getBytes(StandardCharsets.UTF_8), + removed); + } + + private static String resourceKey(String relativePath, String prefix, String suffix) { + return relativePath.substring(prefix.length(), relativePath.length() - suffix.length()); + } + + private static String failureMessage(RuntimeException exception) { + String message = exception.getMessage(); + return message == null || message.isBlank() ? exception.getClass().getSimpleName() : message; + } + + private static Path resolveOwnedResource(Path root, String relativePath) throws IOException { + StructureResourceBundle.validateRelativePath(relativePath); + Path resource = root.resolve(relativePath).normalize(); + if (!resource.startsWith(root) || !Files.isRegularFile(resource, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Owned graph resource is missing or unsafe: " + relativePath); + } + return resource; + } + + private static byte[] addPoolEntry( + byte[] content, + String pieceKey, + int weight, + Path poolPath + ) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw pool is not a JSON object: " + poolPath); + } + JsonObject pool = parsed.getAsJsonObject(); + if (!pool.has("pieces") || !pool.get("pieces").isJsonArray()) { + throw new IOException("Jigsaw pool does not declare a pieces array: " + poolPath); + } + JsonArray pieces = pool.getAsJsonArray("pieces"); + for (JsonElement element : pieces) { + if (element.isJsonObject() + && element.getAsJsonObject().has("piece") + && pieceKey.equals(element.getAsJsonObject().get("piece").getAsString())) { + throw new IOException("Pool already references piece '" + pieceKey + "'"); + } + } + JsonObject entry = new JsonObject(); + entry.addProperty("piece", pieceKey); + entry.addProperty("weight", weight); + pieces.add(entry); + return (GSON.toJson(pool) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private static IrisJigsawPiece readPiece(Path path, String resource) throws IOException { + try { + IrisJigsawPiece piece = GSON.fromJson(Files.readString(path, StandardCharsets.UTF_8), + IrisJigsawPiece.class); + if (piece == null) { + throw new IOException("Jigsaw piece is empty: " + resource); + } + return piece; + } catch (RuntimeException exception) { + throw new IOException("Jigsaw piece is not valid JSON: " + resource, exception); + } + } + + private static IrisStructure readStructure(Path path, String resource) throws IOException { + try { + IrisStructure structure = GSON.fromJson( + Files.readString(path, StandardCharsets.UTF_8), + IrisStructure.class); + if (structure == null) { + throw new IOException("Jigsaw structure is empty: " + resource); + } + return structure; + } catch (RuntimeException exception) { + throw new IOException("Jigsaw structure is not valid JSON: " + resource, exception); + } + } + + private static void requireExclusiveObjectReference( + OwnedGraph graph, + String activePieceKey, + String objectKey + ) throws IOException { + for (String relativePath : graph.manifest().resourceHashes().keySet()) { + if (!relativePath.startsWith("jigsaw-pieces/") || !relativePath.endsWith(".json")) { + continue; + } + String pieceKey = relativePath.substring( + "jigsaw-pieces/".length(), + relativePath.length() - ".json".length()); + if (pieceKey.equals(activePieceKey)) { + continue; + } + IrisJigsawPiece piece = readPiece(resolveOwnedResource(graph.root(), relativePath), relativePath); + if (objectKey.equals(piece.getObject())) { + throw new IOException("Object '" + objectKey + "' is shared by piece '" + pieceKey + + "'; duplicate the active variant before resizing its object bounds"); + } + } + } + + static PlanarPieceObjectResize resizePlanarPieceObject( + IrisObject source, + IrisJigsawPiece piece, + JigsawPlanarArchetype archetype, + JigsawStudioCellDimensions dimensions, + String pieceKey + ) throws IOException { + IrisObject sourceObject = Objects.requireNonNull(source, "Planar Jigsaw Studio source object"); + IrisJigsawPiece targetPiece = Objects.requireNonNull(piece, "Planar Jigsaw Studio piece"); + JigsawPlanarArchetype targetArchetype = Objects.requireNonNull( + archetype, + "Planar Jigsaw Studio archetype"); + JigsawStudioCellDimensions targetDimensions = Objects.requireNonNull( + dimensions, + "Planar Jigsaw Studio target dimensions"); + String targetPieceKey = pieceKey == null || pieceKey.isBlank() ? "unknown" : pieceKey; + if (targetDimensions.width() < 3 || targetDimensions.depth() < 3) { + throw new IllegalArgumentException( + "Planar Jigsaw Studio workcell width and depth must each be at least 3 blocks"); + } + if (sourceObject.getW() < 1 || sourceObject.getH() < 1 || sourceObject.getD() < 1) { + throw new IOException("Planar piece '" + targetPieceKey + "' has invalid object dimensions"); + } + if (IrisJigsawWorkcellArchetype.fromPiece(targetPiece) != targetArchetype.modelArchetype()) { + throw new IOException("Planar piece '" + targetPieceKey + "' does not belong to " + + targetArchetype.stableId()); + } + + int quarterTurns = targetArchetype.modelArchetype().sourceToCanonicalQuarterTurns(targetPiece); + JigsawStudioCellDimensions sourceDimensions = new JigsawStudioCellDimensions( + sourceObject.getW(), + sourceObject.getH(), + sourceObject.getD()); + JigsawStudioCellDimensions canonicalDimensions = canonicalDimensions(sourceDimensions, quarterTurns); + Map canonicalBlocks = canonicalBlocks( + sourceObject, + quarterTurns, + targetPieceKey); + Map canonicalTiles = canonicalTiles( + sourceObject, + quarterTurns, + targetPieceKey); + List connectors = targetPiece.getConnectors(); + if (connectors == null) { + throw new IOException("Planar piece '" + targetPieceKey + "' has no connector list"); + } + List connectorResizes = planConnectorResizes( + connectors, + sourceDimensions, + canonicalDimensions, + targetDimensions, + quarterTurns, + targetPieceKey); + relocateConnectorPayloads(canonicalBlocks, canonicalTiles, connectorResizes, targetPieceKey); + requireContentInsideTarget(canonicalBlocks, canonicalTiles, targetDimensions, targetPieceKey); + + JigsawStudioCellDimensions resizedSourceDimensions = sourceDimensions(targetDimensions, quarterTurns); + IrisObject resizedObject = rebuildSourceObject( + canonicalBlocks, + canonicalTiles, + resizedSourceDimensions, + quarterTurns, + targetPieceKey); + int relocatedConnectors = 0; + for (ConnectorResize connectorResize : connectorResizes) { + LocalPosition sourcePosition = toSource( + connectorResize.targetCanonical(), + resizedSourceDimensions, + quarterTurns); + connectorResize.connector().setPosition(sourcePosition.toIrisPosition()); + if (!connectorResize.sourceCanonical().equals(connectorResize.targetCanonical())) { + relocatedConnectors++; + } + } + return new PlanarPieceObjectResize(resizedObject, relocatedConnectors); + } + + private static Map canonicalBlocks( + IrisObject source, + int quarterTurns, + String pieceKey + ) throws IOException { + Map blocks = new LinkedHashMap<>(); + for (Map.Entry entry : source.getBlocks()) { + LocalPosition sourcePosition = unsignedPosition(entry.getKey(), source); + requireInside(sourcePosition, source.getW(), source.getH(), source.getD(), + "stored block", pieceKey); + PlatformBlockState state = entry.getValue(); + if (state == null) { + throw new IOException("Planar piece '" + pieceKey + "' contains a null stored block state"); + } + LocalPosition canonicalPosition = toCanonical( + sourcePosition, + source.getW(), + source.getD(), + quarterTurns); + if (blocks.putIfAbsent(canonicalPosition, state) != null) { + throw new IOException("Planar piece '" + pieceKey + + "' maps more than one stored block to " + canonicalPosition.describe()); + } + } + return blocks; + } + + private static Map canonicalTiles( + IrisObject source, + int quarterTurns, + String pieceKey + ) throws IOException { + Map tiles = new LinkedHashMap<>(); + for (Map.Entry entry : source.getStates()) { + LocalPosition sourcePosition = unsignedPosition(entry.getKey(), source); + requireInside(sourcePosition, source.getW(), source.getH(), source.getD(), + "tile data", pieceKey); + TileData tile = entry.getValue(); + if (tile == null) { + throw new IOException("Planar piece '" + pieceKey + "' contains null tile data"); + } + LocalPosition canonicalPosition = toCanonical( + sourcePosition, + source.getW(), + source.getD(), + quarterTurns); + if (tiles.putIfAbsent(canonicalPosition, tile.clone()) != null) { + throw new IOException("Planar piece '" + pieceKey + + "' maps more than one tile payload to " + canonicalPosition.describe()); + } + } + return tiles; + } + + private static List planConnectorResizes( + List connectors, + JigsawStudioCellDimensions sourceDimensions, + JigsawStudioCellDimensions canonicalDimensions, + JigsawStudioCellDimensions targetDimensions, + int quarterTurns, + String pieceKey + ) throws IOException { + List planned = new ArrayList<>(connectors.size()); + Set sourcePositions = new LinkedHashSet<>(); + Set targetPositions = new LinkedHashSet<>(); + IrisPosition sourceSize = dimensionsPosition(sourceDimensions); + IrisPosition canonicalSize = dimensionsPosition(canonicalDimensions); + IrisPosition targetSize = dimensionsPosition(targetDimensions); + for (int index = 0; index < connectors.size(); index++) { + IrisJigsawConnector connector = connectors.get(index); + if (connector == null || connector.getPosition() == null || connector.getDirection() == null + || connector.getDirection().isVertical()) { + throw new IOException("Planar piece '" + pieceKey + "' contains invalid connector " + index); + } + LocalPosition sourcePosition = LocalPosition.from(connector.getPosition()); + requireInside( + sourcePosition, + sourceDimensions.width(), + sourceDimensions.height(), + sourceDimensions.depth(), + "connector " + index, + pieceKey); + if (!sourcePositions.add(sourcePosition)) { + throw new IOException("Planar piece '" + pieceKey + + "' has multiple connectors at " + sourcePosition.describe()); + } + LocalPosition sourceCanonical = toCanonical( + sourcePosition, + sourceDimensions.width(), + sourceDimensions.depth(), + quarterTurns); + IrisDirection canonicalDirection = rotateHorizontalDirection( + connector.getDirection(), + quarterTurns); + LocalPosition expectedSource = LocalPosition.from(IrisJigsawConnector.canonicalPlanarPosition( + sourceSize, + connector.getDirection())); + LocalPosition expectedCanonical = LocalPosition.from(IrisJigsawConnector.canonicalPlanarPosition( + canonicalSize, + canonicalDirection)); + boolean canonicalConnector = sourcePosition.equals(expectedSource) + || sourceCanonical.equals(expectedCanonical); + LocalPosition targetCanonical = canonicalConnector + ? LocalPosition.from(IrisJigsawConnector.canonicalPlanarPosition( + targetSize, + canonicalDirection)) + : sourceCanonical; + if (!inside(targetCanonical, targetDimensions)) { + throw new IOException("Planar piece '" + pieceKey + "' connector " + index + " at " + + sourceCanonical.describe() + " would be cropped by the requested workcell bounds"); + } + if (!targetPositions.add(targetCanonical)) { + throw new IOException("Planar piece '" + pieceKey + + "' would place multiple connectors at " + targetCanonical.describe()); + } + planned.add(new ConnectorResize(connector, sourceCanonical, targetCanonical)); + } + return List.copyOf(planned); + } + + private static void relocateConnectorPayloads( + Map blocks, + Map tiles, + List connectorResizes, + String pieceKey + ) throws IOException { + Set relocatedSources = new LinkedHashSet<>(); + for (ConnectorResize connectorResize : connectorResizes) { + if (tiles.containsKey(connectorResize.sourceCanonical()) + || tiles.containsKey(connectorResize.targetCanonical())) { + throw new IOException("Planar piece '" + pieceKey + "' has tile data at connector position " + + connectorResize.sourceCanonical().describe() + + "; connector tile data cannot be resized safely"); + } + if (!connectorResize.sourceCanonical().equals(connectorResize.targetCanonical())) { + relocatedSources.add(connectorResize.sourceCanonical()); + } + } + for (ConnectorResize connectorResize : connectorResizes) { + if (connectorResize.sourceCanonical().equals(connectorResize.targetCanonical())) { + continue; + } + if (blocks.containsKey(connectorResize.targetCanonical()) + && !relocatedSources.contains(connectorResize.targetCanonical())) { + throw new IOException("Planar piece '" + pieceKey + "' cannot relocate connector from " + + connectorResize.sourceCanonical().describe() + " to " + + connectorResize.targetCanonical().describe() + + " because the destination contains a stored block"); + } + } + List payloads = new ArrayList<>(relocatedSources.size()); + for (ConnectorResize connectorResize : connectorResizes) { + if (connectorResize.sourceCanonical().equals(connectorResize.targetCanonical())) { + continue; + } + boolean present = blocks.containsKey(connectorResize.sourceCanonical()); + PlatformBlockState state = blocks.remove(connectorResize.sourceCanonical()); + payloads.add(new BlockRelocation(connectorResize.targetCanonical(), state, present)); + } + for (BlockRelocation payload : payloads) { + if (!payload.present()) { + continue; + } + if (blocks.putIfAbsent(payload.target(), payload.state()) != null) { + throw new IOException("Planar piece '" + pieceKey + + "' has colliding connector block payloads at " + payload.target().describe()); + } + } + } + + private static void requireContentInsideTarget( + Map blocks, + Map tiles, + JigsawStudioCellDimensions dimensions, + String pieceKey + ) throws IOException { + for (LocalPosition position : blocks.keySet()) { + if (!inside(position, dimensions)) { + throw new IOException("Planar piece '" + pieceKey + "' has a stored block, including explicit air, at " + + position.describe() + " that would be cropped by the requested workcell bounds"); + } + } + for (LocalPosition position : tiles.keySet()) { + if (!inside(position, dimensions)) { + throw new IOException("Planar piece '" + pieceKey + "' has tile data at " + + position.describe() + " that would be cropped by the requested workcell bounds"); + } + } + } + + private static IrisObject rebuildSourceObject( + Map canonicalBlocks, + Map canonicalTiles, + JigsawStudioCellDimensions sourceDimensions, + int quarterTurns, + String pieceKey + ) throws IOException { + IrisObject resized = new IrisObject( + sourceDimensions.width(), + sourceDimensions.height(), + sourceDimensions.depth()); + Set sourcePositions = new LinkedHashSet<>(); + for (Map.Entry entry : canonicalBlocks.entrySet()) { + LocalPosition sourcePosition = toSource(entry.getKey(), sourceDimensions, quarterTurns); + if (!sourcePositions.add(sourcePosition)) { + throw new IOException("Planar piece '" + pieceKey + + "' maps multiple stored blocks to " + sourcePosition.describe()); + } + resized.setUnsigned( + sourcePosition.x(), + sourcePosition.y(), + sourcePosition.z(), + entry.getValue()); + } + sourcePositions.clear(); + for (Map.Entry entry : canonicalTiles.entrySet()) { + LocalPosition sourcePosition = toSource(entry.getKey(), sourceDimensions, quarterTurns); + if (!sourcePositions.add(sourcePosition)) { + throw new IOException("Planar piece '" + pieceKey + + "' maps multiple tile payloads to " + sourcePosition.describe()); + } + resized.setUnsignedTile( + sourcePosition.x(), + sourcePosition.y(), + sourcePosition.z(), + entry.getValue().clone()); + } + return resized; + } + + private static LocalPosition unsignedPosition(IrisBlockVector signed, IrisObject object) { + return new LocalPosition( + signed.getBlockX() + object.getCenter().getBlockX(), + signed.getBlockY() + object.getCenter().getBlockY(), + signed.getBlockZ() + object.getCenter().getBlockZ()); + } + + private static JigsawStudioCellDimensions canonicalDimensions( + JigsawStudioCellDimensions source, + int quarterTurns + ) { + return Math.floorMod(quarterTurns, 2) == 0 + ? source + : new JigsawStudioCellDimensions(source.depth(), source.height(), source.width()); + } + + private static JigsawStudioCellDimensions sourceDimensions( + JigsawStudioCellDimensions canonical, + int quarterTurns + ) { + return Math.floorMod(quarterTurns, 2) == 0 + ? canonical + : new JigsawStudioCellDimensions(canonical.depth(), canonical.height(), canonical.width()); + } + + private static LocalPosition toCanonical( + LocalPosition source, + int sourceWidth, + int sourceDepth, + int quarterTurns + ) { + return switch (Math.floorMod(quarterTurns, 4)) { + case 0 -> source; + case 1 -> new LocalPosition(sourceDepth - 1 - source.z(), source.y(), source.x()); + case 2 -> new LocalPosition( + sourceWidth - 1 - source.x(), + source.y(), + sourceDepth - 1 - source.z()); + case 3 -> new LocalPosition(source.z(), source.y(), sourceWidth - 1 - source.x()); + default -> throw new IllegalStateException("Unreachable planar object rotation"); + }; + } + + private static LocalPosition toSource( + LocalPosition canonical, + JigsawStudioCellDimensions sourceDimensions, + int quarterTurns + ) { + return switch (Math.floorMod(quarterTurns, 4)) { + case 0 -> canonical; + case 1 -> new LocalPosition( + canonical.z(), + canonical.y(), + sourceDimensions.depth() - 1 - canonical.x()); + case 2 -> new LocalPosition( + sourceDimensions.width() - 1 - canonical.x(), + canonical.y(), + sourceDimensions.depth() - 1 - canonical.z()); + case 3 -> new LocalPosition( + sourceDimensions.width() - 1 - canonical.z(), + canonical.y(), + canonical.x()); + default -> throw new IllegalStateException("Unreachable planar object rotation"); + }; + } + + private static IrisDirection rotateHorizontalDirection(IrisDirection direction, int quarterTurns) { + JigsawPlanarDirection planarDirection = switch (direction) { + case NORTH_NEGATIVE_Z -> JigsawPlanarDirection.NORTH; + case EAST_POSITIVE_X -> JigsawPlanarDirection.EAST; + case SOUTH_POSITIVE_Z -> JigsawPlanarDirection.SOUTH; + case WEST_NEGATIVE_X -> JigsawPlanarDirection.WEST; + case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> throw new IllegalArgumentException( + "Planar connector direction must be horizontal"); + }; + return planarDirection.rotateClockwise(quarterTurns).irisDirection(); + } + + private static IrisPosition dimensionsPosition(JigsawStudioCellDimensions dimensions) { + return new IrisPosition(dimensions.width(), dimensions.height(), dimensions.depth()); + } + + private static boolean inside(LocalPosition position, JigsawStudioCellDimensions dimensions) { + return inside(position, dimensions.width(), dimensions.height(), dimensions.depth()); + } + + private static boolean inside(LocalPosition position, int width, int height, int depth) { + return position.x() >= 0 && position.x() < width + && position.y() >= 0 && position.y() < height + && position.z() >= 0 && position.z() < depth; + } + + private static void requireInside( + LocalPosition position, + int width, + int height, + int depth, + String content, + String pieceKey + ) throws IOException { + if (!inside(position, width, height, depth)) { + throw new IOException("Planar piece '" + pieceKey + "' has " + content + " at " + + position.describe() + " outside its object bounds"); + } + } + + static IrisObject resizeObject( + IrisObject source, + JigsawStudioCellDimensions dimensions, + String pieceKey + ) throws IOException { + IrisObject object = Objects.requireNonNull(source, "Jigsaw Studio source object"); + JigsawStudioCellDimensions target = Objects.requireNonNull( + dimensions, + "Jigsaw Studio target object dimensions"); + String normalizedPiece = pieceKey == null || pieceKey.isBlank() ? "unknown" : pieceKey; + IrisObject resized = new IrisObject(target.width(), target.height(), target.depth()); + for (Map.Entry entry : object.getBlocks()) { + IrisBlockVector position = entry.getKey(); + LocalPosition unsigned = unsignedPosition(position, object); + if (!inside(unsigned, target)) { + throw new IOException("Spatial piece '" + normalizedPiece + + "' has a stored block, including explicit air, at " + unsigned.describe() + + " that would be cropped by the requested variant size"); + } + resized.setUnsigned( + unsigned.x(), + unsigned.y(), + unsigned.z(), + entry.getValue()); + } + for (Map.Entry entry : object.getStates()) { + IrisBlockVector position = entry.getKey(); + LocalPosition unsigned = unsignedPosition(position, object); + if (!inside(unsigned, target)) { + throw new IOException("Spatial piece '" + normalizedPiece + "' has tile data at " + + unsigned.describe() + " that would be cropped by the requested variant size"); + } + resized.setUnsignedTile( + unsigned.x(), + unsigned.y(), + unsigned.z(), + entry.getValue().clone()); + } + return resized; + } + + private static void requireConnectorsInside( + IrisJigsawPiece piece, + JigsawStudioCellDimensions dimensions, + String pieceKey + ) throws IOException { + if (piece.getConnectors() == null) { + throw new IOException("Spatial piece '" + pieceKey + "' has no connector list"); + } + for (int index = 0; index < piece.getConnectors().size(); index++) { + IrisJigsawConnector connector = piece.getConnectors().get(index); + if (connector == null || connector.getPosition() == null) { + throw new IOException("Spatial piece '" + pieceKey + "' contains invalid connector " + index); + } + LocalPosition position = LocalPosition.from(connector.getPosition()); + if (!inside(position, dimensions)) { + throw new IOException("Spatial piece '" + pieceKey + "' connector " + index + " at " + + position.describe() + " would be cropped by the requested variant size"); + } + } + } + + private static String describeDimensions(JigsawStudioCellDimensions dimensions) { + return dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth(); + } + + private static String normalizeChannel(String channel) { + String normalized = channel == null ? "" : channel.trim(); + if (normalized.equalsIgnoreCase("none")) { + return ""; + } + if (normalized.length() > 128) { + throw new IllegalArgumentException("Jigsaw connector channels cannot exceed 128 characters"); + } + for (int index = 0; index < normalized.length(); index++) { + if (Character.isWhitespace(normalized.charAt(index))) { + throw new IllegalArgumentException("Jigsaw connector channels cannot contain whitespace"); + } + } + return normalized; + } + + private static StructureWriteResult write(OwnedGraph graph, StructureResourceBundle bundle) throws IOException { + StructureResourceBundleGraphCompiler.requireViable(bundle); + return writeCompiled(graph, bundle); + } + + private static StructureWriteResult writeCompiled( + OwnedGraph graph, + StructureResourceBundle bundle + ) throws IOException { + StructureWriteResult result = graph.writer().write( + bundle, + StructureWriteOptions.overwriteExpected(graph.expectedManifestHash())); + if (!result.successful()) { + String conflict = result.conflicts().isEmpty() + ? result.status().name() + : result.conflicts().getFirst().relativePath() + ": " + + result.conflicts().getFirst().reason(); + throw new IOException("Atomic graph edit was rejected: " + conflict); + } + return result; + } + + public record VariantResizeResult( + StructureWriteResult writeResult, + JigsawStudioCellDimensions previousDimensions, + JigsawStudioCellDimensions dimensions, + int relocatedConnectors + ) { + public VariantResizeResult { + Objects.requireNonNull(writeResult, "Jigsaw Studio variant-resize write result"); + Objects.requireNonNull(previousDimensions, "Previous Jigsaw Studio variant dimensions"); + Objects.requireNonNull(dimensions, "Jigsaw Studio variant dimensions"); + if (relocatedConnectors < 0) { + throw new IllegalArgumentException("Jigsaw Studio relocated connector count cannot be negative"); + } + } + } + + public record WorkcellCapacityResult( + StructureWriteResult writeResult, + int checkedVariants + ) { + public WorkcellCapacityResult { + Objects.requireNonNull(writeResult, "Jigsaw Studio workcell-capacity write result"); + if (checkedVariants < 0) { + throw new IllegalArgumentException("Jigsaw Studio checked variant count cannot be negative"); + } + } + } + + public record PieceDeletionResult( + StructureWriteResult writeResult, + int removedPoolMemberships, + int changedPools, + int removedPieceResources, + int removedObjectResources + ) { + public PieceDeletionResult { + Objects.requireNonNull(writeResult, "Jigsaw Studio piece deletion write result"); + if (removedPoolMemberships < 0 || changedPools < 0 + || removedPieceResources < 0 || removedObjectResources < 0) { + throw new IllegalArgumentException("Jigsaw Studio piece deletion counts cannot be negative"); + } + } + } + + public record VariantFamilyCreation( + Map pieceKeysByWorkcell, + StructureWriteResult writeResult + ) { + public VariantFamilyCreation { + Objects.requireNonNull(pieceKeysByWorkcell, "Jigsaw Studio variant-family piece keys"); + pieceKeysByWorkcell = Collections.unmodifiableMap(new LinkedHashMap<>(pieceKeysByWorkcell)); + Objects.requireNonNull(writeResult, "Jigsaw Studio variant-family write result"); + } + } + + @FunctionalInterface + private interface PieceContentEditor { + void edit(JsonObject piece, String pieceResource) throws IOException; + } + + private record PoolEntryRemoval(byte[] content, int removedEntries) { + private PoolEntryRemoval { + Objects.requireNonNull(content, "Jigsaw Studio pool entry removal content"); + if (removedEntries < 0) { + throw new IllegalArgumentException("Removed jigsaw pool entry count cannot be negative"); + } + } + } + + private record PoolMembershipDuplication(byte[] content, int duplicatedEntries) { + private PoolMembershipDuplication { + Objects.requireNonNull(content, "Jigsaw Studio duplicated pool content"); + if (duplicatedEntries < 0) { + throw new IllegalArgumentException("Duplicated pool membership count cannot be negative"); + } + } + } + + private enum VariantObjectMode { + COPY_SOURCE, + EMPTY_SOURCE_SIZE + } + + record PlanarPieceObjectResize(IrisObject object, int relocatedConnectors) { + PlanarPieceObjectResize { + Objects.requireNonNull(object, "Resized planar Jigsaw Studio object"); + if (relocatedConnectors < 0) { + throw new IllegalArgumentException("Relocated connector count cannot be negative"); + } + } + } + + private record ConnectorResize( + IrisJigsawConnector connector, + LocalPosition sourceCanonical, + LocalPosition targetCanonical + ) { + private ConnectorResize { + Objects.requireNonNull(connector, "Planar Jigsaw Studio connector"); + Objects.requireNonNull(sourceCanonical, "Planar Jigsaw Studio source connector position"); + Objects.requireNonNull(targetCanonical, "Planar Jigsaw Studio target connector position"); + } + } + + private record BlockRelocation(LocalPosition target, PlatformBlockState state, boolean present) { + private BlockRelocation { + Objects.requireNonNull(target, "Planar Jigsaw Studio connector block target"); + if (present) { + Objects.requireNonNull(state, "Planar Jigsaw Studio connector block state"); + } + } + } + + private record LocalPosition(int x, int y, int z) { + private static LocalPosition from(IrisPosition position) { + return new LocalPosition(position.getX(), position.getY(), position.getZ()); + } + + private IrisPosition toIrisPosition() { + return new IrisPosition(x, y, z); + } + + private String describe() { + return x + "," + y + "," + z; + } + } + + private record OwnedGraph( + Path root, + StructureTransactionWriter writer, + StructureOwnershipManifest manifest, + String expectedManifestHash + ) { + private StructureResourceBundle.Builder bundleBuilder() { + return StructureResourceBundle.builder(manifest.structure()) + .source(manifest.source()) + .backend(manifest.backend()) + .capabilities(manifest.capabilities()) + .losses(manifest.losses()); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioGraphMapper.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioGraphMapper.java new file mode 100644 index 000000000..e18007ec4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioGraphMapper.java @@ -0,0 +1,381 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.engine.framework.structure.PlanarJigsawWorkcellResolver; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawMode; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPieceEntry; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +public final class JigsawStudioGraphMapper { + private JigsawStudioGraphMapper() { + } + + public static JigsawStudioLayout map(IrisData data, IrisStructure structure) { + Objects.requireNonNull(data, "Jigsaw Studio graph mapping requires pack data"); + Objects.requireNonNull(structure, "Jigsaw Studio graph mapping requires a structure"); + JigsawStudioMode mode = structure.resolvedMode() == IrisJigsawMode.PLANAR_JIGSAW + ? JigsawStudioMode.PLANAR_JIGSAW + : JigsawStudioMode.SPATIAL_JIGSAW; + JigsawStudioVariantCatalog catalog = catalog(data, structure, mode); + IrisPosition configuredCell = structure.getCellSize(); + JigsawStudioCellDimensions dimensions = configuredCell == null + ? new JigsawStudioCellDimensions(15, 15, 15) + : new JigsawStudioCellDimensions( + Math.max(1, configuredCell.getX()), + Math.max(1, configuredCell.getY()), + Math.max(1, configuredCell.getZ())); + if (mode == JigsawStudioMode.SPATIAL_JIGSAW) { + dimensions = expandSpatialDimensions(data, catalog, dimensions); + return JigsawStudioLayout.createSpatial( + dimensions, + catalog, + structure.getSpatialWorkcellDisplayName()); + } + Map resolved = + PlanarJigsawWorkcellResolver.resolve(structure); + List workcells = new ArrayList<>(resolved.size()); + for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) { + PlanarJigsawWorkcellResolver.ResolvedWorkcell workcell = resolved.get(archetype.modelArchetype()); + workcells.add(new JigsawStudioWorkcellSpec( + archetype, + workcell.displayName(), + new JigsawStudioCellDimensions( + workcell.width(), + workcell.height(), + workcell.depth()), + workcell.enabled())); + } + return JigsawStudioLayout.createPlanar(dimensions, workcells, catalog); + } + + public static JigsawStudioVariantCatalog catalog( + IrisData data, + IrisStructure structure, + JigsawStudioMode mode + ) { + IrisData source = Objects.requireNonNull(data, "Jigsaw Studio catalog pack data"); + IrisStructure root = Objects.requireNonNull(structure, "Jigsaw Studio catalog structure"); + JigsawStudioMode activeMode = Objects.requireNonNull(mode, "Jigsaw Studio catalog mode"); + OwnedResources owned = ownedResources(source, root); + Map variants = new LinkedHashMap<>(); + Set visitedPools = new LinkedHashSet<>(); + Set queuedPools = new HashSet<>(); + ArrayDeque pools = new ArrayDeque<>(); + enqueuePool(root.getStartPool(), pools, queuedPools); + traversePools(source, activeMode, owned, pools, queuedPools, visitedPools, variants); + + for (String poolKey : owned.poolKeys()) { + enqueuePool(poolKey, pools, queuedPools); + } + traversePools(source, activeMode, owned, pools, queuedPools, visitedPools, variants); + + for (String pieceKey : owned.pieceKeys()) { + addOwnedVariant(source, activeMode, owned, pieceKey, variants); + } + + List built = new ArrayList<>(variants.size()); + for (MutableVariant variant : variants.values()) { + built.add(variant.build(source)); + } + return new JigsawStudioVariantCatalog(built, owned.editable()); + } + + public static JigsawPlanarTopology topologyOf(IrisJigsawPiece piece) { + Objects.requireNonNull(piece, "Planar topology requires a jigsaw piece"); + int mask = 0; + if (piece.getConnectors() != null) { + for (IrisJigsawConnector connector : piece.getConnectors()) { + if (connector == null || connector.getDirection() == null) { + continue; + } + mask |= directionBit(connector.getDirection()); + } + } + return JigsawPlanarTopology.fromMask(mask); + } + + static JigsawStudioCellDimensions expandSpatialDimensions( + IrisData data, + JigsawStudioVariantCatalog catalog, + JigsawStudioCellDimensions configured + ) { + if (data.getObjectLoader() == null) { + return configured; + } + int width = configured.width(); + int height = configured.height(); + int depth = configured.depth(); + for (JigsawStudioVariant variant : catalog.spatialVariants()) { + IrisObject object = data.getObjectLoader().load(variant.objectKey()); + if (object == null) { + continue; + } + int objectWidth = object.getW(); + int objectDepth = object.getD(); + if (variant.rotatable()) { + int horizontalSpan = Math.max(objectWidth, objectDepth); + objectWidth = horizontalSpan; + objectDepth = horizontalSpan; + } + width = Math.max(width, objectWidth); + height = Math.max(height, object.getH()); + depth = Math.max(depth, objectDepth); + } + return new JigsawStudioCellDimensions(width, height, depth); + } + + private static void traversePools( + IrisData data, + JigsawStudioMode mode, + OwnedResources owned, + ArrayDeque pools, + Set queuedPools, + Set visitedPools, + Map variants + ) { + while (!pools.isEmpty()) { + String poolKey = pools.removeFirst(); + if (!visitedPools.add(poolKey)) { + continue; + } + IrisJigsawPool pool = data.getJigsawPoolLoader().load(poolKey); + if (pool == null) { + continue; + } + enqueuePool(pool.getFallback(), pools, queuedPools); + if (pool.getPieces() == null) { + continue; + } + for (int entryIndex = 0; entryIndex < pool.getPieces().size(); entryIndex++) { + IrisJigsawPieceEntry entry = pool.getPieces().get(entryIndex); + if (entry == null || entry.isEmpty() || entry.getPiece() == null || entry.getPiece().isBlank()) { + continue; + } + String pieceKey = entry.getPiece(); + IrisJigsawPiece piece = data.getJigsawPieceLoader().load(pieceKey); + if (piece == null || piece.getObject() == null || piece.getObject().isBlank()) { + continue; + } + MutableVariant variant = variants.computeIfAbsent( + pieceKey, + key -> new MutableVariant( + key, + piece, + mode, + owned.owns(key, piece.getObject()))); + variant.addMembership(new JigsawStudioPoolMembership( + poolKey, + entryIndex, + entry.getWeight(), + entry.getChance())); + enqueueConnectorPools(piece, pools, queuedPools); + } + } + } + + private static void addOwnedVariant( + IrisData data, + JigsawStudioMode mode, + OwnedResources owned, + String pieceKey, + Map variants + ) { + if (variants.containsKey(pieceKey)) { + return; + } + IrisJigsawPiece piece = data.getJigsawPieceLoader().load(pieceKey); + if (piece == null || piece.getObject() == null || piece.getObject().isBlank()) { + return; + } + variants.put(pieceKey, new MutableVariant(pieceKey, piece, mode, owned.owns(pieceKey, piece.getObject()))); + } + + private static OwnedResources ownedResources(IrisData data, IrisStructure structure) { + String structureKey = structure.getLoadKey(); + File dataFolder = data.getDataFolder(); + if (structureKey == null || structureKey.isBlank() || dataFolder == null) { + return OwnedResources.empty(); + } + try { + Path root = dataFolder.toPath().toAbsolutePath().normalize(); + StructureTransactionWriter writer = new StructureTransactionWriter(root); + Path manifestPath = writer.ownershipManifestPath(StructureKey.parse(structureKey, "iris")); + if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + return OwnedResources.empty(); + } + StructureOwnershipManifest manifest = StructureOwnershipManifest.fromJson(Files.readAllBytes(manifestPath)); + Set pieceKeys = new TreeSet<>(); + Set poolKeys = new TreeSet<>(); + Set objectKeys = new TreeSet<>(); + for (String path : manifest.resourceHashes().keySet()) { + addOwnedKey(path, "jigsaw-pieces/", ".json", pieceKeys); + addOwnedKey(path, "jigsaw-pools/", ".json", poolKeys); + addOwnedKey(path, "objects/", ".iob", objectKeys); + } + boolean editable = JigsawStudioAuthoringAccess.isEditable(manifest); + return new OwnedResources(pieceKeys, poolKeys, objectKeys, editable); + } catch (Exception exception) { + throw new IllegalStateException( + "Failed to read Jigsaw Studio ownership for structure '" + structureKey + "'", + exception); + } + } + + private static void addOwnedKey(String path, String prefix, String suffix, Set keys) { + if (path.startsWith(prefix) && path.endsWith(suffix) && path.length() > prefix.length() + suffix.length()) { + keys.add(path.substring(prefix.length(), path.length() - suffix.length())); + } + } + + private static int directionBit(IrisDirection direction) { + return switch (direction) { + case NORTH_NEGATIVE_Z -> JigsawPlanarDirection.NORTH.bit(); + case EAST_POSITIVE_X -> JigsawPlanarDirection.EAST.bit(); + case SOUTH_POSITIVE_Z -> JigsawPlanarDirection.SOUTH.bit(); + case WEST_NEGATIVE_X -> JigsawPlanarDirection.WEST.bit(); + case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> 0; + }; + } + + private static void enqueueConnectorPools( + IrisJigsawPiece piece, + ArrayDeque pools, + Set queuedPools + ) { + if (piece.getConnectors() == null) { + return; + } + for (IrisJigsawConnector connector : piece.getConnectors()) { + if (connector != null) { + enqueuePool(connector.getPool(), pools, queuedPools); + } + } + } + + private static void enqueuePool(String poolKey, ArrayDeque pools, Set queuedPools) { + if (poolKey == null || poolKey.isBlank() || !queuedPools.add(poolKey)) { + return; + } + pools.addLast(poolKey); + } + + private static final class MutableVariant { + private final String pieceKey; + private final IrisJigsawPiece piece; + private final JigsawStudioMode mode; + private final boolean owned; + private final List memberships = new ArrayList<>(); + + private MutableVariant( + String pieceKey, + IrisJigsawPiece piece, + JigsawStudioMode mode, + boolean owned + ) { + this.pieceKey = pieceKey; + this.piece = piece; + this.mode = mode; + this.owned = owned; + } + + private void addMembership(JigsawStudioPoolMembership membership) { + memberships.add(membership); + } + + private JigsawStudioVariant build(IrisData data) { + Optional dimensions = objectDimensions(data, piece, mode); + return new JigsawStudioVariant( + pieceKey, + piece.getObject(), + piece.getDisplayName(), + dimensions, + mode, + mode == JigsawStudioMode.PLANAR_JIGSAW + ? Optional.of(topologyOf(piece)) + : Optional.empty(), + piece.isRotatable(), + owned, + piece.getThemes() == null ? List.of() : piece.getThemes(), + JigsawStudioPieceRules.from(piece.resolvedRules()), + memberships); + } + + private static Optional objectDimensions( + IrisData data, + IrisJigsawPiece piece, + JigsawStudioMode mode + ) { + if (data.getObjectLoader() == null) { + return Optional.empty(); + } + IrisObject object = data.getObjectLoader().load(piece.getObject()); + if (object == null) { + return Optional.empty(); + } + JigsawStudioCellDimensions sourceDimensions = new JigsawStudioCellDimensions( + object.getW(), + object.getH(), + object.getD()); + if (mode == JigsawStudioMode.SPATIAL_JIGSAW) { + return Optional.of(sourceDimensions); + } + JigsawPlanarTopology topology = topologyOf(piece); + int quarterTurns = JigsawPlanarArchetype.fromTopology(topology) + .sourceToCanonicalQuarterTurns(topology); + return Math.floorMod(quarterTurns, 2) == 0 + ? Optional.of(sourceDimensions) + : Optional.of(new JigsawStudioCellDimensions( + sourceDimensions.depth(), + sourceDimensions.height(), + sourceDimensions.width())); + } + } + + private record OwnedResources( + Set pieceKeys, + Set poolKeys, + Set objectKeys, + boolean editable + ) { + private OwnedResources { + pieceKeys = Collections.unmodifiableSet(new LinkedHashSet<>(pieceKeys)); + poolKeys = Collections.unmodifiableSet(new LinkedHashSet<>(poolKeys)); + objectKeys = Collections.unmodifiableSet(new LinkedHashSet<>(objectKeys)); + } + + private boolean owns(String pieceKey, String objectKey) { + return editable && pieceKeys.contains(pieceKey) && objectKeys.contains(objectKey); + } + + private static OwnedResources empty() { + return new OwnedResources(Set.of(), Set.of(), Set.of(), false); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioLayout.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioLayout.java new file mode 100644 index 000000000..407e5fdcd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioLayout.java @@ -0,0 +1,344 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public final class JigsawStudioLayout { + public static final int FLOOR_Y = 64; + public static final int PLANAR_COLUMNS = 3; + public static final int PLANAR_GAP = 1; + public static final int MAX_VARIANTS = 512; + public static final String SPATIAL_WORKCELL_ID = "workcell/spatial"; + + private static final int FIRST_ORIGIN = 16; + private static final JigsawStudioControlPosition CONTROL_POSITION = + new JigsawStudioControlPosition(8, FLOOR_Y + 1, 8); + + private final JigsawStudioMode mode; + private final JigsawStudioCellDimensions cellDimensions; + private final int columns; + private final int gap; + private final JigsawStudioVariantCatalog variantCatalog; + private final List bays; + private final Map byStableId; + private final Map spatialVariantByBay; + + private JigsawStudioLayout( + JigsawStudioMode mode, + JigsawStudioCellDimensions cellDimensions, + int columns, + int gap, + JigsawStudioVariantCatalog variantCatalog, + List bays, + Map spatialVariantByBay + ) { + this.mode = mode; + this.cellDimensions = cellDimensions; + this.columns = columns; + this.gap = gap; + this.variantCatalog = variantCatalog; + this.bays = Collections.unmodifiableList(new ArrayList<>(bays)); + Map index = new LinkedHashMap<>(); + for (JigsawStudioBay bay : bays) { + JigsawStudioBay previous = index.putIfAbsent(bay.stableId(), bay); + if (previous != null) { + throw new IllegalArgumentException("Duplicate Jigsaw Studio workcell stable ID " + bay.stableId()); + } + } + this.byStableId = Collections.unmodifiableMap(index); + this.spatialVariantByBay = Collections.unmodifiableMap(new LinkedHashMap<>(spatialVariantByBay)); + } + + public static JigsawStudioLayout create( + JigsawStudioMode mode, + JigsawStudioCellDimensions cellDimensions, + JigsawStudioVariantCatalog variantCatalog + ) { + JigsawStudioMode activeMode = Objects.requireNonNull(mode, "Jigsaw Studio layout mode"); + JigsawStudioCellDimensions dimensions = Objects.requireNonNull( + cellDimensions, + "Jigsaw Studio layout cell dimensions"); + JigsawStudioVariantCatalog catalog = Objects.requireNonNull( + variantCatalog, + "Jigsaw Studio variant catalog"); + validateCatalogMode(activeMode, catalog); + + if (activeMode == JigsawStudioMode.PLANAR_JIGSAW) { + return createPlanar(dimensions, uniformSpecs(dimensions), catalog); + } + + return createSpatial(dimensions, catalog, ""); + } + + public static JigsawStudioLayout createSpatial( + JigsawStudioCellDimensions cellDimensions, + JigsawStudioVariantCatalog variantCatalog, + String displayName + ) { + JigsawStudioCellDimensions dimensions = Objects.requireNonNull( + cellDimensions, + "Spatial Jigsaw Studio cell dimensions"); + JigsawStudioVariantCatalog catalog = Objects.requireNonNull( + variantCatalog, + "Spatial Jigsaw Studio variant catalog"); + validateCatalogMode(JigsawStudioMode.SPATIAL_JIGSAW, catalog); + String resolvedDisplayName = displayName == null ? "" : displayName.trim(); + + List variants = catalog.spatialVariants(); + List workcells = new ArrayList<>(Math.max(1, variants.size())); + Map variantsByWorkcell = new LinkedHashMap<>(); + if (variants.isEmpty()) { + workcells.add(new JigsawStudioBay( + SPATIAL_WORKCELL_ID, + JigsawStudioBayKind.SPATIAL_WORKCELL, + Optional.empty(), + resolvedDisplayName, + new JigsawStudioBounds(FIRST_ORIGIN, FLOOR_Y + 1, FIRST_ORIGIN, dimensions))); + } else { + int originX = FIRST_ORIGIN; + for (int index = 0; index < variants.size(); index++) { + JigsawStudioVariant variant = variants.get(index); + String stableId = index == 0 + ? SPATIAL_WORKCELL_ID + : SPATIAL_WORKCELL_ID + "/" + variant.pieceKey(); + workcells.add(new JigsawStudioBay( + stableId, + JigsawStudioBayKind.SPATIAL_WORKCELL, + Optional.empty(), + variant.resolvedDisplayName(), + new JigsawStudioBounds(originX, FLOOR_Y + 1, FIRST_ORIGIN, dimensions))); + variantsByWorkcell.put(stableId, variant.pieceKey()); + originX = Math.addExact(originX, Math.addExact(dimensions.width(), PLANAR_GAP)); + } + } + return new JigsawStudioLayout( + JigsawStudioMode.SPATIAL_JIGSAW, + dimensions, + workcells.size(), + PLANAR_GAP, + catalog, + workcells, + variantsByWorkcell); + } + + public static JigsawStudioLayout createPlanar( + JigsawStudioCellDimensions defaultDimensions, + List workcellSpecs, + JigsawStudioVariantCatalog variantCatalog + ) { + JigsawStudioCellDimensions defaults = Objects.requireNonNull( + defaultDimensions, + "Jigsaw Studio default cell dimensions"); + JigsawStudioVariantCatalog catalog = Objects.requireNonNull( + variantCatalog, + "Jigsaw Studio variant catalog"); + validateCatalogMode(JigsawStudioMode.PLANAR_JIGSAW, catalog); + Map specs = indexSpecs(workcellSpecs); + int[] columnWidths = new int[PLANAR_COLUMNS]; + int[] rowDepths = new int[2]; + JigsawPlanarArchetype[] archetypes = JigsawPlanarArchetype.values(); + for (int index = 0; index < archetypes.length; index++) { + JigsawStudioCellDimensions dimensions = specs.get(archetypes[index]).dimensions(); + int column = index % PLANAR_COLUMNS; + int row = index / PLANAR_COLUMNS; + columnWidths[column] = Math.max(columnWidths[column], dimensions.width()); + rowDepths[row] = Math.max(rowDepths[row], dimensions.depth()); + } + List workcells = new ArrayList<>(archetypes.length); + for (int index = 0; index < archetypes.length; index++) { + JigsawPlanarArchetype archetype = archetypes[index]; + JigsawStudioWorkcellSpec spec = specs.get(archetype); + workcells.add(new JigsawStudioBay( + archetype.stableId(), + JigsawStudioBayKind.PLANAR_WORKCELL, + Optional.of(spec), + spec.displayName(), + planarBounds(index, spec.dimensions(), columnWidths, rowDepths))); + } + return new JigsawStudioLayout( + JigsawStudioMode.PLANAR_JIGSAW, + defaults, + PLANAR_COLUMNS, + PLANAR_GAP, + catalog, + workcells, + Map.of()); + } + + public JigsawStudioMode mode() { + return mode; + } + + public JigsawStudioCellDimensions cellDimensions() { + return cellDimensions; + } + + public int columns() { + return columns; + } + + public int gap() { + return gap; + } + + public JigsawStudioVariantCatalog variantCatalog() { + return variantCatalog; + } + + public List bays() { + return bays; + } + + public JigsawStudioBay get(String stableId) { + return stableId == null ? null : byStableId.get(stableId); + } + + public JigsawStudioBay findAt(int worldX, int worldY, int worldZ) { + for (JigsawStudioBay bay : bays) { + if (bay.bounds().contains(worldX, worldY, worldZ)) { + return bay; + } + } + return null; + } + + public List variants(JigsawStudioBay workcell) { + JigsawStudioBay activeWorkcell = requireWorkcell(workcell); + if (activeWorkcell.kind() == JigsawStudioBayKind.SPATIAL_WORKCELL) { + String pieceKey = spatialVariantByBay.get(activeWorkcell.stableId()); + if (pieceKey == null) { + return variantCatalog.spatialVariants(); + } + return variantCatalog.find(pieceKey).map(List::of).orElseGet(List::of); + } + return variantCatalog.variants(activeWorkcell.archetype().orElseThrow()); + } + + public Optional defaultVariant(JigsawStudioBay workcell) { + List variants = variants(workcell); + return variants.isEmpty() ? Optional.empty() : Optional.of(variants.getFirst()); + } + + public boolean accepts(JigsawStudioBay workcell, JigsawStudioVariant variant) { + JigsawStudioBay activeWorkcell = requireWorkcell(workcell); + JigsawStudioVariant activeVariant = Objects.requireNonNull(variant, "Jigsaw Studio variant"); + if (variantCatalog.find(activeVariant.pieceKey()).filter(activeVariant::equals).isEmpty()) { + return false; + } + if (activeWorkcell.kind() == JigsawStudioBayKind.SPATIAL_WORKCELL) { + return activeVariant.mode() == JigsawStudioMode.SPATIAL_JIGSAW + && variants(activeWorkcell).contains(activeVariant); + } + return activeVariant.archetype().filter(activeWorkcell.archetype().orElseThrow()::equals).isPresent(); + } + + public Optional workcellForVariant(String pieceKey) { + Optional variant = variantCatalog.find(pieceKey); + if (variant.isEmpty()) { + return Optional.empty(); + } + if (mode == JigsawStudioMode.PLANAR_JIGSAW) { + return variant.get().archetype().map(archetype -> get(archetype.stableId())); + } + for (Map.Entry entry : spatialVariantByBay.entrySet()) { + if (entry.getValue().equals(variant.get().pieceKey())) { + return Optional.ofNullable(get(entry.getKey())); + } + } + return Optional.ofNullable(get(SPATIAL_WORKCELL_ID)); + } + + public JigsawStudioControlPosition controlPosition() { + return CONTROL_POSITION; + } + + public int extentX() { + int maximum = CONTROL_POSITION.worldX(); + for (JigsawStudioBay bay : bays) { + maximum = Math.max(maximum, bay.bounds().maxX() + PLANAR_GAP); + } + return maximum; + } + + public int extentZ() { + int maximum = CONTROL_POSITION.worldZ(); + for (JigsawStudioBay bay : bays) { + maximum = Math.max(maximum, bay.bounds().maxZ() + PLANAR_GAP); + } + return maximum; + } + + private JigsawStudioBay requireWorkcell(JigsawStudioBay workcell) { + JigsawStudioBay activeWorkcell = Objects.requireNonNull(workcell, "Jigsaw Studio workcell"); + if (byStableId.get(activeWorkcell.stableId()) != activeWorkcell) { + throw new IllegalArgumentException("Workcell does not belong to this Jigsaw Studio layout"); + } + return activeWorkcell; + } + + private static void validateCatalogMode(JigsawStudioMode mode, JigsawStudioVariantCatalog catalog) { + for (JigsawStudioVariant variant : catalog.variants()) { + if (variant.mode() != mode) { + throw new IllegalArgumentException("Jigsaw Studio variant mode does not match the layout mode"); + } + } + } + + private static List uniformSpecs(JigsawStudioCellDimensions dimensions) { + List specs = new ArrayList<>(JigsawPlanarArchetype.values().length); + for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) { + specs.add(new JigsawStudioWorkcellSpec(archetype, "", dimensions, true)); + } + return specs; + } + + private static Map indexSpecs( + List workcellSpecs + ) { + List source = List.copyOf(Objects.requireNonNull( + workcellSpecs, + "Jigsaw Studio workcell specifications")); + Map specs = + new EnumMap<>(JigsawPlanarArchetype.class); + for (JigsawStudioWorkcellSpec spec : source) { + JigsawStudioWorkcellSpec active = Objects.requireNonNull( + spec, + "Jigsaw Studio workcell specification"); + if (specs.putIfAbsent(active.archetype(), active) != null) { + throw new IllegalArgumentException( + "Duplicate Jigsaw Studio workcell specification " + active.archetype()); + } + } + for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) { + if (!specs.containsKey(archetype)) { + throw new IllegalArgumentException("Missing Jigsaw Studio workcell specification " + archetype); + } + } + return Collections.unmodifiableMap(specs); + } + + private static JigsawStudioBounds planarBounds( + int index, + JigsawStudioCellDimensions dimensions, + int[] columnWidths, + int[] rowDepths + ) { + int column = index % PLANAR_COLUMNS; + int row = index / PLANAR_COLUMNS; + int originX = FIRST_ORIGIN; + for (int current = 0; current < column; current++) { + originX = Math.addExact(originX, Math.addExact(columnWidths[current], PLANAR_GAP)); + } + int originZ = FIRST_ORIGIN; + for (int current = 0; current < row; current++) { + originZ = Math.addExact(originZ, Math.addExact(rowDepths[current], PLANAR_GAP)); + } + return new JigsawStudioBounds(originX, FLOOR_Y + 1, originZ, dimensions); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioMarkerKeyCodec.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioMarkerKeyCodec.java new file mode 100644 index 000000000..43fa4c18b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioMarkerKeyCodec.java @@ -0,0 +1,35 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.Locale; +import java.util.Objects; +import java.util.regex.Pattern; + +public final class JigsawStudioMarkerKeyCodec { + private static final String STUDIO_NAMESPACE = "iris:"; + private static final Pattern INTERNAL_PATH = Pattern.compile("[a-z0-9._-]+(?:/[a-z0-9._-]+)*"); + + private JigsawStudioMarkerKeyCodec() { + } + + public static String encodePool(String internalPoolKey) { + return STUDIO_NAMESPACE + requireInternalPath(internalPoolKey, "pool"); + } + + public static String decodePool(String markerPoolKey) { + String markerKey = Objects.requireNonNull(markerPoolKey, "Jigsaw Studio marker pool key").trim(); + if (!markerKey.toLowerCase(Locale.ROOT).startsWith(STUDIO_NAMESPACE)) { + throw new IllegalArgumentException( + "Jigsaw Studio marker pools must use iris:, not '" + markerKey + "'"); + } + return requireInternalPath(markerKey.substring(STUDIO_NAMESPACE.length()), "pool"); + } + + public static String requireInternalPath(String value, String kind) { + String path = Objects.requireNonNull(value, "Jigsaw Studio " + kind + " key").trim(); + if (!INTERNAL_PATH.matcher(path).matches()) { + throw new IllegalArgumentException("Jigsaw Studio " + kind + + " keys must use lowercase [a-z0-9._-/] resource-path characters"); + } + return path; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioMode.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioMode.java new file mode 100644 index 000000000..e114f3ee6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioMode.java @@ -0,0 +1,6 @@ +package art.arcane.iris.core.runtime.jigsaw; + +public enum JigsawStudioMode { + PLANAR_JIGSAW, + SPATIAL_JIGSAW +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPieceRules.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPieceRules.java new file mode 100644 index 000000000..2238d8f0b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPieceRules.java @@ -0,0 +1,34 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.engine.object.IrisJigsawPieceRules; + +import java.util.Objects; + +public record JigsawStudioPieceRules( + int minimumDepth, + int maximumDepth, + int minimumPlacements, + int maximumPlacements, + boolean terminal +) { + public JigsawStudioPieceRules { + if (minimumDepth < 0 || maximumDepth < minimumDepth || maximumDepth > 30) { + throw new IllegalArgumentException("Jigsaw Studio piece depth rules are invalid"); + } + if (minimumPlacements < 0 || maximumPlacements < 0 + || minimumPlacements > 512 || maximumPlacements > 512 + || maximumPlacements != 0 && minimumPlacements > maximumPlacements) { + throw new IllegalArgumentException("Jigsaw Studio piece placement-count rules are invalid"); + } + } + + public static JigsawStudioPieceRules from(IrisJigsawPieceRules rules) { + IrisJigsawPieceRules source = Objects.requireNonNull(rules, "Jigsaw Studio piece rules"); + return new JigsawStudioPieceRules( + source.getMinimumDepth(), + source.getMaximumDepth(), + source.getMinimumPlacements(), + source.getMaximumPlacements(), + source.isTerminal()); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPoolEditor.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPoolEditor.java new file mode 100644 index 000000000..85937116e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPoolEditor.java @@ -0,0 +1,442 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.authoring.StructureWriteOptions; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; + +public final class JigsawStudioPoolEditor { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private JigsawStudioPoolEditor() { + } + + public static WeightUpdate updateWeight( + Path packRoot, + String structureKey, + String poolKey, + String pieceKey, + int weight + ) throws IOException { + if (weight < 1) { + throw new IllegalArgumentException("Jigsaw variant weight must be positive"); + } + PoolUpdate update = updateOwnedPool( + packRoot, + structureKey, + poolKey, + (content, poolPath) -> mutateWeight(content, pieceKey, weight, poolPath)); + return new WeightUpdate( + update.changed(), + update.changedEntries(), + update.poolPath(), + update.writeResult()); + } + + public static WeightUpdate updateWeightAtIndex( + Path packRoot, + String structureKey, + String poolKey, + int entryIndex, + String expectedPieceKey, + int weight + ) throws IOException { + if (weight < 1) { + throw new IllegalArgumentException("Jigsaw variant weight must be positive"); + } + if (entryIndex < 0) { + throw new IllegalArgumentException("Jigsaw pool entry index cannot be negative"); + } + PoolUpdate update = updateOwnedPool( + packRoot, + structureKey, + poolKey, + (content, poolPath) -> mutateWeightAtIndex( + content, + entryIndex, + expectedPieceKey, + weight, + poolPath)); + return new WeightUpdate( + update.changed(), + update.changedEntries(), + update.poolPath(), + update.writeResult()); + } + + public static ChanceUpdate updateChanceAtIndex( + Path packRoot, + String structureKey, + String poolKey, + int entryIndex, + String expectedPieceKey, + double chance + ) throws IOException { + if (!Double.isFinite(chance) || chance < 0D || chance > 1D) { + throw new IllegalArgumentException("Jigsaw variant chance must be finite and within 0 and 1"); + } + if (entryIndex < 0) { + throw new IllegalArgumentException("Jigsaw pool entry index cannot be negative"); + } + PoolUpdate update = updateOwnedPool( + packRoot, + structureKey, + poolKey, + (content, poolPath) -> mutateChanceAtIndex( + content, + entryIndex, + expectedPieceKey, + chance, + poolPath)); + return new ChanceUpdate( + update.changed(), + update.changedEntries(), + update.poolPath(), + update.writeResult()); + } + + public static PoolUpdate addPiece( + Path packRoot, + String structureKey, + String poolKey, + String pieceKey, + int weight + ) throws IOException { + if (weight < 1) { + throw new IllegalArgumentException("Jigsaw piece weight must be positive"); + } + return updateOwnedPool( + packRoot, + structureKey, + poolKey, + (content, poolPath) -> mutateAdd(content, pieceKey, weight, poolPath)); + } + + public static PoolUpdate removePiece( + Path packRoot, + String structureKey, + String poolKey, + String pieceKey + ) throws IOException { + return updateOwnedPool( + packRoot, + structureKey, + poolKey, + (content, poolPath) -> mutateRemove(content, pieceKey, poolPath)); + } + + public static PoolUpdate removeEntry( + Path packRoot, + String structureKey, + String poolKey, + int entryIndex, + String expectedPieceKey + ) throws IOException { + if (entryIndex < 0) { + throw new IllegalArgumentException("Jigsaw pool entry index cannot be negative"); + } + return updateOwnedPool( + packRoot, + structureKey, + poolKey, + (content, poolPath) -> mutateRemoveAtIndex( + content, + entryIndex, + expectedPieceKey, + poolPath)); + } + + public static PoolUpdate updateFallback( + Path packRoot, + String structureKey, + String poolKey, + String fallbackPoolKey + ) throws IOException { + String fallback = fallbackPoolKey == null ? "" : fallbackPoolKey.trim(); + return updateOwnedPool( + packRoot, + structureKey, + poolKey, + (content, poolPath) -> mutateFallback(content, fallback, poolPath)); + } + + private static PoolUpdate updateOwnedPool( + Path packRoot, + String structureKey, + String poolKey, + PoolContentEditor editor + ) throws IOException { + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + StructureKey ownershipKey = StructureKey.parse(structureKey, "iris"); + StructureTransactionWriter writer = new StructureTransactionWriter(root); + Path manifestPath = writer.ownershipManifestPath(ownershipKey); + if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("This graph is read-only because it is not Studio-owned; create a new Jigsaw Studio project before editing pools"); + } + byte[] manifestContent = Files.readAllBytes(manifestPath); + String expectedManifestHash = StructureHash.sha256(manifestContent); + StructureOwnershipManifest manifest = JigsawStudioAuthoringAccess.requireEditable( + StructureOwnershipManifest.fromJson(manifestContent)); + String normalizedPool = JigsawStudioProjectCreator.Options.requireResourceKey(poolKey); + String targetResource = "jigsaw-pools/" + normalizedPool + ".json"; + Path targetPath = root.resolve(targetResource).normalize(); + if (!manifest.resourceHashes().containsKey(targetResource)) { + return new PoolUpdate(false, 0, targetPath, null); + } + + StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(manifest.structure()) + .source(manifest.source()) + .backend(manifest.backend()) + .capabilities(manifest.capabilities()) + .losses(manifest.losses()); + int changedEntries = 0; + for (Map.Entry resource : manifest.resourceHashes().entrySet()) { + Path resourcePath = resolveOwnedResource(root, resource.getKey()); + byte[] content = Files.readAllBytes(resourcePath); + if (resource.getKey().equals(targetResource)) { + PoolMutation mutation = editor.edit(content, resourcePath); + changedEntries = mutation.changedEntries(); + content = mutation.content(); + } + bundle.resource(resource.getKey(), content); + } + if (changedEntries == 0) { + return new PoolUpdate(false, 0, targetPath, null); + } + StructureResourceBundle updatedBundle = bundle.build(); + StructureResourceBundleGraphCompiler.requireViable(updatedBundle); + StructureWriteResult writeResult = writer.write( + updatedBundle, + StructureWriteOptions.overwriteExpected(expectedManifestHash)); + if (!writeResult.successful()) { + String conflict = writeResult.conflicts().isEmpty() + ? writeResult.status().name() + : writeResult.conflicts().getFirst().relativePath() + ": " + + writeResult.conflicts().getFirst().reason(); + throw new IOException("Atomic graph update was rejected: " + conflict); + } + return new PoolUpdate(true, changedEntries, targetPath, writeResult); + } + + private static Path resolveOwnedResource(Path root, String relativePath) throws IOException { + StructureResourceBundle.validateRelativePath(relativePath); + Path resource = root.resolve(relativePath).normalize(); + if (!resource.startsWith(root) || !Files.isRegularFile(resource, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Owned graph resource is missing or unsafe: " + relativePath); + } + return resource; + } + + private static PoolMutation mutateWeight( + byte[] content, + String pieceKey, + int weight, + Path poolPath + ) throws IOException { + JsonObject root = parsePool(content, poolPath); + JsonArray pieces = root.getAsJsonArray("pieces"); + int changedEntries = 0; + for (JsonElement element : pieces) { + if (!isPiece(element, pieceKey)) { + continue; + } + element.getAsJsonObject().addProperty("weight", weight); + changedEntries++; + } + return mutation(root, changedEntries); + } + + private static PoolMutation mutateWeightAtIndex( + byte[] content, + int entryIndex, + String expectedPieceKey, + int weight, + Path poolPath + ) throws IOException { + JsonObject root = parsePool(content, poolPath); + JsonArray pieces = root.getAsJsonArray("pieces"); + JsonObject entry = requirePieceEntry(pieces, entryIndex, expectedPieceKey, poolPath); + int currentWeight = entry.has("weight") ? entry.get("weight").getAsInt() : 1; + if (currentWeight == weight) { + return mutation(root, 0); + } + entry.addProperty("weight", weight); + return mutation(root, 1); + } + + private static PoolMutation mutateChanceAtIndex( + byte[] content, + int entryIndex, + String expectedPieceKey, + double chance, + Path poolPath + ) throws IOException { + JsonObject root = parsePool(content, poolPath); + JsonArray pieces = root.getAsJsonArray("pieces"); + JsonObject entry = requirePieceEntry(pieces, entryIndex, expectedPieceKey, poolPath); + double currentChance = entry.has("chance") ? entry.get("chance").getAsDouble() : 1D; + if (Double.compare(currentChance, chance) == 0) { + return mutation(root, 0); + } + entry.addProperty("chance", chance); + return mutation(root, 1); + } + + private static PoolMutation mutateAdd( + byte[] content, + String pieceKey, + int weight, + Path poolPath + ) throws IOException { + JsonObject root = parsePool(content, poolPath); + JsonArray pieces = root.getAsJsonArray("pieces"); + for (JsonElement element : pieces) { + if (isPiece(element, pieceKey)) { + return mutation(root, 0); + } + } + JsonObject entry = new JsonObject(); + entry.addProperty("piece", pieceKey); + entry.addProperty("weight", weight); + pieces.add(entry); + return mutation(root, 1); + } + + private static PoolMutation mutateRemove( + byte[] content, + String pieceKey, + Path poolPath + ) throws IOException { + JsonObject root = parsePool(content, poolPath); + JsonArray pieces = root.getAsJsonArray("pieces"); + int changedEntries = 0; + for (int index = pieces.size() - 1; index >= 0; index--) { + if (isPiece(pieces.get(index), pieceKey)) { + pieces.remove(index); + changedEntries++; + } + } + return mutation(root, changedEntries); + } + + private static PoolMutation mutateRemoveAtIndex( + byte[] content, + int entryIndex, + String expectedPieceKey, + Path poolPath + ) throws IOException { + JsonObject root = parsePool(content, poolPath); + JsonArray pieces = root.getAsJsonArray("pieces"); + requirePieceEntry(pieces, entryIndex, expectedPieceKey, poolPath); + pieces.remove(entryIndex); + return mutation(root, 1); + } + + private static PoolMutation mutateFallback( + byte[] content, + String fallbackPoolKey, + Path poolPath + ) throws IOException { + JsonObject root = parsePool(content, poolPath); + String existing = root.has("fallback") && !root.get("fallback").isJsonNull() + ? root.get("fallback").getAsString().trim() + : ""; + if (existing.equals(fallbackPoolKey)) { + return mutation(root, 0); + } + root.addProperty("fallback", fallbackPoolKey); + return mutation(root, 1); + } + + private static JsonObject parsePool(byte[] content, Path poolPath) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw pool is not a JSON object: " + poolPath); + } + JsonObject root = parsed.getAsJsonObject(); + if (!root.has("pieces") || !root.get("pieces").isJsonArray()) { + throw new IOException("Jigsaw pool does not declare a pieces array: " + poolPath); + } + return root; + } + + private static boolean isPiece(JsonElement element, String pieceKey) { + return element.isJsonObject() + && element.getAsJsonObject().has("piece") + && pieceKey.equals(element.getAsJsonObject().get("piece").getAsString()); + } + + private static JsonObject requirePieceEntry( + JsonArray pieces, + int entryIndex, + String expectedPieceKey, + Path poolPath + ) throws IOException { + if (entryIndex >= pieces.size()) { + throw new IOException("Jigsaw pool entry " + entryIndex + " no longer exists in " + poolPath); + } + JsonElement element = pieces.get(entryIndex); + if (!isPiece(element, expectedPieceKey)) { + throw new IOException("Jigsaw pool entry " + entryIndex + " changed before the update in " + + poolPath); + } + return element.getAsJsonObject(); + } + + private static PoolMutation mutation(JsonObject root, int changedEntries) { + return new PoolMutation( + (GSON.toJson(root) + "\n").getBytes(StandardCharsets.UTF_8), + changedEntries); + } + + public record WeightUpdate( + boolean changed, + int changedEntries, + Path poolPath, + StructureWriteResult writeResult + ) { + } + + public record ChanceUpdate( + boolean changed, + int changedEntries, + Path poolPath, + StructureWriteResult writeResult + ) { + } + + public record PoolUpdate( + boolean changed, + int changedEntries, + Path poolPath, + StructureWriteResult writeResult + ) { + } + + @FunctionalInterface + private interface PoolContentEditor { + PoolMutation edit(byte[] content, Path poolPath) throws IOException; + } + + private record PoolMutation(byte[] content, int changedEntries) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPoolMembership.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPoolMembership.java new file mode 100644 index 000000000..8cbeed936 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioPoolMembership.java @@ -0,0 +1,22 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.Objects; + +public record JigsawStudioPoolMembership(String poolKey, int entryIndex, int weight, double chance) { + public JigsawStudioPoolMembership { + Objects.requireNonNull(poolKey, "Jigsaw Studio pool key"); + poolKey = poolKey.trim(); + if (poolKey.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio pool key cannot be blank"); + } + if (entryIndex < 0) { + throw new IllegalArgumentException("Jigsaw Studio pool entry index cannot be negative"); + } + if (weight < 1) { + throw new IllegalArgumentException("Jigsaw Studio pool membership weight must be positive"); + } + if (!Double.isFinite(chance) || chance < 0D || chance > 1D) { + throw new IllegalArgumentException("Jigsaw Studio pool membership chance must be within 0 and 1"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioProjectCreator.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioProjectCreator.java new file mode 100644 index 000000000..ba9ae56fe --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioProjectCreator.java @@ -0,0 +1,294 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.core.structure.authoring.StructureBackend; +import art.arcane.iris.core.structure.authoring.StructureCapability; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureSource; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.authoring.StructureWriteMode; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler; +import art.arcane.iris.engine.object.IrisJigsawBranchFailurePolicy; +import art.arcane.iris.engine.object.IrisJigsawCompatibility; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawMode; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPieceEntry; +import art.arcane.iris.engine.object.IrisJigsawPieceRules; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisJigsawThemeSet; +import art.arcane.iris.engine.object.IrisJigsawWorkcell; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.JigsawJoint; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +public final class JigsawStudioProjectCreator { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final List SPATIAL_CONNECTOR_ORDER = List.of( + IrisDirection.NORTH_NEGATIVE_Z, + IrisDirection.SOUTH_POSITIVE_Z, + IrisDirection.EAST_POSITIVE_X, + IrisDirection.WEST_NEGATIVE_X, + IrisDirection.UP_POSITIVE_Y, + IrisDirection.DOWN_NEGATIVE_Y); + + private JigsawStudioProjectCreator() { + } + + public static StructureWriteResult create(Path packRoot, Options options) throws IOException { + Options activeOptions = Objects.requireNonNull(options, "Jigsaw Studio project options"); + StructureResourceBundle bundle = bundle(activeOptions); + StructureResourceBundleGraphCompiler.requireViable(bundle); + return new StructureTransactionWriter(packRoot).write(bundle, StructureWriteMode.ADD_ONLY); + } + + static StructureResourceBundle bundle(Options options) throws IOException { + String resourceKey = options.structureKey(); + StructureKey ownershipKey = new StructureKey("iris", resourceKey); + IrisStructure structure = new IrisStructure() + .setStartPool(resourceKey + "/start") + .setMaxDepth(7) + .setMaxSizeChunks(8) + .setMode(toModelMode(options.mode())) + .setCompatibility(toModelCompatibility(options.compatibilityTarget())) + .setBranchFailurePolicy(toBranchFailurePolicy(options.compatibilityTarget())) + .setCellSize(new IrisPosition( + options.cellDimensions().width(), + options.cellDimensions().height(), + options.cellDimensions().depth())); + if (options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) { + structure.getThemeSets().add(new IrisJigsawThemeSet("variant-1", 1)); + } + StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(ownershipKey) + .source(StructureSource.of(StructureSource.Kind.IRIS, ownershipKey)) + .backend(StructureBackend.IRIS_ASSEMBLY) + .capability(StructureCapability.BLOCKS) + .capability(StructureCapability.CONNECTORS) + .capability(StructureCapability.IRIS_PLACEMENT); + IrisJigsawPool pool = new IrisJigsawPool(); + if (options.mode() == JigsawStudioMode.PLANAR_JIGSAW) { + addPlanarDefaults(bundle, structure, pool, options); + } else { + addSpatialDefaults(bundle, pool, options); + } + bundle.textResource("jigsaw-pools/" + resourceKey + "/start.json", GSON.toJson(pool) + "\n"); + bundle.textResource("structures/" + resourceKey + ".json", GSON.toJson(structure) + "\n"); + return bundle.build(); + } + + static byte[] serialize(IrisObject object) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + object.write(output); + return output.toByteArray(); + } + + private static IrisJigsawMode toModelMode(JigsawStudioMode mode) { + return mode == JigsawStudioMode.PLANAR_JIGSAW + ? IrisJigsawMode.PLANAR_JIGSAW + : IrisJigsawMode.SPATIAL_JIGSAW; + } + + private static IrisJigsawCompatibility toModelCompatibility( + JigsawStudioCompatibilityTarget compatibilityTarget + ) { + return compatibilityTarget == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE + ? IrisJigsawCompatibility.VANILLA_PORTABLE + : IrisJigsawCompatibility.IRIS_EXTENDED; + } + + private static IrisJigsawBranchFailurePolicy toBranchFailurePolicy( + JigsawStudioCompatibilityTarget compatibilityTarget + ) { + return compatibilityTarget == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE + ? IrisJigsawBranchFailurePolicy.TERMINATE_BRANCH + : IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY; + } + + private static void addPlanarDefaults( + StructureResourceBundle.Builder bundle, + IrisStructure structure, + IrisJigsawPool pool, + Options options + ) throws IOException { + JigsawStudioCellDimensions dimensions = options.cellDimensions(); + IrisPosition size = new IrisPosition(dimensions.width(), dimensions.height(), dimensions.depth()); + String piecePoolKey = options.structureKey() + "/pieces"; + String capPoolKey = options.structureKey() + "/caps"; + IrisJigsawPool piecePool = new IrisJigsawPool().setFallback(capPoolKey); + IrisJigsawPool capPool = new IrisJigsawPool(); + for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) { + String key = options.structureKey() + "/" + archetype.name().toLowerCase(Locale.ROOT); + IrisJigsawPiece piece = planarPiece(key, piecePoolKey, size, archetype); + if (options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) { + piece.getThemes().add("variant-1"); + } + if (archetype == JigsawPlanarArchetype.CROSS) { + pool.getPieces().add(new IrisJigsawPieceEntry(key, 1)); + } + if (archetype != JigsawPlanarArchetype.BLANK) { + piecePool.getPieces().add(new IrisJigsawPieceEntry(key, 1)); + } + if (archetype == JigsawPlanarArchetype.END) { + if (options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) { + piece.setRules(new IrisJigsawPieceRules().setTerminal(true)); + } + capPool.getPieces().add(new IrisJigsawPieceEntry(key, 1)); + } + structure.getPlanarWorkcells().add(new IrisJigsawWorkcell( + "", + archetype.modelArchetype(), + dimensions.width(), + dimensions.height(), + dimensions.depth(), + true)); + bundle.resource("objects/" + key + ".iob", serialize(new IrisObject( + dimensions.width(), + dimensions.height(), + dimensions.depth()))); + bundle.textResource("jigsaw-pieces/" + key + ".json", GSON.toJson(piece) + "\n"); + } + capPool.getPieces().add(new IrisJigsawPieceEntry().setEmpty(true)); + bundle.textResource("jigsaw-pools/" + piecePoolKey + ".json", GSON.toJson(piecePool) + "\n"); + bundle.textResource("jigsaw-pools/" + capPoolKey + ".json", GSON.toJson(capPool) + "\n"); + } + + private static void addSpatialDefaults( + StructureResourceBundle.Builder bundle, + IrisJigsawPool pool, + Options options + ) throws IOException { + JigsawStudioCellDimensions dimensions = options.cellDimensions(); + String piecePoolKey = options.structureKey() + "/pieces"; + IrisJigsawPool piecePool = new IrisJigsawPool(); + for (int connectorCount = 0; connectorCount <= SPATIAL_CONNECTOR_ORDER.size(); connectorCount++) { + String key = options.structureKey() + "/" + + (connectorCount == 0 ? "start" : "connectors-" + connectorCount); + IrisJigsawPiece piece = spatialPiece( + key, + piecePoolKey, + dimensions, + connectorCount); + if (options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) { + piece.getThemes().add("variant-1"); + } + pool.getPieces().add(new IrisJigsawPieceEntry(key, 1)); + if (connectorCount > 0) { + piecePool.getPieces().add(new IrisJigsawPieceEntry(key, 1)); + } + bundle.resource("objects/" + key + ".iob", serialize(new IrisObject( + dimensions.width(), + dimensions.height(), + dimensions.depth()))); + bundle.textResource("jigsaw-pieces/" + key + ".json", GSON.toJson(piece) + "\n"); + } + piecePool.getPieces().add(new IrisJigsawPieceEntry().setEmpty(true)); + bundle.textResource("jigsaw-pools/" + piecePoolKey + ".json", GSON.toJson(piecePool) + "\n"); + } + + private static IrisJigsawPiece spatialPiece( + String objectKey, + String poolKey, + JigsawStudioCellDimensions dimensions, + int connectorCount + ) { + IrisJigsawPiece piece = new IrisJigsawPiece() + .setDisplayName(connectorCount + (connectorCount == 1 ? " Connector" : " Connectors")) + .setObject(objectKey) + .setRotatable(true) + .setRules(new IrisJigsawPieceRules().setMaximumPlacements(16)); + for (int index = 0; index < connectorCount; index++) { + IrisDirection direction = SPATIAL_CONNECTOR_ORDER.get(index); + piece.getConnectors().add(new IrisJigsawConnector() + .setPosition(spatialConnectorPosition(dimensions, direction)) + .setDirection(direction) + .setTop(direction.isVertical() + ? IrisDirection.NORTH_NEGATIVE_Z + : IrisDirection.UP_POSITIVE_Y) + .setPool(poolKey) + .setName("iris:spatial") + .setTargetName("iris:spatial") + .setJoint(JigsawJoint.ROLLABLE) + .setFinalState("minecraft:structure_void")); + } + return piece; + } + + private static IrisPosition spatialConnectorPosition( + JigsawStudioCellDimensions dimensions, + IrisDirection direction + ) { + int centerX = dimensions.width() / 2; + int centerY = dimensions.height() / 2; + int centerZ = dimensions.depth() / 2; + return switch (direction) { + case NORTH_NEGATIVE_Z -> new IrisPosition(centerX, centerY, 0); + case SOUTH_POSITIVE_Z -> new IrisPosition(centerX, centerY, dimensions.depth() - 1); + case EAST_POSITIVE_X -> new IrisPosition(dimensions.width() - 1, centerY, centerZ); + case WEST_NEGATIVE_X -> new IrisPosition(0, centerY, centerZ); + case UP_POSITIVE_Y -> new IrisPosition(centerX, dimensions.height() - 1, centerZ); + case DOWN_NEGATIVE_Y -> new IrisPosition(centerX, 0, centerZ); + }; + } + + private static IrisJigsawPiece planarPiece( + String objectKey, + String poolKey, + IrisPosition dimensions, + JigsawPlanarArchetype archetype + ) { + IrisJigsawPiece piece = new IrisJigsawPiece().setObject(objectKey).setRotatable(true); + for (JigsawPlanarDirection planarDirection : archetype.canonicalTopology().directions()) { + IrisDirection direction = planarDirection.irisDirection(); + piece.getConnectors().add(new IrisJigsawConnector() + .setPosition(IrisJigsawConnector.canonicalPlanarPosition(dimensions, direction)) + .setDirection(direction) + .setTop(IrisDirection.UP_POSITIVE_Y) + .setPool(poolKey) + .setName("iris:planar") + .setTargetName("iris:planar") + .setJoint(JigsawJoint.ALIGNED) + .setFinalState("minecraft:structure_void")); + } + return piece; + } + + public record Options( + String structureKey, + JigsawStudioMode mode, + JigsawStudioCompatibilityTarget compatibilityTarget, + JigsawStudioCellDimensions cellDimensions + ) { + public Options { + structureKey = requireResourceKey(structureKey); + mode = Objects.requireNonNull(mode, "Jigsaw Studio mode"); + compatibilityTarget = Objects.requireNonNull( + compatibilityTarget, + "Jigsaw Studio compatibility target"); + cellDimensions = Objects.requireNonNull(cellDimensions, "Jigsaw Studio cell dimensions"); + if (mode == JigsawStudioMode.PLANAR_JIGSAW + && (cellDimensions.width() < 3 || cellDimensions.depth() < 3)) { + throw new IllegalArgumentException( + "Planar Jigsaw Studio cells require width and depth of at least 3 blocks"); + } + } + + static String requireResourceKey(String value) { + String key = JigsawStudioMarkerKeyCodec.requireInternalPath(value, "resource"); + StructureResourceBundle.validateRelativePath("structures/" + key + ".json"); + new StructureKey("iris", key); + return key; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioProjectDeletionService.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioProjectDeletionService.java new file mode 100644 index 000000000..4c01e088f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioProjectDeletionService.java @@ -0,0 +1,435 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureSource; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import java.util.stream.Stream; + +public final class JigsawStudioProjectDeletionService { + private static final long MAX_JSON_BYTES = 16L * 1024L * 1024L; + private static final int MAX_SCANNED_FILES = 100_000; + + private JigsawStudioProjectDeletionService() { + } + + public static DeletionPlan inspect(Path packRoot, String structureKey) throws IOException { + Path root = canonicalRoot(packRoot); + StructureKey key = StructureKey.parse(structureKey, "iris"); + ManifestSnapshot snapshot = loadSnapshot(root, key); + List blockers = scanReverseReferences(root, snapshot); + return new DeletionPlan( + UUID.randomUUID(), + root, + key, + snapshot.manifest().source(), + snapshot.manifestHash(), + snapshot.sourceHash(), + snapshot.manifest().resourceHashes(), + blockers); + } + + public static ProjectDeletionResult delete(DeletionPlan plan) throws IOException { + DeletionPlan expected = Objects.requireNonNull(plan, "Jigsaw Studio project deletion plan"); + if (!expected.deletable()) { + throw new IOException("Jigsaw Studio project deletion is blocked by " + + expected.blockers().size() + " reverse references"); + } + Path root = canonicalRoot(expected.packRoot()); + StructureTransactionWriter writer = new StructureTransactionWriter(root); + StructureTransactionWriter.OwnedRemoval request = new StructureTransactionWriter.OwnedRemoval( + expected.structureKey(), + expected.expectedSource().kind(), + expected.expectedSource().key(), + Optional.empty(), + Optional.of(expected.expectedManifestHash())); + try (StructureTransactionWriter.PreparedRemoval removal = writer.prepareOwnedRemovals( + List.of(request), + () -> validateDeletion(root, expected))) { + if (!removal.changed()) { + throw new IOException("Jigsaw Studio project no longer exists"); + } + removal.markCommitted(); + removal.finishCommit(); + } + return new ProjectDeletionResult( + expected.planId(), + expected.structureKey(), + expected.expectedResourceHashes().size(), + true); + } + + private static void validateDeletion(Path root, DeletionPlan expected) throws IOException { + ManifestSnapshot current = loadSnapshot(root, expected.structureKey()); + if (!expected.expectedManifestHash().equals(current.manifestHash())) { + throw new IOException("Jigsaw Studio project changed after deletion was inspected"); + } + if (!expected.expectedSource().equals(current.manifest().source()) + || !expected.expectedSourceHash().equals(current.sourceHash())) { + throw new IOException("Jigsaw Studio project source changed after deletion was inspected"); + } + if (!expected.expectedResourceHashes().equals(current.manifest().resourceHashes())) { + throw new IOException("Jigsaw Studio project resources changed after deletion was inspected"); + } + List currentBlockers = scanReverseReferences(root, current); + if (!currentBlockers.isEmpty()) { + throw new IOException("Jigsaw Studio project gained " + currentBlockers.size() + + " reverse references after deletion was inspected"); + } + } + + private static Path canonicalRoot(Path packRoot) throws IOException { + Path normalized = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + Path resolved; + try { + resolved = normalized.toRealPath(); + } catch (IOException exception) { + throw new IOException("Cannot resolve Jigsaw Studio pack root " + normalized, exception); + } + if (!Files.isDirectory(resolved, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Jigsaw Studio pack root is not a directory: " + resolved); + } + return resolved; + } + + private static ManifestSnapshot loadSnapshot(Path root, StructureKey key) throws IOException { + StructureTransactionWriter writer = new StructureTransactionWriter(root); + Path manifestPath = writer.ownershipManifestPath(key); + if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("This graph is read-only because it is not Studio-owned; only Studio-owned projects can be deleted"); + } + byte[] manifestContent = readBoundedJson(manifestPath, "Jigsaw Studio ownership manifest"); + StructureOwnershipManifest manifest; + try { + manifest = JigsawStudioAuthoringAccess.requireEditable( + StructureOwnershipManifest.fromJson(manifestContent)); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid Jigsaw Studio ownership manifest at " + manifestPath, exception); + } + if (!manifest.structure().equals(key)) { + throw new IOException("Jigsaw Studio ownership manifest belongs to " + manifest.structure()); + } + String structureResource = "structures/" + manifest.structure().path() + ".json"; + String structureHash = manifest.resourceHashes().get(structureResource); + if (structureHash == null) { + throw new IOException("Jigsaw Studio ownership manifest does not include " + structureResource); + } + for (Map.Entry resource : manifest.resourceHashes().entrySet()) { + Path resourcePath = resolveOwnedResource(root, resource.getKey()); + String actualHash; + try (InputStream input = Files.newInputStream(resourcePath)) { + actualHash = StructureHash.sha256(input); + } + if (!resource.getValue().equals(actualHash)) { + throw new IOException("Owned graph resource changed outside Studio: " + resource.getKey()); + } + } + String sourceHash = manifest.source().contentHash().isEmpty() + ? structureHash + : manifest.source().contentHash(); + return new ManifestSnapshot( + manifest, + StructureHash.sha256(manifestContent), + sourceHash, + manifestPath); + } + + private static Path resolveOwnedResource(Path root, String relativePath) throws IOException { + StructureResourceBundle.validateRelativePath(relativePath); + Path resource = root.resolve(relativePath).normalize(); + if (!resource.startsWith(root) + || !Files.isRegularFile(resource, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Owned graph resource is missing or unsafe: " + relativePath); + } + return resource; + } + + private static List scanReverseReferences( + Path root, + ManifestSnapshot snapshot + ) throws IOException { + Map> targetsByKey = referenceTargets(snapshot.manifest()); + Set ownedPaths = snapshot.manifest().resourceHashes().keySet(); + Set references = new LinkedHashSet<>(); + int scannedFiles = 0; + try (Stream paths = Files.walk(root)) { + Iterator iterator = paths.iterator(); + while (iterator.hasNext()) { + Path path = iterator.next(); + Path relative = root.relativize(path); + String relativePath = relative.toString().replace(path.getFileSystem().getSeparator(), "/"); + if (relativePath.isEmpty() || relativePath.startsWith(".iris/")) { + continue; + } + if (Files.isSymbolicLink(path)) { + if (relativePath.endsWith(".json") || Files.isDirectory(path)) { + throw new IOException("Cannot prove deletion safety through symbolic pack path " + + relativePath); + } + continue; + } + if (!relativePath.endsWith(".json") || ownedPaths.contains(relativePath)) { + continue; + } + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + scannedFiles++; + if (scannedFiles > MAX_SCANNED_FILES) { + throw new IOException("Jigsaw Studio project deletion scan exceeds " + + MAX_SCANNED_FILES + " JSON resources"); + } + JsonElement json = parseBoundedJson(path, relativePath); + collectReferences(json, "$", relativePath, targetsByKey, references); + } + } + scanOwnershipManifests(root, snapshot, references); + List ordered = new ArrayList<>(references); + ordered.sort(Comparator.comparing(ReverseReference::ownerPath) + .thenComparing(ReverseReference::location) + .thenComparing(ReverseReference::targetResourcePath)); + return List.copyOf(ordered); + } + + private static Map> referenceTargets(StructureOwnershipManifest manifest) { + Map> targets = new LinkedHashMap<>(); + addReferenceTarget(targets, manifest.structure().path(), + "structures/" + manifest.structure().path() + ".json"); + addReferenceTarget(targets, manifest.structure().value(), + "structures/" + manifest.structure().path() + ".json"); + for (String relativePath : manifest.resourceHashes().keySet()) { + addResourceReferenceTarget(targets, relativePath, "jigsaw-pools/", ".json"); + addResourceReferenceTarget(targets, relativePath, "jigsaw-pieces/", ".json"); + addResourceReferenceTarget(targets, relativePath, "objects/", ".iob"); + } + return targets; + } + + private static void addResourceReferenceTarget( + Map> targets, + String relativePath, + String prefix, + String suffix + ) { + if (!relativePath.startsWith(prefix) || !relativePath.endsWith(suffix)) { + return; + } + String key = relativePath.substring(prefix.length(), relativePath.length() - suffix.length()); + addReferenceTarget(targets, key, relativePath); + addReferenceTarget(targets, "iris:" + key, relativePath); + } + + private static void addReferenceTarget( + Map> targets, + String key, + String relativePath + ) { + targets.computeIfAbsent(key, ignored -> new LinkedHashSet<>()).add(relativePath); + } + + private static void collectReferences( + JsonElement element, + String location, + String ownerPath, + Map> targetsByKey, + Set references + ) { + if (element == null || element.isJsonNull()) { + return; + } + if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + Set targets = targetsByKey.get(element.getAsString()); + if (targets != null) { + for (String target : targets) { + references.add(new ReverseReference(ownerPath, location, target)); + } + } + return; + } + if (element.isJsonArray()) { + JsonArray array = element.getAsJsonArray(); + for (int index = 0; index < array.size(); index++) { + collectReferences( + array.get(index), + location + "[" + index + "]", + ownerPath, + targetsByKey, + references); + } + return; + } + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + for (Map.Entry entry : object.entrySet()) { + collectReferences( + entry.getValue(), + location + "." + entry.getKey(), + ownerPath, + targetsByKey, + references); + } + } + } + + private static void scanOwnershipManifests( + Path root, + ManifestSnapshot target, + Set references + ) throws IOException { + Path manifestRoot = root.resolve(".iris/structure-manifests"); + if (!Files.isDirectory(manifestRoot, LinkOption.NOFOLLOW_LINKS)) { + return; + } + try (Stream paths = Files.list(manifestRoot)) { + Iterator iterator = paths.iterator(); + while (iterator.hasNext()) { + Path path = iterator.next(); + if (Files.isSameFile(path, target.manifestPath()) + || !path.getFileName().toString().endsWith(".json")) { + continue; + } + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Cannot prove deletion safety through ownership manifest " + path); + } + StructureOwnershipManifest manifest; + try { + manifest = StructureOwnershipManifest.fromJson( + readBoundedJson(path, "Structure ownership manifest")); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid structure ownership manifest at " + path, exception); + } + for (String relativePath : target.manifest().resourceHashes().keySet()) { + if (manifest.resourceHashes().containsKey(relativePath)) { + references.add(new ReverseReference( + root.relativize(path).toString().replace(path.getFileSystem().getSeparator(), "/"), + "$.resourceHashes", + relativePath)); + } + } + } + } + } + + private static JsonElement parseBoundedJson(Path path, String relativePath) throws IOException { + byte[] content = readBoundedJson(path, "JSON resource"); + try { + return JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + } catch (RuntimeException exception) { + throw new IOException("Cannot prove deletion safety through invalid JSON resource " + + relativePath, exception); + } + } + + private static byte[] readBoundedJson(Path path, String kind) throws IOException { + long size = Files.size(path); + if (size > MAX_JSON_BYTES) { + throw new IOException(kind + " exceeds " + MAX_JSON_BYTES + " bytes: " + path); + } + return Files.readAllBytes(path); + } + + public record DeletionPlan( + UUID planId, + Path packRoot, + StructureKey structureKey, + StructureSource expectedSource, + String expectedManifestHash, + String expectedSourceHash, + Map expectedResourceHashes, + List blockers + ) { + public DeletionPlan { + Objects.requireNonNull(planId, "Jigsaw Studio project deletion plan ID"); + packRoot = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + Objects.requireNonNull(structureKey, "Jigsaw Studio project deletion structure key"); + Objects.requireNonNull(expectedSource, "Jigsaw Studio project deletion source"); + if (!StructureHash.isSha256(expectedManifestHash)) { + throw new IllegalArgumentException("Expected Jigsaw Studio ownership manifest hash must be SHA-256"); + } + if (!StructureHash.isSha256(expectedSourceHash)) { + throw new IllegalArgumentException("Expected Jigsaw Studio source hash must be SHA-256"); + } + Objects.requireNonNull(expectedResourceHashes, "Jigsaw Studio project deletion resources"); + expectedResourceHashes = Collections.unmodifiableMap(new TreeMap<>(expectedResourceHashes)); + blockers = List.copyOf(Objects.requireNonNull( + blockers, + "Jigsaw Studio project deletion blockers")); + } + + public boolean deletable() { + return blockers.isEmpty(); + } + } + + public record ReverseReference( + String ownerPath, + String location, + String targetResourcePath + ) { + public ReverseReference { + ownerPath = requireNonBlank(ownerPath, "reverse-reference owner path"); + location = requireNonBlank(location, "reverse-reference location"); + targetResourcePath = StructureResourceBundle.validateRelativePath(targetResourcePath); + } + } + + public record ProjectDeletionResult( + UUID planId, + StructureKey structureKey, + int removedResourceCount, + boolean manifestRemoved + ) { + public ProjectDeletionResult { + Objects.requireNonNull(planId, "Jigsaw Studio project deletion plan ID"); + Objects.requireNonNull(structureKey, "Deleted Jigsaw Studio structure key"); + if (removedResourceCount < 0) { + throw new IllegalArgumentException("Removed Jigsaw Studio resource count cannot be negative"); + } + } + } + + private static String requireNonBlank(String value, String name) { + Objects.requireNonNull(value, name); + String normalized = value.trim(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException(name + " cannot be blank"); + } + return normalized; + } + + private record ManifestSnapshot( + StructureOwnershipManifest manifest, + String manifestHash, + String sourceHash, + Path manifestPath + ) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioSession.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioSession.java new file mode 100644 index 000000000..f847f83bf --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioSession.java @@ -0,0 +1,689 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +public final class JigsawStudioSession { + private final UUID sessionId; + private final String packKey; + private final String structureKey; + private final Map workcells = new LinkedHashMap<>(); + private JigsawStudioLayout layout; + private String selectedBayId; + private long nextLoadGeneration; + private long nextMutationGeneration; + private long nextOperationGeneration; + private long revision; + + public JigsawStudioSession(String packKey, String structureKey, JigsawStudioLayout layout) { + this(UUID.randomUUID(), packKey, structureKey, layout); + } + + public JigsawStudioSession( + UUID sessionId, + String packKey, + String structureKey, + JigsawStudioLayout layout + ) { + this.sessionId = Objects.requireNonNull(sessionId, "Jigsaw Studio session ID"); + this.packKey = requireKey(packKey, "pack"); + this.structureKey = requireKey(structureKey, "structure"); + this.layout = Objects.requireNonNull(layout, "Jigsaw Studio session layout"); + initializeWorkcells(layout); + } + + public synchronized UUID sessionId() { + return sessionId; + } + + public synchronized String packKey() { + return packKey; + } + + public synchronized String structureKey() { + return structureKey; + } + + public synchronized JigsawStudioLayout layout() { + return layout; + } + + public synchronized Optional selectedBayId() { + return Optional.ofNullable(selectedBayId); + } + + public synchronized boolean selectBay(String stableId) { + Objects.requireNonNull(stableId, "Jigsaw Studio selected workcell ID"); + if (layout.get(stableId) == null) { + return false; + } + selectedBayId = stableId; + return true; + } + + public synchronized void clearSelection() { + selectedBayId = null; + } + + public synchronized Optional activeVariant(String workcellId) { + MutableWorkcellState state = workcells.get(workcellId); + if (state == null || state.activeVariantKey.isEmpty()) { + return Optional.empty(); + } + return layout.variantCatalog().find(state.activeVariantKey); + } + + public synchronized WorkcellSnapshot workcellSnapshot(String workcellId) { + MutableWorkcellState state = requireWorkcellState(workcellId); + return state.snapshot(workcellId); + } + + public synchronized boolean setConnectorsVisible(String workcellId, boolean visible) { + MutableWorkcellState state = requireWorkcellState(workcellId); + if (state.connectorsVisible == visible) { + return false; + } + state.connectorsVisible = visible; + revision++; + return true; + } + + public synchronized boolean replaceLayout(JigsawStudioLayout replacement) { + JigsawStudioLayout nextLayout = Objects.requireNonNull(replacement, "Replacement Jigsaw Studio layout"); + if (layout.mode() != nextLayout.mode()) { + throw new IllegalArgumentException("Replacement Jigsaw Studio layout mode does not match the session"); + } + if (operationInProgress()) { + throw new IllegalStateException("Jigsaw Studio layout cannot change during a save or variant switch"); + } + if (layout == nextLayout) { + return false; + } + + Map updated = new LinkedHashMap<>(); + for (JigsawStudioBay workcell : nextLayout.bays()) { + MutableWorkcellState previous = workcells.get(workcell.stableId()); + String activeVariantKey = retainedVariantKey(nextLayout, workcell, previous); + if (previous != null && previous.activeVariantKey.equals(activeVariantKey)) { + updated.put(workcell.stableId(), previous.copy()); + continue; + } + updated.put(workcell.stableId(), new MutableWorkcellState( + activeVariantKey, + nextLoadGeneration(), + nextMutationGeneration(), + false, + previous != null && previous.connectorsVisible)); + } + layout = nextLayout; + workcells.clear(); + workcells.putAll(updated); + if (selectedBayId != null && layout.get(selectedBayId) == null) { + selectedBayId = null; + } + revision++; + return true; + } + + public synchronized boolean replaceLayoutAndRebind( + JigsawStudioLayout replacement, + Map activeVariantsByWorkcell + ) { + JigsawStudioLayout nextLayout = Objects.requireNonNull(replacement, "Replacement Jigsaw Studio layout"); + Map bindings = Map.copyOf(Objects.requireNonNull( + activeVariantsByWorkcell, + "Replacement Jigsaw Studio active variants")); + if (layout.mode() != nextLayout.mode()) { + throw new IllegalArgumentException("Replacement Jigsaw Studio layout mode does not match the session"); + } + if (operationInProgress()) { + throw new IllegalStateException("Jigsaw Studio layout cannot change during a save or variant switch"); + } + for (Map.Entry binding : bindings.entrySet()) { + JigsawStudioBay workcell = nextLayout.get(binding.getKey()); + JigsawStudioVariant variant = nextLayout.variantCatalog().find(binding.getValue()) + .orElseThrow(() -> new IllegalArgumentException( + "Unknown replacement Jigsaw Studio variant " + binding.getValue())); + if (workcell == null || !nextLayout.accepts(workcell, variant)) { + throw new IllegalArgumentException("Replacement Jigsaw Studio variant " + binding.getValue() + + " does not belong to workcell " + binding.getKey()); + } + } + + Map updated = new LinkedHashMap<>(); + boolean changed = layout != nextLayout; + for (JigsawStudioBay workcell : nextLayout.bays()) { + MutableWorkcellState previous = workcells.get(workcell.stableId()); + String targetVariantKey = bindings.get(workcell.stableId()); + if (targetVariantKey == null) { + targetVariantKey = retainedVariantKey(nextLayout, workcell, previous); + } + if (previous != null && previous.activeVariantKey.equals(targetVariantKey)) { + updated.put(workcell.stableId(), previous.copy()); + continue; + } + changed = true; + updated.put(workcell.stableId(), new MutableWorkcellState( + targetVariantKey, + nextLoadGeneration(), + nextMutationGeneration(), + false, + previous != null && previous.connectorsVisible)); + } + layout = nextLayout; + workcells.clear(); + workcells.putAll(updated); + if (selectedBayId != null && layout.get(selectedBayId) == null) { + selectedBayId = null; + } + if (changed) { + revision++; + } + return changed; + } + + public synchronized SwitchStart beginVariantSwitch( + String workcellId, + String targetPieceKey, + boolean discardDirty + ) { + return beginVariantTransition(workcellId, targetPieceKey, discardDirty, false); + } + + public synchronized SwitchStart beginVariantReload(String workcellId) { + MutableWorkcellState state = workcells.get(workcellId); + if (state == null) { + return SwitchStart.failure(SwitchStatus.UNKNOWN_WORKCELL); + } + if (state.activeVariantKey.isEmpty()) { + return SwitchStart.failure(SwitchStatus.UNKNOWN_VARIANT); + } + return beginVariantTransition(workcellId, state.activeVariantKey, false, true); + } + + private SwitchStart beginVariantTransition( + String workcellId, + String targetPieceKey, + boolean discardDirty, + boolean allowActive + ) { + MutableWorkcellState state = workcells.get(workcellId); + if (state == null) { + return SwitchStart.failure(SwitchStatus.UNKNOWN_WORKCELL); + } + Optional target = layout.variantCatalog().find(targetPieceKey); + if (target.isEmpty()) { + return SwitchStart.failure(SwitchStatus.UNKNOWN_VARIANT); + } + JigsawStudioBay workcell = layout.get(workcellId); + if (!layout.accepts(workcell, target.get())) { + return SwitchStart.failure(SwitchStatus.WRONG_WORKCELL); + } + if (state.switchInProgress) { + return SwitchStart.failure(SwitchStatus.SWITCH_IN_PROGRESS); + } + if (state.saveInProgress) { + return SwitchStart.failure(SwitchStatus.SAVE_IN_PROGRESS); + } + if (state.activeVariantKey.equals(targetPieceKey) && !allowActive) { + return SwitchStart.failure(SwitchStatus.ALREADY_ACTIVE); + } + if (state.dirty && !discardDirty) { + return SwitchStart.failure(SwitchStatus.DIRTY); + } + + JigsawStudioVariant previous = state.activeVariantKey.isEmpty() + ? null + : layout.variantCatalog().find(state.activeVariantKey).orElse(null); + long switchGeneration = nextOperationGeneration(); + state.switchInProgress = true; + state.switchGeneration = switchGeneration; + VariantSwitchToken token = new VariantSwitchToken( + sessionId, + workcellId, + Optional.ofNullable(previous), + target.get(), + state.loadGeneration, + state.mutationGeneration, + switchGeneration, + discardDirty); + revision++; + return SwitchStart.started(token); + } + + public synchronized boolean completeVariantSwitch(VariantSwitchToken expected) { + VariantSwitchToken token = Objects.requireNonNull(expected, "Jigsaw Studio variant switch token"); + MutableWorkcellState state = workcells.get(token.workcellId()); + if (!validSwitchToken(state, token)) { + return false; + } + state.activeVariantKey = token.targetVariant().pieceKey(); + state.loadGeneration = nextLoadGeneration(); + state.mutationGeneration = nextMutationGeneration(); + state.dirty = false; + state.switchInProgress = false; + state.switchGeneration = 0L; + revision++; + return true; + } + + public synchronized boolean isVariantSwitchCurrent(VariantSwitchToken expected) { + VariantSwitchToken token = Objects.requireNonNull(expected, "Jigsaw Studio variant switch token"); + return validSwitchToken(workcells.get(token.workcellId()), token); + } + + public synchronized boolean abortVariantSwitch(VariantSwitchToken expected) { + VariantSwitchToken token = Objects.requireNonNull(expected, "Jigsaw Studio variant switch token"); + MutableWorkcellState state = workcells.get(token.workcellId()); + if (!validSwitchToken(state, token)) { + return false; + } + state.switchInProgress = false; + state.switchGeneration = 0L; + revision++; + return true; + } + + public synchronized DirtyMark markWorkcellDirty(String workcellId) { + MutableWorkcellState state = workcells.get(workcellId); + if (state == null) { + return DirtyMark.failure(DirtyStatus.UNKNOWN_WORKCELL); + } + if (state.activeVariantKey.isEmpty()) { + return DirtyMark.failure(DirtyStatus.NO_ACTIVE_VARIANT); + } + if (state.switchInProgress) { + return DirtyMark.failure(DirtyStatus.SWITCH_IN_PROGRESS); + } + state.mutationGeneration = nextMutationGeneration(); + boolean newlyDirty = !state.dirty; + state.dirty = true; + revision++; + return DirtyMark.marked(new DirtyIdentity( + sessionId, + workcellId, + state.activeVariantKey, + state.loadGeneration, + state.mutationGeneration), newlyDirty); + } + + public synchronized boolean isDirtyCurrent(DirtyIdentity expected) { + DirtyIdentity identity = Objects.requireNonNull(expected, "Jigsaw Studio dirty identity"); + MutableWorkcellState state = workcells.get(identity.workcellId()); + return sessionId.equals(identity.sessionId()) + && state != null + && state.dirty + && state.activeVariantKey.equals(identity.variantKey()) + && state.loadGeneration == identity.loadGeneration() + && state.mutationGeneration == identity.mutationGeneration(); + } + + public synchronized SaveStart beginSave(String workcellId) { + MutableWorkcellState state = workcells.get(workcellId); + if (state == null) { + return SaveStart.failure(SaveStatus.UNKNOWN_WORKCELL); + } + if (state.activeVariantKey.isEmpty()) { + return SaveStart.failure(SaveStatus.NO_ACTIVE_VARIANT); + } + if (state.switchInProgress) { + return SaveStart.failure(SaveStatus.SWITCH_IN_PROGRESS); + } + if (state.saveInProgress) { + return SaveStart.failure(SaveStatus.SAVE_IN_PROGRESS); + } + long saveGeneration = nextOperationGeneration(); + state.saveInProgress = true; + state.saveGeneration = saveGeneration; + SaveIdentity identity = new SaveIdentity( + sessionId, + workcellId, + state.activeVariantKey, + state.loadGeneration, + state.mutationGeneration, + saveGeneration); + revision++; + return SaveStart.started(identity); + } + + public synchronized boolean markWorkcellSaved(SaveIdentity expected) { + SaveIdentity identity = Objects.requireNonNull(expected, "Jigsaw Studio save identity"); + MutableWorkcellState state = workcells.get(identity.workcellId()); + if (!validSaveReservation(state, identity)) { + return false; + } + boolean unchanged = state.activeVariantKey.equals(identity.variantKey()) + && state.loadGeneration == identity.loadGeneration() + && state.mutationGeneration == identity.mutationGeneration(); + state.saveInProgress = false; + state.saveGeneration = 0L; + if (unchanged) { + state.dirty = false; + } + revision++; + return unchanged; + } + + public synchronized boolean isSaveCurrent(SaveIdentity expected) { + SaveIdentity identity = Objects.requireNonNull(expected, "Jigsaw Studio save identity"); + MutableWorkcellState state = workcells.get(identity.workcellId()); + return validSaveReservation(state, identity) + && state.activeVariantKey.equals(identity.variantKey()) + && state.loadGeneration == identity.loadGeneration() + && state.mutationGeneration == identity.mutationGeneration(); + } + + public synchronized boolean abortSave(SaveIdentity expected) { + SaveIdentity identity = Objects.requireNonNull(expected, "Jigsaw Studio save identity"); + MutableWorkcellState state = workcells.get(identity.workcellId()); + if (!validSaveReservation(state, identity)) { + return false; + } + state.saveInProgress = false; + state.saveGeneration = 0L; + revision++; + return true; + } + + public synchronized boolean isDirty() { + for (MutableWorkcellState state : workcells.values()) { + if (state.dirty) { + return true; + } + } + return false; + } + + public synchronized List dirtyWorkcellIds() { + List dirty = new ArrayList<>(); + for (Map.Entry entry : workcells.entrySet()) { + if (entry.getValue().dirty) { + dirty.add(entry.getKey()); + } + } + return List.copyOf(dirty); + } + + public synchronized boolean operationInProgress() { + for (MutableWorkcellState state : workcells.values()) { + if (state.saveInProgress || state.switchInProgress) { + return true; + } + } + return false; + } + + public synchronized long revision() { + return revision; + } + + private void initializeWorkcells(JigsawStudioLayout initialLayout) { + for (JigsawStudioBay workcell : initialLayout.bays()) { + String variantKey = initialLayout.defaultVariant(workcell) + .map(JigsawStudioVariant::pieceKey) + .orElse(""); + workcells.put(workcell.stableId(), new MutableWorkcellState( + variantKey, + nextLoadGeneration(), + nextMutationGeneration(), + false, + false)); + } + } + + private String retainedVariantKey( + JigsawStudioLayout nextLayout, + JigsawStudioBay workcell, + MutableWorkcellState previous + ) { + if (previous != null && previous.activeVariantKey.isEmpty()) { + return ""; + } + if (previous != null) { + Optional retained = nextLayout.variantCatalog().find(previous.activeVariantKey); + if (retained.isPresent() && nextLayout.accepts(workcell, retained.get())) { + return previous.activeVariantKey; + } + } + return nextLayout.defaultVariant(workcell).map(JigsawStudioVariant::pieceKey).orElse(""); + } + + private boolean validSwitchToken(MutableWorkcellState state, VariantSwitchToken token) { + if (!sessionId.equals(token.sessionId()) || state == null || !state.switchInProgress) { + return false; + } + String previousKey = token.previousVariant().map(JigsawStudioVariant::pieceKey).orElse(""); + return state.switchGeneration == token.switchGeneration() + && state.activeVariantKey.equals(previousKey) + && state.loadGeneration == token.loadGeneration() + && state.mutationGeneration == token.mutationGeneration(); + } + + private boolean validSaveReservation(MutableWorkcellState state, SaveIdentity identity) { + return sessionId.equals(identity.sessionId()) + && state != null + && state.saveInProgress + && state.saveGeneration == identity.saveGeneration(); + } + + private MutableWorkcellState requireWorkcellState(String workcellId) { + MutableWorkcellState state = workcells.get(Objects.requireNonNull(workcellId, "Jigsaw Studio workcell ID")); + if (state == null) { + throw new IllegalArgumentException("Unknown Jigsaw Studio workcell " + workcellId); + } + return state; + } + + private long nextLoadGeneration() { + return nextLoadGeneration = Math.incrementExact(nextLoadGeneration); + } + + private long nextMutationGeneration() { + return nextMutationGeneration = Math.incrementExact(nextMutationGeneration); + } + + private long nextOperationGeneration() { + return nextOperationGeneration = Math.incrementExact(nextOperationGeneration); + } + + private static String requireKey(String value, String name) { + Objects.requireNonNull(value, "Jigsaw Studio " + name + " key"); + String normalized = value.trim(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio " + name + " key cannot be blank"); + } + return normalized; + } + + public enum SwitchStatus { + STARTED, + UNKNOWN_WORKCELL, + UNKNOWN_VARIANT, + WRONG_WORKCELL, + ALREADY_ACTIVE, + DIRTY, + SAVE_IN_PROGRESS, + SWITCH_IN_PROGRESS + } + + public enum SaveStatus { + STARTED, + UNKNOWN_WORKCELL, + NO_ACTIVE_VARIANT, + SWITCH_IN_PROGRESS, + SAVE_IN_PROGRESS + } + + public enum DirtyStatus { + MARKED, + UNKNOWN_WORKCELL, + NO_ACTIVE_VARIANT, + SWITCH_IN_PROGRESS + } + + public record WorkcellSnapshot( + String workcellId, + String activeVariantKey, + long loadGeneration, + long mutationGeneration, + boolean dirty, + boolean saveInProgress, + boolean switchInProgress, + boolean connectorsVisible + ) { + } + + public record DirtyIdentity( + UUID sessionId, + String workcellId, + String variantKey, + long loadGeneration, + long mutationGeneration + ) { + public DirtyIdentity { + Objects.requireNonNull(sessionId, "Jigsaw Studio dirty session ID"); + Objects.requireNonNull(workcellId, "Jigsaw Studio dirty workcell ID"); + Objects.requireNonNull(variantKey, "Jigsaw Studio dirty variant key"); + } + } + + public record DirtyMark( + DirtyStatus status, + Optional identity, + boolean newlyDirty + ) { + public DirtyMark { + status = Objects.requireNonNull(status, "Jigsaw Studio dirty status"); + identity = Objects.requireNonNull(identity, "Jigsaw Studio dirty identity"); + if (status == DirtyStatus.MARKED && identity.isEmpty()) { + throw new IllegalArgumentException("A marked Jigsaw Studio workcell requires a dirty identity"); + } + if (status != DirtyStatus.MARKED && (identity.isPresent() || newlyDirty)) { + throw new IllegalArgumentException("A rejected Jigsaw Studio dirty mark cannot carry an identity"); + } + } + + private static DirtyMark marked(DirtyIdentity identity, boolean newlyDirty) { + return new DirtyMark(DirtyStatus.MARKED, Optional.of(identity), newlyDirty); + } + + private static DirtyMark failure(DirtyStatus status) { + return new DirtyMark(status, Optional.empty(), false); + } + } + + public record VariantSwitchToken( + UUID sessionId, + String workcellId, + Optional previousVariant, + JigsawStudioVariant targetVariant, + long loadGeneration, + long mutationGeneration, + long switchGeneration, + boolean discardDirty + ) { + public VariantSwitchToken { + Objects.requireNonNull(sessionId, "Jigsaw Studio switch session ID"); + Objects.requireNonNull(workcellId, "Jigsaw Studio switch workcell ID"); + previousVariant = Objects.requireNonNull(previousVariant, "Jigsaw Studio previous variant"); + targetVariant = Objects.requireNonNull(targetVariant, "Jigsaw Studio target variant"); + } + } + + public record SwitchStart(SwitchStatus status, Optional token) { + public SwitchStart { + status = Objects.requireNonNull(status, "Jigsaw Studio switch status"); + token = Objects.requireNonNull(token, "Jigsaw Studio switch token"); + } + + private static SwitchStart started(VariantSwitchToken token) { + return new SwitchStart(SwitchStatus.STARTED, Optional.of(token)); + } + + private static SwitchStart failure(SwitchStatus status) { + return new SwitchStart(status, Optional.empty()); + } + } + + public record SaveIdentity( + UUID sessionId, + String workcellId, + String variantKey, + long loadGeneration, + long mutationGeneration, + long saveGeneration + ) { + public SaveIdentity { + Objects.requireNonNull(sessionId, "Jigsaw Studio save session ID"); + Objects.requireNonNull(workcellId, "Jigsaw Studio save workcell ID"); + Objects.requireNonNull(variantKey, "Jigsaw Studio save variant key"); + } + } + + public record SaveStart(SaveStatus status, Optional identity) { + public SaveStart { + status = Objects.requireNonNull(status, "Jigsaw Studio save status"); + identity = Objects.requireNonNull(identity, "Jigsaw Studio save identity"); + } + + private static SaveStart started(SaveIdentity identity) { + return new SaveStart(SaveStatus.STARTED, Optional.of(identity)); + } + + private static SaveStart failure(SaveStatus status) { + return new SaveStart(status, Optional.empty()); + } + } + + private static final class MutableWorkcellState { + private String activeVariantKey; + private long loadGeneration; + private long mutationGeneration; + private boolean dirty; + private boolean saveInProgress; + private long saveGeneration; + private boolean switchInProgress; + private long switchGeneration; + private boolean connectorsVisible; + + private MutableWorkcellState( + String activeVariantKey, + long loadGeneration, + long mutationGeneration, + boolean dirty, + boolean connectorsVisible + ) { + this.activeVariantKey = activeVariantKey; + this.loadGeneration = loadGeneration; + this.mutationGeneration = mutationGeneration; + this.dirty = dirty; + this.connectorsVisible = connectorsVisible; + } + + private MutableWorkcellState copy() { + return new MutableWorkcellState( + activeVariantKey, + loadGeneration, + mutationGeneration, + dirty, + connectorsVisible); + } + + private WorkcellSnapshot snapshot(String workcellId) { + return new WorkcellSnapshot( + workcellId, + activeVariantKey, + loadGeneration, + mutationGeneration, + dirty, + saveInProgress, + switchInProgress, + connectorsVisible); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioStructureEditor.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioStructureEditor.java new file mode 100644 index 000000000..c98270d5b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioStructureEditor.java @@ -0,0 +1,465 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.authoring.StructureWriteOptions; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import art.arcane.iris.engine.framework.structure.PlanarJigsawWorkcellResolver; +import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler; +import art.arcane.iris.engine.object.IrisJigsawMode; +import art.arcane.iris.engine.object.IrisJigsawThemeSet; +import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.util.common.math.IrisBlockVector; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public final class JigsawStudioStructureEditor { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private JigsawStudioStructureEditor() { + } + + public static StructureWriteResult updateCellSize( + Path packRoot, + String structureKey, + JigsawStudioCellDimensions dimensions + ) throws IOException { + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + ManifestSnapshot snapshot = loadManifest(root, structureKey); + requireOwnedObjectsFit(root, snapshot.manifest(), dimensions); + return updateOwnedStructure( + root, + structureKey, + snapshot, + (content, structurePath) -> updateCellSize(content, dimensions, structurePath)); + } + + public static StructureWriteResult updateLimits( + Path packRoot, + String structureKey, + int maxDepth, + int maxSizeChunks + ) throws IOException { + if (maxDepth < 1 || maxDepth > 30) { + throw new IllegalArgumentException("Jigsaw max depth must be between 1 and 30"); + } + if (maxSizeChunks < 1 || maxSizeChunks > 32) { + throw new IllegalArgumentException("Jigsaw maximum size must be between 1 and 32 chunks"); + } + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + ManifestSnapshot snapshot = loadManifest(root, structureKey); + return updateOwnedStructure( + root, + structureKey, + snapshot, + (content, structurePath) -> updateLimits( + content, maxDepth, maxSizeChunks, structurePath)); + } + + public static StructureWriteResult updateThemeSets( + Path packRoot, + String structureKey, + List themeSets + ) throws IOException { + List normalizedThemeSets = normalizeThemeSets(themeSets); + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + ManifestSnapshot snapshot = loadManifest(root, structureKey); + return updateOwnedStructure( + root, + structureKey, + snapshot, + (content, structurePath) -> updateThemeSets( + content, + normalizedThemeSets, + structurePath)); + } + + public static StructureWriteResult updateRequireCaps( + Path packRoot, + String structureKey, + boolean requireCaps + ) throws IOException { + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + ManifestSnapshot snapshot = loadManifest(root, structureKey); + return updateOwnedStructure( + root, + structureKey, + snapshot, + (content, structurePath) -> updateRequireCaps( + content, + requireCaps, + structurePath)); + } + + public static StructureWriteResult updateWorkcellEnabled( + Path packRoot, + String structureKey, + JigsawPlanarArchetype archetype, + boolean enabled + ) throws IOException { + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + JigsawPlanarArchetype target = Objects.requireNonNull(archetype, "Planar Jigsaw Studio archetype"); + ManifestSnapshot snapshot = loadManifest(root, structureKey); + return updateOwnedStructure( + root, + structureKey, + snapshot, + (content, structurePath) -> updateWorkcell( + content, + target, + null, + enabled, + null, + structurePath)); + } + + public static StructureWriteResult updateWorkcellDimensions( + Path packRoot, + String structureKey, + JigsawPlanarArchetype archetype, + JigsawStudioCellDimensions dimensions + ) throws IOException { + return JigsawStudioGraphEditor.updatePlanarWorkcellCapacity( + packRoot, + structureKey, + archetype, + dimensions).writeResult(); + } + + public static StructureWriteResult updateWorkcellDisplayName( + Path packRoot, + String structureKey, + JigsawPlanarArchetype archetype, + String displayName + ) throws IOException { + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + JigsawPlanarArchetype target = Objects.requireNonNull(archetype, "Planar Jigsaw Studio archetype"); + String normalizedName = JigsawStudioGraphEditor.normalizeDisplayName(displayName); + ManifestSnapshot snapshot = loadManifest(root, structureKey); + return updateOwnedStructure( + root, + structureKey, + snapshot, + (content, structurePath) -> updateWorkcell( + content, + target, + null, + null, + normalizedName, + structurePath)); + } + + public static StructureWriteResult updateSpatialWorkcellDisplayName( + Path packRoot, + String structureKey, + String displayName + ) throws IOException { + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root") + .toAbsolutePath().normalize(); + String normalizedName = JigsawStudioGraphEditor.normalizeDisplayName(displayName); + ManifestSnapshot snapshot = loadManifest(root, structureKey); + return updateOwnedStructure( + root, + structureKey, + snapshot, + (content, structurePath) -> updateSpatialWorkcellDisplayName( + content, + normalizedName, + structurePath)); + } + + private static ManifestSnapshot loadManifest(Path root, String structureKey) throws IOException { + StructureKey ownershipKey = StructureKey.parse(structureKey, "iris"); + StructureTransactionWriter writer = new StructureTransactionWriter(root); + Path manifestPath = writer.ownershipManifestPath(ownershipKey); + if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("This graph is read-only because it is not Studio-owned; create a new Jigsaw Studio project before editing rules"); + } + byte[] manifestContent = Files.readAllBytes(manifestPath); + return new ManifestSnapshot( + JigsawStudioAuthoringAccess.requireEditable( + StructureOwnershipManifest.fromJson(manifestContent)), + StructureHash.sha256(manifestContent)); + } + + private static StructureWriteResult updateOwnedStructure( + Path root, + String structureKey, + ManifestSnapshot snapshot, + StructureContentEditor editor + ) throws IOException { + StructureTransactionWriter writer = new StructureTransactionWriter(root); + StructureOwnershipManifest manifest = snapshot.manifest(); + String normalizedStructure = JigsawStudioProjectCreator.Options.requireResourceKey(structureKey); + String targetResource = "structures/" + normalizedStructure + ".json"; + if (!manifest.resourceHashes().containsKey(targetResource)) { + throw new IOException("The owned graph manifest does not include " + targetResource); + } + + StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(manifest.structure()) + .source(manifest.source()) + .backend(manifest.backend()) + .capabilities(manifest.capabilities()) + .losses(manifest.losses()); + for (Map.Entry resource : manifest.resourceHashes().entrySet()) { + Path resourcePath = resolveOwnedResource(root, resource.getKey()); + byte[] content = Files.readAllBytes(resourcePath); + if (resource.getKey().equals(targetResource)) { + content = editor.edit(content, resourcePath); + } + bundle.resource(resource.getKey(), content); + } + StructureResourceBundle updatedBundle = bundle.build(); + StructureResourceBundleGraphCompiler.requireViable(updatedBundle); + StructureWriteResult result = writer.write( + updatedBundle, + StructureWriteOptions.overwriteExpected(snapshot.expectedManifestHash())); + if (!result.successful()) { + String conflict = result.conflicts().isEmpty() + ? result.status().name() + : result.conflicts().getFirst().relativePath() + ": " + + result.conflicts().getFirst().reason(); + throw new IOException("Atomic graph structure update was rejected: " + conflict); + } + return result; + } + + private static Path resolveOwnedResource(Path root, String relativePath) throws IOException { + StructureResourceBundle.validateRelativePath(relativePath); + Path resource = root.resolve(relativePath).normalize(); + if (!resource.startsWith(root) || !Files.isRegularFile(resource, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Owned graph resource is missing or unsafe: " + relativePath); + } + return resource; + } + + private static void requireOwnedObjectsFit( + Path root, + StructureOwnershipManifest manifest, + JigsawStudioCellDimensions dimensions + ) throws IOException { + for (String relativePath : manifest.resourceHashes().keySet()) { + if (!relativePath.startsWith("objects/") || !relativePath.endsWith(".iob")) { + continue; + } + Path objectPath = resolveOwnedResource(root, relativePath); + IrisBlockVector size = IrisObject.sampleSize(objectPath.toFile()); + if (size.getBlockX() > dimensions.width() + || size.getBlockY() > dimensions.height() + || size.getBlockZ() > dimensions.depth()) { + throw new IOException("Owned object '" + relativePath + "' is " + + size.getBlockX() + "x" + size.getBlockY() + "x" + size.getBlockZ() + + " and does not fit the requested cell bounds"); + } + } + } + + private static byte[] updateCellSize( + byte[] content, + JigsawStudioCellDimensions dimensions, + Path structurePath + ) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw structure is not a JSON object: " + structurePath); + } + JsonObject cellSize = new JsonObject(); + JsonObject structure = parsed.getAsJsonObject(); + if (structure.has("mode") + && "PLANAR_JIGSAW".equals(structure.get("mode").getAsString()) + && dimensions.width() != dimensions.depth()) { + throw new IOException("Planar Jigsaw Studio cells require equal width and depth"); + } + cellSize.addProperty("x", dimensions.width()); + cellSize.addProperty("y", dimensions.height()); + cellSize.addProperty("z", dimensions.depth()); + structure.add("cellSize", cellSize); + return (GSON.toJson(structure) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private static byte[] updateLimits( + byte[] content, + int maxDepth, + int maxSizeChunks, + Path structurePath + ) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw structure is not a JSON object: " + structurePath); + } + JsonObject structure = parsed.getAsJsonObject(); + structure.addProperty("maxDepth", maxDepth); + structure.addProperty("maxSizeChunks", maxSizeChunks); + return (GSON.toJson(structure) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private static List normalizeThemeSets(List themeSets) { + Objects.requireNonNull(themeSets, "Jigsaw Studio theme sets"); + List normalized = new ArrayList<>(themeSets.size()); + Set keys = new LinkedHashSet<>(); + for (IrisJigsawThemeSet themeSet : themeSets) { + IrisJigsawThemeSet source = Objects.requireNonNull(themeSet, "Jigsaw Studio theme set"); + String key = source.getKey() == null ? "" : source.getKey().trim(); + if (key.isEmpty() || !key.equals(source.getKey())) { + throw new IllegalArgumentException( + "Jigsaw Studio theme keys must be non-blank and whitespace-normalized"); + } + if (!keys.add(key)) { + throw new IllegalArgumentException("Duplicate Jigsaw Studio theme key '" + key + "'"); + } + if (source.getWeight() < 1) { + throw new IllegalArgumentException("Jigsaw Studio theme weights must be positive"); + } + normalized.add(new IrisJigsawThemeSet(key, source.getWeight())); + } + return List.copyOf(normalized); + } + + private static byte[] updateThemeSets( + byte[] content, + List themeSets, + Path structurePath + ) throws IOException { + JsonObject structure = parseStructure(content, structurePath); + JsonArray values = new JsonArray(); + for (IrisJigsawThemeSet themeSet : themeSets) { + JsonObject value = new JsonObject(); + value.addProperty("key", themeSet.getKey()); + value.addProperty("weight", themeSet.getWeight()); + values.add(value); + } + structure.add("themeSets", values); + return (GSON.toJson(structure) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private static byte[] updateRequireCaps( + byte[] content, + boolean requireCaps, + Path structurePath + ) throws IOException { + JsonObject structure = parseStructure(content, structurePath); + structure.addProperty("requireCaps", requireCaps); + return (GSON.toJson(structure) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private static byte[] updateSpatialWorkcellDisplayName( + byte[] content, + String displayName, + Path structurePath + ) throws IOException { + JsonObject structure = parseStructure(content, structurePath); + IrisStructure model; + try { + model = GSON.fromJson(structure, IrisStructure.class); + } catch (RuntimeException exception) { + throw new IOException("Jigsaw structure is not valid JSON: " + structurePath, exception); + } + if (model == null || model.resolvedMode() != IrisJigsawMode.SPATIAL_JIGSAW) { + throw new IOException("Spatial workcell labels require a spatial Jigsaw Studio structure"); + } + if (displayName.isEmpty()) { + structure.remove("spatialWorkcellDisplayName"); + } else { + structure.addProperty("spatialWorkcellDisplayName", displayName); + } + return (GSON.toJson(structure) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private static JsonObject parseStructure(byte[] content, Path structurePath) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw structure is not a JSON object: " + structurePath); + } + return parsed.getAsJsonObject(); + } + + static byte[] updateWorkcell( + byte[] content, + JigsawPlanarArchetype archetype, + JigsawStudioCellDimensions dimensions, + Boolean enabled, + String displayName, + Path structurePath + ) throws IOException { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Jigsaw structure is not a JSON object: " + structurePath); + } + JsonObject structureJson = parsed.getAsJsonObject(); + IrisStructure structure; + try { + structure = GSON.fromJson(structureJson, IrisStructure.class); + } catch (RuntimeException exception) { + throw new IOException("Jigsaw structure is not valid JSON: " + structurePath, exception); + } + if (structure == null || structure.resolvedMode() != IrisJigsawMode.PLANAR_JIGSAW) { + throw new IOException("Workcell settings require a planar Jigsaw Studio structure"); + } + Map resolved; + try { + resolved = PlanarJigsawWorkcellResolver.resolve(structure); + } catch (IllegalArgumentException exception) { + throw new IOException("Planar workcell configuration is invalid: " + exception.getMessage(), exception); + } + JsonArray workcells = new JsonArray(); + for (JigsawPlanarArchetype current : JigsawPlanarArchetype.values()) { + PlanarJigsawWorkcellResolver.ResolvedWorkcell source = resolved.get(current.modelArchetype()); + JsonObject workcell = new JsonObject(); + workcell.addProperty("archetype", current.name()); + String resolvedDisplayName = current == archetype && displayName != null + ? displayName : source.displayName(); + if (!resolvedDisplayName.isEmpty()) { + workcell.addProperty("displayName", resolvedDisplayName); + } + workcell.addProperty("width", current == archetype && dimensions != null + ? dimensions.width() : source.width()); + workcell.addProperty("height", current == archetype && dimensions != null + ? dimensions.height() : source.height()); + workcell.addProperty("depth", current == archetype && dimensions != null + ? dimensions.depth() : source.depth()); + workcell.addProperty("enabled", current == archetype && enabled != null + ? enabled : source.enabled()); + workcells.add(workcell); + } + structureJson.add("planarWorkcells", workcells); + return (GSON.toJson(structureJson) + "\n").getBytes(StandardCharsets.UTF_8); + } + + @FunctionalInterface + private interface StructureContentEditor { + byte[] edit(byte[] content, Path structurePath) throws IOException; + } + + private record ManifestSnapshot( + StructureOwnershipManifest manifest, + String expectedManifestHash + ) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioToolAction.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioToolAction.java new file mode 100644 index 000000000..a94d84ffc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioToolAction.java @@ -0,0 +1,45 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.Objects; + +public enum JigsawStudioToolAction { + OPEN_MENU("Open Control Menu", false), + SELECT_WORKCELL("Select Workcell", false), + TOGGLE_WORKCELL("Toggle Workcell", false), + LOAD_VARIANT("Load Variant", false), + CREATE_VARIANT("New Blank Variant", false), + DUPLICATE_VARIANT("Duplicate This Cell's Variant", false), + DUPLICATE_FAMILY("Duplicate All Enabled Cells as Family", false), + PREVIEW_GRAPH("Go to Preview", false), + FLUSH_AUTOSAVE("Flush Autosave", false), + TOGGLE_ROTATION("Toggle Rotation", false), + EXPAND_TO_CELL("Resize Variant to Capacity", false), + RESIZE_VARIANT("Resize This Variant", false), + RESIZE_WORKCELL("Resize Workcell Capacity", false), + RENAME_VARIANT("Rename This Variant", false), + RENAME_WORKCELL("Rename This Workcell", false), + ADJUST_VARIANT_WEIGHT("Adjust Variant Weight", false), + ADJUST_VARIANT_CHANCE("Adjust Variant Chance", false), + SET_THEME("Set Theme", false), + SET_PIECE_RULES("Set Piece Rules", false), + TOGGLE_REQUIRE_CAPS("Toggle Required Caps", false), + UNLINK_MEMBERSHIP("Unlink Pool Entry", true), + DELETE_VARIANT("Delete Variant", true), + DELETE_PROJECT("Delete Project", true); + + private final String displayName; + private final boolean destructive; + + JigsawStudioToolAction(String displayName, boolean destructive) { + this.displayName = Objects.requireNonNull(displayName, "Jigsaw Studio tool display name"); + this.destructive = destructive; + } + + public String displayName() { + return displayName; + } + + public boolean destructive() { + return destructive; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioToolPayload.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioToolPayload.java new file mode 100644 index 000000000..9c5272d2a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioToolPayload.java @@ -0,0 +1,111 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.Objects; +import java.util.UUID; + +public record JigsawStudioToolPayload( + int schemaVersion, + JigsawStudioToolAction action, + UUID requestId, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex, + int amount +) { + public static final int CURRENT_SCHEMA_VERSION = 2; + public static final int NO_ENTRY_INDEX = -1; + + private static final int MAX_FIELD_LENGTH = 512; + + public JigsawStudioToolPayload { + if (schemaVersion < 1) { + throw new IllegalArgumentException("Jigsaw Studio tool schema version must be positive"); + } + action = Objects.requireNonNull(action, "Jigsaw Studio tool action"); + requestId = Objects.requireNonNull(requestId, "Jigsaw Studio tool request ID"); + workcellId = normalize(workcellId, "workcell ID"); + pieceKey = normalize(pieceKey, "piece key"); + poolKey = normalize(poolKey, "pool key"); + if (entryIndex < NO_ENTRY_INDEX) { + throw new IllegalArgumentException("Jigsaw Studio tool entry index cannot be lower than -1"); + } + } + + public static JigsawStudioToolPayload request( + JigsawStudioToolAction action, + UUID requestId + ) { + return new JigsawStudioToolPayload( + CURRENT_SCHEMA_VERSION, + action, + requestId, + "", + "", + "", + NO_ENTRY_INDEX, + 0); + } + + public static JigsawStudioToolPayload workcell( + JigsawStudioToolAction action, + UUID requestId, + String workcellId + ) { + return new JigsawStudioToolPayload( + CURRENT_SCHEMA_VERSION, + action, + requestId, + workcellId, + "", + "", + NO_ENTRY_INDEX, + 0); + } + + public static JigsawStudioToolPayload variant( + JigsawStudioToolAction action, + UUID requestId, + String workcellId, + String pieceKey + ) { + return new JigsawStudioToolPayload( + CURRENT_SCHEMA_VERSION, + action, + requestId, + workcellId, + pieceKey, + "", + NO_ENTRY_INDEX, + 0); + } + + public static JigsawStudioToolPayload membership( + JigsawStudioToolAction action, + UUID requestId, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex, + int amount + ) { + return new JigsawStudioToolPayload( + CURRENT_SCHEMA_VERSION, + action, + requestId, + workcellId, + pieceKey, + poolKey, + entryIndex, + amount); + } + + private static String normalize(String value, String fieldName) { + String normalized = value == null ? "" : value.trim(); + if (normalized.length() > MAX_FIELD_LENGTH) { + throw new IllegalArgumentException("Jigsaw Studio tool " + fieldName + + " cannot exceed " + MAX_FIELD_LENGTH + " characters"); + } + return normalized; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioTripleSneakTracker.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioTripleSneakTracker.java new file mode 100644 index 000000000..8c300a720 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioTripleSneakTracker.java @@ -0,0 +1,112 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +public final class JigsawStudioTripleSneakTracker { + public static final long DEFAULT_WINDOW_NANOS = 1_500_000_000L; + + private static final int REQUIRED_SNEAKS = 3; + + private final long windowNanos; + private final Map gestures = new HashMap<>(); + + public JigsawStudioTripleSneakTracker() { + this(DEFAULT_WINDOW_NANOS); + } + + public JigsawStudioTripleSneakTracker(long windowNanos) { + if (windowNanos <= 0L) { + throw new IllegalArgumentException("Jigsaw Studio triple-sneak window must be positive"); + } + this.windowNanos = windowNanos; + } + + public synchronized Progress recordSneak( + UUID playerId, + UUID worldId, + UUID requestId, + long nowNanos + ) { + UUID player = Objects.requireNonNull(playerId, "Jigsaw Studio gesture player ID"); + UUID world = Objects.requireNonNull(worldId, "Jigsaw Studio gesture world ID"); + UUID request = Objects.requireNonNull(requestId, "Jigsaw Studio gesture request ID"); + GestureState previous = gestures.get(player); + if (previous == null + || !previous.worldId().equals(world) + || !previous.requestId().equals(request) + || expired(previous, nowNanos)) { + gestures.put(player, new GestureState(world, request, nowNanos, nowNanos, 1)); + return Progress.FIRST; + } + + int count = previous.count() + 1; + if (count >= REQUIRED_SNEAKS) { + gestures.remove(player); + return Progress.TRIGGERED; + } + gestures.put(player, new GestureState( + world, + request, + previous.startedAtNanos(), + nowNanos, + count)); + return Progress.SECOND; + } + + public synchronized void clearPlayer(UUID playerId) { + if (playerId != null) { + gestures.remove(playerId); + } + } + + public synchronized int clearRequest(UUID requestId) { + if (requestId == null) { + return 0; + } + int removed = 0; + Iterator states = gestures.values().iterator(); + while (states.hasNext()) { + GestureState state = states.next(); + if (state.requestId().equals(requestId)) { + states.remove(); + removed++; + } + } + return removed; + } + + public synchronized void clearAll() { + gestures.clear(); + } + + public synchronized int trackedPlayers() { + return gestures.size(); + } + + private boolean expired(GestureState state, long nowNanos) { + long elapsedSinceStart = nowNanos - state.startedAtNanos(); + long elapsedSinceLast = nowNanos - state.lastSneakAtNanos(); + return elapsedSinceStart < 0L + || elapsedSinceLast < 0L + || elapsedSinceStart > windowNanos; + } + + public enum Progress { + FIRST, + SECOND, + TRIGGERED + } + + private record GestureState( + UUID worldId, + UUID requestId, + long startedAtNanos, + long lastSneakAtNanos, + int count + ) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioVariant.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioVariant.java new file mode 100644 index 000000000..2130d21ea --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioVariant.java @@ -0,0 +1,141 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import art.arcane.iris.engine.object.IrisPosition; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public record JigsawStudioVariant( + String pieceKey, + String objectKey, + String displayName, + Optional dimensions, + JigsawStudioMode mode, + Optional sourceTopology, + boolean rotatable, + boolean owned, + List themes, + JigsawStudioPieceRules rules, + List memberships +) { + public JigsawStudioVariant { + pieceKey = requireKey(pieceKey, "piece"); + objectKey = requireKey(objectKey, "object"); + displayName = displayName == null ? "" : displayName.trim(); + dimensions = Objects.requireNonNull(dimensions, "Jigsaw Studio variant dimensions"); + mode = Objects.requireNonNull(mode, "Jigsaw Studio variant mode"); + sourceTopology = Objects.requireNonNull(sourceTopology, "Jigsaw Studio variant topology"); + themes = List.copyOf(Objects.requireNonNull(themes, "Jigsaw Studio variant themes")); + rules = Objects.requireNonNull(rules, "Jigsaw Studio variant rules"); + memberships = List.copyOf(Objects.requireNonNull(memberships, "Jigsaw Studio pool memberships")); + if (mode == JigsawStudioMode.PLANAR_JIGSAW && sourceTopology.isEmpty()) { + throw new IllegalArgumentException("Planar Jigsaw Studio variants require a topology"); + } + if (mode == JigsawStudioMode.SPATIAL_JIGSAW && sourceTopology.isPresent()) { + throw new IllegalArgumentException("Spatial Jigsaw Studio variants cannot declare a planar topology"); + } + } + + public String resolvedDisplayName() { + if (!displayName.isEmpty()) { + return displayName; + } + int separator = Math.max(pieceKey.lastIndexOf('/'), pieceKey.lastIndexOf(':')); + return separator < 0 ? pieceKey : pieceKey.substring(separator + 1); + } + + public Optional archetype() { + return sourceTopology.map(JigsawPlanarArchetype::fromTopology); + } + + public int sourceToCanonicalQuarterTurns() { + JigsawPlanarTopology topology = sourceTopology.orElse(null); + return topology == null ? 0 : JigsawPlanarArchetype.fromTopology(topology) + .sourceToCanonicalQuarterTurns(topology); + } + + public int canonicalToSourceQuarterTurns() { + JigsawPlanarTopology topology = sourceTopology.orElse(null); + return topology == null ? 0 : JigsawPlanarArchetype.fromTopology(topology) + .canonicalToSourceQuarterTurns(topology); + } + + public IrisPosition sourceToCanonicalPosition( + IrisPosition sourcePosition, + JigsawStudioCellDimensions sourceDimensions + ) { + return rotatePosition( + sourcePosition, + sourceDimensions, + sourceToCanonicalQuarterTurns()); + } + + public IrisPosition canonicalToSourcePosition( + IrisPosition canonicalPosition, + JigsawStudioCellDimensions sourceDimensions + ) { + JigsawStudioCellDimensions canonicalDimensions = canonicalDimensions(sourceDimensions); + return rotatePosition( + canonicalPosition, + canonicalDimensions, + canonicalToSourceQuarterTurns()); + } + + public JigsawStudioCellDimensions canonicalDimensions(JigsawStudioCellDimensions sourceDimensions) { + JigsawStudioCellDimensions dimensions = Objects.requireNonNull( + sourceDimensions, + "Jigsaw Studio source dimensions"); + return Math.floorMod(sourceToCanonicalQuarterTurns(), 2) == 0 + ? dimensions + : new JigsawStudioCellDimensions( + dimensions.depth(), + dimensions.height(), + dimensions.width()); + } + + public boolean assigned() { + return !memberships.isEmpty(); + } + + private static IrisPosition rotatePosition( + IrisPosition position, + JigsawStudioCellDimensions dimensions, + int quarterTurns + ) { + IrisPosition source = Objects.requireNonNull(position, "Jigsaw Studio variant position"); + JigsawStudioCellDimensions bounds = Objects.requireNonNull( + dimensions, + "Jigsaw Studio variant position bounds"); + if (source.getX() < 0 || source.getX() >= bounds.width() + || source.getY() < 0 || source.getY() >= bounds.height() + || source.getZ() < 0 || source.getZ() >= bounds.depth()) { + throw new IllegalArgumentException("Jigsaw Studio variant position is outside its object bounds"); + } + return switch (Math.floorMod(quarterTurns, 4)) { + case 0 -> new IrisPosition(source.getX(), source.getY(), source.getZ()); + case 1 -> new IrisPosition( + bounds.depth() - 1 - source.getZ(), + source.getY(), + source.getX()); + case 2 -> new IrisPosition( + bounds.width() - 1 - source.getX(), + source.getY(), + bounds.depth() - 1 - source.getZ()); + case 3 -> new IrisPosition( + source.getZ(), + source.getY(), + bounds.width() - 1 - source.getX()); + default -> throw new IllegalStateException("Unreachable Jigsaw Studio rotation"); + }; + } + + private static String requireKey(String value, String name) { + Objects.requireNonNull(value, "Jigsaw Studio " + name + " key"); + String normalized = value.trim(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio " + name + " key cannot be blank"); + } + return normalized; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioVariantCatalog.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioVariantCatalog.java new file mode 100644 index 000000000..f25979904 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioVariantCatalog.java @@ -0,0 +1,95 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public final class JigsawStudioVariantCatalog { + private final List variants; + private final Map byPieceKey; + private final Map> byArchetype; + private final List spatialVariants; + private final boolean editableGraph; + + public JigsawStudioVariantCatalog(List variants) { + this(variants, true); + } + + public JigsawStudioVariantCatalog( + List variants, + boolean editableGraph + ) { + Objects.requireNonNull(variants, "Jigsaw Studio variants"); + if (variants.size() > JigsawStudioLayout.MAX_VARIANTS) { + throw new IllegalArgumentException("Jigsaw Studio catalogs cannot exceed " + + JigsawStudioLayout.MAX_VARIANTS + " variants"); + } + List copied = List.copyOf(variants); + Map pieceIndex = new LinkedHashMap<>(); + Map> archetypeIndex = + new EnumMap<>(JigsawPlanarArchetype.class); + List spatial = new ArrayList<>(); + for (JigsawStudioVariant variant : copied) { + JigsawStudioVariant activeVariant = Objects.requireNonNull(variant, "Jigsaw Studio variant"); + JigsawStudioVariant previous = pieceIndex.putIfAbsent(activeVariant.pieceKey(), activeVariant); + if (previous != null) { + throw new IllegalArgumentException("Duplicate Jigsaw Studio variant piece key " + + activeVariant.pieceKey()); + } + Optional archetype = activeVariant.archetype(); + if (archetype.isPresent()) { + archetypeIndex.computeIfAbsent(archetype.get(), key -> new ArrayList<>()).add(activeVariant); + } else { + spatial.add(activeVariant); + } + } + Map> immutableArchetypes = + new EnumMap<>(JigsawPlanarArchetype.class); + for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) { + immutableArchetypes.put( + archetype, + List.copyOf(archetypeIndex.getOrDefault(archetype, List.of()))); + } + this.variants = copied; + this.byPieceKey = Collections.unmodifiableMap(pieceIndex); + this.byArchetype = Collections.unmodifiableMap(immutableArchetypes); + this.spatialVariants = List.copyOf(spatial); + this.editableGraph = editableGraph; + } + + public static JigsawStudioVariantCatalog empty() { + return new JigsawStudioVariantCatalog(List.of()); + } + + public List variants() { + return variants; + } + + public Optional find(String pieceKey) { + if (pieceKey == null) { + return Optional.empty(); + } + return Optional.ofNullable(byPieceKey.get(pieceKey)); + } + + public List variants(JigsawPlanarArchetype archetype) { + return byArchetype.get(Objects.requireNonNull(archetype, "Planar archetype")); + } + + public List spatialVariants() { + return spatialVariants; + } + + public boolean editableGraph() { + return editableGraph; + } + + public int size() { + return variants.size(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioWorkcellSpec.java b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioWorkcellSpec.java new file mode 100644 index 000000000..fe0b7f933 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/runtime/jigsaw/JigsawStudioWorkcellSpec.java @@ -0,0 +1,20 @@ +package art.arcane.iris.core.runtime.jigsaw; + +import java.util.Objects; + +public record JigsawStudioWorkcellSpec( + JigsawPlanarArchetype archetype, + String displayName, + JigsawStudioCellDimensions dimensions, + boolean enabled +) { + public JigsawStudioWorkcellSpec { + archetype = Objects.requireNonNull(archetype, "Jigsaw Studio workcell archetype"); + displayName = displayName == null ? "" : displayName.trim(); + dimensions = Objects.requireNonNull(dimensions, "Jigsaw Studio workcell dimensions"); + } + + public String resolvedDisplayName() { + return displayName.isEmpty() ? archetype.displayName() : displayName; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java b/core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java new file mode 100644 index 000000000..92232b937 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java @@ -0,0 +1,117 @@ +package art.arcane.iris.core.safeguard; + +import art.arcane.iris.core.safeguard.task.Diagnostic; +import art.arcane.iris.core.safeguard.task.Task; +import art.arcane.iris.core.safeguard.task.Tasks; +import art.arcane.iris.core.safeguard.task.ValueWithDiagnostics; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.format.C; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class IrisSafeguard { + private static Map> results = Collections.emptyMap(); + private static Map context = Collections.emptyMap(); + private static Map> attachment = Collections.emptyMap(); + private static Mode mode = Mode.STABLE; + private static int count = 0; + + private IrisSafeguard() { + } + + public static void execute() { + List tasks = Tasks.getTasks(); + LinkedHashMap> resultValues = new LinkedHashMap<>(tasks.size()); + LinkedHashMap contextValues = new LinkedHashMap<>(tasks.size()); + LinkedHashMap> attachmentValues = new LinkedHashMap<>(tasks.size()); + Mode currentMode = Mode.STABLE; + int issueCount = 0; + + for (Task task : tasks) { + ValueWithDiagnostics result; + try { + result = task.run(); + } catch (Throwable e) { + IrisLogging.reportError(e); + result = new ValueWithDiagnostics<>( + Mode.WARNING, + new Diagnostic(Diagnostic.Logger.ERROR, "Error while running task " + task.getId(), e) + ); + } + + currentMode = currentMode.highest(result.getValue()); + resultValues.put(task, result); + contextValues.put(task.getId(), result.getValue().getId()); + + List lines = new ArrayList<>(); + for (Diagnostic diagnostic : result.getDiagnostics()) { + String[] split = diagnostic.toString().split("\\n"); + Collections.addAll(lines, split); + } + attachmentValues.put(task.getId(), lines); + + if (result.getValue() != Mode.STABLE) { + issueCount++; + } + } + + results = Collections.unmodifiableMap(resultValues); + context = Collections.unmodifiableMap(contextValues); + attachment = Collections.unmodifiableMap(attachmentValues); + mode = currentMode; + count = issueCount; + } + + public static Mode mode() { + return mode; + } + + public static Map asContext() { + return context; + } + + public static Map> asAttachment() { + return attachment; + } + + public static void printReports() { + switch (mode) { + case STABLE -> IrisLogging.info(C.BLUE + "0 Conflicts found"); + case WARNING -> IrisLogging.warn(C.GOLD + "%s Issues found", count); + case UNSTABLE -> IrisLogging.error(C.DARK_RED + "%s Issues found", count); + } + + for (ValueWithDiagnostics value : results.values()) { + // Without the stack trace: Diagnostic.Logger splits on newlines, so a trace became one log + // record per frame at the diagnostic's own severity. Traces go through reportError. + value.log(true, false); + } + } + + public static void printFooter() { + switch (mode) { + case STABLE -> IrisLogging.info(C.BLUE + "Iris is running Stable"); + case WARNING -> warning(); + case UNSTABLE -> unstable(); + } + } + + // A log record carries a level, so a blank record renders as an empty [WARN] line and a rule of + // dashes renders as a [SEVERE] one. Spacing belongs to a console, not to the server log. + private static void warning() { + IrisLogging.warn(C.GOLD + "Iris is running in Warning Mode"); + IrisLogging.warn(C.GRAY + "Some startup checks need attention. Review the messages above for tuning suggestions."); + IrisLogging.warn(C.GRAY + "Iris will continue startup normally."); + } + + private static void unstable() { + IrisLogging.error(C.DARK_RED + "Iris is running in Danger Mode"); + IrisLogging.error("Critical startup checks failed. Review and resolve the errors above as soon as possible."); + // No startup sleep: blocking the boot thread protected nothing — world creation and + // player admission are already gated by IrisStartupValidation. + } +} diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java b/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java new file mode 100644 index 000000000..5ff90ec79 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java @@ -0,0 +1,116 @@ +package art.arcane.iris.core.safeguard; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.splash.IrisSplashComposer; +import art.arcane.iris.core.splash.IrisSplashRenderer; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.format.C; +import org.bukkit.Bukkit; + +import java.util.Locale; + +public enum Mode { + STABLE(C.IRIS, C.AQUA), + WARNING(C.GOLD, C.YELLOW), + UNSTABLE(C.RED, C.GOLD); + + private final C color; + private final C glow; + private final String id; + + Mode(C color, C glow) { + this.color = color; + this.glow = glow; + this.id = name().toLowerCase(Locale.ROOT); + } + + public String getId() { + return id; + } + + public Mode highest(Mode mode) { + if (mode.ordinal() > ordinal()) { + return mode; + } + return this; + } + + public String tag(String subTag) { + if (subTag == null || subTag.isBlank()) { + return wrap("Iris") + C.GRAY + ": "; + } + return wrap("Iris") + " " + wrap(subTag) + C.GRAY + ": "; + } + + public void trySplash() { + if (!IrisSettings.get().getGeneral().isSplashLogoStartup()) { + return; + } + splash(); + } + + public void splash() { + String version = BukkitPlatform.plugin().getDescription().getVersion(); + String[] splash = IrisSplashRenderer.render(this::splashTone); + String[] info = IrisSplashComposer.composeInfo(version, getServerVersion(), infoStyle()); + IrisLogging.info(IrisSplashComposer.compose(splash, info)); + } + + private IrisSplashComposer.InfoStyle infoStyle() { + return new IrisSplashComposer.InfoStyle() { + @Override + public String linePrefix() { + return " ".repeat(4); + } + + @Override + public String title(String text) { + return color + text; + } + + @Override + public String subtitle(String text) { + return C.AQUA + text; + } + + @Override + public String tag(String text) { + return C.RED + text; + } + + @Override + public String label(String text) { + return C.GRAY + text; + } + + @Override + public String value(String text) { + return color + text; + } + }; + } + + private String splashTone(char glyph) { + return switch (glyph) { + case '=' -> C.GRAY.toString(); + case '*' -> glow.toString(); + case '%', '@' -> color.toString(); + case '(', ')' -> C.WHITE.toString(); + default -> C.DARK_GRAY.toString(); + }; + } + + private String wrap(String tag) { + return C.BOLD.toString() + C.DARK_GRAY + "[" + C.BOLD + color + tag + C.BOLD + C.DARK_GRAY + "]" + C.RESET; + } + + private String getServerVersion() { + String version = Bukkit.getVersion(); + int marker = version.indexOf(" (MC:"); + if (marker != -1) { + return version.substring(0, marker); + } + return version; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/task/Diagnostic.java b/core/src/main/java/art/arcane/iris/core/safeguard/task/Diagnostic.java new file mode 100644 index 000000000..7b69cef02 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/safeguard/task/Diagnostic.java @@ -0,0 +1,112 @@ +package art.arcane.iris.core.safeguard.task; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.format.C; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.function.Consumer; + +public class Diagnostic { + private final Logger logger; + private final String message; + private final Throwable exception; + + public Diagnostic(String message) { + this(Logger.ERROR, message, null); + } + + public Diagnostic(Logger logger, String message) { + this(logger, message, null); + } + + public Diagnostic(Logger logger, String message, Throwable exception) { + this.logger = logger; + this.message = message; + this.exception = exception; + } + + public Logger getLogger() { + return logger; + } + + public String getMessage() { + return message; + } + + public Throwable getException() { + return exception; + } + + public void log() { + log(true, false); + } + + public void log(boolean withException) { + log(withException, false); + } + + public void log(boolean withException, boolean withStackTrace) { + logger.print(render(withException, withStackTrace)); + } + + public String render() { + return render(true, false); + } + + public String render(boolean withException) { + return render(withException, false); + } + + public String render(boolean withException, boolean withStackTrace) { + StringBuilder builder = new StringBuilder(); + builder.append(message); + if (withException && exception != null) { + builder.append(": "); + builder.append(exception); + if (withStackTrace) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(os); + exception.printStackTrace(ps); + ps.flush(); + builder.append("\n"); + builder.append(os); + } + } + return builder.toString(); + } + + @Override + public String toString() { + return C.strip(render()); + } + + public enum Logger { + DEBUG(IrisLogging::debug), + RAW(IrisLogging::msg), + INFO(IrisLogging::info), + WARN(IrisLogging::warn), + ERROR(IrisLogging::error); + + private final Consumer logger; + + Logger(Consumer logger) { + this.logger = logger; + } + + public void print(String message) { + String[] lines = message.split("\\n"); + for (String line : lines) { + logger.accept(line); + } + } + + public Diagnostic create(String message) { + return create(message, null); + } + + public Diagnostic create(String message, Throwable exception) { + return new Diagnostic(this, message, exception); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/task/Task.java b/core/src/main/java/art/arcane/iris/core/safeguard/task/Task.java new file mode 100644 index 000000000..adfc428d3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/safeguard/task/Task.java @@ -0,0 +1,49 @@ +package art.arcane.iris.core.safeguard.task; + +import art.arcane.iris.core.safeguard.Mode; +import art.arcane.volmlib.util.format.Form; + +import java.util.Locale; +import java.util.function.Supplier; + +public abstract class Task { + private final String id; + private final String name; + + public Task(String id) { + this(id, Form.capitalizeWords(id.replace(" ", "_").toLowerCase(Locale.ROOT))); + } + + public Task(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public abstract ValueWithDiagnostics run(); + + public static Task of(String id, String name, Supplier> action) { + return new Task(id, name) { + @Override + public ValueWithDiagnostics run() { + return action.get(); + } + }; + } + + public static Task of(String id, Supplier> action) { + return new Task(id) { + @Override + public ValueWithDiagnostics run() { + return action.get(); + } + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java b/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java new file mode 100644 index 000000000..1e45cb74a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java @@ -0,0 +1,229 @@ +package art.arcane.iris.core.safeguard.task; + +import art.arcane.iris.BuildConstants; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.IrisWorlds; +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.v1X.NMSBinding1X; +import art.arcane.iris.core.safeguard.Mode; +import art.arcane.iris.core.splash.IrisSplashComposer; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.util.common.misc.getHardware; +import art.arcane.iris.util.project.agent.Agent; +import org.bukkit.Bukkit; +import org.bukkit.Server; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +public final class Tasks { + private static final Task MEMORY = Task.of("memory", () -> { + long mem = getHardware.getProcessMemory(); + if (mem >= 3072L) { + return withDiagnostics(Mode.STABLE); + } + + if (mem > 2048L) { + return withDiagnostics(Mode.STABLE, + Diagnostic.Logger.INFO.create("Memory Recommendation"), + Diagnostic.Logger.INFO.create("- 3GB+ process memory is recommended for Iris."), + Diagnostic.Logger.INFO.create("- Process Memory: " + mem + " MB")); + } + + return withDiagnostics(Mode.WARNING, + Diagnostic.Logger.WARN.create("Low Memory"), + Diagnostic.Logger.WARN.create("- Iris is running with 2GB or less process memory."), + Diagnostic.Logger.WARN.create("- 3GB+ process memory is recommended for Iris."), + Diagnostic.Logger.WARN.create("- Process Memory: " + mem + " MB")); + }); + + private static final Task INCOMPATIBILITIES = Task.of("incompatibilities", () -> { + Set plugins = new HashSet<>(Set.of("dynmap", "Stratos")); + plugins.removeIf(name -> server().getPluginManager().getPlugin(name) == null); + + if (plugins.isEmpty()) { + return withDiagnostics(Mode.STABLE); + } + + List diagnostics = new ArrayList<>(); + if (plugins.contains("dynmap")) { + addAllDiagnostics(diagnostics, + Diagnostic.Logger.ERROR.create("Dynmap"), + Diagnostic.Logger.ERROR.create("- The plugin Dynmap is not compatible with the server."), + Diagnostic.Logger.ERROR.create("- If you want to have a map plugin like Dynmap, consider Bluemap.")); + } + if (plugins.contains("Stratos")) { + addAllDiagnostics(diagnostics, + Diagnostic.Logger.ERROR.create("Stratos"), + Diagnostic.Logger.ERROR.create("- Iris is not compatible with other worldgen plugins.")); + } + return withDiagnostics(Mode.WARNING, diagnostics); + }); + + private static final Task SOFTWARE = Task.of("software", () -> { + Set supported = Set.of("canvas", "folia", "purpur", "pufferfish", "leaf", "paper", "spigot", "bukkit"); + String serverName = server().getName().toLowerCase(Locale.ROOT); + boolean supportedServer = isCanvasServer(); + if (!supportedServer) { + for (String candidate : supported) { + if (serverName.contains(candidate)) { + supportedServer = true; + break; + } + } + } + + if (supportedServer) { + return withDiagnostics(Mode.STABLE); + } + + return withDiagnostics(Mode.WARNING, + Diagnostic.Logger.WARN.create("Unsupported Server Software"), + Diagnostic.Logger.WARN.create("- Please consider using Canvas, Folia, Leaf, Paper, or Purpur instead.")); + }); + + private static final Task VERSION = Task.of("version", () -> { + String[] parts = BukkitPlatform.plugin().getDescription().getVersion().split("-"); + String supportedVersions; + if (parts.length >= 3) { + String minVersion = parts[1]; + String maxVersion = parts[2]; + supportedVersions = minVersion.equals(maxVersion) ? minVersion : minVersion + " - " + maxVersion; + } else if (parts.length >= 2) { + supportedVersions = parts[1]; + } else { + supportedVersions = BuildConstants.MINECRAFT_VERSION; + } + + if (!INMS.isBound()) { + String cause = INMS.bindFailure() == null ? "Unknown NMS bind failure" : INMS.bindFailure().getMessage(); + return withDiagnostics(Mode.UNSTABLE, + Diagnostic.Logger.ERROR.create("Server Version"), + Diagnostic.Logger.ERROR.create("- " + cause), + Diagnostic.Logger.ERROR.create("- Iris only supports " + supportedVersions)); + } + + if (!(INMS.get() instanceof NMSBinding1X)) { + return withDiagnostics(Mode.STABLE); + } + + return withDiagnostics(Mode.UNSTABLE, + Diagnostic.Logger.ERROR.create("NMS Disabled"), + Diagnostic.Logger.ERROR.create("- NMS support is disabled (general.disableNMS); Iris world creation is unavailable.")); + }); + + private static final Task INJECTION = Task.of("injection", () -> { + if (!Agent.install()) { + return withDiagnostics(Mode.UNSTABLE, + Diagnostic.Logger.ERROR.create("Java Agent"), + Diagnostic.Logger.ERROR.create("- Please enable dynamic agent loading by adding -XX:+EnableDynamicAgentLoading to your jvm arguments."), + Diagnostic.Logger.ERROR.create("- or add the jvm argument -javaagent:" + Agent.AGENT_JAR.getPath())); + } + + if (!INMS.get().injectBukkit()) { + return withDiagnostics(Mode.UNSTABLE, + Diagnostic.Logger.ERROR.create("Code Injection"), + Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support")); + } + + return withDiagnostics(Mode.STABLE); + }); + + private static final Task DIMENSION_TYPES = Task.of("dimensionTypes", () -> { + Set keys = IrisWorlds.get().getDimensions().map(IrisDimension::getDimensionTypeKey).collect(Collectors.toSet()); + if (!INMS.get().missingDimensionTypes(keys.toArray(String[]::new))) { + return withDiagnostics(Mode.STABLE); + } + + return withDiagnostics(Mode.UNSTABLE, + Diagnostic.Logger.ERROR.create("Dimension Types"), + Diagnostic.Logger.ERROR.create("- Required Iris dimension types were not loaded."), + Diagnostic.Logger.ERROR.create("- If this still happens after a restart please contact support.")); + }); + + private static final Task DISK_SPACE = Task.of("diskSpace", () -> { + double freeGiB = IrisWorldStorage.levelRoot().getFreeSpace() / (double) 0x4000_0000; + if (freeGiB > 3.0) { + return withDiagnostics(Mode.STABLE); + } + + return withDiagnostics(Mode.WARNING, + Diagnostic.Logger.WARN.create("Insufficient Disk Space"), + Diagnostic.Logger.WARN.create("- 3GB of free space is required for Iris to function.")); + }); + + private static final Task JAVA = Task.of("java", () -> { + int version = IrisSplashComposer.javaVersion(); + if (version < 0) { + return withDiagnostics(Mode.WARNING, + Diagnostic.Logger.WARN.create("Java Runtime"), + Diagnostic.Logger.WARN.create("- Java version could not be determined (java.version=" + + System.getProperty("java.version") + ").")); + } + if (version == 25) { + return withDiagnostics(Mode.STABLE); + } + + if (version > 25) { + return withDiagnostics(Mode.STABLE, + Diagnostic.Logger.INFO.create("Java Runtime"), + Diagnostic.Logger.INFO.create("- Running Java " + version + ". Iris is tested primarily on Java 25.")); + } + + return withDiagnostics(Mode.WARNING, + Diagnostic.Logger.WARN.create("Unsupported Java version"), + Diagnostic.Logger.WARN.create("- Java 25+ is recommended. Current runtime: Java " + version)); + }); + + private static final List TASKS = List.of( + MEMORY, + INCOMPATIBILITIES, + SOFTWARE, + VERSION, + INJECTION, + DIMENSION_TYPES, + DISK_SPACE, + JAVA + ); + + private Tasks() { + } + + public static List getTasks() { + return TASKS; + } + + private static Server server() { + return Bukkit.getServer(); + } + + private static boolean isCanvasServer() { + ClassLoader loader = server().getClass().getClassLoader(); + try { + Class.forName("io.canvasmc.canvas.region.WorldRegionizer", false, loader); + return true; + } catch (Throwable ignored) { + return server().getName().toLowerCase(Locale.ROOT).contains("canvas"); + } + } + + private static void addAllDiagnostics(List diagnostics, Diagnostic... values) { + for (Diagnostic value : values) { + diagnostics.add(value); + } + } + + private static ValueWithDiagnostics withDiagnostics(Mode mode, Diagnostic... diagnostics) { + return new ValueWithDiagnostics<>(mode, diagnostics); + } + + private static ValueWithDiagnostics withDiagnostics(Mode mode, List diagnostics) { + return new ValueWithDiagnostics<>(mode, diagnostics); + } + +} diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/task/ValueWithDiagnostics.java b/core/src/main/java/art/arcane/iris/core/safeguard/task/ValueWithDiagnostics.java new file mode 100644 index 000000000..6650cc4a0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/safeguard/task/ValueWithDiagnostics.java @@ -0,0 +1,40 @@ +package art.arcane.iris.core.safeguard.task; + +import java.util.List; + +public class ValueWithDiagnostics { + private final T value; + private final List diagnostics; + + public ValueWithDiagnostics(T value, List diagnostics) { + this.value = value; + this.diagnostics = List.copyOf(diagnostics); + } + + public ValueWithDiagnostics(T value, Diagnostic... diagnostics) { + this.value = value; + this.diagnostics = List.of(diagnostics); + } + + public T getValue() { + return value; + } + + public List getDiagnostics() { + return diagnostics; + } + + public void log() { + log(true, false); + } + + public void log(boolean withException) { + log(withException, false); + } + + public void log(boolean withException, boolean withStackTrace) { + for (Diagnostic diagnostic : diagnostics) { + diagnostic.log(withException, withStackTrace); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java new file mode 100644 index 000000000..fe177d1aa --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java @@ -0,0 +1,539 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.core.localization.BukkitUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.volmlib.util.board.Board; +import art.arcane.volmlib.util.board.BoardProvider; +import art.arcane.volmlib.util.board.BoardSettings; +import art.arcane.volmlib.util.board.ScoreDirection; +import art.arcane.volmlib.util.format.Form; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.localization.MessageArgument; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scoreboard.Objective; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.scoreboard.Team; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +public class BoardSVC implements IrisService, BoardProvider { + private static final String SEPARATOR = "&7&m-------------------"; + private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]"); + + private final Map boards = new ConcurrentHashMap<>(); + private final Map jigsawContexts = new ConcurrentHashMap<>(); + private final Map yieldedWorlds = new ConcurrentHashMap<>(); + private final Set hiddenPlayers = ConcurrentHashMap.newKeySet(); + private volatile BoardSettings settings; + private volatile boolean boardEnabled; + + @Override + public void onEnable() { + boardEnabled = true; + settings = BoardSettings.builder() + .boardProvider(this) + .scoreDirection(ScoreDirection.DOWN) + .build(); + + cleanupLeakedMainScoreboard(); + + for (Player player : art.arcane.iris.platform.bukkit.BukkitPlatform.volmitPlugin().getServer().getOnlinePlayers()) { + J.runEntity(player, () -> updatePlayer(player)); + } + } + + private void cleanupLeakedMainScoreboard() { + try { + if (Bukkit.getScoreboardManager() == null) { + return; + } + Scoreboard main = Bukkit.getScoreboardManager().getMainScoreboard(); + if (main == null) { + return; + } + + Objective objective = main.getObjective("board"); + if (objective == null || !"Iris".equalsIgnoreCase(ChatColor.stripColor(objective.getDisplayName()))) { + return; + } + + objective.unregister(); + Team team = main.getTeam("board"); + if (team != null) { + team.unregister(); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + @Override + public void onDisable() { + boardEnabled = false; + for (PlayerBoard board : new ArrayList<>(boards.values())) { + board.cancel(); + } + boards.clear(); + jigsawContexts.clear(); + yieldedWorlds.clear(); + hiddenPlayers.clear(); + settings = null; + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void on(PlayerChangedWorldEvent e) { + J.runEntity(e.getPlayer(), () -> updatePlayer(e.getPlayer())); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void on(PlayerJoinEvent e) { + J.runEntity(e.getPlayer(), () -> updatePlayer(e.getPlayer())); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void on(PlayerQuitEvent e) { + remove(e.getPlayer()); + jigsawContexts.remove(e.getPlayer().getUniqueId()); + yieldedWorlds.remove(e.getPlayer().getUniqueId()); + clearPlayerPreference(e.getPlayer().getUniqueId()); + } + + public void updatePlayer(Player p) { + if (p == null || !boardEnabled || settings == null) { + return; + } + + if (!J.isOwnedByCurrentRegion(p)) { + J.runEntity(p, () -> updatePlayer(p)); + return; + } + + UUID playerId = p.getUniqueId(); + UUID worldId = p.getWorld().getUID(); + UUID yieldedWorld = yieldedWorlds.get(playerId); + if (yieldedWorld != null) { + if (yieldedWorld.equals(worldId)) { + remove(p); + return; + } + yieldedWorlds.remove(playerId, yieldedWorld); + } + + if (!isEligibleWorld(p)) { + jigsawContexts.remove(playerId); + remove(p); + return; + } + + PlayerBoard playerBoard = boards.computeIfAbsent(p, PlayerBoard::new); + JigsawStudioBoardContext jigsawContext = currentJigsawContext(p); + if (jigsawContext != null) { + playerBoard.showJigsaw(jigsawContext); + } else { + playerBoard.showOrdinary(); + } + } + + public void applyJigsawContext(Player player, JigsawStudioBoardContext context) { + Objects.requireNonNull(player, "Jigsaw Studio board player"); + Objects.requireNonNull(context, "Jigsaw Studio board context"); + if (!J.isOwnedByCurrentRegion(player)) { + J.runEntity(player, () -> applyJigsawContext(player, context)); + return; + } + if (!player.isOnline()) { + return; + } + jigsawContexts.put(player.getUniqueId(), context); + if (context.worldId().equals(player.getWorld().getUID())) { + updatePlayer(player); + } + } + + public void clearJigsawContext(Player player) { + Objects.requireNonNull(player, "Jigsaw Studio board player"); + if (!J.isOwnedByCurrentRegion(player)) { + J.runEntity(player, () -> clearJigsawContext(player)); + return; + } + jigsawContexts.remove(player.getUniqueId()); + updatePlayer(player); + } + + public void refreshOrdinaryContext(Player player) { + Objects.requireNonNull(player, "Studio board player"); + if (!J.isOwnedByCurrentRegion(player)) { + J.runEntity(player, () -> refreshOrdinaryContext(player)); + return; + } + PlayerBoard previousBoard = boards.get(player); + boolean alreadyOrdinary = previousBoard != null && previousBoard.isOrdinary(); + jigsawContexts.remove(player.getUniqueId()); + updatePlayer(player); + PlayerBoard playerBoard = boards.get(player); + if (alreadyOrdinary && playerBoard == previousBoard) { + playerBoard.refreshOrdinary(); + } + } + + private void remove(Player player) { + if (player == null) { + return; + } + + if (!J.isOwnedByCurrentRegion(player)) { + J.runEntity(player, () -> remove(player)); + return; + } + + PlayerBoard board = boards.remove(player); + if (board != null) { + board.cancel(); + } + } + + public boolean toggle(Player player) { + Objects.requireNonNull(player, "player"); + boolean visible = togglePlayerBoard(player.getUniqueId()); + if (visible) { + yieldedWorlds.remove(player.getUniqueId()); + } + updatePlayer(player); + return visible; + } + + @Override + public String getTitle(Player player) { + return IrisLanguage.text(BukkitUiMessages.SCOREBOARD_TITLE); + } + + @Override + public List getLines(Player player) { + PlayerBoard board = boards.get(player); + if (board == null) { + return List.of(); + } + return board.lines; + } + + private boolean isEligibleWorld(Player player) { + if (player == null) { + return false; + } + + return isPlayerBoardEnabled(player.getUniqueId()) + && isStudioGeneratorEligible(IrisToolbelt.access(player.getWorld())); + } + + static boolean isStudioGeneratorEligible(PlatformChunkGenerator generator) { + return generator != null + && generator.isStudio() + && !generator.isClosing() + && generator.getEngine() != null; + } + + static List jigsawLines(JigsawStudioBoardContext context) { + Objects.requireNonNull(context, "Jigsaw Studio board context"); + List lines = new ArrayList<>(11); + lines.add(SEPARATOR); + lines.add("&dJigsaw Studio"); + lines.add("&bStructure&7: " + untrustedBoardValue(context.structureKey())); + if (!context.insideWorkcell()) { + lines.add("&bMode&7: " + context.modeDisplayName()); + lines.add(SEPARATOR); + lines.add("&eWalk into a workcell"); + if (!context.controlHint().isEmpty()) { + lines.add("&7" + untrustedBoardValue(context.controlHint())); + } + lines.add(SEPARATOR); + return List.copyOf(lines); + } + + lines.add("&bWorkcell&7: " + untrustedBoardValue(context.workcellName())); + if (!context.workcellRole().isEmpty() && !context.workcellRole().equals(context.workcellName())) { + lines.add("&bRole&7: " + untrustedBoardValue(context.workcellRole())); + } + lines.add("&bVariant&7: " + untrustedBoardValue( + context.variantName().isEmpty() ? "None" : context.variantName())); + lines.add("&bState&7: " + context.state().displayName()); + lines.add(SEPARATOR); + if (!context.controlHint().isEmpty()) { + lines.add("&e" + untrustedBoardValue(context.controlHint())); + } + lines.add(SEPARATOR); + return List.copyOf(lines); + } + + static boolean shouldRenderJigsaw( + JigsawStudioBoardContext previous, + JigsawStudioBoardContext next + ) { + return !Objects.equals(previous, next); + } + + static String untrustedBoardValue(String value) { + String normalized = value == null ? "" : value.replace('\n', ' ').replace('\r', ' '); + return LEGACY_COLOR.matcher(normalized).replaceAll("") + .replace("&", "&") + .replace("<", "‹") + .replace(">", "›"); + } + + boolean isPlayerBoardEnabled(UUID playerId) { + return playerId != null && !hiddenPlayers.contains(playerId); + } + + boolean togglePlayerBoard(UUID playerId) { + Objects.requireNonNull(playerId, "playerId"); + if (hiddenPlayers.remove(playerId)) { + return true; + } + + hiddenPlayers.add(playerId); + return false; + } + + void clearPlayerPreference(UUID playerId) { + if (playerId != null) { + hiddenPlayers.remove(playerId); + } + } + + private JigsawStudioBoardContext currentJigsawContext(Player player) { + JigsawStudioBoardContext context = jigsawContexts.get(player.getUniqueId()); + if (context == null || !context.worldId().equals(player.getWorld().getUID())) { + return null; + } + return context; + } + + public class PlayerBoard { + private final Player player; + private final Board board; + private volatile List lines; + private volatile JigsawStudioBoardContext jigsawContext; + private volatile BoardView view; + private volatile boolean cancelled; + private volatile boolean ordinaryTickScheduled; + + public PlayerBoard(Player player) { + this.player = player; + this.board = new Board(player, settings); + this.lines = List.of(); + this.jigsawContext = null; + this.view = BoardView.NONE; + this.cancelled = false; + this.ordinaryTickScheduled = false; + } + + private void showOrdinary() { + if (cancelled) { + return; + } + if (!board.ownsScoreboardAssignment()) { + yieldBoard(player, this); + return; + } + boolean switched = view != BoardView.ORDINARY; + view = BoardView.ORDINARY; + jigsawContext = null; + if (switched) { + updateOrdinary(); + board.update(); + } + scheduleOrdinaryTick(); + } + + private void refreshOrdinary() { + if (cancelled || view != BoardView.ORDINARY || !board.ownsScoreboardAssignment()) { + return; + } + updateOrdinary(); + board.update(); + } + + private boolean isOrdinary() { + return !cancelled && view == BoardView.ORDINARY; + } + + private void showJigsaw(JigsawStudioBoardContext context) { + if (cancelled) { + return; + } + if (!board.ownsScoreboardAssignment()) { + yieldBoard(player, this); + return; + } + if (view == BoardView.JIGSAW && !shouldRenderJigsaw(jigsawContext, context)) { + return; + } + view = BoardView.JIGSAW; + jigsawContext = context; + lines = jigsawLines(context); + board.update(); + } + + private void scheduleOrdinaryTick() { + if (ordinaryTickScheduled || cancelled || view != BoardView.ORDINARY + || !boardEnabled || !player.isOnline()) { + return; + } + ordinaryTickScheduled = true; + boolean scheduled = J.runEntity( + player, + () -> { + ordinaryTickScheduled = false; + ordinaryTick(); + }, + 20, + () -> ordinaryTickScheduled = false); + if (!scheduled) { + ordinaryTickScheduled = false; + } + } + + private void ordinaryTick() { + if (cancelled || view != BoardView.ORDINARY || !boardEnabled || !player.isOnline()) { + return; + } + if (!isEligibleWorld(player)) { + boards.remove(player, this); + cancel(); + return; + } + JigsawStudioBoardContext context = currentJigsawContext(player); + if (context != null) { + showJigsaw(context); + return; + } + if (!board.ownsScoreboardAssignment()) { + yieldBoard(player, this); + return; + } + updateOrdinary(); + board.update(); + scheduleOrdinaryTick(); + } + + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + if (J.isOwnedByCurrentRegion(player) && player.isOnline()) { + removeNow(); + } else { + J.runEntity(player, this::removeNow); + } + } + + private void removeNow() { + try { + board.remove(); + } catch (Throwable e) { + IrisLogging.reportError("Failed to remove the Studio scoreboard for " + player.getName() + ".", e); + } + } + + private void updateOrdinary() { + World world = player.getWorld(); + Location loc = player.getLocation(); + + PlatformChunkGenerator access = IrisToolbelt.access(world); + if (access == null) { + return; + } + + Engine engine = access.getEngine(); + if (engine == null) { + return; + } + + int x = loc.getBlockX(); + int y = loc.getBlockY() - world.getMinHeight(); + int z = loc.getBlockZ(); + + List lines = new ArrayList<>(this.lines.size()); + lines.add("&7&m "); + lines.add(IrisLanguage.text( + BukkitUiMessages.SCOREBOARD_SPEED, + MessageArgument.trusted("speed", Form.f(engine.getGeneratedPerSecond(), 0)), + MessageArgument.trusted("duration", Form.duration(1000D / engine.getGeneratedPerSecond(), 0)) + )); + lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_CACHE, MessageArgument.trusted("count", Form.f(IrisData.cacheSize())))); + lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_MANTLE, MessageArgument.trusted("count", engine.getMantle().getLoadedRegionCount()))); + + if (IrisSettings.get().getGeneral().debug) { + boolean carving = engine.getMantle().getMantle().get(x, y, z, MatterCavern.class) != null; + lines.add(IrisLanguage.text( + BukkitUiMessages.SCOREBOARD_CARVING, + MessageArgument.trusted("state", IrisLanguage.text(carving ? RuntimeUiMessages.STATUS_TRUE : RuntimeUiMessages.STATUS_FALSE)) + )); + } + + lines.add("&7&m "); + lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, z).getName()))); + lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BIOME, MessageArgument.untrusted("biome", engine.getBiomeOrMantle(x, y, z).getName()))); + lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_HEIGHT, MessageArgument.trusted("height", Math.round(engine.getHeight(x, z))))); + lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_SLOPE, MessageArgument.trusted("slope", Form.f(engine.getComplex().getSlopeStream().get(x, z), 2)))); + lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BLOCK_UPDATES, MessageArgument.trusted("updates", Form.f(engine.getBlockUpdatesPerSecond())))); + lines.add("&7&m "); + this.lines = lines; + } + } + + private void yieldBoard(Player player, PlayerBoard playerBoard) { + yieldedWorlds.put(player.getUniqueId(), player.getWorld().getUID()); + boards.remove(player, playerBoard); + playerBoard.cancel(); + } + + private enum BoardView { + NONE, + ORDINARY, + JIGSAW + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/EngineMaintenance.java b/core/src/main/java/art/arcane/iris/core/service/EngineMaintenance.java new file mode 100644 index 000000000..6776dbda8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/EngineMaintenance.java @@ -0,0 +1,104 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.pregenerator.MantleHeapPressure; +import art.arcane.iris.core.runtime.GoldenHashEngine; +import art.arcane.iris.core.tools.WorldMaintenance; +import art.arcane.iris.engine.framework.Engine; + +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +public final class EngineMaintenance { + private EngineMaintenance() { + } + + public static boolean isAvailable(Engine engine) { + return engine != null + && !engine.isClosing() + && !engine.isClosed() + && !engine.getMantle().getMantle().isClosed(); + } + + public static boolean pregeneratorTargets(Engine engine) { + if (engine == null || engine.getWorld() == null) { + return false; + } + + PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); + return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(engine.getWorld().identity()); + } + + public static boolean shouldRun(Engine engine) { + if (!shouldRunStudioMaintenance( + engine.isStudio(), + IrisSettings.get().getPerformance().isTrimMantleInStudio(), + engine.isStudio() && MantleHeapPressure.overHighWater())) { + return false; + } + if (GoldenHashEngine.isActive()) { + return false; + } + + return engine.getWorld() == null + || !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().identity()); + } + + static boolean shouldRunStudioMaintenance( + boolean studio, + boolean trimMantleInStudio, + boolean heapPressure + ) { + return !studio || trimMantleInStudio || heapPressure; + } + + public static int workerParallelism() { + return IrisSettings.get().getPerformance().getEngineSVC().getParallelism(); + } + + public static Outcome run(Engine engine) { + IrisSettings.IrisSettingsPerformance settings = IrisSettings.get().getPerformance(); + double heapUsage = MantleHeapPressure.usedFraction(); + Plan plan = plan(settings.getMantleKeepAlive(), heapUsage, settings.getEngineSVC().isForceMulticoreWrite()); + + engine.getMantle().trim(plan.idleDurationMillis()); + + long unloadStart = System.nanoTime(); + int unloadedTectonicPlates = engine.getMantle().unloadTectonicPlate( + plan.multicoreUnload() ? 0 : Integer.MAX_VALUE); + if (plan.heapPressure()) { + MantleHeapPressure.requestPanicReclaim(); + } + + return new Outcome( + unloadedTectonicPlates, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - unloadStart)); + } + + public static boolean isMantleClosed(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + String message = current.getMessage(); + if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) { + return true; + } + current = current.getCause(); + } + return false; + } + + static Plan plan(int mantleKeepAliveSeconds, double heapUsage, boolean forceMulticoreWrite) { + long baseIdleDurationMillis = TimeUnit.SECONDS.toMillis(Math.max(0, mantleKeepAliveSeconds)); + double reclaimUrgency = MantleHeapPressure.reclaimUrgency(heapUsage); + long idleDurationMillis = Math.round(baseIdleDurationMillis * (1D - reclaimUrgency)); + boolean heapPressure = reclaimUrgency >= 1D; + return new Plan(idleDurationMillis, heapPressure || forceMulticoreWrite, heapPressure); + } + + public record Outcome(int unloadedTectonicPlates, long unloadDurationMillis) { + } + + record Plan(long idleDurationMillis, boolean multicoreUnload, boolean heapPressure) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/EntityRiseSVC.java b/core/src/main/java/art/arcane/iris/core/service/EntityRiseSVC.java new file mode 100644 index 000000000..79dd598fc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/EntityRiseSVC.java @@ -0,0 +1,82 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.util.common.plugin.IrisService; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.world.EntitiesLoadEvent; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +public class EntityRiseSVC implements IrisService { + private static final NamespacedKey RISE_STAMP = new NamespacedKey("iris", "rise_restore"); + + @Override + public void onEnable() { + } + + @Override + public void onDisable() { + } + + public static void stamp(Entity entity, boolean invulnerable, boolean ai, boolean collidable) { + byte flags = 0; + if (invulnerable) { + flags |= 1; + } + if (ai) { + flags |= 2; + } + if (collidable) { + flags |= 4; + } + entity.getPersistentDataContainer().set(RISE_STAMP, PersistentDataType.BYTE, flags); + } + + public static void clearStamp(Entity entity) { + entity.getPersistentDataContainer().remove(RISE_STAMP); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on(EntitiesLoadEvent e) { + for (Entity entity : e.getEntities()) { + restoreStranded(entity); + } + } + + private static void restoreStranded(Entity entity) { + PersistentDataContainer container = entity.getPersistentDataContainer(); + Byte flags = container.get(RISE_STAMP, PersistentDataType.BYTE); + if (flags == null) { + return; + } + + container.remove(RISE_STAMP); + entity.setInvulnerable((flags & 1) != 0); + if (entity instanceof LivingEntity living) { + living.setAI((flags & 2) != 0); + living.setCollidable((flags & 4) != 0); + living.setNoDamageTicks(0); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java b/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java new file mode 100644 index 000000000..45b2aaab0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java @@ -0,0 +1,279 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.link.data.DataType; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.plugin.IrisService; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.server.PluginEnableEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; + +import java.util.Collection; +import java.util.List; +import java.util.MissingResourceException; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class ExternalDataSVC implements IrisService { + private static final String PROVIDER_PACKAGE = "art.arcane.iris.core.link.data."; + private static final List BUILT_IN_PROVIDERS = List.of( + new ProviderDefinition("CraftEngine", PROVIDER_PACKAGE + "CraftEngineDataProvider"), + new ProviderDefinition("Nexo", PROVIDER_PACKAGE + "NexoDataProvider"), + new ProviderDefinition("ItemsAdder", PROVIDER_PACKAGE + "ItemAdderDataProvider"), + new ProviderDefinition("ExecutableItems", PROVIDER_PACKAGE + "ExecutableItemsDataProvider"), + new ProviderDefinition("MMOItems", PROVIDER_PACKAGE + "MMOItemsDataProvider"), + new ProviderDefinition("EcoItems", PROVIDER_PACKAGE + "EcoItemsDataProvider"), + new ProviderDefinition("MythicMobs", PROVIDER_PACKAGE + "MythicMobsDataProvider"), + new ProviderDefinition("MythicCrucible", PROVIDER_PACKAGE + "MythicCrucibleDataProvider"), + new ProviderDefinition("KGenerators", PROVIDER_PACKAGE + "KGeneratorsDataProvider") + ); + + private final KList providers = new KList<>(); + private final KList activeProviders = new KList<>(); + + @Override + public void onEnable() { + IrisLogging.info("Loading ExternalDataProvider..."); + // enable() registers every enabled service as a listener; self-registration here + // doubled every handler invocation. + + for (ProviderDefinition definition : BUILT_IN_PROVIDERS) { + activateConfiguredProvider(definition); + } + } + + @Override + public void onDisable() { + activeProviders.clear(); + providers.clear(); + } + + @EventHandler + public void onPluginEnable(PluginEnableEvent event) { + String pluginId = event.getPlugin().getName(); + Optional existingProvider = findProvider(pluginId); + if (existingProvider.isPresent()) { + activateProvider(existingProvider.get()); + return; + } + + BUILT_IN_PROVIDERS.stream() + .filter(definition -> definition.pluginId().equalsIgnoreCase(pluginId)) + .findFirst() + .ifPresent(this::activateConfiguredProvider); + } + + public void registerProvider(ExternalDataProvider provider) { + ExternalDataProvider registeredProvider = Objects.requireNonNull(provider); + String pluginId = registeredProvider.getPluginId(); + boolean builtIn = BUILT_IN_PROVIDERS.stream() + .anyMatch(definition -> definition.pluginId().equalsIgnoreCase(pluginId)); + if (builtIn || findProvider(pluginId).isPresent()) { + throw new IllegalArgumentException("A provider with the same plugin id already exists."); + } + + providers.add(registeredProvider); + if (registeredProvider.isReady()) { + activateProvider(registeredProvider); + } + } + + public Optional getBlockData(final Identifier key) { + Pair> pair; + try { + pair = parseState(key); + } catch (IllegalArgumentException e) { + IrisLogging.error(e.getMessage()); + return Optional.empty(); + } + Identifier mod = pair.getA(); + + Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(mod, DataType.BLOCK)).findFirst(); + if (provider.isEmpty()) + return Optional.empty(); + try { + return Optional.of(provider.get().getBlockData(mod, pair.getB())); + } catch (MissingResourceException e) { + IrisLogging.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]"); + return Optional.empty(); + } + } + + public Optional> getBlockProperties(final Identifier key) { + Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(key, DataType.BLOCK)).findFirst(); + if (provider.isEmpty()) + return Optional.empty(); + try { + return Optional.of(provider.get().getBlockProperties(key)); + } catch (MissingResourceException e) { + IrisLogging.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]"); + return Optional.empty(); + } + } + + public Optional getItemStack(Identifier key, KMap customNbt) { + Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(key, DataType.ITEM)).findFirst(); + if (provider.isEmpty()) { + IrisLogging.warn("No matching Provider found for modded material \"%s\"!", key); + return Optional.empty(); + } + try { + return Optional.of(provider.get().getItemStack(key, customNbt)); + } catch (MissingResourceException e) { + IrisLogging.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]"); + return Optional.empty(); + } + } + + public void processUpdate(Engine engine, Block block, Identifier blockId) { + Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(blockId, DataType.BLOCK)).findFirst(); + if (provider.isEmpty()) { + IrisLogging.warn("No matching Provider found for modded material \"%s\"!", blockId); + return; + } + provider.get().processUpdate(engine, block, blockId); + } + + public Entity spawnMob(Location location, Identifier mobId) { + Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(mobId, DataType.ENTITY)).findFirst(); + if (provider.isEmpty()) { + IrisLogging.warn("No matching Provider found for modded mob \"%s\"!", mobId); + return null; + } + try { + return provider.get().spawnMob(location, mobId); + } catch (MissingResourceException e) { + IrisLogging.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]"); + return null; + } + } + + public Collection getAllIdentifiers(DataType dataType) { + return activeProviders.stream() + .flatMap(p -> p.getTypes(dataType).stream()) + .toList(); + } + + public Collection>> getAllBlockProperties() { + return activeProviders.stream() + .flatMap(p -> p.getTypes(DataType.BLOCK) + .stream() + .map(id -> new Pair<>(id, p.getBlockProperties(id)))) + .toList(); + } + + public static Pair> parseState(Identifier key) { + String raw = key.key(); + int open = raw.indexOf('['); + int close = raw.lastIndexOf(']'); + if (open < 0 || close < open) { + return new Pair<>(key, new KMap<>()); + } + + String state = raw.substring(open + 1, close); + KMap stateMap = new KMap<>(); + if (!state.isEmpty()) { + for (String entry : state.split(",")) { + String[] pair = entry.split("=", 2); + if (pair.length != 2 || pair[0].isBlank() || pair[1].isBlank()) { + throw new IllegalArgumentException("Malformed block state \"" + entry + "\" in \"" + key + "\" (expected key=value)"); + } + stateMap.put(pair[0], pair[1]); + } + } + return new Pair<>(new Identifier(key.namespace(), raw.substring(0, open)), stateMap); + } + + public static Identifier buildState(Identifier key, KMap state) { + if (state.isEmpty()) { + return key; + } + String path = state.entrySet() + .stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(",", key.key() + "[", "]")); + return new Identifier(key.namespace(), path); + } + + private Optional findProvider(String pluginId) { + return providers.stream() + .filter(provider -> provider.getPluginId().equalsIgnoreCase(pluginId)) + .findFirst(); + } + + private void activateConfiguredProvider(ProviderDefinition definition) { + if (!isPluginEnabled(definition.pluginId()) || findProvider(definition.pluginId()).isPresent()) { + return; + } + + try { + Class rawProviderClass = Class.forName(definition.className(), true, ExternalDataSVC.class.getClassLoader()); + Class providerClass = rawProviderClass.asSubclass(ExternalDataProvider.class); + ExternalDataProvider provider = providerClass.getDeclaredConstructor().newInstance(); + IrisLogging.info(definition.pluginId() + " found, loading " + providerClass.getSimpleName() + "..."); + activateProvider(provider); + } catch (Throwable error) { + IrisLogging.reportError("Failed to create Iris external data provider " + definition.className() + ".", error); + } + } + + private void activateProvider(ExternalDataProvider provider) { + if (activeProviders.contains(provider)) { + return; + } + try { + provider.init(); + if (provider instanceof Listener listener) { + BukkitPlatform.volmitPlugin().registerListener(listener); + } + if (!providers.contains(provider)) { + providers.add(provider); + } + activeProviders.add(provider); + IrisLogging.info("Enabled ExternalDataProvider for %s.", provider.getPluginId()); + } catch (Throwable error) { + IrisLogging.reportError("Failed to enable Iris external data provider " + provider.getPluginId() + ".", error); + } + } + + private boolean isPluginEnabled(String pluginId) { + Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginId); + return plugin != null && plugin.isEnabled(); + } + + private record ProviderDefinition(String pluginId, String className) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java b/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java new file mode 100644 index 000000000..9b2466f18 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java @@ -0,0 +1,166 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.pregenerator.cache.PregenCache; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.volmlib.util.scheduling.Looper; +import lombok.NonNull; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.WorldInitEvent; +import org.bukkit.event.world.WorldUnloadEvent; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.function.Function; + +public class GlobalCacheSVC implements IrisService { + private static final long TRIMMER_JOIN_MS = 2_000; + private static final KMap> REFERENCE_CACHE = new KMap<>(); + private final KMap globalCache = new KMap<>(); + private transient boolean lastState; + private static volatile boolean disabled = true; + private Looper trimmer; + + @Override + public void onEnable() { + disabled = false; + trimmer = new Looper() { + @Override + protected long loop() { + var it = REFERENCE_CACHE.values().iterator(); + while (it.hasNext()) { + var cache = it.next().get(); + if (cache == null) it.remove(); + else cache.trim(10_000); + } + return disabled ? -1 : 2_000; + } + }; + trimmer.start(); + lastState = !IrisSettings.get().getWorld().isGlobalPregenCache(); + if (lastState) return; + Bukkit.getWorlds().forEach(this::createCache); + } + + @Override + public void onDisable() { + disabled = true; + Looper activeTrimmer = trimmer; + trimmer = null; + if (activeTrimmer != null) { + activeTrimmer.interrupt(); + try { + activeTrimmer.join(TRIMMER_JOIN_MS); + } catch (InterruptedException ignored) { + } + if (activeTrimmer.isAlive()) { + IrisLogging.warn("Global cache trimmer did not stop within " + TRIMMER_JOIN_MS + "ms."); + } + } + globalCache.qclear((world, cache) -> cache.write()); + } + + @Nullable + public PregenCache get(@NonNull World world) { + return globalCache.get(WorldIdentity.serialize(world)); + } + + @Nullable + public PregenCache get(@NonNull String world) { + return globalCache.get(world); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on(WorldInitEvent event) { + if (isDisabled()) return; + createCache(event.getWorld()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on(WorldUnloadEvent event) { + PregenCache cache = globalCache.remove(WorldIdentity.serialize(event.getWorld())); + if (cache == null) return; + cache.write(); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on(ChunkLoadEvent event) { + var cache = get(event.getWorld()); + if (cache == null) return; + cache.cacheChunk(event.getChunk().getX(), event.getChunk().getZ()); + } + + private void createCache(World world) { + if (!IrisToolbelt.isIrisWorld(world)) return; + String worldIdentity = WorldIdentity.serialize(world); + globalCache.computeIfAbsent(worldIdentity, GlobalCacheSVC::createDefault); + } + + private boolean isDisabled() { + boolean conf = IrisSettings.get().getWorld().isGlobalPregenCache(); + if (lastState != conf) + return lastState; + + if (conf) { + Bukkit.getWorlds().forEach(this::createCache); + } else { + globalCache.values().removeIf(cache -> { + cache.write(); + return true; + }); + } + + return lastState = !conf; + } + + + @NonNull + public static PregenCache createCache(@NonNull String worldIdentity, @NonNull Function provider) { + PregenCache[] holder = new PregenCache[1]; + REFERENCE_CACHE.compute(worldIdentity, (identity, ref) -> { + if (ref != null) { + if ((holder[0] = ref.get()) != null) + return ref; + } + return new WeakReference<>(holder[0] = provider.apply(worldIdentity)); + }); + return holder[0]; + } + + @NonNull + public static PregenCache createDefault(@NonNull String worldIdentity) { + return createCache(worldIdentity, GlobalCacheSVC::createDefault0); + } + + private static PregenCache createDefault0(String worldIdentity) { + if (disabled) return PregenCache.EMPTY; + NamespacedKey worldKey = WorldIdentity.parse(worldIdentity); + File dimensionRoot = requireCacheDimensionRoot( + Bukkit.getWorldContainer(), + IrisWorldStorage.levelRoot(), + worldKey + ); + return PregenCache.create(new File(dimensionRoot, "iris/pregen")).sync(); + } + + static File requireCacheDimensionRoot(File worldContainer, File levelRoot, NamespacedKey worldKey) { + return IrisWorldStorage.requireFrozenDimensionRoot( + worldContainer, + levelRoot, + IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName()), + worldKey + ); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioBoardContext.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioBoardContext.java new file mode 100644 index 000000000..4e04ceb5f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioBoardContext.java @@ -0,0 +1,56 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode; + +import java.util.Objects; +import java.util.UUID; + +public record JigsawStudioBoardContext( + UUID worldId, + UUID requestId, + String structureKey, + JigsawStudioMode mode, + String workcellRole, + String workcellName, + String variantName, + JigsawStudioBoardState state, + String controlHint +) { + public JigsawStudioBoardContext { + worldId = Objects.requireNonNull(worldId, "Jigsaw Studio board world ID"); + requestId = Objects.requireNonNull(requestId, "Jigsaw Studio board request ID"); + structureKey = requireText(structureKey, "structure key"); + mode = Objects.requireNonNull(mode, "Jigsaw Studio board mode"); + workcellRole = optionalText(workcellRole); + workcellName = optionalText(workcellName); + variantName = optionalText(variantName); + state = Objects.requireNonNull(state, "Jigsaw Studio board state"); + controlHint = optionalText(controlHint); + if (workcellName.isEmpty() && (!workcellRole.isEmpty() || !variantName.isEmpty())) { + throw new IllegalArgumentException("Jigsaw Studio board variants require a workcell"); + } + } + + public boolean insideWorkcell() { + return !workcellName.isEmpty(); + } + + public String modeDisplayName() { + return switch (mode) { + case PLANAR_JIGSAW -> "Planar"; + case SPATIAL_JIGSAW -> "Spatial"; + }; + } + + private static String requireText(String value, String name) { + String normalized = optionalText(value); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio board " + name + " cannot be blank"); + } + return normalized; + } + + private static String optionalText(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioBoardState.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioBoardState.java new file mode 100644 index 000000000..b37852ecd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioBoardState.java @@ -0,0 +1,21 @@ +package art.arcane.iris.core.service; + +public enum JigsawStudioBoardState { + LOADING("Loading"), + SAVED("Saved"), + UNSAVED("Unsaved"), + SAVING("Saving"), + DISABLED("Disabled"), + INVALID("Invalid"), + READ_ONLY("Read-only"); + + private final String displayName; + + JigsawStudioBoardState(String displayName) { + this.displayName = displayName; + } + + public String displayName() { + return displayName; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioEvaluationState.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioEvaluationState.java new file mode 100644 index 000000000..66bebb410 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioEvaluationState.java @@ -0,0 +1,9 @@ +package art.arcane.iris.core.service; + +public enum JigsawStudioEvaluationState { + PENDING, + VALID, + WARNING, + INVALID, + STALE +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioGraphEvaluation.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioGraphEvaluation.java new file mode 100644 index 000000000..6405c0555 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioGraphEvaluation.java @@ -0,0 +1,50 @@ +package art.arcane.iris.core.service; + +import java.util.Objects; +import java.util.UUID; + +public record JigsawStudioGraphEvaluation( + UUID requestId, + long generation, + long seed, + JigsawStudioEvaluationState state, + String selectedTheme, + int pieceCount, + String detail, + JigsawStudioPreviewRenderer.PreviewBounds previewBounds +) { + public JigsawStudioGraphEvaluation { + requestId = Objects.requireNonNull(requestId, "Jigsaw Studio evaluation request ID"); + if (generation < 1L) { + throw new IllegalArgumentException("Jigsaw Studio evaluation generation must be positive"); + } + state = Objects.requireNonNull(state, "Jigsaw Studio evaluation state"); + selectedTheme = normalize(selectedTheme); + if (pieceCount < 0) { + throw new IllegalArgumentException("Jigsaw Studio evaluation piece count cannot be negative"); + } + detail = normalize(detail); + previewBounds = Objects.requireNonNull(previewBounds, "Jigsaw Studio evaluation preview bounds"); + } + + public JigsawStudioGraphEvaluation stale(String reason) { + return new JigsawStudioGraphEvaluation( + requestId, + generation, + seed, + JigsawStudioEvaluationState.STALE, + selectedTheme, + pieceCount, + reason, + previewBounds); + } + + public boolean successful() { + return state == JigsawStudioEvaluationState.VALID + || state == JigsawStudioEvaluationState.WARNING; + } + + private static String normalize(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioHistoryStore.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioHistoryStore.java new file mode 100644 index 000000000..14a05b3be --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioHistoryStore.java @@ -0,0 +1,519 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.structure.authoring.StructureBackend; +import art.arcane.iris.core.structure.authoring.StructureCapability; +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureLoss; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureSource; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.authoring.StructureWriteOptions; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReentrantLock; + +final class JigsawStudioHistoryStore { + static final int MAX_ITERATIONS = 5; + private static final int SCHEMA_VERSION = 1; + private static final long MAX_HISTORY_BYTES = 512L * 1024L * 1024L; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final ConcurrentMap LOCKS = new ConcurrentHashMap<>(); + + private final Path packRoot; + private final StructureKey structureKey; + private final StructureTransactionWriter writer; + private final Path historyPath; + private final ReentrantLock lock; + + JigsawStudioHistoryStore(Path packRoot, String structureKey) { + this.packRoot = canonicalPackRoot(packRoot); + this.structureKey = new StructureKey( + "iris", + Objects.requireNonNull(structureKey, "Jigsaw Studio history structure key")); + writer = new StructureTransactionWriter(this.packRoot); + String identityHash = StructureHash.sha256(this.structureKey.value().getBytes(StandardCharsets.UTF_8)); + historyPath = this.packRoot.resolve(".iris/jigsaw-history/key-" + identityHash + ".json").normalize(); + if (!historyPath.startsWith(this.packRoot)) { + throw new IllegalArgumentException("Jigsaw Studio history path escapes its pack root"); + } + lock = LOCKS.computeIfAbsent(historyPath, ignored -> new ReentrantLock()); + } + + Snapshot snapshotCurrent(String pieceKey) throws IOException { + lock.lock(); + try { + return readCurrentSnapshot(pieceKey); + } finally { + lock.unlock(); + } + } + + int append(Snapshot snapshot) throws IOException { + Objects.requireNonNull(snapshot, "Jigsaw Studio history snapshot"); + lock.lock(); + try { + HistoryDocument document = readDocument(); + if (!document.structure().equals(structureKey.value())) { + throw new IOException("Jigsaw Studio history belongs to " + document.structure()); + } + ArrayList iterations = new ArrayList<>(document.iterations()); + HistoryIteration iteration = snapshot.iteration(); + if (!iterations.isEmpty() && iterations.getLast().sameState(iteration)) { + return iterations.size(); + } + iterations.add(iteration); + while (iterations.size() > MAX_ITERATIONS) { + iterations.removeFirst(); + } + TreeMap blobs = new TreeMap<>(document.blobs()); + for (Map.Entry resource : snapshot.resources().entrySet()) { + String hash = StructureHash.sha256(resource.getValue()); + blobs.putIfAbsent(hash, Base64.getEncoder().encodeToString(resource.getValue())); + } + retainReferencedBlobs(blobs, iterations); + writeDocument(new HistoryDocument( + SCHEMA_VERSION, + structureKey.value(), + List.copyOf(iterations), + Map.copyOf(blobs))); + return iterations.size(); + } finally { + lock.unlock(); + } + } + + UndoResult undoLatest() throws IOException { + lock.lock(); + try { + HistoryDocument document = readDocument(); + if (document.iterations().isEmpty()) { + return UndoResult.unavailable(); + } + HistoryIteration iteration = document.iterations().getLast(); + StructureResourceBundle bundle = restoreBundle(iteration, document.blobs()); + StructureResourceBundleGraphCompiler.requireViable(bundle); + Path manifestPath = writer.ownershipManifestPath(structureKey); + byte[] currentManifest = readRegularFile(manifestPath, "ownership manifest"); + StructureWriteResult result = writer.write( + bundle, + StructureWriteOptions.overwriteExpected(StructureHash.sha256(currentManifest))); + if (!result.successful()) { + return new UndoResult( + false, + true, + document.iterations().size(), + iteration.pieceKey(), + result, + ""); + } + ArrayList remaining = new ArrayList<>(document.iterations()); + remaining.removeLast(); + TreeMap blobs = new TreeMap<>(document.blobs()); + retainReferencedBlobs(blobs, remaining); + String warning = ""; + try { + if (remaining.isEmpty()) { + Files.deleteIfExists(historyPath); + forceDirectory(historyPath.getParent()); + } else { + writeDocument(new HistoryDocument( + SCHEMA_VERSION, + structureKey.value(), + List.copyOf(remaining), + Map.copyOf(blobs))); + } + } catch (IOException historyFailure) { + warning = historyFailure.getMessage() == null + ? historyFailure.getClass().getSimpleName() + : historyFailure.getMessage(); + } + return new UndoResult( + true, + true, + remaining.size(), + iteration.pieceKey(), + result, + warning); + } finally { + lock.unlock(); + } + } + + int availableIterations() throws IOException { + lock.lock(); + try { + return readDocument().iterations().size(); + } finally { + lock.unlock(); + } + } + + void delete() throws IOException { + lock.lock(); + try { + if (Files.deleteIfExists(historyPath)) { + forceDirectory(historyPath.getParent()); + } + } finally { + lock.unlock(); + } + } + + Path historyPath() { + return historyPath; + } + + private Snapshot readCurrentSnapshot(String pieceKey) throws IOException { + Path manifestPath = writer.ownershipManifestPath(structureKey); + byte[] manifestContent = readRegularFile(manifestPath, "ownership manifest"); + StructureOwnershipManifest manifest; + try { + manifest = StructureOwnershipManifest.fromJson(manifestContent); + } catch (RuntimeException exception) { + throw new IOException("Jigsaw Studio history cannot parse the ownership manifest", exception); + } + if (!manifest.structure().equals(structureKey)) { + throw new IOException("Jigsaw Studio ownership manifest belongs to " + manifest.structure()); + } + TreeMap resources = new TreeMap<>(); + for (Map.Entry resource : manifest.resourceHashes().entrySet()) { + Path resourcePath = resolveOwnedResource(resource.getKey()); + byte[] content = readRegularFile(resourcePath, "owned resource " + resource.getKey()); + String actualHash = StructureHash.sha256(content); + if (!resource.getValue().equals(actualHash)) { + throw new IOException("Jigsaw Studio owned resource changed before history capture: " + + resource.getKey()); + } + resources.put(resource.getKey(), content); + } + return new Snapshot( + new HistoryIteration( + System.currentTimeMillis(), + Objects.requireNonNull(pieceKey, "Jigsaw Studio history piece key"), + manifest.source(), + manifest.backend(), + manifest.capabilities(), + manifest.losses(), + manifest.resourceHashes()), + resources); + } + + private StructureResourceBundle restoreBundle( + HistoryIteration iteration, + Map blobs + ) throws IOException { + StructureResourceBundle.Builder builder = StructureResourceBundle.builder(structureKey) + .source(iteration.source()) + .backend(iteration.backend()) + .capabilities(iteration.capabilities()) + .losses(iteration.losses()); + for (Map.Entry resource : iteration.resourceHashes().entrySet()) { + String encoded = blobs.get(resource.getValue()); + if (encoded == null) { + throw new IOException("Jigsaw Studio history is missing resource blob " + resource.getValue()); + } + byte[] content; + try { + content = Base64.getDecoder().decode(encoded); + } catch (IllegalArgumentException exception) { + throw new IOException("Jigsaw Studio history contains invalid resource data", exception); + } + if (!resource.getValue().equals(StructureHash.sha256(content))) { + throw new IOException("Jigsaw Studio history resource hash does not match " + + resource.getKey()); + } + builder.resource(resource.getKey(), content); + } + return builder.build(); + } + + private HistoryDocument readDocument() throws IOException { + if (!Files.exists(historyPath, LinkOption.NOFOLLOW_LINKS)) { + return HistoryDocument.empty(structureKey.value()); + } + byte[] content = readRegularFile(historyPath, "history file"); + if (content.length > MAX_HISTORY_BYTES) { + throw new IOException("Jigsaw Studio history exceeds " + MAX_HISTORY_BYTES + " bytes"); + } + HistoryDocument document; + try { + document = GSON.fromJson(new String(content, StandardCharsets.UTF_8), HistoryDocument.class); + } catch (RuntimeException exception) { + throw new IOException("Jigsaw Studio history is invalid", exception); + } + if (document == null || document.schemaVersion() != SCHEMA_VERSION) { + throw new IOException("Unsupported Jigsaw Studio history schema"); + } + HistoryDocument validated; + try { + validated = document.validated(); + } catch (RuntimeException exception) { + throw new IOException("Jigsaw Studio history is invalid", exception); + } + if (!validated.structure().equals(structureKey.value())) { + throw new IOException("Jigsaw Studio history belongs to " + validated.structure()); + } + return validated; + } + + private void writeDocument(HistoryDocument document) throws IOException { + HistoryDocument validated = document.validated(); + byte[] content = (GSON.toJson(validated) + "\n").getBytes(StandardCharsets.UTF_8); + if (content.length > MAX_HISTORY_BYTES) { + throw new IOException("Jigsaw Studio history exceeds " + MAX_HISTORY_BYTES + " bytes"); + } + Path historyRoot = historyPath.getParent(); + Files.createDirectories(historyRoot); + rejectSymbolicPath(historyRoot); + Path temporary = historyRoot.resolve(historyPath.getFileName() + "." + + UUID.randomUUID() + ".tmp").normalize(); + try { + try (FileChannel channel = FileChannel.open( + temporary, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(content); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move( + temporary, + historyPath, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException exception) { + Files.move(temporary, historyPath, StandardCopyOption.REPLACE_EXISTING); + } + forceDirectory(historyRoot); + } finally { + Files.deleteIfExists(temporary); + } + } + + private Path resolveOwnedResource(String relativePath) throws IOException { + StructureResourceBundle.validateRelativePath(relativePath); + Path resource = packRoot.resolve(relativePath).normalize(); + if (!resource.startsWith(packRoot)) { + throw new IOException("Jigsaw Studio history resource escapes its pack root: " + relativePath); + } + rejectSymbolicPath(resource.getParent()); + return resource; + } + + private static byte[] readRegularFile(Path path, String kind) throws IOException { + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Jigsaw Studio " + kind + " is missing or not a regular file: " + path); + } + return Files.readAllBytes(path); + } + + private static void retainReferencedBlobs( + Map blobs, + List iterations + ) { + Set retained = new TreeSet<>(); + for (HistoryIteration iteration : iterations) { + retained.addAll(iteration.resourceHashes().values()); + } + blobs.keySet().retainAll(retained); + } + + private void rejectSymbolicPath(Path path) throws IOException { + Path current = path; + while (current != null && current.startsWith(packRoot)) { + if (Files.isSymbolicLink(current)) { + throw new IOException("Jigsaw Studio history path contains a symbolic link: " + current); + } + if (current.equals(packRoot)) { + return; + } + current = current.getParent(); + } + throw new IOException("Jigsaw Studio history path escapes its pack root: " + path); + } + + private static Path canonicalPackRoot(Path root) { + Path normalized = Objects.requireNonNull(root, "Jigsaw Studio history pack root") + .toAbsolutePath().normalize(); + try { + return normalized.toRealPath(); + } catch (IOException exception) { + throw new IllegalArgumentException("Jigsaw Studio history pack root is unavailable: " + + normalized, exception); + } + } + + private static void forceDirectory(Path directory) throws IOException { + // A directory can only be opened and fsynced on a POSIX filesystem; Windows rejects the + // open outright. Matches DirectoryDurability and DatapackIngestService, which already + // skip the barrier there. + if (!Files.getFileStore(directory).supportsFileAttributeView("posix")) { + return; + } + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + record Snapshot(HistoryIteration iteration, Map resources) { + Snapshot { + Objects.requireNonNull(iteration, "Jigsaw Studio history iteration"); + Objects.requireNonNull(resources, "Jigsaw Studio history resources"); + LinkedHashMap copies = new LinkedHashMap<>(); + for (Map.Entry resource : resources.entrySet()) { + copies.put(resource.getKey(), resource.getValue().clone()); + } + resources = Map.copyOf(copies); + } + + boolean matches(StructureResourceBundle bundle) { + if (!iteration.source().equals(bundle.source()) + || iteration.backend() != bundle.backend() + || !Set.copyOf(iteration.capabilities()).equals(bundle.capabilities()) + || !iteration.losses().equals(bundle.losses()) + || iteration.resourceHashes().size() != bundle.resources().size()) { + return false; + } + for (Map.Entry resource + : bundle.resources().entrySet()) { + if (!resource.getValue().contentHash().equals( + iteration.resourceHashes().get(resource.getKey()))) { + return false; + } + } + return true; + } + } + + record UndoResult( + boolean successful, + boolean available, + int remainingIterations, + String pieceKey, + StructureWriteResult writeResult, + String warning + ) { + UndoResult { + pieceKey = pieceKey == null ? "" : pieceKey; + warning = warning == null ? "" : warning; + } + + static UndoResult unavailable() { + return new UndoResult(false, false, 0, "", null, ""); + } + } + + private record HistoryDocument( + int schemaVersion, + String structure, + List iterations, + Map blobs + ) { + private HistoryDocument { + structure = structure == null ? "" : structure; + iterations = iterations == null ? List.of() : List.copyOf(iterations); + blobs = blobs == null ? Map.of() : Map.copyOf(blobs); + } + + private static HistoryDocument empty(String structure) { + return new HistoryDocument(SCHEMA_VERSION, structure, List.of(), Map.of()); + } + + private HistoryDocument validated() throws IOException { + if (schemaVersion != SCHEMA_VERSION || structure.isBlank()) { + throw new IOException("Jigsaw Studio history header is invalid"); + } + if (iterations.size() > MAX_ITERATIONS) { + throw new IOException("Jigsaw Studio history contains too many iterations"); + } + TreeMap validatedBlobs = new TreeMap<>(); + for (Map.Entry blob : blobs.entrySet()) { + if (!StructureHash.isSha256(blob.getKey()) || blob.getValue() == null) { + throw new IOException("Jigsaw Studio history contains an invalid resource blob"); + } + validatedBlobs.put(blob.getKey(), blob.getValue()); + } + return new HistoryDocument( + SCHEMA_VERSION, + structure, + List.copyOf(iterations), + Map.copyOf(validatedBlobs)); + } + } + + private record HistoryIteration( + long recordedAtEpochMilli, + String pieceKey, + StructureSource source, + StructureBackend backend, + List capabilities, + List losses, + Map resourceHashes + ) { + private HistoryIteration { + pieceKey = Objects.requireNonNull(pieceKey, "Jigsaw Studio history piece key").trim(); + if (pieceKey.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio history piece key cannot be empty"); + } + Objects.requireNonNull(source, "Jigsaw Studio history source"); + Objects.requireNonNull(backend, "Jigsaw Studio history backend"); + capabilities = List.copyOf(Objects.requireNonNull( + capabilities, + "Jigsaw Studio history capabilities")); + losses = List.copyOf(Objects.requireNonNull(losses, "Jigsaw Studio history losses")); + TreeMap hashes = new TreeMap<>(); + for (Map.Entry resource : Objects.requireNonNull( + resourceHashes, + "Jigsaw Studio history resource hashes").entrySet()) { + String relativePath = StructureResourceBundle.validateRelativePath(resource.getKey()); + if (!StructureHash.isSha256(resource.getValue())) { + throw new IllegalArgumentException("Invalid Jigsaw Studio history resource hash"); + } + hashes.put(relativePath, resource.getValue()); + } + if (hashes.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio history iteration cannot be empty"); + } + resourceHashes = Map.copyOf(hashes); + } + + private boolean sameState(HistoryIteration other) { + return source.equals(other.source) + && backend == other.backend + && pieceKey.equals(other.pieceKey) + && capabilities.equals(other.capabilities) + && losses.equals(other.losses) + && resourceHashes.equals(other.resourceHashes); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMarkerParser.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMarkerParser.java new file mode 100644 index 000000000..fd2c64b20 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMarkerParser.java @@ -0,0 +1,105 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMarkerKeyCodec; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.JigsawJoint; +import org.bukkit.block.Orientation; + +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +final class JigsawStudioMarkerParser { + private JigsawStudioMarkerParser() { + } + + static IrisJigsawConnector parse( + Map nbt, + Orientation orientation, + int x, + int y, + int z + ) { + Map properties = Objects.requireNonNull(nbt, "Jigsaw marker NBT"); + Directions directions = directions(orientation); + String jointName = requiredString(properties, "joint").toUpperCase(Locale.ROOT); + JigsawJoint joint; + try { + joint = JigsawJoint.valueOf(jointName); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException("Unsupported jigsaw joint '" + jointName + "'", exception); + } + return new IrisJigsawConnector() + .setPosition(new IrisPosition(x, y, z)) + .setDirection(directions.front()) + .setTop(directions.top()) + .setPool(JigsawStudioMarkerKeyCodec.decodePool(requiredString(properties, "pool"))) + .setName(requiredString(properties, "name")) + .setTargetName(requiredString(properties, "target")) + .setChannel(optionalString(properties, "channel")) + .setJoint(joint) + .setFinalState(requiredString(properties, "final_state")) + .setSelectionPriority(optionalInt(properties, "selection_priority")) + .setPlacementPriority(optionalInt(properties, "placement_priority")); + } + + static Directions directions(Orientation orientation) { + return switch (Objects.requireNonNull(orientation, "Jigsaw orientation")) { + case DOWN_EAST -> new Directions(IrisDirection.DOWN_NEGATIVE_Y, IrisDirection.EAST_POSITIVE_X); + case DOWN_NORTH -> new Directions(IrisDirection.DOWN_NEGATIVE_Y, IrisDirection.NORTH_NEGATIVE_Z); + case DOWN_SOUTH -> new Directions(IrisDirection.DOWN_NEGATIVE_Y, IrisDirection.SOUTH_POSITIVE_Z); + case DOWN_WEST -> new Directions(IrisDirection.DOWN_NEGATIVE_Y, IrisDirection.WEST_NEGATIVE_X); + case UP_EAST -> new Directions(IrisDirection.UP_POSITIVE_Y, IrisDirection.EAST_POSITIVE_X); + case UP_NORTH -> new Directions(IrisDirection.UP_POSITIVE_Y, IrisDirection.NORTH_NEGATIVE_Z); + case UP_SOUTH -> new Directions(IrisDirection.UP_POSITIVE_Y, IrisDirection.SOUTH_POSITIVE_Z); + case UP_WEST -> new Directions(IrisDirection.UP_POSITIVE_Y, IrisDirection.WEST_NEGATIVE_X); + case WEST_UP -> new Directions(IrisDirection.WEST_NEGATIVE_X, IrisDirection.UP_POSITIVE_Y); + case EAST_UP -> new Directions(IrisDirection.EAST_POSITIVE_X, IrisDirection.UP_POSITIVE_Y); + case NORTH_UP -> new Directions(IrisDirection.NORTH_NEGATIVE_Z, IrisDirection.UP_POSITIVE_Y); + case SOUTH_UP -> new Directions(IrisDirection.SOUTH_POSITIVE_Z, IrisDirection.UP_POSITIVE_Y); + }; + } + + private static String requiredString(Map properties, String key) { + Object value = properties.get(key); + if (!(value instanceof String stringValue) || stringValue.isBlank()) { + throw new IllegalArgumentException("Jigsaw marker requires non-empty string NBT '" + key + "'"); + } + return stringValue.trim(); + } + + private static String optionalString(Map properties, String key) { + Object value = properties.get(key); + if (value == null) { + return ""; + } + if (!(value instanceof String stringValue)) { + throw new IllegalArgumentException("Jigsaw marker NBT '" + key + "' must be a string"); + } + return stringValue.trim(); + } + + private static int optionalInt(Map properties, String key) { + Object value = properties.get(key); + if (value == null) { + return 0; + } + if (!(value instanceof Number number)) { + throw new IllegalArgumentException("Jigsaw marker NBT '" + key + "' must be an integer"); + } + long longValue = number.longValue(); + if (longValue < Integer.MIN_VALUE || longValue > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Jigsaw marker NBT '" + key + "' is outside the integer range"); + } + return (int) longValue; + } + + record Directions(IrisDirection front, IrisDirection top) { + Directions { + Objects.requireNonNull(front, "Jigsaw front direction"); + Objects.requireNonNull(top, "Jigsaw top direction"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuController.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuController.java new file mode 100644 index 000000000..2cbf2e6f2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuController.java @@ -0,0 +1,3393 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPieceRules; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolAction; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.data.MaterialBlock; +import art.arcane.volmlib.util.inventorygui.UIElement; +import art.arcane.volmlib.util.inventorygui.UIPaneDecorator; +import art.arcane.volmlib.util.inventorygui.UIWindow; +import art.arcane.volmlib.util.inventorygui.WindowResolution; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.List; +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; + +public final class JigsawStudioMenuController { + static final int VARIANTS_PER_PAGE = 28; + static final int MEMBERSHIPS_PER_PAGE = 28; + static final int THEME_SETS_PER_PAGE = 21; + static final int TOOLS_PER_PAGE = 28; + static final int CHANCE_STEP_PERCENTAGE_POINTS = 5; + static final int RULE_SHIFT_STEP = 5; + static final int PLACEMENT_RULE_SHIFT_STEP = 16; + + private static final long DESTRUCTIVE_CONFIRM_NANOS = 10_000_000_000L; + private static final int WORKCELL_RESIZE_REFRESH_TICKS = 2; + private static final int WORKCELL_RESIZE_REFRESH_ATTEMPTS = 200; + private static final int[] GRID_POSITIONS = {-3, -2, -1, 0, 1, 2, 3}; + + private final JavaPlugin plugin; + private final Actions actions; + private final Map windows = new ConcurrentHashMap<>(); + private final Map pendingUnlinks = new ConcurrentHashMap<>(); + private final Map pendingDeletes = new ConcurrentHashMap<>(); + private final Map pendingProjectDeletes = new ConcurrentHashMap<>(); + private final Map pendingWorkcellResizes = new ConcurrentHashMap<>(); + + public JigsawStudioMenuController(JavaPlugin plugin, Actions actions) { + this.plugin = Objects.requireNonNull(plugin, "Jigsaw Studio menu plugin"); + this.actions = Objects.requireNonNull(actions, "Jigsaw Studio menu actions"); + } + + public boolean open(Player player) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> open(player)); + } + + Optional available = actions.menuState(player); + if (available.isEmpty()) { + close(player); + player.sendMessage(ChatColor.RED + "Iris Jigsaw Studio is not active in this world."); + return false; + } + JigsawStudioMenuState state = available.get(); + if (state.workcells().isEmpty()) { + close(player); + player.sendMessage(ChatColor.RED + "This Jigsaw Studio has no workcells."); + return false; + } + + JigsawStudioMenuState.Workcell selected = state.selectedWorkcell(); + if (selected == null) { + String firstWorkcellId = state.workcells().getFirst().stableId(); + if (!actions.selectWorkcell(player, firstWorkcellId)) { + return false; + } + Optional selectedState = matchingState(player, state.requestId(), false); + if (selectedState.isEmpty()) { + return false; + } + state = selectedState.get(); + selected = state.selectedWorkcell(); + if (selected == null) { + return false; + } + } + + close(player); + UUID playerId = player.getUniqueId(); + UIWindow window = new UIWindow(plugin, player); + window.setResolution(WindowResolution.W9_H6); + window.setViewportHeight(6); + window.setDecorator(new UIPaneDecorator(Material.BLACK_STAINED_GLASS_PANE)); + window.setTitle(title(state.structureKey())); + window.onClosed(closed -> { + windows.remove(playerId, window); + pendingUnlinks.remove(playerId); + pendingDeletes.remove(playerId); + pendingProjectDeletes.remove(playerId); + pendingWorkcellResizes.remove(playerId); + }); + windows.put(playerId, window); + pendingUnlinks.remove(playerId); + pendingDeletes.remove(playerId); + pendingProjectDeletes.remove(playerId); + pendingWorkcellResizes.remove(playerId); + renderMain(window, state, selected, 0); + window.open(); + return true; + } + + public boolean openWorkcellSettings(Player player, String workcellId) { + if (player == null || workcellId == null || workcellId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> openWorkcellSettings(player, workcellId)); + } + if (!open(player)) { + return false; + } + Optional current = actions.menuState(player); + if (current.isEmpty() || current.get().workcell(workcellId) == null) { + stale(player); + return false; + } + openWorkcellSettings(player, current.get().requestId(), workcellId); + return true; + } + + public boolean openVariantSettings(Player player, String workcellId, String pieceKey) { + if (player == null + || workcellId == null + || workcellId.isBlank() + || pieceKey == null + || pieceKey.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> openVariantSettings(player, workcellId, pieceKey)); + } + if (!open(player)) { + return false; + } + Optional current = actions.menuState(player); + if (current.isEmpty()) { + stale(player); + return false; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + if (variant == null || !variant.active() || !variant.owned()) { + stale(player); + return false; + } + if (!current.get().irisExtended()) { + player.sendMessage(ChatColor.YELLOW + + "Vanilla-portable pieces cannot encode Iris theme or piece-rule metadata."); + close(player); + return false; + } + openVariantSettings(player, current.get().requestId(), workcellId, pieceKey, 0); + return true; + } + + public boolean openVariantSizeSettings(Player player, String workcellId, String pieceKey) { + if (player == null || workcellId == null || workcellId.isBlank() + || pieceKey == null || pieceKey.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> openVariantSizeSettings(player, workcellId, pieceKey)); + } + if (!open(player)) { + return false; + } + Optional current = actions.menuState(player); + if (current.isEmpty()) { + stale(player); + return false; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + if (workcell == null || variant == null || !variant.owned() || variant.dimensions().isEmpty()) { + stale(player); + return false; + } + UIWindow window = windows.get(player.getUniqueId()); + if (window == null) { + return false; + } + renderVariantSizeSettings(window, current.get(), workcell, variant); + return true; + } + + public boolean openStructureSettings(Player player) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> openStructureSettings(player)); + } + if (!open(player)) { + return false; + } + Optional current = actions.menuState(player); + if (current.isEmpty() || current.get().selectedWorkcell() == null) { + stale(player); + return false; + } + openStructureSettings( + player, + current.get().requestId(), + current.get().selectedWorkcellId(), + 0); + return true; + } + + public void close(Player player) { + if (player == null) { + return; + } + UUID playerId = player.getUniqueId(); + UIWindow window = windows.remove(playerId); + pendingUnlinks.remove(playerId); + pendingDeletes.remove(playerId); + pendingProjectDeletes.remove(playerId); + if (window == null) { + return; + } + if (J.isOwnedByCurrentRegion(player)) { + window.close(); + return; + } + J.runEntity(player, window::close); + } + + public void closeAll() { + List activeWindows = new ArrayList<>(windows.values()); + windows.clear(); + pendingUnlinks.clear(); + pendingDeletes.clear(); + pendingProjectDeletes.clear(); + pendingWorkcellResizes.clear(); + for (UIWindow window : activeWindows) { + Player player = window.getViewer(); + if (J.isOwnedByCurrentRegion(player)) { + window.close(); + } else { + J.runEntity(player, window::close); + } + } + } + + private void renderMain( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell selected, + int requestedPage + ) { + int page = clampPage(requestedPage, selected.variants().size(), VARIANTS_PER_PAGE); + window.batch(() -> { + window.clearElements(); + addWorkcells(window, state, selected.stableId()); + addFamilyControl(window, state); + addStructureControl(window, state, selected.stableId()); + addVariants(window, state, selected, page); + addMainActions(window, state, selected, page); + }); + } + + private void addFamilyControl(UIWindow window, JigsawStudioMenuState state) { + String nextThemeKey = nextThemeSetKey(state.themeSets()); + boolean available = state.irisExtended() && state.workcells().stream() + .filter(JigsawStudioMenuState.Workcell::enabled) + .allMatch(workcell -> workcell.activeVariant() != null && workcell.activeVariant().owned()); + UIElement family = element( + "duplicate-family", + available ? Material.PURPLE_DYE : Material.GRAY_DYE, + available + ? ChatColor.LIGHT_PURPLE + "Duplicate All Enabled Cells as Family" + : ChatColor.GRAY + "Duplicate All Enabled Cells as Family"); + family.addLore(ChatColor.GRAY + "Copies each enabled cell's loaded variant into one coherent family."); + family.addLore(ChatColor.GRAY + "New family: " + safe(nextThemeKey)); + if (available) { + family.addLore(ChatColor.YELLOW + "Left-click to duplicate the complete family atomically"); + family.onLeftClick(clicked -> duplicateActiveFamily( + window.getViewer(), state.requestId(), nextThemeKey)); + } else if (!state.irisExtended()) { + family.addLore(ChatColor.GRAY + "Coherent families require Iris compatibility."); + } else { + family.addLore(ChatColor.GRAY + "Load an owned variant in every enabled workcell first."); + } + window.setElement(3, 0, family); + } + + private void addStructureControl( + UIWindow window, + JigsawStudioMenuState state, + String selectedWorkcellId + ) { + UIElement structure = element( + "structure-rules", + state.irisExtended() + ? state.requireCaps() ? Material.IRON_BARS : Material.TRIPWIRE_HOOK + : Material.BARRIER, + ChatColor.LIGHT_PURPLE + "Structure Rules"); + structure.addLore(ChatColor.GRAY + "Mandatory caps: " + yesNo(state.requireCaps())); + structure.addLore(ChatColor.GRAY + "Theme sets: " + state.themeSets().size()); + structure.addLore(ChatColor.GRAY + "Compatibility: " + compatibilityName(state.compatibilityTarget())); + structure.addLore(ChatColor.YELLOW + "Left-click for themes and rules"); + structure.addLore(state.irisExtended() + ? ChatColor.YELLOW + "Right-click to toggle mandatory caps" + : ChatColor.GRAY + "Mandatory caps and themes require Iris compatibility"); + structure.onLeftClick(clicked -> openStructureSettings( + window.getViewer(), state.requestId(), selectedWorkcellId, 0)); + if (state.irisExtended()) { + structure.onRightClick(clicked -> setRequireCaps( + window.getViewer(), state.requestId(), !state.requireCaps())); + } + window.setElement(4, 0, structure); + } + + private void addWorkcells(UIWindow window, JigsawStudioMenuState state, String selectedWorkcellId) { + int count = state.workcells().size(); + int start = count == 1 ? 0 : -(count / 2); + for (int index = 0; index < count; index++) { + JigsawStudioMenuState.Workcell workcell = state.workcells().get(index); + boolean selected = workcell.stableId().equals(selectedWorkcellId); + UIElement element = element( + "workcell-" + index, + workcellMaterial(workcell, selected), + (selected ? ChatColor.AQUA : workcell.enabled() ? ChatColor.WHITE : ChatColor.GRAY) + + safe(workcell.displayName())) + .setEnchanted(selected); + element.addLore(workcell.enabled() + ? ChatColor.GREEN + "Enabled" + : ChatColor.RED + "Disabled for assembly and export"); + if (!workcell.canonicalName().equals(workcell.displayName())) { + element.addLore(ChatColor.GRAY + "Solver role: " + safe(workcell.canonicalName())); + } + element.addLore(ChatColor.GRAY + "Capacity: " + dimensions(workcell.capacity())); + element.addLore(ChatColor.GRAY + "Variants: " + workcell.variants().size()); + JigsawStudioMenuState.Variant active = workcell.activeVariant(); + element.addLore(ChatColor.GRAY + "Loaded: " + + (active == null ? "None" : safe(active.displayName()))); + element.addLore(workcellStatus(workcell)); + element.addLore(ChatColor.GRAY + "Connector blocks: " + + (workcell.connectorsVisible() ? "Visible" : "Hidden")); + element.addLore(ChatColor.YELLOW + "Left-click to select and teleport"); + element.addLore(ChatColor.YELLOW + "Right-click for workcell settings"); + if (workcell.dirty() && !workcell.saving()) { + element.addLore(ChatColor.GOLD + "Shift-left: Flush Autosave Now"); + element.onShiftLeftClick(clicked -> flushNow( + window.getViewer(), state.requestId(), workcell.stableId())); + } + element.onLeftClick(clicked -> selectWorkcell( + window.getViewer(), + state.requestId(), + workcell.stableId())); + element.onRightClick(clicked -> J.runEntity( + window.getViewer(), + () -> openWorkcellSettings( + window.getViewer(), + state.requestId(), + workcell.stableId()), + 1)); + window.setElement(start + index, 0, element); + } + } + + private void addVariants( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + int page + ) { + List variants = page( + workcell.variants(), page, VARIANTS_PER_PAGE); + if (variants.isEmpty()) { + UIElement empty = element("no-variants", Material.BARRIER, ChatColor.RED + "No variants"); + empty.addLore(ChatColor.GRAY + "Create a variant to begin authoring this workcell."); + window.setElement(0, 2, empty); + return; + } + + for (int index = 0; index < variants.size(); index++) { + JigsawStudioMenuState.Variant variant = variants.get(index); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 1 + index / GRID_POSITIONS.length; + UIElement element = element( + "variant-" + index, + variantMaterial(variant), + (variant.active() ? ChatColor.GREEN : ChatColor.WHITE) + + safe(variant.displayName()) + + (variant.active() ? ChatColor.AQUA + " [Loaded]" : "")) + .setEnchanted(variant.active()); + element.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); + element.addLore(variant.owned() + ? ChatColor.GREEN + "Owned" + : ChatColor.GRAY + "Read-only"); + element.addLore(ChatColor.GRAY + "Rotation: " + (variant.rotatable() ? "Enabled" : "Disabled")); + element.addLore(ChatColor.GRAY + "Size: " + variant.dimensions() + .map(JigsawStudioMenuController::dimensions) + .orElse("Object missing")); + element.addLore(ChatColor.GRAY + "Themes: " + themes(variant.themes())); + element.addLore(ChatColor.GRAY + "Pool entries: " + variant.memberships().size()); + element.addLore(variant.active() + ? ChatColor.YELLOW + "Loaded; right-click for details" + : ChatColor.YELLOW + "Left-click to load"); + if (!variant.active()) { + element.addLore(ChatColor.YELLOW + "Right-click for details or deletion"); + } + element.onLeftClick(clicked -> switchVariant( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey())); + element.onRightClick(clicked -> J.runEntity( + window.getViewer(), + () -> openDetails( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + 0), + 1)); + window.setElement(position, row, element); + } + } + + private void addMainActions( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + int page + ) { + UIElement create = element("create", Material.NETHER_STAR, ChatColor.GREEN + "New Blank Variant"); + create.addLore(ChatColor.GRAY + "Create an empty owned variant with this cell's current size."); + create.onLeftClick(clicked -> createVariant( + window.getViewer(), state.requestId(), workcell.stableId(), false)); + window.setElement(-4, 5, create); + + JigsawStudioMenuState.Variant active = workcell.activeVariant(); + boolean duplicateAvailable = active != null && active.owned(); + UIElement duplicate = element( + "duplicate", + duplicateAvailable ? Material.PAPER : Material.GRAY_DYE, + duplicateAvailable + ? ChatColor.AQUA + "Duplicate This Cell's Variant" + : active == null + ? ChatColor.GRAY + "Duplicate This Cell's Variant" + : ChatColor.GRAY + "Duplicate This Cell's Variant"); + duplicate.addLore(duplicateAvailable + ? ChatColor.GRAY + "Clone the loaded variant into an owned variant." + : active == null + ? ChatColor.GRAY + "Load a variant before duplicating it." + : ChatColor.GRAY + "Adopt the read-only graph before duplicating it."); + if (duplicateAvailable) { + duplicate.onLeftClick(clicked -> createVariant( + window.getViewer(), state.requestId(), workcell.stableId(), true)); + } + window.setElement(-3, 5, duplicate); + + UIElement settings = element("settings", Material.CRAFTING_TABLE, ChatColor.GOLD + "Workcell Settings"); + settings.addLore(ChatColor.GRAY + "Enabled: " + yesNo(workcell.enabled())); + settings.addLore(ChatColor.GRAY + "Capacity: " + dimensions(workcell.capacity())); + settings.onLeftClick(clicked -> openWorkcellSettings( + window.getViewer(), state.requestId(), workcell.stableId())); + window.setElement(-2, 5, settings); + + int pageCount = pageCount(workcell.variants().size(), VARIANTS_PER_PAGE); + if (page > 0) { + UIElement previous = element("previous", Material.ARROW, ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), page - 1)); + window.setElement(-1, 5, previous); + } + + UIElement pageIndicator = element("page", Material.BOOK, ChatColor.WHITE + "Page " + + (page + 1) + "/" + pageCount); + pageIndicator.addLore(ChatColor.GRAY + "" + workcell.variants().size() + " variants"); + window.setElement(0, 5, pageIndicator); + + if (page + 1 < pageCount) { + UIElement next = element("next", Material.ARROW, ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), page + 1)); + window.setElement(1, 5, next); + } + + window.setElement(2, 5, evaluationElement(state.evaluation())); + + UIElement preview = element("preview", Material.ENDER_EYE, ChatColor.LIGHT_PURPLE + "Go to Preview"); + preview.addLore(ChatColor.GRAY + "Open the deterministic assembly preview."); + preview.onLeftClick(clicked -> goToPreview(window.getViewer(), state.requestId())); + window.setElement(3, 5, preview); + + UIElement toolbox = element("toolbox", Material.STICK, ChatColor.AQUA + "Toolbox"); + toolbox.addLore(ChatColor.GRAY + "Take named sticks bound to this Studio session."); + toolbox.onLeftClick(clicked -> openToolbox( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(4, 5, toolbox); + } + + private void renderWorkcellSettings( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell + ) { + PendingWorkcellResize pendingResize = pendingWorkcellResize( + window.getViewer().getUniqueId(), + state.requestId(), + workcell.stableId()); + JigsawStudioMenuState.Workcell renderedWorkcell = pendingResize == null + ? workcell + : withCapacity(workcell, pendingResize.dimensions()); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("settings-back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); + back.onLeftClick(clicked -> leaveWorkcellSettings( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + window.setElement(-4, 0, back); + + UIElement identity = element( + "settings-identity", + renderedWorkcell.enabled() ? Material.LIME_WOOL : Material.GRAY_WOOL, + ChatColor.AQUA + safe(renderedWorkcell.displayName())); + identity.addLore(ChatColor.DARK_GRAY + safe(renderedWorkcell.stableId())); + identity.addLore(renderedWorkcell.enabled() + ? ChatColor.GREEN + "Enabled" + : ChatColor.RED + "Disabled for assembly and export"); + if (!renderedWorkcell.canonicalName().equals(renderedWorkcell.displayName())) { + identity.addLore(ChatColor.GRAY + "Solver role: " + safe(renderedWorkcell.canonicalName())); + } + identity.addLore(ChatColor.GRAY + "Capacity: " + dimensions(renderedWorkcell.capacity())); + if (pendingResize != null) { + identity.addLore(pendingResize.applying() + ? ChatColor.YELLOW + "Applying one live relayout" + : ChatColor.GOLD + "Pending; apply when all dimensions are ready"); + } + identity.addLore(ChatColor.YELLOW + "Left-click for a rename stick"); + identity.addLore(ChatColor.GRAY + "Rename that stick in an anvil, then right-click it."); + identity.addLore(ChatColor.GRAY + "Sneak-right-click the stick to reset this label."); + identity.onLeftClick(clicked -> actions.giveTool( + window.getViewer(), + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.RENAME_WORKCELL, + state.requestId(), + renderedWorkcell.stableId()))); + window.setElement(0, 0, identity); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + if (state.mode() == JigsawStudioMode.PLANAR_JIGSAW) { + UIElement enabled = element( + "settings-enabled", + renderedWorkcell.enabled() ? Material.LEVER : Material.REDSTONE_TORCH, + renderedWorkcell.enabled() + ? ChatColor.GREEN + "Workcell Enabled" + : ChatColor.RED + "Workcell Disabled"); + enabled.addLore(ChatColor.GRAY + "Disabled workcells remain editable and keep their size."); + enabled.addLore(ChatColor.YELLOW + "Left-click to " + + (renderedWorkcell.enabled() ? "disable" : "enable")); + enabled.onLeftClick(clicked -> setWorkcellEnabled( + window.getViewer(), + state.requestId(), + renderedWorkcell.stableId(), + !renderedWorkcell.enabled())); + window.setElement(0, 1, enabled); + } else { + UIElement spatial = element( + "spatial-bounds", + Material.SCAFFOLDING, + ChatColor.AQUA + "Spatial Workcell Bounds"); + spatial.addLore(ChatColor.GRAY + "Spatial project bounds are shared by the project."); + spatial.addLore(ChatColor.GRAY + "Changing them regenerates the Studio layout."); + window.setElement(0, 1, spatial); + } + + UIElement connectors = element( + "settings-connectors", + renderedWorkcell.connectorsVisible() ? Material.JIGSAW : Material.STRUCTURE_VOID, + renderedWorkcell.connectorsVisible() + ? ChatColor.GREEN + "Connector Blocks Visible" + : ChatColor.YELLOW + "Connector Blocks Hidden"); + connectors.addLore(ChatColor.GRAY + "Hidden connectors retain their metadata and final block state."); + connectors.addLore(ChatColor.YELLOW + "Left-click to " + + (renderedWorkcell.connectorsVisible() ? "hide" : "show") + " connector blocks"); + connectors.onLeftClick(clicked -> toggleConnectorBlocks( + window.getViewer(), + state.requestId(), + renderedWorkcell.stableId(), + !renderedWorkcell.connectorsVisible())); + window.setElement(2, 1, connectors); + + UIElement resetConnectors = element( + "settings-reset-connectors", + Material.RECOVERY_COMPASS, + ChatColor.AQUA + "Reset Connector Blocks"); + resetConnectors.addLore(ChatColor.GRAY + "Restore every connector in this workcell from disk."); + resetConnectors.addLore(ChatColor.GRAY + "Other edited blocks are left unchanged."); + resetConnectors.onLeftClick(clicked -> resetConnectorBlocks( + window.getViewer(), + state.requestId(), + renderedWorkcell.stableId())); + window.setElement(4, 1, resetConnectors); + + window.setElement(-2, 2, axisElement( + window, + state, + renderedWorkcell, + DimensionAxis.WIDTH, + Material.IRON_INGOT)); + window.setElement(0, 2, axisElement( + window, + state, + renderedWorkcell, + DimensionAxis.HEIGHT, + Material.GOLD_INGOT)); + window.setElement(2, 2, axisElement( + window, + state, + renderedWorkcell, + DimensionAxis.DEPTH, + Material.COPPER_INGOT)); + + if (pendingResize != null) { + UIElement apply = element( + "settings-apply-capacity", + pendingResize.applying() ? Material.CLOCK : Material.EMERALD_BLOCK, + pendingResize.applying() + ? ChatColor.YELLOW + "Applying Cell Size" + : ChatColor.GREEN + "Apply Cell Size"); + apply.addLore(ChatColor.WHITE + dimensions(pendingResize.dimensions())); + apply.addLore(ChatColor.GRAY + "Regenerates the layout once after all size edits."); + if (!pendingResize.applying()) { + apply.onLeftClick(clicked -> applyWorkcellResize( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + } + window.setElement(0, 3, apply); + + if (!pendingResize.applying()) { + UIElement discard = element( + "settings-discard-capacity", + Material.BARRIER, + ChatColor.RED + "Discard Size Changes"); + discard.onLeftClick(clicked -> discardWorkcellResize( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + window.setElement(2, 3, discard); + } + } + + UIElement footerBack = element("settings-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> leaveWorkcellSettings( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + window.setElement(-4, 5, footerBack); + + UIElement undo = element( + "settings-undo", + Material.CLOCK, + ChatColor.LIGHT_PURPLE + "Undo Last Autosave"); + undo.addLore(ChatColor.GRAY + "Restore the previous owned graph iteration."); + undo.addLore(ChatColor.GRAY + "Up to five autosave iterations are retained on disk."); + undo.onLeftClick(clicked -> undoAutosave( + window.getViewer(), + state.requestId())); + window.setElement(0, 5, undo); + + if (renderedWorkcell.dirty() && !renderedWorkcell.saving()) { + UIElement saveNow = element( + "save-now", + Material.EMERALD, + ChatColor.GOLD + "Flush Autosave Now"); + saveNow.addLore(ChatColor.GRAY + "Autosave is automatic."); + saveNow.addLore(ChatColor.GRAY + "Use this only to flush pending work or recover immediately."); + saveNow.onLeftClick(clicked -> flushNow( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + window.setElement(2, 5, saveNow); + } + + UIElement preview = element( + "settings-preview", + Material.ENDER_EYE, + ChatColor.LIGHT_PURPLE + "Go to Preview"); + preview.onLeftClick(clicked -> goToPreview(window.getViewer(), state.requestId())); + window.setElement(3, 5, preview); + + UIElement toolbox = element("settings-toolbox", Material.STICK, ChatColor.AQUA + "Toolbox"); + toolbox.onLeftClick(clicked -> openToolbox( + window.getViewer(), state.requestId(), renderedWorkcell.stableId(), 0)); + window.setElement(4, 5, toolbox); + }); + } + + private void renderVariantSizeSettings( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant + ) { + JigsawStudioCellDimensions current = variant.dimensions().orElseThrow(); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("variant-size-back", Material.ARROW, ChatColor.YELLOW + "Back to Details"); + back.onLeftClick(clicked -> openDetails( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + window.setElement(-4, 0, back); + + UIElement identity = element( + "variant-size-identity", + Material.SCAFFOLDING, + ChatColor.AQUA + safe(variant.displayName()) + " Size"); + identity.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); + identity.addLore(ChatColor.WHITE + "Current: " + dimensions(current)); + identity.addLore(ChatColor.GRAY + "Workcell capacity: " + dimensions(workcell.capacity())); + identity.addLore(ChatColor.GRAY + "Only this variant object and its edge connectors change."); + window.setElement(0, 0, identity); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + window.setElement(-2, 2, variantAxisElement( + window, state, workcell, variant, DimensionAxis.WIDTH, Material.IRON_INGOT)); + window.setElement(0, 2, variantAxisElement( + window, state, workcell, variant, DimensionAxis.HEIGHT, Material.GOLD_INGOT)); + window.setElement(2, 2, variantAxisElement( + window, state, workcell, variant, DimensionAxis.DEPTH, Material.COPPER_INGOT)); + + UIElement capacity = element( + "variant-size-capacity", + variant.resizableToCapacity() ? Material.NETHER_STAR : Material.GRAY_DYE, + variant.resizableToCapacity() + ? ChatColor.GREEN + "Resize This Variant to Capacity" + : ChatColor.GRAY + "Variant Already Matches Capacity"); + capacity.addLore(ChatColor.GRAY + dimensions(workcell.capacity())); + if (variant.resizableToCapacity()) { + capacity.addLore(ChatColor.YELLOW + "Left-click to resize only this variant"); + capacity.onLeftClick(clicked -> resizeVariant( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + workcell.capacity())); + } + window.setElement(0, 3, capacity); + + UIElement footerBack = element("variant-size-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> openDetails( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + window.setElement(-4, 5, footerBack); + }); + } + + private UIElement variantAxisElement( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + DimensionAxis axis, + Material material + ) { + JigsawStudioCellDimensions current = variant.dimensions().orElseThrow(); + UIElement element = element( + "variant-axis-" + axis.name().toLowerCase(Locale.ROOT), + material, + ChatColor.AQUA + axis.displayName() + ": " + axis.value(current)); + element.addLore(ChatColor.GREEN + "Left-click: +1"); + element.addLore(ChatColor.YELLOW + "Right-click: -1"); + element.addLore(ChatColor.GREEN + "Shift-left: +8"); + element.addLore(ChatColor.YELLOW + "Shift-right: -8"); + element.addLore(ChatColor.GRAY + "Maximum: " + axis.value(workcell.capacity())); + element.onLeftClick(clicked -> resizeVariantAxis( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, 1)); + element.onRightClick(clicked -> resizeVariantAxis( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, -1)); + element.onShiftLeftClick(clicked -> resizeVariantAxis( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, 8)); + element.onShiftRightClick(clicked -> resizeVariantAxis( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, -8)); + return element; + } + + private void renderStructureSettings( + UIWindow window, + JigsawStudioMenuState state, + String selectedWorkcellId, + int requestedPage + ) { + int page = clampPage(requestedPage, state.themeSets().size(), THEME_SETS_PER_PAGE); + purgeExpiredConfirmations(window.getViewer().getUniqueId()); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("structure-back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); + back.onLeftClick(clicked -> refreshMain( + window.getViewer(), state.requestId(), selectedWorkcellId, 0)); + window.setElement(-4, 0, back); + + UIElement identity = element( + "structure-identity", + Material.JIGSAW, + ChatColor.LIGHT_PURPLE + "Structure Themes & Caps"); + identity.addLore(ChatColor.DARK_GRAY + safe(state.structureKey())); + identity.addLore(ChatColor.GRAY + "Compatibility: " + compatibilityName(state.compatibilityTarget())); + identity.addLore(ChatColor.GRAY + "Theme sets select one coherent variant family per assembly."); + identity.addLore(ChatColor.GRAY + "Empty piece themes remain available to every family."); + window.setElement(0, 0, identity); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + UIElement requireCaps = element( + "require-caps", + state.irisExtended() + ? state.requireCaps() ? Material.IRON_BARS : Material.TRIPWIRE_HOOK + : Material.BARRIER, + !state.irisExtended() + ? ChatColor.GRAY + "Mandatory Caps Require Iris Compatibility" + : state.requireCaps() + ? ChatColor.GREEN + "Mandatory Caps Enabled" + : ChatColor.YELLOW + "Mandatory Caps Disabled"); + requireCaps.addLore(!state.irisExtended() + ? ChatColor.GRAY + "Vanilla-portable export rejects Iris mandatory-cap metadata." + : state.requireCaps() + ? ChatColor.GRAY + "Every open connector must close with a terminal piece." + : ChatColor.GRAY + "Assembly may leave an open connector when no cap can fit."); + if (state.irisExtended()) { + requireCaps.addLore(ChatColor.YELLOW + "Left-click to toggle"); + requireCaps.onLeftClick(clicked -> setRequireCaps( + window.getViewer(), state.requestId(), !state.requireCaps())); + } + window.setElement(-3, 1, requireCaps); + + String nextThemeKey = nextThemeSetKey(state.themeSets()); + boolean themeEditingAvailable = state.irisExtended(); + UIElement createTheme = element( + "new-theme-set", + themeEditingAvailable ? Material.NETHER_STAR : Material.BARRIER, + themeEditingAvailable + ? ChatColor.GREEN + "Duplicate All Enabled Cells as Family: " + safe(nextThemeKey) + : ChatColor.GRAY + "Theme Sets Require Iris Compatibility"); + if (themeEditingAvailable) { + createTheme.addLore(ChatColor.GRAY + "Creates one new owned variant for every enabled workcell."); + createTheme.addLore(ChatColor.GRAY + "All created variants join " + safe(nextThemeKey) + "."); + createTheme.addLore(ChatColor.GRAY + "One family is selected for the complete assembly."); + createTheme.addLore(ChatColor.YELLOW + "Left-click to create the complete set"); + createTheme.onLeftClick(clicked -> duplicateActiveFamily( + window.getViewer(), state.requestId(), nextThemeKey)); + } else { + createTheme.addLore(ChatColor.GRAY + "Vanilla jigsaw resources cannot encode Iris theme metadata."); + } + window.setElement(0, 1, createTheme); + + boolean deleteConfirmed = isProjectDeleteConfirmed( + window.getViewer().getUniqueId(), state.requestId()); + UIElement deleteProject = element( + "delete-project", + deleteConfirmed ? Material.REDSTONE : Material.LAVA_BUCKET, + deleteConfirmed + ? ChatColor.RED + "Confirm Delete Jigsaw" + : ChatColor.RED + "Delete Jigsaw") + .setEnchanted(deleteConfirmed); + deleteProject.addLore(deleteConfirmed + ? ChatColor.RED + "Click again to delete this entire managed project." + : ChatColor.RED + "Click twice within 10 seconds to confirm."); + deleteProject.onLeftClick(clicked -> confirmOrDeleteProject( + window.getViewer(), state.requestId(), selectedWorkcellId, page)); + window.setElement(3, 1, deleteProject); + + addThemeSets(window, state, page); + + UIElement footerBack = element("structure-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> refreshMain( + window.getViewer(), state.requestId(), selectedWorkcellId, 0)); + window.setElement(-4, 5, footerBack); + + int pageCount = pageCount(state.themeSets().size(), THEME_SETS_PER_PAGE); + if (page > 0) { + UIElement previous = element( + "theme-previous", + Material.ARROW, + ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> refreshStructureSettings( + window.getViewer(), state.requestId(), selectedWorkcellId, page - 1)); + window.setElement(-1, 5, previous); + } + UIElement indicator = element( + "theme-page", + Material.BOOK, + ChatColor.WHITE + "Page " + (page + 1) + "/" + pageCount); + indicator.addLore(ChatColor.GRAY + "" + state.themeSets().size() + " theme sets"); + window.setElement(0, 5, indicator); + if (page + 1 < pageCount) { + UIElement next = element("theme-next", Material.ARROW, ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> refreshStructureSettings( + window.getViewer(), state.requestId(), selectedWorkcellId, page + 1)); + window.setElement(1, 5, next); + } + window.setElement(4, 5, evaluationElement(state.evaluation())); + }); + } + + private void addThemeSets( + UIWindow window, + JigsawStudioMenuState state, + int page + ) { + List themeSets = page( + state.themeSets(), page, THEME_SETS_PER_PAGE); + if (themeSets.isEmpty()) { + UIElement empty = element( + "no-theme-sets", + Material.GRAY_DYE, + ChatColor.GRAY + "Implicit Unthemed Assembly"); + empty.addLore(ChatColor.GRAY + "Create variant-1 to begin coherent theme selection."); + window.setElement(0, 3, empty); + return; + } + for (int index = 0; index < themeSets.size(); index++) { + JigsawStudioMenuState.ThemeSet themeSet = themeSets.get(index); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 2 + index / GRID_POSITIONS.length; + UIElement element = element( + "theme-set-" + index, + state.irisExtended() ? Material.PURPLE_DYE : Material.GRAY_DYE, + (state.irisExtended() ? ChatColor.LIGHT_PURPLE : ChatColor.GRAY) + safe(themeSet.key())); + element.addLore(ChatColor.WHITE + "Selection weight: " + themeSet.weight()); + element.addLore(ChatColor.WHITE + "Whole-assembly chance: " + + themeSelectionPercent(state.themeSets(), themeSet)); + if (state.irisExtended()) { + element.addLore(ChatColor.GREEN + "Left-click: weight +1"); + element.addLore(ChatColor.YELLOW + "Right-click: weight -1"); + element.addLore(ChatColor.GREEN + "Shift-left: weight +8"); + element.addLore(ChatColor.YELLOW + "Shift-right: weight -8"); + element.onLeftClick(clicked -> adjustThemeSetWeight( + window.getViewer(), state.requestId(), themeSet, 1)); + element.onRightClick(clicked -> adjustThemeSetWeight( + window.getViewer(), state.requestId(), themeSet, -1)); + element.onShiftLeftClick(clicked -> adjustThemeSetWeight( + window.getViewer(), state.requestId(), themeSet, 8)); + element.onShiftRightClick(clicked -> adjustThemeSetWeight( + window.getViewer(), state.requestId(), themeSet, -8)); + } else { + element.addLore(ChatColor.GRAY + "Theme metadata is unavailable for vanilla-portable graphs."); + } + window.setElement(position, row, element); + } + } + + private UIElement axisElement( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + DimensionAxis axis, + Material material + ) { + UIElement element = element( + "axis-" + axis.name().toLowerCase(Locale.ROOT), + material, + ChatColor.AQUA + axis.displayName() + " Capacity: " + axis.value(workcell.capacity())); + element.addLore(ChatColor.GREEN + "Left-click: +1"); + element.addLore(ChatColor.YELLOW + "Right-click: -1"); + element.addLore(ChatColor.GREEN + "Shift-left: +8"); + element.addLore(ChatColor.YELLOW + "Shift-right: -8"); + element.addLore(ChatColor.GRAY + "Changes stay in this menu until Apply Cell Size."); + element.onLeftClick(clicked -> resizeWorkcell( + window.getViewer(), state.requestId(), workcell.stableId(), axis, 1)); + element.onRightClick(clicked -> resizeWorkcell( + window.getViewer(), state.requestId(), workcell.stableId(), axis, -1)); + element.onShiftLeftClick(clicked -> resizeWorkcell( + window.getViewer(), state.requestId(), workcell.stableId(), axis, 8)); + element.onShiftRightClick(clicked -> resizeWorkcell( + window.getViewer(), state.requestId(), workcell.stableId(), axis, -8)); + return element; + } + + private void renderDetails( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int requestedPage + ) { + int page = clampPage(requestedPage, variant.memberships().size(), MEMBERSHIPS_PER_PAGE); + purgeExpiredConfirmations(window.getViewer().getUniqueId()); + window.batch(() -> { + window.clearElements(); + addDetailsHeader(window, state, workcell, variant, page); + addMemberships(window, state, workcell, variant, page); + addDetailsFooter(window, state, workcell, variant, page); + }); + } + + private void addDetailsHeader( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int page + ) { + UIElement back = element("back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); + back.onLeftClick(clicked -> refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(-4, 0, back); + + UIElement identity = element("identity", Material.NAME_TAG, ChatColor.AQUA + safe(variant.displayName())); + identity.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); + identity.addLore(variant.owned() + ? ChatColor.GREEN + "Owned variant" + : ChatColor.GRAY + "Read-only variant"); + identity.addLore(variant.active() + ? ChatColor.GREEN + "Loaded in this workcell" + : ChatColor.GRAY + "Not currently loaded"); + identity.addLore(ChatColor.GRAY + "Size: " + variant.dimensions() + .map(JigsawStudioMenuController::dimensions) + .orElse("Object missing")); + identity.addLore(ChatColor.GRAY + "Themes: " + themes(variant.themes())); + identity.addLore(ChatColor.GRAY + "Depth: " + variant.rules().minimumDepth() + + "-" + variant.rules().maximumDepth()); + identity.addLore(ChatColor.GRAY + "Placements: " + variant.rules().minimumPlacements() + + "-" + maximumPlacements(variant.rules().maximumPlacements())); + identity.addLore(ChatColor.GRAY + "Terminal: " + yesNo(variant.rules().terminal())); + identity.addLore(ChatColor.GRAY + "Pool entries: " + variant.memberships().size()); + if (variant.owned()) { + identity.addLore(ChatColor.YELLOW + "Left-click for a rename stick"); + identity.addLore(ChatColor.GRAY + "Rename that stick in an anvil, then right-click it."); + identity.addLore(ChatColor.GRAY + "Sneak-right-click the stick to reset this label."); + identity.onLeftClick(clicked -> actions.giveTool( + window.getViewer(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RENAME_VARIANT, + state.requestId(), + workcell.stableId(), + variant.pieceKey()))); + } + window.setElement(0, 0, identity); + + boolean activeOwned = variant.active() && variant.owned(); + boolean irisRuleEditing = activeOwned && state.irisExtended(); + UIElement settings = element( + "variant-settings", + irisRuleEditing ? Material.COMPARATOR : Material.GRAY_DYE, + irisRuleEditing + ? ChatColor.GOLD + "Themes & Piece Rules" + : ChatColor.GRAY + "Themes & Rules Unavailable"); + settings.addLore(irisRuleEditing + ? ChatColor.GRAY + "Edit this loaded variant's theme membership and placement rules." + : state.irisExtended() + ? ChatColor.GRAY + "Load an owned variant before editing its rules." + : ChatColor.GRAY + "Vanilla-portable pieces cannot encode Iris theme or rule metadata."); + if (irisRuleEditing) { + settings.addLore(ChatColor.YELLOW + "Left-click to edit"); + settings.onLeftClick(clicked -> openVariantSettings( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + } + window.setElement(-2, 0, settings); + + boolean rotationEditable = variant.active() && variant.rotationEditable(); + UIElement rotation = element( + "rotation", + rotationEditable ? Material.REPEATER : Material.BARRIER, + rotationEditable + ? ChatColor.LIGHT_PURPLE + "Toggle Rotation" + : ChatColor.GRAY + "Rotation is Read-only"); + rotation.addLore(ChatColor.GRAY + "Currently: " + (variant.rotatable() ? "Enabled" : "Disabled")); + if (rotationEditable) { + rotation.addLore(ChatColor.YELLOW + "Left-click to toggle"); + rotation.onLeftClick(clicked -> toggleRotation( + window.getViewer(), state.requestId(), workcell.stableId())); + } else if (!variant.active()) { + rotation.addLore(ChatColor.GRAY + "Load this variant before editing rotation."); + } else if (variant.owned() && variant.rotatable()) { + rotation.addLore(ChatColor.GRAY + "Vanilla-portable variants must remain rotatable."); + } + window.setElement(3, 0, rotation); + + boolean sizeEditable = variant.owned() && variant.dimensions().isPresent(); + UIElement size = element( + "variant-size", + sizeEditable ? Material.SCAFFOLDING : Material.BARRIER, + sizeEditable + ? ChatColor.GREEN + "Edit This Variant's Size" + : ChatColor.GRAY + "Variant Size is Read-only"); + size.addLore(ChatColor.GRAY + "Current: " + variant.dimensions() + .map(JigsawStudioMenuController::dimensions) + .orElse("Object missing")); + size.addLore(ChatColor.GRAY + "Workcell capacity: " + dimensions(workcell.capacity())); + if (sizeEditable) { + size.addLore(ChatColor.YELLOW + "Left-click for width, height, and depth controls"); + size.onLeftClick(clicked -> openVariantSizeSettings( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey())); + if (variant.resizableToCapacity()) { + size.addLore(ChatColor.YELLOW + "Shift-left: resize this variant to capacity"); + size.onShiftLeftClick(clicked -> resizeVariant( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + workcell.capacity())); + } + } + window.setElement(2, 0, size); + + boolean lastVariant = workcell.variants().size() <= 1; + boolean deletionAvailable = variant.owned() && !variant.active() && !lastVariant; + boolean confirmed = deletionAvailable && isDeleteConfirmed( + window.getViewer().getUniqueId(), + state.requestId(), + workcell.stableId(), + variant.pieceKey()); + UIElement delete = element( + "delete-variant", + deletionAvailable ? confirmed ? Material.REDSTONE : Material.LAVA_BUCKET : Material.BARRIER, + deletionAvailable + ? confirmed ? ChatColor.RED + "Confirm Delete Variant" : ChatColor.RED + "Delete Variant" + : variant.active() + ? ChatColor.GRAY + "Loaded Variant Cannot Be Deleted" + : lastVariant + ? ChatColor.GRAY + "Last Variant Cannot Be Deleted" + : ChatColor.GRAY + "Deletion is Read-only") + .setEnchanted(confirmed); + if (deletionAvailable) { + delete.addLore(confirmed + ? ChatColor.RED + "Click again to permanently delete this owned variant." + : ChatColor.RED + "Click twice within 10 seconds to confirm."); + delete.onLeftClick(clicked -> confirmOrDeleteVariant( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page)); + } else if (variant.active()) { + delete.addLore(ChatColor.GRAY + "Load another variant in this workcell first."); + } else if (lastVariant) { + delete.addLore(ChatColor.GRAY + "Create another variant before deleting this one."); + } + window.setElement(4, 0, delete); + } + + private void addMemberships( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int page + ) { + List memberships = page( + variant.memberships(), page, MEMBERSHIPS_PER_PAGE); + if (memberships.isEmpty()) { + UIElement empty = element("no-memberships", Material.BARRIER, ChatColor.RED + "No Pool Entries"); + empty.addLore(ChatColor.GRAY + "This variant is not linked from an owned pool."); + window.setElement(0, 2, empty); + return; + } + + for (int index = 0; index < memberships.size(); index++) { + JigsawStudioMenuState.Membership membership = memberships.get(index); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 1 + index / GRID_POSITIONS.length; + boolean confirmed = variant.active() && variant.owned() && isUnlinkConfirmed( + window.getViewer().getUniqueId(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + membership); + UIElement element = element( + "membership-" + index, + confirmed ? Material.REDSTONE : Material.GOLD_NUGGET, + (confirmed ? ChatColor.RED : ChatColor.GOLD) + + safe(displayKey(membership.poolKey())) + + " [" + membership.entryIndex() + "]") + .setEnchanted(confirmed); + element.addLore(ChatColor.DARK_GRAY + safe(membership.poolKey())); + element.addLore(ChatColor.GRAY + "Exact entry: " + membership.entryIndex()); + element.addLore(ChatColor.WHITE + "Weight: " + membership.weight()); + element.addLore((state.irisExtended() ? ChatColor.WHITE : ChatColor.GRAY) + + "Chance: " + chance(membership.chance()) + + (state.irisExtended() ? "" : " (Iris only)")); + if (variant.active() && variant.owned()) { + element.addLore(ChatColor.GREEN + "Left-click: weight +1"); + element.addLore(ChatColor.YELLOW + "Right-click: weight -1"); + if (state.irisExtended()) { + element.addLore(ChatColor.GREEN + "Shift-left: chance +" + + CHANCE_STEP_PERCENTAGE_POINTS + "%"); + element.addLore(ChatColor.YELLOW + "Shift-right: chance -" + + CHANCE_STEP_PERCENTAGE_POINTS + "%"); + } else { + element.addLore(ChatColor.GRAY + "Vanilla-portable pool entries always have 100% chance."); + } + element.addLore(confirmed + ? ChatColor.RED + "Middle-click again to unlink" + : ChatColor.RED + "Middle-click twice to unlink"); + element.onLeftClick(clicked -> adjustMembershipWeight( + window.getViewer(), state.requestId(), workcell.stableId(), membership, 1)); + element.onRightClick(clicked -> adjustMembershipWeight( + window.getViewer(), state.requestId(), workcell.stableId(), membership, -1)); + if (state.irisExtended()) { + element.onShiftLeftClick(clicked -> adjustMembershipChance( + window.getViewer(), + state.requestId(), + workcell.stableId(), + membership, + CHANCE_STEP_PERCENTAGE_POINTS)); + element.onShiftRightClick(clicked -> adjustMembershipChance( + window.getViewer(), + state.requestId(), + workcell.stableId(), + membership, + -CHANCE_STEP_PERCENTAGE_POINTS)); + } + element.onMiddleClick(clicked -> confirmOrUnlink( + window.getViewer(), state.requestId(), workcell.stableId(), membership, page)); + } else { + element.addLore(variant.active() + ? ChatColor.GRAY + "Read-only pool entry" + : ChatColor.GRAY + "Load this variant to edit its pool entry"); + } + window.setElement(position, row, element); + } + } + + private void addDetailsFooter( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int page + ) { + UIElement back = element("footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + back.onLeftClick(clicked -> refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(-4, 5, back); + + int pageCount = pageCount(variant.memberships().size(), MEMBERSHIPS_PER_PAGE); + if (page > 0) { + UIElement previous = element("membership-previous", Material.ARROW, ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> refreshDetails( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page - 1)); + window.setElement(-1, 5, previous); + } + UIElement indicator = element("membership-page", Material.BOOK, ChatColor.WHITE + "Page " + + (page + 1) + "/" + pageCount); + indicator.addLore(ChatColor.GRAY + "" + variant.memberships().size() + " exact pool entries"); + window.setElement(0, 5, indicator); + if (page + 1 < pageCount) { + UIElement next = element("membership-next", Material.ARROW, ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> refreshDetails( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page + 1)); + window.setElement(1, 5, next); + } + window.setElement(2, 5, evaluationElement(state.evaluation())); + + UIElement toolbox = element("details-toolbox", Material.STICK, ChatColor.AQUA + "Toolbox"); + toolbox.onLeftClick(clicked -> openToolbox( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(4, 5, toolbox); + } + + private void renderVariantSettings( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int requestedPage + ) { + int page = clampPage(requestedPage, state.themeSets().size(), THEME_SETS_PER_PAGE); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("variant-settings-back", Material.ARROW, ChatColor.YELLOW + "Back to Details"); + back.onLeftClick(clicked -> openDetails( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + window.setElement(-4, 0, back); + + UIElement identity = element( + "variant-settings-identity", + Material.COMPARATOR, + ChatColor.GOLD + safe(variant.displayName()) + " Rules"); + identity.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); + identity.addLore(ChatColor.GRAY + "Theme memberships: " + themes(variant.themes())); + identity.addLore(ChatColor.GRAY + "Use Duplicate All Enabled Cells as Family for coherent sets."); + window.setElement(0, 0, identity); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + if (!variant.active() || !variant.owned() || !state.irisExtended()) { + UIElement unavailable = element( + "variant-settings-unavailable", + Material.BARRIER, + state.irisExtended() + ? ChatColor.RED + "Load an Owned Variant First" + : ChatColor.GRAY + "Iris Metadata Unavailable"); + unavailable.addLore(state.irisExtended() + ? ChatColor.GRAY + "Rules and themes can only change for the loaded owned variant." + : ChatColor.GRAY + "Vanilla-portable pieces use vanilla placement behavior."); + window.setElement(0, 2, unavailable); + } else { + window.setElement(-4, 1, ruleElement( + window, state, workcell, variant, RuleField.MINIMUM_DEPTH)); + window.setElement(-2, 1, ruleElement( + window, state, workcell, variant, RuleField.MAXIMUM_DEPTH)); + window.setElement(0, 1, ruleElement( + window, state, workcell, variant, RuleField.MINIMUM_PLACEMENTS)); + window.setElement(2, 1, ruleElement( + window, state, workcell, variant, RuleField.MAXIMUM_PLACEMENTS)); + + UIElement terminal = element( + "rule-terminal", + variant.rules().terminal() ? Material.REDSTONE_TORCH : Material.LEVER, + variant.rules().terminal() + ? ChatColor.GREEN + "Terminal Piece: Yes" + : ChatColor.YELLOW + "Terminal Piece: No"); + terminal.addLore(ChatColor.GRAY + "Terminal pieces consume a connector without expanding."); + terminal.addLore(ChatColor.YELLOW + "Left-click to toggle"); + terminal.onLeftClick(clicked -> updateVariantRules( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + withTerminal(variant.rules(), !variant.rules().terminal()))); + window.setElement(4, 1, terminal); + + addVariantThemeMemberships(window, state, workcell, variant, page); + } + + UIElement footerBack = element("variant-rules-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> openDetails( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + window.setElement(-4, 5, footerBack); + + int pageCount = pageCount(state.themeSets().size(), THEME_SETS_PER_PAGE); + if (page > 0) { + UIElement previous = element( + "variant-theme-previous", + Material.ARROW, + ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> refreshVariantSettings( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page - 1)); + window.setElement(-1, 5, previous); + } + UIElement indicator = element( + "variant-theme-page", + Material.BOOK, + ChatColor.WHITE + "Theme Page " + (page + 1) + "/" + pageCount); + indicator.addLore(ChatColor.GRAY + "" + state.themeSets().size() + " declared themes"); + window.setElement(0, 5, indicator); + if (page + 1 < pageCount) { + UIElement next = element( + "variant-theme-next", + Material.ARROW, + ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> refreshVariantSettings( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page + 1)); + window.setElement(1, 5, next); + } + window.setElement(4, 5, evaluationElement(state.evaluation())); + }); + } + + private UIElement ruleElement( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + RuleField field + ) { + int value = field.value(variant.rules()); + UIElement element = element( + "rule-" + field.name().toLowerCase(Locale.ROOT), + field.material(), + ChatColor.AQUA + field.displayName() + ": " + field.displayValue(value)); + boolean unlimitedMaximum = field == RuleField.MAXIMUM_PLACEMENTS && value == 0; + if (unlimitedMaximum) { + element.addLore(ChatColor.GRAY + "Increase is already unlimited."); + } else { + element.addLore(ChatColor.GREEN + "Left-click: +1"); + element.addLore(ChatColor.GREEN + "Shift-left: +" + field.shiftStep()); + element.onLeftClick(clicked -> adjustVariantRule( + window.getViewer(), state.requestId(), workcell, variant, field, 1)); + element.onShiftLeftClick(clicked -> adjustVariantRule( + window.getViewer(), state.requestId(), workcell, variant, field, field.shiftStep())); + } + element.addLore(unlimitedMaximum + ? ChatColor.YELLOW + "Right-click: set 512" + : ChatColor.YELLOW + "Right-click: -1"); + element.addLore(unlimitedMaximum + ? ChatColor.YELLOW + "Shift-right: set 497" + : ChatColor.YELLOW + "Shift-right: -" + field.shiftStep()); + element.onRightClick(clicked -> adjustVariantRule( + window.getViewer(), state.requestId(), workcell, variant, field, -1)); + element.onShiftRightClick(clicked -> adjustVariantRule( + window.getViewer(), state.requestId(), workcell, variant, field, -field.shiftStep())); + return element; + } + + private void addVariantThemeMemberships( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int page + ) { + List themeSets = page( + state.themeSets(), page, THEME_SETS_PER_PAGE); + if (themeSets.isEmpty()) { + UIElement empty = element( + "variant-no-themes", + Material.GRAY_DYE, + ChatColor.GRAY + "Available to Every Theme"); + empty.addLore(ChatColor.GRAY + "Create a coherent theme set from Structure Rules first."); + window.setElement(0, 3, empty); + return; + } + for (int index = 0; index < themeSets.size(); index++) { + JigsawStudioMenuState.ThemeSet themeSet = themeSets.get(index); + boolean member = variant.themes().contains(themeSet.key()); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 2 + index / GRID_POSITIONS.length; + UIElement element = element( + "variant-theme-" + index, + member ? Material.LIME_DYE : Material.GRAY_DYE, + (member ? ChatColor.GREEN : ChatColor.GRAY) + safe(themeSet.key())) + .setEnchanted(member); + element.addLore(member + ? ChatColor.GREEN + "This variant belongs to the theme." + : ChatColor.GRAY + "This variant does not belong to the theme."); + element.addLore(ChatColor.GRAY + "Only variants in the selected family are eligible."); + element.addLore(ChatColor.YELLOW + "Left-click to toggle membership"); + element.onLeftClick(clicked -> toggleVariantTheme( + window.getViewer(), state.requestId(), workcell, variant, themeSet.key())); + window.setElement(position, row, element); + } + } + + private void renderToolbox( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + int requestedPage + ) { + List allTools = toolboxTools(state, workcell); + int page = clampPage(requestedPage, allTools.size(), TOOLS_PER_PAGE); + List tools = page(allTools, page, TOOLS_PER_PAGE); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("toolbox-back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); + back.onLeftClick(clicked -> refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(-4, 0, back); + + UIElement heading = element("toolbox-heading", Material.CHEST, ChatColor.AQUA + "Bound Tool Sticks"); + heading.addLore(ChatColor.GRAY + "Every stick is bound to request " + state.requestId()); + heading.addLore(ChatColor.GRAY + "Stale sticks stop working when this Studio closes."); + window.setElement(0, 0, heading); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + for (int index = 0; index < tools.size(); index++) { + ToolboxTool tool = tools.get(index); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 1 + index / GRID_POSITIONS.length; + UIElement element = element( + "tool-" + index, + Material.STICK, + ChatColor.AQUA + safe(tool.displayName())); + element.addLore(ChatColor.GRAY + "Action: " + tool.payload().action().displayName()); + addToolBindingLore(element, tool.payload()); + element.addLore(tool.payload().action().destructive() + ? ChatColor.RED + "Bound tools require confirmation when used." + : ChatColor.YELLOW + "Left-click to receive this named stick."); + element.onLeftClick(clicked -> giveTool( + window.getViewer(), state.requestId(), tool.payload())); + window.setElement(position, row, element); + } + + UIElement footerBack = element("toolbox-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(-4, 5, footerBack); + + int pageCount = pageCount(allTools.size(), TOOLS_PER_PAGE); + if (page > 0) { + UIElement previous = element("toolbox-previous", Material.ARROW, ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> refreshToolbox( + window.getViewer(), state.requestId(), workcell.stableId(), page - 1)); + window.setElement(-1, 5, previous); + } + UIElement indicator = element("toolbox-page", Material.BOOK, ChatColor.WHITE + "Page " + + (page + 1) + "/" + pageCount); + indicator.addLore(ChatColor.GRAY + "" + allTools.size() + " bound tools"); + window.setElement(0, 5, indicator); + if (page + 1 < pageCount) { + UIElement next = element("toolbox-next", Material.ARROW, ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> refreshToolbox( + window.getViewer(), state.requestId(), workcell.stableId(), page + 1)); + window.setElement(1, 5, next); + } + }); + } + + private void selectWorkcell(Player player, UUID requestId, String workcellId) { + if (matchingState(player, requestId, true).isEmpty()) { + return; + } + if (actions.teleportToWorkcell(player, workcellId)) { + clearConfirmations(player.getUniqueId()); + closeAfterAction(player); + } + } + + private void toggleConnectorBlocks( + Player player, + UUID requestId, + String workcellId, + boolean visible + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty() || current.get().workcell(workcellId) == null) { + return; + } + if (actions.setConnectorBlocksVisible(player, workcellId, visible)) { + closeAfterAction(player); + } + } + + private void resetConnectorBlocks(Player player, UUID requestId, String workcellId) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty() || current.get().workcell(workcellId) == null) { + return; + } + if (actions.resetConnectorBlocks(player, workcellId)) { + closeAfterAction(player); + } + } + + private void undoAutosave(Player player, UUID requestId) { + if (matchingState(player, requestId, true).isEmpty()) { + return; + } + if (actions.undoAutosave(player)) { + closeAfterAction(player); + } + } + + private void switchVariant(Player player, UUID requestId, String workcellId, String pieceKey) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + if (variant == null) { + stale(player); + return; + } + if (variant.active()) { + player.sendMessage(ChatColor.YELLOW + "That variant is already loaded. Right-click it for details."); + return; + } + if (actions.switchVariant(player, workcellId, pieceKey, false)) { + closeAfterAction(player); + } + } + + private void openDetails( + Player player, + UUID requestId, + String workcellId, + String pieceKey, + int page + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + if (variant == null) { + stale(player); + return; + } + UIWindow window = windows.get(player.getUniqueId()); + if (window != null) { + renderDetails(window, current.get(), workcell, variant, page); + } + } + + private void openWorkcellSettings(Player player, UUID requestId, String workcellId) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + UIWindow window = windows.get(player.getUniqueId()); + if (workcell == null || window == null) { + stale(player); + return; + } + renderWorkcellSettings(window, current.get(), workcell); + } + + private void openStructureSettings( + Player player, + UUID requestId, + String selectedWorkcellId, + int page + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + UIWindow window = windows.get(player.getUniqueId()); + if (current.get().workcell(selectedWorkcellId) == null || window == null) { + stale(player); + return; + } + renderStructureSettings(window, current.get(), selectedWorkcellId, page); + } + + private void openVariantSettings( + Player player, + UUID requestId, + String workcellId, + String pieceKey, + int page + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + UIWindow window = windows.get(player.getUniqueId()); + if (workcell == null || variant == null || window == null) { + stale(player); + return; + } + if (!variant.active() || !variant.owned()) { + player.sendMessage(ChatColor.YELLOW + "Load an owned variant before editing its rules."); + return; + } + if (!current.get().irisExtended()) { + player.sendMessage(ChatColor.YELLOW + + "Vanilla-portable pieces cannot encode Iris theme or piece-rule metadata."); + return; + } + renderVariantSettings(window, current.get(), workcell, variant, page); + } + + private void openVariantSizeSettings( + Player player, + UUID requestId, + String workcellId, + String pieceKey + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + UIWindow window = windows.get(player.getUniqueId()); + if (workcell == null || variant == null || !variant.owned() + || variant.dimensions().isEmpty() || window == null) { + stale(player); + return; + } + renderVariantSizeSettings(window, current.get(), workcell, variant); + } + + private void openToolbox(Player player, UUID requestId, String workcellId, int page) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + UIWindow window = windows.get(player.getUniqueId()); + if (workcell == null || window == null) { + stale(player); + return; + } + renderToolbox(window, current.get(), workcell, page); + } + + private void createVariant( + Player player, + UUID requestId, + String workcellId, + boolean duplicateActive + ) { + if (matchingState(player, requestId, true).isEmpty()) { + return; + } + if (actions.createVariant(player, workcellId, duplicateActive)) { + closeAfterAction(player); + } + } + + private void setWorkcellEnabled( + Player player, + UUID requestId, + String workcellId, + boolean enabled + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + if (current.get().workcell(workcellId) == null) { + stale(player); + return; + } + if (actions.setWorkcellEnabled(player, workcellId, enabled)) { + closeAfterAction(player); + } + } + + private void resizeWorkcell( + Player player, + UUID requestId, + String workcellId, + DimensionAxis axis, + int delta + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + if (workcell == null) { + stale(player); + return; + } + UUID playerId = player.getUniqueId(); + PendingWorkcellResize existing = pendingWorkcellResize(playerId, requestId, workcellId); + if (existing != null && existing.applying()) { + player.sendMessage(ChatColor.YELLOW + "That cell size is already being applied."); + return; + } + JigsawStudioCellDimensions base = existing == null + ? workcell.capacity() + : existing.dimensions(); + Optional adjusted = adjustedDimensions( + base, axis, delta); + if (adjusted.isEmpty()) { + player.sendMessage(ChatColor.RED + "That workcell size is outside Iris limits."); + return; + } + PendingWorkcellResize pending = new PendingWorkcellResize( + requestId, + workcellId, + adjusted.get(), + false); + pendingWorkcellResizes.put(playerId, pending); + UIWindow window = windows.get(playerId); + if (window != null) { + renderWorkcellSettings(window, current.get(), withCapacity(workcell, pending.dimensions())); + } + } + + private void applyWorkcellResize(Player player, UUID requestId, String workcellId) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + UUID playerId = player.getUniqueId(); + PendingWorkcellResize pending = pendingWorkcellResize(playerId, requestId, workcellId); + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + if (pending == null || pending.applying() || workcell == null) { + return; + } + if (workcell.capacity().equals(pending.dimensions())) { + pendingWorkcellResizes.remove(playerId, pending); + refreshWorkcellSettings(player, requestId, workcellId); + return; + } + PendingWorkcellResize applying = new PendingWorkcellResize( + requestId, + workcellId, + pending.dimensions(), + true); + pendingWorkcellResizes.put(playerId, applying); + UIWindow window = windows.get(playerId); + if (window != null) { + renderWorkcellSettings(window, current.get(), withCapacity(workcell, applying.dimensions())); + } + if (!actions.updateWorkcellDimensions(player, workcellId, applying.dimensions())) { + pendingWorkcellResizes.replace(playerId, applying, pending); + refreshWorkcellSettings(player, requestId, workcellId); + return; + } + scheduleWorkcellResizeRefresh(player, applying, WORKCELL_RESIZE_REFRESH_ATTEMPTS); + } + + private void scheduleWorkcellResizeRefresh( + Player player, + PendingWorkcellResize pending, + int attemptsRemaining + ) { + boolean scheduled = J.runEntity( + player, + () -> refreshAppliedWorkcellResize(player, pending, attemptsRemaining), + WORKCELL_RESIZE_REFRESH_TICKS); + if (!scheduled) { + pendingWorkcellResizes.remove(player.getUniqueId(), pending); + } + } + + private void refreshAppliedWorkcellResize( + Player player, + PendingWorkcellResize pending, + int attemptsRemaining + ) { + UUID playerId = player.getUniqueId(); + if (!pending.equals(pendingWorkcellResizes.get(playerId))) { + return; + } + Optional current = matchingState(player, pending.requestId(), false); + JigsawStudioMenuState.Workcell workcell = current + .map(state -> state.workcell(pending.workcellId())) + .orElse(null); + if (workcell == null) { + pendingWorkcellResizes.remove(playerId, pending); + return; + } + if (workcell.capacity().equals(pending.dimensions())) { + pendingWorkcellResizes.remove(playerId, pending); + UIWindow window = windows.get(playerId); + if (window != null) { + renderWorkcellSettings(window, current.orElseThrow(), workcell); + } + return; + } + if (attemptsRemaining > 0) { + scheduleWorkcellResizeRefresh(player, pending, attemptsRemaining - 1); + return; + } + PendingWorkcellResize retry = new PendingWorkcellResize( + pending.requestId(), + pending.workcellId(), + pending.dimensions(), + false); + pendingWorkcellResizes.replace(playerId, pending, retry); + UIWindow window = windows.get(playerId); + if (window != null) { + renderWorkcellSettings(window, current.orElseThrow(), withCapacity(workcell, retry.dimensions())); + } + player.sendMessage(ChatColor.YELLOW + + "Cell resizing is still pending; use Apply Cell Size to retry after the current operation settles."); + } + + private void discardWorkcellResize(Player player, UUID requestId, String workcellId) { + PendingWorkcellResize pending = pendingWorkcellResize(player.getUniqueId(), requestId, workcellId); + if (pending != null && !pending.applying()) { + pendingWorkcellResizes.remove(player.getUniqueId(), pending); + } + refreshWorkcellSettings(player, requestId, workcellId); + } + + private void leaveWorkcellSettings(Player player, UUID requestId, String workcellId) { + PendingWorkcellResize pending = pendingWorkcellResize(player.getUniqueId(), requestId, workcellId); + if (pending != null && !pending.applying()) { + pendingWorkcellResizes.remove(player.getUniqueId(), pending); + } + refreshMain(player, requestId, workcellId, 0); + } + + private void refreshWorkcellSettings(Player player, UUID requestId, String workcellId) { + Optional current = matchingState(player, requestId, true); + UIWindow window = windows.get(player.getUniqueId()); + JigsawStudioMenuState.Workcell workcell = current + .map(state -> state.workcell(workcellId)) + .orElse(null); + if (window == null || workcell == null) { + return; + } + renderWorkcellSettings(window, current.orElseThrow(), workcell); + } + + private PendingWorkcellResize pendingWorkcellResize(UUID playerId, UUID requestId, String workcellId) { + PendingWorkcellResize pending = pendingWorkcellResizes.get(playerId); + if (pending == null + || !pending.requestId().equals(requestId) + || !pending.workcellId().equals(workcellId)) { + return null; + } + return pending; + } + + private void resizeVariantAxis( + Player player, + UUID requestId, + String workcellId, + String pieceKey, + DimensionAxis axis, + int delta + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + if (workcell == null || variant == null || !variant.owned() || variant.dimensions().isEmpty()) { + stale(player); + return; + } + Optional adjusted = adjustedDimensions( + variant.dimensions().orElseThrow(), axis, delta); + if (adjusted.isEmpty()) { + player.sendMessage(ChatColor.RED + "That variant size is outside Iris limits."); + return; + } + resizeVariant(player, requestId, workcellId, pieceKey, adjusted.get()); + } + + private void resizeVariant( + Player player, + UUID requestId, + String workcellId, + String pieceKey, + JigsawStudioCellDimensions dimensions + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + if (workcell == null || variant == null || !variant.owned()) { + stale(player); + return; + } + if (dimensions.width() > workcell.capacity().width() + || dimensions.height() > workcell.capacity().height() + || dimensions.depth() > workcell.capacity().depth()) { + player.sendMessage(ChatColor.RED + "Increase this workcell's capacity before making the variant larger."); + return; + } + if (actions.resizeVariant(player, workcellId, pieceKey, dimensions)) { + closeAfterAction(player); + } + } + + private void setRequireCaps(Player player, UUID requestId, boolean requireCaps) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + if (!current.get().irisExtended()) { + player.sendMessage(ChatColor.YELLOW + "Mandatory caps require Iris compatibility."); + return; + } + if (current.get().requireCaps() == requireCaps) { + player.sendMessage(ChatColor.YELLOW + "Mandatory caps are already " + + (requireCaps ? "enabled." : "disabled.")); + return; + } + if (actions.setRequireCaps(player, requireCaps)) { + clearConfirmations(player.getUniqueId()); + closeAfterAction(player); + } + } + + private void duplicateActiveFamily(Player player, UUID requestId, String themeKey) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + if (!current.get().irisExtended()) { + player.sendMessage(ChatColor.YELLOW + "Theme sets require Iris compatibility."); + return; + } + if (!nextThemeSetKey(current.get().themeSets()).equals(themeKey)) { + stale(player); + return; + } + if (actions.duplicateActiveFamily(player, themeKey)) { + clearConfirmations(player.getUniqueId()); + closeAfterAction(player); + } + } + + private void adjustThemeSetWeight( + Player player, + UUID requestId, + JigsawStudioMenuState.ThemeSet expected, + int delta + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + if (!current.get().irisExtended()) { + player.sendMessage(ChatColor.YELLOW + "Theme weights require Iris compatibility."); + return; + } + JigsawStudioMenuState.ThemeSet themeSet = current.get().themeSet(expected.key()); + if (!expected.equals(themeSet)) { + stale(player); + return; + } + Optional weight = adjustedPositiveValue(themeSet.weight(), delta); + if (weight.isEmpty()) { + player.sendMessage(ChatColor.RED + "Theme weights must remain positive."); + return; + } + if (actions.updateThemeSetWeight(player, themeSet.key(), weight.get())) { + clearConfirmations(player.getUniqueId()); + closeAfterAction(player); + } + } + + private void flushNow(Player player, UUID requestId, String workcellId) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + if (workcell == null) { + stale(player); + return; + } + if (!workcell.dirty()) { + player.sendMessage(ChatColor.YELLOW + "This workcell has no pending changes to flush."); + return; + } + if (actions.flushAutosave(player, workcellId)) { + closeAfterAction(player); + } + } + + private void goToPreview(Player player, UUID requestId) { + if (matchingState(player, requestId, true).isPresent() + && actions.goToPreview(player)) { + closeAfterAction(player); + } + } + + private void toggleRotation(Player player, UUID requestId, String workcellId) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty() || activeVariant(current.get(), workcellId) == null) { + return; + } + if (actions.toggleVariantRotatable(player, workcellId)) { + closeAfterAction(player); + } + } + + private void adjustVariantRule( + Player player, + UUID requestId, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant expected, + RuleField field, + int delta + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Variant variant = activeVariant(current.get(), workcell.stableId()); + if (variant == null || !variant.owned() || !variant.pieceKey().equals(expected.pieceKey())) { + stale(player); + return; + } + Optional rules = adjustedRules(variant.rules(), field, delta); + if (rules.isEmpty()) { + player.sendMessage(ChatColor.RED + "That piece rule value is outside Iris limits."); + return; + } + updateVariantRules( + player, + requestId, + workcell.stableId(), + variant.pieceKey(), + rules.get()); + } + + private void updateVariantRules( + Player player, + UUID requestId, + String workcellId, + String pieceKey, + JigsawStudioPieceRules rules + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + if (!current.get().irisExtended()) { + player.sendMessage(ChatColor.YELLOW + "Piece rules require Iris compatibility."); + return; + } + JigsawStudioMenuState.Variant variant = activeVariant(current.get(), workcellId); + if (variant == null || !variant.owned() || !variant.pieceKey().equals(pieceKey)) { + stale(player); + return; + } + if (actions.updateVariantRules(player, workcellId, pieceKey, rules)) { + clearConfirmations(player.getUniqueId()); + closeAfterAction(player); + } + } + + private void toggleVariantTheme( + Player player, + UUID requestId, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant expected, + String themeKey + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + if (!current.get().irisExtended()) { + player.sendMessage(ChatColor.YELLOW + "Theme membership requires Iris compatibility."); + return; + } + JigsawStudioMenuState.Variant variant = activeVariant(current.get(), workcell.stableId()); + if (variant == null + || !variant.owned() + || !variant.pieceKey().equals(expected.pieceKey()) + || current.get().themeSet(themeKey) == null) { + stale(player); + return; + } + List themes = new ArrayList<>(variant.themes()); + if (!themes.remove(themeKey)) { + themes.add(themeKey); + } + if (actions.updateVariantThemes( + player, + workcell.stableId(), + variant.pieceKey(), + List.copyOf(themes))) { + clearConfirmations(player.getUniqueId()); + closeAfterAction(player); + } + } + + private void adjustMembershipWeight( + Player player, + UUID requestId, + String workcellId, + JigsawStudioMenuState.Membership membership, + int delta + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty() || !hasMembership(current.get(), workcellId, membership)) { + stale(player); + return; + } + JigsawStudioMenuState.Variant active = activeVariant(current.get(), workcellId); + if (active == null) { + stale(player); + return; + } + if (actions.adjustVariantWeight( + player, + workcellId, + active.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + delta)) { + clearConfirmations(player.getUniqueId()); + closeAfterAction(player); + } + } + + private void adjustMembershipChance( + Player player, + UUID requestId, + String workcellId, + JigsawStudioMenuState.Membership membership, + int deltaPercentagePoints + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty() || !hasMembership(current.get(), workcellId, membership)) { + stale(player); + return; + } + if (!current.get().irisExtended()) { + player.sendMessage(ChatColor.YELLOW + "Per-entry chance requires Iris compatibility."); + return; + } + JigsawStudioMenuState.Variant active = activeVariant(current.get(), workcellId); + if (active == null) { + stale(player); + return; + } + if (actions.adjustVariantChance( + player, + workcellId, + active.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + deltaPercentagePoints)) { + clearConfirmations(player.getUniqueId()); + closeAfterAction(player); + } + } + + private void confirmOrUnlink( + Player player, + UUID requestId, + String workcellId, + JigsawStudioMenuState.Membership membership, + int page + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty() || !hasMembership(current.get(), workcellId, membership)) { + stale(player); + return; + } + + UUID playerId = player.getUniqueId(); + JigsawStudioMenuState.Variant active = activeVariant(current.get(), workcellId); + if (active == null || !active.owned()) { + stale(player); + return; + } + if (!isUnlinkConfirmed(playerId, requestId, workcellId, active.pieceKey(), membership)) { + pendingUnlinks.put(playerId, new PendingUnlink( + requestId, + workcellId, + active.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + System.nanoTime() + DESTRUCTIVE_CONFIRM_NANOS)); + J.runEntity(player, () -> refreshDetails(player, requestId, workcellId, page), 1); + return; + } + + if (actions.unlinkVariantMembership( + player, + workcellId, + active.pieceKey(), + membership.poolKey(), + membership.entryIndex())) { + clearConfirmations(playerId); + closeAfterAction(player); + } + } + + private void confirmOrDeleteVariant( + Player player, + UUID requestId, + String workcellId, + String pieceKey, + int page + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant target = variant(workcell, pieceKey); + if (target == null || !target.owned() || target.active()) { + stale(player); + return; + } + + UUID playerId = player.getUniqueId(); + if (!isDeleteConfirmed(playerId, requestId, workcellId, pieceKey)) { + pendingDeletes.put(playerId, new PendingDelete( + requestId, + workcellId, + pieceKey, + System.nanoTime() + DESTRUCTIVE_CONFIRM_NANOS)); + J.runEntity(player, () -> refreshDetails( + player, requestId, workcellId, pieceKey, page), 1); + return; + } + + if (actions.deleteVariant(player, workcellId, pieceKey)) { + clearConfirmations(playerId); + closeAfterAction(player); + } + } + + private void confirmOrDeleteProject( + Player player, + UUID requestId, + String selectedWorkcellId, + int page + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + UUID playerId = player.getUniqueId(); + if (!isProjectDeleteConfirmed(playerId, requestId)) { + pendingProjectDeletes.put(playerId, new PendingProjectDelete( + requestId, + System.nanoTime() + DESTRUCTIVE_CONFIRM_NANOS)); + J.runEntity(player, () -> refreshStructureSettings( + player, requestId, selectedWorkcellId, page), 1); + return; + } + if (actions.deleteProject(player)) { + clearConfirmations(playerId); + closeAfterAction(player); + } + } + + private void giveTool(Player player, UUID requestId, JigsawStudioToolPayload payload) { + if (matchingState(player, requestId, true).isEmpty()) { + return; + } + if (!payload.requestId().equals(requestId)) { + stale(player); + return; + } + actions.giveTool(player, payload); + } + + private void refreshMain(Player player, UUID requestId, String workcellId, int page) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + UIWindow window = windows.get(player.getUniqueId()); + if (workcell == null || window == null) { + stale(player); + return; + } + renderMain(window, current.get(), workcell, page); + } + + private void refreshDetails(Player player, UUID requestId, String workcellId, int page) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Variant active = activeVariant(current.get(), workcellId); + if (active == null) { + stale(player); + return; + } + refreshDetails(player, requestId, workcellId, active.pieceKey(), page); + } + + private void refreshDetails( + Player player, + UUID requestId, + String workcellId, + String pieceKey, + int page + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + UIWindow window = windows.get(player.getUniqueId()); + if (workcell == null || variant == null || window == null) { + stale(player); + return; + } + renderDetails(window, current.get(), workcell, variant, page); + } + + private void refreshStructureSettings( + Player player, + UUID requestId, + String selectedWorkcellId, + int page + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + UIWindow window = windows.get(player.getUniqueId()); + if (current.get().workcell(selectedWorkcellId) == null || window == null) { + stale(player); + return; + } + renderStructureSettings(window, current.get(), selectedWorkcellId, page); + } + + private void refreshVariantSettings( + Player player, + UUID requestId, + String workcellId, + String pieceKey, + int page + ) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + JigsawStudioMenuState.Variant variant = variant(workcell, pieceKey); + UIWindow window = windows.get(player.getUniqueId()); + if (workcell == null || variant == null || window == null) { + stale(player); + return; + } + renderVariantSettings(window, current.get(), workcell, variant, page); + } + + private void refreshToolbox(Player player, UUID requestId, String workcellId, int page) { + Optional current = matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + UIWindow window = windows.get(player.getUniqueId()); + if (workcell == null || window == null) { + stale(player); + return; + } + renderToolbox(window, current.get(), workcell, page); + } + + private Optional matchingState(Player player, UUID requestId, boolean closeStale) { + Optional current = actions.menuState(player); + if (current.isPresent() && current.get().requestId().equals(requestId)) { + return current; + } + if (closeStale) { + stale(player); + } + return Optional.empty(); + } + + private void stale(Player player) { + closeAfterAction(player); + player.sendMessage(ChatColor.RED + "This Jigsaw Studio menu is stale. Open the control chest again."); + } + + private void closeAfterAction(Player player) { + J.runEntity(player, () -> close(player), 1); + } + + private boolean isUnlinkConfirmed( + UUID playerId, + UUID requestId, + String workcellId, + String pieceKey, + JigsawStudioMenuState.Membership membership + ) { + PendingUnlink pending = pendingUnlinks.get(playerId); + if (pending == null) { + return false; + } + if (pending.expiresAtNanos() < System.nanoTime()) { + pendingUnlinks.remove(playerId, pending); + return false; + } + return pending.requestId().equals(requestId) + && pending.workcellId().equals(workcellId) + && pending.pieceKey().equals(pieceKey) + && pending.poolKey().equals(membership.poolKey()) + && pending.entryIndex() == membership.entryIndex(); + } + + private boolean isDeleteConfirmed( + UUID playerId, + UUID requestId, + String workcellId, + String pieceKey + ) { + PendingDelete pending = pendingDeletes.get(playerId); + if (pending == null) { + return false; + } + if (pending.expiresAtNanos() < System.nanoTime()) { + pendingDeletes.remove(playerId, pending); + return false; + } + return pending.requestId().equals(requestId) + && pending.workcellId().equals(workcellId) + && pending.pieceKey().equals(pieceKey); + } + + private boolean isProjectDeleteConfirmed(UUID playerId, UUID requestId) { + PendingProjectDelete pending = pendingProjectDeletes.get(playerId); + if (pending == null) { + return false; + } + if (pending.expiresAtNanos() < System.nanoTime()) { + pendingProjectDeletes.remove(playerId, pending); + return false; + } + return pending.requestId().equals(requestId); + } + + private void purgeExpiredConfirmations(UUID playerId) { + PendingUnlink unlink = pendingUnlinks.get(playerId); + if (unlink != null && unlink.expiresAtNanos() < System.nanoTime()) { + pendingUnlinks.remove(playerId, unlink); + } + PendingDelete delete = pendingDeletes.get(playerId); + if (delete != null && delete.expiresAtNanos() < System.nanoTime()) { + pendingDeletes.remove(playerId, delete); + } + PendingProjectDelete projectDelete = pendingProjectDeletes.get(playerId); + if (projectDelete != null && projectDelete.expiresAtNanos() < System.nanoTime()) { + pendingProjectDeletes.remove(playerId, projectDelete); + } + } + + private void clearConfirmations(UUID playerId) { + pendingUnlinks.remove(playerId); + pendingDeletes.remove(playerId); + pendingProjectDeletes.remove(playerId); + } + + private static boolean hasMembership( + JigsawStudioMenuState state, + String workcellId, + JigsawStudioMenuState.Membership expected + ) { + JigsawStudioMenuState.Variant active = activeVariant(state, workcellId); + if (active == null) { + return false; + } + for (JigsawStudioMenuState.Membership membership : active.memberships()) { + if (membership.poolKey().equals(expected.poolKey()) + && membership.entryIndex() == expected.entryIndex()) { + return true; + } + } + return false; + } + + private static JigsawStudioMenuState.Variant activeVariant( + JigsawStudioMenuState state, + String workcellId + ) { + JigsawStudioMenuState.Workcell workcell = state.workcell(workcellId); + return workcell == null ? null : workcell.activeVariant(); + } + + private static JigsawStudioMenuState.Variant variant( + JigsawStudioMenuState.Workcell workcell, + String pieceKey + ) { + if (workcell == null || pieceKey == null) { + return null; + } + for (JigsawStudioMenuState.Variant variant : workcell.variants()) { + if (variant.pieceKey().equals(pieceKey)) { + return variant; + } + } + return null; + } + + static String nextThemeSetKey(List themeSets) { + List values = List.copyOf(Objects.requireNonNull( + themeSets, + "Jigsaw Studio theme sets")); + int candidate = 1; + while (candidate > 0) { + String key = "variant-" + candidate; + boolean present = false; + for (JigsawStudioMenuState.ThemeSet themeSet : values) { + if (Objects.requireNonNull(themeSet, "Jigsaw Studio theme set").key().equals(key)) { + present = true; + break; + } + } + if (!present) { + return key; + } + candidate = Math.incrementExact(candidate); + } + throw new IllegalStateException("Jigsaw Studio cannot allocate another numbered theme set"); + } + + static String themeSelectionPercent( + List themeSets, + JigsawStudioMenuState.ThemeSet target + ) { + List activeThemeSets = Objects.requireNonNull( + themeSets, + "Jigsaw Studio theme sets"); + JigsawStudioMenuState.ThemeSet activeTarget = Objects.requireNonNull( + target, + "Jigsaw Studio target theme set"); + int totalWeight = 0; + for (JigsawStudioMenuState.ThemeSet themeSet : activeThemeSets) { + totalWeight = Math.addExact(totalWeight, Objects.requireNonNull( + themeSet, + "Jigsaw Studio theme set").weight()); + } + if (totalWeight < 1) { + return "0.0%"; + } + return String.format( + Locale.ROOT, + "%.1f%%", + activeTarget.weight() * 100.0D / totalWeight); + } + + static Optional adjustedPositiveValue(int value, int delta) { + if (value < 1 || delta == 0) { + throw new IllegalArgumentException("Jigsaw Studio positive value adjustment is invalid"); + } + try { + int adjusted = Math.addExact(value, delta); + return adjusted < 1 ? Optional.empty() : Optional.of(adjusted); + } catch (ArithmeticException exception) { + return Optional.empty(); + } + } + + static Optional adjustedRules( + JigsawStudioPieceRules rules, + RuleField field, + int delta + ) { + JigsawStudioPieceRules current = Objects.requireNonNull(rules, "Jigsaw Studio piece rules"); + RuleField target = Objects.requireNonNull(field, "Jigsaw Studio piece rule field"); + if (delta == 0) { + throw new IllegalArgumentException("Jigsaw Studio piece rule delta cannot be zero"); + } + try { + int minimumDepth = current.minimumDepth(); + int maximumDepth = current.maximumDepth(); + int minimumPlacements = current.minimumPlacements(); + int maximumPlacements = current.maximumPlacements(); + switch (target) { + case MINIMUM_DEPTH -> minimumDepth = Math.addExact(minimumDepth, delta); + case MAXIMUM_DEPTH -> maximumDepth = Math.addExact(maximumDepth, delta); + case MINIMUM_PLACEMENTS -> minimumPlacements = Math.addExact(minimumPlacements, delta); + case MAXIMUM_PLACEMENTS -> { + Optional adjustedMaximum = adjustedMaximumPlacements(maximumPlacements, delta); + if (adjustedMaximum.isEmpty()) { + return Optional.empty(); + } + maximumPlacements = adjustedMaximum.get(); + } + } + return Optional.of(new JigsawStudioPieceRules( + minimumDepth, + maximumDepth, + minimumPlacements, + maximumPlacements, + current.terminal())); + } catch (ArithmeticException | IllegalArgumentException exception) { + return Optional.empty(); + } + } + + static JigsawStudioPieceRules withTerminal(JigsawStudioPieceRules rules, boolean terminal) { + JigsawStudioPieceRules current = Objects.requireNonNull(rules, "Jigsaw Studio piece rules"); + return new JigsawStudioPieceRules( + current.minimumDepth(), + current.maximumDepth(), + current.minimumPlacements(), + current.maximumPlacements(), + terminal); + } + + private static Optional adjustedMaximumPlacements(int value, int delta) { + if (value == 0) { + if (delta > 0) { + return Optional.empty(); + } + int adjusted = Math.addExact(513, delta); + return adjusted < 1 ? Optional.empty() : Optional.of(adjusted); + } + int adjusted = Math.addExact(value, delta); + if (adjusted > 512) { + return Optional.of(0); + } + return adjusted < 1 ? Optional.empty() : Optional.of(adjusted); + } + + static Optional adjustedDimensions( + JigsawStudioCellDimensions dimensions, + DimensionAxis axis, + int delta + ) { + JigsawStudioCellDimensions current = Objects.requireNonNull( + dimensions, + "Jigsaw Studio workcell dimensions"); + DimensionAxis target = Objects.requireNonNull(axis, "Jigsaw Studio workcell dimension axis"); + if (delta == 0) { + throw new IllegalArgumentException("Jigsaw Studio workcell dimension delta cannot be zero"); + } + try { + int width = target == DimensionAxis.WIDTH + ? Math.addExact(current.width(), delta) + : current.width(); + int height = target == DimensionAxis.HEIGHT + ? Math.addExact(current.height(), delta) + : current.height(); + int depth = target == DimensionAxis.DEPTH + ? Math.addExact(current.depth(), delta) + : current.depth(); + return Optional.of(new JigsawStudioCellDimensions(width, height, depth)); + } catch (ArithmeticException | IllegalArgumentException exception) { + return Optional.empty(); + } + } + + static JigsawStudioMenuState.Workcell withCapacity( + JigsawStudioMenuState.Workcell workcell, + JigsawStudioCellDimensions capacity + ) { + JigsawStudioMenuState.Workcell source = Objects.requireNonNull( + workcell, + "Jigsaw Studio menu workcell"); + return new JigsawStudioMenuState.Workcell( + source.stableId(), + source.canonicalName(), + source.displayName(), + Objects.requireNonNull(capacity, "Jigsaw Studio staged workcell capacity"), + source.enabled(), + source.activeVariantKey(), + source.dirty(), + source.saving(), + source.loading(), + source.connectorsVisible(), + source.variants()); + } + + static List toolboxTools( + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell + ) { + JigsawStudioMenuState menu = Objects.requireNonNull(state, "Jigsaw Studio toolbox state"); + JigsawStudioMenuState.Workcell selected = Objects.requireNonNull( + workcell, + "Jigsaw Studio toolbox workcell"); + UUID requestId = menu.requestId(); + List tools = new ArrayList<>(); + tools.add(new ToolboxTool( + "Open Control Menu", + JigsawStudioToolPayload.request(JigsawStudioToolAction.OPEN_MENU, requestId))); + tools.add(new ToolboxTool( + "Select " + selected.displayName(), + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.SELECT_WORKCELL, + requestId, + selected.stableId()))); + if (menu.mode() == JigsawStudioMode.PLANAR_JIGSAW) { + tools.add(new ToolboxTool( + "Toggle " + selected.displayName(), + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.TOGGLE_WORKCELL, + requestId, + selected.stableId()))); + } + tools.add(new ToolboxTool( + "Resize " + selected.displayName() + " Capacity", + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.RESIZE_WORKCELL, + requestId, + selected.stableId()))); + tools.add(new ToolboxTool( + "Rename " + selected.displayName(), + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.RENAME_WORKCELL, + requestId, + selected.stableId()))); + tools.add(new ToolboxTool( + "New Blank " + selected.displayName() + " Variant", + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.CREATE_VARIANT, + requestId, + selected.stableId()))); + tools.add(new ToolboxTool( + "Go to Preview", + JigsawStudioToolPayload.request(JigsawStudioToolAction.PREVIEW_GRAPH, requestId))); + tools.add(new ToolboxTool( + "Flush " + selected.displayName() + " Autosave", + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.FLUSH_AUTOSAVE, + requestId, + selected.stableId()))); + if (menu.irisExtended()) { + tools.add(new ToolboxTool( + "Duplicate All Enabled Cells as Family: " + nextThemeSetKey(menu.themeSets()), + JigsawStudioToolPayload.request(JigsawStudioToolAction.DUPLICATE_FAMILY, requestId))); + } + + JigsawStudioMenuState.Variant active = selected.activeVariant(); + if (active != null) { + if (active.owned()) { + tools.add(new ToolboxTool( + "Duplicate This Cell's Variant: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.DUPLICATE_VARIANT, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.owned()) { + tools.add(new ToolboxTool( + "Rename This Variant: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RENAME_VARIANT, + requestId, + selected.stableId(), + active.pieceKey()))); + tools.add(new ToolboxTool( + "Resize This Variant: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RESIZE_VARIANT, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.rotationEditable()) { + tools.add(new ToolboxTool( + "Toggle Rotation: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.TOGGLE_ROTATION, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.owned() && active.resizableToCapacity()) { + tools.add(new ToolboxTool( + "Resize " + active.displayName() + " to Capacity", + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.EXPAND_TO_CELL, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.owned() && menu.irisExtended()) { + tools.add(new ToolboxTool( + "Open Theme Memberships: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.SET_THEME, + requestId, + selected.stableId(), + active.pieceKey()))); + tools.add(new ToolboxTool( + "Open Piece Rules: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.SET_PIECE_RULES, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.owned()) { + for (JigsawStudioMenuState.Membership membership : active.memberships()) { + addMembershipTools( + tools, + requestId, + selected, + active, + membership, + menu.irisExtended()); + } + } + } + for (JigsawStudioMenuState.Variant variant : selected.variants()) { + if (variant.active()) { + continue; + } + tools.add(new ToolboxTool( + "Load " + variant.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.LOAD_VARIANT, + requestId, + selected.stableId(), + variant.pieceKey()))); + if (variant.owned()) { + tools.add(new ToolboxTool( + "Rename " + variant.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RENAME_VARIANT, + requestId, + selected.stableId(), + variant.pieceKey()))); + tools.add(new ToolboxTool( + "Resize " + variant.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RESIZE_VARIANT, + requestId, + selected.stableId(), + variant.pieceKey()))); + } + if (variant.owned() && selected.variants().size() > 1) { + tools.add(new ToolboxTool( + "Delete " + variant.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.DELETE_VARIANT, + requestId, + selected.stableId(), + variant.pieceKey()))); + } + } + if (menu.irisExtended()) { + tools.add(new ToolboxTool( + menu.requireCaps() ? "Disable Mandatory Caps" : "Enable Mandatory Caps", + JigsawStudioToolPayload.request(JigsawStudioToolAction.TOGGLE_REQUIRE_CAPS, requestId))); + } + tools.add(new ToolboxTool( + "Delete Project", + JigsawStudioToolPayload.request(JigsawStudioToolAction.DELETE_PROJECT, requestId))); + return List.copyOf(tools); + } + + private static void addMembershipTools( + List tools, + UUID requestId, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + JigsawStudioMenuState.Membership membership, + boolean chanceEditable + ) { + String membershipName = displayKey(membership.poolKey()) + " [" + membership.entryIndex() + "]"; + tools.add(new ToolboxTool( + "Weight +1: " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.ADJUST_VARIANT_WEIGHT, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + 1))); + tools.add(new ToolboxTool( + "Weight -1: " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.ADJUST_VARIANT_WEIGHT, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + -1))); + if (chanceEditable) { + tools.add(new ToolboxTool( + "Chance +" + CHANCE_STEP_PERCENTAGE_POINTS + "%: " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.ADJUST_VARIANT_CHANCE, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + CHANCE_STEP_PERCENTAGE_POINTS))); + tools.add(new ToolboxTool( + "Chance -" + CHANCE_STEP_PERCENTAGE_POINTS + "%: " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.ADJUST_VARIANT_CHANCE, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + -CHANCE_STEP_PERCENTAGE_POINTS))); + } + tools.add(new ToolboxTool( + "Unlink " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.UNLINK_MEMBERSHIP, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + 0))); + } + + private static void addToolBindingLore(UIElement element, JigsawStudioToolPayload payload) { + if (!payload.workcellId().isEmpty()) { + element.addLore(ChatColor.GRAY + "Workcell: " + safe(payload.workcellId())); + } + if (!payload.pieceKey().isEmpty()) { + element.addLore(ChatColor.GRAY + "Variant: " + safe(payload.pieceKey())); + } + if (!payload.poolKey().isEmpty()) { + element.addLore(ChatColor.GRAY + "Pool: " + safe(payload.poolKey()) + + (payload.entryIndex() < 0 ? "" : " [" + payload.entryIndex() + "]")); + } + if (payload.amount() != 0) { + element.addLore(ChatColor.GRAY + "Amount: " + payload.amount()); + } + } + + static int pageCount(int itemCount, int pageSize) { + if (itemCount < 0 || pageSize < 1) { + throw new IllegalArgumentException("Jigsaw Studio menu page bounds are invalid"); + } + return Math.max(1, (itemCount + pageSize - 1) / pageSize); + } + + static int clampPage(int requestedPage, int itemCount, int pageSize) { + int maximum = pageCount(itemCount, pageSize) - 1; + return Math.max(0, Math.min(requestedPage, maximum)); + } + + static List page(List items, int requestedPage, int pageSize) { + List values = List.copyOf(Objects.requireNonNull(items, "Jigsaw Studio menu page items")); + int page = clampPage(requestedPage, values.size(), pageSize); + int start = page * pageSize; + int end = Math.min(values.size(), start + pageSize); + return values.subList(start, end); + } + + private static UIElement evaluationElement(JigsawStudioMenuState.Evaluation evaluation) { + UIElement element = element( + "evaluation", + evaluationMaterial(evaluation.state()), + evaluationColor(evaluation.state()) + evaluationName(evaluation.state())); + element.addLore(ChatColor.GRAY + "Evaluation updates automatically."); + element.addLore(ChatColor.GRAY + "Seed: " + evaluation.seed()); + if (evaluation.generation() > 0L) { + element.addLore(ChatColor.GRAY + "Generation: " + evaluation.generation()); + } + if (!evaluation.selectedTheme().isEmpty()) { + element.addLore(ChatColor.GRAY + "Theme: " + safe(evaluation.selectedTheme())); + } + element.addLore(ChatColor.GRAY + "Pieces: " + evaluation.pieceCount()); + if (!evaluation.detail().isEmpty()) { + element.addLore(ChatColor.GRAY + safe(evaluation.detail())); + } + return element; + } + + static Material evaluationMaterial(JigsawStudioEvaluationState state) { + return switch (state) { + case PENDING -> Material.CLOCK; + case VALID -> Material.EMERALD; + case WARNING -> Material.YELLOW_DYE; + case INVALID -> Material.RED_DYE; + case STALE -> Material.GRAY_DYE; + }; + } + + private static ChatColor evaluationColor(JigsawStudioEvaluationState state) { + return switch (state) { + case PENDING -> ChatColor.AQUA; + case VALID -> ChatColor.GREEN; + case WARNING -> ChatColor.YELLOW; + case INVALID -> ChatColor.RED; + case STALE -> ChatColor.GRAY; + }; + } + + private static String evaluationName(JigsawStudioEvaluationState state) { + return switch (state) { + case PENDING -> "Evaluation Pending"; + case VALID -> "Ready to Assemble"; + case WARNING -> "Ready with Warnings"; + case INVALID -> "Assembly Blocked"; + case STALE -> "Evaluation Stale"; + }; + } + + private static Material workcellMaterial(JigsawStudioMenuState.Workcell workcell, boolean selected) { + if (!workcell.enabled()) { + return Material.GRAY_WOOL; + } + if (workcell.loading() || workcell.saving()) { + return Material.YELLOW_WOOL; + } + if (workcell.dirty()) { + return Material.ORANGE_WOOL; + } + return selected ? Material.LIME_WOOL : Material.LIGHT_GRAY_WOOL; + } + + static Material variantMaterial(JigsawStudioMenuState.Variant variant) { + if (variant.active()) { + return Material.JIGSAW; + } + return variant.owned() ? Material.PAPER : Material.GRAY_DYE; + } + + static String workcellStatus(JigsawStudioMenuState.Workcell workcell) { + if (workcell.loading()) { + return ChatColor.YELLOW + "Loading variant"; + } + if (workcell.saving()) { + return ChatColor.YELLOW + "Autosave in progress"; + } + if (workcell.dirty()) { + return ChatColor.GOLD + "Autosave pending"; + } + return ChatColor.GREEN + "Autosaved"; + } + + private static String dimensions(JigsawStudioCellDimensions dimensions) { + return dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth(); + } + + private static String themes(List themes) { + return themes.isEmpty() ? "None" : safe(String.join(", ", themes)); + } + + private static String maximumPlacements(int maximum) { + return maximum == 0 ? "Unlimited" : Integer.toString(maximum); + } + + private static String chance(double chance) { + return String.format(Locale.ROOT, "%.1f%%", chance * 100D); + } + + private static String yesNo(boolean value) { + return value ? "Yes" : "No"; + } + + private static String compatibilityName(JigsawStudioCompatibilityTarget target) { + return switch (target) { + case IRIS_EXTENDED -> "Iris Extended"; + case VANILLA_PORTABLE -> "Vanilla Portable"; + }; + } + + private static UIElement element(String id, Material material, String name) { + return new UIElement(id) + .setMaterial(new MaterialBlock(material)) + .setName(name); + } + + private static String title(String structureKey) { + String value = "Iris Jigsaw: " + safe(displayKey(structureKey)); + return value.length() > 32 ? value.substring(0, 32) : value; + } + + private static String displayKey(String resourceKey) { + int separator = resourceKey.lastIndexOf('/'); + return separator < 0 ? resourceKey : resourceKey.substring(separator + 1); + } + + private static String safe(String value) { + return BoardSVC.untrustedBoardValue(value); + } + + public interface Actions { + Optional menuState(Player player); + + boolean selectWorkcell(Player player, String workcellId); + + boolean teleportToWorkcell(Player player, String workcellId); + + boolean setConnectorBlocksVisible(Player player, String workcellId, boolean visible); + + boolean resetConnectorBlocks(Player player, String workcellId); + + boolean undoAutosave(Player player); + + boolean switchVariant(Player player, String workcellId, String pieceKey, boolean discardDirty); + + boolean createVariant(Player player, String workcellId, boolean duplicateActive); + + boolean setWorkcellEnabled(Player player, String workcellId, boolean enabled); + + boolean updateWorkcellDimensions( + Player player, + String workcellId, + JigsawStudioCellDimensions dimensions + ); + + boolean updateWorkcellDisplayName(Player player, String workcellId, String displayName); + + boolean setRequireCaps(Player player, boolean requireCaps); + + boolean duplicateActiveFamily(Player player, String themeKey); + + boolean updateThemeSetWeight(Player player, String themeKey, int weight); + + boolean flushAutosave(Player player, String workcellId); + + boolean goToPreview(Player player); + + boolean toggleVariantRotatable(Player player, String workcellId); + + boolean expandVariantToCell(Player player, String workcellId); + + boolean resizeVariant( + Player player, + String workcellId, + String pieceKey, + JigsawStudioCellDimensions dimensions + ); + + boolean updateVariantDisplayName( + Player player, + String workcellId, + String pieceKey, + String displayName + ); + + boolean updateVariantThemes( + Player player, + String workcellId, + String pieceKey, + List themes + ); + + boolean updateVariantRules( + Player player, + String workcellId, + String pieceKey, + JigsawStudioPieceRules rules + ); + + boolean adjustVariantWeight( + Player player, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex, + int delta + ); + + boolean adjustVariantChance( + Player player, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex, + int deltaPercentagePoints + ); + + boolean unlinkVariantMembership( + Player player, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex + ); + + boolean deleteVariant(Player player, String workcellId, String pieceKey); + + boolean deleteProject(Player player); + + boolean giveTool(Player player, JigsawStudioToolPayload payload); + } + + enum RuleField { + MINIMUM_DEPTH("Minimum Depth", Material.LIGHT_BLUE_DYE, RULE_SHIFT_STEP) { + @Override + int value(JigsawStudioPieceRules rules) { + return rules.minimumDepth(); + } + }, + MAXIMUM_DEPTH("Maximum Depth", Material.BLUE_DYE, RULE_SHIFT_STEP) { + @Override + int value(JigsawStudioPieceRules rules) { + return rules.maximumDepth(); + } + }, + MINIMUM_PLACEMENTS("Minimum Placements", Material.TARGET, PLACEMENT_RULE_SHIFT_STEP) { + @Override + int value(JigsawStudioPieceRules rules) { + return rules.minimumPlacements(); + } + }, + MAXIMUM_PLACEMENTS("Maximum Placements", Material.GREEN_DYE, PLACEMENT_RULE_SHIFT_STEP) { + @Override + int value(JigsawStudioPieceRules rules) { + return rules.maximumPlacements(); + } + + @Override + String displayValue(int value) { + return maximumPlacements(value); + } + }; + + private final String displayName; + private final Material material; + private final int shiftStep; + + RuleField(String displayName, Material material, int shiftStep) { + this.displayName = displayName; + this.material = material; + this.shiftStep = shiftStep; + } + + String displayName() { + return displayName; + } + + Material material() { + return material; + } + + int shiftStep() { + return shiftStep; + } + + String displayValue(int value) { + return Integer.toString(value); + } + + abstract int value(JigsawStudioPieceRules rules); + } + + enum DimensionAxis { + WIDTH("Width") { + @Override + int value(JigsawStudioCellDimensions dimensions) { + return dimensions.width(); + } + }, + HEIGHT("Height") { + @Override + int value(JigsawStudioCellDimensions dimensions) { + return dimensions.height(); + } + }, + DEPTH("Depth") { + @Override + int value(JigsawStudioCellDimensions dimensions) { + return dimensions.depth(); + } + }; + + private final String displayName; + + DimensionAxis(String displayName) { + this.displayName = displayName; + } + + String displayName() { + return displayName; + } + + abstract int value(JigsawStudioCellDimensions dimensions); + } + + record ToolboxTool(String displayName, JigsawStudioToolPayload payload) { + ToolboxTool { + displayName = Objects.requireNonNull(displayName, "Jigsaw Studio toolbox display name").trim(); + if (displayName.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio toolbox display name cannot be blank"); + } + payload = Objects.requireNonNull(payload, "Jigsaw Studio toolbox payload"); + } + } + + private record PendingUnlink( + UUID requestId, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex, + long expiresAtNanos + ) { + } + + private record PendingDelete( + UUID requestId, + String workcellId, + String pieceKey, + long expiresAtNanos + ) { + } + + private record PendingProjectDelete(UUID requestId, long expiresAtNanos) { + } + + private record PendingWorkcellResize( + UUID requestId, + String workcellId, + JigsawStudioCellDimensions dimensions, + boolean applying + ) { + private PendingWorkcellResize { + requestId = Objects.requireNonNull(requestId, "Jigsaw Studio resize request ID"); + workcellId = Objects.requireNonNull(workcellId, "Jigsaw Studio resize workcell ID"); + dimensions = Objects.requireNonNull(dimensions, "Jigsaw Studio resize dimensions"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuState.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuState.java new file mode 100644 index 000000000..93f59cd4e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuState.java @@ -0,0 +1,290 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPieceRules; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +public record JigsawStudioMenuState( + UUID worldId, + UUID requestId, + String structureKey, + JigsawStudioMode mode, + JigsawStudioCompatibilityTarget compatibilityTarget, + boolean requireCaps, + List themeSets, + String selectedWorkcellId, + Evaluation evaluation, + List workcells +) { + public JigsawStudioMenuState { + worldId = Objects.requireNonNull(worldId, "Jigsaw Studio menu world ID"); + requestId = Objects.requireNonNull(requestId, "Jigsaw Studio menu request ID"); + structureKey = requireText(structureKey, "structure key"); + mode = Objects.requireNonNull(mode, "Jigsaw Studio menu mode"); + compatibilityTarget = Objects.requireNonNull( + compatibilityTarget, + "Jigsaw Studio menu compatibility target"); + themeSets = List.copyOf(Objects.requireNonNull(themeSets, "Jigsaw Studio menu theme sets")); + selectedWorkcellId = optionalText(selectedWorkcellId); + evaluation = Objects.requireNonNull(evaluation, "Jigsaw Studio menu evaluation"); + workcells = List.copyOf(Objects.requireNonNull(workcells, "Jigsaw Studio menu workcells")); + + Set workcellIds = new HashSet<>(); + for (Workcell workcell : workcells) { + Workcell resolved = Objects.requireNonNull(workcell, "Jigsaw Studio menu workcell"); + if (!workcellIds.add(resolved.stableId())) { + throw new IllegalArgumentException("Duplicate Jigsaw Studio menu workcell " + resolved.stableId()); + } + } + if (!selectedWorkcellId.isEmpty() && !workcellIds.contains(selectedWorkcellId)) { + throw new IllegalArgumentException("Selected Jigsaw Studio menu workcell is not present"); + } + + Set themeKeys = new HashSet<>(); + for (ThemeSet themeSet : themeSets) { + ThemeSet resolved = Objects.requireNonNull(themeSet, "Jigsaw Studio menu theme set"); + if (!themeKeys.add(resolved.key())) { + throw new IllegalArgumentException("Duplicate Jigsaw Studio menu theme " + resolved.key()); + } + } + } + + public Workcell selectedWorkcell() { + if (selectedWorkcellId.isEmpty()) { + return null; + } + return workcell(selectedWorkcellId); + } + + public boolean irisExtended() { + return compatibilityTarget == JigsawStudioCompatibilityTarget.IRIS_EXTENDED; + } + + public Workcell workcell(String stableId) { + if (stableId == null) { + return null; + } + for (Workcell workcell : workcells) { + if (workcell.stableId().equals(stableId)) { + return workcell; + } + } + return null; + } + + public ThemeSet themeSet(String key) { + if (key == null) { + return null; + } + for (ThemeSet themeSet : themeSets) { + if (themeSet.key().equals(key)) { + return themeSet; + } + } + return null; + } + + public record ThemeSet(String key, int weight) { + public ThemeSet { + key = requireText(key, "theme key"); + if (weight < 1) { + throw new IllegalArgumentException("Jigsaw Studio menu theme weight must be positive"); + } + } + } + + public record Evaluation( + JigsawStudioEvaluationState state, + long generation, + long seed, + String selectedTheme, + int pieceCount, + String detail + ) { + public Evaluation { + state = Objects.requireNonNull(state, "Jigsaw Studio evaluation state"); + if (generation < 0L) { + throw new IllegalArgumentException("Jigsaw Studio evaluation generation cannot be negative"); + } + selectedTheme = optionalText(selectedTheme); + if (pieceCount < 0) { + throw new IllegalArgumentException("Jigsaw Studio evaluation piece count cannot be negative"); + } + detail = optionalText(detail); + } + + public static Evaluation pending() { + return new Evaluation( + JigsawStudioEvaluationState.PENDING, + 0L, + 1337L, + "", + 0, + "Iris evaluates the graph automatically as authoring state changes."); + } + + public static Evaluation from(JigsawStudioGraphEvaluation evaluation) { + JigsawStudioGraphEvaluation source = Objects.requireNonNull( + evaluation, + "Jigsaw Studio graph evaluation"); + return new Evaluation( + source.state(), + source.generation(), + source.seed(), + source.selectedTheme(), + source.pieceCount(), + source.detail()); + } + } + + public record Workcell( + String stableId, + String canonicalName, + String displayName, + JigsawStudioCellDimensions capacity, + boolean enabled, + String activeVariantKey, + boolean dirty, + boolean saving, + boolean loading, + boolean connectorsVisible, + List variants + ) { + public Workcell { + stableId = requireText(stableId, "workcell ID"); + canonicalName = requireText(canonicalName, "workcell canonical name"); + displayName = requireText(displayName, "workcell display name"); + capacity = Objects.requireNonNull(capacity, "Jigsaw Studio menu workcell capacity"); + activeVariantKey = optionalText(activeVariantKey); + variants = List.copyOf(Objects.requireNonNull(variants, "Jigsaw Studio menu variants")); + + Set variantKeys = new HashSet<>(); + int activeVariants = 0; + for (Variant variant : variants) { + Variant resolved = Objects.requireNonNull(variant, "Jigsaw Studio menu variant"); + if (!variantKeys.add(resolved.pieceKey())) { + throw new IllegalArgumentException("Duplicate Jigsaw Studio menu variant " + resolved.pieceKey()); + } + if (resolved.active()) { + activeVariants++; + if (!resolved.pieceKey().equals(activeVariantKey)) { + throw new IllegalArgumentException("Active Jigsaw Studio menu variant key does not match"); + } + } + } + if ((activeVariantKey.isEmpty() && activeVariants != 0) + || (!activeVariantKey.isEmpty() && activeVariants != 1)) { + throw new IllegalArgumentException("Jigsaw Studio menu workcell active variant is inconsistent"); + } + } + + public Variant activeVariant() { + if (activeVariantKey.isEmpty()) { + return null; + } + for (Variant variant : variants) { + if (variant.pieceKey().equals(activeVariantKey)) { + return variant; + } + } + return null; + } + + public boolean busy() { + return saving || loading; + } + } + + public record Variant( + String pieceKey, + String displayName, + Optional dimensions, + boolean active, + boolean owned, + boolean rotatable, + boolean rotationEditable, + boolean resizableToCapacity, + List themes, + JigsawStudioPieceRules rules, + List memberships + ) { + public Variant { + pieceKey = requireText(pieceKey, "variant piece key"); + displayName = requireText(displayName, "variant display name"); + dimensions = Objects.requireNonNull(dimensions, "Jigsaw Studio menu variant dimensions"); + List resolvedThemes = new ArrayList<>(); + Set uniqueThemes = new HashSet<>(); + for (String theme : Objects.requireNonNull(themes, "Jigsaw Studio menu variant themes")) { + String resolvedTheme = requireText(theme, "variant theme"); + if (!uniqueThemes.add(resolvedTheme)) { + throw new IllegalArgumentException("Duplicate Jigsaw Studio menu variant theme " + + resolvedTheme); + } + resolvedThemes.add(resolvedTheme); + } + themes = List.copyOf(resolvedThemes); + rules = Objects.requireNonNull(rules, "Jigsaw Studio menu variant rules"); + memberships = List.copyOf(Objects.requireNonNull( + memberships, + "Jigsaw Studio menu variant memberships")); + if (rotationEditable && !owned) { + throw new IllegalArgumentException("Read-only Jigsaw Studio variants cannot edit rotation"); + } + if (resizableToCapacity && (!owned || !active)) { + throw new IllegalArgumentException( + "Only the active owned Jigsaw Studio variant can resize to its workcell capacity"); + } + + Set identities = new HashSet<>(); + for (Membership membership : memberships) { + Membership resolved = Objects.requireNonNull( + membership, + "Jigsaw Studio menu variant membership"); + MembershipIdentity identity = new MembershipIdentity(resolved.poolKey(), resolved.entryIndex()); + if (!identities.add(identity)) { + throw new IllegalArgumentException("Duplicate Jigsaw Studio menu membership " + + resolved.poolKey() + "[" + resolved.entryIndex() + "]"); + } + } + } + } + + public record Membership(String poolKey, int entryIndex, int weight, double chance) { + public Membership { + poolKey = requireText(poolKey, "membership pool key"); + if (entryIndex < 0) { + throw new IllegalArgumentException("Jigsaw Studio menu membership index cannot be negative"); + } + if (weight < 1) { + throw new IllegalArgumentException("Jigsaw Studio menu membership weight must be positive"); + } + if (!Double.isFinite(chance) || chance < 0D || chance > 1D) { + throw new IllegalArgumentException("Jigsaw Studio menu membership chance must be within 0 and 1"); + } + } + } + + private record MembershipIdentity(String poolKey, int entryIndex) { + } + + private static String requireText(String value, String name) { + String normalized = optionalText(value); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio menu " + name + " cannot be blank"); + } + return normalized; + } + + private static String optionalText(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioPreviewRenderer.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioPreviewRenderer.java new file mode 100644 index 000000000..b512d123f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioPreviewRenderer.java @@ -0,0 +1,455 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.engine.framework.PlacedStructurePiece; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +public final class JigsawStudioPreviewRenderer { + private static final int MAX_BLOCKS = 250_000; + private static final String AIR = "minecraft:air"; + private static final String STRUCTURE_VOID = "minecraft:structure_void"; + + private final Map requests = new HashMap<>(); + + public static PreviewPlan plan(List pieces) throws IOException { + List source = List.copyOf(Objects.requireNonNull( + pieces, + "Jigsaw Studio preview pieces")); + if (source.isEmpty()) { + return PreviewPlan.empty(); + } + Map blocks = new LinkedHashMap<>(); + int minimumX = Integer.MAX_VALUE; + int minimumY = Integer.MAX_VALUE; + int minimumZ = Integer.MAX_VALUE; + int maximumX = Integer.MIN_VALUE; + int maximumY = Integer.MIN_VALUE; + int maximumZ = Integer.MIN_VALUE; + for (PlacedStructurePiece piece : source) { + if (piece == null || piece.getObject() == null || piece.getPiece() == null + || piece.getRotation() == null) { + throw new IOException("Jigsaw Studio preview contains an incomplete placed piece"); + } + minimumX = Math.min(minimumX, piece.getMinX()); + minimumY = Math.min(minimumY, piece.getMinY()); + minimumZ = Math.min(minimumZ, piece.getMinZ()); + maximumX = Math.max(maximumX, piece.getMaxX()); + maximumY = Math.max(maximumY, piece.getMaxY()); + maximumZ = Math.max(maximumZ, piece.getMaxZ()); + appendObject(blocks, piece); + appendFinalStates(blocks, piece); + if (blocks.size() > MAX_BLOCKS) { + throw new IOException("The seed-1337 preview exceeds the Studio render limit of " + + MAX_BLOCKS + " explicit blocks"); + } + } + return new PreviewPlan( + Map.copyOf(blocks), + new PreviewBounds(minimumX, minimumY, minimumZ, maximumX, maximumY, maximumZ)); + } + + public void render( + World world, + UUID requestId, + long generation, + PreviewPlan plan, + Consumer completion + ) { + World activeWorld = Objects.requireNonNull(world, "Jigsaw Studio preview world"); + UUID activeRequestId = Objects.requireNonNull(requestId, "Jigsaw Studio preview request ID"); + PreviewPlan activePlan = Objects.requireNonNull(plan, "Jigsaw Studio preview plan"); + Consumer callback = Objects.requireNonNull(completion, "Jigsaw Studio preview callback"); + Map previous; + Set uncertain; + Map> updates; + synchronized (this) { + RenderState old = requests.get(activeRequestId); + previous = old == null || !old.worldId().equals(activeWorld.getUID()) + ? Map.of() + : old.blocks(); + uncertain = old == null || !old.worldId().equals(activeWorld.getUID()) + ? Set.of() + : Set.copyOf(old.pending()); + updates = updates(previous, activePlan.blocks(), uncertain); + Set pending = new HashSet<>(); + for (List chunkUpdates : updates.values()) { + for (BlockUpdate update : chunkUpdates) { + pending.add(update.position()); + } + } + requests.put(activeRequestId, new RenderState( + activeWorld.getUID(), generation, activePlan.blocks(), activePlan.bounds(), pending)); + } + if (updates.isEmpty()) { + callback.accept(new RenderResult(true, 0, "")); + return; + } + AtomicInteger remaining = new AtomicInteger(updates.size()); + AtomicBoolean failed = new AtomicBoolean(); + for (Map.Entry> chunk : updates.entrySet()) { + int chunkX = chunkX(chunk.getKey()); + int chunkZ = chunkZ(chunk.getKey()); + boolean scheduled = J.runRegion( + activeWorld, + chunkX, + chunkZ, + () -> applyChunk( + activeWorld, + activeRequestId, + generation, + chunk.getValue(), + remaining, + failed, + callback)); + if (!scheduled) { + failed.set(true); + if (remaining.decrementAndGet() == 0) { + callback.accept(new RenderResult( + false, + activePlan.blocks().size(), + "One or more preview chunks could not be scheduled")); + } + } + } + } + + public synchronized PreviewBounds bounds(UUID requestId) { + RenderState state = requests.get(requestId); + return state == null ? null : state.bounds(); + } + + public synchronized boolean contains(UUID requestId, int x, int y, int z) { + RenderState state = requests.get(requestId); + return state != null && state.bounds().contains(x, y, z); + } + + public void removeRequest(UUID requestId) { + if (requestId == null) { + return; + } + RenderState removed; + synchronized (this) { + removed = requests.remove(requestId); + } + if (removed != null) { + World world = Bukkit.getWorld(removed.worldId()); + if (world != null) { + clear(world, removalPositions(removed)); + } + } + } + + void forgetRequest(UUID requestId) { + if (requestId == null) { + return; + } + synchronized (this) { + requests.remove(requestId); + } + } + + public void removeAll() { + Map removed; + synchronized (this) { + removed = Map.copyOf(requests); + requests.clear(); + } + for (RenderState state : removed.values()) { + World world = Bukkit.getWorld(state.worldId()); + if (world != null) { + clear(world, removalPositions(state)); + } + } + } + + private static void appendObject( + Map blocks, + PlacedStructurePiece piece + ) throws IOException { + IrisObject object = piece.getObject(); + IrisObjectRotation rotation = piece.getRotation(); + for (Map.Entry entry : object.getBlocks()) { + IrisBlockVector rotated = rotation.rotate(entry.getKey()); + PlatformBlockState state = rotation.rotate(entry.getValue(), 0, 0, 0); + putState( + blocks, + new BlockPosition( + piece.getX() + rotated.getBlockX(), + piece.getY() + rotated.getBlockY(), + piece.getZ() + rotated.getBlockZ()), + state, + "object block"); + } + } + + private static void appendFinalStates( + Map blocks, + PlacedStructurePiece piece + ) throws IOException { + IrisObject object = piece.getObject(); + IrisObjectRotation rotation = piece.getRotation(); + if (piece.getPiece().getConnectors() == null) { + return; + } + for (IrisJigsawConnector connector : piece.getPiece().getConnectors()) { + if (connector == null || connector.getPosition() == null) { + throw new IOException("Jigsaw Studio preview contains an incomplete connector"); + } + IrisBlockVector signed = new IrisBlockVector( + connector.getPosition().getX() - object.getCenter().getBlockX(), + connector.getPosition().getY() - object.getCenter().getBlockY(), + connector.getPosition().getZ() - object.getCenter().getBlockZ()); + IrisBlockVector rotated = rotation.rotate(signed); + PlatformBlockState source = B.getStateOrNull(connector.getFinalState(), false); + PlatformBlockState state = source == null ? null : rotation.rotate(source, 0, 0, 0); + putState( + blocks, + new BlockPosition( + piece.getX() + rotated.getBlockX(), + piece.getY() + rotated.getBlockY(), + piece.getZ() + rotated.getBlockZ()), + state, + "connector final state"); + } + } + + private static void putState( + Map blocks, + BlockPosition position, + PlatformBlockState state, + String source + ) throws IOException { + if (state == null || state.key() == null || state.key().isBlank()) { + throw new IOException("Jigsaw Studio preview could not resolve a " + source); + } + String key = state.key(); + if (AIR.equals(key) || STRUCTURE_VOID.equals(key)) { + blocks.remove(position); + return; + } + blocks.put(position, key); + } + + private void applyChunk( + World world, + UUID requestId, + long generation, + List updates, + AtomicInteger remaining, + AtomicBoolean failed, + Consumer completion + ) { + int changed = 0; + List applied = new ArrayList<>(updates.size()); + try { + if (!isCurrent(world.getUID(), requestId, generation)) { + return; + } + for (BlockUpdate update : updates) { + Block block = world.getBlockAt(update.position().x(), update.position().y(), update.position().z()); + BlockData data = Bukkit.createBlockData(update.stateKey()); + block.setBlockData(data, false); + applied.add(update.position()); + changed++; + } + } catch (RuntimeException exception) { + failed.set(true); + IrisLogging.reportError(exception); + } finally { + synchronized (this) { + RenderState active = requests.get(requestId); + if (active != null + && active.worldId().equals(world.getUID()) + && active.generation() == generation) { + active.pending().removeAll(applied); + } + } + if (remaining.decrementAndGet() == 0) { + RenderState state; + boolean pendingEmpty; + synchronized (this) { + state = requests.get(requestId); + pendingEmpty = state != null && state.pending().isEmpty(); + } + boolean current = state != null + && state.worldId().equals(world.getUID()) + && state.generation() == generation; + completion.accept(new RenderResult( + current && pendingEmpty && !failed.get(), + current ? state.blocks().size() : changed, + failed.get() || current && !pendingEmpty + ? "One or more preview blocks could not be rendered" + : "")); + } + } + } + + private synchronized boolean isCurrent(UUID worldId, UUID requestId, long generation) { + RenderState state = requests.get(requestId); + return state != null && state.worldId().equals(worldId) && state.generation() == generation; + } + + private static Map> updates( + Map previous, + Map next, + Set uncertain + ) { + Set positions = new HashSet<>(previous.keySet()); + positions.addAll(next.keySet()); + positions.addAll(uncertain); + Map> updates = new HashMap<>(); + for (BlockPosition position : positions) { + String nextState = next.getOrDefault(position, AIR); + if (!requiresUpdate(position, previous, next, uncertain)) { + continue; + } + updates.computeIfAbsent( + chunkKey(position.x() >> 4, position.z() >> 4), + ignored -> new ArrayList<>()).add(new BlockUpdate(position, nextState)); + } + return updates; + } + + static boolean requiresUpdate( + BlockPosition position, + Map previous, + Map next, + Set uncertain + ) { + return uncertain.contains(position) + || !next.getOrDefault(position, AIR).equals(previous.get(position)); + } + + private static void clear(World world, Set positions) { + Map> updates = new HashMap<>(); + for (BlockPosition position : positions) { + updates.computeIfAbsent( + chunkKey(position.x() >> 4, position.z() >> 4), + ignored -> new ArrayList<>()).add(new BlockUpdate(position, AIR)); + } + for (Map.Entry> chunk : updates.entrySet()) { + J.runRegion(world, chunkX(chunk.getKey()), chunkZ(chunk.getKey()), () -> { + for (BlockUpdate update : chunk.getValue()) { + world.getBlockAt( + update.position().x(), + update.position().y(), + update.position().z()) + .setType(Material.AIR, false); + } + }); + } + } + + private static Set removalPositions(RenderState state) { + Set positions = new HashSet<>(state.blocks().keySet()); + positions.addAll(state.pending()); + return Set.copyOf(positions); + } + + private static long chunkKey(int chunkX, int chunkZ) { + return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL); + } + + private static int chunkX(long chunkKey) { + return (int) (chunkKey >> 32); + } + + private static int chunkZ(long chunkKey) { + return (int) chunkKey; + } + + public record PreviewPlan(Map blocks, PreviewBounds bounds) { + public PreviewPlan { + blocks = Map.copyOf(Objects.requireNonNull(blocks, "Jigsaw Studio preview blocks")); + bounds = Objects.requireNonNull(bounds, "Jigsaw Studio preview bounds"); + } + + static PreviewPlan empty() { + return new PreviewPlan(Map.of(), PreviewBounds.empty()); + } + } + + public record PreviewBounds( + int minimumX, + int minimumY, + int minimumZ, + int maximumX, + int maximumY, + int maximumZ + ) { + static PreviewBounds empty() { + return new PreviewBounds(0, 0, 0, -1, -1, -1); + } + + public boolean isEmpty() { + return maximumX < minimumX || maximumY < minimumY || maximumZ < minimumZ; + } + + public int centerX() { + return isEmpty() ? 0 : minimumX + (maximumX - minimumX) / 2; + } + + public int centerZ() { + return isEmpty() ? 0 : minimumZ + (maximumZ - minimumZ) / 2; + } + + public boolean contains(int x, int y, int z) { + return !isEmpty() + && x >= minimumX && x <= maximumX + && y >= minimumY && y <= maximumY + && z >= minimumZ && z <= maximumZ; + } + } + + public record BlockPosition(int x, int y, int z) { + } + + public record RenderResult(boolean successful, int blockCount, String failure) { + public RenderResult { + failure = failure == null ? "" : failure; + } + } + + private record RenderState( + UUID worldId, + long generation, + Map blocks, + PreviewBounds bounds, + Set pending + ) { + private RenderState { + Objects.requireNonNull(worldId, "Jigsaw Studio preview world ID"); + blocks = Map.copyOf(Objects.requireNonNull(blocks, "Jigsaw Studio preview state blocks")); + bounds = Objects.requireNonNull(bounds, "Jigsaw Studio preview state bounds"); + pending = Objects.requireNonNull(pending, "Jigsaw Studio preview pending blocks"); + } + } + + private record BlockUpdate(BlockPosition position, String stateKey) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioResourceBundleAssembler.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioResourceBundleAssembler.java new file mode 100644 index 000000000..8bf127797 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioResourceBundleAssembler.java @@ -0,0 +1,185 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.pack.StructurePackageClosure; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioAuthoringAccess; +import art.arcane.iris.core.structure.authoring.StructureCapability; +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.volmlib.util.collection.KList; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +final class JigsawStudioResourceBundleAssembler { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private JigsawStudioResourceBundleAssembler() { + } + + static Assembly assemble( + Path packRoot, + String structureKey, + String pieceKey, + byte[] objectContent, + List connectors, + boolean hasBlockEntities + ) throws IOException { + Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root").toAbsolutePath().normalize(); + String rootStructure = requireResourceKey(structureKey, "structure"); + String editedPiece = requireResourceKey(pieceKey, "piece"); + StructureKey ownershipKey = new StructureKey("iris", rootStructure); + StructureTransactionWriter writer = new StructureTransactionWriter(root); + Path manifestPath = writer.ownershipManifestPath(ownershipKey); + if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Jigsaw Studio cannot save '" + rootStructure + + "' because it is not Studio-owned. Existing unowned graphs are read-only; create a new Jigsaw Studio project to author in-game."); + } + byte[] manifestContent = Files.readAllBytes(manifestPath); + StructureOwnershipManifest manifest; + try { + manifest = StructureOwnershipManifest.fromJson(manifestContent); + } catch (RuntimeException exception) { + throw new IOException("Jigsaw Studio cannot read the ownership manifest for '" + rootStructure + "'", exception); + } + JigsawStudioAuthoringAccess.requireEditable(manifest); + if (!manifest.structure().equals(ownershipKey)) { + throw new IOException("Jigsaw Studio ownership manifest belongs to " + manifest.structure() + + ", not " + ownershipKey); + } + + StructurePackageClosure closure = StructurePackageClosure.collect(root.toFile(), List.of(rootStructure)); + if (!closure.isValid()) { + throw new IOException("Jigsaw Studio cannot save an invalid structure graph: " + + String.join("; ", closure.errors())); + } + Set reachablePaths = resourcePaths(closure); + for (String relativePath : reachablePaths) { + if (!manifest.resourceHashes().containsKey(relativePath)) { + throw new IOException("Jigsaw Studio ownership conflict: reachable resource '" + relativePath + + "' is not owned by structure '" + rootStructure + "'."); + } + } + + String piecePath = "jigsaw-pieces/" + editedPiece + ".json"; + if (!manifest.resourceHashes().containsKey(piecePath)) { + throw new IOException("Jigsaw Studio ownership conflict: piece '" + editedPiece + + "' is not owned by structure '" + rootStructure + "'."); + } + Path absolutePiecePath = resolveOwnedResource(root, piecePath); + IrisJigsawPiece piece; + JsonObject pieceJson; + try { + JsonElement parsed = GSON.fromJson( + Files.readString(absolutePiecePath, StandardCharsets.UTF_8), + JsonElement.class); + if (parsed == null || !parsed.isJsonObject()) { + throw new IllegalArgumentException("Jigsaw piece is not a JSON object"); + } + pieceJson = parsed.getAsJsonObject(); + piece = GSON.fromJson(pieceJson, IrisJigsawPiece.class); + } catch (RuntimeException exception) { + throw new IOException("Jigsaw Studio cannot parse piece '" + editedPiece + "'", exception); + } + if (piece == null || piece.getObject() == null || piece.getObject().isBlank()) { + throw new IOException("Jigsaw Studio piece '" + editedPiece + "' does not declare an object"); + } + String objectKey = requireResourceKey(piece.getObject(), "object"); + String objectPath = "objects/" + objectKey + ".iob"; + if (!manifest.resourceHashes().containsKey(objectPath)) { + throw new IOException("Jigsaw Studio ownership conflict: object '" + objectKey + + "' is not owned by structure '" + rootStructure + "'."); + } + piece.setConnectors(new KList<>(List.copyOf(connectors))); + pieceJson.add("connectors", GSON.toJsonTree(piece.getConnectors())); + + StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(ownershipKey) + .source(manifest.source()) + .backend(manifest.backend()) + .capabilities(manifest.capabilities()) + .losses(manifest.losses()) + .capability(StructureCapability.CONNECTORS); + if (hasBlockEntities) { + bundle.capability(StructureCapability.BLOCK_ENTITIES); + } + for (String relativePath : manifest.resourceHashes().keySet()) { + if (relativePath.equals(piecePath)) { + bundle.textResource(relativePath, GSON.toJson(pieceJson) + "\n"); + } else if (relativePath.equals(objectPath)) { + bundle.resource(relativePath, objectContent); + } else { + Path resource = resolveOwnedResource(root, relativePath); + bundle.resource(relativePath, Files.readAllBytes(resource)); + } + } + return new Assembly(bundle.build(), objectKey, piece, StructureHash.sha256(manifestContent)); + } + + private static Set resourcePaths(StructurePackageClosure closure) { + Set resources = new LinkedHashSet<>(); + addPaths(resources, "structures", closure.structures(), ".json"); + addPaths(resources, "jigsaw-pools", closure.pools(), ".json"); + addPaths(resources, "jigsaw-pieces", closure.pieces(), ".json"); + addPaths(resources, "objects", closure.objects(), ".iob"); + addPaths(resources, "loot", closure.loot(), ".json"); + return resources; + } + + private static void addPaths(Set resources, String folder, Set keys, String extension) { + for (String key : keys) { + resources.add(folder + "/" + key + extension); + } + } + + private static Path resolveOwnedResource(Path root, String relativePath) throws IOException { + StructureResourceBundle.validateRelativePath(relativePath); + Path resource = root.resolve(relativePath).normalize(); + if (!resource.startsWith(root)) { + throw new IOException("Jigsaw Studio resource escapes its pack root: " + relativePath); + } + if (!Files.isRegularFile(resource, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Jigsaw Studio owned resource is missing or not a regular file: " + relativePath); + } + Path realRoot = root.toRealPath(); + Path realResource = resource.toRealPath(); + if (!realResource.startsWith(realRoot)) { + throw new IOException("Jigsaw Studio owned resource escapes through a symbolic link: " + relativePath); + } + return resource; + } + + private static String requireResourceKey(String key, String kind) { + String normalized = Objects.requireNonNull(key, "Jigsaw Studio " + kind + " key").trim(); + StructureResourceBundle.validateRelativePath(normalized); + return normalized; + } + + record Assembly( + StructureResourceBundle bundle, + String objectKey, + IrisJigsawPiece piece, + String expectedManifestHash + ) { + Assembly { + Objects.requireNonNull(bundle, "Jigsaw Studio resource bundle"); + Objects.requireNonNull(objectKey, "Jigsaw Studio object key"); + Objects.requireNonNull(piece, "Jigsaw Studio piece"); + Objects.requireNonNull(expectedManifestHash, "Jigsaw Studio expected manifest hash"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java new file mode 100644 index 000000000..6dd2631a2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java @@ -0,0 +1,8829 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.runtime.InPlaceChunkRegenerator; +import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype; +import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarDirection; +import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarTopology; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioControlPosition; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioGraphMapper; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioGraphEditor; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMarkerKeyCodec; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolEditor; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolMembership; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPieceRules; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioProjectDeletionService; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioStructureEditor; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolAction; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioTripleSneakTracker; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.authoring.StructureWriteOptions; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.framework.PlacedStructurePiece; +import art.arcane.iris.engine.framework.StructureAssembler; +import art.arcane.iris.engine.framework.structure.StructureAssemblyResult; +import art.arcane.iris.engine.framework.structure.StructureGraphCompilation; +import art.arcane.iris.engine.framework.structure.StructureGraphCompiler; +import art.arcane.iris.engine.framework.structure.StructureGraphDiagnostic; +import art.arcane.iris.engine.framework.structure.StructureGraphResolver; +import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawThemeSet; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.JigsawJoint; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.Color; +import org.bukkit.GameRules; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.DoubleChest; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Jigsaw; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockCookEvent; +import org.bukkit.event.block.BlockDispenseEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockFadeEvent; +import org.bukkit.event.block.BlockFormEvent; +import org.bukkit.event.block.BlockFromToEvent; +import org.bukkit.event.block.BlockGrowEvent; +import org.bukkit.event.block.BlockMultiPlaceEvent; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.BlockRedstoneEvent; +import org.bukkit.event.block.BlockSpreadEvent; +import org.bukkit.event.block.BrewingStartEvent; +import org.bukkit.event.block.CrafterCraftEvent; +import org.bukkit.event.entity.EntityChangeBlockEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.inventory.BrewEvent; +import org.bukkit.event.inventory.BrewingStandFuelEvent; +import org.bukkit.event.inventory.FurnaceBurnEvent; +import org.bukkit.event.inventory.FurnaceStartSmeltEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryMoveItemEvent; +import org.bukkit.event.inventory.InventoryPickupItemEvent; +import org.bukkit.event.player.PlayerBucketEmptyEvent; +import org.bukkit.event.player.PlayerBucketFillEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerPortalEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; +import org.bukkit.event.server.ServerCommandEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.StructureGrowEvent; +import org.bukkit.event.world.WorldUnloadEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.BlockInventoryHolder; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +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.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +public final class JigsawStudioService implements IrisService, JigsawStudioMenuController.Actions { + private static final int AUTOSAVE_DEBOUNCE_TICKS = 40; + private static final int AUTOSAVE_RETRY_TICKS = 5; + private static final int REPLACEMENT_CLOSE_WAIT_TICKS = 2_400; + private static final List AUTOSAVE_PERSISTENT_RETRY_DELAYS = + List.of(40, 80, 160, 320, 600); + private static final long PREVIEW_SEED = 1337L; + private static final int SPATIAL_PREVIEW_BASE_Y = JigsawStudioLayout.FLOOR_Y + 48; + private static final JigsawStudioPieceRules DEFAULT_PIECE_RULES = + new JigsawStudioPieceRules(0, 30, 0, 0, false); + private static final long TOOL_CONFIRM_NANOS = 10_000_000_000L; + private static final int VISUAL_INTERVAL_TICKS = 8; + private static final int PARTICLE_BUDGET = 384; + private static final int HYDRATION_RETRY_TICKS = 2; + private static final int MAX_HYDRATION_ATTEMPTS = 40; + private static final int JIGSAW_TILE_WATCH_INTERVAL_TICKS = 5; + private static final double VISUAL_RANGE = 96.0D; + private static final double VISUAL_RANGE_SQUARED = VISUAL_RANGE * VISUAL_RANGE; + private static final Color SELECTED_COLOR = Color.AQUA; + private static final Color NEARBY_COLOR = Color.fromRGB(92, 102, 118); + private static final Color INVALID_BAY_COLOR = Color.RED; + private static final Color INVALID_CONNECTOR_COLOR = Color.RED; + private static final Color VALID_CONNECTOR_COLOR = Color.LIME; + private static final Color ASSEMBLY_PREVIEW_COLOR = Color.fromRGB(180, 90, 255); + private static final Color LIVE_PREVIEW_WARNING_COLOR = Color.fromRGB(255, 180, 55); + private static final long ASSEMBLY_PREVIEW_MILLIS = 10_000L; + private static final Set MUTATING_COMMANDS = Set.of( + "biome", "brush", "chunk", "clone", "copy", "cut", "data", "deform", "drain", + "execute", "faces", "fill", "fillbiome", "fixlava", "fixwater", "flip", "flora", + "forest", "function", "gmask", "green", "hollow", "item", "load", "mask", "move", + "naturalize", "overlay", "paste", "place", "redo", "regen", "replace", "replacenear", + "restore", "rotate", "schedule", "schem", "schematic", "set", "setblock", "smooth", + "snow", "sphere", "stack", "thaw", "undo", "walls"); + private static final Set SAFE_NON_OWNER_COMMANDS = Set.of( + "help", "list", "me", "msg", "ping", "pl", "plugins", "r", "reply", "rules", + "say", "tell", "ver", "version", "w", "whisper"); + private static final Comparator NEW_CONNECTOR_SOURCE_POSITION_ORDER = Comparator + .comparingInt((IrisJigsawConnector connector) -> connector.getPosition().getX()) + .thenComparingInt(connector -> connector.getPosition().getY()) + .thenComparingInt(connector -> connector.getPosition().getZ()); + private static JigsawStudioService INSTANCE; + + private final Map studios = new ConcurrentHashMap<>(); + private final Set particlesDisabled = ConcurrentHashMap.newKeySet(); + private final Set visualizationLoops = ConcurrentHashMap.newKeySet(); + private final Map assemblyPreviews = new ConcurrentHashMap<>(); + private final Map playerWorkcells = new ConcurrentHashMap<>(); + private final Map autosaves = new ConcurrentHashMap<>(); + private final Map deferredDuplications = new ConcurrentHashMap<>(); + private final Map jigsawTileWatches = new ConcurrentHashMap<>(); + private final Map toolConfirmations = new ConcurrentHashMap<>(); + private final Map evaluations = new ConcurrentHashMap<>(); + private final JigsawStudioTripleSneakTracker tripleSneakTracker = new JigsawStudioTripleSneakTracker(); + private final JigsawStudioToolCodec toolCodec = new JigsawStudioToolCodec(); + private final JigsawStudioPreviewRenderer previewRenderer = new JigsawStudioPreviewRenderer(); + private final AtomicBoolean disableStarted = new AtomicBoolean(); + private final Object saveLifecycleLock = new Object(); + private final Set savesInProgress = new HashSet<>(); + private final Set graphMutationsInProgress = new HashSet<>(); + private final Set materializationsInProgress = new HashSet<>(); + private final Set exportsInProgress = new HashSet<>(); + private final Set closingRequests = new HashSet<>(); + private final Set discardingRequests = new HashSet<>(); + private final Set reopenRequiredRequests = ConcurrentHashMap.newKeySet(); + private final Set unregisterRetries = ConcurrentHashMap.newKeySet(); + private final Set unregisterDrainWarnings = ConcurrentHashMap.newKeySet(); + private volatile JigsawStudioMenuController menuController; + private volatile boolean enabled; + + public static JigsawStudioService get() { + JigsawStudioService service = INSTANCE; + return service == null ? IrisServices.get(JigsawStudioService.class) : service; + } + + public static void clearAutosaveHistory(Path packRoot, String structureKey) throws IOException { + new JigsawStudioHistoryStore(packRoot, structureKey).delete(); + } + + @Override + public void onEnable() { + disableStarted.set(false); + enabled = true; + menuController = new JigsawStudioMenuController(BukkitPlatform.volmitPlugin(), this); + INSTANCE = this; + } + + @Override + public void onDisable() { + quiesceForServerShutdown(); + } + + public void quiesceForServerShutdown() { + if (!disableStarted.compareAndSet(false, true)) { + return; + } + try { + finalizeAllJigsawTileWatches(); + } catch (Throwable exception) { + IrisLogging.reportError("Failed to finalize Jigsaw Studio tile watches during shutdown.", exception); + } + try { + drainAutosavesBeforeDisable(); + } catch (Throwable exception) { + IrisLogging.reportError("Failed to drain Jigsaw Studio autosaves during shutdown.", exception); + } + enabled = false; + JigsawStudioMenuController activeMenuController = menuController; + menuController = null; + if (activeMenuController != null) { + try { + activeMenuController.closeAll(); + } catch (Throwable exception) { + IrisLogging.reportError("Failed to close Jigsaw Studio menus during shutdown.", exception); + } + } + particlesDisabled.clear(); + visualizationLoops.clear(); + assemblyPreviews.clear(); + playerWorkcells.clear(); + autosaves.clear(); + deferredDuplications.clear(); + jigsawTileWatches.clear(); + toolConfirmations.clear(); + tripleSneakTracker.clearAll(); + evaluations.clear(); + try { + previewRenderer.removeAll(); + } catch (Throwable exception) { + IrisLogging.reportError("Failed to remove Jigsaw Studio previews during shutdown.", exception); + } + reopenRequiredRequests.clear(); + unregisterRetries.clear(); + unregisterDrainWarnings.clear(); + synchronized (saveLifecycleLock) { + studios.clear(); + savesInProgress.clear(); + graphMutationsInProgress.clear(); + materializationsInProgress.clear(); + exportsInProgress.clear(); + closingRequests.clear(); + discardingRequests.clear(); + } + INSTANCE = null; + } + + public void register(Engine engine, JigsawStudioGenerator generator) { + if (!enabled || disableStarted.get()) { + return; + } + Engine activeEngine = Objects.requireNonNull(engine, "Jigsaw Studio engine"); + JigsawStudioGenerator activeGenerator = Objects.requireNonNull(generator, "Jigsaw Studio generator"); + World world = BukkitWorldBinding.world(activeEngine.getTarget().getWorld()); + if (world == null) { + return; + } + ActiveStudio existing = studios.get(world.getUID()); + if (existing != null && existing.generator() == activeGenerator) { + return; + } + if (existing != null) { + UUID existingRequestId = existing.generator().getRequest().requestId(); + finalizeJigsawTileWatches(existingRequestId); + drainAutosavesBeforeRemoval(existing); + if (requiresLifecycleDrain(existing) && !discardingRequest(existingRequestId)) { + IrisLogging.warn("Jigsaw Studio registration deferred while request %s finishes its final autosave drain", + existingRequestId); + return; + } + } + ActiveStudio next = new ActiveStudio( + world.getUID(), + world, + activeEngine, + activeGenerator, + new ConcurrentHashMap<>(), + ConcurrentHashMap.newKeySet(), + new AtomicLong()); + UUID displacedRequestId = null; + synchronized (saveLifecycleLock) { + if (!enabled || disableStarted.get()) { + return; + } + ActiveStudio previous = studios.get(world.getUID()); + if (previous != null && previous.generator() == activeGenerator) { + return; + } + ActiveStudio displaced = studios.put(world.getUID(), next); + if (displaced != null) { + displacedRequestId = displaced.generator().getRequest().requestId(); + savesInProgress.remove(displacedRequestId); + graphMutationsInProgress.remove(displacedRequestId); + materializationsInProgress.remove(displacedRequestId); + exportsInProgress.remove(displacedRequestId); + closingRequests.remove(displacedRequestId); + discardingRequests.remove(displacedRequestId); + reopenRequiredRequests.remove(displacedRequestId); + } + } + if (displacedRequestId != null) { + clearAutosaves(displacedRequestId); + clearJigsawTileWatches(displacedRequestId); + unregisterRetries.remove(displacedRequestId); + unregisterDrainWarnings.remove(displacedRequestId); + tripleSneakTracker.clearRequest(displacedRequestId); + evaluations.remove(displacedRequestId); + previewRenderer.removeRequest(displacedRequestId); + } + IrisLogging.info("Jigsaw Studio authoring registered: world=%s structure=%s bays=%d", + world.getName(), activeGenerator.getSession().structureKey(), activeGenerator.getLayout().bays().size()); + scheduleInitialEvaluation(next); + scheduleOnlinePlayers(world.getUID()); + } + + public void activationCommitted(World world, UUID requestId) { + if (!enabled || disableStarted.get() || world == null || requestId == null) { + return; + } + ActiveStudio studio = studios.get(world.getUID()); + if (studio == null || !requestId.equals(studio.generator().getRequest().requestId())) { + return; + } + disableNaturalStudioSpawning(world); + scheduleInitialEvaluation(studio); + } + + public void markChunkGenerated( + Engine engine, + JigsawStudioGenerator generator, + int chunkX, + int chunkZ + ) { + if (!enabled || disableStarted.get() || engine == null || generator == null) { + return; + } + World world = BukkitWorldBinding.world(engine.getTarget().getWorld()); + if (world == null) { + return; + } + ActiveStudio studio = studios.get(world.getUID()); + if (!enabled || disableStarted.get() + || studio == null || studio.engine() != engine || studio.generator() != generator) { + return; + } + markChunkAvailable(studio, chunkX, chunkZ); + } + + private void markChunkAvailable(ActiveStudio studio, int chunkX, int chunkZ) { + long chunkKey = chunkKey(chunkX, chunkZ); + boolean relevant = false; + for (JigsawStudioBay bay : studio.generator().getLayout().bays()) { + BayPopulation population = studio.population(bay); + if (population.markGenerated(chunkKey)) { + relevant = true; + } + } + if (relevant) { + scheduleHydration(studio, studio.world(), chunkX, chunkZ, 0); + } + } + + public void unregister(World world) { + if (world == null) { + return; + } + ActiveStudio active = studios.get(world.getUID()); + if (active != null) { + UUID activeRequestId = active.generator().getRequest().requestId(); + finalizeJigsawTileWatches(activeRequestId); + drainAutosavesBeforeRemoval(active); + if (requiresLifecycleDrain(active) && !discardingRequest(activeRequestId)) { + if (unregisterDrainWarnings.add(activeRequestId)) { + IrisLogging.warn("Jigsaw Studio unregister deferred while request %s finishes its final autosave drain", + activeRequestId); + } + scheduleUnregisterRetry(world, activeRequestId); + return; + } + } + ActiveStudio removed; + JigsawStudioActivation.Request request; + synchronized (saveLifecycleLock) { + removed = studios.remove(world.getUID()); + if (removed == null) { + return; + } + request = removed.generator().getRequest(); + savesInProgress.remove(request.requestId()); + graphMutationsInProgress.remove(request.requestId()); + materializationsInProgress.remove(request.requestId()); + exportsInProgress.remove(request.requestId()); + closingRequests.remove(request.requestId()); + discardingRequests.remove(request.requestId()); + reopenRequiredRequests.remove(request.requestId()); + } + clearAutosaves(request.requestId()); + deferredDuplications.remove(request.requestId()); + clearJigsawTileWatches(request.requestId()); + unregisterRetries.remove(request.requestId()); + unregisterDrainWarnings.remove(request.requestId()); + toolConfirmations.entrySet().removeIf(entry -> entry.getValue().payload().requestId().equals(request.requestId())); + tripleSneakTracker.clearRequest(request.requestId()); + evaluations.remove(request.requestId()); + previewRenderer.forgetRequest(request.requestId()); + JigsawStudioActivation.deactivate(request.packKey(), request.requestId()); + clearWorldPlayerContexts(world.getUID()); + IrisLogging.info("Jigsaw Studio authoring unregistered: world=%s", world.getName()); + } + + private void scheduleUnregisterRetry(World world, UUID requestId) { + if (!enabled || !unregisterRetries.add(requestId)) { + return; + } + try { + J.s(() -> { + unregisterRetries.remove(requestId); + ActiveStudio current = studios.get(world.getUID()); + if (current != null + && current.generator().getRequest().requestId().equals(requestId)) { + unregister(world); + } + }, AUTOSAVE_RETRY_TICKS); + } catch (Throwable exception) { + unregisterRetries.remove(requestId); + IrisLogging.reportError(exception); + } + } + + public CloseStart tryBeginClose(UUID requestId, UUID ownerId, boolean discard) { + if (requestId == null) { + return CloseStart.NOT_ACTIVE; + } + finalizeJigsawTileWatches(requestId); + synchronized (saveLifecycleLock) { + JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(requestId); + JigsawStudioSession session = JigsawStudioActivation.getSession(requestId); + if (request == null || session == null) { + return CloseStart.NOT_ACTIVE; + } + if (request.ownerId() != null && !request.ownerId().equals(ownerId)) { + return CloseStart.NOT_OWNER; + } + if (savesInProgress.contains(requestId)) { + return CloseStart.SAVE_IN_PROGRESS; + } + if (graphMutationsInProgress.contains(requestId)) { + return CloseStart.OPERATION_IN_PROGRESS; + } + if (exportsInProgress.contains(requestId)) { + return CloseStart.OPERATION_IN_PROGRESS; + } + if (materializationsInProgress.contains(requestId)) { + return CloseStart.OPERATION_IN_PROGRESS; + } + if (session.operationInProgress()) { + return CloseStart.OPERATION_IN_PROGRESS; + } + if (hasJigsawTileWatch(requestId)) { + return CloseStart.OPERATION_IN_PROGRESS; + } + if (session.isDirty() && !discard) { + return CloseStart.DIRTY; + } + closingRequests.add(requestId); + if (discard) { + discardingRequests.add(requestId); + } else { + discardingRequests.remove(requestId); + } + return CloseStart.STARTED; + } + } + + public String closeProtectionFailure(UUID requestId) { + if (requestId == null) { + return null; + } + synchronized (saveLifecycleLock) { + if (closingRequests.contains(requestId) && !savesInProgress.contains(requestId)) { + return null; + } + if (savesInProgress.contains(requestId)) { + return "The active Jigsaw Studio is saving and cannot be closed or replaced yet."; + } + if (graphMutationsInProgress.contains(requestId)) { + return "The active Jigsaw Studio is updating its graph and cannot be closed or replaced yet."; + } + if (exportsInProgress.contains(requestId)) { + return "The active Jigsaw Studio is exporting and cannot be closed or replaced yet."; + } + if (materializationsInProgress.contains(requestId)) { + return "The active Jigsaw Studio is loading or restoring a variant and cannot be closed yet."; + } + JigsawStudioSession session = JigsawStudioActivation.getSession(requestId); + if (session != null && session.operationInProgress()) { + return "The active Jigsaw Studio is loading a variant and cannot be closed or replaced yet."; + } + if (hasJigsawTileWatch(requestId)) { + return "The active Jigsaw Studio is finalizing an open vanilla jigsaw-block editor."; + } + if (session != null && session.isDirty()) { + return "The active Jigsaw Studio is waiting for autosave. Let it finish before closing."; + } + return "The active Jigsaw Studio is owner-controlled. Close it with /iris jigsaw close."; + } + } + + public CompletableFuture awaitCloseForReplacement(UUID requestId, UUID ownerId) { + CompletableFuture readiness = new CompletableFuture<>(); + awaitCloseForReplacement(requestId, ownerId, readiness, 0); + return readiness; + } + + private void awaitCloseForReplacement( + UUID requestId, + UUID ownerId, + CompletableFuture readiness, + int waitedTicks + ) { + if (readiness.isDone()) { + return; + } + CloseStart closeStart = tryBeginClose(requestId, ownerId, false); + switch (closeStart) { + case STARTED, NOT_ACTIVE -> readiness.complete(null); + case NOT_OWNER -> readiness.completeExceptionally(new IllegalStateException( + "The active Jigsaw Studio is owned by another player session.")); + case DIRTY, SAVE_IN_PROGRESS, OPERATION_IN_PROGRESS -> { + if (waitedTicks == 0) { + expediteAutosaves(requestId); + } + if (waitedTicks >= REPLACEMENT_CLOSE_WAIT_TICKS) { + String failure = closeProtectionFailure(requestId); + readiness.completeExceptionally(new IllegalStateException( + failure == null + ? "The active Jigsaw Studio did not become ready for replacement." + : failure)); + return; + } + try { + J.s(() -> awaitCloseForReplacement( + requestId, + ownerId, + readiness, + waitedTicks + AUTOSAVE_RETRY_TICKS), AUTOSAVE_RETRY_TICKS); + } catch (Throwable exception) { + readiness.completeExceptionally(exception); + } + } + } + } + + public void cancelClose(UUID requestId) { + if (requestId == null) { + return; + } + synchronized (saveLifecycleLock) { + closingRequests.remove(requestId); + discardingRequests.remove(requestId); + } + } + + SaveStart tryBeginSave(UUID requestId) { + Objects.requireNonNull(requestId, "Jigsaw Studio save request ID"); + synchronized (saveLifecycleLock) { + if (closingRequests.contains(requestId)) { + return SaveStart.CLOSING; + } + if (graphMutationsInProgress.contains(requestId)) { + return SaveStart.GRAPH_OPERATION; + } + if (exportsInProgress.contains(requestId)) { + return SaveStart.EXPORT_OPERATION; + } + if (materializationsInProgress.contains(requestId)) { + return SaveStart.VARIANT_OPERATION; + } + if (!savesInProgress.add(requestId)) { + return SaveStart.IN_PROGRESS; + } + return SaveStart.STARTED; + } + } + + void finishSave(UUID requestId) { + if (requestId == null) { + return; + } + synchronized (saveLifecycleLock) { + savesInProgress.remove(requestId); + } + } + + public ExportStart tryBeginExport(UUID requestId, UUID ownerId) { + Objects.requireNonNull(requestId, "Jigsaw Studio export request ID"); + Objects.requireNonNull(ownerId, "Jigsaw Studio export owner ID"); + finalizeJigsawTileWatches(requestId); + synchronized (saveLifecycleLock) { + JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(requestId); + JigsawStudioSession session = JigsawStudioActivation.getSession(requestId); + if (request == null || session == null) { + return ExportStart.NOT_ACTIVE; + } + if (request.ownerId() != null && !request.ownerId().equals(ownerId)) { + return ExportStart.NOT_OWNER; + } + if (closingRequests.contains(requestId)) { + return ExportStart.CLOSING; + } + if (savesInProgress.contains(requestId)) { + return ExportStart.SAVE_IN_PROGRESS; + } + if (graphMutationsInProgress.contains(requestId)) { + return ExportStart.OPERATION_IN_PROGRESS; + } + if (materializationsInProgress.contains(requestId) || session.operationInProgress()) { + return ExportStart.OPERATION_IN_PROGRESS; + } + if (hasJigsawTileWatch(requestId)) { + return ExportStart.OPERATION_IN_PROGRESS; + } + if (session.isDirty()) { + return ExportStart.DIRTY; + } + if (!exportsInProgress.add(requestId)) { + return ExportStart.IN_PROGRESS; + } + return ExportStart.STARTED; + } + } + + public void finishExport(UUID requestId) { + if (requestId == null) { + return; + } + synchronized (saveLifecycleLock) { + exportsInProgress.remove(requestId); + } + } + + public boolean saveSelected(Player player) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> saveSelected(player)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + if (!authorizeOwner(player, studio)) { + return false; + } + if (reopenRequiredRequests.contains(studio.generator().getRequest().requestId())) { + message(player, "Close and reopen Jigsaw Studio before loading variants in the resized layout."); + return false; + } + if (graphMutationInProgress(studio.generator().getRequest().requestId())) { + message(player, "Wait for the current Jigsaw Studio graph update to finish."); + return false; + } + UUID requestId = studio.generator().getRequest().requestId(); + finalizeJigsawTileWatches(requestId); + if (hasJigsawTileWatch(requestId)) { + message(player, "Wait for the open vanilla jigsaw-block editor to finish its final snapshot."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String selected = session.selectedBayId().orElse(null); + if (selected == null) { + Location location = player.getLocation(); + JigsawStudioBay underPlayer = session.layout().findAt( + location.getBlockX(), location.getBlockY(), location.getBlockZ()); + if (underPlayer != null) { + selected = underPlayer.stableId(); + session.selectBay(selected); + } + } + if (selected == null) { + message(player, "Stand inside or select a Jigsaw Studio workcell before saving."); + return false; + } + return saveBay(player, selected); + } + + public boolean saveBay(Player player, String bayId) { + if (player == null || bayId == null || bayId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> saveBay(player, bayId)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + if (!authorizeOwner(player, studio)) { + return false; + } + JigsawStudioBay bay = findBay(studio.generator().getSession().layout(), bayId); + if (bay == null) { + message(player, "No Jigsaw Studio bay matches '" + bayId + "'."); + return false; + } + return startSave(studio, player.getWorld(), bay, player, true) == SaveAttempt.STARTED; + } + + private SaveAttempt startSave( + ActiveStudio studio, + World world, + JigsawStudioBay bay, + Player player, + boolean report + ) { + UUID requestId = studio.generator().getRequest().requestId(); + if (reopenRequiredRequests.contains(requestId)) { + report(player, report, "Close and reopen Jigsaw Studio before saving the resized layout."); + return SaveAttempt.DEFERRED; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioVariant activeVariant = session.activeVariant(bay.stableId()).orElse(null); + if (activeVariant == null) { + report(player, report, "Workcell '" + bay.stableId() + "' has no active variant to save."); + return SaveAttempt.DEFERRED; + } + if (!activeVariant.owned()) { + report(player, report, "Variant '" + activeVariant.pieceKey() + + "' is read-only. Adopt or clone its graph before editing it."); + return SaveAttempt.DEFERRED; + } + BayReadiness readiness = studio.population(bay).readiness(); + if (!readiness.ready()) { + report(player, report, readinessMessage(bay, readiness)); + return readiness.failure().isEmpty() ? SaveAttempt.RETRY : SaveAttempt.DEFERRED; + } + CaptureTarget captureTarget; + try { + captureTarget = resolveCaptureTarget(studio, bay, activeVariant); + } catch (IOException exception) { + String failure = "Jigsaw Studio cannot capture this workcell: " + failureMessage(exception); + report(player, report, failure); + return retainCurrentAutosaveFailure( + studio, + bay.stableId(), + new AutosavePersistentFailure(failure, exception)) + ? SaveAttempt.PERSISTENT_FAILURE + : SaveAttempt.DEFERRED; + } + JigsawStudioSession.SaveStart sessionSave = session.beginSave(bay.stableId()); + if (sessionSave.status() != JigsawStudioSession.SaveStatus.STARTED) { + report(player, report, switch (sessionSave.status()) { + case SWITCH_IN_PROGRESS -> "This workcell is loading another variant."; + case SAVE_IN_PROGRESS -> "This workcell is already saving."; + case NO_ACTIVE_VARIANT -> "This workcell has no active variant."; + case UNKNOWN_WORKCELL -> "This workcell is no longer active."; + case STARTED -> "The workcell save could not start."; + }); + return sessionSave.status() == JigsawStudioSession.SaveStatus.SAVE_IN_PROGRESS + || sessionSave.status() == JigsawStudioSession.SaveStatus.SWITCH_IN_PROGRESS + ? SaveAttempt.RETRY + : SaveAttempt.DEFERRED; + } + JigsawStudioSession.SaveIdentity saveIdentity = sessionSave.identity().orElseThrow(); + SaveStart saveStart = tryBeginSave(requestId); + if (saveStart == SaveStart.CLOSING) { + session.abortSave(saveIdentity); + report(player, report, "This Jigsaw Studio is closing and cannot start another save."); + return SaveAttempt.DEFERRED; + } + if (saveStart == SaveStart.IN_PROGRESS) { + session.abortSave(saveIdentity); + report(player, report, "A save is already running for this Jigsaw Studio."); + return SaveAttempt.RETRY; + } + if (saveStart == SaveStart.GRAPH_OPERATION) { + session.abortSave(saveIdentity); + report(player, report, "Wait for the current Jigsaw Studio graph update to finish."); + return SaveAttempt.RETRY; + } + if (saveStart == SaveStart.EXPORT_OPERATION) { + session.abortSave(saveIdentity); + report(player, report, "Wait for the current Jigsaw Studio export to finish."); + return SaveAttempt.RETRY; + } + if (saveStart == SaveStart.VARIANT_OPERATION) { + session.abortSave(saveIdentity); + report(player, report, "Wait for the current Jigsaw Studio variant load or rollback to finish."); + return SaveAttempt.RETRY; + } + if (report) { + session.selectBay(bay.stableId()); + } + refreshWorkcellContext(studio.worldId(), bay.stableId()); + report(player, report, "Capturing variant '" + activeVariant.pieceKey() + "' from " + + bay.stableId() + "..."); + try { + boolean scheduled = scheduleCapture( + studio, + world, + bay, + captureTarget, + player, + requestId, + saveIdentity, + autosaveFailureState(studio, saveIdentity)); + return scheduled ? SaveAttempt.STARTED : SaveAttempt.DEFERRED; + } catch (Throwable exception) { + session.abortSave(saveIdentity); + finishSave(requestId); + refreshWorkcellContext(studio.worldId(), bay.stableId()); + IrisLogging.reportError(exception); + report(player, report, "Jigsaw Studio could not schedule the workcell capture: " + + failureMessage(exception)); + return SaveAttempt.DEFERRED; + } + } + + public boolean teleportTo(Player player, String bayId) { + if (player == null || bayId == null || bayId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> teleportTo(player, bayId)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + if (!authorizeOwner(player, studio)) { + return false; + } + if (reopenRequiredRequests.contains(studio.generator().getRequest().requestId())) { + message(player, "Close and reopen Jigsaw Studio before loading variants in the resized layout."); + return false; + } + JigsawStudioBay bay = findBay(studio.generator().getSession().layout(), bayId); + if (bay == null) { + message(player, "No Jigsaw Studio bay matches '" + bayId + "'."); + return false; + } + JigsawStudioBounds bounds = bay.bounds(); + Location destination = new Location( + player.getWorld(), + bounds.originX() + bounds.dimensions().width() / 2.0D, + bounds.maxY() + 2.0D, + bounds.originZ() + bounds.dimensions().depth() / 2.0D, + player.getLocation().getYaw(), + 45.0F + ); + studio.generator().getSession().selectBay(bay.stableId()); + BukkitPlatform.teleportAsync(player, destination).thenRun(() -> J.runEntity( + player, + () -> reconcilePlayerContext(player, player.getLocation()))); + message(player, "Selected Jigsaw Studio workcell '" + bay.stableId() + "'."); + ensureVisualizationLoop(player); + return true; + } + + @Override + public boolean teleportToWorkcell(Player player, String workcellId) { + return teleportTo(player, workcellId); + } + + @Override + public boolean setConnectorBlocksVisible(Player player, String workcellId, boolean visible) { + if (player == null || workcellId == null || workcellId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> setConnectorBlocksVisible(player, workcellId, visible)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + if (workcell == null) { + message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'."); + return false; + } + JigsawStudioSession.WorkcellSnapshot snapshot = session.workcellSnapshot(workcellId); + if (snapshot.connectorsVisible() == visible) { + message(player, "Connector blocks are already " + (visible ? "visible." : "hidden.")); + return false; + } + JigsawStudioSession.SwitchStart start = session.beginVariantReload(workcellId); + if (start.status() != JigsawStudioSession.SwitchStatus.STARTED) { + message(player, switch (start.status()) { + case DIRTY -> "Wait for this workcell to finish autosaving before changing connector visibility."; + case SAVE_IN_PROGRESS -> "Wait for the current workcell save to finish."; + case SWITCH_IN_PROGRESS -> "This workcell is already loading another view."; + case UNKNOWN_WORKCELL -> "The selected workcell no longer exists."; + case UNKNOWN_VARIANT -> "This workcell has no active variant."; + case ALREADY_ACTIVE, WRONG_WORKCELL, STARTED -> + "Connector visibility could not change: " + start.status() + "."; + }); + return false; + } + JigsawStudioSession.VariantSwitchToken token = start.token().orElseThrow(); + JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderVariant( + workcell, + token.targetVariant()); + if (!rendered.valid()) { + session.abortVariantSwitch(token); + message(player, "Connector visibility cannot change: " + rendered.failure()); + return false; + } + message(player, (visible ? "Showing" : "Hiding") + " connector blocks in " + workcellId + "..."); + return scheduleMaterialization(new MaterializationWork( + studio, + player.getWorld(), + player, + workcell, + token, + rendered, + rendered, + snapshot.connectorsVisible(), + visible, + true)); + } + + @Override + public boolean resetConnectorBlocks(Player player, String workcellId) { + if (player == null || workcellId == null || workcellId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> resetConnectorBlocks(player, workcellId)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + if (workcell == null) { + message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'."); + return false; + } + JigsawStudioVariant activeVariant = session.activeVariant(workcellId).orElse(null); + if (activeVariant == null || !activeVariant.owned()) { + message(player, "Load an owned variant before resetting its connector blocks."); + return false; + } + JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderVariant( + workcell, + activeVariant); + String validationFailure = validateMaterialization(rendered); + if (!validationFailure.isEmpty()) { + message(player, "Connector blocks cannot reset: " + validationFailure); + return false; + } + if (rendered.connectors().isEmpty()) { + message(player, "The active variant has no saved connectors to reset."); + return false; + } + String reservationFailure = beginConnectorRepair(studio); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + boolean connectorsVisible = session.workcellSnapshot(workcellId).connectorsVisible(); + message(player, "Resetting " + rendered.connectors().size() + + " connector block(s) from the last saved iteration..."); + return scheduleConnectorRepair( + player, + studio, + workcell, + rendered, + connectorsVisible); + } + + @Override + public boolean undoAutosave(Player player) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> undoAutosave(player)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + Map activeReloads = new HashMap<>(); + for (JigsawStudioBay workcell : session.layout().bays()) { + session.activeVariant(workcell.stableId()).ifPresent(variant -> activeReloads.put( + variant.pieceKey(), + new VariantReloadRequest( + workcell.stableId(), + studio.generator().renderBay(workcell)))); + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + message(player, "Restoring the previous Jigsaw Studio autosave iteration..."); + return scheduleGraphMutation( + player, + studio, + () -> { + Path packRoot = request.source().getDataFolder().toPath(); + JigsawStudioHistoryStore.UndoResult result = new JigsawStudioHistoryStore( + packRoot, + request.structureKey()).undoLatest(); + if (!result.available()) { + return new CommandGraphMutationResult( + session.layout(), + "", + "", + "No earlier autosave iteration is available."); + } + if (!result.successful()) { + throw new IOException("Jigsaw Studio undo failed: " + + writeFailure(result.writeResult())); + } + request.source().invalidateStructureResources(); + JigsawStudioLayout restoredLayout = loadMappedLayout(studio); + VariantReloadRequest reload = activeReloads.get(result.pieceKey()); + Optional activeReload = reload == null + || restoredLayout.get(reload.workcellId()) == null + ? Optional.empty() + : Optional.of(reload); + String warning = result.warning().isEmpty() + ? "" + : " History cleanup warning: " + result.warning(); + return new CommandGraphMutationResult( + restoredLayout, + "", + "", + Map.of(), + activeReload, + "Restored the previous autosave iteration. " + + result.remainingIterations() + " earlier iteration(s) remain." + + warning); + }); + } + + public boolean setParticles(Player player, boolean visible) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> setParticles(player, visible)); + } + if (!studios.containsKey(player.getWorld().getUID())) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + if (visible) { + particlesDisabled.remove(player.getUniqueId()); + ensureVisualizationLoop(player); + } else { + particlesDisabled.add(player.getUniqueId()); + } + message(player, "Jigsaw Studio particles " + (visible ? "enabled." : "disabled.")); + return true; + } + + public boolean switchVariant( + Player player, + String workcellId, + String targetPieceKey, + boolean discardDirty + ) { + if (player == null || workcellId == null || targetPieceKey == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> switchVariant( + player, workcellId, targetPieceKey, discardDirty)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + if (!authorizeOwner(player, studio)) { + return false; + } + if (graphMutationInProgress(studio.generator().getRequest().requestId())) { + message(player, "Wait for the current Jigsaw Studio graph update to finish."); + return false; + } + UUID requestId = studio.generator().getRequest().requestId(); + finalizeJigsawTileWatches(requestId); + if (hasJigsawTileWatch(requestId)) { + message(player, "Wait for the open vanilla jigsaw-block editor to finish its final snapshot."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + if (workcell == null) { + message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'."); + return false; + } + JigsawStudioSession.SwitchStart start = session.beginVariantSwitch( + workcell.stableId(), targetPieceKey, discardDirty); + if (start.status() != JigsawStudioSession.SwitchStatus.STARTED) { + message(player, switch (start.status()) { + case DIRTY -> "Wait for the current variant to finish autosaving before switching."; + case SAVE_IN_PROGRESS -> "Wait for the current variant save to finish."; + case SWITCH_IN_PROGRESS -> "This workcell is already loading another variant."; + case ALREADY_ACTIVE -> "Variant '" + targetPieceKey + "' is already active."; + case WRONG_WORKCELL -> "Variant '" + targetPieceKey + "' belongs to another workcell."; + case UNKNOWN_VARIANT -> "No variant '" + targetPieceKey + "' exists in this Studio catalog."; + case UNKNOWN_WORKCELL -> "The selected workcell no longer exists."; + case STARTED -> "The variant switch could not start."; + }); + return false; + } + return materializeVariant(player, studio, workcell, start, null); + } + + private void reloadActiveVariant( + Player player, + ActiveStudio studio, + VariantReloadRequest reload + ) { + if (!isCurrentRequest(studio, studio.generator().getRequest().requestId())) { + return; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(reload.workcellId()); + if (workcell == null) { + message(player, "The resized variant was saved, but its workcell is no longer active. Reopen Studio."); + return; + } + JigsawStudioSession.SwitchStart start = session.beginVariantReload(workcell.stableId()); + if (start.status() != JigsawStudioSession.SwitchStatus.STARTED) { + reopenRequiredRequests.add(studio.generator().getRequest().requestId()); + message(player, "The resized variant was saved, but Studio could not reload it: " + + start.status() + ". Close and reopen this project before editing that workcell."); + return; + } + materializeVariant(player, studio, workcell, start, reload.previous()); + } + + private boolean materializeVariant( + Player player, + ActiveStudio studio, + JigsawStudioBay workcell, + JigsawStudioSession.SwitchStart start, + JigsawStudioGenerator.RenderedBay previousOverride + ) { + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioSession.VariantSwitchToken token = start.token().orElseThrow(); + String targetPieceKey = token.targetVariant().pieceKey(); + JigsawStudioGenerator.RenderedBay target = studio.generator().renderVariant( + workcell, + token.targetVariant()); + if (!target.valid()) { + session.abortVariantSwitch(token); + refreshWorkcellContext(studio.worldId(), workcell.stableId()); + message(player, "Variant '" + targetPieceKey + "' cannot load: " + target.failure()); + return false; + } + JigsawStudioGenerator.RenderedBay previous = previousOverride == null + ? token.previousVariant() + .map(variant -> studio.generator().renderVariant(workcell, variant)) + .orElseGet(() -> JigsawStudioGenerator.RenderedBay.empty(workcell.bounds().dimensions())) + : previousOverride; + if (!previous.valid()) { + session.abortVariantSwitch(token); + refreshWorkcellContext(studio.worldId(), workcell.stableId()); + message(player, "The current variant cannot be retained for rollback: " + previous.failure()); + return false; + } + JigsawStudioVariant previousVariant = token.previousVariant().orElse(null); + if (previousVariant != null) { + String rollbackFailure = validateMaterialization(previous); + if (!rollbackFailure.isEmpty()) { + session.abortVariantSwitch(token); + refreshWorkcellContext(studio.worldId(), workcell.stableId()); + message(player, "The current variant cannot be retained for rollback: " + rollbackFailure); + return false; + } + } + session.selectBay(workcell.stableId()); + refreshWorkcellContext(studio.worldId(), workcell.stableId()); + message(player, "Loading variant '" + targetPieceKey + "' into " + workcell.stableId() + "..."); + return scheduleMaterialization(new MaterializationWork( + studio, + player.getWorld(), + player, + workcell, + token, + previous, + target, + session.workcellSnapshot(workcell.stableId()).connectorsVisible(), + session.workcellSnapshot(workcell.stableId()).connectorsVisible(), + false)); + } + + public boolean createVariant( + Player player, + String workcellId, + boolean duplicateActive + ) { + if (player == null || workcellId == null || workcellId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> createVariant(player, workcellId, duplicateActive)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + if (!authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + if (workcell == null) { + message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'."); + return false; + } + if (!canCreateVariants(session.layout())) { + message(player, "This Jigsaw Studio graph is read-only. " + + "Adopt or clone it before creating variants."); + return false; + } + JigsawStudioVariant activeVariant = session.activeVariant(workcellId).orElse(null); + String sourceFailure = variantCreationSourceFailure(activeVariant, duplicateActive); + if (!sourceFailure.isEmpty()) { + message(player, sourceFailure); + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + if (duplicateActive && deferDuplicationUntilAutosaved( + player, + studio, + DeferredDuplication.single( + request.requestId(), + session.sessionId(), + player, + studio.worldId(), + workcell.stableId(), + activeVariant.pieceKey()))) { + return true; + } + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + String archetypeKey = workcell.archetype() + .map(archetype -> archetype.name().toLowerCase(Locale.ROOT)) + .orElse("spatial"); + String sourcePieceKey = activeVariant.pieceKey(); + message(player, duplicateActive ? "Duplicating the active variant..." : "Creating a new variant..."); + return scheduleGraphMutation( + player, + studio, + () -> { + Path packRoot = request.source().getDataFolder().toPath(); + String pieceKey = JigsawStudioGraphEditor.nextVariantKey( + packRoot, request.structureKey(), archetypeKey); + if (duplicateActive) { + JigsawStudioGraphEditor.duplicatePiece( + packRoot, + request.structureKey(), + sourcePieceKey, + pieceKey); + } else { + JigsawStudioGraphEditor.createBlankVariant( + packRoot, + request.structureKey(), + sourcePieceKey, + pieceKey); + } + JigsawStudioLayout updatedLayout = loadMappedLayout(studio); + String targetWorkcellId = updatedLayout.workcellForVariant(pieceKey) + .map(JigsawStudioBay::stableId) + .orElseThrow(() -> new IOException( + "Created variant '" + pieceKey + "' has no Studio workcell")); + if (updatedLayout.mode() == JigsawStudioMode.SPATIAL_JIGSAW) { + return new CommandGraphMutationResult( + updatedLayout, + "", + "", + (duplicateActive ? "Duplicated" : "Created") + " variant '" + pieceKey + + "' in " + targetWorkcellId + "."); + } + return new CommandGraphMutationResult( + updatedLayout, + targetWorkcellId, + pieceKey, + (duplicateActive ? "Duplicated" : "Created") + " variant '" + pieceKey + "'."); + }); + } + + @Override + public boolean setWorkcellEnabled(Player player, String workcellId, boolean enabled) { + if (player == null || workcellId == null || workcellId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> setWorkcellEnabled(player, workcellId, enabled)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + if (workcell == null || workcell.archetype().isEmpty()) { + message(player, "Only planar Jigsaw Studio workcells can be enabled or disabled."); + return false; + } + if (workcell.enabled() == enabled) { + message(player, "Workcell '" + workcellId + "' is already " + + (enabled ? "enabled." : "disabled.")); + return false; + } + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + JigsawPlanarArchetype archetype = workcell.archetype().orElseThrow(); + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioStructureEditor.updateWorkcellEnabled( + request.source().getDataFolder().toPath(), + request.structureKey(), + archetype, + enabled); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Workcell '" + workcellId + "' is now " + + (enabled ? "enabled." : "disabled for assembly and export.")); + }); + } + + @Override + public boolean updateWorkcellDimensions( + Player player, + String workcellId, + JigsawStudioCellDimensions dimensions + ) { + if (player == null || workcellId == null || workcellId.isBlank() || dimensions == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> updateWorkcellDimensions(player, workcellId, dimensions)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + if (workcell == null) { + message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'."); + return false; + } + if (workcell.capacity().equals(dimensions)) { + message(player, "Workcell '" + workcellId + "' already has capacity " + + dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth() + "."); + return false; + } + if (workcell.archetype().isPresent() + && (dimensions.width() < 3 || dimensions.depth() < 3)) { + message(player, "Planar workcell width and depth must each be at least 3 blocks."); + return false; + } + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + JigsawPlanarArchetype archetype = workcell.archetype().orElse(null); + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioGraphEditor.WorkcellCapacityResult capacityResult = null; + if (archetype == null) { + JigsawStudioStructureEditor.updateCellSize( + request.source().getDataFolder().toPath(), + request.structureKey(), + dimensions); + } else { + capacityResult = JigsawStudioGraphEditor.updatePlanarWorkcellCapacity( + request.source().getDataFolder().toPath(), + request.structureKey(), + archetype, + dimensions); + } + request.source().invalidateStructureResources(); + String resizeSummary = capacityResult == null + ? "" + : " Verified " + capacityResult.checkedVariants() + " existing variant" + + (capacityResult.checkedVariants() == 1 ? "" : "s") + "; no variant object was resized."; + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Updated '" + workcellId + "' capacity to " + + dimensions.width() + "x" + dimensions.height() + "x" + + dimensions.depth() + "." + resizeSummary + + " The live workcell layout was regenerated."); + }); + } + + @Override + public boolean setRequireCaps(Player player, boolean requireCaps) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> setRequireCaps(player, requireCaps)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE + && requireCaps) { + message(player, "Mandatory caps are Iris-only and cannot be enabled for a vanilla-portable graph."); + return false; + } + IrisStructure structure = loadStudioStructure(studio); + if (structure == null) { + message(player, "The active jigsaw structure could not be loaded."); + return false; + } + if (structure.isRequireCaps() == requireCaps) { + message(player, "Mandatory caps are already " + (requireCaps ? "enabled." : "disabled.")); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioStructureEditor.updateRequireCaps( + request.source().getDataFolder().toPath(), + request.structureKey(), + requireCaps); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Mandatory physical caps are now " + + (requireCaps ? "enabled." : "disabled.")); + }); + } + + @Override + public boolean duplicateActiveFamily(Player player, String themeKey) { + if (player == null || themeKey == null || themeKey.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> duplicateActiveFamily(player, themeKey)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE) { + message(player, "Coherent variant families are Iris-only and cannot be added to a vanilla-portable graph."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + Map sourcePieces = new LinkedHashMap<>(); + for (JigsawStudioBay workcell : session.layout().bays()) { + if (!workcell.enabled()) { + continue; + } + JigsawStudioVariant active = session.activeVariant(workcell.stableId()).orElse(null); + if (active == null || !active.owned()) { + message(player, "Load an owned source variant in every enabled workcell before duplicating a family."); + return false; + } + sourcePieces.put(workcell.stableId(), active.pieceKey()); + } + if (deferDuplicationUntilAutosaved( + player, + studio, + DeferredDuplication.family( + request.requestId(), + session.sessionId(), + player, + studio.worldId(), + sourcePieces, + themeKey))) { + return true; + } + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioGraphEditor.VariantFamilyCreation creation = + JigsawStudioGraphEditor.duplicateActiveFamily( + request.source().getDataFolder().toPath(), + request.structureKey(), + sourcePieces, + themeKey); + if (!creation.writeResult().successful()) { + throw new IOException("Variant-family transaction failed with " + + creation.writeResult().status()); + } + JigsawStudioLayout updatedLayout = loadMappedLayout(studio); + Map rebinds = updatedLayout.mode() == JigsawStudioMode.SPATIAL_JIGSAW + ? Map.of() + : creation.pieceKeysByWorkcell(); + return new CommandGraphMutationResult( + updatedLayout, + "", + "", + rebinds, + Optional.empty(), + "Duplicated every enabled workcell as coherent family '" + themeKey + "' with " + + creation.pieceKeysByWorkcell().size() + " variant(s)."); + }); + } + + @Override + public boolean updateThemeSetWeight(Player player, String themeKey, int weight) { + if (player == null || themeKey == null || themeKey.isBlank() || weight < 1) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> updateThemeSetWeight(player, themeKey, weight)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE) { + message(player, "Coherent theme sets are Iris-only."); + return false; + } + IrisStructure structure = loadStudioStructure(studio); + if (structure == null || structure.getThemeSets() == null) { + message(player, "The active jigsaw structure has no theme sets."); + return false; + } + List updated = new ArrayList<>(structure.getThemeSets().size()); + boolean found = false; + for (IrisJigsawThemeSet theme : structure.getThemeSets()) { + if (theme != null && themeKey.equals(theme.getKey())) { + if (theme.getWeight() == weight) { + message(player, "Theme '" + themeKey + "' already has weight " + weight + "."); + return false; + } + updated.add(new IrisJigsawThemeSet(themeKey, weight)); + found = true; + } else if (theme != null) { + updated.add(new IrisJigsawThemeSet(theme.getKey(), theme.getWeight())); + } + } + if (!found) { + message(player, "Theme set '" + themeKey + "' no longer exists."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioStructureEditor.updateThemeSets( + request.source().getDataFolder().toPath(), + request.structureKey(), + updated); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Set theme '" + themeKey + "' weight to " + weight + "."); + }); + } + + @Override + public boolean updateVariantThemes( + Player player, + String workcellId, + String pieceKey, + List themes + ) { + if (player == null || pieceKey == null || themes == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> updateVariantThemes( + player, workcellId, pieceKey, themes)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioVariant variant = activeOwnedVariant(player, studio, workcellId, pieceKey); + if (variant == null) { + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + List targetThemes = List.copyOf(themes); + if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE + && !targetThemes.isEmpty()) { + message(player, "Piece themes are Iris-only and cannot be added to a vanilla-portable graph."); + return false; + } + IrisStructure structure = loadStudioStructure(studio); + Set declaredThemes = new HashSet<>(); + if (structure != null && structure.getThemeSets() != null) { + for (IrisJigsawThemeSet theme : structure.getThemeSets()) { + if (theme != null) { + declaredThemes.add(theme.getKey()); + } + } + } + if (!declaredThemes.containsAll(targetThemes)) { + message(player, "One or more selected theme sets no longer exist."); + return false; + } + if (variant.themes().equals(targetThemes)) { + message(player, "Variant theme membership is unchanged."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioGraphEditor.updatePieceThemes( + request.source().getDataFolder().toPath(), + request.structureKey(), + variant.pieceKey(), + targetThemes); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Updated theme membership for '" + variant.pieceKey() + "'."); + }); + } + + @Override + public boolean updateVariantRules( + Player player, + String workcellId, + String pieceKey, + JigsawStudioPieceRules rules + ) { + if (player == null || pieceKey == null || rules == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> updateVariantRules( + player, workcellId, pieceKey, rules)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioVariant variant = activeOwnedVariant(player, studio, workcellId, pieceKey); + if (variant == null) { + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE + && !DEFAULT_PIECE_RULES.equals(rules)) { + message(player, "Piece placement rules are Iris-only for a vanilla-portable graph."); + return false; + } + if (variant.rules().equals(rules)) { + message(player, "Variant piece rules are unchanged."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioGraphEditor.updatePieceRules( + request.source().getDataFolder().toPath(), + request.structureKey(), + variant.pieceKey(), + rules); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Updated placement rules for '" + variant.pieceKey() + "'."); + }); + } + + public boolean toggleVariantRotatable(Player player, String workcellId) { + if (player == null || workcellId == null || workcellId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> toggleVariantRotatable(player, workcellId)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio != null && !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioVariant variant = activeOwnedVariant(player, studio, workcellId); + if (variant == null) { + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + if (!canToggleVariantRotation(request.compatibilityTarget(), variant)) { + message(player, "Vanilla-portable variants must remain rotatable."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + boolean rotatable = !variant.rotatable(); + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioGraphEditor.updateRotatable( + request.source().getDataFolder().toPath(), + request.structureKey(), + variant.pieceKey(), + rotatable); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Variant rotation is now " + (rotatable ? "enabled." : "disabled.")); + }); + } + + public boolean expandVariantToCell(Player player, String workcellId) { + if (player == null || workcellId == null || workcellId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> expandVariantToCell(player, workcellId)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio != null && !authorizeOwner(player, studio)) { + return false; + } + if (studio == null) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioVariant variant = activeOwnedVariant(player, studio, workcellId); + JigsawStudioBay workcell = session.layout().get(workcellId); + if (variant == null || workcell == null) { + return false; + } + return resizeVariant(player, workcellId, variant.pieceKey(), workcell.capacity()); + } + + @Override + public boolean resizeVariant( + Player player, + String workcellId, + String pieceKey, + JigsawStudioCellDimensions dimensions + ) { + if (player == null || workcellId == null || workcellId.isBlank() + || pieceKey == null || pieceKey.isBlank() || dimensions == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> resizeVariant(player, workcellId, pieceKey, dimensions)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + JigsawStudioVariant variant = session.layout().variantCatalog().find(pieceKey).orElse(null); + if (workcell == null || variant == null || !session.layout().accepts(workcell, variant)) { + message(player, "Variant '" + pieceKey + "' does not belong to workcell '" + workcellId + "'."); + return false; + } + if (!variant.owned()) { + message(player, "Variant '" + pieceKey + "' is read-only. Duplicate it before resizing."); + return false; + } + if (dimensions.width() > workcell.capacity().width() + || dimensions.height() > workcell.capacity().height() + || dimensions.depth() > workcell.capacity().depth()) { + message(player, "Requested variant size " + dimensions.width() + "x" + dimensions.height() + "x" + + dimensions.depth() + " exceeds workcell capacity " + workcell.capacity().width() + "x" + + workcell.capacity().height() + "x" + workcell.capacity().depth() + + ". Increase the workcell capacity first."); + return false; + } + if (variant.mode() == JigsawStudioMode.PLANAR_JIGSAW + && (dimensions.width() < 3 || dimensions.depth() < 3)) { + message(player, "Planar variant width and depth must each be at least 3 blocks."); + return false; + } + if (variant.dimensions().filter(dimensions::equals).isPresent()) { + message(player, "Variant '" + variant.resolvedDisplayName() + "' already uses " + + dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth() + "."); + return false; + } + boolean active = session.activeVariant(workcellId) + .map(activeVariant -> activeVariant.pieceKey().equals(pieceKey)) + .orElse(false); + JigsawStudioGenerator.RenderedBay previous = active + ? studio.generator().renderVariant(workcell, variant) + : null; + if (previous != null && !previous.valid()) { + message(player, "The active variant cannot be retained for live resize rollback: " + previous.failure()); + return false; + } + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + message(player, "Resizing variant '" + variant.resolvedDisplayName() + "' to " + + dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth() + "..."); + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioGraphEditor.VariantResizeResult resize = + JigsawStudioGraphEditor.resizePieceObject( + request.source().getDataFolder().toPath(), + request.structureKey(), + variant.pieceKey(), + dimensions); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + Map.of(), + active ? Optional.of(new VariantReloadRequest(workcellId, previous)) : Optional.empty(), + "Resized variant '" + variant.resolvedDisplayName() + "' from " + + resize.previousDimensions().width() + "x" + + resize.previousDimensions().height() + "x" + + resize.previousDimensions().depth() + " to " + + dimensions.width() + "x" + dimensions.height() + "x" + + dimensions.depth() + ". " + resize.relocatedConnectors() + + " connector(s) moved with the new bounds."); + }); + } + + @Override + public boolean updateVariantDisplayName( + Player player, + String workcellId, + String pieceKey, + String displayName + ) { + if (player == null || workcellId == null || pieceKey == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> updateVariantDisplayName( + player, workcellId, pieceKey, displayName)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + JigsawStudioVariant variant = session.layout().variantCatalog().find(pieceKey).orElse(null); + if (workcell == null || variant == null || !session.layout().accepts(workcell, variant)) { + message(player, "Variant '" + pieceKey + "' does not belong to workcell '" + workcellId + "'."); + return false; + } + if (!variant.owned()) { + message(player, "Variant '" + pieceKey + "' is read-only. Duplicate it before renaming."); + return false; + } + String normalizedName; + try { + normalizedName = JigsawStudioGraphEditor.normalizeDisplayName(displayName); + } catch (IllegalArgumentException exception) { + message(player, exception.getMessage()); + return false; + } + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioGraphEditor.updatePieceDisplayName( + request.source().getDataFolder().toPath(), + request.structureKey(), + pieceKey, + normalizedName); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + normalizedName.isEmpty() + ? "Reset the variant label to its resource-key fallback." + : "Renamed the variant to '" + normalizedName + "'."); + }); + } + + @Override + public boolean updateWorkcellDisplayName( + Player player, + String workcellId, + String displayName + ) { + if (player == null || workcellId == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> updateWorkcellDisplayName(player, workcellId, displayName)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + if (workcell == null) { + message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'."); + return false; + } + String normalizedName; + try { + normalizedName = JigsawStudioGraphEditor.normalizeDisplayName(displayName); + } catch (IllegalArgumentException exception) { + message(player, exception.getMessage()); + return false; + } + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + JigsawPlanarArchetype archetype = workcell.archetype().orElse(null); + return scheduleGraphMutation( + player, + studio, + () -> { + if (archetype == null) { + JigsawStudioStructureEditor.updateSpatialWorkcellDisplayName( + request.source().getDataFolder().toPath(), + request.structureKey(), + normalizedName); + } else { + JigsawStudioStructureEditor.updateWorkcellDisplayName( + request.source().getDataFolder().toPath(), + request.structureKey(), + archetype, + normalizedName); + } + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + normalizedName.isEmpty() + ? "Reset the workcell label to '" + workcell.canonicalDisplayName() + "'." + : "Renamed the workcell to '" + normalizedName + "'."); + }); + } + + @Override + public boolean adjustVariantWeight( + Player player, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex, + int delta + ) { + if (delta == 0) { + return false; + } + if (player == null || workcellId == null || pieceKey == null || poolKey == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> adjustVariantWeight( + player, workcellId, pieceKey, poolKey, entryIndex, delta)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio != null && !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioVariant variant = activeOwnedVariant(player, studio, workcellId, pieceKey); + if (variant == null) { + return false; + } + JigsawStudioPoolMembership membership = findMembership( + variant, poolKey, entryIndex); + if (membership == null) { + message(player, "That exact pool membership is no longer present."); + return false; + } + int weight; + try { + weight = Math.addExact(membership.weight(), delta); + } catch (ArithmeticException exception) { + message(player, "The requested variant weight is outside the supported integer range."); + return false; + } + if (weight < 1) { + message(player, "Variant weights cannot be lower than 1; unlink the membership instead."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioPoolEditor.updateWeightAtIndex( + request.source().getDataFolder().toPath(), + request.structureKey(), + membership.poolKey(), + membership.entryIndex(), + variant.pieceKey(), + weight); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Set '" + variant.pieceKey() + "' weight to " + weight + + " in pool '" + membership.poolKey() + "'."); + }); + } + + @Override + public boolean adjustVariantChance( + Player player, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex, + int deltaPercentagePoints + ) { + if (deltaPercentagePoints == 0 || player == null || workcellId == null + || pieceKey == null || poolKey == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> adjustVariantChance( + player, + workcellId, + pieceKey, + poolKey, + entryIndex, + deltaPercentagePoints)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio != null && !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioVariant variant = activeOwnedVariant(player, studio, workcellId, pieceKey); + if (variant == null) { + return false; + } + JigsawStudioPoolMembership membership = findMembership(variant, poolKey, entryIndex); + if (membership == null) { + message(player, "That exact pool membership is no longer present."); + return false; + } + double chance = membership.chance() + deltaPercentagePoints / 100.0D; + if (!Double.isFinite(chance) || chance < 0.0D || chance > 1.0D) { + message(player, "Variant chance must stay between 0% and 100%."); + return false; + } + chance = Math.round(chance * 1_000_000.0D) / 1_000_000.0D; + JigsawStudioActivation.Request request = studio.generator().getRequest(); + if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE + && chance != 1.0D) { + message(player, "Chance gates are Iris-only and cannot be added to a vanilla-portable graph."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + double targetChance = chance; + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioPoolEditor.updateChanceAtIndex( + request.source().getDataFolder().toPath(), + request.structureKey(), + membership.poolKey(), + membership.entryIndex(), + variant.pieceKey(), + targetChance); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Set '" + variant.pieceKey() + "' chance to " + + Math.round(targetChance * 100.0D) + "% in pool '" + + membership.poolKey() + "'."); + }); + } + + @Override + public boolean deleteVariant(Player player, String workcellId, String pieceKey) { + if (player == null || workcellId == null || workcellId.isBlank() + || pieceKey == null || pieceKey.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> deleteVariant(player, workcellId, pieceKey)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().get(workcellId); + JigsawStudioVariant variant = session.layout().variantCatalog().find(pieceKey).orElse(null); + if (workcell == null || variant == null || !session.layout().accepts(workcell, variant)) { + message(player, "Variant '" + pieceKey + "' no longer belongs to workcell '" + workcellId + "'."); + return false; + } + if (!variant.owned()) { + message(player, "Variant '" + pieceKey + "' is read-only. Adopt or clone it before deletion."); + return false; + } + String activePieceKey = session.activeVariant(workcellId) + .map(JigsawStudioVariant::pieceKey) + .orElse(""); + if (session.layout().mode() == JigsawStudioMode.PLANAR_JIGSAW + && activePieceKey.equals(pieceKey)) { + message(player, "Load another variant in this workcell before deleting '" + pieceKey + "'."); + return false; + } + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioGraphEditor.PieceDeletionResult deletion = + JigsawStudioGraphEditor.deletePieceVariant( + request.source().getDataFolder().toPath(), + request.structureKey(), + pieceKey); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Deleted variant '" + pieceKey + "', removed " + + deletion.removedPoolMemberships() + " pool membership(s), and removed " + + deletion.removedObjectResources() + " unshared object(s)."); + }); + } + + public boolean deleteProject(Player player) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> deleteProject(player)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + CloseStart closeStart = tryBeginClose(request.requestId(), player.getUniqueId(), false); + if (closeStart != CloseStart.STARTED) { + message(player, switch (closeStart) { + case DIRTY -> "Wait for autosave to finish before deleting this project."; + case SAVE_IN_PROGRESS -> "Wait for the active autosave to finish before deleting this project."; + case OPERATION_IN_PROGRESS -> "Wait for the active Jigsaw Studio operation to finish."; + case NOT_OWNER -> "This Jigsaw Studio is owned by another player session."; + case NOT_ACTIVE -> "This Jigsaw Studio session is no longer active."; + case STARTED -> "Project deletion could not start."; + }); + return false; + } + Path packRoot = request.source().getDataFolder().toPath(); + message(player, "Inspecting owned resources and reverse references before project deletion..."); + J.a(() -> inspectProjectDeletion(player, studio, request, packRoot)); + return true; + } + + private void inspectProjectDeletion( + Player player, + ActiveStudio studio, + JigsawStudioActivation.Request request, + Path packRoot + ) { + JigsawStudioProjectDeletionService.DeletionPlan plan; + try { + plan = JigsawStudioProjectDeletionService.inspect(packRoot, request.structureKey()); + } catch (Throwable exception) { + cancelClose(request.requestId()); + IrisLogging.reportError(exception); + message(player, "Project deletion inspection failed: " + failureMessage(exception)); + return; + } + if (!plan.deletable()) { + cancelClose(request.requestId()); + message(player, "Project deletion is blocked by " + plan.blockers().size() + + " external reference(s). The first is " + + plan.blockers().getFirst().ownerPath() + " at " + + plan.blockers().getFirst().location() + "."); + return; + } + boolean scheduled = J.runEntity( + player, + () -> closeAndDeleteProject(player, studio, request, plan), + 0, + () -> cancelClose(request.requestId())); + if (!scheduled) { + cancelClose(request.requestId()); + message(player, "Project deletion stopped because the owner session ended."); + } + } + + private void closeAndDeleteProject( + Player player, + ActiveStudio studio, + JigsawStudioActivation.Request request, + JigsawStudioProjectDeletionService.DeletionPlan plan + ) { + if (!isCurrentRequest(studio, request.requestId())) { + cancelClose(request.requestId()); + message(player, "Project deletion stopped because the Jigsaw Studio session changed."); + return; + } + IrisServices.get(StudioSVC.class).close().whenComplete((result, throwable) -> { + Throwable failure = throwable != null + ? throwable + : result == null ? new IllegalStateException("Studio close completed without a result") + : result.failureCause(); + if (failure != null) { + cancelClose(request.requestId()); + IrisLogging.reportError(failure); + message(player, "Project deletion stopped because Studio could not close: " + + failureMessage(failure)); + return; + } + J.a(() -> applyProjectDeletion(player, request, plan)); + }); + } + + private void applyProjectDeletion( + Player player, + JigsawStudioActivation.Request request, + JigsawStudioProjectDeletionService.DeletionPlan plan + ) { + try { + JigsawStudioProjectDeletionService.ProjectDeletionResult result = + JigsawStudioProjectDeletionService.delete(plan); + try { + clearAutosaveHistory( + request.source().getDataFolder().toPath(), + request.structureKey()); + } catch (IOException historyFailure) { + IrisLogging.reportError(historyFailure); + message(player, "The project graph was deleted, but its autosave history could not be removed: " + + failureMessage(historyFailure)); + } + request.source().invalidateStructureResources(); + message(player, "Deleted Jigsaw project '" + request.structureKey() + "' and " + + result.removedResourceCount() + " owned resource(s)."); + IrisLogging.info("Jigsaw Studio project deleted: structure=%s resources=%d", + request.structureKey(), result.removedResourceCount()); + } catch (Throwable exception) { + IrisLogging.reportError(exception); + message(player, "Studio closed, but the project was not deleted: " + + failureMessage(exception) + ". Its files remain recoverable on disk."); + } + } + + @Override + public boolean unlinkVariantMembership( + Player player, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex + ) { + if (player == null || workcellId == null || pieceKey == null || poolKey == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> unlinkVariantMembership( + player, workcellId, pieceKey, poolKey, entryIndex)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio != null && !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioVariant variant = activeOwnedVariant(player, studio, workcellId, pieceKey); + if (variant == null) { + return false; + } + JigsawStudioPoolMembership membership = findMembership(variant, poolKey, entryIndex); + if (membership == null) { + message(player, "That exact pool membership is no longer present."); + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String reservationFailure = beginGraphMutation(studio, session); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + return scheduleGraphMutation( + player, + studio, + () -> { + JigsawStudioPoolEditor.removeEntry( + request.source().getDataFolder().toPath(), + request.structureKey(), + membership.poolKey(), + membership.entryIndex(), + variant.pieceKey()); + return new CommandGraphMutationResult( + loadMappedLayout(studio), + "", + "", + "Unlinked '" + variant.pieceKey() + "' from pool '" + + membership.poolKey() + "'."); + }); + } + + public boolean previewStudio(Player player, long seed) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> previewStudio(player, seed)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + if (!authorizeOwner(player, studio)) { + return false; + } + Location location = player.getLocation(); + IrisPosition origin = new IrisPosition( + location.getBlockX(), location.getBlockY(), location.getBlockZ()); + JigsawStudioActivation.Request request = studio.generator().getRequest(); + J.a(() -> { + try { + IrisStructure structure = request.source().load( + IrisStructure.class, request.structureKey(), false); + if (structure == null) { + throw new IOException("The active structure resource no longer exists"); + } + StructureAssembler assembler = StructureAssembler.forData( + request.source(), structure, origin); + StructureAssemblyResult assembly = assembler.assemble(new RNG(seed)); + if (!assembly.hasOutput()) { + message(player, "The active graph assembled zero preview pieces."); + return; + } + List pieces = assembly.pieces(); + if (!J.runEntity(player, () -> { + if (isCurrentRequest(studio, request.requestId())) { + showAssemblyPreview(player, pieces); + message(player, "Showing a " + pieces.size() + + "-piece particle preview for 10 seconds."); + } + })) { + message(player, "The preview completed after the player session ended."); + } + } catch (Throwable exception) { + IrisLogging.reportError(exception); + message(player, "Preview assembly failed: " + failureMessage(exception)); + } + }); + return true; + } + + public Optional evaluation(Player player) { + if (player == null || !J.isOwnedByCurrentRegion(player)) { + return Optional.empty(); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !ownerMatches(player, studio)) { + return Optional.empty(); + } + return Optional.ofNullable(evaluations.get(studio.generator().getRequest().requestId())); + } + + public boolean goToPreview(Player player) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> goToPreview(player)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + UUID requestId = studio.generator().getRequest().requestId(); + JigsawStudioPreviewRenderer.PreviewBounds bounds = previewRenderer.bounds(requestId); + if (bounds == null || bounds.isEmpty()) { + JigsawStudioGraphEvaluation evaluation = evaluations.get(requestId); + message(player, evaluation == null + ? "The seed-1337 preview is still being evaluated." + : "The seed-1337 preview has no generated blocks: " + evaluation.detail()); + return false; + } + Location destination = new Location( + studio.world(), + bounds.centerX() + 0.5D, + bounds.maximumY() + 2.0D, + bounds.centerZ() + 0.5D, + player.getLocation().getYaw(), + player.getLocation().getPitch()); + BukkitPlatform.teleportAsync(player, destination).thenRun(() -> J.runEntity( + player, + () -> message(player, "Teleported to the live seed-1337 preview."))); + return true; + } + + private void scheduleInitialEvaluation(ActiveStudio studio) { + if (studio == null) { + return; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + UUID requestId = request.requestId(); + if (!claimInitialEvaluation( + studio.evaluationGeneration(), + isCurrentRequest(studio, requestId))) { + return; + } + publishEvaluation(studio, requestId, 1L); + } + + static boolean claimInitialEvaluation(AtomicLong generation, boolean currentRequest) { + return currentRequest + && Objects.requireNonNull(generation, "Jigsaw Studio evaluation generation") + .compareAndSet(0L, 1L); + } + + private void scheduleEvaluation(ActiveStudio studio) { + if (studio == null) { + return; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + UUID requestId = request.requestId(); + if (!isCurrentRequest(studio, requestId)) { + return; + } + publishEvaluation(studio, requestId, studio.evaluationGeneration().incrementAndGet()); + } + + private void publishEvaluation(ActiveStudio studio, UUID requestId, long generation) { + JigsawStudioGraphEvaluation previous = evaluations.get(requestId); + JigsawStudioPreviewRenderer.PreviewBounds previousBounds = previous == null + ? JigsawStudioPreviewRenderer.PreviewBounds.empty() + : previous.previewBounds(); + evaluations.put(requestId, new JigsawStudioGraphEvaluation( + requestId, + generation, + PREVIEW_SEED, + JigsawStudioEvaluationState.PENDING, + previous == null ? "" : previous.selectedTheme(), + previous == null ? 0 : previous.pieceCount(), + "Compiling the committed graph and assembling seed 1337", + previousBounds)); + refreshAllWorkcellContexts(studio); + J.a(() -> evaluateGraph(studio, requestId, generation)); + } + + private void evaluateGraph(ActiveStudio studio, UUID requestId, long generation) { + EvaluationComputation computation; + try { + JigsawStudioActivation.Request request = studio.generator().getRequest(); + IrisStructure structure = request.source().load( + IrisStructure.class, + request.structureKey(), + false); + if (structure == null) { + throw new IOException("The active structure resource no longer exists"); + } + StructureGraphCompilation compilation = StructureGraphCompiler.compile( + structure, + StructureGraphResolver.forData(request.source())); + StructureGraphDiagnostic firstError = firstDiagnostic( + compilation, + StructureGraphDiagnostic.Severity.ERROR); + if (firstError != null) { + computation = invalidEvaluation( + requestId, + generation, + firstError.code() + ": " + firstError.message()); + } else { + IrisPosition origin = previewOrigin(studio.generator().getLayout(), structure); + StructureAssembler assembler = StructureAssembler.forCompilation(compilation, origin); + StructureAssemblyResult assembly = assembler.assemble(new RNG(PREVIEW_SEED)); + computation = evaluationForAssembly( + requestId, + generation, + compilation, + assembly, + studio.generator().getLayout().mode()); + } + } catch (Throwable exception) { + IrisLogging.reportError(exception); + computation = invalidEvaluation(requestId, generation, failureMessage(exception)); + } + completeEvaluation(studio, computation); + } + + private EvaluationComputation evaluationForAssembly( + UUID requestId, + long generation, + StructureGraphCompilation compilation, + StructureAssemblyResult assembly, + JigsawStudioMode mode + ) throws IOException { + if (assembly.status().isFailure()) { + return invalidEvaluation( + requestId, + generation, + assembly.status().name().toLowerCase(Locale.ROOT) + ": " + assembly.detail()); + } + if (!assembly.hasOutput()) { + return new EvaluationComputation( + new JigsawStudioGraphEvaluation( + requestId, + generation, + PREVIEW_SEED, + JigsawStudioEvaluationState.WARNING, + assembly.selectedTheme(), + 0, + assembly.detail().isEmpty() + ? "Seed 1337 intentionally generated no structure" + : assembly.detail(), + JigsawStudioPreviewRenderer.PreviewBounds.empty()), + JigsawStudioPreviewRenderer.PreviewPlan.empty()); + } + List aligned = alignPreviewPieces(assembly.pieces(), mode); + JigsawStudioPreviewRenderer.PreviewPlan plan = JigsawStudioPreviewRenderer.plan(aligned); + StructureGraphDiagnostic firstWarning = firstDiagnostic( + compilation, + StructureGraphDiagnostic.Severity.WARNING); + JigsawStudioEvaluationState state = firstWarning == null + ? JigsawStudioEvaluationState.VALID + : JigsawStudioEvaluationState.WARNING; + String detail = firstWarning == null + ? "Seed 1337 assembled " + aligned.size() + " piece(s)" + : firstWarning.code() + ": " + firstWarning.message(); + return new EvaluationComputation( + new JigsawStudioGraphEvaluation( + requestId, + generation, + PREVIEW_SEED, + state, + assembly.selectedTheme(), + aligned.size(), + detail, + plan.bounds()), + plan); + } + + private void completeEvaluation(ActiveStudio studio, EvaluationComputation computation) { + JigsawStudioGraphEvaluation evaluated = computation.evaluation(); + UUID requestId = evaluated.requestId(); + if (!isCurrentEvaluation(studio, requestId, evaluated.generation())) { + return; + } + previewRenderer.render( + studio.world(), + requestId, + evaluated.generation(), + computation.plan(), + result -> { + if (!isCurrentEvaluation(studio, requestId, evaluated.generation())) { + return; + } + JigsawStudioGraphEvaluation completed = result.successful() + ? evaluated + : new JigsawStudioGraphEvaluation( + requestId, + evaluated.generation(), + PREVIEW_SEED, + JigsawStudioEvaluationState.WARNING, + evaluated.selectedTheme(), + evaluated.pieceCount(), + evaluated.detail() + "; " + result.failure(), + evaluated.previewBounds()); + evaluations.put(requestId, completed); + refreshAllWorkcellContexts(studio); + }); + } + + private boolean isCurrentEvaluation(ActiveStudio studio, UUID requestId, long generation) { + if (!isCurrentRequest(studio, requestId) + || studio.evaluationGeneration().get() != generation) { + return false; + } + JigsawStudioGraphEvaluation current = evaluations.get(requestId); + return current != null && current.generation() == generation; + } + + private static EvaluationComputation invalidEvaluation( + UUID requestId, + long generation, + String detail + ) { + return new EvaluationComputation( + new JigsawStudioGraphEvaluation( + requestId, + generation, + PREVIEW_SEED, + JigsawStudioEvaluationState.INVALID, + "", + 0, + detail, + JigsawStudioPreviewRenderer.PreviewBounds.empty()), + JigsawStudioPreviewRenderer.PreviewPlan.empty()); + } + + private static StructureGraphDiagnostic firstDiagnostic( + StructureGraphCompilation compilation, + StructureGraphDiagnostic.Severity severity + ) { + for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) { + if (diagnostic.severity() == severity) { + return diagnostic; + } + } + return null; + } + + private static IrisPosition previewOrigin(JigsawStudioLayout layout, IrisStructure structure) { + int radius = Math.max(1, structure.getMaxSizeChunks()) * 16; + return new IrisPosition( + -radius - 32, + 0, + Math.max(16, layout.extentZ() / 2)); + } + + static List alignPreviewPieces( + List pieces, + JigsawStudioMode mode + ) { + int minimumY = Integer.MAX_VALUE; + for (PlacedStructurePiece piece : pieces) { + minimumY = Math.min(minimumY, piece.getMinY()); + } + int baseY = mode == JigsawStudioMode.SPATIAL_JIGSAW + ? SPATIAL_PREVIEW_BASE_Y + : JigsawStudioLayout.FLOOR_Y + 1; + int shiftY = baseY - minimumY; + List aligned = new ArrayList<>(pieces.size()); + for (PlacedStructurePiece piece : pieces) { + aligned.add(new PlacedStructurePiece( + piece.getPiece(), + piece.getObject(), + piece.getX(), + piece.getY() + shiftY, + piece.getZ(), + piece.getRotation(), + piece.getMinX(), + piece.getMinY() + shiftY, + piece.getMinZ(), + piece.getMaxX(), + piece.getMaxY() + shiftY, + piece.getMaxZ())); + } + return List.copyOf(aligned); + } + + private void refreshAllWorkcellContexts(ActiveStudio studio) { + scheduleOnlinePlayers(studio.worldId()); + } + + private static IrisStructure loadStudioStructure(ActiveStudio studio) { + JigsawStudioActivation.Request request = studio.generator().getRequest(); + return request.source().load(IrisStructure.class, request.structureKey(), false); + } + + private static boolean canResizeVariantToCapacity( + JigsawStudioBay workcell, + JigsawStudioVariant variant, + boolean active + ) { + if (!active || !variant.owned()) { + return false; + } + JigsawStudioCellDimensions canonical = variant.dimensions().orElse(null); + if (canonical == null) { + return false; + } + JigsawStudioCellDimensions target = workcell.capacity(); + return canonical.width() <= target.width() + && canonical.height() <= target.height() + && canonical.depth() <= target.depth() + && (canonical.width() < target.width() + || canonical.height() < target.height() + || canonical.depth() < target.depth()); + } + + public Optional menuState(Player player) { + if (player == null || !J.isOwnedByCurrentRegion(player)) { + return Optional.empty(); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null) { + return Optional.empty(); + } + if (!ownerMatches(player, studio)) { + return Optional.empty(); + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioLayout layout = session.layout(); + IrisStructure structure = loadStudioStructure(studio); + List themeSets = new ArrayList<>(); + if (structure != null && structure.getThemeSets() != null) { + for (IrisJigsawThemeSet themeSet : structure.getThemeSets()) { + if (themeSet != null) { + themeSets.add(new JigsawStudioMenuState.ThemeSet( + themeSet.getKey(), + themeSet.getWeight())); + } + } + } + List workcells = new ArrayList<>(layout.bays().size()); + for (JigsawStudioBay workcell : layout.bays()) { + JigsawStudioSession.WorkcellSnapshot snapshot = session.workcellSnapshot(workcell.stableId()); + List variants = new ArrayList<>(); + for (JigsawStudioVariant variant : layout.variants(workcell)) { + boolean active = variant.pieceKey().equals(snapshot.activeVariantKey()); + List memberships = new ArrayList<>( + variant.memberships().size()); + for (JigsawStudioPoolMembership membership : variant.memberships()) { + memberships.add(new JigsawStudioMenuState.Membership( + membership.poolKey(), + membership.entryIndex(), + membership.weight(), + membership.chance())); + } + variants.add(new JigsawStudioMenuState.Variant( + variant.pieceKey(), + variant.resolvedDisplayName(), + variant.dimensions(), + active, + variant.owned(), + variant.rotatable(), + canToggleVariantRotation(request.compatibilityTarget(), variant), + canResizeVariantToCapacity(workcell, variant, active), + variant.themes(), + variant.rules(), + memberships)); + } + workcells.add(new JigsawStudioMenuState.Workcell( + workcell.stableId(), + workcell.canonicalDisplayName(), + workcell.displayName(), + workcell.capacity(), + workcell.enabled(), + snapshot.activeVariantKey(), + snapshot.dirty(), + snapshot.saveInProgress(), + snapshot.switchInProgress(), + snapshot.connectorsVisible(), + variants)); + } + + return Optional.of(new JigsawStudioMenuState( + studio.worldId(), + request.requestId(), + request.structureKey(), + layout.mode(), + request.compatibilityTarget(), + structure != null && structure.isRequireCaps(), + themeSets, + session.selectedBayId().orElse(""), + Optional.ofNullable(evaluations.get(request.requestId())) + .map(JigsawStudioMenuState.Evaluation::from) + .orElseGet(JigsawStudioMenuState.Evaluation::pending), + workcells)); + } + + public boolean selectWorkcell(Player player, String workcellId) { + if (player == null || workcellId == null || workcellId.isBlank()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> selectWorkcell(player, workcellId)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + if (!authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + if (!session.selectBay(workcellId)) { + message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'."); + return false; + } + ensureVisualizationLoop(player); + return true; + } + + public boolean openControlMenu(Player player) { + if (player != null && J.isOwnedByCurrentRegion(player)) { + finalizeJigsawTileWatchesForPlayer(player.getUniqueId()); + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio != null + && reopenRequiredRequests.contains(studio.generator().getRequest().requestId())) { + message(player, "Close and reopen Jigsaw Studio to apply the resized workcell layout."); + return false; + } + } + JigsawStudioMenuController activeMenuController = menuController; + return activeMenuController != null && activeMenuController.open(player); + } + + public boolean giveTool(Player player, JigsawStudioToolPayload payload) { + if (player == null || payload == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> giveTool(player, payload)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null + || !studio.generator().getRequest().requestId().equals(payload.requestId()) + || !authorizeOwner(player, studio)) { + message(player, "This tool belongs to a different or closed Jigsaw Studio session."); + return false; + } + ItemStack tool = toolCodec.create(payload); + Map rejected = player.getInventory().addItem(tool); + if (!rejected.isEmpty()) { + message(player, "Your inventory is full; no Jigsaw Studio tool was added."); + return false; + } + message(player, "Added " + payload.action().displayName() + " tool."); + return true; + } + + public boolean flushAutosave(Player player, String workcellId) { + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> flushAutosave(player, workcellId)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !authorizeOwner(player, studio)) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + String targetId = workcellId == null || workcellId.isBlank() + ? session.selectedBayId().orElse("") + : workcellId; + JigsawStudioBay bay = session.layout().get(targetId); + if (bay == null) { + message(player, "Select a Jigsaw Studio workcell before saving now."); + return false; + } + UUID requestId = studio.generator().getRequest().requestId(); + finalizeJigsawTileWatches(requestId); + if (hasJigsawTileWatch(requestId)) { + message(player, "Wait for the open vanilla jigsaw-block editor to finish its final snapshot."); + return false; + } + SaveAttempt attempt = startSave(studio, studio.world(), bay, player, true); + if (attempt == SaveAttempt.STARTED) { + return true; + } + if (attempt == SaveAttempt.PERSISTENT_FAILURE) { + return false; + } + AutosaveTicket ticket = autosaves.get(new AutosaveKey(requestId, bay.stableId())); + if (ticket != null) { + expediteAutosave( + ticket, + attempt == SaveAttempt.RETRY + ? AUTOSAVE_RETRY_TICKS + : AUTOSAVE_DEBOUNCE_TICKS); + } + return false; + } + + public void closeMenu(Player player) { + JigsawStudioMenuController activeMenuController = menuController; + if (activeMenuController != null) { + activeMenuController.close(player); + } + } + + private boolean useTool( + Player player, + JigsawStudioToolPayload payload, + ItemStack tool, + boolean resetLabel + ) { + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> useTool(player, payload, tool, resetLabel)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null + || !studio.generator().getRequest().requestId().equals(payload.requestId()) + || !authorizeOwner(player, studio)) { + message(player, "This Jigsaw Studio tool is stale or belongs to another session."); + return false; + } + if (payload.action().destructive() && !confirmTool(player, payload)) { + message(player, "Right-click the same tool again within 10 seconds to confirm " + + payload.action().displayName() + "."); + return true; + } + return switch (payload.action()) { + case OPEN_MENU -> openControlMenu(player); + case SELECT_WORKCELL -> selectWorkcell(player, payload.workcellId()); + case LOAD_VARIANT -> switchVariant( + player, + payload.workcellId(), + payload.pieceKey(), + false); + case CREATE_VARIANT -> createVariant(player, payload.workcellId(), false); + case DUPLICATE_VARIANT -> createVariant(player, payload.workcellId(), true); + case DUPLICATE_FAMILY -> duplicateFamilyFromTool(player); + case PREVIEW_GRAPH -> goToPreview(player); + case FLUSH_AUTOSAVE -> flushAutosave(player, payload.workcellId()); + case TOGGLE_ROTATION -> toggleVariantRotatable(player, payload.workcellId()); + case EXPAND_TO_CELL -> expandVariantToCell(player, payload.workcellId()); + case RESIZE_VARIANT -> openVariantSizeSettingsFromTool(player, payload); + case RENAME_VARIANT -> renameVariantFromTool(player, payload, tool, resetLabel); + case ADJUST_VARIANT_WEIGHT -> adjustVariantWeight( + player, + payload.workcellId(), + payload.pieceKey(), + payload.poolKey(), + payload.entryIndex(), + payload.amount()); + case ADJUST_VARIANT_CHANCE -> adjustVariantChance( + player, + payload.workcellId(), + payload.pieceKey(), + payload.poolKey(), + payload.entryIndex(), + payload.amount()); + case UNLINK_MEMBERSHIP -> unlinkVariantMembership( + player, + payload.workcellId(), + payload.pieceKey(), + payload.poolKey(), + payload.entryIndex()); + case TOGGLE_WORKCELL -> toggleWorkcellFromTool(player, payload.workcellId()); + case RESIZE_WORKCELL -> openWorkcellSettingsFromTool(player, payload.workcellId()); + case RENAME_WORKCELL -> renameWorkcellFromTool(player, payload, tool, resetLabel); + case SET_THEME -> useThemeTool(player, payload); + case SET_PIECE_RULES -> openVariantSettingsFromTool(player, payload); + case DELETE_VARIANT -> deleteVariant(player, payload.workcellId(), payload.pieceKey()); + case DELETE_PROJECT -> deleteProject(player); + case TOGGLE_REQUIRE_CAPS -> toggleRequireCapsFromTool(player); + }; + } + + private boolean toggleWorkcellFromTool(Player player, String workcellId) { + Optional state = menuState(player); + JigsawStudioMenuState.Workcell workcell = state + .map(menu -> menu.workcell(workcellId)) + .orElse(null); + if (workcell == null) { + message(player, "This bound workcell no longer exists."); + return false; + } + return setWorkcellEnabled(player, workcellId, !workcell.enabled()); + } + + private boolean openWorkcellSettingsFromTool(Player player, String workcellId) { + JigsawStudioMenuController activeMenuController = menuController; + return activeMenuController != null + && activeMenuController.openWorkcellSettings(player, workcellId); + } + + private boolean openVariantSettingsFromTool(Player player, JigsawStudioToolPayload payload) { + JigsawStudioMenuController activeMenuController = menuController; + return activeMenuController != null + && activeMenuController.openVariantSettings( + player, + payload.workcellId(), + payload.pieceKey()); + } + + private boolean openVariantSizeSettingsFromTool(Player player, JigsawStudioToolPayload payload) { + JigsawStudioMenuController activeMenuController = menuController; + return activeMenuController != null + && activeMenuController.openVariantSizeSettings( + player, + payload.workcellId(), + payload.pieceKey()); + } + + private boolean renameVariantFromTool( + Player player, + JigsawStudioToolPayload payload, + ItemStack tool, + boolean resetLabel + ) { + Optional displayName = toolDisplayName(player, payload, tool, resetLabel); + return displayName.isPresent() && updateVariantDisplayName( + player, + payload.workcellId(), + payload.pieceKey(), + displayName.get()); + } + + private boolean renameWorkcellFromTool( + Player player, + JigsawStudioToolPayload payload, + ItemStack tool, + boolean resetLabel + ) { + Optional displayName = toolDisplayName(player, payload, tool, resetLabel); + return displayName.isPresent() && updateWorkcellDisplayName( + player, + payload.workcellId(), + displayName.get()); + } + + private static Optional toolDisplayName( + Player player, + JigsawStudioToolPayload payload, + ItemStack tool, + boolean resetLabel + ) { + if (resetLabel) { + return Optional.of(""); + } + String displayName = tool == null || !tool.hasItemMeta() || tool.getItemMeta() == null + ? "" + : tool.getItemMeta().getDisplayName(); + String normalized = ChatColor.stripColor(displayName); + normalized = normalized == null ? "" : normalized.trim(); + String defaultName = "Jigsaw Studio: " + payload.action().displayName(); + if (normalized.isEmpty() || normalized.equals(defaultName)) { + message(player, "Rename this bound stick in an anvil to the desired label, then right-click it. " + + "Sneak-right-click resets the label."); + return Optional.empty(); + } + return Optional.of(normalized); + } + + private boolean useThemeTool(Player player, JigsawStudioToolPayload payload) { + if (!payload.pieceKey().isBlank()) { + return openVariantSettingsFromTool(player, payload); + } + Optional state = menuState(player); + if (state.isEmpty()) { + return false; + } + return duplicateActiveFamily( + player, + JigsawStudioMenuController.nextThemeSetKey(state.get().themeSets())); + } + + private boolean duplicateFamilyFromTool(Player player) { + Optional state = menuState(player); + return state.isPresent() && duplicateActiveFamily( + player, + JigsawStudioMenuController.nextThemeSetKey(state.get().themeSets())); + } + + private boolean toggleRequireCapsFromTool(Player player) { + Optional state = menuState(player); + return state.isPresent() && setRequireCaps(player, !state.get().requireCaps()); + } + + private boolean confirmTool(Player player, JigsawStudioToolPayload payload) { + long now = System.nanoTime(); + ToolConfirmation previous = toolConfirmations.get(player.getUniqueId()); + if (previous != null + && previous.expiresAtNanos() >= now + && previous.payload().equals(payload)) { + toolConfirmations.remove(player.getUniqueId(), previous); + return true; + } + toolConfirmations.put( + player.getUniqueId(), + new ToolConfirmation(payload, now + TOOL_CONFIRM_NANOS)); + return false; + } + + private JigsawStudioVariant activeOwnedVariant( + Player player, + ActiveStudio studio, + String workcellId + ) { + if (studio == null) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return null; + } + JigsawStudioSession session = studio.generator().getSession(); + if (session.layout().get(workcellId) == null) { + message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'."); + return null; + } + JigsawStudioVariant variant = session.activeVariant(workcellId).orElse(null); + if (variant == null) { + message(player, "This workcell has no active variant."); + return null; + } + if (!variant.owned()) { + message(player, "Variant '" + variant.pieceKey() + + "' is read-only. Adopt or clone its graph before editing it."); + return null; + } + return variant; + } + + private JigsawStudioVariant activeOwnedVariant( + Player player, + ActiveStudio studio, + String workcellId, + String expectedPieceKey + ) { + JigsawStudioVariant variant = activeOwnedVariant(player, studio, workcellId); + if (variant == null) { + return null; + } + if (!variant.pieceKey().equals(expectedPieceKey)) { + message(player, "The bound variant changed. Open the control menu or request a fresh tool."); + return null; + } + return variant; + } + + private static JigsawStudioPoolMembership findMembership( + JigsawStudioVariant variant, + String poolKey, + int entryIndex + ) { + for (JigsawStudioPoolMembership membership : variant.memberships()) { + if (membership.poolKey().equals(poolKey) && membership.entryIndex() == entryIndex) { + return membership; + } + } + return null; + } + + private boolean deferDuplicationUntilAutosaved( + Player player, + ActiveStudio studio, + DeferredDuplication requested + ) { + JigsawStudioSession session = studio.generator().getSession(); + UUID requestId = studio.generator().getRequest().requestId(); + boolean autosavePending; + synchronized (saveLifecycleLock) { + autosavePending = session.isDirty() || savesInProgress.contains(requestId); + } + if (!autosavePending) { + return false; + } + DeferredDuplication existing = deferredDuplications.putIfAbsent(requestId, requested); + if (existing == null) { + message(player, requested.kind() == DeferredDuplicationKind.SINGLE + ? "Autosave is finishing first; this cell's variant will duplicate automatically." + : "Autosave is finishing every edited cell first; the coherent family will duplicate automatically."); + expediteAutosaves(requestId); + scheduleDeferredDuplication(requested); + return true; + } + message(player, existing.sameIntent(requested) + ? "That duplicate action is already queued behind autosave." + : "Another duplicate action is already queued behind autosave for this Studio."); + return true; + } + + private void scheduleDeferredDuplication(DeferredDuplication deferred) { + if (deferredDuplications.get(deferred.requestId()) != deferred + || !deferred.scheduled().compareAndSet(false, true)) { + return; + } + try { + J.s(() -> { + deferred.scheduled().set(false); + if (deferredDuplications.get(deferred.requestId()) != deferred) { + return; + } + Player player = Bukkit.getPlayer(deferred.playerId()); + if (player == null) { + deferredDuplications.remove(deferred.requestId(), deferred); + return; + } + boolean scheduled = J.runEntity( + player, + () -> resumeDeferredDuplication(player, deferred), + 0, + () -> deferredDuplications.remove(deferred.requestId(), deferred)); + if (!scheduled) { + deferredDuplications.remove(deferred.requestId(), deferred); + } + }, AUTOSAVE_RETRY_TICKS); + } catch (Throwable exception) { + deferred.scheduled().set(false); + deferredDuplications.remove(deferred.requestId(), deferred); + IrisLogging.reportError(exception); + } + } + + private void resumeDeferredDuplication(Player player, DeferredDuplication deferred) { + if (deferredDuplications.get(deferred.requestId()) != deferred) { + return; + } + ActiveStudio studio = studios.get(deferred.worldId()); + JigsawStudioSession session = studio == null ? null : studio.generator().getSession(); + boolean currentRequest = studio != null + && studio.generator().getRequest().requestId().equals(deferred.requestId()) + && session.sessionId().equals(deferred.sessionId()) + && player.getWorld().getUID().equals(deferred.worldId()); + boolean sourcesCurrent = currentRequest && sourceBindingsCurrent(session, deferred.sourcePieces()); + boolean autosavePending; + boolean operationPending; + boolean terminal; + synchronized (saveLifecycleLock) { + autosavePending = currentRequest + && (session.isDirty() || savesInProgress.contains(deferred.requestId())); + operationPending = currentRequest + && (graphMutationsInProgress.contains(deferred.requestId()) + || materializationsInProgress.contains(deferred.requestId()) + || exportsInProgress.contains(deferred.requestId()) + || session.operationInProgress() + || hasJigsawTileWatch(deferred.requestId())); + terminal = !currentRequest + || closingRequests.contains(deferred.requestId()) + || reopenRequiredRequests.contains(deferred.requestId()); + } + DeferredDuplicationReadiness readiness = deferredDuplicationReadiness( + currentRequest, + sourcesCurrent, + autosavePending, + operationPending, + terminal); + if (readiness == DeferredDuplicationReadiness.STALE) { + deferredDuplications.remove(deferred.requestId(), deferred); + message(player, "The queued duplicate was cancelled because its Studio or source variant changed."); + return; + } + if (readiness == DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE) { + expediteAutosaves(deferred.requestId()); + scheduleDeferredDuplication(deferred); + return; + } + if (readiness == DeferredDuplicationReadiness.WAITING_FOR_OPERATION) { + scheduleDeferredDuplication(deferred); + return; + } + boolean started = deferred.kind() == DeferredDuplicationKind.SINGLE + ? createVariant(player, deferred.workcellId(), true) + : duplicateActiveFamily(player, deferred.themeKey()); + if (started) { + deferredDuplications.remove(deferred.requestId(), deferred); + return; + } + if (isCurrentRequest(studio, deferred.requestId()) + && sourceBindingsCurrent(session, deferred.sourcePieces())) { + scheduleDeferredDuplication(deferred); + } else { + deferredDuplications.remove(deferred.requestId(), deferred); + } + } + + static DeferredDuplicationReadiness deferredDuplicationReadiness( + boolean currentRequest, + boolean sourcesCurrent, + boolean autosavePending, + boolean operationPending, + boolean terminal + ) { + if (!currentRequest || !sourcesCurrent || terminal) { + return DeferredDuplicationReadiness.STALE; + } + if (autosavePending) { + return DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE; + } + if (operationPending) { + return DeferredDuplicationReadiness.WAITING_FOR_OPERATION; + } + return DeferredDuplicationReadiness.READY; + } + + private static boolean sourceBindingsCurrent( + JigsawStudioSession session, + Map expectedSources + ) { + if (session == null) { + return false; + } + for (Map.Entry source : expectedSources.entrySet()) { + if (session.activeVariant(source.getKey()) + .map(JigsawStudioVariant::pieceKey) + .filter(source.getValue()::equals) + .isEmpty()) { + return false; + } + } + return true; + } + + private String beginGraphMutation( + ActiveStudio studio, + JigsawStudioSession session + ) { + UUID requestId = studio.generator().getRequest().requestId(); + finalizeJigsawTileWatches(requestId); + synchronized (saveLifecycleLock) { + if (closingRequests.contains(requestId)) { + return "This Jigsaw Studio is closing and cannot update its graph."; + } + if (savesInProgress.contains(requestId)) { + return "Wait for the current Jigsaw Studio save to finish."; + } + if (graphMutationsInProgress.contains(requestId)) { + return "A Jigsaw Studio graph update is already running."; + } + if (exportsInProgress.contains(requestId)) { + return "Wait for the current Jigsaw Studio export to finish."; + } + if (materializationsInProgress.contains(requestId)) { + return "Wait for the current Jigsaw Studio variant load or rollback to finish."; + } + if (session.operationInProgress()) { + return "Wait for the current workcell operation to finish."; + } + if (reopenRequiredRequests.contains(requestId)) { + return "Close and reopen Jigsaw Studio before making another graph change."; + } + if (hasJigsawTileWatch(requestId)) { + return "Finish or close the open vanilla jigsaw-block editor before changing graph metadata."; + } + if (session.isDirty()) { + return "Wait for every dirty workcell to finish autosaving before changing graph metadata."; + } + graphMutationsInProgress.add(requestId); + return ""; + } + } + + public boolean runCommandGraphMutation( + Player player, + UUID expectedRequestId, + CommandGraphMutation task + ) { + Objects.requireNonNull(expectedRequestId, "Jigsaw Studio command graph request ID"); + Objects.requireNonNull(task, "Jigsaw Studio command graph mutation"); + if (player == null) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> runCommandGraphMutation(player, expectedRequestId, task)); + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null + || !studio.generator().getRequest().requestId().equals(expectedRequestId)) { + message(player, "This Jigsaw Studio session is no longer active."); + return false; + } + if (!authorizeOwner(player, studio)) { + return false; + } + String reservationFailure = beginGraphMutation(studio, studio.generator().getSession()); + if (!reservationFailure.isEmpty()) { + message(player, reservationFailure); + return false; + } + return scheduleGraphMutation(player, studio, task); + } + + private boolean scheduleGraphMutation( + Player player, + ActiveStudio studio, + CommandGraphMutation task + ) { + UUID requestId = studio.generator().getRequest().requestId(); + J.a(() -> { + CommandGraphMutationResult result = null; + Throwable failure = null; + try { + result = task.run(); + } catch (Throwable exception) { + failure = exception; + } + CommandGraphMutationResult completedResult = result; + Throwable completedFailure = failure; + boolean scheduled = J.runEntity( + player, + () -> completeGraphMutation( + player, + studio, + requestId, + completedResult, + completedFailure), + 0, + () -> finishGraphMutation(requestId)); + if (!scheduled) { + finishGraphMutation(requestId); + if (completedFailure != null) { + IrisLogging.reportError(completedFailure); + } + } + }); + return true; + } + + private void completeGraphMutation( + Player player, + ActiveStudio studio, + UUID requestId, + CommandGraphMutationResult result, + Throwable failure + ) { + try { + if (failure != null) { + IrisLogging.reportError(failure); + message(player, "Graph update failed: " + failureMessage(failure)); + finishGraphMutation(requestId); + return; + } + if (result == null) { + message(player, "Graph update completed without a result; no Studio state changed."); + finishGraphMutation(requestId); + return; + } + if (!isCurrentRequest(studio, requestId)) { + message(player, "The graph updated on disk after this Studio session changed; reopen it to continue."); + finishGraphMutation(requestId); + return; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioLayout previousLayout = session.layout(); + if (result.rebindActiveVariants().isEmpty()) { + session.replaceLayout(result.layout()); + } else { + session.replaceLayoutAndRebind(result.layout(), result.rebindActiveVariants()); + } + for (JigsawStudioBay workcell : result.layout().bays()) { + studio.generator().invalidateRender(workcell.stableId()); + } + if (layoutGeometryChanged(previousLayout, result.layout())) { + scheduleLiveRelayout(player, studio, requestId, previousLayout, result); + return; + } + finishGraphMutationSuccess(player, studio, requestId, result); + } catch (Throwable exception) { + IrisLogging.reportError(exception); + message(player, "The graph updated on disk, but Studio could not refresh: " + + failureMessage(exception) + ". Reopen this project before editing further."); + reopenRequiredRequests.add(requestId); + finishGraphMutation(requestId); + } + } + + private void finishGraphMutationSuccess( + Player player, + ActiveStudio studio, + UUID requestId, + CommandGraphMutationResult result + ) { + for (JigsawStudioBay workcell : result.layout().bays()) { + refreshWorkcellContext(studio.worldId(), workcell.stableId()); + } + scheduleEvaluation(studio); + message(player, result.message()); + finishGraphMutation(requestId); + if (!result.activatePieceKey().isEmpty()) { + switchVariant( + player, + result.activateWorkcellId(), + result.activatePieceKey(), + false); + } else if (result.reload().isPresent()) { + reloadActiveVariant(player, studio, result.reload().orElseThrow()); + } + } + + private void scheduleLiveRelayout( + Player player, + ActiveStudio studio, + UUID requestId, + JigsawStudioLayout previousLayout, + CommandGraphMutationResult result + ) { + Set chunks = relayoutChunks(previousLayout, result.layout()); + AtomicInteger remaining = new AtomicInteger(chunks.size()); + AtomicReference failure = new AtomicReference<>(""); + AtomicReference cause = new AtomicReference<>(); + if (chunks.isEmpty()) { + completeLiveRelayout(player, studio, requestId, result, "", null); + return; + } + for (long chunkKey : chunks) { + int chunkX = (int) (chunkKey >> 32); + int chunkZ = (int) chunkKey; + boolean scheduled = J.runRegion(studio.world(), chunkX, chunkZ, () -> { + try { + repaintStudioChunk(studio, chunkX, chunkZ); + } catch (Throwable exception) { + failure.compareAndSet("", "chunk " + chunkX + "," + chunkZ + + " could not regenerate: " + failureMessage(exception)); + cause.compareAndSet(null, exception); + } + if (remaining.decrementAndGet() == 0) { + scheduleLiveRelayoutCompletion( + player, + studio, + requestId, + result, + failure.get(), + cause.get()); + } + }); + if (!scheduled) { + failure.compareAndSet("", "chunk " + chunkX + "," + chunkZ + + " could not be scheduled on its owning region"); + if (remaining.decrementAndGet() == 0) { + scheduleLiveRelayoutCompletion( + player, + studio, + requestId, + result, + failure.get(), + cause.get()); + } + } + } + } + + private void scheduleLiveRelayoutCompletion( + Player player, + ActiveStudio studio, + UUID requestId, + CommandGraphMutationResult result, + String failure, + Throwable cause + ) { + boolean scheduled = J.runEntity( + player, + () -> completeLiveRelayout(player, studio, requestId, result, failure, cause)); + if (!scheduled) { + reopenRequiredRequests.add(requestId); + finishGraphMutation(requestId); + if (cause != null) { + IrisLogging.reportError(cause); + } + } + } + + private void completeLiveRelayout( + Player player, + ActiveStudio studio, + UUID requestId, + CommandGraphMutationResult result, + String failure, + Throwable cause + ) { + if (!failure.isEmpty() || !isCurrentRequest(studio, requestId)) { + reopenRequiredRequests.add(requestId); + studio.populations().clear(); + if (cause != null) { + IrisLogging.reportError(cause); + } + message(player, "The workcell size was saved, but live regeneration failed" + + (failure.isEmpty() ? "." : ": " + failure + ".") + + " Close and reopen this project before editing further."); + finishGraphMutation(requestId); + return; + } + studio.populations().clear(); + for (JigsawStudioBay workcell : result.layout().bays()) { + JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(workcell); + studio.replacePopulation( + workcell, + rendered, + rendered.valid() ? "" : rendered.failure(), + rendered.valid()); + } + reopenRequiredRequests.remove(requestId); + finishGraphMutationSuccess(player, studio, requestId, result); + studio.generator().getSession().selectedBayId().ifPresent(workcellId -> teleportTo(player, workcellId)); + } + + static boolean layoutGeometryChanged( + JigsawStudioLayout previous, + JigsawStudioLayout current + ) { + if (previous.bays().size() != current.bays().size()) { + return true; + } + for (JigsawStudioBay previousBay : previous.bays()) { + JigsawStudioBay currentBay = current.get(previousBay.stableId()); + if (currentBay == null || !previousBay.bounds().equals(currentBay.bounds())) { + return true; + } + } + return false; + } + + static Set relayoutChunks( + JigsawStudioLayout previous, + JigsawStudioLayout current + ) { + Set chunks = new HashSet<>(); + addRelayoutChunks(chunks, previous); + addRelayoutChunks(chunks, current); + return Set.copyOf(chunks); + } + + private static void addRelayoutChunks(Set chunks, JigsawStudioLayout layout) { + for (JigsawStudioBay workcell : layout.bays()) { + JigsawStudioBounds bounds = workcell.bounds(); + int minimumChunkX = (bounds.originX() - 1) >> 4; + int maximumChunkX = (bounds.maxX() + 1) >> 4; + int minimumChunkZ = (bounds.originZ() - 1) >> 4; + int maximumChunkZ = (bounds.maxZ() + 1) >> 4; + for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) { + for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) { + chunks.add(chunkKey(chunkX, chunkZ)); + } + } + } + } + + private static void repaintStudioChunk(ActiveStudio studio, int chunkX, int chunkZ) throws IOException { + World world = studio.world(); + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + TerrainChunk generated = TerrainChunk.create(world); + studio.generator().paintChunk(generated, chunkX, chunkZ); + if (!INMS.get().applyChunkBlocks(chunk, generated)) { + InPlaceChunkRegenerator.applyBlockDiffs( + chunk, + generated.getChunkData(), + world.getMinHeight(), + world.getMaxHeight()); + } + for (JigsawStudioBay workcell : studio.generator().getLayout().bays()) { + JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(workcell); + if (!rendered.valid()) { + continue; + } + boolean connectorsVisible = studio.generator().getSession() + .workcellSnapshot(workcell.stableId()) + .connectorsVisible(); + applyRenderedBayChunk(world, workcell, rendered, chunkX, chunkZ, connectorsVisible); + verifyRenderedBayChunk(world, workcell, rendered, chunkX, chunkZ, connectorsVisible); + } + world.refreshChunk(chunkX, chunkZ); + } + + private boolean graphMutationInProgress(UUID requestId) { + synchronized (saveLifecycleLock) { + return graphMutationsInProgress.contains(requestId); + } + } + + private void finishGraphMutation(UUID requestId) { + synchronized (saveLifecycleLock) { + graphMutationsInProgress.remove(requestId); + } + } + + private static JigsawStudioLayout loadMappedLayout(ActiveStudio studio) throws IOException { + JigsawStudioActivation.Request request = studio.generator().getRequest(); + IrisData source = request.source(); + source.invalidateStructureResources(); + IrisStructure structure = source.load(IrisStructure.class, request.structureKey(), false); + if (structure == null) { + throw new IOException("The structure could not be reloaded after the graph transaction"); + } + return JigsawStudioGraphMapper.map(source, structure); + } + + public boolean showAssemblyPreview(Player player, List pieces) { + if (player == null || pieces == null || pieces.isEmpty()) { + return false; + } + if (!J.isOwnedByCurrentRegion(player)) { + return J.runEntity(player, () -> showAssemblyPreview(player, pieces)); + } + if (!studios.containsKey(player.getWorld().getUID())) { + message(player, "Iris Jigsaw Studio is not active in this world."); + return false; + } + List bounds = new ArrayList<>(pieces.size()); + for (PlacedStructurePiece piece : pieces) { + int width = Math.addExact(Math.subtractExact(piece.getMaxX(), piece.getMinX()), 1); + int height = Math.addExact(Math.subtractExact(piece.getMaxY(), piece.getMinY()), 1); + int depth = Math.addExact(Math.subtractExact(piece.getMaxZ(), piece.getMinZ()), 1); + bounds.add(new JigsawStudioBounds( + piece.getMinX(), + piece.getMinY(), + piece.getMinZ(), + new JigsawStudioCellDimensions(width, height, depth))); + } + assemblyPreviews.put(player.getUniqueId(), new AssemblyPreview( + player.getWorld().getUID(), + System.currentTimeMillis() + ASSEMBLY_PREVIEW_MILLIS, + List.copyOf(bounds))); + particlesDisabled.remove(player.getUniqueId()); + ensureVisualizationLoop(player); + return true; + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onWorldUnload(WorldUnloadEvent event) { + ActiveStudio studio = studios.get(event.getWorld().getUID()); + if (studio != null) { + UUID requestId = studio.generator().getRequest().requestId(); + finalizeJigsawTileWatches(requestId); + if (requiresLifecycleDrain(studio) && !discardingRequest(requestId)) { + event.setCancelled(true); + expediteAutosaves(requestId); + IrisLogging.warn("Jigsaw Studio kept world %s loaded while pending edits finish autosaving", + event.getWorld().getName()); + return; + } + } + unregister(event.getWorld()); + } + + @EventHandler + public void onChunkLoad(ChunkLoadEvent event) { + ActiveStudio studio = studios.get(event.getWorld().getUID()); + if (studio != null) { + markChunkAvailable(studio, event.getChunk().getX(), event.getChunk().getZ()); + } + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + ensureVisualizationLoop(event.getPlayer()); + reconcilePlayerContext(event.getPlayer(), event.getPlayer().getLocation()); + } + + @EventHandler + public void onPlayerChangedWorld(PlayerChangedWorldEvent event) { + closeMenu(event.getPlayer()); + finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId()); + tripleSneakTracker.clearPlayer(event.getPlayer().getUniqueId()); + visualizationLoops.remove(event.getPlayer().getUniqueId()); + assemblyPreviews.remove(event.getPlayer().getUniqueId()); + playerWorkcells.remove(event.getPlayer().getUniqueId()); + ensureVisualizationLoop(event.getPlayer()); + reconcilePlayerContext(event.getPlayer(), event.getPlayer().getLocation()); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + closeMenu(event.getPlayer()); + finalizeJigsawTileWatchesForPlayer(playerId); + tripleSneakTracker.clearPlayer(playerId); + toolConfirmations.remove(playerId); + visualizationLoops.remove(playerId); + particlesDisabled.remove(playerId); + assemblyPreviews.remove(playerId); + playerWorkcells.remove(playerId); + deferredDuplications.entrySet().removeIf(entry -> entry.getValue().playerId().equals(playerId)); + boardService().clearJigsawContext(event.getPlayer()); + } + + @EventHandler + public void onTripleSneak(PlayerToggleSneakEvent event) { + if (!event.isSneaking()) { + return; + } + Player player = event.getPlayer(); + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null || !ownerMatches(player, studio)) { + tripleSneakTracker.clearPlayer(player.getUniqueId()); + return; + } + JigsawStudioTripleSneakTracker.Progress progress = tripleSneakTracker.recordSneak( + player.getUniqueId(), + studio.worldId(), + studio.generator().getRequest().requestId(), + System.nanoTime()); + if (progress == JigsawStudioTripleSneakTracker.Progress.TRIGGERED) { + openControlMenu(player); + } + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onUseTool(PlayerInteractEvent event) { + if (event.getHand() != EquipmentSlot.HAND + || (event.getAction() != Action.RIGHT_CLICK_AIR + && event.getAction() != Action.RIGHT_CLICK_BLOCK)) { + return; + } + Optional decoded = toolCodec.decode(event.getItem()); + if (decoded.isEmpty()) { + return; + } + finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId()); + event.setCancelled(true); + useTool(event.getPlayer(), decoded.get(), event.getItem(), event.getPlayer().isSneaking()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerMove(PlayerMoveEvent event) { + if (sameBlockPosition(event.getFrom(), event.getTo())) { + return; + } + reconcilePlayerContext(event.getPlayer(), event.getTo()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerTeleport(PlayerTeleportEvent event) { + finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId()); + reconcilePlayerContext(event.getPlayer(), event.getTo()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerPortal(PlayerPortalEvent event) { + finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId()); + reconcilePlayerContext(event.getPlayer(), event.getTo()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerRespawn(PlayerRespawnEvent event) { + finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId()); + reconcilePlayerContext(event.getPlayer(), event.getRespawnLocation()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onNaturalCreatureSpawn(CreatureSpawnEvent event) { + if (studios.containsKey(event.getLocation().getWorld().getUID()) + && isNaturalStudioSpawn(event.getSpawnReason())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onUnauthorizedBlockPlace(BlockPlaceEvent event) { + if (isUnauthorizedStudioEdit(event.getPlayer(), event.getBlockPlaced())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onUnauthorizedBlockMultiPlace(BlockMultiPlaceEvent event) { + for (BlockState state : event.getReplacedBlockStates()) { + if (isUnauthorizedStudioEdit(event.getPlayer(), state.getBlock())) { + event.setCancelled(true); + return; + } + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockBreak(BlockBreakEvent event) { + if (isControlChest(event.getBlock())) { + event.setCancelled(true); + message(event.getPlayer(), "The Jigsaw Studio control chest cannot be removed."); + return; + } + if (isUnauthorizedStudioEdit(event.getPlayer(), event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onUnauthorizedBucketEmpty(PlayerBucketEmptyEvent event) { + Block target = event.getBlockClicked().getRelative(event.getBlockFace()); + if (isUnauthorizedStudioEdit(event.getPlayer(), target)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onUnauthorizedBucketFill(PlayerBucketFillEvent event) { + if (isUnauthorizedStudioEdit(event.getPlayer(), event.getBlockClicked())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onUnauthorizedInteract(PlayerInteractEvent event) { + if (event.getAction() == Action.RIGHT_CLICK_BLOCK + && !isControlChest(event.getClickedBlock()) + && isUnauthorizedStudioEdit(event.getPlayer(), event.getClickedBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + markDirty(event.getBlockPlaced()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockMultiPlace(BlockMultiPlaceEvent event) { + for (BlockState state : event.getReplacedBlockStates()) { + markDirty(state.getBlock()); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBucketEmpty(PlayerBucketEmptyEvent event) { + markDirty(event.getBlockClicked().getRelative(event.getBlockFace())); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBucketFill(PlayerBucketFillEvent event) { + markDirty(event.getBlockClicked()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onControlChest(PlayerInteractEvent event) { + if (event.getAction() != Action.RIGHT_CLICK_BLOCK + || event.getHand() != EquipmentSlot.HAND + || !isControlChest(event.getClickedBlock())) { + return; + } + event.setCancelled(true); + ActiveStudio studio = studios.get(event.getPlayer().getWorld().getUID()); + if (!authorizeOwner(event.getPlayer(), studio)) { + return; + } + openControlMenu(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockExplode(BlockExplodeEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID()))) { + event.blockList().clear(); + return; + } + event.blockList().removeIf(this::isImmutableStudioBlock); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedEntityExplode(EntityExplodeEvent event) { + if (materializationInProgress(studios.get(event.getEntity().getWorld().getUID()))) { + event.blockList().clear(); + return; + } + event.blockList().removeIf(this::isImmutableStudioBlock); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedPistonExtend(BlockPistonExtendEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || movesProtectedBlocks(event.getBlocks(), event.getDirection())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedPistonRetract(BlockPistonRetractEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || movesProtectedBlocks(event.getBlocks(), event.getDirection())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedEntityChangeBlock(EntityChangeBlockEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || isImmutableStudioBlock(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockFromTo(BlockFromToEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || isImmutableStudioBlock(event.getBlock()) + || isImmutableStudioBlock(event.getToBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockForm(BlockFormEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || isImmutableStudioBlock(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockGrow(BlockGrowEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || isImmutableStudioBlock(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockSpread(BlockSpreadEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || isImmutableStudioBlock(event.getBlock()) + || isImmutableStudioBlock(event.getSource())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockFade(BlockFadeEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || isImmutableStudioBlock(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockBurn(BlockBurnEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || isImmutableStudioBlock(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onProtectedBlockRedstone(BlockRedstoneEvent event) { + if (materializationInProgress(studios.get(event.getBlock().getWorld().getUID())) + || isImmutableStudioBlock(event.getBlock())) { + event.setNewCurrent(event.getOldCurrent()); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedStructureGrow(StructureGrowEvent event) { + if (event.getLocation().getWorld() != null + && (materializationInProgress(studios.get(event.getLocation().getWorld().getUID())) + || containsImmutableStudioBlock(event.getBlocks()))) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedInventoryClick(InventoryClickEvent event) { + if (!blocksInventoryMutation( + event.getView().getTopInventory(), + event.getWhoClicked().getUniqueId())) { + return; + } + event.setCancelled(true); + if (event.getWhoClicked() instanceof Player player) { + message(player, "This Jigsaw Studio container is read-only during the current operation or owner session."); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedInventoryDrag(InventoryDragEvent event) { + if (!blocksInventoryMutation( + event.getView().getTopInventory(), + event.getWhoClicked().getUniqueId())) { + return; + } + event.setCancelled(true); + if (event.getWhoClicked() instanceof Player player) { + message(player, "This Jigsaw Studio container is read-only during the current operation or owner session."); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedInventoryMove(InventoryMoveItemEvent event) { + if (blocksInventoryMutation(event.getSource(), null) + || blocksInventoryMutation(event.getDestination(), null)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedInventoryPickup(InventoryPickupItemEvent event) { + if (blocksInventoryMutation(event.getInventory(), null)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockCook(BlockCookEvent event) { + if (blocksMachineMutation(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedFurnaceBurn(FurnaceBurnEvent event) { + if (blocksMachineMutation(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBrew(BrewEvent event) { + if (blocksMachineMutation(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBrewingStandFuel(BrewingStandFuelEvent event) { + if (blocksMachineMutation(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedBlockDispense(BlockDispenseEvent event) { + if (blocksMachineMutation(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onProtectedCrafterCraft(CrafterCraftEvent event) { + if (blocksMachineMutation(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + if (event.getAction() != Action.RIGHT_CLICK_BLOCK && event.getAction() != Action.PHYSICAL) { + return; + } + Block clicked = event.getClickedBlock(); + if (clicked != null) { + if (event.getAction() == Action.RIGHT_CLICK_BLOCK) { + finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId()); + if (clicked.getType() == Material.JIGSAW) { + startJigsawTileWatch(event.getPlayer(), clicked); + } + } + markDirty(clicked); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryClick(InventoryClickEvent event) { + markDirty(event.getView().getTopInventory()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryDrag(InventoryDragEvent event) { + markDirty(event.getView().getTopInventory()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onInventoryClose(InventoryCloseEvent event) { + markDirty(event.getView().getTopInventory()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryMove(InventoryMoveItemEvent event) { + markDirty(event.getSource()); + markDirty(event.getDestination()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryPickup(InventoryPickupItemEvent event) { + markDirty(event.getInventory()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockCook(BlockCookEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onFurnaceBurn(FurnaceBurnEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onFurnaceStartSmelt(FurnaceStartSmeltEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBrew(BrewEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onBrewingStart(BrewingStartEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBrewingStandFuel(BrewingStandFuelEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockDispense(BlockDispenseEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onCrafterCraft(CrafterCraftEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockExplode(BlockExplodeEvent event) { + markDirty(event.blockList()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityExplode(EntityExplodeEvent event) { + markDirty(event.blockList()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityChangeBlock(EntityChangeBlockEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockFromTo(BlockFromToEvent event) { + markDirty(event.getToBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockForm(BlockFormEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockGrow(BlockGrowEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockSpread(BlockSpreadEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockFade(BlockFadeEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockBurn(BlockBurnEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockRedstone(BlockRedstoneEvent event) { + markDirty(event.getBlock()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPistonExtend(BlockPistonExtendEvent event) { + markDirty(event.getBlock()); + markDirty(event.getBlock().getRelative(event.getDirection())); + for (Block block : event.getBlocks()) { + markDirty(block); + markDirty(block.getRelative(event.getDirection())); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPistonRetract(BlockPistonRetractEvent event) { + markDirty(event.getBlock()); + markDirty(event.getBlock().getRelative(event.getDirection())); + for (Block block : event.getBlocks()) { + markDirty(block); + markDirty(block.getRelative(event.getDirection())); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onStructureGrow(StructureGrowEvent event) { + for (BlockState state : event.getBlocks()) { + markDirty(state.getBlock()); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onUnauthorizedPlayerCommand(PlayerCommandPreprocessEvent event) { + finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId()); + ActiveStudio studio = studios.get(event.getPlayer().getWorld().getUID()); + if (studio != null + && materializationInProgress(studio) + && !isSafeNonOwnerCommand(event.getMessage())) { + event.setCancelled(true); + message(event.getPlayer(), "Wait for the current Jigsaw Studio variant load or rollback to finish."); + return; + } + if (studio != null && blocksNonEditableWorkcellMutation( + hasNonEditableWorkcell(studio), event.getMessage())) { + event.setCancelled(true); + message(event.getPlayer(), "Mutating commands are disabled while this Studio has an empty or read-only workcell."); + return; + } + UUID ownerId = studio == null ? null : studio.generator().getRequest().ownerId(); + if (!blocksMutatingCommand(ownerId, event.getPlayer().getUniqueId(), event.getMessage())) { + return; + } + event.setCancelled(true); + message(event.getPlayer(), "This Jigsaw Studio is owned by another player session."); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerCommand(PlayerCommandPreprocessEvent event) { + if (isMutatingCommand(event.getMessage())) { + markAllDirty(event.getPlayer().getWorld()); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onServerCommand(ServerCommandEvent event) { + if (!isSafeNonOwnerCommand(event.getCommand()) && anyMaterializationInProgress()) { + event.setCancelled(true); + return; + } + if (isMutatingCommand(event.getCommand()) && anyNonEditableWorkcell()) { + event.setCancelled(true); + return; + } + if (isMutatingCommand(event.getCommand())) { + markAllDirty(); + } + } + + public boolean markDirty(World world, int worldX, int worldY, int worldZ) { + if (world == null) { + return false; + } + ActiveStudio studio = studios.get(world.getUID()); + if (studio == null) { + return false; + } + if (reopenRequiredRequests.contains(studio.generator().getRequest().requestId())) { + return false; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay bay = session.layout().findAt(worldX, worldY, worldZ); + if (bay == null) { + return false; + } + JigsawStudioVariant activeVariant = session.activeVariant(bay.stableId()).orElse(null); + if (activeVariant == null || !activeVariant.owned()) { + return false; + } + JigsawStudioSession.DirtyMark dirtyMark = session.markWorkcellDirty(bay.stableId()); + if (dirtyMark.status() != JigsawStudioSession.DirtyStatus.MARKED) { + return false; + } + scheduleAutosave(studio, bay, dirtyMark.identity().orElseThrow(), AUTOSAVE_DEBOUNCE_TICKS); + markEvaluationStale(studio); + if (dirtyMark.newlyDirty()) { + refreshWorkcellContext(studio.worldId(), bay.stableId()); + } + return true; + } + + public int markAllDirty(World world) { + if (world == null) { + return 0; + } + ActiveStudio studio = studios.get(world.getUID()); + return studio == null ? 0 : markAllDirty(studio); + } + + private boolean scheduleCapture( + ActiveStudio studio, + World world, + JigsawStudioBay bay, + CaptureTarget captureTarget, + Player player, + UUID requestId, + JigsawStudioSession.SaveIdentity saveIdentity, + AutosaveFailureState autosaveFailureState + ) { + List areas = chunkIntersections(captureTarget.bounds()); + CaptureWork work = new CaptureWork( + studio, + world, + bay, + captureTarget, + player, + requestId, + saveIdentity, + autosaveFailureState); + CaptureCoordinator coordinator = new CaptureCoordinator( + work, + areas); + for (ChunkCaptureArea area : areas) { + if (coordinator.stopped()) { + return false; + } + boolean scheduled = J.runRegion( + world, + area.chunkX(), + area.chunkZ(), + () -> captureChunk(coordinator, area)); + if (!scheduled) { + coordinator.fail("Iris could not schedule bay chunk " + + area.chunkX() + "," + area.chunkZ() + " on its owning region.", null); + return false; + } + } + return !coordinator.stopped(); + } + + private boolean scheduleMaterialization(MaterializationWork work) { + String validationFailure = validateMaterialization(work.target()); + if (!validationFailure.isEmpty()) { + work.studio().generator().getSession().abortVariantSwitch(work.token()); + refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId()); + message(work.player(), "Variant '" + work.token().targetVariant().pieceKey() + + "' cannot load: " + validationFailure); + return false; + } + String reservationFailure = beginMaterialization(work); + if (!reservationFailure.isEmpty()) { + work.studio().generator().getSession().abortVariantSwitch(work.token()); + refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId()); + message(work.player(), reservationFailure); + return false; + } + List areas = chunkIntersections(work.workcell().bounds()); + MaterializationCoordinator coordinator = new MaterializationCoordinator(work, areas); + for (ChunkCaptureArea area : areas) { + boolean scheduled = J.runRegion( + work.world(), + area.chunkX(), + area.chunkZ(), + () -> materializeCandidateChunk(coordinator, area)); + if (!scheduled) { + coordinator.candidateComplete( + area, + "Iris could not schedule workcell chunk " + + area.chunkX() + "," + area.chunkZ() + " on its owning region.", + null); + } else { + coordinator.markScheduled(); + } + } + return coordinator.scheduledAny(); + } + + private String beginMaterialization(MaterializationWork work) { + UUID requestId = work.studio().generator().getRequest().requestId(); + synchronized (saveLifecycleLock) { + if (!isCurrentVariantSwitch(work)) { + return "The Jigsaw Studio changed before the variant load could start."; + } + if (closingRequests.contains(requestId)) { + return "This Jigsaw Studio is closing and cannot load another variant."; + } + if (savesInProgress.contains(requestId)) { + return "Wait for the current Jigsaw Studio save to finish."; + } + if (graphMutationsInProgress.contains(requestId)) { + return "Wait for the current Jigsaw Studio graph update to finish."; + } + if (exportsInProgress.contains(requestId)) { + return "Wait for the current Jigsaw Studio export to finish."; + } + if (hasJigsawTileWatch(requestId)) { + return "Finish or close the open vanilla jigsaw-block editor before loading another variant."; + } + if (!materializationsInProgress.add(requestId)) { + return "Another Jigsaw Studio variant load or rollback is already running."; + } + return ""; + } + } + + private String beginConnectorRepair(ActiveStudio studio) { + UUID requestId = studio.generator().getRequest().requestId(); + finalizeJigsawTileWatches(requestId); + synchronized (saveLifecycleLock) { + if (!isCurrentRequest(studio, requestId)) { + return "This Jigsaw Studio session is no longer active."; + } + if (closingRequests.contains(requestId)) { + return "This Jigsaw Studio is closing and cannot reset connector blocks."; + } + if (savesInProgress.contains(requestId)) { + return "Wait for the current Jigsaw Studio save to finish."; + } + if (graphMutationsInProgress.contains(requestId)) { + return "Wait for the current Jigsaw Studio graph update to finish."; + } + if (exportsInProgress.contains(requestId)) { + return "Wait for the current Jigsaw Studio export to finish."; + } + if (studio.generator().getSession().operationInProgress()) { + return "Wait for the current workcell operation to finish."; + } + if (hasJigsawTileWatch(requestId)) { + return "Finish or close the open vanilla jigsaw-block editor before resetting connectors."; + } + if (!materializationsInProgress.add(requestId)) { + return "Another Jigsaw Studio variant load or repair is already running."; + } + return ""; + } + } + + private boolean scheduleConnectorRepair( + Player player, + ActiveStudio studio, + JigsawStudioBay workcell, + JigsawStudioGenerator.RenderedBay rendered, + boolean connectorsVisible + ) { + Map> connectorsByChunk = new HashMap<>(); + JigsawStudioBounds bounds = workcell.bounds(); + for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) { + int worldX = bounds.originX() + connector.x(); + int worldZ = bounds.originZ() + connector.z(); + connectorsByChunk.computeIfAbsent( + chunkKey(worldX >> 4, worldZ >> 4), + ignored -> new ArrayList<>()).add(connector); + } + AtomicInteger remaining = new AtomicInteger(connectorsByChunk.size()); + AtomicReference failure = new AtomicReference<>(""); + AtomicReference cause = new AtomicReference<>(); + AtomicBoolean scheduledAny = new AtomicBoolean(false); + Map renderedBlocks = new HashMap<>(); + for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) { + renderedBlocks.put(new LocalPosition(block.x(), block.y(), block.z()), block); + } + for (Map.Entry> entry + : connectorsByChunk.entrySet()) { + int chunkX = (int) (entry.getKey() >> 32); + int chunkZ = (int) entry.getKey().longValue(); + boolean scheduled = J.runRegion(studio.world(), chunkX, chunkZ, () -> { + try { + if (!studio.world().isChunkLoaded(chunkX, chunkZ)) { + throw new IOException("connector chunk " + chunkX + "," + chunkZ + + " is not loaded"); + } + restoreConnectorChunk( + studio.world(), + workcell, + entry.getValue(), + renderedBlocks, + connectorsVisible); + } catch (Throwable exception) { + failure.compareAndSet("", failureMessage(exception)); + cause.compareAndSet(null, exception); + } + if (remaining.decrementAndGet() == 0) { + completeConnectorRepair( + player, + studio, + rendered.connectors().size(), + failure.get(), + cause.get()); + } + }); + if (scheduled) { + scheduledAny.set(true); + } else { + failure.compareAndSet("", "connector chunk " + chunkX + "," + chunkZ + + " could not be scheduled on its owning region"); + if (remaining.decrementAndGet() == 0) { + completeConnectorRepair( + player, + studio, + rendered.connectors().size(), + failure.get(), + cause.get()); + } + } + } + if (!scheduledAny.get()) { + finishMaterialization(studio.generator().getRequest().requestId()); + } + return scheduledAny.get(); + } + + private void completeConnectorRepair( + Player player, + ActiveStudio studio, + int connectorCount, + String failure, + Throwable cause + ) { + UUID requestId = studio.generator().getRequest().requestId(); + boolean scheduled = J.runEntity(player, () -> { + finishMaterialization(requestId); + if (cause != null) { + IrisLogging.reportError(cause); + } + if (!failure.isEmpty()) { + message(player, "Connector reset was incomplete: " + failure + ". Retry after visiting the workcell."); + return; + } + message(player, "Restored " + connectorCount + + " connector block(s) from the last saved iteration."); + }); + if (!scheduled) { + finishMaterialization(requestId); + if (cause != null) { + IrisLogging.reportError(cause); + } + } + } + + static void restoreConnectorChunk( + World world, + JigsawStudioBay workcell, + List connectors, + Map renderedBlocks, + boolean connectorsVisible + ) throws IOException { + JigsawStudioBounds bounds = workcell.bounds(); + for (JigsawStudioGenerator.RenderedConnector connector : connectors) { + LocalPosition position = new LocalPosition(connector.x(), connector.y(), connector.z()); + Block target = world.getBlockAt( + bounds.originX() + connector.x(), + bounds.originY() + connector.y(), + bounds.originZ() + connector.z()); + if (connectorsVisible) { + BlockData marker; + try { + marker = Bukkit.createBlockData( + "minecraft:jigsaw[orientation=" + connector.orientation() + "]"); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid saved connector orientation '" + + connector.orientation() + "'", exception); + } + target.setBlockData(marker, false); + BukkitPlatform.deserializeTile(markerNbt(connector.connector()), target.getLocation()); + continue; + } + JigsawStudioGenerator.RenderedBlock renderedBlock = renderedBlocks.get(position); + BlockData restored; + if (renderedBlock != null) { + if (renderedBlock.state().isCustom() + || !(renderedBlock.state().nativeHandle() instanceof BlockData blockData)) { + throw new IOException("Saved connector block '" + renderedBlock.state().key() + + "' cannot be restored directly in Bukkit Studio"); + } + restored = blockData; + } else { + try { + restored = Bukkit.createBlockData(connector.connector().getFinalState()); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid saved connector final state '" + + connector.connector().getFinalState() + "'", exception); + } + } + target.setBlockData(restored, false); + if (renderedBlock != null && renderedBlock.tileData() != null) { + TileData tileData = renderedBlock.tileData(); + if (!tileData.isApplicable(target.getBlockData()) || !tileData.toBukkitTry(target)) { + throw new IOException("Saved connector tile data could not be restored at " + + target.getX() + "," + target.getY() + "," + target.getZ()); + } + } + } + } + + private void finishMaterialization(UUID requestId) { + synchronized (saveLifecycleLock) { + materializationsInProgress.remove(requestId); + } + } + + private void materializeCandidateChunk( + MaterializationCoordinator coordinator, + ChunkCaptureArea area + ) { + MaterializationWork work = coordinator.work(); + if (!isCurrentVariantSwitch(work)) { + coordinator.candidateComplete( + area, + "Jigsaw Studio changed before the variant could finish loading.", + null); + return; + } + if (!work.world().isChunkLoaded(area.chunkX(), area.chunkZ())) { + coordinator.candidateComplete( + area, + "Workcell chunk " + area.chunkX() + "," + area.chunkZ() + + " is not loaded. Visit the whole workcell and try again.", + null); + return; + } + try { + coordinator.markCandidateTouched(area); + writeMaterializedChunk( + work.world(), + work.workcell(), + work.target(), + area, + work.targetConnectorsVisible()); + coordinator.candidateComplete(area, "", null); + } catch (Throwable exception) { + coordinator.candidateComplete( + area, + "Variant materialization failed in chunk " + + area.chunkX() + "," + area.chunkZ() + ": " + failureMessage(exception), + exception); + } + } + + private void materializeRollbackChunk( + MaterializationCoordinator coordinator, + ChunkCaptureArea area + ) { + MaterializationWork work = coordinator.work(); + if (!isRollbackVariantSwitch(work)) { + coordinator.rollbackComplete( + area, + "Jigsaw Studio changed before the previous variant could be restored.", + null); + return; + } + if (!work.world().isChunkLoaded(area.chunkX(), area.chunkZ())) { + coordinator.rollbackComplete( + area, + "Rollback chunk " + area.chunkX() + "," + area.chunkZ() + " is not loaded.", + null); + return; + } + try { + writeMaterializedChunk( + work.world(), + work.workcell(), + work.previous(), + area, + work.previousConnectorsVisible()); + coordinator.rollbackComplete(area, "", null); + } catch (Throwable exception) { + coordinator.rollbackComplete( + area, + "Rollback failed in chunk " + area.chunkX() + "," + area.chunkZ() + + ": " + failureMessage(exception), + exception); + } + } + + static String validateMaterialization(JigsawStudioGenerator.RenderedBay rendered) { + if (!rendered.valid()) { + return rendered.failure(); + } + for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) { + PlatformBlockState state = block.state(); + if (state.isCustom()) { + return "custom block '" + state.key() + + "' requires provider-owned placement and cannot be swapped live in Studio"; + } + if (!(state.nativeHandle() instanceof BlockData)) { + return "block '" + state.key() + "' has no Bukkit block-data representation"; + } + } + for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) { + try { + BlockData data = Bukkit.createBlockData( + "minecraft:jigsaw[orientation=" + connector.orientation() + "]"); + if (!(data instanceof Jigsaw)) { + return "connector orientation '" + connector.orientation() + + "' did not resolve to a jigsaw marker"; + } + } catch (IllegalArgumentException exception) { + return "connector orientation '" + connector.orientation() + "' is invalid"; + } + } + return ""; + } + + private static void writeMaterializedChunk( + World world, + JigsawStudioBay workcell, + JigsawStudioGenerator.RenderedBay rendered, + ChunkCaptureArea area, + boolean connectorsVisible + ) throws IOException { + JigsawStudioBounds bounds = workcell.bounds(); + Map expected = materializedBlockData(rendered, connectorsVisible); + BlockData air = Material.AIR.createBlockData(); + for (int x = area.minimumX(); x < area.maximumX(); x++) { + for (int y = 0; y < bounds.dimensions().height(); y++) { + for (int z = area.minimumZ(); z < area.maximumZ(); z++) { + Block target = world.getBlockAt( + bounds.originX() + x, + bounds.originY() + y, + bounds.originZ() + z); + BlockData blockData = expected.getOrDefault(new LocalPosition(x, y, z), air); + target.setBlockData(blockData, false); + } + } + } + applyRenderedBayChunk(world, workcell, rendered, area.chunkX(), area.chunkZ(), connectorsVisible); + verifyMaterializedChunk(world, workcell, rendered, area, expected, connectorsVisible); + } + + private static Map materializedBlockData( + JigsawStudioGenerator.RenderedBay rendered, + boolean connectorsVisible + ) throws IOException { + Map expected = new HashMap<>(); + for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) { + if (block.state().isCustom() || !(block.state().nativeHandle() instanceof BlockData blockData)) { + throw new IOException("Rendered block '" + block.state().key() + + "' cannot be placed directly in Bukkit Studio"); + } + expected.put(new LocalPosition(block.x(), block.y(), block.z()), blockData); + } + for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) { + if (!connectorsVisible && expected.containsKey( + new LocalPosition(connector.x(), connector.y(), connector.z()))) { + continue; + } + BlockData marker; + try { + marker = Bukkit.createBlockData(connectorsVisible + ? "minecraft:jigsaw[orientation=" + connector.orientation() + "]" + : connector.connector().getFinalState()); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid rendered connector orientation '" + + connector.orientation() + "'", exception); + } + expected.put(new LocalPosition(connector.x(), connector.y(), connector.z()), marker); + } + return expected; + } + + private static void verifyMaterializedChunk( + World world, + JigsawStudioBay workcell, + JigsawStudioGenerator.RenderedBay rendered, + ChunkCaptureArea area, + Map expected, + boolean connectorsVisible + ) throws IOException { + JigsawStudioBounds bounds = workcell.bounds(); + BlockData air = Material.AIR.createBlockData(); + for (int x = area.minimumX(); x < area.maximumX(); x++) { + for (int y = 0; y < bounds.dimensions().height(); y++) { + for (int z = area.minimumZ(); z < area.maximumZ(); z++) { + Block target = world.getBlockAt( + bounds.originX() + x, + bounds.originY() + y, + bounds.originZ() + z); + BlockData expectedData = expected.getOrDefault(new LocalPosition(x, y, z), air); + if (!target.getBlockData().equals(expectedData)) { + throw new IOException("Workcell block did not materialize at " + + target.getX() + "," + target.getY() + "," + target.getZ()); + } + } + } + } + verifyRenderedBayChunk(world, workcell, rendered, area.chunkX(), area.chunkZ(), connectorsVisible); + } + + private boolean isCurrentVariantSwitch(MaterializationWork work) { + return isCurrentRequest( + work.studio(), + work.studio().generator().getRequest().requestId()) + && work.studio().generator().getSession().isVariantSwitchCurrent(work.token()); + } + + private boolean isRollbackVariantSwitch(MaterializationWork work) { + return studios.get(work.studio().worldId()) == work.studio() + && materializationInProgress(work.studio()); + } + + private void markDirty(Block block) { + if (block != null) { + markDirty(block.getWorld(), block.getX(), block.getY(), block.getZ()); + } + } + + private void markDirty(List blocks) { + for (Block block : blocks) { + markDirty(block); + } + } + + private void markDirty(Inventory inventory) { + for (Block block : inventoryBlocks(inventory)) { + markDirty(block); + } + } + + private boolean blocksInventoryMutation(Inventory inventory, UUID actorId) { + for (Block block : inventoryBlocks(inventory)) { + StudioBlockMutationContext context = studioBlockMutationContext(block); + if (context != null && blocksStudioInventoryMutation( + true, + context.immutable(), + materializationInProgress(context.studio()), + reopenRequiredRequests.contains(context.requestId()), + context.ownerId(), + actorId)) { + return true; + } + } + return false; + } + + private boolean blocksMachineMutation(Block block) { + StudioBlockMutationContext context = studioBlockMutationContext(block); + return context != null && blocksStudioInventoryMutation( + true, + context.immutable(), + materializationInProgress(context.studio()), + reopenRequiredRequests.contains(context.requestId()), + context.ownerId(), + null); + } + + private StudioBlockMutationContext studioBlockMutationContext(Block block) { + if (block == null) { + return null; + } + ActiveStudio studio = studios.get(block.getWorld().getUID()); + if (studio == null) { + return null; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + boolean preview = previewRenderer.contains( + request.requestId(), block.getX(), block.getY(), block.getZ()); + JigsawStudioControlPosition control = studio.generator().getLayout().controlPosition(); + boolean controlChest = block.getType() == Material.CHEST + && block.getX() == control.worldX() + && block.getY() == control.worldY() + && block.getZ() == control.worldZ(); + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay bay = session.layout().findAt( + block.getX(), block.getY(), block.getZ()); + boolean workcell = bay != null; + if (!preview && !controlChest && !workcell) { + return null; + } + JigsawStudioVariant variant = bay == null + ? null + : session.activeVariant(bay.stableId()).orElse(null); + boolean nonEditableWorkcell = bay != null && (variant == null || !variant.owned()); + return new StudioBlockMutationContext( + studio, + request.requestId(), + request.ownerId(), + preview || controlChest || nonEditableWorkcell); + } + + private static List inventoryBlocks(Inventory inventory) { + if (inventory == null) { + return List.of(); + } + List blocks = new ArrayList<>(2); + InventoryHolder holder = inventory.getHolder(); + if (holder instanceof BlockInventoryHolder blockInventoryHolder) { + blocks.add(blockInventoryHolder.getBlock()); + } else if (holder instanceof DoubleChest doubleChest) { + addInventoryBlock(blocks, doubleChest.getLeftSide()); + addInventoryBlock(blocks, doubleChest.getRightSide()); + } + return blocks; + } + + private static void addInventoryBlock(List blocks, InventoryHolder holder) { + if (holder instanceof BlockInventoryHolder blockInventoryHolder + && !blocks.contains(blockInventoryHolder.getBlock())) { + blocks.add(blockInventoryHolder.getBlock()); + } + } + + private void startJigsawTileWatch(Player player, Block block) { + if (player == null || block == null || block.getType() != Material.JIGSAW) { + return; + } + World world = block.getWorld(); + int worldX = block.getX(); + int worldY = block.getY(); + int worldZ = block.getZ(); + UUID playerId = player.getUniqueId(); + if (J.isOwnedByCurrentRegion(world, worldX >> 4, worldZ >> 4)) { + initializeJigsawTileWatch(playerId, world, worldX, worldY, worldZ); + return; + } + boolean scheduled = J.runRegion( + world, + worldX >> 4, + worldZ >> 4, + () -> initializeJigsawTileWatch(playerId, world, worldX, worldY, worldZ)); + if (!scheduled) { + IrisLogging.warn("Jigsaw Studio could not watch jigsaw marker NBT at %d,%d,%d in %s", + worldX, worldY, worldZ, world.getName()); + } + } + + private void initializeJigsawTileWatch( + UUID playerId, + World world, + int worldX, + int worldY, + int worldZ + ) { + ActiveStudio studio = studios.get(world.getUID()); + if (!enabled || studio == null) { + return; + } + Block block = world.getBlockAt(worldX, worldY, worldZ); + if (block.getType() != Material.JIGSAW) { + return; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay bay = session.layout().findAt(worldX, worldY, worldZ); + JigsawStudioVariant variant = bay == null + ? null + : session.activeVariant(bay.stableId()).orElse(null); + if (bay == null || variant == null || !variant.owned()) { + return; + } + KMap snapshot; + try { + snapshot = BukkitPlatform.serializeTile(block.getLocation()); + } catch (Throwable exception) { + IrisLogging.reportError(exception); + return; + } + if (snapshot == null) { + IrisLogging.warn("Jigsaw Studio could not read jigsaw marker NBT at %d,%d,%d in %s", + worldX, worldY, worldZ, world.getName()); + return; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + JigsawTileWatchKey key = new JigsawTileWatchKey( + request.requestId(), world.getUID(), worldX, worldY, worldZ); + JigsawTileWatch watch = new JigsawTileWatch( + key, + studio, + playerId, + bay.stableId(), + snapshot, + new AtomicBoolean(false), + new AtomicBoolean(false)); + jigsawTileWatches.put(key, watch); + scheduleJigsawTileWatch(watch); + } + + private void scheduleJigsawTileWatch(JigsawTileWatch watch) { + boolean scheduled = J.runRegion( + watch.studio().world(), + watch.key().worldX() >> 4, + watch.key().worldZ() >> 4, + () -> runJigsawTileWatch(watch, false), + JIGSAW_TILE_WATCH_INTERVAL_TICKS); + if (!scheduled && jigsawTileWatches.get(watch.key()) == watch) { + warnJigsawTileWatchScheduleFailure(watch); + scheduleJigsawTileWatchReconciliation(watch, false); + } + } + + private void runJigsawTileWatch(JigsawTileWatch watch, boolean release) { + if (jigsawTileWatches.get(watch.key()) != watch) { + return; + } + ActiveStudio studio = studios.get(watch.key().worldId()); + if (!enabled || studio != watch.studio() + || !isCurrentRequest(studio, watch.key().requestId())) { + jigsawTileWatches.remove(watch.key(), watch); + return; + } + Block block = studio.world().getBlockAt( + watch.key().worldX(), watch.key().worldY(), watch.key().worldZ()); + JigsawStudioBay bay = studio.generator().getSession().layout().findAt( + watch.key().worldX(), watch.key().worldY(), watch.key().worldZ()); + if (block.getType() != Material.JIGSAW + || bay == null + || !bay.stableId().equals(watch.workcellId())) { + jigsawTileWatches.remove(watch.key(), watch); + return; + } + KMap snapshot; + try { + snapshot = BukkitPlatform.serializeTile(block.getLocation()); + } catch (Throwable exception) { + jigsawTileWatches.remove(watch.key(), watch); + IrisLogging.reportError(exception); + return; + } + if (snapshot == null) { + jigsawTileWatches.remove(watch.key(), watch); + IrisLogging.warn("Jigsaw Studio stopped watching unreadable jigsaw marker NBT at %d,%d,%d in %s", + watch.key().worldX(), + watch.key().worldY(), + watch.key().worldZ(), + studio.world().getName()); + return; + } + JigsawTilePollDecision decision = jigsawTilePollDecision( + watch.snapshot(), snapshot, release); + if (decision.changed()) { + markDirty( + studio.world(), + watch.key().worldX(), + watch.key().worldY(), + watch.key().worldZ()); + } + if (!decision.continueWatching()) { + jigsawTileWatches.remove(watch.key(), watch); + return; + } + JigsawTileWatch next = new JigsawTileWatch( + watch.key(), + watch.studio(), + watch.playerId(), + watch.workcellId(), + snapshot, + watch.scheduleFailureLogged(), + new AtomicBoolean(false)); + if (jigsawTileWatches.replace(watch.key(), watch, next)) { + scheduleJigsawTileWatch(next); + } + } + + private boolean hasJigsawTileWatch(UUID requestId) { + if (requestId == null) { + return false; + } + for (JigsawTileWatchKey key : jigsawTileWatches.keySet()) { + if (requestId.equals(key.requestId())) { + return true; + } + } + return false; + } + + private void clearJigsawTileWatches(UUID requestId) { + if (requestId != null) { + jigsawTileWatches.keySet().removeIf(key -> requestId.equals(key.requestId())); + } + } + + private void finalizeJigsawTileWatchesForPlayer(UUID playerId) { + if (playerId == null) { + return; + } + for (JigsawTileWatch watch : List.copyOf(jigsawTileWatches.values())) { + if (playerId.equals(watch.playerId())) { + finalizeJigsawTileWatch(watch); + } + } + } + + private void finalizeJigsawTileWatches(UUID requestId) { + if (requestId == null) { + return; + } + for (JigsawTileWatch watch : List.copyOf(jigsawTileWatches.values())) { + if (requestId.equals(watch.key().requestId())) { + finalizeJigsawTileWatch(watch); + } + } + } + + private void finalizeAllJigsawTileWatches() { + for (JigsawTileWatch watch : List.copyOf(jigsawTileWatches.values())) { + try { + finalizeJigsawTileWatch(watch); + } catch (Throwable exception) { + IrisLogging.reportError("Failed to finalize a Jigsaw Studio tile watch during shutdown.", exception); + } + } + } + + private void finalizeJigsawTileWatch(JigsawTileWatch watch) { + if (jigsawTileWatches.get(watch.key()) != watch) { + return; + } + World world = watch.studio().world(); + int chunkX = watch.key().worldX() >> 4; + int chunkZ = watch.key().worldZ() >> 4; + if (J.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { + runJigsawTileWatch(watch, true); + return; + } + boolean scheduled = J.runRegion( + world, + chunkX, + chunkZ, + () -> runJigsawTileWatch(watch, true)); + if (!scheduled) { + warnJigsawTileWatchScheduleFailure(watch); + scheduleJigsawTileWatchReconciliation(watch, true); + } + } + + private void warnJigsawTileWatchScheduleFailure(JigsawTileWatch watch) { + if (watch.scheduleFailureLogged().compareAndSet(false, true)) { + IrisLogging.warn("Jigsaw Studio could not schedule jigsaw marker NBT at %d,%d,%d in %s; Iris will keep retrying", + watch.key().worldX(), + watch.key().worldY(), + watch.key().worldZ(), + watch.studio().world().getName()); + } + } + + private void scheduleJigsawTileWatchReconciliation(JigsawTileWatch watch, boolean release) { + if (!watch.reconciliationScheduled().compareAndSet(false, true)) { + return; + } + try { + J.s(() -> { + watch.reconciliationScheduled().set(false); + if (jigsawTileWatches.get(watch.key()) != watch) { + return; + } + if (release) { + finalizeJigsawTileWatch(watch); + } else { + scheduleJigsawTileWatch(watch); + } + }, AUTOSAVE_RETRY_TICKS); + } catch (Throwable exception) { + watch.reconciliationScheduled().set(false); + IrisLogging.reportError(exception); + } + } + + static boolean tileSnapshotChanged( + KMap previous, + KMap current + ) { + return !Objects.equals(previous, current); + } + + static JigsawTilePollDecision jigsawTilePollDecision( + KMap previous, + KMap current, + boolean release + ) { + return new JigsawTilePollDecision(tileSnapshotChanged(previous, current), !release); + } + + private void markAllDirty() { + for (ActiveStudio studio : studios.values()) { + markAllDirty(studio); + } + } + + private int markAllDirty(ActiveStudio studio) { + int changed = 0; + JigsawStudioSession session = studio.generator().getSession(); + for (JigsawStudioBay bay : session.layout().bays()) { + JigsawStudioVariant activeVariant = session.activeVariant(bay.stableId()).orElse(null); + if (activeVariant == null || !activeVariant.owned()) { + continue; + } + JigsawStudioSession.DirtyMark dirtyMark = session.markWorkcellDirty(bay.stableId()); + if (dirtyMark.status() == JigsawStudioSession.DirtyStatus.MARKED) { + scheduleAutosave( + studio, + bay, + dirtyMark.identity().orElseThrow(), + AUTOSAVE_DEBOUNCE_TICKS); + changed++; + } + } + if (changed > 0) { + markEvaluationStale(studio); + for (JigsawStudioBay bay : session.layout().bays()) { + refreshWorkcellContext(studio.worldId(), bay.stableId()); + } + } + return changed; + } + + private void scheduleAutosave( + ActiveStudio studio, + JigsawStudioBay bay, + JigsawStudioSession.DirtyIdentity identity, + int delayTicks + ) { + UUID requestId = studio.generator().getRequest().requestId(); + AutosaveKey key = new AutosaveKey(requestId, bay.stableId()); + AutosaveTicket ticket = new AutosaveTicket( + key, + studio, + identity, + new AtomicBoolean(false), + new AtomicBoolean(false)); + autosaves.put(key, ticket); + scheduleAutosave(ticket, delayTicks); + } + + private void scheduleAutosave(AutosaveTicket ticket, int delayTicks) { + if (autosaves.get(ticket.key()) != ticket + || !ticket.scheduled().compareAndSet(false, true)) { + return; + } + JigsawStudioBay bay = resolveAutosaveBay(ticket); + if (bay == null) { + ticket.scheduled().set(false); + return; + } + JigsawStudioBounds bounds = bay.bounds(); + boolean scheduled = J.runRegion( + ticket.studio().world(), + bounds.originX() >> 4, + bounds.originZ() >> 4, + () -> runAutosave(ticket), + delayTicks); + if (!scheduled) { + ticket.scheduled().set(false); + if (ticket.scheduleFailureLogged().compareAndSet(false, true)) { + IrisLogging.warn("Jigsaw Studio autosave could not schedule workcell %s for request %s; Iris will keep retrying", + ticket.key().workcellId(), ticket.key().requestId()); + } + scheduleAutosaveReconciliation(ticket); + } + } + + private JigsawStudioBay resolveAutosaveBay(AutosaveTicket ticket) { + if (autosaves.get(ticket.key()) != ticket) { + return null; + } + JigsawStudioBay bay = resolveCurrentAutosaveBay( + ticket.studio().generator().getSession(), + ticket.key().workcellId()); + if (bay != null) { + return bay; + } + if (autosaves.remove(ticket.key(), ticket)) { + reportAutosaveFailure( + ticket, + new AutosavePersistentFailure( + "workcell is absent from the current Studio layout; pending ticket retired", + null)); + } + return null; + } + + static JigsawStudioBay resolveCurrentAutosaveBay( + JigsawStudioSession session, + String workcellId + ) { + JigsawStudioSession activeSession = Objects.requireNonNull( + session, + "Jigsaw Studio autosave session"); + return activeSession.layout().get(Objects.requireNonNull( + workcellId, + "Jigsaw Studio autosave workcell ID")); + } + + private boolean retainCurrentAutosaveFailure( + ActiveStudio studio, + String workcellId, + AutosavePersistentFailure failure + ) { + AutosaveKey key = new AutosaveKey( + studio.generator().getRequest().requestId(), + workcellId); + AutosaveTicket ticket = autosaves.get(key); + return ticket != null && retainPersistentAutosaveFailure(ticket, failure); + } + + private AutosaveFailureState autosaveFailureState( + ActiveStudio studio, + JigsawStudioSession.SaveIdentity saveIdentity + ) { + AutosaveKey key = new AutosaveKey( + studio.generator().getRequest().requestId(), + saveIdentity.workcellId()); + AutosaveTicket ticket = autosaves.get(key); + return ticket != null && matchesSaveIdentity(ticket.identity(), saveIdentity) + ? ticket.failureState() + : new AutosaveFailureState(); + } + + private boolean retainPersistentAutosaveFailure( + AutosaveTicket ticket, + AutosavePersistentFailure failure + ) { + if (autosaves.get(ticket.key()) != ticket + || !isCurrentRequest(ticket.studio(), ticket.key().requestId()) + || !ticket.studio().generator().getSession().isDirtyCurrent(ticket.identity())) { + return false; + } + AutosaveFailureDecision decision = reportAutosaveFailure(ticket, failure); + expediteAutosave(ticket, decision.retryTicks()); + return true; + } + + private static AutosaveFailureDecision reportAutosaveFailure( + AutosaveTicket ticket, + AutosavePersistentFailure failure + ) { + JigsawStudioActivation.Request request = ticket.studio().generator().getRequest(); + return recordPersistentAutosaveFailure( + ticket.failureState(), + ticket.key().requestId(), + request.structureKey(), + ticket.key().workcellId(), + ticket.identity().variantKey(), + failure.detail(), + failure.cause()); + } + + private static void reportPersistentSaveFailure( + ActiveStudio studio, + JigsawStudioSession.SaveIdentity saveIdentity, + AutosaveFailureState failureState, + AutosavePersistentFailure failure + ) { + JigsawStudioActivation.Request request = studio.generator().getRequest(); + recordPersistentAutosaveFailure( + failureState, + request.requestId(), + request.structureKey(), + saveIdentity.workcellId(), + saveIdentity.variantKey(), + failure.detail(), + failure.cause()); + } + + static AutosaveFailureDecision recordPersistentAutosaveFailure( + AutosaveFailureState state, + UUID requestId, + String structureKey, + String workcellId, + String pieceKey, + String detail, + Throwable cause + ) { + AutosaveFailureState activeState = Objects.requireNonNull( + state, + "Jigsaw Studio autosave failure state"); + AutosaveFailureDecision decision = activeState.recordPersistentFailure(); + if (!decision.logFailure()) { + return decision; + } + String context = autosaveFailureContext( + requestId, + structureKey, + workcellId, + pieceKey, + detail); + if (cause == null) { + IrisLogging.warn("%s", context); + } else { + IrisLogging.reportError(context, cause); + } + return decision; + } + + static String autosaveFailureContext( + UUID requestId, + String structureKey, + String workcellId, + String pieceKey, + String detail + ) { + return "Jigsaw Studio save failure: request=" + + Objects.requireNonNull(requestId, "Jigsaw Studio autosave request ID") + + " structure=" + + Objects.requireNonNull(structureKey, "Jigsaw Studio autosave structure key") + + " workcell=" + + Objects.requireNonNull(workcellId, "Jigsaw Studio autosave workcell ID") + + " piece=" + + Objects.requireNonNull(pieceKey, "Jigsaw Studio autosave piece key") + + " failure=" + + Objects.requireNonNull(detail, "Jigsaw Studio autosave failure detail"); + } + + static int persistentAutosaveRetryTicks(int failureCount) { + int index = Math.max(0, Math.min( + failureCount - 1, + AUTOSAVE_PERSISTENT_RETRY_DELAYS.size() - 1)); + return AUTOSAVE_PERSISTENT_RETRY_DELAYS.get(index); + } + + private void scheduleAutosaveReconciliation(AutosaveTicket ticket) { + try { + J.s(() -> { + if (autosaves.get(ticket.key()) == ticket + && !ticket.scheduled().get()) { + scheduleAutosave(ticket, AUTOSAVE_RETRY_TICKS); + } + }, AUTOSAVE_RETRY_TICKS); + } catch (Throwable exception) { + IrisLogging.reportError(exception); + } + } + + private void runAutosave(AutosaveTicket ticket) { + if (autosaves.get(ticket.key()) != ticket) { + return; + } + ticket.scheduled().set(false); + ActiveStudio studio = ticket.studio(); + JigsawStudioSession session = studio.generator().getSession(); + if (!isCurrentRequest(studio, ticket.key().requestId())) { + autosaves.remove(ticket.key(), ticket); + return; + } + JigsawStudioBay bay = resolveAutosaveBay(ticket); + if (bay == null) { + return; + } + if (!session.isDirtyCurrent(ticket.identity())) { + autosaves.remove(ticket.key(), ticket); + return; + } + SaveAttempt attempt = startSave(studio, studio.world(), bay, null, false); + if (attempt == SaveAttempt.STARTED || attempt == SaveAttempt.PERSISTENT_FAILURE) { + return; + } + scheduleAutosave( + ticket, + attempt == SaveAttempt.RETRY + ? AUTOSAVE_RETRY_TICKS + : AUTOSAVE_DEBOUNCE_TICKS); + } + + private void completeAutosaveAttempt( + ActiveStudio studio, + JigsawStudioSession.SaveIdentity saveIdentity, + AutosaveFailureState failureState, + AutosavePersistentFailure persistentFailure + ) { + AutosaveKey key = new AutosaveKey( + studio.generator().getRequest().requestId(), + saveIdentity.workcellId()); + AutosaveTicket ticket = autosaves.get(key); + if (ticket == null || !matchesSaveIdentity(ticket.identity(), saveIdentity)) { + if (persistentFailure != null) { + reportPersistentSaveFailure(studio, saveIdentity, failureState, persistentFailure); + } + return; + } + if (resolveAutosaveBay(ticket) == null) { + return; + } + if (!studio.generator().getSession().isDirtyCurrent(ticket.identity())) { + autosaves.remove(key, ticket); + return; + } + if (persistentFailure != null) { + retainPersistentAutosaveFailure(ticket, persistentFailure); + return; + } + scheduleAutosave(ticket, AUTOSAVE_RETRY_TICKS); + } + + private static boolean matchesSaveIdentity( + JigsawStudioSession.DirtyIdentity dirtyIdentity, + JigsawStudioSession.SaveIdentity saveIdentity + ) { + return dirtyIdentity.sessionId().equals(saveIdentity.sessionId()) + && dirtyIdentity.workcellId().equals(saveIdentity.workcellId()) + && dirtyIdentity.variantKey().equals(saveIdentity.variantKey()) + && dirtyIdentity.loadGeneration() == saveIdentity.loadGeneration() + && dirtyIdentity.mutationGeneration() == saveIdentity.mutationGeneration(); + } + + private void drainAutosavesBeforeDisable() { + for (ActiveStudio studio : List.copyOf(studios.values())) { + try { + drainAutosavesBeforeRemoval(studio); + } catch (Throwable exception) { + IrisLogging.reportError( + "Failed to drain Jigsaw Studio autosaves in world " + + studio.worldId() + " during shutdown.", + exception); + } + } + if (!autosaves.isEmpty()) { + IrisLogging.warn("Jigsaw Studio disabled with %d autosave operation(s) still pending after the final drain attempt", + autosaves.size()); + } + } + + private void drainAutosavesBeforeRemoval(ActiveStudio studio) { + if (studio == null) { + return; + } + UUID requestId = studio.generator().getRequest().requestId(); + if (discardingRequest(requestId)) { + return; + } + if (J.isFolia() || !J.isPrimaryThread()) { + expediteAutosaves(requestId); + return; + } + for (AutosaveTicket ticket : List.copyOf(autosaves.values())) { + if (requestId.equals(ticket.key().requestId())) { + drainAutosaveSynchronously(ticket); + } + } + } + + private boolean drainAutosaveSynchronously(AutosaveTicket ticket) { + if (autosaves.get(ticket.key()) != ticket) { + return true; + } + ActiveStudio studio = ticket.studio(); + JigsawStudioSession session = studio.generator().getSession(); + if (!isCurrentRequest(studio, ticket.key().requestId())) { + autosaves.remove(ticket.key(), ticket); + return true; + } + JigsawStudioBay bay = resolveAutosaveBay(ticket); + if (bay == null) { + return false; + } + if (!session.isDirtyCurrent(ticket.identity())) { + autosaves.remove(ticket.key(), ticket); + return true; + } + JigsawStudioVariant activeVariant = session.activeVariant(bay.stableId()).orElse(null); + if (activeVariant == null || !activeVariant.owned()) { + return false; + } + BayReadiness readiness = studio.population(bay).readiness(); + if (!readiness.ready()) { + return false; + } + CaptureTarget captureTarget; + try { + captureTarget = resolveCaptureTarget(studio, bay, activeVariant); + } catch (IOException exception) { + retainPersistentAutosaveFailure( + ticket, + new AutosavePersistentFailure( + "Jigsaw Studio cannot capture this workcell: " + failureMessage(exception), + exception)); + return false; + } + JigsawStudioSession.SaveStart sessionSave = session.beginSave(bay.stableId()); + if (sessionSave.status() != JigsawStudioSession.SaveStatus.STARTED) { + return false; + } + JigsawStudioSession.SaveIdentity saveIdentity = sessionSave.identity().orElseThrow(); + if (tryBeginSave(ticket.key().requestId()) != SaveStart.STARTED) { + session.abortSave(saveIdentity); + return false; + } + List areas = chunkIntersections(captureTarget.bounds()); + CaptureWork work = new CaptureWork( + studio, + studio.world(), + bay, + captureTarget, + null, + ticket.key().requestId(), + saveIdentity, + ticket.failureState()); + CaptureCoordinator coordinator = new CaptureCoordinator(work, areas); + List snapshots = new ArrayList<>(areas.size()); + try { + for (ChunkCaptureArea area : areas) { + if (!studio.world().isChunkLoaded(area.chunkX(), area.chunkZ())) { + throw new IOException("Workcell chunk " + area.chunkX() + "," + area.chunkZ() + + " is not loaded during the final autosave drain"); + } + snapshots.add(captureChunkIntersection( + studio.world(), + captureTarget.bounds(), + captureTarget.piece(), + captureTarget.object(), + area, + captureTarget.displayRotationQuarterTurns(), + captureTarget.connectorsVisible())); + } + assembleAndPersist(coordinator, snapshots); + } catch (Throwable exception) { + coordinator.fail("Jigsaw Studio final autosave drain failed: " + + failureMessage(exception), exception); + } + return !session.isDirtyCurrent(ticket.identity()); + } + + private void expediteAutosaves(UUID requestId) { + if (requestId == null) { + return; + } + for (AutosaveTicket current : List.copyOf(autosaves.values())) { + if (!requestId.equals(current.key().requestId())) { + continue; + } + expediteAutosave(current, 0); + } + } + + private void expediteAutosave(AutosaveTicket current, int delayTicks) { + AutosaveTicket expedited = new AutosaveTicket( + current.key(), + current.studio(), + current.identity(), + new AtomicBoolean(false), + current.scheduleFailureLogged(), + current.failureState()); + if (autosaves.replace(current.key(), current, expedited)) { + scheduleAutosave(expedited, delayTicks); + } + } + + private boolean requiresLifecycleDrain(ActiveStudio studio) { + UUID requestId = studio.generator().getRequest().requestId(); + if (hasJigsawTileWatch(requestId) + || studio.generator().getSession().isDirty() + || studio.generator().getSession().operationInProgress()) { + return true; + } + for (AutosaveKey key : autosaves.keySet()) { + if (requestId.equals(key.requestId())) { + return true; + } + } + synchronized (saveLifecycleLock) { + return savesInProgress.contains(requestId) + || graphMutationsInProgress.contains(requestId) + || materializationsInProgress.contains(requestId) + || exportsInProgress.contains(requestId); + } + } + + private boolean discardingRequest(UUID requestId) { + synchronized (saveLifecycleLock) { + return discardingRequests.contains(requestId); + } + } + + private void clearAutosaves(UUID requestId) { + if (requestId != null) { + autosaves.keySet().removeIf(key -> key.requestId().equals(requestId)); + } + } + + private void markEvaluationStale(ActiveStudio studio) { + UUID requestId = studio.generator().getRequest().requestId(); + evaluations.computeIfPresent( + requestId, + (ignored, current) -> current.state() == JigsawStudioEvaluationState.STALE + ? current + : current.stale("Autosave is pending for one or more edited workcells")); + } + + static boolean isMutatingCommand(String commandLine) { + return MUTATING_COMMANDS.contains(commandLabel(commandLine)); + } + + static boolean blocksMutatingCommand(UUID ownerId, UUID actorId, String commandLine) { + return !ownerMatches(ownerId, actorId) + && !isSafeNonOwnerCommand(commandLine); + } + + static boolean blocksNonEditableWorkcellMutation( + boolean hasNonEditableWorkcell, + String commandLine + ) { + return hasNonEditableWorkcell && isMutatingCommand(commandLine); + } + + static boolean canToggleVariantRotation( + JigsawStudioCompatibilityTarget compatibilityTarget, + JigsawStudioVariant variant + ) { + JigsawStudioCompatibilityTarget target = Objects.requireNonNull( + compatibilityTarget, + "Jigsaw Studio compatibility target"); + JigsawStudioVariant activeVariant = Objects.requireNonNull(variant, "Jigsaw Studio variant"); + return activeVariant.owned() + && (target != JigsawStudioCompatibilityTarget.VANILLA_PORTABLE + || !activeVariant.rotatable()); + } + + static boolean isSafeNonOwnerCommand(String commandLine) { + String normalized = normalizeCommandLine(commandLine); + String label = commandLabel(normalized); + if (SAFE_NON_OWNER_COMMANDS.contains(label)) { + return true; + } + int separator = normalized.indexOf(' '); + if (!label.equals("iris") || separator < 0) { + return false; + } + String arguments = normalized.substring(separator + 1).trim(); + return arguments.equals("jigsaw status") || arguments.startsWith("jigsaw status "); + } + + private static String commandLabel(String commandLine) { + String normalized = normalizeCommandLine(commandLine); + int separator = normalized.indexOf(' '); + String label = separator < 0 ? normalized : normalized.substring(0, separator); + int namespace = label.lastIndexOf(':'); + return namespace < 0 ? label : label.substring(namespace + 1); + } + + private static String normalizeCommandLine(String commandLine) { + if (commandLine == null) { + return ""; + } + String normalized = commandLine.trim().toLowerCase(Locale.ROOT); + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + return normalized; + } + + private void captureChunk(CaptureCoordinator coordinator, ChunkCaptureArea area) { + if (coordinator.stopped()) { + return; + } + ActiveStudio studio = coordinator.studio(); + World world = coordinator.world(); + if (!isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) { + coordinator.fail("Jigsaw Studio changed before every bay chunk could be captured; save cancelled.", null); + return; + } + if (!world.isChunkLoaded(area.chunkX(), area.chunkZ())) { + coordinator.fail("Bay chunk " + area.chunkX() + "," + area.chunkZ() + + " is not loaded. Visit the whole bay and try again.", null); + return; + } + try { + ChunkSnapshot snapshot = captureChunkIntersection( + world, + coordinator.captureTarget().bounds(), + coordinator.captureTarget().piece(), + coordinator.captureTarget().object(), + area, + coordinator.captureTarget().displayRotationQuarterTurns(), + coordinator.captureTarget().connectorsVisible()); + if (!isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) { + coordinator.fail("Jigsaw Studio changed while bay chunks were being captured; save cancelled.", null); + return; + } + coordinator.accept(snapshot); + } catch (Throwable exception) { + coordinator.fail("Jigsaw Studio capture failed in chunk " + + area.chunkX() + "," + area.chunkZ() + ": " + failureMessage(exception), exception); + } + } + + private void assembleAndPersist(CaptureCoordinator coordinator, List snapshots) { + if (coordinator.failed()) { + return; + } + ActiveStudio studio = coordinator.studio(); + if (!isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) { + coordinator.fail("Jigsaw Studio changed before captured chunks could be assembled; save cancelled.", null); + return; + } + try { + Capture capture = aggregateSnapshots( + coordinator.captureTarget().bounds(), + coordinator.areas(), + snapshots, + coordinator.captureTarget().displayRotationQuarterTurns()); + List connectors = preserveCapturedConnectorOrder( + coordinator.captureTarget().piece(), + capture.connectors()); + requireWorkcellTopology( + coordinator.bay(), + connectors, + coordinator.captureTarget().displayRotationQuarterTurns()); + if (!isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) { + coordinator.fail("Jigsaw Studio changed before the captured bay could be written; save cancelled.", null); + return; + } + message(coordinator.player(), "Captured " + connectors.size() + + " connector(s) from " + snapshots.size() + + " chunk(s); validating and writing the owned structure graph..."); + persistCapture(coordinator, capture, connectors); + } catch (WorkcellTopologyException exception) { + coordinator.failPersistent( + "Jigsaw Studio cannot autosave this planar workcell: " + + failureMessage(exception), + null); + } catch (Throwable exception) { + coordinator.failPersistent( + "Jigsaw Studio capture assembly failed: " + failureMessage(exception), + exception); + } + } + + private void persistCapture( + CaptureCoordinator coordinator, + Capture capture, + List connectors + ) { + ActiveStudio studio = coordinator.studio(); + JigsawStudioBay bay = coordinator.bay(); + Player player = coordinator.player(); + try { + if (!isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) { + message(player, "Jigsaw Studio changed before the captured bay could be written; save cancelled."); + return; + } + JigsawStudioActivation.Request request = studio.generator().getRequest(); + IrisData source = request.source(); + Path packRoot = source.getDataFolder().toPath(); + JigsawStudioResourceBundleAssembler.Assembly assembly = + JigsawStudioResourceBundleAssembler.assemble( + packRoot, + request.structureKey(), + coordinator.saveIdentity().variantKey(), + capture.objectContent(), + connectors, + capture.hasBlockEntities()); + StructureResourceBundleGraphCompiler.requireViable(assembly.bundle()); + if (!isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) { + message(player, "Jigsaw Studio changed during validation; save cancelled before writing."); + return; + } + JigsawStudioHistoryStore historyStore = new JigsawStudioHistoryStore( + packRoot, + request.structureKey()); + JigsawStudioHistoryStore.Snapshot previous = historyStore.snapshotCurrent( + coordinator.saveIdentity().variantKey()); + if (!previous.matches(assembly.bundle())) { + historyStore.append(previous); + } + StructureWriteResult result; + synchronized (saveLifecycleLock) { + if (!isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) { + message(player, "Jigsaw Studio changed before the validated save entered the writer; save cancelled."); + return; + } + result = new StructureTransactionWriter(packRoot) + .write(assembly.bundle(), StructureWriteOptions.overwriteExpected( + assembly.expectedManifestHash())); + } + if (!result.successful()) { + String failure = writeFailure(result); + coordinator.recordPersistentFailure(failure, result.failure().orElse(null)); + message(player, failure); + return; + } + source.invalidateStructureResources(); + studio.generator().invalidateRender(bay.stableId(), coordinator.saveIdentity().variantKey()); + JigsawStudioSession session = studio.generator().getSession(); + boolean unchanged = session.markWorkcellSaved(coordinator.saveIdentity()); + try { + reloadSessionLayout(studio); + } catch (IOException | RuntimeException refreshFailure) { + IrisLogging.reportError(refreshFailure); + message(player, "The graph saved, but Studio could not refresh its variant catalog. " + + "Close and reopen this project before continuing: " + + failureMessage(refreshFailure)); + } + String cleanup = result.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED + ? " The graph committed, but transaction cleanup requires operator attention in the console." + : ""; + String mutationNotice = unchanged ? "" : " Newer edits remain unsaved."; + message(player, "Saved piece '" + coordinator.saveIdentity().variantKey() + "' and object '" + + assembly.objectKey() + "' atomically." + mutationNotice + cleanup); + playSaveSound(player); + if (unchanged) { + scheduleEvaluation(studio); + } + IrisLogging.info("Jigsaw Studio saved: structure=%s piece=%s object=%s connectors=%d status=%s", + request.structureKey(), coordinator.saveIdentity().variantKey(), assembly.objectKey(), + connectors.size(), result.status()); + } catch (Throwable exception) { + String failure = "Jigsaw Studio save failed: " + failureMessage(exception); + coordinator.recordPersistentFailure(failure, exception); + message(player, failure); + } finally { + coordinator.complete(); + } + } + + static List chunkIntersections(JigsawStudioBounds bounds) { + JigsawStudioBounds captureBounds = Objects.requireNonNull(bounds, "Jigsaw Studio capture bounds"); + List areas = new ArrayList<>(); + int minimumChunkX = captureBounds.originX() >> 4; + int maximumChunkX = captureBounds.maxX() >> 4; + int minimumChunkZ = captureBounds.originZ() >> 4; + int maximumChunkZ = captureBounds.maxZ() >> 4; + for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) { + int chunkOriginX = chunkX << 4; + int minimumX = Math.max(0, chunkOriginX - captureBounds.originX()); + int maximumX = Math.min( + captureBounds.dimensions().width(), + chunkOriginX + 16 - captureBounds.originX()); + for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) { + int chunkOriginZ = chunkZ << 4; + int minimumZ = Math.max(0, chunkOriginZ - captureBounds.originZ()); + int maximumZ = Math.min( + captureBounds.dimensions().depth(), + chunkOriginZ + 16 - captureBounds.originZ()); + areas.add(new ChunkCaptureArea( + chunkX, + chunkZ, + minimumX, + maximumX, + minimumZ, + maximumZ)); + } + } + return List.copyOf(areas); + } + + static ChunkSnapshot captureChunkIntersection( + World world, + JigsawStudioBounds bounds, + IrisJigsawPiece sourcePiece, + IrisObject sourceObject, + ChunkCaptureArea area + ) throws IOException { + return captureChunkIntersection(world, bounds, sourcePiece, sourceObject, area, 0); + } + + static ChunkSnapshot captureChunkIntersection( + World world, + JigsawStudioBounds bounds, + IrisJigsawPiece sourcePiece, + IrisObject sourceObject, + ChunkCaptureArea area, + int displayRotationQuarterTurns + ) throws IOException { + return captureChunkIntersection( + world, + bounds, + sourcePiece, + sourceObject, + area, + displayRotationQuarterTurns, + true); + } + + static ChunkSnapshot captureChunkIntersection( + World world, + JigsawStudioBounds bounds, + IrisJigsawPiece sourcePiece, + IrisObject sourceObject, + ChunkCaptureArea area, + int displayRotationQuarterTurns, + boolean connectorsVisible + ) throws IOException { + World captureWorld = Objects.requireNonNull(world, "Jigsaw Studio capture world"); + JigsawStudioBounds captureBounds = Objects.requireNonNull(bounds, "Jigsaw Studio capture bounds"); + IrisObject captureSource = Objects.requireNonNull(sourceObject, "Jigsaw Studio source object"); + ChunkCaptureArea captureArea = Objects.requireNonNull(area, "Jigsaw Studio chunk capture area"); + int quarterTurns = Math.floorMod(displayRotationQuarterTurns, 4); + int sourceWidth = (quarterTurns & 1) == 0 + ? captureBounds.dimensions().width() + : captureBounds.dimensions().depth(); + int sourceDepth = (quarterTurns & 1) == 0 + ? captureBounds.dimensions().depth() + : captureBounds.dimensions().width(); + List blocks = new ArrayList<>(); + List connectors = new ArrayList<>(); + Map hiddenConnectors = connectorsVisible + ? Map.of() + : displayedSourceConnectors(sourcePiece, captureBounds.dimensions(), quarterTurns); + for (int x = captureArea.minimumX(); x < captureArea.maximumX(); x++) { + for (int y = 0; y < bounds.dimensions().height(); y++) { + for (int z = captureArea.minimumZ(); z < captureArea.maximumZ(); z++) { + Block block = captureWorld.getBlockAt( + captureBounds.originX() + x, + captureBounds.originY() + y, + captureBounds.originZ() + z); + BlockData blockData = block.getBlockData(); + IrisJigsawConnector hiddenConnector = hiddenConnectors.get(new LocalPosition(x, y, z)); + if (hiddenConnector != null) { + hiddenConnector.setFinalState(blockData.getAsString()); + connectors.add(CapturedConnector.from(hiddenConnector)); + } + if (hiddenConnector == null && blockData instanceof Jigsaw jigsaw) { + KMap nbt = BukkitPlatform.serializeTile(block.getLocation()); + if (nbt == null) { + throw new IOException("Cannot read jigsaw marker NBT at " + + block.getX() + "," + block.getY() + "," + block.getZ() + + ". The active NMS binding must support tile serialization."); + } + IrisJigsawConnector connector; + try { + connector = JigsawStudioMarkerParser.parse(nbt, jigsaw.getOrientation(), x, y, z); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid jigsaw marker at " + + block.getX() + "," + block.getY() + "," + block.getZ() + + ": " + failureMessage(exception), exception); + } + restoreCapturedMetadataForDisplay( + connector, + sourcePiece, + captureBounds.dimensions(), + quarterTurns); + BlockData finalData; + try { + finalData = Bukkit.createBlockData(connector.getFinalState()); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid final_state '" + connector.getFinalState() + + "' at " + block.getX() + "," + block.getY() + "," + block.getZ(), exception); + } + connector.setFinalState(finalData.getAsString()); + connectors.add(CapturedConnector.from(connector)); + if (finalData.getMaterial() != Material.STRUCTURE_VOID) { + blocks.add(new CapturedBlock( + x, + y, + z, + BukkitBlockState.of(finalData), + null)); + } + continue; + } + if (isAir(blockData.getMaterial())) { + LocalPosition sourcePosition = inversePosition( + x, + y, + z, + sourceWidth, + sourceDepth, + quarterTurns); + PlatformBlockState retainedAir = retainedSourceAir( + captureSource, + sourcePosition.x(), + sourcePosition.y(), + sourcePosition.z(), + blockData); + if (retainedAir != null) { + blocks.add(new CapturedBlock(x, y, z, retainedAir, null)); + } + continue; + } + if (blockData.getMaterial() == Material.STRUCTURE_VOID) { + continue; + } + blocks.add(new CapturedBlock( + x, + y, + z, + BukkitBlockState.of(blockData), + TileData.getTileState(block, false))); + } + } + } + return new ChunkSnapshot(captureArea, blocks, connectors); + } + + private static Map displayedSourceConnectors( + IrisJigsawPiece sourcePiece, + JigsawStudioCellDimensions displayDimensions, + int displayRotationQuarterTurns + ) throws IOException { + IrisJigsawPiece source = Objects.requireNonNull(sourcePiece, "Jigsaw Studio source piece"); + if (source.getConnectors() == null) { + throw new IOException("Jigsaw Studio source piece has no connector list"); + } + int quarterTurns = Math.floorMod(displayRotationQuarterTurns, 4); + int sourceWidth = (quarterTurns & 1) == 0 + ? displayDimensions.width() + : displayDimensions.depth(); + int sourceDepth = (quarterTurns & 1) == 0 + ? displayDimensions.depth() + : displayDimensions.width(); + IrisObjectRotation rotation = IrisObjectRotation.of(0, -90.0D * quarterTurns, 0); + Map displayed = new HashMap<>(source.getConnectors().size()); + for (IrisJigsawConnector sourceConnector : source.getConnectors()) { + LocalPosition sourcePosition = connectorPosition(sourceConnector, "source piece"); + LocalPosition position = forwardPosition( + sourcePosition.x(), + sourcePosition.y(), + sourcePosition.z(), + sourceWidth, + sourceDepth, + quarterTurns); + IrisJigsawConnector connector = CapturedConnector.from(sourceConnector).toConnector() + .setPosition(new IrisPosition(position.x(), position.y(), position.z())) + .setDirection(rotation.rotate(sourceConnector.getDirection())) + .setTop(rotation.rotate(sourceConnector.getTop())); + if (displayed.put(position, connector) != null) { + throw new IOException("Jigsaw Studio source piece has duplicate displayed connector position " + + connectorLocation(position)); + } + } + return Map.copyOf(displayed); + } + + static PlatformBlockState retainedSourceAir( + IrisObject sourceObject, + int x, + int y, + int z, + BlockData current + ) { + IrisObject source = Objects.requireNonNull(sourceObject, "Jigsaw Studio source object"); + BlockData currentState = Objects.requireNonNull(current, "Jigsaw Studio current block state"); + if (!isAir(currentState.getMaterial()) + || x < 0 + || y < 0 + || z < 0 + || x >= source.getW() + || y >= source.getH() + || z >= source.getD()) { + return null; + } + PlatformBlockState original = source.getBlocks().get(source.getSigned(x, y, z)); + return original != null && original.isAir() ? original : null; + } + + private static boolean isAir(Material material) { + return material == Material.AIR || material == Material.CAVE_AIR || material == Material.VOID_AIR; + } + + static Capture aggregateSnapshots( + JigsawStudioBounds bounds, + List expectedAreas, + List snapshots + ) throws IOException { + return aggregateSnapshots(bounds, expectedAreas, snapshots, 0); + } + + static Capture aggregateSnapshots( + JigsawStudioBounds bounds, + List expectedAreas, + List snapshots, + int displayRotationQuarterTurns + ) throws IOException { + JigsawStudioBounds captureBounds = Objects.requireNonNull(bounds, "Jigsaw Studio capture bounds"); + int quarterTurns = Math.floorMod(displayRotationQuarterTurns, 4); + List requiredAreas = List.copyOf(expectedAreas); + List capturedSnapshots = List.copyOf(snapshots); + Map requiredByChunk = new HashMap<>(); + for (ChunkCaptureArea area : requiredAreas) { + if (requiredByChunk.put(chunkKey(area.chunkX(), area.chunkZ()), area) != null) { + throw new IOException("Duplicate required bay chunk " + area.chunkX() + "," + area.chunkZ()); + } + } + if (capturedSnapshots.size() != requiredByChunk.size()) { + throw new IOException("Expected " + requiredByChunk.size() + " bay chunk snapshot(s), received " + + capturedSnapshots.size()); + } + Map capturedByChunk = new HashMap<>(); + for (ChunkSnapshot snapshot : capturedSnapshots) { + ChunkCaptureArea area = snapshot.area(); + long key = chunkKey(area.chunkX(), area.chunkZ()); + ChunkCaptureArea required = requiredByChunk.get(key); + if (!area.equals(required)) { + throw new IOException("Unexpected bay chunk intersection for " + + area.chunkX() + "," + area.chunkZ()); + } + if (capturedByChunk.put(key, snapshot) != null) { + throw new IOException("Duplicate bay chunk snapshot " + area.chunkX() + "," + area.chunkZ()); + } + } + if (!capturedByChunk.keySet().containsAll(requiredByChunk.keySet())) { + throw new IOException("One or more required bay chunk snapshots are missing"); + } + + List ordered = new ArrayList<>(capturedSnapshots); + ordered.sort(Comparator + .comparingInt((ChunkSnapshot snapshot) -> snapshot.area().chunkX()) + .thenComparingInt(snapshot -> snapshot.area().chunkZ())); + int sourceWidth = (quarterTurns & 1) == 0 + ? captureBounds.dimensions().width() + : captureBounds.dimensions().depth(); + int sourceDepth = (quarterTurns & 1) == 0 + ? captureBounds.dimensions().depth() + : captureBounds.dimensions().width(); + IrisObject object = new IrisObject( + sourceWidth, + captureBounds.dimensions().height(), + sourceDepth); + IrisObjectRotation inverseRotation = IrisObjectRotation.of(0, 90.0D * quarterTurns, 0); + List connectors = new ArrayList<>(); + Set capturedBlocks = new HashSet<>(); + Set capturedConnectors = new HashSet<>(); + boolean hasBlockEntities = false; + for (ChunkSnapshot snapshot : ordered) { + ChunkCaptureArea area = snapshot.area(); + for (CapturedBlock block : snapshot.blocks()) { + if (!area.contains(block.x(), block.z()) + || block.y() < 0 + || block.y() >= captureBounds.dimensions().height()) { + throw new IOException("Captured block falls outside bay chunk " + + area.chunkX() + "," + area.chunkZ()); + } + LocalPosition position = inversePosition( + block.x(), block.y(), block.z(), sourceWidth, sourceDepth, quarterTurns); + if (!capturedBlocks.add(position)) { + throw new IOException("Duplicate captured block at " + + block.x() + "," + block.y() + "," + block.z()); + } + PlatformBlockState sourceState = quarterTurns == 0 + ? block.state() + : inverseRotation.rotate(block.state(), 0, 0, 0); + if (sourceState == null) { + throw new IOException("Captured block state cannot be inverse-rotated at " + + block.x() + "," + block.y() + "," + block.z()); + } + object.setUnsigned(position.x(), position.y(), position.z(), sourceState); + TileData tileData = block.tileData(); + if (tileData != null) { + object.setUnsignedTile(position.x(), position.y(), position.z(), tileData.clone()); + hasBlockEntities = true; + } + } + for (CapturedConnector capturedConnector : snapshot.connectors()) { + if (!area.contains(capturedConnector.x(), capturedConnector.z()) + || capturedConnector.y() < 0 + || capturedConnector.y() >= captureBounds.dimensions().height()) { + throw new IOException("Captured connector falls outside bay chunk " + + area.chunkX() + "," + area.chunkZ()); + } + LocalPosition position = inversePosition( + capturedConnector.x(), + capturedConnector.y(), + capturedConnector.z(), + sourceWidth, + sourceDepth, + quarterTurns); + if (!capturedConnectors.add(position)) { + throw new IOException("Duplicate captured connector at " + + capturedConnector.x() + "," + capturedConnector.y() + "," + + capturedConnector.z()); + } + IrisJigsawConnector connector = capturedConnector.toConnector(); + if (quarterTurns != 0) { + connector.setPosition(new IrisPosition(position.x(), position.y(), position.z())); + connector.setDirection(inverseRotation.rotate(connector.getDirection())); + connector.setTop(inverseRotation.rotate(connector.getTop())); + PlatformBlockState finalState = B.getStateOrNull(connector.getFinalState(), false); + if (finalState == null) { + throw new IOException("Captured connector final state cannot be parsed at " + + capturedConnector.x() + "," + capturedConnector.y() + "," + + capturedConnector.z()); + } + PlatformBlockState sourceFinalState = inverseRotation.rotate(finalState, 0, 0, 0); + if (sourceFinalState == null) { + throw new IOException("Captured connector final state cannot be inverse-rotated at " + + capturedConnector.x() + "," + capturedConnector.y() + "," + + capturedConnector.z()); + } + connector.setFinalState(sourceFinalState.key()); + } + connectors.add(connector); + } + } + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + object.write(output); + return new Capture(output.toByteArray(), connectors, hasBlockEntities); + } + } + + static List preserveCapturedConnectorOrder( + IrisJigsawPiece sourcePiece, + List capturedConnectors + ) throws IOException { + IrisJigsawPiece source = Objects.requireNonNull(sourcePiece, "Jigsaw Studio source piece"); + List sourceConnectors = source.getConnectors(); + if (sourceConnectors == null) { + throw new IOException("Jigsaw Studio source piece has no connector list"); + } + List captured = new ArrayList<>(Objects.requireNonNull( + capturedConnectors, + "Jigsaw Studio captured connectors")); + Map capturedByPosition = new HashMap<>(captured.size()); + for (IrisJigsawConnector connector : captured) { + LocalPosition position = connectorPosition(connector, "captured"); + if (capturedByPosition.put(position, connector) != null) { + throw new IOException("Jigsaw Studio capture has duplicate connector position " + + connectorLocation(position)); + } + } + + List ordered = new ArrayList<>(captured.size()); + Set sourcePositions = new HashSet<>(sourceConnectors.size()); + for (IrisJigsawConnector connector : sourceConnectors) { + LocalPosition position = connectorPosition(connector, "source piece"); + if (!sourcePositions.add(position)) { + throw new IOException("Jigsaw Studio source piece has duplicate connector position " + + connectorLocation(position)); + } + IrisJigsawConnector matched = capturedByPosition.remove(position); + if (matched != null) { + ordered.add(matched); + } + } + + List appended = new ArrayList<>(capturedByPosition.values()); + appended.sort(NEW_CONNECTOR_SOURCE_POSITION_ORDER); + ordered.addAll(appended); + return List.copyOf(ordered); + } + + private static LocalPosition connectorPosition(IrisJigsawConnector connector, String owner) throws IOException { + if (connector == null || connector.getPosition() == null) { + throw new IOException("Jigsaw Studio " + owner + " contains a connector without a position"); + } + IrisPosition position = connector.getPosition(); + return new LocalPosition(position.getX(), position.getY(), position.getZ()); + } + + private static String connectorLocation(LocalPosition position) { + return position.x() + "," + position.y() + "," + position.z(); + } + + private static LocalPosition inversePosition( + int x, + int y, + int z, + int sourceWidth, + int sourceDepth, + int displayRotationQuarterTurns + ) { + return switch (Math.floorMod(displayRotationQuarterTurns, 4)) { + case 0 -> new LocalPosition(x, y, z); + case 1 -> new LocalPosition(z, y, sourceDepth - 1 - x); + case 2 -> new LocalPosition(sourceWidth - 1 - x, y, sourceDepth - 1 - z); + case 3 -> new LocalPosition(sourceWidth - 1 - z, y, x); + default -> throw new IllegalStateException("Unreachable Jigsaw Studio inverse rotation"); + }; + } + + private static LocalPosition forwardPosition( + int x, + int y, + int z, + int sourceWidth, + int sourceDepth, + int displayRotationQuarterTurns + ) { + return switch (Math.floorMod(displayRotationQuarterTurns, 4)) { + case 0 -> new LocalPosition(x, y, z); + case 1 -> new LocalPosition(sourceDepth - 1 - z, y, x); + case 2 -> new LocalPosition(sourceWidth - 1 - x, y, sourceDepth - 1 - z); + case 3 -> new LocalPosition(z, y, sourceWidth - 1 - x); + default -> throw new IllegalStateException("Unreachable Jigsaw Studio display rotation"); + }; + } + + static void requireWorkcellTopology( + JigsawStudioBay workcell, + List connectors, + int displayRotationQuarterTurns + ) throws IOException { + JigsawPlanarArchetype expected = workcell.archetype().orElse(null); + if (expected == null) { + return; + } + int mask = 0; + for (IrisJigsawConnector connector : connectors) { + mask |= switch (connector.getDirection()) { + case NORTH_NEGATIVE_Z -> JigsawPlanarDirection.NORTH.bit(); + case EAST_POSITIVE_X -> JigsawPlanarDirection.EAST.bit(); + case SOUTH_POSITIVE_Z -> JigsawPlanarDirection.SOUTH.bit(); + case WEST_NEGATIVE_X -> JigsawPlanarDirection.WEST.bit(); + case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> throw new WorkcellTopologyException( + "Planar workcell '" + workcell.stableId() + + "' cannot save a vertical connector. Remove it or use Reset Connector Blocks."); + }; + } + JigsawPlanarTopology sourceTopology = JigsawPlanarTopology.fromMask(mask); + JigsawPlanarTopology displayedTopology = sourceTopology.rotateClockwise(displayRotationQuarterTurns); + if (displayedTopology != expected.canonicalTopology()) { + throw new WorkcellTopologyException("Workcell '" + workcell.stableId() + "' requires " + + topologyDescription(expected.canonicalTopology()) + + ", but the edited markers form " + + topologyDescription(displayedTopology) + + ". Use Reset Connector Blocks to restore the saved topology, or edit the markers to match the floor glyph."); + } + } + + private static String topologyDescription(JigsawPlanarTopology topology) { + int connectorCount = topology.directions().size(); + return topology.name().toLowerCase(Locale.ROOT).replace('_', ' ') + + " (" + connectorCount + " horizontal connector" + + (connectorCount == 1 ? "" : "s") + ")"; + } + + static void storeConnectorFinalState( + IrisObject object, + int x, + int y, + int z, + BlockData finalData + ) { + Objects.requireNonNull(object, "Jigsaw Studio captured object"); + BlockData activeFinalData = Objects.requireNonNull(finalData, "Jigsaw connector final state"); + if (activeFinalData.getMaterial() != Material.STRUCTURE_VOID) { + object.setUnsigned(x, y, z, BukkitBlockState.of(activeFinalData)); + } + } + + private void scheduleHydration( + ActiveStudio studio, + World world, + int chunkX, + int chunkZ, + int attempt + ) { + if (!enabled || studios.get(world.getUID()) != studio) { + return; + } + long key = chunkKey(chunkX, chunkZ); + if (!studio.hydrationsInProgress().add(key)) { + return; + } + boolean scheduled = J.runRegion( + world, + chunkX, + chunkZ, + () -> runHydrationAttempt(studio, world, chunkX, chunkZ, attempt, key), + HYDRATION_RETRY_TICKS); + if (!scheduled) { + studio.hydrationsInProgress().remove(key); + } + } + + private void runHydrationAttempt( + ActiveStudio studio, + World world, + int chunkX, + int chunkZ, + int attempt, + long key + ) { + studio.hydrationsInProgress().remove(key); + if (!enabled || studios.get(world.getUID()) != studio) { + return; + } + if (!world.isChunkLoaded(chunkX, chunkZ)) { + if (attempt < MAX_HYDRATION_ATTEMPTS) { + scheduleHydration(studio, world, chunkX, chunkZ, attempt + 1); + } + return; + } + if (hydrateChunk(studio, world, chunkX, chunkZ)) { + scheduleHydration(studio, world, chunkX, chunkZ, 0); + } + } + + private static boolean hydrateChunk( + ActiveStudio studio, + World world, + int chunkX, + int chunkZ + ) { + long key = chunkKey(chunkX, chunkZ); + boolean verificationRequired = false; + for (JigsawStudioBay bay : studio.generator().getLayout().bays()) { + BayPopulation population = studio.population(bay); + if (!population.needsApplication(key) && !population.needsVerification(key)) { + continue; + } + JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(bay); + if (!rendered.valid()) { + population.fail(rendered.failure()); + continue; + } + try { + if (population.needsApplication(key)) { + applyRenderedBayChunk( + world, + bay, + rendered, + chunkX, + chunkZ, + studio.generator().getSession().workcellSnapshot(bay.stableId()).connectorsVisible()); + population.markApplied(key); + verificationRequired = true; + } else { + verifyRenderedBayChunk( + world, + bay, + rendered, + chunkX, + chunkZ, + studio.generator().getSession().workcellSnapshot(bay.stableId()).connectorsVisible()); + population.markHydrated(key); + } + } catch (Throwable exception) { + if (population.fail("chunk " + chunkX + "," + chunkZ + + " could not hydrate: " + failureMessage(exception))) { + IrisLogging.reportError(exception); + } + } + } + return verificationRequired; + } + + private static void applyRenderedBayChunk( + World world, + JigsawStudioBay bay, + JigsawStudioGenerator.RenderedBay rendered, + int chunkX, + int chunkZ, + boolean connectorsVisible + ) throws IOException { + JigsawStudioBounds bounds = bay.bounds(); + Set connectorPositions = new HashSet<>(rendered.connectors().size()); + if (connectorsVisible) { + for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) { + connectorPositions.add(new LocalPosition(connector.x(), connector.y(), connector.z())); + } + } + for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) { + TileData tileData = block.tileData(); + if (tileData == null + || connectorPositions.contains(new LocalPosition(block.x(), block.y(), block.z()))) { + continue; + } + int worldX = bounds.originX() + block.x(); + int worldZ = bounds.originZ() + block.z(); + if ((worldX >> 4) != chunkX || (worldZ >> 4) != chunkZ) { + continue; + } + Block target = world.getBlockAt(worldX, bounds.originY() + block.y(), worldZ); + if (!tileData.isApplicable(target.getBlockData())) { + throw new IOException("source tile state does not match rendered block at " + + target.getX() + "," + target.getY() + "," + target.getZ()); + } + if (!tileData.toBukkitTry(target)) { + throw new IOException("source tile state could not hydrate at " + + target.getX() + "," + target.getY() + "," + target.getZ()); + } + } + if (!connectorsVisible) { + return; + } + for (JigsawStudioGenerator.RenderedConnector renderedConnector : rendered.connectors()) { + int worldX = bounds.originX() + renderedConnector.x(); + int worldZ = bounds.originZ() + renderedConnector.z(); + if ((worldX >> 4) != chunkX || (worldZ >> 4) != chunkZ) { + continue; + } + Block target = world.getBlockAt( + worldX, + bounds.originY() + renderedConnector.y(), + worldZ); + BlockData blockData = target.getBlockData(); + if (!(blockData instanceof Jigsaw jigsaw)) { + throw new IOException("expected a jigsaw marker at " + + target.getX() + "," + target.getY() + "," + target.getZ()); + } + String actualOrientation = jigsaw.getOrientation().name().toLowerCase(Locale.ROOT); + if (!actualOrientation.equals(renderedConnector.orientation())) { + throw new IOException("jigsaw marker orientation at " + + target.getX() + "," + target.getY() + "," + target.getZ() + + " is " + actualOrientation + " instead of " + renderedConnector.orientation()); + } + KMap expected = markerNbt(renderedConnector.connector()); + BukkitPlatform.deserializeTile(expected, target.getLocation()); + } + } + + private static void verifyRenderedBayChunk( + World world, + JigsawStudioBay bay, + JigsawStudioGenerator.RenderedBay rendered, + int chunkX, + int chunkZ, + boolean connectorsVisible + ) throws IOException { + JigsawStudioBounds bounds = bay.bounds(); + Set connectorPositions = new HashSet<>(rendered.connectors().size()); + if (connectorsVisible) { + for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) { + connectorPositions.add(new LocalPosition(connector.x(), connector.y(), connector.z())); + } + } + for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) { + TileData tileData = block.tileData(); + if (tileData == null + || connectorPositions.contains(new LocalPosition(block.x(), block.y(), block.z()))) { + continue; + } + int worldX = bounds.originX() + block.x(); + int worldZ = bounds.originZ() + block.z(); + if ((worldX >> 4) != chunkX || (worldZ >> 4) != chunkZ) { + continue; + } + Block target = world.getBlockAt(worldX, bounds.originY() + block.y(), worldZ); + if (!tileData.isApplicable(target.getBlockData())) { + throw new IOException("hydrated tile state no longer matches its block at " + + target.getX() + "," + target.getY() + "," + target.getZ()); + } + KMap hydrated = BukkitPlatform.serializeTile(target.getLocation()); + if (hydrated == null + || tileData.getProperties() != null + && !nbtContains(tileData.getProperties(), hydrated)) { + throw new IOException("the active NMS binding did not preserve source tile NBT at " + + target.getX() + "," + target.getY() + "," + target.getZ()); + } + } + if (!connectorsVisible) { + return; + } + for (JigsawStudioGenerator.RenderedConnector renderedConnector : rendered.connectors()) { + int worldX = bounds.originX() + renderedConnector.x(); + int worldZ = bounds.originZ() + renderedConnector.z(); + if ((worldX >> 4) != chunkX || (worldZ >> 4) != chunkZ) { + continue; + } + Block target = world.getBlockAt( + worldX, + bounds.originY() + renderedConnector.y(), + worldZ); + BlockData blockData = target.getBlockData(); + if (!(blockData instanceof Jigsaw jigsaw) + || !jigsaw.getOrientation().name().toLowerCase(Locale.ROOT) + .equals(renderedConnector.orientation())) { + throw new IOException("jigsaw marker changed before hydration completed at " + + target.getX() + "," + target.getY() + "," + target.getZ()); + } + KMap expected = markerNbt(renderedConnector.connector()); + KMap hydrated = BukkitPlatform.serializeTile(target.getLocation()); + if (!markerNbtMatches(expected, hydrated)) { + throw new IOException("the active NMS binding did not preserve jigsaw marker NBT at " + + target.getX() + "," + target.getY() + "," + target.getZ()); + } + } + } + + static KMap markerNbt(IrisJigsawConnector connector) { + IrisJigsawConnector activeConnector = Objects.requireNonNull(connector, "Jigsaw Studio connector"); + KMap nbt = new KMap<>(); + nbt.put("name", studioIdentifier(activeConnector.getName())); + nbt.put("target", studioIdentifier(activeConnector.getTargetName())); + nbt.put("pool", JigsawStudioMarkerKeyCodec.encodePool(activeConnector.getPool())); + nbt.put("final_state", activeConnector.getFinalState()); + nbt.put("joint", activeConnector.getJoint().name().toLowerCase(Locale.ROOT)); + nbt.put("selection_priority", activeConnector.getSelectionPriority()); + nbt.put("placement_priority", activeConnector.getPlacementPriority()); + return nbt; + } + + private static boolean markerNbtMatches( + Map expected, + Map hydrated + ) { + if (hydrated == null) { + return false; + } + for (Map.Entry entry : expected.entrySet()) { + Object actual = hydrated.get(entry.getKey()); + if (entry.getValue() instanceof Number expectedNumber) { + if (!(actual instanceof Number actualNumber) + || expectedNumber.longValue() != actualNumber.longValue()) { + return false; + } + } else if (!Objects.equals(entry.getValue(), actual)) { + return false; + } + } + return true; + } + + private static boolean nbtContains(Object expected, Object actual) { + if (expected instanceof Number expectedNumber) { + if (!(actual instanceof Number actualNumber)) { + return false; + } + if (isIntegral(expectedNumber) && isIntegral(actualNumber)) { + return expectedNumber.longValue() == actualNumber.longValue(); + } + return Double.compare(expectedNumber.doubleValue(), actualNumber.doubleValue()) == 0; + } + if (expected instanceof Map expectedMap) { + if (!(actual instanceof Map actualMap)) { + return false; + } + for (Map.Entry entry : expectedMap.entrySet()) { + if (!actualMap.containsKey(entry.getKey()) + || !nbtContains(entry.getValue(), actualMap.get(entry.getKey()))) { + return false; + } + } + return true; + } + if (expected instanceof List expectedList) { + if (!(actual instanceof List actualList) || expectedList.size() != actualList.size()) { + return false; + } + for (int index = 0; index < expectedList.size(); index++) { + if (!nbtContains(expectedList.get(index), actualList.get(index))) { + return false; + } + } + return true; + } + return Objects.equals(expected, actual); + } + + private static boolean isIntegral(Number number) { + return number instanceof Byte + || number instanceof Short + || number instanceof Integer + || number instanceof Long; + } + + static void restoreCapturedMetadata( + IrisJigsawConnector captured, + IrisJigsawPiece sourcePiece + ) { + IrisJigsawConnector activeCaptured = Objects.requireNonNull(captured, "Captured jigsaw connector"); + IrisJigsawConnector original = connectorAt(sourcePiece, activeCaptured.getPosition()); + if (original == null) { + return; + } + activeCaptured.setName(restoreIdentifier(activeCaptured.getName(), original.getName())); + activeCaptured.setTargetName(restoreIdentifier( + activeCaptured.getTargetName(), + original.getTargetName())); + activeCaptured.setChannel(original.getChannel() == null ? "" : original.getChannel()); + } + + private static void restoreCapturedMetadataForDisplay( + IrisJigsawConnector captured, + IrisJigsawPiece sourcePiece, + JigsawStudioCellDimensions displayDimensions, + int displayRotationQuarterTurns + ) { + if (Math.floorMod(displayRotationQuarterTurns, 4) == 0) { + restoreCapturedMetadata(captured, sourcePiece); + return; + } + int quarterTurns = Math.floorMod(displayRotationQuarterTurns, 4); + int sourceWidth = (quarterTurns & 1) == 0 + ? displayDimensions.width() + : displayDimensions.depth(); + int sourceDepth = (quarterTurns & 1) == 0 + ? displayDimensions.depth() + : displayDimensions.width(); + LocalPosition position = inversePosition( + captured.getPosition().getX(), + captured.getPosition().getY(), + captured.getPosition().getZ(), + sourceWidth, + sourceDepth, + quarterTurns); + IrisObjectRotation inverseRotation = IrisObjectRotation.of(0, 90.0D * quarterTurns, 0); + IrisJigsawConnector sourceOriented = CapturedConnector.from(captured).toConnector() + .setPosition(new IrisPosition(position.x(), position.y(), position.z())) + .setDirection(inverseRotation.rotate(captured.getDirection())) + .setTop(inverseRotation.rotate(captured.getTop())); + restoreCapturedMetadata(sourceOriented, sourcePiece); + captured.setName(sourceOriented.getName()); + captured.setTargetName(sourceOriented.getTargetName()); + captured.setChannel(sourceOriented.getChannel()); + } + + private static IrisJigsawConnector connectorAt(IrisJigsawPiece piece, IrisPosition position) { + if (piece == null || piece.getConnectors() == null || position == null) { + return null; + } + for (IrisJigsawConnector connector : piece.getConnectors()) { + if (connector != null && position.equals(connector.getPosition())) { + return connector; + } + } + return null; + } + + private static String restoreIdentifier(String captured, String original) { + if (original != null && studioIdentifier(original).equals(captured)) { + return original; + } + return captured; + } + + private static String studioIdentifier(String value) { + String normalized = Objects.requireNonNull(value, "Jigsaw Studio marker identifier").trim(); + return normalized.indexOf(':') < 0 ? "iris:" + normalized : normalized; + } + + private boolean isCurrentRequest(ActiveStudio studio, UUID requestId) { + if (studio == null + || requestId == null + || studios.get(studio.worldId()) != studio + || !studio.generator().getRequest().requestId().equals(requestId)) { + return false; + } + JigsawStudioActivation.Request active = JigsawStudioActivation.getRequest( + studio.generator().getRequest().packKey()); + return active != null && active.requestId().equals(requestId); + } + + private boolean isCurrentSave( + ActiveStudio studio, + UUID requestId, + JigsawStudioSession.SaveIdentity identity + ) { + return isCurrentRequest(studio, requestId) + && studio.generator().getSession().isSaveCurrent(identity); + } + + private static String readinessMessage(JigsawStudioBay bay, BayReadiness readiness) { + if (!readiness.failure().isEmpty()) { + return "Bay '" + bay.stableId() + "' is invalid and cannot be saved: " + readiness.failure(); + } + return "Bay '" + bay.stableId() + "' is not ready to save: " + + readiness.generatedChunks() + "/" + readiness.requiredChunks() + " chunk(s) populated, " + + readiness.hydratedChunks() + "/" + readiness.requiredChunks() + + " chunk(s) hydrated. Visit the whole bay and wait for its markers to finish loading."; + } + + private static long chunkKey(int chunkX, int chunkZ) { + return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL); + } + + private static Set requiredChunks( + JigsawStudioBay bay, + JigsawStudioGenerator.RenderedBay rendered + ) { + Set chunks = new HashSet<>(); + if (!rendered.valid()) { + return chunks; + } + JigsawStudioBounds bounds = bay.bounds(); + int minimumChunkX = bounds.originX() >> 4; + int maximumChunkX = bounds.maxX() >> 4; + int minimumChunkZ = bounds.originZ() >> 4; + int maximumChunkZ = bounds.maxZ() >> 4; + for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) { + for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) { + chunks.add(chunkKey(chunkX, chunkZ)); + } + } + return Set.copyOf(chunks); + } + + private static CaptureTarget resolveCaptureTarget( + ActiveStudio studio, + JigsawStudioBay bay, + JigsawStudioVariant variant + ) + throws IOException { + IrisData source = studio.generator().getRequest().source(); + JigsawStudioLayout currentLayout = studio.generator().getSession().layout(); + JigsawStudioBay currentBay = currentLayout.get(bay.stableId()); + if (currentBay == null) { + throw new IOException("workcell '" + bay.stableId() + "' is absent from the current Studio layout"); + } + JigsawStudioVariant activeVariant = Objects.requireNonNull(variant, "Jigsaw Studio capture variant"); + if (!currentLayout.accepts(currentBay, activeVariant)) { + throw new IOException("variant '" + activeVariant.pieceKey() + + "' does not belong to workcell '" + currentBay.stableId() + "'"); + } + IrisJigsawPiece piece = source.getJigsawPieceLoader().load(activeVariant.pieceKey(), false); + if (piece == null || piece.getObject() == null || piece.getObject().isBlank()) { + throw new IOException("piece '" + activeVariant.pieceKey() + "' is missing or has no object"); + } + IrisObject object = source.getObjectLoader().load(piece.getObject(), false); + if (object == null || object.getW() < 1 || object.getH() < 1 || object.getD() < 1) { + throw new IOException("object '" + piece.getObject() + "' is missing or has invalid dimensions"); + } + JigsawStudioBounds bayBounds = currentBay.bounds(); + JigsawStudioCellDimensions sourceDimensions = new JigsawStudioCellDimensions( + object.getW(), object.getH(), object.getD()); + JigsawStudioCellDimensions canonicalDimensions = activeVariant.canonicalDimensions(sourceDimensions); + if (canonicalDimensions.width() > bayBounds.dimensions().width() + || canonicalDimensions.height() > bayBounds.dimensions().height() + || canonicalDimensions.depth() > bayBounds.dimensions().depth()) { + throw new IOException("object '" + piece.getObject() + "' is " + + canonicalDimensions.width() + "x" + canonicalDimensions.height() + "x" + + canonicalDimensions.depth() + " in its displayed orientation" + + " but bay '" + currentBay.stableId() + "' is only " + + bayBounds.dimensions().width() + "x" + bayBounds.dimensions().height() + "x" + + bayBounds.dimensions().depth()); + } + return new CaptureTarget( + new JigsawStudioBounds( + bayBounds.originX(), + bayBounds.originY(), + bayBounds.originZ(), + canonicalDimensions), + piece, + object, + activeVariant.sourceToCanonicalQuarterTurns(), + studio.generator().getSession().workcellSnapshot(currentBay.stableId()).connectorsVisible()); + } + + private void reloadSessionLayout(ActiveStudio studio) throws IOException { + JigsawStudioLayout layout = loadMappedLayout(studio); + studio.generator().getSession().replaceLayout(layout); + for (JigsawStudioBay workcell : layout.bays()) { + studio.generator().invalidateRender(workcell.stableId()); + } + } + + private void scheduleOnlinePlayers(UUID worldId) { + J.runGlobal(() -> { + for (Player player : Bukkit.getOnlinePlayers()) { + J.runEntity(player, () -> { + if (player.getWorld().getUID().equals(worldId)) { + ensureVisualizationLoop(player); + reconcilePlayerContext(player, player.getLocation()); + } + }); + } + }); + } + + public void reconcilePlayerContext(Player player, Location location) { + if (player == null) { + return; + } + Location target = location == null ? player.getLocation() : location; + if (!J.isOwnedByCurrentRegion(player) + || target.getWorld() == null + || !target.getWorld().getUID().equals(player.getWorld().getUID())) { + J.runEntity(player, () -> reconcilePlayerContext(player, player.getLocation()), 1); + return; + } + ActiveStudio studio = studios.get(target.getWorld().getUID()); + if (studio == null) { + clearPlayerContext(player); + return; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay workcell = session.layout().findAt( + target.getBlockX(), target.getBlockY(), target.getBlockZ()); + boolean owner = ownerMatches(player, studio); + selectEnteredWorkcell(session, workcell, owner); + String workcellId = workcell == null ? "" : workcell.stableId(); + playerWorkcells.put(player.getUniqueId(), new PlayerWorkcellContext( + studio.worldId(), + studio.generator().getRequest().requestId(), + workcellId)); + + JigsawStudioVariant variant = workcell == null + ? null + : session.activeVariant(workcell.stableId()).orElse(null); + JigsawStudioBoardState state = boardState(studio, session, workcell, variant); + String workcellRole = workcell == null ? "" : workcell.canonicalDisplayName(); + String workcellName = workcell == null ? "" : workcell.displayName(); + String variantName = variant == null ? "" : variant.resolvedDisplayName(); + String hint = "Triple-sneak for controls"; + boardService().applyJigsawContext(player, new JigsawStudioBoardContext( + studio.worldId(), + studio.generator().getRequest().requestId(), + studio.generator().getRequest().structureKey(), + session.layout().mode(), + workcellRole, + workcellName, + variantName, + state, + hint)); + } + + static boolean selectEnteredWorkcell( + JigsawStudioSession session, + JigsawStudioBay workcell, + boolean owner + ) { + return owner && workcell != null && session.selectBay(workcell.stableId()); + } + + static boolean isNaturalStudioSpawn(CreatureSpawnEvent.SpawnReason reason) { + return reason == CreatureSpawnEvent.SpawnReason.NATURAL; + } + + static void disableNaturalStudioSpawning(World world) { + Objects.requireNonNull(world, "world").setGameRule(GameRules.SPAWN_MOBS, false); + } + + public void refreshWorkcellContext(UUID worldId, String workcellId) { + if (worldId == null || workcellId == null) { + return; + } + J.runGlobal(() -> { + for (Player player : Bukkit.getOnlinePlayers()) { + J.runEntity(player, () -> { + if (!player.getWorld().getUID().equals(worldId)) { + return; + } + PlayerWorkcellContext context = playerWorkcells.get(player.getUniqueId()); + if (context != null && workcellId.equals(context.workcellId())) { + reconcilePlayerContext(player, player.getLocation()); + } + }); + } + }); + } + + private void clearWorldPlayerContexts(UUID worldId) { + J.runGlobal(() -> { + for (Player player : Bukkit.getOnlinePlayers()) { + J.runEntity(player, () -> { + PlayerWorkcellContext context = playerWorkcells.get(player.getUniqueId()); + if (context != null && context.worldId().equals(worldId)) { + closeMenu(player); + clearPlayerContext(player); + } + }); + } + }); + } + + private void clearPlayerContext(Player player) { + if (player == null) { + return; + } + playerWorkcells.remove(player.getUniqueId()); + boardService().clearJigsawContext(player); + } + + private static BoardSVC boardService() { + return IrisServices.get(BoardSVC.class); + } + + private static boolean ownerMatches(Player player, ActiveStudio studio) { + if (player == null || studio == null) { + return false; + } + UUID ownerId = studio.generator().getRequest().ownerId(); + return ownerMatches(ownerId, player.getUniqueId()); + } + + static boolean canCreateVariants(JigsawStudioLayout layout) { + return Objects.requireNonNull(layout, "Jigsaw Studio layout") + .variantCatalog() + .editableGraph(); + } + + static String variantCreationSourceFailure( + JigsawStudioVariant activeVariant, + boolean duplicateActive + ) { + if (activeVariant == null) { + return duplicateActive + ? "This workcell has no active variant to duplicate." + : "This workcell has no active variant whose pool role can be copied. Use " + + "/iris jigsaw piece create to choose an owned pool explicitly."; + } + if (!activeVariant.owned()) { + return "Adopt or clone this graph before creating from its read-only variant."; + } + if (activeVariant.memberships().isEmpty()) { + return "The active variant has no owned pool membership to copy. Use " + + "/iris jigsaw piece create to choose an owned pool explicitly."; + } + return ""; + } + + static boolean ownerMatches(UUID ownerId, UUID actorId) { + return ownerId == null || ownerId.equals(actorId); + } + + private static boolean authorizeOwner(Player player, ActiveStudio studio) { + if (ownerMatches(player, studio)) { + return true; + } + message(player, "This Jigsaw Studio is owned by another player session."); + return false; + } + + private boolean isUnauthorizedStudioEdit(Player player, Block block) { + if (player == null || block == null) { + return false; + } + ActiveStudio studio = studios.get(block.getWorld().getUID()); + if (studio == null) { + return false; + } + UUID requestId = studio.generator().getRequest().requestId(); + if (previewRenderer.contains(requestId, block.getX(), block.getY(), block.getZ())) { + message(player, "The seed-1337 Jigsaw preview is read-only and refreshes automatically."); + return true; + } + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioBay bay = session.layout().findAt(block.getX(), block.getY(), block.getZ()); + JigsawStudioVariant variant = bay == null + ? null + : session.activeVariant(bay.stableId()).orElse(null); + if (bay != null && (variant == null || !variant.owned())) { + message(player, variant == null + ? "This empty Jigsaw Studio workcell has no loaded editable variant. Load one before editing it." + : "This Jigsaw Studio variant is read-only. Adopt or clone its graph before editing it."); + return true; + } + if (reopenRequiredRequests.contains(requestId)) { + message(player, "Close and reopen Jigsaw Studio before editing the resized layout."); + return true; + } + if (materializationInProgress(studio)) { + message(player, "Wait for the current Jigsaw Studio variant load or rollback to finish."); + return true; + } + if (!blocksStudioEdit(studio.generator().getRequest().ownerId(), player.getUniqueId())) { + return false; + } + message(player, "This Jigsaw Studio is owned by another player session."); + return true; + } + + private boolean materializationInProgress(ActiveStudio studio) { + if (studio == null) { + return false; + } + UUID requestId = studio.generator().getRequest().requestId(); + synchronized (saveLifecycleLock) { + return materializationsInProgress.contains(requestId); + } + } + + private boolean anyMaterializationInProgress() { + synchronized (saveLifecycleLock) { + return !materializationsInProgress.isEmpty(); + } + } + + private boolean anyNonEditableWorkcell() { + for (ActiveStudio studio : studios.values()) { + if (hasNonEditableWorkcell(studio)) { + return true; + } + } + return false; + } + + private static boolean hasNonEditableWorkcell(ActiveStudio studio) { + JigsawStudioSession session = studio.generator().getSession(); + for (JigsawStudioBay bay : session.layout().bays()) { + JigsawStudioVariant variant = session.activeVariant(bay.stableId()).orElse(null); + if (variant == null || !variant.owned()) { + return true; + } + } + return false; + } + + static boolean blocksStudioEdit(UUID ownerId, UUID actorId) { + return !ownerMatches(ownerId, actorId); + } + + static boolean blocksStudioInventoryMutation( + boolean studioBlockInventory, + boolean immutable, + boolean materializing, + boolean reopenRequired, + UUID ownerId, + UUID actorId + ) { + return studioBlockInventory + && (immutable + || materializing + || reopenRequired + || actorId != null && blocksStudioEdit(ownerId, actorId)); + } + + private boolean movesProtectedBlocks(List blocks, BlockFace direction) { + for (Block block : blocks) { + if (isImmutableStudioBlock(block) + || isImmutableStudioBlock(block.getRelative(direction)) + || isImmutableStudioBlock(block.getRelative(direction.getOppositeFace()))) { + return true; + } + } + return false; + } + + private boolean containsImmutableStudioBlock(List states) { + for (BlockState state : states) { + if (isImmutableStudioBlock(state.getBlock())) { + return true; + } + } + return false; + } + + private boolean isImmutableStudioBlock(Block block) { + StudioBlockMutationContext context = studioBlockMutationContext(block); + return context != null && context.immutable(); + } + + private boolean isPreviewBlock(Block block) { + if (block == null) { + return false; + } + ActiveStudio studio = studios.get(block.getWorld().getUID()); + if (studio == null) { + return false; + } + UUID requestId = studio.generator().getRequest().requestId(); + return previewRenderer.contains(requestId, block.getX(), block.getY(), block.getZ()); + } + + private boolean isControlChest(Block block) { + if (block == null || block.getType() != Material.CHEST) { + return false; + } + ActiveStudio studio = studios.get(block.getWorld().getUID()); + if (studio == null) { + return false; + } + JigsawStudioControlPosition control = studio.generator().getLayout().controlPosition(); + return block.getX() == control.worldX() + && block.getY() == control.worldY() + && block.getZ() == control.worldZ(); + } + + private static JigsawStudioBoardState boardState( + ActiveStudio studio, + JigsawStudioSession session, + JigsawStudioBay workcell, + JigsawStudioVariant variant + ) { + if (workcell == null) { + return JigsawStudioBoardState.SAVED; + } + JigsawStudioSession.WorkcellSnapshot snapshot = session.workcellSnapshot(workcell.stableId()); + if (snapshot.switchInProgress()) { + return JigsawStudioBoardState.LOADING; + } + if (snapshot.saveInProgress()) { + return JigsawStudioBoardState.SAVING; + } + if (!workcell.enabled()) { + return JigsawStudioBoardState.DISABLED; + } + if (variant != null && !variant.owned()) { + return JigsawStudioBoardState.READ_ONLY; + } + BayReadiness readiness = studio.population(workcell).readiness(); + if (!readiness.failure().isEmpty()) { + return JigsawStudioBoardState.INVALID; + } + return snapshot.dirty() ? JigsawStudioBoardState.UNSAVED : JigsawStudioBoardState.SAVED; + } + + private static String displayKey(String resourceKey) { + int separator = resourceKey.lastIndexOf('/'); + return separator < 0 ? resourceKey : resourceKey.substring(separator + 1); + } + + private static boolean sameBlockPosition(Location first, Location second) { + if (first == null || second == null || first.getWorld() == null || second.getWorld() == null) { + return false; + } + return first.getWorld().getUID().equals(second.getWorld().getUID()) + && first.getBlockX() == second.getBlockX() + && first.getBlockY() == second.getBlockY() + && first.getBlockZ() == second.getBlockZ(); + } + + private void ensureVisualizationLoop(Player player) { + if (player == null) { + return; + } + if (!J.isOwnedByCurrentRegion(player)) { + J.runEntity(player, () -> ensureVisualizationLoop(player)); + return; + } + UUID playerId = player.getUniqueId(); + if (!enabled || particlesDisabled.contains(playerId) + || !studios.containsKey(player.getWorld().getUID()) + || !visualizationLoops.add(playerId)) { + return; + } + visualizationTick(player); + } + + private void visualizationTick(Player player) { + UUID playerId = player.getUniqueId(); + if (!enabled || particlesDisabled.contains(playerId)) { + visualizationLoops.remove(playerId); + return; + } + ActiveStudio studio = studios.get(player.getWorld().getUID()); + if (studio == null) { + visualizationLoops.remove(playerId); + return; + } + try { + renderVisualization(player, studio); + } catch (Throwable exception) { + visualizationLoops.remove(playerId); + IrisLogging.reportError(exception); + return; + } + boolean scheduled = J.runEntity( + player, + () -> visualizationTick(player), + VISUAL_INTERVAL_TICKS, + () -> visualizationLoops.remove(playerId)); + if (!scheduled) { + visualizationLoops.remove(playerId); + } + } + + private void renderVisualization(Player player, ActiveStudio studio) { + JigsawStudioSession session = studio.generator().getSession(); + JigsawStudioLayout layout = session.layout(); + Location playerLocation = player.getLocation(); + JigsawStudioBay focused = layout.findAt( + playerLocation.getBlockX(), + playerLocation.getBlockY(), + playerLocation.getBlockZ()); + ParticleBudget budget = new ParticleBudget(PARTICLE_BUDGET); + if (focused != null) { + Color focusedColor = bayValid(studio, focused) ? SELECTED_COLOR : INVALID_BAY_COLOR; + drawConnectors(player, playerLocation, studio, focused, budget); + drawBounds(player, playerLocation, focused.bounds(), focusedColor, 1.15F, 1.0D, budget); + } + drawLivePreview(player, playerLocation, studio, budget); + drawAssemblyPreview(player, playerLocation, budget); + for (JigsawStudioBay bay : layout.bays()) { + if (bay == focused || !isNearby(playerLocation, bay.bounds())) { + continue; + } + Color nearbyColor = bayValid(studio, bay) ? NEARBY_COLOR : INVALID_BAY_COLOR; + drawBounds(player, playerLocation, bay.bounds(), nearbyColor, 0.55F, 4.0D, budget); + if (budget.empty()) { + return; + } + } + } + + private void drawLivePreview( + Player player, + Location playerLocation, + ActiveStudio studio, + ParticleBudget budget + ) { + UUID requestId = studio.generator().getRequest().requestId(); + JigsawStudioGraphEvaluation evaluation = evaluations.get(requestId); + if (evaluation == null || evaluation.previewBounds().isEmpty()) { + return; + } + JigsawStudioPreviewRenderer.PreviewBounds preview = evaluation.previewBounds(); + JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions( + preview.maximumX() - preview.minimumX() + 1, + preview.maximumY() - preview.minimumY() + 1, + preview.maximumZ() - preview.minimumZ() + 1); + Color color = switch (evaluation.state()) { + case VALID -> ASSEMBLY_PREVIEW_COLOR; + case PENDING, WARNING, STALE -> LIVE_PREVIEW_WARNING_COLOR; + case INVALID -> INVALID_BAY_COLOR; + }; + drawBounds( + player, + playerLocation, + new JigsawStudioBounds( + preview.minimumX(), + preview.minimumY(), + preview.minimumZ(), + dimensions), + color, + 0.85F, + 2.0D, + budget); + } + + private void drawAssemblyPreview( + Player player, + Location playerLocation, + ParticleBudget budget + ) { + AssemblyPreview preview = assemblyPreviews.get(player.getUniqueId()); + if (preview == null) { + return; + } + if (!preview.worldId().equals(player.getWorld().getUID()) + || preview.expiresAtMillis() < System.currentTimeMillis()) { + assemblyPreviews.remove(player.getUniqueId(), preview); + return; + } + for (JigsawStudioBounds bounds : preview.bounds()) { + drawBounds(player, playerLocation, bounds, ASSEMBLY_PREVIEW_COLOR, 0.85F, 2.0D, budget); + if (budget.empty()) { + return; + } + } + } + + private static void drawConnectors( + Player player, + Location playerLocation, + ActiveStudio studio, + JigsawStudioBay bay, + ParticleBudget budget + ) { + JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(bay); + if (!rendered.valid()) { + return; + } + for (JigsawStudioGenerator.RenderedConnector renderedConnector : rendered.connectors()) { + IrisJigsawConnector connector = renderedConnector.connector(); + IrisDirection direction = connector.getDirection(); + drawConnectorLine( + player, + playerLocation, + bay.bounds().originX() + renderedConnector.x() + 0.5D, + bay.bounds().originY() + renderedConnector.y() + 0.5D, + bay.bounds().originZ() + renderedConnector.z() + 0.5D, + direction, + connectorColor(connector), + budget); + if (budget.empty()) { + return; + } + } + } + + private static void drawConnectorLine( + Player player, + Location playerLocation, + double startX, + double startY, + double startZ, + IrisDirection direction, + Color color, + ParticleBudget budget + ) { + drawLine( + player, + playerLocation, + startX, + startY, + startZ, + startX + direction.x() * 1.75D, + startY + direction.y() * 1.75D, + startZ + direction.z() * 1.75D, + color, + 1.05F, + 0.35D, + budget); + } + + private static boolean bayValid(ActiveStudio studio, JigsawStudioBay bay) { + JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(bay); + return rendered.valid() && studio.population(bay).readiness().failure().isEmpty(); + } + + private static Color connectorColor(IrisJigsawConnector connector) { + if (connector.getPool() == null || connector.getPool().isBlank() + || connector.getName() == null || connector.getName().isBlank() + || connector.getTargetName() == null || connector.getTargetName().isBlank()) { + return INVALID_CONNECTOR_COLOR; + } + String channel = connector.getChannel(); + if (channel == null || channel.isBlank()) { + return VALID_CONNECTOR_COLOR; + } + int hash = channel.toLowerCase(Locale.ROOT).hashCode(); + int red = 80 + (hash & 127); + int green = 80 + ((hash >>> 8) & 127); + int blue = 80 + ((hash >>> 16) & 127); + return Color.fromRGB(red, green, blue); + } + + private static void drawBounds( + Player player, + Location playerLocation, + JigsawStudioBounds bounds, + Color color, + float size, + double step, + ParticleBudget budget + ) { + double minX = bounds.originX(); + double minY = bounds.originY(); + double minZ = bounds.originZ(); + double maxX = bounds.maxX() + 1.0D; + double maxY = bounds.maxY() + 1.0D; + double maxZ = bounds.maxZ() + 1.0D; + drawLine(player, playerLocation, minX, minY, minZ, maxX, minY, minZ, color, size, step, budget); + drawLine(player, playerLocation, minX, minY, maxZ, maxX, minY, maxZ, color, size, step, budget); + drawLine(player, playerLocation, minX, maxY, minZ, maxX, maxY, minZ, color, size, step, budget); + drawLine(player, playerLocation, minX, maxY, maxZ, maxX, maxY, maxZ, color, size, step, budget); + drawLine(player, playerLocation, minX, minY, minZ, minX, maxY, minZ, color, size, step, budget); + drawLine(player, playerLocation, maxX, minY, minZ, maxX, maxY, minZ, color, size, step, budget); + drawLine(player, playerLocation, minX, minY, maxZ, minX, maxY, maxZ, color, size, step, budget); + drawLine(player, playerLocation, maxX, minY, maxZ, maxX, maxY, maxZ, color, size, step, budget); + drawLine(player, playerLocation, minX, minY, minZ, minX, minY, maxZ, color, size, step, budget); + drawLine(player, playerLocation, maxX, minY, minZ, maxX, minY, maxZ, color, size, step, budget); + drawLine(player, playerLocation, minX, maxY, minZ, minX, maxY, maxZ, color, size, step, budget); + drawLine(player, playerLocation, maxX, maxY, minZ, maxX, maxY, maxZ, color, size, step, budget); + } + + private static void drawLine( + Player player, + Location playerLocation, + double startX, + double startY, + double startZ, + double endX, + double endY, + double endZ, + Color color, + float size, + double step, + ParticleBudget budget + ) { + if (budget.empty()) { + return; + } + double deltaX = endX - startX; + double deltaY = endY - startY; + double deltaZ = endZ - startZ; + double length = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ); + int samples = Math.max(1, (int) Math.ceil(length / step)); + Particle.DustOptions dust = new Particle.DustOptions(color, size); + for (int index = 0; index <= samples; index++) { + double progress = (double) index / samples; + double x = startX + deltaX * progress; + double y = startY + deltaY * progress; + double z = startZ + deltaZ * progress; + double distanceX = playerLocation.getX() - x; + double distanceY = playerLocation.getY() - y; + double distanceZ = playerLocation.getZ() - z; + if (distanceX * distanceX + distanceY * distanceY + distanceZ * distanceZ > VISUAL_RANGE_SQUARED) { + continue; + } + if (!budget.consume()) { + return; + } + player.spawnParticle(Particle.DUST, x, y, z, 1, 0.0D, 0.0D, 0.0D, 0.0D, dust); + } + } + + private static boolean isNearby(Location location, JigsawStudioBounds bounds) { + double centerX = bounds.originX() + bounds.dimensions().width() / 2.0D; + double centerY = bounds.originY() + bounds.dimensions().height() / 2.0D; + double centerZ = bounds.originZ() + bounds.dimensions().depth() / 2.0D; + double deltaX = location.getX() - centerX; + double deltaY = location.getY() - centerY; + double deltaZ = location.getZ() - centerZ; + return deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ <= VISUAL_RANGE_SQUARED; + } + + private static JigsawStudioBay findBay(JigsawStudioLayout layout, String requested) { + String key = requested.trim(); + JigsawStudioBay exact = layout.get(key); + if (exact != null) { + return exact; + } + if (key.regionMatches(true, 0, "topology/", 0, "topology/".length())) { + String maskText = key.substring("topology/".length()); + try { + int mask = Integer.parseInt(maskText, 16); + JigsawPlanarArchetype archetype = JigsawPlanarArchetype.fromTopology( + JigsawPlanarTopology.fromMask(mask)); + return layout.get(archetype.stableId()); + } catch (IllegalArgumentException ignored) { + return null; + } + } + for (JigsawStudioBay bay : layout.bays()) { + if (bay.stableId().equalsIgnoreCase(key) + || bay.archetype().map(Enum::name).filter(name -> name.equalsIgnoreCase(key)).isPresent()) { + return bay; + } + } + for (JigsawStudioVariant variant : layout.variantCatalog().variants()) { + if (!variant.pieceKey().equalsIgnoreCase(key)) { + continue; + } + return layout.workcellForVariant(variant.pieceKey()).orElse(null); + } + return null; + } + + static String writeFailure(StructureWriteResult result) { + if (!result.conflicts().isEmpty()) { + StructureWriteResult.Conflict conflict = result.conflicts().getFirst(); + return "Jigsaw Studio ownership conflict at '" + conflict.relativePath() + "': " + + conflict.reason().name().toLowerCase(Locale.ROOT) + + ". No authored files were changed."; + } + String detail = result.failure().map(JigsawStudioService::failureMessage) + .orElse(result.status().name().toLowerCase(Locale.ROOT)); + return "Jigsaw Studio atomic save failed: " + detail; + } + + private static String failureMessage(Throwable exception) { + String message = exception.getMessage(); + return message == null || message.isBlank() ? exception.getClass().getSimpleName() : message; + } + + private static void message(Player player, String text) { + if (player != null) { + J.runEntity(player, () -> player.sendMessage("[Iris Jigsaw Studio] " + text)); + } + } + + static void playSaveSound(Player player) { + if (player != null) { + J.runEntity(player, () -> player.playSound( + player.getLocation(), + "minecraft:block.note_block.bell", + 0.65F, + 1.65F)); + } + } + + private static void report(Player player, boolean enabled, String text) { + if (enabled) { + message(player, text); + } + } + + private record StudioBlockMutationContext( + ActiveStudio studio, + UUID requestId, + UUID ownerId, + boolean immutable + ) { + private StudioBlockMutationContext { + Objects.requireNonNull(studio, "Jigsaw Studio block mutation studio"); + Objects.requireNonNull(requestId, "Jigsaw Studio block mutation request ID"); + } + } + + private record ActiveStudio( + UUID worldId, + World world, + Engine engine, + JigsawStudioGenerator generator, + ConcurrentHashMap populations, + Set hydrationsInProgress, + AtomicLong evaluationGeneration + ) { + ActiveStudio { + Objects.requireNonNull(worldId, "Jigsaw Studio world ID"); + Objects.requireNonNull(world, "Jigsaw Studio world"); + Objects.requireNonNull(engine, "Jigsaw Studio engine"); + Objects.requireNonNull(generator, "Jigsaw Studio generator"); + Objects.requireNonNull(populations, "Jigsaw Studio bay populations"); + Objects.requireNonNull(hydrationsInProgress, "Jigsaw Studio hydrations"); + Objects.requireNonNull(evaluationGeneration, "Jigsaw Studio evaluation generation"); + } + + BayPopulation population(JigsawStudioBay bay) { + return populations.computeIfAbsent(bay.stableId(), key -> { + JigsawStudioGenerator.RenderedBay rendered = generator.renderBay(bay); + return new BayPopulation( + requiredChunks(bay, rendered), + rendered.valid() ? "" : rendered.failure()); + }); + } + + BayPopulation replacePopulation( + JigsawStudioBay bay, + JigsawStudioGenerator.RenderedBay rendered, + String failure, + boolean ready + ) { + BayPopulation replacement = new BayPopulation( + requiredChunks(bay, rendered), + failure); + if (ready) { + replacement.markFullyReady(); + } + populations.put(bay.stableId(), replacement); + return replacement; + } + } + + private final class MaterializationCoordinator { + private final MaterializationWork work; + private final List areas; + private final Set candidateCompleted = new HashSet<>(); + private final Set candidateTouched = new HashSet<>(); + private final Set rollbackCompleted = new HashSet<>(); + private int candidateRemaining; + private int rollbackRemaining; + private String candidateFailure = ""; + private String rollbackFailure = ""; + private boolean rollbackStarted; + private boolean finished; + private boolean leaseReleased; + private boolean scheduledAny; + + private MaterializationCoordinator( + MaterializationWork work, + List areas + ) { + this.work = Objects.requireNonNull(work, "Jigsaw Studio materialization work"); + this.areas = List.copyOf(areas); + candidateRemaining = this.areas.size(); + } + + private MaterializationWork work() { + return work; + } + + private synchronized void markScheduled() { + scheduledAny = true; + } + + private synchronized boolean scheduledAny() { + return scheduledAny; + } + + private synchronized void markCandidateTouched(ChunkCaptureArea area) { + candidateTouched.add(chunkKey(area.chunkX(), area.chunkZ())); + } + + private void candidateComplete( + ChunkCaptureArea area, + String failure, + Throwable exception + ) { + boolean finishSuccess = false; + boolean startRollback = false; + synchronized (this) { + if (finished || rollbackStarted + || !candidateCompleted.add(chunkKey(area.chunkX(), area.chunkZ()))) { + return; + } + if (failure != null && !failure.isBlank() && candidateFailure.isEmpty()) { + candidateFailure = failure; + } + candidateRemaining--; + if (candidateRemaining == 0) { + if (candidateFailure.isEmpty()) { + finishSuccess = true; + } else { + rollbackStarted = true; + startRollback = true; + } + } + } + if (exception != null) { + IrisLogging.reportError(exception); + } + if (finishSuccess) { + finishSuccess(); + } else if (startRollback) { + scheduleRollback(); + } + } + + private void finishSuccess() { + JigsawStudioSession session = work.studio().generator().getSession(); + if (!isCurrentVariantSwitch(work) || !session.completeVariantSwitch(work.token())) { + beginLateRollback("Jigsaw Studio changed before the loaded variant could be activated."); + return; + } + if (work.connectorVisibilityChange()) { + session.setConnectorsVisible(work.workcell().stableId(), work.targetConnectorsVisible()); + } + synchronized (this) { + if (finished) { + return; + } + finished = true; + } + try { + work.studio().generator().invalidateRender(work.workcell().stableId()); + work.studio().replacePopulation(work.workcell(), work.target(), "", true); + refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId()); + message(work.player(), work.connectorVisibilityChange() + ? "Connector blocks are now " + + (work.targetConnectorsVisible() ? "visible." : "hidden.") + : "Loaded variant '" + work.token().targetVariant().pieceKey() + + "' into " + work.workcell().stableId() + "."); + } finally { + releaseLease(); + } + } + + private void beginLateRollback(String failure) { + synchronized (this) { + if (finished || rollbackStarted) { + return; + } + candidateFailure = failure; + rollbackStarted = true; + } + scheduleRollback(); + } + + private void scheduleRollback() { + List rollbackAreas = new ArrayList<>(); + synchronized (this) { + for (ChunkCaptureArea area : areas) { + if (candidateTouched.contains(chunkKey(area.chunkX(), area.chunkZ()))) { + rollbackAreas.add(area); + } + } + rollbackRemaining = rollbackAreas.size(); + if (rollbackRemaining == 0) { + finished = true; + } + } + if (rollbackAreas.isEmpty()) { + finishRollback(); + return; + } + for (ChunkCaptureArea area : rollbackAreas) { + boolean scheduled = J.runRegion( + work.world(), + area.chunkX(), + area.chunkZ(), + () -> materializeRollbackChunk(this, area)); + if (!scheduled) { + rollbackComplete( + area, + "Iris could not schedule rollback for workcell chunk " + + area.chunkX() + "," + area.chunkZ() + ".", + null); + } + } + } + + private void rollbackComplete( + ChunkCaptureArea area, + String failure, + Throwable exception + ) { + boolean finalizeRollback = false; + synchronized (this) { + if (finished || !rollbackStarted + || !rollbackCompleted.add(chunkKey(area.chunkX(), area.chunkZ()))) { + return; + } + if (failure != null && !failure.isBlank() && rollbackFailure.isEmpty()) { + rollbackFailure = failure; + } + rollbackRemaining--; + if (rollbackRemaining == 0) { + finished = true; + finalizeRollback = true; + } + } + if (exception != null) { + IrisLogging.reportError(exception); + } + if (finalizeRollback) { + finishRollback(); + } + } + + private void finishRollback() { + JigsawStudioSession session = work.studio().generator().getSession(); + boolean released = session.abortVariantSwitch(work.token()); + try { + work.studio().generator().invalidateRender(work.workcell().stableId()); + if (rollbackFailure.isEmpty() && released) { + work.studio().replacePopulation(work.workcell(), work.previous(), "", true); + refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId()); + message(work.player(), candidateFailure + " The previous variant was restored."); + return; + } + String failure = rollbackFailure.isEmpty() + ? "The previous variant could not be restored because the Studio session changed." + : rollbackFailure; + work.studio().replacePopulation(work.workcell(), work.previous(), failure, false); + refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId()); + message(work.player(), candidateFailure + " Automatic rollback also failed: " + failure + + " Do not save this workcell until it is reopened."); + } finally { + releaseLease(); + } + } + + private void releaseLease() { + synchronized (this) { + if (leaseReleased) { + return; + } + leaseReleased = true; + } + finishMaterialization(work.studio().generator().getRequest().requestId()); + } + } + + private final class CaptureCoordinator { + private final CaptureWork work; + private final List areas; + private final Map snapshots = new ConcurrentHashMap<>(); + private final AtomicInteger remaining; + private final AtomicBoolean failed = new AtomicBoolean(false); + private final AtomicBoolean assemblyStarted = new AtomicBoolean(false); + private final AtomicBoolean operationFinished = new AtomicBoolean(false); + private volatile AutosavePersistentFailure persistentFailure; + + private CaptureCoordinator(CaptureWork work, List areas) { + this.work = Objects.requireNonNull(work, "Jigsaw Studio capture work"); + this.areas = List.copyOf(areas); + this.remaining = new AtomicInteger(this.areas.size()); + } + + private ActiveStudio studio() { + return work.studio(); + } + + private World world() { + return work.world(); + } + + private JigsawStudioBay bay() { + return work.bay(); + } + + private CaptureTarget captureTarget() { + return work.captureTarget(); + } + + private Player player() { + return work.player(); + } + + private UUID requestId() { + return work.requestId(); + } + + private JigsawStudioSession.SaveIdentity saveIdentity() { + return work.saveIdentity(); + } + + private List areas() { + return areas; + } + + private boolean stopped() { + return failed.get(); + } + + private boolean failed() { + return failed.get(); + } + + private void accept(ChunkSnapshot snapshot) { + if (failed.get() || assemblyStarted.get()) { + return; + } + ChunkCaptureArea area = snapshot.area(); + long key = chunkKey(area.chunkX(), area.chunkZ()); + if (snapshots.putIfAbsent(key, snapshot) != null) { + fail("Jigsaw Studio received duplicate snapshot for bay chunk " + + area.chunkX() + "," + area.chunkZ() + ".", null); + return; + } + int incomplete = remaining.decrementAndGet(); + if (incomplete < 0) { + fail("Jigsaw Studio received more bay chunk snapshots than expected.", null); + return; + } + if (incomplete == 0 && assemblyStarted.compareAndSet(false, true)) { + List completed = List.copyOf(snapshots.values()); + J.a(() -> assembleAndPersist(this, completed)); + } + } + + private void fail(String failure, Throwable exception) { + if (!failed.compareAndSet(false, true)) { + return; + } + if (exception != null) { + IrisLogging.reportError(exception); + } + complete(); + message(player(), failure); + } + + private void failPersistent(String failure, Throwable exception) { + if (!failed.compareAndSet(false, true)) { + return; + } + recordPersistentFailure(failure, exception); + complete(); + message(player(), failure); + } + + private void recordPersistentFailure(String failure, Throwable exception) { + persistentFailure = new AutosavePersistentFailure(failure, exception); + } + + private void complete() { + if (operationFinished.compareAndSet(false, true)) { + studio().generator().getSession().abortSave(saveIdentity()); + finishSave(requestId()); + completeAutosaveAttempt( + studio(), + saveIdentity(), + work.autosaveFailureState(), + persistentFailure); + refreshWorkcellContext(studio().worldId(), saveIdentity().workcellId()); + } + } + } + + private record CaptureWork( + ActiveStudio studio, + World world, + JigsawStudioBay bay, + CaptureTarget captureTarget, + Player player, + UUID requestId, + JigsawStudioSession.SaveIdentity saveIdentity, + AutosaveFailureState autosaveFailureState + ) { + CaptureWork { + Objects.requireNonNull(studio, "Jigsaw Studio capture studio"); + Objects.requireNonNull(world, "Jigsaw Studio capture world"); + Objects.requireNonNull(bay, "Jigsaw Studio capture bay"); + Objects.requireNonNull(captureTarget, "Jigsaw Studio capture target"); + Objects.requireNonNull(requestId, "Jigsaw Studio capture request ID"); + Objects.requireNonNull(saveIdentity, "Jigsaw Studio capture save identity"); + Objects.requireNonNull(autosaveFailureState, "Jigsaw Studio capture autosave failure state"); + } + } + + private record EvaluationComputation( + JigsawStudioGraphEvaluation evaluation, + JigsawStudioPreviewRenderer.PreviewPlan plan + ) { + private EvaluationComputation { + Objects.requireNonNull(evaluation, "Jigsaw Studio graph evaluation"); + Objects.requireNonNull(plan, "Jigsaw Studio evaluation preview plan"); + } + } + + private record MaterializationWork( + ActiveStudio studio, + World world, + Player player, + JigsawStudioBay workcell, + JigsawStudioSession.VariantSwitchToken token, + JigsawStudioGenerator.RenderedBay previous, + JigsawStudioGenerator.RenderedBay target, + boolean previousConnectorsVisible, + boolean targetConnectorsVisible, + boolean connectorVisibilityChange + ) { + MaterializationWork { + Objects.requireNonNull(studio, "Jigsaw Studio materialization studio"); + Objects.requireNonNull(world, "Jigsaw Studio materialization world"); + Objects.requireNonNull(player, "Jigsaw Studio materialization player"); + Objects.requireNonNull(workcell, "Jigsaw Studio materialization workcell"); + Objects.requireNonNull(token, "Jigsaw Studio materialization token"); + Objects.requireNonNull(previous, "Jigsaw Studio previous rendered variant"); + Objects.requireNonNull(target, "Jigsaw Studio target rendered variant"); + } + } + + @FunctionalInterface + public interface CommandGraphMutation { + CommandGraphMutationResult run() throws IOException; + } + + public record CommandGraphMutationResult( + JigsawStudioLayout layout, + String activateWorkcellId, + String activatePieceKey, + Map rebindActiveVariants, + Optional reload, + String message + ) { + public CommandGraphMutationResult { + Objects.requireNonNull(layout, "Jigsaw Studio graph-mutation layout"); + activateWorkcellId = activateWorkcellId == null ? "" : activateWorkcellId; + activatePieceKey = activatePieceKey == null ? "" : activatePieceKey; + rebindActiveVariants = Map.copyOf(Objects.requireNonNull( + rebindActiveVariants, + "Jigsaw Studio graph-mutation active variant bindings")); + reload = Objects.requireNonNull(reload, "Jigsaw Studio graph-mutation reload"); + message = message == null ? "Graph update completed." : message; + if (activatePieceKey.isEmpty() != activateWorkcellId.isEmpty()) { + throw new IllegalArgumentException( + "Jigsaw Studio graph activation requires both a workcell and piece key"); + } + int followUpActions = (activatePieceKey.isEmpty() ? 0 : 1) + + (rebindActiveVariants.isEmpty() ? 0 : 1) + + (reload.isEmpty() ? 0 : 1); + if (followUpActions > 1) { + throw new IllegalArgumentException( + "Jigsaw Studio graph mutations may activate, rebind, or reload, but not combine them"); + } + } + + public CommandGraphMutationResult( + JigsawStudioLayout layout, + String activateWorkcellId, + String activatePieceKey, + String message + ) { + this(layout, activateWorkcellId, activatePieceKey, Map.of(), Optional.empty(), message); + } + } + + public record VariantReloadRequest( + String workcellId, + JigsawStudioGenerator.RenderedBay previous + ) { + public VariantReloadRequest { + workcellId = workcellId == null ? "" : workcellId.trim(); + if (workcellId.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio reload workcell ID cannot be blank"); + } + Objects.requireNonNull(previous, "Jigsaw Studio previous rendered variant"); + } + } + + static final class BayPopulation { + private final Set requiredChunks; + private final Set generatedChunks = ConcurrentHashMap.newKeySet(); + private final Set appliedChunks = ConcurrentHashMap.newKeySet(); + private final Set hydratedChunks = ConcurrentHashMap.newKeySet(); + private volatile String failure; + + BayPopulation(Set requiredChunks, String failure) { + this.requiredChunks = Set.copyOf(requiredChunks); + this.failure = failure == null ? "" : failure; + } + + boolean markGenerated(long chunk) { + if (!requiredChunks.contains(chunk)) { + return false; + } + generatedChunks.add(chunk); + return !hydratedChunks.contains(chunk) && failure.isEmpty(); + } + + boolean needsApplication(long chunk) { + return failure.isEmpty() + && requiredChunks.contains(chunk) + && generatedChunks.contains(chunk) + && !appliedChunks.contains(chunk); + } + + boolean needsVerification(long chunk) { + return failure.isEmpty() + && requiredChunks.contains(chunk) + && generatedChunks.contains(chunk) + && appliedChunks.contains(chunk) + && !hydratedChunks.contains(chunk); + } + + void markApplied(long chunk) { + if (requiredChunks.contains(chunk)) { + appliedChunks.add(chunk); + } + } + + void markHydrated(long chunk) { + if (requiredChunks.contains(chunk)) { + hydratedChunks.add(chunk); + } + } + + void markFullyReady() { + generatedChunks.addAll(requiredChunks); + appliedChunks.addAll(requiredChunks); + hydratedChunks.addAll(requiredChunks); + } + + synchronized boolean fail(String reason) { + if (!failure.isEmpty()) { + return false; + } + failure = reason == null || reason.isBlank() ? "unknown hydration failure" : reason; + return true; + } + + BayReadiness readiness() { + boolean ready = failure.isEmpty() + && !requiredChunks.isEmpty() + && generatedChunks.containsAll(requiredChunks) + && hydratedChunks.containsAll(requiredChunks); + return new BayReadiness( + ready, + failure, + requiredChunks.size(), + generatedChunks.size(), + hydratedChunks.size()); + } + } + + record BayReadiness( + boolean ready, + String failure, + int requiredChunks, + int generatedChunks, + int hydratedChunks + ) { + } + + enum SaveStart { + STARTED, + IN_PROGRESS, + CLOSING, + GRAPH_OPERATION, + EXPORT_OPERATION, + VARIANT_OPERATION + } + + public enum ExportStart { + STARTED, + NOT_ACTIVE, + NOT_OWNER, + DIRTY, + CLOSING, + SAVE_IN_PROGRESS, + OPERATION_IN_PROGRESS, + IN_PROGRESS + } + + private enum SaveAttempt { + STARTED, + RETRY, + DEFERRED, + PERSISTENT_FAILURE + } + + enum DeferredDuplicationReadiness { + READY, + WAITING_FOR_AUTOSAVE, + WAITING_FOR_OPERATION, + STALE + } + + private enum DeferredDuplicationKind { + SINGLE, + FAMILY + } + + public enum CloseStart { + STARTED, + NOT_ACTIVE, + NOT_OWNER, + DIRTY, + SAVE_IN_PROGRESS, + OPERATION_IN_PROGRESS + } + + private record CaptureTarget( + JigsawStudioBounds bounds, + IrisJigsawPiece piece, + IrisObject object, + int displayRotationQuarterTurns, + boolean connectorsVisible + ) { + CaptureTarget { + Objects.requireNonNull(bounds, "Jigsaw Studio capture bounds"); + Objects.requireNonNull(piece, "Jigsaw Studio capture piece"); + Objects.requireNonNull(object, "Jigsaw Studio capture object"); + displayRotationQuarterTurns = Math.floorMod(displayRotationQuarterTurns, 4); + } + } + + private record AutosaveKey(UUID requestId, String workcellId) { + private AutosaveKey { + Objects.requireNonNull(requestId, "Jigsaw Studio autosave request ID"); + Objects.requireNonNull(workcellId, "Jigsaw Studio autosave workcell ID"); + } + } + + private record JigsawTileWatchKey( + UUID requestId, + UUID worldId, + int worldX, + int worldY, + int worldZ + ) { + private JigsawTileWatchKey { + Objects.requireNonNull(requestId, "Jigsaw Studio tile watch request ID"); + Objects.requireNonNull(worldId, "Jigsaw Studio tile watch world ID"); + } + } + + record JigsawTilePollDecision(boolean changed, boolean continueWatching) { + } + + private record JigsawTileWatch( + JigsawTileWatchKey key, + ActiveStudio studio, + UUID playerId, + String workcellId, + KMap snapshot, + AtomicBoolean scheduleFailureLogged, + AtomicBoolean reconciliationScheduled + ) { + private JigsawTileWatch { + Objects.requireNonNull(key, "Jigsaw Studio tile watch key"); + Objects.requireNonNull(studio, "Jigsaw Studio tile watch studio"); + Objects.requireNonNull(playerId, "Jigsaw Studio tile watch player ID"); + Objects.requireNonNull(workcellId, "Jigsaw Studio tile watch workcell ID"); + Objects.requireNonNull(snapshot, "Jigsaw Studio tile watch snapshot"); + Objects.requireNonNull(scheduleFailureLogged, "Jigsaw Studio tile watch schedule warning state"); + Objects.requireNonNull(reconciliationScheduled, "Jigsaw Studio tile watch retry schedule state"); + } + } + + static final class AutosaveFailureState { + private final AtomicInteger persistentFailures = new AtomicInteger(); + private final AtomicBoolean failureLogged = new AtomicBoolean(false); + + AutosaveFailureDecision recordPersistentFailure() { + int failureCount = persistentFailures.incrementAndGet(); + return new AutosaveFailureDecision( + persistentAutosaveRetryTicks(failureCount), + failureLogged.compareAndSet(false, true)); + } + } + + record AutosaveFailureDecision(int retryTicks, boolean logFailure) { + } + + private record AutosavePersistentFailure(String detail, Throwable cause) { + private AutosavePersistentFailure { + Objects.requireNonNull(detail, "Jigsaw Studio persistent autosave failure detail"); + } + } + + private record AutosaveTicket( + AutosaveKey key, + ActiveStudio studio, + JigsawStudioSession.DirtyIdentity identity, + AtomicBoolean scheduled, + AtomicBoolean scheduleFailureLogged, + AutosaveFailureState failureState + ) { + private AutosaveTicket( + AutosaveKey key, + ActiveStudio studio, + JigsawStudioSession.DirtyIdentity identity, + AtomicBoolean scheduled, + AtomicBoolean scheduleFailureLogged + ) { + this( + key, + studio, + identity, + scheduled, + scheduleFailureLogged, + new AutosaveFailureState()); + } + + private AutosaveTicket { + Objects.requireNonNull(key, "Jigsaw Studio autosave key"); + Objects.requireNonNull(studio, "Jigsaw Studio autosave studio"); + Objects.requireNonNull(identity, "Jigsaw Studio autosave dirty identity"); + Objects.requireNonNull(scheduled, "Jigsaw Studio autosave schedule state"); + Objects.requireNonNull(scheduleFailureLogged, "Jigsaw Studio autosave schedule warning state"); + Objects.requireNonNull(failureState, "Jigsaw Studio autosave failure state"); + } + } + + private record DeferredDuplication( + UUID requestId, + UUID sessionId, + UUID playerId, + UUID worldId, + DeferredDuplicationKind kind, + String workcellId, + Map sourcePieces, + String themeKey, + AtomicBoolean scheduled + ) { + private DeferredDuplication { + Objects.requireNonNull(requestId, "Jigsaw Studio deferred duplicate request ID"); + Objects.requireNonNull(sessionId, "Jigsaw Studio deferred duplicate session ID"); + Objects.requireNonNull(playerId, "Jigsaw Studio deferred duplicate player ID"); + Objects.requireNonNull(worldId, "Jigsaw Studio deferred duplicate world ID"); + Objects.requireNonNull(kind, "Jigsaw Studio deferred duplicate kind"); + workcellId = workcellId == null ? "" : workcellId; + sourcePieces = Map.copyOf(Objects.requireNonNull( + sourcePieces, + "Jigsaw Studio deferred duplicate source pieces")); + themeKey = themeKey == null ? "" : themeKey; + Objects.requireNonNull(scheduled, "Jigsaw Studio deferred duplicate schedule state"); + if (sourcePieces.isEmpty()) { + throw new IllegalArgumentException("A deferred duplicate requires at least one source variant"); + } + if (kind == DeferredDuplicationKind.SINGLE && workcellId.isBlank()) { + throw new IllegalArgumentException("A deferred single duplicate requires a workcell"); + } + if (kind == DeferredDuplicationKind.FAMILY && themeKey.isBlank()) { + throw new IllegalArgumentException("A deferred family duplicate requires a theme key"); + } + } + + private static DeferredDuplication single( + UUID requestId, + UUID sessionId, + Player player, + UUID worldId, + String workcellId, + String sourcePiece + ) { + return new DeferredDuplication( + requestId, + sessionId, + player.getUniqueId(), + worldId, + DeferredDuplicationKind.SINGLE, + workcellId, + Map.of(workcellId, sourcePiece), + "", + new AtomicBoolean()); + } + + private static DeferredDuplication family( + UUID requestId, + UUID sessionId, + Player player, + UUID worldId, + Map sourcePieces, + String themeKey + ) { + return new DeferredDuplication( + requestId, + sessionId, + player.getUniqueId(), + worldId, + DeferredDuplicationKind.FAMILY, + "", + sourcePieces, + themeKey, + new AtomicBoolean()); + } + + private boolean sameIntent(DeferredDuplication other) { + return other != null + && playerId.equals(other.playerId) + && kind == other.kind + && workcellId.equals(other.workcellId) + && sourcePieces.equals(other.sourcePieces) + && themeKey.equals(other.themeKey); + } + } + + private record ToolConfirmation(JigsawStudioToolPayload payload, long expiresAtNanos) { + private ToolConfirmation { + Objects.requireNonNull(payload, "Jigsaw Studio tool confirmation payload"); + } + } + + private record AssemblyPreview( + UUID worldId, + long expiresAtMillis, + List bounds + ) { + AssemblyPreview { + Objects.requireNonNull(worldId, "Jigsaw Studio preview world"); + bounds = List.copyOf(bounds); + } + } + + private record PlayerWorkcellContext( + UUID worldId, + UUID requestId, + String workcellId + ) { + PlayerWorkcellContext { + Objects.requireNonNull(worldId, "Jigsaw Studio player-context world ID"); + Objects.requireNonNull(requestId, "Jigsaw Studio player-context request ID"); + workcellId = workcellId == null ? "" : workcellId; + } + } + + record ChunkCaptureArea( + int chunkX, + int chunkZ, + int minimumX, + int maximumX, + int minimumZ, + int maximumZ + ) { + ChunkCaptureArea { + if (minimumX < 0 || minimumZ < 0 || maximumX <= minimumX || maximumZ <= minimumZ) { + throw new IllegalArgumentException("Jigsaw Studio chunk capture intersection is invalid"); + } + } + + boolean contains(int x, int z) { + return x >= minimumX && x < maximumX && z >= minimumZ && z < maximumZ; + } + } + + record CapturedBlock( + int x, + int y, + int z, + PlatformBlockState state, + TileData tileData + ) { + CapturedBlock { + Objects.requireNonNull(state, "Jigsaw Studio captured block state"); + tileData = tileData == null ? null : tileData.clone(); + } + + @Override + public TileData tileData() { + return tileData == null ? null : tileData.clone(); + } + } + + record CapturedConnector( + int x, + int y, + int z, + IrisDirection direction, + IrisDirection top, + String pool, + String name, + String targetName, + String channel, + JigsawJoint joint, + String finalState, + int selectionPriority, + int placementPriority + ) { + CapturedConnector { + Objects.requireNonNull(direction, "Jigsaw Studio captured connector direction"); + Objects.requireNonNull(top, "Jigsaw Studio captured connector top"); + Objects.requireNonNull(pool, "Jigsaw Studio captured connector pool"); + Objects.requireNonNull(name, "Jigsaw Studio captured connector name"); + Objects.requireNonNull(targetName, "Jigsaw Studio captured connector target"); + channel = channel == null ? "" : channel; + Objects.requireNonNull(joint, "Jigsaw Studio captured connector joint"); + Objects.requireNonNull(finalState, "Jigsaw Studio captured connector final state"); + } + + static CapturedConnector from(IrisJigsawConnector connector) { + IrisJigsawConnector source = Objects.requireNonNull(connector, "Jigsaw Studio captured connector"); + IrisPosition position = source.getPosition(); + return new CapturedConnector( + position.getX(), + position.getY(), + position.getZ(), + source.getDirection(), + source.getTop(), + source.getPool(), + source.getName(), + source.getTargetName(), + source.getChannel(), + source.getJoint(), + source.getFinalState(), + source.getSelectionPriority(), + source.getPlacementPriority()); + } + + IrisJigsawConnector toConnector() { + return new IrisJigsawConnector() + .setPosition(new IrisPosition(x, y, z)) + .setDirection(direction) + .setTop(top) + .setPool(pool) + .setName(name) + .setTargetName(targetName) + .setChannel(channel) + .setJoint(joint) + .setFinalState(finalState) + .setSelectionPriority(selectionPriority) + .setPlacementPriority(placementPriority); + } + } + + record ChunkSnapshot( + ChunkCaptureArea area, + List blocks, + List connectors + ) { + ChunkSnapshot { + Objects.requireNonNull(area, "Jigsaw Studio captured chunk area"); + blocks = List.copyOf(blocks); + connectors = List.copyOf(connectors); + } + } + + record LocalPosition(int x, int y, int z) { + } + + record Capture(byte[] objectContent, List connectors, boolean hasBlockEntities) { + Capture { + objectContent = Objects.requireNonNull(objectContent, "Jigsaw Studio object content").clone(); + connectors = List.copyOf(connectors); + } + + @Override + public byte[] objectContent() { + return objectContent.clone(); + } + } + + static final class WorkcellTopologyException extends IOException { + WorkcellTopologyException(String message) { + super(message); + } + } + + private static final class ParticleBudget { + private int remaining; + + private ParticleBudget(int remaining) { + this.remaining = remaining; + } + + private boolean consume() { + if (remaining < 1) { + return false; + } + remaining--; + return true; + } + + private boolean empty() { + return remaining < 1; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioToolCodec.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioToolCodec.java new file mode 100644 index 000000000..48a208d84 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioToolCodec.java @@ -0,0 +1,150 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolAction; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +public final class JigsawStudioToolCodec { + static final NamespacedKey SCHEMA_KEY = new NamespacedKey("iris", "jigsaw_tool_schema"); + static final NamespacedKey ACTION_KEY = new NamespacedKey("iris", "jigsaw_tool_action"); + static final NamespacedKey REQUEST_KEY = new NamespacedKey("iris", "jigsaw_tool_request"); + static final NamespacedKey WORKCELL_KEY = new NamespacedKey("iris", "jigsaw_tool_workcell"); + static final NamespacedKey PIECE_KEY = new NamespacedKey("iris", "jigsaw_tool_piece"); + static final NamespacedKey POOL_KEY = new NamespacedKey("iris", "jigsaw_tool_pool"); + static final NamespacedKey ENTRY_INDEX_KEY = new NamespacedKey("iris", "jigsaw_tool_entry_index"); + static final NamespacedKey AMOUNT_KEY = new NamespacedKey("iris", "jigsaw_tool_amount"); + + public ItemStack create(JigsawStudioToolPayload payload) { + ItemStack tool = new ItemStack(Material.STICK); + bind(tool, payload); + return tool; + } + + public void bind(ItemStack tool, JigsawStudioToolPayload payload) { + ItemStack item = Objects.requireNonNull(tool, "Jigsaw Studio tool item"); + JigsawStudioToolPayload binding = requireCurrentSchema(payload); + if (item.getType() != Material.STICK) { + throw new IllegalArgumentException("Jigsaw Studio tools must use a stick"); + } + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + throw new IllegalStateException("Jigsaw Studio tool stick has no item metadata"); + } + write(meta.getPersistentDataContainer(), binding); + meta.setDisplayName(ChatColor.AQUA + "Jigsaw Studio: " + binding.action().displayName()); + meta.setLore(lore(binding)); + if (!item.setItemMeta(meta)) { + throw new IllegalStateException("Jigsaw Studio tool metadata was rejected"); + } + } + + public Optional decode(ItemStack tool) { + if (tool == null || tool.getType() != Material.STICK || !tool.hasItemMeta()) { + return Optional.empty(); + } + ItemMeta meta = tool.getItemMeta(); + return meta == null ? Optional.empty() : decode(meta.getPersistentDataContainer()); + } + + public boolean isTool(ItemStack tool) { + return decode(tool).isPresent(); + } + + void write(PersistentDataContainer container, JigsawStudioToolPayload payload) { + PersistentDataContainer data = Objects.requireNonNull( + container, + "Jigsaw Studio tool persistent data"); + JigsawStudioToolPayload binding = requireCurrentSchema(payload); + data.set(SCHEMA_KEY, PersistentDataType.INTEGER, binding.schemaVersion()); + data.set(ACTION_KEY, PersistentDataType.STRING, binding.action().name()); + data.set(REQUEST_KEY, PersistentDataType.STRING, binding.requestId().toString()); + data.set(WORKCELL_KEY, PersistentDataType.STRING, binding.workcellId()); + data.set(PIECE_KEY, PersistentDataType.STRING, binding.pieceKey()); + data.set(POOL_KEY, PersistentDataType.STRING, binding.poolKey()); + data.set(ENTRY_INDEX_KEY, PersistentDataType.INTEGER, binding.entryIndex()); + data.set(AMOUNT_KEY, PersistentDataType.INTEGER, binding.amount()); + } + + Optional decode(PersistentDataContainer container) { + if (container == null) { + return Optional.empty(); + } + Integer schemaVersion = container.get(SCHEMA_KEY, PersistentDataType.INTEGER); + String actionName = container.get(ACTION_KEY, PersistentDataType.STRING); + String requestValue = container.get(REQUEST_KEY, PersistentDataType.STRING); + if (schemaVersion == null + || schemaVersion != JigsawStudioToolPayload.CURRENT_SCHEMA_VERSION + || actionName == null + || requestValue == null) { + return Optional.empty(); + } + try { + JigsawStudioToolAction action = JigsawStudioToolAction.valueOf(actionName); + UUID requestId = UUID.fromString(requestValue); + String workcellId = optionalString(container, WORKCELL_KEY); + String pieceKey = optionalString(container, PIECE_KEY); + String poolKey = optionalString(container, POOL_KEY); + Integer entryIndex = container.get(ENTRY_INDEX_KEY, PersistentDataType.INTEGER); + Integer amount = container.get(AMOUNT_KEY, PersistentDataType.INTEGER); + return Optional.of(new JigsawStudioToolPayload( + schemaVersion, + action, + requestId, + workcellId, + pieceKey, + poolKey, + entryIndex == null ? JigsawStudioToolPayload.NO_ENTRY_INDEX : entryIndex, + amount == null ? 0 : amount)); + } catch (IllegalArgumentException exception) { + return Optional.empty(); + } + } + + private static JigsawStudioToolPayload requireCurrentSchema(JigsawStudioToolPayload payload) { + JigsawStudioToolPayload binding = Objects.requireNonNull(payload, "Jigsaw Studio tool payload"); + if (binding.schemaVersion() != JigsawStudioToolPayload.CURRENT_SCHEMA_VERSION) { + throw new IllegalArgumentException("Unsupported Jigsaw Studio tool schema " + + binding.schemaVersion()); + } + return binding; + } + + private static String optionalString(PersistentDataContainer container, NamespacedKey key) { + String value = container.get(key, PersistentDataType.STRING); + return value == null ? "" : value; + } + + private static List lore(JigsawStudioToolPayload payload) { + List lore = new ArrayList<>(); + if (!payload.workcellId().isEmpty()) { + lore.add(ChatColor.GRAY + "Workcell: " + payload.workcellId()); + } + if (!payload.pieceKey().isEmpty()) { + lore.add(ChatColor.GRAY + "Variant: " + payload.pieceKey()); + } + if (!payload.poolKey().isEmpty()) { + lore.add(ChatColor.GRAY + "Pool: " + payload.poolKey() + + (payload.entryIndex() < 0 ? "" : " [" + payload.entryIndex() + "]")); + } + if (payload.amount() != 0) { + lore.add(ChatColor.GRAY + "Amount: " + payload.amount()); + } + lore.add(payload.action().destructive() + ? ChatColor.RED + "Right-click twice to confirm" + : ChatColor.YELLOW + "Right-click to use"); + return List.copyOf(lore); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/service/LogFilterSVC.java b/core/src/main/java/art/arcane/iris/core/service/LogFilterSVC.java similarity index 80% rename from core/src/main/java/com/volmit/iris/core/service/LogFilterSVC.java rename to core/src/main/java/art/arcane/iris/core/service/LogFilterSVC.java index c501d1e35..352da58f2 100644 --- a/core/src/main/java/com/volmit/iris/core/service/LogFilterSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/LogFilterSVC.java @@ -1,7 +1,6 @@ -package com.volmit.iris.core.service; +package art.arcane.iris.core.service; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.plugin.IrisService; +import art.arcane.iris.util.common.plugin.IrisService; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Marker; @@ -10,17 +9,26 @@ import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.Logger; import org.apache.logging.log4j.message.Message; +import java.util.List; + public class LogFilterSVC implements IrisService, Filter { private static final String HEIGHTMAP_MISMATCH = "Ignoring heightmap data for chunk"; private static final String RAID_PERSISTENCE = "Could not save data net.minecraft.world.entity.raid.PersistentRaid"; private static final String DUPLICATE_ENTITY_UUID = "UUID of added entity already exists"; - private static final KList FILTERS = new KList<>(); + // Immutable: check() runs on arbitrary logger threads, and the root logger is JVM-global, + // so a mutable static here was both a CME hazard and grew by three entries per enable. + private static final List FILTERS = List.of(HEIGHTMAP_MISMATCH, RAID_PERSISTENCE, DUPLICATE_ENTITY_UUID); + + private boolean installed = false; public void onEnable() { - FILTERS.add(HEIGHTMAP_MISMATCH, RAID_PERSISTENCE, DUPLICATE_ENTITY_UUID); + if (installed) { + return; + } ((Logger) LogManager.getRootLogger()).addFilter(this); + installed = true; } public void initialize() { @@ -33,6 +41,11 @@ public class LogFilterSVC implements IrisService, Filter { } public void onDisable() { + if (!installed) { + return; + } + ((Logger) LogManager.getRootLogger()).get().removeFilter(this); + installed = false; } public boolean isStarted() { @@ -116,8 +129,14 @@ public class LogFilterSVC implements IrisService, Filter { } private Result check(String string) { - if (FILTERS.stream().anyMatch(string::contains)) - return Result.DENY; + if (string == null) { + return Result.NEUTRAL; + } + for (String filter : FILTERS) { + if (string.contains(filter)) { + return Result.DENY; + } + } return Result.NEUTRAL; } } diff --git a/core/src/main/java/art/arcane/iris/core/service/MultiverseSVC.java b/core/src/main/java/art/arcane/iris/core/service/MultiverseSVC.java new file mode 100644 index 000000000..bb27f660d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/MultiverseSVC.java @@ -0,0 +1,109 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.core.link.MultiverseCoreLink; +import art.arcane.iris.core.link.MultiverseGuardListener; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.event.server.PluginEnableEvent; + +/** + * Brings Iris' Multiverse integration up exactly when Multiverse is usable and takes it back down when it + * is not. + *

+ * Nothing in this class names a Multiverse type. The guard listener does, and Bukkit resolves a listener's + * handler parameter types at registration, so it is only constructed once Multiverse is enabled. Iris + * declares {@code loadbefore: Multiverse-Core}, which means Multiverse normally enables after Iris and the + * plugin event is the live path; a reload or a plugin manager can invert that, so enablement is checked + * directly as well. + */ +public class MultiverseSVC implements IrisService { + private Listener guard; + + @Override + public void onEnable() { + activate(); + } + + @Override + public void onDisable() { + deactivate(); + } + + @EventHandler + public void onPluginEnable(PluginEnableEvent event) { + if (!MultiverseCoreLink.MULTIVERSE_PLUGIN.equals(event.getPlugin().getName())) { + return; + } + activate(); + } + + @EventHandler + public void onPluginDisable(PluginDisableEvent event) { + if (!MultiverseCoreLink.MULTIVERSE_PLUGIN.equals(event.getPlugin().getName())) { + return; + } + deactivate(); + } + + /** + * Registers the destructive-command guard and re-imposes Iris' intended Multiverse state on every + * world Iris owns. Multiverse imports the worlds it did not know about during its own enable, which is + * before this runs, so the reconciliation pass is the only thing that can correct those entries. + */ + private synchronized void activate() { + if (guard != null) { + return; + } + MultiverseCoreLink link = IrisServices.getOrNull(MultiverseCoreLink.class); + if (link == null || !link.isActive()) { + return; + } + try { + Listener listener = new MultiverseGuardListener(link); + BukkitPlatform.volmitPlugin().registerListener(listener); + guard = listener; + } catch (Throwable failure) { + // A Multiverse whose world events Iris cannot bind to is a Multiverse Iris must not touch. + IrisLogging.warn("Multiverse world events are not bindable (%s); Iris will not guard its" + + " destructive commands.", failure.getClass().getSimpleName()); + return; + } + link.reconcileOwnedWorlds(); + } + + private synchronized void deactivate() { + Listener listener = guard; + if (listener == null) { + return; + } + guard = null; + try { + BukkitPlatform.volmitPlugin().unregisterListener(listener); + } catch (Throwable ignored) { + // Shutdown ordering already unregisters every handler. + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/ObjectSVC.java b/core/src/main/java/art/arcane/iris/core/service/ObjectSVC.java new file mode 100644 index 000000000..e52870b87 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/ObjectSVC.java @@ -0,0 +1,97 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.scheduling.J; +import lombok.Getter; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; + +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.Iterator; +import java.util.Map; + +public class ObjectSVC implements IrisService { + + @Getter + // Concurrent + global-thread pipeline: pastes publish via J.runGlobal while the undo + // command arrives on an async pool thread; a plain ArrayDeque was mutated from both. + private final Deque> undos = new ConcurrentLinkedDeque<>(); + + + @Override + public void onEnable() { + + } + + @Override + public void onDisable() { + + } + + public void addChanges(Map oldBlocks) { + undos.add(oldBlocks); + } + + public void revertChanges(int amount) { + if (!J.runGlobal(() -> loopChange(amount))) { + loopChange(amount); + } + } + + private void loopChange(int amount) { + if (undos.size() > 0) { + revert(undos.pollLast()); + if (amount > 1) { + J.s(() -> loopChange(amount - 1), 2); + } + } + } + + /** + * Reverts all the block changes provided, 200 blocks per tick + * + * @param blocks The blocks to remove + */ + private void revert(Map blocks) { + if (blocks == null || blocks.isEmpty()) { + return; + } + + J.s(() -> { + Iterator> it = blocks.entrySet().iterator(); + int amount = 0; + while (it.hasNext()) { + Map.Entry entry = it.next(); + entry.getKey().setBlockData(entry.getValue(), false); + it.remove(); + + if (++amount >= 200) { + break; + } + } + + if (!blocks.isEmpty()) { + J.s(() -> revert(blocks), 1); + } + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java b/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java new file mode 100644 index 000000000..4d5b7fd4c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java @@ -0,0 +1,330 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.runtime.ObjectStudioActivation; +import art.arcane.iris.core.runtime.ObjectStudioLayout; +import art.arcane.iris.core.runtime.ObjectStudioLayout.GridCell; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.scheduling.J; +import io.papermc.lib.PaperLib; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.world.WorldUnloadEvent; +import org.bukkit.inventory.EquipmentSlot; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.volmlib.util.localization.MessageArgument; +public class ObjectStudioSaveService implements IrisService { + private static ObjectStudioSaveService INSTANCE; + + private final Map studios = new ConcurrentHashMap<>(); + + public static ObjectStudioSaveService get() { + ObjectStudioSaveService svc = INSTANCE; + if (svc != null) return svc; + svc = IrisServices.get(ObjectStudioSaveService.class); + return svc; + } + + @Override + public void onEnable() { + INSTANCE = this; + } + + @Override + public void onDisable() { + studios.clear(); + INSTANCE = null; + } + + public void register(Engine engine, ObjectStudioGenerator generator) { + World world = BukkitWorldBinding.world(engine.getTarget().getWorld()); + if (world == null) return; + ObjectStudioLayout layout = generator.getLayout(); + if (layout == null) return; + + Map sources = generator.getPackData(); + if (sources == null || sources.isEmpty()) { + IrisLogging.warn("Object Studio save disabled: no pack data sources available for world %s", world.getName()); + return; + } + + Map objectsDirs = new ConcurrentHashMap<>(); + for (Map.Entry e : sources.entrySet()) { + File dir = resolveObjectsDir(e.getValue()); + if (dir != null) { + objectsDirs.put(e.getKey(), dir); + } + } + if (objectsDirs.isEmpty()) { + IrisLogging.warn("Object Studio save disabled: no resolvable objects folders for world %s", world.getName()); + return; + } + + ActiveStudio existing = studios.get(world.getUID()); + if (existing != null && existing.layout == layout) { + return; + } + + String packKey = engine.getDimension() == null ? null : engine.getDimension().getLoadKey(); + studios.put(world.getUID(), new ActiveStudio(world.getUID(), layout, objectsDirs, packKey)); + IrisLogging.info("Object Studio live-save registered: world=%s cells=%d packs=%d", + world.getName(), layout.cells().size(), objectsDirs.size()); + } + + public void unregister(World world) { + if (world == null) return; + ActiveStudio removed = studios.remove(world.getUID()); + if (removed != null) { + if (removed.packKey != null) { + ObjectStudioActivation.deactivate(removed.packKey); + } + IrisLogging.info("Object Studio live-save unregistered: world=%s", world.getName()); + } + } + + @EventHandler + public void onWorldUnload(WorldUnloadEvent event) { + unregister(event.getWorld()); + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + if (event.getHand() != EquipmentSlot.HAND) return; + Action action = event.getAction(); + if (action != Action.RIGHT_CLICK_BLOCK && action != Action.LEFT_CLICK_BLOCK) return; + Block clicked = event.getClickedBlock(); + if (clicked == null) return; + + World world = clicked.getWorld(); + ActiveStudio studio = studios.get(world.getUID()); + if (studio == null) return; + + Player player = event.getPlayer(); + GridCell cell = findCellNear(studio, clicked.getX(), clicked.getZ()); + if (cell == null) { + player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CELL_UNDER_CLICK_X_Z, MessageArgument.untrusted("x", String.valueOf(clicked.getX())), MessageArgument.untrusted("z", String.valueOf(clicked.getZ())))); + return; + } + + player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVING_X_X, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())), MessageArgument.untrusted("w", String.valueOf(cell.w())), MessageArgument.untrusted("h", String.valueOf(cell.h())), MessageArgument.untrusted("d", String.valueOf(cell.d())))); + IrisLogging.info("Object Studio save triggered by %s for %s/%s", player.getName(), cell.pack(), cell.key()); + J.runRegion(world, cell.chunkMinX(), cell.chunkMinZ(), () -> { + try { + captureAndSave(studio, world, cell, player); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + }); + } + + private static GridCell findCellNear(ActiveStudio studio, int x, int z) { + GridCell inside = studio.layout.findAt(x, z); + if (inside != null) return inside; + int reach = Math.max(1, studio.layout.padding() + 1); + GridCell best = null; + int bestDist = Integer.MAX_VALUE; + for (GridCell cell : studio.layout.cells()) { + int dx = 0; + if (x < cell.originX()) dx = cell.originX() - x; + else if (x >= cell.originX() + cell.w()) dx = x - (cell.originX() + cell.w() - 1); + int dz = 0; + if (z < cell.originZ()) dz = cell.originZ() - z; + else if (z >= cell.originZ() + cell.d()) dz = z - (cell.originZ() + cell.d() - 1); + int dist = Math.max(dx, dz); + if (dist <= reach && dist < bestDist) { + bestDist = dist; + best = cell; + } + } + return best; + } + + public boolean teleportTo(Player player, String objectKey) { + if (player == null || objectKey == null) return false; + for (ActiveStudio studio : studios.values()) { + GridCell cell = studio.layout.get(objectKey); + if (cell == null) continue; + World world = Bukkit.getWorld(studio.worldId); + if (world == null) continue; + + double targetX = cell.originX() + cell.w() / 2.0D + 0.5D; + double targetZ = cell.originZ() + cell.d() / 2.0D + 0.5D; + double targetY = cell.originY() + cell.h() + 2.0D; + Location location = new Location(world, targetX, targetY, targetZ); + J.runEntity(player, () -> PaperLib.teleportAsync(player, location)); + IrisLogging.info("Object Studio goto: %s -> %s at %.0f,%.0f,%.0f", + player.getName(), objectKey, location.getX(), location.getY(), location.getZ()); + return true; + } + return false; + } + + private static File resolveObjectsDir(IrisData data) { + File root = data.getDataFolder(); + if (root == null) return null; + File objects = new File(root, "objects"); + if (!objects.exists()) { + objects.mkdirs(); + } + return objects; + } + + private void captureAndSave(ActiveStudio studio, World world, GridCell cell, Player notify) { + if (!allChunksLoaded(world, cell)) { + return; + } + + IrisObject snapshot = new IrisObject(cell.w(), cell.h(), cell.d()); + int originX = cell.originX(); + int originY = cell.originY(); + int originZ = cell.originZ(); + + boolean anyBlock = false; + for (int dx = 0; dx < cell.w(); dx++) { + for (int dy = 0; dy < cell.h(); dy++) { + for (int dz = 0; dz < cell.d(); dz++) { + Block block = world.getBlockAt(originX + dx, originY + dy, originZ + dz); + if (block.getType() == Material.AIR) continue; + snapshot.setUnsigned(dx, dy, dz, block, false); + anyBlock = true; + } + } + } + + String hashKey = cell.pack() + "/" + cell.key(); + long hash = hashOf(snapshot); + Long prior = studio.hashes.get(hashKey); + if (prior != null && prior == hash) { + if (notify != null) { + notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CHANGES, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())))); + } + return; + } + + if (!anyBlock && prior == null) { + studio.hashes.put(hashKey, hash); + if (notify != null) { + notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_EMPTY_CELL_NOTHING_WRITE, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())))); + } + return; + } + + studio.hashes.put(hashKey, hash); + + File targetFile = objectFileFor(studio, cell); + if (targetFile == null) { + if (notify != null) { + notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_TARGET_FILE, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())))); + } + return; + } + + J.a(() -> { + try { + File parent = targetFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + snapshot.write(targetFile); + IrisLogging.info("Object Studio saved: %s/%s (%dx%dx%d)", + cell.pack(), cell.key(), cell.w(), cell.h(), cell.d()); + if (notify != null) { + J.runEntity(notify, () -> notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVED, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key()))))); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + if (notify != null) { + J.runEntity(notify, () -> notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVE_FAILED, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())), MessageArgument.untrusted("error", String.valueOf(e.getMessage()))))); + } + } + }); + } + + private boolean allChunksLoaded(World world, GridCell cell) { + for (int cx = cell.chunkMinX(); cx <= cell.chunkMaxX(); cx++) { + for (int cz = cell.chunkMinZ(); cz <= cell.chunkMaxZ(); cz++) { + if (!world.isChunkLoaded(cx, cz)) { + return false; + } + } + } + return true; + } + + private static long hashOf(IrisObject snapshot) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + snapshot.write(baos); + byte[] bytes = baos.toByteArray(); + long h = 1125899906842597L; + for (byte b : bytes) { + h = 31 * h + b; + } + return h; + } catch (Throwable e) { + IrisLogging.reportError(e); + return System.nanoTime(); + } + } + + private static File objectFileFor(ActiveStudio studio, GridCell cell) { + File objectsDir = studio.objectsDirs.get(cell.pack()); + if (objectsDir == null) return null; + String relative = cell.key().replace('\\', '/'); + return new File(objectsDir, relative + ".iob"); + } + + private static final class ActiveStudio { + final UUID worldId; + final ObjectStudioLayout layout; + final Map objectsDirs; + final String packKey; + final Map hashes = new ConcurrentHashMap<>(); + + ActiveStudio(UUID worldId, ObjectStudioLayout layout, Map objectsDirs, String packKey) { + this.worldId = worldId; + this.layout = layout; + this.objectsDirs = objectsDirs; + this.packKey = packKey; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java b/core/src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java new file mode 100644 index 000000000..5963f08f4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java @@ -0,0 +1,537 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.PackDownloadMessages; +import art.arcane.iris.core.pack.PackDownloader; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.localization.MessageArgument; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.entity.Player; + +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; + +final class PackDownloadProgressReporter implements PackDownloader.DownloadProgressListener { + static final int PROGRESS_BAR_WIDTH = 24; + private static final int INDETERMINATE_SEGMENT_WIDTH = 5; + private static final int HUD_PULSE_TICKS = 5; + private static final int HUD_TERMINAL_TICKS = 60; + private static final long ACTION_INTERVAL_MILLIS = 250L; + private static final long CHAT_INTERVAL_MILLIS = 5_000L; + private static final int CHAT_PERCENT_STEP = 10; + private static final int MAX_DETAIL_CHARACTERS = 320; + private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]"); + private static final AtomicLong SESSION_IDS = new AtomicLong(); + + private final VolmitSender sender; + private final String source; + private final String sensitiveSource; + private final String escapedSensitiveSource; + private final Player player; + private final UUID playerId; + private final String hudLaneId; + private PackDownloader.DownloadPhase phase; + private PackDownloader.DownloadProgress latestProgress; + private boolean hudActive; + private long transferredBytes; + private long transferElapsedMillis; + private long phaseStartedMillis; + private long lastActionMillis; + private long lastChatMillis; + private int lastChatPercent; + private int pulseTaskId; + private boolean started; + private boolean finished; + private boolean listenerDisabled; + private boolean hudDisabled; + + PackDownloadProgressReporter(VolmitSender sender, String source) { + this(sender, source, null); + } + + PackDownloadProgressReporter(VolmitSender sender, String source, String sensitiveSource) { + this.sender = Objects.requireNonNull(sender, "sender"); + this.source = normalizeUntrusted(source); + this.sensitiveSource = normalizeSensitive(sensitiveSource); + escapedSensitiveSource = this.sensitiveSource == null + ? null + : escapeLocalizationUntrusted(this.sensitiveSource); + player = sender.isPlayer() ? sender.player() : null; + playerId = player == null ? null : player.getUniqueId(); + hudLaneId = "iris:pack-download-" + Long.toUnsignedString(SESSION_IDS.incrementAndGet()); + lastActionMillis = Long.MIN_VALUE; + lastChatMillis = Long.MIN_VALUE; + lastChatPercent = -CHAT_PERCENT_STEP; + pulseTaskId = -1; + } + + synchronized void start() { + if (started || finished) { + return; + } + started = true; + phaseStartedMillis = System.currentTimeMillis(); + deliverChat(IrisLanguage.text( + PackDownloadMessages.PROGRESS_START, + MessageArgument.untrusted("source", source) + )); + if (player == null || !BukkitPlatform.hasHud()) { + return; + } + hudActive = true; + int scheduledTaskId = J.ar(this::pulseHud, HUD_PULSE_TICKS); + pulseTaskId = scheduledTaskId; + if (finished || hudDisabled) { + J.car(scheduledTaskId); + pulseTaskId = -1; + } + } + + @Override + public synchronized void onProgress(PackDownloader.DownloadProgress progress) { + if (finished || listenerDisabled || progress == null) { + return; + } + try { + if (!started) { + start(); + } + latestProgress = progress; + if (progress.phase() == PackDownloader.DownloadPhase.DOWNLOADING) { + transferredBytes = Math.max(transferredBytes, Math.max(0L, progress.transferredBytes())); + transferElapsedMillis = Math.max(transferElapsedMillis, Math.max(0L, progress.elapsedMillis())); + } + long now = System.currentTimeMillis(); + if (phase != progress.phase()) { + phase = progress.phase(); + phaseStartedMillis = now; + deliverChat(IrisLanguage.text( + PackDownloadMessages.PROGRESS_PHASE, + MessageArgument.trusted("phase", phaseLabel(progress.phase())), + MessageArgument.untrusted("source", source) + )); + } + if (progress.phase() == PackDownloader.DownloadPhase.DOWNLOADING + && shouldSendChatProgress(progress, now)) { + lastChatMillis = now; + if (progress.totalBytes() > 0L) { + lastChatPercent = percent(progress.transferredBytes(), progress.totalBytes()); + } + deliverChat(progressLine(progress)); + } + } catch (RuntimeException failure) { + listenerDisabled = true; + disableHud(null); + throw failure; + } + } + + synchronized void detail(String detail) { + if (finished || listenerDisabled || detail == null || detail.isBlank()) { + return; + } + try { + String[] lines = detail.split("\\R"); + for (String line : lines) { + String normalized = normalizeUntrusted(redactSensitiveSource(line)); + if (normalized.isBlank()) { + continue; + } + deliverChat(IrisLanguage.text( + PackDownloadMessages.PROGRESS_DETAIL, + MessageArgument.untrusted("detail", normalized) + )); + } + } catch (RuntimeException failure) { + listenerDisabled = true; + disableHud(null); + throw failure; + } + } + + synchronized void succeed(PackDownloader.PackInstallResult result) { + if (finished) { + return; + } + finished = true; + stopPulse(); + String pack = result == null || result.key() == null || result.key().isBlank() + ? source + : normalizeUntrusted(result.key()); + if (result == null || !result.changed()) { + String unchanged = IrisLanguage.text( + PackDownloadMessages.PROGRESS_UNCHANGED, + MessageArgument.untrusted("pack", pack) + ); + deliverChat(unchanged); + deliverTerminalHud(unchanged, BarColor.YELLOW, 1.0D); + return; + } + + String complete = IrisLanguage.text( + PackDownloadMessages.PROGRESS_COMPLETE, + MessageArgument.untrusted("pack", pack), + MessageArgument.trusted("transferred", Form.fileSize(transferredBytes)), + MessageArgument.trusted("elapsed", Form.duration(transferElapsedMillis, 1)) + ); + deliverChat(complete); + deliverTerminalHud(complete, BarColor.GREEN, 1.0D); + if (result.restartRequired()) { + deliverChat(IrisLanguage.text(PackDownloadMessages.PROGRESS_RESTART)); + } + } + + synchronized void fail(Throwable failure) { + if (finished) { + return; + } + finished = true; + stopPulse(); + String detail = failure == null ? "" : normalizeUntrusted(redactSensitiveSource(failure.getMessage())); + String failed = detail.isBlank() + ? IrisLanguage.text(PackDownloadMessages.PROGRESS_FAILED) + : IrisLanguage.text( + PackDownloadMessages.PROGRESS_FAILED_DETAIL, + MessageArgument.untrusted("error", detail) + ); + deliverChat(failed); + deliverTerminalHud(failed, BarColor.RED, 1.0D); + } + + synchronized void cancel() { + if (finished) { + return; + } + finished = true; + stopPulse(); + String cancelled = IrisLanguage.text(PackDownloadMessages.PROGRESS_CANCELLED); + deliverChat(cancelled); + deliverTerminalHud(cancelled, BarColor.YELLOW, 1.0D); + } + + synchronized void executionComplete() { + if (!finished) { + cancel(); + } + } + + private void pulseHud() { + HudSnapshot snapshot; + synchronized (this) { + if (finished || hudDisabled || !hudActive) { + stopPulse(); + return; + } + long now = System.currentTimeMillis(); + if (!mayEmitAction(lastActionMillis, now)) { + return; + } + lastActionMillis = now; + snapshot = hudSnapshot(now); + } + boolean scheduled = J.runEntity(player, () -> renderHudPulse(snapshot)); + if (!scheduled) { + disableHud(null); + } + } + + private void renderHudPulse(HudSnapshot snapshot) { + try { + if (IrisSettings.get().getGeneral().isProgressBossBar()) { + BukkitPlatform.hudLanes().show( + player, + hudLaneId, + snapshot.line(), + snapshot.progress(), + BarColor.BLUE, + BarStyle.SEGMENTED_20, + 1_500L + ); + } + sender.sendAction(snapshot.line()); + } catch (RuntimeException failure) { + disableHud(failure); + } + } + + private synchronized HudSnapshot hudSnapshot(long now) { + PackDownloader.DownloadProgress observed = latestProgress; + PackDownloader.DownloadPhase currentPhase = observed == null ? phase : observed.phase(); + if (currentPhase == null) { + currentPhase = PackDownloader.DownloadPhase.CONNECTING; + } + long animationMillis = Math.max(0L, now - phaseStartedMillis); + PackDownloader.DownloadProgress displayed; + if (observed != null && currentPhase == PackDownloader.DownloadPhase.DOWNLOADING) { + displayed = observed; + } else { + displayed = new PackDownloader.DownloadProgress( + currentPhase, + transferredBytes, + -1L, + transferElapsedMillis, + false + ); + } + double progress = displayed.totalBytes() > 0L + ? Math.max(0.0D, Math.min(1.0D, (double) displayed.transferredBytes() / displayed.totalBytes())) + : indeterminateProgress(animationMillis); + return new HudSnapshot(progressLine(displayed, animationMillis), progress); + } + + private void deliverChat(String message) { + if (player == null) { + sender.sendMessage(message); + return; + } + J.runEntity(player, () -> sender.sendMessage(message)); + } + + private synchronized void deliverTerminalHud(String message, BarColor color, double progress) { + boolean active = hudActive; + hudActive = false; + if (player == null || !active || hudDisabled) { + return; + } + long now = System.currentTimeMillis(); + long elapsed = elapsedSince(lastActionMillis, now); + long delayMillis = Math.max(0L, ACTION_INTERVAL_MILLIS - Math.min(ACTION_INTERVAL_MILLIS, elapsed)); + int delayTicks = (int) Math.ceil(delayMillis / 50.0D); + lastActionMillis = now + delayMillis; + AtomicBoolean cleaned = new AtomicBoolean(); + Runnable cleanup = () -> releaseHudLane(cleaned); + Runnable retiredCleanup = () -> retireHudLane(cleaned); + Runnable display = () -> { + try { + if (IrisSettings.get().getGeneral().isProgressBossBar()) { + BukkitPlatform.hudLanes().show( + player, + hudLaneId, + message, + progress, + color, + BarStyle.SOLID, + 4_000L + ); + } + sender.sendAction(message); + } finally { + if (!J.runEntity(player, cleanup, HUD_TERMINAL_TICKS, retiredCleanup)) { + retiredCleanup.run(); + } + } + }; + boolean scheduled = J.runEntity(player, display, delayTicks, retiredCleanup); + if (!scheduled) { + hudDisabled = true; + retiredCleanup.run(); + } + } + + private void disableHud(Throwable failure) { + boolean active; + synchronized (this) { + if (hudDisabled) { + return; + } + hudDisabled = true; + stopPulse(); + active = hudActive; + hudActive = false; + } + if (failure != null) { + IrisLogging.reportError("Pack download HUD disabled after a delivery failure.", failure); + } + if (player != null && active) { + AtomicBoolean cleaned = new AtomicBoolean(); + Runnable cleanup = () -> releaseHudLane(cleaned); + Runnable retiredCleanup = () -> retireHudLane(cleaned); + if (!J.runEntity(player, cleanup, 0, retiredCleanup)) { + retiredCleanup.run(); + } + } + } + + private void releaseHudLane(AtomicBoolean cleaned) { + if (!cleaned.compareAndSet(false, true)) { + return; + } + BukkitPlatform.hudLanes().hide(player, hudLaneId); + } + + private void retireHudLane(AtomicBoolean cleaned) { + if (!cleaned.compareAndSet(false, true)) { + return; + } + BukkitPlatform.hudLanes().retire(playerId, hudLaneId); + } + + private synchronized void stopPulse() { + int activeTaskId = pulseTaskId; + pulseTaskId = -1; + if (activeTaskId >= 0) { + J.car(activeTaskId); + } + } + + private boolean shouldSendChatProgress(PackDownloader.DownloadProgress progress, long now) { + if (lastChatMillis == Long.MIN_VALUE) { + return true; + } + if (elapsedSince(lastChatMillis, now) >= CHAT_INTERVAL_MILLIS) { + return true; + } + if (progress.totalBytes() <= 0L) { + return false; + } + return percent(progress.transferredBytes(), progress.totalBytes()) >= lastChatPercent + CHAT_PERCENT_STEP; + } + + private String redactSensitiveSource(String value) { + if (value == null || sensitiveSource == null) { + return value; + } + return value.replace(sensitiveSource, source).replace(escapedSensitiveSource, source); + } + + static String progressLine(PackDownloader.DownloadProgress progress) { + return progressLine(progress, progress.elapsedMillis()); + } + + static String progressLine(PackDownloader.DownloadProgress progress, long animationMillis) { + long transferred = Math.max(0L, progress.transferredBytes()); + long rate = bytesPerSecond(transferred, progress.elapsedMillis()); + if (progress.totalBytes() > 0L) { + int currentPercent = percent(transferred, progress.totalBytes()); + return IrisLanguage.text( + PackDownloadMessages.PROGRESS_DETERMINATE, + MessageArgument.trusted("bar", determinateBar(currentPercent / 100.0D)), + MessageArgument.trusted("percent", currentPercent), + MessageArgument.trusted("transferred", Form.fileSize(transferred)), + MessageArgument.trusted("total", Form.fileSize(progress.totalBytes())), + MessageArgument.trusted("rate", Form.fileSize(rate)) + ); + } + return IrisLanguage.text( + PackDownloadMessages.PROGRESS_INDETERMINATE, + MessageArgument.trusted("bar", indeterminateBar(animationMillis)), + MessageArgument.trusted("phase", phaseLabel(progress.phase())), + MessageArgument.trusted("transferred", Form.fileSize(transferred)), + MessageArgument.trusted("rate", Form.fileSize(rate)) + ); + } + + static String determinateBar(double progress) { + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * PROGRESS_BAR_WIDTH); + StringBuilder bar = new StringBuilder(PROGRESS_BAR_WIDTH * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int cell = 0; cell < PROGRESS_BAR_WIDTH; cell++) { + bar.append(cell < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } + return bar.append(C.DARK_GRAY).append("]").toString(); + } + + static String indeterminateBar(long elapsedMillis) { + int travel = PROGRESS_BAR_WIDTH - INDETERMINATE_SEGMENT_WIDTH; + int cycle = travel * 2; + int step = cycle == 0 ? 0 : (int) ((Math.max(0L, elapsedMillis) / ACTION_INTERVAL_MILLIS) % cycle); + int start = step <= travel ? step : cycle - step; + StringBuilder bar = new StringBuilder(PROGRESS_BAR_WIDTH * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int cell = 0; cell < PROGRESS_BAR_WIDTH; cell++) { + boolean active = cell >= start && cell < start + INDETERMINATE_SEGMENT_WIDTH; + bar.append(active ? C.AQUA : C.DARK_GRAY).append("|"); + } + return bar.append(C.DARK_GRAY).append("]").toString(); + } + + static double indeterminateProgress(long elapsedMillis) { + int travel = PROGRESS_BAR_WIDTH - INDETERMINATE_SEGMENT_WIDTH; + int cycle = travel * 2; + int step = cycle == 0 ? 0 : (int) ((Math.max(0L, elapsedMillis) / ACTION_INTERVAL_MILLIS) % cycle); + int start = step <= travel ? step : cycle - step; + return Math.max(0.0D, Math.min(1.0D, + (start + INDETERMINATE_SEGMENT_WIDTH / 2.0D) / PROGRESS_BAR_WIDTH)); + } + + static int percent(long transferredBytes, long totalBytes) { + if (totalBytes <= 0L) { + return 0; + } + double fraction = Math.max(0.0D, Math.min(1.0D, (double) transferredBytes / totalBytes)); + return (int) Math.round(fraction * 100.0D); + } + + static long bytesPerSecond(long transferredBytes, long elapsedMillis) { + if (transferredBytes <= 0L || elapsedMillis <= 0L) { + return 0L; + } + double rate = transferredBytes * 1000.0D / elapsedMillis; + return rate >= Long.MAX_VALUE ? Long.MAX_VALUE : Math.round(rate); + } + + static boolean mayEmitAction(long lastEmissionMillis, long nowMillis) { + return elapsedSince(lastEmissionMillis, nowMillis) >= ACTION_INTERVAL_MILLIS; + } + + static String phaseLabel(PackDownloader.DownloadPhase phase) { + return IrisLanguage.text(switch (phase) { + case CONNECTING -> PackDownloadMessages.PROGRESS_PHASE_CONNECTING; + case DOWNLOADING -> PackDownloadMessages.PROGRESS_PHASE_DOWNLOADING; + case UNPACKING -> PackDownloadMessages.PROGRESS_PHASE_UNPACKING; + case VALIDATING -> PackDownloadMessages.PROGRESS_PHASE_VALIDATING; + case PUBLISHING -> PackDownloadMessages.PROGRESS_PHASE_PUBLISHING; + }); + } + + private static long elapsedSince(long earlier, long later) { + if (earlier == Long.MIN_VALUE || later < earlier) { + return Long.MAX_VALUE; + } + return later - earlier; + } + + private static String normalizeSensitive(String value) { + if (value == null || value.isBlank()) { + return null; + } + return value.trim(); + } + + private static String escapeLocalizationUntrusted(String value) { + return LEGACY_COLOR.matcher(value).replaceAll("") + .replace("&", "&") + .replace("<", "‹") + .replace(">", "›"); + } + + private static String normalizeUntrusted(String value) { + if (value == null || value.isBlank()) { + return "Pack"; + } + StringBuilder normalized = new StringBuilder(Math.min(value.length(), MAX_DETAIL_CHARACTERS)); + for (int index = 0; index < value.length() && normalized.length() < MAX_DETAIL_CHARACTERS; index++) { + char character = value.charAt(index); + if (character >= 0x20 && character != 0x7f) { + normalized.append(character); + } + } + if (value.length() > normalized.length()) { + normalized.append("..."); + } + return normalized.toString().trim(); + } + + private record HudSnapshot(String line, double progress) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java b/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java new file mode 100644 index 000000000..52ec73137 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java @@ -0,0 +1,151 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.volmlib.util.data.KCache; +import art.arcane.volmlib.util.format.Form; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.volmlib.util.scheduling.Looper; +import org.jetbrains.annotations.Unmodifiable; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class PreservationSVC implements IrisService, PreservationRegistry { + private final List threads = new CopyOnWriteArrayList<>(); + private final List services = new CopyOnWriteArrayList<>(); + private final List> caches = new CopyOnWriteArrayList<>(); + private Looper dereferencer; + + public void register(Thread t) { + threads.add(t); + } + + public void register(ExecutorService service) { + services.add(service); + } + + public void printCaches() { + List c = getCaches(); + long s = 0; + long m = 0; + double p = 0; + double mf = Math.max(c.size(), 1); + + for (MeteredCache i : c) { + s += i.getSize(); + m += i.getMaxSize(); + p += i.getUsage(); + } + + IrisLogging.info("Cached " + Form.f(s) + " / " + Form.f(m) + " (" + Form.pc(p / mf) + ") from " + caches.size() + " Caches"); + } + + public void dereference() { + IrisData.dereference(); + threads.removeIf((i) -> !i.isAlive()); + services.removeIf(ExecutorService::isShutdown); + updateCaches(); + } + + @Override + public void onEnable() { + /* + * Dereferences copies of Engine instances that are closed to prevent memory from + * hanging around and keeping copies of complex, caches and other dead objects. + */ + dereferencer = new Looper() { + @Override + protected long loop() { + dereference(); + return 60000; + } + }; + dereferencer.setName("Iris Preservation"); + dereferencer.setDaemon(true); + dereferencer.setPriority(Thread.MIN_PRIORITY); + dereferencer.start(); + } + + @Override + public void onDisable() { + if (dereferencer != null) { + dereferencer.interrupt(); + } + dereference(); + + postShutdown(() -> { + for (Thread i : threads) { + if (i.isAlive()) { + try { + i.interrupt(); + IrisLogging.info("Shutdown Thread " + i.getName()); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + + for (ExecutorService i : services) { + try { + i.shutdownNow(); + IrisLogging.info("Shutdown Executor Service " + i); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + }); + } + + public void updateCaches() { + caches.removeIf(ref -> { + MeteredCache c = ref.get(); + return c == null || c.isClosed(); + }); + } + + public void registerCache(MeteredCache cache) { + caches.add(new WeakReference<>(cache)); + } + + public List> caches() { + return cacheStream().map(MeteredCache::getRawCache).collect(Collectors.toList()); + } + + @Unmodifiable + public List getCaches() { + return cacheStream().toList(); + } + + private Stream cacheStream() { + return caches.stream() + .map(WeakReference::get) + .filter(Objects::nonNull) + .filter(cache -> !cache.isClosed()); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java new file mode 100644 index 000000000..90c42b1e7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java @@ -0,0 +1,1472 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisStartupValidation; +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.DatapackInstallResult; +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.AtomicDirectoryPublisher; +import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.core.pack.PackDownloadExecution; +import art.arcane.iris.core.pack.PackDownloader; +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.core.project.IrisProject; +import art.arcane.iris.core.project.IrisPackageCompiler; +import art.arcane.iris.core.project.IrisCodeWorkspace; +import art.arcane.iris.core.project.IrisProjectCopier; +import art.arcane.iris.core.runtime.StudioOpenCoordinator; +import art.arcane.iris.core.runtime.TransientWorldCleanupSupport; +import art.arcane.iris.core.runtime.WorldDeletionQueue; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.exceptions.IrisException; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.World; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.PackDownloadMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public class StudioSVC implements IrisService { + public static final String WORKSPACE_NAME = "packs"; + private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L; + private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+"); + private static final AtomicCache counter = new AtomicCache<>(); + private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue(); + private final Object downloadAdmissionMonitor = new Object(); + private volatile IrisProject activeProject; + private volatile CompletableFuture activeOpen; + private PackDownloadExecution activeDownload; + private boolean downloadAdmissionOpen; + + @Override + public void onEnable() { + synchronized (downloadAdmissionMonitor) { + activeDownload = null; + downloadAdmissionOpen = true; + } + String configuredPack = IrisSettings.get().getGenerator().getDefaultWorldType(); + if (!PackDownloader.isPackPresent(getWorkspaceFolder(), configuredPack)) { + IrisLogging.warn("Default pack '" + configuredPack + + "' is not installed. Install a built-in pack with /iris download pack=overworld or provide /iris download link=."); + } + } + + @Override + public void onDisable() { + quiesceDownloadsForShutdown(); + IrisLogging.debug("Studio Mode Active: Closing Projects"); + boolean stopping = IrisToolbelt.isServerStopping(); + LinkedHashSet worldNamesToDelete = new LinkedHashSet<>(TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())); + + if (activeProject != null) { + PlatformChunkGenerator activeProvider = activeProject.getActiveProvider(); + if (activeProvider != null) { + String activeWorldName = IrisWorldStorage.logicalName( + WorldIdentity.parse(activeProvider.getTarget().getWorld().identity())); + if (activeWorldName != null && !activeWorldName.isBlank()) { + worldNamesToDelete.add(activeWorldName); + } + } + } + + for (World i : Bukkit.getWorlds()) { + if (!IrisToolbelt.isIrisWorld(i) || !IrisToolbelt.isStudio(i)) { + continue; + } + + worldNamesToDelete.add(IrisWorldStorage.logicalName(i)); + PlatformChunkGenerator generator = IrisToolbelt.access(i); + if (!stopping) { + destroyStudioWorld(i, generator); + continue; + } + + if (generator != null) { + try { + generator.close(); + } catch (Throwable e) { + IrisLogging.reportError("Failed to close studio generator for \"" + i.getName() + "\" during shutdown.", e); + } + } + } + + activeProject = null; + + try { + art.arcane.iris.core.tools.IrisCreator.removeTransientStudioWorldsFromBukkitYml(); + } catch (Throwable e) { + IrisLogging.reportError("Failed to unregister transient studio worlds from bukkit.yml during shutdown.", e); + } + + queueStudioWorldDeletionOnStartup(worldNamesToDelete); + } + + public IrisDimension installIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) { + return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), false); + } + + public IrisDimension replaceIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) { + return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), true); + } + + public IrisDimension replaceIntoPackDirectory(VolmitSender sender, IrisDimension dimension, File folder) { + return installIntoDirectory(sender, dimension, folder, true); + } + + private IrisDimension installIntoDirectory( + VolmitSender sender, + IrisDimension dimension, + File folder, + boolean replaceExisting + ) { + if (J.isPrimaryThread()) { + sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD + )); + return null; + } + String dimensionKey = dimension.getLoadKey(); + Path source; + try { + source = resolveSafePackSource(dimension.getLoader().getDataFolder()); + } catch (IOException e) { + IrisLogging.reportError("Failed to inspect source dimension pack '" + dimensionKey + "'.", e); + sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_PACK_INSTALL_FAILED, + MessageArgument.untrusted("dimension", dimensionKey), + MessageArgument.untrusted("error", errorDetail(e)) + )); + return null; + } + Path target = folder.toPath().toAbsolutePath().normalize(); + Path parent = target.getParent(); + Path stage = null; + AtomicDirectoryPublisher.Publication publication = null; + PackValidationRegistry.RootMutation validationMutation = null; + IrisData previousData = IrisData.getLoaded(target.toFile()).orElse(null); + IrisData createdData = null; + boolean refreshedPreviousData = false; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_INSTALLING_PACKAGE, MessageArgument.untrusted("name", String.valueOf(source.getFileName())), MessageArgument.untrusted("loadKey", String.valueOf(dimensionKey)))); + try { + if (parent == null) { + throw new IOException("World pack target has no parent: " + target); + } + Files.createDirectories(parent); + requireSafePublicationTarget(target, replaceExisting); + + stage = Files.createTempDirectory(parent, ".pack.installing-"); + copyPackTree(source, stage); + IrisData stagedData = IrisData.openDatapackCompiler(stage.toFile()); + try { + IrisDimension stagedDimension = stagedData.getDimensionLoader().load(dimensionKey); + if (stagedDimension == null) { + throw new IOException("Copied pack does not contain a loadable dimension '" + dimensionKey + "'."); + } + } finally { + stagedData.close(); + } + validationMutation = PackValidationRegistry.beginRootMutation(target); + requireSafePublicationTarget(target, replaceExisting); + publication = AtomicDirectoryPublisher.publish(stage, target); + stage = null; + String copiedFingerprint = ServerConfigurator.computePackTreeFingerprint(target.toFile()); + PackValidationResult publishedValidation = + validatePublishedPack(target, source, copiedFingerprint, validationMutation); + if (!publishedValidation.isLoadable()) { + throw new BrokenPackException( + target.toString(), + publishedValidation.getBlockingErrors()); + } + + IrisData installedData; + // Live engines only ever attach to detached openRuntime loaders, never to the + // dataLoaders-cached previousData — so the old previousData.getEngines() test was + // always empty and a running world's pack could be swapped with no restart. + boolean activeRuntime = IrisData.hasActiveEngines(target.toFile()); + if (previousData == null) { + createdData = IrisData.get(target.toFile()); + installedData = createdData; + } else if (!activeRuntime) { + previousData.hotloaded(); + refreshedPreviousData = true; + installedData = previousData; + } else { + installedData = previousData; + } + IrisDimension installedDimension = activeRuntime + ? dimension + : installedData.getDimensionLoader().load(dimensionKey); + if (installedDimension == null) { + throw new IOException("Published pack does not contain a loadable dimension '" + dimensionKey + "'."); + } + publication.commit(); + validationMutation.commit(); + try { + publication.cleanupBackup(); + } catch (IOException cleanupFailure) { + IrisLogging.warn("World pack was committed but its backup could not be removed: " + + cleanupFailure.getMessage()); + } + if (activeRuntime) { + ServerConfigurator.restart("An active Iris world pack was replaced."); + } + return installedDimension; + } catch (Throwable e) { + rollbackFailedPublication(createdData, publication, e); + if (publication != null) { + invalidatePackValidation(target); + } + if (refreshedPreviousData) { + try { + previousData.hotloaded(); + } catch (Throwable restoreFailure) { + e.addSuppressed(restoreFailure); + } + } + IrisLogging.reportError("Failed to install dimension pack '" + dimensionKey + "' into " + folder.getPath(), e); + sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_PACK_INSTALL_FAILED, + MessageArgument.untrusted("dimension", dimensionKey), + MessageArgument.untrusted("error", errorDetail(e)) + )); + return null; + } finally { + if (validationMutation != null) { + validationMutation.close(); + } + if (stage != null) { + try { + AtomicDirectoryPublisher.deleteTree(stage); + } catch (IOException cleanupFailure) { + IrisLogging.reportError("Failed to clean staged world pack " + stage, cleanupFailure); + } + } + } + } + + static void invalidatePackValidation(Path packRoot) { + PackValidationRegistry.remove(packRoot); + } + + static void requireSafePublicationTarget(Path target, boolean replaceExisting) throws IOException { + if (Files.isSymbolicLink(target)) { + throw new IOException("World pack target is a symbolic link: " + target); + } + if (!replaceExisting && Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { + throw new FileAlreadyExistsException(target.toString()); + } + } + + static PackValidationResult validatePublishedPack(Path packRoot) { + try (PackValidationRegistry.RootMutation mutation = + PackValidationRegistry.beginRootMutation(packRoot)) { + PackValidationResult result = PackValidator.validate(packRoot.toFile()); + mutation.stage(result); + mutation.commit(); + return PackValidationRegistry.requireLoadable(packRoot); + } + } + + static PackValidationResult validatePublishedPack( + Path packRoot, + Path validatedSource, + String copiedContentFingerprint + ) { + try (PackValidationRegistry.RootMutation mutation = + PackValidationRegistry.beginRootMutation(packRoot)) { + PackValidationResult result = validatePublishedPack( + packRoot, + validatedSource, + copiedContentFingerprint, + mutation); + mutation.commit(); + return PackValidationRegistry.requireLoadable(packRoot); + } + } + + private static PackValidationResult validatePublishedPack( + Path packRoot, + Path validatedSource, + String copiedContentFingerprint, + PackValidationRegistry.RootMutation mutation + ) { + PackValidationResult result = mutation.stageMatchingCopy( + validatedSource, + copiedContentFingerprint); + if (result == null) { + result = PackValidator.validate(packRoot.toFile()); + mutation.stage(result); + } + return result; + } + + static void rollbackFailedPublication( + IrisData createdData, + AtomicDirectoryPublisher.Publication publication, + Throwable failure + ) { + if (createdData != null) { + try { + createdData.close(); + } catch (Throwable cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + } + if (publication != null) { + try { + publication.close(); + } catch (IOException cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + } + } + + static Path resolveSafePackSource(File sourceFolder) throws IOException { + Path source = sourceFolder.toPath().toAbsolutePath().normalize().toRealPath(); + if (!Files.isDirectory(source, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(source)) { + throw new IOException("Source pack is missing or unsafe: " + sourceFolder); + } + return source; + } + + public IrisDimension installInto(VolmitSender sender, String type, File folder) { + if (J.isPrimaryThread()) { + sender.sendMessage("Iris refused to download or copy a pack on the Bukkit primary thread."); + return null; + } + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_LOOKING_PACKAGE, MessageArgument.untrusted("type", String.valueOf(type)))); + IrisDimension dimension = IrisData.loadAnyDimension(type, null); + if (dimension == null) { + sender.sendMessage("Iris cannot repair world pack '" + type + + "' because no installed source contains it."); + return null; + } + return replaceIntoPackDirectory(sender, dimension, folder); + } + + public void downloadBuiltIn(VolmitSender sender, String key) { + if (!PackDownloader.isBuiltInPack(key)) { + sender.sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_BUILT_IN)); + return; + } + PackDownloadProgressReporter reporter = new PackDownloadProgressReporter(sender, key); + runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, key, reporter, cancellation -> { + PackDownloader.PackInstallResult result = downloadBuiltInLocked(key, cancellation, reporter); + if (result == null) { + reporter.fail(null); + return; + } + reporter.succeed(result); + }, "Failed to download built-in Iris pack '" + key + "'."); + } + + public void downloadUrl(VolmitSender sender, String url) { + if (!PackDownloader.isDirectZipUrl(url)) { + sender.sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_URL)); + return; + } + PackDownloadProgressReporter reporter = new PackDownloadProgressReporter( + sender, + IrisLanguage.text(PackDownloadMessages.PROGRESS_SOURCE_REMOTE), + url + ); + runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, "remote-zip", reporter, cancellation -> { + PackDownloader.PackInstallResult result = PackDownloader.downloadUrl( + getWorkspaceFolder(), + url, + false, + reporter::detail, + cancellation, + reporter + ); + if (result == null) { + reporter.fail(null); + return; + } + reporter.succeed(result); + }, "Failed to download Iris pack."); + } + + private PackDownloader.PackInstallResult downloadBuiltInLocked( + String expectedKey, + PackDownloader.DownloadCancellation cancellation, + PackDownloadProgressReporter reporter + ) throws IOException { + if (PackDownloader.isBuiltInPackPresent(getWorkspaceFolder(), expectedKey)) { + return new PackDownloader.PackInstallResult(expectedKey, false, false); + } + + return PackDownloader.downloadBuiltIn( + getWorkspaceFolder(), + expectedKey, + false, + reporter::detail, + cancellation, + reporter + ); + } + + public boolean isProjectOpen() { + return activeProject != null && activeProject.isOpen(); + } + + public void open(VolmitSender sender, String dimm) { + open(sender, 1337, dimm); + } + + public void open(VolmitSender sender, long seed, String dimm) { + try { + open(sender, seed, dimm, (w) -> { + }); + } catch (Exception e) { + IrisLogging.reportError("Failed to open studio world \"" + dimm + "\".", e); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_OPEN_STUDIO_WORLD, MessageArgument.untrusted("error", String.valueOf(e.getMessage())))); + } + } + + private static BrokenPackException reportPackAdmissionFailure( + VolmitSender sender, String dimm) { + Optional startupDenial = IrisStartupValidation.denialReason(); + IrisDimension dimension = IrisToolbelt.getDimension(dimm); + String packName = dimension == null || dimension.getLoader() == null + ? dimm + : dimension.getLoader().getDataFolder().getName(); + PackValidationResult validation = PackValidationRegistry.get(packName); + BrokenPackException failure = resolvePackAdmissionFailure( + packName, startupDenial, validation); + if (failure == null) { + return null; + } + if (startupDenial.isPresent()) { + sender.sendMessage(startupDenial.get()); + return failure; + } + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_CANNOT_OPEN_STUDIO_PACK_HAS_BLOCKING_ERRORS, MessageArgument.untrusted("dimm", String.valueOf(dimm)))); + for (String reason : failure.getReasons()) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_MESSAGE, MessageArgument.untrusted("reason", String.valueOf(reason)))); + } + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("dimm", String.valueOf(packName)))); + return failure; + } + + static BrokenPackException resolvePackAdmissionFailure( + String packName, + Optional startupDenial, + PackValidationResult validation + ) { + if (startupDenial.isPresent()) { + return new BrokenPackException(packName, List.of(startupDenial.get())); + } + if (validation != null && validation.isLoadable()) { + return null; + } + List failures = validation == null + ? List.of("Required pack validation has not completed. Studio creation fails closed until validation succeeds.") + : validation.getBlockingErrors(); + return new BrokenPackException(packName, failures); + } + + public void open(VolmitSender sender, long seed, String dimm, Consumer onDone) throws IrisException { + if (reportPackAdmissionFailure(sender, dimm) != null) { + return; + } + studioTransitions.submit(() -> replaceActiveProject(sender, seed, dimm, onDone)) + .whenComplete((ignored, throwable) -> { + if (throwable == null) { + return; + } + IrisLogging.reportError("Failed to replace the active studio project with \"" + dimm + "\".", throwable); + J.s(() -> sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_OPEN_STUDIO_WORLD_2, + MessageArgument.untrusted("error", String.valueOf(errorDetail(throwable)))))); + }); + } + + public CompletableFuture openTracked( + VolmitSender sender, + long seed, + String dimension, + StudioOpenCoordinator.StudioOpenKind openKind, + Runnable beforeOpen, + Consumer onDone + ) { + BrokenPackException failure = reportPackAdmissionFailure(sender, dimension); + if (failure != null) { + return CompletableFuture.failedFuture(failure); + } + return studioTransitions.submit(() -> replaceActiveProjectTracked( + sender, + seed, + dimension, + Objects.requireNonNull(openKind, "Studio open kind"), + Objects.requireNonNull(beforeOpen, "Studio before-open callback"), + Objects.requireNonNull(onDone, "Studio open completion callback"))); + } + + private CompletableFuture replaceActiveProjectTracked( + VolmitSender sender, + long seed, + String dimension, + StudioOpenCoordinator.StudioOpenKind openKind, + Runnable beforeOpen, + Consumer onDone + ) { + return closeActiveProject().thenCompose(closeResult -> { + if (closeResult == null) { + return CompletableFuture.failedFuture( + new IllegalStateException("Studio close completed without a result.")); + } + if (closeResult.failureCause() != null) { + return CompletableFuture.failedFuture(closeResult.failureCause()); + } + beforeOpen.run(); + return beginStudioOpenTracked(sender, seed, dimension, openKind, onDone); + }); + } + + private CompletableFuture beginStudioOpenTracked( + VolmitSender sender, + long seed, + String dimension, + StudioOpenCoordinator.StudioOpenKind openKind, + Consumer onDone + ) { + IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension)); + activeProject = project; + CompletableFuture opening; + try { + opening = project.open(sender, seed, openKind, onDone); + } catch (IrisException exception) { + if (activeProject == project) { + activeProject = null; + } + return CompletableFuture.failedFuture(exception); + } + + activeOpen = opening; + return opening.thenApply(result -> Objects.requireNonNull( + result, + "Studio open completed without a result.")) + .whenComplete((result, throwable) -> { + if (activeOpen == opening) { + activeOpen = null; + } + if (throwable != null && activeProject == project && !project.isOpen()) { + activeProject = null; + } + }); + } + + private CompletableFuture replaceActiveProject( + VolmitSender sender, + long seed, + String dimension, + Consumer onDone + ) { + return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> { + if (closeThrowable != null) { + IrisLogging.reportError("Failed while closing an existing studio project before opening \"" + dimension + "\".", closeThrowable); + J.s(() -> sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_CLOSE_EXISTING_STUDIO_PROJECT, + MessageArgument.untrusted("error", String.valueOf(errorDetail(closeThrowable)))))); + return false; + } + if (closeResult == null) { + IllegalStateException failure = new IllegalStateException("Studio close completed without a result."); + IrisLogging.reportError("Failed while closing an existing studio project before opening \"" + dimension + "\".", failure); + J.s(() -> sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_CLOSE_EXISTING_STUDIO_PROJECT_2, + MessageArgument.untrusted("error", String.valueOf(errorDetail(failure)))))); + return false; + } + if (closeResult.failureCause() != null) { + Throwable failure = closeResult.failureCause(); + IrisLogging.reportError("Failed while closing an existing studio project before opening \"" + dimension + "\".", failure); + J.s(() -> sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_CLOSE_EXISTING_STUDIO_PROJECT_2, + MessageArgument.untrusted("error", String.valueOf(errorDetail(failure)))))); + return false; + } + return true; + }).thenCompose(closed -> closed + ? beginStudioOpen(sender, seed, dimension, onDone) + : CompletableFuture.completedFuture(null)); + } + + private CompletableFuture beginStudioOpen( + VolmitSender sender, + long seed, + String dimension, + Consumer onDone + ) { + IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension)); + activeProject = project; + CompletableFuture opening; + try { + opening = project.open( + sender, + seed, + StudioOpenCoordinator.StudioOpenKind.STANDARD, + onDone); + } catch (IrisException e) { + if (activeProject == project) { + activeProject = null; + } + J.s(() -> sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_OPEN_STUDIO_WORLD_2, + MessageArgument.untrusted("error", String.valueOf(errorDetail(e)))))); + return CompletableFuture.completedFuture(null); + } + + activeOpen = opening; + return opening.handle((result, throwable) -> { + if (activeOpen == opening) { + activeOpen = null; + } + if (throwable != null && activeProject == project && !project.isOpen()) { + activeProject = null; + } + return null; + }); + } + + public void openVSCode(VolmitSender sender, String dim) { + new IrisCodeWorkspace(new IrisProject(new File(getWorkspaceFolder(), dim))).openVSCode(sender); + } + + public File getWorkspaceFolder(String... sub) { + return art.arcane.iris.platform.bukkit.BukkitPlatform.volmitPlugin().getDataFolderList(WORKSPACE_NAME, sub); + } + + public File getWorkspaceFile(String... sub) { + return art.arcane.iris.platform.bukkit.BukkitPlatform.volmitPlugin().getDataFileList(WORKSPACE_NAME, sub); + } + + public CompletableFuture close() { + return studioTransitions.submit(this::closeActiveProject); + } + + CompletableFuture closeActiveProjectForReplacement( + VolmitSender sender + ) { + IrisProject project = activeProject; + if (project == null) { + return closeActiveProject(); + } + JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(project.getName()); + if (request == null || !sender.isPlayer()) { + return closeActiveProject(); + } + UUID ownerId = sender.player().getUniqueId(); + return JigsawStudioService.get() + .awaitCloseForReplacement(request.requestId(), ownerId) + .thenCompose(ignored -> { + if (activeProject != project) { + return CompletableFuture.failedFuture(new IllegalStateException( + "The active Studio project changed while replacement was waiting to close.")); + } + return closeActiveProject(); + }); + } + + private CompletableFuture closeActiveProject() { + IrisProject project = activeProject; + if (project == null) { + return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult( + null, + true, + true, + false, + null + )); + } + + JigsawStudioActivation.Request jigsawRequest = JigsawStudioActivation.getRequest(project.getName()); + if (jigsawRequest != null) { + String protectionFailure = JigsawStudioService.get() + .closeProtectionFailure(jigsawRequest.requestId()); + if (protectionFailure != null) { + return CompletableFuture.failedFuture(new IllegalStateException(protectionFailure)); + } + } + + IrisLogging.debug("Closing Active Project"); + CompletableFuture closing = project.close(); + return closing.whenComplete((result, throwable) -> { + if (throwable == null + && result != null + && result.failureCause() == null + && activeProject == project) { + activeProject = null; + } + }); + } + + private void destroyStudioWorld(World world, PlatformChunkGenerator generator) { + IrisToolbelt.beginWorldMaintenance(world, "studio-disable", true); + try { + IrisToolbelt.evacuate(world); + } catch (Throwable e) { + IrisLogging.reportError("Failed to evacuate studio world \"" + world.getName() + "\" during shutdown cleanup.", e); + } + try { + WorldLifecycleService.get().unloadAsync(world, false) + .thenCompose(unloaded -> { + if (!Boolean.TRUE.equals(unloaded) || generator == null) { + return CompletableFuture.completedFuture(Boolean.TRUE.equals(unloaded)); + } + return generator.closeAsync().thenApply(ignored -> true); + }) + .whenComplete((unloaded, throwable) -> { + IrisToolbelt.endWorldMaintenance(world, "studio-disable", true); + if (throwable != null) { + IrisLogging.reportError("Failed to unload studio world \"" + world.getName() + + "\" during disable cleanup; startup deletion remains queued.", throwable); + } else if (!Boolean.TRUE.equals(unloaded)) { + IrisLogging.warn("Studio world \"" + world.getName() + + "\" remained loaded during disable cleanup; startup deletion remains queued."); + } + }); + } catch (Throwable e) { + IrisToolbelt.endWorldMaintenance(world, "studio-disable", true); + IrisLogging.reportError("Failed to unload studio world \"" + world.getName() + "\" during shutdown cleanup.", e); + } + } + + private void queueStudioWorldDeletionOnStartup(LinkedHashSet worldNamesToDelete) { + if (worldNamesToDelete.isEmpty()) { + return; + } + + LinkedHashSet normalizedNames = new LinkedHashSet<>(); + for (String worldName : worldNamesToDelete) { + String baseWorldName = TransientWorldCleanupSupport.transientStudioBaseWorldName(worldName); + if (baseWorldName != null) { + normalizedNames.add(baseWorldName); + continue; + } + + if (worldName != null && !worldName.isBlank()) { + normalizedNames.add(worldName); + } + } + + if (normalizedNames.isEmpty()) { + return; + } + + try { + IrisServices.get(WorldDeletionQueue.class).queueFamilyForStartupDeletion(List.copyOf(normalizedNames)); + } catch (IOException e) { + IrisLogging.reportError("Failed to queue studio world deletion on startup.", e); + } + } + + public File compilePackage(VolmitSender sender, String d, boolean obfuscate, boolean minify) { + return new IrisPackageCompiler(new IrisProject(new File(getWorkspaceFolder(), d))).compilePackage(sender, obfuscate, minify); + } + + private void createFrom(File sourcePack, String sourceDimensionKey, String newName) throws IOException { + if (J.isPrimaryThread()) { + throw new IOException("Studio project copying cannot run on the Bukkit primary thread."); + } + String sourceKey = normalizeProjectName(sourceDimensionKey); + String targetName = normalizeProjectName(newName); + File workspace = requireSafeWorkspace(getWorkspaceFolder()); + File newPack = new File(workspace, targetName); + IrisProjectCopier.copyProject(sourcePack, workspace, sourceKey, targetName); + + try { + IrisProject project = new IrisProject(newPack); + JSONObject workspaceConfiguration = new IrisCodeWorkspace(project).createCodeWorkspaceConfig(); + writeWorkspaceAtomically(newPack.toPath(), targetName, workspaceConfiguration.toString(0)); + } catch (Throwable e) { + try { + AtomicDirectoryPublisher.deleteTree(newPack.toPath()); + } catch (IOException cleanupFailure) { + e.addSuppressed(cleanupFailure); + } + if (e instanceof Error error) { + throw error; + } + throw new IOException("Failed to create the editor workspace for project '" + targetName + "'.", e); + } + } + + public void create(VolmitSender sender, String s, String downloadable) { + Runnable work = () -> createProject(sender, s, downloadable, null); + runOffPrimaryThread(work); + } + + public void create(VolmitSender sender, String name, IrisDimension template) { + IrisData loader = template == null ? null : template.getLoader(); + File sourcePack = loader == null ? null : loader.getDataFolder(); + String sourceKey = template == null ? null : template.getLoadKey(); + Runnable work = () -> createProject(sender, name, sourceKey, sourcePack); + runOffPrimaryThread(work); + } + + public void create(VolmitSender sender, String s) { + Runnable work = () -> createProject(sender, s, null, null); + runOffPrimaryThread(work); + } + + private void createProject(VolmitSender sender, String requestedName, String requestedTemplate, File selectedTemplatePack) { + Optional startupDenial = IrisStartupValidation.denialReason(); + if (startupDenial.isPresent()) { + sender.sendMessage("Studio project creation refused: " + startupDenial.get()); + return; + } + String normalizedName; + String templateName; + File workspace; + try { + normalizedName = normalizeProjectName(requestedName); + templateName = requestedTemplate == null ? null : normalizeProjectName(requestedTemplate); + workspace = requireSafeWorkspace(getWorkspaceFolder()); + } catch (IOException e) { + sender.sendMessage("Studio project creation refused: " + errorDetail(e)); + return; + } + + LifecycleOperationCoordinator.Lease lease; + try { + lease = LifecycleOperationCoordinator.get().acquire( + LifecycleOperationCoordinator.Domain.PACK_MUTATION, + LifecycleOperationCoordinator.OperationKind.PACK_CREATE, + normalizedName + ); + } catch (LifecycleOperationCoordinator.BusyException e) { + sendBusy(sender, e); + return; + } + + String projectName = normalizedName; + File createdPack = null; + boolean projectPublished = false; + CreationOutcome outcome = CreationOutcome.FAILED; + try { + if ("studio".equals(projectName)) { + projectName = nextAvailableProjectName(workspace, projectName); + } + + File newPack = new File(workspace, projectName); + createdPack = newPack; + if (Files.exists(newPack.toPath(), LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(newPack.toPath())) { + sender.sendMessage("Studio project '" + projectName + "' already exists; nothing was changed."); + return; + } + + if (templateName == null) { + createStarterProject(workspace, projectName); + } else { + File importPack = selectedTemplatePack == null ? new File(workspace, templateName) : selectedTemplatePack; + if (!hasLoadableDimensionFile(importPack, templateName)) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_COULDN_T_FIND_PACK_CREATE_NEW_DIMENSION_FROM)); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_MISSING_IMPORTED_DIMENSION_FILE)); + return; + } + PackValidationRegistry.requireLoadable(importPack.getName()); + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_IMPORTING_INTO_NEW_PROJECT, MessageArgument.untrusted("downloadable", String.valueOf(templateName)), MessageArgument.untrusted("s", String.valueOf(projectName)))); + createFrom(importPack, templateName, projectName); + } + projectPublished = true; + + PackValidationResult createdValidation = PackValidator.validate(newPack); + PackValidationRegistry.publish(createdValidation); + if (!createdValidation.isLoadable()) { + rollbackCreatedProject(sender, newPack, + "Studio project validation failed; the new project was rolled back."); + for (String reason : createdValidation.getBlockingErrors()) { + sender.sendMessage(reason); + } + return; + } + + DatapackInstallResult installResult = ServerConfigurator.installDataPacksIfChanged(true); + CreationOutcome installOutcome = switch (installResult.status()) { + case FAILED -> CreationOutcome.FAILED; + case RESTART_REQUIRED -> CreationOutcome.RESTART; + case READY, UNCHANGED -> CreationOutcome.OPEN; + }; + if (installOutcome == CreationOutcome.FAILED) { + rollbackCreatedProject(sender, newPack, "Datapack installation failed; the new project was rolled back."); + return; + } + + sender.sendMessage("Created studio project '" + projectName + "' at " + newPack.getAbsolutePath() + "."); + if (installOutcome == CreationOutcome.RESTART) { + sender.sendMessage("The project is complete, but Iris must restart before opening it. After restart, run /iris studio open " + projectName + "."); + } + outcome = installOutcome; + } catch (Throwable e) { + if (projectPublished && createdPack != null) { + rollbackCreatedProject(sender, createdPack, "Studio project creation failed; the new project was rolled back."); + } + IrisLogging.reportError("Failed to create studio project '" + projectName + "'.", e); + sender.sendMessage("Studio project creation failed: " + errorDetail(e)); + } finally { + closeLease(lease); + } + + if (outcome == CreationOutcome.RESTART) { + ServerConfigurator.restart(); + } else if (outcome == CreationOutcome.OPEN) { + String completedProjectName = projectName; + LifecycleOperationCoordinator.get().whenIdle(() -> open(sender, completedProjectName)); + } + } + + private void runPackMutation( + VolmitSender sender, + LifecycleOperationCoordinator.OperationKind operationKind, + String target, + PackDownloadProgressReporter reporter, + PackMutation mutation, + String failureMessage + ) { + String operationTarget = target == null || target.isBlank() ? "unspecified-pack" : target.trim(); + PackDownloadExecution execution; + synchronized (downloadAdmissionMonitor) { + if (!downloadAdmissionOpen) { + sender.sendMessage(IrisLanguage.text(PackDownloadMessages.SHUTTING_DOWN)); + return; + } + + LifecycleOperationCoordinator.Lease lease; + try { + lease = LifecycleOperationCoordinator.get().acquire( + LifecycleOperationCoordinator.Domain.PACK_MUTATION, + operationKind, + operationTarget + ); + } catch (LifecycleOperationCoordinator.BusyException e) { + sendBusy(sender, e); + return; + } + + execution = new PackDownloadExecution( + lease, + cancellation -> executePackMutation(mutation, reporter, failureMessage, cancellation) + ); + PackDownloadExecution trackedExecution = execution; + execution.onCompletion(() -> { + clearActiveDownload(trackedExecution); + reporter.executionComplete(); + }); + activeDownload = execution; + } + + try { + reporter.start(); + Future future = MultiBurst.ioBurst.submit(execution); + execution.bind(future); + } catch (Throwable e) { + try { + IrisLogging.reportError(failureMessage, e); + reporter.fail(e); + } catch (Throwable reportingFailure) { + IrisLogging.reportError("Failed to report an Iris pack download startup failure.", reportingFailure); + } finally { + execution.cancel(); + } + } + } + + private void executePackMutation( + PackMutation mutation, + PackDownloadProgressReporter reporter, + String failureMessage, + PackDownloader.DownloadCancellation cancellation + ) throws PackDownloader.PackDownloadCancelledException { + try { + mutation.run(cancellation); + } catch (PackDownloader.PackDownloadCancelledException e) { + reporter.cancel(); + throw e; + } catch (PackDownloader.PackDownloadBusyException e) { + reporter.fail(e); + return; + } catch (Throwable e) { + IrisLogging.reportError(failureMessage, e); + reporter.fail(e); + } + } + + public void quiesceDownloadsForShutdown() { + PackDownloadExecution execution; + synchronized (downloadAdmissionMonitor) { + downloadAdmissionOpen = false; + execution = activeDownload; + } + if (execution == null) { + return; + } + + execution.cancel(); + boolean interrupted = false; + boolean warned = false; + while (!execution.isComplete()) { + try { + if (!execution.await(DOWNLOAD_SHUTDOWN_POLL_SECONDS, TimeUnit.SECONDS) && !warned) { + warned = true; + IrisLogging.warn(execution.isPublishing() + ? "Waiting for atomic pack publication to finish before Iris shutdown." + : "Waiting for the active pack download to cancel before Iris shutdown."); + } + } catch (InterruptedException e) { + interrupted = true; + execution.cancel(); + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + private void clearActiveDownload(PackDownloadExecution execution) { + synchronized (downloadAdmissionMonitor) { + if (activeDownload == execution) { + activeDownload = null; + } + } + } + + private void runOffPrimaryThread(Runnable work) { + if (J.isPrimaryThread()) { + J.a(work); + return; + } + work.run(); + } + + static String normalizeProjectName(String value) throws IOException { + if (value == null) { + throw new IOException("Project name cannot be empty."); + } + String normalized = value.trim().toLowerCase(Locale.ROOT); + if (normalized.isEmpty() || normalized.length() > 64 || !PROJECT_NAME.matcher(normalized).matches()) { + throw new IOException("Invalid project name '" + value + "' (allowed: lowercase a-z, 0-9, _ and -)."); + } + return normalized; + } + + static File requireSafeWorkspace(File workspaceFolder) throws IOException { + if (workspaceFolder == null) { + throw new IOException("Pack workspace is unavailable."); + } + Path workspace = workspaceFolder.toPath().toAbsolutePath().normalize(); + Files.createDirectories(workspace); + if (Files.isSymbolicLink(workspace) || !Files.isDirectory(workspace, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Pack workspace is missing or unsafe: " + workspace); + } + return workspace.toFile(); + } + + static String nextAvailableProjectName(File workspace, String baseName) throws IOException { + String normalizedBase = normalizeProjectName(baseName); + Path root = requireSafeWorkspace(workspace).toPath(); + String candidate = normalizedBase; + int suffix = 2; + while (Files.exists(root.resolve(candidate), LinkOption.NOFOLLOW_LINKS)) { + candidate = normalizedBase + suffix++; + } + return candidate; + } + + static void createStarterProject(File workspace, String projectName) throws IOException { + File safeWorkspace = requireSafeWorkspace(workspace); + String safeName = normalizeProjectName(projectName); + Path target = safeWorkspace.toPath().resolve(safeName); + Path stage = Files.createTempDirectory(safeWorkspace.toPath(), ".iris-starter-" + safeName + "-"); + boolean published = false; + try { + Files.createDirectories(stage.resolve("dimensions")); + Files.createDirectories(stage.resolve("regions")); + Files.createDirectories(stage.resolve("biomes")); + Files.createDirectories(stage.resolve("generators")); + Files.writeString(stage.resolve("dimensions/" + safeName + ".json"), """ + { + "name": "%s", + "version": 1, + "regions": ["starter"], + "logicalHeight": 384, + "dimensionHeight": {"min": -64, "max": 320} + } + """.formatted(safeName), StandardCharsets.UTF_8); + Files.writeString(stage.resolve("regions/starter.json"), """ + { + "name": "Starter", + "landBiomes": ["starter"], + "seaBiomes": ["starter"], + "shoreBiomes": ["starter"] + } + """, StandardCharsets.UTF_8); + Files.writeString(stage.resolve("biomes/starter.json"), """ + { + "name": "Starter Plains", + "layers": [{"palette": [{"block": "minecraft:grass_block"}]}], + "generators": [{"generator": "flat", "min": 96, "max": 96}], + "derivative": "minecraft:plains", + "vanillaDerivative": "minecraft:plains" + } + """, StandardCharsets.UTF_8); + Files.writeString(stage.resolve("generators/flat.json"), """ + { + "interpolator": {"function": "NONE", "horizontalScale": 1}, + "seed": 310, + "composite": [{"seed": 310, "style": {"style": "FLAT"}}] + } + """, StandardCharsets.UTF_8); + publishNewDirectory(stage, target); + published = true; + IrisProject project = new IrisProject(target.toFile()); + JSONObject workspaceConfiguration = new IrisCodeWorkspace(project).createCodeWorkspaceConfig(); + writeWorkspaceAtomically(target, safeName, workspaceConfiguration.toString(0)); + } catch (Throwable failure) { + if (published) { + Throwable cleanupFailure = rollbackCreatedProjectFiles(target.toFile()); + if (cleanupFailure != null && cleanupFailure != failure) { + failure.addSuppressed(cleanupFailure); + } + } + if (failure instanceof Error error) { + throw error; + } + if (failure instanceof IOException ioFailure) { + throw ioFailure; + } + throw new IOException("Failed to create starter project '" + safeName + "'.", failure); + } finally { + AtomicDirectoryPublisher.deleteTree(stage); + } + } + + private static boolean hasLoadableDimensionFile(File pack, String key) { + Path packPath = pack.toPath().toAbsolutePath().normalize(); + Path dimension = packPath.resolve("dimensions").resolve(key + ".json").normalize(); + try { + PackDirectoryResolver.requireSafePackTree(pack); + } catch (IOException exception) { + return false; + } + return dimension.startsWith(packPath) + && Files.isDirectory(packPath) + && !Files.isSymbolicLink(dimension) + && Files.isRegularFile(dimension, LinkOption.NOFOLLOW_LINKS); + } + + private static void rollbackCreatedProject(VolmitSender sender, File project, String message) { + Throwable cleanupFailure = rollbackCreatedProjectFiles(project); + if (cleanupFailure == null) { + sender.sendMessage(message); + return; + } + IrisLogging.reportError("Failed to fully roll back studio project " + project.getPath(), cleanupFailure); + sender.sendMessage(message + " Cleanup was incomplete; check the console and " + + project.getAbsolutePath() + "."); + } + + static Throwable rollbackCreatedProjectFiles(File project) { + File target = project.toPath().toAbsolutePath().normalize().toFile(); + Throwable cleanupFailure = null; + IrisData loadedData = IrisData.getLoaded(target).orElse(null); + if (loadedData != null) { + try { + loadedData.close(); + } catch (Throwable closeFailure) { + cleanupFailure = closeFailure; + } + } + try { + AtomicDirectoryPublisher.deleteTree(target.toPath()); + } catch (Throwable deleteFailure) { + if (cleanupFailure == null) { + cleanupFailure = deleteFailure; + } else if (cleanupFailure != deleteFailure) { + cleanupFailure.addSuppressed(deleteFailure); + } + } + return cleanupFailure; + } + + private static void sendBusy(VolmitSender sender, LifecycleOperationCoordinator.BusyException busy) { + sender.sendMessage(packMutationBusyMessage(busy.currentOperation())); + } + + static String packMutationBusyMessage(LifecycleOperationCoordinator.ActiveOperation operation) { + if (operation.domain() == LifecycleOperationCoordinator.Domain.PACK_MUTATION + && operation.kind() == LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD) { + return IrisLanguage.plain(PackDownloadMessages.IN_PROGRESS); + } + return "Iris pack changes are busy with " + operation.kind().name().toLowerCase(Locale.ROOT) + + " for '" + operation.target() + "'. Try again when it completes."; + } + + private static void closeLease(LifecycleOperationCoordinator.Lease lease) { + try { + lease.close(); + } catch (Throwable e) { + IrisLogging.reportError("Lifecycle idle callback failed after a studio pack operation.", e); + } + } + + private static String errorDetail(Throwable failure) { + Throwable detail = failure; + while (detail.getCause() != null && detail.getCause() != detail) { + detail = detail.getCause(); + } + String message = detail.getMessage(); + return message == null || message.isBlank() ? detail.getClass().getSimpleName() : message; + } + + static void copyPackTree(Path source, Path target) throws IOException { + Path normalizedSource = source.toRealPath(); + if (!Files.isDirectory(normalizedSource, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Pack source is not a directory: " + normalizedSource); + } + Path requestedTarget = target.toAbsolutePath().normalize(); + if (Files.isSymbolicLink(requestedTarget)) { + throw new IOException("Pack installation stage is a symbolic link: " + requestedTarget); + } + Path normalizedTarget; + if (Files.exists(requestedTarget, LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isDirectory(requestedTarget, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Pack installation stage is not a directory: " + requestedTarget); + } + normalizedTarget = requestedTarget.toRealPath(); + } else { + Path parent = Objects.requireNonNull( + requestedTarget.getParent(), + "Pack installation stage parent"); + normalizedTarget = parent.toRealPath().resolve(requestedTarget.getFileName()).normalize(); + } + if (normalizedTarget.startsWith(normalizedSource) + || normalizedSource.startsWith(normalizedTarget)) { + throw new IOException("Pack source and installation stage overlap: " + + normalizedSource + " and " + normalizedTarget); + } + Files.walkFileTree(normalizedSource, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory( + Path directory, + BasicFileAttributes attributes + ) throws IOException { + if (attributes.isSymbolicLink()) { + throw new IOException("Pack contains a symbolic link: " + directory); + } + if (!directory.equals(normalizedSource) + && normalizedSource.relativize(directory).getNameCount() == 1 + && PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) { + return FileVisitResult.SKIP_SUBTREE; + } + Files.createDirectories(copyDestination( + normalizedSource, + normalizedTarget, + directory)); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { + if (attributes.isSymbolicLink()) { + throw new IOException("Pack contains a symbolic link: " + file); + } + if (!attributes.isRegularFile()) { + throw new IOException("Pack contains an unsupported entry: " + file); + } + String fileName = file.getFileName().toString(); + if ((normalizedSource.relativize(file).getNameCount() == 1 + && PackDirectoryResolver.isHiddenName(fileName)) + || fileName.endsWith(".code-workspace")) { + return FileVisitResult.CONTINUE; + } + Path destination = copyDestination(normalizedSource, normalizedTarget, file); + Files.createDirectories(Objects.requireNonNull(destination.getParent(), "Pack entry parent")); + Files.copy(file, destination, StandardCopyOption.COPY_ATTRIBUTES); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException { + throw new IOException("Unable to copy pack entry: " + file, failure); + } + }); + } + + private static Path copyDestination(Path source, Path target, Path entry) throws IOException { + Path destination = target.resolve(source.relativize(entry)).normalize(); + if (!destination.startsWith(target)) { + throw new IOException("Pack entry escapes its installation stage: " + entry); + } + return destination; + } + + static void publishNewDirectory(Path stage, Path target) throws IOException { + if (Files.exists(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) { + throw new FileAlreadyExistsException(target.toString()); + } + try { + Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(stage, target); + } + } + + private static void writeWorkspaceAtomically(Path project, String projectName, String content) throws IOException { + Path target = project.resolve(projectName + ".code-workspace"); + Path stage = Files.createTempFile(project, "." + projectName + ".workspace-", ".tmp"); + IOException operationFailure = null; + try { + Files.writeString(stage, content, StandardCharsets.UTF_8); + if (Files.exists(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) { + throw new FileAlreadyExistsException(target.toString()); + } + try { + Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(stage, target); + } + } catch (IOException e) { + operationFailure = e; + throw e; + } finally { + try { + Files.deleteIfExists(stage); + } catch (IOException cleanupFailure) { + if (operationFailure != null) { + operationFailure.addSuppressed(cleanupFailure); + } else { + throw cleanupFailure; + } + } + } + } + + static final class StudioTransitionQueue { + private final Object monitor; + private CompletableFuture tail; + + StudioTransitionQueue() { + monitor = new Object(); + tail = CompletableFuture.completedFuture(null); + } + + CompletableFuture submit(Supplier> operation) { + Supplier> queuedOperation = Objects.requireNonNull(operation, "operation"); + CompletableFuture result = new CompletableFuture<>(); + synchronized (monitor) { + tail = tail.handle((ignored, previousFailure) -> null) + .thenCompose(ignored -> execute(queuedOperation, result)); + } + return result; + } + + private CompletableFuture execute( + Supplier> operation, + CompletableFuture result + ) { + CompletableFuture running; + try { + running = Objects.requireNonNull(operation.get(), "Studio transition returned no future."); + } catch (Throwable failure) { + result.completeExceptionally(failure); + return CompletableFuture.completedFuture(null); + } + return running.handle((value, failure) -> { + if (failure == null) { + result.complete(value); + } else { + result.completeExceptionally(failure); + } + return null; + }); + } + } + + @FunctionalInterface + private interface PackMutation { + void run(PackDownloader.DownloadCancellation cancellation) throws Exception; + } + + private enum CreationOutcome { + FAILED, + OPEN, + RESTART + } + + public IrisProject getActiveProject() { + return activeProject; + } + + public void updateWorkspace() { + if (isProjectOpen()) { + new IrisCodeWorkspace(activeProject).updateWorkspace(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java b/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java new file mode 100644 index 000000000..cefa5cc3e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java @@ -0,0 +1,420 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.service; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineLifecycleTasks; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.IrisBiome; +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.IrisRegion; +import art.arcane.iris.engine.object.IrisTreeModes; +import art.arcane.iris.engine.object.IrisTreeSize; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.data.Cuboid; +import art.arcane.iris.util.common.data.IrisCustomData; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.HeightMap; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.TreeType; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Sapling; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.world.StructureGrowEvent; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Predicate; + +public class TreeSVC implements IrisService { + // Re-entrancy guard for the synthetic StructureGrowEvent this service fires from inside + // its own handler. callEvent dispatches inline on the calling thread, so a ThreadLocal + // is exact; a shared boolean let one region's growth suppress another's on Folia. + private static final ThreadLocal REENTRANT = ThreadLocal.withInitial(() -> false); + + @Override + public void onEnable() { + + } + + @Override + public void onDisable() { + + } + + /** + * This function does the following + *
1. Is the sapling growing in an Iris world? No -> exit
+ *
2. Is the Iris world accessible? No -> exit
+ *
3. Is the sapling overwriting setting on in that dimension? No -> exit
+ *
4. Check biome, region and dimension for overrides for that sapling type -> Found -> use
+ *
5. Exit if none are found, cancel event if one or more are.
+ * + * @param event Checks the given event for sapling overrides + */ + @EventHandler(priority = EventPriority.HIGHEST) + public void on(StructureGrowEvent event) { + if (REENTRANT.get() || event.isCancelled()) { + return; + } + IrisLogging.debug(this.getClass().getName() + " received a structure grow event"); + + if (!IrisToolbelt.isIrisWorld(event.getWorld())) { + IrisLogging.debug(this.getClass().getName() + " passed grow event off to vanilla since not an Iris world"); + return; + } + + PlatformChunkGenerator worldAccess = IrisToolbelt.access(event.getWorld()); + if (worldAccess == null) { + IrisLogging.debug(this.getClass().getName() + " passed it off to vanilla because could not get IrisAccess for this world"); + IrisLogging.reportError(new NullPointerException(event.getWorld().getName() + " could not be accessed despite being an Iris world")); + return; + } + + Engine engine = worldAccess.getEngine(); + + if (engine == null) { + IrisLogging.debug(this.getClass().getName() + " passed it off to vanilla because could not get Engine for this world"); + IrisLogging.reportError(new NullPointerException(event.getWorld().getName() + " could not be accessed despite being an Iris world")); + return; + } + + IrisDimension dimension = engine.getDimension(); + + if (dimension == null) { + IrisLogging.debug(this.getClass().getName() + " passed it off to vanilla because could not get Dimension for this world"); + IrisLogging.reportError(new NullPointerException(event.getWorld().getName() + " could not be accessed despite being an Iris world")); + return; + } + + if (!dimension.getTreeSettings().isEnabled()) { + IrisLogging.debug(this.getClass().getName() + " cancelled because tree overrides are disabled"); + return; + } + + BlockData first = event.getLocation().getBlock().getBlockData().clone(); + Cuboid saplingPlane = getSaplings(event.getLocation(), blockData -> blockData instanceof Sapling && blockData.getMaterial().equals(first.getMaterial()), event.getWorld()); + + IrisLogging.debug("Sapling grew @ " + event.getLocation() + " for " + event.getSpecies().name() + " usedBoneMeal is " + event.isFromBonemeal()); + IrisLogging.debug("Sapling plane is: " + saplingPlane.getSizeX() + " by " + saplingPlane.getSizeZ()); + IrisObjectPlacement placement = getObjectPlacement(worldAccess, event.getLocation(), event.getSpecies(), new IrisTreeSize(1, 1)); + + if (placement == null) { + IrisLogging.debug(this.getClass().getName() + " had options but did not manage to find objectPlacements for them"); + return; + } + + saplingPlane.forEach(block -> block.setType(Material.AIR)); + IrisObject object = worldAccess.getData().getObjectLoader().load(placement.getPlace().getRandom(RNG.r)); + String treeMarker = object.getLoadKey() + "@" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE); + List blockStateList = new KList<>(); + KMap dataCache = new KMap<>(); + // TODO: REAL CLASSES!!!! + + IObjectPlacer placer = new IObjectPlacer() { + + @Override + public int getHighest(int x, int z, IrisData data) { + return event.getWorld().getHighestBlockYAt(x, z); + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return event.getWorld().getHighestBlockYAt(x, z, ignoreFluid ? HeightMap.OCEAN_FLOOR : HeightMap.WORLD_SURFACE); + } + + @Override + public void set(int x, int y, int z, PlatformBlockState s) { + BlockData d = (BlockData) s.nativeHandle(); + Block b = event.getWorld().getBlockAt(x, y, z); + // Listeners get the post-growth tree, per the StructureGrowEvent contract; + // a fresh snapshot here handed every plugin an all-air "tree". + BlockState state = b.getState(); + if (d instanceof IrisCustomData data) + state.setBlockData(data.getBase()); + else state.setBlockData(d); + blockStateList.add(state); + dataCache.put(new Location(event.getWorld(), x, y, z), d); + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + return BukkitBlockState.of(event.getWorld().getBlockAt(x, y, z).getBlockData()); + } + + @Override + public boolean isPreventingDecay() { + return true; + } + + @Override + public boolean isCarved(int x, int y, int z) { + return false; + } + + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + return event.getWorld().getBlockAt(x, y, z).getType().isSolid(); + } + + @Override + public boolean isSolid(int x, int y, int z) { + return event.getWorld().getBlockAt(x, y, z).getBlockData().getMaterial().isSolid(); + } + + @Override + public boolean isUnderwater(int x, int z) { + return false; + } + + @Override + public int getFluidHeight() { + return worldAccess.getEngine().getDimension().getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return false; + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + + } + + @Override + public void setData(int xx, int yy, int zz, T data) { + + } + + @Override + public T getData(int xx, int yy, int zz, Class t) { + return null; + } + + @Override + public Engine getEngine() { + return engine; + } + }; + + object.place( + saplingPlane.getCenter().getBlockX(), + (saplingPlane.getCenter().getBlockY() + object.getH() / 2), + saplingPlane.getCenter().getBlockZ(), + placer, + placement, + RNG.r, + Objects.requireNonNull(worldAccess).getData() + ); + + event.setCancelled(true); + + Runnable growTask = () -> { + + StructureGrowEvent iGrow = new StructureGrowEvent(event.getLocation(), event.getSpecies(), event.isFromBonemeal(), event.getPlayer(), blockStateList); + REENTRANT.set(true); + try { + Bukkit.getServer().getPluginManager().callEvent(iGrow); + } finally { + REENTRANT.set(false); + } + + if (!iGrow.isCancelled()) { + for (BlockState state : iGrow.getBlocks()) { + Location l = state.getLocation(); + + BlockData d = dataCache.get(l); + if (d == null) continue; + Block block = l.getBlock(); + + if (d instanceof IrisCustomData data) { + block.setBlockData(data.getBase(), false); + IrisServices.get(ExternalDataSVC.class).processUpdate(engine, block, data.getCustom()); + } else block.setBlockData(d, false); + int mantleY = block.getY() - event.getWorld().getMinHeight(); + // Lease-gated: the grow task can land after the engine started closing, and + // an unleased mantle write would race the region flush. + EngineLifecycleTasks.run(engine, "tree_grow_mantle", () -> { + engine.getMantle().getMantle().set(block.getX(), mantleY, block.getZ(), treeMarker); + engine.getMantle().getMantle().set( + block.getX(), + mantleY, + block.getZ(), + TreeBlockMaterial.of(block.getBlockData().getAsString()) + ); + }); + } + } + }; + if (!J.runAt(event.getLocation(), growTask) && !J.isFolia()) { + J.s(growTask); + } + } + + /** + * Finds a single object placement (which may contain more than one object) for the requirements species, location & + * size + * + * @param worldAccess The world to access (check for biome, region, dimension, etc) + * @param location The location of the growth event (For biome/region finding) + * @param type The bukkit TreeType to match + * @param size The size of the sapling area + * @return An object placement which contains the matched tree, or null if none were found / it's disabled. + */ + private IrisObjectPlacement getObjectPlacement(PlatformChunkGenerator worldAccess, Location location, TreeType type, IrisTreeSize size) { + + KList placements = new KList<>(); + boolean isUseAll = worldAccess.getEngine().getDimension().getTreeSettings().getMode().equals(IrisTreeModes.ALL); + + // Retrieve objectPlacements of type `species` from biome + IrisBiome biome = worldAccess.getEngine().getBiome(location.getBlockX(), location.getBlockY()-worldAccess.getTarget().getWorld().minHeight(), location.getBlockZ()); + placements.addAll(matchObjectPlacements(biome.getObjects(), size, type)); + + // Add more or find any in the region + if (isUseAll || placements.isEmpty()) { + IrisRegion region = worldAccess.getEngine().getRegion(location.getBlockX(), location.getBlockZ()); + placements.addAll(matchObjectPlacements(region.getObjects(), size, type)); + } + + // Check if no matches were found, return a random one if they are + return placements.isNotEmpty() ? placements.getRandom() : null; + } + + /** + * Filters out mismatches and returns matches + * + * @param objects The object placements to check + * @param size The size of the sapling area to filter with + * @param type The type of the tree to filter with + * @return A list of objectPlacements that matched. May be empty. + */ + private KList matchObjectPlacements(KList objects, IrisTreeSize size, TreeType type) { + + KList p = new KList<>(); + + for (IrisObjectPlacement i : objects) { + if (i.matches(size, type)) { + p.add(i); + } + } + + return p; + } + + /** + * Get the Cuboid of sapling sizes at a location & blockData predicate + * + * @param at this location + * @param valid with this blockData predicate + * @param world the world to check in + * @return A cuboid containing only saplings + */ + public Cuboid getSaplings(Location at, Predicate valid, World world) { + KList blockPositions = new KList<>(); + grow(at.getWorld(), new BlockPosition(at.getBlockX(), at.getBlockY(), at.getBlockZ()), valid, blockPositions); + if (blockPositions.isEmpty()) { + // No matching saplings (e.g. a giant-mushroom grow event): a 1x1 plane at the + // event location, never the MIN/MAX sentinel cuboid below. + return new Cuboid(at, at); + } + BlockPosition a = new BlockPosition(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + BlockPosition b = new BlockPosition(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + + // Maximise the block position in x and z to get max cuboid bounds + for (BlockPosition blockPosition : blockPositions) { + a.max(blockPosition); + b.min(blockPosition); + } + + IrisLogging.debug("Blocks: " + blockPositions.size()); + IrisLogging.debug("Min: " + a + " Max: " + b); + + // Create a cuboid with the size calculated before + Cuboid cuboid = new Cuboid(a.toBlock(world).getLocation(), b.toBlock(world).getLocation()); + boolean cuboidIsValid = true; + + // Loop while the cuboid is larger than 2 + while (Math.min(cuboid.getSizeX(), cuboid.getSizeZ()) > 0) { + checking: + for (int i = cuboid.getLowerX(); i < cuboid.getUpperX(); i++) { + for (int j = cuboid.getLowerY(); j < cuboid.getUpperY(); j++) { + for (int k = cuboid.getLowerZ(); k < cuboid.getUpperZ(); k++) { + if (!blockPositions.contains(new BlockPosition(i, j, k))) { + cuboidIsValid = false; + break checking; + } + } + } + } + + // Return this cuboid if it's valid + if (cuboidIsValid) { + return cuboid; + } + + // Inset the cuboid and try again (revalidate) + cuboid = cuboid.inset(Cuboid.CuboidDirection.Horizontal, 1); + cuboidIsValid = true; + } + + return new Cuboid(at, at); + } + + /** + * Grows the blockPosition list by means of checking neighbours in + * + * @param world the world to check in + * @param center the location of this position + * @param valid validation on blockData to check block with + * @param l list of block positions to add new neighbors too + */ + private void grow(World world, BlockPosition center, Predicate valid, KList l) { + // Make sure size is less than 50, the block to check isn't already in, and make sure the blockData still matches + if (l.size() <= 50 && !l.contains(center) && valid.test(center.toBlock(world).getBlockData())) { + l.add(center); + grow(world, center.add(1, 0, 0), valid, l); + grow(world, center.add(-1, 0, 0), valid, l); + grow(world, center.add(0, 0, 1), valid, l); + grow(world, center.add(0, 0, -1), valid, l); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/tree/BlockDropRouter.java b/core/src/main/java/art/arcane/iris/core/service/tree/BlockDropRouter.java new file mode 100644 index 000000000..5aad1eab0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/tree/BlockDropRouter.java @@ -0,0 +1,6 @@ +package art.arcane.iris.core.service.tree; + +@FunctionalInterface +public interface BlockDropRouter { + boolean routeDrop(Object drop); +} diff --git a/core/src/main/java/art/arcane/iris/core/service/tree/TreeDefinitionIndex.java b/core/src/main/java/art/arcane/iris/core/service/tree/TreeDefinitionIndex.java new file mode 100644 index 000000000..c468c9fe3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/tree/TreeDefinitionIndex.java @@ -0,0 +1,97 @@ +package art.arcane.iris.core.service.tree; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.StructurePlacementMarker; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisProceduralObjects; +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.engine.object.IrisRegion; + +import java.util.HashSet; +import java.util.Set; + +public final class TreeDefinitionIndex { + private static final String PROCEDURAL_TREE_PREFIX = "procedural/tree/"; + + private final Set explicitObjectKeys; + private final Set proceduralTreeKeys; + + private TreeDefinitionIndex(Set explicitObjectKeys, Set proceduralTreeKeys) { + this.explicitObjectKeys = Set.copyOf(explicitObjectKeys); + this.proceduralTreeKeys = Set.copyOf(proceduralTreeKeys); + } + + public static TreeDefinitionIndex build(Engine engine) { + Set explicitObjectKeys = new HashSet<>(); + Set proceduralTreeKeys = new HashSet<>(); + + for (IrisRegion region : engine.getDimension().getAllRegions(engine)) { + if (region == null) { + continue; + } + collectPlacements(region.getObjects(), explicitObjectKeys); + collectProceduralTrees(region.getProceduralObjects(), proceduralTreeKeys); + } + + for (IrisBiome biome : engine.getDimension().getReachableBiomes(engine)) { + if (biome == null) { + continue; + } + collectPlacements(biome.getObjects(), explicitObjectKeys); + collectProceduralTrees(biome.getProceduralObjects(), proceduralTreeKeys); + } + + return new TreeDefinitionIndex(explicitObjectKeys, proceduralTreeKeys); + } + + public boolean isTreeMarker(String marker) { + StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker); + if (decoded == null || decoded.structureAware()) { + return false; + } + + String objectKey = decoded.objectKey(); + if (objectKey.startsWith(PROCEDURAL_TREE_PREFIX)) { + return proceduralTreeKeys.contains(objectKey); + } + if (objectKey.startsWith("procedural/")) { + return false; + } + return objectKey.startsWith("trees/") || explicitObjectKeys.contains(objectKey); + } + + private static void collectPlacements(Iterable placements, Set objectKeys) { + if (placements == null) { + return; + } + for (IrisObjectPlacement placement : placements) { + if (placement == null + || placement.getTrees() == null + || placement.getTrees().isEmpty() + || placement.getPlace() == null) { + continue; + } + for (String objectKey : placement.getPlace()) { + if (objectKey != null && !objectKey.isBlank()) { + objectKeys.add(objectKey); + } + } + } + } + + private static void collectProceduralTrees(IrisProceduralObjects proceduralObjects, Set objectKeys) { + if (proceduralObjects == null || proceduralObjects.getTrees() == null) { + return; + } + for (IrisProceduralTree tree : proceduralObjects.getTrees()) { + if (tree == null || tree.getName() == null || tree.getName().isBlank()) { + continue; + } + int variants = Math.max(1, tree.getVariants()); + for (int index = 0; index < variants; index++) { + objectKeys.add(tree.getVariantLoadKey(index)); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/tree/TreeMarkerTraversal.java b/core/src/main/java/art/arcane/iris/core/service/tree/TreeMarkerTraversal.java new file mode 100644 index 000000000..9a9c96ab1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/tree/TreeMarkerTraversal.java @@ -0,0 +1,84 @@ +package art.arcane.iris.core.service.tree; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class TreeMarkerTraversal { + public static final int MAX_MEMBERS = 131_072; + public static final int MAX_VISITED = 1_000_000; + public static final int MAX_AXIS_DISTANCE = 256; + + private TreeMarkerTraversal() { + } + + public static Discovery discover(Position trigger, String marker, int minimumY, int maximumY, MarkerLookup lookup) { + ArrayDeque pending = new ArrayDeque<>(); + Set visited = new HashSet<>(); + List members = new ArrayList<>(); + pending.add(trigger); + visited.add(trigger); + + while (!pending.isEmpty()) { + Position current = pending.removeFirst(); + if (!marker.equals(lookup.markerAt(current.x(), current.y(), current.z()))) { + continue; + } + if (members.size() >= MAX_MEMBERS) { + return new Discovery(List.copyOf(members), false); + } + members.add(current); + + for (int xOffset = -1; xOffset <= 1; xOffset++) { + for (int yOffset = -1; yOffset <= 1; yOffset++) { + for (int zOffset = -1; zOffset <= 1; zOffset++) { + if (xOffset == 0 && yOffset == 0 && zOffset == 0) { + continue; + } + Position next = current.offset(xOffset, yOffset, zOffset); + if (next.y() < minimumY || next.y() >= maximumY) { + continue; + } + if (!withinAxisBounds(trigger, next)) { + if (marker.equals(lookup.markerAt(next.x(), next.y(), next.z()))) { + return new Discovery(List.copyOf(members), false); + } + continue; + } + if (!visited.add(next)) { + continue; + } + if (visited.size() > MAX_VISITED) { + return new Discovery(List.copyOf(members), false); + } + pending.addLast(next); + } + } + } + } + + return new Discovery(List.copyOf(members), true); + } + + private static boolean withinAxisBounds(Position trigger, Position position) { + return Math.abs(position.x() - trigger.x()) <= MAX_AXIS_DISTANCE + && Math.abs(position.y() - trigger.y()) <= MAX_AXIS_DISTANCE + && Math.abs(position.z() - trigger.z()) <= MAX_AXIS_DISTANCE; + } + + @FunctionalInterface + public interface MarkerLookup { + String markerAt(int x, int y, int z); + } + + public record Position(int x, int y, int z) { + public Position offset(int xOffset, int yOffset, int zOffset) { + return new Position(x + xOffset, y + yOffset, z + zOffset); + } + } + + public record Discovery(List members, boolean complete) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/splash/IrisSplashComposer.java b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashComposer.java new file mode 100644 index 000000000..930c68c5b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashComposer.java @@ -0,0 +1,120 @@ +package art.arcane.iris.core.splash; + +import art.arcane.iris.BuildConstants; + +import java.io.File; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +public final class IrisSplashComposer { + private static final String SPLASH_PADDING = " ".repeat(4); + + private IrisSplashComposer() { + } + + public static String[] composeInfo(String version, String serverLine, InfoStyle style) { + String releaseTrain = releaseTrain(version); + String prefix = style.linePrefix(); + return new String[]{ + "", + prefix + style.title(" Iris, ") + style.subtitle("Dimension Engine ") + style.tag("[" + releaseTrain + "]"), + prefix + style.label(" Version: ") + style.value(version), + prefix + style.label(" By: ") + style.value("Volmit Software (Arcane Arts)"), + prefix + style.label(" Web: ") + style.value("VolmitSoftware.com"), + prefix + style.label(" Server: ") + style.value(serverLine), + prefix + style.label(" Java: ") + style.value(javaVersion() < 0 ? "unknown" : String.valueOf(javaVersion())) + style.label(" | Date: ") + style.value(startupDate()), + prefix + style.label(" Commit: ") + style.value(BuildConstants.COMMIT) + style.label("/") + style.value(BuildConstants.ENVIRONMENT), + "", + "", + "" + }; + } + + public static String compose(String[] splash, String[] info) { + StringBuilder builder = new StringBuilder("\n\n"); + for (int i = 0; i < splash.length; i++) { + builder.append(SPLASH_PADDING).append(splash[i]).append(info[i]).append('\n'); + } + return builder.toString(); + } + + public static List composePackLines(File packFolder, IrisSplashPackScanner.SplashPackErrorReporter reporter) { + List packs = IrisSplashPackScanner.collect(packFolder, reporter); + if (packs.isEmpty()) { + return List.of(); + } + + List lines = new ArrayList<>(packs.size() + 1); + lines.add("Custom Dimensions: " + packs.size()); + for (IrisSplashPackScanner.SplashPackMetadata pack : packs) { + lines.add(" " + pack.name() + " v" + pack.version()); + } + return lines; + } + + /** + * Total parser: java.version values like "25-ea" or "26-internal" have no dot but a + * non-numeric suffix, and a cosmetic banner must never be able to abort startup. + * + * @return the feature version, or -1 when it cannot be determined + */ + public static int javaVersion() { + String version = System.getProperty("java.version", ""); + if (version.startsWith("1.")) { + version = version.length() > 2 ? version.substring(2, 3) : ""; + } + int end = 0; + while (end < version.length() && Character.isDigit(version.charAt(end))) { + end++; + } + return end == 0 ? -1 : Integer.parseInt(version, 0, end, 10); + } + + public static String releaseTrain(String version) { + String value = version == null ? "unknown" : version; + int suffixIndex = value.indexOf('-'); + if (suffixIndex >= 0) { + value = value.substring(0, suffixIndex); + } + String[] split = value.split("\\."); + if (split.length >= 2) { + return split[0] + "." + split[1]; + } + return value; + } + + private static String startupDate() { + return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE); + } + + public interface InfoStyle { + InfoStyle PLAIN = new InfoStyle() { + }; + + default String linePrefix() { + return ""; + } + + default String title(String text) { + return text; + } + + default String subtitle(String text) { + return text; + } + + default String tag(String text) { + return text; + } + + default String label(String text) { + return text; + } + + default String value(String text) { + return text; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/splash/IrisSplashPackScanner.java b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashPackScanner.java new file mode 100644 index 000000000..68d442f73 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashPackScanner.java @@ -0,0 +1,81 @@ +package art.arcane.iris.core.splash; + +import art.arcane.iris.core.pack.PackDirectoryResolver; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public final class IrisSplashPackScanner { + private IrisSplashPackScanner() { + } + + public static List collect(File packFolder, SplashPackErrorReporter reporter) { + if (packFolder == null || !packFolder.isDirectory()) { + return List.of(); + } + + List folders = PackDirectoryResolver.listVisiblePackDirectories(packFolder); + if (folders.isEmpty()) { + return List.of(); + } + + List packs = new ArrayList<>(folders.size()); + for (File folder : folders) { + SplashPackMetadata metadata = read(folder, reporter); + if (metadata != null) { + packs.add(metadata); + } + } + + packs.sort(Comparator.comparing(SplashPackMetadata::name)); + return packs; + } + + public static SplashPackMetadata read(File pack, SplashPackErrorReporter reporter) { + if (pack == null || !pack.isDirectory()) { + return null; + } + + String dimName = pack.getName(); + File dimensionFile = new File(pack, "dimensions/" + dimName + ".json"); + if (!dimensionFile.isFile()) { + return null; + } + + try (FileReader reader = new FileReader(dimensionFile)) { + JsonObject json = JsonParser.parseReader(reader).getAsJsonObject(); + JsonElement version = json.get("version"); + if (version == null || !version.isJsonPrimitive()) { + return null; + } + + return new SplashPackMetadata(dimName, version.getAsString()); + } catch (IOException | JsonParseException | IllegalStateException | UnsupportedOperationException error) { + report(reporter, "Failed to read splash metadata for dimension pack \"" + dimName + "\".", error); + return null; + } + } + + private static void report(SplashPackErrorReporter reporter, String message, Throwable error) { + if (reporter == null) { + return; + } + + reporter.report(message, error); + } + + public record SplashPackMetadata(String name, String version) { + } + + public interface SplashPackErrorReporter { + void report(String message, Throwable error); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/splash/IrisSplashRenderer.java b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashRenderer.java new file mode 100644 index 000000000..6a01a8428 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashRenderer.java @@ -0,0 +1,181 @@ +package art.arcane.iris.core.splash; + +import java.util.concurrent.ThreadLocalRandom; + +public final class IrisSplashRenderer { + private static final int SPLASH_WIDTH = 53; + private static final int SPLASH_HEIGHT = 11; + private static final double SPLASH_CENTER_X = 26.0; + private static final double SPLASH_CENTER_Y = 5.0; + private static final double SPLASH_ASPECT = 2.15; + private static final double SPLASH_EYE_A = 5.6; + private static final double SPLASH_EYE_B = 4.6; + private static final SplashTone PLAIN_TONE = (glyph) -> ""; + + private IrisSplashRenderer() { + } + + public static String[] renderPlain() { + return render(PLAIN_TONE); + } + + public static String[] render(SplashTone tone) { + SplashTone splashTone = tone == null ? PLAIN_TONE : tone; + ThreadLocalRandom random = ThreadLocalRandom.current(); + double headThickness = 3.7 + random.nextDouble() * 0.6; + double tipReach = 19.0 + random.nextDouble() * 4.0; + double notchX = 3.5 + random.nextDouble() * 1.5; + double[][][] spines = splashFinSpines(tipReach); + double[][][] voids = splashNotchVoids(notchX); + String[] lines = new String[SPLASH_HEIGHT]; + for (int y = 0; y < SPLASH_HEIGHT; y++) { + StringBuilder line = new StringBuilder(); + String activeTone = null; + for (int x = 0; x < SPLASH_WIDTH; x++) { + char glyph = splashEyeGlyph(x, y); + if (glyph == 0) { + glyph = splashFinGlyph(x, y, spines, voids, headThickness); + } + if (glyph == 0 || glyph == ' ') { + line.append(' '); + continue; + } + String glyphTone = splashTone.tone(glyph); + if (!glyphTone.isEmpty() && !glyphTone.equals(activeTone)) { + line.append(glyphTone); + activeTone = glyphTone; + } + line.append(glyph); + } + lines[y] = line.toString(); + } + return lines; + } + + private static char splashEyeGlyph(int x, int y) { + if (y == 5 && Math.abs(x - 26) <= 1) { + if (x == 25) { + return '('; + } + if (x == 27) { + return ')'; + } + return ' '; + } + double radius = splashEyeRadius(x, y); + if (radius <= 0.50) { + return '@'; + } + if (radius <= 0.72) { + return '%'; + } + if (radius <= 0.88) { + return '*'; + } + return 0; + } + + private static char splashFinGlyph(int x, int y, double[][][] spines, double[][][] voids, double headThickness) { + if (splashEyeRadius(x, y) <= 1.26) { + return 0; + } + double voidDistance = Double.MAX_VALUE; + for (double[][] notch : voids) { + for (double[] sample : notch) { + double voidRadius = Math.max(0.25, 0.95 - 0.65 * sample[2]); + double dx = (x - sample[0]) / SPLASH_ASPECT; + double dy = y - sample[1]; + double distance = Math.sqrt(dx * dx + dy * dy) / voidRadius; + if (distance < voidDistance) { + voidDistance = distance; + } + } + } + if (voidDistance < 1.0) { + return 0; + } + double best = Double.MAX_VALUE; + double bestT = 0.0; + for (double[][] spine : spines) { + for (int i = 0; i < spine.length; i++) { + double t = i / (double) (spine.length - 1); + double thickness = headThickness * Math.pow(1.0 - t, 1.3) + 0.6; + double dx = (x - spine[i][0]) / SPLASH_ASPECT; + double dy = y - spine[i][1]; + double distance = Math.sqrt(dx * dx + dy * dy) / thickness; + if (distance < best) { + best = distance; + bestT = t; + } + } + } + double shade = best + + Math.max(0.0, 3.0 - Math.min(x, SPLASH_WIDTH - 1 - x)) * 0.12 + + Math.max(0.0, 0.10 - bestT) * 1.5 + + Math.max(0.0, 1.30 - voidDistance) * 0.15; + if (shade > 1.0) { + return 0; + } + int level = shade <= 0.70 ? 0 : shade <= 0.90 ? 1 : 2; + if (bestT > 0.97) { + level += 2; + } else if (bestT > 0.88) { + level += 1; + } + return switch (Math.min(level, 2)) { + case 0 -> '#'; + case 1 -> '='; + default -> '-'; + }; + } + + private static double splashEyeRadius(int x, int y) { + double dx = (x - SPLASH_CENTER_X) / SPLASH_ASPECT; + double dy = y - SPLASH_CENTER_Y; + double nx = dx / SPLASH_EYE_A; + double ny = dy / SPLASH_EYE_B; + return Math.sqrt(nx * nx + ny * ny); + } + + private static double[][][] splashNotchVoids(double notchX) { + double[][] control = {{notchX + 0.8, 12.0}, {notchX, 9.6}, {notchX - 1.4, 7.8}}; + int samples = 31; + double[][] left = new double[samples][3]; + double[][] right = new double[samples][3]; + for (int i = 0; i < samples; i++) { + double t = i / (double) (samples - 1); + double u = 1.0 - t; + double bx = u * u * control[0][0] + 2.0 * u * t * control[1][0] + t * t * control[2][0]; + double by = u * u * control[0][1] + 2.0 * u * t * control[1][1] + t * t * control[2][1]; + left[i][0] = bx; + left[i][1] = by; + left[i][2] = t; + right[i][0] = (SPLASH_WIDTH - 1.0) - bx; + right[i][1] = (SPLASH_HEIGHT - 1.0) - by; + right[i][2] = t; + } + return new double[][][]{left, right}; + } + + private static double[][][] splashFinSpines(double tipReach) { + double[][] control = {{-1.5, 9.8}, {0.4, 4.4}, {5.2, 1.0}, {tipReach, 0.4}}; + int samples = 61; + double[][] left = new double[samples][2]; + double[][] right = new double[samples][2]; + for (int i = 0; i < samples; i++) { + double t = i / (double) (samples - 1); + double u = 1.0 - t; + double bx = u * u * u * control[0][0] + 3.0 * u * u * t * control[1][0] + 3.0 * u * t * t * control[2][0] + t * t * t * control[3][0]; + double by = u * u * u * control[0][1] + 3.0 * u * u * t * control[1][1] + 3.0 * u * t * t * control[2][1] + t * t * t * control[3][1]; + left[i][0] = bx; + left[i][1] = by; + right[i][0] = (SPLASH_WIDTH - 1.0) - bx; + right[i][1] = (SPLASH_HEIGHT - 1.0) - by; + } + return new double[][][]{left, right}; + } + + public interface SplashTone { + String tone(char glyph); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java new file mode 100644 index 000000000..53a1854d2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java @@ -0,0 +1,676 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KList; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Predicate; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class BulkStructureImporter { + public record Report( + int total, + int imported, + int skipped, + int failed, + Map successfulBundles, + boolean retryRequired, + Set captureCandidates + ) { + public Report(int total, int imported, int skipped, int failed) { + this(total, imported, skipped, failed, Map.of(), false, Set.of()); + } + + public Report(int total, int imported, int skipped, int failed, Map successfulBundles) { + this(total, imported, skipped, failed, successfulBundles, false, Set.of()); + } + + public Report(int total, int imported, int skipped, int failed, + Map successfulBundles, boolean retryRequired) { + this(total, imported, skipped, failed, successfulBundles, retryRequired, Set.of()); + } + + public Report { + successfulBundles = Collections.unmodifiableMap(new TreeMap<>(successfulBundles)); + captureCandidates = Collections.unmodifiableSet(new TreeSet<>(captureCandidates)); + } + } + + private BulkStructureImporter() { + } + + public static Report importAllVanilla(IrisData data, StructureImporter.Mode mode, boolean includeNonJigsaw, VolmitSender sender) { + KList keys = INMS.get().getStructureKeys(); + List vanilla = new ArrayList<>(); + for (String k : keys) { + if (k != null && !k.isBlank()) { + vanilla.add(k); + } + } + Collections.sort(vanilla); + + int total = vanilla.size(); + int imported = 0; + int skipped = 0; + int failed = 0; + TreeSet captureCandidates = new TreeSet<>(); + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW, MessageArgument.untrusted("total", String.valueOf(total)), MessageArgument.untrusted("mode", String.valueOf(mode)), MessageArgument.untrusted("includeNonJigsaw", String.valueOf(includeNonJigsaw)))); + + for (String keyString : vanilla) { + NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase()); + if (nk == null) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY, MessageArgument.untrusted("keyString", String.valueOf(keyString)))); + continue; + } + String name = StructureImporter.deriveName(nk); + + try { + VillageImporter.Result jigsaw = VillageImporter.importVillage(data, nk, name, mode); + if (jigsaw.success()) { + imported++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_JIGSAW, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("name", String.valueOf(name)))); + continue; + } + + String message = jigsaw.message() == null ? "" : jigsaw.message(); + if (message.contains("is not a jigsaw structure")) { + if (!includeNonJigsaw) { + skipped++; + continue; + } + StructureImporter.Result single = StructureImporter.importStructure(data, nk, name, mode); + if (single.success()) { + imported++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_SINGLE, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("name", String.valueOf(name)))); + } else if (single.message() != null && single.message().startsWith("Skipped")) { + skipped++; + } else if (isCaptureCandidate(message, single.message())) { + skipped++; + captureCandidates.add(nk.toString()); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_SKIP_NO_SINGLE_TEMPLATE_NBT_VANILLA_BUILDS_THIS_CODE_FROM_SEPARATE_PIECE, MessageArgument.untrusted("keyString", String.valueOf(keyString)))); + } else { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("message", String.valueOf(single.message())))); + } + continue; + } + + if (message.startsWith("Skipped")) { + skipped++; + continue; + } + + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_2, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("message", String.valueOf(message)))); + } catch (Throwable e) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_3, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("error", String.valueOf(e.getMessage())))); + } + } + + StructureIndexService.write(data); + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_BULK_IMPORT_COMPLETE_IMPORTED_SKIPPED_FAILED_TOTAL, MessageArgument.untrusted("imported", String.valueOf(imported)), MessageArgument.untrusted("skipped", String.valueOf(skipped)), MessageArgument.untrusted("failed", String.valueOf(failed)), MessageArgument.untrusted("total", String.valueOf(total)))); + return new Report(total, imported, skipped, failed, Map.of(), false, captureCandidates); + } + + public static Report importTemplateGroups(IrisData data, StructureImporter.Mode mode, VolmitSender sender) { + String[][] groups = { + {"shipwreck", "minecraft:shipwreck", "shipwreck"}, + {"ruined_portal", "minecraft:ruined_portal", "ruined_portal"}, + {"ocean_ruin", "minecraft:ocean_ruin_cold", "underwater_ruin"}, + {"nether_fossil", "minecraft:nether_fossil", "nether_fossils"}, + }; + + int imported = 0; + int skipped = 0; + int failed = 0; + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_BUILDING_SINGLE_TEMPLATE_STRUCTURES_FROM_IMPORTED_PIECES_ONE_VARIANT_PLACED_PER_GENERATION)); + for (String[] g : groups) { + try { + StructureImporter.Result result = StructureImporter.importTemplateGroup(data, g[0], g[1], g[2], mode); + if (result.success()) { + imported++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_GROUP_VARIANTS, MessageArgument.untrusted("value", String.valueOf(g[1])), MessageArgument.untrusted("value2", String.valueOf(g[0])), MessageArgument.untrusted("blocks", String.valueOf(result.blocks())))); + } else if (result.message() != null && result.message().startsWith("Skipped")) { + skipped++; + } else { + skipped++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_SKIP, MessageArgument.untrusted("value", String.valueOf(g[0])), MessageArgument.untrusted("message", String.valueOf(result.message())))); + } + } catch (Throwable e) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_4, MessageArgument.untrusted("value", String.valueOf(g[0])), MessageArgument.untrusted("error", String.valueOf(e.getMessage())))); + } + } + + StructureIndexService.write(data); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_SINGLE_TEMPLATE_STRUCTURES_BUILT_SKIPPED_FAILED, MessageArgument.untrusted("imported", String.valueOf(imported)), MessageArgument.untrusted("skipped", String.valueOf(skipped)), MessageArgument.untrusted("failed", String.valueOf(failed)))); + return new Report(groups.length, imported, skipped, failed); + } + + public static Report importAllTemplates(IrisData data, StructureImporter.Mode mode, VolmitSender sender) { + List templateKeys; + try { + templateKeys = enumerateTemplateKeys(); + } catch (Throwable e) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAILED_ENUMERATE_STRUCTURE_TEMPLATES_VIA_SERVER_RESOURCEMANAGER, MessageArgument.untrusted("e", String.valueOf(e)))); + return enumerationFailureReport(); + } + + List all = new ArrayList<>(); + for (String key : templateKeys) { + if (key != null && !key.isBlank()) { + all.add(key); + } + } + Collections.sort(all); + + int total = all.size(); + int imported = 0; + int skipped = 0; + int failed = 0; + + if (total == 0) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_NO_STRUCTURE_TEMPLATES_WERE_FOUND_UNDER_STRUCTURE_RESOURCE_PATH)); + return new Report(0, 0, 0, 0); + } + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_IMPORTING_STRUCTURE_TEMPLATES_MODE, MessageArgument.untrusted("total", String.valueOf(total)), MessageArgument.untrusted("mode", String.valueOf(mode)))); + + for (String keyString : all) { + NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase()); + if (nk == null) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY_2, MessageArgument.untrusted("keyString", String.valueOf(keyString)))); + continue; + } + String name = templateNameFor(keyString); + + try { + StructureImporter.Result result = StructureImporter.importStructure(data, nk, name, mode, true); + if (result.success()) { + imported++; + } else if (result.message() != null && result.message().startsWith("Skipped")) { + skipped++; + } else { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_5, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("message", String.valueOf(result.message())))); + } + } catch (Throwable e) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_6, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("error", String.valueOf(e.getMessage())))); + } + + int processed = imported + skipped + failed; + if (processed % 50 == 0) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_IMPORTED_SKIPPED_FAILED, MessageArgument.untrusted("processed", String.valueOf(processed)), MessageArgument.untrusted("total", String.valueOf(total)), MessageArgument.untrusted("imported", String.valueOf(imported)), MessageArgument.untrusted("skipped", String.valueOf(skipped)), MessageArgument.untrusted("failed", String.valueOf(failed)))); + } + } + + StructureIndexService.write(data); + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_TEMPLATE_IMPORT_COMPLETE_IMPORTED_SKIPPED_FAILED_TOTAL, MessageArgument.untrusted("imported", String.valueOf(imported)), MessageArgument.untrusted("skipped", String.valueOf(skipped)), MessageArgument.untrusted("failed", String.valueOf(failed)), MessageArgument.untrusted("total", String.valueOf(total)))); + return new Report(total, imported, skipped, failed); + } + + public static Report importDatapackStructures(IrisData data, StructureImporter.Mode mode, VolmitSender sender) { + return importDatapackStructures( + data, + mode, + sender, + null, + null, + StructureImporter.Ownership.EDITABLE + ); + } + + public static Report importManagedDatapackStructures( + IrisData data, + StructureImporter.Mode mode, + VolmitSender sender, + Set allowedStructureKeys, + Set allowedTemplateKeys + ) { + return importDatapackStructures( + data, + mode, + sender, + allowedStructureKeys, + allowedTemplateKeys, + StructureImporter.Ownership.MANAGED_DATAPACK + ); + } + + private static Report importDatapackStructures( + IrisData data, + StructureImporter.Mode mode, + VolmitSender sender, + Set allowedStructureKeys, + Set allowedTemplateKeys, + StructureImporter.Ownership ownership + ) { + KList keys = INMS.get().getStructureKeys(); + KeySelection structureSelection = selectDatapackKeys(keys, allowedStructureKeys); + List datapack = structureSelection.present(); + + int structureAttempts = structureSelection.total(); + int templateAttempts = 0; + int imported = 0; + int skipped = 0; + int failed = structureSelection.missing().size(); + boolean retryRequired = false; + Map successfulBundles = new TreeMap<>(); + + if (structureAttempts == 0) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_NO_DATAPACK_NON_MINECRAFT_STRUCTURES_ARE_REGISTERED_INGEST_DATAPACK_RESTART_FIRST_THEN)); + } else { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_IMPORTING_DATAPACK_STRUCTURES_MODE, MessageArgument.untrusted("total", String.valueOf(structureAttempts)), MessageArgument.untrusted("mode", String.valueOf(mode)))); + for (String missingKey : structureSelection.missing()) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_8, MessageArgument.untrusted("keyString", missingKey), MessageArgument.untrusted("message", "allowed structure key is absent from the live registry"))); + } + for (String keyString : datapack) { + NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase()); + if (nk == null) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY_3, MessageArgument.untrusted("keyString", String.valueOf(keyString)))); + continue; + } + String name = StructureImporter.deriveName(nk); + + try { + VillageImporter.Result jigsaw = VillageImporter.importVillage( + data, + nk, + name, + mode, + ownership + ); + if (jigsaw.success()) { + imported++; + successfulBundles.put(bundleKey(name), keyString); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_JIGSAW_2, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("name", String.valueOf(name)))); + continue; + } + retryRequired |= jigsaw.retryableFailure(); + + String message = jigsaw.message() == null ? "" : jigsaw.message(); + if (message.contains("is not a jigsaw structure")) { + StructureImporter.Result single = StructureImporter.importStructure( + data, + nk, + name, + mode, + false, + ownership + ); + if (single.success()) { + imported++; + successfulBundles.put(bundleKey(name), keyString); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_SINGLE_2, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("name", String.valueOf(name)))); + } else if (single.message() != null && single.message().startsWith("Skipped")) { + skipped++; + } else if (single.message() != null && single.message().contains("No loadable structure NBT")) { + skipped++; + } else { + failed++; + retryRequired |= single.retryableFailure(); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_7, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("message", String.valueOf(single.message())))); + } + continue; + } + + if (message.startsWith("Skipped")) { + skipped++; + continue; + } + + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_8, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("message", String.valueOf(message)))); + } catch (Throwable e) { + failed++; + retryRequired = true; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_9, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("error", String.valueOf(e.getMessage())))); + } + } + } + + if (allowedTemplateKeys == null || !allowedTemplateKeys.isEmpty()) { + List templateKeys = List.of(); + boolean enumerationSucceeded = false; + try { + templateKeys = enumerateTemplateKeys(); + enumerationSucceeded = true; + } catch (Throwable e) { + templateAttempts++; + failed++; + retryRequired = true; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_COULD_NOT_ENUMERATE_DATAPACK_TEMPLATES_VIA_SERVER_RESOURCEMANAGER, MessageArgument.untrusted("error", String.valueOf(e.getMessage())))); + } + if (enumerationSucceeded) { + KeySelection templateSelection = selectDatapackKeys(templateKeys, allowedTemplateKeys); + List datapackTemplates = templateSelection.present(); + templateAttempts += templateSelection.total(); + failed += templateSelection.missing().size(); + for (String missingKey : templateSelection.missing()) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_10, MessageArgument.untrusted("keyString", missingKey), MessageArgument.untrusted("message", "allowed template key is absent from the live server resources"))); + } + if (!datapackTemplates.isEmpty()) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_IMPORTING_DATAPACK_STRUCTURE_TEMPLATES, MessageArgument.untrusted("size", String.valueOf(datapackTemplates.size())))); + for (String keyString : datapackTemplates) { + NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase()); + if (nk == null) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY_2, MessageArgument.untrusted("keyString", String.valueOf(keyString)))); + continue; + } + String name = templateNameFor(keyString); + try { + StructureImporter.Result result = StructureImporter.importStructure( + data, + nk, + name, + mode, + true, + ownership + ); + if (result.success()) { + imported++; + successfulBundles.put(bundleKey(name), keyString); + } else if (result.message() != null && result.message().startsWith("Skipped")) { + skipped++; + } else { + failed++; + retryRequired |= result.retryableFailure(); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_10, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("message", String.valueOf(result.message())))); + } + } catch (Throwable e) { + failed++; + retryRequired = true; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_FAIL_11, MessageArgument.untrusted("keyString", String.valueOf(keyString)), MessageArgument.untrusted("error", String.valueOf(e.getMessage())))); + } + } + } + } + } + + StructureIndexService.write(data); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.BULK_STRUCTURE_IMPORTER_DATAPACK_STRUCTURE_IMPORT_COMPLETE_IMPORTED_SKIPPED_FAILED, MessageArgument.untrusted("imported", String.valueOf(imported)), MessageArgument.untrusted("skipped", String.valueOf(skipped)), MessageArgument.untrusted("failed", String.valueOf(failed)))); + return datapackReport( + structureAttempts, + templateAttempts, + imported, + skipped, + failed, + successfulBundles, + retryRequired); + } + + static boolean isAllowedDatapackKey(String key, Set allowedKeys) { + return allowedKeys == null ? !key.startsWith("minecraft:") : allowedKeys.contains(key); + } + + static boolean isCaptureCandidate(String jigsawMessage, String singleMessage) { + return jigsawMessage != null + && jigsawMessage.contains("is not a jigsaw structure") + && singleMessage != null + && singleMessage.contains("No loadable structure NBT"); + } + + static KeySelection selectDatapackKeys(Iterable discoveredKeys, Set allowedKeys) { + TreeSet discovered = normalizeKeys(discoveredKeys); + TreeSet present = new TreeSet<>(); + TreeSet missing = new TreeSet<>(); + if (allowedKeys == null) { + for (String key : discovered) { + if (isAllowedDatapackKey(key, null)) { + present.add(key); + } + } + } else { + TreeSet allowed = normalizeKeys(allowedKeys); + for (String key : allowed) { + if (discovered.contains(key)) { + present.add(key); + } else { + missing.add(key); + } + } + } + return new KeySelection(new ArrayList<>(present), new ArrayList<>(missing)); + } + + static Report datapackReport( + int structureAttempts, + int templateAttempts, + int imported, + int skipped, + int failed, + Map successfulBundles + ) { + return new Report(structureAttempts + templateAttempts, imported, skipped, failed, successfulBundles); + } + + static Report datapackReport( + int structureAttempts, + int templateAttempts, + int imported, + int skipped, + int failed, + Map successfulBundles, + boolean retryRequired + ) { + return new Report( + structureAttempts + templateAttempts, + imported, + skipped, + failed, + successfulBundles, + retryRequired); + } + + static Report enumerationFailureReport() { + return new Report(1, 0, 0, 1, Map.of(), true); + } + + public static String templateNameFor(String key) { + int colon = key.indexOf(':'); + String namespace = colon >= 0 ? key.substring(0, colon) : "minecraft"; + String path = colon >= 0 ? key.substring(colon + 1) : key; + String safePath = path.toLowerCase().replaceAll("[^a-z0-9_/-]", "_"); + while (safePath.contains("//")) { + safePath = safePath.replace("//", "/"); + } + if (safePath.startsWith("/")) { + safePath = safePath.substring(1); + } + return "minecraft".equals(namespace) ? safePath : namespace + "/" + safePath; + } + + private static List enumerateTemplateKeys() throws Exception { + Object craftServer = Bukkit.getServer(); + Object dedicated = invoke(craftServer, "getHandle"); + Object server = invoke(dedicated, "getServer"); + Object resourceManager = resolveResourceManager(server); + if (resourceManager == null) { + throw new IllegalStateException("Could not resolve the server ResourceManager via reflection"); + } + + Method listResources = null; + for (Method m : resourceManager.getClass().getMethods()) { + if (m.getName().equals("listResources") && m.getParameterCount() == 2 + && m.getParameterTypes()[0] == String.class + && m.getParameterTypes()[1] == Predicate.class + && Map.class.isAssignableFrom(m.getReturnType())) { + listResources = m; + break; + } + } + if (listResources == null) { + throw new NoSuchMethodException("listResources(String, Predicate) on " + resourceManager.getClass().getName()); + } + listResources.setAccessible(true); + + Predicate endsWithNbt = location -> { + String path = identifierPath(location); + return path != null && path.endsWith(".nbt"); + }; + + Object resultMap = listResources.invoke(resourceManager, "structure", endsWithNbt); + TreeSet keys = new TreeSet<>(); + if (resultMap instanceof Map map) { + for (Object location : map.keySet()) { + String namespace = identifierNamespace(location); + String path = identifierPath(location); + if (namespace == null || path == null) { + continue; + } + String stripped = path; + if (stripped.startsWith("structure/")) { + stripped = stripped.substring("structure/".length()); + } + if (stripped.endsWith(".nbt")) { + stripped = stripped.substring(0, stripped.length() - ".nbt".length()); + } + if (stripped.isEmpty()) { + continue; + } + keys.add(namespace + ":" + stripped); + } + } + return new ArrayList<>(keys); + } + + private static Object resolveResourceManager(Object server) { + try { + Class resourceManagerClass = Class.forName("net.minecraft.server.packs.resources.ResourceManager"); + Method getter = null; + for (Method m : server.getClass().getMethods()) { + if (m.getName().equals("getResourceManager") && m.getParameterCount() == 0 + && resourceManagerClass.isAssignableFrom(m.getReturnType())) { + getter = m; + break; + } + } + if (getter == null) { + for (Method m : server.getClass().getMethods()) { + if (m.getParameterCount() == 0 && resourceManagerClass.isAssignableFrom(m.getReturnType())) { + getter = m; + break; + } + } + } + if (getter == null) { + return null; + } + getter.setAccessible(true); + return getter.invoke(server); + } catch (Throwable e) { + return null; + } + } + + private static TreeSet normalizeKeys(Iterable keys) { + TreeSet normalized = new TreeSet<>(); + for (String key : keys) { + if (key == null || key.isBlank()) { + continue; + } + normalized.add(key.trim().toLowerCase(Locale.ROOT)); + } + return normalized; + } + + private static String bundleKey(String name) { + return "iris:" + name; + } + + private static String identifierNamespace(Object location) { + try { + Method m = location.getClass().getMethod("getNamespace"); + m.setAccessible(true); + Object value = m.invoke(location); + return value == null ? null : value.toString(); + } catch (Throwable e) { + return null; + } + } + + private static String identifierPath(Object location) { + try { + Method m = location.getClass().getMethod("getPath"); + m.setAccessible(true); + Object value = m.invoke(location); + return value == null ? null : value.toString(); + } catch (Throwable e) { + return null; + } + } + + private static Object invoke(Object target, String method) throws Exception { + Class c = target.getClass(); + while (c != null) { + for (Method m : c.getDeclaredMethods()) { + if (m.getName().equals(method) && m.getParameterCount() == 0) { + m.setAccessible(true); + return m.invoke(target); + } + } + c = c.getSuperclass(); + } + for (Method m : target.getClass().getMethods()) { + if (m.getName().equals(method) && m.getParameterCount() == 0) { + m.setAccessible(true); + return m.invoke(target); + } + } + throw new NoSuchMethodException(method + " on " + target.getClass().getName()); + } + + record KeySelection(List present, List missing) { + KeySelection { + present = List.copyOf(present); + missing = List.copyOf(missing); + } + + int total() { + return present.size() + missing.size(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java new file mode 100644 index 000000000..353c39446 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java @@ -0,0 +1,739 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.WorldCreatorCompat; +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.nms.INMS; +import art.arcane.iris.core.pack.AtomicDirectoryPublisher; +import art.arcane.iris.core.runtime.WorldDeletionQueue; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.core.tools.TreePlausibilizer; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.platform.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.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.WorldType; +import org.bukkit.block.Block; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.function.Supplier; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class FeatureImporter { + public record Report(int total, int imported, int skipped, int failed) { + } + + private static final String SCRATCH_WORLD_PREFIX = "iris-feature-import-"; + private static final int SCRATCH_ID_ATTEMPTS = 32; + private static final long SCRATCH_CREATE_TIMEOUT_SECONDS = 120L; + private static final long SCRATCH_TEARDOWN_TIMEOUT_SECONDS = 120L; + private static final int CAPTURE_RADIUS = 16; + private static final int CAPTURE_HEIGHT = 40; + private static final int CELL_STRIDE = 48; + private static final int CELL_COLUMNS = 16; + private static final int PLACE_ATTEMPTS = 6; + private static final long REGION_TIMEOUT_SECONDS = 30L; + private static final Set RESERVED_SCRATCH_NAMES = ConcurrentHashMap.newKeySet(); + private static final ConcurrentHashMap ACTIVE_SCRATCH_WORLDS = new ConcurrentHashMap<>(); + + private FeatureImporter() { + } + + public static Report importAllObjectFeatures(IrisData data, int variants, VolmitSender sender) { + int wantVariants = Math.max(1, variants); + + List rows = parseRows(INMS.get().getObjectFeatureKeys()); + int total = rows.size(); + if (total == 0) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.FEATURE_IMPORTER_NO_VANILLA_TREE_OBJECT_FEATURES_ARE_EXPOSED_BY_ACTIVE_NMS_BINDING_IMPORTING)); + return new Report(0, 0, 0, 0); + } + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.FEATURE_IMPORTER_IMPORTING_VANILLA_TREE_OBJECT_FEATURES_VARIANTS_EACH_INTO_SCRATCH_WORLD, MessageArgument.untrusted("total", String.valueOf(total)), MessageArgument.untrusted("wantVariants", String.valueOf(wantVariants)))); + + World scratch = createScratchWorld(sender); + if (scratch == null) { + return new Report(total, 0, 0, total); + } + + int imported = 0; + int skipped = 0; + int failed = 0; + int cellIndex = 0; + + try { + for (Row row : rows) { + try { + int written = 0; + Set hashes = new HashSet<>(); + for (int v = 0; v < wantVariants; v++) { + CaptureResult capture = captureOne(scratch, row.key(), v, cellIndex++); + if (capture == null || !capture.placed() || capture.object() == null) { + continue; + } + IrisObject object = capture.object(); + if (object.getBlocks().isEmpty()) { + continue; + } + object.shrinkwrap(); + if (row.group().equals("trees") || row.group().equals("fallen_trees")) { + try { + TreePlausibilizer.apply(object, TreePlausibilizer.seedOf(row.key() + "#" + written), TreePlausibilizer.DEFAULT_REACH); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + long hash = hashOf(object); + if (!hashes.add(hash)) { + continue; + } + write(objectFile(data, row.group(), row.safeName(), written), object); + written++; + } + + if (written > 0) { + imported++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.FEATURE_IMPORTER_OBJ_OBJECTS_VANILLA, MessageArgument.untrusted("key", String.valueOf(row.key())), MessageArgument.untrusted("group", String.valueOf(row.group())), MessageArgument.untrusted("safeName", String.valueOf(row.safeName())), MessageArgument.untrusted("written", String.valueOf(written)))); + } else { + skipped++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.FEATURE_IMPORTER_SKIP_FEATURE_PLACED_NOTHING_AFTER_RETRIES, MessageArgument.untrusted("key", String.valueOf(row.key())))); + } + } catch (Throwable e) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.FEATURE_IMPORTER_FAIL, MessageArgument.untrusted("key", String.valueOf(row.key())), MessageArgument.untrusted("error", String.valueOf(e.getMessage())))); + IrisLogging.reportError(e); + } + + int processed = imported + skipped + failed; + if (processed % 25 == 0) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.FEATURE_IMPORTER_IMPORTED_SKIPPED_FAILED, MessageArgument.untrusted("processed", String.valueOf(processed)), MessageArgument.untrusted("total", String.valueOf(total)), MessageArgument.untrusted("imported", String.valueOf(imported)), MessageArgument.untrusted("skipped", String.valueOf(skipped)), MessageArgument.untrusted("failed", String.valueOf(failed)))); + } + } + } finally { + destroyScratchWorld(scratch, sender); + } + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.FEATURE_IMPORTER_FEATURE_IMPORT_COMPLETE_FEATURES_WRITTEN_SKIPPED_FAILED_TOTAL, MessageArgument.untrusted("imported", String.valueOf(imported)), MessageArgument.untrusted("skipped", String.valueOf(skipped)), MessageArgument.untrusted("failed", String.valueOf(failed)), MessageArgument.untrusted("total", String.valueOf(total)))); + return new Report(total, imported, skipped, failed); + } + + private static List parseRows(KList raw) { + List rows = new ArrayList<>(); + if (raw == null) { + return rows; + } + for (String entry : raw) { + if (entry == null) { + continue; + } + int pipe = entry.indexOf('|'); + if (pipe <= 0 || pipe >= entry.length() - 1) { + continue; + } + String group = entry.substring(0, pipe); + String key = entry.substring(pipe + 1); + if (key.isBlank()) { + continue; + } + rows.add(new Row(group, key, BulkStructureImporter.templateNameFor(key))); + } + rows.sort(Comparator.comparing(r -> r.group() + "/" + r.safeName())); + return rows; + } + + private static CaptureResult captureOne(World world, String featureKey, int variant, int cellIndex) { + int originX = (cellIndex % CELL_COLUMNS) * CELL_STRIDE; + int originZ = (cellIndex / CELL_COLUMNS) * CELL_STRIDE; + int centerX = originX + CELL_STRIDE / 2; + int centerZ = originZ + CELL_STRIDE / 2; + int anchorChunkX = centerX >> 4; + int anchorChunkZ = centerZ >> 4; + + AtomicReference objectRef = new AtomicReference<>(); + AtomicBoolean placedRef = new AtomicBoolean(false); + AtomicReference errorRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + boolean scheduled = J.runRegion(world, anchorChunkX, anchorChunkZ, () -> { + try { + int chunkMinX = (centerX - CAPTURE_RADIUS) >> 4; + int chunkMaxX = (centerX + CAPTURE_RADIUS) >> 4; + int chunkMinZ = (centerZ - CAPTURE_RADIUS) >> 4; + int chunkMaxZ = (centerZ + CAPTURE_RADIUS) >> 4; + for (int cx = chunkMinX; cx <= chunkMaxX; cx++) { + for (int cz = chunkMinZ; cz <= chunkMaxZ; cz++) { + world.getChunkAt(cx, cz); + } + } + + int baseY = world.getHighestBlockYAt(centerX, centerZ) + 1; + + boolean placed = false; + for (int attempt = 0; attempt < PLACE_ATTEMPTS; attempt++) { + if (INMS.get().placeFeature(world, centerX, baseY, centerZ, featureKey, featureSeed(featureKey, variant, attempt))) { + placed = true; + break; + } + } + placedRef.set(placed); + + if (placed) { + int width = CAPTURE_RADIUS * 2 + 1; + int depth = CAPTURE_RADIUS * 2 + 1; + int minX = centerX - CAPTURE_RADIUS; + int minZ = centerZ - CAPTURE_RADIUS; + IrisObject object = new IrisObject(width, CAPTURE_HEIGHT, depth); + for (int dx = 0; dx < width; dx++) { + for (int dy = 0; dy < CAPTURE_HEIGHT; dy++) { + for (int dz = 0; dz < depth; dz++) { + Block block = world.getBlockAt(minX + dx, baseY + dy, minZ + dz); + if (block.getType() == Material.AIR) { + continue; + } + object.setUnsigned(dx, dy, dz, block, false); + } + } + } + objectRef.set(object); + } + + for (int cx = chunkMinX; cx <= chunkMaxX; cx++) { + for (int cz = chunkMinZ; cz <= chunkMaxZ; cz++) { + try { + world.unloadChunk(cx, cz, false); + } catch (Throwable ignored) { + } + } + } + } catch (Throwable e) { + errorRef.set(e); + } finally { + latch.countDown(); + } + }); + + if (!scheduled) { + return null; + } + + try { + if (!latch.await(REGION_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + return null; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + + if (errorRef.get() != null) { + IrisLogging.reportError(errorRef.get()); + return null; + } + return new CaptureResult(placedRef.get(), objectRef.get()); + } + + private static long featureSeed(String key, int variant, int attempt) { + long h = key.hashCode() & 0xFFFFFFFFL; + h = h * 0x9E3779B97F4A7C15L + variant * 0x100000001B3L + attempt * 31L + 0x5DEECE66DL; + return h; + } + + private static long hashOf(IrisObject object) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + object.write(out); + byte[] bytes = out.toByteArray(); + long h = 1125899906842597L; + for (byte b : bytes) { + h = 31 * h + b; + } + return h; + } catch (Throwable e) { + return object.getW() * 73856093L + object.getH() * 19349663L + object.getD(); + } + } + + private static File objectFile(IrisData data, String group, String safeName, int writtenIndex) { + String suffix = writtenIndex == 0 ? "" : "_" + (writtenIndex + 1); + return new File(data.getDataFolder(), "objects/vanilla/" + group + "/" + safeName + suffix + ".iob"); + } + + private static void write(File file, IrisObject object) throws IOException { + File parent = file.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + object.write(file); + } + + static World createScratchWorld(VolmitSender sender) { + LifecycleOperationCoordinator.Lease lease = null; + ScratchWorldReservation reservation = null; + World createdWorld = null; + try { + reservation = reserveScratchWorld(); + lease = LifecycleOperationCoordinator.get().acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_CREATE, + reservation.key().toString() + ); + requireUnusedReservation(reservation); + WorldCreator creator = WorldCreatorCompat.ofKey(reservation.key()) + .environment(World.Environment.NORMAL) + .type(WorldType.FLAT) + .generateStructures(false); + createdWorld = J.sfut(() -> INMS.get().createWorldAsync(creator)) + .thenCompose(Function.identity()) + .get(SCRATCH_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (createdWorld == null) { + throw new IllegalStateException("Scratch world creation returned no world."); + } + if (!reservation.key().equals(WorldIdentity.key(createdWorld))) { + throw new IllegalStateException("Scratch world creation returned an unexpected world identity."); + } + Path createdFolder = createdWorld.getWorldFolder().toPath().toAbsolutePath().normalize(); + if (!reservation.folder().equals(createdFolder)) { + throw new IllegalStateException("Scratch world creation returned an unexpected storage folder."); + } + + String identity = WorldIdentity.serialize(createdWorld); + ScratchWorldState state = new ScratchWorldState(reservation, lease); + if (ACTIVE_SCRATCH_WORLDS.putIfAbsent(identity, state) != null) { + throw new IllegalStateException("Scratch world identity is already active: " + identity); + } + lease = null; + reservation = null; + return createdWorld; + } catch (Throwable e) { + Throwable failure = unwrapFailure(e); + IrisLogging.reportError(failure); + boolean settled = createdWorld != null + && reservation != null + && settleFailedScratchCreation(createdWorld, reservation, failure); + if (reservation != null && (!settled || !matchesReservation(createdWorld, reservation))) { + queueScratchCleanup(reservation.name(), failure); + } + if (containsTimeout(failure)) { + ServerConfigurator.restart("Feature-import scratch world creation timed out for \"" + + (reservation == null ? "unreserved" : reservation.name()) + "\"."); + } + if (sender != null) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.FEATURE_IMPORTER_COULD_NOT_CREATE_SCRATCH_WORLD_FEATURE_IMPORT_SKIPPING_TREE_OBJECT_PASS, MessageArgument.untrusted("error", String.valueOf(failure.getMessage())))); + } + return null; + } finally { + if (reservation != null) { + RESERVED_SCRATCH_NAMES.remove(reservation.name()); + } + if (lease != null) { + lease.close(); + } + } + } + + private static boolean settleFailedScratchCreation( + World world, + ScratchWorldReservation expectedReservation, + Throwable creationFailure + ) { + ScratchWorldReservation actualReservation = reservationForCreatedScratch(world, creationFailure); + if (actualReservation == null) { + return false; + } + + PlatformChunkGenerator generator = null; + try { + generator = IrisToolbelt.access(world); + } catch (Throwable accessFailure) { + creationFailure.addSuppressed(accessFailure); + } + + AtomicBoolean terminalTimeout = new AtomicBoolean(false); + PlatformChunkGenerator capturedGenerator = generator; + CompletableFuture sequence = sequenceScratchTeardown( + () -> WorldLifecycleService.get().unloadAsync(world, false), + () -> WorldIdentity.resolve(actualReservation.key()).isPresent(), + () -> capturedGenerator == null + ? CompletableFuture.completedFuture(null) + : capturedGenerator.closeAsync(), + () -> { + try { + deleteScratchFolder(actualReservation, world); + return CompletableFuture.completedFuture(null); + } catch (IOException deletionFailure) { + return CompletableFuture.failedFuture(deletionFailure); + } + }, + actualReservation.name(), + terminalTimeout::get); + try { + guardScratchTeardown(sequence, terminalTimeout, actualReservation.name()).join(); + if (!expectedReservation.name().equals(actualReservation.name())) { + queueScratchCleanup(expectedReservation.name(), creationFailure); + } + return true; + } catch (Throwable cleanupFailure) { + Throwable cause = unwrapFailure(cleanupFailure); + creationFailure.addSuppressed(cause); + queueScratchCleanup(actualReservation.name(), creationFailure); + if (!expectedReservation.name().equals(actualReservation.name())) { + queueScratchCleanup(expectedReservation.name(), creationFailure); + } + if (containsTimeout(cause) || WorldIdentity.resolve(actualReservation.key()).isPresent()) { + ServerConfigurator.restart("Feature-import scratch cleanup did not reach a safe boundary for \"" + + actualReservation.name() + "\"."); + } + return false; + } + } + + static void destroyScratchWorld(World world, VolmitSender sender) { + if (world == null) { + return; + } + + String identity = WorldIdentity.serialize(world); + ScratchWorldState state = ACTIVE_SCRATCH_WORLDS.remove(identity); + if (state == null) { + IrisLogging.warn("Refusing to destroy unreserved feature-import scratch world \"" + world.getName() + "\"."); + return; + } + + Throwable failure = null; + try { + PlatformChunkGenerator generator = IrisToolbelt.access(world); + AtomicBoolean terminalTimeout = new AtomicBoolean(false); + CompletableFuture sequence = sequenceScratchTeardown( + () -> WorldLifecycleService.get().unloadAsync(world, false), + () -> WorldIdentity.resolve(state.reservation().key()).isPresent(), + () -> generator == null ? CompletableFuture.completedFuture(null) : generator.closeAsync(), + () -> { + try { + deleteScratchFolder(state, world); + return CompletableFuture.completedFuture(null); + } catch (IOException deletionFailure) { + return CompletableFuture.failedFuture(deletionFailure); + } + }, + state.reservation().name(), + terminalTimeout::get + ); + guardScratchTeardown(sequence, terminalTimeout, state.reservation().name()).join(); + } catch (Throwable e) { + failure = unwrapFailure(e); + queueScratchCleanup(state.reservation().name(), failure); + if (containsTimeout(failure) || WorldIdentity.resolve(state.reservation().key()).isPresent()) { + ServerConfigurator.restart("Feature-import scratch cleanup did not reach a safe boundary for \"" + + state.reservation().name() + "\"."); + } + IrisLogging.reportError("Feature-import scratch world cleanup failed for \"" + + state.reservation().name() + "\"; startup cleanup was queued.", failure); + } finally { + RESERVED_SCRATCH_NAMES.remove(state.reservation().name()); + state.lease().close(); + } + if (failure != null && sender != null) { + sender.sendMessage("Feature-import scratch world cleanup was deferred until the next startup: " + + failure.getMessage()); + } + } + + private static ScratchWorldReservation reserveScratchWorld() throws IOException { + for (int attempt = 0; attempt < SCRATCH_ID_ATTEMPTS; attempt++) { + String name = SCRATCH_WORLD_PREFIX + UUID.randomUUID(); + if (!RESERVED_SCRATCH_NAMES.add(name)) { + continue; + } + + NamespacedKey key = IrisWorldStorage.managedKeyFromName(name); + Path folder = IrisWorldStorage.requireSafeManagedDimensionRoot(key) + .toPath() + .toAbsolutePath() + .normalize(); + ScratchWorldReservation reservation = new ScratchWorldReservation(name, key, folder); + if (isReservationUnused(reservation)) { + return reservation; + } + RESERVED_SCRATCH_NAMES.remove(name); + } + throw new IOException("Could not reserve a collision-free feature-import scratch world identity."); + } + + private static void requireUnusedReservation(ScratchWorldReservation reservation) throws IOException { + if (!RESERVED_SCRATCH_NAMES.contains(reservation.name()) || !isReservationUnused(reservation)) { + throw new IOException("Feature-import scratch world reservation was claimed before creation: " + + reservation.name()); + } + } + + private static boolean isReservationUnused(ScratchWorldReservation reservation) { + return WorldIdentity.resolve(reservation.key()).isEmpty() + && !Files.exists(reservation.folder(), LinkOption.NOFOLLOW_LINKS) + && !Files.isSymbolicLink(reservation.folder()); + } + + private static boolean matchesReservation(World world, ScratchWorldReservation reservation) { + if (world == null || reservation == null) { + return false; + } + try { + return reservation.key().equals(WorldIdentity.key(world)) + && reservation.folder().equals(world.getWorldFolder().toPath().toAbsolutePath().normalize()); + } catch (Throwable failure) { + return false; + } + } + + private static ScratchWorldReservation reservationForCreatedScratch(World world, Throwable failure) { + try { + NamespacedKey key = WorldIdentity.key(world); + String name = IrisWorldStorage.logicalName(key); + if (!isReservedScratchWorldName(name)) { + failure.addSuppressed(new IllegalStateException( + "Refusing cleanup for unexpected scratch world identity \"" + key + "\".")); + return null; + } + Path folder = IrisWorldStorage.requireSafeManagedDimensionRoot(key) + .toPath() + .toAbsolutePath() + .normalize(); + return new ScratchWorldReservation(name, key, folder); + } catch (Throwable identityFailure) { + failure.addSuppressed(identityFailure); + return null; + } + } + + private static void deleteScratchFolder(ScratchWorldState state, World world) throws IOException { + deleteScratchFolder(state.reservation(), world); + } + + private static void deleteScratchFolder(ScratchWorldReservation reservation, World world) throws IOException { + Path actualFolder = world.getWorldFolder().toPath().toAbsolutePath().normalize(); + if (!reservation.folder().equals(actualFolder)) { + throw new IOException("Scratch world storage changed during import; refusing deletion."); + } + if (WorldIdentity.resolve(reservation.key()).isPresent()) { + throw new IOException("Scratch world is still loaded; refusing deletion."); + } + AtomicDirectoryPublisher.deleteTree(reservation.folder()); + } + + private static void queueScratchCleanup(String worldName, Throwable failure) { + try { + WorldDeletionQueue queue = IrisServices.getOrNull(WorldDeletionQueue.class); + if (queue == null) { + throw new IllegalStateException("World deletion queue is unavailable."); + } + queue.queueExactForStartupDeletion(List.of(worldName)); + } catch (Throwable queueFailure) { + if (failure != null) { + failure.addSuppressed(queueFailure); + } + IrisLogging.reportError("Failed to queue startup cleanup for feature-import scratch world \"" + + worldName + "\".", queueFailure); + } + } + + private static Throwable unwrapFailure(Throwable throwable) { + Throwable cursor = throwable; + while (cursor instanceof CompletionException || cursor instanceof ExecutionException) { + if (cursor.getCause() == null) { + break; + } + cursor = cursor.getCause(); + } + return cursor; + } + + static CompletableFuture sequenceScratchTeardown( + Supplier> unload, + BooleanSupplier stillLoaded, + Supplier> closeGenerator, + Supplier> deleteFolder, + String worldName + ) { + return sequenceScratchTeardown( + unload, + stillLoaded, + closeGenerator, + deleteFolder, + worldName, + () -> false); + } + + static CompletableFuture sequenceScratchTeardown( + Supplier> unload, + BooleanSupplier stillLoaded, + Supplier> closeGenerator, + Supplier> deleteFolder, + String worldName, + BooleanSupplier terminalTimeout + ) { + CompletableFuture unloadFuture; + try { + unloadFuture = unload.get(); + } catch (Throwable failure) { + return CompletableFuture.failedFuture(failure); + } + if (unloadFuture == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Scratch world unload returned no completion future.")); + } + + return unloadFuture.thenCompose(unloaded -> { + if (terminalTimeout.getAsBoolean()) { + return CompletableFuture.failedFuture(new TimeoutException( + "Scratch world cleanup stopped after its terminal timeout.")); + } + if (!Boolean.TRUE.equals(unloaded) || stillLoaded.getAsBoolean()) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Scratch world unload was not confirmed for \"" + worldName + "\".")); + } + return invokeScratchPhase(closeGenerator, "generator close"); + }).thenCompose(ignored -> { + if (terminalTimeout.getAsBoolean()) { + return CompletableFuture.failedFuture(new TimeoutException( + "Scratch world cleanup stopped after its terminal timeout.")); + } + return invokeScratchPhase(deleteFolder, "folder deletion"); + }); + } + + private static CompletableFuture guardScratchTeardown( + CompletableFuture source, + AtomicBoolean terminalTimeout, + String worldName + ) { + CompletableFuture guarded = new CompletableFuture<>(); + AtomicBoolean settled = new AtomicBoolean(false); + source.whenComplete((ignored, throwable) -> { + if (!settled.compareAndSet(false, true)) { + return; + } + if (throwable == null) { + guarded.complete(null); + } else { + guarded.completeExceptionally(throwable); + } + }); + CompletableFuture.delayedExecutor(SCRATCH_TEARDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS).execute(() -> { + if (!settled.compareAndSet(false, true)) { + return; + } + terminalTimeout.set(true); + TimeoutException timeout = new TimeoutException( + "Scratch world cleanup did not settle within " + SCRATCH_TEARDOWN_TIMEOUT_SECONDS + + " seconds for \"" + worldName + "\"."); + ServerConfigurator.restart("Feature-import scratch cleanup timed out for \"" + worldName + "\"."); + guarded.completeExceptionally(timeout); + }); + return guarded; + } + + private static CompletableFuture invokeScratchPhase( + Supplier> phase, + String phaseName + ) { + try { + CompletableFuture future = phase.get(); + if (future == null) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Scratch world " + phaseName + " returned no completion future.")); + } + return future; + } catch (Throwable failure) { + return CompletableFuture.failedFuture(failure); + } + } + + private static boolean containsTimeout(Throwable throwable) { + Throwable cursor = throwable; + while (cursor != null) { + if (cursor instanceof TimeoutException) { + return true; + } + cursor = cursor.getCause(); + } + return false; + } + + static boolean isReservedScratchWorldName(String worldName) { + if (worldName == null || !worldName.startsWith(SCRATCH_WORLD_PREFIX)) { + return false; + } + try { + String identifier = worldName.substring(SCRATCH_WORLD_PREFIX.length()); + return UUID.fromString(identifier).toString().equals(identifier); + } catch (IllegalArgumentException failure) { + return false; + } + } + + private record Row(String group, String key, String safeName) { + } + + private record CaptureResult(boolean placed, IrisObject object) { + } + + private record ScratchWorldReservation(String name, NamespacedKey key, Path folder) { + } + + private record ScratchWorldState( + ScratchWorldReservation reservation, + LifecycleOperationCoordinator.Lease lease + ) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureCaptureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/StructureCaptureImporter.java new file mode 100644 index 000000000..c706bc814 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureCaptureImporter.java @@ -0,0 +1,262 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class StructureCaptureImporter { + public record Report(int total, int imported, int skipped, int failed) { + } + + private static final int MAX_SPAN = 48; + private static final int CELL_STRIDE = 128; + private static final int CELL_COLUMNS = 8; + private static final long REGION_TIMEOUT_SECONDS = 30L; + + private StructureCaptureImporter() { + } + + public static Report importAllStructures(IrisData data, StructureImporter.Mode mode, VolmitSender sender) { + return importStructures(data, mode, sender, null); + } + + public static Report importStructures(IrisData data, StructureImporter.Mode mode, VolmitSender sender, + Set allowedStructureKeys) { + StructureImporter.Mode activeMode = mode == null ? StructureImporter.Mode.ADD_ONLY : mode; + if (!INMS.get().supportsStructureCapture()) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_STRUCTURE_CAPTURE_IS_NOT_SUPPORTED_BY_ACTIVE_NMS_BINDING_SKIPPING_CAPTURE_PASS)); + return new Report(0, 0, 0, 0); + } + + KList keys = INMS.get().getStructureKeys(); + if (keys == null || keys.isEmpty()) { + return new Report(0, 0, 0, 0); + } + + ArrayList targets = new ArrayList<>(); + for (String key : selectCaptureKeys(keys, allowedStructureKeys)) { + String name = StructureImporter.deriveName(key); + File structureFile = new File(data.getDataFolder(), "structures/" + name + ".json"); + if (structureFile.exists() && activeMode != StructureImporter.Mode.OVERWRITE) { + continue; + } + targets.add(key); + } + + int total = targets.size(); + if (total == 0) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_NO_CODE_GENERATED_STRUCTURES_LEFT_CAPTURE_EVERYTHING_IS_ALREADY_IMPORTED_AS_STRUCTURE)); + return new Report(0, 0, 0, 0); + } + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_CAPTURING_CODE_GENERATED_STRUCTURES_NO_NBT_TEMPLATE_INTO_SCRATCH_WORLD_SKIPPING_ANY, MessageArgument.untrusted("total", String.valueOf(total)), MessageArgument.untrusted("MAXSPAN", String.valueOf(MAX_SPAN)))); + + World scratch = FeatureImporter.createScratchWorld(sender); + if (scratch == null) { + return new Report(total, 0, 0, total); + } + + int imported = 0; + int skipped = 0; + int failed = 0; + int cellIndex = 0; + + try { + for (String key : targets) { + String name = StructureImporter.deriveName(key); + try { + IrisObject object = captureOne(scratch, key, cellIndex++); + if (object == null || object.getBlocks().isEmpty()) { + skipped++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_SKIP_DID_NOT_PLACE_CAPTURABLE_STRUCTURE_HERE_TOO_LARGE_WRONG_DIMENSION_NO, MessageArgument.untrusted("key", String.valueOf(key)))); + continue; + } + object.shrinkwrap(); + StructureImporter.Result result = StructureImporter.writeSinglePieceStructure( + data, name, key, object, "CENTER_HEIGHT", activeMode); + if (!result.success()) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_FAIL, MessageArgument.untrusted("key", String.valueOf(key)), MessageArgument.untrusted("message", String.valueOf(result.message())))); + continue; + } + imported++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_CAPTURE_OBJECTS_IOB_X_X, MessageArgument.untrusted("key", String.valueOf(key)), MessageArgument.untrusted("name", String.valueOf(name)), MessageArgument.untrusted("w", String.valueOf(object.getW())), MessageArgument.untrusted("h", String.valueOf(object.getH())), MessageArgument.untrusted("d", String.valueOf(object.getD())))); + } catch (Throwable e) { + failed++; + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_FAIL_2, MessageArgument.untrusted("key", String.valueOf(key)), MessageArgument.untrusted("error", String.valueOf(e.getMessage())))); + IrisLogging.reportError(e); + e.printStackTrace(); + } + + int processed = imported + skipped + failed; + if (processed % 10 == 0) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_CAPTURED_SKIPPED_FAILED, MessageArgument.untrusted("processed", String.valueOf(processed)), MessageArgument.untrusted("total", String.valueOf(total)), MessageArgument.untrusted("imported", String.valueOf(imported)), MessageArgument.untrusted("skipped", String.valueOf(skipped)), MessageArgument.untrusted("failed", String.valueOf(failed)))); + } + } + } finally { + FeatureImporter.destroyScratchWorld(scratch, sender); + } + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STRUCTURE_CAPTURE_IMPORTER_STRUCTURE_CAPTURE_COMPLETE_CAPTURED_SKIPPED_FAILED_TOTAL, MessageArgument.untrusted("imported", String.valueOf(imported)), MessageArgument.untrusted("skipped", String.valueOf(skipped)), MessageArgument.untrusted("failed", String.valueOf(failed)), MessageArgument.untrusted("total", String.valueOf(total)))); + return new Report(total, imported, skipped, failed); + } + + static List selectCaptureKeys(Iterable keys, Set allowedStructureKeys) { + TreeSet allowed = null; + if (allowedStructureKeys != null) { + allowed = normalizeKeys(allowedStructureKeys); + } + TreeSet selected = new TreeSet<>(); + for (String key : keys) { + if (key == null || key.isBlank()) { + continue; + } + String normalized = key.trim().toLowerCase(Locale.ROOT); + if (allowed == null || allowed.contains(normalized)) { + selected.add(normalized); + } + } + return new ArrayList<>(selected); + } + + private static TreeSet normalizeKeys(Iterable keys) { + TreeSet normalized = new TreeSet<>(); + for (String key : keys) { + if (key == null || key.isBlank()) { + continue; + } + normalized.add(key.trim().toLowerCase(Locale.ROOT)); + } + return normalized; + } + + private static IrisObject captureOne(World world, String key, int cellIndex) { + int originX = (cellIndex % CELL_COLUMNS) * CELL_STRIDE; + int originZ = (cellIndex / CELL_COLUMNS) * CELL_STRIDE; + int anchorChunkX = (originX + CELL_STRIDE / 2) >> 4; + int anchorChunkZ = (originZ + CELL_STRIDE / 2) >> 4; + long seed = key.hashCode() * 0x9E3779B97F4A7C15L + cellIndex * 0x100000001B3L + 0x5DEECE66DL; + + AtomicReference objectRef = new AtomicReference<>(); + AtomicReference errorRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + boolean scheduled = J.runRegion(world, anchorChunkX, anchorChunkZ, () -> { + try { + int[] box = INMS.get().placeStructure(world, anchorChunkX, anchorChunkZ, key, seed, MAX_SPAN); + if (box == null) { + return; + } + int width = box[3] - box[0] + 1; + int height = box[4] - box[1] + 1; + int depth = box[5] - box[2] + 1; + if (width <= 0 || height <= 0 || depth <= 0) { + return; + } + IrisObject object = new IrisObject(width, height, depth); + boolean any = false; + for (int dx = 0; dx < width; dx++) { + for (int dy = 0; dy < height; dy++) { + for (int dz = 0; dz < depth; dz++) { + Block block = world.getBlockAt(box[0] + dx, box[1] + dy, box[2] + dz); + if (block.getType() == Material.AIR) { + continue; + } + object.setUnsigned(dx, dy, dz, block, false); + any = true; + } + } + } + if (any) { + objectRef.set(object); + } + + int minCX = box[0] >> 4; + int maxCX = box[3] >> 4; + int minCZ = box[2] >> 4; + int maxCZ = box[5] >> 4; + for (int cx = minCX; cx <= maxCX; cx++) { + for (int cz = minCZ; cz <= maxCZ; cz++) { + try { + world.unloadChunk(cx, cz, false); + } catch (Throwable ignored) { + } + } + } + } catch (Throwable e) { + errorRef.set(e); + } finally { + latch.countDown(); + } + }); + + if (!scheduled) { + throw captureFailure(key, anchorChunkX, anchorChunkZ, + "region task was not accepted", null); + } + + try { + if (!latch.await(REGION_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + throw captureFailure(key, anchorChunkX, anchorChunkZ, + "region task did not complete within " + REGION_TIMEOUT_SECONDS + " seconds", null); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw captureFailure(key, anchorChunkX, anchorChunkZ, + "interrupted while waiting for the region task", e); + } + + Throwable error = errorRef.get(); + if (error != null) { + throw captureFailure(key, anchorChunkX, anchorChunkZ, + "platform placement failed", error); + } + return objectRef.get(); + } + + static IllegalStateException captureFailure(String key, int chunkX, int chunkZ, + String detail, Throwable cause) { + String message = "Structure capture failed for '" + key + "' at chunk " + + chunkX + "," + chunkZ + ": " + detail; + return cause == null ? new IllegalStateException(message) : new IllegalStateException(message, cause); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java new file mode 100644 index 000000000..613577974 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java @@ -0,0 +1,617 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.structure.authoring.IrisStructureBundleFactory; +import art.arcane.iris.core.structure.authoring.StructureBackend; +import art.arcane.iris.core.structure.authoring.StructureCapability; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureLoss; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureSource; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.authoring.StructureWriteMode; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import art.arcane.iris.engine.framework.structure.StructureGraphValidationException; +import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.LegacyTileData; +import art.arcane.iris.spi.IrisLogging; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.structure.Palette; +import org.bukkit.structure.Structure; +import org.bukkit.util.BlockVector; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public final class StructureImporter { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public enum Mode { + OVERWRITE, + ADD_ONLY + } + + enum Ownership { + EDITABLE, + MANAGED_DATAPACK; + + StructureWriteResult write( + IrisData data, + StructureResourceBundle bundle, + StructureWriteMode mode + ) { + StructureTransactionWriter writer = new StructureTransactionWriter(data.getDataFolder().toPath()); + return this == MANAGED_DATAPACK + ? writer.writeManagedDatapack(bundle, mode) + : writer.write(bundle, mode); + } + } + + public record Result( + boolean success, + String message, + int blocks, + List losses, + boolean retryableFailure + ) { + public Result(boolean success, String message, int blocks, List losses) { + this(success, message, blocks, losses, false); + } + + public Result { + losses = List.copyOf(losses); + } + } + + record CapturedStructure( + IrisObject object, + int blocks, + int nonAirBlocks, + int tiles, + int width, + int height, + int depth, + int structureMarkers, + List capabilities, + List losses + ) { + CapturedStructure { + Objects.requireNonNull(object); + capabilities = List.copyOf(capabilities); + losses = List.copyOf(losses); + } + } + + private StructureImporter() { + } + + public static Mode parseMode(String s) { + if (s == null || s.isBlank()) { + return Mode.ADD_ONLY; + } + return switch (s.toLowerCase().replace('-', '_')) { + case "add_only", "addonly", "add" -> Mode.ADD_ONLY; + case "overwrite", "replace" -> Mode.OVERWRITE; + default -> throw new IllegalArgumentException("Unknown structure import mode: " + s); + }; + } + + public static String deriveName(NamespacedKey key) { + return (key.getNamespace() + "_" + key.getKey()).replace('/', '_').replace(':', '_'); + } + + public static String deriveName(String key) { + return key.toLowerCase().replace('/', '_').replace(':', '_'); + } + + public static Result importStructure(IrisData data, NamespacedKey key, String name, Mode mode) { + return importStructure(data, key, name, mode, false, Ownership.EDITABLE); + } + + public static Result importStructure(IrisData data, NamespacedKey key, String name, Mode mode, boolean objectOnly) { + return importStructure(data, key, name, mode, objectOnly, Ownership.EDITABLE); + } + + static Result importStructure( + IrisData data, + NamespacedKey key, + String name, + Mode mode, + boolean objectOnly, + Ownership ownership + ) { + Mode activeMode = mode == null ? Mode.ADD_ONLY : mode; + Structure structure; + try { + structure = Bukkit.getStructureManager().loadStructure(key); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + return new Result(false, "Failed to load structure " + key + ": " + e.getMessage(), 0, List.of(), true); + } + if (structure == null || structure.getPalettes().isEmpty()) { + return new Result(false, "No loadable structure NBT for key " + key + " (jigsaw structures must be imported by their piece keys)", 0, List.of()); + } + + CapturedStructure captured; + try { + captured = captureStructure(structure); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + return new Result(false, "Failed to capture structure " + key + ": " + e.getMessage(), 0, List.of(), true); + } + + IrisObject object = captured.object(); + int count = captured.blocks(); + int tiles = captured.tiles(); + int w = captured.width(); + int h = captured.height(); + int d = captured.depth(); + List losses = new ArrayList<>(captured.losses()); + if (captured.structureMarkers() > 0) { + losses.add(StructureLoss.warning( + StructureCapability.CONNECTORS, + "connectors_not_imported", + captured.structureMarkers() + " jigsaw or structure marker(s) were flattened without importing their structure graph.")); + } + String writeNote = ""; + try { + StructureKey sourceKey = StructureKey.parse(key.toString()); + StructureKey bundleKey = new StructureKey("iris", name); + StructureSource.Kind sourceKind = key.getNamespace().equals("minecraft") + ? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK; + StructureSource source = StructureSource.of(sourceKind, sourceKey); + IrisStructureBundleFactory.SinglePieceOptions options = new IrisStructureBundleFactory.SinglePieceOptions( + bundleKey, source, name, object, Math.max(w, d), "CENTER_HEIGHT", objectOnly, + captured.capabilities(), losses); + StructureResourceBundle bundle = IrisStructureBundleFactory.singlePiece(options); + if (!objectOnly) { + StructureResourceBundleGraphCompiler.requireViable(bundle); + } + StructureWriteMode writeMode = activeMode == Mode.ADD_ONLY + ? StructureWriteMode.ADD_ONLY : StructureWriteMode.OVERWRITE; + StructureWriteResult writeResult = ownership.write(data, bundle, writeMode); + reportWriteFailure(writeResult); + if (!writeResult.successful()) { + return new Result(false, writeFailureMessage(name, activeMode, writeResult), count, losses, + writeResult.failure().isPresent()); + } + if (writeResult.committed()) { + data.invalidateStructureResources(); + } + writeNote = writeResultNote(writeResult); + } catch (StructureGraphValidationException e) { + return new Result(false, "Failed writing import for '" + name + "': " + e.getMessage(), count, losses); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + return new Result(false, "Failed writing import for '" + name + "': " + e.getMessage(), count, losses, + true); + } + + String lossSummary = losses.isEmpty() ? "" : ", " + losses.size() + " fidelity warning(s) recorded"; + return new Result(true, "Imported " + key + " as '" + name + "' (" + count + " blocks, " + tiles + + " tiles, " + w + "x" + h + "x" + d + lossSummary + ")" + writeNote, count, losses); + } + + static CapturedStructure captureStructure(Structure structure) { + Structure activeStructure = Objects.requireNonNull(structure); + if (activeStructure.getPalettes().isEmpty()) { + throw new IllegalArgumentException("Structure has no palettes"); + } + + BlockVector size = activeStructure.getSize(); + int w = Math.max(1, size.getBlockX()); + int h = Math.max(1, size.getBlockY()); + int d = Math.max(1, size.getBlockZ()); + IrisObject object = new IrisObject(w, h, d); + int count = 0; + int nonAirBlocks = 0; + int tiles = 0; + int structureMarkers = 0; + int invalidFinalStates = 0; + Palette palette = activeStructure.getPalettes().get(0); + for (BlockState block : palette.getBlocks()) { + Location loc = block.getLocation(); + int x = loc.getBlockX(); + int y = loc.getBlockY(); + int z = loc.getBlockZ(); + if (x < 0 || y < 0 || z < 0 || x >= w || y >= h || z >= d) { + continue; + } + BlockData blockData = block.getBlockData(); + Material mat = blockData.getMaterial(); + if (mat == Material.STRUCTURE_VOID) { + continue; + } + boolean structural = mat == Material.JIGSAW || mat == Material.STRUCTURE_BLOCK; + if (structural) { + structureMarkers++; + } + if (mat == Material.JIGSAW) { + FinalStateResult finalState = readJigsawFinalState(block); + BlockData resolved = finalState.blockData(); + if (finalState.invalidSourceState()) { + invalidFinalStates++; + } + if (resolved == null || isAir(resolved)) { + continue; + } + blockData = resolved; + } else if (mat == Material.STRUCTURE_BLOCK) { + continue; + } + object.setUnsigned(x, y, z, art.arcane.iris.platform.bukkit.BukkitBlockState.of(blockData)); + count++; + if (!isAir(blockData)) { + nonAirBlocks++; + } + if (!structural) { + LegacyTileData tile = captureTile(block); + if (tile != null) { + object.setUnsignedTile(x, y, z, tile); + tiles++; + } + } + } + + List capabilities = new ArrayList<>(); + capabilities.add(StructureCapability.BLOCKS); + if (tiles > 0) { + capabilities.add(StructureCapability.BLOCK_ENTITIES); + } + return new CapturedStructure( + object, + count, + nonAirBlocks, + tiles, + w, + h, + d, + structureMarkers, + capabilities, + importLosses(activeStructure, structureMarkers, invalidFinalStates) + ); + } + + private static boolean isAir(BlockData data) { + Material m = data.getMaterial(); + return m == Material.AIR || m == Material.CAVE_AIR || m == Material.VOID_AIR; + } + + private static List importLosses( + Structure structure, + int structureMarkers, + int invalidFinalStates + ) { + List losses = new ArrayList<>(); + if (structure.getPaletteCount() > 1) { + losses.add(StructureLoss.warning( + StructureCapability.BLOCKS, + "palette_variants_not_imported", + "Only palette 0 was converted; " + (structure.getPaletteCount() - 1) + " additional palette(s) remain native-only.")); + } + if (structure.getEntityCount() > 0) { + losses.add(StructureLoss.warning( + StructureCapability.ENTITIES, + "entities_not_imported", + structure.getEntityCount() + " structure entit" + (structure.getEntityCount() == 1 ? "y was" : "ies were") + + " not converted into the Iris snapshot.")); + } + if (structureMarkers > 0) { + losses.add(StructureLoss.warning( + StructureCapability.BLOCKS, + "structure_markers_resolved", + structureMarkers + " jigsaw or structure marker(s) were resolved to final blocks or omitted from the Iris snapshot.")); + } + if (invalidFinalStates > 0) { + losses.add(StructureLoss.warning( + StructureCapability.BLOCKS, + "invalid_jigsaw_final_state", + invalidFinalStates + " jigsaw final-state value(s) were invalid and omitted from the Iris snapshot.")); + } + return losses; + } + + private static void reportWriteFailure(StructureWriteResult result) { + result.failure().ifPresent((Throwable failure) -> { + IrisLogging.reportError(failure); + failure.printStackTrace(); + }); + } + + private static String writeResultNote(StructureWriteResult result) { + return result.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED + ? " (committed; staging cleanup is required, see console)" : ""; + } + + private static String writeFailureMessage(String name, Mode mode, StructureWriteResult result) { + if (result.status() == StructureWriteResult.Status.ADD_ONLY_CONFLICT) { + return "Skipped (add-only): '" + name + "' already exists"; + } + if (!result.conflicts().isEmpty()) { + StructureWriteResult.Conflict conflict = result.conflicts().getFirst(); + return "Import conflict for '" + name + "': " + conflict.relativePath() + " is " + + conflict.reason().name().toLowerCase() + ". Existing authored files were preserved."; + } + String failure = result.failure().map(Throwable::getMessage).orElse(result.status().name()); + return "Failed writing import for '" + name + "' in " + mode.name().toLowerCase() + " mode: " + failure; + } + + private static FinalStateResult readJigsawFinalState(BlockState block) { + String finalState; + try { + Object nbt = block.getClass().getMethod("getSnapshotNBT").invoke(block); + if (nbt == null) { + return new FinalStateResult(null, false); + } + Object res = nbt.getClass().getMethod("getString", String.class).invoke(nbt, "final_state"); + finalState = null; + if (res instanceof String s) { + finalState = s; + } else if (res instanceof Optional o && o.isPresent()) { + finalState = String.valueOf(o.get()); + } + if (finalState == null || finalState.isBlank()) { + return new FinalStateResult(null, false); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + if (VillageImporter.shouldPrintFullTrace(e)) { + e.printStackTrace(); + } + return new FinalStateResult(null, false); + } + try { + return new FinalStateResult(Bukkit.createBlockData(normalizeJigsawFinalState(finalState)), false); + } catch (IllegalArgumentException e) { + return new FinalStateResult(null, true); + } + } + + static String normalizeJigsawFinalState(String finalState) { + if (finalState == null) { + return null; + } + int propertiesStart = finalState.indexOf('['); + String blockId = propertiesStart < 0 ? finalState : finalState.substring(0, propertiesStart); + if (blockId.equals("minecraft:chisled_polished_blackstone")) { + finalState = "minecraft:chiseled_polished_blackstone" + + (propertiesStart < 0 ? "" : finalState.substring(propertiesStart)); + propertiesStart = finalState.indexOf('['); + } + if (propertiesStart < 0 || !finalState.substring(0, propertiesStart).endsWith("_slab") + || finalState.contains("type=")) { + return finalState; + } + return finalState.replaceAll("([\\[,])half=(top|bottom)(?=[,\\]])", "$1type=$2"); + } + + private record FinalStateResult(BlockData blockData, boolean invalidSourceState) { + } + + private static LegacyTileData captureTile(BlockState block) { + try { + return LegacyTileData.fromBukkit(block); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + return null; + } + } + + public static Result importTemplateGroup(IrisData data, String groupName, String vanillaSource, String prefix, Mode mode) { + Mode activeMode = mode == null ? Mode.ADD_ONLY : mode; + File objectsRoot = new File(data.getDataFolder(), "objects"); + File prefixDir = new File(objectsRoot, prefix); + if (!prefixDir.isDirectory()) { + return new Result(false, "No imported templates under objects/" + prefix + " for " + groupName, 0, List.of()); + } + + List iobs = new ArrayList<>(); + collectIob(prefixDir, iobs); + if (iobs.isEmpty()) { + return new Result(false, "No .iob templates under objects/" + prefix + " for " + groupName, 0, List.of()); + } + iobs.sort(Comparator.comparing(File::getAbsolutePath)); + + String rootPath = objectsRoot.getAbsolutePath() + File.separator; + List pieceNames = new ArrayList<>(); + int maxSpan = 1; + for (File iob : iobs) { + String rel = iob.getAbsolutePath().substring(rootPath.length()).replace(File.separatorChar, '/'); + if (rel.endsWith(".iob")) { + rel = rel.substring(0, rel.length() - ".iob".length()); + } + pieceNames.add(rel); + try { + maxSpan = Math.max(maxSpan, readObjectSpan(iob)); + } catch (IOException e) { + IrisLogging.reportError(e); + e.printStackTrace(); + return new Result(false, "Cannot read imported object '" + rel + "': " + e.getMessage(), 0, + List.of(), true); + } + } + + try { + for (String piece : pieceNames) { + File pieceFile = new File(data.getDataFolder(), "jigsaw-pieces/" + piece + ".json"); + if (!pieceFile.isFile()) { + return new Result(false, "Missing imported jigsaw piece '" + piece + + "'; import the source templates before building group '" + groupName + "'", 0, List.of()); + } + } + + StructureKey sourceKey = StructureKey.parse(vanillaSource, "minecraft"); + StructureSource.Kind sourceKind = sourceKey.namespace().equals("minecraft") + ? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK; + StructureResourceBundle bundle = StructureResourceBundle.builder(new StructureKey("iris", groupName)) + .source(StructureSource.of(sourceKind, sourceKey)) + .backend(StructureBackend.IRIS_ASSEMBLY) + .capability(StructureCapability.BLOCKS) + .capability(StructureCapability.IRIS_PLACEMENT) + .textResource("jigsaw-pools/" + groupName + ".json", GSON.toJson(poolJsonMulti(pieceNames))) + .textResource("structures/" + groupName + ".json", + GSON.toJson(structureJson(groupName, vanillaSource, maxSpan))) + .build(); + StructureWriteMode writeMode = activeMode == Mode.ADD_ONLY + ? StructureWriteMode.ADD_ONLY : StructureWriteMode.OVERWRITE; + StructureWriteResult writeResult = new StructureTransactionWriter(data.getDataFolder().toPath()) + .write(bundle, writeMode); + reportWriteFailure(writeResult); + if (!writeResult.successful()) { + return new Result(false, writeFailureMessage(groupName, activeMode, writeResult), 0, List.of(), + writeResult.failure().isPresent()); + } + if (writeResult.committed()) { + data.invalidateStructureResources(); + } + return new Result(true, "Built structure '" + groupName + "' from " + pieceNames.size() + + " template variants" + writeResultNote(writeResult), pieceNames.size(), List.of()); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + return new Result(false, "Failed writing group structure '" + groupName + "': " + e.getMessage(), 0, + List.of(), true); + } + + } + + private static void collectIob(File dir, List out) { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + collectIob(f, out); + } else if (f.getName().endsWith(".iob")) { + out.add(f); + } + } + } + + private static int readObjectSpan(File iob) throws IOException { + try (DataInputStream din = new DataInputStream(new BufferedInputStream(new FileInputStream(iob)))) { + int w = din.readInt(); + int h = din.readInt(); + int d = din.readInt(); + if (w < 1 || h < 1 || d < 1) { + throw new IOException("Invalid IOB dimensions " + w + "x" + h + "x" + d + " in " + iob); + } + return Math.max(w, d); + } + } + + private static Map poolJsonMulti(List pieceNames) { + List pieces = new ArrayList<>(); + for (String name : pieceNames) { + Map entry = new LinkedHashMap<>(); + entry.put("piece", name); + entry.put("weight", 1); + pieces.add(entry); + } + Map pool = new LinkedHashMap<>(); + pool.put("pieces", pieces); + return pool; + } + + public static Result writeSinglePieceStructure( + IrisData data, + String name, + String vanillaSource, + IrisObject object, + String placeMode, + Mode mode + ) { + Mode activeMode = mode == null ? Mode.ADD_ONLY : mode; + int span = Math.max(object.getW(), object.getD()); + try { + StructureKey sourceKey = StructureKey.parse(vanillaSource, "minecraft"); + StructureSource.Kind sourceKind = sourceKey.namespace().equals("minecraft") + ? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK; + IrisStructureBundleFactory.SinglePieceOptions options = new IrisStructureBundleFactory.SinglePieceOptions( + new StructureKey("iris", name), + StructureSource.of(sourceKind, sourceKey), + name, + object, + span, + placeMode, + false, + List.of(StructureCapability.BLOCKS, StructureCapability.IRIS_PLACEMENT), + List.of()); + StructureResourceBundle bundle = IrisStructureBundleFactory.singlePiece(options); + StructureResourceBundleGraphCompiler.requireViable(bundle); + StructureWriteMode writeMode = activeMode == Mode.ADD_ONLY + ? StructureWriteMode.ADD_ONLY : StructureWriteMode.OVERWRITE; + StructureWriteResult writeResult = new StructureTransactionWriter(data.getDataFolder().toPath()) + .write(bundle, writeMode); + reportWriteFailure(writeResult); + if (!writeResult.successful()) { + return new Result(false, writeFailureMessage(name, activeMode, writeResult), 0, List.of(), + writeResult.failure().isPresent()); + } + if (writeResult.committed()) { + data.invalidateStructureResources(); + } + return new Result(true, "Captured '" + vanillaSource + "' as '" + name + "'" + + writeResultNote(writeResult), object.getBlocks().size(), List.of()); + } catch (StructureGraphValidationException e) { + return new Result(false, "Failed writing capture for '" + name + "': " + e.getMessage(), 0, List.of()); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + return new Result(false, "Failed writing capture for '" + name + "': " + e.getMessage(), 0, List.of(), + true); + } + } + + private static Map structureJson(String name, String source, int maxSpan) { + Map root = new LinkedHashMap<>(); + root.put("startPool", name); + root.put("maxDepth", 1); + root.put("maxSizeChunks", Math.max(1, (maxSpan / 16) + 1)); + root.put("placeMode", "CENTER_HEIGHT"); + root.put("vanillaSource", source); + return root; + } + +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java b/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java new file mode 100644 index 000000000..b0a1715cf --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java @@ -0,0 +1,159 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.object.IrisStructure; +import com.google.gson.GsonBuilder; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class StructureIndexService { + private static final String INDEX_PATH = ".iris/structure-index.json"; + private static final String LEGACY_INDEX_PATH = "structures/structure-index.json"; + private static final String LEGACY_INDEX_KEY = "structure-index"; + private static final Set GENERATED = ConcurrentHashMap.newKeySet(); + + private StructureIndexService() { + } + + public static File writeOnce(IrisData data) { + if (data == null || !IrisPlatforms.isBound() || IrisPlatforms.get().structureHooks() == null) { + return null; + } + + File dataFolder = data.getDataFolder(); + String key = dataFolder == null ? null : dataFolder.getAbsolutePath(); + if (key == null || !GENERATED.add(key)) { + return null; + } + + try { + return write(data); + } catch (Throwable e) { + GENERATED.remove(key); + IrisLogging.reportError(e); + return null; + } + } + + public static File write(IrisData data) { + removeLegacyIndex(data); + List structures = IrisPlatforms.get().structureHooks().structureKeys(); + List sets = IrisPlatforms.get().structureHooks().structureSetKeys(); + + List vanilla = new ArrayList<>(); + List datapack = new ArrayList<>(); + for (String k : structures) { + if (k.startsWith("minecraft:")) { + vanilla.add(k); + } else { + datapack.add(k); + } + } + + List vanillaSets = new ArrayList<>(); + List datapackSets = new ArrayList<>(); + for (String k : sets) { + if (k.startsWith("minecraft:")) { + vanillaSets.add(k); + } else { + datapackSets.add(k); + } + } + + List iris = new ArrayList<>(); + if (data.getStructureLoader() != null) { + Collections.addAll(iris, data.getStructureLoader().getPossibleKeys()); + iris.removeIf(LEGACY_INDEX_KEY::equals); + } + + Collections.sort(vanilla); + Collections.sort(datapack); + Collections.sort(vanillaSets); + Collections.sort(datapackSets); + Collections.sort(iris); + + Map structuresNode = new LinkedHashMap<>(); + structuresNode.put("vanilla", vanilla); + structuresNode.put("datapack", datapack); + + Map setsNode = new LinkedHashMap<>(); + setsNode.put("vanilla", vanillaSets); + setsNode.put("datapack", datapackSets); + + Map counts = new LinkedHashMap<>(); + counts.put("vanillaStructures", vanilla.size()); + counts.put("datapackStructures", datapack.size()); + counts.put("structureSets", sets.size()); + counts.put("irisStructures", iris.size()); + + Map root = new LinkedHashMap<>(); + root.put("note", "Generated index of every structure available at runtime. Toggle vanilla/datapack generation via the dimension importedStructures block (by structure key). Place structures via biome/region/dimension structures lists."); + root.put("counts", counts); + root.put("structures", structuresNode); + root.put("structureSets", setsNode); + root.put("iris", iris); + + File file = indexFile(data.getDataFolder()); + try { + file.getParentFile().mkdirs(); + String json = new GsonBuilder().setPrettyPrinting().create().toJson(root); + Files.writeString(file.toPath(), json, StandardCharsets.UTF_8); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + return file; + } + + static File indexFile(File dataFolder) { + return new File(dataFolder, INDEX_PATH); + } + + static File legacyIndexFile(File dataFolder) { + return new File(dataFolder, LEGACY_INDEX_PATH); + } + + static void removeLegacyIndex(IrisData data) { + File legacyFile = legacyIndexFile(data.getDataFolder()); + try { + Files.deleteIfExists(legacyFile.toPath()); + ResourceLoader structureLoader = data.getStructureLoader(); + if (structureLoader != null) { + structureLoader.getLoadCache().invalidate(LEGACY_INDEX_KEY); + structureLoader.clearList(); + } + } catch (Throwable e) { + IrisLogging.reportError("Failed to remove legacy structure index '" + + legacyFile.getAbsolutePath() + "'", e); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureSchemaKeys.java b/core/src/main/java/art/arcane/iris/core/structure/StructureSchemaKeys.java new file mode 100644 index 000000000..ca036489d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureSchemaKeys.java @@ -0,0 +1,57 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.volmlib.util.collection.KList; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; + +public final class StructureSchemaKeys { + private StructureSchemaKeys() { + } + + public static KList collect(Collection structureKeys, Collection jigsawPieceKeys) { + Set pieces = new HashSet<>(); + if (jigsawPieceKeys != null) { + for (String piece : jigsawPieceKeys) { + if (piece != null && !piece.isBlank()) { + pieces.add(piece); + } + } + } + + TreeSet merged = new TreeSet<>(); + if (structureKeys != null) { + for (String key : structureKeys) { + if (key == null || key.isBlank()) { + continue; + } + if (pieces.contains(key)) { + continue; + } + merged.add(key); + } + } + + return new KList<>(merged); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java b/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java new file mode 100644 index 000000000..928fe60d1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java @@ -0,0 +1,1267 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.structure.authoring.StructureBackend; +import art.arcane.iris.core.structure.authoring.StructureCapability; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureLoss; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureSource; +import art.arcane.iris.core.structure.authoring.StructureWriteMode; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import art.arcane.iris.engine.framework.structure.StructureGraphValidationException; +import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler; +import art.arcane.iris.engine.object.IrisJigsawBranchFailurePolicy; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.spi.IrisLogging; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.structure.Structure; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class VillageImporter { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Set PRINTED_FAILURE_SIGNATURES = ConcurrentHashMap.newKeySet(); + + public record Result( + boolean success, + String message, + int pools, + int pieces, + List losses, + boolean retryableFailure + ) { + public Result(boolean success, String message, int pools, int pieces, List losses) { + this(success, message, pools, pieces, losses, false); + } + + public Result { + losses = List.copyOf(losses); + } + } + + private VillageImporter() { + } + + public static Result importVillage(IrisData data, NamespacedKey structureKey, String name, StructureImporter.Mode mode) { + return importVillage(data, structureKey, name, mode, StructureImporter.Ownership.EDITABLE); + } + + static Result importVillage( + IrisData data, + NamespacedKey structureKey, + String name, + StructureImporter.Mode mode, + StructureImporter.Ownership ownership + ) { + StructureImporter.Mode activeMode = mode == null ? StructureImporter.Mode.ADD_ONLY : mode; + List losses = new ArrayList<>(); + boolean retryableFailure = false; + Object server; + Object registryAccess; + Object structureManager; + String writeNote = ""; + try { + Object craftServer = Bukkit.getServer(); + Object dedicated = invoke(craftServer, "getHandle"); + server = invoke(dedicated, "getServer"); + registryAccess = resolveRegistryAccess(server); + structureManager = invoke(server, "getStructureManager"); + } catch (Throwable e) { + reportFailure(e); + return failed("Failed to access server registries via reflection: " + e, losses, true); + } + if (registryAccess == null) { + return failed("Could not resolve RegistryAccess from the server", losses, true); + } + + Object startPool; + int maxDepth; + int maxDistanceFromCenter; + try { + Object structureRegistry = lookupRegistry(registryAccess, "STRUCTURE"); + Object structure = registryGet(structureRegistry, structureKey); + if (structure == null) { + return failed("No structure registered for key " + structureKey, losses); + } + if (!structure.getClass().getName().endsWith("JigsawStructure")) { + return failed("Structure " + structureKey + " is not a jigsaw structure (" + + structure.getClass().getSimpleName() + "); use 'import' for single-template structures", losses); + } + Object startPoolHolder = invoke(structure, "getStartPool"); + startPool = unwrapHolder(startPoolHolder); + maxDepth = readIntMember(structure, "maxDepth"); + maxDistanceFromCenter = readIntMember(structure, "maxDistanceFromCenter"); + } catch (Throwable e) { + reportFailure(e); + return failed("Failed to read jigsaw structure graph: " + e, losses, true); + } + + Object templatePoolRegistry; + java.util.Random random; + try { + templatePoolRegistry = lookupRegistry(registryAccess, "TEMPLATE_POOL"); + random = new java.util.Random(structureKey.hashCode()); + } catch (Throwable e) { + reportFailure(e); + return failed("Failed to access TEMPLATE_POOL registry: " + e, losses, true); + } + + String startPoolKey; + try { + startPoolKey = registryKeyOf(templatePoolRegistry, startPool); + } catch (Throwable e) { + reportFailure(e); + return failed("Could not resolve the start pool key for " + structureKey + ": " + e, losses, true); + } + if (startPoolKey == null) { + return failed("Could not resolve the start pool key for " + structureKey, losses); + } + + Set visitedPools = new HashSet<>(); + Deque poolQueue = new ArrayDeque<>(); + poolQueue.add(startPoolKey); + + Map> emittedPools = new LinkedHashMap<>(); + Map> emittedPieces = new LinkedHashMap<>(); + Map emittedObjects = new LinkedHashMap<>(); + Map importedTemplates = new LinkedHashMap<>(); + Set capabilities = new HashSet<>(); + capabilities.add(StructureCapability.BLOCKS); + capabilities.add(StructureCapability.CONNECTORS); + capabilities.add(StructureCapability.IRIS_PLACEMENT); + List fatalErrors = new ArrayList<>(); + losses.add(StructureLoss.warning( + StructureCapability.NATIVE_PLACEMENT, + "native_placement_settings_not_imported", + "Native jigsaw placement settings other than the start pool, maximum depth, and maximum distance are not represented by the Iris assembly.")); + int pieceBlocks = 0; + int emittedPoolMembers = 0; + + while (!poolQueue.isEmpty()) { + String poolKey = poolQueue.poll(); + if (!visitedPools.add(poolKey)) { + continue; + } + Object pool; + try { + pool = registryGetByKey(templatePoolRegistry, poolKey); + } catch (Throwable e) { + reportFailure(e); + retryableFailure = true; + fatalErrors.add("pool " + poolKey + ": " + e.getMessage()); + continue; + } + if (pool == null) { + fatalErrors.add("pool " + poolKey + " is not registered"); + continue; + } + + String irisPoolName = poolName(name, poolKey); + List pieceEntries = new ArrayList<>(); + + String fallbackKey = null; + try { + Object fallbackHolder = invoke(pool, "getFallback"); + Object fallbackPool = unwrapHolder(fallbackHolder); + fallbackKey = registryKeyOf(templatePoolRegistry, fallbackPool); + } catch (Throwable e) { + reportFailure(e); + retryableFailure = true; + losses.add(StructureLoss.warning( + StructureCapability.CONNECTORS, + "fallback_pool_not_imported", + "The fallback for source pool " + poolKey + " could not be resolved: " + failureDetail(e)) + .affecting("jigsaw-pools/" + irisPoolName + ".json")); + } + if (fallbackKey != null && !fallbackKey.equals(poolKey)) { + poolQueue.add(fallbackKey); + } + + List templates; + try { + templates = (List) invoke(pool, "getTemplates"); + } catch (Throwable e) { + reportFailure(e); + retryableFailure = true; + fatalErrors.add("templates " + poolKey + ": " + e.getMessage()); + templates = List.of(); + } + + for (Object pair : templates) { + Object element; + int weight; + try { + element = invoke(pair, "getFirst"); + Object second = invoke(pair, "getSecond"); + weight = second instanceof Number ? Math.max(1, ((Number) second).intValue()) : 1; + } catch (Throwable e) { + reportFailure(e); + retryableFailure = true; + losses.add(StructureLoss.warning( + StructureCapability.LIST_ELEMENTS, + "pool_entry_not_imported", + "A source entry in pool " + poolKey + " could not be read: " + failureDetail(e)) + .affecting("jigsaw-pools/" + irisPoolName + ".json")); + continue; + } + if (element == null) { + continue; + } + PoolElementResolution elementResolution; + try { + elementResolution = resolvePoolElement(element); + } catch (Throwable e) { + reportFailure(e); + retryableFailure = true; + losses.add(StructureLoss.warning( + StructureCapability.BLOCKS, + "template_location_not_imported", + "A source template location in pool " + poolKey + " could not be read: " + failureDetail(e)) + .affecting("jigsaw-pools/" + irisPoolName + ".json")); + continue; + } + if (elementResolution.omittedElements() > 0) { + losses.add(listElementFallbackLoss(elementResolution, poolKey) + .affecting("jigsaw-pools/" + irisPoolName + ".json")); + } + Object physicalElement = elementResolution.physicalElement(); + String templateLocation = elementResolution.templateLocation(); + if (templateLocation == null) { + String elementType = physicalElement == null + ? element.getClass().getSimpleName() + : physicalElement.getClass().getSimpleName(); + if (elementType.endsWith("EmptyPoolElement")) { + pieceEntries.add(emptyPoolEntry(weight)); + emittedPoolMembers++; + } else { + StructureCapability unsupportedCapability = unsupportedCapability(elementType); + losses.add(StructureLoss.warning( + unsupportedCapability, + "unsupported_pool_element", + "Skipped unsupported " + elementType + " in source pool " + poolKey + ".") + .affecting("jigsaw-pools/" + irisPoolName + ".json")); + } + continue; + } + NamespacedKey pieceNbtKey = NamespacedKey.fromString(templateLocation.toLowerCase()); + if (pieceNbtKey == null) { + fatalErrors.add("invalid piece key " + templateLocation + " in pool " + poolKey); + continue; + } + String irisPieceName = pieceName(name, templateLocation); + + ImportedTemplate importedTemplate = importedTemplates.get(irisPieceName); + if (importedTemplate == null) { + Structure sourceTemplate; + try { + sourceTemplate = Bukkit.getStructureManager().loadStructure(pieceNbtKey); + } catch (Throwable e) { + reportFailure(e); + retryableFailure = true; + fatalErrors.add(templateLocation + ": failed to load structure template: " + failureDetail(e)); + continue; + } + if (sourceTemplate == null || sourceTemplate.getPalettes().isEmpty()) { + fatalErrors.add(templateLocation + ": no loadable structure template was registered"); + continue; + } + + StructureImporter.CapturedStructure captured; + try { + captured = StructureImporter.captureStructure(sourceTemplate); + } catch (Throwable e) { + reportFailure(e); + retryableFailure = true; + fatalErrors.add(templateLocation + ": failed to capture structure template: " + failureDetail(e)); + continue; + } + for (StructureLoss loss : captured.losses()) { + losses.add(loss.affecting("objects/" + irisPieceName + ".iob")); + } + + Connectors result = readConnectors(element, structureManager, random, name, irisPieceName); + retryableFailure |= result.retryableFailure(); + importedTemplate = new ImportedTemplate( + captured.object(), + captured.blocks(), + captured.nonAirBlocks(), + result.json(), + captured.capabilities()); + importedTemplates.put(irisPieceName, importedTemplate); + poolQueue.addAll(result.targetPoolKeys()); + losses.addAll(result.losses()); + } + + PoolMemberNormalization normalization = normalizePoolMember( + poolKey, + irisPoolName, + poolKey.equals(startPoolKey), + templates.size(), + fallbackKey, + templateLocation, + irisPieceName, + weight, + importedTemplate.nonAirBlocks(), + importedTemplate.connectors()); + losses.addAll(normalization.losses()); + if (!emittedPieces.containsKey(irisPieceName)) { + pieceBlocks += importedTemplate.emittedBlocks(normalization); + capabilities.addAll(importedTemplate.emittedCapabilities(normalization)); + if (normalization.disposition() == PoolMemberDisposition.PHYSICAL) { + emittedObjects.put(irisPieceName, importedTemplate.object()); + emittedPieces.put(irisPieceName, pieceJson( + irisPieceName, + importedTemplate.connectors(), + importedTemplate.nonAirBlocks())); + } + } + if (!normalization.poolEntry().isEmpty()) { + pieceEntries.add(normalization.poolEntry()); + emittedPoolMembers++; + } + } + + Map poolJson = new LinkedHashMap<>(); + poolJson.put("pieces", pieceEntries); + if (fallbackKey != null && !fallbackKey.equals(poolKey)) { + poolJson.put("fallback", poolName(name, fallbackKey)); + } + emittedPools.put(irisPoolName, poolJson); + } + + if (!fatalErrors.isEmpty()) { + return failed("Failed to capture the complete graph for " + structureKey + ": " + fatalErrors.getFirst() + + (fatalErrors.size() == 1 ? "" : " (" + (fatalErrors.size() - 1) + " more)"), losses, + retryableFailure); + } + if (emittedPoolMembers == 0) { + return failed("Imported 0 attachable or empty pool members for " + structureKey, losses); + } + for (StructureLoss loss : losses) { + if (loss.capability() == StructureCapability.CONNECTORS) { + capabilities.remove(StructureCapability.CONNECTORS); + break; + } + } + + try { + StructureKey sourceKey = StructureKey.parse(structureKey.toString()); + StructureSource.Kind sourceKind = structureKey.getNamespace().equals("minecraft") + ? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK; + StructureSource source = new StructureSource(sourceKind, sourceKey, Bukkit.getBukkitVersion(), ""); + Map objectResources = new LinkedHashMap<>(); + for (Map.Entry entry : emittedObjects.entrySet()) { + objectResources.put(entry.getKey(), serialize(entry.getValue())); + } + Map rootStructure = structureJson( + structureKey.toString(), + poolName(name, startPoolKey), + maxDepth, + maxDistanceFromCenter); + StructureResourceBundle bundle = buildBundle( + new StructureKey("iris", name), + source, + objectResources, + emittedPieces, + emittedPools, + rootStructure, + capabilities, + losses); + StructureResourceBundleGraphCompiler.requireViable(bundle); + StructureWriteMode writeMode = activeMode == StructureImporter.Mode.OVERWRITE + ? StructureWriteMode.OVERWRITE : StructureWriteMode.ADD_ONLY; + StructureWriteResult writeResult = ownership.write(data, bundle, writeMode); + reportWriteFailure(writeResult); + if (!writeResult.successful()) { + return new Result(false, writeFailureMessage(name, writeResult), emittedPools.size(), + emittedPieces.size(), losses, writeResult.failure().isPresent()); + } + if (writeResult.committed()) { + data.invalidateStructureResources(); + } + writeNote = writeResultNote(writeResult); + } catch (StructureGraphValidationException e) { + return new Result(false, "Failed writing jigsaw resources for '" + name + "': " + e.getMessage(), + emittedPools.size(), emittedPieces.size(), losses, retryableFailure); + } catch (Throwable e) { + reportFailure(e); + return new Result(false, "Failed writing jigsaw resources for '" + name + "': " + e, + emittedPools.size(), emittedPieces.size(), losses, true); + } + + String msg = "Imported jigsaw structure " + structureKey + " as '" + name + "': " + emittedPieces.size() + " pieces, " + emittedPools.size() + " pools, " + pieceBlocks + " blocks"; + if (!losses.isEmpty()) { + msg += " (" + losses.size() + " fidelity warning(s) recorded)"; + } + return new Result(true, msg + writeNote, emittedPools.size(), emittedPieces.size(), losses); + } + + static StructureResourceBundle buildBundle( + StructureKey bundleKey, + StructureSource source, + Map objects, + Map> pieces, + Map> pools, + Map structure, + Set capabilities, + List losses + ) { + StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(bundleKey) + .source(source) + .backend(StructureBackend.IRIS_ASSEMBLY) + .capabilities(capabilities) + .losses(losses); + for (Map.Entry entry : objects.entrySet()) { + bundle.resource("objects/" + entry.getKey() + ".iob", entry.getValue()); + } + for (Map.Entry> entry : pieces.entrySet()) { + bundle.textResource("jigsaw-pieces/" + entry.getKey() + ".json", GSON.toJson(entry.getValue())); + } + for (Map.Entry> entry : pools.entrySet()) { + bundle.textResource("jigsaw-pools/" + entry.getKey() + ".json", GSON.toJson(entry.getValue())); + } + bundle.textResource("structures/" + bundleKey.path() + ".json", GSON.toJson(structure)); + return bundle.build(); + } + + private record Connectors( + List> json, + Set targetPoolKeys, + List losses, + boolean retryableFailure + ) { + } + + record ImportedTemplate( + IrisObject object, + int blocks, + int nonAirBlocks, + List> connectors, + List capabilities + ) { + ImportedTemplate { + connectors = List.copyOf(connectors); + capabilities = List.copyOf(capabilities); + } + + int emittedBlocks(PoolMemberNormalization normalization) { + return normalization.disposition() == PoolMemberDisposition.PHYSICAL ? blocks : 0; + } + + List emittedCapabilities(PoolMemberNormalization normalization) { + return normalization.disposition() == PoolMemberDisposition.PHYSICAL ? capabilities : List.of(); + } + } + + enum PoolMemberDisposition { + PHYSICAL, + EMPTY, + OMITTED + } + + record PoolMemberNormalization( + PoolMemberDisposition disposition, + Map poolEntry, + List losses + ) { + PoolMemberNormalization { + poolEntry = Collections.unmodifiableMap(new LinkedHashMap<>(poolEntry)); + losses = List.copyOf(losses); + } + } + + private static Connectors readConnectors( + Object element, + Object structureManager, + java.util.Random random, + String baseName, + String pieceName + ) { + List> connectors = new ArrayList<>(); + Set targets = new HashSet<>(); + List losses = new ArrayList<>(); + boolean retryableFailure = false; + String affectedResource = "jigsaw-pieces/" + pieceName + ".json"; + try { + Object zero = staticField("net.minecraft.core.BlockPos", "ZERO"); + Object rotationNone = staticField("net.minecraft.world.level.block.Rotation", "NONE"); + Method m = findMethod4(element.getClass(), "getShuffledJigsawBlocks"); + if (m == null) { + losses.add(StructureLoss.warning( + StructureCapability.CONNECTORS, + "connector_extraction_unavailable", + "The source pool element does not expose jigsaw connector extraction on this server version.") + .affecting(affectedResource)); + return new Connectors(connectors, targets, losses, false); + } + m.setAccessible(true); + Object random0 = freshRandomSource(random); + List blocks = (List) m.invoke(element, structureManager, zero, rotationNone, random0); + if (blocks == null) { + losses.add(StructureLoss.warning( + StructureCapability.CONNECTORS, + "connector_extraction_returned_null", + "The source pool element returned no connector collection.") + .affecting(affectedResource)); + return new Connectors(connectors, targets, losses, false); + } + for (Object jigsaw : blocks) { + String[] rawPoolKey = new String[1]; + try { + Map connector = connectorFrom(jigsaw, baseName, rawPoolKey); + connectors.add(connector); + if (rawPoolKey[0] != null && !rawPoolKey[0].isEmpty()) { + targets.add(rawPoolKey[0]); + } + } catch (Throwable e) { + reportFailure(e); + retryableFailure = true; + losses.add(StructureLoss.warning( + StructureCapability.CONNECTORS, + "connector_not_imported", + "A source jigsaw connector could not be converted: " + failureDetail(e)) + .affecting(affectedResource)); + } + } + } catch (Throwable e) { + reportFailure(e); + retryableFailure = true; + losses.add(StructureLoss.warning( + StructureCapability.CONNECTORS, + "connector_extraction_failed", + "Source jigsaw connectors could not be extracted: " + failureDetail(e)) + .affecting(affectedResource)); + } + return new Connectors(connectors, targets, losses, retryableFailure); + } + + private static Map connectorFrom(Object jigsaw, String baseName, String[] rawPoolKeyOut) throws Exception { + Object info = invoke(jigsaw, "info"); + Object pos = invoke(info, "pos"); + Object blockState = invoke(info, "state"); + int x = readInt(pos, "getX"); + int y = readInt(pos, "getY"); + int z = readInt(pos, "getZ"); + + Object poolKey = invoke(jigsaw, "pool"); + String poolId = identifierString(invoke(poolKey, "identifier")); + Object nameId = invoke(jigsaw, "name"); + Object targetId = invoke(jigsaw, "target"); + Object jointType = invoke(jigsaw, "jointType"); + + String front = frontFacing(blockState); + String top = topFacing(blockState); + rawPoolKeyOut[0] = poolId; + + ConnectorMetadata metadata = readConnectorMetadata(jigsaw, info); + return connectorJson( + x, + y, + z, + front, + top, + poolId, + baseName, + identifierString(nameId), + identifierString(targetId), + jointType, + metadata + ); + } + + static Map connectorJson( + int x, + int y, + int z, + String front, + String top, + String poolId, + String baseName, + String nameId, + String targetId, + Object jointType, + ConnectorMetadata metadata + ) { + Map connector = new LinkedHashMap<>(); + Map position = new LinkedHashMap<>(); + position.put("x", x); + position.put("y", y); + position.put("z", z); + connector.put("position", position); + connector.put("direction", irisDirection(front)); + connector.put("top", irisDirection(top)); + connector.put("pool", poolId == null ? "" : poolName(baseName, poolId)); + connector.put("name", nameId); + connector.put("targetName", targetId); + connector.put("joint", jointType != null && jointType.toString().toUpperCase().contains("ALIGN") ? "ALIGNED" : "ROLLABLE"); + connector.put("finalState", metadata.finalState()); + connector.put("selectionPriority", metadata.selectionPriority()); + connector.put("placementPriority", metadata.placementPriority()); + return connector; + } + + static ConnectorMetadata readConnectorMetadata(Object jigsaw, Object info) throws Exception { + Object nbt = invoke(info, "nbt"); + String finalState = readNbtString(nbt, "final_state"); + String normalizedFinalState = finalState == null || finalState.isBlank() + ? "minecraft:air" + : StructureImporter.normalizeJigsawFinalState(finalState); + return new ConnectorMetadata( + normalizedFinalState, + readIntAccessor(jigsaw, "selectionPriority"), + readIntAccessor(jigsaw, "placementPriority") + ); + } + + private static String readNbtString(Object nbt, String key) throws Exception { + if (nbt == null) { + return null; + } + Method method = findMethod(nbt.getClass(), "getString", 1); + if (method == null) { + throw new NoSuchMethodException("getString(String) on " + nbt.getClass().getName()); + } + method.setAccessible(true); + Object value = method.invoke(nbt, key); + if (value instanceof String string) { + return string; + } + if (value instanceof Optional optional) { + return optional.isPresent() ? String.valueOf(optional.get()) : null; + } + return value == null ? null : String.valueOf(value); + } + + private static int readIntAccessor(Object value, String accessor) throws Exception { + Method method = findMethod(value.getClass(), accessor); + if (method == null) { + throw new NoSuchMethodException(accessor + "() on " + value.getClass().getName()); + } + method.setAccessible(true); + Object result = method.invoke(value); + if (result instanceof Number number) { + return number.intValue(); + } + throw new IllegalStateException(accessor + " on " + value.getClass().getName() + " is not numeric"); + } + + private static String frontFacing(Object blockState) throws Exception { + Class jigsawBlock = Class.forName("net.minecraft.world.level.block.JigsawBlock"); + Method getFront = jigsawBlock.getMethod("getFrontFacing", Class.forName("net.minecraft.world.level.block.state.BlockState")); + Object direction = getFront.invoke(null, blockState); + if (direction == null) { + return "north"; + } + Method getName = direction.getClass().getMethod("getName"); + getName.setAccessible(true); + return String.valueOf(getName.invoke(direction)).toLowerCase(); + } + + private static String topFacing(Object blockState) throws Exception { + Class jigsawBlock = Class.forName("net.minecraft.world.level.block.JigsawBlock"); + Method getTop = jigsawBlock.getMethod("getTopFacing", Class.forName("net.minecraft.world.level.block.state.BlockState")); + Object direction = getTop.invoke(null, blockState); + if (direction == null) { + return "up"; + } + Method getName = direction.getClass().getMethod("getName"); + getName.setAccessible(true); + return String.valueOf(getName.invoke(direction)).toLowerCase(); + } + + private static String irisDirection(String front) { + return switch (front) { + case "up" -> "UP_POSITIVE_Y"; + case "down" -> "DOWN_NEGATIVE_Y"; + case "south" -> "SOUTH_POSITIVE_Z"; + case "east" -> "EAST_POSITIVE_X"; + case "west" -> "WEST_NEGATIVE_X"; + default -> "NORTH_NEGATIVE_Z"; + }; + } + + private static String templateLocationOf(Object element) throws Exception { + Method m = findMethod(element.getClass(), "getTemplateLocation"); + if (m == null) { + return null; + } + m.setAccessible(true); + Object id = m.invoke(element); + return identifierString(id); + } + + static PoolElementResolution resolvePoolElement(Object element) throws Exception { + if (element == null) { + return new PoolElementResolution(null, null, 0, 0); + } + if (!element.getClass().getSimpleName().endsWith("ListPoolElement")) { + return new PoolElementResolution(element, templateLocationOf(element), 0, 0); + } + Object rawElements = invoke(element, "getElements"); + if (!(rawElements instanceof List elements)) { + throw new IllegalStateException("getElements on " + element.getClass().getName() + " is not a list"); + } + if (elements.isEmpty()) { + return new PoolElementResolution(null, null, 1, 0); + } + PoolElementResolution primary = resolvePoolElement(elements.getFirst()); + return new PoolElementResolution( + primary.physicalElement(), + primary.templateLocation(), + primary.listLevels() + 1, + primary.omittedElements() + elements.size() - 1 + ); + } + + static StructureLoss listElementFallbackLoss(PoolElementResolution resolution, String poolKey) { + int omitted = resolution.omittedElements(); + String elementLabel = omitted == 1 ? "element" : "elements"; + String levelLabel = resolution.listLevels() == 1 ? "list level" : "nested list levels"; + return StructureLoss.warning( + StructureCapability.LIST_ELEMENTS, + "list_pool_overlays_not_imported", + "Converted the first physical template from a ListPoolElement in source pool " + poolKey + + " and omitted " + omitted + " colocated " + elementLabel + + ", including their processors, across " + resolution.listLevels() + " " + levelLabel + "." + ); + } + + private static Object resolveRegistryAccess(Object server) { + try { + Class frozen = Class.forName("net.minecraft.core.RegistryAccess$Frozen"); + for (Method m : server.getClass().getMethods()) { + if (m.getParameterCount() == 0 && frozen.isAssignableFrom(m.getReturnType())) { + m.setAccessible(true); + Object o = m.invoke(server); + if (o != null) { + return o; + } + } + } + Class ra = Class.forName("net.minecraft.core.RegistryAccess"); + for (Method m : server.getClass().getMethods()) { + if (m.getParameterCount() == 0 && ra.isAssignableFrom(m.getReturnType())) { + m.setAccessible(true); + Object o = m.invoke(server); + if (o != null) { + return o; + } + } + } + } catch (Throwable e) { + reportFailure(e); + } + return null; + } + + private static Object lookupRegistry(Object registryAccess, String registryName) throws Exception { + Class registries = Class.forName("net.minecraft.core.registries.Registries"); + Object resourceKey = registries.getField(registryName).get(null); + Class registryClass = Class.forName("net.minecraft.core.Registry"); + Method registryOverload = null; + for (Method m : registryAccess.getClass().getMethods()) { + if (m.getName().equals("lookupOrThrow") && m.getParameterCount() == 1 + && m.getParameterTypes()[0].getName().endsWith("ResourceKey") + && registryClass.isAssignableFrom(m.getReturnType())) { + registryOverload = m; + break; + } + } + if (registryOverload == null) { + for (Method m : registryAccess.getClass().getMethods()) { + if (m.getName().equals("lookupOrThrow") && m.getParameterCount() == 1 + && m.getParameterTypes()[0].getName().endsWith("ResourceKey")) { + registryOverload = m; + break; + } + } + } + if (registryOverload == null) { + throw new NoSuchMethodException("lookupOrThrow(ResourceKey) on " + registryAccess.getClass().getName()); + } + registryOverload.setAccessible(true); + return registryOverload.invoke(registryAccess, resourceKey); + } + + private static Object registryGet(Object registry, NamespacedKey key) throws Exception { + Object id = identifierOf(key); + for (Method m : registry.getClass().getMethods()) { + if (m.getName().equals("getValue") && m.getParameterCount() == 1 && m.getParameterTypes()[0].getName().endsWith("Identifier")) { + m.setAccessible(true); + return m.invoke(registry, id); + } + } + for (Method m : registry.getClass().getMethods()) { + if (m.getName().equals("getOptional") && m.getParameterCount() == 1 && m.getParameterTypes()[0].getName().endsWith("Identifier")) { + m.setAccessible(true); + return unwrapOptional(m.invoke(registry, id)); + } + } + return null; + } + + private static Object registryGetByKey(Object registry, String key) throws Exception { + NamespacedKey nk = NamespacedKey.fromString(key.toLowerCase()); + if (nk == null) { + return null; + } + return registryGet(registry, nk); + } + + private static String registryKeyOf(Object registry, Object value) throws Exception { + if (value == null) { + return null; + } + for (Method m : registry.getClass().getMethods()) { + if (m.getName().equals("getKey") && m.getParameterCount() == 1) { + m.setAccessible(true); + Object id = m.invoke(registry, value); + String s = identifierString(id); + if (s != null) { + return s; + } + } + } + return null; + } + + private static Object identifierOf(NamespacedKey key) throws Exception { + Class identifier = Class.forName("net.minecraft.resources.Identifier"); + try { + Method fromNamespaceAndPath = identifier.getMethod("fromNamespaceAndPath", String.class, String.class); + return fromNamespaceAndPath.invoke(null, key.getNamespace(), key.getKey()); + } catch (NoSuchMethodException e) { + Method withDefaultNamespace = identifier.getMethod("parse", String.class); + return withDefaultNamespace.invoke(null, key.toString()); + } + } + + private static String identifierString(Object id) { + if (id == null) { + return null; + } + try { + Method getNamespace = id.getClass().getMethod("getNamespace"); + Method getPath = id.getClass().getMethod("getPath"); + getNamespace.setAccessible(true); + getPath.setAccessible(true); + return getNamespace.invoke(id) + ":" + getPath.invoke(id); + } catch (Throwable e) { + return id.toString(); + } + } + + private static Object unwrapHolder(Object holder) { + if (holder == null) { + return null; + } + try { + Method value = findMethod(holder.getClass(), "value"); + if (value != null) { + value.setAccessible(true); + return value.invoke(holder); + } + } catch (Throwable ignored) { + } + return holder; + } + + private static Object unwrapOptional(Object opt) { + if (opt == null) { + return null; + } + if (opt instanceof java.util.Optional o) { + return o.orElse(null); + } + return opt; + } + + static int readIntMember(Object value, String memberName) throws Exception { + Class type = value.getClass(); + while (type != null) { + try { + Field field = type.getDeclaredField(memberName); + field.setAccessible(true); + return coerceInt(field.get(value), memberName, value); + } catch (NoSuchFieldException ignored) { + type = type.getSuperclass(); + } + } + Method method = findMethod(value.getClass(), memberName); + if (method != null) { + method.setAccessible(true); + return coerceInt(method.invoke(value), memberName, value); + } + throw new NoSuchFieldException(memberName + " on " + value.getClass().getName()); + } + + /** + * Members that are plain numbers on one server build are wrapper objects on another + * (JigsawStructure.maxDistanceFromCenter became a MaxDistance{horizontal, vertical} record). + * Numbers pass through; a wrapper contributes its horizontal component, else its largest + * integral component, so a distance bound is never under-read. + */ + private static int coerceInt(Object member, String memberName, Object owner) throws Exception { + if (member instanceof Number n) { + return n.intValue(); + } + if (member == null) { + throw new NoSuchFieldException(memberName + " on " + owner.getClass().getName() + " is null"); + } + Method horizontal = findMethod(member.getClass(), "horizontal"); + if (horizontal != null && Number.class.isAssignableFrom(boxedType(horizontal.getReturnType()))) { + horizontal.setAccessible(true); + return ((Number) horizontal.invoke(member)).intValue(); + } + Integer widest = null; + for (Field component : member.getClass().getDeclaredFields()) { + if (Modifier.isStatic(component.getModifiers()) + || !Number.class.isAssignableFrom(boxedType(component.getType()))) { + continue; + } + component.setAccessible(true); + Object componentValue = component.get(member); + if (componentValue instanceof Number n && (widest == null || n.intValue() > widest)) { + widest = n.intValue(); + } + } + if (widest != null) { + return widest; + } + throw new NoSuchFieldException(memberName + " on " + owner.getClass().getName() + + " is a " + member.getClass().getName() + " with no integral component"); + } + + private static Class boxedType(Class type) { + return type == int.class ? Integer.class : type; + } + + private static int readInt(Object o, String method) throws Exception { + Method m = o.getClass().getMethod(method); + m.setAccessible(true); + return ((Number) m.invoke(o)).intValue(); + } + + private static Object staticField(String className, String fieldName) throws Exception { + Class c = Class.forName(className); + Field f = c.getField(fieldName); + return f.get(null); + } + + private static Object invoke(Object target, String method) throws Exception { + Method m = findMethod(target.getClass(), method); + if (m == null) { + throw new NoSuchMethodException(method + " on " + target.getClass().getName()); + } + m.setAccessible(true); + return m.invoke(target); + } + + private static Method findMethod(Class type, String name) { + Class c = type; + while (c != null) { + for (Method m : c.getDeclaredMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == 0) { + return m; + } + } + c = c.getSuperclass(); + } + for (Method m : type.getMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == 0) { + return m; + } + } + return null; + } + + private static Method findMethod4(Class type, String name) { + return findMethod(type, name, 4); + } + + private static Method findMethod(Class type, String name, int parameterCount) { + Class c = type; + while (c != null) { + for (Method m : c.getDeclaredMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == parameterCount) { + return m; + } + } + c = c.getSuperclass(); + } + for (Method m : type.getMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == parameterCount) { + return m; + } + } + return null; + } + + private static Object freshRandomSource(java.util.Random random) throws Exception { + Class randomSource = Class.forName("net.minecraft.util.RandomSource"); + Method create = randomSource.getMethod("create", long.class); + return create.invoke(null, random.nextLong()); + } + + static String poolName(String base, String poolKey) { + StructureKey key = StructureKey.parse(poolKey); + return base + "/pool/" + key.namespace() + "/" + key.path(); + } + + static String pieceName(String base, String templateLocation) { + StructureKey key = StructureKey.parse(templateLocation); + return base + "/piece/" + key.namespace() + "/" + key.path(); + } + + static Map pieceJson( + String pieceName, + List> connectors, + int nonAirBlocks + ) { + Map piece = new LinkedHashMap<>(); + piece.put("object", pieceName); + piece.put("connectors", connectors); + piece.put("rotatable", true); + if (nonAirBlocks == 0) { + piece.put("collidable", false); + } + return piece; + } + + static PoolMemberNormalization normalizePoolMember( + String sourcePoolKey, + String irisPoolName, + boolean startPoolMember, + int sourcePoolMembershipCount, + String sourceFallbackKey, + String templateLocation, + String irisPieceName, + int weight, + int nonAirBlocks, + List> connectors + ) { + if (!connectors.isEmpty() || startPoolMember) { + return new PoolMemberNormalization( + PoolMemberDisposition.PHYSICAL, + piecePoolEntry(irisPieceName, weight), + List.of()); + } + if (sourceFallbackKey != null + && !sourceFallbackKey.isBlank() + && !sourceFallbackKey.equals(sourcePoolKey)) { + return new PoolMemberNormalization( + PoolMemberDisposition.PHYSICAL, + piecePoolEntry(irisPieceName, weight), + List.of()); + } + String affectedResource = "jigsaw-pools/" + irisPoolName + ".json"; + if (nonAirBlocks == 0 && sourcePoolMembershipCount == 1) { + StructureLoss loss = StructureLoss.warning( + StructureCapability.IRIS_PLACEMENT, + "connectorless_all_air_member_normalized_empty", + "Source pool member " + templateLocation + " in " + sourcePoolKey + + " captured no non-air blocks and exposed no jigsaw connectors;" + + " its singleton membership was normalized to an explicit empty Iris pool entry" + + fallbackContext(sourcePoolKey, sourceFallbackKey) + ".") + .affecting(affectedResource); + return new PoolMemberNormalization( + PoolMemberDisposition.EMPTY, + emptyPoolEntry(weight), + List.of(loss)); + } + if (nonAirBlocks == 0) { + StructureLoss loss = StructureLoss.warning( + StructureCapability.IRIS_PLACEMENT, + "connectorless_all_air_mixed_member_omitted", + "Source pool member " + templateLocation + " in " + sourcePoolKey + + " captured no non-air blocks and exposed no jigsaw connectors;" + + " it was omitted from the mixed " + sourcePoolMembershipCount + "-member pool" + + fallbackContext(sourcePoolKey, sourceFallbackKey) + + ", changing source selection weights and RNG consumption.") + .affecting(affectedResource); + return new PoolMemberNormalization( + PoolMemberDisposition.OMITTED, + Map.of(), + List.of(loss)); + } + StructureLoss loss = StructureLoss.warning( + StructureCapability.BLOCKS, + "connectorless_non_air_member_omitted", + "Source pool member " + templateLocation + " in " + sourcePoolKey + + " captured " + nonAirBlocks + " non-air block(s) but exposed no jigsaw connectors;" + + " it was omitted because it cannot attach to the Iris assembly graph" + + fallbackContext(sourcePoolKey, sourceFallbackKey) + + ", changing source selection weights and RNG consumption while dropping those blocks.") + .affecting(affectedResource); + return new PoolMemberNormalization( + PoolMemberDisposition.OMITTED, + Map.of(), + List.of(loss)); + } + + private static String fallbackContext(String sourcePoolKey, String sourceFallbackKey) { + if (sourceFallbackKey == null || sourceFallbackKey.isBlank()) { + return " with no source fallback"; + } + if (sourceFallbackKey.equals(sourcePoolKey)) { + return " with its source self-fallback"; + } + return " before source fallback " + sourceFallbackKey; + } + + static Map emptyPoolEntry(int weight) { + Map entry = new LinkedHashMap<>(); + entry.put("empty", true); + entry.put("weight", weight); + return entry; + } + + static Map piecePoolEntry(String pieceName, int weight) { + Map entry = new LinkedHashMap<>(); + entry.put("piece", pieceName); + entry.put("weight", weight); + return entry; + } + + static Map structureJson( + String source, + String startPool, + int maxDepth, + int maxDistanceFromCenter + ) { + Map root = new LinkedHashMap<>(); + root.put("startPool", startPool); + root.put("maxDepth", Math.max(1, Math.min(30, maxDepth))); + int maxSizeChunks = Math.max(1, Math.min(32, (Math.max(1, maxDistanceFromCenter) + 15) / 16)); + root.put("maxSizeChunks", maxSizeChunks); + root.put("placeMode", "STRUCTURE_PIECE"); + root.put("branchFailurePolicy", IrisJigsawBranchFailurePolicy.TERMINATE_BRANCH.name()); + root.put("vanillaSource", source); + return root; + } + + private static StructureCapability unsupportedCapability(String elementType) { + if (elementType.endsWith("ListPoolElement")) { + return StructureCapability.LIST_ELEMENTS; + } + if (elementType.endsWith("FeaturePoolElement")) { + return StructureCapability.FEATURE_ELEMENTS; + } + return StructureCapability.BLOCKS; + } + + private static byte[] serialize(IrisObject object) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + object.write(output); + return output.toByteArray(); + } + + private static Result failed(String message, List losses) { + return new Result(false, message, 0, 0, losses); + } + + private static Result failed(String message, List losses, boolean retryableFailure) { + return new Result(false, message, 0, 0, losses, retryableFailure); + } + + private static void reportFailure(Throwable failure) { + IrisLogging.reportError(failure); + if (shouldPrintFullTrace(failure)) { + failure.printStackTrace(); + } + } + + /** + * True the first time a failure signature is seen. A bulk import repeats the same failure once + * per registered structure, so printing every trace buries the boot log in hundreds of copies + * of one problem; the per-structure "[fail] key: message" line still reports each occurrence. + */ + static boolean shouldPrintFullTrace(Throwable failure) { + if (failure == null) { + return false; + } + StackTraceElement[] trace = failure.getStackTrace(); + String signature = failure.getClass().getName() + '|' + failure.getMessage() + + '|' + (trace.length == 0 ? "" : trace[0].toString()); + return PRINTED_FAILURE_SIGNATURES.add(signature); + } + + static void resetFailureLogState() { + PRINTED_FAILURE_SIGNATURES.clear(); + } + + private static void reportWriteFailure(StructureWriteResult result) { + result.failure().ifPresent(VillageImporter::reportFailure); + } + + private static String writeResultNote(StructureWriteResult result) { + return result.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED + ? " (committed; staging cleanup is required, see console)" : ""; + } + + private static String writeFailureMessage(String name, StructureWriteResult result) { + if (result.status() == StructureWriteResult.Status.ADD_ONLY_CONFLICT) { + return "Skipped (add-only): '" + name + "' already exists"; + } + if (!result.conflicts().isEmpty()) { + StructureWriteResult.Conflict conflict = result.conflicts().getFirst(); + return "Import conflict for '" + name + "': " + conflict.relativePath() + " is " + + conflict.reason().name().toLowerCase() + ". Existing authored files were preserved."; + } + String failure = result.failure().map(VillageImporter::failureDetail).orElse(result.status().name()); + return "Failed writing jigsaw import for '" + name + "': " + failure; + } + + private static String failureDetail(Throwable failure) { + String message = failure.getMessage(); + return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message; + } + + record ConnectorMetadata(String finalState, int selectionPriority, int placementPriority) { + } + + record PoolElementResolution( + Object physicalElement, + String templateLocation, + int listLevels, + int omittedElements + ) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/IrisStructureBundleFactory.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/IrisStructureBundleFactory.java new file mode 100644 index 000000000..f0c25d359 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/IrisStructureBundleFactory.java @@ -0,0 +1,107 @@ +package art.arcane.iris.core.structure.authoring; + +import art.arcane.iris.engine.object.IrisObject; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class IrisStructureBundleFactory { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private IrisStructureBundleFactory() { + } + + public static StructureResourceBundle singlePiece(SinglePieceOptions options) throws IOException { + SinglePieceOptions activeOptions = Objects.requireNonNull(options); + StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(activeOptions.bundleKey()) + .source(activeOptions.source()) + .backend(StructureBackend.SNAPSHOT) + .capabilities(activeOptions.capabilities()) + .losses(activeOptions.losses()) + .resource("objects/" + activeOptions.resourceKey() + ".iob", serialize(activeOptions.object())) + .textResource("jigsaw-pieces/" + activeOptions.resourceKey() + ".json", + GSON.toJson(pieceJson(activeOptions.resourceKey()))); + if (!activeOptions.objectOnly()) { + bundle.capability(StructureCapability.IRIS_PLACEMENT) + .textResource("jigsaw-pools/" + activeOptions.resourceKey() + ".json", + GSON.toJson(poolJson(activeOptions.resourceKey()))) + .textResource("structures/" + activeOptions.resourceKey() + ".json", + GSON.toJson(structureJson(activeOptions))); + } + return bundle.build(); + } + + private static byte[] serialize(IrisObject object) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + object.write(output); + return output.toByteArray(); + } + + private static Map pieceJson(String resourceKey) { + Map piece = new LinkedHashMap<>(); + piece.put("object", resourceKey); + piece.put("connectors", new ArrayList<>()); + piece.put("rotatable", true); + return piece; + } + + private static Map poolJson(String resourceKey) { + Map entry = new LinkedHashMap<>(); + entry.put("piece", resourceKey); + entry.put("weight", 1); + List pieces = new ArrayList<>(); + pieces.add(entry); + Map pool = new LinkedHashMap<>(); + pool.put("pieces", pieces); + return pool; + } + + private static Map structureJson(SinglePieceOptions options) { + Map structure = new LinkedHashMap<>(); + structure.put("startPool", options.resourceKey()); + structure.put("maxDepth", 1); + structure.put("maxSizeChunks", Math.max(1, (options.maxSpan() / 16) + 1)); + structure.put("placeMode", options.placeMode()); + structure.put("vanillaSource", options.source().key().value()); + return structure; + } + + public record SinglePieceOptions( + StructureKey bundleKey, + StructureSource source, + String resourceKey, + IrisObject object, + int maxSpan, + String placeMode, + boolean objectOnly, + Collection capabilities, + Collection losses + ) { + public SinglePieceOptions { + Objects.requireNonNull(bundleKey); + Objects.requireNonNull(source); + Objects.requireNonNull(resourceKey); + Objects.requireNonNull(object); + Objects.requireNonNull(placeMode); + Objects.requireNonNull(capabilities); + Objects.requireNonNull(losses); + StructureResourceBundle.validateRelativePath("structures/" + resourceKey + ".json"); + if (maxSpan < 1) { + throw new IllegalArgumentException("Structure span must be positive"); + } + if (placeMode.isBlank()) { + throw new IllegalArgumentException("Structure place mode cannot be blank"); + } + capabilities = List.copyOf(capabilities); + losses = List.copyOf(losses); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureBackend.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureBackend.java new file mode 100644 index 000000000..015f2b869 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureBackend.java @@ -0,0 +1,25 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +public enum StructureBackend { + NATIVE, + IRIS_ASSEMBLY, + SNAPSHOT +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureCapability.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureCapability.java new file mode 100644 index 000000000..394e28720 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureCapability.java @@ -0,0 +1,35 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +public enum StructureCapability { + BLOCKS, + BLOCK_ENTITIES, + ENTITIES, + CONNECTORS, + PROCESSORS, + PROJECTION, + LIST_ELEMENTS, + FEATURE_ELEMENTS, + LIQUID_SETTINGS, + TERRAIN_ADAPTATION, + MIRRORING, + NATIVE_PLACEMENT, + IRIS_PLACEMENT +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureFileOperations.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureFileOperations.java new file mode 100644 index 000000000..da695e5c1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureFileOperations.java @@ -0,0 +1,160 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +interface StructureFileOperations { + boolean exists(Path path); + + boolean isRegularFile(Path path); + + default boolean isDirectory(Path path) { + return Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS); + } + + byte[] readAllBytes(Path path) throws IOException; + + String sha256(Path path) throws IOException; + + void createDirectories(Path path) throws IOException; + + void writeNew(Path path, byte[] content) throws IOException; + + void move(Path source, Path target) throws IOException; + + default void moveNew(Path source, Path target) throws IOException { + move(source, target); + } + + void deleteIfExists(Path path) throws IOException; + + void deleteTree(Path root) throws IOException; + + default List list(Path root) throws IOException { + try (Stream stream = Files.list(root)) { + return stream.sorted().toList(); + } + } + + default List list(Path root, int maxEntries) throws IOException { + List entries = list(root); + return entries.size() <= maxEntries ? entries : entries.subList(0, maxEntries + 1); + } + + default void forceFile(Path path) throws IOException { + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE)) { + channel.force(true); + } + } + + default void forceDirectory(Path path) throws IOException { + if (!Files.getFileStore(path).supportsFileAttributeView("posix")) { + return; + } + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { + channel.force(true); + } + } +} + +final class NioStructureFileOperations implements StructureFileOperations { + @Override + public boolean exists(Path path) { + return Files.exists(path, LinkOption.NOFOLLOW_LINKS); + } + + @Override + public boolean isRegularFile(Path path) { + return Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS); + } + + @Override + public byte[] readAllBytes(Path path) throws IOException { + return Files.readAllBytes(path); + } + + @Override + public String sha256(Path path) throws IOException { + try (InputStream input = Files.newInputStream(path)) { + return StructureHash.sha256(input); + } + } + + @Override + public void createDirectories(Path path) throws IOException { + Files.createDirectories(path); + } + + @Override + public void writeNew(Path path, byte[] content) throws IOException { + Files.write(path, content, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); + } + + @Override + public void move(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } + + @Override + public void moveNew(Path source, Path target) throws IOException { + Files.move(source, target); + } + + @Override + public void deleteIfExists(Path path) throws IOException { + Files.deleteIfExists(path); + } + + @Override + public void deleteTree(Path root) throws IOException { + if (!exists(root)) { + return; + } + List paths; + try (Stream stream = Files.walk(root)) { + paths = stream.sorted(Comparator.reverseOrder()).toList(); + } + for (Path path : paths) { + Files.deleteIfExists(path); + } + } + + @Override + public List list(Path root, int maxEntries) throws IOException { + try (Stream stream = Files.list(root)) { + return stream.limit(maxEntries + 1L).sorted().toList(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureHash.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureHash.java new file mode 100644 index 000000000..db64f7f0d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureHash.java @@ -0,0 +1,65 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Objects; +import java.util.regex.Pattern; + +public final class StructureHash { + private static final Pattern SHA_256_PATTERN = Pattern.compile("[a-f0-9]{64}"); + + private StructureHash() { + } + + public static String sha256(byte[] content) { + Objects.requireNonNull(content, "content"); + MessageDigest digest = createSha256Digest(); + return HexFormat.of().formatHex(digest.digest(content)); + } + + public static String sha256(InputStream input) throws IOException { + Objects.requireNonNull(input, "input"); + MessageDigest digest = createSha256Digest(); + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) >= 0) { + if (read > 0) { + digest.update(buffer, 0, read); + } + } + return HexFormat.of().formatHex(digest.digest()); + } + + public static boolean isSha256(String value) { + return value != null && SHA_256_PATTERN.matcher(value).matches(); + } + + private static MessageDigest createSha256Digest() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is not available", e); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureKey.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureKey.java new file mode 100644 index 000000000..e3789de6e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureKey.java @@ -0,0 +1,87 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import java.util.Objects; +import java.util.regex.Pattern; + +public record StructureKey(String namespace, String path) implements Comparable { + private static final Pattern NAMESPACE_PATTERN = Pattern.compile("[a-z0-9._-]+"); + private static final Pattern PATH_PATTERN = Pattern.compile("[a-z0-9._/-]+"); + + public StructureKey { + Objects.requireNonNull(namespace, "namespace"); + Objects.requireNonNull(path, "path"); + if (!NAMESPACE_PATTERN.matcher(namespace).matches()) { + throw new IllegalArgumentException("Invalid structure namespace: " + namespace); + } + if (!PATH_PATTERN.matcher(path).matches()) { + throw new IllegalArgumentException("Invalid structure path: " + path); + } + validatePathSegments(path); + } + + public static StructureKey parse(String value) { + Objects.requireNonNull(value, "value"); + int separator = value.indexOf(':'); + if (separator <= 0 || separator == value.length() - 1 || separator != value.lastIndexOf(':')) { + throw new IllegalArgumentException("Structure key must use namespace:path: " + value); + } + return new StructureKey(value.substring(0, separator), value.substring(separator + 1)); + } + + public static StructureKey parse(String value, String defaultNamespace) { + Objects.requireNonNull(value, "value"); + Objects.requireNonNull(defaultNamespace, "defaultNamespace"); + if (value.indexOf(':') < 0) { + return new StructureKey(defaultNamespace, value); + } + return parse(value); + } + + public String value() { + return namespace + ":" + path; + } + + @Override + public int compareTo(StructureKey other) { + int namespaceComparison = namespace.compareTo(other.namespace); + if (namespaceComparison != 0) { + return namespaceComparison; + } + return path.compareTo(other.path); + } + + @Override + public String toString() { + return value(); + } + + private static void validatePathSegments(String path) { + if (path.startsWith("/") || path.endsWith("/") || path.contains("//")) { + throw new IllegalArgumentException("Invalid structure path: " + path); + } + String[] segments = path.split("/"); + for (String segment : segments) { + if (segment.equals(".") || segment.equals("..")) { + throw new IllegalArgumentException("Invalid structure path: " + path); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureLoss.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureLoss.java new file mode 100644 index 000000000..b148ec8c2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureLoss.java @@ -0,0 +1,65 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import java.util.Objects; +import java.util.regex.Pattern; + +public record StructureLoss( + StructureCapability capability, + Severity severity, + String code, + String detail, + String affectedResource +) { + private static final Pattern CODE_PATTERN = Pattern.compile("[a-z0-9._-]+"); + + public StructureLoss { + Objects.requireNonNull(capability, "capability"); + Objects.requireNonNull(severity, "severity"); + Objects.requireNonNull(code, "code"); + Objects.requireNonNull(detail, "detail"); + Objects.requireNonNull(affectedResource, "affectedResource"); + if (!CODE_PATTERN.matcher(code).matches()) { + throw new IllegalArgumentException("Invalid structure loss code: " + code); + } + if (detail.isBlank()) { + throw new IllegalArgumentException("Structure loss detail cannot be blank"); + } + } + + public static StructureLoss warning(StructureCapability capability, String code, String detail) { + return new StructureLoss(capability, Severity.WARNING, code, detail, ""); + } + + public static StructureLoss error(StructureCapability capability, String code, String detail) { + return new StructureLoss(capability, Severity.ERROR, code, detail, ""); + } + + public StructureLoss affecting(String resource) { + Objects.requireNonNull(resource, "resource"); + return new StructureLoss(capability, severity, code, detail, resource); + } + + public enum Severity { + INFO, + WARNING, + ERROR + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureOwnershipManifest.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureOwnershipManifest.java new file mode 100644 index 000000000..a4b452826 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureOwnershipManifest.java @@ -0,0 +1,260 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.UUID; + +public record StructureOwnershipManifest( + int schemaVersion, + StructureKey structure, + StructureSource source, + StructureBackend backend, + List capabilities, + List losses, + Map resourceHashes, + Provenance provenance +) { + public static final int CURRENT_SCHEMA_VERSION = 1; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public StructureOwnershipManifest { + if (schemaVersion != CURRENT_SCHEMA_VERSION) { + throw new IllegalArgumentException("Unsupported structure ownership manifest schema: " + schemaVersion); + } + Objects.requireNonNull(structure, "structure"); + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(backend, "backend"); + Objects.requireNonNull(capabilities, "capabilities"); + Objects.requireNonNull(losses, "losses"); + Objects.requireNonNull(resourceHashes, "resourceHashes"); + provenance = provenance == null ? Provenance.created() : provenance; + ArrayList orderedCapabilities = new ArrayList<>(capabilities); + orderedCapabilities.sort(Comparator.naturalOrder()); + capabilities = List.copyOf(orderedCapabilities); + losses = List.copyOf(losses); + TreeMap orderedHashes = new TreeMap<>(); + TreeMap portablePaths = new TreeMap<>(); + for (Map.Entry entry : resourceHashes.entrySet()) { + String relativePath = StructureResourceBundle.validateRelativePath(entry.getKey()); + String portablePath = relativePath.toLowerCase(Locale.ROOT); + String previousPath = portablePaths.putIfAbsent(portablePath, relativePath); + if (previousPath != null) { + throw new IllegalArgumentException( + "Ownership manifest contains case-colliding resources: " + previousPath + " and " + relativePath + ); + } + String contentHash = Objects.requireNonNull(entry.getValue(), "contentHash"); + if (!StructureHash.isSha256(contentHash)) { + throw new IllegalArgumentException("Invalid SHA-256 hash for resource " + relativePath); + } + orderedHashes.put(relativePath, contentHash); + } + resourceHashes = Collections.unmodifiableMap(orderedHashes); + } + + public StructureOwnershipManifest( + int schemaVersion, + StructureKey structure, + StructureSource source, + StructureBackend backend, + List capabilities, + List losses, + Map resourceHashes + ) { + this(schemaVersion, structure, source, backend, capabilities, losses, resourceHashes, Provenance.created()); + } + + public static StructureOwnershipManifest from(StructureResourceBundle bundle) { + return from(bundle, Provenance.created()); + } + + public static StructureOwnershipManifest from(StructureResourceBundle bundle, Provenance provenance) { + Objects.requireNonNull(bundle, "bundle"); + Objects.requireNonNull(provenance, "provenance"); + TreeMap hashes = new TreeMap<>(); + for (StructureResourceBundle.Resource resource : bundle.resources().values()) { + hashes.put(resource.relativePath(), resource.contentHash()); + } + return new StructureOwnershipManifest( + CURRENT_SCHEMA_VERSION, + bundle.key(), + bundle.source(), + bundle.backend(), + new ArrayList<>(bundle.capabilities()), + bundle.losses(), + hashes, + provenance + ); + } + + public static StructureOwnershipManifest fromJson(byte[] content) { + Objects.requireNonNull(content, "content"); + StructureOwnershipManifest manifest = GSON.fromJson( + new String(content, StandardCharsets.UTF_8), + StructureOwnershipManifest.class + ); + if (manifest == null) { + throw new IllegalArgumentException("Structure ownership manifest is empty"); + } + return manifest; + } + + public byte[] toJson() { + return GSON.toJson(this).getBytes(StandardCharsets.UTF_8); + } + + public String relativePath() { + return relativePath(structure); + } + + public static String relativePath(StructureKey structure) { + Objects.requireNonNull(structure, "structure"); + String identityHash = StructureHash.sha256(structure.value().getBytes(StandardCharsets.UTF_8)); + return ".iris/structure-manifests/key-" + identityHash + ".json"; + } + + public record Provenance( + Origin origin, + String receiptId, + String planHash, + String sourceClosureHash, + long appliedAtEpochMilli, + Map sourceResourceHashes, + Map sourceToTargetPaths, + RollbackDisposition rollbackDisposition + ) { + public Provenance { + origin = origin == null ? Origin.CREATED : origin; + receiptId = normalize(receiptId); + planHash = normalize(planHash); + sourceClosureHash = normalize(sourceClosureHash); + sourceResourceHashes = immutableHashes(sourceResourceHashes); + sourceToTargetPaths = immutableMappings(sourceToTargetPaths); + rollbackDisposition = rollbackDisposition == null + ? RollbackDisposition.NONE + : rollbackDisposition; + if (origin == Origin.CREATED) { + if (!receiptId.isEmpty() || !planHash.isEmpty() || !sourceClosureHash.isEmpty() + || appliedAtEpochMilli != 0L || !sourceResourceHashes.isEmpty() + || !sourceToTargetPaths.isEmpty() || rollbackDisposition != RollbackDisposition.NONE) { + throw new IllegalArgumentException("Created structure provenance cannot declare adoption metadata"); + } + } else { + requireUuid(receiptId); + requireHash(planHash, "plan"); + requireHash(sourceClosureHash, "source closure"); + if (appliedAtEpochMilli <= 0L) { + throw new IllegalArgumentException("Adoption provenance requires a positive application time"); + } + if (sourceResourceHashes.isEmpty() || sourceToTargetPaths.isEmpty()) { + throw new IllegalArgumentException("Adoption provenance requires source hashes and path mappings"); + } + } + } + + public static Provenance created() { + return new Provenance( + Origin.CREATED, + "", + "", + "", + 0L, + Map.of(), + Map.of(), + RollbackDisposition.NONE + ); + } + + public boolean adopted() { + return origin != Origin.CREATED; + } + + private static Map immutableHashes(Map hashes) { + if (hashes == null || hashes.isEmpty()) { + return Map.of(); + } + TreeMap ordered = new TreeMap<>(); + for (Map.Entry entry : hashes.entrySet()) { + String relativePath = StructureResourceBundle.validateRelativePath(entry.getKey()); + String hash = Objects.requireNonNull(entry.getValue(), "source resource hash"); + requireHash(hash, "source resource"); + ordered.put(relativePath, hash); + } + return Collections.unmodifiableMap(ordered); + } + + private static Map immutableMappings(Map mappings) { + if (mappings == null || mappings.isEmpty()) { + return Map.of(); + } + TreeMap ordered = new TreeMap<>(); + for (Map.Entry entry : mappings.entrySet()) { + String sourcePath = StructureResourceBundle.validateRelativePath(entry.getKey()); + String targetPath = StructureResourceBundle.validateRelativePath( + Objects.requireNonNull(entry.getValue(), "target resource path")); + ordered.put(sourcePath, targetPath); + } + return Collections.unmodifiableMap(ordered); + } + + private static void requireUuid(String value) { + try { + UUID.fromString(value); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException("Adoption receipt ID must be a UUID", exception); + } + } + + private static void requireHash(String value, String kind) { + if (!StructureHash.isSha256(value)) { + throw new IllegalArgumentException("Adoption " + kind + " hash must be SHA-256"); + } + } + + private static String normalize(String value) { + return value == null ? "" : value.trim(); + } + } + + public enum Origin { + CREATED, + ADOPTED_EXISTING, + ADOPTED_CLONE, + ADOPTED_MANAGED_CLONE, + CONVERTED, + MANAGED_DATAPACK + } + + public enum RollbackDisposition { + NONE, + DELETE_CREATED_IF_UNCHANGED + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureRecoveryResult.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureRecoveryResult.java new file mode 100644 index 000000000..0dd5f0143 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureRecoveryResult.java @@ -0,0 +1,56 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +public record StructureRecoveryResult( + int restoredPreparedTransactions, + int cleanedCommittedTransactions, + int cleanedOrphanTransactions, + List failures +) { + public StructureRecoveryResult { + if (restoredPreparedTransactions < 0 + || cleanedCommittedTransactions < 0 + || cleanedOrphanTransactions < 0) { + throw new IllegalArgumentException("Structure recovery counts cannot be negative"); + } + Objects.requireNonNull(failures, "failures"); + failures = List.copyOf(failures); + } + + public boolean successful() { + return failures.isEmpty(); + } + + public int recoveredTransactions() { + return restoredPreparedTransactions + cleanedCommittedTransactions + cleanedOrphanTransactions; + } + + public record Failure(Path transactionRoot, Throwable cause) { + public Failure { + Objects.requireNonNull(transactionRoot, "transactionRoot"); + Objects.requireNonNull(cause, "cause"); + transactionRoot = transactionRoot.toAbsolutePath().normalize(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureResourceBundle.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureResourceBundle.java new file mode 100644 index 000000000..bc72603e8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureResourceBundle.java @@ -0,0 +1,234 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Pattern; + +public final class StructureResourceBundle { + private static final Pattern WINDOWS_RESERVED_NAME = Pattern.compile( + "(?i)(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\..*)?" + ); + + private final StructureKey key; + private final StructureSource source; + private final StructureBackend backend; + private final Set capabilities; + private final List losses; + private final Map resources; + + private StructureResourceBundle(Builder builder) { + key = builder.key; + source = Objects.requireNonNull(builder.source, "source"); + backend = Objects.requireNonNull(builder.backend, "backend"); + capabilities = immutableCapabilities(builder.capabilities); + losses = List.copyOf(builder.losses); + resources = Collections.unmodifiableMap(new TreeMap<>(builder.resources)); + if (resources.isEmpty()) { + throw new IllegalStateException("Structure resource bundle cannot be empty"); + } + } + + public static Builder builder(StructureKey key) { + return new Builder(key); + } + + public StructureKey key() { + return key; + } + + public StructureSource source() { + return source; + } + + public StructureBackend backend() { + return backend; + } + + public Set capabilities() { + return capabilities; + } + + public List losses() { + return losses; + } + + public Map resources() { + return resources; + } + + public static String validateRelativePath(String relativePath) { + Objects.requireNonNull(relativePath, "relativePath"); + if (relativePath.isBlank() || relativePath.startsWith("/") || relativePath.endsWith("/")) { + throw new IllegalArgumentException("Resource path must be a non-empty relative path: " + relativePath); + } + if (relativePath.indexOf('\\') >= 0 || relativePath.indexOf(':') >= 0 || relativePath.contains("//")) { + throw new IllegalArgumentException("Resource path is not portable: " + relativePath); + } + String[] segments = relativePath.split("/"); + if (segments[0].equalsIgnoreCase(".iris")) { + throw new IllegalArgumentException("Resource path uses the reserved .iris directory: " + relativePath); + } + for (String segment : segments) { + validatePathSegment(relativePath, segment); + } + return relativePath; + } + + private static Set immutableCapabilities(EnumSet capabilities) { + if (capabilities.isEmpty()) { + return Set.of(); + } + return Collections.unmodifiableSet(EnumSet.copyOf(capabilities)); + } + + private static void validatePathSegment(String relativePath, String segment) { + if (segment.isEmpty() || segment.equals(".") || segment.equals("..") || segment.endsWith(".") || segment.endsWith(" ")) { + throw new IllegalArgumentException("Resource path is not portable: " + relativePath); + } + if (WINDOWS_RESERVED_NAME.matcher(segment).matches()) { + throw new IllegalArgumentException("Resource path uses a reserved file name: " + relativePath); + } + for (int i = 0; i < segment.length(); i++) { + char character = segment.charAt(i); + if (Character.isISOControl(character) || character == '"' || character == '*' || character == '<' + || character == '>' || character == '?' || character == '|') { + throw new IllegalArgumentException("Resource path is not portable: " + relativePath); + } + } + } + + public static final class Builder { + private final StructureKey key; + private final EnumSet capabilities; + private final List losses; + private final Map resources; + private final Map portableResourcePaths; + private StructureSource source; + private StructureBackend backend; + + private Builder(StructureKey key) { + this.key = Objects.requireNonNull(key, "key"); + capabilities = EnumSet.noneOf(StructureCapability.class); + losses = new ArrayList<>(); + resources = new TreeMap<>(); + portableResourcePaths = new HashMap<>(); + } + + public Builder source(StructureSource source) { + this.source = Objects.requireNonNull(source, "source"); + return this; + } + + public Builder backend(StructureBackend backend) { + this.backend = Objects.requireNonNull(backend, "backend"); + return this; + } + + public Builder capability(StructureCapability capability) { + capabilities.add(Objects.requireNonNull(capability, "capability")); + return this; + } + + public Builder capabilities(Collection capabilities) { + Objects.requireNonNull(capabilities, "capabilities"); + for (StructureCapability capability : capabilities) { + capability(capability); + } + return this; + } + + public Builder loss(StructureLoss loss) { + losses.add(Objects.requireNonNull(loss, "loss")); + return this; + } + + public Builder losses(Collection losses) { + Objects.requireNonNull(losses, "losses"); + for (StructureLoss loss : losses) { + loss(loss); + } + return this; + } + + public Builder resource(String relativePath, byte[] content) { + Resource resource = new Resource(relativePath, content); + String portablePath = resource.relativePath().toLowerCase(Locale.ROOT); + String previousPath = portableResourcePaths.putIfAbsent(portablePath, resource.relativePath()); + if (previousPath != null) { + throw new IllegalArgumentException("Duplicate structure resource: " + relativePath); + } + resources.put(resource.relativePath(), resource); + return this; + } + + public Builder textResource(String relativePath, String content) { + Objects.requireNonNull(content, "content"); + return resource(relativePath, content.getBytes(StandardCharsets.UTF_8)); + } + + public StructureResourceBundle build() { + return new StructureResourceBundle(this); + } + } + + public static final class Resource { + private final String relativePath; + private final byte[] content; + private final String contentHash; + + private Resource(String relativePath, byte[] content) { + this.relativePath = validateRelativePath(relativePath); + Objects.requireNonNull(content, "content"); + this.content = content.clone(); + contentHash = StructureHash.sha256(this.content); + } + + public String relativePath() { + return relativePath; + } + + public byte[] content() { + return content.clone(); + } + + public int size() { + return content.length; + } + + public String contentHash() { + return contentHash; + } + + byte[] contentForWrite() { + return content; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureSource.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureSource.java new file mode 100644 index 000000000..1569f2631 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureSource.java @@ -0,0 +1,49 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import java.util.Objects; + +public record StructureSource(Kind kind, StructureKey key, String version, String contentHash) { + public StructureSource { + Objects.requireNonNull(kind, "kind"); + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(version, "version"); + Objects.requireNonNull(contentHash, "contentHash"); + if (!contentHash.isEmpty() && !StructureHash.isSha256(contentHash)) { + throw new IllegalArgumentException("Source content hash must be SHA-256"); + } + } + + public static StructureSource of(Kind kind, StructureKey key) { + return new StructureSource(kind, key, "", ""); + } + + public static StructureSource identified(Kind kind, StructureKey key, String version, byte[] content) { + return new StructureSource(kind, key, version, StructureHash.sha256(content)); + } + + public enum Kind { + IRIS, + VANILLA, + DATAPACK, + MOD, + UNKNOWN + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionJournal.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionJournal.java new file mode 100644 index 000000000..6ae3ff0b5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionJournal.java @@ -0,0 +1,143 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +record StructureTransactionJournal( + int schemaVersion, + UUID transactionId, + Phase phase, + List targets +) { + static final int CURRENT_SCHEMA_VERSION = 1; + static final String FILE_NAME = "transaction.json"; + static final String NEXT_FILE_NAME = "transaction.json.next"; + private static final int MAX_TARGETS = 100_000; + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Pattern MANIFEST_PATH = Pattern.compile( + "\\.iris/structure-manifests/key-[a-f0-9]{64}\\.json" + ); + + StructureTransactionJournal { + if (schemaVersion != CURRENT_SCHEMA_VERSION) { + throw new IllegalArgumentException("Unsupported structure transaction schema: " + schemaVersion); + } + Objects.requireNonNull(transactionId, "transactionId"); + Objects.requireNonNull(phase, "phase"); + Objects.requireNonNull(targets, "targets"); + if (targets.isEmpty()) { + throw new IllegalArgumentException("Structure transaction must contain at least one target"); + } + if (targets.size() > MAX_TARGETS) { + throw new IllegalArgumentException("Structure transaction contains too many targets"); + } + ArrayList orderedTargets = new ArrayList<>(targets); + orderedTargets.sort(Comparator.comparing(Target::relativePath)); + Set portablePaths = new HashSet<>(orderedTargets.size()); + for (Target target : orderedTargets) { + String portablePath = target.relativePath().toLowerCase(Locale.ROOT); + if (!portablePaths.add(portablePath)) { + throw new IllegalArgumentException("Structure transaction contains duplicate target " + + target.relativePath()); + } + } + targets = List.copyOf(orderedTargets); + } + + static StructureTransactionJournal prepared(UUID transactionId, List targets) { + return new StructureTransactionJournal(CURRENT_SCHEMA_VERSION, transactionId, Phase.PREPARED, targets); + } + + static StructureTransactionJournal fromJson(byte[] content) { + Objects.requireNonNull(content, "content"); + StructureTransactionJournal journal = GSON.fromJson( + new String(content, StandardCharsets.UTF_8), + StructureTransactionJournal.class + ); + if (journal == null) { + throw new IllegalArgumentException("Structure transaction journal is empty"); + } + return journal; + } + + StructureTransactionJournal committed() { + return new StructureTransactionJournal(schemaVersion, transactionId, Phase.COMMITTED, targets); + } + + byte[] toJson() { + return GSON.toJson(this).getBytes(StandardCharsets.UTF_8); + } + + enum Phase { + PREPARED, + COMMITTED + } + + record Target( + String relativePath, + boolean hadOriginal, + String originalHash, + String replacementHash + ) { + Target { + Objects.requireNonNull(relativePath, "relativePath"); + Objects.requireNonNull(originalHash, "originalHash"); + Objects.requireNonNull(replacementHash, "replacementHash"); + if (relativePath.startsWith(".iris/")) { + StructureResourceBundle.validateRelativePath("transaction/" + relativePath); + if (!MANIFEST_PATH.matcher(relativePath).matches()) { + throw new IllegalArgumentException("Invalid internal structure transaction target: " + + relativePath); + } + } else { + relativePath = StructureResourceBundle.validateRelativePath(relativePath); + } + if (hadOriginal && !StructureHash.isSha256(originalHash)) { + throw new IllegalArgumentException("Structure transaction original hash is invalid for " + + relativePath); + } + if (!hadOriginal && !originalHash.isEmpty()) { + throw new IllegalArgumentException("Structure transaction has a hash for a new target " + + relativePath); + } + if (!replacementHash.isEmpty() && !StructureHash.isSha256(replacementHash)) { + throw new IllegalArgumentException("Structure transaction replacement hash is invalid for " + + relativePath); + } + if (!hadOriginal && replacementHash.isEmpty()) { + throw new IllegalArgumentException("Structure transaction new target has no replacement hash for " + + relativePath); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionReadSet.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionReadSet.java new file mode 100644 index 000000000..fdb9493b0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionReadSet.java @@ -0,0 +1,147 @@ +package art.arcane.iris.core.structure.authoring; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +public record StructureTransactionReadSet( + Map fileHashes, + Set absentPaths, + Map> directoryEntries +) { + public static final int MAX_ENTRIES = 100_000; + + public StructureTransactionReadSet { + Objects.requireNonNull(fileHashes, "fileHashes"); + Objects.requireNonNull(absentPaths, "absentPaths"); + Objects.requireNonNull(directoryEntries, "directoryEntries"); + TreeMap orderedHashes = new TreeMap<>(); + for (Map.Entry entry : fileHashes.entrySet()) { + String path = validatePath(entry.getKey(), false); + String hash = Objects.requireNonNull(entry.getValue(), "read-set file hash"); + if (!StructureHash.isSha256(hash)) { + throw new IllegalArgumentException("Read-set file hash must be SHA-256 for " + path); + } + orderedHashes.put(path, hash); + } + TreeSet orderedAbsent = new TreeSet<>(); + for (String path : absentPaths) { + orderedAbsent.add(validatePath(path, false)); + } + for (String path : orderedHashes.keySet()) { + if (orderedAbsent.contains(path)) { + throw new IllegalArgumentException("Read-set path cannot be both present and absent: " + path); + } + } + TreeMap> orderedDirectories = new TreeMap<>(); + TreeSet uniqueEntries = new TreeSet<>(orderedHashes.keySet()); + uniqueEntries.addAll(orderedAbsent); + for (Map.Entry> entry : directoryEntries.entrySet()) { + String directory = validatePath(entry.getKey(), true); + uniqueEntries.add(directory); + Objects.requireNonNull(entry.getValue(), "read-set directory entries"); + TreeSet entries = new TreeSet<>(); + for (String child : entry.getValue()) { + String childPath = validatePath(child, false); + if (!childPath.startsWith(directory + "/")) { + throw new IllegalArgumentException( + "Read-set directory entry is outside " + directory + ": " + childPath); + } + entries.add(childPath); + } + uniqueEntries.addAll(entries); + orderedDirectories.put(directory, List.copyOf(entries)); + } + if (uniqueEntries.size() > MAX_ENTRIES) { + throw new IllegalArgumentException("Read set exceeds " + MAX_ENTRIES + " entries"); + } + fileHashes = Collections.unmodifiableMap(orderedHashes); + absentPaths = Collections.unmodifiableSet(orderedAbsent); + directoryEntries = Collections.unmodifiableMap(orderedDirectories); + } + + public static StructureTransactionReadSet empty() { + return new StructureTransactionReadSet(Map.of(), Set.of(), Map.of()); + } + + public boolean isEmpty() { + return fileHashes.isEmpty() && absentPaths.isEmpty() && directoryEntries.isEmpty(); + } + + public List paths() { + TreeSet paths = new TreeSet<>(fileHashes.keySet()); + paths.addAll(absentPaths); + for (List entries : directoryEntries.values()) { + paths.addAll(entries); + } + return List.copyOf(paths); + } + + public static Builder builder() { + return new Builder(); + } + + private static String validatePath(String value, boolean directory) { + Objects.requireNonNull(value, "read-set path"); + String normalized = value.trim(); + if (normalized.isEmpty() || !normalized.equals(value) || normalized.startsWith("/") + || normalized.endsWith("/") || normalized.indexOf('\\') >= 0 || normalized.contains(":")) { + throw new IllegalArgumentException("Invalid read-set path: " + value); + } + Path path = Path.of(normalized).normalize(); + if (path.isAbsolute() || path.getNameCount() == 0 || path.startsWith("..") + || path.toString().equals(".")) { + throw new IllegalArgumentException("Invalid read-set path: " + value); + } + String portable = path.toString().replace('\\', '/'); + if (!portable.equals(normalized)) { + throw new IllegalArgumentException("Read-set path is not normalized: " + value); + } + if (!directory && normalized.endsWith("/.")) { + throw new IllegalArgumentException("Invalid read-set file path: " + value); + } + return normalized; + } + + public static final class Builder { + private final Map fileHashes = new TreeMap<>(); + private final Set absentPaths = new TreeSet<>(); + private final Map> directoryEntries = new TreeMap<>(); + + public Builder file(String relativePath, String contentHash) { + fileHashes.put(relativePath, contentHash); + return this; + } + + public Builder files(Map hashes) { + fileHashes.putAll(Objects.requireNonNull(hashes, "read-set files")); + return this; + } + + public Builder absent(String relativePath) { + absentPaths.add(relativePath); + return this; + } + + public Builder absent(Collection relativePaths) { + absentPaths.addAll(Objects.requireNonNull(relativePaths, "absent read-set paths")); + return this; + } + + public Builder directory(String relativePath, Collection entries) { + directoryEntries.put(relativePath, new ArrayList<>(Objects.requireNonNull(entries, "directory entries"))); + return this; + } + + public StructureTransactionReadSet build() { + return new StructureTransactionReadSet(fileHashes, absentPaths, directoryEntries); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionWriter.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionWriter.java new file mode 100644 index 000000000..a8bba28d5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureTransactionWriter.java @@ -0,0 +1,2394 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Stream; + +public final class StructureTransactionWriter { + private static final String STAGING_RELATIVE_PATH = ".iris/structure-staging"; + private static final String PROCESS_LOCK_RELATIVE_PATH = ".iris/structure-authoring.lock"; + private static final String RECOVERY_CLAIM_FILE = "external-coordinator.json"; + private static final int RECOVERY_CLAIM_SCHEMA = 1; + private static final int MAX_RECOVERY_CLAIM_BYTES = 64 * 1024; + private static final int MAX_COORDINATOR_JOURNAL_BYTES = 4 * 1024 * 1024; + private static final int MAX_STRUCTURE_STATE_BYTES = 64 * 1024 * 1024; + private static final long MAX_VERIFIED_READ_FILE_BYTES = 256L * 1024L * 1024L; + private static final int MAX_RECOVERY_TRANSACTIONS = 1_024; + private static final LockedRemovalValidator NO_REMOVAL_VALIDATION = () -> { + }; + private static final ConcurrentMap ROOT_LOCKS = new ConcurrentHashMap<>(); + private static final Gson GSON = new Gson(); + + private final Path packRoot; + private final StructureFileOperations files; + private final ReentrantLock rootLock; + + public StructureTransactionWriter(Path packRoot) { + this(packRoot, new NioStructureFileOperations()); + } + + StructureTransactionWriter(Path packRoot, StructureFileOperations files) { + this.packRoot = canonicalPackRoot(Objects.requireNonNull(packRoot, "packRoot")); + this.files = Objects.requireNonNull(files, "files"); + rootLock = ROOT_LOCKS.computeIfAbsent(this.packRoot, ignored -> new ReentrantLock()); + } + + public Path packRoot() { + return packRoot; + } + + public Path ownershipManifestPath(StructureKey key) { + Objects.requireNonNull(key, "key"); + return resolveTarget(StructureOwnershipManifest.relativePath(key)); + } + + public StructureRecoveryResult recoverIncompleteTransactions() { + rootLock.lock(); + try (ProcessLock ignored = acquireProcessLock()) { + return recoverIncompleteTransactionsLocked(); + } catch (IOException | RuntimeException e) { + return new StructureRecoveryResult( + 0, + 0, + 0, + List.of(new StructureRecoveryResult.Failure(packRoot, e)) + ); + } finally { + rootLock.unlock(); + } + } + + public StructureWriteResult write(StructureResourceBundle bundle, StructureWriteMode mode) { + return write(bundle, new StructureWriteOptions(mode, false)); + } + + public Optional ownedSource(StructureKey key) throws IOException { + Objects.requireNonNull(key, "key"); + rootLock.lock(); + try (ProcessLock ignored = acquireProcessLock()) { + StructureRecoveryResult recovery = recoverIncompleteTransactionsLocked(); + if (!recovery.successful()) { + throw recoveryFailure(recovery); + } + Path manifestPath = ownershipManifestPath(key); + if (!files.exists(manifestPath)) { + return Optional.empty(); + } + if (!files.isRegularFile(manifestPath)) { + throw new IOException("Structure ownership manifest is not a regular file: " + manifestPath); + } + StructureOwnershipManifest manifest; + try { + manifest = StructureOwnershipManifest.fromJson(readBoundedBytes( + manifestPath, + MAX_STRUCTURE_STATE_BYTES, + "Structure ownership manifest" + )); + } catch (RuntimeException e) { + throw new IOException("Invalid structure ownership manifest at " + manifestPath, e); + } + if (!manifest.structure().equals(key)) { + throw new IOException("Structure ownership manifest belongs to " + manifest.structure()); + } + return Optional.of(manifest.source()); + } finally { + rootLock.unlock(); + } + } + + public boolean removeOwned(StructureKey key, StructureSource.Kind sourceKind, StructureKey sourceKey) throws IOException { + return removeOwned(new OwnedRemoval( + key, + sourceKind, + sourceKey, + Optional.empty(), + Optional.empty())); + } + + public boolean removeManagedDatapackOwned( + StructureKey key, + StructureSource.Kind sourceKind, + StructureKey sourceKey + ) throws IOException { + return removeOwned(OwnedRemoval.managedDatapack(key, sourceKind, sourceKey)); + } + + private boolean removeOwned(OwnedRemoval request) throws IOException { + try (PreparedRemoval removal = prepareOwnedRemovals(List.of(request))) { + boolean changed = removal.changed(); + removal.markCommitted(); + removal.finishCommit(); + return changed; + } + } + + public PreparedRemoval prepareOwnedRemovals(List removals) throws IOException { + return prepareOwnedRemovals(removals, NO_REMOVAL_VALIDATION); + } + + public PreparedRemoval prepareOwnedRemovals( + List removals, + LockedRemovalValidator validator + ) throws IOException { + return prepareOwnedRemovals(removals, false, validator); + } + + public PreparedRemoval prepareMatchingOwnedRemovals(List removals) throws IOException { + return prepareOwnedRemovals(removals, true, NO_REMOVAL_VALIDATION); + } + + private PreparedRemoval prepareOwnedRemovals( + List removals, + boolean skipOwnershipMismatches, + LockedRemovalValidator validator + ) throws IOException { + Objects.requireNonNull(removals, "removals"); + List requests = List.copyOf(removals); + LockedRemovalValidator requiredValidator = Objects.requireNonNull( + validator, + "locked removal validator"); + rootLock.lock(); + ProcessLock processLock = null; + Path transactionRoot = null; + LinkedHashMap backups = new LinkedHashMap<>(); + try { + processLock = acquireProcessLock(); + StructureRecoveryResult recovery = recoverIncompleteTransactionsLocked(); + if (!recovery.successful()) { + throw recoveryFailure(recovery); + } + requiredValidator.validate(); + RemovalPlan plan = buildRemovalPlan(requests, skipOwnershipMismatches); + if (plan.targets().isEmpty()) { + return new PreparedRemoval(null, null, backups, processLock, false); + } + + UUID transactionId = UUID.randomUUID(); + transactionRoot = stagingRoot().resolve(transactionId.toString()).normalize(); + Path backupRoot = transactionRoot.resolve("backup"); + StructureTransactionJournal journal = StructureTransactionJournal.prepared( + transactionId, + plan.targets() + ); + files.createDirectories(backupRoot); + writeJournal(transactionRoot, journal); + files.forceDirectory(transactionRoot); + files.forceDirectory(stagingRoot()); + verifyTargetSnapshot(journal); + backupTargets(journal, backupRoot, backups); + return new PreparedRemoval(transactionRoot, journal, backups, processLock, true); + } catch (IOException | RuntimeException preparationFailure) { + Optional rollbackFailure = rollback(backups, List.of()); + if (rollbackFailure.isPresent()) { + preparationFailure.addSuppressed(rollbackFailure.get()); + } else if (transactionRoot != null) { + cleanupAfterFailure(transactionRoot, preparationFailure); + } + if (processLock != null) { + try { + processLock.close(); + } catch (IOException closeFailure) { + preparationFailure.addSuppressed(closeFailure); + } + } + rootLock.unlock(); + if (preparationFailure instanceof IOException ioFailure) { + throw ioFailure; + } + throw new IOException("Failed preparing owned structure removal", preparationFailure); + } + } + + public record OwnedRemoval( + StructureKey key, + StructureSource.Kind sourceKind, + StructureKey sourceKey, + Optional requiredOrigin, + Optional expectedManifestHash + ) { + public OwnedRemoval { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(sourceKind, "sourceKind"); + Objects.requireNonNull(sourceKey, "sourceKey"); + requiredOrigin = Objects.requireNonNull(requiredOrigin, "requiredOrigin"); + expectedManifestHash = Objects.requireNonNull(expectedManifestHash, "expectedManifestHash"); + if (expectedManifestHash.isPresent() + && !StructureHash.isSha256(expectedManifestHash.get())) { + throw new IllegalArgumentException("Expected ownership manifest hash must be SHA-256"); + } + } + + public static OwnedRemoval managedDatapack( + StructureKey key, + StructureSource.Kind sourceKind, + StructureKey sourceKey + ) { + return new OwnedRemoval( + key, + sourceKind, + sourceKey, + Optional.of(StructureOwnershipManifest.Origin.MANAGED_DATAPACK), + Optional.empty() + ); + } + } + + @FunctionalInterface + public interface LockedRemovalValidator { + void validate() throws IOException; + } + + public record PreparedRemovalToken(Path packRoot, UUID transactionId) { + public PreparedRemovalToken { + packRoot = canonicalPackRoot(Objects.requireNonNull(packRoot, "packRoot")); + Objects.requireNonNull(transactionId, "transactionId"); + } + } + + public record RecoveryOwner(Path transactionRoot, UUID transactionId, UUID claimId) { + public RecoveryOwner { + transactionRoot = validateRecoveryOwnerRoot(Objects.requireNonNull(transactionRoot, "transactionRoot")); + Objects.requireNonNull(transactionId, "transactionId"); + Objects.requireNonNull(claimId, "claimId"); + if (!transactionId.toString().equals(transactionRoot.getFileName().toString())) { + throw new IllegalArgumentException("Recovery owner transaction id does not match its directory"); + } + } + } + + public boolean verifyRecoveryOwner( + PreparedRemovalToken token, + RecoveryOwner owner, + boolean verifyPreparedState + ) throws IOException { + Objects.requireNonNull(token, "token"); + Objects.requireNonNull(owner, "owner"); + if (!packRoot.equals(token.packRoot())) { + throw new IOException("Prepared removal token belongs to a different pack root"); + } + Path transactionRoot = stagingRoot().resolve(token.transactionId().toString()).normalize(); + if (!files.exists(transactionRoot)) { + return false; + } + verifyRecoveryClaim(transactionRoot, token.transactionId(), owner); + if (verifyPreparedState) { + verifyPreparedRemovalAuthority(transactionRoot, token.transactionId()); + } + return true; + } + + public void resolvePreparedRemoval(PreparedRemovalToken token, boolean commit) throws IOException { + resolvePreparedRemoval(token, null, commit); + } + + public void resolvePreparedRemoval( + PreparedRemovalToken token, + RecoveryOwner owner, + boolean commit + ) throws IOException { + Objects.requireNonNull(token, "token"); + if (!packRoot.equals(token.packRoot())) { + throw new IOException("Prepared removal token belongs to a different pack root"); + } + rootLock.lock(); + try (ProcessLock ignored = acquireProcessLock()) { + Path transactionRoot = stagingRoot().resolve(token.transactionId().toString()).normalize(); + if (!files.exists(transactionRoot)) { + return; + } + if (owner != null) { + verifyRecoveryClaim(transactionRoot, token.transactionId(), owner); + } + Path journalPath = recoveryJournalPath(transactionRoot); + if (journalPath == null || !files.isRegularFile(journalPath)) { + throw new IOException("Missing prepared removal journal at " + transactionRoot); + } + StructureTransactionJournal journal; + try { + journal = StructureTransactionJournal.fromJson(readBoundedBytes( + journalPath, + MAX_STRUCTURE_STATE_BYTES, + "Prepared removal journal" + )); + } catch (RuntimeException e) { + throw new IOException("Invalid prepared removal journal at " + journalPath, e); + } + if (!journal.transactionId().equals(token.transactionId())) { + throw new IOException("Prepared removal journal id does not match " + token.transactionId()); + } + if (commit) { + verifyCommittedTransaction(journal); + } else { + restorePreparedTransaction(transactionRoot, journal); + } + cleanupTransaction(transactionRoot); + } finally { + rootLock.unlock(); + } + } + + public final class PreparedRemoval implements AutoCloseable { + private final Path transactionRoot; + private final StructureTransactionJournal journal; + private final LinkedHashMap backups; + private final ProcessLock processLock; + private final boolean changed; + private boolean committed; + private boolean closed; + + private PreparedRemoval( + Path transactionRoot, + StructureTransactionJournal journal, + LinkedHashMap backups, + ProcessLock processLock, + boolean changed + ) { + this.transactionRoot = transactionRoot; + this.journal = journal; + this.backups = backups; + this.processLock = processLock; + this.changed = changed; + } + + public boolean changed() { + return changed; + } + + public Optional recoveryToken() { + if (transactionRoot == null) { + return Optional.empty(); + } + UUID transactionId = UUID.fromString(Objects.requireNonNull( + transactionRoot.getFileName(), + "prepared removal transaction directory" + ).toString()); + return Optional.of(new PreparedRemovalToken(packRoot, transactionId)); + } + + public void claimRecoveryOwner(RecoveryOwner owner) throws IOException { + requireOpen(); + Objects.requireNonNull(owner, "owner"); + if (transactionRoot == null) { + return; + } + RecoveryClaim claim = new RecoveryClaim( + RECOVERY_CLAIM_SCHEMA, + owner.transactionRoot().toString(), + owner.transactionId(), + owner.claimId() + ); + byte[] claimContent = GSON.toJson(claim).getBytes(StandardCharsets.UTF_8); + if (claimContent.length > MAX_RECOVERY_CLAIM_BYTES) { + throw new IOException("External recovery claim exceeds " + MAX_RECOVERY_CLAIM_BYTES + " bytes"); + } + Path claimPath = transactionRoot.resolve(RECOVERY_CLAIM_FILE); + files.writeNew(claimPath, claimContent); + files.forceFile(claimPath); + files.forceDirectory(transactionRoot); + } + + public void markCommitted() throws IOException { + requireOpen(); + if (transactionRoot == null) { + committed = true; + return; + } + boolean committedJournalWritten = false; + try { + writeJournal(transactionRoot, journal.committed()); + committedJournalWritten = true; + files.forceDirectory(transactionRoot); + committed = true; + } catch (IOException | RuntimeException commitFailure) { + if (committedJournalWritten || isCommittedJournal(transactionRoot, commitFailure)) { + committed = true; + } + if (commitFailure instanceof IOException ioFailure) { + throw ioFailure; + } + throw new IOException("Failed marking owned structure removal committed", commitFailure); + } + } + + public void finishCommit() throws IOException { + requireOpen(); + if (!committed) { + throw new IllegalStateException("Owned structure removal has not been marked committed"); + } + IOException failure = null; + if (transactionRoot != null) { + try { + cleanupTransaction(transactionRoot); + } catch (IOException | RuntimeException cleanupFailure) { + failure = new IOException("Owned structure removal committed but cleanup remains at " + + transactionRoot, cleanupFailure); + } + } + IOException releaseFailure = release(); + if (failure == null) { + failure = releaseFailure; + } else if (releaseFailure != null) { + failure.addSuppressed(releaseFailure); + } + if (failure != null) { + throw failure; + } + } + + public void rollback() throws IOException { + if (closed) { + return; + } + IOException failure = null; + if (transactionRoot != null) { + Optional rollbackFailure = StructureTransactionWriter.this.rollback(backups, List.of()); + if (rollbackFailure.isPresent()) { + failure = new IOException("Failed restoring prepared owned structure removal at " + + transactionRoot, rollbackFailure.get()); + } else { + try { + cleanupTransaction(transactionRoot); + } catch (IOException | RuntimeException cleanupFailure) { + failure = new IOException("Restored owned structure removal but cleanup remains at " + + transactionRoot, cleanupFailure); + } + } + } + IOException releaseFailure = release(); + if (failure == null) { + failure = releaseFailure; + } else if (releaseFailure != null) { + failure.addSuppressed(releaseFailure); + } + if (failure != null) { + throw failure; + } + } + + public void leaveForRecovery() throws IOException { + if (closed) { + return; + } + IOException releaseFailure = release(); + if (releaseFailure != null) { + throw releaseFailure; + } + } + + @Override + public void close() throws IOException { + rollback(); + } + + private void requireOpen() { + if (closed) { + throw new IllegalStateException("Owned structure removal transaction is closed"); + } + } + + private IOException release() { + IOException failure = null; + try { + processLock.close(); + } catch (IOException e) { + failure = e; + } finally { + closed = true; + rootLock.unlock(); + } + return failure; + } + } + + public StructureWriteResult preview(StructureResourceBundle bundle, StructureWriteMode mode) { + return write(bundle, StructureWriteOptions.preview(mode)); + } + + public StructureWriteResult claimExisting( + StructureOwnershipManifest manifest, + StructureTransactionReadSet readSet + ) { + Objects.requireNonNull(manifest, "manifest"); + Objects.requireNonNull(readSet, "readSet"); + for (Map.Entry resource : manifest.resourceHashes().entrySet()) { + String expectedHash = readSet.fileHashes().get(resource.getKey()); + if (!resource.getValue().equals(expectedHash)) { + throw new IllegalArgumentException( + "Claim read set does not pin owned resource " + resource.getKey()); + } + } + rootLock.lock(); + try (ProcessLock ignored = acquireProcessLock()) { + StructureRecoveryResult recovery = recoverIncompleteTransactionsLocked(); + if (!recovery.successful()) { + return claimResult( + StructureWriteResult.Status.FAILED, + manifest, + List.of(), + Optional.of(recoveryFailure(recovery)) + ); + } + List conflicts = verifyReadSet(readSet); + Path manifestPath = ownershipManifestPath(manifest.structure()); + if (files.exists(manifestPath)) { + ArrayList updated = new ArrayList<>(conflicts); + updated.add(StructureWriteResult.Conflict.at( + manifest.relativePath(), + StructureWriteResult.ConflictReason.MANIFEST_EXISTS)); + conflicts = orderedConflicts(updated); + } + if (!conflicts.isEmpty()) { + StructureWriteResult.Status status = conflicts.stream().anyMatch(conflict -> + conflict.reason() == StructureWriteResult.ConflictReason.MANIFEST_EXISTS) + ? StructureWriteResult.Status.ADD_ONLY_CONFLICT + : StructureWriteResult.Status.OWNERSHIP_CONFLICT; + return claimResult(status, manifest, conflicts, Optional.empty()); + } + return commitClaim(manifest, readSet); + } catch (IOException | RuntimeException exception) { + return claimResult( + StructureWriteResult.Status.FAILED, + manifest, + List.of(), + Optional.of(exception) + ); + } finally { + rootLock.unlock(); + } + } + + public StructureWriteResult write(StructureResourceBundle bundle, StructureWriteOptions options) { + return writeVerified( + bundle, + options, + StructureTransactionReadSet.empty(), + StructureOwnershipManifest.Provenance.created() + ); + } + + public StructureWriteResult writeManagedDatapack( + StructureResourceBundle bundle, + StructureWriteMode mode + ) { + Objects.requireNonNull(bundle, "bundle"); + Objects.requireNonNull(mode, "mode"); + if (bundle.source().kind() != StructureSource.Kind.DATAPACK + && bundle.source().kind() != StructureSource.Kind.VANILLA) { + throw new IllegalArgumentException("Managed datapack writes require a datapack or vanilla source"); + } + return writeVerified( + bundle, + new StructureWriteOptions(mode, false), + StructureTransactionReadSet.empty(), + managedDatapackProvenance(bundle), + ExistingProvenancePolicy.INSTALL_MANAGED_DATAPACK + ); + } + + public StructureWriteResult writeVerified( + StructureResourceBundle bundle, + StructureWriteOptions options, + StructureTransactionReadSet readSet, + StructureOwnershipManifest.Provenance provenance + ) { + return writeVerified( + bundle, + options, + readSet, + provenance, + ExistingProvenancePolicy.PRESERVE + ); + } + + private StructureWriteResult writeVerified( + StructureResourceBundle bundle, + StructureWriteOptions options, + StructureTransactionReadSet readSet, + StructureOwnershipManifest.Provenance provenance, + ExistingProvenancePolicy provenancePolicy + ) { + Objects.requireNonNull(bundle, "bundle"); + Objects.requireNonNull(options, "options"); + Objects.requireNonNull(readSet, "readSet"); + Objects.requireNonNull(provenance, "provenance"); + Objects.requireNonNull(provenancePolicy, "provenancePolicy"); + rootLock.lock(); + try { + if (options.dryRun()) { + return writeLocked(bundle, options, readSet, provenance, provenancePolicy); + } + try (ProcessLock ignored = acquireProcessLock()) { + return writeLocked(bundle, options, readSet, provenance, provenancePolicy); + } + } catch (IOException | RuntimeException e) { + return failedResult(bundle, e); + } finally { + rootLock.unlock(); + } + } + + private StructureWriteResult writeLocked( + StructureResourceBundle bundle, + StructureWriteOptions options, + StructureTransactionReadSet readSet, + StructureOwnershipManifest.Provenance provenance, + ExistingProvenancePolicy provenancePolicy + ) + throws IOException { + if (!options.dryRun()) { + StructureRecoveryResult recovery = recoverIncompleteTransactionsLocked(); + if (!recovery.successful()) { + return failedResult(bundle, recoveryFailure(recovery)); + } + } + List readSetConflicts = verifyReadSet(readSet); + if (!readSetConflicts.isEmpty()) { + return readSetConflictResult(bundle, readSetConflicts); + } + WritePlan plan = buildPlan(bundle, options, provenance, provenancePolicy); + if (!plan.conflicts().isEmpty()) { + StructureWriteResult.Status status = options.mode() == StructureWriteMode.ADD_ONLY + ? StructureWriteResult.Status.ADD_ONLY_CONFLICT + : StructureWriteResult.Status.OWNERSHIP_CONFLICT; + return result(status, plan, Optional.empty()); + } + if (options.dryRun()) { + return result(StructureWriteResult.Status.DRY_RUN, plan, Optional.empty()); + } + if (plan.action() == StructureWriteResult.Action.NONE) { + return result(StructureWriteResult.Status.UNCHANGED, plan, Optional.empty()); + } + return commit(plan, readSet); + } + + private RemovalPlan buildRemovalPlan( + List removals, + boolean skipOwnershipMismatches + ) throws IOException { + TreeMap targets = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (OwnedRemoval removal : removals) { + Path manifestPath = ownershipManifestPath(removal.key()); + if (!files.exists(manifestPath)) { + continue; + } + if (!files.isRegularFile(manifestPath)) { + throw new IOException("Structure ownership manifest is not a regular file: " + manifestPath); + } + byte[] manifestContent = readBoundedBytes( + manifestPath, + MAX_STRUCTURE_STATE_BYTES, + "Structure ownership manifest" + ); + String manifestHash = StructureHash.sha256(manifestContent); + if (removal.expectedManifestHash().isPresent() + && !removal.expectedManifestHash().get().equals(manifestHash)) { + throw new IOException("Structure ownership manifest changed after removal was planned: " + + removal.key()); + } + StructureOwnershipManifest manifest; + try { + manifest = StructureOwnershipManifest.fromJson(manifestContent); + } catch (RuntimeException e) { + throw new IOException("Invalid structure ownership manifest at " + manifestPath, e); + } + if (!manifest.structure().equals(removal.key())) { + throw new IOException("Structure ownership manifest belongs to " + manifest.structure()); + } + boolean sourceMismatch = manifest.source().kind() != removal.sourceKind() + || !manifest.source().key().equals(removal.sourceKey()); + boolean originMismatch = removal.requiredOrigin().isPresent() + && manifest.provenance().origin() != removal.requiredOrigin().get(); + if (sourceMismatch || originMismatch) { + if (skipOwnershipMismatches) { + continue; + } + if (originMismatch) { + throw new IOException("Structure '" + removal.key() + + "' is not owned by managed datapack ingest"); + } + throw new IOException("Structure '" + removal.key() + "' is owned by source " + + manifest.source().key() + " (" + manifest.source().kind() + "), not " + + removal.sourceKey() + " (" + removal.sourceKind() + ")"); + } + + for (Map.Entry resource : manifest.resourceHashes().entrySet()) { + Path target = resolveTarget(resource.getKey()); + verifyRemovalTarget(target, resource.getKey(), resource.getValue()); + addRemovalTarget(targets, resource.getKey(), resource.getValue()); + } + addRemovalTarget(targets, manifest.relativePath(), StructureHash.sha256(manifestContent)); + } + return new RemovalPlan(List.copyOf(targets.values())); + } + + private void addRemovalTarget( + Map targets, + String relativePath, + String contentHash + ) throws IOException { + StructureTransactionJournal.Target target = new StructureTransactionJournal.Target( + relativePath, + true, + contentHash, + "" + ); + StructureTransactionJournal.Target existing = targets.putIfAbsent(relativePath, target); + if (existing != null && (!existing.relativePath().equals(relativePath) + || !existing.originalHash().equals(contentHash))) { + throw new IOException("Owned structure removals overlap at incompatible resource path " + relativePath); + } + } + + private void verifyRemovalTarget(Path target, String relativePath, String expectedHash) throws IOException { + if (!files.isRegularFile(target)) { + throw new IOException("Owned structure resource is missing or not a regular file: " + relativePath); + } + String actualHash = files.sha256(target); + if (!expectedHash.equals(actualHash)) { + throw new IOException("Owned structure resource was modified and will not be removed: " + relativePath); + } + } + + private StructureRecoveryResult recoverIncompleteTransactionsLocked() { + Path stagingRoot = stagingRoot(); + ArrayList failures = new ArrayList<>(); + int restoredPrepared = 0; + int cleanedCommitted = 0; + int cleanedOrphans = 0; + if (!files.exists(stagingRoot)) { + return new StructureRecoveryResult(0, 0, 0, List.of()); + } + if (!files.isDirectory(stagingRoot)) { + IOException failure = new IOException("Structure staging root is not a directory: " + stagingRoot); + return new StructureRecoveryResult( + 0, + 0, + 0, + List.of(new StructureRecoveryResult.Failure(stagingRoot, failure)) + ); + } + + List transactionRoots; + try { + transactionRoots = files.list(stagingRoot, MAX_RECOVERY_TRANSACTIONS); + } catch (IOException | RuntimeException e) { + return new StructureRecoveryResult( + 0, + 0, + 0, + List.of(new StructureRecoveryResult.Failure(stagingRoot, e)) + ); + } + if (transactionRoots.size() > MAX_RECOVERY_TRANSACTIONS) { + IOException failure = new IOException("Structure recovery transaction count exceeds " + + MAX_RECOVERY_TRANSACTIONS); + return new StructureRecoveryResult( + 0, + 0, + 0, + List.of(new StructureRecoveryResult.Failure(stagingRoot, failure)) + ); + } + + for (Path transactionRoot : transactionRoots) { + try { + RecoveryOutcome outcome = recoverTransaction(transactionRoot); + switch (outcome) { + case RESTORED_PREPARED -> restoredPrepared++; + case CLEANED_COMMITTED -> cleanedCommitted++; + case CLEANED_ORPHAN -> cleanedOrphans++; + } + } catch (IOException | RuntimeException e) { + failures.add(new StructureRecoveryResult.Failure(transactionRoot, e)); + } + } + return new StructureRecoveryResult( + restoredPrepared, + cleanedCommitted, + cleanedOrphans, + failures + ); + } + + private RecoveryOutcome recoverTransaction(Path transactionRoot) throws IOException { + Path normalizedRoot = transactionRoot.toAbsolutePath().normalize(); + Path stagingRoot = stagingRoot(); + if (!Objects.equals(normalizedRoot.getParent(), stagingRoot)) { + throw new IOException("Structure transaction is not a direct child of " + stagingRoot); + } + rejectSymbolicLinks(stagingRoot, normalizedRoot); + if (!files.isDirectory(normalizedRoot)) { + files.deleteTree(normalizedRoot); + files.forceDirectory(stagingRoot); + return RecoveryOutcome.CLEANED_ORPHAN; + } + + Path journalPath = recoveryJournalPath(normalizedRoot); + if (journalPath == null) { + if (hasRecoveryData(normalizedRoot.resolve("backup"))) { + throw new IOException("Structure transaction has backups but no recovery journal: " + + normalizedRoot); + } + cleanupTransaction(normalizedRoot); + return RecoveryOutcome.CLEANED_ORPHAN; + } + if (!files.isRegularFile(journalPath)) { + throw new IOException("Structure transaction journal is not a regular file: " + journalPath); + } + + StructureTransactionJournal journal; + try { + journal = StructureTransactionJournal.fromJson(readBoundedBytes( + journalPath, + MAX_STRUCTURE_STATE_BYTES, + "Structure transaction journal" + )); + } catch (RuntimeException e) { + throw new IOException("Invalid structure transaction journal at " + journalPath, e); + } + String directoryName = Objects.requireNonNull(normalizedRoot.getFileName(), "transaction directory name") + .toString(); + if (!journal.transactionId().toString().equals(directoryName)) { + throw new IOException("Structure transaction journal id " + journal.transactionId() + + " does not match directory " + directoryName); + } + if (hasActiveRecoveryOwner(normalizedRoot, journal.transactionId())) { + throw new IOException("Structure transaction recovery is owned by an active datapack coordinator: " + + normalizedRoot); + } + + return switch (journal.phase()) { + case PREPARED -> { + restorePreparedTransaction(normalizedRoot, journal); + cleanupTransaction(normalizedRoot); + yield RecoveryOutcome.RESTORED_PREPARED; + } + case COMMITTED -> { + verifyCommittedTransaction(journal); + cleanupTransaction(normalizedRoot); + yield RecoveryOutcome.CLEANED_COMMITTED; + } + }; + } + + private Path recoveryJournalPath(Path transactionRoot) throws IOException { + Path journalPath = transactionRoot.resolve(StructureTransactionJournal.FILE_NAME); + if (files.exists(journalPath)) { + return journalPath; + } + Path nextJournalPath = transactionRoot.resolve(StructureTransactionJournal.NEXT_FILE_NAME); + if (!files.exists(nextJournalPath)) { + return null; + } + if (!files.isRegularFile(nextJournalPath)) { + throw new IOException("Structure transaction next journal is not a regular file: " + + nextJournalPath); + } + return nextJournalPath; + } + + private boolean hasActiveRecoveryOwner(Path transactionRoot, UUID transactionId) throws IOException { + Path claimPath = transactionRoot.resolve(RECOVERY_CLAIM_FILE); + if (!files.exists(claimPath)) { + return false; + } + RecoveryClaim claim = readRecoveryClaim(claimPath); + RecoveryOwner owner; + try { + owner = new RecoveryOwner( + Path.of(claim.coordinatorTransactionRoot()), + claim.coordinatorTransactionId(), + claim.claimId() + ); + } catch (RuntimeException e) { + throw new IOException("Invalid external recovery claim at " + claimPath, e); + } + return coordinatorReferencesClaim( + new PreparedRemovalToken(packRoot, transactionId), + owner + ); + } + + private void verifyRecoveryClaim( + Path transactionRoot, + UUID transactionId, + RecoveryOwner owner + ) throws IOException { + Path claimPath = transactionRoot.resolve(RECOVERY_CLAIM_FILE); + RecoveryClaim claim = readRecoveryClaim(claimPath); + if (!Objects.equals(claim.coordinatorTransactionRoot(), owner.transactionRoot().toString()) + || !Objects.equals(claim.coordinatorTransactionId(), owner.transactionId()) + || !Objects.equals(claim.claimId(), owner.claimId())) { + throw new IOException("Prepared removal recovery claim does not match its datapack coordinator"); + } + if (!coordinatorReferencesClaim(new PreparedRemovalToken(packRoot, transactionId), owner)) { + throw new IOException("Prepared removal recovery coordinator was not durably published"); + } + } + + private void verifyPreparedRemovalAuthority(Path transactionRoot, UUID transactionId) throws IOException { + Path journalPath = recoveryJournalPath(transactionRoot); + if (journalPath == null || !files.isRegularFile(journalPath)) { + throw new IOException("Missing prepared removal journal at " + transactionRoot); + } + StructureTransactionJournal journal; + try { + journal = StructureTransactionJournal.fromJson(readBoundedBytes( + journalPath, + MAX_STRUCTURE_STATE_BYTES, + "Prepared removal journal" + )); + } catch (RuntimeException e) { + throw new IOException("Invalid prepared removal journal at " + journalPath, e); + } + if (!journal.transactionId().equals(transactionId) + || journal.phase() != StructureTransactionJournal.Phase.PREPARED) { + throw new IOException("Prepared removal journal does not match its datapack coordinator"); + } + boolean ownershipManifestPresent = false; + Path backupRoot = transactionRoot.resolve("backup").normalize(); + for (StructureTransactionJournal.Target state : journal.targets()) { + if (!state.hadOriginal() || !state.replacementHash().isEmpty()) { + throw new IOException("External coordinator claimed a non-removal structure transaction"); + } + ownershipManifestPresent |= state.relativePath().startsWith(".iris/structure-manifests/"); + Path target = resolveTarget(state.relativePath()); + if (files.exists(target)) { + throw new IOException("Prepared removal target reappeared before coordinator recovery: " + + state.relativePath()); + } + Path backup = resolveTransactionPath(backupRoot, state.relativePath()); + if (!files.isRegularFile(backup)) { + throw new IOException("Prepared removal backup is missing or not a regular file: " + + state.relativePath()); + } + verifyOriginalContent(backup, state); + } + if (!ownershipManifestPresent) { + throw new IOException("External coordinator removal has no structure ownership manifest"); + } + } + + private RecoveryClaim readRecoveryClaim(Path claimPath) throws IOException { + if (!files.isRegularFile(claimPath)) { + throw new IOException("Invalid external recovery claim " + claimPath); + } + byte[] content = readBoundedBytes( + claimPath, + MAX_RECOVERY_CLAIM_BYTES, + "External recovery claim" + ); + try { + RecoveryClaim claim = GSON.fromJson( + new String(content, StandardCharsets.UTF_8), + RecoveryClaim.class + ); + if (claim == null || claim.schemaVersion() != RECOVERY_CLAIM_SCHEMA + || claim.coordinatorTransactionRoot() == null + || claim.coordinatorTransactionId() == null || claim.claimId() == null) { + throw new IOException("Incomplete external recovery claim " + claimPath); + } + return claim; + } catch (RuntimeException e) { + throw new IOException("Invalid external recovery claim " + claimPath, e); + } + } + + private boolean coordinatorReferencesClaim( + PreparedRemovalToken token, + RecoveryOwner owner + ) throws IOException { + Path ownerRoot = owner.transactionRoot(); + Path ownerParent = Objects.requireNonNull(ownerRoot.getParent(), "recovery owner parent"); + if (Files.isSymbolicLink(ownerParent) || Files.isSymbolicLink(ownerRoot)) { + throw new IOException("External recovery owner path contains a symbolic link: " + ownerRoot); + } + if (!Files.exists(ownerRoot, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + if (!Files.isDirectory(ownerRoot, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("External recovery owner is not a directory: " + ownerRoot); + } + Path committed = ownerRoot.resolve("journal.json"); + Path next = ownerRoot.resolve("journal.next.json"); + Path journalPath; + if (Files.exists(committed, LinkOption.NOFOLLOW_LINKS)) { + journalPath = committed; + } else if (Files.exists(next, LinkOption.NOFOLLOW_LINKS)) { + journalPath = next; + } else { + try (Stream contents = Files.list(ownerRoot)) { + if (contents.findAny().isEmpty()) { + return false; + } + } + throw new IOException("External recovery owner has no transaction journal: " + ownerRoot); + } + if (Files.isSymbolicLink(journalPath) + || !Files.isRegularFile(journalPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid external recovery owner journal " + journalPath); + } + JsonObject journal; + try { + journal = JsonParser.parseString(new String( + readBoundedBytes( + journalPath, + MAX_COORDINATOR_JOURNAL_BYTES, + "External recovery owner journal" + ), + StandardCharsets.UTF_8 + )).getAsJsonObject(); + } catch (RuntimeException e) { + if (journalPath.equals(next) && isOnlyOwnerArtifact(ownerRoot, next)) { + return false; + } + throw new IOException("Invalid external recovery owner journal " + journalPath, e); + } + if (!journal.has("schemaVersion") || journal.get("schemaVersion").getAsInt() != 2 + || !journal.has("transactionId") + || !owner.transactionId().toString().equals(journal.get("transactionId").getAsString()) + || !journal.has("operation") || !"REMOVE".equals(journal.get("operation").getAsString()) + || !journal.has("editables") || !journal.get("editables").isJsonArray()) { + throw new IOException("External recovery owner journal does not match its claim"); + } + JsonArray editables = journal.getAsJsonArray("editables"); + for (JsonElement element : editables) { + if (!element.isJsonObject()) { + continue; + } + JsonObject editable = element.getAsJsonObject(); + if (editable.has("packRoot") && editable.has("transactionId") && editable.has("claimId") + && packRoot.toString().equals(editable.get("packRoot").getAsString()) + && token.transactionId().toString().equals(editable.get("transactionId").getAsString()) + && owner.claimId().toString().equals(editable.get("claimId").getAsString())) { + return true; + } + } + throw new IOException("External recovery owner journal does not contain its claimed structure transaction"); + } + + private boolean isOnlyOwnerArtifact(Path ownerRoot, Path artifact) throws IOException { + try (Stream contents = Files.list(ownerRoot)) { + List entries = contents.limit(2).toList(); + return entries.size() == 1 && Objects.equals(entries.getFirst(), artifact); + } + } + + private void verifyCommittedTransaction(StructureTransactionJournal journal) throws IOException { + for (StructureTransactionJournal.Target state : journal.targets()) { + Path target = resolveTarget(state.relativePath()); + if (state.replacementHash().isEmpty()) { + if (files.exists(target)) { + throw new IOException("Committed structure transaction retained a removed target: " + + state.relativePath()); + } + continue; + } + if (!files.isRegularFile(target)) { + throw new IOException("Committed structure transaction target is missing or is not a regular file: " + + state.relativePath()); + } + String actualHash = files.sha256(target); + if (!state.replacementHash().equals(actualHash)) { + throw new IOException("Committed structure transaction target hash mismatch for " + + state.relativePath()); + } + } + } + + private void restorePreparedTransaction( + Path transactionRoot, + StructureTransactionJournal journal + ) throws IOException { + Throwable recoveryFailure = null; + ArrayList targets = new ArrayList<>(journal.targets()); + Collections.reverse(targets); + for (StructureTransactionJournal.Target state : targets) { + try { + restorePreparedTarget(transactionRoot, state); + } catch (IOException | RuntimeException e) { + recoveryFailure = appendFailure(recoveryFailure, e); + } + } + if (recoveryFailure != null) { + throw new IOException("Unable to restore prepared structure transaction " + transactionRoot, + recoveryFailure); + } + } + + private void restorePreparedTarget( + Path transactionRoot, + StructureTransactionJournal.Target state + ) throws IOException { + Path target = resolveTarget(state.relativePath()); + Path backupRoot = transactionRoot.resolve("backup").normalize(); + Path backup = resolveTransactionPath(backupRoot, state.relativePath()); + Path targetParent = Objects.requireNonNull(target.getParent(), "recovery target parent"); + if (!state.hadOriginal()) { + removeReplacementIfPresent(target, state); + return; + } + if (!files.exists(backup)) { + verifyOriginalTarget(target, state); + files.forceFile(target); + files.forceDirectory(targetParent); + return; + } + if (!files.isRegularFile(backup)) { + throw new IOException("Structure transaction backup is not a regular file: " + backup); + } + verifyOriginalContent(backup, state); + if (files.isRegularFile(target) && state.originalHash().equals(files.sha256(target))) { + files.deleteIfExists(backup); + files.forceFile(target); + files.forceDirectory(targetParent); + files.forceDirectory(Objects.requireNonNull(backup.getParent(), "recovery backup parent")); + return; + } + removeReplacementIfPresent(target, state); + files.createDirectories(targetParent); + files.moveNew(backup, target); + files.forceFile(target); + files.forceDirectory(targetParent); + files.forceDirectory(Objects.requireNonNull(backup.getParent(), "recovery backup parent")); + } + + private boolean hasRecoveryData(Path backupRoot) throws IOException { + if (!files.exists(backupRoot)) { + return false; + } + if (!files.isDirectory(backupRoot)) { + return true; + } + for (Path child : files.list(backupRoot)) { + if (!files.isDirectory(child) || hasRecoveryData(child)) { + return true; + } + } + return false; + } + + private IOException recoveryFailure(StructureRecoveryResult recovery) { + IOException failure = new IOException("Unable to recover " + recovery.failures().size() + + " incomplete structure transaction(s) under " + stagingRoot()); + for (StructureRecoveryResult.Failure recoveryFailure : recovery.failures()) { + failure.addSuppressed(recoveryFailure.cause()); + } + return failure; + } + + private StructureOwnershipManifest.Provenance managedDatapackProvenance( + StructureResourceBundle bundle + ) { + TreeMap hashes = new TreeMap<>(); + TreeMap mappings = new TreeMap<>(); + StringBuilder closure = new StringBuilder(); + for (StructureResourceBundle.Resource resource : bundle.resources().values()) { + hashes.put(resource.relativePath(), resource.contentHash()); + mappings.put(resource.relativePath(), resource.relativePath()); + closure.append(resource.relativePath()) + .append('=') + .append(resource.contentHash()) + .append('\n'); + } + String closureHash = StructureHash.sha256(closure.toString().getBytes(StandardCharsets.UTF_8)); + StructureSource source = bundle.source(); + String planInput = "managed-datapack\n" + + source.kind() + '\n' + + source.key().value() + '\n' + + source.version() + '\n' + + source.contentHash() + '\n' + + closureHash; + return new StructureOwnershipManifest.Provenance( + StructureOwnershipManifest.Origin.MANAGED_DATAPACK, + UUID.randomUUID().toString(), + StructureHash.sha256(planInput.getBytes(StandardCharsets.UTF_8)), + closureHash, + Math.max(1L, System.currentTimeMillis()), + hashes, + mappings, + StructureOwnershipManifest.RollbackDisposition.NONE + ); + } + + private boolean managedDatapackUpgradeAllowed( + StructureOwnershipManifest previousManifest, + StructureSource nextSource + ) { + StructureOwnershipManifest.Origin origin = previousManifest.provenance().origin(); + if (origin != StructureOwnershipManifest.Origin.CREATED + && origin != StructureOwnershipManifest.Origin.MANAGED_DATAPACK) { + return false; + } + return previousManifest.source().kind() == nextSource.kind() + && previousManifest.source().key().equals(nextSource.key()); + } + + private WritePlan buildPlan( + StructureResourceBundle bundle, + StructureWriteOptions options, + StructureOwnershipManifest.Provenance provenance, + ExistingProvenancePolicy provenancePolicy + ) throws IOException { + StructureWriteMode mode = options.mode(); + StructureOwnershipManifest nextManifest = StructureOwnershipManifest.from(bundle, provenance); + String manifestRelativePath = nextManifest.relativePath(); + Path manifestPath = resolveTarget(manifestRelativePath); + ArrayList conflicts = new ArrayList<>(); + TreeMap previousResourceHashes = new TreeMap<>(); + + if (mode == StructureWriteMode.ADD_ONLY) { + if (files.exists(manifestPath)) { + conflicts.add(StructureWriteResult.Conflict.at( + manifestRelativePath, + StructureWriteResult.ConflictReason.MANIFEST_EXISTS + )); + } + findAddOnlyConflicts(bundle, conflicts); + return createPlan( + bundle, + nextManifest, + previousResourceHashes, + StructureWriteResult.Action.ADD, + conflicts + ); + } + + if (!files.exists(manifestPath)) { + if (!options.expectedManifestHash().isEmpty()) { + conflicts.add(StructureWriteResult.Conflict.staleManifest( + manifestRelativePath, + options.expectedManifestHash(), + "")); + } + findUnownedResources(bundle, previousResourceHashes, conflicts); + return createPlan( + bundle, + nextManifest, + previousResourceHashes, + StructureWriteResult.Action.ADD, + conflicts + ); + } + + if (!files.isRegularFile(manifestPath)) { + conflicts.add(StructureWriteResult.Conflict.invalidManifest( + manifestRelativePath, + "Ownership manifest is not a regular file" + )); + return createPlan( + bundle, + nextManifest, + previousResourceHashes, + StructureWriteResult.Action.OVERWRITE, + conflicts + ); + } + + byte[] manifestContent; + try { + manifestContent = readBoundedBytes( + manifestPath, + MAX_STRUCTURE_STATE_BYTES, + "Structure ownership manifest" + ); + } catch (IOException exception) { + conflicts.add(StructureWriteResult.Conflict.invalidManifest( + manifestRelativePath, + exception.toString())); + return createPlan( + bundle, + nextManifest, + previousResourceHashes, + StructureWriteResult.Action.OVERWRITE, + conflicts + ); + } + if (!options.expectedManifestHash().isEmpty()) { + String actualManifestHash = StructureHash.sha256(manifestContent); + if (!options.expectedManifestHash().equals(actualManifestHash)) { + conflicts.add(StructureWriteResult.Conflict.staleManifest( + manifestRelativePath, + options.expectedManifestHash(), + actualManifestHash)); + return createPlan( + bundle, + nextManifest, + previousResourceHashes, + StructureWriteResult.Action.OVERWRITE, + conflicts + ); + } + } + + StructureOwnershipManifest previousManifest; + try { + previousManifest = StructureOwnershipManifest.fromJson(manifestContent); + } catch (RuntimeException e) { + conflicts.add(StructureWriteResult.Conflict.invalidManifest(manifestRelativePath, e.toString())); + return createPlan( + bundle, + nextManifest, + previousResourceHashes, + StructureWriteResult.Action.OVERWRITE, + conflicts + ); + } + + if (!previousManifest.structure().equals(bundle.key())) { + conflicts.add(StructureWriteResult.Conflict.invalidManifest( + manifestRelativePath, + "Ownership manifest belongs to " + previousManifest.structure() + )); + return createPlan( + bundle, + nextManifest, + previousResourceHashes, + StructureWriteResult.Action.OVERWRITE, + conflicts + ); + } + + if (provenancePolicy == ExistingProvenancePolicy.PRESERVE) { + nextManifest = StructureOwnershipManifest.from(bundle, previousManifest.provenance()); + } else if (!managedDatapackUpgradeAllowed(previousManifest, bundle.source())) { + conflicts.add(new StructureWriteResult.Conflict( + manifestRelativePath, + StructureWriteResult.ConflictReason.PROVENANCE_MISMATCH, + "", + "", + "Managed datapack import cannot replace non-managed structure provenance" + )); + return createPlan( + bundle, + nextManifest, + previousResourceHashes, + StructureWriteResult.Action.OVERWRITE, + conflicts + ); + } + previousResourceHashes.putAll(previousManifest.resourceHashes()); + verifyOwnedResources(previousResourceHashes, conflicts); + findUnownedResources(bundle, previousResourceHashes, conflicts); + StructureWriteResult.Action action = nextManifest.equals(previousManifest) && conflicts.isEmpty() + ? StructureWriteResult.Action.NONE + : StructureWriteResult.Action.OVERWRITE; + return createPlan(bundle, nextManifest, previousResourceHashes, action, conflicts); + } + + private void findAddOnlyConflicts( + StructureResourceBundle bundle, + List conflicts + ) { + for (String relativePath : bundle.resources().keySet()) { + if (files.exists(resolveTarget(relativePath))) { + conflicts.add(StructureWriteResult.Conflict.at( + relativePath, + StructureWriteResult.ConflictReason.RESOURCE_EXISTS + )); + } + } + } + + private void findUnownedResources( + StructureResourceBundle bundle, + Map previousResourceHashes, + List conflicts + ) { + for (String relativePath : bundle.resources().keySet()) { + if (!previousResourceHashes.containsKey(relativePath) && files.exists(resolveTarget(relativePath))) { + conflicts.add(StructureWriteResult.Conflict.at( + relativePath, + StructureWriteResult.ConflictReason.UNOWNED_RESOURCE + )); + } + } + } + + private void verifyOwnedResources( + Map previousResourceHashes, + List conflicts + ) throws IOException { + for (Map.Entry entry : previousResourceHashes.entrySet()) { + String relativePath = entry.getKey(); + Path target = resolveTarget(relativePath); + if (!files.exists(target)) { + conflicts.add(StructureWriteResult.Conflict.at( + relativePath, + StructureWriteResult.ConflictReason.MISSING_OWNED_RESOURCE + )); + continue; + } + if (!files.isRegularFile(target)) { + conflicts.add(StructureWriteResult.Conflict.at( + relativePath, + StructureWriteResult.ConflictReason.NON_FILE_RESOURCE + )); + continue; + } + String actualHash = files.sha256(target); + if (!entry.getValue().equals(actualHash)) { + conflicts.add(StructureWriteResult.Conflict.modified(relativePath, entry.getValue(), actualHash)); + } + } + } + + private WritePlan createPlan( + StructureResourceBundle bundle, + StructureOwnershipManifest nextManifest, + Map previousResourceHashes, + StructureWriteResult.Action action, + List conflicts + ) { + TreeSet affectedResources = new TreeSet<>(previousResourceHashes.keySet()); + affectedResources.addAll(bundle.resources().keySet()); + affectedResources.add(nextManifest.relativePath()); + ArrayList orderedConflicts = new ArrayList<>(conflicts); + orderedConflicts.sort((left, right) -> left.relativePath().compareTo(right.relativePath())); + return new WritePlan( + bundle, + nextManifest, + nextManifest.toJson(), + action, + List.copyOf(orderedConflicts), + List.copyOf(affectedResources) + ); + } + + private List verifyReadSet(StructureTransactionReadSet readSet) { + if (readSet.isEmpty()) { + return List.of(); + } + ArrayList conflicts = new ArrayList<>(); + for (Map.Entry entry : readSet.fileHashes().entrySet()) { + String relativePath = entry.getKey(); + String expectedHash = entry.getValue(); + try { + Path target = resolveTarget(relativePath); + if (!files.exists(target)) { + conflicts.add(StructureWriteResult.Conflict.staleReadSet( + relativePath, + expectedHash, + "", + "Read-set file is missing")); + } else if (!files.isRegularFile(target)) { + conflicts.add(StructureWriteResult.Conflict.staleReadSet( + relativePath, + expectedHash, + "", + "Read-set path is not a regular file")); + } else { + String actualHash = sha256ReadSetTarget(target); + if (!expectedHash.equals(actualHash)) { + conflicts.add(StructureWriteResult.Conflict.staleReadSet( + relativePath, + expectedHash, + actualHash, + "Read-set file content changed")); + } + } + } catch (IOException | RuntimeException exception) { + conflicts.add(StructureWriteResult.Conflict.staleReadSet( + relativePath, + expectedHash, + "", + "Cannot verify read-set file: " + describe(exception))); + } + } + for (String relativePath : readSet.absentPaths()) { + try { + Path target = resolveTarget(relativePath); + if (files.exists(target)) { + conflicts.add(StructureWriteResult.Conflict.staleReadSet( + relativePath, + "", + "", + "Read-set path was expected to remain absent")); + } + } catch (RuntimeException exception) { + conflicts.add(StructureWriteResult.Conflict.staleReadSet( + relativePath, + "", + "", + "Cannot verify absent read-set path: " + describe(exception))); + } + } + for (Map.Entry> entry : readSet.directoryEntries().entrySet()) { + String relativePath = entry.getKey(); + List expectedEntries = entry.getValue(); + try { + List actualEntries = actualDirectoryEntries(relativePath); + if (!expectedEntries.equals(actualEntries)) { + String expectedHash = StructureHash.sha256( + String.join("\n", expectedEntries).getBytes(StandardCharsets.UTF_8)); + String actualHash = StructureHash.sha256( + String.join("\n", actualEntries).getBytes(StandardCharsets.UTF_8)); + conflicts.add(StructureWriteResult.Conflict.staleReadSet( + relativePath, + expectedHash, + actualHash, + "Read-set directory membership changed")); + } + } catch (IOException | RuntimeException exception) { + conflicts.add(StructureWriteResult.Conflict.staleReadSet( + relativePath, + "", + "", + "Cannot verify read-set directory: " + describe(exception))); + } + } + return orderedConflicts(conflicts); + } + + private List actualDirectoryEntries(String relativePath) throws IOException { + Path directory = resolveTarget(relativePath); + if (!files.exists(directory)) { + return List.of(); + } + if (!files.isDirectory(directory)) { + throw new IOException("Read-set directory is not a directory: " + relativePath); + } + ArrayList entries = new ArrayList<>(); + try (Stream paths = Files.walk(directory)) { + Iterator iterator = paths.iterator(); + while (iterator.hasNext()) { + Path path = iterator.next(); + if (path.equals(directory)) { + continue; + } + if (Files.isSymbolicLink(path)) { + throw new IOException("Read-set directory contains a symbolic link: " + path); + } + if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Read-set directory contains a non-file entry: " + path); + } + entries.add(packRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/')); + if (entries.size() > StructureTransactionReadSet.MAX_ENTRIES) { + throw new IOException("Read-set directory exceeds " + + StructureTransactionReadSet.MAX_ENTRIES + " entries"); + } + } + } + Collections.sort(entries); + return List.copyOf(entries); + } + + private String sha256ReadSetTarget(Path target) throws IOException { + try (InputStream input = Files.newInputStream( + target, + StandardOpenOption.READ, + LinkOption.NOFOLLOW_LINKS)) { + return StructureHash.sha256(new BoundedReadSetInputStream(input)); + } + } + + private void requireReadSetUnchanged(StructureTransactionReadSet readSet) throws IOException { + List conflicts = verifyReadSet(readSet); + if (conflicts.isEmpty()) { + return; + } + StructureWriteResult.Conflict conflict = conflicts.getFirst(); + throw new ReadSetChangedException(conflict); + } + + private StructureWriteResult readSetConflictResult( + StructureResourceBundle bundle, + List conflicts + ) { + StructureOwnershipManifest manifest = StructureOwnershipManifest.from(bundle); + TreeSet affectedResources = new TreeSet<>(bundle.resources().keySet()); + affectedResources.addAll(conflicts.stream().map(StructureWriteResult.Conflict::relativePath).toList()); + affectedResources.add(manifest.relativePath()); + return new StructureWriteResult( + StructureWriteResult.Status.OWNERSHIP_CONFLICT, + StructureWriteResult.Action.NONE, + orderedConflicts(conflicts), + List.copyOf(affectedResources), + manifest.relativePath(), + Optional.empty() + ); + } + + private List orderedConflicts( + List conflicts + ) { + ArrayList ordered = new ArrayList<>(conflicts); + ordered.sort((left, right) -> left.relativePath().compareTo(right.relativePath())); + return List.copyOf(ordered); + } + + private StructureWriteResult commitClaim( + StructureOwnershipManifest manifest, + StructureTransactionReadSet readSet + ) { + UUID transactionId = UUID.randomUUID(); + Path transactionRoot = stagingRoot().resolve(transactionId.toString()).normalize(); + Path stagedRoot = transactionRoot.resolve("staged"); + Path backupRoot = transactionRoot.resolve("backup"); + Path stagedManifest = stagedRoot.resolve("ownership-manifest.json"); + byte[] manifestContent = manifest.toJson(); + ArrayList installedTargets = new ArrayList<>(); + StructureTransactionJournal journal; + try { + if (manifestContent.length > MAX_STRUCTURE_STATE_BYTES) { + throw new IOException("Structure ownership manifest exceeds " + + MAX_STRUCTURE_STATE_BYTES + " bytes"); + } + files.createDirectories(stagedRoot); + files.createDirectories(backupRoot); + files.writeNew(stagedManifest, manifestContent); + files.forceFile(stagedManifest); + StructureTransactionJournal.Target target = new StructureTransactionJournal.Target( + manifest.relativePath(), + false, + "", + StructureHash.sha256(manifestContent)); + journal = StructureTransactionJournal.prepared(transactionId, List.of(target)); + writeJournal(transactionRoot, journal); + files.forceDirectory(transactionRoot); + files.forceDirectory(stagingRoot()); + requireReadSetUnchanged(readSet); + verifyTargetSnapshot(journal); + Path manifestTarget = resolveTarget(manifest.relativePath()); + files.createDirectories(Objects.requireNonNull(manifestTarget.getParent(), "manifest target parent")); + files.moveNew(stagedManifest, manifestTarget); + installedTargets.add(new InstalledTarget(manifestTarget, StructureHash.sha256(manifestContent))); + files.forceFile(manifestTarget); + files.forceDirectory(Objects.requireNonNull(manifestTarget.getParent(), "manifest target parent")); + } catch (IOException | RuntimeException commitFailure) { + return rollbackClaimResult(manifest, transactionRoot, installedTargets, commitFailure); + } + + boolean committedJournalWritten = false; + try { + writeJournal(transactionRoot, journal.committed()); + committedJournalWritten = true; + files.forceDirectory(transactionRoot); + } catch (IOException | RuntimeException commitPhaseFailure) { + if (committedJournalWritten || isCommittedJournal(transactionRoot, commitPhaseFailure)) { + return claimResult( + StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED, + manifest, + List.of(), + Optional.of(commitPhaseFailure)); + } + return rollbackClaimResult(manifest, transactionRoot, installedTargets, commitPhaseFailure); + } + + try { + cleanupTransaction(transactionRoot); + } catch (IOException | RuntimeException cleanupFailure) { + return claimResult( + StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED, + manifest, + List.of(), + Optional.of(cleanupFailure)); + } + return claimResult( + StructureWriteResult.Status.ADDED, + manifest, + List.of(), + Optional.empty()); + } + + private StructureWriteResult rollbackClaimResult( + StructureOwnershipManifest manifest, + Path transactionRoot, + List installedTargets, + Throwable failure + ) { + Optional rollbackFailure = rollback(Map.of(), installedTargets); + if (rollbackFailure.isPresent()) { + failure.addSuppressed(new IOException( + "Transaction recovery data retained at " + transactionRoot, + rollbackFailure.get())); + return claimResult( + StructureWriteResult.Status.FAILED, + manifest, + List.of(), + Optional.of(failure)); + } + cleanupAfterFailure(transactionRoot, failure); + if (failure instanceof ReadSetChangedException readSetChanged) { + return claimResult( + StructureWriteResult.Status.OWNERSHIP_CONFLICT, + manifest, + List.of(readSetChanged.conflict()), + Optional.empty()); + } + return claimResult( + StructureWriteResult.Status.ROLLED_BACK, + manifest, + List.of(), + Optional.of(failure)); + } + + private StructureWriteResult claimResult( + StructureWriteResult.Status status, + StructureOwnershipManifest manifest, + List conflicts, + Optional failure + ) { + TreeSet affectedResources = new TreeSet<>(manifest.resourceHashes().keySet()); + affectedResources.add(manifest.relativePath()); + return new StructureWriteResult( + status, + StructureWriteResult.Action.ADD, + orderedConflicts(conflicts), + List.copyOf(affectedResources), + manifest.relativePath(), + failure + ); + } + + private StructureWriteResult commit(WritePlan plan, StructureTransactionReadSet readSet) { + UUID transactionId = UUID.randomUUID(); + Path transactionRoot = stagingRoot().resolve(transactionId.toString()).normalize(); + Path stagedRoot = transactionRoot.resolve("staged"); + Path backupRoot = transactionRoot.resolve("backup"); + Path stagedManifest = stagedRoot.resolve("ownership-manifest.json"); + LinkedHashMap backups = new LinkedHashMap<>(); + ArrayList installedTargets = new ArrayList<>(); + StructureTransactionJournal journal; + + try { + files.createDirectories(stagedRoot); + files.createDirectories(backupRoot); + stageResources(plan, stagedRoot, stagedManifest); + journal = createJournal(plan, transactionId); + writeJournal(transactionRoot, journal); + files.forceDirectory(transactionRoot); + files.forceDirectory(stagingRoot()); + requireReadSetUnchanged(readSet); + verifyTargetSnapshot(journal); + backupTargets(journal, backupRoot, backups); + installResources(plan, stagedRoot, stagedManifest, installedTargets); + } catch (IOException | RuntimeException commitFailure) { + return rollbackResult(plan, transactionRoot, backups, installedTargets, commitFailure); + } + + boolean committedJournalWritten = false; + try { + writeJournal(transactionRoot, journal.committed()); + committedJournalWritten = true; + files.forceDirectory(transactionRoot); + } catch (IOException | RuntimeException commitPhaseFailure) { + if (committedJournalWritten || isCommittedJournal(transactionRoot, commitPhaseFailure)) { + return result( + StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED, + plan, + Optional.of(commitPhaseFailure) + ); + } + return rollbackResult(plan, transactionRoot, backups, installedTargets, commitPhaseFailure); + } + + try { + cleanupTransaction(transactionRoot); + } catch (IOException | RuntimeException cleanupFailure) { + return result( + StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED, + plan, + Optional.of(cleanupFailure) + ); + } + + StructureWriteResult.Status status = plan.action() == StructureWriteResult.Action.ADD + ? StructureWriteResult.Status.ADDED + : StructureWriteResult.Status.OVERWRITTEN; + return result(status, plan, Optional.empty()); + } + + private void stageResources(WritePlan plan, Path stagedRoot, Path stagedManifest) throws IOException { + for (StructureResourceBundle.Resource resource : plan.bundle().resources().values()) { + Path staged = stagedRoot.resolve(resource.relativePath()).normalize(); + files.createDirectories(Objects.requireNonNull(staged.getParent(), "staged parent")); + files.writeNew(staged, resource.contentForWrite()); + files.forceFile(staged); + } + files.createDirectories(Objects.requireNonNull(stagedManifest.getParent(), "manifest parent")); + files.writeNew(stagedManifest, plan.nextManifestContent()); + files.forceFile(stagedManifest); + } + + private StructureTransactionJournal createJournal(WritePlan plan, UUID transactionId) throws IOException { + ArrayList targets = new ArrayList<>(plan.affectedResources().size()); + for (String relativePath : plan.affectedResources()) { + Path target = resolveTarget(relativePath); + boolean hadOriginal = files.exists(target); + targets.add(new StructureTransactionJournal.Target( + relativePath, + hadOriginal, + hadOriginal ? files.sha256(target) : "", + replacementHash(plan, relativePath) + )); + } + return StructureTransactionJournal.prepared(transactionId, targets); + } + + private void writeJournal( + Path transactionRoot, + StructureTransactionJournal journal + ) throws IOException { + byte[] content = journal.toJson(); + if (content.length > MAX_STRUCTURE_STATE_BYTES) { + throw new IOException("Structure transaction journal exceeds " + + MAX_STRUCTURE_STATE_BYTES + " bytes"); + } + Path journalPath = transactionRoot.resolve(StructureTransactionJournal.FILE_NAME); + Path nextJournalPath = transactionRoot.resolve(StructureTransactionJournal.NEXT_FILE_NAME); + files.deleteIfExists(nextJournalPath); + files.writeNew(nextJournalPath, content); + files.forceFile(nextJournalPath); + files.move(nextJournalPath, journalPath); + } + + private boolean isCommittedJournal(Path transactionRoot, Throwable commitPhaseFailure) { + Path journalPath = transactionRoot.resolve(StructureTransactionJournal.FILE_NAME); + if (!files.isRegularFile(journalPath)) { + return false; + } + try { + return StructureTransactionJournal.fromJson(readBoundedBytes( + journalPath, + MAX_STRUCTURE_STATE_BYTES, + "Structure transaction journal" + )).phase() + == StructureTransactionJournal.Phase.COMMITTED; + } catch (IOException | RuntimeException e) { + commitPhaseFailure.addSuppressed(e); + return false; + } + } + + private void verifyTargetSnapshot(StructureTransactionJournal journal) throws IOException { + for (StructureTransactionJournal.Target state : journal.targets()) { + Path target = resolveTarget(state.relativePath()); + boolean exists = files.exists(target); + if (exists != state.hadOriginal()) { + throw new IOException("Structure transaction target changed during preparation: " + + state.relativePath()); + } + if (state.hadOriginal()) { + verifyOriginalTarget(target, state); + } + } + } + + private void backupTargets( + StructureTransactionJournal journal, + Path backupRoot, + Map backups + ) throws IOException { + for (StructureTransactionJournal.Target state : journal.targets()) { + if (!state.hadOriginal()) { + continue; + } + Path target = resolveTarget(state.relativePath()); + Path backup = resolveTransactionPath(backupRoot, state.relativePath()); + files.createDirectories(Objects.requireNonNull(backup.getParent(), "backup parent")); + backups.put(target, backup); + files.moveNew(target, backup); + verifyOriginalContent(backup, state); + files.forceDirectory(Objects.requireNonNull(target.getParent(), "backup target parent")); + files.forceDirectory(Objects.requireNonNull(backup.getParent(), "backup parent")); + } + } + + private void installResources( + WritePlan plan, + Path stagedRoot, + Path stagedManifest, + List installedTargets + ) throws IOException { + for (StructureResourceBundle.Resource resource : plan.bundle().resources().values()) { + Path source = stagedRoot.resolve(resource.relativePath()).normalize(); + Path target = resolveTarget(resource.relativePath()); + files.createDirectories(Objects.requireNonNull(target.getParent(), "target parent")); + files.moveNew(source, target); + installedTargets.add(new InstalledTarget(target, resource.contentHash())); + files.forceFile(target); + files.forceDirectory(Objects.requireNonNull(target.getParent(), "target parent")); + } + Path manifestTarget = resolveTarget(plan.nextManifest().relativePath()); + files.createDirectories(Objects.requireNonNull(manifestTarget.getParent(), "manifest target parent")); + files.moveNew(stagedManifest, manifestTarget); + installedTargets.add(new InstalledTarget( + manifestTarget, + StructureHash.sha256(plan.nextManifestContent()) + )); + files.forceFile(manifestTarget); + files.forceDirectory(Objects.requireNonNull(manifestTarget.getParent(), "manifest target parent")); + } + + private StructureWriteResult rollbackResult( + WritePlan plan, + Path transactionRoot, + Map backups, + List installedTargets, + Throwable commitFailure + ) { + Optional rollbackFailure = rollback(backups, installedTargets); + if (rollbackFailure.isPresent()) { + IOException retainedRecovery = new IOException( + "Transaction recovery data retained at " + transactionRoot, + rollbackFailure.get() + ); + commitFailure.addSuppressed(retainedRecovery); + return result(StructureWriteResult.Status.FAILED, plan, Optional.of(commitFailure)); + } + cleanupAfterFailure(transactionRoot, commitFailure); + if (commitFailure instanceof ReadSetChangedException readSetChanged) { + return readSetConflictResult(plan.bundle(), List.of(readSetChanged.conflict())); + } + return result(StructureWriteResult.Status.ROLLED_BACK, plan, Optional.of(commitFailure)); + } + + private Optional rollback(Map backups, List installedTargets) { + Throwable rollbackFailure = null; + ArrayList reversedTargets = new ArrayList<>(installedTargets); + Collections.reverse(reversedTargets); + for (InstalledTarget installedTarget : reversedTargets) { + try { + removeInstalledTarget(installedTarget); + } catch (IOException | RuntimeException e) { + rollbackFailure = appendFailure(rollbackFailure, e); + } + } + + ArrayList> reversedBackups = new ArrayList<>(backups.entrySet()); + Collections.reverse(reversedBackups); + for (Map.Entry entry : reversedBackups) { + Path target = entry.getKey(); + Path backup = entry.getValue(); + if (!files.exists(backup)) { + continue; + } + try { + files.createDirectories(Objects.requireNonNull(target.getParent(), "rollback target parent")); + files.moveNew(backup, target); + files.forceFile(target); + files.forceDirectory(Objects.requireNonNull(target.getParent(), "rollback target parent")); + files.forceDirectory(Objects.requireNonNull(backup.getParent(), "rollback backup parent")); + } catch (IOException | RuntimeException e) { + rollbackFailure = appendFailure(rollbackFailure, e); + } + } + return Optional.ofNullable(rollbackFailure); + } + + private void cleanupAfterFailure(Path transactionRoot, Throwable failure) { + try { + cleanupTransaction(transactionRoot); + } catch (IOException | RuntimeException cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + } + + private void cleanupTransaction(Path transactionRoot) throws IOException { + files.deleteTree(transactionRoot.resolve("staged")); + files.deleteTree(transactionRoot.resolve("backup")); + files.deleteIfExists(transactionRoot.resolve(StructureTransactionJournal.NEXT_FILE_NAME)); + forceExistingDirectory(transactionRoot); + files.deleteIfExists(transactionRoot.resolve(StructureTransactionJournal.FILE_NAME)); + files.deleteTree(transactionRoot); + Path stagingRoot = stagingRoot(); + if (files.exists(stagingRoot)) { + files.forceDirectory(stagingRoot); + } + } + + private Throwable appendFailure(Throwable existing, Throwable next) { + if (existing == null) { + return next; + } + existing.addSuppressed(next); + return existing; + } + + private String describe(Throwable throwable) { + String message = throwable.getMessage(); + return message == null || message.isBlank() ? throwable.getClass().getSimpleName() : message; + } + + private void verifyOriginalTarget( + Path target, + StructureTransactionJournal.Target state + ) throws IOException { + if (!files.isRegularFile(target)) { + throw new IOException("Structure transaction original target is missing or is not a regular file for " + + state.relativePath()); + } + verifyOriginalContent(target, state); + } + + private void verifyOriginalContent( + Path path, + StructureTransactionJournal.Target state + ) throws IOException { + String actualHash = files.sha256(path); + if (!state.originalHash().equals(actualHash)) { + throw new IOException("Structure transaction original content hash mismatch for " + + state.relativePath()); + } + } + + private void removeReplacementIfPresent( + Path target, + StructureTransactionJournal.Target state + ) throws IOException { + if (!files.exists(target)) { + return; + } + if (!files.isRegularFile(target)) { + throw new IOException("Structure transaction replacement is not a regular file for " + + state.relativePath()); + } + if (state.replacementHash().isEmpty()) { + throw new IOException("Unexpected replacement appeared while recovering " + state.relativePath()); + } + String actualHash = files.sha256(target); + if (!state.replacementHash().equals(actualHash)) { + throw new IOException("Structure transaction replacement content changed after the interrupted write for " + + state.relativePath()); + } + files.deleteIfExists(target); + forceExistingDirectory(Objects.requireNonNull(target.getParent(), "replacement target parent")); + } + + private String replacementHash(WritePlan plan, String relativePath) { + StructureResourceBundle.Resource resource = plan.bundle().resources().get(relativePath); + if (resource != null) { + return resource.contentHash(); + } + if (plan.nextManifest().relativePath().equals(relativePath)) { + return StructureHash.sha256(plan.nextManifestContent()); + } + return ""; + } + + private void forceExistingDirectory(Path path) throws IOException { + if (files.isDirectory(path)) { + files.forceDirectory(path); + } + } + + private void removeInstalledTarget(InstalledTarget installedTarget) throws IOException { + Path target = installedTarget.path(); + if (!files.exists(target)) { + return; + } + if (!files.isRegularFile(target) || !installedTarget.contentHash().equals(files.sha256(target))) { + throw new IOException("Installed structure target changed before rollback: " + target); + } + files.deleteIfExists(target); + forceExistingDirectory(Objects.requireNonNull(target.getParent(), "rollback target parent")); + } + + private StructureWriteResult result( + StructureWriteResult.Status status, + WritePlan plan, + Optional failure + ) { + return new StructureWriteResult( + status, + plan.action(), + plan.conflicts(), + plan.affectedResources(), + plan.nextManifest().relativePath(), + failure + ); + } + + private StructureWriteResult failedResult(StructureResourceBundle bundle, Throwable failure) { + StructureOwnershipManifest manifest = StructureOwnershipManifest.from(bundle); + TreeSet affectedResources = new TreeSet<>(bundle.resources().keySet()); + affectedResources.add(manifest.relativePath()); + return new StructureWriteResult( + StructureWriteResult.Status.FAILED, + StructureWriteResult.Action.NONE, + List.of(), + List.copyOf(affectedResources), + manifest.relativePath(), + Optional.of(failure) + ); + } + + private Path resolveTarget(String relativePath) { + return resolveWithin(packRoot, relativePath, "Resource path escapes the pack root: "); + } + + private Path resolveTransactionPath(Path root, String relativePath) { + return resolveWithin(root, relativePath, "Transaction path escapes its root: "); + } + + private Path stagingRoot() { + return resolveWithin(packRoot, STAGING_RELATIVE_PATH, "Structure staging path escapes the pack root: "); + } + + private ProcessLock acquireProcessLock() throws IOException { + Path lockPath = resolveWithin( + packRoot, + PROCESS_LOCK_RELATIVE_PATH, + "Structure authoring lock path escapes the pack root: " + ); + Files.createDirectories(Objects.requireNonNull(lockPath.getParent(), "structure authoring lock parent")); + FileChannel channel = FileChannel.open( + lockPath, + StandardOpenOption.CREATE, + StandardOpenOption.WRITE + ); + try { + return new ProcessLock(channel, channel.lock()); + } catch (IOException | OverlappingFileLockException e) { + channel.close(); + throw new IOException("Unable to acquire the structure authoring lock for " + packRoot, e); + } + } + + private byte[] readBoundedBytes(Path path, int maxBytes, String purpose) throws IOException { + byte[] content; + try (InputStream input = Files.newInputStream( + path, + StandardOpenOption.READ, + LinkOption.NOFOLLOW_LINKS + )) { + content = input.readNBytes(maxBytes + 1); + } + if (content.length > maxBytes) { + throw new IOException(purpose + " exceeds " + maxBytes + " bytes"); + } + return content; + } + + private Path resolveWithin(Path root, String relativePath, String errorPrefix) { + Path target = root.resolve(relativePath).normalize(); + if (!target.startsWith(root) || target.equals(root)) { + throw new IllegalArgumentException(errorPrefix + relativePath); + } + rejectSymbolicLinks(root, target); + return target; + } + + private void rejectSymbolicLinks(Path root, Path target) { + Path current = root; + Path relative = root.relativize(target); + for (Path segment : relative) { + current = current.resolve(segment); + if (Files.exists(current, LinkOption.NOFOLLOW_LINKS) && Files.isSymbolicLink(current)) { + throw new IllegalArgumentException("Structure authoring path contains a symbolic link: " + current); + } + } + } + + private static Path canonicalPackRoot(Path root) { + Path normalized = root.toAbsolutePath().normalize(); + if (!Files.exists(normalized)) { + return normalized; + } + try { + return normalized.toRealPath(); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to resolve the structure pack root " + normalized, e); + } + } + + private static Path validateRecoveryOwnerRoot(Path root) { + Path normalized = root.toAbsolutePath().normalize(); + Path parent = Objects.requireNonNull(normalized.getParent(), "recovery owner parent"); + Path parentName = Objects.requireNonNull(parent.getFileName(), "recovery owner directory"); + Path transactionName = Objects.requireNonNull(normalized.getFileName(), "recovery owner transaction"); + if (!".iris-datapack-transactions".equals(parentName.toString())) { + throw new IllegalArgumentException("Recovery owner is outside the datapack transaction directory"); + } + try { + UUID.fromString(transactionName.toString()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Recovery owner has an invalid transaction directory", e); + } + try { + if (Files.isSymbolicLink(parent) || !Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) { + throw new IllegalArgumentException("Recovery owner transaction parent is unsafe"); + } + return parent.toRealPath().resolve(transactionName.toString()); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to resolve recovery owner transaction parent", e); + } + } + + private record RecoveryClaim( + int schemaVersion, + String coordinatorTransactionRoot, + UUID coordinatorTransactionId, + UUID claimId + ) { + } + + private enum RecoveryOutcome { + RESTORED_PREPARED, + CLEANED_COMMITTED, + CLEANED_ORPHAN + } + + private enum ExistingProvenancePolicy { + PRESERVE, + INSTALL_MANAGED_DATAPACK + } + + private record InstalledTarget(Path path, String contentHash) { + private InstalledTarget { + Objects.requireNonNull(path, "path"); + if (!StructureHash.isSha256(contentHash)) { + throw new IllegalArgumentException("Installed target content hash is invalid"); + } + } + } + + private static final class ReadSetChangedException extends IOException { + private final StructureWriteResult.Conflict conflict; + + private ReadSetChangedException(StructureWriteResult.Conflict conflict) { + super("Structure transaction read set changed at " + conflict.relativePath() + + ": " + conflict.detail()); + this.conflict = conflict; + } + + private StructureWriteResult.Conflict conflict() { + return conflict; + } + } + + private static final class BoundedReadSetInputStream extends InputStream { + private final InputStream delegate; + private long consumed; + + private BoundedReadSetInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + int value = delegate.read(); + if (value >= 0) { + recordBytes(1L); + } + return value; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + int read = delegate.read(buffer, offset, length); + if (read > 0) { + recordBytes(read); + } + return read; + } + + private void recordBytes(long bytes) throws IOException { + consumed = Math.addExact(consumed, bytes); + if (consumed > MAX_VERIFIED_READ_FILE_BYTES) { + throw new IOException("Verified read-set file exceeds " + + MAX_VERIFIED_READ_FILE_BYTES + " bytes"); + } + } + } + + private record RemovalPlan(List targets) { + } + + private record ProcessLock(FileChannel channel, FileLock lock) implements AutoCloseable { + private ProcessLock { + Objects.requireNonNull(channel, "channel"); + Objects.requireNonNull(lock, "lock"); + } + + @Override + public void close() throws IOException { + IOException failure = null; + try { + lock.release(); + } catch (IOException e) { + failure = e; + } + try { + channel.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + if (failure != null) { + throw failure; + } + } + } + + private record WritePlan( + StructureResourceBundle bundle, + StructureOwnershipManifest nextManifest, + byte[] nextManifestContent, + StructureWriteResult.Action action, + List conflicts, + List affectedResources + ) { + private WritePlan { + nextManifestContent = nextManifestContent.clone(); + } + + @Override + public byte[] nextManifestContent() { + return nextManifestContent.clone(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteMode.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteMode.java new file mode 100644 index 000000000..d9824887c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteMode.java @@ -0,0 +1,24 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +public enum StructureWriteMode { + ADD_ONLY, + OVERWRITE +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteOptions.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteOptions.java new file mode 100644 index 000000000..976c03fa3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteOptions.java @@ -0,0 +1,54 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import java.util.Objects; + +public record StructureWriteOptions(StructureWriteMode mode, boolean dryRun, String expectedManifestHash) { + public StructureWriteOptions { + Objects.requireNonNull(mode, "mode"); + expectedManifestHash = expectedManifestHash == null ? "" : expectedManifestHash.trim(); + if (!expectedManifestHash.isEmpty() && !StructureHash.isSha256(expectedManifestHash)) { + throw new IllegalArgumentException("Expected structure manifest hash must be SHA-256"); + } + if (!expectedManifestHash.isEmpty() && mode != StructureWriteMode.OVERWRITE) { + throw new IllegalArgumentException("Expected structure manifest hashes require overwrite mode"); + } + } + + public StructureWriteOptions(StructureWriteMode mode, boolean dryRun) { + this(mode, dryRun, ""); + } + + public static StructureWriteOptions addOnly() { + return new StructureWriteOptions(StructureWriteMode.ADD_ONLY, false); + } + + public static StructureWriteOptions overwrite() { + return new StructureWriteOptions(StructureWriteMode.OVERWRITE, false); + } + + public static StructureWriteOptions preview(StructureWriteMode mode) { + return new StructureWriteOptions(mode, true); + } + + public static StructureWriteOptions overwriteExpected(String manifestHash) { + return new StructureWriteOptions(StructureWriteMode.OVERWRITE, false, manifestHash); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteResult.java b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteResult.java new file mode 100644 index 000000000..3826f9593 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/authoring/StructureWriteResult.java @@ -0,0 +1,147 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.structure.authoring; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public record StructureWriteResult( + Status status, + Action action, + List conflicts, + List affectedResources, + String manifestPath, + Optional failure +) { + public StructureWriteResult { + Objects.requireNonNull(status, "status"); + Objects.requireNonNull(action, "action"); + Objects.requireNonNull(conflicts, "conflicts"); + Objects.requireNonNull(affectedResources, "affectedResources"); + Objects.requireNonNull(manifestPath, "manifestPath"); + Objects.requireNonNull(failure, "failure"); + conflicts = List.copyOf(conflicts); + affectedResources = List.copyOf(affectedResources); + } + + public boolean successful() { + return switch (status) { + case DRY_RUN, ADDED, OVERWRITTEN, UNCHANGED, COMMITTED_CLEANUP_REQUIRED -> true; + case ADD_ONLY_CONFLICT, OWNERSHIP_CONFLICT, ROLLED_BACK, FAILED -> false; + }; + } + + public boolean committed() { + return switch (status) { + case ADDED, OVERWRITTEN, UNCHANGED, COMMITTED_CLEANUP_REQUIRED -> true; + case DRY_RUN, ADD_ONLY_CONFLICT, OWNERSHIP_CONFLICT, ROLLED_BACK, FAILED -> false; + }; + } + + public enum Status { + DRY_RUN, + ADDED, + OVERWRITTEN, + UNCHANGED, + ADD_ONLY_CONFLICT, + OWNERSHIP_CONFLICT, + ROLLED_BACK, + FAILED, + COMMITTED_CLEANUP_REQUIRED + } + + public enum Action { + ADD, + OVERWRITE, + NONE + } + + public enum ConflictReason { + RESOURCE_EXISTS, + MANIFEST_EXISTS, + UNOWNED_RESOURCE, + MODIFIED_RESOURCE, + MISSING_OWNED_RESOURCE, + NON_FILE_RESOURCE, + INVALID_MANIFEST, + PROVENANCE_MISMATCH, + STALE_MANIFEST, + STALE_READ_SET + } + + public record Conflict( + String relativePath, + ConflictReason reason, + String expectedHash, + String actualHash, + String detail + ) { + public Conflict { + Objects.requireNonNull(relativePath, "relativePath"); + Objects.requireNonNull(reason, "reason"); + Objects.requireNonNull(expectedHash, "expectedHash"); + Objects.requireNonNull(actualHash, "actualHash"); + Objects.requireNonNull(detail, "detail"); + } + + public static Conflict at(String relativePath, ConflictReason reason) { + return new Conflict(relativePath, reason, "", "", ""); + } + + public static Conflict modified(String relativePath, String expectedHash, String actualHash) { + return new Conflict( + relativePath, + ConflictReason.MODIFIED_RESOURCE, + expectedHash, + actualHash, + "Resource content differs from its ownership manifest" + ); + } + + public static Conflict invalidManifest(String relativePath, String detail) { + return new Conflict(relativePath, ConflictReason.INVALID_MANIFEST, "", "", detail); + } + + public static Conflict staleManifest(String relativePath, String expectedHash, String actualHash) { + return new Conflict( + relativePath, + ConflictReason.STALE_MANIFEST, + expectedHash, + actualHash, + "Ownership manifest changed after the graph edit was loaded" + ); + } + + public static Conflict staleReadSet( + String relativePath, + String expectedHash, + String actualHash, + String detail + ) { + return new Conflict( + relativePath, + ConflictReason.STALE_READ_SET, + expectedHash, + actualHash, + detail + ); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionDiagnostic.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionDiagnostic.java new file mode 100644 index 000000000..7ef7c495d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionDiagnostic.java @@ -0,0 +1,88 @@ +package art.arcane.iris.core.structure.conversion; + +import java.util.Objects; + +public record IrisStructureAdoptionDiagnostic( + Severity severity, + Code code, + String resource, + String detail, + String recommendation +) implements Comparable { + public IrisStructureAdoptionDiagnostic { + Objects.requireNonNull(severity, "severity"); + Objects.requireNonNull(code, "code"); + resource = normalize(resource); + detail = requireText(detail, "detail"); + recommendation = normalize(recommendation); + } + + public String summary() { + String location = resource.isEmpty() ? "" : " [" + resource + "]"; + String action = recommendation.isEmpty() ? "" : " " + recommendation; + return severity + " " + code + location + ": " + detail + action; + } + + public boolean blocking() { + return severity == Severity.ERROR; + } + + @Override + public int compareTo(IrisStructureAdoptionDiagnostic other) { + int severityComparison = severity.compareTo(other.severity); + if (severityComparison != 0) { + return severityComparison; + } + int codeComparison = code.compareTo(other.code); + if (codeComparison != 0) { + return codeComparison; + } + int resourceComparison = resource.compareTo(other.resource); + if (resourceComparison != 0) { + return resourceComparison; + } + return detail.compareTo(other.detail); + } + + private static String requireText(String value, String name) { + String normalized = normalize(value); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Adoption diagnostic " + name + " cannot be blank"); + } + return normalized; + } + + private static String normalize(String value) { + return value == null ? "" : value.trim(); + } + + public enum Severity { + ERROR, + WARNING, + INFO + } + + public enum Code { + SOURCE_GRAPH_INVALID, + SOURCE_ALREADY_OWNED, + SOURCE_RESOURCE_UNSAFE, + SOURCE_RESOURCE_LIMIT, + SOURCE_RESOURCE_CHANGED, + TARGET_NAMESPACE_UNSUPPORTED, + TARGET_RESOURCE_EXISTS, + TARGET_RESOURCE_UNSAFE, + TARGET_MAPPING_COLLISION, + TARGET_REQUIRED_FOR_CLONE, + MANAGED_INPUT_REQUIRES_CLONE, + SHARED_DEPENDENCY, + EXCLUSIVITY_UNPROVEN, + IN_PLACE_AVAILABLE, + CLONE_SELECTED, + PLAN_BLOCKED, + PLAN_EXPIRED, + PLAN_UNKNOWN, + PLAN_STALE, + TRANSACTION_FAILED, + APPLIED + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionDisposition.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionDisposition.java new file mode 100644 index 000000000..9d90e5a74 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionDisposition.java @@ -0,0 +1,7 @@ +package art.arcane.iris.core.structure.conversion; + +public enum IrisStructureAdoptionDisposition { + IN_PLACE, + CLONE_REQUIRED, + BLOCKED +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionInputKind.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionInputKind.java new file mode 100644 index 000000000..ee40b8d36 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionInputKind.java @@ -0,0 +1,6 @@ +package art.arcane.iris.core.structure.conversion; + +public enum IrisStructureAdoptionInputKind { + UNOWNED_IRIS, + MANAGED_DATAPACK +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionLimits.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionLimits.java new file mode 100644 index 000000000..1273b2ea4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionLimits.java @@ -0,0 +1,56 @@ +package art.arcane.iris.core.structure.conversion; + +import java.time.Duration; +import java.util.Objects; + +public record IrisStructureAdoptionLimits( + int maxResources, + int maxJsonBytes, + int maxBinaryBytes, + long maxTotalBytes, + int maxStructuresScanned, + int maxDiagnostics, + int maxActivePlans, + Duration planTtl +) { + public IrisStructureAdoptionLimits { + if (maxResources < 1 || maxResources > 100_000) { + throw new IllegalArgumentException("Adoption resource limit must be between 1 and 100000"); + } + if (maxJsonBytes < 1 || maxJsonBytes > 64 * 1024 * 1024) { + throw new IllegalArgumentException("Adoption JSON limit must be between 1 and 67108864 bytes"); + } + if (maxBinaryBytes < 1 || maxBinaryBytes > 256 * 1024 * 1024) { + throw new IllegalArgumentException("Adoption binary limit must be between 1 and 268435456 bytes"); + } + if (maxTotalBytes < 1L || maxTotalBytes > 1024L * 1024L * 1024L) { + throw new IllegalArgumentException("Adoption aggregate limit must be between 1 and 1073741824 bytes"); + } + if (maxStructuresScanned < 1 || maxStructuresScanned > 100_000) { + throw new IllegalArgumentException("Adoption structure scan limit must be between 1 and 100000"); + } + if (maxDiagnostics < 1 || maxDiagnostics > 10_000) { + throw new IllegalArgumentException("Adoption diagnostic limit must be between 1 and 10000"); + } + if (maxActivePlans < 1 || maxActivePlans > 10_000) { + throw new IllegalArgumentException("Adoption active-plan limit must be between 1 and 10000"); + } + planTtl = Objects.requireNonNull(planTtl, "planTtl"); + if (planTtl.isNegative() || planTtl.isZero() || planTtl.compareTo(Duration.ofHours(24L)) > 0) { + throw new IllegalArgumentException("Adoption plan TTL must be between one nanosecond and 24 hours"); + } + } + + public static IrisStructureAdoptionLimits defaults() { + return new IrisStructureAdoptionLimits( + 10_000, + 8 * 1024 * 1024, + 64 * 1024 * 1024, + 1024L * 1024L * 1024L, + 10_000, + 1_000, + 1_024, + Duration.ofMinutes(15L) + ); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionPlan.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionPlan.java new file mode 100644 index 000000000..e0d3a56f8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionPlan.java @@ -0,0 +1,107 @@ +package art.arcane.iris.core.structure.conversion; + +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureTransactionReadSet; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.UUID; + +public record IrisStructureAdoptionPlan( + UUID planId, + Instant createdAt, + Instant expiresAt, + IrisStructureAdoptionRequest request, + StructureKey targetStructure, + IrisStructureAdoptionDisposition disposition, + List diagnostics, + Map sourceResourceHashes, + Map sourceToTargetPaths, + StructureTransactionReadSet readSet, + long totalSourceBytes, + String sourceClosureHash, + String planHash +) { + public IrisStructureAdoptionPlan { + Objects.requireNonNull(planId, "planId"); + Objects.requireNonNull(createdAt, "createdAt"); + Objects.requireNonNull(expiresAt, "expiresAt"); + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(targetStructure, "targetStructure"); + Objects.requireNonNull(disposition, "disposition"); + Objects.requireNonNull(diagnostics, "diagnostics"); + diagnostics = sortedDiagnostics(diagnostics); + sourceResourceHashes = immutableMap(sourceResourceHashes); + sourceToTargetPaths = immutableMap(sourceToTargetPaths); + readSet = Objects.requireNonNull(readSet, "readSet"); + if (!expiresAt.isAfter(createdAt)) { + throw new IllegalArgumentException("Adoption plan expiry must be after creation"); + } + if (totalSourceBytes < 0L) { + throw new IllegalArgumentException("Adoption plan source bytes cannot be negative"); + } + requireHash(sourceClosureHash, "source closure"); + requireHash(planHash, "plan"); + if (sourceResourceHashes.isEmpty() && disposition != IrisStructureAdoptionDisposition.BLOCKED) { + throw new IllegalArgumentException("Applicable adoption plan requires source resources"); + } + } + + public boolean canApply() { + return disposition != IrisStructureAdoptionDisposition.BLOCKED + && diagnostics.stream().noneMatch(IrisStructureAdoptionDiagnostic::blocking); + } + + public boolean expiredAt(Instant instant) { + return !Objects.requireNonNull(instant, "instant").isBefore(expiresAt); + } + + public int resourceCount() { + return sourceResourceHashes.size(); + } + + public long errorCount() { + return diagnostics.stream().filter(IrisStructureAdoptionDiagnostic::blocking).count(); + } + + public long warningCount() { + return diagnostics.stream().filter(diagnostic -> + diagnostic.severity() == IrisStructureAdoptionDiagnostic.Severity.WARNING).count(); + } + + public List summaryLines() { + ArrayList lines = new ArrayList<>(); + lines.add("Plan " + planId + " -> " + disposition + " for " + targetStructure.value()); + lines.add(resourceCount() + " resources, " + totalSourceBytes + " bytes, " + + errorCount() + " errors, " + warningCount() + " warnings"); + for (IrisStructureAdoptionDiagnostic diagnostic : diagnostics) { + lines.add(diagnostic.summary()); + } + return List.copyOf(lines); + } + + private static List sortedDiagnostics( + List values + ) { + ArrayList ordered = new ArrayList<>(values); + ordered.sort(IrisStructureAdoptionDiagnostic::compareTo); + return List.copyOf(ordered); + } + + private static Map immutableMap(Map values) { + Objects.requireNonNull(values, "plan values"); + return Collections.unmodifiableMap(new TreeMap<>(values)); + } + + private static void requireHash(String value, String kind) { + if (!StructureHash.isSha256(value)) { + throw new IllegalArgumentException("Adoption " + kind + " hash must be SHA-256"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionReceipt.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionReceipt.java new file mode 100644 index 000000000..e6ef0af02 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionReceipt.java @@ -0,0 +1,82 @@ +package art.arcane.iris.core.structure.conversion; + +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; + +import java.time.Instant; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.UUID; + +public record IrisStructureAdoptionReceipt( + UUID receiptId, + UUID planId, + StructureOwnershipManifest.Origin origin, + Instant appliedAt, + StructureKey sourceStructure, + StructureKey targetStructure, + String sourceClosureHash, + String planHash, + Map sourceResourceHashes, + Map targetResourceHashes, + Map sourceToTargetPaths, + StructureOwnershipManifest.RollbackDisposition rollbackDisposition +) { + public IrisStructureAdoptionReceipt { + Objects.requireNonNull(receiptId, "receiptId"); + Objects.requireNonNull(planId, "planId"); + Objects.requireNonNull(origin, "origin"); + Objects.requireNonNull(appliedAt, "appliedAt"); + Objects.requireNonNull(sourceStructure, "sourceStructure"); + Objects.requireNonNull(targetStructure, "targetStructure"); + requireHash(sourceClosureHash, "source closure"); + requireHash(planHash, "plan"); + sourceResourceHashes = immutableMap(sourceResourceHashes, true); + targetResourceHashes = immutableMap(targetResourceHashes, true); + sourceToTargetPaths = immutableMap(sourceToTargetPaths, false); + rollbackDisposition = Objects.requireNonNull(rollbackDisposition, "rollbackDisposition"); + if (sourceResourceHashes.isEmpty() || targetResourceHashes.isEmpty() || sourceToTargetPaths.isEmpty()) { + throw new IllegalArgumentException("Adoption receipt requires source, target, and mapping entries"); + } + } + + public StructureOwnershipManifest.Provenance provenance() { + return new StructureOwnershipManifest.Provenance( + origin, + receiptId.toString(), + planHash, + sourceClosureHash, + appliedAt.toEpochMilli(), + sourceResourceHashes, + sourceToTargetPaths, + rollbackDisposition + ); + } + + public boolean rollbackAvailable() { + return rollbackDisposition != StructureOwnershipManifest.RollbackDisposition.NONE; + } + + private static Map immutableMap(Map values, boolean hashes) { + Objects.requireNonNull(values, "receipt values"); + TreeMap ordered = new TreeMap<>(); + for (Map.Entry entry : values.entrySet()) { + String key = Objects.requireNonNull(entry.getKey(), "receipt map key"); + String value = Objects.requireNonNull(entry.getValue(), "receipt map value"); + if (hashes) { + requireHash(value, "resource"); + } + ordered.put(key, value); + } + return Collections.unmodifiableMap(ordered); + } + + private static void requireHash(String value, String kind) { + if (!StructureHash.isSha256(value)) { + throw new IllegalArgumentException("Adoption receipt " + kind + " hash must be SHA-256"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionRequest.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionRequest.java new file mode 100644 index 000000000..1279b54d5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionRequest.java @@ -0,0 +1,54 @@ +package art.arcane.iris.core.structure.conversion; + +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; + +import java.util.Objects; +import java.util.Optional; + +public record IrisStructureAdoptionRequest( + String sourceStructure, + Optional requestedTarget, + IrisStructureAdoptionStrategy strategy, + IrisStructureAdoptionInputKind inputKind +) { + public IrisStructureAdoptionRequest { + sourceStructure = requireInternalKey(sourceStructure); + requestedTarget = Objects.requireNonNull(requestedTarget, "requestedTarget"); + strategy = Objects.requireNonNull(strategy, "strategy"); + inputKind = Objects.requireNonNull(inputKind, "inputKind"); + } + + public static IrisStructureAdoptionRequest unowned(String sourceStructure) { + return new IrisStructureAdoptionRequest( + sourceStructure, + Optional.empty(), + IrisStructureAdoptionStrategy.AUTO, + IrisStructureAdoptionInputKind.UNOWNED_IRIS + ); + } + + public static IrisStructureAdoptionRequest cloneTo(String sourceStructure, StructureKey target) { + return new IrisStructureAdoptionRequest( + sourceStructure, + Optional.of(Objects.requireNonNull(target, "target")), + IrisStructureAdoptionStrategy.CLONE, + IrisStructureAdoptionInputKind.UNOWNED_IRIS + ); + } + + public StructureKey sourceOwnershipKey() { + return new StructureKey("iris", sourceStructure); + } + + private static String requireInternalKey(String value) { + Objects.requireNonNull(value, "sourceStructure"); + String normalized = value.trim(); + if (!normalized.equals(value)) { + throw new IllegalArgumentException("Source structure key cannot contain surrounding whitespace"); + } + StructureResourceBundle.validateRelativePath("structures/" + normalized + ".json"); + new StructureKey("iris", normalized); + return normalized; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionResult.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionResult.java new file mode 100644 index 000000000..2ad166805 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionResult.java @@ -0,0 +1,54 @@ +package art.arcane.iris.core.structure.conversion; + +import art.arcane.iris.core.structure.authoring.StructureWriteResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +public record IrisStructureAdoptionResult( + Status status, + UUID planId, + List diagnostics, + Optional receipt, + Optional writeResult +) { + public IrisStructureAdoptionResult { + Objects.requireNonNull(status, "status"); + Objects.requireNonNull(planId, "planId"); + Objects.requireNonNull(diagnostics, "diagnostics"); + Objects.requireNonNull(receipt, "receipt"); + Objects.requireNonNull(writeResult, "writeResult"); + diagnostics = List.copyOf(diagnostics); + if (status == Status.APPLIED && receipt.isEmpty()) { + throw new IllegalArgumentException("Applied adoption result requires a receipt"); + } + if (status != Status.APPLIED && receipt.isPresent()) { + throw new IllegalArgumentException("Failed adoption result cannot expose a receipt"); + } + } + + public boolean successful() { + return status == Status.APPLIED; + } + + public List summaryLines() { + ArrayList lines = new ArrayList<>(); + lines.add("Adoption plan " + planId + ": " + status); + for (IrisStructureAdoptionDiagnostic diagnostic : diagnostics) { + lines.add(diagnostic.summary()); + } + return List.copyOf(lines); + } + + public enum Status { + APPLIED, + BLOCKED, + EXPIRED, + UNKNOWN_PLAN, + STALE, + FAILED + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionService.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionService.java new file mode 100644 index 000000000..db367ab36 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionService.java @@ -0,0 +1,1313 @@ +package art.arcane.iris.core.structure.conversion; + +import art.arcane.iris.core.pack.StructurePackageClosure; +import art.arcane.iris.core.structure.authoring.StructureBackend; +import art.arcane.iris.core.structure.authoring.StructureCapability; +import art.arcane.iris.core.structure.authoring.StructureHash; +import art.arcane.iris.core.structure.authoring.StructureKey; +import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.core.structure.authoring.StructureSource; +import art.arcane.iris.core.structure.authoring.StructureTransactionReadSet; +import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; +import art.arcane.iris.core.structure.authoring.StructureWriteOptions; +import art.arcane.iris.core.structure.authoring.StructureWriteResult; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Clock; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Stream; + +public final class IrisStructureAdoptionService { + private static final String STRUCTURES = "structures"; + private static final String POOLS = "jigsaw-pools"; + private static final String PIECES = "jigsaw-pieces"; + private static final String OBJECTS = "objects"; + private static final String LOOT = "loot"; + private static final String MANIFESTS = ".iris/structure-manifests"; + private static final String ADOPTION_VERSION = "iris-adoption-1"; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private final Path packRoot; + private final IrisStructureAdoptionLimits limits; + private final Clock clock; + private final StructureTransactionWriter writer; + private final ConcurrentMap plans = new ConcurrentHashMap<>(); + + public IrisStructureAdoptionService(Path packRoot) { + this(packRoot, IrisStructureAdoptionLimits.defaults(), Clock.systemUTC()); + } + + public IrisStructureAdoptionService( + Path packRoot, + IrisStructureAdoptionLimits limits, + Clock clock + ) { + this.packRoot = canonicalRoot(Objects.requireNonNull(packRoot, "packRoot")); + this.limits = Objects.requireNonNull(limits, "limits"); + this.clock = Objects.requireNonNull(clock, "clock"); + writer = new StructureTransactionWriter(this.packRoot); + } + + public synchronized IrisStructureAdoptionPlan inspect(IrisStructureAdoptionRequest request) { + Objects.requireNonNull(request, "request"); + pruneExpiredPlans(); + Instant createdAt = clock.instant(); + Instant expiresAt = createdAt.plus(limits.planTtl()); + UUID planId = UUID.randomUUID(); + PlanDiagnostics diagnostics = new PlanDiagnostics(limits.maxDiagnostics()); + boolean capacityAvailable = plans.size() < limits.maxActivePlans(); + if (!capacityAvailable) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.PLAN_BLOCKED, + "", + "The active adoption plan limit of " + limits.maxActivePlans() + " was reached", + "Apply or wait for an existing plan to expire."); + } + StructureKey sourceKey = request.sourceOwnershipKey(); + StructureKey initialTarget = request.requestedTarget().orElse(sourceKey); + if (!initialTarget.namespace().equals("iris")) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.TARGET_NAMESPACE_UNSUPPORTED, + initialTarget.value(), + "Iris graph ownership targets must use the iris namespace", + "Choose an iris: target."); + } + + StructurePackageClosure closure = StructurePackageClosure.collect( + packRoot.toFile(), + List.of(request.sourceStructure()), + new StructurePackageClosure.Limits(limits.maxResources(), limits.maxJsonBytes())); + for (String error : closure.errors()) { + diagnostics.error( + error.contains("exceeds") + ? IrisStructureAdoptionDiagnostic.Code.SOURCE_RESOURCE_LIMIT + : IrisStructureAdoptionDiagnostic.Code.SOURCE_GRAPH_INVALID, + request.sourceStructure(), + error, + "Repair the source graph before adoption."); + } + + Map sourceHashes = new TreeMap<>(); + long sourceBytes = 0L; + if (!closure.structures().isEmpty()) { + for (String relativePath : resourcePaths(closure)) { + try { + HashedResource resource = hashResource(relativePath); + sourceHashes.put(relativePath, resource.contentHash()); + sourceBytes = Math.addExact(sourceBytes, resource.size()); + if (sourceBytes > limits.maxTotalBytes()) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.SOURCE_RESOURCE_LIMIT, + relativePath, + "Source closure exceeds " + limits.maxTotalBytes() + " bytes", + "Reduce the graph or raise a safe configured limit."); + break; + } + } catch (IOException | RuntimeException exception) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.SOURCE_RESOURCE_UNSAFE, + relativePath, + describe(exception), + "Repair or replace this resource before adoption."); + } + } + } + String closureHash = canonicalMapHash(sourceHashes); + + String sourceManifestPath = StructureOwnershipManifest.relativePath(sourceKey); + Map ownershipReadHashes = new TreeMap<>(); + Path sourceManifest = null; + try { + sourceManifest = resolveSafe(sourceManifestPath); + } catch (IllegalArgumentException exception) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.SOURCE_RESOURCE_UNSAFE, + sourceManifestPath, + describe(exception), + "Repair the structure authoring metadata path before adoption."); + } + if (sourceManifest != null && Files.exists(sourceManifest, LinkOption.NOFOLLOW_LINKS)) { + if (request.inputKind() == IrisStructureAdoptionInputKind.MANAGED_DATAPACK) { + try { + byte[] manifestContent = readResource(sourceManifestPath, limits.maxJsonBytes()); + StructureOwnershipManifest ownership = StructureOwnershipManifest.fromJson(manifestContent); + if (!ownership.structure().equals(sourceKey) + || ownership.backend() != StructureBackend.IRIS_ASSEMBLY) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.SOURCE_ALREADY_OWNED, + sourceManifestPath, + "The managed source manifest does not identify this Iris assembly graph", + "Refresh or repair the managed import before cloning it."); + } else if ((ownership.source().kind() != StructureSource.Kind.DATAPACK + && ownership.source().kind() != StructureSource.Kind.VANILLA) + || ownership.provenance().origin() + != StructureOwnershipManifest.Origin.MANAGED_DATAPACK) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.SOURCE_ALREADY_OWNED, + sourceManifestPath, + "The source manifest is not owned by managed datapack ingest", + "Clone from the correct managed source or open its existing owner."); + } else if (!manifestMatchesSource(ownership, sourceHashes)) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.SOURCE_RESOURCE_CHANGED, + sourceManifestPath, + "Managed source ownership resources do not match their manifest", + "Refresh or repair the managed import before cloning it."); + } else { + ownershipReadHashes.put(sourceManifestPath, StructureHash.sha256(manifestContent)); + } + } catch (IOException | RuntimeException exception) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.SOURCE_ALREADY_OWNED, + sourceManifestPath, + "Cannot verify managed source ownership: " + describe(exception), + "Repair the managed import before cloning it."); + } + } else { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.SOURCE_ALREADY_OWNED, + sourceManifestPath, + "The source graph already has an ownership manifest", + "Open the owned project or choose a different source."); + } + } + + boolean differentTarget = request.requestedTarget().filter(target -> !target.equals(sourceKey)).isPresent(); + if (request.strategy() == IrisStructureAdoptionStrategy.IN_PLACE && differentTarget) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.TARGET_REQUIRED_FOR_CLONE, + initialTarget.value(), + "In-place adoption cannot change the structure key", + "Use clone strategy for a different target."); + } + boolean clone = request.strategy() == IrisStructureAdoptionStrategy.CLONE + || request.strategy() != IrisStructureAdoptionStrategy.IN_PLACE && differentTarget; + if (request.inputKind() == IrisStructureAdoptionInputKind.MANAGED_DATAPACK) { + if (request.strategy() == IrisStructureAdoptionStrategy.IN_PLACE) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.MANAGED_INPUT_REQUIRES_CLONE, + request.sourceStructure(), + "Managed datapack graphs cannot be adopted in place", + "Inspect a clone target so managed cleanup remains isolated."); + } else { + diagnostics.warning( + IrisStructureAdoptionDiagnostic.Code.MANAGED_INPUT_REQUIRES_CLONE, + request.sourceStructure(), + "Managed datapack ownership requires a private clone", + "The source will remain managed and unchanged."); + clone = true; + } + } + + ExclusivitySnapshot exclusivity = ExclusivitySnapshot.empty(); + if (!clone && !diagnostics.blocked()) { + exclusivity = inspectExclusivity(request.sourceStructure(), sourceHashes); + for (ExclusivityIssue issue : exclusivity.issues()) { + if (request.strategy() == IrisStructureAdoptionStrategy.IN_PLACE) { + diagnostics.error(issue.code(), issue.resource(), issue.detail(), + "Choose clone adoption instead."); + } else { + diagnostics.warning(issue.code(), issue.resource(), issue.detail(), + "A private clone will avoid overlapping ownership."); + clone = true; + } + } + } + + StructureKey target = initialTarget; + if (clone && request.requestedTarget().isEmpty()) { + try { + target = firstAvailableCloneTarget(request.sourceStructure(), sourceHashes.keySet()); + } catch (IllegalArgumentException | IllegalStateException exception) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.TARGET_RESOURCE_UNSAFE, + request.sourceStructure(), + describe(exception), + "Choose an explicit safe target after repairing the pack path."); + target = new StructureKey("iris", request.sourceStructure() + "-studio-unavailable"); + } + } + if (clone && target.equals(sourceKey)) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.TARGET_REQUIRED_FOR_CLONE, + target.value(), + "Clone adoption cannot write over the source graph", + "Choose a different iris: target."); + } + if (!clone && !target.equals(sourceKey)) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.TARGET_REQUIRED_FOR_CLONE, + target.value(), + "In-place adoption must keep the source structure key", + "Use clone strategy for a different target."); + } + + Map mappings = sourceHashes.isEmpty() + ? Map.of() + : clone ? cloneMappings(request.sourceStructure(), target.path(), sourceHashes.keySet()) + : identityMappings(sourceHashes.keySet()); + StructureTransactionReadSet.Builder readSet = StructureTransactionReadSet.builder() + .files(sourceHashes) + .files(ownershipReadHashes); + if (clone) { + TreeSet targetPaths = new TreeSet<>(mappings.values()); + targetPaths.add(StructureOwnershipManifest.relativePath(target)); + TreeSet absentTargetPaths = new TreeSet<>(targetPaths); + absentTargetPaths.removeAll(sourceHashes.keySet()); + absentTargetPaths.removeAll(ownershipReadHashes.keySet()); + readSet.absent(absentTargetPaths); + for (String targetPath : targetPaths) { + Path resolvedTarget; + try { + resolvedTarget = resolveSafe(targetPath); + } catch (IllegalArgumentException exception) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.TARGET_RESOURCE_UNSAFE, + targetPath, + describe(exception), + "Choose a target whose resource paths stay inside the pack."); + continue; + } + if (Files.exists(resolvedTarget, LinkOption.NOFOLLOW_LINKS)) { + diagnostics.error( + IrisStructureAdoptionDiagnostic.Code.TARGET_RESOURCE_EXISTS, + targetPath, + "Clone target path already exists", + "Choose a different target key."); + } + } + if (!diagnostics.blocked()) { + diagnostics.info( + IrisStructureAdoptionDiagnostic.Code.CLONE_SELECTED, + target.value(), + "The graph will be copied into project-private resource paths", + "Apply the inspected plan ID to commit it."); + } + } else { + readSet.absent(sourceManifestPath); + readSet.files(exclusivity.fileHashes()); + for (Map.Entry> directory : exclusivity.directoryEntries().entrySet()) { + readSet.directory(directory.getKey(), directory.getValue()); + } + if (!diagnostics.blocked()) { + diagnostics.info( + IrisStructureAdoptionDiagnostic.Code.IN_PLACE_AVAILABLE, + target.value(), + "The exclusive source graph can be claimed without rewriting resource bytes", + "Apply the inspected plan ID to install ownership."); + } + } + + IrisStructureAdoptionDisposition disposition = diagnostics.blocked() + ? IrisStructureAdoptionDisposition.BLOCKED + : clone + ? IrisStructureAdoptionDisposition.CLONE_REQUIRED + : IrisStructureAdoptionDisposition.IN_PLACE; + StructureTransactionReadSet transactionReadSet = readSet.build(); + String planHash = planHash( + planId, + createdAt, + expiresAt, + request, + target, + disposition, + diagnostics.values(), + sourceHashes, + mappings, + transactionReadSet, + closureHash); + IrisStructureAdoptionPlan plan = new IrisStructureAdoptionPlan( + planId, + createdAt, + expiresAt, + request, + target, + disposition, + diagnostics.values(), + sourceHashes, + mappings, + transactionReadSet, + sourceBytes, + closureHash, + planHash + ); + if (capacityAvailable) { + plans.put(planId, plan); + } + return plan; + } + + public synchronized IrisStructureAdoptionResult apply(IrisStructureAdoptionPlan plan) { + Objects.requireNonNull(plan, "plan"); + IrisStructureAdoptionPlan stored = plans.remove(plan.planId()); + if (stored == null || !stored.planHash().equals(plan.planHash())) { + return result( + IrisStructureAdoptionResult.Status.UNKNOWN_PLAN, + plan.planId(), + diagnostic( + IrisStructureAdoptionDiagnostic.Severity.ERROR, + IrisStructureAdoptionDiagnostic.Code.PLAN_UNKNOWN, + "", + "The adoption plan is unknown or has already been consumed", + "Run inspect again."), + Optional.empty(), + Optional.empty()); + } + if (stored.expiredAt(clock.instant())) { + return result( + IrisStructureAdoptionResult.Status.EXPIRED, + stored.planId(), + diagnostic( + IrisStructureAdoptionDiagnostic.Severity.ERROR, + IrisStructureAdoptionDiagnostic.Code.PLAN_EXPIRED, + "", + "The adoption plan expired at " + stored.expiresAt(), + "Run inspect again."), + Optional.empty(), + Optional.empty()); + } + if (!stored.canApply()) { + return new IrisStructureAdoptionResult( + IrisStructureAdoptionResult.Status.BLOCKED, + stored.planId(), + stored.diagnostics(), + Optional.empty(), + Optional.empty()); + } + + try { + return stored.disposition() == IrisStructureAdoptionDisposition.IN_PLACE + ? applyInPlace(stored) + : applyClone(stored); + } catch (StalePlanException exception) { + return result( + IrisStructureAdoptionResult.Status.STALE, + stored.planId(), + diagnostic( + IrisStructureAdoptionDiagnostic.Severity.ERROR, + IrisStructureAdoptionDiagnostic.Code.PLAN_STALE, + exception.resource(), + exception.getMessage(), + "Run inspect again."), + Optional.empty(), + Optional.empty()); + } catch (IOException | RuntimeException exception) { + return result( + IrisStructureAdoptionResult.Status.FAILED, + stored.planId(), + diagnostic( + IrisStructureAdoptionDiagnostic.Severity.ERROR, + IrisStructureAdoptionDiagnostic.Code.TRANSACTION_FAILED, + "", + describe(exception), + "No adoption receipt was committed."), + Optional.empty(), + Optional.empty()); + } + } + + public synchronized Optional plan(UUID planId) { + Objects.requireNonNull(planId, "planId"); + pruneExpiredPlans(); + return Optional.ofNullable(plans.get(planId)); + } + + public synchronized List activePlanIds() { + pruneExpiredPlans(); + ArrayList ids = new ArrayList<>(plans.keySet()); + Collections.sort(ids); + return List.copyOf(ids); + } + + public synchronized int pruneExpiredPlans() { + Instant now = clock.instant(); + int removed = 0; + for (Map.Entry entry : plans.entrySet()) { + if (entry.getValue().expiredAt(now) && plans.remove(entry.getKey(), entry.getValue())) { + removed++; + } + } + return removed; + } + + private IrisStructureAdoptionResult applyInPlace(IrisStructureAdoptionPlan plan) { + IrisStructureAdoptionReceipt receipt = receipt( + plan, + StructureOwnershipManifest.Origin.ADOPTED_EXISTING, + plan.sourceResourceHashes(), + StructureOwnershipManifest.RollbackDisposition.NONE); + StructureOwnershipManifest manifest = new StructureOwnershipManifest( + StructureOwnershipManifest.CURRENT_SCHEMA_VERSION, + plan.targetStructure(), + source(plan), + StructureBackend.IRIS_ASSEMBLY, + defaultCapabilities(), + List.of(), + plan.sourceResourceHashes(), + receipt.provenance()); + StructureWriteResult writeResult = writer.claimExisting(manifest, plan.readSet()); + return completeApply(plan, receipt, writeResult); + } + + private IrisStructureAdoptionResult applyClone(IrisStructureAdoptionPlan plan) throws IOException { + StructureResourceBundle bundle = cloneBundle(plan); + Map targetHashes = new TreeMap<>(); + for (StructureResourceBundle.Resource resource : bundle.resources().values()) { + targetHashes.put(resource.relativePath(), resource.contentHash()); + } + IrisStructureAdoptionReceipt receipt = receipt( + plan, + plan.request().inputKind() == IrisStructureAdoptionInputKind.MANAGED_DATAPACK + ? StructureOwnershipManifest.Origin.ADOPTED_MANAGED_CLONE + : StructureOwnershipManifest.Origin.ADOPTED_CLONE, + targetHashes, + StructureOwnershipManifest.RollbackDisposition.DELETE_CREATED_IF_UNCHANGED); + StructureWriteResult writeResult = writer.writeVerified( + bundle, + StructureWriteOptions.addOnly(), + plan.readSet(), + receipt.provenance()); + return completeApply(plan, receipt, writeResult); + } + + private IrisStructureAdoptionResult completeApply( + IrisStructureAdoptionPlan plan, + IrisStructureAdoptionReceipt receipt, + StructureWriteResult writeResult + ) { + if (writeResult.committed()) { + ArrayList diagnostics = new ArrayList<>(); + diagnostics.add(diagnostic( + IrisStructureAdoptionDiagnostic.Severity.INFO, + IrisStructureAdoptionDiagnostic.Code.APPLIED, + plan.targetStructure().value(), + "Ownership and adoption provenance were committed", + "The graph is ready for owned authoring.")); + if (writeResult.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED) { + diagnostics.add(diagnostic( + IrisStructureAdoptionDiagnostic.Severity.WARNING, + IrisStructureAdoptionDiagnostic.Code.TRANSACTION_FAILED, + writeResult.manifestPath(), + "The transaction committed but recovery cleanup remains", + "Run structure transaction recovery before another mutation.")); + } + return new IrisStructureAdoptionResult( + IrisStructureAdoptionResult.Status.APPLIED, + plan.planId(), + diagnostics, + Optional.of(receipt), + Optional.of(writeResult)); + } + boolean stale = writeResult.conflicts().stream().anyMatch(conflict -> + conflict.reason() == StructureWriteResult.ConflictReason.STALE_READ_SET + || conflict.reason() == StructureWriteResult.ConflictReason.RESOURCE_EXISTS + || conflict.reason() == StructureWriteResult.ConflictReason.MANIFEST_EXISTS); + IrisStructureAdoptionDiagnostic diagnostic = diagnostic( + IrisStructureAdoptionDiagnostic.Severity.ERROR, + stale + ? IrisStructureAdoptionDiagnostic.Code.PLAN_STALE + : IrisStructureAdoptionDiagnostic.Code.TRANSACTION_FAILED, + writeResult.conflicts().isEmpty() ? writeResult.manifestPath() + : writeResult.conflicts().getFirst().relativePath(), + failureDetail(writeResult), + stale ? "Run inspect again." : "No adoption receipt was committed."); + return result( + stale ? IrisStructureAdoptionResult.Status.STALE : IrisStructureAdoptionResult.Status.FAILED, + plan.planId(), + diagnostic, + Optional.empty(), + Optional.of(writeResult)); + } + + private StructureResourceBundle cloneBundle(IrisStructureAdoptionPlan plan) throws IOException { + StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(plan.targetStructure()) + .source(source(plan)) + .backend(StructureBackend.IRIS_ASSEMBLY) + .capabilities(defaultCapabilities()); + for (Map.Entry mapping : plan.sourceToTargetPaths().entrySet()) { + String sourcePath = mapping.getKey(); + byte[] content = readResource(sourcePath); + String observedHash = StructureHash.sha256(content); + String expectedHash = plan.sourceResourceHashes().get(sourcePath); + if (!observedHash.equals(expectedHash)) { + throw new StalePlanException(sourcePath, "Source resource changed after inspection"); + } + byte[] targetContent = rewriteResource(sourcePath, content, plan.sourceToTargetPaths()); + bundle.resource(mapping.getValue(), targetContent); + } + return bundle.build(); + } + + private byte[] rewriteResource( + String sourcePath, + byte[] content, + Map mappings + ) throws IOException { + if (sourcePath.startsWith(OBJECTS + "/") || sourcePath.startsWith(LOOT + "/")) { + return content; + } + JsonObject root; + try { + JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("Graph resource is not a JSON object: " + sourcePath); + } + root = parsed.getAsJsonObject(); + } catch (RuntimeException exception) { + throw new IOException("Cannot parse graph resource " + sourcePath, exception); + } + if (sourcePath.startsWith(STRUCTURES + "/")) { + rewriteString(root, "startPool", POOLS, ".json", mappings, sourcePath); + rewriteArray(root, "loot", LOOT, ".json", mappings, sourcePath); + } else if (sourcePath.startsWith(POOLS + "/")) { + JsonArray pieces = root.getAsJsonArray("pieces"); + if (pieces != null) { + for (JsonElement element : pieces) { + if (element.isJsonObject() && element.getAsJsonObject().has("piece")) { + rewriteString(element.getAsJsonObject(), "piece", PIECES, ".json", mappings, sourcePath); + } + } + } + if (root.has("fallback")) { + rewriteString(root, "fallback", POOLS, ".json", mappings, sourcePath); + } + } else if (sourcePath.startsWith(PIECES + "/")) { + rewriteString(root, "object", OBJECTS, ".iob", mappings, sourcePath); + JsonArray connectors = root.getAsJsonArray("connectors"); + if (connectors != null) { + for (JsonElement element : connectors) { + if (element.isJsonObject()) { + rewriteString(element.getAsJsonObject(), "pool", POOLS, ".json", mappings, sourcePath); + } + } + } + } + return (GSON.toJson(root) + "\n").getBytes(StandardCharsets.UTF_8); + } + + private void rewriteString( + JsonObject object, + String field, + String folder, + String extension, + Map mappings, + String owner + ) throws IOException { + JsonElement value = object.get(field); + if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { + throw new IOException("Graph resource " + owner + " has invalid field " + field); + } + if (value.getAsString().isBlank()) { + return; + } + String sourcePath = folder + "/" + value.getAsString() + extension; + String targetPath = mappings.get(sourcePath); + if (targetPath == null) { + throw new IOException("Graph resource " + owner + " references unmapped resource " + sourcePath); + } + object.addProperty(field, internalKey(targetPath, folder, extension)); + } + + private void rewriteArray( + JsonObject object, + String field, + String folder, + String extension, + Map mappings, + String owner + ) throws IOException { + if (!object.has(field)) { + return; + } + JsonArray values = object.getAsJsonArray(field); + if (values == null) { + throw new IOException("Graph resource " + owner + " has invalid field " + field); + } + JsonArray rewritten = new JsonArray(); + for (JsonElement value : values) { + if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { + throw new IOException("Graph resource " + owner + " has non-string " + field + " entry"); + } + String sourcePath = folder + "/" + value.getAsString() + extension; + String targetPath = mappings.get(sourcePath); + if (targetPath == null) { + throw new IOException("Graph resource " + owner + " references unmapped resource " + sourcePath); + } + rewritten.add(internalKey(targetPath, folder, extension)); + } + object.add(field, rewritten); + } + + private ExclusivitySnapshot inspectExclusivity( + String sourceStructure, + Map sourceHashes + ) { + TreeMap globalHashes = new TreeMap<>(); + TreeMap> directoryEntries = new TreeMap<>(); + ArrayList issues = new ArrayList<>(); + long inspectedBytes = 0L; + int inspectedResources = 0; + try { + for (String directory : List.of(STRUCTURES, POOLS, PIECES, MANIFESTS)) { + List entries = snapshotDirectory(directory); + inspectedResources = Math.addExact(inspectedResources, entries.size()); + if (inspectedResources > limits.maxResources()) { + throw new IOException("Exclusivity snapshot exceeds " + + limits.maxResources() + " resources"); + } + directoryEntries.put(directory, entries); + for (String relativePath : entries) { + int maximum = relativePath.startsWith(MANIFESTS + "/") + ? limits.maxJsonBytes() + : limits.maxJsonBytes(); + HashedResource resource = hashResource(relativePath, maximum); + globalHashes.put(relativePath, resource.contentHash()); + inspectedBytes = Math.addExact(inspectedBytes, resource.size()); + if (inspectedBytes > limits.maxTotalBytes()) { + throw new IOException("Exclusivity snapshot exceeds " + + limits.maxTotalBytes() + " bytes"); + } + } + } + List structurePaths = directoryEntries.getOrDefault(STRUCTURES, List.of()); + if (structurePaths.size() > limits.maxStructuresScanned()) { + issues.add(new ExclusivityIssue( + IrisStructureAdoptionDiagnostic.Code.EXCLUSIVITY_UNPROVEN, + STRUCTURES, + "Structure scan exceeds " + limits.maxStructuresScanned() + " roots")); + } else { + for (String structurePath : structurePaths) { + if (!structurePath.endsWith(".json")) { + continue; + } + String structureKey = internalKey(structurePath, STRUCTURES, ".json"); + if (structureKey.equals(sourceStructure)) { + continue; + } + JsonObject structure = readJsonObject(structurePath); + if (structure.has("startPool")) { + String startPool = requiredReference(structure, "startPool", structurePath); + String referencedPath = POOLS + "/" + startPool + ".json"; + if (sourceHashes.containsKey(referencedPath)) { + issues.add(new ExclusivityIssue( + IrisStructureAdoptionDiagnostic.Code.SHARED_DEPENDENCY, + referencedPath, + "Structure '" + structureKey + "' directly references the source graph")); + } + } + if (structure.has("loot")) { + JsonArray loot = structure.getAsJsonArray("loot"); + if (loot == null) { + throw new IOException("Structure has invalid loot references: " + structurePath); + } + for (JsonElement lootEntry : loot) { + if (!lootEntry.isJsonPrimitive() || !lootEntry.getAsJsonPrimitive().isString()) { + throw new IOException("Structure has a non-string loot reference: " + structurePath); + } + addSharedBoundary( + issues, + sourceHashes, + LOOT, + lootEntry.getAsString(), + ".json", + structurePath); + } + } + } + } + for (String poolPath : directoryEntries.getOrDefault(POOLS, List.of())) { + if (!poolPath.endsWith(".json") || sourceHashes.containsKey(poolPath)) { + continue; + } + JsonObject pool = readJsonObject(poolPath); + if (pool.has("fallback")) { + String fallback = requiredReference(pool, "fallback", poolPath); + if (!fallback.isBlank()) { + addSharedBoundary(issues, sourceHashes, POOLS, fallback, ".json", poolPath); + } + } + JsonArray entries = pool.getAsJsonArray("pieces"); + if (entries == null) { + throw new IOException("Jigsaw pool has no pieces array: " + poolPath); + } + for (JsonElement entry : entries) { + if (!entry.isJsonObject() || !entry.getAsJsonObject().has("piece")) { + continue; + } + String piece = requiredReference(entry.getAsJsonObject(), "piece", poolPath); + addSharedBoundary(issues, sourceHashes, PIECES, piece, ".json", poolPath); + } + } + for (String piecePath : directoryEntries.getOrDefault(PIECES, List.of())) { + if (!piecePath.endsWith(".json") || sourceHashes.containsKey(piecePath)) { + continue; + } + JsonObject piece = readJsonObject(piecePath); + String object = requiredReference(piece, "object", piecePath); + addSharedBoundary(issues, sourceHashes, OBJECTS, object, ".iob", piecePath); + if (!piece.has("connectors")) { + continue; + } + JsonArray connectors = piece.getAsJsonArray("connectors"); + if (connectors == null) { + throw new IOException("Jigsaw piece has invalid connectors: " + piecePath); + } + for (JsonElement connector : connectors) { + if (!connector.isJsonObject()) { + throw new IOException("Jigsaw piece has a non-object connector: " + piecePath); + } + String pool = requiredReference(connector.getAsJsonObject(), "pool", piecePath); + addSharedBoundary(issues, sourceHashes, POOLS, pool, ".json", piecePath); + } + } + for (String manifestPath : directoryEntries.getOrDefault(MANIFESTS, List.of())) { + StructureOwnershipManifest manifest; + try { + manifest = StructureOwnershipManifest.fromJson(readResource(manifestPath, limits.maxJsonBytes())); + } catch (IOException | RuntimeException exception) { + issues.add(new ExclusivityIssue( + IrisStructureAdoptionDiagnostic.Code.EXCLUSIVITY_UNPROVEN, + manifestPath, + "An ownership manifest is invalid: " + describe(exception))); + continue; + } + Set overlap = new TreeSet<>(manifest.resourceHashes().keySet()); + overlap.retainAll(sourceHashes.keySet()); + if (!overlap.isEmpty()) { + issues.add(new ExclusivityIssue( + IrisStructureAdoptionDiagnostic.Code.SHARED_DEPENDENCY, + overlap.iterator().next(), + "Owned structure '" + manifest.structure() + "' already claims " + + overlap.size() + " source resources")); + } + } + } catch (IOException | RuntimeException exception) { + issues.add(new ExclusivityIssue( + IrisStructureAdoptionDiagnostic.Code.EXCLUSIVITY_UNPROVEN, + "", + describe(exception))); + } + return new ExclusivitySnapshot(globalHashes, directoryEntries, issues); + } + + private void addSharedBoundary( + List issues, + Map sourceHashes, + String folder, + String key, + String extension, + String owner + ) { + String referencedPath = folder + "/" + key + extension; + if (sourceHashes.containsKey(referencedPath)) { + issues.add(new ExclusivityIssue( + IrisStructureAdoptionDiagnostic.Code.SHARED_DEPENDENCY, + referencedPath, + owner + " references a source graph resource")); + } + } + + private String requiredReference(JsonObject object, String field, String owner) throws IOException { + JsonElement value = object.get(field); + if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { + throw new IOException("Graph resource " + owner + " has invalid field " + field); + } + return value.getAsString(); + } + + private List snapshotDirectory(String relativePath) throws IOException { + Path directory = resolveSafe(relativePath); + if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + return List.of(); + } + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Snapshot path is not a directory: " + relativePath); + } + ArrayList entries = new ArrayList<>(); + try (Stream paths = Files.walk(directory)) { + Iterator iterator = paths.iterator(); + while (iterator.hasNext()) { + Path path = iterator.next(); + if (path.equals(directory)) { + continue; + } + if (Files.isSymbolicLink(path)) { + throw new IOException("Snapshot directory contains a symbolic link: " + path); + } + if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Snapshot directory contains a non-file entry: " + path); + } + entries.add(relative(path)); + if (entries.size() > limits.maxResources()) { + throw new IOException("Snapshot directory exceeds " + limits.maxResources() + " resources"); + } + } + } + Collections.sort(entries); + return List.copyOf(entries); + } + + private StructureKey firstAvailableCloneTarget(String sourceStructure, Set sourcePaths) { + for (int suffix = 0; suffix < 100; suffix++) { + String path = sourceStructure + "-studio" + (suffix == 0 ? "" : "-" + (suffix + 1)); + StructureKey candidate = new StructureKey("iris", path); + Map mappings = cloneMappings(sourceStructure, path, sourcePaths); + boolean occupied = Files.exists( + resolveSafe(StructureOwnershipManifest.relativePath(candidate)), + LinkOption.NOFOLLOW_LINKS); + if (!occupied) { + occupied = mappings.values().stream().anyMatch(target -> + Files.exists(resolveSafe(target), LinkOption.NOFOLLOW_LINKS)); + } + if (!occupied) { + return candidate; + } + } + throw new IllegalStateException("No available automatic clone target was found after 100 candidates"); + } + + private Map cloneMappings( + String sourceStructure, + String targetStructure, + Set sourcePaths + ) { + TreeMap mappings = new TreeMap<>(); + Set targetPaths = new LinkedHashSet<>(); + for (String sourcePath : sourcePaths) { + String targetPath; + if (sourcePath.equals(STRUCTURES + "/" + sourceStructure + ".json")) { + targetPath = STRUCTURES + "/" + targetStructure + ".json"; + } else if (sourcePath.startsWith(POOLS + "/")) { + targetPath = POOLS + "/" + targetStructure + "/pool/" + + internalKey(sourcePath, POOLS, ".json") + ".json"; + } else if (sourcePath.startsWith(PIECES + "/")) { + targetPath = PIECES + "/" + targetStructure + "/piece/" + + internalKey(sourcePath, PIECES, ".json") + ".json"; + } else if (sourcePath.startsWith(OBJECTS + "/")) { + targetPath = OBJECTS + "/" + targetStructure + "/object/" + + internalKey(sourcePath, OBJECTS, ".iob") + ".iob"; + } else if (sourcePath.startsWith(LOOT + "/")) { + targetPath = LOOT + "/" + targetStructure + "/loot/" + + internalKey(sourcePath, LOOT, ".json") + ".json"; + } else { + throw new IllegalArgumentException("Unsupported graph resource path " + sourcePath); + } + StructureResourceBundle.validateRelativePath(targetPath); + if (!targetPaths.add(targetPath)) { + throw new IllegalArgumentException("Clone resource mapping collision at " + targetPath); + } + mappings.put(sourcePath, targetPath); + } + return Collections.unmodifiableMap(mappings); + } + + private Map identityMappings(Set sourcePaths) { + TreeMap mappings = new TreeMap<>(); + for (String sourcePath : sourcePaths) { + mappings.put(sourcePath, sourcePath); + } + return Collections.unmodifiableMap(mappings); + } + + private Set resourcePaths(StructurePackageClosure closure) { + TreeSet resources = new TreeSet<>(); + addPaths(resources, STRUCTURES, closure.structures(), ".json"); + addPaths(resources, POOLS, closure.pools(), ".json"); + addPaths(resources, PIECES, closure.pieces(), ".json"); + addPaths(resources, OBJECTS, closure.objects(), ".iob"); + addPaths(resources, LOOT, closure.loot(), ".json"); + return Collections.unmodifiableSet(resources); + } + + private void addPaths(Set resources, String folder, Set keys, String extension) { + for (String key : keys) { + resources.add(folder + "/" + key + extension); + } + } + + private HashedResource hashResource(String relativePath) throws IOException { + int maximum = relativePath.endsWith(".iob") ? limits.maxBinaryBytes() : limits.maxJsonBytes(); + return hashResource(relativePath, maximum); + } + + private HashedResource hashResource(String relativePath, int maximumBytes) throws IOException { + byte[] content = readResource(relativePath, maximumBytes); + return new HashedResource(content.length, StructureHash.sha256(content)); + } + + private byte[] readResource(String relativePath) throws IOException { + int maximum = relativePath.endsWith(".iob") ? limits.maxBinaryBytes() : limits.maxJsonBytes(); + return readResource(relativePath, maximum); + } + + private byte[] readResource(String relativePath, int maximumBytes) throws IOException { + Path resource = resolveRegularFile(relativePath); + try (InputStream input = Files.newInputStream( + resource, + StandardOpenOption.READ, + LinkOption.NOFOLLOW_LINKS)) { + byte[] content = input.readNBytes(maximumBytes + 1); + if (content.length > maximumBytes) { + throw new IOException("Resource exceeds " + maximumBytes + " bytes"); + } + return content; + } + } + + private JsonObject readJsonObject(String relativePath) throws IOException { + try { + JsonElement parsed = JsonParser.parseString( + new String(readResource(relativePath, limits.maxJsonBytes()), StandardCharsets.UTF_8)); + if (!parsed.isJsonObject()) { + throw new IOException("JSON resource is not an object: " + relativePath); + } + return parsed.getAsJsonObject(); + } catch (RuntimeException exception) { + throw new IOException("Cannot parse JSON resource " + relativePath, exception); + } + } + + private Path resolveRegularFile(String relativePath) throws IOException { + Path resource = resolveSafe(relativePath); + if (!Files.isRegularFile(resource, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Resource is missing or not a regular file: " + relativePath); + } + Path realResource = resource.toRealPath(); + Path realRoot = packRoot.toRealPath(); + if (!realResource.startsWith(realRoot)) { + throw new IOException("Resource escapes the pack through a symbolic link: " + relativePath); + } + return resource; + } + + private Path resolveSafe(String relativePath) { + Path target = packRoot.resolve(relativePath).normalize(); + if (!target.startsWith(packRoot) || target.equals(packRoot)) { + throw new IllegalArgumentException("Resource path escapes the pack root: " + relativePath); + } + Path current = packRoot; + for (Path segment : packRoot.relativize(target)) { + current = current.resolve(segment); + if (!Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + if (Files.isSymbolicLink(current)) { + throw new IllegalArgumentException("Resource path contains a symbolic link: " + current); + } + if (!current.equals(target) && !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS)) { + throw new IllegalArgumentException("Resource path contains a non-directory parent: " + current); + } + } + return target; + } + + private String relative(Path path) { + return packRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'); + } + + private String internalKey(String relativePath, String folder, String extension) { + String prefix = folder + "/"; + if (!relativePath.startsWith(prefix) || !relativePath.endsWith(extension)) { + throw new IllegalArgumentException("Resource path is outside " + folder + ": " + relativePath); + } + return relativePath.substring(prefix.length(), relativePath.length() - extension.length()); + } + + private StructureSource source(IrisStructureAdoptionPlan plan) { + return new StructureSource( + StructureSource.Kind.IRIS, + plan.request().sourceOwnershipKey(), + ADOPTION_VERSION, + plan.sourceClosureHash()); + } + + private List defaultCapabilities() { + return List.copyOf(EnumSet.of( + StructureCapability.BLOCKS, + StructureCapability.CONNECTORS, + StructureCapability.IRIS_PLACEMENT)); + } + + private IrisStructureAdoptionReceipt receipt( + IrisStructureAdoptionPlan plan, + StructureOwnershipManifest.Origin origin, + Map targetHashes, + StructureOwnershipManifest.RollbackDisposition rollbackDisposition + ) { + return new IrisStructureAdoptionReceipt( + UUID.randomUUID(), + plan.planId(), + origin, + clock.instant(), + plan.request().sourceOwnershipKey(), + plan.targetStructure(), + plan.sourceClosureHash(), + plan.planHash(), + plan.sourceResourceHashes(), + targetHashes, + plan.sourceToTargetPaths(), + rollbackDisposition); + } + + private String planHash( + UUID planId, + Instant createdAt, + Instant expiresAt, + IrisStructureAdoptionRequest request, + StructureKey target, + IrisStructureAdoptionDisposition disposition, + List diagnostics, + Map sourceHashes, + Map mappings, + StructureTransactionReadSet readSet, + String closureHash + ) { + StringBuilder canonical = new StringBuilder(); + canonical.append(planId).append('\n') + .append(createdAt).append('\n') + .append(expiresAt).append('\n') + .append(request.sourceStructure()).append('\n') + .append(request.strategy()).append('\n') + .append(request.inputKind()).append('\n') + .append(target.value()).append('\n') + .append(disposition).append('\n') + .append(closureHash).append('\n'); + appendMap(canonical, sourceHashes); + appendMap(canonical, mappings); + appendMap(canonical, readSet.fileHashes()); + for (String absentPath : readSet.absentPaths()) { + canonical.append("absent=").append(absentPath).append('\n'); + } + for (Map.Entry> directory : readSet.directoryEntries().entrySet()) { + canonical.append("directory=").append(directory.getKey()).append('\n'); + for (String entry : directory.getValue()) { + canonical.append("entry=").append(entry).append('\n'); + } + } + ArrayList orderedDiagnostics = new ArrayList<>(diagnostics); + Collections.sort(orderedDiagnostics); + for (IrisStructureAdoptionDiagnostic diagnostic : orderedDiagnostics) { + canonical.append(diagnostic.summary()).append('\n'); + } + return StructureHash.sha256(canonical.toString().getBytes(StandardCharsets.UTF_8)); + } + + private void appendMap(StringBuilder canonical, Map values) { + for (Map.Entry entry : new TreeMap<>(values).entrySet()) { + canonical.append(entry.getKey()).append('=').append(entry.getValue()).append('\n'); + } + } + + private boolean manifestMatchesSource( + StructureOwnershipManifest manifest, + Map sourceHashes + ) { + if (manifest.resourceHashes().isEmpty()) { + return false; + } + String rootStructurePath = STRUCTURES + "/" + manifest.structure().path() + ".json"; + if (!manifest.resourceHashes().containsKey(rootStructurePath)) { + return false; + } + for (Map.Entry owned : manifest.resourceHashes().entrySet()) { + if (!owned.getValue().equals(sourceHashes.get(owned.getKey()))) { + return false; + } + } + return true; + } + + private String canonicalMapHash(Map values) { + StringBuilder canonical = new StringBuilder(); + appendMap(canonical, values); + return StructureHash.sha256(canonical.toString().getBytes(StandardCharsets.UTF_8)); + } + + private String failureDetail(StructureWriteResult writeResult) { + if (!writeResult.conflicts().isEmpty()) { + StructureWriteResult.Conflict conflict = writeResult.conflicts().getFirst(); + return conflict.detail().isBlank() + ? "Transaction conflict at " + conflict.relativePath() + ": " + conflict.reason() + : conflict.detail(); + } + return writeResult.failure().map(IrisStructureAdoptionService::describe) + .orElse("Structure transaction did not commit"); + } + + private IrisStructureAdoptionResult result( + IrisStructureAdoptionResult.Status status, + UUID planId, + IrisStructureAdoptionDiagnostic diagnostic, + Optional receipt, + Optional writeResult + ) { + return new IrisStructureAdoptionResult( + status, + planId, + List.of(diagnostic), + receipt, + writeResult); + } + + private IrisStructureAdoptionDiagnostic diagnostic( + IrisStructureAdoptionDiagnostic.Severity severity, + IrisStructureAdoptionDiagnostic.Code code, + String resource, + String detail, + String recommendation + ) { + return new IrisStructureAdoptionDiagnostic(severity, code, resource, detail, recommendation); + } + + private static Path canonicalRoot(Path root) { + Path normalized = root.toAbsolutePath().normalize(); + if (!Files.exists(normalized)) { + return normalized; + } + try { + return normalized.toRealPath(); + } catch (IOException exception) { + throw new IllegalArgumentException("Cannot resolve adoption pack root " + normalized, exception); + } + } + + private static String describe(Throwable throwable) { + String message = throwable.getMessage(); + return message == null || message.isBlank() ? throwable.getClass().getSimpleName() : message; + } + + private record HashedResource(long size, String contentHash) { + } + + private record ExclusivityIssue( + IrisStructureAdoptionDiagnostic.Code code, + String resource, + String detail + ) { + } + + private record ExclusivitySnapshot( + Map fileHashes, + Map> directoryEntries, + List issues + ) { + private ExclusivitySnapshot { + fileHashes = Collections.unmodifiableMap(new TreeMap<>(fileHashes)); + directoryEntries = Collections.unmodifiableMap(new TreeMap<>(directoryEntries)); + issues = List.copyOf(issues); + } + + private static ExclusivitySnapshot empty() { + return new ExclusivitySnapshot(Map.of(), Map.of(), List.of()); + } + } + + private static final class PlanDiagnostics { + private final int maximum; + private final List values = new ArrayList<>(); + private boolean blocked; + + private PlanDiagnostics(int maximum) { + this.maximum = maximum; + } + + private void error( + IrisStructureAdoptionDiagnostic.Code code, + String resource, + String detail, + String recommendation + ) { + add(IrisStructureAdoptionDiagnostic.Severity.ERROR, code, resource, detail, recommendation); + } + + private void warning( + IrisStructureAdoptionDiagnostic.Code code, + String resource, + String detail, + String recommendation + ) { + add(IrisStructureAdoptionDiagnostic.Severity.WARNING, code, resource, detail, recommendation); + } + + private void info( + IrisStructureAdoptionDiagnostic.Code code, + String resource, + String detail, + String recommendation + ) { + add(IrisStructureAdoptionDiagnostic.Severity.INFO, code, resource, detail, recommendation); + } + + private void add( + IrisStructureAdoptionDiagnostic.Severity severity, + IrisStructureAdoptionDiagnostic.Code code, + String resource, + String detail, + String recommendation + ) { + if (severity == IrisStructureAdoptionDiagnostic.Severity.ERROR) { + blocked = true; + } + if (values.size() < maximum) { + values.add(new IrisStructureAdoptionDiagnostic( + severity, + code, + resource, + detail, + recommendation)); + } + } + + private boolean blocked() { + return blocked; + } + + private List values() { + return List.copyOf(values); + } + } + + private static final class StalePlanException extends IOException { + private final String resource; + + private StalePlanException(String resource, String message) { + super(message); + this.resource = resource; + } + + private String resource() { + return resource; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionStrategy.java b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionStrategy.java new file mode 100644 index 000000000..170cb2787 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionStrategy.java @@ -0,0 +1,7 @@ +package art.arcane.iris.core.structure.conversion; + +public enum IrisStructureAdoptionStrategy { + AUTO, + IN_PLACE, + CLONE +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/AtomicDatapackPublisher.java b/core/src/main/java/art/arcane/iris/core/structure/export/AtomicDatapackPublisher.java new file mode 100644 index 000000000..57362fc72 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/AtomicDatapackPublisher.java @@ -0,0 +1,160 @@ +package art.arcane.iris.core.structure.export; + +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import java.util.stream.Stream; + +final class AtomicDatapackPublisher { + Publication publish( + Map resources, + Path output, + VanillaJigsawExportFormat format, + boolean replaceExisting + ) throws IOException { + Path parent = output.getParent(); + if (parent == null) { + throw new IOException("Export output has no parent directory: " + output); + } + Files.createDirectories(parent); + Path staging = Files.createTempDirectory(parent, ".iris-jigsaw-export-"); + List cleanupWarnings = new ArrayList<>(); + try { + Path artifact = format == VanillaJigsawExportFormat.DIRECTORY + ? createDirectoryArtifact(staging, resources) + : createZipArtifact(staging, resources); + replaceAtomically(artifact, output, replaceExisting, cleanupWarnings); + } finally { + try { + deleteTree(staging); + } catch (IOException exception) { + cleanupWarnings.add("Could not remove export staging path '" + staging + "': " + exception.getMessage()); + } + } + return new Publication(List.copyOf(cleanupWarnings)); + } + + private Path createDirectoryArtifact(Path staging, Map resources) throws IOException { + Path root = staging.resolve("pack"); + Files.createDirectory(root); + for (String resource : sortedPaths(resources)) { + Path target = root.resolve(resource).normalize(); + if (!target.startsWith(root)) { + throw new IOException("Export resource escapes staging root: " + resource); + } + Files.createDirectories(target.getParent()); + Files.write(target, resources.get(resource)); + } + return root; + } + + private Path createZipArtifact(Path staging, Map resources) throws IOException { + Path zip = staging.resolve("pack.zip"); + try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(zip))) { + for (String resource : sortedPaths(resources)) { + ZipEntry entry = new ZipEntry(resource); + entry.setTime(0L); + output.putNextEntry(entry); + output.write(resources.get(resource)); + output.closeEntry(); + } + } + return zip; + } + + private void replaceAtomically( + Path artifact, + Path output, + boolean replaceExisting, + List cleanupWarnings + ) throws IOException { + Path backup = null; + if (Files.exists(output)) { + if (!replaceExisting) { + throw new IOException("Export output already exists: " + output); + } + backup = uniqueBackup(output); + atomicMove(output, backup); + } + + try { + atomicMove(artifact, output); + } catch (IOException publicationFailure) { + if (backup != null && Files.exists(backup) && !Files.exists(output)) { + try { + atomicMove(backup, output); + } catch (IOException restoreFailure) { + publicationFailure.addSuppressed(restoreFailure); + } + } + throw publicationFailure; + } + + if (backup != null) { + try { + deleteTree(backup); + } catch (IOException exception) { + cleanupWarnings.add("Export succeeded, but the replaced output backup remains at '" + + backup + "': " + exception.getMessage()); + } + } + } + + private void atomicMove(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException exception) { + throw new IOException("Filesystem does not support an atomic export move from '" + + source + "' to '" + target + "'.", exception); + } + } + + private Path uniqueBackup(Path output) { + return output.resolveSibling("." + output.getFileName() + ".iris-backup-" + UUID.randomUUID()); + } + + private List sortedPaths(Map resources) { + List paths = new ArrayList<>(resources.keySet()); + paths.sort(String::compareTo); + return paths; + } + + private void deleteTree(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + try (Stream paths = Files.walk(root)) { + List ordered = paths.sorted(Comparator.reverseOrder()).toList(); + IOException failure = null; + for (Path path : ordered) { + try { + Files.deleteIfExists(path); + } catch (IOException exception) { + if (failure == null) { + failure = exception; + } else { + failure.addSuppressed(exception); + } + } + } + if (failure != null) { + throw failure; + } + } + } + + record Publication(List cleanupWarnings) { + Publication { + cleanupWarnings = List.copyOf(cleanupWarnings); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaBlockState.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaBlockState.java new file mode 100644 index 000000000..888521311 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaBlockState.java @@ -0,0 +1,88 @@ +package art.arcane.iris.core.structure.export; + +import art.arcane.volmlib.util.nbt.tag.CompoundTag; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; + +record VanillaBlockState(String name, Map properties) { + private static final Pattern PROPERTY_NAME = Pattern.compile("[a-z0-9_.-]+"); + private static final Pattern PROPERTY_VALUE = Pattern.compile("[a-z0-9_.-]+"); + + VanillaBlockState { + properties = Collections.unmodifiableMap(new LinkedHashMap<>(properties)); + } + + static VanillaBlockState parse(String value) { + if (value == null) { + throw new IllegalArgumentException("Block state must not be null"); + } + String source = value.trim(); + int propertiesStart = source.indexOf('['); + String name = propertiesStart < 0 ? source : source.substring(0, propertiesStart); + if (!VanillaResourceIdentifier.validIdentifier(name)) { + throw new IllegalArgumentException("Invalid block identifier '" + name + "'"); + } + if (!name.startsWith("minecraft:")) { + throw new IllegalArgumentException("Vanilla cannot resolve non-Minecraft block '" + name + "'"); + } + if (propertiesStart < 0) { + return new VanillaBlockState(name, Map.of()); + } + if (!source.endsWith("]") || source.indexOf('[', propertiesStart + 1) >= 0) { + throw new IllegalArgumentException("Invalid property block in '" + value + "'"); + } + String body = source.substring(propertiesStart + 1, source.length() - 1); + if (body.isEmpty()) { + throw new IllegalArgumentException("Empty property block in '" + value + "'"); + } + Map properties = new TreeMap<>(); + for (String property : body.split(",", -1)) { + int separator = property.indexOf('='); + if (separator <= 0 || separator != property.lastIndexOf('=') || separator == property.length() - 1) { + throw new IllegalArgumentException("Invalid block property '" + property + "'"); + } + String propertyName = property.substring(0, separator); + String propertyValue = property.substring(separator + 1); + if (!PROPERTY_NAME.matcher(propertyName).matches() || !PROPERTY_VALUE.matcher(propertyValue).matches()) { + throw new IllegalArgumentException("Invalid block property '" + property + "'"); + } + if (properties.put(propertyName, propertyValue) != null) { + throw new IllegalArgumentException("Duplicate block property '" + propertyName + "'"); + } + } + return new VanillaBlockState(name, properties); + } + + String canonical() { + if (properties.isEmpty()) { + return name; + } + StringBuilder builder = new StringBuilder(name).append('['); + boolean first = true; + for (Map.Entry property : properties.entrySet()) { + if (!first) { + builder.append(','); + } + builder.append(property.getKey()).append('=').append(property.getValue()); + first = false; + } + return builder.append(']').toString(); + } + + CompoundTag toNbt() { + CompoundTag state = new CompoundTag(); + state.putString("Name", name); + if (!properties.isEmpty()) { + CompoundTag propertyTag = new CompoundTag(); + for (Map.Entry property : properties.entrySet()) { + propertyTag.putString(property.getKey(), property.getValue()); + } + state.put("Properties", propertyTag); + } + return state; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawDatapackExporter.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawDatapackExporter.java new file mode 100644 index 000000000..d2e2a9b50 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawDatapackExporter.java @@ -0,0 +1,74 @@ +package art.arcane.iris.core.structure.export; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +public final class VanillaJigsawDatapackExporter { + public VanillaJigsawExportValidation validate(VanillaJigsawExportRequest request) { + VanillaJigsawExportCompiler.Compilation compilation = new VanillaJigsawExportCompiler().compile(request); + List resources = new ArrayList<>(compilation.resources().keySet()); + resources.sort(String::compareTo); + return new VanillaJigsawExportValidation(resources, compilation.diagnostics()); + } + + public VanillaJigsawExportResult export(VanillaJigsawExportRequest request) { + if (Files.exists(request.output()) && !request.replaceExisting()) { + VanillaJigsawExportDiagnostic diagnostic = new VanillaJigsawExportDiagnostic( + VanillaJigsawExportDiagnostic.Severity.ERROR, + VanillaJigsawExportDiagnostic.Code.OUTPUT_EXISTS, + request.output().toString(), + "Export output already exists and replaceExisting is false."); + return new VanillaJigsawExportResult( + VanillaJigsawExportResult.Status.REJECTED, + request.output(), + List.of(), + List.of(diagnostic)); + } + + VanillaJigsawExportCompiler.Compilation compilation = new VanillaJigsawExportCompiler().compile(request); + if (compilation.hasErrors()) { + return new VanillaJigsawExportResult( + VanillaJigsawExportResult.Status.REJECTED, + request.output(), + List.of(), + compilation.diagnostics()); + } + + List diagnostics = new ArrayList<>(compilation.diagnostics()); + try { + AtomicDatapackPublisher.Publication publication = new AtomicDatapackPublisher().publish( + compilation.resources(), + request.output(), + request.format(), + request.replaceExisting()); + for (String warning : publication.cleanupWarnings()) { + diagnostics.add(new VanillaJigsawExportDiagnostic( + VanillaJigsawExportDiagnostic.Severity.WARNING, + VanillaJigsawExportDiagnostic.Code.CLEANUP_FAILED, + request.output().toString(), + warning)); + } + } catch (IOException exception) { + diagnostics.add(new VanillaJigsawExportDiagnostic( + VanillaJigsawExportDiagnostic.Severity.ERROR, + VanillaJigsawExportDiagnostic.Code.PUBLICATION_FAILED, + request.output().toString(), + "Atomic datapack publication failed: " + exception.getMessage())); + return new VanillaJigsawExportResult( + VanillaJigsawExportResult.Status.FAILED, + request.output(), + List.of(), + diagnostics); + } + + List resources = new ArrayList<>(compilation.resources().keySet()); + resources.sort(String::compareTo); + return new VanillaJigsawExportResult( + VanillaJigsawExportResult.Status.EXPORTED, + request.output(), + resources, + diagnostics); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportCompiler.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportCompiler.java new file mode 100644 index 000000000..73f45f97d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportCompiler.java @@ -0,0 +1,815 @@ +package art.arcane.iris.core.structure.export; + +import art.arcane.iris.engine.framework.structure.CompiledStructureGraph; +import art.arcane.iris.engine.framework.structure.PlanarJigsawWorkcellResolver; +import art.arcane.iris.engine.framework.structure.StructureGraphCompilation; +import art.arcane.iris.engine.framework.structure.StructureGraphCompiler; +import art.arcane.iris.engine.framework.structure.StructureGraphDiagnostic; +import art.arcane.iris.engine.object.IrisJigsawCompatibility; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawMode; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPieceEntry; +import art.arcane.iris.engine.object.IrisJigsawPieceRules; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.IrisBlockVector; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class VanillaJigsawExportCompiler { + private static final int MAX_DEPTH = 20; + private static final int MAX_HORIZONTAL_DISTANCE = 128; + private static final int MAX_VERTICAL_DISTANCE = 4064; + private static final int MIN_ABSOLUTE_Y = -2032; + private static final int MAX_ABSOLUTE_Y = 2031; + private static final int MAX_POOL_WEIGHT = 150; + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); + + Compilation compile(VanillaJigsawExportRequest request) { + List diagnostics = new ArrayList<>(); + validateIdentity(request, diagnostics); + validateSettings(request.settings(), diagnostics); + + IrisStructure structure; + try { + structure = request.source().loadStructure(); + } catch (RuntimeException exception) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.SOURCE_STRUCTURE_MISSING, + request.source().structureKey(), + "Could not load Iris structure '" + request.source().structureKey() + "': " + + exception.getMessage()); + return rejected(diagnostics); + } + if (structure == null) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.SOURCE_STRUCTURE_MISSING, + request.source().structureKey(), + "Iris structure '" + request.source().structureKey() + "' does not exist."); + return rejected(diagnostics); + } + + validateStructure(structure, request.settings(), diagnostics); + StructureGraphCompilation graphCompilation; + try { + graphCompilation = StructureGraphCompiler.compile(structure, request.source().resolver()); + } catch (RuntimeException exception) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.GRAPH_VALIDATION, + request.source().structureKey(), + "Structure graph compilation failed: " + exception.getMessage()); + return rejected(diagnostics); + } + addGraphDiagnostics(graphCompilation, diagnostics); + CompiledStructureGraph graph = graphCompilation.getGraph(); + validateGraph(graph, diagnostics); + if (hasErrors(diagnostics)) { + return rejected(diagnostics); + } + + try { + Map resources = createResources(request, graph); + return new Compilation(resources, List.copyOf(diagnostics)); + } catch (IOException | RuntimeException exception) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.SERIALIZATION_FAILED, + request.source().structureKey(), + "Vanilla datapack serialization failed: " + exception.getMessage()); + return rejected(diagnostics); + } + } + + private void validateIdentity( + VanillaJigsawExportRequest request, + List diagnostics + ) { + if (!VanillaResourceIdentifier.validNamespace(request.namespace())) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_NAMESPACE, + request.namespace(), + "Datapack namespace must match [a-z0-9_.-]+."); + } + if (!VanillaResourceIdentifier.validPath(request.resourcePath())) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_RESOURCE_PATH, + request.resourcePath(), + "Datapack resource path must use lowercase resource-path characters without traversal."); + } + } + + private void validateSettings( + VanillaJigsawExportSettings settings, + List diagnostics + ) { + if (settings.biomes().isEmpty()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_BIOME, + "biomes", + "At least one biome identifier is required."); + } + for (String biome : settings.biomes()) { + if (!VanillaResourceIdentifier.validIdentifier(biome)) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_BIOME, + biome, + "Biome identifier '" + biome + "' is not a valid namespaced identifier."); + } + } + if (settings.startHeight() < MIN_ABSOLUTE_Y || settings.startHeight() > MAX_ABSOLUTE_Y) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_SETTINGS, + "start_height", + "Absolute start height must be between " + MIN_ABSOLUTE_Y + " and " + MAX_ABSOLUTE_Y + "."); + } + if (settings.maxDistanceVertical() < 1 || settings.maxDistanceVertical() > MAX_VERTICAL_DISTANCE) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE, + "max_distance_from_center.vertical", + "Vertical maximum distance must be between 1 and " + MAX_VERTICAL_DISTANCE + "."); + } + if (settings.spacing() < 0 || settings.spacing() > 4096 + || settings.separation() < 0 || settings.separation() > 4096 + || settings.spacing() <= settings.separation()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_SETTINGS, + "random_spread", + "Random-spread spacing and separation must be within 0..4096, with spacing greater than separation."); + } + if (settings.salt() < 0) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_SETTINGS, + "random_spread.salt", + "Random-spread salt must be non-negative."); + } + if (!Float.isFinite(settings.frequency()) || settings.frequency() < 0.0F || settings.frequency() > 1.0F) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_SETTINGS, + "random_spread.frequency", + "Random-spread frequency must be finite and within 0..1."); + } + } + + private void validateStructure( + IrisStructure structure, + VanillaJigsawExportSettings settings, + List diagnostics + ) { + if (structure.resolvedCompatibility() != IrisJigsawCompatibility.VANILLA_PORTABLE) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_COMPATIBILITY, + structureKey(structure), + "Strict export requires compatibility VANILLA_PORTABLE."); + } + if (structure.getPlaceMode() != ObjectPlaceMode.STRUCTURE_PIECE) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_PLACE_MODE, + structureKey(structure), + "Vanilla export requires placeMode STRUCTURE_PIECE; Iris terrain and stilt modes have no vanilla jigsaw equivalent."); + } + if (structure.getEdit() != null && !structure.getEdit().isEmpty()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_EDIT, + structureKey(structure), + "Structure-wide Iris block edits cannot be represented losslessly by vanilla template pools."); + } + if (structure.getLoot() != null && !structure.getLoot().isEmpty()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_LOOT, + structureKey(structure), + "Structure-wide Iris loot injection cannot be represented losslessly by this vanilla exporter."); + } + if (structure.getThemeSets() != null && !structure.getThemeSets().isEmpty()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_THEME_METADATA, + structureKey(structure), + "Coherent Iris theme selection has no lossless vanilla jigsaw representation."); + } + if (structure.isRequireCaps()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_REQUIRED_CAPS, + structureKey(structure), + "Vanilla jigsaws cannot enforce Iris requireCaps terminal-closure semantics."); + } + if (structure.getMaxDepth() < 1 || structure.getMaxDepth() > MAX_DEPTH) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DEPTH, + structureKey(structure), + "Minecraft 26.2 jigsaw size must be within 0..20; Iris export requires 1..20."); + } + long horizontalDistance = (long) structure.getMaxSizeChunks() * 16L; + if (horizontalDistance < 1L || horizontalDistance > MAX_HORIZONTAL_DISTANCE) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE, + structureKey(structure), + "Iris maxSizeChunks maps to " + horizontalDistance + + " blocks, outside Minecraft 26.2's 1..128 horizontal limit."); + } + int terrainPadding = settings.terrainAdaptation() == VanillaJigsawExportSettings.TerrainAdaptation.NONE + ? 0 : 12; + if (horizontalDistance + terrainPadding > MAX_HORIZONTAL_DISTANCE) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE, + structureKey(structure), + "Horizontal distance plus vanilla terrain-adaptation padding must not exceed 128 blocks."); + } + } + + private void addGraphDiagnostics( + StructureGraphCompilation graphCompilation, + List diagnostics + ) { + for (StructureGraphDiagnostic diagnostic : graphCompilation.getDiagnostics()) { + VanillaJigsawExportDiagnostic.Severity severity = diagnostic.severity() + == StructureGraphDiagnostic.Severity.ERROR + ? VanillaJigsawExportDiagnostic.Severity.ERROR + : VanillaJigsawExportDiagnostic.Severity.WARNING; + diagnostics.add(new VanillaJigsawExportDiagnostic( + severity, + VanillaJigsawExportDiagnostic.Code.GRAPH_VALIDATION, + diagnostic.code().name(), + diagnostic.message())); + } + } + + private void validateGraph( + CompiledStructureGraph graph, + List diagnostics + ) { + Map workcells = + resolvedWorkcells(graph.getStructure()); + for (Map.Entry poolEntry : graph.getPools().entrySet()) { + String poolKey = poolEntry.getKey(); + if (!VanillaResourceIdentifier.validPath(poolKey)) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_RESOURCE_PATH, + poolKey, + "Iris pool keys must be valid lowercase resource paths for vanilla export."); + } + validatePool(graph, workcells, poolKey, poolEntry.getValue(), diagnostics); + } + for (Map.Entry pieceEntry : graph.getPieces().entrySet()) { + if (!pieceEnabled(workcells, pieceEntry.getValue())) { + continue; + } + String pieceKey = pieceEntry.getKey(); + if (!VanillaResourceIdentifier.validPath(pieceKey)) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_RESOURCE_PATH, + pieceKey, + "Iris piece keys must be valid lowercase resource paths for vanilla export."); + } + validatePiece(graph, pieceKey, pieceEntry.getValue(), diagnostics); + } + } + + private void validatePool( + CompiledStructureGraph graph, + Map workcells, + String poolKey, + IrisJigsawPool pool, + List diagnostics + ) { + if (pool.isMandatoryFallback()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_REQUIRED_CAPS, + poolKey, + "Vanilla template pools cannot enforce Iris mandatoryFallback terminal closure."); + } + if (pool.getFallback() != null && !pool.getFallback().isBlank() + && !VanillaResourceIdentifier.validPath(pool.getFallback().trim())) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_RESOURCE_PATH, + poolKey, + "Fallback pool key '" + pool.getFallback() + "' is not a valid vanilla resource path."); + } + if (pool.getPieces() == null) { + return; + } + for (int index = 0; index < pool.getPieces().size(); index++) { + IrisJigsawPieceEntry entry = pool.getPieces().get(index); + if (entry == null) { + continue; + } + if (!entry.isEmpty() + && !pieceEnabled(workcells, graph.getPieces().get(trim(entry.getPiece())))) { + continue; + } + if (entry.getWeight() < 1 || entry.getWeight() > MAX_POOL_WEIGHT) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_POOL_WEIGHT, + poolKey + "/pieces[" + index + "]", + "Minecraft 26.2 template-pool weights must be within 1..150."); + } + if (entry.getChance() != 1D) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_CHANCE, + poolKey + "/pieces[" + index + "]", + "Independent Iris membership chance cannot be represented by vanilla pool weights."); + } + if (!entry.isEmpty() && !VanillaResourceIdentifier.validPath(trim(entry.getPiece()))) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_RESOURCE_PATH, + poolKey + "/pieces[" + index + "]", + "Piece key '" + entry.getPiece() + "' is not a valid vanilla resource path."); + } + } + } + + private void validatePiece( + CompiledStructureGraph graph, + String pieceKey, + IrisJigsawPiece piece, + List diagnostics + ) { + if (piece.getThemes() != null && !piece.getThemes().isEmpty()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_THEME_METADATA, + pieceKey, + "Iris piece theme membership has no vanilla template-pool equivalent."); + } + IrisJigsawPieceRules rules = piece.resolvedRules(); + if (rules.getMinimumDepth() != 0 + || rules.getMaximumDepth() != 30 + || rules.getMinimumPlacements() != 0 + || rules.getMaximumPlacements() != 0 + || rules.isTerminal()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_PIECE_RULES, + pieceKey, + "Iris depth, placement-count, and terminal rules cannot be serialized losslessly to vanilla."); + } + if (!piece.isRotatable()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_FIXED_ROTATION, + pieceKey, + "Vanilla template pools do not provide an exact fixed-rotation equivalent for Iris rotatable=false."); + } + if (!piece.isCollidable()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_NON_COLLIDABLE_PIECE, + pieceKey, + "Vanilla template pools cannot serialize Iris collidable=false assembly metadata."); + } + IrisObject object = graph.getObjects().get(trim(piece.getObject())); + if (object == null) { + return; + } + validateObject(pieceKey, object, diagnostics); + validateConnectors(graph, pieceKey, piece, object, diagnostics); + } + + private void validateObject( + String pieceKey, + IrisObject object, + List diagnostics + ) { + if (!object.getStates().isEmpty()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_TILE_DATA, + pieceKey, + "The .iob contains tile payloads; exact registry-aware block-entity NBT export is not available in core."); + } + for (Map.Entry entry : object.getBlocks()) { + PlatformBlockState state = entry.getValue(); + String resource = pieceKey + "@" + entry.getKey(); + IrisBlockVector position = entry.getKey(); + int x = position.getBlockX() + object.getCenter().getX(); + int y = position.getBlockY() + object.getCenter().getY(); + int z = position.getBlockZ() + object.getCenter().getZ(); + if (x < 0 || x >= object.getW() || y < 0 || y >= object.getH() || z < 0 || z >= object.getD()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_BLOCK_STATE, + resource, + "The .iob contains a block outside its declared dimensions."); + continue; + } + if (state.isCustom()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_CUSTOM_BLOCK, + resource, + "Custom-content block '" + state.key() + "' is not available in unmodded vanilla."); + continue; + } + if (state.hasTileEntity()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_BLOCK_ENTITY, + resource, + "Block '" + state.key() + "' requires block-entity NBT that core cannot export losslessly."); + } + try { + VanillaBlockState parsed = VanillaBlockState.parse(state.key()); + if (parsed.name().equals("minecraft:jigsaw") + || parsed.name().equals("minecraft:structure_block") + || parsed.name().equals("minecraft:structure_void")) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_MARKER_BLOCK, + resource, + "Marker block '" + parsed.name() + "' must be represented by Iris connector metadata, not object blocks."); + } + } catch (IllegalArgumentException exception) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_BLOCK_STATE, + resource, + exception.getMessage()); + } + } + } + + private void validateConnectors( + CompiledStructureGraph graph, + String pieceKey, + IrisJigsawPiece piece, + IrisObject object, + List diagnostics + ) { + if (piece.getConnectors() == null) { + return; + } + Set positions = new LinkedHashSet<>(); + for (int index = 0; index < piece.getConnectors().size(); index++) { + IrisJigsawConnector connector = piece.getConnectors().get(index); + if (connector == null) { + continue; + } + String resource = pieceKey + "/connectors[" + index + "]"; + if (connector.getChannel() != null && !connector.getChannel().isBlank()) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.UNSUPPORTED_CHANNEL, + resource, + "Iris connector channels have no vanilla jigsaw NBT equivalent."); + } + validateConnectorIdentifier(connector.getName(), resource + "/name", diagnostics); + validateConnectorIdentifier(connector.getTargetName(), resource + "/target", diagnostics); + String poolKey = trim(connector.getPool()); + if (!VanillaResourceIdentifier.validPath(poolKey) || !graph.getPools().containsKey(poolKey)) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_RESOURCE_PATH, + resource + "/pool", + "Connector pool '" + connector.getPool() + "' cannot be mapped to an exported template pool."); + } + try { + VanillaStructureTemplateEncoder.orientation(connector.getDirection(), connector.getTop()); + } catch (IllegalArgumentException | NullPointerException exception) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_CONNECTOR_ORIENTATION, + resource, + exception.getMessage() == null ? "Connector orientation is incomplete." : exception.getMessage()); + } + IrisPosition position = connector.getPosition(); + if (position == null || !inside(position, object)) { + continue; + } + String positionKey = position.getX() + "," + position.getY() + "," + position.getZ(); + if (!positions.add(positionKey)) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.DUPLICATE_CONNECTOR_POSITION, + resource, + "Multiple vanilla jigsaw block entities cannot occupy " + positionKey + "."); + } + validateFinalState(object, connector, resource, diagnostics); + } + } + + private void validateConnectorIdentifier( + String value, + String resource, + List diagnostics + ) { + try { + VanillaResourceIdentifier.normalizeConnectorIdentifier(value); + } catch (IllegalArgumentException exception) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_CONNECTOR_ID, + resource, + exception.getMessage()); + } + } + + private void validateFinalState( + IrisObject object, + IrisJigsawConnector connector, + String resource, + List diagnostics + ) { + VanillaBlockState configured; + try { + configured = VanillaBlockState.parse(connector.getFinalState()); + } catch (IllegalArgumentException exception) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_CONNECTOR_FINAL_STATE, + resource, + exception.getMessage()); + return; + } + IrisPosition position = connector.getPosition(); + PlatformBlockState objectState = object.getBlocks().get( + object.getSigned(position.getX(), position.getY(), position.getZ())); + String expectedSource = objectState == null ? "minecraft:structure_void" : objectState.key(); + try { + VanillaBlockState expected = VanillaBlockState.parse(expectedSource); + if (!configured.canonical().equals(expected.canonical())) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_CONNECTOR_FINAL_STATE, + resource, + "Connector finalState '" + configured.canonical() + "' does not match the .iob block '" + + expected.canonical() + "' at that position. Use minecraft:structure_void for an absent block."); + } + } catch (IllegalArgumentException exception) { + addError(diagnostics, + VanillaJigsawExportDiagnostic.Code.INVALID_CONNECTOR_FINAL_STATE, + resource, + "The .iob block under the connector is not vanilla-compatible: " + exception.getMessage()); + } + } + + private Map createResources( + VanillaJigsawExportRequest request, + CompiledStructureGraph graph + ) throws IOException { + Map resources = new LinkedHashMap<>(); + putJson(resources, "pack.mcmeta", packMetadata(request)); + putJson(resources, biomeTagPath(request), biomeTag(request)); + putJson(resources, processorPath(request), processorList()); + putJson(resources, structurePath(request), structure(request, graph.getStructure())); + putJson(resources, structureSetPath(request), structureSet(request)); + + for (Map.Entry poolEntry : graph.getPools().entrySet()) { + putJson(resources, + poolPath(request, poolEntry.getKey()), + templatePool(request, graph, poolEntry.getValue())); + } + + VanillaStructureTemplateEncoder encoder = new VanillaStructureTemplateEncoder(); + Map workcells = + resolvedWorkcells(graph.getStructure()); + for (Map.Entry pieceEntry : graph.getPieces().entrySet()) { + if (!pieceEnabled(workcells, pieceEntry.getValue())) { + continue; + } + IrisObject object = graph.getObjects().get(trim(pieceEntry.getValue().getObject())); + byte[] template = encoder.encode( + object, + pieceEntry.getValue(), + poolKey -> poolIdentifier(request, trim(poolKey))); + resources.put(templatePath(request, pieceEntry.getKey()), template); + } + return resources; + } + + private JsonObject packMetadata(VanillaJigsawExportRequest request) { + JsonObject pack = new JsonObject(); + pack.addProperty("description", request.description()); + JsonArray minimum = new JsonArray(); + minimum.add(107); + minimum.add(1); + pack.add("min_format", minimum); + pack.addProperty("max_format", 107); + JsonObject root = new JsonObject(); + root.add("pack", pack); + return root; + } + + private JsonObject biomeTag(VanillaJigsawExportRequest request) { + JsonObject root = new JsonObject(); + root.addProperty("replace", false); + JsonArray values = new JsonArray(); + for (String biome : request.settings().biomes()) { + values.add(biome); + } + root.add("values", values); + return root; + } + + private JsonObject processorList() { + JsonObject root = new JsonObject(); + root.add("processors", new JsonArray()); + return root; + } + + private JsonObject structure(VanillaJigsawExportRequest request, IrisStructure structure) { + VanillaJigsawExportSettings settings = request.settings(); + JsonObject root = new JsonObject(); + root.addProperty("type", "minecraft:jigsaw"); + root.addProperty("biomes", "#" + request.namespace() + ":" + request.resourcePath()); + int horizontalDistance = structure.getMaxSizeChunks() * 16; + if (horizontalDistance == settings.maxDistanceVertical()) { + root.addProperty("max_distance_from_center", horizontalDistance); + } else { + JsonObject distance = new JsonObject(); + distance.addProperty("horizontal", horizontalDistance); + distance.addProperty("vertical", settings.maxDistanceVertical()); + root.add("max_distance_from_center", distance); + } + if (settings.projectHeightmap() != VanillaJigsawExportSettings.ProjectHeightmap.NONE) { + root.addProperty("project_start_to_heightmap", settings.projectHeightmap().serializedName()); + } + root.addProperty("size", structure.getMaxDepth()); + root.add("spawn_overrides", new JsonObject()); + JsonObject startHeight = new JsonObject(); + startHeight.addProperty("absolute", settings.startHeight()); + root.add("start_height", startHeight); + root.addProperty("start_pool", poolIdentifier(request, trim(structure.getStartPool()))); + root.addProperty("step", settings.generationStep().serializedName()); + root.addProperty("terrain_adaptation", settings.terrainAdaptation().serializedName()); + root.addProperty("use_expansion_hack", settings.expansionHack()); + return root; + } + + private JsonObject structureSet(VanillaJigsawExportRequest request) { + JsonObject structureEntry = new JsonObject(); + structureEntry.addProperty("structure", structureIdentifier(request)); + structureEntry.addProperty("weight", 1); + JsonArray structures = new JsonArray(); + structures.add(structureEntry); + + VanillaJigsawExportSettings settings = request.settings(); + JsonObject placement = new JsonObject(); + placement.addProperty("type", "minecraft:random_spread"); + placement.addProperty("frequency", settings.frequency()); + placement.addProperty("salt", settings.salt()); + placement.addProperty("separation", settings.separation()); + placement.addProperty("spacing", settings.spacing()); + placement.addProperty("spread_type", settings.spreadType().serializedName()); + + JsonObject root = new JsonObject(); + root.add("placement", placement); + root.add("structures", structures); + return root; + } + + private JsonObject templatePool( + VanillaJigsawExportRequest request, + CompiledStructureGraph graph, + IrisJigsawPool pool + ) { + JsonObject root = new JsonObject(); + String fallback = trim(pool.getFallback()); + root.addProperty("fallback", fallback.isEmpty() ? "minecraft:empty" : poolIdentifier(request, fallback)); + JsonArray elements = new JsonArray(); + Map workcells = + resolvedWorkcells(graph.getStructure()); + if (pool.getPieces() != null) { + for (IrisJigsawPieceEntry entry : pool.getPieces()) { + if (!entry.isEmpty() + && !pieceEnabled(workcells, graph.getPieces().get(trim(entry.getPiece())))) { + continue; + } + JsonObject weightedElement = new JsonObject(); + JsonObject element = new JsonObject(); + if (entry.isEmpty()) { + element.addProperty("element_type", "minecraft:empty_pool_element"); + } else { + element.addProperty("element_type", "minecraft:single_pool_element"); + element.addProperty("location", templateIdentifier(request, trim(entry.getPiece()))); + element.addProperty("processors", processorIdentifier(request)); + element.addProperty("projection", "rigid"); + } + weightedElement.add("element", element); + weightedElement.addProperty("weight", entry.getWeight()); + elements.add(weightedElement); + } + } + root.add("elements", elements); + return root; + } + + private static Map + resolvedWorkcells(IrisStructure structure) { + return structure.resolvedMode() == IrisJigsawMode.PLANAR_JIGSAW + ? PlanarJigsawWorkcellResolver.resolve(structure) + : Map.of(); + } + + private static boolean pieceEnabled( + Map workcells, + IrisJigsawPiece piece + ) { + return workcells.isEmpty() + || piece == null + || PlanarJigsawWorkcellResolver.workcell(workcells, piece).enabled(); + } + + private void putJson(Map resources, String path, JsonObject value) { + resources.put(path, (GSON.toJson(value) + "\n").getBytes(StandardCharsets.UTF_8)); + } + + private String biomeTagPath(VanillaJigsawExportRequest request) { + return "data/" + request.namespace() + "/tags/worldgen/biome/" + request.resourcePath() + ".json"; + } + + private String processorPath(VanillaJigsawExportRequest request) { + return "data/" + request.namespace() + "/worldgen/processor_list/" + + request.resourcePath() + "/empty.json"; + } + + private String structurePath(VanillaJigsawExportRequest request) { + return "data/" + request.namespace() + "/worldgen/structure/" + request.resourcePath() + ".json"; + } + + private String structureSetPath(VanillaJigsawExportRequest request) { + return "data/" + request.namespace() + "/worldgen/structure_set/" + request.resourcePath() + ".json"; + } + + private String poolPath(VanillaJigsawExportRequest request, String poolKey) { + return "data/" + request.namespace() + "/worldgen/template_pool/" + + request.resourcePath() + "/pool/" + poolKey + ".json"; + } + + private String templatePath(VanillaJigsawExportRequest request, String pieceKey) { + return "data/" + request.namespace() + "/structure/" + + request.resourcePath() + "/piece/" + pieceKey + ".nbt"; + } + + private String structureIdentifier(VanillaJigsawExportRequest request) { + return request.namespace() + ":" + request.resourcePath(); + } + + private String processorIdentifier(VanillaJigsawExportRequest request) { + return request.namespace() + ":" + request.resourcePath() + "/empty"; + } + + private String poolIdentifier(VanillaJigsawExportRequest request, String poolKey) { + return request.namespace() + ":" + request.resourcePath() + "/pool/" + poolKey; + } + + private String templateIdentifier(VanillaJigsawExportRequest request, String pieceKey) { + return request.namespace() + ":" + request.resourcePath() + "/piece/" + pieceKey; + } + + private boolean inside(IrisPosition position, IrisObject object) { + return position.getX() >= 0 && position.getX() < object.getW() + && position.getY() >= 0 && position.getY() < object.getH() + && position.getZ() >= 0 && position.getZ() < object.getD(); + } + + private boolean hasErrors(List diagnostics) { + for (VanillaJigsawExportDiagnostic diagnostic : diagnostics) { + if (diagnostic.isBlocking()) { + return true; + } + } + return false; + } + + private Compilation rejected(List diagnostics) { + return new Compilation(Map.of(), List.copyOf(diagnostics)); + } + + private void addError( + List diagnostics, + VanillaJigsawExportDiagnostic.Code code, + String resource, + String message + ) { + diagnostics.add(new VanillaJigsawExportDiagnostic( + VanillaJigsawExportDiagnostic.Severity.ERROR, + code, + resource, + message)); + } + + private String structureKey(IrisStructure structure) { + String key = structure.getLoadKey(); + return key == null || key.isBlank() ? "" : key; + } + + private String trim(String value) { + return value == null ? "" : value.trim(); + } + + record Compilation( + Map resources, + List diagnostics + ) { + Compilation { + resources = Map.copyOf(resources); + diagnostics = List.copyOf(diagnostics); + } + + boolean hasErrors() { + for (VanillaJigsawExportDiagnostic diagnostic : diagnostics) { + if (diagnostic.isBlocking()) { + return true; + } + } + return false; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportDiagnostic.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportDiagnostic.java new file mode 100644 index 000000000..42aa3b160 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportDiagnostic.java @@ -0,0 +1,58 @@ +package art.arcane.iris.core.structure.export; + +import java.util.Objects; + +public record VanillaJigsawExportDiagnostic(Severity severity, Code code, String resource, String message) { + public VanillaJigsawExportDiagnostic { + Objects.requireNonNull(severity); + Objects.requireNonNull(code); + resource = resource == null ? "" : resource; + Objects.requireNonNull(message); + } + + public boolean isBlocking() { + return severity == Severity.ERROR; + } + + public enum Severity { + ERROR, + WARNING + } + + public enum Code { + SOURCE_STRUCTURE_MISSING, + GRAPH_VALIDATION, + INVALID_NAMESPACE, + INVALID_RESOURCE_PATH, + INVALID_BIOME, + INVALID_SETTINGS, + UNSUPPORTED_COMPATIBILITY, + UNSUPPORTED_PLACE_MODE, + UNSUPPORTED_EDIT, + UNSUPPORTED_LOOT, + UNSUPPORTED_THEME_METADATA, + UNSUPPORTED_CHANCE, + UNSUPPORTED_PIECE_RULES, + UNSUPPORTED_REQUIRED_CAPS, + UNSUPPORTED_CHANNEL, + UNSUPPORTED_FIXED_ROTATION, + UNSUPPORTED_NON_COLLIDABLE_PIECE, + UNSUPPORTED_TILE_DATA, + UNSUPPORTED_BLOCK_ENTITY, + UNSUPPORTED_CUSTOM_BLOCK, + UNSUPPORTED_MARKER_BLOCK, + INVALID_MAX_DEPTH, + INVALID_MAX_DISTANCE, + INVALID_POOL_WEIGHT, + INVALID_CONNECTOR, + INVALID_CONNECTOR_ID, + INVALID_CONNECTOR_ORIENTATION, + INVALID_CONNECTOR_FINAL_STATE, + DUPLICATE_CONNECTOR_POSITION, + INVALID_BLOCK_STATE, + OUTPUT_EXISTS, + SERIALIZATION_FAILED, + PUBLICATION_FAILED, + CLEANUP_FAILED + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportFormat.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportFormat.java new file mode 100644 index 000000000..f9665dd78 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportFormat.java @@ -0,0 +1,6 @@ +package art.arcane.iris.core.structure.export; + +public enum VanillaJigsawExportFormat { + DIRECTORY, + ZIP +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportRequest.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportRequest.java new file mode 100644 index 000000000..1f0def324 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportRequest.java @@ -0,0 +1,113 @@ +package art.arcane.iris.core.structure.export; + +import java.nio.file.Path; +import java.util.Objects; + +public final class VanillaJigsawExportRequest { + private final VanillaJigsawExportSource source; + private final Path output; + private final String namespace; + private final String resourcePath; + private final String description; + private final VanillaJigsawExportFormat format; + private final VanillaJigsawExportSettings settings; + private final boolean replaceExisting; + + private VanillaJigsawExportRequest(Builder builder) { + source = builder.source; + output = builder.output.toAbsolutePath().normalize(); + namespace = builder.namespace; + resourcePath = builder.resourcePath; + description = builder.description; + format = builder.format; + settings = builder.settings; + replaceExisting = builder.replaceExisting; + } + + public static Builder builder(VanillaJigsawExportSource source, Path output) { + return new Builder(source, output); + } + + public VanillaJigsawExportSource source() { + return source; + } + + public Path output() { + return output; + } + + public String namespace() { + return namespace; + } + + public String resourcePath() { + return resourcePath; + } + + public String description() { + return description; + } + + public VanillaJigsawExportFormat format() { + return format; + } + + public VanillaJigsawExportSettings settings() { + return settings; + } + + public boolean replaceExisting() { + return replaceExisting; + } + + public static final class Builder { + private final VanillaJigsawExportSource source; + private final Path output; + private String namespace = "iris"; + private String resourcePath; + private String description = "Iris vanilla jigsaw export for Minecraft 26.2"; + private VanillaJigsawExportFormat format = VanillaJigsawExportFormat.DIRECTORY; + private VanillaJigsawExportSettings settings = VanillaJigsawExportSettings.defaults(); + private boolean replaceExisting; + + private Builder(VanillaJigsawExportSource source, Path output) { + this.source = Objects.requireNonNull(source); + this.output = Objects.requireNonNull(output); + resourcePath = source.structureKey(); + } + + public Builder namespace(String value) { + namespace = Objects.requireNonNull(value).trim(); + return this; + } + + public Builder resourcePath(String value) { + resourcePath = Objects.requireNonNull(value).trim(); + return this; + } + + public Builder description(String value) { + description = Objects.requireNonNull(value); + return this; + } + + public Builder format(VanillaJigsawExportFormat value) { + format = Objects.requireNonNull(value); + return this; + } + + public Builder settings(VanillaJigsawExportSettings value) { + settings = Objects.requireNonNull(value); + return this; + } + + public Builder replaceExisting(boolean value) { + replaceExisting = value; + return this; + } + + public VanillaJigsawExportRequest build() { + return new VanillaJigsawExportRequest(this); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportResult.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportResult.java new file mode 100644 index 000000000..e3fc24e20 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportResult.java @@ -0,0 +1,38 @@ +package art.arcane.iris.core.structure.export; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +public record VanillaJigsawExportResult( + Status status, + Path output, + List resources, + List diagnostics +) { + public VanillaJigsawExportResult { + Objects.requireNonNull(status); + Objects.requireNonNull(output); + resources = List.copyOf(resources); + diagnostics = List.copyOf(diagnostics); + } + + public boolean isSuccess() { + return status == Status.EXPORTED; + } + + public boolean hasBlockingDiagnostics() { + for (VanillaJigsawExportDiagnostic diagnostic : diagnostics) { + if (diagnostic.isBlocking()) { + return true; + } + } + return false; + } + + public enum Status { + EXPORTED, + REJECTED, + FAILED + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportSettings.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportSettings.java new file mode 100644 index 000000000..ae4522815 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportSettings.java @@ -0,0 +1,243 @@ +package art.arcane.iris.core.structure.export; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class VanillaJigsawExportSettings { + private final List biomes; + private final int startHeight; + private final ProjectHeightmap projectHeightmap; + private final GenerationStep generationStep; + private final TerrainAdaptation terrainAdaptation; + private final boolean expansionHack; + private final int maxDistanceVertical; + private final int spacing; + private final int separation; + private final int salt; + private final float frequency; + private final SpreadType spreadType; + + private VanillaJigsawExportSettings(Builder builder) { + biomes = List.copyOf(builder.biomes); + startHeight = builder.startHeight; + projectHeightmap = builder.projectHeightmap; + generationStep = builder.generationStep; + terrainAdaptation = builder.terrainAdaptation; + expansionHack = builder.expansionHack; + maxDistanceVertical = builder.maxDistanceVertical; + spacing = builder.spacing; + separation = builder.separation; + salt = builder.salt; + frequency = builder.frequency; + spreadType = builder.spreadType; + } + + public static Builder builder() { + return new Builder(); + } + + public static VanillaJigsawExportSettings defaults() { + return builder().build(); + } + + public List biomes() { + return biomes; + } + + public int startHeight() { + return startHeight; + } + + public ProjectHeightmap projectHeightmap() { + return projectHeightmap; + } + + public GenerationStep generationStep() { + return generationStep; + } + + public TerrainAdaptation terrainAdaptation() { + return terrainAdaptation; + } + + public boolean expansionHack() { + return expansionHack; + } + + public int maxDistanceVertical() { + return maxDistanceVertical; + } + + public int spacing() { + return spacing; + } + + public int separation() { + return separation; + } + + public int salt() { + return salt; + } + + public float frequency() { + return frequency; + } + + public SpreadType spreadType() { + return spreadType; + } + + public enum ProjectHeightmap { + NONE(null), + WORLD_SURFACE_WG("WORLD_SURFACE_WG"), + WORLD_SURFACE("WORLD_SURFACE"), + OCEAN_FLOOR_WG("OCEAN_FLOOR_WG"), + OCEAN_FLOOR("OCEAN_FLOOR"), + MOTION_BLOCKING("MOTION_BLOCKING"), + MOTION_BLOCKING_NO_LEAVES("MOTION_BLOCKING_NO_LEAVES"); + + private final String serializedName; + + ProjectHeightmap(String serializedName) { + this.serializedName = serializedName; + } + + public String serializedName() { + return serializedName; + } + } + + public enum GenerationStep { + RAW_GENERATION("raw_generation"), + LAKES("lakes"), + LOCAL_MODIFICATIONS("local_modifications"), + UNDERGROUND_STRUCTURES("underground_structures"), + SURFACE_STRUCTURES("surface_structures"), + STRONGHOLDS("strongholds"), + UNDERGROUND_ORES("underground_ores"), + UNDERGROUND_DECORATION("underground_decoration"), + FLUID_SPRINGS("fluid_springs"), + VEGETAL_DECORATION("vegetal_decoration"), + TOP_LAYER_MODIFICATION("top_layer_modification"); + + private final String serializedName; + + GenerationStep(String serializedName) { + this.serializedName = serializedName; + } + + public String serializedName() { + return serializedName; + } + } + + public enum TerrainAdaptation { + NONE("none"), + BURY("bury"), + BEARD_THIN("beard_thin"), + BEARD_BOX("beard_box"), + ENCAPSULATE("encapsulate"); + + private final String serializedName; + + TerrainAdaptation(String serializedName) { + this.serializedName = serializedName; + } + + public String serializedName() { + return serializedName; + } + } + + public enum SpreadType { + LINEAR("linear"), + TRIANGULAR("triangular"); + + private final String serializedName; + + SpreadType(String serializedName) { + this.serializedName = serializedName; + } + + public String serializedName() { + return serializedName; + } + } + + public static final class Builder { + private final List biomes = new ArrayList<>(List.of("minecraft:plains")); + private int startHeight; + private ProjectHeightmap projectHeightmap = ProjectHeightmap.WORLD_SURFACE_WG; + private GenerationStep generationStep = GenerationStep.SURFACE_STRUCTURES; + private TerrainAdaptation terrainAdaptation = TerrainAdaptation.NONE; + private boolean expansionHack; + private int maxDistanceVertical = 4064; + private int spacing = 32; + private int separation = 8; + private int salt; + private float frequency = 1.0F; + private SpreadType spreadType = SpreadType.LINEAR; + + private Builder() { + } + + public Builder biomes(List values) { + biomes.clear(); + biomes.addAll(Objects.requireNonNull(values)); + return this; + } + + public Builder startHeight(int value) { + startHeight = value; + return this; + } + + public Builder projectHeightmap(ProjectHeightmap value) { + projectHeightmap = Objects.requireNonNull(value); + return this; + } + + public Builder generationStep(GenerationStep value) { + generationStep = Objects.requireNonNull(value); + return this; + } + + public Builder terrainAdaptation(TerrainAdaptation value) { + terrainAdaptation = Objects.requireNonNull(value); + return this; + } + + public Builder expansionHack(boolean value) { + expansionHack = value; + return this; + } + + public Builder maxDistanceVertical(int value) { + maxDistanceVertical = value; + return this; + } + + public Builder randomSpread(int spacingValue, int separationValue, int saltValue) { + spacing = spacingValue; + separation = separationValue; + salt = saltValue; + return this; + } + + public Builder frequency(float value) { + frequency = value; + return this; + } + + public Builder spreadType(SpreadType value) { + spreadType = Objects.requireNonNull(value); + return this; + } + + public VanillaJigsawExportSettings build() { + return new VanillaJigsawExportSettings(this); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportSource.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportSource.java new file mode 100644 index 000000000..e063d0faa --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportSource.java @@ -0,0 +1,69 @@ +package art.arcane.iris.core.structure.export; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.structure.StructureGraphResolver; +import art.arcane.iris.engine.object.IrisStructure; + +import java.util.Objects; + +public final class VanillaJigsawExportSource { + private final String structureKey; + private final IrisData data; + private final IrisStructure structure; + private final StructureGraphResolver resolver; + + private VanillaJigsawExportSource( + String structureKey, + IrisData data, + IrisStructure structure, + StructureGraphResolver resolver + ) { + this.structureKey = requireKey(structureKey); + this.data = data; + this.structure = structure; + this.resolver = resolver; + } + + public static VanillaJigsawExportSource forData(IrisData data, String structureKey) { + return new VanillaJigsawExportSource( + structureKey, + Objects.requireNonNull(data), + null, + StructureGraphResolver.forData(data)); + } + + public static VanillaJigsawExportSource forStructure( + String structureKey, + IrisStructure structure, + StructureGraphResolver resolver + ) { + return new VanillaJigsawExportSource( + structureKey, + null, + Objects.requireNonNull(structure), + Objects.requireNonNull(resolver)); + } + + public String structureKey() { + return structureKey; + } + + public IrisStructure loadStructure() { + if (structure != null) { + return structure; + } + return data.load(IrisStructure.class, structureKey, false); + } + + public StructureGraphResolver resolver() { + return resolver; + } + + private static String requireKey(String value) { + String key = Objects.requireNonNull(value).trim(); + if (key.isEmpty()) { + throw new IllegalArgumentException("Structure key must not be blank"); + } + return key; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportValidation.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportValidation.java new file mode 100644 index 000000000..6144e593c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaJigsawExportValidation.java @@ -0,0 +1,22 @@ +package art.arcane.iris.core.structure.export; + +import java.util.List; + +public record VanillaJigsawExportValidation( + List plannedResources, + List diagnostics +) { + public VanillaJigsawExportValidation { + plannedResources = List.copyOf(plannedResources); + diagnostics = List.copyOf(diagnostics); + } + + public boolean isExportable() { + for (VanillaJigsawExportDiagnostic diagnostic : diagnostics) { + if (diagnostic.isBlocking()) { + return false; + } + } + return true; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaResourceIdentifier.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaResourceIdentifier.java new file mode 100644 index 000000000..a457c14de --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaResourceIdentifier.java @@ -0,0 +1,56 @@ +package art.arcane.iris.core.structure.export; + +import java.util.regex.Pattern; + +final class VanillaResourceIdentifier { + private static final Pattern NAMESPACE = Pattern.compile("[a-z0-9_.-]+"); + private static final Pattern PATH = Pattern.compile("[a-z0-9/._-]+"); + + private VanillaResourceIdentifier() { + } + + static boolean validNamespace(String value) { + return value != null && NAMESPACE.matcher(value).matches(); + } + + static boolean validPath(String value) { + if (value == null + || !PATH.matcher(value).matches() + || value.startsWith("/") + || value.endsWith("/") + || value.contains("//")) { + return false; + } + for (String segment : value.split("/")) { + if (segment.equals(".") || segment.equals("..")) { + return false; + } + } + return true; + } + + static boolean validIdentifier(String value) { + if (value == null) { + return false; + } + int separator = value.indexOf(':'); + if (separator <= 0 || separator != value.lastIndexOf(':')) { + return false; + } + return validNamespace(value.substring(0, separator)) && validPath(value.substring(separator + 1)); + } + + static String normalizeConnectorIdentifier(String value) { + if (value == null || value.isBlank()) { + return "minecraft:empty"; + } + String normalized = value.trim(); + if (normalized.indexOf(':') < 0) { + normalized = "minecraft:" + normalized; + } + if (!validIdentifier(normalized)) { + throw new IllegalArgumentException("Invalid resource identifier '" + value + "'"); + } + return normalized; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/structure/export/VanillaStructureTemplateEncoder.java b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaStructureTemplateEncoder.java new file mode 100644 index 000000000..2de7db6c3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/export/VanillaStructureTemplateEncoder.java @@ -0,0 +1,176 @@ +package art.arcane.iris.core.structure.export; + +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.nbt.io.NBTUtil; +import art.arcane.volmlib.util.nbt.io.NamedTag; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.tag.IntTag; +import art.arcane.volmlib.util.nbt.tag.ListTag; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +final class VanillaStructureTemplateEncoder { + private static final int DATA_VERSION_26_2 = 4903; + private static final Comparator BLOCK_ORDER = Comparator + .comparingInt((BlockEntry entry) -> entry.position().y()) + .thenComparingInt(entry -> entry.position().x()) + .thenComparingInt(entry -> entry.position().z()); + + byte[] encode( + IrisObject object, + IrisJigsawPiece piece, + Function poolIdentifier + ) throws IOException { + Map blocks = objectBlocks(object); + addConnectors(blocks, piece, poolIdentifier); + List orderedBlocks = new ArrayList<>(blocks.values()); + orderedBlocks.sort(BLOCK_ORDER); + + Map paletteIndexes = new LinkedHashMap<>(); + List paletteStates = new ArrayList<>(); + for (BlockEntry block : orderedBlocks) { + String key = block.state().canonical(); + if (!paletteIndexes.containsKey(key)) { + paletteIndexes.put(key, paletteStates.size()); + paletteStates.add(block.state()); + } + } + + CompoundTag root = new CompoundTag(); + root.put("size", intList(object.getW(), object.getH(), object.getD())); + root.put("palette", palette(paletteStates)); + root.put("blocks", blocks(orderedBlocks, paletteIndexes)); + root.put("entities", new ListTag<>(CompoundTag.class)); + root.putInt("DataVersion", DATA_VERSION_26_2); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + NBTUtil.write(new NamedTag("", root), output, true); + return output.toByteArray(); + } + + private Map objectBlocks(IrisObject object) { + Map blocks = new LinkedHashMap<>(); + Vector3i center = object.getCenter(); + for (Map.Entry entry : object.getBlocks()) { + IrisBlockVector signed = entry.getKey(); + BlockPosition position = new BlockPosition( + signed.getBlockX() + center.getX(), + signed.getBlockY() + center.getY(), + signed.getBlockZ() + center.getZ()); + VanillaBlockState state = VanillaBlockState.parse(entry.getValue().key()); + blocks.put(position, new BlockEntry(position, state, null)); + } + return blocks; + } + + private void addConnectors( + Map blocks, + IrisJigsawPiece piece, + Function poolIdentifier + ) { + for (IrisJigsawConnector connector : piece.getConnectors()) { + IrisPosition sourcePosition = connector.getPosition(); + BlockPosition position = new BlockPosition( + sourcePosition.getX(), sourcePosition.getY(), sourcePosition.getZ()); + String orientation = orientation(connector.getDirection(), connector.getTop()); + VanillaBlockState state = VanillaBlockState.parse( + "minecraft:jigsaw[orientation=" + orientation + "]"); + CompoundTag nbt = connectorNbt(connector, poolIdentifier.apply(connector.getPool())); + blocks.put(position, new BlockEntry(position, state, nbt)); + } + } + + private CompoundTag connectorNbt(IrisJigsawConnector connector, String poolIdentifier) { + CompoundTag nbt = new CompoundTag(); + nbt.putString("id", "minecraft:jigsaw"); + nbt.putString("name", VanillaResourceIdentifier.normalizeConnectorIdentifier(connector.getName())); + nbt.putString("target", VanillaResourceIdentifier.normalizeConnectorIdentifier(connector.getTargetName())); + nbt.putString("pool", poolIdentifier); + nbt.putString("final_state", VanillaBlockState.parse(connector.getFinalState()).canonical()); + nbt.putString("joint", connector.getJoint().name().toLowerCase(Locale.ROOT)); + nbt.putInt("selection_priority", connector.getSelectionPriority()); + nbt.putInt("placement_priority", connector.getPlacementPriority()); + return nbt; + } + + static String orientation(IrisDirection front, IrisDirection top) { + if (front == IrisDirection.UP_POSITIVE_Y || front == IrisDirection.DOWN_NEGATIVE_Y) { + if (top == IrisDirection.NORTH_NEGATIVE_Z + || top == IrisDirection.SOUTH_POSITIVE_Z + || top == IrisDirection.EAST_POSITIVE_X + || top == IrisDirection.WEST_NEGATIVE_X) { + return directionName(front) + "_" + directionName(top); + } + throw new IllegalArgumentException("Vertical jigsaw fronts require a horizontal top direction"); + } + if (top != IrisDirection.UP_POSITIVE_Y) { + throw new IllegalArgumentException("Horizontal jigsaw fronts require an upward top direction"); + } + return directionName(front) + "_up"; + } + + private static String directionName(IrisDirection direction) { + return switch (direction) { + case UP_POSITIVE_Y -> "up"; + case DOWN_NEGATIVE_Y -> "down"; + case NORTH_NEGATIVE_Z -> "north"; + case SOUTH_POSITIVE_Z -> "south"; + case EAST_POSITIVE_X -> "east"; + case WEST_NEGATIVE_X -> "west"; + }; + } + + private ListTag palette(List states) { + ListTag palette = new ListTag<>(CompoundTag.class); + for (VanillaBlockState state : states) { + palette.add(state.toNbt()); + } + return palette; + } + + private ListTag blocks( + List entries, + Map paletteIndexes + ) { + ListTag blocks = new ListTag<>(CompoundTag.class); + for (BlockEntry entry : entries) { + CompoundTag block = new CompoundTag(); + block.put("pos", intList(entry.position().x(), entry.position().y(), entry.position().z())); + block.putInt("state", paletteIndexes.get(entry.state().canonical())); + if (entry.nbt() != null) { + block.put("nbt", entry.nbt()); + } + blocks.add(block); + } + return blocks; + } + + private ListTag intList(int... values) { + ListTag list = new ListTag<>(IntTag.class); + for (int value : values) { + list.add(new IntTag(value)); + } + return list; + } + + private record BlockPosition(int x, int y, int z) { + } + + private record BlockEntry(BlockPosition position, VanillaBlockState state, CompoundTag nbt) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java b/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java new file mode 100644 index 000000000..577762ee8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java @@ -0,0 +1,187 @@ +package art.arcane.iris.core.tools; + +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.volmlib.util.data.Varint; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.nbt.io.NBTUtil; +import art.arcane.volmlib.util.nbt.io.NamedTag; +import art.arcane.volmlib.util.nbt.tag.ByteArrayTag; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.tag.IntTag; +import art.arcane.volmlib.util.nbt.tag.ShortTag; +import art.arcane.volmlib.util.nbt.tag.Tag; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import org.apache.commons.io.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.block.data.BlockData; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public class IrisConverter { + public static void convertSchematics(VolmitSender sender) { + File folder = IrisPlatforms.get().dataFolder("convert"); + + FilenameFilter filter = (dir, name) -> name.endsWith(".schem"); + File[] fileList = folder.listFiles(filter); + if (fileList == null) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_CONVERTER_NO_SCHEMATIC_FILES_CONVERT_FOUND, MessageArgument.untrusted("path", String.valueOf(folder.getAbsolutePath())))); + return; + } + + AtomicInteger counter = new AtomicInteger(0); + var stopwatch = PrecisionStopwatch.start(); + ExecutorService executorService = Executors.newFixedThreadPool(1); + executorService.submit(() -> { + for (File schem : fileList) { + try { + PrecisionStopwatch p = PrecisionStopwatch.start(); + IrisObject object; + boolean largeObject = false; + NamedTag tag; + try { + tag = NBTUtil.read(schem); + } catch (IOException e) { + IrisLogging.info(C.RED + "Failed to read: " + schem.getName()); + throw new RuntimeException(e); + } + CompoundTag compound = (CompoundTag) tag.getTag(); + int version = resolveVersion(compound); + if (!(version == 2 || version == 3)) + throw new RuntimeException(C.RED + "Unsupported schematic version: " + version); + + compound = version == 3 ? (CompoundTag) compound.get("Schematic") : compound; + int objW = ((ShortTag) compound.get("Width")).getValue(); + int objH = ((ShortTag) compound.get("Height")).getValue(); + int objD = ((ShortTag) compound.get("Length")).getValue(); + int i = -1; + int mv = objW * objH * objD; + AtomicInteger v = new AtomicInteger(0); + boolean reportProgress = mv > 2_000_000 && sender.isPlayer(); + // try/finally over the whole decode: a throw must never leak the + // self-rescheduling progress task or the HUD lane. + try { + if (mv > 2_000_000) { + largeObject = true; + IrisLogging.info(C.GRAY + "Converting.. " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); + IrisLogging.info(C.GRAY + "- It may take a while"); + if (reportProgress) { + i = J.ar(() -> { + double conversionProgress = (double) v.get() / mv; + sender.sendProgress(conversionProgress, IrisLanguage.text(RuntimeUiMessages.CONVERTING)); + BukkitPlatform.showProgressLane(sender.player(), "iris:job", IrisLanguage.text(RuntimeUiMessages.CONVERTING) + " " + Form.pc(conversionProgress, 0), conversionProgress, 4000L); + }, 0); + } + } + + compound = version == 3 ? (CompoundTag) compound.get("Blocks") : compound; + CompoundTag paletteTag = (CompoundTag) compound.get("Palette"); + Map blockmap = new HashMap<>(paletteTag.size(), 0.9f); + for (Map.Entry> entry : paletteTag.getValue().entrySet()) { + String blockName = entry.getKey(); + BlockData bd = Bukkit.createBlockData(blockName); + Tag blockTag = entry.getValue(); + int blockId = ((IntTag) blockTag).getValue(); + blockmap.put(blockId, bd); + } + + boolean isBytes = version == 3 ? compound.getByteArrayTag("Data").length() < 128 : ((IntTag) compound.get("PaletteMax")).getValue() < 128; + ByteArrayTag byteArray = version == 3 ? (ByteArrayTag) compound.get("Data") : (ByteArrayTag) compound.get("BlockData"); + byte[] originalBlockArray = byteArray.getValue(); + var din = new DataInputStream(new ByteArrayInputStream(originalBlockArray)); + object = new IrisObject(objW, objH, objD); + for (int h = 0; h < objH; h++) { + for (int d = 0; d < objD; d++) { + for (int w = 0; w < objW; w++) { + int blockIndex = isBytes ? din.read() & 0xFF : Varint.readUnsignedVarInt(din); + BlockData bd = blockmap.get(blockIndex); + if (!bd.getMaterial().isAir()) { + object.setUnsigned(w, h, d, art.arcane.iris.platform.bukkit.BukkitBlockState.of(bd)); + } + v.getAndAdd(1); + } + } + } + + } finally { + if (i != -1) J.car(i); + if (reportProgress) { + sender.sendAction(" "); + BukkitPlatform.hudLanes().hide(sender.player(), "iris:job"); + } + } + try { + object.shrinkwrap(); + object.write(new File(folder, schem.getName().replace(".schem", ".iob"))); + counter.incrementAndGet(); + if (sender.isPlayer()) { + if (largeObject) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_CONVERTER_CONVERTED, MessageArgument.untrusted("name", String.valueOf(schem.getName())), MessageArgument.untrusted("value", String.valueOf(schem.getName().replace(".schem", ".iob"))), MessageArgument.untrusted("value2", String.valueOf(Form.duration(p.getMillis()))))); + } else { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_CONVERTER_CONVERTED_2, MessageArgument.untrusted("name", String.valueOf(schem.getName())), MessageArgument.untrusted("value", String.valueOf(schem.getName().replace(".schem", ".iob"))))); + } + } + if (largeObject) { + IrisLogging.info(C.GRAY + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob") + " in " + Form.duration(p.getMillis())); + } else { + IrisLogging.info(C.GRAY + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); + } + FileUtils.delete(schem); + } catch (IOException e) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_CONVERTER_FAILED_SAVE, MessageArgument.untrusted("name", String.valueOf(schem.getName())))); + throw new IOException(e); + } + + + } catch (Exception e) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_CONVERTER_FAILED_CONVERT, MessageArgument.untrusted("name", String.valueOf(schem.getName())))); + e.printStackTrace(); + } + } + stopwatch.end(); + if (counter.get() != 0) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_CONVERTER_CONVERTED_3, MessageArgument.untrusted("get", String.valueOf(counter.get())), MessageArgument.untrusted("value", String.valueOf(Form.duration(stopwatch.getMillis()))))); + } + if (counter.get() < fileList.length) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_CONVERTER_SOME_SCHEMATICS_FAILED_CONVERT_CHECK_CONSOLE_DETAILS)); + } + }); + // Single-use pool: retire the worker once the queued conversion finishes instead of + // leaking a non-daemon thread per invocation. + executorService.shutdown(); + } + + private static int resolveVersion(CompoundTag compound) throws Exception { + try { + + IntTag root = compound.getIntTag("Version"); + if (root != null) { + return root.getValue(); + } + CompoundTag schematic = (CompoundTag) compound.get("Schematic"); + return schematic.getIntTag("Version").getValue(); + } catch (NullPointerException e) { + throw new Exception("Cannot resolve schematic version", e); + } + } +} + diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java new file mode 100644 index 000000000..17a7cfc5f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -0,0 +1,854 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.tools; + +import art.arcane.iris.core.runtime.TransientWorldCleanupSupport; +import art.arcane.iris.core.runtime.WorldRuntimeControlService; +import com.google.common.util.concurrent.AtomicDouble; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.link.MultiverseCoreLink; +import art.arcane.iris.core.IrisRuntimeSchedulerMode; +import art.arcane.iris.core.IrisStartupValidation; +import art.arcane.iris.core.DatapackInstallResult; +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.IrisWorlds; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.WorldCreatorCompat; +import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration; +import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; +import art.arcane.iris.core.lifecycle.WorldLifecycleCaller; +import art.arcane.iris.core.lifecycle.WorldLifecycleRequest; +import art.arcane.iris.core.lifecycle.WorldLifecycleService; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.pregenerator.PregenTask; +import art.arcane.iris.core.pack.AtomicDirectoryPublisher; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.core.runtime.WorldDeletionQueue; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.volmlib.util.exceptions.IrisException; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.FoliaScheduler; +import lombok.Data; +import lombok.experimental.Accessors; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.entity.Player; + +import java.io.IOException; +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +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.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.IntSupplier; + +import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML; + +/** + * Makes it a lot easier to setup an engine, world, studio or whatever + */ +@Data +@Accessors(fluent = true, chain = true) +public class IrisCreator { + private static final long WORLD_CREATE_TIMEOUT_SECONDS = 120L; + private static final long WORLD_ENTRY_TELEPORT_TIMEOUT_SECONDS = 60L; + private static final long ROLLBACK_PHASE_TIMEOUT_SECONDS = 120L; + + /** + * Specify an area to pregenerate during creation + */ + private PregenTask pregen; + /** + * Specify a sender to get updates & progress info + tp when world is created. + */ + private VolmitSender sender; + /** + * The seed to use for this generator + */ + private long seed = 1337; + /** + * The dimension to use. This can be any online dimension, or a dimension in the + * packs folder + */ + private String dimension = IrisSettings.get().getGenerator().getDefaultWorldType(); + /** + * The name of this world. + */ + private String name = "irisworld"; + /** + * Studio mode makes the engine hotloadable and uses the dimension in + * your Iris/packs folder instead of copying the dimension files into + * the world itself. Studio worlds are deleted when they are unloaded. + */ + private boolean studio = false; + /** + * Benchmark mode + */ + private boolean benchmark = false; + private BiConsumer studioProgressConsumer; + private BiConsumer studioTimingConsumer; + private DatapackPreparation datapackPreparation = DatapackPreparation.INSTALL_IF_CHANGED; + + public static boolean removeFromBukkitYml(NamespacedKey worldKey) throws IOException { + return BukkitWorldConfiguration.remove( + BUKKIT_YML, + IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName()) + ); + } + + public static int removeTransientStudioWorldsFromBukkitYml() throws IOException { + return BukkitWorldConfiguration.removeMatching( + BUKKIT_YML, + TransientWorldCleanupSupport::isTransientStudioWorldName); + } + public static boolean worldLoaded(){ + return true; + } + + /** + * Create the IrisAccess (contains the world) + * + * @return the IrisAccess + * @throws IrisException shit happens + */ + + public World create() throws IrisException { + if (Bukkit.isPrimaryThread()) { + throw new IrisException("You cannot invoke create() on the main thread."); + } + if (sender == null) { + sender = BukkitPlatform.console(); + } + NamespacedKey worldKey; + try { + worldKey = IrisWorldStorage.managedKeyFromName(name); + } catch (RuntimeException e) { + throw new IrisException(e.getMessage(), e); + } + name = IrisWorldStorage.logicalName(worldKey); + WorldCreationProgressReporter creationReporter = !studio && !benchmark + ? WorldCreationProgressReporter.start(sender, name) + : null; + + LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get(); + LifecycleOperationCoordinator.Lease worldLease = null; + try { + reportStudioProgress(0.02D, "resolve_dimension"); + reportCreationProgress(creationReporter, 0.02D, "resolve_dimension"); + IrisDimension resolvedDimension = IrisToolbelt.getDimension(dimension()); + if (resolvedDimension == null) { + throw new IrisException("Dimension cannot be found for id " + dimension()); + } + reportCreationProgress(creationReporter, 0.06D, "validate_pack"); + IrisStartupValidation.requireWorldCreationReady(); + PackValidationRegistry.requireLoadable( + resolvedDimension.getLoader().getDataFolder().getName()); + worldLease = coordinator.acquire( + LifecycleOperationCoordinator.Domain.WORLD_MUTATION, + LifecycleOperationCoordinator.OperationKind.WORLD_CREATE, + worldKey.toString()); + World world = createReserved(worldKey, resolvedDimension, creationReporter); + reportCreationProgress(creationReporter, 1.0D, "complete"); + if (creationReporter != null) { + creationReporter.succeed(); + } + return world; + } catch (LifecycleOperationCoordinator.BusyException e) { + if (creationReporter != null) { + creationReporter.fail(); + } + throw new IrisException(e.getMessage(), e); + } catch (Throwable e) { + if (creationReporter != null) { + creationReporter.fail(); + } + if (e instanceof IrisException irisException) { + throw irisException; + } + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (e instanceof Error error) { + throw error; + } + throw new IrisException("Failed to create world \"" + name + "\".", e); + } finally { + if (worldLease != null) { + worldLease.close(); + } + } + } + + private World createReserved( + NamespacedKey worldKey, + IrisDimension resolvedDimension, + WorldCreationProgressReporter creationReporter + ) throws IrisException { + File dimensionRoot; + File storageRoot; + try { + if (!studio && !benchmark) { + dimensionRoot = WorldCreatorCompat.persistentDimensionRoot(worldKey); + storageRoot = WorldCreatorCompat.persistentLevelRoot(worldKey); + } else { + dimensionRoot = IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey); + storageRoot = dimensionRoot; + } + } catch (RuntimeException e) { + throw new IrisException(e.getMessage(), e); + } + if (Files.exists(storageRoot.toPath()) || WorldIdentity.resolve(worldKey).isPresent()) { + throw new IrisException("World \"" + name + "\" already exists or is loaded."); + } + World world = null; + boolean bukkitRegistered = false; + PlatformChunkGenerator stagedGenerator = null; + try { + reportStudioProgress(0.08D, "resolve_dimension"); + reportStudioProgress(0.16D, "prepare_world_pack"); + reportCreationProgress(creationReporter, 0.10D, "install_datapacks"); + DatapackInstallResult datapackResult = prepareDatapacks(resolvedDimension); + if (!datapackResult.succeeded()) { + throw new IrisException("Failed to compile datapacks for dimension \"" + dimension() + "\"."); + } + if (datapackResult.restartRequired() || !ServerConfigurator.verifyDataPackInstalled(resolvedDimension)) { + ServerConfigurator.restart(); + throw new IrisException("The dimension types for pack \"" + dimension() + "\" are not loaded yet. " + + "Iris queued a restart; run the command again after the server returns."); + } + + IrisDimension installedDimension = resolvedDimension; + if (!studio() || benchmark) { + reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack"); + installedDimension = IrisServices.get(StudioSVC.class) + .installIntoWorld(sender, resolvedDimension, dimensionRoot); + if (installedDimension == null) { + throw new IrisException("Failed to install dimension pack for " + dimension()); + } + dimension = installedDimension.getLoadKey(); + } + if (studio()) { + IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen()); + IrisLogging.debug("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT) + + ", regionizedRuntime=" + FoliaScheduler.isRegionizedRuntime(Bukkit.getServer())); + } + + reportStudioProgress(0.28D, "install_datapacks"); + reportCreationProgress(creationReporter, 0.36D, "prepare_generator"); + AtomicDouble pp = new AtomicDouble(0); + AtomicBoolean done = new AtomicBoolean(false); + long generatorPrepareStart = System.nanoTime(); + WorldCreator wc = new IrisWorldCreator() + .dimension(installedDimension) + .name(name) + .seed(seed) + .studio(studio) + .persistent(!studio && !benchmark) + .create(); + reportStudioTiming("prepare_studio_generator", generatorPrepareStart); + reportStudioProgress(0.40D, "install_datapacks"); + + PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator(); + if (access == null) { + throw new IrisException("Access is null. Something bad happened."); + } + stagedGenerator = access; + AtomicInteger createProgressTask = startCreateProgressReporter(access, done, creationReporter); + + reportStudioProgress(0.46D, "create_world"); + reportCreationProgress(creationReporter, 0.44D, "create_world"); + long nmsStartNanos = System.nanoTime(); + try { + WorldLifecycleCaller callerKind = benchmark ? WorldLifecycleCaller.BENCHMARK : studio() ? WorldLifecycleCaller.STUDIO : WorldLifecycleCaller.CREATE; + WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(wc, studio(), benchmark, callerKind); + world = J.sfut(() -> INMS.get().createWorldAsync(wc, request)) + .thenCompose(Function.identity()) + .get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Throwable e) { + done.set(true); + cancelRepeatingTask(createProgressTask); + if (e instanceof TimeoutException) { + ServerConfigurator.restart("World creation timed out for \"" + name + "\"."); + } + if (J.isFolia() && containsCreateWorldUnsupportedOperation(e)) { + throw new IrisException("Runtime world creation is blocked and the selected world lifecycle backend could not create the world.", e); + } + if (containsMissingDimensionTypes(e)) { + ServerConfigurator.restart(); + throw new IrisException("The dimension types for pack \"" + dimension() + "\" are not loaded on this server yet. " + + "Iris queued a restart; run the command again after the server returns.", e); + } + throw new IrisException("Failed to create world with backend family " + WorldLifecycleService.get().capabilities().serverFamily().id() + "!", e); + } finally { + reportStudioTiming("create_bukkit_world", nmsStartNanos); + } + + done.set(true); + cancelRepeatingTask(createProgressTask); + reportStudioProgress(0.86D, "create_world"); + reportCreationProgress(creationReporter, 0.84D, "register_world"); + + if (!studio && !benchmark) { + // bukkit.yml and Multiverse must agree on the startup name, or Multiverse re-imports the + // world on the next boot and collides with its own config key. + String registrationName = IrisWorldStorage.configuredWorldName( + worldKey, + IrisWorldStorage.levelRoot().getName() + ); + BukkitWorldConfiguration.register( + BUKKIT_YML, + registrationName, + dimension, + seed + ); + bukkitRegistered = true; + World createdWorld = world; + CompletableFuture multiverseRegistration = J.sfut( + () -> IrisServices.get(MultiverseCoreLink.class) + .updateWorld(createdWorld, registrationName, dimension) + ); + if (multiverseRegistration == null) { + throw new IrisException("Failed to schedule Multiverse registration for world \"" + name + "\"."); + } + try { + multiverseRegistration.get(30L, TimeUnit.SECONDS); + } catch (TimeoutException e) { + ServerConfigurator.restart("Multiverse registration timed out for \"" + name + "\"."); + throw e; + } + } + reportCreationProgress(creationReporter, 0.92D, "teleport_player"); + awaitSenderTeleport(world); + + if (pregen != null) { + CompletableFuture ff = new CompletableFuture<>(); + IrisToolbelt.pregenerate(pregen, access) + .onProgress(pp::set) + .whenDone(() -> ff.complete(true)); + + AtomicBoolean dx = new AtomicBoolean(false); + boolean pregenHud = creationReporter == null && sender.isPlayer(); + AtomicInteger pregenProgressTask = startPregenProgressReporter( + pp, + dx, + pregenHud, + creationReporter + ); + try { + ff.get(); + dx.set(true); + cancelRepeatingTask(pregenProgressTask); + hidePregenLane(pregenHud); + } catch (Throwable e) { + dx.set(true); + cancelRepeatingTask(pregenProgressTask); + hidePregenLane(pregenHud); + IrisLogging.reportError(e); + } + } + reportCreationProgress(creationReporter, 0.99D, "finalize"); + return world; + } catch (Throwable failure) { + rollbackWorldCreation(worldKey, world, stagedGenerator, storageRoot, bukkitRegistered, failure); + if (failure instanceof IrisException irisException) { + throw irisException; + } + throw new IrisException("Failed to create world \"" + name + "\".", failure); + } + } + + static Player createTeleportTarget(VolmitSender sender, boolean studio, boolean benchmark) { + if (studio || benchmark || sender == null || !sender.isPlayer()) { + return null; + } + return sender.player(); + } + + static CompletableFuture teleportSenderToCreatedWorld( + Player player, + World world, + WorldRuntimeControlService runtimeControl + ) { + Player requiredPlayer = Objects.requireNonNull(player, "player"); + World requiredWorld = Objects.requireNonNull(world, "world"); + WorldRuntimeControlService requiredRuntime = Objects.requireNonNull(runtimeControl, "runtimeControl"); + Location entryAnchor = requiredRuntime.resolveEntryAnchor(requiredWorld); + if (entryAnchor == null) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Unable to resolve the entry anchor for world \"" + requiredWorld.getName() + "\".")); + } + + int chunkX = entryAnchor.getBlockX() >> 4; + int chunkZ = entryAnchor.getBlockZ() >> 4; + return requiredRuntime.requestChunkAsync(requiredWorld, chunkX, chunkZ, true) + .thenCompose(chunk -> requiredRuntime.resolveSafeEntry(requiredWorld, entryAnchor)) + .thenCompose(safeEntry -> { + if (safeEntry == null) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Unable to resolve a safe entry for world \"" + requiredWorld.getName() + "\".")); + } + return requiredRuntime.teleport(requiredPlayer, safeEntry); + }); + } + + private void awaitSenderTeleport(World world) { + Player player = createTeleportTarget(sender, studio, benchmark); + if (player == null) { + return; + } + + CompletableFuture teleportFuture; + try { + teleportFuture = teleportSenderToCreatedWorld(player, world, WorldRuntimeControlService.get()); + } catch (Throwable e) { + reportSenderTeleportFailure(player, world, e); + return; + } + + Throwable failure = awaitTeleportFailure( + teleportFuture, + player.getName(), + WORLD_ENTRY_TELEPORT_TIMEOUT_SECONDS, + TimeUnit.SECONDS + ); + if (failure != null) { + reportSenderTeleportFailure(player, world, failure); + } + } + + static Throwable awaitTeleportFailure( + CompletableFuture teleportFuture, + String playerName, + long timeout, + TimeUnit unit + ) { + CompletableFuture requiredFuture = Objects.requireNonNull(teleportFuture, "teleportFuture"); + String requiredPlayerName = Objects.requireNonNull(playerName, "playerName"); + TimeUnit requiredUnit = Objects.requireNonNull(unit, "unit"); + try { + Boolean teleported = requiredFuture.get(timeout, requiredUnit); + return Boolean.TRUE.equals(teleported) + ? null + : new IllegalStateException( + "The runtime teleport operation returned false for player \"" + requiredPlayerName + "\"."); + } catch (TimeoutException e) { + requiredFuture.cancel(false); + return e; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return e; + } catch (ExecutionException e) { + return e.getCause() == null ? e : e.getCause(); + } catch (Throwable e) { + return e; + } + } + + private void reportSenderTeleportFailure(Player player, World world, Throwable throwable) { + IrisLogging.reportError("World \"" + world.getName() + + "\" was created, but automatic teleport failed for player \"" + player.getName() + "\".", throwable); + J.runEntity(player, () -> new VolmitSender(player).sendMessage(IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_TELEPORT_FAILED, + MessageArgument.untrusted("world", IrisWorldStorage.logicalName(world)) + ))); + } + + private void reportStudioProgress(double progress, String stage) { + BiConsumer consumer = studioProgressConsumer; + if (consumer == null) { + return; + } + + double clamped = Math.max(0D, Math.min(1D, progress)); + try { + consumer.accept(clamped, stage); + } catch (Throwable e) { + IrisLogging.reportError("Studio progress consumer failed for world \"" + name() + "\".", e); + } + } + + private void reportCreationProgress( + WorldCreationProgressReporter reporter, + double progress, + String stage + ) { + if (reporter != null) { + reporter.update(progress, stage); + } + } + + private void reportStudioTiming(String phase, long startedAtNanos) { + BiConsumer consumer = studioTimingConsumer; + if (consumer == null) { + return; + } + + long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos); + try { + consumer.accept(phase, duration); + } catch (Throwable e) { + IrisLogging.reportError("Studio timing consumer failed for world \"" + name() + "\".", e); + } + } + + private DatapackInstallResult prepareDatapacks(IrisDimension resolvedDimension) { + DatapackPreparation preparation = Objects.requireNonNull( + datapackPreparation, + "Datapack preparation mode"); + boolean runtimeReady = preparation == DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY + && ServerConfigurator.isLoadedDatapackRuntimeReady(resolvedDimension); + if (!preparation.requiresInstall(runtimeReady)) { + long reuseStart = System.nanoTime(); + reportStudioTiming("datapack_reuse_loaded_runtime", reuseStart); + return DatapackInstallResult.unchangedResult(); + } + return ServerConfigurator.installDataPacksIfChanged(true, studioTimingConsumer); + } + + private AtomicInteger startCreateProgressReporter( + PlatformChunkGenerator access, + AtomicBoolean done, + WorldCreationProgressReporter creationReporter + ) { + AtomicInteger taskId = new AtomicInteger(-1); + if (benchmark) { + return taskId; + } + + IntSupplier generatedSupplier = () -> { + if (access.getEngine() == null) { + return 0; + } + return access.getEngine().getGenerated(); + }; + access.getSpawnChunks().whenComplete((required, throwable) -> { + if (throwable != null) { + IrisLogging.reportError("Failed to resolve spawn chunk target for world \"" + name() + "\".", throwable); + return; + } + + if (done.get() || required == null || required <= 0) { + return; + } + + int interval = studioProgressConsumer != null || creationReporter != null && sender.isPlayer() ? 1 : 20; + taskId.set(J.ar(() -> { + if (done.get()) { + cancelRepeatingTask(taskId); + return; + } + + int generated = generatedSupplier.getAsInt(); + if (generated >= required) { + cancelRepeatingTask(taskId); + return; + } + + double progress = (double) generated / required; + if (studioProgressConsumer != null) { + reportStudioProgress(0.40D + (0.42D * progress), "create_world"); + } + + if (creationReporter != null) { + creationReporter.update( + 0.44D + (0.38D * progress), + "create_world", + C.DARK_GRAY + " (" + Form.f(generated) + "/" + Form.f(required) + " chunks)" + ); + } + }, interval)); + }); + return taskId; + } + + private AtomicInteger startPregenProgressReporter( + AtomicDouble progress, + AtomicBoolean done, + boolean showLoaderHud, + WorldCreationProgressReporter creationReporter + ) { + AtomicInteger taskId = new AtomicInteger(-1); + int interval = sender.isPlayer() ? 1 : 20; + taskId.set(J.ar(() -> { + if (done.get()) { + cancelRepeatingTask(taskId); + return; + } + + double p = progress.get(); + int percent = (int) Math.round(p * 100.0D); + if (creationReporter != null) { + creationReporter.update(0.94D + (0.05D * p), "pregenerate"); + return; + } + if (showLoaderHud) { + if (IrisSettings.get().getGeneral().isProgressBossBar()) { + BukkitPlatform.hudLanes().show(sender.player(), "iris:pregen", IrisLanguage.text( + RuntimeProgressMessages.WORLD_PREGEN_ACTION, + MessageArgument.trusted("bar", ""), + MessageArgument.trusted("percent", percent) + ), p, BarColor.GREEN, BarStyle.SOLID, 4000L); + } + int barWidth = 44; + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth); + StringBuilder bar = new StringBuilder(barWidth * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int bi = 0; bi < barWidth; bi++) { + bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } + bar.append(C.DARK_GRAY).append("]"); + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.WORLD_PREGEN_ACTION, + MessageArgument.trusted("bar", bar.toString()), + MessageArgument.trusted("percent", percent) + )); + return; + } + + sender.sendMessage(IrisLanguage.text( + RuntimeProgressMessages.WORLD_PREGEN_CONSOLE, + MessageArgument.trusted("percent", percent) + )); + }, interval)); + return taskId; + } + + private void hidePregenLane(boolean shown) { + if (!shown) { + return; + } + BukkitPlatform.hudLanes().hide(sender.player(), "iris:pregen"); + sender.sendAction(" "); + } + + private void cancelRepeatingTask(AtomicInteger taskId) { + if (taskId == null) { + return; + } + + int id = taskId.getAndSet(-1); + if (id >= 0) { + J.car(id); + } + } + + private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) { + Throwable cursor = throwable; + while (cursor != null) { + if (cursor instanceof UnsupportedOperationException) { + for (StackTraceElement element : cursor.getStackTrace()) { + if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName()) + && "createWorld".equals(element.getMethodName())) { + return true; + } + } + } + cursor = cursor.getCause(); + } + return false; + } + + private static boolean containsMissingDimensionTypes(Throwable throwable) { + Throwable cursor = throwable; + while (cursor != null) { + if (cursor instanceof IllegalStateException && String.valueOf(cursor.getMessage()).contains("Missing dimension types")) { + return true; + } + cursor = cursor.getCause(); + } + return false; + } + + private void rollbackWorldCreation( + NamespacedKey worldKey, + World createdWorld, + PlatformChunkGenerator stagedGenerator, + File storageRoot, + boolean bukkitRegistered, + Throwable failure + ) { + World activeWorld = createdWorld == null ? WorldIdentity.resolve(worldKey).orElse(null) : createdWorld; + boolean safeToDelete = activeWorld != null || !containsTimeout(failure); + if (activeWorld == null && stagedGenerator != null) { + // The world never materialized, so no unload path will ever close the staged + // generator; without this it stays registered on WorldInitEvent and attaches a + // second engine when a same-name world is created later. + try { + stagedGenerator.closeAsync().get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Throwable rollbackFailure) { + failure.addSuppressed(unwrapFailure(rollbackFailure)); + } + } + if (activeWorld != null) { + IrisToolbelt.beginWorldMaintenance(activeWorld, "world-create-rollback", true); + try { + PlatformChunkGenerator generator = IrisToolbelt.access(activeWorld); + boolean evacuated = Boolean.TRUE.equals(IrisToolbelt.evacuateAsync(activeWorld) + .get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)); + if (!evacuated) { + safeToDelete = false; + failure.addSuppressed(new IllegalStateException( + "Rollback could not evacuate world \"" + name + "\".")); + } + boolean unloaded = safeToDelete && Boolean.TRUE.equals(WorldLifecycleService.get() + .unloadAsync(activeWorld, true) + .get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)); + if (!unloaded) { + safeToDelete = false; + failure.addSuppressed(new IllegalStateException("Rollback could not unload world \"" + name + "\".")); + } + if (safeToDelete && generator != null) { + generator.closeAsync().get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + // A staged generator the world never bound (e.g. the creation failed after a + // retry re-staged a fresh one) has no unload path either. + if (stagedGenerator != null && stagedGenerator != generator) { + stagedGenerator.closeAsync().get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + } catch (Throwable rollbackFailure) { + Throwable cause = unwrapFailure(rollbackFailure); + failure.addSuppressed(cause); + safeToDelete = false; + if (cause instanceof TimeoutException) { + ServerConfigurator.restart("World creation rollback timed out for \"" + name + "\"."); + } + } finally { + IrisToolbelt.endWorldMaintenance(activeWorld, "world-create-rollback", true); + } + } + + if (bukkitRegistered) { + try { + CompletableFuture multiverseRemoval = J.sfut( + () -> IrisServices.get(MultiverseCoreLink.class).removeFromConfig( + IrisWorldStorage.configuredWorldName( + worldKey, + IrisWorldStorage.levelRoot().getName() + ) + ) + ); + if (multiverseRemoval == null) { + throw new IllegalStateException("Failed to schedule Multiverse rollback for \"" + name + "\"."); + } + try { + multiverseRemoval.get(30L, TimeUnit.SECONDS); + } catch (TimeoutException e) { + ServerConfigurator.restart("Multiverse rollback timed out for \"" + name + "\"."); + throw e; + } + } catch (Throwable rollbackFailure) { + failure.addSuppressed(rollbackFailure); + } + try { + BukkitWorldConfiguration.remove( + BUKKIT_YML, + IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName()) + ); + } catch (Throwable rollbackFailure) { + failure.addSuppressed(rollbackFailure); + } + } + try { + IrisWorlds.get().remove(worldKey.toString()); + } catch (Throwable rollbackFailure) { + failure.addSuppressed(rollbackFailure); + } + if (!safeToDelete) { + queueRollbackDeletion(name, failure); + return; + } + try { + AtomicDirectoryPublisher.deleteTree(storageRoot.toPath()); + } catch (Throwable rollbackFailure) { + failure.addSuppressed(rollbackFailure); + queueRollbackDeletion(name, failure); + } + } + + private void queueRollbackDeletion(String worldName, Throwable failure) { + try { + IrisServices.get(WorldDeletionQueue.class).queueExactForStartupDeletion(List.of(worldName)); + } catch (Throwable queueFailure) { + failure.addSuppressed(queueFailure); + } + } + + private static boolean containsTimeout(Throwable throwable) { + Throwable cursor = throwable; + while (cursor != null) { + if (cursor instanceof TimeoutException) { + return true; + } + cursor = cursor.getCause(); + } + return false; + } + + private static Throwable unwrapFailure(Throwable throwable) { + Throwable cursor = throwable; + while (cursor instanceof CompletionException || cursor instanceof ExecutionException) { + if (cursor.getCause() == null) { + break; + } + cursor = cursor.getCause(); + } + return cursor; + } + + public enum DatapackPreparation { + INSTALL_IF_CHANGED(false), + REUSE_LOADED_RUNTIME_IF_READY(true); + + private final boolean reusesLoadedRuntime; + + DatapackPreparation(boolean reusesLoadedRuntime) { + this.reusesLoadedRuntime = reusesLoadedRuntime; + } + + boolean requiresInstall(boolean runtimeReady) { + return !reusesLoadedRuntime || !runtimeReady; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java b/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java new file mode 100644 index 000000000..ffda6e75d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java @@ -0,0 +1,198 @@ +package art.arcane.iris.core.tools; + + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.lifecycle.WorldLifecycleService; +import art.arcane.iris.core.pregenerator.PregenTask; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.exceptions.IrisException; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.IO; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import org.bukkit.World; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + + +public class IrisPackBenchmarking { + private static final ThreadLocal instance = new ThreadLocal<>(); + private final PrecisionStopwatch stopwatch = new PrecisionStopwatch(); + private final IrisDimension dimension; + private final int radius; + private final boolean gui; + + public IrisPackBenchmarking(IrisDimension dimension, int radius, boolean gui) { + this.dimension = dimension; + this.radius = radius; + this.gui = gui; + runBenchmark(); + } + + public static IrisPackBenchmarking getInstance() { + return instance.get(); + } + + private void runBenchmark() { + Thread.ofVirtual() + .name("PackBenchmarking") + .start(() -> { + IrisLogging.info("Setting up benchmark environment "); + IrisToolbelt.applyPregenPerformanceProfile(); + IO.delete(IrisWorldStorage.dimensionRoot("benchmark")); + createBenchmark(); + while (!IrisToolbelt.isIrisWorld(benchmarkWorld())) { + J.sleep(1000); + IrisLogging.debug("Iris PackBenchmark: Waiting..."); + } + IrisLogging.info("Starting Benchmark!"); + stopwatch.begin(); + startBenchmark(); + }); + + } + + public void finishedBenchmark(KList cps, double overallCps) { + try { + String time = Form.duration((long) stopwatch.getMilliseconds()); + World benchmarkWorld = benchmarkWorld(); + if (benchmarkWorld == null) { + throw new IllegalStateException("Benchmark world is not loaded."); + } + Engine engine = IrisToolbelt.access(benchmarkWorld).getEngine(); + IrisLogging.info("-----------------"); + IrisLogging.info("Results:"); + IrisLogging.info("- Total time: " + time); + IrisLogging.info("- Average CPS: " + overallCps); + IrisLogging.info(" - Median CPS: " + calculateMedian(cps)); + IrisLogging.info(" - Highest CPS: " + findHighest(cps)); + IrisLogging.info(" - Lowest CPS: " + findLowest(cps)); + IrisLogging.info("-----------------"); + IrisLogging.info("Creating a report.."); + File results = IrisPlatforms.get().dataFile("packbenchmarks", dimension.getName() + " " + LocalDateTime.now(Clock.systemDefaultZone()).toString().replace(':', '-') + ".txt"); + KMap metrics = engine.getMetrics().pull(); + try (FileWriter writer = new FileWriter(results)) { + writer.write("-----------------\n"); + writer.write("Results:\n"); + writer.write("Dimension: " + dimension.getName() + "\n"); + writer.write("- Date of Benchmark: " + LocalDateTime.now(Clock.systemDefaultZone()) + "\n"); + writer.write("\n"); + writer.write("Metrics"); + for (String m : metrics.k()) { + double i = metrics.get(m); + writer.write("- " + m + ": " + i); + } + writer.write("- " + metrics); + writer.write("Benchmark: " + LocalDateTime.now(Clock.systemDefaultZone()) + "\n"); + writer.write("- Total time: " + time + "\n"); + writer.write("- Average CPS: " + overallCps + "\n"); + writer.write(" - Median CPS: " + calculateMedian(cps) + "\n"); + writer.write(" - Highest CPS: " + findHighest(cps) + "\n"); + writer.write(" - Lowest CPS: " + findLowest(cps) + "\n"); + writer.write("-----------------\n"); + IrisLogging.info("Finished generating a report!"); + } catch (IOException e) { + IrisLogging.error("An error occurred writing to the file."); + e.printStackTrace(); + } + + J.sfut(() -> { + World world = benchmarkWorld(); + if (world == null) { + return null; + } + IrisToolbelt.evacuate(world); + return world; + }).thenCompose(world -> { + if (world == null) { + return CompletableFuture.completedFuture(false); + } + PlatformChunkGenerator generator = IrisToolbelt.access(world); + CompletableFuture closeFuture = generator == null + ? CompletableFuture.completedFuture(null) + : generator.closeAsync(); + return closeFuture.thenCompose(unused -> WorldLifecycleService.get().unloadAsync(world, true)); + }).whenComplete((unloaded, throwable) -> { + if (throwable != null) { + IrisLogging.reportError("Failed to close the benchmark world.", throwable); + } else if (!Boolean.TRUE.equals(unloaded)) { + IrisLogging.error("Failed to unload the benchmark world."); + } + }); + + stopwatch.end(); + } catch (Exception e) { + IrisLogging.error("Something has gone wrong!"); + e.printStackTrace(); + } + } + + private void createBenchmark() { + try { + IrisToolbelt.createWorld() + .dimension(dimension.getLoadKey()) + .name("benchmark") + .seed(1337) + .studio(false) + .benchmark(true) + .create(); + } catch (IrisException e) { + throw new RuntimeException(e); + } + } + + private void startBenchmark() { + try { + instance.set(this); + IrisToolbelt.pregenerate(PregenTask + .builder() + .gui(gui) + .radiusX(radius) + .radiusZ(radius) + .build(), benchmarkWorld() + ); + } finally { + instance.remove(); + } + } + + private static World benchmarkWorld() { + return WorldIdentity.resolve(IrisWorldStorage.keyFromName("benchmark")).orElse(null); + } + + private double calculateMedian(KList list) { + if (list.isEmpty()) { + return 0D; + } + KList sorted = new KList<>(list); + Collections.sort(sorted); + int middle = sorted.size() / 2; + + if (sorted.size() % 2 == 1) { + return sorted.get(middle); + } else { + return (sorted.get(middle - 1) + sorted.get(middle)) / 2.0; + } + } + + private int findLowest(KList list) { + return list.isEmpty() ? 0 : Collections.min(list); + } + + private int findHighest(KList list) { + return list.isEmpty() ? 0 : Collections.max(list); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisReflectiveAPI.java b/core/src/main/java/art/arcane/iris/core/tools/IrisReflectiveAPI.java new file mode 100644 index 000000000..b01742a80 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisReflectiveAPI.java @@ -0,0 +1,44 @@ +package art.arcane.iris.core.tools; + +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import org.bukkit.World; +import org.bukkit.block.data.BlockData; + +/** + * This class is used by an external IrisLib for other plugins to interact with Iris. Do not change + * existing methods or their parameters as it will break the library that uses these methods + * feel free to add more methods so long as you also add the reflective methods to the library + */ +public class IrisReflectiveAPI { + public static boolean isIrisWorld(World world) { + return IrisToolbelt.isIrisWorld(world); + } + + public static boolean isIrisStudioWorld(World world) { + return IrisToolbelt.isIrisStudioWorld(world); + } + + public static void registerCustomBlockData(String namespace, String key, BlockData blockData) { + BukkitBlockResolution.registerCustomBlockData(namespace, key, blockData); + } + + public static void retainMantleData(String classname) { + WorldMaintenance.retainMantleDataForSlice(classname); + } + + // These delegate to IrisToolbelt so the caller's WORLD Y is rebased into mantle space + // (0..worldHeight). Raw pass-through silently no-opped below Y=0 and read the wrong + // cell everywhere else, diverging from IrisToolbelt and IrisModdedAPI semantics. + public static void setMantleData(World world, int x, int y, int z, Object data) { + IrisToolbelt.setMantleData(world, x, y, z, data); + } + + public static void deleteMantleData(World world, int x, int y, int z, Class c) { + IrisToolbelt.deleteMantleData(world, x, y, z, c); + } + + public static Object getMantleData(World world, int x, int y, int z, Class c) { + return IrisToolbelt.getMantleData(world, x, y, z, c); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java new file mode 100644 index 000000000..20b351d12 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java @@ -0,0 +1,700 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.tools; + +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.IrisRuntimeSchedulerMode; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pregenerator.PregenPerformanceProfile; +import art.arcane.iris.core.pregenerator.PregenTask; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.core.pregenerator.cache.PregenCache; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.core.pack.PackDownloader; +import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod; +import art.arcane.iris.core.pregenerator.methods.HybridPregenMethod; +import art.arcane.iris.core.service.GlobalCacheSVC; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineLifecycleTasks; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import io.papermc.lib.PaperLib; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.volmlib.util.localization.MessageArgument; +/** + * Something you really want to wear if working on Iris. Shit gets pretty hectic down there. + * Hope you packed snacks & road sodas. + */ +public class IrisToolbelt { + @ApiStatus.Internal + public static Map toolbeltConfiguration = new HashMap<>(); + private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod(); + + /** + * Finds an installed dimension or returns null. + * + * @param dimension the dimension id such as overworld or flat + * @return the IrisDimension or null + */ + public static IrisDimension getDimension(String dimension) { + if (dimension == null) { + return null; + } + + PackReference reference = parsePackReference(dimension); + if (reference == null) { + return null; + } + + File packsFolder = IrisPlatforms.get().packsFolder(); + File pack = PackDirectoryResolver.resolveExisting(packsFolder, reference.pack()); + if (pack == null) { + File found = findCaseInsensitivePack(packsFolder, reference.pack()); + if (found != null) { + pack = found; + } + } + + if (pack == null) { + IrisLogging.warn("Iris pack '" + reference.pack() + + "' is not installed. Install it with " + PackDownloader.downloadCommandFor(reference.pack()) + + " and restart the server."); + return null; + } + + IrisData data = IrisData.get(pack); + IrisDimension resolved = data.getDimensionLoader().load(reference.dimension(), false); + if (resolved != null) { + return resolved; + } + if (reference.explicitDimension()) { + return null; + } + + String packName = pack.getName(); + if (!packName.equals(reference.pack())) { + resolved = data.getDimensionLoader().load(packName, false); + if (resolved != null) { + return resolved; + } + } + + for (String key : data.getDimensionLoader().getPossibleKeys()) { + if (!key.equalsIgnoreCase(reference.pack()) && !key.equalsIgnoreCase(packName)) { + continue; + } + + resolved = data.getDimensionLoader().load(key, false); + if (resolved != null) { + return resolved; + } + } + + return null; + } + + static PackReference parsePackReference(String value) { + if (value == null) { + return null; + } + String requested = value.trim(); + if (requested.isEmpty()) { + return null; + } + int separator = requested.indexOf(':'); + if (separator < 0) { + if (!isSafePackDescriptor(requested)) { + return null; + } + return new PackReference(requested, installedPackName(requested), false); + } + String pack = requested.substring(0, separator).trim(); + String dimension = requested.substring(separator + 1).trim(); + if (!isSafePackDescriptor(pack) + || !isSafeDimensionKey(dimension) + || pack.equalsIgnoreCase(dimension)) { + return null; + } + return new PackReference(pack, dimension, true); + } + + private static boolean isSafePackDescriptor(String value) { + String[] segments = value.split("/", -1); + if (segments.length < 1 || segments.length > 3) { + return false; + } + for (String segment : segments) { + if (segment.isEmpty() + || segment.equals(".") + || segment.equals("..") + || segment.startsWith(".") + || !segment.matches("[A-Za-z0-9_-]+")) { + return false; + } + } + return true; + } + + private static boolean isSafeDimensionKey(String value) { + if (value.isEmpty() || value.length() > 256) { + return false; + } + String[] segments = value.split("/", -1); + if (segments.length > 16) { + return false; + } + for (String segment : segments) { + if (segment.isEmpty() + || segment.equals(".") + || segment.equals("..") + || segment.startsWith(".") + || !segment.matches("[A-Za-z0-9_-]+")) { + return false; + } + } + return true; + } + + private static String installedPackName(String descriptor) { + String[] segments = descriptor.split("/"); + return segments.length > 1 ? segments[1] : segments[0]; + } + + private static File findCaseInsensitivePack(File packsFolder, String requested) { + for (File child : PackDirectoryResolver.listVisiblePackDirectories(packsFolder)) { + if (child.getName().equalsIgnoreCase(requested)) { + return child; + } + } + + return null; + } + + /** + * Create a world with plenty of options + * + * @return the creator builder + */ + public static IrisCreator createWorld() { + return new IrisCreator(); + } + + /** + * Checks if the given world is an Iris World (same as access(world) != null) + * + * @param world the world + * @return true if it is an Iris Access world + */ + public static boolean isIrisWorld(World world) { + if (world == null) { + return false; + } + + if (world.getGenerator() instanceof PlatformChunkGenerator f) { + if (f instanceof BukkitChunkGenerator bukkit) { + bukkit.touch(world); + } + return true; + } + + return false; + } + + public static boolean isIrisStudioWorld(World world) { + return isIrisWorld(world) && access(world).isStudio(); + } + + /** + * Get the Iris generator for the given world + * + * @param world the given world + * @return the IrisAccess or null if it's not an Iris World + */ + public static PlatformChunkGenerator access(World world) { + if (world == null) { + return null; + } + + if (isIrisWorld(world)) { + return ((PlatformChunkGenerator) world.getGenerator()); + } + + StudioSVC studioService = IrisServices.getOrNull(StudioSVC.class); + if (studioService != null && studioService.isProjectOpen()) { + IrisProject activeProject = studioService.getActiveProject(); + if (activeProject != null) { + PlatformChunkGenerator activeProvider = activeProject.getActiveProvider(); + if (activeProvider != null) { + World activeWorld = BukkitWorldBinding.world(activeProvider.getTarget().getWorld()); + if (activeWorld != null && WorldIdentity.key(activeWorld).equals(WorldIdentity.key(world))) { + if (activeProvider instanceof BukkitChunkGenerator bukkit) { + bukkit.touch(world); + } + return activeProvider; + } + } + } + } + + return null; + } + + /** + * Start a pregenerator task + * + * @param task the scheduled task + * @param method the method to execute the task + * @return the pregenerator job (already started) + */ + public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine) { + return pregenerate(task, method, engine, true); + } + + /** + * Start a pregenerator task + * + * @param task the scheduled task + * @param method the method to execute the task + * @return the pregenerator job (already started) + */ + public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) { + // Match the modded adapter's contract: a running job is a rejection, never a silent + // kill whose teardown (60s drain + 120s flush) overlaps the new job's generation. + // Callers that genuinely want replacement must stop the old job first + // (PregeneratorJob.shutdownAndWait). + if (PregeneratorJob.getInstance() != null) { + throw new IllegalStateException("An Iris pregeneration job is already running; stop it first with /iris pregen stop."); + } + applyPregenPerformanceProfile(engine); + boolean useCachedWrapper = false; + if (cached && engine != null) { + IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen()); + useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA; + } + return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld()), task) : method, engine); + } + + private static PregenCache resolvePregenCache(IrisWorld world) { + String worldIdentity = world.identity(); + if (worldIdentity == null) { + throw new IllegalStateException("Pregeneration requires a namespaced world identity."); + } + PregenCache cache = IrisServices.get(GlobalCacheSVC.class).get(worldIdentity); + if (cache == null) { + IrisLogging.debug("Could not find existing cache for " + worldIdentity + " creating fallback"); + cache = GlobalCacheSVC.createDefault(worldIdentity); + } + return cache; + } + + public static boolean applyPregenPerformanceProfile() { + return PregenPerformanceProfile.apply(); + } + + public static void applyPregenPerformanceProfile(Engine engine) { + PlatformChunkGenerator generator = resolvePlatformGenerator(engine); + if (generator != null) { + PregenPerformanceProfile.applyToGenerator(generator); + return; + } + PregenPerformanceProfile.apply(engine); + } + + private static PlatformChunkGenerator resolvePlatformGenerator(Engine engine) { + if (engine == null) { + return null; + } + World world = BukkitWorldBinding.world(engine.getWorld()); + if (world == null) { + return null; + } + if (!(world.getGenerator() instanceof PlatformChunkGenerator generator)) { + throw new IllegalStateException("Live Iris engine is not bound to its platform chunk generator."); + } + if (generator.getEngine() != engine) { + throw new IllegalStateException("Live Iris engine does not match its platform chunk generator runtime."); + } + return generator; + } + + public static boolean supportsStrictSerialPregeneration() { + return PaperLib.isPaper(); + } + + /** + * Start a pregenerator task. If the supplied generator is headless, headless mode is used, + * otherwise Hybrid mode is used. + * + * @param task the scheduled task + * @param gen the Iris Generator + * @return the pregenerator job (already started) + */ + public static PregeneratorJob pregenerate(PregenTask task, PlatformChunkGenerator gen) { + World world = BukkitWorldBinding.world(gen.getEngine().getWorld()); + int threads = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()); + PregeneratorMethod method = new HybridPregenMethod(world, threads); + return pregenerate(task, method, gen.getEngine()); + } + + public static PregeneratorJob pregenerateSerial(PregenTask task, PlatformChunkGenerator gen) { + World world = BukkitWorldBinding.world(gen.getEngine().getWorld()); + PregeneratorMethod method = HybridPregenMethod.strictSerial(world); + return pregenerate(task, method, gen.getEngine()); + } + + /** + * Start a pregenerator task. If the supplied generator is headless, headless mode is used, + * otherwise Hybrid mode is used. + * + * @param task the scheduled task + * @param world the World + * @return the pregenerator job (already started) + */ + public static PregeneratorJob pregenerate(PregenTask task, World world) { + if (isIrisWorld(world)) { + return pregenerate(task, access(world)); + } + + int threads = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()); + PregeneratorMethod method = new HybridPregenMethod(world, threads); + return pregenerate(task, method, null); + } + + public static PregeneratorJob pregenerateSerial(PregenTask task, World world) { + if (isIrisWorld(world)) { + return pregenerateSerial(task, access(world)); + } + + PregeneratorMethod method = HybridPregenMethod.strictSerial(world); + return pregenerate(task, method, null); + } + + /** + * Evacuate all players from the world into literally any other world. + * If there are no other worlds, kick them! Not the best but what's mine is mine sometimes... + * + * @param world the world to evac + */ + public static boolean evacuate(World world) { + return beginEvacuation(evacuateAsync(world)); + } + + public static CompletableFuture evacuateAsync(World world) { + return evacuateAsync(world, null, false); + } + + /** + * Evacuate all players from the world + * + * @param world the world to leave + * @param m the message + * @return true if it was evacuated. + */ + public static boolean evacuate(World world, String m) { + return beginEvacuation(evacuateAsync(world, m)); + } + + public static CompletableFuture evacuateAsync(World world, String message) { + return evacuateAsync(world, message, true); + } + + public static boolean isStudio(World i) { + if (!isIrisWorld(i)) { + return false; + } + + PlatformChunkGenerator generator = access(i); + return generator != null && generator.isStudio(); + } + + static CompletableFuture settleEvacuations(List> evacuations) { + List> pending = List.copyOf(evacuations); + if (pending.isEmpty()) { + return CompletableFuture.completedFuture(true); + } + + return CompletableFuture.allOf(pending.toArray(CompletableFuture[]::new)) + .handle((ignored, throwable) -> { + if (throwable != null) { + return false; + } + for (CompletableFuture evacuation : pending) { + if (!Boolean.TRUE.equals(evacuation.getNow(false))) { + return false; + } + } + return true; + }); + } + + private static CompletableFuture evacuateAsync(World world, String message, boolean customMessage) { + if (world == null || isServerStopping()) { + return CompletableFuture.completedFuture(false); + } + + ArrayList players = new ArrayList<>(world.getPlayers()); + if (players.isEmpty()) { + return CompletableFuture.completedFuture(true); + } + + World targetWorld = null; + for (World candidate : Bukkit.getWorlds()) { + if (!WorldIdentity.key(candidate).equals(WorldIdentity.key(world))) { + targetWorld = candidate; + break; + } + } + if (targetWorld == null) { + return CompletableFuture.completedFuture(false); + } + + Location target = targetWorld.getSpawnLocation(); + ArrayList> evacuations = new ArrayList<>(players.size()); + for (Player player : players) { + CompletableFuture evacuation = new CompletableFuture<>(); + Runnable teleportTask = () -> { + try { + if (customMessage) { + new VolmitSender(player, BukkitPlatform.volmitPlugin().getTag()).sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.IRIS_TOOLBELT_YOU_HAVE_BEEN_EVACUATED_FROM_THIS_WORLD_2, + MessageArgument.untrusted("m", String.valueOf(message)))); + } else { + new VolmitSender(player, BukkitPlatform.volmitPlugin().getTag()).sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.IRIS_TOOLBELT_YOU_HAVE_BEEN_EVACUATED_FROM_THIS_WORLD)); + } + teleportAsyncSafely(player, target).whenComplete((teleported, throwable) -> { + if (throwable == null) { + evacuation.complete(Boolean.TRUE.equals(teleported)); + } else { + evacuation.completeExceptionally(throwable); + } + }); + } catch (Throwable failure) { + evacuation.completeExceptionally(failure); + } + }; + try { + if (!J.runEntity(player, teleportTask)) { + teleportTask.run(); + } + } catch (Throwable failure) { + evacuation.completeExceptionally(failure); + } + evacuations.add(evacuation); + } + return settleEvacuations(evacuations); + } + + private static boolean beginEvacuation(CompletableFuture evacuation) { + if (evacuation.isDone()) { + try { + return Boolean.TRUE.equals(evacuation.getNow(false)); + } catch (Throwable failure) { + if (!isServerStopping()) { + IrisLogging.reportError(failure); + } + return false; + } + } + evacuation.exceptionally(throwable -> { + if (!isServerStopping()) { + IrisLogging.reportError(throwable); + } + return false; + }); + return true; + } + + private static CompletableFuture teleportAsyncSafely(Player player, Location target) { + if (player == null || target == null || isServerStopping()) { + return CompletableFuture.completedFuture(false); + } + + try { + CompletableFuture teleportFuture = PaperLib.teleportAsync(player, target); + if (teleportFuture == null) { + return CompletableFuture.completedFuture(false); + } + return teleportFuture.exceptionally(throwable -> { + if (!isServerStopping()) { + IrisLogging.reportError(throwable); + } + return false; + }); + } catch (Throwable throwable) { + if (!isServerStopping()) { + IrisLogging.reportError(throwable); + } + return CompletableFuture.completedFuture(false); + } + } + + public static boolean isServerStopping() { + Method method = BUKKIT_IS_STOPPING_METHOD; + if (method != null) { + try { + Object value = method.invoke(null); + if (value instanceof Boolean) { + return (Boolean) value; + } + } catch (Throwable ignored) { + } + } + + return !BukkitPlatform.hasPlugin() || !BukkitPlatform.plugin().isEnabled(); + } + + private static Method resolveBukkitIsStoppingMethod() { + try { + return Bukkit.class.getMethod("isStopping"); + } catch (Throwable ignored) { + return null; + } + } + + public static void beginWorldMaintenance(World world, String reason) { + beginWorldMaintenance(world, reason, false); + } + + public static void beginWorldMaintenance(World world, String reason, boolean bypassMantleStages) { + if (world == null) { + return; + } + + beginWorldMaintenance(WorldIdentity.serialize(world), reason, bypassMantleStages); + } + + public static void beginWorldMaintenance(String worldName, String reason) { + beginWorldMaintenance(worldName, reason, false); + } + + public static void beginWorldMaintenance(String worldName, String reason, boolean bypassMantleStages) { + WorldMaintenance.beginWorldMaintenance(worldName, reason, bypassMantleStages); + } + + public static void endWorldMaintenance(World world, String reason) { + endWorldMaintenance(world, reason, false); + } + + public static void endWorldMaintenance(World world, String reason, boolean bypassMantleStages) { + if (world == null) { + return; + } + + endWorldMaintenance(WorldIdentity.serialize(world), reason, bypassMantleStages); + } + + public static void endWorldMaintenance(String worldName, String reason) { + WorldMaintenance.endWorldMaintenance(worldName, reason); + } + + public static void endWorldMaintenance(String worldName, String reason, boolean bypassMantleStages) { + WorldMaintenance.endWorldMaintenance(worldName, reason, bypassMantleStages); + } + + public static boolean isWorldMaintenanceActive(World world) { + return world != null && isWorldMaintenanceActive(WorldIdentity.serialize(world)); + } + + public static boolean isWorldMaintenanceActive(String worldName) { + return WorldMaintenance.isWorldMaintenanceActive(worldName); + } + + public static boolean isWorldMaintenanceBypassingMantleStages(World world) { + return world != null && isWorldMaintenanceBypassingMantleStages(WorldIdentity.serialize(world)); + } + + public static boolean isWorldMaintenanceBypassingMantleStages(String worldName) { + return WorldMaintenance.isWorldMaintenanceBypassingMantleStages(worldName); + } + + public static void retainMantleDataForSlice(String className) { + WorldMaintenance.retainMantleDataForSlice(className); + } + + public static boolean isRetainingMantleDataForSlice(String className) { + return WorldMaintenance.isRetainingMantleDataForSlice(className); + } + + public static T getMantleData(World world, int x, int y, int z, Class of) { + PlatformChunkGenerator e = access(world); + if (e == null) { + return null; + } + // Lease-gated so the engine shutdown drain covers these public accessors; a mantle + // mid-close refuses the lease instead of racing the region flush. + return EngineLifecycleTasks.call(e.getEngine(), "api_mantle_get", + () -> e.getEngine().getMantle().getMantle().get(x, y - world.getMinHeight(), z, of), null); + } + + public static void deleteMantleData(World world, int x, int y, int z, Class of) { + PlatformChunkGenerator e = access(world); + if (e == null) { + return; + } + EngineLifecycleTasks.run(e.getEngine(), "api_mantle_delete", + () -> e.getEngine().getMantle().getMantle().remove(x, y - world.getMinHeight(), z, of)); + } + + public static void setMantleData(World world, int x, int y, int z, T data) { + PlatformChunkGenerator e = access(world); + if (e == null || data == null) { + return; + } + EngineLifecycleTasks.run(e.getEngine(), "api_mantle_set", + () -> e.getEngine().getMantle().getMantle().set(x, y - world.getMinHeight(), z, data)); + } + + public static boolean removeWorld(World world) throws IOException { + return IrisCreator.removeFromBukkitYml(WorldIdentity.key(world)); + } + + record PackReference(String pack, String dimension, boolean explicitDimension) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java new file mode 100644 index 000000000..06f735a8c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java @@ -0,0 +1,121 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.tools; + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.WorldCreatorCompat; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import art.arcane.iris.platform.bukkit.BukkitEnvironment; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.generator.ChunkGenerator; + +import java.io.File; + +public class IrisWorldCreator { + private String name; + private boolean studio = false; + private String dimensionName = null; + private IrisDimension dimension; + private long seed = 1337; + private boolean persistent; + + public IrisWorldCreator() { + + } + + public IrisWorldCreator dimension(String loadKey) { + this.dimensionName = loadKey; + this.dimension = null; + return this; + } + + public IrisWorldCreator dimension(IrisDimension dimension) { + this.dimension = dimension; + this.dimensionName = dimension.getLoadKey(); + return this; + } + + public IrisWorldCreator name(String name) { + this.name = name; + return this; + } + + public IrisWorldCreator seed(long seed) { + this.seed = seed; + return this; + } + + public IrisWorldCreator studioMode() { + this.studio = true; + return this; + } + + public IrisWorldCreator productionMode() { + this.studio = false; + return this; + } + + public IrisWorldCreator persistent(boolean persistent) { + this.persistent = persistent; + return this; + } + + public WorldCreator create() { + IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension; + NamespacedKey worldKey = IrisWorldStorage.keyFromName(name); + World.Environment environment = findEnvironment(); + WorldCreator creator = persistent + ? WorldCreatorCompat.ofPersistentKey(worldKey) + : WorldCreatorCompat.ofKey(worldKey); + File worldFolder = persistent + ? WorldCreatorCompat.persistentDimensionRoot(worldKey) + : IrisWorldStorage.dimensionRoot(worldKey); + + IrisWorld w = IrisWorld.builder() + .platformIdentity(worldKey.toString()) + .name(creator.name()) + .minHeight(dim.getMinHeight()) + .maxHeight(dim.getMaxHeight()) + .seed(seed) + .worldFolder(worldFolder) + .build(); + ChunkGenerator g = new BukkitChunkGenerator(w, studio, studio + ? dim.getLoader().getDataFolder() : + new File(w.worldFolder(), "iris/pack"), dimensionName); + + return creator.environment(environment) + .generateStructures(true) + .generator(g).seed(seed); + } + + private World.Environment findEnvironment() { + IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension; + return BukkitEnvironment.from(dim == null ? null : dim.getEnvironment()); + } + + public IrisWorldCreator studio(boolean studio) { + this.studio = studio; + return this; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizeBatch.java b/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizeBatch.java new file mode 100644 index 000000000..ede80be84 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizeBatch.java @@ -0,0 +1,228 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.tools; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.localization.TextKey; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; + +public final class TreePlausibilizeBatch { + private TreePlausibilizeBatch() { + } + + public record Target(String key, File file) { + } + + public static List resolve(String target, IrisData data) { + List out = new ArrayList<>(); + if (target == null || target.isEmpty()) { + return out; + } + + File direct = new File(target); + if (direct.isFile() && target.toLowerCase(Locale.ROOT).endsWith(".iob")) { + out.add(new Target(direct.getName().replaceAll("\\.iob$", ""), direct)); + return out; + } + if (direct.isDirectory()) { + walkIob(direct, direct, out); + return out; + } + + if (data != null) { + ResourceLoader loader = data.getObjectLoader(); + if (!target.endsWith("/") && loader.findFile(target) != null) { + out.add(new Target(target, null)); + return out; + } + String prefix = target.endsWith("/") ? target : target + "/"; + for (String k : loader.getPossibleKeys()) { + if (k.startsWith(prefix)) { + out.add(new Target(k, null)); + } + } + } + return out; + } + + public static void walkIob(File root, File keyRoot, List out) { + File[] kids = root.listFiles(); + if (kids == null) { + return; + } + for (File f : kids) { + if (f.isDirectory()) { + walkIob(f, keyRoot, out); + } else if (f.getName().toLowerCase(Locale.ROOT).endsWith(".iob")) { + String rel = keyRoot.toPath().relativize(f.toPath()).toString() + .replace(File.separatorChar, '/') + .replaceAll("\\.iob$", ""); + out.add(new Target(rel, f)); + } + } + } + + public static void run(List targets, boolean dryRun, int reach, IrisData nearest, Consumer out) { + int processed = 0; + int changed = 0; + int skipped = 0; + int failed = 0; + long totalWood = 0L; + long totalBranches = 0L; + long totalConverted = 0L; + long totalRewritten = 0L; + long totalPinned = 0L; + long totalUnreachableBefore = 0L; + long totalUnreachableAfter = 0L; + + int progressStep = Math.max(1, targets.size() / 20); + int index = 0; + + for (Target t : targets) { + index++; + try { + IrisObject o = load(t, nearest); + if (o == null) { + out.accept(output( + RuntimeUiMessages.TREE_SKIP_LOAD, + false, + MessageArgument.untrusted("object", t.key()) + )); + skipped++; + continue; + } + + long seed = TreePlausibilizer.seedOf(t.key()); + TreePlausibilizer.Result r = dryRun + ? TreePlausibilizer.analyze(o, seed, reach) + : TreePlausibilizer.apply(o, seed, reach); + + if (!dryRun && r.mutated()) { + File dest = o.getLoadFile() != null ? o.getLoadFile() : t.file(); + if (dest != null) { + o.write(dest); + changed++; + } + } + + processed++; + totalWood += r.woodPlaced(); + totalBranches += r.branchesGrown(); + totalConverted += r.leavesConvertedToWood(); + totalRewritten += r.distancesRewritten(); + totalPinned += r.leavesPinnedPersistent(); + totalUnreachableBefore += r.unreachableBefore(); + totalUnreachableAfter += r.unreachableAfter(); + + if (r.mutated() || targets.size() == 1) { + if (r.leavesPinnedPersistent() > 0) { + out.accept(output( + RuntimeUiMessages.TREE_RESULT_PINNED, + false, + MessageArgument.untrusted("object", t.key()), + MessageArgument.trusted("wood", r.woodPlaced()), + MessageArgument.trusted("branches", r.branchesGrown()), + MessageArgument.trusted("converted", r.leavesConvertedToWood()), + MessageArgument.trusted("distances", r.distancesRewritten()), + MessageArgument.trusted("pinned", r.leavesPinnedPersistent()) + )); + } else { + out.accept(output( + RuntimeUiMessages.TREE_RESULT, + false, + MessageArgument.untrusted("object", t.key()), + MessageArgument.trusted("wood", r.woodPlaced()), + MessageArgument.trusted("branches", r.branchesGrown()), + MessageArgument.trusted("converted", r.leavesConvertedToWood()), + MessageArgument.trusted("distances", r.distancesRewritten()) + )); + } + } + + if (targets.size() > 1 && index % progressStep == 0) { + out.accept(output( + RuntimeUiMessages.TREE_PROGRESS, + true, + MessageArgument.trusted("current", index), + MessageArgument.trusted("total", targets.size()) + )); + } + } catch (Throwable e) { + out.accept(output( + RuntimeUiMessages.TREE_FAILED, + false, + MessageArgument.untrusted("object", t.key()), + MessageArgument.untrusted("type", e.getClass().getSimpleName()), + MessageArgument.untrusted("error", String.valueOf(e.getMessage())) + )); + IrisLogging.reportError(e); + failed++; + } + } + + out.accept(output( + dryRun ? RuntimeUiMessages.TREE_DONE_DRY : RuntimeUiMessages.TREE_DONE, + true, + MessageArgument.trusted("processed", processed), + MessageArgument.trusted("changed", changed), + MessageArgument.trusted("skipped", skipped), + MessageArgument.trusted("failed", failed) + )); + out.accept(output( + RuntimeUiMessages.TREE_TOTALS, + true, + MessageArgument.trusted("wood", totalWood), + MessageArgument.trusted("branches", totalBranches), + MessageArgument.trusted("converted", totalConverted), + MessageArgument.trusted("distances", totalRewritten), + MessageArgument.trusted("pinned", totalPinned), + MessageArgument.trusted("before", totalUnreachableBefore), + MessageArgument.trusted("after", totalUnreachableAfter) + )); + } + + private static Output output(TextKey key, boolean headline, MessageArgument... arguments) { + return new Output(IrisLanguage.plain(key, arguments), headline); + } + + private static IrisObject load(Target t, IrisData nearest) throws IOException { + if (t.file() != null) { + IrisObject o = new IrisObject(); + o.read(t.file()); + o.setLoadFile(t.file()); + return o; + } + return IrisData.loadAnyObject(t.key(), nearest); + } + + public record Output(String text, boolean headline) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizer.java b/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizer.java new file mode 100644 index 000000000..4debad381 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizer.java @@ -0,0 +1,722 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.tools; + +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class TreePlausibilizer { + public static final int MAX_DISTANCE = 6; + public static final int DEFAULT_REACH = 12; + public static final int STOP_SHORT = 3; + private static final int MAX_TENDRILS_PER_CLUSTER = 24; + private static final int MAX_STALLS_PER_CLUSTER = 2; + private static final int SCATTER_CLUSTER_LIMIT = 256; + private static final double MAX_HELIX_AMPLITUDE = 1.6D; + private static final long GOLDEN_GAMMA = 0x9E3779B97F4A7C15L; + private static final int[][] NEIGHBORS = { + {1, 0, 0}, {-1, 0, 0}, + {0, 1, 0}, {0, -1, 0}, + {0, 0, 1}, {0, 0, -1} + }; + private static final Set STEM_WOOD = Set.of( + "minecraft:bamboo_block", + "minecraft:stripped_bamboo_block", + "minecraft:crimson_stem", + "minecraft:stripped_crimson_stem", + "minecraft:warped_stem", + "minecraft:stripped_warped_stem" + ); + + private TreePlausibilizer() { + } + + public record Result( + int totalLeaves, + int unreachableBefore, + int clusters, + int branchesGrown, + int woodPlaced, + int leavesConvertedToWood, + int leavesPinnedPersistent, + int distancesRewritten, + int unreachableAfter + ) { + public boolean mutated() { + return woodPlaced + leavesConvertedToWood + distancesRewritten > 0; + } + } + + public static long seedOf(String key) { + long hash = 0xCBF29CE484222325L; + for (int i = 0; i < key.length(); i++) { + hash ^= key.charAt(i); + hash *= 0x100000001B3L; + } + return hash; + } + + public static Result analyze(IrisObject obj, long seed, int reach) { + return run(obj, false, seed, reach); + } + + public static Result apply(IrisObject obj, long seed, int reach) { + return run(obj, true, seed, reach); + } + + private static Result run(IrisObject obj, boolean mutate, long seed, int reach) { + VectorMap blocks = obj.getBlocks(); + Workspace ws = new Workspace(blocks); + + int totalLeaves = ws.leaves.size(); + if (totalLeaves == 0) { + return new Result(0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + ws.template = pickBranchTemplate(ws.positions, ws.wood); + ws.distances = seedDistances(ws.wood, ws.leaves); + ws.unreached = new HashSet<>(ws.leaves); + ws.unreached.removeAll(ws.distances.keySet()); + int unreachableBefore = ws.unreached.size(); + + List> clusters = clusterize(ws.unreached); + int clusterCount = clusters.size(); + + if (ws.template != null && clusters.size() <= SCATTER_CLUSTER_LIMIT) { + int ordinal = 0; + for (List cluster : clusters) { + long clusterSeed = seed ^ (ordinal * GOLDEN_GAMMA); + ordinal++; + GapTracker tracker = new GapTracker(cluster, ws.wood); + if (reach > 0 && tracker.minGap() > reach) { + continue; + } + ws.growCluster(cluster, clusterSeed, tracker); + } + } + + Map finalDistances = seedDistances(ws.wood, ws.leaves); + List rewrites = new ArrayList<>(); + int pinned = 0; + int unreachableAfter = 0; + List sortedLeaves = new ArrayList<>(ws.leaves); + Collections.sort(sortedLeaves); + for (long leaf : sortedLeaves) { + PlatformBlockState state = ws.positions.get(leaf); + Integer d = finalDistances.get(leaf); + PlatformBlockState next; + if (d != null) { + next = state.withProperty("persistent", "false") + .withProperty("distance", String.valueOf(Math.max(1, Math.min(MAX_DISTANCE, d)))); + } else { + next = state.withProperty("persistent", "true").withProperty("distance", "7"); + pinned++; + unreachableAfter++; + } + if (!next.key().equals(state.key())) { + rewrites.add(new LeafRewrite(leaf, next)); + } + } + + if (mutate) { + for (WoodPlacement placement : ws.placements) { + blocks.put(unpackKey(placement.key()), placement.state()); + } + for (LeafRewrite rewrite : rewrites) { + blocks.put(unpackKey(rewrite.key()), rewrite.state()); + } + } + + return new Result( + totalLeaves, + unreachableBefore, + clusterCount, + ws.branches, + ws.woodPlaced, + ws.converted, + pinned, + rewrites.size(), + unreachableAfter + ); + } + + private static PlatformBlockState pickBranchTemplate(Map positions, Set wood) { + if (wood.isEmpty()) { + return null; + } + Map materialCounts = new HashMap<>(); + for (long key : wood) { + materialCounts.merge(IrisProceduralBlocks.materialKey(positions.get(key)), 1, Integer::sum); + } + String dominant = null; + int dominantCount = -1; + for (Map.Entry e : materialCounts.entrySet()) { + if (e.getValue() > dominantCount + || (e.getValue() == dominantCount && e.getKey().compareTo(dominant) < 0)) { + dominantCount = e.getValue(); + dominant = e.getKey(); + } + } + Map stateCounts = new HashMap<>(); + Map statesByKey = new HashMap<>(); + for (long key : wood) { + PlatformBlockState state = positions.get(key); + if (!IrisProceduralBlocks.materialKey(state).equals(dominant)) { + continue; + } + stateCounts.merge(state.key(), 1, Integer::sum); + statesByKey.putIfAbsent(state.key(), state); + } + String best = null; + int bestCount = -1; + for (Map.Entry e : stateCounts.entrySet()) { + if (e.getValue() > bestCount + || (e.getValue() == bestCount && e.getKey().compareTo(best) < 0)) { + bestCount = e.getValue(); + best = e.getKey(); + } + } + return statesByKey.get(best); + } + + private static List> clusterize(Set unreached) { + List> clusters = new ArrayList<>(); + List sorted = new ArrayList<>(unreached); + Collections.sort(sorted); + Set assigned = new HashSet<>(); + for (long seedKey : sorted) { + if (assigned.contains(seedKey)) { + continue; + } + List cluster = new ArrayList<>(); + Deque queue = new ArrayDeque<>(); + queue.add(seedKey); + assigned.add(seedKey); + while (!queue.isEmpty()) { + long pos = queue.poll(); + cluster.add(pos); + int[] xyz = unpack(pos); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]); + if (unreached.contains(nk) && assigned.add(nk)) { + queue.add(nk); + } + } + } + Collections.sort(cluster); + clusters.add(cluster); + } + return clusters; + } + + private static final class GapTracker { + private final Map leafCells = new HashMap<>(); + private final Map gaps = new HashMap<>(); + private final Map anchors = new HashMap<>(); + private final Map anchorDistances = new HashMap<>(); + + private GapTracker(List cluster, Set wood) { + for (long leaf : cluster) { + leafCells.put(leaf, unpack(leaf)); + gaps.put(leaf, Integer.MAX_VALUE); + anchors.put(leaf, -1L); + anchorDistances.put(leaf, Long.MAX_VALUE); + } + for (long w : wood) { + onWoodPlaced(w); + } + } + + private void onWoodPlaced(long woodKey) { + int[] wx = unpack(woodKey); + for (Map.Entry entry : leafCells.entrySet()) { + long leaf = entry.getKey(); + int[] lx = entry.getValue(); + int cheb = chebyshev(lx, wx); + if (cheb < gaps.get(leaf)) { + gaps.put(leaf, cheb); + } + long dx = lx[0] - wx[0]; + long dy = lx[1] - wx[1]; + long dz = lx[2] - wx[2]; + long d2 = dx * dx + dy * dy + dz * dz; + long bestD2 = anchorDistances.get(leaf); + long bestAnchor = anchors.get(leaf); + if (d2 < bestD2 || (d2 == bestD2 && woodKey < bestAnchor)) { + anchorDistances.put(leaf, d2); + anchors.put(leaf, woodKey); + } + } + } + + private void drop(long leaf) { + leafCells.remove(leaf); + gaps.remove(leaf); + anchors.remove(leaf); + anchorDistances.remove(leaf); + } + + private int minGap() { + int best = Integer.MAX_VALUE; + for (int gap : gaps.values()) { + if (gap < best) { + best = gap; + } + } + return best; + } + + private long worst(Set unreached) { + long worst = -1L; + int worstGap = -1; + for (Map.Entry entry : gaps.entrySet()) { + long leaf = entry.getKey(); + if (!unreached.contains(leaf)) { + continue; + } + int gap = entry.getValue(); + if (gap > worstGap || (gap == worstGap && leaf < worst)) { + worstGap = gap; + worst = leaf; + } + } + return worst; + } + + private long anchor(long leaf) { + Long anchor = anchors.get(leaf); + return anchor == null ? -1L : anchor; + } + + private int gapOf(long leaf) { + Integer gap = gaps.get(leaf); + return gap == null ? Integer.MAX_VALUE : gap; + } + } + + private static Map seedDistances(Set wood, Set leaves) { + Map dist = new HashMap<>(); + Deque queue = new ArrayDeque<>(); + for (long leaf : leaves) { + int[] xyz = unpack(leaf); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]); + if (wood.contains(nk)) { + dist.put(leaf, 1); + queue.add(leaf); + break; + } + } + } + while (!queue.isEmpty()) { + long pos = queue.poll(); + int d = dist.get(pos); + if (d >= MAX_DISTANCE) { + continue; + } + int[] xyz = unpack(pos); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]); + if (leaves.contains(nk) && !dist.containsKey(nk)) { + dist.put(nk, d + 1); + queue.add(nk); + } + } + } + return dist; + } + + private static boolean isWood(PlatformBlockState state) { + String material = IrisProceduralBlocks.materialKey(state); + return material.endsWith("_log") + || material.endsWith("_wood") + || material.endsWith("_hyphae") + || STEM_WOOD.contains(material); + } + + private static boolean isDecayableLeaf(PlatformBlockState state) { + return IrisProceduralBlocks.hasProperty(state, "distance") + && IrisProceduralBlocks.hasProperty(state, "persistent"); + } + + private static PlatformBlockState axisVariant(PlatformBlockState template, int dx, int dy, int dz) { + if (!IrisProceduralBlocks.hasProperty(template, "axis")) { + return template; + } + double weightedY = Math.abs(dy) * 1.8D; + String axis; + if (weightedY >= Math.abs(dx) && weightedY >= Math.abs(dz)) { + axis = "y"; + } else if (Math.abs(dx) >= Math.abs(dz)) { + axis = "x"; + } else { + axis = "z"; + } + return template.withProperty("axis", axis); + } + + private static int chebyshev(int[] a, int[] b) { + return Math.max(Math.max(Math.abs(a[0] - b[0]), Math.abs(a[1] - b[1])), Math.abs(a[2] - b[2])); + } + + private static double[] cross(double[] a, double[] b) { + return new double[]{ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + }; + } + + private static double lengthOf(double[] a) { + return Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]); + } + + private static double[] normalize(double[] a) { + double len = lengthOf(a); + return new double[]{a[0] / len, a[1] / len, a[2] / len}; + } + + private static long packKey(IrisBlockVector pos) { + return packXYZ(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); + } + + private static long packXYZ(int x, int y, int z) { + long lx = (x + 32768L) & 0xFFFFL; + long ly = (y + 32768L) & 0xFFFFL; + long lz = (z + 32768L) & 0xFFFFL; + return (lx << 32) | (ly << 16) | lz; + } + + private static int[] unpack(long key) { + int x = (int) ((key >> 32) & 0xFFFFL) - 32768; + int y = (int) ((key >> 16) & 0xFFFFL) - 32768; + int z = (int) (key & 0xFFFFL) - 32768; + return new int[]{x, y, z}; + } + + private static IrisBlockVector unpackKey(long key) { + int[] xyz = unpack(key); + return new IrisBlockVector(xyz[0], xyz[1], xyz[2]); + } + + private record WoodPlacement(long key, PlatformBlockState state) { + } + + private record LeafRewrite(long key, PlatformBlockState state) { + } + + private static final class Workspace { + private final Map positions; + private final Set wood = new HashSet<>(); + private final Set leaves = new HashSet<>(); + private final List placements = new ArrayList<>(); + private Map distances = new HashMap<>(); + private Set unreached = new HashSet<>(); + private PlatformBlockState template; + private GapTracker tracker; + private int minX = Integer.MAX_VALUE; + private int minY = Integer.MAX_VALUE; + private int minZ = Integer.MAX_VALUE; + private int maxX = Integer.MIN_VALUE; + private int maxY = Integer.MIN_VALUE; + private int maxZ = Integer.MIN_VALUE; + private int woodPlaced; + private int converted; + private int branches; + + private Workspace(VectorMap blocks) { + positions = new HashMap<>(blocks.size() * 2); + for (Map.Entry entry : blocks) { + IrisBlockVector pos = entry.getKey(); + PlatformBlockState state = entry.getValue(); + long key = packKey(pos); + positions.put(key, state); + int x = pos.getBlockX(); + int y = pos.getBlockY(); + int z = pos.getBlockZ(); + if (x < minX) minX = x; + if (y < minY) minY = y; + if (z < minZ) minZ = z; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + if (z > maxZ) maxZ = z; + if (isWood(state)) { + wood.add(key); + } else if (isDecayableLeaf(state)) { + leaves.add(key); + } + } + } + + private void growCluster(List cluster, long clusterSeed, GapTracker gapTracker) { + tracker = gapTracker; + try { + int stalls = 0; + int budget = Math.max(MAX_TENDRILS_PER_CLUSTER, cluster.size() / 8); + for (int tendril = 0; tendril < budget; tendril++) { + long target = gapTracker.worst(unreached); + if (target == -1L) { + return; + } + long anchor = gapTracker.anchor(target); + if (anchor == -1L) { + return; + } + int placedBefore = placements.size(); + int unreachedBefore = unreachedInCluster(cluster); + RNG rng = new RNG(clusterSeed).nextParallelRNG(tendril); + growTendril(anchor, target, rng); + boolean progressed = placements.size() > placedBefore || unreachedInCluster(cluster) < unreachedBefore; + if (placements.size() > placedBefore) { + branches++; + } + if (progressed) { + stalls = 0; + continue; + } + stalls++; + if (stalls >= MAX_STALLS_PER_CLUSTER) { + break; + } + } + directConnect(cluster); + } finally { + tracker = null; + } + } + + private void growTendril(long anchorKey, long targetKey, RNG rng) { + int[] anchor = unpack(anchorKey); + int[] target = unpack(targetKey); + double[] delta = new double[]{target[0] - anchor[0], target[1] - anchor[1], target[2] - anchor[2]}; + double length = lengthOf(delta); + if (length < 1.0D) { + return; + } + double[] direction = normalize(delta); + double[] side = cross(direction, new double[]{0.0D, 1.0D, 0.0D}); + if (lengthOf(side) < 0.05D) { + side = cross(direction, new double[]{1.0D, 0.0D, 0.0D}); + } + side = normalize(side); + double[] up = cross(direction, side); + + double amplitudeMax = Math.min(MAX_HELIX_AMPLITUDE, length / 5.0D); + double totalTwist = (1.25D + rng.d()) * Math.PI; + double phase = rng.d() * 2.0D * Math.PI; + double handedness = rng.b() ? 1.0D : -1.0D; + double drawLength = Math.max(1.0D, length - STOP_SHORT); + int steps = (int) Math.ceil(drawLength * 2.0D); + + int[] previous = anchor; + for (int i = 1; i <= steps; i++) { + double arc = drawLength * i / steps; + double fraction = arc / drawLength; + double amplitude = amplitudeMax * Math.sin(Math.PI * fraction); + double theta = phase + handedness * totalTwist * (arc / length); + double offsetSide = amplitude * Math.cos(theta); + double offsetUp = amplitude * Math.sin(theta); + int cx = clamp((int) Math.round(anchor[0] + direction[0] * arc + side[0] * offsetSide + up[0] * offsetUp), minX, maxX); + int cy = clamp((int) Math.round(anchor[1] + direction[1] * arc + side[1] * offsetSide + up[1] * offsetUp), minY, maxY); + int cz = clamp((int) Math.round(anchor[2] + direction[2] * arc + side[2] * offsetSide + up[2] * offsetUp), minZ, maxZ); + int[] cell = new int[]{cx, cy, cz}; + bridge(previous, cell); + previous = cell; + } + } + + private void directConnect(List cluster) { + List ordered = new ArrayList<>(cluster); + if (tracker != null) { + GapTracker gapTracker = tracker; + ordered.sort((Long a, Long b) -> { + int ga = gapTracker.gapOf(a); + int gb = gapTracker.gapOf(b); + if (ga != gb) { + return Integer.compare(ga, gb); + } + return Long.compare(a, b); + }); + } + for (long target : ordered) { + if (!unreached.contains(target)) { + continue; + } + long anchor = tracker == null ? -1L : tracker.anchor(target); + if (anchor == -1L) { + return; + } + int placedBefore = placements.size(); + int[] pos = unpack(anchor); + int[] t = unpack(target); + int guard = chebyshev(pos, t) * 4 + 8; + while (guard-- > 0 && unreached.contains(target) && !distances.containsKey(target)) { + int dx = Integer.signum(t[0] - pos[0]); + int dy = Integer.signum(t[1] - pos[1]); + int dz = Integer.signum(t[2] - pos[2]); + if (dx == 0 && dy == 0 && dz == 0) { + break; + } + boolean landsOnTarget = pos[0] + dx == t[0] && pos[1] + dy == t[1] && pos[2] + dz == t[2]; + boolean diagonal = Math.abs(dx) + Math.abs(dy) + Math.abs(dz) > 1; + if (landsOnTarget && diagonal) { + if (dx != 0) { + dy = 0; + dz = 0; + } else if (dy != 0) { + dz = 0; + } + } + int[] step = new int[]{dx, dy, dz}; + int[][] candidates = {{dx, dy, dz}, {dx, 0, 0}, {0, dy, 0}, {0, 0, dz}}; + for (int[] candidate : candidates) { + if (candidate[0] == 0 && candidate[1] == 0 && candidate[2] == 0) { + continue; + } + int nx = pos[0] + candidate[0]; + int ny = pos[1] + candidate[1]; + int nz = pos[2] + candidate[2]; + if (nx == t[0] && ny == t[1] && nz == t[2]) { + continue; + } + if (leaves.contains(packXYZ(nx, ny, nz))) { + step = candidate; + break; + } + } + pos = new int[]{pos[0] + step[0], pos[1] + step[1], pos[2] + step[2]}; + placeCell(pos[0], pos[1], pos[2], step[0], step[1], step[2]); + } + if (placements.size() > placedBefore) { + branches++; + } + } + } + + private void bridge(int[] from, int[] to) { + int[] pos = from; + int guard = chebyshev(from, to) * 3 + 4; + while (guard-- > 0 && (pos[0] != to[0] || pos[1] != to[1] || pos[2] != to[2])) { + int dx = Integer.signum(to[0] - pos[0]); + int dy = Integer.signum(to[1] - pos[1]); + int dz = Integer.signum(to[2] - pos[2]); + pos = new int[]{pos[0] + dx, pos[1] + dy, pos[2] + dz}; + placeCell(pos[0], pos[1], pos[2], dx, dy, dz); + } + } + + private void placeCell(int x, int y, int z, int dx, int dy, int dz) { + long key = packXYZ(x, y, z); + if (wood.contains(key)) { + return; + } + PlatformBlockState existing = positions.get(key); + boolean convertLeaf = existing != null && leaves.contains(key); + if (existing != null && !convertLeaf) { + return; + } + PlatformBlockState state = axisVariant(template, dx, dy, dz); + positions.put(key, state); + wood.add(key); + if (convertLeaf) { + leaves.remove(key); + unreached.remove(key); + distances.remove(key); + converted++; + if (tracker != null) { + tracker.drop(key); + } + } else { + woodPlaced++; + } + placements.add(new WoodPlacement(key, state)); + if (tracker != null) { + tracker.onWoodPlaced(key); + } + propagate(key); + } + + private void propagate(long woodKey) { + int[] center = unpack(woodKey); + Deque queue = new ArrayDeque<>(); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(center[0] + n[0], center[1] + n[1], center[2] + n[2]); + if (leaves.contains(nk)) { + Integer current = distances.get(nk); + if (current == null || current > 1) { + if (current == null) { + unreached.remove(nk); + } + distances.put(nk, 1); + queue.add(nk); + } + } + } + while (!queue.isEmpty()) { + long pos = queue.poll(); + int d = distances.get(pos); + if (d >= MAX_DISTANCE) { + continue; + } + int[] xyz = unpack(pos); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]); + if (leaves.contains(nk)) { + Integer current = distances.get(nk); + if (current == null || current > d + 1) { + if (current == null) { + unreached.remove(nk); + } + distances.put(nk, d + 1); + queue.add(nk); + } + } + } + } + } + + private int unreachedInCluster(List cluster) { + int count = 0; + for (long leaf : cluster) { + if (unreached.contains(leaf)) { + count++; + } + } + return count; + } + + private static int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java b/core/src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java new file mode 100644 index 000000000..47e9f2966 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java @@ -0,0 +1,401 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.tools; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.localization.TextKey; +import org.bukkit.Bukkit; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +final class WorldCreationProgressReporter { + private static final int PLAYER_BAR_WIDTH = 44; + private static final int CONSOLE_BAR_WIDTH = 20; + private static final long CONSOLE_UPDATE_INTERVAL_MILLIS = 1500L; + + private final VolmitSender sender; + private final String worldName; + private final long startedAtMillis; + private final AtomicReference stage; + private final AtomicReference detail; + private final AtomicReference progress; + private final AtomicBoolean complete; + private final AtomicBoolean failed; + private final AtomicBoolean terminalRendered; + private final AtomicBoolean playerRenderQueued; + private final AtomicInteger taskId; + private final AtomicLong nextConsoleUpdate; + private volatile BossBar bossBar; + private volatile boolean hudDisabled; + + private WorldCreationProgressReporter(VolmitSender sender, String worldName) { + this.sender = sender; + this.worldName = worldName; + this.startedAtMillis = System.currentTimeMillis(); + this.stage = new AtomicReference<>("resolve_dimension"); + this.detail = new AtomicReference<>(""); + this.progress = new AtomicReference<>(0.01D); + this.complete = new AtomicBoolean(false); + this.failed = new AtomicBoolean(false); + this.terminalRendered = new AtomicBoolean(false); + this.playerRenderQueued = new AtomicBoolean(false); + this.taskId = new AtomicInteger(-1); + this.nextConsoleUpdate = new AtomicLong(0L); + + this.bossBar = null; + this.hudDisabled = false; + } + + static WorldCreationProgressReporter start(VolmitSender sender, String worldName) { + WorldCreationProgressReporter reporter = new WorldCreationProgressReporter(sender, worldName); + if (sender.isPlayer() && sender.player() != null + && IrisSettings.get().getGeneral().isProgressBossBar()) { + try { + J.sfut(reporter::initializePlayerHud).get(5L, TimeUnit.SECONDS); + } catch (Throwable failure) { + reporter.hudDisabled = true; + J.runGlobal(reporter::releaseHud); + IrisLogging.reportError("Failed to initialize world creation progress HUD for \"" + + worldName + "\".", failure); + } + } + reporter.taskId.set(J.ar(reporter::tick, 3)); + return reporter; + } + + private void initializePlayerHud() { + if (hudDisabled) { + return; + } + bossBar = Bukkit.createBossBar( + IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_WORKING, + MessageArgument.untrusted("world", worldName) + ), + BarColor.BLUE, + BarStyle.SEGMENTED_20 + ); + bossBar.setProgress(0.01D); + bossBar.addPlayer(sender.player()); + bossBar.setVisible(true); + } + + void update(double progress, String stage) { + update(progress, stage, ""); + } + + void update(double progress, String stage, String detail) { + this.progress.set(clampProgress(progress)); + if (stage != null && !stage.isBlank()) { + this.stage.set(stage); + } + this.detail.set(detail == null ? "" : detail); + } + + void succeed() { + progress.set(1.0D); + stage.set("complete"); + detail.set(""); + complete.set(true); + requestTerminalRender(); + } + + void fail() { + failed.set(true); + complete.set(true); + requestTerminalRender(); + } + + private void tick() { + double currentProgress = complete.get() && !failed.get() + ? 1.0D + : Math.min(0.99D, clampProgress(progress.get())); + String currentStage = IrisLanguage.text(stageKey(stage.get())); + String currentDetail = detail.get(); + int percent = (int) Math.round(currentProgress * 100.0D); + long elapsed = System.currentTimeMillis() - startedAtMillis; + + if (complete.get()) { + cancel(); + if (!terminalRendered.compareAndSet(false, true)) { + return; + } + renderTerminal(currentProgress, currentStage, currentDetail, percent, elapsed); + return; + } + + if (sender.isPlayer() && sender.player() != null) { + if (hasPlayerHud()) { + schedulePlayerRender(() -> renderPlayerProgress( + currentProgress, + currentStage, + currentDetail, + percent, + elapsed + )); + } else { + schedulePlayerRender(() -> sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION, + MessageArgument.trusted("bar", buildPlayerBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + ))); + } + return; + } + + long now = System.currentTimeMillis(); + if (now >= nextConsoleUpdate.get()) { + sender.sendMessage(IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_CONSOLE, + MessageArgument.untrusted("world", worldName), + MessageArgument.trusted("bar", buildConsoleBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + )); + nextConsoleUpdate.set(now + CONSOLE_UPDATE_INTERVAL_MILLIS); + } + } + + private void renderPlayerProgress( + double currentProgress, + String currentStage, + String currentDetail, + int percent, + long elapsed + ) { + bossBar.setProgress(currentProgress); + bossBar.setTitle(IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_PROGRESS, + MessageArgument.untrusted("world", worldName), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage) + )); + + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION, + MessageArgument.trusted("bar", buildPlayerBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + )); + } + + private void renderTerminal( + double currentProgress, + String currentStage, + String currentDetail, + int percent, + long elapsed + ) { + if (sender.isPlayer() && sender.player() != null) { + if (hasPlayerHud()) { + schedulePlayerTerminalRender(() -> renderPlayerTerminal( + currentProgress, + currentStage, + currentDetail, + percent, + elapsed + )); + } else { + schedulePlayerTerminalRender(() -> sender.sendAction(IrisLanguage.text( + failed.get() + ? RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_FAILED + : RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_READY, + MessageArgument.trusted("bar", buildPlayerBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + ))); + } + return; + } + + sender.sendMessage(IrisLanguage.text( + failed.get() + ? RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_CONSOLE_FAILED + : RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_CONSOLE_READY, + MessageArgument.untrusted("world", worldName), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + )); + } + + private void renderPlayerTerminal( + double currentProgress, + String currentStage, + String currentDetail, + int percent, + long elapsed + ) { + bossBar.setProgress(currentProgress); + bossBar.setColor(failed.get() ? BarColor.RED : BarColor.GREEN); + bossBar.setTitle(IrisLanguage.text( + failed.get() + ? RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_FAILED + : RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_READY, + MessageArgument.untrusted("world", worldName), + MessageArgument.trusted("percent", percent) + )); + + sender.sendAction(IrisLanguage.text( + failed.get() + ? RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_FAILED + : RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_READY, + MessageArgument.trusted("bar", buildPlayerBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + )); + + Runnable cleanup = () -> { + bossBar.removeAll(); + bossBar.setVisible(false); + }; + J.runEntity(sender.player(), cleanup, 60, cleanup); + } + + private boolean hasPlayerHud() { + return !hudDisabled + && sender.isPlayer() + && sender.player() != null + && bossBar != null; + } + + private void schedulePlayerRender(Runnable render) { + if (!playerRenderQueued.compareAndSet(false, true)) { + return; + } + Runnable guardedRender = () -> { + try { + render.run(); + } finally { + playerRenderQueued.set(false); + } + }; + if (J.runEntity(sender.player(), guardedRender)) { + return; + } + playerRenderQueued.set(false); + hudDisabled = true; + J.runGlobal(this::releaseHud); + } + + private void schedulePlayerTerminalRender(Runnable render) { + if (J.runEntity(sender.player(), render)) { + return; + } + hudDisabled = true; + J.runGlobal(this::releaseHud); + } + + private void releaseHud() { + BossBar activeBossBar = bossBar; + if (activeBossBar != null) { + activeBossBar.removeAll(); + activeBossBar.setVisible(false); + } + } + + private void cancel() { + int scheduledTaskId = taskId.getAndSet(-1); + if (scheduledTaskId >= 0) { + J.car(scheduledTaskId); + } + } + + private void requestTerminalRender() { + if (!J.runGlobal(this::tick)) { + cancel(); + } + } + + static double clampProgress(double progress) { + return Math.max(0.0D, Math.min(1.0D, progress)); + } + + static String buildPlayerBar(double progress) { + return buildBar(progress, PLAYER_BAR_WIDTH, true); + } + + static String buildConsoleBar(double progress) { + return buildBar(progress, CONSOLE_BAR_WIDTH, false); + } + + private static String buildBar(double progress, int width, boolean colored) { + int filled = (int) Math.round(clampProgress(progress) * width); + StringBuilder bar = new StringBuilder(colored ? width * 3 + 4 : width + 2); + if (colored) { + bar.append(C.DARK_GRAY); + } + bar.append("["); + for (int index = 0; index < width; index++) { + if (colored) { + bar.append(index < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } else { + bar.append(index < filled ? "#" : "-"); + } + } + if (colored) { + bar.append(C.DARK_GRAY); + } + return bar.append("]").toString(); + } + + static TextKey stageKey(String stage) { + if (stage == null || stage.isBlank()) { + return RuntimeProgressMessages.WORLD_CREATE_STAGE_INITIALIZING; + } + return switch (stage) { + case "resolve_dimension" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_RESOLVE_DIMENSION; + case "validate_pack" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_VALIDATE_PACK; + case "prepare_world_pack" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_PREPARE_WORLD_PACK; + case "install_datapacks" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_INSTALL_DATAPACKS; + case "prepare_generator" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_PREPARE_GENERATOR; + case "create_world" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_CREATE_WORLD; + case "register_world" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_REGISTER_WORLD; + case "teleport_player" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_TELEPORT_PLAYER; + case "pregenerate" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_PREGENERATE; + case "finalize" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_FINALIZE; + case "complete" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_COMPLETE; + default -> RuntimeProgressMessages.WORLD_CREATE_STAGE_INITIALIZING; + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/WorldMaintenance.java b/core/src/main/java/art/arcane/iris/core/tools/WorldMaintenance.java new file mode 100644 index 000000000..1397be1d8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/WorldMaintenance.java @@ -0,0 +1,108 @@ +package art.arcane.iris.core.tools; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.mantle.MantleSliceRetention; +import art.arcane.iris.spi.IrisLogging; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public final class WorldMaintenance { + private static final Map worldMaintenanceDepth = new ConcurrentHashMap<>(); + private static final Map worldMaintenanceMantleBypassDepth = new ConcurrentHashMap<>(); + + private WorldMaintenance() { + } + + public static void beginWorldMaintenance(String worldName, String reason) { + beginWorldMaintenance(worldName, reason, false); + } + + public static void beginWorldMaintenance(String worldName, String reason, boolean bypassMantleStages) { + if (worldName == null) { + return; + } + + int depth = incrementDepth(worldMaintenanceDepth, worldName); + if (bypassMantleStages) { + incrementDepth(worldMaintenanceMantleBypassDepth, worldName); + } + if (IrisSettings.get().getGeneral().isDebug()) { + IrisLogging.info("World maintenance enter: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages); + } else { + IrisLogging.debug("World maintenance enter: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages); + } + } + + public static void endWorldMaintenance(String worldName, String reason) { + endWorldMaintenance(worldName, reason, false); + } + + public static void endWorldMaintenance(String worldName, String reason, boolean bypassMantleStages) { + if (worldName == null) { + return; + } + + if (!worldMaintenanceDepth.containsKey(worldName)) { + return; + } + + int depth = decrementDepth(worldMaintenanceDepth, worldName); + + // Only a bypass-begin's paired end releases bypass credit: a plain end overlapping a + // bypassing operation stole its credit and re-enabled mantle stages under it. + int bypassDepth = 0; + if (bypassMantleStages) { + bypassDepth = decrementDepth(worldMaintenanceMantleBypassDepth, worldName); + } + + if (IrisSettings.get().getGeneral().isDebug()) { + IrisLogging.info("World maintenance exit: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth); + } else { + IrisLogging.debug("World maintenance exit: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth); + } + } + + // Depth mutations run inside compute() so an identity-based remove can never clear a + // registration a concurrent begin just re-incremented. + private static int incrementDepth(Map depths, String worldName) { + return depths.compute(worldName, (key, current) -> { + AtomicInteger counter = current == null ? new AtomicInteger() : current; + counter.incrementAndGet(); + return counter; + }).get(); + } + + private static int decrementDepth(Map depths, String worldName) { + AtomicInteger remaining = depths.computeIfPresent( + worldName, (key, current) -> current.decrementAndGet() <= 0 ? null : current); + return remaining == null ? 0 : Math.max(0, remaining.get()); + } + + public static boolean isWorldMaintenanceActive(String worldName) { + if (worldName == null) { + return false; + } + + AtomicInteger counter = worldMaintenanceDepth.get(worldName); + return counter != null && counter.get() > 0; + } + + public static boolean isWorldMaintenanceBypassingMantleStages(String worldName) { + if (worldName == null) { + return false; + } + + AtomicInteger counter = worldMaintenanceMantleBypassDepth.get(worldName); + return counter != null && counter.get() > 0; + } + + public static void retainMantleDataForSlice(String className) { + MantleSliceRetention.retain(className); + } + + public static boolean isRetainingMantleDataForSlice(String className) { + return MantleSliceRetention.isRetained(className); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineBackgroundTasks.java b/core/src/main/java/art/arcane/iris/engine/EngineBackgroundTasks.java new file mode 100644 index 000000000..951d0354f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineBackgroundTasks.java @@ -0,0 +1,184 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static art.arcane.iris.engine.EngineShutdownSequence.appendFailure; +import static art.arcane.iris.engine.EngineShutdownSequence.propagate; + +/** + * Admission gate and drain tracker for the asynchronous work an {@link IrisEngine} schedules. + * Admission is closed before any lifecycle transition so a draining engine can never accrue new + * background work, and every tracked task is awaited (or cancelled) before resources are released. + */ +final class EngineBackgroundTasks { + private static final long BACKGROUND_TASK_TIMEOUT_MILLIS = 15000L; + + private final Object backgroundTaskLock = new Object(); + private final List backgroundTasks = new ArrayList<>(); + private boolean backgroundTaskAdmission; + + boolean scheduleTrackedTask(Runnable task) { + synchronized (backgroundTaskLock) { + // A finished task is not outstanding work, however it finished. Retaining failed + // entries made the NEXT transition's drain re-report a long-settled failure and + // blocked close() from ever marking the engine closed. + backgroundTasks.removeIf(tracked -> tracked.completion.isDone()); + if (!backgroundTaskAdmission) { + return false; + } + TrackedBackgroundTask tracked = new TrackedBackgroundTask(); + Future future = J.a(() -> { + tracked.started.set(true); + try { + task.run(); + tracked.completion.complete(null); + return null; + } catch (Throwable exception) { + tracked.completion.completeExceptionally(exception); + // J.a(Callable) routes through a future nobody reads; report here or the + // failure is invisible. + IrisLogging.reportError(exception); + IrisLogging.error("Iris background task failed."); + throw propagate(exception); + } + }); + if (future == null) { + throw new IllegalStateException("Iris background task scheduler returned no task handle."); + } + tracked.future = future; + backgroundTasks.add(tracked); + return true; + } + } + + BackgroundTaskDrain drainBackgroundTasks(String reason) { + List tasks; + synchronized (backgroundTaskLock) { + tasks = List.copyOf(backgroundTasks); + } + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(BACKGROUND_TASK_TIMEOUT_MILLIS); + Throwable failure = null; + // Settled-set snapshot taken ONCE at drain entry: a per-iteration isDone() sample + // would also suppress tasks that failed while the drain was blocked on an earlier + // task, letting a real in-flight failure pass the transition unreported. + Set settledAtEntry = Collections.newSetFromMap(new IdentityHashMap<>()); + for (TrackedBackgroundTask task : tasks) { + if (task.completion.isDone()) { + settledAtEntry.add(task); + } + } + for (TrackedBackgroundTask task : tasks) { + long remaining = deadline - System.nanoTime(); + if (remaining <= 0L) { + cancelBackgroundTask(task, reason); + failure = appendFailure(failure, new TimeoutException("Timed out waiting for Iris background tasks during " + reason + ".")); + continue; + } + boolean alreadyDone = settledAtEntry.contains(task); + try { + task.completion.get(remaining, TimeUnit.NANOSECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + cancelBackgroundTask(task, reason); + failure = appendFailure(failure, e); + } catch (ExecutionException | TimeoutException e) { + cancelBackgroundTask(task, reason); + // A task that had settled before this drain began was already reported when it + // failed; only genuinely in-flight failures may poison this transition. + if (!alreadyDone) { + failure = appendFailure(failure, e); + } else { + IrisLogging.debug("Ignoring pre-settled background task failure during " + reason + "."); + } + } + } + boolean complete; + synchronized (backgroundTaskLock) { + backgroundTasks.removeIf(tracked -> tracked.completion.isDone()); + complete = backgroundTasks.isEmpty(); + } + return new BackgroundTaskDrain(failure, complete); + } + + private void cancelBackgroundTask(TrackedBackgroundTask task, String reason) { + Future future = task.future; + if (future != null && future.cancel(true) && !task.started.get()) { + task.completion.completeExceptionally( + new IllegalStateException("Iris background task was cancelled before starting during " + reason + ".")); + } + } + + void openBackgroundTaskAdmission() { + synchronized (backgroundTaskLock) { + backgroundTaskAdmission = true; + } + } + + void closeBackgroundTaskAdmission() { + synchronized (backgroundTaskLock) { + backgroundTaskAdmission = false; + } + } + + void cancelBackgroundTasks(String reason) { + List tasks; + synchronized (backgroundTaskLock) { + tasks = List.copyOf(backgroundTasks); + } + for (TrackedBackgroundTask task : tasks) { + cancelBackgroundTask(task, reason); + } + } + + private static final class TrackedBackgroundTask { + private final AtomicBoolean started = new AtomicBoolean(); + private final CompletableFuture completion = new CompletableFuture<>(); + private volatile Future future; + } + + record BackgroundTaskDrain(Throwable failure, boolean complete) { + boolean allowsResourceRelease() { + return complete; + } + + void requireComplete(String reason) { + if (failure != null) { + throw new IllegalStateException("Iris background tasks failed to drain during " + reason + ".", failure); + } + if (!complete) { + throw new IllegalStateException("Iris background tasks remain active during " + reason + "."); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineDataStore.java b/core/src/main/java/art/arcane/iris/engine/EngineDataStore.java new file mode 100644 index 000000000..84d54756b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineDataStore.java @@ -0,0 +1,185 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.lifecycle.VanishedWorldStorage; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisEngineData; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.io.IO; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * On-disk persistence for a single engine's {@link IrisEngineData}. Loads are double-checked against + * the engine's volatile field and serialized on a dedicated lock, and every write goes through an + * atomic temp-file move so a crash mid-save can never truncate the live engine data. + */ +final class EngineDataStore { + private static final String ENGINE_DATA_DIRECTORY = "iris/engine-data"; + private final IrisEngine engine; + private final Object engineDataLock = new Object(); + /** + * Set once this store has written {@code iris/engine-data} into the world folder. From then on that + * directory going missing is a delete, whether or not the server has already put the world folder back. + */ + private volatile boolean engineDataEstablished; + + EngineDataStore(IrisEngine engine) { + this.engine = engine; + } + + IrisEngineData getEngineData() { + IrisEngineData loaded = engine.engineData; + if (loaded != null) { + return loaded; + } + synchronized (engineDataLock) { + loaded = engine.engineData; + if (loaded != null) { + return loaded; + } + File f = engineDataFile(); + if (f.exists()) { + engineDataEstablished = true; + try { + loaded = new Gson().fromJson(IO.readAll(f), IrisEngineData.class); + if (loaded == null) { + throw new IllegalStateException("Engine data file contains no JSON object: " + f.getAbsolutePath()); + } + } catch (IOException | JsonParseException e) { + IrisLogging.reportError(e); + e.printStackTrace(); + throw new IllegalStateException("Failed to read Iris engine data without modifying it: " + f.getAbsolutePath(), e); + } + } + + if (loaded == null) { + loaded = new IrisEngineData(); + loaded.getStatistics().setVersion(IrisPlatforms.get().irisVersionNumber()); + loaded.getStatistics().setMCVersion(IrisPlatforms.get().minecraftVersionNumber()); + loaded.getStatistics().setUpgradedVersion(IrisPlatforms.get().irisVersionNumber()); + if (loaded.getStatistics().getVersion() == -1 || loaded.getStatistics().getMCVersion() == -1) { + IrisLogging.error("Failed to setup Engine Data!"); + } + if (!storageVanished()) { + try { + writeEngineDataAtomically(f, loaded); + engineDataEstablished = true; + } catch (IOException e) { + IrisLogging.reportError(e); + e.printStackTrace(); + throw new IllegalStateException("Failed to create Iris engine data: " + f.getAbsolutePath(), e); + } + } + } + engine.engineData = loaded; + return loaded; + } + } + + /** + * True when this engine's world storage is gone, which stops persistence rather than letting the write + * rebuild the tree it is supposed to be writing into. + *

+ * The world folder existing is not enough. A {@code save-all} after a hot delete writes the level's own + * {@code data/*.dat} files back and recreates the folder, and Iris' {@code WorldSaveEvent} handler runs + * after that, so a folder check alone lets the save rebuild {@code iris/engine-data} - which is exactly + * the directory the next boot's storage audit reads as "this is an Iris world whose pack snapshot broke". + * Once this store has written that directory, its absence is the delete. + */ + private boolean storageVanished() { + File worldFolder = engine.getWorld().worldFolder(); + if (!engineDataEstablished) { + return VanishedWorldStorage.vanished(worldFolder); + } + return VanishedWorldStorage.vanished(worldFolder, new File(worldFolder, ENGINE_DATA_DIRECTORY)); + } + + private File engineDataFile() { + return new File( + engine.getWorld().worldFolder(), + ENGINE_DATA_DIRECTORY + "/" + engine.getDimension().getLoadKey() + ".json"); + } + + void saveEngineData() { + synchronized (engineDataLock) { + if (storageVanished()) { + return; + } + File f = engineDataFile(); + try { + writeEngineDataAtomically(f, engine.getEngineData()); + engineDataEstablished = true; + IrisLogging.debug("Saved Engine Data"); + } catch (IOException e) { + IrisLogging.error("Failed to save Engine Data"); + IrisLogging.reportError(e); + e.printStackTrace(); + throw new IllegalStateException("Failed to save Iris engine data: " + f.getAbsolutePath(), e); + } + } + } + + void releaseEngineData() { + IrisData data = engine.getData(); + data.unregisterEngine(engine); + if (data.getEngines().isEmpty()) { + data.close(); + data.clearLists(); + } + } + + static void writeEngineDataAtomically(File file, IrisEngineData data) throws IOException { + Path output = file.toPath(); + Path parent = output.getParent(); + if (parent == null) { + throw new IOException("Engine data path has no parent: " + output); + } + // /iris/engine-data/.json: the world folder itself is never created here, so a + // deleted world cannot be rebuilt by a save that races the guard in saveEngineData. + Path irisRoot = parent.getParent(); + Path worldFolder = irisRoot == null ? null : irisRoot.getParent(); + if (worldFolder == null || !Files.isDirectory(worldFolder, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Iris world storage is missing: " + worldFolder); + } + Files.createDirectories(parent); + Path temporary = Files.createTempFile(parent, output.getFileName().toString(), ".tmp"); + try { + Files.writeString(temporary, new Gson().toJson(data), StandardCharsets.UTF_8); + try { + Files.move(temporary, output, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(temporary, output, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(temporary); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java b/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java new file mode 100644 index 000000000..7357f1c69 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java @@ -0,0 +1,205 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.protocol.IrisProtocolServer; +import art.arcane.iris.engine.EngineRuntime.BiomeMaxes; +import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly; +import art.arcane.iris.engine.IrisEngine.LifecycleState; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.IrisStructureLocator; +import art.arcane.iris.engine.framework.StructureReachability; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.localization.MessageArgument; + +import static art.arcane.iris.engine.EngineShutdownSequence.runCleanup; + +/** + * Performs the two supported live reload transitions for an {@link IrisEngine}: a full pack hotload + * and a biome-complex-only rebuild. Both run entirely under the engine lifecycle lock, seal and + * drain generation before swapping anything, and roll the previous runtime back on failure. + */ +final class EngineHotloader { + private final IrisEngine engine; + + EngineHotloader(IrisEngine engine) { + this.engine = engine; + } + + void hotload() { + hotloadSilently(); + engine.getPlatformHooks().fireHotloadEvent(engine); + } + + void hotloadComplex() { + synchronized (engine.lifecycleLock) { + engine.requireRunning("rebuild the biome complex"); + engine.awaitNativeStructureBootstrap("complex hotload"); + engine.lifecycleState = LifecycleState.HOTLOADING; + EngineRuntime previous = engine.runtime; + IrisComplex nextComplex = null; + try { + engine.sealForTransition("complex hotload", false); + RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target()); + engine.runtimeAssembly.set(assembly); + EngineRuntime next; + try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { + assembly.complex = new IrisComplex(engine); + nextComplex = assembly.complex; + assembly.upperContext = engine.runtimeBuilder.buildUpperContext(); + BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes(); + next = previous.withComplex(assembly.cacheId, assembly.complex, assembly.upperContext, biomeMaxes); + } finally { + engine.runtimeAssembly.remove(); + } + Throwable retirementFailure = runCleanup(null, previous.complex()::close); + if (retirementFailure != null) { + retirementFailure = runCleanup(retirementFailure, nextComplex::close); + nextComplex = null; + engine.lifecycleState = LifecycleState.FAILED; + throw new IllegalStateException("Failed to retire the previous Iris biome complex.", retirementFailure); + } + engine.runtime = next; + engine.getGenerationSessions().activateNextSession(); + engine.lifecycleState = LifecycleState.RUNNING; + engine.getClosing().set(false); + engine.backgroundTasks.openBackgroundTaskAdmission(); + } catch (Throwable e) { + if (nextComplex != null && nextComplex != previous.complex()) { + Throwable cleanupFailure = runCleanup(null, nextComplex::close); + if (cleanupFailure != null) { + e.addSuppressed(cleanupFailure); + } + } + if (engine.lifecycleState != LifecycleState.FAILED) { + engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous); + } + throw new IllegalStateException("Failed to rebuild the Iris biome complex.", e); + } + } + } + + void hotloadSilently() { + synchronized (engine.lifecycleLock) { + engine.requireRunning("hotload"); + engine.awaitNativeStructureBootstrap("hotload"); + engine.lifecycleState = LifecycleState.HOTLOADING; + EngineRuntime previousRuntime = engine.runtime; + IrisDimension previousDimension = engine.getDimension(); + IrisData previousData = engine.getData(); + IrisData replacementData = null; + boolean published = false; + try { + engine.sealForTransition("hotload", false); + replacementData = IrisData.openRuntime(previousData.getDataFolder()); + IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey()); + if (replacement == null) { + throw new IllegalStateException("Studio hotload could not reload Iris dimension '" + previousDimension.getLoadKey() + "'"); + } + engine.getPlatformHooks().validateDimensionHotload(engine, replacement); + replacementData.registerEngine(engine); + IrisStructureLocator.invalidate(engine); + StructureReachability.invalidate(engine); + EngineTarget replacementTarget = new EngineTarget(engine.getWorld(), replacement, replacementData); + EngineRuntime nextRuntime = engine.runtimeBuilder.buildRuntime(replacementTarget); + engine.runtimeBuilder.publishRuntime(nextRuntime, previousRuntime); + published = true; + previousData.unregisterEngine(engine); + Throwable previousDataFailure = runCleanup(null, previousData::close); + if (previousDataFailure != null) { + IrisLogging.error("Failed to completely release the previous Iris data runtime."); + IrisLogging.reportError(previousDataFailure); + previousDataFailure.printStackTrace(); + } + engine.getPrefetchSaveStarted().set(false); + engine.getEngineData().getStatistics().hotloaded(); + if (engine.getWorld().hasPlatformWorld()) { + if (!engine.backgroundTasks.scheduleTrackedTask(() -> { + engine.getPlatformHooks().refreshWorkspace(engine); + engine.getPlatformHooks().reloadDatapacks(engine); + })) { + throw new IllegalStateException("Iris background task admission closed before workspace refresh."); + } + } + broadcastStudioHotload(false, ""); + } catch (Throwable e) { + if (!published) { + if (replacementData != null) { + replacementData.unregisterEngine(engine); + Throwable replacementDataFailure = runCleanup(null, replacementData::close); + if (replacementDataFailure != null) { + e.addSuppressed(replacementDataFailure); + } + } + IrisStructureLocator.invalidate(engine); + StructureReachability.invalidate(engine); + if (engine.lifecycleState != LifecycleState.FAILED) { + Throwable rollbackFailure = runCleanup(null, engine.getMantle()::hotload); + if (rollbackFailure == null) { + engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previousRuntime); + } else { + engine.runtime = previousRuntime; + engine.lifecycleState = LifecycleState.FAILED; + e.addSuppressed(rollbackFailure); + } + } + } + broadcastStudioHotload(true, e.getClass().getSimpleName() + ": " + e.getMessage()); + if (e instanceof Error error) { + throw error; + } + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException("Iris hotload failed.", e); + } + } + } + + /** + * Never throws. It is called from the hotload failure handler, where a frame-cap + * {@code IllegalStateException} out of the codec (a long pack key or exception message overruns + * MAX_FRAME_BYTES) would otherwise propagate in place of the real hotload error and lose it. + */ + private void broadcastStudioHotload(boolean failed, String message) { + try { + IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class); + if (protocolServer == null) { + return; + } + IrisDimension dimension = engine.getDimension(); + String packKey = dimension == null ? "" : dimension.getLoadKey(); + protocolServer.broadcastStudioHotload(packKey, 0, failed, message); + protocolServer.broadcastToast( + failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS, + IrisLanguage.plain(ClientUiMessages.TOAST_STUDIO_HOTLOAD), + failed ? IrisLanguage.plain(ClientUiMessages.TOAST_PACK_FAILED, MessageArgument.untrusted("pack", packKey)) : packKey); + } catch (Throwable broadcastFailure) { + IrisLogging.error("Iris studio hotload broadcast failed: " + broadcastFailure.getClass().getSimpleName() + + ": " + broadcastFailure.getMessage()); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java b/core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java new file mode 100644 index 000000000..ce4c5e190 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java @@ -0,0 +1,112 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.math.M; + +/** + * Derives human readable timing reports and the rolling generation rate for an {@link IrisEngine}. + * Purely a reader of the engine's metric accumulators; it never mutates generation state. + */ +final class EngineMetricsReport { + private final IrisEngine engine; + + EngineMetricsReport(IrisEngine engine) { + this.engine = engine; + } + + void printMetrics(VolmitSender sender) { + KMap totals = new KMap<>(); + KMap weights = new KMap<>(); + double masterWallClock = engine.getWallClock().getAverage(); + KMap timings = engine.getMetrics().pull(); + double totalWeight = 0; + double wallClock = engine.getMetrics().getTotal().getAverage(); + + for (double j : timings.values()) { + totalWeight += j; + } + + for (String j : timings.k()) { + weights.put(engine.getName() + "." + j, (wallClock / totalWeight) * timings.get(j)); + } + + totals.put(engine.getName(), wallClock); + + double mtotals = 0; + + for (double i : totals.values()) { + mtotals += i; + } + + for (String i : totals.k()) { + totals.put(i, (masterWallClock / mtotals) * totals.get(i)); + } + + double v = 0; + + for (double i : weights.values()) { + v += i; + } + + for (String i : weights.k()) { + weights.put(i, weights.get(i) / v); + } + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_TOTAL, MessageArgument.untrusted("value", String.valueOf(Form.duration(masterWallClock, 0))))); + + for (String i : totals.k()) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_ENGINE, MessageArgument.untrusted("i", String.valueOf(i)), MessageArgument.untrusted("value", String.valueOf(Form.duration(totals.get(i), 0))))); + } + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_DETAILS)); + + for (String i : weights.sortKNumber().reverse()) { + String befb = C.UNDERLINE + "" + C.GREEN + "" + i.split("\\Q[\\E")[0] + C.RESET + C.GRAY + "["; + String num = C.GOLD + i.split("\\Q[\\E")[1].split("]")[0] + C.RESET + C.GRAY + "]."; + String afb = C.ITALIC + "" + C.AQUA + i.split("\\Q]\\E")[1].substring(1) + C.RESET + C.GRAY; + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_MESSAGE, MessageArgument.untrusted("befb", String.valueOf(befb)), MessageArgument.untrusted("num", String.valueOf(num)), MessageArgument.untrusted("afb", String.valueOf(afb)), MessageArgument.untrusted("value", String.valueOf(Form.pc(weights.get(i), 0))))); + } + } + + double getGeneratedPerSecond() { + if (engine.getPerSecondLatch().flip()) { + double g = engine.getGenerated() - engine.getGeneratedLast().get(); + engine.getGeneratedLast().set(engine.getGenerated()); + + if (g == 0) { + return 0; + } + + long dur = M.ms() - engine.getLastGPS().get(); + engine.getLastGPS().set(M.ms()); + engine.getPerSecond().set(g / ((double) (dur) / 1000D)); + } + + return engine.getPerSecond().get(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EnginePanic.java b/core/src/main/java/art/arcane/iris/engine/EnginePanic.java new file mode 100644 index 000000000..0e82c5c97 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EnginePanic.java @@ -0,0 +1,105 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.spi.IrisLogging; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +public final class EnginePanic { + private static final Diagnostics GLOBAL = scoped("global"); + + private EnginePanic() { + } + + public static Diagnostics scoped(String scope) { + return new Diagnostics(scope); + } + + public static void add(String key, String value) { + GLOBAL.add(key, value); + } + + public static void saveLast() { + GLOBAL.saveLast(); + } + + public static void lastPanic() { + GLOBAL.lastPanic(); + } + + public static void panic() { + GLOBAL.panic(); + } + + public static final class Diagnostics { + private final String scope; + private final ThreadLocal> current = ThreadLocal.withInitial(LinkedHashMap::new); + private final AtomicReference> last = new AtomicReference<>(Map.of()); + + private Diagnostics(String scope) { + this.scope = scope == null || scope.isBlank() ? "unknown" : scope; + } + + public void add(String key, String value) { + current.get().put(String.valueOf(key), String.valueOf(value)); + } + + public void saveLast() { + last.set(currentSnapshot()); + current.remove(); + } + + public void lastPanic() { + log("Last Panic", last.get()); + } + + public void panic() { + Map currentSnapshot = currentSnapshot(); + lastPanic(); + log("Engine Panic", currentSnapshot); + current.remove(); + } + + Map currentSnapshot() { + return immutableSnapshot(current.get()); + } + + Map lastSnapshot() { + return last.get(); + } + + private void log(String prefix, Map snapshot) { + for (Map.Entry entry : snapshot.entrySet()) { + IrisLogging.error(prefix + " [" + scope + "] " + entry.getKey() + ": " + entry.getValue()); + } + } + + private static Map immutableSnapshot(Map values) { + if (values.isEmpty()) { + return Map.of(); + } + + return Collections.unmodifiableMap(new LinkedHashMap<>(values)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineRuntime.java b/core/src/main/java/art/arcane/iris/engine/EngineRuntime.java new file mode 100644 index 000000000..1686edc9d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineRuntime.java @@ -0,0 +1,63 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.engine.framework.EngineEffects; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.EngineWorldManager; + +import java.util.concurrent.CompletableFuture; + +/** + * Immutable snapshot of everything an {@link IrisEngine} publishes as a single unit. + * Instances are built by {@link EngineRuntimeBuilder} and retired by {@link EngineShutdownSequence}. + */ +record EngineRuntime( + int cacheId, + EngineTarget target, + IrisComplex complex, + UpperDimensionContext upperContext, + EngineEffects effects, + EngineMode mode, + EngineWorldManager worldManager, + CompletableFuture hash32, + BiomeMaxes biomeMaxes +) { + EngineRuntime withComplex( + int nextCacheId, + IrisComplex nextComplex, + UpperDimensionContext nextUpperContext, + BiomeMaxes nextBiomeMaxes + ) { + return new EngineRuntime( + nextCacheId, + target, + nextComplex, + nextUpperContext, + effects, + mode, + worldManager, + hash32, + nextBiomeMaxes); + } + + record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java b/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java new file mode 100644 index 000000000..872382230 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java @@ -0,0 +1,328 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.EngineRuntime.BiomeMaxes; +import art.arcane.iris.engine.IrisEngine.LifecycleState; +import art.arcane.iris.engine.framework.EngineEffects; +import art.arcane.iris.engine.framework.EngineEffectsProvider; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.EngineWorldManager; +import art.arcane.iris.engine.framework.EngineWorldManagerProvider; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomePaletteLayer; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionMode; +import art.arcane.iris.engine.object.IrisDimensionModeType; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.math.M; + +import java.io.File; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; + +import static art.arcane.iris.engine.EngineShutdownSequence.propagate; + +/** + * Assembles and publishes {@link EngineRuntime} snapshots for an {@link IrisEngine}. + * A runtime is built into a thread-local {@link RuntimeAssembly} so partially constructed state is + * visible only to the building thread, then frozen and published to the engine's volatile runtime + * field as a single atomic swap. + */ +final class EngineRuntimeBuilder { + private final IrisEngine engine; + + EngineRuntimeBuilder(IrisEngine engine) { + this.engine = engine; + } + + EngineRuntime buildRuntime() { + return buildRuntime(engine.getTarget()); + } + + EngineRuntime buildRuntime(EngineTarget runtimeTarget) { + RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), runtimeTarget); + engine.runtimeAssembly.set(assembly); + try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { + IrisLogging.debug("Setup Engine " + assembly.cacheId); + long started = M.ms(); + assembly.complex = new IrisComplex(engine); + IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms"); + started = M.ms(); + assembly.upperContext = buildUpperContext(); + IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms"); + started = M.ms(); + assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(engine); + if (assembly.effects == null) { + throw new IllegalStateException("Engine effects provider returned null"); + } + IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms"); + assembly.hash32 = new CompletableFuture<>(); + started = M.ms(); + engine.getMantle().hotload(); + IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms"); + started = M.ms(); + assembly.mode = createMode(); + IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms"); + started = M.ms(); + assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine); + if (assembly.worldManager == null) { + throw new IllegalStateException("Engine world manager provider returned null"); + } + IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms"); + BiomeMaxes biomeMaxes = computeBiomeMaxes(); + return assembly.freeze(biomeMaxes); + } catch (Throwable e) { + Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e); + if (cleanupFailure != e) { + e.addSuppressed(cleanupFailure); + } + throw new IllegalStateException("Failed to build a complete Iris engine runtime.", e); + } finally { + engine.runtimeAssembly.remove(); + } + } + + void publishRuntime(EngineRuntime next, EngineRuntime previous) { + Throwable retirementFailure = engine.shutdownSequence.closeRuntime(previous, null); + if (retirementFailure != null) { + retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure); + engine.lifecycleState = LifecycleState.FAILED; + throw new IllegalStateException("Failed to retire the previous Iris engine runtime.", retirementFailure); + } + if (engine.runtime == previous) { + engine.runtime = null; + } + + engine.runtime = next; + engine.publishedTarget = next.target(); + engine.getGenerationSessions().activateNextSession(); + engine.lifecycleState = LifecycleState.RUNNING; + engine.getClosing().set(false); + engine.backgroundTasks.openBackgroundTaskAdmission(); + try { + next.worldManager().start(); + } catch (Throwable e) { + engine.getClosing().set(true); + engine.backgroundTasks.closeBackgroundTaskAdmission(); + engine.lifecycleState = LifecycleState.FAILED; + try { + engine.getGenerationSessions().sealAndAwait( + "failed world manager start", + IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS, + true + ); + } catch (Throwable drainFailure) { + e.addSuppressed(drainFailure); + } + engine.shutdownSequence.closeRuntime(next, e); + if (engine.runtime == next) { + engine.runtime = null; + } + throw new IllegalStateException("Failed to start the Iris world manager.", e); + } + scheduleRuntimeTasks(next); + IrisLogging.debug("Engine Setup Complete " + next.cacheId()); + } + + private void scheduleRuntimeTasks(EngineRuntime engineRuntime) { + try { + if (!engine.backgroundTasks.scheduleTrackedTask(() -> { + try { + File[] roots = engine.getData().getLoaders() + .values() + .stream() + .map(ResourceLoader::getFolderName) + .map(name -> new File(engine.getData().getDataFolder(), name)) + .filter(File::exists) + .filter(File::isDirectory) + .toArray(File[]::new); + engineRuntime.hash32().complete(IO.hashRecursiveMeta(roots)); + } catch (Throwable e) { + engineRuntime.hash32().completeExceptionally(e); + throw propagate(e); + } + })) { + throw new IllegalStateException("Iris background task admission closed before pack hashing."); + } + } catch (Throwable e) { + engineRuntime.hash32().completeExceptionally(e); + IrisLogging.reportError(e); + e.printStackTrace(); + } + try { + if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) { + throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh."); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + + UpperDimensionContext buildUpperContext() { + IrisDimension dim = engine.getDimension(); + if (!dim.hasUpperDimension()) { + return null; + } + String upperKey = dim.getUpperDimension(); + IrisDimension upperDim = upperKey.equals(dim.getLoadKey()) + ? dim + : IrisData.loadAnyDimension(upperKey, engine.getData()); + if (upperDim != null) { + UpperDimensionContext ctx = UpperDimensionContext.create(engine, upperDim); + IrisLogging.info("Upper dimension enabled: " + upperKey + + (ctx.isSelfReferencing() ? " (self-referencing)" : " (cross-referencing)")); + return ctx; + } + IrisLogging.warn("Upper dimension '" + upperKey + "' could not be resolved, skipping upper terrain."); + return null; + } + + private EngineMode createMode() { + Throwable configuredFailure = null; + try { + IrisDimensionMode configuredMode = engine.getDimension().getMode(); + if (configuredMode == null) { + configuredMode = new IrisDimensionMode(); + engine.getDimension().setMode(configuredMode); + } + EngineMode configured = configuredMode.create(engine); + if (configured == null) { + throw new IllegalStateException("Dimension mode factory returned null"); + } + return configured; + } catch (Throwable e) { + configuredFailure = e; + IrisLogging.reportError(e); + e.printStackTrace(); + if (engine.getModeFallbackLogged().compareAndSet(false, true)) { + IrisLogging.warn("Failed to initialize configured dimension mode for " + engine.getDimension().getLoadKey() + ", falling back to OVERWORLD mode."); + } + } + + try { + EngineMode fallback = IrisDimensionModeType.OVERWORLD.create(engine); + if (fallback == null) { + throw new IllegalStateException("OVERWORLD mode factory returned null"); + } + return fallback; + } catch (Throwable fallbackFailure) { + fallbackFailure.addSuppressed(configuredFailure); + throw new IllegalStateException("Both configured and fallback Iris engine modes failed.", fallbackFailure); + } + } + + BiomeMaxes computeBiomeMaxes() { + double objectDensity = 0D; + double layerDensity = 0D; + double decoratorDensity = 0D; + for (IrisBiome i : engine.getDimension().getReachableBiomes(engine)) { + double density = 0; + + for (IrisObjectPlacement j : i.getObjects()) { + density += j.getDensity() * j.getChance(); + } + + objectDensity = Math.max(objectDensity, density); + density = 0; + + for (IrisDecorator j : i.getDecorators()) { + density += Math.max(j.getStackMax(), 1) * j.getChance(); + } + + decoratorDensity = Math.max(decoratorDensity, density); + density = 0; + + for (IrisBiomePaletteLayer j : i.getLayers()) { + density++; + } + + layerDensity = Math.max(layerDensity, density); + } + return new BiomeMaxes(objectDensity, layerDensity, decoratorDensity); + } + + void restoreRuntimeAfterFailedTransition(EngineRuntime previous) { + engine.runtime = previous; + if (engine.getGenerationSessions().activeLeases() == 0) { + engine.getGenerationSessions().activateNextSession(); + engine.lifecycleState = LifecycleState.RUNNING; + engine.getClosing().set(false); + engine.backgroundTasks.openBackgroundTaskAdmission(); + return; + } + engine.lifecycleState = LifecycleState.FAILED; + } + + EngineRuntime requireRuntime(String operation) { + EngineRuntime current = engine.runtime; + if (current == null) { + throw new IllegalStateException("Cannot " + operation + " without an active Iris runtime for " + + engine.getWorld().name() + "."); + } + return current; + } + + static final class RuntimeAssembly { + private static final AtomicInteger RUNTIME_IDS = new AtomicInteger(); + + final int cacheId; + final EngineTarget target; + + static int nextRuntimeId() { + return RUNTIME_IDS.incrementAndGet(); + } + IrisComplex complex; + UpperDimensionContext upperContext; + EngineEffects effects; + EngineMode mode; + EngineWorldManager worldManager; + CompletableFuture hash32; + + RuntimeAssembly(int cacheId, EngineTarget target) { + this.cacheId = cacheId; + this.target = target; + } + + EngineRuntime freeze(BiomeMaxes biomeMaxes) { + if (complex == null || effects == null || mode == null || worldManager == null || hash32 == null) { + throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime."); + } + return new EngineRuntime( + cacheId, + target, + complex, + upperContext, + effects, + mode, + worldManager, + hash32, + biomeMaxes); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java b/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java new file mode 100644 index 000000000..64393913f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java @@ -0,0 +1,348 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain; +import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly; +import art.arcane.iris.engine.IrisEngine.LifecycleState; +import art.arcane.iris.engine.framework.GenerationSessionException; +import art.arcane.iris.engine.framework.NativeStructureOwnershipStore; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; + +/** + * Owns the ordered teardown of a single {@link IrisEngine}, both for a normal close and for a + * construction that failed part way through. Every resource release is gated on the previous one + * having succeeded so a partially released engine never reports itself as closed. + */ +final class EngineShutdownSequence { + private static final long CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS = 5000L; + + private final IrisEngine engine; + private boolean runtimeReleased; + private boolean targetReleased; + private boolean mantleReleased; + private boolean engineDataReleased; + private boolean preservationReleased; + + EngineShutdownSequence(IrisEngine engine) { + this.engine = engine; + } + + void close() { + Throwable failure; + synchronized (engine.lifecycleLock) { + if (engine.closed) { + return; + } + engine.awaitNativeStructureBootstrap("close"); + engine.lifecycleState = LifecycleState.CLOSING; + engine.getClosing().set(true); + engine.backgroundTasks.closeBackgroundTaskAdmission(); + EngineTickRegistry.unregisterTicking(engine); + // Best-effort like every other step: a pregen join timeout must not escape the + // synchronized block before anything is saved or released. On failure, save what + // can be saved and leave the close incomplete-but-retryable (a live pregen writer + // may still hold the mantle, so the releases are skipped). + Throwable pregenFailure = runCleanup(null, () -> engine.getPlatformHooks().shutdownPregenerator(engine)); + if (pregenFailure != null) { + engine.lifecycleState = LifecycleState.FAILED; + failure = pregenFailure; + failure = runCleanup(failure, engine::savePrefetchOnce); + failure = runCleanup(failure, engine::saveEngineData); + failure = runCleanup(failure, () -> engine.getMantle().saveAllNow()); + reportIncompleteClose(failure); + return; + } + Throwable drainFailure = null; + try { + engine.getGenerationSessions().sealAndAwait("close", IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS, true); + } catch (GenerationSessionException e) { + drainFailure = e; + } + if (drainFailure != null) { + // A drain timeout must not abandon teardown: the world manager is the lease + // producer, so stop it first, then re-drain briefly. + IrisLogging.warn("Iris generation did not drain for close on " + engine.getWorld().name() + + "; stopping the world manager and retrying."); + Throwable managerFailure = engine.runtime == null + ? null + : runCleanup(null, engine.runtime.worldManager()::close); + try { + engine.getGenerationSessions().sealAndAwait("close-retry", CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS, true); + drainFailure = null; + } catch (GenerationSessionException e) { + drainFailure = appendFailure(drainFailure, e); + } + drainFailure = appendFailure(drainFailure, managerFailure); + } + if (drainFailure != null) { + // A live lease may be mid-write, so the mantle must not be closed at it — but + // dirty plates can still be flushed (saveAll is synchronized) so terrain since + // the last periodic save is not lost. The close stays incomplete and retryable. + engine.lifecycleState = LifecycleState.FAILED; + failure = runCleanup(drainFailure, () -> engine.getMantle().saveAllNow()); + reportIncompleteClose(failure); + return; + } + + BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("close"); + failure = backgroundDrain.failure(); + if (!backgroundDrain.allowsResourceRelease() && failure == null) { + failure = new IllegalStateException("Iris background tasks remain active during close."); + } + + if (backgroundDrain.allowsResourceRelease()) { + Throwable ownershipFailure = runCleanup(null, + () -> NativeStructureOwnershipStore.close(engine)); + failure = appendFailure(failure, ownershipFailure); + if (ownershipFailure == null) { + Throwable prefetchFailure = runCleanup(null, engine::savePrefetchOnce); + Throwable engineDataFailure = runCleanup(null, engine::saveEngineData); + failure = appendFailure(failure, prefetchFailure); + failure = appendFailure(failure, engineDataFailure); + failure = releaseRuntime(failure); + if (runtimeReleased) { + failure = releaseTarget(failure); + } + if (targetReleased) { + failure = releaseMantle(failure); + } + if (prefetchFailure == null + && engineDataFailure == null + && runtimeReleased + && targetReleased + && mantleReleased) { + failure = releaseEngineDataForShutdown(failure); + } + if (engineDataReleased) { + failure = releasePreservation(failure); + } + } + } + if (failure == null + && runtimeReleased + && targetReleased + && mantleReleased + && engineDataReleased + && preservationReleased) { + engine.closed = true; + engine.lifecycleState = LifecycleState.CLOSED; + IrisLogging.debug("Engine Fully Shutdown!"); + } + } + if (failure != null) { + reportIncompleteClose(failure); + } + } + + private void reportIncompleteClose(Throwable failure) { + IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + "."); + IrisLogging.reportError(failure); + failure.printStackTrace(); + throw new IllegalStateException("Iris engine shutdown remains incomplete after cleanup failures.", failure); + } + + void cleanupFailedConstruction(Throwable original) { + engine.getClosing().set(true); + engine.backgroundTasks.closeBackgroundTaskAdmission(); + EngineTickRegistry.unregisterTicking(engine); + engine.lifecycleState = LifecycleState.FAILED; + Throwable cleanupFailure = null; + try { + engine.getGenerationSessions().sealAndAwait("failed initialization", 0L, true); + } catch (Throwable e) { + cleanupFailure = appendFailure(cleanupFailure, e); + } + engine.backgroundTasks.cancelBackgroundTasks("failed initialization"); + BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("failed initialization"); + cleanupFailure = appendFailure(cleanupFailure, backgroundDrain.failure()); + if (!backgroundDrain.allowsResourceRelease()) { + cleanupFailure = appendFailure(cleanupFailure, + new IllegalStateException("Iris background tasks remain active after failed initialization.")); + if (cleanupFailure != original) { + original.addSuppressed(cleanupFailure); + } + return; + } + Throwable ownershipFailure = runCleanup(null, + () -> NativeStructureOwnershipStore.close(engine)); + cleanupFailure = appendFailure(cleanupFailure, ownershipFailure); + if (ownershipFailure == null) { + cleanupFailure = closeRuntime(engine.runtime, cleanupFailure); + engine.runtime = null; + cleanupFailure = runCleanup(cleanupFailure, engine.getTarget()::close); + cleanupFailure = runCleanup(cleanupFailure, engine.getMantle()::close); + cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData); + engine.closed = true; + cleanupFailure = runCleanup(cleanupFailure, () -> { + PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class); + if (registry != null) { + registry.dereference(); + } + }); + } + if (cleanupFailure != null && cleanupFailure != original) { + original.addSuppressed(cleanupFailure); + } + } + + Throwable closeAssembly(RuntimeAssembly assembly, Throwable failure) { + if (assembly == null) { + return failure; + } + failure = runCleanup(failure, () -> { + if (assembly.worldManager != null) { + assembly.worldManager.close(); + } + }); + failure = runCleanup(failure, () -> { + if (assembly.effects != null) { + assembly.effects.close(); + } + }); + failure = runCleanup(failure, () -> { + if (assembly.mode != null) { + assembly.mode.close(); + } + }); + failure = runCleanup(failure, () -> { + if (assembly.complex != null) { + assembly.complex.close(); + } + }); + failure = runCleanup(failure, () -> { + if (assembly.hash32 != null) { + assembly.hash32.cancel(true); + } + }); + return failure; + } + + Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) { + if (engineRuntime == null) { + return failure; + } + failure = runCleanup(failure, engineRuntime.worldManager()::close); + failure = runCleanup(failure, engineRuntime.effects()::close); + failure = runCleanup(failure, engineRuntime.mode()::close); + failure = runCleanup(failure, engineRuntime.complex()::close); + failure = runCleanup(failure, () -> engineRuntime.hash32().cancel(true)); + return failure; + } + + private Throwable releaseRuntime(Throwable failure) { + if (runtimeReleased) { + return failure; + } + Throwable runtimeFailure = closeRuntime(engine.runtime, null); + if (runtimeFailure != null) { + return appendFailure(failure, runtimeFailure); + } + engine.runtime = null; + runtimeReleased = true; + return failure; + } + + private Throwable releaseTarget(Throwable failure) { + if (targetReleased) { + return failure; + } + Throwable targetFailure = runCleanup(null, engine.getTarget()::close); + if (targetFailure != null) { + return appendFailure(failure, targetFailure); + } + targetReleased = true; + return failure; + } + + private Throwable releaseMantle(Throwable failure) { + if (mantleReleased) { + return failure; + } + Throwable mantleFailure = runCleanup(null, engine.getMantle()::close); + if (mantleFailure != null) { + return appendFailure(failure, mantleFailure); + } + mantleReleased = true; + return failure; + } + + private Throwable releaseEngineDataForShutdown(Throwable failure) { + if (engineDataReleased) { + return failure; + } + Throwable dataFailure = runCleanup(null, engine.engineDataStore::releaseEngineData); + if (dataFailure != null) { + return appendFailure(failure, dataFailure); + } + engineDataReleased = true; + return failure; + } + + private Throwable releasePreservation(Throwable failure) { + if (preservationReleased) { + return failure; + } + Throwable preservationFailure = runCleanup(null, () -> { + PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class); + if (registry != null) { + registry.dereference(); + } + }); + if (preservationFailure != null) { + return appendFailure(failure, preservationFailure); + } + preservationReleased = true; + return failure; + } + + static Throwable runCleanup(Throwable failure, Runnable cleanup) { + try { + cleanup.run(); + } catch (Throwable e) { + return appendFailure(failure, e); + } + return failure; + } + + static Throwable appendFailure(Throwable failure, Throwable additional) { + if (additional == null) { + return failure; + } + if (failure == null) { + return additional; + } + if (failure != additional) { + failure.addSuppressed(additional); + } + return failure; + } + + static RuntimeException propagate(Throwable throwable) { + if (throwable instanceof RuntimeException runtimeException) { + return runtimeException; + } + if (throwable instanceof Error error) { + throw error; + } + return new IllegalStateException(throwable); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineTickRegistry.java b/core/src/main/java/art/arcane/iris/engine/EngineTickRegistry.java new file mode 100644 index 000000000..0c7f25d50 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineTickRegistry.java @@ -0,0 +1,75 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; + +/** + * Process-wide registry of live {@link IrisEngine} instances driven by one shared repeating task. + * The task is created on the first registration and cancelled once the last engine unregisters, so + * no ticking work runs when no engine is loaded. + */ +final class EngineTickRegistry { + private static final Object TICK_LOCK = new Object(); + private static final Set TICK_ENGINES = Collections.newSetFromMap(new IdentityHashMap<>()); + private static int sharedTickTask = -1; + + private EngineTickRegistry() { + } + + static void registerTicking(IrisEngine engine) { + synchronized (TICK_LOCK) { + TICK_ENGINES.add(engine); + if (sharedTickTask == -1) { + sharedTickTask = J.ar(EngineTickRegistry::tickEngines, 1); + } + } + } + + static void unregisterTicking(IrisEngine engine) { + synchronized (TICK_LOCK) { + TICK_ENGINES.remove(engine); + if (TICK_ENGINES.isEmpty() && sharedTickTask != -1) { + J.car(sharedTickTask); + sharedTickTask = -1; + } + } + } + + private static void tickEngines() { + List engines; + synchronized (TICK_LOCK) { + engines = List.copyOf(TICK_ENGINES); + } + for (IrisEngine engine : engines) { + try { + engine.tickRandomPlayer(); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java b/core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java new file mode 100644 index 000000000..235187313 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java @@ -0,0 +1,127 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisOreGenerator; +import art.arcane.iris.engine.object.IrisProceduralObjects; +import art.arcane.iris.engine.object.IrisProceduralPlacement; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; + +import java.util.Comparator; + +public final class GenerationCacheWarmer { + private GenerationCacheWarmer() { + } + + public static void warm(Engine engine) { + long start = M.ms(); + IrisData data = engine.getData(); + RNG root = new RNG(engine.getSeedManager().getComponent() + 7777L); + int[] counter = {0}; + + try { + KList biomes = engine.getAllBiomes(); + biomes.sort(Comparator.comparing(IrisBiome::getLoadKey)); + for (IrisBiome biome : biomes) { + warmPlacements(biome.getObjects(), root, counter, data, engine); + warmDecorators(biome.getDecorators(), root, counter, data); + warmOres(biome.getOres(), root, counter, data); + warmProcedural(biome.getProceduralObjects(), root, counter, data); + } + + KList regions = engine.getDimension().getAllRegions(engine); + regions.sort(Comparator.comparing(IrisRegion::getLoadKey)); + for (IrisRegion region : regions) { + warmPlacements(region.getObjects(), root, counter, data, engine); + warmOres(region.getOres(), root, counter, data); + warmProcedural(region.getProceduralObjects(), root, counter, data); + } + + warmOres(engine.getDimension().getOres(), root, counter, data); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Generation cache warm pass failed: " + e.getMessage()); + } + + IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms"); + } + + private static void warmPlacements(KList placements, RNG root, int[] counter, + IrisData data, Engine engine) { + if (placements == null) { + return; + } + for (IrisObjectPlacement placement : placements) { + if (placement == null) { + continue; + } + RNG rng = root.nextParallelRNG(counter[0]++); + placement.getSurfaceWarp(rng, data, engine); + placement.getDensity(rng, 0, 0, data); + } + } + + private static void warmDecorators(KList decorators, RNG root, int[] counter, IrisData data) { + if (decorators == null) { + return; + } + for (IrisDecorator decorator : decorators) { + if (decorator == null) { + continue; + } + RNG rng = root.nextParallelRNG(counter[0]++); + decorator.getHeightGenerator(rng, data); + decorator.getGenerator(rng, data); + decorator.getVarianceGenerator(rng, data); + } + } + + private static void warmOres(KList ores, RNG root, int[] counter, IrisData data) { + if (ores == null) { + return; + } + for (IrisOreGenerator ore : ores) { + if (ore == null) { + continue; + } + ore.warm(root.nextParallelRNG(counter[0]++), data); + } + } + + private static void warmProcedural(IrisProceduralObjects procedural, RNG root, int[] counter, IrisData data) { + if (procedural == null) { + return; + } + for (IrisProceduralPlacement placement : procedural.getAllPlacements()) { + if (placement == null) { + continue; + } + placement.getVariantObject(data, root.nextParallelRNG(counter[0]++)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java new file mode 100644 index 000000000..04aef3d5f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -0,0 +1,927 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.InferredType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.engine.object.IrisInterpolator; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisShapedGeneratorStyle; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.data.DataProvider; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.interpolation.NoiseBounds; +import art.arcane.iris.util.project.interpolation.NoiseBoundsProvider; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +@Data +@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"}) +@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"}) +public class IrisComplex implements DataProvider { + private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); + private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L); + private static final int GRID_BOUNDS_CACHE_SIZE = 8192; + private static final int HEIGHT_BOUNDS_GRID = 4; + @Getter(AccessLevel.NONE) + private final transient ThreadLocal gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new); + /** + * Immutable snapshot of {@link #generators} taken once at the end of construction, in the exact + * iteration order the map produces. The per-column height paths walk these arrays instead of + * allocating map/set iterators, and the frozen order keeps the floating point accumulation order + * identical to the map iteration it replaces. Mutating {@link #generators} after construction is + * not reflected here. + */ + @Getter(AccessLevel.NONE) + private final transient IrisInterpolator[] frozenInterpolators; + @Getter(AccessLevel.NONE) + private final transient IrisGenerator[][] frozenGenerators; + private RNG rng; + private double fluidHeight; + private IrisData data; + private Map> generators; + private ProceduralStream regionStream; + private ProceduralStream regionStyleStream; + private ProceduralStream regionIdentityStream; + private ProceduralStream regionIDStream; + private ProceduralStream bridgeStream; + private ProceduralStream landBiomeStream; + private ProceduralStream caveBiomeStream; + private ProceduralStream seaBiomeStream; + private ProceduralStream shoreBiomeStream; + private ProceduralStream baseBiomeStream; + private ProceduralStream baseBiomeIDStream; + private ProceduralStream trueBiomeStream; + private ProceduralStream trueBiomeDerivativeStream; + private ProceduralStream heightStream; + private ProceduralStream roundedHeighteightStream; + private ProceduralStream maxHeightStream; + private ProceduralStream overlayStream; + private ProceduralStream heightFluidStream; + private ProceduralStream slopeStream; + private ProceduralStream topSurfaceStream; + private ProceduralStream terrainSurfaceDecoration; + private ProceduralStream terrainCeilingDecoration; + private ProceduralStream terrainCaveSurfaceDecoration; + private ProceduralStream terrainCaveCeilingDecoration; + private ProceduralStream seaSurfaceDecoration; + private ProceduralStream seaFloorDecoration; + private ProceduralStream shoreSurfaceDecoration; + private ProceduralStream rockStream; + private ProceduralStream fluidStream; + private IrisBiome focusBiome; + private IrisRegion focusRegion; + private Map> generatorBounds; + private Set generatorBiomes; + // Copy-on-write: reads happen per column on every burst thread; the synchronizedMap + // monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map + // per insert is cheap. Identity keying is load-bearing (IrisBiome is mutable/value-hashed). + private volatile IdentityHashMap childSelectionPlans = new IdentityHashMap<>(); + private final Object childSelectionPlanLock = new Object(); + + public IrisComplex(Engine engine) { + this(engine, false); + } + + public IrisComplex(Engine engine, boolean simple) { + int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize(); + IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE); + UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes()); + this.rng = new RNG(engine.getSeedManager().getComplex()); + this.data = engine.getData(); + double height = engine.getMaxHeight(); + fluidHeight = engine.getDimension().getFluidHeight(); + generators = new HashMap<>(); + generatorBiomes = Collections.newSetFromMap(new IdentityHashMap<>()); + focusBiome = engine.getFocus(); + focusRegion = engine.getFocusRegion(); + Map> inferredStreams = new HashMap<>(); + + if (focusBiome != null) { + focusBiome = focusBiome.withInferredType(InferredType.LAND); + focusRegion = findRegion(focusBiome, engine); + } + + //@builder + if (focusRegion != null) { + prepareInferredBiomes(focusRegion); + focusRegion.getAllBiomes(this).forEach(this::registerGenerators); + } else { + engine.getDimension().getRegions().forEach(regionKey -> { + IrisRegion region = data.getRegionLoader().load(regionKey); + if (region == null) { + return; + } + prepareInferredBiomes(region); + region.getAllBiomes(this).forEach(this::registerGenerators); + }); + } + int interpolatorCount = generators.size(); + frozenInterpolators = new IrisInterpolator[interpolatorCount]; + frozenGenerators = new IrisGenerator[interpolatorCount][]; + int frozenIndex = 0; + for (Map.Entry> entry : generators.entrySet()) { + frozenInterpolators[frozenIndex] = entry.getKey(); + frozenGenerators[frozenIndex] = entry.getValue().toArray(new IrisGenerator[0]); + frozenIndex++; + } + generatorBounds = buildGeneratorBounds(engine); + KList overlayNoise = engine.getDimension().getOverlayNoise(); + overlayStream = overlayNoise.isEmpty() + ? ProceduralStream.ofDouble((x, z) -> 0.0D) + : ProceduralStream.ofDouble((x, z) -> { + double value = 0D; + + for (IrisShapedGeneratorStyle style : overlayNoise) { + value += style.get(rng, getData(), x, z); + } + + return value; + }); + rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() + .select(engine.getDimension().getRockPalette().getBlockData(data)); + fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream() + .select(engine.getDimension().getFluidPalette().getBlockData(data)); + regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream() + .zoom(engine.getDimension().getRegionZoom()); + regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE); + regionStream = focusRegion != null ? + ProceduralStream.of((x, z) -> focusRegion, + Interpolated.of(a -> 0D, a -> focusRegion)) + : regionStyleStream + .selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions())) + .cache2D("regionStream", engine, cacheSize); + regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i), + String.valueOf(i * 38445).hashCode() * 3245556666L)); + caveBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z)) + .convert((r) + -> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream() + .zoom(engine.getDimension().getBiomeZoom()) + .zoom(r.getCaveBiomeZoom()) + .selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE)) + .onNull(emptyBiome) + ).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize); + inferredStreams.put(InferredType.CAVE, caveBiomeStream); + landBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z)) + .convert((r) + -> engine.getDimension().getLandBiomeStyle().create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream() + .zoom(engine.getDimension().getBiomeZoom()) + .zoom(engine.getDimension().getLandZoom()) + .zoom(r.getLandBiomeZoom()) + .selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND)) + ).convertAware2D(ProceduralStream::get) + .cache2D("landBiomeStream", engine, cacheSize); + inferredStreams.put(InferredType.LAND, landBiomeStream); + seaBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z)) + .convert((r) + -> engine.getDimension().getSeaBiomeStyle().create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream() + .zoom(engine.getDimension().getBiomeZoom()) + .zoom(engine.getDimension().getSeaZoom()) + .zoom(r.getSeaBiomeZoom()) + .selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA)) + ).convertAware2D(ProceduralStream::get) + .cache2D("seaBiomeStream", engine, cacheSize); + inferredStreams.put(InferredType.SEA, seaBiomeStream); + shoreBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z)) + .convert((r) + -> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream() + .zoom(engine.getDimension().getBiomeZoom()) + .zoom(r.getShoreBiomeZoom()) + .selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE)) + ).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize); + inferredStreams.put(InferredType.SHORE, shoreBiomeStream); + bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(), + Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) : + engine.getDimension().getContinentalStyle().create(rng.nextParallelRNG(234234565), getData()) + .bake().scale(1D / engine.getDimension().getContinentZoom()).bake().stream() + .convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND) + .cache2D("bridgeStream", engine, cacheSize); + baseBiomeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome, + Interpolated.of(a -> 0D, a -> focusBiome)) : + bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z)) + .convertAware2D(this::implode) + .cache2D("baseBiomeStream", engine, cacheSize); + heightStream = ProceduralStream.of((x, z) -> { + IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z); + return getHeight(engine, b, x, z, engine.getSeedManager().getHeight()); + }, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize); + roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) + .round(); + slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) + .slope(3).cache2DDouble("slopeStream", engine, cacheSize); + trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D, + b -> focusBiome)) + .cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream + .convertAware2D((h, x, z) -> + fixBiomeType(h, baseBiomeStream.get(x, z), + regionStream.contextInjecting(engine, (c, xx, zz) -> c.getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight)) + .cache2D("trueBiomeStream", engine, cacheSize); + trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z)) + .convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize); + heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) + .max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize); + maxHeightStream = ProceduralStream.ofDouble((x, z) -> height); + terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z)) + .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize); + terrainCeilingDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z)) + .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCeilingDecoration", engine, cacheSize); + terrainCaveSurfaceDecoration = caveBiomeStream.contextInjecting(engine, (c, x, z) -> c.getCave().get(x, z)) + .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainCaveSurfaceDecoration", engine, cacheSize); + terrainCaveCeilingDecoration = caveBiomeStream.contextInjecting(engine, (c, x, z) -> c.getCave().get(x, z)) + .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCaveCeilingDecoration", engine, cacheSize); + shoreSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z)) + .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SHORE_LINE)).cache2D("shoreSurfaceDecoration", engine, cacheSize); + seaSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z)) + .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_SURFACE)).cache2D("seaSurfaceDecoration", engine, cacheSize); + seaFloorDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z)) + .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_FLOOR)).cache2D("seaFloorDecoration", engine, cacheSize); + baseBiomeIDStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z)) + .convertAware2D((b, x, z) -> { + UUID d = regionIDStream.get(x, z); + return new UUID(b.getLoadKey().hashCode() * 818223L, + d.hashCode()); + }) + .cache2D("", engine, cacheSize); + //@done + } + + public ProceduralStream getBiomeStream(InferredType type) { + switch (type) { + case CAVE: + return caveBiomeStream; + case LAND: + return landBiomeStream; + case SEA: + return seaBiomeStream; + case SHORE: + return shoreBiomeStream; + default: + break; + } + + return null; + } + + private IrisRegion findRegion(IrisBiome focus, Engine engine) { + for (IrisRegion i : engine.getDimension().getAllRegions(engine)) { + if (i.getAllBiomeIds().contains(focus.getLoadKey())) { + return i; + } + } + + String key = UUID.randomUUID().toString(); + IrisRegion region = new IrisRegion(); + region.getLandBiomes().add(focus.getLoadKey()); + region.getSeaBiomes().add(focus.getLoadKey()); + region.getShoreBiomes().add(focus.getLoadKey()); + region.setLoadKey(key); + region.setLoader(data); + region.setLoadFile(new File(data.getDataFolder(), data.getRegionLoader().getFolderName() + "/" + key + ".json")); + return region; + } + + private IrisDecorator decorateFor(IrisBiome b, double x, double z, IrisDecorationPart part) { + RNG rngc = new RNG(Cache.key(((int) x), ((int) z))); + + for (IrisDecorator i : b.getDecorators()) { + if (!i.getPartOf().equals(part)) { + continue; + } + + PlatformBlockState block = i.getBlockData(b, rngc, x, z, data); + + if (block != null) { + return i; + } + } + + return null; + } + + private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) { + IrisBiome resolved = resolveSurfaceBiome( + height, + biome, + region, + x, + z, + fluidHeight, + landBiomeStream, + seaBiomeStream, + shoreBiomeStream); + return resolved == biome ? biome : implode(resolved, x, z); + } + + static IrisBiome resolveSurfaceBiome( + double height, + IrisBiome biome, + IrisRegion region, + double x, + double z, + double fluidHeight, + ProceduralStream landBiomes, + ProceduralStream seaBiomes, + ProceduralStream shoreBiomes + ) { + if (biome == null || region == null) { + return biome; + } + double sh = region.getShoreHeight(x, z); + + if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) { + return shoreBiomes.get(x, z); + } + + if (height > fluidHeight + sh && !biome.isLand()) { + return landBiomes.get(x, z); + } + + if (height < fluidHeight && !biome.isAquatic()) { + return seaBiomes.get(x, z); + } + + if (height == fluidHeight && !biome.isShore()) { + return shoreBiomes.get(x, z); + } + + return biome; + } + + private double interpolateGenerators(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z, long seed) { + if (generators.length == 0) { + return 0; + } + + NoiseBounds sampledBounds = gridSampleBounds(engine, interpolator, interpolatorIndex, generators, x, z); + double hi = sampledBounds.max(); + double lo = sampledBounds.min(); + + double d = 0; + + for (IrisGenerator i : generators) { + d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945)); + } + + return d / generators.length; + } + + private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) { + int grid = HEIGHT_BOUNDS_GRID; + GridBoundsCache cache = gridBoundsCache.get(); + if (grid <= 1) { + return sampleBoundsRaw(cache, engine, interpolator, generators, x, z); + } + + int xi = (int) Math.floor(x); + int zi = (int) Math.floor(z); + int mask = grid - 1; + int gx = xi & ~mask; + int gz = zi & ~mask; + double fx = (x - gx) / grid; + double fz = (z - gz) / grid; + + long b00 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz); + if (fx == 0D && fz == 0D) { + return new NoiseBounds(boundsLow(b00), boundsHigh(b00)); + } + + long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz); + long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid); + long b11 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz + grid); + + double lo = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz); + double hi = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz); + return new NoiseBounds(lo, hi); + } + + private long cornerBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, int gx, int gz) { + int slot = cache.slot(gx, gz, interpolatorIndex); + if (cache.valid[slot] && cache.gx[slot] == gx && cache.gz[slot] == gz && cache.idx[slot] == interpolatorIndex) { + return cache.packed[slot]; + } + + NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz); + long packed = (((long) Float.floatToRawIntBits((float) bounds.min())) << 32) | (Float.floatToRawIntBits((float) bounds.max()) & 0xFFFFFFFFL); + cache.gx[slot] = gx; + cache.gz[slot] = gz; + cache.idx[slot] = interpolatorIndex; + cache.packed[slot] = packed; + cache.valid[slot] = true; + return packed; + } + + private static double boundsLow(long packed) { + return Float.intBitsToFloat((int) (packed >>> 32)); + } + + private static double boundsHigh(long packed) { + return Float.intBitsToFloat((int) (packed & 0xFFFFFFFFL)); + } + + private static double biLerp(double v00, double v10, double v01, double v11, double fx, double fz) { + double a = v00 + ((v10 - v00) * fx); + double b = v01 + ((v11 - v01) * fx); + return a + ((b - a) * fz); + } + + private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, double x, double z) { + IdentityHashMap cachedBounds = generatorBounds.get(interpolator); + BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler; + sampler.bind(this, engine, generators, cachedBounds); + + try { + return interpolator.interpolateBounds(x, z, sampler); + } finally { + sampler.release(); + } + } + + private double getInterpolatedHeight(Engine engine, double x, double z, long seed) { + double h = 0; + + for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) { + h += interpolateGenerators(engine, frozenInterpolators[interpolatorIndex], interpolatorIndex, frozenGenerators[interpolatorIndex], x, z, seed); + } + + return h; + } + + private double getHeight(Engine engine, IrisBiome b, double x, double z, long seed) { + return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0); + } + + private void prepareInferredBiomes(IrisRegion region) { + loadInferredBiomes(region.getLandBiomes(), InferredType.LAND); + loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE); + loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA); + loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE); + } + + private KList loadInferredBiomes(KList keys, InferredType type) { + KList inferred = new KList<>(); + for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) { + inferred.add(biome.withInferredType(type)); + } + return inferred; + } + + private void registerGenerators(IrisBiome biome) { + generatorBiomes.add(biome); + biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this))); + } + + private void registerGenerator(IrisGenerator cachedGenerator) { + generators.computeIfAbsent(cachedGenerator.getInterpolator(), (k) -> new HashSet<>()).add(cachedGenerator); + } + + private Map> buildGeneratorBounds(Engine engine) { + Map> bounds = new HashMap<>(); + KList allBiomes = new KList<>(generatorBiomes); + + if (focusBiome != null && !allBiomes.contains(focusBiome)) { + allBiomes.add(focusBiome); + } + + for (int i = 0; i < frozenInterpolators.length; i++) { + IdentityHashMap interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16)); + for (IrisBiome biome : allBiomes) { + interpolatorBounds.put(biome, computeGeneratorBounds(engine, frozenGenerators[i], biome)); + } + bounds.put(frozenInterpolators[i], interpolatorBounds); + } + + return bounds; + } + + private GeneratorBounds computeGeneratorBounds(Engine engine, IrisGenerator[] generators, IrisBiome biome) { + double min = 0D; + double max = 0D; + + for (IrisGenerator gen : generators) { + String key = gen.getLoadKey(); + if (key == null || key.isBlank()) { + continue; + } + + max += biome.getGenLinkMax(key, engine); + min += biome.getGenLinkMin(key, engine); + } + + return new GeneratorBounds(min, max); + } + + private GeneratorBounds resolveGeneratorBounds( + Engine engine, + IrisGenerator[] generators, + IrisBiome biome, + IdentityHashMap cachedBounds, + IdentityHashMap localBounds + ) { + GeneratorBounds bounds = cachedBounds == null ? null : cachedBounds.get(biome); + if (bounds != null) { + return bounds; + } + + GeneratorBounds local = localBounds.get(biome); + if (local != null) { + return local; + } + + GeneratorBounds computed = computeGeneratorBounds(engine, generators, biome); + localBounds.put(biome, computed); + return computed; + } + + private IrisBiome implode(IrisBiome b, Double x, Double z) { + if (b.getChildren().isEmpty()) { + return b; + } + + return implode(b, x, z, 3); + } + + private IrisBiome implode(IrisBiome b, Double x, Double z, int max) { + if (max < 0) { + return b; + } + + if (b.getChildren().isEmpty()) { + return b; + } + + CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor()); + ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b); + IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType()); + return implode(biome, x, z, max - 1); + } + + private ChildSelectionPlan resolveChildSelectionPlan(IrisBiome biome) { + ChildSelectionPlan cachedPlan = childSelectionPlans.get(biome); + if (cachedPlan != null) { + return cachedPlan; + } + + synchronized (childSelectionPlanLock) { + ChildSelectionPlan synchronizedPlan = childSelectionPlans.get(biome); + if (synchronizedPlan != null) { + return synchronizedPlan; + } + + KList children = biome.getRealChildren(this); + KList options = new KList<>(); + for (IrisBiome child : children) { + if (child != null) { + options.add(child); + } + } + options.add(biome); + + ChildSelectionPlan createdPlan = ChildSelectionPlan.create(options); + IdentityHashMap next = new IdentityHashMap<>(childSelectionPlans); + next.put(biome, createdPlan); + childSelectionPlans = next; + return createdPlan; + } + } + + private static class GeneratorBounds { + private final double min; + private final double max; + private final NoiseBounds noiseBounds; + + private GeneratorBounds(double min, double max) { + this.min = min; + this.max = max; + this.noiseBounds = new NoiseBounds(min, max); + } + } + + private static final class GridBoundsCache { + private final int[] gx = new int[GRID_BOUNDS_CACHE_SIZE]; + private final int[] gz = new int[GRID_BOUNDS_CACHE_SIZE]; + private final int[] idx = new int[GRID_BOUNDS_CACHE_SIZE]; + private final long[] packed = new long[GRID_BOUNDS_CACHE_SIZE]; + private final boolean[] valid = new boolean[GRID_BOUNDS_CACHE_SIZE]; + private final BoundsSampler sampler = new BoundsSampler(); + + private int slot(int cornerX, int cornerZ, int interpolatorIndex) { + long h = (cornerX * 0x9E3779B97F4A7C15L) ^ (cornerZ * 0xC2B2AE3D27D4EB4FL) ^ (interpolatorIndex * 0x165667B19E3779F9L); + h ^= (h >>> 32); + return (int) (h & (GRID_BOUNDS_CACHE_SIZE - 1)); + } + } + + /** + * Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator + * bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind} + * resets both, giving each pass the same empty-scratch semantics a fresh allocation had. + *

+ * Single threaded and non reentrant by contract, matching the thread local sample caches in + * IrisInterpolation that this provider is invoked through. If a nested pass ever does appear, + * {@link #isInUse()} makes the caller fall back to a freshly allocated sampler. + */ + private static final class BoundsSampler implements NoiseBoundsProvider { + private final CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64); + private final IdentityHashMap localBounds = new IdentityHashMap<>(8); + private IrisComplex complex; + private Engine engine; + private IrisGenerator[] generators; + private IdentityHashMap cachedBounds; + private boolean inUse; + + private boolean isInUse() { + return inUse; + } + + private void bind(IrisComplex complex, Engine engine, IrisGenerator[] generators, IdentityHashMap cachedBounds) { + this.complex = complex; + this.engine = engine; + this.generators = generators; + this.cachedBounds = cachedBounds; + this.inUse = true; + sampleCache.clear(); + + if (!localBounds.isEmpty()) { + localBounds.clear(); + } + } + + private void release() { + complex = null; + engine = null; + generators = null; + cachedBounds = null; + inUse = false; + } + + @Override + public NoiseBounds noise(double xx, double zz) { + try { + IrisBiome bx = sampleCache.get(xx, zz); + if (bx == null) { + bx = complex.baseBiomeStream.get(xx, zz); + sampleCache.put(xx, zz, bx); + } + + GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds); + return bounds.noiseBounds; + } catch (Throwable e) { + long now = System.currentTimeMillis(); + long last = lastBoundsFailureLog.get(); + // The five second gate keeps this off the hot path; the once key keeps it out of a log + // scan for the rest of the generation, where the same cause repeats for every column. + if (now - last >= 5000L && lastBoundsFailureLog.compareAndSet(last, now) + && IrisLogging.warnOnce("biome-bounds:" + e.getClass().getName() + ":" + e.getMessage(), + "Failed to sample interpolated biome bounds at " + xx + " " + zz + ", flattening height to zero: " + e.getClass().getSimpleName() + ": " + e.getMessage())) { + IrisLogging.reportError(e); + } + } + + return ZERO_NOISE_BOUNDS; + } + } + + /** + * Open addressed biome memo keyed on the packed coordinate bits, replacing a linear scan that was + * quadratic in the number of columns a wide starcast touches. Single threaded by contract. + */ + private static final class CoordinateBiomeCache { + private long[] xBits; + private long[] zBits; + private IrisBiome[] values; + private byte[] states; + private int mask; + private int resizeThreshold; + private int size; + + private CoordinateBiomeCache(int initialCapacity) { + int minimumCapacity = Math.max(8, initialCapacity); + int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); + xBits = new long[tableSize]; + zBits = new long[tableSize]; + values = new IrisBiome[tableSize]; + states = new byte[tableSize]; + mask = tableSize - 1; + resizeThreshold = Math.max(1, (tableSize * 3) >> 2); + size = 0; + } + + private void clear() { + if (size == 0) { + return; + } + + Arrays.fill(states, (byte) 0); + size = 0; + } + + private IrisBiome get(double x, double z) { + int slot = findSlot(Double.doubleToLongBits(x), Double.doubleToLongBits(z)); + return states[slot] == 0 ? null : values[slot]; + } + + private void put(double x, double z, IrisBiome biome) { + long xb = Double.doubleToLongBits(x); + long zb = Double.doubleToLongBits(z); + int slot = findSlot(xb, zb); + boolean occupied = states[slot] != 0; + xBits[slot] = xb; + zBits[slot] = zb; + values[slot] = biome; + states[slot] = 1; + + if (occupied) { + return; + } + + size++; + if (size >= resizeThreshold) { + grow(); + } + } + + private int findSlot(long xb, long zb) { + int slot = mix(xb, zb) & mask; + while (states[slot] != 0) { + if (xBits[slot] == xb && zBits[slot] == zb) { + break; + } + slot = (slot + 1) & mask; + } + return slot; + } + + private int mix(long xb, long zb) { + long hash = xb * 0x9E3779B97F4A7C15L; + hash ^= Long.rotateLeft(zb * 0xC2B2AE3D27D4EB4FL, 32); + hash ^= (hash >>> 33); + hash *= 0xff51afd7ed558ccdL; + hash ^= (hash >>> 33); + return (int) hash; + } + + private void grow() { + long[] previousXBits = xBits; + long[] previousZBits = zBits; + IrisBiome[] previousValues = values; + byte[] previousStates = states; + + int nextLength = previousXBits.length << 1; + xBits = new long[nextLength]; + zBits = new long[nextLength]; + values = new IrisBiome[nextLength]; + states = new byte[nextLength]; + mask = nextLength - 1; + resizeThreshold = Math.max(1, (nextLength * 3) >> 2); + size = 0; + + for (int i = 0; i < previousStates.length; i++) { + if (previousStates[i] == 0) { + continue; + } + + int slot = findSlot(previousXBits[i], previousZBits[i]); + xBits[slot] = previousXBits[i]; + zBits[slot] = previousZBits[i]; + values[slot] = previousValues[i]; + states[slot] = 1; + size++; + } + } + + private int tableSizeFor(int value) { + int n = value - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + int tableSize = n + 1; + if (tableSize < 8) { + return 8; + } + return tableSize; + } + } + + static final class ChildSelectionPlan { + private final IrisBiome[] mappedBiomes; + private final int maxIndex; + + private ChildSelectionPlan(IrisBiome[] mappedBiomes) { + this.mappedBiomes = mappedBiomes; + this.maxIndex = mappedBiomes.length - 1; + } + + static ChildSelectionPlan create(KList options) { + if (options.isEmpty()) { + return new ChildSelectionPlan(new IrisBiome[0]); + } + + int maxRarity = 1; + for (IrisBiome biome : options) { + if (biome != null && biome.getRarity() > maxRarity) { + maxRarity = biome.getRarity(); + } + } + + int rarityMax = maxRarity + 1; + boolean flip = false; + KList mapped = new KList<>(); + for (IrisBiome biome : options) { + if (biome == null) { + continue; + } + + int rarity = Math.max(1, biome.getRarity()); + int count = rarityMax - rarity; + for (int index = 0; index < count; index++) { + flip = !flip; + if (flip) { + mapped.add(biome); + } else { + mapped.add(0, biome); + } + } + } + + if (mapped.isEmpty()) { + IrisBiome[] fallback = new IrisBiome[]{options.get(0)}; + return new ChildSelectionPlan(fallback); + } + + IrisBiome[] mappedBiomes = mapped.toArray(new IrisBiome[0]); + return new ChildSelectionPlan(mappedBiomes); + } + + IrisBiome select(CNG childCell, double x, double z) { + if (mappedBiomes.length == 0) { + return null; + } + + if (mappedBiomes.length == 1) { + return mappedBiomes[0]; + } + + int selectedIndex = childCell.fit2D(0, maxIndex, x, z); + if (selectedIndex < 0) { + return mappedBiomes[0]; + } + + if (selectedIndex > maxIndex) { + return mappedBiomes[maxIndex]; + } + + return mappedBiomes[selectedIndex]; + } + } + + public void close() { + + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java new file mode 100644 index 000000000..5646c1fc3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -0,0 +1,727 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain; +import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineEffects; +import art.arcane.iris.engine.framework.EngineMetrics; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.EnginePlatformHooks; +import art.arcane.iris.engine.framework.NativeStructureVolume; +import art.arcane.iris.engine.framework.NativeStructureVolumeMemo; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.EngineWorldManager; +import art.arcane.iris.engine.framework.GenerationSessionException; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.framework.GenerationSessionManager; +import art.arcane.iris.engine.framework.SeedManager; +import art.arcane.iris.engine.framework.WrongEngineBroException; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisEngineData; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.spi.IrisLogging; +import com.google.common.util.concurrent.AtomicDouble; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.core.nms.container.BlockPos; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.core.structure.StructureIndexService; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.atomics.AtomicRollingSequence; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.matter.MatterStructurePOI; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +@Data +public class IrisEngine implements Engine { + static final long SESSION_DRAIN_TIMEOUT_MILLIS = 15000L; + + private final AtomicInteger bud; + private final AtomicInteger buds; + private final AtomicInteger generated; + private final AtomicInteger generatedLast; + private final AtomicDouble perSecond; + private final AtomicLong lastGPS; + private final EngineTarget target; + private final InitializationMode initializationMode; + private final EngineMantle mantle; + private final ChronoLatch perSecondLatch; + private final ChronoLatch perSecondBudLatch; + private final EngineMetrics metrics; + private final boolean studio; + private final AtomicRollingSequence wallClock; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final Object lifecycleLock = new Object(); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final ThreadLocal runtimeAssembly = new ThreadLocal<>(); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final EngineBackgroundTasks backgroundTasks = new EngineBackgroundTasks(); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final EngineDataStore engineDataStore = new EngineDataStore(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final EngineRuntimeBuilder runtimeBuilder = new EngineRuntimeBuilder(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final EngineShutdownSequence shutdownSequence = new EngineShutdownSequence(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final EngineHotloader hotloader = new EngineHotloader(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final NativeStructureBootstrapBarrier nativeStructureBootstrapBarrier = new NativeStructureBootstrapBarrier(); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final EngineMetricsReport metricsReport = new EngineMetricsReport(this); + private final AtomicBoolean cleaning; + private final ChronoLatch cleanLatch; + private final SeedManager seedManager; + private final GenerationSessionManager generationSessions; + private final EnginePlatformHooks platformHooks; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final NativeStructureVolumeMemo nativeStructureVolumeMemo = new NativeStructureVolumeMemo(); + private final AtomicBoolean closing; + private final AtomicBoolean nativeStructureVolumeQueriesEnabled; + @Setter(AccessLevel.NONE) + volatile IrisEngineData engineData; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + volatile EngineRuntime runtime; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + volatile EngineTarget publishedTarget; + private volatile int parallelism; + private volatile boolean failing; + volatile boolean closed; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + volatile LifecycleState lifecycleState; + private final AtomicBoolean modeFallbackLogged; + private final AtomicBoolean prefetchSaveStarted; + + /** + * Object identity, not value identity. An engine is a live mutable service, and {@code @Data} would otherwise + * generate equals/hashCode over every field above - counters, latches, rolling averages - so an engine's hash + * would change on every generated chunk and its equality would depend on transient timing state. + *

+ * Three live maps key on an engine: the modded GUI host registry and the two WeakHashMap tree-feller indexes. A + * mutating hash silently loses their entries, and a value-based equals lets two distinct engines collide. + */ + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + public IrisEngine(EngineTarget target, InitializationMode initializationMode) { + InitializationMode requiredMode = Objects.requireNonNull(initializationMode, "initialization mode"); + this.initializationMode = requiredMode; + this.studio = requiredMode.studio(); + this.target = target; + this.publishedTarget = target; + this.platformHooks = IrisServices.get(EnginePlatformHooks.class); + this.generationSessions = new GenerationSessionManager(); + this.closing = new AtomicBoolean(true); + this.nativeStructureVolumeQueriesEnabled = new AtomicBoolean(!requiredMode.studio()); + this.lifecycleState = LifecycleState.INITIALIZING; + this.closed = false; + this.failing = false; + getEngineData(); + verifySeed(); + this.seedManager = new SeedManager(target.getWorld().getRawWorldSeed()); + bud = new AtomicInteger(0); + buds = new AtomicInteger(0); + metrics = new EngineMetrics(32); + cleanLatch = new ChronoLatch(10000); + generatedLast = new AtomicInteger(0); + perSecond = new AtomicDouble(0); + perSecondLatch = new ChronoLatch(1000, false); + perSecondBudLatch = new ChronoLatch(1000, false); + wallClock = new AtomicRollingSequence(32); + lastGPS = new AtomicLong(M.ms()); + generated = new AtomicInteger(0); + long _t0 = M.ms(); + mantle = new IrisEngineMantle(this); + IrisLogging.debug("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - _t0) + "ms"); + cleaning = new AtomicBoolean(false); + modeFallbackLogged = new AtomicBoolean(false); + prefetchSaveStarted = new AtomicBoolean(false); + sealInitialGenerationSession(); + + try { + if (studio) { + _t0 = M.ms(); + getData().dump(); + getData().clearLists(); + IrisDimension replacement = getData().getDimensionLoader().load(getDimension().getLoadKey()); + if (replacement == null) { + throw new IllegalStateException("Studio engine could not load Iris dimension '" + getDimension().getLoadKey() + "'"); + } + publishedTarget = new EngineTarget(getWorld(), replacement, getData()); + IrisLogging.debug("[IrisEngine timing] dump+clearLists+reload=" + (M.ms() - _t0) + "ms"); + } + getData().registerEngine(this); + _t0 = M.ms(); + long phaseStartedAt = System.nanoTime(); + getData().loadPrefetch(this); + IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms"); + logStudioInitializationPhase("load_prefetch", phaseStartedAt, false); + try { + StructureIndexService.writeOnce(getData()); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + IrisLogging.notice("Engine init: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " seed=" + getSeedManager().getSeed()); + _t0 = M.ms(); + phaseStartedAt = System.nanoTime(); + EngineRuntime initialRuntime = runtimeBuilder.buildRuntime(); + runtimeBuilder.publishRuntime(initialRuntime, null); + IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms"); + logStudioInitializationPhase("build_runtime", phaseStartedAt, false); + _t0 = M.ms(); + phaseStartedAt = System.nanoTime(); + if (requiredMode.warmGenerationCaches()) { + GenerationCacheWarmer.warm(this); + } + IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - _t0) + "ms"); + logStudioInitializationPhase( + "generation_cache_warm", + phaseStartedAt, + !requiredMode.warmGenerationCaches()); + EngineTickRegistry.registerTicking(this); + } catch (Throwable e) { + shutdownSequence.cleanupFailedConstruction(e); + throw new IllegalStateException("Failed to initialize Iris engine for world '" + target.getWorld().name() + "'.", e); + } + IrisLogging.debug("Engine Initialized " + getCacheID()); + } + + private void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) { + if (!studio) { + return; + } + IrisLogging.info("[Studio engine timing] world=%s kind=%s phase=%s duration=%dms skipped=%s", + target.getWorld().name(), + initializationMode.name().toLowerCase(Locale.ROOT), + phase, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos), + Boolean.toString(skipped)); + } + + private void verifySeed() { + if (getEngineData().getSeed() != null && getEngineData().getSeed() != target.getWorld().getRawWorldSeed()) { + target.getWorld().setRawWorldSeed(getEngineData().getSeed()); + } + } + + void tickRandomPlayer() { + if (closing.get() || closed) { + return; + } + recycle(); + if (perSecondBudLatch.flip()) { + buds.set(bud.get()); + bud.set(0); + } + + EngineEffects currentEffects = getEffects(); + if (currentEffects != null) { + currentEffects.tickRandomPlayer(); + } + } + + private void sealInitialGenerationSession() { + try { + generationSessions.sealAndAwait("initialization", 0L); + } catch (GenerationSessionException e) { + throw new IllegalStateException("Failed to seal the initial Iris generation session.", e); + } + } + + void sealForTransition(String reason, boolean teardown) { + closing.set(true); + backgroundTasks.closeBackgroundTaskAdmission(); + try { + generationSessions.sealAndAwait(reason, SESSION_DRAIN_TIMEOUT_MILLIS, teardown); + BackgroundTaskDrain backgroundDrain = backgroundTasks.drainBackgroundTasks(reason); + backgroundDrain.requireComplete(reason); + } catch (GenerationSessionException e) { + throw new IllegalStateException("Failed to drain Iris generation for " + reason + ".", e); + } + } + + public CompletableFuture startNativeStructureBootstrap( + Runnable claim, + Supplier> starter, + Runnable activation + ) { + synchronized (lifecycleLock) { + requireRunning("prepare native structure placements"); + CompletableFuture completion = nativeStructureBootstrapBarrier.start(claim, starter); + activation.run(); + return completion; + } + } + + void awaitNativeStructureBootstrap(String transition) { + nativeStructureBootstrapBarrier.await(transition); + } + + @Override + public void generateMatter(int x, int z, boolean multicore, ChunkContext context) { + try (GenerationSessionLease lease = acquireGenerationLease("matter_generate"); + IrisContext.Scope ignored = IrisContext.open(this, lease.sessionId(), context)) { + IrisComplex activeComplex = getComplex(); + if (context == null || context.getComplex() != activeComplex) { + throw new IllegalStateException("Matter generation context does not belong to the active Iris runtime."); + } + if (context.getGenerationSessionId() != 0L + && context.getGenerationSessionId() != lease.sessionId()) { + throw new IllegalStateException("Matter generation context belongs to Iris session " + + context.getGenerationSessionId() + " instead of " + lease.sessionId() + "."); + } + getMantle().generateMatter(x, z, multicore, context); + } catch (GenerationSessionException e) { + throw new IllegalStateException("Matter generation was rejected by the Iris lifecycle.", e); + } + } + + @Override + public Set getObjectsAt(int x, int z) { + return getMantle().getObjectComponent().guess(x, z); + } + + @Override + public Set> getPOIsAt(int chunkX, int chunkY) { + Set> pois = new HashSet<>(); + getMantle().getMantle().iterateChunk(chunkX, chunkY, MatterStructurePOI.class, (x, y, z, d) -> pois.add(new Pair<>(d.getType(), new BlockPos(x, y, z)))); + return pois; + } + + private void warmupChunk(int x, int z) { + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + int xx = x + (i << 4); + int zz = z + (z << 4); + getComplex().getTrueBiomeStream().get(xx, zz); + getComplex().getHeightStream().get(xx, zz); + } + } + } + + @Override + public void hotload() { + hotloader.hotload(); + } + + public void hotloadComplex() { + hotloader.hotloadComplex(); + } + + public void hotloadSilently() { + nativeStructureVolumeMemo.clear(); + hotloader.hotloadSilently(); + } + + @Override + public KList getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) { + if (!nativeStructureVolumeQueriesEnabled.get()) { + return NativeStructureVolume.NONE; + } + return nativeStructureVolumeMemo.volumes(this, platformHooks, minX, minZ, maxX, maxZ); + } + + public void setNativeStructureVolumeQueriesEnabled(boolean enabled) { + if (!enabled) { + nativeStructureVolumeQueriesEnabled.set(false); + nativeStructureVolumeMemo.clear(); + return; + } + nativeStructureVolumeMemo.clear(); + nativeStructureVolumeQueriesEnabled.set(true); + } + + @Override + public IrisEngineData getEngineData() { + return engineDataStore.getEngineData(); + } + + @Override + public int getGenerated() { + return generated.get(); + } + + @Override + public double getGeneratedPerSecond() { + return metricsReport.getGeneratedPerSecond(); + } + + @Override + public boolean isStudio() { + return studio; + } + + @Override + public int getBlockUpdatesPerSecond() { + return buds.get(); + } + + public void printMetrics(VolmitSender sender) { + metricsReport.printMetrics(sender); + } + + @Override + public void close() { + shutdownSequence.close(); + } + + private boolean isPregeneratorActiveForThisWorld() { + return platformHooks.isPregeneratorActive(this); + } + + void savePrefetchOnce() { + if (prefetchSaveStarted.compareAndSet(false, true)) { + try { + getData().savePrefetch(this); + } catch (Throwable e) { + prefetchSaveStarted.set(false); + throw EngineShutdownSequence.propagate(e); + } + } + } + + @Override + public IrisComplex getComplex() { + RuntimeAssembly assembly = runtimeAssembly.get(); + if (assembly != null && assembly.complex != null) { + return assembly.complex; + } + EngineRuntime current = runtime; + return current == null ? null : current.complex(); + } + + @Override + public EngineTarget getTarget() { + RuntimeAssembly assembly = runtimeAssembly.get(); + if (assembly != null) { + return assembly.target; + } + EngineRuntime current = runtime; + return current == null ? publishedTarget : current.target(); + } + + @Override + public EngineMode getMode() { + RuntimeAssembly assembly = runtimeAssembly.get(); + if (assembly != null && assembly.mode != null) { + return assembly.mode; + } + EngineRuntime current = runtimeBuilder.requireRuntime("access the engine mode"); + return current.mode(); + } + + @Override + public EngineEffects getEffects() { + RuntimeAssembly assembly = runtimeAssembly.get(); + if (assembly != null && assembly.effects != null) { + return assembly.effects; + } + EngineRuntime current = runtime; + return current == null ? null : current.effects(); + } + + @Override + public EngineWorldManager getWorldManager() { + RuntimeAssembly assembly = runtimeAssembly.get(); + if (assembly != null && assembly.worldManager != null) { + return assembly.worldManager; + } + EngineRuntime current = runtime; + return current == null ? null : current.worldManager(); + } + + @Override + public UpperDimensionContext getUpperContext() { + RuntimeAssembly assembly = runtimeAssembly.get(); + if (assembly != null) { + return assembly.upperContext; + } + EngineRuntime current = runtime; + return current == null ? null : current.upperContext(); + } + + @Override + public CompletableFuture getHash32() { + RuntimeAssembly assembly = runtimeAssembly.get(); + if (assembly != null && assembly.hash32 != null) { + return assembly.hash32; + } + EngineRuntime current = runtimeBuilder.requireRuntime("access the pack hash"); + return current.hash32(); + } + + @Override + public double getMaxBiomeObjectDensity() { + EngineRuntime current = runtime; + return current == null ? 0D : current.biomeMaxes().objectDensity(); + } + + @Override + public double getMaxBiomeLayerDensity() { + EngineRuntime current = runtime; + return current == null ? 0D : current.biomeMaxes().layerDensity(); + } + + @Override + public double getMaxBiomeDecoratorDensity() { + EngineRuntime current = runtime; + return current == null ? 0D : current.biomeMaxes().decoratorDensity(); + } + + @Override + public boolean isClosed() { + return closed; + } + + public boolean isClosing() { + return closing.get(); + } + + @Override + public void recycle() { + if (closing.get() || closed) { + return; + } + if (!cleanLatch.flip()) { + return; + } + + if (cleaning.get()) { + cleanLatch.flipDown(); + return; + } + + cleaning.set(true); + + if (!backgroundTasks.scheduleTrackedTask(() -> { + try { + getData().getObjectLoader().clean(); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Cleanup failed! Enable debug to see stacktrace."); + } + + cleaning.lazySet(false); + })) { + cleaning.set(false); + } + } + + @BlockCoordinates + @Override + public void generate(int x, int z, Hunk vblocks, Hunk vbiomes, boolean multicore) throws WrongEngineBroException { + try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate"); + IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) { + getEngineData().getStatistics().generatedChunk(); + PrecisionStopwatch p = PrecisionStopwatch.start(); + Hunk blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t)); + + if (getDimension().isDebugChunkCrossSections() && ((x >> 4) % getDimension().getDebugCrossSectionsMod() == 0 || (z >> 4) % getDimension().getDebugCrossSectionsMod() == 0)) { + PlatformBlockState crossSection = B.getState("CRYING_OBSIDIAN"); + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + blocks.set(i, 0, j, crossSection); + } + } + } else { + EngineMode activeMode = getMode(); + activeMode.generate(x, z, blocks, vbiomes, multicore, lease.sessionId()); + } + + boolean skipRealFlag = platformHooks.shouldBypassMantleStages(this); + if (!skipRealFlag) { + getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true); + } + getMetrics().getTotal().put(p.getMilliseconds()); + generated.incrementAndGet(); + + if (generated.get() == 661 && !isPregeneratorActiveForThisWorld()) { + backgroundTasks.scheduleTrackedTask(this::savePrefetchOnce); + } + } catch (GenerationSessionException e) { + throw e; + } catch (Throwable e) { + IrisLogging.reportError(e); + fail("Failed to generate " + x + ", " + z, e); + if (e instanceof WrongEngineBroException wrongEngine) { + throw wrongEngine; + } + throw new WrongEngineBroException("Failed to generate chunk at " + x + ", " + z + ".", e); + } + } + + @Override + public GenerationSessionManager getGenerationSessions() { + return generationSessions; + } + + @Override + public void saveEngineData() { + engineDataStore.saveEngineData(); + } + + @Override + public void blockUpdatedMetric() { + bud.incrementAndGet(); + } + + @Override + public IrisBiome getFocus() { + if (getDimension().getFocus() == null || getDimension().getFocus().trim().isEmpty()) { + return null; + } + + return getData().getBiomeLoader().load(getDimension().getFocus()); + } + + @Override + public IrisRegion getFocusRegion() { + if (getDimension().getFocusRegion() == null || getDimension().getFocusRegion().trim().isEmpty()) { + return null; + } + + return getData().getRegionLoader().load(getDimension().getFocusRegion()); + } + + @Override + public void fail(String error, Throwable e) { + failing = true; + IrisLogging.error(error); + IrisLogging.reportError(e); + e.printStackTrace(); + } + + @Override + public boolean hasFailed() { + return failing; + } + + @Override + public int getCacheID() { + RuntimeAssembly assembly = runtimeAssembly.get(); + if (assembly != null) { + return assembly.cacheId; + } + EngineRuntime current = runtime; + return current == null ? -1 : current.cacheId(); + } + + void requireRunning(String operation) { + if (closed || closing.get() || lifecycleState != LifecycleState.RUNNING || runtime == null) { + throw new IllegalStateException("Cannot " + operation + " while Iris engine " + getWorld().name() + + " is " + lifecycleState.name().toLowerCase() + "."); + } + } + + private boolean EngineSafe() { + // Todo: this has potential if done right + int EngineMCVersion = getEngineData().getStatistics().getMCVersion(); + int EngineIrisVersion = getEngineData().getStatistics().getVersion(); + int MinecraftVersion = IrisPlatforms.get().minecraftVersionNumber(); + int IrisVersion = IrisPlatforms.get().irisVersionNumber(); + if (EngineIrisVersion != IrisVersion) { + return false; + } + if (EngineMCVersion != MinecraftVersion) { + return false; + } + return true; + } + + enum LifecycleState { + INITIALIZING, + RUNNING, + HOTLOADING, + CLOSING, + CLOSED, + FAILED + } + + public enum InitializationMode { + RUNTIME(false, true), + STUDIO(true, true), + JIGSAW_STUDIO(true, false); + + private final boolean studio; + private final boolean warmGenerationCaches; + + InitializationMode(boolean studio, boolean warmGenerationCaches) { + this.studio = studio; + this.warmGenerationCaches = warmGenerationCaches; + } + + public boolean studio() { + return studio; + } + + public boolean warmGenerationCaches() { + return warmGenerationCaches; + } + } + +} diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngineEffects.java b/core/src/main/java/art/arcane/iris/engine/IrisEngineEffects.java new file mode 100644 index 000000000..7592bf3b8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngineEffects.java @@ -0,0 +1,127 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedComponent; +import art.arcane.iris.engine.framework.EngineEffects; +import art.arcane.iris.engine.framework.EnginePlayer; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.math.M; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +public class IrisEngineEffects extends EngineAssignedComponent implements EngineEffects { + private static final long EFFECT_BUDGET_NANOS = 1_500_000L; + private static final long EMPTY_PLAYER_REFRESH_NANOS = 1_000_000_000L; + + private final ConcurrentHashMap players; + private final Semaphore limit; + private final AtomicBoolean playerMapUpdateQueued; + private final AtomicLong nextEmptyRefresh; + + public IrisEngineEffects(Engine engine) { + super(engine, "FX"); + players = new ConcurrentHashMap<>(); + limit = new Semaphore(1); + playerMapUpdateQueued = new AtomicBoolean(false); + nextEmptyRefresh = new AtomicLong(0L); + } + + @Override + public void updatePlayerMap() { + if (!playerMapUpdateQueued.compareAndSet(false, true)) { + return; + } + if (!J.runGlobal(() -> { + try { + syncPlayerMap(); + } finally { + playerMapUpdateQueued.set(false); + } + })) { + playerMapUpdateQueued.set(false); + } + } + + private void syncPlayerMap() { + List pr = BukkitWorldBinding.players(getEngine().getWorld()); + + if (pr == null) { + return; + } + + Set activeIds = new HashSet<>(Math.max(16, pr.size() * 2)); + for (Player player : pr) { + UUID playerId = player.getUniqueId(); + activeIds.add(playerId); + EnginePlayer existing = players.get(playerId); + if (existing == null || existing.getPlayer() != player) { + players.put(playerId, new EnginePlayer(getEngine(), player)); + } + } + + players.keySet().removeIf((UUID playerId) -> !activeIds.contains(playerId)); + } + + @Override + public void tickRandomPlayer() { + if (!limit.tryAcquire()) { + return; + } + try { + if (players.isEmpty()) { + long now = System.nanoTime(); + long next = nextEmptyRefresh.get(); + if (now >= next && nextEmptyRefresh.compareAndSet(next, now + EMPTY_PLAYER_REFRESH_NANOS)) { + updatePlayerMap(); + } + return; + } + if (M.r(0.02)) { + updatePlayerMap(); + return; + } + + List snapshot = new ArrayList<>(players.values()); + if (snapshot.isEmpty()) { + return; + } + + long started = System.nanoTime(); + int remaining = snapshot.size(); + while (remaining-- > 0 && System.nanoTime() - started < EFFECT_BUDGET_NANOS) { + snapshot.get(ThreadLocalRandom.current().nextInt(snapshot.size())).tick(); + } + } finally { + limit.release(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java new file mode 100644 index 000000000..58690dae0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java @@ -0,0 +1,423 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.tools.WorldMaintenance; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.mantle.MantleComponent; +import art.arcane.iris.engine.mantle.MantlePass; +import art.arcane.iris.engine.mantle.components.MantleCarvingComponent; +import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent; +import art.arcane.iris.engine.mantle.components.MantleObjectComponent; +import art.arcane.iris.engine.mantle.components.IrisStructureComponent; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.matter.IrisMatterContext; +import art.arcane.iris.util.project.matter.IrisMatterSupport; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.mantle.io.Lz4IOWorkerCodecSupport; +import art.arcane.volmlib.util.mantle.runtime.IOWorker; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter; +import art.arcane.volmlib.util.mantle.runtime.MantleHooks; +import art.arcane.volmlib.util.mantle.runtime.TectonicPlate; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.matter.IrisMatter; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterSlice; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.util.common.parallel.HyperLock; +import art.arcane.iris.util.common.parallel.MultiBurst; +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +@Data +@EqualsAndHashCode(exclude = "engine") +@ToString(exclude = "engine") +public class IrisEngineMantle implements EngineMantle { + private static final boolean BUKKIT_PRESENT = IrisMatterSupport.isBukkitPresent(); + private final Engine engine; + private final Mantle mantle; + @Getter(AccessLevel.NONE) + private final KMap> components; + private final KMap registeredComponents = new KMap<>(); + private final AtomicCache> componentsCache = new AtomicCache<>(); + private final AtomicCache> disabledFlags = new AtomicCache<>(); + private final MantleObjectComponent object; + + public IrisEngineMantle(Engine engine) { + this.engine = engine; + this.mantle = createMantle(engine); + components = new KMap<>(); + registerComponent(new MantleCarvingComponent(this)); + object = new MantleObjectComponent(this); + registerComponent(object); + registerComponent(new MantleFloatingObjectComponent(this)); + registerComponent(new IrisStructureComponent(this)); + } + + @Override + public int getRadius() { + if (components.isEmpty()) return 0; + return getComponents().getFirst().passChunkRadius(); + } + + @Override + public int getRealRadius() { + if (components.isEmpty()) return 0; + return getComponents().getLast().passChunkRadius(); + } + + @Override + public List getComponents() { + return componentsCache.aquire(() -> { + List> passes = components.keySet() + .stream() + .sorted() + .map(components::get) + .map(List::copyOf) + .filter(pass -> !pass.isEmpty()) + .toList(); + + MantlePass[] built = new MantlePass[passes.size()]; + int downstreamBlockRadius = 0; + for (int i = passes.size() - 1; i >= 0; i--) { + List pass = passes.get(i); + int passBlockRadius = pass.stream() + .filter(MantleComponent::isEnabled) + .mapToInt(MantleComponent::getRadius) + .max() + .orElse(0); + int cumulative = downstreamBlockRadius + passBlockRadius; + built[i] = new MantlePass(pass, Math.ceilDiv(cumulative, 16), downstreamBlockRadius); + downstreamBlockRadius = cumulative; + } + + return List.of(built); + }); + } + + @Override + public Map getRegisteredComponents() { + return Collections.unmodifiableMap(registeredComponents); + } + + @Override + public boolean registerComponent(MantleComponent c) { + if (registeredComponents.putIfAbsent(c.getFlag(), c) != null) return false; + c.setEnabled(!getDisabledFlags().contains(c.getFlag())); + components.computeIfAbsent(c.getPriority(), k -> new KList<>()).add(c); + componentsCache.reset(); + return true; + } + + @Override + public KList getComponentFlags() { + return new KList<>(registeredComponents.keySet()); + } + + @Override + public void hotload() { + disabledFlags.reset(); + for (var component : registeredComponents.values()) { + component.hotload(); + component.setEnabled(!getDisabledFlags().contains(component.getFlag())); + } + componentsCache.reset(); + } + + private Set getDisabledFlags() { + return disabledFlags.aquire(() -> { + KList disabled = new KList<>(); + disabled.addAll(getDimension().getDisabledComponents()); + if (!getDimension().isCarvingEnabled()) { + disabled.addIfMissing(ReservedFlag.CARVED); + } + return Set.copyOf(disabled); + }); + } + + @Override + public MantleObjectComponent getObjectComponent() { + return object; + } + + private static Mantle createMantle(Engine engine) { + IrisMatterSupport.ensureRegistered(); + File dataFolder = new File(engine.getWorld().worldFolder(), "mantle"); + int worldHeight = engine.getTarget().getHeight(); + MantleDataAdapter adapter = createDataAdapter(engine::getData); + MantleHooks hooks = createHooks(EnginePanic.scoped("world " + engine.getWorld().name())); + art.arcane.volmlib.util.mantle.Mantle.RegionIO> regionIO = + createRegionIO(dataFolder, worldHeight, adapter, hooks); + return new Mantle<>( + dataFolder, + worldHeight, + Short.MAX_VALUE, + new HyperLock(), + MultiBurst.ioBurst, + regionIO, + adapter, + hooks + ); + } + + public static MantleDataAdapter createRuntimeDataAdapter(IrisData data) { + return createDataAdapter(() -> data); + } + + public static MantleHooks createRuntimeHooks() { + return createHooks(EnginePanic.scoped("runtime mantle")); + } + + private static MantleDataAdapter createDataAdapter(Supplier dataSupplier) { + return new MantleDataAdapter<>() { + @Override + public Matter createSection() { + return new IrisMatter(16, 16, 16); + } + + @Override + public Matter readSection(art.arcane.volmlib.util.io.CountingDataInputStream din) throws IOException { + IrisData data = Objects.requireNonNull(dataSupplier.get(), "Iris mantle data is unavailable."); + try (IrisMatterContext.Scope scope = IrisMatterContext.open(data)) { + return Matter.readDin(din); + } + } + + @Override + public void writeSection(Matter section, java.io.DataOutputStream dos) throws IOException { + section.writeDos(dos); + } + + @Override + public void trimSection(Matter section) { + section.trimSlices(); + } + + @Override + public boolean isSectionEmpty(Matter section) { + return section.getSliceMap().isEmpty(); + } + + @Override + public Class classifyValue(Object value) { + if (value instanceof PlatformBlockState) { + return PlatformBlockState.class; + } + if (BUKKIT_PRESENT) { + return BukkitPlatform.classifyMantleValue(value); + } + return value.getClass(); + } + + @Override + @SuppressWarnings("unchecked") + public void set(Matter section, int x, int y, int z, Class type, T value) { + MatterSlice slice = (MatterSlice) section.slice(type); + slice.set(x, y, z, value); + } + + @Override + public void remove(Matter section, int x, int y, int z, Class type) { + MatterSlice slice = section.slice(type); + slice.set(x, y, z, null); + } + + @Override + public T get(Matter section, int x, int y, int z, Class type) { + MatterSlice slice = section.slice(type); + return slice.get(x, y, z); + } + + @Override + public void iterate(Matter section, Class type, art.arcane.volmlib.util.function.Consumer4 iterator) { + MatterSlice slice = section.getSlice(type); + if (slice != null) { + slice.iterateSync(iterator); + } + } + + @Override + public boolean hasSlice(Matter section, Class type) { + return section.hasSlice(type); + } + + @Override + public void deleteSlice(Matter section, Class type) { + section.deleteSlice(type); + } + }; + } + + private static MantleHooks createHooks(EnginePanic.Diagnostics panic) { + return new MantleHooks() { + @Override + public void onBeforeReadSection(int index) { + panic.add("read.section", "Section[" + index + "]"); + } + + @Override + public void onReadSectionFailure(int index, + long start, + long end, + art.arcane.volmlib.util.io.CountingDataInputStream din, + IOException error) { + IrisLogging.error("Failed to read chunk section, skipping it."); + panic.add("read.byte.range", start + " " + end); + panic.add("read.byte.current", din.count() + ""); + IrisLogging.reportError(error); + error.printStackTrace(); + panic.panic(); + TectonicPlate.addError(); + } + + @Override + public void onBeforeReadChunk(int index) { + panic.add("read-chunk", "Chunk[" + index + "]"); + } + + @Override + public void onAfterReadChunk(int index) { + panic.saveLast(); + } + + @Override + public void onReadChunkFailure(int index, + long start, + long end, + art.arcane.volmlib.util.io.CountingDataInputStream din, + Throwable error) { + IrisLogging.error("Failed to read chunk, creating a new chunk instead."); + panic.add("read.byte.range", start + " " + end); + panic.add("read.byte.current", din.count() + ""); + IrisLogging.reportError(error); + error.printStackTrace(); + panic.panic(); + } + + @Override + public boolean shouldRetainSlice(Class sliceType) { + return WorldMaintenance.isRetainingMantleDataForSlice(sliceType.getCanonicalName()); + } + + @Override + public String formatDuration(double millis) { + return Form.duration(millis, 0); + } + + @Override + public void onDebug(String message) { + IrisLogging.debug(message); + } + + @Override + public void onWarn(String message) { + IrisLogging.warn(message); + } + + @Override + public void onError(Throwable throwable) { + IrisLogging.reportError(throwable); + } + }; + } + + private static art.arcane.volmlib.util.mantle.Mantle.RegionIO> createRegionIO(File root, + int worldHeight, + MantleDataAdapter adapter, + MantleHooks hooks) { + IOWorker> worker = new IOWorker<>( + root, + new Lz4IOWorkerCodecSupport(), + 128, + (name, millis) -> { + String threadName = Thread.currentThread().getName(); + String message = "Acquired Channel for " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(millis, 2) + + C.GRAY + " thread=" + threadName; + if (millis >= 1000L) { + IrisLogging.warn(message); + } else { + IrisLogging.debug(message); + } + } + ); + + return new art.arcane.volmlib.util.mantle.Mantle.RegionIO<>() { + @Override + public TectonicPlate read(String name) throws Exception { + PrecisionStopwatch stopwatch = PrecisionStopwatch.start(); + try { + return worker.read(name, (regionName, in) -> + TectonicPlate.read(worldHeight, in, regionName.startsWith("pv."), adapter, hooks)); + } finally { + // hasError() is a consuming ThreadLocal read: evaluate exactly once. The + // dump must never throw out of this finally — that would replace the + // successfully parsed plate with the dump failure, and the caller would + // overwrite 1024 good chunks with a fresh empty plate. + boolean errored = TectonicPlate.hasError(); + if (errored && IrisSettings.get().getGeneral().isDumpMantleOnError()) { + try { + File dump = IrisPlatforms.get().dataFile("dump", name + ".bin"); + worker.dumpDecoded(name, dump.toPath()); + } catch (Throwable dumpFailure) { + IrisLogging.warn("Failed to dump mantle region " + name + " for diagnostics"); + IrisLogging.reportError(dumpFailure); + } + } + IrisLogging.debug("Read Tectonic Plate " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(stopwatch.getMilliseconds(), 2)); + } + } + + @Override + public void write(String name, TectonicPlate region) throws Exception { + PrecisionStopwatch stopwatch = PrecisionStopwatch.start(); + worker.write(name, "iris", ".bin", region, TectonicPlate::write); + IrisLogging.debug("Saved Tectonic Plate " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(stopwatch.getMilliseconds(), 2)); + } + + @Override + public void close() throws Exception { + worker.close(); + } + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java new file mode 100644 index 000000000..c5d9d752d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -0,0 +1,351 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedWorldManager; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.Looper; +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Chunk; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerTeleportEvent; + +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +@EqualsAndHashCode(callSuper = true) +@Data +public class IrisWorldManager extends EngineAssignedWorldManager { + private static final long CLOSE_AWAIT_MS = 3_000; + + private final Looper looper; + private final KList updateQueue = new KList<>(); + final ChronoLatch cl; + private final ChronoLatch clw; + private final ChronoLatch cln; + private final ChronoLatch chunkUpdater; + private final ChronoLatch chunkDiscovery; + private final KMap> cleanup = new KMap<>(); + private final ScheduledExecutorService cleanupService; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final WorldEntitySpawner entitySpawner = new WorldEntitySpawner(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final WorldChunkMaintenance chunkMaintenance = new WorldChunkMaintenance(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final MarkerSpawnScanner markerScanner = new MarkerSpawnScanner(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final WorldBlockDropRouter blockDropRouter = new WorldBlockDropRouter(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup(this); + private boolean looperStopped; + private boolean cleanupServiceStopped; + volatile int entityCount = 0; + volatile boolean entityCountValid = false; + volatile boolean playersPresent = false; + private KSet injectBiomes = new KSet<>(); + volatile int loadedChunkCount = 0; + + public IrisWorldManager() { + super(null); + cl = null; + cln = null; + clw = null; + looper = null; + chunkUpdater = null; + chunkDiscovery = null; + cleanupService = null; + } + + public IrisWorldManager(Engine engine) { + super(engine); + chunkUpdater = new ChronoLatch(3000); + chunkDiscovery = new ChronoLatch(5000); + cln = new ChronoLatch(60000); + cl = new ChronoLatch(3000); + clw = new ChronoLatch(1000, true); + cleanupService = Executors.newSingleThreadScheduledExecutor(runnable -> { + Thread thread = new Thread(runnable, "Iris Mantle Cleanup " + getTarget().getWorld().name()); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + looper = new Looper() { + @Override + protected long loop() { + if (!isManagerStarted()) { + return -1L; + } + return callManagerTask( + "bukkit_world_manager_loop", + IrisWorldManager.this::runLoop, + 250L); + } + }; + looper.setPriority(Thread.MIN_PRIORITY); + looper.setName("Iris World Manager " + getTarget().getWorld().name()); + } + + private long runLoop() { + if (getEngine().isClosed()) { + looper.interrupt(); + return -1L; + } + + if (!getEngine().getWorld().hasPlatformWorld() && clw.flip()) { + J.runGlobal(() -> runManagerTask( + "bukkit_world_manager_bind", + () -> BukkitWorldBinding.tryBind(getEngine().getWorld()))); + } + + if (getEngine().getWorld().hasPlatformWorld()) { + if (chunkUpdater.flip()) { + chunkMaintenance.updateChunks(); + } + + if (!playersPresent) { + return 5000L; + } + + if (chunkDiscovery.flip()) { + chunkMaintenance.discoverChunks(); + } + + if (cln.flip()) { + getEngine().getEngineData().cleanup(getEngine()); + } + + if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem() + && !IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) { + return 3000L; + } + + entitySpawner.onAsyncTick(); + } + + return IrisSettings.get().getWorld().getAsyncTickIntervalMS(); + } + + @Override + public void start() { + super.start(); + if (!looper.isAlive()) { + looper.start(); + } + } + + Runnable managedTask(String operation, Runnable task) { + return () -> runManagerTask(operation, task); + } + + Runnable managedTask(String operation, Runnable task, Runnable unavailable) { + return () -> { + if (!runManagerTask(operation, task)) { + unavailable.run(); + } + }; + } + + AtomicBoolean ignoreTeleport() { + return ignoreTP; + } + + @Override + public void onTick() { + + } + + @Override + public void onSave() { + getEngine().getMantle().save(); + } + + public void requestBiomeInject(Position2 p) { + injectBiomes.add(p); + } + + @Override + public void onChunkLoad(Chunk e, boolean generated) { + if (getEngine().isClosed()) { + return; + } + + if (cleanupServiceStopped || cleanupService == null || cleanupService.isShutdown()) { + return; + } + + int cX = e.getX(), cZ = e.getZ(); + Long key = Cache.key(cX, cZ); + long delay = Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0); + final Future[] self = new Future[1]; + Runnable forget = () -> cleanup.remove(key, self[0]); + Runnable task = managedTask("bukkit_world_manager_chunk_cleanup", () -> { + forget.run(); + getEngine().cleanupMantleChunk(cX, cZ); + }, forget); + + try { + // compute() keys the bin so the task cannot drop a newer mapping than its own. + cleanup.compute(key, (k, displaced) -> { + if (displaced != null) { + displaced.cancel(false); + } + + return self[0] = cleanupService.schedule(task, delay, TimeUnit.MILLISECONDS); + }); + } catch (RejectedExecutionException ex) { + IrisLogging.debug("Skipped mantle cleanup schedule for " + cX + ", " + cZ + "; cleanup executor is stopped."); + } + } + + @Override + public void onChunkUnload(Chunk e) { + final var future = cleanup.remove(Cache.key(e.getX(), e.getZ())); + if (future != null) { + future.cancel(false); + } + } + + public Mantle getMantle() { + return getEngine().getMantle().getMantle(); + } + + @Override + public void teleportAsync(PlayerTeleportEvent e) { + teleportWarmup.teleportAsync(e); + } + + @Override + public void onBlockBreak(BlockBreakEvent e) { + blockDropRouter.onBlockBreak(e); + } + + @Override + public void onBlockPlace(BlockPlaceEvent e) { + blockDropRouter.onBlockPlace(e); + } + + @Override + public synchronized void close() { + Throwable failure = null; + try { + super.close(); + } catch (Throwable e) { + failure = e; + } + if (!looperStopped) { + try { + looperStopped = true; + if (looper != null) { + looper.interrupt(); + joinQuietly(looper); + } + } catch (Throwable e) { + failure = appendCloseFailure(failure, e); + } + } + if (!cleanupServiceStopped) { + try { + cleanupServiceStopped = true; + if (cleanupService != null) { + cleanupService.shutdownNow(); + awaitQuietly(cleanupService); + } + } catch (Throwable e) { + failure = appendCloseFailure(failure, e); + } + } + if (failure != null) { + throw new IllegalStateException("Failed to completely stop the Bukkit Iris world manager.", failure); + } + } + + private void joinQuietly(Thread thread) { + if (thread == Thread.currentThread()) { + return; + } + + try { + thread.join(CLOSE_AWAIT_MS); + if (thread.isAlive()) { + IrisLogging.warn("Thread " + thread.getName() + " did not stop within " + CLOSE_AWAIT_MS + "ms."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void awaitQuietly(ScheduledExecutorService service) { + try { + if (!service.awaitTermination(CLOSE_AWAIT_MS, TimeUnit.MILLISECONDS)) { + IrisLogging.warn("Mantle cleanup executor did not stop within " + CLOSE_AWAIT_MS + "ms."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public int getChunkCount() { + return loadedChunkCount; + } + + @Override + public double getEntitySaturation() { + if (!getEngine().getWorld().hasPlatformWorld()) { + return 1; + } + + return (double) entityCount / (loadedChunkCount + 1) * 1.28; + } + + private static Throwable appendCloseFailure(Throwable failure, Throwable next) { + if (failure == null) { + return next; + } + if (failure != next) { + failure.addSuppressed(next); + } + return failure; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/MarkerSpawnScanner.java b/core/src/main/java/art/arcane/iris/engine/MarkerSpawnScanner.java new file mode 100644 index 000000000..5abb03270 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/MarkerSpawnScanner.java @@ -0,0 +1,224 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.object.IrisMarker; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.Chunks; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.matter.MatterMarker; +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; + +/** + * Resolves the mantle spawn markers of a chunk into spawners. On Folia the mantle read runs + * asynchronously and only the obstruction check and the consumer callback hop back onto the region + * thread that owns the chunk; obstructed markers are removed off-thread. + */ +final class MarkerSpawnScanner { + private final IrisWorldManager manager; + private final Set markerScanQueue = ConcurrentHashMap.newKeySet(); + + MarkerSpawnScanner(IrisWorldManager manager) { + this.manager = manager; + } + + Map> getSpawnersFromMarkers(Chunk c) { + Map> p = new KMap<>(); + Set b = new KSet<>(); + + if (J.isFolia()) { + if (!manager.getMantle().isChunkLoaded(c.getX(), c.getZ())) { + manager.chunkMaintenance.warmupMantleChunkAsync(c.getX(), c.getZ()); + } + return p; + } + + manager.getMantle().iterateChunk(c.getX(), c.getZ(), MatterMarker.class, (x, y, z, t) -> { + if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) { + return; + } + + IrisMarker mark = manager.getData().getMarkerLoader().load(t.getTag()); + if (mark == null) { + return; + } + + IrisPosition pos = new IrisPosition((c.getX() << 4) + x, y, (c.getZ() << 4) + z); + + if (isMarkerObstructed(c, pos, mark.isEmptyAbove())) { + b.add(pos); + return; + } + + for (String i : mark.getSpawners()) { + IrisSpawner m = manager.getData().getSpawnerLoader().load(i); + if (m == null) { + IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName()); + continue; + } + m.setReferenceMarker(mark); + + // This is so fucking incorrect its a joke + //noinspection ConstantConditions + if (m != null) { + p.computeIfAbsent(pos, (k) -> new KSet<>()).add(m); + } + } + }); + + for (IrisPosition i : b) { + manager.getEngine().getMantle().getMantle().remove(i.getX(), i.getY(), i.getZ(), MatterMarker.class); + } + + return p; + } + + void forEachMarkerSpawner(Chunk c, BiConsumer> consumer) { + if (c == null || consumer == null) { + return; + } + + if (!J.isFolia()) { + int minY = manager.getEngine().getWorld().minHeight(); + getSpawnersFromMarkers(c).forEach((relative, spawners) -> { + if (spawners.isEmpty()) { + return; + } + + consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), spawners); + }); + return; + } + + int chunkX = c.getX(); + int chunkZ = c.getZ(); + World world = c.getWorld(); + long key = Cache.key(chunkX, chunkZ); + if (!markerScanQueue.add(key)) { + return; + } + + J.a(manager.managedTask("bukkit_world_manager_marker_scan", () -> { + try { + Map markerData = collectMarkerSpawnData(chunkX, chunkZ); + if (markerData.isEmpty()) { + return; + } + + J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_marker_scan_region", () -> { + if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; + } + + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + int minY = manager.getEngine().getWorld().minHeight(); + markerData.forEach((relative, data) -> { + if (data.spawners.isEmpty()) { + return; + } + + if (isMarkerObstructed(chunk, relative, data.requiresEmptyAbove)) { + removeMarkerAsync(relative); + return; + } + + consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners); + }); + })); + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + markerScanQueue.remove(key); + } + }, () -> markerScanQueue.remove(key))); + } + + private Map collectMarkerSpawnData(int chunkX, int chunkZ) { + Map markerData = new KMap<>(); + manager.getMantle().iterateChunk(chunkX, chunkZ, MatterMarker.class, (x, y, z, t) -> { + if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) { + return; + } + + IrisMarker mark = manager.getData().getMarkerLoader().load(t.getTag()); + if (mark == null) { + return; + } + + IrisPosition position = new IrisPosition((chunkX << 4) + x, y, (chunkZ << 4) + z); + MarkerSpawnData data = markerData.computeIfAbsent(position, k -> new MarkerSpawnData()); + data.requiresEmptyAbove = data.requiresEmptyAbove || mark.isEmptyAbove(); + + for (String i : mark.getSpawners()) { + IrisSpawner spawner = manager.getData().getSpawnerLoader().load(i); + if (spawner == null) { + IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName()); + continue; + } + spawner.setReferenceMarker(mark); + data.spawners.add(spawner); + } + }); + + return markerData; + } + + private boolean isMarkerObstructed(Chunk chunk, IrisPosition relative, boolean requiresEmptyAbove) { + if (!requiresEmptyAbove) { + return false; + } + + int minY = manager.getEngine().getWorld().minHeight(); + int markerY = WorldBlockDropRouter.toWorldY(relative.getY(), minY); + if (markerY + 2 >= chunk.getWorld().getMaxHeight()) { + return true; + } + + int localX = relative.getX() & 15; + int localZ = relative.getZ() & 15; + return chunk.getBlock(localX, markerY + 1, localZ).getBlockData().getMaterial().isSolid() + || chunk.getBlock(localX, markerY + 2, localZ).getBlockData().getMaterial().isSolid(); + } + + private void removeMarkerAsync(IrisPosition marker) { + J.a(manager.managedTask("bukkit_world_manager_remove_marker", () -> { + try { + manager.getMantle().remove(marker.getX(), marker.getY(), marker.getZ(), MatterMarker.class); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + })); + } + + private static final class MarkerSpawnData { + private final KSet spawners = new KSet<>(); + private boolean requiresEmptyAbove; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/NativeStructureBootstrapBarrier.java b/core/src/main/java/art/arcane/iris/engine/NativeStructureBootstrapBarrier.java new file mode 100644 index 000000000..0817e0829 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/NativeStructureBootstrapBarrier.java @@ -0,0 +1,95 @@ +package art.arcane.iris.engine; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +final class NativeStructureBootstrapBarrier { + private static final long TRANSITION_TIMEOUT_SECONDS = 120L; + + private CompletableFuture active; + private CompletableFuture poisoned; + + synchronized CompletableFuture start( + Runnable claim, + Supplier> starter + ) { + Objects.requireNonNull(claim, "Native structure bootstrap claim"); + Objects.requireNonNull(starter, "Native structure bootstrap starter"); + if (active != null) { + throw new IllegalStateException("Native structure bootstrap is already active."); + } + claim.run(); + CompletableFuture bridge = new CompletableFuture<>(); + active = bridge; + bridge.whenComplete((ignored, failure) -> clearSuccessful(bridge, failure)); + try { + CompletableFuture completion = Objects.requireNonNull( + starter.get(), + "Native structure bootstrap completion"); + completion.whenComplete((ignored, failure) -> completeBridge(bridge, failure)); + return bridge; + } catch (Throwable failure) { + poisoned = bridge; + bridge.completeExceptionally(failure); + if (failure instanceof RuntimeException runtimeFailure) { + throw runtimeFailure; + } + if (failure instanceof Error error) { + throw error; + } + throw new IllegalStateException("Native structure bootstrap could not start.", failure); + } + } + + synchronized void await(String transition) { + CompletableFuture completion = active; + if (completion == null) { + return; + } + try { + completion.get(TRANSITION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + active = null; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException( + "Interrupted while waiting for native structure bootstrap before " + transition + ".", e); + } catch (ExecutionException e) { + if (poisoned != completion) { + active = null; + } + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new IllegalStateException( + "Native structure bootstrap failed before " + transition + ".", cause); + } catch (TimeoutException e) { + throw new IllegalStateException( + "Native structure bootstrap did not finish before " + transition + " within " + + TRANSITION_TIMEOUT_SECONDS + " seconds.", e); + } + } + + synchronized boolean isActive() { + return active != null; + } + + synchronized boolean isPoisoned() { + return poisoned != null; + } + + private void completeBridge(CompletableFuture bridge, Throwable failure) { + if (failure == null) { + bridge.complete(null); + return; + } + bridge.completeExceptionally(failure); + } + + private synchronized void clearSuccessful(CompletableFuture completion, Throwable failure) { + if (failure == null && active == completion) { + active = null; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java new file mode 100644 index 000000000..8df80bd79 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java @@ -0,0 +1,355 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.InferredType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.engine.object.IrisInterpolator; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisShapedGeneratorStyle; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.data.DataProvider; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.interpolation.NoiseBounds; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +public class UpperDimensionContext implements DataProvider { + private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); + private final IrisDimension dimension; + private final IrisData data; + private final int chunkHeight; + private final ProceduralStream heightStream; + private final ProceduralStream biomeStream; + private final ProceduralStream regionStream; + private final ProceduralStream rockStream; + private final boolean selfReferencing; + + private UpperDimensionContext(IrisDimension dimension, IrisData data, int chunkHeight, + ProceduralStream heightStream, + ProceduralStream biomeStream, + ProceduralStream regionStream, + ProceduralStream rockStream, + boolean selfReferencing) { + this.dimension = dimension; + this.data = data; + this.chunkHeight = chunkHeight; + this.heightStream = heightStream; + this.biomeStream = biomeStream; + this.regionStream = regionStream; + this.rockStream = rockStream; + this.selfReferencing = selfReferencing; + } + + public static UpperDimensionContext create(Engine engine, IrisDimension upperDim) { + boolean selfRef = upperDim.getLoadKey().equals(engine.getDimension().getLoadKey()); + int chunkHeight = engine.getHeight(); + if (selfRef) { + return createSelfReferencing(engine, chunkHeight); + } + return createCrossReferencing(engine, upperDim, chunkHeight); + } + + private static UpperDimensionContext createSelfReferencing(Engine engine, int chunkHeight) { + IrisComplex complex = engine.getComplex(); + return new UpperDimensionContext( + engine.getDimension(), + engine.getData(), + chunkHeight, + complex.getHeightStream(), + complex.getTrueBiomeStream(), + complex.getRegionStream(), + complex.getRockStream(), + true + ); + } + + private static UpperDimensionContext createCrossReferencing(Engine engine, IrisDimension upperDim, int chunkHeight) { + IrisData resolvedData = upperDim.getLoader(); + if (resolvedData == null) { + resolvedData = engine.getData(); + } + IrisData upperData = resolvedData; + long seedOffset = upperDim.getLoadKey().hashCode(); + RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset); + double fluidHeight = upperDim.getFluidHeight(); + DataProvider dataProvider = () -> upperData; + + Map> generators = new HashMap<>(); + Set allBiomes = Collections.newSetFromMap(new IdentityHashMap<>()); + Map childSelectionPlans = + Collections.synchronizedMap(new IdentityHashMap<>()); + upperDim.getRegions().forEach(regionKey -> { + IrisRegion region = upperData.getRegionLoader().load(regionKey); + if (region != null) { + region.getAllBiomes(dataProvider).forEach(biome -> { + allBiomes.add(biome); + biome.getGenerators().forEach(link -> { + IrisGenerator gen = link.getCachedGenerator(dataProvider); + if (gen != null) { + generators.computeIfAbsent(gen.getInterpolator(), k -> new HashSet<>()).add(gen); + } + }); + }); + } + }); + + Map> generatorBounds = new HashMap<>(); + for (Map.Entry> entry : generators.entrySet()) { + IdentityHashMap interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16)); + for (IrisBiome biome : allBiomes) { + double min = 0D; + double max = 0D; + for (IrisGenerator gen : entry.getValue()) { + String key = gen.getLoadKey(); + if (key == null || key.isBlank()) { + continue; + } + max += biome.getGenLinkMax(key, engine); + min += biome.getGenLinkMin(key, engine); + } + interpolatorBounds.put(biome, new NoiseBounds(min, max)); + } + generatorBounds.put(entry.getKey(), interpolatorBounds); + } + + ProceduralStream regionStyleStream = upperDim.getRegionStyle() + .create(rng.nextParallelRNG(883), upperData).stream() + .zoom(upperDim.getRegionZoom()); + ProceduralStream regionStream = regionStyleStream + .selectRarity(upperData.getRegionLoader().loadAll(upperDim.getRegions())); + + ProceduralStream landBiomeStream = regionStream + .convert(r -> upperDim.getLandBiomeStyle() + .create(rng.nextParallelRNG(InferredType.LAND.ordinal()), upperData).stream() + .zoom(upperDim.getBiomeZoom()) + .zoom(upperDim.getLandZoom()) + .zoom(r.getLandBiomeZoom()) + .selectRarity(loadInferredBiomes(upperData, r.getLandBiomes(), InferredType.LAND))) + .convertAware2D(ProceduralStream::get); + ProceduralStream seaBiomeStream = regionStream + .convert(r -> upperDim.getSeaBiomeStyle() + .create(rng.nextParallelRNG(InferredType.SEA.ordinal()), upperData).stream() + .zoom(upperDim.getBiomeZoom()) + .zoom(upperDim.getSeaZoom()) + .zoom(r.getSeaBiomeZoom()) + .selectRarity(loadInferredBiomes(upperData, r.getSeaBiomes(), InferredType.SEA))) + .convertAware2D(ProceduralStream::get); + ProceduralStream shoreBiomeStream = regionStream + .convert(r -> upperDim.getShoreBiomeStyle() + .create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream() + .zoom(upperDim.getBiomeZoom()) + .zoom(r.getShoreBiomeZoom()) + .selectRarity(loadInferredBiomes(upperData, r.getShoreBiomes(), InferredType.SHORE))) + .convertAware2D(ProceduralStream::get); + + Map> inferredStreams = new HashMap<>(); + inferredStreams.put(InferredType.LAND, landBiomeStream); + inferredStreams.put(InferredType.SEA, seaBiomeStream); + inferredStreams.put(InferredType.SHORE, shoreBiomeStream); + + ProceduralStream bridgeStream = upperDim.getContinentalStyle() + .create(rng.nextParallelRNG(234234565), upperData) + .bake().scale(1D / upperDim.getContinentZoom()).bake().stream() + .convert(v -> v >= upperDim.getLandChance() ? InferredType.SEA : InferredType.LAND); + + ProceduralStream baseBiomeStream = bridgeStream + .convertAware2D((t, x, z) -> { + ProceduralStream stream = inferredStreams.get(t); + return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z); + }) + .convertAware2D((biome, x, z) -> implode( + biome, x, z, rng, dataProvider, childSelectionPlans, 3)); + + KList overlayNoise = upperDim.getOverlayNoise(); + ProceduralStream overlayStream = overlayNoise.isEmpty() + ? ProceduralStream.ofDouble((x, z) -> 0.0D) + : ProceduralStream.ofDouble((x, z) -> { + double value = 0D; + for (IrisShapedGeneratorStyle style : overlayNoise) { + value += style.get(rng, upperData, x, z); + } + return value; + }); + + long heightSeed = engine.getSeedManager().getHeight() ^ seedOffset; + + ProceduralStream heightStream = ProceduralStream.of((x, z) -> { + IrisBiome b = baseBiomeStream.get(x, z); + if (b == null) { + return fluidHeight; + } + double interpolatedHeight = 0; + for (Map.Entry> entry : generators.entrySet()) { + IrisInterpolator interpolator = entry.getKey(); + Set gens = entry.getValue(); + if (gens.isEmpty()) { + continue; + } + IdentityHashMap cachedBounds = generatorBounds.get(interpolator); + NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (xx, zz) -> { + try { + IrisBiome bx = baseBiomeStream.get(xx, zz); + if (bx == null) { + return ZERO_NOISE_BOUNDS; + } + NoiseBounds bounds = cachedBounds != null ? cachedBounds.get(bx) : null; + if (bounds != null) { + return bounds; + } + double bMin = 0D; + double bMax = 0D; + for (IrisGenerator gen : gens) { + String key = gen.getLoadKey(); + if (key == null || key.isBlank()) { + continue; + } + bMax += bx.getGenLinkMax(key, engine); + bMin += bx.getGenLinkMin(key, engine); + } + return new NoiseBounds(bMin, bMax); + } catch (Throwable e) { + IrisLogging.reportError(e); + return ZERO_NOISE_BOUNDS; + } + }); + double hi = sampledBounds.max(); + double lo = sampledBounds.min(); + double d = 0; + for (IrisGenerator gen : gens) { + d += M.lerp(lo, hi, gen.getHeight(x, z, heightSeed + 239945)); + } + interpolatedHeight += d / gens.size(); + } + return Math.max(Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight), 0); + }, Interpolated.DOUBLE); + + ProceduralStream finalBiomeStream = heightStream.convertAware2D((height, x, z) -> { + IrisBiome baseBiome = baseBiomeStream.get(x, z); + IrisBiome resolved = IrisComplex.resolveSurfaceBiome( + height, + baseBiome, + regionStream.get(x, z), + x, + z, + fluidHeight, + landBiomeStream, + seaBiomeStream, + shoreBiomeStream); + return resolved == baseBiome + ? baseBiome + : implode(resolved, x, z, rng, dataProvider, childSelectionPlans, 3); + }); + + ProceduralStream rockStream = upperDim.getRockPalette() + .getLayerGenerator(rng.nextParallelRNG(45), upperData).stream() + .select(upperDim.getRockPalette().getBlockData(upperData)); + + return new UpperDimensionContext( + upperDim, + upperData, + chunkHeight, + heightStream, + finalBiomeStream, + regionStream, + rockStream, + false + ); + } + + private static KList loadInferredBiomes(IrisData data, KList keys, InferredType type) { + KList inferred = new KList<>(); + for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) { + inferred.add(biome.withInferredType(type)); + } + return inferred; + } + + private static IrisBiome implode( + IrisBiome biome, + double x, + double z, + RNG rng, + DataProvider dataProvider, + Map childSelectionPlans, + int remainingDepth + ) { + if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) { + return biome; + } + + IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome); + if (selectionPlan == null) { + synchronized (childSelectionPlans) { + selectionPlan = childSelectionPlans.get(biome); + if (selectionPlan == null) { + KList options = new KList<>(); + for (IrisBiome child : biome.getRealChildren(dataProvider)) { + if (child != null) { + options.add(child); + } + } + options.add(biome); + selectionPlan = IrisComplex.ChildSelectionPlan.create(options); + childSelectionPlans.put(biome, selectionPlan); + } + } + } + + IrisBiome selected = selectionPlan.select( + biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z); + if (selected == null) { + return biome; + } + return implode( + selected.withInferredType(biome.getInferredType()), + x, + z, + rng, + dataProvider, + childSelectionPlans, + remainingDepth - 1); + } + + public int getUpperSurfaceY(int x, int z) { + double rawHeight = heightStream.get((double) x, (double) z); + return chunkHeight - 1 - (int) Math.round(rawHeight); + } + + public IrisBiome getUpperBiome(int x, int z) { + return biomeStream.get((double) x, (double) z); + } + + public IrisRegion getUpperRegion(int x, int z) { + return regionStream == null ? null : regionStream.get((double) x, (double) z); + } + + public PlatformBlockState getRockBlock(int x, int z) { + return rockStream.get((double) x, (double) z); + } + + public IrisDimension getDimension() { + return dimension; + } + + @Override + public IrisData getData() { + return data; + } + + public boolean isSelfReferencing() { + return selfReferencing; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/WorldBlockDropRouter.java b/core/src/main/java/art/arcane/iris/engine/WorldBlockDropRouter.java new file mode 100644 index 000000000..9d1b69a46 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/WorldBlockDropRouter.java @@ -0,0 +1,134 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.service.tree.BlockDropRouter; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBlockDrops; +import art.arcane.iris.engine.object.IrisMarker; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.matter.MatterMarker; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Biome, region and dimension block drops for a Bukkit Iris world. The drop list is resolved on the + * event thread, the marker cleanup runs asynchronously and the drops themselves are handed to the + * event's own router when it offers one so a deferred break keeps its inventory destination. + */ +final class WorldBlockDropRouter { + private final IrisWorldManager manager; + + WorldBlockDropRouter(IrisWorldManager manager) { + this.manager = manager; + } + + void onBlockBreak(BlockBreakEvent e) { + if (e.getBlock().getWorld().equals(BukkitWorldBinding.world(manager.getTarget().getWorld()))) { + int blockX = e.getBlock().getX(); + int mantleY = toMantleY(e.getBlock().getY(), manager.getEngine().getWorld().minHeight()); + int blockZ = e.getBlock().getZ(); + + KList d = new KList<>(); + IrisBiome b = EngineBukkitOps.getBiome(manager.getEngine(), e.getBlock().getLocation()); + List dropProviders = filterDrops(b.getBlockDrops(), e, manager.getData()); + + if (dropProviders.stream().noneMatch(IrisBlockDrops::isSkipParents)) { + IrisRegion r = EngineBukkitOps.getRegion(manager.getEngine(), e.getBlock().getLocation()); + dropProviders.addAll(filterDrops(r.getBlockDrops(), e, manager.getData())); + dropProviders.addAll(filterDrops(manager.getEngine().getDimension().getBlockDrops(), e, manager.getData())); + } + + dropProviders.forEach(provider -> provider.fillDrops(false, d)); + + if (dropProviders.stream().anyMatch(IrisBlockDrops::isReplaceVanillaDrops)) { + e.setDropItems(false); + } + + World w = e.getBlock().getWorld(); + Location blockLocation = e.getBlock().getLocation(); + Location dropLocation = blockLocation.clone().add(.5, .5, .5); + BlockDropRouter dropRouter = e instanceof BlockDropRouter router ? router : null; + Runnable finalizedBreak = manager.managedTask("bukkit_world_manager_block_break_finalize", () -> { + if (e.isCancelled()) { + return; + } + J.a(manager.managedTask("bukkit_world_manager_block_break_marker", () -> { + MatterMarker marker = manager.getMantle().get(blockX, mantleY, blockZ, MatterMarker.class); + if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) { + return; + } + + IrisMarker mark = manager.getData().getMarkerLoader().load(marker.getTag()); + if (mark == null || mark.isRemoveOnChange()) { + manager.getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class); + } + })); + routeDrops(d, dropRouter, item -> w.dropItemNaturally(dropLocation, item)); + }); + if (!J.runAt(blockLocation, finalizedBreak, 1) && !J.isFolia()) { + J.s(finalizedBreak, 1); + } + } + } + + void onBlockPlace(BlockPlaceEvent e) { + + } + + static int toMantleY(int worldY, int minHeight) { + return worldY - minHeight; + } + + static void routeDrops(Iterable drops, BlockDropRouter router, Consumer fallback) { + for (T drop : drops) { + boolean routed = false; + if (router != null) { + try { + routed = router.routeDrop(drop); + } catch (Throwable error) { + IrisLogging.reportError("Failed to route a deferred Iris block drop.", error); + } + } + if (!routed) { + fallback.accept(drop); + } + } + } + + static int toWorldY(int mantleY, int minHeight) { + return mantleY + minHeight; + } + + private List filterDrops(KList drops, BlockBreakEvent e, IrisData data) { + return new KList<>(drops.stream().filter(d -> d.shouldDropFor(e.getBlock().getBlockData(), data)).toList()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/WorldChunkMaintenance.java b/core/src/main/java/art/arcane/iris/engine/WorldChunkMaintenance.java new file mode 100644 index 000000000..fb57aa84d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/WorldChunkMaintenance.java @@ -0,0 +1,405 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.Chunks; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Chunk discovery, post-load block updates and mantle warmup for a Bukkit Iris world. Every scan is + * scheduled from the global thread, fans out per player onto the owning entity thread and touches a + * chunk only from its own region thread; the scan-scheduled flags and per-chunk key sets keep a + * single pass in flight per chunk and are always cleared on the rejection path. + */ +final class WorldChunkMaintenance { + private static final int MAX_FORCED_CHUNK_UPDATES = 128; + + private final IrisWorldManager manager; + private final Set mantleWarmupQueue = ConcurrentHashMap.newKeySet(); + private final Set markerFlagQueue = ConcurrentHashMap.newKeySet(); + private final Set discoveredFlagQueue = ConcurrentHashMap.newKeySet(); + private final Set chunkUpdateQueue = ConcurrentHashMap.newKeySet(); + private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean(); + private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean(); + private int forcedChunkUpdateCursor = 0; + + WorldChunkMaintenance(IrisWorldManager manager) { + this.manager = manager; + } + + void discoverChunks() { + World world = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (world == null) { + return; + } + + if (manager.entitySpawner.isPregenActiveForThisWorld()) { + return; + } + + if (!chunkDiscoveryScanScheduled.compareAndSet(false, true)) { + return; + } + + boolean scheduled = J.runGlobal(manager.managedTask("bukkit_world_manager_discover_chunks", () -> { + try { + if (manager.getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(manager.getEngine().getWorld()))) { + return; + } + + for (Player player : world.getPlayers()) { + if (player == null) { + continue; + } + + J.runEntity(player, manager.managedTask("bukkit_world_manager_discover_player", () -> { + if (!player.isOnline() || !world.equals(player.getWorld())) { + return; + } + + int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX()); + int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ()); + int radius = 1; + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + int chunkX = centerX + x; + int chunkZ = centerZ + z; + raiseDiscoveredChunkFlag(world, chunkX, chunkZ); + } + } + })); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + chunkDiscoveryScanScheduled.set(false); + } + }, () -> chunkDiscoveryScanScheduled.set(false))); + if (!scheduled) { + chunkDiscoveryScanScheduled.set(false); + } + } + + private void raiseDiscoveredChunkFlag(World world, int chunkX, int chunkZ) { + if (world == null) { + return; + } + + if (!J.isFolia()) { + manager.getMantle().getChunk(chunkX, chunkZ).flag(MantleFlag.DISCOVERED, true); + return; + } + + long key = Cache.key(chunkX, chunkZ); + if (!discoveredFlagQueue.add(key)) { + return; + } + + J.a(manager.managedTask("bukkit_world_manager_discovered_flag", () -> { + try { + Mantle mantle = manager.getMantle(); + if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.DISCOVERED)) { + mantle.flag(chunkX, chunkZ, MantleFlag.DISCOVERED, true); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + discoveredFlagQueue.remove(key); + } + }, () -> discoveredFlagQueue.remove(key))); + } + + void updateChunks() { + World world = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (world == null) { + return; + } + + if (manager.entitySpawner.isPregenActiveForThisWorld()) { + return; + } + + if (!chunkUpdateScanScheduled.compareAndSet(false, true)) { + return; + } + + boolean scheduled = J.runGlobal(manager.managedTask( + "bukkit_world_manager_update_chunks", + () -> updateChunksOnGlobal(world), + () -> chunkUpdateScanScheduled.set(false))); + if (!scheduled) { + chunkUpdateScanScheduled.set(false); + } + } + + private void updateChunksOnGlobal(World world) { + try { + if (manager.getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(manager.getEngine().getWorld()))) { + return; + } + + List players = new ArrayList<>(world.getPlayers()); + manager.playersPresent = !players.isEmpty(); + manager.loadedChunkCount = world.getLoadedChunks().length; + for (Player player : players) { + if (player == null) { + continue; + } + + J.runEntity(player, manager.managedTask( + "bukkit_world_manager_player_chunk_updates", + () -> schedulePlayerChunkUpdates(world, player))); + } + + scheduleForcedChunkUpdates(world); + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + chunkUpdateScanScheduled.set(false); + } + } + + private void schedulePlayerChunkUpdates(World world, Player player) { + if (!player.isOnline() || !world.equals(player.getWorld())) { + return; + } + + int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX()); + int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ()); + int radius = 1; + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + scheduleChunkUpdate(world, centerX + x, centerZ + z); + } + } + } + + private void scheduleForcedChunkUpdates(World world) { + List forcedChunks = new ArrayList<>(); + for (Chunk chunk : world.getForceLoadedChunks()) { + forcedChunks.add(new Position2(chunk.getX(), chunk.getZ())); + } + forcedChunks.sort(Comparator.comparingInt(Position2::getX).thenComparingInt(Position2::getZ)); + + int forcedChunkCount = forcedChunks.size(); + if (forcedChunkCount == 0) { + forcedChunkUpdateCursor = 0; + return; + } + + int updateCount = Math.min(forcedChunkCount, MAX_FORCED_CHUNK_UPDATES); + int start = Math.floorMod(forcedChunkUpdateCursor, forcedChunkCount); + for (int i = 0; i < updateCount; i++) { + Position2 chunk = forcedChunks.get((start + i) % forcedChunkCount); + scheduleChunkUpdate(world, chunk.getX(), chunk.getZ()); + } + forcedChunkUpdateCursor = (start + updateCount) % forcedChunkCount; + } + + private void scheduleChunkUpdate(World world, int chunkX, int chunkZ) { + long key = Cache.key(chunkX, chunkZ); + if (!chunkUpdateQueue.add(key)) { + return; + } + + try { + boolean scheduled = J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_chunk_update", () -> { + try { + updateChunkRegion(world, chunkX, chunkZ); + } finally { + chunkUpdateQueue.remove(key); + } + }, () -> chunkUpdateQueue.remove(key))); + if (!scheduled) { + chunkUpdateQueue.remove(key); + } + } catch (Throwable e) { + chunkUpdateQueue.remove(key); + IrisLogging.reportError(e); + } + } + + private void updateChunkRegion(World world, int chunkX, int chunkZ) { + if (world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; + } + + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + + if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) { + if (!manager.getMantle().isChunkLoaded(chunkX, chunkZ)) { + warmupMantleChunkAsync(chunkX, chunkZ); + return; + } + EngineBukkitOps.updateChunk(manager.getEngine(), chunk); + } + + if (!manager.entitySpawner.isEntitySpawningEnabledForCurrentWorld()) { + return; + } + + if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { + return; + } + + if (!J.isFolia() && !manager.getMantle().isChunkLoaded(chunkX, chunkZ)) { + warmupMantleChunkAsync(chunkX, chunkZ); + return; + } + + raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> { + int delay = RNG.r.i(5, 200); + J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_initial_spawn_followup", () -> { + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return; + } + manager.entitySpawner.spawnIn(world.getChunkAt(chunkX, chunkZ), true); + }), delay); + + Chunk markerChunk = world.getChunkAt(chunkX, chunkZ); + manager.markerScanner.forEachMarkerSpawner(markerChunk, (block, spawners) -> { + IrisSpawner s = new KList<>(spawners).getRandom(); + if (s == null) { + return; + } + manager.entitySpawner.spawn(block, s, true); + }); + }); + } + + void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) { + if (world == null || onFirstRaise == null) { + return; + } + + if (!J.isFolia()) { + manager.getMantle().raiseFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, onFirstRaise); + return; + } + + long key = Cache.key(chunkX, chunkZ); + if (!markerFlagQueue.add(key)) { + return; + } + + J.a(manager.managedTask("bukkit_world_manager_spawn_marker_flag", () -> { + boolean raised = false; + try { + Mantle mantle = manager.getMantle(); + if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) { + mantle.flag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, true); + raised = true; + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + markerFlagQueue.remove(key); + } + + if (!raised) { + return; + } + + J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_spawn_marker_callback", () -> { + if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; + } + onFirstRaise.run(); + })); + }, () -> markerFlagQueue.remove(key))); + } + + void warmupMantleChunkAsync(int chunkX, int chunkZ) { + long key = Cache.key(chunkX, chunkZ); + if (!mantleWarmupQueue.add(key)) { + return; + } + + J.a(manager.managedTask("bukkit_world_manager_mantle_warmup", () -> { + try { + manager.getMantle().getChunk(chunkX, chunkZ); + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + mantleWarmupQueue.remove(key); + } + }, () -> mantleWarmupQueue.remove(key))); + } + + Position2[] getLoadedChunkPositionsSnapshot(World world) { + if (world == null) { + return new Position2[0]; + } + + CompletableFuture future = new CompletableFuture<>(); + boolean scheduled = J.runGlobal(() -> { + try { + Chunk[] chunks = world.getLoadedChunks(); + Position2[] positions = new Position2[chunks.length]; + for (int i = 0; i < chunks.length; i++) { + positions[i] = new Position2(chunks[i].getX(), chunks[i].getZ()); + } + manager.loadedChunkCount = positions.length; + future.complete(positions); + } catch (Throwable e) { + future.completeExceptionally(e); + } + }); + if (!scheduled) { + return new Position2[0]; + } + + try { + return future.get(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return new Position2[0]; + } catch (ExecutionException | TimeoutException e) { + IrisLogging.reportError(e); + return new Position2[0]; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/WorldEntitySpawner.java b/core/src/main/java/art/arcane/iris/engine/WorldEntitySpawner.java new file mode 100644 index 000000000..5a8d808c8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/WorldEntitySpawner.java @@ -0,0 +1,547 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.object.IRare; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisEntitySpawn; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.Chunks; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.RNG; +import lombok.Data; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Ambient and marker entity spawning for a Bukkit Iris world. Every count and spawn hops onto the + * thread that owns the data it reads (global for whole-world scans, entity for Folia candidate + * scans, region for the chunk being populated) and spawning is paused whenever a count could not + * be completed, so an incomplete saturation reading can never authorize a spawn. + */ +final class WorldEntitySpawner { + private final IrisWorldManager manager; + private final AtomicInteger actuallySpawned = new AtomicInteger(); + private final AtomicBoolean entityCountWarningReported = new AtomicBoolean(); + private final AtomicBoolean entityCountErrorReported = new AtomicBoolean(); + private int cooldown = 0; + + WorldEntitySpawner(IrisWorldManager manager) { + this.manager = manager; + } + + boolean onAsyncTick() { + if (manager.getEngine().isClosing() || manager.getEngine().isClosed()) { + return false; + } + + if (isPregenActiveForThisWorld()) { + J.sleep(500); + return false; + } + + actuallySpawned.set(0); + + if (!manager.getEngine().getWorld().hasPlatformWorld()) { + IrisLogging.debug("Can't spawn. No real world"); + J.sleep(5000); + return false; + } + + if (manager.cl.flip()) { + try { + World realWorld = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (realWorld == null) { + manager.entityCount = 0; + manager.entityCountValid = false; + } else if (J.isFolia()) { + Integer count = getFoliaLivingEntityCount(realWorld); + if (count != null) { + manager.entityCount = count; + manager.entityCountValid = true; + resetEntityCountFailures(); + } else { + manager.entityCountValid = false; + } + } else { + CompletableFuture future = new CompletableFuture<>(); + boolean scheduled = J.runGlobal(() -> { + try { + int count = 0; + for (Entity entity : realWorld.getEntities()) { + if (entity instanceof LivingEntity && !entity.isDead()) { + count++; + } + } + future.complete(count); + } catch (Throwable ex) { + future.completeExceptionally(ex); + } + }); + if (scheduled) { + manager.entityCount = future.get(2, TimeUnit.SECONDS); + manager.entityCountValid = true; + resetEntityCountFailures(); + } else { + reportEntityCountFailure("Unable to schedule the global entity count; pausing Iris entity spawning until a complete count is available.", null); + } + } + } catch (InterruptedException e) { + manager.entityCountValid = false; + Thread.currentThread().interrupt(); + return false; + } catch (TimeoutException e) { + reportEntityCountFailure("Timed out while counting entities; pausing Iris entity spawning until a complete count is available.", null); + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", cause); + } catch (Throwable e) { + reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", e); + } + } + + if (!manager.entityCountValid) { + return false; + } + + double epx = manager.getEntitySaturation(); + if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) { + IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + manager.entityCount + ")"); + J.sleep(5000); + return false; + } + + int spawnBuffer = RNG.r.i(2, 12); + World world = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (world == null) { + return false; + } + + Position2[] cc = manager.chunkMaintenance.getLoadedChunkPositionsSnapshot(world); + while (spawnBuffer-- > 0) { + if (manager.getEngine().isClosing() || manager.getEngine().isClosed()) { + return actuallySpawned.get() > 0; + } + + if (cc.length == 0) { + IrisLogging.debug("Can't spawn. No chunks!"); + return false; + } + + Position2 c = cc[RNG.r.nextInt(cc.length)]; + if (!spawnChunkSafely(world, c.getX(), c.getZ(), false)) { + return actuallySpawned.get() > 0; + } + } + + return actuallySpawned.get() > 0; + } + + boolean isPregenActiveForThisWorld() { + World world = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (world == null) { + return false; + } + + if (IrisToolbelt.isWorldMaintenanceActive(world)) { + return true; + } + + PregeneratorJob job = PregeneratorJob.getInstance(); + if (job == null) { + return false; + } + + return job.targetsWorldIdentity(WorldIdentity.serialize(world)); + } + + private Integer getFoliaLivingEntityCount(World world) { + CompletableFuture> playerFuture = new CompletableFuture<>(); + boolean scheduled = J.runGlobal(() -> { + try { + playerFuture.complete(new ArrayList<>(world.getPlayers())); + } catch (Throwable e) { + playerFuture.completeExceptionally(e); + } + }); + if (!scheduled) { + reportEntityCountFailure("Unable to schedule the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null); + return null; + } + + List players; + try { + players = playerFuture.get(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + manager.entityCountValid = false; + Thread.currentThread().interrupt(); + return null; + } catch (TimeoutException e) { + reportEntityCountFailure("Timed out while reading the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null); + return null; + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + reportEntityCountFailure("Failed to read the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", cause); + return null; + } + + Map candidates = new ConcurrentHashMap<>(); + AtomicBoolean incomplete = new AtomicBoolean(); + AtomicReference failure = new AtomicReference<>(); + + CountDownLatch latch = new CountDownLatch(players.size()); + for (Player player : players) { + if (player == null) { + latch.countDown(); + continue; + } + + if (!J.runEntity(player, () -> { + try { + if (!player.isOnline() || !world.equals(player.getWorld())) { + return; + } + candidates.put(player.getUniqueId().toString(), player); + for (Entity nearby : player.getNearbyEntities(64, 64, 64)) { + if (nearby != null) { + candidates.put(nearby.getUniqueId().toString(), nearby); + } + } + } catch (Throwable e) { + incomplete.set(true); + failure.compareAndSet(null, e); + } finally { + latch.countDown(); + } + })) { + incomplete.set(true); + latch.countDown(); + } + } + + if (!awaitEntityTasks(latch, 2, TimeUnit.SECONDS) || incomplete.get()) { + if (!Thread.currentThread().isInterrupted()) { + reportEntityCountFailure("The Folia entity candidate scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get()); + } + return null; + } + + AtomicInteger count = new AtomicInteger(); + incomplete.set(false); + failure.set(null); + CountDownLatch entityLatch = new CountDownLatch(candidates.size()); + for (Entity entity : candidates.values()) { + if (!J.runEntity(entity, () -> { + try { + if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) { + count.incrementAndGet(); + } + } catch (Throwable e) { + incomplete.set(true); + failure.compareAndSet(null, e); + } finally { + entityLatch.countDown(); + } + })) { + incomplete.set(true); + entityLatch.countDown(); + } + } + + if (!awaitEntityTasks(entityLatch, 2, TimeUnit.SECONDS) || incomplete.get()) { + if (!Thread.currentThread().isInterrupted()) { + reportEntityCountFailure("The Folia entity validation scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get()); + } + return null; + } + + return count.get(); + } + + static boolean awaitEntityTasks(CountDownLatch latch, long timeout, TimeUnit unit) { + try { + return latch.await(timeout, unit); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + private boolean spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) { + if (world == null) { + return false; + } + + CompletableFuture future = new CompletableFuture<>(); + AtomicBoolean failureReported = new AtomicBoolean(); + future.whenComplete((ignored, failure) -> { + if (failure != null) { + reportSpawnFailure(chunkX, chunkZ, failure, failureReported); + } + }); + boolean scheduled; + try { + scheduled = J.runRegion(world, chunkX, chunkZ, () -> { + try { + if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + future.complete(null); + return; + } + + spawnIn(world.getChunkAt(chunkX, chunkZ), initial); + future.complete(null); + } catch (Throwable e) { + future.completeExceptionally(e); + } + }); + } catch (Throwable e) { + IrisLogging.reportError("Failed to schedule an Iris entity spawn for chunk " + chunkX + "," + chunkZ + ".", e); + return false; + } + + if (!scheduled) { + IrisLogging.debug("Skipped Iris entity spawning because the region task was not accepted for chunk " + chunkX + "," + chunkZ + "."); + return false; + } + + try { + future.get(5, TimeUnit.SECONDS); + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } catch (TimeoutException e) { + IrisLogging.warn("Timed out waiting for Iris entity spawning in chunk %d,%d; deferring the remaining spawn buffer.", chunkX, chunkZ); + return false; + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + reportSpawnFailure(chunkX, chunkZ, cause, failureReported); + return false; + } + } + + private void reportEntityCountFailure(String message, Throwable error) { + manager.entityCountValid = false; + if (error != null) { + if (entityCountErrorReported.compareAndSet(false, true)) { + IrisLogging.reportError(message, error); + } + return; + } + + if (entityCountWarningReported.compareAndSet(false, true)) { + IrisLogging.warn(message); + } + } + + private void resetEntityCountFailures() { + entityCountWarningReported.set(false); + entityCountErrorReported.set(false); + } + + private void reportSpawnFailure(int chunkX, int chunkZ, Throwable failure, AtomicBoolean failureReported) { + if (!failureReported.compareAndSet(false, true)) { + return; + } + Throwable cause = failure.getCause() == null ? failure : failure.getCause(); + IrisLogging.reportError("Failed to spawn Iris entities in chunk " + chunkX + "," + chunkZ + ".", cause); + } + + void spawnIn(Chunk c, boolean initial) { + if (manager.getEngine().isClosed()) { + return; + } + + if (!isEntitySpawningEnabledForCurrentWorld()) { + return; + } + + IrisComplex complex = manager.getEngine().getComplex(); + if (complex == null) { + return; + } + + if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { + manager.markerScanner.forEachMarkerSpawner(c, (block, spawners) -> { + IrisSpawner s = new KList<>(spawners).getRandom(); + if (s == null) { + return; + } + + spawn(block, s, false); + J.runRegion(c.getWorld(), c.getX(), c.getZ(), manager.managedTask( + "bukkit_world_manager_marker_spawn_followup", + () -> manager.chunkMaintenance.raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(), + () -> spawn(block, s, true)))); + }); + } + + if (!IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) { + return; + } + + //@builder + Predicate filter = i -> i.canSpawn(manager.getEngine(), c.getX(), c.getZ()); + ChunkCounter counter = new ChunkCounter(c.getEntities()); + + IrisBiome biome = EngineBukkitOps.getSurfaceBiome(manager.getEngine(), c); + IrisEntitySpawn v = spawnRandomly(Stream.concat(manager.getData().getSpawnerLoader() + .loadAll(manager.getDimension().getEntitySpawners()) + .shuffleCopy(RNG.r) + .stream() + .filter(filter) + .filter((i) -> i.isValid(biome)), + Stream.concat(manager.getData() + .getSpawnerLoader() + .loadAll(manager.getEngine().getRegion(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners()) + .shuffleCopy(RNG.r) + .stream() + .filter(filter), + manager.getData().getSpawnerLoader() + .loadAll(manager.getEngine().getSurfaceBiome(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners()) + .shuffleCopy(RNG.r) + .stream() + .filter(filter))) + .filter(counter) + .flatMap((i) -> stream(i, initial)) + .collect(Collectors.toList())) + .getRandom(); + //@done + if (v == null || v.getReferenceSpawner() == null) + return; + + spawn(c, v); + } + + private void spawn(Chunk c, IrisEntitySpawn i) { + IrisSpawner ref = i.getReferenceSpawner(); + int s = i.spawn(manager.getEngine(), c, RNG.r); + actuallySpawned.addAndGet(s); + if (s > 0) { + ref.spawn(manager.getEngine(), c.getX(), c.getZ()); + } + } + + private void spawn(IrisPosition pos, IrisEntitySpawn i) { + IrisSpawner ref = i.getReferenceSpawner(); + if (!ref.canSpawn(manager.getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()))) + return; + + int s = i.spawn(manager.getEngine(), pos, RNG.r); + actuallySpawned.addAndGet(s); + if (s > 0) { + ref.spawn(manager.getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ())); + } + } + + void spawn(IrisPosition block, IrisSpawner spawner, boolean initial) { + if (manager.getEngine().isClosed()) { + return; + } + + if (spawner == null) { + return; + } + + KList s = initial ? spawner.getInitialSpawns() : spawner.getSpawns(); + if (s.isEmpty()) { + return; + } + + IrisEntitySpawn ss = spawnRandomly(s).getRandom(); + ss.setReferenceSpawner(spawner); + ss.setReferenceMarker(spawner.getReferenceMarker()); + spawn(block, ss); + } + + private Stream stream(IrisSpawner s, boolean initial) { + for (IrisEntitySpawn i : initial ? s.getInitialSpawns() : s.getSpawns()) { + i.setReferenceSpawner(s); + i.setReferenceMarker(s.getReferenceMarker()); + } + + return (initial ? s.getInitialSpawns() : s.getSpawns()).stream(); + } + + boolean isEntitySpawningEnabledForCurrentWorld() { + if (!manager.getEngine().isStudio()) { + return true; + } + + return IrisSettings.get().getStudio().isEntitySpawning(); + } + + private KList spawnRandomly(List types) { + return IRare.expandWeighted(types); + } + + @Data + private static class ChunkCounter implements Predicate { + private final Entity[] entities; + private transient int index = 0; + private transient int count = 0; + + @Override + public boolean test(IrisSpawner spawner) { + int max = spawner.getMaxEntitiesPerChunk(); + if (max <= count) + return false; + + while (index < entities.length) { + if (entities[index++] instanceof LivingEntity) { + if (++count >= max) + return false; + } + } + + return true; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/WorldTeleportWarmup.java b/core/src/main/java/art/arcane/iris/engine/WorldTeleportWarmup.java new file mode 100644 index 000000000..a98a74075 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/WorldTeleportWarmup.java @@ -0,0 +1,104 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.spi.IrisLogging; +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.iris.util.common.scheduling.jobs.QueueJob; +import art.arcane.volmlib.util.collection.KList; +import io.papermc.lib.PaperLib; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * Cancels a teleport, loads the destination chunks off the main thread and then replays the teleport + * on the thread that owns the player. The replay sets the manager's ignore flag so the reissued + * teleport is not intercepted again. + */ +final class WorldTeleportWarmup { + private final IrisWorldManager manager; + + WorldTeleportWarmup(IrisWorldManager manager) { + this.manager = manager; + } + + void teleportAsync(PlayerTeleportEvent e) { + e.setCancelled(true); + warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), manager.managedTask( + "bukkit_world_manager_teleport", + () -> { + manager.ignoreTeleport().set(true); + e.getPlayer().teleport(e.getTo(), e.getCause()); + manager.ignoreTeleport().set(false); + }))); + } + + private void warmupAreaAsync(Player player, Location to, Runnable r) { + J.a(manager.managedTask("bukkit_world_manager_teleport_warmup", () -> { + int viewDistance = 2; + KList> futures = new KList<>(); + for (int i = -viewDistance; i <= viewDistance; i++) { + for (int j = -viewDistance; j <= viewDistance; j++) { + int finalJ = j; + int finalI = i; + + if (to.getWorld().isChunkLoaded((to.getBlockX() >> 4) + i, (to.getBlockZ() >> 4) + j)) { + futures.add(CompletableFuture.completedFuture(null)); + continue; + } + + futures.add(MultiBurst.burst.completeValue(() + -> PaperLib.getChunkAtAsync(to.getWorld(), + (to.getBlockX() >> 4) + finalI, + (to.getBlockZ() >> 4) + finalJ, + true, false).get())); + } + } + + new QueueJob>() { + @Override + public void execute(Future chunkFuture) { + try { + chunkFuture.get(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + IrisLogging.debug("Chunk warmup interrupted while loading async teleport chunk."); + } catch (ExecutionException ex) { + IrisLogging.reportError(ex); + } + } + + @Override + public String getName() { + return IrisLanguage.text(RuntimeUiMessages.JOB_LOADING_CHUNKS); + } + }.queue(futures).execute(new VolmitSender(player), true, r); + })); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java new file mode 100644 index 000000000..0bc50bcec --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java @@ -0,0 +1,115 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.actuator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedActuator; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.ChunkedDataCache; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterBiomeInject; +import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; + +public class IrisBiomeActuator extends EngineAssignedActuator { + private final RNG rng; + private final ChronoLatch cl = new ChronoLatch(5000); + private final KMap resolvedBiomes = new KMap<>(); + + public IrisBiomeActuator(Engine engine) { + super(engine, "Biome"); + rng = new RNG(engine.getSeedManager().getBiome()); + } + + @BlockCoordinates + @Override + public void onActuate(int x, int z, Hunk h, boolean multicore, ChunkContext context) { + // No catch here: a partial biome write must fail the chunk (EngineAssignedActuator + // rethrows), or the chunk is flagged REAL with default plains columns forever. + PrecisionStopwatch p = PrecisionStopwatch.start(); + int width = h.getWidth(); + int depth = h.getDepth(); + int height = h.getHeight(); + Engine engine = getEngine(); + Mantle mantle = engine.getMantle().getMantle(); + ChunkedDataCache biomeCache = context.getBiome(); + + for (int xf = 0; xf < width; xf++) { + IrisBiome ib; + for (int zf = 0; zf < depth; zf++) { + ib = biomeCache.get(xf, zf); + String key; + + if (ib.isCustom()) { + IrisBiomeCustom custom = ib.getCustomBiome(rng, engine, x + xf, 0, z + zf); + key = getDimension().getLoadKey() + ":" + custom.getId(); + } else { + key = ib.getSkyBiomeKey(rng, engine, x + xf, 0, z + zf); + } + + ResolvedBiome resolved = resolve(key); + PlatformBiome biome = resolved.biome(); + + if (biome != null) { + h.set(xf, 0, zf, xf, height - 1, zf, biome); + } + + mantle.set(x + xf, 0, z + zf, resolved.matter()); + } + } + engine.getMetrics().getBiome().put(p.getMilliseconds()); + } + + /** + * The registry lookup and the biome id only depend on the scatter resolved key, so cache the pair + * instead of paying two string keyed lookups per column. Unresolved biomes are never cached, so a + * biome registered later still resolves. + */ + private ResolvedBiome resolve(String key) { + ResolvedBiome cached = key == null ? null : resolvedBiomes.get(key); + + if (cached != null) { + return cached; + } + + IrisPlatform platform = IrisPlatforms.get(); + PlatformBiome biome = platform.registries().biome(key); + ResolvedBiome resolved = new ResolvedBiome(biome, BiomeInjectMatter.get(platform.biomeWriter().biomeIdFor(key))); + + if (key != null && biome != null) { + resolvedBiomes.put(key, resolved); + } + + return resolved; + } + + private record ResolvedBiome(PlatformBiome biome, MatterBiomeInject matter) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java new file mode 100644 index 000000000..15b3ed91b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java @@ -0,0 +1,143 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.actuator; + +import art.arcane.iris.engine.decorator.IrisCeilingDecorator; +import art.arcane.iris.engine.decorator.IrisSeaFloorDecorator; +import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator; +import art.arcane.iris.engine.decorator.IrisShoreLineDecorator; +import art.arcane.iris.engine.decorator.IrisSurfaceDecorator; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedActuator; +import art.arcane.iris.engine.framework.EngineDecorator; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.spi.PlatformBlockState; +import lombok.Getter; + +import java.util.function.Predicate; + +public class IrisDecorantActuator extends EngineAssignedActuator { + private static final Predicate PREDICATE_SOLID = (s) -> s != null && !B.isAirOrFluid(s); + private final RNG rng; + @Getter + private final IrisSurfaceDecorator surfaceDecorator; + @Getter + private final IrisCeilingDecorator ceilingDecorator; + @Getter + private final EngineDecorator seaSurfaceDecorator; + @Getter + private final EngineDecorator seaFloorDecorator; + @Getter + private final EngineDecorator shoreLineDecorator; + private final boolean shouldRay; + + public IrisDecorantActuator(Engine engine) { + super(engine, "Decorant"); + shouldRay = shouldRayDecorate(); + this.rng = new RNG(engine.getSeedManager().getDecorator()); + surfaceDecorator = new IrisSurfaceDecorator(getEngine()); + ceilingDecorator = new IrisCeilingDecorator(getEngine()); + seaSurfaceDecorator = new IrisSeaSurfaceDecorator(getEngine()); + shoreLineDecorator = new IrisShoreLineDecorator(getEngine()); + seaFloorDecorator = new IrisSeaFloorDecorator(getEngine()); + } + + @BlockCoordinates + @Override + public void onActuate(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + if (!getEngine().getDimension().isDecorate()) { + return; + } + + PrecisionStopwatch p = PrecisionStopwatch.start(); + + for (int i = 0; i < output.getWidth(); i++) { + int height; + int realX = Math.round(x + i); + int realZ; + IrisBiome biome, cave; + for (int j = 0; j < output.getDepth(); j++) { + boolean solid; + int emptyFor = 0; + int lastSolid = 0; + realZ = Math.round(z + j); + height = context.getRoundedHeight(i, j); + biome = context.getBiome().get(i, j); + cave = shouldRay ? context.getCave().get(i, j) : null; + + if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) { + continue; + } + + if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j)) + && height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) { + getSeaSurfaceDecorator().decorate(i, j, + realX, Math.round(i + 1), Math.round(x + i - 1), + realZ, Math.round(z + j + 1), Math.round(z + j - 1), + output, biome, getDimension().getFluidHeight(), getEngine().getHeight()); + getSeaFloorDecorator().decorate(i, j, + realX, realZ, output, biome, height + 1, + getDimension().getFluidHeight() + 1); + } + + if (height == getDimension().getFluidHeight()) { + getShoreLineDecorator().decorate(i, j, + realX, Math.round(x + i + 1), Math.round(x + i - 1), + realZ, Math.round(z + j + 1), Math.round(z + j - 1), + output, biome, height, getEngine().getHeight()); + } + + if (height >= 0 && height < output.getHeight()) { + getSurfaceDecorator().decorate(i, j, realX, realZ, output, biome, height, getEngine().getHeight() - height); + } + + + if (cave != null && cave.getDecorators().isNotEmpty()) { + for (int k = Math.min(height, output.getHeight() - 1); k > 0; k--) { + solid = PREDICATE_SOLID.test(output.get(i, k, j)); + + if (solid) { + if (emptyFor > 0) { + getSurfaceDecorator().decorate(i, j, realX, realZ, output, cave, k, lastSolid); + getCeilingDecorator().decorate(i, j, realX, realZ, output, cave, lastSolid - 1, emptyFor); + emptyFor = 0; + } + lastSolid = k; + } else { + emptyFor++; + } + } + } + } + } + + getEngine().getMetrics().getDecoration().put(p.getMilliseconds()); + + } + + private boolean shouldRayDecorate() { + return false; // TODO CAVES + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java new file mode 100644 index 000000000..fe066ad39 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java @@ -0,0 +1,255 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.actuator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedActuator; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.UpperDimensionContext; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisOreGenerator; +import art.arcane.iris.engine.object.IrisOreGeneratorBounds; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.project.context.ChunkedDataCache; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import lombok.Getter; + +public class IrisTerrainNormalActuator extends EngineAssignedActuator { + private static final PlatformBlockState AIR = B.getState("AIR"); + private static final PlatformBlockState BEDROCK = B.getState("BEDROCK"); + private static final PlatformBlockState LAVA = B.getState("LAVA"); + private static final PlatformBlockState GLASS = B.getState("GLASS"); + private static final PlatformBlockState CAVE_AIR = B.getState("CAVE_AIR"); + @Getter + private final RNG rng; + @Getter + private int lastBedrock = -1; + + public IrisTerrainNormalActuator(Engine engine) { + super(engine, "Terrain"); + rng = new RNG(engine.getSeedManager().getTerrain()); + } + + @BlockCoordinates + @Override + public void onActuate(int x, int z, Hunk h, boolean multicore, ChunkContext context) { + PrecisionStopwatch p = PrecisionStopwatch.start(); + + for (int xf = 0; xf < h.getWidth(); xf++) { + terrainSliver(x, z, xf, h, context); + } + + getEngine().getMetrics().getTerrain().put(p.getMilliseconds()); + } + + private int fluidOrHeight(int height) { + return Math.max(getDimension().getFluidHeight(), height); + } + + /** + * This is calling 1/16th of a chunk x/z slice. It is a plane from sky to bedrock 1 thick in the x direction. + * + * @param x the chunk x in blocks + * @param z the chunk z in blocks + * @param xf the current x slice + * @param h the blockdata + */ + @BlockCoordinates + public void terrainSliver(int x, int z, int xf, Hunk h, ChunkContext context) { + terrainSliverOptimized(x, z, xf, h, context); + } + + @BlockCoordinates + private void terrainSliverOptimized(int x, int z, int xf, Hunk h, ChunkContext context) { + int chunkHeight = h.getHeight(); + int chunkDepth = h.getDepth(); + IrisDimension dimension = getDimension(); + IrisData data = getData(); + IrisComplex complex = getComplex(); + RNG localRng = rng; + int fluidHeight = dimension.getFluidHeight(); + int clampedFluidHeight = Math.min(chunkHeight, fluidHeight); + boolean bedrockEnabled = dimension.isBedrock(); + boolean hideOres = dimension.isHideOresForHiddenOre(); + ChunkedDataCache biomeCache = context.getBiome(); + ChunkedDataCache regionCache = context.getRegion(); + ChunkedDataCache fluidCache = context.getFluid(); + ChunkedDataCache rockCache = context.getRock(); + int realX = xf + x; + UpperDimensionContext upperContext = getEngine().getUpperContext(); + // Dimension-level ore lookups are chunk-invariant; resolving them per column paid + // four accessor chains times 256 per chunk for constants. + KList dimensionSurfaceOres = hideOres ? null : dimension.getSurfaceOreGenerators(); + KList dimensionUndergroundOres = hideOres ? null : dimension.getUndergroundOreGenerators(); + IrisOreGeneratorBounds dimensionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getSurfaceOreGeneratorBounds(); + IrisOreGeneratorBounds dimensionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getUndergroundOreGeneratorBounds(); + + for (int zf = 0; zf < chunkDepth; zf++) { + int realZ = zf + z; + IrisBiome biome = biomeCache.get(xf, zf); + IrisRegion region = regionCache.get(xf, zf); + int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf)); + int hf = Math.max(clampedFluidHeight, he); + if (hf < 0) { + continue; + } + + int topY = Math.min(hf, chunkHeight - 1); + PlatformBlockState fluid = fluidCache.get(xf, zf); + PlatformBlockState rock = rockCache.get(xf, zf); + KList biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators(); + KList regionSurfaceOres = hideOres ? null : region.getSurfaceOreGenerators(); + KList biomeUndergroundOres = hideOres ? null : biome.getUndergroundOreGenerators(); + KList regionUndergroundOres = hideOres ? null : region.getUndergroundOreGenerators(); + IrisOreGeneratorBounds biomeSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : biome.getSurfaceOreGeneratorBounds(); + IrisOreGeneratorBounds regionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : region.getSurfaceOreGeneratorBounds(); + IrisOreGeneratorBounds biomeUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : biome.getUndergroundOreGeneratorBounds(); + IrisOreGeneratorBounds regionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : region.getUndergroundOreGeneratorBounds(); + boolean hasSurfaceOres = biomeSurfaceOreBounds.hasOres() || regionSurfaceOreBounds.hasOres() || dimensionSurfaceOreBounds.hasOres(); + boolean hasUndergroundOres = biomeUndergroundOreBounds.hasOres() || regionUndergroundOreBounds.hasOres() || dimensionUndergroundOreBounds.hasOres(); + KList blocks = null; + KList fblocks = null; + + for (int i = topY; i >= 0; i--) { + if (i == 0 && bedrockEnabled) { + h.setRaw(xf, i, zf, BEDROCK); + lastBedrock = i; + continue; + } + + PlatformBlockState ore = null; + if (hasSurfaceOres) { + if (biomeSurfaceOreBounds.contains(i)) { + ore = generateOres(biomeSurfaceOres, realX, i, realZ, localRng, data); + } + if (ore == null && regionSurfaceOreBounds.contains(i)) { + ore = generateOres(regionSurfaceOres, realX, i, realZ, localRng, data); + } + if (ore == null && dimensionSurfaceOreBounds.contains(i)) { + ore = generateOres(dimensionSurfaceOres, realX, i, realZ, localRng, data); + } + } + if (ore != null) { + h.setRaw(xf, i, zf, ore); + continue; + } + + if (i > he && i <= hf) { + int fdepth = hf - i; + if (fblocks == null) { + fblocks = biome.generateSeaLayers(realX, realZ, localRng, hf - he, data); + } + + if (fblocks.hasIndex(fdepth)) { + h.setRaw(xf, i, zf, fblocks.get(fdepth)); + } else { + h.setRaw(xf, i, zf, fluid); + } + continue; + } + + if (i <= he) { + int depth = he - i; + if (blocks == null) { + blocks = biome.generateLayers(dimension, realX, realZ, localRng, he, he, data, complex); + } + + if (blocks.hasIndex(depth)) { + h.setRaw(xf, i, zf, blocks.get(depth)); + continue; + } + + if (hasUndergroundOres) { + if (biomeUndergroundOreBounds.contains(i)) { + ore = generateOres(biomeUndergroundOres, realX, i, realZ, localRng, data); + } + if (ore == null && regionUndergroundOreBounds.contains(i)) { + ore = generateOres(regionUndergroundOres, realX, i, realZ, localRng, data); + } + if (ore == null && dimensionUndergroundOreBounds.contains(i)) { + ore = generateOres(dimensionUndergroundOres, realX, i, realZ, localRng, data); + } + } + + if (ore != null) { + h.setRaw(xf, i, zf, ore); + } else { + h.setRaw(xf, i, zf, rock); + } + } + } + + if (upperContext != null) { + int rawUpperSurface = upperContext.getUpperSurfaceY(realX, realZ); + int upperGap = dimension.getUpperDimensionGap(); + int upperSurfaceY = Math.max(rawUpperSurface, he + upperGap); + + if (upperSurfaceY < chunkHeight - 1) { + IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ); + PlatformBlockState upperRock = upperContext.getRockBlock(realX, realZ); + int upperThickness = chunkHeight - 1 - upperSurfaceY; + KList upperBlocks = upperBiome != null + ? upperBiome.generateLayers(upperContext.getDimension(), + realX, realZ, localRng, upperThickness, upperThickness, + upperContext.getData(), complex) + : null; + + for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) { + if (y == chunkHeight - 1 && bedrockEnabled) { + h.setRaw(xf, y, zf, BEDROCK); + continue; + } + int depthFromFace = y - upperSurfaceY; + if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) { + h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace)); + } else { + h.setRaw(xf, y, zf, upperRock); + } + } + } + } + } + } + + private PlatformBlockState generateOres(KList oreGenerators, int x, int y, int z, RNG rng, IrisData data) { + if (oreGenerators == null || oreGenerators.isEmpty()) { + return null; + } + + int oreCount = oreGenerators.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = oreGenerators.get(oreIndex); + PlatformBlockState ore = oreGenerator.generate(x, y, z, rng, data); + if (ore != null) { + return ore; + } + } + + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/data/cache/AtomicCache.java b/core/src/main/java/art/arcane/iris/engine/data/cache/AtomicCache.java new file mode 100644 index 000000000..a9e9ca322 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/data/cache/AtomicCache.java @@ -0,0 +1,197 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.data.cache; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.function.NastySupplier; + +import java.util.function.Supplier; + +public class AtomicCache { + private static final Object NULL_VALUE = new Object(); + private transient final Object initLock = new Object(); + private transient final boolean nullSupport; + private transient volatile Object value; + private transient volatile Throwable failure; + + public AtomicCache() { + this(false); + } + + public AtomicCache(boolean nullSupport) { + this.nullSupport = nullSupport; + } + + public void reset() { + synchronized (initLock) { + value = null; + failure = null; + } + } + + public T getIfPresent() { + return unwrap(value); + } + + public T aquireNasty(NastySupplier t) { + return aquire(() -> { + try { + return t.get(); + } catch (Throwable e) { + return null; + } + }); + } + + public T aquireNastyPrint(NastySupplier t) { + return aquire(() -> { + try { + return t.get(); + } catch (Throwable e) { + e.printStackTrace(); + return null; + } + }); + } + + /** + * Like {@link #aquire(Supplier)} but propagates a supplier failure to the caller instead + * of swallowing it into a null return. For values that are mandatory: a caller of a + * "@NotNull" accessor should see the supplier's real exception, not a downstream NPE. + *

+ * The supplier is retried on every call until it produces a value, which is what a caller that can + * repair the cause between attempts needs. Use {@link #aquireOnceOrThrow(Supplier)} when re-running a + * failed supplier is itself the problem. + */ + public T aquireOrThrow(Supplier t) { + Object v = value; + + if (v != null) { + return unwrap(v); + } + + synchronized (initLock) { + v = value; + + if (v != null) { + return unwrap(v); + } + + T computed = t.get(); + if (computed == null) { + throw new IllegalStateException("Atomic cache supplier produced null"); + } + value = computed; + return computed; + } + } + + /** + * Like {@link #aquireOrThrow(Supplier)} but memoizes the failure as well as the value, so a supplier + * that cannot produce one is run exactly once and every later caller is told the same reason. For + * values whose supplier rebuilds shared state, where re-running it per call repeats that work and + * hides the original cause behind whatever the retry happens to fail on. {@link #reset()} clears it. + */ + public T aquireOnceOrThrow(Supplier t) { + Object v = value; + + if (v != null) { + return unwrap(v); + } + rethrowFailure(failure); + + synchronized (initLock) { + v = value; + + if (v != null) { + return unwrap(v); + } + rethrowFailure(failure); + + T computed; + try { + computed = t.get(); + if (computed == null) { + throw new IllegalStateException("Atomic cache supplier produced null"); + } + } catch (Throwable e) { + failure = e; + throw e; + } + value = computed; + return computed; + } + } + + private static void rethrowFailure(Throwable memoized) { + if (memoized == null) { + return; + } + if (memoized instanceof RuntimeException runtimeFailure) { + throw runtimeFailure; + } + if (memoized instanceof Error error) { + throw error; + } + throw new IllegalStateException("Atomic cache supplier failed", memoized); + } + + public T aquire(Supplier t) { + Object v = value; + + if (v != null) { + return unwrap(v); + } + + synchronized (initLock) { + v = value; + + if (v != null) { + return unwrap(v); + } + + try { + T computed = t.get(); + + if (computed != null) { + value = computed; + return computed; + } + + if (nullSupport) { + value = NULL_VALUE; + } + } catch (Throwable e) { + // aquire retries on every call, so a supplier that stays broken is reached per sample. + // The first statement of it carries the stack; the rest are traces of the same cause. + if (IrisLogging.warnOnce("atomic-cache:" + e.getClass().getName() + ":" + e.getMessage(), + "Atomic cache supplier failed: %s: %s", e.getClass().getSimpleName(), e.getMessage())) { + IrisLogging.reportError(e); + } + } + + return null; + } + } + + @SuppressWarnings("unchecked") + private T unwrap(Object v) { + return v == null || v == NULL_VALUE ? null : (T) v; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/data/cache/Cache.java b/core/src/main/java/art/arcane/iris/engine/data/cache/Cache.java new file mode 100644 index 000000000..acc1879e7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/data/cache/Cache.java @@ -0,0 +1,47 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.data.cache; + +import art.arcane.volmlib.util.cache.CacheKey; + +public interface Cache { + static long key(int x, int z) { + return CacheKey.key(x, z); + } + + static int keyX(long key) { + return CacheKey.keyX(key); + } + + static int keyZ(long key) { + return CacheKey.keyZ(key); + } + + static int to1D(int x, int y, int z, int w, int h) { + return CacheKey.to1D(x, y, z, w, h); + } + + static int[] to3D(int idx, int w, int h) { + return CacheKey.to3D(idx, w, h); + } + + int getId(); + + V get(int x, int z); +} diff --git a/core/src/main/java/art/arcane/iris/engine/data/cache/LazyBoundedCache.java b/core/src/main/java/art/arcane/iris/engine/data/cache/LazyBoundedCache.java new file mode 100644 index 000000000..4e1a0bbe2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/data/cache/LazyBoundedCache.java @@ -0,0 +1,52 @@ +package art.arcane.iris.engine.data.cache; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Objects; +import java.util.function.Function; + +public final class LazyBoundedCache { + private final int maximumSize; + private transient LinkedHashMap entries; + + public LazyBoundedCache(int maximumSize) { + if (maximumSize <= 0) { + throw new IllegalArgumentException("Maximum cache size must be positive"); + } + this.maximumSize = maximumSize; + } + + public synchronized V computeIfAbsent(K key, Function mappingFunction) { + K requiredKey = Objects.requireNonNull(key, "Cache key"); + Function requiredMapping = Objects.requireNonNull(mappingFunction, "Cache mapping function"); + if (entries != null) { + V cached = entries.get(requiredKey); + if (cached != null) { + return cached; + } + } + + V computed = requiredMapping.apply(requiredKey); + if (computed == null) { + return null; + } + if (entries == null) { + entries = new LinkedHashMap<>(maximumSize, 1F, true); + } + entries.put(requiredKey, computed); + if (entries.size() > maximumSize) { + Iterator iterator = entries.keySet().iterator(); + iterator.next(); + iterator.remove(); + } + return computed; + } + + synchronized boolean isInitialized() { + return entries != null; + } + + synchronized int size() { + return entries == null ? 0 : entries.size(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java b/core/src/main/java/art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java new file mode 100644 index 000000000..50ee56d32 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java @@ -0,0 +1,123 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.data.chunk; + +import art.arcane.iris.platform.bukkit.BukkitBiome; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.IrisCustomData; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.generator.ChunkGenerator.ChunkData; + +public class LinkedTerrainChunk implements TerrainChunk { + private static final int CHUNK_SIZE = 16; + private final ChunkData rawChunkData; + private final int minHeight; + private final int maxHeight; + private final int biomeHeight; + private final Biome[] biomes; + + public LinkedTerrainChunk(World world) { + this(Bukkit.createChunkData(world)); + } + + public LinkedTerrainChunk(ChunkData data) { + rawChunkData = data; + minHeight = data.getMinHeight(); + maxHeight = data.getMaxHeight(); + biomeHeight = Math.max(1, maxHeight - minHeight); + biomes = new Biome[CHUNK_SIZE * biomeHeight * CHUNK_SIZE]; + } + + @Override + public PlatformBiome getBiome(int x, int y, int z) { + int index = biomeIndex(x, y, z); + Biome biome = biomes[index]; + return BukkitBiome.of(biome == null ? Biome.PLAINS : biome); + } + + @Override + public void setBiome(int x, int y, int z, PlatformBiome bio) { + biomes[biomeIndex(x, y, z)] = (Biome) bio.nativeHandle(); + } + + /** + * Writes the whole vertical column in one pass, striding the flat biome array directly. Equivalent to + * calling setBiome for every y in [minHeight, maxHeight). + */ + public void fillBiomeColumn(int x, int z, PlatformBiome bio) { + Biome handle = (Biome) bio.nativeHandle(); + int stride = CHUNK_SIZE * CHUNK_SIZE; + int index = (z & (CHUNK_SIZE - 1)) * CHUNK_SIZE + (x & (CHUNK_SIZE - 1)); + + for (int y = 0; y < biomeHeight; y++) { + biomes[index] = handle; + index += stride; + } + } + + @Override + public int getMinHeight() { + return minHeight; + } + + @Override + public int getMaxHeight() { + return maxHeight; + } + + @Override + public synchronized void setBlock(int x, int y, int z, PlatformBlockState state) { + BlockData blockData = (BlockData) state.nativeHandle(); + if (blockData instanceof IrisCustomData data) { + blockData = data.getBase(); + } + rawChunkData.setBlock(x, y, z, blockData); + } + + @Override + public synchronized void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, PlatformBlockState state) { + BlockData blockData = (BlockData) state.nativeHandle(); + if (blockData instanceof IrisCustomData data) { + blockData = data.getBase(); + } + rawChunkData.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, blockData); + } + + @Override + public PlatformBlockState getBlockData(int x, int y, int z) { + return BukkitBlockState.of(rawChunkData.getBlockData(x, y, z)); + } + + @Override + public ChunkData getChunkData() { + return rawChunkData; + } + + private int biomeIndex(int x, int y, int z) { + int clampedX = x & (CHUNK_SIZE - 1); + int clampedZ = z & (CHUNK_SIZE - 1); + int clampedY = Math.max(minHeight, Math.min(maxHeight - 1, y)) - minHeight; + return (clampedY * CHUNK_SIZE + clampedZ) * CHUNK_SIZE + clampedX; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/data/chunk/TerrainChunk.java b/core/src/main/java/art/arcane/iris/engine/data/chunk/TerrainChunk.java new file mode 100644 index 000000000..c4992d6fd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/data/chunk/TerrainChunk.java @@ -0,0 +1,44 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.data.chunk; + +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import org.bukkit.World; +import org.bukkit.generator.ChunkGenerator.ChunkData; + +public interface TerrainChunk { + static TerrainChunk create(World world) { + return new LinkedTerrainChunk(world); + } + + static TerrainChunk create(ChunkData raw) { + return new LinkedTerrainChunk(raw); + } + + PlatformBiome getBiome(int x, int y, int z); + + void setBiome(int x, int y, int z, PlatformBiome bio); + int getMinHeight(); + int getMaxHeight(); + void setBlock(int x, int y, int z, PlatformBlockState blockData); + void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, PlatformBlockState blockData); + PlatformBlockState getBlockData(int x, int y, int z); + ChunkData getChunkData(); +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java new file mode 100644 index 000000000..b752e8910 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java @@ -0,0 +1,546 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockSupport; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.MultipleFacing; + +final class DecoratorCore { + + private static final long SEED_OFFSET = 29356788L; + private static final long PART_FACTOR = 10439677L; + private static final String WEEPING_VINES = "minecraft:weeping_vines"; + private static final String WEEPING_VINES_PLANT = "minecraft:weeping_vines_plant"; + private static final String TWISTING_VINES = "minecraft:twisting_vines"; + private static final String TWISTING_VINES_PLANT = "minecraft:twisting_vines_plant"; + private static final boolean BUKKIT_PRESENT = detectBukkit(); + private static volatile PlatformBlockState weepingVines; + private static volatile PlatformBlockState weepingVinesPlant; + private static volatile PlatformBlockState twistingVines; + private static volatile PlatformBlockState twistingVinesPlant; + + private static boolean detectBukkit() { + try { + Class.forName("org.bukkit.Bukkit", false, DecoratorCore.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + static final ThreadLocal SCRATCH_OPTS = ThreadLocal.withInitial(PlaceOpts::new); + + static final class PlaceOpts { + boolean caveSkipFluid; + boolean underwater; + int fluidHeight; + + void reset() { + caveSkipFluid = false; + underwater = false; + fluidHeight = 0; + } + } + + static long partSeed(long baseSeed, int partOrdinal) { + return baseSeed + SEED_OFFSET - (partOrdinal * PART_FACTOR); + } + + static long partSeed(long baseSeed, IrisDecorationPart part) { + return partSeed(baseSeed, part.ordinal()); + } + + static IrisDecorator pickDecorator(IrisBiome biome, IrisDecorationPart part, RNG gRNG, + RNG colRng, IrisData data, double realX, double realZ) { + IrisDecorator[] bucket = biome.getDecoratorBucket(part); + if (bucket.length == 0) { + return null; + } + + IrisDecorator picked = null; + int count = 0; + + for (IrisDecorator d : bucket) { + try { + if (d.passesChanceGate(gRNG, realX, realZ, data)) { + count++; + if (count == 1 || colRng.nextInt(count) == 0) { + picked = d; + } + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + return picked; + } + + private static String topHalfValue(String half) { + return half.equals("upper") || half.equals("lower") ? "upper" : "top"; + } + + private static String bottomHalfValue(String half) { + return half.equals("upper") || half.equals("lower") ? "lower" : "bottom"; + } + + static void placeSurfaceSingle(IrisDecorator decorator, + int x, int z, int realX, int height, int realZ, + Hunk data, RNG rng, IrisData irisData, + boolean underwater, boolean caveSkipFluid, EngineMantle mantle) { + if (height < 0 || height >= data.getHeight()) { + return; + } + + PlatformBlockState bdx = data.get(x, height, z); + PlatformBlockState bd = decorator.pickBlockData(rng, irisData, realX, realZ); + + if (!underwater && !canGoOn(bd, bdx) + && !decorator.isForcePlace() && decorator.getForceBlock() == null) { + return; + } + + if (decorator.getForceBlock() != null) { + if (caveSkipFluid && B.isFluid(bdx)) { + return; + } + data.set(x, height, z, fixFacesForHunk( + decorator.getForceBlock().getBlockData(irisData), data, x, z, realX, height, realZ, mantle)); + } + + if (!decorator.isForcePlace()) { + if (decorator.getWhitelist() != null && !matchesPalette(decorator.getWhitelistArray(irisData), bdx)) { + return; + } + if (decorator.getBlacklist() != null && matchesPalette(decorator.getBlacklistArray(irisData), bdx)) { + return; + } + } + + String half = bd == null ? null : IrisProceduralBlocks.propertyValue(bd, "half"); + if (half != null) { + int lowerY = height + 1; + int upperY = height + 2; + if (!canPlaceTwoBlockPlant(data, x, z, lowerY, upperY, caveSkipFluid)) { + return; + } + + try { + PlatformBlockState upper = bd.withProperty("half", topHalfValue(half)); + PlatformBlockState lower = fixFacesForHunk( + bd.withProperty("half", bottomHalfValue(half)), + data, x, z, realX, lowerY, realZ, mantle); + data.set(x, lowerY, z, lower); + data.set(x, upperY, z, upper); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + return; + } + + int targetY = height + 1; + if (targetY < data.getHeight() && B.isAir(data.get(x, targetY, z))) { + data.set(x, targetY, z, fixFacesForHunk(bd, data, x, z, realX, targetY, realZ, mantle)); + } + } + + private static boolean matchesPalette(PlatformBlockState[] palette, PlatformBlockState surface) { + for (int i = 0; i < palette.length; i++) { + if (palette[i].equals(surface)) { + return true; + } + } + + return false; + } + + static void placeSingleAt(IrisDecorator decorator, int x, int z, + int realX, int height, int realZ, Hunk data, + RNG rng, IrisData irisData, boolean applyFixFaces, EngineMantle mantle) { + PlatformBlockState bd = decorator.pickBlockData(rng, irisData, realX, realZ); + if (bd == null) { + return; + } + if (applyFixFaces) { + bd = fixFacesForHunk(bd, data, x, z, realX, height, realZ, mantle); + } + data.set(x, height, z, bd); + } + + static void placeStackUp(IrisDecorator decorator, int x, int z, int realX, int realZ, + int height, int max, Hunk data, + RNG rng, IrisData irisData, PlaceOpts opts) { + if (height < 0 || height >= data.getHeight()) { + return; + } + + int effectiveMax = max; + if (opts.underwater && height < opts.fluidHeight) { + effectiveMax = opts.fluidHeight; + } + + int stack = computeStack(decorator, rng, realX, realZ, irisData, effectiveMax); + + if (stack == 1) { + int targetY = height + 1; + if (targetY >= data.getHeight()) { + return; + } + + PlatformBlockState existing = data.get(x, targetY, z); + if (!canReplaceStackTarget(existing, opts.underwater) + || (opts.caveSkipFluid && B.isFluid(existing))) { + return; + } + + PlatformBlockState block = decorator.pickBlockDataTop(rng, irisData, realX, realZ); + if (block == null || (!opts.underwater && !canGoOn(block, data.get(x, height, z)))) { + return; + } + + block = stackedVineBlock(block, stack, 0); + data.set(x, targetY, z, block); + return; + } + + PlatformBlockState bdx = data.get(x, height, z); + + for (int i = 0; i < stack; i++) { + int h = height + i; + double threshold = ((double) i) / (stack - 1); + PlatformBlockState bd = threshold >= decorator.getTopThreshold() + ? decorator.pickBlockDataTop(rng, irisData, realX, realZ) + : decorator.pickBlockData(rng, irisData, realX, realZ); + + if (bd == null) { + break; + } + + if (i == 0 && !opts.underwater && !canGoOn(bd, bdx)) { + break; + } + + if (opts.underwater && height + 1 + i > opts.fluidHeight) { + break; + } + + if (height + 1 + i >= data.getHeight()) { + break; + } + + PlatformBlockState existing = data.get(x, height + 1 + i, z); + if (!canReplaceStackTarget(existing, opts.underwater) + || (opts.caveSkipFluid && B.isFluid(existing))) { + break; + } + + if (IrisProceduralBlocks.materialKey(bd).equals("minecraft:pointed_dripstone")) { + bd = dripstoneBlock(stack, i, "up"); + } + + bd = stackedVineBlock(bd, stack, i); + data.set(x, height + 1 + i, z, bd); + } + } + + static void placeStackDown(IrisDecorator decorator, int x, int z, int realX, int realZ, + int height, int minHeight, Hunk data, + RNG rng, IrisData irisData, int max, PlaceOpts opts, EngineMantle mantle) { + if (height < 0 || height >= data.getHeight()) { + return; + } + + int stack = computeStack(decorator, rng, realX, realZ, irisData, max); + + if (stack == 1) { + if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) { + return; + } + PlatformBlockState block = decorator.pickBlockDataTop(rng, irisData, realX, realZ); + if (block == null) { + return; + } + block = stackedVineBlock(block, stack, 0); + data.set(x, height, z, fixFacesForHunk(block, data, x, z, realX, height, realZ, mantle)); + return; + } + + for (int i = 0; i < stack; i++) { + int h = height - i; + if (h < 0 || h < minHeight) { + break; + } + + double threshold = ((double) i) / (double) (stack - 1); + PlatformBlockState bd = threshold >= decorator.getTopThreshold() + ? decorator.pickBlockDataTop(rng, irisData, realX, realZ) + : decorator.pickBlockData(rng, irisData, realX, realZ); + + if (bd == null) { + break; + } + + if (IrisProceduralBlocks.materialKey(bd).equals("minecraft:pointed_dripstone")) { + bd = dripstoneBlock(stack, i, "down"); + } + + bd = stackedVineBlock(bd, stack, i); + if (opts.caveSkipFluid && B.isFluid(data.get(x, h, z))) { + break; + } + data.set(x, h, z, fixFacesForHunk(bd, data, x, z, realX, h, realZ, mantle)); + } + } + + static void placeFloatingSimple(IrisDecorator decorator, + int xf, int zf, int realX, int realZ, + int height, int max, Hunk data, + RNG rng, IrisData irisData) { + PlatformBlockState bd = decorator.pickBlockData(rng, irisData, realX, realZ); + if (bd == null) { + return; + } + + String half = IrisProceduralBlocks.propertyValue(bd, "half"); + if (half != null) { + int lowerY = height + 1; + int upperY = height + 2; + if (max <= 2 || !canPlaceTwoBlockPlant(data, xf, zf, lowerY, upperY, false)) { + return; + } + + try { + PlatformBlockState upper = bd.withProperty("half", topHalfValue(half)); + PlatformBlockState lower = bd.withProperty("half", bottomHalfValue(half)); + data.set(xf, lowerY, zf, lower); + data.set(xf, upperY, zf, upper); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + return; + } + + if (max > 1 && height + 1 < data.getHeight()) { + data.set(xf, height + 1, zf, bd); + } + } + + static int placeFloatingStacked(IrisDecorator decorator, + int xf, int zf, int realX, int realZ, + int height, int max, Hunk data, + RNG rng, IrisData irisData) { + int stack = decorator.getHeight(rng, realX, realZ, irisData); + if (decorator.isScaleStack()) { + stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack()); + } else { + stack = Math.min(max, stack); + } + + int placed = 0; + for (int i = 0; i < stack; i++) { + int h = height + 1 + i; + if (h >= height + max || h >= data.getHeight()) { + break; + } + double threshold = stack == 1 ? 0.0 : ((double) i) / (stack - 1); + PlatformBlockState bd = threshold >= decorator.getTopThreshold() + ? decorator.pickBlockDataTop(rng, irisData, realX, realZ) + : decorator.pickBlockData(rng, irisData, realX, realZ); + if (bd == null) { + break; + } + bd = stackedVineBlock(bd, stack, i); + data.set(xf, h, zf, bd); + placed++; + } + return placed; + } + + static PlatformBlockState fixFacesForHunk(PlatformBlockState b, Hunk hunk, int rX, int rZ, + int x, int y, int z, EngineMantle mantle) { + if (!B.isVineBlock(b)) { + return b; + } + if (!BUKKIT_PRESENT) { + DecoratorPlatformHooks.FaceFixer fixer = DecoratorPlatformHooks.faceFixer(); + return fixer == null ? b : fixer.fixFaces(b, hunk, rX, rZ, x, y, z, mantle); + } + BlockData rawB = (BlockData) b.nativeHandle(); + BlockData cloned = rawB.clone(); + MultipleFacing data = (MultipleFacing) cloned; + data.getFaces().forEach(f -> data.setFace(f, false)); + + boolean found = false; + for (BlockFace f : BlockFace.values()) { + if (!f.isCartesian()) { + continue; + } + int yy = y + f.getModY(); + + PlatformBlockState rs = null; + if (mantle != null) { + rs = mantle.getMantle().get(x + f.getModX(), yy, z + f.getModZ(), PlatformBlockState.class); + } + BlockData r = rs == null ? (BlockData) EngineMantle.AIR.nativeHandle() : (BlockData) rs.nativeHandle(); + if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { + if (data.getAllowedFaces().contains(f)) { + found = true; + data.setFace(f, true); + } + continue; + } + + int xx = rX + f.getModX(); + int zz = rZ + f.getModZ(); + if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy >= hunk.getHeight()) { + continue; + } + + r = (BlockData) hunk.get(xx, yy, zz).nativeHandle(); + if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { + if (data.getAllowedFaces().contains(f)) { + found = true; + data.setFace(f, true); + } + } + } + if (!found) { + BlockFace fallback = data.getAllowedFaces().contains(BlockFace.DOWN) ? BlockFace.DOWN : BlockFace.UP; + if (data.getAllowedFaces().contains(fallback)) { + data.setFace(fallback, true); + } + } + return BukkitBlockState.of(cloned); + } + + static boolean canGoOn(PlatformBlockState decorator, PlatformBlockState surface) { + if (!B.canPlaceOnto(decorator, surface)) { + return false; + } + if (!BUKKIT_PRESENT) { + DecoratorPlatformHooks.SurfaceSturdiness sturdiness = DecoratorPlatformHooks.surfaceSturdiness(); + return sturdiness == null ? B.isSolid(surface) : sturdiness.canGoOn(surface); + } + return ((BlockData) surface.nativeHandle()).isFaceSturdy(BlockFace.UP, BlockSupport.FULL); + } + + static boolean isValidShorelineSupport(IrisDecorator decorator, PlatformBlockState decorant, PlatformBlockState surface) { + return surface != null + && B.isSolid(surface) + && (decorator.isForcePlace() || canGoOn(decorant, surface)); + } + + static boolean canReplaceStackTarget(PlatformBlockState state, boolean allowFluid) { + return B.isAir(state) || allowFluid && B.isFluid(state); + } + + private static boolean canPlaceTwoBlockPlant(Hunk data, int x, int z, + int lowerY, int upperY, boolean caveSkipFluid) { + if (lowerY < 0 || upperY >= data.getHeight()) { + return false; + } + + PlatformBlockState lower = data.get(x, lowerY, z); + PlatformBlockState upper = data.get(x, upperY, z); + return B.isAir(lower) && B.isAir(upper) + && (!caveSkipFluid || !B.isFluid(lower) && !B.isFluid(upper)); + } + + private static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ, + IrisData irisData, int max) { + int stack = decorator.getHeight(rng, realX, realZ, irisData); + if (decorator.isScaleStack()) { + stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack()); + } else { + stack = Math.min(max, stack); + } + return stack; + } + + private static volatile PlatformBlockState[] dripstoneUp; + private static volatile PlatformBlockState[] dripstoneDown; + + private static PlatformBlockState[] buildDripstoneArr(String direction) { + String[] order = {"tip", "frustum", "base"}; + PlatformBlockState[] arr = new PlatformBlockState[3]; + for (int k = 0; k < 3; k++) { + arr[k] = B.getState("minecraft:pointed_dripstone[thickness=" + order[k] + ",vertical_direction=" + direction + "]"); + } + return arr; + } + + private static PlatformBlockState dripstoneBlock(int stack, int i, String direction) { + int thIdx; + if (i == stack - 1) { + thIdx = 0; + } else if (i == stack - 2) { + thIdx = 1; + } else { + thIdx = 2; + } + if (direction.equals("up")) { + if (dripstoneUp == null) { + dripstoneUp = buildDripstoneArr("up"); + } + return dripstoneUp[thIdx]; + } + if (dripstoneDown == null) { + dripstoneDown = buildDripstoneArr("down"); + } + return dripstoneDown[thIdx]; + } + + static String stackedVineKey(PlatformBlockState state, int stack, int index) { + String material = IrisProceduralBlocks.materialKey(state); + boolean tip = index == stack - 1; + return switch (material) { + case WEEPING_VINES, WEEPING_VINES_PLANT -> tip ? WEEPING_VINES : WEEPING_VINES_PLANT; + case TWISTING_VINES, TWISTING_VINES_PLANT -> tip ? TWISTING_VINES : TWISTING_VINES_PLANT; + default -> null; + }; + } + + private static PlatformBlockState stackedVineBlock(PlatformBlockState state, int stack, int index) { + String key = stackedVineKey(state, stack, index); + if (key == null) { + return state; + } + return switch (key) { + case WEEPING_VINES -> weepingVines == null ? weepingVines = B.getState(key) : weepingVines; + case WEEPING_VINES_PLANT -> weepingVinesPlant == null ? weepingVinesPlant = B.getState(key) : weepingVinesPlant; + case TWISTING_VINES -> twistingVines == null ? twistingVines = B.getState(key) : twistingVines; + case TWISTING_VINES_PLANT -> twistingVinesPlant == null ? twistingVinesPlant = B.getState(key) : twistingVinesPlant; + default -> state; + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorPlatformHooks.java b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorPlatformHooks.java new file mode 100644 index 000000000..b14f63ab0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorPlatformHooks.java @@ -0,0 +1,62 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; + +public final class DecoratorPlatformHooks { + private static volatile FaceFixer FACE_FIXER = null; + private static volatile SurfaceSturdiness SURFACE_STURDINESS = null; + + private DecoratorPlatformHooks() { + } + + public interface FaceFixer { + PlatformBlockState fixFaces(PlatformBlockState state, Hunk hunk, int rX, int rZ, int x, int y, int z, EngineMantle mantle); + } + + public interface SurfaceSturdiness { + boolean canGoOn(PlatformBlockState surface); + } + + public static synchronized Bindings bind(FaceFixer faceFixer, SurfaceSturdiness surfaceSturdiness) { + Bindings previous = new Bindings(FACE_FIXER, SURFACE_STURDINESS); + FACE_FIXER = faceFixer; + SURFACE_STURDINESS = surfaceSturdiness; + return previous; + } + + public static synchronized void restore(Bindings bindings) { + FACE_FIXER = bindings.faceFixer(); + SURFACE_STURDINESS = bindings.surfaceSturdiness(); + } + + static FaceFixer faceFixer() { + return FACE_FIXER; + } + + static SurfaceSturdiness surfaceSturdiness() { + return SURFACE_STURDINESS; + } + + public record Bindings(FaceFixer faceFixer, SurfaceSturdiness surfaceSturdiness) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java new file mode 100644 index 000000000..760fed42b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/FloatingDecorator.java @@ -0,0 +1,55 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.spi.PlatformBlockState; + +/* + * Floating island decoration path. Bypasses all canGoOn, slope, whitelist, and blacklist + * gating from IrisSurfaceDecorator — the island top IS the biome's designated surface by + * construction, so those material-compatibility checks are never meaningful here. + */ +public class FloatingDecorator { + + public static int decorateColumn(Engine engine, IrisBiome target, IrisDecorationPart part, + int xf, int zf, int realX, int realZ, + int height, int max, Hunk data, RNG rng, + Runnable candidatesNullCallback) { + RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getDecorator(), part)); + IrisDecorator decorator = DecoratorCore.pickDecorator(target, part, gRNG, rng, engine.getData(), realX, realZ); + + if (decorator == null) { + candidatesNullCallback.run(); + return 0; + } + + if (!decorator.isStacking()) { + DecoratorCore.placeFloatingSimple(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData()); + return max > 1 ? 1 : 0; + } + + return DecoratorCore.placeFloatingStacked(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java new file mode 100644 index 000000000..305eb0be7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java @@ -0,0 +1,74 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.InferredType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.spi.PlatformBlockState; + +public class IrisCeilingDecorator extends IrisEngineDecorator { + private final RNG partRNG; + + public IrisCeilingDecorator(Engine engine) { + super(engine, "Ceiling", IrisDecorationPart.CEILING); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.CEILING)); + } + + @BlockCoordinates + @Override + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { + decorate(x, z, realX, realX1, realX_1, realZ, realZ1, realZ_1, data, biome, biome.getInferredType(), height, max); + } + + @BlockCoordinates + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, InferredType inferredType, int height, int max) { + boolean caveSkipFluid = IrisSurfaceDecorator.skipsFluid(inferredType); + RNG rng = getRNG(realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); + + if (decorator == null) { + return; + } + + if (!decorator.isStacking()) { + if (caveSkipFluid) { + PlatformBlockState state = data.get(x, height, z); + if (B.isFluid(state)) { + return; + } + } + DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, rng, getData(), true, getEngine().getMantle()); + return; + } + + DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get(); + opts.reset(); + opts.caveSkipFluid = caveSkipFluid; + DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, 0, data, rng, getData(), max, opts, getEngine().getMantle()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisEngineDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisEngineDecorator.java new file mode 100644 index 000000000..89a2c36b6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisEngineDecorator.java @@ -0,0 +1,45 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedComponent; +import art.arcane.iris.engine.framework.EngineDecorator; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.volmlib.util.math.RNG; +import lombok.Getter; + +public abstract class IrisEngineDecorator extends EngineAssignedComponent implements EngineDecorator { + @Getter + private final IrisDecorationPart part; + + public IrisEngineDecorator(Engine engine, String name, IrisDecorationPart part) { + super(engine, name + " Decorator"); + this.part = part; + } + + @BlockCoordinates + protected RNG getRNG(int x, int z) { + long seed = DecoratorCore.partSeed(getSeed(), part); + long modX = 29356788L ^ (part.ordinal() + 6); + long modZ = 10439677L ^ (part.ordinal() + 1); + return new RNG(x * modX + z * modZ + seed); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java new file mode 100644 index 000000000..9de54ad7b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaFloorDecorator.java @@ -0,0 +1,84 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.spi.PlatformBlockState; + +public class IrisSeaFloorDecorator extends IrisEngineDecorator { + private final RNG partRNG; + + public IrisSeaFloorDecorator(Engine engine) { + super(engine, "Sea Floor", IrisDecorationPart.SEA_FLOOR); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SEA_FLOOR)); + } + + @BlockCoordinates + @Override + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { + RNG rng = getRNG(realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); + + if (decorator == null) { + return; + } + + if (!decorator.isStacking()) { + if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() + && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { + return; + } + if (height >= 0 && height < getEngine().getHeight()) { + data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); + } + return; + } + + int stack = decorator.getHeight(rng, realX, realZ, getData()); + if (decorator.isScaleStack()) { + stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100)); + } else { + stack = Math.min(stack, max - height); + } + + if (stack == 1) { + data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); + return; + } + + int engineHeight = getEngine().getHeight(); + for (int i = 0; i < stack; i++) { + int h = height + i; + if (h > max || h > engineHeight) { + continue; + } + double threshold = ((double) i) / (stack - 1); + data.set(x, h, z, threshold >= decorator.getTopThreshold() + ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) + : decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java new file mode 100644 index 000000000..e310f29ea --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSeaSurfaceDecorator.java @@ -0,0 +1,97 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.spi.PlatformBlockState; + +public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { + private final RNG partRNG; + + public IrisSeaSurfaceDecorator(Engine engine) { + super(engine, "Sea Surface", IrisDecorationPart.SEA_SURFACE); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SEA_SURFACE)); + } + + @BlockCoordinates + @Override + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { + RNG rng = getRNG(realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); + + if (decorator == null) { + return; + } + + if (!decorator.isStacking()) { + int targetY = height + 1; + if (height >= 0 && targetY < getEngine().getHeight() + && DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); + if (block != null) { + data.set(x, targetY, z, block); + } + } + return; + } + + int stack = decorator.getHeight(rng, realX, realZ, getData()); + if (decorator.isScaleStack()) { + stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100)); + } + + if (stack == 1) { + int targetY = height + 1; + if (targetY >= data.getHeight() || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + return; + } + + PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()); + if (block != null) { + data.set(x, targetY, z, block); + } + return; + } + + int engineHeight = getEngine().getHeight(); + for (int i = 0; i < stack; i++) { + int h = height + i; + int targetY = h + 1; + if (h >= max || targetY >= engineHeight + || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + break; + } + double threshold = ((double) i) / (stack - 1); + PlatformBlockState block = threshold >= decorator.getTopThreshold() + ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) + : decorator.getBlockData100(biome, rng, realX, h, realZ, getData()); + if (block == null) { + break; + } + data.set(x, targetY, z, block); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java new file mode 100644 index 000000000..d49f384e1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java @@ -0,0 +1,126 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.spi.PlatformBlockState; + +public class IrisShoreLineDecorator extends IrisEngineDecorator { + private final RNG partRNG; + + public IrisShoreLineDecorator(Engine engine) { + super(engine, "Shore Line", IrisDecorationPart.SHORE_LINE); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SHORE_LINE)); + } + + @BlockCoordinates + @Override + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { + if (height != getDimension().getFluidHeight()) { + return; + } + + double complexFluidHeight = getComplex().getFluidHeight(); + ProceduralStream heightStream = getComplex().getHeightStream(); + if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight + && Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight + && Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight + && Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) { + return; + } + + RNG rng = getRNG(realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); + + if (decorator == null) { + return; + } + + if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() + && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { + return; + } + + PlatformBlockState support = data.get(x, height, z); + if (support == null || !support.isSolid()) { + return; + } + + if (!decorator.isStacking()) { + int targetY = height + 1; + if (targetY >= data.getHeight() + || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + return; + } + PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); + if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) { + data.set(x, targetY, z, block); + } + return; + } + + int stack = decorator.getHeight(rng, realX, realZ, getData()); + if (decorator.isScaleStack()) { + stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100)); + } else { + stack = Math.min(max - height, stack); + } + + if (stack == 1) { + int targetY = height + 1; + if (targetY >= data.getHeight() || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + return; + } + + PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()); + if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) { + data.set(x, targetY, z, block); + } + return; + } + + for (int i = 0; i < stack; i++) { + int h = height + i; + int targetY = h + 1; + if (targetY >= data.getHeight() + || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) { + break; + } + double threshold = ((double) i) / (stack - 1); + PlatformBlockState block = threshold >= decorator.getTopThreshold() + ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) + : decorator.getBlockData100(biome, rng, realX, h, realZ, getData()); + if (block == null) { + break; + } + if (i == 0 && !DecoratorCore.isValidShorelineSupport(decorator, block, support)) { + break; + } + data.set(x, targetY, z, block); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java new file mode 100644 index 000000000..225567af6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java @@ -0,0 +1,96 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.decorator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.InferredType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.spi.PlatformBlockState; + +public class IrisSurfaceDecorator extends IrisEngineDecorator { + private final RNG partRNG; + + public IrisSurfaceDecorator(Engine engine) { + super(engine, "Surface", IrisDecorationPart.NONE); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.NONE)); + } + + protected IrisSurfaceDecorator(Engine engine, String name) { + super(engine, name, IrisDecorationPart.NONE); + this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.NONE)); + } + + protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) { + if (decorator.isForcePlace() || decorator.getSlopeCondition().isDefault()) { + return true; + } + return decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ)); + } + + @BlockCoordinates + @Override + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, int height, int max) { + decorate(x, z, realX, realX1, realX_1, realZ, realZ1, realZ_1, data, biome, biome.getInferredType(), height, max); + } + + @BlockCoordinates + public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, + Hunk data, IrisBiome biome, InferredType inferredType, int height, int max) { + int fluidHeight = getDimension().getFluidHeight(); + if (inferredType == InferredType.SHORE && height < fluidHeight) { + return; + } + + boolean underwater = isUnderwater(inferredType, height, fluidHeight); + boolean caveSkipFluid = skipsFluid(inferredType); + RNG rng = getRNG(realX, realZ); + IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ); + + if (decorator == null || !isSlopeValid(decorator, realX, realZ)) { + return; + } + + if (decorator.isStacking()) { + DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get(); + opts.reset(); + opts.underwater = underwater; + opts.fluidHeight = fluidHeight; + opts.caveSkipFluid = caveSkipFluid; + DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts); + return; + } + + DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ, + data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle()); + } + + static boolean isUnderwater(InferredType inferredType, int height, int fluidHeight) { + return height < fluidHeight && inferredType != InferredType.CAVE; + } + + static boolean skipsFluid(InferredType inferredType) { + return inferredType == InferredType.CAVE; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/BlockEditAccess.java b/core/src/main/java/art/arcane/iris/engine/framework/BlockEditAccess.java new file mode 100644 index 000000000..65873fb71 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/BlockEditAccess.java @@ -0,0 +1,29 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +public interface BlockEditAccess { + B get(W world, int x, int y, int z); + + void set(W world, int x, int y, int z, B block); + + M getBiome(W world, int x, int y, int z); + + void setBiome(W world, int x, int y, int z, M biome); +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/BlockUpdater.java b/core/src/main/java/art/arcane/iris/engine/framework/BlockUpdater.java new file mode 100644 index 000000000..8cd73acbd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/BlockUpdater.java @@ -0,0 +1,26 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.spi.PlatformBlockState; + +public interface BlockUpdater { + + void catchBlockUpdates(int x, int y, int z, PlatformBlockState data); +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/BukkitEngineWorldManager.java b/core/src/main/java/art/arcane/iris/engine/framework/BukkitEngineWorldManager.java new file mode 100644 index 000000000..8f60c26fd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/BukkitEngineWorldManager.java @@ -0,0 +1,36 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import org.bukkit.Chunk; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerTeleportEvent; + +public interface BukkitEngineWorldManager { + void onBlockBreak(BlockBreakEvent event); + + void onBlockPlace(BlockPlaceEvent event); + + void onChunkLoad(Chunk chunk, boolean generated); + + void onChunkUnload(Chunk chunk); + + void teleportAsync(PlayerTeleportEvent event); +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java new file mode 100644 index 000000000..6f2adc84f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -0,0 +1,558 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.engine.framework.render.Renderer; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.nms.container.BlockPos; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.UpperDimensionContext; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisColor; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionCarvingEntry; +import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; +import art.arcane.iris.engine.object.IrisEngineData; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.iris.util.common.data.DataProvider; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.iris.util.project.stream.ProceduralStream; +import org.jetbrains.annotations.Nullable; + +import java.awt.Color; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, Hotloadable { + IrisComplex getComplex(); + + default @Nullable UpperDimensionContext getUpperContext() { + return null; + } + + EngineMode getMode(); + + EnginePlatformHooks getPlatformHooks(); + + /** + * World-space native structure piece bounds overlapping the given XZ rect. The answer is a pure function of the + * seed, the registries and this pack's structure policy, so it never depends on generation order. + */ + default KList getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) { + EnginePlatformHooks hooks = getPlatformHooks(); + return hooks == null ? NativeStructureVolume.NONE : hooks.nativeStructureVolumes(this, minX, minZ, maxX, maxZ); + } + + int getBlockUpdatesPerSecond(); + + void printMetrics(VolmitSender sender); + + EngineMantle getMantle(); + + void hotloadSilently(); + + void hotloadComplex(); + + void recycle(); + + void close(); + + default boolean isClosing() { + return isClosed(); + } + + double getMaxBiomeObjectDensity(); + + double getMaxBiomeDecoratorDensity(); + + double getMaxBiomeLayerDensity(); + + boolean isClosed(); + + default GenerationSessionManager getGenerationSessions() { + return null; + } + + default GenerationSessionLease acquireGenerationLease(String operation) throws GenerationSessionException { + GenerationSessionManager generationSessions = getGenerationSessions(); + if (generationSessions == null) { + return GenerationSessionLease.noop(); + } + + IrisContext context = IrisContext.get(); + if (context != null && context.getEngine() == this && context.getGenerationSessionId() != 0L) { + return generationSessions.continueSession(operation, context.getGenerationSessionId()); + } + if (isClosing() || isClosed()) { + throw new GenerationSessionException("Generation session rejected new work for " + operation + + " while the Iris engine is closing.", isClosed()); + } + + return generationSessions.acquire(operation); + } + + default long getGenerationSessionId() { + GenerationSessionManager generationSessions = getGenerationSessions(); + return generationSessions == null ? 0L : generationSessions.currentSessionId(); + } + + EngineWorldManager getWorldManager(); + + default UUID getBiomeID(int x, int z) { + return getComplex().getBaseBiomeIDStream().get(x, z); + } + + int getParallelism(); + + void setParallelism(int parallelism); + + EngineTarget getTarget(); + + default int getMaxHeight() { + return getTarget().getWorld().maxHeight(); + } + + default int getMinHeight() { + return getTarget().getWorld().minHeight(); + } + + default void setMinHeight(int min) { + getTarget().getWorld().minHeight(min); + } + + @BlockCoordinates + default void generate(int x, int z, TerrainChunk tc, boolean multicore) throws WrongEngineBroException { + generate(x, z, Hunk.view(tc), Hunk.viewBiomes(tc), multicore); + } + + @BlockCoordinates + void generate(int x, int z, Hunk blocks, Hunk biomes, boolean multicore) throws WrongEngineBroException; + + EngineMetrics getMetrics(); + + default void save() { + NativeStructureOwnershipStore.flush(this); + getMantle().save(); + getWorldManager().onSave(); + saveEngineData(); + } + + default void saveNow() { + NativeStructureOwnershipStore.flush(this); + getMantle().saveAllNow(); + saveEngineData(); + } + + SeedManager getSeedManager(); + + void saveEngineData(); + + default String getName() { + return getDimension().getName(); + } + + default IrisData getData() { + return getTarget().getData(); + } + + default IrisWorld getWorld() { + return getTarget().getWorld(); + } + + default IrisDimension getDimension() { + return getTarget().getDimension(); + } + + @BlockCoordinates + default Color draw(double x, double z) { + IrisRegion region = getRegion((int) x, (int) z); + IrisBiome biome = getSurfaceBiome((int) x, (int) z); + int height = getHeight((int) x, (int) z); + double heightFactor = M.lerpInverse(0, getTarget().getHeight(), height); + Color irc = region.getColor(this.getComplex(), RenderType.BIOME); + Color ibc = biome.getColor(this, RenderType.BIOME); + Color rc = irc != null ? irc : Color.GREEN.darker(); + Color bc = ibc != null ? ibc : biome.isAquatic() ? Color.BLUE : Color.YELLOW; + + return IrisColor.blend(rc, bc, bc, Color.getHSBColor(0, 0, (float) heightFactor)); + } + + @BlockCoordinates + default IrisRegion getRegion(int x, int z) { + return getComplex().getRegionStream().get(x, z); + } + + void generateMatter(int x, int z, boolean multicore, ChunkContext context); + + @BlockCoordinates + default IrisBiome getCaveOrMantleBiome(int x, int y, int z) { + MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class); + + if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) { + IrisBiome biome = getData().getBiomeLoader().load(m.getCustomBiome()); + + if (biome != null) { + return biome; + } + } + + return getCaveBiome(x, y, z); + } + + @ChunkCoordinates + Set getObjectsAt(int x, int z); + + @ChunkCoordinates + Set> getPOIsAt(int x, int z); + + @BlockCoordinates + default IrisBiome getCaveBiome(int x, int z) { + return getComplex().getCaveBiomeStream().get(x, z); + } + + @BlockCoordinates + default IrisBiome getCaveBiome(int x, int y, int z) { + return getCaveBiome(x, y, z, null); + } + + @BlockCoordinates + default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) { + IrisBiome surfaceBiome = getSurfaceBiome(x, z); + int worldY = y + getWorld().minHeight(); + IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state); + if (rootCarvingEntry != null) { + IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(this, rootCarvingEntry, x, z, state); + IrisBiome resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(this, resolvedCarvingEntry, state); + if (resolvedCarvingBiome != null) { + return resolvedCarvingBiome; + } + } + + IrisBiome caveBiome = getCaveBiome(x, z); + if (caveBiome == null) { + return surfaceBiome; + } + + int surfaceY = getComplex().getHeightStream().get(x, z).intValue(); + int depthBelowSurface = surfaceY - y; + if (depthBelowSurface <= 0) { + return surfaceBiome; + } + + int minDepth = Math.max(0, caveBiome.getCaveMinDepthBelowSurface()); + if (depthBelowSurface < minDepth) { + return surfaceBiome; + } + + return caveBiome; + } + + @BlockCoordinates + default IrisBiome getSurfaceBiome(int x, int z) { + return getComplex().getTrueBiomeStream().get(x, z); + } + + @BlockCoordinates + default int getHeight(int x, int z) { + return getHeight(x, z, true); + } + + @BlockCoordinates + default int getHeight(int x, int z, boolean ignoreFluid) { + return getMantle().getHighest(x, z, getData(), ignoreFluid); + } + + @BlockCoordinates + @Override + default void catchBlockUpdates(int x, int y, int z, PlatformBlockState data) { + if (data == null) { + return; + } + + if (B.isUpdatable(data)) { + getMantle().updateBlock(x, y, z); + } + if (data.isCustom()) { + getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.CUSTOM_ACTIVE, true); + } + } + + void blockUpdatedMetric(); + + EngineEffects getEffects(); + + default MultiBurst burst() { + return getTarget().getBurster(); + } + + default void clean() { + burst().lazy(() -> getMantle().trim(10)); + } + + IrisBiome getFocus(); + + IrisRegion getFocusRegion(); + + + IrisEngineData getEngineData(); + + default KList getAllBiomes() { + KMap v = new KMap<>(); + + IrisDimension dim = getDimension(); + dim.getReachableBiomes(this).forEach((i) -> v.put(i.getLoadKey(), i)); + + return v.v(); + } + + int getGenerated(); + + CompletableFuture getHash32(); + + default IrisPosition lookForStreamResult(T find, ProceduralStream stream, Function2 matcher, long timeout) { + AtomicInteger checked = new AtomicInteger(); + AtomicLong time = new AtomicLong(M.ms()); + AtomicReference r = new AtomicReference<>(); + BurstExecutor b = burst().burst(); + + while (M.ms() - time.get() < timeout && r.get() == null) { + b.queue(() -> { + for (int i = 0; i < 1000; i++) { + if (M.ms() - time.get() > timeout) { + return; + } + + int x = RNG.r.i(-29999970, 29999970); + int z = RNG.r.i(-29999970, 29999970); + checked.incrementAndGet(); + if (matcher.apply(stream.get(x, z), find)) { + r.set(new IrisPosition(x, 120, z)); + time.set(0); + } + } + }); + } + + return r.get(); + } + + double getGeneratedPerSecond(); + + default int getHeight() { + return getWorld().getHeight(); + } + + boolean isStudio(); + + default IrisBiome getBiome(int x, int y, int z) { + if (y <= getHeight(x, z) - 2) { + return getCaveBiome(x, y, z); + } + + return getSurfaceBiome(x, z); + } + + default IrisBiome getBiomeOrMantle(int x, int y, int z) { + if (y <= getHeight(x, z) - 2) { + return getCaveOrMantleBiome(x, y, z); + } + + return getSurfaceBiome(x, z); + } + + default String getObjectPlacementKey(int x, int y, int z) { + PlacedObject o = getObjectPlacement(x, y, z); + + if (o != null && o.getObject() != null) { + return o.getObject().getLoadKey() + "@" + o.getId(); + } + + MantleChunk chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use(); + try { + String raw = chunk.get(x & 15, y, z & 15, String.class); + return (raw == null || raw.isEmpty()) ? null : raw; + } finally { + chunk.release(); + } + } + + default PlacedObject getObjectPlacement(int x, int y, int z) { + MantleChunk chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use(); + try { + return getObjectPlacement(x, y, z, chunk); + } finally { + chunk.release(); + } + } + + default PlacedObject getObjectPlacement(int x, int y, int z, MantleChunk chunk) { + String objectAt = chunk.get(x & 15, y, z & 15, String.class); + if (objectAt == null || objectAt.isEmpty()) { + return null; + } + + StructurePlacementMarker.Decoded marker = StructurePlacementMarker.decode(objectAt); + if (marker == null) { + return null; + } + String object = marker.objectKey(); + if (object.startsWith("procedural/")) { + return null; + } + int id = marker.placementId(); + + if (marker.structureAware()) { + IrisObject placedObject = getData().getObjectLoader().load(object); + IrisStructure structure = getData().load(IrisStructure.class, marker.structureKey(), false); + IrisObjectPlacement placement = placedObject == null || structure == null + ? null + : structure.createLootPlacement(object); + return new PlacedObject(placement, placedObject, id, x, z); + } + + + IrisRegion region = getRegion(x, z); + + for (IrisObjectPlacement i : region.getObjects()) { + if (i.getPlace().contains(object)) { + return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z); + } + } + + IrisBiome biome = getSurfaceBiome(x, z); + + for (IrisObjectPlacement i : biome.getObjects()) { + if (i.getPlace().contains(object)) { + return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z); + } + } + + return new PlacedObject(null, getData().getObjectLoader().load(object), id, x, z); + } + + int getCacheID(); + + default boolean hasObjectPlacement(String objectKey) { + String normalizedObjectKey = normalizeObjectPlacementKey(objectKey); + if (normalizedObjectKey.isBlank()) { + return false; + } + + Set biomeKeys = getDimension().getReachableBiomes(this).stream() + .filter((i) -> containsObjectPlacement(i.getObjects(), normalizedObjectKey)) + .map(IrisRegistrant::getLoadKey) + .collect(Collectors.toSet()); + Set regionKeys = getDimension().getAllRegions(this).stream() + .filter((i) -> i.getAllBiomeIds().stream().anyMatch(biomeKeys::contains) + || containsObjectPlacement(i.getObjects(), normalizedObjectKey)) + .map(IrisRegistrant::getLoadKey) + .collect(Collectors.toSet()); + return !regionKeys.isEmpty(); + } + + private static boolean containsObjectPlacement(KList placements, String normalizedObjectKey) { + if (placements == null || placements.isEmpty() || normalizedObjectKey.isBlank()) { + return false; + } + + for (IrisObjectPlacement placement : placements) { + if (placement == null || placement.getPlace() == null || placement.getPlace().isEmpty()) { + continue; + } + + for (String placedObject : placement.getPlace()) { + String normalizedPlacedObject = normalizeObjectPlacementKey(placedObject); + if (!normalizedPlacedObject.isBlank() && normalizedPlacedObject.equals(normalizedObjectKey)) { + return true; + } + } + } + + return false; + } + + private static String normalizeObjectPlacementKey(String objectKey) { + if (objectKey == null) { + return ""; + } + + String normalized = objectKey.trim().replace('\\', '/'); + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + while (normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + if (normalized.endsWith(".iob")) { + normalized = normalized.substring(0, normalized.length() - 4); + } + return normalized.toLowerCase(Locale.ROOT); + } + + default void cleanupMantleChunk(int x, int z) { + if (getPlatformHooks().shouldSkipMantleCleanup(this)) { + return; + } + if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) { + getMantle().cleanupChunk(x, z); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineActuator.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineActuator.java similarity index 81% rename from core/src/main/java/com/volmit/iris/engine/framework/EngineActuator.java rename to core/src/main/java/art/arcane/iris/engine/framework/EngineActuator.java index b8871b905..245a4d6bf 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineActuator.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; - -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; +package art.arcane.iris.engine.framework; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; public interface EngineActuator extends EngineComponent { @BlockCoordinates void actuate(int x, int z, Hunk output, boolean multicore, ChunkContext context); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedActuator.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedActuator.java new file mode 100644 index 000000000..6ce3c16b4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedActuator.java @@ -0,0 +1,52 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; +public abstract class EngineAssignedActuator extends EngineAssignedComponent implements EngineActuator { + public EngineAssignedActuator(Engine engine, String name) { + super(engine, name); + } + + public abstract void onActuate(int x, int z, Hunk output, boolean multicore, ChunkContext context); + + @BlockCoordinates + @Override + public void actuate(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + try { + onActuate(x, z, output, multicore, context); + } catch (Throwable e) { + // Same contract as EngineAssignedModifier.modify: a failed actuator leaves a + // half-written chunk. Never continue as if it succeeded; let the chunk + // generation failure path abort the chunk instead of persisting it. + IrisLogging.error("Actuator Failure: " + getName()); + IrisLogging.reportError(e); + if (e instanceof Error error) { + throw error; + } + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException("Actuator Failure: " + getName(), e); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedComponent.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedComponent.java similarity index 85% rename from core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedComponent.java rename to core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedComponent.java index b356eb722..e551e8968 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedComponent.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; -import com.volmit.iris.Iris; -import com.volmit.iris.util.math.RollingSequence; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RollingSequence; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -33,7 +33,7 @@ public class EngineAssignedComponent implements EngineComponent { private final String name; public EngineAssignedComponent(Engine engine, String name) { - Iris.debug("Engine: " + engine.getCacheID() + " Starting " + name); + IrisLogging.debug("Engine: " + engine.getCacheID() + " Starting " + name); this.engine = engine; this.metrics = new RollingSequence(16); this.name = name; diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedModifier.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedModifier.java new file mode 100644 index 000000000..a2ba49676 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedModifier.java @@ -0,0 +1,55 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; +public abstract class EngineAssignedModifier extends EngineAssignedComponent implements EngineModifier { + public EngineAssignedModifier(Engine engine, String name) { + super(engine, name); + } + + @BlockCoordinates + public abstract void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context); + + @BlockCoordinates + @Override + public void modify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + try { + onModify(x, z, output, multicore, context); + } catch (Throwable e) { + IrisLogging.error("Modifier Failure: " + getName()); + IrisLogging.reportError(e); + + // A failed modifier leaves a half-written chunk. Never continue as if it succeeded, + // let the chunk generation failure path handle it. + if (e instanceof Error error) { + throw error; + } + + if (e instanceof RuntimeException runtime) { + throw runtime; + } + + throw new IllegalStateException("Modifier Failure: " + getName(), e); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java new file mode 100644 index 000000000..292495ac4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java @@ -0,0 +1,228 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.events.IrisEngineHotloadEvent; +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 org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.bukkit.event.world.WorldSaveEvent; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +public abstract class EngineAssignedWorldManager extends EngineAssignedComponent implements EngineWorldManager, BukkitEngineWorldManager, Listener { + private final Object managerLifecycleLock; + private final AtomicBoolean started; + private boolean listenerRegistered; + private boolean closeRequested; + private int taskId; + protected AtomicBoolean ignoreTP = new AtomicBoolean(false); + + public EngineAssignedWorldManager() { + super(null, null); + managerLifecycleLock = new Object(); + started = new AtomicBoolean(false); + taskId = -1; + } + + public EngineAssignedWorldManager(Engine engine) { + super(engine, "World"); + managerLifecycleLock = new Object(); + started = new AtomicBoolean(false); + taskId = -1; + } + + @Override + public void start() { + synchronized (managerLifecycleLock) { + if (started.get()) { + return; + } + if (closeRequested) { + throw new IllegalStateException("Cannot restart a closed Iris world manager."); + } + + started.set(true); + try { + listenerRegistered = true; + registerManagerListener(); + taskId = scheduleManagerTick(() -> runManagerTask("bukkit_world_manager_tick", this::onTick)); + } catch (Throwable e) { + started.set(false); + closeRequested = true; + Throwable cleanupFailure = closeManagerResources(); + if (cleanupFailure != null) { + e.addSuppressed(cleanupFailure); + } + throw propagate(e); + } + } + } + + @EventHandler + public void on(IrisEngineHotloadEvent e) { + runManagerTask("bukkit_world_manager_hotload_event", () -> { + for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) { + i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f); + VolmitSender s = new VolmitSender(i); + s.sendAction(C.IRIS + IrisLanguage.text(RuntimeUiMessages.ENGINE_HOTLOADED)); + } + }); + } + + @EventHandler + public void on(WorldSaveEvent e) { + runManagerTask("bukkit_world_manager_save", () -> { + if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) { + getEngine().save(); + } + }); + } + + @EventHandler + public void on(BlockBreakEvent e) { + runManagerTask("bukkit_world_manager_block_break", () -> { + if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) { + onBlockBreak(e); + } + }); + } + + @EventHandler + public void on(BlockPlaceEvent e) { + runManagerTask("bukkit_world_manager_block_place", () -> { + if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) { + onBlockPlace(e); + } + }); + } + + @EventHandler + public void on(ChunkLoadEvent e) { + runManagerTask("bukkit_world_manager_chunk_load", () -> { + if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) { + onChunkLoad(e.getChunk(), e.isNewChunk()); + } + }); + } + + @EventHandler + public void on(ChunkUnloadEvent e) { + runManagerTask("bukkit_world_manager_chunk_unload", () -> { + if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) { + onChunkUnload(e.getChunk()); + } + }); + } + + @Override + public void close() { + Throwable failure; + synchronized (managerLifecycleLock) { + closeRequested = true; + started.set(false); + failure = closeManagerResources(); + } + if (failure != null) { + throw new IllegalStateException("Failed to completely stop the Iris world manager.", failure); + } + } + + protected void registerManagerListener() { + BukkitPlatform.volmitPlugin().registerListener(this); + } + + protected int scheduleManagerTick(Runnable tick) { + return J.sr(tick, 1); + } + + protected void unregisterManagerListener() { + BukkitPlatform.volmitPlugin().unregisterListener(this); + } + + protected void cancelManagerTick(int scheduledTaskId) { + J.csr(scheduledTaskId); + } + + private Throwable closeManagerResources() { + Throwable failure = null; + if (listenerRegistered) { + try { + unregisterManagerListener(); + listenerRegistered = false; + } catch (Throwable e) { + failure = e; + } + } + if (taskId != -1) { + try { + cancelManagerTick(taskId); + taskId = -1; + } catch (Throwable e) { + if (failure == null) { + failure = e; + } else if (failure != e) { + failure.addSuppressed(e); + } + } + } + return failure; + } + + private RuntimeException propagate(Throwable failure) { + if (failure instanceof RuntimeException runtimeException) { + return runtimeException; + } + if (failure instanceof Error error) { + throw error; + } + return new IllegalStateException(failure); + } + + protected boolean runManagerTask(String operation, Runnable task) { + if (!started.get()) { + return false; + } + return EngineLifecycleTasks.run(getEngine(), operation, task); + } + + protected T callManagerTask(String operation, Supplier task, T unavailable) { + if (!started.get()) { + return unavailable; + } + return EngineLifecycleTasks.call(getEngine(), operation, task, unavailable); + } + + protected boolean isManagerStarted() { + return started.get(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineComponent.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineComponent.java new file mode 100644 index 000000000..a7d34420a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineComponent.java @@ -0,0 +1,74 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.volmlib.util.math.RollingSequence; +import art.arcane.iris.util.common.parallel.MultiBurst; + +public interface EngineComponent { + Engine getEngine(); + + RollingSequence getMetrics(); + + String getName(); + + default MultiBurst burst() { + return getEngine().burst(); + } + + default void close() { + try { + EngineComponentCleanup cleanup = IrisServices.getOrNull(EngineComponentCleanup.class); + if (cleanup != null) { + cleanup.release(this); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + default IrisData getData() { + return getEngine().getData(); + } + + default EngineTarget getTarget() { + return getEngine().getTarget(); + } + + default IrisDimension getDimension() { + return getEngine().getDimension(); + } + + default long getSeed() { + return getEngine().getSeedManager().getComponent(); + } + + default int getParallelism() { + return getEngine().getParallelism(); + } + + default IrisComplex getComplex() { + return getEngine().getComplex(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineComponentCleanup.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineComponentCleanup.java new file mode 100644 index 000000000..9b828a691 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineComponentCleanup.java @@ -0,0 +1,23 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +public interface EngineComponentCleanup { + void release(EngineComponent component); +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineDecorator.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineDecorator.java new file mode 100644 index 000000000..bd8dcdc36 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineDecorator.java @@ -0,0 +1,45 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; + +public interface EngineDecorator extends EngineComponent { + + @BlockCoordinates + void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max); + + @BlockCoordinates + default void decorate(int x, int z, int realX, int realZ, Hunk data, IrisBiome biome, int height, int max) { + decorate(x, z, realX, realX, realX, realZ, realZ, realZ, data, biome, height, max); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + default boolean canGoOn(PlatformBlockState decorant, PlatformBlockState atop) { + if (atop == null || atop.isAir()) { + return false; + } + + return B.canPlaceOnto(decorant, atop); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineEffects.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineEffects.java similarity index 95% rename from core/src/main/java/com/volmit/iris/engine/framework/EngineEffects.java rename to core/src/main/java/art/arcane/iris/engine/framework/EngineEffects.java index ff6326985..e79318275 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineEffects.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineEffects.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; public interface EngineEffects extends EngineComponent { void updatePlayerMap(); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineEffectsProvider.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineEffectsProvider.java new file mode 100644 index 000000000..dbbcd1772 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineEffectsProvider.java @@ -0,0 +1,23 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +public interface EngineEffectsProvider { + EngineEffects create(Engine engine); +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineLifecycleTasks.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineLifecycleTasks.java new file mode 100644 index 000000000..d358c8bd3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineLifecycleTasks.java @@ -0,0 +1,40 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.util.project.context.IrisContext; + +import java.util.Objects; +import java.util.function.Supplier; + +public final class EngineLifecycleTasks { + private EngineLifecycleTasks() { + } + + public static boolean run(Engine engine, String operation, Runnable task) { + Objects.requireNonNull(engine); + Objects.requireNonNull(operation); + Objects.requireNonNull(task); + try (GenerationSessionLease lease = engine.acquireGenerationLease(operation); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + task.run(); + return true; + } catch (GenerationSessionException exception) { + if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) { + return false; + } + throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception); + } + } + + public static T call(Engine engine, String operation, Supplier task, T unavailable) { + Objects.requireNonNull(task); + try (GenerationSessionLease lease = engine.acquireGenerationLease(operation); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + return task.get(); + } catch (GenerationSessionException exception) { + if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) { + return unavailable; + } + throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java new file mode 100644 index 000000000..fb165b81b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java @@ -0,0 +1,137 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.volmlib.util.atomics.AtomicRollingSequence; +import art.arcane.volmlib.util.collection.KMap; +import lombok.Data; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Data +public class EngineMetrics { + private final AtomicRollingSequence total; + private final AtomicRollingSequence updates; + private final AtomicRollingSequence terrain; + private final AtomicRollingSequence biome; + private final AtomicRollingSequence parallax; + private final AtomicRollingSequence parallaxInsert; + private final AtomicRollingSequence post; + private final AtomicRollingSequence perfection; + private final AtomicRollingSequence api; + private final AtomicRollingSequence decoration; + private final AtomicRollingSequence cave; + private final AtomicRollingSequence ravine; + private final AtomicRollingSequence deposit; + private final AtomicRollingSequence carveResolve; + private final AtomicRollingSequence carveApply; + private final AtomicRollingSequence noiseHeightInterpolate; + private final AtomicRollingSequence noiseHeightGenerator; + private final AtomicRollingSequence contextPrefill; + private final AtomicRollingSequence contextPrefillHeight; + private final AtomicRollingSequence contextPrefillBiome; + private final AtomicRollingSequence contextPrefillRock; + private final AtomicRollingSequence contextPrefillFluid; + private final AtomicRollingSequence contextPrefillRegion; + private final AtomicRollingSequence contextPrefillCave; + private final AtomicRollingSequence pregenWaitPermit; + private final AtomicRollingSequence pregenWaitAdaptive; + + public EngineMetrics(int mem) { + this.total = new AtomicRollingSequence(mem); + this.terrain = new AtomicRollingSequence(mem); + this.api = new AtomicRollingSequence(mem); + this.biome = new AtomicRollingSequence(mem); + this.perfection = new AtomicRollingSequence(mem); + this.parallax = new AtomicRollingSequence(mem); + this.parallaxInsert = new AtomicRollingSequence(mem); + this.post = new AtomicRollingSequence(mem); + this.decoration = new AtomicRollingSequence(mem); + this.updates = new AtomicRollingSequence(mem); + this.cave = new AtomicRollingSequence(mem); + this.ravine = new AtomicRollingSequence(mem); + this.deposit = new AtomicRollingSequence(mem); + this.carveResolve = new AtomicRollingSequence(mem); + this.carveApply = new AtomicRollingSequence(mem); + this.noiseHeightInterpolate = new AtomicRollingSequence(mem); + this.noiseHeightGenerator = new AtomicRollingSequence(mem); + this.contextPrefill = new AtomicRollingSequence(mem); + this.contextPrefillHeight = new AtomicRollingSequence(mem); + this.contextPrefillBiome = new AtomicRollingSequence(mem); + this.contextPrefillRock = new AtomicRollingSequence(mem); + this.contextPrefillFluid = new AtomicRollingSequence(mem); + this.contextPrefillRegion = new AtomicRollingSequence(mem); + this.contextPrefillCave = new AtomicRollingSequence(mem); + this.pregenWaitPermit = new AtomicRollingSequence(mem); + this.pregenWaitAdaptive = new AtomicRollingSequence(mem); + } + + public KMap pull() { + KMap v = new KMap<>(); + v.put("total", total.getAverage()); + v.put("terrain", terrain.getAverage()); + v.put("biome", biome.getAverage()); + v.put("parallax", parallax.getAverage()); + v.put("parallax.insert", parallaxInsert.getAverage()); + v.put("post", post.getAverage()); + v.put("perfection", perfection.getAverage()); + v.put("decoration", decoration.getAverage()); + v.put("api", api.getAverage()); + v.put("updates", updates.getAverage()); + v.put("cave", cave.getAverage()); + v.put("ravine", ravine.getAverage()); + v.put("deposit", deposit.getAverage()); + v.put("carve.resolve", carveResolve.getAverage()); + v.put("carve.apply", carveApply.getAverage()); + v.put("noise.height.interpolate", noiseHeightInterpolate.getAverage()); + v.put("noise.height.generator", noiseHeightGenerator.getAverage()); + v.put("context.prefill", contextPrefill.getAverage()); + v.put("context.prefill.height", contextPrefillHeight.getAverage()); + v.put("context.prefill.biome", contextPrefillBiome.getAverage()); + v.put("context.prefill.rock", contextPrefillRock.getAverage()); + v.put("context.prefill.fluid", contextPrefillFluid.getAverage()); + v.put("context.prefill.region", contextPrefillRegion.getAverage()); + v.put("context.prefill.cave", contextPrefillCave.getAverage()); + v.put("pregen.wait.permit", pregenWaitPermit.getAverage()); + v.put("pregen.wait.adaptive", pregenWaitAdaptive.getAverage()); + + return v; + } + + public Map telemetryAverages() { + Map averages = new LinkedHashMap<>(); + averages.put("total", total.getAverage()); + averages.put("updates", updates.getAverage()); + averages.put("terrain", terrain.getAverage()); + averages.put("biome", biome.getAverage()); + averages.put("post", post.getAverage()); + averages.put("perfection", perfection.getAverage()); + averages.put("decoration", decoration.getAverage()); + averages.put("cave", cave.getAverage()); + averages.put("deposit", deposit.getAverage()); + averages.put("carve.resolve", carveResolve.getAverage()); + averages.put("carve.apply", carveApply.getAverage()); + averages.put("context.prefill", contextPrefill.getAverage()); + averages.put("pregen.wait.permit", pregenWaitPermit.getAverage()); + averages.put("pregen.wait.adaptive", pregenWaitAdaptive.getAverage()); + averages.entrySet().removeIf(entry -> !Double.isFinite(entry.getValue()) || entry.getValue() < 0D); + return Map.copyOf(averages); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java new file mode 100644 index 000000000..d152e8ed2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java @@ -0,0 +1,116 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; + +public interface EngineMode extends Staged { + void close(); + + Engine getEngine(); + + default MultiBurst burst() { + return getEngine().burst(); + } + + default EngineStage burst(EngineStage... stages) { + return (x, z, blocks, biomes, multicore, ctx) -> { + BurstExecutor e = burst().burst(stages.length); + e.setMulticore(multicore); + // BurstExecutor.complete() logs-and-swallows stage failures; without re-propagation + // a multicore run would commit a half-written chunk that the inline (production) + // path correctly aborts. + java.util.concurrent.atomic.AtomicReference failure = new java.util.concurrent.atomic.AtomicReference<>(); + + for (EngineStage i : stages) { + e.queue(() -> { + if (failure.get() != null) { + return; + } + try (IrisContext.Scope stageScope = IrisContext.open(getEngine(), ctx.getGenerationSessionId(), ctx)) { + i.generate(x, z, blocks, biomes, multicore, ctx); + } catch (Throwable t) { + failure.compareAndSet(null, t); + // Rethrow so the inline (multicore=false) path still aborts out of + // queue() on the first failure, exactly as before. + if (t instanceof Error error) { + throw error; + } + if (t instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(t); + } + }); + } + + e.complete(); + + Throwable t = failure.get(); + if (t != null) { + if (t instanceof Error error) { + throw error; + } + if (t instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException("Burst stage failure during chunk generation", t); + } + }; + } + + default IrisComplex getComplex() { + return getEngine().getComplex(); + } + + default EngineMantle getMantle() { + return getEngine().getMantle(); + } + + default void generateMatter(int x, int z, boolean multicore, ChunkContext context) { + getMantle().generateMatter(x, z, multicore, context); + } + + @BlockCoordinates + default void generate(int x, int z, Hunk blocks, Hunk biomes, boolean multicore, long generationSessionId) { + boolean cacheContext = !getEngine().getPlatformHooks().shouldDisableChunkContextCache(getEngine()); + ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE; + ChunkContext ctx = new ChunkContext(x, z, getComplex(), generationSessionId, cacheContext, prefillPlan, getEngine().getMetrics()); + + EngineStage[] stages = getStages().toArray(new EngineStage[0]); + try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) { + for (EngineStage i : stages) { + i.generate(x, z, blocks, biomes, multicore, ctx); + } + } + } + + static boolean shouldDisableContextCacheForMaintenance(boolean maintenanceActive, boolean pregeneratorTargetsWorld) { + return maintenanceActive && !pregeneratorTargetsWorld; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineModifier.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineModifier.java similarity index 81% rename from core/src/main/java/com/volmit/iris/engine/framework/EngineModifier.java rename to core/src/main/java/art/arcane/iris/engine/framework/EngineModifier.java index 877fa4616..79f1cf5c5 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineModifier.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; - -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; +package art.arcane.iris.engine.framework; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; public interface EngineModifier extends EngineComponent { @BlockCoordinates void modify(int x, int z, Hunk t, boolean multicore, ChunkContext context); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EnginePlatformHooks.java b/core/src/main/java/art/arcane/iris/engine/framework/EnginePlatformHooks.java new file mode 100644 index 000000000..ca95eb3d5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EnginePlatformHooks.java @@ -0,0 +1,70 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.volmlib.util.collection.KList; + +public interface EnginePlatformHooks { + /** + * World-space piece bounds of every native structure that will generate inside the given XZ rect. Platforms + * without native structures return no volumes, which keeps the object veto free on those platforms. + */ + default KList nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) { + return NativeStructureVolume.NONE; + } + + default void refreshWorkspace(Engine engine) { + } + + default void refreshDatapackWorkspace(Engine engine) { + } + + default void reloadDatapacks(Engine engine) { + } + + default void fireHotloadEvent(Engine engine) { + } + + default void validateDimensionHotload(Engine engine, IrisDimension replacement) { + } + + default boolean isPregeneratorActive(Engine engine) { + return false; + } + + default void shutdownPregenerator(Engine engine) { + } + + default boolean shouldDisableChunkContextCache(Engine engine) { + return false; + } + + default boolean shouldSkipMantleCleanup(Engine engine) { + return false; + } + + default boolean shouldSkipMantleMarkerRead(Engine engine, int chunkX, int chunkZ) { + return false; + } + + default boolean shouldBypassMantleStages(Engine engine) { + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EnginePlayer.java b/core/src/main/java/art/arcane/iris/engine/framework/EnginePlayer.java new file mode 100644 index 000000000..aefc2be5f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EnginePlayer.java @@ -0,0 +1,111 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisEffect; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.math.M; +import lombok.Data; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +@Data +public class EnginePlayer { + private final Engine engine; + private final Player player; + private IrisBiome biome; + private IrisRegion region; + private Location lastLocation; + private long lastSample; + + public EnginePlayer(Engine engine, Player player) { + this.engine = engine; + this.player = player; + lastLocation = null; + lastSample = -1; + } + + public void tick() { + J.runEntity(player, this::tickOnEntity); + } + + private void tickOnEntity() { + if (sample() || !IrisSettings.get().getWorld().isEffectSystem()) { + return; + } + + if (region != null) { + for (IrisEffect effect : region.getEffects()) { + try { + effect.apply(player, getEngine()); + } catch (Throwable e) { + IrisLogging.reportError(e); + + } + } + } + + if (biome != null) { + for (IrisEffect effect : biome.getEffects()) { + try { + effect.apply(player, getEngine()); + } catch (Throwable e) { + IrisLogging.reportError(e); + + } + } + } + } + + public long ticksSinceLastSample() { + return M.ms() - lastSample; + } + + public boolean sample() { + Location current = player.getLocation().clone(); + if (current.getWorld() != BukkitWorldBinding.world(engine.getWorld())) { + return true; + } + try { + boolean sampled = lastLocation != null; + double distanceSquared = sampled ? current.distanceSquared(lastLocation) : 0D; + if (needsSample(sampled, ticksSinceLastSample(), distanceSquared)) { + lastLocation = current; + lastSample = M.ms(); + biome = EngineBukkitOps.getBiome(engine, current); + region = EngineBukkitOps.getRegion(engine, current); + } + return false; + } catch (Throwable e) { + IrisLogging.reportError(e); + + } + return true; + } + + static boolean needsSample(boolean sampled, long elapsedMillis, double distanceSquared) { + return !sampled || elapsedMillis > 55L && distanceSquared > 81D; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineStage.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineStage.java new file mode 100644 index 000000000..1f7735288 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineStage.java @@ -0,0 +1,36 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; + +public interface EngineStage { + @BlockCoordinates + void generate(int x, int z, Hunk blocks, Hunk biomes, boolean multicore, ChunkContext context); + + default void close() { + if (this instanceof EngineComponent c) { + c.close(); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineTarget.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineTarget.java similarity index 80% rename from core/src/main/java/com/volmit/iris/engine/framework/EngineTarget.java rename to core/src/main/java/art/arcane/iris/engine/framework/EngineTarget.java index 7e9388ffb..c9947a0b6 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineTarget.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineTarget.java @@ -16,12 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.engine.object.IrisWorld; -import com.volmit.iris.util.parallel.MultiBurst; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.util.common.parallel.MultiBurst; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -32,8 +32,8 @@ import lombok.ToString; public class EngineTarget { private final MultiBurst burster; private final IrisData data; - private IrisDimension dimension; - private IrisWorld world; + private final IrisDimension dimension; + private final IrisWorld world; public EngineTarget(IrisWorld world, IrisDimension dimension, IrisData data) { this.world = world; diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineTelemetrySnapshot.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineTelemetrySnapshot.java new file mode 100644 index 000000000..eac0eeb03 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineTelemetrySnapshot.java @@ -0,0 +1,222 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisEngineStatistics; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public record EngineTelemetrySnapshot( + long sampledAtMs, + String worldIdentity, + String worldName, + String dimensionKey, + boolean active, + boolean studio, + boolean closing, + boolean failed, + long loadedChunks, + long loadedEntities, + double entitySaturation, + long generatedSession, + long generatedTotal, + double chunksPerSecond, + long blockUpdatesPerSecond, + int parallelism, + int activeGenerationLeases, + long hotloadsTotal, + long mantleResidentPlates, + long mantleQueuedPlates, + double mantleIdleMs, + Map generationTimingsMs +) { + public EngineTelemetrySnapshot { + if (worldIdentity == null || worldIdentity.isBlank()) { + throw new IllegalArgumentException("Engine telemetry requires a world identity"); + } + worldIdentity = worldIdentity.trim(); + worldName = worldName == null || worldName.isBlank() ? worldIdentity : worldName.trim(); + dimensionKey = dimensionKey == null ? "" : dimensionKey.trim(); + loadedChunks = Math.max(0L, loadedChunks); + loadedEntities = Math.max(0L, loadedEntities); + entitySaturation = finiteNonNegative(entitySaturation); + generatedSession = Math.max(0L, generatedSession); + generatedTotal = Math.max(0L, generatedTotal); + chunksPerSecond = finiteNonNegative(chunksPerSecond); + blockUpdatesPerSecond = Math.max(0L, blockUpdatesPerSecond); + parallelism = Math.max(0, parallelism); + activeGenerationLeases = Math.max(0, activeGenerationLeases); + hotloadsTotal = Math.max(0L, hotloadsTotal); + mantleResidentPlates = Math.max(0L, mantleResidentPlates); + mantleQueuedPlates = Math.max(0L, mantleQueuedPlates); + mantleIdleMs = finiteNonNegative(mantleIdleMs); + generationTimingsMs = sanitizeTimings(generationTimingsMs); + } + + public static EngineTelemetrySnapshot capture(Engine engine, double chunksPerSecond, long sampledAtMs) { + if (engine == null) { + throw new IllegalArgumentException("Engine cannot be null"); + } + + EngineWorldManager worldManager = engine.getWorldManager(); + GenerationSessionManager sessions = engine.getGenerationSessions(); + IrisEngineStatistics statistics = engine.getEngineData().getStatistics(); + boolean closing = engine.isClosing(); + boolean closed = engine.isClosed(); + boolean failed = engine.hasFailed(); + return new EngineTelemetrySnapshot( + sampledAtMs, + engine.getWorld().identity(), + engine.getWorld().name(), + engine.getDimension().getLoadKey(), + !closing && !closed && !failed, + engine.isStudio(), + closing, + failed, + worldManager == null ? 0L : worldManager.getChunkCount(), + worldManager == null ? 0L : worldManager.getEntityCount(), + worldManager == null ? 0D : worldManager.getEntitySaturation(), + engine.getGenerated(), + statistics == null ? 0L : statistics.getChunksGenerated(), + chunksPerSecond, + engine.getBlockUpdatesPerSecond(), + engine.getParallelism(), + sessions == null ? 0 : sessions.activeLeases(), + statistics == null ? 0L : statistics.getTotalHotloads(), + engine.getMantle().getLoadedRegionCount(), + engine.getMantle().getUnloadRegionCount(), + engine.getMantle().getAdjustedIdleDuration(), + engine.getMetrics().telemetryAverages() + ); + } + + public static Aggregate aggregate(List snapshots) { + List safeSnapshots = snapshots == null ? List.of() : snapshots; + int active = 0; + int studio = 0; + int closing = 0; + int failed = 0; + long loadedChunks = 0L; + long loadedEntities = 0L; + double entitySaturation = 0D; + long generatedSession = 0L; + long generatedTotal = 0L; + double chunksPerSecond = 0D; + long blockUpdatesPerSecond = 0L; + int parallelism = 0; + int activeGenerationLeases = 0; + long hotloadsTotal = 0L; + long mantleResidentPlates = 0L; + long mantleQueuedPlates = 0L; + double mantleIdleTotal = 0D; + double mantleIdleMax = 0D; + double mantleIdleMin = Double.POSITIVE_INFINITY; + int mantleIdleCount = 0; + int worldCount = 0; + Map timingMaxima = new LinkedHashMap<>(); + + for (EngineTelemetrySnapshot snapshot : safeSnapshots) { + if (snapshot == null) { + continue; + } + worldCount++; + active += snapshot.active() ? 1 : 0; + studio += snapshot.studio() ? 1 : 0; + closing += snapshot.closing() ? 1 : 0; + failed += snapshot.failed() ? 1 : 0; + loadedChunks += snapshot.loadedChunks(); + loadedEntities += snapshot.loadedEntities(); + entitySaturation = Math.max(entitySaturation, snapshot.entitySaturation()); + generatedSession += snapshot.generatedSession(); + generatedTotal += snapshot.generatedTotal(); + chunksPerSecond += snapshot.chunksPerSecond(); + blockUpdatesPerSecond += snapshot.blockUpdatesPerSecond(); + parallelism += snapshot.parallelism(); + activeGenerationLeases += snapshot.activeGenerationLeases(); + hotloadsTotal += snapshot.hotloadsTotal(); + mantleResidentPlates += snapshot.mantleResidentPlates(); + mantleQueuedPlates += snapshot.mantleQueuedPlates(); + mantleIdleTotal += snapshot.mantleIdleMs(); + mantleIdleMax = Math.max(mantleIdleMax, snapshot.mantleIdleMs()); + mantleIdleMin = Math.min(mantleIdleMin, snapshot.mantleIdleMs()); + mantleIdleCount++; + for (Map.Entry entry : snapshot.generationTimingsMs().entrySet()) { + timingMaxima.merge(entry.getKey(), entry.getValue(), Math::max); + } + } + + return new Aggregate( + worldCount, + active, + studio, + closing, + failed, + loadedChunks, + loadedEntities, + entitySaturation, + generatedSession, + generatedTotal, + chunksPerSecond, + blockUpdatesPerSecond, + parallelism, + activeGenerationLeases, + hotloadsTotal, + mantleResidentPlates, + mantleQueuedPlates, + mantleIdleCount == 0 ? 0D : mantleIdleTotal / mantleIdleCount, + mantleIdleMax, + mantleIdleCount == 0 ? 0D : mantleIdleMin, + Map.copyOf(timingMaxima) + ); + } + + private static double finiteNonNegative(double value) { + return Double.isFinite(value) && value > 0D ? value : 0D; + } + + private static Map sanitizeTimings(Map timings) { + if (timings == null || timings.isEmpty()) { + return Map.of(); + } + Map sanitized = new LinkedHashMap<>(timings.size()); + for (Map.Entry entry : timings.entrySet()) { + String key = entry.getKey(); + Double value = entry.getValue(); + if (key == null || key.isBlank() || value == null || !Double.isFinite(value) || value < 0D) { + continue; + } + sanitized.put(key, value); + } + return Map.copyOf(sanitized); + } + + public record Aggregate( + int worldCount, + int active, + int studio, + int closing, + int failed, + long loadedChunks, + long loadedEntities, + double entitySaturationMax, + long generatedSession, + long generatedTotal, + double chunksPerSecond, + long blockUpdatesPerSecond, + int parallelism, + int activeGenerationLeases, + long hotloadsTotal, + long mantleResidentPlates, + long mantleQueuedPlates, + double mantleIdleAverageMs, + double mantleIdleMaxMs, + double mantleIdleMinMs, + Map generationTimingMaximaMs + ) { + public Aggregate { + generationTimingMaximaMs = generationTimingMaximaMs == null + ? Map.of() + : Map.copyOf(generationTimingMaximaMs); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManager.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManager.java new file mode 100644 index 000000000..3158c9bd5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManager.java @@ -0,0 +1,37 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +@SuppressWarnings("EmptyMethod") +public interface EngineWorldManager { + default void start() { + } + + void close(); + + int getEntityCount(); + + int getChunkCount(); + + double getEntitySaturation(); + + void onTick(); + + void onSave(); +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManagerProvider.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManagerProvider.java new file mode 100644 index 000000000..cdf617f35 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManagerProvider.java @@ -0,0 +1,23 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +public interface EngineWorldManagerProvider { + EngineWorldManager create(Engine engine); +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/Fallible.java b/core/src/main/java/art/arcane/iris/engine/framework/Fallible.java similarity index 90% rename from core/src/main/java/com/volmit/iris/engine/framework/Fallible.java rename to core/src/main/java/art/arcane/iris/engine/framework/Fallible.java index 3ee51d4d1..cb7c8a54b 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/Fallible.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Fallible.java @@ -16,16 +16,16 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; -import com.volmit.iris.Iris; +import art.arcane.iris.spi.IrisLogging; public interface Fallible { default void fail(String error) { try { throw new RuntimeException(); } catch (Throwable e) { - Iris.reportError(e); + IrisLogging.reportError(e); fail(error, e); } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionException.java b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionException.java new file mode 100644 index 000000000..e9a640112 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionException.java @@ -0,0 +1,18 @@ +package art.arcane.iris.engine.framework; + +public class GenerationSessionException extends WrongEngineBroException { + private final boolean expectedTeardown; + + public GenerationSessionException(String message) { + this(message, false); + } + + public GenerationSessionException(String message, boolean expectedTeardown) { + super(message); + this.expectedTeardown = expectedTeardown; + } + + public boolean isExpectedTeardown() { + return expectedTeardown; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionLease.java b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionLease.java new file mode 100644 index 000000000..75e45132f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionLease.java @@ -0,0 +1,35 @@ +package art.arcane.iris.engine.framework; + +public final class GenerationSessionLease implements AutoCloseable { + private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L); + + private final GenerationSessionManager manager; + private final GenerationSessionManager.GenerationSessionState state; + private final long sessionId; + private boolean released; + + GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state, long sessionId) { + this.manager = manager; + this.state = state; + this.sessionId = sessionId; + this.released = false; + } + + public static GenerationSessionLease noop() { + return NOOP; + } + + public long sessionId() { + return sessionId; + } + + @Override + public void close() { + if (released || state == null) { + return; + } + + released = true; + manager.releaseLease(state); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionManager.java b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionManager.java new file mode 100644 index 000000000..d571a8f4e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/GenerationSessionManager.java @@ -0,0 +1,133 @@ +package art.arcane.iris.engine.framework; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +public final class GenerationSessionManager { + private final AtomicLong sessionSequence; + private final AtomicReference current; + private final Object drainMonitor; + + public GenerationSessionManager() { + this.sessionSequence = new AtomicLong(0L); + this.current = new AtomicReference<>(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null))); + this.drainMonitor = new Object(); + } + + public GenerationSessionLease acquire(String operation) throws GenerationSessionException { + while (true) { + GenerationSessionState state = current.get(); + if (state == null || !state.accepting().get()) { + throw rejected(operation, state == null ? null : state); + } + + state.activeLeases().incrementAndGet(); + if (state != current.get()) { + state.activeLeases().decrementAndGet(); + continue; + } + + if (!state.accepting().get()) { + releaseLease(state); + throw rejected(operation, state); + } + + return new GenerationSessionLease(this, state, state.sessionId()); + } + } + + public GenerationSessionLease continueSession(String operation, long sessionId) throws GenerationSessionException { + while (true) { + GenerationSessionState state = current.get(); + if (state == null || state.sessionId() != sessionId) { + throw rejected(operation, state); + } + + state.activeLeases().incrementAndGet(); + if (state != current.get()) { + releaseLease(state); + continue; + } + + return new GenerationSessionLease(this, state, state.sessionId()); + } + } + + public long currentSessionId() { + GenerationSessionState state = current.get(); + return state == null ? 0L : state.sessionId(); + } + + public int activeLeases() { + GenerationSessionState state = current.get(); + return state == null ? 0 : state.activeLeases().get(); + } + + public void sealAndAwait(String reason, long timeoutMs) throws GenerationSessionException { + sealAndAwait(reason, timeoutMs, false); + } + + public void sealAndAwait(String reason, long timeoutMs, boolean teardown) throws GenerationSessionException { + GenerationSessionState state = current.get(); + if (state == null) { + return; + } + + state.accepting().set(false); + state.teardown().set(teardown); + state.sealReason().set(reason); + long deadline = System.currentTimeMillis() + Math.max(0L, timeoutMs); + synchronized (drainMonitor) { + while (state.activeLeases().get() > 0) { + long remaining = deadline - System.currentTimeMillis(); + if (remaining <= 0L) { + break; + } + + try { + drainMonitor.wait(remaining); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new GenerationSessionException("Generation session " + state.sessionId() + " was interrupted while draining for " + reason + ".", teardown); + } + } + } + + if (state.activeLeases().get() > 0) { + throw new GenerationSessionException("Generation session " + state.sessionId() + " failed to drain for " + reason + " after " + timeoutMs + "ms. Active leases=" + state.activeLeases().get() + ".", teardown); + } + } + + public void activateNextSession() { + current.set(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null))); + } + + private long nextSessionId() { + return sessionSequence.incrementAndGet(); + } + + void releaseLease(GenerationSessionState state) { + int remaining = state.activeLeases().decrementAndGet(); + if (remaining <= 0) { + synchronized (drainMonitor) { + drainMonitor.notifyAll(); + } + } + } + + private GenerationSessionException rejected(String operation, GenerationSessionState state) { + long sessionId = state == null ? currentSessionId() : state.sessionId(); + boolean teardown = state != null && state.teardown().get(); + String reason = state == null ? null : state.sealReason().get(); + if (teardown && reason != null && !reason.isBlank()) { + return new GenerationSessionException("Generation session " + sessionId + " rejected new work for " + operation + " during " + reason + ".", true); + } + + return new GenerationSessionException("Generation session " + sessionId + " rejected new work for " + operation + ".", teardown); + } + + record GenerationSessionState(long sessionId, AtomicBoolean accepting, AtomicInteger activeLeases, AtomicBoolean teardown, AtomicReference sealReason) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/HintedLocator.java b/core/src/main/java/art/arcane/iris/engine/framework/HintedLocator.java new file mode 100644 index 000000000..78794dc00 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/HintedLocator.java @@ -0,0 +1,615 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.util.ArrayDeque; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; + +public final class HintedLocator implements Locator { + private static final int BIOME_STRIDE_CHUNKS = 4; + private static final int REGION_STRIDE_CHUNKS = 16; + private static final int MAX_SAMPLE_RADIUS_CHUNKS = 100000; + + private final Locator exact; + private final Function planner; + + public HintedLocator(Locator exact, Function planner) { + this.exact = exact; + this.planner = planner; + } + + @FunctionalInterface + public interface CoarseSample { + boolean test(int blockX, int blockZ); + } + + public static final class SearchPlan { + private final boolean possible; + private final CoarseSample coarse; + private final int strideChunks; + private final Locator exactOverride; + + private SearchPlan(boolean possible, CoarseSample coarse, int strideChunks, Locator exactOverride) { + this.possible = possible; + this.coarse = coarse; + this.strideChunks = strideChunks; + this.exactOverride = exactOverride; + } + + public static SearchPlan impossible() { + return new SearchPlan(false, null, 1, null); + } + + public static SearchPlan unpruned() { + return new SearchPlan(true, null, 1, null); + } + + public static SearchPlan of(CoarseSample coarse, int strideChunks, Locator exactOverride) { + return new SearchPlan(true, coarse, strideChunks, exactOverride); + } + + public boolean isPossible() { + return possible; + } + + public CoarseSample getCoarse() { + return coarse; + } + + public int getStrideChunks() { + return strideChunks; + } + + public Locator getExactOverride() { + return exactOverride; + } + } + + @Override + public boolean matches(Engine engine, Position2 chunk) { + return exact.matches(engine, chunk); + } + + @Override + public CompletableFuture find(Engine engine, Position2 pos, long timeout, Consumer checks) throws WrongEngineBroException { + if (engine.isClosed()) { + throw new WrongEngineBroException(); + } + + AtomicBoolean stop = new AtomicBoolean(false); + CompletableFuture search = MultiBurst.burst.completeValueAsync(() -> { + try (GenerationSessionLease lease = engine.acquireGenerationLease("hinted_locator_search"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + SearchPlan plan = planner.apply(engine); + + if (!plan.isPossible()) { + return null; + } + + return search(engine, plan, pos, timeout, checks, stop); + } + }); + return LocatorCanceller.requestScoped(search, stop); + } + + private Position2 search(Engine engine, SearchPlan plan, Position2 pos, long timeout, Consumer checks, AtomicBoolean stop) { + Locator verifier = plan.getExactOverride() != null ? plan.getExactOverride() : exact; + int stride = Math.max(1, plan.getStrideChunks()); + int batchTarget = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32; + int maxRing = Math.max(1, MAX_SAMPLE_RADIUS_CHUNKS / stride); + PrecisionStopwatch stopwatch = PrecisionStopwatch.start(); + AtomicInteger covered = new AtomicInteger(); + KList batch = new KList<>(); + + for (int ring = 0; ring <= maxRing; ring++) { + if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) { + return null; + } + + appendRing(batch, pos, ring, stride); + + if (batch.size() < batchTarget && ring < maxRing) { + continue; + } + + Position2 result = processBatch(engine, plan, verifier, batch, stride, timeout, stopwatch, covered, checks, stop); + batch.clear(); + + if (result != null) { + return result; + } + } + + return null; + } + + private Position2 processBatch(Engine engine, SearchPlan plan, Locator verifier, KList batch, int stride, long timeout, PrecisionStopwatch stopwatch, AtomicInteger covered, Consumer checks, AtomicBoolean stop) { + int size = batch.size(); + boolean[] hits = new boolean[size]; + boolean fine = stride <= 1; + CoarseSample coarse = plan.getCoarse(); + AtomicBoolean matched = new AtomicBoolean(false); + BurstExecutor executor = MultiBurst.burst.burst(size); + + for (int i = 0; i < size; i++) { + int index = i; + Position2 sample = batch.get(i); + executor.queue(() -> { + if (stop.get() || engine.isClosing() || (fine && matched.get())) { + return; + } + + int blockX = (sample.getX() << 4) + 8; + int blockZ = (sample.getZ() << 4) + 8; + + if (coarse != null && !coarse.test(blockX, blockZ)) { + return; + } + + if (fine) { + if (verifier.matches(engine, sample)) { + hits[index] = true; + matched.set(true); + } + + return; + } + + hits[index] = true; + }); + } + + executor.complete(); + covered.addAndGet(size * stride * stride); + checks.accept(covered.get()); + + for (int i = 0; i < size; i++) { + if (!hits[i]) { + continue; + } + + if (fine) { + return batch.get(i); + } + + if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) { + return null; + } + + Position2 refined = refine(engine, verifier, batch.get(i), stride, timeout, stopwatch, stop); + + if (refined != null) { + return refined; + } + } + + return null; + } + + private Position2 refine(Engine engine, Locator verifier, Position2 candidate, int stride, long timeout, PrecisionStopwatch stopwatch, AtomicBoolean stop) { + KList cells = new KList<>(); + + for (int ring = 0; ring <= stride; ring++) { + if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) { + return null; + } + + cells.clear(); + appendRing(cells, candidate, ring, 1); + AtomicReference found = new AtomicReference<>(); + BurstExecutor executor = MultiBurst.burst.burst(cells.size()); + + for (Position2 cell : cells) { + executor.queue(() -> { + if (stop.get() || engine.isClosing() || found.get() != null) { + return; + } + + if (verifier.matches(engine, cell)) { + found.compareAndSet(null, cell); + } + }); + } + + executor.complete(); + + if (found.get() != null) { + return found.get(); + } + } + + return null; + } + + static void appendRing(KList batch, Position2 origin, int ring, int stride) { + if (ring == 0) { + batch.add(origin); + return; + } + + int offset = ring * stride; + + for (int dx = -ring; dx <= ring; dx++) { + int x = origin.getX() + (dx * stride); + batch.add(new Position2(x, origin.getZ() - offset)); + batch.add(new Position2(x, origin.getZ() + offset)); + } + + for (int dz = -ring + 1; dz <= ring - 1; dz++) { + int z = origin.getZ() + (dz * stride); + batch.add(new Position2(origin.getX() - offset, z)); + batch.add(new Position2(origin.getX() + offset, z)); + } + } + + public static SearchPlan biomePlan(Engine engine, String biomeKey) { + if (engine.getFocus() != null || engine.getFocusRegion() != null) { + return SearchPlan.unpruned(); + } + + IrisComplex complex = engine.getComplex(); + BiomeSource source = new BiomeSource(engine); + KSet landHosts = new KSet<>(); + KSet seaHosts = new KSet<>(); + KSet shoreHosts = new KSet<>(); + KSet surfaceRegions = new KSet<>(); + KSet caveRegions = new KSet<>(); + + for (IrisRegion region : engine.getDimension().getAllRegions(engine)) { + if (region == null) { + continue; + } + + boolean hosted = false; + + for (String root : region.getLandBiomes()) { + if (source.closureContains(root, biomeKey)) { + landHosts.add(root); + hosted = true; + } + } + + for (String root : region.getSeaBiomes()) { + if (source.closureContains(root, biomeKey)) { + seaHosts.add(root); + hosted = true; + } + } + + for (String root : region.getShoreBiomes()) { + if (source.closureContains(root, biomeKey)) { + shoreHosts.add(root); + hosted = true; + } + } + + if (hosted) { + surfaceRegions.add(region.getLoadKey()); + } + + if (region.getCaveBiomes().contains(biomeKey)) { + caveRegions.add(region.getLoadKey()); + } + } + + if (!surfaceRegions.isEmpty()) { + return SearchPlan.of(surfaceCoarse(complex, surfaceRegions, landHosts, seaHosts, shoreHosts), BIOME_STRIDE_CHUNKS, null); + } + + if (!caveRegions.isEmpty()) { + CoarseSample coarse = caveCoarse(complex, caveRegions, biomeKey); + Locator caveExact = (e, c) -> { + IrisBiome biome = e.getCaveBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8); + return biome != null && biomeKey.equals(biome.getLoadKey()); + }; + + return SearchPlan.of(coarse, BIOME_STRIDE_CHUNKS, caveExact); + } + + return SearchPlan.impossible(); + } + + public static SearchPlan caveBiomePlan(Engine engine, String biomeKey) { + if (engine.getFocus() != null || engine.getFocusRegion() != null) { + return SearchPlan.unpruned(); + } + + IrisComplex complex = engine.getComplex(); + KSet caveRegions = new KSet<>(); + + for (IrisRegion region : engine.getDimension().getAllRegions(engine)) { + if (region != null && region.getCaveBiomes().contains(biomeKey)) { + caveRegions.add(region.getLoadKey()); + } + } + + if (caveRegions.isEmpty()) { + return SearchPlan.impossible(); + } + + return SearchPlan.of(caveCoarse(complex, caveRegions, biomeKey), BIOME_STRIDE_CHUNKS, null); + } + + public static SearchPlan regionPlan(Engine engine, String regionKey) { + if (engine.getFocus() != null || engine.getFocusRegion() != null) { + return SearchPlan.unpruned(); + } + + if (!engine.getDimension().getRegions().contains(regionKey)) { + return SearchPlan.impossible(); + } + + IrisComplex complex = engine.getComplex(); + CoarseSample coarse = (x, z) -> { + IrisRegion region = complex.getRegionStream().get(x, z); + return region != null && regionKey.equals(region.getLoadKey()); + }; + + return SearchPlan.of(coarse, REGION_STRIDE_CHUNKS, null); + } + + public static SearchPlan objectPlan(Engine engine, String objectKey) { + if (engine.getFocus() != null || engine.getFocusRegion() != null) { + return SearchPlan.unpruned(); + } + + IrisComplex complex = engine.getComplex(); + BiomeSource source = new BiomeSource(engine); + KSet landHosts = new KSet<>(); + KSet seaHosts = new KSet<>(); + KSet shoreHosts = new KSet<>(); + KSet directRegions = new KSet<>(); + KSet hostedRegions = new KSet<>(); + + for (IrisRegion region : engine.getDimension().getAllRegions(engine)) { + if (region == null) { + continue; + } + + if (placesObject(region.getSurfaceObjects(), objectKey)) { + directRegions.add(region.getLoadKey()); + } + + boolean hosted = false; + + for (String root : region.getLandBiomes()) { + if (source.closurePlacesObject(root, objectKey)) { + landHosts.add(root); + hosted = true; + } + } + + for (String root : region.getSeaBiomes()) { + if (source.closurePlacesObject(root, objectKey)) { + seaHosts.add(root); + hosted = true; + } + } + + for (String root : region.getShoreBiomes()) { + if (source.closurePlacesObject(root, objectKey)) { + shoreHosts.add(root); + hosted = true; + } + } + + if (hosted) { + hostedRegions.add(region.getLoadKey()); + } + } + + if (directRegions.isEmpty() && hostedRegions.isEmpty()) { + return SearchPlan.impossible(); + } + + CoarseSample surface = hostedRegions.isEmpty() ? null : surfaceCoarse(complex, hostedRegions, landHosts, seaHosts, shoreHosts); + CoarseSample coarse = (x, z) -> { + if (!directRegions.isEmpty()) { + IrisRegion region = complex.getRegionStream().get(x, z); + + if (region != null && directRegions.contains(region.getLoadKey())) { + return true; + } + } + + return surface != null && surface.test(x, z); + }; + + return SearchPlan.of(coarse, 1, null); + } + + private static CoarseSample surfaceCoarse(IrisComplex complex, KSet regions, KSet landHosts, KSet seaHosts, KSet shoreHosts) { + return (x, z) -> { + IrisRegion region = complex.getRegionStream().get(x, z); + + if (region == null || !regions.contains(region.getLoadKey())) { + return false; + } + + if (!landHosts.isEmpty()) { + IrisBiome biome = complex.getLandBiomeStream().get(x, z); + + if (biome != null && landHosts.contains(biome.getLoadKey())) { + return true; + } + } + + if (!seaHosts.isEmpty()) { + IrisBiome biome = complex.getSeaBiomeStream().get(x, z); + + if (biome != null && seaHosts.contains(biome.getLoadKey())) { + return true; + } + } + + if (!shoreHosts.isEmpty()) { + IrisBiome biome = complex.getShoreBiomeStream().get(x, z); + + if (biome != null && shoreHosts.contains(biome.getLoadKey())) { + return true; + } + } + + return false; + }; + } + + private static CoarseSample caveCoarse(IrisComplex complex, KSet caveRegions, String biomeKey) { + return (x, z) -> { + IrisRegion region = complex.getRegionStream().get(x, z); + + if (region == null || !caveRegions.contains(region.getLoadKey())) { + return false; + } + + IrisBiome cave = complex.getCaveBiomeStream().get(x, z); + return cave != null && biomeKey.equals(cave.getLoadKey()); + }; + } + + private static boolean placesObject(KList placements, String objectKey) { + for (IrisObjectPlacement placement : placements) { + if (placement != null && placement.getPlace().contains(objectKey)) { + return true; + } + } + + return false; + } + + private static final class BiomeSource { + private final Engine engine; + private final KMap cache = new KMap<>(); + + private BiomeSource(Engine engine) { + this.engine = engine; + } + + private IrisBiome load(String key) { + if (key == null) { + return null; + } + + IrisBiome cached = cache.get(key); + + if (cached != null) { + return cached; + } + + IrisBiome loaded = engine.getData().getBiomeLoader().load(key); + + if (loaded != null) { + cache.put(key, loaded); + } + + return loaded; + } + + private boolean closureContains(String rootKey, String targetKey) { + if (rootKey == null || targetKey == null) { + return false; + } + + KSet visited = new KSet<>(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(rootKey); + + while (!queue.isEmpty()) { + String key = queue.poll(); + + if (!visited.add(key)) { + continue; + } + + if (key.equals(targetKey)) { + return true; + } + + IrisBiome biome = load(key); + + if (biome == null) { + continue; + } + + for (String child : biome.getChildren()) { + if (child != null && !visited.contains(child)) { + queue.add(child); + } + } + } + + return false; + } + + private boolean closurePlacesObject(String rootKey, String objectKey) { + if (rootKey == null || objectKey == null) { + return false; + } + + KSet visited = new KSet<>(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(rootKey); + + while (!queue.isEmpty()) { + String key = queue.poll(); + + if (!visited.add(key)) { + continue; + } + + IrisBiome biome = load(key); + + if (biome == null) { + continue; + } + + if (placesObject(biome.getSurfaceObjects(), objectKey)) { + return true; + } + + for (String child : biome.getChildren()) { + if (child != null && !visited.contains(child)) { + queue.add(child); + } + } + } + + return false; + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/Hotloadable.java b/core/src/main/java/art/arcane/iris/engine/framework/Hotloadable.java similarity index 95% rename from core/src/main/java/com/volmit/iris/engine/framework/Hotloadable.java rename to core/src/main/java/art/arcane/iris/engine/framework/Hotloadable.java index 7a6605090..f3aa7dfcc 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/Hotloadable.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Hotloadable.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; public interface Hotloadable { void hotload(); diff --git a/core/src/main/java/com/volmit/iris/engine/framework/IrisEngineMode.java b/core/src/main/java/art/arcane/iris/engine/framework/IrisEngineMode.java similarity index 94% rename from core/src/main/java/com/volmit/iris/engine/framework/IrisEngineMode.java rename to core/src/main/java/art/arcane/iris/engine/framework/IrisEngineMode.java index 0f10b8807..dd5aca0d2 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/IrisEngineMode.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/IrisEngineMode.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; -import com.volmit.iris.util.collection.KList; +import art.arcane.volmlib.util.collection.KList; public abstract class IrisEngineMode implements EngineMode { private final Engine engine; diff --git a/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java b/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java new file mode 100644 index 000000000..0a920b312 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java @@ -0,0 +1,1321 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.structure.StructureGraphCatalog; +import art.arcane.iris.engine.framework.structure.StructureGraphCompilation; +import art.arcane.iris.engine.framework.structure.StructureAssemblyResult; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisNativeStructure; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.IrisStructureAnchorMode; +import art.arcane.iris.engine.object.IrisStructureCarveShape; +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.engine.object.NativeStructureSuppression; +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.iris.engine.object.StructureDistribution; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Finds where Iris assembly and native jigsaw placements generate. A structure key matches + * an Iris structure load key, its {@code vanillaSource}, or a configured native registry key. + * + * A per-engine index of placed keys is cached so {@code /locate} and {@code /iris goto} skip + * the grid scan entirely for keys that are not placed (keeping them as fast as vanilla). + * + * For RANDOM_SPREAD placements (the common, vanilla-style spaced grid) the locator jumps + * straight to the single candidate chunk in each spacing-sized grid cell rather than testing + * every chunk -- roughly a {@code spacing^2} reduction in placement checks, matching vanilla + * locate performance. Density placements use a bounded per-chunk ring scan, while concentric + * placements enumerate their finite configured starts directly. + */ +public final class IrisStructureLocator { + private static final int CANDIDATE_BUDGET = 4_096; + private static final int MAX_BURIAL_COLUMNS = 2_000_000; + private static final int UNDERGROUND_SURFACE_CLEARANCE = 1; + private static final long CAVE_ANCHOR_DOMAIN = 0x43415645414E4348L; + private static final Pattern NAMESPACED_RESOURCE_KEY = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); + + private static final Cache INDEX_CACHE = Caffeine.newBuilder().weakKeys().build(); + private static final Cache LOCATABLE_INDEX_CACHE = + Caffeine.newBuilder().weakKeys().build(); + private static final PlacementIndex EMPTY_INDEX = new PlacementIndex( + Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), + Collections.emptySet(), Collections.emptyList()); + private static final LocatableIndex EMPTY_LOCATABLE_INDEX = new LocatableIndex( + Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), + Collections.emptySet(), Collections.emptySet(), Collections.emptySet()); + private static final LocateResult NOT_FOUND_RESULT = new LocateResult(LocateStatus.NOT_FOUND, 0, 0, 0); + private static final LocateResult SEARCH_LIMIT_RESULT = + new LocateResult(LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0); + + private IrisStructureLocator() { + } + + /** Structure keys referenced by any Iris assembly or native jigsaw placement. */ + public static Set placedKeys(Engine engine) { + if (engine == null) { + return Collections.emptySet(); + } + return index(engine).loadKeys; + } + + public static boolean isPlaced(Engine engine, String key) { + if (engine == null || key == null || key.isEmpty()) { + return false; + } + PlacementIndex placementIndex = index(engine); + String normalizedKey = normalize(key); + return placementIndex.normalizedLoadKeys.contains(normalizedKey) + || placementIndex.vanillaAliases.contains(normalizedKey); + } + + public static boolean hasLocatablePlacement(Engine engine, String key) { + if (engine == null || key == null || key.isBlank()) { + return false; + } + return locatableIndex(engine).normalizedKeys().contains(normalize(key)); + } + + public static boolean hasLocatableEditablePlacement(Engine engine, String key) { + if (engine == null || key == null || key.isBlank()) { + return false; + } + return locatableIndex(engine).normalizedEditableKeys().contains(normalize(key)); + } + + public static boolean hasLocatableNativePlacement(Engine engine, String key) { + if (engine == null || key == null || key.isBlank()) { + return false; + } + return locatableIndex(engine).normalizedNativeKeys().contains(normalize(key)); + } + + public static Set locatableKeys(Engine engine) { + return locatableIndex(engine).keys(); + } + + public static Set locatableEditableKeys(Engine engine) { + return locatableIndex(engine).editableKeys(); + } + + public static Set locatableNativeKeys(Engine engine) { + return locatableIndex(engine).nativeKeys(); + } + + public static boolean hasNativePlacement(Engine engine, String key) { + if (engine == null || key == null || key.isBlank()) { + return false; + } + return index(engine).nativeKeys.contains(normalize(key)); + } + + public static boolean hasEditablePlacement(Engine engine, String key) { + if (engine == null || key == null || key.isBlank()) { + return false; + } + for (IrisStructurePlacement placement : index(engine).placements) { + if (placement.hasIrisStructures() && matches(placement, key, engine.getData())) { + return true; + } + } + return false; + } + + public static boolean suppressesVanilla(Engine engine, String vanillaKey) { + if (engine == null || vanillaKey == null || vanillaKey.isEmpty()) { + return false; + } + return index(engine).suppressedVanillaSources.contains(normalize(vanillaKey)); + } + + public static void invalidate(Engine engine) { + if (engine != null) { + INDEX_CACHE.invalidate(engine); + LOCATABLE_INDEX_CACHE.invalidate(engine); + } + } + + public static boolean startsInChunk(Engine engine, String key, int cx, int cz) { + if (!isPlaced(engine, key)) { + return false; + } + return resolveInChunk(engine, key, cx, cz) != null; + } + + public static LocateResult locate(Engine engine, String key, int fromBlockX, int fromBlockZ, int maxRadiusChunks) { + return locate(engine, key, fromBlockX, fromBlockZ, maxRadiusChunks, (chunkX, chunkZ) -> true); + } + + public static LocateResult locate(Engine engine, String key, int fromBlockX, int fromBlockZ, + int maxRadiusChunks, CandidateFilter candidateFilter) { + try { + return locateFiltered(engine, key, fromBlockX, fromBlockZ, maxRadiusChunks, candidateFilter); + } catch (CandidateSearchLimitException ignored) { + return SEARCH_LIMIT_RESULT; + } + } + + public static LocateResult locateInPlacementRings( + Engine engine, String key, int fromBlockX, int fromBlockZ, + int maxSearchRadius, CandidateFilter candidateFilter) { + try { + return locateInPlacementRingsFiltered( + engine, key, fromBlockX, fromBlockZ, maxSearchRadius, candidateFilter); + } catch (CandidateSearchLimitException ignored) { + return SEARCH_LIMIT_RESULT; + } + } + + private static LocateResult locateInPlacementRingsFiltered( + Engine engine, String key, int fromBlockX, int fromBlockZ, + int maxSearchRadius, CandidateFilter candidateFilter) { + if (!isPlaced(engine, key)) { + return NOT_FOUND_RESULT; + } + CandidateFilter activeFilter = Objects.requireNonNull( + candidateFilter, "Structure locate filter must not be null"); + int maximumRing = Math.max(0, Math.min(maxSearchRadius, 2048)); + int centerChunkX = fromBlockX >> 4; + int centerChunkZ = fromBlockZ >> 4; + PlacementCatalog catalog = collectPlacementCatalog(engine, key); + if (catalog.randomSpread().isEmpty() + && catalog.concentricRings().isEmpty() && !catalog.hasDensity()) { + return NOT_FOUND_RESULT; + } + SeedManager seedManager = engine.getSeedManager(); + if (seedManager == null) { + throw new IllegalStateException( + "Iris structure locate requires a bound seed manager for '" + key + "'"); + } + long seed = seedManager.getMantle(); + CandidateBudget candidateBudget = new CandidateBudget(); + LocatedCandidate best = nearestConcentricCandidate( + engine, key, fromBlockX, fromBlockZ, seed, catalog, activeFilter, candidateBudget); + + for (int ring = 0; ring <= maximumRing; ring++) { + boolean foundInRing = false; + for (RandomSpreadParameters parameters : catalog.randomSpread()) { + LocatedCandidate candidate = firstRandomSpreadCandidateInRing( + engine, key, fromBlockX, fromBlockZ, + centerChunkX, centerChunkZ, ring, seed, + parameters, activeFilter, candidateBudget); + if (candidate == null) { + continue; + } + foundInRing = true; + if (best == null || candidate.distanceSquared() < best.distanceSquared()) { + best = candidate; + } + } + if (catalog.hasDensity()) { + LocatedCandidate candidate = firstDensityCandidateInRing( + engine, key, fromBlockX, fromBlockZ, + centerChunkX, centerChunkZ, ring, + activeFilter, candidateBudget); + if (candidate != null) { + foundInRing = true; + if (best == null || candidate.distanceSquared() < best.distanceSquared()) { + best = candidate; + } + } + } + if (foundInRing) { + return found(best); + } + } + return best == null ? NOT_FOUND_RESULT : found(best); + } + + private static LocatedCandidate nearestConcentricCandidate( + Engine engine, String key, int fromBlockX, int fromBlockZ, + long seed, PlacementCatalog catalog, CandidateFilter candidateFilter, + CandidateBudget candidateBudget) { + LocatedCandidate best = null; + Set checkedChunks = new LinkedHashSet<>(); + for (IrisStructurePlacement placement : catalog.concentricRings()) { + int count = Math.max(1, placement.getRingCount()); + for (int placementIndex = 0; placementIndex < count; placementIndex++) { + candidateBudget.claim(); + int[] candidate = StructurePlacementGrid.concentricRingChunk( + placement, placementIndex, seed); + if (candidate == null || !checkedChunks.add(chunkKey(candidate[0], candidate[1]))) { + continue; + } + ResolvedStart resolved = resolveInChunk(engine, key, candidate[0], candidate[1]); + if (resolved == null || !candidateFilter.accept(candidate[0], candidate[1])) { + continue; + } + long distanceSquared = blockDistanceSquared( + resolved, fromBlockX, fromBlockZ); + if (best == null || distanceSquared < best.distanceSquared()) { + best = new LocatedCandidate(resolved, distanceSquared); + } + } + } + return best; + } + + private static LocatedCandidate firstRandomSpreadCandidateInRing( + Engine engine, String key, int fromBlockX, int fromBlockZ, + int centerChunkX, int centerChunkZ, int ring, long seed, + RandomSpreadParameters parameters, CandidateFilter candidateFilter, + CandidateBudget candidateBudget) { + int spacing = Math.max(1, parameters.spacing()); + int centerCellX = Math.floorDiv(centerChunkX, spacing); + int centerCellZ = Math.floorDiv(centerChunkZ, spacing); + for (int offsetX = -ring; offsetX <= ring; offsetX++) { + boolean edgeX = offsetX == -ring || offsetX == ring; + for (int offsetZ = -ring; offsetZ <= ring; offsetZ++) { + boolean edgeZ = offsetZ == -ring || offsetZ == ring; + if (!edgeX && !edgeZ) { + continue; + } + candidateBudget.claim(); + int[] candidate = StructurePlacementGrid.randomSpreadCellChunk( + centerCellX + offsetX, centerCellZ + offsetZ, + spacing, parameters.separation(), parameters.salt(), seed); + ResolvedStart resolved = resolveInChunk(engine, key, candidate[0], candidate[1]); + if (resolved != null && candidateFilter.accept(candidate[0], candidate[1])) { + return new LocatedCandidate( + resolved, blockDistanceSquared(resolved, fromBlockX, fromBlockZ)); + } + } + } + return null; + } + + private static LocatedCandidate firstDensityCandidateInRing( + Engine engine, String key, int fromBlockX, int fromBlockZ, + int centerChunkX, int centerChunkZ, int ring, + CandidateFilter candidateFilter, CandidateBudget candidateBudget) { + for (int offsetX = -ring; offsetX <= ring; offsetX++) { + boolean edgeX = offsetX == -ring || offsetX == ring; + for (int offsetZ = -ring; offsetZ <= ring; offsetZ++) { + boolean edgeZ = offsetZ == -ring || offsetZ == ring; + if (!edgeX && !edgeZ) { + continue; + } + int chunkX = centerChunkX + offsetX; + int chunkZ = centerChunkZ + offsetZ; + candidateBudget.claim(); + ResolvedStart resolved = resolveInChunk(engine, key, chunkX, chunkZ); + if (resolved != null && candidateFilter.accept(chunkX, chunkZ)) { + return new LocatedCandidate( + resolved, blockDistanceSquared(resolved, fromBlockX, fromBlockZ)); + } + } + } + return null; + } + + private static LocateResult locateFiltered(Engine engine, String key, int fromBlockX, int fromBlockZ, + int maxRadiusChunks, CandidateFilter candidateFilter) { + if (!isPlaced(engine, key)) { + return NOT_FOUND_RESULT; + } + CandidateFilter activeFilter = Objects.requireNonNull(candidateFilter, "Structure locate filter must not be null"); + int max = Math.max(0, Math.min(maxRadiusChunks, 2048)); + int pcx = fromBlockX >> 4; + int pcz = fromBlockZ >> 4; + LocatedCandidate best = null; + PlacementCatalog catalog = collectPlacementCatalog(engine, key); + if (catalog.randomSpread().isEmpty() && catalog.concentricRings().isEmpty() && !catalog.hasDensity()) { + return NOT_FOUND_RESULT; + } + SeedManager seedManager = engine.getSeedManager(); + if (seedManager == null) { + throw new IllegalStateException("Iris structure locate requires a bound seed manager for '" + key + "'"); + } + long seed = seedManager.getMantle(); + CandidateBudget candidateBudget = new CandidateBudget(); + + for (RandomSpreadParameters parameters : catalog.randomSpread) { + int spacing = Math.max(1, parameters.spacing()); + int centerCellX = Math.floorDiv(pcx, spacing); + int centerCellZ = Math.floorDiv(pcz, spacing); + int cellRadius = (max / spacing) + 2; + + for (int r = 0; r <= cellRadius; r++) { + long chunkLowerBound = cellRingDistanceLowerBound( + r, spacing, pcx, pcz, centerCellX, centerCellZ); + long blockLowerBound = chunkDeltaBlockDistanceLowerBound(chunkLowerBound); + if (best != null && squareSaturated(blockLowerBound) >= best.distanceSquared()) { + break; + } + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + if (Math.max(Math.abs(dx), Math.abs(dz)) != r) { + continue; + } + candidateBudget.claim(); + int[] candidate = StructurePlacementGrid.randomSpreadCellChunk( + centerCellX + dx, centerCellZ + dz, spacing, parameters.separation(), parameters.salt(), seed); + int cx = candidate[0]; + int cz = candidate[1]; + if (!withinRadius(cx, cz, pcx, pcz, max) + || best != null && chunkBlockDistanceSquaredLowerBound( + cx, cz, fromBlockX, fromBlockZ) >= best.distanceSquared()) { + continue; + } + ResolvedStart resolved = resolveInChunk(engine, key, cx, cz); + if (resolved != null && activeFilter.accept(cx, cz)) { + long distanceSquared = blockDistanceSquared( + resolved, fromBlockX, fromBlockZ); + if (best == null || distanceSquared < best.distanceSquared()) { + best = new LocatedCandidate(resolved, distanceSquared); + } + } + } + } + } + } + + Set checkedRingChunks = new LinkedHashSet<>(); + for (IrisStructurePlacement placement : catalog.concentricRings) { + int count = Math.max(1, placement.getRingCount()); + for (int placementIndex = 0; placementIndex < count; placementIndex++) { + candidateBudget.claim(); + int[] candidate = StructurePlacementGrid.concentricRingChunk(placement, placementIndex, seed); + if (candidate == null || !checkedRingChunks.add(chunkKey(candidate[0], candidate[1]))) { + continue; + } + if (!withinRadius(candidate[0], candidate[1], pcx, pcz, max) + || best != null && chunkBlockDistanceSquaredLowerBound( + candidate[0], candidate[1], fromBlockX, fromBlockZ) >= best.distanceSquared()) { + continue; + } + ResolvedStart resolved = resolveInChunk(engine, key, candidate[0], candidate[1]); + if (resolved != null && activeFilter.accept(candidate[0], candidate[1])) { + long distanceSquared = blockDistanceSquared( + resolved, fromBlockX, fromBlockZ); + if (best == null || distanceSquared < best.distanceSquared()) { + best = new LocatedCandidate(resolved, distanceSquared); + } + } + } + } + + if (catalog.hasDensity) { + for (int r = 0; r <= max; r++) { + long ringBlockLowerBound = chunkDeltaBlockDistanceLowerBound(r); + if (best != null && squareSaturated(ringBlockLowerBound) >= best.distanceSquared()) { + break; + } + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + if (Math.max(Math.abs(dx), Math.abs(dz)) != r) { + continue; + } + int cx = pcx + dx; + int cz = pcz + dz; + if (best != null && chunkBlockDistanceSquaredLowerBound( + cx, cz, fromBlockX, fromBlockZ) >= best.distanceSquared()) { + continue; + } + candidateBudget.claim(); + ResolvedStart resolved = resolveInChunk(engine, key, cx, cz); + if (resolved != null && activeFilter.accept(cx, cz)) { + long distanceSquared = blockDistanceSquared( + resolved, fromBlockX, fromBlockZ); + if (best == null || distanceSquared < best.distanceSquared()) { + best = new LocatedCandidate(resolved, distanceSquared); + } + } + } + } + } + } + + if (best == null) { + return NOT_FOUND_RESULT; + } + return found(best); + } + + static boolean withinRadius(int chunkX, int chunkZ, + int centerChunkX, int centerChunkZ, int radius) { + return Math.abs((long) chunkX - centerChunkX) <= radius + && Math.abs((long) chunkZ - centerChunkZ) <= radius; + } + + public static ResolvedPlacement resolvePlacement(Engine engine, IrisStructurePlacement placement, int cx, int cz) { + if (engine == null || engine.getData() == null || engine.getSeedManager() == null) { + throw new IllegalStateException("Iris structure placement requires a fully bound engine"); + } + NativeStructurePlacementPlanner.validateBackend(placement); + if (!placement.hasIrisStructures() || placement.getDistribution() == null) { + throw new IllegalStateException("Iris structure placement is missing its distribution or structure list"); + } + long seed = engine.getSeedManager().getMantle(); + if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed)) { + return null; + } + + RNG rng = StructurePlacementGrid.placementRng(placement, cx, cz, seed); + int originX = (cx << 4) + rng.nextInt(16); + int originZ = (cz << 4) + rng.nextInt(16); + PlacementAnchor anchor = resolveAnchor(engine, placement, cx, cz, originX, originZ, rng); + if (anchor == null) { + return null; + } + originX = anchor.x(); + originZ = anchor.z(); + int baseY = anchor.y(); + + String selectedKey = selectStructureKey(placement, rng); + if (selectedKey == null || selectedKey.isBlank()) { + throw new IllegalStateException("Iris structure placement selected a blank structure key"); + } + IrisStructure structure = engine.getData().load(IrisStructure.class, selectedKey, false); + if (structure == null) { + throw new IllegalStateException("Iris structure placement references missing structure '" + + selectedKey + "'"); + } + StructureGraphCompilation compilation = StructureGraphCatalog.compile(engine.getData(), structure); + if (!compilation.isAssemblyViable()) { + throw new IllegalStateException("Iris structure '" + selectedKey + + "' has no runtime-viable assembly graph"); + } + + StructureAssembler assembler = StructureAssembler.forData( + engine.getData(), structure, new IrisPosition(originX, baseY, originZ)); + StructureAssemblyResult assembly = assembler.assemble(rng); + if (!requirePlacementOutput(placement, selectedKey, cx, cz, + assembly.hasOutput(), "runtime assembly produced " + assembly.status() + ": " + assembly.detail())) { + return null; + } + KList pieces = new KList<>(assembly.pieces()); + pieces = alignCavePieces(pieces, placement, baseY); + pieces = alignSurfacePieces(pieces, placement, structure, baseY); + if (!requirePlacementOutput(placement, selectedKey, cx, cz, + pieces != null && !pieces.isEmpty(), "surface alignment produced no pieces")) { + return null; + } + boolean exactY = hasExactY(placement, structure, pieces); + + int worldMin = engine.getMinHeight() + 1; + int worldMax = engine.getMinHeight() + engine.getHeight() - 1; + if (exactY) { + Integer verticalShift = resolveVerticalShift(pieces, placement, baseY, worldMin, worldMax); + if (!requirePlacementOutput(placement, selectedKey, cx, cz, verticalShift != null, + "assembled pieces cannot fit the configured vertical and world bounds")) { + return null; + } + if (verticalShift != 0) { + pieces = shiftPieces(pieces, verticalShift); + baseY += verticalShift; + } + } + if (placement.isAnchoredUnderground() && !placement.resolvedAnchor().isCave()) { + Integer burialShift = resolveUndergroundBurialShift( + engine, pieces, placement, baseY, worldMin, worldMax); + if (!requirePlacementOutput(placement, selectedKey, cx, cz, burialShift != null, + "assembled pieces and carving envelope cannot remain fully buried beneath terrain")) { + return null; + } + if (burialShift != 0) { + pieces = shiftPieces(pieces, burialShift); + baseY += burialShift; + } + } + + long configuredRadius = (long) Math.max(1, structure.getMaxSizeChunks()) * 16L; + int structureRadius = (int) Math.min(Integer.MAX_VALUE, configuredRadius); + boolean withinBounds = fitsHorizontalBounds(pieces, originX, originZ, structureRadius) + && (!exactY || fitsVerticalBounds(pieces, worldMin, worldMax)); + if (!requirePlacementOutput(placement, selectedKey, cx, cz, withinBounds, + "assembled pieces exceed the configured structure or world bounds")) { + return null; + } + return new ResolvedPlacement(placement, selectedKey, structure, pieces, rng, originX, baseY, originZ, exactY); + } + + public static boolean requirePlacementOutput(IrisStructurePlacement placement, String structureKey, + int chunkX, int chunkZ, boolean outputPresent, + String failureReason) { + if (outputPresent) { + return true; + } + if (placement == null + || NativeStructureSuppression.REPLACE_SOURCE != placement.getNativeSuppression()) { + return false; + } + String key = structureKey == null || structureKey.isBlank() + ? String.valueOf(placement.getStructures()) : structureKey; + String reason = failureReason == null || failureReason.isBlank() + ? "placement produced no output" : failureReason; + throw new IllegalStateException("REPLACE_SOURCE placement for Iris structure '" + key + + "' failed in chunk " + chunkX + "," + chunkZ + ": " + reason + + ". Native generation is suppressed and will not be used as a fallback"); + } + + static boolean fitsWorldBounds(KList pieces, int worldMin, int worldMax, + int originX, int originZ, int structureRadius) { + int[] bounds = computeBounds(pieces); + if (bounds == null) { + return false; + } + return fitsVerticalBounds(bounds, worldMin, worldMax) + && fitsHorizontalBounds(bounds, originX, originZ, structureRadius); + } + + private static ResolvedStart resolveInChunk(Engine engine, String key, int cx, int cz) { + IrisData data = engine.getData(); + String normalizedKey = normalize(key); + if (hasNativePlacement(engine, key)) { + for (NativeStructureStartPlan plan : NativeStructurePlacementPlanner.plansAt(engine, cx, cz)) { + if (normalize(plan.source().getStructure()).equals(normalizedKey)) { + return new ResolvedStart(cx << 4, plan.baseY(), cz << 4); + } + } + } + KList placements = StructurePlacementScope.placementsAt(engine, cx, cz); + for (IrisStructurePlacement placement : placements) { + if (!matches(placement, key, data)) { + continue; + } + if (placement.hasNativeStructures()) { + continue; + } + ResolvedPlacement resolved = resolvePlacement(engine, placement, cx, cz); + if (resolved != null && matchesResolved(resolved, key)) { + return new ResolvedStart( + resolved.originX(), resolved.baseY(), resolved.originZ()); + } + } + return null; + } + + private static PlacementAnchor resolveAnchor( + Engine engine, + IrisStructurePlacement placement, + int chunkX, + int chunkZ, + int originX, + int originZ, + RNG rng + ) { + if (placement.resolvedAnchor().isCave()) { + StructureCaveAnchorResolver.Anchor caveAnchor = StructureCaveAnchorResolver.resolve( + engine, placement, chunkX, chunkZ, rng.nextParallelRNG(CAVE_ANCHOR_DOMAIN)); + return caveAnchor == null ? null : new PlacementAnchor(caveAnchor.x(), caveAnchor.y(), caveAnchor.z()); + } + Integer baseY = resolveBaseY(engine, placement, originX, originZ, rng); + return baseY == null ? null : new PlacementAnchor(originX, baseY, originZ); + } + + private static Integer resolveBaseY(Engine engine, IrisStructurePlacement placement, int originX, int originZ, RNG rng) { + int worldMin = engine.getMinHeight() + 1; + int worldMax = engine.getMinHeight() + engine.getHeight() - 1; + if (worldMin > worldMax) { + return null; + } + if (!placement.isUnderwater() + && NativeStructurePlacementPlanner.isSubmerged(engine, originX, originZ)) { + return null; + } + if (placement.resolvedAnchor() == IrisStructureAnchorMode.SURFACE) { + int surfaceY = engine.getHeight(originX, originZ, true) + engine.getMinHeight(); + return surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight() ? null : surfaceY; + } + int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight())); + int bandMax = Math.min(worldMax, Math.max(placement.getMinHeight(), placement.getMaxHeight())); + if (bandMin > bandMax) { + return null; + } + return randomInclusive(rng, bandMin, bandMax); + } + + static Integer resolveVerticalShift(KList pieces, IrisStructurePlacement placement, + int baseY, int worldMin, int worldMax) { + int[] bounds = computeBounds(pieces); + if (bounds == null) { + return null; + } + if (!placement.isAnchoredUnderground() || placement.resolvedAnchor().isCave()) { + return bounds[1] >= worldMin && bounds[4] <= worldMax ? 0 : null; + } + int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight())); + int bandMax = Math.min(worldMax, Math.max(placement.getMinHeight(), placement.getMaxHeight())); + int minimumShift = Math.max(worldMin - bounds[1], bandMin - baseY); + int maximumShift = Math.min(worldMax - bounds[4], bandMax - baseY); + if (minimumShift > maximumShift) { + return null; + } + if (minimumShift > 0) { + return minimumShift; + } + if (maximumShift < 0) { + return maximumShift; + } + return 0; + } + + static Integer resolveUndergroundBurialShift(Engine engine, KList pieces, + IrisStructurePlacement placement, int baseY, + int worldMin, int worldMax) { + int[] bounds = computeBounds(pieces); + if (engine == null || bounds == null || placement == null || !placement.isAnchoredUnderground() + || placement.resolvedAnchor().isCave()) { + return null; + } + + IrisStructureTerrain terrain = placement.resolvedTerrain(); + IrisStructureTerrainMode terrainMode = terrain.resolvedMode(); + boolean forceCarve = terrainMode == IrisStructureTerrainMode.FORCE_CARVE; + boolean bore = terrainMode == IrisStructureTerrainMode.BORE; + int sideExtension = forceCarve || bore ? Math.max(0, terrain.getHorizontalPadding()) : 0; + IrisStructureCarveShape carveShape = terrain.resolvedShape(); + int topExtension = forceCarve + ? carveShape.maximumCeilingExtension( + terrain.getCeilingPadding(), terrain.resolvedErosionStrength()) + : bore ? Math.max(0, terrain.getCeilingPadding()) : 0; + int bottomExtension = forceCarve || bore ? Math.max(0, terrain.getFloorPadding()) : 0; + int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight())); + int bandMax = Math.min(worldMax, Math.max(placement.getMinHeight(), placement.getMaxHeight())); + int minimumShift = Math.max(worldMin - (bounds[1] - bottomExtension), bandMin - baseY); + int maximumShift = Math.min(0, Math.min(worldMax - (bounds[4] + topExtension), bandMax - baseY)); + if (minimumShift > maximumShift) { + return null; + } + + Long2IntOpenHashMap surfaceHeights = new Long2IntOpenHashMap(); + surfaceHeights.defaultReturnValue(Integer.MIN_VALUE); + if (bore || forceCarve && carveShape == IrisStructureCarveShape.BOX) { + maximumShift = resolveBurialEnvelopeShift( + engine, + bounds[0] - sideExtension, + bounds[3] + sideExtension, + bounds[2] - sideExtension, + bounds[5] + sideExtension, + bounds[4] + topExtension, + maximumShift, + surfaceHeights + ); + if (maximumShift == Integer.MIN_VALUE) { + return null; + } + } else { + for (PlacedStructurePiece piece : pieces) { + maximumShift = resolveBurialEnvelopeShift( + engine, + piece.getMinX() - sideExtension, + piece.getMaxX() + sideExtension, + piece.getMinZ() - sideExtension, + piece.getMaxZ() + sideExtension, + piece.getMaxY() + topExtension, + maximumShift, + surfaceHeights + ); + if (maximumShift == Integer.MIN_VALUE) { + return null; + } + } + } + return minimumShift > maximumShift ? null : maximumShift; + } + + private static int resolveBurialEnvelopeShift(Engine engine, int minX, int maxX, int minZ, int maxZ, + int envelopeTopY, int maximumShift, + Long2IntOpenHashMap surfaceHeights) { + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + long columnKey = ((long) x << 32) ^ (z & 0xffffffffL); + int surfaceY = surfaceHeights.get(columnKey); + if (surfaceY == Integer.MIN_VALUE) { + if (surfaceHeights.size() >= MAX_BURIAL_COLUMNS) { + return Integer.MIN_VALUE; + } + surfaceY = engine.getHeight(x, z, true) + engine.getMinHeight(); + surfaceHeights.put(columnKey, surfaceY); + } + int allowedTopY = surfaceY - UNDERGROUND_SURFACE_CLEARANCE; + maximumShift = Math.min(maximumShift, allowedTopY - envelopeTopY); + } + } + return maximumShift; + } + + static String selectStructureKey(IrisStructurePlacement placement, RNG rng) { + if (placement == null || placement.getStructures() == null || placement.getStructures().isEmpty()) { + return null; + } + return placement.getStructures().get(rng.nextInt(placement.getStructures().size())); + } + + static KList alignCavePieces( + KList pieces, + IrisStructurePlacement placement, + int anchorY + ) { + IrisStructureAnchorMode mode = placement.resolvedAnchor(); + if (!mode.isCave()) { + return pieces; + } + int[] bounds = computeBounds(pieces); + if (bounds == null) { + return pieces; + } + int targetY = switch (mode) { + case CAVE_FLOOR -> bounds[1]; + case CAVE_CEILING -> bounds[4]; + case CAVE_CENTER, CAVE_ANY -> bounds[1] + Math.floorDiv(bounds[4] - bounds[1], 2); + default -> anchorY; + }; + return shiftPieces(pieces, anchorY - targetY); + } + + private static KList alignSurfacePieces(KList pieces, + IrisStructurePlacement placement, + IrisStructure structure, int baseY) { + if (placement.isAnchoredUnderground() || pieces.size() <= 1 + || structure.getPlaceMode() == ObjectPlaceMode.STRUCTURE_PIECE + || structure.getPlaceMode() == ObjectPlaceMode.FLOATING) { + return pieces; + } + int[] bounds = computeBounds(pieces); + return bounds == null ? pieces : shiftPieces(pieces, baseY - bounds[1]); + } + + static boolean hasExactY(IrisStructurePlacement placement, IrisStructure structure, + KList pieces) { + return placement.isAnchoredUnderground() || pieces.size() > 1 + || structure.getPlaceMode() == ObjectPlaceMode.STRUCTURE_PIECE + || structure.getPlaceMode() == ObjectPlaceMode.FLOATING; + } + + private static boolean fitsVerticalBounds(KList pieces, int worldMin, int worldMax) { + int[] bounds = computeBounds(pieces); + return bounds != null && fitsVerticalBounds(bounds, worldMin, worldMax); + } + + private static boolean fitsVerticalBounds(int[] bounds, int worldMin, int worldMax) { + return bounds[1] >= worldMin && bounds[4] <= worldMax; + } + + private static boolean fitsHorizontalBounds(KList pieces, int originX, int originZ, + int structureRadius) { + int[] bounds = computeBounds(pieces); + return bounds != null && fitsHorizontalBounds(bounds, originX, originZ, structureRadius); + } + + private static boolean fitsHorizontalBounds(int[] bounds, int originX, int originZ, int structureRadius) { + return bounds[0] >= originX - structureRadius && bounds[3] <= originX + structureRadius + && bounds[2] >= originZ - structureRadius && bounds[5] <= originZ + structureRadius; + } + + private static KList shiftPieces(KList pieces, int shiftY) { + KList shifted = new KList<>(); + for (PlacedStructurePiece piece : pieces) { + shifted.add(new PlacedStructurePiece( + piece.getPiece(), piece.getObject(), piece.getX(), piece.getY() + shiftY, piece.getZ(), piece.getRotation(), + piece.getMinX(), piece.getMinY() + shiftY, piece.getMinZ(), + piece.getMaxX(), piece.getMaxY() + shiftY, piece.getMaxZ())); + } + return shifted; + } + + private static int[] computeBounds(KList pieces) { + if (pieces == null || pieces.isEmpty()) { + return null; + } + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (PlacedStructurePiece piece : pieces) { + minX = Math.min(minX, piece.getMinX()); + minY = Math.min(minY, piece.getMinY()); + minZ = Math.min(minZ, piece.getMinZ()); + maxX = Math.max(maxX, piece.getMaxX()); + maxY = Math.max(maxY, piece.getMaxY()); + maxZ = Math.max(maxZ, piece.getMaxZ()); + } + return new int[]{minX, minY, minZ, maxX, maxY, maxZ}; + } + + private static int randomInclusive(RNG rng, int minimum, int maximum) { + if (minimum == maximum) { + return minimum; + } + return minimum + rng.nextInt((maximum - minimum) + 1); + } + + private static PlacementCatalog collectPlacementCatalog(Engine engine, String key) { + Set randomSpread = new LinkedHashSet<>(); + List concentricRings = new ArrayList<>(); + boolean hasDensity = false; + for (IrisStructurePlacement placement : index(engine).placements) { + if (!isSearchablePlacement(engine, placement) + || !matches(placement, key, engine.getData())) { + continue; + } + if (placement.getDistribution() == StructureDistribution.RANDOM_SPREAD) { + randomSpread.add(new RandomSpreadParameters( + Math.max(1, placement.getSpacing()), placement.getSeparation(), + StructurePlacementGrid.placementSalt(placement))); + } else if (placement.getDistribution() == StructureDistribution.CONCENTRIC_RINGS) { + concentricRings.add(placement); + } else if (placement.getDistribution() == StructureDistribution.DENSITY) { + hasDensity = true; + } + } + return new PlacementCatalog(List.copyOf(randomSpread), List.copyOf(concentricRings), hasDensity); + } + + static boolean isSearchableDensityPlacement(Engine engine, IrisStructurePlacement placement) { + return placement != null + && placement.getDistribution() == StructureDistribution.DENSITY + && isSearchablePlacement(engine, placement); + } + + static boolean isSearchablePlacement(Engine engine, IrisStructurePlacement placement) { + if (engine == null || placement == null || placement.getDistribution() == null + || engine.getHeight() <= 0) { + return false; + } + if (placement.getDistribution() == StructureDistribution.DENSITY + && !(placement.getDensity() > 0.0)) { + return false; + } + long worldMin = (long) engine.getMinHeight() + (placement.isAnchoredUnderground() ? 1L : 0L); + long worldMax = (long) engine.getMinHeight() + engine.getHeight() + - (placement.resolvedAnchor().isCave() ? 2L : 1L); + if (worldMin > worldMax) { + return false; + } + long configuredMin = placement.isAnchoredUnderground() + ? Math.min(placement.getMinHeight(), placement.getMaxHeight()) + : placement.getMinHeight(); + long configuredMax = placement.isAnchoredUnderground() + ? Math.max(placement.getMinHeight(), placement.getMaxHeight()) + : placement.getMaxHeight(); + return configuredMin <= configuredMax && configuredMin <= worldMax && configuredMax >= worldMin; + } + + private static long blockDistanceSquared( + ResolvedStart resolved, int fromBlockX, int fromBlockZ) { + long dx = (long) resolved.originX() - fromBlockX; + long dz = (long) resolved.originZ() - fromBlockZ; + return distanceSquaredSaturated(dx, dz); + } + + static long chunkBlockDistanceSquaredLowerBound( + int chunkX, int chunkZ, int fromBlockX, int fromBlockZ) { + long minimumX = (long) chunkX << 4; + long minimumZ = (long) chunkZ << 4; + long dx = axisDistance(fromBlockX, minimumX, minimumX + 15L); + long dz = axisDistance(fromBlockZ, minimumZ, minimumZ + 15L); + return distanceSquaredSaturated(dx, dz); + } + + private static long axisDistance(long point, long minimum, long maximum) { + if (point < minimum) { + return minimum - point; + } + if (point > maximum) { + return point - maximum; + } + return 0L; + } + + private static long chunkDeltaBlockDistanceLowerBound(long chunkDelta) { + return chunkDelta <= 0L ? 0L : chunkDelta * 16L - 15L; + } + + private static long distanceSquaredSaturated(long dx, long dz) { + long xSquared = squareSaturated(dx); + long zSquared = squareSaturated(dz); + if (Long.MAX_VALUE - xSquared < zSquared) { + return Long.MAX_VALUE; + } + return xSquared + zSquared; + } + + private static long squareSaturated(long value) { + long absolute = Math.abs(value); + if (absolute > 3_037_000_499L) { + return Long.MAX_VALUE; + } + return absolute * absolute; + } + + private static LocateResult found(LocatedCandidate candidate) { + ResolvedStart resolved = candidate.resolved(); + return new LocateResult(LocateStatus.FOUND, resolved.originX(), resolved.baseY(), resolved.originZ()); + } + + static long cellRingDistanceLowerBound(int ring, int spacing, int pcx, int pcz, + int centerCellX, int centerCellZ) { + if (ring <= 0) { + return 0L; + } + int localX = pcx - (centerCellX * spacing); + int localZ = pcz - (centerCellZ * spacing); + long ringOffset = (long) ring * spacing; + long positiveX = ringOffset - localX; + long negativeX = ringOffset + localX - (spacing - 1L); + long positiveZ = ringOffset - localZ; + long negativeZ = ringOffset + localZ - (spacing - 1L); + return Math.min(Math.min(positiveX, negativeX), Math.min(positiveZ, negativeZ)); + } + + private static long chunkKey(int cx, int cz) { + return ((long) cx << 32) ^ (cz & 0xffffffffL); + } + + private record PlacementAnchor(int x, int y, int z) { + } + + private static boolean matches(IrisStructurePlacement placement, String key, IrisData data) { + if (placement == null || key == null || data == null) { + return false; + } + String normalizedKey = normalize(key); + if (placement.getNativeStructures() != null) { + for (IrisNativeStructure source : placement.getNativeStructures()) { + if (source != null && normalize(source.getStructure()).equals(normalizedKey)) { + return true; + } + } + } + if (placement.getStructures() == null) { + return false; + } + for (String structureKey : placement.getStructures()) { + if (structureKey == null || structureKey.isBlank()) { + continue; + } + IrisStructure structure = data.load(IrisStructure.class, structureKey, false); + if (structure == null) { + continue; + } + if (normalize(structureKey).equals(normalizedKey) + || normalize(structure.getLoadKey()).equals(normalizedKey) + || normalize(structure.getVanillaSource()).equals(normalizedKey)) { + return true; + } + } + return false; + } + + static boolean matchesResolved(ResolvedPlacement resolved, String key) { + String normalizedKey = normalize(key); + return normalize(resolved.structureKey()).equals(normalizedKey) + || normalize(resolved.structure().getLoadKey()).equals(normalizedKey) + || normalize(resolved.structure().getVanillaSource()).equals(normalizedKey); + } + + private static String normalize(String key) { + return key == null ? "" : key.toLowerCase(Locale.ROOT); + } + + private static PlacementIndex index(Engine engine) { + if (engine == null) { + return EMPTY_INDEX; + } + if (engine.getData() == null || engine.getDimension() == null) { + throw new IllegalStateException("Iris structure index requires a fully bound engine and dimension"); + } + return INDEX_CACHE.get(engine, ignored -> build(engine)); + } + + private static LocatableIndex locatableIndex(Engine engine) { + if (engine == null) { + return EMPTY_LOCATABLE_INDEX; + } + return LOCATABLE_INDEX_CACHE.get(engine, ignored -> buildLocatableIndex(engine)); + } + + private static LocatableIndex buildLocatableIndex(Engine engine) { + IrisData data = engine.getData(); + Set keys = new LinkedHashSet<>(); + Set normalizedKeys = new LinkedHashSet<>(); + Set editableKeys = new LinkedHashSet<>(); + Set normalizedEditableKeys = new LinkedHashSet<>(); + Set nativeKeys = new LinkedHashSet<>(); + Set normalizedNativeKeys = new LinkedHashSet<>(); + for (IrisStructurePlacement placement : index(engine).placements) { + if (!isSearchablePlacement(engine, placement)) { + continue; + } + if (placement.hasNativeStructures()) { + for (IrisNativeStructure source : placement.getNativeStructures()) { + addLocatableKey(source.getStructure(), keys, normalizedKeys); + addLocatableKey(source.getStructure(), nativeKeys, normalizedNativeKeys); + } + continue; + } + for (String structureKey : placement.getStructures()) { + IrisStructure structure = data.load(IrisStructure.class, structureKey, false); + if (structure == null) { + throw new IllegalStateException( + "Iris structure placement references missing structure '" + structureKey + "'"); + } + addLocatableKey(structureKey, keys, normalizedKeys); + addLocatableKey(structureKey, editableKeys, normalizedEditableKeys); + addLocatableKey(structure.getLoadKey(), keys, normalizedKeys); + addLocatableKey(structure.getLoadKey(), editableKeys, normalizedEditableKeys); + addLocatableKey(structure.getVanillaSource(), keys, normalizedKeys); + addLocatableKey(structure.getVanillaSource(), editableKeys, normalizedEditableKeys); + } + } + return new LocatableIndex( + Collections.unmodifiableSet(keys), + Collections.unmodifiableSet(normalizedKeys), + Collections.unmodifiableSet(editableKeys), + Collections.unmodifiableSet(normalizedEditableKeys), + Collections.unmodifiableSet(nativeKeys), + Collections.unmodifiableSet(normalizedNativeKeys)); + } + + private static void addLocatableKey(String key, Set keys, Set normalizedKeys) { + if (key == null || key.isBlank()) { + return; + } + String trimmedKey = key.trim(); + keys.add(trimmedKey); + normalizedKeys.add(normalize(trimmedKey)); + } + + private static PlacementIndex build(Engine engine) { + IrisData data = engine.getData(); + Set loadKeys = new LinkedHashSet<>(); + Set normalizedLoadKeys = new LinkedHashSet<>(); + Set vanillaAliases = new LinkedHashSet<>(); + Set suppressedVanillaSources = new LinkedHashSet<>(); + List placements = new ArrayList<>(); + collect(engine.getDimension().getStructures(), data, loadKeys, normalizedLoadKeys, vanillaAliases, + suppressedVanillaSources, placements, true); + KList regions = engine.getDimension().getAllRegions(engine); + if (regions != null) { + for (IrisRegion region : regions) { + collect(region == null ? null : region.getStructures(), data, + loadKeys, normalizedLoadKeys, vanillaAliases, + suppressedVanillaSources, placements, false); + } + } + KList biomes = engine.getDimension().getReachableBiomes(engine); + if (biomes != null) { + for (IrisBiome biome : biomes) { + collect(biome == null ? null : biome.getStructures(), data, + loadKeys, normalizedLoadKeys, vanillaAliases, + suppressedVanillaSources, placements, false); + } + } + return new PlacementIndex( + Collections.unmodifiableSet(loadKeys), + Collections.unmodifiableSet(normalizedLoadKeys), + Collections.unmodifiableSet(vanillaAliases), + Collections.unmodifiableSet(suppressedVanillaSources), + nativeKeys(placements), + List.copyOf(placements)); + } + + private static Set nativeKeys(List placements) { + Set nativeKeys = new LinkedHashSet<>(); + for (IrisStructurePlacement placement : placements) { + if (!placement.hasNativeStructures()) { + continue; + } + for (IrisNativeStructure source : placement.getNativeStructures()) { + if (source != null) { + nativeKeys.add(normalize(source.getStructure())); + } + } + } + return Collections.unmodifiableSet(nativeKeys); + } + + private static void collect(KList source, IrisData data, Set loadKeys, + Set normalizedLoadKeys, Set vanillaAliases, + Set suppressedVanillaSources, + List placements, boolean allowNativeSuppression) { + if (source == null) { + return; + } + for (IrisStructurePlacement placement : source) { + if (placement == null) { + throw new IllegalStateException("Iris structure placement list contains a null placement"); + } + if (placement.getDistribution() == null) { + throw new IllegalStateException("Iris structure placement is missing its distribution"); + } + NativeStructurePlacementPlanner.validateBackend(placement); + boolean replacesNative = NativeStructureSuppression.REPLACE_SOURCE == placement.getNativeSuppression(); + if (replacesNative && !allowNativeSuppression) { + throw new IllegalStateException("REPLACE_SOURCE is only valid on dimension-level structure placements"); + } + if (placement.hasNativeStructures()) { + for (IrisNativeStructure nativeStructure : placement.getNativeStructures()) { + if (nativeStructure == null + || nativeStructure.getStructure() == null + || !NAMESPACED_RESOURCE_KEY.matcher(nativeStructure.getStructure()).matches()) { + throw new IllegalStateException("Native structure placement contains an invalid structure key"); + } + String sourceKey = nativeStructure.getStructure(); + loadKeys.add(sourceKey); + normalizedLoadKeys.add(normalize(sourceKey)); + vanillaAliases.add(normalize(sourceKey)); + if (replacesNative) { + suppressedVanillaSources.add(normalize(sourceKey)); + } + } + placements.add(placement); + continue; + } + for (String structureKey : placement.getStructures()) { + if (structureKey == null || structureKey.isBlank()) { + throw new IllegalStateException(replacesNative + ? "REPLACE_SOURCE contains a blank Iris structure reference" + : "Iris structure placement contains a blank structure reference"); + } + IrisStructure structure = data.load(IrisStructure.class, structureKey, false); + if (structure == null) { + throw new IllegalStateException((replacesNative + ? "REPLACE_SOURCE references missing Iris structure '" + : "Iris structure placement references missing structure '") + + structureKey + "'"); + } + loadKeys.add(structureKey); + normalizedLoadKeys.add(normalize(structureKey)); + if (structure.getLoadKey() != null && !structure.getLoadKey().isBlank()) { + loadKeys.add(structure.getLoadKey()); + normalizedLoadKeys.add(normalize(structure.getLoadKey())); + } + if (structure.getVanillaSource() != null && !structure.getVanillaSource().isBlank()) { + vanillaAliases.add(normalize(structure.getVanillaSource())); + } + if (replacesNative) { + String vanillaSource = structure.getVanillaSource(); + if (vanillaSource == null || !NAMESPACED_RESOURCE_KEY.matcher(vanillaSource).matches()) { + throw new IllegalStateException("REPLACE_SOURCE structure '" + structureKey + + "' must declare a valid namespaced vanillaSource"); + } + if (!StructureGraphCatalog.guaranteesRuntimeOutput(data, structure)) { + throw new IllegalStateException("REPLACE_SOURCE structure '" + structureKey + + "' is not runtime-viable; native generation will not be used as a fallback"); + } + suppressedVanillaSources.add(normalize(vanillaSource)); + } + } + placements.add(placement); + } + } + + public record ResolvedPlacement(IrisStructurePlacement placement, String structureKey, IrisStructure structure, + KList pieces, RNG rng, + int originX, int baseY, int originZ, boolean exactY) { + } + + public enum LocateStatus { + FOUND, + NOT_FOUND, + SEARCH_LIMIT_REACHED + } + + public record LocateResult(LocateStatus status, int originX, int baseY, int originZ) { + public boolean found() { + return status == LocateStatus.FOUND; + } + } + + @FunctionalInterface + public interface CandidateFilter { + boolean accept(int chunkX, int chunkZ); + } + + public static final class CandidateSearchLimitException extends RuntimeException { + public CandidateSearchLimitException() { + super(null, null, false, false); + } + } + + private record RandomSpreadParameters(int spacing, int separation, int salt) { + } + + private record PlacementCatalog(List randomSpread, + List concentricRings, boolean hasDensity) { + } + + private record LocatableIndex(Set keys, Set normalizedKeys, + Set editableKeys, Set normalizedEditableKeys, + Set nativeKeys, Set normalizedNativeKeys) { + } + + private record ResolvedStart(int originX, int baseY, int originZ) { + } + + private record LocatedCandidate(ResolvedStart resolved, long distanceSquared) { + } + + private static final class CandidateBudget { + private int checked; + + private void claim() { + if (checked >= CANDIDATE_BUDGET) { + throw new CandidateSearchLimitException(); + } + checked++; + } + } + + private static final class PlacementIndex { + private final Set loadKeys; + private final Set normalizedLoadKeys; + private final Set vanillaAliases; + private final Set suppressedVanillaSources; + private final Set nativeKeys; + private final List placements; + + private PlacementIndex(Set loadKeys, Set normalizedLoadKeys, + Set vanillaAliases, Set suppressedVanillaSources, + Set nativeKeys, + List placements) { + this.loadKeys = loadKeys; + this.normalizedLoadKeys = normalizedLoadKeys; + this.vanillaAliases = vanillaAliases; + this.suppressedVanillaSources = suppressedVanillaSources; + this.nativeKeys = nativeKeys; + this.placements = placements; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/ListFunction.java b/core/src/main/java/art/arcane/iris/engine/framework/ListFunction.java new file mode 100644 index 000000000..706c7fccd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/ListFunction.java @@ -0,0 +1,10 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.core.loader.IrisData; + +import java.util.function.Function; + +public interface ListFunction extends Function { + String key(); + String fancyName(); +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Locator.java b/core/src/main/java/art/arcane/iris/engine/framework/Locator.java new file mode 100644 index 000000000..3fcc116b9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/Locator.java @@ -0,0 +1,153 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.nms.container.BlockPos; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.Spiraler; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +@FunctionalInterface +public interface Locator { + static Locator region(String loadKey) { + Locator exact = (e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey); + return new HintedLocator<>(exact, (engine) -> HintedLocator.regionPlan(engine, loadKey)); + } + + static Locator object(String loadKey) { + Locator exact = (e, c) -> e.getObjectsAt(c.getX(), c.getZ()).contains(loadKey); + return new HintedLocator<>(exact, (engine) -> HintedLocator.objectPlan(engine, loadKey)); + } + + static Locator surfaceBiome(String loadKey) { + Locator exact = (e, c) -> e.getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey); + return new HintedLocator<>(exact, (engine) -> HintedLocator.biomePlan(engine, loadKey)); + } + + static Locator structure(String key) { + return (e, c) -> IrisStructureLocator.startsInChunk(e, key, c.getX(), c.getZ()); + } + + static Locator poi(String type) { + return (e, c) -> { + Set> pos = e.getPOIsAt(c.getX(), c.getZ()); + return pos.stream().anyMatch(p -> p.getA().equals(type)); + }; + } + + static Locator caveBiome(String loadKey) { + Locator exact = (e, c) -> e.getCaveBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey); + return new HintedLocator<>(exact, (engine) -> HintedLocator.caveBiomePlan(engine, loadKey)); + } + + static Locator caveOrMantleBiome(String loadKey) { + return (e, c) -> { + AtomicBoolean found = new AtomicBoolean(false); + try (GenerationSessionLease lease = e.acquireGenerationLease("locator_generate_matter")) { + ChunkContext chunkContext = new ChunkContext(c.getX() << 4, c.getZ() << 4, e.getComplex(), lease.sessionId(), false, ChunkContext.PrefillPlan.NONE, null); + try (IrisContext.Scope locatorScope = IrisContext.open(e, lease.sessionId(), chunkContext)) { + e.generateMatter(c.getX(), c.getZ(), true, chunkContext); + } + } catch (GenerationSessionException sessionException) { + throw new IllegalStateException(sessionException); + } + e.getMantle().getMantle().iterateChunk(c.getX(), c.getZ(), MatterCavern.class, (x, y, z, t) -> { + if (found.get()) { + return; + } + + if (t != null && t.getCustomBiome().equals(loadKey)) { + found.set(true); + } + }); + + return found.get(); + }; + } + + boolean matches(Engine engine, Position2 chunk); + + default CompletableFuture find(Engine engine, Position2 pos, long timeout, Consumer checks) throws WrongEngineBroException { + if (engine.isClosed()) { + throw new WrongEngineBroException(); + } + + AtomicBoolean stop = new AtomicBoolean(false); + CompletableFuture search = MultiBurst.burst.completeValueAsync(() -> { + try (GenerationSessionLease lease = engine.acquireGenerationLease("locator_search"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32; + MultiBurst burst = MultiBurst.burst; + AtomicBoolean found = new AtomicBoolean(false); + AtomicInteger searched = new AtomicInteger(); + AtomicReference foundPos = new AtomicReference<>(); + PrecisionStopwatch px = PrecisionStopwatch.start(); + AtomicReference next = new AtomicReference<>(pos); + Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z))); + s.setOffset(pos.getX(), pos.getZ()); + s.next(); + while (!found.get() && !stop.get() && !engine.isClosing() && px.getMilliseconds() < timeout) { + BurstExecutor e = burst.burst(tc); + + for (int i = 0; i < tc; i++) { + Position2 p = next.get(); + s.next(); + e.queue(() -> { + if (found.get() || stop.get() || engine.isClosing()) { + return; + } + if (matches(engine, p)) { + foundPos.compareAndSet(null, p); + found.set(true); + } + searched.incrementAndGet(); + }); + } + + e.complete(); + checks.accept(searched.get()); + } + + if (found.get() && foundPos.get() != null) { + return foundPos.get(); + } + + return null; + } + }); + return LocatorCanceller.requestScoped(search, stop); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/LocatorCanceller.java b/core/src/main/java/art/arcane/iris/engine/framework/LocatorCanceller.java new file mode 100644 index 000000000..7525db96d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/LocatorCanceller.java @@ -0,0 +1,66 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class LocatorCanceller { + protected static Runnable cancel = null; + + private LocatorCanceller() { + } + + static CompletableFuture requestScoped(CompletableFuture future, AtomicBoolean stop) { + return new RequestFuture<>(Objects.requireNonNull(future), Objects.requireNonNull(stop)); + } + + private static final class RequestFuture extends CompletableFuture { + private final CompletableFuture delegate; + private final AtomicBoolean stop; + + private RequestFuture(CompletableFuture delegate, AtomicBoolean stop) { + this.delegate = delegate; + this.stop = stop; + delegate.whenComplete((value, exception) -> { + if (isDone()) { + return; + } + if (exception == null) { + complete(value); + } else { + completeExceptionally(exception); + } + }); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (isDone()) { + return false; + } + + stop.set(true); + boolean cancelled = super.cancel(mayInterruptIfRunning); + delegate.cancel(mayInterruptIfRunning); + return cancelled; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/LootResolver.java b/core/src/main/java/art/arcane/iris/engine/framework/LootResolver.java new file mode 100644 index 000000000..c4d6c0945 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/LootResolver.java @@ -0,0 +1,303 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisLootMode; +import art.arcane.iris.engine.object.IrisLootReference; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +public final class LootResolver { + private static final int MAX_SCALED_SOURCE_COUNT = 256; + private static final long CONTAINER_SALT = 0x632BE59BD9B4E019L; + private static final long TABLE_SALT = 0x94D049BB133111EBL; + private static final long ENTRY_SALT = 0x9E3779B97F4A7C15L; + + private LootResolver() { + } + + public static RNG containerRng(long lootSeed, int x, int y, int z) { + return new RNG(containerSeed(lootSeed, x, y, z)); + } + + public static long containerSeed(long lootSeed, int x, int y, int z) { + long seed = mix(lootSeed ^ CONTAINER_SALT); + seed = mix(seed ^ ((long) x * 0xD6E8FEB86659FD93L)); + seed = mix(seed ^ ((long) y * 0xA5A3564E27F886A7L)); + return mix(seed ^ ((long) z * 0x8CB92BA72F3D8DD7L)); + } + + public static RNG tableRng(long lootSeed, IrisLootTable table, int x, int y, int z) { + return new RNG(tableSeed(lootSeed, table, x, y, z)); + } + + public static long tableSeed(long lootSeed, IrisLootTable table, int x, int y, int z) { + return mix(containerSeed(lootSeed, x, y, z) ^ stableHash(tableIdentity(table)) ^ TABLE_SALT); + } + + public static boolean spatialOneIn( + long lootSeed, + IrisLootTable table, + int entryIndex, + int x, + int y, + int z, + long rarity + ) { + if (rarity <= 1L) { + return true; + } + long roll = mix(tableSeed(lootSeed, table, x, y, z) ^ ((long) entryIndex * ENTRY_SALT)); + return new RNG(roll).nextLong(rarity) == 0L; + } + + public static long combinedRarity(int tableRarity, int entryRarity) { + long table = Math.max(1, tableRarity); + long entry = Math.max(1, entryRarity); + if (table > Long.MAX_VALUE / entry) { + return Long.MAX_VALUE; + } + return table * entry; + } + + public static boolean oneIn(RNG rng, int rarity) { + int bound = Math.max(1, rarity); + return bound == 1 || rng.nextInt(bound) == 0; + } + + public static int inclusive(RNG rng, int first, int second) { + int lower = Math.min(first, second); + int upper = Math.max(first, second); + if (lower == upper) { + return lower; + } + long span = (long) upper - lower + 1L; + return (int) (lower + rng.nextLong(span)); + } + + public static T pickWeighted(List choices, ToIntFunction weight, RNG rng) { + long total = 0L; + for (T choice : choices) { + int value = Math.max(0, weight.applyAsInt(choice)); + total = Long.MAX_VALUE - total < value ? Long.MAX_VALUE : total + value; + } + if (total <= 0L) { + return null; + } + long pull = rng.nextLong(total); + for (T choice : choices) { + pull -= Math.max(0, weight.applyAsInt(choice)); + if (pull < 0L) { + return choice; + } + } + return null; + } + + public static void resolveEnvironmentSources( + List sources, + Engine engine, + RNG rng, + int x, + int relativeY, + int z, + boolean objectLootDefined, + Function mapper + ) { + IrisRegion region = engine.getComplex().getRegionStream().get(x, z); + IrisBiome surfaceBiome = engine.getComplex().getTrueBiomeStream().get(x, z); + double terrainHeight = engine.getComplex().getHeightStream().get(x, z); + IrisBiome contextualBiome = relativeY < terrainHeight + ? engine.getCaveBiome(x, relativeY, z) + : surfaceBiome; + if (contextualBiome == null) { + contextualBiome = surfaceBiome; + } + + boolean distinctContextualBiome = !sameBiome(surfaceBiome, contextualBiome); + double multiplier = engine.getDimension().getLoot().getMultiplier() + * region.getLoot().getMultiplier() + * surfaceBiome.getLoot().getMultiplier(); + if (distinctContextualBiome) { + multiplier *= contextualBiome.getLoot().getMultiplier(); + } + + boolean fallback = !objectLootDefined; + injectReference(sources, engine.getDimension().getLoot(), engine, mapper, fallback); + injectReference(sources, region.getLoot(), engine, mapper, fallback); + injectReference(sources, surfaceBiome.getLoot(), engine, mapper, fallback); + if (distinctContextualBiome) { + injectReference(sources, contextualBiome.getLoot(), engine, mapper, fallback); + } + scaleSources(sources, multiplier, rng); + } + + public static void injectSources( + List sources, + List additions, + IrisLootMode mode, + boolean fallback + ) { + if (mode == IrisLootMode.FALLBACK && !fallback) { + return; + } + if (mode == IrisLootMode.CLEAR) { + sources.clear(); + return; + } + if (mode == IrisLootMode.REPLACE) { + sources.clear(); + } + sources.addAll(additions); + } + + public static void scaleSources(List sources, double multiplier, RNG rng) { + if (!Double.isFinite(multiplier) || multiplier < 0D) { + throw new IllegalArgumentException("Effective loot source multiplier must be finite and non-negative, got " + + multiplier + "."); + } + if (sources.isEmpty()) { + return; + } + if (sources.size() > MAX_SCALED_SOURCE_COUNT) { + throw new IllegalArgumentException("Loot source scaling starts above the maximum of " + + MAX_SCALED_SOURCE_COUNT + " sources: " + sources.size() + "."); + } + double scaledSize = sources.size() * multiplier; + if (!Double.isFinite(scaledSize) || scaledSize > MAX_SCALED_SOURCE_COUNT) { + throw new IllegalArgumentException("Loot source scaling exceeds the maximum of " + + MAX_SCALED_SOURCE_COUNT + " sources: " + sources.size() + " * " + multiplier + "."); + } + int target = (int) Math.round(scaledSize); + if (target == sources.size()) { + return; + } + List original = new ArrayList<>(sources); + int[] order = new int[original.size()]; + if (target < original.size()) { + shrinkSources(sources, original, order, target, rng); + return; + } + expandSources(sources, original, order, target, rng); + } + + static boolean sameBiome(IrisBiome first, IrisBiome second) { + if (first == second) { + return true; + } + if (first == null || second == null) { + return false; + } + return Objects.equals(first.getLoadKey(), second.getLoadKey()); + } + + static String tableIdentity(IrisLootTable table) { + if (table == null) { + return ""; + } + String loadKey = table.getLoadKey(); + if (loadKey != null && !loadKey.isBlank()) { + return loadKey; + } + String name = table.getName(); + return name == null ? "" : name; + } + + private static void injectReference( + List sources, + IrisLootReference reference, + Engine engine, + Function mapper, + boolean fallback + ) { + KList tables = reference.getLootTables(engine.getComplex()); + KList additions = new KList<>(); + for (IrisLootTable table : tables) { + if (table == null) { + continue; + } + T source = mapper.apply(table); + if (source != null) { + additions.add(source); + } + } + injectSources(sources, additions, reference.getMode(), fallback); + } + + private static void shrinkSources(List sources, List original, int[] order, int target, RNG rng) { + shuffleOrder(order, rng); + boolean[] selected = new boolean[original.size()]; + for (int index = 0; index < target; index++) { + selected[order[index]] = true; + } + sources.clear(); + for (int index = 0; index < original.size(); index++) { + if (selected[index]) { + sources.add(original.get(index)); + } + } + } + + private static void expandSources(List sources, List original, int[] order, int target, RNG rng) { + while (sources.size() < target) { + shuffleOrder(order, rng); + int additions = Math.min(target - sources.size(), original.size()); + for (int index = 0; index < additions; index++) { + sources.add(original.get(order[index])); + } + } + } + + private static void shuffleOrder(int[] order, RNG rng) { + for (int index = 0; index < order.length; index++) { + order[index] = index; + } + for (int index = 0; index < order.length - 1; index++) { + int selected = index + rng.nextInt(order.length - index); + int previous = order[index]; + order[index] = order[selected]; + order[selected] = previous; + } + } + + private static long stableHash(String value) { + long hash = 0xCBF29CE484222325L; + for (int i = 0; i < value.length(); i++) { + hash ^= value.charAt(i); + hash *= 0x100000001B3L; + } + return mix(hash); + } + + private static long mix(long value) { + long mixed = value; + mixed = (mixed ^ (mixed >>> 30)) * 0xBF58476D1CE4E5B9L; + mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL; + return mixed ^ (mixed >>> 31); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/MeteredCache.java b/core/src/main/java/art/arcane/iris/engine/framework/MeteredCache.java similarity index 92% rename from core/src/main/java/com/volmit/iris/engine/framework/MeteredCache.java rename to core/src/main/java/art/arcane/iris/engine/framework/MeteredCache.java index 50f5accf2..80dabc783 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/MeteredCache.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/MeteredCache.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; -import com.volmit.iris.util.data.KCache; +import art.arcane.volmlib.util.data.KCache; public interface MeteredCache { long getSize(); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeFeatureGenerationPolicy.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeFeatureGenerationPolicy.java new file mode 100644 index 000000000..6cc54244f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeFeatureGenerationPolicy.java @@ -0,0 +1,92 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +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.engine.object.NativeFeatureGenerationStatus; + +import java.util.Objects; + +/** + * Single decision point for native placed-feature generation, mirroring {@link NativeStructureGenerationPolicy}. + * Both platform generators call only through here so the Bukkit and modded feature passes cannot drift. + */ +public final class NativeFeatureGenerationPolicy { + /** + * Stand-in for a dimension that has no control block. Immutable in practice: nothing here hands it out for + * mutation, and every default in {@link IrisImportedFeatureControl} is "off". + */ + private static final IrisImportedFeatureControl DISABLED = new IrisImportedFeatureControl(); + + private NativeFeatureGenerationPolicy() { + } + + /** + * The dimension's control block, or a disabled default when it has none. + *

+ * An absent {@code importedFeatures} member deserializes to the field's initializer, but an explicit + * {@code "importedFeatures": null} in dimension JSON overwrites that initializer with null - Gson assigns what + * the document says. This runs on the generation path for every feature decision, so throwing there turns one + * stray null in a pack into a failed chunk rather than a dimension that simply generates no native features. + */ + public static IrisImportedFeatureControl control(Engine engine) { + Engine activeEngine = Objects.requireNonNull(engine, "Native feature policy requires an engine"); + IrisDimension dimension = Objects.requireNonNull(activeEngine.getDimension(), + "Native feature policy requires a bound dimension"); + IrisImportedFeatureControl control = dimension.getImportedFeatures(); + return control == null ? DISABLED : control; + } + + /** + * True when this dimension opted into the native feature pass. Checked before any feature machinery is + * built so a disabled dimension never allocates a feature table. + */ + public static boolean isEnabled(Engine engine) { + return control(engine).shouldGenerateFeatures(); + } + + public static NativeFeatureGenerationStatus resolve(Engine engine, String placedFeatureKey, + IrisDecorationStep step) { + return control(engine).resolve(placedFeatureKey, step); + } + + public static boolean shouldGenerateStep(Engine engine, IrisDecorationStep step) { + return control(engine).shouldGenerateStep(step); + } + + public static String generationStatusMessage(String placedFeatureKey, + NativeFeatureGenerationStatus status) { + String key = placedFeatureKey == null ? "" : placedFeatureKey.trim(); + return switch (Objects.requireNonNull(status, "Native feature status must not be null")) { + case GENERATE_NATIVE -> "Native feature " + key + " generates natively."; + case FEATURES_DISABLED -> "Native feature " + key + + " does not generate because this dimension's importedFeatures.enabled is false."; + case STEP_DISABLED -> "Native feature " + key + + " does not generate because its decoration step is excluded by importedFeatures."; + case DISABLED_BY_PACK -> "Native feature " + key + + " is disabled by this dimension's importedFeatures.disabled list."; + case CYCLE_DEGRADED -> "Native feature " + key + + " does not generate because the registered features could not be ordered " + + "(feature order cycle); importedFeatures was degraded to off for this dimension."; + case INVALID_REGISTRY_KEY -> "Native feature registry key is invalid: " + key; + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureFrequencyScale.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureFrequencyScale.java new file mode 100644 index 000000000..b3263a3dc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureFrequencyScale.java @@ -0,0 +1,63 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +public record NativeStructureFrequencyScale(float frequency, int spacing) { + public static NativeStructureFrequencyScale randomSpread( + float frequency, + int spacing, + int separation, + double multiplier + ) { + if (!Float.isFinite(frequency) || frequency < 0F || frequency > 1F) { + throw new IllegalArgumentException("Native structure frequency must be between 0 and 1"); + } + if (spacing <= separation) { + throw new IllegalArgumentException("Native structure spacing must exceed separation"); + } + requireMultiplier(multiplier); + if (frequency == 0F || multiplier == 1D) { + return new NativeStructureFrequencyScale(frequency, spacing); + } + + double requestedFrequency = frequency * multiplier; + float scaledFrequency = (float) Math.min(1D, requestedFrequency); + double remainingDensity = requestedFrequency / scaledFrequency; + int scaledSpacing = spacing; + if (remainingDensity > 1D) { + int requestedSpacing = (int) Math.round(spacing / Math.sqrt(remainingDensity)); + scaledSpacing = Math.max(separation + 1, requestedSpacing); + } + return new NativeStructureFrequencyScale(scaledFrequency, scaledSpacing); + } + + public static float probability(float frequency, double multiplier) { + if (!Float.isFinite(frequency) || frequency < 0F || frequency > 1F) { + throw new IllegalArgumentException("Native structure frequency must be between 0 and 1"); + } + requireMultiplier(multiplier); + return (float) Math.min(1D, frequency * multiplier); + } + + private static void requireMultiplier(double multiplier) { + if (!Double.isFinite(multiplier) || multiplier < 0.01D || multiplier > 16D) { + throw new IllegalArgumentException("Native structure frequency multiplier must be between 0.01 and 16"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicy.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicy.java new file mode 100644 index 000000000..fac5bfc63 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicy.java @@ -0,0 +1,69 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisImportedStructureControl; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; + +import java.util.Objects; + +public final class NativeStructureGenerationPolicy { + private NativeStructureGenerationPolicy() { + } + + public static IrisNativeStructureDecision resolve(Engine engine, String structureKey, + boolean undergroundStep) { + Engine activeEngine = Objects.requireNonNull(engine, "Native structure policy requires an engine"); + IrisDimension dimension = Objects.requireNonNull(activeEngine.getDimension(), + "Native structure policy requires a bound dimension"); + IrisImportedStructureControl control = Objects.requireNonNull( + dimension.getImportedStructures(), + "Dimension importedStructures must not be null"); + IrisNativeStructureDecision decision = control.resolve(structureKey, undergroundStep); + if (!decision.generate()) { + // A disabled key with an active Iris placement is the "blanket-disable, re-place + // explicitly" pattern: the placement planner ignores the disable list, so generation + // places it — report REPLACED_BY_IRIS so find/goto/verify locate the placement. + if (decision.status() == NativeStructureGenerationStatus.DISABLED_BY_PACK + && IrisStructureLocator.isPlaced(activeEngine, structureKey)) { + return decision.withStatus(NativeStructureGenerationStatus.REPLACED_BY_IRIS); + } + return decision; + } + if (IrisStructureLocator.suppressesVanilla(activeEngine, structureKey)) { + return decision.withStatus(NativeStructureGenerationStatus.REPLACED_BY_IRIS); + } + return decision; + } + + public static String generationStatusMessage(String structureKey, + NativeStructureGenerationStatus status) { + String key = structureKey == null ? "" : structureKey.trim(); + return switch (Objects.requireNonNull(status, "Native structure status must not be null")) { + case GENERATE_NATIVE -> "Native structure " + key + " generates natively."; + case DISABLED_BY_PACK -> "Native structure " + key + + " is disabled by this dimension's importedStructures settings."; + case REPLACED_BY_IRIS -> "Native structure " + key + + " is replaced by an Iris placement in this pack and locates through that explicit replacement."; + case INVALID_REGISTRY_KEY -> "Native structure registry key is invalid: " + key; + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipBundle.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipBundle.java new file mode 100644 index 000000000..cc8a47824 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipBundle.java @@ -0,0 +1,117 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord.OwnershipKey; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public record NativeStructureOwnershipBundle(Map records) { + public static final int MAX_RECORDS = 16_384; + public static final int MAX_ENCODED_BYTES = 2 * 1024 * 1024; + private static final Comparator RECORD_ORDER = Comparator + .comparing(NativeStructureOwnershipRecord::structureKey) + .thenComparingInt(NativeStructureOwnershipRecord::originChunkX) + .thenComparingInt(NativeStructureOwnershipRecord::originChunkZ); + + public NativeStructureOwnershipBundle { + records = Map.copyOf(Objects.requireNonNull(records, + "Native structure ownership records must not be null")); + if (records.size() > MAX_RECORDS) { + throw new IllegalArgumentException("Native structure ownership bundle exceeds " + + MAX_RECORDS + " records"); + } + for (Map.Entry entry : records.entrySet()) { + if (!entry.getKey().equals(entry.getValue().ownershipKey())) { + throw new IllegalArgumentException("Native structure ownership key does not match its record"); + } + } + try { + encodePayload(records); + } catch (IOException error) { + throw new IllegalArgumentException("Native structure ownership bundle is not encodable", error); + } + } + + public static NativeStructureOwnershipBundle empty() { + return new NativeStructureOwnershipBundle(Map.of()); + } + + public NativeStructureOwnershipRecord find(String structureKey, int originChunkX, int originChunkZ) { + return records.get(new OwnershipKey(structureKey, originChunkX, originChunkZ)); + } + + public NativeStructureOwnershipBundle with(NativeStructureOwnershipRecord record) { + NativeStructureOwnershipRecord resolved = Objects.requireNonNull( + record, "Native structure ownership record must not be null"); + Map updated = new LinkedHashMap<>(records); + updated.put(resolved.ownershipKey(), resolved); + return new NativeStructureOwnershipBundle(updated); + } + + public NativeStructureOwnershipBundle without(String structureKey, int originChunkX, int originChunkZ) { + Map updated = new LinkedHashMap<>(records); + updated.remove(new OwnershipKey(structureKey, originChunkX, originChunkZ)); + return new NativeStructureOwnershipBundle(updated); + } + + public void write(DataOutputStream output) throws IOException { + byte[] encoded = encodePayload(records); + output.writeInt(encoded.length); + output.write(encoded); + } + + public static NativeStructureOwnershipBundle read(DataInputStream input) throws IOException { + int byteCount = input.readInt(); + if (byteCount < 0 || byteCount > MAX_ENCODED_BYTES) { + throw new IOException("Native structure ownership bundle has invalid length " + byteCount); + } + byte[] encoded = input.readNBytes(byteCount); + if (encoded.length != byteCount) { + throw new IOException("Native structure ownership bundle ended early"); + } + try (DataInputStream payload = new DataInputStream(new ByteArrayInputStream(encoded))) { + int count = payload.readInt(); + if (count < 0 || count > MAX_RECORDS) { + throw new IOException("Native structure ownership bundle has invalid record count " + count); + } + Map records = new LinkedHashMap<>(count); + for (int i = 0; i < count; i++) { + NativeStructureOwnershipRecord record = NativeStructureOwnershipRecord.read(payload); + if (records.put(record.ownershipKey(), record) != null) { + throw new IOException("Native structure ownership bundle contains a duplicate record"); + } + } + if (payload.available() != 0) { + throw new IOException("Native structure ownership bundle contains trailing data"); + } + return new NativeStructureOwnershipBundle(records); + } + } + + private static byte[] encodePayload( + Map records) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream payload = new DataOutputStream(bytes)) { + payload.writeInt(records.size()); + List ordered = new ArrayList<>(records.values()); + ordered.sort(RECORD_ORDER); + for (NativeStructureOwnershipRecord record : ordered) { + record.write(payload); + } + } + if (bytes.size() > MAX_ENCODED_BYTES) { + throw new IOException("Native structure ownership bundle exceeds " + + MAX_ENCODED_BYTES + " bytes"); + } + return bytes.toByteArray(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipRecord.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipRecord.java new file mode 100644 index 000000000..ed6a4acbc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipRecord.java @@ -0,0 +1,307 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.IrisStructureStiltSettings; +import art.arcane.iris.engine.object.IrisStructureTerrain; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import com.google.gson.Gson; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Objects; + +public record NativeStructureOwnershipRecord( + int schema, + String structureKey, + int originChunkX, + int originChunkZ, + long placementIdentity, + int baseY, + int contentMinX, + int contentMinY, + int contentMinZ, + int contentMaxX, + int contentMaxY, + int contentMaxZ, + int locatorY, + int referenceMinChunkX, + int referenceMaxChunkX, + int referenceMinChunkZ, + int referenceMaxChunkZ, + String contentFingerprint, + DecisionSnapshot decision +) { + public static final int CURRENT_SCHEMA = 2; + public static final int MAX_REFERENCE_DISTANCE_CHUNKS = 8; + private static final int MAX_KEY_BYTES = 512; + private static final int MAX_FINGERPRINT_BYTES = 128; + + public NativeStructureOwnershipRecord { + if (schema != CURRENT_SCHEMA) { + throw new IllegalArgumentException("Unsupported native structure ownership schema " + schema); + } + structureKey = normalizeKey(structureKey); + requireUtf8Limit(structureKey, MAX_KEY_BYTES, "structure key"); + if (contentMinX > contentMaxX || contentMinY > contentMaxY || contentMinZ > contentMaxZ) { + throw new IllegalArgumentException("Native structure content bounds are inverted"); + } + if ((long) originChunkX - referenceMinChunkX > MAX_REFERENCE_DISTANCE_CHUNKS + || (long) referenceMaxChunkX - originChunkX > MAX_REFERENCE_DISTANCE_CHUNKS + || (long) originChunkZ - referenceMinChunkZ > MAX_REFERENCE_DISTANCE_CHUNKS + || (long) referenceMaxChunkZ - originChunkZ > MAX_REFERENCE_DISTANCE_CHUNKS) { + throw new IllegalArgumentException("Native structure ownership exceeds Minecraft's reference range"); + } + long referenceMinBlockX = (long) referenceMinChunkX << 4; + long referenceMaxBlockX = ((long) referenceMaxChunkX << 4) + 15L; + long referenceMinBlockZ = (long) referenceMinChunkZ << 4; + long referenceMaxBlockZ = ((long) referenceMaxChunkZ << 4) + 15L; + if (contentMinX < referenceMinBlockX || contentMaxX > referenceMaxBlockX + || contentMinZ < referenceMinBlockZ || contentMaxZ > referenceMaxBlockZ) { + throw new IllegalArgumentException( + "Native structure content bounds exceed their reference envelope"); + } + contentFingerprint = Objects.requireNonNull(contentFingerprint, + "Native structure content fingerprint must not be null").toLowerCase(Locale.ROOT); + requireUtf8Limit(contentFingerprint, MAX_FINGERPRINT_BYTES, "content fingerprint"); + if (!contentFingerprint.matches("[0-9a-f]{64}")) { + throw new IllegalArgumentException("Native structure content fingerprint must be SHA-256 hex"); + } + decision = Objects.requireNonNull(decision, "Native structure decision snapshot must not be null"); + } + + public static NativeStructureOwnershipRecord create(String structureKey, + int originChunkX, + int originChunkZ, + long placementIdentity, + int baseY, + int contentMinX, + int contentMinY, + int contentMinZ, + int contentMaxX, + int contentMaxY, + int contentMaxZ, + int locatorY, + int referenceMinChunkX, + int referenceMaxChunkX, + int referenceMinChunkZ, + int referenceMaxChunkZ, + String contentFingerprint, + IrisNativeStructureDecision decision) { + return new NativeStructureOwnershipRecord( + CURRENT_SCHEMA, + structureKey, + originChunkX, + originChunkZ, + placementIdentity, + baseY, + contentMinX, + contentMinY, + contentMinZ, + contentMaxX, + contentMaxY, + contentMaxZ, + locatorY, + referenceMinChunkX, + referenceMaxChunkX, + referenceMinChunkZ, + referenceMaxChunkZ, + contentFingerprint, + DecisionSnapshot.capture(decision) + ); + } + + public boolean covers(int chunkX, int chunkZ) { + return chunkX >= referenceMinChunkX && chunkX <= referenceMaxChunkX + && chunkZ >= referenceMinChunkZ && chunkZ <= referenceMaxChunkZ; + } + + public NativeStructureOwnershipRecord withReferenceEnvelope( + int referenceMinChunkX, int referenceMaxChunkX, + int referenceMinChunkZ, int referenceMaxChunkZ) { + return new NativeStructureOwnershipRecord( + schema, + structureKey, + originChunkX, + originChunkZ, + placementIdentity, + baseY, + contentMinX, + contentMinY, + contentMinZ, + contentMaxX, + contentMaxY, + contentMaxZ, + locatorY, + referenceMinChunkX, + referenceMaxChunkX, + referenceMinChunkZ, + referenceMaxChunkZ, + contentFingerprint, + decision + ); + } + + public OwnershipKey ownershipKey() { + return new OwnershipKey(structureKey, originChunkX, originChunkZ); + } + + public IrisNativeStructureDecision restoredDecision() { + return decision.restore(); + } + + void write(DataOutputStream output) throws IOException { + output.writeInt(schema); + writeString(output, structureKey, MAX_KEY_BYTES, "structure key"); + output.writeInt(originChunkX); + output.writeInt(originChunkZ); + output.writeLong(placementIdentity); + output.writeInt(baseY); + output.writeInt(contentMinX); + output.writeInt(contentMinY); + output.writeInt(contentMinZ); + output.writeInt(contentMaxX); + output.writeInt(contentMaxY); + output.writeInt(contentMaxZ); + output.writeInt(locatorY); + output.writeInt(referenceMinChunkX); + output.writeInt(referenceMaxChunkX); + output.writeInt(referenceMinChunkZ); + output.writeInt(referenceMaxChunkZ); + writeString(output, contentFingerprint, MAX_FINGERPRINT_BYTES, "content fingerprint"); + decision.write(output); + } + + static NativeStructureOwnershipRecord read(DataInputStream input) throws IOException { + int schema = input.readInt(); + if (schema != CURRENT_SCHEMA) { + throw new IOException("Unsupported native structure ownership schema " + schema); + } + try { + return new NativeStructureOwnershipRecord( + schema, + readString(input, MAX_KEY_BYTES, "structure key"), + input.readInt(), + input.readInt(), + input.readLong(), + input.readInt(), + input.readInt(), + input.readInt(), + input.readInt(), + input.readInt(), + input.readInt(), + input.readInt(), + input.readInt(), + input.readInt(), + input.readInt(), + input.readInt(), + input.readInt(), + readString(input, MAX_FINGERPRINT_BYTES, "content fingerprint"), + DecisionSnapshot.read(input) + ); + } catch (IllegalArgumentException exception) { + throw new IOException("Native structure ownership record is invalid", exception); + } + } + + private static String normalizeKey(String key) { + String normalized = Objects.requireNonNull(key, + "Native structure key must not be null").trim().toLowerCase(Locale.ROOT); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Native structure key must not be blank"); + } + return normalized; + } + + static void writeString(DataOutputStream output, String value, int maximumBytes, + String fieldName) throws IOException { + byte[] encoded = Objects.requireNonNull(value, fieldName + " must not be null") + .getBytes(StandardCharsets.UTF_8); + if (encoded.length > maximumBytes) { + throw new IOException("Native structure " + fieldName + " exceeds " + maximumBytes + " bytes"); + } + output.writeInt(encoded.length); + output.write(encoded); + } + + static String readString(DataInputStream input, int maximumBytes, String fieldName) throws IOException { + int length = input.readInt(); + if (length < 0 || length > maximumBytes) { + throw new IOException("Native structure " + fieldName + " has invalid length " + length); + } + byte[] encoded = input.readNBytes(length); + if (encoded.length != length) { + throw new IOException("Native structure " + fieldName + " ended early"); + } + return new String(encoded, StandardCharsets.UTF_8); + } + + private static void requireUtf8Limit(String value, int maximumBytes, String fieldName) { + if (value.getBytes(StandardCharsets.UTF_8).length > maximumBytes) { + throw new IllegalArgumentException("Native structure " + fieldName + + " exceeds " + maximumBytes + " bytes"); + } + } + + public record OwnershipKey(String structureKey, int originChunkX, int originChunkZ) { + public OwnershipKey { + structureKey = normalizeKey(structureKey); + } + } + + public record DecisionSnapshot(String stiltJson, String terrainJson) { + private static final Gson GSON = new Gson(); + private static final int MAX_JSON_BYTES = 65_536; + + public DecisionSnapshot { + stiltJson = Objects.requireNonNull(stiltJson, "Native structure stilt snapshot must not be null"); + terrainJson = Objects.requireNonNull(terrainJson, "Native structure terrain snapshot must not be null"); + requireUtf8Limit(stiltJson, MAX_JSON_BYTES, "stilt snapshot"); + requireUtf8Limit(terrainJson, MAX_JSON_BYTES, "terrain snapshot"); + } + + public static DecisionSnapshot capture(IrisNativeStructureDecision decision) { + IrisNativeStructureDecision resolved = Objects.requireNonNull( + decision, "Native structure decision must not be null"); + if (!resolved.generate()) { + throw new IllegalArgumentException("Only generated native structure decisions can be persisted"); + } + return new DecisionSnapshot( + GSON.toJson(resolved.stilt()), + GSON.toJson(Objects.requireNonNullElseGet( + resolved.terrain(), IrisStructureTerrain::new)) + ); + } + + public IrisNativeStructureDecision restore() { + IrisStructureStiltSettings stilt = "null".equals(stiltJson) + ? null : GSON.fromJson(stiltJson, IrisStructureStiltSettings.class); + IrisStructureTerrain terrain = GSON.fromJson(terrainJson, IrisStructureTerrain.class); + if (terrain == null) { + throw new IllegalStateException("Persisted native structure terrain snapshot is null"); + } + return new IrisNativeStructureDecision( + NativeStructureGenerationStatus.GENERATE_NATIVE, + 0, + null, + false, + stilt, + terrain + ); + } + + void write(DataOutputStream output) throws IOException { + writeString(output, stiltJson, MAX_JSON_BYTES, "stilt snapshot"); + writeString(output, terrainJson, MAX_JSON_BYTES, "terrain snapshot"); + } + + static DecisionSnapshot read(DataInputStream input) throws IOException { + return new DecisionSnapshot( + readString(input, MAX_JSON_BYTES, "stilt snapshot"), + readString(input, MAX_JSON_BYTES, "terrain snapshot") + ); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStore.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStore.java new file mode 100644 index 000000000..7e1febdd6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStore.java @@ -0,0 +1,376 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterSlice; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public final class NativeStructureOwnershipStore { + private static final int MAX_CACHED_AUTHORITIES = 65_536; + private static final Cache STATES = Caffeine.newBuilder().weakKeys().build(); + + private NativeStructureOwnershipStore() { + } + + public static void record(Engine engine, NativeStructureOwnershipRecord record) { + state(engine).record(record); + } + + public static NativeStructureOwnershipRecord find(Engine engine, int targetChunkX, int targetChunkZ, + String structureKey, int originChunkX, int originChunkZ) { + return state(engine).find(targetChunkX, targetChunkZ, + structureKey, originChunkX, originChunkZ); + } + + public static NativeStructureOwnershipRecord findPersisted( + Engine engine, String structureKey, int originChunkX, int originChunkZ) { + return state(engine).findPersisted(structureKey, originChunkX, originChunkZ); + } + + public static void discard(Engine engine, String structureKey, int originChunkX, int originChunkZ) { + state(engine).discard(structureKey, originChunkX, originChunkZ); + } + + public static void flush(Engine engine) { + State state = STATES.getIfPresent(Objects.requireNonNull(engine, + "Native structure ownership store requires an engine")); + if (state != null) { + state.flush(); + } + } + + public static void close(Engine engine) { + Engine resolved = Objects.requireNonNull(engine, + "Native structure ownership store requires an engine"); + State state = STATES.get(resolved, State::new); + state.close(); + } + + private static State state(Engine engine) { + Engine resolved = Objects.requireNonNull(engine, + "Native structure ownership store requires an engine"); + return STATES.get(resolved, State::new); + } + + static final class State { + private final WeakReference engine; + private final Storage storage; + private final Cache authorities; + private final ReentrantReadWriteLock lifecycleLock; + private volatile boolean dirty; + private volatile boolean closed; + + private State(Engine engine) { + this(engine, new MantleStorage(engine)); + } + + State(Engine engine, Storage storage) { + this.engine = new WeakReference<>(Objects.requireNonNull(engine, + "Native structure ownership store requires an engine")); + this.storage = Objects.requireNonNull(storage, + "Native structure ownership storage must not be null"); + this.authorities = Caffeine.newBuilder().maximumSize(MAX_CACHED_AUTHORITIES).build(); + this.lifecycleLock = new ReentrantReadWriteLock(true); + } + + void record(NativeStructureOwnershipRecord record) { + lifecycleLock.readLock().lock(); + try { + requireOpen(); + NativeStructureOwnershipRecord resolved = Objects.requireNonNull( + record, "Native structure ownership record must not be null"); + authorities.asMap().compute(resolved.ownershipKey(), (ignored, current) -> { + storage.write(pack(resolved.originChunkX(), resolved.originChunkZ()), resolved); + dirty = true; + return Authority.present(resolved); + }); + } finally { + lifecycleLock.readLock().unlock(); + } + } + + NativeStructureOwnershipRecord find(int targetChunkX, int targetChunkZ, + String structureKey, int originChunkX, int originChunkZ) { + lifecycleLock.readLock().lock(); + try { + requireOpen(); + NativeStructureOwnershipRecord.OwnershipKey key = + new NativeStructureOwnershipRecord.OwnershipKey( + structureKey, originChunkX, originChunkZ); + Authority authority = authorities.asMap().computeIfAbsent( + key, this::loadAuthority); + NativeStructureOwnershipRecord record = authority.record(); + return record != null && record.covers(targetChunkX, targetChunkZ) + ? record : null; + } finally { + lifecycleLock.readLock().unlock(); + } + } + + NativeStructureOwnershipRecord findPersisted( + String structureKey, int originChunkX, int originChunkZ) { + lifecycleLock.readLock().lock(); + try { + requireOpen(); + NativeStructureOwnershipRecord.OwnershipKey key = + new NativeStructureOwnershipRecord.OwnershipKey( + structureKey, originChunkX, originChunkZ); + return authorities.asMap().computeIfAbsent( + key, this::loadAuthority).record(); + } finally { + lifecycleLock.readLock().unlock(); + } + } + + void discard(String structureKey, int originChunkX, int originChunkZ) { + lifecycleLock.readLock().lock(); + try { + requireOpen(); + NativeStructureOwnershipRecord.OwnershipKey key = + new NativeStructureOwnershipRecord.OwnershipKey( + structureKey, originChunkX, originChunkZ); + authorities.asMap().compute(key, (ignored, current) -> { + storage.remove(pack(originChunkX, originChunkZ), + structureKey, originChunkX, originChunkZ); + dirty = true; + return Authority.absent(); + }); + } finally { + lifecycleLock.readLock().unlock(); + } + } + + void flush() { + lifecycleLock.writeLock().lock(); + try { + if (closed) { + throw new IllegalStateException("Native structure ownership store is closed"); + } + flushDirty(); + } finally { + lifecycleLock.writeLock().unlock(); + } + } + + void close() { + lifecycleLock.writeLock().lock(); + try { + if (closed) { + return; + } + if (!flushDirty()) { + throw new IllegalStateException( + "Cannot close native structure ownership storage while Mantle regions are in use"); + } + closed = true; + authorities.invalidateAll(); + } finally { + lifecycleLock.writeLock().unlock(); + } + } + + private boolean flushDirty() { + if (!dirty) { + return true; + } + if (storage.flush()) { + dirty = false; + return true; + } + return false; + } + + private Authority loadAuthority( + NativeStructureOwnershipRecord.OwnershipKey key) { + NativeStructureOwnershipBundle origin = storage.read( + key.originChunkX(), key.originChunkZ()); + NativeStructureOwnershipRecord record = origin == null ? null + : origin.find(key.structureKey(), key.originChunkX(), key.originChunkZ()); + return record == null ? Authority.absent() : Authority.present(record); + } + + private void requireOpen() { + engine(); + if (closed) { + throw new IllegalStateException("Native structure ownership store is closed"); + } + } + + private Engine engine() { + Engine active = engine.get(); + if (active == null) { + throw new IllegalStateException("Native structure ownership engine is unavailable"); + } + return active; + } + } + + interface Storage { + NativeStructureOwnershipBundle read(int chunkX, int chunkZ); + + void write(long target, NativeStructureOwnershipRecord record); + + void remove(long target, String structureKey, int originChunkX, int originChunkZ); + + boolean flush(); + } + + private static final class MantleStorage implements Storage { + private final WeakReference engine; + private final Map pendingBundles; + + private MantleStorage(Engine engine) { + this.engine = new WeakReference<>(Objects.requireNonNull(engine, + "Native structure ownership store requires an engine")); + this.pendingBundles = new ConcurrentHashMap<>(); + } + + @Override + public NativeStructureOwnershipBundle read(int chunkX, int chunkZ) { + MantleChunk chunk = mantle().getChunk(chunkX, chunkZ).use(); + try { + synchronized (chunk) { + return chunk.get(0, 0, 0, NativeStructureOwnershipBundle.class); + } + } finally { + chunk.release(); + } + } + + @Override + public void write(long target, NativeStructureOwnershipRecord record) { + int targetChunkX = unpackX(target); + int targetChunkZ = unpackZ(target); + MantleChunk chunk = mantle().getChunk(targetChunkX, targetChunkZ).use(); + try { + synchronized (chunk) { + Matter section = chunk.getOrCreate(0); + MatterSlice slice = + section.slice(NativeStructureOwnershipBundle.class); + NativeStructureOwnershipBundle bundle = slice.get(0, 0, 0); + NativeStructureOwnershipBundle updated = Objects.requireNonNullElseGet( + bundle, NativeStructureOwnershipBundle::empty).with(record); + slice.set(0, 0, 0, updated); + pendingBundles.put(target, updated); + } + } finally { + chunk.release(); + } + } + + @Override + public void remove(long target, String structureKey, + int originChunkX, int originChunkZ) { + int targetChunkX = unpackX(target); + int targetChunkZ = unpackZ(target); + MantleChunk chunk = mantle().getChunk(targetChunkX, targetChunkZ).use(); + try { + synchronized (chunk) { + Matter section = chunk.getOrCreate(0); + MatterSlice slice = + section.getSlice(NativeStructureOwnershipBundle.class); + if (slice == null) { + return; + } + NativeStructureOwnershipBundle bundle = slice.get(0, 0, 0); + if (bundle == null) { + return; + } + NativeStructureOwnershipBundle updated = bundle.without( + structureKey, originChunkX, originChunkZ); + if (updated.equals(bundle)) { + return; + } + slice.set(0, 0, 0, updated.records().isEmpty() ? null : updated); + section.trimSlices(); + pendingBundles.put(target, updated); + } + } finally { + chunk.release(); + } + } + + @Override + public boolean flush() { + Map pending = new TreeMap<>(pendingBundles); + if (pending.isEmpty()) { + return true; + } + + Mantle mantle = mantle(); + Set regions = new TreeSet<>(); + for (Map.Entry entry : pending.entrySet()) { + long target = entry.getKey(); + restorePendingBundle(mantle, target, entry.getValue()); + regions.add(Mantle.key(unpackX(target) >> 5, unpackZ(target) >> 5)); + } + Set deferredRegions = mantle.saveIdleTectonicPlates(regions); + for (Map.Entry entry : pending.entrySet()) { + long target = entry.getKey(); + long region = Mantle.key(unpackX(target) >> 5, unpackZ(target) >> 5); + if (!deferredRegions.contains(region)) { + pendingBundles.remove(target, entry.getValue()); + } + } + return deferredRegions.isEmpty(); + } + + private void restorePendingBundle(Mantle mantle, long target, + NativeStructureOwnershipBundle bundle) { + MantleChunk chunk = mantle.getChunk(unpackX(target), unpackZ(target)).use(); + try { + synchronized (chunk) { + Matter section = chunk.getOrCreate(0); + MatterSlice slice = + section.slice(NativeStructureOwnershipBundle.class); + slice.set(0, 0, 0, bundle.records().isEmpty() ? null : bundle); + section.trimSlices(); + } + } finally { + chunk.release(); + } + } + + private Mantle mantle() { + Engine active = engine.get(); + if (active == null) { + throw new IllegalStateException("Native structure ownership engine is unavailable"); + } + return active.getMantle().getMantle(); + } + } + + private record Authority(NativeStructureOwnershipRecord record) { + private static Authority present(NativeStructureOwnershipRecord record) { + return new Authority(Objects.requireNonNull(record, + "Native structure ownership authority must not be null")); + } + + private static Authority absent() { + return new Authority(null); + } + } + + static long pack(int chunkX, int chunkZ) { + return (chunkX & 0xffffffffL) | ((chunkZ & 0xffffffffL) << 32); + } + + static int unpackX(long packed) { + return (int) packed; + } + + static int unpackZ(long packed) { + return (int) (packed >>> 32); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlanner.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlanner.java new file mode 100644 index 000000000..153da99d4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlanner.java @@ -0,0 +1,173 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisNativeStructure; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.IrisStructureAnchorMode; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +public final class NativeStructurePlacementPlanner { + private static final long NATIVE_SELECTION_DOMAIN = 0x4E41544956454A53L; + + private NativeStructurePlacementPlanner() { + } + + public static KList plansAt(Engine engine, int chunkX, int chunkZ) { + Map plansByStructure = new LinkedHashMap<>(); + for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt(engine, chunkX, chunkZ)) { + NativeStructureStartPlan plan = planAt(engine, placement, chunkX, chunkZ); + if (plan != null) { + String structureKey = normalize(plan.source().getStructure()); + NativeStructureStartPlan current = plansByStructure.get(structureKey); + if (current == null || comparePlacementPriority(plan.placement(), current.placement()) < 0) { + plansByStructure.put(structureKey, plan); + } + } + } + return new KList<>(plansByStructure.values()); + } + + public static NativeStructureStartPlan planAt(Engine engine, IrisStructurePlacement placement, + int chunkX, int chunkZ) { + validateBackend(placement); + if (!placement.hasNativeStructures()) { + return null; + } + Engine activeEngine = Objects.requireNonNull(engine, "Native structure planner requires an engine"); + if (activeEngine.getSeedManager() == null) { + throw new IllegalStateException("Native structure planner requires a bound seed manager"); + } + long mantleSeed = activeEngine.getSeedManager().getMantle(); + if (!StructurePlacementGrid.startsInChunk(placement, chunkX, chunkZ, mantleSeed)) { + return null; + } + RNG placementRng = StructurePlacementGrid.placementRng( + placement, chunkX, chunkZ, mantleSeed).nextParallelRNG(NATIVE_SELECTION_DOMAIN); + IrisNativeStructure source = selectSource(placement.getNativeStructures(), placementRng); + Integer baseY = resolveBaseY(activeEngine, placement, chunkX, chunkZ, placementRng); + if (baseY == null) { + return null; + } + return new NativeStructureStartPlan(placement, source, chunkX, chunkZ, baseY); + } + + public static NativeStructureStartPlan matchingPlan(Engine engine, String structureKey, + int chunkX, int chunkZ) { + if (structureKey == null || structureKey.isBlank()) { + return null; + } + String normalizedKey = normalize(structureKey); + for (NativeStructureStartPlan plan : plansAt(engine, chunkX, chunkZ)) { + if (normalizedKey.equals(normalize(plan.source().getStructure()))) { + return plan; + } + } + return null; + } + + public static IrisNativeStructureDecision decisionFor(NativeStructureStartPlan plan) { + Objects.requireNonNull(plan, "Native structure start plan must not be null"); + IrisStructurePlacement placement = plan.placement(); + return new IrisNativeStructureDecision( + NativeStructureGenerationStatus.GENERATE_NATIVE, + 0, + null, + false, + placement.getStilt(), + placement.resolvedTerrain() + ); + } + + public static void validateBackend(IrisStructurePlacement placement) { + if (placement == null) { + throw new IllegalStateException("Structure placement must not be null"); + } + boolean iris = placement.hasIrisStructures(); + boolean nativeStructure = placement.hasNativeStructures(); + if (iris == nativeStructure) { + throw new IllegalStateException("Structure placement must declare exactly one backend: " + + "structures or nativeStructures"); + } + if (nativeStructure && placement.getAnchor() != null + && placement.getAnchor() != IrisStructureAnchorMode.LEGACY) { + throw new IllegalStateException("Native structure placements do not support the Iris anchor field"); + } + } + + static IrisNativeStructure selectSource(KList sources, RNG rng) { + Objects.requireNonNull(sources, "Native structure sources must not be null"); + Objects.requireNonNull(rng, "Native structure selection requires an RNG"); + long totalWeight = 0L; + for (IrisNativeStructure source : sources) { + if (source == null || source.getStructure() == null || source.getStructure().isBlank()) { + throw new IllegalStateException("Native structure placement contains a blank source"); + } + if (source.getWeight() < 1) { + throw new IllegalStateException("Native structure source '" + source.getStructure() + + "' has invalid weight " + source.getWeight()); + } + totalWeight = Math.addExact(totalWeight, source.getWeight()); + } + if (totalWeight > Integer.MAX_VALUE) { + throw new IllegalStateException("Native structure source weights exceed " + Integer.MAX_VALUE); + } + int roll = rng.nextInt((int) totalWeight); + for (IrisNativeStructure source : sources) { + roll -= source.getWeight(); + if (roll < 0) { + return source; + } + } + throw new IllegalStateException("Native structure source selection produced no result"); + } + + private static Integer resolveBaseY(Engine engine, IrisStructurePlacement placement, + int chunkX, int chunkZ, RNG rng) { + int worldMin = engine.getMinHeight() + 1; + int worldMax = engine.getMinHeight() + engine.getHeight() - 1; + if (worldMin > worldMax) { + throw new IllegalStateException("Native structure placement has invalid world height bounds"); + } + int blockX = (chunkX << 4) + 8; + int blockZ = (chunkZ << 4) + 8; + if (!placement.isUnderwater() + && isSubmerged(engine, blockX, blockZ)) { + return null; + } + if (!placement.isUnderground()) { + int surfaceY = engine.getHeight(blockX, blockZ, true) + engine.getMinHeight(); + if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) { + return null; + } + return surfaceY; + } + int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight())); + int bandMax = Math.min(worldMax, Math.max(placement.getMinHeight(), placement.getMaxHeight())); + if (bandMin > bandMax) { + return null; + } + return bandMin == bandMax ? bandMin : bandMin + rng.nextInt((bandMax - bandMin) + 1); + } + + static boolean isSubmerged(Engine engine, int blockX, int blockZ) { + return engine.getHeight(blockX, blockZ, true) < engine.getDimension().getFluidHeight(); + } + + private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) { + long leftIdentity = StructurePlacementGrid.placementIdentity(left); + long rightIdentity = StructurePlacementGrid.placementIdentity(right); + return Long.compareUnsigned(leftIdentity, rightIdentity); + } + + private static String normalize(String structureKey) { + return structureKey == null ? "" : structureKey.toLowerCase(Locale.ROOT); + } + +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureStartPlan.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureStartPlan.java new file mode 100644 index 000000000..9c188c02e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureStartPlan.java @@ -0,0 +1,13 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisNativeStructure; +import art.arcane.iris.engine.object.IrisStructurePlacement; + +public record NativeStructureStartPlan( + IrisStructurePlacement placement, + IrisNativeStructure source, + int chunkX, + int chunkZ, + int baseY +) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureVolume.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureVolume.java new file mode 100644 index 000000000..a277630ef --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureVolume.java @@ -0,0 +1,105 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.volmlib.util.collection.KList; + +/** + * World-space axis-aligned bounds of one native structure piece. Volumes are resolved from seed, registry and + * pack policy alone so the same query answers identically regardless of which chunks exist. + */ +public record NativeStructureVolume( + String structure, + int minX, + int minY, + int minZ, + int maxX, + int maxY, + int maxZ +) { + // Shared empty sentinel handed to every engine and memoized by the volume caches; a silent + // add()/clear() here would corrupt them all globally, so mutation fails loudly instead. + public static final KList NONE = new KList<>() { + @Override + public boolean add(NativeStructureVolume volume) { + throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable"); + } + + @Override + public void add(int index, NativeStructureVolume volume) { + throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable"); + } + + @Override + public boolean addAll(java.util.Collection volumes) { + throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable"); + } + + @Override + public boolean addAll(int index, java.util.Collection volumes) { + throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable"); + } + + @Override + public NativeStructureVolume remove(int index) { + throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable"); + } + + @Override + public boolean remove(Object volume) { + throw new UnsupportedOperationException("NativeStructureVolume.NONE is immutable"); + } + }; + + public static NativeStructureVolume of(String structure, int aX, int aY, int aZ, int bX, int bY, int bZ) { + return new NativeStructureVolume( + structure, + Math.min(aX, bX), + Math.min(aY, bY), + Math.min(aZ, bZ), + Math.max(aX, bX), + Math.max(aY, bY), + Math.max(aZ, bZ)); + } + + public boolean intersectsRect(int rectMinX, int rectMinZ, int rectMaxX, int rectMaxZ) { + return maxX >= rectMinX && minX <= rectMaxX && maxZ >= rectMinZ && minZ <= rectMaxZ; + } + + public boolean intersects(int boxMinX, int boxMinY, int boxMinZ, int boxMaxX, int boxMaxY, int boxMaxZ) { + return maxX >= boxMinX && minX <= boxMaxX + && maxY >= boxMinY && minY <= boxMaxY + && maxZ >= boxMinZ && minZ <= boxMaxZ; + } + + public boolean contains(int x, int y, int z) { + return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ; + } + + public boolean containsWithin(int x, int y, int z, int margin) { + return x >= minX - margin && x <= maxX + margin + && y >= minY - margin && y <= maxY + margin + && z >= minZ - margin && z <= maxZ + margin; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureVolumeMemo.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureVolumeMemo.java new file mode 100644 index 000000000..c48ac3357 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureVolumeMemo.java @@ -0,0 +1,94 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.volmlib.util.collection.KList; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; + +/** + * Chunk keyed memo in front of the platform volume hook. One chunk's object pass fires the same rect query dozens of + * times, so the hook is asked once per chunk column and every later object reuses that answer. Long keyed so the + * overwhelmingly common "no native structure anywhere near" lookup allocates nothing at all. + */ +public final class NativeStructureVolumeMemo { + private static final int MAX_CACHED_CHUNKS = 2_048; + + private final Long2ObjectLinkedOpenHashMap> chunks = + new Long2ObjectLinkedOpenHashMap<>(); + + public KList volumes(Engine engine, EnginePlatformHooks hooks, + int minX, int minZ, int maxX, int maxZ) { + if (hooks == null) { + return NativeStructureVolume.NONE; + } + + int fromChunkX = Math.min(minX, maxX) >> 4; + int toChunkX = Math.max(minX, maxX) >> 4; + int fromChunkZ = Math.min(minZ, maxZ) >> 4; + int toChunkZ = Math.max(minZ, maxZ) >> 4; + KList matches = null; + for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) { + for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) { + for (NativeStructureVolume volume : chunkVolumes(engine, hooks, chunkX, chunkZ)) { + if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) { + continue; + } + if (matches == null) { + matches = new KList<>(); + } + if (!matches.contains(volume)) { + matches.add(volume); + } + } + } + } + return matches == null ? NativeStructureVolume.NONE : matches; + } + + public void clear() { + synchronized (chunks) { + chunks.clear(); + } + } + + private KList chunkVolumes(Engine engine, EnginePlatformHooks hooks, + int chunkX, int chunkZ) { + long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL); + synchronized (chunks) { + KList hit = chunks.getAndMoveToFirst(key); + if (hit != null) { + return hit; + } + } + + int minX = chunkX << 4; + int minZ = chunkZ << 4; + KList resolved = hooks.nativeStructureVolumes(engine, minX, minZ, minX + 15, minZ + 15); + if (resolved == null) { + resolved = NativeStructureVolume.NONE; + } + synchronized (chunks) { + chunks.putAndMoveToFirst(key, resolved); + while (chunks.size() > MAX_CACHED_CHUNKS) { + chunks.removeLast(); + } + } + return resolved; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/PlacedObject.java b/core/src/main/java/art/arcane/iris/engine/framework/PlacedObject.java similarity index 87% rename from core/src/main/java/com/volmit/iris/engine/framework/PlacedObject.java rename to core/src/main/java/art/arcane/iris/engine/framework/PlacedObject.java index 31cdf1b59..c72d0d080 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/PlacedObject.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/PlacedObject.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; -import com.volmit.iris.engine.object.IrisObject; -import com.volmit.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectPlacement; import lombok.AllArgsConstructor; import lombok.Data; import org.jetbrains.annotations.Nullable; diff --git a/core/src/main/java/art/arcane/iris/engine/framework/PlacedStructurePiece.java b/core/src/main/java/art/arcane/iris/engine/framework/PlacedStructurePiece.java new file mode 100644 index 000000000..7756b271b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/PlacedStructurePiece.java @@ -0,0 +1,54 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PlacedStructurePiece { + private final IrisJigsawPiece piece; + private final IrisObject object; + private final int x; + private final int y; + private final int z; + private final IrisObjectRotation rotation; + private final int minX; + private final int minY; + private final int minZ; + private final int maxX; + private final int maxY; + private final int maxZ; + + public boolean intersects(PlacedStructurePiece o) { + long maxExclusiveX = (long) maxX + 1L; + long maxExclusiveY = (long) maxY + 1L; + long maxExclusiveZ = (long) maxZ + 1L; + long otherMaxExclusiveX = (long) o.maxX + 1L; + long otherMaxExclusiveY = (long) o.maxY + 1L; + long otherMaxExclusiveZ = (long) o.maxZ + 1L; + return minX < otherMaxExclusiveX && maxExclusiveX > o.minX + && minY < otherMaxExclusiveY && maxExclusiveY > o.minY + && minZ < otherMaxExclusiveZ && maxExclusiveZ > o.minZ; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java b/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java new file mode 100644 index 000000000..2319d965f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java @@ -0,0 +1,31 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import java.util.concurrent.ExecutorService; + +public interface PreservationRegistry { + void register(Thread thread); + + void register(ExecutorService service); + + void registerCache(MeteredCache cache); + + void dereference(); +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/SeedManager.java b/core/src/main/java/art/arcane/iris/engine/framework/SeedManager.java similarity index 94% rename from core/src/main/java/com/volmit/iris/engine/framework/SeedManager.java rename to core/src/main/java/art/arcane/iris/engine/framework/SeedManager.java index b1544b38a..def691b12 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/SeedManager.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/SeedManager.java @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; -import com.volmit.iris.engine.object.NoiseStyle; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; +import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; import lombok.AccessLevel; import lombok.Data; import lombok.Setter; @@ -46,12 +46,12 @@ public class SeedManager { private final long decorator; private final long terrain; private final long spawn; - private final long jigsaw; private final long carve; private final long deposit; private final long post; private final long bodies; private final long mode; + private final long loot; @Setter(AccessLevel.NONE) private long fullMixedSeed; @@ -71,12 +71,12 @@ public class SeedManager { decorator = of("decorator"); terrain = of("terrain"); spawn = of("spawn"); - jigsaw = of("jigsaw"); carve = of("carve"); deposit = of("deposit"); post = of("post"); bodies = of("bodies"); mode = of("mode"); + loot = of("loot"); } private long of(String name) { diff --git a/core/src/main/java/com/volmit/iris/engine/framework/Staged.java b/core/src/main/java/art/arcane/iris/engine/framework/Staged.java similarity index 91% rename from core/src/main/java/com/volmit/iris/engine/framework/Staged.java rename to core/src/main/java/art/arcane/iris/engine/framework/Staged.java index 52a8b8e07..03d98ed6b 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/Staged.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Staged.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework; +package art.arcane.iris.engine.framework; -import com.volmit.iris.util.collection.KList; +import art.arcane.volmlib.util.collection.KList; public interface Staged { KList getStages(); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructureAssembler.java b/core/src/main/java/art/arcane/iris/engine/framework/StructureAssembler.java new file mode 100644 index 000000000..3ff75d307 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructureAssembler.java @@ -0,0 +1,1357 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.structure.CompiledStructureGraph; +import art.arcane.iris.engine.framework.structure.JigsawPoolSelection; +import art.arcane.iris.engine.framework.structure.PlanarJigsawWorkcellResolver; +import art.arcane.iris.engine.framework.structure.StructureAssemblyResult; +import art.arcane.iris.engine.framework.structure.StructureAssemblyStatus; +import art.arcane.iris.engine.framework.structure.StructureGraphCatalog; +import art.arcane.iris.engine.framework.structure.StructureGraphCompilation; +import art.arcane.iris.engine.framework.structure.StructureGraphCompiler; +import art.arcane.iris.engine.framework.structure.StructureGraphDiagnostic; +import art.arcane.iris.engine.framework.structure.StructureGraphResolver; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawBranchFailurePolicy; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawMode; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPieceEntry; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.JigsawJoint; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public final class StructureAssembler { + private static final int HARD_PIECE_CAP = 512; + private static final int MAX_DEPTH = 30; + private static final int MAX_SIZE_CHUNKS = 32; + private static final int[] NO_ROTATION = {0}; + private static final int[] Y_DEGREES = {0, 90, 180, 270}; + + private final StructureGraphResolver resolver; + private final CompiledStructureGraph graph; + private final IrisStructure structure; + private final int originX; + private final int originY; + private final int originZ; + private final int radius; + private final Map + planarWorkcells; + + private StructureAssembler(AssemblyOptions options) { + this.resolver = options.resolver(); + this.graph = options.graph(); + this.structure = options.structure(); + this.originX = options.origin().getX(); + this.originY = options.origin().getY(); + this.originZ = options.origin().getZ(); + this.planarWorkcells = this.structure.resolvedMode() == IrisJigsawMode.PLANAR_JIGSAW + ? PlanarJigsawWorkcellResolver.resolve(this.structure) + : Map.of(); + this.radius = this.structure.getMaxSizeChunks() >= 1 + && this.structure.getMaxSizeChunks() <= MAX_SIZE_CHUNKS + ? this.structure.getMaxSizeChunks() * 16 : 0; + } + + public static StructureAssembler forData(IrisData data, IrisStructure structure, IrisPosition origin) { + IrisData activeData = Objects.requireNonNull(data, "Structure assembly data must not be null"); + IrisStructure activeStructure = Objects.requireNonNull( + structure, "Structure assembly structure must not be null"); + return forCompilation(StructureGraphCatalog.compile(activeData, activeStructure), origin); + } + + public static StructureAssembler forResolver(StructureGraphResolver resolver, IrisStructure structure, + IrisPosition origin) { + StructureGraphResolver activeResolver = Objects.requireNonNull( + resolver, "Structure assembly resolver must not be null"); + IrisStructure activeStructure = Objects.requireNonNull( + structure, "Structure assembly structure must not be null"); + return forCompilation(StructureGraphCompiler.compile(activeStructure, activeResolver), origin); + } + + public static StructureAssembler forCompilation(StructureGraphCompilation compilation, IrisPosition origin) { + StructureGraphCompilation activeCompilation = Objects.requireNonNull( + compilation, "Structure graph compilation must not be null"); + if (activeCompilation.hasErrors()) { + throw invalidGraph(activeCompilation); + } + return new StructureAssembler(new AssemblyOptions( + StructureGraphResolver.forCompiledGraph(activeCompilation.getGraph()), + activeCompilation.getGraph(), activeCompilation.getGraph().getStructure(), origin)); + } + + public StructureAssemblyResult assemble(RNG rng) { + Objects.requireNonNull(rng, "Structure assembly RNG must not be null"); + if (radius == 0) { + throw new IllegalStateException("Structure '" + structure.getLoadKey() + + "' has maxSizeChunks outside 1.." + MAX_SIZE_CHUNKS); + } + if (structure.getMaxDepth() < 1 || structure.getMaxDepth() > MAX_DEPTH) { + throw new IllegalStateException("Structure '" + structure.getLoadKey() + + "' has maxDepth outside 1.." + MAX_DEPTH); + } + String startPoolKey = normalize(structure.getStartPool()); + IrisJigsawPool startPool = resolver.loadPool(startPoolKey); + if (startPool == null || startPool.getPieces() == null || startPool.getPieces().isEmpty()) { + throw new IllegalStateException("Structure '" + structure.getLoadKey() + + "' has no resolvable start pool '" + startPoolKey + "'"); + } + + KList placed = new KList<>(); + Deque pending = new ArrayDeque<>(); + Map placementCounts = new HashMap<>(); + Map> consumedConnectors = new IdentityHashMap<>(); + Map> openConnectors = + new IdentityHashMap<>(); + String selectedTheme = JigsawPoolSelection.selectTheme(structure, rng); + + List eligibleStartEntries = eligibleEntries( + startPool, selectedTheme, 0, placementCounts, true, false); + if (eligibleStartEntries.isEmpty()) { + return StructureAssemblyResult.failed( + StructureAssemblyStatus.FAILED_RULES, + placed, + selectedTheme, + "The start pool has no entry eligible for theme '" + themeLabel(selectedTheme) + + "' at depth zero"); + } + List startEntries = chanceEligibleEntries(eligibleStartEntries, rng); + if (startEntries.isEmpty()) { + return StructureAssemblyResult.intentionalEmpty( + selectedTheme, + "No start-pool membership passed its chance gate"); + } + IrisJigsawPieceEntry startEntry = weightedPick(startEntries, rng); + if (startEntry == null) { + throw new IllegalStateException("Structure '" + structure.getLoadKey() + + "' start pool has no positively weighted entry"); + } + if (startEntry.isEmpty()) { + return StructureAssemblyResult.intentionalEmpty( + selectedTheme, + "The selected start-pool membership intentionally produces no structure"); + } + String startPieceKey = normalize(startEntry.getPiece()); + IrisJigsawPiece startPiece = resolver.loadPiece(startPieceKey); + if (startPiece == null) { + throw new IllegalStateException("Structure '" + structure.getLoadKey() + + "' start pool references missing piece '" + startPieceKey + "'"); + } + String startObjectKey = normalize(startPiece.getObject()); + IrisObject startObject = resolver.loadObject(startObjectKey); + if (startObject == null) { + throw new IllegalStateException("Structure '" + structure.getLoadKey() + + "' start piece references missing object '" + startObjectKey + "'"); + } + if (startPiece.getConnectors() == null) { + throw new IllegalStateException("Structure '" + structure.getLoadKey() + + "' start piece has no connector list"); + } + + int startRotY = startPiece.isRotatable() ? Y_DEGREES[rng.i(Y_DEGREES.length)] : 0; + IrisObjectRotation startRot = IrisObjectRotation.of(0, startRotY, 0); + PlacedStructurePiece start = build(startPiece, startObject, originX, originY, originZ, startRot); + placed.add(start); + registerPlacedPiece(consumedConnectors, start, null); + incrementPlacement(placementCounts, startPieceKey); + if (!startPiece.resolvedRules().isTerminal()) { + enqueuePieceExpansion(pending, openConnectors, start, startObject, 0, 1, null, 0); + } + + while (!pending.isEmpty()) { + PieceExpansion expansion = removeNextPieceExpansion(pending); + for (OpenConnector connector : expansion.connectors()) { + if (isConsumed(consumedConnectors, connector)) { + continue; + } + if (placed.size() >= HARD_PIECE_CAP) { + return StructureAssemblyResult.failed( + StructureAssemblyStatus.HARD_CAP, + placed, + selectedTheme, + "Exceeded the hard piece cap of " + HARD_PIECE_CAP + + " with queued piece expansions unresolved"); + } + int depth = connector.depth; + if (depth > structure.getMaxDepth()) { + continue; + } + + IrisJigsawPool pool = resolver.loadPool(connector.pool); + if (pool == null) { + throw new IllegalStateException("Structure '" + structure.getLoadKey() + + "' references missing connector pool '" + connector.pool + "'"); + } + + AttachmentResult attachment = attachOne( + pending, + placed, + placementCounts, + consumedConnectors, + openConnectors, + pool, + connector, + depth, + selectedTheme, + rng); + consume(consumedConnectors, connector.sourcePiece(), connector.sourceConnector()); + if (attachment.state() == AttachmentState.FAILED) { + return StructureAssemblyResult.failed( + StructureAssemblyStatus.FAILED_UNCAPPED, + placed, + selectedTheme, + attachment.detail()); + } + } + } + String unsatisfiedRule = unsatisfiedMinimumPlacement(placementCounts, selectedTheme); + if (!unsatisfiedRule.isEmpty()) { + return StructureAssemblyResult.failed( + StructureAssemblyStatus.FAILED_RULES, + placed, + selectedTheme, + unsatisfiedRule); + } + return StructureAssemblyResult.complete(placed, selectedTheme); + } + + private AttachmentResult attachOne( + Deque pending, + KList placed, + Map placementCounts, + Map> consumedConnectors, + Map> openConnectors, + IrisJigsawPool pool, + OpenConnector connector, + int depth, + String selectedTheme, + RNG rng + ) { + if (pool.getPieces() == null) { + throw assemblyFailure("connector pool '" + connector.pool + "' has no piece list"); + } + if (closeAgainstPlacedNeighbor(placed, consumedConnectors, connector)) { + return AttachmentResult.success(AttachmentState.CLOSED); + } + boolean fallbackRequired = pool.requiresFallback(structure.isRequireCaps()); + if (depth < structure.getMaxDepth() && !pool.getPieces().isEmpty()) { + AttachmentState primary = attachFromPool( + pending, + placed, + placementCounts, + consumedConnectors, + openConnectors, + pool, + connector, + selectedTheme, + false, + !fallbackRequired, + rng); + if (primary != AttachmentState.NO_MATCH) { + return AttachmentResult.success(primary); + } + } else if (pool.getPieces().isEmpty() && !fallbackRequired) { + return AttachmentResult.success(AttachmentState.TERMINATED); + } + String fallbackKey = JigsawPoolSelection.directFallbackKey(pool); + if (fallbackKey.isEmpty()) { + if (branchFailureRequiresAssemblyFailure(fallbackRequired, depth)) { + return AttachmentResult.failed( + "Connector pool '" + connector.pool + + "' could not place a piece and has no direct fallback at " + + connectorLocation(connector)); + } + return AttachmentResult.success(AttachmentState.TERMINATED); + } + IrisJigsawPool fallback = resolver.loadPool(fallbackKey); + if (fallback == null) { + throw assemblyFailure("references missing authored fallback pool '" + fallbackKey + "'"); + } + if (fallback.getPieces() == null) { + throw assemblyFailure("authored fallback pool '" + fallbackKey + "' has no piece list"); + } + if (fallback.getPieces().isEmpty()) { + return fallbackRequired + ? AttachmentResult.failed( + "Required fallback pool '" + fallbackKey + "' has no physical terminal piece") + : AttachmentResult.success(AttachmentState.TERMINATED); + } + AttachmentState fallbackState = attachFromPool( + pending, + placed, + placementCounts, + consumedConnectors, + openConnectors, + fallback, + connector, + selectedTheme, + fallbackRequired, + !fallbackRequired, + rng); + if (fallbackState != AttachmentState.NO_MATCH) { + return AttachmentResult.success(fallbackState); + } + if (branchFailureRequiresAssemblyFailure(fallbackRequired, depth)) { + return AttachmentResult.failed( + "Direct fallback pool '" + fallbackKey + "' could not place" + + (fallbackRequired ? " a required terminal piece" : " a compatible piece") + + " at " + connectorLocation(connector)); + } + return AttachmentResult.success(AttachmentState.TERMINATED); + } + + private boolean branchFailureRequiresAssemblyFailure(boolean fallbackRequired, int depth) { + return fallbackRequired + || depth < structure.getMaxDepth() + && structure.resolvedBranchFailurePolicy() == IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY; + } + + private AttachmentState attachFromPool( + Deque pending, + KList placed, + Map placementCounts, + Map> consumedConnectors, + Map> openConnectors, + IrisJigsawPool pool, + OpenConnector connector, + String selectedTheme, + boolean terminalOnly, + boolean allowEmpty, + RNG rng + ) { + int ruleDepth = Math.min(connector.pieceDepth, structure.getMaxDepth()); + for (IrisJigsawPieceEntry entry : weightedOrder( + pool, + selectedTheme, + ruleDepth, + placementCounts, + allowEmpty, + terminalOnly, + rng)) { + if (entry.isEmpty()) { + return AttachmentState.TERMINATED; + } + String pieceName = normalize(entry.getPiece()); + if (pieceName.isEmpty()) { + throw assemblyFailure("pool contains a weighted entry without a piece key"); + } + IrisJigsawPiece piece = resolver.loadPiece(pieceName); + if (piece == null) { + throw assemblyFailure("pool references missing piece '" + pieceName + "'"); + } + String objectKey = normalize(piece.getObject()); + if (objectKey.isEmpty()) { + throw assemblyFailure("piece '" + pieceName + "' has no object key"); + } + IrisObject object = resolver.loadObject(objectKey); + if (object == null) { + throw assemblyFailure("piece '" + pieceName + "' references missing object '" + + objectKey + "'"); + } + if (piece.getConnectors() == null) { + throw assemblyFailure("piece '" + pieceName + "' has no connector list"); + } + + for (IrisJigsawConnector pieceConnector : piece.getConnectors()) { + requireConnector(pieceConnector, pieceName); + } + List candidateConnectors = orderedConnectors(piece.getConnectors(), null); + for (IrisJigsawConnector cb : candidateConnectors) { + if (connector.targetName == null) { + throw assemblyFailure("open connector from pool '" + connector.pool + "' has no target name"); + } + if (!Objects.equals(connector.channel, cb.getChannel())) { + continue; + } + if (!cb.getName().equals(connector.targetName)) { + continue; + } + + IrisDirection needed = connector.facing.reverse(); + int[] rotations = piece.isRotatable() ? rotationCandidates(connector.joint, rng) : NO_ROTATION; + for (int yDeg : rotations) { + IrisObjectRotation rot = IrisObjectRotation.of(0, yDeg, 0); + IrisDirection rotatedFace = rot.rotate(cb.getDirection()); + if (rotatedFace != needed) { + continue; + } + if (connector.joint == JigsawJoint.ALIGNED && rot.rotate(cb.getTop()) != connector.top) { + continue; + } + + IrisPosition rcr = connectorOffset(object, cb, rot); + int wcx = connector.wx + connector.facing.x(); + int wcy = connector.wy + connector.facing.y(); + int wcz = connector.wz + connector.facing.z(); + int px = wcx - rcr.getX(); + int py = wcy - rcr.getY(); + int pz = wcz - rcr.getZ(); + + PlacedStructurePiece candidate = build(piece, object, px, py, pz, rot); + if (!withinRadius(candidate) || collides(placed, candidate)) { + continue; + } + List closures = requiredClosures( + placed, + consumedConnectors, + connector, + candidate, + cb); + if (closures == null) { + continue; + } + if (!capReservationsRemainViable( + placed, + placementCounts, + consumedConnectors, + openConnectors, + connector, + candidate, + cb, + closures, + pieceName, + selectedTheme)) { + continue; + } + + placed.add(candidate); + registerPlacedPiece(consumedConnectors, candidate, cb); + for (ConnectorClosure closure : closures) { + consume(consumedConnectors, closure.existingPiece(), closure.existingConnector()); + consume(consumedConnectors, candidate, closure.candidateConnector()); + } + incrementPlacement(placementCounts, pieceName); + if (!piece.resolvedRules().isTerminal() && connector.depth < structure.getMaxDepth()) { + enqueuePieceExpansion( + pending, + openConnectors, + candidate, + object, + connector.depth + 1, + connector.pieceDepth + 1, + cb, + connector.placementPriority); + } + return AttachmentState.ATTACHED; + } + } + } + return AttachmentState.NO_MATCH; + } + + private boolean closeAgainstPlacedNeighbor( + KList placed, + Map> consumedConnectors, + OpenConnector source + ) { + int targetX = source.wx() + source.facing().x(); + int targetY = source.wy() + source.facing().y(); + int targetZ = source.wz() + source.facing().z(); + for (PlacedStructurePiece candidatePiece : placed) { + if (candidatePiece == source.sourcePiece() || candidatePiece.getPiece().getConnectors() == null) { + continue; + } + for (IrisJigsawConnector candidate : candidatePiece.getPiece().getConnectors()) { + if (isConsumed(consumedConnectors, candidatePiece, candidate) + || !existingConnectorMatches(source, candidatePiece, candidate, + targetX, targetY, targetZ)) { + continue; + } + consume(consumedConnectors, candidatePiece, candidate); + return true; + } + } + return false; + } + + private List requiredClosures( + KList placed, + Map> consumedConnectors, + OpenConnector attachmentSource, + PlacedStructurePiece candidatePiece, + IrisJigsawConnector attachmentConnector + ) { + if (!structure.isRequireCaps()) { + return List.of(); + } + List closures = new ArrayList<>(); + Set matchedCandidate = Collections.newSetFromMap(new IdentityHashMap<>()); + Map> matchedExisting = new IdentityHashMap<>(); + matchedCandidate.add(attachmentConnector); + matchedExisting.computeIfAbsent( + attachmentSource.sourcePiece(), + ignored -> Collections.newSetFromMap(new IdentityHashMap<>())) + .add(attachmentSource.sourceConnector()); + + for (PlacedStructurePiece existingPiece : placed) { + for (IrisJigsawConnector existingConnector : existingPiece.getPiece().getConnectors()) { + if (normalize(existingConnector.getPool()).isEmpty() + || isConsumed(consumedConnectors, existingPiece, existingConnector) + || isMatched(matchedExisting, existingPiece, existingConnector)) { + continue; + } + IrisPosition target = connectorTarget(existingPiece, existingConnector); + if (!contains(candidatePiece, target)) { + continue; + } + IrisJigsawConnector matchingCandidate = matchingConnector( + existingPiece, + existingConnector, + candidatePiece, + matchedCandidate); + if (matchingCandidate == null) { + return null; + } + matchedCandidate.add(matchingCandidate); + matchedExisting.computeIfAbsent( + existingPiece, + ignored -> Collections.newSetFromMap(new IdentityHashMap<>())) + .add(existingConnector); + closures.add(new ConnectorClosure(existingPiece, existingConnector, matchingCandidate)); + } + } + + if (candidatePiece.getPiece().resolvedRules().isTerminal()) { + return closures; + } + for (IrisJigsawConnector candidateConnector : candidatePiece.getPiece().getConnectors()) { + if (matchedCandidate.contains(candidateConnector) + || normalize(candidateConnector.getPool()).isEmpty()) { + continue; + } + IrisPosition target = connectorTarget(candidatePiece, candidateConnector); + PlacedStructurePiece occupied = containingPiece(placed, target); + if (occupied == null) { + continue; + } + Set unavailable = matchedExisting.getOrDefault(occupied, Set.of()); + IrisJigsawConnector matchingExisting = matchingConnector( + candidatePiece, + candidateConnector, + occupied, + unavailable); + if (matchingExisting == null + || isConsumed(consumedConnectors, occupied, matchingExisting)) { + return null; + } + matchedCandidate.add(candidateConnector); + matchedExisting.computeIfAbsent( + occupied, + ignored -> Collections.newSetFromMap(new IdentityHashMap<>())) + .add(matchingExisting); + closures.add(new ConnectorClosure(occupied, matchingExisting, candidateConnector)); + } + return closures; + } + + private boolean capReservationsRemainViable( + KList placed, + Map placementCounts, + Map> consumedConnectors, + Map> openConnectors, + OpenConnector attachmentSource, + PlacedStructurePiece candidatePiece, + IrisJigsawConnector attachmentConnector, + List closures, + String candidatePieceKey, + String selectedTheme + ) { + if (!structure.isRequireCaps() || structure.resolvedMode() != IrisJigsawMode.PLANAR_JIGSAW) { + return true; + } + List unresolved = new ArrayList<>(); + for (PlacedStructurePiece placedPiece : placed) { + Map definitions = openConnectors.get(placedPiece); + if (definitions == null) { + continue; + } + for (IrisJigsawConnector connector : placedPiece.getPiece().getConnectors()) { + OpenConnector open = definitions.get(connector); + if (open == null + || isConsumed(consumedConnectors, placedPiece, connector) + || connectorWillBeConsumed( + placedPiece, connector, attachmentSource, candidatePiece, attachmentConnector, closures)) { + continue; + } + unresolved.add(open); + } + } + if (!candidatePiece.getPiece().resolvedRules().isTerminal() + && attachmentSource.depth() < structure.getMaxDepth()) { + for (IrisJigsawConnector connector : candidatePiece.getPiece().getConnectors()) { + if (normalize(connector.getPool()).isEmpty() + || connectorWillBeConsumed( + candidatePiece, connector, attachmentSource, candidatePiece, attachmentConnector, closures)) { + continue; + } + unresolved.add(openConnector( + candidatePiece, + candidatePiece.getObject(), + connector, + attachmentSource.depth() + 1, + attachmentSource.pieceDepth() + 1)); + } + } + + List occupied = new ArrayList<>(placed.size() + unresolved.size() + 1); + occupied.addAll(placed); + occupied.add(candidatePiece); + Map reservedCounts = new HashMap<>(placementCounts); + incrementPlacement(reservedCounts, candidatePieceKey); + for (OpenConnector open : unresolved) { + CapReservation reservation = reserveTerminalCap( + open, + occupied, + reservedCounts, + selectedTheme); + if (reservation == null) { + return false; + } + occupied.add(reservation.piece()); + incrementPlacement(reservedCounts, reservation.pieceKey()); + } + return true; + } + + private boolean connectorWillBeConsumed( + PlacedStructurePiece piece, + IrisJigsawConnector connector, + OpenConnector attachmentSource, + PlacedStructurePiece candidatePiece, + IrisJigsawConnector attachmentConnector, + List closures + ) { + if (piece == attachmentSource.sourcePiece() && connector == attachmentSource.sourceConnector() + || piece == candidatePiece && connector == attachmentConnector + || piece == candidatePiece && candidatePiece.getPiece().resolvedRules().isTerminal()) { + return true; + } + for (ConnectorClosure closure : closures) { + if (piece == closure.existingPiece() && connector == closure.existingConnector() + || piece == candidatePiece && connector == closure.candidateConnector()) { + return true; + } + } + return false; + } + + private CapReservation reserveTerminalCap( + OpenConnector source, + List occupied, + Map placementCounts, + String selectedTheme + ) { + IrisJigsawPool sourcePool = resolver.loadPool(source.pool()); + if (sourcePool == null) { + return null; + } + String fallbackKey = JigsawPoolSelection.directFallbackKey(sourcePool); + if (fallbackKey.isEmpty()) { + return null; + } + IrisJigsawPool fallback = resolver.loadPool(fallbackKey); + if (fallback == null || fallback.getPieces() == null) { + return null; + } + int ruleDepth = Math.min(source.pieceDepth(), structure.getMaxDepth()); + for (IrisJigsawPieceEntry entry : eligibleEntries( + fallback, + selectedTheme, + ruleDepth, + placementCounts, + false, + true)) { + if (entry.isEmpty() || entry.getChance() <= 0D) { + continue; + } + String pieceKey = normalize(entry.getPiece()); + IrisJigsawPiece piece = resolver.loadPiece(pieceKey); + if (piece == null || piece.getConnectors() == null) { + continue; + } + IrisObject object = resolver.loadObject(normalize(piece.getObject())); + if (object == null) { + continue; + } + for (IrisJigsawConnector connector : orderedConnectors(piece.getConnectors(), null)) { + if (!Objects.equals(source.channel(), connector.getChannel()) + || !Objects.equals(source.targetName(), connector.getName())) { + continue; + } + int[] rotations = piece.isRotatable() ? Y_DEGREES : NO_ROTATION; + for (int yDegrees : rotations) { + IrisObjectRotation rotation = IrisObjectRotation.of(0, yDegrees, 0); + if (rotation.rotate(connector.getDirection()) != source.facing().reverse() + || source.joint() == JigsawJoint.ALIGNED + && rotation.rotate(connector.getTop()) != source.top()) { + continue; + } + IrisPosition offset = connectorOffset(object, connector, rotation); + int logicalX = source.wx() + source.facing().x() - offset.getX(); + int logicalY = source.wy() + source.facing().y() - offset.getY(); + int logicalZ = source.wz() + source.facing().z() - offset.getZ(); + PlacedStructurePiece reservation = build( + piece, + object, + logicalX, + logicalY, + logicalZ, + rotation); + if (withinRadius(reservation) && !collides(occupied, reservation)) { + return new CapReservation(pieceKey, reservation); + } + } + } + } + return null; + } + + private IrisJigsawConnector matchingConnector( + PlacedStructurePiece sourcePiece, + IrisJigsawConnector sourceConnector, + PlacedStructurePiece targetPiece, + Set unavailable + ) { + IrisPosition target = connectorTarget(sourcePiece, sourceConnector); + IrisDirection sourceFacing = sourcePiece.getRotation().rotate(sourceConnector.getDirection()); + IrisDirection sourceTop = sourcePiece.getRotation().rotate(sourceConnector.getTop()); + for (IrisJigsawConnector candidate : targetPiece.getPiece().getConnectors()) { + if (unavailable.contains(candidate) + || !Objects.equals(sourceConnector.getChannel(), candidate.getChannel()) + || !Objects.equals(sourceConnector.getTargetName(), candidate.getName())) { + continue; + } + IrisPosition candidatePosition = connectorPosition(targetPiece, candidate); + if (!target.equals(candidatePosition) + || targetPiece.getRotation().rotate(candidate.getDirection()) != sourceFacing.reverse()) { + continue; + } + if (sourceConnector.getJoint() != JigsawJoint.ALIGNED + || targetPiece.getRotation().rotate(candidate.getTop()) == sourceTop) { + return candidate; + } + } + return null; + } + + private IrisPosition connectorPosition(PlacedStructurePiece piece, IrisJigsawConnector connector) { + IrisPosition center = logicalCenter(piece); + IrisPosition offset = connectorOffset(piece.getObject(), connector, piece.getRotation()); + return new IrisPosition( + center.getX() + offset.getX(), + center.getY() + offset.getY(), + center.getZ() + offset.getZ()); + } + + private IrisPosition connectorTarget(PlacedStructurePiece piece, IrisJigsawConnector connector) { + IrisPosition position = connectorPosition(piece, connector); + IrisDirection facing = piece.getRotation().rotate(connector.getDirection()); + return new IrisPosition( + position.getX() + facing.x(), + position.getY() + facing.y(), + position.getZ() + facing.z()); + } + + private PlacedStructurePiece containingPiece( + KList pieces, + IrisPosition position + ) { + for (PlacedStructurePiece piece : pieces) { + if (contains(piece, position)) { + return piece; + } + } + return null; + } + + private boolean contains(PlacedStructurePiece piece, IrisPosition position) { + return position.getX() >= piece.getMinX() && position.getX() <= piece.getMaxX() + && position.getY() >= piece.getMinY() && position.getY() <= piece.getMaxY() + && position.getZ() >= piece.getMinZ() && position.getZ() <= piece.getMaxZ(); + } + + private boolean isMatched( + Map> matched, + PlacedStructurePiece piece, + IrisJigsawConnector connector + ) { + Set connectors = matched.get(piece); + return connectors != null && connectors.contains(connector); + } + + private boolean existingConnectorMatches( + OpenConnector source, + PlacedStructurePiece candidatePiece, + IrisJigsawConnector candidate, + int targetX, + int targetY, + int targetZ + ) { + requireConnector(candidate, candidatePiece.getObject().getLoadKey()); + if (!Objects.equals(source.channel(), candidate.getChannel()) + || !Objects.equals(source.targetName(), candidate.getName())) { + return false; + } + IrisPosition rotated = connectorOffset( + candidatePiece.getObject(), candidate, candidatePiece.getRotation()); + IrisPosition center = logicalCenter(candidatePiece); + if (center.getX() + rotated.getX() != targetX + || center.getY() + rotated.getY() != targetY + || center.getZ() + rotated.getZ() != targetZ) { + return false; + } + if (candidatePiece.getRotation().rotate(candidate.getDirection()) != source.facing().reverse()) { + return false; + } + return source.joint() != JigsawJoint.ALIGNED + || candidatePiece.getRotation().rotate(candidate.getTop()) == source.top(); + } + + private void registerPlacedPiece( + Map> consumedConnectors, + PlacedStructurePiece piece, + IrisJigsawConnector attachment + ) { + Set consumed = Collections.newSetFromMap(new IdentityHashMap<>()); + if (piece.getPiece().resolvedRules().isTerminal()) { + consumed.addAll(piece.getPiece().getConnectors()); + } else if (attachment != null) { + consumed.add(attachment); + } + consumedConnectors.put(piece, consumed); + } + + private boolean isConsumed( + Map> consumedConnectors, + OpenConnector connector + ) { + return isConsumed(consumedConnectors, connector.sourcePiece(), connector.sourceConnector()); + } + + private boolean isConsumed( + Map> consumedConnectors, + PlacedStructurePiece piece, + IrisJigsawConnector connector + ) { + Set consumed = consumedConnectors.get(piece); + return consumed != null && consumed.contains(connector); + } + + private void consume( + Map> consumedConnectors, + PlacedStructurePiece piece, + IrisJigsawConnector connector + ) { + Set consumed = consumedConnectors.get(piece); + if (consumed == null) { + throw assemblyFailure("placed piece connector ownership was not registered"); + } + consumed.add(connector); + } + + private void enqueuePieceExpansion( + Deque pending, + Map> openConnectors, + PlacedStructurePiece p, + IrisObject object, int depth, int pieceDepth, IrisJigsawConnector skip, + int placementPriority) { + if (p.getPiece().getConnectors() == null) { + throw assemblyFailure("placed piece for object '" + object.getLoadKey() + "' has no connector list"); + } + List connectors = new ArrayList<>(); + Map definitions = new IdentityHashMap<>(); + for (IrisJigsawConnector con : orderedConnectors(p.getPiece().getConnectors(), skip)) { + requireConnector(con, object.getLoadKey()); + if (con.getPool() == null || con.getPool().isBlank()) { + continue; + } + OpenConnector openConnector = openConnector(p, object, con, depth, pieceDepth); + connectors.add(openConnector); + definitions.put(con, openConnector); + } + openConnectors.put(p, definitions); + if (!connectors.isEmpty()) { + pending.addLast(new PieceExpansion(connectors, placementPriority)); + } + } + + private OpenConnector openConnector( + PlacedStructurePiece piece, + IrisObject object, + IrisJigsawConnector connector, + int depth, + int pieceDepth + ) { + IrisPosition offset = connectorOffset(object, connector, piece.getRotation()); + IrisPosition center = logicalCenter(piece); + return new OpenConnector( + piece, + connector, + center.getX() + offset.getX(), + center.getY() + offset.getY(), + center.getZ() + offset.getZ(), + piece.getRotation().rotate(connector.getDirection()), + piece.getRotation().rotate(connector.getTop()), + normalize(connector.getPool()), + connector.getName(), + connector.getTargetName(), + connector.getChannel(), + connector.getJoint(), + depth, + pieceDepth, + connector.getPlacementPriority()); + } + + private List orderedConnectors(List connectors, + IrisJigsawConnector skip) { + List ordered = new ArrayList<>(connectors.size()); + for (IrisJigsawConnector connector : connectors) { + if (connector != skip) { + ordered.add(connector); + } + } + ordered.sort((first, second) -> Integer.compare( + second.getSelectionPriority(), first.getSelectionPriority())); + return ordered; + } + + private PieceExpansion removeNextPieceExpansion(Deque pending) { + int highestPriority = Integer.MIN_VALUE; + for (PieceExpansion expansion : pending) { + highestPriority = Math.max(highestPriority, expansion.placementPriority()); + } + Iterator iterator = pending.iterator(); + while (iterator.hasNext()) { + PieceExpansion expansion = iterator.next(); + if (expansion.placementPriority() == highestPriority) { + iterator.remove(); + return expansion; + } + } + throw assemblyFailure("piece expansion queue was unexpectedly empty"); + } + + private PlacedStructurePiece build(IrisJigsawPiece piece, IrisObject object, int x, int y, int z, IrisObjectRotation rot) { + int width = Math.max(1, object.getW()); + int height = Math.max(1, object.getH()); + int depth = Math.max(1, object.getD()); + if (structure.resolvedMode() == IrisJigsawMode.PLANAR_JIGSAW) { + IrisPosition dimensions = rotatedDimensions(object, rot); + IrisPosition offset = planarPlacementOffset(object, rot); + int minX = x - dimensions.getX() / 2; + int minY = y - dimensions.getY() / 2; + int minZ = z - dimensions.getZ() / 2; + return new PlacedStructurePiece( + piece, + object, + x + offset.getX(), + y + offset.getY(), + z + offset.getZ(), + rot, + minX, + minY, + minZ, + minX + dimensions.getX() - 1, + minY + dimensions.getY() - 1, + minZ + dimensions.getZ() - 1); + } + int localMinX = -(width / 2); + int localMinY = -(height / 2); + int localMinZ = -(depth / 2); + int localMaxX = localMinX + width - 1; + int localMaxY = localMinY + height - 1; + int localMaxZ = localMinZ + depth - 1; + IrisPosition rotatedMin = rot.rotate(new IrisPosition(localMinX, localMinY, localMinZ), 0, 0, 0); + IrisPosition rotatedMax = rot.rotate(new IrisPosition(localMaxX, localMaxY, localMaxZ), 0, 0, 0); + int minX = Math.min(rotatedMin.getX(), rotatedMax.getX()); + int minY = Math.min(rotatedMin.getY(), rotatedMax.getY()); + int minZ = Math.min(rotatedMin.getZ(), rotatedMax.getZ()); + int maxX = Math.max(rotatedMin.getX(), rotatedMax.getX()); + int maxY = Math.max(rotatedMin.getY(), rotatedMax.getY()); + int maxZ = Math.max(rotatedMin.getZ(), rotatedMax.getZ()); + return new PlacedStructurePiece(piece, object, x, y, z, rot, + x + minX, y + minY, z + minZ, x + maxX, y + maxY, z + maxZ); + } + + private boolean withinRadius(PlacedStructurePiece p) { + return p.getMinX() >= originX - radius && p.getMaxX() <= originX + radius + && p.getMinZ() >= originZ - radius && p.getMaxZ() <= originZ + radius; + } + + private boolean collides(List placed, PlacedStructurePiece candidate) { + if (!candidate.getPiece().isCollidable()) { + return false; + } + for (PlacedStructurePiece p : placed) { + if (p.getPiece().isCollidable() && p.intersects(candidate)) { + return true; + } + } + return false; + } + + private int[] rotationCandidates(JigsawJoint joint, RNG rng) { + if (joint == JigsawJoint.ALIGNED) { + return Y_DEGREES; + } + int[] shuffled = {0, 90, 180, 270}; + for (int i = shuffled.length - 1; i > 0; i--) { + int j = rng.i(i + 1); + int t = shuffled[i]; + shuffled[i] = shuffled[j]; + shuffled[j] = t; + } + return shuffled; + } + + private IrisPosition centerOf(IrisObject object) { + return new IrisPosition(object.getW() / 2, object.getH() / 2, object.getD() / 2); + } + + private IrisPosition connectorOffset( + IrisObject object, + IrisJigsawConnector connector, + IrisObjectRotation rotation + ) { + if (structure.resolvedMode() == IrisJigsawMode.PLANAR_JIGSAW) { + IrisPosition dimensions = rotatedDimensions(object, rotation); + IrisPosition position = IrisJigsawConnector.canonicalPlanarPosition( + dimensions, + rotation.rotate(connector.getDirection())); + return new IrisPosition( + position.getX() - dimensions.getX() / 2, + position.getY() - dimensions.getY() / 2, + position.getZ() - dimensions.getZ() / 2); + } + IrisPosition center = centerOf(object); + IrisPosition relative = new IrisPosition( + connector.getPosition().getX() - center.getX(), + connector.getPosition().getY() - center.getY(), + connector.getPosition().getZ() - center.getZ()); + return rotation.rotate(relative, 0, 0, 0); + } + + private IrisPosition logicalCenter(PlacedStructurePiece piece) { + if (structure.resolvedMode() != IrisJigsawMode.PLANAR_JIGSAW) { + return new IrisPosition(piece.getX(), piece.getY(), piece.getZ()); + } + IrisPosition offset = planarPlacementOffset(piece.getObject(), piece.getRotation()); + return new IrisPosition( + piece.getX() - offset.getX(), + piece.getY() - offset.getY(), + piece.getZ() - offset.getZ()); + } + + private IrisPosition rotatedDimensions(IrisObject object, IrisObjectRotation rotation) { + int width = Math.max(1, object.getW()); + int height = Math.max(1, object.getH()); + int depth = Math.max(1, object.getD()); + return quarterTurns(rotation) % 2 == 0 + ? new IrisPosition(width, height, depth) + : new IrisPosition(depth, height, width); + } + + private IrisPosition planarPlacementOffset(IrisObject object, IrisObjectRotation rotation) { + int width = Math.max(1, object.getW()); + int depth = Math.max(1, object.getD()); + return switch (quarterTurns(rotation)) { + case 0 -> new IrisPosition(); + case 1 -> new IrisPosition(0, 0, width % 2 == 0 ? -1 : 0); + case 2 -> new IrisPosition( + width % 2 == 0 ? -1 : 0, + 0, + depth % 2 == 0 ? -1 : 0); + case 3 -> new IrisPosition(depth % 2 == 0 ? -1 : 0, 0, 0); + default -> throw new IllegalStateException("Unsupported planar quarter-turn count"); + }; + } + + private int quarterTurns(IrisObjectRotation rotation) { + return switch (rotation.rotate(IrisDirection.NORTH_NEGATIVE_Z)) { + case NORTH_NEGATIVE_Z -> 0; + case WEST_NEGATIVE_X -> 1; + case SOUTH_POSITIVE_Z -> 2; + case EAST_POSITIVE_X -> 3; + case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> throw new IllegalStateException( + "Planar jigsaw rotation moved north onto a vertical axis"); + }; + } + + private IrisJigsawPieceEntry weightedPick(List entries, RNG rng) { + long total = 0L; + for (IrisJigsawPieceEntry entry : entries) { + total += entry.getWeight(); + } + if (total <= 0) { + return null; + } + long target = rng.nextLong(total); + for (IrisJigsawPieceEntry entry : entries) { + target -= entry.getWeight(); + if (target < 0) { + return entry; + } + } + return null; + } + + private KList weightedOrder( + IrisJigsawPool pool, + String selectedTheme, + int depth, + Map placementCounts, + boolean allowEmpty, + boolean terminalOnly, + RNG rng + ) { + List eligible = eligibleEntries( + pool, + selectedTheme, + depth, + placementCounts, + allowEmpty, + terminalOnly); + KList remaining = new KList<>(chanceEligibleEntries(eligible, rng)); + KList order = new KList<>(); + while (!remaining.isEmpty()) { + long total = 0L; + for (IrisJigsawPieceEntry e : remaining) { + total += e.getWeight(); + } + long t = rng.nextLong(total); + int idx = 0; + for (int i = 0; i < remaining.size(); i++) { + t -= remaining.get(i).getWeight(); + if (t < 0) { + idx = i; + break; + } + } + order.add(remaining.remove(idx)); + } + return order; + } + + private List eligibleEntries( + IrisJigsawPool pool, + String selectedTheme, + int depth, + Map placementCounts, + boolean allowEmpty, + boolean terminalOnly + ) { + if (pool.getPieces() == null) { + throw assemblyFailure("pool has no piece list"); + } + List eligible = new ArrayList<>(); + List required = new ArrayList<>(); + for (IrisJigsawPieceEntry entry : pool.getPieces()) { + if (entry == null || entry.getWeight() <= 0) { + throw assemblyFailure("pool contains a null or non-positive weighted entry"); + } + if (entry.isEmpty()) { + if (allowEmpty) { + eligible.add(entry); + } + continue; + } + String pieceKey = normalize(entry.getPiece()); + if (pieceKey.isEmpty()) { + throw assemblyFailure("pool contains a weighted entry without a piece key"); + } + IrisJigsawPiece piece = resolver.loadPiece(pieceKey); + if (piece == null) { + throw assemblyFailure("pool references missing piece '" + pieceKey + "'"); + } + int placements = placementCounts.getOrDefault(pieceKey, 0); + if (!pieceEnabled(piece) + || terminalOnly && !piece.resolvedRules().isTerminal() + || !JigsawPoolSelection.pieceEligible(piece, selectedTheme, depth, placements)) { + continue; + } + eligible.add(entry); + if (JigsawPoolSelection.needsMinimumPlacement(piece, placements)) { + required.add(entry); + } + } + return required.isEmpty() ? eligible : required; + } + + private List chanceEligibleEntries( + List entries, + RNG rng + ) { + List eligible = new ArrayList<>(entries.size()); + for (IrisJigsawPieceEntry entry : entries) { + if (JigsawPoolSelection.passesChance(entry, rng)) { + eligible.add(entry); + } + } + return eligible; + } + + private boolean pieceEnabled(IrisJigsawPiece piece) { + return structure.resolvedMode() != IrisJigsawMode.PLANAR_JIGSAW + || PlanarJigsawWorkcellResolver.workcell(planarWorkcells, piece).enabled(); + } + + private void incrementPlacement(Map placementCounts, String pieceKey) { + placementCounts.merge(normalize(pieceKey), 1, Integer::sum); + } + + private String unsatisfiedMinimumPlacement( + Map placementCounts, + String selectedTheme + ) { + for (Map.Entry entry : graph.getPieces().entrySet()) { + IrisJigsawPiece piece = entry.getValue(); + if (!pieceEnabled(piece) || !piece.supportsTheme(selectedTheme)) { + continue; + } + int actual = placementCounts.getOrDefault(entry.getKey(), 0); + int minimum = piece.resolvedRules().getMinimumPlacements(); + if (actual < minimum) { + return "Piece '" + entry.getKey() + "' requires at least " + minimum + + " placement(s) for theme '" + themeLabel(selectedTheme) + + "' but assembled " + actual; + } + } + return ""; + } + + private String themeLabel(String selectedTheme) { + return selectedTheme == null || selectedTheme.isBlank() ? "" : selectedTheme; + } + + private String connectorLocation(OpenConnector connector) { + return connector.wx() + "," + connector.wy() + "," + connector.wz() + + " facing " + connector.facing(); + } + + private String normalize(String value) { + return value == null ? "" : value.trim(); + } + + private void requireConnector(IrisJigsawConnector connector, String pieceKey) { + if (connector == null || connector.getPosition() == null || connector.getDirection() == null + || connector.getTop() == null || connector.getName() == null + || connector.getTargetName() == null || connector.getChannel() == null + || connector.getJoint() == null || connector.getFinalState() == null + || connector.getFinalState().isBlank()) { + throw assemblyFailure("piece '" + pieceKey + "' contains a malformed connector"); + } + } + + private IllegalStateException assemblyFailure(String detail) { + return new IllegalStateException("Structure '" + structure.getLoadKey() + "' assembly failed: " + detail); + } + + private static IllegalStateException invalidGraph(StructureGraphCompilation compilation) { + String structureKey = compilation.getGraph().getStructureKey(); + StringBuilder detail = new StringBuilder(); + for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) { + if (diagnostic.severity() != StructureGraphDiagnostic.Severity.ERROR) { + continue; + } + if (!detail.isEmpty()) { + detail.append("; "); + } + detail.append(diagnostic.message()); + } + return new IllegalStateException("Structure '" + structureKey + + "' assembly rejected its invalid graph: " + detail); + } + + private record OpenConnector(PlacedStructurePiece sourcePiece, IrisJigsawConnector sourceConnector, + int wx, int wy, int wz, IrisDirection facing, IrisDirection top, + String pool, String name, + String targetName, String channel, JigsawJoint joint, int depth, + int pieceDepth, int placementPriority) { + } + + private record PieceExpansion(List connectors, int placementPriority) { + } + + private record ConnectorClosure( + PlacedStructurePiece existingPiece, + IrisJigsawConnector existingConnector, + IrisJigsawConnector candidateConnector + ) { + } + + private record CapReservation(String pieceKey, PlacedStructurePiece piece) { + } + + private enum AttachmentState { + ATTACHED, + CLOSED, + TERMINATED, + NO_MATCH, + FAILED + } + + private record AttachmentResult(AttachmentState state, String detail) { + private AttachmentResult { + Objects.requireNonNull(state); + detail = detail == null ? "" : detail; + } + + private static AttachmentResult success(AttachmentState state) { + if (state == AttachmentState.NO_MATCH || state == AttachmentState.FAILED) { + throw new IllegalArgumentException("A successful attachment requires a resolved state"); + } + return new AttachmentResult(state, ""); + } + + private static AttachmentResult failed(String detail) { + return new AttachmentResult(AttachmentState.FAILED, detail); + } + } + + private record AssemblyOptions(StructureGraphResolver resolver, CompiledStructureGraph graph, + IrisStructure structure, + IrisPosition origin) { + private AssemblyOptions { + Objects.requireNonNull(resolver, "Structure assembly resolver must not be null"); + Objects.requireNonNull(graph, "Compiled structure graph must not be null"); + Objects.requireNonNull(structure, "Structure assembly structure must not be null"); + Objects.requireNonNull(origin, "Structure assembly origin must not be null"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolver.java b/core/src/main/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolver.java new file mode 100644 index 000000000..2c120b114 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolver.java @@ -0,0 +1,359 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisStructureAnchorMode; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterCavern; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.function.IntPredicate; + +public final class StructureCaveAnchorResolver { + private static final int CHUNK_COLUMN_COUNT = 256; + private static final int MAX_ATTEMPTS = 64; + private static final int MAX_SCAN_STEP = 16; + private static final int MAX_CLEARANCE = 64; + private static final int MAX_GENERIC_CENTER_SEARCH = 4096; + private static final byte LIQUID_FORCED_AIR = 3; + + private StructureCaveAnchorResolver() { + } + + public static Anchor resolve(Engine engine, IrisStructurePlacement placement, int chunkX, int chunkZ, RNG rng) { + Objects.requireNonNull(engine, "Cave structure anchor requires an engine"); + Objects.requireNonNull(placement, "Cave structure anchor requires a placement"); + Objects.requireNonNull(rng, "Cave structure anchor requires a random source"); + IrisStructureAnchorMode mode = placement.resolvedAnchor(); + if (!mode.isCave() || engine.getMantle() == null || engine.getMantle().getMantle() == null) { + return null; + } + + int worldMin = engine.getMinHeight() + 1; + int worldMax = engine.getMinHeight() + engine.getHeight() - 2; + int minimumY = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight())); + int maximumY = Math.min(worldMax, Math.max(placement.getMinHeight(), placement.getMaxHeight())); + if (minimumY > maximumY) { + return null; + } + + int attempts = Math.min(MAX_ATTEMPTS, Math.max(1, placement.getCaveAnchorAttempts())); + int scanStep = Math.min(MAX_SCAN_STEP, Math.max(1, placement.getCaveAnchorScanStep())); + int clearance = Math.min(MAX_CLEARANCE, Math.max(1, placement.getCaveMinimumClearance())); + int chunkMinX = chunkX << 4; + int chunkMinZ = chunkZ << 4; + int[] candidateColumns = candidateColumnIndices(rng, attempts); + for (int candidateColumn : candidateColumns) { + int blockX = chunkMinX + (candidateColumn & 15); + int blockZ = chunkMinZ + (candidateColumn >>> 4); + List anchors = anchorsInColumn( + engine, placement, mode, blockX, blockZ, + minimumY, maximumY, scanStep, clearance); + if (!anchors.isEmpty()) { + return new Anchor(blockX, anchors.get(rng.nextInt(anchors.size())), blockZ); + } + } + return null; + } + + static int[] candidateColumnIndices(RNG rng, int attempts) { + Objects.requireNonNull(rng, "Cave structure column selection requires a random source"); + int count = Math.min(MAX_ATTEMPTS, Math.max(1, attempts)); + int start = rng.nextInt(CHUNK_COLUMN_COUNT); + int stride = (rng.nextInt(CHUNK_COLUMN_COUNT / 2) * 2) + 1; + int[] columns = new int[count]; + for (int index = 0; index < count; index++) { + columns[index] = (start + (index * stride)) & (CHUNK_COLUMN_COUNT - 1); + } + return columns; + } + + static List anchorsInColumn( + Engine engine, + IrisStructurePlacement placement, + IrisStructureAnchorMode mode, + int blockX, + int blockZ, + int minimumY, + int maximumY, + int scanStep, + int clearance + ) { + if (mode == IrisStructureAnchorMode.CAVE_CENTER) { + return centerAnchorsInColumn( + engine, placement, blockX, blockZ, + minimumY, maximumY, scanStep, clearance); + } + List anchors = new ArrayList<>(); + int step = Math.max(1, scanStep); + for (int worldY = minimumY; worldY <= maximumY; worldY += step) { + int mantleY = toMantleY(worldY, engine.getMinHeight()); + if (!matchesGeometry(engine, mode, blockX, mantleY, blockZ, clearance)) { + continue; + } + if (!matchesAnchorFluid(engine, placement, blockX, mantleY, blockZ)) { + continue; + } + if (!matchesBiome(engine, placement, blockX, mantleY, blockZ)) { + continue; + } + anchors.add(worldY); + } + return List.copyOf(anchors); + } + + private static List centerAnchorsInColumn( + Engine engine, + IrisStructurePlacement placement, + int blockX, + int blockZ, + int minimumY, + int maximumY, + int scanStep, + int clearance + ) { + List anchors = new ArrayList<>(); + int step = Math.max(1, scanStep); + int worldOffset = engine.getMinHeight(); + int minimumMantleY = 0; + int maximumMantleY = engine.getHeight() - 1; + int processedThroughWorldY = Integer.MIN_VALUE; + IntPredicate carved = y -> isCarved(engine, blockX, y, blockZ); + for (int worldY = minimumY; worldY <= maximumY; worldY += step) { + if (worldY <= processedThroughWorldY) { + continue; + } + int mantleY = toMantleY(worldY, worldOffset); + if (!carved.test(mantleY)) { + continue; + } + int lower = cavernLowerBound(carved, mantleY, minimumMantleY); + int upper = cavernUpperBound(carved, mantleY, maximumMantleY); + processedThroughWorldY = upper + worldOffset; + if ((long) upper - lower + 1L < Math.max(1, clearance)) { + continue; + } + int lowerCenterWorldY = lower + Math.floorDiv(upper - lower, 2) + worldOffset; + addCenterAnchor( + anchors, engine, placement, blockX, blockZ, + lowerCenterWorldY, minimumY, maximumY); + int upperCenterWorldY = lower + Math.ceilDiv(upper - lower, 2) + worldOffset; + if (upperCenterWorldY != lowerCenterWorldY) { + addCenterAnchor( + anchors, engine, placement, blockX, blockZ, + upperCenterWorldY, minimumY, maximumY); + } + } + return List.copyOf(anchors); + } + + private static void addCenterAnchor( + List anchors, + Engine engine, + IrisStructurePlacement placement, + int blockX, + int blockZ, + int worldY, + int minimumY, + int maximumY + ) { + if (worldY < minimumY || worldY > maximumY) { + return; + } + int mantleY = toMantleY(worldY, engine.getMinHeight()); + if (matchesAnchorFluid(engine, placement, blockX, mantleY, blockZ) + && matchesBiome(engine, placement, blockX, mantleY, blockZ)) { + anchors.add(worldY); + } + } + + static boolean matchesGeometry( + Engine engine, + IrisStructureAnchorMode mode, + int blockX, + int mantleY, + int blockZ, + int clearance + ) { + IntPredicate carved = y -> isCarved(engine, blockX, y, blockZ); + if (mode == IrisStructureAnchorMode.CAVE_CENTER) { + return matchesCenterGeometry( + carved, mantleY, Math.max(1, clearance), 0, engine.getHeight() - 1); + } + return matchesGeometry(carved, mode, mantleY, clearance); + } + + static boolean matchesGeometry( + IntPredicate carved, + IrisStructureAnchorMode mode, + int mantleY, + int clearance + ) { + if (!carved.test(mantleY)) { + return false; + } + int required = Math.max(1, clearance); + return switch (mode) { + case CAVE_FLOOR -> !carved.test(mantleY - 1) + && carvedRun(carved, mantleY, 1, required); + case CAVE_CEILING -> !carved.test(mantleY + 1) + && carvedRun(carved, mantleY, -1, required); + case CAVE_CENTER -> matchesCenterGeometry( + carved, mantleY, required, + saturatedSubtract(mantleY, MAX_GENERIC_CENTER_SEARCH), + saturatedAdd(mantleY, MAX_GENERIC_CENTER_SEARCH)); + case CAVE_ANY -> carvedRun( + carved, mantleY - Math.floorDiv(required - 1, 2), 1, required); + default -> false; + }; + } + + private static boolean matchesCenterGeometry( + IntPredicate carved, + int mantleY, + int clearance, + int minimumY, + int maximumY + ) { + if (!carved.test(mantleY)) { + return false; + } + int lower = cavernLowerBound(carved, mantleY, minimumY); + int upper = cavernUpperBound(carved, mantleY, maximumY); + if ((long) upper - lower + 1L < clearance) { + return false; + } + int lowerCenter = lower + Math.floorDiv(upper - lower, 2); + int upperCenter = lower + Math.ceilDiv(upper - lower, 2); + return mantleY == lowerCenter || mantleY == upperCenter; + } + + private static int cavernLowerBound(IntPredicate carved, int mantleY, int minimumY) { + int lower = mantleY; + while (lower > minimumY && carved.test(lower - 1)) { + lower--; + } + return lower; + } + + private static int cavernUpperBound(IntPredicate carved, int mantleY, int maximumY) { + int upper = mantleY; + while (upper < maximumY && carved.test(upper + 1)) { + upper++; + } + return upper; + } + + private static int saturatedSubtract(int value, int amount) { + long result = (long) value - amount; + return result < Integer.MIN_VALUE ? Integer.MIN_VALUE : (int) result; + } + + private static int saturatedAdd(int value, int amount) { + long result = (long) value + amount; + return result > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) result; + } + + private static boolean carvedRun( + IntPredicate carved, + int mantleY, + int direction, + int length + ) { + for (int offset = 0; offset < length; offset++) { + if (!carved.test(mantleY + (direction * offset))) { + return false; + } + } + return true; + } + + private static boolean isCarved(Engine engine, int blockX, int mantleY, int blockZ) { + if (mantleY < 0 || mantleY >= engine.getHeight()) { + return false; + } + MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ); + return cavern != null && cavern.isCavern(); + } + + private static boolean matchesAnchorFluid( + Engine engine, + IrisStructurePlacement placement, + int blockX, + int mantleY, + int blockZ + ) { + MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ); + return acceptsAnchorFluid( + placement.isUnderwater(), + cavern, + mantleY, + engine.getDimension().getCaveLavaHeight()); + } + + static boolean acceptsAnchorFluid( + boolean underwater, + MatterCavern cavern, + int mantleY, + int defaultLavaHeight + ) { + if (cavern == null || !cavern.isCavern()) { + return false; + } + if (underwater) { + return true; + } + if (cavern.getLiquid() == LIQUID_FORCED_AIR) { + return true; + } + return cavern.isAir() && mantleY > defaultLavaHeight; + } + + private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) { + return engine.getMantle().getMantle().get(blockX, mantleY, blockZ, MatterCavern.class); + } + + static int toMantleY(int worldY, int worldMinHeight) { + return Math.subtractExact(worldY, worldMinHeight); + } + + private static boolean matchesBiome( + Engine engine, + IrisStructurePlacement placement, + int blockX, + int mantleY, + int blockZ + ) { + if (placement.getCaveBiomes() == null || placement.getCaveBiomes().isEmpty()) { + return true; + } + IrisBiome biome = engine.getCaveOrMantleBiome(blockX, mantleY, blockZ); + if (biome == null || biome.getLoadKey() == null) { + return false; + } + String actual = biome.getLoadKey().toLowerCase(Locale.ROOT); + for (String allowed : placement.getCaveBiomes()) { + if (matchesBiomeKey(actual, allowed)) { + return true; + } + } + return false; + } + + static boolean matchesBiomeKey(String actual, String allowed) { + if (actual == null || actual.isBlank() || allowed == null || allowed.isBlank()) { + return false; + } + String normalizedActual = actual.trim().toLowerCase(Locale.ROOT); + String normalizedAllowed = allowed.trim().toLowerCase(Locale.ROOT); + return normalizedActual.equals(normalizedAllowed) + || normalizedActual.endsWith(":" + normalizedAllowed) + || normalizedAllowed.endsWith(":" + normalizedActual); + } + + public record Anchor(int x, int y, int z) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java new file mode 100644 index 000000000..4ae945a10 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementGrid.java @@ -0,0 +1,287 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisNativeStructure; +import art.arcane.iris.engine.object.IrisStructureAnchorMode; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.volmlib.util.math.RNG; + +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; + +public final class StructurePlacementGrid { + private static final long DENSITY_SIGNATURE = 0x6A09E667F3BCC909L; + + private StructurePlacementGrid() { + } + + public static boolean startsInChunk(IrisStructurePlacement placement, int cx, int cz, long seed) { + return switch (placement.getDistribution()) { + case RANDOM_SPREAD -> randomSpreadStart(cx, cz, placement.getSpacing(), placement.getSeparation(), + placementSalt(placement), seed); + case DENSITY -> densityStart(placement, cx, cz, seed); + case CONCENTRIC_RINGS -> concentricRingsStart(cx, cz, placement, seed); + }; + } + + public static RNG placementRng(IrisStructurePlacement placement, int cx, int cz, long seed) { + return new RNG(placementSeed(placement, cx, cz, seed)); + } + + public static boolean randomSpreadStart(int cx, int cz, int spacing, int separation, int salt, long seed) { + int sp = Math.max(1, spacing); + int cellX = Math.floorDiv(cx, sp); + int cellZ = Math.floorDiv(cz, sp); + int[] start = randomSpreadCellChunk(cellX, cellZ, spacing, separation, salt, seed); + return start[0] == cx && start[1] == cz; + } + + /** + * Returns the single {chunkX, chunkZ} that a RANDOM_SPREAD placement occupies within the given + * grid cell. This is the inverse of {@link #randomSpreadStart} and uses the identical formula, so + * a locator can jump straight to the one candidate per cell instead of testing every chunk. + */ + public static int[] randomSpreadCellChunk(int cellX, int cellZ, int spacing, int separation, int salt, long seed) { + int sp = Math.max(1, spacing); + int sep = Math.max(0, Math.min(separation, sp - 1)); + RNG r = new RNG(mix(seed, cellX, cellZ, salt)); + int range = Math.max(1, sp - sep); + int startCx = cellX * sp + r.nextInt(range); + int startCz = cellZ * sp + r.nextInt(range); + return new int[]{startCx, startCz}; + } + + public static int[] concentricRingChunk(IrisStructurePlacement placement, int placementIndex, long seed) { + int count = Math.max(1, placement.getRingCount()); + if (placementIndex < 0 || placementIndex >= count) { + return null; + } + int distance = Math.max(1, placement.getRingDistance()); + int spread = Math.max(1, placement.getRingSpread()); + int ring = Math.floorDiv(placementIndex, spread) + 1; + int firstIndex = (ring - 1) * spread; + int slots = Math.min(spread, count - firstIndex); + int slot = placementIndex - firstIndex; + double slotAngle = (2.0 * Math.PI) / slots; + double offset = unitDouble(mix(seed, ring, count, placementSalt(placement))) * slotAngle; + double angle = offset + (slot * slotAngle); + long configuredRadius = (long) ring * distance; + if (configuredRadius > Integer.MAX_VALUE) { + return null; + } + int radius = (int) configuredRadius; + return new int[]{ + (int) Math.round(Math.cos(angle) * radius), + (int) Math.round(Math.sin(angle) * radius) + }; + } + + private static boolean concentricRingsStart(int cx, int cz, IrisStructurePlacement placement, long seed) { + if (cx == 0 && cz == 0) { + return false; + } + int distance = Math.max(1, placement.getRingDistance()); + int count = Math.max(1, placement.getRingCount()); + int spread = Math.max(1, placement.getRingSpread()); + double dist = Math.sqrt((double) cx * cx + (double) cz * cz); + int estimatedRing = (int) Math.round(dist / distance); + int ringCount = Math.ceilDiv(count, spread); + int firstRing = Math.max(1, estimatedRing - 1); + int lastRing = Math.min(ringCount, estimatedRing + 1); + for (int ring = firstRing; ring <= lastRing; ring++) { + int firstIndex = (ring - 1) * spread; + int slots = Math.min(spread, count - firstIndex); + if (slots <= 0) { + continue; + } + double slotAngle = (2.0 * Math.PI) / slots; + double offset = unitDouble(mix(seed, ring, count, placementSalt(placement))) * slotAngle; + double slotPosition = (Math.atan2(cz, cx) - offset) / slotAngle; + int nearestSlot = Math.floorMod((int) Math.round(slotPosition), slots); + for (int delta = -1; delta <= 1; delta++) { + int slot = Math.floorMod(nearestSlot + delta, slots); + int[] candidate = concentricRingChunk(placement, firstIndex + slot, seed); + if (candidate != null && candidate[0] == cx && candidate[1] == cz) { + return true; + } + } + } + return false; + } + + private static boolean densityStart(IrisStructurePlacement placement, int cx, int cz, long seed) { + double density = placement.getDensity(); + if (density <= 0.0) { + return false; + } + if (density >= 1.0) { + return true; + } + RNG rng = new RNG(placementSeed(placement, cx, cz, seed) ^ DENSITY_SIGNATURE); + return rng.chance(density); + } + + public static long placementIdentity(IrisStructurePlacement placement) { + long signature = 1469598103934665603L; + signature = appendLong(signature, placement.getDistribution().ordinal()); + signature = appendLong(signature, placement.getSalt()); + String placementId = placement.getPlacementId(); + if (placementId != null && !placementId.isBlank()) { + signature = appendLong(signature, 1L); + signature = appendSignature(signature, placementId.trim()); + } else { + signature = appendLong(signature, 0L); + signature = appendLong(signature, placement.getSpacing()); + signature = appendLong(signature, placement.getSeparation()); + signature = appendLong(signature, Double.doubleToLongBits(placement.getDensity())); + signature = appendLong(signature, placement.getRingCount()); + signature = appendLong(signature, placement.getRingDistance()); + signature = appendLong(signature, placement.getRingSpread()); + signature = appendLong(signature, placement.getMinHeight()); + signature = appendLong(signature, placement.getMaxHeight()); + signature = appendLong(signature, placement.isUnderground() ? 1L : 0L); + signature = appendLong(signature, placement.isUnderwater() ? 1L : 0L); + signature = appendSources(signature, placement); + signature = appendAnchor(signature, placement); + } + return signature; + } + + private static long placementSeed(IrisStructurePlacement placement, int cx, int cz, long seed) { + long signature = placementIdentity(placement); + int identitySalt = (int) (signature ^ (signature >>> 32)); + return mix(seed ^ signature, cx, cz, placementSalt(placement) ^ identitySalt); + } + + private static long appendSignature(long signature, String value) { + long result = appendLong(signature, value.length()); + for (int index = 0; index < value.length(); index++) { + result = (result ^ value.charAt(index)) * 1099511628211L; + } + return result; + } + + private static long appendLong(long signature, long value) { + long result = signature; + long remaining = value; + for (int index = 0; index < Long.BYTES; index++) { + result = (result ^ (remaining & 0xffL)) * 1099511628211L; + remaining >>>= Byte.SIZE; + } + return result; + } + + private static long appendSources(long signature, IrisStructurePlacement placement) { + long result = signature; + if (placement.hasIrisStructures()) { + result = appendLong(result, 1L); + result = appendLong(result, placement.getStructures().size()); + for (String key : placement.getStructures()) { + result = appendSignature(result, key == null ? "" : key); + } + return result; + } + result = appendLong(result, 2L); + if (placement.getNativeStructures() == null) { + return appendLong(result, 0L); + } + result = appendLong(result, placement.getNativeStructures().size()); + for (IrisNativeStructure source : placement.getNativeStructures()) { + if (source == null) { + result = appendSignature(result, ""); + result = appendLong(result, 0L); + continue; + } + result = appendSignature(result, + source.getStructure() == null ? "" : source.getStructure()); + result = appendLong(result, source.getWeight()); + } + return result; + } + + private static long appendAnchor(long signature, IrisStructurePlacement placement) { + IrisStructureAnchorMode anchor = placement.getAnchor(); + if (anchor == null || anchor == IrisStructureAnchorMode.LEGACY) { + return signature; + } + long result = appendLong(signature, 0x4A49475341574C41L); + result = appendLong(result, anchor.ordinal()); + if (!anchor.isCave()) { + return result; + } + result = appendLong(result, placement.getCaveAnchorAttempts()); + result = appendLong(result, placement.getCaveAnchorScanStep()); + result = appendLong(result, placement.getCaveMinimumClearance()); + if (placement.getCaveBiomes() == null) { + return appendLong(result, 0L); + } + Set biomes = new TreeSet<>(); + for (String biome : placement.getCaveBiomes()) { + if (biome != null && !biome.isBlank()) { + biomes.add(biome.trim().toLowerCase(Locale.ROOT)); + } + } + result = appendLong(result, biomes.size()); + for (String biome : biomes) { + result = appendSignature(result, biome); + } + return result; + } + + static int placementSalt(IrisStructurePlacement placement) { + String placementId = placement.getPlacementId(); + long identity; + if (placementId != null && !placementId.isBlank()) { + identity = appendSignature(1469598103934665603L, placementId.trim()); + } else { + identity = 1469598103934665603L; + identity = appendLong(identity, placement.getDistribution().ordinal()); + identity = appendLong(identity, placement.getSpacing()); + identity = appendLong(identity, placement.getSeparation()); + identity = appendLong(identity, Double.doubleToLongBits(placement.getDensity())); + identity = appendLong(identity, placement.getRingCount()); + identity = appendLong(identity, placement.getRingDistance()); + identity = appendLong(identity, placement.getRingSpread()); + identity = appendLong(identity, placement.getMinHeight()); + identity = appendLong(identity, placement.getMaxHeight()); + identity = appendLong(identity, placement.isUnderground() ? 1L : 0L); + identity = appendLong(identity, placement.isUnderwater() ? 1L : 0L); + identity = appendSources(identity, placement); + identity = appendAnchor(identity, placement); + } + return placement.getSalt() ^ (int) (identity ^ (identity >>> 32)); + } + + private static double unitDouble(long value) { + return (value >>> 11) * 0x1.0p-53; + } + + public static long mix(long seed, int a, int b, int salt) { + long h = seed; + h = h * 6364136223846793005L + (a * 341873128712L); + h = h * 6364136223846793005L + (b * 132897987541L); + h = h * 6364136223846793005L + salt; + h ^= (h >>> 33); + h *= 0xff51afd7ed558ccdL; + h ^= (h >>> 33); + return h; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementMarker.java b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementMarker.java new file mode 100644 index 000000000..13bd4bed2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementMarker.java @@ -0,0 +1,95 @@ +package art.arcane.iris.engine.framework; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + +public final class StructurePlacementMarker { + private static final String FAMILY_PREFIX = "@iris-structure:"; + private static final String VERSION_PREFIX = FAMILY_PREFIX + "v1:"; + + private StructurePlacementMarker() { + } + + public static String encodeStructure(String objectKey, int placementId, String structureKey) { + String normalizedObjectKey = requireKey(objectKey, "objectKey"); + String normalizedStructureKey = requireKey(structureKey, "structureKey"); + return VERSION_PREFIX + + encodeKey(normalizedObjectKey) + + ":" + placementId + + ":" + encodeKey(normalizedStructureKey); + } + + public static Decoded decode(String marker) { + if (marker == null || marker.isBlank()) { + return null; + } + if (marker.startsWith(FAMILY_PREFIX)) { + return decodeStructure(marker); + } + return decodeLegacy(marker); + } + + private static Decoded decodeStructure(String marker) { + if (!marker.startsWith(VERSION_PREFIX)) { + return null; + } + String[] fields = marker.split(":", -1); + if (fields.length != 5 || !fields[0].equals("@iris-structure") || !fields[1].equals("v1")) { + return null; + } + try { + String objectKey = decodeCanonicalKey(fields[2]); + int placementId = Integer.parseInt(fields[3]); + String structureKey = decodeCanonicalKey(fields[4]); + if (objectKey.isBlank() || structureKey.isBlank()) { + return null; + } + return new Decoded(objectKey, placementId, structureKey); + } catch (IllegalArgumentException exception) { + return null; + } + } + + private static Decoded decodeLegacy(String marker) { + int separator = marker.indexOf('@'); + if (separator <= 0 || separator != marker.lastIndexOf('@') || separator == marker.length() - 1) { + return null; + } + String objectKey = marker.substring(0, separator); + if (objectKey.isEmpty() || objectKey.equals("null")) { + return null; + } + try { + return new Decoded(objectKey, Integer.parseInt(marker.substring(separator + 1)), null); + } catch (NumberFormatException exception) { + return null; + } + } + + private static String requireKey(String key, String name) { + String required = Objects.requireNonNull(key, name); + if (required.isBlank()) { + throw new IllegalArgumentException(name + " must not be blank"); + } + return required; + } + + private static String encodeKey(String key) { + return Base64.getUrlEncoder().withoutPadding().encodeToString(key.getBytes(StandardCharsets.UTF_8)); + } + + private static String decodeCanonicalKey(String key) { + String decoded = new String(Base64.getUrlDecoder().decode(key), StandardCharsets.UTF_8); + if (!encodeKey(decoded).equals(key)) { + throw new IllegalArgumentException("Non-canonical marker key"); + } + return decoded; + } + + public record Decoded(String objectKey, int placementId, String structureKey) { + public boolean structureAware() { + return structureKey != null; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementScope.java b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementScope.java new file mode 100644 index 000000000..a7ae4cd2d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructurePlacementScope.java @@ -0,0 +1,64 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.volmlib.util.collection.KList; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Objects; +import java.util.Set; + +public final class StructurePlacementScope { + private StructurePlacementScope() { + } + + public static KList placementsAt(Engine engine, int chunkX, int chunkZ) { + Engine activeEngine = Objects.requireNonNull(engine, "Structure placement scope requires an engine"); + IrisComplex complex = activeEngine.getComplex(); + int blockX = (chunkX << 4) + 8; + int blockZ = (chunkZ << 4) + 8; + KList placements = new KList<>(); + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + if (complex != null) { + IrisBiome biome = complex.getTrueBiomeStream().get(blockX, blockZ); + IrisBiome caveBiome = complex.getCaveBiomeStream().get(blockX, blockZ); + IrisRegion region = complex.getRegionStream().get(blockX, blockZ); + addUnique(placements, seen, biome == null ? null : biome.getStructures()); + addCaveUnique(placements, seen, caveBiome == null ? null : caveBiome.getStructures()); + addUnique(placements, seen, region == null ? null : region.getStructures()); + } + if (activeEngine.getDimension() != null) { + addUnique(placements, seen, activeEngine.getDimension().getStructures()); + } + return placements; + } + + private static void addUnique(KList destination, + Set seen, + KList source) { + if (source == null) { + return; + } + for (IrisStructurePlacement placement : source) { + if (placement != null && seen.add(placement)) { + destination.add(placement); + } + } + } + + private static void addCaveUnique(KList destination, + Set seen, + KList source) { + if (source == null) { + return; + } + for (IrisStructurePlacement placement : source) { + if (placement != null && placement.resolvedAnchor().isCave() && seen.add(placement)) { + destination.add(placement); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructureReachability.java b/core/src/main/java/art/arcane/iris/engine/framework/StructureReachability.java new file mode 100644 index 000000000..aa780a7eb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructureReachability.java @@ -0,0 +1,127 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.collection.KList; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +/** + * Determines which vanilla/datapack structures can ever generate in a world, using the same biome + * gate vanilla applies: a structure is reachable when its biome filter intersects the set of vanilla + * biomes the pack actually produces (its {@code possibleBiomes}). Structures whose required biomes are + * never produced cannot generate and must not be scanned for by {@code /locate} or {@code /iris find} + * (an unbounded scan for an absent biome is what stalls the server). + * + *

The reachable set is fixed per live engine/world and is rebuilt after hotload. + */ +public final class StructureReachability { + private static final Cache> REACHABLE_CACHE = Caffeine.newBuilder().weakKeys().build(); + + private StructureReachability() { + } + + public static Set reachableKeys(Engine engine) { + Engine activeEngine = Objects.requireNonNull(engine, "Structure reachability requires an engine"); + if (activeEngine.getData() == null) { + throw new IllegalStateException("Structure reachability requires a bound pack data loader"); + } + return REACHABLE_CACHE.get(activeEngine, ignored -> build(activeEngine)); + } + + public static boolean isReachable(Engine engine, String structureKey) { + String normalizedStructureKey = normalizeStructureKey(structureKey); + return reachableKeys(engine).contains(normalizedStructureKey); + } + + public static void invalidate(Engine engine) { + if (engine == null) { + return; + } + REACHABLE_CACHE.invalidate(engine); + } + + private static Set build(Engine engine) { + IrisWorld world = engine.getWorld(); + if (world == null) { + throw new IllegalStateException("Structure reachability requires a bound Iris world"); + } + if (world.platformWorld() == null) { + throw new IllegalStateException("Structure reachability requires a bound platform world"); + } + Set reachable = new LinkedHashSet<>(); + for (String key : IrisPlatforms.get().structureHooks().reachableStructureKeys(world.platformWorld())) { + if (key == null || key.isBlank()) { + throw new IllegalStateException("Platform structure reachability returned a blank registry key"); + } + reachable.add(key.toLowerCase(Locale.ROOT)); + } + return Collections.unmodifiableSet(reachable); + } + + /** + * The vanilla biome keys (e.g. {@code minecraft:taiga}) from a structure's filter that the pack + * does not produce. An empty list means the structure is reachable. Used by the structure verify + * diagnostic to explain why a structure cannot generate. + */ + public static KList missingBiomeKeys(Engine engine, String structureKey) { + KList missing = new KList<>(); + Engine activeEngine = Objects.requireNonNull(engine, "Structure biome diagnostics require an engine"); + String normalizedStructureKey = normalizeStructureKey(structureKey); + IrisWorld world = activeEngine.getWorld(); + if (world == null) { + throw new IllegalStateException("Structure biome diagnostics require a bound Iris world"); + } + if (world.platformWorld() == null) { + throw new IllegalStateException("Structure biome diagnostics require a bound platform world"); + } + Set possible = new LinkedHashSet<>(); + for (String key : IrisPlatforms.get().structureHooks().possibleBiomeKeys(world.platformWorld())) { + if (key == null || key.isBlank()) { + throw new IllegalStateException("Platform structure biome query returned a blank possible-biome key"); + } + possible.add(key.toLowerCase(Locale.ROOT)); + } + for (String biomeKey : IrisPlatforms.get().structureHooks().structureBiomeKeys(normalizedStructureKey)) { + if (biomeKey == null || biomeKey.isBlank()) { + throw new IllegalStateException("Platform structure biome query returned a blank required-biome key for " + + normalizedStructureKey); + } + if (!possible.contains(biomeKey.toLowerCase(Locale.ROOT))) { + missing.add(biomeKey); + } + } + return missing; + } + + private static String normalizeStructureKey(String structureKey) { + if (structureKey == null || structureKey.isBlank()) { + throw new IllegalArgumentException("Registered structure key must not be blank"); + } + return structureKey.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructureVerticalBounds.java b/core/src/main/java/art/arcane/iris/engine/framework/StructureVerticalBounds.java new file mode 100644 index 000000000..05e67668c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructureVerticalBounds.java @@ -0,0 +1,51 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +public final class StructureVerticalBounds { + private StructureVerticalBounds() { + } + + public static int clampOffset( + int structureMinY, + int structureMaxY, + int requestedOffset, + int worldMinY, + int worldMaxYExclusive + ) { + if (structureMaxY < structureMinY) { + throw new IllegalArgumentException("Structure maximum Y cannot be below its minimum Y"); + } + if (worldMaxYExclusive <= worldMinY) { + throw new IllegalArgumentException("World maximum Y must be above its minimum Y"); + } + + long structureHeight = (long) structureMaxY - structureMinY + 1L; + long worldHeight = (long) worldMaxYExclusive - worldMinY; + if (structureHeight > worldHeight) { + throw new IllegalArgumentException("Structure height " + structureHeight + + " exceeds writable world height " + worldHeight); + } + + long minimumOffset = (long) worldMinY - structureMinY; + long maximumOffset = (long) worldMaxYExclusive - 1L - structureMaxY; + long clamped = Math.max(minimumOffset, Math.min(maximumOffset, (long) requestedOffset)); + return Math.toIntExact(clamped); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/TreeBlockMaterial.java b/core/src/main/java/art/arcane/iris/engine/framework/TreeBlockMaterial.java new file mode 100644 index 000000000..b24b0915a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/TreeBlockMaterial.java @@ -0,0 +1,28 @@ +package art.arcane.iris.engine.framework; + +import art.arcane.iris.spi.PlatformBlockState; + +import java.util.Objects; + +public record TreeBlockMaterial(String materialKey) { + public TreeBlockMaterial { + Objects.requireNonNull(materialKey, "materialKey"); + if (materialKey.isBlank()) { + throw new IllegalArgumentException("materialKey must not be blank"); + } + } + + public static TreeBlockMaterial of(PlatformBlockState state) { + return of(Objects.requireNonNull(state, "state").key()); + } + + public static TreeBlockMaterial of(String blockStateKey) { + String key = Objects.requireNonNull(blockStateKey, "blockStateKey"); + int properties = key.indexOf('['); + return new TreeBlockMaterial(properties < 0 ? key : key.substring(0, properties)); + } + + public boolean matches(String blockStateKey) { + return equals(of(blockStateKey)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/WrongEngineBroException.java b/core/src/main/java/art/arcane/iris/engine/framework/WrongEngineBroException.java new file mode 100644 index 000000000..943a35a9b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/WrongEngineBroException.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +public class WrongEngineBroException extends Exception { + public WrongEngineBroException() { + super(); + } + + public WrongEngineBroException(String message) { + super(message); + } + + public WrongEngineBroException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/placer/HeightmapObjectPlacer.java b/core/src/main/java/art/arcane/iris/engine/framework/placer/HeightmapObjectPlacer.java similarity index 78% rename from core/src/main/java/com/volmit/iris/engine/framework/placer/HeightmapObjectPlacer.java rename to core/src/main/java/art/arcane/iris/engine/framework/placer/HeightmapObjectPlacer.java index c6762e0b4..d87ca5800 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/placer/HeightmapObjectPlacer.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/placer/HeightmapObjectPlacer.java @@ -16,22 +16,22 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.framework.placer; +package art.arcane.iris.engine.framework.placer; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IObjectPlacer; -import com.volmit.iris.engine.object.IrisObjectPlacement; -import com.volmit.iris.engine.object.TileData; -import com.volmit.iris.util.math.RNG; -import org.bukkit.block.data.BlockData; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.math.RNG; public class HeightmapObjectPlacer implements IObjectPlacer { private final long s; private final IrisObjectPlacement config; private final IObjectPlacer oplacer; - public HeightmapObjectPlacer(Engine engine, RNG rng, int x, int yv, int z, IrisObjectPlacement config, IObjectPlacer oplacer) { + public HeightmapObjectPlacer(RNG rng, int x, int yv, int z, IrisObjectPlacement config, IObjectPlacer oplacer) { s = rng.nextLong() + yv + z - x; this.config = config; this.oplacer = oplacer; @@ -45,11 +45,11 @@ public class HeightmapObjectPlacer implements IObjectPlacer { return (int) Math.round(config.getHeightmap().getNoise(this.s, param1Int1, param1Int2, data)); } - public void set(int param1Int1, int param1Int2, int param1Int3, BlockData param1BlockData) { + public void set(int param1Int1, int param1Int2, int param1Int3, PlatformBlockState param1BlockData) { oplacer.set(param1Int1, param1Int2, param1Int3, param1BlockData); } - public BlockData get(int param1Int1, int param1Int2, int param1Int3) { + public PlatformBlockState get(int param1Int1, int param1Int2, int param1Int3) { return oplacer.get(param1Int1, param1Int2, param1Int3); } @@ -62,6 +62,11 @@ public class HeightmapObjectPlacer implements IObjectPlacer { return oplacer.isCarved(x,y,z); } + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + return oplacer.isSurfaceSolid(x, y, z); + } + public boolean isSolid(int param1Int1, int param1Int2, int param1Int3) { return oplacer.isSolid(param1Int1, param1Int2, param1Int3); } @@ -94,6 +99,6 @@ public class HeightmapObjectPlacer implements IObjectPlacer { @Override public Engine getEngine() { - return null; + return oplacer.getEngine(); } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java b/core/src/main/java/art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java new file mode 100644 index 000000000..0d706e139 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java @@ -0,0 +1,165 @@ +package art.arcane.iris.engine.framework.placer; + +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.core.events.IrisLootEvent; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.InventorySlotType; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.data.IrisCustomData; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterCavern; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.inventory.InventoryHolder; + +@Getter +@EqualsAndHashCode(exclude = {"engine", "mantle"}) +public class WorldObjectPlacer implements IObjectPlacer { + private final World world; + private final Engine engine; + private final EngineMantle mantle; + + public WorldObjectPlacer(World world) { + PlatformChunkGenerator a = IrisToolbelt.access(world); + if (a == null || a.getEngine() == null) throw new IllegalStateException(world.getName() + " is not an Iris World!"); + this.world = world; + this.engine = a.getEngine(); + this.mantle = engine.getMantle(); + } + + @Override + public int getHighest(int x, int z, IrisData data) { + return mantle.getHighest(x, z, data); + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return mantle.getHighest(x, z, data, ignoreFluid); + } + + @Override + public void set(int x, int y, int z, PlatformBlockState state) { + BlockData d = (BlockData) state.nativeHandle(); + int worldY = y + world.getMinHeight(); + if (worldY < world.getMinHeight() || worldY >= world.getMaxHeight()) return; + Block block = world.getBlockAt(x, worldY, z); + + if (block.getType() == Material.BEDROCK) return; + boolean storageChest = BukkitBlockResolution.isStorageChest(d); + + if (d instanceof IrisCustomData data) { + block.setBlockData(data.getBase(), false); + // Reached per block for every custom block in the pack; the pack is what needs changing, and + // one statement of that is enough. + IrisLogging.warnOnce("custom-block-placer", "Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported."); + } else block.setBlockData(d, false); + + if (storageChest && !J.runRegion(world, x >> 4, z >> 4, () -> fillLoot(block), 1)) { + IrisLogging.warn("Failed to schedule loot resolution at " + x + ", " + worldY + ", " + z); + } + } + + private void fillLoot(Block block) { + if (!BukkitBlockResolution.isStorageChest(block.getBlockData()) || !EngineBukkitOps.isCanonicalContainer(block)) { + return; + } + int x = block.getX(); + int y = block.getY(); + int z = block.getZ(); + RNG rng = LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z); + KList tables = EngineBukkitOps.getLootTables(engine, rng, block); + + try { + Bukkit.getPluginManager().callEvent(new IrisLootEvent(engine, block, InventorySlotType.STORAGE, tables)); + if (tables.isEmpty()) { + return; + } + InventoryHolder holder = (InventoryHolder) block.getState(); + EngineBukkitOps.addItems(engine, false, holder.getInventory(), tables, InventorySlotType.STORAGE, world, x, y, z); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + return BukkitBlockState.of(world.getBlockAt(x, y + world.getMinHeight(), z).getBlockData()); + } + + @Override + public boolean isPreventingDecay() { + return mantle.isPreventingDecay(); + } + + @Override + public boolean isCarved(int x, int y, int z) { + return mantle.getMantle().get(x, y, z, MatterCavern.class) != null; + } + + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + return isSolid(x, y, z); + } + + @Override + public boolean isSolid(int x, int y, int z) { + return world.getBlockAt(x, y + world.getMinHeight(), z).getType().isSolid(); + } + + @Override + public boolean isUnderwater(int x, int z) { + return mantle.isUnderwater(x, z); + } + + @Override + public int getFluidHeight() { + return mantle.getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return mantle.isDebugSmartBore(); + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + int worldY = yy + world.getMinHeight(); + if (worldY < world.getMinHeight() || worldY >= world.getMaxHeight()) return; + tile.toBukkitTry(world.getBlockAt(xx, worldY, zz)); + } + + @Override + public void setData(int xx, int yy, int zz, T data) { + if (data == null || yy < 0 || yy >= engine.getHeight()) { + return; + } + mantle.getMantle().set(xx, yy, zz, data); + } + + @Override + public T getData(int xx, int yy, int zz, Class t) { + if (yy < 0 || yy >= engine.getHeight()) { + return null; + } + return mantle.getMantle().get(xx, yy, zz, t); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java b/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java new file mode 100644 index 000000000..2ba61eacf --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java @@ -0,0 +1,80 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework.render; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeGeneratorLink; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.util.function.BiFunction; + +public class IrisRenderer { + private static final int BLUE = Color.BLUE.getRGB(); + private static final int YELLOW = Color.YELLOW.getRGB(); + private static final int GREEN = Color.GREEN.getRGB(); + + private final Engine renderer; + + public IrisRenderer(Engine renderer) { + this.renderer = renderer; + } + + public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) { + BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB); + int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + BiFunction colorFunction = (d, dx) -> 0; + + switch (currentType) { + case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> + colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); + case BIOME_LAND -> + colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); + case BIOME_SEA -> + colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); + case REGION -> + colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB(); + case CAVE_LAND -> + colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); + case HEIGHT -> + colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 1f, 1f).getRGB(); + case CONTINENT -> colorFunction = (x, z) -> { + IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z)); + IrisBiomeGeneratorLink g = b.getGenerators().get(0); + if (g.getMax() <= 0) return BLUE; + if (g.getMin() < 0) return YELLOW; + return GREEN; + }; + } + + double x, z; + for (int i = 0; i < resolution; i++) { + x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) resolution); + for (int j = 0; j < resolution; j++) { + z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) resolution); + pixels[j * resolution + i] = colorFunction.apply(x, z); + } + } + + return image; + } +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/components/RenderType.java b/core/src/main/java/art/arcane/iris/engine/framework/render/RenderType.java similarity index 94% rename from core/src/main/java/com/volmit/iris/core/gui/components/RenderType.java rename to core/src/main/java/art/arcane/iris/engine/framework/render/RenderType.java index 778286fe5..2ec4a17d1 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/components/RenderType.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/render/RenderType.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.gui.components; +package art.arcane.iris.engine.framework.render; public enum RenderType { BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD diff --git a/core/src/main/java/com/volmit/iris/core/gui/components/Renderer.java b/core/src/main/java/art/arcane/iris/engine/framework/render/Renderer.java similarity index 92% rename from core/src/main/java/com/volmit/iris/core/gui/components/Renderer.java rename to core/src/main/java/art/arcane/iris/engine/framework/render/Renderer.java index f6ca29398..676bea97b 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/components/Renderer.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/render/Renderer.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.gui.components; +package art.arcane.iris.engine.framework.render; -import java.awt.*; +import java.awt.Color; @FunctionalInterface public interface Renderer { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/CompiledStructureGraph.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/CompiledStructureGraph.java new file mode 100644 index 000000000..3a5e70c2c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/CompiledStructureGraph.java @@ -0,0 +1,101 @@ +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisStructure; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public final class CompiledStructureGraph { + private final IrisStructure structure; + private final Map pools; + private final Map pieces; + private final Map objects; + private final Set reachablePools; + private final Set reachablePieces; + + private CompiledStructureGraph(Builder builder) { + structure = builder.structure; + pools = Collections.unmodifiableMap(new LinkedHashMap<>(builder.pools)); + pieces = Collections.unmodifiableMap(new LinkedHashMap<>(builder.pieces)); + objects = Collections.unmodifiableMap(new LinkedHashMap<>(builder.objects)); + reachablePools = Collections.unmodifiableSet(new LinkedHashSet<>(builder.reachablePools)); + reachablePieces = Collections.unmodifiableSet(new LinkedHashSet<>(builder.reachablePieces)); + } + + static Builder builder(IrisStructure structure) { + return new Builder(structure); + } + + public IrisStructure getStructure() { + return structure; + } + + public String getStructureKey() { + String key = structure.getLoadKey(); + return key == null || key.isBlank() ? "" : key; + } + + public Map getPools() { + return pools; + } + + public Map getPieces() { + return pieces; + } + + public Map getObjects() { + return objects; + } + + public Set getReachablePools() { + return reachablePools; + } + + public Set getReachablePieces() { + return reachablePieces; + } + + static final class Builder { + private final IrisStructure structure; + private final Map pools = new LinkedHashMap<>(); + private final Map pieces = new LinkedHashMap<>(); + private final Map objects = new LinkedHashMap<>(); + private final Set reachablePools = new LinkedHashSet<>(); + private final Set reachablePieces = new LinkedHashSet<>(); + + private Builder(IrisStructure structure) { + this.structure = Objects.requireNonNull(structure); + } + + Map pools() { + return pools; + } + + Map pieces() { + return pieces; + } + + Map objects() { + return objects; + } + + Set reachablePools() { + return reachablePools; + } + + Set reachablePieces() { + return reachablePieces; + } + + CompiledStructureGraph build() { + return new CompiledStructureGraph(this); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/IrisObjectFrameReader.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/IrisObjectFrameReader.java new file mode 100644 index 000000000..0a4341e2a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/IrisObjectFrameReader.java @@ -0,0 +1,386 @@ +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.engine.object.IrisObject; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +public final class IrisObjectFrameReader { + private static final String HEADER = "Iris V2 IOB;"; + private static final int MAX_FILE_BYTES = 64 * 1024 * 1024; + private static final int MAX_TILE_COUNT = 65_536; + private static final int MAX_CANDIDATE_OFFSETS = 1_024; + private static final long MAX_TILE_PARSE_STATES = 262_144L; + private static final int MAX_LEGACY_SIGN_COLOR_INDEX = 15; + + private IrisObjectFrameReader() { + } + + public static IrisObject readBounds(InputStream stream, String resourceName) throws IOException { + String activeResourceName = resourceName == null || resourceName.isBlank() + ? "" : resourceName; + try { + byte[] content = readLimitedContent(Objects.requireNonNull(stream), activeResourceName); + return readBounds(content, activeResourceName); + } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().startsWith("Malformed Iris object resource ")) { + throw e; + } + throw malformed(activeResourceName, e.getMessage() == null + ? e.getClass().getSimpleName() : e.getMessage()); + } catch (RuntimeException e) { + throw malformed(activeResourceName, e.getMessage() == null + ? e.getClass().getSimpleName() : e.getMessage()); + } + } + + private static IrisObject readBounds(byte[] content, String resourceName) throws IOException { + ByteArrayInputStream bytes = new ByteArrayInputStream(content); + DataInputStream input = new DataInputStream(bytes); + int width = input.readInt(); + int height = input.readInt(); + int depth = input.readInt(); + long volume = requireDimensions(resourceName, width, height, depth); + if (!HEADER.equals(input.readUTF())) { + throw malformed(resourceName, "invalid header"); + } + + int paletteSize = input.readShort(); + if (paletteSize < 0) { + throw malformed(resourceName, "palette exceeds the signed IOB limit"); + } + for (int index = 0; index < paletteSize; index++) { + if (input.readUTF().isBlank()) { + throw malformed(resourceName, "palette entry " + index + " is blank"); + } + } + + int blockCount = input.readInt(); + if (blockCount < 0 || blockCount > volume || blockCount > 0 && paletteSize == 0) { + throw malformed(resourceName, "invalid block count " + blockCount); + } + for (int index = 0; index < blockCount; index++) { + int x = input.readShort(); + int y = input.readShort(); + int z = input.readShort(); + int paletteIndex = input.readShort(); + requirePosition(resourceName, "block", index, x, y, z, width, height, depth); + if (paletteIndex < 0 || paletteIndex >= paletteSize) { + throw malformed(resourceName, "block " + index + + " references invalid palette index " + paletteIndex); + } + } + + int tileCount = input.readInt(); + if (tileCount < 0 || tileCount > volume) { + throw malformed(resourceName, "invalid tile count " + tileCount); + } + if (tileCount > MAX_TILE_COUNT) { + throw malformed(resourceName, "tile count " + tileCount + + " exceeds limit " + MAX_TILE_COUNT); + } + int tileStart = content.length - bytes.available(); + requireCompleteTileFraming(content, tileStart, tileCount, width, height, depth, resourceName); + return new IrisObject(width, height, depth); + } + + private static void requireCompleteTileFraming( + byte[] content, + int tileStart, + int tileCount, + int width, + int height, + int depth, + String resourceName + ) throws IOException { + Set offsets = Set.of(tileStart); + long parseStates = 0L; + for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) { + Set nextOffsets = new LinkedHashSet<>(); + TileFailureTracker failures = new TileFailureTracker(); + for (int offset : offsets) { + parseStates++; + requireParseStateBudget(resourceName, tileIndex, parseStates); + TilePrefix prefix = readTilePrefix( + content, offset, width, height, depth, tileIndex, failures); + if (prefix == null) { + continue; + } + int modernEnd = readModernTileEnd( + content, prefix.payloadOffset(), tileIndex, failures); + if (modernEnd >= 0) { + addCandidateOffset(resourceName, tileIndex, nextOffsets, modernEnd); + } + for (int legacyEnd : readLegacyTileEnds( + content, prefix.payloadOffset(), tileIndex, failures)) { + addCandidateOffset(resourceName, tileIndex, nextOffsets, legacyEnd); + } + } + if (nextOffsets.isEmpty()) { + throw malformed(resourceName, failures.detailOr( + "tile " + tileIndex + " is truncated or malformed")); + } + offsets = nextOffsets; + } + if (!offsets.contains(content.length)) { + throw malformed(resourceName, "tile records leave trailing or incomplete data"); + } + } + + private static TilePrefix readTilePrefix( + byte[] content, + int offset, + int width, + int height, + int depth, + int tileIndex, + TileFailureTracker failures + ) { + try { + ByteArrayInputStream bytes = slice(content, offset); + DataInputStream input = new DataInputStream(bytes); + int x = input.readShort(); + int y = input.readShort(); + int z = input.readShort(); + if (!withinSignedObjectAxis(x, width) || !withinSignedObjectAxis(y, height) + || !withinSignedObjectAxis(z, depth)) { + failures.record("tile " + tileIndex + " position " + x + "," + y + "," + z + + " is outside object bounds", 20); + return null; + } + return new TilePrefix(content.length - bytes.available()); + } catch (IOException | RuntimeException e) { + return null; + } + } + + private static int readModernTileEnd( + byte[] content, + int offset, + int tileIndex, + TileFailureTracker failures + ) { + try { + ByteArrayInputStream bytes = slice(content, offset); + DataInputStream input = new DataInputStream(bytes); + String material = input.readUTF(); + if (material.isBlank()) { + failures.record("tile " + tileIndex + " modern material is blank", 40); + return -1; + } + JsonElement properties = JsonParser.parseString(input.readUTF()); + if (!properties.isJsonObject()) { + failures.record("tile " + tileIndex + + " modern properties are not a JSON object", 30); + return -1; + } + return content.length - bytes.available(); + } catch (IOException | RuntimeException e) { + return -1; + } + } + + private static Set readLegacyTileEnds( + byte[] content, + int offset, + int tileIndex, + TileFailureTracker failures + ) { + try { + ByteArrayInputStream bytes = slice(content, offset); + DataInputStream input = new DataInputStream(bytes); + int id = input.readShort(); + return switch (id) { + case 0 -> singletonEnd(content, readLegacySign(input, bytes, tileIndex, failures)); + case 1 -> readLegacySpawnerEnds(content, content.length - bytes.available()); + case 2 -> readLegacyBannerEnds(content, input, bytes); + case 3 -> singletonEnd(content, readLegacyLootable(input, bytes)); + default -> Set.of(); + }; + } catch (IOException | RuntimeException e) { + return Set.of(); + } + } + + private static int readLegacySign( + DataInputStream input, + ByteArrayInputStream bytes, + int tileIndex, + TileFailureTracker failures + ) throws IOException { + input.readUTF(); + input.readUTF(); + input.readUTF(); + input.readUTF(); + int colorIndex = input.readByte(); + if (colorIndex < 0 || colorIndex > MAX_LEGACY_SIGN_COLOR_INDEX) { + failures.record("tile " + tileIndex + " legacy sign color index " + colorIndex + + " is outside 0.." + MAX_LEGACY_SIGN_COLOR_INDEX, 50); + throw new IOException("Invalid legacy sign color index"); + } + return bytes.available(); + } + + private static Set readLegacySpawnerEnds(byte[] content, int payloadOffset) { + Set ends = new LinkedHashSet<>(); + try { + ByteArrayInputStream keyedBytes = slice(content, payloadOffset); + new DataInputStream(keyedBytes).readUTF(); + ends.add(content.length - keyedBytes.available()); + } catch (IOException | RuntimeException ignored) { + } + if (payloadOffset <= content.length - Short.BYTES) { + ends.add(payloadOffset + Short.BYTES); + } + return Set.copyOf(ends); + } + + private static Set readLegacyBannerEnds( + byte[] content, + DataInputStream input, + ByteArrayInputStream bytes + ) throws IOException { + input.readUnsignedByte(); + int patternCount = input.readUnsignedByte(); + int patternsOffset = content.length - bytes.available(); + Set ends = new LinkedHashSet<>(); + try { + ByteArrayInputStream keyedBytes = slice(content, patternsOffset); + DataInputStream keyedInput = new DataInputStream(keyedBytes); + for (int index = 0; index < patternCount; index++) { + keyedInput.readUnsignedByte(); + keyedInput.readUTF(); + } + ends.add(content.length - keyedBytes.available()); + } catch (IOException | RuntimeException ignored) { + } + long legacyEnd = (long) patternsOffset + (long) patternCount * 2L; + if (legacyEnd <= content.length) { + ends.add((int) legacyEnd); + } + return Set.copyOf(ends); + } + + private static int readLegacyLootable(DataInputStream input, ByteArrayInputStream bytes) throws IOException { + input.readUTF(); + input.readUTF(); + input.readLong(); + return bytes.available(); + } + + private static Set singletonEnd(byte[] content, int remaining) { + return Set.of(content.length - remaining); + } + + private static byte[] readLimitedContent(InputStream stream, String resourceName) throws IOException { + return readLimitedContent(stream, resourceName, MAX_FILE_BYTES); + } + + static byte[] readLimitedContent( + InputStream stream, + String resourceName, + int maximumBytes + ) throws IOException { + byte[] content = stream.readNBytes(maximumBytes + 1); + if (content.length > maximumBytes) { + throw malformed(resourceName, "file exceeds " + maximumBytes + "-byte limit"); + } + return content; + } + + private static void addCandidateOffset( + String resourceName, + int tileIndex, + Set offsets, + int offset + ) throws IOException { + offsets.add(offset); + if (offsets.size() > MAX_CANDIDATE_OFFSETS) { + throw malformed(resourceName, "tile " + tileIndex + " exceeds candidate-offset limit " + + MAX_CANDIDATE_OFFSETS); + } + } + + private static void requireParseStateBudget( + String resourceName, + int tileIndex, + long parseStates + ) throws IOException { + if (parseStates > MAX_TILE_PARSE_STATES) { + throw malformed(resourceName, "tile " + tileIndex + " exceeds parse-state limit " + + MAX_TILE_PARSE_STATES); + } + } + + private static ByteArrayInputStream slice(byte[] content, int offset) { + if (offset < 0 || offset > content.length) { + throw new IllegalArgumentException("Invalid IOB frame offset " + offset); + } + return new ByteArrayInputStream(content, offset, content.length - offset); + } + + private static long requireDimensions(String resourceName, int width, int height, int depth) throws IOException { + if (width < 1 || height < 1 || depth < 1) { + throw malformed(resourceName, "invalid dimensions " + width + "x" + height + "x" + depth); + } + try { + return Math.multiplyExact(Math.multiplyExact((long) width, height), depth); + } catch (ArithmeticException e) { + throw malformed(resourceName, "dimensions overflow the IOB volume limit"); + } + } + + private static void requirePosition( + String resourceName, + String type, + int index, + int x, + int y, + int z, + int width, + int height, + int depth + ) throws IOException { + if (!withinSignedObjectAxis(x, width) || !withinSignedObjectAxis(y, height) + || !withinSignedObjectAxis(z, depth)) { + throw malformed(resourceName, type + " " + index + " position " + + x + "," + y + "," + z + " is outside object bounds"); + } + } + + private static boolean withinSignedObjectAxis(int coordinate, int size) { + int center = size / 2; + return coordinate >= -center && coordinate < size - center; + } + + private static IOException malformed(String resourceName, String detail) { + return new IOException("Malformed Iris object resource " + resourceName + ": " + detail); + } + + private record TilePrefix(int payloadOffset) { + } + + private static final class TileFailureTracker { + private String detail; + private int priority = Integer.MIN_VALUE; + + private void record(String candidateDetail, int candidatePriority) { + if (candidatePriority <= priority) { + return; + } + detail = candidateDetail; + priority = candidatePriority; + } + + private String detailOr(String fallback) { + return detail == null ? fallback : detail; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/JigsawPoolSelection.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/JigsawPoolSelection.java new file mode 100644 index 000000000..3cf93c2f7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/JigsawPoolSelection.java @@ -0,0 +1,104 @@ +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPieceEntry; +import art.arcane.iris.engine.object.IrisJigsawPieceRules; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisJigsawThemeSet; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class JigsawPoolSelection { + private JigsawPoolSelection() { + } + + public static String directFallbackKey(IrisJigsawPool pool) { + Objects.requireNonNull(pool); + return normalize(pool.getFallback()); + } + + public static List candidatePoolKeys( + String primaryKey, + IrisJigsawPool primaryPool, + boolean includePrimary + ) { + Objects.requireNonNull(primaryPool); + String normalizedPrimary = normalize(primaryKey); + String directFallback = directFallbackKey(primaryPool); + if (!includePrimary) { + return directFallback.isEmpty() ? List.of() : List.of(directFallback); + } + if (normalizedPrimary.isEmpty()) { + return List.of(); + } + return directFallback.isEmpty() + ? List.of(normalizedPrimary) + : List.of(normalizedPrimary, directFallback); + } + + public static String selectTheme(IrisStructure structure, RNG rng) { + IrisStructure activeStructure = Objects.requireNonNull(structure); + RNG activeRng = Objects.requireNonNull(rng); + if (activeStructure.getThemeSets() == null || activeStructure.getThemeSets().isEmpty()) { + return ""; + } + List themes = new ArrayList<>(activeStructure.getThemeSets().size()); + long totalWeight = 0L; + for (IrisJigsawThemeSet theme : activeStructure.getThemeSets()) { + if (theme == null || normalize(theme.getKey()).isEmpty() || theme.getWeight() <= 0) { + throw new IllegalArgumentException("Jigsaw theme sets require non-blank keys and positive weights"); + } + themes.add(theme); + totalWeight = Math.addExact(totalWeight, theme.getWeight()); + } + if (themes.size() == 1) { + return normalize(themes.getFirst().getKey()); + } + long target = activeRng.nextLong(totalWeight); + for (IrisJigsawThemeSet theme : themes) { + target -= theme.getWeight(); + if (target < 0L) { + return normalize(theme.getKey()); + } + } + return normalize(themes.getLast().getKey()); + } + + public static boolean passesChance(IrisJigsawPieceEntry entry, RNG rng) { + IrisJigsawPieceEntry activeEntry = Objects.requireNonNull(entry); + RNG activeRng = Objects.requireNonNull(rng); + double chance = activeEntry.getChance(); + if (!Double.isFinite(chance) || chance < 0D || chance > 1D) { + throw new IllegalArgumentException("Jigsaw pool membership chance must be finite and within 0 and 1"); + } + if (!activeEntry.requiresChanceRoll()) { + return chance >= 1D; + } + return activeEntry.passesChance(activeRng.nextDouble()); + } + + public static boolean pieceEligible( + IrisJigsawPiece piece, + String selectedTheme, + int depth, + int existingPlacements + ) { + IrisJigsawPiece activePiece = Objects.requireNonNull(piece); + IrisJigsawPieceRules rules = activePiece.resolvedRules(); + return activePiece.supportsTheme(normalize(selectedTheme)) + && rules.allowsDepth(depth) + && rules.allowsPlacement(existingPlacements); + } + + public static boolean needsMinimumPlacement(IrisJigsawPiece piece, int existingPlacements) { + return Objects.requireNonNull(piece).resolvedRules().requiresMorePlacements(existingPlacements); + } + + private static String normalize(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/PlanarJigsawWorkcellResolver.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/PlanarJigsawWorkcellResolver.java new file mode 100644 index 000000000..26237e087 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/PlanarJigsawWorkcellResolver.java @@ -0,0 +1,139 @@ +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawWorkcell; +import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import java.util.Objects; + +public final class PlanarJigsawWorkcellResolver { + public static final int DEFAULT_SIZE = 16; + public static final int MAX_WIDTH = 128; + public static final int MAX_HEIGHT = 192; + public static final int MAX_DEPTH = 128; + public static final long MAX_VOLUME = 2_097_152L; + + private PlanarJigsawWorkcellResolver() { + } + + public static Map resolve(IrisStructure structure) { + IrisStructure source = Objects.requireNonNull(structure, "Planar jigsaw structure"); + EnumMap resolved = + new EnumMap<>(IrisJigsawWorkcellArchetype.class); + if (source.getPlanarWorkcells() == null || source.getPlanarWorkcells().isEmpty()) { + IrisPosition fallback = source.getCellSize() == null + ? new IrisPosition(DEFAULT_SIZE, DEFAULT_SIZE, DEFAULT_SIZE) + : source.getCellSize(); + for (IrisJigsawWorkcellArchetype archetype : IrisJigsawWorkcellArchetype.values()) { + ResolvedWorkcell workcell = new ResolvedWorkcell( + archetype, + "", + fallback.getX(), + fallback.getY(), + fallback.getZ(), + true); + validate(workcell); + resolved.put(archetype, workcell); + } + return Collections.unmodifiableMap(resolved); + } + + for (IrisJigsawWorkcell configured : source.getPlanarWorkcells()) { + if (configured == null || configured.getArchetype() == null) { + throw new IllegalArgumentException("Planar jigsaw workcells require an archetype"); + } + ResolvedWorkcell workcell = new ResolvedWorkcell( + configured.getArchetype(), + normalizeDisplayName(configured.getDisplayName()), + configured.getWidth(), + configured.getHeight(), + configured.getDepth(), + configured.isEnabled()); + validate(workcell); + if (resolved.putIfAbsent(workcell.archetype(), workcell) != null) { + throw new IllegalArgumentException( + "Planar jigsaw workcell " + workcell.archetype() + " is configured more than once"); + } + } + if (resolved.size() != IrisJigsawWorkcellArchetype.values().length) { + for (IrisJigsawWorkcellArchetype archetype : IrisJigsawWorkcellArchetype.values()) { + if (!resolved.containsKey(archetype)) { + throw new IllegalArgumentException("Planar jigsaw workcell " + archetype + " is missing"); + } + } + } + return Collections.unmodifiableMap(resolved); + } + + public static ResolvedWorkcell workcell( + Map workcells, + IrisJigsawPiece piece + ) { + Map resolved = Objects.requireNonNull( + workcells, + "Planar jigsaw workcells"); + IrisJigsawWorkcellArchetype archetype = IrisJigsawWorkcellArchetype.fromPiece(piece); + ResolvedWorkcell workcell = resolved.get(archetype); + if (workcell == null) { + throw new IllegalArgumentException("Planar jigsaw workcell " + archetype + " is not configured"); + } + return workcell; + } + + public static IrisPosition canonicalDimensions(IrisJigsawPiece piece, IrisObject object) { + IrisJigsawPiece sourcePiece = Objects.requireNonNull(piece, "Planar jigsaw piece"); + IrisObject sourceObject = Objects.requireNonNull(object, "Planar jigsaw object"); + IrisJigsawWorkcellArchetype archetype = IrisJigsawWorkcellArchetype.fromPiece(sourcePiece); + int quarterTurns = archetype.sourceToCanonicalQuarterTurns(sourcePiece); + return Math.floorMod(quarterTurns, 2) == 0 + ? new IrisPosition(sourceObject.getW(), sourceObject.getH(), sourceObject.getD()) + : new IrisPosition(sourceObject.getD(), sourceObject.getH(), sourceObject.getW()); + } + + public record ResolvedWorkcell( + IrisJigsawWorkcellArchetype archetype, + String displayName, + int width, + int height, + int depth, + boolean enabled + ) { + public ResolvedWorkcell { + Objects.requireNonNull(archetype, "Planar jigsaw workcell archetype"); + displayName = normalizeDisplayName(displayName); + } + + public IrisPosition dimensions() { + return new IrisPosition(width, height, depth); + } + + public boolean contains(IrisPosition dimensions) { + IrisPosition size = Objects.requireNonNull(dimensions, "Planar jigsaw object dimensions"); + return size.getX() <= width && size.getY() <= height && size.getZ() <= depth; + } + } + + private static String normalizeDisplayName(String displayName) { + return displayName == null ? "" : displayName.trim(); + } + + private static void validate(ResolvedWorkcell workcell) { + if (workcell.width() < 3 || workcell.width() > MAX_WIDTH + || workcell.height() < 1 || workcell.height() > MAX_HEIGHT + || workcell.depth() < 3 || workcell.depth() > MAX_DEPTH) { + throw new IllegalArgumentException("Planar jigsaw workcell " + workcell.archetype() + + " dimensions must keep width/depth within 3..128 and height within 1..192"); + } + long volume = (long) workcell.width() * workcell.height() * workcell.depth(); + if (volume > MAX_VOLUME) { + throw new IllegalArgumentException("Planar jigsaw workcell " + workcell.archetype() + + " volume cannot exceed " + MAX_VOLUME + " blocks"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureAssemblyResult.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureAssemblyResult.java new file mode 100644 index 000000000..02d7d1e87 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureAssemblyResult.java @@ -0,0 +1,71 @@ +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.engine.framework.PlacedStructurePiece; + +import java.util.List; +import java.util.Objects; + +public record StructureAssemblyResult( + StructureAssemblyStatus status, + List pieces, + String selectedTheme, + String detail +) { + public StructureAssemblyResult { + status = Objects.requireNonNull(status, "Structure assembly status"); + pieces = List.copyOf(Objects.requireNonNull(pieces, "Structure assembly pieces")); + selectedTheme = normalize(selectedTheme); + detail = normalize(detail); + if (status == StructureAssemblyStatus.COMPLETE && pieces.isEmpty()) { + throw new IllegalArgumentException("A complete structure assembly must contain at least one piece"); + } + if (status == StructureAssemblyStatus.INTENTIONAL_EMPTY && !pieces.isEmpty()) { + throw new IllegalArgumentException("An intentionally empty structure assembly cannot contain pieces"); + } + if (status.isFailure() && detail.isEmpty()) { + throw new IllegalArgumentException("A failed structure assembly requires failure detail"); + } + } + + public static StructureAssemblyResult complete( + List pieces, + String selectedTheme + ) { + return new StructureAssemblyResult( + StructureAssemblyStatus.COMPLETE, + pieces, + selectedTheme, + ""); + } + + public static StructureAssemblyResult intentionalEmpty( + String selectedTheme, + String detail + ) { + return new StructureAssemblyResult( + StructureAssemblyStatus.INTENTIONAL_EMPTY, + List.of(), + selectedTheme, + detail); + } + + public static StructureAssemblyResult failed( + StructureAssemblyStatus status, + List pieces, + String selectedTheme, + String detail + ) { + if (Objects.requireNonNull(status, "Structure assembly failure status").isComplete()) { + throw new IllegalArgumentException("A failed structure assembly requires a failure status"); + } + return new StructureAssemblyResult(status, pieces, selectedTheme, detail); + } + + public boolean hasOutput() { + return status == StructureAssemblyStatus.COMPLETE && !pieces.isEmpty(); + } + + private static String normalize(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureAssemblyStatus.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureAssemblyStatus.java new file mode 100644 index 000000000..c8642ab59 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureAssemblyStatus.java @@ -0,0 +1,17 @@ +package art.arcane.iris.engine.framework.structure; + +public enum StructureAssemblyStatus { + COMPLETE, + INTENTIONAL_EMPTY, + FAILED_UNCAPPED, + FAILED_RULES, + HARD_CAP; + + public boolean isComplete() { + return this == COMPLETE || this == INTENTIONAL_EMPTY; + } + + public boolean isFailure() { + return !isComplete(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphAssemblySample.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphAssemblySample.java new file mode 100644 index 000000000..b045843f6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphAssemblySample.java @@ -0,0 +1,30 @@ +package art.arcane.iris.engine.framework.structure; + +import java.util.List; +import java.util.Objects; + +public record StructureGraphAssemblySample(long seed, Outcome outcome) { + public record Outcome( + List pieceKeys, + StructureAssemblyStatus status, + String detail + ) { + public Outcome { + pieceKeys = List.copyOf(Objects.requireNonNull(pieceKeys)); + status = Objects.requireNonNull(status); + detail = Objects.requireNonNull(detail); + } + + public boolean pieceCapReached() { + return status == StructureAssemblyStatus.HARD_CAP; + } + + public boolean intentionalEmpty() { + return status == StructureAssemblyStatus.INTENTIONAL_EMPTY; + } + + public boolean isComplete() { + return status.isComplete(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCatalog.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCatalog.java new file mode 100644 index 000000000..938bf048f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCatalog.java @@ -0,0 +1,100 @@ +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.StructureAssembler; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RNG; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public final class StructureGraphCatalog { + private static final Cache> CACHE = + Caffeine.newBuilder().weakKeys().build(); + private static final Cache> RUNTIME_OUTPUT = + Caffeine.newBuilder().weakKeys().build(); + private static final List RUNTIME_SAMPLE_SEEDS = List.of( + 0L, 1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L, 55L, 89L, 144L, 233L, 377L, 610L, 987L); + + private StructureGraphCatalog() { + } + + public static StructureGraphCompilation compile(IrisData data, IrisStructure structure) { + IrisData activeData = Objects.requireNonNull(data); + IrisStructure activeStructure = Objects.requireNonNull(structure); + String structureKey = activeStructure.getLoadKey(); + if (structureKey == null || structureKey.isBlank()) { + return compileAndReport(activeData, activeStructure); + } + ConcurrentMap catalog = CACHE.get( + activeData, ignored -> new ConcurrentHashMap<>()); + return catalog.computeIfAbsent(structureKey, ignored -> compileAndReport(activeData, activeStructure)); + } + + public static void invalidate(IrisData data) { + if (data != null) { + CACHE.invalidate(data); + RUNTIME_OUTPUT.invalidate(data); + } + } + + public static boolean guaranteesRuntimeOutput(IrisData data, IrisStructure structure) { + IrisData activeData = Objects.requireNonNull(data); + IrisStructure activeStructure = Objects.requireNonNull(structure); + if (!compile(activeData, activeStructure).guaranteesAssemblyOutput()) { + return false; + } + String structureKey = activeStructure.getLoadKey(); + if (structureKey == null || structureKey.isBlank()) { + return sampleRuntimeAssembly(activeData, activeStructure); + } + ConcurrentMap catalog = RUNTIME_OUTPUT.get( + activeData, ignored -> new ConcurrentHashMap<>()); + return catalog.computeIfAbsent( + structureKey, ignored -> sampleRuntimeAssembly(activeData, activeStructure)); + } + + private static StructureGraphCompilation compileAndReport(IrisData data, IrisStructure structure) { + StructureGraphCompilation compilation = StructureGraphCompiler.compile( + structure, StructureGraphResolver.forData(data)); + String structureKey = structure.getLoadKey() == null || structure.getLoadKey().isBlank() + ? "" : structure.getLoadKey(); + for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) { + String message = "[StructureGraph:" + structureKey + "] " + diagnostic.message(); + if (diagnostic.severity() == StructureGraphDiagnostic.Severity.ERROR) { + IrisLogging.error(message); + } else { + IrisLogging.warn(message); + } + } + if (!compilation.isAssemblyViable()) { + IrisLogging.error("[StructureGraph:" + structureKey + + "] Graph rejected because deterministic assembly did not complete."); + } + return compilation; + } + + private static boolean sampleRuntimeAssembly(IrisData data, IrisStructure structure) { + for (long seed : RUNTIME_SAMPLE_SEEDS) { + try { + StructureAssembler assembler = StructureAssembler.forData( + data, structure, new IrisPosition(0, 64, 0)); + StructureAssemblyResult result = assembler.assemble(new RNG(seed)); + if (!result.hasOutput()) { + return false; + } + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + return false; + } + } + return true; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCompilation.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCompilation.java new file mode 100644 index 000000000..a0accb9c7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCompilation.java @@ -0,0 +1,242 @@ +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawBranchFailurePolicy; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPieceEntry; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisJigsawThemeSet; +import art.arcane.iris.engine.object.IrisObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public final class StructureGraphCompilation { + private final CompiledStructureGraph graph; + private final List diagnostics; + private final List assemblySamples; + + StructureGraphCompilation(Builder builder) { + graph = Objects.requireNonNull(builder.graph); + diagnostics = Collections.unmodifiableList(new ArrayList<>(builder.diagnostics)); + assemblySamples = Collections.unmodifiableList(new ArrayList<>(builder.assemblySamples)); + } + + static Builder builder(CompiledStructureGraph graph) { + return new Builder(graph); + } + + public CompiledStructureGraph getGraph() { + return graph; + } + + public List getDiagnostics() { + return diagnostics; + } + + public List getAssemblySamples() { + return assemblySamples; + } + + public boolean hasErrors() { + for (StructureGraphDiagnostic diagnostic : diagnostics) { + if (diagnostic.severity() == StructureGraphDiagnostic.Severity.ERROR) { + return true; + } + } + return false; + } + + public boolean isAssemblyViable() { + if (hasErrors() || assemblySamples.isEmpty()) { + return false; + } + for (StructureGraphAssemblySample sample : assemblySamples) { + if (!sample.outcome().isComplete()) { + return false; + } + } + return true; + } + + public boolean guaranteesAssemblyOutput() { + if (!isAssemblyViable()) { + return false; + } + String startPoolKey = graph.getStructure().getStartPool(); + if (startPoolKey == null || startPoolKey.isBlank()) { + return false; + } + IrisJigsawPool startPool = graph.getPools().get(startPoolKey.trim()); + if (startPool == null || startPool.getPieces() == null || startPool.getPieces().isEmpty()) { + return false; + } + for (String theme : themes()) { + if (!startPoolGuaranteesOutput(startPool, theme) + || !reachableBranchesCanTerminate(theme)) { + return false; + } + } + return true; + } + + private boolean startPoolGuaranteesOutput(IrisJigsawPool startPool, String theme) { + boolean certainPhysicalEntry = false; + for (IrisJigsawPieceEntry entry : startPool.getPieces()) { + if (entry == null || entry.getWeight() <= 0 || !Double.isFinite(entry.getChance()) + || entry.getChance() <= 0D || entry.getChance() > 1D) { + continue; + } + if (entry.isEmpty()) { + return false; + } + IrisJigsawPiece piece = graph.getPieces().get(normalize(entry.getPiece())); + if (piece == null || !JigsawPoolSelection.pieceEligible(piece, theme, 0, 0)) { + continue; + } + IrisObject object = graph.getObjects().get(normalize(piece.getObject())); + if (object == null || !fitsStartRadius(object)) { + return false; + } + if (entry.getChance() == 1D) { + certainPhysicalEntry = true; + } + } + return certainPhysicalEntry; + } + + private boolean reachableBranchesCanTerminate(String theme) { + for (String pieceKey : graph.getReachablePieces()) { + IrisJigsawPiece piece = graph.getPieces().get(pieceKey); + if (piece == null || piece.getConnectors() == null) { + return false; + } + if (!piece.supportsTheme(theme) || piece.resolvedRules().isTerminal()) { + continue; + } + for (IrisJigsawConnector connector : piece.getConnectors()) { + if (connector == null) { + return false; + } + IrisJigsawPool pool = graph.getPools().get(normalize(connector.getPool())); + if (pool == null) { + return false; + } + if (pool.requiresFallback(graph.getStructure().isRequireCaps())) { + if (!hasRequiredTerminal(pool, theme)) { + return false; + } + } else if (graph.getStructure().resolvedBranchFailurePolicy() + != IrisJigsawBranchFailurePolicy.TERMINATE_BRANCH + && !canTerminateWithoutPlacement(pool)) { + return false; + } + } + } + return true; + } + + private boolean hasRequiredTerminal(IrisJigsawPool pool, String theme) { + String fallbackKey = JigsawPoolSelection.directFallbackKey(pool); + if (fallbackKey.isEmpty()) { + return false; + } + IrisJigsawPool fallback = graph.getPools().get(fallbackKey); + if (fallback == null || fallback.getPieces() == null) { + return false; + } + for (IrisJigsawPieceEntry entry : fallback.getPieces()) { + if (entry == null || entry.isEmpty() || entry.getWeight() <= 0 || entry.getChance() != 1D) { + continue; + } + IrisJigsawPiece piece = graph.getPieces().get(normalize(entry.getPiece())); + if (piece != null && piece.supportsTheme(theme) && piece.resolvedRules().isTerminal()) { + return true; + } + } + return false; + } + + private boolean canTerminateWithoutPlacement(IrisJigsawPool pool) { + if (hasEmptyChoice(pool)) { + return true; + } + if (pool == null) { + return false; + } + String fallbackKey = JigsawPoolSelection.directFallbackKey(pool); + return !fallbackKey.isEmpty() && hasEmptyChoice(graph.getPools().get(fallbackKey)); + } + + private boolean hasEmptyChoice(IrisJigsawPool pool) { + if (pool == null || pool.getPieces() == null) { + return false; + } + if (pool.getPieces().isEmpty()) { + return true; + } + for (IrisJigsawPieceEntry entry : pool.getPieces()) { + if (entry != null && entry.getWeight() > 0 && entry.getChance() == 1D && entry.isEmpty()) { + return true; + } + } + return false; + } + + private boolean fitsStartRadius(IrisObject object) { + long radius = (long) graph.getStructure().getMaxSizeChunks() * 16L; + return axisFitsRadius(object.getW(), radius) && axisFitsRadius(object.getD(), radius); + } + + private List themes() { + if (graph.getStructure().getThemeSets() == null || graph.getStructure().getThemeSets().isEmpty()) { + return List.of(""); + } + List themes = new ArrayList<>(graph.getStructure().getThemeSets().size()); + for (IrisJigsawThemeSet theme : graph.getStructure().getThemeSets()) { + if (theme != null && theme.getKey() != null && !theme.getKey().isBlank()) { + themes.add(theme.getKey().trim()); + } + } + return List.copyOf(themes); + } + + private boolean axisFitsRadius(int size, long radius) { + if (size < 1 || radius < 1L) { + return false; + } + long minimum = -(size / 2L); + long maximum = minimum + size - 1L; + return minimum >= -radius && maximum <= radius; + } + + private String normalize(String value) { + return value == null ? "" : value.trim(); + } + + static final class Builder { + private final CompiledStructureGraph graph; + private final List diagnostics = new ArrayList<>(); + private final List assemblySamples = new ArrayList<>(); + + private Builder(CompiledStructureGraph graph) { + this.graph = Objects.requireNonNull(graph); + } + + Builder diagnostics(List values) { + diagnostics.addAll(values); + return this; + } + + Builder assemblySamples(List values) { + assemblySamples.addAll(values); + return this; + } + + StructureGraphCompilation build() { + return new StructureGraphCompilation(this); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCompiler.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCompiler.java new file mode 100644 index 000000000..48546695e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphCompiler.java @@ -0,0 +1,1264 @@ +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.engine.framework.PlacedStructurePiece; +import art.arcane.iris.engine.framework.StructureAssembler; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawBranchFailurePolicy; +import art.arcane.iris.engine.object.IrisJigsawCompatibility; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawMode; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPieceEntry; +import art.arcane.iris.engine.object.IrisJigsawPieceRules; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisJigsawThemeSet; +import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.JigsawJoint; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public final class StructureGraphCompiler { + private static final int ASSEMBLY_PIECE_CAP = 512; + private static final int MAX_DEPTH = 30; + private static final int MAX_SIZE_CHUNKS = 32; + private static final int[] NO_ROTATION = {0}; + private static final int[] Y_ROTATIONS = {0, 90, 180, 270}; + private static final List ASSEMBLY_SEEDS = List.of(0L, 1L, 2L, 3L, 5L, 8L, 13L, 21L); + + private StructureGraphCompiler() { + } + + public static StructureGraphCompilation compile(IrisStructure structure, StructureGraphResolver resolver) { + CompilationState state = new CompilationState( + Objects.requireNonNull(structure), Objects.requireNonNull(resolver)); + state.loadClosure(); + state.detectFallbackCycles(); + state.analyzeReachability(); + state.validatePlanarReachableObjects(); + state.validateRuleFeasibility(); + state.validateRequiredCaps(); + CompiledStructureGraph graph = state.buildGraph(); + StructureGraphCompilation validated = StructureGraphCompilation.builder(graph) + .diagnostics(state.diagnostics()) + .build(); + List samples = state.sampleAssemblies(validated); + state.reportPieceCapSamples(samples); + return StructureGraphCompilation.builder(graph) + .diagnostics(state.diagnostics()) + .assemblySamples(samples) + .build(); + } + + private static final class CompilationState { + private final IrisStructure structure; + private final StructureGraphResolver resolver; + private final CompiledStructureGraph.Builder graph; + private final Deque pendingPools = new ArrayDeque<>(); + private final Map> poolReferences = new LinkedHashMap<>(); + private final Map> poolEntries = new LinkedHashMap<>(); + private final Map> pieceConnectors = new LinkedHashMap<>(); + private final Map pieceObjects = new LinkedHashMap<>(); + private final Set processedPools = new HashSet<>(); + private final Set missingPools = new HashSet<>(); + private final Set attemptedPieces = new HashSet<>(); + private final Set missingPieces = new HashSet<>(); + private final Set attemptedObjects = new HashSet<>(); + private final Set missingObjects = new HashSet<>(); + private final Set reachableEntries = new LinkedHashSet<>(); + private final Set reachablePieceStates = new LinkedHashSet<>(); + private final Map> reachablePiecesByTheme = new LinkedHashMap<>(); + private final Map> activeConnectors = new LinkedHashMap<>(); + private final List diagnostics = new ArrayList<>(); + private final Set diagnosticKeys = new HashSet<>(); + private Map + planarWorkcells = Map.of(); + + private CompilationState(IrisStructure structure, StructureGraphResolver resolver) { + this.structure = structure; + this.resolver = resolver; + this.graph = CompiledStructureGraph.builder(structure); + } + + private void loadClosure() { + validateStructureLimits(); + String startPool = normalize(structure.getStartPool()); + if (startPool.isEmpty()) { + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_START_POOL, + "Structure '" + structureKey() + "' does not declare a start pool.", + "structure:start-pool:empty"); + return; + } + + requestPool(startPool, new PoolReference( + "Structure '" + structureKey() + "' references missing start pool '" + startPool + "'.", + StructureGraphDiagnostic.Code.MISSING_START_POOL)); + while (!pendingPools.isEmpty()) { + processPool(pendingPools.removeFirst()); + } + } + + private void validateStructureLimits() { + validateThemes(); + if (structure.getMaxDepth() < 1 || structure.getMaxDepth() > MAX_DEPTH) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_MAX_DEPTH, + "Structure '" + structureKey() + "' has maxDepth " + structure.getMaxDepth() + + "; it must be between 1 and " + MAX_DEPTH + ".", + "structure:max-depth"); + } + if (structure.getMaxSizeChunks() < 1 || structure.getMaxSizeChunks() > MAX_SIZE_CHUNKS) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_MAX_SIZE, + "Structure '" + structureKey() + "' has maxSizeChunks " + structure.getMaxSizeChunks() + + "; it must be between 1 and " + MAX_SIZE_CHUNKS + ".", + "structure:max-size"); + } + if (structure.resolvedMode() != IrisJigsawMode.PLANAR_JIGSAW) { + return; + } + try { + planarWorkcells = PlanarJigsawWorkcellResolver.resolve(structure); + } catch (IllegalArgumentException exception) { + boolean legacy = structure.getPlanarWorkcells() == null || structure.getPlanarWorkcells().isEmpty(); + addDiagnostic( + legacy + ? StructureGraphDiagnostic.Code.INVALID_PLANAR_CELL_SIZE + : StructureGraphDiagnostic.Code.INVALID_PLANAR_WORKCELL, + "Structure '" + structureKey() + "' has invalid planar workcells: " + + exception.getMessage() + ".", + "structure:planar-workcells"); + } + } + + private void validateThemes() { + KList themeSets = structure.getThemeSets(); + if (themeSets == null) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_THEME, + "Structure '" + structureKey() + "' declares a null themeSets list.", + "structure:themes:null"); + return; + } + Set keys = new LinkedHashSet<>(); + for (int index = 0; index < themeSets.size(); index++) { + IrisJigsawThemeSet theme = themeSets.get(index); + if (theme == null) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_THEME, + "Structure '" + structureKey() + "' themeSets[" + index + "] is null.", + "structure:theme:null:" + index); + continue; + } + String key = normalize(theme.getKey()); + if (key.isEmpty() || !key.equals(theme.getKey())) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_THEME, + "Structure '" + structureKey() + "' themeSets[" + index + + "] must declare a non-blank, whitespace-normalized key.", + "structure:theme:key:" + index); + } else if (!keys.add(key)) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_THEME, + "Structure '" + structureKey() + "' declares duplicate theme key '" + key + "'.", + "structure:theme:duplicate:" + key); + } + if (theme.getWeight() <= 0) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_WEIGHT, + "Structure '" + structureKey() + "' theme '" + key + + "' has non-positive weight " + theme.getWeight() + ".", + "structure:theme:weight:" + index); + } + } + if (!themeSets.isEmpty() + && structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE) { + addDiagnostic(StructureGraphDiagnostic.Code.NON_PORTABLE_METADATA, + "Structure '" + structureKey() + + "' declares coherent Iris theme sets in a VANILLA_PORTABLE graph.", + "structure:portable:themes"); + } + if (structure.isRequireCaps() + && structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE) { + addDiagnostic(StructureGraphDiagnostic.Code.NON_PORTABLE_METADATA, + "Structure '" + structureKey() + + "' requires physical terminal caps, which vanilla jigsaws cannot enforce.", + "structure:portable:require-caps"); + } + if (structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE + && structure.resolvedBranchFailurePolicy() + != IrisJigsawBranchFailurePolicy.TERMINATE_BRANCH) { + addDiagnostic(StructureGraphDiagnostic.Code.NON_PORTABLE_METADATA, + "Structure '" + structureKey() + + "' fails unresolved optional connector branches, which vanilla jigsaws terminate.", + "structure:portable:branch-failure-policy"); + } + } + + private void requestPool(String rawKey, PoolReference reference) { + String key = normalize(rawKey); + if (key.isEmpty()) { + return; + } + poolReferences.computeIfAbsent(key, ignored -> new ArrayList<>()).add(reference); + if (missingPools.contains(key)) { + addDiagnostic(reference.code(), reference.message(), "missing-pool:" + key + ":" + reference.message()); + return; + } + if (!processedPools.contains(key) && !pendingPools.contains(key)) { + pendingPools.addLast(key); + } + } + + private void processPool(String key) { + if (!processedPools.add(key)) { + return; + } + IrisJigsawPool pool = resolver.loadPool(key); + if (pool == null) { + missingPools.add(key); + for (PoolReference reference : poolReferences.getOrDefault(key, List.of())) { + addDiagnostic(reference.code(), reference.message(), + "missing-pool:" + key + ":" + reference.message()); + } + return; + } + + graph.pools().put(key, pool); + List entries = new ArrayList<>(); + poolEntries.put(key, entries); + KList configuredEntries = pool.getPieces(); + if (configuredEntries == null || configuredEntries.isEmpty()) { + if (key.equals(normalize(structure.getStartPool()))) { + addDiagnostic(StructureGraphDiagnostic.Code.EMPTY_START_POOL, + "Jigsaw pool '" + key + "' is empty.", + "empty-pool:" + key); + } + } else { + for (int index = 0; index < configuredEntries.size(); index++) { + scanPoolEntry(key, index, configuredEntries.get(index), entries); + } + } + + String fallback = JigsawPoolSelection.directFallbackKey(pool); + if (pool.isMandatoryFallback() && fallback.isEmpty()) { + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_REQUIRED_FALLBACK, + "Jigsaw pool '" + key + + "' requires a direct fallback but does not declare one.", + "pool:required-fallback:" + key); + } + if (pool.isMandatoryFallback() + && structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE) { + addDiagnostic(StructureGraphDiagnostic.Code.NON_PORTABLE_METADATA, + "Jigsaw pool '" + key + + "' declares mandatoryFallback, which vanilla jigsaws cannot enforce.", + "pool:portable:mandatory-fallback:" + key); + } + if (!fallback.isEmpty()) { + requestPool(fallback, new PoolReference( + "Jigsaw pool '" + key + "' references missing fallback pool '" + fallback + "'.", + StructureGraphDiagnostic.Code.MISSING_POOL)); + } + } + + private void scanPoolEntry(String poolKey, int index, IrisJigsawPieceEntry entry, + List entries) { + if (entry == null) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_POOL_ENTRY, + "Jigsaw pool '" + poolKey + "' pieces[" + index + "] is null.", + "pool-entry:null:" + poolKey + ":" + index); + return; + } + + String pieceKey = normalize(entry.getPiece()); + PoolEntryReference entryReference = new PoolEntryReference( + poolKey, index, pieceKey, entry.getWeight(), entry.getChance(), entry.isEmpty()); + entries.add(entryReference); + if (entry.getWeight() <= 0) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_WEIGHT, + "Jigsaw pool '" + poolKey + "' pieces[" + index + "] has non-positive weight " + + entry.getWeight() + ".", + "pool-entry:weight:" + poolKey + ":" + index); + } + if (!Double.isFinite(entry.getChance()) || entry.getChance() < 0D || entry.getChance() > 1D) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CHANCE, + "Jigsaw pool '" + poolKey + "' pieces[" + index + "] has chance " + + entry.getChance() + "; it must be finite and within 0..1.", + "pool-entry:chance:" + poolKey + ":" + index); + } + if (entry.getChance() != 1D + && structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE) { + addDiagnostic(StructureGraphDiagnostic.Code.NON_PORTABLE_METADATA, + "Jigsaw pool '" + poolKey + "' pieces[" + index + + "] declares an Iris chance gate in a VANILLA_PORTABLE graph.", + "pool-entry:portable-chance:" + poolKey + ":" + index); + } + if (entry.isEmpty()) { + if (!pieceKey.isEmpty()) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_POOL_ENTRY, + "Jigsaw pool '" + poolKey + "' pieces[" + index + + "] declares both empty=true and a piece.", + "pool-entry:ambiguous-empty:" + poolKey + ":" + index); + } + return; + } + if (pieceKey.isEmpty()) { + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_PIECE, + "Jigsaw pool '" + poolKey + "' pieces[" + index + "] does not declare a piece.", + "pool-entry:piece-empty:" + poolKey + ":" + index); + return; + } + loadPiece(pieceKey, "Jigsaw pool '" + poolKey + "' pieces[" + index + + "] references missing piece '" + pieceKey + "'."); + } + + private void loadPiece(String key, String missingMessage) { + if (missingPieces.contains(key)) { + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_PIECE, missingMessage, + "missing-piece:" + key + ":" + missingMessage); + return; + } + if (!attemptedPieces.add(key)) { + return; + } + + IrisJigsawPiece piece = resolver.loadPiece(key); + if (piece == null) { + missingPieces.add(key); + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_PIECE, missingMessage, + "missing-piece:" + key + ":" + missingMessage); + return; + } + + graph.pieces().put(key, piece); + IrisObject object = loadPieceObject(key, piece); + validatePieceMetadata(key, piece); + scanConnectors(key, piece, object); + } + + private void validatePieceMetadata(String pieceKey, IrisJigsawPiece piece) { + KList themes = piece.getThemes(); + if (themes == null) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_THEME, + "Jigsaw piece '" + pieceKey + "' declares a null themes list.", + "piece:themes:null:" + pieceKey); + } else { + Set declaredThemes = declaredThemeKeys(); + Set seenThemes = new LinkedHashSet<>(); + for (int index = 0; index < themes.size(); index++) { + String configured = themes.get(index); + String theme = normalize(configured); + if (theme.isEmpty() || !theme.equals(configured)) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_THEME, + "Jigsaw piece '" + pieceKey + "' themes[" + index + + "] must be non-blank and whitespace-normalized.", + "piece:theme:key:" + pieceKey + ":" + index); + } else if (!seenThemes.add(theme)) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_THEME, + "Jigsaw piece '" + pieceKey + "' declares duplicate theme '" + theme + "'.", + "piece:theme:duplicate:" + pieceKey + ":" + theme); + } else if (!declaredThemes.contains(theme)) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_THEME, + "Jigsaw piece '" + pieceKey + "' references undeclared theme '" + theme + "'.", + "piece:theme:undeclared:" + pieceKey + ":" + theme); + } + } + } + + IrisJigsawPieceRules rules = piece.resolvedRules(); + boolean validDepths = rules.getMinimumDepth() >= 0 + && rules.getMaximumDepth() >= rules.getMinimumDepth() + && rules.getMaximumDepth() <= MAX_DEPTH; + boolean validPlacements = rules.getMinimumPlacements() >= 0 + && rules.getMaximumPlacements() >= 0 + && rules.getMinimumPlacements() <= ASSEMBLY_PIECE_CAP + && rules.getMaximumPlacements() <= ASSEMBLY_PIECE_CAP + && (rules.getMaximumPlacements() == 0 + || rules.getMinimumPlacements() <= rules.getMaximumPlacements()); + if (!validDepths || !validPlacements) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_PIECE_RULE, + "Jigsaw piece '" + pieceKey + + "' has invalid depth or placement-count rules.", + "piece:rules:" + pieceKey); + } + if (structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE + && (themes != null && !themes.isEmpty() || !defaultRules(rules))) { + addDiagnostic(StructureGraphDiagnostic.Code.NON_PORTABLE_METADATA, + "Jigsaw piece '" + pieceKey + + "' declares Iris theme or placement rules in a VANILLA_PORTABLE graph.", + "piece:portable:metadata:" + pieceKey); + } + } + + private Set declaredThemeKeys() { + Set themes = new LinkedHashSet<>(); + if (structure.getThemeSets() == null) { + return themes; + } + for (IrisJigsawThemeSet theme : structure.getThemeSets()) { + if (theme != null && !normalize(theme.getKey()).isEmpty()) { + themes.add(normalize(theme.getKey())); + } + } + return themes; + } + + private boolean defaultRules(IrisJigsawPieceRules rules) { + return rules.getMinimumDepth() == 0 + && rules.getMaximumDepth() == MAX_DEPTH + && rules.getMinimumPlacements() == 0 + && rules.getMaximumPlacements() == 0 + && !rules.isTerminal(); + } + + private IrisObject loadPieceObject(String pieceKey, IrisJigsawPiece piece) { + String objectKey = normalize(piece.getObject()); + pieceObjects.put(pieceKey, objectKey); + if (objectKey.isEmpty()) { + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_OBJECT, + "Jigsaw piece '" + pieceKey + "' does not declare an object.", + "piece-object:empty:" + pieceKey); + return null; + } + if (missingObjects.contains(objectKey)) { + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_OBJECT, + "Jigsaw piece '" + pieceKey + "' references missing object '" + objectKey + "'.", + "missing-object:" + objectKey + ":" + pieceKey); + return null; + } + if (!attemptedObjects.add(objectKey)) { + return graph.objects().get(objectKey); + } + + IrisObject object = resolver.loadObject(objectKey); + if (object == null) { + missingObjects.add(objectKey); + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_OBJECT, + "Jigsaw piece '" + pieceKey + "' references missing object '" + objectKey + "'.", + "missing-object:" + objectKey + ":" + pieceKey); + return null; + } + graph.objects().put(objectKey, object); + if (object.getW() < 1 || object.getH() < 1 || object.getD() < 1) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_OBJECT_BOUNDS, + "Object '" + objectKey + "' used by jigsaw piece '" + pieceKey + + "' has invalid dimensions " + object.getW() + "x" + object.getH() + "x" + + object.getD() + ".", + "object-bounds:" + objectKey); + } + return object; + } + + private void scanConnectors(String pieceKey, IrisJigsawPiece piece, IrisObject object) { + List references = new ArrayList<>(); + pieceConnectors.put(pieceKey, references); + KList connectors = piece.getConnectors(); + if (connectors == null) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR, + "Jigsaw piece '" + pieceKey + "' declares a null connector list.", + "connector-list:null:" + pieceKey); + return; + } + for (int index = 0; index < connectors.size(); index++) { + IrisJigsawConnector connector = connectors.get(index); + if (connector == null) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + "] is null.", + "connector:null:" + pieceKey + ":" + index); + continue; + } + + boolean validDirection = connector.getDirection() != null; + if (!validDirection) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR_DIRECTION, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] does not declare a direction.", + "connector:direction:" + pieceKey + ":" + index); + } + boolean validTop = connector.getTop() != null; + if (!validTop) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR_DIRECTION, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] does not declare a top direction.", + "connector:top:" + pieceKey + ":" + index); + } + boolean validJoint = connector.getJoint() != null; + if (!validJoint) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] does not declare a joint type.", + "connector:joint:" + pieceKey + ":" + index); + } + boolean validPosition = isValidPosition(connector.getPosition(), object); + if (connector.getPosition() == null || object != null && !validPosition) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR_POSITION, + invalidPositionMessage(pieceKey, index, connector.getPosition(), object), + "connector:position:" + pieceKey + ":" + index); + } + + boolean validNames = connector.getName() != null && connector.getTargetName() != null; + if (!validNames) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] must declare non-null name and targetName values.", + "connector:names:" + pieceKey + ":" + index); + } + + boolean validMetadata = connector.getChannel() != null + && connector.getFinalState() != null + && !connector.getFinalState().isBlank(); + if (!validMetadata) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_CONNECTOR_METADATA, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] must declare a channel and non-blank finalState.", + "connector:metadata:" + pieceKey + ":" + index); + } + if (structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE + && connector.getChannel() != null && !connector.getChannel().isBlank()) { + addDiagnostic(StructureGraphDiagnostic.Code.NON_PORTABLE_CONNECTOR, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] declares Iris-only channel '" + connector.getChannel() + + "' in a VANILLA_PORTABLE structure.", + "connector:portable-channel:" + pieceKey + ":" + index); + } + + boolean validPlanarContract = validatePlanarConnector( + pieceKey, index, connector, object, validDirection, validTop, validPosition); + + String poolKey = normalize(connector.getPool()); + if (poolKey.isEmpty()) { + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_CONNECTOR_POOL, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] does not declare a target pool.", + "connector:pool-empty:" + pieceKey + ":" + index); + } else { + requestPool(poolKey, new PoolReference( + "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] references missing pool '" + poolKey + "'.", + StructureGraphDiagnostic.Code.MISSING_POOL)); + } + references.add(new ConnectorReference( + pieceKey, index, connector, validPosition, validDirection, validTop, validJoint, validNames, + validMetadata, validPlanarContract)); + } + } + + private boolean validatePlanarConnector(String pieceKey, int index, IrisJigsawConnector connector, + IrisObject object, boolean validDirection, boolean validTop, + boolean validPosition) { + if (structure.resolvedMode() != IrisJigsawMode.PLANAR_JIGSAW) { + return true; + } + + boolean valid = true; + if (validDirection && connector.getDirection().isVertical()) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_PLANAR_CONNECTOR, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + "] uses vertical direction " + + connector.getDirection().name() + + "; planar connectors must face north, east, south," + + " or west.", + "connector:planar-direction:" + pieceKey + ":" + index); + valid = false; + } + if (validTop && connector.getTop() != IrisDirection.UP_POSITIVE_Y) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_PLANAR_CONNECTOR, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + "] has top " + + connector.getTop().name() + + "; planar connectors must use UP_POSITIVE_Y to keep" + + " connected cells on one Y level.", + "connector:planar-top:" + pieceKey + ":" + index); + valid = false; + } + + if (!validDirection || connector.getDirection().isVertical() + || object == null || !validPosition) { + return valid; + } + IrisPosition objectSize = new IrisPosition(object.getW(), object.getH(), object.getD()); + IrisPosition expected = IrisJigsawConnector.canonicalPlanarPosition( + objectSize, connector.getDirection()); + if (!expected.equals(connector.getPosition())) { + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_PLANAR_CONNECTOR, + "Jigsaw piece '" + pieceKey + "' connectors[" + index + "] is at " + + connector.getPosition() + "; the canonical " + connector.getDirection().name() + + " face-center socket for object dimensions " + objectSize + " is " + expected + ".", + "connector:planar-position:" + pieceKey + ":" + index); + valid = false; + } + return valid; + } + + private String invalidPositionMessage(String pieceKey, int index, IrisPosition position, IrisObject object) { + if (position == null) { + return "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] does not declare a position."; + } + if (object == null) { + return "Jigsaw piece '" + pieceKey + "' connectors[" + index + + "] position " + position + " cannot be checked because its object is missing."; + } + return "Jigsaw piece '" + pieceKey + "' connectors[" + index + "] position " + position + + " is outside object bounds 0.." + (object.getW() - 1) + ", 0.." + (object.getH() - 1) + + ", 0.." + (object.getD() - 1) + "."; + } + + private boolean isValidPosition(IrisPosition position, IrisObject object) { + if (position == null || object == null) { + return false; + } + return position.getX() >= 0 && position.getX() < object.getW() + && position.getY() >= 0 && position.getY() < object.getH() + && position.getZ() >= 0 && position.getZ() < object.getD(); + } + + private void detectFallbackCycles() { + Map states = new HashMap<>(); + for (String poolKey : graph.pools().keySet()) { + detectFallbackCycle(poolKey, states); + } + } + + private void detectFallbackCycle(String startPool, Map states) { + if (states.getOrDefault(startPool, 0) == 2) { + return; + } + List path = new ArrayList<>(); + Map pathIndexes = new HashMap<>(); + String poolKey = startPool; + while (!poolKey.isEmpty() && graph.pools().containsKey(poolKey) + && states.getOrDefault(poolKey, 0) != 2) { + Integer cycleStart = pathIndexes.get(poolKey); + if (cycleStart != null) { + List cycle = new ArrayList<>(path.subList(cycleStart, path.size())); + cycle.add(poolKey); + String cyclePath = String.join(" -> ", cycle); + addDiagnostic(StructureGraphDiagnostic.Code.FALLBACK_CYCLE, + "Jigsaw pool fallback cycle detected: " + cyclePath + ".", + "fallback-cycle:" + cyclePath); + break; + } + pathIndexes.put(poolKey, path.size()); + path.add(poolKey); + states.put(poolKey, 1); + IrisJigsawPool pool = graph.pools().get(poolKey); + poolKey = pool == null ? "" : JigsawPoolSelection.directFallbackKey(pool); + } + for (String visitedPool : path) { + states.put(visitedPool, 2); + } + } + + private void analyzeReachability() { + String startPool = normalize(structure.getStartPool()); + if (!graph.pools().containsKey(startPool)) { + return; + } + graph.reachablePools().add(startPool); + for (String theme : reachabilityThemes()) { + analyzeThemeReachability(startPool, theme); + } + reportUnmatchedReachableConnectors(); + reportUnreachableResources(); + } + + private void analyzeThemeReachability(String startPool, String theme) { + Deque pendingPieces = new ArrayDeque<>(); + boolean viableStart = false; + for (PoolEntryReference entry : poolEntries.getOrDefault(startPool, List.of())) { + if (!entryIsViable(entry, theme, 0)) { + continue; + } + viableStart = true; + markReachableEntry(entry, theme); + if (entry.empty()) { + continue; + } + IrisJigsawPiece piece = graph.pieces().get(entry.pieceKey()); + if (piece == null) { + continue; + } + for (int rotation : rotationsFor(piece)) { + markReachablePieceState( + new PieceReachState(entry.pieceKey(), -1, rotation, 0, theme), + pendingPieces); + } + } + if (!viableStart) { + boolean disabledStart = false; + for (PoolEntryReference entry : poolEntries.getOrDefault(startPool, List.of())) { + if (!entry.empty() && entryIsResolvable(entry) && !pieceEnabled(entry.pieceKey())) { + disabledStart = true; + break; + } + } + if (disabledStart) { + addDiagnostic(StructureGraphDiagnostic.Code.NO_ENABLED_START_PIECE, + "Structure '" + structureKey() + + "' has no enabled planar workcell represented by a viable start entry.", + "structure:no-enabled-start:" + theme); + } else if (!theme.isEmpty()) { + addDiagnostic(StructureGraphDiagnostic.Code.UNSATISFIABLE_PIECE_RULE, + "Structure '" + structureKey() + "' theme '" + theme + + "' has no positive-chance start entry eligible at depth zero.", + "structure:no-theme-start:" + theme); + } else { + addDiagnostic(StructureGraphDiagnostic.Code.NO_VIABLE_START_PIECE, + "Structure '" + structureKey() + "' has no start-pool entry with a positive weight," + + " positive chance, resolvable piece, and resolvable object.", + "structure:no-viable-start"); + } + } + + while (!pendingPieces.isEmpty()) { + PieceReachState pieceState = pendingPieces.removeFirst(); + for (ConnectorReference source : pieceConnectors.getOrDefault(pieceState.pieceKey(), List.of())) { + if (source.index() == pieceState.skippedConnectorIndex() || !source.canSource()) { + continue; + } + OrientedConnector orientedSource = new OrientedConnector(source, pieceState.rotation()); + ReachableConnectorKey connectorKey = new ReachableConnectorKey( + pieceState.theme(), source.id(), pieceState.depth()); + activeConnectors.computeIfAbsent(connectorKey, ignored -> new ArrayList<>()) + .add(orientedSource); + boolean includePrimary = pieceState.depth() < Math.max(1, structure.getMaxDepth()); + int candidateDepth = Math.min( + pieceState.depth() + 1, + Math.max(1, structure.getMaxDepth())); + List candidatePools = candidatePools( + normalize(source.connector().getPool()), includePrimary); + for (String candidatePool : candidatePools) { + graph.reachablePools().add(candidatePool); + for (PoolEntryReference entry : poolEntries.getOrDefault(candidatePool, List.of())) { + if (!entryIsViable(entry, pieceState.theme(), candidateDepth)) { + continue; + } + if (entry.empty()) { + markReachableEntry(entry, pieceState.theme()); + continue; + } + List compatible = findCompatibleConnectors( + orientedSource, entry.pieceKey()); + if (!compatible.isEmpty()) { + markReachableEntry(entry, pieceState.theme()); + if (!includePrimary) { + continue; + } + for (ConnectorMatch target : compatible) { + markReachablePieceState( + new PieceReachState(entry.pieceKey(), target.connector().index(), + target.rotation(), candidateDepth, pieceState.theme()), + pendingPieces); + } + } + } + } + } + } + } + + private List reachabilityThemes() { + if (structure.getThemeSets() == null || structure.getThemeSets().isEmpty()) { + return List.of(""); + } + List themes = new ArrayList<>(structure.getThemeSets().size()); + for (IrisJigsawThemeSet theme : structure.getThemeSets()) { + if (theme != null && !normalize(theme.getKey()).isEmpty()) { + themes.add(normalize(theme.getKey())); + } + } + return themes.isEmpty() ? List.of("") : List.copyOf(themes); + } + + private void validatePlanarReachableObjects() { + if (structure.resolvedMode() != IrisJigsawMode.PLANAR_JIGSAW + || planarWorkcells.isEmpty()) { + return; + } + for (String pieceKey : graph.reachablePieces()) { + IrisJigsawPiece piece = graph.pieces().get(pieceKey); + String objectKey = pieceObjects.get(pieceKey); + IrisObject object = graph.objects().get(objectKey); + if (piece == null || object == null) { + continue; + } + PlanarJigsawWorkcellResolver.ResolvedWorkcell workcell = + PlanarJigsawWorkcellResolver.workcell(planarWorkcells, piece); + IrisPosition dimensions = PlanarJigsawWorkcellResolver.canonicalDimensions(piece, object); + if (workcell.contains(dimensions)) { + continue; + } + addDiagnostic(StructureGraphDiagnostic.Code.INVALID_PLANAR_OBJECT_SIZE, + "Object '" + objectKey + "' used by reachable planar piece '" + pieceKey + + "' is " + object.getW() + "x" + object.getH() + "x" + object.getD() + + " and canonically occupies " + dimensions.getX() + "x" + dimensions.getY() + + "x" + dimensions.getZ() + "; it does not fit " + workcell.archetype() + + " workcell " + workcell.width() + "x" + workcell.height() + "x" + + workcell.depth() + ".", + "object:planar-size:" + pieceKey); + } + } + + private void markReachableEntry(PoolEntryReference entry, String theme) { + reachableEntries.add(entry.id()); + if (!entry.empty()) { + graph.reachablePieces().add(entry.pieceKey()); + reachablePiecesByTheme.computeIfAbsent(theme, ignored -> new LinkedHashSet<>()) + .add(entry.pieceKey()); + } + } + + private void markReachablePieceState(PieceReachState state, Deque pendingPieces) { + if (!graph.reachablePieces().contains(state.pieceKey())) { + return; + } + IrisJigsawPiece piece = graph.pieces().get(state.pieceKey()); + if (piece == null || piece.resolvedRules().isTerminal()) { + return; + } + if (reachablePieceStates.add(state)) { + pendingPieces.addLast(state); + } + } + + private boolean entryIsViable(PoolEntryReference entry, String theme, int depth) { + if (!entryIsResolvable(entry)) { + return false; + } + if (entry.empty()) { + return true; + } + IrisJigsawPiece piece = graph.pieces().get(entry.pieceKey()); + return piece != null + && pieceEnabled(entry.pieceKey()) + && JigsawPoolSelection.pieceEligible(piece, theme, depth, 0); + } + + private boolean entryIsResolvable(PoolEntryReference entry) { + if (entry.weight() <= 0 || !Double.isFinite(entry.chance()) || entry.chance() <= 0D + || entry.chance() > 1D) { + return false; + } + if (entry.empty()) { + return entry.pieceKey().isEmpty(); + } + if (entry.pieceKey().isEmpty() || !graph.pieces().containsKey(entry.pieceKey())) { + return false; + } + String objectKey = pieceObjects.getOrDefault(entry.pieceKey(), ""); + return !objectKey.isEmpty() && graph.objects().containsKey(objectKey); + } + + private boolean entryIsViableForAnyTheme(PoolEntryReference entry) { + for (String theme : reachabilityThemes()) { + for (int depth = 0; depth <= Math.max(1, structure.getMaxDepth()); depth++) { + if (entryIsViable(entry, theme, depth)) { + return true; + } + } + } + return false; + } + + private boolean pieceEnabled(String pieceKey) { + if (structure.resolvedMode() != IrisJigsawMode.PLANAR_JIGSAW || planarWorkcells.isEmpty()) { + return true; + } + IrisJigsawPiece piece = graph.pieces().get(pieceKey); + return piece == null || PlanarJigsawWorkcellResolver.workcell(planarWorkcells, piece).enabled(); + } + + private void reportUnmatchedReachableConnectors() { + for (Map.Entry> active : activeConnectors.entrySet()) { + ReachableConnectorKey connectorKey = active.getKey(); + List orientations = active.getValue(); + OrientedConnector representative = orientations.getFirst(); + boolean includePrimary = connectorKey.depth() < Math.max(1, structure.getMaxDepth()); + int candidateDepth = Math.min( + connectorKey.depth() + 1, + Math.max(1, structure.getMaxDepth())); + List candidates = candidatePools( + normalize(representative.definition().getPool()), includePrimary); + boolean hasViableEntry = false; + boolean hasCompatibleEntry = false; + for (String candidatePool : candidates) { + for (PoolEntryReference entry : poolEntries.getOrDefault(candidatePool, List.of())) { + if (!entryIsViable(entry, connectorKey.theme(), candidateDepth)) { + continue; + } + hasViableEntry = true; + if (entry.empty()) { + hasCompatibleEntry = true; + break; + } + for (OrientedConnector source : orientations) { + if (!findCompatibleConnectors(source, entry.pieceKey()).isEmpty()) { + hasCompatibleEntry = true; + break; + } + } + if (hasCompatibleEntry) { + break; + } + } + if (hasCompatibleEntry) { + break; + } + } + if (hasViableEntry && !hasCompatibleEntry) { + addDiagnostic(StructureGraphDiagnostic.Code.NO_COMPATIBLE_CONNECTOR, + "Jigsaw piece '" + representative.connector().pieceKey() + "' connectors[" + + representative.connector().index() + "] targets pool '" + + normalize(representative.definition().getPool()) + + "', but no reachable candidate exposes the requested target name with a" + + " compatible direction for theme '" + themeLabel(connectorKey.theme()) + "'.", + "connector:no-match:" + representative.connector().pieceKey() + ":" + + representative.connector().index() + ":" + connectorKey.theme() + + ":" + connectorKey.depth()); + } + } + } + + private void reportUnreachableResources() { + for (String poolKey : graph.pools().keySet()) { + if (!graph.reachablePools().contains(poolKey)) { + addDiagnostic(StructureGraphDiagnostic.Code.UNREACHABLE_POOL, + "Jigsaw pool '" + poolKey + "' is in the structure closure but cannot be reached" + + " from the start pool.", + "unreachable-pool:" + poolKey); + continue; + } + for (PoolEntryReference entry : poolEntries.getOrDefault(poolKey, List.of())) { + if (!entry.empty() && entryIsViableForAnyTheme(entry) + && !reachableEntries.contains(entry.id())) { + addDiagnostic(StructureGraphDiagnostic.Code.UNREACHABLE_PIECE, + "Jigsaw pool '" + poolKey + "' pieces[" + entry.index() + "] references piece '" + + entry.pieceKey() + "', but no reachable connector can attach it.", + "unreachable-piece:" + entry.id()); + } + } + } + } + + private String themeLabel(String theme) { + return theme == null || theme.isEmpty() ? "" : theme; + } + + private List findCompatibleConnectors(OrientedConnector source, + String candidatePieceKey) { + List compatible = new ArrayList<>(); + IrisJigsawPiece candidatePiece = graph.pieces().get(candidatePieceKey); + if (candidatePiece == null) { + return compatible; + } + for (ConnectorReference candidate : pieceConnectors.getOrDefault(candidatePieceKey, List.of())) { + for (int rotation : rotationsFor(candidatePiece)) { + if (connectorsCompatible(source, candidate, rotation)) { + compatible.add(new ConnectorMatch(candidate, rotation)); + } + } + } + compatible.sort((first, second) -> Integer.compare( + second.connector().connector().getSelectionPriority(), + first.connector().connector().getSelectionPriority())); + return compatible; + } + + private boolean connectorsCompatible(OrientedConnector source, ConnectorReference candidate, + int candidateRotation) { + if (!source.connector().canSource() || !candidate.canTarget()) { + return false; + } + IrisJigsawConnector sourceConnector = source.definition(); + IrisJigsawConnector candidateConnector = candidate.connector(); + if (!Objects.equals(sourceConnector.getChannel(), candidateConnector.getChannel())) { + return false; + } + if (!Objects.equals(sourceConnector.getTargetName(), candidateConnector.getName())) { + return false; + } + + IrisDirection sourceDirection = rotateDirection( + sourceConnector.getDirection(), source.rotation()); + IrisDirection candidateDirection = rotateDirection( + candidateConnector.getDirection(), candidateRotation); + if (candidateDirection != sourceDirection.reverse()) { + return false; + } + if (sourceConnector.getJoint() != JigsawJoint.ALIGNED) { + return true; + } + IrisDirection sourceTop = rotateDirection(sourceConnector.getTop(), source.rotation()); + IrisDirection candidateTop = rotateDirection(candidateConnector.getTop(), candidateRotation); + return candidateTop == sourceTop; + } + + private List candidatePools(String firstPool, boolean includeFirst) { + List result = new ArrayList<>(); + IrisJigsawPool pool = graph.pools().get(firstPool); + if (pool == null) { + return result; + } + for (String candidate : JigsawPoolSelection.candidatePoolKeys(firstPool, pool, includeFirst)) { + if (graph.pools().containsKey(candidate)) { + result.add(candidate); + } + } + return result; + } + + private void validateRuleFeasibility() { + for (String theme : reachabilityThemes()) { + int minimumTotal = 0; + Set reachable = reachablePiecesByTheme.getOrDefault(theme, Set.of()); + for (Map.Entry entry : graph.pieces().entrySet()) { + String pieceKey = entry.getKey(); + IrisJigsawPiece piece = entry.getValue(); + if (!pieceEnabled(pieceKey) || !piece.supportsTheme(theme)) { + continue; + } + IrisJigsawPieceRules rules = piece.resolvedRules(); + if (rules.getMinimumPlacements() <= 0) { + continue; + } + minimumTotal = Math.addExact(minimumTotal, rules.getMinimumPlacements()); + if (!reachable.contains(pieceKey)) { + addDiagnostic(StructureGraphDiagnostic.Code.UNSATISFIABLE_PIECE_RULE, + "Jigsaw piece '" + pieceKey + "' requires at least " + + rules.getMinimumPlacements() + " placement(s) for theme '" + + themeLabel(theme) + "' but is not reachable for that theme.", + "piece:minimum:unreachable:" + pieceKey + ":" + theme); + } + if (rules.getMinimumDepth() > Math.max(1, structure.getMaxDepth())) { + addDiagnostic(StructureGraphDiagnostic.Code.UNSATISFIABLE_PIECE_RULE, + "Jigsaw piece '" + pieceKey + "' requires placement for theme '" + + themeLabel(theme) + "' but its minimumDepth " + + rules.getMinimumDepth() + " exceeds the structure depth boundary.", + "piece:minimum:depth:" + pieceKey + ":" + theme); + } + } + if (minimumTotal > ASSEMBLY_PIECE_CAP) { + addDiagnostic(StructureGraphDiagnostic.Code.UNSATISFIABLE_PIECE_RULE, + "Structure '" + structureKey() + "' requires " + minimumTotal + + " minimum piece placements for theme '" + themeLabel(theme) + + "', exceeding the " + ASSEMBLY_PIECE_CAP + "-piece safety cap.", + "structure:minimum-total:" + theme); + } + } + } + + private void validateRequiredCaps() { + for (Map.Entry> active : activeConnectors.entrySet()) { + ReachableConnectorKey connectorKey = active.getKey(); + List orientations = active.getValue(); + OrientedConnector representative = orientations.getFirst(); + String poolKey = normalize(representative.definition().getPool()); + IrisJigsawPool pool = graph.pools().get(poolKey); + if (pool == null || !pool.requiresFallback(structure.isRequireCaps())) { + continue; + } + String fallbackKey = JigsawPoolSelection.directFallbackKey(pool); + if (fallbackKey.isEmpty() || !graph.pools().containsKey(fallbackKey)) { + addDiagnostic(StructureGraphDiagnostic.Code.MISSING_REQUIRED_FALLBACK, + "Reachable connector '" + representative.connector().id() + + "' requires a direct fallback from pool '" + poolKey + "'.", + "connector:required-fallback:" + connectorKey); + continue; + } + int candidateDepth = Math.min( + connectorKey.depth() + 1, + Math.max(1, structure.getMaxDepth())); + boolean compatibleTerminal = false; + for (PoolEntryReference entry : poolEntries.getOrDefault(fallbackKey, List.of())) { + if (entry.empty() || !entryIsViable(entry, connectorKey.theme(), candidateDepth)) { + continue; + } + IrisJigsawPiece piece = graph.pieces().get(entry.pieceKey()); + if (piece == null || !piece.resolvedRules().isTerminal()) { + continue; + } + for (OrientedConnector source : orientations) { + if (!findCompatibleConnectors(source, entry.pieceKey()).isEmpty()) { + compatibleTerminal = true; + break; + } + } + if (compatibleTerminal) { + break; + } + } + if (!compatibleTerminal) { + addDiagnostic(StructureGraphDiagnostic.Code.NO_REQUIRED_TERMINAL, + "Direct fallback pool '" + fallbackKey + "' has no compatible positive-chance" + + " terminal piece for connector '" + representative.connector().id() + + "' and theme '" + themeLabel(connectorKey.theme()) + "'.", + "connector:required-terminal:" + connectorKey); + } + } + } + + private List sampleAssemblies(StructureGraphCompilation compilation) { + if (compilation.hasErrors()) { + return List.of(); + } + List samples = new ArrayList<>(ASSEMBLY_SEEDS.size()); + Map pieceKeys = new IdentityHashMap<>(); + for (Map.Entry entry : compilation.getGraph().getPieces().entrySet()) { + pieceKeys.put(entry.getValue(), entry.getKey()); + } + for (long seed : ASSEMBLY_SEEDS) { + try { + StructureAssembler assembler = StructureAssembler.forCompilation( + compilation, new IrisPosition(0, 64, 0)); + StructureAssemblyResult result = assembler.assemble(new RNG(seed)); + List pieces = new ArrayList<>(result.pieces().size()); + for (PlacedStructurePiece placed : result.pieces()) { + String pieceKey = pieceKeys.get(placed.getPiece()); + pieces.add(pieceKey == null ? "" : pieceKey); + } + samples.add(new StructureGraphAssemblySample( + seed, + new StructureGraphAssemblySample.Outcome( + pieces, + result.status(), + result.detail()))); + } catch (RuntimeException exception) { + addDiagnostic(StructureGraphDiagnostic.Code.ASSEMBLY_RUNTIME_FAILURE, + "Runtime assembly sampling failed at seed " + seed + ": " + + exception.getClass().getSimpleName() + ": " + + failureMessage(exception) + ".", + "assembly:runtime:" + seed); + } + } + return samples; + } + + private String failureMessage(RuntimeException exception) { + return exception.getMessage() == null || exception.getMessage().isBlank() + ? "no failure detail" : exception.getMessage(); + } + + private void reportPieceCapSamples(List samples) { + List cappedSeeds = new ArrayList<>(); + for (StructureGraphAssemblySample sample : samples) { + if (sample.outcome().pieceCapReached()) { + cappedSeeds.add(Long.toString(sample.seed())); + } + } + if (!cappedSeeds.isEmpty()) { + addDiagnostic(StructureGraphDiagnostic.Code.ASSEMBLY_PIECE_CAP_REACHED, + "Deterministic structural assembly reached the " + ASSEMBLY_PIECE_CAP + + "-piece safety cap for seeds " + String.join(", ", cappedSeeds) + ".", + "assembly:piece-cap"); + } + } + + private CompiledStructureGraph buildGraph() { + return graph.build(); + } + + private List diagnostics() { + return diagnostics; + } + + private void addDiagnostic(StructureGraphDiagnostic.Code code, String message, String deduplicationKey) { + if (diagnosticKeys.add(code.name() + ":" + deduplicationKey)) { + diagnostics.add(new StructureGraphDiagnostic(code, message)); + } + } + + private String structureKey() { + String key = structure.getLoadKey(); + return key == null || key.isBlank() ? "" : key; + } + } + + private static String normalize(String value) { + return value == null ? "" : value.trim(); + } + + private static int[] rotationsFor(IrisJigsawPiece piece) { + return piece.isRotatable() ? Y_ROTATIONS : NO_ROTATION; + } + + private static IrisDirection rotateDirection(IrisDirection direction, int degrees) { + if (direction.isVertical()) { + return direction; + } + int turns = Math.floorMod(degrees, 360) / 90; + IrisDirection rotated = direction; + for (int turn = 0; turn < turns; turn++) { + rotated = switch (rotated) { + case NORTH_NEGATIVE_Z -> IrisDirection.WEST_NEGATIVE_X; + case WEST_NEGATIVE_X -> IrisDirection.SOUTH_POSITIVE_Z; + case SOUTH_POSITIVE_Z -> IrisDirection.EAST_POSITIVE_X; + case EAST_POSITIVE_X -> IrisDirection.NORTH_NEGATIVE_Z; + case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> rotated; + }; + } + return rotated; + } + + private record PoolReference(String message, StructureGraphDiagnostic.Code code) { + } + + private record PoolEntryReference( + String poolKey, + int index, + String pieceKey, + int weight, + double chance, + boolean empty + ) { + private String id() { + return poolKey + "#" + index; + } + } + + private record ConnectorReference(String pieceKey, int index, IrisJigsawConnector connector, + boolean validPosition, boolean validDirection, boolean validTop, + boolean validJoint, boolean validNames, boolean validMetadata, + boolean validPlanarContract) { + private String id() { + return pieceKey + "#" + index; + } + + private boolean canSource() { + return canTarget() && !normalize(connector.getPool()).isEmpty(); + } + + private boolean canTarget() { + return connector != null && validPosition && validDirection && validTop && validJoint && validNames + && validMetadata && validPlanarContract; + } + } + + private record PieceReachState( + String pieceKey, + int skippedConnectorIndex, + int rotation, + int depth, + String theme + ) { + } + + private record ReachableConnectorKey(String theme, String connectorId, int depth) { + } + + private record OrientedConnector(ConnectorReference connector, int rotation) { + private IrisJigsawConnector definition() { + return connector.connector(); + } + } + + private record ConnectorMatch(ConnectorReference connector, int rotation) { + } + +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphDiagnostic.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphDiagnostic.java new file mode 100644 index 000000000..e6fc5a636 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphDiagnostic.java @@ -0,0 +1,67 @@ +package art.arcane.iris.engine.framework.structure; + +import java.util.Objects; + +public record StructureGraphDiagnostic(Code code, String message) { + public StructureGraphDiagnostic { + Objects.requireNonNull(code); + Objects.requireNonNull(message); + } + + public Severity severity() { + return code.severity(); + } + + public enum Severity { + ERROR, + WARNING + } + + public enum Code { + MISSING_START_POOL(Severity.ERROR), + EMPTY_START_POOL(Severity.ERROR), + MISSING_POOL(Severity.ERROR), + INVALID_MAX_DEPTH(Severity.ERROR), + INVALID_MAX_SIZE(Severity.ERROR), + INVALID_POOL_ENTRY(Severity.ERROR), + INVALID_WEIGHT(Severity.ERROR), + INVALID_CHANCE(Severity.ERROR), + INVALID_THEME(Severity.ERROR), + INVALID_PIECE_RULE(Severity.ERROR), + UNSATISFIABLE_PIECE_RULE(Severity.ERROR), + MISSING_REQUIRED_FALLBACK(Severity.ERROR), + NO_REQUIRED_TERMINAL(Severity.ERROR), + NON_PORTABLE_METADATA(Severity.ERROR), + MISSING_PIECE(Severity.ERROR), + MISSING_OBJECT(Severity.ERROR), + INVALID_OBJECT_BOUNDS(Severity.ERROR), + INVALID_CONNECTOR(Severity.ERROR), + INVALID_CONNECTOR_METADATA(Severity.ERROR), + INVALID_CONNECTOR_POSITION(Severity.ERROR), + INVALID_CONNECTOR_DIRECTION(Severity.ERROR), + INVALID_PLANAR_CELL_SIZE(Severity.ERROR), + INVALID_PLANAR_WORKCELL(Severity.ERROR), + INVALID_PLANAR_OBJECT_SIZE(Severity.ERROR), + INVALID_PLANAR_CONNECTOR(Severity.ERROR), + NON_PORTABLE_CONNECTOR(Severity.ERROR), + MISSING_CONNECTOR_POOL(Severity.ERROR), + FALLBACK_CYCLE(Severity.ERROR), + NO_ENABLED_START_PIECE(Severity.ERROR), + NO_VIABLE_START_PIECE(Severity.WARNING), + NO_COMPATIBLE_CONNECTOR(Severity.WARNING), + UNREACHABLE_POOL(Severity.WARNING), + UNREACHABLE_PIECE(Severity.WARNING), + ASSEMBLY_RUNTIME_FAILURE(Severity.ERROR), + ASSEMBLY_PIECE_CAP_REACHED(Severity.WARNING); + + private final Severity severity; + + Code(Severity severity) { + this.severity = severity; + } + + public Severity severity() { + return severity; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphResolver.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphResolver.java new file mode 100644 index 000000000..d252b0a5a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphResolver.java @@ -0,0 +1,74 @@ +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisObject; + +import java.util.Objects; + +public interface StructureGraphResolver { + IrisJigsawPool loadPool(String key); + + IrisJigsawPiece loadPiece(String key); + + IrisObject loadObject(String key); + + static StructureGraphResolver forData(IrisData data) { + return new IrisDataStructureGraphResolver(Objects.requireNonNull(data)); + } + + static StructureGraphResolver forCompiledGraph(CompiledStructureGraph graph) { + return new CompiledStructureGraphResolver(Objects.requireNonNull(graph)); + } +} + +final class IrisDataStructureGraphResolver implements StructureGraphResolver { + private final IrisData data; + + IrisDataStructureGraphResolver(IrisData data) { + this.data = data; + } + + @Override + public IrisJigsawPool loadPool(String key) { + return data.load(IrisJigsawPool.class, key, false); + } + + @Override + public IrisJigsawPiece loadPiece(String key) { + return data.load(IrisJigsawPiece.class, key, false); + } + + @Override + public IrisObject loadObject(String key) { + return data.load(IrisObject.class, key, false); + } +} + +final class CompiledStructureGraphResolver implements StructureGraphResolver { + private final CompiledStructureGraph graph; + + CompiledStructureGraphResolver(CompiledStructureGraph graph) { + this.graph = graph; + } + + @Override + public IrisJigsawPool loadPool(String key) { + return graph.getPools().get(normalize(key)); + } + + @Override + public IrisJigsawPiece loadPiece(String key) { + return graph.getPieces().get(normalize(key)); + } + + @Override + public IrisObject loadObject(String key) { + return graph.getObjects().get(normalize(key)); + } + + private static String normalize(String key) { + return key == null ? "" : key.trim(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphValidationException.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphValidationException.java new file mode 100644 index 000000000..fc2b25643 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureGraphValidationException.java @@ -0,0 +1,11 @@ +package art.arcane.iris.engine.framework.structure; + +public final class StructureGraphValidationException extends IllegalArgumentException { + public StructureGraphValidationException(String message) { + super(message); + } + + public StructureGraphValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureResourceBundleGraphCompiler.java b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureResourceBundleGraphCompiler.java new file mode 100644 index 000000000..6365de942 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/structure/StructureResourceBundleGraphCompiler.java @@ -0,0 +1,201 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework.structure; + +import art.arcane.iris.core.structure.authoring.StructureResourceBundle; +import art.arcane.iris.engine.framework.StructureAssembler; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisJigsawPool; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.volmlib.util.math.RNG; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class StructureResourceBundleGraphCompiler { + private static final Gson GSON = new GsonBuilder().create(); + private static final List ASSEMBLY_SEEDS = List.of( + 0L, 1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L, 55L, 89L, 144L, 233L, 377L, 610L, 987L); + + private StructureResourceBundleGraphCompiler() { + } + + public static List compile(StructureResourceBundle bundle) { + BundleGraph graph = parse(Objects.requireNonNull(bundle)); + List compilations = new ArrayList<>(graph.structures().size()); + for (IrisStructure structure : graph.structures().values()) { + compilations.add(StructureGraphCompiler.compile(structure, graph.resolver())); + } + return List.copyOf(compilations); + } + + public static void requireViable(StructureResourceBundle bundle) { + BundleGraph graph = parse(Objects.requireNonNull(bundle)); + if (graph.structures().isEmpty()) { + throw new StructureGraphValidationException("Structure bundle does not contain a root structure resource"); + } + for (IrisStructure structure : graph.structures().values()) { + StructureGraphCompilation compilation = StructureGraphCompiler.compile(structure, graph.resolver()); + if (!compilation.isAssemblyViable()) { + throw new StructureGraphValidationException( + "Structure bundle graph is not viable: " + viabilityFailure(compilation)); + } + requireGeometryViable(compilation); + } + } + + private static String viabilityFailure(StructureGraphCompilation compilation) { + for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) { + if (diagnostic.severity() == StructureGraphDiagnostic.Severity.ERROR) { + return diagnostic.message(); + } + } + for (StructureGraphAssemblySample sample : compilation.getAssemblySamples()) { + StructureGraphAssemblySample.Outcome outcome = sample.outcome(); + if (!outcome.isComplete()) { + return "sampled assembly at seed " + sample.seed() + " returned " + + outcome.status() + ": " + outcome.detail(); + } + } + return "deterministic assembly did not complete"; + } + + private static BundleGraph parse(StructureResourceBundle activeBundle) { + Map structures = new LinkedHashMap<>(); + Map pools = new LinkedHashMap<>(); + Map pieces = new LinkedHashMap<>(); + Map objects = new LinkedHashMap<>(); + for (StructureResourceBundle.Resource resource : activeBundle.resources().values()) { + String path = resource.relativePath(); + if (matches(path, "structures/", ".json")) { + IrisStructure structure = readJson(resource, IrisStructure.class); + String key = key(path, "structures/", ".json"); + structure.setLoadKey(key); + structures.put(key, structure); + } else if (matches(path, "jigsaw-pools/", ".json")) { + pools.put(key(path, "jigsaw-pools/", ".json"), readJson(resource, IrisJigsawPool.class)); + } else if (matches(path, "jigsaw-pieces/", ".json")) { + pieces.put(key(path, "jigsaw-pieces/", ".json"), readJson(resource, IrisJigsawPiece.class)); + } else if (matches(path, "objects/", ".iob")) { + objects.put(key(path, "objects/", ".iob"), readObjectBounds(resource)); + } + } + return new BundleGraph(structures, new BundleResolver(pools, pieces, objects)); + } + + private static void requireGeometryViable(StructureGraphCompilation compilation) { + String structureKey = compilation.getGraph().getStructureKey(); + for (long seed : ASSEMBLY_SEEDS) { + try { + StructureAssembler assembler = StructureAssembler.forCompilation( + compilation, new IrisPosition(0, 64, 0)); + StructureAssemblyResult result = assembler.assemble(new RNG(seed)); + if (result.status().isFailure()) { + throw new StructureGraphValidationException("Structure bundle graph '" + structureKey + + "' fails sampled runtime geometry assembly at seed " + seed + ": " + + result.status() + ": " + result.detail()); + } + } catch (StructureGraphValidationException e) { + throw e; + } catch (RuntimeException e) { + throw new IllegalStateException("Structure bundle graph '" + structureKey + + "' encountered an unexpected sampled runtime geometry failure at seed " + seed + ": " + + e.getClass().getSimpleName() + ": " + failureMessage(e), e); + } + } + } + + private static String failureMessage(RuntimeException exception) { + return exception.getMessage() == null || exception.getMessage().isBlank() + ? "no failure detail" : exception.getMessage(); + } + + private static boolean matches(String path, String prefix, String suffix) { + return path.startsWith(prefix) && path.endsWith(suffix) && path.length() > prefix.length() + suffix.length(); + } + + private static String key(String path, String prefix, String suffix) { + return path.substring(prefix.length(), path.length() - suffix.length()); + } + + private static T readJson(StructureResourceBundle.Resource resource, Class type) { + T value; + try { + value = GSON.fromJson(new String(resource.content(), StandardCharsets.UTF_8), type); + } catch (RuntimeException e) { + throw new StructureGraphValidationException("Malformed structure resource " + resource.relativePath(), e); + } + if (value == null) { + throw new StructureGraphValidationException("Empty structure resource " + resource.relativePath()); + } + return value; + } + + private static IrisObject readObjectBounds(StructureResourceBundle.Resource resource) { + try (ByteArrayInputStream input = new ByteArrayInputStream(resource.content())) { + return IrisObjectFrameReader.readBounds(input, resource.relativePath()); + } catch (IOException e) { + throw new StructureGraphValidationException(e.getMessage(), e); + } + } + + private record BundleResolver( + Map pools, + Map pieces, + Map objects + ) implements StructureGraphResolver { + private BundleResolver { + pools = Map.copyOf(pools); + pieces = Map.copyOf(pieces); + objects = Map.copyOf(objects); + } + + @Override + public IrisJigsawPool loadPool(String key) { + return pools.get(key); + } + + @Override + public IrisJigsawPiece loadPiece(String key) { + return pieces.get(key); + } + + @Override + public IrisObject loadObject(String key) { + return objects.get(key); + } + } + + private record BundleGraph(Map structures, BundleResolver resolver) { + private BundleGraph { + structures = Map.copyOf(structures); + Objects.requireNonNull(resolver); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/ComponentFlag.java b/core/src/main/java/art/arcane/iris/engine/mantle/ComponentFlag.java similarity index 75% rename from core/src/main/java/com/volmit/iris/engine/mantle/ComponentFlag.java rename to core/src/main/java/art/arcane/iris/engine/mantle/ComponentFlag.java index 0ed617a7d..1d29dfd3e 100644 --- a/core/src/main/java/com/volmit/iris/engine/mantle/ComponentFlag.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/ComponentFlag.java @@ -1,6 +1,6 @@ -package com.volmit.iris.engine.mantle; +package art.arcane.iris.engine.mantle; -import com.volmit.iris.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java b/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java new file mode 100644 index 000000000..348ef617e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java @@ -0,0 +1,297 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.UpperDimensionContext; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.engine.mantle.components.MantleObjectComponent; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterFluidBody; +import art.arcane.volmlib.util.matter.MatterMarker; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.slices.UpdateMatter; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.spi.PlatformBlockState; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public interface EngineMantle extends MatterGenerator { + PlatformBlockState AIR = B.getState("AIR"); + + Mantle getMantle(); + + Engine getEngine(); + + int getRadius(); + + int getRealRadius(); + + @UnmodifiableView + List getComponents(); + + @UnmodifiableView + Map getRegisteredComponents(); + + boolean registerComponent(MantleComponent c); + + @UnmodifiableView + KList getComponentFlags(); + + void hotload(); + + default int getHighest(int x, int z) { + return getHighest(x, z, getData()); + } + + @ChunkCoordinates + default KList findMarkers(int x, int z, MatterMarker marker) { + KList p = new KList<>(); + if (getEngine().getPlatformHooks().shouldSkipMantleMarkerRead(getEngine(), x, z)) { + return p; + } + + getMantle().iterateChunk(x, z, MatterMarker.class, (xx, yy, zz, mm) -> { + if (marker.equals(mm)) { + p.add(new IrisPosition(xx + (x << 4), yy + getEngine().getMinHeight(), zz + (z << 4))); + } + }); + + return p; + } + + default int getHighest(int x, int z, boolean ignoreFluid) { + return getHighest(x, z, getData(), ignoreFluid); + } + + default int getHighest(int x, int z, IrisData data) { + return getHighest(x, z, data, false); + } + + default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getEngine().getDimension().getFluidHeight()); + } + + default int trueHeight(int x, int z) { + return getComplex().getRoundedHeighteightStream().get(x, z); + } + + default boolean isCarved(int x, int h, int z) { + return getMantle().get(x, h, z, MatterCavern.class) != null; + } + + default PlatformBlockState get(int x, int y, int z) { + PlatformBlockState block = getMantle().get(x, y, z, PlatformBlockState.class); + if (block == null) + return AIR; + return block; + } + + default boolean isPreventingDecay() { + return getEngine().getDimension().isPreventLeafDecay(); + } + + default boolean isUnderwater(int x, int z) { + return getHighest(x, z, true) <= getFluidHeight(); + } + + default int getFluidHeight() { + return getEngine().getDimension().getFluidHeight(); + } + + default boolean isDebugSmartBore() { + return getEngine().getDimension().isDebugSmartBore(); + } + + default void trim(long duration) { + getMantle().trim(duration); + } + + default void trim(long dur, int limit) { + getMantle().trim(dur, limit); + } + + default IrisData getData() { + return getEngine().getData(); + } + + default EngineTarget getTarget() { + return getEngine().getTarget(); + } + + default IrisDimension getDimension() { + return getEngine().getDimension(); + } + + default IrisComplex getComplex() { + return getEngine().getComplex(); + } + + default void close() { + getMantle().close(); + } + + default void saveAllNow() { + getMantle().saveAll(); + } + + default void save() { + + } + + default void trim(int limit) { + getMantle().trim(TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()), limit); + } + default int unloadTectonicPlate(int tectonicLimit){ + return getMantle().unloadTectonicPlate(tectonicLimit); + } + + default MultiBurst burst() { + return getEngine().burst(); + } + + @ChunkCoordinates + default void insertMatter(int x, int z, Hunk blocks, boolean multicore) { + if (!getEngine().getDimension().isUseMantle()) { + return; + } + + UpperDimensionContext upperCtx = getEngine().getUpperContext(); + boolean protectUpper = upperCtx != null; + + MantleChunk chunk = getMantle().getChunk(x, z).use(); + try { + if (protectUpper) { + int chunkBlockX = x << 4; + int chunkBlockZ = z << 4; + int gap = getEngine().getDimension().getUpperDimensionGap(); + int[] upperYs = new int[256]; + for (int i = 0; i < 256; i++) { + int lx = i >> 4; + int lz = i & 15; + int worldX = chunkBlockX + lx; + int worldZ = chunkBlockZ + lz; + int he = (int) Math.round(getEngine().getComplex().getHeightStream().get((double) worldX, (double) worldZ)); + int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ); + upperYs[i] = Math.max(rawUpper, he + gap); + } + chunk.iterate(PlatformBlockState.class, (lx, y, lz, value) -> { + int colIdx = (lx << 4) | (lz & 15); + if (y < upperYs[colIdx]) { + blocks.set(lx, y, lz, value); + } + }); + } else { + chunk.iterate(PlatformBlockState.class, (lx, y, lz, value) -> blocks.set(lx, y, lz, value)); + } + } finally { + chunk.release(); + } + } + + @BlockCoordinates + default void updateBlock(int x, int y, int z) { + getMantle().set(x, y, z, UpdateMatter.ON); + } + + default int getLoadedRegionCount() { + return getMantle().getLoadedRegionCount(); + } + + MantleObjectComponent getObjectComponent(); + + default boolean isCovered(int x, int z) { + int s = getRealRadius(); + + for (int i = -s; i <= s; i++) { + for (int j = -s; j <= s; j++) { + int xx = i + x; + int zz = j + z; + if (!getMantle().hasFlag(xx, zz, MantleFlag.REAL)) { + return false; + } + } + } + + return true; + } + + default void cleanupChunk(int x, int z) { + if (!isCovered(x, z)) return; + doCleanupChunk(x, z); + } + + default void forceCleanupChunk(int x, int z) { + MantleChunk chunk = getMantle().getChunk(x, z).use(); + try { + chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> { + MantleSliceRetention.deleteUnlessRetained(chunk, PlatformBlockState.class); + MantleSliceRetention.deleteUnlessRetained(chunk, String.class); + MantleSliceRetention.deleteUnlessRetained(chunk, UpdateMatter.class); + MantleSliceRetention.deleteUnlessRetained(chunk, MatterCavern.class); + MantleSliceRetention.deleteUnlessRetained(chunk, MatterFluidBody.class); + MantleSliceRetention.deleteUnlessRetained(chunk, MatterMarker.class); + MantleSliceRetention.deleteUnlessRetained(chunk, TreeBlockMaterial.class); + chunk.trimSlices(); + }); + } finally { + chunk.release(); + } + } + + private void doCleanupChunk(int x, int z) { + MantleChunk chunk = getMantle().getChunk(x, z).use(); + try { + chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> { + MantleSliceRetention.deleteUnlessRetained(chunk, PlatformBlockState.class); + MantleSliceRetention.deleteUnlessRetained(chunk, UpdateMatter.class); + MantleSliceRetention.deleteUnlessRetained(chunk, MatterCavern.class); + MantleSliceRetention.deleteUnlessRetained(chunk, MatterFluidBody.class); + chunk.trimSlices(); + }); + } finally { + chunk.release(); + } + } + + default int getUnloadRegionCount() { + return getMantle().getUnloadRegionCount(); + } + + default double getAdjustedIdleDuration() { + return getMantle().getAdjustedIdleDuration(); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/IrisMantleComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/IrisMantleComponent.java similarity index 94% rename from core/src/main/java/com/volmit/iris/engine/mantle/IrisMantleComponent.java rename to core/src/main/java/art/arcane/iris/engine/mantle/IrisMantleComponent.java index c123183d5..67af4d505 100644 --- a/core/src/main/java/com/volmit/iris/engine/mantle/IrisMantleComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/IrisMantleComponent.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.mantle; +package art.arcane.iris.engine.mantle; -import com.volmit.iris.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java new file mode 100644 index 000000000..cf60c9fba --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java @@ -0,0 +1,84 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import org.jetbrains.annotations.NotNull; + +public interface MantleComponent extends Comparable { + int getPriority(); + + int getRadius(); + + default IrisData getData() { + return getEngineMantle().getData(); + } + + default IrisDimension getDimension() { + return getEngineMantle().getEngine().getDimension(); + } + + default IrisComplex getComplex() { + return getEngineMantle().getComplex(); + } + + default long seed() { + return getEngineMantle().getEngine().getSeedManager().getMantle(); + } + + default BurstExecutor burst() { + return getEngineMantle().getEngine().burst().burst(); + } + + EngineMantle getEngineMantle(); + + default Mantle getMantle() { + return getEngineMantle().getMantle(); + } + + MantleFlag getFlag(); + + default MantleFlag[] getPrerequisiteFlags() { + return EMPTY_PREREQUISITES; + } + + MantleFlag[] EMPTY_PREREQUISITES = new MantleFlag[0]; + + boolean isEnabled(); + + void setEnabled(boolean b); + + void hotload(); + + @ChunkCoordinates + void generateLayer(MantleWriter writer, int x, int z, ChunkContext context); + + @Override + default int compareTo(@NotNull MantleComponent o) { + return Integer.compare(getPriority(), o.getPriority()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantlePass.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantlePass.java new file mode 100644 index 000000000..ea338bfc9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantlePass.java @@ -0,0 +1,34 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle; + +import java.util.List; + +/** + * One priority level of mantle components. + * + * @param components every component registered at this priority, enabled or not + * @param passChunkRadius chunk ring this pass must complete: ceilDiv of this pass' widest + * component reach plus every later pass' reach + * @param downstreamBlockRadius block reach summed over all LATER passes only. A component must be + * generated across its own reach plus this, or later passes read + * half-written data from it + */ +public record MantlePass(List components, int passChunkRadius, int downstreamBlockRadius) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleSliceRetention.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleSliceRetention.java new file mode 100644 index 000000000..eec7c30dc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleSliceRetention.java @@ -0,0 +1,71 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Process-wide registry of mantle slice types that survive chunk cleanup. Declared through + * IrisToolbelt/IrisModdedAPI's retainMantleDataForSlice; honored by both EngineMantle cleanup + * paths (normal trim and pregen force-cleanup). Retained slices are not a leak: they persist to + * tectonic plates and unload with the region - the cost is larger region files. + * + *

The block-state slice is deliberately never retainable: it is the largest mantle consumer + * and is fully regenerable, so retaining it would balloon every region file with no consumer. + */ +public final class MantleSliceRetention { + private static final Set retained = ConcurrentHashMap.newKeySet(); + + private MantleSliceRetention() { + } + + public static void retain(String className) { + if (className == null || PlatformBlockState.class.getCanonicalName().equals(className)) { + return; + } + if (retained.add(className)) { + IrisLogging.info("Mantle slice retained across chunk cleanup: " + className); + } + } + + public static boolean isRetained(String className) { + return className != null && retained.contains(className); + } + + public static boolean isRetained(Class sliceType) { + return sliceType != null && retained.contains(sliceType.getCanonicalName()); + } + + static void deleteUnlessRetained(MantleChunk chunk, Class sliceType) { + if (isRetained(sliceType)) { + return; + } + chunk.deleteSlices(sliceType); + } + + static void clearForTesting() { + retained.clear(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java new file mode 100644 index 000000000..a17e4a4a7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -0,0 +1,979 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle; + + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.project.matter.TileWrapper; +import com.google.common.collect.ImmutableList; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.tools.WorldMaintenance; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.IrisGeneratorStyle; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.TileData; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.common.scheduling.J; +import lombok.Data; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import org.bukkit.util.Vector; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReferenceArray; + +import static art.arcane.iris.engine.mantle.EngineMantle.AIR; + +@Data +public class MantleWriter implements IObjectPlacer, AutoCloseable { + private final EngineMantle engineMantle; + private final Mantle mantle; + private final int radius; + private final int x; + private final int z; + private final int windowSide; + private final AtomicReferenceArray> window; + + public MantleWriter(EngineMantle engineMantle, Mantle mantle, int x, int z, int radius, boolean multicore) { + this.engineMantle = engineMantle; + this.mantle = mantle; + this.radius = radius * 2; + this.x = x; + this.z = z; + // Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the + // boxed per-block map lookup on the placement and carve hot paths. + this.windowSide = (this.radius * 2) + 1; + this.window = new AtomicReferenceArray<>(windowSide * windowSide); + + final boolean foliaMaintenance = J.isFolia() + && WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity()); + final int parallelism = foliaMaintenance ? 1 : (multicore ? Runtime.getRuntime().availableProcessors() / 2 : 4); + if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) { + IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + "."); + } + // try-with-resources never calls close() when the initializer throws, so a failed + // prefetch must release the permits already pinned into the window here. + try { + mantle.getChunks( + x - radius, + x + radius, + z - radius, + z + radius, + parallelism, + this::storePrefetchedChunk + ); + } catch (Throwable e) { + close(); + throw e; + } + } + + private static Set getBallooned(Set vset, double radius) { + Set returnset = new HashSet<>(); + int ceilrad = (int) Math.ceil(radius); + double r2 = Math.pow(radius, 2); + + for (IrisPosition v : vset) { + int tipx = v.getX(); + int tipy = v.getY(); + int tipz = v.getZ(); + + for (int loopx = tipx - ceilrad; loopx <= tipx + ceilrad; loopx++) { + for (int loopy = tipy - ceilrad; loopy <= tipy + ceilrad; loopy++) { + for (int loopz = tipz - ceilrad; loopz <= tipz + ceilrad; loopz++) { + if (hypot(loopx - tipx, loopy - tipy, loopz - tipz) <= r2) { + returnset.add(new IrisPosition(loopx, loopy, loopz)); + } + } + } + } + } + return returnset; + } + + private static Set getHollowed(Set vset) { + Set returnset = new KSet<>(); + for (IrisPosition v : vset) { + double x = v.getX(); + double y = v.getY(); + double z = v.getZ(); + if (!(vset.contains(new IrisPosition(x + 1, y, z)) + && vset.contains(new IrisPosition(x - 1, y, z)) + && vset.contains(new IrisPosition(x, y + 1, z)) + && vset.contains(new IrisPosition(x, y - 1, z)) + && vset.contains(new IrisPosition(x, y, z + 1)) + && vset.contains(new IrisPosition(x, y, z - 1)))) { + returnset.add(v); + } + } + return returnset; + } + + private static double hypot(double... pars) { + double sum = 0; + for (double d : pars) { + sum += Math.pow(d, 2); + } + return sum; + } + + private static double lengthSq(double x, double y, double z) { + return (x * x) + (y * y) + (z * z); + } + + private static double lengthSq(double x, double z) { + return (x * x) + (z * z); + } + + public void setDataWarped(int x, int y, int z, T t, RNG rng, IrisData data, IrisGeneratorStyle style) { + setData((int) Math.round(style.warp(rng, data, x, x, y, -z)), + (int) Math.round(style.warp(rng, data, y, z, -x, y)), + (int) Math.round(style.warp(rng, data, z, -y, z, x)), t); + } + + public void setData(int x, int y, int z, T t) { + if (t == null) { + return; + } + + int cx = x >> 4; + int cz = z >> 4; + + if (y < 0 || y >= mantle.getWorldHeight()) { + return; + } + + if (y == 0 && t instanceof PlatformBlockState && engineMantle.getEngine().getDimension().isBedrock()) { + return; + } + + MantleChunk chunk = acquireChunk(cx, cz); + if (chunk == null) return; + + Matter matter = chunk.getOrCreate(y >> 4); + if (t instanceof PlatformBlockState) { + clearDeferredPlacement(matter, x, y, z); + } + Class sliceType = t instanceof PlatformBlockState ? PlatformBlockState.class : matter.getClass(t); + matter.slice(sliceType).set(x & 15, y & 15, z & 15, t); + } + + public boolean setDataIfAbsent(int x, int y, int z, MatterCavern value) { + if (value == null) { + return false; + } + + int cx = x >> 4; + int cz = z >> 4; + + if (y < 0 || y >= mantle.getWorldHeight()) { + return false; + } + + MantleChunk chunk = acquireChunk(cx, cz); + if (chunk == null) { + return false; + } + + Matter matter = chunk.getOrCreate(y >> 4); + MatterCavern existing = matter.slice(MatterCavern.class).get(x & 15, y & 15, z & 15); + if (existing != null) { + return false; + } + + matter.slice(MatterCavern.class).set(x & 15, y & 15, z & 15, value); + return true; + } + + public boolean carveDataIfAbsent(int x, int y, int z, MatterCavern value) { + if (value == null || y < 0 || y >= mantle.getWorldHeight()) { + return false; + } + + MantleChunk chunk = acquireChunk(x >> 4, z >> 4); + if (chunk == null) { + return false; + } + + Matter matter = chunk.getOrCreate(y >> 4); + if (matter.hasSlice(PlatformBlockState.class)) { + matter.getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null); + } + clearDeferredPlacement(matter, x, y, z); + MatterSlice cavernSlice = matter.slice(MatterCavern.class); + if (cavernSlice.get(x & 15, y & 15, z & 15) != null) { + return false; + } + cavernSlice.set(x & 15, y & 15, z & 15, value); + return true; + } + + public void setForcedCarve(int x, int y, int z, MatterCavern value) { + if (value == null || y < 0 || y >= mantle.getWorldHeight()) { + return; + } + MantleChunk chunk = acquireChunk(x >> 4, z >> 4); + if (chunk == null) { + throw new IllegalStateException("Forced structure carve exceeded its prepared Mantle radius at " + + x + "," + y + "," + z); + } + Matter matter = chunk.getOrCreate(y >> 4); + if (matter.hasSlice(PlatformBlockState.class)) { + matter.getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null); + } + clearDeferredPlacement(matter, x, y, z); + matter.slice(MatterCavern.class).set(x & 15, y & 15, z & 15, value); + } + + public void clearBlock(int x, int y, int z) { + if (y < 0 || y >= mantle.getWorldHeight()) { + return; + } + MantleChunk chunk = acquireChunk(x >> 4, z >> 4); + if (chunk == null) { + return; + } + int section = y >> 4; + if (!chunk.exists(section)) { + return; + } + Matter matter = chunk.get(section); + if (matter == null) { + return; + } + if (matter.hasSlice(PlatformBlockState.class)) { + matter.getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null); + } + clearDeferredPlacement(matter, x, y, z); + } + + public T getData(int x, int y, int z, Class type) { + int cx = x >> 4; + int cz = z >> 4; + + if (y < 0 || y >= mantle.getWorldHeight()) { + return null; + } + + MantleChunk chunk = acquireChunk(cx, cz); + if (chunk == null) { + return null; + } + + return chunk.getOrCreate(y >> 4) + .slice(type) + .get(x & 15, y & 15, z & 15); + } + + public T getDataIfPresent(int x, int y, int z, Class type) { + if (y < 0 || y >= mantle.getWorldHeight()) { + return null; + } + + MantleChunk chunk = acquireChunk(x >> 4, z >> 4); + int section = y >> 4; + if (chunk == null || !chunk.exists(section)) { + return null; + } + + Matter matter = chunk.get(section); + if (matter == null || !matter.hasSlice(type)) { + return null; + } + return matter.getSlice(type).get(x & 15, y & 15, z & 15); + } + + public void clearData(int x, int y, int z, Class type) { + if (y < 0 || y >= mantle.getWorldHeight()) { + return; + } + + MantleChunk chunk = acquireChunk(x >> 4, z >> 4); + int section = y >> 4; + if (chunk == null || !chunk.exists(section)) { + return; + } + + Matter matter = chunk.get(section); + if (matter == null || !matter.hasSlice(type)) { + return; + } + matter.getSlice(type).set(x & 15, y & 15, z & 15, null); + } + + private static void clearDeferredPlacement(Matter matter, int x, int y, int z) { + if (matter.hasSlice(Identifier.class)) { + matter.getSlice(Identifier.class).set(x & 15, y & 15, z & 15, null); + } + } + + @ChunkCoordinates + public MantleChunk acquireChunk(int cx, int cz) { + int index = windowIndex(cx, cz); + if (index < 0) { + IrisLogging.debug("Mantle Writer Accessed chunk out of bounds" + cx + "," + cz); + return null; + } + + while (true) { + MantleChunk chunk = window.get(index); + if (chunk != null) { + return chunk; + } + + // Losing this race must release our own use, never the winner's: the winner is already + // writing through the chunk it published. + MantleChunk acquired = mantle.getChunk(cx, cz).use(); + if (window.compareAndSet(index, null, acquired)) { + return acquired; + } + acquired.release(); + } + } + + @ChunkCoordinates + private int windowIndex(int cx, int cz) { + int localX = cx - x + radius; + int localZ = cz - z + radius; + if (localX < 0 || localX >= windowSide || localZ < 0 || localZ >= windowSide) { + return -1; + } + return (localX * windowSide) + localZ; + } + + @ChunkCoordinates + private void storePrefetchedChunk(int cx, int cz, MantleChunk chunk) { + int index = windowIndex(cx, cz); + if (index < 0) { + return; + } + + if (!window.compareAndSet(index, null, chunk.use())) { + chunk.release(); + } + } + + @Override + public int getHighest(int x, int z, IrisData data) { + return engineMantle.getHighest(x, z, data); + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return engineMantle.getHighest(x, z, data, ignoreFluid); + } + + @Override + public void set(int x, int y, int z, PlatformBlockState s) { + if (s == null) { + return; + } + String placementKey = s.deferredPlacementKey(); + PlatformBlockState baseState = s.placementBaseState(); + if (s.isCustom() && placementKey != null && baseState != null) { + setData(x, y, z, baseState); + setData(x, y, z, Identifier.fromString(placementKey)); + return; + } + setData(x, y, z, s); + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + // Read-only probe: getDataIfPresent returns the identical answer without materializing + // a 16^3 section + slice on a miss the way getData's getOrCreate path does. + PlatformBlockState block = getDataIfPresent(x, y, z, PlatformBlockState.class); + if (block == null) + return AIR; + return block; + } + + @Override + public boolean isPreventingDecay() { + return getEngineMantle().isPreventingDecay(); + } + + @Override + public boolean isCarved(int x, int y, int z) { + return getDataIfPresent(x, y, z, MatterCavern.class) != null; + } + + public byte[] getCarvedColumn(int x, int z, int height) { + int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight()); + byte[] carvedColumn = new byte[cappedHeight]; + if (cappedHeight <= 0) { + return carvedColumn; + } + + MantleChunk chunk = acquireChunk(x >> 4, z >> 4); + if (chunk == null) { + return carvedColumn; + } + + int localX = x & 15; + int localZ = z & 15; + int lastSection = (cappedHeight - 1) >> 4; + for (int section = 0; section <= lastSection; section++) { + if (!chunk.exists(section)) { + continue; + } + + Matter matter = chunk.get(section); + if (matter == null || !matter.hasSlice(MatterCavern.class)) { + continue; + } + + MatterSlice slice = matter.getSlice(MatterCavern.class); + int sectionBaseY = section << 4; + int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16); + for (int y = sectionBaseY; y < sectionMaxY; y++) { + if (slice.get(localX, y & 15, localZ) != null) { + carvedColumn[y] = 1; + } + } + } + + return carvedColumn; + } + + @Override + public boolean isSolid(int x, int y, int z) { + return B.isSolid(get(x, y, z)); + } + + @Override + public boolean isUnderwater(int x, int z) { + return getEngineMantle().isUnderwater(x, z); + } + + @Override + public int getFluidHeight() { + return getEngineMantle().getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return getEngineMantle().isDebugSmartBore(); + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + setData(xx, yy, zz, new TileWrapper(tile)); + } + + @Override + public Engine getEngine() { + return getEngineMantle().getEngine(); + } + + /** + * Set a sphere into the mantle + * + * @param cx the center x + * @param cy the center y + * @param cz the center z + * @param radius the radius of this sphere + * @param fill should it be filled? or just the outer shell? + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setSphere(int cx, int cy, int cz, double radius, boolean fill, T data) { + setElipsoid(cx, cy, cz, radius, radius, radius, fill, data); + } + + public void setElipsoid(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, T data) { + setElipsoidFunction(cx, cy, cz, rx, ry, rz, fill, (a, b, c) -> data); + } + + public void setElipsoidWarped(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, T data, RNG rng, IrisData idata, IrisGeneratorStyle style) { + setElipsoidFunctionWarped(cx, cy, cz, rx, ry, rz, fill, (a, b, c) -> data, rng, idata, style); + } + + /** + * Set an elipsoid into the mantle + * + * @param cx the center x + * @param cy the center y + * @param cz the center z + * @param rx the x radius + * @param ry the y radius + * @param rz the z radius + * @param fill should it be filled or just the outer shell? + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setElipsoidFunction(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, Function3 data) { + rx += 0.5; + ry += 0.5; + rz += 0.5; + final double invRadiusX = 1 / rx; + final double invRadiusY = 1 / ry; + final double invRadiusZ = 1 / rz; + final int ceilRadiusX = (int) Math.ceil(rx); + final int ceilRadiusY = (int) Math.ceil(ry); + final int ceilRadiusZ = (int) Math.ceil(rz); + double nextXn = 0; + + forX: + for (int x = 0; x <= ceilRadiusX; ++x) { + final double xn = nextXn; + nextXn = (x + 1) * invRadiusX; + double nextYn = 0; + forY: + for (int y = 0; y <= ceilRadiusY; ++y) { + final double yn = nextYn; + nextYn = (y + 1) * invRadiusY; + double nextZn = 0; + for (int z = 0; z <= ceilRadiusZ; ++z) { + final double zn = nextZn; + nextZn = (z + 1) * invRadiusZ; + + double distanceSq = lengthSq(xn, yn, zn); + if (distanceSq > 1) { + if (z == 0) { + if (y == 0) { + break forX; + } + break forY; + } + break; + } + + if (!fill) { + if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) { + continue; + } + } + + setData(x + cx, y + cy, z + cz, data.apply(x + cx, y + cy, z + cz)); + setData(-x + cx, y + cy, z + cz, data.apply(-x + cx, y + cy, z + cz)); + setData(x + cx, -y + cy, z + cz, data.apply(x + cx, -y + cy, z + cz)); + setData(x + cx, y + cy, -z + cz, data.apply(x + cx, y + cy, -z + cz)); + setData(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz)); + setData(-x + cx, -y + cy, z + cz, data.apply(-x + cx, -y + cy, z + cz)); + setData(x + cx, -y + cy, -z + cz, data.apply(x + cx, -y + cy, -z + cz)); + setData(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz)); + setData(-x + cx, -y + cy, -z + cz, data.apply(-x + cx, -y + cy, -z + cz)); + } + } + } + } + + public void setElipsoidFunctionWarped(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, Function3 data, RNG rng, IrisData idata, IrisGeneratorStyle style) { + rx += 0.5; + ry += 0.5; + rz += 0.5; + final double invRadiusX = 1 / rx; + final double invRadiusY = 1 / ry; + final double invRadiusZ = 1 / rz; + final int ceilRadiusX = (int) Math.ceil(rx); + final int ceilRadiusY = (int) Math.ceil(ry); + final int ceilRadiusZ = (int) Math.ceil(rz); + double nextXn = 0; + + forX: + for (int x = 0; x <= ceilRadiusX; ++x) { + final double xn = nextXn; + nextXn = (x + 1) * invRadiusX; + double nextYn = 0; + forY: + for (int y = 0; y <= ceilRadiusY; ++y) { + final double yn = nextYn; + nextYn = (y + 1) * invRadiusY; + double nextZn = 0; + for (int z = 0; z <= ceilRadiusZ; ++z) { + final double zn = nextZn; + nextZn = (z + 1) * invRadiusZ; + + double distanceSq = lengthSq(xn, yn, zn); + if (distanceSq > 1) { + if (z == 0) { + if (y == 0) { + break forX; + } + break forY; + } + break; + } + + if (!fill) { + if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) { + continue; + } + } + + setDataWarped(x + cx, y + cy, z + cz, data.apply(x + cx, y + cy, z + cz), rng, idata, style); + setDataWarped(-x + cx, y + cy, z + cz, data.apply(-x + cx, y + cy, z + cz), rng, idata, style); + setDataWarped(x + cx, -y + cy, z + cz, data.apply(x + cx, -y + cy, z + cz), rng, idata, style); + setDataWarped(x + cx, y + cy, -z + cz, data.apply(x + cx, y + cy, -z + cz), rng, idata, style); + setDataWarped(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz), rng, idata, style); + setDataWarped(-x + cx, -y + cy, z + cz, data.apply(-x + cx, -y + cy, z + cz), rng, idata, style); + setDataWarped(x + cx, -y + cy, -z + cz, data.apply(x + cx, -y + cy, -z + cz), rng, idata, style); + setDataWarped(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz), rng, idata, style); + setDataWarped(-x + cx, -y + cy, -z + cz, data.apply(-x + cx, -y + cy, -z + cz), rng, idata, style); + } + } + } + } + + /** + * Set a cuboid of data in the mantle + * + * @param x1 the min x + * @param y1 the min y + * @param z1 the min z + * @param x2 the max x + * @param y2 the max y + * @param z2 the max z + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setCuboid(int x1, int y1, int z1, int x2, int y2, int z2, T data) { + int j, k; + + for (int i = x1; i <= x2; i++) { + for (j = x1; j <= x2; j++) { + for (k = x1; k <= x2; k++) { + setData(i, j, k, data); + } + } + } + } + + /** + * Set a pyramid of data in the mantle + * + * @param cx the center x + * @param cy the base y + * @param cz the center z + * @param data the data to set + * @param size the size of the pyramid (width of base & height) + * @param filled should it be filled or hollow + * @param the type of data to apply to the mantle + */ + @SuppressWarnings("ConstantConditions") + public void setPyramid(int cx, int cy, int cz, T data, int size, boolean filled) { + int height = size; + + for (int y = 0; y <= height; ++y) { + size--; + for (int x = 0; x <= size; ++x) { + for (int z = 0; z <= size; ++z) { + if ((filled && z <= size && x <= size) || z == size || x == size) { + setData(x + cx, y + cy, z + cz, data); + setData(-x + cx, y + cy, z + cz, data); + setData(x + cx, y + cy, -z + cz, data); + setData(-x + cx, y + cy, -z + cz, data); + } + } + } + } + } + + /** + * Set a 3d line + * + * @param a the first point + * @param b the second point + * @param radius the radius + * @param filled hollow or filled? + * @param data the data + * @param the type of data to apply to the mantle + */ + public void setLine(IrisPosition a, IrisPosition b, double radius, boolean filled, T data) { + setLine(ImmutableList.of(a, b), radius, filled, data); + } + + public void setLine(List vectors, double radius, boolean filled, T data) { + setLineConsumer(vectors, radius, filled, (_x, _y, _z) -> data); + } + + /** + * Set lines for points + * + * @param vectors the points + * @param radius the radius + * @param filled hollow or filled? + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setLineConsumer(List vectors, double radius, boolean filled, Function3 data) { + Set vset = cleanup(vectors); + vset = getBallooned(vset, radius); + + if (!filled) { + vset = getHollowed(vset); + } + + setConsumer(vset, data); + } + + /** + * Set lines for points + * + * @param vectors the points + * @param radius the radius + * @param filled hollow or filled? + * @param data the data to set + * @param the type of data to apply to the mantle + */ + public void setNoiseMasked(List vectors, double radius, double threshold, CNG shape, Set masks, boolean filled, Function3 data) { + Set vset = cleanup(vectors); + vset = masks == null ? getBallooned(vset, radius) : getMasked(vset, masks, radius); + vset.removeIf(p -> shape.noise(p.getX(), p.getY(), p.getZ()) < threshold); + + if (!filled) { + vset = getHollowed(vset); + } + + setConsumer(vset, data); + } + + private static Set getMasked(Set vectors, Set masks, double radius) { + Set vset = new KSet<>(); + int ceil = (int) Math.ceil(radius); + double r2 = Math.pow(radius, 2); + + for (IrisPosition v : vectors) { + int tipX = v.getX(); + int tipY = v.getY(); + int tipZ = v.getZ(); + + for (int x = -ceil; x <= ceil; x++) { + for (int y = -ceil; y <= ceil; y++) { + for (int z = -ceil; z <= ceil; z++) { + if (hypot(x, y, z) > r2 || !masks.contains(new IrisPosition(x, y, z))) + continue; + vset.add(new IrisPosition(tipX + x, tipY + y, tipZ + z)); + } + } + } + } + + return vset; + } + + private static Set cleanup(List vectors) { + Set vset = new KSet<>(); + + for (int i = 0; vectors.size() != 0 && i < vectors.size() - 1; i++) { + IrisPosition pos1 = vectors.get(i); + IrisPosition pos2 = vectors.get(i + 1); + int x1 = pos1.getX(); + int y1 = pos1.getY(); + int z1 = pos1.getZ(); + int x2 = pos2.getX(); + int y2 = pos2.getY(); + int z2 = pos2.getZ(); + int tipx = x1; + int tipy = y1; + int tipz = z1; + int dx = Math.abs(x2 - x1); + int dy = Math.abs(y2 - y1); + int dz = Math.abs(z2 - z1); + + if (dx + dy + dz == 0) { + vset.add(new IrisPosition(tipx, tipy, tipz)); + continue; + } + + int dMax = Math.max(Math.max(dx, dy), dz); + if (dMax == dx) { + for (int domstep = 0; domstep <= dx; domstep++) { + tipx = x1 + domstep * (x2 - x1 > 0 ? 1 : -1); + tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dx) * (y2 - y1 > 0 ? 1 : -1)); + tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dx) * (z2 - z1 > 0 ? 1 : -1)); + + vset.add(new IrisPosition(tipx, tipy, tipz)); + } + } else if (dMax == dy) { + for (int domstep = 0; domstep <= dy; domstep++) { + tipy = y1 + domstep * (y2 - y1 > 0 ? 1 : -1); + tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dy) * (x2 - x1 > 0 ? 1 : -1)); + tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dy) * (z2 - z1 > 0 ? 1 : -1)); + + vset.add(new IrisPosition(tipx, tipy, tipz)); + } + } else /* if (dMax == dz) */ { + for (int domstep = 0; domstep <= dz; domstep++) { + tipz = z1 + domstep * (z2 - z1 > 0 ? 1 : -1); + tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dz) * (y2 - y1 > 0 ? 1 : -1)); + tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dz) * (x2 - x1 > 0 ? 1 : -1)); + + vset.add(new IrisPosition(tipx, tipy, tipz)); + } + } + } + + return vset; + } + + /** + * Set a cylinder in the mantle + * + * @param cx the center x + * @param cy the base y + * @param cz the center z + * @param data the data to set + * @param radius the radius + * @param height the height of the cyl + * @param filled filled or not + */ + public void setCylinder(int cx, int cy, int cz, T data, double radius, int height, boolean filled) { + setCylinder(cx, cy, cz, data, radius, radius, height, filled); + } + + /** + * Set a cylinder in the mantle + * + * @param cx the center x + * @param cy the base y + * @param cz the center z + * @param data the data to set + * @param radiusX the x radius + * @param radiusZ the z radius + * @param height the height of this cyl + * @param filled filled or hollow? + */ + public void setCylinder(int cx, int cy, int cz, T data, double radiusX, double radiusZ, int height, boolean filled) { + int affected = 0; + radiusX += 0.5; + radiusZ += 0.5; + + if (height == 0) { + return; + } else if (height < 0) { + height = -height; + cy = cy - height; + } + + if (cy < 0) { + cy = 0; + } else if (cy + height - 1 > getMantle().getWorldHeight()) { + height = getMantle().getWorldHeight() - cy + 1; + } + + final double invRadiusX = 1 / radiusX; + final double invRadiusZ = 1 / radiusZ; + final int ceilRadiusX = (int) Math.ceil(radiusX); + final int ceilRadiusZ = (int) Math.ceil(radiusZ); + double nextXn = 0; + + forX: + for (int x = 0; x <= ceilRadiusX; ++x) { + final double xn = nextXn; + nextXn = (x + 1) * invRadiusX; + double nextZn = 0; + for (int z = 0; z <= ceilRadiusZ; ++z) { + final double zn = nextZn; + nextZn = (z + 1) * invRadiusZ; + double distanceSq = lengthSq(xn, zn); + + if (distanceSq > 1) { + if (z == 0) { + break forX; + } + + break; + } + + if (!filled) { + if (lengthSq(nextXn, zn) <= 1 && lengthSq(xn, nextZn) <= 1) { + continue; + } + } + + for (int y = 0; y < height; ++y) { + setData(cx + x, cy + y, cz + z, data); + setData(cx + -x, cy + y, cz + z, data); + setData(cx + x, cy + y, cz + -z, data); + setData(cx + -x, cy + y, cz + -z, data); + } + } + } + } + + public void set(IrisPosition pos, T data) { + try { + setData(pos.getX(), pos.getY(), pos.getZ(), data); + } catch (Throwable e) { + // Reached per position while an object writes past the edge of the writer window, which is + // how a large object is clipped. Nothing here is actionable from outside the engine. + IrisLogging.debug("No set? " + data.toString() + " for " + pos.toString()); + } + } + + public void set(List positions, T data) { + for (IrisPosition i : positions) { + set(i, data); + } + } + + public void set(Set positions, T data) { + for (IrisPosition i : positions) { + set(i, data); + } + } + + public void setConsumer(Set positions, Function3 data) { + for (IrisPosition i : positions) { + set(i, data.apply(i.getX(), i.getY(), i.getZ())); + } + } + + public boolean isWithin(Vector pos) { + return isWithin(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); + } + + public boolean isWithin(int x, int y, int z) { + int cx = x >> 4; + int cz = z >> 4; + + if (y < 0 || y >= mantle.getWorldHeight()) { + return false; + } + + return cx >= this.x - radius && cx <= this.x + radius + && cz >= this.z - radius && cz <= this.z + radius; + } + + @Override + public void close() { + for (int index = 0; index < window.length(); index++) { + MantleChunk chunk = window.getAndSet(index, null); + if (chunk != null) { + chunk.release(); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java new file mode 100644 index 000000000..907dbea12 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java @@ -0,0 +1,406 @@ +package art.arcane.iris.engine.mantle; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public interface MatterGenerator { + MultiBurst DISPATCHER = MultiBurst.burst; + ConcurrentHashMap> IN_FLIGHT_COMPONENTS = new ConcurrentHashMap<>(); + long COMPONENT_TASK_POLL_MS = 1000L; + long COMPONENT_TASK_TIMEOUT_MS = Long.getLong("iris.mantle.componentTimeout", 120000L); + + Engine getEngine(); + + Mantle getMantle(); + + int getRadius(); + + int getRealRadius(); + + List getComponents(); + + @ChunkCoordinates + default void generateMatter(int x, int z, boolean multicore, ChunkContext context) { + if (!getEngine().getDimension().isUseMantle()) { + return; + } + + int writeRadius = getRadius(); + LongOpenHashSet partialChunks = new LongOpenHashSet(); + + try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, multicore)) { + // Every task launched below writes through this writer. They must all be finished before + // close() releases the cached chunks, even when a pass throws, or detached pool threads + // write into released chunks. + List outstandingTasks = null; + + try { + for (MantlePass pass : getComponents()) { + int passRadius = pass.passChunkRadius(); + List passComponents = pass.components(); + MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()]; + int[] componentPassRadii = new int[passComponents.size()]; + int enabledComponentCount = 0; + for (MantleComponent component : passComponents) { + if (component.isEnabled()) { + // A component must cover its own reach plus every later pass' reach, or a + // later pass reads this component's data from chunks it never wrote. + int componentReach = component.getRadius() + pass.downstreamBlockRadius(); + componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0; + enabledComponents[enabledComponentCount++] = component; + } + } + + if (enabledComponentCount == 0) { + continue; + } + + // A dispatcher thread runs its components inline: dispatching from inside the pool + // only trades a worker for a blocked worker. + boolean asyncComponents = multicore && !DISPATCHER.ownsCurrentThread(); + if (asyncComponents && outstandingTasks == null) { + outstandingTasks = new ArrayList<>(); + } + MantleComponent[] eligibleComponents = new MantleComponent[enabledComponentCount]; + + for (int i = -passRadius; i <= passRadius; i++) { + int absI = Math.abs(i); + for (int j = -passRadius; j <= passRadius; j++) { + int absJ = Math.abs(j); + int passX = x + i; + int passZ = z + j; + long passKey = chunkKey(passX, passZ); + boolean partial = false; + boolean anyComponentInRadius = false; + + for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) { + int componentPassRadius = componentPassRadii[componentIndex]; + if (absI > componentPassRadius || absJ > componentPassRadius) { + partial = true; + } else { + anyComponentInRadius = true; + } + } + + if (!anyComponentInRadius) { + partialChunks.add(passKey); + continue; + } + + if (partial) { + partialChunks.add(passKey); + } + + MantleChunk chunk = writer.acquireChunk(passX, passZ); + if (chunk == null) { + throw new IllegalStateException("Mantle pass chunk " + passX + "," + passZ + + " is outside the writer prepared at " + x + "," + z + " with radius " + writeRadius); + } + + if (chunk.isFlagged(MantleFlag.PLANNED)) { + continue; + } + + int eligibleComponentCount = 0; + for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) { + MantleComponent component = enabledComponents[componentIndex]; + int componentPassRadius = componentPassRadii[componentIndex]; + if (absI > componentPassRadius || absJ > componentPassRadius) { + continue; + } + + if (chunk.isFlagged(component.getFlag())) { + continue; + } + + MantleFlag[] prerequisites = component.getPrerequisiteFlags(); + if (prerequisites.length > 0) { + boolean prerequisitesMet = true; + for (MantleFlag prereq : prerequisites) { + if (!chunk.isFlagged(prereq)) { + prerequisitesMet = false; + break; + } + } + if (!prerequisitesMet) { + partialChunks.add(passKey); + continue; + } + } + + eligibleComponents[eligibleComponentCount++] = component; + } + + if (eligibleComponentCount == 0) { + continue; + } + + int finalPassX = passX; + int finalPassZ = passZ; + if (asyncComponents) { + for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) { + MantleComponent component = eligibleComponents[componentIndex]; + outstandingTasks.add(runComponentAsync(chunk, component, writer, finalPassX, finalPassZ, context)); + } + } else { + for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) { + MantleComponent component = eligibleComponents[componentIndex]; + runComponentInline(chunk, component, writer, finalPassX, finalPassZ, context); + } + } + } + } + + if (asyncComponents) { + awaitComponentTasks(outstandingTasks); + } + } + + for (int i = -getRealRadius(); i <= getRealRadius(); i++) { + for (int j = -getRealRadius(); j <= getRealRadius(); j++) { + int realX = x + i; + int realZ = z + j; + long realKey = chunkKey(realX, realZ); + if (partialChunks.contains(realKey)) { + continue; + } + writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true); + } + } + } finally { + abandonComponentTasks(outstandingTasks); + } + } + } + + private static long chunkKey(int x, int z) { + return (((long) x) << 32) ^ (z & 0xffffffffL); + } + + private MatterComponentTask runComponentAsync( + MantleChunk chunk, + MantleComponent component, + MantleWriter writer, + int chunkX, + int chunkZ, + ChunkContext context + ) { + MantleFlag flag = component.getFlag(); + MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag); + if (chunk.isFlagged(flag)) { + return new MatterComponentTask(key, CompletableFuture.completedFuture(null), null); + } + + CompletableFuture future = new CompletableFuture<>(); + CompletableFuture existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, future); + if (existing != null) { + return new MatterComponentTask(key, existing, null); + } + + try { + if (DISPATCHER.ownsCurrentThread()) { + completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context); + return new MatterComponentTask(key, future, null); + } + + Future submission = DISPATCHER.submit(() -> completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context)); + return new MatterComponentTask(key, future, submission); + } catch (Throwable throwable) { + IN_FLIGHT_COMPONENTS.remove(key, future); + future.completeExceptionally(throwable); + throw throwable; + } + } + + /** + * Pass barrier. Waits for every launched task, even if one of them failed, so that no task is + * still writing through the writer once this returns. The first failure is rethrown. + */ + private static void awaitComponentTasks(List tasks) { + if (tasks.isEmpty()) { + return; + } + + Throwable failure = null; + for (MatterComponentTask task : tasks) { + try { + awaitComponentTask(task); + } catch (Throwable throwable) { + if (failure == null) { + failure = throwable; + } else if (failure != throwable) { + failure.addSuppressed(throwable); + } + } + } + tasks.clear(); + + if (failure != null) { + throw failure instanceof RuntimeException runtime ? runtime : new CompletionException(failure); + } + } + + /** + * Drains tasks that never reached their pass barrier because the pass threw. The generation + * attempt is already failing, so the wait itself is the point and failures are dropped rather + * than masking the original throwable. + */ + private static void abandonComponentTasks(List tasks) { + if (tasks == null || tasks.isEmpty()) { + return; + } + + try { + awaitComponentTasks(tasks); + } catch (Throwable ignored) { + } + } + + private static void awaitComponentTask(MatterComponentTask task) { + CompletableFuture future = task.future(); + long start = System.currentTimeMillis(); + boolean interrupted = false; + + try { + while (true) { + try { + future.get(COMPONENT_TASK_POLL_MS, TimeUnit.MILLISECONDS); + return; + } catch (InterruptedException interruption) { + // The writer cannot close while this task still writes through it, so the wait is + // uninterruptible and the flag is restored on the way out. + interrupted = true; + } catch (TimeoutException timeout) { + Future submission = task.submission(); + boolean dropped = submission != null && submission.isDone(); + if (future.isDone()) { + continue; + } + + long waited = System.currentTimeMillis() - start; + if (!dropped && waited < COMPONENT_TASK_TIMEOUT_MS) { + continue; + } + + // The task can no longer complete (the dispatcher dropped it) or is wedged. Drop + // the dedup entry so later generations do not inherit a future that never + // completes, and release anything else already waiting on it. + IllegalStateException failure = new IllegalStateException("Mantle component " + task.key() + + (dropped ? " was dropped by the dispatcher" : " did not complete in " + waited + "ms")); + IN_FLIGHT_COMPONENTS.remove(task.key(), future); + future.completeExceptionally(failure); + IrisLogging.error(failure.getMessage()); + throw new CompletionException(failure); + } catch (ExecutionException executionFailure) { + Throwable cause = executionFailure.getCause(); + throw new CompletionException(cause != null ? cause : executionFailure); + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + private void completeComponentTask( + CompletableFuture future, + MatterTaskKey key, + MantleChunk chunk, + MantleComponent component, + MantleWriter writer, + int chunkX, + int chunkZ, + ChunkContext context + ) { + try { + runComponentInline(chunk, component, writer, chunkX, chunkZ, context); + future.complete(null); + } catch (Throwable throwable) { + future.completeExceptionally(throwable); + throw throwable; + } finally { + IN_FLIGHT_COMPONENTS.remove(key, future); + } + } + + private void runComponentInline( + MantleChunk chunk, + MantleComponent component, + MantleWriter writer, + int chunkX, + int chunkZ, + ChunkContext context + ) { + chunk.raiseFlagSuspend(component.getFlag(), () -> component.generateLayer(writer, chunkX, chunkZ, context)); + } + + /** + * A launched component task. {@code submission} is the dispatcher handle when this generation + * owns the task, and null when the future belongs to another generation or already completed. + */ + record MatterComponentTask(MatterTaskKey key, CompletableFuture future, Future submission) { + } + + final class MatterTaskKey { + private final Mantle mantle; + private final int chunkX; + private final int chunkZ; + private final MantleFlag flag; + + MatterTaskKey(Mantle mantle, int chunkX, int chunkZ, MantleFlag flag) { + this.mantle = mantle; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.flag = flag; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof MatterTaskKey other)) { + return false; + } + + return mantle == other.mantle + && chunkX == other.chunkX + && chunkZ == other.chunkZ + && flag.ordinal() == other.flag.ordinal(); + } + + @Override + public int hashCode() { + int result = System.identityHashCode(mantle); + result = 31 * result + chunkX; + result = 31 * result + chunkZ; + result = 31 * result + flag.ordinal(); + return result; + } + + @Override + public String toString() { + return flag.name() + " at " + chunkX + "," + chunkZ; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java new file mode 100644 index 000000000..a9e964f2e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java @@ -0,0 +1,313 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; + +import java.util.Arrays; + +public final class CarveOrphanSweep { + private static final int CHUNK_SIZE = 16; + private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE; + private static final int BAND_FLOOR_MARGIN = 4; + private static final int MAX_ORPHAN_CELLS = 16; + private static final MatterCavern ORPHAN_CAVERN = new MatterCavern(true, "", (byte) 0); + private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(SweepScratch::new); + + public interface CarveAccess { + boolean isCarved(int localX, int y, int localZ); + + void markCarved(int localX, int y, int localZ); + + default boolean isProtected(int localX, int y, int localZ) { + return false; + } + + /** + * Cheap pre-check: may any block in [minY, maxY] be carved at all? Default true keeps + * every implementor correct; the mantle-backed access answers from section slice + * presence so an uncarved chunk skips the full band fill + scan. + */ + default boolean mayContainCarvedCells(int minY, int maxY) { + return true; + } + } + + private CarveOrphanSweep() { + } + + static int sweepChunk( + MantleChunk chunk, + int[] surfaceHeights, + int maxSurfaceBreakDepth, + int worldCeilingY, + int[] surfaceFluidBoundaryStartY, + int fluidHeight + ) { + if (chunk == null) { + return 0; + } + + return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY, + new MantleCarveAccess(chunk, surfaceFluidBoundaryStartY, fluidHeight)); + } + + public static int sweep(int[] surfaceHeights, int maxSurfaceBreakDepth, int worldFloorY, int worldCeilingY, CarveAccess access) { + if (surfaceHeights == null || surfaceHeights.length < CHUNK_AREA || access == null) { + return 0; + } + + int minSurfaceY = Integer.MAX_VALUE; + int maxSurfaceY = Integer.MIN_VALUE; + for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) { + int surfaceY = surfaceHeights[columnIndex]; + if (surfaceY < minSurfaceY) { + minSurfaceY = surfaceY; + } + if (surfaceY > maxSurfaceY) { + maxSurfaceY = surfaceY; + } + } + + int bandTop = Math.min(worldCeilingY, maxSurfaceY); + int bandFloor = Math.max(worldFloorY + 1, minSurfaceY - Math.max(0, maxSurfaceBreakDepth) - BAND_FLOOR_MARGIN); + if (bandTop < bandFloor) { + return 0; + } + + if (!access.mayContainCarvedCells(bandFloor, bandTop)) { + // Same early return the carvedPresent check reaches, without zero-filling and + // scanning the whole band first. + return 0; + } + + int bandHeight = bandTop - bandFloor + 1; + int cellCount = bandHeight * CHUNK_AREA; + SweepScratch scratch = SCRATCH.get(); + scratch.prepare(cellCount); + long[] solid = scratch.solid; + long[] visited = scratch.visited; + int[] stack = scratch.stack; + int[] component = scratch.component; + + boolean carvedPresent = false; + for (int y = bandFloor; y <= bandTop; y++) { + int layer = (y - bandFloor) * CHUNK_AREA; + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + if (y > surfaceHeights[columnIndex]) { + continue; + } + if (access.isCarved(localX, y, localZ)) { + carvedPresent = true; + continue; + } + + setBit(solid, layer + columnIndex); + } + } + } + + if (!carvedPresent) { + return 0; + } + + int marked = 0; + for (int rootIndex = 0; rootIndex < cellCount; rootIndex++) { + if (!getBit(solid, rootIndex) || getBit(visited, rootIndex)) { + continue; + } + + setBit(visited, rootIndex); + stack[0] = rootIndex; + int stackSize = 1; + int componentSize = 0; + boolean anchored = false; + + while (stackSize > 0) { + int current = stack[--stackSize]; + int columnIndex = current & (CHUNK_AREA - 1); + int localX = columnIndex >>> 4; + int localZ = columnIndex & (CHUNK_SIZE - 1); + int y = bandFloor + (current / CHUNK_AREA); + + if (!anchored) { + if (access.isProtected(localX, y, localZ)) { + anchored = true; + } else if (localX == 0 || localX == CHUNK_SIZE - 1 || localZ == 0 || localZ == CHUNK_SIZE - 1) { + anchored = true; + } else if (y == bandFloor && isSolidBelowBand(access, surfaceHeights, columnIndex, localX, localZ, bandFloor - 1, worldFloorY)) { + anchored = true; + } else if (componentSize >= MAX_ORPHAN_CELLS) { + anchored = true; + } else { + component[componentSize++] = current; + } + } + + if (y > bandFloor) { + stackSize = push(solid, visited, stack, stackSize, current - CHUNK_AREA); + } + if (y < bandTop) { + stackSize = push(solid, visited, stack, stackSize, current + CHUNK_AREA); + } + if (localX > 0) { + stackSize = push(solid, visited, stack, stackSize, current - CHUNK_SIZE); + } + if (localX < CHUNK_SIZE - 1) { + stackSize = push(solid, visited, stack, stackSize, current + CHUNK_SIZE); + } + if (localZ > 0) { + stackSize = push(solid, visited, stack, stackSize, current - 1); + } + if (localZ < CHUNK_SIZE - 1) { + stackSize = push(solid, visited, stack, stackSize, current + 1); + } + } + + if (anchored) { + continue; + } + + for (int index = 0; index < componentSize; index++) { + int cell = component[index]; + int columnIndex = cell & (CHUNK_AREA - 1); + access.markCarved(columnIndex >>> 4, bandFloor + (cell / CHUNK_AREA), columnIndex & (CHUNK_SIZE - 1)); + marked++; + } + } + + return marked; + } + + private static int push(long[] solid, long[] visited, int[] stack, int stackSize, int neighbor) { + if (!getBit(solid, neighbor) || getBit(visited, neighbor)) { + return stackSize; + } + + setBit(visited, neighbor); + stack[stackSize] = neighbor; + return stackSize + 1; + } + + private static boolean isSolidBelowBand(CarveAccess access, int[] surfaceHeights, int columnIndex, int localX, int localZ, int belowY, int worldFloorY) { + if (belowY < worldFloorY || belowY > surfaceHeights[columnIndex]) { + return false; + } + + return !access.isCarved(localX, belowY, localZ); + } + + private static boolean getBit(long[] bits, int index) { + return (bits[index >>> 6] & (1L << (index & 63))) != 0L; + } + + private static void setBit(long[] bits, int index) { + bits[index >>> 6] |= 1L << (index & 63); + } + + private static final class MantleCarveAccess implements CarveAccess { + private final MantleChunk chunk; + private final int[] surfaceFluidBoundaryStartY; + private final int fluidHeight; + private MatterSlice cachedSlice; + private int cachedSectionIndex = -1; + + private MantleCarveAccess(MantleChunk chunk, int[] surfaceFluidBoundaryStartY, int fluidHeight) { + this.chunk = chunk; + this.surfaceFluidBoundaryStartY = surfaceFluidBoundaryStartY; + this.fluidHeight = fluidHeight; + } + + @Override + public boolean isCarved(int localX, int y, int localZ) { + MatterSlice cavernSlice = resolveSlice(y >> 4); + if (cavernSlice == null) { + return false; + } + + MatterCavern cavern = cavernSlice.get(localX, y & 15, localZ); + return cavern != null && cavern.isCavern(); + } + + @Override + public void markCarved(int localX, int y, int localZ) { + chunk.getOrCreate(y >> 4).slice(MatterCavern.class).set(localX, y & 15, localZ, ORPHAN_CAVERN); + cachedSectionIndex = -1; + cachedSlice = null; + } + + @Override + public boolean isProtected(int localX, int y, int localZ) { + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight); + } + + @Override + public boolean mayContainCarvedCells(int minY, int maxY) { + int minSection = Math.max(0, minY >> 4); + int maxSection = Math.min(chunk.sectionCount() - 1, maxY >> 4); + for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) { + Matter section = chunk.get(sectionIndex); + if (section != null && section.getSlice(MatterCavern.class) != null) { + return true; + } + } + return false; + } + + private MatterSlice resolveSlice(int sectionIndex) { + if (sectionIndex == cachedSectionIndex) { + return cachedSlice; + } + + Matter section = sectionIndex >= 0 && sectionIndex < chunk.sectionCount() ? chunk.get(sectionIndex) : null; + cachedSlice = section == null ? null : section.getSlice(MatterCavern.class); + cachedSectionIndex = sectionIndex; + return cachedSlice; + } + } + + private static final class SweepScratch { + private final int[] component = new int[MAX_ORPHAN_CELLS]; + private long[] solid = new long[0]; + private long[] visited = new long[0]; + private int[] stack = new int[0]; + + private void prepare(int cellCount) { + int words = (cellCount + 63) >>> 6; + if (solid.length < words) { + solid = new long[words]; + visited = new long[words]; + } else { + Arrays.fill(solid, 0, words, 0L); + Arrays.fill(visited, 0, words, 0L); + } + + if (stack.length < cellCount) { + stack = new int[cellCount]; + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java new file mode 100644 index 000000000..c83dc6bc7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java @@ -0,0 +1,71 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; + +final class CaveCarveScratch { + final int[] columnMaxY = new int[256]; + final int[] fluidMaxY = new int[256]; + final int[] surfaceBreakFloorY = new int[256]; + final boolean[] surfaceBreakColumn = new boolean[256]; + final double[] columnThreshold = new double[256]; + final double[] passThreshold = new double[256]; + final double[] fullWeights = new double[256]; + final double[] clampedColumnWeights = new double[256]; + final int[] activeColumnIndices = new int[256]; + final int[] activeColumnTopY = new int[256]; + final int[] planeColumnIndices = new int[256]; + final double[] planeThresholdLimit = new double[256]; + final boolean[] planeCarve = new boolean[256]; + final double[] adaptivePlaneDensity = new double[81]; + final double[] adaptivePlanePrediction = new double[256]; + final double[] adaptivePlaneAmbiguity = new double[256]; + final int[] adaptivePlaneSampleBounds = new int[4]; + final int[] adaptiveCellX = new int[256]; + final int[] adaptiveCellZ = new int[256]; + final int[] adaptiveRow0 = new int[256]; + final int[] adaptiveRow1 = new int[256]; + final double[] adaptiveTx = new double[256]; + final double[] adaptiveTz = new double[256]; + final boolean[] warpCacheSet = new boolean[256]; + final int[] warpCacheX = new int[256]; + final int[] warpCacheY = new int[256]; + final int[] warpCacheZ = new int[256]; + final double[] warpCacheA = new double[256]; + final double[] warpCacheB = new double[256]; + final int[] tileIndices = new int[4]; + final int[] tileLocalX = new int[4]; + final int[] tileLocalZ = new int[4]; + final int[] tileTopY = new int[4]; + CaveFieldModuleState[] activeModules = new CaveFieldModuleState[0]; + double[] activeModuleRemainingMin = new double[0]; + double[] activeModuleRemainingMax = new double[0]; + int activeModulesY = Integer.MIN_VALUE; + int activeModuleCount; + double[] verticalEdgeFade = new double[0]; + MatterCavern[] matterByY = new MatterCavern[0]; + Matter[] sectionMatter = new Matter[0]; + MatterSlice[] sectionSlices = new MatterSlice[0]; + int adaptiveGeometryStep = -1; + int adaptiveGeometryAxisCells = -1; + boolean fullWeightsInitialized; +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFieldModuleState.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFieldModuleState.java new file mode 100644 index 000000000..295edc3e1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFieldModuleState.java @@ -0,0 +1,58 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.engine.object.IrisCaveFieldModule; +import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.util.project.noise.CNG; + +final class CaveFieldModuleState { + final CNG density; + final int minY; + final int maxY; + final double weight; + final double threshold; + final boolean invert; + final double minContribution; + final double maxContribution; + + CaveFieldModuleState(IrisCaveFieldModule module, CNG density) { + IrisRange range = module.getVerticalRange(); + this.density = density; + this.minY = (int) Math.floor(range.getMin()); + this.maxY = (int) Math.ceil(range.getMax()); + this.weight = module.getWeight(); + this.threshold = module.getThreshold(); + this.invert = module.isInvert(); + double rawMin = invert ? threshold - 1D : -1D - threshold; + double rawMax = invert ? threshold + 1D : 1D - threshold; + this.minContribution = rawMin * weight; + this.maxContribution = rawMax * weight; + } + + double sample(double x, int y, double z) { + double sampled = density.noiseFastSigned3D(x, y, z); + return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight; + } + + double sample(double x, double y, double z) { + double sampled = density.noiseFastSigned3D(x, y, z); + return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFluidSupportPlan.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFluidSupportPlan.java new file mode 100644 index 000000000..ca98541ed --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFluidSupportPlan.java @@ -0,0 +1,120 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; + +import java.util.BitSet; +import java.util.IdentityHashMap; +import java.util.Map; + +final class CaveFluidSupportPlan { + private final IdentityHashMap groups = new IdentityHashMap<>(); + + void add(int localX, int y, int localZ, MatterCavern fluid, MatterCavern air) { + FluidCandidateGroup group = groups.computeIfAbsent(fluid, key -> new FluidCandidateGroup(fluid, air)); + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + group.positions.set((y << 8) | columnIndex); + } + + void resolve(MantleChunk chunk) { + if (chunk == null) { + groups.clear(); + return; + } + + for (Map.Entry entry : groups.entrySet()) { + FluidCandidateGroup group = entry.getValue(); + for (int position = group.positions.nextSetBit(0); position >= 0; position = group.positions.nextSetBit(position + 1)) { + int y = position >>> 8; + int columnIndex = position & 255; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + MatterCavern current = getCavern(chunk, localX, y, localZ); + if (current != group.fluid || hasCupSupport(chunk, localX, y, localZ)) { + continue; + } + + Matter section = chunk.get(y >> 4); + MatterSlice cavernSlice = section.getSlice(MatterCavern.class); + cavernSlice.set(localX, y & 15, localZ, group.air); + } + } + groups.clear(); + } + + private static boolean hasCupSupport(MantleChunk chunk, int localX, int y, int localZ) { + if (localX <= 0 || localX >= 15 || localZ <= 0 || localZ >= 15 + || y <= 1 || !isSolid(chunk, localX, y - 1, localZ) + || !isSolid(chunk, localX, y - 2, localZ)) { + return false; + } + + int support = 0; + if (isSolid(chunk, localX + 1, y, localZ)) { + support++; + } + if (isSolid(chunk, localX - 1, y, localZ)) { + support++; + } + if (isSolid(chunk, localX, y, localZ + 1)) { + support++; + } + if (isSolid(chunk, localX, y, localZ - 1)) { + support++; + } + if (isSolid(chunk, localX, y + 1, localZ)) { + support++; + } + return support >= 4; + } + + private static boolean isSolid(MantleChunk chunk, int localX, int y, int localZ) { + if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16) { + return false; + } + MatterCavern cavern = getCavern(chunk, localX, y, localZ); + return cavern == null || !cavern.isCavern(); + } + + private static MatterCavern getCavern(MantleChunk chunk, int localX, int y, int localZ) { + Matter section = chunk.get(y >> 4); + if (section == null) { + return null; + } + + MatterSlice cavernSlice = section.getSlice(MatterCavern.class); + return cavernSlice == null ? null : cavernSlice.get(localX, y & 15, localZ); + } + + private static final class FluidCandidateGroup { + private final MatterCavern fluid; + private final MatterCavern air; + private final BitSet positions = new BitSet(); + + private FluidCandidateGroup(MatterCavern fluid, MatterCavern air) { + this.fluid = fluid; + this.air = air; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransaction.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransaction.java new file mode 100644 index 000000000..fd838eb6d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransaction.java @@ -0,0 +1,286 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.collection.KList; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +final class CaveObjectPlacementTransaction implements IObjectPlacer { + private static final int CACHE_MISS = Integer.MIN_VALUE; + + private final IObjectPlacer delegate; + private final Engine engine; + private final int anchorY; + private final int minDepthBelowSurface; + private final int worldHeight; + private final KList mutations; + private final Map bufferedBlocks; + private final Long2IntOpenHashMap surfaceHeights; + private final Long2IntOpenHashMap caveCeilings; + private int blockWrites; + + CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) { + this.delegate = delegate; + this.engine = delegate.getEngine(); + this.anchorY = anchorY; + this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface); + this.worldHeight = engine == null ? 0 : engine.getHeight(); + this.mutations = new KList<>(); + this.bufferedBlocks = new HashMap<>(); + this.surfaceHeights = new Long2IntOpenHashMap(); + this.caveCeilings = new Long2IntOpenHashMap(); + this.surfaceHeights.defaultReturnValue(CACHE_MISS); + this.caveCeilings.defaultReturnValue(CACHE_MISS); + } + + CommitResult commit() { + if (blockWrites == 0) { + discard(); + return CommitResult.EMPTY; + } + + for (BufferedMutation mutation : mutations) { + if (!isWithinBounds(mutation.x(), mutation.y(), mutation.z())) { + discard(); + return CommitResult.REJECTED_BOUNDS; + } + } + + for (BufferedMutation mutation : mutations) { + mutation.apply(delegate); + } + discard(); + return CommitResult.COMMITTED; + } + + void discard() { + mutations.clear(); + bufferedBlocks.clear(); + blockWrites = 0; + } + + int getCaveCeiling(int x, int z) { + long key = Cache.key(x, z); + int cached = caveCeilings.get(key); + if (cached != CACHE_MISS) { + return cached; + } + + int ceiling = findCaveCeiling(x, z); + caveCeilings.put(key, ceiling); + return ceiling; + } + + static int maxBuriedY(int worldHeight, int surfaceY, int minDepthBelowSurface) { + return Math.min(worldHeight - 1, surfaceY - Math.max(0, minDepthBelowSurface)); + } + + @Override + public int getHighest(int x, int z, IrisData data) { + return delegate.getHighest(x, z, data); + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return delegate.getHighest(x, z, data, ignoreFluid); + } + + @Override + public void set(int x, int y, int z, PlatformBlockState state) { + if (state == null) { + return; + } + mutations.add(new BlockMutation(x, y, z, state)); + bufferedBlocks.put(new PositionKey(x, y, z), state); + blockWrites++; + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + PlatformBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z)); + return buffered == null ? delegate.get(x, y, z) : buffered; + } + + @Override + public boolean isPreventingDecay() { + return delegate.isPreventingDecay(); + } + + @Override + public boolean isCarved(int x, int y, int z) { + return delegate.isCarved(x, y, z); + } + + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + return delegate.isSurfaceSolid(x, y, z); + } + + @Override + public boolean isSolid(int x, int y, int z) { + PlatformBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z)); + return buffered == null ? delegate.isSolid(x, y, z) : B.isSolid(buffered); + } + + @Override + public boolean isUnderwater(int x, int z) { + return delegate.isUnderwater(x, z); + } + + @Override + public int getFluidHeight() { + return delegate.getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return delegate.isDebugSmartBore(); + } + + @Override + public void setTile(int x, int y, int z, TileData tile) { + if (tile != null) { + mutations.add(new TileMutation(x, y, z, tile)); + } + } + + @Override + public void setData(int x, int y, int z, T data) { + if (data == null) { + return; + } + mutations.add(new DataMutation(x, y, z, data)); + if (data instanceof PlatformBlockState state) { + bufferedBlocks.put(new PositionKey(x, y, z), state); + blockWrites++; + } + } + + @Override + public @Nullable T getData(int x, int y, int z, Class type) { + for (int i = mutations.size() - 1; i >= 0; i--) { + BufferedMutation mutation = mutations.get(i); + if (mutation.x() != x || mutation.y() != y || mutation.z() != z) { + continue; + } + Object value = mutation.value(); + if (type.isInstance(value)) { + return type.cast(value); + } + } + return delegate.getData(x, y, z, type); + } + + @Override + public Engine getEngine() { + return engine; + } + + private boolean isWithinBounds(int x, int y, int z) { + if (engine == null || y < 0 || y >= worldHeight) { + return false; + } + int surfaceY = getSurfaceHeight(x, z); + if (y > maxBuriedY(worldHeight, surfaceY, minDepthBelowSurface)) { + return false; + } + return y < getCaveCeiling(x, z); + } + + private int getSurfaceHeight(int x, int z) { + long key = Cache.key(x, z); + int cached = surfaceHeights.get(key); + if (cached != CACHE_MISS) { + return cached; + } + int surfaceY = engine.getHeight(x, z, true); + surfaceHeights.put(key, surfaceY); + return surfaceY; + } + + private int findCaveCeiling(int x, int z) { + if (worldHeight <= 0 || anchorY < 0 || anchorY >= worldHeight) { + return 0; + } + if (!delegate.isCarved(x, anchorY, z)) { + return Math.min(worldHeight, anchorY + 1); + } + + int scanLimit = Math.min(worldHeight - 1, Math.max(anchorY, getSurfaceHeight(x, z))); + for (int y = anchorY + 1; y <= scanLimit; y++) { + if (!delegate.isCarved(x, y, z)) { + return y; + } + } + return Math.min(worldHeight, scanLimit + 1); + } + + enum CommitResult { + COMMITTED, + EMPTY, + REJECTED_BOUNDS + } + + private interface BufferedMutation { + int x(); + + int y(); + + int z(); + + Object value(); + + void apply(IObjectPlacer placer); + } + + private record BlockMutation(int x, int y, int z, PlatformBlockState value) implements BufferedMutation { + @Override + public void apply(IObjectPlacer placer) { + placer.set(x, y, z, value); + } + } + + private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation { + @Override + public void apply(IObjectPlacer placer) { + placer.setTile(x, y, z, value); + } + } + + private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation { + @Override + public void apply(IObjectPlacer placer) { + placer.setData(x, y, z, value); + } + } + + private record PositionKey(int x, int y, int z) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingIslandSampleResolver.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingIslandSampleResolver.java new file mode 100644 index 000000000..db4cbdbc6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingIslandSampleResolver.java @@ -0,0 +1,74 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier; +import art.arcane.iris.engine.object.FloatingIslandBoundarySampler; +import art.arcane.iris.engine.object.FloatingIslandSample; +import art.arcane.iris.engine.object.IrisBiome; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import org.jetbrains.annotations.Nullable; + +final class FloatingIslandSampleResolver implements IslandObjectPlacer.SampleProvider { + private final IrisData data; + private final Engine engine; + private final int chunkHeight; + private final long baseSeed; + private final FloatingIslandBoundarySampler boundarySampler; + private final Long2ObjectOpenHashMap sampleCache; + private final LongOpenHashSet resolvedSamples; + + FloatingIslandSampleResolver(EngineMantle engineMantle, FloatingIslandBoundarySampler boundarySampler) { + this.data = engineMantle.getData(); + this.engine = engineMantle.getEngine(); + this.chunkHeight = engine.getHeight(); + this.baseSeed = engine.getSeedManager().getTerrain() ^ IrisFloatingChildBiomeModifier.FLOATING_BASE_SEED_SALT; + this.boundarySampler = boundarySampler; + this.sampleCache = new Long2ObjectOpenHashMap<>(); + this.resolvedSamples = new LongOpenHashSet(); + } + + @Override + public @Nullable FloatingIslandSample sample(int x, int z) { + long key = Cache.key(x, z); + if (resolvedSamples.contains(key)) { + return sampleCache.get(key); + } + IrisBiome parent = parent(x, z); + if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) { + resolvedSamples.add(key); + return null; + } + FloatingIslandSample sample = FloatingIslandSample.sample(parent, x, z, chunkHeight, baseSeed, data, engine, boundarySampler); + resolvedSamples.add(key); + if (sample != null) { + sampleCache.put(key, sample); + } + return sample; + } + + @Nullable IrisBiome parent(int x, int z) { + return boundarySampler.parent(x, z); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingObjectPlacementTransaction.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingObjectPlacementTransaction.java new file mode 100644 index 000000000..6d703de1d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingObjectPlacementTransaction.java @@ -0,0 +1,211 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.collection.KList; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +final class FloatingObjectPlacementTransaction implements IObjectPlacer { + private final IslandObjectPlacer delegate; + private final KList mutations; + private final Map bufferedBlocks; + private int blockWrites; + + FloatingObjectPlacementTransaction(IslandObjectPlacer delegate) { + this.delegate = delegate; + this.mutations = new KList<>(); + this.bufferedBlocks = new HashMap<>(); + } + + CommitResult commit() { + if (blockWrites == 0) { + discard(); + return CommitResult.EMPTY; + } + for (BufferedMutation mutation : mutations) { + if (!delegate.canWriteObjectBlock(mutation.x(), mutation.y(), mutation.z())) { + discard(); + return CommitResult.REJECTED_SUPPORT; + } + } + for (BufferedMutation mutation : mutations) { + mutation.apply(delegate); + } + discard(); + return CommitResult.COMMITTED; + } + + void discard() { + mutations.clear(); + bufferedBlocks.clear(); + blockWrites = 0; + } + + @Override + public int getHighest(int x, int z, IrisData data) { + return delegate.getHighest(x, z, data); + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return delegate.getHighest(x, z, data, ignoreFluid); + } + + @Override + public void set(int x, int y, int z, PlatformBlockState state) { + if (state == null) { + return; + } + mutations.add(new BlockMutation(x, y, z, state)); + bufferedBlocks.put(new PositionKey(x, y, z), state); + blockWrites++; + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + PlatformBlockState state = bufferedBlocks.get(new PositionKey(x, y, z)); + return state == null ? delegate.get(x, y, z) : state; + } + + @Override + public boolean isPreventingDecay() { + return delegate.isPreventingDecay(); + } + + @Override + public boolean isCarved(int x, int y, int z) { + return delegate.isCarved(x, y, z); + } + + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + return delegate.isSurfaceSolid(x, y, z); + } + + @Override + public boolean isSolid(int x, int y, int z) { + PlatformBlockState state = bufferedBlocks.get(new PositionKey(x, y, z)); + return state == null ? delegate.isSolid(x, y, z) : B.isSolid(state); + } + + @Override + public boolean isUnderwater(int x, int z) { + return delegate.isUnderwater(x, z); + } + + @Override + public int getFluidHeight() { + return delegate.getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return delegate.isDebugSmartBore(); + } + + @Override + public void setTile(int x, int y, int z, TileData tile) { + if (tile != null) { + mutations.add(new TileMutation(x, y, z, tile)); + } + } + + @Override + public void setData(int x, int y, int z, T data) { + if (data == null) { + return; + } + mutations.add(new DataMutation(x, y, z, data)); + if (data instanceof PlatformBlockState state) { + bufferedBlocks.put(new PositionKey(x, y, z), state); + blockWrites++; + } + } + + @Override + public @Nullable T getData(int x, int y, int z, Class type) { + for (int i = mutations.size() - 1; i >= 0; i--) { + BufferedMutation mutation = mutations.get(i); + if (mutation.x() != x || mutation.y() != y || mutation.z() != z) { + continue; + } + Object value = mutation.value(); + if (type.isInstance(value)) { + return type.cast(value); + } + } + return delegate.getData(x, y, z, type); + } + + @Override + public Engine getEngine() { + return delegate.getEngine(); + } + + enum CommitResult { + COMMITTED, + EMPTY, + REJECTED_SUPPORT + } + + private interface BufferedMutation { + int x(); + + int y(); + + int z(); + + Object value(); + + void apply(IObjectPlacer placer); + } + + private record BlockMutation(int x, int y, int z, PlatformBlockState value) implements BufferedMutation { + @Override + public void apply(IObjectPlacer placer) { + placer.set(x, y, z, value); + } + } + + private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation { + @Override + public void apply(IObjectPlacer placer) { + placer.setTile(x, y, z, value); + } + } + + private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation { + @Override + public void apply(IObjectPlacer placer) { + placer.setData(x, y, z, value); + } + } + + private record PositionKey(int x, int y, int z) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/GoldenDebugObjectPlacer.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/GoldenDebugObjectPlacer.java new file mode 100644 index 000000000..34fa73ebe --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/GoldenDebugObjectPlacer.java @@ -0,0 +1,159 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; + +final class GoldenDebugObjectPlacer implements IObjectPlacer { + private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec()); + private static final boolean GOLDEN_DEBUG = GOLDEN_DEBUG_TARGET != null; + + private final IObjectPlacer delegate; + private final String tag; + + GoldenDebugObjectPlacer(IObjectPlacer delegate, String tag) { + this.delegate = delegate; + this.tag = tag; + } + + private static String resolveGoldenDebugSpec() { + String property = System.getProperty("iris.goldendebug"); + if (property != null && !property.isBlank()) { + return property; + } + try { + java.io.File marker = new java.io.File("plugins/Iris/goldendebug.txt"); + if (marker.isFile()) { + return java.nio.file.Files.readString(marker.toPath()).trim(); + } + } catch (Throwable ignored) { + } + return null; + } + + private static int[] parseGoldenDebugTarget(String raw) { + if (raw == null || raw.isBlank()) { + return null; + } + String[] parts = raw.split(","); + if (parts.length != 2 && parts.length != 3) { + return null; + } + try { + int radius = parts.length == 3 ? Integer.parseInt(parts[2].trim()) : 0; + return new int[]{Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim()), radius}; + } catch (NumberFormatException e) { + return null; + } + } + + static boolean isGoldenDebugChunk(int x, int z) { + return GOLDEN_DEBUG + && Math.abs(GOLDEN_DEBUG_TARGET[0] - x) <= GOLDEN_DEBUG_TARGET[2] + && Math.abs(GOLDEN_DEBUG_TARGET[1] - z) <= GOLDEN_DEBUG_TARGET[2]; + } + + @Override + public int getHighest(int x, int z, IrisData data) { + int result = delegate.getHighest(x, z, data); + IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ")=" + result); + return result; + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + int result = delegate.getHighest(x, z, data, ignoreFluid); + IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ",ignoreFluid=" + ignoreFluid + ")=" + result); + return result; + } + + @Override + public void set(int x, int y, int z, PlatformBlockState d) { + delegate.set(x, y, z, d); + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + return delegate.get(x, y, z); + } + + @Override + public boolean isPreventingDecay() { + return delegate.isPreventingDecay(); + } + + @Override + public boolean isCarved(int x, int y, int z) { + boolean result = delegate.isCarved(x, y, z); + IrisLogging.info("Goldendebug query: tag=" + tag + " isCarved(" + x + "," + y + "," + z + ")=" + result); + return result; + } + + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + return delegate.isSurfaceSolid(x, y, z); + } + + @Override + public boolean isSolid(int x, int y, int z) { + boolean result = delegate.isSolid(x, y, z); + IrisLogging.info("Goldendebug query: tag=" + tag + " isSolid(" + x + "," + y + "," + z + ")=" + result); + return result; + } + + @Override + public boolean isUnderwater(int x, int z) { + return delegate.isUnderwater(x, z); + } + + @Override + public int getFluidHeight() { + return delegate.getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return delegate.isDebugSmartBore(); + } + + @Override + public void setData(int xx, int yy, int zz, T data) { + delegate.setData(xx, yy, zz, data); + } + + @Override + public T getData(int xx, int yy, int zz, Class t) { + return delegate.getData(xx, yy, zz, t); + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + delegate.setTile(xx, yy, zz, tile); + } + + @Override + public Engine getEngine() { + return delegate.getEngine(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java new file mode 100644 index 000000000..a42b92f45 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -0,0 +1,2322 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.IrisCaveFieldModule; +import art.arcane.iris.engine.object.IrisCaveProfile; +import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.engine.object.IrisStyledRange; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class IrisCaveCarver3D { + private static final byte LIQUID_AIR = 0; + private static final byte LIQUID_FLUID = 1; + private static final byte LIQUID_LAVA = 2; + private static final byte LIQUID_FORCED_AIR = 3; + private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 16; + private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8; + private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D; + private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D; + + private final Engine engine; + private final IrisData data; + private final IrisCaveProfile profile; + private final CNG baseDensity; + private final CNG detailDensity; + private final CNG warpDensity; + private final CNG surfaceBreakDensity; + private final RNG thresholdRng; + private final CaveFieldModuleState[] modules; + private final double inverseNormalization; + private final MatterCavern carveAir; + private final MatterCavern carveFluid; + private final MatterCavern carveLava; + private final MatterCavern carveForcedAir; + private final double normalizationFactor; + private final double baseWeight; + private final double detailWeight; + private final double detailMinContribution; + private final double detailMaxContribution; + private final double warpStrength; + private final boolean hasWarp; + private final boolean hasModules; + private final int warpResolution; + private final boolean allowFluid; + private final boolean fluidRequiresFloor; + private final int fluidMinDepthBelowSurface; + private final int fluidHeight; + private final int aquiferCeilingY; + private final ThreadLocal scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new); + + public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) { + this.engine = engine; + this.data = engine.getData(); + this.profile = profile; + this.carveAir = new MatterCavern(true, "", LIQUID_AIR); + this.carveFluid = new MatterCavern(true, "", LIQUID_FLUID); + this.carveLava = new MatterCavern(true, "", LIQUID_LAVA); + this.carveForcedAir = new MatterCavern(true, "", LIQUID_FORCED_AIR); + List moduleStates = new ArrayList<>(); + + RNG baseRng = new RNG(engine.getSeedManager().getCarve()); + this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data); + this.detailDensity = profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data); + this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data); + this.surfaceBreakDensity = profile.getSurfaceBreakStyle().create(baseRng.nextParallelRNG(341_219), data); + this.thresholdRng = baseRng.nextParallelRNG(489_112); + this.baseWeight = profile.getBaseWeight(); + this.detailWeight = profile.getDetailWeight(); + this.warpStrength = profile.getWarpStrength(); + this.hasWarp = this.warpStrength > 0D; + this.warpResolution = 2; + this.allowFluid = profile.isAllowFluid(); + this.fluidRequiresFloor = profile.isFluidRequiresFloor(); + this.fluidMinDepthBelowSurface = Math.max(0, profile.getFluidMinDepthBelowSurface()); + this.fluidHeight = engine.getDimension().getFluidHeight(); + this.aquiferCeilingY = engine.getHeight() - 1; + + double weight = Math.abs(baseWeight) + Math.abs(detailWeight); + int index = 0; + for (IrisCaveFieldModule module : profile.getModules()) { + CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data); + CaveFieldModuleState state = new CaveFieldModuleState(module, moduleDensity); + moduleStates.add(state); + weight += Math.abs(state.weight); + index++; + } + + this.modules = moduleStates.toArray(new CaveFieldModuleState[0]); + double normalization = weight <= 0 ? 1 : weight; + normalizationFactor = normalization; + inverseNormalization = 1D / normalization; + hasModules = modules.length > 0; + detailMinContribution = -detailWeight; + detailMaxContribution = detailWeight; + } + + public int carve(MantleWriter writer, int chunkX, int chunkZ) { + CaveCarveScratch scratch = scratchCache.get(); + if (!scratch.fullWeightsInitialized) { + Arrays.fill(scratch.fullWeights, 1D); + scratch.fullWeightsInitialized = true; + } + return carve(writer, chunkX, chunkZ, scratch.fullWeights, 0D, 0D, null, null); + } + + public int carve( + MantleWriter writer, + int chunkX, + int chunkZ, + double[] columnWeights, + double minWeight, + double thresholdPenalty + ) { + return carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, null, null); + } + + public int carve( + MantleWriter writer, + int chunkX, + int chunkZ, + double[] columnWeights, + double minWeight, + double thresholdPenalty, + IrisRange worldYRange + ) { + return carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, worldYRange, null); + } + + public int carve( + MantleWriter writer, + int chunkX, + int chunkZ, + double[] columnWeights, + double minWeight, + double thresholdPenalty, + IrisRange worldYRange, + int[] precomputedSurfaceHeights + ) { + return carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, worldYRange, precomputedSurfaceHeights, null); + } + + public int carve( + MantleWriter writer, + int chunkX, + int chunkZ, + double[] columnWeights, + double minWeight, + double thresholdPenalty, + IrisRange worldYRange, + int[] precomputedSurfaceHeights, + IrisRange overrideVerticalRange + ) { + CaveFluidSupportPlan fluidSupportPlan = new CaveFluidSupportPlan(); + int carved = carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, + worldYRange, precomputedSurfaceHeights, null, overrideVerticalRange, fluidSupportPlan); + fluidSupportPlan.resolve(writer.acquireChunk(chunkX, chunkZ)); + return carved; + } + + int carve( + MantleWriter writer, + int chunkX, + int chunkZ, + double[] columnWeights, + double minWeight, + double thresholdPenalty, + IrisRange worldYRange, + int[] precomputedSurfaceHeights, + int[] surfaceFluidBoundaryStartY, + IrisRange overrideVerticalRange, + CaveFluidSupportPlan fluidSupportPlan + ) { + PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); + try { + CaveCarveScratch scratch = scratchCache.get(); + if (columnWeights == null || columnWeights.length < 256) { + if (!scratch.fullWeightsInitialized) { + Arrays.fill(scratch.fullWeights, 1D); + scratch.fullWeightsInitialized = true; + } + columnWeights = scratch.fullWeights; + } + + double resolvedMinWeight = Math.max(0D, Math.min(1D, minWeight)); + double resolvedThresholdPenalty = Math.max(0D, thresholdPenalty); + int worldHeight = writer.getMantle().getWorldHeight(); + IrisRange effectiveVerticalRange = overrideVerticalRange != null ? overrideVerticalRange : profile.getVerticalRange(); + int minY = Math.max(0, (int) Math.floor(effectiveVerticalRange.getMin())); + int maxY = Math.min(worldHeight - 1, (int) Math.ceil(effectiveVerticalRange.getMax())); + if (worldYRange != null) { + int worldMinHeight = engine.getWorld().minHeight(); + int rangeMinY = (int) Math.floor(worldYRange.getMin() - worldMinHeight); + int rangeMaxY = (int) Math.ceil(worldYRange.getMax() - worldMinHeight); + minY = Math.max(minY, rangeMinY); + maxY = Math.min(maxY, rangeMaxY); + } + int sampleStep = Math.max(1, profile.getSampleStep()); + boolean exactSampling = sampleStep <= 2; + boolean adaptiveSampling = exactSampling && profile.isAdaptiveSampling(); + int adaptiveSampleStep = Math.max(2, profile.getAdaptiveSampleStep()); + double adaptiveThresholdMargin = Math.max(0D, profile.getAdaptiveThresholdMargin()); + int surfaceClearance = Math.max(0, profile.getSurfaceClearance()); + int surfaceBreakDepth = Math.max(0, profile.getSurfaceBreakDepth()); + double surfaceBreakNoiseThreshold = profile.getSurfaceBreakNoiseThreshold(); + double surfaceBreakThresholdBoost = Math.max(0, profile.getSurfaceBreakThresholdBoost()); + boolean allowSurfaceBreak = profile.isAllowSurfaceBreak(); + if (maxY < minY) { + return 0; + } + + MantleChunk chunk = writer.acquireChunk(chunkX, chunkZ); + if (chunk == null) { + return 0; + } + + int x0 = PowerOfTwoCoordinates.chunkToBlock(chunkX); + int z0 = PowerOfTwoCoordinates.chunkToBlock(chunkZ); + int[] columnMaxY = scratch.columnMaxY; + int[] fluidMaxY = scratch.fluidMaxY; + int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY; + boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn; + double[] columnThreshold = scratch.columnThreshold; + double[] clampedWeights = scratch.clampedColumnWeights; + double[] verticalEdgeFade = prepareVerticalEdgeFadeTable(scratch, minY, maxY); + MatterCavern[] matterByY = prepareMatterByYTable(scratch, minY, maxY); + prepareSectionCaches(scratch, minY, maxY); + + // IrisStyledRange.get() resolves its CNG per call, which costs a cache-key allocation, + // a capturing lambda and a shared LRU touch. The CNG is constant for (thresholdRng, data), + // so resolve it once here and replicate the min==max / isFlat short circuits verbatim. + IrisStyledRange densityThreshold = profile.getDensityThreshold(); + double thresholdMin = densityThreshold.getMin(); + double thresholdMax = densityThreshold.getMax(); + double thresholdBias = profile.getThresholdBias(); + CNG thresholdDensity = null; + double constantThreshold = 0D; + if (thresholdMin == thresholdMax) { + constantThreshold = thresholdMin; + } else if (densityThreshold.getStyle().isFlat()) { + constantThreshold = M.lerp(thresholdMin, thresholdMax, 0.5); + } else { + thresholdDensity = densityThreshold.getStyle().create(thresholdRng, data); + } + + for (int lx = 0; lx < 16; lx++) { + int x = x0 + lx; + for (int lz = 0; lz < 16; lz++) { + int z = z0 + lz; + int index = PowerOfTwoCoordinates.packLocal16(lx, lz); + int columnSurfaceY; + if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) { + columnSurfaceY = precomputedSurfaceHeights[index]; + } else { + columnSurfaceY = engine.getHeight(x, z); + } + int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance)); + boolean breakColumn = allowSurfaceBreak + && surfaceBreakDensity.noiseFastSigned2D(x, z) >= surfaceBreakNoiseThreshold; + int columnTopY = breakColumn + ? Math.min(maxY, Math.max(minY, columnSurfaceY)) + : clearanceTopY; + + columnMaxY[index] = columnTopY; + fluidMaxY[index] = allowFluid + ? Math.min(fluidHeight, columnSurfaceY - fluidMinDepthBelowSurface) + : Integer.MIN_VALUE; + surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth); + surfaceBreakColumn[index] = breakColumn; + columnThreshold[index] = (thresholdDensity == null + ? constantThreshold + : thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias; + clampedWeights[index] = clampColumnWeight(columnWeights[index]); + } + } + + int carved; + if (exactSampling) { + if (adaptiveSampling) { + carved = carvePassAdaptive( + chunk, + x0, + z0, + minY, + maxY, + adaptiveSampleStep, + adaptiveThresholdMargin, + surfaceBreakThresholdBoost, + columnMaxY, + fluidMaxY, + surfaceFluidBoundaryStartY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + fluidRequiresFloor ? fluidSupportPlan : null, + resolvedMinWeight, + resolvedThresholdPenalty, + 0D, + false + ); + } else { + carved = carvePassExact( + chunk, + x0, + z0, + minY, + maxY, + surfaceBreakThresholdBoost, + columnMaxY, + fluidMaxY, + surfaceFluidBoundaryStartY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + fluidRequiresFloor ? fluidSupportPlan : null, + resolvedMinWeight, + resolvedThresholdPenalty, + 0D, + false + ); + } + } else { + int latticeStep = sampleStep; + carved = carvePassLattice( + chunk, + x0, + z0, + minY, + maxY, + latticeStep, + surfaceBreakThresholdBoost, + columnMaxY, + fluidMaxY, + surfaceFluidBoundaryStartY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + fluidRequiresFloor ? fluidSupportPlan : null, + resolvedMinWeight, + resolvedThresholdPenalty, + 0D, + false + ); + if (carved == 0 && hasFallbackCandidates(columnMaxY, clampedWeights, minY, resolvedMinWeight)) { + carved += carvePassFallback( + chunk, + x0, + z0, + minY, + maxY, + sampleStep, + surfaceBreakThresholdBoost, + columnMaxY, + fluidMaxY, + surfaceFluidBoundaryStartY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + fluidRequiresFloor ? fluidSupportPlan : null, + resolvedMinWeight, + resolvedThresholdPenalty, + 0D, + false + ); + } + } + + return carved; + } finally { + engine.getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds()); + } + } + + private int carvePassExact( + MantleChunk chunk, + int x0, + int z0, + int minY, + int maxY, + double surfaceBreakThresholdBoost, + int[] columnMaxY, + int[] fluidMaxY, + int[] surfaceFluidBoundaryStartY, + int[] surfaceBreakFloorY, + boolean[] surfaceBreakColumn, + double[] columnThreshold, + double[] clampedWeights, + double[] verticalEdgeFade, + MatterCavern[] matterByY, + CaveFluidSupportPlan fluidSupportPlan, + double minWeight, + double thresholdPenalty, + double thresholdBoost, + boolean skipExistingCarved + ) { + int carved = 0; + CaveCarveScratch scratch = scratchCache.get(); + double[] passThreshold = scratch.passThreshold; + int[] activeColumnIndices = scratch.activeColumnIndices; + int[] activeColumnTopY = scratch.activeColumnTopY; + int activeColumnCount = 0; + + for (int index = 0; index < 256; index++) { + double columnWeight = clampedWeights[index]; + if (columnWeight <= minWeight || columnMaxY[index] < minY) { + passThreshold[index] = Double.NaN; + continue; + } + + passThreshold[index] = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty); + activeColumnIndices[activeColumnCount] = index; + activeColumnTopY[activeColumnCount] = columnMaxY[index]; + activeColumnCount++; + } + + if (activeColumnCount == 0) { + return 0; + } + + int[] planeColumnIndices = scratch.planeColumnIndices; + double[] planeThresholdLimit = scratch.planeThresholdLimit; + boolean[] planeCarve = scratch.planeCarve; + int minSection = PowerOfTwoCoordinates.floorDivPow2(minY, 4); + int maxSection = PowerOfTwoCoordinates.floorDivPow2(maxY, 4); + + for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) { + int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex)); + int sectionMaxY = Math.min(maxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15); + MatterSlice cavernSlice = resolveCavernSlice(scratch, chunk, sectionIndex); + + for (int y = sectionMinY; y <= sectionMaxY; y++) { + int planeCount = 0; + for (int activeIndex = 0; activeIndex < activeColumnCount; activeIndex++) { + if (activeColumnTopY[activeIndex] < y) { + continue; + } + + int columnIndex = activeColumnIndices[activeIndex]; + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) { + continue; + } + planeColumnIndices[planeCount] = columnIndex; + double localThreshold = passThreshold[columnIndex]; + if (surfaceBreakColumn[columnIndex] && y >= surfaceBreakFloorY[columnIndex]) { + localThreshold += surfaceBreakThresholdBoost; + } + localThreshold -= verticalEdgeFade[y - minY]; + planeThresholdLimit[planeCount] = localThreshold * normalizationFactor; + planeCount++; + } + + if (planeCount == 0) { + continue; + } + + classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + int fadeIndex = y - minY; + int localY = y & 15; + MatterCavern verticalMatter = matterByY[fadeIndex]; + + if (skipExistingCarved) { + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + if (!planeCarve[planeIndex]) { + continue; + } + + int columnIndex = planeColumnIndices[planeIndex]; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + if (cavernSlice.get(localX, localY, localZ) != null) { + continue; + } + + double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization; + MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ, + columnIndex, fluidMaxY, localThreshold); + writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan); + carved++; + } + continue; + } + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + if (!planeCarve[planeIndex]) { + continue; + } + + int columnIndex = planeColumnIndices[planeIndex]; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization; + MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ, + columnIndex, fluidMaxY, localThreshold); + writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan); + carved++; + } + } + } + + return carved; + } + + private int carvePassAdaptive( + MantleChunk chunk, + int x0, + int z0, + int minY, + int maxY, + int adaptiveSampleStep, + double adaptiveThresholdMargin, + double surfaceBreakThresholdBoost, + int[] columnMaxY, + int[] fluidMaxY, + int[] surfaceFluidBoundaryStartY, + int[] surfaceBreakFloorY, + boolean[] surfaceBreakColumn, + double[] columnThreshold, + double[] clampedWeights, + double[] verticalEdgeFade, + MatterCavern[] matterByY, + CaveFluidSupportPlan fluidSupportPlan, + double minWeight, + double thresholdPenalty, + double thresholdBoost, + boolean skipExistingCarved + ) { + int carved = 0; + CaveCarveScratch scratch = scratchCache.get(); + double[] passThreshold = scratch.passThreshold; + int[] activeColumnIndices = scratch.activeColumnIndices; + int[] activeColumnTopY = scratch.activeColumnTopY; + int activeColumnCount = 0; + + for (int index = 0; index < 256; index++) { + double columnWeight = clampedWeights[index]; + if (columnWeight <= minWeight || columnMaxY[index] < minY) { + passThreshold[index] = Double.NaN; + continue; + } + + passThreshold[index] = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty); + activeColumnIndices[activeColumnCount] = index; + activeColumnTopY[activeColumnCount] = columnMaxY[index]; + activeColumnCount++; + } + + if (activeColumnCount == 0) { + return 0; + } + + int[] planeColumnIndices = scratch.planeColumnIndices; + double[] planeThresholdLimit = scratch.planeThresholdLimit; + boolean[] planeCarve = scratch.planeCarve; + int minSection = PowerOfTwoCoordinates.floorDivPow2(minY, 4); + int maxSection = PowerOfTwoCoordinates.floorDivPow2(maxY, 4); + int effectiveAdaptiveSampleStep = Math.max(adaptiveSampleStep, ADAPTIVE_DEEP_SAMPLE_STEP); + double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin( + adaptiveThresholdMargin, + adaptiveSampleStep, + effectiveAdaptiveSampleStep + ); + + for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) { + int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex)); + int sectionMaxY = Math.min(maxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15); + MatterSlice cavernSlice = resolveCavernSlice(scratch, chunk, sectionIndex); + + for (int y = sectionMinY; y <= sectionMaxY; y++) { + int planeCount = 0; + for (int activeIndex = 0; activeIndex < activeColumnCount; activeIndex++) { + if (activeColumnTopY[activeIndex] < y) { + continue; + } + + int columnIndex = activeColumnIndices[activeIndex]; + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) { + continue; + } + planeColumnIndices[planeCount] = columnIndex; + double localThreshold = passThreshold[columnIndex]; + if (surfaceBreakColumn[columnIndex] && y >= surfaceBreakFloorY[columnIndex]) { + localThreshold += surfaceBreakThresholdBoost; + } + localThreshold -= verticalEdgeFade[y - minY]; + planeThresholdLimit[planeCount] = localThreshold * normalizationFactor; + planeCount++; + } + + if (planeCount == 0) { + continue; + } + + classifyDensityPlaneAdaptive( + scratch, + x0, + z0, + y, + planeColumnIndices, + planeThresholdLimit, + planeCount, + planeCarve, + effectiveAdaptiveSampleStep, + effectiveAdaptiveThresholdMargin + ); + int fadeIndex = y - minY; + int localY = y & 15; + MatterCavern verticalMatter = matterByY[fadeIndex]; + + if (skipExistingCarved) { + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + if (!planeCarve[planeIndex]) { + continue; + } + + int columnIndex = planeColumnIndices[planeIndex]; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + if (cavernSlice.get(localX, localY, localZ) != null) { + continue; + } + + double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization; + MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ, + columnIndex, fluidMaxY, localThreshold); + writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan); + carved++; + } + continue; + } + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + if (!planeCarve[planeIndex]) { + continue; + } + + int columnIndex = planeColumnIndices[planeIndex]; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization; + MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ, + columnIndex, fluidMaxY, localThreshold); + writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan); + carved++; + } + } + } + + return carved; + } + + private double resolveAdaptivePlaneThresholdMargin( + double adaptiveThresholdMargin, + int adaptiveSampleStep, + int effectiveAdaptiveSampleStep + ) { + if (effectiveAdaptiveSampleStep <= adaptiveSampleStep) { + return adaptiveThresholdMargin; + } + + return adaptiveThresholdMargin + ((effectiveAdaptiveSampleStep - adaptiveSampleStep) * ADAPTIVE_DEEP_MARGIN_BOOST); + } + + private int carvePassLattice( + MantleChunk chunk, + int x0, + int z0, + int minY, + int maxY, + int latticeStep, + double surfaceBreakThresholdBoost, + int[] columnMaxY, + int[] fluidMaxY, + int[] surfaceFluidBoundaryStartY, + int[] surfaceBreakFloorY, + boolean[] surfaceBreakColumn, + double[] columnThreshold, + double[] clampedWeights, + double[] verticalEdgeFade, + MatterCavern[] matterByY, + CaveFluidSupportPlan fluidSupportPlan, + double minWeight, + double thresholdPenalty, + double thresholdBoost, + boolean skipExistingCarved + ) { + int carved = 0; + CaveCarveScratch scratch = scratchCache.get(); + double[] passThreshold = scratch.passThreshold; + int[] tileIndices = scratch.tileIndices; + int[] tileLocalX = scratch.tileLocalX; + int[] tileLocalZ = scratch.tileLocalZ; + int[] tileTopY = scratch.tileTopY; + + for (int index = 0; index < 256; index++) { + double columnWeight = clampedWeights[index]; + if (columnWeight <= minWeight || columnMaxY[index] < minY) { + passThreshold[index] = Double.NaN; + continue; + } + passThreshold[index] = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty); + } + + for (int lx = 0; lx < 16; lx += 2) { + int x = x0 + lx; + int lx1 = lx + 1; + for (int lz = 0; lz < 16; lz += 2) { + int z = z0 + lz; + int lz1 = lz + 1; + int activeColumns = 0; + + int index00 = PowerOfTwoCoordinates.packLocal16(lx, lz); + if (!Double.isNaN(passThreshold[index00])) { + tileIndices[activeColumns] = index00; + tileLocalX[activeColumns] = lx; + tileLocalZ[activeColumns] = lz; + tileTopY[activeColumns] = columnMaxY[index00]; + activeColumns++; + } + + int index01 = PowerOfTwoCoordinates.packLocal16(lx, lz1); + if (!Double.isNaN(passThreshold[index01])) { + tileIndices[activeColumns] = index01; + tileLocalX[activeColumns] = lx; + tileLocalZ[activeColumns] = lz1; + tileTopY[activeColumns] = columnMaxY[index01]; + activeColumns++; + } + + int index10 = PowerOfTwoCoordinates.packLocal16(lx1, lz); + if (!Double.isNaN(passThreshold[index10])) { + tileIndices[activeColumns] = index10; + tileLocalX[activeColumns] = lx1; + tileLocalZ[activeColumns] = lz; + tileTopY[activeColumns] = columnMaxY[index10]; + activeColumns++; + } + + int index11 = PowerOfTwoCoordinates.packLocal16(lx1, lz1); + if (!Double.isNaN(passThreshold[index11])) { + tileIndices[activeColumns] = index11; + tileLocalX[activeColumns] = lx1; + tileLocalZ[activeColumns] = lz1; + tileTopY[activeColumns] = columnMaxY[index11]; + activeColumns++; + } + + if (activeColumns == 0) { + continue; + } + + int tileMaxY = minY; + for (int columnIndex = 0; columnIndex < activeColumns; columnIndex++) { + if (tileTopY[columnIndex] > tileMaxY) { + tileMaxY = tileTopY[columnIndex]; + } + } + if (tileMaxY < minY) { + continue; + } + + for (int y = minY; y <= tileMaxY; y += latticeStep) { + double density = sampleDensityOptimized(scratch, x, y, z); + int stampMaxY = Math.min(maxY, y + 1); + for (int yy = y; yy <= stampMaxY; yy++) { + MatterCavern verticalMatter = matterByY[yy - minY]; + MatterSlice cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4)); + int localY = yy & 15; + int fadeIndex = yy - minY; + for (int columnIndex = 0; columnIndex < activeColumns; columnIndex++) { + if (yy > tileTopY[columnIndex]) { + continue; + } + + int index = tileIndices[columnIndex]; + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) { + continue; + } + double localThreshold = passThreshold[index]; + if (surfaceBreakColumn[index] && yy >= surfaceBreakFloorY[index]) { + localThreshold += surfaceBreakThresholdBoost; + } + localThreshold -= verticalEdgeFade[fadeIndex]; + if (density > localThreshold) { + continue; + } + + int localX = tileLocalX[columnIndex]; + int localZ = tileLocalZ[columnIndex]; + int worldX = x0 + localX; + int worldZ = z0 + localZ; + MatterCavern matter = resolveMatter(verticalMatter, worldX, yy, worldZ, + index, fluidMaxY, localThreshold); + if (skipExistingCarved) { + if (cavernSlice.get(localX, localY, localZ) == null) { + writeCavern(cavernSlice, localX, yy, localZ, matter, fluidSupportPlan); + carved++; + } + continue; + } + + writeCavern(cavernSlice, localX, yy, localZ, matter, fluidSupportPlan); + carved++; + } + } + } + } + } + + return carved; + } + + private int carvePassFallback( + MantleChunk chunk, + int x0, + int z0, + int minY, + int maxY, + int sampleStep, + double surfaceBreakThresholdBoost, + int[] columnMaxY, + int[] fluidMaxY, + int[] surfaceFluidBoundaryStartY, + int[] surfaceBreakFloorY, + boolean[] surfaceBreakColumn, + double[] columnThreshold, + double[] clampedWeights, + double[] verticalEdgeFade, + MatterCavern[] matterByY, + CaveFluidSupportPlan fluidSupportPlan, + double minWeight, + double thresholdPenalty, + double thresholdBoost, + boolean skipExistingCarved + ) { + int carved = 0; + CaveCarveScratch scratch = scratchCache.get(); + + for (int lx = 0; lx < 16; lx++) { + int x = x0 + lx; + for (int lz = 0; lz < 16; lz++) { + int z = z0 + lz; + int index = PowerOfTwoCoordinates.packLocal16(lx, lz); + double columnWeight = clampedWeights[index]; + if (columnWeight <= minWeight) { + continue; + } + + int columnTopY = columnMaxY[index]; + if (columnTopY < minY) { + continue; + } + + boolean breakColumn = surfaceBreakColumn[index]; + int breakFloorY = surfaceBreakFloorY[index]; + double threshold = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty); + + for (int y = minY; y <= columnTopY; y += sampleStep) { + double localThreshold = threshold; + if (breakColumn && y >= breakFloorY) { + localThreshold += surfaceBreakThresholdBoost; + } + + localThreshold -= verticalEdgeFade[y - minY]; + if (sampleDensityOptimized(scratch, x, y, z) > localThreshold) { + continue; + } + + int carveMaxY = Math.min(columnTopY, y + sampleStep - 1); + for (int yy = y; yy <= carveMaxY; yy++) { + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) { + continue; + } + MatterCavern verticalMatter = matterByY[yy - minY]; + MatterCavern matter = resolveMatter(verticalMatter, x, yy, z, + index, fluidMaxY, localThreshold); + MatterSlice cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4)); + int localY = yy & 15; + if (skipExistingCarved) { + if (cavernSlice.get(lx, localY, lz) == null) { + writeCavern(cavernSlice, lx, yy, lz, matter, fluidSupportPlan); + carved++; + } + continue; + } + + writeCavern(cavernSlice, lx, yy, lz, matter, fluidSupportPlan); + carved++; + } + } + } + } + + return carved; + } + + private boolean hasFallbackCandidates(int[] columnMaxY, double[] clampedWeights, int minY, double minWeight) { + for (int index = 0; index < 256; index++) { + if (clampedWeights[index] <= minWeight) { + continue; + } + + if (columnMaxY[index] >= minY) { + return true; + } + } + + return false; + } + + private double sampleDensityOptimized(int x, int y, int z) { + if (!hasWarp && !hasModules) { + return sampleDensityNoWarpNoModules(x, y, z); + } + + return sampleDensityOptimized(scratchCache.get(), x, y, z); + } + + private double sampleDensityOptimized(CaveCarveScratch scratch, int x, int y, int z) { + if (!hasWarp) { + if (!hasModules) { + return sampleDensityNoWarpNoModules(x, y, z); + } + + return sampleDensityNoWarpModules(scratch, x, y, z); + } + + if (!hasModules) { + return sampleDensityWarpOnly(scratch, x, y, z); + } + + return sampleDensityWarpModules(scratch, x, y, z); + } + + private void classifyDensityPlane(CaveCarveScratch scratch, int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { + if (!hasWarp) { + if (!hasModules) { + classifyDensityPlaneNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + return; + } + + classifyDensityPlaneNoWarpModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + return; + } + + if (!hasModules) { + classifyDensityPlaneWarpOnly(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + return; + } + + classifyDensityPlaneWarpModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + } + + private void classifyDensityPlaneAdaptive( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin + ) { + if (adaptiveSampleStep <= 1 || planeCount < ADAPTIVE_MIN_PLANE_COLUMNS) { + classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + return; + } + + if (!hasWarp) { + if (!hasModules) { + classifyDensityPlaneAdaptiveNoWarpNoModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + return; + } + + classifyDensityPlaneAdaptiveNoWarpModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + return; + } + + if (!hasModules) { + classifyDensityPlaneAdaptiveWarpOnly(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + return; + } + + classifyDensityPlaneAdaptiveWarpModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + } + + private void classifyDensityPlaneNoWarpNoModules(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int x = x0 + PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int z = z0 + (columnIndex & 15); + planeCarve[planeIndex] = classifyDensityPointNoWarpNoModules(x, y, z, planeThresholdLimit[planeIndex]); + } + } + + private void classifyDensityPlaneNoWarpModules(CaveCarveScratch scratch, int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { + int activeModuleCount = prepareActiveModules(scratch, y); + if (activeModuleCount == 0) { + classifyDensityPlaneNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + return; + } + + CaveFieldModuleState[] localModules = scratch.activeModules; + double[] remainingMin = scratch.activeModuleRemainingMin; + double[] remainingMax = scratch.activeModuleRemainingMax; + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int x = x0 + PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int z = z0 + (columnIndex & 15); + planeCarve[planeIndex] = classifyDensityPointNoWarpModules( + x, + y, + z, + planeThresholdLimit[planeIndex], + localModules, + activeModuleCount, + remainingMin, + remainingMax + ); + } + } + + private void classifyDensityPlaneWarpOnly(CaveCarveScratch scratch, int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int x = x0 + PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int z = z0 + (columnIndex & 15); + planeCarve[planeIndex] = classifyDensityPointWarpOnly(scratch, x, y, z, planeThresholdLimit[planeIndex]); + } + } + + private void classifyDensityPlaneWarpModules(CaveCarveScratch scratch, int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { + int activeModuleCount = prepareActiveModules(scratch, y); + if (activeModuleCount == 0) { + classifyDensityPlaneWarpOnly(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + return; + } + + CaveFieldModuleState[] localModules = scratch.activeModules; + double[] remainingMin = scratch.activeModuleRemainingMin; + double[] remainingMax = scratch.activeModuleRemainingMax; + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int x = x0 + PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int z = z0 + (columnIndex & 15); + planeCarve[planeIndex] = classifyDensityPointWarpModules( + scratch, + x, + y, + z, + planeThresholdLimit[planeIndex], + localModules, + activeModuleCount, + remainingMin, + remainingMax + ); + } + } + + private void classifyDensityPlaneAdaptiveNoWarpNoModules( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin + ) { + double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity; + int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep; + int axisSamples = axisCells + 1; + int[] adaptivePlaneSampleBounds = scratch.adaptivePlaneSampleBounds; + prepareAdaptivePlaneSampleBounds(scratch, planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); + for (int sampleXIndex = adaptivePlaneSampleBounds[0]; sampleXIndex <= adaptivePlaneSampleBounds[1]; sampleXIndex++) { + int sampleLocalX = Math.min(sampleXIndex * adaptiveSampleStep, 16); + int x = x0 + sampleLocalX; + int rowOffset = sampleXIndex * axisSamples; + for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { + int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); + adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityNoWarpNoModules(x, y, z0 + sampleLocalZ); + } + } + + classifyAdaptivePlaneColumnsNoWarpNoModules( + scratch, + x0, + z0, + y, + planeColumnIndices, + planeThresholdLimit, + planeCount, + planeCarve, + adaptiveSampleStep, + adaptiveThresholdMargin, + adaptivePlaneDensity, + axisCells, + axisSamples + ); + } + + private void classifyDensityPlaneAdaptiveNoWarpModules( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin + ) { + int activeModuleCount = prepareActiveModules(scratch, y); + if (activeModuleCount == 0) { + classifyDensityPlaneAdaptiveNoWarpNoModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + return; + } + + CaveFieldModuleState[] localModules = scratch.activeModules; + double[] remainingMin = scratch.activeModuleRemainingMin; + double[] remainingMax = scratch.activeModuleRemainingMax; + double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity; + int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep; + int axisSamples = axisCells + 1; + int[] adaptivePlaneSampleBounds = scratch.adaptivePlaneSampleBounds; + prepareAdaptivePlaneSampleBounds(scratch, planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); + for (int sampleXIndex = adaptivePlaneSampleBounds[0]; sampleXIndex <= adaptivePlaneSampleBounds[1]; sampleXIndex++) { + int sampleLocalX = Math.min(sampleXIndex * adaptiveSampleStep, 16); + int x = x0 + sampleLocalX; + int rowOffset = sampleXIndex * axisSamples; + for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { + int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); + adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityNoWarpNoModules(x, y, z0 + sampleLocalZ); + } + } + + classifyAdaptivePlaneColumnsNoWarpModules( + scratch, + x0, + z0, + y, + planeColumnIndices, + planeThresholdLimit, + planeCount, + planeCarve, + adaptiveSampleStep, + adaptiveThresholdMargin, + adaptivePlaneDensity, + axisCells, + axisSamples, + localModules, + activeModuleCount, + remainingMin, + remainingMax + ); + } + + private void classifyDensityPlaneAdaptiveWarpOnly( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin + ) { + double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity; + int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep; + int axisSamples = axisCells + 1; + int[] adaptivePlaneSampleBounds = scratch.adaptivePlaneSampleBounds; + prepareAdaptivePlaneSampleBounds(scratch, planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); + for (int sampleXIndex = adaptivePlaneSampleBounds[0]; sampleXIndex <= adaptivePlaneSampleBounds[1]; sampleXIndex++) { + int sampleLocalX = Math.min(sampleXIndex * adaptiveSampleStep, 16); + int x = x0 + sampleLocalX; + int rowOffset = sampleXIndex * axisSamples; + for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { + int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); + adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpOnly(scratch, x, y, z0 + sampleLocalZ); + } + } + + classifyAdaptivePlaneColumnsWarpOnly( + scratch, + x0, + z0, + y, + planeColumnIndices, + planeThresholdLimit, + planeCount, + planeCarve, + adaptiveSampleStep, + adaptiveThresholdMargin, + adaptivePlaneDensity, + axisCells, + axisSamples + ); + } + + private void classifyDensityPlaneAdaptiveWarpModules( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin + ) { + int activeModuleCount = prepareActiveModules(scratch, y); + if (activeModuleCount == 0) { + classifyDensityPlaneAdaptiveWarpOnly(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + return; + } + + CaveFieldModuleState[] localModules = scratch.activeModules; + double[] remainingMin = scratch.activeModuleRemainingMin; + double[] remainingMax = scratch.activeModuleRemainingMax; + double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity; + int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep; + int axisSamples = axisCells + 1; + int[] adaptivePlaneSampleBounds = scratch.adaptivePlaneSampleBounds; + prepareAdaptivePlaneSampleBounds(scratch, planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); + for (int sampleXIndex = adaptivePlaneSampleBounds[0]; sampleXIndex <= adaptivePlaneSampleBounds[1]; sampleXIndex++) { + int sampleLocalX = Math.min(sampleXIndex * adaptiveSampleStep, 16); + int x = x0 + sampleLocalX; + int rowOffset = sampleXIndex * axisSamples; + for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { + int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); + adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpModules( + scratch, + x, + y, + z0 + sampleLocalZ, + localModules, + activeModuleCount + ); + } + } + + classifyAdaptivePlaneColumnsWarpModulesSampled( + scratch, + x0, + z0, + y, + planeColumnIndices, + planeThresholdLimit, + planeCount, + planeCarve, + adaptiveSampleStep, + adaptiveThresholdMargin, + adaptivePlaneDensity, + axisCells, + axisSamples, + localModules, + activeModuleCount, + remainingMin, + remainingMax + ); + } + + private void classifyAdaptivePlaneColumnsNoWarpNoModules( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin, + double[] adaptivePlaneDensity, + int axisCells, + int axisSamples + ) { + double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; + double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; + prepareAdaptivePlaneColumns( + scratch, + planeColumnIndices, + planeCount, + adaptiveSampleStep, + adaptiveThresholdMargin, + adaptivePlaneDensity, + axisCells, + axisSamples, + adaptivePlanePrediction, + adaptivePlaneAmbiguity + ); + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + double threshold = planeThresholdLimit[planeIndex] * inverseNormalization; + double predictedDensity = adaptivePlanePrediction[planeIndex]; + double ambiguityMargin = adaptivePlaneAmbiguity[planeIndex]; + if (isAdaptivePlaneSampleAligned(localX, localZ, adaptiveSampleStep)) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } + if (predictedDensity <= threshold - ambiguityMargin) { + planeCarve[planeIndex] = true; + continue; + } + if (predictedDensity > threshold + ambiguityMargin) { + planeCarve[planeIndex] = false; + continue; + } + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } + + planeCarve[planeIndex] = classifyDensityPointNoWarpNoModules(x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]); + } + } + + private void classifyAdaptivePlaneColumnsNoWarpModules( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin, + double[] adaptivePlaneDensity, + int axisCells, + int axisSamples, + CaveFieldModuleState[] localModules, + int activeModuleCount, + double[] remainingMin, + double[] remainingMax + ) { + double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; + double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; + prepareAdaptivePlaneColumns( + scratch, + planeColumnIndices, + planeCount, + adaptiveSampleStep, + adaptiveThresholdMargin, + adaptivePlaneDensity, + axisCells, + axisSamples, + adaptivePlanePrediction, + adaptivePlaneAmbiguity + ); + double minRemaining = remainingMin[0] * inverseNormalization; + double maxRemaining = remainingMax[0] * inverseNormalization; + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + double threshold = planeThresholdLimit[planeIndex] * inverseNormalization; + double predictedDensity = adaptivePlanePrediction[planeIndex]; + double ambiguityMargin = adaptivePlaneAmbiguity[planeIndex]; + if (isAdaptivePlaneSampleAligned(localX, localZ, adaptiveSampleStep)) { + planeCarve[planeIndex] = classifyDensityPointNoWarpModulesFromExactDensity( + x0 + localX, + y, + z0 + localZ, + threshold, + predictedDensity, + localModules, + activeModuleCount, + remainingMin, + remainingMax + ); + continue; + } + if ((predictedDensity + maxRemaining) <= threshold - ambiguityMargin) { + planeCarve[planeIndex] = true; + continue; + } + if ((predictedDensity + minRemaining) > threshold + ambiguityMargin) { + planeCarve[planeIndex] = false; + continue; + } + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } + + planeCarve[planeIndex] = classifyDensityPointNoWarpModules( + x0 + localX, + y, + z0 + localZ, + planeThresholdLimit[planeIndex], + localModules, + activeModuleCount, + remainingMin, + remainingMax + ); + } + } + + private void classifyAdaptivePlaneColumnsWarpOnly( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin, + double[] adaptivePlaneDensity, + int axisCells, + int axisSamples + ) { + double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; + double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; + prepareAdaptivePlaneColumns( + scratch, + planeColumnIndices, + planeCount, + adaptiveSampleStep, + adaptiveThresholdMargin, + adaptivePlaneDensity, + axisCells, + axisSamples, + adaptivePlanePrediction, + adaptivePlaneAmbiguity + ); + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + double threshold = planeThresholdLimit[planeIndex] * inverseNormalization; + double predictedDensity = adaptivePlanePrediction[planeIndex]; + double ambiguityMargin = adaptivePlaneAmbiguity[planeIndex]; + if (isAdaptivePlaneSampleAligned(localX, localZ, adaptiveSampleStep)) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } + if (predictedDensity <= threshold - ambiguityMargin) { + planeCarve[planeIndex] = true; + continue; + } + if (predictedDensity > threshold + ambiguityMargin) { + planeCarve[planeIndex] = false; + continue; + } + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } + + planeCarve[planeIndex] = classifyDensityPointWarpOnly(scratch, x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]); + } + } + + private void classifyAdaptivePlaneColumnsWarpModules( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin, + double[] adaptivePlaneDensity, + int axisCells, + int axisSamples, + CaveFieldModuleState[] localModules, + int activeModuleCount, + double[] remainingMin, + double[] remainingMax + ) { + double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; + double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; + prepareAdaptivePlaneColumns( + scratch, + planeColumnIndices, + planeCount, + adaptiveSampleStep, + adaptiveThresholdMargin, + adaptivePlaneDensity, + axisCells, + axisSamples, + adaptivePlanePrediction, + adaptivePlaneAmbiguity + ); + double minRemaining = remainingMin[0] * inverseNormalization; + double maxRemaining = remainingMax[0] * inverseNormalization; + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + double threshold = planeThresholdLimit[planeIndex] * inverseNormalization; + double predictedDensity = adaptivePlanePrediction[planeIndex]; + double ambiguityMargin = adaptivePlaneAmbiguity[planeIndex]; + if (isAdaptivePlaneSampleAligned(localX, localZ, adaptiveSampleStep)) { + planeCarve[planeIndex] = classifyDensityPointWarpModulesFromExactDensity( + scratch, + x0 + localX, + y, + z0 + localZ, + threshold, + predictedDensity, + localModules, + activeModuleCount, + remainingMin, + remainingMax + ); + continue; + } + if ((predictedDensity + maxRemaining) <= threshold - ambiguityMargin) { + planeCarve[planeIndex] = true; + continue; + } + if ((predictedDensity + minRemaining) > threshold + ambiguityMargin) { + planeCarve[planeIndex] = false; + continue; + } + + planeCarve[planeIndex] = classifyDensityPointWarpModules( + scratch, + x0 + localX, + y, + z0 + localZ, + planeThresholdLimit[planeIndex], + localModules, + activeModuleCount, + remainingMin, + remainingMax + ); + } + } + + private void classifyAdaptivePlaneColumnsWarpModulesSampled( + CaveCarveScratch scratch, + int x0, + int z0, + int y, + int[] planeColumnIndices, + double[] planeThresholdLimit, + int planeCount, + boolean[] planeCarve, + int adaptiveSampleStep, + double adaptiveThresholdMargin, + double[] adaptivePlaneDensity, + int axisCells, + int axisSamples, + CaveFieldModuleState[] localModules, + int activeModuleCount, + double[] remainingMin, + double[] remainingMax + ) { + double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; + double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; + prepareAdaptivePlaneColumns( + scratch, + planeColumnIndices, + planeCount, + adaptiveSampleStep, + adaptiveThresholdMargin, + adaptivePlaneDensity, + axisCells, + axisSamples, + adaptivePlanePrediction, + adaptivePlaneAmbiguity + ); + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + double threshold = planeThresholdLimit[planeIndex] * inverseNormalization; + double predictedDensity = adaptivePlanePrediction[planeIndex]; + double ambiguityMargin = adaptivePlaneAmbiguity[planeIndex]; + if (isAdaptivePlaneSampleAligned(localX, localZ, adaptiveSampleStep)) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } + if (predictedDensity <= threshold - ambiguityMargin) { + planeCarve[planeIndex] = true; + continue; + } + if (predictedDensity > threshold + ambiguityMargin) { + planeCarve[planeIndex] = false; + continue; + } + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } + + planeCarve[planeIndex] = classifyDensityPointWarpModules( + scratch, + x0 + localX, + y, + z0 + localZ, + planeThresholdLimit[planeIndex], + localModules, + activeModuleCount, + remainingMin, + remainingMax + ); + } + } + + private boolean classifyDensityPointNoWarpNoModules(int x, int y, int z, double thresholdLimit) { + double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight; + if ((density + detailMinContribution) > thresholdLimit) { + return false; + } + if ((density + detailMaxContribution) <= thresholdLimit) { + return true; + } + + density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight; + return density <= thresholdLimit; + } + + private boolean classifyDensityPointNoWarpModules( + int x, + int y, + int z, + double thresholdLimit, + CaveFieldModuleState[] localModules, + int activeModuleCount, + double[] remainingMin, + double[] remainingMax + ) { + if (activeModuleCount == 0) { + return classifyDensityPointNoWarpNoModules(x, y, z, thresholdLimit); + } + + double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight; + if ((density + detailMinContribution + remainingMin[0]) > thresholdLimit) { + return false; + } + if ((density + detailMaxContribution + remainingMax[0]) <= thresholdLimit) { + return true; + } + + density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight; + if ((density + remainingMin[0]) > thresholdLimit) { + return false; + } + if ((density + remainingMax[0]) <= thresholdLimit) { + return true; + } + + for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) { + density += localModules[moduleIndex].sample(x, y, z); + if ((density + remainingMin[moduleIndex + 1]) > thresholdLimit) { + return false; + } + if ((density + remainingMax[moduleIndex + 1]) <= thresholdLimit) { + return true; + } + } + + return density <= thresholdLimit; + } + + private int snapWarp(int c) { + int g = warpResolution; + if (g <= 1) { + return c; + } + if ((g & (g - 1)) == 0) { + return c & -g; + } + return Math.floorDiv(c, g) * g; + } + + private boolean classifyDensityPointWarpOnly(CaveCarveScratch scratch, int x, int y, int z, double thresholdLimit) { + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; + double warpedX = x + (warpA * warpStrength); + double warpedY = y + (warpB * warpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); + double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight; + if ((density + detailMinContribution) > thresholdLimit) { + return false; + } + if ((density + detailMaxContribution) <= thresholdLimit) { + return true; + } + + density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight; + return density <= thresholdLimit; + } + + private boolean classifyDensityPointWarpModules( + CaveCarveScratch scratch, + int x, + int y, + int z, + double thresholdLimit, + CaveFieldModuleState[] localModules, + int activeModuleCount, + double[] remainingMin, + double[] remainingMax + ) { + if (activeModuleCount == 0) { + return classifyDensityPointWarpOnly(scratch, x, y, z, thresholdLimit); + } + + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; + double warpedX = x + (warpA * warpStrength); + double warpedY = y + (warpB * warpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); + double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight; + if ((density + detailMinContribution + remainingMin[0]) > thresholdLimit) { + return false; + } + if ((density + detailMaxContribution + remainingMax[0]) <= thresholdLimit) { + return true; + } + + density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight; + if ((density + remainingMin[0]) > thresholdLimit) { + return false; + } + if ((density + remainingMax[0]) <= thresholdLimit) { + return true; + } + + for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) { + density += localModules[moduleIndex].sample(warpedX, warpedY, warpedZ); + if ((density + remainingMin[moduleIndex + 1]) > thresholdLimit) { + return false; + } + if ((density + remainingMax[moduleIndex + 1]) <= thresholdLimit) { + return true; + } + } + + return density <= thresholdLimit; + } + + private boolean classifyDensityPointNoWarpModulesFromExactDensity( + int x, + int y, + int z, + double threshold, + double density, + CaveFieldModuleState[] localModules, + int activeModuleCount, + double[] remainingMin, + double[] remainingMax + ) { + if (activeModuleCount == 0) { + return density <= threshold; + } + + double minRemaining = remainingMin[0] * inverseNormalization; + double maxRemaining = remainingMax[0] * inverseNormalization; + if ((density + minRemaining) > threshold) { + return false; + } + if ((density + maxRemaining) <= threshold) { + return true; + } + + for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) { + density += localModules[moduleIndex].sample(x, y, z) * inverseNormalization; + if ((density + (remainingMin[moduleIndex + 1] * inverseNormalization)) > threshold) { + return false; + } + if ((density + (remainingMax[moduleIndex + 1] * inverseNormalization)) <= threshold) { + return true; + } + } + + return density <= threshold; + } + + private boolean classifyDensityPointWarpModulesFromExactDensity( + CaveCarveScratch scratch, + int x, + int y, + int z, + double threshold, + double density, + CaveFieldModuleState[] localModules, + int activeModuleCount, + double[] remainingMin, + double[] remainingMax + ) { + if (activeModuleCount == 0) { + return density <= threshold; + } + + double minRemaining = remainingMin[0] * inverseNormalization; + double maxRemaining = remainingMax[0] * inverseNormalization; + if ((density + minRemaining) > threshold) { + return false; + } + if ((density + maxRemaining) <= threshold) { + return true; + } + + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; + double warpedX = x + (warpA * warpStrength); + double warpedY = y + (warpB * warpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); + for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) { + density += localModules[moduleIndex].sample(warpedX, warpedY, warpedZ) * inverseNormalization; + if ((density + (remainingMin[moduleIndex + 1] * inverseNormalization)) > threshold) { + return false; + } + if ((density + (remainingMax[moduleIndex + 1] * inverseNormalization)) <= threshold) { + return true; + } + } + + return density <= threshold; + } + + private boolean isAdaptivePlaneSampleAligned(int localX, int localZ, int adaptiveSampleStep) { + if ((adaptiveSampleStep & (adaptiveSampleStep - 1)) == 0) { + int mask = adaptiveSampleStep - 1; + return (localX & mask) == 0 && (localZ & mask) == 0; + } + return localX % adaptiveSampleStep == 0 && localZ % adaptiveSampleStep == 0; + } + + private void prepareAdaptivePlaneSampleBounds( + CaveCarveScratch scratch, + int[] planeColumnIndices, + int planeCount, + int adaptiveSampleStep, + int[] adaptivePlaneSampleBounds, + int axisCells + ) { + prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisCells + 1); + int[] adaptiveCellX = scratch.adaptiveCellX; + int[] adaptiveCellZ = scratch.adaptiveCellZ; + int minSampleX = axisCells; + int maxSampleX = 0; + int minSampleZ = axisCells; + int maxSampleZ = 0; + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int sampleX = adaptiveCellX[columnIndex]; + int sampleZ = adaptiveCellZ[columnIndex]; + if (sampleX < minSampleX) { + minSampleX = sampleX; + } + if (sampleX + 1 > maxSampleX) { + maxSampleX = sampleX + 1; + } + if (sampleZ < minSampleZ) { + minSampleZ = sampleZ; + } + if (sampleZ + 1 > maxSampleZ) { + maxSampleZ = sampleZ + 1; + } + } + + adaptivePlaneSampleBounds[0] = minSampleX; + adaptivePlaneSampleBounds[1] = maxSampleX; + adaptivePlaneSampleBounds[2] = minSampleZ; + adaptivePlaneSampleBounds[3] = maxSampleZ; + } + + private void prepareAdaptivePlaneColumns( + CaveCarveScratch scratch, + int[] planeColumnIndices, + int planeCount, + int adaptiveSampleStep, + double adaptiveThresholdMargin, + double[] adaptivePlaneDensity, + int axisCells, + int axisSamples, + double[] adaptivePlanePrediction, + double[] adaptivePlaneAmbiguity + ) { + prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisSamples); + int[] adaptiveCellZ = scratch.adaptiveCellZ; + int[] adaptiveRow0 = scratch.adaptiveRow0; + int[] adaptiveRow1 = scratch.adaptiveRow1; + double[] adaptiveTx = scratch.adaptiveTx; + double[] adaptiveTz = scratch.adaptiveTz; + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int cellZ = adaptiveCellZ[columnIndex]; + int row0 = adaptiveRow0[columnIndex]; + int row1 = adaptiveRow1[columnIndex]; + double tx = adaptiveTx[columnIndex]; + double tz = adaptiveTz[columnIndex]; + double d00 = adaptivePlaneDensity[row0 + cellZ]; + double d01 = adaptivePlaneDensity[row0 + cellZ + 1]; + double d10 = adaptivePlaneDensity[row1 + cellZ]; + double d11 = adaptivePlaneDensity[row1 + cellZ + 1]; + double dx0 = d00 + ((d10 - d00) * tx); + double dx1 = d01 + ((d11 - d01) * tx); + adaptivePlanePrediction[planeIndex] = dx0 + ((dx1 - dx0) * tz); + double minDensity = Math.min(Math.min(d00, d01), Math.min(d10, d11)); + double maxDensity = Math.max(Math.max(d00, d01), Math.max(d10, d11)); + adaptivePlaneAmbiguity[planeIndex] = adaptiveThresholdMargin + ((maxDensity - minDensity) * ADAPTIVE_LOCAL_RANGE_SCALE); + } + } + + private void prepareAdaptiveGeometry(CaveCarveScratch scratch, int adaptiveSampleStep, int axisCells, int axisSamples) { + if (scratch.adaptiveGeometryStep == adaptiveSampleStep && scratch.adaptiveGeometryAxisCells == axisCells) { + return; + } + + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + int localX = columnIndex >> 4; + int localZ = columnIndex & 15; + int cellX = Math.min(localX / adaptiveSampleStep, axisCells - 1); + int cellZ = Math.min(localZ / adaptiveSampleStep, axisCells - 1); + int x0 = cellX * adaptiveSampleStep; + int z0 = cellZ * adaptiveSampleStep; + int x1 = Math.min(x0 + adaptiveSampleStep, 16); + int z1 = Math.min(z0 + adaptiveSampleStep, 16); + scratch.adaptiveCellX[columnIndex] = cellX; + scratch.adaptiveCellZ[columnIndex] = cellZ; + scratch.adaptiveRow0[columnIndex] = cellX * axisSamples; + scratch.adaptiveRow1[columnIndex] = (cellX + 1) * axisSamples; + scratch.adaptiveTx[columnIndex] = x1 == x0 ? 0D : (localX - x0) / (double) (x1 - x0); + scratch.adaptiveTz[columnIndex] = z1 == z0 ? 0D : (localZ - z0) / (double) (z1 - z0); + } + + scratch.adaptiveGeometryStep = adaptiveSampleStep; + scratch.adaptiveGeometryAxisCells = axisCells; + } + + private double sampleDensityNoWarpNoModules(int x, int y, int z) { + double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight; + density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight; + return density * inverseNormalization; + } + + private double sampleDensityNoWarpModules(CaveCarveScratch scratch, int x, int y, int z) { + int activeModuleCount = prepareActiveModules(scratch, y); + if (activeModuleCount == 0) { + return sampleDensityNoWarpNoModules(x, y, z); + } + + CaveFieldModuleState[] localModules = scratch.activeModules; + double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight; + density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight; + for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) { + CaveFieldModuleState module = localModules[moduleIndex]; + double moduleDensity = module.density.noiseFastSigned3D(x, y, z) - module.threshold; + if (module.invert) { + moduleDensity = -moduleDensity; + } + + density += moduleDensity * module.weight; + } + + return density * inverseNormalization; + } + + private double sampleDensityWarpOnly(CaveCarveScratch scratch, int x, int y, int z) { + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; + double warpedX = x + (warpA * warpStrength); + double warpedY = y + (warpB * warpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); + double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight; + density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight; + return density * inverseNormalization; + } + + private double sampleDensityWarpModules(CaveCarveScratch scratch, int x, int y, int z) { + int activeModuleCount = prepareActiveModules(scratch, y); + if (activeModuleCount == 0) { + return sampleDensityWarpOnly(scratch, x, y, z); + } + + CaveFieldModuleState[] localModules = scratch.activeModules; + return sampleDensityWarpModules(scratch, x, y, z, localModules, activeModuleCount); + } + + private double sampleDensityWarpModules(CaveCarveScratch scratch, int x, int y, int z, CaveFieldModuleState[] localModules, int activeModuleCount) { + int sx = snapWarp(x); + int sy = snapWarp(y); + int sz = snapWarp(z); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; + double warpedX = x + (warpA * warpStrength); + double warpedY = y + (warpB * warpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); + double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight; + density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight; + for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) { + CaveFieldModuleState module = localModules[moduleIndex]; + double moduleDensity = module.density.noiseFastSigned3D(warpedX, warpedY, warpedZ) - module.threshold; + if (module.invert) { + moduleDensity = -moduleDensity; + } + + density += moduleDensity * module.weight; + } + + return density * inverseNormalization; + } + + private int prepareWarpSample(CaveCarveScratch scratch, int sx, int sy, int sz) { + int slot = mixWarpKey(sx, sy, sz) & (scratch.warpCacheX.length - 1); + if (scratch.warpCacheSet[slot] + && scratch.warpCacheX[slot] == sx + && scratch.warpCacheY[slot] == sy + && scratch.warpCacheZ[slot] == sz) { + return slot; + } + + scratch.warpCacheSet[slot] = true; + scratch.warpCacheX[slot] = sx; + scratch.warpCacheY[slot] = sy; + scratch.warpCacheZ[slot] = sz; + scratch.warpCacheA[slot] = warpDensity.noiseFastSigned3D(sx, sy, sz); + scratch.warpCacheB[slot] = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); + return slot; + } + + private static int mixWarpKey(int sx, int sy, int sz) { + int hash = sx * 73428767; + hash ^= sy * 91227153; + hash ^= sz * 43828939; + hash ^= hash >>> 16; + return hash; + } + + private int prepareActiveModules(CaveCarveScratch scratch, int y) { + CaveFieldModuleState[] configuredModules = modules; + int configuredCount = configuredModules.length; + if (configuredCount == 0) { + return 0; + } + + // Pure function of (y, modules). modules is final and the scratch is per carver per thread, + // so the band selection and the remaining-contribution prefix sums only change when y does. + if (scratch.activeModulesY == y) { + return scratch.activeModuleCount; + } + + if (scratch.activeModules.length < configuredCount) { + scratch.activeModules = new CaveFieldModuleState[configuredCount]; + } + + int activeCount = 0; + for (int moduleIndex = 0; moduleIndex < configuredCount; moduleIndex++) { + CaveFieldModuleState module = configuredModules[moduleIndex]; + if (y < module.minY || y > module.maxY) { + continue; + } + + scratch.activeModules[activeCount] = module; + activeCount++; + } + + if (scratch.activeModuleRemainingMin.length < activeCount + 1) { + scratch.activeModuleRemainingMin = new double[activeCount + 1]; + scratch.activeModuleRemainingMax = new double[activeCount + 1]; + } + + scratch.activeModuleRemainingMin[activeCount] = 0D; + scratch.activeModuleRemainingMax[activeCount] = 0D; + for (int moduleIndex = activeCount - 1; moduleIndex >= 0; moduleIndex--) { + CaveFieldModuleState module = scratch.activeModules[moduleIndex]; + scratch.activeModuleRemainingMin[moduleIndex] = scratch.activeModuleRemainingMin[moduleIndex + 1] + module.minContribution; + scratch.activeModuleRemainingMax[moduleIndex] = scratch.activeModuleRemainingMax[moduleIndex + 1] + module.maxContribution; + } + + scratch.activeModulesY = y; + scratch.activeModuleCount = activeCount; + return activeCount; + } + + private MatterSlice resolveCavernSlice(CaveCarveScratch scratch, MantleChunk chunk, int sectionIndex) { + @SuppressWarnings("unchecked") + MatterSlice cachedSlice = (MatterSlice) scratch.sectionSlices[sectionIndex]; + if (cachedSlice != null) { + return cachedSlice; + } + + Matter sectionMatter = scratch.sectionMatter[sectionIndex]; + if (sectionMatter == null) { + sectionMatter = chunk.getOrCreate(sectionIndex); + scratch.sectionMatter[sectionIndex] = sectionMatter; + } + + MatterSlice resolvedSlice = sectionMatter.slice(MatterCavern.class); + scratch.sectionSlices[sectionIndex] = resolvedSlice; + return resolvedSlice; + } + + private MatterCavern[] prepareMatterByYTable(CaveCarveScratch scratch, int minY, int maxY) { + int size = Math.max(0, maxY - minY + 1); + if (scratch.matterByY.length < size) { + scratch.matterByY = new MatterCavern[size]; + } + + MatterCavern[] matterByY = scratch.matterByY; + boolean allowLava = profile.isAllowLava(); + int lavaHeight = engine.getDimension().getCaveLavaHeight(); + + for (int y = minY; y <= maxY; y++) { + int offset = y - minY; + if (allowLava && y <= lavaHeight) { + matterByY[offset] = carveLava; + continue; + } + if (!allowLava && y <= lavaHeight) { + matterByY[offset] = carveForcedAir; + continue; + } + + matterByY[offset] = carveAir; + } + + return matterByY; + } + + private MatterCavern resolveMatter(MatterCavern verticalMatter, int x, int y, int z, + int columnIndex, int[] fluidMaxY, double localThreshold) { + if (verticalMatter != carveLava + && y <= fluidMaxY[columnIndex] + && isAquiferCandidate(x, y, z, localThreshold)) { + return carveFluid; + } + return verticalMatter; + } + + private boolean isAquiferCandidate(int x, int y, int z, double localThreshold) { + double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D)); + double cutoff = 0.35D + (depthFactor * 0.2D); + if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) { + return false; + } + return !fluidRequiresFloor || hasAquiferCupSupport(x, y, z, localThreshold); + } + + private boolean hasAquiferCupSupport(int x, int y, int z, double threshold) { + int floorY = Math.max(0, y - 1); + int deepFloorY = Math.max(0, y - 2); + int aboveY = Math.min(aquiferCeilingY, y + 1); + if (!isDensitySolid(x, floorY, z, threshold)) { + return false; + } + if (!isDensitySolid(x, deepFloorY, z, threshold - 0.05D)) { + return false; + } + + int support = 0; + if (isDensitySolid(x + 1, y, z, threshold)) { + support++; + } + if (isDensitySolid(x - 1, y, z, threshold)) { + support++; + } + if (isDensitySolid(x, y, z + 1, threshold)) { + support++; + } + if (isDensitySolid(x, y, z - 1, threshold)) { + support++; + } + if (isDensitySolid(x, aboveY, z, threshold)) { + support++; + } + return support >= 4; + } + + private boolean isDensitySolid(int x, int y, int z, double threshold) { + return sampleDensityOptimized(x, y, z) > threshold; + } + + private void writeCavern(MatterSlice cavernSlice, int localX, int y, int localZ, + MatterCavern matter, CaveFluidSupportPlan fluidSupportPlan) { + cavernSlice.set(localX, y & 15, localZ, matter); + if (fluidSupportPlan != null && matter == carveFluid) { + fluidSupportPlan.add(localX, y, localZ, carveFluid, carveAir); + } + } + + private void prepareSectionCaches(CaveCarveScratch scratch, int minY, int maxY) { + int minSection = Math.max(0, PowerOfTwoCoordinates.floorDivPow2(minY, 4)); + int maxSection = Math.max(minSection, PowerOfTwoCoordinates.floorDivPow2(maxY, 4)); + int requiredSections = maxSection + 1; + if (scratch.sectionMatter.length < requiredSections) { + scratch.sectionMatter = new Matter[requiredSections]; + scratch.sectionSlices = new MatterSlice[requiredSections]; + return; + } + + for (int section = minSection; section <= maxSection; section++) { + scratch.sectionMatter[section] = null; + scratch.sectionSlices[section] = null; + } + } + + private double clampColumnWeight(double weight) { + if (Double.isNaN(weight) || Double.isInfinite(weight)) { + return 0D; + } + + if (weight <= 0D) { + return 0D; + } + + if (weight >= 1D) { + return 1D; + } + + return weight; + } + + private double signed(double value) { + return (value * 2D) - 1D; + } + + private double[] prepareVerticalEdgeFadeTable(CaveCarveScratch scratch, int minY, int maxY) { + int size = Math.max(0, maxY - minY + 1); + if (scratch.verticalEdgeFade.length < size) { + scratch.verticalEdgeFade = new double[size]; + } + + double[] verticalEdgeFade = scratch.verticalEdgeFade; + int fadeRange = Math.max(0, profile.getVerticalEdgeFade()); + double fadeStrength = Math.max(0D, profile.getVerticalEdgeFadeStrength()); + if (size <= 0 || fadeRange <= 0 || maxY <= minY || fadeStrength <= 0D) { + Arrays.fill(verticalEdgeFade, 0, size, 0D); + return verticalEdgeFade; + } + + for (int y = minY; y <= maxY; y++) { + int floorDistance = y - minY; + int ceilingDistance = maxY - y; + int edgeDistance = Math.min(floorDistance, ceilingDistance); + int offsetIndex = y - minY; + if (edgeDistance >= fadeRange) { + verticalEdgeFade[offsetIndex] = 0D; + continue; + } + + double t = Math.max(0D, Math.min(1D, edgeDistance / (double) fadeRange)); + double smooth = t * t * (3D - (2D * t)); + verticalEdgeFade[offsetIndex] = (1D - smooth) * fadeStrength; + } + + return verticalEdgeFade; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java new file mode 100644 index 000000000..dd76a03c0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java @@ -0,0 +1,678 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.IrisStructureLocator; +import art.arcane.iris.engine.framework.PlacedStructurePiece; +import art.arcane.iris.engine.framework.StructurePlacementScope; +import art.arcane.iris.engine.framework.StructurePlacementMarker; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.engine.mantle.ComponentFlag; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.mantle.IrisMantleComponent; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisStructure; +import art.arcane.iris.engine.object.IrisStructureCarveShape; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.engine.object.IrisStructureStiltSettings; +import art.arcane.iris.engine.object.IrisStructureTerrain; +import art.arcane.iris.engine.object.IrisStructureTerrainMode; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.project.matter.TileWrapper; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterStructurePOI; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.math.RNG; +import it.unimi.dsi.fastutil.longs.Long2IntMap; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@ComponentFlag(ReservedFlag.JIGSAW) +public class IrisStructureComponent extends IrisMantleComponent { + private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3); + + public IrisStructureComponent(EngineMantle engineMantle) { + super(engineMantle, ReservedFlag.JIGSAW, 3); + } + + @Override + @ChunkCoordinates + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt( + getEngineMantle().getEngine(), x, z)) { + if (!placement.hasIrisStructures()) { + continue; + } + placeFromPlacement(writer, placement, x, z); + } + } + + @ChunkCoordinates + private void placeFromPlacement(MantleWriter writer, IrisStructurePlacement placement, int cx, int cz) { + IrisStructureLocator.ResolvedPlacement resolved = IrisStructureLocator.resolvePlacement( + getEngineMantle().getEngine(), placement, cx, cz); + if (resolved == null) { + return; + } + + boolean trace = IrisSettings.get().getGeneral().isDebug(); + if (trace) { + IrisLogging.info("[StructTrace] ORIGIN chunk=" + cx + "," + cz + " structures=" + placement.getStructures() + + " anchor=" + placement.resolvedAnchor() + " band=" + placement.getMinHeight() + ".." + placement.getMaxHeight()); + } + + String key = resolved.structureKey(); + IrisStructure structure = resolved.structure(); + KList pieces = resolvedPiecesOrNull(resolved, cx, cz); + if (pieces == null) { + return; + } + RNG rng = resolved.rng(); + int baseY = resolved.baseY(); + if (trace) { + IrisLogging.info("[StructTrace] ASSEMBLED chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY + " pieces=" + pieces.size()); + } + + if (!placement.isAnchoredUnderground()) { + clearIntersectingObjectTrees(writer, resolved); + } + + IrisStructureTerrain terrain = placement.resolvedTerrain(); + IrisStructureTerrainMode terrainMode = terrain.resolvedMode(); + if (terrainMode == IrisStructureTerrainMode.FORCE_CARVE) { + forceCarveStructure(writer, pieces, terrain); + } else if (terrainMode == IrisStructureTerrainMode.BORE) { + boreStructure(writer, pieces, terrain); + } else if (terrainMode != IrisStructureTerrainMode.PRESERVE + && terrainMode != IrisStructureTerrainMode.SOURCE) { + throw new IllegalStateException("Iris assembly terrain mode " + terrainMode + + " is not implemented for structure '" + key + "'"); + } + + ObjectPlaceMode mode = structure.getPlaceMode(); + int failedPieces = 0; + IrisStructureStiltSettings stilt = placement.getStilt(); + Long2IntOpenHashMap foundationColumns = stilt == null + ? null : new Long2IntOpenHashMap(); + boolean supportNonOccluding = stilt != null && stilt.isSupportNonOccluding(); + if (placement.isAnchoredUnderground()) { + ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG) + ? mode : ObjectPlaceMode.STRUCTURE_PIECE; + for (PlacedStructurePiece p : pieces) { + if (placeObject(writer, structure, p, undergroundMode, p.getY(), rng, + foundationColumns, supportNonOccluding) == -1) { + failedPieces++; + } + } + } else if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) { + for (PlacedStructurePiece p : pieces) { + if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng, + foundationColumns, supportNonOccluding) == -1) { + failedPieces++; + } + } + } else if (pieces.size() == 1) { + if (placeObject(writer, structure, pieces.getFirst(), mode, -1, rng, + foundationColumns, supportNonOccluding) == -1) { + failedPieces++; + } + } else { + for (PlacedStructurePiece p : pieces) { + if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng, + foundationColumns, supportNonOccluding) == -1) { + failedPieces++; + } + } + } + requireAppliedPieces(resolved, cx, cz, failedPieces); + if (failedPieces == 0 && stilt != null) { + placeFoundation(writer, foundationColumns, stilt, rng, !placement.isAnchoredUnderground()); + } + } + + private void placeFoundation(MantleWriter writer, Long2IntOpenHashMap columns, + IrisStructureStiltSettings settings, RNG rng, + boolean surfaceStructure) { + IrisMaterialPalette palette = Objects.requireNonNull( + settings.getPalette(), "Structure stilt palette must not be null"); + int mantleOffset = getEngineMantle().getEngine().getMinHeight(); + int maxDepth = Math.max(1, settings.getMaxDepth()); + for (Long2IntMap.Entry column : columns.long2IntEntrySet()) { + int worldX = StructureFoundationPlanner.unpackX(column.getLongKey()); + int worldZ = StructureFoundationPlanner.unpackZ(column.getLongKey()); + int foundationY = column.getIntValue(); + PlatformBlockState foundationState = writer.getDataIfPresent( + worldX, foundationY, worldZ, PlatformBlockState.class); + if (foundationState == null || !foundationState.isSolid()) { + continue; + } + int terrainHeight = getEngineMantle().trueHeight(worldX, worldZ); + int groundY = StructureFoundationPlanner.findGroundY( + foundationY, maxDepth, 0, + y -> { + PlatformBlockState overlay = writer.getDataIfPresent( + worldX, y, worldZ, PlatformBlockState.class); + boolean carved = writer.getDataIfPresent( + worldX, y, worldZ, MatterCavern.class) != null; + return surfaceStructure + ? StructureFoundationPlanner.isSurfaceSupportBoundary( + overlay, carved, y, terrainHeight) + : StructureFoundationPlanner.isGroundSolid( + overlay, carved, y, terrainHeight); + }); + if (groundY == StructureFoundationPlanner.NO_GROUND) { + continue; + } + StructureFoundationPlanner.fillSupportColumn(foundationY, groundY, y -> { + writeFoundationSupport( + writer, palette, rng, getData(), worldX, y, worldZ, mantleOffset); + }); + } + } + + static void writeFoundationSupport(MantleWriter writer, IrisMaterialPalette palette, RNG rng, + IrisData data, int worldX, int mantleY, int worldZ, + int mantleOffset) { + int worldY = mantleY + mantleOffset; + PlatformBlockState support = palette.get(rng, worldX, worldY, worldZ, data); + if (support == null) { + throw new IllegalStateException("Structure stilt palette resolved no block at " + + worldX + "," + worldY + "," + worldZ); + } + writer.clearData(worldX, mantleY, worldZ, MatterCavern.class); + writer.set(worldX, mantleY, worldZ, support); + } + + static KList resolvedPiecesOrNull( + IrisStructureLocator.ResolvedPlacement resolved, + int chunkX, + int chunkZ + ) { + if (resolved == null) { + return null; + } + KList pieces = resolved.pieces(); + if (!IrisStructureLocator.requirePlacementOutput( + resolved.placement(), resolved.structureKey(), chunkX, chunkZ, + pieces != null && !pieces.isEmpty(), "placement application received no assembled pieces")) { + return null; + } + return pieces; + } + + static void requireAppliedPieces(IrisStructureLocator.ResolvedPlacement resolved, + int chunkX, int chunkZ, int failedPieces) { + IrisStructureLocator.requirePlacementOutput( + resolved.placement(), resolved.structureKey(), chunkX, chunkZ, failedPieces == 0, + "object placement rejected " + failedPieces + " of " + resolved.pieces().size() + + " assembled piece(s)"); + } + + private void boreStructure(MantleWriter writer, KList pieces, + IrisStructureTerrain terrain) { + int[] bounds = computePieceBounds(pieces); + if (bounds == null) { + return; + } + int pad = Math.max(0, terrain.getHorizontalPadding()); + int minX = bounds[0] - pad; + int minY = bounds[1] - Math.max(0, terrain.getFloorPadding()); + int minZ = bounds[2] - pad; + int maxX = bounds[3] + pad; + int maxY = bounds[4] + Math.max(0, terrain.getCeilingPadding()); + int maxZ = bounds[5] + pad; + int worldMin = getEngineMantle().getEngine().getMinHeight() + 1; + int worldMax = getEngineMantle().getEngine().getMinHeight() + getEngineMantle().getEngine().getHeight() - 1; + minY = Math.max(minY, worldMin); + maxY = Math.min(maxY, worldMax); + if (maxX < minX || maxY < minY || maxZ < minZ) { + return; + } + int mantleOffset = getEngineMantle().getEngine().getMinHeight(); + for (int bx = minX; bx <= maxX; bx++) { + for (int by = minY; by <= maxY; by++) { + for (int bz = minZ; bz <= maxZ; bz++) { + writer.setForcedCarve(bx, by - mantleOffset, bz, CARVE_CAVERN); + } + } + } + } + + private void forceCarveStructure(MantleWriter writer, KList pieces, + IrisStructureTerrain terrain) { + int[] bounds = computePieceBounds(pieces); + if (bounds == null) { + return; + } + IrisStructureCarveShape shape = terrain.resolvedShape(); + int margin = Math.max(0, terrain.getHorizontalPadding()); + int head = Math.max(0, terrain.getCeilingPadding()); + int floorCut = Math.max(0, terrain.getFloorPadding()); + double strength = terrain.resolvedErosionStrength(); + int mantleOffset = getEngineMantle().getEngine().getMinHeight(); + int worldMin = getEngineMantle().getEngine().getMinHeight() + 1; + int worldMax = getEngineMantle().getEngine().getMinHeight() + getEngineMantle().getEngine().getHeight() - 1; + int upExtension = shape.maximumCeilingExtension(head, strength); + + if (shape == IrisStructureCarveShape.BOX) { + carveOverboreBox(writer, bounds, margin, head, floorCut, mantleOffset, worldMin, worldMax); + return; + } + + long work = overboreCandidateVolume(bounds, margin, upExtension, floorCut); + StructureCarvingFootprint footprint = StructureCarvingFootprint.from( + pieces, margin, Integer.MAX_VALUE); + if (footprint == null) { + throw new IllegalStateException("Structure force-carve footprint is empty or exceeds addressable memory"); + } + + double frequency = terrain.resolvedErosionFrequency(); + boolean eroded = shape == IrisStructureCarveShape.ERODED && strength > 0D; + CNG blob = null; + CNG roll = null; + if (eroded) { + RNG noiseRng = new RNG(seed() + Cache.key(bounds[0], bounds[2])); + blob = CNG.signature(noiseRng); + roll = CNG.signature(noiseRng.nextParallelRNG(0x2A17)); + } + + if (IrisSettings.get().getGeneral().isDebug()) { + IrisLogging.info("Overbore carving structure cavern: shape=" + shape + " pieces=" + pieces.size() + + " footprint=" + footprint.width() + "x" + footprint.depth() + + " margin=" + margin + " head=" + head + " floorCut=" + floorCut + " work=" + work); + } + + double sideReachSquared = (double) margin * margin; + double downReach = Math.max(1D, floorCut); + for (int bz = footprint.minZ(); bz <= footprint.maxZ(); bz++) { + int footprintIndex = footprint.indexAt(footprint.minX(), bz); + for (int bx = footprint.minX(); bx <= footprint.maxX(); bx++, footprintIndex++) { + long horizontalDistanceSquared = footprint.distanceSquaredAt(footprintIndex); + if (horizontalDistanceSquared < 0L || horizontalDistanceSquared > sideReachSquared) { + continue; + } + double normalizedHorizontalDistanceSquared = sideReachSquared == 0D + ? 0D : horizontalDistanceSquared / sideReachSquared; + int sourceMinY = footprint.sourceMinYAt(footprintIndex); + int sourceMaxY = footprint.sourceMaxYAt(footprintIndex); + double upReach = eroded + ? StructureCarveEnvelope.erodedUpReach(roll, frequency, strength, bx, bz, head) + : Math.max(1D, head); + int columnUpExtension = eroded + ? (int) Math.ceil(upReach) : head; + int minY = Math.max(worldMin, sourceMinY - floorCut); + int maxY = Math.min(worldMax, sourceMaxY + columnUpExtension); + for (int by = minY; by <= maxY; by++) { + double normalizedY = StructureCarveEnvelope.normalizedVerticalDistance( + by, sourceMinY, sourceMaxY, upReach, downReach); + double distanceSquared = normalizedHorizontalDistanceSquared + + normalizedY * normalizedY; + if (distanceSquared <= 0D) { + writer.setForcedCarve(bx, by - mantleOffset, bz, CARVE_CAVERN); + continue; + } + if (distanceSquared > 1D) { + continue; + } + if (!eroded) { + writer.setForcedCarve(bx, by - mantleOffset, bz, CARVE_CAVERN); + continue; + } + double noise = blob.fitDouble( + 0D, 1D, bx * frequency, by * frequency, bz * frequency); + if (StructureCarveEnvelope.shouldCarveOverboreCell( + shape, distanceSquared, noise, strength)) { + writer.setForcedCarve(bx, by - mantleOffset, bz, CARVE_CAVERN); + } + } + } + } + } + + private void carveOverboreBox(MantleWriter writer, int[] bounds, int margin, int head, int floorCut, + int mantleOffset, int worldMin, int worldMax) { + int minX = bounds[0] - margin; + int minY = Math.max(worldMin, bounds[1] - floorCut); + int minZ = bounds[2] - margin; + int maxX = bounds[3] + margin; + int maxY = Math.min(worldMax, bounds[4] + head); + int maxZ = bounds[5] + margin; + for (int bx = minX; bx <= maxX; bx++) { + for (int by = minY; by <= maxY; by++) { + for (int bz = minZ; bz <= maxZ; bz++) { + writer.setForcedCarve(bx, by - mantleOffset, bz, CARVE_CAVERN); + } + } + } + } + + private static long overboreCandidateVolume(int[] bounds, int margin, int upExtension, + int floorCut) { + return inclusiveVolume( + bounds[0] - margin, bounds[1] - floorCut, bounds[2] - margin, + bounds[3] + margin, bounds[4] + upExtension, bounds[5] + margin); + } + + private static long inclusiveVolume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + long width = (long) maxX - minX + 1L; + long height = (long) maxY - minY + 1L; + long depth = (long) maxZ - minZ + 1L; + if (width <= 0L || height <= 0L || depth <= 0L + || width > Long.MAX_VALUE / height + || width * height > Long.MAX_VALUE / depth) { + return Long.MAX_VALUE; + } + return width * height * depth; + } + + private void clearIntersectingObjectTrees(MantleWriter writer, IrisStructureLocator.ResolvedPlacement resolved) { + int mantleOffset = getEngineMantle().getEngine().getMinHeight(); + int worldHeight = getEngineMantle().getEngine().getHeight(); + Map intersectingObjects = new HashMap<>(); + for (PlacedStructurePiece piece : resolved.pieces()) { + for (IrisBlockVector local : piece.getObject().getBlocks().keys()) { + PlatformBlockState structureState = piece.getObject().getBlocks().get(local); + if (!isOccupiedStructureState(structureState)) { + continue; + } + IrisBlockVector rotated = piece.getRotation().rotate(local.clone()); + int y = piece.getY() - mantleOffset + rotated.getBlockY(); + if (y >= 0 && y < worldHeight) { + collectIntersectingObjectTreeMarker( + writer, piece.getX() + rotated.getBlockX(), y, + piece.getZ() + rotated.getBlockZ(), intersectingObjects); + } + } + } + for (Map.Entry entry : intersectingObjects.entrySet()) { + clearMarkerOwnedObject(writer, entry.getKey(), entry.getValue(), worldHeight); + } + } + + static boolean isOccupiedStructureState(PlatformBlockState state) { + return !B.isAir(state); + } + + private void collectIntersectingObjectTreeMarker(MantleWriter writer, int x, int y, int z, + Map intersectingObjects) { + PlatformBlockState state = writer.getDataIfPresent(x, y, z, PlatformBlockState.class); + if (state == null || !state.isTreeBlock()) { + return; + } + String marker = writer.getDataIfPresent(x, y, z, String.class); + if (isOrdinaryObjectMarker(marker)) { + intersectingObjects.computeIfAbsent(marker, ignored -> new ObjectMarkerBounds()) + .include(x, y, z); + } + } + + private void clearMarkerOwnedObject(MantleWriter writer, String marker, + ObjectMarkerBounds intersections, int worldHeight) { + StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker); + if (decoded == null) { + return; + } + IrisObject object = getData().load(IrisObject.class, decoded.objectKey(), false); + if (object == null) { + return; + } + int horizontalSpan = Math.max(1, Math.max(object.getW(), object.getD())); + int verticalSpan = Math.max(1, object.getH()); + ObjectMarkerBounds search = intersections.expand(horizontalSpan - 1, verticalSpan - 1, worldHeight); + KList positions = new KList<>(); + int minChunkX = Math.floorDiv(search.minX, 16); + int maxChunkX = Math.floorDiv(search.maxX, 16); + int minChunkZ = Math.floorDiv(search.minZ, 16); + int maxChunkZ = Math.floorDiv(search.maxZ, 16); + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + MantleChunk chunk = writer.acquireChunk(chunkX, chunkZ); + if (chunk == null) { + continue; + } + int blockX = chunkX << 4; + int blockZ = chunkZ << 4; + chunk.iterate(String.class, (localX, y, localZ, value) -> { + int x = blockX + localX; + int z = blockZ + localZ; + if (marker.equals(value) && search.contains(x, y, z)) { + positions.add(new ObjectBlockPosition(x, y, z)); + } + }); + } + } + for (ObjectBlockPosition position : positions) { + writer.clearBlock(position.x(), position.y(), position.z()); + writer.clearData(position.x(), position.y(), position.z(), String.class); + writer.clearData(position.x(), position.y(), position.z(), TreeBlockMaterial.class); + writer.clearData(position.x(), position.y(), position.z(), TileWrapper.class); + writer.clearData(position.x(), position.y(), position.z(), MatterStructurePOI.class); + } + } + + static boolean isOrdinaryObjectMarker(String marker) { + StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker); + return decoded != null && !decoded.structureAware(); + } + + static ObjectMarkerBounds markerSearchBounds(int minX, int minY, int minZ, + int maxX, int maxY, int maxZ, + int horizontalSpan, int verticalSpan, + int worldHeight) { + ObjectMarkerBounds bounds = new ObjectMarkerBounds(); + bounds.minX = minX; + bounds.minY = minY; + bounds.minZ = minZ; + bounds.maxX = maxX; + bounds.maxY = maxY; + bounds.maxZ = maxZ; + return bounds.expand( + Math.max(0, horizontalSpan - 1), Math.max(0, verticalSpan - 1), worldHeight); + } + + private int[] computePieceBounds(KList pieces) { + if (pieces == null || pieces.isEmpty()) { + return null; + } + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (PlacedStructurePiece p : pieces) { + minX = Math.min(minX, p.getMinX()); + minY = Math.min(minY, p.getMinY()); + minZ = Math.min(minZ, p.getMinZ()); + maxX = Math.max(maxX, p.getMaxX()); + maxY = Math.max(maxY, p.getMaxY()); + maxZ = Math.max(maxZ, p.getMaxZ()); + } + return new int[]{minX, minY, minZ, maxX, maxY, maxZ}; + } + + private int placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p, + ObjectPlaceMode mode, int y, RNG rng, Long2IntOpenHashMap foundationColumns, + boolean supportNonOccluding) { + IrisObject object = p.getObject(); + String objectKey = object.getLoadKey(); + IrisObjectPlacement config = structure.createLootPlacement(objectKey); + config.setMode(mode); + config.setRotation(p.getRotation()); + if (!structure.getEdit().isEmpty()) { + config.setEdit(structure.getEdit()); + } + if (mode != ObjectPlaceMode.STRUCTURE_PIECE && mode != ObjectPlaceMode.FLOATING) { + config.setForcePlace(true); + } + int placeY = (y == -1) ? -1 : y - getEngineMantle().getEngine().getMinHeight(); + String marker = structurePlacementMarker(structure, p, objectKey); + return object.place(p.getX(), placeY, p.getZ(), writer, config, rng, (position, state) -> { + StructureFoundationPlanner.recordBaseCell( + foundationColumns, position.getX(), position.getY(), position.getZ(), state, + supportNonOccluding); + if (marker != null && shouldWriteStructureMarker(state)) { + writer.setData(position.getX(), position.getY(), position.getZ(), marker); + } + }, null, getData()); + } + + static boolean shouldWriteStructureMarker(PlatformBlockState state) { + return state != null && state.isStorageChest(); + } + + static int structurePlacementId(String structureKey, String objectKey, int x, int y, int z) { + int hash = 17; + hash = 31 * hash + structureKey.hashCode(); + hash = 31 * hash + objectKey.hashCode(); + hash = 31 * hash + x; + hash = 31 * hash + y; + hash = 31 * hash + z; + return hash & Integer.MAX_VALUE; + } + + private static String structurePlacementMarker(IrisStructure structure, PlacedStructurePiece piece, String objectKey) { + String structureKey = structure.getLoadKey(); + if (structureKey == null || structureKey.isBlank() || objectKey == null || objectKey.isBlank()) { + return null; + } + int placementId = structurePlacementId(structureKey, objectKey, piece.getX(), piece.getY(), piece.getZ()); + return StructurePlacementMarker.encodeStructure(objectKey, placementId, structureKey); + } + + @Override + protected int computeRadius() { + IrisDimension dimension = getDimension(); + int maxBlocks = 0; + + for (IrisRegion region : dimension.getAllRegions(this::getData)) { + maxBlocks = Math.max(maxBlocks, maxBlocksFrom(region.getStructures())); + } + for (IrisBiome biome : dimension.getReachableBiomes(this::getData)) { + maxBlocks = Math.max(maxBlocks, maxBlocksFrom(biome.getStructures())); + } + maxBlocks = Math.max(maxBlocks, maxBlocksFrom(dimension.getStructures())); + + return maxBlocks; + } + + private int maxBlocksFrom(KList placements) { + int max = 0; + for (IrisStructurePlacement placement : placements) { + if (!placement.hasIrisStructures()) { + continue; + } + IrisStructureTerrain terrain = placement.resolvedTerrain(); + int carvePadding = terrain.resolvedMode() == IrisStructureTerrainMode.FORCE_CARVE + || terrain.resolvedMode() == IrisStructureTerrainMode.BORE + ? Math.max(0, terrain.getHorizontalPadding()) : 0; + for (String key : placement.getStructures()) { + IrisStructure structure = getData().load(IrisStructure.class, key, false); + if (structure != null) { + max = Math.max(max, Math.max(1, structure.getMaxSizeChunks()) * 16 + carvePadding); + } + } + } + return max; + } + + static final class ObjectMarkerBounds { + private int minX = Integer.MAX_VALUE; + private int minY = Integer.MAX_VALUE; + private int minZ = Integer.MAX_VALUE; + private int maxX = Integer.MIN_VALUE; + private int maxY = Integer.MIN_VALUE; + private int maxZ = Integer.MIN_VALUE; + + void include(int x, int y, int z) { + minX = Math.min(minX, x); + minY = Math.min(minY, y); + minZ = Math.min(minZ, z); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + maxZ = Math.max(maxZ, z); + } + + ObjectMarkerBounds expand(int horizontal, int vertical, int worldHeight) { + ObjectMarkerBounds expanded = new ObjectMarkerBounds(); + expanded.minX = minX - horizontal; + expanded.minY = Math.max(0, minY - vertical); + expanded.minZ = minZ - horizontal; + expanded.maxX = maxX + horizontal; + expanded.maxY = Math.min(worldHeight - 1, maxY + vertical); + expanded.maxZ = maxZ + horizontal; + return expanded; + } + + boolean contains(int x, int y, int z) { + return x >= minX && x <= maxX + && y >= minY && y <= maxY + && z >= minZ && z <= maxZ; + } + + int minX() { + return minX; + } + + int minY() { + return minY; + } + + int minZ() { + return minZ; + } + + int maxX() { + return maxX; + } + + int maxY() { + return maxY; + } + + int maxZ() { + return maxZ; + } + } + + private record ObjectBlockPosition(int x, int y, int z) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IslandObjectPlacer.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IslandObjectPlacer.java new file mode 100644 index 000000000..8432fa289 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IslandObjectPlacer.java @@ -0,0 +1,220 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.FloatingIslandSample; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.IrisFloatingChildBiomes; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.spi.PlatformBlockState; +import org.jetbrains.annotations.Nullable; + +public final class IslandObjectPlacer implements IObjectPlacer { + private static final int OVERHANG_RADIUS = 2; + private static final int OVERHANG_HEIGHT_TOLERANCE = 4; + + private final MantleWriter wrapped; + private final SampleProvider samples; + private final IrisFloatingChildBiomes entry; + private final int anchorY; + private final AnchorFace face; + + private IslandObjectPlacer(MantleWriter wrapped, AnchorSettings settings) { + this.wrapped = wrapped; + this.samples = settings.samples(); + this.entry = settings.entry(); + this.anchorY = settings.anchorY(); + this.face = settings.face(); + } + + public static IslandObjectPlacer top(MantleWriter wrapped, SampleProvider samples, + IrisFloatingChildBiomes entry, int anchorY) { + return new IslandObjectPlacer(wrapped, new AnchorSettings(samples, entry, anchorY, AnchorFace.TOP)); + } + + public static IslandObjectPlacer bottom(MantleWriter wrapped, SampleProvider samples, + IrisFloatingChildBiomes entry, int anchorY) { + return new IslandObjectPlacer(wrapped, new AnchorSettings(samples, entry, anchorY, AnchorFace.BOTTOM)); + } + + public boolean canWriteObjectBlock(int x, int y, int z) { + return !shouldSkipAirColumn(x, y, z); + } + + @Override + public int getHighest(int x, int z, IrisData data) { + FloatingIslandSample sample = samples.sample(x, z); + if (face == AnchorFace.TOP) { + return sample == null ? anchorY : sample.topY(); + } + if (sample == null) { + return anchorY; + } + int bottomY = sample.bottomY(); + return bottomY < 0 ? anchorY : bottomY; + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return getHighest(x, z, data); + } + + @Override + public boolean isUnderwater(int x, int z) { + return false; + } + + @Override + public boolean isSolid(int x, int y, int z) { + FloatingIslandSample sample = samples.sample(x, z); + if (sample != null) { + int index = y - sample.islandBaseY; + if (index >= 0 && index < sample.solidMask.length) { + return sample.solidMask[index]; + } + return false; + } + return wrapped.isSolid(x, y, z); + } + + @Override + public boolean isCarved(int x, int y, int z) { + return wrapped.isCarved(x, y, z); + } + + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + return wrapped.isSurfaceSolid(x, y, z); + } + + @Override + public void set(int x, int y, int z, PlatformBlockState state) { + if (!shouldSkipAirColumn(x, y, z)) { + wrapped.set(x, y, z, state); + } + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + return wrapped.get(x, y, z); + } + + @Override + public boolean isPreventingDecay() { + return wrapped.isPreventingDecay(); + } + + @Override + public int getFluidHeight() { + return wrapped.getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return wrapped.isDebugSmartBore(); + } + + @Override + public void setTile(int x, int y, int z, TileData tile) { + if (!shouldSkipAirColumn(x, y, z)) { + wrapped.setTile(x, y, z, tile); + } + } + + @Override + public void setData(int x, int y, int z, T data) { + if (!shouldSkipAirColumn(x, y, z)) { + wrapped.setData(x, y, z, data); + } + } + + @Override + public @Nullable T getData(int x, int y, int z, Class type) { + return wrapped.getData(x, y, z, type); + } + + @Override + public Engine getEngine() { + return wrapped == null ? null : wrapped.getEngine(); + } + + static boolean matchesAnchor(FloatingIslandSample sample, IrisFloatingChildBiomes entry, AnchorFace face) { + if (sample == null) { + return false; + } + return face == AnchorFace.TOP ? sample.entry == entry : sample.bottomEntry() == entry; + } + + private boolean shouldSkipAirColumn(int x, int y, int z) { + Engine engine = getEngine(); + if (engine != null && (y < 0 || y >= engine.getHeight())) { + return true; + } + + FloatingIslandSample sample = samples.sample(x, z); + if (matchesAnchor(sample, entry, face) && isNearAnchorHeight(sample)) { + return face == AnchorFace.BOTTOM && y >= anchorY; + } + if (face == AnchorFace.TOP && y <= anchorY) { + return true; + } + if (face == AnchorFace.BOTTOM && y >= anchorY) { + return true; + } + return !hasNearbySupport(x, z); + } + + private boolean hasNearbySupport(int x, int z) { + for (int dz = -OVERHANG_RADIUS; dz <= OVERHANG_RADIUS; dz++) { + for (int dx = -OVERHANG_RADIUS; dx <= OVERHANG_RADIUS; dx++) { + FloatingIslandSample sample = samples.sample(x + dx, z + dz); + if (matchesAnchor(sample, entry, face) && isNearAnchorHeight(sample)) { + return true; + } + } + } + return false; + } + + private boolean isNearAnchorHeight(FloatingIslandSample sample) { + int faceY = face == AnchorFace.TOP ? sample.topY() : sample.bottomY(); + return faceY >= 0 && Math.abs(faceY - anchorY) <= OVERHANG_HEIGHT_TOLERANCE; + } + + public enum AnchorFace { + TOP, + BOTTOM + } + + @FunctionalInterface + public interface SampleProvider { + @Nullable FloatingIslandSample sample(int x, int z); + } + + private record AnchorSettings( + SampleProvider samples, + IrisFloatingChildBiomes entry, + int anchorY, + AnchorFace face + ) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java new file mode 100644 index 000000000..3afd69d85 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -0,0 +1,708 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.UpperDimensionContext; +import art.arcane.iris.engine.mantle.ComponentFlag; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.mantle.IrisMantleComponent; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisCaveProfile; +import art.arcane.iris.engine.object.IrisDimensionCarvingEntry; +import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D; +import art.arcane.iris.util.simd.SimdKernels; +import art.arcane.iris.util.simd.SimdSupport; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +@ComponentFlag(ReservedFlag.CARVED) +public class MantleCarvingComponent extends IrisMantleComponent { + private static final int CHUNK_SIZE = 16; + private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE; + private static final int BLEND_RADIUS = 3; + private static final int FIELD_SIZE = CHUNK_SIZE + (BLEND_RADIUS * 2); + private static final double MIN_WEIGHT = 0.08D; + private static final double THRESHOLD_PENALTY = 0.24D; + private static final int MAX_BLENDED_PROFILE_PASSES = 2; + private static final int KERNEL_WIDTH = (BLEND_RADIUS * 2) + 1; + private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH; + private static final int[] KERNEL_DX = new int[KERNEL_SIZE]; + private static final int[] KERNEL_DZ = new int[KERNEL_SIZE]; + private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE]; + private static final ThreadLocal BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new); + + private final Object profileCarverLock = new Object(); + private volatile Map profileCarvers = new IdentityHashMap<>(); + + static { + int kernelIndex = 0; + for (int offsetX = -BLEND_RADIUS; offsetX <= BLEND_RADIUS; offsetX++) { + for (int offsetZ = -BLEND_RADIUS; offsetZ <= BLEND_RADIUS; offsetZ++) { + KERNEL_DX[kernelIndex] = offsetX; + KERNEL_DZ[kernelIndex] = offsetZ; + int edgeDistance = Math.max(Math.abs(offsetX), Math.abs(offsetZ)); + KERNEL_WEIGHT[kernelIndex] = (BLEND_RADIUS + 1D) - edgeDistance; + kernelIndex++; + } + } + } + + public MantleCarvingComponent(EngineMantle engineMantle) { + super(engineMantle, ReservedFlag.CARVED, 0); + } + + @Override + public void hotload() { + super.hotload(); + // Hotload swaps in fresh IrisCaveProfile instances (identity keys), so retained + // entries would strand a full carver set pinning the closed IrisData per reload. + // Carvers rebuild deterministically from the carve seed, so output is unchanged. + synchronized (profileCarverLock) { + profileCarvers = new IdentityHashMap<>(); + } + } + + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + IrisComplex complex = context.getComplex(); + IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + BlendScratch blendScratch = BLEND_SCRATCH.get(); + int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights); + PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); + List weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState); + getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); + int fluidHeight = getDimension().getFluidHeight(); + int[] surfaceFluidBoundaryStartY = blendScratch.surfaceFluidBoundaryStartY; + SurfaceFluidBoundaryPlan.fill( + chunkSurfaceHeights, + blendScratch.fieldSurfaceHeights, + blendScratch.fieldHasFluid, + FIELD_SIZE, + BLEND_RADIUS, + fluidHeight, + surfaceFluidBoundaryStartY + ); + CaveFluidSupportPlan fluidSupportPlan = new CaveFluidSupportPlan(); + for (WeightedProfile weightedProfile : weightedProfiles) { + carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaryStartY, fluidSupportPlan); + } + + UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext(); + if (upperCtx != null && getDimension().isUpperDimensionCarving()) { + carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, chunkSurfaceHeights, fluidSupportPlan); + } + fluidSupportPlan.resolve(writer.acquireChunk(x, z)); + + if (!weightedProfiles.isEmpty()) { + CarveOrphanSweep.sweepChunk( + writer.acquireChunk(x, z), + chunkSurfaceHeights, + maxSurfaceBreakDepth(weightedProfiles), + writer.getMantle().getWorldHeight() - 1, + surfaceFluidBoundaryStartY, + fluidHeight + ); + } + } + + private static int maxSurfaceBreakDepth(List weightedProfiles) { + int maxDepth = 0; + for (WeightedProfile weightedProfile : weightedProfiles) { + maxDepth = Math.max(maxDepth, Math.max(0, weightedProfile.profile.getSurfaceBreakDepth())); + } + return maxDepth; + } + + @ChunkCoordinates + private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz, + int[] chunkSurfaceHeights, int[] surfaceFluidBoundaryStartY, + CaveFluidSupportPlan fluidSupportPlan) { + IrisCaveCarver3D carver = getCarver(weightedProfile.profile); + carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, + weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaryStartY, null, fluidSupportPlan); + } + + private void carveUpperTerrain(UpperDimensionContext upperCtx, List normalProfiles, + MantleWriter writer, int cx, int cz, int[] lowerSurfaceHeights, + CaveFluidSupportPlan fluidSupportPlan) { + int chunkHeight = getEngineMantle().getEngine().getHeight(); + int worldMinHeight = getEngineMantle().getEngine().getWorld().minHeight(); + int gap = getDimension().getUpperDimensionGap(); + int baseX = PowerOfTwoCoordinates.chunkToBlock(cx); + int baseZ = PowerOfTwoCoordinates.chunkToBlock(cz); + + int minUpperSurfaceY = chunkHeight; + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + int worldX = baseX + localX; + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int worldZ = baseZ + localZ; + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ); + int upperY = Math.max(rawUpper, lowerSurfaceHeights[columnIndex] + gap); + if (upperY < minUpperSurfaceY) { + minUpperSurfaceY = upperY; + } + } + } + + if (minUpperSurfaceY >= chunkHeight - 2) { + return; + } + + IrisRange upperYRange = new IrisRange( + minUpperSurfaceY + worldMinHeight, + chunkHeight - 1 + worldMinHeight + ); + IrisRange fullVerticalRange = new IrisRange(0, chunkHeight); + + int[] ceilingSurfaceHeights = new int[CHUNK_AREA]; + Arrays.fill(ceilingSurfaceHeights, chunkHeight - 1); + + for (WeightedProfile weightedProfile : normalProfiles) { + IrisRange constrainedRange; + if (weightedProfile.worldYRange != null) { + double min = Math.max(weightedProfile.worldYRange.getMin(), upperYRange.getMin()); + double max = Math.min(weightedProfile.worldYRange.getMax(), upperYRange.getMax()); + if (min >= max) { + continue; + } + constrainedRange = new IrisRange(min, max); + } else { + constrainedRange = upperYRange; + } + IrisCaveCarver3D carver = getCarver(weightedProfile.profile); + carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, + constrainedRange, ceilingSurfaceHeights, null, fullVerticalRange, fluidSupportPlan); + } + } + + private List resolveWeightedProfiles(int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState) { + BlendScratch blendScratch = BLEND_SCRATCH.get(); + IrisCaveProfile[] profileField = blendScratch.profileField; + Map columnProfileWeights = blendScratch.columnProfileWeights; + IdentityHashMap activeProfiles = blendScratch.activeProfiles; + List profileOrder = blendScratch.profileOrder; + IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles; + double[] kernelProfileWeights = blendScratch.kernelProfileWeights; + activeProfiles.clear(); + profileOrder.clear(); + fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, blendScratch); + + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int profileCount = 0; + int centerX = localX + BLEND_RADIUS; + int centerZ = localZ + BLEND_RADIUS; + double totalKernelWeight = 0D; + + for (int kernelIndex = 0; kernelIndex < KERNEL_SIZE; kernelIndex++) { + int sampleX = centerX + KERNEL_DX[kernelIndex]; + int sampleZ = centerZ + KERNEL_DZ[kernelIndex]; + IrisCaveProfile profile = profileField[(sampleX * FIELD_SIZE) + sampleZ]; + if (!isProfileEnabled(profile)) { + continue; + } + + double kernelWeight = KERNEL_WEIGHT[kernelIndex]; + int existingIndex = findProfileIndex(kernelProfiles, profileCount, profile); + if (existingIndex >= 0) { + kernelProfileWeights[existingIndex] += kernelWeight; + } else { + kernelProfiles[profileCount] = profile; + kernelProfileWeights[profileCount] = kernelWeight; + profileCount++; + } + totalKernelWeight += kernelWeight; + } + + if (totalKernelWeight <= 0D || profileCount == 0) { + continue; + } + + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + for (int profileIndex = 0; profileIndex < profileCount; profileIndex++) { + IrisCaveProfile profile = kernelProfiles[profileIndex]; + double kernelWeight = kernelProfileWeights[profileIndex]; + kernelProfiles[profileIndex] = null; + kernelProfileWeights[profileIndex] = 0D; + + double columnWeight = clampWeight(kernelWeight / totalKernelWeight); + if (columnWeight < MIN_WEIGHT) { + continue; + } + + double[] weights = columnProfileWeights.get(profile); + if (weights == null) { + weights = new double[CHUNK_AREA]; + columnProfileWeights.put(profile, weights); + } else if (!activeProfiles.containsKey(profile)) { + Arrays.fill(weights, 0D); + } + if (activeProfiles.put(profile, Boolean.TRUE) == null) { + profileOrder.add(profile); + } + weights[columnIndex] = columnWeight; + } + } + } + + List columnWeightedProfiles = new ArrayList<>(); + for (IrisCaveProfile profile : profileOrder) { + double[] weights = columnProfileWeights.get(profile); + if (weights == null) { + continue; + } + + // averageWeight ranks and orders carve passes, so it must be bit-identical across + // installations: kernels.sum reassociates FP addition under the vector kernel, + // making profile order depend on a JVM flag. max() is order-independent and safe. + double maxWeight = SimdSupport.kernels().max(weights, weights.length); + + if (maxWeight < MIN_WEIGHT) { + continue; + } + + double averageWeight = computeAverageWeight(weights); + columnWeightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null, columnWeightedProfiles.size())); + } + + List blendedProfiles = limitAndMergeBlendedProfiles(columnWeightedProfiles, MAX_BLENDED_PROFILE_PASSES, CHUNK_AREA); + List resolvedProfiles = resolveDimensionCarvingProfiles(chunkX, chunkZ, resolverState, blendScratch); + resolvedProfiles.addAll(blendedProfiles); + return resolvedProfiles; + } + + private List resolveDimensionCarvingProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) { + List weightedProfiles = new ArrayList<>(); + List entries = getDimension().getCarving(); + if (entries == null || entries.isEmpty()) { + return weightedProfiles; + } + + Map dimensionColumnPlans = blendScratch.dimensionColumnPlans; + dimensionColumnPlans.clear(); + + for (IrisDimensionCarvingEntry entry : entries) { + if (entry == null || !entry.isEnabled()) { + continue; + } + + IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry, resolverState); + if (rootBiome == null) { + continue; + } + + IrisDimensionCarvingEntry[] columnPlan = dimensionColumnPlans.computeIfAbsent(entry, key -> new IrisDimensionCarvingEntry[CHUNK_AREA]); + buildDimensionColumnPlan(columnPlan, chunkX, chunkZ, entry, resolverState); + + Map rootProfileColumnWeights = new IdentityHashMap<>(); + List rootProfileOrder = new ArrayList<>(); + IrisRange worldYRange = entry.getWorldYRange(); + for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) { + IrisDimensionCarvingEntry resolvedEntry = columnPlan[columnIndex]; + IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState); + if (resolvedBiome == null) { + continue; + } + + IrisCaveProfile profile = resolvedBiome.getCaveProfile(); + if (!isProfileEnabled(profile)) { + continue; + } + + double[] columnWeights = rootProfileColumnWeights.get(profile); + if (columnWeights == null) { + columnWeights = new double[CHUNK_AREA]; + rootProfileColumnWeights.put(profile, columnWeights); + rootProfileOrder.add(profile); + } + columnWeights[columnIndex] = 1D; + } + + for (IrisCaveProfile profile : rootProfileOrder) { + weightedProfiles.add(new WeightedProfile(profile, rootProfileColumnWeights.get(profile), -1D, worldYRange, weightedProfiles.size())); + } + } + + return weightedProfiles; + } + + private void buildDimensionColumnPlan(IrisDimensionCarvingEntry[] columnPlan, int chunkX, int chunkZ, IrisDimensionCarvingEntry entry, IrisDimensionCarvingResolver.State resolverState) { + int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX); + int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ); + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + int worldX = baseX + localX; + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int worldZ = baseZ + localZ; + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + columnPlan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState); + } + } + } + + private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) { + int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS; + int startZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ) - BLEND_RADIUS; + prefillProfileFieldSamples(startX, startZ, complex, blendScratch); + + for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { + int worldX = startX + fieldX; + for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { + int worldZ = startZ + fieldZ; + int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ; + profileField[fieldIndex] = resolveColumnProfile( + worldX, + worldZ, + blendScratch.fieldSurfaceHeights[fieldIndex], + blendScratch.fieldRegions[fieldIndex], + blendScratch.fieldSurfaceBiomes[fieldIndex], + blendScratch.fieldCaveBiomes[fieldIndex], + resolverState + ); + } + } + } + + private int findProfileIndex(IrisCaveProfile[] profiles, int size, IrisCaveProfile profile) { + for (int index = 0; index < size; index++) { + if (profiles[index] == profile) { + return index; + } + } + + return -1; + } + + private IrisCaveProfile resolveColumnProfile( + int worldX, + int worldZ, + double surfaceHeight, + IrisRegion region, + IrisBiome surfaceBiome, + IrisBiome caveBiome, + IrisDimensionCarvingResolver.State resolverState + ) { + IrisCaveProfile resolved = null; + IrisCaveProfile dimensionProfile = getDimension().getCaveProfile(); + if (isProfileEnabled(dimensionProfile)) { + resolved = dimensionProfile; + } + + if (region != null) { + IrisCaveProfile regionProfile = region.getCaveProfile(); + if (isProfileEnabled(regionProfile)) { + resolved = regionProfile; + } + } + + if (surfaceBiome != null) { + IrisCaveProfile surfaceProfile = surfaceBiome.getCaveProfile(); + if (isProfileEnabled(surfaceProfile)) { + resolved = surfaceProfile; + } + } + + int roundedSurfaceY = (int) Math.round(surfaceHeight); + int sampleY = Math.max(1, roundedSurfaceY - 56); + int worldY = sampleY + getEngineMantle().getEngine().getWorld().minHeight(); + IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(getEngineMantle().getEngine(), worldY, resolverState); + IrisBiome resolvedCarvingBiome = null; + if (rootCarvingEntry != null) { + IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), rootCarvingEntry, worldX, worldZ, resolverState); + resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedCarvingEntry, resolverState); + if (resolvedCarvingBiome != null) { + caveBiome = resolvedCarvingBiome; + } + } + if (caveBiome != null) { + IrisBiome effectiveCaveBiome = caveBiome; + if (resolvedCarvingBiome == null && surfaceBiome != null) { + if (surfaceBiome != null) { + int truncatedSurfaceY = (int) surfaceHeight; + int depthBelowSurface = truncatedSurfaceY - sampleY; + if (depthBelowSurface <= 0) { + effectiveCaveBiome = surfaceBiome; + } else { + int minDepth = Math.max(0, caveBiome.getCaveMinDepthBelowSurface()); + if (depthBelowSurface < minDepth) { + effectiveCaveBiome = surfaceBiome; + } + } + } + } + + IrisCaveProfile caveProfile = effectiveCaveBiome.getCaveProfile(); + if (isProfileEnabled(caveProfile)) { + resolved = caveProfile; + } + } + + return resolved; + } + + private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) { + fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights); + fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldHasFluid); + fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions); + fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes); + fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes); + } + + private void fillFieldObjects(ProceduralStream stream, int startX, int startZ, T[] target) { + for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { + int worldX = startX + fieldX; + for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { + target[(fieldX * FIELD_SIZE) + fieldZ] = stream.get(worldX, startZ + fieldZ); + } + } + } + + private void fillFieldHeights(ProceduralStream stream, int startX, int startZ, double[] target) { + for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { + int worldX = startX + fieldX; + for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { + target[(fieldX * FIELD_SIZE) + fieldZ] = stream.getDouble(worldX, startZ + fieldZ); + } + } + } + + private void fillFieldFluidPresence(ProceduralStream stream, int startX, int startZ, boolean[] target) { + for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { + int worldX = startX + fieldX; + for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { + target[(fieldX * FIELD_SIZE) + fieldZ] = B.isFluid(stream.get(worldX, startZ + fieldZ)); + } + } + } + + private IrisCaveCarver3D getCarver(IrisCaveProfile profile) { + IrisCaveCarver3D carver = profileCarvers.get(profile); + if (carver != null) { + return carver; + } + + IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile); + synchronized (profileCarverLock) { + IrisCaveCarver3D published = profileCarvers.get(profile); + if (published != null) { + return published; + } + + Map updated = new IdentityHashMap<>(profileCarvers); + updated.put(profile, createdCarver); + profileCarvers = updated; + return createdCarver; + } + } + + private boolean isProfileEnabled(IrisCaveProfile profile) { + return profile != null && profile.isEnabled(); + } + + protected int computeRadius() { + return 1; + } + + private int[] prepareChunkSurfaceHeights(int chunkX, int chunkZ, ChunkContext context, int[] scratch) { + int[] surfaceHeights = scratch; + int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX); + int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ); + boolean useContextHeight = context != null + && context.getHeight() != null + && context.getX() == baseX + && context.getZ() == baseZ; + double[] cachedChunkHeights = null; + if (!useContextHeight && context != null) { + ProceduralStream heightStream = context.getComplex().getHeightStream(); + if (heightStream instanceof ChunkFillableDoubleStream2D cachedHeightStream) { + cachedChunkHeights = BLEND_SCRATCH.get().chunkSurfaceHeightSamples; + cachedHeightStream.fillChunkDoubles(baseX, baseZ, cachedChunkHeights); + } + } + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + int worldX = baseX + localX; + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int worldZ = baseZ + localZ; + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + if (useContextHeight) { + surfaceHeights[columnIndex] = context.getRoundedHeight(localX, localZ); + continue; + } + if (cachedChunkHeights != null) { + surfaceHeights[columnIndex] = (int) Math.round(cachedChunkHeights[(localZ << 4) + localX]); + continue; + } + surfaceHeights[columnIndex] = getEngineMantle().getEngine().getHeight(worldX, worldZ); + } + } + return surfaceHeights; + } + + private static List limitAndMergeBlendedProfiles(List blendedProfiles, int maxProfiles) { + return limitAndMergeBlendedProfiles(blendedProfiles, maxProfiles, CHUNK_AREA); + } + + private static List limitAndMergeBlendedProfiles(List blendedProfiles, int maxProfiles, int areaSize) { + if (blendedProfiles == null || blendedProfiles.isEmpty()) { + return new ArrayList<>(); + } + + int clampedLimit = Math.max(1, maxProfiles); + List rankedProfiles = new ArrayList<>(blendedProfiles); + rankedProfiles.sort(MantleCarvingComponent::compareBySelectionRank); + List keptProfiles = new ArrayList<>(); + int keptCount = Math.min(clampedLimit, rankedProfiles.size()); + for (int index = 0; index < keptCount; index++) { + keptProfiles.add(rankedProfiles.get(index)); + } + + if (rankedProfiles.size() > keptCount) { + for (int columnIndex = 0; columnIndex < areaSize; columnIndex++) { + int dominantIndex = 0; + double dominantWeight = Double.NEGATIVE_INFINITY; + for (int keptIndex = 0; keptIndex < keptProfiles.size(); keptIndex++) { + double keptWeight = keptProfiles.get(keptIndex).columnWeights[columnIndex]; + if (keptWeight > dominantWeight) { + dominantWeight = keptWeight; + dominantIndex = keptIndex; + } + } + + double droppedWeight = 0D; + for (int droppedIndex = keptCount; droppedIndex < rankedProfiles.size(); droppedIndex++) { + droppedWeight += rankedProfiles.get(droppedIndex).columnWeights[columnIndex]; + } + if (droppedWeight <= 0D) { + continue; + } + + WeightedProfile dominantProfile = keptProfiles.get(dominantIndex); + double mergedWeight = dominantProfile.columnWeights[columnIndex] + droppedWeight; + dominantProfile.columnWeights[columnIndex] = clampWeight(mergedWeight); + } + } + + List mergedProfiles = new ArrayList<>(); + for (WeightedProfile keptProfile : keptProfiles) { + double averageWeight = computeAverageWeight(keptProfile.columnWeights, areaSize); + mergedProfiles.add(new WeightedProfile(keptProfile.profile, keptProfile.columnWeights, averageWeight, keptProfile.worldYRange, keptProfile.sequence)); + } + mergedProfiles.sort(MantleCarvingComponent::compareByCarveOrder); + return mergedProfiles; + } + + private static int compareBySelectionRank(WeightedProfile a, WeightedProfile b) { + int weightOrder = Double.compare(b.averageWeight, a.averageWeight); + if (weightOrder != 0) { + return weightOrder; + } + return Integer.compare(a.sequence, b.sequence); + } + + private static int compareByCarveOrder(WeightedProfile a, WeightedProfile b) { + int weightOrder = Double.compare(a.averageWeight, b.averageWeight); + if (weightOrder != 0) { + return weightOrder; + } + return Integer.compare(a.sequence, b.sequence); + } + + private static double computeAverageWeight(double[] weights) { + return computeAverageWeight(weights, CHUNK_AREA); + } + + private static double computeAverageWeight(double[] weights, int areaSize) { + if (weights == null || weights.length == 0) { + return 0D; + } + double sum = 0D; + for (double weight : weights) { + sum += weight; + } + return sum / Math.max(1, areaSize); + } + + private static double clampWeight(double value) { + if (Double.isNaN(value) || Double.isInfinite(value)) { + return 0D; + } + if (value <= 0D) { + return 0D; + } + if (value >= 1D) { + return 1D; + } + return value; + } + + private static final class WeightedProfile { + private final IrisCaveProfile profile; + private final double[] columnWeights; + private final double averageWeight; + private final IrisRange worldYRange; + private final int sequence; + + private WeightedProfile(IrisCaveProfile profile, double[] columnWeights, double averageWeight, IrisRange worldYRange, int sequence) { + this.profile = profile; + this.columnWeights = columnWeights; + this.averageWeight = averageWeight; + this.worldYRange = worldYRange; + this.sequence = sequence; + } + + private double averageWeight() { + return averageWeight; + } + } + + private static final class BlendScratch { + private final IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE]; + private final IrisCaveProfile[] kernelProfiles = new IrisCaveProfile[KERNEL_SIZE]; + private final double[] kernelProfileWeights = new double[KERNEL_SIZE]; + private final IdentityHashMap columnProfileWeights = new IdentityHashMap<>(); + private final IdentityHashMap dimensionColumnPlans = new IdentityHashMap<>(); + private final IdentityHashMap activeProfiles = new IdentityHashMap<>(); + private final List profileOrder = new ArrayList<>(); + private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE]; + private final boolean[] fieldHasFluid = new boolean[FIELD_SIZE * FIELD_SIZE]; + private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE]; + private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; + private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; + private final int[] chunkSurfaceHeights = new int[CHUNK_AREA]; + private final int[] surfaceFluidBoundaryStartY = new int[CHUNK_AREA]; + private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA]; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java new file mode 100644 index 000000000..c351b8928 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java @@ -0,0 +1,614 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.mantle.ComponentFlag; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.mantle.IrisMantleComponent; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.FloatingIslandSample; +import art.arcane.iris.engine.object.FloatingObjectFootprint; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisFloatingChildBiomes; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.engine.object.IrisObjectTranslate; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.math.RNG; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +@ComponentFlag(ReservedFlag.FLOATING_OBJECT) +public class MantleFloatingObjectComponent extends IrisMantleComponent { + private static final int INVERTED_PICK_ATTEMPTS = 8; + private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0); + + public MantleFloatingObjectComponent(EngineMantle engineMantle) { + super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2); + } + + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + IrisComplex complex = context.getComplex(); + IrisData data = getData(); + int minX = x << 4; + int minZ = z << 4; + RNG chunkRng = new RNG(Cache.key(x, z) + seed() + 0x0FA710BEL); + FloatingIslandSampleResolver sampleResolver = new FloatingIslandSampleResolver(getEngineMantle(), context.getFloatingIslandBoundarySampler()); + + FloatingIslandSample.clearChunkMemo(); + + FloatingIslandSample[] samples = new FloatingIslandSample[256]; + for (int xf = 0; xf < 16; xf++) { + for (int zf = 0; zf < 16; zf++) { + int wx = minX + xf; + int wz = minZ + zf; + FloatingIslandSample sample = sampleResolver.sample(wx, wz); + if (sample != null) { + samples[(zf << 4) | xf] = sample; + } + } + } + + // The IdentityHashMaps are lookup indices only. Iteration MUST follow the encounter + // order of the deterministic 0..255 column scan: identity-hash order changes per JVM + // run, and the shared chunkRng makes placement output depend on iteration order. + IdentityHashMap> entryColumns = new IdentityHashMap<>(); + IdentityHashMap> bottomEntryColumns = new IdentityHashMap<>(); + KList entryOrder = new KList<>(); + KList bottomEntryOrder = new KList<>(); + for (int i = 0; i < 256; i++) { + FloatingIslandSample s = samples[i]; + if (s == null || s.entry == null) { + continue; + } + entryColumns.computeIfAbsent(s.entry, e -> { + entryOrder.add(e); + return new KList<>(); + }).add(i); + IrisFloatingChildBiomes bottomEntry = s.bottomEntry(); + if (bottomEntry != null) { + bottomEntryColumns.computeIfAbsent(bottomEntry, e -> { + bottomEntryOrder.add(e); + return new KList<>(); + }).add(i); + } + } + + for (IrisFloatingChildBiomes entry : entryOrder) { + KList columns = entryColumns.get(entry); + if (columns.isEmpty()) { + continue; + } + + int firstKey = columns.get(0); + IrisBiome parent = sampleResolver.parent(minX + (firstKey & 15), minZ + (firstKey >> 4)); + IrisBiome target = entry.getRealBiome(parent, data); + + KList floating = entry.getFloatingObjects(); + if (floating != null && !floating.isEmpty()) { + for (IrisObjectPlacement placement : floating) { + tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, columns, minX, minZ, entry); + } + } + + KList surface = target != null ? entry.resolveTopObjects(target) : null; + KList extras = entry.getExtraObjects(); + boolean hasSurface = surface != null && !surface.isEmpty(); + boolean hasExtras = extras != null && !extras.isEmpty(); + KList interior = null; + if (hasSurface || hasExtras) { + interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.TOP); + if (hasSurface) { + for (IrisObjectPlacement placement : surface) { + tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry); + } + } + if (hasExtras) { + for (IrisObjectPlacement placement : extras) { + tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry); + } + } + } + } + + for (IrisFloatingChildBiomes entry : bottomEntryOrder) { + KList columns = bottomEntryColumns.get(entry); + if (columns.isEmpty()) { + continue; + } + + int firstKey = columns.get(0); + IrisBiome parent = sampleResolver.parent(minX + (firstKey & 15), minZ + (firstKey >> 4)); + IrisBiome target = entry.getRealBiome(parent, data); + KList bottom = target != null ? entry.resolveBottomObjects(target) : null; + if (bottom != null && !bottom.isEmpty()) { + KList interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.BOTTOM); + for (IrisObjectPlacement placement : bottom) { + tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry); + } + } + } + } + + @ChunkCoordinates + private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, KList columns, int minX, int minZ, IrisFloatingChildBiomes entry) { + if (placement == null || columns == null || columns.isEmpty()) { + return; + } + int density = placement.getDensity(rng, minX, minZ, data); + double perAttempt = placement.getChance(); + for (int i = 0; i < density; i++) { + if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) { + continue; + } + IrisObject raw = placement.getObject(complex, rng); + if (raw == null) { + continue; + } + IrisObject obj0 = placement.getScale().get(rng, raw); + if (obj0 == null) { + continue; + } + if (entry != null && entry.hasObjectShrink()) { + obj0 = entry.getShrinkScale().get(rng, obj0); + if (obj0 == null) { + continue; + } + } + final IrisObject obj = obj0; + + int key = columns.get(rng.i(columns.size())); + int xx = minX + (key & 15); + int zz = minZ + (key >> 4); + IrisObjectPlacement floatingPlacement = placement.toPlacement(obj.getLoadKey()); + int id = rng.i(0, Integer.MAX_VALUE); + + try { + obj.place(xx, -1, zz, writer, floatingPlacement, rng, (b, bd) -> { + String marker = placementMarker(obj, id); + if (marker != null && shouldWritePlacementMarker(writer, bd, b.getX(), b.getY(), b.getZ())) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } + }, null, data); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + + @ChunkCoordinates + private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList columns, KList interior, int minX, int minZ, IrisFloatingChildBiomes entry) { + if (placement == null || columns.isEmpty()) { + return; + } + int density = placement.getDensity(rng, minX, minZ, data); + double perAttempt = placement.getChance(); + + for (int i = 0; i < density; i++) { + if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) { + continue; + } + + IrisObject raw = placement.getObject(complex, rng); + if (raw == null) { + continue; + } + IrisObject obj0 = placement.getScale().get(rng, raw); + if (obj0 == null) { + continue; + } + if (entry != null && entry.hasObjectShrink()) { + obj0 = entry.getShrinkScale().get(rng, obj0); + if (obj0 == null) { + continue; + } + } + final IrisObject obj = obj0; + + FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj); + + KList pool = interior.isEmpty() ? columns : interior; + + int pickedKey = pool.get(rng.i(pool.size())); + int pickedXf = pickedKey & 15; + int pickedZf = pickedKey >> 4; + FloatingIslandSample pickedSample = samples[(pickedZf << 4) | pickedXf]; + if (pickedSample == null) { + continue; + } + int pickTopY = pickedSample.topY(); + + int pickedX = minX + pickedXf; + int pickedZ = minZ + pickedZf; + if (!isFootprintFlat(fp, pickedX, pickedZ, pickTopY, sampleProvider, entry, 2)) { + if (!isFootprintFlat(fp, pickedX, pickedZ, pickTopY, sampleProvider, entry, 4)) { + continue; + } + } + + int wx = minX + pickedXf - fp.getTallestKx(); + int wz = minZ + pickedZf - fp.getTallestKz(); + + IrisObjectPlacement anchored = placement.toPlacement(obj.getLoadKey()); + anchored.setMode(translateStiltModeForFloating(anchored.getMode())); + anchored.setTranslate(new IrisObjectTranslate()); + anchored.setRotation(ROTATION_NONE); + anchored.setForcePlace(true); + anchored.setBottom(false); + + int yv = pickTopY + 1 - fp.getLowestSolidKeyY(); + + IslandObjectPlacer islandPlacer = IslandObjectPlacer.top(writer, sampleProvider, entry, pickTopY); + FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer); + int id = rng.i(0, Integer.MAX_VALUE); + + try { + int resultY = obj.place(wx, yv, wz, transaction, anchored, rng, (b, bd) -> { + String marker = placementMarker(obj, id); + if (marker != null && shouldWritePlacementMarker(transaction, bd, b.getX(), b.getY(), b.getZ())) { + transaction.setData(b.getX(), b.getY(), b.getZ(), marker); + } + }, null, data); + if (resultY < 0) { + transaction.discard(); + } else { + transaction.commit(); + } + } catch (Throwable e) { + transaction.discard(); + IrisLogging.reportError(e); + } + } + } + + @ChunkCoordinates + private void tryPlaceInvertedChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList columns, KList interior, int minX, int minZ, IrisFloatingChildBiomes entry) { + if (placement == null || columns.isEmpty()) { + return; + } + int density = placement.getDensity(rng, minX, minZ, data); + double perAttempt = placement.getChance(); + + for (int i = 0; i < density; i++) { + if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) { + continue; + } + + IrisObject raw = placement.getObject(complex, rng); + if (raw == null) { + continue; + } + IrisObject obj0 = placement.getScale().get(rng, raw); + if (obj0 == null) { + continue; + } + if (entry != null && entry.hasObjectShrink()) { + obj0 = entry.getShrinkScale().get(rng, obj0); + if (obj0 == null) { + continue; + } + } + final IrisObject obj = obj0; + + FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj); + int invertedYRotation = rng.i(4) * 90; + IrisObjectRotation invertedRotation = IrisObjectRotation.xFlip180WithY(invertedYRotation); + + KList pool = interior.isEmpty() ? columns : interior; + + int pickedXf = -1; + int pickedZf = -1; + int pickBottomY = -1; + boolean foundBottomAnchor = false; + for (int attempt = 0; attempt < INVERTED_PICK_ATTEMPTS; attempt++) { + int pickedKey = pool.get(rng.i(pool.size())); + int candidateXf = pickedKey & 15; + int candidateZf = pickedKey >> 4; + FloatingIslandSample candidateSample = samples[(candidateZf << 4) | candidateXf]; + if (candidateSample == null) { + continue; + } + int candidateBottomY = candidateSample.bottomY(); + if (candidateBottomY < 0) { + continue; + } + int candidateX = minX + candidateXf; + int candidateZ = minZ + candidateZf; + if (!isFootprintFlatBottom(fp, invertedRotation, candidateX, candidateZ, candidateBottomY, sampleProvider, entry, 2) + && !isFootprintFlatBottom(fp, invertedRotation, candidateX, candidateZ, candidateBottomY, sampleProvider, entry, 4)) { + continue; + } + pickedXf = candidateXf; + pickedZf = candidateZf; + pickBottomY = candidateBottomY; + foundBottomAnchor = true; + break; + } + if (!foundBottomAnchor) { + continue; + } + + int wx = invertedBaseX(minX, pickedXf, fp, invertedRotation); + int wz = invertedBaseZ(minZ, pickedZf, fp, invertedRotation); + + IrisObjectPlacement inverted = placement.toPlacement(obj.getLoadKey()); + inverted.setMode(translateStiltModeForFloating(inverted.getMode())); + inverted.setTranslate(new IrisObjectTranslate()); + inverted.setRotation(invertedRotation); + inverted.setForcePlace(true); + inverted.setBottom(false); + + int yv = invertedBaseY(pickBottomY, fp, invertedRotation); + + IslandObjectPlacer islandPlacer = IslandObjectPlacer.bottom(writer, sampleProvider, entry, pickBottomY); + FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer); + int id = rng.i(0, Integer.MAX_VALUE); + + try { + int resultY = obj.place(wx, yv, wz, transaction, inverted, rng, (b, bd) -> { + String marker = placementMarker(obj, id); + if (marker != null && shouldWritePlacementMarker(transaction, bd, b.getX(), b.getY(), b.getZ())) { + transaction.setData(b.getX(), b.getY(), b.getZ(), marker); + } + }, null, data); + if (resultY < 0) { + transaction.discard(); + } else { + transaction.commit(); + } + } catch (Throwable e) { + transaction.discard(); + IrisLogging.reportError(e); + } + } + } + + static boolean isFootprintFlatBottom(FloatingObjectFootprint fp, IrisObjectRotation rotation, + int pickedX, int pickedZ, int pickBottomY, + IslandObjectPlacer.SampleProvider samples, + IrisFloatingChildBiomes entry, int tolerance) { + IrisBlockVector anchor = invertedFootprintAnchor(fp, rotation); + long[] cells = fp.footprintXZ(); + if (cells.length == 0) { + return false; + } + for (int i = 0, n = cells.length; i < n; i++) { + long encoded = cells[i]; + int kx = (int) (encoded >> 32); + int kz = (int) (encoded & 0xFFFFFFFFL); + IrisBlockVector cell = rotation.rotate(new IrisBlockVector(kx, 0, kz), 0, 0, 0); + int columnX = pickedX + cell.getBlockX() - anchor.getBlockX(); + int columnZ = pickedZ + cell.getBlockZ() - anchor.getBlockZ(); + FloatingIslandSample sample = samples.sample(columnX, columnZ); + if (!IslandObjectPlacer.matchesAnchor(sample, entry, IslandObjectPlacer.AnchorFace.BOTTOM)) { + return false; + } + int bottomY = sample.bottomY(); + if (bottomY < 0 || Math.abs(bottomY - pickBottomY) > tolerance) { + return false; + } + } + return true; + } + + static int invertedBaseX(int minX, int pickedXf, FloatingObjectFootprint fp) { + return invertedBaseX(minX, pickedXf, fp, IrisObjectRotation.xFlip180()); + } + + static int invertedBaseY(int pickBottomY, FloatingObjectFootprint fp) { + return invertedBaseY(pickBottomY, fp, IrisObjectRotation.xFlip180()); + } + + static int invertedBaseZ(int minZ, int pickedZf, FloatingObjectFootprint fp) { + return invertedBaseZ(minZ, pickedZf, fp, IrisObjectRotation.xFlip180()); + } + + static int invertedBaseX(int minX, int pickedXf, FloatingObjectFootprint fp, IrisObjectRotation rotation) { + return minX + pickedXf - invertedFootprintAnchor(fp, rotation).getBlockX(); + } + + static int invertedBaseY(int pickBottomY, FloatingObjectFootprint fp, IrisObjectRotation rotation) { + return pickBottomY - 1 - invertedSolidAnchor(fp, rotation).getBlockY(); + } + + static int invertedBaseZ(int minZ, int pickedZf, FloatingObjectFootprint fp, IrisObjectRotation rotation) { + return minZ + pickedZf - invertedFootprintAnchor(fp, rotation).getBlockZ(); + } + + private static IrisBlockVector invertedFootprintAnchor(FloatingObjectFootprint fp, IrisObjectRotation rotation) { + return rotation.rotate(new IrisBlockVector(fp.getTallestKx(), 0, fp.getTallestKz()), 0, 0, 0); + } + + private static IrisBlockVector invertedSolidAnchor(FloatingObjectFootprint fp, IrisObjectRotation rotation) { + return rotation.rotate(new IrisBlockVector(fp.getTallestKx(), fp.getLowestSolidKeyY(), fp.getTallestKz()), 0, 0, 0); + } + + private static boolean shouldWritePlacementMarker(IObjectPlacer placer, PlatformBlockState state, int x, int y, int z) { + if (state == null) { + return false; + } + PlatformBlockState existingState = placer.get(x, y, z); + boolean wouldReplace = B.isSolid(existingState) && B.isVineBlock(state); + String material = IrisProceduralBlocks.materialKey(state); + boolean placesBlock = !material.equals("minecraft:air") && !material.equals("minecraft:cave_air") && !wouldReplace; + return state.isCustom() || placesBlock; + } + + static boolean isFootprintFlat(FloatingObjectFootprint fp, int pickedX, int pickedZ, int pickTopY, + IslandObjectPlacer.SampleProvider samples, + IrisFloatingChildBiomes entry, int tolerance) { + int tallestKx = fp.getTallestKx(); + int tallestKz = fp.getTallestKz(); + long[] cells = fp.footprintXZ(); + if (cells.length == 0) { + return false; + } + for (int i = 0, n = cells.length; i < n; i++) { + long encoded = cells[i]; + int kx = (int) (encoded >> 32); + int kz = (int) (encoded & 0xFFFFFFFFL); + int columnX = pickedX + (kx - tallestKx); + int columnZ = pickedZ + (kz - tallestKz); + FloatingIslandSample sample = samples.sample(columnX, columnZ); + if (!IslandObjectPlacer.matchesAnchor(sample, entry, IslandObjectPlacer.AnchorFace.TOP) + || Math.abs(sample.topY() - pickTopY) > tolerance) { + return false; + } + } + return true; + } + + private static KList interiorColumns(IslandObjectPlacer.SampleProvider samples, + KList columns, int minX, int minZ, + IrisFloatingChildBiomes entry, + IslandObjectPlacer.AnchorFace face) { + KList interior = new KList<>(); + for (int key : columns) { + int x = minX + (key & 15); + int z = minZ + (key >> 4); + if (!IslandObjectPlacer.matchesAnchor(samples.sample(x + 1, z), entry, face)) { + continue; + } + if (!IslandObjectPlacer.matchesAnchor(samples.sample(x - 1, z), entry, face)) { + continue; + } + if (!IslandObjectPlacer.matchesAnchor(samples.sample(x, z + 1), entry, face)) { + continue; + } + if (!IslandObjectPlacer.matchesAnchor(samples.sample(x, z - 1), entry, face)) { + continue; + } + interior.add(key); + } + return interior; + } + + private static String placementMarker(IrisObject object, int id) { + if (object == null) { + return null; + } + String key = object.getLoadKey(); + if (key == null || key.isEmpty() || key.equals("null")) { + return null; + } + return key + "@" + id; + } + + private static ObjectPlaceMode translateStiltModeForFloating(ObjectPlaceMode m) { + return switch (m) { + case STILT -> ObjectPlaceMode.MAX_HEIGHT; + case FAST_STILT -> ObjectPlaceMode.FAST_MAX_HEIGHT; + case MIN_STILT -> ObjectPlaceMode.MIN_HEIGHT; + case FAST_MIN_STILT -> ObjectPlaceMode.FAST_MIN_HEIGHT; + case CENTER_STILT -> ObjectPlaceMode.CENTER_HEIGHT; + case ERODE_STILT -> ObjectPlaceMode.MAX_HEIGHT; + case STRUCTURE_PIECE -> ObjectPlaceMode.CENTER_HEIGHT; + default -> m; + }; + } + + @Override + protected int computeRadius() { + int maxObjectExtent = 16; + Map sizeCache = new HashMap<>(); + Set warnedLargeObjects = new HashSet<>(); + try { + IrisData data = getData(); + for (IrisBiome biome : getDimension().getReachableBiomes(this::getData)) { + KList entries = biome.getFloatingChildBiomes(); + if (entries == null || entries.isEmpty()) { + continue; + } + for (IrisFloatingChildBiomes entry : entries) { + maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getFloatingObjects(), data, sizeCache, warnedLargeObjects)); + maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getExtraObjects(), data, sizeCache, warnedLargeObjects)); + maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getTopObjectOverrides(), data, sizeCache, warnedLargeObjects)); + maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getBottomObjectOverrides(), data, sizeCache, warnedLargeObjects)); + try { + IrisBiome target = entry.getRealBiome(biome, data); + if (target != null) { + maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects)); + maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects)); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + return maxObjectExtent; + } + + private int computePlacementRadius(KList placements, IrisData data, + Map sizeCache, + Set warnedLargeObjects) { + int radius = 0; + if (placements == null) { + return radius; + } + for (IrisObjectPlacement placement : placements) { + if (placement == null || placement.getPlace() == null) { + continue; + } + for (String objectKey : placement.getPlace()) { + try { + IrisBlockVector size = sizeCache.get(objectKey); + if (size == null) { + File file = data.getObjectLoader().findFile(objectKey); + if (file == null) { + continue; + } + size = IrisObject.sampleSize(file); + sizeCache.put(objectKey, size); + } + int reach = MantleObjectComponent.calculatePlacementReach(size, placement); + if (reach > 128 && warnedLargeObjects.add(objectKey)) { + IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!"); + } + radius = Math.max(radius, reach); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + return radius; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java new file mode 100644 index 000000000..c20a08ac7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -0,0 +1,1658 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.UpperDimensionContext; +import art.arcane.iris.engine.mantle.ComponentFlag; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.mantle.IrisMantleComponent; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.engine.object.CarvingMode; +import art.arcane.iris.engine.object.DecayControlPlacer; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.IrisAxisRotationClamp; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisCaveAnchorMode; +import art.arcane.iris.engine.object.IrisCaveProfile; +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.IrisObjectRotation; +import art.arcane.iris.engine.object.IrisObjectTranslate; +import art.arcane.iris.engine.object.IrisObjectVacuum; +import art.arcane.iris.engine.object.IrisProceduralObjects; +import art.arcane.iris.engine.object.IrisProceduralPlacement; +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.iris.spi.IrisLogging; +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.project.context.ChunkContext; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterStructurePOI; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.project.noise.NoiseType; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.IrisBlockVector; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +@ComponentFlag(ReservedFlag.OBJECT) +public class MantleObjectComponent extends IrisMantleComponent { + private static final long CAVE_REJECT_LOG_THROTTLE_MS = 5000L; + private static final int BEDROCK_CLEARANCE = 6; + private static final byte LIQUID_FORCED_AIR = 3; + private static final Map CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>(); + private static final Set MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet(); + + public MantleObjectComponent(EngineMantle engineMantle) { + super(engineMantle, ReservedFlag.OBJECT, 1); + } + + private static String placementMarker(IrisObject object, int id, String context) { + String key = object == null ? null : object.getLoadKey(); + if (key == null || key.isEmpty() || key.equals("null")) { + String fingerprint = context + "|" + (object == null ? "" : object.getClass().getSimpleName()); + if (MISSING_LOAD_KEY_WARNED.add(fingerprint)) { + java.io.File file = object == null ? null : object.getLoadFile(); + IrisLogging.warn("Skipping placement marker write: IrisObject has no loadKey (context=" + context + + ", file=" + (file == null ? "" : file.getPath()) + "). " + + "This would previously produce 'Couldn't find Object: null' warnings on chunk reload."); + } + return null; + } + return key + "@" + id; + } + + private static boolean isTreePlacement(IrisObject object, IrisObjectPlacement placement) { + String key = object == null ? null : object.getLoadKey(); + return (key != null && key.startsWith("trees/")) + || (placement != null && placement.getTrees() != null && placement.getTrees().isNotEmpty()); + } + + private static void writeTreeMaterial(IObjectPlacer placer, int x, int y, int z, PlatformBlockState data) { + placer.setData(x, y, z, TreeBlockMaterial.of(data)); + } + + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + IrisComplex complex = context.getComplex(); + boolean traceRegen = isRegenTraceThread(); + RNG rng = applyNoise(x, z, Cache.key(x, z) + seed()); + int xxx = 8 + (x << 4); + int zzz = 8 + (z << 4); + IrisRegion region = complex.getRegionStream().get(xxx, zzz); + IrisBiome surfaceBiome = complex.getTrueBiomeStream().get(xxx, zzz); + int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true); + IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome); + if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) { + int carvedBlocks = 0; + int minY = 1; + int maxY = Math.min(getEngineMantle().getEngine().getHeight() - 1, surfaceY - 14); + for (int sy = minY; sy < maxY; sy++) { + if (writer.isCarved(8 + (x << 4), sy, 8 + (z << 4))) { + carvedBlocks++; + } + } + IrisLogging.info("Cave object diag: chunk=" + x + "," + z + + " surfaceBiome=" + surfaceBiome.getLoadKey() + + " caveBiome=" + caveBiome.getLoadKey() + + " surfaceY=" + surfaceY + + " maxAnchorY=" + maxY + + " carvedAtCenter=" + carvedBlocks + + " biomeCarvingObjects=" + caveBiome.getCarvingObjects().size() + + " regionCarvingObjects=" + region.getCarvingObjects().size() + + " sameBiome=" + (caveBiome == surfaceBiome || java.util.Objects.equals(caveBiome.getLoadKey(), surfaceBiome.getLoadKey()))); + } + if (traceRegen) { + IrisLogging.info("Regen object layer start: chunk=" + x + "," + z + + " surfaceBiome=" + surfaceBiome.getLoadKey() + + " caveBiome=" + caveBiome.getLoadKey() + + " region=" + region.getLoadKey() + + " biomeSurfacePlacers=" + surfaceBiome.getSurfaceObjects().size() + + " biomeCavePlacers=" + caveBiome.getCarvingObjects().size() + + " regionSurfacePlacers=" + region.getSurfaceObjects().size() + + " regionCavePlacers=" + region.getCarvingObjects().size()); + } + ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen); + placeProceduralObjects(writer, rng, x, z, surfaceBiome, caveBiome, region); + UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext(); + IrisDimension dimension = getDimension(); + if (upperCtx != null && dimension.isUpperDimensionObjects()) { + placeUpperObjects(writer, rng, x, z, xxx, zzz, surfaceY, upperCtx, dimension, complex, traceRegen); + } + if (traceRegen) { + IrisLogging.info("Regen object layer done: chunk=" + x + "," + z + + " biomeSurfacePlacersChecked=" + summary.biomeSurfacePlacersChecked() + + " biomeSurfacePlacersTriggered=" + summary.biomeSurfacePlacersTriggered() + + " biomeCavePlacersChecked=" + summary.biomeCavePlacersChecked() + + " biomeCavePlacersTriggered=" + summary.biomeCavePlacersTriggered() + + " regionSurfacePlacersChecked=" + summary.regionSurfacePlacersChecked() + + " regionSurfacePlacersTriggered=" + summary.regionSurfacePlacersTriggered() + + " regionCavePlacersChecked=" + summary.regionCavePlacersChecked() + + " regionCavePlacersTriggered=" + summary.regionCavePlacersTriggered() + + " objectAttempts=" + summary.objectAttempts() + + " objectPlaced=" + summary.objectPlaced() + + " objectRejected=" + summary.objectRejected() + + " objectNull=" + summary.objectNull() + + " objectErrors=" + summary.objectErrors()); + } + } + + private RNG applyNoise(int x, int z, long seed) { + CNG noise = CNG.signatureFast(new RNG(seed), NoiseType.WHITE, NoiseType.GLOB); + return new RNG((long) (seed * noise.noise(x, z))); + } + + private IrisBiome resolveCaveObjectBiome(int x, int z, int surfaceY, IrisBiome surfaceBiome) { + int legacySampleY = Math.max(1, surfaceY - 48); + IrisBiome legacyCaveBiome = getEngineMantle().getEngine().getCaveBiome(x, legacySampleY, z); + if (legacyCaveBiome == null) { + legacyCaveBiome = surfaceBiome; + } + + int[] sampleDepths = new int[]{48, 80, 112}; + IrisBiome ladderChoice = null; + for (int sampleDepth : sampleDepths) { + int sampleY = Math.max(1, surfaceY - sampleDepth); + IrisBiome sampled = getEngineMantle().getEngine().getCaveBiome(x, sampleY, z); + boolean sameSurface = sampled == surfaceBiome; + if (!sameSurface && sampled != null && surfaceBiome != null) { + String sampledKey = sampled.getLoadKey(); + String surfaceKey = surfaceBiome.getLoadKey(); + sameSurface = sampledKey != null && sampledKey.equals(surfaceKey); + } + + if (sampled == null || sameSurface) { + continue; + } + + if (!sampled.getCarvingObjects().isEmpty()) { + ladderChoice = sampled; + } + } + + if (ladderChoice != null) { + return ladderChoice; + } + + return legacyCaveBiome; + } + + @ChunkCoordinates + private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, IrisComplex complex, boolean traceRegen) { + int biomeSurfaceChecked = 0; + int biomeSurfaceTriggered = 0; + int biomeCaveChecked = 0; + int biomeCaveTriggered = 0; + int regionSurfaceChecked = 0; + int regionSurfaceTriggered = 0; + int regionCaveChecked = 0; + int regionCaveTriggered = 0; + int attempts = 0; + int placed = 0; + int rejected = 0; + int nullObjects = 0; + int errors = 0; + IrisCaveProfile biomeCaveProfile = resolveCaveProfile(caveBiome.getCaveProfile(), region.getCaveProfile()); + IrisCaveProfile regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome.getCaveProfile()); + CaveAnchorCache caveAnchorCache = new CaveAnchorCache(); + + for (IrisObjectPlacement i : surfaceBiome.getSurfaceObjects()) { + biomeSurfaceChecked++; + boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); + if (traceRegen) { + IrisLogging.info("Regen object placer chance: chunk=" + x + "," + z + + " scope=biome-surface" + + " chanceResult=" + chance + + " chanceBase=" + i.getChance() + + " densityMid=" + i.getDensity() + + " objects=" + i.getPlace().size()); + } + if (chance) { + biomeSurfaceTriggered++; + try { + ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, complex, traceRegen, x, z, "biome-surface"); + attempts += result.attempts(); + placed += result.placed(); + rejected += result.rejected(); + nullObjects += result.nullObjects(); + errors += result.errors(); + } catch (Throwable e) { + errors++; + IrisLogging.reportError(e); + IrisLogging.error("Failed to place objects in the following biome: " + surfaceBiome.getName()); + IrisLogging.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); + IrisLogging.error("Are these objects missing?"); + e.printStackTrace(); + } + } + } + + for (IrisObjectPlacement i : caveBiome.getCarvingObjects()) { + if (!i.getCarvingSupport().supportsCarving()) { + continue; + } + biomeCaveChecked++; + boolean chance = rng.chance(i.getChance()); + if (traceRegen) { + IrisLogging.info("Regen object placer chance: chunk=" + x + "," + z + + " scope=biome-cave" + + " chanceResult=" + chance + + " chanceBase=" + i.getChance() + + " densityMid=" + i.getDensity() + + " objects=" + i.getPlace().size()); + } + if (chance) { + biomeCaveTriggered++; + try { + ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, biomeCaveProfile, complex, traceRegen, x, z, "biome-cave", caveBiome.getLoadKey(), caveAnchorCache); + attempts += result.attempts(); + placed += result.placed(); + rejected += result.rejected(); + nullObjects += result.nullObjects(); + errors += result.errors(); + } catch (Throwable e) { + errors++; + IrisLogging.reportError(e); + IrisLogging.error("Failed to place cave objects in the following biome: " + caveBiome.getName()); + IrisLogging.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); + IrisLogging.error("Are these objects missing?"); + e.printStackTrace(); + } + } + } + + for (IrisObjectPlacement i : region.getSurfaceObjects()) { + regionSurfaceChecked++; + boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); + if (traceRegen) { + IrisLogging.info("Regen object placer chance: chunk=" + x + "," + z + + " scope=region-surface" + + " chanceResult=" + chance + + " chanceBase=" + i.getChance() + + " densityMid=" + i.getDensity() + + " objects=" + i.getPlace().size()); + } + if (chance) { + regionSurfaceTriggered++; + try { + ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, complex, traceRegen, x, z, "region-surface"); + attempts += result.attempts(); + placed += result.placed(); + rejected += result.rejected(); + nullObjects += result.nullObjects(); + errors += result.errors(); + } catch (Throwable e) { + errors++; + IrisLogging.reportError(e); + IrisLogging.error("Failed to place objects in the following region: " + region.getName()); + IrisLogging.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); + IrisLogging.error("Are these objects missing?"); + e.printStackTrace(); + } + } + } + + for (IrisObjectPlacement i : region.getCarvingObjects()) { + if (!i.getCarvingSupport().supportsCarving()) { + continue; + } + regionCaveChecked++; + boolean chance = rng.chance(i.getChance()); + if (traceRegen) { + IrisLogging.info("Regen object placer chance: chunk=" + x + "," + z + + " scope=region-cave" + + " chanceResult=" + chance + + " chanceBase=" + i.getChance() + + " densityMid=" + i.getDensity() + + " objects=" + i.getPlace().size()); + } + if (chance) { + regionCaveTriggered++; + try { + ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, regionCaveProfile, complex, traceRegen, x, z, "region-cave", null, caveAnchorCache); + attempts += result.attempts(); + placed += result.placed(); + rejected += result.rejected(); + nullObjects += result.nullObjects(); + errors += result.errors(); + } catch (Throwable e) { + errors++; + IrisLogging.reportError(e); + IrisLogging.error("Failed to place cave objects in the following region: " + region.getName()); + IrisLogging.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); + IrisLogging.error("Are these objects missing?"); + e.printStackTrace(); + } + } + } + + return new ObjectPlacementSummary( + biomeSurfaceChecked, + biomeSurfaceTriggered, + biomeCaveChecked, + biomeCaveTriggered, + regionSurfaceChecked, + regionSurfaceTriggered, + regionCaveChecked, + regionCaveTriggered, + attempts, + placed, + rejected, + nullObjects, + errors + ); + } + + @ChunkCoordinates + private void placeProceduralObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region) { + IrisCaveProfile surfaceCaveProfile = resolveCaveProfile(surfaceBiome.getCaveProfile(), region.getCaveProfile()); + IrisCaveProfile regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome == null ? null : caveBiome.getCaveProfile()); + placeProceduralFrom(writer, rng, x, z, surfaceBiome.getProceduralObjects(), surfaceBiome.getName(), surfaceCaveProfile, surfaceBiome.getLoadKey()); + placeProceduralFrom(writer, rng, x, z, region.getProceduralObjects(), region.getName(), regionCaveProfile, null); + if (caveBiome != null && caveBiome != surfaceBiome) { + IrisCaveProfile caveProfile = resolveCaveProfile(caveBiome.getCaveProfile(), region.getCaveProfile()); + placeProceduralFrom(writer, rng, x, z, caveBiome.getProceduralObjects(), caveBiome.getName(), caveProfile, caveBiome.getLoadKey()); + } + } + + @ChunkCoordinates + private void placeProceduralFrom( + MantleWriter writer, + RNG rng, + int x, + int z, + IrisProceduralObjects proceduralObjects, + String scope, + IrisCaveProfile caveProfile, + String expectedCaveBiomeKey + ) { + if (proceduralObjects == null || proceduralObjects.isEmpty()) { + return; + } + + int blockX = x << 4; + int blockZ = z << 4; + boolean golden = GoldenDebugObjectPlacer.isGoldenDebugChunk(x, z); + CaveAnchorCache caveAnchorCache = new CaveAnchorCache(); + for (IrisProceduralPlacement p : proceduralObjects.getAllPlacements()) { + boolean treePlacement = p instanceof IrisProceduralTree; + boolean chancePassed = passesProceduralChance(rng, p.getChance()); + if (golden) { + IrisLogging.info("Goldendebug procedural chance: chunk=" + x + "," + z + + " scope=" + scope + + " placement=" + p.getName() + + " passed=" + chancePassed); + } + if (!chancePassed) { + continue; + } + + IrisObjectPlacement placement = p.asPlacement(); + boolean carving = placement.getCarvingSupport() == CarvingMode.CARVING_ONLY; + IrisCaveAnchorMode anchorMode = resolveAnchorMode(placement, caveProfile); + if (placement.getMode() == ObjectPlaceMode.CEILING_HANG) { + anchorMode = IrisCaveAnchorMode.CEILING; + } + int anchorScanStep = resolveAnchorScanStep(caveProfile); + int minDepthBelowSurface = resolveObjectMinDepthBelowSurface(caveProfile); + int anchorSearchAttempts = resolveAnchorSearchAttempts(caveProfile); + IObjectPlacer basePlacer = p.isPlausible() ? new DecayControlPlacer(writer) : writer; + IObjectPlacer placer = golden ? new GoldenDebugObjectPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer; + int density = Math.max(1, p.getDensity()); + for (int i = 0; i < density; i++) { + IrisObject variant = p.getVariantObject(getData(), rng); + if (variant == null) { + if (golden) { + IrisLogging.info("Goldendebug procedural pick: chunk=" + x + "," + z + + " placement=" + p.getName() + + " densityIndex=" + i + + " variant=null"); + } + continue; + } + CavePlacementAnchor caveAnchor = null; + if (carving) { + caveAnchor = findCavePlacementAnchor( + writer, + rng, + blockX, + blockZ, + anchorMode, + anchorScanStep, + minDepthBelowSurface, + anchorSearchAttempts, + expectedCaveBiomeKey, + placement.isUnderwater(), + caveAnchorCache + ); + } + if (carving && caveAnchor == null) { + if (golden) { + IrisLogging.info("Goldendebug procedural cave anchor rejected: chunk=" + x + "," + z + + " placement=" + p.getName() + + " minDepthBelowSurface=" + minDepthBelowSurface); + } + continue; + } + int xx = caveAnchor == null ? rng.i(blockX, blockX + 16) : caveAnchor.x(); + int zz = caveAnchor == null ? rng.i(blockZ, blockZ + 16) : caveAnchor.z(); + int id = rng.i(0, Integer.MAX_VALUE); + if (golden) { + KList pool = p.getVariantObjects(getData()); + IrisLogging.info("Goldendebug procedural pick: chunk=" + x + "," + z + + " placement=" + p.getName() + + " densityIndex=" + i + + " variant=" + variant.getLoadKey() + + " variantIndex=" + pool.indexOf(variant) + "/" + pool.size() + + " xx=" + xx + + " zz=" + zz + + " mode=" + placement.getMode() + + " carving=" + carving); + } + try { + int placeResult = -1; + CaveObjectPlacementTransaction.CommitResult commitResult = CaveObjectPlacementTransaction.CommitResult.EMPTY; + if (carving) { + int caveFloorY = caveAnchor.y(); + if (golden) { + IrisLogging.info("Goldendebug procedural caveFloor: chunk=" + x + "," + z + + " placement=" + p.getName() + + " xx=" + xx + + " zz=" + zz + + " caveFloorY=" + caveFloorY); + } + IrisObjectPlacement effectivePlacement = resolveCavePlacement(placement, variant, caveProfile); + ContainedPlacementResult contained = placeContainedCaveObject( + placer, + variant, + xx, + caveFloorY, + zz, + effectivePlacement, + minDepthBelowSurface, + id, + "procedural", + treePlacement, + rng + ); + placeResult = contained.resultY(); + commitResult = contained.commitResult(); + } else { + String marker = placementMarker(variant, id, "procedural"); + placeResult = variant.place(xx, -1, zz, placer, placement, rng, (b, data) -> { + if (marker != null) { + placer.setData(b.getX(), b.getY(), b.getZ(), marker); + } + if (treePlacement && marker != null) { + writeTreeMaterial(placer, b.getX(), b.getY(), b.getZ(), data); + } + }, null, getData()); + } + if (golden) { + IrisLogging.info("Goldendebug procedural result: chunk=" + x + "," + z + + " placement=" + p.getName() + + " variant=" + variant.getLoadKey() + + " xx=" + xx + + " zz=" + zz + + " resultY=" + placeResult + + " commit=" + commitResult); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Failed to place procedural object '" + p.getName() + "' in " + scope); + e.printStackTrace(); + } + } + } + } + + static boolean passesProceduralChance(RNG rng, double chance) { + if (chance <= 0.0) { + return false; + } + if (chance >= 1.0) { + return true; + } + return rng.chance(Math.max(0.0, Math.min(1.0, chance + rng.d(-0.005, 0.005)))); + } + + private CavePlacementAnchor findCavePlacementAnchor( + MantleWriter writer, + RNG rng, + int minX, + int minZ, + IrisCaveAnchorMode anchorMode, + int anchorScanStep, + int minDepthBelowSurface, + int searchAttempts, + String expectedCaveBiomeKey, + boolean underwater, + CaveAnchorCache anchorCache + ) { + for (int search = 0; search < searchAttempts; search++) { + int candidateX = rng.i(minX, minX + 16); + int candidateZ = rng.i(minZ, minZ + 16); + int candidateY = findCaveAnchorY( + writer, + rng, + candidateX, + candidateZ, + anchorMode, + anchorScanStep, + minDepthBelowSurface, + anchorCache + ); + if (candidateY < 0 + || caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey) + || !acceptsCaveAnchorFluid( + underwater, + writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class), + candidateY, + getDimension().getCaveLavaHeight())) { + continue; + } + return new CavePlacementAnchor(candidateX, candidateY, candidateZ); + } + return null; + } + + static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) { + if (cavern == null || !cavern.isCavern()) { + return false; + } + if (underwater || cavern.getLiquid() == LIQUID_FORCED_AIR) { + return true; + } + return cavern.isAir() && y > lavaHeight; + } + + private ContainedPlacementResult placeContainedCaveObject( + IObjectPlacer placer, + IrisObject object, + int x, + int anchorY, + int z, + IrisObjectPlacement placement, + int minDepthBelowSurface, + int id, + String markerContext, + boolean treePlacement, + RNG rng + ) { + CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(placer, anchorY, minDepthBelowSurface); + int placeY = anchorY; + if (placement.getMode() == ObjectPlaceMode.CEILING_HANG) { + placeY = Math.max(1, transaction.getCaveCeiling(x, z) - 1 - Math.floorDiv(object.getH(), 2)); + } + String marker = placementMarker(object, id, markerContext); + int result = object.place(x, placeY, z, transaction, placement, rng, (block, data) -> { + if (marker != null) { + transaction.setData(block.getX(), block.getY(), block.getZ(), marker); + } + if (treePlacement && marker != null) { + writeTreeMaterial(transaction, block.getX(), block.getY(), block.getZ(), data); + } + if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) { + transaction.setData(block.getX(), block.getY(), block.getZ(), MatterStructurePOI.BURIED_TREASURE); + } + }, null, getData()); + if (result < 0) { + transaction.discard(); + return new ContainedPlacementResult(result, CaveObjectPlacementTransaction.CommitResult.EMPTY); + } + return new ContainedPlacementResult(result, transaction.commit()); + } + + @BlockCoordinates + private ObjectPlacementResult placeObject( + MantleWriter writer, + RNG rng, + int x, + int z, + IrisObjectPlacement objectPlacement, + IrisComplex complex, + boolean traceRegen, + int chunkX, + int chunkZ, + String scope + ) { + int attempts = 0; + int placed = 0; + int rejected = 0; + int nullObjects = 0; + int errors = 0; + int density = objectPlacement.getDensity(rng, x, z, getData()); + boolean golden = GoldenDebugObjectPlacer.isGoldenDebugChunk(chunkX, chunkZ); + + for (int i = 0; i < density; i++) { + attempts++; + IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng)); + if (v == null) { + nullObjects++; + if (traceRegen) { + IrisLogging.warn("Regen object placement null object: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " densityIndex=" + i + + " density=" + density + + " placementKeys=" + objectPlacement.getPlace().toString(",")); + } + continue; + } + int xx = rng.i(x, x + 16); + int zz = rng.i(z, z + 16); + IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v); + boolean treePlacement = isTreePlacement(v, effectivePlacement); + int id = rng.i(0, Integer.MAX_VALUE); + IObjectPlacer placePlacer = golden ? new GoldenDebugObjectPlacer(writer, scope + "/" + v.getLoadKey()) : writer; + if (golden) { + IrisLogging.info("Goldendebug object attempt: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " object=" + v.getLoadKey() + + " densityIndex=" + i + + " xx=" + xx + + " zz=" + zz + + " mode=" + effectivePlacement.getMode()); + } + try { + String marker = placementMarker(v, id, "surface"); + int result = v.place(xx, -1, zz, placePlacer, effectivePlacement, rng, (b, data) -> { + if (marker != null) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } + if (treePlacement && marker != null) { + writeTreeMaterial(writer, b.getX(), b.getY(), b.getZ(), data); + } + if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) { + writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + } + }, null, getData()); + + if (result >= 0) { + placed++; + } else { + rejected++; + } + + if (golden) { + IrisLogging.info("Goldendebug object result: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " object=" + v.getLoadKey() + + " resultY=" + result + + " fallback=surface"); + } + + if (traceRegen) { + IrisLogging.info("Regen object placement result: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " object=" + v.getLoadKey() + + " resultY=" + result + + " px=" + xx + + " pz=" + zz + + " fallback=surface" + + " densityIndex=" + i + + " density=" + density); + } + } catch (Throwable e) { + errors++; + IrisLogging.reportError(e); + IrisLogging.error("Regen object placement exception: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " object=" + v.getLoadKey() + + " densityIndex=" + i + + " density=" + density + + " error=" + e.getClass().getSimpleName() + ":" + e.getMessage()); + } + } + + return new ObjectPlacementResult(attempts, placed, rejected, nullObjects, errors); + } + + @ChunkCoordinates + private boolean caveAnchorBiomeConflicts(int x, int y, int z, String expectedCaveBiomeKey) { + if (expectedCaveBiomeKey == null) { + return false; + } + Engine engine = getEngineMantle().getEngine(); + IrisBiome at = engine.getCaveBiome(x, y, z); + IrisBiome surface = engine.getSurfaceBiome(x, z); + return caveAnchorBiomeConflicts(at, surface, expectedCaveBiomeKey); + } + + static boolean caveAnchorBiomeConflicts(IrisBiome at, IrisBiome surface, String expectedCaveBiomeKey) { + if (expectedCaveBiomeKey == null || at == null) { + return false; + } + String atKey = at.getLoadKey(); + if (atKey == null || atKey.equals(expectedCaveBiomeKey)) { + return false; + } + if (surface != null && atKey.equals(surface.getLoadKey())) { + return false; + } + return true; + } + + private ObjectPlacementResult placeCaveObject( + MantleWriter writer, + RNG rng, + int chunkX, + int chunkZ, + IrisObjectPlacement objectPlacement, + IrisCaveProfile caveProfile, + IrisComplex complex, + boolean traceRegen, + int metricChunkX, + int metricChunkZ, + String scope, + String expectedCaveBiomeKey, + CaveAnchorCache anchorCache + ) { + int attempts = 0; + int placed = 0; + int rejected = 0; + int nullObjects = 0; + int errors = 0; + int minX = chunkX << 4; + int minZ = chunkZ << 4; + int density = objectPlacement.getDensity(rng, minX, minZ, getData()); + IrisCaveAnchorMode anchorMode = resolveAnchorMode(objectPlacement, caveProfile); + if (objectPlacement.getMode() == ObjectPlaceMode.CEILING_HANG) { + anchorMode = IrisCaveAnchorMode.CEILING; + } + int anchorScanStep = resolveAnchorScanStep(caveProfile); + int objectMinDepthBelowSurface = resolveObjectMinDepthBelowSurface(caveProfile); + int anchorSearchAttempts = resolveAnchorSearchAttempts(caveProfile); + + for (int i = 0; i < density; i++) { + attempts++; + IrisObject object = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng)); + if (object == null) { + nullObjects++; + if (traceRegen) { + IrisLogging.warn("Regen cave object placement null object: chunk=" + metricChunkX + "," + metricChunkZ + + " scope=" + scope + + " densityIndex=" + i + + " density=" + density + + " placementKeys=" + objectPlacement.getPlace().toString(",")); + } + logCaveReject( + scope, + "NULL_OBJECT", + metricChunkX, + metricChunkZ, + objectPlacement, + null, + i, + density, + anchorMode, + anchorSearchAttempts, + anchorScanStep, + objectMinDepthBelowSurface, + null, + null + ); + continue; + } + + CavePlacementAnchor anchor = findCavePlacementAnchor( + writer, + rng, + minX, + minZ, + anchorMode, + anchorScanStep, + objectMinDepthBelowSurface, + anchorSearchAttempts, + expectedCaveBiomeKey, + objectPlacement.isUnderwater(), + anchorCache + ); + + if (anchor == null) { + rejected++; + logCaveReject( + scope, + "NO_ANCHOR", + metricChunkX, + metricChunkZ, + objectPlacement, + object, + i, + density, + anchorMode, + anchorSearchAttempts, + anchorScanStep, + objectMinDepthBelowSurface, + null, + null + ); + continue; + } + + int x = anchor.x(); + int y = anchor.y(); + int z = anchor.z(); + + int id = rng.i(0, Integer.MAX_VALUE); + IrisObjectPlacement effectivePlacement = resolveCavePlacement(objectPlacement, object, caveProfile); + + try { + ContainedPlacementResult contained = placeContainedCaveObject( + writer, + object, + x, + y, + z, + effectivePlacement, + objectMinDepthBelowSurface, + id, + "cave", + isTreePlacement(object, effectivePlacement), + rng + ); + int result = contained.resultY(); + boolean wroteBlocks = contained.commitResult() == CaveObjectPlacementTransaction.CommitResult.COMMITTED; + if (wroteBlocks) { + placed++; + } else { + rejected++; + String rejectReason; + if (result < 0) { + rejectReason = "PLACE_NEGATIVE"; + } else if (contained.commitResult() == CaveObjectPlacementTransaction.CommitResult.REJECTED_BOUNDS) { + rejectReason = "CONTAINMENT"; + } else { + rejectReason = "NO_WRITES"; + } + logCaveReject( + scope, + rejectReason, + metricChunkX, + metricChunkZ, + objectPlacement, + object, + i, + density, + anchorMode, + anchorSearchAttempts, + anchorScanStep, + objectMinDepthBelowSurface, + y, + null + ); + } + + if (traceRegen) { + IrisLogging.info("Regen cave object placement result: chunk=" + metricChunkX + "," + metricChunkZ + + " scope=" + scope + + " object=" + object.getLoadKey() + + " resultY=" + result + + " anchorY=" + y + + " px=" + x + + " pz=" + z + + " wroteBlocks=" + wroteBlocks + + " densityIndex=" + i + + " density=" + density); + } + } catch (Throwable e) { + errors++; + IrisLogging.reportError(e); + IrisLogging.error("Regen cave object placement exception: chunk=" + metricChunkX + "," + metricChunkZ + + " scope=" + scope + + " object=" + object.getLoadKey() + + " densityIndex=" + i + + " density=" + density + + " error=" + e.getClass().getSimpleName() + ":" + e.getMessage()); + logCaveReject( + scope, + "EXCEPTION", + metricChunkX, + metricChunkZ, + objectPlacement, + object, + i, + density, + anchorMode, + anchorSearchAttempts, + anchorScanStep, + objectMinDepthBelowSurface, + y, + e + ); + } + } + + return new ObjectPlacementResult(attempts, placed, rejected, nullObjects, errors); + } + + @ChunkCoordinates + private void placeUpperObjects( + MantleWriter writer, + RNG rng, + int chunkX, + int chunkZ, + int centerX, + int centerZ, + int lowerSurfaceCenterY, + UpperDimensionContext upperCtx, + IrisDimension dimension, + IrisComplex complex, + boolean traceRegen + ) { + IrisBiome upperBiome = upperCtx.getUpperBiome(centerX, centerZ); + IrisRegion upperRegion = upperCtx.getUpperRegion(centerX, centerZ); + if (upperBiome == null && upperRegion == null) { + return; + } + + boolean forcePlace = dimension.isUpperObjectsForcePlace(); + if (upperBiome != null) { + for (IrisObjectPlacement i : upperBiome.getSurfaceObjects()) { + if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { + continue; + } + try { + placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, dimension, complex, forcePlace, traceRegen, "upper-biome-surface"); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Failed to place upper-dimension objects in biome " + upperBiome.getName() + + ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")"); + e.printStackTrace(); + } + } + } + + if (upperRegion != null) { + for (IrisObjectPlacement i : upperRegion.getSurfaceObjects()) { + if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { + continue; + } + try { + placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, dimension, complex, forcePlace, traceRegen, "upper-region-surface"); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Failed to place upper-dimension objects in region " + upperRegion.getName() + + ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")"); + e.printStackTrace(); + } + } + } + } + + @ChunkCoordinates + private void placeUpperObject( + MantleWriter writer, + RNG rng, + int chunkX, + int chunkZ, + IrisObjectPlacement objectPlacement, + UpperDimensionContext upperCtx, + IrisDimension dimension, + IrisComplex complex, + boolean forcePlace, + boolean traceRegen, + String scope + ) { + int chunkHeight = getEngineMantle().getEngine().getHeight(); + int upperGap = dimension.getUpperDimensionGap(); + int minX = chunkX << 4; + int minZ = chunkZ << 4; + int density = objectPlacement.getDensity(rng, minX, minZ, getData()); + + for (int i = 0; i < density; i++) { + IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng)); + if (v == null) { + continue; + } + + int xx = rng.i(minX, minX + 16); + int zz = rng.i(minZ, minZ + 16); + int columnLowerSurfaceY = getEngineMantle().getEngine().getHeight(xx, zz, true); + int rawUpperSurface = upperCtx.getUpperSurfaceY(xx, zz); + int upperSurfaceY = Math.max(rawUpperSurface, columnLowerSurfaceY + upperGap); + if (upperSurfaceY >= chunkHeight - 2) { + continue; + } + + int halfH = Math.floorDiv(v.getH(), 2); + int anchorY = upperSurfaceY - 1 - halfH; + if (anchorY <= 1) { + continue; + } + + int id = rng.i(0, Integer.MAX_VALUE); + IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey()); + placement.setMode(ObjectPlaceMode.CENTER_HEIGHT); + placement.setRotation(buildUpsideDownRotation()); + placement.setCarvingSupport(CarvingMode.ANYWHERE); + if (forcePlace) { + placement.setForcePlace(true); + } + boolean treePlacement = isTreePlacement(v, objectPlacement); + String marker = placementMarker(v, id, "upper"); + + int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> { + if (marker != null) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } + if (treePlacement && marker != null) { + writeTreeMaterial(writer, b.getX(), b.getY(), b.getZ(), data); + } + if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) { + writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + } + }, null, getData()); + + if (traceRegen) { + IrisLogging.info("Upper object placement: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " object=" + v.getLoadKey() + + " anchorY=" + anchorY + + " upperSurfaceY=" + upperSurfaceY + + " resultY=" + result + + " forcePlace=" + forcePlace); + } + } + } + + private IrisObjectRotation buildUpsideDownRotation() { + IrisObjectRotation rt = new IrisObjectRotation(); + rt.setEnabled(true); + rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D)); + rt.setYAxis(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D)); + rt.setZAxis(new IrisAxisRotationClamp()); + return rt; + } + + private void logCaveReject( + String scope, + String reason, + int chunkX, + int chunkZ, + IrisObjectPlacement objectPlacement, + IrisObject object, + int densityIndex, + int density, + IrisCaveAnchorMode anchorMode, + int anchorSearchAttempts, + int anchorScanStep, + int objectMinDepthBelowSurface, + Integer anchorY, + Throwable error + ) { + if (!IrisSettings.get().getGeneral().isDebug()) { + return; + } + + String placementKeys = objectPlacement == null ? "none" : objectPlacement.getPlace().toString(","); + String objectKey = object == null ? "null" : object.getLoadKey(); + String throttleKey = scope + "|" + reason + "|" + placementKeys + "|" + objectKey; + CaveRejectLogState state = CAVE_REJECT_LOG_STATE.computeIfAbsent(throttleKey, (k) -> new CaveRejectLogState()); + long now = System.currentTimeMillis(); + long last = state.lastLogMs.get(); + if ((now - last) < CAVE_REJECT_LOG_THROTTLE_MS) { + state.suppressed.incrementAndGet(); + return; + } + + if (!state.lastLogMs.compareAndSet(last, now)) { + state.suppressed.incrementAndGet(); + return; + } + + int suppressed = state.suppressed.getAndSet(0); + String anchorYText = anchorY == null ? "none" : String.valueOf(anchorY); + String errorText = error == null ? "none" : error.getClass().getSimpleName() + ":" + String.valueOf(error.getMessage()); + IrisLogging.warn("Cave object reject: scope=" + scope + + " reason=" + reason + + " chunk=" + chunkX + "," + chunkZ + + " object=" + objectKey + + " placements=" + placementKeys + + " densityIndex=" + densityIndex + + " density=" + density + + " anchorMode=" + anchorMode + + " anchorSearchAttempts=" + anchorSearchAttempts + + " anchorScanStep=" + anchorScanStep + + " minDepthBelowSurface=" + objectMinDepthBelowSurface + + " anchorY=" + anchorYText + + " forcePlace=" + (objectPlacement != null && objectPlacement.isForcePlace()) + + " carvingSupport=" + (objectPlacement == null ? "none" : objectPlacement.getCarvingSupport()) + + " bottom=" + (objectPlacement != null && objectPlacement.isBottom()) + + " suppressed=" + suppressed + + " error=" + errorText); + } + + private IrisObjectPlacement resolveEffectivePlacement(IrisObjectPlacement objectPlacement, IrisObject object) { + if (objectPlacement == null || object == null) { + return objectPlacement; + } + + return applyDimensionSurfaceSupport(resolveImportedPlacement(objectPlacement, object)); + } + + /** Dimension acts as the floor: it can force support on and widen the buffer, never weaken either. */ + private IrisObjectPlacement applyDimensionSurfaceSupport(IrisObjectPlacement placement) { + IrisDimension dimension = getDimension(); + boolean require = placement.isRequireSurfaceSupport() && dimension.isRequireObjectSurfaceSupport(); + int buffer = Math.max(placement.getSurfaceSupportBuffer(), dimension.getObjectSurfaceSupportBuffer()); + if (require == placement.isRequireSurfaceSupport() && buffer == placement.getSurfaceSupportBuffer()) { + return placement; + } + + IrisObjectPlacement resolved = placement.toPlacement(placement.getPlace().toArray(new String[0])); + resolved.setRequireSurfaceSupport(require); + resolved.setSurfaceSupportBuffer(buffer); + return resolved; + } + + private IrisObjectPlacement resolveImportedPlacement(IrisObjectPlacement objectPlacement, IrisObject object) { + String loadKey = object.getLoadKey(); + if (loadKey == null || loadKey.isBlank()) { + return objectPlacement; + } + + String normalized = loadKey.toLowerCase(Locale.ROOT); + boolean imported = normalized.startsWith("imports/") + || normalized.contains("/imports/") + || normalized.contains("imports/"); + + if (!imported) { + return objectPlacement; + } + + ObjectPlaceMode mode = objectPlacement.getMode(); + if (mode == ObjectPlaceMode.FLOATING || mode == ObjectPlaceMode.STRUCTURE_PIECE) { + return objectPlacement; + } + boolean needsModeChange = mode != ObjectPlaceMode.FAST_MIN_STILT; + if (!needsModeChange) { + return objectPlacement; + } + + IrisObjectPlacement effectivePlacement = objectPlacement.toPlacement(loadKey); + effectivePlacement.setMode(ObjectPlaceMode.FAST_MIN_STILT); + return effectivePlacement; + } + + private IrisObjectPlacement resolveCavePlacement(IrisObjectPlacement objectPlacement, IrisObject object, IrisCaveProfile caveProfile) { + IrisObjectPlacement resolvedPlacement = resolveEffectivePlacement(objectPlacement, object); + if (resolvedPlacement.getMode() != ObjectPlaceMode.CENTER_HEIGHT || caveProfile == null) { + return resolvedPlacement; + } + + ObjectPlaceMode profileMode = caveProfile.getDefaultObjectPlaceMode(); + if (profileMode == null) { + return resolvedPlacement; + } + + String loadKey = object.getLoadKey(); + if (loadKey == null || loadKey.isBlank()) { + resolvedPlacement = resolvedPlacement.toPlacement(); + } else { + resolvedPlacement = resolvedPlacement.toPlacement(loadKey); + } + resolvedPlacement.setMode(profileMode); + return resolvedPlacement; + } + + static int caveAnchorScanUpperBound(int worldHeight, int surfaceY, int minDepthBelowSurface) { + int maxAnchorY = CaveObjectPlacementTransaction.maxBuriedY(worldHeight, surfaceY, minDepthBelowSurface); + if (maxAnchorY <= 1) { + return 0; + } + return maxAnchorY + 1; + } + + private int findCaveAnchorY(MantleWriter writer, RNG rng, int x, int z, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, CaveAnchorCache anchorCache) { + KList anchors = anchorCache.get(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z); + if (anchors.isEmpty()) { + return -1; + } + + if (anchors.size() == 1) { + return anchors.get(0); + } + + return anchors.get(rng.i(anchors.size())); + } + + private KList scanCaveAnchorColumn(MantleWriter writer, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, int x, int z, CaveAnchorCache anchorCache) { + int height = getEngineMantle().getEngine().getHeight(); + int step = Math.max(1, anchorScanStep); + int surfaceY = anchorCache.getSurfaceHeight(x, z); + int maxAnchorExclusive = caveAnchorScanUpperBound(height, surfaceY, objectMinDepthBelowSurface); + if (maxAnchorExclusive == 0) { + return new KList<>(); + } + + int carvedHeight = Math.min(height, maxAnchorExclusive + 3); + byte[] carvedColumn = anchorCache.getCarvedColumn(writer, x, z, carvedHeight); + return scanCaveAnchorRange(anchorMode, step, carvedHeight, BEDROCK_CLEARANCE, maxAnchorExclusive, carvedColumn); + } + + private KList scanCaveAnchorRange(IrisCaveAnchorMode anchorMode, int step, int height, int minAnchorY, int maxAnchorY, byte[] carvedColumn) { + KList anchors = new KList<>(); + int startY = alignAnchorScanStart(minAnchorY, step); + for (int y = startY; y < maxAnchorY; y += step) { + if (!isCarved(carvedColumn, y)) { + continue; + } + + boolean solidBelow = hasSolidNeighbor(carvedColumn, y, height, -1); + boolean solidAbove = hasSolidNeighbor(carvedColumn, y, height, 1); + if (matchesCaveAnchor(anchorMode, solidBelow, solidAbove)) { + anchors.add(y); + } + } + return anchors; + } + + private int alignAnchorScanStart(int minAnchorY, int step) { + if (minAnchorY <= BEDROCK_CLEARANCE) { + return BEDROCK_CLEARANCE; + } + + int delta = minAnchorY - BEDROCK_CLEARANCE; + int steps = (delta + step - 1) / step; + return BEDROCK_CLEARANCE + (steps * step); + } + + private boolean isCarved(byte[] carvedColumn, int y) { + return y >= 0 && y < carvedColumn.length && carvedColumn[y] == 1; + } + + private boolean hasSolidNeighbor(byte[] carvedColumn, int y, int height, int direction) { + for (int d = 1; d <= 3; d++) { + int ny = y + (direction * d); + if (ny < 0 || ny >= height) { + return true; + } + if (!isCarved(carvedColumn, ny)) { + return true; + } + } + return false; + } + + private boolean matchesCaveAnchor(IrisCaveAnchorMode anchorMode, boolean solidBelow, boolean solidAbove) { + return switch (anchorMode) { + case PROFILE_DEFAULT, FLOOR -> solidBelow; + case CEILING -> solidAbove; + case CENTER -> !solidBelow && !solidAbove; + case ANY -> true; + }; + } + + private IrisCaveProfile resolveCaveProfile(IrisCaveProfile preferred, IrisCaveProfile secondary) { + IrisCaveProfile dimensionProfile = getDimension().getCaveProfile(); + if (preferred != null && preferred.isEnabled()) { + return preferred; + } + + if (secondary != null && secondary.isEnabled()) { + return secondary; + } + + if (dimensionProfile != null) { + return dimensionProfile; + } + + return new IrisCaveProfile(); + } + + private IrisCaveAnchorMode resolveAnchorMode(IrisObjectPlacement objectPlacement, IrisCaveProfile caveProfile) { + IrisCaveAnchorMode placementMode = objectPlacement.getCaveAnchorMode(); + if (placementMode != null && !placementMode.equals(IrisCaveAnchorMode.PROFILE_DEFAULT)) { + return placementMode; + } + + if (caveProfile == null) { + return IrisCaveAnchorMode.FLOOR; + } + + IrisCaveAnchorMode profileMode = caveProfile.getDefaultObjectAnchor(); + if (profileMode == null || profileMode.equals(IrisCaveAnchorMode.PROFILE_DEFAULT)) { + return IrisCaveAnchorMode.FLOOR; + } + + return profileMode; + } + + private int resolveAnchorScanStep(IrisCaveProfile caveProfile) { + if (caveProfile == null) { + return 1; + } + + return Math.max(1, caveProfile.getAnchorScanStep()); + } + + private int resolveObjectMinDepthBelowSurface(IrisCaveProfile caveProfile) { + if (caveProfile == null) { + return 6; + } + + return Math.max(0, caveProfile.getObjectMinDepthBelowSurface()); + } + + private int resolveAnchorSearchAttempts(IrisCaveProfile caveProfile) { + if (caveProfile == null) { + return 6; + } + + return Math.max(1, caveProfile.getAnchorSearchAttempts()); + } + + private boolean isRegenTraceThread() { + return Thread.currentThread().getName().startsWith("Iris-Regen-") + && IrisSettings.get().getGeneral().isDebug(); + } + + private record ObjectPlacementSummary( + int biomeSurfacePlacersChecked, + int biomeSurfacePlacersTriggered, + int biomeCavePlacersChecked, + int biomeCavePlacersTriggered, + int regionSurfacePlacersChecked, + int regionSurfacePlacersTriggered, + int regionCavePlacersChecked, + int regionCavePlacersTriggered, + int objectAttempts, + int objectPlaced, + int objectRejected, + int objectNull, + int objectErrors + ) { + } + + private record ObjectPlacementResult(int attempts, int placed, int rejected, int nullObjects, int errors) { + } + + private record CavePlacementAnchor(int x, int y, int z) { + } + + private record ContainedPlacementResult( + int resultY, + CaveObjectPlacementTransaction.CommitResult commitResult + ) { + } + + private static final class CaveRejectLogState { + private final AtomicLong lastLogMs = new AtomicLong(0L); + private final AtomicInteger suppressed = new AtomicInteger(0); + } + + @BlockCoordinates + private Set guessPlacedKeys(RNG rng, int x, int z, IrisObjectPlacement objectPlacement) { + Set f = new KSet<>(); + for (int i = 0; i < objectPlacement.getDensity(rng, x, z, getData()); i++) { + IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng)); + if (v == null) { + continue; + } + + f.add(v.getLoadKey()); + } + + return f; + } + + public Set guess(int x, int z) { + // todo The guess doesnt bring into account that the placer may return -1 + RNG rng = applyNoise(x, z, Cache.key(x, z) + seed()); + IrisBiome biome = getEngineMantle().getEngine().getSurfaceBiome((x << 4) + 8, (z << 4) + 8); + IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8); + Set v = new KSet<>(); + for (IrisObjectPlacement i : biome.getSurfaceObjects()) { + if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { + v.addAll(guessPlacedKeys(rng, x, z, i)); + } + } + + for (IrisObjectPlacement i : region.getSurfaceObjects()) { + if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { + v.addAll(guessPlacedKeys(rng, x, z, i)); + } + } + + return v; + } + + protected int computeRadius() { + IrisDimension dimension = getDimension(); + KMap sizeCache = new KMap<>(); + KSet warnedLargeObjects = new KSet<>(); + int radius = 0; + for (IrisRegion region : dimension.getAllRegions(this::getData)) { + if (region == null) { + continue; + } + radius = Math.max(radius, computePlacementRadius(region.getObjects(), sizeCache, warnedLargeObjects)); + radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects())); + } + for (IrisBiome biome : dimension.getReachableBiomes(this::getData)) { + if (biome == null) { + continue; + } + radius = Math.max(radius, computePlacementRadius(biome.getObjects(), sizeCache, warnedLargeObjects)); + radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects())); + } + + return radius; + } + + private int computePlacementRadius( + KList placements, + KMap sizeCache, + KSet warnedLargeObjects + ) { + int radius = 0; + for (IrisObjectPlacement placement : placements) { + if (placement == null) { + continue; + } + for (String objectKey : placement.getPlace()) { + try { + IrisBlockVector size = loadObjectSize(sizeCache, objectKey); + if (size == null) { + continue; + } + int reach = calculatePlacementReach(size, placement); + if (reach > 128 && warnedLargeObjects.add(objectKey)) { + IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!"); + } + radius = Math.max(radius, reach); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + return radius; + } + + private int computeProceduralRadius(IrisProceduralObjects procedural) { + if (procedural == null || procedural.isEmpty()) { + return 0; + } + int radius = 0; + for (IrisProceduralPlacement placement : procedural.getAllPlacements()) { + if (placement == null) { + continue; + } + KList variants = placement.getVariantObjects(getData()); + if (variants == null) { + continue; + } + IrisObjectPlacement objectPlacement = placement.asPlacement(); + for (IrisObject variant : variants) { + if (variant == null) { + continue; + } + IrisBlockVector size = new IrisBlockVector(variant.getW(), variant.getH(), variant.getD()); + radius = Math.max(radius, calculatePlacementReach(size, objectPlacement)); + } + } + return radius; + } + + static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement) { + if (size == null) { + return 0; + } + if (placement == null) { + return Math.max(Math.abs(size.getBlockX()), Math.abs(size.getBlockZ())); + } + + double scale = placement.getScale() != null ? Math.max(1D, placement.getScale().getMaxScale()) : 1D; + int width = scaledDimension(size.getBlockX(), scale); + int height = scaledDimension(size.getBlockY(), scale); + int depth = scaledDimension(size.getBlockZ(), scale); + IrisObjectRotation rotation = placement.getRotation(); + boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled(); + boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled(); + int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth); + int translation = calculateTranslationReach(placement.getTranslate(), rotation, rotateX || rotateZ); + int warp = placement.getWarp() != null && !placement.getWarp().isFlat() + ? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D) + : 0; + int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode()) + ? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings()) + : 0; + long reach = (long) footprint + translation + warp + vacuum; + return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach; + } + + private static int scaledDimension(int dimension, double scale) { + int absoluteDimension = Math.abs(dimension); + if (scale <= 1D) { + return absoluteDimension; + } + return (int) Math.ceil((absoluteDimension * scale) + (scale * 2D)); + } + + private static int calculateTranslationReach(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) { + if (translate == null || !translate.canTranslate()) { + return 0; + } + double x = translate.getX(); + double y = translate.getY(); + double z = translate.getZ(); + if (rotateVertically) { + return (int) Math.ceil(Math.sqrt((x * x) + (y * y) + (z * z))); + } + boolean rotateY = rotation != null && rotation.isEnabled() && rotation.getYAxis() != null && rotation.getYAxis().isEnabled(); + return rotateY ? (int) Math.ceil(Math.hypot(x, z)) : (int) Math.max(Math.abs(x), Math.abs(z)); + } + + private IrisBlockVector loadObjectSize(KMap sizeCache, String objectKey) { + return sizeCache.computeIfAbsent(objectKey, k -> { + try { + return IrisObject.sampleSize(getData().getObjectLoader().findFile(objectKey)); + } catch (IOException e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + + return null; + }); + } + + private final class CaveAnchorCache { + private final Long2ObjectOpenHashMap>> settingCaches; + private final Long2ObjectOpenHashMap carvedColumns; + private final Long2IntOpenHashMap surfaceHeights; + + private CaveAnchorCache() { + this.settingCaches = new Long2ObjectOpenHashMap<>(); + this.carvedColumns = new Long2ObjectOpenHashMap<>(); + this.surfaceHeights = new Long2IntOpenHashMap(); + this.surfaceHeights.defaultReturnValue(Integer.MIN_VALUE); + } + + private KList get(MantleWriter writer, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, int x, int z) { + if (anchorScanStep > 65535 || objectMinDepthBelowSurface > 65535) { + return scanCaveAnchorColumn(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z, this); + } + + long settingKey = ((long) anchorMode.ordinal() << 32) | ((long) anchorScanStep << 16) | objectMinDepthBelowSurface; + Long2ObjectOpenHashMap> columnCache = settingCaches.get(settingKey); + if (columnCache == null) { + columnCache = new Long2ObjectOpenHashMap<>(); + settingCaches.put(settingKey, columnCache); + } + + long columnKey = Cache.key(x, z); + KList anchors = columnCache.get(columnKey); + if (anchors != null) { + return anchors; + } + + anchors = scanCaveAnchorColumn(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z, this); + columnCache.put(columnKey, anchors); + return anchors; + } + + private byte[] getCarvedColumn(MantleWriter writer, int x, int z, int height) { + long columnKey = Cache.key(x, z); + byte[] carvedColumn = carvedColumns.get(columnKey); + if (carvedColumn != null && carvedColumn.length == height) { + return carvedColumn; + } + + carvedColumn = writer.getCarvedColumn(x, z, height); + carvedColumns.put(columnKey, carvedColumn); + return carvedColumn; + } + + private int getSurfaceHeight(int x, int z) { + long columnKey = Cache.key(x, z); + int surfaceY = surfaceHeights.get(columnKey); + if (surfaceY != Integer.MIN_VALUE) { + return surfaceY; + } + + surfaceY = getEngineMantle().getEngine().getHeight(x, z); + surfaceHeights.put(columnKey, surfaceY); + return surfaceY; + } + } + +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/StructureCarveEnvelope.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/StructureCarveEnvelope.java new file mode 100644 index 000000000..4ae24cd62 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/StructureCarveEnvelope.java @@ -0,0 +1,147 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.engine.object.IrisStructureCarveShape; +import art.arcane.iris.util.project.noise.CNG; + +/** + * Carve envelope math shared by the Iris-assembled structure path and the native (vanilla template) + * structure path so both shrinkwrap a structure footprint the same way. The lobe channel and the + * eroded floor are only sampled by the native path; the Iris-assembled path still carves a uniform + * side reach and a uniform floor cut. + */ +public final class StructureCarveEnvelope { + private static final double CEILING_LOBE_RATIO = 0.5D; + private static final double CEILING_ROLL_FREQUENCY_RATIO = 3D / 7D; + private static final double FLOOR_ROLL_AMPLITUDE = 0.35D; + private static final double FLOOR_ROLL_FREQUENCY_RATIO = 5D / 11D; + // Maps a lobe frequency onto the smooth 2D channel so the auto-derived 0.015 lands near a + // 40 block wavelength: wide enough that walls bulge and recede instead of tracking the footprint. + private static final double LOBE_FREQUENCY_SCALE = 256D; + + private StructureCarveEnvelope() { + } + + /** + * Per-column horizontal carve reach. The lobe channel only ever removes padding, so the boundary + * stays inside the footprint bounds the carve was sized against and never falls below + * {@code horizontalPadding * (1 - lobeStrength)}. + */ + public static double lobedSideReach(CNG lobe, double lobeFrequency, double lobeStrength, + int x, int z, int horizontalPadding) { + if (horizontalPadding <= 0) { + return 0D; + } + return horizontalPadding * lobeFactor(lobe, lobeFrequency, lobeStrength, x, z); + } + + /** + * Ceiling counterpart of {@link #lobedSideReach}, deliberately half as strong so the roof swells + * with the same lobes without collapsing onto the pieces. + */ + public static double lobedUpReach(CNG lobe, double lobeFrequency, double lobeStrength, + int x, int z, double upReach) { + if (upReach <= 0D) { + return upReach; + } + return Math.max(1D, upReach + * lobeFactor(lobe, lobeFrequency, lobeStrength * CEILING_LOBE_RATIO, x, z)); + } + + private static double lobeFactor(CNG lobe, double lobeFrequency, double lobeStrength, + int x, int z) { + double clampedStrength = Math.max(0D, Math.min(1D, lobeStrength)); + if (lobe == null || clampedStrength <= 0D) { + return 1D; + } + double scaledFrequency = lobeFrequency * LOBE_FREQUENCY_SCALE; + double sample = Math.max(0D, Math.min(1D, lobe.fitDouble( + 0D, 1D, x * scaledFrequency, z * scaledFrequency))); + return 1D - clampedStrength * (1D - sample); + } + + public static double erodedUpReach(CNG roll, double frequency, double strength, int x, int z, int head) { + if (head <= 0) { + return 0D; + } + double clampedStrength = Math.max(0D, Math.min(1D, strength)); + if (clampedStrength <= 0D) { + return Math.max(1D, head); + } + double rollFrequency = frequency * CEILING_ROLL_FREQUENCY_RATIO; + double sample = roll.fitDouble(0D, 1D, x * rollFrequency, z * rollFrequency) * 0.7D + + roll.fitDouble(0D, 1D, x * rollFrequency * 3D, z * rollFrequency * 3D) * 0.3D; + double contrast = Math.max(0D, Math.min(1D, (sample - 0.5D) * 2.6D + 0.5D)); + double roundedReach = Math.max(1D, head); + double erodedReach = Math.max(1D, head * (0.4D + 1.4D * contrast)); + return roundedReach + clampedStrength * (erodedReach - roundedReach); + } + + /** + * Floor counterpart of {@link #erodedUpReach}. The amplitude is deliberately small and the result + * never exceeds the configured floor padding, so a padding of zero leaves every column standing on + * its own supporting block instead of undermining the structure. + */ + public static double erodedDownReach(CNG roll, double frequency, double strength, int x, int z, int floorCut) { + if (floorCut <= 0) { + return 0D; + } + double clampedStrength = Math.max(0D, Math.min(1D, strength)); + if (clampedStrength <= 0D) { + return floorCut; + } + double rollFrequency = frequency * FLOOR_ROLL_FREQUENCY_RATIO; + double sample = roll.fitDouble(0D, 1D, x * rollFrequency, z * rollFrequency); + return floorCut * (1D - clampedStrength * FLOOR_ROLL_AMPLITUDE * (1D - sample)); + } + + public static double normalizedVerticalDistance(int y, int sourceMinY, int sourceMaxY, + double upReach, double downReach) { + if (y > sourceMaxY) { + return (y - sourceMaxY) / upReach; + } + if (y < sourceMinY) { + return (sourceMinY - y) / downReach; + } + return 0D; + } + + public static double overboreBoundaryLimit(double noise, double strength) { + double clampedNoise = Math.max(0.0, Math.min(1.0, noise)); + double clampedStrength = Math.max(0.0, Math.min(1.0, strength)); + return 1D - clampedStrength * (1D - clampedNoise); + } + + public static boolean shouldCarveOverboreCell(IrisStructureCarveShape shape, double distanceSquared, + double noise, double strength) { + if (distanceSquared <= 0.0) { + return true; + } + IrisStructureCarveShape resolvedShape = shape == null ? IrisStructureCarveShape.ERODED : shape; + return switch (resolvedShape) { + case BOX -> true; + case ROUNDED -> distanceSquared <= 1D; + case ERODED -> { + double limit = overboreBoundaryLimit(noise, strength); + yield distanceSquared <= 1D && distanceSquared <= limit * limit; + } + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/StructureCarvingFootprint.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/StructureCarvingFootprint.java new file mode 100644 index 000000000..1068a6178 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/StructureCarvingFootprint.java @@ -0,0 +1,456 @@ +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.engine.framework.PlacedStructurePiece; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.volmlib.util.collection.KList; +import it.unimi.dsi.fastutil.longs.Long2LongMap; +import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; + +import java.util.Arrays; + +public final class StructureCarvingFootprint { + static final int DEFAULT_MAX_CELLS = 1_048_576; + + private final int minX; + private final int maxX; + private final int minZ; + private final int maxZ; + private final int width; + private final int depth; + private final long[] distanceSquared; + private final int[] nearestSourceIds; + private final int[] sourceMinY; + private final int[] sourceMaxY; + + private StructureCarvingFootprint(FootprintData data) { + minX = data.minX(); + maxX = data.maxX(); + minZ = data.minZ(); + maxZ = data.maxZ(); + width = data.width(); + depth = data.depth(); + distanceSquared = data.distanceSquared(); + nearestSourceIds = data.nearestSourceIds(); + sourceMinY = data.sourceMinY(); + sourceMaxY = data.sourceMaxY(); + } + + static StructureCarvingFootprint from(KList pieces, int padding) { + return from(pieces, padding, DEFAULT_MAX_CELLS); + } + + static StructureCarvingFootprint from(KList pieces, int padding, int maxCells) { + return fromColumns(sink -> emitPieceColumns(pieces, sink), padding, maxCells); + } + + /** + * Builds a footprint from any per-column occupancy source. The source reports one contribution per + * occupied column; overlapping contributions merge into a single column whose vertical extents span + * every contribution. + */ + public static StructureCarvingFootprint fromColumns(ColumnSource source, int padding, int maxCells) { + if (padding < 0) { + throw new IllegalArgumentException("padding must be non-negative"); + } + if (maxCells < 1) { + throw new IllegalArgumentException("maxCells must be positive"); + } + if (source == null) { + return null; + } + + SourceCollection sources = collectSources(source); + if (sources == null || sources.columns().isEmpty()) { + return null; + } + + Bounds bounds = paddedBounds(sources.bounds(), padding, maxCells); + if (bounds == null) { + return null; + } + + int[] nearestSourceIds = new int[bounds.cellCount()]; + Arrays.fill(nearestSourceIds, -1); + int sourceCount = sources.columns().size(); + int[] sourceX = new int[sourceCount]; + int[] sourceZ = new int[sourceCount]; + int[] sourceMinY = new int[sourceCount]; + int[] sourceMaxY = new int[sourceCount]; + int sourceId = 0; + for (Long2LongMap.Entry entry : sources.columns().long2LongEntrySet()) { + int localX = unpackX(entry.getLongKey()) - bounds.minX(); + int localZ = unpackZ(entry.getLongKey()) - bounds.minZ(); + long heights = entry.getLongValue(); + sourceX[sourceId] = localX; + sourceZ[sourceId] = localZ; + sourceMinY[sourceId] = unpackMinY(heights); + sourceMaxY[sourceId] = unpackMaxY(heights); + nearestSourceIds[localZ * bounds.width() + localX] = sourceId; + sourceId++; + } + + transformRows(nearestSourceIds, sourceX, sourceZ, bounds.width(), bounds.depth()); + long[] distanceSquared = transformColumns( + nearestSourceIds, sourceX, sourceZ, bounds.width(), bounds.depth()); + FootprintData data = new FootprintData( + bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(), + bounds.width(), bounds.depth(), distanceSquared, nearestSourceIds, + sourceMinY, sourceMaxY); + return new StructureCarvingFootprint(data); + } + + public int minX() { + return minX; + } + + public int maxX() { + return maxX; + } + + public int minZ() { + return minZ; + } + + public int maxZ() { + return maxZ; + } + + public int width() { + return width; + } + + public int depth() { + return depth; + } + + public boolean contains(int worldX, int worldZ) { + return worldX >= minX && worldX <= maxX && worldZ >= minZ && worldZ <= maxZ; + } + + public int indexAt(int worldX, int worldZ) { + if (!contains(worldX, worldZ)) { + return -1; + } + return (worldZ - minZ) * width + worldX - minX; + } + + public long distanceSquaredAt(int worldX, int worldZ) { + return distanceSquaredAt(requireIndex(worldX, worldZ)); + } + + public long distanceSquaredAt(int index) { + return distanceSquared[index]; + } + + public int sourceMinYAt(int worldX, int worldZ) { + return sourceMinYAt(requireIndex(worldX, worldZ)); + } + + public int sourceMinYAt(int index) { + return sourceMinY[nearestSourceIds[index]]; + } + + public int sourceMaxYAt(int worldX, int worldZ) { + return sourceMaxYAt(requireIndex(worldX, worldZ)); + } + + public int sourceMaxYAt(int index) { + return sourceMaxY[nearestSourceIds[index]]; + } + + Column sample(int worldX, int worldZ) { + int index = indexAt(worldX, worldZ); + if (index < 0) { + return null; + } + return new Column(distanceSquaredAt(index), sourceMinYAt(index), sourceMaxYAt(index)); + } + + private static SourceCollection collectSources(ColumnSource source) { + Long2LongOpenHashMap columns = new Long2LongOpenHashMap(); + MutableBounds bounds = new MutableBounds(); + boolean usable = source.emit((worldX, worldZ, minY, maxY) -> { + mergeColumn(columns, worldX, worldZ, minY, maxY); + bounds.include(worldX, worldZ); + }); + if (!usable) { + return null; + } + return columns.isEmpty() ? null : new SourceCollection(columns, bounds.freeze()); + } + + private static boolean emitPieceColumns(KList pieces, ColumnSink sink) { + if (pieces == null) { + return true; + } + for (PlacedStructurePiece piece : pieces) { + if (piece == null) { + continue; + } + IrisObject object = piece.getObject(); + IrisObjectRotation rotation = piece.getRotation(); + if (object == null || rotation == null || object.getBlocks() == null) { + continue; + } + for (IrisBlockVector local : object.getBlocks().keys()) { + if (local == null || B.isAir(object.getBlocks().get(local))) { + continue; + } + IrisBlockVector rotated = rotation.rotate(local.clone()); + long worldX = (long) piece.getX() + rotated.getBlockX(); + long worldY = (long) piece.getY() + rotated.getBlockY(); + long worldZ = (long) piece.getZ() + rotated.getBlockZ(); + if (!fitsInteger(worldX) || !fitsInteger(worldY) || !fitsInteger(worldZ)) { + return false; + } + sink.column((int) worldX, (int) worldZ, (int) worldY, (int) worldY); + } + } + return true; + } + + private static Bounds paddedBounds(MutableBoundsSnapshot sourceBounds, int padding, int maxCells) { + long paddedMinX = (long) sourceBounds.minX() - padding; + long paddedMaxX = (long) sourceBounds.maxX() + padding; + long paddedMinZ = (long) sourceBounds.minZ() - padding; + long paddedMaxZ = (long) sourceBounds.maxZ() + padding; + if (!fitsInteger(paddedMinX) || !fitsInteger(paddedMaxX) + || !fitsInteger(paddedMinZ) || !fitsInteger(paddedMaxZ)) { + return null; + } + long width = paddedMaxX - paddedMinX + 1L; + long depth = paddedMaxZ - paddedMinZ + 1L; + if (width < 1L || depth < 1L || width > Integer.MAX_VALUE || depth > Integer.MAX_VALUE + || width > maxCells / depth) { + return null; + } + int cellCount = (int) (width * depth); + return new Bounds( + (int) paddedMinX, (int) paddedMaxX, (int) paddedMinZ, (int) paddedMaxZ, + (int) width, (int) depth, cellCount); + } + + private static void mergeColumn(Long2LongOpenHashMap columns, int x, int z, int minY, int maxY) { + long key = pack(x, z); + if (!columns.containsKey(key)) { + columns.put(key, packHeights(minY, maxY)); + return; + } + long existing = columns.get(key); + columns.put(key, packHeights( + Math.min(minY, unpackMinY(existing)), Math.max(maxY, unpackMaxY(existing)))); + } + + private static void transformRows(int[] nearestSourceIds, int[] sourceX, int[] sourceZ, + int width, int depth) { + for (int z = 0; z < depth; z++) { + int rowStart = z * width; + int previousSource = -1; + for (int x = 0; x < width; x++) { + int index = rowStart + x; + int sourceId = nearestSourceIds[index]; + if (isExplicitSource(sourceId, x, z, sourceX, sourceZ)) { + previousSource = sourceId; + } else { + nearestSourceIds[index] = previousSource; + } + } + int nextSource = -1; + for (int x = width - 1; x >= 0; x--) { + int index = rowStart + x; + int currentSource = nearestSourceIds[index]; + if (isExplicitSource(currentSource, x, z, sourceX, sourceZ)) { + nextSource = currentSource; + } + if (isBetterHorizontalSource(nextSource, currentSource, x, sourceX)) { + nearestSourceIds[index] = nextSource; + } + } + } + } + + private static long[] transformColumns(int[] nearestSourceIds, int[] sourceX, int[] sourceZ, + int width, int depth) { + long[] distanceSquared = new long[nearestSourceIds.length]; + int[] siteRows = new int[depth]; + int[] siteSourceIds = new int[depth]; + int[] starts = new int[depth]; + long[] siteCosts = new long[depth]; + for (int x = 0; x < width; x++) { + int siteCount = buildEnvelope( + nearestSourceIds, sourceX, width, depth, x, + siteRows, siteSourceIds, starts, siteCosts); + int siteIndex = 0; + for (int z = 0; z < depth; z++) { + while (siteIndex + 1 < siteCount && starts[siteIndex + 1] <= z) { + siteIndex++; + } + int sourceId = siteSourceIds[siteIndex]; + int index = z * width + x; + long deltaX = (long) x - sourceX[sourceId]; + long deltaZ = (long) z - sourceZ[sourceId]; + nearestSourceIds[index] = sourceId; + distanceSquared[index] = deltaX * deltaX + deltaZ * deltaZ; + } + } + return distanceSquared; + } + + private static int buildEnvelope(int[] nearestSourceIds, int[] sourceX, int width, int depth, + int x, int[] siteRows, int[] siteSourceIds, + int[] starts, long[] siteCosts) { + int siteCount = 0; + for (int z = 0; z < depth; z++) { + int sourceId = nearestSourceIds[z * width + x]; + if (sourceId < 0) { + continue; + } + long deltaX = (long) x - sourceX[sourceId]; + long cost = deltaX * deltaX; + int start = Integer.MIN_VALUE; + while (siteCount > 0) { + start = firstStrictlyBetterAt( + siteRows[siteCount - 1], siteCosts[siteCount - 1], z, cost); + if (start > starts[siteCount - 1]) { + break; + } + siteCount--; + } + if (siteCount == 0) { + start = Integer.MIN_VALUE; + } + siteRows[siteCount] = z; + siteSourceIds[siteCount] = sourceId; + siteCosts[siteCount] = cost; + starts[siteCount] = start; + siteCount++; + } + return siteCount; + } + + private static int firstStrictlyBetterAt(int existingRow, long existingCost, + int candidateRow, long candidateCost) { + long numerator = candidateCost + (long) candidateRow * candidateRow + - existingCost - (long) existingRow * existingRow; + long denominator = 2L * (candidateRow - existingRow); + long start = Math.floorDiv(numerator, denominator) + 1L; + if (start < Integer.MIN_VALUE) { + return Integer.MIN_VALUE; + } + if (start > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int) start; + } + + private static boolean isExplicitSource(int sourceId, int x, int z, + int[] sourceX, int[] sourceZ) { + return sourceId >= 0 && sourceX[sourceId] == x && sourceZ[sourceId] == z; + } + + private static boolean isBetterHorizontalSource(int candidateId, int currentId, + int x, int[] sourceX) { + if (candidateId < 0) { + return false; + } + if (currentId < 0) { + return true; + } + long candidateDelta = (long) x - sourceX[candidateId]; + long currentDelta = (long) x - sourceX[currentId]; + long candidateDistance = candidateDelta * candidateDelta; + long currentDistance = currentDelta * currentDelta; + return candidateDistance < currentDistance + || candidateDistance == currentDistance && sourceX[candidateId] < sourceX[currentId]; + } + + private int requireIndex(int worldX, int worldZ) { + int index = indexAt(worldX, worldZ); + if (index < 0) { + throw new IndexOutOfBoundsException( + "coordinate outside carving footprint: " + worldX + "," + worldZ); + } + return index; + } + + private static boolean fitsInteger(long value) { + return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE; + } + + private static long pack(int x, int z) { + return (long) x << 32 | z & 0xffffffffL; + } + + private static int unpackX(long packed) { + return (int) (packed >> 32); + } + + private static int unpackZ(long packed) { + return (int) packed; + } + + private static long packHeights(int minY, int maxY) { + return (long) minY << 32 | maxY & 0xffffffffL; + } + + private static int unpackMinY(long packed) { + return (int) (packed >> 32); + } + + private static int unpackMaxY(long packed) { + return (int) packed; + } + + @FunctionalInterface + public interface ColumnSink { + void column(int worldX, int worldZ, int minY, int maxY); + } + + @FunctionalInterface + public interface ColumnSource { + /** + * Reports every occupied column to the sink. Returning false abandons the footprint, which is how + * a source rejects occupancy it cannot address (for example coordinates outside the integer range). + */ + boolean emit(ColumnSink sink); + } + + record Column(long distanceSquared, int sourceMinY, int sourceMaxY) { + } + + private record Bounds(int minX, int maxX, int minZ, int maxZ, + int width, int depth, int cellCount) { + } + + private record FootprintData(int minX, int maxX, int minZ, int maxZ, + int width, int depth, long[] distanceSquared, + int[] nearestSourceIds, int[] sourceMinY, int[] sourceMaxY) { + } + + private record SourceCollection(Long2LongOpenHashMap columns, MutableBoundsSnapshot bounds) { + } + + private record MutableBoundsSnapshot(int minX, int maxX, int minZ, int maxZ) { + } + + private static final class MutableBounds { + private int minX = Integer.MAX_VALUE; + private int maxX = Integer.MIN_VALUE; + private int minZ = Integer.MAX_VALUE; + private int maxZ = Integer.MIN_VALUE; + + private void include(int x, int z) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minZ = Math.min(minZ, z); + maxZ = Math.max(maxZ, z); + } + + private MutableBoundsSnapshot freeze() { + return new MutableBoundsSnapshot(minX, maxX, minZ, maxZ); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/StructureFoundationPlanner.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/StructureFoundationPlanner.java new file mode 100644 index 000000000..3ac2c9737 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/StructureFoundationPlanner.java @@ -0,0 +1,79 @@ +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.spi.PlatformBlockState; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; + +import java.util.function.IntConsumer; +import java.util.function.IntPredicate; + +final class StructureFoundationPlanner { + static final int NO_GROUND = Integer.MIN_VALUE; + + private StructureFoundationPlanner() { + } + + static void recordBaseCell(Long2IntOpenHashMap columns, int x, int y, int z, + PlatformBlockState state, boolean supportNonOccluding) { + if (columns == null || state == null + || !(supportNonOccluding ? state.isSolid() : state.isOccluding())) { + return; + } + long columnKey = pack(x, z); + if (!columns.containsKey(columnKey) || y < columns.get(columnKey)) { + columns.put(columnKey, y); + } + } + + static int findGroundY(int foundationY, int maxDepth, int minimumY, IntPredicate solidAtY) { + if (foundationY <= minimumY) { + return NO_GROUND; + } + long requestedFloor = (long) foundationY - Math.max(1, maxDepth); + int scanFloor = (int) Math.max(minimumY, requestedFloor); + for (int y = foundationY - 1; y >= scanFloor; y--) { + if (solidAtY.test(y)) { + return y; + } + } + return NO_GROUND; + } + + static boolean isGroundSolid(PlatformBlockState overlay, boolean carved, int mantleY, int terrainHeight) { + if (mantleY <= 0) { + return true; + } + if (overlay != null) { + return overlay.isSolid(); + } + return !carved && mantleY <= terrainHeight; + } + + static boolean isSurfaceSupportBoundary(PlatformBlockState overlay, boolean carved, + int mantleY, int terrainHeight) { + return carved || isGroundSolid(overlay, false, mantleY, terrainHeight); + } + + static int fillSupportColumn(int foundationY, int groundY, IntConsumer supportAtY) { + if (groundY == NO_GROUND || groundY >= foundationY - 1) { + return 0; + } + int written = 0; + for (int y = foundationY - 1; y > groundY; y--) { + supportAtY.accept(y); + written++; + } + return written; + } + + static long pack(int x, int z) { + return ((long) x << 32) | (z & 0xffffffffL); + } + + static int unpackX(long packed) { + return (int) (packed >> 32); + } + + static int unpackZ(long packed) { + return (int) packed; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java new file mode 100644 index 000000000..ec0cf02e9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java @@ -0,0 +1,94 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; + +final class SurfaceFluidBoundaryPlan { + static final int NO_BOUNDARY = Integer.MAX_VALUE; + private static final int CHUNK_SIZE = 16; + private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE; + + private SurfaceFluidBoundaryPlan() { + } + + static void fill( + int[] chunkSurfaceHeights, + double[] fieldSurfaceHeights, + boolean[] fieldHasFluid, + int fieldSize, + int padding, + int fluidHeight, + int[] boundaryStartY + ) { + if (chunkSurfaceHeights == null || chunkSurfaceHeights.length < CHUNK_AREA + || boundaryStartY == null || boundaryStartY.length < CHUNK_AREA + || padding < 1 || fieldSize < CHUNK_SIZE + (padding * 2) + || fieldSurfaceHeights == null || fieldSurfaceHeights.length < fieldSize * fieldSize + || fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize) { + throw new IllegalArgumentException("Surface fluid boundary fields do not cover a padded chunk"); + } + + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + int fieldX = localX + padding; + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int fieldZ = localZ + padding; + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + int boundaryY = NO_BOUNDARY; + int surfaceY = chunkSurfaceHeights[columnIndex]; + int fieldIndex = (fieldX * fieldSize) + fieldZ; + if (fieldHasFluid[fieldIndex] && surfaceY < fluidHeight) { + boundaryY = surfaceY; + } + + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + ((fieldX - 1) * fieldSize) + fieldZ, fluidHeight); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + ((fieldX + 1) * fieldSize) + fieldZ, fluidHeight); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + (fieldX * fieldSize) + fieldZ - 1, fluidHeight); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + (fieldX * fieldSize) + fieldZ + 1, fluidHeight); + boundaryStartY[columnIndex] = boundaryY; + } + } + } + + static boolean protects(int[] boundaryStartY, int columnIndex, int y, int fluidHeight) { + return boundaryStartY != null + && columnIndex >= 0 + && columnIndex < boundaryStartY.length + && y >= boundaryStartY[columnIndex] + && y <= fluidHeight; + } + + private static int lowerBoundary( + int currentBoundaryY, + double[] fieldSurfaceHeights, + boolean[] fieldHasFluid, + int fieldIndex, + int fluidHeight + ) { + int neighborSurfaceY = (int) Math.round(fieldSurfaceHeights[fieldIndex]); + if (!fieldHasFluid[fieldIndex] || neighborSurfaceY >= fluidHeight) { + return currentBoundaryY; + } + return Math.min(currentBoundaryY, neighborSurfaceY + 1); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/mode/ModeEnclosure.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeEnclosure.java similarity index 79% rename from core/src/main/java/com/volmit/iris/engine/mode/ModeEnclosure.java rename to core/src/main/java/art/arcane/iris/engine/mode/ModeEnclosure.java index 1e66e180e..09d2f9f15 100644 --- a/core/src/main/java/com/volmit/iris/engine/mode/ModeEnclosure.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeEnclosure.java @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.mode; +package art.arcane.iris.engine.mode; -import com.volmit.iris.engine.actuator.IrisBiomeActuator; -import com.volmit.iris.engine.actuator.IrisTerrainNormalActuator; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineMode; -import com.volmit.iris.engine.framework.IrisEngineMode; +import art.arcane.iris.engine.actuator.IrisBiomeActuator; +import art.arcane.iris.engine.actuator.IrisTerrainNormalActuator; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.IrisEngineMode; public class ModeEnclosure extends IrisEngineMode implements EngineMode { public ModeEnclosure(Engine engine) { diff --git a/core/src/main/java/com/volmit/iris/engine/mode/ModeIslands.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeIslands.java similarity index 79% rename from core/src/main/java/com/volmit/iris/engine/mode/ModeIslands.java rename to core/src/main/java/art/arcane/iris/engine/mode/ModeIslands.java index ef3abeae1..494b46bc8 100644 --- a/core/src/main/java/com/volmit/iris/engine/mode/ModeIslands.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeIslands.java @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.mode; +package art.arcane.iris.engine.mode; -import com.volmit.iris.engine.actuator.IrisBiomeActuator; -import com.volmit.iris.engine.actuator.IrisTerrainNormalActuator; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineMode; -import com.volmit.iris.engine.framework.IrisEngineMode; +import art.arcane.iris.engine.actuator.IrisBiomeActuator; +import art.arcane.iris.engine.actuator.IrisTerrainNormalActuator; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.IrisEngineMode; public class ModeIslands extends IrisEngineMode implements EngineMode { public ModeIslands(Engine engine) { diff --git a/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java new file mode 100644 index 000000000..1ed14014e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java @@ -0,0 +1,129 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mode; + +import art.arcane.iris.engine.actuator.IrisBiomeActuator; +import art.arcane.iris.engine.actuator.IrisDecorantActuator; +import art.arcane.iris.engine.actuator.IrisTerrainNormalActuator; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.EnginePlatformHooks; +import art.arcane.iris.engine.framework.EngineStage; +import art.arcane.iris.engine.framework.IrisEngineMode; +import art.arcane.iris.engine.modifier.IrisCarveModifier; +import art.arcane.iris.engine.modifier.IrisCustomModifier; +import art.arcane.iris.engine.modifier.IrisDepositModifier; +import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier; +import art.arcane.iris.engine.modifier.IrisPerfectionModifier; +import art.arcane.iris.engine.modifier.IrisPostModifier; +import art.arcane.iris.spi.IrisLogging; + +import java.util.concurrent.atomic.AtomicLong; + +public class ModeOverworld extends IrisEngineMode implements EngineMode { + private static final AtomicLong lastMaintenanceBypassLog = new AtomicLong(0L); + private final EnginePlatformHooks platformHooks; + + public ModeOverworld(Engine engine) { + super(engine); + platformHooks = engine.getPlatformHooks(); + IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine()); + IrisBiomeActuator biome = new IrisBiomeActuator(getEngine()); + IrisDecorantActuator decorant = new IrisDecorantActuator(getEngine()); + IrisCarveModifier cave = new IrisCarveModifier(getEngine()); + IrisPostModifier post = new IrisPostModifier(getEngine()); + IrisDepositModifier deposit = new IrisDepositModifier(getEngine()); + IrisPerfectionModifier perfection = new IrisPerfectionModifier(getEngine()); + IrisCustomModifier custom = new IrisCustomModifier(getEngine()); + IrisFloatingChildBiomeModifier floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine()); + EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c); + EngineStage sGenMatter = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages()) { + return; + } + generateMatter(x >> 4, z >> 4, m, c); + }; + EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c); + EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c); + EngineStage sCave = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages()) { + return; + } + cave.modify(x >> 4, z >> 4, k, m, c); + }; + EngineStage sDeposit = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages()) { + return; + } + deposit.modify(x, z, k, m, c); + }; + EngineStage sPost = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages()) { + return; + } + post.modify(x, z, k, m, c); + }; + EngineStage sInsertMatter = (x, z, K, p, m, c) -> { + if (shouldBypassMantleStages()) { + return; + } + getMantle().insertMatter(x >> 4, z >> 4, K, m); + }; + EngineStage sFloatingTerrainSolid = (x, z, k, p, m, c) -> floatingChildBiomes.modify(x, z, k, m, c); + EngineStage sFloatingDecorate = (x, z, k, p, m, c) -> floatingChildBiomes.decorateColumns(x, z, k, m, c); + EngineStage sPerfection = (x, z, k, p, m, c) -> perfection.modify(x, z, k, m, c); + EngineStage sCustom = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages()) { + return; + } + custom.modify(x, z, k, m, c); + }; + + registerStage(burst( + sBiome, + sGenMatter, + sTerrain + )); + registerStage(sCave); + registerStage(sPost); + registerStage(sFloatingTerrainSolid); + // Never burst these three: all of them write the same block hunk (and sDecorant reads + // the surface sInsertMatter writes), so parallel order is scheduler-dependent. The + // production path already runs them inline in this order; sequential registration + // makes studio (the only multicore path) match production and the goldenhash baseline. + registerStage(sDeposit); + registerStage(sInsertMatter); + registerStage(sDecorant); + registerStage(sFloatingDecorate); + registerStage(sPerfection); + registerStage(sCustom); + } + + private boolean shouldBypassMantleStages() { + boolean active = platformHooks.shouldBypassMantleStages(getEngine()); + if (active) { + long now = System.currentTimeMillis(); + long last = lastMaintenanceBypassLog.get(); + if (now - last >= 5000L && lastMaintenanceBypassLog.compareAndSet(last, now)) { + IrisLogging.info("Maintenance regen bypass: skipping mantle-backed overworld stages for Folia safety."); + } + } + return active; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/mode/ModeSuperFlat.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeSuperFlat.java similarity index 79% rename from core/src/main/java/com/volmit/iris/engine/mode/ModeSuperFlat.java rename to core/src/main/java/art/arcane/iris/engine/mode/ModeSuperFlat.java index 5a0e93caf..6cc04308b 100644 --- a/core/src/main/java/com/volmit/iris/engine/mode/ModeSuperFlat.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeSuperFlat.java @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.mode; +package art.arcane.iris.engine.mode; -import com.volmit.iris.engine.actuator.IrisBiomeActuator; -import com.volmit.iris.engine.actuator.IrisTerrainNormalActuator; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineMode; -import com.volmit.iris.engine.framework.IrisEngineMode; +import art.arcane.iris.engine.actuator.IrisBiomeActuator; +import art.arcane.iris.engine.actuator.IrisTerrainNormalActuator; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.IrisEngineMode; public class ModeSuperFlat extends IrisEngineMode implements EngineMode { public ModeSuperFlat(Engine engine) { diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java new file mode 100644 index 000000000..460042988 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -0,0 +1,765 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.engine.UpperDimensionContext; +import art.arcane.iris.engine.actuator.IrisDecorantActuator; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedModifier; +import art.arcane.iris.engine.object.InferredType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.mantle.runtime.TectonicPlate; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.slices.MarkerMatter; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import lombok.Data; +import art.arcane.iris.spi.PlatformBlockState; + +import java.util.Map; + +public class IrisCarveModifier extends EngineAssignedModifier { + private static final byte LIQUID_FLUID = 1; + private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(IrisCarveScratch::new); + private static final int CAVE_BIOME_BLEND_RADIUS = 3; + private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4; + private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8; + private static final MatterCavern BASIC_CAVERN = new MatterCavern(true, "", (byte) 0); + private final RNG rng; + private final PlatformBlockState AIR = B.getState("CAVE_AIR"); + private final PlatformBlockState LAVA = B.getState("LAVA"); + private final IrisDecorantActuator decorant; + + public IrisCarveModifier(Engine engine) { + super(engine, "Carve"); + rng = new RNG(getEngine().getSeedManager().getCarve()); + decorant = new IrisDecorantActuator(engine); + } + + @Override + @ChunkCoordinates + public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + PrecisionStopwatch caveStopwatch = PrecisionStopwatch.start(); + Mantle mantle = getEngine().getMantle().getMantle(); + IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(2048); + IrisCarveScratch scratch = SCRATCH.get(); + scratch.reset(); + CarveWallBuffer walls = scratch.walls; + CarveColumnMask[] columnMasks = scratch.columnMasks; + CarveColumnMask[] boundaryMasks = scratch.boundaryMasks; + int[] surfaceHeights = scratch.surfaceHeights; + Map customBiomeCache = scratch.customBiomeCache; + UpperDimensionContext upperCtx = getEngine().getUpperContext(); + boolean protectUpper = upperCtx != null && !getEngine().getDimension().isUpperDimensionCarving(); + int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null; + int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x); + int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z); + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + surfaceHeights[columnIndex] = context.getRoundedHeight(localX, localZ); + if (protectUpper) { + int worldX = localX + chunkBlockX; + int worldZ = localZ + chunkBlockZ; + int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ); + int gap = getEngine().getDimension().getUpperDimensionGap(); + upperSurfaceHeights[columnIndex] = Math.max(rawUpper, surfaceHeights[columnIndex] + gap); + } + } + + MantleChunk mantleChunk = mantle.getChunk(x, z).use(); + try { + PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); + int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); + int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight(); + mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> { + if (cavern == null) { + return; + } + + if (yy >= worldHeightSpan || yy <= 0) { + return; + } + + int rx = xx & 15; + int rz = zz & 15; + int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz); + + if (upperSurfaceHeights != null && yy >= upperSurfaceHeights[columnIndex]) { + return; + } + + PlatformBlockState current = output.getRaw(rx, yy, rz); + boolean explicitCarveIntent = hasExplicitCarveIntent(cavern); + + if (shouldPreserveExistingFluid(cavern, current)) { + return; + } + + columnMasks[columnIndex].add(yy); + + if (!cavern.getCustomBiome().isEmpty()) { + scratch.customCaveBiomePresent = true; + } + + if (current.isAir() && !explicitCarveIntent) { + return; + } + + if (explicitCarveIntent) { + // Only a fluid cavern consumes the fluid sample, and on the maintenance path that + // sample is a full procedural stream evaluation, so never take it per voxel. + PlatformBlockState fluid = isFluidIntent(cavern) ? context.getFluid().get(rx, rz) : null; + output.setRaw(rx, yy, rz, resolveExplicitCarveState(cavern, fluid, LAVA, AIR)); + } else if (usesDefaultLava(caveLavaHeight, yy)) { + output.setRaw(rx, yy, rz, LAVA); + } else { + output.setRaw(rx, yy, rz, AIR); + } + }); + if (scratch.customCaveBiomePresent) { + addInternalWallsFromMantle(mantleChunk, walls, columnMasks); + } else { + addInternalWallsFromMasks(walls, columnMasks); + } + addCrossChunkBoundaryWalls(mantle, mantleChunk, walls, boundaryMasks, x, z, surfaceHeights); + getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); + + PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); + try { + walls.forEach((rx, yy, rz, cavern) -> { + int worldX = rx + chunkBlockX; + int worldZ = rz + chunkBlockZ; + String customBiome = cavern.getCustomBiome(); + IrisBiome biome = customBiome.isEmpty() + ? resolveCaveBiome(caveBiomeCache, worldX, yy, worldZ, resolverState) + : resolveCustomBiome(customBiomeCache, customBiome); + + if (biome != null) { + PlatformBlockState data = biome.getWall().get(rng, worldX, yy, worldZ, getData()); + int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz); + + if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy < surfaceHeights[columnIndex]) { + output.setRaw(rx, yy, rz, data); + } + } + }); + + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + processColumnFromMask(output, mantleChunk, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache); + } + + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) { + continue; + } + processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], walls, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache); + } + + // Surface-break carving must not leave an ore cap suspended across the opening. + for (int columnIndex = 0; columnIndex < surfaceHeights.length; columnIndex++) { + int surfaceY = surfaceHeights[columnIndex]; + if (surfaceY <= 0 || surfaceY >= output.getHeight()) { + continue; + } + + int belowY = surfaceY - 1; + if (!columnMasks[columnIndex].contains(belowY) && !boundaryMasks[columnIndex].contains(belowY)) { + continue; + } + + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + PlatformBlockState surface = output.getRaw(localX, surfaceY, localZ); + PlatformBlockState below = output.getRaw(localX, belowY, localZ); + if (isUnsupportedSurfaceOre(surface, below)) { + output.setRaw(localX, surfaceY, localZ, AIR); + } + } + } finally { + getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds()); + } + } finally { + getEngine().getMetrics().getCave().put(caveStopwatch.getMilliseconds()); + mantleChunk.release(); + } + } + + static boolean hasExplicitCarveIntent(MatterCavern cavern) { + return cavern != null && (isFluidIntent(cavern) || cavern.isLava() || cavern.getLiquid() == 3); + } + + static boolean isFluidIntent(MatterCavern cavern) { + return cavern != null && cavern.getLiquid() == LIQUID_FLUID; + } + + static boolean shouldPreserveExistingFluid(MatterCavern cavern, PlatformBlockState current) { + return B.isFluid(current) && !hasExplicitCarveIntent(cavern); + } + + static boolean usesDefaultLava(int caveLavaHeight, int y) { + return y <= caveLavaHeight; + } + + static boolean isUnsupportedSurfaceOre(PlatformBlockState surface, PlatformBlockState below) { + return B.isOre(surface) && !B.isSolid(below); + } + + static PlatformBlockState resolveExplicitCarveState(MatterCavern cavern, PlatformBlockState fluid, + PlatformBlockState lava, PlatformBlockState air) { + if (cavern == null) { + return null; + } + if (isFluidIntent(cavern)) { + return fluid; + } + if (cavern.isLava()) { + return lava; + } + return cavern.getLiquid() == 3 ? air : null; + } + + private void addInternalWallsFromMasks(CarveWallBuffer walls, CarveColumnMask[] columnMasks) { + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + CarveColumnMask columnMask = columnMasks[columnIndex]; + if (columnMask.isEmpty()) { + continue; + } + + int rx = columnIndex >> 4; + int rz = columnIndex & 15; + int yy = columnMask.nextSetBit(0); + while (yy >= 0) { + if (rz < 15 && !columnMasks[columnIndex + 1].contains(yy)) { + walls.put(rx, yy, rz + 1, BASIC_CAVERN); + } + if (rx < 15 && !columnMasks[columnIndex + 16].contains(yy)) { + walls.put(rx + 1, yy, rz, BASIC_CAVERN); + } + if (rz > 0 && !columnMasks[columnIndex - 1].contains(yy)) { + walls.put(rx, yy, rz - 1, BASIC_CAVERN); + } + if (rx > 0 && !columnMasks[columnIndex - 16].contains(yy)) { + walls.put(rx - 1, yy, rz, BASIC_CAVERN); + } + yy = columnMask.nextSetBit(yy + 1); + } + } + } + + private void addInternalWallsFromMantle(MantleChunk mc, CarveWallBuffer walls, CarveColumnMask[] columnMasks) { + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + CarveColumnMask columnMask = columnMasks[columnIndex]; + if (columnMask.isEmpty()) { + continue; + } + + int rx = columnIndex >> 4; + int rz = columnIndex & 15; + int yy = columnMask.nextSetBit(0); + while (yy >= 0) { + MatterCavern cavern = mc.get(rx, yy, rz, MatterCavern.class); + if (cavern != null) { + if (rz < 15 && mc.get(rx, yy, rz + 1, MatterCavern.class) == null) { + walls.put(rx, yy, rz + 1, cavern); + } + if (rx < 15 && mc.get(rx + 1, yy, rz, MatterCavern.class) == null) { + walls.put(rx + 1, yy, rz, cavern); + } + if (rz > 0 && mc.get(rx, yy, rz - 1, MatterCavern.class) == null) { + walls.put(rx, yy, rz - 1, cavern); + } + if (rx > 0 && mc.get(rx - 1, yy, rz, MatterCavern.class) == null) { + walls.put(rx - 1, yy, rz, cavern); + } + } + yy = columnMask.nextSetBit(yy + 1); + } + } + } + + private void addCrossChunkBoundaryWalls( + Mantle mantle, + MantleChunk mc, + CarveWallBuffer walls, + CarveColumnMask[] boundaryMasks, + int chunkX, + int chunkZ, + int[] surfaceHeights + ) { + int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX); + int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ); + int maxSurfaceY = 0; + for (int index = 0; index < surfaceHeights.length; index++) { + if (surfaceHeights[index] > maxSurfaceY) { + maxSurfaceY = surfaceHeights[index]; + } + } + int maxY = Math.min(getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() - 1, maxSurfaceY + 1); + if (maxY < 1) { + return; + } + + MantleChunk west = existingMantleChunk(mantle, chunkX - 1, chunkZ); + MantleChunk east = existingMantleChunk(mantle, chunkX + 1, chunkZ); + MantleChunk north = existingMantleChunk(mantle, chunkX, chunkZ - 1); + MantleChunk south = existingMantleChunk(mantle, chunkX, chunkZ + 1); + if (west == null && east == null && north == null && south == null) { + return; + } + + for (int yy = 1; yy <= maxY; yy++) { + for (int offset = 0; offset < 16; offset++) { + if (west != null) { + tryAddBoundaryWall(mc, west, walls, boundaryMasks, 0, yy, offset, 15, offset); + } + if (east != null) { + tryAddBoundaryWall(mc, east, walls, boundaryMasks, 15, yy, offset, 0, offset); + } + if (north != null) { + tryAddBoundaryWall(mc, north, walls, boundaryMasks, offset, yy, 0, offset, 15); + } + if (south != null) { + tryAddBoundaryWall(mc, south, walls, boundaryMasks, offset, yy, 15, offset, 0); + } + } + } + } + + private void tryAddBoundaryWall( + MantleChunk mc, + MantleChunk neighborChunk, + CarveWallBuffer walls, + CarveColumnMask[] boundaryMasks, + int localX, + int yy, + int localZ, + int neighborX, + int neighborZ + ) { + if (mc.get(localX, yy, localZ, MatterCavern.class) != null) { + return; + } + + MatterCavern neighbor = neighborChunk.get(neighborX, yy, neighborZ, MatterCavern.class); + if (neighbor == null) { + return; + } + + walls.put(localX, yy, localZ, neighbor); + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + boundaryMasks[columnIndex].add(yy); + } + + private MantleChunk existingMantleChunk(Mantle mantle, int chunkX, int chunkZ) { + TectonicPlate plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5)); + if (plate == null || plate.isClosed()) { + return null; + } + return plate.get(chunkX & 31, chunkZ & 31); + } + + private void processColumnFromMask( + Hunk output, + MantleChunk mc, + Mantle mantle, + CarveColumnMask columnMask, + int columnIndex, + int chunkX, + int chunkZ, + IrisDimensionCarvingResolver.State resolverState, + Long2ObjectOpenHashMap caveBiomeCache, + Map customBiomeCache + ) { + if (columnMask == null || columnMask.isEmpty()) { + return; + } + + int firstHeight = columnMask.nextSetBit(0); + if (firstHeight < 0) { + return; + } + + int rx = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int rz = columnIndex & 15; + int worldX = rx + PowerOfTwoCoordinates.chunkToBlock(chunkX); + int worldZ = rz + PowerOfTwoCoordinates.chunkToBlock(chunkZ); + CaveZone zone = new CaveZone(); + zone.setFloor(firstHeight); + int buf = firstHeight - 1; + int y = firstHeight; + + while (y >= 0) { + if (y <= getEngine().getHeight()) { + if (y == buf + 1) { + buf = y; + zone.ceiling = buf; + } else { + if (zone.isValid(getEngine())) { + processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache); + } + zone = new CaveZone(); + zone.setFloor(y); + buf = y; + } + } + + y = columnMask.nextSetBit(y + 1); + } + + if (zone.isValid(getEngine())) { + processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache); + } + } + + private void processBoundaryColumnFromMask( + Hunk output, + CarveColumnMask boundaryMask, + CarveWallBuffer walls, + int columnIndex, + int chunkX, + int chunkZ, + IrisDimensionCarvingResolver.State resolverState, + Long2ObjectOpenHashMap caveBiomeCache, + Map customBiomeCache + ) { + int firstHeight = boundaryMask.nextSetBit(0); + if (firstHeight < 0) { + return; + } + + int rx = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int rz = columnIndex & 15; + int worldX = rx + PowerOfTwoCoordinates.chunkToBlock(chunkX); + int worldZ = rz + PowerOfTwoCoordinates.chunkToBlock(chunkZ); + int zoneFloor = firstHeight; + int zoneCeiling = firstHeight; + int y = boundaryMask.nextSetBit(firstHeight + 1); + + while (y >= 0) { + if (y == zoneCeiling + 1) { + zoneCeiling = y; + } else { + paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache); + zoneFloor = y; + zoneCeiling = y; + } + y = boundaryMask.nextSetBit(y + 1); + } + + paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache); + } + + private void paintBoundaryZone( + Hunk output, + CarveWallBuffer walls, + int rx, + int rz, + int worldX, + int worldZ, + int zoneFloor, + int zoneCeiling, + IrisDimensionCarvingResolver.State resolverState, + Long2ObjectOpenHashMap caveBiomeCache, + Map customBiomeCache + ) { + IrisBiome floorBiome = resolveCaveBoundaryBiome( + walls.get(rx, zoneFloor, rz), worldX, zoneFloor, worldZ, + resolverState, caveBiomeCache, customBiomeCache); + IrisBiome ceilingBiome = resolveCaveBoundaryBiome( + walls.get(rx, zoneCeiling, rz), worldX, zoneCeiling, worldZ, + resolverState, caveBiomeCache, customBiomeCache); + if (floorBiome == null && ceilingBiome == null) { + return; + } + + if (floorBiome != null) { + KList floorLayers = floorBiome.generateLayers( + getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex()); + for (int i = 0; i < zoneFloor - 1; i++) { + if (!floorLayers.hasIndex(i)) { + break; + } + int floorY = zoneFloor - i - 1; + if (floorY < 0) { + break; + } + PlatformBlockState existing = output.getRaw(rx, floorY, rz); + PlatformBlockState layer = floorLayers.get(i); + if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) { + continue; + } + if (B.isOre(existing)) { + output.setRaw(rx, floorY, rz, B.toDeepSlateOre(existing, layer)); + continue; + } + output.setRaw(rx, floorY, rz, layer); + } + } + + if (ceilingBiome != null) { + int worldMaxY = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); + KList ceilingLayers = ceilingBiome.generateCeilingLayers( + getDimension(), worldX, worldZ, rng, 3, zoneCeiling, getData(), getComplex()); + for (int i = 0; i < ceilingLayers.size(); i++) { + int ceilingY = zoneCeiling + i + 1; + if (ceilingY >= worldMaxY) { + break; + } + PlatformBlockState existing = output.getRaw(rx, ceilingY, rz); + if (!B.isSolid(existing)) { + continue; + } + PlatformBlockState layer = ceilingLayers.get(i); + if (B.isOre(existing)) { + output.setRaw(rx, ceilingY, rz, B.toDeepSlateOre(existing, layer)); + continue; + } + output.setRaw(rx, ceilingY, rz, layer); + } + } + } + + /** + * 1-in-16 marker roll from a SplitMix64 finalizer over (carve seed, block position, salt). + * Deterministic per seed and position, thread-order independent, allocation free. + */ + private boolean markerRoll(int x, int y, int z, long salt) { + long h = (getEngine().getSeedManager().getCarve() + salt) ^ BlockPosition.toLong(x, y, z); + h = (h ^ (h >>> 30)) * 0xBF58476D1CE4E5B9L; + h = (h ^ (h >>> 27)) * 0x94D049BB133111EBL; + h ^= h >>> 31; + return (h & 15L) == 0L; + } + + private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { + int maxY = output.getHeight(); + + if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) { + output.setRaw(rx, zone.ceiling + 1, rz, AIR); + } + + if (B.isDecorant(output.getRaw(rx, zone.ceiling, rz))) { + output.setRaw(rx, zone.ceiling, rz, AIR); + } + + // Seed-derived and position-keyed, never Math.random(): these markers persist into the + // mantle and drive cave spawning, so the same seed must stamp the same markers on + // every run and every platform. Inline mix, no allocation on this hot path. + if (markerRoll(xx, zone.ceiling, zz, 0x9E3779B97F4A7C15L)) { + mantle.set(xx, zone.ceiling, zz, MarkerMatter.CAVE_CEILING); + } + + if (markerRoll(xx, zone.floor, zz, 0xC2B2AE3D27D4EB4FL)) { + mantle.set(xx, zone.floor, zz, MarkerMatter.CAVE_FLOOR); + } + + IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); + IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); + if (floorBiome == null && ceilingBiome == null) { + return; + } + + if (floorBiome != null) { + KList floorBlocks = floorBiome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex()); + for (int i = 0; i < zone.floor - 1; i++) { + if (!floorBlocks.hasIndex(i)) { + break; + } + int y = zone.floor - i - 1; + PlatformBlockState block = floorBlocks.get(i); + PlatformBlockState existing = output.getRaw(rx, y, rz); + if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, y, rz, block)) { + continue; + } + if (B.isOre(existing)) { + output.setRaw(rx, y, rz, B.toDeepSlateOre(existing, block)); + continue; + } + output.setRaw(rx, y, rz, block); + } + } + + if (ceilingBiome != null) { + KList ceilingBlocks = ceilingBiome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex()); + for (int i = 0; i < ceilingBlocks.size(); i++) { + int cy = zone.ceiling + i + 1; + if (cy >= maxY) { + break; + } + PlatformBlockState block = ceilingBlocks.get(i); + PlatformBlockState existing = output.getRaw(rx, cy, rz); + if (!B.isSolid(existing)) { + continue; + } + if (B.isOre(existing)) { + output.setRaw(rx, cy, rz, B.toDeepSlateOre(existing, block)); + continue; + } + output.setRaw(rx, cy, rz, block); + } + } + + IrisDecorator[] surfaceDecorators = floorBiome == null + ? new IrisDecorator[0] + : floorBiome.getDecoratorBucket(IrisDecorationPart.NONE); + if (surfaceDecorators.length > 0 && hasStableCaveFloorSupport(output, rx, zone.getFloor(), rz)) { + decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, floorBiome, InferredType.CAVE, zone.getFloor() - 1, zone.airThickness()); + } + + IrisDecorator[] ceilingDecorators = ceilingBiome == null + ? new IrisDecorator[0] + : ceilingBiome.getDecoratorBucket(IrisDecorationPart.CEILING); + if (ceilingDecorators.length > 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) { + decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness()); + } + } + + IrisBiome resolveCaveBoundaryBiome(MantleChunk mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { + MatterCavern cavern = mantleChunk.get(x, y, z, MatterCavern.class); + return resolveCaveBoundaryBiome( + cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache); + } + + IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { + if (cavern != null && !cavern.getCustomBiome().isEmpty()) { + return resolveCustomBiome(customBiomeCache, cavern.getCustomBiome()); + } + return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState); + } + + static boolean canReplaceCaveFloorLayer(Hunk output, int x, int y, int z, PlatformBlockState layer) { + return !isGravityAffected(layer) || y > 0 && B.isSolid(output.getRaw(x, y - 1, z)); + } + + static boolean hasStableCaveFloorSupport(Hunk output, int x, int floorY, int z) { + if (floorY <= 0) { + return false; + } + PlatformBlockState support = output.getRaw(x, floorY - 1, z); + if (!B.isSolid(support)) { + return false; + } + return !isGravityAffected(support) || floorY > 1 && B.isSolid(output.getRaw(x, floorY - 2, z)); + } + + static boolean isGravityAffected(PlatformBlockState state) { + if (state == null) { + return false; + } + String key = IrisProceduralBlocks.materialKey(state); + return key.equals("minecraft:sand") + || key.equals("minecraft:red_sand") + || key.equals("minecraft:gravel") + || key.equals("minecraft:suspicious_sand") + || key.equals("minecraft:suspicious_gravel") + || key.endsWith("_concrete_powder"); + } + + private IrisBiome resolveCaveBiome(Long2ObjectOpenHashMap caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) { + IrisBiome center = sampleCaveBiome(caveBiomeCache, x, y, z, resolverState); + if (center == null) { + return null; + } + + IrisBiome xPos = sampleCaveBiome(caveBiomeCache, x + CAVE_BIOME_BLEND_RADIUS, y, z, resolverState); + IrisBiome xNeg = sampleCaveBiome(caveBiomeCache, x - CAVE_BIOME_BLEND_RADIUS, y, z, resolverState); + IrisBiome zPos = sampleCaveBiome(caveBiomeCache, x, y, z + CAVE_BIOME_BLEND_RADIUS, resolverState); + IrisBiome zNeg = sampleCaveBiome(caveBiomeCache, x, y, z - CAVE_BIOME_BLEND_RADIUS, resolverState); + + if (xPos == center && xNeg == center && zPos == center && zNeg == center) { + return center; + } + + int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT); + if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) { + return center; + } + roll -= CAVE_BIOME_BLEND_CENTER_WEIGHT; + if (roll == 0) { + return xPos != null ? xPos : center; + } + if (roll == 1) { + return xNeg != null ? xNeg : center; + } + if (roll == 2) { + return zPos != null ? zPos : center; + } + return zNeg != null ? zNeg : center; + } + + private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) { + long key = BlockPosition.toLong(x, y, z); + IrisBiome cachedBiome = caveBiomeCache.get(key); + if (cachedBiome != null) { + return cachedBiome; + } + + IrisBiome resolvedBiome = getEngine().getCaveBiome(x, y, z, resolverState); + if (resolvedBiome != null) { + caveBiomeCache.put(key, resolvedBiome); + } + return resolvedBiome; + } + + private IrisBiome resolveCustomBiome(Map customBiomeCache, String customBiome) { + if (customBiomeCache.containsKey(customBiome)) { + return customBiomeCache.get(customBiome); + } + + IrisBiome loaded = getEngine().getData().getBiomeLoader().load(customBiome); + customBiomeCache.put(customBiome, loaded); + return loaded; + } + + @Data + public static class CaveZone { + private int ceiling = -1; + private int floor = -1; + + public int airThickness() { + return (ceiling - floor) - 1; + } + + public boolean isValid(Engine engine) { + return floor < ceiling && ceiling - floor >= 1 && floor >= 0 && ceiling <= engine.getHeight() && airThickness() > 0; + } + + public String toString() { + return floor + "-" + ceiling; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java new file mode 100644 index 000000000..678fbd38c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java @@ -0,0 +1,277 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +final class IrisCarveScratch { + final CarveColumnMask[] columnMasks = new CarveColumnMask[256]; + final CarveColumnMask[] boundaryMasks = new CarveColumnMask[256]; + final int[] surfaceHeights = new int[256]; + final CarveWallBuffer walls = new CarveWallBuffer(512); + final Map customBiomeCache = new HashMap<>(); + int[] upperSurfaceHeights; + boolean customCaveBiomePresent; + + IrisCarveScratch() { + for (int index = 0; index < columnMasks.length; index++) { + columnMasks[index] = new CarveColumnMask(); + boundaryMasks[index] = new CarveColumnMask(); + } + } + + int[] getOrCreateUpperSurfaceHeights() { + if (upperSurfaceHeights == null) { + upperSurfaceHeights = new int[256]; + } + return upperSurfaceHeights; + } + + void reset() { + for (int index = 0; index < columnMasks.length; index++) { + columnMasks[index].clear(); + boundaryMasks[index].clear(); + } + walls.clear(); + customBiomeCache.clear(); + customCaveBiomePresent = false; + } +} + +final class CarveColumnMask { + private long[] words = new long[8]; + private int maxWord = -1; + + void add(int y) { + if (y < 0) { + return; + } + + int wordIndex = y >> 6; + if (wordIndex >= words.length) { + words = Arrays.copyOf(words, Math.max(words.length << 1, wordIndex + 1)); + } + + words[wordIndex] |= 1L << (y & 63); + if (wordIndex > maxWord) { + maxWord = wordIndex; + } + } + + int nextSetBit(int fromBit) { + if (maxWord < 0) { + return -1; + } + + int startBit = Math.max(0, fromBit); + int wordIndex = startBit >> 6; + if (wordIndex > maxWord) { + return -1; + } + + long word = words[wordIndex] & (-1L << (startBit & 63)); + while (true) { + if (word != 0L) { + return (wordIndex << 6) + Long.numberOfTrailingZeros(word); + } + + wordIndex++; + if (wordIndex > maxWord) { + return -1; + } + word = words[wordIndex]; + } + } + + boolean isEmpty() { + return maxWord < 0; + } + + boolean contains(int y) { + if (y < 0) { + return false; + } + + int wordIndex = y >> 6; + if (wordIndex > maxWord) { + return false; + } + + return (words[wordIndex] & (1L << (y & 63))) != 0L; + } + + void clear() { + if (maxWord < 0) { + return; + } + + for (int index = 0; index <= maxWord; index++) { + words[index] = 0L; + } + maxWord = -1; + } +} + +final class CarveWallBuffer { + private static final int EMPTY_KEY = -1; + private static final double LOAD_FACTOR = 0.75D; + + private int[] keys; + private MatterCavern[] values; + private int mask; + private int resizeAt; + private int size; + + CarveWallBuffer(int expectedSize) { + int capacity = 1; + int minimumCapacity = Math.max(8, expectedSize); + while (capacity < minimumCapacity) { + capacity <<= 1; + } + + keys = new int[capacity]; + Arrays.fill(keys, EMPTY_KEY); + values = new MatterCavern[capacity]; + mask = capacity - 1; + resizeAt = Math.max(1, (int) (capacity * LOAD_FACTOR)); + } + + void put(int x, int y, int z, MatterCavern value) { + int key = pack(x, y, z); + int index = mix(key) & mask; + + while (true) { + int existingKey = keys[index]; + if (existingKey == EMPTY_KEY) { + keys[index] = key; + values[index] = value; + size++; + if (size >= resizeAt) { + resize(); + } + return; + } + + if (existingKey == key) { + values[index] = value; + return; + } + + index = (index + 1) & mask; + } + } + + MatterCavern get(int x, int y, int z) { + int key = pack(x, y, z); + int index = mix(key) & mask; + while (true) { + int existingKey = keys[index]; + if (existingKey == EMPTY_KEY) { + return null; + } + if (existingKey == key) { + return values[index]; + } + index = (index + 1) & mask; + } + } + + void forEach(Consumer consumer) { + for (int index = 0; index < keys.length; index++) { + int key = keys[index]; + if (key == EMPTY_KEY) { + continue; + } + + MatterCavern cavern = values[index]; + if (cavern != null) { + consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern); + } + } + } + + void clear() { + Arrays.fill(keys, EMPTY_KEY); + Arrays.fill(values, null); + size = 0; + } + + private void resize() { + int[] oldKeys = keys; + MatterCavern[] oldValues = values; + int nextCapacity = oldKeys.length << 1; + keys = new int[nextCapacity]; + Arrays.fill(keys, EMPTY_KEY); + values = new MatterCavern[nextCapacity]; + mask = nextCapacity - 1; + resizeAt = Math.max(1, (int) (nextCapacity * LOAD_FACTOR)); + size = 0; + + for (int index = 0; index < oldKeys.length; index++) { + int key = oldKeys[index]; + MatterCavern value = oldValues[index]; + if (key != EMPTY_KEY && value != null) { + reinsert(key, value); + } + } + } + + private void reinsert(int key, MatterCavern value) { + int index = mix(key) & mask; + while (keys[index] != EMPTY_KEY) { + index = (index + 1) & mask; + } + + keys[index] = key; + values[index] = value; + size++; + } + + private int pack(int x, int y, int z) { + return (y << 8) | PowerOfTwoCoordinates.packLocal16(x & 15, z & 15); + } + + private int unpackX(int key) { + return PowerOfTwoCoordinates.unpackLocal16X(key & 255); + } + + private int unpackY(int key) { + return key >> 8; + } + + private int unpackZ(int key) { + return PowerOfTwoCoordinates.unpackLocal16Z(key); + } + + private int mix(int value) { + int mixed = value * 0x9E3779B9; + return mixed ^ (mixed >>> 16); + } + + @FunctionalInterface + interface Consumer { + void accept(int x, int y, int z, MatterCavern cavern); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCustomModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCustomModifier.java new file mode 100644 index 000000000..328fea675 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCustomModifier.java @@ -0,0 +1,64 @@ +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedModifier; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; + +public class IrisCustomModifier extends EngineAssignedModifier { + public IrisCustomModifier(Engine engine) { + super(engine, "Custom"); + } + + @Override + public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + MantleChunk mc = getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4); + if (!mc.isFlagged(MantleFlag.CUSTOM_ACTIVE)) { + return; + } + mc.use(); + + BurstExecutor burst = MultiBurst.burst.burst(output.getHeight()); + burst.setMulticore(multicore); + // complete() must run before release() even when queueing throws — detached burst + // tasks must never write into a released chunk, and a lost permit wedges close(). + try { + for (int y = 0; y < output.getHeight(); y++) { + int finalY = y; + burst.queue(() -> { + for (int rX = 0; rX < output.getWidth(); rX++) { + for (int rZ = 0; rZ < output.getDepth(); rZ++) { + PlatformBlockState b = output.get(rX, finalY, rZ); + if (b == null || !b.isCustom()) { + continue; + } + String placementKey = b.deferredPlacementKey(); + PlatformBlockState baseState = b.placementBaseState(); + if (placementKey == null || baseState == null) { + continue; + } + + mc.getOrCreate(finalY >> 4) + .slice(Identifier.class) + .set(rX, finalY & 15, rZ, Identifier.fromString(placementKey)); + output.set(rX, finalY, rZ, baseState); + } + } + }); + } + } finally { + try { + burst.complete(); + } finally { + mc.release(); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java new file mode 100644 index 000000000..936a7569c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java @@ -0,0 +1,357 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedModifier; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDepositGenerator; +import art.arcane.iris.engine.object.IrisDepositHeightDistribution; +import art.arcane.iris.engine.object.IrisDepositPlacementScope; +import art.arcane.iris.engine.object.IrisDepositVariant; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.data.HeightMap; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; + +public class IrisDepositModifier extends EngineAssignedModifier { + private final RNG rng; + + public IrisDepositModifier(Engine engine) { + super(engine, "Deposit"); + rng = new RNG(getEngine().getSeedManager().getDeposit()); + } + + @Override + public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + PrecisionStopwatch p = PrecisionStopwatch.start(); + generateDeposits(output, Math.floorDiv(x, 16), Math.floorDiv(z, 16), multicore, context); + getEngine().getMetrics().getDeposit().put(p.getMilliseconds()); + } + + public void generateDeposits(Hunk terrain, int x, int z, boolean multicore, ChunkContext context) { + IrisRegion region = context.getRegion().get(7, 7); + IrisBiome biome = context.getBiome().get(7, 7); + BurstExecutor burst = burst().burst(multicore); + + long seed = x * 341873128712L + z * 132897987541L; + long mask = 0; + MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use(); + try { + for (IrisDepositGenerator k : getDimension().getDeposits()) { + long finalSeed = seed * ++mask; + burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + } + + for (IrisDepositGenerator k : region.getDeposits()) { + long finalSeed = seed * ++mask; + burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + } + + for (IrisDepositGenerator k : biome.getDeposits()) { + long finalSeed = seed * ++mask; + burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); + } + } finally { + // complete() must run before release() even when queueing throws — already + // submitted burst tasks must never write into a released chunk. + try { + burst.complete(); + } finally { + chunk.release(); + } + } + } + + public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk data, RNG rng, int cx, int cz, boolean safe, ChunkContext context) { + generate(k, chunk, data, rng, cx, cz, safe, null, context); + } + + public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) { + IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State(); + if (k.getSpawnChance() < rng.d()) + return; + + for (int l = 0; l < rng.i(k.getMinPerChunk(), k.getMaxPerChunk() + 1); l++) { + if (k.getPerClumpSpawnChance() < rng.d()) + continue; + + IrisObject clump = k.getClump(getEngine(), rng, getData()); + + int dim = clump.getW(); + int min = dim / 2; + int max = (int) (16D - dim / 2D); + + if (min > max || min < 0 || max > 15) { + min = 6; + max = 9; + } + + int x = rng.i(min, max + 1); + int z = rng.i(min, max + 1); + int terrainSurface = getDepositTerrainSurface(cx, cz, x, z, he, context); + int height = k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN + ? depositSurfaceLimit(terrainSurface, k.getSurfaceClearance()) + : getEngine().getHeight() - 1; + + if (height < 0) { + continue; + } + + int y = sampleHeight( + k.getHeightDistribution(), rng, k.getMinHeight(), k.getMaxHeight(), + Math.min(height, getEngine().getHeight() - 1)); + if (y == Integer.MIN_VALUE) { + continue; + } + + boolean clippedHeight = k.getHeightDistribution() == IrisDepositHeightDistribution.CLIPPED_UNIFORM; + if (clippedHeight && y > height - 2) + continue; + + int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y)); + boolean oreDeposit = k.isOre(getData()); + IrisBiome surfaceBiome = context.getBiome().get(x, z); + IrisBiome depositBiome = oreDeposit || k.usesCaveBiomeFilter() + ? getEngine().getCaveBiome( + (cx << 4) + x, biomeY, (cz << 4) + z, carvingState) + : null; + if (!k.matchesBiome(surfaceBiome, depositBiome)) { + continue; + } + + if (oreDeposit) { + if (depositBiome != null) { + double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier(); + if (frequencyMultiplier < 1D + && !passesOreFrequency(frequencyMultiplier, rng.d())) { + continue; + } + + double sizeMultiplier = depositBiome.getOreDepositSizeMultiplier(); + if (sizeMultiplier != 1D) { + IrisObject scaledClump = k.getClump(getEngine(), rng, getData(), sizeMultiplier); + int scaledDimension = scaledClump.getW(); + x = clampDepositCenter(x, scaledDimension, 16); + if (clippedHeight) { + y = clampDepositCenter(y, scaledDimension, getEngine().getHeight()); + } + z = clampDepositCenter(z, scaledDimension, 16); + clump = scaledClump; + } + } + } + + IrisDimension dimension = getDimension(); + + for (art.arcane.iris.util.common.math.IrisBlockVector j : clump.getBlocks().keys()) { + int nx = j.getBlockX() + x; + int ny = j.getBlockY() + y; + int nz = j.getBlockZ() + z; + + if (nx > 15 || nx < 0 || ny >= getEngine().getHeight() || ny < 0 || nz < 0 || nz > 15) { + continue; + } + int columnSurface = getDepositTerrainSurface(cx, cz, nx, nz, he, context); + if (!placementSurfaceAllows( + k.getPlacementScope(), ny, columnSurface, k.getSurfaceClearance())) { + continue; + } + + PlatformBlockState current = data.get(nx, ny, nz); + if (!canReplaceDepositTarget(current)) { + continue; + } + if (!k.canReplace(current)) { + continue; + } + if (!k.isReplaceBedrock() && IrisProceduralBlocks.materialKey(current).equals("minecraft:bedrock")) { + continue; + } + if (shouldDiscardExposed( + k.getDiscardChanceOnAirExposure(), rng.d(), isAdjacentToAir(data, nx, ny, nz))) { + continue; + } + + if (chunk.get(nx, ny, nz, MatterCavern.class) == null) { + PlatformBlockState ore = clump.getBlocks().get(j); + PlatformBlockState remapped = resolveDepositVariant( + cx, cz, nx, ny, nz, ore, dimension, context, carvingState); + PlatformBlockState finalBlock = remapped != null + ? remapped + : B.toDeepSlateOre(current, ore); + data.set(nx, ny, nz, finalBlock); + } + } + } + } + + private int getDepositTerrainSurface( + int cx, int cz, int localX, int localZ, HeightMap heightMap, + ChunkContext context) { + return heightMap != null + ? heightMap.getHeight((cx << 4) + localX, (cz << 4) + localZ) + : context.getRoundedHeight(localX, localZ); + } + + static int depositSurfaceLimit(int surfaceY) { + return depositSurfaceLimit(surfaceY, 7); + } + + static int depositSurfaceLimit(int surfaceY, int surfaceClearance) { + return surfaceY - Math.max(0, surfaceClearance); + } + + static boolean placementSurfaceAllows( + IrisDepositPlacementScope scope, int candidateY, int surfaceY, int surfaceClearance) { + int clearance = Math.max(0, surfaceClearance); + return switch (scope) { + case ABOVE_TERRAIN -> candidateY > surfaceY + clearance; + case FULL_HEIGHT -> true; + case TERRAIN -> candidateY <= surfaceY - clearance; + }; + } + + static int absoluteWorldY(int minHeight, int localY) { + return minHeight + localY; + } + + static boolean canReplaceDepositTarget(PlatformBlockState state) { + return state != null && !state.isAir() && !state.isFluid(); + } + + static int sampleHeight( + IrisDepositHeightDistribution distribution, RNG rng, + int configuredMinimum, int configuredMaximum, int clippedMaximum) { + int minimum = distribution == IrisDepositHeightDistribution.CLIPPED_UNIFORM + ? Math.max(0, configuredMinimum) + : configuredMinimum; + int maximum = distribution == IrisDepositHeightDistribution.CLIPPED_UNIFORM + ? Math.min(clippedMaximum, configuredMaximum) + : configuredMaximum; + if (minimum > maximum) { + return Integer.MIN_VALUE; + } + if (minimum == maximum) { + return minimum; + } + if (distribution != IrisDepositHeightDistribution.TRIANGLE) { + return minimum + rng.nextInt(maximum - minimum + 1); + } + + int span = maximum - minimum; + int lowerHalf = span / 2; + int upperHalf = span - lowerHalf; + return minimum + rng.nextInt(upperHalf + 1) + rng.nextInt(lowerHalf + 1); + } + + static boolean shouldDiscardExposed(double chance, double sample, boolean adjacentToAir) { + return adjacentToAir && chance > 0D && sample < Math.min(1D, chance); + } + + static boolean isAdjacentToAir(Hunk data, int x, int y, int z) { + return isAirAt(data, x - 1, y, z) + || isAirAt(data, x + 1, y, z) + || isAirAt(data, x, y - 1, z) + || isAirAt(data, x, y + 1, z) + || isAirAt(data, x, y, z - 1) + || isAirAt(data, x, y, z + 1); + } + + private static boolean isAirAt(Hunk data, int x, int y, int z) { + if (x < 0 || x >= data.getWidth() + || y < 0 || y >= data.getHeight() + || z < 0 || z >= data.getDepth()) { + return false; + } + PlatformBlockState state = data.getRaw(x, y, z); + return state == null || state.isAir(); + } + + static boolean passesOreFrequency(double multiplier, double sample) { + return multiplier >= 1D || sample < Math.max(0D, multiplier); + } + + static int clampDepositCenter(int center, int dimension, int limit) { + int minimum = dimension / 2; + int maximum = (int) (limit - dimension / 2D); + return Math.max(minimum, Math.min(center, maximum)); + } + + private PlatformBlockState resolveDepositVariant(int cx, int cz, int nx, int localY, int nz, PlatformBlockState ore, IrisDimension dimension, ChunkContext context, IrisDimensionCarvingResolver.State carvingState) { + int worldX = (cx << 4) + nx; + int worldZ = (cz << 4) + nz; + int worldY = absoluteWorldY(getEngine().getMinHeight(), localY); + + IrisBiome biome = getEngine().getCaveBiome(worldX, localY, worldZ, carvingState); + if (biome != null) { + PlatformBlockState match = matchDepositVariant(biome.getDepositVariants(), ore, worldY); + if (match != null) { + return match; + } + } + + IrisRegion region = context.getRegion().get(nx, nz); + if (region != null) { + PlatformBlockState match = matchDepositVariant(region.getDepositVariants(), ore, worldY); + if (match != null) { + return match; + } + } + + if (dimension != null) { + PlatformBlockState match = matchDepositVariant(dimension.getDepositVariants(), ore, worldY); + if (match != null) { + return match; + } + } + + return null; + } + + private PlatformBlockState matchDepositVariant(java.util.List variants, PlatformBlockState ore, int y) { + if (variants == null || variants.isEmpty()) { + return null; + } + + for (IrisDepositVariant variant : variants) { + if (y < variant.getMinHeight() || y > variant.getMaxHeight()) { + continue; + } + + PlatformBlockState swapped = variant.remapOrNull(ore, getData()); + if (swapped != null) { + return swapped; + } + } + + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java new file mode 100644 index 000000000..0b73da9b2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java @@ -0,0 +1,407 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.modifier; + + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.decorator.FloatingDecorator; +import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedModifier; +import art.arcane.iris.engine.framework.EngineDecorator; +import art.arcane.iris.engine.object.FloatingBottomPaletteMode; +import art.arcane.iris.engine.object.FloatingIslandBoundarySampler; +import art.arcane.iris.engine.object.FloatingIslandSample; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomePaletteLayer; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisFloatingChildBiomes; +import art.arcane.iris.engine.object.IrisSlopeClip; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterBiomeInject; +import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockState; + +import java.util.IdentityHashMap; + +import static art.arcane.iris.engine.mantle.EngineMantle.AIR; + +public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier { + public static final long FLOATING_BASE_SEED_SALT = 0x5EED_F107_00F1B10CL; + private static final Runnable NOOP_DECORATION_MISS = () -> { + }; + private final RNG rng; + private final EngineDecorator seaSurfaceDecorator; + + private static KList generateBottomPaletteLayers(IrisFloatingChildBiomes entry, IrisDimension dimension, double wx, double wz, RNG random, int paletteDepth, IrisData data, IrisComplex complex) { + if (entry == null || entry.getBottomPaletteMode() != FloatingBottomPaletteMode.CUSTOM || entry.getBottomPalette() == null || entry.getBottomPalette().isEmpty()) { + return null; + } + return generatePaletteLayers(dimension, entry.getBottomPalette(), wx, wz, random.nextParallelRNG(0xB0770B), paletteDepth, data, complex); + } + + private static KList generatePaletteLayers(IrisDimension dimension, KList layers, double wx, double wz, RNG random, int maxDepth, IrisData data, IrisComplex complex) { + KList generated = new KList<>(); + if (layers == null || layers.isEmpty() || maxDepth <= 0) { + return generated; + } + + int generatorSeed = 7235; + for (int i = 0; i < layers.size(); i++) { + IrisBiomePaletteLayer layer = layers.get(i); + CNG heightGenerator = layer.getHeightGenerator(random.nextParallelRNG((generatorSeed++) * generatorSeed * generatorSeed * generatorSeed), data); + if (heightGenerator == null) { + continue; + } + double layerDepth = heightGenerator.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / layer.getZoom(), wz / layer.getZoom()); + IrisSlopeClip slopeClip = layer.getSlopeCondition(); + + if (slopeClip != null && !slopeClip.isDefault() && complex != null && !slopeClip.isValid(complex.getSlopeStream().get(wx, wz))) { + layerDepth = 0; + } + + if (layerDepth <= 0) { + continue; + } + + for (int j = 0; j < layerDepth; j++) { + if (generated.size() >= maxDepth) { + break; + } + + try { + generated.add(layer.get(random.nextParallelRNG(i + j), (wx + j) / layer.getZoom(), j, (wz - j) / layer.getZoom(), data)); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + + if (generated.size() >= maxDepth) { + break; + } + + if (dimension != null && dimension.isExplodeBiomePalettes()) { + PlatformBlockState barrier = B.getState("minecraft:barrier"); + for (int j = 0; j < dimension.getExplodeBiomePaletteSize(); j++) { + generated.add(barrier); + + if (generated.size() >= maxDepth) { + break; + } + } + } + } + + return generated; + } + + private static boolean usesBottomPalette(IrisFloatingChildBiomes entry) { + FloatingBottomPaletteMode mode = entry == null || entry.getBottomPaletteMode() == null ? FloatingBottomPaletteMode.DEPTH : entry.getBottomPaletteMode(); + return mode != FloatingBottomPaletteMode.DEPTH; + } + + private static int[] bottomDepths(FloatingIslandSample sample, int chunkHeight) { + int[] bottomDepths = new int[sample.solidMask.length]; + for (int i = 0; i < bottomDepths.length; i++) { + bottomDepths[i] = -1; + } + + IdentityHashMap depthByEntry = new IdentityHashMap<>(); + int max = Math.min(sample.topIdx, sample.solidMask.length - 1); + for (int k = 0; k <= max; k++) { + if (!sample.solidMask[k]) { + continue; + } + int y = sample.islandBaseY + k; + if (y < 0 || y >= chunkHeight) { + continue; + } + IrisFloatingChildBiomes entry = sample.entryAt(k); + int depth = depthByEntry.getOrDefault(entry, 0); + bottomDepths[k] = depth; + depthByEntry.put(entry, depth + 1); + } + + return bottomDepths; + } + + private static PlatformBlockState selectPaletteBlock(IrisFloatingChildBiomes entry, KList topBlocks, KList bottomBlocks, int topDepth, int bottomDepth, PlatformBlockState fallbackSolid) { + FloatingBottomPaletteMode mode = entry == null || entry.getBottomPaletteMode() == null ? FloatingBottomPaletteMode.DEPTH : entry.getBottomPaletteMode(); + if (mode == FloatingBottomPaletteMode.MIRROR_TOP) { + return paletteBlock(topBlocks, Math.min(topDepth, bottomDepth), fallbackSolid); + } + if (mode == FloatingBottomPaletteMode.CUSTOM && bottomDepth < topDepth) { + if (bottomBlocks != null && !bottomBlocks.isEmpty()) { + return paletteBlock(bottomBlocks, bottomDepth, fallbackSolid); + } + return paletteBlock(topBlocks, bottomDepth, fallbackSolid); + } + return paletteBlock(topBlocks, topDepth, fallbackSolid); + } + + private static PlatformBlockState paletteBlock(KList blocks, int depth, PlatformBlockState fallbackSolid) { + if (blocks == null || blocks.isEmpty()) { + return fallbackSolid; + } + PlatformBlockState block = blocks.hasIndex(depth) ? blocks.get(depth) : blocks.getLast(); + return block == null ? fallbackSolid : block; + } + + private PaletteContext createPaletteContext(IrisBiome parent, IrisFloatingChildBiomes entry, IrisDimension dimension, int wx, int wz, long colSeed, int paletteDepth, IrisData data, IrisComplex complex) { + IrisBiome target = entry == null ? parent : entry.getRealBiome(parent, data); + int entrySeed = entry == null || entry.getBiome() == null ? 0 : entry.getBiome().hashCode(); + RNG layerRng = rng.nextParallelRNG((int) (colSeed ^ 0x7A4E ^ entrySeed)); + KList topBlocks = target == null ? null : target.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex); + if (topBlocks == null || topBlocks.isEmpty()) { + topBlocks = parent.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex); + } + KList bottomBlocks = generateBottomPaletteLayers(entry, dimension, wx, wz, layerRng, paletteDepth, data, complex); + return new PaletteContext(topBlocks, bottomBlocks, B.getState("minecraft:stone")); + } + + private static final class PaletteContext { + private final KList topBlocks; + private final KList bottomBlocks; + private final PlatformBlockState fallbackSolid; + + private PaletteContext(KList topBlocks, KList bottomBlocks, PlatformBlockState fallbackSolid) { + this.topBlocks = topBlocks; + this.bottomBlocks = bottomBlocks; + this.fallbackSolid = fallbackSolid; + } + } + + public IrisFloatingChildBiomeModifier(Engine engine) { + super(engine, "FloatingChildBiomes"); + rng = new RNG(engine.getSeedManager().getTerrain() ^ 0x7EB0A73F1DCE514DL); + seaSurfaceDecorator = new IrisSeaSurfaceDecorator(engine); + } + + @Override + public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + PrecisionStopwatch p = PrecisionStopwatch.start(); + int chunkHeight = output.getHeight(); + IrisData data = getData(); + IrisDimension dimension = getDimension(); + IrisComplex complex = getComplex(); + long baseSeed = getEngine().getSeedManager().getTerrain() ^ FLOATING_BASE_SEED_SALT; + FloatingIslandBoundarySampler boundarySampler = context.getFloatingIslandBoundarySampler(); + FloatingIslandSample.clearChunkMemo(); + + for (int xf = 0; xf < 16; xf++) { + for (int zf = 0; zf < 16; zf++) { + int wx = x + xf; + int wz = z + zf; + IrisBiome parent = boundarySampler.parent(wx, wz); + if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) { + continue; + } + FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine(), boundarySampler); + if (sample == null) { + continue; + } + + long colSeed = FloatingIslandSample.columnSeed(baseSeed, wx, wz); + int paletteDepth = Math.max(4, sample.solidCount + 4); + IdentityHashMap paletteContexts = new IdentityHashMap<>(); + IdentityHashMap topDepthByEntry = new IdentityHashMap<>(); + int[] bottomDepths = bottomDepths(sample, chunkHeight); + for (int k = sample.topIdx; k >= 0; k--) { + if (!sample.solidMask[k]) { + continue; + } + int y = sample.islandBaseY + k; + if (y < 0 || y >= chunkHeight) { + continue; + } + IrisFloatingChildBiomes entry = sample.entryAt(k); + PaletteContext paletteContext = paletteContexts.get(entry); + if (paletteContext == null) { + paletteContext = createPaletteContext(parent, entry, dimension, wx, wz, colSeed, paletteDepth, data, complex); + paletteContexts.put(entry, paletteContext); + } + int depth = topDepthByEntry.getOrDefault(entry, 0); + int bottomDepth = bottomDepths == null || bottomDepths[k] < 0 ? depth : bottomDepths[k]; + PlatformBlockState block = selectPaletteBlock(entry, paletteContext.topBlocks, paletteContext.bottomBlocks, depth, bottomDepth, paletteContext.fallbackSolid); + if (block != null) { + output.set(xf, y, zf, block); + } + topDepthByEntry.put(entry, depth + 1); + } + + IrisFloatingChildBiomes entry = sample.entry; + Integer localFluidHeight = entry.getLocalFluidHeight(); + if (localFluidHeight != null && localFluidHeight > 0) { + PlatformBlockState fluid = B.getStateOrNull(entry.getFluidBlock()); + if (fluid == null) { + fluid = B.getState("minecraft:water"); + } + int fluidCap = Math.min(sample.thickness - 1, localFluidHeight); + for (int k = 1; k <= fluidCap; k++) { + if (sample.solidMask[k]) { + continue; + } + int y = sample.islandBaseY + k; + if (y < 0 || y >= chunkHeight) { + continue; + } + boolean hasSolidBelow = false; + for (int kb = k - 1; kb >= 0; kb--) { + if (sample.solidMask[kb]) { + hasSolidBelow = true; + break; + } + } + if (hasSolidBelow) { + output.set(xf, y, zf, fluid); + } + } + } + + writeIslandSkyBiomes(parent, wx, wz, sample, chunkHeight, data); + } + } + + getEngine().getMetrics().getTerrain().put(p.getMilliseconds()); + } + + public void decorateColumns(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + int chunkHeight = output.getHeight(); + IrisData data = getData(); + IrisComplex complex = getComplex(); + long baseSeed = getEngine().getSeedManager().getTerrain() ^ FLOATING_BASE_SEED_SALT; + FloatingIslandBoundarySampler boundarySampler = context.getFloatingIslandBoundarySampler(); + + for (int xf = 0; xf < 16; xf++) { + for (int zf = 0; zf < 16; zf++) { + int wx = x + xf; + int wz = z + zf; + IrisBiome parent = boundarySampler.parent(wx, wz); + if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) { + continue; + } + FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngine(), boundarySampler); + if (sample == null) { + continue; + } + IrisFloatingChildBiomes entry = sample.entry; + IrisBiome target = entry.getRealBiome(parent, data); + + if (!entry.isInheritDecorators() || target == null) { + continue; + } + + int topY = sample.topY(); + int max = Math.max(1, chunkHeight - topY); + if (topY + 1 < chunkHeight) { + PlatformBlockState above = output.get(xf, topY + 1, zf); + if (above == null || above.isAir()) { + try { + RNG colRng = rng.nextParallelRNG((int) FloatingIslandSample.columnSeed(baseSeed, wx, wz)); + FloatingDecorator.decorateColumn(getEngine(), target, IrisDecorationPart.NONE, xf, zf, wx, wz, topY, max, output, colRng, NOOP_DECORATION_MISS); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + + Integer localFluidHeight = entry.getLocalFluidHeight(); + if (localFluidHeight != null && localFluidHeight > 0) { + int fluidCap = Math.min(sample.thickness - 1, localFluidHeight); + int fluidTopY = -1; + for (int k = 1; k <= fluidCap; k++) { + if (sample.solidMask[k]) { + continue; + } + int y = sample.islandBaseY + k; + if (y < 0 || y >= chunkHeight) { + continue; + } + boolean hasSolidBelow = false; + for (int kb = k - 1; kb >= 0; kb--) { + if (sample.solidMask[kb]) { + hasSolidBelow = true; + break; + } + } + if (hasSolidBelow && y > fluidTopY) { + fluidTopY = y; + } + } + if (fluidTopY > 0 && fluidTopY + 1 < chunkHeight && B.isAir(output.get(xf, fluidTopY + 1, zf))) { + try { + seaSurfaceDecorator.decorate(xf, zf, wx, wx + 1, wx - 1, wz, wz + 1, wz - 1, output, target, fluidTopY, chunkHeight); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + } + } + } + + private void writeIslandSkyBiomes(IrisBiome parent, int wx, int wz, FloatingIslandSample sample, int chunkHeight, IrisData data) { + try { + IdentityHashMap matterByEntry = new IdentityHashMap<>(); + for (int k = 0; k <= sample.topIdx; k++) { + if (!sample.solidMask[k]) { + continue; + } + int y = sample.islandBaseY + k; + if (y < 0 || y >= chunkHeight) { + continue; + } + IrisFloatingChildBiomes entry = sample.entryAt(k); + MatterBiomeInject matter = matterByEntry.get(entry); + if (matter == null) { + IrisBiome target = entry == null ? parent : entry.getRealBiome(parent, data); + if (target == null) { + continue; + } + matter = createSkyBiomeMatter(target, wx, wz); + matterByEntry.put(entry, matter); + } + getEngine().getMantle().getMantle().set(wx, y, wz, matter); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) { + if (target.isCustom()) { + IrisBiomeCustom custom = target.getCustomBiome(rng, getEngine(), wx, 0, wz); + return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(getDimension().getLoadKey() + ":" + custom.getId())); + } + + return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(target.getSkyBiomeKey(rng, getEngine(), wx, 0, wz))); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPerfectionModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPerfectionModifier.java new file mode 100644 index 000000000..29c3e878d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPerfectionModifier.java @@ -0,0 +1,200 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedModifier; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.spi.PlatformBlockState; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +public class IrisPerfectionModifier extends EngineAssignedModifier { + private static final class States { + private static final PlatformBlockState AIR = B.getState("AIR"); + private static final PlatformBlockState WATER = B.getState("WATER"); + private static final Map ORE_BASES = buildOreBases(); + } + + public IrisPerfectionModifier(Engine engine) { + super(engine, "Perfection"); + } + + private static Map buildOreBases() { + Map map = new HashMap<>(); + PlatformBlockState stone = B.getState("STONE"); + PlatformBlockState deepslate = B.getState("DEEPSLATE"); + PlatformBlockState netherrack = B.getState("NETHERRACK"); + PlatformBlockState blackstone = B.getState("BLACKSTONE"); + map.put("minecraft:coal_ore", stone); + map.put("minecraft:copper_ore", stone); + map.put("minecraft:iron_ore", stone); + map.put("minecraft:gold_ore", stone); + map.put("minecraft:redstone_ore", stone); + map.put("minecraft:lapis_ore", stone); + map.put("minecraft:diamond_ore", stone); + map.put("minecraft:emerald_ore", stone); + map.put("minecraft:deepslate_coal_ore", deepslate); + map.put("minecraft:deepslate_copper_ore", deepslate); + map.put("minecraft:deepslate_iron_ore", deepslate); + map.put("minecraft:deepslate_gold_ore", deepslate); + map.put("minecraft:deepslate_redstone_ore", deepslate); + map.put("minecraft:deepslate_lapis_ore", deepslate); + map.put("minecraft:deepslate_diamond_ore", deepslate); + map.put("minecraft:deepslate_emerald_ore", deepslate); + map.put("minecraft:nether_gold_ore", netherrack); + map.put("minecraft:nether_quartz_ore", netherrack); + map.put("minecraft:ancient_debris", netherrack); + map.put("minecraft:gilded_blackstone", blackstone); + return map; + } + + private static String baseKey(PlatformBlockState state) { + String key = state.key(); + int bracket = key.indexOf('['); + return bracket < 0 ? key : key.substring(0, bracket); + } + + @Override + public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + PrecisionStopwatch p = PrecisionStopwatch.start(); + if (getDimension().isHideOresForHiddenOre()) { + hideOres(output, multicore); + } + AtomicBoolean changed = new AtomicBoolean(true); + BurstExecutor burst = burst().burst(multicore); + while (changed.get()) { + changed.set(false); + for (int i = 0; i < 16; i++) { + int finalI = i; + burst.queue(() -> { + List surfaces = new ArrayList<>(); + List ceilings = new ArrayList<>(); + for (int j = 0; j < 16; j++) { + surfaces.clear(); + ceilings.clear(); + int top = getHeight(output, finalI, j); + boolean inside = true; + surfaces.add(top); + + for (int k = top; k >= 0; k--) { + PlatformBlockState b = output.get(finalI, k, j); + boolean now = b != null && !(B.isAir(b) || B.isFluid(b)); + + if (now != inside) { + inside = now; + + if (inside) { + surfaces.add(k); + } else { + ceilings.add(k + 1); + } + } + } + + for (int k : surfaces) { + PlatformBlockState tip = output.get(finalI, k, j); + + if (tip == null) { + continue; + } + + boolean remove = false; + boolean remove2 = false; + + if (B.isDecorant(tip)) { + PlatformBlockState bel = output.get(finalI, k - 1, j); + + if (bel == null) { + remove = true; + } else if (!B.canPlaceOnto(tip, bel)) { + remove = true; + } else if (IrisProceduralBlocks.hasProperty(bel, "half")) { + PlatformBlockState bb = output.get(finalI, k - 2, j); + if (bb == null || !B.canPlaceOnto(bel, bb)) { + remove = true; + remove2 = true; + } + } + + if (remove) { + changed.set(true); + output.set(finalI, k, j, States.AIR); + + if (remove2) { + output.set(finalI, k - 1, j, States.AIR); + } + } + } + } + } + }); + } + burst.complete(); + } + + getEngine().getMetrics().getPerfection().put(p.getMilliseconds()); + } + + private void hideOres(Hunk output, boolean multicore) { + BurstExecutor burst = burst().burst(multicore); + int height = output.getHeight(); + for (int i = 0; i < 16; i++) { + int finalI = i; + burst.queue(() -> { + for (int j = 0; j < 16; j++) { + for (int k = height - 1; k >= 0; k--) { + PlatformBlockState block = output.get(finalI, k, j); + if (block == null) { + continue; + } + PlatformBlockState base = States.ORE_BASES.get(baseKey(block)); + if (base != null) { + output.set(finalI, k, j, base); + } + } + } + }); + } + burst.complete(); + } + + private int getHeight(Hunk output, int x, int z) { + for (int i = output.getHeight() - 1; i >= 0; i--) { + PlatformBlockState b = output.get(x, i, z); + + if (b != null) { + if (!B.isAir(b) && !B.isFluid(b)) { + return i; + } + } + } + + return 0; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java new file mode 100644 index 000000000..4a8e82851 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java @@ -0,0 +1,345 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedModifier; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.engine.object.IrisSlopeClip; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.spi.PlatformBlockState; + +public class IrisPostModifier extends EngineAssignedModifier { + private static final class States { + private static final PlatformBlockState AIR = B.getState("AIR"); + } + + private final RNG rng; + + public IrisPostModifier(Engine engine) { + super(engine, "Post"); + rng = new RNG(getEngine().getSeedManager().getPost()); + } + + @Override + public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + PrecisionStopwatch p = PrecisionStopwatch.start(); + // The post stage runs sequentially on production (multicore false); an uncontended + // monitor per probe is still a monitor times ~10k probes per chunk. + Hunk sync = multicore ? output.synchronize() : output; + int width = output.getWidth(); + int depth = output.getDepth(); + int planeWidth = width + 2; + int[] heights = heightPlane(x, z, width, depth, planeWidth); + IrisDimension dimension = getDimension(); + boolean walls = dimension.isPostProcessingWalls(); + boolean slabs = dimension.isPostProcessingSlabs(); + int fluidHeight = dimension.getFluidHeight(); + for (int i = 0; i < width; i++) { + for (int j = 0; j < depth; j++) { + post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs, fluidHeight); + } + } + + getEngine().getMetrics().getPost().put(p.getMilliseconds()); + } + + /** + * Every column reads its own height plus its four neighbours, so adjacent columns would otherwise + * resolve the same height stream entry up to five times. Resolve the padded plane once instead. The + * four diagonal corners are never read, so they are left unresolved. + */ + private int[] heightPlane(int x, int z, int width, int depth, int planeWidth) { + int[] heights = new int[planeWidth * (depth + 2)]; + EngineMantle mantle = getEngine().getMantle(); + + for (int j = -1; j <= depth; j++) { + boolean edge = j == -1 || j == depth; + int from = edge ? 0 : -1; + int to = edge ? width - 1 : width; + int row = (j + 1) * planeWidth; + + for (int i = from; i <= to; i++) { + heights[row + i + 1] = mantle.trueHeight(x + i, z + j); + } + } + + return heights; + } + + private void post(int currentPostX, int currentPostZ, Hunk currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs, int fluidHeight) { + // x/z are world coordinates, the hunk is indexed relative to this chunk origin. + int originX = x - currentPostX; + int originZ = z - currentPostZ; + int center = (currentPostZ + 1) * planeWidth + currentPostX + 1; + int h = heights[center]; + int ha = heights[center + 1]; + int hb = heights[center + planeWidth]; + int hc = heights[center - 1]; + int hd = heights[center - planeWidth]; + + // Floating Nibs + int g = 0; + + if (h < 1) { + return; + } + + g += ha < h - 1 ? 1 : 0; + g += hb < h - 1 ? 1 : 0; + g += hc < h - 1 ? 1 : 0; + g += hd < h - 1 ? 1 : 0; + + if (g == 4 && isAir(x, h - 1, z, originX, originZ, currentData)) { + setPostBlock(x, h, z, States.AIR, originX, originZ, currentData); + + for (int i = h - 1; i > 0; i--) { + if (!isAir(x, i, z, originX, originZ, currentData)) { + h = i; + break; + } + } + } + + // Nibs + g = 0; + g += ha == h - 1 ? 1 : 0; + g += hb == h - 1 ? 1 : 0; + g += hc == h - 1 ? 1 : 0; + g += hd == h - 1 ? 1 : 0; + + if (g >= 4) { + PlatformBlockState bcState = getPostBlock(x, h, z, originX, originZ, currentData); + PlatformBlockState bState = getPostBlock(x, h + 1, z, originX, originZ, currentData); + + if (bState.isOccluding() && bState.isSolid()) { + if (bcState.isSolid()) { + setPostBlock(x, h, z, bState, originX, originZ, currentData); + h--; + } + } + } else { + // Potholes + g = 0; + g += ha == h + 1 ? 1 : 0; + g += hb == h + 1 ? 1 : 0; + g += hc == h + 1 ? 1 : 0; + g += hd == h + 1 ? 1 : 0; + + if (g >= 4) { + PlatformBlockState ba = getPostBlock(x, ha, z, originX, originZ, currentData); + PlatformBlockState bb = getPostBlock(x, hb, z, originX, originZ, currentData); + PlatformBlockState bc = getPostBlock(x, hc, z, originX, originZ, currentData); + PlatformBlockState bd = getPostBlock(x, hd, z, originX, originZ, currentData); + g = 0; + g = B.isSolid(ba) ? g + 1 : g; + g = B.isSolid(bb) ? g + 1 : g; + g = B.isSolid(bc) ? g + 1 : g; + g = B.isSolid(bd) ? g + 1 : g; + + if (g >= 3) { + setPostBlock(x, h + 1, z, getPostBlock(x, h, z, originX, originZ, currentData), originX, originZ, currentData); + h++; + } + } + } + + // Wall Patcher + IrisBiome biome = context.getBiome().get(currentPostX, currentPostZ); + + if (walls) { + if (!biome.getWall().getPalette().isEmpty()) { + if (ha < h - 2 || hb < h - 2 || hc < h - 2 || hd < h - 2) { + boolean brokeGround = false; + int max = Math.abs(Math.max(h - ha, Math.max(h - hb, Math.max(h - hc, h - hd)))); + + for (int i = h; i > h - max; i--) { + PlatformBlockState d = biome.getWall().get(rng, x + i, i + h, z + i, getData()); + + if (d != null) { + if (isAirOrWater(x, i, z, originX, originZ, currentData)) { + if (brokeGround) { + break; + } + + continue; + } + + setPostBlock(x, i, z, d, originX, originZ, currentData); + brokeGround = true; + } + } + } + } + } + + // Slab + if (slabs) { + //@builder + if ((ha == h + 1 && isSolidNonSlab(x + 1, ha, z, originX, originZ, currentData)) + || (hb == h + 1 && isSolidNonSlab(x, hb, z + 1, originX, originZ, currentData)) + || (hc == h + 1 && isSolidNonSlab(x - 1, hc, z, originX, originZ, currentData)) + || (hd == h + 1 && isSolidNonSlab(x, hd, z - 1, originX, originZ, currentData))) + //@done + { + IrisSlopeClip sc = biome.getSlab().getSlopeCondition(); + PlatformBlockState d = sc.isValid(getComplex().getSlopeStream().get(x, z)) ? biome.getSlab().get(rng, x, h, z, getData()) : null; + + if (d != null) { + boolean cancel = B.isAir(d); + + if (IrisProceduralBlocks.materialKey(d).equals("minecraft:snow") && h + 1 <= fluidHeight) { + cancel = true; + } + + if (isSnowLayer(x, h, z, originX, originZ, currentData)) { + cancel = true; + } + + if (!cancel && isAirOrWater(x, h + 1, z, originX, originZ, currentData)) { + setPostBlock(x, h + 1, z, d, originX, originZ, currentData); + h++; + } + } + } + } + + // Waterlogging + PlatformBlockState b = getPostBlock(x, h, z, originX, originZ, currentData); + + if (IrisProceduralBlocks.hasProperty(b, "waterlogged")) { + boolean w = false; + + if (h <= fluidHeight + 1) { + if (isWaterOrWaterlogged(x, h + 1, z, originX, originZ, currentData)) { + w = true; + } else if ((isWaterOrWaterlogged(x + 1, h, z, originX, originZ, currentData) || isWaterOrWaterlogged(x - 1, h, z, originX, originZ, currentData) || isWaterOrWaterlogged(x, h, z + 1, originX, originZ, currentData) || isWaterOrWaterlogged(x, h, z - 1, originX, originZ, currentData))) { + w = true; + } + } + + if (w != "true".equals(IrisProceduralBlocks.propertyValue(b, "waterlogged"))) { + setPostBlock(x, h, z, b.withProperty("waterlogged", String.valueOf(w)), originX, originZ, currentData); + } + } + + // Foliage + b = getPostBlock(x, h + 1, z, originX, originZ, currentData); + + if (B.isVineBlock(b)) { + PlatformBlockState result = b; + int finalH = h + 1; + + for (String face : IrisProceduralBlocks.FACE_PROPERTIES) { + if (!IrisProceduralBlocks.hasProperty(b, face)) { + continue; + } + int[] mod = IrisProceduralBlocks.faceOffset(face); + PlatformBlockState d = getPostBlock(x + mod[0], finalH + mod[1], z + mod[2], originX, originZ, currentData); + result = result.withProperty(face, String.valueOf(!B.isAir(d) && !B.isVineBlock(d))); + } + if (!result.equals(b)) { + setPostBlock(x, h + 1, z, result, originX, originZ, currentData); + } + } + + if (B.isFoliage(b) || IrisProceduralBlocks.materialKey(b).equals("minecraft:dead_bush")) { + PlatformBlockState onto = getPostBlock(x, h, z, originX, originZ, currentData); + + if (!B.canPlaceOnto(b, onto) && !B.isDecorant(b)) { + setPostBlock(x, h + 1, z, States.AIR, originX, originZ, currentData); + } + } + } + + public boolean isAir(int x, int y, int z, int originX, int originZ, Hunk currentData) { + String material = IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)); + return material.equals("minecraft:air") || material.equals("minecraft:cave_air"); + } + + public boolean hasGravity(int x, int y, int z, int originX, int originZ, Hunk currentData) { + String material = IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)); + return material.equals("minecraft:sand") || material.equals("minecraft:red_sand") || material.endsWith("_concrete_powder"); + } + + public boolean isSolid(int x, int y, int z, int originX, int originZ, Hunk currentData) { + PlatformBlockState d = getPostBlock(x, y, z, originX, originZ, currentData); + return B.isSolid(d) && !B.isVineBlock(d); + } + + public boolean isSolidNonSlab(int x, int y, int z, int originX, int originZ, Hunk currentData) { + PlatformBlockState d = getPostBlock(x, y, z, originX, originZ, currentData); + return B.isSolid(d) && !IrisProceduralBlocks.materialKey(d).endsWith("_slab"); + } + + public boolean isAirOrWater(int x, int y, int z, int originX, int originZ, Hunk currentData) { + String material = IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)); + return material.equals("minecraft:water") || material.equals("minecraft:air") || material.equals("minecraft:cave_air"); + } + + public boolean isSlab(int x, int y, int z, int originX, int originZ, Hunk currentData) { + return IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)).endsWith("_slab"); + } + + public boolean isSnowLayer(int x, int y, int z, int originX, int originZ, Hunk currentData) { + return IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)).equals("minecraft:snow"); + } + + public boolean isWater(int x, int y, int z, int originX, int originZ, Hunk currentData) { + return IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)).equals("minecraft:water"); + } + + public boolean isWaterOrWaterlogged(int x, int y, int z, int originX, int originZ, Hunk currentData) { + PlatformBlockState d = getPostBlock(x, y, z, originX, originZ, currentData); + return IrisProceduralBlocks.materialKey(d).equals("minecraft:water") || "true".equals(IrisProceduralBlocks.propertyValue(d, "waterlogged")); + } + + public boolean isLiquid(int x, int y, int z, int originX, int originZ, Hunk currentData) { + return IrisProceduralBlocks.hasProperty(getPostBlock(x, y, z, originX, originZ, currentData), "level"); + } + + public void setPostBlock(int x, int y, int z, PlatformBlockState d, int originX, int originZ, Hunk currentData) { + int lx = x - originX; + int lz = z - originZ; + + if (lx < 0 || lz < 0 || lx >= currentData.getWidth() || lz >= currentData.getDepth() || y < 0 || y >= currentData.getHeight()) { + return; + } + + currentData.set(lx, y, lz, d); + } + + /** + * Neighbour columns can sit one block outside this chunk, and the blocks of an adjacent chunk are not + * available while generating this one. Resolve the hunk index relative to the chunk origin and let + * getClosest clamp to the nearest in-chunk column instead of wrapping to the opposite chunk edge. + */ + public PlatformBlockState getPostBlock(int x, int y, int z, int originX, int originZ, Hunk h) { + PlatformBlockState b = h.getClosest(x - originX, y, z - originZ); + + return b == null ? States.AIR : b; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/BlockDataMergeSupport.java b/core/src/main/java/art/arcane/iris/engine/object/BlockDataMergeSupport.java new file mode 100644 index 000000000..562d0d6fe --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/BlockDataMergeSupport.java @@ -0,0 +1,92 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; +import org.bukkit.block.data.BlockData; + +import java.util.Objects; +import java.util.function.Function; + +public final class BlockDataMergeSupport { + private static final boolean BUKKIT_PRESENT = detectBukkit(); + private static volatile StateMerger PLATFORM_MERGER = null; + + private BlockDataMergeSupport() { + } + + public interface StateMerger { + PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update); + } + + public static synchronized StateMerger bindPlatformMerger(StateMerger merger) { + StateMerger previous = PLATFORM_MERGER; + PLATFORM_MERGER = Objects.requireNonNull(merger, "merger"); + return previous; + } + + public static synchronized void restorePlatformMerger(StateMerger merger) { + PLATFORM_MERGER = merger; + } + + private static boolean detectBukkit() { + try { + Class.forName("org.bukkit.Bukkit", false, BlockDataMergeSupport.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + static PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) { + if (!BUKKIT_PRESENT) { + StateMerger merger = requirePlatformMerger(PLATFORM_MERGER); + return merger.merge(base, update); + } + BlockData merged = merge((BlockData) base.nativeHandle(), (BlockData) update.nativeHandle(), BukkitBlockResolution::get); + return merged == null ? null : BukkitBlockState.of(merged); + } + + static StateMerger requirePlatformMerger(StateMerger merger) { + if (merger == null) { + throw new IllegalStateException("No platform block-state merger is bound"); + } + return merger; + } + + static BlockData merge(BlockData base, BlockData update, Function resolver) { + try { + return base.merge(update); + } catch (IllegalArgumentException e) { + BlockData normalizedBase = resolve(base, resolver); + BlockData normalizedUpdate = resolve(update, resolver); + + if (normalizedBase != null && normalizedUpdate != null) { + try { + return normalizedBase.merge(normalizedUpdate); + } catch (IllegalArgumentException ignored) { + return normalizedUpdate; + } + } + + if (normalizedUpdate != null) { + return normalizedUpdate; + } + + return update; + } + } + + private static BlockData resolve(BlockData data, Function resolver) { + if (data == null || resolver == null) { + return null; + } + + String serialized = data.getAsString(false); + if (serialized == null || serialized.isBlank()) { + return null; + } + + return resolver.apply(serialized); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/CarveResult.java b/core/src/main/java/art/arcane/iris/engine/object/CarveResult.java similarity index 96% rename from core/src/main/java/com/volmit/iris/engine/object/CarveResult.java rename to core/src/main/java/art/arcane/iris/engine/object/CarveResult.java index 06dff5b90..c8fb4578b 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/CarveResult.java +++ b/core/src/main/java/art/arcane/iris/engine/object/CarveResult.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; import lombok.Value; diff --git a/core/src/main/java/com/volmit/iris/engine/object/CarvingMode.java b/core/src/main/java/art/arcane/iris/engine/object/CarvingMode.java similarity index 93% rename from core/src/main/java/com/volmit/iris/engine/object/CarvingMode.java rename to core/src/main/java/art/arcane/iris/engine/object/CarvingMode.java index 77f48277c..e9601480b 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/CarvingMode.java +++ b/core/src/main/java/art/arcane/iris/engine/object/CarvingMode.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("Defines if an object is allowed to place in carvings, surfaces or both.") public enum CarvingMode { diff --git a/core/src/main/java/com/volmit/iris/engine/object/CaveResult.java b/core/src/main/java/art/arcane/iris/engine/object/CaveResult.java similarity index 96% rename from core/src/main/java/com/volmit/iris/engine/object/CaveResult.java rename to core/src/main/java/art/arcane/iris/engine/object/CaveResult.java index 19a099d6b..6216a0b62 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/CaveResult.java +++ b/core/src/main/java/art/arcane/iris/engine/object/CaveResult.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/DecayControlPlacer.java b/core/src/main/java/art/arcane/iris/engine/object/DecayControlPlacer.java new file mode 100644 index 000000000..3255294e5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/DecayControlPlacer.java @@ -0,0 +1,107 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.PlatformBlockState; +import org.jetbrains.annotations.Nullable; + +public class DecayControlPlacer implements IObjectPlacer { + private final IObjectPlacer delegate; + + public DecayControlPlacer(IObjectPlacer delegate) { + this.delegate = delegate; + } + + @Override + public int getHighest(int x, int z, IrisData data) { + return delegate.getHighest(x, z, data); + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + return delegate.getHighest(x, z, data, ignoreFluid); + } + + @Override + public void set(int x, int y, int z, PlatformBlockState d) { + delegate.set(x, y, z, d); + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + return delegate.get(x, y, z); + } + + @Override + public boolean isPreventingDecay() { + return false; + } + + @Override + public boolean isCarved(int x, int y, int z) { + return delegate.isCarved(x, y, z); + } + + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + return delegate.isSurfaceSolid(x, y, z); + } + + @Override + public boolean isSolid(int x, int y, int z) { + return delegate.isSolid(x, y, z); + } + + @Override + public boolean isUnderwater(int x, int z) { + return delegate.isUnderwater(x, z); + } + + @Override + public int getFluidHeight() { + return delegate.getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return delegate.isDebugSmartBore(); + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + delegate.setTile(xx, yy, zz, tile); + } + + @Override + public void setData(int xx, int yy, int zz, T data) { + delegate.setData(xx, yy, zz, data); + } + + @Override + public @Nullable T getData(int xx, int yy, int zz, Class t) { + return delegate.getData(xx, yy, zz, t); + } + + @Override + public Engine getEngine() { + return delegate.getEngine(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/FloatingBottomPaletteMode.java b/core/src/main/java/art/arcane/iris/engine/object/FloatingBottomPaletteMode.java new file mode 100644 index 000000000..cc9c938d4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/FloatingBottomPaletteMode.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls how floating-child island underside blocks choose their palette.") +public enum FloatingBottomPaletteMode { + @Desc("Use the normal top-down biome layer depth for the whole island column.") + DEPTH, + + @Desc("Use the target biome's top palette from both the island top and underside, meeting in the middle.") + MIRROR_TOP, + + @Desc("Use bottomPalette near the underside and the target biome's normal palette near the top.") + CUSTOM +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandBoundarySampler.java b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandBoundarySampler.java new file mode 100644 index 000000000..d1d9d01fb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandBoundarySampler.java @@ -0,0 +1,594 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import org.jetbrains.annotations.Nullable; + +import java.util.IdentityHashMap; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public final class FloatingIslandBoundarySampler { + static final int EDGE_TAPER_WIDTH = FloatingIslandEdgeProfile.DEFAULT_WIDTH; + static final int EDGE_FADE_RADIUS = EDGE_TAPER_WIDTH + 1; + private static final int CHUNK_SIZE = 16; + private static final int MIN_CORE_DISTANCE = 3; + + private final BiomeSampler source; + private final ConcurrentHashMap parentFields; + private final ConcurrentHashMap footprintFields; + private final ConcurrentHashMap ownershipFields; + + public FloatingIslandBoundarySampler(BiomeSampler source) { + this.source = Objects.requireNonNull(source); + this.parentFields = new ConcurrentHashMap<>(); + this.footprintFields = new ConcurrentHashMap<>(); + this.ownershipFields = new ConcurrentHashMap<>(); + } + + public @Nullable IrisBiome parent(int x, int z) { + return parentField(x, z, EDGE_TAPER_WIDTH).parent(x, z); + } + + public double edgeFade(IrisBiome parent, int x, int z) { + return edgeFade(parent, x, z, EDGE_TAPER_WIDTH); + } + + public double edgeFade(IrisBiome parent, int x, int z, int taperWidth) { + int boundaryDistance = edgeDistance(parent, x, z, taperWidth); + return edgeFadeForDistance(boundaryDistance, taperWidth); + } + + public int edgeDistance(IrisBiome parent, int x, int z, int taperWidth) { + if (parent == null) { + return 0; + } + + return parentField(x, z, taperWidth).edgeDistance(parent, x, z); + } + + public FootprintSample footprint(CNG footprint, int x, int z, double signedCut) { + return footprint(footprint, x, z, signedCut, EDGE_TAPER_WIDTH); + } + + public FootprintSample footprint(CNG footprint, int x, int z, double signedCut, int taperWidth) { + int width = FloatingIslandEdgeProfile.clampWidth(taperWidth); + int chunkX = Math.floorDiv(x, CHUNK_SIZE); + int chunkZ = Math.floorDiv(z, CHUNK_SIZE); + FootprintFieldKey key = new FootprintFieldKey( + footprint, chunkX, chunkZ, Double.doubleToLongBits(signedCut), width); + FootprintField field = footprintFields.computeIfAbsent(key, + ignored -> new FootprintField(footprint, chunkX, chunkZ, signedCut, width)); + return field.sample(x, z); + } + + public OwnershipSample ownership(KList entries, CNG picker, int x, int z) { + return ownership(entries, picker, x, z, EDGE_TAPER_WIDTH); + } + + public OwnershipSample ownership(KList entries, CNG picker, int x, int z, + int taperWidth) { + int width = FloatingIslandEdgeProfile.clampWidth(taperWidth); + int chunkX = Math.floorDiv(x, CHUNK_SIZE); + int chunkZ = Math.floorDiv(z, CHUNK_SIZE); + OwnershipFieldKey key = new OwnershipFieldKey(entries, picker, chunkX, chunkZ, width); + OwnershipField field = ownershipFields.computeIfAbsent(key, + ignored -> new OwnershipField(entries, picker, chunkX, chunkZ, width)); + return field.sample(x, z); + } + + static double edgeFadeForDistance(int boundaryDistance) { + return edgeFadeForDistance(boundaryDistance, EDGE_TAPER_WIDTH); + } + + static double edgeFadeForDistance(int boundaryDistance, int taperWidth) { + int width = FloatingIslandEdgeProfile.clampWidth(taperWidth); + double normalized = Math.max(0.0D, Math.min(1.0D, (boundaryDistance - 1.0D) / width)); + return normalized * normalized * (3.0D - (2.0D * normalized)); + } + + private ParentBoundaryField parentField(int x, int z, int taperWidth) { + int width = FloatingIslandEdgeProfile.clampWidth(taperWidth); + int chunkX = Math.floorDiv(x, CHUNK_SIZE); + int chunkZ = Math.floorDiv(z, CHUNK_SIZE); + ParentFieldKey key = new ParentFieldKey(Cache.key(chunkX, chunkZ), width); + return parentFields.computeIfAbsent(key, + ignored -> new ParentBoundaryField(source, chunkX, chunkZ, width)); + } + + private static boolean sameBiome(@Nullable IrisBiome expected, @Nullable IrisBiome actual) { + if (actual == expected) { + return true; + } + if (expected == null || actual == null || expected.getLoadKey() == null) { + return false; + } + return expected.getLoadKey().equals(actual.getLoadKey()); + } + + public record FootprintSample(double signed, int cardinalSupport, int diagonalSupport, boolean accepted, + int boundaryDistance, double edgeFade) { + } + + public record OwnershipSample(IrisFloatingChildBiomes owner, int boundaryDistance, double edgeFade) { + } + + private record ParentFieldKey(long chunkKey, int taperWidth) { + } + + private static final class FootprintFieldKey { + private final CNG footprint; + private final int chunkX; + private final int chunkZ; + private final long signedCutBits; + private final int taperWidth; + + private FootprintFieldKey(CNG footprint, int chunkX, int chunkZ, long signedCutBits, int taperWidth) { + this.footprint = footprint; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.signedCutBits = signedCutBits; + this.taperWidth = taperWidth; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof FootprintFieldKey key)) { + return false; + } + return footprint == key.footprint && chunkX == key.chunkX && chunkZ == key.chunkZ + && signedCutBits == key.signedCutBits && taperWidth == key.taperWidth; + } + + @Override + public int hashCode() { + int result = System.identityHashCode(footprint); + result = (31 * result) + chunkX; + result = (31 * result) + chunkZ; + result = (31 * result) + Long.hashCode(signedCutBits); + return (31 * result) + taperWidth; + } + } + + private static final class OwnershipFieldKey { + private final KList entries; + private final CNG picker; + private final int chunkX; + private final int chunkZ; + private final int taperWidth; + + private OwnershipFieldKey(KList entries, CNG picker, int chunkX, int chunkZ, + int taperWidth) { + this.entries = entries; + this.picker = picker; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.taperWidth = taperWidth; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof OwnershipFieldKey key)) { + return false; + } + return entries == key.entries && picker == key.picker && chunkX == key.chunkX && chunkZ == key.chunkZ + && taperWidth == key.taperWidth; + } + + @Override + public int hashCode() { + int result = System.identityHashCode(entries); + result = (31 * result) + System.identityHashCode(picker); + result = (31 * result) + chunkX; + result = (31 * result) + chunkZ; + return (31 * result) + taperWidth; + } + } + + private static final class ParentBoundaryField { + private final FieldGeometry geometry; + private final int minX; + private final int minZ; + private final IrisBiome[] parents; + private final byte[] distance; + private final boolean[] viable; + + private ParentBoundaryField(BiomeSampler source, int chunkX, int chunkZ, int taperWidth) { + this.geometry = FieldGeometry.forWidth(taperWidth); + this.minX = (chunkX * CHUNK_SIZE) - geometry.fadeRadius(); + this.minZ = (chunkZ * CHUNK_SIZE) - geometry.fadeRadius(); + this.parents = new IrisBiome[geometry.area()]; + this.distance = new byte[geometry.area()]; + this.viable = new boolean[geometry.area()]; + build(source); + } + + private @Nullable IrisBiome parent(int x, int z) { + return parents[geometry.index(x - minX, z - minZ)]; + } + + private int edgeDistance(IrisBiome expected, int x, int z) { + int index = geometry.index(x - minX, z - minZ); + if (!sameBiome(expected, parents[index]) || !viable[index]) { + return 0; + } + return Math.min(geometry.maxRenderableDistance(), Byte.toUnsignedInt(distance[index])); + } + + private void build(BiomeSampler source) { + int rawSize = geometry.fieldSize() + 2; + IrisBiome[] raw = new IrisBiome[rawSize * rawSize]; + int rawMinX = minX - 1; + int rawMinZ = minZ - 1; + for (int z = 0; z < rawSize; z++) { + for (int x = 0; x < rawSize; x++) { + raw[rawIndex(x, z, rawSize)] = source.sample(rawMinX + x, rawMinZ + z); + } + } + for (int z = 0; z < geometry.fieldSize(); z++) { + for (int x = 0; x < geometry.fieldSize(); x++) { + int index = geometry.index(x, z); + IrisBiome parent = raw[rawIndex(x + 1, z + 1, rawSize)]; + parents[index] = parent; + distance[index] = (byte) (touchesDifferentBiome(raw, parent, x, z, rawSize) + ? 1 + : geometry.maxDistance()); + } + } + propagateDistanceField(distance, geometry); + markLabelCoreSupported(parents, distance, viable, geometry); + } + + private static boolean touchesDifferentBiome(IrisBiome[] raw, @Nullable IrisBiome parent, int x, int z, + int rawSize) { + for (int dz = -1; dz <= 1; dz++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dz == 0) { + continue; + } + if (!sameBiome(parent, raw[rawIndex(x + dx + 1, z + dz + 1, rawSize)])) { + return true; + } + } + } + return false; + } + + private static int rawIndex(int x, int z, int rawSize) { + return (z * rawSize) + x; + } + } + + private static final class FootprintField { + private final FieldGeometry geometry; + private final int occupancyMinX; + private final int occupancyMinZ; + private final double[] signed; + private final byte[] cardinalSupport; + private final byte[] diagonalSupport; + private final boolean[] accepted; + private final byte[] distance; + private final boolean[] viable; + + private FootprintField(CNG footprint, int chunkX, int chunkZ, double signedCut, int taperWidth) { + this.geometry = FieldGeometry.forWidth(taperWidth); + this.occupancyMinX = (chunkX * CHUNK_SIZE) - geometry.fadeRadius(); + this.occupancyMinZ = (chunkZ * CHUNK_SIZE) - geometry.fadeRadius(); + this.signed = new double[geometry.area()]; + this.cardinalSupport = new byte[geometry.area()]; + this.diagonalSupport = new byte[geometry.area()]; + this.accepted = new boolean[geometry.area()]; + this.distance = new byte[geometry.area()]; + this.viable = new boolean[geometry.area()]; + build(footprint, signedCut); + } + + private FootprintSample sample(int x, int z) { + int index = geometry.index(x - occupancyMinX, z - occupancyMinZ); + int inwardDistance = Byte.toUnsignedInt(distance[index]); + boolean renderable = accepted[index] && viable[index] && inwardDistance > 1; + int boundaryDistance = renderable + ? Math.min(geometry.maxRenderableDistance(), inwardDistance) + : 0; + double fade = edgeFadeForDistance(boundaryDistance, geometry.taperWidth()); + return new FootprintSample(signed[index], Byte.toUnsignedInt(cardinalSupport[index]), + Byte.toUnsignedInt(diagonalSupport[index]), renderable, boundaryDistance, fade); + } + + private void build(CNG footprint, double signedCut) { + double[] rawSigned = sampleRawFootprint(footprint); + buildOccupancy(rawSigned, signedCut); + buildDistanceField(); + } + + private double[] sampleRawFootprint(CNG footprint) { + int rawSize = geometry.fieldSize() + 2; + double[] rawSigned = new double[rawSize * rawSize]; + int rawMinX = occupancyMinX - 1; + int rawMinZ = occupancyMinZ - 1; + for (int z = 0; z < rawSize; z++) { + for (int x = 0; x < rawSize; x++) { + rawSigned[rawIndex(x, z, rawSize)] = signedFromUnit(footprint.noise(rawMinX + x, rawMinZ + z)); + } + } + return rawSigned; + } + + private void buildOccupancy(double[] rawSigned, double signedCut) { + int rawSize = geometry.fieldSize() + 2; + for (int z = 0; z < geometry.fieldSize(); z++) { + for (int x = 0; x < geometry.fieldSize(); x++) { + int index = geometry.index(x, z); + double value = rawSigned[rawIndex(x + 1, z + 1, rawSize)]; + int cardinal = 0; + int diagonal = 0; + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) { + continue; + } + if (rawSigned[rawIndex(x + dx + 1, z + dz + 1, rawSize)] <= signedCut) { + continue; + } + if (Math.abs(dx) + Math.abs(dz) == 1) { + cardinal++; + } else { + diagonal++; + } + } + } + boolean solid = value > signedCut; + FloatingIslandSample.NeighborSupport support = new FloatingIslandSample.NeighborSupport(cardinal, diagonal); + boolean supported = support.hasSolidSupport(); + boolean repairedPinhole = FloatingIslandSample.isFootprintPinholeRepairable(support); + signed[index] = value; + cardinalSupport[index] = (byte) cardinal; + diagonalSupport[index] = (byte) diagonal; + accepted[index] = (solid && supported) || (!solid && repairedPinhole); + } + } + } + + private void buildDistanceField() { + fillDistanceField(accepted, distance, geometry); + markCoreSupported(accepted, distance, viable, geometry); + } + + private static int rawIndex(int x, int z, int rawSize) { + return (z * rawSize) + x; + } + + private static double signedFromUnit(double value) { + return (Math.max(0.0D, Math.min(1.0D, value)) * 2.0D) - 1.0D; + } + } + + private static final class OwnershipField { + private final FieldGeometry geometry; + private final int minX; + private final int minZ; + private final IrisFloatingChildBiomes[] owners; + private final IdentityHashMap distances; + + private OwnershipField(KList entries, CNG picker, int chunkX, int chunkZ, + int taperWidth) { + this.geometry = FieldGeometry.forWidth(taperWidth); + this.minX = (chunkX * CHUNK_SIZE) - geometry.fadeRadius(); + this.minZ = (chunkZ * CHUNK_SIZE) - geometry.fadeRadius(); + this.owners = new IrisFloatingChildBiomes[geometry.area()]; + this.distances = new IdentityHashMap<>(); + for (int z = 0; z < geometry.fieldSize(); z++) { + for (int x = 0; x < geometry.fieldSize(); x++) { + double value = Math.max(0.0D, Math.min(1.0D, picker.noise(minX + x, minZ + z))); + owners[geometry.index(x, z)] = IRare.pick(entries, value); + } + } + for (IrisFloatingChildBiomes entry : entries) { + if (entry != null && !distances.containsKey(entry)) { + distances.put(entry, buildDistanceField(entry)); + } + } + } + + private OwnershipSample sample(int x, int z) { + int index = geometry.index(x - minX, z - minZ); + IrisFloatingChildBiomes owner = owners[index]; + if (owner == null) { + return new OwnershipSample(null, 0, 0.0D); + } + ComponentDistanceField field = distances.get(owner); + if (field == null) { + return new OwnershipSample(null, 0, 0.0D); + } + int inwardDistance = Byte.toUnsignedInt(field.distance()[index]); + int boundaryDistance = field.viable()[index] + ? Math.min(geometry.maxRenderableDistance(), inwardDistance) + : 0; + double fade = edgeFadeForDistance(boundaryDistance, geometry.taperWidth()); + return new OwnershipSample(owner, boundaryDistance, fade); + } + + private ComponentDistanceField buildDistanceField(IrisFloatingChildBiomes owner) { + boolean[] owned = new boolean[owners.length]; + byte[] distance = new byte[owners.length]; + boolean[] viable = new boolean[owners.length]; + for (int i = 0; i < owners.length; i++) { + owned[i] = owners[i] == owner; + } + fillDistanceField(owned, distance, geometry); + markCoreSupported(owned, distance, viable, geometry); + return new ComponentDistanceField(distance, viable); + } + } + + private record ComponentDistanceField(byte[] distance, boolean[] viable) { + } + + private record FieldGeometry(int taperWidth, int fadeRadius, int fieldSize, int maxDistance) { + private static FieldGeometry forWidth(int taperWidth) { + int width = FloatingIslandEdgeProfile.clampWidth(taperWidth); + int radius = width + 1; + return new FieldGeometry(width, radius, CHUNK_SIZE + (radius * 2), radius + 1); + } + + private int area() { + return fieldSize * fieldSize; + } + + private int maxRenderableDistance() { + return fadeRadius; + } + + private int index(int x, int z) { + return (z * fieldSize) + x; + } + } + + private static void fillDistanceField(boolean[] inside, byte[] distance, FieldGeometry geometry) { + for (int i = 0; i < inside.length; i++) { + distance[i] = (byte) (inside[i] ? geometry.maxDistance() : 0); + } + for (int z = 0; z < geometry.fieldSize(); z++) { + for (int x = 0; x < geometry.fieldSize(); x++) { + if (!inside[geometry.index(x, z)]) { + continue; + } + setMinimumDistance(distance, x, z, x - 1, z, geometry); + setMinimumDistance(distance, x, z, x, z - 1, geometry); + setMinimumDistance(distance, x, z, x - 1, z - 1, geometry); + setMinimumDistance(distance, x, z, x + 1, z - 1, geometry); + } + } + for (int z = geometry.fieldSize() - 1; z >= 0; z--) { + for (int x = geometry.fieldSize() - 1; x >= 0; x--) { + if (!inside[geometry.index(x, z)]) { + continue; + } + setMinimumDistance(distance, x, z, x + 1, z, geometry); + setMinimumDistance(distance, x, z, x, z + 1, geometry); + setMinimumDistance(distance, x, z, x + 1, z + 1, geometry); + setMinimumDistance(distance, x, z, x - 1, z + 1, geometry); + } + } + } + + private static void propagateDistanceField(byte[] distance, FieldGeometry geometry) { + for (int z = 0; z < geometry.fieldSize(); z++) { + for (int x = 0; x < geometry.fieldSize(); x++) { + setMinimumDistance(distance, x, z, x - 1, z, geometry); + setMinimumDistance(distance, x, z, x, z - 1, geometry); + setMinimumDistance(distance, x, z, x - 1, z - 1, geometry); + setMinimumDistance(distance, x, z, x + 1, z - 1, geometry); + } + } + for (int z = geometry.fieldSize() - 1; z >= 0; z--) { + for (int x = geometry.fieldSize() - 1; x >= 0; x--) { + setMinimumDistance(distance, x, z, x + 1, z, geometry); + setMinimumDistance(distance, x, z, x, z + 1, geometry); + setMinimumDistance(distance, x, z, x + 1, z + 1, geometry); + setMinimumDistance(distance, x, z, x - 1, z + 1, geometry); + } + } + } + + private static void setMinimumDistance(byte[] distance, int x, int z, int neighborX, int neighborZ, + FieldGeometry geometry) { + if (neighborX < 0 || neighborX >= geometry.fieldSize() + || neighborZ < 0 || neighborZ >= geometry.fieldSize()) { + return; + } + int index = geometry.index(x, z); + int neighborDistance = Byte.toUnsignedInt(distance[geometry.index(neighborX, neighborZ)]); + int currentDistance = Byte.toUnsignedInt(distance[index]); + distance[index] = (byte) Math.min(currentDistance, neighborDistance + 1); + } + + private static void markCoreSupported(boolean[] inside, byte[] distance, boolean[] viable, + FieldGeometry geometry) { + for (int z = 0; z < geometry.fieldSize(); z++) { + for (int x = 0; x < geometry.fieldSize(); x++) { + int index = geometry.index(x, z); + if (!inside[index]) { + continue; + } + int inwardDistance = Byte.toUnsignedInt(distance[index]); + viable[index] = inwardDistance >= MIN_CORE_DISTANCE + || inwardDistance == MIN_CORE_DISTANCE - 1 + && hasCoreNeighbor(inside, distance, null, x, z, geometry); + } + } + } + + private static void markLabelCoreSupported(IrisBiome[] labels, byte[] distance, boolean[] viable, + FieldGeometry geometry) { + for (int z = 0; z < geometry.fieldSize(); z++) { + for (int x = 0; x < geometry.fieldSize(); x++) { + int index = geometry.index(x, z); + int inwardDistance = Byte.toUnsignedInt(distance[index]); + viable[index] = inwardDistance >= MIN_CORE_DISTANCE + || inwardDistance == MIN_CORE_DISTANCE - 1 + && hasCoreNeighbor(null, distance, labels, x, z, geometry); + } + } + } + + private static boolean hasCoreNeighbor(@Nullable boolean[] inside, byte[] distance, + @Nullable IrisBiome[] labels, int x, int z, FieldGeometry geometry) { + int index = geometry.index(x, z); + for (int dz = -1; dz <= 1; dz++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dz == 0) { + continue; + } + int neighborX = x + dx; + int neighborZ = z + dz; + if (neighborX < 0 || neighborX >= geometry.fieldSize() + || neighborZ < 0 || neighborZ >= geometry.fieldSize()) { + continue; + } + int neighbor = geometry.index(neighborX, neighborZ); + if (Byte.toUnsignedInt(distance[neighbor]) < MIN_CORE_DISTANCE) { + continue; + } + if (inside != null && !inside[neighbor]) { + continue; + } + if (labels != null && !sameBiome(labels[index], labels[neighbor])) { + continue; + } + return true; + } + } + return false; + } + + @FunctionalInterface + public interface BiomeSampler { + @Nullable IrisBiome sample(int x, int z); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandEdgeProfile.java b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandEdgeProfile.java new file mode 100644 index 000000000..e401bb58d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandEdgeProfile.java @@ -0,0 +1,91 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.util.project.noise.CNG; + +public final class FloatingIslandEdgeProfile { + public static final int DEFAULT_WIDTH = 10; + public static final int MIN_WIDTH = 2; + public static final int MAX_WIDTH = 32; + public static final double DEFAULT_EXPONENT = 1.0D; + public static final double MIN_EXPONENT = 0.25D; + public static final double MAX_EXPONENT = 4.0D; + public static final double MAX_VARIATION_AMPLITUDE = 8.0D; + public static final FloatingIslandEdgeProfile DEFAULT = new FloatingIslandEdgeProfile( + DEFAULT_WIDTH, DEFAULT_EXPONENT, 0.0D, null); + + private final int width; + private final double exponent; + private final double variationAmplitude; + private final CNG variation; + + public FloatingIslandEdgeProfile(int width, double exponent, double variationAmplitude, CNG variation) { + this.width = clampWidth(width); + this.exponent = clampExponent(exponent); + this.variationAmplitude = clampVariationAmplitude(variationAmplitude, this.width); + this.variation = variation; + } + + public int width() { + return width; + } + + public double exponent() { + return exponent; + } + + public double variationAmplitude() { + return variationAmplitude; + } + + public int fieldWidth() { + return Math.min(MAX_WIDTH, (int) Math.ceil(width + variationAmplitude)); + } + + public double fade(int boundaryDistance, int x, int z) { + if (boundaryDistance <= 1) { + return 0.0D; + } + double localWidth = width; + if (variation != null && variationAmplitude > 0.0D) { + double noise = Math.max(0.0D, Math.min(1.0D, variation.noise(x, z))); + double signedNoise = (noise * 2.0D) - 1.0D; + localWidth += signedNoise * variationAmplitude; + } + + double position = Math.max(0.0D, Math.min(1.0D, (boundaryDistance - 1.0D) / localWidth)); + double smooth = position * position * (3.0D - (2.0D * position)); + return exponent == DEFAULT_EXPONENT ? smooth : Math.pow(smooth, exponent); + } + + public static int clampWidth(int width) { + return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width)); + } + + public static double clampExponent(double exponent) { + return Math.max(MIN_EXPONENT, Math.min(MAX_EXPONENT, exponent)); + } + + public static double clampVariationAmplitude(double amplitude, int width) { + int clampedWidth = clampWidth(width); + double widthLimit = Math.min(clampedWidth - MIN_WIDTH, MAX_WIDTH - clampedWidth); + return Math.max(0.0D, Math.min(Math.min(MAX_VARIATION_AMPLITUDE, widthLimit), amplitude)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java new file mode 100644 index 000000000..147d6656b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java @@ -0,0 +1,800 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class FloatingIslandSample { + public static final int REJECT_NONE = 0; + public static final int REJECT_NO_ENTRIES = 1; + public static final int REJECT_NO_SEED = 2; + public static final int REJECT_NO_PICK = 3; + public static final int REJECT_ABOVE_HEIGHT = 4; + public static final int REJECT_NO_THICKNESS = 5; + public static final int REJECT_NO_SOLID = 6; + public static final int REJECT_COUNT = 7; + public static final int REJECT_CLUSTER = REJECT_NO_SEED; + + private static final int PINHOLE_CARDINAL_FILL = 3; + private static final int PINHOLE_TOTAL_FILL = 7; + private static final int CARVE_CARDINAL_SUPPORT = 2; + private static final int CARVE_TOTAL_SUPPORT = 4; + private static final double CARVE_SHELL_FRACTION = 0.14D; + private static final double CARVE_MAX_VERTICAL_RUN_FRACTION = 0.18D; + private static final ThreadLocal LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]); + private static final ThreadLocal LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]); + private static final ThreadLocal> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new); + private static final ThreadLocal> FOOTPRINT_MEMO = ThreadLocal.withInitial(IdentityHashMap::new); + private static final AtomicBoolean NULL_CNG_WARNED = new AtomicBoolean(false); + + public static int getLastReject() { + return LAST_REJECT.get()[0]; + } + + public static double getLastClusterValue() { + return LAST_DENSITY.get()[0]; + } + + public static double getLastClusterThreshold() { + return LAST_DENSITY.get()[1]; + } + + public static void clearThreadCaches() { + } + + public static void clearChunkMemo() { + CHUNK_MEMO.get().clear(); + FOOTPRINT_MEMO.get().clear(); + } + + public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine, FloatingIslandBoundarySampler boundarySampler) { + long key = (((long) wx) << 32) ^ (wz & 0xFFFFFFFFL); + HashMap memo = CHUNK_MEMO.get(); + if (memo.containsKey(key)) { + return memo.get(key); + } + FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, engine, boundarySampler); + memo.put(key, result); + return result; + } + + private static FloatingIslandSample reject(int code) { + LAST_REJECT.get()[0] = code; + return null; + } + + private static void warnNullCng(String styleField, IrisBiome parent) { + if (NULL_CNG_WARNED.compareAndSet(false, true)) { + String biomeKey = parent == null ? "" : parent.getLoadKey(); + IrisLogging.warn("Floating child biome on " + biomeKey + " has a null CNG for " + styleField + + " (style factory returned null or AtomicCache swallowed an exception); skipping floating sampling until pack is fixed"); + } + } + + public final IrisFloatingChildBiomes entry; + public final int islandBaseY; + public final int thickness; + public final int topIdx; + public final int solidCount; + public final boolean[] solidMask; + private final IrisFloatingChildBiomes[] entryMask; + private transient int cachedBottomIdx = -2; + + private FloatingIslandSample(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) { + this(entry, islandBaseY, thickness, topIdx, solidCount, solidMask, null); + } + + private FloatingIslandSample(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask, IrisFloatingChildBiomes[] entryMask) { + this.entry = entry; + this.islandBaseY = islandBaseY; + this.thickness = thickness; + this.topIdx = topIdx; + this.solidCount = solidCount; + this.solidMask = solidMask; + this.entryMask = entryMask; + } + + static FloatingIslandSample constructForTest(int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) { + return new FloatingIslandSample(null, islandBaseY, thickness, topIdx, solidCount, solidMask); + } + + static FloatingIslandSample constructForTest(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask, IrisFloatingChildBiomes[] entryMask) { + return new FloatingIslandSample(entry, islandBaseY, thickness, topIdx, solidCount, solidMask, entryMask); + } + + public int topY() { + return islandBaseY + topIdx; + } + + public int bottomY() { + if (cachedBottomIdx == -2) { + cachedBottomIdx = -1; + for (int i = 0; i < solidMask.length; i++) { + if (solidMask[i]) { + cachedBottomIdx = i; + break; + } + } + } + return cachedBottomIdx == -1 ? -1 : islandBaseY + cachedBottomIdx; + } + + public IrisFloatingChildBiomes entryAt(int maskIndex) { + if (entryMask != null && maskIndex >= 0 && maskIndex < entryMask.length && entryMask[maskIndex] != null) { + return entryMask[maskIndex]; + } + + return entry; + } + + public IrisFloatingChildBiomes bottomEntry() { + int bottomY = bottomY(); + if (bottomY < 0) { + return entry; + } + + return entryAt(bottomY - islandBaseY); + } + + public boolean hasMergedEntries() { + return entryMask != null; + } + + public static long columnSeed(long baseSeed, int wx, int wz) { + return baseSeed ^ ((long) wx * 341873128712L) ^ ((long) wz * 132897987541L); + } + + private static int maximumEdgeFieldWidth(KList entries, long baseSeed, + IrisData data, IrisBiome parent) { + int maximumWidth = FloatingIslandEdgeProfile.MIN_WIDTH; + for (IrisFloatingChildBiomes entry : entries) { + FloatingIslandEdgeProfile profile = resolveEdgeProfile(entry, baseSeed, data, parent); + maximumWidth = Math.max(maximumWidth, profile.fieldWidth()); + } + return maximumWidth; + } + + private static FloatingIslandEdgeProfile resolveEdgeProfile(IrisFloatingChildBiomes entry, long baseSeed, + IrisData data, IrisBiome parent) { + FloatingIslandEdgeProfile profile = entry.getEdgeTaperProfile(baseSeed, data); + if (profile != null) { + return profile; + } + warnNullCng("edgeTaperVariationStyle", parent); + return FloatingIslandEdgeProfile.DEFAULT; + } + + public static FloatingIslandSample sample(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine, FloatingIslandBoundarySampler boundarySampler) { + KList entries = parent.getFloatingChildBiomes(); + if (entries == null || entries.isEmpty()) { + return reject(REJECT_NO_ENTRIES); + } + + int boundaryFieldWidth = maximumEdgeFieldWidth(entries, baseSeed, data, parent); + int parentBoundaryDistance = boundarySampler.edgeDistance(parent, wx, wz, boundaryFieldWidth); + + if (parent.isMergeFloatingChildBiomes()) { + return sampleMerged(parent, entries, wx, wz, chunkHeight, baseSeed, data, engine, boundarySampler, + parentBoundaryDistance, boundaryFieldWidth); + } + + IrisFloatingChildBiomes entry; + int ownershipBoundaryDistance = boundaryFieldWidth + 1; + if (entries.size() == 1) { + entry = entries.getFirst(); + } else { + IrisFloatingChildBiomes reference = entries.getFirst(); + CNG picker = reference.getPickerCng(baseSeed, data); + if (picker == null) { + warnNullCng("pickerStyle", parent); + return reject(REJECT_NO_PICK); + } + FloatingIslandBoundarySampler.OwnershipSample ownership = boundarySampler.ownership( + entries, picker, wx, wz, boundaryFieldWidth); + entry = ownership.owner(); + if (entry == null) { + return reject(REJECT_NO_PICK); + } + ownershipBoundaryDistance = ownership.boundaryDistance(); + } + + return sampleEntry(parent, entry, wx, wz, chunkHeight, baseSeed, data, engine, boundarySampler, + parentBoundaryDistance, ownershipBoundaryDistance); + } + + private static FloatingIslandSample sampleMerged(IrisBiome parent, KList entries, + int wx, int wz, int chunkHeight, long baseSeed, IrisData data, + Engine engine, FloatingIslandBoundarySampler boundarySampler, + int parentBoundaryDistance, int boundaryFieldWidth) { + KList samples = new KList<>(); + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + + for (IrisFloatingChildBiomes entry : entries) { + FloatingIslandSample sample = sampleEntry(parent, entry, wx, wz, chunkHeight, baseSeed, data, engine, + boundarySampler, parentBoundaryDistance, boundaryFieldWidth + 1); + if (sample == null) { + continue; + } + + int bottomY = sample.bottomY(); + if (bottomY < 0) { + continue; + } + + samples.add(sample); + if (bottomY < minY) { + minY = bottomY; + } + int topY = sample.topY(); + if (topY > maxY) { + maxY = topY; + } + } + + if (samples.isEmpty() || minY > maxY) { + return reject(REJECT_NO_SEED); + } + + int thickness = maxY - minY + 1; + boolean[] solidMask = new boolean[thickness]; + IrisFloatingChildBiomes[] entryMask = new IrisFloatingChildBiomes[thickness]; + int[] entryTopY = new int[thickness]; + Arrays.fill(entryTopY, Integer.MIN_VALUE); + int solidCount = 0; + + for (FloatingIslandSample sample : samples) { + int sampleTopY = sample.topY(); + for (int i = 0; i < sample.solidMask.length; i++) { + if (!sample.solidMask[i]) { + continue; + } + + int y = sample.islandBaseY + i; + int mergedIndex = y - minY; + if (!solidMask[mergedIndex]) { + solidMask[mergedIndex] = true; + solidCount++; + } + if (sampleTopY >= entryTopY[mergedIndex]) { + entryTopY[mergedIndex] = sampleTopY; + entryMask[mergedIndex] = sample.entryAt(i); + } + } + } + + int topIdx = highestSolidIndex(solidMask); + if (solidCount == 0 || topIdx < 0) { + return reject(REJECT_NO_SOLID); + } + + IrisFloatingChildBiomes topEntry = entryMask[topIdx] == null ? samples.getFirst().entry : entryMask[topIdx]; + LAST_REJECT.get()[0] = REJECT_NONE; + return new FloatingIslandSample(topEntry, minY, thickness, topIdx, solidCount, solidMask, entryMask); + } + + private static FloatingIslandSample sampleEntry(IrisBiome parent, IrisFloatingChildBiomes entry, int wx, int wz, + int chunkHeight, long baseSeed, IrisData data, Engine engine, + FloatingIslandBoundarySampler boundarySampler, + int parentBoundaryDistance, int ownershipBoundaryDistance) { + FloatingIslandEdgeProfile edgeProfile = resolveEdgeProfile(entry, baseSeed, data, parent); + CNG footprintCng = entry.getFootprintCng(baseSeed, data); + if (footprintCng == null) { + warnNullCng("footprintStyle", parent); + return reject(REJECT_NO_SEED); + } + double threshold = Math.max(0, Math.min(1, entry.getFootprintThreshold())); + double signedCut = (threshold * 2.0) - 1.0; + FloatingIslandBoundarySampler.FootprintSample footprint = boundarySampler.footprint( + footprintCng, wx, wz, signedCut, edgeProfile.fieldWidth()); + double signed = footprint.signed(); + + double[] diag = LAST_DENSITY.get(); + diag[0] = signed; + diag[1] = signedCut; + + NeighborSupport footprintSupport = new NeighborSupport(footprint.cardinalSupport(), footprint.diagonalSupport()); + if (!footprint.accepted()) { + return reject(REJECT_NO_SEED); + } + int boundaryDistance = Math.min(footprint.boundaryDistance(), + Math.min(parentBoundaryDistance, ownershipBoundaryDistance)); + double edgeFade = edgeProfile.fade(boundaryDistance, wx, wz); + if (edgeFade <= 0.0D) { + return reject(REJECT_NO_THICKNESS); + } + + CNG altitudeCng = entry.getAltitudeCng(baseSeed, data); + if (altitudeCng == null) { + warnNullCng("altitudeStyle", parent); + return reject(REJECT_NO_SEED); + } + double altNoise = altitudeCng.noise(wx, wz); + double altClamped = Math.max(0, Math.min(1, altNoise)); + int worldMin = engine.getWorld().minHeight(); + int minAlt = Math.max(0, entry.getMinHeightAboveSurface() - worldMin); + int maxAlt = Math.max(minAlt, entry.getMaxHeightAboveSurface() - worldMin); + int baseY = minAlt + (int) Math.round(altClamped * (maxAlt - minAlt)); + + IrisBiome target = entry.getRealBiome(parent, data); + int fullTopHeight = computeTopHeight(entry, target, engine, baseSeed, wx, wz, data); + int topH = roundedEdgeHeight(fullTopHeight, edgeFade); + + CNG bottomCng = entry.getBottomCng(baseSeed, data); + if (bottomCng == null) { + warnNullCng("bottomStyle", parent); + return reject(REJECT_NO_SEED); + } + double bottomNoise = bottomCng.noise(wx, wz); + double bottomClamped = Math.max(0, Math.min(1, bottomNoise)); + double bottomShaped = Math.pow(bottomClamped, Math.max(0.1, entry.getBottomExponent())); + int minDepth = Math.max(0, entry.getBottomDepthMin()); + int maxDepth = Math.max(minDepth, entry.getBottomDepthMax()); + int depth = roundedEdgeDepth(minDepth, maxDepth, bottomShaped, edgeFade); + if (topH == 0 && depth == 0) { + double fullDepth = minDepth + (bottomShaped * (maxDepth - minDepth)); + if (fullDepth > 0.0D) { + depth = 1; + } else if (fullTopHeight > 0) { + topH = 1; + } else { + return reject(REJECT_NO_THICKNESS); + } + } + int topY = baseY + topH; + int botY = baseY - depth; + + Integer minAbsoluteY = entry.getMinAbsoluteY(); + if (minAbsoluteY != null && botY < minAbsoluteY - worldMin) { + botY = minAbsoluteY - worldMin; + } + Integer maxAbsoluteY = entry.getMaxAbsoluteY(); + if (maxAbsoluteY != null && topY > maxAbsoluteY - worldMin) { + topY = maxAbsoluteY - worldMin; + } + + if (botY < 0) { + botY = 0; + } + if (topY >= chunkHeight) { + topY = chunkHeight - 1; + } + if (topY < botY) { + return reject(REJECT_ABOVE_HEIGHT); + } + + int thickness = topY - botY + 1; + int maxThickness = Math.max(1, entry.getMaxThickness()); + if (thickness > maxThickness) { + botY = topY - maxThickness + 1; + if (botY < 0) { + botY = 0; + } + thickness = topY - botY + 1; + } + if (thickness <= 0) { + return reject(REJECT_NO_THICKNESS); + } + + boolean[] solidMask = new boolean[thickness]; + CNG wallWarp = entry.getWallWarpCng(baseSeed, data); + double warpAmp = Math.max(0, entry.getWallWarpAmplitude()); + double effectiveWarpAmp = effectiveWallWarpAmplitude(warpAmp, edgeFade); + IrisCaveProfileSampler carvingProfileSampler = entry.getCarvingProfileSampler(engine, data); + CNG carve = carvingProfileSampler == null && !entry.hasCarvingReference() ? entry.getCarveCng(baseSeed, data) : null; + double carveThreshold = entry.getCarveThreshold(); + boolean useWarp = wallWarp != null && effectiveWarpAmp > 0; + boolean useProfileCarve = carvingProfileSampler != null; + boolean useCarve = directCarveEnabled(entry, carvingProfileSampler, carve, carveThreshold); + boolean[] lateralCarveMask = useProfileCarve || useCarve ? new boolean[thickness] : null; + boolean baseCarveInterior = lateralCarveMask != null && canCarveLaterally(edgeFade, footprintSupport); + + if (useWarp) { + for (int k = 0; k < thickness; k++) { + int wy = botY + k; + boolean layerSolid = layerFootprintSolid(footprintCng, wallWarp, true, effectiveWarpAmp, wx, wy, wz, signedCut); + NeighborSupport layerSupport = layerNeighborSupport(footprintCng, wallWarp, true, effectiveWarpAmp, wx, wy, wz, signedCut); + if (layerSolid && !layerSupport.hasSolidSupport()) { + continue; + } + if (!layerSolid && !layerSupport.canFillPinhole()) { + continue; + } + solidMask[k] = true; + if (lateralCarveMask != null) { + lateralCarveMask[k] = baseCarveInterior && layerSolid && layerSupport.isFullySurrounded(); + } + } + } else { + Arrays.fill(solidMask, true); + if (baseCarveInterior) { + Arrays.fill(lateralCarveMask, true); + } + } + + int solidCount = solidifyUncarvedInterior(solidMask); + if (useProfileCarve) { + solidCount = carveSolidInterior(solidMask, lateralCarveMask, botY, wx, wz, carvingProfileSampler, carveThreshold); + } else if (useCarve) { + solidCount = carveSolidInterior(solidMask, lateralCarveMask, botY, wx, wz, carve, carveThreshold); + } + int highestSolidIdx = highestSolidIndex(solidMask); + + if (solidCount == 0 || highestSolidIdx < 0) { + return reject(REJECT_NO_SOLID); + } + + int topIdx = highestSolidIdx; + + LAST_REJECT.get()[0] = REJECT_NONE; + return new FloatingIslandSample(entry, botY, thickness, topIdx, solidCount, solidMask); + } + + static int roundedEdgeHeight(int topHeight, double edgeFade) { + return Math.max(0, (int) Math.round(Math.max(0, topHeight) * Math.max(0, Math.min(1, edgeFade)))); + } + + static int roundedEdgeDepth(int minDepth, int maxDepth, double bottomShaped, double edgeFade) { + int min = Math.max(0, minDepth); + int max = Math.max(min, maxDepth); + double shaped = Math.max(0, Math.min(1, bottomShaped)); + double fade = Math.max(0, Math.min(1, edgeFade)); + double fullDepth = min + shaped * (max - min); + return (int) Math.round(fullDepth * fade); + } + + static double effectiveWallWarpAmplitude(double wallWarpAmplitude, double edgeFade) { + double amplitude = Math.max(0, wallWarpAmplitude); + double fade = Math.max(0, Math.min(1, edgeFade)); + return amplitude * fade; + } + + static boolean directCarveEnabled(IrisFloatingChildBiomes entry, IrisCaveProfileSampler carvingProfileSampler, CNG carve, double carveThreshold) { + return carvingProfileSampler == null && (entry == null || !entry.hasCarvingReference()) && carve != null && carveThreshold < 1.0; + } + + static boolean canCarveLaterally(double edgeFade, NeighborSupport support) { + return edgeFade >= 1.0 && support.isFullySurrounded(); + } + + static int carveSolidInterior(boolean[] solidMask, boolean[] lateralCarveMask, int botY, int wx, int wz, CNG carve, double carveThreshold) { + return carveSolidInterior(solidMask, lateralCarveMask, botY, wx, wz, (x, y, z) -> { + double carveNoise = carve.noise(x, y, z); + double carveClamped = Math.max(0, Math.min(1, carveNoise)); + return carveClamped > carveThreshold; + }); + } + + static int carveSolidInterior(boolean[] solidMask, boolean[] lateralCarveMask, int botY, int wx, int wz, IrisCaveProfileSampler carve, double carveThreshold) { + return carveSolidInterior(solidMask, lateralCarveMask, botY, wx, wz, (x, y, z) -> carve.shouldCarve(x, y, z, carveThreshold)); + } + + private static int carveSolidInterior(boolean[] solidMask, boolean[] lateralCarveMask, int botY, int wx, int wz, CarveSampler carve) { + int firstSolid = -1; + int lastSolid = -1; + for (int i = 0; i < solidMask.length; i++) { + if (!solidMask[i]) { + continue; + } + if (firstSolid < 0) { + firstSolid = i; + } + lastSolid = i; + } + if (firstSolid < 0) { + return 0; + } + int span = lastSolid - firstSolid + 1; + int shell = carveShellThickness(span); + int carveStart = firstSolid + shell; + int carveEnd = lastSolid - shell; + boolean[] carveMask = new boolean[solidMask.length]; + if (carveStart <= carveEnd) { + for (int i = carveStart; i <= carveEnd; i++) { + if (!solidMask[i] || !lateralCarveMask[i]) { + continue; + } + + int y = botY + i; + if (carve.shouldCarve(wx, y, wz) && hasCarveClusterSupport(carve, wx, y, wz)) { + carveMask[i] = true; + } + } + clampVerticalCarveRuns(carveMask, carveStart, carveEnd, maxVerticalCarveRun(span)); + for (int i = carveStart; i <= carveEnd; i++) { + if (carveMask[i]) { + solidMask[i] = false; + } + } + } + int count = 0; + for (boolean solid : solidMask) { + if (solid) { + count++; + } + } + return count; + } + + static int carveShellThickness(int solidSpan) { + if (solidSpan <= 4) { + return 1; + } + + int proportional = (int) Math.ceil(solidSpan * CARVE_SHELL_FRACTION); + return Math.max(2, Math.min(5, proportional)); + } + + static int maxVerticalCarveRun(int solidSpan) { + int proportional = (int) Math.round(solidSpan * CARVE_MAX_VERTICAL_RUN_FRACTION); + return Math.max(2, Math.min(6, proportional)); + } + + static void clampVerticalCarveRuns(boolean[] carveMask, int start, int end, int maxRun) { + int i = start; + while (i <= end) { + if (!carveMask[i]) { + i++; + continue; + } + + int runStart = i; + while (i <= end && carveMask[i]) { + i++; + } + int runEnd = i - 1; + int runLength = runEnd - runStart + 1; + if (runLength <= maxRun) { + continue; + } + + int keepStart = runStart + ((runLength - maxRun) / 2); + int keepEnd = keepStart + maxRun - 1; + for (int j = runStart; j <= runEnd; j++) { + carveMask[j] = j >= keepStart && j <= keepEnd; + } + } + } + + private static boolean hasCarveClusterSupport(CarveSampler carve, int wx, int wy, int wz) { + int cardinal = 0; + int diagonal = 0; + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) { + continue; + } + if (!carve.shouldCarve(wx + dx, wy, wz + dz)) { + continue; + } + if (Math.abs(dx) + Math.abs(dz) == 1) { + cardinal++; + } else { + diagonal++; + } + } + } + return cardinal >= CARVE_CARDINAL_SUPPORT && cardinal + diagonal >= CARVE_TOTAL_SUPPORT; + } + + static boolean hasFootprintNeighborSupport(CNG footprintCng, int wx, int wz, double signedCut) { + return footprintNeighborSupport(footprintCng, wx, wz, signedCut).hasSolidSupport(); + } + + static boolean isFootprintPinholeRepairable(NeighborSupport support) { + return support.canFillPinhole(); + } + + static NeighborSupport footprintNeighborSupport(CNG footprintCng, int wx, int wz, double signedCut) { + int cardinal = 0; + int diagonal = 0; + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) { + continue; + } + double signed = footprintSigned(footprintCng, wx + dx, wz + dz); + if (signed <= signedCut) { + continue; + } + if (Math.abs(dx) + Math.abs(dz) == 1) { + cardinal++; + } else { + diagonal++; + } + } + } + return new NeighborSupport(cardinal, diagonal); + } + + static NeighborSupport layerNeighborSupport(CNG footprintCng, CNG wallWarp, boolean useWarp, double warpAmp, int wx, int wy, int wz, double signedCut) { + int cardinal = 0; + int diagonal = 0; + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) { + continue; + } + if (!layerFootprintSolid(footprintCng, wallWarp, useWarp, warpAmp, wx + dx, wy, wz + dz, signedCut)) { + continue; + } + if (Math.abs(dx) + Math.abs(dz) == 1) { + cardinal++; + } else { + diagonal++; + } + } + } + return new NeighborSupport(cardinal, diagonal); + } + + static boolean layerFootprintSolid(CNG footprintCng, CNG wallWarp, boolean useWarp, double warpAmp, int wx, int wy, int wz, double signedCut) { + double sx = wx; + double sz = wz; + if (useWarp) { + double wnX = wallWarp.noise(wx, wy, wz); + double signedWarpX = signedFromUnit(wnX); + sx = wx + signedWarpX * warpAmp; + double wnZ = wallWarp.noise(wx + 1987.3, wy, wz + 2341.1); + double signedWarpZ = signedFromUnit(wnZ); + sz = wz + signedWarpZ * warpAmp; + } + double layerFoot = footprintCng.noise(sx, sz); + return signedFromUnit(layerFoot) > signedCut; + } + + private static double footprintSigned(CNG footprintCng, int wx, int wz) { + IdentityHashMap memoByCng = FOOTPRINT_MEMO.get(); + Long2DoubleOpenHashMap memo = memoByCng.get(footprintCng); + if (memo == null) { + memo = new Long2DoubleOpenHashMap(512); + memo.defaultReturnValue(Double.NaN); + memoByCng.put(footprintCng, memo); + } + + long key = columnKey(wx, wz); + double cached = memo.get(key); + if (!Double.isNaN(cached)) { + return cached; + } + + double signed = signedFromUnit(footprintCng.noise(wx, wz)); + memo.put(key, signed); + return signed; + } + + private static long columnKey(int wx, int wz) { + return ((long) wx << 32) ^ (wz & 0xFFFFFFFFL); + } + + private static double signedFromUnit(double value) { + return (Math.max(0, Math.min(1, value)) * 2.0) - 1.0; + } + + static int solidifyUncarvedInterior(boolean[] solidMask) { + int firstSolid = -1; + int lastSolid = -1; + for (int i = 0; i < solidMask.length; i++) { + if (!solidMask[i]) { + continue; + } + if (firstSolid < 0) { + firstSolid = i; + } + lastSolid = i; + } + if (firstSolid < 0) { + return 0; + } + for (int i = firstSolid; i <= lastSolid; i++) { + solidMask[i] = true; + } + return lastSolid - firstSolid + 1; + } + + private static int highestSolidIndex(boolean[] solidMask) { + for (int i = solidMask.length - 1; i >= 0; i--) { + if (solidMask[i]) { + return i; + } + } + return -1; + } + + @FunctionalInterface + private interface CarveSampler { + boolean shouldCarve(int x, int y, int z); + } + + static final class NeighborSupport { + private final int cardinal; + private final int diagonal; + + NeighborSupport(int cardinal, int diagonal) { + this.cardinal = cardinal; + this.diagonal = diagonal; + } + + int cardinal() { + return cardinal; + } + + int diagonal() { + return diagonal; + } + + int total() { + return cardinal + diagonal; + } + + boolean hasSolidSupport() { + return cardinal > 0 || diagonal >= 2; + } + + boolean canFillPinhole() { + return cardinal >= PINHOLE_CARDINAL_FILL && total() >= PINHOLE_TOTAL_FILL; + } + + boolean isFullySurrounded() { + return cardinal == 4 && diagonal == 4; + } + } + + private static int computeTopHeight(IrisFloatingChildBiomes entry, IrisBiome target, Engine engine, long baseSeed, int wx, int wz, IrisData data) { + int maxTopHeight = Math.max(0, entry.getMaxTopHeight()); + if (maxTopHeight == 0) { + return 0; + } + return switch (entry.getTopShapeMode()) { + case FLAT -> maxTopHeight; + case NOISE -> { + CNG topCng = entry.getTopShapeCng(baseSeed, data); + if (topCng == null) { + warnNullCng("topShapeStyle", null); + yield maxTopHeight / 2; + } + double n = topCng.noise(wx, wz); + double clamped = Math.max(0, Math.min(1, n)); + double amp = Math.max(0, Math.min(1, entry.getTopShapeAmp())); + yield (int) Math.round(clamped * amp * maxTopHeight); + } + case BIOME -> { + if (target == null) { + yield maxTopHeight / 2; + } + double h = target.getHeight(engine, wx, wz, baseSeed); + int rounded = (int) Math.round(h); + if (rounded < 0) { + yield 0; + } + yield Math.min(maxTopHeight, rounded); + } + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/FloatingObjectFootprint.java b/core/src/main/java/art/arcane/iris/engine/object/FloatingObjectFootprint.java new file mode 100644 index 000000000..45ab1b21c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/FloatingObjectFootprint.java @@ -0,0 +1,170 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import java.util.HashMap; +import java.util.Map; + +public class FloatingObjectFootprint { + private final int lowestSolidKeyY; + private final int highestSolidKeyY; + private final int centerX; + private final int centerY; + private final int centerZ; + private final int tallestKx; + private final int tallestKz; + private final int tallestKxBottom; + private final int tallestKzBottom; + private final long[] footprintXZ; + + private FloatingObjectFootprint(int lowestSolidKeyY, int highestSolidKeyY, int centerX, int centerY, int centerZ, int tallestKx, int tallestKz, int tallestKxBottom, int tallestKzBottom, long[] footprintXZ) { + this.lowestSolidKeyY = lowestSolidKeyY; + this.highestSolidKeyY = highestSolidKeyY; + this.centerX = centerX; + this.centerY = centerY; + this.centerZ = centerZ; + this.tallestKx = tallestKx; + this.tallestKz = tallestKz; + this.tallestKxBottom = tallestKxBottom; + this.tallestKzBottom = tallestKzBottom; + this.footprintXZ = footprintXZ; + } + + /** + * Memoized per object instance, never in a global map: loadKey + declared dimensions is + * not unique across packs or across hotloads (a block edit keeps the header W/H/D), so a + * shared key space served stale or foreign footprints. A reload builds a new IrisObject, + * which drops the memo for free. + */ + public static FloatingObjectFootprint compute(IrisObject obj) { + return obj.floatingFootprint.aquire(() -> doCompute(obj)); + } + + private static FloatingObjectFootprint doCompute(IrisObject obj) { + int cx = obj.getCenter().getBlockX(); + int cy = obj.getCenter().getBlockY(); + int cz = obj.getCenter().getBlockZ(); + Map columnStats = new HashMap<>(); + + int[] globalHighestY = {Integer.MIN_VALUE}; + int[] globalHighestKx = {0}; + int[] globalHighestKz = {0}; + + obj.getBlocks().forEach((key, bd) -> { + if (!bd.isSolid()) { + return; + } + int kx = key.getBlockX(); + int ky = key.getBlockY(); + int kz = key.getBlockZ(); + long packed = ((long) kx << 32) | (kz & 0xFFFFFFFFL); + int[] stats = columnStats.get(packed); + if (stats == null) { + stats = new int[]{ky, 1}; + columnStats.put(packed, stats); + } else { + if (ky < stats[0]) { + stats[0] = ky; + } + stats[1]++; + } + if (ky > globalHighestY[0]) { + globalHighestY[0] = ky; + globalHighestKx[0] = kx; + globalHighestKz[0] = kz; + } + }); + + long[] footprintArray = new long[columnStats.size()]; + int idx = 0; + for (Long packed : columnStats.keySet()) { + footprintArray[idx++] = packed; + } + + long tallestPacked = resolveTallestColumn(columnStats); + int lowestSolidKeyY = columnStats.isEmpty() ? cy : columnStats.get(tallestPacked)[0]; + int highestSolidKeyY = columnStats.isEmpty() ? cy : globalHighestY[0]; + int tallestKx = columnStats.isEmpty() ? 0 : (int) (tallestPacked >> 32); + int tallestKz = columnStats.isEmpty() ? 0 : (int) (tallestPacked & 0xFFFFFFFFL); + int tallestKxBottom = columnStats.isEmpty() ? 0 : globalHighestKx[0]; + int tallestKzBottom = columnStats.isEmpty() ? 0 : globalHighestKz[0]; + return new FloatingObjectFootprint(lowestSolidKeyY, highestSolidKeyY, cx, cy, cz, tallestKx, tallestKz, tallestKxBottom, tallestKzBottom, footprintArray); + } + + private static long resolveTallestColumn(Map columnStats) { + long bestPacked = 0L; + int tallestCount = 0; + int tallestLow = Integer.MAX_VALUE; + for (Map.Entry e : columnStats.entrySet()) { + int[] stats = e.getValue(); + int count = stats[1]; + int low = stats[0]; + if (count > tallestCount || (count == tallestCount && low < tallestLow)) { + tallestCount = count; + tallestLow = low; + bestPacked = e.getKey(); + } + } + return bestPacked; + } + + public int lowestSolidRelCenterY() { + return lowestSolidKeyY - centerY; + } + + public int getLowestSolidKeyY() { + return lowestSolidKeyY; + } + + public int getCenterX() { + return centerX; + } + + public int getCenterY() { + return centerY; + } + + public int getCenterZ() { + return centerZ; + } + + public int getTallestKx() { + return tallestKx; + } + + public int getTallestKz() { + return tallestKz; + } + + public int getHighestSolidKeyY() { + return highestSolidKeyY; + } + + public int getTallestKxBottom() { + return tallestKxBottom; + } + + public int getTallestKzBottom() { + return tallestKzBottom; + } + + public long[] footprintXZ() { + return footprintXZ; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IObjectLoot.java b/core/src/main/java/art/arcane/iris/engine/object/IObjectLoot.java new file mode 100644 index 000000000..6f7b64e1a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IObjectLoot.java @@ -0,0 +1,13 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; + +public interface IObjectLoot { + KList getFilter(); + KList getFilter(IrisData manager); + boolean isExact(); + String getName(); + int getWeight(); +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IObjectPlacer.java b/core/src/main/java/art/arcane/iris/engine/object/IObjectPlacer.java new file mode 100644 index 000000000..305f410d4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IObjectPlacer.java @@ -0,0 +1,62 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.PlatformBlockState; +import org.jetbrains.annotations.Nullable; + +public interface IObjectPlacer { + int getHighest(int x, int z, IrisData data); + + int getHighest(int x, int z, IrisData data, boolean ignoreFluid); + + void set(int x, int y, int z, PlatformBlockState d); + + PlatformBlockState get(int x, int y, int z); + + boolean isPreventingDecay(); + + boolean isCarved(int x, int y, int z); + + /** + * Whether the block at this position is real, solid ground. Only live-world placers can answer this; + * at mantle time terrain has not been written yet, so the height stream is the authority. + */ + default boolean isSurfaceSolid(int x, int y, int z) { + return true; + } + + boolean isSolid(int x, int y, int z); + + boolean isUnderwater(int x, int z); + + int getFluidHeight(); + + boolean isDebugSmartBore(); + + void setTile(int xx, int yy, int zz, TileData tile); + + void setData(int xx, int yy, int zz, T data); + + @Nullable T getData(int xx, int yy, int zz, Class t); + + Engine getEngine(); +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IPostBlockAccess.java b/core/src/main/java/art/arcane/iris/engine/object/IPostBlockAccess.java new file mode 100644 index 000000000..d1c50bc1f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IPostBlockAccess.java @@ -0,0 +1,37 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import org.bukkit.generator.ChunkGenerator.ChunkData; + +public interface IPostBlockAccess { + PlatformBlockState getPostBlock(int x, int y, int z, int currentPostX, int currentPostZ, ChunkData currentData); + + void setPostBlock(int x, int y, int z, PlatformBlockState d, int currentPostX, int currentPostZ, ChunkData currentData); + + int highestTerrainOrFluidBlock(int x, int z); + + int highestTerrainBlock(int x, int z); + + void updateHeight(int x, int z, int h); + + KList caveFloors(int x, int z); +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IRare.java b/core/src/main/java/art/arcane/iris/engine/object/IRare.java new file mode 100644 index 000000000..2c13b2708 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IRare.java @@ -0,0 +1,115 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; +import java.util.List; + +public interface IRare { + static ProceduralStream stream(ProceduralStream noise, List possibilities) { + return ProceduralStream.of((x, z) -> pick(possibilities, noise.get(x, z)), + (x, y, z) -> pick(possibilities, noise.get(x, y, z)), + new Interpolated() { + @Override + public double toDouble(T t) { + return 0; + } + + @Override + public T fromDouble(double d) { + return null; + } + }); + } + + + static T pickSlowly(List possibilities, double noiseValue) { + if (possibilities.isEmpty()) { + return null; + } + + if (possibilities.size() == 1) { + return possibilities.get(0); + } + + KList rarityTypes = new KList<>(); + int totalRarity = 0; + for (T i : possibilities) { + totalRarity += IRare.get(i); + } + + for (T i : possibilities) { + rarityTypes.addMultiple(i, totalRarity / IRare.get(i)); + } + + return rarityTypes.get((int) (noiseValue * rarityTypes.last())); + } + + static T pick(List possibilities, double noiseValue) { + if (possibilities.isEmpty()) { + return null; + } + + if (possibilities.size() == 1) { + return possibilities.getFirst(); + } + + double total = 0; + for (T i : possibilities) { + total += 1d / IRare.get(i); + } + + double threshold = total * noiseValue; + double buffer = 0; + for (T i : possibilities) { + buffer += 1d / IRare.get(i); + if (buffer >= threshold) { + return i; + } + } + + return possibilities.getLast(); + } + + static int get(Object v) { + return v instanceof IRare ? Math.max(1, ((IRare) v).getRarity()) : 1; + } + + /** + * Expands a candidate list into a rarity-weighted list: each entry appears totalRarity/rarity + * times, so picking uniformly from the result applies rarity exactly once. Both the Bukkit and + * modded entity spawners select through this expansion; rarity must never be applied a second + * time per spawn position. + */ + static KList expandWeighted(List possibilities) { + KList rarityTypes = new KList<>(); + int totalRarity = 0; + for (T i : possibilities) { + totalRarity += get(i); + } + for (T i : possibilities) { + rarityTypes.addMultiple(i, totalRarity / get(i)); + } + return rarityTypes; + } + + int getRarity(); +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/InferredType.java b/core/src/main/java/art/arcane/iris/engine/object/InferredType.java similarity index 91% rename from core/src/main/java/com/volmit/iris/engine/object/InferredType.java rename to core/src/main/java/art/arcane/iris/engine/object/InferredType.java index 37439bb4d..8f6f61ceb 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/InferredType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/InferredType.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("Represents a biome type") public enum InferredType { diff --git a/core/src/main/java/com/volmit/iris/engine/object/InventorySlotType.java b/core/src/main/java/art/arcane/iris/engine/object/InventorySlotType.java similarity index 93% rename from core/src/main/java/com/volmit/iris/engine/object/InventorySlotType.java rename to core/src/main/java/art/arcane/iris/engine/object/InventorySlotType.java index 2110fdba3..0014d1ade 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/InventorySlotType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/InventorySlotType.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("An inventory slot type is used to represent a type of slot for items to fit into in any given inventory.") public enum InventorySlotType { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisAttributeModifier.java b/core/src/main/java/art/arcane/iris/engine/object/IrisAttributeModifier.java new file mode 100644 index 000000000..9d9baa007 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisAttributeModifier.java @@ -0,0 +1,154 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.attribute.Attributable; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.attribute.AttributeModifier.Operation; +import org.bukkit.inventory.EquipmentSlotGroup; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.Locale; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Snippet("attribute-modifier") +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Desc("Represents an attribute modifier for an item or an entity. This allows you to create modifications to basic game attributes such as MAX_HEALTH or ARMOR_VALUE.") +@Data +public class IrisAttributeModifier { + private static final Set WARNED_ATTRIBUTES = ConcurrentHashMap.newKeySet(); + + @Required + @Desc("The Attribute type key such as max_health or minecraft:armor. This type is pulled from the game attributes. Zombie & Horse attributes will not work on non-zombie/horse entities.\nUsing an attribute on an item will have affects when held, or worn. There is no way to specify further granularity as the game picks this depending on the item type.") + private String attribute = null; + + @MinNumber(2) + @Required + @Desc("The Attribute Name is used internally only for the game. This value should be unique to all other attributes applied to this item/entity. It is not shown in game.") + private String name = ""; + + @Desc("The application operation: ADD_NUMBER (default), ADD_SCALAR or MULTIPLY_SCALAR_1. Add Number adds to the default value. \nAdd scalar_1 will multiply by 1 for example if the health is 20 and you multiply_scalar_1 by 0.5, the health will result in 30, not 10. Use negative values to achieve that.") + private String operation = "ADD_NUMBER"; + + @Desc("Minimum amount for this modifier. Iris randomly chooses an amount, this is the minimum it can choose randomly for this attribute.") + private double minAmount = 1; + + @Desc("Maximum amount for this modifier Iris randomly chooses an amount, this is the maximum it can choose randomly for this attribute.") + private double maxAmount = 1; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance that this attribute is applied (0 to 1). If the chance is 0.5 (50%), then Iris will only apply this attribute 50% of the time.") + private double chance = 1; + + public void apply(RNG rng, ItemMeta meta) { + if (rng.nextDouble() < getChance()) { + Attribute resolved = resolveAttribute(); + if (resolved == null) { + return; + } + meta.addAttributeModifier(resolved, createModifier(rng)); + } + } + + public void apply(RNG rng, Attributable meta) { + if (rng.nextDouble() < getChance()) { + Attribute resolved = resolveAttribute(); + if (resolved == null) { + return; + } + meta.getAttribute(resolved).addModifier(createModifier(rng)); + } + } + + public double getAmount(RNG rng) { + return rng.d(getMinAmount(), getMaxAmount()); + } + + private Attribute resolveAttribute() { + if (attribute == null || attribute.isBlank()) { + return null; + } + String value = attribute.trim().toLowerCase(Locale.ROOT); + Attribute resolved; + if (value.contains(":")) { + NamespacedKey key = NamespacedKey.fromString(value); + resolved = key == null ? null : Registry.ATTRIBUTE.get(key); + } else { + resolved = Registry.ATTRIBUTE.get(NamespacedKey.minecraft(value)); + if (resolved == null && value.startsWith("generic_")) { + resolved = Registry.ATTRIBUTE.get(NamespacedKey.minecraft(value.substring("generic_".length()))); + } + if (resolved == null && value.startsWith("generic.")) { + resolved = Registry.ATTRIBUTE.get(NamespacedKey.minecraft(value.substring("generic.".length()))); + } + } + if (resolved == null && WARNED_ATTRIBUTES.add(value)) { + IrisLogging.warn("Unknown attribute '" + attribute + "' in attribute-modifier '" + name + "'"); + } + return resolved; + } + + private Operation resolveOperation() { + if (operation == null || operation.isBlank()) { + return Operation.ADD_NUMBER; + } + try { + return Operation.valueOf(operation.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return Operation.ADD_NUMBER; + } + } + + private AttributeModifier createModifier(RNG rng) { + NamespacedKey key = NamespacedKey.minecraft(generateModifierKey()); + return new AttributeModifier(key, getAmount(rng), resolveOperation(), EquipmentSlotGroup.ANY); + } + + private String generateModifierKey() { + String source = getName() == null ? "modifier" : getName(); + String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_"); + if (normalized.isBlank()) { + normalized = "modifier"; + } + if (normalized.length() > 32) { + normalized = normalized.substring(0, 32); + } + String random = UUID.randomUUID().toString().replace("-", ""); + return normalized + "_" + random; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisAxisRotationClamp.java b/core/src/main/java/art/arcane/iris/engine/object/IrisAxisRotationClamp.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/IrisAxisRotationClamp.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisAxisRotationClamp.java index 44b1bf421..1fbbd8c82 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisAxisRotationClamp.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisAxisRotationClamp.java @@ -16,10 +16,15 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.M; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.math.M; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -78,11 +83,8 @@ public class IrisAxisRotationClamp { } if (isUnlimited()) { - if (interval < 1) { - interval = 1; - } - - return Math.toRadians((interval * (Math.ceil(Math.abs((rng % 360D) / interval)))) % 360D); + double resolvedInterval = interval < 1 ? 1 : interval; + return Math.toRadians((resolvedInterval * (Math.ceil(Math.abs((rng % 360D) / resolvedInterval)))) % 360D); } if (min == max && min != 0) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java new file mode 100644 index 000000000..ec7001c39 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -0,0 +1,649 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.data.cache.LazyBoundedCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBiome; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +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.data.DataProvider; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.project.context.IrisContext; +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import art.arcane.iris.spi.PlatformBlockState; +import org.bukkit.block.Biome; + +import java.awt.Color; +import java.util.EnumMap; +import java.util.Objects; + +/** + * Represents a biome in a pack. This type is Gson deserialized straight out of user pack JSON, so + * the field block and the transient {@link AtomicCache} block below are load bearing and must not + * move. Behavior lives in the same-package companions: + * {@link IrisBiomeLayerGenerator}, {@link IrisBiomeDerivatives}, {@link IrisBiomeOres}, + * {@link IrisBiomeColorRenderer} and {@link IrisBiomeGenLinks}. + */ +@Accessors(chain = true) +@NoArgsConstructor +@Desc("Represents a biome in iris. Biomes are placed inside of regions and hold objects.\nA biome consists of layers (block palletes), decorations, objects & generators.") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisBiome extends IrisRegistrant implements IRare { + private static final int BIOME_GENERATOR_CACHE_SIZE = 8; + + private final transient AtomicCache> genCache = new AtomicCache<>(); + private final transient AtomicCache> genCacheMax = new AtomicCache<>(); + private final transient AtomicCache> genCacheMin = new AtomicCache<>(); + private final transient AtomicCache> surfaceObjectsCache = new AtomicCache<>(); + private final transient AtomicCache> carveObjectsCache = new AtomicCache<>(); + private final transient AtomicCache cacheColor = new AtomicCache<>(); + private final transient AtomicCache cacheColorObjectDensity = new AtomicCache<>(); + private final transient AtomicCache cacheColorDecoratorLoad = new AtomicCache<>(); + private final transient AtomicCache cacheColorLayerLoad = new AtomicCache<>(); + private final transient AtomicCache cacheColorDepositLoad = new AtomicCache<>(); + private final transient AtomicCache childrenCell = new AtomicCache<>(); + @Getter(AccessLevel.NONE) + private final transient LazyBoundedCache biomeGenerators = + new LazyBoundedCache<>(BIOME_GENERATOR_CACHE_SIZE); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private transient volatile SeededBiomeGenerator recentBiomeGenerator; + private final transient AtomicCache maxHeight = new AtomicCache<>(); + private final transient AtomicCache maxWithObjectHeight = new AtomicCache<>(); + private final transient AtomicCache realCarveBiome = new AtomicCache<>(); + private final transient AtomicCache> realChildren = new AtomicCache<>(); + private final transient AtomicCache> layerHeightGenerators = new AtomicCache<>(); + private final transient AtomicCache> layerSeaHeightGenerators = new AtomicCache<>(); + private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); + private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); + private final transient AtomicCache surfaceOreBoundsCache = new AtomicCache<>(); + private final transient AtomicCache undergroundOreBoundsCache = new AtomicCache<>(); + private final transient AtomicCache> decoratorBuckets = new AtomicCache<>(); + private final transient AtomicCache derivativeResolved = new AtomicCache<>(); + private final transient AtomicCache vanillaDerivativeResolved = new AtomicCache<>(); + private final transient AtomicCache> biomeScatterResolved = new AtomicCache<>(); + private final transient AtomicCache> biomeSkyScatterResolved = new AtomicCache<>(); + private static final IrisDecorator[] EMPTY_BUCKET = new IrisDecorator[0]; + @MinNumber(2) + @Required + @Desc("This is the human readable name for this biome. This can and should be different than the file name. This is not used for loading biomes in other objects.") + private String name = "Subterranean Land"; + @ArrayType(min = 1, type = IrisBiomeCustom.class) + @Desc("If the biome type custom is defined, specify this") + private KList customDerivitives; + @Desc("Spawn Entities in this area over time. Iris will continually replenish these mobs just like vanilla does.") + @ArrayType(min = 1, type = String.class) + @RegistryListResource(IrisSpawner.class) + private KList entitySpawners = new KList<>(); + @ArrayType(min = 1, type = IrisEffect.class) + @Desc("Effects are ambient effects such as potion effects, random sounds, or even particles around each player. All of these effects are played via packets so two players won't see/hear each others effects.\nDue to performance reasons, effects will play around the player even if where the effect was played is no longer in the biome the player is in.") + private KList effects = new KList<>(); + @DependsOn({"biomeStyle", "biomeZoom", "biomeScatter"}) + @Desc("This changes the dispersion of the biome colors if multiple derivatives are chosen.") + private IrisGeneratorStyle biomeStyle = NoiseStyle.SIMPLEX.style(); + @ArrayType(min = 1, type = IrisBlockDrops.class) + @Desc("Define custom block drops for this biome") + private KList blockDrops = new KList<>(); + @Desc("Reference loot tables in this area") + private IrisLootReference loot = new IrisLootReference(); + @Desc("Layers no longer descend from the surface block, they descend from the max possible height the biome can produce (constant) creating mesa like layers.") + private boolean lockLayers = false; + @Desc("The max layers to iterate below the surface for locked layer biomes (mesa).") + private int lockLayersMax = 7; + @Desc("Profile-driven 3D cave configuration") + private IrisCaveProfile caveProfile = new IrisCaveProfile(); + @MinNumber(1) + @MaxNumber(512) + @Desc("The rarity of this biome (integer)") + private int rarity = 1; + @Desc("A color for visualizing this biome with a color. I.e. #F13AF5. This will show up on the map.") + private String color = null; + @Required + @RegistryListBiome + @Desc("The raw derivative of this biome. This is required or the terrain will not properly generate. Use any vanilla biome type. Look in examples/biome-list.txt") + private String derivative = "minecraft:the_void"; + @Required + @RegistryListBiome + @Desc("Override the derivative used for vanilla structure selection. Iris still enforces the generated terrain role: land-only Minecraft derivatives on sea biomes expose no native structure biome, and land-only derivatives on shore biomes resolve as beach, while exact ocean, river, beach, and shore variants remain eligible. Non-Minecraft namespaces remain authoritative. Not defining this value selects derivative.") + private String vanillaDerivative = null; + @ArrayType(min = 1, type = String.class) + @RegistryListBiome + @Desc("You can instead specify multiple biome derivatives to randomly scatter colors in this biome") + private KList biomeScatter = new KList<>(); + @ArrayType(min = 1, type = String.class) + @RegistryListBiome + @Desc("Since 1.13 supports 3D biomes, you can add different derivative colors for anything above the terrain. (Think swampy tree leaves with a desert looking grass surface)") + private KList biomeSkyScatter = new KList<>(); + @DependsOn({"children"}) + @Desc("If this biome has children biomes, and the gen layer chooses one of this biomes children, how much smaller will it be (inside of this biome). Higher values means a smaller biome relative to this biome's size. Set higher than 1.0 and below 3.0 for best results.") + private double childShrinkFactor = 1.5; + @DependsOn({"children"}) + @Desc("If this biome has children biomes, and the gen layer chooses one of this biomes children, How will it be shaped?") + private IrisGeneratorStyle childStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + @RegistryListResource(IrisBiome.class) + @ArrayType(min = 1, type = String.class) + @Desc("List any biome names (file names without.json) here as children. Portions of this biome can sometimes morph into their children. Iris supports cyclic relationships such as A > B > A > B. Iris will stop checking 4 biomes down the tree.") + private KList children = new KList<>(); + @RegistryListResource(IrisBiome.class) + @Desc("Registers the referenced biome with this pack's reachable biome set. It is NOT applied as a substitute under carvings; cave biomes come from the region's caveBiomes list.") + private String carvingBiome = ""; + @MinNumber(0) + @MaxNumber(256) + @Desc("Minimum depth below terrain surface required before this cave biome can be selected.") + private int caveMinDepthBelowSurface = 0; + @Desc("The default slab if iris decides to place a slab in this biome. Default is no slab.") + private IrisBiomePaletteLayer slab = new IrisBiomePaletteLayer().zero(); + @Desc("The default wall if iris decides to place a wall higher than 2 blocks (steep hills or possibly cliffs)") + private IrisBiomePaletteLayer wall = new IrisBiomePaletteLayer().zero(); + @Required + @ArrayType(type = IrisBiomePaletteLayer.class) + @Desc("This defines the layers of materials in this biome. Each layer has a palette and min/max height and some other properties. Usually a grassy/sandy layer then a dirt layer then a stone layer. Iris will fill in the remaining blocks below your layers with stone.") + private KList layers = new KList().qadd(new IrisBiomePaletteLayer()); + @ArrayType(type = IrisBiomePaletteLayer.class) + @Desc("Layers of materials placed on cave ceilings in this biome, indexed upward from the ceiling surface. Must not have more entries than layers, whose height generators it reuses. Omitting this leaves cave ceilings unchanged.") + private KList caveCeilingLayers = new KList<>(); + @ArrayType(type = IrisBiomePaletteLayer.class) + @Desc("Layers of materials filling the water column of sea biomes, indexed downward from the water surface. Anything below the last layer is filled with the dimension fluid palette, not stone.") + private KList seaLayers = new KList<>(); + @ArrayType(min = 1, type = IrisDecorator.class) + @Desc("Decorators are used for things like tall grass, bisected flowers, and even kelp or cactus (random heights)") + private KList decorators = new KList<>(); + @ArrayType(min = 1, type = IrisObjectPlacement.class) + @Desc("Objects define what schematics (iob files) iris will place in this biome") + private KList objects = new KList<>(); + @Desc("Procedural objects (such as trees) iris generates from scratch and places in this biome, instead of loading them from iob files") + private IrisProceduralObjects proceduralObjects = new IrisProceduralObjects(); + @ArrayType(min = 1, type = IrisStructurePlacement.class) + @Desc("Structures define jigsaw or vanilla/datapack structures iris will place in this biome") + private KList structures = new KList<>(); + @ArrayType(min = 1, type = IrisFloatingChildBiomes.class) + @Desc("Floating child biomes that procedurally generate above this biome's terrain. Each entry references a target biome whose layers, decorators, and objects drive the floating island's visual design, while the config here drives size, shape, altitude, rarity, and water level. Multiple entries are supported and selected by rarity per column unless mergeFloatingChildBiomes is enabled.") + private KList floatingChildBiomes = new KList<>(); + @Desc("When true, every floating child entry is sampled independently and their solid masks are unioned, so multiple floating islands can stack, overlap, and collide instead of the picker choosing only one child per column.") + private boolean mergeFloatingChildBiomes = false; + @ArrayType(min = 1, type = IrisBiomeGeneratorLink.class) + @Desc("Generators for this biome. Multiple generators with different interpolation sizes will mix with other biomes how you would expect. This defines your biome height relative to the fluid height. Use negative for oceans.") + private KList generators = new KList().qadd(new IrisBiomeGeneratorLink()); + @ArrayType(min = 1, type = IrisDepositGenerator.class) + @Desc("Define biome deposit generators that add onto the existing regional and global deposit generators") + private KList deposits = new KList<>(); + @MinNumber(0) + @MaxNumber(1) + @Desc("Multiplier applied to the frequency of every ore deposit whose center is inside this biome. A value of 0.4 keeps 40% of ore veins while leaving non-ore deposits unchanged.") + private double oreDepositFrequencyMultiplier = 1D; + @MinNumber(0.01) + @MaxNumber(16) + @Desc("Multiplier applied to the block count of every ore deposit whose center is inside this biome. Non-ore deposits are unchanged.") + private double oreDepositSizeMultiplier = 1D; + @ArrayType(min = 1, type = IrisDepositVariant.class) + @Desc("Deposit ore remap rules scoped to this biome. Each entry declares a vertical band and a source->replacement block id map. Applied before regional and dimension rules; first matching biome rule wins.") + private KList depositVariants = new KList<>(); + private transient volatile InferredType inferredType; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final transient EnumMap inferredVariants = new EnumMap<>(InferredType.class); + @Desc("Collection of ores to be generated") + @ArrayType(type = IrisOreGenerator.class, min = 1) + private KList ores = new KList<>(); + + public PlatformBlockState generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) { + return IrisBiomeOres.generateOres(this, x, y, z, rng, data, surface); + } + + public PlatformBlockState generateSurfaceOres(int x, int y, int z, RNG rng, IrisData data) { + return IrisBiomeOres.generateSurfaceOres(this, x, y, z, rng, data); + } + + public PlatformBlockState generateUndergroundOres(int x, int y, int z, RNG rng, IrisData data) { + return IrisBiomeOres.generateUndergroundOres(this, x, y, z, rng, data); + } + + public boolean hasSurfaceOres() { + return IrisBiomeOres.hasSurfaceOres(this); + } + + public boolean hasUndergroundOres() { + return IrisBiomeOres.hasUndergroundOres(this); + } + + public synchronized IrisBiome setInferredType(InferredType inferredType) { + this.inferredType = inferredType; + return this; + } + + public IrisBiome withInferredType(InferredType type) { + Objects.requireNonNull(type, "type"); + // Lock-free fast path on the volatile field: this runs per column per implode level + // against biome instances shared by every burst thread. The variant map below stays + // monitor-guarded (plain EnumMap, not safe for concurrent reads during writes). + if (inferredType == type) { + return this; + } + return withInferredTypeSlow(type); + } + + private synchronized IrisBiome withInferredTypeSlow(InferredType type) { + if (inferredType == null) { + inferredType = type; + return this; + } + if (inferredType == type) { + return this; + } + IrisBiome cached = inferredVariants.get(type); + if (cached != null) { + return cached; + } + IrisData data = getLoader(); + if (data == null) { + throw new IllegalStateException("Cannot create an inferred biome variant without an Iris data loader."); + } + IrisBiome variant = data.getGson().fromJson(data.getGson().toJson(this), IrisBiome.class); + variant.setLoader(data); + variant.setLoadKey(getLoadKey()); + variant.setLoadFile(getLoadFile()); + variant.inferredType = type; + inferredVariants.put(type, variant); + return variant; + } + + /** + * Hand written override of the Lombok setter. It must stay here because it invalidates the ore + * caches that {@link IrisBiomeOres} reads. + */ + public void setOres(KList ores) { + this.ores = ores == null ? new KList<>() : ores; + surfaceOreCache.reset(); + undergroundOreCache.reset(); + surfaceOreBoundsCache.reset(); + undergroundOreBoundsCache.reset(); + } + + public KList getSurfaceOreGenerators() { + return IrisBiomeOres.getSurfaceOreGenerators(this); + } + + public KList getUndergroundOreGenerators() { + return IrisBiomeOres.getUndergroundOreGenerators(this); + } + + public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() { + return IrisBiomeOres.getSurfaceOreGeneratorBounds(this); + } + + public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() { + return IrisBiomeOres.getUndergroundOreGeneratorBounds(this); + } + + public Biome getDerivative() { + Biome cached = derivativeResolved.getIfPresent(); + + if (cached != null) { + return cached; + } + + return derivativeResolved.aquire(() -> IrisBiomeDerivatives.resolveBiomeKey(derivative)); + } + + public Biome getVanillaDerivative() { + Biome resolved = vanillaDerivative == null + ? null + : vanillaDerivativeResolved.aquire(() -> IrisBiomeDerivatives.resolveBiomeKey(vanillaDerivative)); + return resolved == null ? getDerivative() : resolved; + } + + public String getVanillaDerivativeKey() { + return IrisBiomeDerivatives.getVanillaDerivativeKey(derivative, vanillaDerivative); + } + + public String getStructureDerivativeKey() { + return IrisBiomeDerivatives.getStructureDerivativeKey(this); + } + + public String getDerivativeKey() { + return IrisBiomeDerivatives.namespacedBiomeKey(derivative); + } + + KList getBiomeScatterResolved() { + KList cached = biomeScatterResolved.getIfPresent(); + + if (cached != null) { + return cached; + } + + return biomeScatterResolved.aquire(() -> IrisBiomeDerivatives.resolveBiomeKeys(biomeScatter)); + } + + KList getBiomeSkyScatterResolved() { + KList cached = biomeSkyScatterResolved.getIfPresent(); + + if (cached != null) { + return cached; + } + + return biomeSkyScatterResolved.aquire(() -> IrisBiomeDerivatives.resolveBiomeKeys(biomeSkyScatter)); + } + + public boolean isCustom() { + return getCustomDerivitives() != null && getCustomDerivitives().isNotEmpty(); + } + + public double getGenLinkMax(String loadKey, Engine engine) { + return IrisBiomeGenLinks.getGenLinkMax(this, loadKey, engine); + } + + public double getGenLinkMin(String loadKey, Engine engine) { + return IrisBiomeGenLinks.getGenLinkMin(this, loadKey, engine); + } + + public IrisBiomeGeneratorLink getGenLink(String loadKey) { + return IrisBiomeGenLinks.getGenLink(this, loadKey); + } + + public IrisBiome getRealCarvingBiome(IrisData data) { + return realCarveBiome.aquire(() -> + { + IrisBiome biome = data.getBiomeLoader().load(getCarvingBiome()); + + if (biome == null) { + biome = this; + } + + return biome; + }); + } + + public KList getSurfaceObjects() { + return getSurfaceObjectsCache().aquire(() -> + { + KList o = getObjects().copy(); + + for (IrisObjectPlacement i : o.copy()) { + if (!i.getCarvingSupport().supportsSurface()) { + o.remove(i); + } + } + + return o; + }); + } + + public KList getCarvingObjects() { + return getCarveObjectsCache().aquire(() -> + { + KList o = getObjects().copy(); + + for (IrisObjectPlacement i : o.copy()) { + if (!i.getCarvingSupport().supportsCarving()) { + o.remove(i); + } + } + + return o; + }); + } + + public double getHeight(Engine xg, double x, double z, long seed) { + double height = 0; + + for (IrisBiomeGeneratorLink i : generators) { + height += i.getHeight(xg, x, z, seed); + } + + return Math.max(0, Math.min(height, xg.getHeight())); + } + + public CNG getBiomeGenerator(RNG random) { + IrisData loader = getLoader(); + Engine engine = resolveBiomeGeneratorEngine(loader); + return getBiomeGenerator(random, engine); + } + + public CNG getBiomeGenerator(RNG random, Engine engine) { + IrisData loader = getLoader(); + long seed = engine == null ? random.getSeed() : engine.getSeedManager().getBiome(); + SeededBiomeGenerator recent = recentBiomeGenerator; + if (recent != null && recent.seed == seed) { + return recent.generator; + } + + CNG generator = biomeGenerators.computeIfAbsent(seed, ignored -> createBiomeGenerator(seed, loader)); + recentBiomeGenerator = new SeededBiomeGenerator(seed, generator); + return generator; + } + + private Engine resolveBiomeGeneratorEngine(IrisData loader) { + IrisContext context = IrisContext.get(); + if (context != null) { + Engine contextEngine = context.getEngine(); + if (!contextEngine.isClosed() && (loader == null || contextEngine.getData() == loader)) { + return contextEngine; + } + } + + return loader == null ? null : loader.getEngine(); + } + + private CNG createBiomeGenerator(long seed, IrisData loader) { + int signature = 213949 + 228888 + getRarity() + getName().length(); + return biomeStyle.createNoCache(new RNG(seed).nextParallelRNG(signature), loader); + } + + public CNG getChildrenGenerator(RNG random, int sig, double scale) { + return childrenCell.aquire(() -> getChildStyle().create(random.nextParallelRNG(sig * 2137), getLoader()).bake().scale(scale).bake()); + } + + public KList generateLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { + return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex); + } + + public KList generateCeilingLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { + return IrisBiomeLayerGenerator.generateCeilingLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex); + } + + public KList generateLockedLayers(double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex) { + return IrisBiomeLayerGenerator.generateLockedLayers(this, wx, wz, random, maxDepthf, height, rdata, complex); + } + + public KList generateSeaLayers(double wx, double wz, RNG random, int maxDepth, IrisData rdata) { + return IrisBiomeLayerGenerator.generateSeaLayers(this, wx, wz, random, maxDepth, rdata); + } + + /** + * Note the arity split: {@code getMaxHeight()} is the Lombok getter for the height cache field, + * {@code getMaxHeight(Engine)} is the resolved biome height. + */ + public int getMaxHeight(Engine engine) { + return IrisBiomeLayerGenerator.getMaxHeight(this, engine); + } + + public int getMaxWithObjectHeight(IrisData data, Engine engine) { + return IrisBiomeLayerGenerator.getMaxWithObjectHeight(this, data, engine); + } + + public KList getLayerHeightGenerators(RNG rng, IrisData rdata) { + return IrisBiomeLayerGenerator.getLayerHeightGenerators(this, rng, rdata); + } + + public KList getLayerSeaHeightGenerators(RNG rng, IrisData data) { + return IrisBiomeLayerGenerator.getLayerSeaHeightGenerators(this, rng, data); + } + + public PlatformBlockState getSurfaceBlock(int x, int z, RNG rng, IrisData idm) { + return IrisBiomeLayerGenerator.getSurfaceBlock(this, x, z, rng, idm); + } + + public boolean isLand() { + if (inferredType == null) { + return true; + } + + return inferredType.equals(InferredType.LAND); + } + + public boolean isSea() { + if (inferredType == null) { + return false; + } + return inferredType.equals(InferredType.SEA); + } + + public boolean isAquatic() { + return isSea(); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isShore() { + if (inferredType == null) { + return false; + } + return inferredType.equals(InferredType.SHORE); + } + + public Biome getSkyBiome(RNG rng, double x, double y, double z) { + return IrisBiomeDerivatives.getSkyBiome(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + } + + public Biome getSkyBiome(RNG rng, Engine engine, double x, double y, double z) { + return IrisBiomeDerivatives.getSkyBiome(this, rng, engine, x, y, z); + } + + public IrisBiomeCustom getCustomBiome(RNG rng, double x, double y, double z) { + return IrisBiomeDerivatives.getCustomBiome(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + } + + public IrisBiomeCustom getCustomBiome(RNG rng, Engine engine, double x, double y, double z) { + return IrisBiomeDerivatives.getCustomBiome(this, rng, engine, x, y, z); + } + + public KList getRealChildren(DataProvider g) { + return realChildren.aquire(() -> + { + KList realChildren = new KList<>(); + + for (String i : getChildren()) { + realChildren.add(g.getData().getBiomeLoader().load(i)); + } + + return realChildren; + }); + } + + public KList getAllChildren(DataProvider g, int limit) { + KSet m = new KSet<>(); + m.addAll(getChildren()); + limit--; + + if (limit > 0) { + for (String i : getChildren()) { + IrisBiome b = g.getData().getBiomeLoader().load(i); + m.addAll(b.getAllChildren(g, limit)); + } + } + + return new KList<>(m); + } + + //TODO: Test + public Biome getGroundBiome(RNG rng, double x, double y, double z) { + return IrisBiomeDerivatives.getGroundBiome(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + } + + public Biome getGroundBiome(RNG rng, Engine engine, double x, double y, double z) { + return IrisBiomeDerivatives.getGroundBiome(this, rng, engine, x, y, z); + } + + public String getSkyBiomeKey(RNG rng, double x, double y, double z) { + return IrisBiomeDerivatives.getSkyBiomeKey(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + } + + public String getSkyBiomeKey(RNG rng, Engine engine, double x, double y, double z) { + return IrisBiomeDerivatives.getSkyBiomeKey(this, rng, engine, x, y, z); + } + + public String getGroundBiomeKey(RNG rng, double x, double y, double z) { + return IrisBiomeDerivatives.getGroundBiomeKey(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + } + + public String getGroundBiomeKey(RNG rng, Engine engine, double x, double y, double z) { + return IrisBiomeDerivatives.getGroundBiomeKey(this, rng, engine, x, y, z); + } + + public Color getColor(Engine engine, RenderType type) { + return IrisBiomeColorRenderer.getColor(this, engine, type); + } + + @Override + public String getFolderName() { + return "biomes"; + } + + public IrisDecorator[] getDecoratorBucket(IrisDecorationPart part) { + return decoratorBuckets.aquire(this::buildDecoratorBuckets).getOrDefault(part, EMPTY_BUCKET); + } + + private EnumMap buildDecoratorBuckets() { + EnumMap> staging = new EnumMap<>(IrisDecorationPart.class); + for (IrisDecorator d : decorators) { + staging.computeIfAbsent(d.getPartOf(), k -> new KList<>()).add(d); + } + EnumMap result = new EnumMap<>(IrisDecorationPart.class); + for (IrisDecorationPart part : IrisDecorationPart.values()) { + KList list = staging.get(part); + result.put(part, list == null ? EMPTY_BUCKET : list.toArray(EMPTY_BUCKET)); + } + return result; + } + + public String getTypeName() { + return "Biome"; + } + + private static final class SeededBiomeGenerator { + private final long seed; + private final CNG generator; + + private SeededBiomeGenerator(long seed, CNG generator) { + this.seed = seed; + this.generator = generator; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java new file mode 100644 index 000000000..6ba1918dd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java @@ -0,0 +1,93 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.data.VanillaBiomeColors; +import art.arcane.volmlib.util.inventorygui.RandomColor; + +import java.awt.Color; + +/** + * Map / render color resolution for {@link IrisBiome}. IrisBiome is Gson deserialized from pack + * JSON, so its fields stay put and only the behavior lives here. + */ +final class IrisBiomeColorRenderer { + private IrisBiomeColorRenderer() { + } + + static Color getColor(IrisBiome biome, Engine engine, RenderType type) { + switch (type) { + case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> { + return biome.getCacheColor().aquire(() -> { + if (biome.getColor() == null) { + RandomColor randomColor = new RandomColor(biome.getName().hashCode()); + String vanillaKey = biome.getVanillaDerivativeKey(); + RandomColor.Color col = vanillaKey == null ? null : VanillaBiomeColors.getColorType(vanillaKey); + if (col == null) { + IrisLogging.warn("No vanilla biome found for " + biome.getName()); + return new Color(randomColor.randomColor()); + } + RandomColor.Luminosity lum = VanillaBiomeColors.getColorLuminosity(vanillaKey); + RandomColor.SaturationType sat = VanillaBiomeColors.getColorSaturation(vanillaKey); + int newColorI = randomColor.randomColor(col, col == RandomColor.Color.MONOCHROME ? RandomColor.SaturationType.MONOCHROME : sat, lum); + + return new Color(newColorI); + } + + try { + return Color.decode(biome.getColor()); + } catch (NumberFormatException e) { + IrisLogging.warn("Could not parse color \"" + biome.getColor() + "\" for biome " + biome.getName()); + return new Color(new RandomColor(biome.getName().hashCode()).randomColor()); + } + }); + } + case OBJECT_LOAD -> { + return biome.getCacheColorObjectDensity().aquire(() -> { + double density = 0; + + for (IrisObjectPlacement i : biome.getObjects()) { + density += i.getDensity() * i.getChance(); + } + + return Color.getHSBColor(0.225f, (float) (density / engine.getMaxBiomeObjectDensity()), 1f); + }); + } + case DECORATOR_LOAD -> { + return biome.getCacheColorDecoratorLoad().aquire(() -> { + double density = 0; + + for (IrisDecorator i : biome.getDecorators()) { + density += i.getChance() * Math.min(1, i.getStackMax()) * 256; + } + + return Color.getHSBColor(0.41f, (float) (density / engine.getMaxBiomeDecoratorDensity()), 1f); + }); + } + case LAYER_LOAD -> { + return biome.getCacheColorLayerLoad().aquire(() -> Color.getHSBColor(0.625f, (float) (biome.getLayers().size() / engine.getMaxBiomeLayerDensity()), 1f)); + } + } + + return Color.black; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java new file mode 100644 index 000000000..2e61d0b3f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java @@ -0,0 +1,234 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.nms.datapack.IDataFixer; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.awt.Color; +import java.util.Locale; + +@Snippet("custom-biome") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A custom biome, generated through a datapack") +@Data +public class IrisBiomeCustom { + @Required + @Desc("The resource key of this biome. Just a simple id such as 'plains' or something.") + private String id = ""; + + @MinNumber(-3) + @MaxNumber(3) + @Desc("The biome's temperature") + private double temperature = 0.8; + + @MinNumber(-3) + @MaxNumber(3) + @Desc("The biome's downfall amount (snow / rain), see preci") + private double humidity = 0.4; + + @DependsOn("spawnRarity") + @ArrayType(min = 1, type = IrisBiomeCustomSpawn.class) + @Desc("The biome's mob spawns") + private KList spawns = new KList<>(); + + @ArrayType(min = 1, type = String.class) + @Desc("Biome tags to opt this custom biome into, such as minecraft:allows_surface_slime_spawns") + private KList tags = new KList<>(); + + @Desc("The biome's downfall type") + private IrisBiomeCustomPrecipType downfallType = IrisBiomeCustomPrecipType.rain; + + @Desc("Define an ambient particle to be rendered clientside (no server cost!)") + private IrisBiomeCustomParticle ambientParticle = null; + + @Required + @Desc("The biome's category type") + private IrisBiomeCustomCategory category = IrisBiomeCustomCategory.plains; + + @MinNumber(0) + @MaxNumber(20) + @Desc("The spawn rarity of any defined spawners") + private int spawnRarity = 0; + + @Desc("The color of the sky, top half of sky. (hex format)") + private String skyColor = "#79a8e1"; + + @Desc("The color of the fog, bottom half of sky. (hex format)") + private String fogColor = "#c0d8e1"; + + @Desc("The color of the water (hex format). Leave blank / don't define to not change") + private String waterColor = "#3f76e4"; + + @Desc("The color of water fog (hex format). Leave blank / don't define to not change") + private String waterFogColor = "#050533"; + + @Desc("The color of the grass (hex format). Leave blank / don't define to not change") + private String grassColor = ""; + + @Desc("The color of foliage (hex format). Leave blank / don't define to not change") + private String foliageColor = ""; + + /** + * The tags this custom biome is installed into: the pack author's own {@code tags} plus the direct tag + * membership of the Iris biome's vanilla derivative. Inheriting the derivative's tags is what makes + * {@code #minecraft:is_overworld} and every mod-authored tag selector resolve against Iris custom biomes; + * without it a custom biome sits in no tag at all and mod content that gates on tags silently never runs. + * + *

Author tags win order but duplicates collapse: the tag files are written through a sorted set. + * Structure tags ({@code has_structure/*}) are deliberately not inherited - native structure placement + * resolves through the biome's structure derivative, so injecting them would place a structure twice. + * + * @param vanillaDerivativeKey the owning Iris biome's vanilla derivative key, may be null + */ + public KList getEffectiveTags(String vanillaDerivativeKey) { + KList resolved = new KList<>(); + KList authored = getTags(); + + if (authored != null) { + for (String tag : authored) { + resolved.addIfMissing(tag); + } + } + + for (String inherited : IrisVanillaBiomeTags.tagsFor(vanillaDerivativeKey)) { + resolved.addIfMissing(inherited); + } + + return resolved; + } + + public String generateJson(IDataFixer fixer) { + JSONObject effects = new JSONObject(); + effects.put("sky_color", parseColor(getSkyColor())); + effects.put("fog_color", parseColor(getFogColor())); + effects.put("water_color", parseColor(getWaterColor())); + effects.put("water_fog_color", parseColor(getWaterFogColor())); + + if (ambientParticle != null) { + // Never touch IrisBiomeCustomParticle.getParticle() here: it returns a Bukkit Particle + // and this method also runs during modded datapack staging. + String particleKey = ambientParticle.getParticleKey(); + if (particleKey == null) { + IrisLogging.warn("Custom biome " + getId() + " declares an ambientParticle with no particle key, skipping it"); + } else { + JSONObject particle = new JSONObject(); + JSONObject po = new JSONObject(); + po.put("type", particleKey); + particle.put("options", po); + particle.put("probability", 1f / ambientParticle.getRarity()); + effects.put("particle", particle); + } + } + + if (!getGrassColor().isEmpty()) { + effects.put("grass_color", parseColor(getGrassColor())); + } + + if (!getFoliageColor().isEmpty()) { + effects.put("foliage_color", parseColor(getFoliageColor())); + } + + JSONObject j = new JSONObject(); + j.put("surface_builder", "minecraft:grass"); + j.put("depth", 0.125); + j.put("scale", 0.05); + j.put("temperature", getTemperature()); + j.put("downfall", getHumidity()); + j.put("creature_spawn_probability", getSpawnRarity()); + j.put("has_precipitation", getDownfallType() != IrisBiomeCustomPrecipType.none); + j.put("precipitation", getDownfallType().toString().toLowerCase()); + j.put("category", getCategory().toString().toLowerCase()); + j.put("effects", effects); + j.put("starts", new JSONArray()); + j.put("spawners", new JSONObject()); + j.put("spawn_costs", new JSONObject()); + j.put("carvers", new JSONObject()); + j.put("features", new JSONArray()); + + if (spawnRarity > 0) { + j.put("creature_spawn_probability", spawnRarity); + } + + if (getSpawns() != null && getSpawns().isNotEmpty()) { + JSONObject spawners = new JSONObject(); + KMap groups = new KMap<>(); + + for (IrisBiomeCustomSpawn i : getSpawns()) { + if (i == null) { + continue; + } + String key = i.getTypeKey(); + if (key == null) { + IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + getId()); + continue; + } + IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup(); + JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray()); + JSONObject o = new JSONObject(); + o.put("type", key); + o.put("weight", i.getWeight()); + o.put("minCount", i.getMinCount()); + o.put("maxCount", i.getMaxCount()); + g.put(o); + } + + for (IrisBiomeCustomSpawnType i : groups.k()) { + spawners.put(i.name().toLowerCase(Locale.ROOT), groups.get(i)); + } + + j.put("spawners", spawners); + } + + return fixer.fixCustomBiome(this, j).toString(4); + } + + private int parseColor(String c) { + String v = (c.startsWith("#") ? c : "#" + c).trim(); + try { + return Color.decode(v).getRGB() & 0x00FFFFFF; + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Error Parsing '''color''', (" + c + ")"); + } + + return 0; + } + + public String getId() { + return id.toLowerCase(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomCategory.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomCategory.java new file mode 100644 index 000000000..52dd71a38 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomCategory.java @@ -0,0 +1,75 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("The custom biome category. Vanilla asks for this, basically what represents your biome closest?") +public enum IrisBiomeCustomCategory { + @Desc("Tags the generated datapack biome as vanilla category 'beach' (shoreline biomes).") + beach, + + @Desc("Tags the generated datapack biome as vanilla category 'desert' (hot, dry sand biomes).") + desert, + + @Desc("Tags the generated datapack biome as vanilla category 'extreme_hills' (mountain biomes).") + extreme_hills, + + @Desc("Tags the generated datapack biome as vanilla category 'forest' (tree-dense temperate biomes).") + forest, + + @Desc("Tags the generated datapack biome as vanilla category 'icy' (snow and ice biomes).") + icy, + + @Desc("Tags the generated datapack biome as vanilla category 'jungle'.") + jungle, + + @Desc("Tags the generated datapack biome as vanilla category 'mesa' (badlands).") + mesa, + + @Desc("Tags the generated datapack biome as vanilla category 'mushroom' (mushroom fields).") + mushroom, + + @Desc("Tags the generated datapack biome as vanilla category 'nether'.") + nether, + + @Desc("Tags the generated datapack biome as vanilla category 'none' (no classification).") + none, + + @Desc("Tags the generated datapack biome as vanilla category 'ocean'.") + ocean, + + @Desc("Tags the generated datapack biome as vanilla category 'plains'. This is the default when category is omitted.") + plains, + + @Desc("Tags the generated datapack biome as vanilla category 'river'.") + river, + + @Desc("Tags the generated datapack biome as vanilla category 'savanna'.") + savanna, + + @Desc("Tags the generated datapack biome as vanilla category 'swamp'.") + swamp, + + @Desc("Tags the generated datapack biome as vanilla category 'taiga'.") + taiga, + + @Desc("Tags the generated datapack biome as vanilla category 'the_end'.") + the_end +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java new file mode 100644 index 000000000..4e8cfb046 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java @@ -0,0 +1,69 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.common.data.registry.RegistryUtil; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.NamespacedKey; +import org.bukkit.Particle; + +@Snippet("custom-biome-particle") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A custom biome ambient particle") +@Data +public class IrisBiomeCustomParticle { + private final transient AtomicCache particleResolved = new AtomicCache<>(); + @Required + @Desc("The biome's particle type") + private String particle = "minecraft:flash"; + + @MinNumber(1) + @MaxNumber(10000) + @Desc("The rarity") + private int rarity = 35; + + /** + * The authored particle key, normalized to a namespaced key. Platform-neutral: datapack + * emission must use this and never {@link #getParticle()}, which only resolves on Bukkit. + */ + public String getParticleKey() { + if (particle == null || particle.isEmpty()) { + return null; + } + return particle.indexOf(':') >= 0 ? particle : "minecraft:" + particle; + } + + public Particle getParticle() { + return particleResolved.aquire(() -> { + NamespacedKey namespacedKey = NamespacedKey.fromString(particle); + return namespacedKey == null ? null : RegistryUtil.lookup(Particle.class).get(namespacedKey); + }); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomPrecipType.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomPrecipType.java similarity index 91% rename from core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomPrecipType.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomPrecipType.java index c6e242bed..f44176a77 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomPrecipType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomPrecipType.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("Snow, rain, or nothing") public enum IrisBiomeCustomPrecipType { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java new file mode 100644 index 000000000..1354e3430 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java @@ -0,0 +1,85 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListEntityType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.common.data.registry.RegistryUtil; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.EntityType; + +import java.util.Locale; + +@Snippet("custom-biome-spawn") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A custom biome spawn") +@Data +public class IrisBiomeCustomSpawn { + private final transient AtomicCache typeResolved = new AtomicCache<>(); + @Required + @RegistryListEntityType + @Desc("The biome's entity type") + private String type = "minecraft:cow"; + + public EntityType getType() { + String typeKey = getTypeKey(); + if (typeKey == null) { + return null; + } + return typeResolved.aquire(() -> { + NamespacedKey namespacedKey = NamespacedKey.fromString(typeKey); + return namespacedKey == null ? null : RegistryUtil.lookup(EntityType.class).get(namespacedKey); + }); + } + + public String getTypeKey() { + if (type == null || type.isBlank()) { + return null; + } + String normalized = type.trim().toLowerCase(Locale.ROOT); + return normalized.contains(":") ? normalized : "minecraft:" + normalized; + } + + @MinNumber(1) + @Desc("The min to spawn") + private int minCount = 2; + + @MinNumber(1) + @Desc("The max to spawn") + private int maxCount = 5; + + @MinNumber(1) + @MaxNumber(1000) + @Desc("The weight in this group. Higher weight, the more common this type is spawned") + private int weight = 1; + + @Desc("The rarity") + private IrisBiomeCustomSpawnType group = IrisBiomeCustomSpawnType.MISC; +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomSpawnType.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawnType.java similarity index 90% rename from core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomSpawnType.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawnType.java index 1c393e42d..ef614b7e6 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomSpawnType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawnType.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("The mob spawn group") public enum IrisBiomeCustomSpawnType { @@ -31,6 +31,9 @@ public enum IrisBiomeCustomSpawnType { @Desc("Eg bats") AMBIENT, + @Desc("Axolotls") + AXOLOTLS, + @Desc("Odd spawn group but ok") UNDERGROUND_WATER_CREATURE, diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeDerivatives.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeDerivatives.java new file mode 100644 index 000000000..6c61dcdb6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeDerivatives.java @@ -0,0 +1,152 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.common.data.registry.RegistryUtil; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.NamespacedKey; +import org.bukkit.block.Biome; + +/** + * Vanilla derivative and biome scatter resolution for {@link IrisBiome}. IrisBiome is Gson + * deserialized from pack JSON, so its fields stay put and only the behavior lives here. + */ +final class IrisBiomeDerivatives { + private IrisBiomeDerivatives() { + } + + static String namespacedBiomeKey(String key) { + if (key == null || key.isBlank()) { + return null; + } + String trimmed = key.trim(); + return trimmed.indexOf(':') >= 0 ? trimmed : "minecraft:" + trimmed; + } + + static String getVanillaDerivativeKey(String derivative, String vanillaDerivative) { + String resolved = namespacedBiomeKey(vanillaDerivative); + return resolved == null ? namespacedBiomeKey(derivative) : resolved; + } + + static String getStructureDerivativeKey(IrisBiome biome) { + String key = biome.getVanillaDerivativeKey(); + if (key == null || !key.startsWith("minecraft:")) { + return key; + } + if (biome.isSea() && !isVanillaSeaStructureBiome(key)) { + return "minecraft:the_void"; + } + if (biome.isShore() && !isVanillaShoreStructureBiome(key)) { + return "minecraft:beach"; + } + return key; + } + + static boolean isVanillaSeaStructureBiome(String key) { + return key != null && (key.contains("ocean") || key.endsWith("river")); + } + + static boolean isVanillaShoreStructureBiome(String key) { + return key != null && (key.endsWith("beach") || key.endsWith("shore")); + } + + static KList resolveBiomeKeys(KList keys) { + KList resolved = new KList<>(); + for (String key : keys) { + resolved.add(resolveBiomeKey(key)); + } + return resolved; + } + + static Biome resolveBiomeKey(String key) { + if (key == null) { + return null; + } + NamespacedKey namespacedKey = NamespacedKey.fromString(key); + return namespacedKey == null ? null : RegistryUtil.lookup(Biome.class).get(namespacedKey); + } + + static Biome getSkyBiome(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList biomeSkyScatter = biome.getBiomeSkyScatter(); + + if (biomeSkyScatter.size() == 1) { + return biome.getBiomeSkyScatterResolved().get(0); + } + + if (biomeSkyScatter.isEmpty()) { + return getGroundBiome(biome, rng, engine, x, y, z); + } + + return biome.getBiomeSkyScatterResolved().get(biome.getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z)); + } + + static Biome getGroundBiome(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList biomeScatter = biome.getBiomeScatter(); + + if (biomeScatter.isEmpty()) { + return biome.getDerivative(); + } + + if (biomeScatter.size() == 1) { + return biome.getBiomeScatterResolved().get(0); + } + + return biome.getBiomeGenerator(rng, engine).fit(biome.getBiomeScatterResolved(), x, y, z); + } + + static String getSkyBiomeKey(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList biomeSkyScatter = biome.getBiomeSkyScatter(); + + if (biomeSkyScatter.size() == 1) { + return namespacedBiomeKey(biomeSkyScatter.get(0)); + } + + if (biomeSkyScatter.isEmpty()) { + return getGroundBiomeKey(biome, rng, engine, x, y, z); + } + + return namespacedBiomeKey(biomeSkyScatter.get(biome.getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z))); + } + + static String getGroundBiomeKey(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList biomeScatter = biome.getBiomeScatter(); + + if (biomeScatter.isEmpty()) { + return biome.getDerivativeKey(); + } + + if (biomeScatter.size() == 1) { + return namespacedBiomeKey(biomeScatter.get(0)); + } + + return namespacedBiomeKey(biomeScatter.get(biome.getBiomeGenerator(rng, engine).fit(0, biomeScatter.size() - 1, x, y, z))); + } + + static IrisBiomeCustom getCustomBiome(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList customDerivitives = biome.getCustomDerivitives(); + + if (customDerivitives.size() == 1) { + return customDerivitives.get(0); + } + + return customDerivitives.get(biome.getBiomeGenerator(rng, engine).fit(0, customDerivitives.size() - 1, x, y, z)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGenLinks.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGenLinks.java new file mode 100644 index 000000000..08371424f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGenLinks.java @@ -0,0 +1,137 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KMap; + +/** + * Generator link lookups for {@link IrisBiome}. IrisBiome is Gson deserialized from pack JSON, so + * its fields stay put and only the behavior lives here. These are resolved per generation column, + * so the caches are read through {@link AtomicCache#getIfPresent()} first. + */ +final class IrisBiomeGenLinks { + private IrisBiomeGenLinks() { + } + + static double getGenLinkMax(IrisBiome biome, String loadKey, Engine engine) { + if (loadKey == null || loadKey.isBlank()) { + return 0; + } + + Integer v = maxIndex(biome).get(loadKey); + + return v == null ? 0 : v; + } + + static double getGenLinkMin(IrisBiome biome, String loadKey, Engine engine) { + if (loadKey == null || loadKey.isBlank()) { + return 0; + } + + Integer v = minIndex(biome).get(loadKey); + + return v == null ? 0 : v; + } + + static IrisBiomeGeneratorLink getGenLink(IrisBiome biome, String loadKey) { + if (loadKey == null || loadKey.isBlank()) { + return null; + } + + return linkIndex(biome).get(loadKey); + } + + private static KMap maxIndex(IrisBiome biome) { + AtomicCache> cache = biome.getGenCacheMax(); + KMap cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KMap l = new KMap<>(); + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + String generatorKey = i.getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + continue; + } + + l.put(generatorKey, i.getMax()); + + } + + return l; + }); + } + + private static KMap minIndex(IrisBiome biome) { + AtomicCache> cache = biome.getGenCacheMin(); + KMap cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KMap l = new KMap<>(); + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + String generatorKey = i.getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + continue; + } + + l.put(generatorKey, i.getMin()); + } + + return l; + }); + } + + private static KMap linkIndex(IrisBiome biome) { + AtomicCache> cache = biome.getGenCache(); + KMap cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KMap l = new KMap<>(); + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + String generatorKey = i.getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + continue; + } + + l.put(generatorKey, i); + } + + return l; + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java new file mode 100644 index 000000000..77881b1bb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java @@ -0,0 +1,89 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.common.data.DataProvider; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("generator-layer") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("This represents a link to a generator for a biome") +@Data +public class IrisBiomeGeneratorLink { + + private final transient AtomicCache gen = new AtomicCache<>(); + @RegistryListResource(IrisGenerator.class) + @Desc("The generator id") + private String generator = "default"; + @DependsOn({"min", "max"}) + @Required + @MinNumber(-2032) + @MaxNumber(2032) + @Desc("The min block value (value + fluidHeight)") + private int min = 0; + @DependsOn({"min", "max"}) + @Required + @MinNumber(-2032) + @MaxNumber(2032) + @Desc("The max block value (value + fluidHeight)") + private int max = 0; + + public IrisGenerator getCachedGenerator(DataProvider g) { + return gen.aquire(() -> { + String generatorKey = getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + generatorKey = "default"; + } + + IrisGenerator gen = g.getData().getGeneratorLoader().load(generatorKey); + + if (gen == null) { + gen = new IrisGenerator(); + } + + if (gen.getLoadKey() == null || gen.getLoadKey().isBlank()) { + gen.setLoadKey(generatorKey); + } + + return gen; + }); + } + + public double getHeight(DataProvider xg, double x, double z, long seed) { + double g = getCachedGenerator(xg).getHeight(x, z, seed); + g = g < 0 ? 0 : g; + g = g > 1 ? 1 : g; + + return IrisInterpolation.lerp(min, max, g); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java new file mode 100644 index 000000000..6bd9f17e0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java @@ -0,0 +1,358 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +/** + * Layer (block palette) generation for {@link IrisBiome}. IrisBiome is Gson deserialized from pack + * JSON, so its fields stay put and only the behavior lives here. + */ +final class IrisBiomeLayerGenerator { + private static final class States { + private static final PlatformBlockState BARRIER = B.getState("BARRIER"); + } + + private IrisBiomeLayerGenerator() { + } + + static KList generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { + if (biome.isLockLayers()) { + return generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex); + } + + KList data = new KList<>(); + + if (maxDepth <= 0) { + return data; + } + + KList layers = biome.getLayers(); + int layerCount = layers.size(); + + if (layerCount <= 0) { + return data; + } + + KList heightGenerators = getLayerHeightGenerators(biome, random, rdata); + + for (int i = 0; i < layerCount; i++) { + IrisBiomePaletteLayer layer = layers.get(i); + double zoom = layer.getZoom(); + CNG hgen = heightGenerators.get(i); + double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + + IrisSlopeClip sc = layer.getSlopeCondition(); + + if (!sc.isDefault()) { + if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { + d = 0; + } + } + + if (d <= 0) { + continue; + } + + for (int j = 0; j < d; j++) { + if (data.size() >= maxDepth) { + break; + } + + try { + data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + + if (data.size() >= maxDepth) { + break; + } + + if (dim.isExplodeBiomePalettes()) { + for (int j = 0; j < dim.getExplodeBiomePaletteSize(); j++) { + data.add(States.BARRIER); + + if (data.size() >= maxDepth) { + break; + } + } + } + } + + return data; + } + + static KList generateCeilingLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { + KList data = new KList<>(); + + if (maxDepth <= 0) { + return data; + } + + KList ceilingLayers = biome.getCaveCeilingLayers(); + int layerCount = ceilingLayers.size(); + + if (layerCount <= 0) { + return data; + } + + KList heightGenerators = getLayerHeightGenerators(biome, random, rdata); + // Ceiling layers reuse the surface-layer height generators, so entries beyond layers.size() have no generator; skip them. + int usableLayers = Math.min(layerCount, heightGenerators.size()); + + for (int i = 0; i < usableLayers; i++) { + IrisBiomePaletteLayer layer = ceilingLayers.get(i); + double zoom = layer.getZoom(); + CNG hgen = heightGenerators.get(i); + double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + + if (d <= 0) { + continue; + } + + for (int j = 0; j < d; j++) { + if (data.size() >= maxDepth) { + break; + } + + try { + data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + + if (data.size() >= maxDepth) { + break; + } + + if (dim.isExplodeBiomePalettes()) { + for (int j = 0; j < dim.getExplodeBiomePaletteSize(); j++) { + data.add(States.BARRIER); + + if (data.size() >= maxDepth) { + break; + } + } + } + } + + return data; + } + + static KList generateLockedLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex) { + KList data = new KList<>(); + KList real = new KList<>(); + int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax()); + if (maxDepth <= 0) { + return data; + } + + KList layers = biome.getLayers(); + int layerCount = layers.size(); + + if (layerCount > 0) { + KList heightGenerators = getLayerHeightGenerators(biome, random, rdata); + + for (int i = 0; i < layerCount; i++) { + IrisBiomePaletteLayer layer = layers.get(i); + double zoom = layer.getZoom(); + CNG hgen = heightGenerators.get(i); + double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + + IrisSlopeClip sc = layer.getSlopeCondition(); + + if (!sc.isDefault()) { + if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { + d = 0; + } + } + + if (d <= 0) { + continue; + } + + for (int j = 0; j < d; j++) { + try { + data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + } + } + + if (data.isEmpty()) { + return real; + } + + for (int i = 0; i < maxDepth; i++) { + int offset = (512 - height) - i; + int index = offset % data.size(); + real.add(data.get(Math.max(index, 0))); + } + + return real; + } + + static KList generateSeaLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepth, IrisData rdata) { + KList data = new KList<>(); + + KList seaLayers = biome.getSeaLayers(); + int layerCount = seaLayers.size(); + + if (layerCount <= 0) { + return data; + } + + KList heightGenerators = getLayerSeaHeightGenerators(biome, random, rdata); + + for (int i = 0; i < layerCount; i++) { + IrisBiomePaletteLayer layer = seaLayers.get(i); + double zoom = layer.getZoom(); + CNG hgen = heightGenerators.get(i); + int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + + if (d < 0) { + continue; + } + + for (int j = 0; j < d; j++) { + if (data.size() >= maxDepth) { + break; + } + + try { + data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + + if (data.size() >= maxDepth) { + break; + } + } + + return data; + } + + static KList getLayerHeightGenerators(IrisBiome biome, RNG rng, IrisData rdata) { + AtomicCache> cache = biome.getLayerHeightGenerators(); + KList cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KList layerHeightGenerators = new KList<>(); + + int m = 7235; + + for (IrisBiomePaletteLayer i : biome.getLayers()) { + layerHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), rdata)); + } + + return layerHeightGenerators; + }); + } + + static KList getLayerSeaHeightGenerators(IrisBiome biome, RNG rng, IrisData data) { + AtomicCache> cache = biome.getLayerSeaHeightGenerators(); + KList cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KList layerSeaHeightGenerators = new KList<>(); + + int m = 7735; + + for (IrisBiomePaletteLayer i : biome.getSeaLayers()) { + layerSeaHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data)); + } + + return layerSeaHeightGenerators; + }); + } + + static PlatformBlockState getSurfaceBlock(IrisBiome biome, int x, int z, RNG rng, IrisData idm) { + KList layers = biome.getLayers(); + + if (layers.isEmpty()) { + return B.getState("AIR"); + } + + return layers.get(0).get(rng, x, 0, z, idm); + } + + static int getMaxHeight(IrisBiome biome, Engine engine) { + return biome.getMaxHeight().aquire(() -> + { + int maxHeight = 0; + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + maxHeight += i.getMax(); + } + + return maxHeight; + }); + } + + static int getMaxWithObjectHeight(IrisBiome biome, IrisData data, Engine engine) { + return biome.getMaxWithObjectHeight().aquire(() -> + { + int maxHeight = 0; + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + maxHeight += i.getMax(); + } + + int gg = 0; + + for (IrisObjectPlacement i : biome.getObjects()) { + for (IrisObject j : data.getObjectLoader().loadAll(i.getPlace())) { + gg = Math.max(gg, j.getH()); + } + } + + return maxHeight + gg + 3; + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeOres.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeOres.java new file mode 100644 index 000000000..ea8c71769 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeOres.java @@ -0,0 +1,129 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +/** + * Ore generation for {@link IrisBiome}. IrisBiome is Gson deserialized from pack JSON, so its + * fields stay put and only the behavior lives here. Note that {@code IrisBiome.setOres(KList)} + * stays on IrisBiome: it is a hand written Lombok setter override that resets these caches. + */ +final class IrisBiomeOres { + private IrisBiomeOres() { + } + + static PlatformBlockState generateOres(IrisBiome biome, int x, int y, int z, RNG rng, IrisData data, boolean surface) { + KList localOres = surface ? getSurfaceOres(biome) : getUndergroundOres(biome); + return generateOres(localOres, x, y, z, rng, data); + } + + static PlatformBlockState generateSurfaceOres(IrisBiome biome, int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getSurfaceOres(biome), x, y, z, rng, data); + } + + static PlatformBlockState generateUndergroundOres(IrisBiome biome, int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getUndergroundOres(biome), x, y, z, rng, data); + } + + static boolean hasSurfaceOres(IrisBiome biome) { + return !getSurfaceOres(biome).isEmpty(); + } + + static boolean hasUndergroundOres(IrisBiome biome) { + return !getUndergroundOres(biome).isEmpty(); + } + + static KList getSurfaceOreGenerators(IrisBiome biome) { + return getOres(biome, true); + } + + static KList getUndergroundOreGenerators(IrisBiome biome) { + return getOres(biome, false); + } + + static IrisOreGeneratorBounds getSurfaceOreGeneratorBounds(IrisBiome biome) { + // getIfPresent fast path: aquire allocates a capturing lambda even on a hit, and this + // runs per column on the terrain hot path. + IrisOreGeneratorBounds cached = biome.getSurfaceOreBoundsCache().getIfPresent(); + if (cached != null) { + return cached; + } + return biome.getSurfaceOreBoundsCache().aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres(biome))); + } + + static IrisOreGeneratorBounds getUndergroundOreGeneratorBounds(IrisBiome biome) { + IrisOreGeneratorBounds cached = biome.getUndergroundOreBoundsCache().getIfPresent(); + if (cached != null) { + return cached; + } + return biome.getUndergroundOreBoundsCache().aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres(biome))); + } + + private static KList getSurfaceOres(IrisBiome biome) { + return getOres(biome, true); + } + + private static KList getUndergroundOres(IrisBiome biome) { + return getOres(biome, false); + } + + private static KList getOres(IrisBiome biome, boolean surface) { + AtomicCache> oreCache = surface ? biome.getSurfaceOreCache() : biome.getUndergroundOreCache(); + KList cached = oreCache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return oreCache.aquire(() -> { + KList filtered = new KList<>(); + KList localOres = biome.getOres(); + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + if (oreGenerator.isGenerateSurface() == surface) { + filtered.add(oreGenerator); + } + } + + return filtered; + }); + } + + private static PlatformBlockState generateOres(KList localOres, int x, int y, int z, RNG rng, IrisData data) { + if (localOres.isEmpty()) { + return null; + } + + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + PlatformBlockState ore = oreGenerator.generate(x, y, z, rng, data); + if (ore != null) { + return ore; + } + } + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java new file mode 100644 index 000000000..693f98eba --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java @@ -0,0 +1,161 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; +import lombok.experimental.Accessors; + +@Snippet("biome-palette") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A layer of surface / subsurface material in biomes") +@Data +public class IrisBiomePaletteLayer { + private final transient AtomicCache> blockData = new AtomicCache<>(); + private final transient AtomicCache layerGenerator = new AtomicCache<>(); + private final transient AtomicCache heightGenerator = new AtomicCache<>(); + @Desc("The style of noise") + private IrisGeneratorStyle style = NoiseStyle.STATIC.style(); + @DependsOn({"minHeight", "maxHeight"}) + @MinNumber(0) + @MaxNumber(2032) + + @Desc("The min thickness of this layer") + private int minHeight = 1; + @DependsOn({"minHeight", "maxHeight"}) + @MinNumber(1) + @MaxNumber(2032) + + @Desc("The max thickness of this layer") + private int maxHeight = 1; + @Desc("If set, this layer only shows where the terrain slope is within these bounds; outside them the layer is skipped entirely. Thickness is not scaled by slope.") + private IrisSlopeClip slopeCondition = new IrisSlopeClip(); + @MinNumber(0.0001) + @Desc("The terrain zoom mostly for zooming in on a wispy palette") + private double zoom = 5; + @Required + @ArrayType(min = 1, type = IrisBlockData.class) + @Desc("The palette of blocks to be used in this layer") + private KList palette = new KList().qadd(new IrisBlockData("GRASS_BLOCK")); + + public CNG getHeightGenerator(RNG rng, IrisData data) { + CNG cached = heightGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + + return heightGenerator.aquire(() -> CNG.signature(rng.nextParallelRNG(minHeight * maxHeight + getBlockData(data).size()))); + } + + public PlatformBlockState get(RNG rng, double x, double y, double z, IrisData data) { + return get(rng, 0, x, y, z, data); + } + + /** + * Resolves a block for this layer without allocating a child RNG per call. The child RNG is + * only built inside the lazy layer generator initializer, using the exact same seed derivation + * as {@code parent.nextParallelRNG(signature)} followed by the generator signature. + */ + public PlatformBlockState get(RNG parent, int signature, double x, double y, double z, IrisData data) { + KList localBlockData = getBlockData(data); + + if (localBlockData.isEmpty()) { + return null; + } + + if (localBlockData.size() == 1) { + return localBlockData.get(0); + } + + double localZoom = zoom; + double scaledX = x / localZoom; + double scaledY = y / localZoom; + double scaledZ = z / localZoom; + return getLayerGenerator(parent, signature, data).fit(localBlockData, scaledX, scaledY, scaledZ); + } + + public CNG getLayerGenerator(RNG rng, IrisData data) { + return getLayerGenerator(rng, 0, data); + } + + public CNG getLayerGenerator(RNG parent, int signature, IrisData data) { + CNG cached = layerGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + + return layerGenerator.aquire(() -> + { + RNG rngx = parent.nextParallelRNG(signature).nextParallelRNG(minHeight + maxHeight + getBlockData(data).size()); + return style.create(rngx, data); + }); + } + + public KList add(String b) { + palette.add(new IrisBlockData(b)); + + return palette; + } + + public KList getBlockData(IrisData data) { + KList cached = blockData.getIfPresent(); + + if (cached != null) { + return cached; + } + + return blockData.aquire(() -> + { + KList blockData = new KList<>(); + for (IrisBlockData ix : palette) { + PlatformBlockState bx = ix.getBlockData(data); + if (bx != null) { + for (int i = 0; i < ix.getWeight(); i++) { + blockData.add(bx); + } + } + } + + return blockData; + }); + } + + public IrisBiomePaletteLayer zero() { + palette.clear(); + return this; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBlockData.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBlockData.java new file mode 100644 index 000000000..2bbaa2c69 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBlockData.java @@ -0,0 +1,236 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + + +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.RegistryMapBlockState; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.data.B; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Map; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents Block Data") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisBlockData extends IrisRegistrant { + private final transient AtomicCache blockdata = new AtomicCache<>(); + private final transient AtomicCache realProperties = new AtomicCache<>(); + @RegistryListBlockType + @Required + @Desc("The block to use") + private String block = "air"; + @Desc("Debug this block by printing it to the console when it's used. Must have debug turned on in settings.") + private boolean debug = false; + @MinNumber(1) + @MaxNumber(1000) + @Desc("The weight is used when this block data is inside of a list of blockdata. A weight of two is just as if you placed two of the same block data values in the same list making it more common when randomly picked.") + private int weight = 1; + @Desc("If the block cannot be created on this version, Iris will attempt to use this backup block data instead.") + private IrisBlockData backup = null; + @RegistryMapBlockState("block") + @Desc("Optional properties for this block data such as 'waterlogged': true") + private KMap data = new KMap<>(); + @Desc("Optional tile data for this block data") + private KMap tileData = new KMap<>(); + + public IrisBlockData(String b) { + this.block = b; + } + + public static IrisBlockData from(String j) { + IrisBlockData b = new IrisBlockData(); + String v = j.toLowerCase().trim(); + + if (v.contains("[")) { + KList props = new KList<>(); + String rp = v.split("\\Q[\\E")[1].replaceAll("\\Q]\\E", ""); + b.setBlock(v.split("\\Q[\\E")[0]); + + if (rp.contains(",")) { + props.add(rp.split("\\Q,\\E")); + } else { + props.add(rp); + } + + for (String i : props) { + Object kg = filter(i.split("\\Q=\\E")[1]); + b.data.put(i.split("\\Q=\\E")[0], kg); + } + } else { + b.setBlock(v); + } + + return b; + } + + private static Object filter(String string) { + if (string.equals("true")) { + return true; + } + + if (string.equals("false")) { + return false; + } + + try { + return Integer.parseInt(string); + } catch (Throwable ignored) { + // Checks + } + + try { + return Double.valueOf(string).intValue(); + } catch (Throwable ignored) { + // Checks + } + + return string; + } + + public String computeProperties(KMap data) { + if (data.isEmpty()) { + return ""; + } + + KList r = new KList<>(); + + for (Map.Entry entry : data.entrySet()) { + r.add(entry.getKey() + "=" + filter(entry.getValue().toString())); + } + + return "[" + r.toString(",") + "]"; + } + + public String computeProperties() { + return computeProperties(getData()); + } + + public PlatformBlockState getBlockData(IrisData data) { + return blockdata.aquire(() -> + { + IrisBlockData customData = data.getBlockLoader().load(getBlock(), false); + + if (customData != null) { + PlatformBlockState customState = customData.getBlockData(data); + + if (customState != null) { + String st = customState.key(); + + if (st.contains("[")) { + st = st.split("\\Q[\\E")[0]; + } + + KMap cdata = customData.getData().copy(); + + for (String i : getData().keySet()) { + cdata.put(i, getData().get(i)); + } + + String sx = keyify(st) + computeProperties(cdata); + + if (debug) { + IrisLogging.debug("Block Data used " + sx + " (CUSTOM)"); + } + + PlatformBlockState bx = B.getState(sx); + + if (bx != null) { + return bx; + } + + return customState; + } + } + + String ss = keyify(getBlock()) + computeProperties(); + PlatformBlockState resolved = B.getState(ss); + + if (debug) { + IrisLogging.debug("Block Data used " + ss); + } + + if (resolved != null) { + return resolved; + } + + if (backup != null) { + return backup.getBlockData(data); + } + + return B.getState("AIR"); + }); + } + + public TileData tryGetTile(IrisData data) { + //TODO Do like a registry thing with the tile data registry. Also update the parsing of data to include **block** entities. + PlatformBlockState state = getBlockData(data); + String stateKey = state.key(); + int bracket = stateKey.indexOf('['); + String blockKey = bracket >= 0 ? stateKey.substring(0, bracket) : stateKey; + if (blockKey.equals("minecraft:spawner") && this.data.containsKey("entitySpawn")) { + String id = (String) this.data.get("entitySpawn"); + if (tileData == null) tileData = new KMap<>(); + KMap spawnData = (KMap) tileData.computeIfAbsent("SpawnData", k -> new KMap<>()); + KMap entity = (KMap) spawnData.computeIfAbsent("entity", k -> new KMap<>()); + entity.putIfAbsent("id", Identifier.fromString(id).toString()); + } + + if (tileData == null || tileData.isEmpty() || !state.hasTileEntity()) + return null; + return TileData.of(state, this.tileData); + } + + private String keyify(String dat) { + if (dat.contains(":")) { + return dat; + } + + return "minecraft:" + dat; + } + + @Override + public String getFolderName() { + return "blocks"; + } + + @Override + public String getTypeName() { + return "Block"; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBlockDrops.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBlockDrops.java similarity index 78% rename from core/src/main/java/com/volmit/iris/engine/object/IrisBlockDrops.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisBlockDrops.java index bcc2b4b5f..83eb8bd21 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBlockDrops.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBlockDrops.java @@ -16,19 +16,21 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; import lombok.experimental.Accessors; import org.bukkit.block.data.BlockData; import org.bukkit.inventory.ItemStack; @@ -60,10 +62,10 @@ public class IrisBlockDrops { KList b = new KList<>(); for (IrisBlockData i : getBlocks()) { - BlockData dd = i.getBlockData(rdata); + PlatformBlockState state = i.getBlockData(rdata); - if (dd != null) { - b.add(dd); + if (state != null) { + b.add((BlockData) state.nativeHandle()); } } @@ -81,7 +83,7 @@ public class IrisBlockDrops { public void fillDrops(boolean debug, KList d) { for (IrisLoot i : getDrops()) { - if (RNG.r.i(1, i.getRarity()) == i.getRarity()) { + if (LootResolver.oneIn(RNG.r, i.getRarity())) { d.add(i.get(debug, RNG.r)); } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveAnchorMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveAnchorMode.java new file mode 100644 index 000000000..e6a82f168 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveAnchorMode.java @@ -0,0 +1,21 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Defines which carved-space anchor to target for cave object placement.") +public enum IrisCaveAnchorMode { + @Desc("Use the active cave profile default anchor mode.") + PROFILE_DEFAULT, + + @Desc("Target cave floor anchors where carved space has solid support below.") + FLOOR, + + @Desc("Target cave ceiling anchors where carved space has solid support above.") + CEILING, + + @Desc("Target carved positions with no immediate solid support above or below.") + CENTER, + + @Desc("Target any carved-space anchor.") + ANY +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java new file mode 100644 index 000000000..0368457cb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java @@ -0,0 +1,36 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("cave-field-module") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a modular cave-density layer.") +@Data +public class IrisCaveFieldModule { + @Desc("Density style used by this module.") + private IrisGeneratorStyle style = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + + @MinNumber(0) + @Desc("Layer contribution multiplier.") + private double weight = 1; + + @MinNumber(-1) + @MaxNumber(1) + @Desc("Threshold offset applied to this layer before blending.") + private double threshold = 0; + + @Desc("Vertical bounds where this module can contribute, in engine-local Y where 0 is the bottom of the dimension, not world Y.") + private IrisRange verticalRange = new IrisRange(0, 384); + + @Desc("Invert this module before weighting.") + private boolean invert = false; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java new file mode 100644 index 000000000..39aac502c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java @@ -0,0 +1,148 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("cave-profile") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a configurable 3D cave profile.") +@Data +public class IrisCaveProfile { + @Desc("Enable profile-driven cave carving.") + private boolean enabled = false; + + @Desc("Global vertical bounds for profile cave carving, in engine-local Y where 0 is the bottom of the dimension, not world Y.") + private IrisRange verticalRange = new IrisRange(0, 384); + + @MinNumber(0) + @MaxNumber(128) + @Desc("Vertical fade range applied near cave profile min/max bounds to avoid abrupt hard-stop ceilings/floors.") + private int verticalEdgeFade = 20; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Strength of the vertical edge fade at cave profile min/max bounds.") + private double verticalEdgeFadeStrength = 0.18; + + @Desc("Base density style for cave field generation.") + private IrisGeneratorStyle baseDensityStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + + @Desc("Detail density style blended into base caves.") + private IrisGeneratorStyle detailDensityStyle = new IrisGeneratorStyle(NoiseStyle.SIMPLEX); + + @Desc("Warp style used to distort cave coordinates.") + private IrisGeneratorStyle warpStyle = new IrisGeneratorStyle(NoiseStyle.FLAT); + + @MinNumber(0) + @Desc("Base cave field multiplier.") + private double baseWeight = 1; + + @MinNumber(0) + @Desc("Detail cave field multiplier.") + private double detailWeight = 0.35; + + @MinNumber(0) + @Desc("Coordinate warp strength for cave fields.") + private double warpStrength = 0; + + @Desc("Threshold range used for carve cutoff decisions.") + private IrisStyledRange densityThreshold = new IrisStyledRange(-0.2, 0.2, NoiseStyle.CELLULAR_IRIS_DOUBLE.style()); + + @MinNumber(0) + @MaxNumber(1) + @Desc("Extra threshold bias subtracted from sampled threshold before carve tests.") + private double thresholdBias = 0.16; + + @MinNumber(1) + @MaxNumber(8) + @Desc("Vertical sample step used while evaluating cave density.") + private int sampleStep = 1; + + @Desc("Use adaptive cave density prediction so only threshold-adjacent cells fall back to full-resolution evaluation.") + private boolean adaptiveSampling = true; + + @MinNumber(2) + @MaxNumber(4) + @Desc("Horizontal adaptive predictor tuning. The carver always samples the predictor grid at a fixed step of 8; values below 8 only widen the ambiguity margin slightly and do not tighten the grid.") + private int adaptiveSampleStep = 2; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Additional adaptive ambiguity margin used before the cave predictor falls back to exact sampling.") + private double adaptiveThresholdMargin = 0.04; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Minimum solid clearance below terrain surface where carving may occur.") + private int surfaceClearance = 4; + + @Desc("Allow profile-driven cave carving to break through terrain surface in selected columns.") + private boolean allowSurfaceBreak = true; + + @Desc("Noise style used to decide where surface-breaking cave columns are allowed.") + private IrisGeneratorStyle surfaceBreakStyle = new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.08); + + @MinNumber(-1) + @MaxNumber(1) + @Desc("Minimum signed surface-break noise value required before near-surface carving is allowed.") + private double surfaceBreakNoiseThreshold = 0.62; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Near-surface depth window used for surface-break carve logic.") + private int surfaceBreakDepth = 18; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Additional threshold boost applied while carving in the surface-break depth window.") + private double surfaceBreakThresholdBoost = 0.2; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Minimum depth below terrain surface required for cave-only object anchor placement.") + private int objectMinDepthBelowSurface = 6; + + @ArrayType(type = IrisCaveFieldModule.class, min = 1) + @Desc("Additional layered cave-density modules.") + private KList modules = new KList<>(); + + @Desc("Default cave anchor mode for cave-only object placement.") + private IrisCaveAnchorMode defaultObjectAnchor = IrisCaveAnchorMode.FLOOR; + + @Desc("Default placement mode for cave objects. Only applies to placements that are still on the default CENTER_HEIGHT mode; an explicit mode on the placement always wins. Stilt modes tile the object base block down to the cave floor surface. FAST_MIN_STILT is recommended for cave objects to prevent floating.") + private ObjectPlaceMode defaultObjectPlaceMode = null; + + @MinNumber(1) + @MaxNumber(8) + @Desc("Vertical scan step used while searching cave anchors.") + private int anchorScanStep = 1; + + @MinNumber(1) + @MaxNumber(64) + @Desc("Maximum random column retries while searching a valid cave object anchor in the chunk.") + private int anchorSearchAttempts = 6; + + @Desc("Allow cave fluid placement from the dimension fluid palette below fluid level.") + private boolean allowFluid = true; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Minimum depth below terrain surface required before cave fluid may be placed.") + private int fluidMinDepthBelowSurface = 12; + + @Desc("Require solid floor support below cave fluid to reduce unsupported fluid flows.") + private boolean fluidRequiresFloor = true; + + @Desc("Allow cave lava placement based on lava height.") + private boolean allowLava = true; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfileSampler.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfileSampler.java new file mode 100644 index 000000000..87f542f1a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfileSampler.java @@ -0,0 +1,165 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayList; +import java.util.List; + +public final class IrisCaveProfileSampler { + private static final double FLOATING_THRESHOLD_BIAS_SCALE = 0.2D; + + private final IrisData data; + private final IrisCaveProfile profile; + private final CNG baseDensity; + private final CNG detailDensity; + private final CNG warpDensity; + private final RNG thresholdRng; + private final ModuleState[] modules; + private final double inverseNormalization; + private final double baseWeight; + private final double detailWeight; + private final double warpStrength; + private final boolean enabled; + private final boolean hasWarp; + private final boolean hasModules; + + public IrisCaveProfileSampler(Engine engine, IrisCaveProfile profile) { + this.data = engine.getData(); + this.profile = profile; + List moduleStates = new ArrayList<>(); + RNG baseRng = new RNG(engine.getSeedManager().getCarve()); + this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data); + this.detailDensity = profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data); + this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data); + this.thresholdRng = baseRng.nextParallelRNG(489_112); + this.baseWeight = profile.getBaseWeight(); + this.detailWeight = profile.getDetailWeight(); + this.warpStrength = profile.getWarpStrength(); + this.hasWarp = warpStrength > 0D; + + double weight = Math.abs(baseWeight) + Math.abs(detailWeight); + int index = 0; + for (IrisCaveFieldModule module : profile.getModules()) { + CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data); + ModuleState state = new ModuleState(module, moduleDensity); + moduleStates.add(state); + weight += Math.abs(state.weight); + index++; + } + + this.modules = moduleStates.toArray(new ModuleState[0]); + double normalization = weight <= 0 ? 1D : weight; + this.inverseNormalization = 1D / normalization; + this.hasModules = modules.length > 0; + this.enabled = profile.isEnabled() && baseDensity != null && detailDensity != null && (!hasWarp || warpDensity != null); + } + + public boolean shouldCarve(int x, int y, int z, double floatingCarveThreshold) { + if (!enabled) { + return false; + } + + double threshold = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias(); + threshold += floatingThresholdBias(floatingCarveThreshold); + return sampleDensity(x, y, z) <= threshold; + } + + double sampleDensity(int x, int y, int z) { + if (hasWarp) { + return sampleDensityWarped(x, y, z); + } + + return sampleDensityUnwarped(x, y, z); + } + + private double sampleDensityUnwarped(int x, int y, int z) { + double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight; + density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight; + if (hasModules) { + for (ModuleState module : modules) { + if (!module.isActive(y)) { + continue; + } + + density += module.sample(x, y, z); + } + } + + return density * inverseNormalization; + } + + private double sampleDensityWarped(int x, int y, int z) { + double warpA = warpDensity.noiseFastSigned3D(x, y, z); + double warpB = warpDensity.noiseFastSigned3D(x + 31.37D, y - 17.21D, z + 23.91D); + double warpedX = x + (warpA * warpStrength); + double warpedY = y + (warpB * warpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); + double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight; + density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight; + if (hasModules) { + for (ModuleState module : modules) { + if (!module.isActive(y)) { + continue; + } + + density += module.sample(warpedX, warpedY, warpedZ); + } + } + + return density * inverseNormalization; + } + + private double floatingThresholdBias(double floatingCarveThreshold) { + double clamped = Math.max(0D, Math.min(1D, floatingCarveThreshold)); + return (1D - clamped) * FLOATING_THRESHOLD_BIAS_SCALE; + } + + private static final class ModuleState { + private final CNG density; + private final int minY; + private final int maxY; + private final double weight; + private final double threshold; + private final boolean invert; + + private ModuleState(IrisCaveFieldModule module, CNG density) { + IrisRange range = module.getVerticalRange(); + this.density = density; + this.minY = (int) Math.floor(range.getMin()); + this.maxY = (int) Math.ceil(range.getMax()); + this.weight = module.getWeight(); + this.threshold = module.getThreshold(); + this.invert = module.isInvert(); + } + + private boolean isActive(int y) { + return density != null && y >= minY && y <= maxY; + } + + private double sample(double x, double y, double z) { + double sampled = density.noiseFastSigned3D(x, y, z); + return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight; + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisColor.java b/core/src/main/java/art/arcane/iris/engine/object/IrisColor.java similarity index 85% rename from core/src/main/java/com/volmit/iris/engine/object/IrisColor.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisColor.java index bfbb7cf7d..3a8b255be 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisColor.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisColor.java @@ -16,19 +16,19 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MaxNumber; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import java.awt.*; +import java.awt.Color; @Snippet("color") @Accessors(chain = true) @@ -87,7 +87,7 @@ public class IrisColor { try { return Color.decode(v); } catch (Throwable e) { - Iris.reportError(e); + IrisLogging.reportError(e); } } @@ -96,10 +96,6 @@ public class IrisColor { }); } - public org.bukkit.Color getBukkitColor() { - return org.bukkit.Color.fromRGB(getColor().getRGB()); - } - public int getAsRGB() { if (hex != null) { try { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCommand.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCommand.java new file mode 100644 index 000000000..bd9262b9a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCommand.java @@ -0,0 +1,84 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformWorld; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("command") +@Accessors(chain = true) +@NoArgsConstructor +@Desc("Represents a set of Iris commands") +@Data +public class IrisCommand { + + @Required + @ArrayType(min = 1, type = String.class) + @Desc("List of commands. Iris replaces {x} {y} and {z} with the location of the entity spawn") + private KList commands = new KList<>(); + + @Desc("The delay for running the command. Instant by default") + private long delay = 0; + + @Desc("If this should be repeated (indefinitely, cannot be cancelled). This does not persist with server-restarts, so it only repeats when the chunk is generated.") + private boolean repeat = false; + + @Desc("The delay between repeats, in server ticks (by default 100, so 5 seconds)") + private long repeatDelay = 100; + + @Desc("The block of 24 hour time in which the command should execute.") + private IrisTimeBlock timeBlock = new IrisTimeBlock(); + + @Desc("The weather that is required for the command to execute.") + private IrisWeather weather = IrisWeather.ANY; + + public boolean isValid(PlatformWorld world) { + return timeBlock.isWithin(world) && weather.is(world); + } + + public void run(PlatformWorld world, int x, int y, int z) { + if (!isValid(world)) { + return; + } + + for (String command : commands) { + command = (command.startsWith("/") ? command.replaceFirst("/", "") : command) + .replaceAll("\\Q{x}\\E", String.valueOf(x)) + .replaceAll("\\Q{y}\\E", String.valueOf(y)) + .replaceAll("\\Q{z}\\E", String.valueOf(z)); + final String finalCommand = command; + int safeDelay = (int) Math.max(0, Math.min(Integer.MAX_VALUE, delay)); + if (repeat) { + int safeRepeatDelay = (int) Math.max(1, Math.min(Integer.MAX_VALUE, repeatDelay)); + J.s(() -> J.sr(() -> IrisPlatforms.get().dispatchConsoleCommand(finalCommand), safeRepeatDelay), safeDelay); + } else { + J.s(() -> IrisPlatforms.get().dispatchConsoleCommand(finalCommand), safeDelay); + } + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisCommandRegistry.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCommandRegistry.java similarity index 81% rename from core/src/main/java/com/volmit/iris/engine/object/IrisCommandRegistry.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisCommandRegistry.java index 39f36c3b5..b850e3ec6 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisCommandRegistry.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCommandRegistry.java @@ -16,11 +16,19 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.platform.bukkit.BukkitWorld; +import art.arcane.iris.spi.PlatformWorld; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; @@ -67,12 +75,13 @@ public class IrisCommandRegistry { public void run(Player p) { if (rawCommands.isNotEmpty()) { + PlatformWorld world = new BukkitWorld(p.getWorld()); Location part = p.getLocation().clone().add( commandRandomAltX ? RNG.r.d(-commandOffsetX, commandOffsetX) : commandOffsetX, commandRandomAltY ? RNG.r.d(-commandOffsetY, commandOffsetY) : commandOffsetY, commandRandomAltZ ? RNG.r.d(-commandOffsetZ, commandOffsetZ) : commandOffsetZ); for (IrisCommand rawCommand : rawCommands) { - rawCommand.run(part); + rawCommand.run(world, part.getBlockX(), part.getBlockY(), part.getBlockZ()); if (commandAllRandomLocations) { part = p.getLocation().clone().add( commandRandomAltX ? RNG.r.d(-commandOffsetX, commandOffsetX) : commandOffsetX, diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisCompat.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCompat.java similarity index 93% rename from core/src/main/java/com/volmit/iris/engine/object/IrisCompat.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisCompat.java index 8d2b32fb6..0e8097ccc 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisCompat.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCompat.java @@ -16,15 +16,16 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.spi.IrisLogging; import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.iris.util.common.scheduling.J; import lombok.Data; import org.bukkit.Material; import org.bukkit.block.data.BlockData; @@ -53,8 +54,8 @@ public class IrisCompat { try { IO.writeAll(f, defa); } catch (IOException e) { - Iris.error("Failed to writeNodeData to compat file"); - Iris.reportError(e); + IrisLogging.error("Failed to writeNodeData to compat file"); + IrisLogging.reportError(e); } }); } else { @@ -72,7 +73,7 @@ public class IrisCompat { } } catch (Throwable e) { e.printStackTrace(); - Iris.reportError(e); + IrisLogging.reportError(e); } } @@ -266,7 +267,7 @@ public class IrisCompat { String buf = n; int err = 16; - BlockData tx = B.getOrNull(buf, false); + BlockData tx = BukkitBlockResolution.getOrNull(buf, false); if (tx != null) { return tx; @@ -275,8 +276,8 @@ public class IrisCompat { searching: while (true) { if (err-- <= 0) { - Iris.error("Can't find block data for " + n); - return B.getNoCompat("STONE"); + IrisLogging.warnOnce("compat-block:" + n, "Can't find block data for " + n + "; using STONE."); + return BukkitBlockResolution.getNoCompat("STONE"); } String m = buf; if (m.contains("[")) { @@ -299,15 +300,15 @@ public class IrisCompat { } } - Iris.error("Can't find block data for " + n); - return B.getNoCompat("STONE"); + IrisLogging.warnOnce("compat-block:" + n, "Can't find block data for " + n + "; using STONE."); + return BukkitBlockResolution.getNoCompat("STONE"); } } public Material getItem(String n) { String buf = n; int err = 16; - Material txf = B.getMaterialOrNull(buf); + Material txf = BukkitBlockResolution.getMaterialOrNull(buf); if (txf != null) { return txf; @@ -318,7 +319,7 @@ public class IrisCompat { searching: while (true) { if (nomore < 0) { - return B.getMaterial("STONE"); + return BukkitBlockResolution.getMaterial("STONE"); } nomore--; @@ -343,7 +344,7 @@ public class IrisCompat { } buf = n; - BlockData tx = B.getOrNull(buf, false); + BlockData tx = BukkitBlockResolution.getOrNull(buf, false); if (tx != null) { return tx.getMaterial(); @@ -353,13 +354,13 @@ public class IrisCompat { searching: while (true) { if (nomore < 0) { - return B.getMaterial("STONE"); + return BukkitBlockResolution.getMaterial("STONE"); } nomore--; if (err-- <= 0) { - return B.getMaterial("STONE"); + return BukkitBlockResolution.getMaterial("STONE"); } for (IrisCompatabilityBlockFilter i : blockFilters) { @@ -375,7 +376,7 @@ public class IrisCompat { } } - return B.getMaterial("STONE"); + return BukkitBlockResolution.getMaterial("STONE"); } } } diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisCompatabilityBlockFilter.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCompatabilityBlockFilter.java similarity index 77% rename from core/src/main/java/com/volmit/iris/engine/object/IrisCompatabilityBlockFilter.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisCompatabilityBlockFilter.java index 379a72e24..dd8d1fd8d 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisCompatabilityBlockFilter.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCompatabilityBlockFilter.java @@ -16,13 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.util.data.B; +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.spi.IrisLogging; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -51,19 +52,19 @@ public class IrisCompatabilityBlockFilter { } public BlockData getFind() { - return findData.aquire(() -> B.get(when)); + return findData.aquire(() -> BukkitBlockResolution.get(when)); } public BlockData getReplace() { return replaceData.aquire(() -> { - BlockData b = B.getOrNull(supplement, false); + BlockData b = BukkitBlockResolution.getOrNull(supplement, false); if (b == null) { return null; } - Iris.warn("Compat: Using '%s' in place of '%s' since this server doesnt support '%s'", supplement, when, when); + IrisLogging.warn("Compat: Using '%s' in place of '%s' since this server doesnt support '%s'", supplement, when, when); return b; }); diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisCompatabilityItemFilter.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCompatabilityItemFilter.java similarity index 75% rename from core/src/main/java/com/volmit/iris/engine/object/IrisCompatabilityItemFilter.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisCompatabilityItemFilter.java index 520092841..ef960581d 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisCompatabilityItemFilter.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCompatabilityItemFilter.java @@ -16,13 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.util.data.B; +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; @@ -48,19 +49,19 @@ public class IrisCompatabilityItemFilter { } public Material getFind() { - return findData.aquire(() -> B.getMaterial(when)); + return findData.aquire(() -> BukkitBlockResolution.getMaterial(when)); } public Material getReplace() { return replaceData.aquire(() -> { - Material b = B.getMaterialOrNull(supplement); + Material b = BukkitBlockResolution.getMaterialOrNull(supplement); if (b == null) { return null; } - Iris.verbose("Compat: Using " + supplement + " in place of " + when + " since this server doesnt support '" + supplement + "'"); + IrisLogging.debug("Compat: Using " + supplement + " in place of " + when + " since this server doesnt support '" + supplement + "'"); return b; }); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCoral.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCoral.java new file mode 100644 index 000000000..08377ba22 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCoral.java @@ -0,0 +1,238 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.coral.CoralGenerator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("coral") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A single procedurally generated coral reef structure. Iris bakes a pool of deterministic variants from these settings and scatters them across the seafloor at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file. Mirrors the procedural tree system but tuned for underwater reefs: structural blocks are waterlogged so the coral stays alive, and placement defaults to anchoring on the terrain beneath the water surface.") +@Data +public class IrisCoral implements IrisProceduralPlacement { + private final transient AtomicCache> variantCache = new AtomicCache<>(); + + @Desc("A human readable name used in logs and as the variant load key.") + private String name = "coral"; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance per chunk for this coral to attempt placement. Use density for multiple per chunk.") + private double chance = 0.4; + + @MinNumber(1) + @Desc("If the chance check passes, attempt this many placements in the chunk.") + private int density = 1; + + @MinNumber(1) + @MaxNumber(64) + @Desc("How many distinct variants to pre-bake for this coral. Higher means more variety at a small memory cost.") + private int variants = 6; + + @Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.") + private long seed = 1337; + + @Desc("The placement mode used to anchor the coral to the seafloor.") + private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT; + + @Desc("Rotate this coral's placement.") + private IrisObjectRotation rotation = new IrisObjectRotation(); + + @Desc("Limit the max or min height of placement.") + private IrisObjectLimit clamp = new IrisObjectLimit(); + + @Desc("Whether this coral may place on the terrain surface, under carvings, or both.") + private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY; + + @Desc("If true (default for coral), the coral anchors on the terrain height beneath the water, so it grows up from the seafloor rather than floating at the water surface.") + private boolean underwater = true; + + @Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the seafloor.") + private IrisObjectTranslate translate = new IrisObjectTranslate(); + + @Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).") + private IrisStiltSettings stiltSettings; + + @Desc("Settings for the vacuum place modes (VACUUM, VACUUM_HIGH, VACUUM_FAST, VACUUM_ORGANIC).") + private IrisVacuumSettings vacuumSettings; + + @Desc("If true (default), every resolved coral block that is waterloggable is forced waterlogged so the coral does not die in water (coral fans, sea pickles and kelp also waterlog). Set false to place dead/dry coral.") + private boolean waterlogged = true; + + @Desc("The overall silhouette this coral grows into (branching, fan, brain, pillar, tendril). Each form runs a different generation routine.") + private IrisCoralForm form = IrisCoralForm.BRANCHING; + + @Required + @Desc("The structural coral block (the living wood of the reef), e.g. minecraft:tube_coral_block. Ignored when blockPalette is set.") + @RegistryListBlockType + private String block = "minecraft:tube_coral_block"; + + @Desc("A noise-driven palette for the structural coral block. When set this overrides the single block, letting the reef mix tube/brain/bubble/fire/horn coral_block tones across its body.") + private IrisMaterialPalette blockPalette = null; + + @Desc("Optional tip block placed at branch tips and at the very top of the structure, e.g. coral fans or minecraft:sea_pickle. Ignored when tipPalette is set. Null disables tips.") + @RegistryListBlockType + private String tipBlock = null; + + @Desc("A noise-driven palette for the tip block. When set this overrides the single tipBlock, letting tips mix fan / pickle decorations.") + private IrisMaterialPalette tipPalette = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance per eligible tip position to place a tip block (coral fan / sea pickle).") + private double tipChance = 0.6; + + @MinNumber(1) + @Desc("Minimum overall height of the coral in blocks.") + private int heightMin = 4; + + @MinNumber(1) + @Desc("Maximum overall height of the coral in blocks.") + private int heightMax = 8; + + @MinNumber(0) + @Desc("Horizontal spread of the structure in blocks. For BRANCHING this is how far arms reach out, for BRAIN/PILLAR/FAN it widens the base footprint.") + private double spread = 3; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Lateral noise wobble (0-1) applied to branches and tendrils so they are not ruler-straight. 0 is perfectly straight, 1 is heavily wandering.") + private double sway = 0.5; + + @MinNumber(1) + @MaxNumber(12) + @Desc("BRANCHING only: how many arms sprout from the central stalk.") + private int branchCount = 4; + + @MinNumber(1) + @Desc("BRANCHING only: the length of each arm in blocks before reaching its tip.") + private double branchLength = 3; + + @MinNumber(0) + @MaxNumber(90) + @Desc("BRANCHING only: the upward elevation in degrees of each arm from horizontal. 90 is straight up, 0 is flat out.") + private double branchElevation = 55; + + @Desc("BRANCHING only: how the arms are distributed around the stalk by azimuth.") + private IrisCoralBranchAzimuth branchAzimuth = IrisCoralBranchAzimuth.GOLDEN_ANGLE; + + @Desc("BRANCHING only: if true, each arm splits once into a small fan of sub-arms for a bushier reef.") + private boolean subBranches = true; + + @MinNumber(1) + @MaxNumber(5) + @Desc("BRANCHING only: how many sub-arms each arm splits into when subBranches is true.") + private int subBranchCount = 2; + + @MinNumber(0) + @MaxNumber(1) + @Desc("BRANCHING only: the length of each sub-arm as a fraction of its parent arm.") + private double subBranchScale = 0.5; + + @MinNumber(0) + @MaxNumber(4) + @Desc("BRANCHING and PILLAR only: the radius of the small coral tip cluster grown at each tip.") + private int tipClusterRadius = 1; + + @MinNumber(1) + @MaxNumber(8) + @Desc("BRAIN only: the horizontal radius of the brain-coral blob in blocks.") + private int brainRadius = 3; + + @MinNumber(0) + @MaxNumber(1) + @Desc("BRAIN only: how strongly 3D value noise perturbs the ellipsoid surface, for a wrinkled organic blob. 0 is a smooth dome.") + private double brainRoughness = 0.35; + + @MinNumber(1) + @MaxNumber(12) + @Desc("PILLAR only: the radius of the stout vertical column in blocks.") + private int pillarRadius = 1; + + @MinNumber(1) + @MaxNumber(8) + @Desc("FAN only: the half-width of the upright fan plane in blocks (the disc widens toward its mid-height).") + private int fanWidth = 3; + + @MinNumber(1) + @MaxNumber(12) + @Desc("TENDRIL only: how many thin wavy stalks rise from the base.") + private int tendrilCount = 4; + + public KList getVariantObjects(IrisData data) { + return variantCache.aquire(() -> { + KList baked = new KList<>(); + int count = Math.max(1, variants); + + for (int i = 0; i < count; i++) { + IrisObject object = CoralGenerator.generate(this, i, new RNG(seed + (i * 7919L)), data); + if (object == null || object.getBlocks().isEmpty()) { + continue; + } + object.setLoadKey("procedural/" + name + "#" + i); + object.setLoader(data); + baked.add(object); + } + + return baked; + }); + } + + public IrisObject getVariantObject(IrisData data, RNG rng) { + KList baked = getVariantObjects(data); + if (baked == null || baked.isEmpty()) { + return null; + } + return baked.get(rng.i(baked.size())); + } + + public IrisObjectPlacement asPlacement() { + IrisObjectPlacement placement = new IrisObjectPlacement(); + placement.setMode(mode); + placement.setRotation(rotation); + placement.setClamp(clamp); + placement.setCarvingSupport(carvingSupport); + placement.setUnderwater(underwater); + placement.setTranslate(translate); + placement.setStiltSettings(stiltSettings); + placement.setVacuumSettings(vacuumSettings); + placement.setChance(chance); + placement.setDensity(density); + return placement; + } + + public boolean isPlausible() { + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCoralBranchAzimuth.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCoralBranchAzimuth.java new file mode 100644 index 000000000..1e711dd10 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCoralBranchAzimuth.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("How the arms of a BRANCHING coral are distributed around the central stalk by compass azimuth.") +public enum IrisCoralBranchAzimuth { + @Desc("Distribute arms by the golden angle (about 137.5 degrees per arm) for a natural phyllotactic spiral where no two arms overlap.") + GOLDEN_ANGLE, + + @Desc("Distribute arms in evenly spaced spokes, dividing 360 degrees equally between them like a radial whorl.") + EVEN, + + @Desc("Place each arm at a fully random azimuth using the deterministic per-variant RNG, for an irregular wild reef.") + RANDOM +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCoralForm.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCoralForm.java new file mode 100644 index 000000000..8f3b321d2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCoralForm.java @@ -0,0 +1,39 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("The overall silhouette a procedural coral grows into. Each form drives a completely different generation routine in the coral generator, from tree-like branching arms to rounded brain blobs and thin wavy tendrils.") +public enum IrisCoralForm { + @Desc("A tree-like coral built from a stout central stalk that sprouts several upward-leaning arms (distributed by golden angle or randomly), each optionally splitting once into sub-arms, with small coral tip clusters (fans / sea pickles) at the ends. The most structurally complex form and the closest analogue to a procedural tree.") + BRANCHING, + + @Desc("A thin, single-block-thick vertical fan plane that rises and widens with height like a sheet of fire or horn coral. Reads as a flat decorative blade rather than a volume.") + FAN, + + @Desc("A rounded, noise-perturbed ellipsoid blob of brain coral that bulges out of the seafloor like a boulder, with an irregular wrinkled surface driven by 3D value noise.") + BRAIN, + + @Desc("A stout, thick vertical column of coral block topped with a dense tip cluster. Wider and blunter than a tendril, reading as a solid coral pillar or stack.") + PILLAR, + + @Desc("Several very thin (one block wide) wavy stalks that rise upward and wander laterally using value noise, like a clump of swaying coral whips or kelp-thin growths.") + TENDRIL +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCrystal.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCrystal.java new file mode 100644 index 000000000..52ed68c4d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCrystal.java @@ -0,0 +1,219 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.crystal.CrystalGenerator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("crystal") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A single procedurally generated crystal cluster: a budding base blob with faceted, tapered shards radiating from it (geode / amethyst-style). Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file. Built for caves.") +@Data +public class IrisCrystal implements IrisProceduralPlacement { + private final transient AtomicCache> variantCache = new AtomicCache<>(); + + @Desc("A human readable name used in logs and as the variant load key.") + private String name = "crystal"; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance per chunk for this crystal cluster to attempt placement. Use density for multiple per chunk.") + private double chance = 0.2; + + @MinNumber(1) + @Desc("If the chance check passes, attempt this many placements in the chunk.") + private int density = 1; + + @MinNumber(1) + @MaxNumber(64) + @Desc("How many distinct variants to pre-bake for this crystal cluster. Higher means more variety at a small memory cost.") + private int variants = 6; + + @Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.") + private long seed = 1337; + + @Desc("The placement mode used to anchor the crystal cluster to the terrain.") + private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT; + + @Desc("Rotate this crystal cluster's placement.") + private IrisObjectRotation rotation = new IrisObjectRotation(); + + @Desc("Limit the max or min height of placement.") + private IrisObjectLimit clamp = new IrisObjectLimit(); + + @Desc("Whether this crystal cluster may place on the terrain surface, under carvings, or both. Crystals are meant for caves, so this defaults to CARVING_ONLY.") + private CarvingMode carvingSupport = CarvingMode.CARVING_ONLY; + + @Desc("If true, the crystal cluster anchors on the terrain height ignoring the water surface.") + private boolean underwater = false; + + @Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.") + private IrisObjectTranslate translate = new IrisObjectTranslate(); + + @Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).") + private IrisStiltSettings stiltSettings; + + @Desc("Settings for the vacuum place modes (VACUUM, VACUUM_HIGH, VACUUM_FAST, VACUUM_ORGANIC).") + private IrisVacuumSettings vacuumSettings; + + @Desc("The surface this crystal grows from (FLOOR shards up, CEILING shards down, WALL shards outward). This orients the baked geometry only.") + private IrisCrystalSurface growthSurface = IrisCrystalSurface.FLOOR; + + @Required + @Desc("The primary crystal shard block, e.g. minecraft:amethyst_block. Ignored when blockPalette is set.") + @RegistryListBlockType + private String block = "minecraft:amethyst_block"; + + @Desc("A noise-driven palette for the crystal shards (a prismatic mix, e.g. amethyst_block, calcite, tinted_glass). When set this overrides the single block, letting the shards mix blocks by noise. Palette wins.") + private IrisMaterialPalette blockPalette = null; + + @Desc("Optional block placed at the very tip of each shard for a different colored or sparkling point (e.g. minecraft:amethyst_cluster or glowstone). Ignored when tipPalette is set. If unset and glow is true, a light-emitting block is sprinkled among the tips instead.") + @RegistryListBlockType + private String tipBlock = null; + + @Desc("A noise-driven palette for the shard tips. When set this overrides the single tipBlock. Palette wins.") + private IrisMaterialPalette tipPalette = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance per shard that its very tip is replaced by the tip block (or glow block).") + private double tipChance = 0.6; + + @Desc("If true and no tipBlock/tipPalette is set, sprinkle a light-emitting block (the glowBlock) among the shard tips for a sparkling geode.") + private boolean glow = false; + + @Desc("The light-emitting block sprinkled among the tips when glow is true and no tip block is configured.") + @RegistryListBlockType + private String glowBlock = "minecraft:glowstone"; + + @Desc("Optional block for the budding base blob the shards grow from, e.g. minecraft:budding_amethyst or calcite. Ignored when basePalette is set. If unset, the base is built from the primary shard block.") + @RegistryListBlockType + private String baseBlock = "minecraft:budding_amethyst"; + + @Desc("A noise-driven palette for the budding base blob. When set this overrides the single baseBlock. Palette wins.") + private IrisMaterialPalette basePalette = null; + + @MinNumber(0) + @Desc("Radius in blocks of the budding base blob the shards radiate from. 0 places no base blob (shards spring from a single point).") + private double baseRadius = 1.6; + + @MinNumber(0) + @MaxNumber(1) + @Desc("How strongly value noise perturbs the surface of the base blob, for a lumpy organic budding base rather than a clean sphere.") + private double baseNoise = 0.35; + + @MinNumber(1) + @Desc("Minimum number of shards radiating from the base.") + private int shardCountMin = 5; + + @MinNumber(1) + @Desc("Maximum number of shards radiating from the base.") + private int shardCountMax = 11; + + @MinNumber(1) + @Desc("Minimum length in blocks of a shard, measured from the base out to its pointed tip.") + private int shardLengthMin = 3; + + @MinNumber(1) + @Desc("Maximum length in blocks of a shard, measured from the base out to its pointed tip.") + private int shardLengthMax = 8; + + @MinNumber(0.5) + @Desc("Starting radius in blocks of a shard at its thick base end. The shard tapers from this down to a single block at the tip.") + private double shardBaseRadius = 1.4; + + @MinNumber(0) + @MaxNumber(1) + @Desc("How aggressively each shard narrows from base to tip. 0 keeps a near-constant column; 1 tapers fully to a sharp point at the very tip. Each shard always ends in a single pointed block.") + private double shardTaper = 0.85; + + @MinNumber(0) + @MaxNumber(90) + @Desc("The half-angle in degrees of the cone the shards fan within around the surface normal. 0 makes all shards parallel to the normal; larger values splay them outward into a starburst.") + private double spreadAngle = 45; + + @Desc("How the shard azimuths are distributed around the normal (RANDOM for a chaotic clump, GOLDEN_ANGLE for an evenly spaced rosette).") + private IrisCrystalDistribution distribution = IrisCrystalDistribution.GOLDEN_ANGLE; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Per-shard angular randomness (0-1) added on top of the chosen distribution so the cluster never looks mechanically regular. 0 is perfectly distributed; 1 is heavily jittered.") + private double jitter = 0.25; + + public KList getVariantObjects(IrisData data) { + return variantCache.aquire(() -> { + KList baked = new KList<>(); + int count = Math.max(1, variants); + + for (int i = 0; i < count; i++) { + IrisObject object = CrystalGenerator.generate(this, i, new RNG(seed + (i * 7919L)), data); + if (object == null || object.getBlocks().isEmpty()) { + continue; + } + object.setLoadKey("procedural/" + name + "#" + i); + object.setLoader(data); + baked.add(object); + } + + return baked; + }); + } + + public IrisObject getVariantObject(IrisData data, RNG rng) { + KList baked = getVariantObjects(data); + if (baked == null || baked.isEmpty()) { + return null; + } + return baked.get(rng.i(baked.size())); + } + + public IrisObjectPlacement asPlacement() { + IrisObjectPlacement placement = new IrisObjectPlacement(); + placement.setMode(mode); + placement.setRotation(rotation); + placement.setClamp(clamp); + placement.setCarvingSupport(carvingSupport); + placement.setUnderwater(underwater); + placement.setTranslate(translate); + placement.setStiltSettings(stiltSettings); + placement.setVacuumSettings(vacuumSettings); + placement.setChance(chance); + placement.setDensity(density); + return placement; + } + + public boolean isPlausible() { + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCrystalDistribution.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCrystalDistribution.java new file mode 100644 index 000000000..adc4a1f7f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCrystalDistribution.java @@ -0,0 +1,30 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("How the shards of a crystal cluster are angularly distributed around the surface normal.") +public enum IrisCrystalDistribution { + @Desc("Each shard gets a uniformly random azimuth around the normal. Produces a chaotic, naturally clumped cluster.") + RANDOM, + + @Desc("Each shard's azimuth advances by the golden angle (~137.5 degrees) from the previous one. Produces an evenly spaced sunflower-like fan with no two shards overlapping, ideal for a geode rosette.") + GOLDEN_ANGLE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCrystalSurface.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCrystalSurface.java new file mode 100644 index 000000000..49cb1b7fb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCrystalSurface.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("The surface a crystal cluster grows from. This only orients the baked geometry (which way the shards point); placement of the object into a cave is handled by the placement system, not here.") +public enum IrisCrystalSurface { + @Desc("Grows from a cave floor. The budding base sits at the bottom and the shards point upward, fanning within the spread cone around the up axis. Built with positive py so the cluster grows up from the base layer.") + FLOOR, + + @Desc("Grows from a cave ceiling. The budding base sits at the top and the shards point downward like stalactites, fanning within the spread cone around the down axis. Built with negative py so the cluster grows down from the ceiling.") + CEILING, + + @Desc("Grows from a cave wall. The budding base hugs the wall and the shards point outward and slightly upward, fanning within the spread cone around a tilted normal so the cluster reads as a wall sconce of shards.") + WALL +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDecorationPart.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorationPart.java similarity index 93% rename from core/src/main/java/com/volmit/iris/engine/object/IrisDecorationPart.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisDecorationPart.java index 104d8af76..c458abb67 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDecorationPart.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorationPart.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("Represents a location where decorations should go") public enum IrisDecorationPart { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDecorationStep.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorationStep.java new file mode 100644 index 000000000..d6bd443ac --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorationStep.java @@ -0,0 +1,96 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +import java.util.Locale; + +/** + * Pure-JVM mirror of Minecraft's decoration generation steps, in registry (ordinal) order. The ordinals and + * serialized names match {@code GenerationStep.Decoration} on MC 26.2 so a platform can convert either way + * without core depending on a Minecraft type. Verified against MC 26.2 GenerationStep.Decoration. + */ +@Desc("A vanilla decoration generation step. Placed features are grouped into these steps and run in this order.") +public enum IrisDecorationStep { + @Desc("Raw generation - the earliest step, before lakes.") + RAW_GENERATION("raw_generation"), + + @Desc("Lakes.") + LAKES("lakes"), + + @Desc("Local modifications such as amethyst geodes, icebergs and dripstone clusters.") + LOCAL_MODIFICATIONS("local_modifications"), + + @Desc("Underground structure features (not the structure system - features tagged as underground structures).") + UNDERGROUND_STRUCTURES("underground_structures"), + + @Desc("Surface structure features.") + SURFACE_STRUCTURES("surface_structures"), + + @Desc("Stronghold step.") + STRONGHOLDS("strongholds"), + + @Desc("Underground ores. This is the step that carries vanilla and mod ore veins.") + UNDERGROUND_ORES("underground_ores"), + + @Desc("Underground decoration such as glow lichen, sculk patches and cave vegetation.") + UNDERGROUND_DECORATION("underground_decoration"), + + @Desc("Fluid springs - the small water and lava spring features.") + FLUID_SPRINGS("fluid_springs"), + + @Desc("Vegetal decoration - trees, grass, flowers, kelp and most surface plant life.") + VEGETAL_DECORATION("vegetal_decoration"), + + @Desc("Top layer modification - freezing and snow placement.") + TOP_LAYER_MODIFICATION("top_layer_modification"); + + private final String serializedName; + + IrisDecorationStep(String serializedName) { + this.serializedName = serializedName; + } + + public String getSerializedName() { + return serializedName; + } + + /** + * Resolves a step by its vanilla ordinal. Returns null when the running Minecraft version declares more + * decoration steps than this enum knows about, which the callers treat as "unknown step, generate it". + */ + public static IrisDecorationStep byOrdinal(int ordinal) { + IrisDecorationStep[] values = values(); + return ordinal < 0 || ordinal >= values.length ? null : values[ordinal]; + } + + public static IrisDecorationStep byKey(String key) { + if (key == null || key.isBlank()) { + return null; + } + String normalized = key.trim().toLowerCase(Locale.ROOT); + for (IrisDecorationStep step : values()) { + if (step.serializedName.equals(normalized) || step.name().toLowerCase(Locale.ROOT).equals(normalized)) { + return step; + } + } + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java new file mode 100644 index 000000000..a508ec57f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java @@ -0,0 +1,351 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; +import lombok.experimental.Accessors; + +@Snippet("decorator") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A biome decorator is used for placing flowers, grass, cacti and so on") +@Data +public class IrisDecorator { + private final transient AtomicCache layerGenerator = new AtomicCache<>(); + private final transient AtomicCache varianceGenerator = new AtomicCache<>(); + private final transient AtomicCache heightGenerator = new AtomicCache<>(); + private final transient AtomicCache> blockData = new AtomicCache<>(); + private final transient AtomicCache> blockDataTops = new AtomicCache<>(); + private final transient AtomicCache blockDataArray = new AtomicCache<>(); + private final transient AtomicCache blockDataTopsArray = new AtomicCache<>(); + private final transient AtomicCache whitelistArray = new AtomicCache<>(); + private final transient AtomicCache blacklistArray = new AtomicCache<>(); + @Desc("The varience dispersion is used when multiple blocks are put in the palette. Scatter scrambles them, Wispy shows streak-looking varience") + private IrisGeneratorStyle variance = NoiseStyle.STATIC.style(); + @Desc("Forcefully place this decorant anywhere it is supposed to go even if it should not go on a specific surface block. For example, you could force tallgrass to place on top of stone by using this.") + private boolean forcePlace = false; + @Desc("Forced the surface block of this decorant to be the specified block. Assumes forcePlace.") + private IrisBlockData forceBlock; + @ArrayType(min = 1, type = IrisBlockData.class) + @Desc("When set, the decorator can only place onto any of these blocks.") + private KList whitelist; + @ArrayType(min = 1, type = IrisBlockData.class) + @Desc("When set, the decorator will never place onto any of these blocks.") + private KList blacklist; + @Desc("The slope at which this decorator can be placed. Range from 0 to 10 by default. Calculated from a 3-block radius from the center of the decorator placement.") + private IrisSlopeClip slopeCondition = new IrisSlopeClip(); + @DependsOn({"scaleStack", "stackMin", "stackMax"}) + @Desc("If stackMax is set to true, use this to limit its max height for large caverns") + private int absoluteMaxStack = 30; + @Desc("Dispersion is used to pick places to spawn. Scatter randomly places them (vanilla) or Wispy for a streak like patch system.") + private IrisGeneratorStyle style = NoiseStyle.STATIC.style(); + @DependsOn({"stackMin", "stackMax"}) + @Desc("If this decorator has a height more than 1 this changes how it picks the height between your maxes. Scatter = random, Wispy = wavy heights") + private IrisGeneratorStyle heightVariance = NoiseStyle.STATIC.style(); + @Desc("Tells iris where this decoration is a part of. I.e. SHORE_LINE or SEA_SURFACE") + private IrisDecorationPart partOf = IrisDecorationPart.NONE; + @DependsOn({"stackMin", "stackMax"}) + @MinNumber(1) + @MaxNumber(2032) + @Desc("The minimum repeat stack height (setting to 3 would stack 3 of on top of each other") + private int stackMin = 1; + @DependsOn({"stackMin", "stackMax"}) + @MinNumber(1) + @MaxNumber(2032) + @Desc("The maximum repeat stack height") + private int stackMax = 1; + @DependsOn({"stackMin", "stackMax"}) + @Desc(""" + Changes stackMin and stackMin from being absolute block heights and instead uses them as a percentage to scale the stack based on the cave height + + Within a cave, setting them stackMin/max to 50 would make the stack 50% of the cave height""") + private boolean scaleStack = false; + @Required + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance for this decorator to decorate at a given X,Y coordinate. This is hit 256 times per chunk (per surface block)") + private double chance = 0.1; + @Required + @ArrayType(min = 1, type = IrisBlockData.class) + @Desc("The palette of blocks to pick from when this decorator needs to place.") + private KList palette = new KList().qadd(new IrisBlockData("grass")); + @ArrayType(min = 1, type = IrisBlockData.class) + @Desc("The palette of blocks used at the very top of a 'stackMax' of higher than 1. For example, bamboo tops.") + private KList topPalette = new KList<>(); + @DependsOn("topPalette") + @MinNumber(0.01) + @MaxNumber(1.0) + @Desc("When the stack passes the top threshold, the top palette will start being used instead of the normal palette.") + private double topThreshold = 1.0; + + public int getHeight(RNG rng, double x, double z, IrisData data) { + if (stackMin == stackMax) { + return stackMin; + } + + return getHeightGenerator(rng, data) + .fit(stackMin, stackMax, + x / heightVariance.getZoom(), + z / heightVariance.getZoom()); + } + + public CNG getHeightGenerator(RNG rng, IrisData data) { + CNG cached = heightGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + + return heightGenerator.aquire(() -> + heightVariance.create(rng.nextParallelRNG(getBlockData(data).size() + stackMax + stackMin), data)); + } + + public CNG getGenerator(RNG rng, IrisData data) { + CNG cached = layerGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + + return layerGenerator.aquire(() -> style.create(rng.nextParallelRNG(getBlockData(data).size()), data)); + } + + public CNG getVarianceGenerator(RNG rng, IrisData data) { + CNG cached = varianceGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + + return varianceGenerator.aquire(() -> + variance.create( + rng.nextParallelRNG(getBlockData(data).size()), data) + .scale(1D / variance.getZoom())); + } + + public KList add(String b) { + palette.add(new IrisBlockData(b)); + return palette; + } + + public boolean passesChanceGate(RNG rng, double x, double z, IrisData data) { + if (getBlockData(data).isEmpty()) { + return false; + } + double xx = x / style.getZoom(); + double zz = z / style.getZoom(); + return getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance; + } + + public PlatformBlockState getBlockData(IrisBiome b, RNG rng, double x, double z, IrisData data) { + if (!passesChanceGate(rng, x, z, data)) { + return null; + } + if (getBlockData(data).size() == 1) { + return getBlockData(data).get(0); + } + return getVarianceGenerator(rng, data).fit(getBlockData(data), z, x); + } + + public PlatformBlockState getBlockData100(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) { + if (getBlockData(data).isEmpty()) { + IrisLogging.warnOnce("decorator-empty:" + b.getName(), "Empty Block Data for " + b.getName()); + return null; + } + + if (getBlockData(data).size() == 1) { + return getBlockData(data).get(0); + } + + return getVarianceGenerator(rng, data).fit(getBlockData(data), z, y, x); //X and Z must be switched + } + + public PlatformBlockState getBlockDataForTop(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) { + if (getBlockDataTops(data).isEmpty()) { + return getBlockData100(b, rng, x, y, z, data); + } + + double xx = x / style.getZoom(); + double zz = z / style.getZoom(); + + if (getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance) { //Exclude y from here + if (getBlockData(data).size() == 1) { + return getBlockDataTops(data).get(0); + } + + return getVarianceGenerator(rng, data).fit(getBlockDataTops(data), z, y, x); //X and Z must be switched + } + + return null; + } + + public KList getBlockData(IrisData data) { + KList cached = blockData.getIfPresent(); + + if (cached != null) { + return cached; + } + + return blockData.aquire(() -> + { + KList blockData = new KList<>(); + for (IrisBlockData i : palette) { + PlatformBlockState bx = i.getBlockData(data); + if (bx != null) { + for (int n = 0; n < i.getWeight(); n++) { + blockData.add(bx); + } + } + } + + return blockData; + }); + } + + public KList getBlockDataTops(IrisData data) { + KList cached = blockDataTops.getIfPresent(); + + if (cached != null) { + return cached; + } + + return blockDataTops.aquire(() -> + { + KList blockDataTops = new KList<>(); + for (IrisBlockData i : topPalette) { + PlatformBlockState bx = i.getBlockData(data); + if (bx != null) { + for (int n = 0; n < i.getWeight(); n++) { + blockDataTops.add(bx); + } + } + } + + return blockDataTops; + }); + } + + public PlatformBlockState[] getBlockDataArray(IrisData data) { + PlatformBlockState[] cached = blockDataArray.getIfPresent(); + + if (cached != null) { + return cached; + } + + return blockDataArray.aquire(() -> { + KList list = getBlockData(data); + return list.toArray(new PlatformBlockState[0]); + }); + } + + public PlatformBlockState[] getBlockDataTopsArray(IrisData data) { + PlatformBlockState[] cached = blockDataTopsArray.getIfPresent(); + + if (cached != null) { + return cached; + } + + return blockDataTopsArray.aquire(() -> { + KList list = getBlockDataTops(data); + return list.toArray(new PlatformBlockState[0]); + }); + } + + /** + * The resolved whitelist palette. Empty when no whitelist is configured; callers must still gate on + * {@link #getWhitelist()} being non null, because an explicitly empty whitelist blocks all placement. + */ + public PlatformBlockState[] getWhitelistArray(IrisData data) { + PlatformBlockState[] cached = whitelistArray.getIfPresent(); + + if (cached != null) { + return cached; + } + + return whitelistArray.aquire(() -> resolvePalette(whitelist, data)); + } + + public PlatformBlockState[] getBlacklistArray(IrisData data) { + PlatformBlockState[] cached = blacklistArray.getIfPresent(); + + if (cached != null) { + return cached; + } + + return blacklistArray.aquire(() -> resolvePalette(blacklist, data)); + } + + private static PlatformBlockState[] resolvePalette(KList list, IrisData data) { + if (list == null) { + return new PlatformBlockState[0]; + } + + PlatformBlockState[] resolved = new PlatformBlockState[list.size()]; + + for (int i = 0; i < resolved.length; i++) { + resolved[i] = list.get(i).getBlockData(data); + } + + return resolved; + } + + public PlatformBlockState pickBlockData(RNG rng, IrisData data, double x, double z) { + PlatformBlockState[] arr = getBlockDataArray(data); + if (arr.length == 0) { + return null; + } + if (arr.length == 1) { + return arr[0]; + } + return arr[Math.abs((int) getVarianceGenerator(rng, data).fit(0, arr.length - 1, z, x))]; + } + + public PlatformBlockState pickBlockDataTop(RNG rng, IrisData data, double x, double z) { + PlatformBlockState[] arr = getBlockDataTopsArray(data); + if (arr.length == 0) { + return pickBlockData(rng, data, x, z); + } + if (arr.length == 1) { + return arr[0]; + } + return arr[Math.abs((int) getVarianceGenerator(rng, data).fit(0, arr.length - 1, z, x))]; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isStacking() { + return getStackMax() > 1; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java new file mode 100644 index 000000000..e68daae49 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java @@ -0,0 +1,11 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Selects which biome identity is checked by a deposit's include and exclude filters.") +public enum IrisDepositBiomeScope { + @Desc("Checks the surface biome selected for the deposit column.") + SURFACE, + @Desc("Checks the cave biome selected at the deposit origin.") + CAVE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java new file mode 100644 index 000000000..cc9f49615 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java @@ -0,0 +1,460 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.volmlib.util.math.RNG; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("deposit") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Creates ore & other block deposits underground") +@Data +public class IrisDepositGenerator { + private final transient ConcurrentMap> objects = new ConcurrentHashMap<>(); + private final transient AtomicCache> blockData = new AtomicCache<>(); + private final transient AtomicCache ore = new AtomicCache<>(); + private final transient AtomicCache> replaceableBlockData = new AtomicCache<>(); + private final transient ConcurrentMap> scaledObjects = new ConcurrentHashMap<>(); + @Required + @MinNumber(-8192) + @MaxNumber(8192) + @Desc("The inclusive minimum origin height in engine-local Y, where 0 is the bottom of the dimension. Negative values let an unclipped vanilla distribution taper into the world floor.") + private int minHeight = 1; + @Required + @MinNumber(-8192) + @MaxNumber(8192) + @Desc("The inclusive maximum origin height in engine-local Y, where 0 is the bottom of the dimension.") + private int maxHeight = 75; + @Desc("How origin heights are sampled. CLIPPED_UNIFORM preserves Iris behavior by clipping the band to terrain; UNIFORM and TRIANGLE sample the authored band first and discard cells outside terrain or build height.") + private IrisDepositHeightDistribution heightDistribution = IrisDepositHeightDistribution.CLIPPED_UNIFORM; + @Desc("TERRAIN places below the generated surface. ABOVE_TERRAIN places only in existing solid hosts above it. FULL_HEIGHT permits existing solid hosts anywhere in the dimension.") + private IrisDepositPlacementScope placementScope = IrisDepositPlacementScope.TERRAIN; + @MinNumber(0) + @MaxNumber(256) + @Desc("Solid blocks kept between a deposit and the terrain surface. Vanilla-like deposits use 0; the Iris default remains 7.") + private int surfaceClearance = 7; + @Required + @MinNumber(0) + @MaxNumber(8192) + @Desc("The minimum Iris block count or vanilla configured vein size, selected according to shape.") + private int minSize = 0; + @Required + @MinNumber(0) + @MaxNumber(8192) + @Desc("The maximum Iris block count or vanilla configured vein size, selected according to shape.") + private int maxSize = 128; + @Desc("IRIS places a fixed-count cube clump. VANILLA_ELLIPSOID uses Minecraft's chained ellipsoid geometry. VANILLA_SCATTERED uses Minecraft's sparse candidate offsets.") + private IrisDepositShape shape = IrisDepositShape.IRIS; + @Required + @MinNumber(0) + @MaxNumber(2048) + @Desc("The maximum amount of clumps per chunk") + private int maxPerChunk = 3; + @Required + @MinNumber(0) + @MaxNumber(2048) + @Desc("The minimum amount of clumps per chunk") + private int minPerChunk = 0; + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance of the deposit spawning in a chunk") + private double spawnChance = 1; + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance of each individual clump spawning in a chunk") + private double perClumpSpawnChance = 1; + @MinNumber(0) + @MaxNumber(1) + @Desc("Chance to discard a candidate ore block when it touches air. This matches vanilla ore exposure reduction.") + private double discardChanceOnAirExposure = 0; + @Required + @ArrayType(min = 1, type = IrisBlockData.class) + @Desc("The palette of blocks to be used in this deposit generator. Each entry is picked uniformly; the per-entry weight field is ignored here.") + private KList palette = new KList<>(); + @MinNumber(1) + @MaxNumber(64) + @Desc("Ore varience is how many different objects clumps iris will create") + private int varience = 3; + @ArrayType(min = 1, type = String.class) + @Desc("Optional block ids this deposit may replace. An empty list retains Iris's any-solid behavior.") + private KList replaceableBlocks = new KList<>(); + @Desc("Chooses whether includedBiomes and excludedBiomes inspect the surface biome or the cave biome at the deposit origin.") + private IrisDepositBiomeScope biomeScope = IrisDepositBiomeScope.CAVE; + @ArrayType(min = 1, type = String.class) + @Desc("Optional Iris biome keys or vanilla derivative ids allowed for this deposit.") + private KList includedBiomes = new KList<>(); + @ArrayType(min = 1, type = String.class) + @Desc("Optional Iris biome keys or vanilla derivative ids denied for this deposit.") + private KList excludedBiomes = new KList<>(); + @Desc("If set to true, this deposit will replace bedrock") + private boolean replaceBedrock = false; + + public IrisObject getClump(Engine engine, RNG rng, IrisData rdata) { + if (shape != IrisDepositShape.IRIS) { + return generateConfiguredClumpObject(rng, rdata, minSize, maxSize); + } + + ClumpCacheKey cacheKey = new ClumpCacheKey(engine.getSeedManager().getDeposit(), minSize, maxSize); + KList objects = this.objects.computeIfAbsent(cacheKey, key -> { + RNG rngv = new RNG(key.depositSeed() + hashCode()); + KList objectsf = new KList<>(); + + for (int i = 0; i < varience; i++) { + objectsf.add(generateClumpObject( + rngv.nextParallelRNG(2349 * i + 3598), rdata, key.minSize(), key.maxSize())); + } + + return objectsf; + }); + return objects.get(rng.i(0, objects.size())); + } + + public IrisObject getClump(Engine engine, RNG rng, IrisData rdata, double sizeMultiplier) { + if (sizeMultiplier == 1D) { + return getClump(engine, rng, rdata); + } + + int scaledMinSize = scaledDepositSize(minSize, sizeMultiplier); + int scaledMaxSize = scaledDepositSize(maxSize, sizeMultiplier); + if (shape != IrisDepositShape.IRIS) { + return generateConfiguredClumpObject(rng, rdata, scaledMinSize, scaledMaxSize); + } + + ClumpCacheKey cacheKey = new ClumpCacheKey( + engine.getSeedManager().getDeposit(), scaledMinSize, scaledMaxSize); + KList objects = scaledObjects.computeIfAbsent(cacheKey, key -> { + long sizeSeed = ((long) key.minSize() << 32) ^ (key.maxSize() & 0xffffffffL); + RNG rngv = new RNG(key.depositSeed() + hashCode() + sizeSeed); + KList generated = new KList<>(); + + for (int i = 0; i < varience; i++) { + generated.add(generateClumpObject( + rngv.nextParallelRNG(2349 * i + 3598), rdata, key.minSize(), key.maxSize())); + } + + return generated; + }); + return objects.get(rng.i(0, objects.size())); + } + + public int getMaxDimension() { + return Math.min(11, (int) Math.ceil(Math.cbrt(maxSize))); + } + + static int scaledDepositSize(int size, double multiplier) { + return Math.max(0, Math.min(8192, (int) Math.round(size * multiplier))); + } + + private IrisObject generateConfiguredClumpObject(RNG rng, IrisData rdata, int clumpMinSize, int clumpMaxSize) { + int size = rng.i(clumpMinSize, clumpMaxSize + 1); + return switch (shape) { + case VANILLA_ELLIPSOID -> generateVanillaEllipsoid(rng, rdata, size); + case VANILLA_SCATTERED -> generateVanillaScattered(rng, rdata, size); + case IRIS -> generateClumpObject(rng, rdata, clumpMinSize, clumpMaxSize); + }; + } + + IrisObject generateVanillaEllipsoid(RNG rng, IrisData rdata, int size) { + if (size <= 0) { + return new IrisObject(1, 1, 1); + } + + float angle = rng.nextFloat() * (float) Math.PI; + float reach = size / 8F; + double startX = Math.sin(angle) * reach; + double endX = -Math.sin(angle) * reach; + double startZ = Math.cos(angle) * reach; + double endZ = -Math.cos(angle) * reach; + double startY = rng.nextInt(3) - 2; + double endY = rng.nextInt(3) - 2; + double[] nodes = new double[size * 4]; + + for (int i = 0; i < size; i++) { + float progress = (float) i / size; + double radiusNoise = rng.nextDouble() * size / 16D; + nodes[i * 4] = startX + (endX - startX) * progress; + nodes[i * 4 + 1] = startY + (endY - startY) * progress; + nodes[i * 4 + 2] = startZ + (endZ - startZ) * progress; + nodes[i * 4 + 3] = ((Math.sin(Math.PI * progress) + 1D) * radiusNoise + 1D) / 2D; + } + + for (int i = 0; i < size - 1; i++) { + if (nodes[i * 4 + 3] <= 0D) { + continue; + } + for (int j = i + 1; j < size; j++) { + if (nodes[j * 4 + 3] <= 0D) { + continue; + } + double dx = nodes[i * 4] - nodes[j * 4]; + double dy = nodes[i * 4 + 1] - nodes[j * 4 + 1]; + double dz = nodes[i * 4 + 2] - nodes[j * 4 + 2]; + double dr = nodes[i * 4 + 3] - nodes[j * 4 + 3]; + if (dr * dr <= dx * dx + dy * dy + dz * dz) { + continue; + } + if (dr > 0D) { + nodes[j * 4 + 3] = -1D; + } else { + nodes[i * 4 + 3] = -1D; + } + } + } + + KSet cells = new KSet<>(); + for (int i = 0; i < size; i++) { + double radius = nodes[i * 4 + 3]; + if (radius < 0D) { + continue; + } + double centerX = nodes[i * 4]; + double centerY = nodes[i * 4 + 1]; + double centerZ = nodes[i * 4 + 2]; + int minX = (int) Math.floor(centerX - radius); + int maxX = Math.max((int) Math.floor(centerX + radius), minX); + int minY = (int) Math.floor(centerY - radius); + int maxY = Math.max((int) Math.floor(centerY + radius), minY); + int minZ = (int) Math.floor(centerZ - radius); + int maxZ = Math.max((int) Math.floor(centerZ + radius), minZ); + + for (int x = minX; x <= maxX; x++) { + double nx = (x + 0.5D - centerX) / radius; + if (nx * nx >= 1D) { + continue; + } + for (int y = minY; y <= maxY; y++) { + double ny = (y + 0.5D - centerY) / radius; + if (nx * nx + ny * ny >= 1D) { + continue; + } + for (int z = minZ; z <= maxZ; z++) { + double nz = (z + 0.5D - centerZ) / radius; + if (nx * nx + ny * ny + nz * nz < 1D) { + cells.add(new BlockPosition(x, y, z)); + } + } + } + } + } + + return objectFromCells(cells, rng, rdata); + } + + IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) { + KSet cells = new KSet<>(); + int candidates = rng.nextInt(Math.max(0, size) + 1); + for (int i = 0; i < candidates; i++) { + int magnitude = Math.min(i, 7); + cells.add(new BlockPosition( + Math.round((rng.nextFloat() - rng.nextFloat()) * magnitude), + Math.round((rng.nextFloat() - rng.nextFloat()) * magnitude), + Math.round((rng.nextFloat() - rng.nextFloat()) * magnitude))); + } + return objectFromCells(cells, rng, rdata); + } + + private IrisObject objectFromCells(KSet cells, RNG rng, IrisData rdata) { + if (cells.isEmpty()) { + return new IrisObject(1, 1, 1); + } + + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (BlockPosition cell : cells) { + minX = Math.min(minX, cell.getX()); + minY = Math.min(minY, cell.getY()); + minZ = Math.min(minZ, cell.getZ()); + maxX = Math.max(maxX, cell.getX()); + maxY = Math.max(maxY, cell.getY()); + maxZ = Math.max(maxZ, cell.getZ()); + } + + int extentX = Math.max(Math.abs(minX), Math.abs(maxX)); + int extentY = Math.max(Math.abs(minY), Math.abs(maxY)); + int extentZ = Math.max(Math.abs(minZ), Math.abs(maxZ)); + IrisObject object = new IrisObject(extentX * 2 + 1, extentY * 2 + 1, extentZ * 2 + 1); + for (BlockPosition cell : cells) { + object.setUnsigned( + cell.getX() + extentX, + cell.getY() + extentY, + cell.getZ() + extentZ, + nextBlock(rng, rdata)); + } + return object; + } + + private IrisObject generateClumpObject(RNG rngv, IrisData rdata, int clumpMinSize, int clumpMaxSize) { + int s = rngv.i(clumpMinSize, clumpMaxSize + 1); + if (s == 1) { + IrisObject o = new IrisObject(1, 1, 1); + Vector3i center = o.getCenter(); + o.getBlocks().put(new IrisBlockVector(center.getX(), center.getY(), center.getZ()), nextBlock(rngv, rdata)); + return o; + } + + int dim = Math.min(11, (int) Math.ceil(Math.cbrt(s))); + IrisObject o = new IrisObject(dim, dim, dim); + + int volume = dim * dim * dim; + if (s >= volume) { + int x = 0, y = 0, z = 0; + + while (z < dim) { + o.setUnsigned(x++, y, z, nextBlock(rngv, rdata)); + + if (x == dim) { + x = 0; + y++; + } + + if (y == dim) { + y = 0; + z++; + } + } + return o; + } + + KSet set = new KSet<>(); + while (s > 0) { + BlockPosition ang = new BlockPosition( + rngv.i(0, dim), + rngv.i(0, dim), + rngv.i(0, dim) + ); + if (!set.add(ang)) continue; + + s--; + o.setUnsigned(ang.getX(), ang.getY(), ang.getZ(), nextBlock(rngv, rdata)); + } + + return o; + } + + private PlatformBlockState nextBlock(RNG rngv, IrisData rdata) { + return getBlockData(rdata).get(rngv.i(0, getBlockData(rdata).size())); + } + + public KList getBlockData(IrisData rdata) { + return blockData.aquire(() -> + { + KList blockData = new KList<>(); + + for (IrisBlockData ix : palette) { + PlatformBlockState bx = ix.getBlockData(rdata); + + if (bx != null) { + blockData.add(bx); + } + } + + return blockData; + }); + } + + public boolean isOre(IrisData rdata) { + return ore.aquire(() -> { + for (PlatformBlockState block : getBlockData(rdata)) { + if (block.isOre()) { + return true; + } + } + + return false; + }); + } + + public boolean canReplace(PlatformBlockState state) { + if (replaceableBlocks == null || replaceableBlocks.isEmpty()) { + return true; + } + return replaceableBlockData.aquire(this::resolveReplaceableBlocks) + .contains(IrisProceduralBlocks.materialKey(state)); + } + + public boolean matchesBiome(IrisBiome surfaceBiome, IrisBiome caveBiome) { + IrisBiome selected = biomeScope == IrisDepositBiomeScope.SURFACE ? surfaceBiome : caveBiome; + if (includedBiomes != null && !includedBiomes.isEmpty() && !matchesAnyBiome(selected, includedBiomes)) { + return false; + } + return excludedBiomes == null || excludedBiomes.isEmpty() || !matchesAnyBiome(selected, excludedBiomes); + } + + public boolean usesCaveBiomeFilter() { + return biomeScope == IrisDepositBiomeScope.CAVE + && ((includedBiomes != null && !includedBiomes.isEmpty()) + || (excludedBiomes != null && !excludedBiomes.isEmpty())); + } + + private KSet resolveReplaceableBlocks() { + KSet resolved = new KSet<>(); + for (String key : replaceableBlocks) { + PlatformBlockState state = B.getStateOrNull(key, false); + if (state != null) { + resolved.add(IrisProceduralBlocks.materialKey(state)); + } + } + return resolved; + } + + private boolean matchesAnyBiome(IrisBiome biome, KList configuredBiomes) { + if (biome == null) { + return false; + } + for (String configured : configuredBiomes) { + String namespaced = configured.contains(":") ? configured : "minecraft:" + configured; + if (configured.equals(biome.getLoadKey()) + || namespaced.equals(biome.getDerivativeKey()) + || namespaced.equals(biome.getVanillaDerivativeKey())) { + return true; + } + } + return false; + } + + record ClumpCacheKey(long depositSeed, int minSize, int maxSize) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java new file mode 100644 index 000000000..7e3400ae4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java @@ -0,0 +1,13 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls how a deposit chooses its origin height within the configured vertical band.") +public enum IrisDepositHeightDistribution { + @Desc("Samples uniformly after clipping the configured band to the terrain and build-height bounds.") + CLIPPED_UNIFORM, + @Desc("Samples uniformly from the configured band, then rejects origins outside the terrain or build-height bounds.") + UNIFORM, + @Desc("Samples toward the middle of the configured band with a triangular distribution, then rejects invalid origins.") + TRIANGLE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java new file mode 100644 index 000000000..e14c49ba6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java @@ -0,0 +1,13 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls which vertical part of the world may contain a deposit origin.") +public enum IrisDepositPlacementScope { + @Desc("Places in solid terrain below the generated surface while preserving the configured surface clearance.") + TERRAIN, + @Desc("Places only in existing solid hosts above the generated terrain surface.") + ABOVE_TERRAIN, + @Desc("Places in existing solid hosts anywhere within the dimension build height.") + FULL_HEIGHT +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java new file mode 100644 index 000000000..4073eb5fc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java @@ -0,0 +1,13 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Selects the geometry used to build each deposit clump.") +public enum IrisDepositShape { + @Desc("Builds the traditional Iris fixed-block-count cube clump.") + IRIS, + @Desc("Builds a chain of overlapping ellipsoids matching Minecraft's ordinary ore-vein geometry.") + VANILLA_ELLIPSOID, + @Desc("Builds sparse candidate offsets matching Minecraft's scattered-ore geometry.") + VANILLA_SCATTERED +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositVariant.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositVariant.java new file mode 100644 index 000000000..5facf11c2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositVariant.java @@ -0,0 +1,86 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KMap; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import lombok.experimental.Accessors; + +@Snippet("deposit-variant") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Remaps ore block ids to alternate block ids within a vertical band. Ores declared at dimension, region, and biome scope can be rewritten at placement time (for example, iron_ore -> deepslate_iron_ore inside a deep carving band, or yourmod:iron -> yourmod:moon_iron inside a lunar biome).") +@Data +public class IrisDepositVariant { + private final transient AtomicCache> resolved = new AtomicCache<>(); + + @Required + @MinNumber(-2048) + @MaxNumber(8192) + @Desc("Inclusive minimum world Y this variant applies at.") + private int minHeight = 0; + + @Required + @MinNumber(-2048) + @MaxNumber(8192) + @Desc("Inclusive maximum world Y this variant applies at.") + private int maxHeight = 0; + + @Required + @Desc("Source block id (for example `minecraft:iron_ore`) -> replacement block id (for example `minecraft:deepslate_iron_ore`). Any block id the data loader resolves is accepted, including external/mod blocks. Source match is by material only, so block properties on the source key are ignored.") + private KMap remap = new KMap<>(); + + public PlatformBlockState remapOrNull(PlatformBlockState ore, IrisData rdata) { + if (ore == null || remap == null || remap.isEmpty()) { + return null; + } + + KMap map = resolved.aquire(() -> buildResolved(rdata)); + return map.get(IrisProceduralBlocks.materialKey(ore)); + } + + private KMap buildResolved(IrisData rdata) { + KMap out = new KMap<>(); + + for (java.util.Map.Entry entry : remap.entrySet()) { + PlatformBlockState source = B.getStateOrNull(entry.getKey(), false); + PlatformBlockState target = B.getStateOrNull(entry.getValue(), true); + + if (source == null || target == null) { + continue; + } + + out.put(IrisProceduralBlocks.materialKey(source), target); + } + + return out; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java new file mode 100644 index 000000000..f5147e22b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -0,0 +1,878 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.IrisDatapackCompiler.DimensionHeight; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.core.nms.datapack.IDataFixer; +import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListFunction; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.functions.ComponentFlagFunction; +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.data.DataProvider; +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.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; + +@Accessors(chain = true) +@NoArgsConstructor +@Desc("Represents a dimension") +@Data +@EqualsAndHashCode(callSuper = false, doNotUseGetters = true) +@ToString(doNotUseGetters = true) +public class IrisDimension extends IrisRegistrant { + private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); + + private final transient AtomicCache parallaxSize = new AtomicCache<>(); + private final transient AtomicCache rockLayerGenerator = new AtomicCache<>(); + private final transient AtomicCache fluidLayerGenerator = new AtomicCache<>(); + private final transient AtomicCache coordFracture = new AtomicCache<>(); + private final transient AtomicCache sinr = new AtomicCache<>(); + private final transient AtomicCache cosr = new AtomicCache<>(); + private final transient AtomicCache rad = new AtomicCache<>(); + private final transient AtomicCache featuresUsed = new AtomicCache<>(); + private final transient AtomicCache> carvingEntryIndex = new AtomicCache<>(); + private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); + private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); + private final transient AtomicCache surfaceOreBoundsCache = new AtomicCache<>(); + private final transient AtomicCache undergroundOreBoundsCache = new AtomicCache<>(); + @MinNumber(2) + @Required + @Desc("The human readable name of this dimension") + private String name = "A Dimension"; + @MinNumber(1) + @MaxNumber(2032) + @Desc("Maximum height at which players can be teleported to through gameplay.") + private int logicalHeight = 256; + @Desc("If set to true, Iris will remove chunks to allow visualizing cross sections of chunks easily") + private boolean debugChunkCrossSections = false; + @Desc("Vertically split up the biome palettes with barrier blocks in between to visualize them. The gap size comes from explodeBiomePaletteSize.") + private boolean explodeBiomePalettes = false; + @Desc("Studio Mode for testing different parts of the world") + private StudioMode studioMode = StudioMode.NORMAL; + @MinNumber(1) + @MaxNumber(16) + @Desc("Customize the palette height explosion") + private int explodeBiomePaletteSize = 3; + @MinNumber(2) + @MaxNumber(16) + @Desc("Every X/Z % debugCrossSectionsMod == 0 cuts the chunk") + private int debugCrossSectionsMod = 3; + @Desc("Tree growth override settings") + private IrisTreeSettings treeSettings = new IrisTreeSettings(); + @Desc("Spawn Entities in this dimension over time. Iris will continually replenish these mobs just like vanilla does.") + @ArrayType(min = 1, type = String.class) + @RegistryListResource(IrisSpawner.class) + private KList entitySpawners = new KList<>(); + @Desc("Reference loot tables in this area") + private IrisLootReference loot = new IrisLootReference(); + @MinNumber(0) + @Desc("The version of this dimension. Changing this will stop users from accidentally upgrading (and breaking their worlds).") + private int version = 1; + @ArrayType(min = 1, type = IrisBlockDrops.class) + @Desc("Define custom block drops for this dimension") + private KList blockDrops = new KList<>(); + @Desc("Should bedrock be generated or not.") + private boolean bedrock = true; + @MinNumber(0) + @MaxNumber(1) + @Desc("The land chance. Up to 1.0 for total land or 0.0 for total sea") + private double landChance = 0.625; + @Desc("The placement style of regions") + private IrisGeneratorStyle regionStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + @Desc("The placement style of land/sea") + private IrisGeneratorStyle continentalStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + @Desc("The noise style used to select land biomes within a region") + private IrisGeneratorStyle landBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + @Desc("The noise style used to select shore biomes within a region") + private IrisGeneratorStyle shoreBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + @Desc("The noise style used to select sea biomes within a region") + private IrisGeneratorStyle seaBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + @Desc("The noise style used to select cave biomes within a region") + private IrisGeneratorStyle caveBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + @Desc("Instead of filling objects with air, fills them with cobweb so you can see them") + private boolean debugSmartBore = false; + @Desc("Generate decorations or not") + private boolean decorate = true; + @Desc("When true, every ore block the generator would place (terrain ores, deposits, ores inside objects) is replaced with its base material such as stone, deepslate or netherrack, so drop-control plugins like HiddenOre can manage ore rewards themselves") + private boolean hideOresForHiddenOre = false; + @Desc("Use post processing or not") + private boolean postProcessing = true; + @Desc("Add slabs in post processing") + private boolean postProcessingSlabs = true; + @Desc("Add painted walls in post processing") + private boolean postProcessingWalls = true; + @Desc("Enable or disable all carving for this dimension") + private boolean carvingEnabled = true; + @ArrayType(type = IrisDimensionCarvingEntry.class, min = 1) + @Desc("Dimension-level cave biome carving overrides with absolute world Y ranges") + private KList carving = new KList<>(); + @Desc("Profile-driven 3D cave configuration") + private IrisCaveProfile caveProfile = new IrisCaveProfile(); + @Desc("Refuse to place surface objects and trees over carved surface openings.") + private boolean requireObjectSurfaceSupport = true; + @MinNumber(0) + @MaxNumber(16) + @Desc("Minimum surface-support buffer, in blocks, applied to every surface object placement in this dimension. A placement may ask for more but never less.") + private int objectSurfaceSupportBuffer = 2; + @Desc("The world environment") + private IrisEnvironment environment = IrisEnvironment.NORMAL; + @RegistryListResource(IrisRegion.class) + @Required + @ArrayType(min = 1, type = String.class) + @Desc("Define all of the regions to include in this dimension. Dimensions -> Regions -> Biomes -> Objects etc") + private KList regions = new KList<>(); + @Required + @MinNumber(0) + @MaxNumber(1024) + @Desc("The fluid (sea level) height for this dimension, in world Y. Water fills every sea column up to this height, so 63 puts the water surface at world Y 63.") + private int fluidHeight = 63; + @Desc("Define the min and max Y bounds of this dimension. Please keep in mind that Iris internally generates from 0 to (max - min). \n\nFor example at -64 to 320, Iris is internally generating to 0 to 384, then on outputting chunks, it shifts it down by the min height (64 blocks). The default is -64 to 320. \n\nfluidHeight stays in world Y: the engine converts it internally by subtracting the min height, so a fluid height of 63 shows up in the world at Y 63.") + private IrisRange dimensionHeight = new IrisRange(-64, 320); + @Desc("Define options for this dimension") + private IrisDimensionTypeOptions dimensionOptions = new IrisDimensionTypeOptions(); + @Desc("When true, sets the dimension's ambient light to maximum, making all blocks fully lit regardless of light level.") + private boolean fullbright = false; + @RegistryListResource(IrisDimension.class) + @Desc("When set, generates the referenced dimension's terrain upside-down at the world ceiling, creating a nether-like canopy. Use the dimension's load key (e.g., 'overworld'). Set to 'none' to disable.") + private String upperDimension = "none"; + @MinNumber(0) + @MaxNumber(256) + @Desc("Minimum air gap in blocks between the lower terrain surface and the upper terrain surface.") + private int upperDimensionGap = 32; + @Desc("When true, cave carving will cut through the upper dimension terrain. When false, the upper terrain is a solid untouched mass.") + private boolean upperDimensionCarving = false; + @Desc("When true, objects from the mantle (structures, trees, etc.) can be placed in the upper dimension terrain zone. When false, the upper terrain is protected from object placement.") + private boolean upperDimensionObjects = false; + @Desc("When true, upper-dimension objects force-place regardless of placement restrictions (slope, underwater, clamp, collisions, carving). Normal dimension objects always place first; upper objects place second and may clip or occlude lower-dimension placements when this is enabled.") + private boolean upperObjectsForcePlace = false; + @RegistryListResource(IrisBiome.class) + @Desc("Keep this either undefined or empty. Setting any biome name into this will force iris to only generate the specified biome. Great for testing.") + private String focus = ""; + @RegistryListResource(IrisRegion.class) + @Desc("Keep this either undefined or empty. Setting any region name into this will force iris to only generate the specified region. Great for testing.") + private String focusRegion = ""; + @MinNumber(0.0001) + @MaxNumber(512) + @Desc("Zoom in or out the biome size. Higher = bigger biomes") + private double biomeZoom = 1D; + @MinNumber(0) + @MaxNumber(360) + @Desc("You can rotate the input coordinates by an angle. This can make terrain appear more natural (less sharp corners and lines). This literally rotates the entire dimension by an angle. Hint: Try 12 degrees or something not on a 90 or 45 degree angle.") + private double dimensionAngleDeg = 0; + @Required + @Desc("Define the mode of this dimension (required!)") + private IrisDimensionMode mode = new IrisDimensionMode(); + @MinNumber(0) + @MaxNumber(8192) + @Desc("Coordinate fracturing applies noise to the input coordinates. This creates the 'iris swirls' and wavy features. The distance pushes these waves further into places they shouldnt be. This is a block value multiplier.") + private double coordFractureDistance = 20; + @MinNumber(0.0001) + @MaxNumber(512) + @Desc("Coordinate fracturing zoom. Higher = less frequent warping, Lower = more frequent and rapid warping / swirls.") + private double coordFractureZoom = 8; + @MinNumber(0.0001) + @MaxNumber(512) + @Desc("This zooms in the land space") + private double landZoom = 1; + @MinNumber(0.0001) + @MaxNumber(512) + @Desc("This zooms oceanic biomes") + private double seaZoom = 1; + @MinNumber(0.0001) + @MaxNumber(512) + @Desc("Zoom in continents") + private double continentZoom = 1; + @MinNumber(0.0001) + @MaxNumber(512) + @Desc("Change the size of regions") + private double regionZoom = 1; + @Desc("Disable this to stop placing objects, entities, features & updates") + private boolean useMantle = true; + @Desc("Prevent Leaf decay as if placed in creative mode") + private boolean preventLeafDecay = false; + @ArrayType(min = 1, type = IrisDepositGenerator.class) + @Desc("Define global deposit generators") + private KList deposits = new KList<>(); + @ArrayType(min = 1, type = IrisDepositVariant.class) + @Desc("Dimension-wide deposit ore remap rules. Each entry declares a vertical band and a source->replacement block id map. Applied after biome and region rules; first matching dimension rule wins.") + private KList depositVariants = new KList<>(); + @ArrayType(min = 1, type = IrisShapedGeneratorStyle.class) + @Desc("Overlay additional noise on top of the interoplated terrain.") + private KList overlayNoise = new KList<>(); + @Desc("The palette of blocks for 'stone'") + private IrisMaterialPalette rockPalette = new IrisMaterialPalette().qclear().qadd("stone"); + @Desc("The dimension fluid block palette used for ocean columns and cave aquifers.") + private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("water"); + @Desc("Collection of ores to be generated") + @ArrayType(type = IrisOreGenerator.class, min = 1) + private KList ores = new KList<>(); + @Desc("Dimension-wide Iris structure placements, independent of biome/region placements") + @ArrayType(type = IrisStructurePlacement.class, min = 1) + private KList structures = new KList<>(); + @Desc("Controls native vanilla, mod, and ingested datapack structure generation for this dimension. Every registered structure is enabled by default; 'disabled' is the sole generation deny list and autocompletes live structure keys.") + private IrisImportedStructureControl importedStructures = new IrisImportedStructureControl(); + @Desc("Controls native vanilla, mod, and ingested datapack PLACED FEATURE generation (ores, trees, plants, springs, geodes) for this dimension. Disabled by default: leaving this out generates exactly the terrain Iris always has. Set 'enabled' true to run the vanilla decoration feature pass over Iris terrain. Carvers are never imported.") + private IrisImportedFeatureControl importedFeatures = new IrisImportedFeatureControl(); + @ArrayType(type = String.class, min = 1) + @Desc("External datapack sources owned by this dimension. Bukkit registers their resources server-wide, but Iris excludes their structure sets and definitions from generation and locate state in vanilla worlds and Iris dimensions that do not declare the same source. Editable auto-import is limited to resources supplied by the URLs declared by each Iris pack. Any registered datapack structure can be placed directly through nativeStructures without conversion. Replacing native generation requires a dimension-level structure placement with nativeSuppression set to REPLACE_SOURCE; provenance alone never disables native structures.") + private KList datapackImports = new KList<>(); + @MinNumber(0) + @MaxNumber(318) + @Desc("The subterranean fluid (cave lava) layer height, in engine-local Y where 0 is the bottom of the dimension, not world Y. With the default -64..320 height, 8 means world Y -56.") + private int caveLavaHeight = 8; + @RegistryListFunction(ComponentFlagFunction.class) + @ArrayType(type = String.class) + @Desc("Collection of disabled components") + private KList disabledComponents = new KList<>(); + + public int getMaxHeight() { + return (int) getDimensionHeight().getMax(); + } + + public int getMinHeight() { + return (int) getDimensionHeight().getMin(); + } + + public Map getCarvingEntryIndex() { + return carvingEntryIndex.aquire(() -> { + Map index = new HashMap<>(); + KList entries = getCarving(); + if (entries == null || entries.isEmpty()) { + return index; + } + + for (IrisDimensionCarvingEntry entry : entries) { + if (entry == null) { + continue; + } + + String entryId = entry.getId(); + if (entryId == null || entryId.isBlank()) { + continue; + } + + index.put(entryId.trim(), entry); + } + + return index; + }); + } + + public void setCarving(KList carving) { + this.carving = carving == null ? new KList<>() : carving; + carvingEntryIndex.reset(); + } + + public PlatformBlockState generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) { + KList localOres = surface ? getSurfaceOres() : getUndergroundOres(); + return generateOres(localOres, x, y, z, rng, data); + } + + public PlatformBlockState generateSurfaceOres(int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getSurfaceOres(), x, y, z, rng, data); + } + + public PlatformBlockState generateUndergroundOres(int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getUndergroundOres(), x, y, z, rng, data); + } + + public boolean hasSurfaceOres() { + return !getSurfaceOres().isEmpty(); + } + + public boolean hasUndergroundOres() { + return !getUndergroundOres().isEmpty(); + } + + private PlatformBlockState generateOres(KList localOres, int x, int y, int z, RNG rng, IrisData data) { + if (localOres.isEmpty()) { + return null; + } + + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + PlatformBlockState ore = oreGenerator.generate(x, y, z, rng, data); + if (ore != null) { + return ore; + } + } + return null; + } + + public void setOres(KList ores) { + this.ores = ores == null ? new KList<>() : ores; + surfaceOreCache.reset(); + undergroundOreCache.reset(); + surfaceOreBoundsCache.reset(); + undergroundOreBoundsCache.reset(); + } + + public KList getSurfaceOreGenerators() { + return getOres(true); + } + + public KList getUndergroundOreGenerators() { + return getOres(false); + } + + public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() { + return surfaceOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres())); + } + + public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() { + return undergroundOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres())); + } + + private KList getSurfaceOres() { + return getOres(true); + } + + private KList getUndergroundOres() { + return getOres(false); + } + + private KList getOres(boolean surface) { + AtomicCache> oreCache = surface ? surfaceOreCache : undergroundOreCache; + return oreCache.aquire(() -> { + KList filtered = new KList<>(); + KList localOres = ores; + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + if (oreGenerator.isGenerateSurface() == surface) { + filtered.add(oreGenerator); + } + } + + return filtered; + }); + } + + public int getFluidHeight() { + return fluidHeight - (int) dimensionHeight.getMin(); + } + + public CNG getCoordFracture(RNG rng, int signature) { + return coordFracture.aquire(() -> + { + CNG coordFracture = CNG.signature(rng.nextParallelRNG(signature)); + coordFracture.scale(0.012 / coordFractureZoom); + return coordFracture; + }); + } + + public double getDimensionAngle() { + return rad.aquire(() -> Math.toRadians(dimensionAngleDeg)); + } + + public boolean hasFocusRegion() { + return !focusRegion.equals(""); + } + + public String getFocusRegion() { + return focusRegion; + } + + public double sinRotate() { + return sinr.aquire(() -> Math.sin(getDimensionAngle())); + } + + public double cosRotate() { + return cosr.aquire(() -> Math.cos(getDimensionAngle())); + } + + public KList getAllRegions(DataProvider g) { + KList r = new KList<>(); + + if (g == null) { + return r; + } + IrisData data = g.getData(); + if (data == null || data.getRegionLoader() == null) { + return r; + } + + for (String i : getRegions()) { + r.add(data.getRegionLoader().load(i)); + } + + return r; + } + + public KList getAllAnyRegions() { + KList r = new KList<>(); + + for (String i : getRegions()) { + r.add(IrisData.loadAnyRegion(i, getLoader())); + } + + return r; + } + + public KList getAllBiomes(DataProvider g) { + if (g == null) { + return new KList<>(); + } + IrisData data = g.getData(); + if (data == null || data.getBiomeLoader() == null) { + return new KList<>(); + } + return data.getBiomeLoader().loadAll(data.getBiomeLoader().getPossibleKeys()); + } + + public KList getReachableBiomes(DataProvider g) { + KMap biomes = new KMap<>(); + if (g == null) { + return biomes.v(); + } + + IrisData data = g.getData(); + if (data == null || data.getRegionLoader() == null || data.getBiomeLoader() == null) { + return biomes.v(); + } + + Deque pending = new ArrayDeque<>(); + KList regionKeys = getRegions(); + if (regionKeys != null) { + for (String regionKey : regionKeys) { + IrisRegion region = data.getRegionLoader().load(regionKey); + if (region == null) { + continue; + } + addReachableBiomeKeys(pending, region.getAllBiomeIds()); + } + } + + KList carvingEntries = getCarving(); + if (carvingEntries != null) { + for (IrisDimensionCarvingEntry entry : carvingEntries) { + if (entry != null && entry.isEnabled()) { + addReachableBiomeKey(pending, entry.getBiome()); + } + } + } + + Set visited = new HashSet<>(); + Map carvingEntryIndex = getCarvingEntryIndex(); + while (!pending.isEmpty()) { + String biomeKey = pending.removeFirst(); + if (!visited.add(biomeKey)) { + continue; + } + + IrisBiome biome = data.getBiomeLoader().load(biomeKey); + if (biome == null) { + continue; + } + + String loadKey = biome.getLoadKey(); + if (loadKey == null || loadKey.isBlank() || biomes.containsKey(loadKey)) { + continue; + } + + visited.add(loadKey); + biomes.put(loadKey, biome); + addReachableBiomeKeys(pending, biome.getChildren()); + addReachableBiomeKey(pending, biome.getCarvingBiome()); + + KList floatingChildren = biome.getFloatingChildBiomes(); + if (floatingChildren == null) { + continue; + } + for (IrisFloatingChildBiomes floatingChild : floatingChildren) { + if (floatingChild != null) { + addReachableBiomeKey(pending, floatingChild.getBiome()); + String carvingReference = floatingChild.getCarving(); + IrisDimensionCarvingEntry carvingEntry = carvingEntryIndex.get(carvingReference); + addReachableBiomeKey(pending, + carvingEntry == null ? carvingReference : carvingEntry.getBiome()); + } + } + } + + return biomes.v(); + } + + private void addReachableBiomeKeys(Deque pending, Iterable biomeKeys) { + if (biomeKeys == null) { + return; + } + for (String biomeKey : biomeKeys) { + addReachableBiomeKey(pending, biomeKey); + } + } + + private void addReachableBiomeKey(Deque pending, String biomeKey) { + if (biomeKey != null && !biomeKey.isBlank()) { + pending.addLast(biomeKey); + } + } + + public KList getAllAnyBiomes() { + KList r = new KList<>(); + + for (IrisRegion i : getAllAnyRegions()) { + if (i == null) { + continue; + } + + r.addAll(i.getAllAnyBiomes()); + } + + return r; + } + + public IrisGeneratorStyle getBiomeStyle(InferredType type) { + switch (type) { + case CAVE: + return caveBiomeStyle; + case LAND: + return landBiomeStyle; + case SEA: + return seaBiomeStyle; + case SHORE: + return shoreBiomeStyle; + default: + break; + } + + return landBiomeStyle; + } + + public void installBiomes(IDataFixer fixer, DataProvider data, KList datapackRoots, KSet biomes) throws IOException { + String namespace = getLoadKey().toLowerCase(Locale.ROOT); + installBiomes(fixer, data, datapackRoots, namespace, "", biomes); + } + + public void installBiomes(IDataFixer fixer, DataProvider data, KList datapackRoots, + String namespace, String pathPrefix, KSet biomes) throws IOException { + // Tag membership is accumulated in memory and flushed once per tag at the end of the walk. Writing a + // tag file per (biome, tag) pair made staging quadratic: every write re-read, re-parsed and re-emitted + // a file that grows with every biome added to that tag. + KMap> tagMembership = new KMap<>(); + + for (IrisBiome irisBiome : getAllBiomes(data)) { + if (!irisBiome.isCustom()) { + continue; + } + + // Tag membership is inherited from the biome's vanilla derivative so that #minecraft:is_overworld + // style selectors (vanilla's own, and every mod that writes them) hit Iris custom biomes. The + // features and carvers arrays in the emitted biome JSON stay empty: native feature passthrough + // comes from the chunk generator's generation-settings getter, not from the datapack. + String derivativeKey = irisBiome.getVanillaDerivativeKey(); + + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + String customBiomeId = customBiome.getId(); + String json = customBiome.generateJson(fixer); + + synchronized (biomes) { + if (!biomes.add(customBiomeId)) { + IrisLogging.debug("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId); + continue; + } + } + + String biomePath = pathPrefix.isBlank() + ? customBiomeId + : pathPrefix + "/" + customBiomeId; + for (File datapackRoot : datapackRoots) { + File output = new File(datapackRoot, "data/" + namespace + "/worldgen/biome/" + biomePath + ".json"); + + IrisLogging.debug(" Installing Data Pack Biome: " + output.getPath()); + output.getParentFile().mkdirs(); + IO.writeAll(output, json); + } + collectBiomeTags(tagMembership, namespace + ":" + biomePath, + customBiome.getEffectiveTags(derivativeKey)); + } + } + + for (File datapackRoot : datapackRoots) { + installBiomeTags(datapackRoot, tagMembership); + } + } + + public static void clearGeneratedBiomeTags(KList datapackRoots) { + for (File datapackRoot : datapackRoots) { + File dataFolder = new File(datapackRoot, "data"); + File[] namespaces = dataFolder.listFiles(File::isDirectory); + if (namespaces == null) { + continue; + } + for (File namespace : namespaces) { + IO.delete(new File(namespace, "tags/worldgen/biome")); + } + } + } + + /** + * Accumulates one biome's tag membership into the tag-to-biomes map. Normalization and rejection happen + * here so a malformed author tag is reported once, against the biome that declared it. + */ + static void collectBiomeTags(KMap> tagMembership, String biomeKey, KList tags) { + if (tags == null || tags.isEmpty()) { + return; + } + for (String rawTag : tags) { + String tag = normalizeResourceKey(rawTag); + if (tag == null) { + IrisLogging.error("Invalid custom biome tag '" + rawTag + "' for " + biomeKey); + continue; + } + tagMembership.computeIfAbsent(tag, ignored -> new KSet<>()).add(biomeKey); + } + } + + /** + * Writes every accumulated tag exactly once into one datapack root, merging with whatever a previous + * dimension or pack already wrote to the same file. + */ + static void installBiomeTags(File datapackRoot, KMap> tagMembership) throws IOException { + if (tagMembership == null || tagMembership.isEmpty()) { + return; + } + for (Map.Entry> entry : tagMembership.entrySet()) { + String tag = entry.getKey(); + int separator = tag.indexOf(':'); + String namespace = tag.substring(0, separator); + String path = tag.substring(separator + 1); + Path tagRoot = new File(datapackRoot, "data/" + namespace + "/tags/worldgen/biome").toPath() + .toAbsolutePath().normalize(); + Path output = tagRoot.resolve(path + ".json").normalize(); + if (!output.startsWith(tagRoot)) { + IrisLogging.error("Unsafe custom biome tag '" + tag + "' for " + entry.getValue()); + continue; + } + writeBiomeTag(output, entry.getValue()); + } + } + + private static String normalizeResourceKey(String key) { + if (key == null || key.isBlank()) { + return null; + } + String normalized = key.trim().toLowerCase(Locale.ROOT); + if (normalized.indexOf(':') < 0) { + normalized = "minecraft:" + normalized; + } + return RESOURCE_KEY_PATTERN.matcher(normalized).matches() ? normalized : null; + } + + static void writeBiomeTag(Path output, Set biomeKeys) throws IOException { + if (biomeKeys == null || biomeKeys.isEmpty()) { + return; + } + synchronized (IrisDimension.class) { + Set values = new TreeSet<>(); + if (Files.isRegularFile(output)) { + JSONObject existing = new JSONObject(Files.readString(output, StandardCharsets.UTF_8)); + JSONArray existingValues = existing.optJSONArray("values"); + if (existingValues != null) { + for (int index = 0; index < existingValues.length(); index++) { + String value = existingValues.optString(index, null); + if (value != null && !value.isBlank()) { + values.add(value); + } + } + } + } + values.addAll(biomeKeys); + + JSONArray outputValues = new JSONArray(); + for (String value : values) { + outputValues.put(value); + } + JSONObject tag = new JSONObject(); + tag.put("replace", false); + tag.put("values", outputValues); + writeAtomic(output, tag.toString(4)); + } + } + + private static void writeAtomic(Path output, String content) throws IOException { + Files.createDirectories(output.getParent()); + Path temporary = Files.createTempFile(output.getParent(), output.getFileName().toString(), ".tmp"); + try { + Files.writeString(temporary, content, StandardCharsets.UTF_8); + try { + Files.move(temporary, output, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(temporary, output, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(temporary); + } + } + + public Dimension getBaseDimension() { + return switch (environment) { + case NETHER -> Dimension.NETHER; + case THE_END -> Dimension.END; + default -> Dimension.OVERWORLD; + }; + } + + public String getDimensionTypeKey() { + return sanitizeDimensionTypeKeyValue(getLoadKey()); + } + + public static String sanitizeDimensionTypeKeyValue(String value) { + if (value == null || value.isBlank()) { + return "dimension"; + } + + String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/"); + sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_"); + sanitized = sanitized.replaceAll("/+", "/"); + sanitized = sanitized.replaceAll("^/+", ""); + sanitized = sanitized.replaceAll("/+$", ""); + if (sanitized.contains("..")) { + sanitized = sanitized.replace("..", "_"); + } + + sanitized = sanitized.replace("/", "_"); + return sanitized.isBlank() ? "dimension" : sanitized; + } + + public IrisDimensionType getDimensionType() { + IrisDimensionTypeOptions options = getDimensionOptions(); + if (fullbright) { + options = options.copy().ambientLight(1.0f); + } + return new IrisDimensionType(getBaseDimension(), options, getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight()); + } + + public boolean hasUpperDimension() { + return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none"); + } + + public void installDimensionType(IDataFixer fixer, KList datapackRoots) throws IOException { + IrisDimensionType type = getDimensionType(); + String json = type.toJson(fixer); + String dimensionTypeKey = getDimensionTypeKey(); + + IrisLogging.debug(" Installing Data Pack Dimension Type: \"iris:" + dimensionTypeKey + '"'); + for (File datapackRoot : datapackRoots) { + File output = new File(datapackRoot, "data/iris/dimension_type/" + dimensionTypeKey + ".json"); + output.getParentFile().mkdirs(); + IO.writeAll(output, json); + } + } + + @Override + public String getFolderName() { + return "dimensions"; + } + + @Override + public String getTypeName() { + return "Dimension"; + } + + public static void writeShared( + KList datapackRoots, + DimensionHeight height, + boolean adjustVanillaHeight + ) throws IOException { + IrisLogging.debug(" Installing Data Pack Vanilla Dimension Types"); + String[] jsonStrings = height.jsonStrings(); + for (File datapackRoot : datapackRoots) { + write(datapackRoot, "overworld", jsonStrings[0], adjustVanillaHeight); + write(datapackRoot, "the_nether", jsonStrings[1], adjustVanillaHeight); + write(datapackRoot, "the_end", jsonStrings[2], adjustVanillaHeight); + } + + // Ranged formats spanning every supported runtime (26.1.2 = 101, 26.2 = 107). The bootstrap + // provisioner stages this datapack before the server (and INMS) exists, so the runtime + // version cannot be resolved there; a range keeps the emitted bytes identical on both + // install paths and both Paper versions parse ranged min_format/max_format. + int minFormat = DataVersion.minSupportedPackFormat(); + int maxFormat = DataVersion.getLatest().getPackFormat(); + String raw = """ + { + "pack": { + "description": "Iris Data Pack. This pack contains all installed Iris Packs' resources.", + "pack_format": %d, + "min_format": %d, + "max_format": %d + } + } + """.formatted(maxFormat, minFormat, maxFormat); + + for (File datapackRoot : datapackRoots) { + File mcm = new File(datapackRoot, "pack.mcmeta"); + IO.writeAll(mcm, raw); + IrisLogging.debug(" Installing Data Pack MCMeta: " + mcm.getPath()); + } + } + + private static void write(File datapackRoot, String type, String json, boolean adjustVanillaHeight) throws IOException { + if (json == null) { + return; + } + File dimTypeVanilla = new File(datapackRoot, "data/minecraft/dimension_type/" + type + ".json"); + + if (adjustVanillaHeight || dimTypeVanilla.exists()) { + dimTypeVanilla.getParentFile().mkdirs(); + IO.writeAll(dimTypeVanilla, json); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingEntry.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingEntry.java new file mode 100644 index 000000000..7dde2e11c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingEntry.java @@ -0,0 +1,80 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Snippet("dimension-carving-entry") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Dimension-level cave biome override with absolute world Y bounds.") +@Data +public class IrisDimensionCarvingEntry { + private final transient AtomicCache realBiome = new AtomicCache<>(true); + private final transient Map childGenerators = new ConcurrentHashMap<>(); + + @Desc("Stable id for this carving entry") + private String id = ""; + + @Desc("Enable or disable this carving entry") + private boolean enabled = true; + + @RegistryListResource(IrisBiome.class) + @Desc("Cave biome to apply when world Y falls within worldYRange") + private String biome = ""; + + @Desc("Absolute world Y bounds where this carving entry applies") + private IrisRange worldYRange = new IrisRange(-64, 320); + + @DependsOn({"children"}) + @Desc("If this carving entry has child carving entries, this controls how small those child carving patches are.") + private double childShrinkFactor = 1.5; + + @DependsOn({"children"}) + @Desc("If this carving entry has child carving entries, this controls the shape pattern used to pick them.") + private IrisGeneratorStyle childStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); + + @ArrayType(min = 1, type = String.class) + @Desc("Child carving entry ids. Child ids can point back to parent ids to create cycles; recursion is bounded by childRecursionDepth.") + private KList children = new KList<>(); + + @Desc("Maximum recursion depth when resolving child carving entries from this entry.") + private int childRecursionDepth = 3; + + public IrisBiome getRealBiome(IrisData data) { + return realBiome.aquire(() -> { + String biomeKey = getBiome(); + if (biomeKey == null || biomeKey.isBlank()) { + return null; + } + + return data.getBiomeLoader().load(biomeKey.trim()); + }); + } + + public CNG getChildrenGenerator(long seed, IrisData data) { + String entryId = getId(); + long idHash = entryId == null ? 0L : entryId.trim().hashCode(); + long generatorSeed = seed ^ (idHash << 32) ^ 2137L; + return childGenerators.computeIfAbsent(generatorSeed, key -> { + double scale = Math.max(0.0001D, getChildShrinkFactor()); + RNG random = new RNG(key); + return getChildStyle().create(random.nextParallelRNG(2137), data).bake().scale(scale).bake(); + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java new file mode 100644 index 000000000..6a4c2aa79 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java @@ -0,0 +1,371 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.project.noise.CNG; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +public final class IrisDimensionCarvingResolver { + private static final int MAX_CHILD_DEPTH = 32; + private static final long CHILD_SEED_SALT = 0x9E3779B97F4A7C15L; + private static final ThreadLocal> THREAD_STATE = + ThreadLocal.withInitial(() -> new WeakReference<>(null)); + + private IrisDimensionCarvingResolver() { + + } + + public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY) { + return resolveRootEntry(engine, worldY, threadState()); + } + + public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) { + State resolvedState = state == null ? threadState() : state; + resolvedState.bind(engine); + if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) { + return resolvedState.rootEntriesByWorldY.get(worldY); + } + + IrisDimension dimension = engine.getDimension(); + List entries = dimension.getCarving(); + if (entries == null || entries.isEmpty()) { + resolvedState.rootEntriesByWorldY.put(worldY, null); + return null; + } + + IrisDimensionCarvingEntry resolved = null; + for (IrisDimensionCarvingEntry entry : entries) { + if (!isRootCandidate(engine, entry, worldY, resolvedState)) { + continue; + } + + resolved = entry; + } + + resolvedState.rootEntriesByWorldY.put(worldY, resolved); + return resolved; + } + + public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) { + return resolveFromRoot(engine, rootEntry, worldX, worldZ, threadState()); + } + + public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) { + State resolvedState = state == null ? threadState() : state; + resolvedState.bind(engine); + if (rootEntry == null) { + return null; + } + + IrisBiome rootBiome = resolveEntryBiome(engine, rootEntry, resolvedState); + if (rootBiome == null) { + return null; + } + + int remainingDepth = clampDepth(rootEntry.getChildRecursionDepth()); + if (remainingDepth <= 0) { + return rootEntry; + } + + Map entryIndex = resolveEntryIndex(engine, resolvedState); + IrisDimensionCarvingEntry current = rootEntry; + int depth = remainingDepth; + while (depth > 0) { + IrisDimensionCarvingEntry selected = selectChild(engine, current, worldX, worldZ, entryIndex, resolvedState); + if (selected == null || selected == current) { + break; + } + + depth--; + int childDepthLimit = clampDepth(selected.getChildRecursionDepth()); + if (childDepthLimit < depth) { + depth = childDepthLimit; + } + current = selected; + } + + return current; + } + + public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry) { + return resolveEntryBiome(engine, entry, null); + } + + public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry, State state) { + if (entry == null) { + return null; + } + + if (state == null) { + return entry.getRealBiome(engine.getData()); + } + + state.bind(engine); + if (state.biomeCache.containsKey(entry)) { + return state.biomeCache.get(entry); + } + + IrisBiome biome = entry.getRealBiome(engine.getData()); + state.biomeCache.put(entry, biome); + return biome; + } + + private static boolean isRootCandidate(Engine engine, IrisDimensionCarvingEntry entry, int worldY, State state) { + if (entry == null || !entry.isEnabled()) { + return false; + } + + IrisRange worldYRange = entry.getWorldYRange(); + if (worldYRange != null && !worldYRange.contains(worldY)) { + return false; + } + + return resolveEntryBiome(engine, entry, state) != null; + } + + private static IrisDimensionCarvingEntry selectChild( + Engine engine, + IrisDimensionCarvingEntry parent, + int worldX, + int worldZ, + Map entryIndex, + State state + ) { + KList children = parent.getChildren(); + if (children == null || children.isEmpty()) { + return parent; + } + + IrisBiome parentBiome = resolveEntryBiome(engine, parent, state); + if (parentBiome == null) { + return parent; + } + + ParentSelectionPlan selectionPlan = state.selectionPlans.get(parent); + if (selectionPlan == null) { + selectionPlan = buildSelectionPlan(engine, parent, parentBiome, entryIndex, state); + state.selectionPlans.put(parent, selectionPlan); + } + + if (selectionPlan.parentOnly) { + return parent; + } + + long seed = resolveChildSeed(engine, state); + CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData()); + double sample = childGenerator.noiseFast2D(worldX, worldZ); + int selectedIndex = (int) Math.round(IrisInterpolation.lerp(0, selectionPlan.maxIndex, sample)); + CarvingChoice selected = selectionPlan.get(selectedIndex); + if (selected == null || selected.entry == null) { + return parent; + } + + return selected.entry; + } + + private static ParentSelectionPlan buildSelectionPlan( + Engine engine, + IrisDimensionCarvingEntry parent, + IrisBiome parentBiome, + Map entryIndex, + State state + ) { + List options = new ArrayList<>(); + KList children = parent.getChildren(); + if (children != null) { + for (String childId : children) { + if (childId == null || childId.isBlank()) { + continue; + } + + IrisDimensionCarvingEntry child = entryIndex.get(childId.trim()); + if (child == null || !child.isEnabled()) { + continue; + } + + IrisBiome childBiome = resolveEntryBiome(engine, child, state); + if (childBiome == null) { + continue; + } + + options.add(new CarvingChoice(child, rarity(childBiome))); + } + } + + options.add(new CarvingChoice(parent, rarity(parentBiome))); + if (options.size() <= 1) { + return ParentSelectionPlan.parentOnly(); + } + + CarvingChoice[] mappedChoices = buildRarityMappedChoices(options); + if (mappedChoices.length == 0) { + return ParentSelectionPlan.parentOnly(); + } + + return new ParentSelectionPlan(mappedChoices); + } + + private static CarvingChoice[] buildRarityMappedChoices(List choices) { + int max = 1; + for (CarvingChoice choice : choices) { + if (choice.rarity > max) { + max = choice.rarity; + } + } + + max++; + List mapped = new ArrayList<>(); + boolean flip = false; + for (CarvingChoice choice : choices) { + int count = max - choice.rarity; + for (int index = 0; index < count; index++) { + flip = !flip; + if (flip) { + mapped.add(choice); + } else { + mapped.add(0, choice); + } + } + } + + return mapped.toArray(new CarvingChoice[0]); + } + + private static int rarity(IrisBiome biome) { + if (biome == null) { + return 1; + } + + int rarity = biome.getRarity(); + return Math.max(rarity, 1); + } + + private static int clampDepth(int depth) { + if (depth <= 0) { + return 0; + } + + return Math.min(depth, MAX_CHILD_DEPTH); + } + + private static Map resolveEntryIndex(Engine engine, State state) { + if (state.entryIndex == null) { + state.entryIndex = engine.getDimension().getCarvingEntryIndex(); + } + + return state.entryIndex; + } + + private static long resolveChildSeed(Engine engine, State state) { + if (state.childSeed == null) { + state.childSeed = engine.getSeedManager().getCarve() ^ CHILD_SEED_SALT; + } + + return state.childSeed; + } + + private static State threadState() { + WeakReference reference = THREAD_STATE.get(); + State state = reference.get(); + if (state != null) { + return state; + } + State replacement = new State(); + THREAD_STATE.set(new WeakReference<>(replacement)); + return replacement; + } + + public static final class State { + private final Map rootEntriesByWorldY = new HashMap<>(); + private final Map selectionPlans = new IdentityHashMap<>(); + private final Map biomeCache = new IdentityHashMap<>(); + private WeakReference engineIdentity; + private WeakReference dimensionIdentity; + private WeakReference dataIdentity; + private Map entryIndex; + private Long childSeed; + + private void bind(Engine engine) { + IrisDimension dimension = engine.getDimension(); + IrisData data = engine.getData(); + if (references(engineIdentity, engine) + && references(dimensionIdentity, dimension) + && references(dataIdentity, data)) { + return; + } + engineIdentity = new WeakReference<>(engine); + dimensionIdentity = new WeakReference<>(dimension); + dataIdentity = new WeakReference<>(data); + rootEntriesByWorldY.clear(); + selectionPlans.clear(); + biomeCache.clear(); + entryIndex = null; + childSeed = null; + } + + private static boolean references(WeakReference identity, Object value) { + return identity != null && identity.get() == value; + } + } + + private static final class ParentSelectionPlan { + private final CarvingChoice[] mappedChoices; + private final int maxIndex; + private final boolean parentOnly; + + private ParentSelectionPlan(CarvingChoice[] mappedChoices) { + this.mappedChoices = mappedChoices; + this.maxIndex = mappedChoices.length - 1; + this.parentOnly = false; + } + + private ParentSelectionPlan() { + this.mappedChoices = null; + this.maxIndex = -1; + this.parentOnly = true; + } + + private static ParentSelectionPlan parentOnly() { + return new ParentSelectionPlan(); + } + + private CarvingChoice get(int index) { + if (mappedChoices == null || mappedChoices.length == 0) { + return null; + } + + if (index < 0) { + return mappedChoices[0]; + } + + if (index >= mappedChoices.length) { + return mappedChoices[mappedChoices.length - 1]; + } + + return mappedChoices[index]; + } + } + + private static final class CarvingChoice implements IRare { + private final IrisDimensionCarvingEntry entry; + private final int rarity; + + private CarvingChoice(IrisDimensionCarvingEntry entry, int rarity) { + this.entry = entry; + this.rarity = rarity; + } + + @Override + public int getRarity() { + return rarity; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionContractException.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionContractException.java new file mode 100644 index 000000000..8d432d410 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionContractException.java @@ -0,0 +1,25 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +public final class IrisDimensionContractException extends IllegalStateException { + public IrisDimensionContractException(String message) { + super(message); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionMode.java new file mode 100644 index 000000000..42170e426 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionMode.java @@ -0,0 +1,43 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("dimension-mode") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a dimensional mode") +@Data +public class IrisDimensionMode { + @Desc("The dimension type") + private IrisDimensionModeType type = IrisDimensionModeType.OVERWORLD; + + public EngineMode create(Engine engine) { + return type.create(engine); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionModeType.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionModeType.java similarity index 80% rename from core/src/main/java/com/volmit/iris/engine/object/IrisDimensionModeType.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisDimensionModeType.java index d971bcbe6..155dc99e7 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionModeType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionModeType.java @@ -16,15 +16,15 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineMode; -import com.volmit.iris.engine.mode.ModeEnclosure; -import com.volmit.iris.engine.mode.ModeIslands; -import com.volmit.iris.engine.mode.ModeOverworld; -import com.volmit.iris.engine.mode.ModeSuperFlat; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.mode.ModeEnclosure; +import art.arcane.iris.engine.mode.ModeIslands; +import art.arcane.iris.engine.mode.ModeOverworld; +import art.arcane.iris.engine.mode.ModeSuperFlat; +import art.arcane.iris.engine.object.annotations.Desc; import java.util.function.Function; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionRuntimeContract.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionRuntimeContract.java new file mode 100644 index 000000000..5c4e226ae --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionRuntimeContract.java @@ -0,0 +1,98 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import java.util.Objects; + +public record IrisDimensionRuntimeContract( + String typeKey, + int minHeight, + int height, + int logicalHeight +) { + public IrisDimensionRuntimeContract { + Objects.requireNonNull(typeKey, "typeKey"); + if (typeKey.isBlank()) { + throw new IllegalArgumentException("Dimension type key cannot be blank"); + } + if (height <= 0) { + throw new IllegalArgumentException("Dimension height must be positive"); + } + if (logicalHeight < 0 || logicalHeight > height) { + throw new IllegalArgumentException("Logical height must be inside the dimension height"); + } + } + + public static IrisDimensionRuntimeContract expected(IrisDimension dimension, String namespace) { + Objects.requireNonNull(dimension, "dimension"); + Objects.requireNonNull(namespace, "namespace"); + return new IrisDimensionRuntimeContract( + namespace + ":" + dimension.getDimensionTypeKey(), + dimension.getMinHeight(), + dimension.getMaxHeight() - dimension.getMinHeight(), + dimension.getLogicalHeight()); + } + + public static void requireHotloadCompatible( + String runtimeName, + IrisDimension active, + IrisDimension replacement, + String namespace + ) { + IrisDimensionRuntimeContract actual = expected(active, namespace); + IrisDimensionRuntimeContract proposed = expected(replacement, namespace); + proposed.requireExact(runtimeName, actual); + } + + public int maxHeight() { + return Math.addExact(minHeight, height); + } + + public void requireExact(String runtimeName, IrisDimensionRuntimeContract actual) { + Objects.requireNonNull(actual, "actual"); + if (equals(actual)) { + return; + } + throw mismatch(runtimeName, actual.typeKey(), actual.minHeight(), actual.height(), actual.logicalHeight()); + } + + public void requireHeight(String runtimeName, int actualMinHeight, int actualHeight) { + if (minHeight == actualMinHeight && height == actualHeight) { + return; + } + throw mismatch(runtimeName, "unknown", actualMinHeight, actualHeight, -1); + } + + private IrisDimensionContractException mismatch( + String runtimeName, + String actualTypeKey, + int actualMinHeight, + int actualHeight, + int actualLogicalHeight + ) { + String expectedRange = minHeight + ".." + maxHeight(); + String actualRange = actualMinHeight + ".." + Math.addExact(actualMinHeight, actualHeight); + String actualLogical = actualLogicalHeight < 0 ? "unknown" : Integer.toString(actualLogicalHeight); + return new IrisDimensionContractException(runtimeName + " requires Iris dimension type " + typeKey + + " with range " + expectedRange + " and logical height " + logicalHeight + + ", but the loaded runtime uses " + actualTypeKey + " with range " + actualRange + + " and logical height " + actualLogical + + ". Generation was refused before any chunk writes. Restart after installing the exact Iris dimension type; terrain clipping is not allowed."); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionType.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionType.java new file mode 100644 index 000000000..1f56ad0b2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionType.java @@ -0,0 +1,104 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.nms.datapack.IDataFixer; +import art.arcane.volmlib.util.data.Varint; +import art.arcane.volmlib.util.io.IO; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +@Getter +@ToString +@Accessors(fluent = true, chain = true) +@EqualsAndHashCode +public final class IrisDimensionType { + @NonNull + private final String key; + @NonNull + private final IDataFixer.Dimension base; + @NonNull + private final IrisDimensionTypeOptions options; + private final int logicalHeight; + private final int height; + private final int minY; + + public static final int MIN_HEIGHT = 16; + public static final int MAX_HEIGHT = 4064; + public static final int MIN_MIN_Y = -2032; + public static final int MAX_MIN_Y = 2031; + public static final int HEIGHT_STEP = 16; + + public IrisDimensionType( + @NonNull IDataFixer.Dimension base, + @NonNull IrisDimensionTypeOptions options, + int logicalHeight, + int height, + int minY + ) { + if (logicalHeight > height) throw new IllegalArgumentException("Logical height cannot be greater than height"); + if (logicalHeight < 0) throw new IllegalArgumentException("Logical height cannot be less than zero"); + if (height < MIN_HEIGHT || height > MAX_HEIGHT) throw new IllegalArgumentException("Height must be between 16 and 4064"); + if ((height & (HEIGHT_STEP - 1)) != 0) throw new IllegalArgumentException("Height must be a multiple of 16"); + if (minY < MIN_MIN_Y || minY > MAX_MIN_Y) throw new IllegalArgumentException("Min Y must be between -2032 and 2031"); + if ((minY & (HEIGHT_STEP - 1)) != 0) throw new IllegalArgumentException("Min Y must be a multiple of 16"); + + this.base = base; + this.options = options; + this.logicalHeight = logicalHeight; + this.height = height; + this.minY = minY; + this.key = createKey(); + } + + public static IrisDimensionType fromKey(String key) { + var stream = new ByteArrayInputStream(IO.decode(key.replace(".", "=").toUpperCase())); + try (var din = new DataInputStream(stream)) { + return new IrisDimensionType( + IDataFixer.Dimension.values()[din.readUnsignedByte()], + new IrisDimensionTypeOptions().read(din), + Varint.readUnsignedVarInt(din), + Varint.readUnsignedVarInt(din), + Varint.readSignedVarInt(din) + ); + } catch (IOException e) { + throw new RuntimeException("This is impossible", e); + } + } + + public String toJson(IDataFixer fixer) { + return fixer.createDimension( + base, + minY, + height, + logicalHeight, + options.copy() + ).toString(4); + } + + private String createKey() { + var stream = new ByteArrayOutputStream(41); + try (var dos = new DataOutputStream(stream)) { + dos.writeByte(base.ordinal()); + options.write(dos); + Varint.writeUnsignedVarInt(logicalHeight, dos); + Varint.writeUnsignedVarInt(height, dos); + Varint.writeSignedVarInt(minY, dos); + } catch (IOException e) { + throw new RuntimeException("This is impossible", e); + } + + return IO.encode(stream.toByteArray()).replace("=", ".").toLowerCase(); + } + + public IrisDimensionTypeOptions options() { + return options.copy(); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionTypeOptions.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionTypeOptions.java similarity index 95% rename from core/src/main/java/com/volmit/iris/engine/object/IrisDimensionTypeOptions.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisDimensionTypeOptions.java index 139d6296d..41394e19e 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionTypeOptions.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionTypeOptions.java @@ -1,11 +1,16 @@ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MaxNumber; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.util.data.Varint; -import com.volmit.iris.util.json.JSONObject; -import lombok.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.DEFAULT; +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.FALSE; +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.TRUE; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.volmlib.util.data.Varint; +import art.arcane.volmlib.util.json.JSONObject; import lombok.experimental.Accessors; import org.jetbrains.annotations.Nullable; @@ -14,7 +19,6 @@ import java.io.DataOutput; import java.io.IOException; import java.util.Arrays; -import static com.volmit.iris.engine.object.IrisDimensionTypeOptions.TriState.*; @Data @NoArgsConstructor diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDirection.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDirection.java similarity index 91% rename from core/src/main/java/com/volmit/iris/engine/object/IrisDirection.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisDirection.java index bd8c70d4d..35502f0d6 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDirection.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDirection.java @@ -16,18 +16,17 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.util.collection.GBiset; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.Cuboid.CuboidDirection; -import com.volmit.iris.util.math.DOP; -import com.volmit.iris.util.math.VectorMath; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.volmlib.util.collection.GBiset; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.data.Cuboid.CuboidDirection; +import art.arcane.volmlib.util.math.DOP; +import art.arcane.volmlib.util.math.VectorMath; import org.bukkit.Axis; import org.bukkit.block.BlockFace; -import org.bukkit.block.data.type.Jigsaw; import org.bukkit.util.Vector; import java.util.Map; @@ -78,27 +77,6 @@ public enum IrisDirection { } - public static IrisDirection fromJigsawBlock(String direction) { - for (IrisDirection i : IrisDirection.values()) { - if (i.name().toLowerCase().split("\\Q_\\E")[0] - .equals(direction.split("\\Q_\\E")[0])) { - return i; - } - } - - return null; - } - - public static IrisDirection getDirection(Jigsaw.Orientation orientation) { - return switch (orientation) { - case DOWN_EAST, UP_EAST, EAST_UP -> EAST_POSITIVE_X; - case DOWN_NORTH, UP_NORTH, NORTH_UP -> NORTH_NEGATIVE_Z; - case DOWN_SOUTH, UP_SOUTH, SOUTH_UP -> SOUTH_POSITIVE_Z; - case DOWN_WEST, UP_WEST, WEST_UP -> WEST_NEGATIVE_X; - }; - - } - public static IrisDirection closest(Vector v) { double m = Double.MAX_VALUE; IrisDirection s = null; diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDuration.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDuration.java similarity index 77% rename from core/src/main/java/com/volmit/iris/engine/object/IrisDuration.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisDuration.java index 0eaf13391..d26c82352 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDuration.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDuration.java @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.format.Form; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.format.Form; import lombok.Data; import java.util.concurrent.TimeUnit; @@ -32,7 +32,7 @@ public class IrisDuration { @Desc("Milliseconds (1000ms = 1 second)") private int milliseconds = 0; - @Desc("Minecraft Ticks (20 minecraft ticks = 1 second") + @Desc("Minecraft Ticks (20 minecraft ticks = 1 second)") private int minecraftTicks = 0; @Desc("Seconds (60 seconds = 1 minute)") @@ -50,10 +50,10 @@ public class IrisDuration { @Desc("Minecraft Days (1 minecraft day = 20 real minutes)") private int minecraftDays = 0; - @Desc("Minecraft Weeks (1 minecraft week = 2 real hours and 18 real minutes)") + @Desc("Minecraft Weeks (7 minecraft days = 2 real hours and 20 real minutes)") private int minecraftWeeks = 0; - @Desc("Minecraft Lunar Cycles (1 minecraft lunar cycle = 2 real hours and 36 real minutes)") + @Desc("Minecraft Lunar Cycles (8 minecraft days = 2 real hours and 40 real minutes)") private int minecraftLunarCycles = 0; @Desc("REAL (not minecraft) Days") @@ -71,9 +71,8 @@ public class IrisDuration { + TimeUnit.DAYS.toMillis(getDays()) + (getMinecraftTicks() * 50L) + (getMinecraftHours() * 50000L) - + (getMinecraftWeeks() * 50000L) - + (getMinecraftDays() * 24000L) - + (getMinecraftWeeks() * 168000L) - + (getMinecraftLunarCycles() * 192000L); + + (getMinecraftDays() * 1_200_000L) + + (getMinecraftWeeks() * 8_400_000L) + + (getMinecraftLunarCycles() * 9_600_000L); } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java new file mode 100644 index 000000000..286d6cf4b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java @@ -0,0 +1,322 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.data.registry.RegistryUtil; +import art.arcane.iris.util.common.scheduling.J; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.Vector; + +@Snippet("effect") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("An iris effect") +@Data +public class IrisEffect { + private final transient AtomicCache pt = new AtomicCache<>(); + private final transient AtomicCache latch = new AtomicCache<>(); + private final transient AtomicCache particleEffectResolved = new AtomicCache<>(); + private final transient AtomicCache soundResolved = new AtomicCache<>(); + @RegistryListPotionEffect + @Desc("The potion effect to apply in this area") + private String potionEffect = ""; + @Desc("The particle effect to apply in the area") + private String particleEffect = null; + @DependsOn({"particleEffect"}) + @MinNumber(-32) + @MaxNumber(32) + @Desc("Randomly offset from the surface to this surface+value") + private int particleOffset = 0; + @DependsOn({"particleEffect"}) + @MinNumber(-8) + @MaxNumber(8) + @Desc("The alt x, usually represents motion if the particle count is zero. Otherwise an offset.") + private double particleAltX = 0; + @DependsOn({"particleEffect"}) + @MinNumber(-8) + @MaxNumber(8) + @Desc("The alt y, usually represents motion if the particle count is zero. Otherwise an offset.") + private double particleAltY = 0; + @DependsOn({"particleEffect"}) + @MinNumber(-8) + @MaxNumber(8) + @Desc("The alt z, usually represents motion if the particle count is zero. Otherwise an offset.") + private double particleAltZ = 0; + @DependsOn({"particleEffect"}) + @Desc("Randomize the altX by -altX to altX") + private boolean randomAltX = true; + @DependsOn({"particleEffect"}) + @Desc("Randomize the altY by -altY to altY") + private boolean randomAltY = false; + @DependsOn({"particleEffect"}) + @Desc("Randomize the altZ by -altZ to altZ") + private boolean randomAltZ = true; + @Desc("The sound to play") + private String sound = null; + @DependsOn({"sound"}) + @MinNumber(0) + @MaxNumber(512) + @Desc("The max distance from the player the sound will play") + private int soundDistance = 12; + @DependsOn({"sound", "maxPitch"}) + @MinNumber(0.01) + @MaxNumber(1.99) + @Desc("The minimum sound pitch") + private double minPitch = 0.5D; + @DependsOn({"sound", "minVolume"}) + @MinNumber(0.01) + @MaxNumber(1.99) + @Desc("The max sound pitch") + private double maxPitch = 1.5D; + @DependsOn({"sound"}) + @MinNumber(0.001) + @MaxNumber(512) + @Desc("The sound volume.") + private double volume = 1.5D; + @DependsOn({"particleEffect"}) + @MinNumber(0) + @MaxNumber(512) + @Desc("The particle count. Try setting to zero for using the alt xyz to a motion value instead of an offset") + private int particleCount = 0; + @DependsOn({"particleEffect"}) + @MinNumber(0) + @MaxNumber(64) + @Desc("How far away from the player particles can play") + private int particleDistance = 20; + @DependsOn({"particleEffect"}) + @MinNumber(0) + @MaxNumber(128) + @Desc("How wide the particles can play (player's view left and right) RADIUS") + private int particleDistanceWidth = 24; + @DependsOn({"particleEffect"}) + @Desc("An extra value for some particles... Which bukkit doesn't even document.") + private double extra = 0; + @DependsOn({"potionEffect"}) + @MinNumber(-1) + @MaxNumber(1024) + @Desc("The Potion Strength or -1 to disable") + private int potionStrength = -1; + @DependsOn({"potionEffect", "potionTicksMin"}) + @MinNumber(1) + @Desc("The max time the potion will last for") + private int potionTicksMax = 155; + @DependsOn({"potionEffect", "potionTicksMax"}) + @MinNumber(1) + @Desc("The min time the potion will last for") + private int potionTicksMin = 75; + @Required + @MinNumber(0) + @Desc("The effect interval in milliseconds") + private int interval = 150; + @DependsOn({"particleEffect"}) + @MinNumber(0) + @MaxNumber(16) + @Desc("The effect distance start away") + private int particleAway = 5; + @Required + @MinNumber(1) + @Desc("The chance is 1 in CHANCE per interval") + private int chance = 50; + @Desc("Run commands, with configurable location parameters") + private IrisCommandRegistry commandRegistry = null; + + + public boolean canTick() { + return latch.aquire(() -> new ChronoLatch(interval)).flip(); + } + + public boolean shouldApplyNow() { + return canTick() && RNG.r.nextInt(chance) == 0; + } + + public String getParticleEffectKey() { + return particleEffect; + } + + public String getSoundKey() { + return sound; + } + + public Particle getParticleEffect() { + if (particleEffect == null) { + return null; + } + return particleEffectResolved.aquire(() -> resolveKeyed(Particle.class, particleEffect)); + } + + public Sound getSound() { + if (sound == null) { + return null; + } + return soundResolved.aquire(() -> resolveKeyed(Sound.class, sound)); + } + + private static T resolveKeyed(Class type, String key) { + NamespacedKey namespacedKey = NamespacedKey.fromString(key); + return namespacedKey == null ? null : RegistryUtil.lookup(type).get(namespacedKey); + } + + public PotionEffectType getRealType() { + return pt.aquire(() -> + { + if (getPotionEffect() == null || getPotionEffect().isEmpty()) { + return PotionEffectType.LUCK; + } + + try { + PotionEffectType resolved = PotionEffectTypes.resolve(getPotionEffect()); + if (resolved != null) { + return resolved; + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + if (PotionEffectTypes.shouldWarn(getPotionEffect())) { + IrisLogging.warn("Unknown Potion Effect Type: \"" + getPotionEffect() + "\". Valid types: " + PotionEffectTypes.knownTypesList()); + } + + return PotionEffectType.LUCK; + }); + } + + private static final class BukkitFx { + private static void run(Player p, Runnable r) { + J.runEntity(p, r); + } + } + + public void apply(Player p, Engine g) { + if (!shouldApplyNow()) { + return; + } + + BukkitFx.run(p, () -> { + Sound soundType = getSound(); + Particle particleType = getParticleEffect(); + if (soundType != null) { + Location part = p.getLocation().clone().add(RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance)); + p.playSound(part, soundType, (float) volume, (float) RNG.r.d(minPitch, maxPitch)); + } + + if (particleType != null) { + Location part = p.getLocation().clone().add(p.getLocation().getDirection().clone().multiply(RNG.r.i(particleDistance) + particleAway)).clone().add(p.getLocation().getDirection().clone().rotateAroundY(Math.toRadians(90)).multiply(RNG.r.d(-particleDistanceWidth, particleDistanceWidth))); + + part.setY(Math.round(g.getHeight(part.getBlockX(), part.getBlockZ())) + 1); + part.add(RNG.r.d(), 0, RNG.r.d()); + int offset = p.getWorld().getMinHeight(); + if (extra != 0) { + p.spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), + part.getZ(), + particleCount, + randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, + randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, + randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ, + extra); + } else { + p.spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), + particleCount, + randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, + randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, + randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ); + } + } + + if (commandRegistry != null) { + commandRegistry.run(p); + } + + if (potionStrength > -1) { + if (p.hasPotionEffect(getRealType())) { + PotionEffect e = p.getPotionEffect(getRealType()); + if (e != null && e.getAmplifier() > getPotionStrength()) { + return; + } + + p.removePotionEffect(getRealType()); + } + + p.addPotionEffect(new PotionEffect(getRealType(), + RNG.r.i(Math.min(potionTicksMax, potionTicksMin), + Math.max(potionTicksMax, potionTicksMin)), + getPotionStrength(), + true, false, false)); + } + }); + } + + public void apply(Entity p) { + if (!shouldApplyNow()) { + return; + } + + J.runEntity(p, () -> { + Sound soundType = getSound(); + Particle particleType = getParticleEffect(); + if (soundType != null) { + Location part = p.getLocation().clone().add(RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance)); + p.getWorld().playSound(part, soundType, (float) volume, (float) RNG.r.d(minPitch, maxPitch)); + } + + if (particleType != null) { + Location part = p.getLocation().clone().add(0, 0.25, 0).add(new Vector(1, 1, 1).multiply(RNG.r.d())).subtract(new Vector(1, 1, 1).multiply(RNG.r.d())); + part.add(RNG.r.d(), 0, RNG.r.d()); + if (extra != 0) { + p.getWorld().spawnParticle(particleType, part.getX(), part.getY() + RNG.r.i(particleOffset), + part.getZ(), + particleCount, + randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, + randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, + randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ, + extra); + } else { + p.getWorld().spawnParticle(particleType, part.getX(), part.getY() + RNG.r.i(particleOffset), part.getZ(), + particleCount, + randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, + randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, + randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ); + } + } + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEnchantment.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEnchantment.java new file mode 100644 index 000000000..f0eda8d44 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEnchantment.java @@ -0,0 +1,105 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListEnchantment; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.Locale; + + +@Snippet("enchantment") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents an enchantment & level") +@Data +public class IrisEnchantment { + @Required + @RegistryListEnchantment + @Desc("The enchantment") + private String enchantment; + + @MinNumber(1) + @Desc("Minimum amount of this loot") + private int minLevel = 1; + + @MinNumber(1) + @Desc("Maximum amount of this loot") + private int maxLevel = 1; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance that this enchantment is applied (0 to 1)") + private double chance = 1; + + public void apply(RNG rng, ItemMeta meta) { + try { + Enchantment enchant = resolve(); + if (enchant == null) { + IrisLogging.warn("Unknown Enchantment: " + getEnchantment()); + return; + } + if (rng.nextDouble() < chance) { + if (meta instanceof EnchantmentStorageMeta) { + ((EnchantmentStorageMeta) meta).addStoredEnchant(enchant, getLevel(rng), true); + return; + } + meta.addEnchant(enchant, getLevel(rng), true); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + + } + } + + /** + * Resolves the authored key against the live enchantment registry. Accepts a bare path + * ({@code sharpness}) or a full namespaced key ({@code mymod:vorpal}) - parity with the modded resolver. + */ + private Enchantment resolve() { + String raw = getEnchantment(); + if (raw == null || raw.isBlank()) { + return null; + } + String value = raw.trim().toLowerCase(Locale.ROOT).replace(' ', '_'); + NamespacedKey key = value.indexOf(':') >= 0 ? NamespacedKey.fromString(value) : NamespacedKey.minecraft(value); + return key == null ? null : Registry.ENCHANTMENT.get(key); + } + + public int getLevel(RNG rng) { + return LootResolver.inclusive(rng, getMinLevel(), getMaxLevel()); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineData.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineData.java similarity index 89% rename from core/src/main/java/com/volmit/iris/engine/object/IrisEngineData.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisEngineData.java index 3e4dff47c..c2c91ff4f 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineData.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineData.java @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KMap; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineSpawnerCooldown.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineSpawnerCooldown.java similarity index 89% rename from core/src/main/java/com/volmit/iris/engine/object/IrisEngineSpawnerCooldown.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisEngineSpawnerCooldown.java index ecc319935..dbd9aff41 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineSpawnerCooldown.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineSpawnerCooldown.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.math.M; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.math.M; import lombok.Data; @Data diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEngineStatistics.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineStatistics.java new file mode 100644 index 000000000..b541bd67d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineStatistics.java @@ -0,0 +1,67 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import lombok.Data; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +@Data +public class IrisEngineStatistics { + private static final AtomicIntegerFieldUpdater TOTAL_HOTLOADS = + AtomicIntegerFieldUpdater.newUpdater(IrisEngineStatistics.class, "totalHotloads"); + private static final AtomicIntegerFieldUpdater CHUNKS_GENERATED = + AtomicIntegerFieldUpdater.newUpdater(IrisEngineStatistics.class, "chunksGenerated"); + + private volatile int totalHotloads = 0; + private volatile int chunksGenerated = 0; + private volatile int IrisToUpgradedVersion = 0; + private volatile int IrisCreationVersion = 0; + private volatile int MinecraftVersion = 0; + + public void generatedChunk() { + CHUNKS_GENERATED.incrementAndGet(this); + } + + public void setUpgradedVersion(int i) { + IrisToUpgradedVersion = i; + } + public int getUpgradedVersion() { + return IrisToUpgradedVersion; + } + public void setVersion(int i) { + IrisCreationVersion = i; + } + + public int getVersion() { + return IrisCreationVersion; + } + + public void setMCVersion(int i) { + MinecraftVersion = i; + } + + public int getMCVersion() { + return MinecraftVersion; + } + + public void hotloaded() { + TOTAL_HOTLOADS.incrementAndGet(this); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineStreamType.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineStreamType.java similarity index 91% rename from core/src/main/java/com/volmit/iris/engine/object/IrisEngineStreamType.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisEngineStreamType.java index 3b122c785..4bdb39890 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineStreamType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineStreamType.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.engine.object; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.util.project.stream.ProceduralStream; import java.util.function.Function; @Desc("Represents a stream from the engine") diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineValueType.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineValueType.java similarity index 92% rename from core/src/main/java/com/volmit/iris/engine/object/IrisEngineValueType.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisEngineValueType.java index 72c4620d6..4c5fe9c32 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineValueType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineValueType.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.annotations.Desc; import java.util.function.Function; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java new file mode 100644 index 000000000..1622f2530 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java @@ -0,0 +1,547 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.service.EntityRiseSVC; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListEntityType; +import art.arcane.iris.engine.object.annotations.RegistryListSpecialEntity; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.util.common.data.registry.RegistryUtil; +import art.arcane.iris.platform.bukkit.BukkitWorld; +import art.arcane.iris.spi.PlatformWorld; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.common.plugin.Chunks; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Sound; +import org.bukkit.attribute.Attributable; +import org.bukkit.entity.Ageable; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Mob; +import org.bukkit.entity.Panda; +import org.bukkit.entity.Villager; +import org.bukkit.entity.Panda.Gene; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.loot.LootContext; +import org.bukkit.loot.LootTable; +import org.bukkit.loot.Lootable; +import org.bukkit.util.Vector; + +import java.util.Collection; +import java.util.Locale; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static art.arcane.iris.util.common.data.registry.Particles.ITEM; + +@SuppressWarnings("ALL") +@Accessors(chain = true) +@NoArgsConstructor + +@Desc("Represents an iris entity.") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisEntity extends IrisRegistrant { + @Required + @RegistryListEntityType + @Desc("The namespaced key of the entity type to spawn, such as minecraft:zombie or alexsmobs:grizzly_bear. To spawn a mob from another plugin or provider, set this type to unknown and define the special type.") + private String type = null; + + @Desc("The SpawnReason name to spawn the entity with, such as NATURAL or COMMAND.") + private String reason = null; + + @Desc("The custom name of this entity") + private String customName = ""; + + @Desc("Should the name on this entity be visible even if you arent looking at it.") + private boolean customNameVisible = false; + + @Desc("If this entity type is a mob, should it be aware of it's surroundings & interact with the world.") + private boolean aware = true; + + @Desc("If this entity type is a creature, should it have ai goals.") + private boolean ai = true; + + @Desc("Should this entity be glowing") + private boolean glowing = false; + + @Desc("Should gravity apply to this entity") + private boolean gravity = true; + + @Desc("When an entity is invulnerable it can only be damaged by players increative mode.") + private boolean invulnerable = false; + + @Desc("When an entity is silent it will not produce any sound.") + private boolean silent = false; + + @Desc("Should this entity be allowed to pickup items") + private boolean pickupItems = false; + + @Desc("Should this entity be removed when far away") + private boolean removable = false; + + @Desc("Entity helmet equipment") + private IrisLoot helmet = null; + + @Desc("Entity chestplate equipment") + private IrisLoot chestplate = null; + + @Desc("Entity boots equipment") + private IrisLoot boots = null; + + @Desc("Entity leggings equipment") + private IrisLoot leggings = null; + + @Desc("Entity main hand equipment") + private IrisLoot mainHand = null; + + @Desc("Entity off hand equipment") + private IrisLoot offHand = null; + + @Desc("Make other entities ride this entity") + @ArrayType(min = 1, type = IrisEntity.class) + private KList passengers = new KList<>(); + + @Desc("Attribute modifiers for this entity") + @ArrayType(min = 1, type = IrisAttributeModifier.class) + private KList attributes = new KList<>(); + + @Desc("Loot tables for drops. Only the tables list is honored for entities; mode and multiplier are ignored, and the tables replace vanilla drops entirely.") + private IrisLootReference loot = new IrisLootReference(); + + @Desc("If specified, this entity will be leashed by this entity. I.e. THIS ENTITY Leashed by SPECIFIED. This has no effect on EnderDragons, Withers, Players, or Bats.Non-living entities excluding leashes will not persist as leashholders.") + private IrisEntity leashHolder = null; + + @Desc("If specified, this entity will spawn with an effect") + private IrisEffect spawnEffect = null; + + @Desc("Simply moves the entity from below the surface slowly out of the ground as a spawn-in effect") + private boolean spawnEffectRiseOutOfGround = false; + + @Desc("The main gene for a panda if the entity type is a panda. One of NORMAL, LAZY, WORRIED, PLAYFUL, BROWN, WEAK or AGGRESSIVE.") + private String pandaMainGene = null; + + @Desc("The hidden gene for a panda if the entity type is a panda. One of NORMAL, LAZY, WORRIED, PLAYFUL, BROWN, WEAK or AGGRESSIVE.") + private String pandaHiddenGene = null; + + @Desc("The this entity is ageable, set it's baby status") + private boolean baby = false; + + @Desc("If the entity should never be culled.") + private boolean keepEntity = false; + + @Desc("The surface type to spawn this mob on") + private IrisSurface surface = IrisSurface.LAND; + + @RegistryListSpecialEntity + @Desc("Create a mob from another plugin, such as Mythic Mobs. Should be in the format of a namespace of PluginName:MobName") + private String specialType = ""; + + @Desc("Set to true if you want to apply all of the settings here to the mob, even though an external plugin has already done so.") + private boolean applySettingsToCustomMobAnyways = false; + + @ArrayType(min = 1, type = IrisCommand.class) + @Desc("Run raw commands when this entity is spawned. Use {x}, {y}, and {z} for location. /summon pig {x} {y} {z}") + private KList rawCommands = new KList<>(); + + public EntityType getBukkitType() { + if (type == null || type.isBlank()) { + return EntityType.UNKNOWN; + } + + NamespacedKey key = NamespacedKey.fromString(type.trim().toLowerCase(Locale.ROOT)); + + if (key == null) { + return EntityType.UNKNOWN; + } + + EntityType entityType = RegistryUtil.lookup(EntityType.class).get(key); + return entityType == null ? EntityType.UNKNOWN : entityType; + } + + public CreatureSpawnEvent.SpawnReason getBukkitReason() { + if (reason == null || reason.isBlank()) { + return CreatureSpawnEvent.SpawnReason.NATURAL; + } + + try { + return CreatureSpawnEvent.SpawnReason.valueOf(reason.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return CreatureSpawnEvent.SpawnReason.NATURAL; + } + } + + public Gene getBukkitPandaMainGene() { + return resolveGene(pandaMainGene); + } + + public Gene getBukkitPandaHiddenGene() { + return resolveGene(pandaHiddenGene); + } + + private static Gene resolveGene(String gene) { + if (gene == null || gene.isBlank()) { + return Gene.NORMAL; + } + + try { + return Gene.valueOf(gene.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return Gene.NORMAL; + } + } + + public Entity spawn(Engine gen, Location at) { + return spawn(gen, at, new RNG(at.hashCode())); + } + + public Entity spawn(Engine gen, Location at, RNG rng) { + if (!Chunks.isSafe(at)) { + return null; + } + if (isSpawnEffectRiseOutOfGround()) { + AtomicReference f = new AtomicReference<>(at); + try { + J.sfut(() -> { + if (Chunks.hasPlayersNearby(f.get())) { + Location b = f.get().clone(); + Location start = new Location(b.getWorld(), b.getX(), b.getY() - 5, b.getZ()); + f.set(start); + } + }).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + at = f.get(); + } + + Entity ee = doSpawn(at); + + if (ee == null && !Chunks.isSafe(at)) { + return null; + } + + if (isSpecialType() && !applySettingsToCustomMobAnyways) { + return ee; + } + + if (ee == null) { + return null; + } + + Entity e = ee; + e.setCustomName(getCustomName() != null ? C.translateAlternateColorCodes('&', getCustomName()) : null); + e.setCustomNameVisible(isCustomNameVisible()); + e.setGlowing(isGlowing()); + e.setGravity(isGravity()); + e.setInvulnerable(isInvulnerable()); + e.setSilent(isSilent()); + e.setPersistent(isKeepEntity() || IrisSettings.get().getWorld().isForcePersistEntities()); + + int gg = 0; + for (IrisEntity i : passengers) { + Entity passenger = i.spawn(gen, at, rng.nextParallelRNG(234858 + gg++)); + if (passenger == null) { + continue; + } + + if (Bukkit.isPrimaryThread()) { + e.addPassenger(passenger); + } else { + J.runEntity(e, () -> e.addPassenger(passenger)); + } + } + + if (e instanceof Attributable) { + Attributable a = (Attributable) e; + + for (IrisAttributeModifier i : getAttributes()) { + i.apply(rng, a); + } + } + + if (e instanceof Lootable) { + Lootable l = (Lootable) e; + + if (getLoot().getTables().isNotEmpty()) { + BukkitOps.bindLoot(this, gen, l, at, rng); + } + } + + if (e instanceof LivingEntity) { + LivingEntity l = (LivingEntity) e; + l.setAI(isAi()); + l.setCanPickupItems(isPickupItems()); + + if (getLeashHolder() != null) { + l.setLeashHolder(getLeashHolder().spawn(gen, at, rng.nextParallelRNG(234548))); + } + + l.setRemoveWhenFarAway(isRemovable()); + + if (getHelmet() != null && LootResolver.oneIn(rng, getHelmet().getRarity())) { + l.getEquipment().setHelmet(getHelmet().get(gen.isStudio(), rng)); + } + + if (getChestplate() != null && LootResolver.oneIn(rng, getChestplate().getRarity())) { + l.getEquipment().setChestplate(getChestplate().get(gen.isStudio(), rng)); + } + + if (getLeggings() != null && LootResolver.oneIn(rng, getLeggings().getRarity())) { + l.getEquipment().setLeggings(getLeggings().get(gen.isStudio(), rng)); + } + + if (getBoots() != null && LootResolver.oneIn(rng, getBoots().getRarity())) { + l.getEquipment().setBoots(getBoots().get(gen.isStudio(), rng)); + } + + if (getMainHand() != null && LootResolver.oneIn(rng, getMainHand().getRarity())) { + l.getEquipment().setItemInMainHand(getMainHand().get(gen.isStudio(), rng)); + } + + if (getOffHand() != null && LootResolver.oneIn(rng, getOffHand().getRarity())) { + l.getEquipment().setItemInOffHand(getOffHand().get(gen.isStudio(), rng)); + } + } + + if (e instanceof Ageable && isBaby()) { + ((Ageable) e).setBaby(); + } + + if (e instanceof Panda) { + ((Panda) e).setMainGene(getBukkitPandaMainGene()); + ((Panda) e).setHiddenGene(getBukkitPandaHiddenGene()); + } + + if (e instanceof Villager) { + Villager villager = (Villager) e; + villager.setRemoveWhenFarAway(false); + BukkitOps.persistVillager(villager); + } + + if (e instanceof Mob) { + Mob m = (Mob) e; + m.setAware(isAware()); + } + + if (spawnEffect != null) { + spawnEffect.apply(e); + } + + if (rawCommands.isNotEmpty()) { + final Location fat = at; + final PlatformWorld commandWorld = new BukkitWorld(fat.getWorld()); + rawCommands.forEach(r -> r.run(commandWorld, fat.getBlockX(), fat.getBlockY(), fat.getBlockZ())); + } + + Location finalAt1 = at; + + J.runEntity(e, () -> { + if (isSpawnEffectRiseOutOfGround() && e instanceof LivingEntity && Chunks.hasPlayersNearby(finalAt1)) { + LivingEntity living = (LivingEntity) e; + Location start = finalAt1.clone(); + boolean originalInvulnerable = e.isInvulnerable(); + boolean originalAi = living.hasAI(); + boolean originalCollidable = living.isCollidable(); + int originalNoDamageTicks = living.getNoDamageTicks(); + EntityRiseSVC.stamp(e, originalInvulnerable, originalAi, originalCollidable); + e.setInvulnerable(true); + living.setAI(false); + living.setCollidable(false); + living.setNoDamageTicks(100000); + Runnable restore = () -> restoreRiseState(e, living, originalInvulnerable, originalAi, + originalCollidable, originalNoDamageTicks); + AtomicInteger t = new AtomicInteger(0); + Runnable[] loop = new Runnable[1]; + loop[0] = () -> { + if (t.get() > 100 || e.isDead()) { + restore.run(); + return; + } + + t.incrementAndGet(); + if (e.getLocation().getBlock().getType().isSolid() || living.getEyeLocation().getBlock().getType().isSolid()) { + e.teleport(start.add(new Vector(0, 0.1, 0))); + ItemStack itemCrackData = new ItemStack(living.getEyeLocation().clone().subtract(0, 2, 0).getBlock().getBlockData().getMaterial()); + e.getWorld().spawnParticle(ITEM, living.getEyeLocation(), 6, 0.2, 0.4, 0.2, 0.06f, itemCrackData); + if (M.r(0.2)) { + e.getWorld().playSound(e.getLocation(), Sound.BLOCK_CHORUS_FLOWER_GROW, 0.8f, 0.1f); + } + if (!J.runEntity(e, loop[0], 1, restore)) { + restore.run(); + } + } else { + restore.run(); + } + }; + if (!J.runEntity(e, loop[0], 0, restore)) { + restore.run(); + } + } + }); + + + return e; + } + + private static void restoreRiseState(Entity entity, LivingEntity living, boolean invulnerable, + boolean ai, boolean collidable, int noDamageTicks) { + living.setNoDamageTicks(noDamageTicks); + living.setCollidable(collidable); + living.setAI(ai); + entity.setInvulnerable(invulnerable); + EntityRiseSVC.clearStamp(entity); + } + + private int surfaceY(Location l) { + int m = l.getBlockY(); + + while (m-- > 0) { + Location ll = l.clone(); + ll.setY(m); + + if (ll.getBlock().getType().isSolid()) { + return m; + } + } + + return 0; + } + + private Entity doSpawn(Location at) { + if (!Chunks.isSafe(at)) { + return null; + } + + if (EntityType.UNKNOWN.equals(getBukkitType()) && !isSpecialType()) { + return null; + } + + if (!Bukkit.isPrimaryThread()) { + // Someone called spawn (worldedit maybe?) on a non server thread + // Due to the structure of iris, we will call it sync and busy wait until it's done. + AtomicReference ae = new AtomicReference<>(); + + try { + if (!J.runAt(at, () -> ae.set(doSpawn(at)))) { + return null; + } + } catch (Throwable e) { + return null; + } + PrecisionStopwatch p = PrecisionStopwatch.start(); + + while (ae.get() == null) { + J.sleep(25); + + if (p.getMilliseconds() > 500) { + return null; + } + } + + return ae.get(); + } + + if (isSpecialType()) { + return IrisServices.get(ExternalDataSVC.class).spawnMob(at, Identifier.fromString(specialType)); + } + + + return BukkitPlatform.spawnEntity(at, getBukkitType(), getBukkitReason()); + } + + public boolean isSpecialType() { + return specialType != null && !specialType.equals(""); + } + + private static final class BukkitOps { + private static void persistVillager(Villager villager) { + villager.setPersistent(true); + } + + private static void bindLoot(IrisEntity entity, Engine gen, Lootable l, Location finalAt, RNG rng) { + l.setLootTable(new LootTable() { + @Override + public NamespacedKey getKey() { + return new NamespacedKey("iris", "loot-" + entity.hashCode()); + } + + @Override + public Collection populateLoot(Random random, LootContext context) { + KList items = new KList<>(); + + for (String fi : entity.getLoot().getTables()) { + IrisLootTable i = gen.getData().getLootLoader().load(fi); + items.addAll(i.getLoot(gen.isStudio(), gen.getSeedManager().getLoot(), InventorySlotType.STORAGE, finalAt.getWorld(), finalAt.getBlockX(), finalAt.getBlockY(), finalAt.getBlockZ())); + } + + return items; + } + + @Override + public void fillInventory(Inventory inventory, Random random, LootContext context) { + for (ItemStack i : populateLoot(random, context)) { + inventory.addItem(i); + } + + EngineBukkitOps.scramble(gen, inventory, rng); + } + }); + } + } + + @Override + public String getFolderName() { + return "entities"; + } + + @Override + public String getTypeName() { + return "Entity"; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java new file mode 100644 index 000000000..5d48b2980 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java @@ -0,0 +1,235 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.math.Vector3d; +import art.arcane.volmlib.util.matter.MatterMarker; +import art.arcane.volmlib.util.matter.slices.MarkerMatter; +import art.arcane.iris.util.common.scheduling.J; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Entity; + +@Snippet("entity-spawn") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents an entity spawn during initial chunk generation") +@Data +public class IrisEntitySpawn implements IRare { + private final transient AtomicCache rng = new AtomicCache<>(); + private final transient AtomicCache ent = new AtomicCache<>(); + @RegistryListResource(IrisEntity.class) + @Required + @Desc("The entity") + private String entity = ""; + @MinNumber(1) + @Desc("The 1 in RARITY chance for this entity to spawn") + private int rarity = 1; + @MinNumber(1) + @Desc("The minumum of this entity to spawn") + private int minSpawns = 1; + @MinNumber(1) + @Desc("The max of this entity to spawn") + private int maxSpawns = 1; + private transient IrisSpawner referenceSpawner; + private transient IrisMarker referenceMarker; + + public int spawn(Engine gen, Chunk c, RNG rng) { + int spawns = LootResolver.inclusive(rng, minSpawns, maxSpawns); + int s = 0; + + if (spawns > 0) { + for (int id = 0; id < spawns; id++) { + int x = (c.getX() * 16) + rng.i(16); + int z = (c.getZ() * 16) + rng.i(16); + World world = BukkitWorldBinding.tryBind(gen.getWorld()) ? BukkitWorldBinding.world(gen.getWorld()) : null; + int h = gen.getHeight(x, z, true) + (world == null ? -64 : world.getMinHeight()); + int hf = gen.getHeight(x, z, false) + (world == null ? -64 : world.getMinHeight()); + Location l = switch (getReferenceSpawner().getGroup()) { + case NORMAL -> new Location(c.getWorld(), x, hf + 1, z); + case CAVE -> findCaveSpawnLocation(gen, c, rng); + case UNDERWATER, BEACH -> new Location(c.getWorld(), x, rng.i(h + 1, hf), z); + }; + + if (l != null) { + if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) { + if (referenceSpawner.getAllowedLightLevels().contains(l.getBlock().getLightLevel())) { + if (spawn100(gen, l) != null) { + s++; + } + } + } else { + if (spawn100(gen, l) != null) { + s++; + } + } + } + } + } + + return s; + } + + public int spawn(Engine gen, IrisPosition c, RNG rng) { + int spawns = LootResolver.inclusive(rng, minSpawns, maxSpawns); + int s = 0; + + if (!BukkitWorldBinding.tryBind(gen.getWorld())) { + return 0; + } + + World world = BukkitWorldBinding.world(gen.getWorld()); + if (spawns > 0) { + + if (referenceMarker != null && referenceMarker.shouldExhaust()) { + if (J.isFolia()) { + J.a(() -> gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class)); + } else { + gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class); + } + } + + for (int id = 0; id < spawns; id++) { + Location l = BukkitPlatform.toLocation(c, world).add(0, 1, 0); + + if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) { + if (referenceSpawner.getAllowedLightLevels().contains(l.getBlock().getLightLevel())) { + if (spawn100(gen, l, true) != null) { + s++; + } + } + } else { + if (spawn100(gen, l, true) != null) { + s++; + } + } + } + } + + return s; + } + + public IrisEntity getRealEntity(Engine g) { + return ent.aquire(() -> g.getData().getEntityLoader().load(getEntity())); + } + + public Entity spawn(Engine g, Location at) { + if (getRealEntity(g) == null) { + return null; + } + + if (LootResolver.oneIn(rng.aquire(() -> new RNG(g.getSeedManager().getEntity())), getRarity())) { + return spawn100(g, at); + } + + return null; + } + + static Location findCaveSpawnLocation(Engine engine, Chunk chunk, RNG rng) { + if (J.isFolia()) { + return null; + } + KList markers = engine.getMantle().findMarkers(chunk.getX(), chunk.getZ(), MarkerMatter.CAVE_FLOOR); + return selectCaveSpawnLocation(markers, chunk.getWorld(), rng); + } + + static Location selectCaveSpawnLocation(KList markers, World world, RNG rng) { + return markers.convert((marker) -> BukkitPlatform.toLocation(marker, world).add(0, 1, 0)).getRandom(rng); + } + + private Entity spawn100(Engine g, Location at) { + return spawn100(g, at, false); + } + + private Entity spawn100(Engine g, Location at, boolean ignoreSurfaces) { + try { + IrisEntity irisEntity = getRealEntity(g); + if (irisEntity == null) { // No entity + IrisLogging.debug(" You are trying to spawn an entity that does not exist!"); + return null; + } + + if (!ignoreSurfaces && !irisEntity.getSurface().matches(at.clone().subtract(0, 1, 0).getBlock())) { + return null; + } + + Vector3d boundingBox = BukkitPlatform.entityBoundingBox(irisEntity.getBukkitType()); + if (!ignoreSurfaces && boundingBox != null) { + boolean isClearForSpawn = isAreaClearForSpawn(at, boundingBox); + if (!isClearForSpawn) { + return null; + } + } + + Entity e = irisEntity.spawn(g, at.add(0.5, 0.5, 0.5), rng.aquire(() -> new RNG(g.getSeedManager().getEntity()))); + if (e != null) { + IrisLogging.debug("Spawned " + C.DARK_AQUA + "Entity<" + getEntity() + "> " + C.GREEN + e.getType() + C.LIGHT_PURPLE + " @ " + C.GRAY + e.getLocation().getX() + ", " + e.getLocation().getY() + ", " + e.getLocation().getZ()); + } + + return e; + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + IrisLogging.error(" Failed to retrieve real entity @ " + at + " (entity: " + getEntity() + ")"); + return null; + } + } + + private boolean isAreaClearForSpawn(Location center, Vector3d boundingBox) { + World world = center.getWorld(); + int startX = center.getBlockX() - (int) (boundingBox.x / 2); + int endX = center.getBlockX() + (int) (boundingBox.x / 2); + int startY = center.getBlockY(); + int endY = center.getBlockY() + (int) boundingBox.y; + int startZ = center.getBlockZ() - (int) (boundingBox.z / 2); + int endZ = center.getBlockZ() + (int) (boundingBox.z / 2); + + for (int x = startX; x <= endX; x++) { + for (int y = startY; y <= endY; y++) { + for (int z = startZ; z <= endZ; z++) { + if (world.getBlockAt(x, y, z).getType() != Material.AIR) { + return false; + } + } + } + } + return true; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEnvironment.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEnvironment.java new file mode 100644 index 000000000..bfd30cbca --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEnvironment.java @@ -0,0 +1,36 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Represents a world environment") +public enum IrisEnvironment { + @Desc("The overworld environment") + NORMAL, + + @Desc("The nether environment") + NETHER, + + @Desc("The end environment") + THE_END, + + @Desc("A custom environment") + CUSTOM +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisExpression.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExpression.java similarity index 81% rename from core/src/main/java/com/volmit/iris/engine/object/IrisExpression.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisExpression.java index 0fbd1c48a..c56b16039 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisExpression.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisExpression.java @@ -16,24 +16,22 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; +import art.arcane.iris.spi.IrisLogging; import com.dfsek.paralithic.Expression; import com.dfsek.paralithic.eval.parser.Parser; import com.dfsek.paralithic.eval.parser.Scope; -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.IrisExpressionFunction.FunctionContext; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.stream.ProceduralStream; -import com.volmit.iris.util.stream.interpolation.Interpolated; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.IrisExpressionFunction.FunctionContext; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -77,7 +75,7 @@ public class IrisExpression extends IrisRegistrant { scope.addInvocationVariable("z"); } catch (Throwable e) { e.printStackTrace(); - Iris.error("Script Variable load error in " + getLoadFile().getPath()); + IrisLogging.error("Script Variable load error in " + getLoadFile().getPath()); } for (IrisExpressionFunction f : functions) { @@ -90,7 +88,7 @@ public class IrisExpression extends IrisRegistrant { return parser.parse(getExpression(), scope); } catch (Throwable e) { e.printStackTrace(); - Iris.error("Script load error in " + getLoadFile().getPath()); + IrisLogging.error("Script load error in " + getLoadFile().getPath()); } return null; @@ -139,9 +137,4 @@ public class IrisExpression extends IrisRegistrant { public String getTypeName() { return "Expression"; } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } } diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisExpressionFunction.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionFunction.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/IrisExpressionFunction.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisExpressionFunction.java index d56cbec7c..240fcabda 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisExpressionFunction.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionFunction.java @@ -1,16 +1,23 @@ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; import com.dfsek.paralithic.functions.dynamic.Context; import com.dfsek.paralithic.functions.dynamic.DynamicFunction; import com.dfsek.paralithic.node.Statefulness; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.math.RNG; -import lombok.*; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.math.RNG; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; import lombok.experimental.Accessors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionLoad.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionLoad.java new file mode 100644 index 000000000..769b4cb34 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionLoad.java @@ -0,0 +1,184 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.data.cache.LazyBoundedCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.project.stream.ProceduralStream; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Map; + +@Snippet("expression-load") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a variable to use in your expression. Do not set the name to x, y, or z, also don't duplicate names.") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisExpressionLoad { + private static final int STYLE_CACHE_SIZE = 8; + @Required + @Desc("The variable to assign this value to. Do not set the name to x, y, or z") + private String name = ""; + + @Desc("If the style value is not defined, this value will be used") + private double staticValue = -1; + + @Desc("If defined, this variable will use a generator style as it's value") + private IrisGeneratorStyle styleValue = null; + + @Desc("If defined, iris will use an internal stream from the engine as it's value") + private IrisEngineStreamType engineStreamValue = null; + + @Desc("If defined, iris will use an internal value from the engine as it's value") + private IrisEngineValueType engineValue = null; + + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private transient final Map engineCaches = + Collections.synchronizedMap(new IdentityHashMap<>()); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private transient final LazyBoundedCache standaloneStyleCache = + new LazyBoundedCache<>(STYLE_CACHE_SIZE); + + public double getValue(RNG rng, IrisData data, double x, double z) { + if (engineValue != null) { + Engine engine = requireEngine(data); + return cacheFor(engine).value.aquire(() -> engineValue.get(engine)); + } + + if (engineStreamValue != null) { + Engine engine = requireEngine(data); + return cacheFor(engine).stream.aquire(() -> engineStreamValue.get(engine)).get(x, z); + } + + if (styleValue != null) { + return style(data, rng).noise(x, z); + } + + return staticValue; + } + + public double getValue(RNG rng, IrisData data, double x, double y, double z) { + if (engineValue != null) { + Engine engine = requireEngine(data); + return cacheFor(engine).value.aquire(() -> engineValue.get(engine)); + } + + if (engineStreamValue != null) { + Engine engine = requireEngine(data); + return cacheFor(engine).stream.aquire(() -> engineStreamValue.get(engine)).get(x, z); + } + + if (styleValue != null) { + return style(data, rng).noise(x, y, z); + } + + return staticValue; + } + + private Engine requireEngine(IrisData data) { + Engine engine = data.getEngine(); + if (engine == null) { + throw new IllegalStateException("Expression variable '" + name + "' requires an active Iris engine."); + } + return engine; + } + + private EngineCache cacheFor(Engine engine) { + synchronized (engineCaches) { + Iterator> iterator = engineCaches.entrySet().iterator(); + while (iterator.hasNext()) { + if (iterator.next().getKey().isClosed()) { + iterator.remove(); + } + } + EngineCache cache = engineCaches.get(engine); + if (cache != null) { + return cache; + } + EngineCache created = new EngineCache(); + engineCaches.put(engine, created); + return created; + } + } + + private CNG style(IrisData data, RNG rng) { + Engine engine = data.getEngine(); + long seed = rng.getSeed(); + if (engine != null) { + return cacheFor(engine).styles.computeIfAbsent(seed, + ignored -> styleValue.createNoCache(new RNG(seed), data)); + } + StandaloneStyleKey key = new StandaloneStyleKey(data, seed); + return standaloneStyleCache.computeIfAbsent(key, + ignored -> styleValue.createNoCache(new RNG(seed), data)); + } + + private static final class EngineCache { + private final AtomicCache> stream = new AtomicCache<>(); + private final AtomicCache value = new AtomicCache<>(); + private final LazyBoundedCache styles = new LazyBoundedCache<>(STYLE_CACHE_SIZE); + } + + private static final class StandaloneStyleKey { + private final IrisData data; + private final long seed; + + private StandaloneStyleKey(IrisData data, long seed) { + this.data = data; + this.seed = seed; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof StandaloneStyleKey key)) { + return false; + } + return data == key.data && seed == key.seed; + } + + @Override + public int hashCode() { + return 31 * System.identityHashCode(data) + Long.hashCode(seed); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java new file mode 100644 index 000000000..6c0df7bdf --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java @@ -0,0 +1,364 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("floating-child-biome") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +@Desc("Declares a floating biome layer above this biome's terrain. A 2D footprint noise decides which columns are part of the island (threshold + style control blankets, swirls, scattered blobs). The top profile is driven by the target biome's own terrain generators (so a mountains biome produces real peaks, desert produces real dunes). The bottom tail hanging below is a separately configurable noise (pick VASCULAR for drippy roots, FRACTAL_RM_SIMPLEX for crystalline spikes, PERLIN for smooth rounded bowls).") +@Data +public class IrisFloatingChildBiomes implements IRare { + private final transient AtomicCache resolvedBiome = new AtomicCache<>(); + private final transient AtomicCache footprintCache = new AtomicCache<>(); + private final transient AtomicCache edgeTaperProfileCache = new AtomicCache<>(); + private final transient AtomicCache pickerCache = new AtomicCache<>(); + private final transient AtomicCache altitudeCache = new AtomicCache<>(); + private final transient AtomicCache topShapeCache = new AtomicCache<>(); + private final transient AtomicCache bottomCache = new AtomicCache<>(); + private final transient AtomicCache wallWarpCache = new AtomicCache<>(); + private final transient AtomicCache carveCache = new AtomicCache<>(); + private final transient AtomicCache carvingProfileSamplerCache = new AtomicCache<>(true); + private final transient AtomicCache shrinkScaleCache = new AtomicCache<>(); + + public CNG getFootprintCng(long baseSeed, IrisData data) { + return footprintCache.aquire(() -> getFootprintStyle().create(new RNG(baseSeed ^ 0xF007B17DL), data)); + } + + public FloatingIslandEdgeProfile getEdgeTaperProfile(long baseSeed, IrisData data) { + return edgeTaperProfileCache.aquire(() -> { + int width = FloatingIslandEdgeProfile.clampWidth(getEdgeTaperWidth()); + double amplitude = FloatingIslandEdgeProfile.clampVariationAmplitude( + getEdgeTaperVariationAmplitude(), width); + IrisGeneratorStyle style = getEdgeTaperVariationStyle(); + CNG variation = amplitude > 0.0D && style != null + ? style.create(new RNG(baseSeed ^ 0xED6E7A9E5L), data) + : null; + return new FloatingIslandEdgeProfile(width, getEdgeTaperExponent(), amplitude, variation); + }); + } + + public CNG getPickerCng(long baseSeed, IrisData data) { + return pickerCache.aquire(() -> getPickerStyle().create(new RNG(baseSeed ^ 0x91C4E72DL), data)); + } + + public CNG getAltitudeCng(long baseSeed, IrisData data) { + return altitudeCache.aquire(() -> getAltitudeStyle().create(new RNG(baseSeed ^ 0xA17DEBBL), data)); + } + + public CNG getTopShapeCng(long baseSeed, IrisData data) { + return topShapeCache.aquire(() -> getTopShapeStyle().create(new RNG(baseSeed ^ 0x70970601DEFL), data)); + } + + public CNG getBottomCng(long baseSeed, IrisData data) { + return bottomCache.aquire(() -> getBottomStyle().create(new RNG(baseSeed ^ 0xB0770075CAFEL), data)); + } + + public CNG getWallWarpCng(long baseSeed, IrisData data) { + IrisGeneratorStyle style = getWallWarpStyle(); + if (style == null) { + return null; + } + return wallWarpCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xA117BA17E0FL), data)); + } + + public CNG getCarveCng(long baseSeed, IrisData data) { + IrisGeneratorStyle style = getCarveStyle(); + if (style == null) { + return null; + } + return carveCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xCA5EC1EE5EL), data)); + } + + public IrisCaveProfileSampler getCarvingProfileSampler(Engine engine, IrisData data) { + if (!hasCarvingReference()) { + return null; + } + + return carvingProfileSamplerCache.aquire(() -> { + IrisBiome resolved = resolveCarvingBiome(carving, engine, data); + if (resolved == null || resolved.getCaveProfile() == null || !resolved.getCaveProfile().isEnabled()) { + return null; + } + + return new IrisCaveProfileSampler(engine, resolved.getCaveProfile()); + }); + } + + public boolean hasCarvingReference() { + return carving != null && !carving.isBlank(); + } + + static IrisBiome resolveCarvingBiome(String carving, Engine engine, IrisData data) { + if (carving == null || carving.isBlank() || engine == null || data == null) { + return null; + } + + IrisDimension dimension = engine.getDimension(); + if (dimension != null && dimension.getCarvingEntryIndex() != null) { + IrisDimensionCarvingEntry entry = dimension.getCarvingEntryIndex().get(carving); + if (entry != null) { + return IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry); + } + } + + if (data.getBiomeLoader() == null) { + return null; + } + + return data.getBiomeLoader().load(carving); + } + + @RegistryListResource(IrisBiome.class) + @Desc("The target biome whose visual design (layers, palette, decorators, surface objects, derivative, and — when topShapeMode=BIOME — generator profile) drives the floating island. Leave empty to reuse the parent biome (self).") + private String biome = ""; + + @MinNumber(1) + @MaxNumber(512) + @Desc("Selection rarity when multiple floating child entries are defined on one parent biome. Lower is more common.") + private int rarity = 1; + + @Desc("2D noise that decides which columns are part of this island. Set feature size via the style's own zoom field (e.g. {\"style\":\"CELLULAR\",\"zoom\":0.3} for ~30-block shards, {\"style\":\"SIMPLEX\",\"zoom\":1.0} for ~100-block blobs). Pick SIMPLEX for smooth blobs, CELLULAR for angular shards, VASCULAR for vein/branch strips, FRACTAL_FBM_SIMPLEX for large irregular blanket regions. Fracture (domain warp) this to get swirly silhouettes.") + private IrisGeneratorStyle footprintStyle = NoiseStyle.SIMPLEX.style(); + + @MinNumber(0) + @MaxNumber(1) + @Desc("Coverage threshold (0..1). Roughly the fraction of the world that is NOT island: 0.0 = every column becomes island, 0.5 ≈ 50% of columns, 0.8 ≈ sparse scattered ~20% coverage, 1.0 = no islands at all. Values near 0.5 feel most natural.") + private double footprintThreshold = 0.5; + + @MinNumber(2) + @MaxNumber(32) + @Desc("Width in blocks of the rounded transition from the floating island contour to its full thickness. Smaller values make a tighter edge; larger values make a broader rounded underside.") + private int edgeTaperWidth = FloatingIslandEdgeProfile.DEFAULT_WIDTH; + + @MinNumber(0.25) + @MaxNumber(4) + @Desc("Shape exponent for the rounded edge. Values below 1 fill into a softer, fuller curve; values above 1 keep the rim thinner before rising into the island interior.") + private double edgeTaperExponent = FloatingIslandEdgeProfile.DEFAULT_EXPONENT; + + @Desc("Broad 2D noise that organically varies the rounded edge without changing the island footprint. Low-frequency SIMPLEX or PERLIN styles give coherent natural contours; high-frequency or cellular styles can look speckled.") + private IrisGeneratorStyle edgeTaperVariationStyle = NoiseStyle.SIMPLEX.style().zoomed(0.18D); + + @MinNumber(0) + @MaxNumber(8) + @Desc("Maximum number of blocks by which edgeTaperVariationStyle locally widens or narrows the rounded transition. 0 disables variation. Runtime keeps the resulting local width between 2 and 32 blocks so every protected rim column remains connected.") + private double edgeTaperVariationAmplitude = 0.0D; + + @Desc("Picker noise used when multiple floating child entries exist. Samples once per column to deterministically choose which entry's footprint is tested there. Use a large style zoom (e.g. zoom: 4 for ~400-block regions) so each entry owns broad coherent areas.") + private IrisGeneratorStyle pickerStyle = NoiseStyle.SIMPLEX.style(); + + @Desc("Altitude noise — varies the base platform Y across one island so it isn't a flat plane. Use a large style zoom (e.g. zoom: 2 for ~200-block altitude patches) so an island sits at roughly one altitude.") + private IrisGeneratorStyle altitudeStyle = NoiseStyle.SIMPLEX.style(); + + @MinNumber(0) + @MaxNumber(2032) + @Desc("Minimum absolute world Y where the island base can sit. Island altitude is independent of the parent biome's terrain height; altitudeStyle noise varies the base between min and max per column.") + private int minHeightAboveSurface = 160; + + @MinNumber(0) + @MaxNumber(2032) + @Desc("Maximum absolute world Y where the island base can sit. Island altitude is independent of the parent biome's terrain height; altitudeStyle noise varies the base between min and max per column.") + private int maxHeightAboveSurface = 210; + + @Desc("Optional absolute minimum world Y for the island base. When set, baseY is clamped upward so the tail bottom stays above this value.") + private Integer minAbsoluteY = null; + + @Desc("Optional absolute maximum world Y for the island top. When set, the top is clamped downward.") + private Integer maxAbsoluteY = null; + + @Desc("How the top profile of the island is shaped. BIOME = evaluate target biome's own generators (mountains biome -> real mountains). NOISE = use topShapeStyle as a user-controlled heightmap. FLAT = constant maxTopHeight slab.") + private TopShapeMode topShapeMode = TopShapeMode.BIOME; + + @MinNumber(0) + @MaxNumber(512) + @Desc("Maximum top profile height in blocks above the island base. Caps how tall the biome terrain can grow on top.") + private int maxTopHeight = 40; + + @Desc("Used only when topShapeMode=NOISE. 2D noise driving the top heightmap. Set feature scale via the style's zoom field (small zoom = rugged peaks, large zoom = broad rolling shapes).") + private IrisGeneratorStyle topShapeStyle = NoiseStyle.SIMPLEX.style(); + + @MinNumber(0) + @MaxNumber(1) + @Desc("Amplitude multiplier applied to the NOISE top profile. 0 = no top (flat at base), 1 = full maxTopHeight range.") + private double topShapeAmp = 1.0; + + @Desc("2D noise driving the bottom tail hanging below the island base. VASCULAR = drippy organic roots. FRACTAL_RM_SIMPLEX = crystalline spikes. CELLULAR = jagged chunks. PERLIN = smooth rounded bowl. SIMPLEX = gentle lobes. Set feature scale via the style's zoom field.") + private IrisGeneratorStyle bottomStyle = NoiseStyle.SIMPLEX.style(); + + @MinNumber(0) + @MaxNumber(512) + @Desc("Minimum blocks below the base where the tail extends.") + private int bottomDepthMin = 4; + + @MinNumber(0) + @MaxNumber(512) + @Desc("Maximum blocks below the base where the tail extends.") + private int bottomDepthMax = 20; + + @MinNumber(0.1) + @MaxNumber(8) + @Desc("Power curve applied to the bottom noise before mapping to depth. >1 = most columns shallow with occasional deeper spikes (sparse roots). <1 = most columns deep with occasional shallow spots (dense curtains). 1.0 = linear.") + private double bottomExponent = 1.0; + + @Desc("Controls the material palette near the island underside. DEPTH keeps the old top-down depth behavior. MIRROR_TOP uses the target biome's shallow/top palette from the underside upward. CUSTOM uses bottomPalette near the underside while keeping the target biome palette near the top.") + private FloatingBottomPaletteMode bottomPaletteMode = FloatingBottomPaletteMode.DEPTH; + + @ArrayType(min = 1, type = IrisBiomePaletteLayer.class) + @Desc("Custom palette layers used near the underside when bottomPaletteMode=CUSTOM. The layer format is the same as normal biome layers.") + private KList bottomPalette = new KList<>(); + + @MinNumber(1) + @MaxNumber(512) + @Desc("Hard cap on the total Y-extent (top minus bottom) of a single island column. Safety limit.") + private int maxThickness = 96; + + @Desc("Optional 3D noise that shifts the footprint's XZ sample position per Y layer — naturalizes the walls so they stop looking like a straight extrusion of the 2D footprint. Leave null to disable and keep straight vertical walls. Good defaults: {\"style\":\"SIMPLEX\",\"zoom\":0.25} for gentle undulation, {\"style\":\"FRACTAL_FBM_SIMPLEX\",\"zoom\":0.4} for craggier walls.") + private IrisGeneratorStyle wallWarpStyle = null; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Amplitude in blocks of the per-layer XZ shift applied when wallWarpStyle is set. 0 = no warp (straight walls). 4..8 = gentle naturalization. 16+ = heavily meandering walls. Ignored when wallWarpStyle is null.") + private double wallWarpAmplitude = 6.0; + + @Desc("Optional 3D noise that swiss-cheeses the island interior by marking individual blocks as air when the noise exceeds carveThreshold. Leave null to keep the island solid. Good defaults: {\"style\":\"CELLULAR\",\"zoom\":0.3} for bubble pockets, {\"style\":\"VASCULAR\",\"zoom\":0.25} for wormy tunnels.") + private IrisGeneratorStyle carveStyle = null; + + @Desc("Optional carving biome key or dimension carving entry id whose caveProfile drives floating-island internal air pockets. Dimension carving ids resolve first; biome keys such as carving/mushroom resolve second. When set, this overrides carveStyle while carveThreshold remains a final tuning bias.") + private String carving = ""; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Threshold (0..1) above which carveStyle noise carves air pockets. 1.0 = no direct-noise carving. 0.75 = sparse pockets. 0.55 = heavy swiss-cheese. 0.4 = shredded lattice. When carving is set, lower values add a final positive threshold bias to the referenced caveProfile.") + private double carveThreshold = 1.0; + + @Desc("Optional water surface height above the island base, in blocks. null = no internal water. Positive = water fills any dip in the top profile up to baseY + localFluidHeight (forms lakes/ponds in concavities of the biome-top heightmap).") + private Integer localFluidHeight = null; + + @Desc("Block used for the internal water pool when localFluidHeight is positive.") + @RegistryListBlockType + private String fluidBlock = "minecraft:water"; + + @Desc("When true, the target biome's decorators apply to the island's top surface.") + private boolean inheritDecorators = true; + + @Desc("When true, the target biome's surface objects are placed on the island's top surface instead of the parent terrain.") + private boolean inheritObjects = true; + + @MinNumber(0.01) + @MaxNumber(1) + @Desc("Uniform shrink factor applied to every object placed on this floating island (inherited, extra, and free-floating). 1.0 = native size, 0.5 = half size, 0.25 = quarter. Useful for making small floating biomes feel believable.") + private double objectShrinkFactor = 1.0; + + @ArrayType(min = 1, type = IrisObjectPlacement.class) + @Desc("Additional object placements anchored to the island top.") + private KList extraObjects = new KList<>(); + + @ArrayType(min = 1, type = IrisObjectPlacement.class) + @Desc("Additional object placements that float freely in air, independent of the island. Forced to ObjectPlaceMode.FLOATING.") + private KList floatingObjects = new KList<>(); + + @Desc("Visualization color for this floating child in Iris Studio.") + private String color = null; + + @Desc("Controls how topObjectOverrides are combined with the inherited surface objects from the target biome. INHERIT_ONLY (default) = behaves identically to before this field was added. MERGE = appends overrides after inherited objects. REPLACE = uses only overrides, ignoring all inherited objects.") + private OverrideMode topObjectMode = OverrideMode.INHERIT_ONLY; + + @Desc("Controls how bottomObjectOverrides are combined. INHERIT_ONLY (default) = no bottom objects placed (there is no inherited bottom set). MERGE = same as REPLACE for bottom (no inherited source). REPLACE = uses bottomObjectOverrides list only.") + private OverrideMode bottomObjectMode = OverrideMode.INHERIT_ONLY; + + @ArrayType(min = 1, type = IrisObjectPlacement.class) + @Desc("Object placements that override or supplement the inherited surface objects on the island TOP. Behaviour depends on topObjectMode. INHERIT_ONLY = this list is ignored. MERGE = appended after inherited. REPLACE = used instead of inherited.") + private KList topObjectOverrides = new KList<>(); + + @ArrayType(min = 1, type = IrisObjectPlacement.class) + @Desc("Object placements anchored to the island BOTTOM face. Each entry is auto-inverted 180 degrees around the X axis and placed flush against the lowest solid face of the island, so objects appear to hang upside-down from the underside. WARNING: directional blocks (stairs, doors, slabs) will not render correctly when flipped — use non-directional content (logs, leaves, stone, mycelium, ice, glass) for bottom placements.") + private KList bottomObjectOverrides = new KList<>(); + + public KList resolveTopObjects(IrisBiome target) { + KList surfaceObjects = (inheritObjects && target != null) ? target.getSurfaceObjects() : new KList<>(); + return resolveTopObjectsFromSurface(surfaceObjects); + } + + KList resolveTopObjectsFromSurface(KList surfaceObjects) { + return switch (topObjectMode) { + case REPLACE -> new KList<>(topObjectOverrides); + case MERGE -> { + KList merged = new KList<>(); + merged.addAll(surfaceObjects); + merged.addAll(topObjectOverrides); + yield merged; + } + case INHERIT_ONLY -> surfaceObjects; + }; + } + + public KList resolveBottomObjects(IrisBiome target) { + return switch (bottomObjectMode) { + case INHERIT_ONLY -> new KList<>(); + case MERGE, REPLACE -> bottomObjectOverrides; + }; + } + + public boolean hasObjectShrink() { + return objectShrinkFactor > 0 && objectShrinkFactor < 1.0; + } + + public IrisObjectScale getShrinkScale() { + return shrinkScaleCache.aquire(() -> { + IrisObjectScale s = new IrisObjectScale(); + s.setSize(Math.max(0.01, Math.min(1.0, objectShrinkFactor))); + return s; + }); + } + + public IrisBiome getRealBiome(IrisBiome parent, IrisData data) { + return resolvedBiome.aquire(() -> { + if (biome == null || biome.isBlank() || biome.equals(parent.getLoadKey())) { + return parent; + } + + IrisBiome loaded = data.getBiomeLoader().load(biome); + if (loaded == null) { + return parent; + } + + return loaded; + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java new file mode 100644 index 000000000..18301b3d9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java @@ -0,0 +1,293 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.formation.FormationGenerator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("formation") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A single procedurally generated natural formation, including rock landmarks and magical glacial silhouettes. Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file.") +@Data +public class IrisFormation implements IrisProceduralPlacement { + private final transient AtomicCache> variantCache = new AtomicCache<>(); + + @Desc("A human readable name used in logs and as the variant load key.") + private String name = "formation"; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance per chunk for this formation to attempt placement. Use density for multiple per chunk.") + private double chance = 0.02; + + @MinNumber(1) + @Desc("If the chance check passes, attempt this many placements in the chunk.") + private int density = 1; + + @MinNumber(1) + @MaxNumber(64) + @Desc("How many distinct variants to pre-bake for this formation. Higher means more variety at a small memory cost.") + private int variants = 6; + + @Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.") + private long seed = 1337; + + @Desc("The placement mode used to anchor the formation to the terrain.") + private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT; + + @Desc("Rotate this formation's placement.") + private IrisObjectRotation rotation = new IrisObjectRotation(); + + @Desc("Limit the max or min height of placement.") + private IrisObjectLimit clamp = new IrisObjectLimit(); + + @Desc("Whether this formation may place on the terrain surface, under carvings, or both.") + private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY; + + @MinNumber(0) + @MaxNumber(16) + @Desc("Horizontal buffer, in blocks, beyond each lowest solid foundation block that must also be solid, un-carved ground.") + private int surfaceSupportBuffer = 3; + + @Desc("If true, the formation anchors on the terrain height ignoring the water surface.") + private boolean underwater = false; + + @Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.") + private IrisObjectTranslate translate = new IrisObjectTranslate(); + + @Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).") + private IrisStiltSettings stiltSettings; + + @Desc("Settings for the vacuum place modes (VACUUM, VACUUM_HIGH, VACUUM_FAST, VACUUM_ORGANIC).") + private IrisVacuumSettings vacuumSettings; + + @Desc("The overall silhouette of the formation. Drives which sculpting routine the generator uses.") + private IrisFormationForm form = IrisFormationForm.SPIRE; + + @Required + @Desc("The main rock block, e.g. minecraft:stone. Ignored when blockPalette is set.") + @RegistryListBlockType + private String block = "minecraft:stone"; + + @Desc("A noise-driven palette for the main rock body. When set this overrides the single block, letting the body mix blocks by noise.") + private IrisMaterialPalette blockPalette = null; + + @Desc("Optional caprock block placed on the top crown of the formation (and the wide overhanging cap for HOODOO). Ignored when capPalette is set. When null and capPalette is unset, the formation uses its main rock everywhere.") + @RegistryListBlockType + private String capBlock = null; + + @Desc("A noise-driven palette for the caprock. When set this overrides the single capBlock.") + private IrisMaterialPalette capPalette = null; + + @Desc("Optional palette of strata (horizontal colored bands) blended into the rock body by y level. When set, every strataThickness blocks of height switches to the next strata block, producing the signature banded look. Falls back to the main block where unset.") + private IrisMaterialPalette strataPalette = null; + + @MinNumber(1) + @MaxNumber(32) + @Desc("The thickness in blocks of each horizontal strata band when strataPalette is set.") + private int strataThickness = 3; + + @MinNumber(1) + @Desc("Minimum total height of the formation in blocks.") + private int heightMin = 14; + + @MinNumber(1) + @Desc("Maximum total height of the formation in blocks.") + private int heightMax = 26; + + @MinNumber(1) + @Desc("Minimum base radius (half-width) of the formation in blocks.") + private int baseWidthMin = 3; + + @MinNumber(1) + @Desc("Maximum base radius (half-width) of the formation in blocks.") + private int baseWidthMax = 6; + + @MinNumber(0) + @Desc("The target radius (half-width) at the very top of the formation in blocks, before the profile is applied. 0 means taper to a point.") + private int topWidth = 0; + + @Desc("The function used to shape the formation radius over its height.") + private IrisFormationProfile profile = IrisFormationProfile.TAPER; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The normalized height (0 base, 1 top) of the pinched waist for the PARABOLIC profile (used by HOODOO).") + private double profileWaist = 0.55; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The minimum radius fraction at the waist for the PARABOLIC profile. Lower values pinch the waist tighter.") + private double profileWaistFloor = 0.35; + + @MinNumber(0) + @Desc("How far the formation leans, in degrees from vertical. 0 is perfectly upright. The whole body is sheared by this amount over its height.") + private double lean = 0; + + @Desc("Compass direction in degrees the formation leans toward when lean is non-zero.") + private double leanAzimuth = 0; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Surface roughness from 0 (a clean smooth solid) to 1 (heavily eroded and pitted). Driven by 3D value noise that perturbs the radius so the formation never reads as a clean cylinder.") + private double roughness = 0.3; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Per-block surface jitter from 0 (none) to 1 (maximum). Randomly carves and adds isolated edge blocks to break up the silhouette.") + private double jitter = 0.15; + + @MinNumber(0) + @Desc("Extra wide cap radius in blocks added on top for the HOODOO form, producing the overhanging mushroom caprock. 0 disables the overhang.") + private int hoodooCapRadius = 3; + + @MinNumber(1) + @MaxNumber(6) + @Desc("The height in blocks of the HOODOO overhanging caprock slab.") + private int hoodooCapHeight = 3; + + @MinNumber(0) + @Desc("How wide the ARCH opening is in blocks (the gap between the two legs). The arch span is rasterized over this width.") + private int archSpan = 10; + + @MinNumber(1) + @Desc("The thickness in blocks of the ARCH legs and spanning curve.") + private int archThickness = 3; + + @MinNumber(0) + @MaxNumber(1) + @Desc("How strongly an ARCH varies its leg steepness, crown position, depth bow and tube width between deterministic variants. 0 is symmetric and 1 is highly organic.") + private double archAsymmetry = 0.35; + + @MinNumber(2) + @MaxNumber(12) + @Desc("How many separate columns make up a BASALT_COLUMN cluster. Each column gets a randomized height around the formation height range.") + private int basaltColumns = 5; + + @MinNumber(1) + @Desc("The radius (half-width) in blocks of each individual column in a BASALT_COLUMN cluster.") + private int basaltColumnRadius = 1; + + @MinNumber(0) + @MaxNumber(1) + @Desc("How much the heights of individual BASALT_COLUMN columns vary from each other, from 0 (all equal) to 1 (highly varied).") + private double basaltHeightVariance = 0.45; + + @MinNumber(1) + @MaxNumber(12) + @Desc("How many tapered summits crown an ICEBERG formation.") + private int icebergPeaks = 3; + + @MinNumber(2) + @MaxNumber(8) + @Desc("How many separated shards form a FISSURE.") + private int fractureCount = 3; + + @MinNumber(1) + @MaxNumber(16) + @Desc("Open-air separation in blocks between the shards of a FISSURE.") + private int fractureSeparation = 2; + + @MinNumber(0.25) + @MaxNumber(6) + @Desc("How many complete turns a SPIRAL makes from base to tip.") + private double spiralTurns = 1.5; + + @MinNumber(1) + @MaxNumber(32) + @Desc("The starting distance in blocks between a SPIRAL and its open center.") + private int spiralRadius = 4; + + @MinNumber(1) + @MaxNumber(8) + @Desc("The radius in blocks of the tube swept along a SPIRAL.") + private int spiralThickness = 2; + + @MinNumber(1) + @MaxNumber(32) + @Desc("How far the hooked arm of an OVERHANG projects from its base.") + private int overhangReach = 8; + + @MinNumber(0) + @MaxNumber(16) + @Desc("How many blocks the tip of an OVERHANG curls downward.") + private int overhangDrop = 3; + + public KList getVariantObjects(IrisData data) { + return variantCache.aquire(() -> { + KList baked = new KList<>(); + int count = Math.max(1, variants); + + for (int i = 0; i < count; i++) { + IrisObject object = FormationGenerator.generate(this, i, new RNG(seed + (i * 7919L)), data); + if (object == null || object.getBlocks().isEmpty()) { + continue; + } + object.setLoadKey("procedural/" + name + "#" + i); + object.setLoader(data); + baked.add(object); + } + + return baked; + }); + } + + public IrisObject getVariantObject(IrisData data, RNG rng) { + KList baked = getVariantObjects(data); + if (baked == null || baked.isEmpty()) { + return null; + } + return baked.get(rng.i(baked.size())); + } + + public IrisObjectPlacement asPlacement() { + IrisObjectPlacement placement = new IrisObjectPlacement(); + placement.setMode(mode); + placement.setRotation(rotation); + placement.setClamp(clamp); + placement.setCarvingSupport(carvingSupport); + placement.setSurfaceSupportBuffer(surfaceSupportBuffer); + placement.setUnderwater(underwater); + placement.setTranslate(translate); + placement.setStiltSettings(stiltSettings); + placement.setVacuumSettings(vacuumSettings); + placement.setChance(chance); + placement.setDensity(density); + return placement; + } + + public boolean isPlausible() { + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFormationForm.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFormationForm.java new file mode 100644 index 000000000..ce0b6eae8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFormationForm.java @@ -0,0 +1,45 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("The overall silhouette of a procedural natural formation. Each form drives a distinct sculpting routine in the formation generator.") +public enum IrisFormationForm { + @Desc("A tall, slender needle of rock that tapers smoothly to a point at the top (a sharp pinnacle or stone spire).") + SPIRE, + @Desc("A column with a pinched, eroded waist and a wide overhanging caprock balanced on top (the classic desert hoodoo / mushroom rock).") + HOODOO, + @Desc("Two stout legs joined by a spanning curved bridge of rock overhead (a natural stone arch).") + ARCH, + @Desc("A chunky, blocky stack that tapers gently as it rises, like an isolated pillar of rock standing in water (a sea stack).") + SEA_STACK, + @Desc("A rounded, lumpy boulder formed from a noise-perturbed ellipsoid that sits low on the terrain.") + BOULDER, + @Desc("A tightly packed cluster of several vertical, near-hexagonal columns of varying height (a basalt column formation / giant's causeway).") + BASALT_COLUMN, + @Desc("A broad, faceted mound crowned by several irregular tapered peaks, suitable for icebergs and glacial outcrops.") + ICEBERG, + @Desc("Several separated, outward-leaning shards divided by open cracks.") + FISSURE, + @Desc("A tapered tube swept around a tightening helix, leaving open air through its curled center.") + SPIRAL, + @Desc("A grounded pillar that bends into a long hooked cantilever and curls downward at its tip.") + OVERHANG +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFormationProfile.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFormationProfile.java new file mode 100644 index 000000000..f252c298c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFormationProfile.java @@ -0,0 +1,35 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("A width-shaping function evaluated over normalized height (0 at the base, 1 at the top). Controls how the formation radius scales between baseWidth at the bottom and topWidth at the top.") +public enum IrisFormationProfile { + @Desc("Same radius at every height (a straight cylinder of constant baseWidth).") + CONSTANT, + @Desc("Radius ramps straight from baseWidth at the bottom to topWidth at the top.") + LINEAR, + @Desc("Radius shrinks smoothly toward a point as height increases (an ease that drives the SPIRE needle and gentle SEA_STACK taper).") + TAPER, + @Desc("Radius pinches to a narrow waist near profileWaist then widens again toward the top (the eroded waist used by HOODOO columns).") + PARABOLIC, + @Desc("Radius bulges out to its maximum in the middle of the height range and narrows at both ends (a barrel / bulged column).") + BULGE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFungus.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFungus.java new file mode 100644 index 000000000..d6613b5b3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFungus.java @@ -0,0 +1,251 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.fungi.FungusGenerator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("fungus") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A single procedurally generated fungus (giant mushroom or shelf fungus). Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file. The fungus is built as a stem column with a cap dome grown on top, mirroring the trunk-and-canopy model used by procedural trees.") +@Data +public class IrisFungus implements IrisProceduralPlacement { + private final transient AtomicCache> variantCache = new AtomicCache<>(); + + @Desc("A human readable name used in logs and as the variant load key.") + private String name = "fungus"; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance per chunk for this fungus to attempt placement. Use density for multiple per chunk.") + private double chance = 0.4; + + @MinNumber(1) + @Desc("If the chance check passes, attempt this many placements in the chunk.") + private int density = 1; + + @MinNumber(1) + @MaxNumber(64) + @Desc("How many distinct variants to pre-bake for this fungus. Higher means more variety at a small memory cost.") + private int variants = 6; + + @Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.") + private long seed = 1337; + + @Desc("The placement mode used to anchor the fungus to the terrain.") + private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT; + + @Desc("Rotate this fungus's placement.") + private IrisObjectRotation rotation = new IrisObjectRotation(); + + @Desc("Limit the max or min height of placement.") + private IrisObjectLimit clamp = new IrisObjectLimit(); + + @Desc("Whether this fungus may place on the terrain surface, under carvings, or both.") + private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY; + + @Desc("If true, the fungus anchors on the terrain height ignoring the water surface.") + private boolean underwater = false; + + @Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.") + private IrisObjectTranslate translate = new IrisObjectTranslate(); + + @Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).") + private IrisStiltSettings stiltSettings; + + @Desc("Settings for the vacuum place modes (VACUUM, VACUUM_HIGH, VACUUM_FAST, VACUUM_ORGANIC).") + private IrisVacuumSettings vacuumSettings; + + @Required + @Desc("The stem block, e.g. minecraft:mushroom_stem. Ignored when stemPalette is set.") + @RegistryListBlockType + private String stem = "minecraft:mushroom_stem"; + + @Desc("A noise-driven palette for the stem. When set this overrides the single stem block, letting the stem mix blocks by noise.") + private IrisMaterialPalette stemPalette = null; + + @Required + @Desc("The cap block, e.g. minecraft:red_mushroom_block. Ignored when capPalette is set.") + @RegistryListBlockType + private String cap = "minecraft:red_mushroom_block"; + + @Desc("A noise-driven palette for the cap. When set this overrides the single cap block, letting the cap mix blocks by noise.") + private IrisMaterialPalette capPalette = null; + + @MinNumber(1) + @Desc("Minimum total stem height in blocks. Variants spread between this and stemHeightMax.") + private int stemHeightMin = 5; + + @MinNumber(1) + @Desc("Maximum total stem height in blocks. Variants spread between stemHeightMin and this.") + private int stemHeightMax = 9; + + @MinNumber(1) + @MaxNumber(3) + @Desc("Base stem width in blocks. 1 is a single column, 2 is a 2x2 stem, 3 is a chunky 3x3 trunk.") + private int stemWidth = 1; + + @Desc("How far the stem leans away from vertical, in degrees. 0 is perfectly upright; small values give an organic tilt.") + private double stemCurve = 0; + + @Desc("Compass direction in degrees the stem leans toward when stemCurve is non-zero.") + private double stemLeanAzimuth = 0; + + @MinNumber(0) + @Desc("Amplitude in blocks of a gentle sideways wave applied up the stem so it is not a perfectly straight ruler.") + private double stemWaveAmplitude = 0.4; + + @MinNumber(0.0001) + @Desc("The number of full sine wobbles applied over the stem height for the stem wave.") + private double stemWavePeriods = 1; + + @Desc("The silhouette of the cap (dome, flat umbrella, funnel, cone, or wide shallow slab).") + private IrisFungusCapShape capShape = IrisFungusCapShape.DOME; + + @MinNumber(1) + @Desc("Minimum cap radius in blocks (measured from the cap center to its rim).") + private int capRadiusMin = 3; + + @MinNumber(1) + @Desc("Maximum cap radius in blocks (measured from the cap center to its rim).") + private int capRadiusMax = 5; + + @MinNumber(1) + @MaxNumber(3) + @Desc("How many blocks thick the cap shell is. 1 is a thin skin, 3 is a fleshy slab.") + private int capThickness = 1; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Vertical flatten factor for the cap. 0 leaves the cap at full height, 1 squashes it flat into a disc.") + private double capSquish = 0.4; + + @MinNumber(0) + @Desc("How far the cap rim droops downward, in degrees, curling the edge of the cap toward the ground.") + private double capDroop = 20; + + @MinNumber(0) + @Desc("How far the cap radius extends past the stem before the rim begins, in blocks. Larger values make the cap overhang the stem more.") + private double capOverhang = 2; + + @Desc("Optional block forming the gill layer on the underside of the cap (gills or a glow layer), e.g. minecraft:brown_mushroom_block or minecraft:shroomlight. Ignored when gillPalette is set or left null.") + @RegistryListBlockType + private String gillBlock = null; + + @Desc("A noise-driven palette for the underside gill layer. When set this overrides the single gillBlock.") + private IrisMaterialPalette gillPalette = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The fraction of underside cap blocks replaced by the gill block when a gill block or palette is set.") + private double gillChance = 0.85; + + @Desc("Optional block speckled across the top of the cap by noise (white toadstool dots, warts, glowing spots), e.g. minecraft:bone_block or minecraft:white_concrete. Ignored when spotPalette is set or left null.") + @RegistryListBlockType + private String spotBlock = null; + + @Desc("A noise-driven palette for the cap top spots. When set this overrides the single spotBlock.") + private IrisMaterialPalette spotPalette = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The fraction of top cap blocks replaced by the spot block, selected by value noise so spots cluster naturally.") + private double spotChance = 0.18; + + @Desc("If true, generate a bracket / shelf fungus instead of an upright mushroom: a small flat fan that grows sideways off a very short or absent stem, like a polypore clinging to a trunk.") + private boolean shelf = false; + + @MinNumber(1) + @Desc("The radius in blocks of the sideways fan when shelf is true.") + private int shelfRadius = 3; + + public KList getVariantObjects(IrisData data) { + return variantCache.aquire(() -> { + KList baked = new KList<>(); + int count = Math.max(1, variants); + int lo = Math.min(stemHeightMin, stemHeightMax); + int hi = Math.max(stemHeightMin, stemHeightMax); + RNG heightRng = new RNG(seed); + + for (int i = 0; i < count; i++) { + int height; + if (count == 1 || lo == hi) { + height = heightRng.i(lo, hi + 1); + } else { + double step = (hi - lo) / (double) (count - 1); + double base = lo + step * i; + double jitter = heightRng.d(-step * 0.3, step * 0.3); + height = (int) Math.round(Math.max(lo, Math.min(hi, base + jitter))); + } + + IrisObject object = FungusGenerator.generate(this, Math.max(1, height), new RNG(seed + (i * 7919L)), data); + if (object == null || object.getBlocks().isEmpty()) { + continue; + } + object.setLoadKey("procedural/" + name + "#" + i); + object.setLoader(data); + baked.add(object); + } + + return baked; + }); + } + + public IrisObject getVariantObject(IrisData data, RNG rng) { + KList baked = getVariantObjects(data); + if (baked == null || baked.isEmpty()) { + return null; + } + return baked.get(rng.i(baked.size())); + } + + public IrisObjectPlacement asPlacement() { + IrisObjectPlacement placement = new IrisObjectPlacement(); + placement.setMode(mode); + placement.setRotation(rotation); + placement.setClamp(clamp); + placement.setCarvingSupport(carvingSupport); + placement.setUnderwater(underwater); + placement.setTranslate(translate); + placement.setStiltSettings(stiltSettings); + placement.setVacuumSettings(vacuumSettings); + placement.setChance(chance); + placement.setDensity(density); + return placement; + } + + public boolean isPlausible() { + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFungusCapShape.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFungusCapShape.java new file mode 100644 index 000000000..194b3baab --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFungusCapShape.java @@ -0,0 +1,35 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("The silhouette of the mushroom cap. Each shape drives how the cap radius and thickness vary from its apex down to the rim, giving the fungus a distinct profile.") +public enum IrisFungusCapShape { + @Desc("A rounded hemisphere cap that bulges up and curls slightly under at the rim, the classic red toadstool dome.") + DOME, + @Desc("A wide, nearly flat umbrella that holds its full radius almost to the edge before dropping, the brown-mushroom plate look.") + FLAT, + @Desc("An upturned bowl whose rim lifts higher than its center, cupping upward like a chanterelle or funnel mushroom.") + FUNNEL, + @Desc("A tall pointed cone that tapers from a broad base to a sharp apex, the witch-hat parasol silhouette.") + CONICAL, + @Desc("A very wide but shallow slab, a low overhanging shelf-like roof that spreads far past the stem with minimal vertical rise.") + FLAT_WIDE +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java similarity index 77% rename from core/src/main/java/com/volmit/iris/engine/object/IrisGenerator.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java index 11bc7fa75..f85726986 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java @@ -16,17 +16,19 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CellGenerator; -import com.volmit.iris.util.plugin.VolmitSender; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CellGenerator; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -99,48 +101,6 @@ public class IrisGenerator extends IrisRegistrant { return cellGen.aquire(() -> new CellGenerator(new RNG(seed + 239466))); } - public T fitRarity(KList b, long superSeed, double rx, double rz) { - if (b.size() == 0) { - return null; - } - - if (b.size() == 1) { - return b.get(0); - } - - KList rarityMapped = new KList<>(); - boolean o = false; - int max = 1; - for (T i : b) { - if (i.getRarity() > max) { - max = i.getRarity(); - } - } - - max++; - - for (T i : b) { - for (int j = 0; j < max - i.getRarity(); j++) { - //noinspection AssignmentUsedAsCondition - if (o = !o) { - rarityMapped.add(i); - } else { - rarityMapped.add(0, i); - } - } - } - - if (rarityMapped.size() == 1) { - return rarityMapped.get(0); - } - - if (rarityMapped.isEmpty()) { - throw new RuntimeException("BAD RARITY MAP! RELATED TO: " + b.toString(", or possibly ")); - } - - return fit(rarityMapped, superSeed, rx, rz); - } - public T fit(T[] v, long superSeed, double rx, double rz) { if (v.length == 0) { return null; @@ -212,21 +172,23 @@ public class IrisGenerator extends IrisRegistrant { int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ); double h = multiplicitive ? 1 : 0; double tp = 0; + double sampleX = (rx + offsetX) / zoom; + double sampleZ = (rz + offsetZ) / zoom; if (composite.size() == 1) { if (multiplicitive) { - h *= composite.get(0).getNoise(getSeed() + superSeed + hc, (rx + offsetX) / zoom, (rz + offsetZ) / zoom, getLoader()); + h *= composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader()); } else { tp += composite.get(0).getOpacity(); - h += composite.get(0).getNoise(getSeed() + superSeed + hc, (rx + offsetX) / zoom, (rz + offsetZ) / zoom, getLoader()); + h += composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader()); } } else { for (IrisNoiseGenerator i : composite) { if (multiplicitive) { - h *= i.getNoise(getSeed() + superSeed + hc, (rx + offsetX) / zoom, (rz + offsetZ) / zoom, getLoader()); + h *= i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader()); } else { tp += i.getOpacity(); - h += i.getNoise(getSeed() + superSeed + hc, (rx + offsetX) / zoom, (rz + offsetZ) / zoom, getLoader()); + h += i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader()); } } } @@ -245,7 +207,9 @@ public class IrisGenerator extends IrisRegistrant { public double cell(double rx, double rz, double v, double superSeed) { getCellGenerator(getSeed() + 46222).setShuffle(getCellFractureShuffle()); - return getCellGenerator(getSeed() + 46222).getDistance(rx / getCellFractureZoom(), rz / getCellFractureZoom()) > getCellPercentSize() ? (v * getCellFractureHeight()) : v; + double fractureX = rx / getCellFractureZoom(); + double fractureZ = rz / getCellFractureZoom(); + return getCellGenerator(getSeed() + 46222).getDistance(fractureX, fractureZ) > getCellPercentSize() ? (v * getCellFractureHeight()) : v; } private boolean hasCellCracks() { @@ -254,7 +218,9 @@ public class IrisGenerator extends IrisRegistrant { public double getCliffHeight(double rx, double rz, double superSeed) { int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ); - double h = cliffHeightGenerator.getNoise((long) (getSeed() + superSeed + hc), (rx + offsetX) / zoom, (rz + offsetZ) / zoom, getLoader()); + double sampleX = (rx + offsetX) / zoom; + double sampleZ = (rz + offsetZ) / zoom; + double h = cliffHeightGenerator.getNoise((long) (getSeed() + superSeed + hc), sampleX, sampleZ, getLoader()); return IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, h); } @@ -287,9 +253,4 @@ public class IrisGenerator extends IrisRegistrant { public String getTypeName() { return "Generator"; } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java new file mode 100644 index 000000000..6da231aff --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java @@ -0,0 +1,280 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.LazyBoundedCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.project.noise.ExpressionNoise; +import art.arcane.iris.util.project.noise.ImageNoise; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.io.File; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +@Snippet("style") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A gen style") +@Data +public class IrisGeneratorStyle { + private static final int GENERATOR_CACHE_SIZE = 8; + private static final ConcurrentHashMap ACTIVE_CACHE_KEYS = new ConcurrentHashMap<>(); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final transient LazyBoundedCache generatorCache = + new LazyBoundedCache<>(GENERATOR_CACHE_SIZE); + @Desc("The base noise style. Used when neither expression nor imageMap is set; a failed expression also falls back to this style.") + private NoiseStyle style = NoiseStyle.FLAT; + + @Desc("If set above 0, this style will be cellularized") + private double cellularFrequency = 0; + + @Desc("Cell zooms") + private double cellularZoom = 1; + @MinNumber(0.00001) + @Desc("The zoom of this style") + private double zoom = 1; + @Desc("Instead of using the style property, use a custom expression to represent this style.") + @RegistryListResource(IrisExpression.class) + private String expression = null; + @Desc("Use an Image map instead of a generated value") + private IrisImageMap imageMap = null; + @MinNumber(0.00001) + @Desc("The Output multiplier. Only used if parent is fracture.") + private double multiplier = 1; + @Desc("Apply a generator to the coordinate field fed into this parent generator. I.e. Distort your generator with another generator.") + private IrisGeneratorStyle fracture = null; + @MinNumber(0.01562) + @MaxNumber(64) + @Desc("The exponent") + private double exponent = 1; + @MinNumber(0) + @MaxNumber(8192) + @Desc("If the cache size is set above 0, this generator will be cached") + private int cacheSize = 0; + + public IrisGeneratorStyle(NoiseStyle s) { + this.style = s; + } + + public IrisGeneratorStyle zoomed(double z) { + this.zoom = z; + return this; + } + + public CNG createNoCache(RNG rng, IrisData data) { + return createNoCache(rng, data, false, 0, false, resolveEngine(data)); + } + + public CNG createForPrebake(RNG rng, IrisData data, int fallbackCacheSize) { + return createNoCache(rng, data, false, Math.max(0, fallbackCacheSize), true, resolveEngine(data)); + } + + + private int imageMapHash() { + if (imageMap == null) { + return 0; + } + + return Objects.hash(imageMap.getImage(), + imageMap.getCoordinateScale(), + imageMap.getInterpolationMethod(), + imageMap.getChannel(), + imageMap.isInverted(), + imageMap.isTiled(), + imageMap.isCentered()); + } + + private int hash() { + return Objects.hash(expression, imageMapHash(), multiplier, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style); + } + + public int prebakeSignature() { + return hash(); + } + + private String cachePrefix(RNG rng, int effectiveCacheSize, long engineStamp) { + return "style-" + Integer.toUnsignedString(hash()) + + "-seed-" + Long.toUnsignedString(rng.getSeed()) + + "-eng-" + Long.toUnsignedString(engineStamp) + + "-sz-" + effectiveCacheSize + + "-src-"; + } + + private String cacheKey(RNG rng, long sourceStamp, int effectiveCacheSize, long engineStamp) { + return cachePrefix(rng, effectiveCacheSize, engineStamp) + Long.toUnsignedString(sourceStamp); + } + + private void clearStaleCacheEntries(IrisData data, String prefix, String key) { + File cacheFolder = new File(data.getDataFolder(), ".cache"); + String cacheFolderKey = cacheFolder.getAbsolutePath() + File.separator + prefix; + String previousKey = ACTIVE_CACHE_KEYS.put(cacheFolderKey, key); + if (key.equals(previousKey)) { + return; + } + File[] files = cacheFolder.listFiles((dir, name) -> name.endsWith(".cnm") && name.startsWith(prefix)); + if (files == null) { + return; + } + + String keepFile = key + ".cnm"; + for (File file : files) { + if (keepFile.equals(file.getName())) { + continue; + } + + if (!file.delete()) { + IrisLogging.debug("Unable to delete stale noise cache " + file.getName()); + } + } + } + + public CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached) { + return createNoCache(rng, data, actuallyCached, 0, false, resolveEngine(data)); + } + + private CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached, int fallbackCacheSize, + boolean quietCacheLog, Engine engine) { + CNG cng = null; + long sourceStamp = 0L; + if (getExpression() != null) { + IrisExpression e = data.getExpressionLoader().load(getExpression()); + if (e != null) { + cng = new CNG(rng, new ExpressionNoise(rng, e), 1D, 1).bake(); + sourceStamp = Integer.toUnsignedLong(Objects.hash(getExpression(), + e.getLoadFile() == null ? 0L : e.getLoadFile().lastModified())); + } + } else if (getImageMap() != null) { + cng = new CNG(rng, new ImageNoise(data, getImageMap()), 1D, 1).bake(); + sourceStamp = Integer.toUnsignedLong(imageMapHash()); + } + + if (cng == null) { + cng = (style != null ? style : NoiseStyle.FLAT).create(rng).bake(); + } + + cng = cng.scale(1D / zoom).pow(exponent).bake(); + + if (fracture != null) { + cng.fractureWith(fracture.createNoCache(rng.nextParallelRNG(2934), data, false, + fallbackCacheSize, quietCacheLog, engine), fracture.getMultiplier()); + } + + if (cellularFrequency > 0) { + cng = cng.cellularize(rng.nextParallelRNG(884466), cellularFrequency).scale(1D / cellularZoom).bake(); + } + + int effectiveCacheSize = cacheSize > 0 ? cacheSize : Math.max(0, fallbackCacheSize); + if (effectiveCacheSize > 0) { + long engineStamp = engineStamp(engine); + String key = cacheKey(rng, sourceStamp, effectiveCacheSize, engineStamp); + clearStaleCacheEntries(data, cachePrefix(rng, effectiveCacheSize, engineStamp), key); + cng = cng.cached(effectiveCacheSize, key, data.getDataFolder(), quietCacheLog); + } + + return cng; + } + + public double warp(RNG rng, IrisData data, double value, double... coords) { + return create(rng, data).noise(coords) + value; + } + + public CNG create(RNG rng, IrisData data) { + return create(rng, data, resolveEngine(data)); + } + + public CNG create(RNG rng, IrisData data, Engine engine) { + GeneratorCacheKey key = new GeneratorCacheKey(data, engine, rng.getSeed()); + return generatorCache.computeIfAbsent(key, + ignored -> createNoCache(rng, data, true, 0, false, engine)); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isFlat() { + return style == null || style.equals(NoiseStyle.FLAT); + } + + public double getMaxFractureDistance() { + return multiplier; + } + + private long engineStamp(Engine engine) { + if (engine == null) { + return 0L; + } + return Integer.toUnsignedLong(Objects.hash( + engine.getSeedManager().getSeed(), + engine.getMinHeight(), + engine.getMaxHeight(), + engine.getDimension().getLoadKey(), + engine.getDimension().getFluidHeight() + )); + } + + private Engine resolveEngine(IrisData data) { + return data == null ? null : data.getEngine(); + } + + private static final class GeneratorCacheKey { + private final IrisData data; + private final Engine engine; + private final long seed; + + private GeneratorCacheKey(IrisData data, Engine engine, long seed) { + this.data = data; + this.engine = engine; + this.seed = seed; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof GeneratorCacheKey key)) { + return false; + } + return data == key.data && engine == key.engine && seed == key.seed; + } + + @Override + public int hashCode() { + int result = System.identityHashCode(data); + result = 31 * result + System.identityHashCode(engine); + return 31 * result + Long.hashCode(seed); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisImage.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java similarity index 76% rename from core/src/main/java/com/volmit/iris/engine/object/IrisImage.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisImage.java index 49afae0e4..c16790273 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisImage.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java @@ -16,18 +16,13 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; +import art.arcane.iris.core.loader.IrisRegistrant; -import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; +import java.util.Objects; public class IrisImage extends IrisRegistrant { private final BufferedImage image; @@ -37,7 +32,7 @@ public class IrisImage extends IrisRegistrant { } public IrisImage(BufferedImage image) { - this.image = image; + this.image = Objects.requireNonNull(image, "IrisImage requires a decoded image (the source file was unreadable or not an image)"); } public int getWidth() { @@ -117,26 +112,4 @@ public class IrisImage extends IrisRegistrant { return "Image"; } - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } - - public void writeDebug(IrisImageChannel channel) { - - - try { - File at = new File(getLoadFile().getParentFile(), "debug-see-" + getLoadFile().getName()); - BufferedImage b = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); - for (int i = 0; i < getWidth(); i++) { - for (int j = 0; j < getHeight(); j++) { - b.setRGB(i, j, Color.getHSBColor(0, 0, (float) getValue(channel, i, j)).getRGB()); - } - } - ImageIO.write(b, "png", at); - Iris.warn("Debug image written to " + at.getPath() + " for channel " + channel.name()); - } catch (IOException e) { - e.printStackTrace(); - } - } } diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisImageChannel.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImageChannel.java similarity index 95% rename from core/src/main/java/com/volmit/iris/engine/object/IrisImageChannel.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisImageChannel.java index dd5405fa7..dd385bfea 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisImageChannel.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImageChannel.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("Determines a derived channel of an image to read") public enum IrisImageChannel { diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisImageMap.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImageMap.java similarity index 80% rename from core/src/main/java/com/volmit/iris/engine/object/IrisImageMap.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisImageMap.java index ba48d1899..8638a1fb5 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisImageMap.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImageMap.java @@ -16,17 +16,17 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.RegistryListResource; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.interpolation.IrisInterpolation; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -67,7 +67,9 @@ public class IrisImageMap { public double getNoise(IrisData data, int x, int z) { IrisImage i = imageCache.aquire(() -> data.getImageLoader().load(image)); if (i == null) { - Iris.error("NULL IMAGE FOR " + image); + // Reached per sample for as long as the pack points at an image that will not load, so the + // pack key is what the operator needs and one statement of it is enough. + IrisLogging.warnOnce("image-map:" + image, "No image %s in the pack; that noise samples as zero.", image); return 0; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisImportedFeatureControl.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedFeatureControl.java new file mode 100644 index 000000000..dadfd23be --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedFeatureControl.java @@ -0,0 +1,106 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Objects; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Controls native vanilla, mod, and ingested datapack PLACED FEATURE generation for this dimension (set as the dimension's 'importedFeatures' field). Placed features are ores, trees, plants, springs, geodes and every other decoration entry a biome declares. This is OFF by default: with 'enabled' false Iris generates exactly the terrain it always has and no native feature runs. With it true, every placed feature the biome's vanilla derivative declares runs over Iris terrain, in vanilla step order, on the same worldgen thread vanilla uses. Carvers are never imported - Iris has no NoiseGeneratorSettings, so there is nothing for a carver to carve against. Family matching on 'disabled' uses namespace, slash, or underscore boundaries, so 'minecraft:ore' covers every vanilla ore feature without matching unrelated names.") +@Data +public class IrisImportedFeatureControl { + @Desc("Master switch. False (the default) means no native placed feature generates and chunk output is identical to a pack without this block at all. True runs the vanilla decoration feature pass over Iris terrain.") + private boolean enabled = false; + + @ArrayType(type = String.class, min = 1) + @Desc("Placed feature keys to deny explicitly, e.g. 'minecraft:ore_diamond'. A namespace:path prefix also matches, so 'minecraft:ore' denies every vanilla ore and 'minecraft:trees' denies every tree placement. Every key not matched here generates while 'enabled' is true.") + private KList disabled = new KList<>(); + + @ArrayType(type = IrisDecorationStep.class, min = 1) + @Desc("Restrict feature generation to these decoration steps only. Empty (the default) means every step is eligible. Use this to import ores without importing vegetation: set it to UNDERGROUND_ORES.") + private KList steps = new KList<>(); + + @ArrayType(type = IrisDecorationStep.class, min = 1) + @Desc("Decoration steps to deny. Applied after 'steps', so a step listed here never generates even if 'steps' allows it. VEGETAL_DECORATION is the usual entry for packs that grow their own trees.") + private KList disabledSteps = new KList<>(); + + /** + * True when this dimension wants the native feature pass at all. Every other accessor on this class is + * meaningless while this is false, and the platform must not build any feature machinery. + */ + public boolean shouldGenerateFeatures() { + return enabled; + } + + public boolean shouldGenerateStep(IrisDecorationStep step) { + if (!enabled) { + return false; + } + KList allowed = Objects.requireNonNull( + steps, "importedFeatures.steps must not be null"); + KList denied = Objects.requireNonNull( + disabledSteps, "importedFeatures.disabledSteps must not be null"); + if (step == null) { + // A step this Iris build does not know about (newer Minecraft): allow it unless the pack + // narrowed generation to an explicit step list. + return allowed.isEmpty(); + } + if (!allowed.isEmpty() && !allowed.contains(step)) { + return false; + } + return !denied.contains(step); + } + + public boolean shouldGenerate(String placedFeatureKey) { + return resolve(placedFeatureKey, null) == NativeFeatureGenerationStatus.GENERATE_NATIVE; + } + + /** + * Resolves one placed feature against this control. A null step skips the step gate, which is what the + * key-only query does. + */ + public NativeFeatureGenerationStatus resolve(String placedFeatureKey, IrisDecorationStep step) { + if (!enabled) { + return NativeFeatureGenerationStatus.FEATURES_DISABLED; + } + if (placedFeatureKey == null || placedFeatureKey.isBlank()) { + return NativeFeatureGenerationStatus.INVALID_REGISTRY_KEY; + } + if (step != null && !shouldGenerateStep(step)) { + return NativeFeatureGenerationStatus.STEP_DISABLED; + } + KList deniedKeys = Objects.requireNonNull( + disabled, "importedFeatures.disabled must not be null"); + for (String entry : deniedKeys) { + if (IrisImportedStructureControl.matchesKey(entry, placedFeatureKey)) { + return NativeFeatureGenerationStatus.DISABLED_BY_PACK; + } + } + return NativeFeatureGenerationStatus.GENERATE_NATIVE; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java new file mode 100644 index 000000000..7d43b31bd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java @@ -0,0 +1,181 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Locale; +import java.util.Objects; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Controls native vanilla, mod, and ingested datapack structure generation for this dimension (set as the dimension's 'importedStructures' field). Every registered structure generates through its native placement unless its key matches 'disabled', equals a key in 'disabledExact', or a viable dimension-level Iris placement explicitly replaces its source. Family matching uses namespace, slash, or underscore boundaries, while exact matching compares normalized complete keys only. Run '/iris structure list ' to dump every valid key. Only affects newly generated chunks and is separate from Iris structure placements.") +@Data +public class IrisImportedStructureControl { + @ArrayType(type = String.class, min = 1) + @RegistryListVanillaStructure(prefixes = true) + @Desc("Structure keys to deny explicitly, e.g. 'minecraft:stronghold'. A namespace:path prefix also matches, so 'minecraft:village' disables every village variant and 'minecraft:ruined_portal' disables every ruined portal. Every key not matched here remains enabled.") + private KList disabled = new KList<>(); + + @ArrayType(type = String.class, min = 1) + @RegistryListVanillaStructure(prefixes = false) + @Desc("Exact structure keys to deny after trimming and case normalization. Unlike 'disabled', entries never match key families, so 'minecraft:ruined_portal' does not disable 'minecraft:ruined_portal_nether'.") + private KList disabledExact = new KList<>(); + + @MinNumber(-512) + @MaxNumber(512) + @Desc("Vertical block offset applied only to UNDERGROUND vanilla structures (the UNDERGROUND_STRUCTURES, UNDERGROUND_DECORATION, and STRONGHOLDS generation steps: strongholds, trial chambers, mineshafts, ancient cities, etc.). Surface structures (villages, outposts, etc.) are never shifted. Use a negative value to push deep structures lower when your dimension's sea/terrain level differs from vanilla's (e.g. -64 if you lowered the fluid height to 0). 0 = no shift.") + private int undergroundYShift = 0; + + @Desc("Controls whether ingested datapacks may replace minecraft-namespaced structure definitions, sets, pools, and templates. When false, those overrides are stripped from installed datapack copies so vanilla definitions stay intact. Non-minecraft structures from datapacks and mods remain governed by disabled because namespace alone cannot identify their origin. Resolved globally across loaded packs: if any dimension sets this false, minecraft-namespaced overrides are stripped from every installed datapack copy.") + private boolean datapackOverrides = true; + + @ArrayType(type = IrisStructureSetFrequencyOverride.class, min = 1) + @Desc("Exact registered structure-set frequency overrides for native generation. The last entry for a normalized structure-set key wins. These do not convert native structures into Iris placements and affect only newly generated chunks.") + private KList frequencyOverrides = new KList<>(); + + @ArrayType(type = IrisVanillaStructureAdjustment.class, min = 1) + @Desc("Per-structure adjustments applied to vanilla, mod, and datapack structures that still generate natively. Vertical shifts from every matching entry stack. A matching preserveSourceY option disables Iris burial repositioning for that structure. The last matching entry with stilt settings controls foundation columns, and likewise for terrain and yBand settings. A structure suppressed by an Iris placement is unaffected.") + private KList adjustments = new KList<>(); + + public boolean shouldGenerate(String key) { + return key != null && !key.isBlank() + && !matches(disabled, key) + && !matchesExact(disabledExact, key); + } + + public IrisNativeStructureDecision resolve(String key, boolean undergroundStep) { + KList activeAdjustments = Objects.requireNonNull( + adjustments, "importedStructures.adjustments must not be null"); + int y = undergroundStep ? undergroundYShift : 0; + boolean preserveSourceY = false; + IrisStructureStiltSettings stilt = null; + IrisStructureTerrain terrain = null; + IrisStructureYBand yBand = null; + for (IrisVanillaStructureAdjustment adjustment : activeAdjustments) { + if (adjustment != null && adjustment.matches(key)) { + y += adjustment.getYShift(); + preserveSourceY |= adjustment.isPreserveSourceY(); + if (adjustment.getStilt() != null) { + stilt = adjustment.getStilt(); + } + if (adjustment.getTerrain() != null) { + terrain = adjustment.getTerrain(); + } + if (adjustment.getYBand() != null) { + yBand = adjustment.getYBand(); + } + } + } + return new IrisNativeStructureDecision( + generationStatus(key), y, yBand, preserveSourceY, stilt, terrain); + } + + public double frequencyMultiplier(String structureSetKey) { + KList activeOverrides = Objects.requireNonNull( + frequencyOverrides, "importedStructures.frequencyOverrides must not be null"); + String normalizedKey = normalizeKey(structureSetKey); + if (normalizedKey.isEmpty()) { + return 1D; + } + double multiplier = 1D; + for (IrisStructureSetFrequencyOverride override : activeOverrides) { + if (override != null && normalizeKey(override.getStructureSet()).equals(normalizedKey)) { + multiplier = override.getMultiplier(); + } + } + return multiplier; + } + + public boolean hasFrequencyOverrides() { + return !Objects.requireNonNull( + frequencyOverrides, "importedStructures.frequencyOverrides must not be null").isEmpty(); + } + + private NativeStructureGenerationStatus generationStatus(String key) { + if (key == null || key.isBlank()) { + return NativeStructureGenerationStatus.INVALID_REGISTRY_KEY; + } + if (matches(disabled, key) || matchesExact(disabledExact, key)) { + return NativeStructureGenerationStatus.DISABLED_BY_PACK; + } + return NativeStructureGenerationStatus.GENERATE_NATIVE; + } + + private boolean matches(KList list, String key) { + if (key == null) { + return false; + } + KList activeList = Objects.requireNonNull( + list, "importedStructures.disabled must not be null"); + for (String entry : activeList) { + if (matchesKey(entry, key)) { + return true; + } + } + return false; + } + + private boolean matchesExact(KList list, String key) { + KList activeList = Objects.requireNonNull( + list, "importedStructures.disabledExact must not be null"); + String normalizedKey = normalizeKey(key); + for (String entry : activeList) { + String normalizedEntry = normalizeKey(entry); + if (!normalizedEntry.isEmpty() && normalizedEntry.equals(normalizedKey)) { + return true; + } + } + return false; + } + + static boolean matchesKey(String pattern, String key) { + if (pattern == null || key == null) { + return false; + } + String normalizedPattern = pattern.trim().toLowerCase(Locale.ROOT); + String normalizedKey = key.trim().toLowerCase(Locale.ROOT); + if (normalizedPattern.isEmpty() || !normalizedKey.startsWith(normalizedPattern)) { + return false; + } + if (normalizedKey.length() == normalizedPattern.length()) { + return true; + } + char patternEnd = normalizedPattern.charAt(normalizedPattern.length() - 1); + if (patternEnd == ':' || patternEnd == '/' || patternEnd == '_') { + return true; + } + char boundary = normalizedKey.charAt(normalizedPattern.length()); + return boundary == '/' || boundary == '_'; + } + + private static String normalizeKey(String key) { + return key == null ? "" : key.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java new file mode 100644 index 000000000..e7f7f3f9f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java @@ -0,0 +1,86 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.volmlib.util.function.NoiseProvider; +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.interpolation.NoiseBounds; +import art.arcane.iris.util.project.interpolation.NoiseBoundsProvider; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Configures rotation for iris") +@Data +public class IrisInterpolator { + public static final IrisInterpolator DEFAULT = new IrisInterpolator(); + + @Required + @Desc("The interpolation method when two biomes use different heights but this same generator") + private InterpolationMethod function = InterpolationMethod.BILINEAR_STARCAST_6; + + @Required + @MinNumber(1) + @MaxNumber(8192) + @Desc("The range checked horizontally. Smaller ranges yeild more detail but are not as smooth.") + private double horizontalScale = 7; + + @Override + public int hashCode() { + // Bit-identical to Objects.hash(horizontalScale, function) without the Object[] + Double boxing. + // The exact value is load bearing: it decides HashMap bucket order for the generator maps in + // IrisComplex, and that order fixes the floating point summation order of interpolated heights. + int result = 31 + Double.hashCode(horizontalScale); + return (31 * result) + (function == null ? 0 : function.hashCode()); + } + + @Override + public boolean equals(Object o) { + if (o instanceof IrisInterpolator i) { + return i.getFunction().equals(function) && i.getHorizontalScale() == horizontalScale; + } + + return false; + } + + public double interpolate(double x, double z, NoiseProvider provider) { + return interpolate((int) Math.round(x), (int) Math.round(z), provider); + } + + public double interpolate(int x, int z, NoiseProvider provider) { + return IrisInterpolation.getNoise(getFunction(), x, z, getHorizontalScale(), provider); + } + + public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) { + return interpolateBounds((int) Math.round(x), (int) Math.round(z), provider); + } + + public NoiseBounds interpolateBounds(int x, int z, NoiseBoundsProvider provider) { + return IrisInterpolation.getNoiseBounds(getFunction(), x, z, getHorizontalScale(), provider); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawBranchFailurePolicy.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawBranchFailurePolicy.java new file mode 100644 index 000000000..b9d08ab07 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawBranchFailurePolicy.java @@ -0,0 +1,12 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls how an Iris jigsaw assembly handles an optional connector branch that exhausts its primary pool and direct fallback without attaching a piece.") +public enum IrisJigsawBranchFailurePolicy { + @Desc("Fails the complete assembly when an optional connector branch cannot attach before the maximum depth.") + FAIL_ASSEMBLY, + + @Desc("Ends only the unresolved optional connector branch, matching vanilla jigsaw placement behavior.") + TERMINATE_BRANCH +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawCompatibility.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawCompatibility.java new file mode 100644 index 000000000..3d71fd251 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawCompatibility.java @@ -0,0 +1,12 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Declares the resource compatibility target of an Iris jigsaw structure.") +public enum IrisJigsawCompatibility { + @Desc("Allows Iris-specific connector metadata and placement behavior.") + IRIS_EXTENDED, + + @Desc("Restricts the graph to metadata that can be represented by vanilla jigsaw resources.") + VANILLA_PORTABLE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConfiguration.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConfiguration.java new file mode 100644 index 000000000..bf743d239 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConfiguration.java @@ -0,0 +1,71 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListNativeJigsawPool; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Data +@Desc("Optional overrides applied to a live vanilla, datapack, or modded jigsaw definition before Minecraft assembles it. Omitted values preserve the registered source definition.") +public class IrisJigsawConfiguration { + @Desc("Optional registered template-pool key used instead of the source jigsaw's start pool. Empty preserves the source pool.") + @RegistryListNativeJigsawPool + private String startPool = ""; + + @Desc("Optional start connector name. Empty preserves the source value; the literal NONE removes the source constraint.") + private String startJigsawName = ""; + + @MinNumber(0) + @MaxNumber(20) + @Desc("Maximum jigsaw recursion depth. Null preserves the source value.") + private Integer maxDepth = null; + + @MinNumber(1) + @MaxNumber(128) + @Desc("Exact horizontal block distance allowed from the start. Null preserves the source value.") + private Integer maxDistanceHorizontal = null; + + @MinNumber(1) + @MaxNumber(4064) + @Desc("Exact vertical block distance allowed from the start. Null preserves the source value.") + private Integer maxDistanceVertical = null; + + @Desc("Expansion-hack override. Null preserves the source value.") + private Boolean useExpansionHack = null; + + @Desc("Heightmap projection override. SOURCE preserves the registered value and NONE removes projection.") + private IrisJigsawHeightmap projectStartToHeightmap = IrisJigsawHeightmap.SOURCE; + + @MinNumber(0) + @MaxNumber(Integer.MAX_VALUE) + @Desc("Minimum distance retained above the dimension floor. Null preserves the source value.") + private Integer dimensionPaddingBottom = null; + + @MinNumber(0) + @MaxNumber(Integer.MAX_VALUE) + @Desc("Minimum distance retained below the dimension ceiling. Null preserves the source value.") + private Integer dimensionPaddingTop = null; + + @Desc("Waterlogging override. SOURCE preserves the registered value.") + private IrisJigsawLiquidSettings liquidSettings = IrisJigsawLiquidSettings.SOURCE; + + public boolean hasOverrides() { + return (startPool != null && !startPool.isBlank()) + || (startJigsawName != null && !startJigsawName.isBlank()) + || maxDepth != null + || maxDistanceHorizontal != null + || maxDistanceVertical != null + || useExpansionHack != null + || (projectStartToHeightmap != null && projectStartToHeightmap != IrisJigsawHeightmap.SOURCE) + || dimensionPaddingBottom != null + || dimensionPaddingTop != null + || (liquidSettings != null && liquidSettings != IrisJigsawLiquidSettings.SOURCE); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConnector.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConnector.java new file mode 100644 index 000000000..3c2477950 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawConnector.java @@ -0,0 +1,94 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Objects; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A jigsaw connection point on a piece. When two connectors face each other and their names match, the assembler may join the pieces.") +@Data +public class IrisJigsawConnector { + public static IrisPosition canonicalPlanarPosition(IrisPosition cellSize, IrisDirection direction) { + IrisPosition dimensions = Objects.requireNonNull(cellSize, "Planar jigsaw cell size"); + IrisDirection face = Objects.requireNonNull(direction, "Planar jigsaw connector direction"); + if (dimensions.getX() < 1 || dimensions.getY() < 1 || dimensions.getZ() < 1) { + throw new IllegalArgumentException("Planar jigsaw cell dimensions must be positive"); + } + int centerX = dimensions.getX() / 2; + int centerY = dimensions.getY() / 2; + int centerZ = dimensions.getZ() / 2; + return switch (face) { + case NORTH_NEGATIVE_Z -> new IrisPosition(centerX, centerY, 0); + case EAST_POSITIVE_X -> new IrisPosition(dimensions.getX() - 1, centerY, centerZ); + case SOUTH_POSITIVE_Z -> new IrisPosition(centerX, centerY, dimensions.getZ() - 1); + case WEST_NEGATIVE_X -> new IrisPosition(0, centerY, centerZ); + case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> throw new IllegalArgumentException( + "Planar jigsaw connector direction must be horizontal"); + }; + } + + @Required + @Desc("The position of this connector relative to the piece object's origin (0,0,0 is the lowest-corner of the object).") + private IrisPosition position = new IrisPosition(); + + @Required + @Desc("The direction this connector faces. The connecting piece is placed on this side.") + private IrisDirection direction = IrisDirection.NORTH_NEGATIVE_Z; + + @Required + @Desc("The connector's authored top direction, used to preserve aligned vanilla jigsaw orientation.") + private IrisDirection top = IrisDirection.UP_POSITIVE_Y; + + @Required + @RegistryListResource(IrisJigsawPool.class) + @Desc("The jigsaw pool to draw the connecting piece from.") + private String pool = ""; + + @Desc("The identity exposed by this connector. Another connector can attach when its targetName equals this name, matching Minecraft's jigsaw target-to-name contract.") + private String name = ""; + + @Desc("The name this connector wants to connect to on the other piece.") + private String targetName = ""; + + @Desc("Optional Iris-only connection channel. Matching is exact, case-sensitive, and whitespace-sensitive; empty channels match only other empty channels.") + private String channel = ""; + + @Desc("How the connecting piece may be rotated relative to this connector.") + private JigsawJoint joint = JigsawJoint.ROLLABLE; + + @Required + @Desc("Block state placed where this jigsaw marker was authored after assembly. Used when exporting or capturing Mojang-style jigsaw templates.") + private String finalState = "minecraft:air"; + + @Desc("Signed priority for selecting this connector within its piece. Higher values are processed before lower values; equal priorities preserve authored order.") + private int selectionPriority = 0; + + @Desc("Signed priority assigned to a child piece attached through this connector. Higher-priority child pieces expand first; equal priorities preserve attachment order.") + private int placementPriority = 0; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawHeightmap.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawHeightmap.java new file mode 100644 index 000000000..7648e2b38 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawHeightmap.java @@ -0,0 +1,30 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls whether a configured native jigsaw projects its start onto a Minecraft heightmap.") +public enum IrisJigsawHeightmap { + @Desc("Keeps the registered structure's own heightmap projection exactly as the vanilla, datapack, or mod definition declares it.") + SOURCE, + + @Desc("Removes heightmap projection entirely, so the start piece lands at the structure's literal start height instead of following terrain.") + NONE, + + @Desc("Drops the start piece onto the terrain top counting water as solid, so ocean starts sit on the water surface.") + WORLD_SURFACE_WG, + + @Desc("Identical to WORLD_SURFACE_WG in Iris worlds: the start piece rides the terrain top including water.") + WORLD_SURFACE, + + @Desc("Drops the start piece onto solid terrain, ignoring water, so ocean starts sit on the seabed.") + OCEAN_FLOOR_WG, + + @Desc("Identical to OCEAN_FLOOR_WG in Iris worlds: the start piece sits on solid ground beneath any water.") + OCEAN_FLOOR, + + @Desc("Behaves like WORLD_SURFACE in Iris worlds; the start piece sits on the terrain top including water.") + MOTION_BLOCKING, + + @Desc("Behaves like WORLD_SURFACE in Iris worlds; Iris height queries have no leaf layer to skip.") + MOTION_BLOCKING_NO_LEAVES +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawLiquidSettings.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawLiquidSettings.java new file mode 100644 index 000000000..8863ff7f7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawLiquidSettings.java @@ -0,0 +1,15 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls waterlogging behavior for a configured native jigsaw.") +public enum IrisJigsawLiquidSettings { + @Desc("Keeps the registered structure's own liquid setting; vanilla defaults to applying waterlogging when the definition omits it.") + SOURCE, + + @Desc("Places pieces dry: existing water is displaced and waterloggable blocks stay unwaterlogged, leaving air-filled interiors underwater.") + IGNORE_WATERLOGGING, + + @Desc("Re-applies pre-existing water into placed waterloggable blocks and floods from neighboring sources, so underwater pieces come out waterlogged.") + APPLY_WATERLOGGING +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawMode.java new file mode 100644 index 000000000..0a4182277 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawMode.java @@ -0,0 +1,12 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls the topology used by an Iris jigsaw structure.") +public enum IrisJigsawMode { + @Desc("A cell-aligned planar jigsaw intended for villages, paths, and other two-dimensional layouts.") + PLANAR_JIGSAW, + + @Desc("A freeform spatial connector graph intended for multi-level and three-dimensional structures.") + SPATIAL_JIGSAW +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPiece.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPiece.java new file mode 100644 index 000000000..a8e943bb3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPiece.java @@ -0,0 +1,93 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A jigsaw piece. A piece is a single Iris object plus the connection points (connectors) that let the assembler attach other pieces to it.") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisJigsawPiece extends IrisRegistrant { + @Desc("Optional author-facing name shown by Jigsaw Studio. The resource key is shown when this is blank.") + private String displayName = ""; + + @Required + @RegistryListResource(IrisObject.class) + @Desc("The object (schematic) that makes up this piece.") + private String object = ""; + + @ArrayType(type = IrisJigsawConnector.class, min = 1) + @Desc("The jigsaw connection points on this piece.") + private KList connectors = new KList<>(); + + @Desc("If true, this piece may be rotated around the Y axis when placed so the assembler can match its connectors. Disable for pieces that must keep a fixed orientation.") + private boolean rotatable = true; + + @Desc("If true, this piece's object bounds block overlapping jigsaw pieces. Disable for connector-only scaffolds whose stored volume is intentionally shared with physical pieces.") + private boolean collidable = true; + + @ArrayType(type = String.class, min = 1) + @Desc("Theme keys that may select this piece. An empty list makes the piece available to every selected theme.") + private KList themes = new KList<>(); + + @Desc("Deterministic depth, placement-count, and terminal-role rules for this piece.") + private IrisJigsawPieceRules rules = new IrisJigsawPieceRules(); + + public boolean supportsTheme(String selectedTheme) { + if (themes == null || themes.isEmpty()) { + return true; + } + if (selectedTheme == null || selectedTheme.isBlank()) { + return false; + } + for (String theme : themes) { + if (selectedTheme.equals(theme)) { + return true; + } + } + return false; + } + + public IrisJigsawPieceRules resolvedRules() { + return rules == null ? new IrisJigsawPieceRules() : rules; + } + + @Override + public String getFolderName() { + return "jigsaw-pieces"; + } + + @Override + public String getTypeName() { + return "Jigsaw Piece"; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceEntry.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceEntry.java new file mode 100644 index 000000000..46bd3afca --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceEntry.java @@ -0,0 +1,66 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A weighted reference to a jigsaw piece inside a jigsaw pool.") +@Data +public class IrisJigsawPieceEntry { + @RegistryListResource(IrisJigsawPiece.class) + @Desc("The jigsaw piece this entry refers to. Omit when empty is true.") + private String piece; + + @MinNumber(1) + @Desc("The relative weight of this piece within its pool. Higher weights are chosen more often.") + private int weight = 1; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The independent eligibility chance applied to this exact pool membership before weighted selection. Zero never passes and one always passes.") + private double chance = 1D; + + @Desc("When true, choosing this entry successfully terminates the current branch without placing a piece.") + private boolean empty = false; + + public IrisJigsawPieceEntry(String piece, int weight) { + this(piece, weight, 1D, false); + } + + public boolean requiresChanceRoll() { + return chance > 0D && chance < 1D; + } + + public boolean passesChance(double roll) { + if (!Double.isFinite(roll) || roll < 0D || roll >= 1D) { + throw new IllegalArgumentException("Jigsaw chance roll must be finite and within 0 inclusive and 1 exclusive"); + } + return chance >= 1D || chance > 0D && roll < chance; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceRules.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceRules.java new file mode 100644 index 000000000..0072dc4f2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPieceRules.java @@ -0,0 +1,52 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Deterministic eligibility, placement-count, and terminal-role rules for one jigsaw piece.") +@Data +public class IrisJigsawPieceRules { + @MinNumber(0) + @MaxNumber(30) + @Desc("The shallowest assembly depth at which this piece may be placed. The start piece is depth zero.") + private int minimumDepth = 0; + + @MinNumber(0) + @MaxNumber(30) + @Desc("The deepest assembly depth at which this piece may be placed.") + private int maximumDepth = 30; + + @MinNumber(0) + @MaxNumber(512) + @Desc("The minimum number of placements required for this piece in a completed assembly.") + private int minimumPlacements = 0; + + @MinNumber(0) + @MaxNumber(512) + @Desc("The maximum number of placements allowed for this piece. Zero means unbounded within the structure safety cap.") + private int maximumPlacements = 0; + + @Desc("Whether this piece is a physical terminal cap. A terminal piece may consume a matching connector but must not continue expansion.") + private boolean terminal = false; + + public boolean allowsDepth(int depth) { + return depth >= minimumDepth && depth <= maximumDepth; + } + + public boolean allowsPlacement(int existingPlacements) { + return existingPlacements >= 0 + && (maximumPlacements == 0 || existingPlacements < maximumPlacements); + } + + public boolean requiresMorePlacements(int existingPlacements) { + return existingPlacements >= 0 && existingPlacements < minimumPlacements; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPool.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPool.java new file mode 100644 index 000000000..849e3655d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawPool.java @@ -0,0 +1,63 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A jigsaw pool. A pool is a weighted set of pieces the assembler chooses from when a connector targets this pool.") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisJigsawPool extends IrisRegistrant { + @ArrayType(type = IrisJigsawPieceEntry.class, min = 1) + @Desc("The weighted pieces in this pool.") + private KList pieces = new KList<>(); + + @RegistryListResource(IrisJigsawPool.class) + @Desc("The direct fallback pool tried after this pool, or used alone at maximum depth. The fallback pool's own fallback is not part of the same selection. Leave empty to stop expanding at maximum depth.") + private String fallback = ""; + + @Desc("Whether failure to place from this pool must be resolved through its direct fallback even when the structure does not require every branch to be capped.") + private boolean mandatoryFallback = false; + + public boolean requiresFallback(boolean structureRequiresCaps) { + return structureRequiresCaps || mandatoryFallback; + } + + @Override + public String getFolderName() { + return "jigsaw-pools"; + } + + @Override + public String getTypeName() { + return "Jigsaw Pool"; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawThemeSet.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawThemeSet.java new file mode 100644 index 000000000..15ed96804 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawThemeSet.java @@ -0,0 +1,24 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A weighted structure-wide jigsaw theme. One declared theme is selected for an assembly, and pieces may opt into one or more themes.") +@Data +public class IrisJigsawThemeSet { + @Required + @Desc("The exact theme key referenced by jigsaw pieces.") + private String key = ""; + + @MinNumber(1) + @Desc("The relative weight used when selecting one theme for an assembly.") + private int weight = 1; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcell.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcell.java new file mode 100644 index 000000000..703b5cd4c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcell.java @@ -0,0 +1,40 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Data +@Desc("Persistent authoring bounds and runtime availability for one planar jigsaw workcell.") +public class IrisJigsawWorkcell { + @Desc("Optional author-facing name shown by Jigsaw Studio. The canonical archetype name is shown when this is blank.") + private String displayName = ""; + + @Desc("The orientation-independent planar connector shape configured by this workcell.") + private IrisJigsawWorkcellArchetype archetype = IrisJigsawWorkcellArchetype.BLANK; + + @MinNumber(3) + @MaxNumber(128) + @Desc("The maximum canonical variant width that this workcell can contain, in blocks.") + private int width = 16; + + @MinNumber(1) + @MaxNumber(192) + @Desc("The maximum variant height that this workcell can contain, in blocks.") + private int height = 16; + + @MinNumber(3) + @MaxNumber(128) + @Desc("The maximum canonical variant depth that this workcell can contain, in blocks.") + private int depth = 16; + + @Desc("Whether pieces with this planar connector shape participate in assembly and vanilla export.") + private boolean enabled = true; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcellArchetype.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcellArchetype.java new file mode 100644 index 000000000..75986aa0d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcellArchetype.java @@ -0,0 +1,88 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("The orientation-independent connector shape represented by a planar jigsaw workcell.") +public enum IrisJigsawWorkcellArchetype { + @Desc("Pieces with no horizontal connectors: sealed filler cells that terminate nothing and are excluded from the default piece pool.") + BLANK(0), + + @Desc("One-connector dead-end pieces; the Studio bay seeds a single north connector and new projects mark these as terminal caps.") + END(1), + + @Desc("Two-connector pass-through pieces with openings on opposite sides; the Studio bay seeds north and south connectors.") + STRAIGHT(5), + + @Desc("Two-connector right-angle pieces with openings on adjacent sides; the Studio bay seeds north and east connectors.") + CORNER(3), + + @Desc("Three-connector junction pieces; the Studio bay seeds north, east, and west connectors.") + TEE(11), + + @Desc("Four-connector pieces open on all sides; new projects use this archetype as the structure's start piece.") + CROSS(15); + + private final int canonicalMask; + + IrisJigsawWorkcellArchetype(int canonicalMask) { + this.canonicalMask = canonicalMask; + } + + public static IrisJigsawWorkcellArchetype fromPiece(IrisJigsawPiece piece) { + if (piece == null || piece.getConnectors() == null) { + return BLANK; + } + return fromMask(horizontalMask(piece)); + } + + public static IrisJigsawWorkcellArchetype fromMask(int mask) { + if (mask < 0 || mask > 15) { + throw new IllegalArgumentException("Planar jigsaw topology mask must be between 0 and 15"); + } + int connections = Integer.bitCount(mask); + return switch (connections) { + case 0 -> BLANK; + case 1 -> END; + case 2 -> mask == 5 || mask == 10 ? STRAIGHT : CORNER; + case 3 -> TEE; + case 4 -> CROSS; + default -> throw new IllegalStateException("Unreachable planar jigsaw topology"); + }; + } + + public static int horizontalMask(IrisJigsawPiece piece) { + if (piece == null || piece.getConnectors() == null) { + return 0; + } + int mask = 0; + for (IrisJigsawConnector connector : piece.getConnectors()) { + if (connector == null || connector.getDirection() == null) { + continue; + } + mask |= switch (connector.getDirection()) { + case NORTH_NEGATIVE_Z -> 1; + case EAST_POSITIVE_X -> 2; + case SOUTH_POSITIVE_Z -> 4; + case WEST_NEGATIVE_X -> 8; + case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> 0; + }; + } + return mask; + } + + public int sourceToCanonicalQuarterTurns(IrisJigsawPiece piece) { + int sourceMask = horizontalMask(piece); + if (fromMask(sourceMask) != this) { + throw new IllegalArgumentException("Jigsaw piece does not match workcell archetype " + name()); + } + for (int quarterTurns = 0; quarterTurns < 4; quarterTurns++) { + int rotated = quarterTurns == 0 + ? sourceMask + : ((sourceMask << quarterTurns) | (sourceMask >>> (4 - quarterTurns))) & 15; + if (rotated == canonicalMask) { + return quarterTurns; + } + } + throw new IllegalStateException("No canonical rotation exists for planar jigsaw piece"); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java new file mode 100644 index 000000000..58cb6b433 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java @@ -0,0 +1,296 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListItemType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +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.volmlib.util.format.Form; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.Damageable; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.LeatherArmorMeta; +import org.bukkit.inventory.meta.components.CustomModelDataComponent; +import org.bukkit.material.Colorable; + +import java.awt.Color; +import java.util.List; +import java.util.Optional; + +@Snippet("loot") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a loot entry") +@Data +@EqualsAndHashCode(doNotUseGetters = true) +@ToString(doNotUseGetters = true) +public class IrisLoot { + public static final int MAX_AMOUNT = 64; + + private final transient AtomicCache dyeColorResolved = new AtomicCache<>(); + @Desc("The target inventory slot types to fill this loot with. World generation only ever fills STORAGE; the other values are inert during worldgen.") + private InventorySlotType slotTypes = InventorySlotType.STORAGE; + @MinNumber(1) + @Desc("The sub rarity of this loot. Calculated after this loot table has been picked.") + private int rarity = 1; + @MinNumber(1) + @MaxNumber(MAX_AMOUNT) + @Desc("Minimum amount of this loot, from 1 to 64") + private int minAmount = 1; + @MinNumber(1) + @MaxNumber(MAX_AMOUNT) + @Desc("Maximum amount of this loot, from 1 to 64") + private int maxAmount = 1; + @MinNumber(1) + @Desc("The display name of this item") + private String displayName = null; + @MinNumber(0) + @MaxNumber(1) + @Desc("Minimum durability percent") + private double minDurability = 0; + @MinNumber(0) + @MaxNumber(1) + @Desc("Maximum durability percent") + private double maxDurability = 1; + @Desc("Define a custom model identifier 1.14+ only") + private Integer customModel = null; + @Desc("Set this to true to prevent it from being broken") + private boolean unbreakable = false; + @ArrayType(min = 1, type = String.class) + @Desc("The item flags to add") + private KList itemFlags = new KList<>(); + @Desc("Apply enchantments to this item") + @ArrayType(min = 1, type = IrisEnchantment.class) + private KList enchantments = new KList<>(); + @Desc("Apply attribute modifiers to this item") + @ArrayType(min = 1, type = IrisAttributeModifier.class) + private KList attributes = new KList<>(); + @ArrayType(min = 1, type = String.class) + @Desc("Add lore to this item") + private KList lore = new KList<>(); + @RegistryListItemType + @Required + @Desc("This is the item or block type. Does not accept minecraft:*, only materials such as DIAMOND_SWORD or DIRT. The exception are modded materials, as they require a namespace.") + private String type = ""; + @Desc("The dye color") + private String dyeColor = null; + @Desc("The leather armor color") + private String leatherColor = null; + @Desc("Defines a custom NBT Tag for the item.") + private KMap customNbt; + + public Material getType() { + return BukkitBlockResolution.getMaterial(type); + } + + /** + * The authored item key, exactly as written in the pack. Platform-neutral: {@link #getType()} resolves it against + * Bukkit, mod loaders resolve it against their own item registry. + */ + public String getTypeKey() { + return type; + } + + /** + * The authored dye colour name, or null when unset. Platform-neutral counterpart to {@link #getDyeColor()}. + */ + public String getDyeColorKey() { + return dyeColor; + } + + public DyeColor getDyeColor() { + if (dyeColor == null) { + return null; + } + return dyeColorResolved.aquire(() -> { + try { + return DyeColor.valueOf(dyeColor); + } catch (IllegalArgumentException e) { + return null; + } + }); + } + + private KList getResolvedItemFlags() { + KList resolved = new KList<>(); + for (String flag : itemFlags) { + if (flag == null) { + continue; + } + try { + resolved.add(ItemFlag.valueOf(flag)); + } catch (IllegalArgumentException ignored) { + } + } + return resolved; + } + + public ItemStack get(boolean debug, RNG rng) { + try { + ItemStack is = getItemStack(rng); + if (is == null) + return new ItemStack(Material.AIR); + is.setItemMeta(applyProperties(is, rng, debug, null)); + return BukkitPlatform.applyCustomNbt(is, customNbt); + } catch (Throwable e) { + IrisLogging.reportError(e); + return new ItemStack(Material.AIR); + } + } + + public ItemStack get(boolean debug, IrisLootTable table, RNG rng, long lootSeed, int entryIndex, int x, int y, int z) { + long combinedRarity = LootResolver.combinedRarity(rarity, table.getRarity()); + if (!LootResolver.spatialOneIn(lootSeed, table, entryIndex, x, y, z, combinedRarity)) { + return null; + } + + try { + ItemStack is = getItemStack(rng); + if (is == null) + return null; + is.setItemMeta(applyProperties(is, rng, debug, table)); + return BukkitPlatform.applyCustomNbt(is, customNbt); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + return null; + } + + // TODO Better Third Party Item Acquisition + private ItemStack getItemStack(RNG rng) { + if (!type.startsWith("minecraft:") && type.contains(":")) { + Optional opt = IrisServices.get(ExternalDataSVC.class).getItemStack(Identifier.fromString(type), customNbt); + if (opt.isEmpty()) { + IrisLogging.warn("Unknown Material: " + type); + return new ItemStack(Material.AIR); + } + ItemStack is = opt.get(); + is.setAmount(Math.max(1, LootResolver.inclusive(rng, getMinAmount(), getMaxAmount()))); + return is; + } + return new ItemStack(getType(), Math.max(1, LootResolver.inclusive(rng, getMinAmount(), getMaxAmount()))); + } + + private ItemMeta applyProperties(ItemStack is, RNG rng, boolean debug, IrisLootTable table) { + ItemMeta m = is.getItemMeta(); + if (m == null) { + return null; + } + + for (IrisEnchantment i : getEnchantments()) { + i.apply(rng, m); + } + + for (IrisAttributeModifier i : getAttributes()) { + i.apply(rng, m); + } + + m.setUnbreakable(isUnbreakable()); + for (ItemFlag i : getResolvedItemFlags()) { + m.addItemFlags(i); + } + + if (getCustomModel() != null) { + CustomModelDataComponent customModelData = m.getCustomModelDataComponent(); + customModelData.setFloats(List.of(getCustomModel().floatValue())); + m.setCustomModelDataComponent(customModelData); + } + + if (is.getType().getMaxDurability() > 0 && m instanceof Damageable d) { + int max = is.getType().getMaxDurability(); + d.setDamage((int) Math.round(Math.max(0, Math.min(max, (1D - rng.d(getMinDurability(), getMaxDurability())) * max)))); + } + + if (getLeatherColor() != null && m instanceof LeatherArmorMeta leather) { + Color c = Color.decode(getLeatherColor()); + leather.setColor(org.bukkit.Color.fromRGB(c.getRed(), c.getGreen(), c.getBlue())); + } + + if (getDyeColor() != null && m instanceof Colorable colorable) { + colorable.setColor(getDyeColor()); + } + + if (displayName != null) { + m.setDisplayName(C.translateAlternateColorCodes('&', displayName)); + } + + KList lore = new KList<>(); + + getLore().forEach((i) -> + { + String mf = C.translateAlternateColorCodes('&', i); + + if (mf.length() > 24) { + for (String g : Form.wrapWords(mf, 24).split("\\Q\n\\E")) { + lore.add(g.trim()); + } + } else { + lore.add(mf); + } + }); + + if (debug) { + if (table == null) { + if (lore.isNotEmpty()) { + lore.add(C.GRAY + "--------------------"); + } + lore.add(C.GRAY + "1 in " + (getRarity()) + " Chance (" + Form.pc(1D / (getRarity()), 5) + ")"); + } else { + if (lore.isNotEmpty()) { + lore.add(C.GRAY + "--------------------"); + } + + lore.add(C.GRAY + "From: " + table.getName() + " (" + Form.pc(1D / table.getRarity(), 5) + ")"); + lore.add(C.GRAY + "1 in " + (table.getRarity() * getRarity()) + " Chance (" + Form.pc(1D / (table.getRarity() * getRarity()), 5) + ")"); + } + } + + m.setLore(lore); + + return m; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLootMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLootMode.java new file mode 100644 index 000000000..e63a59e5e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLootMode.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("A loot mode is used to describe what to do with the existing loot layers before adding this loot. ADD appends to the building list of tables (dimension tables, then region tables, then biome tables). REPLACE swaps the parent tables for this reference's tables. CLEAR suppresses loot entirely at this point.") +public enum IrisLootMode { + @Desc("Add to the existing parent loot tables") + ADD, + @Desc("Clear all parent loot tables and contribute nothing at this level. Any tables listed here are ignored.") + CLEAR, + @Desc("Replace all parent loot tables with this reference's tables") + REPLACE, + @Desc("Only use when there was no loot table defined by an object") + FALLBACK +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java new file mode 100644 index 000000000..189876a40 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java @@ -0,0 +1,68 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.data.DataProvider; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("loot-registry") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a loot entry") +@Data +public class IrisLootReference { + public static final double MAX_MULTIPLIER = 16D; + + private final transient AtomicCache> tt = new AtomicCache<>(); + @Desc("ADD = add on top of parent tables. REPLACE = clear parent tables, then add these. CLEAR = clear parent tables and add nothing, even if tables are listed. FALLBACK = only used when nothing else defined a table.") + private IrisLootMode mode = IrisLootMode.ADD; + @RegistryListResource(IrisLootTable.class) + @ArrayType(min = 1, type = String.class) + @Desc("Add loot table registries here") + private KList tables = new KList<>(); + @MinNumber(0) + @MaxNumber(MAX_MULTIPLIER) + @Desc("Scale loot sources in this area from 0 to 16 times their normal count") + private double multiplier = 1D; + + public KList getLootTables(DataProvider g) { + return tt.aquire(() -> + { + KList t = new KList<>(); + + for (String i : tables) { + t.add(g.getData().getLootLoader().load(i)); + } + + return t; + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLootTable.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLootTable.java new file mode 100644 index 000000000..692f18b55 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLootTable.java @@ -0,0 +1,115 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a loot table. Biomes, Regions & Objects can add or replace the virtual table with these loot tables") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisLootTable extends IrisRegistrant { + public static final int MAX_PICKED = 64; + public static final int MAX_TRIES = 256; + + @Required + @Desc("The name of this loot table") + @MinNumber(2) + private String name = ""; + + @MinNumber(1) + @Desc("The rarity as in 1 in X chance") + private int rarity = 1; + + @MinNumber(1) + @MaxNumber(MAX_PICKED) + @Desc("The maximum amount of loot that can be picked in this table at a time, from 1 to 64") + private int maxPicked = 5; + + @MinNumber(0) + @MaxNumber(MAX_PICKED) + @Desc("The minimum amount of loot that can be picked in this table at a time, from 0 to 64") + private int minPicked = 1; + + @MinNumber(1) + @MaxNumber(MAX_TRIES) + @Desc("The maximum amount of tries to generate loot, from 1 to 256") + private int maxTries = 10; + + @Desc("The loot in this table") + @ArrayType(min = 1, type = IrisLoot.class) + private KList loot = new KList<>(); + + public KList getLoot(boolean debug, long lootSeed, InventorySlotType slot, World world, int x, int y, int z) { + KList lootf = new KList<>(); + if (loot.isEmpty() || maxTries <= 0) { + return lootf; + } + + RNG rng = LootResolver.tableRng(lootSeed, this, x, y, z); + int m = 0; + int c = 0; + int mx = Math.max(0, LootResolver.inclusive(rng, getMinPicked(), getMaxPicked())); + + while (m < mx && c++ < getMaxTries()) { + int num = rng.nextInt(loot.size()); + + IrisLoot l = loot.get(num); + + if (l != null && l.getSlotTypes() == slot) { + ItemStack item = l.get(debug, this, rng, lootSeed, num, x, y, z); + + if (item != null && item.getType() != Material.AIR) { + lootf.add(item); + m++; + } + } + } + + return lootf; + } + + @Override + public String getFolderName() { + return "loot"; + } + + @Override + public String getTypeName() { + return "Loot"; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisMarker.java b/core/src/main/java/art/arcane/iris/engine/object/IrisMarker.java similarity index 80% rename from core/src/main/java/com/volmit/iris/engine/object/IrisMarker.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisMarker.java index fc5a3c761..4446e5194 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisMarker.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisMarker.java @@ -16,16 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.RegistryListResource; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.plugin.VolmitSender; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -66,9 +64,4 @@ public class IrisMarker extends IrisRegistrant { public String getTypeName() { return "Marker"; } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java b/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java new file mode 100644 index 000000000..d31215127 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java @@ -0,0 +1,130 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; +import lombok.experimental.Accessors; + +import java.util.Optional; + +@Snippet("palette") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A palette of materials") +@Data +public class IrisMaterialPalette { + private final transient AtomicCache> blockData = new AtomicCache<>(); + private final transient AtomicCache layerGenerator = new AtomicCache<>(); + private final transient AtomicCache heightGenerator = new AtomicCache<>(); + @Desc("The style of noise") + private IrisGeneratorStyle style = NoiseStyle.STATIC.style(); + @MinNumber(0.0001) + @Desc("The terrain zoom mostly for zooming in on a wispy palette") + private double zoom = 5; + @Required + @ArrayType(min = 1, type = IrisBlockData.class) + @Desc("The palette of blocks to be used in this layer") + private KList palette = new KList().qadd(new IrisBlockData("STONE")); + + public PlatformBlockState get(RNG rng, double x, double y, double z, IrisData rdata) { + KList localBlockData = getBlockData(rdata); + int blockDataSize = localBlockData.size(); + if (blockDataSize == 0) { + return null; + } + + if (blockDataSize == 1) { + return localBlockData.get(0); + } + + double scaledX = x / zoom; + double scaledY = y / zoom; + double scaledZ = z / zoom; + return getLayerGenerator(rng, rdata).fit(localBlockData, scaledX, scaledY, scaledZ); + } + + public Optional getTile(RNG rng, double x, double y, double z, IrisData rdata) { + if (getBlockData(rdata).isEmpty()) + return Optional.empty(); + + TileData tile = getBlockData(rdata).size() == 1 ? palette.get(0).tryGetTile(rdata) : palette.getRandom(rng).tryGetTile(rdata); + return tile != null ? Optional.of(tile) : Optional.empty(); + } + + public CNG getLayerGenerator(RNG rng, IrisData rdata) { + return layerGenerator.aquire(() -> + { + RNG rngx = rng.nextParallelRNG(-23498896 + getBlockData(rdata).size()); + return style.create(rngx, rdata); + }); + } + + public IrisMaterialPalette qclear() { + palette.clear(); + return this; + } + + public KList add(String b) { + palette.add(new IrisBlockData(b)); + + return palette; + } + + public IrisMaterialPalette qadd(String b) { + palette.add(new IrisBlockData(b)); + + return this; + } + + public KList getBlockData(IrisData rdata) { + return blockData.aquire(() -> + { + KList blockData = new KList<>(); + for (IrisBlockData ix : palette) { + PlatformBlockState bx = ix.getBlockData(rdata); + if (bx != null) { + for (int i = 0; i < ix.getWeight(); i++) { + blockData.add(bx); + } + } + } + + return blockData; + }); + } + + public IrisMaterialPalette zero() { + palette.clear(); + return this; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisMod.java b/core/src/main/java/art/arcane/iris/engine/object/IrisMod.java similarity index 81% rename from core/src/main/java/com/volmit/iris/engine/object/IrisMod.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisMod.java index 8a7dab5d2..d8c40fc89 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisMod.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisMod.java @@ -16,13 +16,16 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -32,16 +35,16 @@ import lombok.experimental.Accessors; @Accessors(chain = true) @AllArgsConstructor @NoArgsConstructor -@Desc("Represents a dimension") +@Desc("Represents a pack modification schema") @Data @EqualsAndHashCode(callSuper = false) public class IrisMod extends IrisRegistrant { @MinNumber(2) @Required - @Desc("The human readable name of this dimension") - private String name = "A Dimension Mod"; + @Desc("The human-readable name of this pack modification") + private String name = "A Pack Modification"; - @Desc("If this mod only works with a specific dimension, define it's load key here. Such as overworld, or flat. Otherwise iris will assume this mod works with anything.") + @Desc("The optional dimension load key this modification targets. An empty value does not restrict the target.") private String forDimension = ""; @MinNumber(-1) @@ -90,7 +93,7 @@ public class IrisMod extends IrisRegistrant { private KList regionObjectPlacementInjectors = new KList<>(); @ArrayType(min = 1, type = IrisModRegionReplacer.class) - @Desc("Replace biomes with other biomes") + @Desc("Replace regions with other regions") private KList regionReplacers = new KList<>(); @ArrayType(min = 1, type = IrisObjectReplace.class) @@ -110,9 +113,4 @@ public class IrisMod extends IrisRegistrant { public String getTypeName() { return "Mod"; } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } } diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisModBiomeInjector.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeInjector.java similarity index 77% rename from core/src/main/java/com/volmit/iris/engine/object/IrisModBiomeInjector.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeInjector.java index 911e77714..2129c460b 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisModBiomeInjector.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeInjector.java @@ -16,10 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisModBiomeReplacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeReplacer.java similarity index 77% rename from core/src/main/java/com/volmit/iris/engine/object/IrisModBiomeReplacer.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeReplacer.java index a87e374da..4d6323a15 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisModBiomeReplacer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeReplacer.java @@ -16,10 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisModNoiseStyleReplacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModNoiseStyleReplacer.java similarity index 88% rename from core/src/main/java/com/volmit/iris/engine/object/IrisModNoiseStyleReplacer.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisModNoiseStyleReplacer.java index 66fa4081a..bbe3611cb 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisModNoiseStyleReplacer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModNoiseStyleReplacer.java @@ -16,9 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -33,7 +35,6 @@ import lombok.experimental.Accessors; public class IrisModNoiseStyleReplacer { @Required @Desc("A noise style to find") - @ArrayType(type = String.class, min = 1) private NoiseStyle find = NoiseStyle.IRIS; @Required @@ -42,6 +43,5 @@ public class IrisModNoiseStyleReplacer { @Required @Desc("A noise style to replace it with") - @RegistryListResource(IrisBiome.class) private IrisGeneratorStyle replace = new IrisGeneratorStyle(); } diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisModObjectPlacementBiomeInjector.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementBiomeInjector.java similarity index 75% rename from core/src/main/java/com/volmit/iris/engine/object/IrisModObjectPlacementBiomeInjector.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementBiomeInjector.java index 2846df593..8c1cfd0e9 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisModObjectPlacementBiomeInjector.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementBiomeInjector.java @@ -16,10 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -38,7 +42,7 @@ public class IrisModObjectPlacementBiomeInjector { private String biome = ""; @Required - @Desc("A biome to inject into the region") + @Desc("Object placements to inject into the biome") @ArrayType(type = IrisObjectPlacement.class, min = 1) private KList place = new KList<>(); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementRegionInjector.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementRegionInjector.java new file mode 100644 index 000000000..d9121abf2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementRegionInjector.java @@ -0,0 +1,48 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("object-placement-region-injector") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("An object placement injector") +@Data +public class IrisModObjectPlacementRegionInjector { + @Required + @Desc("The region to find") + @RegistryListResource(IrisRegion.class) + private String biome = ""; + + @Required + @Desc("Object placements to inject into the region") + @ArrayType(type = IrisObjectPlacement.class, min = 1) + private KList place = new KList<>(); +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectReplacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectReplacer.java new file mode 100644 index 000000000..176f580ba --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectReplacer.java @@ -0,0 +1,49 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("object-replacer") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("An object replacer") +@Data +public class IrisModObjectReplacer { + @Required + @Desc("A list of objects to find") + @RegistryListResource(IrisObject.class) + @ArrayType(type = String.class, min = 1) + private KList find = new KList<>(); + + @Required + @Desc("An object to replace it with") + @RegistryListResource(IrisObject.class) + private String replace = ""; +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisModRegionReplacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModRegionReplacer.java similarity index 77% rename from core/src/main/java/com/volmit/iris/engine/object/IrisModRegionReplacer.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisModRegionReplacer.java index 0b1ec5c94..67ac742fe 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisModRegionReplacer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModRegionReplacer.java @@ -16,10 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisNativeStructure.java b/core/src/main/java/art/arcane/iris/engine/object/IrisNativeStructure.java new file mode 100644 index 000000000..a98da1005 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisNativeStructure.java @@ -0,0 +1,29 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; +import art.arcane.iris.engine.object.annotations.Required; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Data +@Desc("A live vanilla, datapack, or modded structure generated by Minecraft at an Iris-controlled placement.") +public class IrisNativeStructure { + @Required + @RegistryListVanillaStructure + @Desc("Registered structure key. The source retains its native generator, pieces, templates, processors, entities, markers, and data.") + private String structure = ""; + + @MinNumber(1) + @Desc("Relative selection weight when a placement offers multiple native structure sources.") + private int weight = 1; + + @Desc("Optional jigsaw-only assembly overrides. Null preserves the complete registered structure definition.") + private IrisJigsawConfiguration jigsaw = null; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisNativeStructureDecision.java b/core/src/main/java/art/arcane/iris/engine/object/IrisNativeStructureDecision.java new file mode 100644 index 000000000..33a8bb34c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisNativeStructureDecision.java @@ -0,0 +1,37 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +public record IrisNativeStructureDecision( + NativeStructureGenerationStatus status, + int yShift, + IrisStructureYBand yBand, + boolean preserveSourceY, + IrisStructureStiltSettings stilt, + IrisStructureTerrain terrain +) { + public boolean generate() { + return status == NativeStructureGenerationStatus.GENERATE_NATIVE; + } + + public IrisNativeStructureDecision withStatus(NativeStructureGenerationStatus replacement) { + return new IrisNativeStructureDecision( + replacement, yShift, yBand, preserveSourceY, stilt, terrain); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisNoiseGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java similarity index 79% rename from core/src/main/java/com/volmit/iris/engine/object/IrisNoiseGenerator.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java index f04a09028..1c4d1b459 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisNoiseGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java @@ -16,15 +16,20 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.noise.CNG; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -100,13 +105,17 @@ public class IrisNoiseGenerator { for (IrisNoiseGenerator i : fracture) { if (i.isEnabled()) { - x += i.getNoise(superSeed + seed + g, xv, zv, data) - (opacity / 2D); - z -= i.getNoise(superSeed + seed + g, zv, xv, data) - (opacity / 2D); + double fractureOffset = i.getOpacity() / 2D; + x += i.getNoise(superSeed + seed + g, xv, zv, data) - fractureOffset; + z += i.getNoise(superSeed + seed + g, zv, xv, data) - fractureOffset; } g += 819; } - double n = getGenerator(superSeed, data).fitDouble(0, opacity, (x / zoom) + offsetX, (z / zoom) + offsetZ); + CNG cng = getGenerator(superSeed, data); + double sampleX = (x / zoom) + offsetX; + double sampleZ = (z / zoom) + offsetZ; + double n = cng.noiseFast2D(sampleX, sampleZ) * opacity; n = negative ? (-n + opacity) : n; n = (exponent != 1 ? n < 0 ? -Math.pow(-n, exponent) : Math.pow(n, exponent) : n) + offsetY; n = parametric ? IrisInterpolation.parametric(n, 1) : n; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java new file mode 100644 index 000000000..1549eb5cb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -0,0 +1,366 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.AxisAlignedBB; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.IrisVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.volmlib.util.math.RNG; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.BiConsumer; + +/** + * A voxel volume loaded from a .iob file. Instances are loader cached and shared across generation threads, so all + * volume access goes through the read/write lock pair below. + *

+ * The heavy behaviour lives in package-private collaborators that read this state directly: + * {@link IrisObjectIO} (binary persistence), {@link IrisObjectShaping} (boring, shrinkwrap, block classification), + * {@link IrisObjectTransforms} (rotation, scaling, interpolated upscaling) and {@link IrisObjectPlacementRunner} + * (world placement). + */ +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +public class IrisObject extends IrisRegistrant { + protected static final IrisVector HALF = new IrisVector(0.5, 0.5, 0.5); + static final class States { + static final PlatformBlockState AIR = B.getState("CAVE_AIR"); + static final PlatformBlockState STONE = B.getState("STONE"); + static final PlatformBlockState VAIR = B.getState("VOID_AIR"); + static final PlatformBlockState VAIR_DEBUG = B.getState("COBWEB"); + static final PlatformBlockState[] SNOW_LAYERS = new PlatformBlockState[]{B.getState("minecraft:snow[layers=1]"), B.getState("minecraft:snow[layers=2]"), B.getState("minecraft:snow[layers=3]"), B.getState("minecraft:snow[layers=4]"), B.getState("minecraft:snow[layers=5]"), B.getState("minecraft:snow[layers=6]"), B.getState("minecraft:snow[layers=7]"), B.getState("minecraft:snow[layers=8]")}; + } + protected transient final Lock readLock; + protected transient final Lock writeLock; + @Getter + @Setter + protected transient volatile boolean smartBored = false; + @Setter + protected transient AtomicCache aabb = new AtomicCache<>(); + transient final AtomicCache> surfaceSupportOffsets = new AtomicCache<>(); + transient final AtomicCache floatingFootprint = new AtomicCache<>(); + @Getter + VectorMap blocks; + @Getter + VectorMap states; + @Getter + @Setter + int w; + @Getter + @Setter + int d; + @Getter + @Setter + int h; + @Getter + @Setter + transient Vector3i center; + @Getter + transient Vector3i shrinkOffset; + + public IrisObject(int w, int h, int d) { + blocks = new VectorMap<>(); + states = new VectorMap<>(); + this.w = w; + this.h = h; + this.d = d; + center = new Vector3i(w / 2, h / 2, d / 2); + shrinkOffset = new Vector3i(0, 0, 0); + var lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + } + + public IrisObject() { + this(0, 0, 0); + } + + public static AxisAlignedBB getAABBFor(IrisBlockVector size) { + IrisBlockVector center = new IrisBlockVector(size.getX() / 2, size.getY() / 2, size.getZ() / 2); + IrisVector min = new IrisBlockVector(0, 0, 0).subtract(center); + IrisVector max = new IrisBlockVector(size.getX() - 1, size.getY() - 1, size.getZ() - 1).subtract(center); + return new AxisAlignedBB(new IrisPosition(min.getBlockX(), min.getBlockY(), min.getBlockZ()), + new IrisPosition(max.getBlockX(), max.getBlockY(), max.getBlockZ())); + } + + public static IrisBlockVector sampleSize(File file) throws IOException { + return IrisObjectIO.sampleSize(file); + } + + public AxisAlignedBB getAABB() { + return aabb.aquire(() -> getAABBFor(new IrisBlockVector(w, h, d))); + } + + public synchronized IrisObject copy() { + IrisObject o = new IrisObject(w, h, d); + o.setLoadKey(getLoadKey()); + o.setLoader(getLoader()); + o.setLoadFile(getLoadFile()); + o.setCenter(getCenter().clone()); + + blocks.forEach((i, v) -> o.blocks.put(i.clone(), v)); + states.forEach((i, v) -> o.states.put(i.clone(), v.clone())); + + return o; + } + + public void readLegacy(InputStream in) throws IOException { + IrisObjectIO.readLegacy(this, in); + } + + public void read(InputStream in) throws Throwable { + IrisObjectIO.read(this, in); + } + + public void read(File file) throws IOException { + IrisObjectIO.read(this, file); + } + + public void write(OutputStream o) throws IOException { + IrisObjectIO.write(this, o); + } + + public void write(OutputStream o, VolmitSender sender) throws IOException { + IrisObjectIO.write(this, o, sender); + } + + public void write(File file) throws IOException { + IrisObjectIO.write(this, file); + } + + public void write(File file, VolmitSender sender) throws IOException { + IrisObjectIO.write(this, file, sender); + } + + public void shrinkwrap() { + // Instances are loader cached and shared across generation threads; mutating the + // volume without the write lock let a concurrent placement mix pre-shrink anchors + // with post-shrink blocks. Rotation paths call the package-private statics while + // already holding this (reentrant) lock. + writeLock.lock(); + try { + IrisObjectShaping.shrinkwrap(this); + } finally { + writeLock.unlock(); + } + } + + public void clean() { + writeLock.lock(); + try { + IrisObjectShaping.clean(this); + } finally { + writeLock.unlock(); + } + } + + public IrisBlockVector getSigned(int x, int y, int z) { + if (x >= w || y >= h || z >= d) { + throw new RuntimeException(x + " " + y + " " + z + " exceeds limit of " + w + " " + h + " " + d); + } + + return new IrisBlockVector(x - center.getX(), y - center.getY(), z - center.getZ()); + } + + public void setUnsigned(int x, int y, int z, PlatformBlockState block) { + IrisBlockVector v = getSigned(x, y, z); + + if (block == null) { + blocks.remove(v); + states.remove(v); + } else { + blocks.put(v, block); + } + + surfaceSupportOffsets.reset(); + floatingFootprint.reset(); + } + + public void setUnsignedTile(int x, int y, int z, TileData tile) { + IrisBlockVector v = getSigned(x, y, z); + + if (tile == null) { + states.remove(v); + } else { + states.put(v, tile); + } + } + + public void setUnsigned(int x, int y, int z, Block block, boolean legacy) { + IrisBlockVector v = getSigned(x, y, z); + + if (block == null) { + blocks.remove(v); + states.remove(v); + } else { + BlockData data = block.getBlockData(); + blocks.put(v, BukkitBlockState.of(data)); + TileData state = TileData.getTileState(block, legacy); + if (state != null) { + IrisLogging.debug("Saved State " + v); + states.put(v, state); + } + } + + surfaceSupportOffsets.reset(); + floatingFootprint.reset(); + } + + public int place(int x, int z, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, IrisData rdata) { + return place(x, -1, z, placer, config, rng, rdata); + } + + public int place(int x, int z, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, CarveResult c, IrisData rdata) { + return place(x, -1, z, placer, config, rng, null, c, rdata); + } + + public int place(int x, int yv, int z, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, IrisData rdata) { + return place(x, yv, z, placer, config, rng, null, null, rdata); + } + + public int place(Location loc, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, IrisData rdata) { + return place(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), placer, config, rng, rdata); + } + + public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer listener, CarveResult c, IrisData rdata) { + return new IrisObjectPlacementRunner(this).place(x, yv, z, oplacer, config, rng, listener, c, rdata); + } + + KList getSurfaceSupportOffsets() { + return surfaceSupportOffsets.aquire(() -> { + readLock.lock(); + try { + int lowestY = Integer.MAX_VALUE; + KList offsets = new KList<>(); + for (Map.Entry entry : blocks) { + PlatformBlockState state = entry.getValue(); + if (state == null || !state.isSolid() || state.isFoliage()) { + continue; + } + IrisBlockVector position = entry.getKey(); + int blockY = position.getBlockY(); + if (blockY < lowestY) { + lowestY = blockY; + offsets.clear(); + } + if (blockY == lowestY) { + offsets.add(position.clone()); + } + } + return offsets; + } finally { + readLock.unlock(); + } + }); + } + + public IrisObject rotateCopy(IrisObjectRotation rt) { + return IrisObjectTransforms.rotateCopy(this, rt); + } + + public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) { + return IrisObjectTransforms.scaled(this, scale, interpolation); + } + + public void place(Location at) { + readLock.lock(); + try { + for (var entry : blocks) { + var i = entry.getKey(); + Block b = at.clone().add(0, getCenter().getY(), 0).add(i.getX(), i.getY(), i.getZ()).getBlock(); + b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false); + + if (states.containsKey(i)) { + IrisLogging.info(Objects.requireNonNull(states.get(i)).toString()); + Objects.requireNonNull(states.get(i)).toBukkitTry(b); + } + } + } finally { + readLock.unlock(); + } + } + + public void placeCenterY(Location at) { + readLock.lock(); + try { + for (var entry : blocks) { + var i = entry.getKey(); + Block b = at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i.getX(), i.getY(), i.getZ()).getBlock(); + b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false); + + if (states.containsKey(i)) { + Objects.requireNonNull(states.get(i)).toBukkitTry(b); + } + } + } finally { + readLock.unlock(); + } + } + + public void unplaceCenterY(Location at) { + readLock.lock(); + try { + for (IrisBlockVector i : blocks.keys()) { + at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i.getX(), i.getY(), i.getZ()).getBlock().setBlockData((BlockData) States.AIR.nativeHandle(), false); + } + } finally { + readLock.unlock(); + } + } + + public int volume() { + return blocks.size(); + } + + @Override + public String getFolderName() { + return "objects"; + } + + @Override + public String getTypeName() { + return "Object"; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectIO.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectIO.java new file mode 100644 index 000000000..f6d311241 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectIO.java @@ -0,0 +1,396 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.jobs.Job; +import art.arcane.volmlib.util.collection.KList; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Binary (.iob) persistence for {@link IrisObject}. The field layout written here is pinned by the on-disk + * format - do not reorder reads or writes. + */ +public final class IrisObjectIO { + private static final String V2_HEADER = "Iris V2 IOB;"; + private static final int MAX_PALETTE_ENTRIES = 32_767; + + private IrisObjectIO() { + } + + /** + * Reads only the V2 palette block-state keys out of an {@code .iob} header. Read-only pack-tooling hook: no + * IrisObject is built and no block state is resolved, so it runs without a bound platform. + *

+ * Returns an empty list for a legacy (V1) object, an unreadable file, or a truncated header - a scan must never + * fail pack validation. + */ + public static List readPaletteKeys(File file) { + if (file == null || !file.isFile()) { + return List.of(); + } + try (DataInputStream din = new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) { + din.readInt(); + din.readInt(); + din.readInt(); + if (!V2_HEADER.equals(din.readUTF())) { + return List.of(); + } + int count = din.readShort(); + if (count <= 0 || count > MAX_PALETTE_ENTRIES) { + return List.of(); + } + List palette = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + palette.add(din.readUTF()); + } + return palette; + } catch (Throwable e) { + return List.of(); + } + } + + static IrisBlockVector sampleSize(File file) throws IOException { + try (DataInputStream din = new DataInputStream(new FileInputStream(file))) { + return new IrisBlockVector(din.readInt(), din.readInt(), din.readInt()); + } + } + + static void readLegacy(IrisObject self, InputStream in) throws IOException { + self.surfaceSupportOffsets.reset(); + self.floatingFootprint.reset(); + DataInputStream din = new DataInputStream(in); + self.w = din.readInt(); + self.h = din.readInt(); + self.d = din.readInt(); + self.center = new Vector3i(self.w / 2, self.h / 2, self.d / 2); + int s = din.readInt(); + + for (int i = 0; i < s; i++) { + IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()); + PlatformBlockState data = B.getState(din.readUTF()); + if (isStructureMarker(data)) { + continue; + } + self.blocks.put(pos, data); + } + + if (din.available() == 0) + return; + + try { + int size = din.readInt(); + + for (int i = 0; i < size; i++) { + self.states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din)); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + static void read(IrisObject self, InputStream in) throws Throwable { + self.surfaceSupportOffsets.reset(); + self.floatingFootprint.reset(); + DataInputStream din = new DataInputStream(in); + self.w = din.readInt(); + self.h = din.readInt(); + self.d = din.readInt(); + if (!din.readUTF().equals("Iris V2 IOB;")) { + throw new HeaderException(); + } + self.center = new Vector3i(self.w / 2, self.h / 2, self.d / 2); + int s = din.readShort(); + int i; + KList palette = new KList<>(); + + for (i = 0; i < s; i++) { + palette.add(din.readUTF()); + } + + // Resolve the palette once: B.getState per BLOCK was a registry lookup times the + // block count (tens of thousands) instead of times the palette size (hundreds). + PlatformBlockState[] resolved = new PlatformBlockState[palette.size()]; + for (i = 0; i < resolved.length; i++) { + resolved[i] = B.getState(palette.get(i)); + } + + s = din.readInt(); + + for (i = 0; i < s; i++) { + IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()); + PlatformBlockState data = resolved[din.readShort()]; + if (isStructureMarker(data)) { + continue; + } + self.blocks.put(pos, data); + } + + s = din.readInt(); + + for (i = 0; i < s; i++) { + self.states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din)); + } + } + + static void read(IrisObject self, File file) throws IOException { + try (var fin = new BufferedInputStream(new FileInputStream(file))) { + read(self, fin); + } catch (Throwable e) { + if (!(e instanceof HeaderException)) + IrisLogging.reportError(e); + // The V2 parse populates blocks/states incrementally; a mid-file failure must not + // leave those entries to be merged with the legacy parse of the same file. + self.blocks.clear(); + self.states.clear(); + try (var fin = new BufferedInputStream(new FileInputStream(file))) { + readLegacy(self, fin); + } + } + } + + private static boolean isStructureMarker(PlatformBlockState data) { + if (data == null) { + return false; + } + String material = IrisObjectShaping.materialKey(data); + return material.equals("minecraft:jigsaw") || material.equals("minecraft:structure_block") || material.equals("minecraft:structure_void"); + } + + /** + * The .iob V2 format stores the palette count, palette indices, and block coordinates as + * shorts. Values beyond the short range used to wrap silently and corrupt the object; every + * write path now rejects them with a descriptive error before any byte is written. + */ + static void validateWritable(IrisObject self) throws IOException { + KList palette = new KList<>(); + for (PlatformBlockState i : self.blocks.values()) { + palette.addIfMissing(i.key()); + } + if (palette.size() > MAX_PALETTE_ENTRIES) { + throw new IOException("Object '" + self.getLoadKey() + "' has " + palette.size() + + " distinct block states; the .iob format supports at most " + MAX_PALETTE_ENTRIES + "."); + } + for (var entry : self.blocks) { + requireShortCoordinates(self, "block", entry.getKey()); + } + for (var entry : self.states) { + requireShortCoordinates(self, "tile", entry.getKey()); + } + } + + private static void requireShortCoordinates(IrisObject self, String kind, IrisBlockVector position) throws IOException { + requireShort(self, kind, "x", position.getBlockX(), position); + requireShort(self, kind, "y", position.getBlockY(), position); + requireShort(self, kind, "z", position.getBlockZ(), position); + } + + private static void requireShort(IrisObject self, String kind, String axis, int value, IrisBlockVector position) throws IOException { + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new IOException("Object '" + self.getLoadKey() + "' " + kind + " at (" + position.getBlockX() + + "," + position.getBlockY() + "," + position.getBlockZ() + + ") exceeds the .iob coordinate range of ±32767 on the " + axis + " axis (" + value + ")."); + } + } + + static void write(IrisObject self, OutputStream o) throws IOException { + validateWritable(self); + writeValidated(self, o); + } + + private static void writeValidated(IrisObject self, OutputStream o) throws IOException { + DataOutputStream dos = new DataOutputStream(o); + dos.writeInt(self.w); + dos.writeInt(self.h); + dos.writeInt(self.d); + dos.writeUTF("Iris V2 IOB;"); + KList palette = new KList<>(); + + for (PlatformBlockState i : self.blocks.values()) { + palette.addIfMissing(i.key()); + } + + dos.writeShort(palette.size()); + + for (String i : palette) { + dos.writeUTF(i); + } + + dos.writeInt(self.blocks.size()); + + for (var entry : self.blocks) { + var i = entry.getKey(); + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + dos.writeShort(palette.indexOf(entry.getValue().key())); + } + + dos.writeInt(self.states.size()); + for (var entry : self.states) { + var i = entry.getKey(); + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + entry.getValue().toBinary(dos); + } + } + + static void write(IrisObject self, OutputStream o, VolmitSender sender) throws IOException { + validateWritable(self); + writeValidated(self, o, sender); + } + + private static void writeValidated(IrisObject self, OutputStream o, VolmitSender sender) throws IOException { + AtomicReference ref = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + new Job() { + private volatile int total = self.blocks.size() * 3 + self.states.size(); + private volatile int c = 0; + + @Override + public String getName() { + return IrisLanguage.text(RuntimeUiMessages.JOB_SAVING_OBJECT); + } + + @Override + public void execute() { + try { + DataOutputStream dos = new DataOutputStream(o); + dos.writeInt(self.w); + dos.writeInt(self.h); + dos.writeInt(self.d); + dos.writeUTF("Iris V2 IOB;"); + + KList palette = new KList<>(); + + for (PlatformBlockState i : self.blocks.values()) { + palette.addIfMissing(i.key()); + ++c; + } + total -= self.blocks.size() - palette.size(); + + dos.writeShort(palette.size()); + + for (String i : palette) { + dos.writeUTF(i); + ++c; + } + + dos.writeInt(self.blocks.size()); + + for (var entry : self.blocks) { + var i = entry.getKey(); + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + dos.writeShort(palette.indexOf(entry.getValue().key())); + ++c; + } + + dos.writeInt(self.states.size()); + for (var entry : self.states) { + var i = entry.getKey(); + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + entry.getValue().toBinary(dos); + ++c; + } + } catch (IOException e) { + ref.set(e); + } finally { + latch.countDown(); + } + } + + @Override + public void completeWork() {} + + @Override + public int getTotalWork() { + return total; + } + + @Override + public int getWorkCompleted() { + return c; + } + }.execute(sender, true, () -> {}); + + try { + latch.await(); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while writing object", interrupted); + } + if (ref.get() != null) + throw ref.get(); + } + + static void write(IrisObject self, File file) throws IOException { + if (file == null) { + return; + } + + // Validate before opening the stream: FileOutputStream truncates, and a rejected + // object must leave the existing .iob untouched. + validateWritable(self); + try (FileOutputStream out = new FileOutputStream(file)) { + writeValidated(self, out); + } + } + + static void write(IrisObject self, File file, VolmitSender sender) throws IOException { + if (file == null) { + return; + } + + validateWritable(self); + try (FileOutputStream out = new FileOutputStream(file)) { + writeValidated(self, out, sender); + } + } + + private static class HeaderException extends IOException { + public HeaderException() { + super("Invalid Header"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLimit.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLimit.java new file mode 100644 index 000000000..004a95baa --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLimit.java @@ -0,0 +1,50 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("object-limit") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Limits object placement by minimum and maximum world height") +@Data +public class IrisObjectLimit { + @MinNumber(-2048) + @MaxNumber(2048) + @Desc("The minimum height for placement (bottom of object)") + private int minimumHeight = -2048; + + @MinNumber(-2048) + @MaxNumber(2048) + @Desc("The maximum height for placement (top of object)") + private int maximumHeight = 2048; + + public boolean canPlace(int h, int l) { + return h <= maximumHeight && l >= minimumHeight; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLoot.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLoot.java new file mode 100644 index 000000000..6d54a99c5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLoot.java @@ -0,0 +1,81 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; +import lombok.experimental.Accessors; + +@Snippet("object-loot") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents loot within this object") +@Data +public class IrisObjectLoot implements IObjectLoot { + private final transient AtomicCache> filterCache = new AtomicCache<>(); + @ArrayType(min = 1, type = IrisBlockData.class) + @Desc("The list of blocks this loot table should apply to") + private KList filter = new KList<>(); + @Desc("Exactly match the block data or not") + private boolean exact = false; + @Desc("The loot table name") + @Required + @RegistryListResource(IrisLootTable.class) + private String name; + @Desc("The weight of this loot table being chosen") + @MinNumber(1) + private int weight = 1; + + public KList getFilter(IrisData rdata) { + return filterCache.aquire(() -> + { + KList b = new KList<>(); + + for (IrisBlockData i : filter) { + PlatformBlockState bx = i.getBlockData(rdata); + + if (bx != null) { + b.add(bx); + } + } + + return b; + }); + } + + public boolean matchesFilter(IrisData manager, PlatformBlockState data) { + for (PlatformBlockState filterState : getFilter(manager)) { + if (B.matches(filterState, data)) return true; + } + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectMarker.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectMarker.java new file mode 100644 index 000000000..d89c2d7db --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectMarker.java @@ -0,0 +1,76 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; +import lombok.experimental.Accessors; + +@Snippet("object-marker") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Find blocks to mark") +@Data +public class IrisObjectMarker { + private final transient AtomicCache> findData = new AtomicCache<>(); + @ArrayType(min = 1, type = IrisBlockData.class) + @Required + @Desc("Find block types to mark") + private KList mark = new KList<>(); + @MinNumber(1) + @MaxNumber(16) + @Desc("The maximum amount of markers to place. Use these sparingly!") + private int maximumMarkers = 8; + @Desc("If true, markers will only be placed if the block matches the mark list perfectly.") + private boolean exact = false; + @Required + @RegistryListResource(IrisMarker.class) + @Desc("The marker to add") + private String marker; + + public KList getMark(IrisData rdata) { + return findData.aquire(() -> + { + KList b = new KList<>(); + + for (IrisBlockData i : mark) { + PlatformBlockState bx = i.getBlockData(rdata); + + if (bx != null) { + b.add(bx); + } + } + + return b; + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java new file mode 100644 index 000000000..d070edd45 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java @@ -0,0 +1,435 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.data.cache.LazyBoundedCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.data.DataProvider; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import com.google.gson.annotations.SerializedName; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.TreeType; +import org.bukkit.block.data.BlockData; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.function.Function; + +@Snippet("object-placer") +@EqualsAndHashCode() +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents an iris object placer. It places objects.") +@Data +public class IrisObjectPlacement { + private static final int SURFACE_WARP_CACHE_SIZE = 8; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final transient LazyBoundedCache surfaceWarpCache = + new LazyBoundedCache<>(SURFACE_WARP_CACHE_SIZE); + @RegistryListResource(IrisObject.class) + @Required + @ArrayType(min = 1, type = String.class) + @Desc("List of objects to place") + private KList place = new KList<>(); + @Desc("Rotate this objects placement") + private IrisObjectRotation rotation = new IrisObjectRotation(); + @Desc("Limit the max height or min height of placement.") + private IrisObjectLimit clamp = new IrisObjectLimit(); + @MinNumber(0) + @MaxNumber(1) + @Desc("The maximum layer level of a snow filter overtop of this placement. Set to 0 to disable. Max of 1.") + private double snow = 0; + @Desc("Whether or not this object can be targeted by a dolphin.") + private boolean isDolphinTarget = false; + @Desc("The slope at which this object can be placed. Range from 0 to 10 by default. Calculated from a 3-block radius from the center of the object placement.") + private IrisSlopeClip slopeCondition = new IrisSlopeClip(); + @Desc("Set to true to add the rotation of the direction of the slope of the terrain (wherever the slope is going down) to the y-axis rotation of the object." + + "Rounded to 90 degrees. Adds the *min* rotation of the y axis as well (to still allow you to rotate objects nicely). Discards *max* and *interval* on *yaxis*") + private boolean rotateTowardsSlope = false; + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance for this to place in a chunk. If you need multiple per chunk, set this to 1 and use density.") + private double chance = 1; + @MinNumber(1) + @Desc("If the chance check passes, place this many in a single chunk") + private int density = 1; + @Desc("If the chance check passes, and you specify this, it picks a number in the range based on noise, and 'density' is ignored.") + private IrisStyledRange densityStyle = null; + @Desc("When stilting is enabled, this object will define various properties related to it.") + private IrisStiltSettings stiltSettings; + @Desc("When a VACUUM place mode is used, this defines the deformation radius and falloff.") + private IrisVacuumSettings vacuumSettings; + @MaxNumber(64) + @MinNumber(0) + @Desc("When bore is enabled, expand max-y of the cuboid it removes") + private int boreExtendMaxY = 0; + @ArrayType(min = 1, type = IrisObjectMarker.class) + @Desc("Add markers to blocks in this object") + private KList markers = new KList<>(); + @MaxNumber(64) + @MinNumber(-1) + @Desc("When bore is enabled, lower min-y of the cuboid it removes") + private int boreExtendMinY = 0; + @Desc("If set to true, objects will place on the terrain height, ignoring the water surface.") + private boolean underwater = false; + @Desc("If set to true, objects will place in carvings (such as underground) or under an overhang.") + private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY; + @MinNumber(0) + @MaxNumber(16) + @Desc("Extra buffer, in blocks, around this object's base footprint that must also be solid, un-carved ground. 0 checks only the footprint itself.") + private int surfaceSupportBuffer = 2; + @MinNumber(1) + @MaxNumber(16) + @Desc("Minimum thickness, in blocks, of un-carved ground required under every column of this object's base footprint.") + private int surfaceSupportDepth = 2; + @Desc("Set to false to let this placement sit over carved surface openings such as cave entrances.") + private boolean requireSurfaceSupport = true; + @Desc("When carving placement is enabled, select which carved-space anchor this placement targets.") + private IrisCaveAnchorMode caveAnchorMode = IrisCaveAnchorMode.PROFILE_DEFAULT; + @Desc("If this is defined, this object wont place on the terrain heightmap, but instead on this virtual heightmap") + private IrisNoiseGenerator heightmap; + @Desc("If set to true, Iris will try to fill the insides of 'rooms' and 'pockets' where air should fit based off of raytrace checks. This prevents a village house placing in an area where a tree already exists, and instead replaces the parts of the tree where the interior of the structure is. \n\nThis operation does not affect warmed-up generation speed however it does slow down loading objects.") + private boolean smartBore = false; + @Desc("If set to true, Blocks placed underwater that could be waterlogged are waterlogged.") + private boolean waterloggable = false; + @Desc("If set to true, objects will place on the fluid height level Such as boats.") + private boolean onwater = false; + @Desc("If set to true, this object will only place parts of itself where blocks already exist. Warning: Melding is very performance intensive!") + private boolean meld = false; + @Desc("If set to true, this object will get placed from the bottom of the world up") + private boolean fromBottom; + @Desc("If set to true, this object will place from the ground up instead of height checks when not y locked to the surface. This is not compatable with X and Z axis rotations (it may look off)") + private boolean bottom = false; + @Desc("If set to true, air will be placed before the schematic places.") + private boolean bore = false; + @Desc("Use a generator to warp the field of coordinates. Using simplex for example would make a square placement warp like a flag") + private IrisGeneratorStyle warp = new IrisGeneratorStyle(NoiseStyle.FLAT); + @Desc("The placement mode") + private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT; + @ArrayType(min = 1, type = IrisObjectReplace.class) + @Desc("Find and replace blocks") + private KList edit = new KList<>(); + @Desc("Translate this object's placement") + private IrisObjectTranslate translate = new IrisObjectTranslate(); + @Desc("Scale Objects") + private IrisObjectScale scale = new IrisObjectScale(); + @ArrayType(min = 1, type = IrisObjectLoot.class) + @Desc("The loot tables to apply to these objects") + private KList loot = new KList<>(); + @ArrayType(min = 1, type = IrisObjectVanillaLoot.class) + @Desc("The vanilla loot tables to apply to these objects") + private KList vanillaLoot = new KList<>(); + @Desc("Whether the given loot tables override any and all other loot tables available in the dimension, region or biome.") + private boolean overrideGlobalLoot = false; + @Desc("This object / these objects override the following trees when they grow...") + @ArrayType(min = 1, type = IrisTree.class) + private KList trees = new KList<>(); + @RegistryListResource(IrisObject.class) + @ArrayType(type = String.class) + @Desc("List of objects to this object is allowed to collied with") + private KList allowedCollisions = new KList<>(); + @RegistryListResource(IrisObject.class) + @ArrayType(type = String.class) + @Desc("List of objects to this object is forbidden to collied with") + private KList forbiddenCollisions = new KList<>(); + @Desc("Ignore any placement restrictions for this object") + @SerializedName(value = "forcePlace", alternate = {"force"}) + private boolean forcePlace = false; + private transient AtomicCache cache = new AtomicCache<>(); + + public IrisObjectPlacement toPlacement(String... place) { + IrisObjectPlacement p = new IrisObjectPlacement(); + p.setPlace(new KList<>(place)); + p.setMode(mode); + p.setEdit(edit); + p.setTranslate(translate); + p.setScale(scale); + p.setWarp(warp); + p.setBore(bore); + p.setMeld(meld); + p.setWaterloggable(waterloggable); + p.setOnwater(onwater); + p.setFromBottom(fromBottom); + p.setBottom(bottom); + p.setSmartBore(smartBore); + p.setCarvingSupport(carvingSupport); + p.setSurfaceSupportBuffer(surfaceSupportBuffer); + p.setSurfaceSupportDepth(surfaceSupportDepth); + p.setRequireSurfaceSupport(requireSurfaceSupport); + p.setCaveAnchorMode(caveAnchorMode); + p.setUnderwater(underwater); + p.setHeightmap(heightmap); + p.setBoreExtendMaxY(boreExtendMaxY); + p.setBoreExtendMinY(boreExtendMinY); + p.setStiltSettings(stiltSettings); + p.setVacuumSettings(vacuumSettings); + p.setDensity(density); + p.setDensityStyle(densityStyle); + p.setChance(chance); + p.setSnow(snow); + p.setClamp(clamp); + p.setRotation(rotation); + p.setSlopeCondition(slopeCondition); + p.setRotateTowardsSlope(rotateTowardsSlope); + p.setDolphinTarget(isDolphinTarget); + p.setMarkers(markers); + p.setLoot(loot); + p.setVanillaLoot(vanillaLoot); + p.setOverrideGlobalLoot(overrideGlobalLoot); + p.setTrees(trees); + p.setAllowedCollisions(allowedCollisions); + p.setForbiddenCollisions(forbiddenCollisions); + p.setForcePlace(forcePlace); + return p; + } + + public CNG getSurfaceWarp(RNG rng, IrisData data) { + return getSurfaceWarp(rng, data, data.getEngine()); + } + + public CNG getSurfaceWarp(RNG rng, IrisData data, @Nullable Engine engine) { + long seed = engine == null ? rng.getSeed() : engine.getSeedManager().getComponent() + 1024L; + SurfaceWarpCacheKey key = new SurfaceWarpCacheKey(data, engine, seed); + return surfaceWarpCache.computeIfAbsent(key, + ignored -> getWarp().create(new RNG(seed), data, engine)); + } + + public double warp(RNG rng, double x, double y, double z, IrisData data) { + return getSurfaceWarp(rng, data).fitDouble(-(getWarp().getMultiplier() / 2D), (getWarp().getMultiplier() / 2D), x, y, z); + } + + public IrisObject getObject(DataProvider g, RNG random) { + if (place.isEmpty()) { + return null; + } + + return g.getData().getObjectLoader().load(place.get(random.nextInt(place.size()))); + } + + public boolean matches(IrisTreeSize size, TreeType type) { + for (IrisTree i : getTrees()) { + if (i.matches(size, type)) { + return true; + } + } + + return false; + } + + public int getDensity() { + if (densityStyle == null) { + return density; + } + return densityStyle.getMid(); + } + + public int getDensity(RNG rng, double x, double z, IrisData data) { + if (densityStyle == null) { + return density; + } + + return (int) Math.round(densityStyle.get(rng, x, z, data)); + } + + private TableCache getCache(IrisData manager) { + return cache.aquire(() -> { + TableCache cache = new TableCache(); + + cache.merge(getCache(manager, getVanillaLoot(), IrisObjectPlacement::getVanillaTable)); + cache.merge(getCache(manager, getLoot(), manager.getLootLoader()::load)); + + return cache; + }); + } + + private TableCache getCache(IrisData manager, KList list, Function loader) { + TableCache tc = new TableCache(); + + for (IObjectLoot loot : list) { + if (loot == null || loot.getWeight() <= 0) + continue; + IrisLootTable table = loader.apply(loot.getName()); + if (table == null) { + IrisLogging.warn("Couldn't find loot table " + loot.getName()); + continue; + } + + if (loot.getFilter().isEmpty()) //Table applies to all containers + { + tc.global.put(table, loot.getWeight()); + } else if (!loot.isExact()) //Table is meant to be by type + { + for (PlatformBlockState filterState : loot.getFilter(manager)) { + BlockData filterData = (BlockData) filterState.nativeHandle(); + if (!tc.basic.containsKey(filterData.getMaterial())) { + tc.basic.put(filterData.getMaterial(), new WeightedTables()); + } + + tc.basic.get(filterData.getMaterial()).put(table, loot.getWeight()); + } + } else //Filter is exact + { + for (PlatformBlockState filterState : loot.getFilter(manager)) { + BlockData filterData = (BlockData) filterState.nativeHandle(); + if (!tc.exact.containsKey(filterData.getMaterial())) { + tc.exact.put(filterData.getMaterial(), new KMap<>()); + } + + if (!tc.exact.get(filterData.getMaterial()).containsKey(filterData)) { + tc.exact.get(filterData.getMaterial()).put(filterData, new WeightedTables()); + } + + tc.exact.get(filterData.getMaterial()).get(filterData).put(table, loot.getWeight()); + } + } + } + return tc; + } + + @Nullable + private static IrisVanillaLootTable getVanillaTable(String name) { + return Optional.ofNullable(NamespacedKey.fromString(name)) + .map(Bukkit::getLootTable) + // Hand over the key, not the LootTable: IrisVanillaLootTable holds no Bukkit fields. + .map(table -> new IrisVanillaLootTable(String.valueOf(table.getKey()))) + .orElse(null); + } + + /** + * Gets the loot table that should be used for the block + * + * @param state The block state of the block + * @param dataManager Iris Data Manager + * @return The loot table it should use. + */ + public IrisLootTable getTable(PlatformBlockState state, IrisData dataManager, RNG rng) { + TableCache cache = getCache(dataManager); + BlockData data = (BlockData) state.nativeHandle(); + if (BukkitBlockResolution.isStorageChest(data)) { + IrisLootTable picked = null; + if (cache.exact.containsKey(data.getMaterial()) && cache.exact.get(data.getMaterial()).containsKey(data)) { + picked = cache.exact.get(data.getMaterial()).get(data).pullRandom(rng); + } else if (cache.basic.containsKey(data.getMaterial())) { + picked = cache.basic.get(data.getMaterial()).pullRandom(rng); + } else if (!cache.global.isEmpty()) { + picked = cache.global.pullRandom(rng); + } + + return picked; + } + + return null; + } + + private static final class SurfaceWarpCacheKey { + private final IrisData data; + private final Engine engine; + private final long seed; + + private SurfaceWarpCacheKey(IrisData data, Engine engine, long seed) { + this.data = data; + this.engine = engine; + this.seed = seed; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SurfaceWarpCacheKey key)) { + return false; + } + return data == key.data && engine == key.engine && seed == key.seed; + } + + @Override + public int hashCode() { + int result = System.identityHashCode(data); + result = 31 * result + System.identityHashCode(engine); + return 31 * result + Long.hashCode(seed); + } + } + + private static class TableCache { + final transient WeightedTables global = new WeightedTables(); + final transient KMap basic = new KMap<>(); + final transient KMap> exact = new KMap<>(); + + private void merge(TableCache other) { + global.merge(other.global); + basic.merge(other.basic, WeightedTables::merge); + exact.merge(other.exact, (a, b) -> a.merge(b, WeightedTables::merge)); + } + } + + private static final class WeightedTables { + private final KList tables = new KList<>(); + + private void put(IrisLootTable table, int weight) { + tables.add(new WeightedTable(table, weight)); + } + + private IrisLootTable pullRandom(RNG rng) { + WeightedTable picked = LootResolver.pickWeighted(tables, WeightedTable::weight, rng); + return picked == null ? null : picked.table(); + } + + private boolean isEmpty() { + return tables.isEmpty(); + } + + private WeightedTables merge(WeightedTables other) { + tables.addAll(other.tables); + return this; + } + } + + private record WeightedTable(IrisLootTable table, int weight) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java new file mode 100644 index 000000000..1cdf98bc1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java @@ -0,0 +1,1232 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeStructureVolume; +import art.arcane.iris.engine.framework.PlacedObject; +import art.arcane.iris.engine.framework.placer.HeightmapObjectPlacer; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.project.noise.SimplexNoise; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterMarker; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.stream.StreamSupport; + +/** + * One-shot placement of an {@link IrisObject} into the world. Instances are cheap and single use: they exist so + * the placement pass can hoist its per-placement invariants without leaking that state onto the loader-cached + * (and thread-shared) object itself. + */ +final class IrisObjectPlacementRunner { + private static final long IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS = 5000L; + private static final long VACUUM_WAVE_SEED = 7392113L; + private static final ConcurrentHashMap IMPLAUSIBLE_BEDROCK_WARNS = new ConcurrentHashMap<>(); + + private final IrisObject self; + + IrisObjectPlacementRunner(IrisObject self) { + this.self = self; + } + + int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer listener, CarveResult c, IrisData rdata) { + IObjectPlacer placer = config.getHeightmap() != null ? new HeightmapObjectPlacer(rng, x, yv, z, config, oplacer) : oplacer; + + boolean evaluateSlopeCondition = !config.isForcePlace() && !config.getSlopeCondition().isDefault(); + if (rdata != null && (evaluateSlopeCondition || config.isRotateTowardsSlope())) { + Engine placementEngine = requireSlopeEngine(placer); + if (evaluateSlopeCondition && + !config.getSlopeCondition().isValid(placementEngine.getComplex().getSlopeStream().get(x, z))) { + return -1; + } + + if (config.isRotateTowardsSlope()) { + int slopeRotationY = 0; + ProceduralStream heightStream = placementEngine.getComplex().getHeightStream(); + // Whichever side of the rectangle that bounds the object is lowest is the 'direction' of the slope (simply said). + double hNorth = heightStream.get(x, z + ((float) self.d) / 2); + double hEast = heightStream.get(x + ((float) self.w) / 2, z); + double hSouth = heightStream.get(x, z - ((float) self.d) / 2); + double hWest = heightStream.get(x - ((float) self.w) / 2, z); + double min = Math.min(Math.min(hNorth, hEast), Math.min(hSouth, hWest)); + if (min == hNorth) { + slopeRotationY = 0; + } else if (min == hEast) { + slopeRotationY = 90; + } else if (min == hSouth) { + slopeRotationY = 180; + } else if (min == hWest) { + slopeRotationY = 270; + } + + double newRotation = config.getRotation().getYAxis().getMin() + slopeRotationY; + IrisObjectRotation originalRotation = config.getRotation(); + IrisObjectRotation slopeRotation = new IrisObjectRotation(); + slopeRotation.setXAxis(originalRotation.getXAxis()); + slopeRotation.setZAxis(originalRotation.getZAxis()); + if (newRotation == 0) { + slopeRotation.setYAxis(new IrisAxisRotationClamp(false, false, 0, 0, 90)); + slopeRotation.setEnabled(originalRotation.canRotateX() || originalRotation.canRotateZ()); + } else { + slopeRotation.setYAxis(new IrisAxisRotationClamp(true, false, newRotation, newRotation, 90)); + slopeRotation.setEnabled(true); + } + config = config.toPlacement(config.getPlace().toArray(new String[0])); + config.setRotation(slopeRotation); + } + } + + if (config.isSmartBore()) { + IrisObjectShaping.ensureSmartBored(self, placer.isDebugSmartBore()); + } + + boolean warped = !config.getWarp().isFlat(); + // Placement-invariant hoists off the per-voxel loop: the warp CNG resolution went + // through a keyed cache probe per block, and the two boolean gates below cut per-block + // property scans that can never match for this placement. + CNG surfaceWarp = warped ? config.getSurfaceWarp(rng, self.getLoader()) : null; + double warpHalf = warped ? config.getWarp().getMultiplier() / 2D : 0D; + boolean preventingDecay = placer.isPreventingDecay(); + boolean waterlogCandidate = config.isWaterloggable() || config.isUnderwater(); + boolean rawStructurePiece = config.getMode() == ObjectPlaceMode.STRUCTURE_PIECE; + boolean organicFloor = config.getMode() == ObjectPlaceMode.ORGANIC_STILT; + boolean ceilingHang = config.getMode() == ObjectPlaceMode.CEILING_HANG; + boolean organic = organicFloor || ceilingHang; + boolean vacuuming = IrisObjectVacuum.isVacuumMode(config.getMode()); + boolean stilting = (config.getMode().equals(ObjectPlaceMode.STILT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT) || + config.getMode() == ObjectPlaceMode.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT || + config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT || organic); + boolean eroding = config.getMode() == ObjectPlaceMode.ERODE_STILT; + KMap heightmap = config.getSnow() > 0 ? new KMap<>() : null; + int spinx = rng.imax() / 1000; + int spiny = rng.imax() / 1000; + int spinz = rng.imax() / 1000; + int rty = config.getRotation().rotate(new IrisBlockVector(0, self.getCenter().getBlockY(), 0), spinx, spiny, spinz).getBlockY(); + int ty = config.getTranslate().translate(new IrisBlockVector(0, self.getCenter().getBlockY(), 0), config.getRotation(), spinx, spiny, spinz).getBlockY(); + // Per-placement invariants. The rotation kernel, the rotated translate offset and the edit-list emptiness + // are pure functions of (config, spin), all of which are fixed from here down. Recomputing them per block + // only cost allocations and transcendentals. + SpinKernel spin = new SpinKernel(config.getRotation(), spinx, spiny, spinz); + IrisObjectTranslate translate = config.getTranslate(); + boolean translating = translate.canTranslate(); + IrisBlockVector translateOffset = translating + ? config.getRotation().rotate(new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ()), spinx, spiny, spinz) + : null; + boolean hasEdits = !config.getEdit().isEmpty(); + int y = -1; + int xx, zz; + int yrand = config.getTranslate().getYRandom(); + yrand = yrand > 0 ? rng.i(0, yrand) : yrand < 0 ? rng.i(yrand, 0) : yrand; + boolean bail = false; + + if (config.isFromBottom()) { + // todo Convert this to a dedicated mode. + y = (self.getH() + 1) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } else if (yv < 0) { + if (config.getMode().equals(ObjectPlaceMode.CENTER_HEIGHT) || config.getMode() == ObjectPlaceMode.CENTER_STILT + || organic || vacuuming) { + y = (c != null ? c.getSurface() : placer.getHighest(x, z, self.getLoader(), config.isUnderwater())) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } else if (config.getMode().equals(ObjectPlaceMode.MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.STILT)) { + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX(); + int minX = Math.min(x - xLength, x + xLength); + int maxX = Math.max(x - xLength, x + xLength); + int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ(); + int minZ = Math.min(z - zLength, z + zLength); + int maxZ = Math.max(z - zLength, z + zLength); + for (int i = minX; i <= maxX; i++) { + for (int ii = minZ; ii <= maxZ; ii++) { + int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { + bail = true; + break; + } + } + if (h > y) + y = h; + } + } + } else if (config.getMode().equals(ObjectPlaceMode.FAST_MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT)) { + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + + int xRadius = (rotatedDimensions.getBlockX() / 2); + int xLength = xRadius + offset.getBlockX(); + int minX = Math.min(x - xLength, x + xLength); + int maxX = Math.max(x - xLength, x + xLength); + int zRadius = (rotatedDimensions.getBlockZ() / 2); + int zLength = zRadius + offset.getBlockZ(); + int minZ = Math.min(z - zLength, z + zLength); + int maxZ = Math.max(z - zLength, z + zLength); + + for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) { + for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { + int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { + bail = true; + break; + } + } + if (h > y) + y = h; + } + } + } else if (config.getMode().equals(ObjectPlaceMode.MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.MIN_STILT) { + y = rdata.getEngine().getHeight() + 1; + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + + int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX(); + int minX = Math.min(x - xLength, x + xLength); + int maxX = Math.max(x - xLength, x + xLength); + int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ(); + int minZ = Math.min(z - zLength, z + zLength); + int maxZ = Math.max(z - zLength, z + zLength); + for (int i = minX; i <= maxX; i++) { + for (int ii = minZ; ii <= maxZ; ii++) { + int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { + bail = true; + break; + } + } + if (h < y) { + y = h; + } + } + } + } else if (config.getMode().equals(ObjectPlaceMode.FAST_MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT) { + y = rdata.getEngine().getHeight() + 1; + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + + int xRadius = (rotatedDimensions.getBlockX() / 2); + int xLength = xRadius + offset.getBlockX(); + int minX = Math.min(x - xLength, x + xLength); + int maxX = Math.max(x - xLength, x + xLength); + int zRadius = (rotatedDimensions.getBlockZ() / 2); + int zLength = zRadius + offset.getBlockZ(); + int minZ = Math.min(z - zLength, z + zLength); + int maxZ = Math.max(z - zLength, z + zLength); + + for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) { + for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { + int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { + bail = true; + break; + } + } + if (h < y) { + y = h; + } + } + } + } else if (config.getMode().equals(ObjectPlaceMode.PAINT)) { + y = placer.getHighest(x, z, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } else if (config.getMode().equals(ObjectPlaceMode.FLOATING)) { + y = rty; + } + } else { + y = yv; + if (!config.isForcePlace() && !rawStructurePiece) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } + + if (yv >= 0 && config.isBottom() && !rawStructurePiece) { + y += Math.floorDiv(self.h, 2); + CarvingMode carvingMode = config.getCarvingSupport(); + if (!config.isForcePlace() && !carvingMode.equals(CarvingMode.CARVING_ONLY)) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } + + if (yv < 0 + && !config.isForcePlace() + && !config.isFromBottom() + && config.getMode() != ObjectPlaceMode.FLOATING + && !rawStructurePiece + && config.getCarvingSupport().supportsSurface() + && placer.getEngine() != null + && placer.getEngine().getDimension().isBedrock() + && y <= 1) { + warnImplausibleBedrockPlacement(placer, config, x, y, z); + return -1; + } + + if (bail && !config.isForcePlace()) { + return -1; + } + + // Surface-anchored placements may never roof or bridge a carved hole. Explicit-Y anchors are only + // guarded for SURFACE_ONLY: ANYWHERE covers the inverted upper dimension and CARVING_ONLY covers + // cave anchors, and neither reads the terrain surface this stencil samples. + boolean surfaceAnchored = yv < 0 + ? config.getCarvingSupport().supportsSurface() + : config.getCarvingSupport() == CarvingMode.SURFACE_ONLY; + if (surfaceAnchored + && !config.isForcePlace() + && !config.isFromBottom() + && config.getMode() != ObjectPlaceMode.FLOATING + && !rawStructurePiece + && !config.isUnderwater() + && !config.isOnwater() + && config.isRequireSurfaceSupport() + && IrisSurfaceSupport.isUnsupported(oplacer, self.getLoader(), x, z, config.getTranslate(), + config.getRotation(), spinx, spiny, spinz, self.getSurfaceSupportOffsets(), + config.getSurfaceSupportBuffer(), config.getSurfaceSupportDepth())) { + return -1; + } + + if (yv < 0 && !config.getMode().equals(ObjectPlaceMode.FLOATING) && !rawStructurePiece) { + if (!config.isForcePlace() && !config.isUnderwater() && !config.isOnwater() && placer.isUnderwater(x, z)) { + return -1; + } + } + + if (!config.isForcePlace() && !rawStructurePiece && c != null && Math.max(0, self.h + yrand + ty) + 1 >= c.getHeight()) { + return -1; + } + + if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) { + return -1; + } + + if (!config.isForcePlace() && !rawStructurePiece && !config.getClamp().canPlace(y + rty + ty, y - rty + ty)) { + return -1; + } + + int warpMargin = warped ? (int) Math.ceil(Math.abs(config.getWarp().getMultiplier()) / 2D) : 0; + if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, translating, translateOffset, ceilingHang, + yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) { + return -1; + } + + if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) { + Engine engine = rdata.getEngine(); + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + for (int i = x - Math.floorDiv(self.w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(self.w, 2) - (self.w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) { + for (int j = y - Math.floorDiv(self.h, 2) + (int) offset.getY(); j <= y + Math.floorDiv(self.h, 2) - (self.h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) { + for (int k = z - Math.floorDiv(self.d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(self.d, 2) - (self.d % 2 == 0 ? 1 : 0) + (int) offset.getZ(); k++) { + PlacedObject p = engine.getObjectPlacement(i, j, k); + if (p == null) continue; + IrisObject o = p.getObject(); + if (o == null) continue; + String key = o.getLoadKey(); + if (key != null) { + if (config.getForbiddenCollisions().contains(key) && !config.getAllowedCollisions().contains(key)) { + return -1; + } + } + } + } + } + } + + if (config.isBore()) { + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + for (int i = x - Math.floorDiv(self.w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(self.w, 2) - (self.w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) { + for (int j = y - Math.floorDiv(self.h, 2) - config.getBoreExtendMinY() + (int) offset.getY(); j <= y + Math.floorDiv(self.h, 2) + config.getBoreExtendMaxY() - (self.h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) { + for (int k = z - Math.floorDiv(self.d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(self.d, 2) - (self.d % 2 == 0 ? 1 : 0) + (int) offset.getZ(); k++) { + placer.set(i, j, k, IrisObject.States.AIR); + } + } + } + } + + int lowest = Integer.MAX_VALUE; + int topLayer = Integer.MIN_VALUE; + int vacuumLowest = Integer.MAX_VALUE; + int vacuumHighest = Integer.MIN_VALUE; + y += yrand; + self.readLock.lock(); + + KMap markers = null; + + try { + VectorMap blocks = self.blocks; + VectorMap states = self.states; + // Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null. + boolean hasStates = !states.isEmpty(); + + if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) { + markers = new KMap<>(); + var list = StreamSupport.stream(blocks.keys().spliterator(), false) + .collect(KList.collector()); + // Marker selection persists into the mantle, so it must be seed-deterministic. + // Derive a side stream keyed on the placement position instead of consuming the + // caller's rng: consuming draws there would shift every later placement in the + // chunk and invalidate existing worlds/goldenhashes. + RNG markerRng = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + yv); + + int markerIndex = 0; + for (IrisObjectMarker j : config.getMarkers()) { + IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker()); + int markerSalt = markerIndex++; + + if (marker == null) { + continue; + } + + int max = j.getMaximumMarkers(); + for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) { + if (max <= 0) { + break; + } + + PlatformBlockState data = blocks.get(i); + if (data == null) { + continue; + } + + for (PlatformBlockState k : j.getMark(rdata)) { + if (max <= 0) { + break; + } + + if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) { + boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0))); + boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0))); + + if (!marker.isEmptyAbove() || (a && fff)) { + markers.put(i, j.getMarker()); + max--; + } + } + } + } + } + } + + VectorMap.Cursor cursor = blocks.cursor(); + while (cursor.next()) { + IrisBlockVector g = cursor.key(); + PlatformBlockState d; + TileData tile = null; + + try { + d = cursor.value(); + if (hasStates) { + tile = states.get(g); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (cme)"); + d = IrisObject.States.AIR; + } + + if (d == null) { + IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (null)"); + d = IrisObject.States.AIR; + } + + PlatformBlockState data = d; + IrisBlockVector i = g.clone(); + spin.rotate(i); + if (ceilingHang) { + i.setY(-i.getBlockY()); + } + if (translating) { + i.add(translateOffset); + } + + if (stilting && IrisObjectShaping.shouldStilt(data)) { + if (i.getBlockY() < lowest) { + lowest = i.getBlockY(); + } + if (i.getBlockY() > topLayer) { + topLayer = i.getBlockY(); + } + } + + if (preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance") && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))) { + data = data.withProperty("persistent", "true"); + } + + if (hasEdits) { + for (IrisObjectReplace j : config.getEdit()) { + if (rng.chance(j.getChance())) { + for (PlatformBlockState k : j.getFind(rdata)) { + if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) { + PlatformBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); + + if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data)) && !(newData.isCustom() || data.isCustom())) + data = BlockDataMergeSupport.merge(data, newData); + else + data = newData; + + Optional t = j.getReplace().getTile(rng, x, y, z, rdata); + if (t.isPresent()) { + tile = t.get(); + } + } + } + } + } + } + + data = config.getRotation().rotate(data, spinx, spiny, spinz); + xx = x + (int) Math.round(i.getX()); + + int yy = y + (int) Math.round(i.getY()); + zz = z + (int) Math.round(i.getZ()); + + if (warped) { + xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z); + zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x); + } + + if (yv < 0 && (config.getMode().equals(ObjectPlaceMode.PAINT)) && !B.isVineBlock(data)) { + yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater()); + } + + if (heightmap != null) { + Position2 pos = new Position2(xx, zz); + + if (!heightmap.containsKey(pos)) { + heightmap.put(pos, yy); + } + + if (heightmap.get(pos) < yy) { + heightmap.put(pos, yy); + } + } + + if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) { + continue; + } + + if (waterlogCandidate && IrisProceduralBlocks.hasProperty(data, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, yy, zz)) { + data = data.withProperty("waterlogged", "true"); + } + + if (B.isVineBlock(data)) { + data = attachVineFaces(placer, data, xx, yy, zz); + } + + // Short-circuit order matters for cost only: the mantle read is paid solely + // for vine blocks. Both operands are pure, so the value is unchanged. + boolean wouldReplace = B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz)); + String material = IrisObjectShaping.materialKey(data); + boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air"); + boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace); + + if (data.isCustom() || place) { + placer.set(xx, yy, zz, data); + if (tile != null) { + placer.setTile(xx, yy, zz, tile); + } + if (markers != null && markers.containsKey(g)) { + placer.setData(xx, yy, zz, new MatterMarker(markers.get(g))); + } + if (listener != null) { + listener.accept(new BlockPosition(xx, yy, zz), data); + } + if (vacuuming && yy < vacuumLowest) { + vacuumLowest = yy; + } + if (vacuuming && yy > vacuumHighest) { + vacuumHighest = yy; + } + } + } + } catch (Throwable e) { + e.printStackTrace(); + IrisLogging.reportError(e); + } finally { + self.readLock.unlock(); + } + + if (stilting) { + self.readLock.lock(); + try { + VectorMap blocks = self.blocks; + IrisStiltSettings settings = config.getStiltSettings(); + + double erodeCentroidX = 0; + double erodeCentroidZ = 0; + double erodeMaxDist = 1; + if (eroding) { + int centroidCount = 0; + VectorMap.Cursor centroidCursor = blocks.cursor(); + while (centroidCursor.next()) { + IrisBlockVector rot = centroidCursor.key().clone(); + spin.rotate(rot); + if (translating) { + rot.add(translateOffset); + } + if (rot.getBlockY() == lowest) { + PlatformBlockState bd = centroidCursor.value(); + if (bd != null && IrisObjectShaping.shouldStilt(bd)) { + erodeCentroidX += rot.getX(); + erodeCentroidZ += rot.getZ(); + centroidCount++; + } + } + } + if (centroidCount > 0) { + erodeCentroidX /= centroidCount; + erodeCentroidZ /= centroidCount; + } + VectorMap.Cursor spreadCursor = blocks.cursor(); + while (spreadCursor.next()) { + IrisBlockVector rot = spreadCursor.key().clone(); + spin.rotate(rot); + if (translating) { + rot.add(translateOffset); + } + if (rot.getBlockY() == lowest) { + PlatformBlockState bd = spreadCursor.value(); + if (bd != null && IrisObjectShaping.shouldStilt(bd)) { + double dx = rot.getX() - erodeCentroidX; + double dz = rot.getZ() - erodeCentroidZ; + double dist = Math.sqrt(dx * dx + dz * dz); + if (dist > erodeMaxDist) { + erodeMaxDist = dist; + } + } + } + } + } + + VectorMap.Cursor stiltCursor = blocks.cursor(); + while (stiltCursor.next()) { + IrisBlockVector g = stiltCursor.key(); + PlatformBlockState sourceData; + try { + sourceData = stiltCursor.value(); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt cme)"); + sourceData = IrisObject.States.AIR; + } + + if (sourceData == null) { + IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt null)"); + sourceData = IrisObject.States.AIR; + } + + if (!IrisObjectShaping.shouldStilt(sourceData)) { + continue; + } + + PlatformBlockState d = sourceData; + if (settings != null && settings.getPalette() != null) { + d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata); + } else { + String mat = IrisObjectShaping.materialKey(d); + if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) { + d = B.getState("minecraft:dirt"); + } + } + + IrisBlockVector i = g.clone(); + spin.rotate(i); + if (ceilingHang) { + i.setY(-i.getBlockY()); + } + if (translating) { + i.add(translateOffset); + } + d = config.getRotation().rotate(d, spinx, spiny, spinz); + + int targetLayer = ceilingHang ? topLayer : lowest; + if (i.getBlockY() != targetLayer) + continue; + + if (hasEdits) { + for (IrisObjectReplace j : config.getEdit()) { + if (rng.chance(j.getChance())) { + for (PlatformBlockState k : j.getFind(rdata)) { + if (d == null) { + continue; + } + if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) { + PlatformBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); + + if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) { + d = BlockDataMergeSupport.merge(d, newData); + } else { + d = newData; + } + } + } + } + } + } + + if (d == null || !d.isOccluding()) + continue; + + xx = x + (int) Math.round(i.getX()); + zz = z + (int) Math.round(i.getZ()); + + if (warped) { + xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z); + zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x); + } + + if (organic) { + int startY = targetLayer + y; + int maxScan = settings != null ? Math.max(1, settings.getOrganicMaxScan()) : 48; + int jitterMax = settings != null ? Math.max(0, settings.getOrganicJitter()) : 3; + double scratch = settings != null ? Math.max(0, Math.min(1, settings.getOrganicScratch())) : 0.55; + long colHash = ((long) xx * 341873128712L) ^ ((long) zz * 132897987541L); + int jitter = jitterMax > 0 ? (int) (Math.abs(colHash) % (jitterMax + 1)) : 0; + + if (ceilingHang) { + int scan = 0; + int solidY = startY + 1; + while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { + solidY++; + scan++; + } + int topBound = (scan < maxScan ? solidY - 1 : startY + Math.min(maxScan, 8)) - jitter; + int total = topBound - startY; + for (int j = startY; j <= topBound; j++) { + if (scratch > 0 && total > 0) { + double ratio = (double) (j - startY) / total; + if (ratio > (1.0 - scratch)) { + long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); + double skipChance = (ratio - (1.0 - scratch)) / scratch; + if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + placer.set(xx, j, zz, d); + } + } else { + int scan = 0; + int solidY = startY - 1; + while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { + solidY--; + scan++; + } + int bottomBound = (scan < maxScan ? solidY + 1 : startY - Math.min(maxScan, 8)) + jitter; + int total = startY - bottomBound; + for (int j = startY; j >= bottomBound; j--) { + if (scratch > 0 && total > 0) { + double ratio = (double) (startY - j) / total; + if (ratio > (1.0 - scratch)) { + long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); + double skipChance = (ratio - (1.0 - scratch)) / scratch; + if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + placer.set(xx, j, zz, d); + } + } + continue; + } + + int highest = placer.getHighest(xx, zz, self.getLoader(), true); + + if (IrisProceduralBlocks.hasProperty(d, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz)) { + d = d.withProperty("waterlogged", "true"); + } + + int lowerBound = highest - 1; + if (settings != null) { + lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand()); + if (settings.getYMax() != 0) + lowerBound -= Math.min(config.getStiltSettings().getYMax() - (lowest + y - highest), 0); + } + + if (eroding) { + double dx = i.getX() - erodeCentroidX; + double dz = i.getZ() - erodeCentroidZ; + double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist; + normalizedDist = Math.min(normalizedDist, 1.0); + int totalDepth = (lowest + y) - lowerBound; + int erodeDepth = (int) (totalDepth * Math.pow(1.0 - normalizedDist, 1.5)); + lowerBound = (lowest + y) - erodeDepth; + } + + for (int j = lowest + y; j > lowerBound; j--) { + PlatformBlockState fluidState = placer.get(xx, j, zz); + if (B.isFluid(fluidState)) { + break; + } + if (eroding) { + int depth = (lowest + y) - j; + int totalDepth = (lowest + y) - lowerBound; + double depthRatio = totalDepth > 0 ? (double) depth / totalDepth : 0; + if (depthRatio > 0.4) { + long hash = ((long) (xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L)); + double skipChance = (depthRatio - 0.4) / 0.6; + if ((Math.abs(hash) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + + if (B.isVineBlock(d)) { + d = attachVineFaces(placer, d, xx, j, zz); + } + placer.set(xx, j, zz, d); + } + + } + } finally { + self.readLock.unlock(); + } + } + + if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) { + IrisBlockVector rotDim = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + int lowX = IrisObjectVacuum.footprintLow(rotDim.getBlockX()); + int highX = IrisObjectVacuum.footprintHigh(rotDim.getBlockX()); + int lowZ = IrisObjectVacuum.footprintLow(rotDim.getBlockZ()); + int highZ = IrisObjectVacuum.footprintHigh(rotDim.getBlockZ()); + int centerX = x + config.getTranslate().getX(); + int centerZ = z + config.getTranslate().getZ(); + vacuumTerrain(placer, config, centerX, centerZ, lowX, highX, lowZ, highZ, vacuumLowest, vacuumHighest); + } + + if (heightmap != null) { + RNG rngx = rng.nextParallelRNG(3468854); + + for (Position2 i : heightmap.k()) { + int vx = i.getX(); + int vy = heightmap.get(i); + int vz = i.getZ(); + + if (config.getSnow() > 0) { + int height = rngx.i(0, (int) (config.getSnow() * 7)); + placer.set(vx, vy + 1, vz, IrisObject.States.SNOW_LAYERS[Math.max(Math.min(height, 7), 0)]); + } + } + } + + return y; + } + + static boolean shouldPlaceObjectBlock(boolean rawStructurePiece, boolean air, boolean wouldReplace) { + return !wouldReplace && (rawStructurePiece || !air); + } + + private static Engine requireSlopeEngine(IObjectPlacer placer) { + Engine engine = placer.getEngine(); + if (engine == null) { + throw new IllegalStateException("Object placement slope settings require an active Iris engine for the target world."); + } + return engine; + } + + /** + * Objects may never intersect a native structure piece. The rect query is the fast path: no native structures + * near this placement means no per block work at all. Only when the placement envelope meets a piece does the + * precise pass run, and the first solid block inside a piece rejects the whole object before any write. + */ + private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin, + boolean translating, IrisBlockVector translateOffset, boolean ceilingHang, + boolean paint, int warpMargin, int x, int y, int z) { + Engine engine = placer.getEngine(); + if (engine == null) { + return false; + } + + int margin = (Math.max(self.getW(), Math.max(self.getH(), self.getD())) / 2) + 1 + warpMargin; + if (translating) { + margin += Math.max(Math.abs(translateOffset.getBlockX()), + Math.max(Math.abs(translateOffset.getBlockY()), Math.abs(translateOffset.getBlockZ()))); + } + + KList volumes = engine.getNativeStructureVolumes(x - margin, z - margin, x + margin, z + margin); + if (volumes == null || volumes.isEmpty()) { + return false; + } + + int worldY = y + engine.getMinHeight(); + int envelopeMinY = paint ? Integer.MIN_VALUE : worldY - margin; + int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + margin; + boolean envelopeMeetsPiece = false; + for (NativeStructureVolume volume : volumes) { + if (volume.intersects(x - margin, envelopeMinY, z - margin, x + margin, envelopeMaxY, z + margin)) { + envelopeMeetsPiece = true; + break; + } + } + + if (!envelopeMeetsPiece) { + return false; + } + + self.readLock.lock(); + try { + VectorMap.Cursor cursor = self.blocks.cursor(); + while (cursor.next()) { + PlatformBlockState state = cursor.value(); + if (state == null || isAirBlock(state)) { + continue; + } + + IrisBlockVector i = cursor.key().clone(); + spin.rotate(i); + if (ceilingHang) { + i.setY(-i.getBlockY()); + } + if (translating) { + i.add(translateOffset); + } + + int xx = x + (int) Math.round(i.getX()); + int zz = z + (int) Math.round(i.getZ()); + int yy = paint + ? (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + + placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater()) + : y + (int) Math.round(i.getY()); + int worldBlockY = yy + engine.getMinHeight(); + + for (NativeStructureVolume volume : volumes) { + if (volume.containsWithin(xx, worldBlockY, zz, warpMargin)) { + return true; + } + } + } + } finally { + self.readLock.unlock(); + } + + return false; + } + + private static boolean isAirBlock(PlatformBlockState state) { + String material = IrisObjectShaping.materialKey(state); + return material.equals("minecraft:air") || material.equals("minecraft:cave_air"); + } + + private void warnImplausibleBedrockPlacement(IObjectPlacer placer, IrisObjectPlacement config, int x, int y, int z) { + String key = self.getLoadKey(); + String fingerprint = (key == null ? "" : key) + "|" + config.getMode(); + long now = System.currentTimeMillis(); + Long last = IMPLAUSIBLE_BEDROCK_WARNS.get(fingerprint); + if (last != null && now - last < IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS) { + return; + } + IMPLAUSIBLE_BEDROCK_WARNS.put(fingerprint, now); + IrisLogging.warn("Implausible object placement rejected: " + + (key == null ? "" : key) + + " resolved anchorY=" + y + " at (" + x + "," + z + ") mode=" + config.getMode() + + " carving=" + config.getCarvingSupport() + + ". Surface-anchored placement should never land on the bedrock row. " + + "Height sampling returned a bogus value — not configured for floor placement " + + "(forcePlace=false, fromBottom=false, mode!=FLOATING). Skipping to protect bedrock."); + } + + private void vacuumTerrain(IObjectPlacer placer, IrisObjectPlacement config, int centerX, int centerZ, int lowX, int highX, int lowZ, int highZ, int baseY, int topY) { + ObjectPlaceMode mode = config.getMode(); + IrisVacuumSettings settings = config.getVacuumSettings(); + int radius = IrisObjectVacuum.resolveRadius(mode, settings); + int step = IrisObjectVacuum.resolveStep(mode); + double falloff = IrisObjectVacuum.resolveFalloff(settings); + int jitter = settings != null ? Math.max(0, settings.getOrganicJitter()) : 4; + boolean organicEdge = mode == ObjectPlaceMode.VACUUM_ORGANIC; + boolean wavyEdge = mode == ObjectPlaceMode.VACUUM_WAVY; + double waveAmplitude = IrisObjectVacuum.resolveWaveAmplitude(settings); + double waveScale = IrisObjectVacuum.resolveWaveScale(settings); + SimplexNoise waveNoise = (wavyEdge && waveAmplitude > 0) ? new SimplexNoise(VACUUM_WAVE_SEED) : null; + int meetY = baseY - 1; + + IrisComplex complex = placer.getEngine().getComplex(); + int worldMin = placer.getEngine().getMinHeight(); + int worldMax = worldMin + placer.getEngine().getHeight() - 1; + + for (int dx = lowX - radius; dx <= highX + radius; dx += step) { + for (int dz = lowZ - radius; dz <= highZ + radius; dz += step) { + int cx = centerX + dx; + int cz = centerZ + dz; + double effRadius = radius; + if (organicEdge && jitter > 0) { + long h = ((long) cx * 341873128712L) ^ ((long) cz * 132897987541L); + double n = ((Math.abs(h) % 1000) / 1000.0) - 0.5; + effRadius = Math.max(1.0, radius + (n * 2.0 * jitter)); + } + int origY = placer.getHighest(cx, cz, self.getLoader(), true); + int targetY = IrisObjectVacuum.columnTargetY(dx, dz, lowX, highX, lowZ, highZ, effRadius, falloff, origY, meetY); + if (waveNoise != null) { + int outX = IrisObjectVacuum.outset(dx, lowX, highX); + int outZ = IrisObjectVacuum.outset(dz, lowZ, highZ); + double waveDistance = Math.sqrt((double) (outX * outX) + (double) (outZ * outZ)); + double sample = waveNoise.noiseSigned(cx * waveScale, cz * waveScale); + targetY += IrisObjectVacuum.waveOffset(waveDistance, effRadius, sample, waveAmplitude); + } + if (targetY == origY) { + continue; + } + targetY = Math.max(worldMin + 1, Math.min(worldMax, targetY)); + if (targetY > origY) { + PlatformBlockState fill = complex != null ? complex.getRockStream().get(cx, cz) : null; + if (B.isAir(fill)) { + fill = IrisObject.States.STONE; + } + for (int yy = origY + 1; yy <= targetY; yy++) { + placer.set(cx, yy, cz, fill); + } + } else if (targetY < origY) { + boolean inside = IrisObjectVacuum.outset(dx, lowX, highX) == 0 && IrisObjectVacuum.outset(dz, lowZ, highZ) == 0; + int carveFloor = IrisObjectVacuum.carveFloorY(targetY, topY, inside); + for (int yy = origY; yy >= carveFloor; yy--) { + placer.set(cx, yy, cz, IrisObject.States.AIR); + } + } + } + } + } + + private boolean shouldBailForCarvingAnchor(IObjectPlacer placer, IrisObjectPlacement placement, int x, int y, int z) { + CarvingMode carvingMode = placement.getCarvingSupport(); + return switch (carvingMode) { + case SURFACE_ONLY -> placer.isCarved(x, y, z); + case CARVING_ONLY -> !isCarvedCaveAnchor(placer, x, y, z); + case ANYWHERE -> false; + }; + } + + private boolean isCarvedCaveAnchor(IObjectPlacer placer, int x, int y, int z) { + return placer.isCarved(x, y, z) + || placer.isCarved(x, y - 1, z) + || placer.isCarved(x, y - 2, z) + || placer.isCarved(x, y - 3, z); + } + + private boolean shouldAutoWaterlogBlock(IObjectPlacer placer, IrisObjectPlacement placement, int yv, int x, int y, int z) { + if (!(placement.isWaterloggable() || placement.isUnderwater())) { + return false; + } + + if (yv >= 0 && placement.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) { + return false; + } + + PlatformBlockState existing = placer.get(x, y, z); + if (existing == null) { + return false; + } + + return B.isWater(existing) || B.isWaterLogged(existing); + } + + private static PlatformBlockState attachVineFaces(IObjectPlacer placer, PlatformBlockState data, int x, int y, int z) { + PlatformBlockState result = data; + for (String face : IrisProceduralBlocks.FACE_PROPERTIES) { + if (!IrisProceduralBlocks.hasProperty(data, face)) { + continue; + } + int[] mod = IrisProceduralBlocks.faceOffset(face); + PlatformBlockState facing = placer.get(x + mod[0], y + mod[1], z + mod[2]); + if (B.isSolid(facing) && !B.isVineBlock(facing)) { + result = result.withProperty(face, "true"); + } + } + return result; + } + + /** + * Precomputed vector rotation for a single placement. + *

+ * This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two + * differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine + * and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the + * spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to + * the value the per-call form would have produced. + */ + private static final class SpinKernel { + private static final int MODE_NONE = 0; + private static final int MODE_FLIP = 1; + private static final int MODE_QUARTER = 2; + private static final int MODE_THREE_QUARTER = 3; + private static final int MODE_ANGLE = 4; + + private final boolean rotates; + private final int xMode; + private final int yMode; + private final int zMode; + private final double xCos; + private final double xSin; + private final double yCos; + private final double ySin; + private final double zCos; + private final double zSin; + + private SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) { + rotates = rotation.canRotate(); + xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE; + zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE; + yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE; + + double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0; + double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0; + double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0; + xCos = Math.cos(xAngle); + xSin = Math.sin(xAngle); + zCos = Math.cos(zAngle); + zSin = Math.sin(zAngle); + yCos = Math.cos(yAngle); + ySin = Math.sin(yAngle); + } + + private static int modeOf(IrisAxisRotationClamp clamp) { + if (!clamp.isLocked()) { + return MODE_ANGLE; + } + + if (Math.abs(clamp.getMax()) % 360D == 180D) { + return MODE_FLIP; + } + + if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) { + return MODE_QUARTER; + } + + if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) { + return MODE_THREE_QUARTER; + } + + return MODE_ANGLE; + } + + /** + * Rotates in place. Axis order (X, then Z, then Y) is load bearing. + */ + private void rotate(IrisBlockVector v) { + if (!rotates) { + return; + } + + switch (xMode) { + case MODE_FLIP -> { + v.setZ(-v.getZ()); + v.setY(-v.getY()); + } + case MODE_QUARTER -> { + double z = v.getZ(); + v.setZ(v.getY()); + v.setY(-z); + } + case MODE_THREE_QUARTER -> { + double z = v.getZ(); + v.setZ(-v.getY()); + v.setY(z); + } + case MODE_ANGLE -> { + double y = xCos * v.getY() - xSin * v.getZ(); + double z = xSin * v.getY() + xCos * v.getZ(); + v.setY(y); + v.setZ(z); + } + default -> { + } + } + + switch (zMode) { + case MODE_FLIP -> { + v.setY(-v.getY()); + v.setX(-v.getX()); + } + case MODE_QUARTER -> { + double y = v.getY(); + v.setY(v.getX()); + v.setX(-y); + } + case MODE_THREE_QUARTER -> { + double y = v.getY(); + v.setY(-v.getX()); + v.setX(y); + } + case MODE_ANGLE -> { + double x = zCos * v.getX() - zSin * v.getY(); + double y = zSin * v.getX() + zCos * v.getY(); + v.setX(x); + v.setY(y); + } + default -> { + } + } + + switch (yMode) { + case MODE_FLIP -> { + v.setX(-v.getX()); + v.setZ(-v.getZ()); + } + case MODE_QUARTER -> { + double x = v.getX(); + v.setX(v.getZ()); + v.setZ(-x); + } + case MODE_THREE_QUARTER -> { + double x = v.getX(); + v.setX(-v.getZ()); + v.setZ(x); + } + case MODE_ANGLE -> { + double x = yCos * v.getX() + ySin * v.getZ(); + double z = -ySin * v.getX() + yCos * v.getZ(); + v.setX(x); + v.setZ(z); + } + default -> { + } + } + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacementScaleInterpolator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementScaleInterpolator.java similarity index 93% rename from core/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacementScaleInterpolator.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementScaleInterpolator.java index da8cafc31..c64a46645 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacementScaleInterpolator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementScaleInterpolator.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("Use 3D Interpolation on scaled objects if they are larger than the origin.") public enum IrisObjectPlacementScaleInterpolator { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectReplace.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectReplace.java new file mode 100644 index 000000000..38c621d5f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectReplace.java @@ -0,0 +1,82 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; +import lombok.experimental.Accessors; + +@Snippet("object-block-replacer") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Find and replace object materials") +@Data +public class IrisObjectReplace { + private final transient AtomicCache replaceGen = new AtomicCache<>(); + private final transient AtomicCache> findData = new AtomicCache<>(); + private final transient AtomicCache> replaceData = new AtomicCache<>(); + @ArrayType(min = 1, type = IrisBlockData.class) + @Required + @Desc("Find this block") + private KList find = new KList<>(); + @Required + @Desc("Replace it with this block palette") + private IrisMaterialPalette replace = new IrisMaterialPalette(); + @Desc("Exactly match the block data or not") + private boolean exact = false; + @MinNumber(0) + @MaxNumber(1) + @Desc("Modifies the chance the block is replaced") + private float chance = 1; + + public KList getFind(IrisData rdata) { + return findData.aquire(() -> + { + KList b = new KList<>(); + + for (IrisBlockData i : find) { + PlatformBlockState bx = i.getBlockData(rdata); + + if (bx != null) { + b.add(bx); + } + } + + return b; + }); + } + + public PlatformBlockState getReplace(RNG seed, double x, double y, double z, IrisData rdata) { + return getReplace().get(seed, x, y, z, rdata); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectRotation.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectRotation.java new file mode 100644 index 000000000..a2094c482 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectRotation.java @@ -0,0 +1,555 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.Axis; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; +import org.bukkit.block.data.MultipleFacing; +import org.bukkit.block.data.Orientable; +import org.bukkit.block.data.Rotatable; +import org.bukkit.block.data.type.RedstoneWire; +import org.bukkit.block.data.type.Wall; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +@Snippet("object-rotator") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Configures rotation for iris") +@Data +public class IrisObjectRotation { + private static final boolean BUKKIT_PRESENT = detectBukkit(); + private static volatile StateRotator PLATFORM_ROTATOR = null; + + public interface StateRotator { + PlatformBlockState rotate(IrisObjectRotation rotation, PlatformBlockState state, int spinx, int spiny, int spinz); + } + + public static synchronized StateRotator bindPlatformRotator(StateRotator rotator) { + StateRotator previous = PLATFORM_ROTATOR; + PLATFORM_ROTATOR = Objects.requireNonNull(rotator, "rotator"); + return previous; + } + + public static synchronized void restorePlatformRotator(StateRotator rotator) { + PLATFORM_ROTATOR = rotator; + } + + private static boolean detectBukkit() { + try { + Class.forName("org.bukkit.Bukkit", false, IrisObjectRotation.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + private static final class Faces { + private static final List WALL_FACES = List.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST); + } + + @Desc("If this rotator is enabled or not") + private boolean enabled = true; + + @Desc("The x axis rotation") + private IrisAxisRotationClamp xAxis = new IrisAxisRotationClamp(); + + @Desc("The y axis rotation") + private IrisAxisRotationClamp yAxis = new IrisAxisRotationClamp(true, false, 0, 0, 90); + + @Desc("The z axis rotation") + private IrisAxisRotationClamp zAxis = new IrisAxisRotationClamp(); + + public static IrisObjectRotation xFlip180() { + IrisObjectRotation rt = new IrisObjectRotation(); + IrisAxisRotationClamp rtx = new IrisAxisRotationClamp(); + IrisAxisRotationClamp rty = new IrisAxisRotationClamp(); + IrisAxisRotationClamp rtz = new IrisAxisRotationClamp(); + rt.setEnabled(true); + rt.setXAxis(rtx); + rt.setYAxis(rty); + rt.setZAxis(rtz); + rtx.setEnabled(true); + rtx.minMax(180); + rty.setEnabled(false); + rtz.setEnabled(false); + return rt; + } + + public static IrisObjectRotation xFlip180RandomY() { + IrisObjectRotation rt = xFlip180(); + rt.setYAxis(new IrisAxisRotationClamp(true, false, 0, 0, 90)); + return rt; + } + + public static IrisObjectRotation xFlip180WithY(double y) { + IrisObjectRotation rt = xFlip180(); + IrisAxisRotationClamp rty = new IrisAxisRotationClamp(); + rty.setEnabled(true); + rty.setInterval(90); + rty.minMax(y); + rt.setYAxis(rty); + return rt; + } + + public static IrisObjectRotation of(double x, double y, double z) { + IrisObjectRotation rt = new IrisObjectRotation(); + IrisAxisRotationClamp rtx = new IrisAxisRotationClamp(); + IrisAxisRotationClamp rty = new IrisAxisRotationClamp(); + IrisAxisRotationClamp rtz = new IrisAxisRotationClamp(); + rt.setEnabled(x != 0 || y != 0 || z != 0); + rt.setXAxis(rtx); + rt.setYAxis(rty); + rt.setZAxis(rtz); + rtx.setEnabled(x != 0); + rty.setEnabled(y != 0); + rtz.setEnabled(z != 0); + rtx.setInterval(90); + rty.setInterval(90); + rtz.setInterval(90); + rtx.minMax(x); + rty.minMax(y); + rtz.minMax(z); + + return rt; + } + + public double getYRotation(int spin) { + return getRotation(spin, yAxis); + } + + public double getXRotation(int spin) { + return getRotation(spin, xAxis); + } + + public double getZRotation(int spin) { + return getRotation(spin, zAxis); + } + + public IrisObject rotateCopy(IrisObject e) { + if (e == null) { + return null; + } + + return e.rotateCopy(this); + } + + public IrisBlockVector rotate(IrisBlockVector direction) { + return rotate(direction, 0, 0, 0); + } + + public IrisDirection rotate(IrisDirection direction) { + IrisBlockVector v = rotate(new IrisBlockVector(direction.x(), direction.y(), direction.z())); + double m = Double.MAX_VALUE; + IrisDirection s = null; + + for (IrisDirection i : IrisDirection.values()) { + double g = new IrisBlockVector(i.x(), i.y(), i.z()).distance(v); + + if (g < m) { + m = g; + s = i; + } + } + + return s; + } + + public double getRotation(int spin, IrisAxisRotationClamp clamp) { + if (!enabled) { + return 0; + } + + if (!clamp.isEnabled()) { + return 0; + } + + return clamp.getRadians(spin); + } + + public BlockFace getFace(IrisBlockVector v) { + int x = (int) Math.round(v.getX()); + int y = (int) Math.round(v.getY()); + int z = (int) Math.round(v.getZ()); + + if (x == 0 && z == -1) { + return BlockFace.NORTH; + } + + if (x == 0 && z == 1) { + return BlockFace.SOUTH; + } + + if (x == 1 && z == 0) { + return BlockFace.EAST; + } + + if (x == -1 && z == 0) { + return BlockFace.WEST; + } + + if (y > 0) { + return BlockFace.UP; + } + + if (y < 0) { + return BlockFace.DOWN; + } + + return BlockFace.SOUTH; + } + + public BlockFace getHexFace(IrisBlockVector v) { + int x = v.getBlockX(); + int y = v.getBlockY(); + int z = v.getBlockZ(); + + if (x == 0 && z == -1) return BlockFace.NORTH; + if (x == 1 && z == -2) return BlockFace.NORTH_NORTH_EAST; + if (x == 1 && z == -1) return BlockFace.NORTH_EAST; + if (x == 2 && z == -1) return BlockFace.EAST_NORTH_EAST; + if (x == 1 && z == 0) return BlockFace.EAST; + if (x == 2 && z == 1) return BlockFace.EAST_SOUTH_EAST; + if (x == 1 && z == 1) return BlockFace.SOUTH_EAST; + if (x == 1 && z == 2) return BlockFace.SOUTH_SOUTH_EAST; + if (x == 0 && z == 1) return BlockFace.SOUTH; + if (x == -1 && z == 2) return BlockFace.SOUTH_SOUTH_WEST; + if (x == -1 && z == 1) return BlockFace.SOUTH_WEST; + if (x == -2 && z == 1) return BlockFace.WEST_SOUTH_WEST; + if (x == -1 && z == 0) return BlockFace.WEST; + if (x == -2 && z == -1) return BlockFace.WEST_NORTH_WEST; + if (x == -1 && z == -1) return BlockFace.NORTH_WEST; + if (x == -1 && z == -2) return BlockFace.NORTH_NORTH_WEST; + + if (y > 0) { + return BlockFace.UP; + } + + if (y < 0) { + return BlockFace.DOWN; + } + + return BlockFace.SOUTH; + } + + public BlockFace faceForAxis(Axis axis) { + return switch (axis) { + case X -> BlockFace.EAST; + case Y -> BlockFace.UP; + case Z -> BlockFace.NORTH; + }; + + } + + public Axis axisFor(BlockFace f) { + return switch (f) { + case NORTH, SOUTH -> Axis.Z; + case EAST, WEST -> Axis.X; + default -> Axis.Y; + }; + + } + + public Axis axisFor2D(BlockFace f) { + return switch (f) { + case EAST, WEST, UP, DOWN -> Axis.X; + default -> Axis.Z; + }; + + } + + public PlatformBlockState rotate(PlatformBlockState state, int spinx, int spiny, int spinz) { + if (state == null) { + return null; + } + + if (!BUKKIT_PRESENT) { + StateRotator rotator = requirePlatformRotator(PLATFORM_ROTATOR); + return rotator.rotate(this, state, spinx, spiny, spinz); + } + + BlockData original = (BlockData) state.nativeHandle(); + if (!canRotate() || !canRotateBlockData(original)) { + return state; + } + + BlockData raw = original.clone(); + BlockData rotated = rotate(raw, spinx, spiny, spinz); + return rotated == null ? null : BukkitBlockState.of(rotated); + } + + static StateRotator requirePlatformRotator(StateRotator rotator) { + if (rotator == null) { + throw new IllegalStateException("No platform block-state rotator is bound"); + } + return rotator; + } + + private static boolean canRotateBlockData(BlockData data) { + return data instanceof Directional + || data instanceof Rotatable + || data instanceof Orientable + || data instanceof MultipleFacing + || data instanceof Wall + || data instanceof RedstoneWire; + } + + public BlockData rotate(BlockData dd, int spinxx, int spinyy, int spinzz) { + BlockData d = dd; + try { + int spinx = (int) (90D * (Math.ceil(Math.abs((spinxx % 360D) / 90D)))); + int spiny = (int) (90D * (Math.ceil(Math.abs((spinyy % 360D) / 90D)))); + int spinz = (int) (90D * (Math.ceil(Math.abs((spinzz % 360D) / 90D)))); + + if (!canRotate()) { + return d; + } + + if (d instanceof Directional g) { + BlockFace f = g.getFacing(); + IrisBlockVector bv = new IrisBlockVector(f.getModX(), f.getModY(), f.getModZ()); + bv = rotate(bv.clone(), spinx, spiny, spinz); + BlockFace t = getFace(bv); + + if (g.getFaces().contains(t)) { + g.setFacing(t); + } else if (!g.getMaterial().isSolid()) { + d = null; + } + } else if (d instanceof Rotatable g) { + BlockFace f = g.getRotation(); + + IrisBlockVector bv = new IrisBlockVector(f.getModX(), 0, f.getModZ()); + bv = rotate(bv.clone(), spinx, spiny, spinz); + BlockFace face = getHexFace(bv); + + g.setRotation(face); + + } else if (d instanceof Orientable g) { + BlockFace f = getFace(g.getAxis()); + IrisBlockVector bv = new IrisBlockVector(f.getModX(), f.getModY(), f.getModZ()); + bv = rotate(bv.clone(), spinx, spiny, spinz); + Axis a = getAxis(bv); + + if (!a.equals(g.getAxis()) && g.getAxes().contains(a)) { + g.setAxis(a); + } + } else if (d instanceof MultipleFacing g) { + List faces = new KList<>(); + + for (BlockFace i : g.getFaces()) { + IrisBlockVector bv = new IrisBlockVector(i.getModX(), i.getModY(), i.getModZ()); + bv = rotate(bv.clone(), spinx, spiny, spinz); + BlockFace r = getFace(bv); + + if (g.getAllowedFaces().contains(r)) { + faces.add(r); + } + } + + for (BlockFace i : g.getFaces()) { + g.setFace(i, false); + } + + for (BlockFace i : faces) { + g.setFace(i, true); + } + } else if (d instanceof Wall wall) { + KMap faces = new KMap<>(); + + for (BlockFace i : Faces.WALL_FACES) { + Wall.Height h = wall.getHeight(i); + IrisBlockVector bv = new IrisBlockVector(i.getModX(), i.getModY(), i.getModZ()); + bv = rotate(bv.clone(), spinx, spiny, spinz); + BlockFace r = getFace(bv); + if (Faces.WALL_FACES.contains(r)) { + faces.put(r, h); + } + } + + for (BlockFace i : Faces.WALL_FACES) { + wall.setHeight(i, faces.getOrDefault(i, Wall.Height.NONE)); + } + } else if (d instanceof RedstoneWire wire) { + Map faces = new HashMap<>(); + + Set allowed = wire.getAllowedFaces(); + for (BlockFace i : allowed) { + RedstoneWire.Connection connection = wire.getFace(i); + IrisBlockVector bv = new IrisBlockVector(i.getModX(), i.getModY(), i.getModZ()); + bv = rotate(bv.clone(), spinx, spiny, spinz); + BlockFace r = getFace(bv); + if (allowed.contains(r)) + faces.put(r, connection); + } + + for (BlockFace i : allowed) { + wire.setFace(i, faces.getOrDefault(i, RedstoneWire.Connection.NONE)); + } + } + } catch (Throwable e) { + IrisLogging.reportError(e); + + } + + return d; + } + + public Axis getAxis(IrisBlockVector v) { + if (Math.abs(v.getBlockX()) > Math.max(Math.abs(v.getBlockY()), Math.abs(v.getBlockZ()))) { + return Axis.X; + } + + if (Math.abs(v.getBlockY()) > Math.max(Math.abs(v.getBlockX()), Math.abs(v.getBlockZ()))) { + return Axis.Y; + } + + if (Math.abs(v.getBlockZ()) > Math.max(Math.abs(v.getBlockX()), Math.abs(v.getBlockY()))) { + return Axis.Z; + } + + return Axis.Y; + } + + private BlockFace getFace(Axis axis) { + return switch (axis) { + case X -> BlockFace.EAST; + case Y -> BlockFace.UP; + case Z -> BlockFace.SOUTH; + }; + } + + public IrisPosition rotate(IrisPosition b) { + return rotate(b, 0, 0, 0); + } + + public IrisPosition rotate(IrisPosition b, int spinx, int spiny, int spinz) { + IrisBlockVector v = rotate(new IrisBlockVector(b.getX(), b.getY(), b.getZ()), spinx, spiny, spinz); + return new IrisPosition(v.getBlockX(), v.getBlockY(), v.getBlockZ()); + } + + public IrisBlockVector rotate(IrisBlockVector b, int spinx, int spiny, int spinz) { + if (!canRotate()) { + return b; + } + + IrisBlockVector v = b.clone(); + + if (canRotateX()) { + if (getXAxis().isLocked()) { + if (Math.abs(getXAxis().getMax()) % 360D == 180D) { + v.setZ(-v.getZ()); + v.setY(-v.getY()); + } else if (getXAxis().getMax() % 360D == 90D || getXAxis().getMax() % 360D == -270D) { + double z = v.getZ(); + v.setZ(v.getY()); + v.setY(-z); + } else if (getXAxis().getMax() == -90D || getXAxis().getMax() % 360D == 270D) { + double z = v.getZ(); + v.setZ(-v.getY()); + v.setY(z); + } else { + v.rotateAroundX(getXRotation(spinx)); + } + } else { + v.rotateAroundX(getXRotation(spinx)); + } + } + + if (canRotateZ()) { + if (getZAxis().isLocked()) { + if (Math.abs(getZAxis().getMax()) % 360D == 180D) { + v.setY(-v.getY()); + v.setX(-v.getX()); + } else if (getZAxis().getMax() % 360D == 90D || getZAxis().getMax() % 360D == -270D) { + double y = v.getY(); + v.setY(v.getX()); + v.setX(-y); + } else if (getZAxis().getMax() == -90D || getZAxis().getMax() % 360D == 270D) { + double y = v.getY(); + v.setY(-v.getX()); + v.setX(y); + } else { + v.rotateAroundZ(getZRotation(spinz)); + } + } else { + v.rotateAroundZ(getZRotation(spinz)); + } + } + + if (canRotateY()) { + if (getYAxis().isLocked()) { + if (Math.abs(getYAxis().getMax()) % 360D == 180D) { + v.setX(-v.getX()); + v.setZ(-v.getZ()); + } else if (getYAxis().getMax() % 360D == 90D || getYAxis().getMax() % 360D == -270D) { + double x = v.getX(); + v.setX(v.getZ()); + v.setZ(-x); + } else if (getYAxis().getMax() == -90D || getYAxis().getMax() % 360D == 270D) { + double x = v.getX(); + v.setX(-v.getZ()); + v.setZ(x); + } else { + v.rotateAroundY(getYRotation(spiny)); + } + } else { + v.rotateAroundY(getYRotation(spiny)); + } + } + + return v; + } + + public boolean canRotateX() { + return enabled && xAxis.isEnabled(); + } + + public boolean canRotateY() { + return enabled && yAxis.isEnabled(); + } + + public boolean canRotateZ() { + return enabled && zAxis.isEnabled(); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean canRotate() { + return canRotateX() || canRotateY() || canRotateZ(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java new file mode 100644 index 000000000..5e0bc41a6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java @@ -0,0 +1,417 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +@Snippet("object-scale") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Scale objects") +@Data +public class IrisObjectScale { + private static final long CACHE_MIN_ESTIMATED_BYTES = 16L * 1024L * 1024L; + private static final long CACHE_MAX_ESTIMATED_BYTES = 64L * 1024L * 1024L; + private static final long CACHE_ENTRY_ESTIMATED_BYTES = 512L; + private static final long CACHE_VARIANT_ESTIMATED_BYTES = 256L; + private static final long CACHE_ESTIMATED_BYTES_PER_VOXEL = 128L; + private static final int CACHE_LOAD_LOCK_COUNT = 256; + private static final ScaleCache CACHE = new ScaleCache(resolveCacheMaximumEstimatedBytes()); + + public static void invalidate(IrisData owner) { + CACHE.invalidate(owner); + } + + @MinNumber(0.01) + @MaxNumber(50) + @Desc("Fixed scale multiplier for this object. 0.5 shrinks to half size, 2.0 doubles the size. When set to anything other than 1, this overrides minimumScale and maximumScale. Leave at 1 to use the minimumScale/maximumScale range.") + private double size = 1; + + @MinNumber(1) + @MaxNumber(32) + @Desc("Iris Objects are scaled and cached to speed up placements. Because of this extra memory is used, so we evenly distribute variations across the defined scale range, then pick one randomly. If the differences is small, use a lower number. For more possibilities on the scale spectrum, increase this at the cost of memory.") + private int variations = 7; + + @MinNumber(0.01) + @MaxNumber(50) + @Desc("The minimum scale. Used when size is 1 to pick a random scale per placement.") + private double minimumScale = 1; + + @MinNumber(0.01) + @MaxNumber(50) + @Desc("The maximum scale. Used when size is 1 to pick a random scale per placement.") + private double maximumScale = 1; + + @Desc("If this object is scaled up beyond its origin size, specify a 3D interpolator. NONE keeps blocky scaled output, TRILINEAR (LERP) smooths with linear interpolation, TRICUBIC and TRIHERMITE produce smoother but slower output.") + private IrisObjectPlacementScaleInterpolator interpolation = IrisObjectPlacementScaleInterpolator.NONE; + + public boolean shouldScale() { + if (size != 1) { + return true; + } + if (variations <= 0) { + return false; + } + return minimumScale != 1 || maximumScale != 1; + } + + public int getMaxSizeFor(int indim) { + return (int) Math.ceil(getMaxScale() * indim); + } + + public double getMaxScale() { + if (size != 1) { + return size; + } + return Math.max(minimumScale, maximumScale); + } + + public IrisObject get(RNG rng, IrisObject origin) { + if (origin == null) { + return null; + } + + ScaleRequest request = snapshotRequest(); + if (!request.shouldScale()) { + return origin; + } + + int variantIndex = request.selectVariant(rng); + CacheKey key = CACHE.key(origin, request, variantIndex); + CacheLookup lookup = CACHE.lookup(key); + if (lookup.variant() != null) { + return lookup.variant(); + } + + synchronized (CACHE.loadLock(key)) { + CacheLookup afterWait = CACHE.lookup(key); + if (afterWait.variant() != null) { + return afterWait.variant(); + } + + IrisObject variant = origin.scaled(request.scaleAt(variantIndex), request.interpolation()); + long estimatedBytes = estimateVariant(variant); + return CACHE.putIfCurrent(key, variant, estimatedBytes, lookup.generation()); + } + } + + public boolean canScaleBeyond() { + return shouldScale() && getMaxScale() > 1; + } + + static int cacheEntryCount() { + return CACHE.size(); + } + + static long cacheEstimatedBytes() { + return CACHE.estimatedBytes(); + } + + static boolean isOriginCached(IrisObject origin) { + return CACHE.containsOrigin(origin); + } + + private ScaleRequest snapshotRequest() { + IrisObjectPlacementScaleInterpolator configuredInterpolation = interpolation == null + ? IrisObjectPlacementScaleInterpolator.NONE + : interpolation; + return new ScaleRequest(size, minimumScale, maximumScale, variations, configuredInterpolation); + } + + private static long estimateVariant(IrisObject variant) { + long width = Math.max(1L, variant.getW()); + long height = Math.max(1L, variant.getH()); + long depth = Math.max(1L, variant.getD()); + long volume = saturatedMultiply(saturatedMultiply(width, height), depth); + long populatedVoxels = saturatedAdd(variant.getBlocks().size(), variant.getStates().size()); + long voxelBytes = saturatedMultiply(Math.max(volume, populatedVoxels), CACHE_ESTIMATED_BYTES_PER_VOXEL); + return saturatedAdd(CACHE_ENTRY_ESTIMATED_BYTES, + saturatedAdd(CACHE_VARIANT_ESTIMATED_BYTES, voxelBytes)); + } + + private static long saturatedAdd(long first, long second) { + if (first >= Long.MAX_VALUE - second) { + return Long.MAX_VALUE; + } + return first + second; + } + + private static long saturatedMultiply(long first, long second) { + if (first == 0L || second == 0L) { + return 0L; + } + if (first > Long.MAX_VALUE / second) { + return Long.MAX_VALUE; + } + return first * second; + } + + private static long resolveCacheMaximumEstimatedBytes() { + long heapShare = Runtime.getRuntime().maxMemory() / 32L; + return Math.max(CACHE_MIN_ESTIMATED_BYTES, Math.min(CACHE_MAX_ESTIMATED_BYTES, heapShare)); + } + + record ScaleRequest(double size, double minimumScale, double maximumScale, int variations, + IrisObjectPlacementScaleInterpolator interpolation) { + boolean shouldScale() { + if (size != 1D) { + return true; + } + return variations > 0 && (minimumScale != 1D || maximumScale != 1D); + } + + int variantCount() { + if (size != 1D || minimumScale == maximumScale) { + return 1; + } + return Math.max(1, Math.min(variations, 32)); + } + + int selectVariant(RNG rng) { + int count = variantCount(); + return count == 1 ? 0 : rng.nextInt(count); + } + + double scaleAt(int index) { + if (size != 1D) { + return size; + } + if (minimumScale == maximumScale) { + return minimumScale; + } + return minimumScale + (((maximumScale - minimumScale) / variantCount()) * index); + } + } + + static final class CacheKey extends WeakReference { + private final IrisData owner; + private final ScaleRequest request; + private final int variantIndex; + private final int originRevision; + private final int loadHashCode; + private final int hashCode; + + CacheKey(IrisObject origin, ScaleRequest request, int variantIndex) { + this(origin, request, variantIndex, null); + } + + CacheKey(IrisObject origin, ScaleRequest request, int variantIndex, + ReferenceQueue collectedOrigins) { + super(origin, collectedOrigins); + this.owner = origin.getLoader(); + this.request = request; + this.variantIndex = variantIndex; + this.originRevision = originRevision(origin); + int result = System.identityHashCode(origin); + result = (31 * result) + System.identityHashCode(owner); + result = (31 * result) + originRevision; + result = (31 * result) + request.hashCode(); + this.loadHashCode = result; + this.hashCode = (31 * result) + variantIndex; + } + + boolean belongsTo(IrisData candidate) { + return owner == candidate; + } + + boolean hasOrigin(IrisObject candidate) { + return get() == candidate; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof CacheKey other)) { + return false; + } + IrisObject origin = get(); + return origin != null + && origin == other.get() + && owner == other.owner + && originRevision == other.originRevision + && request.equals(other.request) + && variantIndex == other.variantIndex; + } + + @Override + public int hashCode() { + return hashCode; + } + + int loadHashCode() { + return loadHashCode; + } + + private static int originRevision(IrisObject origin) { + origin.readLock.lock(); + try { + int result = origin.getW(); + result = (31 * result) + origin.getH(); + result = (31 * result) + origin.getD(); + result = (31 * result) + System.identityHashCode(origin.getBlocks()); + result = (31 * result) + System.identityHashCode(origin.getStates()); + result = (31 * result) + Long.hashCode(origin.getBlocks().modificationRevision()); + result = (31 * result) + Long.hashCode(origin.getStates().modificationRevision()); + Vector3i center = origin.getCenter(); + if (center != null) { + result = (31 * result) + center.getX(); + result = (31 * result) + center.getY(); + result = (31 * result) + center.getZ(); + } + return result; + } finally { + origin.readLock.unlock(); + } + } + } + + record CacheLookup(IrisObject variant, long generation) { + } + + static final class ScaleCache { + private final long maximumEstimatedBytes; + private final LinkedHashMap entries; + private final Object[] loadLocks; + private final ReferenceQueue collectedOrigins; + private long estimatedBytes; + private long generation; + + ScaleCache(long maximumEstimatedBytes) { + if (maximumEstimatedBytes <= 0L) { + throw new IllegalArgumentException("maximumEstimatedBytes must be positive"); + } + this.maximumEstimatedBytes = maximumEstimatedBytes; + this.entries = new LinkedHashMap<>(256, 0.75F, true); + this.loadLocks = new Object[CACHE_LOAD_LOCK_COUNT]; + this.collectedOrigins = new ReferenceQueue<>(); + for (int index = 0; index < loadLocks.length; index++) { + loadLocks[index] = new Object(); + } + } + + CacheKey key(IrisObject origin, ScaleRequest request, int variantIndex) { + return new CacheKey(origin, request, variantIndex, collectedOrigins); + } + + Object loadLock(CacheKey key) { + return loadLocks[key.loadHashCode() & (loadLocks.length - 1)]; + } + + synchronized CacheLookup lookup(CacheKey key) { + drainCollectedOrigins(); + CacheValue value = entries.get(key); + return new CacheLookup(value == null ? null : value.variant(), generation); + } + + synchronized IrisObject putIfCurrent(CacheKey key, IrisObject variant, + long entryEstimatedBytes, long expectedGeneration) { + drainCollectedOrigins(); + CacheValue present = entries.get(key); + if (present != null) { + return present.variant(); + } + if (generation != expectedGeneration || entryEstimatedBytes > maximumEstimatedBytes) { + return variant; + } + + entries.put(key, new CacheValue(variant, entryEstimatedBytes)); + estimatedBytes += entryEstimatedBytes; + evictToBudget(); + return variant; + } + + synchronized void invalidate(IrisData owner) { + drainCollectedOrigins(); + generation++; + Iterator> iterator = entries.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getKey().belongsTo(owner)) { + estimatedBytes -= entry.getValue().estimatedBytes(); + iterator.remove(); + } + } + } + + synchronized int size() { + drainCollectedOrigins(); + return entries.size(); + } + + synchronized long estimatedBytes() { + drainCollectedOrigins(); + return estimatedBytes; + } + + long maximumEstimatedBytes() { + return maximumEstimatedBytes; + } + + synchronized boolean containsOrigin(IrisObject origin) { + drainCollectedOrigins(); + for (CacheKey key : entries.keySet()) { + if (key.hasOrigin(origin)) { + return true; + } + } + return false; + } + + private void evictToBudget() { + Iterator> iterator = entries.entrySet().iterator(); + while (estimatedBytes > maximumEstimatedBytes && iterator.hasNext()) { + Map.Entry entry = iterator.next(); + estimatedBytes -= entry.getValue().estimatedBytes(); + iterator.remove(); + } + } + + private void drainCollectedOrigins() { + CacheKey collected; + while ((collected = (CacheKey) collectedOrigins.poll()) != null) { + CacheValue removed = entries.remove(collected); + if (removed != null) { + estimatedBytes -= removed.estimatedBytes(); + } + } + } + } + + record CacheValue(IrisObject variant, long estimatedBytes) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java new file mode 100644 index 000000000..b75d57964 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java @@ -0,0 +1,311 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.IrisVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Volume shaping for {@link IrisObject}: smart boring, shrinkwrapping, compaction and the shared block + * classification helpers used by placement. + */ +final class IrisObjectShaping { + private IrisObjectShaping() { + } + + static void ensureSmartBored(IrisObject self, boolean debug) { + if (self.smartBored) { + return; + } + + // Whole computation under the object's write lock: two racing callers would otherwise + // both compute, with the loser reading self.blocks while the winner merges into it. + self.writeLock.lock(); + try { + if (self.smartBored) { + return; + } + ensureSmartBoredLocked(self, debug); + } finally { + self.writeLock.unlock(); + } + } + + private static void ensureSmartBoredLocked(IrisObject self, boolean debug) { + PrecisionStopwatch p = PrecisionStopwatch.start(); + PlatformBlockState vair = debug ? IrisObject.States.VAIR_DEBUG : IrisObject.States.VAIR; + AtomicInteger applied = new AtomicInteger(); + IrisBlockVector max = new IrisBlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE); + IrisBlockVector min = new IrisBlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); + VectorMap source; + self.readLock.lock(); + try { + if (self.blocks.isEmpty()) { + IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it."); + self.smartBored = true; + return; + } + + source = self.blocks; + + for (IrisBlockVector i : self.blocks.keys()) { + max.setX(Math.max(i.getX(), max.getX())); + min.setX(Math.min(i.getX(), min.getX())); + max.setY(Math.max(i.getY(), max.getY())); + min.setY(Math.min(i.getY(), min.getY())); + max.setZ(Math.max(i.getZ(), max.getZ())); + min.setZ(Math.min(i.getZ(), min.getZ())); + } + } finally { + self.readLock.unlock(); + } + + VectorMap bore = new VectorMap<>(); + // Inline on purpose: the three axis passes read AND write the shared bore map, so the + // bored volume must come from a fixed X->Y->Z order on the calling (lock-owning) + // thread, not from pool scheduling. This matches what burst-worker callers (studio, + // generation threads) already produced, so output is unchanged where it was stable. + BurstExecutor burst = MultiBurst.burst.burst(); + burst.setMulticore(false); + + // Smash X + for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { + int finalRayY = rayY; + burst.queue(() -> { + for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; + + for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) { + if (boreContains(source, bore, new IrisBlockVector(ray, finalRayY, rayZ))) { + start = Math.min(ray, start); + end = Math.max(ray, end); + } + } + + if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { + for (int i = start; i <= end; i++) { + boreCell(source, bore, new IrisBlockVector(i, finalRayY, rayZ), vair, applied); + } + } + } + }); + } + + // Smash Y + for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { + int finalRayX = rayX; + burst.queue(() -> { + for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; + + for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) { + if (boreContains(source, bore, new IrisBlockVector(finalRayX, ray, rayZ))) { + start = Math.min(ray, start); + end = Math.max(ray, end); + } + } + + if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { + for (int i = start; i <= end; i++) { + boreCell(source, bore, new IrisBlockVector(finalRayX, i, rayZ), vair, applied); + } + } + } + }); + } + + // Smash Z + for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { + int finalRayX = rayX; + burst.queue(() -> { + for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; + + for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) { + if (boreContains(source, bore, new IrisBlockVector(finalRayX, rayY, ray))) { + start = Math.min(ray, start); + end = Math.max(ray, end); + } + } + + if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { + for (int i = start; i <= end; i++) { + boreCell(source, bore, new IrisBlockVector(finalRayX, rayY, i), vair, applied); + } + } + } + }); + } + + burst.complete(); + + self.writeLock.lock(); + try { + if (!self.smartBored) { + bore.forEach((v, s) -> self.blocks.computeIfAbsent(v, (vv) -> s)); + self.smartBored = true; + } + } finally { + self.writeLock.unlock(); + } + + IrisLogging.debug("Smart Bore: " + self.getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied.get()) + ")"); + } + + private static boolean boreContains(VectorMap source, VectorMap bore, IrisBlockVector v) { + return source.containsKey(v) || bore.containsKey(v); + } + + private static void boreCell(VectorMap source, VectorMap bore, IrisBlockVector v, PlatformBlockState vair, AtomicInteger applied) { + PlatformBlockState existing = source.get(v); + + if (existing == null) { + if (vair.equals(bore.get(v))) { + return; + } + + bore.computeIfAbsent(v, (vv) -> vair); + } else if (vair.equals(existing)) { + return; + } + + applied.getAndIncrement(); + } + + static void shrinkwrap(IrisObject self) { + if (self.blocks.isEmpty()) return; + IrisBlockVector min = new IrisBlockVector(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + IrisBlockVector max = new IrisBlockVector(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + + for (IrisBlockVector i : self.blocks.keys()) { + min.setX(Math.min(min.getX(), i.getX())); + min.setY(Math.min(min.getY(), i.getY())); + min.setZ(Math.min(min.getZ(), i.getZ())); + max.setX(Math.max(max.getX(), i.getX())); + max.setY(Math.max(max.getY(), i.getY())); + max.setZ(Math.max(max.getZ(), i.getZ())); + } + + // Compute into locals and assign every field in one block at the end, so a reader can + // never observe post-shrink dimensions paired with pre-shrink volume (or vice versa). + int w = max.getBlockX() - min.getBlockX() + 1; + int h = max.getBlockY() - min.getBlockY() + 1; + int d = max.getBlockZ() - min.getBlockZ() + 1; + Vector3i center = new Vector3i(w / 2, h / 2, d / 2); + + Vector3i offset = new Vector3i( + -center.getBlockX() - min.getBlockX(), + -center.getBlockY() - min.getBlockY(), + -center.getBlockZ() - min.getBlockZ() + ); + if (offset.getBlockX() == 0 && offset.getBlockY() == 0 && offset.getBlockZ() == 0) { + self.w = w; + self.h = h; + self.d = d; + self.center = center; + return; + } + + VectorMap b = new VectorMap<>(); + VectorMap s = new VectorMap<>(); + IrisBlockVector shift = new IrisBlockVector(offset.getX(), offset.getY(), offset.getZ()); + + self.blocks.forEach((vector, data) -> { + vector.add(shift); + b.put(vector, data); + }); + + self.states.forEach((vector, data) -> { + vector.add(shift); + s.put(vector, data); + }); + + self.w = w; + self.h = h; + self.d = d; + self.center = center; + self.shrinkOffset = offset; + self.blocks = b; + self.states = s; + self.surfaceSupportOffsets.reset(); + self.floatingFootprint.reset(); + } + + static void clean(IrisObject self) { + VectorMap d = new VectorMap<>(); + d.putAll(self.blocks); + + VectorMap dx = new VectorMap<>(); + dx.putAll(self.states); + + self.blocks = d; + self.states = dx; + self.surfaceSupportOffsets.reset(); + self.floatingFootprint.reset(); + } + + static boolean shouldStilt(PlatformBlockState state) { + if (!state.isOccluding()) { + return false; + } + String material = materialKey(state); + if (material.endsWith("_stairs") || material.endsWith("_slab")) { + return false; + } + return !material.equals("minecraft:dirt_path"); + } + + static String materialKey(PlatformBlockState state) { + return IrisProceduralBlocks.materialKey(state); + } + + static List blocksBetweenTwoPoints(IrisVector loc1, IrisVector loc2) { + List locations = new ArrayList<>(); + int topBlockX = Math.max(loc1.getBlockX(), loc2.getBlockX()); + int bottomBlockX = Math.min(loc1.getBlockX(), loc2.getBlockX()); + int topBlockY = Math.max(loc1.getBlockY(), loc2.getBlockY()); + int bottomBlockY = Math.min(loc1.getBlockY(), loc2.getBlockY()); + int topBlockZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ()); + int bottomBlockZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ()); + + for (int x = bottomBlockX; x <= topBlockX; x++) { + for (int z = bottomBlockZ; z <= topBlockZ; z++) { + for (int y = bottomBlockY; y <= topBlockY; y++) { + locations.add(new IrisBlockVector(x, y, z)); + } + } + } + return locations; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java new file mode 100644 index 000000000..bf761b406 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java @@ -0,0 +1,273 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.IrisVector; +import art.arcane.iris.util.project.interpolation.Interpolation3D; + +/** + * Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers. + */ +final class IrisObjectTransforms { + private IrisObjectTransforms() { + } + + static IrisObject rotateCopy(IrisObject self, IrisObjectRotation rt) { + IrisObject copy = self.copy(); + rotate(copy, rt, 0, 0, 0); + return copy; + } + + static void rotate(IrisObject self, IrisObjectRotation r, int spinx, int spiny, int spinz) { + self.writeLock.lock(); + try { + VectorMap d = new VectorMap<>(); + + for (var entry : self.blocks) { + d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), r.rotate(entry.getValue(), spinx, spiny, spinz)); + } + + VectorMap dx = new VectorMap<>(); + + for (var entry : self.states) { + dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue()); + } + + self.blocks = d; + self.states = dx; + IrisObjectShaping.shrinkwrap(self); + self.surfaceSupportOffsets.reset(); + self.floatingFootprint.reset(); + } finally { + self.writeLock.unlock(); + } + } + + static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation) { + if (interpolation == null) { + interpolation = IrisObjectPlacementScaleInterpolator.NONE; + } + IrisVector sm1 = new IrisVector(scale - 1, scale - 1, scale - 1); + scale = Math.max(0.001, Math.min(50, scale)); + if (scale < 1) { + scale = scale - 0.0001; + } + + IrisPosition l1 = self.getAABB().max(); + IrisPosition l2 = self.getAABB().min(); + VectorMap placeBlock = new VectorMap<>(); + + IrisVector center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ()); + if (self.getH() == 2) { + center = center.setY(center.getBlockY() + 0.5); + } + if (self.getW() == 2) { + center = center.setX(center.getBlockX() + 0.5); + } + if (self.getD() == 2) { + center = center.setZ(center.getBlockZ() + 0.5); + } + + IrisObject oo = new IrisObject((int) Math.ceil((self.w * scale) + (scale * 2)), (int) Math.ceil((self.h * scale) + (scale * 2)), (int) Math.ceil((self.d * scale) + (scale * 2))); + oo.setLoadKey(self.getLoadKey()); + oo.setLoader(self.getLoader()); + oo.setLoadFile(self.getLoadFile()); + + self.readLock.lock(); + try { + for (var entry : self.blocks) { + PlatformBlockState bd = entry.getValue(); + placeBlock.put(entry.getKey().clone().add(IrisObject.HALF).subtract(center) + .multiply(scale).add(sm1).toBlockVector(), bd); + } + } finally { + self.readLock.unlock(); + } + + for (var entry : placeBlock) { + IrisBlockVector v = entry.getKey(); + if (scale > 1) { + for (IrisBlockVector vec : IrisObjectShaping.blocksBetweenTwoPoints(v.clone().add(center), v.clone().add(center).add(sm1))) { + oo.blocks.put(vec, entry.getValue()); + } + } else { + oo.setUnsigned(v.getBlockX(), v.getBlockY(), v.getBlockZ(), entry.getValue()); + } + } + + if (scale > 1) { + switch (interpolation) { + case TRILINEAR -> trilinear(oo, (int) Math.round(scale)); + case TRICUBIC -> tricubic(oo, (int) Math.round(scale)); + case TRIHERMITE -> trihermite(oo, (int) Math.round(scale)); + } + } + + return oo; + } + + static void trilinear(IrisObject self, int rad) { + self.writeLock.lock(); + try { + VectorMap v = self.blocks; + VectorMap b = new VectorMap<>(); + IrisPosition min = self.getAABB().min(); + IrisPosition max = self.getAABB().max(); + + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + if (Interpolation3D.getTrilinear(x, y, z, rad, (xx, yy, zz) -> { + PlatformBlockState data = v.get(new IrisBlockVector((int) xx, (int) yy, (int) zz)); + + if (B.isAir(data)) { + return 0; + } + + return 1; + }) >= 0.5) { + b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z)); + } else { + b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR); + } + } + } + } + + self.blocks = b; + self.surfaceSupportOffsets.reset(); + self.floatingFootprint.reset(); + } finally { + self.writeLock.unlock(); + } + } + + static void tricubic(IrisObject self, int rad) { + self.writeLock.lock(); + try { + VectorMap v = self.blocks; + VectorMap b = new VectorMap<>(); + IrisPosition min = self.getAABB().min(); + IrisPosition max = self.getAABB().max(); + + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + if (Interpolation3D.getTricubic(x, y, z, rad, (xx, yy, zz) -> { + PlatformBlockState data = v.get(new IrisBlockVector((int) xx, (int) yy, (int) zz)); + + if (B.isAir(data)) { + return 0; + } + + return 1; + }) >= 0.5) { + b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z)); + } else { + b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR); + } + } + } + } + + self.blocks = b; + self.surfaceSupportOffsets.reset(); + self.floatingFootprint.reset(); + } finally { + self.writeLock.unlock(); + } + } + + static void trihermite(IrisObject self, int rad) { + trihermite(self, rad, 0D, 0D); + } + + static void trihermite(IrisObject self, int rad, double tension, double bias) { + self.writeLock.lock(); + try { + VectorMap v = self.blocks; + VectorMap b = new VectorMap<>(); + IrisPosition min = self.getAABB().min(); + IrisPosition max = self.getAABB().max(); + + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + if (Interpolation3D.getTrihermite(x, y, z, rad, (xx, yy, zz) -> { + PlatformBlockState data = v.get(new IrisBlockVector((int) xx, (int) yy, (int) zz)); + + if (B.isAir(data)) { + return 0; + } + + return 1; + }, tension, bias) >= 0.5) { + b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z)); + } else { + b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR); + } + } + } + } + + self.blocks = b; + self.surfaceSupportOffsets.reset(); + self.floatingFootprint.reset(); + } finally { + self.writeLock.unlock(); + } + } + + private static PlatformBlockState nearestBlockData(IrisObject self, int x, int y, int z) { + IrisBlockVector vv = new IrisBlockVector(x, y, z); + self.readLock.lock(); + try { + PlatformBlockState r = self.blocks.get(vv); + + if (!B.isAir(r)) { + return r; + } + + double d = Double.MAX_VALUE; + + for (var entry : self.blocks) { + PlatformBlockState dat = entry.getValue(); + + if (B.isAir(dat)) { + continue; + } + + double dx = entry.getKey().distanceSquared(vv); + + if (dx < d) { + d = dx; + r = dat; + } + } + + return r; + } finally { + self.readLock.unlock(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java new file mode 100644 index 000000000..ce77a67ab --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java @@ -0,0 +1,79 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.common.math.IrisBlockVector; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("object-translator") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Translate objects") +@Data +public class IrisObjectTranslate { + @MinNumber(-128) + @MaxNumber(128) + @Desc("The x shift in blocks") + private int x = 0; + + @Required + @MinNumber(-128) + @MaxNumber(128) + @Desc("The y shift in blocks") + private int y = 0; + + @MinNumber(-128) + @MaxNumber(128) + @Desc("Adds an additional amount of height randomly (translateY + rand(0 - yRandom))") + private int yRandom = 0; + + @MinNumber(-128) + @MaxNumber(128) + @Desc("The z shift in blocks") + private int z = 0; + + public boolean canTranslate() { + return x != 0 || y != 0 || z != 0; + } + + public IrisBlockVector translate(IrisBlockVector i) { + if (canTranslate()) { + return (IrisBlockVector) i.clone().add(new IrisBlockVector(x, y, z)); + } + + return i; + } + + public IrisBlockVector translate(IrisBlockVector clone, IrisObjectRotation rotation, int sx, int sy, int sz) { + if (canTranslate()) { + return (IrisBlockVector) clone.clone().add(rotation.rotate(new IrisBlockVector(x, y, z), sx, sy, sz)); + } + + return clone; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVacuum.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVacuum.java new file mode 100644 index 000000000..1b8b46084 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVacuum.java @@ -0,0 +1,137 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +public final class IrisObjectVacuum { + private static final double DEFAULT_WAVE_AMPLITUDE = 3.0; + private static final double DEFAULT_WAVE_SCALE = 5.0; + + private IrisObjectVacuum() { + } + + public static boolean isVacuumMode(ObjectPlaceMode mode) { + return mode == ObjectPlaceMode.VACUUM + || mode == ObjectPlaceMode.VACUUM_HIGH + || mode == ObjectPlaceMode.VACUUM_FAST + || mode == ObjectPlaceMode.VACUUM_ORGANIC + || mode == ObjectPlaceMode.VACUUM_WAVY; + } + + public static int resolveRadius(ObjectPlaceMode mode, IrisVacuumSettings settings) { + int radius = switch (mode) { + case VACUUM_HIGH -> 20; + case VACUUM_FAST -> 8; + default -> 12; + }; + if (settings != null && settings.getRadius() > 0) { + radius = settings.getRadius(); + } + return Math.max(0, radius); + } + + public static int resolveStep(ObjectPlaceMode mode) { + return mode == ObjectPlaceMode.VACUUM_FAST ? 2 : 1; + } + + public static double resolveFalloff(IrisVacuumSettings settings) { + return settings != null ? Math.max(0.25, settings.getFalloff()) : 2.0; + } + + public static double resolveWaveAmplitude(IrisVacuumSettings settings) { + if (settings == null) { + return DEFAULT_WAVE_AMPLITUDE; + } + return Math.max(0, settings.getWaveAmplitude()); + } + + public static double resolveWaveScale(IrisVacuumSettings settings) { + if (settings == null) { + return DEFAULT_WAVE_SCALE; + } + double scale = settings.getWaveScale(); + return scale > 0 ? scale : DEFAULT_WAVE_SCALE; + } + + public static int footprintLow(int dimension) { + int size = Math.abs(dimension); + return -(size / 2); + } + + public static int footprintHigh(int dimension) { + int size = Math.abs(dimension); + if (size <= 0) { + return 0; + } + return size - (size / 2) - 1; + } + + public static int outset(int delta, int low, int high) { + if (delta < low) { + return low - delta; + } + if (delta > high) { + return delta - high; + } + return 0; + } + + public static int columnTargetY(int dx, int dz, int lowX, int highX, int lowZ, int highZ, + double effectiveRadius, double falloff, int originalY, int meetY) { + double factor = columnInfluence( + dx, dz, lowX, highX, lowZ, highZ, effectiveRadius, falloff); + return (int) Math.round(originalY + (((double) meetY - originalY) * factor)); + } + + public static double columnInfluence(int dx, int dz, int lowX, int highX, int lowZ, int highZ, + double effectiveRadius, double falloff) { + int outX = outset(dx, lowX, highX); + int outZ = outset(dz, lowZ, highZ); + double distance = Math.sqrt((double) outX * outX + (double) outZ * outZ); + if (effectiveRadius <= 0) { + return distance <= 0 ? 1.0 : 0.0; + } + if (distance >= effectiveRadius) { + return 0.0; + } + double t = 1.0 - (distance / effectiveRadius); + if (t <= 0) { + return 0.0; + } + return Math.pow(t, Math.max(0.25, falloff)); + } + + public static int waveOffset(double distance, double effectiveRadius, double signedNoise, double amplitude) { + if (amplitude <= 0 || effectiveRadius <= 0) { + return 0; + } + if (distance <= 0 || distance >= effectiveRadius) { + return 0; + } + double envelope = Math.sin(Math.PI * (distance / effectiveRadius)); + return (int) Math.round(signedNoise * amplitude * envelope); + } + + public static int carveFloorY(int targetY, int objectTopY, boolean insideFootprint) { + int floor = targetY + 1; + if (insideFootprint) { + floor = Math.max(floor, objectTopY + 1); + } + return floor; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVanillaLoot.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVanillaLoot.java new file mode 100644 index 000000000..90e35395f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVanillaLoot.java @@ -0,0 +1,56 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListFunction; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.annotations.functions.LootTableKeyFunction; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; +import lombok.experimental.Accessors; + +@Snippet("object-vanilla-loot") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents vanilla loot within this object") +@Data +public class IrisObjectVanillaLoot implements IObjectLoot { + private final transient AtomicCache> filterCache = new AtomicCache<>(); + @ArrayType(min = 1, type = IrisBlockData.class) + @Desc("The list of blocks this loot table should apply to") + private KList filter = new KList<>(); + @Desc("Exactly match the block data or not") + private boolean exact = false; + @Desc("The vanilla loot table key") + @Required + @RegistryListFunction(LootTableKeyFunction.class) + private String name; + @Desc("The weight of this loot table being chosen") + @MinNumber(1) + private int weight = 1; + + public KList getFilter(IrisData rdata) { + return filterCache.aquire(() -> + { + KList b = new KList<>(); + + for (IrisBlockData i : filter) { + PlatformBlockState bx = i.getBlockData(rdata); + + if (bx != null) { + b.add(bx); + } + } + + return b; + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java new file mode 100644 index 000000000..f124c8e89 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java @@ -0,0 +1,77 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import art.arcane.iris.spi.PlatformBlockState; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Scatters single ore blocks through terrain by sampling a noise field at every block in a vertical band. Defined on dimensions, regions, or biomes; the most specific level wins.") +@Data +public class IrisOreGenerator { + @Desc("The ore blocks to place. An empty palette generates nothing, so this must be set for the generator to have any effect.") + private IrisMaterialPalette palette = new IrisMaterialPalette().qclear(); + @Desc("The noise style sampled per block to decide where ore appears. STATIC gives independent random speckle; smoother styles give veiny clusters.") + private IrisGeneratorStyle chanceStyle = new IrisGeneratorStyle(NoiseStyle.STATIC); + @Desc("When true this generator also replaces surface-layer blocks (it runs before layers and fluid); when false it only replaces underground rock.") + private boolean generateSurface = false; + @MinNumber(0) + @MaxNumber(1) + @Desc("Noise cutoff: a block becomes ore when the sampled noise is at or below this value, so higher means more ore. 0 disables, 1 replaces everything in range.") + private double threshold = 0.5; + @Desc("Vertical band (min, max) this ore can generate in, in engine-local Y where 0 is the bottom of the dimension, not world Y.") + private IrisRange range = new IrisRange(30, 80); + + private transient AtomicCache chanceCache = new AtomicCache<>(); + + public void warm(RNG rng, IrisData data) { + chanceCache.aquire(() -> chanceStyle.create(rng, data)); + palette.getLayerGenerator(rng, data); + } + + public PlatformBlockState generate(int x, int y, int z, RNG rng, IrisData data) { + if (!range.contains(y)) { + return null; + } + + if (palette.getPalette().isEmpty()) { + return null; + } + + CNG chance = chanceCache.aquire(() -> chanceStyle.create(rng, data)); + + if (chance.noise(x, y, z) > threshold) { + return null; + } + + return palette.get(rng, x, y, z, data); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGeneratorBounds.java b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGeneratorBounds.java new file mode 100644 index 000000000..af64c4f4e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGeneratorBounds.java @@ -0,0 +1,43 @@ +package art.arcane.iris.engine.object; + +import art.arcane.volmlib.util.collection.KList; + +public final class IrisOreGeneratorBounds { + public static final IrisOreGeneratorBounds EMPTY = new IrisOreGeneratorBounds(false, 0D, 0D); + + private final boolean hasOres; + private final double min; + private final double max; + + private IrisOreGeneratorBounds(boolean hasOres, double min, double max) { + this.hasOres = hasOres; + this.min = min; + this.max = max; + } + + public static IrisOreGeneratorBounds of(KList ores) { + if (ores == null || ores.isEmpty()) { + return EMPTY; + } + + double min = Double.POSITIVE_INFINITY; + double max = Double.NEGATIVE_INFINITY; + int oreCount = ores.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = ores.get(oreIndex); + IrisRange range = oreGenerator.getRange(); + min = Math.min(min, range.getMin()); + max = Math.max(max, range.getMax()); + } + + return new IrisOreGeneratorBounds(true, min, max); + } + + public boolean hasOres() { + return hasOres; + } + + public boolean contains(int y) { + return hasOres && y >= min && y <= max; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisPosition.java b/core/src/main/java/art/arcane/iris/engine/object/IrisPosition.java new file mode 100644 index 000000000..b2bbbd310 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisPosition.java @@ -0,0 +1,69 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("position-3d") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Represents a position") +@Data +public class IrisPosition { + @Desc("The x position") + private int x = 0; + + @Desc("The y position") + private int y = 0; + + @Desc("The z position") + private int z = 0; + + public IrisPosition(double x, double y, double z) { + this((int) x, (int) y, (int) z); + } + + + public IrisPosition add(IrisPosition relativePosition) { + return new IrisPosition(relativePosition.x + x, relativePosition.y + y, relativePosition.z + z); + } + + public IrisPosition sub(IrisPosition relativePosition) { + return new IrisPosition(x - relativePosition.x, y - relativePosition.y, z - relativePosition.z); + } + + public IrisPosition copy() { + return new IrisPosition(x, y, z); + } + + @Override + public String toString() { + return "[" + getX() + "," + getY() + "," + getZ() + "]"; + } + + public boolean isLongerThan(IrisPosition s, int maxLength) { + return Math.abs(Math.pow(s.x - x, 2) + Math.pow(s.y - y, 2) + Math.pow(s.z - z, 2)) > maxLength * maxLength; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java b/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java new file mode 100644 index 000000000..f5cc2f3f1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java @@ -0,0 +1,102 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.entity.LivingEntity; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor + +@Snippet("potion-effect") +@Desc("An iris potion effect") +@Data +public class IrisPotionEffect { + private final transient AtomicCache pt = new AtomicCache<>(); + @Required + @RegistryListPotionEffect + @Desc("The potion effect to apply in this area") + private String potionEffect = ""; + @Required + @MinNumber(-1) + @MaxNumber(1024) + @Desc("The Potion Strength or -1 to disable") + private int strength = -1; + @Required + @MinNumber(1) + @Desc("The time the potion will last for") + private int ticks = 200; + @Desc("Is the effect ambient") + private boolean ambient = false; + @Desc("Is the effect showing particles") + private boolean particles = true; + + public PotionEffectType getRealType() { + return pt.aquire(() -> + { + if (getPotionEffect() == null || getPotionEffect().isEmpty()) { + return PotionEffectType.LUCK; + } + + try { + PotionEffectType resolved = PotionEffectTypes.resolve(getPotionEffect()); + if (resolved != null) { + return resolved; + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + if (PotionEffectTypes.shouldWarn(getPotionEffect())) { + IrisLogging.warn("Unknown Potion Effect Type: \"" + getPotionEffect() + "\". Valid types: " + PotionEffectTypes.knownTypesList()); + } + + return PotionEffectType.LUCK; + }); + } + + public void apply(LivingEntity p) { + if (strength > -1) { + if (p.hasPotionEffect(getRealType())) { + PotionEffect e = p.getPotionEffect(getRealType()); + if (e.getAmplifier() > strength) { + return; + } + + p.removePotionEffect(getRealType()); + } + + p.addPotionEffect(new PotionEffect(getRealType(), ticks, strength, ambient, particles, false)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralBlocks.java b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralBlocks.java new file mode 100644 index 000000000..845d57556 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralBlocks.java @@ -0,0 +1,166 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +import java.util.Map; + +/** + * Shared low-level helpers for building procedural objects in memory: resolving a + * single block id or a noise palette into a platform block state, and assembling a + * raw block map into a centered IrisObject anchored so its y=0 layer sits one block + * above the terrain surface (same convention the tree generator uses). + */ +public final class IrisProceduralBlocks { + public static final String[] FACE_PROPERTIES = {"north", "east", "south", "west", "up", "down"}; + + private IrisProceduralBlocks() { + } + + public static int[] faceOffset(String face) { + return switch (face) { + case "north" -> new int[]{0, 0, -1}; + case "south" -> new int[]{0, 0, 1}; + case "east" -> new int[]{1, 0, 0}; + case "west" -> new int[]{-1, 0, 0}; + case "up" -> new int[]{0, 1, 0}; + default -> new int[]{0, -1, 0}; + }; + } + + public static boolean paletteSet(IrisMaterialPalette palette) { + return palette != null && palette.getPalette() != null && !palette.getPalette().isEmpty(); + } + + public static PlatformBlockState resolve(String block, IrisMaterialPalette palette, IrisData data, int x, int y, int z, RNG paletteRng) { + if (paletteSet(palette)) { + return palette.get(paletteRng, x, y, z, data); + } + if (block != null && !block.isEmpty()) { + return B.getStateOrNull(block, false); + } + return null; + } + + public static boolean hasProperty(PlatformBlockState state, String property) { + String key = state.key(); + int start = propertyStart(key, property); + if (start < 0) { + return false; + } + int valueStart = start + property.length() + 2; + return key.indexOf(',', valueStart) >= 0 || key.indexOf(']', valueStart) >= 0; + } + + public static String materialKey(PlatformBlockState state) { + String memoized = state.materialKey(); + if (memoized != null) { + return memoized; + } + String key = state.key(); + int bracket = key.indexOf('['); + return bracket < 0 ? key : key.substring(0, bracket); + } + + public static String propertyValue(PlatformBlockState state, String property) { + String key = state.key(); + int start = propertyStart(key, property); + if (start < 0) { + return null; + } + int valueStart = start + property.length() + 2; + int end = key.indexOf(',', valueStart); + if (end < 0) { + end = key.indexOf(']', valueStart); + } + if (end < 0) { + return null; + } + return key.substring(valueStart, end); + } + + /** + * Index of the delimiter opening "[property=" or ",property=" within the key, or -1. Scans in place + * rather than building the search needles, and prefers a "[" match over a "," match. + */ + private static int propertyStart(String key, String property) { + int bracket = key.indexOf('['); + if (bracket < 0) { + return -1; + } + int start = delimitedPropertyIndex(key, property, bracket, '['); + return start < 0 ? delimitedPropertyIndex(key, property, bracket, ',') : start; + } + + private static int delimitedPropertyIndex(String key, String property, int from, char delimiter) { + int length = property.length(); + int limit = key.length() - length - 2; + for (int i = key.indexOf(delimiter, from); i >= 0 && i <= limit; i = key.indexOf(delimiter, i + 1)) { + if (key.regionMatches(i + 1, property, 0, length) && key.charAt(i + 1 + length) == '=') { + return i; + } + } + return -1; + } + + public static IrisObject assemble(Map blocks) { + if (blocks == null || blocks.isEmpty()) { + return null; + } + + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (Vector3i v : blocks.keySet()) { + minX = Math.min(minX, v.getBlockX()); + minY = Math.min(minY, v.getBlockY()); + minZ = Math.min(minZ, v.getBlockZ()); + maxX = Math.max(maxX, v.getBlockX()); + maxY = Math.max(maxY, v.getBlockY()); + maxZ = Math.max(maxZ, v.getBlockZ()); + } + + int w = maxX - minX + 1; + int h = maxY - minY + 1; + int d = maxZ - minZ + 1; + int cx = w / 2; + int cy = h / 2; + int cz = d / 2; + + IrisObject object = new IrisObject(w, h, d); + for (Map.Entry entry : blocks.entrySet()) { + Vector3i v = entry.getKey(); + int nx = v.getBlockX() - minX - cx; + int ny = v.getBlockY() - cy + 1; + int nz = v.getBlockZ() - minZ - cz; + object.getBlocks().put(new IrisBlockVector(nx, ny, nz), entry.getValue()); + } + + return object; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralObjects.java b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralObjects.java new file mode 100644 index 000000000..c5bef01fa --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralObjects.java @@ -0,0 +1,87 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("procedural-objects") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Procedurally generated objects placed in a biome or region. Unlike the objects block (which loads iob files), these are generated from scratch at world-gen time.") +@Data +public class IrisProceduralObjects { + @ArrayType(min = 1, type = IrisProceduralTree.class) + @Desc("Procedurally generated trees.") + private KList trees = new KList<>(); + + @ArrayType(min = 1, type = IrisRuin.class) + @Desc("Procedurally generated ruins (crumbling pillars, walls, arches, rubble).") + private KList ruins = new KList<>(); + + @ArrayType(min = 1, type = IrisFormation.class) + @Desc("Procedurally generated natural rock formations (spires, hoodoos, arches, sea stacks, boulders, basalt columns).") + private KList formations = new KList<>(); + + @ArrayType(min = 1, type = IrisCoral.class) + @Desc("Procedurally generated underwater coral structures.") + private KList coral = new KList<>(); + + @ArrayType(min = 1, type = IrisFungus.class) + @Desc("Procedurally generated mushrooms and shelf fungi.") + private KList fungi = new KList<>(); + + @ArrayType(min = 1, type = IrisCrystal.class) + @Desc("Procedurally generated crystal clusters (usually placed in caves).") + private KList crystals = new KList<>(); + + public KList getAllPlacements() { + KList all = new KList<>(); + if (trees != null) { + all.addAll(trees); + } + if (ruins != null) { + all.addAll(ruins); + } + if (formations != null) { + all.addAll(formations); + } + if (coral != null) { + all.addAll(coral); + } + if (fungi != null) { + all.addAll(fungi); + } + if (crystals != null) { + all.addAll(crystals); + } + return all; + } + + public boolean isEmpty() { + return getAllPlacements().isEmpty(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java new file mode 100644 index 000000000..904c471b4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java @@ -0,0 +1,46 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +/** + * A procedurally generated, placeable thing in a biome or region (trees, ruins, + * formations, coral, fungi, crystals). Implementations bake a deterministic pool + * of in-memory IrisObject variants and expose the placement controls the engine + * needs to scatter them, exactly like an object placement but generated from + * scratch instead of loaded from an iob file. + */ +public interface IrisProceduralPlacement { + String getName(); + + double getChance(); + + int getDensity(); + + boolean isPlausible(); + + IrisObjectPlacement asPlacement(); + + IrisObject getVariantObject(IrisData data, RNG rng); + + KList getVariantObjects(IrisData data); +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralTree.java b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralTree.java new file mode 100644 index 000000000..ac8d26dcb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralTree.java @@ -0,0 +1,332 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.tree.ProceduralTreeGenerator; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("procedural-tree") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A single procedurally generated tree. Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file.") +@Data +public class IrisProceduralTree implements IrisProceduralPlacement { + private final transient AtomicCache> variantCache = new AtomicCache<>(); + + @Desc("A human readable name used in logs and as the variant load key.") + private String name = "procedural-tree"; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance per chunk for this tree to attempt placement. Use density for multiple per chunk.") + private double chance = 0.4; + + @MinNumber(1) + @Desc("If the chance check passes, attempt this many placements in the chunk.") + private int density = 1; + + @MinNumber(1) + @MaxNumber(64) + @Desc("How many distinct variants to pre-bake for this tree. Higher means more variety at a small memory cost.") + private int variants = 8; + + @Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.") + private long seed = 1337; + + @Desc("If true (default) leaves are placed with vanilla distance-from-wood values and persistent=false so leaf decay behaves naturally and the tree reads as plausible. If false, leaves are forced persistent and never decay (a raw object dump).") + private boolean plausible = true; + + @Desc("The placement mode used to anchor the tree to the terrain.") + private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT; + + @Desc("Rotate this tree's placement.") + private IrisObjectRotation rotation = new IrisObjectRotation(); + + @Desc("Limit the max or min height of placement.") + private IrisObjectLimit clamp = new IrisObjectLimit(); + + @Desc("Whether this tree may place on the terrain surface, under carvings, or both.") + private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY; + + @Desc("If true, the tree anchors on the terrain height ignoring the water surface.") + private boolean underwater = false; + + @Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.") + private IrisObjectTranslate translate = new IrisObjectTranslate(); + + @Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).") + private IrisStiltSettings stiltSettings; + + @Desc("Settings for the vacuum place modes (VACUUM, VACUUM_HIGH, VACUUM_FAST, VACUUM_ORGANIC).") + private IrisVacuumSettings vacuumSettings; + + @Required + @Desc("The trunk (log) block, e.g. minecraft:oak_log. Ignored when trunkPalette is set.") + @RegistryListBlockType + private String trunk = "minecraft:oak_log"; + + @Desc("A noise-driven palette for the trunk. When set this overrides the single trunk block, letting the trunk mix blocks by noise.") + private IrisMaterialPalette trunkPalette = null; + + @Required + @Desc("The leaf block, e.g. minecraft:oak_leaves. Ignored when leavesPalette is set.") + @RegistryListBlockType + private String leaves = "minecraft:oak_leaves"; + + @Desc("A noise-driven palette for the leaves. When set this overrides the single leaf block, letting the canopy mix leaf (or other) blocks by noise. Only blocks that are actually leaves get vanilla decay distances.") + private IrisMaterialPalette leavesPalette = null; + + @Desc("The named canopy profile that drives default crown radii and shape.") + private IrisTreeProfile profile = IrisTreeProfile.OAK; + + @MinNumber(1) + @Desc("Minimum trunk height in blocks.") + private int heightMin = 8; + + @MinNumber(1) + @Desc("Maximum trunk height in blocks.") + private int heightMax = 12; + + @MinNumber(1) + @Desc("Base trunk width in blocks. 1 is a single column, 2 is a 2x2 trunk, etc.") + private int trunkWidth = 1; + + @Desc("The function used to shape trunk width over height.") + private IrisTreeFunction trunkShape = IrisTreeFunction.CONSTANT; + + @Desc("Width multiplier at the base for the LINEAR trunk shape.") + private double shapeStart = 1; + + @Desc("Width multiplier at the top for the LINEAR trunk shape.") + private double shapeEnd = 1; + + @Desc("Steepness for the SIGMOID trunk shape.") + private double shapeSteepness = 5; + + @Desc("Base for the LOG trunk shape.") + private double shapeBase = 2.718281828; + + @Desc("Period for the SINE trunk shape.") + private double shapePeriod = 1; + + @Desc("Amplitude for the SINE trunk shape.") + private double shapeAmplitude = 0.2; + + @Desc("Pinch position (0-1) for the PARABOLIC trunk shape.") + private double shapePeakOffset = 0.5; + + @Desc("Minimum width fraction at the waist for the PARABOLIC trunk shape.") + private double shapeFloor = 0.5; + + @Desc("Compass direction in degrees the trunk leans toward.") + private double leanAzimuth = 0; + + @Desc("How far the trunk leans, in degrees from vertical. 0 is perfectly upright.") + private double leanAngle = 0; + + @Desc("How the lean accumulates over height.") + private IrisTreeFunction trunkCurve = IrisTreeFunction.LINEAR; + + @Desc("Steepness for the SIGMOID trunk curve.") + private double curveSteepness = 8; + + @Desc("How the lean azimuth itself changes over height (for spiraling or wandering trunks).") + private IrisTreeAzimuthMode leanAzimuthMode = IrisTreeAzimuthMode.CONSTANT; + + @Desc("Azimuth at the base for the LINEAR azimuth mode.") + private double azimuthStart = 0; + + @Desc("Azimuth at the top for the LINEAR azimuth mode.") + private double azimuthEnd = 0; + + @Desc("Number of full turns for the SPIRAL azimuth mode.") + private double azimuthTurns = 1; + + @Desc("Amplitude for the SINE azimuth mode.") + private double azimuthAmplitude = 90; + + @Desc("Period for the SINE azimuth mode.") + private double azimuthPeriod = 1; + + @Desc("Offset for the SINE azimuth mode.") + private double azimuthOffset = 0; + + @Desc("Scale for the NOISE azimuth mode.") + private double azimuthScale = 1; + + @MinNumber(1) + @Desc("Number of branches per whorl ring for the WHORL branch azimuth mode.") + private int azimuthWhorlCount = 5; + + @Desc("Optional accent leaf block scattered through the canopy (blossoms, shroomlight).") + @RegistryListBlockType + private String secondaryLeaves = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The fraction of leaves replaced by the secondary leaf block(s).") + private double secondaryLeafFraction = 0.35; + + @ArrayType(min = 1, type = IrisTreeSecondaryLeaf.class) + @Desc("Optional weighted set of accent leaf blocks. When set this overrides the single secondaryLeaves block.") + private KList weightedSecondaryLeaves = new KList<>(); + + @Desc("A noise-driven palette for the secondary (accent) leaves. When set this overrides both secondaryLeaves and weightedSecondaryLeaves.") + private IrisMaterialPalette secondaryLeavesPalette = null; + + @Desc("Optional secondary trunk block used over a height band (for color-banded trunks). Ignored when secondaryTrunkPalette is set.") + @RegistryListBlockType + private String secondaryTrunk = null; + + @Desc("A noise-driven palette for the secondary trunk band. When set this overrides the single secondaryTrunk block.") + private IrisMaterialPalette secondaryTrunkPalette = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The normalized height where the secondary trunk band starts.") + private double secondaryTrunkStart = 0.5; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The normalized height where the secondary trunk band ends.") + private double secondaryTrunkEnd = 1; + + @Desc("If true, a root system extends below (and around) the base so the tree connects to the ground on uneven terrain.") + private boolean roots = true; + + @Desc("How the root system is built when roots is true.") + private IrisTreeRootStyle rootStyle = IrisTreeRootStyle.BUTTRESS; + + @MinNumber(0) + @Desc("Override the root depth/reach in blocks. 0 means auto (scales with tree height).") + private int rootDepth = 0; + + @MinNumber(0) + @Desc("Override the root flare radius in blocks. 0 means auto.") + private double rootFlare = 0; + + @MinNumber(1) + @MaxNumber(6) + @Desc("How many separate trunks the tree splits into above forkHeight. 1 is a single trunk.") + private int trunkForks = 1; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The normalized height where the trunk splits into forks.") + private double forkHeight = 0.5; + + @Desc("The outward lean angle in degrees of each fork.") + private double forkAngle = 25; + + @Desc("The leaf crown configuration.") + private IrisTreeCanopy canopy = new IrisTreeCanopy(); + + @ArrayType(min = 1, type = IrisTreeDecorator.class) + @Desc("Accent decorators applied after the tree is built (fruit, vines, snow, glow).") + private KList decorators = new KList<>(); + + public KList getVariantObjects(IrisData data) { + return variantCache.aquire(() -> { + KList baked = new KList<>(); + int count = Math.max(1, variants); + int lo = Math.min(heightMin, heightMax); + int hi = Math.max(heightMin, heightMax); + RNG heightRng = new RNG(seed); + + for (int i = 0; i < count; i++) { + int height; + if (count == 1 || lo == hi) { + height = heightRng.i(lo, hi + 1); + } else { + double step = (hi - lo) / (double) (count - 1); + double base = lo + step * i; + double jitter = heightRng.d(-step * 0.3, step * 0.3); + height = (int) Math.round(Math.max(lo, Math.min(hi, base + jitter))); + } + + IrisObject object = ProceduralTreeGenerator.generate(this, Math.max(2, height), new RNG(seed + (i * 7919L)), data); + if (object == null || object.getBlocks().isEmpty()) { + continue; + } + object.setLoadKey(getVariantLoadKey(i)); + object.setLoader(data); + baked.add(object); + } + + if (new java.io.File("plugins/Iris/goldendebug.txt").isFile()) { + for (IrisObject object : baked) { + long[] digest = new long[]{0L}; + object.getBlocks().forEach((vector, state) -> { + long position = ((long) vector.getBlockX() << 40) ^ ((long) vector.getBlockY() << 20) ^ vector.getBlockZ(); + digest[0] ^= Long.rotateLeft(position * 0x9E3779B97F4A7C15L ^ state.key().hashCode(), (int) (position & 63)); + }); + IrisLogging.info("Goldendebug bake: " + object.getLoadKey() + " blocks=" + object.getBlocks().size() + " digest=" + Long.toHexString(digest[0])); + } + } + + return baked; + }); + } + + public IrisObject getVariantObject(IrisData data, RNG rng) { + KList baked = getVariantObjects(data); + if (baked == null || baked.isEmpty()) { + return null; + } + return baked.get(rng.i(baked.size())); + } + + public String getVariantLoadKey(int index) { + if (index < 0) { + throw new IllegalArgumentException("index must not be negative"); + } + return "procedural/tree/" + name + "#" + index; + } + + public IrisObjectPlacement asPlacement() { + IrisObjectPlacement placement = new IrisObjectPlacement(); + placement.setMode(mode); + placement.setRotation(rotation); + placement.setClamp(clamp); + placement.setCarvingSupport(carvingSupport); + placement.setUnderwater(underwater); + placement.setTranslate(translate); + placement.setStiltSettings(stiltSettings); + placement.setVacuumSettings(vacuumSettings); + placement.setChance(chance); + placement.setDensity(density); + return placement; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisRange.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRange.java similarity index 88% rename from core/src/main/java/com/volmit/iris/engine/object/IrisRange.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisRange.java index b352e2e49..5f7cd5e3d 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisRange.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRange.java @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.math.RNG; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.math.RNG; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisRate.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRate.java similarity index 86% rename from core/src/main/java/com/volmit/iris/engine/object/IrisRate.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisRate.java index 68dbb7927..72b3daa97 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisRate.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRate.java @@ -16,12 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.scheduling.ChronoLatch; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.scheduling.ChronoLatch; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java new file mode 100644 index 000000000..7fd44ba09 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java @@ -0,0 +1,463 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +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.data.DataProvider; +import art.arcane.volmlib.util.data.VanillaBiomeColors; +import art.arcane.volmlib.util.inventorygui.RandomColor; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.awt.Color; +import java.util.Random; + + +@Accessors(chain = true) +@NoArgsConstructor +@Desc("Represents an iris region") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisRegion extends IrisRegistrant implements IRare { + private final transient AtomicCache> surfaceObjectsCache = new AtomicCache<>(); + private final transient AtomicCache> carveObjectsCache = new AtomicCache<>(); + private final transient AtomicCache> cacheRidge = new AtomicCache<>(); + private final transient AtomicCache> cacheSpot = new AtomicCache<>(); + private final transient AtomicCache shoreHeightGenerator = new AtomicCache<>(); + private final transient AtomicCache> realLandBiomes = new AtomicCache<>(); + private final transient AtomicCache> realSeaBiomes = new AtomicCache<>(); + private final transient AtomicCache> realShoreBiomes = new AtomicCache<>(); + private final transient AtomicCache> realCaveBiomes = new AtomicCache<>(); + private final transient AtomicCache cacheColor = new AtomicCache<>(); + private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); + private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); + private final transient AtomicCache surfaceOreBoundsCache = new AtomicCache<>(); + private final transient AtomicCache undergroundOreBoundsCache = new AtomicCache<>(); + @MinNumber(2) + @Required + @Desc("The name of the region") + private String name = "A Region"; + @ArrayType(min = 1, type = IrisEffect.class) + @Desc("Effects are ambient effects such as potion effects, random sounds, or even particles around each player. All of these effects are played via packets so two players won't see/hear each others effects.\nDue to performance reasons, effects will play arround the player even if where the effect was played is no longer in the biome the player is in.") + private KList effects = new KList<>(); + @Desc("Spawn Entities in this region over time. Iris will continually replenish these mobs just like vanilla does.") + @ArrayType(min = 1, type = String.class) + @RegistryListResource(IrisSpawner.class) + private KList entitySpawners = new KList<>(); + @MinNumber(1) + @MaxNumber(128) + @Desc("The rarity of the region") + private int rarity = 1; + @ArrayType(min = 1, type = IrisBlockDrops.class) + @Desc("Define custom block drops for this region") + private KList blockDrops = new KList<>(); + @ArrayType(min = 1, type = IrisObjectPlacement.class) + @Desc("Objects define what schematics (iob files) iris will place in this region") + private KList objects = new KList<>(); + @Desc("Procedural objects (trees, ruins, formations, coral, fungi, crystals) iris generates from scratch and places across this region") + private IrisProceduralObjects proceduralObjects = new IrisProceduralObjects(); + @ArrayType(min = 1, type = IrisStructurePlacement.class) + @Desc("Structures define jigsaw or vanilla/datapack structures iris will place in this region") + private KList structures = new KList<>(); + @MinNumber(0) + @Desc("The min shore height") + private double shoreHeightMin = 1.2; + @Desc("Reference loot tables in this area") + private IrisLootReference loot = new IrisLootReference(); + @MinNumber(0) + @Desc("The the max shore height") + private double shoreHeightMax = 3.2; + @MinNumber(0.0001) + @Desc("The varience of the shore height") + private double shoreHeightZoom = 3.14; + @MinNumber(0.0001) + @Desc("How large land biomes are in this region") + private double landBiomeZoom = 1; + @MinNumber(0.0001) + @Desc("How large shore biomes are in this region") + private double shoreBiomeZoom = 1; + @MinNumber(0.0001) + @Desc("How large sea biomes are in this region") + private double seaBiomeZoom = 1; + @MinNumber(0.0001) + @Desc("How large cave biomes are in this region") + private double caveBiomeZoom = 1; + @Desc("Profile-driven 3D cave configuration") + private IrisCaveProfile caveProfile = new IrisCaveProfile(); + @RegistryListResource(IrisBiome.class) + @Required + @ArrayType(min = 1, type = String.class) + @Desc("A list of root-level biomes in this region. Don't specify child biomes of other biomes here. Just the root parents.") + private KList landBiomes = new KList<>(); + @RegistryListResource(IrisBiome.class) + @ArrayType(type = String.class) + @Desc("A list of root-level sea biomes in this region. Optional: leave empty for land-only dimensions (flat, island, or fully-land worlds where no ocean is generated). Don't specify child biomes of other biomes here. Just the root parents.") + private KList seaBiomes = new KList<>(); + @RegistryListResource(IrisBiome.class) + @ArrayType(type = String.class) + @Desc("A list of root-level shore biomes in this region. Optional: leave empty for land-only dimensions (flat, island, or fully-land worlds where no shoreline is generated). Don't specify child biomes of other biomes here. Just the root parents.") + private KList shoreBiomes = new KList<>(); + @RegistryListResource(IrisBiome.class) + @ArrayType(min = 1, type = String.class) + @Desc("A list of root-level cave biomes in this region, used for carved cave interiors. Don't specify child biomes of other biomes here. Just the root parents.") + private KList caveBiomes = new KList<>(); + @ArrayType(min = 1, type = IrisDepositGenerator.class) + @Desc("Define regional deposit generators that add onto the global deposit generators") + private KList deposits = new KList<>(); + @ArrayType(min = 1, type = IrisDepositVariant.class) + @Desc("Deposit ore remap rules scoped to this region. Each entry declares a vertical band and a source->replacement block id map. Applied after biome rules but before dimension rules; first matching region rule wins.") + private KList depositVariants = new KList<>(); + @Desc("A color for visualizing this region with a color. I.e. #F13AF5. This will show up on the map.") + private String color = null; + @Desc("Collection of ores to be generated") + @ArrayType(type = IrisOreGenerator.class, min = 1) + private KList ores = new KList<>(); + + public PlatformBlockState generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) { + KList localOres = surface ? getSurfaceOres() : getUndergroundOres(); + return generateOres(localOres, x, y, z, rng, data); + } + + public PlatformBlockState generateSurfaceOres(int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getSurfaceOres(), x, y, z, rng, data); + } + + public PlatformBlockState generateUndergroundOres(int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getUndergroundOres(), x, y, z, rng, data); + } + + public boolean hasSurfaceOres() { + return !getSurfaceOres().isEmpty(); + } + + public boolean hasUndergroundOres() { + return !getUndergroundOres().isEmpty(); + } + + private PlatformBlockState generateOres(KList localOres, int x, int y, int z, RNG rng, IrisData data) { + if (localOres.isEmpty()) { + return null; + } + + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + PlatformBlockState ore = oreGenerator.generate(x, y, z, rng, data); + if (ore != null) { + return ore; + } + } + return null; + } + + public void setOres(KList ores) { + this.ores = ores == null ? new KList<>() : ores; + surfaceOreCache.reset(); + undergroundOreCache.reset(); + surfaceOreBoundsCache.reset(); + undergroundOreBoundsCache.reset(); + } + + public KList getSurfaceOreGenerators() { + return getOres(true); + } + + public KList getUndergroundOreGenerators() { + return getOres(false); + } + + public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() { + return surfaceOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres())); + } + + public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() { + return undergroundOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres())); + } + + private KList getSurfaceOres() { + return getOres(true); + } + + private KList getUndergroundOres() { + return getOres(false); + } + + private KList getOres(boolean surface) { + AtomicCache> oreCache = surface ? surfaceOreCache : undergroundOreCache; + return oreCache.aquire(() -> { + KList filtered = new KList<>(); + KList localOres = ores; + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + if (oreGenerator.isGenerateSurface() == surface) { + filtered.add(oreGenerator); + } + } + + return filtered; + }); + } + + public String getName() { + return name; + } + + public KList getSurfaceObjects() { + return getSurfaceObjectsCache().aquire(() -> + { + KList o = getObjects().copy(); + + for (IrisObjectPlacement i : o.copy()) { + if (!i.getCarvingSupport().supportsSurface()) { + o.remove(i); + } + } + + return o; + }); + } + + public KList getCarvingObjects() { + return getCarveObjectsCache().aquire(() -> + { + KList o = getObjects().copy(); + + for (IrisObjectPlacement i : o.copy()) { + if (!i.getCarvingSupport().supportsCarving()) { + o.remove(i); + } + } + + return o; + }); + } + + public CNG getShoreHeightGenerator() { + return shoreHeightGenerator.aquire(() -> + CNG.signature(new RNG((long) (getName().length() + getLandBiomeZoom() + getLandBiomes().size() + 3458612)))); + } + + public double getShoreHeight(double x, double z) { + return getShoreHeightGenerator().fitDouble(shoreHeightMin, shoreHeightMax, x / shoreHeightZoom, z / shoreHeightZoom); + } + + public KSet getAllBiomeIds() { + KSet names = new KSet<>(); + names.addAll(landBiomes); + names.addAll(caveBiomes); + names.addAll(seaBiomes); + names.addAll(shoreBiomes); + + return names; + } + + public KList getAllBiomes(DataProvider g) { + KMap b = new KMap<>(); + KSet names = getAllBiomeIds(); + + while (!names.isEmpty()) { + for (String i : new KList<>(names)) { + if (b.containsKey(i)) { + names.remove(i); + continue; + } + + IrisBiome biome = g.getData().getBiomeLoader().load(i); + + names.remove(i); + if (biome == null) { + continue; + } + + names.add(biome.getCarvingBiome()); + b.put(biome.getLoadKey(), biome); + names.addAll(biome.getChildren()); + } + } + + return b.v(); + } + + public KList getBiomes(DataProvider g, InferredType type) { + if (type.equals(InferredType.LAND)) { + return getRealLandBiomes(g); + } else if (type.equals(InferredType.SEA)) { + return getRealSeaBiomes(g); + } else if (type.equals(InferredType.SHORE)) { + return getRealShoreBiomes(g); + } else if (type.equals(InferredType.CAVE)) { + return getRealCaveBiomes(g); + } + + return new KList<>(); + } + + public KList getRealCaveBiomes(DataProvider g) { + return realCaveBiomes.aquire(() -> + { + KList realCaveBiomes = new KList<>(); + + for (String i : getCaveBiomes()) { + realCaveBiomes.add(g.getData().getBiomeLoader().load(i)); + } + + return realCaveBiomes; + }); + } + + public KList getRealShoreBiomes(DataProvider g) { + return realShoreBiomes.aquire(() -> + { + KList realShoreBiomes = new KList<>(); + + for (String i : getShoreBiomes()) { + realShoreBiomes.add(g.getData().getBiomeLoader().load(i)); + } + + return realShoreBiomes; + }); + } + + public KList getRealSeaBiomes(DataProvider g) { + return realSeaBiomes.aquire(() -> + { + KList realSeaBiomes = new KList<>(); + + for (String i : getSeaBiomes()) { + realSeaBiomes.add(g.getData().getBiomeLoader().load(i)); + } + + return realSeaBiomes; + }); + } + + public KList getRealLandBiomes(DataProvider g) { + return realLandBiomes.aquire(() -> + { + KList realLandBiomes = new KList<>(); + + for (String i : getLandBiomes()) { + realLandBiomes.add(g.getData().getBiomeLoader().load(i)); + } + + return realLandBiomes; + }); + } + + public KList getAllAnyBiomes() { + KMap b = new KMap<>(); + KSet names = new KSet<>(); + names.addAll(landBiomes); + names.addAll(caveBiomes); + names.addAll(seaBiomes); + names.addAll(shoreBiomes); + + while (!names.isEmpty()) { + for (String i : new KList<>(names)) { + if (b.containsKey(i)) { + names.remove(i); + continue; + } + + IrisBiome biome = IrisData.loadAnyBiome(i, getLoader()); + + names.remove(i); + if (biome == null) { + continue; + } + + names.add(biome.getCarvingBiome()); + b.put(biome.getLoadKey(), biome); + names.addAll(biome.getChildren()); + } + } + + return b.v(); + } + + public Color getColor(DataProvider dataProvider, RenderType type) { + return this.cacheColor.aquire(() -> { + if (this.color == null) { + Random rand = new Random(getName().hashCode() + getAllBiomeIds().hashCode()); + RandomColor randomColor = new RandomColor(rand); + + KList biomes = getRealLandBiomes(dataProvider); + + while (biomes.size() > 0) { + int index = rand.nextInt(biomes.size()); + IrisBiome biome = biomes.get(index); + + String vanillaKey = biome.getVanillaDerivativeKey(); + RandomColor.Color col = vanillaKey == null ? null : VanillaBiomeColors.getColorType(vanillaKey); + if (col != null) { + RandomColor.Luminosity lum = VanillaBiomeColors.getColorLuminosity(vanillaKey); + RandomColor.SaturationType sat = VanillaBiomeColors.getColorSaturation(vanillaKey); + int newColorI = randomColor.randomColor(col, col == RandomColor.Color.MONOCHROME ? RandomColor.SaturationType.MONOCHROME : sat, lum); + return new Color(newColorI); + } + + biomes.remove(index); + } + + IrisLogging.warn("Couldn't find a suitable color for region " + getName()); + return new Color(new RandomColor(rand).randomColor()); + } + + try { + return Color.decode(this.color); + } catch (NumberFormatException e) { + IrisLogging.warn("Could not parse color \"" + this.color + "\" for region " + getName()); + return Color.WHITE; + } + }); + } + + public void pickRandomColor(DataProvider data) { + + } + + @Override + public String getFolderName() { + return "regions"; + } + + @Override + public String getTypeName() { + return "Region"; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRuin.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRuin.java new file mode 100644 index 000000000..0f74ddbf4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRuin.java @@ -0,0 +1,207 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.ruin.RuinGenerator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("ruin") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A single procedurally generated ruin: a crumbling man-made structure (pillar, wall, arch, foundation slab, or rubble pile). Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file. Weathering swaps blocks for mossy or cracked variants by noise, erosion deletes blocks so the shape reads as collapsed, and a buried fraction sinks the ruin into the terrain.") +@Data +public class IrisRuin implements IrisProceduralPlacement { + private final transient AtomicCache> variantCache = new AtomicCache<>(); + + @Desc("A human readable name used in logs and as the variant load key (procedural/#).") + private String name = "ruin"; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance (0-1) per chunk for this ruin to attempt placement. Use density to attempt more than one per chunk once the chance check passes.") + private double chance = 0.05; + + @MinNumber(1) + @Desc("If the chance check passes, attempt this many placements in the chunk. Defaults to a single ruin per qualifying chunk.") + private int density = 1; + + @MinNumber(1) + @MaxNumber(64) + @Desc("How many distinct variants to pre-bake for this ruin. Higher means more variety (different heights, gap patterns, erosion seeds) at a small memory cost.") + private int variants = 6; + + @Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants. Every random choice (size, weathering mask, erosion mask, accents) is derived from this.") + private long seed = 1337; + + @Desc("The placement mode used to anchor the ruin to the terrain. MIN_HEIGHT plants the lowest footprint corner on the ground (good for slabs and rubble on slopes); CENTER_HEIGHT averages the footprint (good for tall pillars and arches).") + private ObjectPlaceMode mode = ObjectPlaceMode.MIN_HEIGHT; + + @Desc("Rotate this ruin's placement so variants do not all face the same way.") + private IrisObjectRotation rotation = new IrisObjectRotation(); + + @Desc("Limit the max or min terrain height at which this ruin may place.") + private IrisObjectLimit clamp = new IrisObjectLimit(); + + @Desc("Whether this ruin may place on the terrain surface, under carvings (caves), or both. SURFACE_ONLY keeps ruins above ground.") + private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY; + + @Desc("If true the ruin anchors on the terrain height ignoring the water surface, so it can sit submerged. If false (default) it anchors to the surface and avoids water.") + private boolean underwater = false; + + @Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.") + private IrisObjectTranslate translate = new IrisObjectTranslate(); + + @Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).") + private IrisStiltSettings stiltSettings; + + @Desc("Settings for the vacuum place modes (VACUUM, VACUUM_HIGH, VACUUM_FAST, VACUUM_ORGANIC).") + private IrisVacuumSettings vacuumSettings; + + @Required + @Desc("The primary structural block, e.g. minecraft:cobblestone or minecraft:stone_bricks. Ignored when blockPalette is set. This is the bulk material of the ruin before weathering swaps some of it out.") + @RegistryListBlockType + private String block = "minecraft:cobblestone"; + + @Desc("A noise-driven palette for the primary block. When set this overrides the single block, letting the structure mix materials by noise. Palette wins over the block string via IrisProceduralBlocks.resolve.") + private IrisMaterialPalette blockPalette = null; + + @Desc("The overall silhouette of the ruin: PILLAR (broken column), WALL (gapped segment), ARCH (two legs plus a curved span), FLOOR_SLAB (foundation patch), or RUBBLE (low scattered pile).") + private IrisRuinForm form = IrisRuinForm.PILLAR; + + @MinNumber(1) + @Desc("Minimum structure height in blocks (PILLAR/WALL/ARCH height, slab thickness for FLOOR_SLAB, mound height for RUBBLE). Variants interpolate between this and heightMax.") + private int heightMin = 4; + + @MinNumber(1) + @Desc("Maximum structure height in blocks. Variants interpolate between heightMin and this so taller and shorter ruins coexist.") + private int heightMax = 9; + + @MinNumber(1) + @Desc("Minimum footprint width (X axis) in blocks: column/leg thickness for PILLAR/ARCH, wall thickness for WALL, slab width for FLOOR_SLAB, blob width for RUBBLE.") + private int widthMin = 1; + + @MinNumber(1) + @Desc("Maximum footprint width (X axis) in blocks. Variants interpolate between widthMin and this.") + private int widthMax = 3; + + @MinNumber(1) + @Desc("Minimum footprint length (Z axis) in blocks: wall run length, arch leg spacing, slab length, rubble blob length.") + private int lengthMin = 3; + + @MinNumber(1) + @Desc("Maximum footprint length (Z axis) in blocks. Variants interpolate between lengthMin and this.") + private int lengthMax = 7; + + @Desc("A palette of weathered variants (mossy/cracked block ids) blended over the structure by noise. When set this drives weathering; otherwise weatheredBlock is used.") + private IrisMaterialPalette weatheringPalette = null; + + @Desc("A single weathered block id used when weatheringPalette is not set, e.g. minecraft:mossy_cobblestone. Applied to a noise-selected fraction of the structure, biased toward the lower rows.") + @RegistryListBlockType + private String weatheredBlock = "minecraft:mossy_cobblestone"; + + @MinNumber(0) + @MaxNumber(1) + @Desc("How much of the structure is replaced by weathered blocks (0 none, 1 nearly all). The replacement mask is value-noise driven and weighted so lower rows weather more than upper rows, mimicking moss climbing from the ground.") + private double mossiness = 0.45; + + @MinNumber(0) + @MaxNumber(8) + @Desc("The scale of the weathering noise. Higher values produce smaller, busier moss/crack patches; lower values produce broad continuous weathered zones.") + private double weatheringScale = 1.0; + + @MinNumber(0) + @MaxNumber(1) + @Desc("How crumbled the ruin is (0 intact, 1 mostly gone). Blocks whose value-noise falls below this threshold are deleted, so the shape reads as collapsed. Structural integrity is preserved: the bottom row and core legs are never eroded away.") + private double erosion = 0.25; + + @MinNumber(0) + @MaxNumber(8) + @Desc("The scale of the erosion noise. Higher values knock out small speckled holes; lower values carve larger missing chunks out of the silhouette.") + private double erosionScale = 1.5; + + @MinNumber(0) + @MaxNumber(1) + @Desc("How far the structure extends below the surface as a fraction of its height (0 sits fully on top, 1 sinks it a full height into the ground). The buried rows live at negative y so the ruin reads as settled and partly swallowed by the terrain.") + private double buriedFraction = 0.2; + + @ArrayType(min = 1, type = IrisRuinDecorator.class) + @Desc("Accent decorators applied after the ruin is built and eroded (moss carpets and lanterns on broken tops, vines and lichen on the faces, rubble and growth scattered around the base).") + private KList accents = new KList<>(); + + public KList getVariantObjects(IrisData data) { + return variantCache.aquire(() -> { + KList baked = new KList<>(); + int count = Math.max(1, variants); + + for (int i = 0; i < count; i++) { + IrisObject object = RuinGenerator.generate(this, i, new RNG(seed + (i * 7919L)), data); + if (object == null || object.getBlocks().isEmpty()) { + continue; + } + object.setLoadKey("procedural/" + name + "#" + i); + object.setLoader(data); + baked.add(object); + } + + return baked; + }); + } + + public IrisObject getVariantObject(IrisData data, RNG rng) { + KList baked = getVariantObjects(data); + if (baked == null || baked.isEmpty()) { + return null; + } + return baked.get(rng.i(baked.size())); + } + + public IrisObjectPlacement asPlacement() { + IrisObjectPlacement placement = new IrisObjectPlacement(); + placement.setMode(mode); + placement.setRotation(rotation); + placement.setClamp(clamp); + placement.setCarvingSupport(carvingSupport); + placement.setUnderwater(underwater); + placement.setTranslate(translate); + placement.setStiltSettings(stiltSettings); + placement.setVacuumSettings(vacuumSettings); + placement.setChance(chance); + placement.setDensity(density); + return placement; + } + + public boolean isPlausible() { + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecorator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecorator.java new file mode 100644 index 000000000..959bb559e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecorator.java @@ -0,0 +1,58 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("ruin-decorator") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("An accent block applied to a generated ruin after it is built and eroded, such as moss carpets on broken tops, vines on the faces, or rubble scattered around the base.") +@Data +public class IrisRuinDecorator { + @Desc("Where on the ruin this accent is placed. TOP sits on the highest block of each column, SURFACE clings to air-facing vertical faces, BASE_SCATTER rings the ground footprint around the base.") + private IrisRuinDecoratorTarget target = IrisRuinDecoratorTarget.TOP; + + @Required + @Desc("The block id to place, e.g. minecraft:moss_carpet or minecraft:vine. Ignored when palette is set.") + @RegistryListBlockType + private String block = ""; + + @Desc("A noise-driven palette for this decorator. When set this overrides the single block, letting the accent mix blocks by noise. Resolved through IrisProceduralBlocks.resolve so the palette wins over the string.") + private IrisMaterialPalette palette = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance (0-1) per candidate position for this decorator to actually place. 1 covers every candidate, lower values leave sparse, patchy accents.") + private double chance = 0.4; + + @MinNumber(1) + @Desc("For the BASE_SCATTER target, how many blocks beyond the ruin footprint the scatter ring extends outward in each direction. Higher values fling rubble and growth further from the structure.") + private int scatterRadius = 2; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecoratorTarget.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecoratorTarget.java new file mode 100644 index 000000000..04c373290 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecoratorTarget.java @@ -0,0 +1,31 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Where a ruin accent block is placed after the ruin has been built and eroded.") +public enum IrisRuinDecoratorTarget { + @Desc("On top of the highest surviving block in each column (moss carpets, snow, sculk, lanterns on broken tops).") + TOP, + @Desc("On air-facing vertical sides of any surviving block (vines, glow lichen, climbing growth on the ruin faces).") + SURFACE, + @Desc("Scattered across the ground footprint around the y=0 base ring, wider than the structure itself (loose rubble, mushrooms, grass tufts ringing the ruin).") + BASE_SCATTER +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRuinForm.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRuinForm.java new file mode 100644 index 000000000..313ece19a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRuinForm.java @@ -0,0 +1,35 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("The overall silhouette of a procedural ruin. Each form rasterizes a different crumbling man-made shape from the same size, weathering, and erosion settings.") +public enum IrisRuinForm { + @Desc("A broken vertical column. A square trunk (widthMin..widthMax footprint) rises to a randomized height then snaps off with a jagged crown, leaving a toppled stub. Use for ancient pillars and obelisk stumps.") + PILLAR, + @Desc("A standing wall segment. A flat slab spanning lengthMin..lengthMax long and heightMin..heightMax tall, thickness widthMin..widthMax, punched through with noise-driven gaps and a ragged top edge. Use for collapsed building sides and fortifications.") + WALL, + @Desc("A freestanding arch. Two legs spaced by lengthMin..lengthMax joined at the top by a rasterized semicircular span (the lintel), so the structure literally arches. Use for gateways, aqueduct bays, and ruined doorways.") + ARCH, + @Desc("A flat foundation patch. A single (or few) block-thick slab covering a widthMin..lengthMax footprint at ground level, heavily eroded into an irregular outline. Use for floor remnants, plazas, and building foundations.") + FLOOR_SLAB, + @Desc("A low scattered debris pile. A noise blob of partial blocks mounded near the ground, tallest at the center and thinning toward the edges. Use for collapsed-structure rubble and scree.") + RUBBLE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java new file mode 100644 index 000000000..0aee404c9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java @@ -0,0 +1,85 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("shaped-style") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("This represents a generator with a min and max height") +@Data +public class IrisShapedGeneratorStyle { + @Required + @Desc("The generator id") + private IrisGeneratorStyle generator = new IrisGeneratorStyle(NoiseStyle.IRIS); + + @Required + @MinNumber(-2032) + @MaxNumber(2032) + @Desc("The min block value") + private int min = 0; + + @Required + @MinNumber(-2032) + @MaxNumber(2032) + @Desc("The max block value") + private int max = 0; + + public IrisShapedGeneratorStyle(NoiseStyle style, int min, int max) { + this(style); + this.min = min; + this.max = max; + } + + public IrisShapedGeneratorStyle(NoiseStyle style) { + this.generator = new IrisGeneratorStyle(style); + } + + public double get(RNG rng, IrisData data, double... dim) { + return generator.create(rng, data).fitDouble(min, max, dim); + } + + public double get(RNG rng, IrisData data, double x, double z) { + return generator.create(rng, data).fitDouble(min, max, x, z); + } + + public double get(RNG rng, IrisData data, double x, double y, double z) { + return generator.create(rng, data).fitDouble(min, max, x, y, z); + } + + public boolean isFlat() { + return min == max || getGenerator().isFlat(); + } + + public int getMid() { + return (getMax() + getMin()) / 2; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisSlopeClip.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSlopeClip.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/IrisSlopeClip.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisSlopeClip.java index bd7c301be..3cebbe846 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisSlopeClip.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSlopeClip.java @@ -16,12 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MaxNumber; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -31,7 +31,7 @@ import lombok.experimental.Accessors; @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor -@Desc("Translate objects") +@Desc("Limits placement to a minimum and maximum terrain slope") @Data public class IrisSlopeClip { @MinNumber(0) diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisSpawnGroup.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawnGroup.java similarity index 92% rename from core/src/main/java/com/volmit/iris/engine/object/IrisSpawnGroup.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisSpawnGroup.java index 2b0ff8b69..bb8dc4dab 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisSpawnGroup.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawnGroup.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("Terrain modes are used to decide the generator type currently used") public enum IrisSpawnGroup { diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisSpawner.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/IrisSpawner.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java index c3343a2a0..1c4a0b6a8 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisSpawner.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java @@ -16,21 +16,19 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.spi.PlatformWorld; +import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import org.bukkit.World; @EqualsAndHashCode(callSuper = true) @Accessors(chain = true) @@ -50,16 +48,13 @@ public class IrisSpawner extends IrisRegistrant { @Desc("The entity spawns to add initially. EXECUTES PER CHUNK!") private KList initialSpawns = new KList<>(); - @Desc("The energy multiplier when calculating spawn energy usage") - private double energyMultiplier = 1; - @Desc("This spawner will not spawn in a given chunk if that chunk has more than the defined amount of living entities.") private int maxEntitiesPerChunk = 1; @Desc("The block of 24 hour time to contain this spawn in.") private IrisTimeBlock timeBlock = new IrisTimeBlock(); - @Desc("The block of 24 hour time to contain this spawn in.") + @Desc("The weather condition required for this spawner to fire.") private IrisWeather weather = IrisWeather.ANY; @Desc("The maximum rate this spawner can fire") @@ -92,15 +87,16 @@ public class IrisSpawner extends IrisRegistrant { }; } - public boolean isValid(World world) { + public boolean isValid(PlatformWorld world) { return timeBlock.isWithin(world) && weather.is(world); } public boolean canSpawn(Engine engine) { - if (!isValid(engine.getWorld().realWorld())) + PlatformWorld world = engine.getWorld().platformWorld(); + if (world == null || !isValid(world)) return false; - var rate = getMaximumRate(); + IrisRate rate = getMaximumRate(); return rate.isInfinite() || engine.getEngineData().getCooldown(this).canSpawn(rate); } @@ -108,7 +104,7 @@ public class IrisSpawner extends IrisRegistrant { if (!canSpawn(engine)) return false; - var rate = getMaximumRatePerChunk(); + IrisRate rate = getMaximumRatePerChunk(); return rate.isInfinite() || engine.getEngineData().getChunk(x, z).getCooldown(this).canSpawn(rate); } @@ -136,9 +132,4 @@ public class IrisSpawner extends IrisRegistrant { public String getTypeName() { return "Spawner"; } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } } diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisSpawnerCooldowns.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawnerCooldowns.java similarity index 87% rename from core/src/main/java/com/volmit/iris/engine/object/IrisSpawnerCooldowns.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisSpawnerCooldowns.java index 05447eb74..a784d0889 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisSpawnerCooldowns.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawnerCooldowns.java @@ -1,7 +1,7 @@ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KMap; import lombok.EqualsAndHashCode; import lombok.NonNull; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStiltSettings.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStiltSettings.java new file mode 100644 index 000000000..518cbe945 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStiltSettings.java @@ -0,0 +1,46 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("stilt-settings") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Defines stilting behaviour.") +@Data +public class IrisStiltSettings { + @MinNumber(0) + @MaxNumber(64) + @Desc("Defines the maximum amount of blocks the object stilts verticially before overstilting and randomRange.") + private int yMax; + @MinNumber(0) + @MaxNumber(64) + @Desc("Defines the upper boundary for additional blocks after overstilting and/or maxStiltRange.") + private int yRand; + @MaxNumber(64) + @MinNumber(0) + @Desc("If the place mode is set to stilt, you can over-stilt it even further into the ground. Especially useful when using fast stilt due to inaccuracies.") + private int overStilt; + @Desc("If defined, stilting will be done using this block palette rather than the last layer of the object.") + private IrisMaterialPalette palette; + @MinNumber(1) + @MaxNumber(256) + @Desc("For ORGANIC_STILT / CEILING_HANG: the maximum number of blocks to scan toward the cave floor (or ceiling) looking for solid ground before giving up.") + private int organicMaxScan = 48; + @MinNumber(0) + @MaxNumber(32) + @Desc("For ORGANIC_STILT / CEILING_HANG: the maximum number of blocks each column's stilt is randomly shortened by, giving the underside a ragged organic edge instead of a flat disc.") + private int organicJitter = 3; + @MinNumber(0) + @MaxNumber(1) + @Desc("For ORGANIC_STILT / CEILING_HANG: in the deepest fraction of each stilt column, blocks are randomly skipped to break the tip up. Higher = more broken and scratchy. 0 disables.") + private double organicScratch = 0.55; + +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java new file mode 100644 index 000000000..74113d3ce --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java @@ -0,0 +1,138 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A jigsaw structure. Starting from a pool, the assembler places a start piece and recursively attaches more pieces through matching connectors until it reaches the max depth or size. Pieces are Iris objects, so they carry the full object placement and block-control feature set.") +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisStructure extends IrisRegistrant { + @Required + @RegistryListResource(IrisJigsawPool.class) + @Desc("The pool the assembler draws the start piece from.") + private String startPool = ""; + + @MinNumber(1) + @MaxNumber(30) + @Desc("The maximum jigsaw recursion depth. Larger values allow larger structures (e.g. villages) at the cost of generation time.") + private int maxDepth = 7; + + @MinNumber(1) + @MaxNumber(32) + @Desc("The maximum radius of the structure in chunks from its start piece. Acts as a hard bound on assembly size and collision checks.") + private int maxSizeChunks = 8; + + @Desc("Assembly topology. SPATIAL_JIGSAW preserves the existing freeform connector graph; PLANAR_JIGSAW declares a cell-aligned planar graph.") + private IrisJigsawMode mode = IrisJigsawMode.SPATIAL_JIGSAW; + + @Desc("Target compatibility contract for this structure. IRIS_EXTENDED permits Iris-only metadata; VANILLA_PORTABLE requires a graph that can be represented by vanilla jigsaw resources.") + private IrisJigsawCompatibility compatibility = IrisJigsawCompatibility.IRIS_EXTENDED; + + @Desc("How unresolved optional connector branches affect assembly. FAIL_ASSEMBLY preserves strict Iris behavior; TERMINATE_BRANCH matches vanilla jigsaw branch termination.") + private IrisJigsawBranchFailurePolicy branchFailurePolicy = IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY; + + @Desc("Default Studio cell dimensions. Legacy planar structures use this value for every workcell when planarWorkcells is empty.") + private IrisPosition cellSize = new IrisPosition(15, 15, 15); + + @Desc("Optional author-facing name for the single spatial Jigsaw Studio workcell. Spatial is shown when this is blank.") + private String spatialWorkcellDisplayName = ""; + + @ArrayType(type = IrisJigsawWorkcell.class) + @Desc("The six persistent planar Studio workcells. Leave empty only for the legacy cellSize fallback.") + private KList planarWorkcells = new KList<>(); + + @ArrayType(type = IrisJigsawThemeSet.class, min = 1) + @Desc("Weighted themes available to an assembly. Leave empty for one implicit unthemed assembly path.") + private KList themeSets = new KList<>(); + + @Desc("Whether every open connector must close with a real terminal piece through a mandatory direct fallback.") + private boolean requireCaps = false; + + @Desc("The place mode used when stamping each piece object into the world.") + private ObjectPlaceMode placeMode = ObjectPlaceMode.STRUCTURE_PIECE; + + @ArrayType(min = 1, type = IrisObjectReplace.class) + @Desc("Find-and-replace block edits applied to every piece of this structure, using the exact same syntax as an object's 'edit'. For example, replace stone bricks with obsidian.") + private KList edit = new KList<>(); + + @ArrayType(type = String.class, min = 1) + @RegistryListResource(IrisLootTable.class) + @Desc("Loot tables applied to containers placed by this structure's pieces.") + private KList loot = new KList<>(); + + @RegistryListVanillaStructure + @Desc("If this structure was generated by importing a vanilla or datapack structure, this is that structure's key (provenance). Empty for hand-authored structures.") + private String vanillaSource = ""; + + public IrisJigsawMode resolvedMode() { + return mode == null ? IrisJigsawMode.SPATIAL_JIGSAW : mode; + } + + public IrisJigsawCompatibility resolvedCompatibility() { + return compatibility == null ? IrisJigsawCompatibility.IRIS_EXTENDED : compatibility; + } + + public IrisJigsawBranchFailurePolicy resolvedBranchFailurePolicy() { + return branchFailurePolicy == null + ? IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY + : branchFailurePolicy; + } + + public IrisObjectPlacement createLootPlacement(String objectKey) { + IrisObjectPlacement placement = new IrisObjectPlacement(); + placement.getPlace().add(objectKey); + placement.setOverrideGlobalLoot(false); + if (loot == null) { + return placement; + } + for (String lootTable : loot) { + if (lootTable != null && !lootTable.isBlank()) { + placement.getLoot().add(new IrisObjectLoot().setName(lootTable).setWeight(1)); + } + } + return placement; + } + + @Override + public String getFolderName() { + return "structures"; + } + + @Override + public String getTypeName() { + return "Structure"; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructureAnchorMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureAnchorMode.java new file mode 100644 index 000000000..3956b5b34 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureAnchorMode.java @@ -0,0 +1,34 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Selects the vertical environment used to anchor an Iris structure placement.") +public enum IrisStructureAnchorMode { + @Desc("Preserve the historical underground boolean behavior.") + LEGACY, + + @Desc("Anchor to the terrain surface and use minHeight/maxHeight as a surface gate.") + SURFACE, + + @Desc("Choose a deterministic Y inside the minHeight/maxHeight band.") + HEIGHT_BAND, + + @Desc("Anchor in carved space with solid support below.") + CAVE_FLOOR, + + @Desc("Anchor in carved space with solid support above.") + CAVE_CEILING, + + @Desc("Anchor in carved space without nearby floor or ceiling support.") + CAVE_CENTER, + + @Desc("Anchor at any carved-space position.") + CAVE_ANY; + + public boolean isCave() { + return switch (this) { + case CAVE_FLOOR, CAVE_CEILING, CAVE_CENTER, CAVE_ANY -> true; + default -> false; + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructureCarveShape.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureCarveShape.java new file mode 100644 index 000000000..127793924 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureCarveShape.java @@ -0,0 +1,36 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls the shape of the expanded terrain carve around an overbored structure.") +public enum IrisStructureCarveShape { + @Desc("Carves the full expanded rectangular volume with straight walls, ceiling, and floor.") + BOX, + + @Desc("Carves a smooth rounded volume around the structure without erosion noise.") + ROUNDED, + + @Desc("Carves a rounded volume whose outer boundary is eroded by deterministic noise.") + ERODED; + + public double maximumCeilingScale() { + return maximumCeilingScale(1D); + } + + public double maximumCeilingScale(double erosionStrength) { + double strength = Double.isFinite(erosionStrength) + ? Math.max(0D, Math.min(1D, erosionStrength)) : 1D; + return switch (this) { + case BOX, ROUNDED -> 1D; + case ERODED -> 1D + 0.8D * strength; + }; + } + + public int maximumCeilingExtension(int configuredHeight) { + return maximumCeilingExtension(configuredHeight, 1D); + } + + public int maximumCeilingExtension(int configuredHeight, double erosionStrength) { + return (int) Math.ceil(Math.max(0, configuredHeight) * maximumCeilingScale(erosionStrength)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java new file mode 100644 index 000000000..eb456f9dc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java @@ -0,0 +1,150 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.RegistryListStructure; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Attaches structures to a biome, region or dimension and controls where and how often they generate. This is independent of a structure's own native generation, so you can add custom placements in tandem with native generation or instead of it.") +@Data +public class IrisStructurePlacement { + @ArrayType(type = String.class, min = 1) + @RegistryListStructure + @Desc("Editable Iris assembly resources offered by this placement. Leave empty when nativeStructures supplies the source.") + private KList structures = new KList<>(); + + @ArrayType(type = IrisNativeStructure.class, min = 1) + @Desc("Live registered vanilla, datapack, and modded structures offered by this placement. Minecraft runs their native generator directly, independent of normal biome or dimension eligibility.") + private KList nativeStructures = new KList<>(); + + @Desc("Stable authored identity for this placement. Set this when multiple placements use the same structure and settings. When empty, Iris derives identity from the placement content so list reordering does not move generated structures.") + private String placementId = ""; + + @Desc("Controls native replacement for this placement. REPLACE_SOURCE suppresses each selected Iris structure's vanillaSource or registered native source, and is honored only for dimension-level placements. NONE leaves native generation untouched.") + private NativeStructureSuppression nativeSuppression = NativeStructureSuppression.NONE; + + @Desc("How start positions are scattered.") + private StructureDistribution distribution = StructureDistribution.RANDOM_SPREAD; + + @MinNumber(1) + @MaxNumber(4096) + @Desc("RANDOM_SPREAD only: the grid cell size in chunks. One placement attempt per cell. Larger = rarer.") + private int spacing = 32; + + @MinNumber(0) + @Desc("RANDOM_SPREAD only: the minimum chunk separation between placements within the grid. Must be smaller than spacing.") + private int separation = 8; + + @Desc("RANDOM_SPREAD only: a salt mixed into the placement RNG so different structures using the same spacing do not stack.") + private int salt = 165745296; + + @MinNumber(0) + @MaxNumber(1) + @Desc("DENSITY only: the per-chunk probability (0-1) of a placement starting in a chunk.") + private double density = 0.02; + + @MinNumber(1) + @Desc("CONCENTRIC_RINGS only: the number of placements distributed across the rings.") + private int ringCount = 128; + + @MinNumber(1) + @Desc("CONCENTRIC_RINGS only: the ring spacing in chunks.") + private int ringDistance = 32; + + @MinNumber(1) + @Desc("CONCENTRIC_RINGS only: how many placements share each ring before moving outward.") + private int ringSpread = 3; + + @Desc("Minimum world Y. SURFACE uses it as a terrain-height gate; HEIGHT_BAND and cave anchors use it as the lower candidate bound. LEGACY follows underground.") + private int minHeight = -2032; + + @Desc("Maximum world Y. SURFACE uses it as a terrain-height gate; HEIGHT_BAND and cave anchors use it as the upper candidate bound. LEGACY follows underground.") + private int maxHeight = 2032; + + @Desc("LEGACY anchor only: if true, choose a deterministic world Y inside [minHeight, maxHeight]; otherwise gate the terrain surface. Explicit anchor modes override this field.") + private boolean underground = false; + + @Desc("Vertical anchor policy. LEGACY preserves the underground boolean. Cave modes search existing carved-space data and implicitly behave as underground placements.") + private IrisStructureAnchorMode anchor = IrisStructureAnchorMode.LEGACY; + + @RegistryListResource(IrisBiome.class) + @ArrayType(type = String.class, min = 1) + @Desc("Optional cave-biome allowlist for cave anchor modes. Empty accepts the cave biome resolved at the selected anchor. Entries match by load key, including prefix matches.") + private KList caveBiomes = new KList<>(); + + @MinNumber(1) + @MaxNumber(64) + @Desc("Cave anchor modes only: deterministic X/Z columns tested inside the start chunk before the placement is skipped.") + private int caveAnchorAttempts = 8; + + @MinNumber(1) + @MaxNumber(16) + @Desc("Cave anchor modes only: vertical scan step in blocks. One is exact; larger values trade precision for speed.") + private int caveAnchorScanStep = 1; + + @MinNumber(1) + @MaxNumber(64) + @Desc("Cave anchor modes only: minimum contiguous carved-space height required at the selected anchor.") + private int caveMinimumClearance = 3; + + @Desc("Terrain integration for this placement. The editable structures backend supports SOURCE, PRESERVE, BORE, and FORCE_CARVE. The nativeStructures backend supports every terrain mode.") + private IrisStructureTerrain terrain = new IrisStructureTerrain(); + + @Desc("Optional foundation columns placed beneath the assembled structure's occupied bottom cells. Columns pass through air and fluids until they reach solid ground, up to maxDepth.") + private IrisStructureStiltSettings stilt = null; + + @Desc("If false, the placement is skipped when its selected surface or cave column is submerged.") + private boolean underwater = false; + + public IrisStructureTerrain resolvedTerrain() { + return terrain == null ? new IrisStructureTerrain() : terrain; + } + + public boolean hasIrisStructures() { + return structures != null && !structures.isEmpty(); + } + + public boolean hasNativeStructures() { + return nativeStructures != null && !nativeStructures.isEmpty(); + } + + public IrisStructureAnchorMode resolvedAnchor() { + if (anchor == null || anchor == IrisStructureAnchorMode.LEGACY) { + return underground ? IrisStructureAnchorMode.HEIGHT_BAND : IrisStructureAnchorMode.SURFACE; + } + return anchor; + } + + public boolean isAnchoredUnderground() { + IrisStructureAnchorMode activeAnchor = resolvedAnchor(); + return activeAnchor == IrisStructureAnchorMode.HEIGHT_BAND || activeAnchor.isCave(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructureSetFrequencyOverride.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureSetFrequencyOverride.java new file mode 100644 index 000000000..d73963568 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureSetFrequencyOverride.java @@ -0,0 +1,44 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructureSet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("An exact registered structure-set frequency multiplier. Iris retains the set's structures, weights, biome eligibility, placement algorithm, salt, exclusion zone, starts, processors, mobs, loot, and locate behavior while scaling only its native placement density.") +@Data +public class IrisStructureSetFrequencyOverride { + @RegistryListVanillaStructureSet + @Desc("Exact registered structure-set key, for example 'minecraft:nether_complexes'. Structure keys such as 'minecraft:fortress' are not valid here.") + private String structureSet = ""; + + @MinNumber(0.01) + @MaxNumber(16) + @Desc("Requested placement-density multiplier. Random-spread sets scale their placement frequency first and then their integer chunk spacing; integer spacing and separation constraints can make the realized increase slightly lower or higher. The default 1 leaves the registered placement unchanged.") + private double multiplier = 1D; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructureStiltSettings.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureStiltSettings.java new file mode 100644 index 000000000..bc0676b13 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureStiltSettings.java @@ -0,0 +1,32 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Defines foundation columns placed beneath a structure.") +@Data +public class IrisStructureStiltSettings { + @MinNumber(1) + @MaxNumber(4064) + @Desc("Maximum number of blocks each foundation column may descend while searching for solid ground.") + private int maxDepth = 64; + + @Desc("Block palette used for foundation columns.") + private IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:cobblestone"); + + @MinNumber(1) + @MaxNumber(64) + @Desc("Horizontal spacing between support columns. One fills every eligible foundation column; larger values create sparse stilts.") + private int spacing = 1; + + @Desc("For Iris-authored structures, whether solid partial blocks such as slabs, stairs, and walls may seed foundation columns instead of requiring a fully occluding base block.") + private boolean supportNonOccluding = false; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrain.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrain.java new file mode 100644 index 000000000..bddedbb26 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrain.java @@ -0,0 +1,104 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Data +@Desc("Terrain integration applied after a structure graph is assembled and before its pieces are placed.") +public class IrisStructureTerrain { + private static final double AUTO_LOBE_FREQUENCY_RATIO = 0.3D; + private static final double DEFAULT_EROSION_STRENGTH = 0.8D; + private static final double DEFAULT_EROSION_FREQUENCY = 0.07D; + private static final double DEFAULT_LOBE_STRENGTH = 0.85D; + private static final double MIN_EROSION_FREQUENCY = 0.001D; + private static final double MAX_EROSION_FREQUENCY = 1D; + private static final double MAX_LOBE_FREQUENCY = 1D; + + @Desc("Terrain operation. SOURCE applies the registered native structure's authored terrain adaptation and is a no-op for editable Iris structures. PRESERVE disables terrain integration. VACUUM raises terrain from processed rigid-template foundations at or below each authored ground plane with a 12-block falloff without lowering existing ground. BORE and FORCE_CARVE clear the requested envelope, while ENCASE fills it before placement so native shells are not lost to pre-carved air.") + private IrisStructureTerrainMode mode = IrisStructureTerrainMode.SOURCE; + + @MinNumber(0) + @MaxNumber(128) + @Desc("Horizontal clearance around the assembled pieces for BORE, FORCE_CARVE, and ENCASE. VACUUM uses its fixed 12-block terrain falloff.") + private int horizontalPadding = 0; + + @MinNumber(0) + @MaxNumber(128) + @Desc("Air clearance above the assembled pieces.") + private int ceilingPadding = 0; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Terrain cleared below the assembled pieces. Zero preserves their supporting floor.") + private int floorPadding = 0; + + @Desc("Shape used by FORCE_CARVE.") + private IrisStructureCarveShape shape = IrisStructureCarveShape.BOX; + + @Desc("Block palette used by ENCASE. When unset, the Overworld uses stone or deepslate, the Nether uses netherrack, and the End uses end stone.") + private IrisMaterialPalette encasePalette = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Boundary erosion strength for ERODED force carving.") + private double erosionStrength = DEFAULT_EROSION_STRENGTH; + + @MinNumber(MIN_EROSION_FREQUENCY) + @MaxNumber(MAX_EROSION_FREQUENCY) + @Desc("Boundary noise frequency for ERODED force carving.") + private double erosionFrequency = DEFAULT_EROSION_FREQUENCY; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Low frequency lobe wavelength for ERODED force carving. Zero derives it from the erosion frequency.") + private double lobeFrequency = 0D; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Fraction of the horizontal padding the lobes may remove for ERODED force carving. Zero keeps a uniform boundary.") + private double lobeStrength = DEFAULT_LOBE_STRENGTH; + + public IrisStructureTerrainMode resolvedMode() { + return mode == null ? IrisStructureTerrainMode.SOURCE : mode; + } + + public IrisStructureCarveShape resolvedShape() { + return shape == null ? IrisStructureCarveShape.BOX : shape; + } + + public double resolvedErosionStrength() { + if (!Double.isFinite(erosionStrength)) { + return DEFAULT_EROSION_STRENGTH; + } + return Math.max(0D, Math.min(1D, erosionStrength)); + } + + public double resolvedErosionFrequency() { + if (!Double.isFinite(erosionFrequency)) { + return DEFAULT_EROSION_FREQUENCY; + } + return Math.max(MIN_EROSION_FREQUENCY, Math.min(MAX_EROSION_FREQUENCY, erosionFrequency)); + } + + public double resolvedLobeFrequency() { + if (!Double.isFinite(lobeFrequency) || lobeFrequency <= 0D) { + return resolvedErosionFrequency() * AUTO_LOBE_FREQUENCY_RATIO; + } + return Math.min(MAX_LOBE_FREQUENCY, lobeFrequency); + } + + public double resolvedLobeStrength() { + if (!Double.isFinite(lobeStrength)) { + return DEFAULT_LOBE_STRENGTH; + } + return Math.max(0D, Math.min(1D, lobeStrength)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrainMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrainMode.java new file mode 100644 index 000000000..1cd27a61e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrainMode.java @@ -0,0 +1,24 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls how terrain is integrated with a structure.") +public enum IrisStructureTerrainMode { + @Desc("Replays the registered native structure's own authored terrain adaptation (beard surface fitting, BURY/ENCAPSULATE fill, legacy template-air clearing). Does nothing for editable Iris structures, which have no source adaptation to replay.") + SOURCE, + + @Desc("Disables all terrain integration. Pieces are placed into completely unmodified terrain and may end up embedded in solid rock or floating over dips.") + PRESERVE, + + @Desc("Clears one straight axis-aligned box around the assembled pieces, expanded by horizontalPadding, ceilingPadding, and floorPadding. Ignores the carve shape and erosion settings.") + BORE, + + @Desc("Clears the padded envelope using the configured carve shape (BOX, ROUNDED, or noise-eroded ERODED) so the cavity hugs the assembled pieces instead of a straight box.") + FORCE_CARVE, + + @Desc("Raises surface terrain from processed solid rigid-template foundations at or below each authored ground plane with a 12-block falloff, even when the registered structure has no terrain adaptation. Existing higher terrain and authored air remain untouched.") + VACUUM, + + @Desc("Fills the padded piece volume with solid blocks before any piece is placed so shells, walls, and floors land in solid ground instead of pre-carved air. Only air and liquid cells are filled; existing terrain and structures are never overwritten. Native pieces then carve their own interiors.") + ENCASE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructureYBand.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureYBand.java new file mode 100644 index 000000000..7163332ba --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureYBand.java @@ -0,0 +1,34 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Data +@Desc("An absolute world Y band a native structure is relocated into. Reversed bounds are normalized.") +public class IrisStructureYBand { + @MinNumber(-4064) + @MaxNumber(4064) + @Desc("Lowest absolute world Y of the band.") + private int min = 0; + + @MinNumber(-4064) + @MaxNumber(4064) + @Desc("Highest absolute world Y of the band.") + private int max = 0; + + public int resolvedMin() { + return Math.min(min, max); + } + + public int resolvedMax() { + return Math.max(min, max); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisStyledRange.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStyledRange.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/IrisStyledRange.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisStyledRange.java index 4d2d30dac..daa1bd76c 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisStyledRange.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStyledRange.java @@ -16,15 +16,15 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.stream.ProceduralStream; -import com.volmit.iris.util.stream.interpolation.Interpolated; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisSurface.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSurface.java similarity index 96% rename from core/src/main/java/com/volmit/iris/engine/object/IrisSurface.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisSurface.java index e8a2e2e3c..0b073f40a 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisSurface.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSurface.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.data.Waterlogged; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceSupport.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceSupport.java new file mode 100644 index 000000000..fbb0c086d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceSupport.java @@ -0,0 +1,201 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.util.common.math.IrisBlockVector; + +import java.util.Arrays; +import java.util.List; + +/** + * Hard gate for surface-anchored object and tree placement: every column under the object's lowest solid + * layer, plus a buffer ring, must be solid un-carved ground for the required depth. Anything else floats + * over or bridges a carved hole, so the placement is skipped. + */ +public final class IrisSurfaceSupport { + private static final int MAX_STENCIL_CELLS = 1 << 22; + private static final ThreadLocal STENCIL = ThreadLocal.withInitial(Stencil::new); + + private IrisSurfaceSupport() { + } + + public static boolean isUnsupported(IObjectPlacer placer, IrisData data, int x, int z, + IrisObjectTranslate translate, IrisObjectRotation rotation, + int spinX, int spinY, int spinZ, + List supportOffsets, int buffer, int minSolidDepth) { + Stencil stencil = STENCIL.get(); + if (!stencil.build(supportOffsets, translate, rotation, spinX, spinY, spinZ, Math.max(0, buffer))) { + return true; + } + return stencil.anyColumnUnsupported(placer, data, x, z, Math.max(1, minSolidDepth)); + } + + private static IrisBlockVector transform(IrisBlockVector offset, IrisObjectTranslate translate, + IrisObjectRotation rotation, int spinX, int spinY, int spinZ) { + IrisBlockVector transformed = offset.clone(); + if (rotation != null) { + transformed = rotation.rotate(transformed, spinX, spinY, spinZ); + } + if (translate == null) { + return transformed; + } + return rotation == null + ? translate.translate(transformed) + : translate.translate(transformed, rotation, spinX, spinY, spinZ); + } + + /** + * Rasterized footprint reused per thread. Overlapping support offsets collapse into one cell here so + * each unique column is sampled once no matter how large the buffer is. + */ + private static final class Stencil { + private int[] offsetX = new int[64]; + private int[] offsetZ = new int[64]; + private boolean[] cells = new boolean[0]; + private boolean[] dilateScratch = new boolean[0]; + private int minX; + private int minZ; + private int width; + private int depth; + private int centerX; + private int centerZ; + + private boolean build(List supportOffsets, IrisObjectTranslate translate, + IrisObjectRotation rotation, int spinX, int spinY, int spinZ, int buffer) { + boolean originOnly = supportOffsets == null || supportOffsets.isEmpty(); + int count = originOnly ? 1 : supportOffsets.size(); + if (offsetX.length < count) { + offsetX = new int[count]; + offsetZ = new int[count]; + } + + int lowX = Integer.MAX_VALUE; + int highX = Integer.MIN_VALUE; + int lowZ = Integer.MAX_VALUE; + int highZ = Integer.MIN_VALUE; + for (int i = 0; i < count; i++) { + IrisBlockVector source = originOnly ? new IrisBlockVector(0, 0, 0) : supportOffsets.get(i); + IrisBlockVector transformed = transform(source, translate, rotation, spinX, spinY, spinZ); + int transformedX = transformed.getBlockX(); + int transformedZ = transformed.getBlockZ(); + offsetX[i] = transformedX; + offsetZ[i] = transformedZ; + lowX = Math.min(lowX, transformedX); + highX = Math.max(highX, transformedX); + lowZ = Math.min(lowZ, transformedZ); + highZ = Math.max(highZ, transformedZ); + } + + minX = lowX - buffer; + minZ = lowZ - buffer; + width = (highX - lowX + 1) + (buffer * 2); + depth = (highZ - lowZ + 1) + (buffer * 2); + long area = (long) width * (long) depth; + if (area <= 0L || area > MAX_STENCIL_CELLS) { + return false; + } + + int size = (int) area; + if (cells.length < size) { + cells = new boolean[size]; + dilateScratch = new boolean[size]; + } + Arrays.fill(cells, 0, size, false); + for (int i = 0; i < count; i++) { + cells[((offsetX[i] - minX) * depth) + (offsetZ[i] - minZ)] = true; + } + if (buffer > 0) { + dilate(buffer, size); + } + + int anchorX = -minX; + int anchorZ = -minZ; + centerX = anchorX >= 0 && anchorX < width ? anchorX : width / 2; + centerZ = anchorZ >= 0 && anchorZ < depth ? anchorZ : depth / 2; + return true; + } + + /** Chebyshev dilation, run as two separable passes so a wide buffer stays linear in footprint size. */ + private void dilate(int buffer, int size) { + Arrays.fill(dilateScratch, 0, size, false); + for (int sourceX = 0; sourceX < width; sourceX++) { + int base = sourceX * depth; + int fromX = Math.max(0, sourceX - buffer); + int toX = Math.min(width - 1, sourceX + buffer); + for (int sourceZ = 0; sourceZ < depth; sourceZ++) { + if (!cells[base + sourceZ]) { + continue; + } + for (int targetX = fromX; targetX <= toX; targetX++) { + dilateScratch[(targetX * depth) + sourceZ] = true; + } + } + } + + Arrays.fill(cells, 0, size, false); + for (int sourceX = 0; sourceX < width; sourceX++) { + int base = sourceX * depth; + for (int sourceZ = 0; sourceZ < depth; sourceZ++) { + if (!dilateScratch[base + sourceZ]) { + continue; + } + int fromZ = Math.max(0, sourceZ - buffer); + int toZ = Math.min(depth - 1, sourceZ + buffer); + for (int targetZ = fromZ; targetZ <= toZ; targetZ++) { + cells[base + targetZ] = true; + } + } + } + } + + /** Centre-outward so the common failure, a hole right under the anchor, costs one column. */ + private boolean anyColumnUnsupported(IObjectPlacer placer, IrisData data, int x, int z, int minSolidDepth) { + int maxRing = Math.max( + Math.max(centerX, width - 1 - centerX), + Math.max(centerZ, depth - 1 - centerZ)); + for (int ring = 0; ring <= maxRing; ring++) { + if (ring == 0) { + if (isColumnUnsupported(placer, data, x, z, centerX, centerZ, minSolidDepth)) { + return true; + } + continue; + } + + int lowX = centerX - ring; + int highX = centerX + ring; + int lowZ = centerZ - ring; + int highZ = centerZ + ring; + for (int sx = lowX; sx <= highX; sx++) { + if (isColumnUnsupported(placer, data, x, z, sx, lowZ, minSolidDepth) + || isColumnUnsupported(placer, data, x, z, sx, highZ, minSolidDepth)) { + return true; + } + } + for (int sz = lowZ + 1; sz < highZ; sz++) { + if (isColumnUnsupported(placer, data, x, z, lowX, sz, minSolidDepth) + || isColumnUnsupported(placer, data, x, z, highX, sz, minSolidDepth)) { + return true; + } + } + } + return false; + } + + private boolean isColumnUnsupported(IObjectPlacer placer, IrisData data, int x, int z, + int stencilX, int stencilZ, int minSolidDepth) { + if (stencilX < 0 || stencilX >= width || stencilZ < 0 || stencilZ >= depth + || !cells[(stencilX * depth) + stencilZ]) { + return false; + } + + int columnX = x + minX + stencilX; + int columnZ = z + minZ + stencilZ; + int surfaceY = placer.getHighest(columnX, columnZ, data, true); + for (int below = 0; below < minSolidDepth; below++) { + if (placer.isCarved(columnX, surfaceY - below, columnZ)) { + return true; + } + } + return !placer.isSurfaceSolid(columnX, surfaceY, columnZ); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisTimeBlock.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTimeBlock.java similarity index 87% rename from core/src/main/java/com/volmit/iris/engine/object/IrisTimeBlock.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisTimeBlock.java index c99bb23a1..b681ec45d 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisTimeBlock.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTimeBlock.java @@ -16,12 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.PlatformWorld; import lombok.Data; -import org.bukkit.World; @Snippet("time-block") @Data @@ -33,7 +33,7 @@ public class IrisTimeBlock { @Desc("The ending hour. Set both to the same number for any time. If they are both set to -1, it will always be not allowed.") private double endHour = 0; - public boolean isWithin(World world) { + public boolean isWithin(PlatformWorld world) { return isWithin(((world.getTime() / 1000D) + 6) % 24); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTree.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTree.java new file mode 100644 index 000000000..b00ae06e9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTree.java @@ -0,0 +1,83 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.TreeType; + +@Snippet("tree") +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Desc("Tree replace options for this object placer") +@Data +public class IrisTree { + @Required + @Desc("The types of trees overwritten by this object") + @ArrayType(min = 1, type = String.class) + private KList treeTypes = new KList<>(); + + + @Required + @Desc("The size of the square of saplings this applies to (2 means a 2 * 2 sapling area)") + @ArrayType(min = 1, type = IrisTreeSize.class) + private KList sizes = new KList<>(); + + + public boolean matches(IrisTreeSize size, TreeType type) { + if (!matchesSize(size)) { + return false; + } + + return matchesType(type); + } + + private boolean matchesSize(IrisTreeSize size) { + for (IrisTreeSize i : getSizes()) { + if ((i.getDepth() == size.getDepth() && i.getWidth() == size.getWidth()) || (i.getDepth() == size.getWidth() && i.getWidth() == size.getDepth())) { + return true; + } + } + + return false; + } + + private boolean matchesType(TreeType type) { + if (type == null) { + return false; + } + + String name = type.name(); + for (String i : getTreeTypes()) { + if (i != null && i.equalsIgnoreCase(name)) { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeAzimuthMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeAzimuthMode.java new file mode 100644 index 000000000..75215bdf7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeAzimuthMode.java @@ -0,0 +1,45 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls the compass direction (azimuth) of trunk lean as it rises, or the direction each branch is thrown.") +public enum IrisTreeAzimuthMode { + @Desc("A single fixed azimuth.") + CONSTANT, + @Desc("Azimuth ramps straight from start to end with height.") + LINEAR, + @Desc("Azimuth winds around the trunk a number of turns (corkscrew).") + SPIRAL, + @Desc("Azimuth oscillates with a sine wave.") + SINE, + @Desc("Azimuth wanders using deterministic noise.") + NOISE, + @Desc("Each branch picks a uniformly random azimuth (falls back to constant for trunk lean).") + RANDOM, + @Desc("Phyllotaxis: each successive branch is offset by the 137.5 degree golden angle for the most natural spiral packing (branches).") + GOLDEN_ANGLE, + @Desc("Each successive branch is thrown to the opposite side (180 degrees apart).") + ALTERNATING, + @Desc("Branches are grouped into evenly spaced whorls of azimuthWhorlCount around each ring.") + WHORL, + @Desc("Azimuth zigzags by +/- amplitude on alternating branches.") + ZIGZAG +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeBranchProbability.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeBranchProbability.java new file mode 100644 index 000000000..926f12073 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeBranchProbability.java @@ -0,0 +1,47 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls how the chance of spawning a branch varies with trunk height (0 at the base, 1 at the top).") +public enum IrisTreeBranchProbability { + @Desc("Same branch chance at every height.") + CONSTANT, + @Desc("Ramps straight from base chance to crown chance.") + LINEAR, + @Desc("S-curve that switches on around a midpoint height.") + SIGMOID, + @Desc("Chance rises with height raised to an exponent, clustering branches near the crown.") + TOP_HEAVY, + @Desc("Bell curve peaking in a height band.") + GAUSSIAN, + @Desc("Irregular organic branching from deterministic noise.") + NOISE, + @Desc("Chance falls with height (1-t)^exponent, clustering branches near the base.") + BOTTOM_HEAVY, + @Desc("Regular whorl rings of branches at evenly spaced heights (periods controls count).") + PERIODIC, + @Desc("Branches only within a height window around mean +/- std.") + BAND, + @Desc("Inverse of GAUSSIAN: branches avoid the mean band and favor base and crown.") + INVERSE_GAUSSIAN, + @Desc("Exponential decay from the base upward (constant * e^-exponent*t).") + EXPONENTIAL_DECAY +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeBranches.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeBranches.java new file mode 100644 index 000000000..7b9cdc1ce --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeBranches.java @@ -0,0 +1,131 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("tree-branches") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A branch system that throws limbs out of the trunk and places a leaf ball at each tip. When present, branches drive the canopy and only the topmost crown disc of the profile is still placed.") +@Data +public class IrisTreeBranches { + @Desc("How branch spawn chance varies over trunk height.") + private IrisTreeBranchProbability probabilityFunction = IrisTreeBranchProbability.TOP_HEAVY; + + @MinNumber(0) + @Desc("Branch chance for the CONSTANT probability function.") + private double probabilityConstant = 0.5; + + @MinNumber(0) + @Desc("Branch chance at the base for the LINEAR probability function.") + private double probabilityBase = 0; + + @MinNumber(0) + @Desc("Branch chance at the crown for the LINEAR probability function.") + private double probabilityCrown = 1; + + @Desc("Steepness for the SIGMOID probability function.") + private double probabilitySteepness = 10; + + @Desc("Midpoint height (0-1) for the SIGMOID probability function.") + private double probabilityMidpoint = 0.7; + + @Desc("Exponent for the TOP_HEAVY probability function.") + private double probabilityExponent = 2; + + @Desc("Mean height (0-1) for the GAUSSIAN probability function.") + private double probabilityMean = 0.7; + + @Desc("Standard deviation for the GAUSSIAN probability function.") + private double probabilityStd = 0.15; + + @Desc("Noise scale for the NOISE probability function.") + private double probabilityScale = 1; + + @MinNumber(0) + @Desc("Number of whorl rings over the trunk height for the PERIODIC probability function.") + private double probabilityPeriods = 5; + + @Desc("How branch length varies over trunk height.") + private IrisTreeFunction lengthFunction = IrisTreeFunction.LINEAR; + + @MinNumber(0) + @Desc("Branch length at the base for the LINEAR length function.") + private double lengthBase = 1; + + @MinNumber(0) + @Desc("Branch length at the crown for the LINEAR length function.") + private double lengthCrown = 4; + + @MinNumber(0) + @Desc("Branch length for the CONSTANT length function.") + private double lengthConstant = 3; + + @MinNumber(0) + @Desc("Maximum branch length for the SIGMOID, LOG and PARABOLIC length functions.") + private double lengthMax = 4; + + @Desc("Steepness for the SIGMOID length function.") + private double lengthSteepness = 5; + + @Desc("How the compass direction of each branch is chosen.") + private IrisTreeAzimuthMode azimuthMode = IrisTreeAzimuthMode.RANDOM; + + @Desc("Fixed azimuth in degrees when azimuthMode is CONSTANT.") + private double azimuth = 0; + + @Desc("Elevation angle in degrees from horizontal. 0 is flat, positive points up, negative droops down.") + private double elevation = 0; + + @MinNumber(0) + @MaxNumber(1) + @Desc("How much each branch sags/droops along its length (catenary). 0 is a straight branch; higher values arc the branch and its tip downward.") + private double sag = 0; + + @MinNumber(0) + @MaxNumber(6) + @Desc("How many recursive levels of sub-branches to grow. 0 is none, 1 is a single level, 2+ produces fractal branching.") + private int branchDepth = 1; + + @Desc("If true, branches are clamped to never droop below horizontal (useful for upright species).") + private boolean leafStartUp = false; + + @MinNumber(0) + @Desc("Leaf ball radius placed at each branch tip.") + private int clusterRadius = 2; + + @Desc("How the branch tip leaf cluster fills.") + private IrisTreeLeafMode clusterMode = IrisTreeLeafMode.TRIMMED; + + @MinNumber(0) + @Desc("Fill density for the branch tip leaf cluster when clusterMode is density or noise.") + private double clusterDensity = 0.85; + + @Desc("Optional one level of recursive sub-branches sprouting from each branch tip.") + private IrisTreeSubBranches subBranches = null; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeCanopy.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeCanopy.java new file mode 100644 index 000000000..be8fee597 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeCanopy.java @@ -0,0 +1,71 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("tree-canopy") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("The leaf crown of a procedural tree. Built from stacked dome-shaped discs (driven by the profile or by explicit layers) and optionally a branch system.") +@Data +public class IrisTreeCanopy { + @MinNumber(0) + @MaxNumber(180) + @Desc("Elevation angle in degrees from vertical for each leaf dome. 90 is a flat disc, below 90 domes downward into a sphere, above 90 flares outward into an umbrella.") + private double startAngle = 90; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Vertical scale of the canopy volume. Lower values flatten the crown.") + private double squish = 1; + + @Desc("How leaves fill each canopy disc.") + private IrisTreeLeafMode mode = IrisTreeLeafMode.TRIMMED; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Fill probability used by the density and noise leaf modes.") + private double leafDensity = 0.85; + + @MinNumber(0.1) + @Desc("Horizontal X stretch of the crown for elliptical or wind-blown shapes. 1 is circular.") + private double crownStretchX = 1; + + @MinNumber(0.1) + @Desc("Horizontal Z stretch of the crown for elliptical or wind-blown shapes. 1 is circular.") + private double crownStretchZ = 1; + + @ArrayType(min = 1, type = IrisTreeLayer.class) + @Desc("Optional explicit crown discs. When provided these replace the profile-driven layers and let you sculpt the silhouette by hand.") + private KList layers = new KList<>(); + + @Desc("Optional branch system. When present, branches build most of the canopy and only the topmost profile disc is still placed as a crown.") + private IrisTreeBranches branches = null; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecorator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecorator.java new file mode 100644 index 000000000..fa3dc97d5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecorator.java @@ -0,0 +1,61 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("tree-decorator") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("An accent block applied to a generated tree such as fruit on branch tips, vines on the trunk, snow on the crown, or a glowing underside.") +@Data +public class IrisTreeDecorator { + @Desc("Where this decorator is placed on the tree.") + private IrisTreeDecoratorTarget target = IrisTreeDecoratorTarget.BRANCH_TIP; + + @Required + @Desc("The block id to place, e.g. minecraft:magma_block. Ignored when palette is set.") + @RegistryListBlockType + private String block = ""; + + @Desc("A noise-driven palette for this decorator. When set this overrides the single block.") + private IrisMaterialPalette palette = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("The chance per candidate position for this decorator to place.") + private double chance = 0.5; + + @MinNumber(1) + @Desc("Maximum strand length downward for the CANOPY_HANG target (random 1..length per column).") + private int length = 1; + + @Desc("If true the block's facing is oriented away from the trunk (for trunk-mounted fences/gates/banners).") + private boolean axisAware = false; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecoratorTarget.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecoratorTarget.java new file mode 100644 index 000000000..cdf72f006 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecoratorTarget.java @@ -0,0 +1,45 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Where a tree decorator block is placed.") +public enum IrisTreeDecoratorTarget { + @Desc("At the tip of each generated branch (vines, fruit, lanterns).") + BRANCH_TIP, + @Desc("On air-facing sides of trunk and branch blocks (vines, moss).") + TRUNK_SURFACE, + @Desc("On top of the highest block in each column (snow, sculk).") + CANOPY_TOP, + @Desc("One block below the lowest leaf in each column (glowing underside).") + CANOPY_BOTTOM, + @Desc("Around the y=0 ring at the base of the trunk (roots, mushrooms).") + TRUNK_BASE, + @Desc("On air-facing sides of leaf blocks (berries, lights nestled in the foliage).") + LEAF_SURFACE, + @Desc("Hanging strands of up to 'length' blocks below the lowest leaf in each column (vines, icicles).") + CANOPY_HANG, + @Desc("On the top face of trunk and branch blocks (moss, snow on limbs).") + BRANCH_SURFACE, + @Desc("One block above the very top of the trunk (nest, beehive, beacon).") + TRUNK_TOP, + @Desc("Scattered across the ground footprint around the base, wider than the trunk-base ring.") + GROUND_SCATTER +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeFunction.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeFunction.java new file mode 100644 index 000000000..cada3dff2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeFunction.java @@ -0,0 +1,47 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("A shaping function evaluated over normalized height (0 at the base, 1 at the top). Used for trunk width, trunk curve and branch length.") +public enum IrisTreeFunction { + @Desc("Same value at every height.") + CONSTANT, + @Desc("Ramps straight from start to end.") + LINEAR, + @Desc("S-curve transition controlled by steepness.") + SIGMOID, + @Desc("Logarithmic falloff controlled by base.") + LOG, + @Desc("Sine ripple controlled by period and amplitude.") + SINE, + @Desc("Parabolic pinch, narrowest at peakOffset.") + PARABOLIC, + @Desc("Geometric (exponential) interpolation from start to end.") + EXPONENTIAL, + @Desc("Square-root ease: fast change low, slow high.") + SQRT, + @Desc("Hard step that jumps from start to end at the threshold (peakOffset).") + STEP, + @Desc("Bell bulge peaking in the middle of the height range.") + BELL, + @Desc("Smoothstep ease-in-out (3t^2 - 2t^3).") + EASE_IN_OUT +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeLayer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeLayer.java new file mode 100644 index 000000000..8d3ed1acb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeLayer.java @@ -0,0 +1,42 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("tree-layer") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("An explicit canopy disc at a given vertical offset and radius. Define a list of these to bypass the profile and sculpt the crown by hand.") +@Data +public class IrisTreeLayer { + @Desc("The vertical offset of this disc, measured in blocks above the trunk base.") + private int yOffset = 0; + + @MinNumber(0.5) + @Desc("The radius of this leaf disc in blocks.") + private double radius = 2; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeLeafMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeLeafMode.java new file mode 100644 index 000000000..fc02afca2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeLeafMode.java @@ -0,0 +1,43 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("How leaves fill a disc or cluster.") +public enum IrisTreeLeafMode { + @Desc("Corner-trimmed disc/sphere for a rounded silhouette.") + TRIMMED, + @Desc("Completely filled disc/sphere.") + FILLED, + @Desc("Seeded probabilistic falloff toward the edge controlled by leafDensity.") + DENSITY, + @Desc("Seeded value-noise mask controlled by leafDensity.") + NOISE, + @Desc("Only the outer shell is placed, leaving the interior hollow.") + HOLLOW, + @Desc("Quadratic radial gradient: dense at the center, thinning sharply toward the edge.") + GRADIENT, + @Desc("Coarse low-frequency noise that forms clumps and gaps of foliage.") + CLUMPED, + @Desc("Trimmed with heavy random erosion of the outer ring for a ragged, weathered look.") + TATTERED, + @Desc("Very thin scattering of leaves across the volume.") + SPARSE +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisTreeModes.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeModes.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/IrisTreeModes.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisTreeModes.java index adc5d03c6..2e9dfd0f3 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisTreeModes.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeModes.java @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("Sapling override object picking options") public enum IrisTreeModes { - @Desc("Check biome, then region, then dimension, pick the first one that has options") + @Desc("Check biome, then region, pick the first one that has options. Dimension placements are not consulted.") FIRST, @Desc("Check biome, regions, and dimensions, and pick any option from the total list") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeProfile.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeProfile.java new file mode 100644 index 000000000..14f22d839 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeProfile.java @@ -0,0 +1,53 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Named canopy presets that drive default layer radii and shape, scaled to the tree height.") +public enum IrisTreeProfile { + @Desc("Rounded broadleaf crown.") + OAK, + @Desc("Slim upright broadleaf crown.") + BIRCH, + @Desc("Narrow conifer cone.") + SPRUCE, + @Desc("Tall thin crown concentrated near the very top.") + JUNGLE, + @Desc("Flat shallow umbrella crown.") + ACACIA, + @Desc("Dense rounded broadleaf crown.") + DARK_OAK, + @Desc("Flat wide umbrella crown compressed into the top quarter (vanilla dark oak look).") + DARK_OAK_FLAT, + @Desc("Extra wide flat umbrella that overlaps into a continuous roofed canopy.") + DARK_OAK_FLAT_WIDE, + @Desc("Broad rounded blossom crown.") + CHERRY, + @Desc("Bare trunk with a small tuft of fronds only at the very top.") + PALM, + @Desc("Broad crown with wide drooping radii, ideal with a low startAngle to make a weeping skirt.") + WILLOW, + @Desc("Very narrow tall crown hugging the trunk (cypress / lombardy poplar).") + COLUMNAR, + @Desc("Squat, wide, low crown for shrubs and saplings.") + BUSH, + @Desc("Giant conifer cone for 2x2 mega spruce trunks.") + MEGA_SPRUCE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeRootStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeRootStyle.java new file mode 100644 index 000000000..99b3aeafd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeRootStyle.java @@ -0,0 +1,31 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("How a procedural tree's root system is built below (and around) the trunk base.") +public enum IrisTreeRootStyle { + @Desc("A single tapering taproot straight down from the base.") + TAPROOT, + @Desc("A taproot plus a ring of flared buttress legs that splay outward as they descend (default).") + BUTTRESS, + @Desc("Visible above-ground prop roots that arch out and down from the lower trunk to the ground (mangrove style).") + STILT +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSecondaryLeaf.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSecondaryLeaf.java new file mode 100644 index 000000000..643e69d8d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSecondaryLeaf.java @@ -0,0 +1,46 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("tree-secondary-leaf") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A weighted accent leaf or foliage block scattered through the canopy (blossoms, shroomlight, berries).") +@Data +public class IrisTreeSecondaryLeaf { + @Required + @Desc("The block id to scatter, e.g. minecraft:shroomlight") + @RegistryListBlockType + private String block = ""; + + @MinNumber(0) + @Desc("The relative weight of this block among the secondary leaves.") + private int weight = 1; +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisTreeSettings.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSettings.java similarity index 86% rename from core/src/main/java/com/volmit/iris/engine/object/IrisTreeSettings.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisTreeSettings.java index b3afde29c..add93ef47 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisTreeSettings.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSettings.java @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisTreeSize.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSize.java similarity index 88% rename from core/src/main/java/com/volmit/iris/engine/object/IrisTreeSize.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisTreeSize.java index f27c6699d..dcff6c6ab 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisTreeSize.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSize.java @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; +package art.arcane.iris.engine.object; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.engine.object.annotations.Snippet; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSubBranches.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSubBranches.java new file mode 100644 index 000000000..d68190191 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSubBranches.java @@ -0,0 +1,66 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("tree-sub-branches") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("One recursive level of sub-branches sprouting from each primary branch tip.") +@Data +public class IrisTreeSubBranches { + @MinNumber(1) + @Desc("How many sub-branches sprout from each primary branch tip.") + private int count = 1; + + @Desc("Pitch deflection in degrees applied to each sub-branch relative to its parent. Positive bends up, negative droops down. Cumulative pitch past 90 makes sub-branches point downward.") + private double pitchDelta = 0; + + @Desc("Yaw (horizontal) spread in degrees fanned between sub-branches.") + private double yawDelta = 45; + + @MinNumber(0) + @Desc("Sub-branch length as a fraction of its parent branch length.") + private double lengthScale = 0.5; + + @MinNumber(0) + @MaxNumber(1) + @Desc("How much each sub-branch sags/droops along its length (catenary).") + private double sag = 0; + + @MinNumber(0) + @Desc("Leaf ball radius placed at each sub-branch tip.") + private int clusterRadius = 1; + + @Desc("How the sub-branch tip leaf cluster fills.") + private IrisTreeLeafMode clusterMode = IrisTreeLeafMode.TRIMMED; + + @MinNumber(0) + @Desc("Fill density for the sub-branch leaf cluster when clusterMode is density or noise.") + private double clusterDensity = 0.85; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVacuumSettings.java b/core/src/main/java/art/arcane/iris/engine/object/IrisVacuumSettings.java new file mode 100644 index 000000000..398c562ab --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisVacuumSettings.java @@ -0,0 +1,39 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.Snippet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("vacuum-settings") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("Defines how a VACUUM place mode bends terrain to meet an object's base.") +@Data +public class IrisVacuumSettings { + @MinNumber(0) + @MaxNumber(128) + @Desc("The horizontal radius (in blocks) the terrain deformation extends beyond the object footprint before it blends back to the natural surface. 0 = derive automatically from the place mode (VACUUM, VACUUM_HIGH larger, VACUUM_FAST smaller).") + private int radius = 0; + @MinNumber(0.25) + @MaxNumber(8) + @Desc("The falloff exponent controlling how the deformation eases out across the radius. 1 = linear (cone), 2 = parabolic (default, gentle bowl), higher = flatter near the object with a sharper drop at the edge.") + private double falloff = 2.0; + @MinNumber(0) + @MaxNumber(64) + @Desc("For VACUUM_ORGANIC: the maximum number of blocks the effective radius is randomly perturbed per column, giving the meeting edge an irregular organic outline instead of a clean circle.") + private int organicJitter = 4; + @MinNumber(0) + @MaxNumber(64) + @Desc("For VACUUM_WAVY: the vertical amplitude (in blocks) of the rolling simplex wave applied across the terrain bend. The wave fades to zero under the object (so it stays flush) and at the outer radius (so it blends back to the surface), peaking across the mid-slope. 0 disables the wave.") + private int waveAmplitude = 3; + @MinNumber(0.1) + @MaxNumber(64) + @Desc("For VACUUM_WAVY: the frequency of the simplex wave. Higher values make tighter, choppier ripples; lower values make broad, gentle swells. The wavelength in blocks is roughly 100 / waveScale.") + private double waveScale = 5.0; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaBiomeTags.java b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaBiomeTags.java new file mode 100644 index 000000000..8e7cb261a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaBiomeTags.java @@ -0,0 +1,185 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Direct vanilla biome tag membership, generated from the MC 26.2 built-in datapack + * (data/minecraft/tags/worldgen/biome). Used to give Iris custom biomes the tag membership of the vanilla + * biome they derive from, so tag selectors written by vanilla and by mods (#minecraft:is_overworld and + * friends) resolve against Iris terrain. + * + *

Only DIRECT membership is listed. Vanilla's derived tags reference other tags rather than repeating + * biomes (#minecraft:is_ocean includes #minecraft:is_deep_ocean, water_on_map_outlines includes + * #minecraft:is_river, and so on), so adding a custom biome to the direct tags carries it into the derived + * tags for free. + * + *

The has_structure/* tags are deliberately absent. Iris resolves native structure placement through the + * biome's structure derivative, never through the custom biome, so injecting custom biomes there would place + * a structure twice. + * + *

{@code stronghold_biased_to} is absent for the same reason, even though it is not a has_structure tag. + * It is a structure-placement input: {@code worldgen/structure_set/strongholds.json} reads it as + * {@code preferred_biomes} when it rings strongholds around spawn. Inheriting it would enter every Iris biome + * derived from a tagged vanilla biome into that ring - which is the entire overworld surface of a typical pack - + * and let vanilla place strongholds where the pack's own structure configuration did not ask for them. + * + *

An unknown key (a mod biome, a datapack biome, or a vanilla biome added after 26.2) contributes nothing; + * the pack author's explicit tags still apply. + */ +public final class IrisVanillaBiomeTags { + private static final String TAG_NAMESPACE = "minecraft:"; + private static final Map> DIRECT_TAGS = new HashMap<>(96); + + private IrisVanillaBiomeTags() { + } + + /** + * Tags the given vanilla biome key belongs to directly. Never null; empty for unknown keys. Keys are + * returned fully namespaced and lowercase. + */ + public static List tagsFor(String biomeKey) { + if (biomeKey == null || biomeKey.isBlank()) { + return List.of(); + } + String normalized = biomeKey.trim().toLowerCase(Locale.ROOT); + if (normalized.indexOf(':') < 0) { + normalized = TAG_NAMESPACE + normalized; + } + List tags = DIRECT_TAGS.get(normalized); + return tags == null ? List.of() : tags; + } + + static int knownBiomeCount() { + return DIRECT_TAGS.size(); + } + + private static void put(String biomeKey, String... tagPaths) { + String[] namespaced = new String[tagPaths.length]; + for (int i = 0; i < tagPaths.length; i++) { + namespaced[i] = TAG_NAMESPACE + tagPaths[i]; + } + DIRECT_TAGS.put(biomeKey, List.of(namespaced)); + } + + static { + put("minecraft:badlands", "is_badlands", "is_overworld"); + put("minecraft:bamboo_jungle", "is_jungle", "is_overworld"); + put("minecraft:basalt_deltas", "is_nether"); + put("minecraft:beach", "is_beach", "is_overworld"); + put("minecraft:birch_forest", "is_forest", "is_overworld"); + put("minecraft:cherry_grove", "is_mountain", "is_overworld"); + put("minecraft:cold_ocean", "is_ocean", "is_overworld", "spawns_cold_variant_farm_animals"); + put("minecraft:crimson_forest", "is_nether"); + put("minecraft:dark_forest", "is_forest", "is_overworld"); + put("minecraft:deep_cold_ocean", "is_deep_ocean", "is_overworld", "spawns_cold_variant_farm_animals"); + put("minecraft:deep_dark", + "is_overworld", "mineshaft_blocking", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs"); + put("minecraft:deep_frozen_ocean", + "is_deep_ocean", "is_overworld", "polar_bears_spawn_on_alternate_blocks", "spawns_cold_variant_farm_animals", + "spawns_cold_variant_frogs"); + put("minecraft:deep_lukewarm_ocean", "is_deep_ocean", "is_overworld", "spawns_warm_variant_farm_animals"); + put("minecraft:deep_ocean", "is_deep_ocean", "is_overworld"); + put("minecraft:desert", + "is_overworld", "spawns_gold_rabbits", "spawns_warm_variant_farm_animals", "spawns_warm_variant_frogs"); + put("minecraft:dripstone_caves", "is_overworld"); + put("minecraft:end_barrens", "is_end"); + put("minecraft:end_highlands", "is_end"); + put("minecraft:end_midlands", "is_end"); + put("minecraft:eroded_badlands", "is_badlands", "is_overworld"); + put("minecraft:flower_forest", "is_forest", "is_overworld"); + put("minecraft:forest", "is_forest", "is_overworld"); + put("minecraft:frozen_ocean", + "is_ocean", "is_overworld", "polar_bears_spawn_on_alternate_blocks", "spawns_cold_variant_farm_animals", + "spawns_cold_variant_frogs", "spawns_snow_foxes", "spawns_white_rabbits"); + put("minecraft:frozen_peaks", + "is_mountain", "is_overworld", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs", + "spawns_snow_foxes", "spawns_white_rabbits"); + put("minecraft:frozen_river", + "is_overworld", "is_river", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs", + "spawns_snow_foxes", "spawns_white_rabbits"); + put("minecraft:grove", + "is_forest", "is_overworld", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs", + "spawns_snow_foxes", "spawns_white_rabbits"); + put("minecraft:ice_spikes", + "is_overworld", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs", "spawns_snow_foxes", + "spawns_white_rabbits"); + put("minecraft:jagged_peaks", + "is_mountain", "is_overworld", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs", + "spawns_snow_foxes", "spawns_white_rabbits"); + put("minecraft:jungle", "is_jungle", "is_overworld"); + put("minecraft:lukewarm_ocean", "is_ocean", "is_overworld", "spawns_warm_variant_farm_animals"); + put("minecraft:lush_caves", "allows_tropical_fish_spawns_at_any_height", "is_overworld"); + put("minecraft:mangrove_swamp", + "allows_surface_slime_spawns", "is_overworld", "spawns_warm_variant_farm_animals", + "spawns_warm_variant_frogs", "water_on_map_outlines"); + put("minecraft:meadow", "is_mountain", "is_overworld"); + put("minecraft:mushroom_fields", "is_overworld", "without_zombie_sieges"); + put("minecraft:nether_wastes", "is_nether"); + put("minecraft:ocean", "is_ocean", "is_overworld"); + put("minecraft:old_growth_birch_forest", "is_forest", "is_overworld"); + put("minecraft:old_growth_pine_taiga", + "is_overworld", "is_taiga", "spawns_cold_variant_farm_animals"); + put("minecraft:old_growth_spruce_taiga", + "is_overworld", "is_taiga", "spawns_cold_variant_farm_animals"); + put("minecraft:pale_garden", "is_forest", "is_overworld"); + put("minecraft:plains", "is_overworld"); + put("minecraft:river", "is_overworld", "is_river"); + put("minecraft:savanna", "is_overworld", "is_savanna"); + put("minecraft:savanna_plateau", "is_overworld", "is_savanna"); + put("minecraft:small_end_islands", "is_end"); + put("minecraft:snowy_beach", + "is_beach", "is_overworld", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs", + "spawns_snow_foxes", "spawns_white_rabbits"); + put("minecraft:snowy_plains", + "is_overworld", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs", "spawns_snow_foxes", + "spawns_white_rabbits"); + put("minecraft:snowy_slopes", + "is_mountain", "is_overworld", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs", + "spawns_snow_foxes", "spawns_white_rabbits"); + put("minecraft:snowy_taiga", + "is_overworld", "is_taiga", "spawns_cold_variant_farm_animals", "spawns_cold_variant_frogs", + "spawns_snow_foxes", "spawns_white_rabbits"); + put("minecraft:soul_sand_valley", "is_nether"); + put("minecraft:sparse_jungle", "is_jungle", "is_overworld"); + put("minecraft:stony_peaks", "is_mountain", "is_overworld", "spawns_cold_variant_farm_animals"); + put("minecraft:stony_shore", "is_overworld"); + put("minecraft:sulfur_caves", "is_overworld"); + put("minecraft:sunflower_plains", "is_overworld"); + put("minecraft:swamp", "allows_surface_slime_spawns", "is_overworld", "water_on_map_outlines"); + put("minecraft:taiga", "is_overworld", "is_taiga", "spawns_cold_variant_farm_animals"); + put("minecraft:the_end", "is_end"); + put("minecraft:the_void", "without_wandering_trader_spawns"); + put("minecraft:warm_ocean", + "is_ocean", "is_overworld", "produces_corals_from_bonemeal", "spawns_coral_variant_zombie_nautilus", + "spawns_warm_variant_farm_animals", "spawns_warm_variant_frogs"); + put("minecraft:warped_forest", "is_nether"); + put("minecraft:windswept_forest", + "is_hill", "is_overworld", "spawns_cold_variant_farm_animals"); + put("minecraft:windswept_gravelly_hills", + "is_hill", "is_overworld", "spawns_cold_variant_farm_animals"); + put("minecraft:windswept_hills", "is_hill", "is_overworld", "spawns_cold_variant_farm_animals"); + put("minecraft:windswept_savanna", "is_overworld", "is_savanna"); + put("minecraft:wooded_badlands", "is_badlands", "is_overworld"); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaLootTable.java b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaLootTable.java new file mode 100644 index 000000000..1e87a18f5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaLootTable.java @@ -0,0 +1,111 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; +import org.bukkit.loot.LootContext; +import org.bukkit.loot.LootTable; + +import java.io.File; + +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisVanillaLootTable extends IrisLootTable { + /** + * The vanilla loot table key ("minecraft:chests/simple_dungeon"), not the LootTable itself. A + * raw org.bukkit.loot.LootTable field - and the constructor lombok would generate for it - makes + * every reflective walk over the IrisLootTable hierarchy (Gson, schema generation, the purity + * gate) resolve a Bukkit class. The table is resolved lazily at pull time, which only ever + * happens on Bukkit. The key text is identical to what {@code LootTable.getKey().toString()} + * produced before, so {@code LootResolver.tableIdentity} (which falls back to + * {@link #getName()}) yields the same loot seed. + */ + private final String lootTableKey; + + /** + * The resolved table, looked up once. Every chest pull resolved it again - a NamespacedKey parse plus a registry + * lookup per container, and a dungeon room is a lot of containers. + *

+ * Transient for the same reason the field above is a String: it keeps the Bukkit type argument out of the Gson + * field walk and out of the pack purity gate, both of which resolve generic field types for the fields they visit. + */ + private final transient AtomicCache resolvedTable = new AtomicCache<>(); + + @Override + public String getName() { + return "Vanilla " + lootTableKey; + } + + @Override + public int getRarity() { + return 0; + } + + @Override + public int getMaxPicked() { + return 0; + } + + @Override + public int getMinPicked() { + return 0; + } + + @Override + public int getMaxTries() { + return 0; + } + + @Override + public KList getLoot() { + return new KList<>(); + } + + @Override + public KList getLoot(boolean debug, long lootSeed, InventorySlotType slot, World world, int x, int y, int z) { + LootTable table = resolveTable(); + if (table == null) { + IrisLogging.warn("Unknown vanilla loot table " + lootTableKey); + return new KList<>(); + } + RNG rng = LootResolver.tableRng(lootSeed, this, x, y, z); + return new KList<>(table.populateLoot(rng, new LootContext.Builder(new Location(world, x, y, z)).build())); + } + + private LootTable resolveTable() { + return resolvedTable.aquire(() -> { + NamespacedKey key = NamespacedKey.fromString(lootTableKey); + return key == null ? null : Bukkit.getLootTable(key); + }); + } + + @Override + public String getFolderName() { + throw new UnsupportedOperationException("VanillaLootTables do not have a folder name"); + } + + @Override + public String getTypeName() { + throw new UnsupportedOperationException("VanillaLootTables do not have a type name"); + } + + @Override + public File getLoadFile() { + throw new UnsupportedOperationException("VanillaLootTables do not have a load file"); + } + + @Override + public IrisData getLoader() { + throw new UnsupportedOperationException("VanillaLootTables do not have a loader"); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureAdjustment.java b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureAdjustment.java new file mode 100644 index 000000000..708ff1656 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureAdjustment.java @@ -0,0 +1,68 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; +import art.arcane.volmlib.util.collection.KList; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Desc("A per-structure adjustment applied to vanilla, mod, and datapack structures that still generate natively (those NOT suppressed by an Iris 'structures' placement). Vertical shifts move the structure start, pieces, bounds, and jigsaw metadata together before references and placement. Intersecting trees are cleared automatically inside structure piece envelopes; optional postprocessing can build palette-driven foundation columns.") +@Data +public class IrisVanillaStructureAdjustment { + @ArrayType(type = String.class, min = 1) + @RegistryListVanillaStructure(prefixes = true) + @Desc("Structure keys this adjustment applies to, e.g. 'minecraft:stronghold'. A namespace:path prefix also matches, so 'minecraft:village' adjusts every village variant and 'minecraft:ruined_portal' adjusts every ruined portal. Empty matches nothing.") + private KList match = new KList<>(); + + @MinNumber(-512) + @MaxNumber(512) + @Desc("Vertical block offset. Negative pushes the structure down, positive lifts it. The resolved shift is clamped so the structure remains inside the world's vertical build bounds.") + private int yShift = 0; + + @Desc("Optional absolute world Y band. The structure is relocated so its vertical midpoint lands inside the band, deterministically from its start chunk and clamped to the world's build bounds. Precedence: preserveSourceY wins over yBand, and yBand wins over both yShift and Iris burial repositioning. Structures with a fixed vanilla alignment (ocean monument sea level, desert and jungle pyramid surface fit) ignore it.") + private IrisStructureYBand yBand = null; + + @Desc("When true, skip Iris burial repositioning so the structure keeps the Y its own vanilla placement chose. Underground structures are otherwise pushed below the lowest solid column across their whole footprint, which hides terrain-aware structures such as mineshafts that intentionally breach cliffs and surfaces. Vertical shifts still apply on top of the preserved Y: this dimension's undergroundYShift plus every matching adjustment's yShift.") + private boolean preserveSourceY = false; + + @Desc("Optional foundation columns placed beneath the native structure piece bases after placement.") + private IrisStructureStiltSettings stilt = null; + + @Desc("Optional terrain integration override. VACUUM raises surface terrain from processed solid rigid-template foundations at or below each authored ground plane with a 12-block falloff without lowering existing ground. BORE and FORCE_CARVE clear every intersecting chunk before native pieces are placed, while ENCASE fills it with solid blocks instead. Left unset, SOURCE replays the registered structure's authored terrain adaptation, including surface fitting, burial, and encapsulation.") + private IrisStructureTerrain terrain = null; + + public boolean matches(String key) { + for (String entry : match) { + if (IrisImportedStructureControl.matchesKey(entry, key)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisWeather.java b/core/src/main/java/art/arcane/iris/engine/object/IrisWeather.java new file mode 100644 index 000000000..8c4a2a059 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisWeather.java @@ -0,0 +1,46 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.spi.PlatformWorld; + +@Desc("Represents a weather type") +public enum IrisWeather { + @Desc("Represents when weather is not causing downfall") + NONE, + + @Desc("Represents rain or snow") + DOWNFALL, + + @Desc("Represents rain or snow with thunder") + DOWNFALL_WITH_THUNDER, + + @Desc("Any weather") + ANY; + + public boolean is(PlatformWorld world) { + return switch (this) { + case NONE -> !world.isStorming() && !world.isThundering(); + case DOWNFALL -> world.isStorming(); + case DOWNFALL_WITH_THUNDER -> world.isStorming() && world.isThundering(); + case ANY -> true; + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java b/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java new file mode 100644 index 000000000..dd0bcc81a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java @@ -0,0 +1,65 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.PlatformWorld; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.io.File; + +@Builder +@Data +@Accessors(chain = true, fluent = true) +public class IrisWorld { + private String platformIdentity; + private String name; + private File worldFolder; + + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private long seed; + private PlatformWorld platformWorld; + private int minHeight; + private int maxHeight; + + public long getRawWorldSeed() { + return seed; + } + + public String identity() { + return platformIdentity; + } + + public void setRawWorldSeed(long seed) { + this.seed = seed; + } + + public boolean hasPlatformWorld() { + return platformWorld != null; + } + + public int getHeight() { + return maxHeight - minHeight; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/JigsawJoint.java b/core/src/main/java/art/arcane/iris/engine/object/JigsawJoint.java new file mode 100644 index 000000000..9d83a5369 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/JigsawJoint.java @@ -0,0 +1,30 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("How a jigsaw connection may be rotated when matching two pieces together.") +public enum JigsawJoint { + @Desc("The connecting piece may rotate freely around the connection axis. Use this for vertical connectors (floors, ceilings) and any joint where roll does not matter.") + ROLLABLE, + + @Desc("The connecting piece's rotation is locked to face the connector. Use this for horizontal connectors where the attached piece must align to a specific facing (doorways, street ends).") + ALIGNED +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java b/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java new file mode 100644 index 000000000..bf4458639 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java @@ -0,0 +1,476 @@ +package art.arcane.iris.engine.object; + +import org.bukkit.Bukkit; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.block.Banner; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.block.Sign; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.iris.util.common.reflect.KeyedType; +import art.arcane.iris.util.common.scheduling.J; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.ToString; +import org.apache.commons.io.function.IOFunction; +import org.bukkit.block.banner.Pattern; +import org.bukkit.block.banner.PatternType; +import org.bukkit.block.sign.Side; +import org.bukkit.block.sign.SignSide; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.EntityType; +import org.bukkit.loot.LootTable; +import org.bukkit.loot.Lootable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Bukkit-only pre-key tile format. Unreachable from the modded loaders: every entry point is + * either Bukkit-typed ({@link #fromBukkit(BlockState)}, reached only from + * {@link TileData#getTileState(Block, boolean)} and the Bukkit structure importer) or sits behind + * the {@code BUKKIT_PRESENT} short-circuit in {@link TileData#read(DataInputStream)}, which hands + * off to the bound platform reader before this class is ever referenced. The nested handler types + * therefore keep their raw Bukkit fields. + */ +@ToString +@EqualsAndHashCode(callSuper = false) +public class LegacyTileData extends TileData { + private final int id; + private final Handler handler; + + public LegacyTileData(DataInputStream in) throws IOException { + id = in.readShort(); + Pair> factory = Handlers.LEGACY.get(id); + if (factory == null) + throw new IOException("Unknown tile type: " + id); + handler = factory.getB().apply(in); + } + + private LegacyTileData(int id, Handler handler) { + this.id = id; + this.handler = handler; + } + + @Nullable + public static LegacyTileData fromBukkit(@NonNull BlockState tileState) { + Material type = tileState.getType(); + for (Integer id : Handlers.LEGACY.keySet()) { + Pair> factory = Handlers.LEGACY.get(id); + Handler handler = factory.getA().apply(tileState, type); + if (handler != null) + return new LegacyTileData(id, handler); + } + return null; + } + + private static final class Handlers { + private static final Map>> LEGACY = Map.of( + 0, new Pair<>(SignHandler::fromBukkit, SignHandler::new), + 1, new Pair<>(SpawnerHandler::fromBukkit, SpawnerHandler::new), + 2, new Pair<>(BannerHandler::fromBukkit, BannerHandler::new), + 3, new Pair<>(LootableHandler::fromBukkit, LootableHandler::new)); + } + + @Override + public @NonNull KMap getProperties() { + return new KMap<>(); + } + + @Override + public @NonNull String getMaterialKey() { + return TileData.materialKey(handler.getMaterial()); + } + + @Override + public Material resolveMaterial() { + return handler.getMaterial(); + } + + @Override + public boolean isApplicable(BlockData data) { + return handler.isApplicable(data); + } + + @Override + public void toBukkit(Block block) { + if (!J.runAt(block.getLocation(), () -> handler.toBukkit(block))) { + J.s(() -> handler.toBukkit(block)); + } + } + + @Override + public void toBinary(DataOutputStream out) throws IOException { + out.writeShort(id); + handler.toBinary(out); + } + + @Override + public TileData clone() { + return this; + } + + private interface Handler { + Material getMaterial(); + boolean isApplicable(BlockData data); + void toBinary(DataOutputStream out) throws IOException; + void toBukkit(Block block); + } + + @FunctionalInterface + private interface Builder { + @Nullable Handler apply(@NonNull BlockState blockState, @NonNull Material type); + } + + @ToString + @EqualsAndHashCode + @AllArgsConstructor + private static class SignHandler implements Handler { + private final String line1; + private final String line2; + private final String line3; + private final String line4; + private final DyeColor dyeColor; + + private SignHandler(DataInputStream in) throws IOException { + line1 = in.readUTF(); + line2 = in.readUTF(); + line3 = in.readUTF(); + line4 = in.readUTF(); + dyeColor = DyeColor.values()[in.readByte()]; + } + + private static SignHandler fromBukkit(BlockState blockState, Material type) { + if (!signsTag().isTagged(type) || !(blockState instanceof Sign sign)) + return null; + SignSide front = sign.getSide(Side.FRONT); + return new SignHandler(front.getLine(0), front.getLine(1), front.getLine(2), front.getLine(3), front.getColor()); + } + + @Override + public Material getMaterial() { + return Material.OAK_SIGN; + } + + @Override + public boolean isApplicable(BlockData data) { + return signsTag().isTagged(data.getMaterial()); + } + + @Override + public void toBinary(DataOutputStream out) throws IOException { + out.writeUTF(line1); + out.writeUTF(line2); + out.writeUTF(line3); + out.writeUTF(line4); + out.writeByte(dyeColor.ordinal()); + } + + @Override + public void toBukkit(Block block) { + Sign sign = (Sign) block.getState(); + SignSide front = sign.getSide(Side.FRONT); + SignSide back = sign.getSide(Side.BACK); + front.setLine(0, line1); + front.setLine(1, line2); + front.setLine(2, line3); + front.setLine(3, line4); + front.setColor(dyeColor); + back.setLine(0, line1); + back.setLine(1, line2); + back.setLine(2, line3); + back.setLine(3, line4); + back.setColor(dyeColor); + sign.update(); + } + } + @ToString + @EqualsAndHashCode + @AllArgsConstructor + private static class SpawnerHandler implements Handler { + private final EntityType type; + + private SpawnerHandler(DataInputStream in) throws IOException { + EntityType resolved = null; + if (in.markSupported()) { + in.mark(Integer.MAX_VALUE); + } + + try { + String keyString = in.readUTF(); + NamespacedKey key = NamespacedKey.fromString(keyString); + resolved = key == null ? null : Registry.ENTITY_TYPE.get(key); + if (resolved == null && in.markSupported()) { + in.reset(); + } + } catch (Throwable ignored) { + if (in.markSupported()) { + in.reset(); + } + } + + if (resolved == null) { + short legacyOrdinal = in.readShort(); + EntityType[] legacyValues = EntityType.values(); + if (legacyOrdinal >= 0 && legacyOrdinal < legacyValues.length) { + resolved = legacyValues[legacyOrdinal]; + } + } + + type = resolved == null ? EntityType.PIG : resolved; + } + + private static SpawnerHandler fromBukkit(BlockState blockState, Material material) { + if (material != Material.SPAWNER || !(blockState instanceof CreatureSpawner spawner)) + return null; + return new SpawnerHandler(spawner.getSpawnedType()); + } + + @Override + public Material getMaterial() { + return Material.SPAWNER; + } + + @Override + public boolean isApplicable(BlockData data) { + return data.getMaterial() == Material.SPAWNER; + } + + @Override + public void toBinary(DataOutputStream out) throws IOException { + NamespacedKey key = KeyedType.getKey(type); + out.writeUTF(key == null ? type.name() : key.toString()); + } + + @Override + public void toBukkit(Block block) { + CreatureSpawner spawner = (CreatureSpawner) block.getState(); + spawner.setSpawnedType(type); + spawner.update(); + } + } + @ToString + @EqualsAndHashCode + @AllArgsConstructor + private static class BannerHandler implements Handler { + private final KList patterns; + private final DyeColor baseColor; + + private BannerHandler(DataInputStream in) throws IOException { + DyeColor[] dyeColors = DyeColor.values(); + int baseColorIndex = in.readUnsignedByte(); + baseColor = baseColorIndex >= 0 && baseColorIndex < dyeColors.length ? dyeColors[baseColorIndex] : DyeColor.WHITE; + patterns = new KList<>(); + int listSize = in.readUnsignedByte(); + + if (in.markSupported()) { + in.mark(Integer.MAX_VALUE); + } + + boolean parsedKeyed = false; + try { + KList keyedPatterns = new KList<>(); + for (int i = 0; i < listSize; i++) { + int colorIndex = in.readUnsignedByte(); + DyeColor color = colorIndex >= 0 && colorIndex < dyeColors.length ? dyeColors[colorIndex] : DyeColor.WHITE; + NamespacedKey patternKey = NamespacedKey.fromString(in.readUTF()); + PatternType pattern = patternKey == null ? null : Registry.BANNER_PATTERN.get(patternKey); + if (pattern == null) { + throw new IOException("Unknown banner pattern key"); + } + keyedPatterns.add(new Pattern(color, pattern)); + } + patterns.addAll(keyedPatterns); + parsedKeyed = true; + } catch (Throwable ignored) { + if (in.markSupported()) { + in.reset(); + } + } + + if (parsedKeyed) { + return; + } + + PatternType[] legacyPatternTypes = PatternType.values(); + PatternType fallbackPattern = Registry.BANNER_PATTERN.get(NamespacedKey.minecraft("base")); + if (fallbackPattern == null && legacyPatternTypes.length > 0) { + fallbackPattern = legacyPatternTypes[0]; + } + + for (int i = 0; i < listSize; i++) { + int colorIndex = in.readUnsignedByte(); + DyeColor color = colorIndex >= 0 && colorIndex < dyeColors.length ? dyeColors[colorIndex] : DyeColor.WHITE; + int legacyPatternIndex = in.readUnsignedByte(); + PatternType pattern = legacyPatternIndex >= 0 && legacyPatternIndex < legacyPatternTypes.length ? legacyPatternTypes[legacyPatternIndex] : fallbackPattern; + if (pattern != null) { + patterns.add(new Pattern(color, pattern)); + } + } + } + + private static BannerHandler fromBukkit(BlockState blockState, Material type) { + if (!Tag.BANNERS.isTagged(type) || !(blockState instanceof Banner banner)) + return null; + return new BannerHandler(new KList<>(banner.getPatterns()), banner.getBaseColor()); + } + + @Override + public Material getMaterial() { + return Material.WHITE_BANNER; + } + + @Override + public boolean isApplicable(BlockData data) { + return Tag.BANNERS.isTagged(data.getMaterial()); + } + + @Override + public void toBinary(DataOutputStream out) throws IOException { + out.writeByte(baseColor.ordinal()); + out.writeByte(patterns.size()); + for (Pattern i : patterns) { + out.writeByte(i.getColor().ordinal()); + NamespacedKey key = KeyedType.getKey(i.getPattern()); + if (key == null) { + key = NamespacedKey.minecraft("base"); + } + out.writeUTF(key.toString()); + } + } + + @Override + public void toBukkit(Block block) { + Banner banner = (Banner) block.getState(); + banner.setBaseColor(baseColor); + banner.setPatterns(patterns); + banner.update(); + } + } + + @ToString + @EqualsAndHashCode + @AllArgsConstructor + private static class LootableHandler implements Handler { + private final Material material; + private final NamespacedKey lootTable; + private final long seed; + + private LootableHandler(DataInputStream in) throws IOException { + String materialKey = in.readUTF(); + NamespacedKey resolvedMaterial = NamespacedKey.fromString(materialKey); + Material resolved = resolvedMaterial == null ? null : Registry.MATERIAL.get(resolvedMaterial); + if (resolved == null) { + resolved = Material.matchMaterial(materialKey); + } + material = resolved == null ? Material.CHEST : resolved; + String tableKey = in.readUTF(); + lootTable = tableKey.isEmpty() ? null : NamespacedKey.fromString(tableKey); + seed = in.readLong(); + } + + private static LootableHandler fromBukkit(BlockState blockState, Material type) { + if (!(blockState instanceof Lootable lootable)) { + return null; + } + LootTable table = lootable.getLootTable(); + if (table == null) { + return null; + } + NamespacedKey key = table.getKey(); + if (key == null) { + return null; + } + return new LootableHandler(type, key, lootable.getSeed()); + } + + @Override + public Material getMaterial() { + return material; + } + + @Override + public boolean isApplicable(BlockData data) { + return data.getMaterial() == material; + } + + @Override + public void toBinary(DataOutputStream out) throws IOException { + NamespacedKey key = KeyedType.getKey(material); + out.writeUTF(key == null ? material.name() : key.toString()); + out.writeUTF(lootTable == null ? "" : lootTable.toString()); + out.writeLong(seed); + } + + @Override + public void toBukkit(Block block) { + if (lootTable == null || !(block.getState() instanceof Lootable lootable)) { + return; + } + LootTable table = Bukkit.getLootTable(lootTable); + if (table == null) { + return; + } + lootable.setLootTable(table); + lootable.setSeed(seed); + ((BlockState) lootable).update(true, false); + } + } + + private static Tag signsTag() { + return SignTags.get(); + } + + private static final class SignTags { + private static final AtomicCache> SIGNS = new AtomicCache<>(); + + private static Tag get() { + return SIGNS.aquire(() -> { + Tag signs = Bukkit.getTag("blocks", NamespacedKey.minecraft("all_signs"), Material.class); + if (signs != null) + return signs; + return new Tag<>() { + @Override + public boolean isTagged(@NotNull Material item) { + NamespacedKey key = KeyedType.getKey(item); + if (key != null) { + return key.getKey().endsWith("_sign"); + } + return item.name().toLowerCase(Locale.ROOT).endsWith("_sign"); + } + + @NotNull + @Override + public Set getValues() { + return StreamSupport.stream(Registry.MATERIAL.spliterator(), false) + .filter(this::isTagged) + .collect(Collectors.toUnmodifiableSet()); + } + + @NotNull + @Override + public NamespacedKey getKey() { + return NamespacedKey.minecraft("all_signs"); + } + }; + }); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/NativeFeatureGenerationStatus.java b/core/src/main/java/art/arcane/iris/engine/object/NativeFeatureGenerationStatus.java new file mode 100644 index 000000000..cb8fba394 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/NativeFeatureGenerationStatus.java @@ -0,0 +1,28 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +public enum NativeFeatureGenerationStatus { + GENERATE_NATIVE, + FEATURES_DISABLED, + STEP_DISABLED, + DISABLED_BY_PACK, + CYCLE_DEGRADED, + INVALID_REGISTRY_KEY +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/NativeStructureGenerationStatus.java b/core/src/main/java/art/arcane/iris/engine/object/NativeStructureGenerationStatus.java new file mode 100644 index 000000000..e022a6ba5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/NativeStructureGenerationStatus.java @@ -0,0 +1,26 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +public enum NativeStructureGenerationStatus { + GENERATE_NATIVE, + DISABLED_BY_PACK, + REPLACED_BY_IRIS, + INVALID_REGISTRY_KEY +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/NativeStructureSuppression.java b/core/src/main/java/art/arcane/iris/engine/object/NativeStructureSuppression.java new file mode 100644 index 000000000..8c3cfc178 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/NativeStructureSuppression.java @@ -0,0 +1,12 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls whether an Iris structure placement suppresses its imported native source.") +public enum NativeStructureSuppression { + @Desc("Leaves native generation enabled alongside this Iris structure placement.") + NONE, + + @Desc("Suppresses the structure's vanillaSource when used by a validated dimension-level replacement placement.") + REPLACE_SOURCE +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/NoiseStyle.java b/core/src/main/java/art/arcane/iris/engine/object/NoiseStyle.java similarity index 80% rename from core/src/main/java/com/volmit/iris/engine/object/NoiseStyle.java rename to core/src/main/java/art/arcane/iris/engine/object/NoiseStyle.java index 324268874..1a08e9afe 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/NoiseStyle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/NoiseStyle.java @@ -16,15 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.noise.CNGFactory; -import com.volmit.iris.util.noise.NoiseType; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.engine.object; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.project.noise.CNGFactory; +import art.arcane.iris.util.project.noise.NoiseType; +import art.arcane.iris.util.project.stream.ProceduralStream; @Desc("Styles of noise") public enum NoiseStyle { @Desc("White Noise is like static. Useful for block scattering but not terrain.") @@ -144,6 +143,21 @@ public enum NoiseStyle { @Desc("Cellular noise creates the same noise level for cells, changes noise level on cell borders.") CELLULAR_HERMITE(rng -> new CNG(rng, NoiseType.CELLULAR_HERMITE, 1D, 1)), + @Desc("Hexagonal cells with stable per-cell values.") + HEXAGON(rng -> new CNG(rng, NoiseType.HEXAGON, 1D, 1).scale(1)), + + @Desc("Hex James substitution pattern with 3 large and 3 small recursive hex children plus gaps.") + HEX_JAMES(rng -> new CNG(rng, NoiseType.HEX_JAMES, 1D, 1).scale(1)), + + @Desc("Interlocked solid hex cells with per-cell values from a smooth simplex heatmap.") + HEX_SIMPLEX(rng -> new CNG(rng, NoiseType.HEX_SIMPLEX, 1D, 1).scale(1)), + + @Desc("Randomized finite-depth Sierpinski-style hex recursion with per-level random 3-large/3-small James substitution.") + HEX_RANDOM_SIZE(rng -> new CNG(rng, NoiseType.HEX_RANDOM_SIZE, 1D, 1).scale(1)), + + @Desc("Sierpinski triangle mask blended with simplex heat.") + SIERPINSKI_TRIANGLE(rng -> new CNG(rng, NoiseType.SIERPINSKI_TRIANGLE, 1D, 1).scale(1)), + @Desc("Classic German Engineering") NOWHERE(rng -> CNG.signaturePerlin(rng).scale(0.776).bake()), @@ -153,6 +167,21 @@ public enum NoiseStyle { @Desc("Classic German Engineering") NOWHERE_CLOVER(rng -> CNG.signaturePerlin(rng, NoiseType.CLOVER).scale(1).bake()), + @Desc("Classic German Engineering") + NOWHERE_HEXAGON(rng -> CNG.signaturePerlin(rng, NoiseType.HEXAGON).scale(1).bake()), + + @Desc("Classic German Engineering") + NOWHERE_HEX_JAMES(rng -> CNG.signaturePerlin(rng, NoiseType.HEX_JAMES).scale(1).bake()), + + @Desc("Classic German Engineering") + NOWHERE_HEX_SIMPLEX(rng -> CNG.signaturePerlin(rng, NoiseType.HEX_SIMPLEX).scale(1).bake()), + + @Desc("Classic German Engineering") + NOWHERE_HEX_RANDOM_SIZE(rng -> CNG.signaturePerlin(rng, NoiseType.HEX_RANDOM_SIZE).scale(1).bake()), + + @Desc("Classic German Engineering") + NOWHERE_SIERPINSKI_TRIANGLE(rng -> CNG.signaturePerlin(rng, NoiseType.SIERPINSKI_TRIANGLE).scale(1).bake()), + @Desc("Classic German Engineering") NOWHERE_SIMPLEX(rng -> CNG.signaturePerlin(rng, NoiseType.SIMPLEX).scale(1).bake()), @@ -243,6 +272,21 @@ public enum NoiseStyle { @Desc("FBM Fractal Iris Noise. Single octave.") FRACTAL_FBM_IRIS_THICK(rng -> CNG.signatureThick(rng, NoiseType.FRACTAL_FBM_SIMPLEX)), + @Desc("Fractal hexagonal cell noise.") + FRACTAL_HEXAGON(rng -> new CNG(rng, NoiseType.HEXAGON, 1D, 4).scale(1)), + + @Desc("Fractal Hex James substitution pattern with recursive 3-large/3-small child hexes and gaps.") + FRACTAL_HEX_JAMES(rng -> new CNG(rng, NoiseType.HEX_JAMES, 1D, 4).scale(1)), + + @Desc("Interlocked solid hex cells with per-cell fractal simplex heatmap values.") + FRACTAL_HEX_SIMPLEX(rng -> new CNG(rng, NoiseType.HEX_SIMPLEX, 1D, 4).scale(1)), + + @Desc("Fractal hexagonal gradient noise with randomized local hex sizes and blended random/simplex heatmaps.") + FRACTAL_HEX_RANDOM_SIZE(rng -> new CNG(rng, NoiseType.HEX_RANDOM_SIZE, 1D, 4).scale(1)), + + @Desc("Fractal Sierpinski triangle mask blended with simplex heat.") + FRACTAL_SIERPINSKI_TRIANGLE(rng -> new CNG(rng, NoiseType.SIERPINSKI_TRIANGLE, 1D, 4).scale(1)), + @Desc("Rigid Multi Fractal Simplex Noise. Single octave.") FRACTAL_RM_SIMPLEX(rng -> new CNG(rng, NoiseType.FRACTAL_RIGID_MULTI_SIMPLEX, 1D, 1)), @@ -408,6 +452,54 @@ public enum NoiseStyle { @Desc("Cubic Noise") CUBIC_IRIS_THICK(rng -> CNG.signatureThick(rng, NoiseType.CUBIC).scale(256)), + @Desc("Hexagonal cell noise distorted using Iris styled wispy noise.") + HEXAGON_IRIS(rng -> CNG.signature(rng, NoiseType.HEXAGON).scale(1)), + + @Desc("Hexagonal cell noise distorted using Iris styled wispy noise.") + HEXAGON_IRIS_DOUBLE(rng -> CNG.signatureDouble(rng, NoiseType.HEXAGON).scale(1)), + + @Desc("Hexagonal cell noise distorted using Iris styled wispy noise.") + HEXAGON_IRIS_THICK(rng -> CNG.signatureThick(rng, NoiseType.HEXAGON).scale(1)), + + @Desc("Hexagonal cell noise distorted using Iris styled wispy noise.") + HEXAGON_IRIS_HALF(rng -> CNG.signatureHalf(rng, NoiseType.HEXAGON).scale(1)), + + @Desc("Hex James substitution pattern distorted using Iris styled wispy noise.") + HEX_JAMES_IRIS(rng -> CNG.signature(rng, NoiseType.HEX_JAMES).scale(1)), + + @Desc("Hex James substitution pattern distorted using Iris styled wispy noise.") + HEX_JAMES_IRIS_DOUBLE(rng -> CNG.signatureDouble(rng, NoiseType.HEX_JAMES).scale(1)), + + @Desc("Hex James substitution pattern distorted using Iris styled wispy noise.") + HEX_JAMES_IRIS_THICK(rng -> CNG.signatureThick(rng, NoiseType.HEX_JAMES).scale(1)), + + @Desc("Hex James substitution pattern distorted using Iris styled wispy noise.") + HEX_JAMES_IRIS_HALF(rng -> CNG.signatureHalf(rng, NoiseType.HEX_JAMES).scale(1)), + + @Desc("Interlocked solid hex-cell simplex heatmap distorted using Iris styled wispy noise.") + HEX_SIMPLEX_IRIS(rng -> CNG.signature(rng, NoiseType.HEX_SIMPLEX).scale(1)), + + @Desc("Interlocked solid hex-cell simplex heatmap distorted using Iris styled wispy noise.") + HEX_SIMPLEX_IRIS_DOUBLE(rng -> CNG.signatureDouble(rng, NoiseType.HEX_SIMPLEX).scale(1)), + + @Desc("Interlocked solid hex-cell simplex heatmap distorted using Iris styled wispy noise.") + HEX_SIMPLEX_IRIS_THICK(rng -> CNG.signatureThick(rng, NoiseType.HEX_SIMPLEX).scale(1)), + + @Desc("Interlocked solid hex-cell simplex heatmap distorted using Iris styled wispy noise.") + HEX_SIMPLEX_IRIS_HALF(rng -> CNG.signatureHalf(rng, NoiseType.HEX_SIMPLEX).scale(1)), + + @Desc("Hexagonal random-size gradient noise and distorted using Iris styled wispy noise.") + HEX_RANDOM_SIZE_IRIS(rng -> CNG.signature(rng, NoiseType.HEX_RANDOM_SIZE).scale(1)), + + @Desc("Hexagonal random-size gradient noise and distorted using Iris styled wispy noise.") + HEX_RANDOM_SIZE_IRIS_DOUBLE(rng -> CNG.signatureDouble(rng, NoiseType.HEX_RANDOM_SIZE).scale(1)), + + @Desc("Hexagonal random-size gradient noise and distorted using Iris styled wispy noise.") + HEX_RANDOM_SIZE_IRIS_THICK(rng -> CNG.signatureThick(rng, NoiseType.HEX_RANDOM_SIZE).scale(1)), + + @Desc("Hexagonal random-size gradient noise and distorted using Iris styled wispy noise.") + HEX_RANDOM_SIZE_IRIS_HALF(rng -> CNG.signatureHalf(rng, NoiseType.HEX_RANDOM_SIZE).scale(1)), + @Desc("Cellular noise creates the same noise level for cells, changes noise level on cell borders. Cells are distorted using Iris styled wispy noise.") CELLULAR_IRIS(rng -> CNG.signature(rng, NoiseType.CELLULAR)), diff --git a/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java b/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java new file mode 100644 index 000000000..1160425a9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java @@ -0,0 +1,108 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Object Place modes are useful for positioning objects just right. The default value is CENTER_HEIGHT.") +public enum ObjectPlaceMode { + @Desc("The default place mode. This mode picks a center point (where the center of the object will be) and takes the height. That height is used for the whole object.") + + CENTER_HEIGHT, + + @Desc("Samples a lot of points where the object will cover (horizontally) and picks the highest height, that height is then used to place the object. This mode is useful for preventing any part of your object from being buried though it will float off of cliffs.") + + MAX_HEIGHT, + + @Desc("Samples only 4 points where the object will cover (horizontally) and picks the highest height, that height is then used to place the object. This mode is useful for preventing any part of your object from being buried though it will float off of cliffs.\"") + + FAST_MAX_HEIGHT, + + @Desc("Samples a lot of points where the object will cover (horizontally) and picks the lowest height, that height is then used to place the object. This mode is useful for preventing any part of your object from overhanging a cliff though it gets buried a lot") + + MIN_HEIGHT, + + @Desc("Samples only 4 points where the object will cover (horizontally) and picks the lowest height, that height is then used to place the object. This mode is useful for preventing any part of your object from overhanging a cliff though it gets buried a lot") + + FAST_MIN_HEIGHT, + + @Desc("Stilting is MAX_HEIGHT but it repeats the bottom most block of your object until it hits the surface. This is expensive because it has to first sample every height value for each x,z position of your object. Avoid using this unless its structures for performance reasons.") + + STILT, + + @Desc("Just like stilting but very inaccurate. Useful for stilting a lot of objects without too much care on accuracy (you can use the over-stilt value to force stilts under ground further)") + + FAST_STILT, + + @Desc("Stilting is MIN_HEIGHT but it repeats the bottom most block of your object until it hits the surface. This is expensive because it has to first sample every height value for each x,z position of your object. Avoid using this unless its structures for performance reasons.") + + MIN_STILT, + + @Desc("Just like MIN_STILT but very inaccurate. Useful for stilting a lot of objects without too much care on accuracy (you can use the over-stilt value to force stilts under ground further)") + + FAST_MIN_STILT, + + @Desc("Stilting is CENTER_HEIGHT but it repeats the bottom most block of your object until it hits the surface. This is expensive because it has to first sample every height value for each x,z position of your object. Avoid using this unless its structures for performance reasons.") + + CENTER_STILT, + + @Desc("Erode stilting tapers columns downward like an ice cream cone. Blocks near the center extend deepest while edge blocks drop off first. Blocks are randomly skipped in the lower portion for a rough organic eroded texture.") + + ERODE_STILT, + + @Desc("Organic stilting scans straight down from the bottom of the object to the first solid block (the cave floor, or terrain) and fills the gap with the object's own bottom block. Each column stops at a slightly different, noise-varied depth and the lower portion is randomly broken up, so the underside reads as rough organic roots rather than a flat disc. Designed for objects placed inside caves so they connect to the floor instead of floating. Tune with stilt-settings (organicMaxScan, organicJitter, organicScratch).") + + ORGANIC_STILT, + + @Desc("Hangs the object upside-down from a cave ceiling. The object is flipped vertically (its tip points down like a stalactite) and its top is anchored to the ceiling, while an organic, noise-varied stilt grows up into the ceiling so it never floats below the roof. Use inside caves; tune with stilt-settings (organicMaxScan, organicJitter, organicScratch).") + + CEILING_HANG, + + @Desc("Anchors the object at the terrain surface (plus translate.y) like CENTER_HEIGHT, then bends the surrounding terrain to be flush with the object's base. Columns inside the object footprint are raised (or lowered) to meet the base, and the deformation falls off parabolically out to a radius so the terrain smoothly blends back to its natural height. Use this to seat a floating/translated object on the ground without a flat stilt disc, e.g. a cube translated up 10 on a mountain pulls the terrain up to meet it. Tune with vacuum-settings (radius, falloff).") + + VACUUM, + + @Desc("VACUUM with a larger radius and finer per-column sampling for the smoothest terrain blend. More expensive.") + + VACUUM_HIGH, + + @Desc("VACUUM with a smaller radius and coarser sampling for performance. Use when seating many objects.") + + VACUUM_FAST, + + @Desc("VACUUM whose falloff radius is perturbed by noise per column, so the terrain meets the object with an irregular organic edge rather than a clean parabolic bowl.") + + VACUUM_ORGANIC, + + @Desc("VACUUM whose terrain bend is bent by smooth coherent simplex noise so the surface meets the object with a gently rolling, wavy slope instead of the ragged white-noise edge of VACUUM_ORGANIC or a clean parabolic bowl. The object itself still seats flush; the waves fade to zero under the object and at the outer rim, peaking across the mid-slope. Tune with vacuum-settings (waveAmplitude, waveScale).") + + VACUUM_WAVY, + + @Desc("Samples the height of the terrain at every x,z position of your object and pushes it down to the surface. It's pretty much like a melt function over the terrain.") + + PAINT, + + @Desc("Places the object in pure air at an absolute Y driven entirely by translate.y (plus optional translate.yRandom). Terrain height, underwater, and carving anchor checks are skipped. Use this for floating islands, sky structures, clouds, or blimps where the object must not be translated to the ground.") + + FLOATING, + + @Desc("Raw stamp at the caller-supplied (x, y, z). No terrain sampling, no stilting, no Y recomputation, no underwater or carving anchor guards. Used internally to route native Minecraft structure pieces (villages etc.) through the Iris object placer.") + + STRUCTURE_PIECE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/OverrideMode.java b/core/src/main/java/art/arcane/iris/engine/object/OverrideMode.java new file mode 100644 index 000000000..feb23578d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/OverrideMode.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls how an override list is combined with the inherited object set from the target biome.") +public enum OverrideMode { + @Desc("Ignore the override list entirely. Use only the inherited objects from the target biome (subject to inheritObjects).") + INHERIT_ONLY, + + @Desc("Append override list entries after the inherited objects from the target biome. Both sets are placed.") + MERGE, + + @Desc("Use only the override list. The inherited objects from the target biome are discarded regardless of inheritObjects.") + REPLACE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/PotionEffectTypes.java b/core/src/main/java/art/arcane/iris/engine/object/PotionEffectTypes.java new file mode 100644 index 000000000..ef5fafe57 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/PotionEffectTypes.java @@ -0,0 +1,98 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.util.common.reflect.KeyedType; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.potion.PotionEffectType; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +public final class PotionEffectTypes { + private static final Map LEGACY_ALIASES; + private static final Set MISSING_WARNED = ConcurrentHashMap.newKeySet(); + + static { + Map m = new HashMap<>(); + m.put("SLOW", "SLOWNESS"); + m.put("FAST_DIGGING", "HASTE"); + m.put("SLOW_DIGGING", "MINING_FATIGUE"); + m.put("INCREASE_DAMAGE", "STRENGTH"); + m.put("HEAL", "INSTANT_HEALTH"); + m.put("HARM", "INSTANT_DAMAGE"); + m.put("JUMP", "JUMP_BOOST"); + m.put("CONFUSION", "NAUSEA"); + m.put("DAMAGE_RESISTANCE", "RESISTANCE"); + LEGACY_ALIASES = Collections.unmodifiableMap(m); + } + + private PotionEffectTypes() { + } + + public static String normalize(String input) { + if (input == null) { + return ""; + } + String upper = input.trim().toUpperCase(Locale.ROOT).replace(' ', '_'); + if (upper.contains(":")) { + upper = upper.substring(upper.indexOf(':') + 1); + } + return LEGACY_ALIASES.getOrDefault(upper, upper); + } + + public static PotionEffectType resolve(String rawName) { + if (rawName == null || rawName.trim().isEmpty()) { + return null; + } + String wanted = normalize(rawName); + for (PotionEffectType i : Registry.EFFECT) { + NamespacedKey key = KeyedType.getKey(i); + if (key == null) { + continue; + } + String candidate = key.getKey().toUpperCase(Locale.ROOT).replace(' ', '_'); + if (candidate.equals(wanted)) { + return i; + } + } + return null; + } + + public static String knownTypesList() { + TreeSet names = new TreeSet<>(); + for (PotionEffectType i : Registry.EFFECT) { + NamespacedKey key = KeyedType.getKey(i); + if (key != null) { + names.add(key.getKey().toUpperCase(Locale.ROOT).replace(' ', '_')); + } + } + return String.join(", ", names); + } + + public static boolean shouldWarn(String rawName) { + return MISSING_WARNED.add(normalize(rawName)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/StructureDistribution.java b/core/src/main/java/art/arcane/iris/engine/object/StructureDistribution.java new file mode 100644 index 000000000..ff396886c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/StructureDistribution.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Controls how structure start positions are scattered across the world.") +public enum StructureDistribution { + @Desc("Vanilla-style spacing/separation grid. One placement attempt per grid cell, randomly offset within it by the separation. Use spacing, separation and salt.") + RANDOM_SPREAD, + + @Desc("A per-chunk probability roll, like object density. Use density (0-1).") + DENSITY, + + @Desc("Concentric rings around the world origin, like vanilla strongholds. Use rings.") + CONCENTRIC_RINGS +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java b/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java new file mode 100644 index 000000000..b96769fd8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java @@ -0,0 +1,67 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import art.arcane.iris.engine.platform.studio.generators.BiomeBuffetGenerator; +import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator; + +@Desc("Represents a studio mode") +public enum StudioMode { + @Desc("Installs no studio generator; the dimension generates normally.") + NORMAL, + + @Desc("Debug layout: every biome in the dimension on a square grid, one chunk per biome cell, barrier floor past the last biome. Bukkit studio worlds only.") + BIOME_BUFFET_1x1, + + @Desc("Debug layout: every biome on a square grid with 3x3-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") + BIOME_BUFFET_3x3, + + @Desc("Debug layout: every biome on a square grid with 5x5-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") + BIOME_BUFFET_5x5, + + @Desc("Debug layout: every biome on a square grid with 9x9-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") + BIOME_BUFFET_9x9, + + @Desc("Debug layout: every biome on a square grid with 18x18-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") + BIOME_BUFFET_18x18, + + @Desc("Debug layout: every biome on a square grid with 36x36-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") + BIOME_BUFFET_36x36, + + @Desc("Deprecated: generates exactly like NORMAL and will be removed in a future release.") + REGION_BUFFET, + + @Desc("Replaces terrain with the object studio: a flat polished-deepslate floor laying every pack object out on framed, end-rod-marked grid plinths. Bukkit studio worlds only.") + OBJECT_BUFFET; + + public void inject(BukkitChunkGenerator c) { + switch (this) { + case NORMAL, REGION_BUFFET -> c.setStudioGenerator(null); + case BIOME_BUFFET_1x1 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 1)); + case BIOME_BUFFET_3x3 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 3)); + case BIOME_BUFFET_5x5 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 5)); + case BIOME_BUFFET_9x9 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 9)); + case BIOME_BUFFET_18x18 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 18)); + case BIOME_BUFFET_36x36 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 36)); + case OBJECT_BUFFET -> c.setStudioGenerator(new ObjectStudioGenerator(c.getEngine())); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/TileData.java b/core/src/main/java/art/arcane/iris/engine/object/TileData.java new file mode 100644 index 000000000..cea2c1051 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/TileData.java @@ -0,0 +1,300 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.Strictness; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.util.common.reflect.KeyedType; +import art.arcane.volmlib.util.collection.KMap; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.Block; +import org.bukkit.block.TileState; +import org.bukkit.block.data.BlockData; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +@SuppressWarnings("ALL") +@Getter +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TileData implements Cloneable { + private static final Gson gson = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE).create(); + /** + * Memoized {@link #resolveMaterial()}. Pasting one tile resolves its material at least twice + * ({@link #isApplicable(BlockData)} then {@link #toBukkit(Block)}), and matchMaterial normalizes the string and + * then hits the Bukkit registry - an object with thousands of tiles paid for that thousands of times. + *

+ * Static and keyed by the stored key rather than an instance field for two reasons: distinct tiles overwhelmingly + * repeat a handful of keys, so the cache is far more effective shared; and an instance field of a Bukkit type - + * transient or not - fails the pack purity gate, because {@code Class#getDeclaredFields()} resolves every declared + * field type eagerly, so one such field makes this class unloadable on Fabric/Forge/NeoForge. Static fields are + * never part of that walk. Bounded by the number of distinct material keys a pack can name. + */ + private static final Map RESOLVED_MATERIALS = new ConcurrentHashMap<>(); + private static final boolean BUKKIT_PRESENT = detectBukkit(); + private static volatile TileReader PLATFORM_READER = null; + private static volatile TileFactory PLATFORM_FACTORY = null; + + public interface TileReader { + TileData read(DataInputStream in) throws IOException; + } + + public interface TileFactory { + TileData create(PlatformBlockState state, KMap properties); + } + + public static synchronized TileReader bindPlatformReader(TileReader reader) { + TileReader previous = PLATFORM_READER; + PLATFORM_READER = Objects.requireNonNull(reader, "reader"); + return previous; + } + + public static synchronized TileFactory bindPlatformFactory(TileFactory factory) { + TileFactory previous = PLATFORM_FACTORY; + PLATFORM_FACTORY = Objects.requireNonNull(factory, "factory"); + return previous; + } + + public static synchronized void restorePlatformReader(TileReader reader) { + PLATFORM_READER = reader; + } + + public static synchronized void restorePlatformFactory(TileFactory factory) { + PLATFORM_FACTORY = factory; + } + + private static boolean detectBukkit() { + try { + Class.forName("org.bukkit.Bukkit", false, TileData.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + /** + * The block key this tile belongs to, stored as text so the field type never drags + * org.bukkit.Material onto a Gson field walk or into generated equals/hashCode/toString. + *

+ * Gson data-compat: Material serialized as its enum name, so the same JSON member name with a + * String type parses every existing pack byte-for-byte. Persisted values are either a legacy + * uppercase enum name ("CHEST") or a namespaced key ("minecraft:chest"); both are accepted at + * the Bukkit resolution edge in {@link #resolveMaterial()}. + */ + @Getter(AccessLevel.NONE) + @NonNull + private String material; + @NonNull + private KMap properties; + + /** + * The platform-neutral block key. Safe on every platform - use this instead of resolving a + * Material anywhere outside the Bukkit adapter. + */ + public String getMaterialKey() { + return material; + } + + public static boolean setTileState(Block block, TileData data) { + if (block.getState() instanceof TileState && data.isApplicable(block.getBlockData())) + return data.toBukkitTry(block); + return false; + } + + public static TileData getTileState(Block block, boolean useLegacy) { + if (!BukkitPlatform.hasTile(block.getType())) + return null; + if (useLegacy) { + LegacyTileData legacy = LegacyTileData.fromBukkit(block.getState()); + if (legacy != null) + return legacy; + } + + return new TileData().fromBukkit(block); + } + + public static TileData of(PlatformBlockState state, KMap properties) { + if (!BUKKIT_PRESENT) { + return requirePlatformFactory(PLATFORM_FACTORY).create(state, properties); + } + Object handle = state.nativeHandle(); + if (!(handle instanceof BlockData blockData)) { + return null; + } + return new TileData(materialKey(blockData.getMaterial()), properties); + } + + public static TileData read(DataInputStream in) throws IOException { + if (!BUKKIT_PRESENT) { + return requirePlatformReader(PLATFORM_READER).read(in); + } + if (!in.markSupported()) + throw new IOException("Mark not supported"); + in.mark(Integer.MAX_VALUE); + try { + // Resolving the material is the modern/legacy stream discriminator: an unresolvable + // first UTF means these bytes are a LegacyTileData record, not a modern one. + Material resolved = Material.matchMaterial(in.readUTF()); + if (resolved == null) + throw new IOException("Not a modern tile record"); + return new TileData(materialKey(resolved), gson.fromJson(in.readUTF(), KMap.class)); + } catch (Throwable e) { + in.reset(); + return new LegacyTileData(in); + } finally { + in.mark(0); + } + } + + /** + * Bukkit resolution edge: canonicalizes a Material to the namespaced key form that + * {@link #toBinary(DataOutputStream)} has always written, falling back to the enum name. + */ + static String materialKey(Material material) { + if (material == null) { + return ""; + } + NamespacedKey key = KeyedType.getKey(material); + return key == null ? material.name() : key.toString(); + } + + /** + * Bukkit resolution edge. Accepts both persisted forms: the legacy uppercase enum name + * ("CHEST") and the namespaced key ("minecraft:chest") - matchMaterial handles both. + */ + public Material resolveMaterial() { + if (material == null || material.isEmpty()) { + return null; + } + + Material cached = RESOLVED_MATERIALS.get(material); + + if (cached != null) { + return cached; + } + + Material resolved = Material.matchMaterial(material); + + if (resolved != null) { + RESOLVED_MATERIALS.put(material, resolved); + } + + return resolved; + } + + static TileFactory requirePlatformFactory(TileFactory factory) { + if (factory == null) { + throw new IllegalStateException("No platform tile-data factory is bound"); + } + return factory; + } + + static TileReader requirePlatformReader(TileReader reader) throws IOException { + if (reader == null) { + throw new IOException("No platform tile-data reader is bound"); + } + return reader; + } + + public boolean isApplicable(BlockData data) { + Material resolved = resolveMaterial(); + return resolved != null && data.getMaterial() == resolved; + } + + public void toBukkit(Block block) { + Material resolved = resolveMaterial(); + if (resolved == null) throw new IllegalStateException("Material not set: " + material); + if (block.getType() != resolved) + throw new IllegalStateException("Material mismatch: " + block.getType() + " vs " + resolved); + BukkitPlatform.deserializeTile(properties, block.getLocation()); + } + + public TileData fromBukkit(Block block) { + if (material != null && !material.isEmpty()) { + Material resolved = resolveMaterial(); + if (block.getType() != resolved) + throw new IllegalStateException("Material mismatch: " + block.getType() + " vs " + material); + } else { + material = materialKey(block.getType()); + } + properties = BukkitPlatform.serializeTile(block.getLocation()); + return this; + } + + public boolean toBukkitTry(Block block) { + try { + //noinspection unchecked + toBukkit(block); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + return false; + } + + public boolean fromBukkitTry(Block block) { + try { + //noinspection unchecked + fromBukkit(block); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + + } + + return false; + } + + public void toBinary(DataOutputStream out) throws IOException { + // The field already holds the canonical key form that this stream has always carried. + out.writeUTF(material == null ? "" : material); + out.writeUTF(gson.toJson(properties)); + } + + @Override + public TileData clone() { + TileData clone = new TileData(); + clone.material = material; + clone.properties = properties.copy(); //TODO make a deep copy + return clone; + } + + @Override + public String toString() { + return String.valueOf(material) + gson.toJson(properties); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/TopShapeMode.java b/core/src/main/java/art/arcane/iris/engine/object/TopShapeMode.java new file mode 100644 index 000000000..8dcf8028a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/TopShapeMode.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("How the top profile of a floating-child island is shaped.") +public enum TopShapeMode { + @Desc("Evaluate the target biome's own terrain generators to build the island top. Mountains biome produces real peaks, desert produces dunes, plains is flat. Recommended default.") + BIOME, + + @Desc("Drive the top profile from topShapeStyle noise, independent of the target biome's generators. Amplitude controlled by topShapeAmp.") + NOISE, + + @Desc("Flat slab on top, topHeight blocks above the base. Ignores noise and biome generators.") + FLAT +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/ArrayType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/ArrayType.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/ArrayType.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/ArrayType.java index dfb46e3af..8c07ecbe1 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/ArrayType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/ArrayType.java @@ -16,12 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/DependsOn.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/DependsOn.java similarity index 95% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/DependsOn.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/DependsOn.java index 6d6ecd252..05c175a76 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/DependsOn.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/DependsOn.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/Desc.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/Desc.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/Desc.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/Desc.java index 9cec49518..20f7887e0 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/Desc.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/Desc.java @@ -16,12 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/MaxNumber.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/MaxNumber.java similarity index 95% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/MaxNumber.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/MaxNumber.java index b2c78c577..a50844724 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/MaxNumber.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/MaxNumber.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/MinNumber.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/MinNumber.java similarity index 95% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/MinNumber.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/MinNumber.java index 067e6f597..1e286d941 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/MinNumber.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/MinNumber.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiome.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiome.java new file mode 100644 index 000000000..538c1b367 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiome.java @@ -0,0 +1,37 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Marks a String field (or a list of Strings) as a live biome-registry key, so schema completion offers every + * vanilla, datapack, and mod biome instead of nothing. + */ +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListBiome { + +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListBlockType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBlockType.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListBlockType.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBlockType.java index 07170f0cf..9c698782d 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListBlockType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBlockType.java @@ -16,12 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEnchantment.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEnchantment.java new file mode 100644 index 000000000..918fe7343 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEnchantment.java @@ -0,0 +1,14 @@ +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListEnchantment { +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEntityType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEntityType.java new file mode 100644 index 000000000..74473f2c5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEntityType.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListEntityType { + +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListFont.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFont.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListFont.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFont.java index 0d3b743fd..83a8dc660 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListFont.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFont.java @@ -16,12 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFunction.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFunction.java new file mode 100644 index 000000000..d1157b77a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFunction.java @@ -0,0 +1,18 @@ +package art.arcane.iris.engine.object.annotations; + +import art.arcane.iris.engine.framework.ListFunction; +import art.arcane.volmlib.util.collection.KList; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListFunction { + Class>> value(); +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListItemType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListItemType.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListItemType.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListItemType.java index c3420c2b8..b1ff189dd 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListItemType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListItemType.java @@ -16,12 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListNativeJigsawPool.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListNativeJigsawPool.java new file mode 100644 index 000000000..8fcf6d18d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListNativeJigsawPool.java @@ -0,0 +1,14 @@ +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListNativeJigsawPool { +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java new file mode 100644 index 000000000..9b8493c8f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java @@ -0,0 +1,14 @@ +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListPotionEffect { +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListResource.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListResource.java similarity index 79% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListResource.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListResource.java index d5fab0f82..7f8e40b19 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListResource.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListResource.java @@ -16,14 +16,16 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; -import com.volmit.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.loader.IrisRegistrant; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListSpecialEntity.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListSpecialEntity.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListSpecialEntity.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListSpecialEntity.java index 53976a3cb..528c33c47 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListSpecialEntity.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListSpecialEntity.java @@ -16,12 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java new file mode 100644 index 000000000..8f985907c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListStructure { + +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java new file mode 100644 index 000000000..58bd66442 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java @@ -0,0 +1,40 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListVanillaStructure { + /** + * When true the field accepts family/namespace prefixes in addition to exact registered keys + * (the {@code IrisImportedStructureControl.matchesKey} contract: "minecraft:village" matches + * every village variant, "nova_structures:" matches a whole namespace). The generated editor + * schema then validates entries against the registry enum OR a key/prefix pattern instead of + * rejecting anything that is not an exact registered key. + */ + boolean prefixes() default false; +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java new file mode 100644 index 000000000..e877e6ff5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListVanillaStructureSet { + +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryMapBlockState.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryMapBlockState.java similarity index 85% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryMapBlockState.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryMapBlockState.java index 4bcf14cef..c313daf93 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryMapBlockState.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryMapBlockState.java @@ -1,4 +1,4 @@ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/Required.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/Required.java similarity index 82% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/Required.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/Required.java index 50035d8a0..a449f470c 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/Required.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/Required.java @@ -16,12 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/Snippet.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/Snippet.java similarity index 95% rename from core/src/main/java/com/volmit/iris/engine/object/annotations/Snippet.java rename to core/src/main/java/art/arcane/iris/engine/object/annotations/Snippet.java index 3c17225e9..980e818bd 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/Snippet.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/Snippet.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.object.annotations; +package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/ComponentFlagFunction.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/ComponentFlagFunction.java new file mode 100644 index 000000000..eabc3086e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/ComponentFlagFunction.java @@ -0,0 +1,42 @@ +package art.arcane.iris.engine.object.annotations.functions; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.ListFunction; +import art.arcane.iris.engine.mantle.ComponentFlag; +import art.arcane.iris.engine.mantle.MantleComponent; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.io.JarScanner; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; + +import java.util.Objects; + +public class ComponentFlagFunction implements ListFunction> { + @Override + public String key() { + return "component-flag"; + } + + @Override + public String fancyName() { + return "Component Flag"; + } + + @Override + public KList apply(IrisData data) { + var engine = data.getEngine(); + if (engine != null) return engine.getMantle().getComponentFlags().toStringList(); + JarScanner scanner = new JarScanner(IrisPlatforms.get().pluginJar(), "art.arcane.iris.engine.mantle.components"); + J.attempt(scanner::scan); + return scanner.getClasses() + .stream() + .filter(c -> c.isAnnotationPresent(ComponentFlag.class)) + .filter(MantleComponent.class::isAssignableFrom) + .map(c -> c.getDeclaredAnnotation(ComponentFlag.class)) + .filter(Objects::nonNull) + .map(ComponentFlag::value) + .map(MantleFlag::toString) + .collect(KList.collector()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/LootTableKeyFunction.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/LootTableKeyFunction.java new file mode 100644 index 000000000..37107e219 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/LootTableKeyFunction.java @@ -0,0 +1,23 @@ +package art.arcane.iris.engine.object.annotations.functions; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.ListFunction; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.collection.KList; + +public class LootTableKeyFunction implements ListFunction> { + @Override + public String key() { + return "loot-table-key"; + } + + @Override + public String fancyName() { + return "LootTable Key"; + } + + @Override + public KList apply(IrisData data) { + return new KList<>(IrisPlatforms.get().registries().lootTableKeys()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/coral/CoralCanvas.java b/core/src/main/java/art/arcane/iris/engine/object/coral/CoralCanvas.java new file mode 100644 index 000000000..b9462f457 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/coral/CoralCanvas.java @@ -0,0 +1,64 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.coral; + +import java.util.HashMap; +import java.util.Map; + +public final class CoralCanvas { + public enum Role { + STRUCTURE, + TIP + } + + private static final long BIAS = 1L << 20; + private static final long MASK = (1L << 21) - 1L; + + private final Map cells = new HashMap<>(); + + public void set(int x, int y, int z, Role role) { + if (y < 0) { + return; + } + long key = encode(x, y, z); + Role existing = cells.get(key); + if (existing == Role.TIP && role == Role.STRUCTURE) { + return; + } + cells.put(key, role); + } + + public Map getCells() { + return cells; + } + + public static long encode(int x, int y, int z) { + long ex = (x + BIAS) & MASK; + long ey = (y + BIAS) & MASK; + long ez = (z + BIAS) & MASK; + return (ex << 42) | (ey << 21) | ez; + } + + public static int[] decode(long key) { + int ez = (int) ((key & MASK) - BIAS); + int ey = (int) (((key >> 21) & MASK) - BIAS); + int ex = (int) (((key >> 42) & MASK) - BIAS); + return new int[]{ex, ey, ez}; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/coral/CoralGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/coral/CoralGenerator.java new file mode 100644 index 000000000..dde2938f5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/coral/CoralGenerator.java @@ -0,0 +1,263 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.coral; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisCoral; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.engine.object.tree.TreeFunctions; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +import java.util.HashMap; +import java.util.Map; + +public final class CoralGenerator { + private static final double GOLDEN_ANGLE = 137.50776405003785; + + private CoralGenerator() { + } + + public static IrisObject generate(IrisCoral coral, int variantIndex, RNG rng, IrisData data) { + int lo = Math.min(coral.getHeightMin(), coral.getHeightMax()); + int hi = Math.max(coral.getHeightMin(), coral.getHeightMax()); + int height = Math.max(2, rng.i(lo, hi + 1)); + long shapeSeed = rng.getSeed() + variantIndex * 31L; + + CoralCanvas canvas = new CoralCanvas(); + switch (coral.getForm()) { + case BRANCHING -> buildBranching(canvas, coral, height, shapeSeed, rng); + case FAN -> buildFan(canvas, coral, height, shapeSeed); + case BRAIN -> buildBrain(canvas, coral, height, shapeSeed); + case PILLAR -> buildPillar(canvas, coral, height, shapeSeed); + case TENDRIL -> buildTendril(canvas, coral, height, shapeSeed, rng); + } + + Map resolved = resolve(canvas, coral, data); + return IrisProceduralBlocks.assemble(resolved); + } + + private static void buildBranching(CoralCanvas canvas, IrisCoral coral, int height, long seed, RNG rng) { + int stalkTop = Math.max(1, (int) Math.round(height * 0.5)); + for (int y = 0; y < stalkTop; y++) { + placeWavy(canvas, coral, 0, y, 0, y, seed, CoralCanvas.Role.STRUCTURE); + } + + int count = Math.max(1, coral.getBranchCount()); + for (int b = 0; b < count; b++) { + double az = branchAzimuth(coral, b, count, rng); + int startY = stalkTop - 1 + (int) Math.round((b % 2) * 1.0); + double[] tip = rasterizeArm(canvas, coral, 0, startY, 0, az, + coral.getBranchElevation(), coral.getBranchLength(), seed + b * 131L); + + placeTipCluster(canvas, coral, (int) Math.round(tip[0]), (int) Math.round(tip[1]), (int) Math.round(tip[2]), + coral.getTipClusterRadius(), seed + b * 977L); + + if (coral.isSubBranches()) { + int subCount = Math.max(1, coral.getSubBranchCount()); + for (int s = 0; s < subCount; s++) { + double subAz = az + (s - (subCount - 1) / 2.0) * 35.0; + double subLen = coral.getBranchLength() * coral.getSubBranchScale(); + double[] subTip = rasterizeArm(canvas, coral, + (int) Math.round(tip[0]), (int) Math.round(tip[1]), (int) Math.round(tip[2]), + subAz, coral.getBranchElevation() + 10.0, subLen, seed + b * 131L + s * 17L + 5000L); + placeTipCluster(canvas, coral, (int) Math.round(subTip[0]), (int) Math.round(subTip[1]), (int) Math.round(subTip[2]), + Math.max(0, coral.getTipClusterRadius() - 1), seed + b * 977L + s * 53L); + } + } + } + } + + private static void buildFan(CoralCanvas canvas, IrisCoral coral, int height, long seed) { + int half = Math.max(1, coral.getFanWidth()); + for (int y = 0; y < height; y++) { + double t = y / (double) Math.max(1, height - 1); + double profile = Math.sin(Math.PI * t); + int w = (int) Math.round(half * profile); + for (int dx = -w; dx <= w; dx++) { + canvas.set(dx, y, 0, CoralCanvas.Role.STRUCTURE); + } + } + for (int dx = -half; dx <= half; dx++) { + placeTip(canvas, coral, dx, height - 1, 0, seed + dx); + } + } + + private static void buildBrain(CoralCanvas canvas, IrisCoral coral, int height, long seed) { + int r = Math.max(1, coral.getBrainRadius()); + int ry = Math.max(1, Math.min(r, (int) Math.round(height * 0.6))); + double rough = coral.getBrainRoughness(); + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + for (int dy = 0; dy <= ry * 2; dy++) { + double nx = dx / (double) r; + double ny = (dy - ry) / (double) ry; + double nz = dz / (double) r; + double d = nx * nx + ny * ny + nz * nz; + double wobble = (TreeFunctions.valueNoise3D(dx, dy, dz, seed) - 0.5) * 2.0 * rough; + if (d <= 1.0 + wobble) { + canvas.set(dx, dy, dz, CoralCanvas.Role.STRUCTURE); + } + } + } + } + } + + private static void buildPillar(CoralCanvas canvas, IrisCoral coral, int height, long seed) { + int r = Math.max(1, coral.getPillarRadius()); + for (int y = 0; y < height; y++) { + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + if (dx * dx + dz * dz <= r * r) { + canvas.set(dx, y, dz, CoralCanvas.Role.STRUCTURE); + } + } + } + } + placeTipCluster(canvas, coral, 0, height - 1, 0, coral.getTipClusterRadius(), seed); + } + + private static void buildTendril(CoralCanvas canvas, IrisCoral coral, int height, long seed, RNG rng) { + int count = Math.max(1, coral.getTendrilCount()); + double radius = Math.max(0.0, coral.getSpread()); + for (int c = 0; c < count; c++) { + double az = Math.toRadians(c * (360.0 / count)); + double bx = Math.cos(az) * radius * 0.5; + double bz = Math.sin(az) * radius * 0.5; + int tx = (int) Math.round(bx); + int tz = (int) Math.round(bz); + int top = 0; + for (int y = 0; y < height; y++) { + placeWavy(canvas, coral, tx, y, tz, y * (c + 1), seed + c * 311L, CoralCanvas.Role.STRUCTURE); + top = y; + } + placeTip(canvas, coral, tx, top, tz, seed + c * 311L); + } + } + + private static double[] rasterizeArm(CoralCanvas canvas, IrisCoral coral, int ox, int oy, int oz, + double azimuthDeg, double elevationDeg, double length, long seed) { + double azRad = Math.toRadians(azimuthDeg); + double elRad = Math.toRadians(elevationDeg); + double dx = length * Math.cos(elRad) * Math.sin(azRad); + double dy = length * Math.sin(elRad); + double dz = length * Math.cos(elRad) * Math.cos(azRad); + int steps = Math.max(1, (int) Math.round(Math.max(Math.abs(dy), Math.max(Math.abs(dx), Math.abs(dz))))); + + double[] tip = new double[]{ox, oy, oz}; + for (int i = 0; i <= steps; i++) { + double t = i / (double) steps; + double sway = swayOffset(coral, t * length, seed); + int x = (int) Math.round(ox + dx * t + sway); + int y = (int) Math.round(oy + dy * t); + int z = (int) Math.round(oz + dz * t - sway); + canvas.set(x, y, z, CoralCanvas.Role.STRUCTURE); + tip = new double[]{x, y, z}; + } + return tip; + } + + private static void placeWavy(CoralCanvas canvas, IrisCoral coral, int x, int y, int z, double phase, long seed, CoralCanvas.Role role) { + double sway = swayOffset(coral, phase, seed); + int sx = (int) Math.round(sway); + int sz = (int) Math.round(swayOffset(coral, phase + 53.0, seed + 7L)); + canvas.set(x + sx, y, z + sz, role); + } + + private static double swayOffset(IrisCoral coral, double phase, long seed) { + double amount = Math.max(0.0, Math.min(1.0, coral.getSway())); + if (amount <= 0.0) { + return 0.0; + } + double n = TreeFunctions.valueNoise1D(phase * 0.35, seed) - 0.5; + return n * 2.0 * amount * (1.0 + coral.getSpread() * 0.25); + } + + private static void placeTipCluster(CoralCanvas canvas, IrisCoral coral, int cx, int cy, int cz, int radius, long seed) { + if (radius <= 0) { + placeTip(canvas, coral, cx, cy, cz, seed); + return; + } + for (int dx = -radius; dx <= radius; dx++) { + for (int dy = -radius; dy <= radius; dy++) { + for (int dz = -radius; dz <= radius; dz++) { + if (dx * dx + dy * dy + dz * dz <= radius * radius) { + canvas.set(cx + dx, cy + dy, cz + dz, CoralCanvas.Role.STRUCTURE); + } + } + } + } + placeTip(canvas, coral, cx, cy + radius + 1, cz, seed); + } + + private static void placeTip(CoralCanvas canvas, IrisCoral coral, int x, int y, int z, long seed) { + if (!hasTip(coral)) { + return; + } + if (TreeFunctions.valueNoise1D(x * 31.0 + y * 17.0 + z * 13.0, seed) <= coral.getTipChance()) { + canvas.set(x, y, z, CoralCanvas.Role.TIP); + } + } + + private static boolean hasTip(IrisCoral coral) { + return IrisProceduralBlocks.paletteSet(coral.getTipPalette()) + || (coral.getTipBlock() != null && !coral.getTipBlock().isEmpty()); + } + + private static double branchAzimuth(IrisCoral coral, int index, int count, RNG rng) { + return switch (coral.getBranchAzimuth()) { + case GOLDEN_ANGLE -> index * GOLDEN_ANGLE; + case EVEN -> index * (360.0 / count); + case RANDOM -> rng.d(0.0, 360.0); + }; + } + + private static Map resolve(CoralCanvas canvas, IrisCoral coral, IrisData data) { + Map out = new HashMap<>(); + RNG paletteRng = new RNG(coral.getSeed()); + for (Map.Entry entry : canvas.getCells().entrySet()) { + int[] xyz = CoralCanvas.decode(entry.getKey()); + int x = xyz[0]; + int y = xyz[1]; + int z = xyz[2]; + PlatformBlockState state; + if (entry.getValue() == CoralCanvas.Role.TIP) { + state = IrisProceduralBlocks.resolve(coral.getTipBlock(), coral.getTipPalette(), data, x, y, z, paletteRng); + if (state == null) { + state = IrisProceduralBlocks.resolve(coral.getBlock(), coral.getBlockPalette(), data, x, y, z, paletteRng); + } + } else { + state = IrisProceduralBlocks.resolve(coral.getBlock(), coral.getBlockPalette(), data, x, y, z, paletteRng); + } + if (state == null) { + continue; + } + if (coral.isWaterlogged() && IrisProceduralBlocks.hasProperty(state, "waterlogged")) { + try { + state = state.withProperty("waterlogged", "true"); + } catch (IllegalArgumentException ignored) { + } + } + out.put(new Vector3i(x, y, z), state); + } + return out; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalBaseBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalBaseBuilder.java new file mode 100644 index 000000000..764003979 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalBaseBuilder.java @@ -0,0 +1,49 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.crystal; + +import art.arcane.iris.engine.object.IrisCrystal; +import art.arcane.iris.engine.object.tree.TreeFunctions; + +public final class CrystalBaseBuilder { + private CrystalBaseBuilder() { + } + + public static void build(CrystalCanvas canvas, IrisCrystal crystal, long seed) { + double radius = crystal.getBaseRadius(); + if (radius <= 0.0) { + canvas.set(0, 0, 0, CrystalRole.BASE); + return; + } + + double noiseStrength = crystal.getBaseNoise(); + int reach = (int) Math.ceil(radius + 1.0); + for (int x = -reach; x <= reach; x++) { + for (int y = -reach; y <= reach; y++) { + for (int z = -reach; z <= reach; z++) { + double distance = Math.sqrt((double) x * x + (double) y * y + (double) z * z); + double wobble = (TreeFunctions.valueNoise3D(x, y, z, seed) - 0.5) * 2.0 * noiseStrength * radius; + if (distance <= radius + wobble) { + canvas.set(x, y, z, CrystalRole.BASE); + } + } + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalCanvas.java b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalCanvas.java new file mode 100644 index 000000000..ca7283fe0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalCanvas.java @@ -0,0 +1,52 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.crystal; + +import art.arcane.iris.util.common.math.Vector3i; + +import java.util.HashMap; +import java.util.Map; + +public final class CrystalCanvas { + private final Map cells = new HashMap<>(); + + public void set(int x, int y, int z, CrystalRole role) { + Vector3i key = new Vector3i(x, y, z); + CrystalRole existing = cells.get(key); + if (existing == null || rank(role) > rank(existing)) { + cells.put(key, role); + } + } + + public boolean has(int x, int y, int z) { + return cells.containsKey(new Vector3i(x, y, z)); + } + + public Map getCells() { + return cells; + } + + private static int rank(CrystalRole role) { + return switch (role) { + case BASE -> 0; + case SHARD -> 1; + case TIP -> 2; + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalGenerator.java new file mode 100644 index 000000000..2104c655c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalGenerator.java @@ -0,0 +1,111 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.crystal; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisCrystal; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +import java.util.HashMap; +import java.util.Map; + +public final class CrystalGenerator { + private CrystalGenerator() { + } + + public static IrisObject generate(IrisCrystal crystal, int variantIndex, RNG rng, IrisData data) { + CrystalCanvas canvas = new CrystalCanvas(); + long structureSeed = rng.getSeed(); + + CrystalBaseBuilder.build(canvas, crystal, structureSeed + 977L); + CrystalShardBuilder.build(canvas, crystal, rng); + + Map resolved = new HashMap<>(); + RNG paletteRng = new RNG(crystal.getSeed()); + RNG tipRng = new RNG(crystal.getSeed() + (variantIndex * 31337L) + 4099L); + + for (Map.Entry entry : canvas.getCells().entrySet()) { + Vector3i position = entry.getKey(); + int x = position.getBlockX(); + int y = position.getBlockY(); + int z = position.getBlockZ(); + PlatformBlockState blockData = resolveRole(crystal, entry.getValue(), data, x, y, z, paletteRng, tipRng); + if (blockData == null) { + continue; + } + resolved.put(position, blockData); + } + + return IrisProceduralBlocks.assemble(resolved); + } + + private static PlatformBlockState resolveRole(IrisCrystal crystal, CrystalRole role, IrisData data, int x, int y, int z, RNG paletteRng, RNG tipRng) { + return switch (role) { + case BASE -> resolveBase(crystal, data, x, y, z, paletteRng); + case SHARD -> resolveShard(crystal, data, x, y, z, paletteRng); + case TIP -> resolveTip(crystal, data, x, y, z, paletteRng, tipRng); + }; + } + + private static PlatformBlockState resolveBase(IrisCrystal crystal, IrisData data, int x, int y, int z, RNG paletteRng) { + boolean hasBase = IrisProceduralBlocks.paletteSet(crystal.getBasePalette()) + || (crystal.getBaseBlock() != null && !crystal.getBaseBlock().isEmpty()); + if (hasBase) { + PlatformBlockState base = IrisProceduralBlocks.resolve(crystal.getBaseBlock(), crystal.getBasePalette(), data, x, y, z, paletteRng); + if (base != null) { + return base; + } + } + return resolveShard(crystal, data, x, y, z, paletteRng); + } + + private static PlatformBlockState resolveShard(IrisCrystal crystal, IrisData data, int x, int y, int z, RNG paletteRng) { + return IrisProceduralBlocks.resolve(crystal.getBlock(), crystal.getBlockPalette(), data, x, y, z, paletteRng); + } + + private static PlatformBlockState resolveTip(IrisCrystal crystal, IrisData data, int x, int y, int z, RNG paletteRng, RNG tipRng) { + boolean hasTip = IrisProceduralBlocks.paletteSet(crystal.getTipPalette()) + || (crystal.getTipBlock() != null && !crystal.getTipBlock().isEmpty()); + + if (hasTip) { + if (tipRng.chance(crystal.getTipChance())) { + PlatformBlockState tip = IrisProceduralBlocks.resolve(crystal.getTipBlock(), crystal.getTipPalette(), data, x, y, z, paletteRng); + if (tip != null) { + return tip; + } + } + return resolveShard(crystal, data, x, y, z, paletteRng); + } + + if (crystal.isGlow() && crystal.getGlowBlock() != null && !crystal.getGlowBlock().isEmpty()) { + if (tipRng.chance(crystal.getTipChance())) { + PlatformBlockState glow = IrisProceduralBlocks.resolve(crystal.getGlowBlock(), null, data, x, y, z, paletteRng); + if (glow != null) { + return glow; + } + } + } + + return resolveShard(crystal, data, x, y, z, paletteRng); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalGeometry.java b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalGeometry.java new file mode 100644 index 000000000..2bbe409d6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalGeometry.java @@ -0,0 +1,84 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.crystal; + +import art.arcane.iris.engine.object.IrisCrystalSurface; + +public final class CrystalGeometry { + private CrystalGeometry() { + } + + public static double[] surfaceNormal(IrisCrystalSurface surface) { + return switch (surface) { + case FLOOR -> new double[]{0.0, 1.0, 0.0}; + case CEILING -> new double[]{0.0, -1.0, 0.0}; + case WALL -> { + double len = Math.sqrt(1.0 + 0.5 * 0.5); + yield new double[]{1.0 / len, 0.5 / len, 0.0}; + } + }; + } + + public static double[] orthonormalBasisU(double[] normal) { + double[] reference = Math.abs(normal[1]) > 0.99 ? new double[]{1.0, 0.0, 0.0} : new double[]{0.0, 1.0, 0.0}; + double[] u = cross(reference, normal); + normalize(u); + return u; + } + + public static double[] orthonormalBasisV(double[] normal, double[] u) { + double[] v = cross(normal, u); + normalize(v); + return v; + } + + public static double[] coneDirection(double[] normal, double[] u, double[] v, double coneAngleRadians, double azimuthRadians) { + double sin = Math.sin(coneAngleRadians); + double cos = Math.cos(coneAngleRadians); + double ca = Math.cos(azimuthRadians); + double sa = Math.sin(azimuthRadians); + double[] dir = new double[3]; + for (int i = 0; i < 3; i++) { + dir[i] = cos * normal[i] + sin * (ca * u[i] + sa * v[i]); + } + normalize(dir); + return dir; + } + + public static double[] cross(double[] a, double[] b) { + return new double[]{ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + }; + } + + public static void normalize(double[] vec) { + double len = Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]); + if (len < 1e-9) { + vec[0] = 0.0; + vec[1] = 1.0; + vec[2] = 0.0; + return; + } + vec[0] /= len; + vec[1] /= len; + vec[2] /= len; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalRole.java b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalRole.java new file mode 100644 index 000000000..da6fa15c1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalRole.java @@ -0,0 +1,25 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.crystal; + +public enum CrystalRole { + BASE, + SHARD, + TIP +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalShardBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalShardBuilder.java new file mode 100644 index 000000000..c0b0e8cca --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/crystal/CrystalShardBuilder.java @@ -0,0 +1,125 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.crystal; + +import art.arcane.iris.engine.object.IrisCrystal; +import art.arcane.iris.engine.object.IrisCrystalDistribution; +import art.arcane.volmlib.util.math.RNG; + +public final class CrystalShardBuilder { + private static final double GOLDEN_ANGLE = 137.50776405003785; + + private CrystalShardBuilder() { + } + + public static void build(CrystalCanvas canvas, IrisCrystal crystal, RNG rng) { + double[] normal = CrystalGeometry.surfaceNormal(crystal.getGrowthSurface()); + double[] u = CrystalGeometry.orthonormalBasisU(normal); + double[] v = CrystalGeometry.orthonormalBasisV(normal, u); + + int countLo = Math.min(crystal.getShardCountMin(), crystal.getShardCountMax()); + int countHi = Math.max(crystal.getShardCountMin(), crystal.getShardCountMax()); + int shardCount = Math.max(1, rng.i(countLo, countHi + 1)); + + int lengthLo = Math.min(crystal.getShardLengthMin(), crystal.getShardLengthMax()); + int lengthHi = Math.max(crystal.getShardLengthMin(), crystal.getShardLengthMax()); + + double spreadRadians = Math.toRadians(Math.max(0.0, Math.min(90.0, crystal.getSpreadAngle()))); + double jitterAmount = Math.max(0.0, Math.min(1.0, crystal.getJitter())); + double emitRadius = Math.max(0.0, crystal.getBaseRadius() * 0.6); + + for (int i = 0; i < shardCount; i++) { + double azimuth = azimuth(crystal.getDistribution(), i, rng); + azimuth += rng.d(-Math.PI, Math.PI) * jitterAmount * 0.5; + + double cone = spreadRadians * fanFraction(shardCount, i, rng); + cone += rng.d(-spreadRadians, spreadRadians) * jitterAmount * 0.5; + cone = Math.max(0.0, Math.min(Math.PI / 2.0, cone)); + + double[] direction = CrystalGeometry.coneDirection(normal, u, v, cone, azimuth); + + double ox = normal[0] * emitRadius + u[0] * Math.cos(azimuth) * emitRadius + v[0] * Math.sin(azimuth) * emitRadius; + double oy = normal[1] * emitRadius + u[1] * Math.cos(azimuth) * emitRadius + v[1] * Math.sin(azimuth) * emitRadius; + double oz = normal[2] * emitRadius + u[2] * Math.cos(azimuth) * emitRadius + v[2] * Math.sin(azimuth) * emitRadius; + + int length = Math.max(1, rng.i(lengthLo, lengthHi + 1)); + rasterizeShard(canvas, crystal, ox, oy, oz, direction, length); + } + } + + private static void rasterizeShard(CrystalCanvas canvas, IrisCrystal crystal, double ox, double oy, double oz, double[] direction, int length) { + double baseRadius = Math.max(0.5, crystal.getShardBaseRadius()); + double taper = Math.max(0.0, Math.min(1.0, crystal.getShardTaper())); + + int samples = Math.max(1, length * 3); + + for (int s = 0; s <= samples; s++) { + double t = s / (double) samples; + double cx = ox + direction[0] * length * t; + double cy = oy + direction[1] * length * t; + double cz = oz + direction[2] * length * t; + + double radius = baseRadius * (1.0 - taper * t); + if (t >= 1.0) { + radius = 0.0; + } + radius = Math.max(0.0, radius); + + int reach = (int) Math.floor(radius); + int centerX = (int) Math.round(cx); + int centerY = (int) Math.round(cy); + int centerZ = (int) Math.round(cz); + + if (reach <= 0) { + canvas.set(centerX, centerY, centerZ, CrystalRole.SHARD); + } else { + for (int dx = -reach; dx <= reach; dx++) { + for (int dy = -reach; dy <= reach; dy++) { + for (int dz = -reach; dz <= reach; dz++) { + double dist = Math.sqrt((double) dx * dx + (double) dy * dy + (double) dz * dz); + if (dist <= radius) { + canvas.set(centerX + dx, centerY + dy, centerZ + dz, CrystalRole.SHARD); + } + } + } + } + } + } + + int tipX = (int) Math.round(ox + direction[0] * length); + int tipY = (int) Math.round(oy + direction[1] * length); + int tipZ = (int) Math.round(oz + direction[2] * length); + canvas.set(tipX, tipY, tipZ, CrystalRole.TIP); + } + + private static double azimuth(IrisCrystalDistribution distribution, int index, RNG rng) { + if (distribution == IrisCrystalDistribution.GOLDEN_ANGLE) { + return Math.toRadians(index * GOLDEN_ANGLE); + } + return rng.d(0.0, Math.PI * 2.0); + } + + private static double fanFraction(int count, int index, RNG rng) { + if (count <= 1) { + return rng.d(0.0, 1.0); + } + double base = index / (double) (count - 1); + return Math.max(0.0, Math.min(1.0, base * 0.7 + rng.d(0.0, 0.3))); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/formation/FormationBlockResolver.java b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationBlockResolver.java new file mode 100644 index 000000000..b801ef582 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationBlockResolver.java @@ -0,0 +1,66 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.formation; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisFormation; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +public final class FormationBlockResolver { + private FormationBlockResolver() { + } + + public static PlatformBlockState resolve(IrisFormation f, IrisData data, FormationCanvas.Role role, Vector3i raw) { + int x = raw.getBlockX(); + int y = raw.getBlockY(); + int z = raw.getBlockZ(); + RNG paletteRng = new RNG(f.getSeed()); + + if (role == FormationCanvas.Role.CAP && capDefined(f)) { + PlatformBlockState cap = IrisProceduralBlocks.resolve(f.getCapBlock(), f.getCapPalette(), data, x, y, z, paletteRng); + if (cap != null) { + return cap; + } + } + + if (strataDefined(f)) { + IrisMaterialPalette strata = f.getStrataPalette(); + int thickness = Math.max(1, f.getStrataThickness()); + int band = Math.floorDiv(y, thickness); + PlatformBlockState strataState = strata.get(new RNG(f.getSeed() + (band * 31L)), x, band, z, data); + if (strataState != null) { + return strataState; + } + } + + return IrisProceduralBlocks.resolve(f.getBlock(), f.getBlockPalette(), data, x, y, z, paletteRng); + } + + private static boolean capDefined(IrisFormation f) { + return IrisProceduralBlocks.paletteSet(f.getCapPalette()) || (f.getCapBlock() != null && !f.getCapBlock().isEmpty()); + } + + private static boolean strataDefined(IrisFormation f) { + return IrisProceduralBlocks.paletteSet(f.getStrataPalette()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/formation/FormationCanvas.java b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationCanvas.java new file mode 100644 index 000000000..b7b842a36 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationCanvas.java @@ -0,0 +1,61 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.formation; + +import art.arcane.iris.util.common.math.Vector3i; + +import java.util.HashMap; +import java.util.Map; + +public final class FormationCanvas { + public enum Role { + BODY, + CAP + } + + private final Map cells = new HashMap<>(); + + public void setBody(int x, int y, int z) { + Vector3i key = new Vector3i(x, y, z); + if (cells.get(key) == Role.CAP) { + return; + } + cells.put(key, Role.BODY); + } + + public void setCap(int x, int y, int z) { + cells.put(new Vector3i(x, y, z), Role.CAP); + } + + public void remove(int x, int y, int z) { + cells.remove(new Vector3i(x, y, z)); + } + + public boolean has(int x, int y, int z) { + return cells.containsKey(new Vector3i(x, y, z)); + } + + public Map getCells() { + return cells; + } + + public boolean isEmpty() { + return cells.isEmpty(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/formation/FormationGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationGenerator.java new file mode 100644 index 000000000..e6fe4e577 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationGenerator.java @@ -0,0 +1,75 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.formation; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisFormation; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +import java.util.HashMap; +import java.util.Map; + +public final class FormationGenerator { + private FormationGenerator() { + } + + public static IrisObject generate(IrisFormation f, int variantIndex, RNG rng, IrisData data) { + int lo = Math.min(f.getHeightMin(), f.getHeightMax()); + int hi = Math.max(f.getHeightMin(), f.getHeightMax()); + int height = Math.max(3, rng.i(lo, hi + 1)); + + int wLo = Math.min(f.getBaseWidthMin(), f.getBaseWidthMax()); + int wHi = Math.max(f.getBaseWidthMin(), f.getBaseWidthMax()); + double baseRadius = Math.max(1, rng.i(wLo, wHi + 1)); + + FormationCanvas canvas = new FormationCanvas(); + + switch (f.getForm()) { + case SPIRE -> FormationShapeBuilder.spire(canvas, f, height, baseRadius, rng); + case HOODOO -> FormationShapeBuilder.hoodoo(canvas, f, height, baseRadius, rng); + case ARCH -> FormationShapeBuilder.arch(canvas, f, height, baseRadius, rng); + case SEA_STACK -> FormationShapeBuilder.seaStack(canvas, f, height, baseRadius, rng); + case BOULDER -> FormationShapeBuilder.boulder(canvas, f, height, baseRadius, rng); + case BASALT_COLUMN -> FormationShapeBuilder.basaltColumns(canvas, f, height, baseRadius, rng); + case ICEBERG -> FormationShapeBuilder.iceberg(canvas, f, height, baseRadius, rng); + case FISSURE -> FormationShapeBuilder.fissure(canvas, f, height, baseRadius, rng); + case SPIRAL -> FormationShapeBuilder.spiral(canvas, f, height, baseRadius, rng); + case OVERHANG -> FormationShapeBuilder.overhang(canvas, f, height, baseRadius, rng); + } + + if (canvas.isEmpty()) { + return null; + } + + Map resolved = new HashMap<>(); + for (Map.Entry entry : canvas.getCells().entrySet()) { + PlatformBlockState bd = FormationBlockResolver.resolve(f, data, entry.getValue(), entry.getKey()); + if (bd == null) { + continue; + } + resolved.put(entry.getKey(), bd); + } + + return IrisProceduralBlocks.assemble(resolved); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/formation/FormationProfiles.java b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationProfiles.java new file mode 100644 index 000000000..ebbadb6d1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationProfiles.java @@ -0,0 +1,57 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.formation; + +import art.arcane.iris.engine.object.IrisFormation; + +public final class FormationProfiles { + private FormationProfiles() { + } + + public static double radiusAt(IrisFormation f, double baseRadius, double topRadius, double t) { + double clamped = Math.max(0.0, Math.min(1.0, t)); + return switch (f.getProfile()) { + case CONSTANT -> baseRadius; + case LINEAR -> baseRadius + (topRadius - baseRadius) * clamped; + case TAPER -> baseRadius + (topRadius - baseRadius) * smoothstep(clamped); + case PARABOLIC -> parabolic(f, baseRadius, topRadius, clamped); + case BULGE -> baseRadius + (Math.max(baseRadius, topRadius) - baseRadius) * bell(clamped) + (topRadius - baseRadius) * clamped * 0.25; + }; + } + + private static double parabolic(IrisFormation f, double baseRadius, double topRadius, double t) { + double waist = Math.max(0.0, Math.min(1.0, f.getProfileWaist())); + double floor = Math.max(0.0, Math.min(1.0, f.getProfileWaistFloor())); + double denom = Math.pow(Math.max(waist, 1.0 - waist), 2); + if (denom == 0) { + denom = 1; + } + double envelope = baseRadius + (topRadius - baseRadius) * t; + double pinch = floor + (1.0 - floor) * (Math.pow(t - waist, 2) / denom); + return Math.max(0.5, envelope * pinch); + } + + private static double smoothstep(double t) { + return 3.0 * t * t - 2.0 * t * t * t; + } + + private static double bell(double t) { + return Math.exp(-Math.pow(t - 0.5, 2) / (2.0 * 0.22 * 0.22)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/formation/FormationShapeBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationShapeBuilder.java new file mode 100644 index 000000000..54f3026c0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationShapeBuilder.java @@ -0,0 +1,429 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.formation; + +import art.arcane.iris.engine.object.IrisFormation; +import art.arcane.iris.engine.object.tree.TreeFunctions; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +import java.util.HashMap; +import java.util.Map; + +public final class FormationShapeBuilder { + private FormationShapeBuilder() { + } + + public static void spire(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + double topRadius = f.getTopWidth(); + column(canvas, f, height, baseRadius, topRadius, 0, 0, rng, true); + double lean = Math.toRadians(f.getLean()); + double azimuth = Math.toRadians(f.getLeanAzimuth()); + int previousX = 0; + int previousZ = 0; + for (int y = 0; y < height; y++) { + double shear = Math.tan(lean) * y; + int centerX = (int) Math.round(Math.cos(azimuth) * shear); + int centerZ = (int) Math.round(Math.sin(azimuth) * shear); + boolean cap = y >= height - 2; + for (int x = Math.min(previousX, centerX); x <= Math.max(previousX, centerX); x++) { + setSpireCenter(canvas, x, y, previousZ, cap); + } + for (int z = Math.min(previousZ, centerZ); z <= Math.max(previousZ, centerZ); z++) { + setSpireCenter(canvas, centerX, y, z, cap); + } + previousX = centerX; + previousZ = centerZ; + } + } + + public static void seaStack(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + double topRadius = Math.max(1.0, baseRadius * 0.55); + if (f.getTopWidth() > 0) { + topRadius = f.getTopWidth(); + } + column(canvas, f, height, baseRadius, topRadius, 0, 0, rng, true); + } + + public static void hoodoo(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + double topRadius = f.getTopWidth() > 0 ? f.getTopWidth() : Math.max(1.0, baseRadius * 0.7); + column(canvas, f, height, baseRadius, topRadius, 0, 0, rng, false); + + int capRadius = f.getHoodooCapRadius(); + if (capRadius <= 0) { + return; + } + int capHeight = Math.max(1, f.getHoodooCapHeight()); + double lean = Math.toRadians(f.getLean()); + double azimuth = Math.toRadians(f.getLeanAzimuth()); + double shear = Math.tan(lean) * height; + double topOffX = Math.cos(azimuth) * shear; + double topOffZ = Math.sin(azimuth) * shear; + double wideRadius = topRadius + capRadius; + + for (int cy = 0; cy < capHeight; cy++) { + int y = height + cy; + double shrink = capHeight <= 1 ? 0 : (cy / (double) (capHeight - 1)) * 0.5; + double r = wideRadius * (1.0 - shrink); + disc(canvas, f, (int) Math.round(topOffX), y, (int) Math.round(topOffZ), r, true, rng); + } + } + + public static void boulder(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + double rx = baseRadius; + double ry = Math.max(2.0, height * 0.5); + double rz = baseRadius * (0.8 + rng.d(0.0, 0.3)); + double roughness = Math.max(0.0, Math.min(1.0, f.getRoughness())); + long noiseSeed = f.getSeed() + 4201L; + + int maxR = (int) Math.ceil(Math.max(rx, Math.max(ry, rz))) + 2; + for (int x = -maxR; x <= maxR; x++) { + for (int y = 0; y <= (int) Math.ceil(ry * 2) + 1; y++) { + for (int z = -maxR; z <= maxR; z++) { + double nx = x / rx; + double ny = (y - ry) / ry; + double nz = z / rz; + double d = nx * nx + ny * ny + nz * nz; + double wobble = (TreeFunctions.valueNoise3D(x, y, z, noiseSeed) - 0.5) * roughness * 0.9; + if (d + wobble <= 1.0) { + canvas.setBody(x, y, z); + } + } + } + } + } + + public static void arch(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + double span = Math.max(2.0, f.getArchSpan()); + double tubeRadius = Math.max(0.75, f.getArchThickness() * 0.5); + double asymmetry = Math.max(0.0, Math.min(1.0, f.getArchAsymmetry())); + double leftFootX = -span * 0.5 - tubeRadius - rng.d(0.0, span * 0.16) * asymmetry; + double rightFootX = span * 0.5 + tubeRadius + rng.d(0.0, span * 0.16) * asymmetry; + double leftFootZ = rng.d(-tubeRadius * 0.45, tubeRadius * 0.45) * asymmetry; + double rightFootZ = rng.d(-tubeRadius * 0.45, tubeRadius * 0.45) * asymmetry; + double crownZ = rng.d(-span * 0.2, span * 0.2) * asymmetry; + double crownShiftX = rng.d(-span * 0.12, span * 0.12) * asymmetry; + double depthTwist = rng.d(-span * 0.1, span * 0.1) * asymmetry; + double crownPosition = 0.5 + rng.d(-0.12, 0.12) * asymmetry; + double leftSteepness = 0.58 + rng.d(-0.16, 0.16) * asymmetry; + double rightSteepness = 0.58 + rng.d(-0.16, 0.16) * asymmetry; + double radiusWave = rng.d(-0.22, 0.22) * asymmetry; + double radiusPhase = rng.d(0.0, Math.PI * 2.0); + double crownCenterY = Math.max(tubeRadius + 2.0, height - 1.0 - tubeRadius); + double footRadius = tubeRadius + Math.min(tubeRadius * 0.45, baseRadius * 0.18); + long noiseSeed = rng.nextLong(); + int steps = Math.max(24, (int) Math.ceil((rightFootX - leftFootX + height) * 4.0)); + + ball(canvas, f, (int) Math.round(leftFootX), (int) Math.round(footRadius), + (int) Math.round(leftFootZ), footRadius, false, noiseSeed, rng); + ball(canvas, f, (int) Math.round(rightFootX), (int) Math.round(footRadius), + (int) Math.round(rightFootZ), footRadius, false, noiseSeed, rng); + + for (int step = 0; step <= steps; step++) { + double t = step / (double) steps; + double normalizedRise; + double steepness; + if (t <= crownPosition) { + normalizedRise = Math.sin((t / crownPosition) * Math.PI * 0.5); + steepness = leftSteepness; + } else { + normalizedRise = Math.sin(((1.0 - t) / (1.0 - crownPosition)) * Math.PI * 0.5); + steepness = rightSteepness; + } + double rise = Math.pow(Math.max(0.0, normalizedRise), steepness); + double x = leftFootX + (rightFootX - leftFootX) * t + + Math.sin(Math.PI * t) * crownShiftX; + double inverse = 1.0 - t; + double z = inverse * inverse * leftFootZ + 2.0 * inverse * t * crownZ + t * t * rightFootZ + + Math.sin(Math.PI * 2.0 * t) * depthTwist; + double y = tubeRadius + (crownCenterY - tubeRadius) * rise; + double radius = tubeRadius * (1.0 + radiusWave * Math.sin(Math.PI * t) + * Math.sin(Math.PI * 2.0 * t + radiusPhase)); + radius = Math.max(tubeRadius * 0.68, Math.min(tubeRadius * 1.32, radius)); + ball(canvas, f, (int) Math.round(x), (int) Math.round(y), (int) Math.round(z), + radius, false, noiseSeed, rng); + } + if (asymmetry == 0.0) { + mirrorAcrossX(canvas); + } + } + + public static void basaltColumns(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + int count = Math.max(2, f.getBasaltColumns()); + int colRadius = Math.max(1, f.getBasaltColumnRadius()); + double variance = Math.max(0.0, Math.min(1.0, f.getBasaltHeightVariance())); + int spread = (int) Math.ceil(baseRadius); + + for (int c = 0; c < count; c++) { + double angle = rng.d(0.0, Math.PI * 2.0); + double dist = rng.d(0.0, spread); + int ox = (int) Math.round(Math.cos(angle) * dist); + int oz = (int) Math.round(Math.sin(angle) * dist); + double hVar = 1.0 - variance + rng.d(0.0, variance * 2.0); + int colHeight = Math.max(3, (int) Math.round(height * hVar)); + + for (int y = 0; y < colHeight; y++) { + for (int x = -colRadius; x <= colRadius; x++) { + for (int z = -colRadius; z <= colRadius; z++) { + if (Math.abs(x) + Math.abs(z) > colRadius) { + continue; + } + canvas.setBody(ox + x, y, oz + z); + } + } + } + if (colRadius > 0) { + canvas.setCap(ox, colHeight - 1, oz); + } + } + } + + public static void iceberg(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + long noiseSeed = rng.nextLong(); + double bodyHeight = Math.max(2.0, height * 0.42); + double radiusX = baseRadius * rng.d(1.15, 1.5); + double radiusZ = baseRadius * rng.d(1.0, 1.35); + ellipsoid(canvas, radiusX, bodyHeight, radiusZ, bodyHeight * 0.36, noiseSeed, f.getRoughness()); + + int peaks = Math.max(1, f.getIcebergPeaks()); + double phase = rng.d(0.0, Math.PI * 2.0); + for (int i = 0; i < peaks; i++) { + double angle = phase + (Math.PI * 2.0 * i / peaks) + rng.d(-0.35, 0.35); + double distance = rng.d(0.0, baseRadius * 0.62); + int offsetX = (int) Math.round(Math.cos(angle) * distance); + int offsetZ = (int) Math.round(Math.sin(angle) * distance); + int peakHeight = Math.max(3, (int) Math.round(height * rng.d(0.58, 1.0))); + double peakRadius = Math.max(1.0, baseRadius * rng.d(0.28, 0.58)); + double leanDistance = rng.d(0.0, Math.max(1.0, baseRadius * 0.45)); + taperedPeak(canvas, f, peakHeight, peakRadius, offsetX, offsetZ, angle, leanDistance, rng); + } + } + + public static void fissure(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + int count = Math.max(2, f.getFractureCount()); + double separation = Math.max(1.0, f.getFractureSeparation()); + double shardRadius = Math.max(1.0, baseRadius / Math.max(1.6, count * 0.72)); + double phase = rng.d(0.0, Math.PI * 2.0); + + for (int i = 0; i < count; i++) { + double lane = i - ((count - 1) / 2.0); + double offset = lane * (shardRadius * 2.0 + separation); + int offsetX = (int) Math.round(Math.cos(phase) * offset); + int offsetZ = (int) Math.round(Math.sin(phase) * offset); + double outwardAngle = lane < 0 ? phase + Math.PI : phase; + int shardHeight = Math.max(3, (int) Math.round(height * rng.d(0.62, 1.0))); + double leanDistance = rng.d(separation * 0.4, separation + baseRadius * 0.8); + taperedPeak(canvas, f, shardHeight, shardRadius, offsetX, offsetZ, outwardAngle, leanDistance, rng); + } + } + + public static void spiral(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + int steps = Math.max(12, height * 4); + double turns = Math.max(0.25, f.getSpiralTurns()); + double startRadius = Math.max(1.0, f.getSpiralRadius()); + double startThickness = Math.max(1.0, f.getSpiralThickness()); + double phase = rng.d(0.0, Math.PI * 2.0); + long noiseSeed = rng.nextLong(); + + for (int step = 0; step <= steps; step++) { + double t = step / (double) steps; + double angle = phase + Math.PI * 2.0 * turns * t; + double radius = startRadius * (1.0 - 0.72 * t); + int x = (int) Math.round(Math.cos(angle) * radius); + int y = (int) Math.round(t * (height - 1)); + int z = (int) Math.round(Math.sin(angle) * radius); + double thickness = Math.max(0.75, startThickness * (1.0 - 0.48 * t)); + ball(canvas, f, x, y, z, thickness, step == steps, noiseSeed, rng); + } + + double footRadius = Math.max(1.0, Math.min(baseRadius, startThickness + 1.0)); + disc(canvas, f, (int) Math.round(Math.cos(phase) * startRadius), 0, + (int) Math.round(Math.sin(phase) * startRadius), footRadius, false, rng); + } + + public static void overhang(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + int steps = Math.max(12, height * 4); + double reach = Math.max(1.0, f.getOverhangReach()); + double drop = Math.max(0.0, f.getOverhangDrop()); + double phase = Math.toRadians(f.getLeanAzimuth()) + rng.d(-0.18, 0.18); + double trunkHeight = Math.max(3.0, height * 0.62); + long noiseSeed = rng.nextLong(); + + for (int step = 0; step <= steps; step++) { + double t = step / (double) steps; + double extension; + double y; + if (t <= 0.58) { + double rise = t / 0.58; + extension = reach * 0.16 * rise * rise; + y = trunkHeight * rise; + } else { + double hook = (t - 0.58) / 0.42; + extension = reach * (0.16 + 0.84 * Math.sin(hook * Math.PI * 0.5)); + y = trunkHeight + Math.sin(hook * Math.PI) * height * 0.18 - drop * hook * hook; + } + int x = (int) Math.round(Math.cos(phase) * extension); + int z = (int) Math.round(Math.sin(phase) * extension); + double thickness = Math.max(0.9, baseRadius * (1.0 - 0.64 * t)); + ball(canvas, f, x, Math.max(0, (int) Math.round(y)), z, thickness, step == steps, noiseSeed, rng); + } + } + + private static void column(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, double topRadius, int extraBaseX, int extraBaseZ, RNG rng, boolean capTop) { + double lean = Math.toRadians(f.getLean()); + double azimuth = Math.toRadians(f.getLeanAzimuth()); + double roughness = Math.max(0.0, Math.min(1.0, f.getRoughness())); + double jitter = Math.max(0.0, Math.min(1.0, f.getJitter())); + long noiseSeed = f.getSeed() + 9001L; + + for (int y = 0; y < height; y++) { + double t = height <= 1 ? 1.0 : (y / (double) (height - 1)); + double radius = FormationProfiles.radiusAt(f, baseRadius, topRadius, t); + double shear = Math.tan(lean) * y; + int cx = extraBaseX + (int) Math.round(Math.cos(azimuth) * shear); + int cz = extraBaseZ + (int) Math.round(Math.sin(azimuth) * shear); + boolean cap = capTop && y >= height - 2; + ringDisc(canvas, f, cx, y, cz, radius, roughness, jitter, noiseSeed, cap, rng); + } + } + + private static void setSpireCenter(FormationCanvas canvas, int x, int y, int z, boolean cap) { + if (cap) { + canvas.setCap(x, y, z); + } else { + canvas.setBody(x, y, z); + } + } + + private static void ringDisc(FormationCanvas canvas, IrisFormation f, int cx, int y, int cz, double radius, double roughness, double jitter, long noiseSeed, boolean cap, RNG rng) { + int r = (int) Math.ceil(radius) + 1; + for (int x = -r; x <= r; x++) { + for (int z = -r; z <= r; z++) { + double dist = Math.sqrt(x * x + z * z); + double perturb = (TreeFunctions.valueNoise3D(cx + x, y, cz + z, noiseSeed) - 0.5) * roughness * (radius + 1.0); + double effective = radius + perturb; + if (dist <= effective) { + if (jitter > 0 && dist > effective - 1.0 && rng.chance(jitter * 0.5)) { + continue; + } + if (cap) { + canvas.setCap(cx + x, y, cz + z); + } else { + canvas.setBody(cx + x, y, cz + z); + } + } + } + } + } + + private static void disc(FormationCanvas canvas, IrisFormation f, int cx, int y, int cz, double radius, boolean cap, RNG rng) { + int r = (int) Math.ceil(radius) + 1; + for (int x = -r; x <= r; x++) { + for (int z = -r; z <= r; z++) { + if (Math.sqrt(x * x + z * z) <= radius) { + if (cap) { + canvas.setCap(cx + x, y, cz + z); + } else { + canvas.setBody(cx + x, y, cz + z); + } + } + } + } + } + + private static void taperedPeak(FormationCanvas canvas, IrisFormation f, int height, double radius, + int offsetX, int offsetZ, double leanAngle, double leanDistance, RNG rng) { + long noiseSeed = rng.nextLong(); + double roughness = Math.max(0.0, Math.min(1.0, f.getRoughness())); + double jitter = Math.max(0.0, Math.min(1.0, f.getJitter())); + for (int y = 0; y < height; y++) { + double t = height <= 1 ? 1.0 : y / (double) (height - 1); + int x = offsetX + (int) Math.round(Math.cos(leanAngle) * leanDistance * t * t); + int z = offsetZ + (int) Math.round(Math.sin(leanAngle) * leanDistance * t * t); + double layerRadius = Math.max(0.45, radius * Math.pow(1.0 - t, 0.72)); + ringDisc(canvas, f, x, y, z, layerRadius, roughness, jitter, noiseSeed, y >= height - 2, rng); + } + } + + private static void ellipsoid(FormationCanvas canvas, double radiusX, double radiusY, double radiusZ, + double centerY, long noiseSeed, double configuredRoughness) { + double roughness = Math.max(0.0, Math.min(1.0, configuredRoughness)); + int maxX = (int) Math.ceil(radiusX) + 1; + int maxY = (int) Math.ceil(radiusY) + 1; + int maxZ = (int) Math.ceil(radiusZ) + 1; + for (int x = -maxX; x <= maxX; x++) { + for (int y = 0; y <= maxY; y++) { + for (int z = -maxZ; z <= maxZ; z++) { + double nx = x / radiusX; + double ny = (y - centerY) / radiusY; + double nz = z / radiusZ; + double distance = nx * nx + ny * ny + nz * nz; + double wobble = (TreeFunctions.valueNoise3D(x, y, z, noiseSeed) - 0.5) * roughness * 0.55; + if (distance + wobble <= 1.0) { + canvas.setBody(x, y, z); + } + } + } + } + } + + private static void ball(FormationCanvas canvas, IrisFormation f, int centerX, int centerY, int centerZ, + double radius, boolean cap, long noiseSeed, RNG rng) { + int extent = (int) Math.ceil(radius); + double roughness = Math.max(0.0, Math.min(1.0, f.getRoughness())); + double jitter = Math.max(0.0, Math.min(1.0, f.getJitter())); + for (int x = -extent; x <= extent; x++) { + for (int y = -extent; y <= extent; y++) { + for (int z = -extent; z <= extent; z++) { + double distance = Math.sqrt(x * x + y * y + z * z); + double perturb = (TreeFunctions.valueNoise3D(centerX + x, centerY + y, centerZ + z, noiseSeed) - 0.5) + * roughness * Math.max(1.0, radius * 0.6); + double effectiveRadius = radius + perturb; + if (distance > effectiveRadius + 0.2) { + continue; + } + if (jitter > 0.0 && distance > effectiveRadius - 0.75 && rng.chance(jitter * 0.35)) { + continue; + } + int targetY = centerY + y; + if (targetY < 0) { + continue; + } + if (cap) { + canvas.setCap(centerX + x, targetY, centerZ + z); + } else { + canvas.setBody(centerX + x, targetY, centerZ + z); + } + } + } + } + } + + private static void mirrorAcrossX(FormationCanvas canvas) { + Map cells = new HashMap<>(canvas.getCells()); + for (Map.Entry entry : cells.entrySet()) { + Vector3i position = entry.getKey(); + if (entry.getValue() == FormationCanvas.Role.CAP) { + canvas.setCap(-position.getBlockX(), position.getBlockY(), position.getBlockZ()); + } else { + canvas.setBody(-position.getBlockX(), position.getBlockY(), position.getBlockZ()); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusCapBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusCapBuilder.java new file mode 100644 index 000000000..70a4bedce --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusCapBuilder.java @@ -0,0 +1,118 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.fungi; + +import art.arcane.iris.engine.object.IrisFungus; +import art.arcane.iris.engine.object.IrisFungusCapShape; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.engine.object.tree.TreeFunctions; +import art.arcane.iris.util.common.math.Vector3i; + +import java.util.Map; + +public final class FungusCapBuilder { + private FungusCapBuilder() { + } + + public static void build(Map roles, IrisFungus fungus, int baseRadius, double cx, int baseY, double cz, long seed) { + int radius = (int) Math.round(baseRadius + Math.max(0.0, fungus.getCapOverhang())); + int thickness = Math.max(1, Math.min(3, fungus.getCapThickness())); + double squish = Math.max(0.0, Math.min(1.0, fungus.getCapSquish())); + double droop = Math.max(0.0, fungus.getCapDroop()); + IrisFungusCapShape shape = fungus.getCapShape() == null ? IrisFungusCapShape.DOME : fungus.getCapShape(); + int icx = (int) Math.round(cx); + int icz = (int) Math.round(cz); + + boolean hasGill = blockOrPaletteSet(fungus.getGillBlock(), fungus.getGillPalette()); + boolean hasSpot = blockOrPaletteSet(fungus.getSpotBlock(), fungus.getSpotPalette()); + + double apexRise = apexRise(shape, radius) * (1.0 - 0.6 * squish); + double droopReach = Math.tan(Math.toRadians(droop)) * radius; + + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + double dist = Math.sqrt(dx * dx + dz * dz); + if (dist > radius + 0.5) { + continue; + } + double normalized = dist / Math.max(1.0, radius); + double surfaceTop = baseY + capSurface(shape, normalized, apexRise); + surfaceTop -= droopReach * Math.pow(normalized, 3); + + int topYi = (int) Math.round(surfaceTop); + for (int layer = 0; layer < thickness; layer++) { + int y = topYi - layer; + if (y < baseY - thickness) { + continue; + } + Vector3i pos = new Vector3i(icx + dx, y, icz + dz); + FungusCellRole role = FungusCellRole.CAP; + + boolean underside = layer == thickness - 1; + boolean topFace = layer == 0; + + if (underside && hasGill && rollNoise(icx + dx, y, icz + dz, seed + 4111L) <= fungus.getGillChance()) { + role = FungusCellRole.GILL; + } else if (topFace && hasSpot && spotNoise(icx + dx, y, icz + dz, seed + 9311L) <= fungus.getSpotChance()) { + role = FungusCellRole.SPOT; + } + + roles.putIfAbsent(pos, role); + } + } + } + } + + private static double apexRise(IrisFungusCapShape shape, int radius) { + return switch (shape) { + case DOME -> radius * 0.85; + case CONICAL -> radius * 1.4; + case FUNNEL -> radius * 0.5; + case FLAT -> radius * 0.35; + case FLAT_WIDE -> radius * 0.2; + }; + } + + private static double capSurface(IrisFungusCapShape shape, double normalized, double apexRise) { + double n = Math.max(0.0, Math.min(1.0, normalized)); + return switch (shape) { + case DOME -> apexRise * Math.sqrt(Math.max(0.0, 1.0 - n * n)); + case CONICAL -> apexRise * (1.0 - n); + case FUNNEL -> apexRise * (n * n); + case FLAT -> apexRise * Math.max(0.0, 1.0 - Math.pow(n, 4)); + case FLAT_WIDE -> apexRise * Math.max(0.0, 1.0 - Math.pow(n, 6)); + }; + } + + private static double rollNoise(int x, int y, int z, long seed) { + return TreeFunctions.valueNoise3D(x, y, z, seed); + } + + private static double spotNoise(int x, int y, int z, long seed) { + return TreeFunctions.valueNoise3D(Math.floorDiv(x, 2), y, Math.floorDiv(z, 2), seed); + } + + static boolean blockOrPaletteSet(String block, IrisMaterialPalette palette) { + if (IrisProceduralBlocks.paletteSet(palette)) { + return true; + } + return block != null && !block.isEmpty(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusCellRole.java b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusCellRole.java new file mode 100644 index 000000000..9f1eb9421 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusCellRole.java @@ -0,0 +1,26 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.fungi; + +enum FungusCellRole { + STEM, + CAP, + GILL, + SPOT +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusGenerator.java new file mode 100644 index 000000000..1e63cdfa0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusGenerator.java @@ -0,0 +1,104 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.fungi; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisFungus; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +import java.util.HashMap; +import java.util.Map; + +public final class FungusGenerator { + private FungusGenerator() { + } + + public static IrisObject generate(IrisFungus fungus, int height, RNG rng, IrisData data) { + Map roles = new HashMap<>(); + long baseSeed = rng.getSeed(); + + if (fungus.isShelf()) { + FungusShelfBuilder.build(roles, fungus, rng, baseSeed); + } else { + buildUpright(roles, fungus, height, rng, baseSeed); + } + + if (roles.isEmpty()) { + return null; + } + + RNG paletteRng = new RNG(fungus.getSeed()); + Map resolved = new HashMap<>(); + for (Map.Entry entry : roles.entrySet()) { + Vector3i pos = entry.getKey(); + PlatformBlockState bd = resolveRole(fungus, entry.getValue(), data, pos, paletteRng); + if (bd == null) { + continue; + } + resolved.put(pos, bd); + } + + return IrisProceduralBlocks.assemble(resolved); + } + + private static void buildUpright(Map roles, IrisFungus fungus, int height, RNG rng, long baseSeed) { + int stemHeight = Math.max(1, height); + Map stemCells = new HashMap<>(); + double[] top = FungusStemBuilder.build(stemCells, fungus, stemHeight, baseSeed); + for (Vector3i v : stemCells.keySet()) { + roles.put(v, FungusCellRole.STEM); + } + + int radius = pickRadius(fungus, rng); + double cx = top[0]; + double cz = top[2]; + int capBaseY = (int) Math.round(top[1]); + FungusCapBuilder.build(roles, fungus, radius, cx, capBaseY, cz, baseSeed + 5557L); + } + + private static int pickRadius(IrisFungus fungus, RNG rng) { + int lo = Math.min(fungus.getCapRadiusMin(), fungus.getCapRadiusMax()); + int hi = Math.max(fungus.getCapRadiusMin(), fungus.getCapRadiusMax()); + lo = Math.max(1, lo); + hi = Math.max(lo, hi); + return rng.i(lo, hi + 1); + } + + private static PlatformBlockState resolveRole(IrisFungus fungus, FungusCellRole role, IrisData data, Vector3i pos, RNG paletteRng) { + int x = pos.getBlockX(); + int y = pos.getBlockY(); + int z = pos.getBlockZ(); + return switch (role) { + case STEM -> IrisProceduralBlocks.resolve(fungus.getStem(), fungus.getStemPalette(), data, x, y, z, paletteRng); + case CAP -> IrisProceduralBlocks.resolve(fungus.getCap(), fungus.getCapPalette(), data, x, y, z, paletteRng); + case GILL -> { + PlatformBlockState gill = IrisProceduralBlocks.resolve(fungus.getGillBlock(), fungus.getGillPalette(), data, x, y, z, paletteRng); + yield gill != null ? gill : IrisProceduralBlocks.resolve(fungus.getCap(), fungus.getCapPalette(), data, x, y, z, paletteRng); + } + case SPOT -> { + PlatformBlockState spot = IrisProceduralBlocks.resolve(fungus.getSpotBlock(), fungus.getSpotPalette(), data, x, y, z, paletteRng); + yield spot != null ? spot : IrisProceduralBlocks.resolve(fungus.getCap(), fungus.getCapPalette(), data, x, y, z, paletteRng); + } + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusShelfBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusShelfBuilder.java new file mode 100644 index 000000000..4a90388fe --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusShelfBuilder.java @@ -0,0 +1,81 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.fungi; + +import art.arcane.iris.engine.object.IrisFungus; +import art.arcane.iris.engine.object.tree.TreeFunctions; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +import java.util.Map; + +public final class FungusShelfBuilder { + private FungusShelfBuilder() { + } + + public static void build(Map roles, IrisFungus fungus, RNG rng, long seed) { + int radius = Math.max(1, fungus.getShelfRadius()); + double azimuth = rng.d(0.0, Math.PI * 2.0); + double fanX = Math.sin(azimuth); + double fanZ = Math.cos(azimuth); + int stubHeight = Math.max(0, Math.min(2, fungus.getStemWidth())); + + for (int y = 0; y < stubHeight; y++) { + roles.put(new Vector3i(0, y, 0), FungusCellRole.STEM); + } + + int fanY = stubHeight; + boolean hasGill = FungusCapBuilder.blockOrPaletteSet(fungus.getGillBlock(), fungus.getGillPalette()); + boolean hasSpot = FungusCapBuilder.blockOrPaletteSet(fungus.getSpotBlock(), fungus.getSpotPalette()); + + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + double dist = Math.sqrt(dx * dx + dz * dz); + if (dist > radius + 0.5) { + continue; + } + double forward = dx * fanX + dz * fanZ; + if (forward < -0.5) { + continue; + } + double normalized = dist / Math.max(1.0, radius); + int lift = (int) Math.round(radius * 0.25 * (1.0 - normalized)); + int y = fanY + lift; + + FungusCellRole role = FungusCellRole.CAP; + if (hasSpot && spotNoise(dx, y, dz, seed + 9311L) <= fungus.getSpotChance()) { + role = FungusCellRole.SPOT; + } + roles.putIfAbsent(new Vector3i(dx, y, dz), role); + + if (hasGill && rollNoise(dx, y - 1, dz, seed + 4111L) <= fungus.getGillChance()) { + roles.putIfAbsent(new Vector3i(dx, y - 1, dz), FungusCellRole.GILL); + } + } + } + } + + private static double rollNoise(int x, int y, int z, long seed) { + return TreeFunctions.valueNoise3D(x, y, z, seed); + } + + private static double spotNoise(int x, int y, int z, long seed) { + return TreeFunctions.valueNoise3D(Math.floorDiv(x, 2), y, Math.floorDiv(z, 2), seed); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusStemBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusStemBuilder.java new file mode 100644 index 000000000..58d5120c0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/fungi/FungusStemBuilder.java @@ -0,0 +1,95 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.fungi; + +import art.arcane.iris.engine.object.IrisFungus; +import art.arcane.iris.engine.object.tree.TreeFunctions; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.Vector3i; + +import java.util.Map; + +public final class FungusStemBuilder { + private FungusStemBuilder() { + } + + public static double[] build(Map stemCells, IrisFungus fungus, int stemHeight, long seed) { + int width = Math.max(1, Math.min(3, fungus.getStemWidth())); + double maxLean = stemHeight * Math.tan(Math.toRadians(Math.max(0.0, fungus.getStemCurve()))); + double leanRad = Math.toRadians(fungus.getStemLeanAzimuth()); + double leanX = Math.sin(leanRad); + double leanZ = Math.cos(leanRad); + double waveAmp = Math.max(0.0, fungus.getStemWaveAmplitude()); + double wavePeriods = fungus.getStemWavePeriods() == 0 ? 1.0 : fungus.getStemWavePeriods(); + double waveAzimuth = Math.toRadians(fungus.getStemLeanAzimuth() + 90.0); + double waveX = Math.sin(waveAzimuth); + double waveZ = Math.cos(waveAzimuth); + + double topCx = 0.0; + double topCz = 0.0; + int topY = Math.max(0, stemHeight - 1); + + for (int y = 0; y < stemHeight; y++) { + double t = stemHeight <= 1 ? 0.0 : y / (double) (stemHeight - 1); + double lean = maxLean * (t * t); + double wave = waveAmp * Math.sin(2.0 * Math.PI * wavePeriods * t + TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0); + double cx = lean * leanX + wave * waveX; + double cz = lean * leanZ + wave * waveZ; + + for (int[] xz : squarePositions(cx, cz, width)) { + stemCells.put(new Vector3i(xz[0], y, xz[1]), null); + } + + topCx = cx; + topCz = cz; + } + + return new double[]{topCx, topY, topCz}; + } + + static int[][] squarePositions(double cx, double cz, int width) { + if (width % 2 == 1) { + int half = width / 2; + int icx = (int) Math.round(cx); + int icz = (int) Math.round(cz); + int[][] out = new int[width * width][2]; + int idx = 0; + for (int dx = -half; dx <= half; dx++) { + for (int dz = -half; dz <= half; dz++) { + out[idx][0] = icx + dx; + out[idx][1] = icz + dz; + idx++; + } + } + return out; + } + int ox = (int) Math.floor(cx) - width / 2 + 1; + int oz = (int) Math.floor(cz) - width / 2 + 1; + int[][] out = new int[width * width][2]; + int idx = 0; + for (int dx = 0; dx < width; dx++) { + for (int dz = 0; dz < width; dz++) { + out[idx][0] = ox + dx; + out[idx][1] = oz + dz; + idx++; + } + } + return out; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterObject.java b/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterObject.java new file mode 100644 index 000000000..4df8191a2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterObject.java @@ -0,0 +1,59 @@ +package art.arcane.iris.engine.object.matter; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.util.project.matter.IrisMatterContext; +import art.arcane.iris.util.project.matter.IrisMatterSupport; +import art.arcane.volmlib.util.matter.IrisMatter; +import art.arcane.volmlib.util.matter.Matter; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.File; +import java.io.IOException; + +@Data +@EqualsAndHashCode(callSuper = false) +public class IrisMatterObject extends IrisRegistrant { + private final Matter matter; + + public IrisMatterObject() { + this(1, 1, 1); + } + + public IrisMatterObject(int w, int h, int d) { + this(createMatter(w, h, d)); + } + + public IrisMatterObject(Matter matter) { + IrisMatterSupport.ensureRegistered(); + this.matter = matter; + } + + public static IrisMatterObject from(IrisObject object) { + return new IrisMatterObject(IrisMatterSupport.from(object)); + } + + public static IrisMatterObject from(File j, IrisData data) throws IOException { + IrisMatterSupport.ensureRegistered(); + try (IrisMatterContext.Scope scope = IrisMatterContext.open(data)) { + return new IrisMatterObject(Matter.read(j)); + } + } + + private static Matter createMatter(int w, int h, int d) { + IrisMatterSupport.ensureRegistered(); + return new IrisMatter(w, h, d); + } + + @Override + public String getFolderName() { + return "matter"; + } + + @Override + public String getTypeName() { + return "Matter"; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinBlockCanvas.java b/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinBlockCanvas.java new file mode 100644 index 000000000..a3333ccad --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinBlockCanvas.java @@ -0,0 +1,107 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.ruin; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.Vector3i; + +import java.util.HashMap; +import java.util.Map; + +final class RuinBlockCanvas { + enum Role { + STRUCTURE, + ACCENT + } + + static final class Cell { + private Role role; + private boolean structural; + private PlatformBlockState accentData; + + Cell(Role role, boolean structural) { + this.role = role; + this.structural = structural; + } + + Role role() { + return role; + } + + boolean structural() { + return structural; + } + + PlatformBlockState accentData() { + return accentData; + } + + void promote(Role next, boolean structuralNext) { + this.role = next; + this.structural = this.structural || structuralNext; + } + + void makeAccent(PlatformBlockState data) { + this.role = Role.ACCENT; + this.structural = false; + this.accentData = data; + } + } + + private final Map cells = new HashMap<>(); + + Map cells() { + return cells; + } + + void set(int x, int y, int z, Role role, boolean structural) { + Vector3i key = new Vector3i(x, y, z); + Cell existing = cells.get(key); + if (existing == null) { + cells.put(key, new Cell(role, structural)); + } else { + existing.promote(role, structural); + } + } + + void accent(int x, int y, int z, PlatformBlockState data) { + if (data == null) { + return; + } + cells.put(new Vector3i(x, y, z), accentCell(data)); + } + + private static Cell accentCell(PlatformBlockState data) { + Cell cell = new Cell(Role.ACCENT, false); + cell.makeAccent(data); + return cell; + } + + boolean has(int x, int y, int z) { + return cells.containsKey(new Vector3i(x, y, z)); + } + + Cell get(int x, int y, int z) { + return cells.get(new Vector3i(x, y, z)); + } + + void remove(int x, int y, int z) { + cells.remove(new Vector3i(x, y, z)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinDecoratorApplier.java b/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinDecoratorApplier.java new file mode 100644 index 000000000..6dee8bf8c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinDecoratorApplier.java @@ -0,0 +1,122 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.ruin; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.engine.object.IrisRuinDecorator; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class RuinDecoratorApplier { + private static final int[][] HORIZONTAL = {{1, 0, 0}, {-1, 0, 0}, {0, 0, 1}, {0, 0, -1}}; + + private RuinDecoratorApplier() { + } + + static void apply(RuinBlockCanvas canvas, IrisRuinDecorator decorator, IrisData data, long seed, RNG rng) { + List candidates = switch (decorator.getTarget()) { + case TOP -> topCandidates(canvas); + case SURFACE -> surfaceCandidates(canvas); + case BASE_SCATTER -> baseScatterCandidates(canvas, Math.max(1, decorator.getScatterRadius())); + }; + + RNG paletteRng = new RNG(seed); + for (Vector3i v : candidates) { + if (!rng.chance(decorator.getChance())) { + continue; + } + PlatformBlockState bd = IrisProceduralBlocks.resolve(decorator.getBlock(), decorator.getPalette(), data, v.getBlockX(), v.getBlockY(), v.getBlockZ(), paletteRng); + if (bd != null) { + canvas.accent(v.getBlockX(), v.getBlockY(), v.getBlockZ(), bd); + } + } + } + + private static List topCandidates(RuinBlockCanvas canvas) { + Map highest = new HashMap<>(); + for (Vector3i v : canvas.cells().keySet()) { + long key = columnKey(v.getBlockX(), v.getBlockZ()); + Integer current = highest.get(key); + if (current == null || v.getBlockY() > current) { + highest.put(key, v.getBlockY()); + } + } + List out = new ArrayList<>(); + for (Map.Entry entry : highest.entrySet()) { + int x = (int) (entry.getKey() >> 32); + int z = (int) (entry.getKey() & 0xFFFFFFFFL); + int y = entry.getValue() + 1; + if (!canvas.has(x, y, z)) { + out.add(new Vector3i(x, y, z)); + } + } + return out; + } + + private static List surfaceCandidates(RuinBlockCanvas canvas) { + List out = new ArrayList<>(); + for (Vector3i v : new ArrayList<>(canvas.cells().keySet())) { + for (int[] dir : HORIZONTAL) { + int nx = v.getBlockX() + dir[0]; + int ny = v.getBlockY(); + int nz = v.getBlockZ() + dir[2]; + if (!canvas.has(nx, ny, nz)) { + out.add(new Vector3i(nx, ny, nz)); + } + } + } + return out; + } + + private static List baseScatterCandidates(RuinBlockCanvas canvas, int radius) { + int minX = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + int minY = Integer.MAX_VALUE; + for (Vector3i v : canvas.cells().keySet()) { + minX = Math.min(minX, v.getBlockX()); + minZ = Math.min(minZ, v.getBlockZ()); + maxX = Math.max(maxX, v.getBlockX()); + maxZ = Math.max(maxZ, v.getBlockZ()); + minY = Math.min(minY, v.getBlockY()); + } + + List out = new ArrayList<>(); + for (int x = minX - radius; x <= maxX + radius; x++) { + for (int z = minZ - radius; z <= maxZ + radius; z++) { + if (!canvas.has(x, minY, z) && !canvas.has(x, minY + 1, z)) { + out.add(new Vector3i(x, minY, z)); + } + } + } + return out; + } + + private static long columnKey(int x, int z) { + return ((long) x << 32) | (z & 0xFFFFFFFFL); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinGenerator.java new file mode 100644 index 000000000..f63a7dc23 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinGenerator.java @@ -0,0 +1,199 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.ruin; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.engine.object.IrisRuin; +import art.arcane.iris.engine.object.tree.TreeFunctions; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class RuinGenerator { + private RuinGenerator() { + } + + public static IrisObject generate(IrisRuin ruin, int variantIndex, RNG rng, IrisData data) { + int count = Math.max(1, ruin.getVariants()); + int height = pick(ruin.getHeightMin(), ruin.getHeightMax(), variantIndex, count, rng); + int width = pick(ruin.getWidthMin(), ruin.getWidthMax(), variantIndex, count, rng); + int length = pick(ruin.getLengthMin(), ruin.getLengthMax(), variantIndex, count, rng); + + RuinBlockCanvas canvas = new RuinBlockCanvas(); + switch (ruin.getForm()) { + case PILLAR -> RuinShapes.pillar(canvas, Math.max(1, width), Math.max(2, height), rng); + case WALL -> RuinShapes.wall(canvas, Math.max(1, width), Math.max(2, length), Math.max(2, height), rng); + case ARCH -> RuinShapes.arch(canvas, Math.max(1, width), Math.max(3, length), Math.max(3, height), rng); + case FLOOR_SLAB -> RuinShapes.floorSlab(canvas, Math.max(3, width), Math.max(3, length), Math.max(1, Math.min(height, 2))); + case RUBBLE -> RuinShapes.rubble(canvas, Math.max(3, width), Math.max(3, length), Math.max(1, Math.min(height, 4)), rng); + } + + applyBurial(ruin, canvas, height); + applyErosion(ruin, canvas); + applyAccents(ruin, canvas, rng, data); + + return resolve(ruin, canvas, data); + } + + private static int pick(int lo, int hi, int variantIndex, int count, RNG rng) { + int min = Math.min(lo, hi); + int max = Math.max(lo, hi); + if (min == max || count <= 1) { + return rng.i(min, max + 1); + } + double step = (max - min) / (double) (count - 1); + double base = min + step * variantIndex; + double jitter = rng.d(-step * 0.3, step * 0.3); + return (int) Math.round(Math.max(min, Math.min(max, base + jitter))); + } + + private static void applyBurial(IrisRuin ruin, RuinBlockCanvas canvas, int height) { + double fraction = clamp01(ruin.getBuriedFraction()); + if (fraction <= 0.0) { + return; + } + int sink = (int) Math.round(height * fraction); + if (sink <= 0) { + return; + } + Map moved = new HashMap<>(); + for (Map.Entry entry : canvas.cells().entrySet()) { + Vector3i v = entry.getKey(); + moved.put(new Vector3i(v.getBlockX(), v.getBlockY() - sink, v.getBlockZ()), entry.getValue()); + } + canvas.cells().clear(); + canvas.cells().putAll(moved); + } + + private static void applyErosion(IrisRuin ruin, RuinBlockCanvas canvas) { + double erosion = clamp01(ruin.getErosion()); + if (erosion <= 0.0) { + return; + } + double scale = Math.max(0.0001, ruin.getErosionScale()); + long erosionSeed = ruin.getSeed() * 0x2545F4914F6CDD1DL + 17L; + int minY = Integer.MAX_VALUE; + for (Vector3i v : canvas.cells().keySet()) { + minY = Math.min(minY, v.getBlockY()); + } + + List doomed = new ArrayList<>(); + for (Map.Entry entry : canvas.cells().entrySet()) { + Vector3i v = entry.getKey(); + RuinBlockCanvas.Cell cell = entry.getValue(); + if (cell.structural() || v.getBlockY() <= minY) { + continue; + } + int sx = (int) Math.round(v.getBlockX() * scale); + int sy = (int) Math.round(v.getBlockY() * scale); + int sz = (int) Math.round(v.getBlockZ() * scale); + double n = TreeFunctions.valueNoise3D(sx, sy, sz, erosionSeed); + if (n < erosion) { + doomed.add(v); + } + } + for (Vector3i v : doomed) { + canvas.cells().remove(v); + } + } + + private static void applyAccents(IrisRuin ruin, RuinBlockCanvas canvas, RNG rng, IrisData data) { + if (ruin.getAccents() == null || ruin.getAccents().isEmpty()) { + return; + } + int decoratorIndex = 0; + for (art.arcane.iris.engine.object.IrisRuinDecorator decorator : ruin.getAccents()) { + RuinDecoratorApplier.apply(canvas, decorator, data, ruin.getSeed() + decoratorIndex * 911L, rng.nextParallelRNG(decoratorIndex + 1)); + decoratorIndex++; + } + } + + private static IrisObject resolve(IrisRuin ruin, RuinBlockCanvas canvas, IrisData data) { + if (canvas.cells().isEmpty()) { + return null; + } + double scale = Math.max(0.0001, ruin.getWeatheringScale()); + double mossiness = clamp01(ruin.getMossiness()); + long weatherSeed = ruin.getSeed() * 0x9E3779B97F4A7C15L + 101L; + RNG paletteRng = new RNG(ruin.getSeed()); + + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + for (Vector3i v : canvas.cells().keySet()) { + minY = Math.min(minY, v.getBlockY()); + maxY = Math.max(maxY, v.getBlockY()); + } + int span = Math.max(1, maxY - minY); + + Map blocks = new HashMap<>(); + for (Map.Entry entry : canvas.cells().entrySet()) { + Vector3i v = entry.getKey(); + RuinBlockCanvas.Cell cell = entry.getValue(); + PlatformBlockState bd; + if (cell.role() == RuinBlockCanvas.Role.ACCENT) { + bd = cell.accentData(); + } else { + bd = structuralBlock(ruin, cell, v, data, paletteRng, scale, mossiness, weatherSeed, minY, span); + } + if (bd != null) { + blocks.put(v, bd); + } + } + + return IrisProceduralBlocks.assemble(blocks); + } + + private static PlatformBlockState structuralBlock(IrisRuin ruin, RuinBlockCanvas.Cell cell, Vector3i v, IrisData data, RNG paletteRng, double scale, double mossiness, long weatherSeed, int minY, int span) { + boolean weathered = false; + if (mossiness > 0.0) { + int sx = (int) Math.round(v.getBlockX() * scale); + int sy = (int) Math.round(v.getBlockY() * scale); + int sz = (int) Math.round(v.getBlockZ() * scale); + double n = TreeFunctions.valueNoise3D(sx, sy, sz, weatherSeed); + double normalized = (v.getBlockY() - minY) / (double) span; + double gradient = 1.0 - 0.6 * normalized; + weathered = n < mossiness * gradient; + } + + if (weathered) { + PlatformBlockState wd = IrisProceduralBlocks.resolve(ruin.getWeatheredBlock(), ruin.getWeatheringPalette(), data, v.getBlockX(), v.getBlockY(), v.getBlockZ(), paletteRng); + if (wd != null) { + return wd; + } + } + return IrisProceduralBlocks.resolve(ruin.getBlock(), ruin.getBlockPalette(), data, v.getBlockX(), v.getBlockY(), v.getBlockZ(), paletteRng); + } + + private static double clamp01(double value) { + if (value < 0.0) { + return 0.0; + } + if (value > 1.0) { + return 1.0; + } + return value; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinShapes.java b/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinShapes.java new file mode 100644 index 000000000..db012067a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/ruin/RuinShapes.java @@ -0,0 +1,145 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.ruin; + +import art.arcane.volmlib.util.math.RNG; + +final class RuinShapes { + private RuinShapes() { + } + + static void pillar(RuinBlockCanvas canvas, int width, int height, RNG rng) { + int half = (width - 1) / 2; + int broken = Math.max(2, height - rng.i(0, Math.max(1, height / 3))); + for (int y = 0; y < broken; y++) { + boolean bottom = y == 0; + int jitter = y >= broken - 1 ? rng.i(0, 2) : 0; + for (int x = -half; x <= half; x++) { + for (int z = -half; z <= half; z++) { + if (y == broken - 1 && jitter > 0 && (Math.abs(x) == half || Math.abs(z) == half) && rng.chance(0.5)) { + continue; + } + canvas.set(x, y, z, RuinBlockCanvas.Role.STRUCTURE, bottom); + } + } + } + + int stub = rng.i(0, 3); + if (stub > 0) { + int dir = rng.i(0, 4); + int dx = dir == 0 ? 1 : (dir == 1 ? -1 : 0); + int dz = dir == 2 ? 1 : (dir == 3 ? -1 : 0); + for (int s = 1; s <= stub; s++) { + canvas.set((half + s) * dx, 0, (half + s) * dz, RuinBlockCanvas.Role.STRUCTURE, true); + } + } + } + + static void wall(RuinBlockCanvas canvas, int thickness, int length, int height, RNG rng) { + int half = (length - 1) / 2; + int tHalf = (thickness - 1) / 2; + long edgeSeed = rng.lmax(); + for (int z = -half; z <= half; z++) { + int top = height - jaggedDrop(z, height, edgeSeed); + for (int y = 0; y < top; y++) { + if (y > 0 && rng.chance(0.10)) { + continue; + } + for (int x = -tHalf; x <= thickness - 1 - tHalf; x++) { + canvas.set(x, y, z, RuinBlockCanvas.Role.STRUCTURE, y == 0); + } + } + } + } + + static void arch(RuinBlockCanvas canvas, int thickness, int span, int height, RNG rng) { + int legSpacing = Math.max(2, span); + int legGap = (legSpacing - 1) / 2; + int tHalf = (thickness - 1) / 2; + int legTop = Math.max(2, height - Math.max(2, (legSpacing / 2))); + + for (int side = -1; side <= 1; side += 2) { + int legZ = side * legGap; + for (int y = 0; y < legTop; y++) { + for (int x = -tHalf; x <= thickness - 1 - tHalf; x++) { + canvas.set(x, y, legZ, RuinBlockCanvas.Role.STRUCTURE, y <= 1); + } + } + } + + double radius = legGap; + int crown = legTop + (int) Math.round(radius); + for (int z = -legGap; z <= legGap; z++) { + double inside = radius * radius - (z * z); + if (inside < 0) { + continue; + } + int y = legTop + (int) Math.round(Math.sqrt(inside)); + for (int yy = legTop; yy <= y && yy <= crown; yy++) { + boolean keystone = Math.abs(z) <= 1; + for (int x = -tHalf; x <= thickness - 1 - tHalf; x++) { + canvas.set(x, yy, z, RuinBlockCanvas.Role.STRUCTURE, keystone); + } + } + } + } + + static void floorSlab(RuinBlockCanvas canvas, int width, int length, int thickness) { + int xHalf = (width - 1) / 2; + int zHalf = (length - 1) / 2; + for (int y = 0; y < thickness; y++) { + for (int x = -xHalf; x <= width - 1 - xHalf; x++) { + for (int z = -zHalf; z <= length - 1 - zHalf; z++) { + canvas.set(x, y, z, RuinBlockCanvas.Role.STRUCTURE, y == 0); + } + } + } + } + + static void rubble(RuinBlockCanvas canvas, int width, int length, int peak, RNG rng) { + int xHalf = (width - 1) / 2; + int zHalf = (length - 1) / 2; + long blobSeed = rng.lmax(); + double rx = Math.max(1.0, width / 2.0); + double rz = Math.max(1.0, length / 2.0); + + for (int x = -xHalf; x <= width - 1 - xHalf; x++) { + for (int z = -zHalf; z <= length - 1 - zHalf; z++) { + double radial = (x * x) / (rx * rx) + (z * z) / (rz * rz); + if (radial > 1.2) { + continue; + } + double falloff = Math.max(0.0, 1.0 - radial); + double noise = art.arcane.iris.engine.object.tree.TreeFunctions.valueNoise3D(x, 0, z, blobSeed); + int columnHeight = (int) Math.round(peak * falloff * (0.6 + 0.4 * noise)); + for (int y = 0; y <= columnHeight; y++) { + if (y > 0 && rng.chance(0.25)) { + continue; + } + canvas.set(x, y, z, RuinBlockCanvas.Role.STRUCTURE, y == 0 && radial < 0.5); + } + } + } + } + + private static int jaggedDrop(int z, int height, long seed) { + double n = art.arcane.iris.engine.object.tree.TreeFunctions.valueNoise1D(z * 0.9, seed); + return (int) Math.round(n * Math.max(1, height - 1) * 0.6); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/ProceduralTreeGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/tree/ProceduralTreeGenerator.java new file mode 100644 index 000000000..a6dbe7e36 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/ProceduralTreeGenerator.java @@ -0,0 +1,126 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class ProceduralTreeGenerator { + private ProceduralTreeGenerator() { + } + + public static IrisObject generate(IrisProceduralTree tree, int height, RNG rng, IrisData data) { + TreeBlockCanvas canvas = new TreeBlockCanvas(); + long baseSeed = rng.getSeed(); + + List limbs = TreeTrunkBuilder.build(canvas, tree, height); + + boolean wantEndpoints = tree.getDecorators() != null && !tree.getDecorators().isEmpty(); + List branchEndpoints = wantEndpoints ? new ArrayList<>() : null; + + int limbIndex = 0; + for (TreeTrunkBuilder.Limb limb : limbs) { + TreeCanopyBuilder.build(canvas, tree, height, limb.offsets(), limb.branchStartY(), baseSeed + limbIndex * 131L, branchEndpoints); + limbIndex++; + } + + if (wantEndpoints) { + TreeDecoratorApplier.apply(canvas, tree, baseSeed, branchEndpoints); + } + + if (tree.isRoots()) { + TreeRootBuilder.build(canvas, tree, height, baseSeed); + } + + if (tree.isPlausible()) { + TreeSupport.ensureLeavesSupported(canvas, 24); + } + + Map resolved = new HashMap<>(); + Set trunkPositions = new HashSet<>(); + Set leafPositions = new HashSet<>(); + for (Map.Entry entry : canvas.getCells().entrySet()) { + TreeBlockCanvas.Cell cell = entry.getValue(); + PlatformBlockState state = TreeBlockResolver.resolve(tree, data, cell, entry.getKey()); + if (state == null) { + continue; + } + resolved.put(entry.getKey(), state); + if (cell.role() == TreeBlockCanvas.Role.TRUNK || cell.role() == TreeBlockCanvas.Role.SECONDARY_TRUNK) { + trunkPositions.add(entry.getKey()); + } else if (cell.role() == TreeBlockCanvas.Role.LEAF || cell.role() == TreeBlockCanvas.Role.SECONDARY_LEAF) { + leafPositions.add(entry.getKey()); + } + } + + TreePlausibility.apply(resolved, trunkPositions, leafPositions, tree); + + return assemble(resolved); + } + + private static IrisObject assemble(Map resolved) { + if (resolved.isEmpty()) { + return null; + } + + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (TreeBlockCanvas.Vec v : resolved.keySet()) { + minX = Math.min(minX, v.x()); + minY = Math.min(minY, v.y()); + minZ = Math.min(minZ, v.z()); + maxX = Math.max(maxX, v.x()); + maxY = Math.max(maxY, v.y()); + maxZ = Math.max(maxZ, v.z()); + } + + int w = maxX - minX + 1; + int h = maxY - minY + 1; + int d = maxZ - minZ + 1; + int cx = w / 2; + int cy = h / 2; + int cz = d / 2; + + IrisObject object = new IrisObject(w, h, d); + for (Map.Entry entry : resolved.entrySet()) { + TreeBlockCanvas.Vec v = entry.getKey(); + int nx = v.x() - minX - cx; + int ny = v.y() - cy + 1; + int nz = v.z() - minZ - cz; + object.getBlocks().put(new IrisBlockVector(nx, ny, nz), entry.getValue()); + } + + return object; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockCanvas.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockCanvas.java new file mode 100644 index 000000000..1a70a4c6a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockCanvas.java @@ -0,0 +1,102 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class TreeBlockCanvas { + public record Vec(int x, int y, int z) { + } + + public enum Role { + TRUNK, SECONDARY_TRUNK, LEAF, SECONDARY_LEAF, DECORATOR + } + + public enum Axis { + NONE, X, Y, Z + } + + public record Cell(Role role, Axis axis, boolean exposed, int decoratorIndex, String facing) { + } + + private final Map cells = new HashMap<>(); + private final Set trunk = new HashSet<>(); + private final Set leaf = new HashSet<>(); + + public boolean has(int x, int y, int z) { + return cells.containsKey(new Vec(x, y, z)); + } + + public Cell get(int x, int y, int z) { + return cells.get(new Vec(x, y, z)); + } + + public void setTrunk(int x, int y, int z, Role role, Axis axis) { + Vec v = new Vec(x, y, z); + cells.put(v, new Cell(role, axis, false, -1, null)); + trunk.add(v); + leaf.remove(v); + } + + public boolean setLeaf(int x, int y, int z, Role role) { + Vec v = new Vec(x, y, z); + if (cells.containsKey(v)) { + return false; + } + cells.put(v, new Cell(role, Axis.NONE, false, -1, null)); + leaf.add(v); + return true; + } + + public boolean setDecor(int x, int y, int z, int decoratorIndex, String facing) { + Vec v = new Vec(x, y, z); + if (cells.containsKey(v)) { + return false; + } + cells.put(v, new Cell(Role.DECORATOR, Axis.NONE, false, decoratorIndex, facing)); + return true; + } + + public void markExposed(int x, int y, int z) { + Vec v = new Vec(x, y, z); + Cell c = cells.get(v); + if (c != null && !c.exposed()) { + cells.put(v, new Cell(c.role(), c.axis(), true, c.decoratorIndex(), c.facing())); + } + } + + public Map getCells() { + return cells; + } + + public Set getTrunk() { + return trunk; + } + + public Set getLeaf() { + return leaf; + } + + public boolean isEmpty() { + return cells.isEmpty(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockResolver.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockResolver.java new file mode 100644 index 000000000..04c73e4af --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeBlockResolver.java @@ -0,0 +1,170 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.engine.object.IrisTreeDecorator; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.math.RNG; + +public final class TreeBlockResolver { + private TreeBlockResolver() { + } + + public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos) { + RNG paletteRng = new RNG(tree.getSeed()); + RNG posRng = new RNG(tree.getSeed() ^ positionHash(pos)); + + switch (cell.role()) { + case TRUNK -> { + PlatformBlockState state = resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng); + return finishTrunk(state, cell); + } + case SECONDARY_TRUNK -> { + PlatformBlockState state = resolveBlock(tree.getSecondaryTrunk(), tree.getSecondaryTrunkPalette(), data, pos, paletteRng); + if (state == null) { + state = resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng); + } + return finishTrunk(state, cell); + } + case LEAF -> { + return resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng); + } + case SECONDARY_LEAF -> { + PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng, posRng); + if (state == null) { + state = resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng); + } + return state; + } + case DECORATOR -> { + int index = cell.decoratorIndex(); + if (index < 0 || tree.getDecorators() == null || index >= tree.getDecorators().size()) { + return null; + } + IrisTreeDecorator dec = tree.getDecorators().get(index); + PlatformBlockState state = resolveBlock(dec.getBlock(), dec.getPalette(), data, pos, paletteRng); + if (state != null && cell.facing() != null && IrisProceduralBlocks.hasProperty(state, "facing")) { + try { + state = state.withProperty("facing", cell.facing().toLowerCase()); + } catch (IllegalArgumentException ignored) { + } + } + return state; + } + default -> { + return null; + } + } + } + + private static PlatformBlockState finishTrunk(PlatformBlockState state, TreeBlockCanvas.Cell cell) { + if (state == null) { + return null; + } + if (cell.axis() != TreeBlockCanvas.Axis.NONE && IrisProceduralBlocks.hasProperty(state, "axis")) { + try { + state = state.withProperty("axis", cell.axis().name().toLowerCase()); + } catch (IllegalArgumentException ignored) { + } + } + if (cell.exposed()) { + return woodCap(state); + } + return state; + } + + private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng, RNG posRng) { + if (TreeTrunkBuilder.paletteSet(tree.getSecondaryLeavesPalette())) { + return tree.getSecondaryLeavesPalette().get(paletteRng, pos.x(), pos.y(), pos.z(), data); + } + if (tree.getWeightedSecondaryLeaves() != null && !tree.getWeightedSecondaryLeaves().isEmpty()) { + String picked = pickWeighted(tree, posRng); + return picked == null ? null : B.getStateOrNull(picked, false); + } + if (tree.getSecondaryLeaves() != null && !tree.getSecondaryLeaves().isEmpty()) { + return B.getStateOrNull(tree.getSecondaryLeaves(), false); + } + return null; + } + + private static String pickWeighted(IrisProceduralTree tree, RNG rng) { + int total = 0; + for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) { + total += Math.max(0, s.getWeight()); + } + if (total <= 0) { + return null; + } + double r = rng.nextDouble() * total; + double cumulative = 0; + for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) { + cumulative += Math.max(0, s.getWeight()); + if (r < cumulative) { + return s.getBlock(); + } + } + return tree.getWeightedSecondaryLeaves().get(tree.getWeightedSecondaryLeaves().size() - 1).getBlock(); + } + + private static PlatformBlockState resolveBlock(String block, IrisMaterialPalette palette, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng) { + if (TreeTrunkBuilder.paletteSet(palette)) { + return palette.get(paletteRng, pos.x(), pos.y(), pos.z(), data); + } + if (block != null && !block.isEmpty()) { + return B.getStateOrNull(block, false); + } + return null; + } + + private static PlatformBlockState woodCap(PlatformBlockState state) { + String key = state.key(); + int bracket = key.indexOf('['); + String base = bracket < 0 ? key : key.substring(0, bracket); + String woodKey = null; + if (base.endsWith("_log")) { + woodKey = base.substring(0, base.length() - 4) + "_wood"; + } else if (base.endsWith("_stem")) { + woodKey = base.substring(0, base.length() - 5) + "_hyphae"; + } + if (woodKey == null) { + return state; + } + PlatformBlockState wood = B.getStateOrNull(woodKey, false); + if (wood == null) { + return state; + } + String axis = IrisProceduralBlocks.propertyValue(state, "axis"); + if (axis != null && IrisProceduralBlocks.hasProperty(wood, "axis")) { + try { + wood = wood.withProperty("axis", axis); + } catch (IllegalArgumentException ignored) { + } + } + return wood; + } + + private static long positionHash(TreeBlockCanvas.Vec pos) { + return pos.x() * 73856093L ^ pos.y() * 19349663L ^ pos.z() * 83492791L; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeCanopyBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeCanopyBuilder.java new file mode 100644 index 000000000..ae59834af --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeCanopyBuilder.java @@ -0,0 +1,351 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.engine.object.IrisTreeBranches; +import art.arcane.iris.engine.object.IrisTreeCanopy; +import art.arcane.iris.engine.object.IrisTreeLayer; +import art.arcane.iris.engine.object.IrisTreeLeafMode; +import art.arcane.iris.engine.object.IrisTreeProfile; +import art.arcane.iris.engine.object.IrisTreeSubBranches; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +import java.util.List; + +public final class TreeCanopyBuilder { + private TreeCanopyBuilder() { + } + + public static void build(TreeBlockCanvas canvas, IrisProceduralTree tree, int height, double[][] offsets, + int branchStartY, long baseSeed, List branchEndpoints) { + IrisTreeCanopy canopy = tree.getCanopy(); + if (canopy == null) { + canopy = new IrisTreeCanopy(); + } + IrisTreeBranches branches = canopy.getBranches(); + + double[][] layers = resolveLayers(canopy, tree.getProfile(), height, branches != null); + + if (branches != null) { + if (layers.length > 0) { + double[][] crown = new double[][]{layers[layers.length - 1]}; + volumeCanopy(canvas, tree, crown, canopy, baseSeed, offsets); + } + placeApexCap(canvas, tree, height, layers, offsets, baseSeed); + branchCanopy(canvas, tree, height, branches, branchStartY, baseSeed, offsets, branchEndpoints); + } else { + volumeCanopy(canvas, tree, layers, canopy, baseSeed, offsets); + } + } + + private static void placeApexCap(TreeBlockCanvas canvas, IrisProceduralTree tree, int height, double[][] layers, + double[][] offsets, long baseSeed) { + int ocx = 0; + int ocz = 0; + if (offsets != null && offsets.length > 0) { + ocx = (int) Math.round(offsets[offsets.length - 1][0]); + ocz = (int) Math.round(offsets[offsets.length - 1][1]); + } + double maxR = 2; + for (double[] layer : layers) { + maxR = Math.max(maxR, layer[1]); + } + int capRadius = Math.max(2, Math.min(3, (int) Math.round(maxR * 0.35))); + IrisTreeCanopy canopy = tree.getCanopy(); + IrisTreeLeafMode mode = canopy != null ? canopy.getMode() : IrisTreeLeafMode.TRIMMED; + double density = canopy != null ? canopy.getLeafDensity() : 1.0; + placeLeafCluster(canvas, tree, ocx, height - 1, ocz, capRadius, mode, density, baseSeed + 5555L); + } + + private static double[][] resolveLayers(IrisTreeCanopy canopy, IrisTreeProfile profile, int height, boolean branchDriven) { + KList explicit = canopy.getLayers(); + if (explicit != null && !explicit.isEmpty()) { + double[][] out = new double[explicit.size()][2]; + for (int i = 0; i < explicit.size(); i++) { + out[i][0] = explicit.get(i).getYOffset(); + out[i][1] = explicit.get(i).getRadius(); + } + return out; + } + return TreeProfiles.presetLayers(profile, height, branchDriven); + } + + private static void volumeCanopy(TreeBlockCanvas canvas, IrisProceduralTree tree, double[][] layers, + IrisTreeCanopy canopy, long baseSeed, double[][] offsets) { + int ocx = 0; + int ocz = 0; + if (offsets != null && offsets.length > 0) { + ocx = (int) Math.round(offsets[offsets.length - 1][0]); + ocz = (int) Math.round(offsets[offsets.length - 1][1]); + } + + double startAngle = canopy.getStartAngle(); + double squish = canopy.getSquish(); + IrisTreeLeafMode mode = canopy.getMode(); + double leafDensity = canopy.getLeafDensity(); + + for (double[] layer : layers) { + int yCenter = (int) Math.round(layer[0]); + double radius = layer[1]; + int halfH = layerHalfHeight(radius, startAngle, squish); + int downH = startAngle < 90.0 + ? (int) Math.ceil(radius * Math.cos(Math.toRadians(startAngle)) * squish) + : 0; + int skirt = downH; + + for (int y = yCenter - skirt; y <= yCenter + halfH; y++) { + int dy = y - yCenter; + double layerR; + if (halfH > 0 && dy >= 0) { + layerR = radius * Math.sqrt(Math.max(0.0, 1.0 - Math.pow(dy / (double) halfH, 2))); + } else if (skirt > 0 && dy < 0) { + layerR = radius * Math.sqrt(Math.max(0.0, 1.0 - Math.pow(dy / (double) skirt, 2))); + } else { + layerR = radius; + } + if (layerR < 0.5) { + continue; + } + placeLeafDisc(canvas, tree, ocx, y, ocz, layerR, mode, leafDensity, baseSeed + y); + } + } + } + + private static int layerHalfHeight(double radius, double startAngle, double squish) { + if (startAngle >= 180.0) { + return 0; + } + return (int) Math.ceil(radius * squish); + } + + private static void placeLeafDisc(TreeBlockCanvas canvas, IrisProceduralTree tree, int cx, int cy, int cz, + double radius, IrisTreeLeafMode mode, double density, long seed) { + double sx = stretchX(tree); + double sz = stretchZ(tree); + RNG rng = new RNG(seed); + int irx = (int) Math.ceil(radius * sx); + int irz = (int) Math.ceil(radius * sz); + for (int dx = -irx; dx <= irx; dx++) { + for (int dz = -irz; dz <= irz; dz++) { + double ddx = dx / sx; + double ddz = dz / sz; + double dist = Math.sqrt(ddx * ddx + ddz * ddz); + boolean corner = Math.abs(dx) == irx && Math.abs(dz) == irz; + if (!passesLeafTest(mode, dist, radius, density, rng, cx + dx, cy, cz + dz, seed, corner)) { + continue; + } + canvas.setLeaf(cx + dx, cy, cz + dz, resolveLeaf(tree, rng)); + } + } + } + + private static void placeLeafCluster(TreeBlockCanvas canvas, IrisProceduralTree tree, int cx, int cy, int cz, + int radius, IrisTreeLeafMode mode, double density, long seed) { + double sx = stretchX(tree); + double sz = stretchZ(tree); + RNG rng = new RNG(seed); + int irx = (int) Math.ceil(radius * sx); + int irz = (int) Math.ceil(radius * sz); + for (int dx = -irx; dx <= irx; dx++) { + for (int dy = -radius; dy <= radius; dy++) { + for (int dz = -irz; dz <= irz; dz++) { + double ddx = dx / sx; + double ddz = dz / sz; + double dist = Math.sqrt(ddx * ddx + dy * dy + ddz * ddz); + boolean corner = Math.abs(dx) == irx && Math.abs(dz) == irz; + if (!passesLeafTest(mode, dist, radius, density, rng, cx + dx, cy + dy, cz + dz, seed, corner)) { + continue; + } + canvas.setLeaf(cx + dx, cy + dy, cz + dz, resolveLeaf(tree, rng)); + } + } + } + } + + private static double stretchX(IrisProceduralTree tree) { + IrisTreeCanopy c = tree.getCanopy(); + return c == null ? 1.0 : Math.max(0.1, c.getCrownStretchX()); + } + + private static double stretchZ(IrisProceduralTree tree) { + IrisTreeCanopy c = tree.getCanopy(); + return c == null ? 1.0 : Math.max(0.1, c.getCrownStretchZ()); + } + + private static boolean passesLeafTest(IrisTreeLeafMode mode, double dist, double radius, double density, + RNG rng, int wx, int wy, int wz, long seed, boolean corner) { + switch (mode) { + case TRIMMED -> { + return !(dist > radius || corner); + } + case FILLED -> { + return dist <= radius; + } + case DENSITY -> { + if (dist > radius) { + return false; + } + return rng.nextDouble() <= density * (1.0 - (dist / (radius + 1.0))); + } + case NOISE -> { + if (dist > radius) { + return false; + } + return TreeFunctions.valueNoise3D(wx, wy, wz, seed) <= density; + } + case HOLLOW -> { + return dist <= radius && dist > radius - 1.0; + } + case GRADIENT -> { + if (dist > radius) { + return false; + } + return rng.nextDouble() <= density * Math.pow(1.0 - (dist / (radius + 1.0)), 2); + } + case CLUMPED -> { + if (dist > radius) { + return false; + } + return TreeFunctions.valueNoise3D(Math.floorDiv(wx, 2), Math.floorDiv(wy, 2), Math.floorDiv(wz, 2), seed) <= density; + } + case TATTERED -> { + if (dist > radius || corner) { + return false; + } + return !(dist > radius - 1.5 && rng.nextDouble() < 0.6); + } + case SPARSE -> { + if (dist > radius) { + return false; + } + return rng.nextDouble() <= density * 0.4; + } + default -> { + return dist <= radius; + } + } + } + + private static TreeBlockCanvas.Role resolveLeaf(IrisProceduralTree tree, RNG rng) { + boolean hasSecondary = TreeTrunkBuilder.paletteSet(tree.getSecondaryLeavesPalette()) + || (tree.getWeightedSecondaryLeaves() != null && !tree.getWeightedSecondaryLeaves().isEmpty()) + || (tree.getSecondaryLeaves() != null && !tree.getSecondaryLeaves().isEmpty()); + if (!hasSecondary || tree.getSecondaryLeafFraction() <= 0.0) { + return TreeBlockCanvas.Role.LEAF; + } + if (rng.nextDouble() >= tree.getSecondaryLeafFraction()) { + return TreeBlockCanvas.Role.LEAF; + } + return TreeBlockCanvas.Role.SECONDARY_LEAF; + } + + private static void branchCanopy(TreeBlockCanvas canvas, IrisProceduralTree tree, int height, + IrisTreeBranches branches, int branchStartY, long baseSeed, double[][] offsets, + List branchEndpoints) { + long branchSeed = baseSeed + 9999; + RNG branchRng = new RNG(branchSeed); + IrisTreeSubBranches sub = branches.getSubBranches(); + int depth = branches.getBranchDepth(); + int branchIndex = 0; + + for (int y = Math.max(0, branchStartY); y < height; y++) { + double t = y / (double) Math.max(height - 1, 1); + double p = TreeFunctions.branchProbability(branches, t, branchSeed); + if (branchRng.nextDouble() > p) { + continue; + } + + int ox = 0; + int oz = 0; + if (offsets != null && y < offsets.length) { + ox = (int) Math.round(offsets[y][0]); + oz = (int) Math.round(offsets[y][1]); + } + + double az = TreeFunctions.azimuthDegrees(branches.getAzimuthMode(), t, branchIndex, tree, branches.getAzimuth(), branchRng); + branchIndex++; + double branchLen = TreeFunctions.branchLength(branches, t); + double effElevation = branches.isLeafStartUp() ? Math.max(0.0, branches.getElevation()) : branches.getElevation(); + int[] end = branchEndpoint(ox, y, oz, az, effElevation, branchLen); + int[] tip = rasterizeBranch(canvas, ox, y, oz, end[0], end[1], end[2], branches.getSag()); + + if (branchEndpoints != null) { + branchEndpoints.add(new int[]{tip[0], tip[1], tip[2], ox, oz}); + } + placeLeafCluster(canvas, tree, tip[0], tip[1], tip[2], branches.getClusterRadius(), + branches.getClusterMode(), branches.getClusterDensity(), branchSeed + y); + + growSubBranches(canvas, tree, tip[0], tip[1], tip[2], az, effElevation, branchLen, sub, depth, branchSeed + y + 1000L); + } + } + + private static void growSubBranches(TreeBlockCanvas canvas, IrisProceduralTree tree, int ox, int oy, int oz, + double az, double el, double len, IrisTreeSubBranches sub, int depth, long seed) { + if (sub == null || depth <= 0) { + return; + } + int count = Math.max(1, sub.getCount()); + for (int si = 0; si < count; si++) { + double yawOffset = sub.getYawDelta() * (si - (count - 1) / 2.0); + double subAz = az + yawOffset; + double subEl = el + sub.getPitchDelta(); + double subLen = len * sub.getLengthScale(); + int[] end = branchEndpoint(ox, oy, oz, subAz, subEl, subLen); + int[] tip = rasterizeBranch(canvas, ox, oy, oz, end[0], end[1], end[2], sub.getSag()); + placeLeafCluster(canvas, tree, tip[0], tip[1], tip[2], sub.getClusterRadius(), + sub.getClusterMode(), sub.getClusterDensity(), seed + si * 131L); + growSubBranches(canvas, tree, tip[0], tip[1], tip[2], subAz, subEl, subLen, sub, depth - 1, seed + si * 131L + 17L); + } + } + + private static int[] branchEndpoint(int ox, int oy, int oz, double azimuthDeg, double elevationDeg, double length) { + double azRad = Math.toRadians(azimuthDeg); + double elRad = Math.toRadians(elevationDeg); + double dx = length * Math.cos(elRad) * Math.sin(azRad); + double dy = length * Math.sin(elRad); + double dz = length * Math.cos(elRad) * Math.cos(azRad); + return new int[]{ox + (int) Math.round(dx), oy + (int) Math.round(dy), oz + (int) Math.round(dz)}; + } + + private static int[] rasterizeBranch(TreeBlockCanvas canvas, int ox, int oy, int oz, int ex, int ey, int ez, double sag) { + int dx = ex - ox; + int dy = ey - oy; + int dz = ez - oz; + double horizontal = Math.sqrt(dx * dx + dz * dz); + double sagBlocks = sag * horizontal; + int steps = Math.max(Math.max(Math.abs(dx), Math.abs(dz)), + Math.max((int) (Math.abs(dy) + Math.ceil(sagBlocks)), 1)); + TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz); + + int[] tip = new int[]{ox, oy, oz}; + for (int i = 0; i <= steps; i++) { + double t = i / (double) steps; + int x = (int) Math.round(ox + dx * t); + int y = (int) Math.round(oy + dy * t - sagBlocks * t * t); + int z = (int) Math.round(oz + dz * t); + if (!canvas.has(x, y, z)) { + canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis); + } + tip = new int[]{x, y, z}; + } + return tip; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplier.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplier.java new file mode 100644 index 000000000..a4f34ffa4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeDecoratorApplier.java @@ -0,0 +1,280 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.engine.object.IrisTreeDecorator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.math.RNG; + +import java.util.List; +import java.util.Map; + +public final class TreeDecoratorApplier { + private TreeDecoratorApplier() { + } + + public static void apply(TreeBlockCanvas canvas, IrisProceduralTree tree, long baseSeed, List branchEndpoints) { + KList decorators = tree.getDecorators(); + if (decorators == null || decorators.isEmpty()) { + return; + } + RNG rng = new RNG(baseSeed + 77777L); + + for (int idx = 0; idx < decorators.size(); idx++) { + IrisTreeDecorator dec = decorators.get(idx); + boolean hasBlock = (dec.getBlock() != null && !dec.getBlock().isEmpty()) || TreeTrunkBuilder.paletteSet(dec.getPalette()); + if (!hasBlock) { + continue; + } + switch (dec.getTarget()) { + case BRANCH_TIP -> branchTip(canvas, dec, idx, branchEndpoints, rng); + case TRUNK_SURFACE -> trunkSurface(canvas, dec, idx, rng); + case CANOPY_TOP -> canopyTop(canvas, dec, idx, rng); + case CANOPY_BOTTOM -> canopyBottom(canvas, dec, idx, rng); + case TRUNK_BASE -> trunkBase(canvas, dec, idx, rng); + case LEAF_SURFACE -> leafSurface(canvas, dec, idx, rng); + case CANOPY_HANG -> canopyHang(canvas, dec, idx, rng); + case BRANCH_SURFACE -> branchSurface(canvas, dec, idx, rng); + case TRUNK_TOP -> trunkTop(canvas, dec, idx, rng); + case GROUND_SCATTER -> groundScatter(canvas, dec, idx, rng); + } + } + } + + private static void branchTip(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, List branchEndpoints, RNG rng) { + if (branchEndpoints == null) { + return; + } + for (int[] e : branchEndpoints) { + if (rng.nextDouble() > dec.getChance()) { + continue; + } + if (canvas.has(e[0], e[1], e[2])) { + continue; + } + String facing = dec.isAxisAware() ? facingAway(e[3], e[4], e[0], e[2]) : null; + canvas.setDecor(e[0], e[1], e[2], idx, facing); + } + } + + private static void trunkSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { + int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + String[] facings = {"west", "east", "north", "south"}; + for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) { + for (int i = 0; i < sides.length; i++) { + int nx = v.x() + sides[i][0]; + int nz = v.z() + sides[i][1]; + if (canvas.has(nx, v.y(), nz) || rng.nextDouble() > dec.getChance()) { + continue; + } + canvas.setDecor(nx, v.y(), nz, idx, dec.isAxisAware() ? facings[i] : null); + } + } + } + + private static void leafSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { + int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + String[] facings = {"west", "east", "north", "south"}; + for (TreeBlockCanvas.Vec v : new KList<>(canvas.getLeaf())) { + for (int i = 0; i < sides.length; i++) { + int nx = v.x() + sides[i][0]; + int nz = v.z() + sides[i][1]; + if (canvas.has(nx, v.y(), nz) || rng.nextDouble() > dec.getChance()) { + continue; + } + canvas.setDecor(nx, v.y(), nz, idx, dec.isAxisAware() ? facings[i] : null); + } + } + } + + private static void branchSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { + for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) { + int ay = v.y() + 1; + if (canvas.has(v.x(), ay, v.z()) || rng.nextDouble() > dec.getChance()) { + continue; + } + canvas.setDecor(v.x(), ay, v.z(), idx, null); + } + } + + private static void canopyTop(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { + KMap colTop = new KMap<>(); + for (TreeBlockCanvas.Vec v : canvas.getCells().keySet()) { + long key = pack(v.x(), v.z()); + Integer cur = colTop.get(key); + if (cur == null || v.y() > cur) { + colTop.put(key, v.y()); + } + } + for (Map.Entry e : colTop.entrySet()) { + int x = unpackX(e.getKey()); + int z = unpackZ(e.getKey()); + int aboveY = e.getValue() + 1; + if (canvas.has(x, aboveY, z) || rng.nextDouble() > dec.getChance()) { + continue; + } + canvas.setDecor(x, aboveY, z, idx, null); + } + } + + private static void canopyBottom(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { + KMap colBottom = columnLeafBottoms(canvas); + for (Map.Entry e : colBottom.entrySet()) { + int x = unpackX(e.getKey()); + int z = unpackZ(e.getKey()); + int belowY = e.getValue() - 1; + if (canvas.has(x, belowY, z) || rng.nextDouble() > dec.getChance()) { + continue; + } + canvas.setDecor(x, belowY, z, idx, null); + } + } + + private static void canopyHang(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { + int maxLen = Math.max(1, dec.getLength()); + KMap colBottom = columnLeafBottoms(canvas); + for (Map.Entry e : colBottom.entrySet()) { + if (rng.nextDouble() > dec.getChance()) { + continue; + } + int x = unpackX(e.getKey()); + int z = unpackZ(e.getKey()); + int strand = rng.i(1, maxLen + 1); + for (int k = 1; k <= strand; k++) { + int yy = e.getValue() - k; + if (canvas.has(x, yy, z)) { + break; + } + canvas.setDecor(x, yy, z, idx, null); + } + } + } + + private static void trunkBase(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { + int[][] sides = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; + for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) { + if (v.y() != 0) { + continue; + } + for (int[] side : sides) { + int nx = v.x() + side[0]; + int nz = v.z() + side[1]; + if (canvas.has(nx, 0, nz) || rng.nextDouble() > dec.getChance()) { + continue; + } + canvas.setDecor(nx, 0, nz, idx, null); + } + } + } + + private static void trunkTop(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { + int maxY = Integer.MIN_VALUE; + for (TreeBlockCanvas.Vec v : canvas.getTrunk()) { + maxY = Math.max(maxY, v.y()); + } + if (maxY == Integer.MIN_VALUE) { + return; + } + for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) { + if (v.y() != maxY) { + continue; + } + int aboveY = v.y() + 1; + if (canvas.has(v.x(), aboveY, v.z()) || rng.nextDouble() > dec.getChance()) { + continue; + } + canvas.setDecor(v.x(), aboveY, v.z(), idx, null); + } + } + + private static void groundScatter(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { + double cx = 0; + double cz = 0; + int count = 0; + double footprint = 1.0; + for (TreeBlockCanvas.Vec v : canvas.getTrunk()) { + if (v.y() == 0) { + cx += v.x(); + cz += v.z(); + count++; + } + } + if (count == 0) { + return; + } + cx /= count; + cz /= count; + for (TreeBlockCanvas.Vec v : canvas.getTrunk()) { + if (v.y() == 0) { + footprint = Math.max(footprint, Math.hypot(v.x() - cx, v.z() - cz)); + } + } + int radius = (int) Math.ceil(footprint + 3.0); + int icx = (int) Math.round(cx); + int icz = (int) Math.round(cz); + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + if (Math.hypot(dx, dz) > radius) { + continue; + } + int x = icx + dx; + int z = icz + dz; + if (canvas.has(x, 0, z) || rng.nextDouble() > dec.getChance()) { + continue; + } + canvas.setDecor(x, 0, z, idx, null); + } + } + } + + private static KMap columnLeafBottoms(TreeBlockCanvas canvas) { + KMap colBottom = new KMap<>(); + for (TreeBlockCanvas.Vec v : canvas.getLeaf()) { + long key = pack(v.x(), v.z()); + Integer cur = colBottom.get(key); + if (cur == null || v.y() < cur) { + colBottom.put(key, v.y()); + } + } + return colBottom; + } + + private static String facingAway(int cx, int cz, int x, int z) { + int dx = x - cx; + int dz = z - cz; + if (Math.abs(dx) >= Math.abs(dz)) { + return dx >= 0 ? "east" : "west"; + } + return dz >= 0 ? "south" : "north"; + } + + private static long pack(int x, int z) { + return (((long) x) << 32) ^ (z & 0xFFFFFFFFL); + } + + private static int unpackX(long key) { + return (int) (key >> 32); + } + + private static int unpackZ(long key) { + return (int) (key & 0xFFFFFFFFL); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeFunctions.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeFunctions.java new file mode 100644 index 000000000..1c70b8522 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeFunctions.java @@ -0,0 +1,197 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.engine.object.IrisTreeAzimuthMode; +import art.arcane.iris.engine.object.IrisTreeBranches; +import art.arcane.iris.engine.object.IrisTreeFunction; +import art.arcane.volmlib.util.math.RNG; + +public final class TreeFunctions { + private static final double GOLDEN_ANGLE = 137.50776405003785; + + private TreeFunctions() { + } + + public static double trunkWidthMultiplier(IrisProceduralTree tree, double t) { + double start = tree.getShapeStart(); + double end = tree.getShapeEnd(); + return switch (tree.getTrunkShape()) { + case CONSTANT -> 1.0; + case LINEAR -> start + (end - start) * t; + case SIGMOID -> 1.0 / (1.0 + Math.exp(tree.getShapeSteepness() * (t - 0.5))); + case LOG -> { + double base = Math.max(1.0001, tree.getShapeBase()); + if (t <= 0.0) { + yield 1.0; + } + yield Math.max(0.0, 1.0 - Math.log(1.0 + t * (base - 1.0)) / Math.log(base)); + } + case SINE -> { + double period = tree.getShapePeriod() == 0 ? 1.0 : tree.getShapePeriod(); + yield 1.0 + tree.getShapeAmplitude() * Math.sin(2.0 * Math.PI * t / period); + } + case PARABOLIC -> { + double peak = tree.getShapePeakOffset(); + double floor = tree.getShapeFloor(); + double denom = Math.pow(Math.max(peak, 1.0 - peak), 2); + if (denom == 0) { + denom = 1; + } + yield floor + (1.0 - floor) * (Math.pow(t - peak, 2) / denom); + } + case EXPONENTIAL -> geometric(start, end, t); + case SQRT -> start + (end - start) * Math.sqrt(t); + case STEP -> t < tree.getShapePeakOffset() ? start : end; + case BELL -> start + (end - start) * bell(t); + case EASE_IN_OUT -> start + (end - start) * smoothstep(t); + }; + } + + public static double curveProgress(IrisTreeFunction fn, double t, double steepness) { + return switch (fn) { + case LINEAR, CONSTANT, SINE -> t; + case LOG -> Math.log(1.0 + t * (Math.E - 1.0)); + case SIGMOID -> 1.0 / (1.0 + Math.exp(-steepness * (t - 0.5))); + case PARABOLIC, EASE_IN_OUT -> smoothstep(t); + case EXPONENTIAL -> Math.pow(t, Math.max(1.0, steepness)); + case SQRT -> Math.sqrt(t); + case STEP -> t < 0.5 ? 0.0 : 1.0; + case BELL -> bell(t); + }; + } + + public static double branchLength(IrisTreeBranches b, double t) { + double base = b.getLengthBase(); + double crown = b.getLengthCrown(); + double max = b.getLengthMax(); + double v = switch (b.getLengthFunction()) { + case CONSTANT -> b.getLengthConstant(); + case LINEAR, SINE -> base + (crown - base) * t; + case SIGMOID -> max / (1.0 + Math.exp(-b.getLengthSteepness() * (t - 0.5))); + case LOG -> max * Math.log(1.0 + t * (Math.E - 1.0)); + case PARABOLIC -> max * (1.0 - Math.pow(2.0 * t - 1.0, 2)); + case EXPONENTIAL -> geometric(base, crown, t); + case SQRT -> base + (crown - base) * Math.sqrt(t); + case STEP -> t < 0.5 ? base : crown; + case BELL -> base + (max - base) * bell(t); + case EASE_IN_OUT -> base + (crown - base) * smoothstep(t); + }; + return Math.max(1.0, v); + } + + public static double branchProbability(IrisTreeBranches b, double t, long seed) { + double std = Math.max(b.getProbabilityStd(), 1e-6); + return switch (b.getProbabilityFunction()) { + case CONSTANT -> b.getProbabilityConstant(); + case LINEAR -> b.getProbabilityBase() + (b.getProbabilityCrown() - b.getProbabilityBase()) * t; + case SIGMOID -> 1.0 / (1.0 + Math.exp(-b.getProbabilitySteepness() * (t - b.getProbabilityMidpoint()))); + case TOP_HEAVY -> Math.pow(t, b.getProbabilityExponent()); + case GAUSSIAN -> Math.exp(-0.5 * Math.pow((t - b.getProbabilityMean()) / std, 2)); + case NOISE -> valueNoise1D(t * b.getProbabilityScale(), seed); + case BOTTOM_HEAVY -> Math.pow(1.0 - t, b.getProbabilityExponent()); + case PERIODIC -> b.getProbabilityConstant() * Math.pow(Math.max(0.0, Math.sin(2.0 * Math.PI * t * b.getProbabilityPeriods())), 8); + case BAND -> Math.abs(t - b.getProbabilityMean()) <= b.getProbabilityStd() ? b.getProbabilityConstant() : 0.0; + case INVERSE_GAUSSIAN -> b.getProbabilityConstant() * (1.0 - Math.exp(-0.5 * Math.pow((t - b.getProbabilityMean()) / std, 2))); + case EXPONENTIAL_DECAY -> b.getProbabilityConstant() * Math.exp(-b.getProbabilityExponent() * t); + }; + } + + public static double azimuthDegrees(IrisTreeAzimuthMode mode, double t, int index, IrisProceduralTree tree, double constantBase, RNG rng) { + return switch (mode) { + case CONSTANT -> constantBase; + case LINEAR -> tree.getAzimuthStart() + (tree.getAzimuthEnd() - tree.getAzimuthStart()) * t; + case SPIRAL -> tree.getAzimuthStart() + tree.getAzimuthTurns() * 360.0 * t; + case SINE -> { + double period = tree.getAzimuthPeriod() == 0 ? 1.0 : tree.getAzimuthPeriod(); + yield tree.getAzimuthOffset() + tree.getAzimuthAmplitude() * Math.sin(2.0 * Math.PI * t / period); + } + case NOISE -> noiseAzimuth(t, tree.getAzimuthScale(), tree.getSeed()); + case RANDOM -> rng != null ? rng.d(0.0, 360.0) : constantBase; + case GOLDEN_ANGLE -> index >= 0 ? constantBase + index * GOLDEN_ANGLE : constantBase; + case ALTERNATING -> index >= 0 ? constantBase + (index % 2) * 180.0 : constantBase; + case WHORL -> { + int count = Math.max(1, tree.getAzimuthWhorlCount()); + yield index >= 0 ? constantBase + (index % count) * (360.0 / count) : constantBase; + } + case ZIGZAG -> index >= 0 ? constantBase + ((index % 2 == 0) ? -tree.getAzimuthAmplitude() : tree.getAzimuthAmplitude()) : constantBase; + }; + } + + public static TreeBlockCanvas.Axis logAxis(double dx, double dy, double dz) { + double ax = Math.abs(dx); + double ay = Math.abs(dy) * 1.8; + double az = Math.abs(dz); + if (ay >= ax && ay >= az) { + return TreeBlockCanvas.Axis.Y; + } + if (ax >= az) { + return TreeBlockCanvas.Axis.X; + } + return TreeBlockCanvas.Axis.Z; + } + + private static double geometric(double a, double b, double t) { + if (a <= 0.0 || b <= 0.0) { + return a + (b - a) * t; + } + return a * Math.pow(b / a, t); + } + + private static double bell(double t) { + return Math.exp(-Math.pow(t - 0.5, 2) / (2.0 * 0.15 * 0.15)); + } + + private static double smoothstep(double t) { + return 3.0 * t * t - 2.0 * t * t * t; + } + + public static double noiseAzimuth(double t, double scale, long seed) { + double a = valueNoise1D(t * scale, seed); + double b = valueNoise1D(t * scale * 0.5, seed + 1); + return ((a + b) / 2.0) * 360.0; + } + + public static double valueNoise1D(double x, long seed) { + long h = mix(seed * 0x9E3779B97F4A7C15L + Math.round(x * 1000.0)); + return unit(h); + } + + public static double valueNoise3D(int x, int y, int z, long seed) { + long h = mix(seed); + h = mix(h + x * 0x9E3779B97F4A7C15L); + h = mix(h + y * 0xC2B2AE3D27D4EB4FL); + h = mix(h + z * 0x165667B19E3779F9L); + return unit(h); + } + + private static long mix(long a) { + a ^= (a >>> 33); + a *= 0xff51afd7ed558ccdL; + a ^= (a >>> 33); + a *= 0xc4ceb9fe1a85ec53L; + a ^= (a >>> 33); + return a; + } + + private static double unit(long h) { + return (h >>> 11) * 0x1.0p-53; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreePlausibility.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreePlausibility.java new file mode 100644 index 000000000..b9e6af329 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreePlausibility.java @@ -0,0 +1,104 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.spi.PlatformBlockState; + +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public final class TreePlausibility { + private static final int MAX_DISTANCE = 7; + private static final int[][] NEIGHBORS = { + {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1} + }; + + private TreePlausibility() { + } + + public static void apply(Map resolved, Set trunkPositions, + Set leafPositions, IrisProceduralTree tree) { + Set realLeaves = new HashSet<>(); + for (TreeBlockCanvas.Vec v : leafPositions) { + PlatformBlockState state = resolved.get(v); + if (state != null && IrisProceduralBlocks.hasProperty(state, "persistent") && IrisProceduralBlocks.hasProperty(state, "distance")) { + realLeaves.add(v); + } + } + if (realLeaves.isEmpty()) { + return; + } + + Map distance = computeDistances(trunkPositions, realLeaves); + + for (TreeBlockCanvas.Vec v : realLeaves) { + PlatformBlockState leaf = resolved.get(v); + Integer d = distance.get(v); + if (!tree.isPlausible()) { + leaf = leaf.withProperty("persistent", "true").withProperty("distance", "1"); + } else if (d != null && d < MAX_DISTANCE) { + leaf = leaf.withProperty("persistent", "false").withProperty("distance", String.valueOf(Math.max(1, d))); + } else { + leaf = leaf.withProperty("persistent", "true").withProperty("distance", String.valueOf(MAX_DISTANCE)); + } + resolved.put(v, leaf); + } + } + + static Map computeDistances(Set trunkPositions, Set realLeaves) { + Map distance = new HashMap<>(); + ArrayDeque queue = new ArrayDeque<>(); + + for (TreeBlockCanvas.Vec v : realLeaves) { + for (int[] n : NEIGHBORS) { + if (trunkPositions.contains(new TreeBlockCanvas.Vec(v.x() + n[0], v.y() + n[1], v.z() + n[2]))) { + distance.put(v, 1); + queue.add(v); + break; + } + } + } + + while (!queue.isEmpty()) { + TreeBlockCanvas.Vec v = queue.poll(); + int d = distance.get(v); + if (d >= MAX_DISTANCE) { + continue; + } + for (int[] n : NEIGHBORS) { + TreeBlockCanvas.Vec nv = new TreeBlockCanvas.Vec(v.x() + n[0], v.y() + n[1], v.z() + n[2]); + if (!realLeaves.contains(nv)) { + continue; + } + Integer existing = distance.get(nv); + if (existing == null || existing > d + 1) { + distance.put(nv, d + 1); + queue.add(nv); + } + } + } + + return distance; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeProfiles.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeProfiles.java new file mode 100644 index 000000000..1e31797f9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeProfiles.java @@ -0,0 +1,79 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisTreeProfile; + +public final class TreeProfiles { + private TreeProfiles() { + } + + static double radiusScale(IrisTreeProfile profile) { + return switch (profile) { + case OAK, DARK_OAK, CHERRY -> 1.0; + case BIRCH -> 0.7; + case SPRUCE -> 0.55; + case JUNGLE -> 0.45; + case ACACIA -> 0.6; + case DARK_OAK_FLAT -> 1.25; + case DARK_OAK_FLAT_WIDE -> 1.8; + case PALM -> 0.5; + case WILLOW -> 1.1; + case COLUMNAR -> 0.35; + case BUSH -> 1.3; + case MEGA_SPRUCE -> 0.7; + }; + } + + static double[][] fractions(IrisTreeProfile profile) { + return switch (profile) { + case OAK -> new double[][]{{0.55, 0.6}, {0.65, 0.9}, {0.75, 1.0}, {0.85, 0.9}, {0.92, 0.65}, {0.98, 0.35}}; + case BIRCH -> new double[][]{{0.6, 0.45}, {0.7, 0.75}, {0.8, 0.85}, {0.88, 0.75}, {0.94, 0.45}, {0.99, 0.2}}; + case SPRUCE, MEGA_SPRUCE -> new double[][]{{0.3, 1.0}, {0.42, 0.85}, {0.54, 0.7}, {0.65, 0.55}, {0.75, 0.4}, {0.84, 0.25}, {0.91, 0.15}, {0.97, 0.05}}; + case JUNGLE -> new double[][]{{0.82, 0.4}, {0.88, 0.8}, {0.93, 1.0}, {0.97, 0.7}, {1.0, 0.3}}; + case ACACIA -> new double[][]{{0.9, 0.5}, {0.95, 0.8}, {0.99, 0.4}}; + case DARK_OAK -> new double[][]{{0.5, 0.7}, {0.62, 1.0}, {0.72, 1.0}, {0.82, 0.9}, {0.91, 0.6}, {0.97, 0.3}}; + case DARK_OAK_FLAT -> new double[][]{{0.74, 0.62}, {0.83, 1.0}, {0.92, 1.0}, {1.0, 0.6}}; + case DARK_OAK_FLAT_WIDE -> new double[][]{{0.74, 0.62}, {0.83, 1.0}, {0.92, 1.0}, {1.0, 0.62}}; + case CHERRY -> new double[][]{{0.5, 0.5}, {0.62, 0.9}, {0.74, 1.0}, {0.84, 1.0}, {0.92, 0.7}, {0.98, 0.4}}; + case PALM -> new double[][]{{0.92, 0.45}, {0.97, 0.9}, {1.0, 1.0}, {1.03, 0.6}}; + case WILLOW -> new double[][]{{0.55, 0.7}, {0.68, 1.0}, {0.8, 1.0}, {0.9, 0.85}, {0.97, 0.6}}; + case COLUMNAR -> new double[][]{{0.4, 0.6}, {0.55, 0.9}, {0.7, 1.0}, {0.82, 0.9}, {0.92, 0.7}, {0.98, 0.4}}; + case BUSH -> new double[][]{{0.25, 0.7}, {0.45, 1.0}, {0.65, 1.0}, {0.82, 0.8}, {0.95, 0.5}}; + }; + } + + static double[][] presetLayers(IrisTreeProfile profile, int height, boolean branchDriven) { + double scale = radiusScale(profile); + int baseRadius = Math.max(3, (int) Math.round(height / 2.0 * scale)); + double[][] fractions = fractions(profile); + + int crownExtra = branchDriven ? 0 : Math.max(2, height / 4); + int yTop = (height - 1) + crownExtra; + + double[][] layers = new double[fractions.length][2]; + for (int i = 0; i < fractions.length; i++) { + int yOff = (int) Math.round(fractions[i][0] * yTop); + double radius = Math.max(1.5, baseRadius * fractions[i][1]); + layers[i][0] = yOff; + layers[i][1] = radius; + } + return layers; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeRootBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeRootBuilder.java new file mode 100644 index 000000000..5302ed52f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeRootBuilder.java @@ -0,0 +1,154 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.engine.object.IrisTreeRootStyle; +import art.arcane.volmlib.util.math.RNG; + +import java.util.ArrayList; +import java.util.List; + +public final class TreeRootBuilder { + private TreeRootBuilder() { + } + + public static void build(TreeBlockCanvas canvas, IrisProceduralTree tree, int height, long baseSeed) { + List cells = new ArrayList<>(); + for (TreeBlockCanvas.Vec v : canvas.getTrunk()) { + if (v.y() == 0) { + cells.add(new int[]{v.x(), v.z()}); + } + } + if (cells.isEmpty()) { + return; + } + + int depth = tree.getRootDepth() > 0 ? tree.getRootDepth() : autoDepth(height); + RNG rng = new RNG(baseSeed ^ 0x5009L); + + double cx = 0; + double cz = 0; + for (int[] c : cells) { + cx += c[0]; + cz += c[1]; + } + cx /= cells.size(); + cz /= cells.size(); + + double baseRadius = 1.0; + for (int[] c : cells) { + baseRadius = Math.max(baseRadius, Math.hypot(c[0] - cx, c[1] - cz) + 0.5); + } + double flare = tree.getRootFlare() > 0 ? tree.getRootFlare() : baseRadius + Math.max(2.0, depth * 0.6); + + IrisTreeRootStyle style = tree.getRootStyle(); + switch (style) { + case TAPROOT -> taproot(canvas, cells, cx, cz, baseRadius, depth); + case BUTTRESS -> { + taproot(canvas, cells, cx, cz, baseRadius, depth); + buttress(canvas, cells, cx, cz, baseRadius, depth, flare, rng); + } + case STILT -> { + taproot(canvas, cells, cx, cz, baseRadius, Math.min(depth, 3)); + stilts(canvas, cx, cz, baseRadius, flare, height, cells.size(), rng); + } + } + } + + private static void taproot(TreeBlockCanvas canvas, List cells, double cx, double cz, double baseRadius, int depth) { + int icx = (int) Math.round(cx); + int icz = (int) Math.round(cz); + for (int k = 1; k <= depth; k++) { + double frac = k / (double) depth; + double keepR = baseRadius * (1.0 - 0.6 * frac); + for (int[] c : cells) { + if (Math.hypot(c[0] - cx, c[1] - cz) <= keepR + 1e-6) { + placeRoot(canvas, c[0], -k, c[1], TreeBlockCanvas.Axis.Y); + } + } + placeRoot(canvas, icx, -k, icz, TreeBlockCanvas.Axis.Y); + } + } + + private static void buttress(TreeBlockCanvas canvas, List cells, double cx, double cz, double baseRadius, + int depth, double flare, RNG rng) { + int nLegs = Math.max(4, Math.min(12, cells.size() + 2)); + for (int li = 0; li < nLegs; li++) { + double ang = 2.0 * Math.PI * li / nLegs + rng.d(-0.25, 0.25); + double ca = Math.cos(ang); + double sa = Math.sin(ang); + int prevX = (int) Math.round(cx + baseRadius * ca); + int prevZ = (int) Math.round(cz + baseRadius * sa); + for (int k = 1; k <= depth; k++) { + double frac = k / (double) depth; + double r = baseRadius + (flare - baseRadius) * frac; + int x = (int) Math.round(cx + r * ca); + int z = (int) Math.round(cz + r * sa); + placeRoot(canvas, x, -k, z, TreeBlockCanvas.Axis.Y); + if (x != prevX || z != prevZ) { + placeRoot(canvas, prevX, -k, prevZ, TreeBlockCanvas.Axis.Y); + } + prevX = x; + prevZ = z; + } + } + } + + private static void stilts(TreeBlockCanvas canvas, double cx, double cz, double baseRadius, double flare, + int height, int cellCount, RNG rng) { + int stiltTop = Math.max(2, Math.min(7, (int) Math.round(0.25 * height))); + int nProps = Math.max(4, Math.min(10, cellCount + 2)); + for (int p = 0; p < nProps; p++) { + double ang = 2.0 * Math.PI * p / nProps + rng.d(-0.25, 0.25); + double ca = Math.cos(ang); + double sa = Math.sin(ang); + int sx = (int) Math.round(cx + baseRadius * ca); + int sz = (int) Math.round(cz + baseRadius * sa); + int fx = (int) Math.round(cx + flare * ca); + int fz = (int) Math.round(cz + flare * sa); + rasterizeRoot(canvas, sx, stiltTop, sz, fx, -2, fz); + } + } + + private static void rasterizeRoot(TreeBlockCanvas canvas, int ox, int oy, int oz, int ex, int ey, int ez) { + int dx = ex - ox; + int dy = ey - oy; + int dz = ez - oz; + int steps = Math.max(Math.max(Math.abs(dx), Math.abs(dy)), Math.max(Math.abs(dz), 1)); + TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz); + for (int i = 0; i <= steps; i++) { + double t = i / (double) steps; + int x = (int) Math.round(ox + dx * t); + int y = (int) Math.round(oy + dy * t); + int z = (int) Math.round(oz + dz * t); + placeRoot(canvas, x, y, z, axis); + } + } + + private static void placeRoot(TreeBlockCanvas canvas, int x, int y, int z, TreeBlockCanvas.Axis axis) { + if (!canvas.has(x, y, z)) { + canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis); + } + } + + private static int autoDepth(int height) { + return Math.max(2, Math.min(16, (int) Math.round(0.18 * height))); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeSupport.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeSupport.java new file mode 100644 index 000000000..6f24d9e16 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeSupport.java @@ -0,0 +1,90 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import java.util.ArrayList; +import java.util.Map; + +public final class TreeSupport { + private static final int LEGAL_DISTANCE = 6; + private static final int REACH_GAP = 4; + + private TreeSupport() { + } + + public static void ensureLeavesSupported(TreeBlockCanvas canvas, int maxTendrils) { + for (int iter = 0; iter < maxTendrils; iter++) { + Map dist = TreePlausibility.computeDistances(canvas.getTrunk(), canvas.getLeaf()); + + TreeBlockCanvas.Vec worstLeaf = null; + TreeBlockCanvas.Vec worstWood = null; + int worstGap = -1; + + for (TreeBlockCanvas.Vec leaf : new ArrayList<>(canvas.getLeaf())) { + Integer d = dist.get(leaf); + if (d != null && d <= LEGAL_DISTANCE) { + continue; + } + TreeBlockCanvas.Vec nearest = null; + int best = Integer.MAX_VALUE; + for (TreeBlockCanvas.Vec wood : canvas.getTrunk()) { + int cd = chebyshev(leaf, wood); + if (cd < best) { + best = cd; + nearest = wood; + if (cd <= 1) { + break; + } + } + } + if (nearest != null && best > worstGap) { + worstGap = best; + worstLeaf = leaf; + worstWood = nearest; + } + } + + if (worstLeaf == null) { + return; + } + growTendril(canvas, worstWood, worstLeaf); + } + } + + private static void growTendril(TreeBlockCanvas canvas, TreeBlockCanvas.Vec from, TreeBlockCanvas.Vec to) { + int dx = to.x() - from.x(); + int dy = to.y() - from.y(); + int dz = to.z() - from.z(); + int length = Math.max(Math.max(Math.abs(dx), Math.abs(dy)), Math.max(Math.abs(dz), 1)); + int steps = Math.max(1, length - REACH_GAP); + TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz); + + for (int i = 1; i <= steps; i++) { + double t = i / (double) length; + int x = from.x() + (int) Math.round(dx * t); + int y = from.y() + (int) Math.round(dy * t); + int z = from.z() + (int) Math.round(dz * t); + canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis); + } + } + + private static int chebyshev(TreeBlockCanvas.Vec a, TreeBlockCanvas.Vec b) { + return Math.max(Math.max(Math.abs(a.x() - b.x()), Math.abs(a.y() - b.y())), Math.abs(a.z() - b.z())); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/tree/TreeTrunkBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeTrunkBuilder.java new file mode 100644 index 000000000..e69365a12 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/tree/TreeTrunkBuilder.java @@ -0,0 +1,206 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object.tree; + +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisProceduralTree; +import art.arcane.iris.engine.object.IrisTreeAzimuthMode; + +import java.util.ArrayList; +import java.util.List; + +public final class TreeTrunkBuilder { + public record Limb(double[][] offsets, int branchStartY) { + } + + private TreeTrunkBuilder() { + } + + public static List build(TreeBlockCanvas canvas, IrisProceduralTree tree, int height) { + boolean hasSecondary = hasSecondaryTrunk(tree); + double[][] mainOffsets = leanPath(tree, height); + int forks = Math.max(1, tree.getTrunkForks()); + + List limbs = new ArrayList<>(); + if (forks == 1) { + placeColumn(canvas, tree, mainOffsets, height, 0, height - 1, hasSecondary); + limbs.add(new Limb(mainOffsets, 0)); + markExposedEnds(canvas); + return limbs; + } + + int forkY = Math.max(1, Math.min(height - 2, (int) Math.round(tree.getForkHeight() * (height - 1)))); + placeColumn(canvas, tree, mainOffsets, height, 0, forkY, hasSecondary); + + double baseCx = mainOffsets[forkY][0]; + double baseCz = mainOffsets[forkY][1]; + double reachMax = (height - forkY) * Math.tan(Math.toRadians(tree.getForkAngle())); + + for (int f = 0; f < forks; f++) { + double az = Math.toRadians((360.0 / forks) * f); + double[][] forkOffsets = new double[height][2]; + for (int y = 0; y <= forkY; y++) { + forkOffsets[y][0] = mainOffsets[y][0]; + forkOffsets[y][1] = mainOffsets[y][1]; + } + for (int y = forkY + 1; y < height; y++) { + double p = (y - forkY) / (double) Math.max(1, height - 1 - forkY); + double reach = reachMax * p; + forkOffsets[y][0] = baseCx + reach * Math.sin(az); + forkOffsets[y][1] = baseCz + reach * Math.cos(az); + } + placeColumn(canvas, tree, forkOffsets, height, forkY + 1, height - 1, hasSecondary); + limbs.add(new Limb(forkOffsets, forkY)); + } + + markExposedEnds(canvas); + return limbs; + } + + private static double[][] leanPath(IrisProceduralTree tree, int height) { + double[][] offsets = new double[height][2]; + for (int y = 0; y < height; y++) { + double[] lean = leanOffset(tree, y, height); + offsets[y][0] = lean[0]; + offsets[y][1] = lean[1]; + } + return offsets; + } + + private static void placeColumn(TreeBlockCanvas canvas, IrisProceduralTree tree, double[][] offsets, int height, + int yStart, int yEnd, boolean hasSecondary) { + double prevCx = yStart > 0 ? offsets[yStart - 1][0] : 0.0; + double prevCz = yStart > 0 ? offsets[yStart - 1][1] : 0.0; + + for (int y = yStart; y <= yEnd; y++) { + double cx = offsets[y][0]; + double cz = offsets[y][1]; + int w = widthAt(tree, y, height); + TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(cx - prevCx, 1.0, cz - prevCz); + TreeBlockCanvas.Role role = trunkRole(tree, hasSecondary, y, height); + + for (int[] xz : squarePositions(cx, cz, w)) { + canvas.setTrunk(xz[0], y, xz[1], role, axis); + } + + double shift = Math.hypot(cx - prevCx, cz - prevCz); + if (shift > 1.0) { + int steps = (int) Math.ceil(shift); + for (int s = 1; s < steps; s++) { + double tFill = s / (double) steps; + double icx = prevCx + (cx - prevCx) * tFill; + double icz = prevCz + (cz - prevCz) * tFill; + int fillY = (int) Math.round(y - 1 + tFill); + for (int[] xz : squarePositions(icx, icz, w)) { + if (!canvas.has(xz[0], fillY, xz[1])) { + canvas.setTrunk(xz[0], fillY, xz[1], role, axis); + } + } + } + } + + prevCx = cx; + prevCz = cz; + } + } + + private static void markExposedEnds(TreeBlockCanvas canvas) { + for (TreeBlockCanvas.Vec v : new ArrayList<>(canvas.getTrunk())) { + if (!canvas.has(v.x(), v.y() + 1, v.z()) || !canvas.has(v.x(), v.y() - 1, v.z())) { + canvas.markExposed(v.x(), v.y(), v.z()); + } + } + } + + private static int widthAt(IrisProceduralTree tree, int y, int height) { + double t = y / (double) Math.max(height - 1, 1); + double multiplier = TreeFunctions.trunkWidthMultiplier(tree, t); + return Math.max(1, (int) Math.round(tree.getTrunkWidth() * multiplier)); + } + + private static TreeBlockCanvas.Role trunkRole(IrisProceduralTree tree, boolean hasSecondary, int y, int height) { + if (!hasSecondary) { + return TreeBlockCanvas.Role.TRUNK; + } + double t = y / (double) Math.max(height - 1, 1); + if (tree.getSecondaryTrunkStart() <= t && t <= tree.getSecondaryTrunkEnd()) { + return TreeBlockCanvas.Role.SECONDARY_TRUNK; + } + return TreeBlockCanvas.Role.TRUNK; + } + + private static boolean hasSecondaryTrunk(IrisProceduralTree tree) { + if (paletteSet(tree.getSecondaryTrunkPalette())) { + return true; + } + return tree.getSecondaryTrunk() != null && !tree.getSecondaryTrunk().isEmpty(); + } + + static boolean paletteSet(IrisMaterialPalette palette) { + return palette != null && palette.getPalette() != null && !palette.getPalette().isEmpty(); + } + + private static double[] leanOffset(IrisProceduralTree tree, int y, int height) { + if (tree.getLeanAngle() == 0.0 || height <= 1) { + return new double[]{0.0, 0.0}; + } + double t = y / (double) Math.max(height - 1, 1); + double progress = TreeFunctions.curveProgress(tree.getTrunkCurve(), t, tree.getCurveSteepness()); + double maxReach = height * Math.tan(Math.toRadians(tree.getLeanAngle())); + double reach = maxReach * progress; + double azDeg = tree.getLeanAzimuthMode() == IrisTreeAzimuthMode.CONSTANT + ? tree.getLeanAzimuth() + : TreeFunctions.azimuthDegrees(tree.getLeanAzimuthMode(), t, -1, tree, tree.getLeanAzimuth(), null); + double azRad = Math.toRadians(azDeg); + double dx = reach * Math.sin(azRad); + double dz = reach * Math.cos(azRad); + return new double[]{dx, dz}; + } + + private static List squarePositions(double cx, double cz, int width) { + List out = new ArrayList<>(); + if (width % 2 == 1) { + int half = width / 2; + int icx = (int) Math.round(cx); + int icz = (int) Math.round(cz); + for (int dx = -half; dx <= half; dx++) { + for (int dz = -half; dz <= half; dz++) { + out.add(new int[]{icx + dx, icz + dz}); + } + } + } else { + int ox = (int) Math.floor(cx) - width / 2 + 1; + int oz = (int) Math.floor(cz) - width / 2 + 1; + for (int dx = 0; dx < width; dx++) { + for (int dz = 0; dz < width; dz++) { + out.add(new int[]{ox + dx, oz + dz}); + } + } + } + return out; + } + + static String stripState(String block) { + if (block == null) { + return null; + } + int idx = block.indexOf('['); + return idx < 0 ? block : block.substring(0, idx); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java new file mode 100644 index 000000000..fa151945e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -0,0 +1,1132 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.platform; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.HeightMap; +import org.bukkit.Location; +import org.bukkit.World; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisWorlds; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.IrisEngine; +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.EngineTarget; +import art.arcane.iris.engine.framework.GenerationSessionException; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.framework.WrongEngineBroException; +import art.arcane.iris.engine.object.IrisDimensionContractException; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionRuntimeContract; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.engine.object.StudioMode; +import art.arcane.iris.engine.platform.studio.StudioGenerator; +import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.M; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.io.ReactiveFolder; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.Looper; +import io.papermc.lib.PaperLib; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Setter; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldInitEvent; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.BlockPopulator; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.generator.WorldInfo; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.PrintWriter; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +@EqualsAndHashCode(callSuper = true) +@Data +public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChunkGenerator, Listener { + private static final int LOAD_LOCKS = Runtime.getRuntime().availableProcessors() * 4; + private static final long HOTLOAD_LOOP_DELAY_MS = 250L; + private static final long HOTLOAD_MAINTENANCE_DELAY_MS = 4000L; + private final GenerationStageGate loadLock; + private final IrisWorld world; + private final File dataLocation; + private final String dimensionKey; + private final ReactiveFolder folder; + private final ReentrantLock lock = new ReentrantLock(); + private final KList populators; + private final ChronoLatch hotloadChecker; + private final AtomicBoolean setup; + private final boolean studio; + private final AtomicBoolean studioEntryBootstrapActive; + private final AtomicInteger a = new AtomicInteger(0); + private volatile long lastChunkGenTime = 0L; + private final CompletableFuture spawnChunks = new CompletableFuture<>(); + private final AtomicCache targetCache = new AtomicCache<>(); + private final AtomicReference> closeFuture = new AtomicReference<>(); + private volatile Engine engine; + private volatile Looper hotloader; + private volatile StudioMode lastMode; + private volatile UUID lastJigsawStudioRequestId; + private volatile boolean jigsawStudioActive; + private volatile DummyBiomeProvider dummyBiomeProvider; + private volatile Throwable initializationFailure; + private volatile IrisDimension validatedDimension; + private volatile IrisDimensionRuntimeContract validatedWorldContract; + private volatile boolean closing; + @Setter + private volatile StudioGenerator studioGenerator; + + public BukkitChunkGenerator(IrisWorld world, boolean studio, File dataLocation, String dimensionKey) { + setup = new AtomicBoolean(false); + studioGenerator = null; + dummyBiomeProvider = new DummyBiomeProvider(); + populators = new KList<>(); + loadLock = new GenerationStageGate(LOAD_LOCKS, () -> closing); + this.world = world; + this.hotloadChecker = new ChronoLatch(1000, false); + this.studio = studio; + this.studioEntryBootstrapActive = new AtomicBoolean(studio); + this.dataLocation = dataLocation; + this.dimensionKey = dimensionKey; + this.folder = new ReactiveFolder( + dataLocation, + (_a, _b, _c) -> hotloadFromWatcher(), + new KList<>(".iob", ".json"), + new KList<>(".iris"), + new KList<>() + ); + this.initializationFailure = null; + this.jigsawStudioActive = false; + this.validatedDimension = null; + this.validatedWorldContract = null; + this.closing = false; + Bukkit.getServer().getPluginManager().registerEvents(this, BukkitPlatform.plugin()); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onWorldInit(WorldInitEvent event) { + if (!Objects.equals(world.identity(), WorldIdentity.key(event.getWorld()).toString())) return; + if (event.getWorld().getGenerator() != this) { + // A generator leaked by an earlier failed creation of a same-key world; only the + // instance Bukkit actually bound may attach an engine to this world. + BukkitPlatform.volmitPlugin().unregisterListener(this); + return; + } + BukkitPlatform.volmitPlugin().unregisterListener(this); + world.setRawWorldSeed(event.getWorld().getSeed()); + if (initialize(event.getWorld())) return; + + IrisLogging.warn("Failed to get Engine for " + event.getWorld().getName() + " re-trying..."); + J.s(() -> { + if (!initialize(event.getWorld())) { + IrisLogging.error("Failed to get Engine for " + event.getWorld().getName() + "!"); + } + }, 10); + } + + private boolean initialize(World world) { + try { + Engine engine = getEngine(world); + if (engine == null) { + return false; + } + INMS.get().inject(world.getSeed(), engine, world); + IrisLogging.debug("Injected Iris Biome Source into " + world.getName()); + if (!studio) { + J.s(() -> updateSpawnLocation(world), 1); + } + } catch (Throwable e) { + initializationFailure = e; + spawnChunks.completeExceptionally(e); + IrisLogging.reportError(e); + IrisLogging.error("Failed to initialize Iris generator for " + world.getName()); + e.printStackTrace(); + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (e instanceof Error error) { + throw error; + } + throw new IllegalStateException("Failed to initialize Iris for world '" + world.getName() + "'", e); + } + spawnChunks.complete(INMS.get().getSpawnChunkCount(world)); + BukkitPlatform.volmitPlugin().unregisterListener(this); + if (shouldPersistWorldRegistration(studio)) { + IrisWorlds.get().put(WorldIdentity.serialize(world), dimensionKey); + } + return true; + } + + @Nullable + @Override + public Location getFixedSpawnLocation(@NotNull World world, @NotNull Random random) { + return getInitialSpawnLocation(world); + } + + public Location getInitialSpawnLocation(World world) { + int minY = world.getMinHeight() + 1; + int maxY = world.getMaxHeight() - 2; + int y = Math.max(minY, Math.min(maxY, 96)); + return new Location(world, 0.5D, y, 0.5D); + } + + private void updateSpawnLocation(World world) { + Location initialSpawn = getInitialSpawnLocation(world); + int chunkX = initialSpawn.getBlockX() >> 4; + int chunkZ = initialSpawn.getBlockZ() >> 4; + CompletableFuture chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true); + if (chunkFuture == null) { + return; + } + + chunkFuture.thenAccept(chunk -> + J.runRegion(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> applySpawnLocation(chunk.getWorld(), initialSpawn))); + } + + private void applySpawnLocation(World world, Location initialSpawn) { + Location currentSpawn = world.getSpawnLocation(); + if (currentSpawn == null) { + return; + } + + if (!studio && (currentSpawn.getBlockX() != initialSpawn.getBlockX() || currentSpawn.getBlockZ() != initialSpawn.getBlockZ())) { + return; + } + + int minY = world.getMinHeight() + 1; + int maxY = world.getMaxHeight() - 2; + int y = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1)); + world.setSpawnLocation(new Location(world, initialSpawn.getX(), y, initialSpawn.getZ(), initialSpawn.getYaw(), initialSpawn.getPitch())); + } + + @SuppressWarnings("unchecked") + private CompletableFuture requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) { + try { + Object result = World.class + .getMethod("getChunkAtAsync", int.class, int.class, boolean.class) + .invoke(world, chunkX, chunkZ, generate); + if (result instanceof CompletableFuture) { + return (CompletableFuture) result; + } + if (PaperLib.isPaper()) { + return CompletableFuture.failedFuture(new IllegalStateException("Paper World#getChunkAtAsync returned a non-future result.")); + } + } catch (Throwable e) { + if (PaperLib.isPaper()) { + return CompletableFuture.failedFuture(new IllegalStateException("Paper World#getChunkAtAsync is unavailable.", e)); + } + } + + return PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate); + } + + private void setupEngine() { + lastMode = StudioMode.NORMAL; + lastJigsawStudioRequestId = null; + EngineTarget engineTarget = getTarget(); + String packKey = engineTarget.getDimension().getLoadKey(); + JigsawStudioActivation.Request request = studio + ? JigsawStudioActivation.getGeneratorRequest(packKey) + : null; + JigsawStudioSession session = request == null + ? null + : JigsawStudioActivation.getGeneratorSession(packKey); + jigsawStudioActive = request != null + && session != null + && session.sessionId().equals(request.requestId()); + IrisEngine createdEngine = new IrisEngine( + engineTarget, + selectInitializationMode(studio, jigsawStudioActive)); + createdEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures( + jigsawStudioActive, + studioEntryBootstrapActive.get(), + initializationFailure != null)); + engine = createdEngine; + populators.clear(); + targetCache.reset(); + } + + static IrisEngine.InitializationMode selectInitializationMode(boolean studio, boolean jigsawStudioActive) { + if (!studio) { + return IrisEngine.InitializationMode.RUNTIME; + } + return jigsawStudioActive + ? IrisEngine.InitializationMode.JIGSAW_STUDIO + : IrisEngine.InitializationMode.STUDIO; + } + + static boolean shouldPersistWorldRegistration(boolean studio) { + return !studio; + } + + @NotNull + @Override + public EngineTarget getTarget() { + if (engine != null) return engine.getTarget(); + + return targetCache.aquireOrThrow(() -> { + IrisData data = IrisData.openRuntime(dataLocation); + data.dump(); + data.clearLists(); + IrisDimension dimension = data.getDimensionLoader().load(dimensionKey); + + if (dimension == null) { + IrisLogging.error("Oh No! There's no pack in " + data.getDataFolder().getPath() + " or... there's no dimension for the key " + dimensionKey); + IrisDimension test = IrisData.loadAnyDimension(dimensionKey, null); + + if (test != null) { + IrisLogging.warn("Looks like " + dimensionKey + " exists in " + test.getLoadFile().getPath() + " "); + test = IrisServices.get(StudioSVC.class).installInto(BukkitPlatform.console(), dimensionKey, dataLocation); + IrisLogging.warn("Attempted to install into " + data.getDataFolder().getPath()); + + if (test != null) { + IrisLogging.msg(C.IRIS + "Woo! Patched the Engine!"); + dimension = test; + } else { + IrisLogging.error("Failed to patch dimension!"); + throw new RuntimeException("Missing Dimension: " + dimensionKey); + } + } else { + IrisLogging.error("Nope, you don't have an installation containing " + dimensionKey + " try downloading it?"); + throw new RuntimeException("Missing Dimension: " + dimensionKey); + } + } + + return new EngineTarget(world, dimension, data); + }); + } + + private Engine getEngine(WorldInfo world) { + throwIfInitializationFailed(); + validateAndBindWorld(world); + if (setup.get()) { + return getEngine(); + } + + lock.lock(); + + try { + if (setup.get()) { + return getEngine(); + } + + + getWorld().setRawWorldSeed(world.getSeed()); + setupEngine(); + setup.set(true); + this.hotloader = shouldRunStudioHotload(studio, closing, jigsawStudioActive) ? new Looper() { + @Override + protected long loop() { + if (shouldThrottleHotload()) { + return HOTLOAD_MAINTENANCE_DELAY_MS; + } + + if (hotloadChecker.flip()) { + folder.check(); + } + + return HOTLOAD_LOOP_DELAY_MS; + } + } : null; + + if (hotloader != null) { + hotloader.setPriority(Thread.MIN_PRIORITY); + hotloader.start(); + hotloader.setName(getTarget().getWorld().name() + " Hotloader"); + } + + return engine; + } finally { + lock.unlock(); + } + } + + private void throwIfInitializationFailed() { + Throwable failure = initializationFailure; + if (failure == null) { + return; + } + if (failure instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (failure instanceof Error error) { + throw error; + } + throw new IllegalStateException("Iris initialization previously failed for world '" + world.name() + "'", failure); + } + + private void validateAndBindWorld(WorldInfo worldInfo) { + IrisDimension dimension = getTarget().getDimension(); + IrisDimensionRuntimeContract activeContract = validatedWorldContract; + if (activeContract == null || validatedDimension != dimension) { + IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected(dimension, "iris"); + if (activeContract != null) { + expected.requireExact("Bukkit world '" + worldInfo.getName() + "'", activeContract); + } + int runtimeHeight = worldInfo.getMaxHeight() - worldInfo.getMinHeight(); + expected.requireHeight("Bukkit world '" + worldInfo.getName() + "'", worldInfo.getMinHeight(), runtimeHeight); + BukkitWorldBinding.bind(world, worldInfo); + validatedWorldContract = expected; + validatedDimension = dimension; + return; + } + if (!world.hasPlatformWorld() && worldInfo instanceof World) { + BukkitWorldBinding.bind(world, worldInfo); + } + } + + @Override + public void close() { + closeAsync(); + } + + @Override + public CompletableFuture closeAsync() { + closing = true; + // Outside the exclusive-control block so every close path detaches the WorldInit + // listener, including a rollback where the world never materialized. Guarded: with no + // hosted plugin (unit tests, teardown) volmitPlugin() throws, and that must not stop + // the close. + try { + BukkitPlatform.volmitPlugin().unregisterListener(this); + } catch (Throwable ignored) { + } + CompletableFuture future = new CompletableFuture<>(); + while (!closeFuture.compareAndSet(null, future)) { + CompletableFuture existing = closeFuture.get(); + if (existing != null) { + return existing; + } + } + + CompletableFuture operation; + try { + operation = withExclusiveControlFuture(() -> { + Looper activeHotloader = hotloader; + hotloader = null; + if (isStudio() && activeHotloader != null) { + activeHotloader.interrupt(); + try { + activeHotloader.join(1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + IrisLogging.reportError(e); + } + } + + Engine currentEngine = engine; + if (currentEngine != null && !currentEngine.isClosed()) { + currentEngine.close(); + } + folder.clear(); + populators.clear(); + }); + } catch (Throwable throwable) { + future.completeExceptionally(throwable); + closeFuture.compareAndSet(future, null); + // A failed close must stay retryable; leaving closing latched would permanently + // reject generation for a world that may still be loaded. + closing = false; + return future; + } + + operation.whenComplete((ignored, throwable) -> { + if (throwable == null) { + future.complete(null); + } else { + // The close body already ran and tore the engine down; unlike the pre-dispatch + // failure above, resetting the gate here would advertise a healthy generator + // over a CLOSED or FAILED engine. Stay latched and surface the failure. + future.completeExceptionally(throwable); + } + }); + return future; + } + + @Override + public void quiesceForServerShutdown() { + Looper activeHotloader = hotloader; + hotloader = null; + if (activeHotloader != null) { + activeHotloader.interrupt(); + } + } + + @Override + public boolean isStudio() { + return studio; + } + + public void endStudioEntryBootstrap() { + Engine activeEngine = engine; + if (activeEngine instanceof IrisEngine irisEngine) { + irisEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures( + jigsawStudioActive, + false, + initializationFailure != null)); + } + studioEntryBootstrapActive.set(false); + } + + public boolean isStudioEntryBootstrapActive() { + return studioEntryBootstrapActive.get(); + } + + public boolean isJigsawStudioActive() { + return jigsawStudioActive; + } + + @Override + public void hotload() { + if (!shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) { + return; + } + + withExclusiveControl(() -> getEngine().hotload()); + } + + private void hotloadFromWatcher() { + if (!shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) { + return; + } + withExclusiveControlFuture(() -> getEngine().hotload(), 30L, TimeUnit.SECONDS).join(); + } + + @Override + public CompletableFuture hotloadComplexAsync(long acquisitionTimeout, TimeUnit unit) { + Engine activeEngine = getEngine(); + if (activeEngine == null) { + return CompletableFuture.completedFuture(null); + } + return withExclusiveControlFuture(activeEngine::hotloadComplex, acquisitionTimeout, unit); + } + + static boolean shouldRunStudioHotload(boolean studio, boolean closing, boolean jigsawStudioActive) { + return studio && !closing && !jigsawStudioActive; + } + + public GenerationStagePermit acquireGenerationStage(String operation) { + return loadLock.acquireStage(operation); + } + + public GenerationStagePermit acquireNoiseGenerationStage( + Engine expectedEngine, + int chunkX, + int chunkZ, + String operation + ) { + Engine activeEngine = Objects.requireNonNull(expectedEngine, "Noise generation engine"); + return acquirePreparedGenerationStage( + loadLock, + operation, + () -> resolveStudioGeneratorForNoise(activeEngine), + activeEngine, + chunkX, + chunkZ); + } + + static GenerationStagePermit acquirePreparedGenerationStage( + GenerationStageGate gate, + String operation, + Supplier resolver, + Engine engine, + int chunkX, + int chunkZ + ) { + GenerationStageGate activeGate = Objects.requireNonNull(gate, "Generation stage gate"); + Supplier activeResolver = Objects.requireNonNull(resolver, "Studio generator resolver"); + Engine activeEngine = Objects.requireNonNull(engine, "Prepared generation engine"); + while (true) { + GenerationStagePermit stage = activeGate.acquireStage(operation); + StudioGenerator selected; + boolean retained = false; + try { + selected = activeResolver.get(); + if (selected == null || !selected.requiresPreSessionPreparation()) { + if (activeResolver.get() == selected) { + retained = true; + return stage; + } + continue; + } + } finally { + if (!retained) { + stage.close(); + } + } + + GenerationStageExclusivePermit exclusive = activeGate.acquireExclusiveStage(operation); + try { + StudioGenerator active = activeResolver.get(); + if (active != selected) { + continue; + } + active.prepareChunkBeforeSession(activeEngine, chunkX, chunkZ); + return exclusive.downgradeToStage(); + } catch (WrongEngineBroException e) { + throw new IllegalStateException("Iris generation stage " + operation + + " could not prepare its Studio generator.", e); + } finally { + exclusive.close(); + } + } + } + + private StudioGenerator resolveStudioGeneratorForNoise(Engine expectedEngine) { + if (engine != expectedEngine) { + throw new IllegalStateException("Iris noise generation belongs to a replaced engine runtime."); + } + computeStudioGenerator(); + if (engine != expectedEngine) { + throw new IllegalStateException("Iris noise generation changed engine runtime during Studio resolution."); + } + return studioGenerator; + } + + public void withExclusiveControl(Runnable r) { + J.a(() -> { + boolean acquired = false; + try { + loadLock.acquireExclusive(); + acquired = true; + r.run(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + IrisLogging.reportError(e); + e.printStackTrace(); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } finally { + if (acquired) { + loadLock.releaseExclusive(); + } + } + }); + } + + public CompletableFuture withExclusiveControlFuture(Runnable r) { + CompletableFuture future = new CompletableFuture<>(); + J.a(() -> completeExclusiveControlFuture(loadLock, r, future)); + return future; + } + + CompletableFuture withExclusiveControlFuture( + Runnable operation, + long acquisitionTimeout, + TimeUnit unit + ) { + CompletableFuture future = new CompletableFuture<>(); + J.a(() -> completeExclusiveControlFuture( + loadLock, + operation, + future, + acquisitionTimeout, + unit)); + return future; + } + + static void completeExclusiveControlFuture( + GenerationStageGate gate, + Runnable operation, + CompletableFuture future + ) { + GenerationStageGate activeGate = Objects.requireNonNull(gate, "Exclusive control gate"); + Runnable activeOperation = Objects.requireNonNull(operation, "Exclusive control operation"); + CompletableFuture outward = Objects.requireNonNull(future, "Exclusive control future"); + boolean acquired = false; + Throwable failure = null; + try { + activeGate.acquireExclusive(); + acquired = true; + activeOperation.run(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + failure = e; + } catch (Throwable e) { + failure = e; + } finally { + if (acquired) { + activeGate.releaseExclusive(); + } + } + + if (failure == null) { + outward.complete(null); + } else { + outward.completeExceptionally(failure); + } + } + + static void completeExclusiveControlFuture( + GenerationStageGate gate, + Runnable operation, + CompletableFuture future, + long acquisitionTimeout, + TimeUnit unit + ) { + GenerationStageGate activeGate = Objects.requireNonNull(gate, "Exclusive control gate"); + Runnable activeOperation = Objects.requireNonNull(operation, "Exclusive control operation"); + CompletableFuture outward = Objects.requireNonNull(future, "Exclusive control future"); + TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive control timeout unit"); + boolean acquired = false; + Throwable failure = null; + try { + acquired = activeGate.tryAcquireExclusive( + acquisitionTimeout, + activeUnit, + outward::isCancelled); + if (!acquired) { + if (!outward.isCancelled()) { + failure = new TimeoutException("Timed out waiting for exclusive Iris generation control after " + + acquisitionTimeout + " " + activeUnit.name().toLowerCase(Locale.ROOT) + "."); + } + } else if (!outward.isCancelled()) { + activeOperation.run(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + if (!outward.isCancelled()) { + failure = e; + } + } catch (Throwable e) { + failure = e; + } finally { + if (acquired) { + activeGate.releaseExclusive(); + } + } + + if (outward.isCancelled()) { + return; + } + if (failure == null) { + outward.complete(null); + } else { + outward.completeExceptionally(failure); + } + } + + public void touch(World world) { + getEngine(world); + } + + @Override + public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) { + if (closing) { + throw new IllegalStateException("Iris chunk generation was rejected while the generator is closing."); + } + throwIfInitializationFailed(); + + try { + Engine engine = getEngine(world); + lastChunkGenTime = System.currentTimeMillis(); + computeStudioGenerator(); + TerrainChunk tc = TerrainChunk.create(d); + if (studioGenerator != null) { + studioGenerator.generateChunk(engine, tc, x, z); + } else { + ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d); + Hunk biomes = Hunk.viewBiomes(tc); + boolean useMulticore = studio && !J.isFolia(); + try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + engine.generate(x << 4, z << 4, blocks, biomes, useMulticore); + blocks.apply(); + } + } + + IrisLogging.debug("Generated " + x + " " + z); + } catch (IrisDimensionContractException e) { + throw e; + } catch (GenerationSessionException e) { + if (closing || isExpectedTeardown(engine, e)) { + throw new IllegalStateException("Iris chunk generation was rejected during an engine transition.", e); + } + + IrisLogging.error("======================================"); + e.printStackTrace(); + reportErrorChunk(x, z, e); + IrisLogging.error("======================================"); + + throw new IllegalStateException("Iris chunk generation could not acquire its engine runtime.", e); + } catch (Throwable e) { + IrisLogging.error("======================================"); + e.printStackTrace(); + reportErrorChunk(x, z, e); + IrisLogging.error("======================================"); + + throw new IllegalStateException("Iris failed to generate chunk " + x + "," + z + ".", e); + } + } + + private boolean isExpectedTeardown(Engine currentEngine, Throwable throwable) { + if (throwable instanceof GenerationSessionException generationSessionException && generationSessionException.isExpectedTeardown()) { + return true; + } + + if (currentEngine != null && currentEngine.isClosing()) { + return true; + } + + return isMaintenanceActive(); + } + + private boolean isMaintenanceActive() { + World realWorld = BukkitWorldBinding.world(this.world); + return realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld); + } + + private static void reportErrorChunk(int x, int z, Throwable e) { + if (IrisSettings.get().getGeneral().isDebug()) { + File f = IrisPlatforms.get().dataFile("debug", "chunk-errors", "chunk." + x + "." + z + ".txt"); + + if (!f.exists()) { + J.attempt(() -> { + PrintWriter pw = new PrintWriter(f); + pw.println("Thread: " + Thread.currentThread().getName()); + pw.println("First: " + new Date(M.ms())); + e.printStackTrace(pw); + pw.close(); + }); + } + + IrisLogging.debug("Chunk " + x + "," + z + " Exception Logged: " + e.getClass().getSimpleName() + ": " + C.RESET + "" + C.LIGHT_PURPLE + e.getMessage()); + } + } + + private boolean shouldThrottleHotload() { + if (isMaintenanceActive()) { + return true; + } + + if (System.currentTimeMillis() - lastChunkGenTime < 2000L) { + return true; + } + + World realWorld = BukkitWorldBinding.world(this.world); + PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); + return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(realWorld)); + } + + @Override + public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) { + Engine currentEngine = getEngine(worldInfo); + + boolean ignoreFluid = switch (heightMap) { + case OCEAN_FLOOR, OCEAN_FLOOR_WG -> true; + default -> false; + }; + + try (GenerationSessionLease lease = currentEngine.acquireGenerationLease("bukkit_base_height"); + IrisContext.Scope ignored = IrisContext.open(currentEngine, lease.sessionId(), null)) { + return currentEngine.getMinHeight() + currentEngine.getHeight(x, z, ignoreFluid) + 1; + } catch (GenerationSessionException e) { + throw new IllegalStateException("Iris base height query was rejected for world '" + + worldInfo.getName() + "'.", e); + } + } + + private void computeStudioGenerator() { + String packKey = getEngine().getDimension().getLoadKey(); + JigsawStudioActivation.Request jigsawRequest = studio + ? JigsawStudioActivation.getGeneratorRequest(packKey) + : null; + if (jigsawRequest != null) { + JigsawStudioSession session = JigsawStudioActivation.getGeneratorSession(packKey); + if (session != null && session.sessionId().equals(jigsawRequest.requestId())) { + if (!jigsawRequest.requestId().equals(lastJigsawStudioRequestId)) { + setStudioGenerator(new JigsawStudioGenerator(getEngine(), jigsawRequest, session)); + lastJigsawStudioRequestId = jigsawRequest.requestId(); + lastMode = null; + } + return; + } + } + if (lastJigsawStudioRequestId != null) { + lastJigsawStudioRequestId = null; + lastMode = null; + } + // Gson nulls unknown enum names, so a pack carrying a removed or typo'd studioMode must not NPE the generator. + // Pack-declared studio modes are studio-only: a shipped pack that forgot to reset + // studioMode must never replace production terrain with a debug generator. + StudioMode desired = studio + ? java.util.Optional.ofNullable(getEngine().getDimension().getStudioMode()).orElse(StudioMode.NORMAL) + : StudioMode.NORMAL; + if (studio && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) { + desired = StudioMode.OBJECT_BUFFET; + } + if (!desired.equals(lastMode)) { + lastMode = desired; + desired.inject(this); + } + } + + @NotNull + @Override + public List getDefaultPopulators(@NotNull World world) { + return populators; + } + + @Override + public boolean shouldGenerateCaves() { + return false; + } + + @Override + public boolean shouldGenerateDecorations() { + return false; + } + + @Override + public boolean shouldGenerateMobs() { + return false; + } + + @Override + public boolean shouldGenerateStructures() { + return shouldGenerateNativeStructures( + jigsawStudioActive, + studioEntryBootstrapActive.get(), + initializationFailure != null); + } + + static boolean shouldGenerateNativeStructures( + boolean jigsawStudioActive, + boolean studioEntryBootstrapActive, + boolean initializationFailed + ) { + return !jigsawStudioActive && !studioEntryBootstrapActive && !initializationFailed; + } + + @Override + public boolean shouldGenerateNoise() { + return false; + } + + @Override + public boolean shouldGenerateSurface() { + return false; + } + + @Override + public void generateBedrock(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull ChunkData chunkData) { + + } + + @Nullable + @Override + public BiomeProvider getDefaultBiomeProvider(@NotNull WorldInfo worldInfo) { + return dummyBiomeProvider; + } + + static final class GenerationStageGate { + private final int permitCount; + private final @Nullable Semaphore permits; + private final BooleanSupplier closing; + + GenerationStageGate(int permitCount, BooleanSupplier closing) { + if (permitCount <= 0) { + throw new IllegalArgumentException("Generation stage permit count must be positive."); + } + this.permitCount = permitCount; + this.permits = new Semaphore(permitCount, true); + this.closing = Objects.requireNonNull(closing, "Generation stage close state"); + } + + GenerationStagePermit acquireStage(String operation) { + String activeOperation = Objects.requireNonNull(operation, "Generation stage operation"); + if (closing.getAsBoolean()) { + throw rejected(activeOperation); + } + try { + permits.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Iris generation stage " + activeOperation + + " was interrupted while waiting for engine access.", e); + } + if (closing.getAsBoolean()) { + permits.release(); + throw rejected(activeOperation); + } + return new GenerationStagePermit(permits); + } + + GenerationStageExclusivePermit acquireExclusiveStage(String operation) { + String activeOperation = Objects.requireNonNull(operation, "Generation stage operation"); + if (closing.getAsBoolean()) { + throw rejected(activeOperation); + } + try { + permits.acquire(permitCount); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Iris generation stage " + activeOperation + + " was interrupted while waiting for exclusive engine access.", e); + } + if (closing.getAsBoolean()) { + permits.release(permitCount); + throw rejected(activeOperation); + } + return new GenerationStageExclusivePermit(permits, permitCount); + } + + void acquireExclusive() throws InterruptedException { + permits.acquire(permitCount); + } + + boolean tryAcquireExclusive( + long timeout, + TimeUnit unit, + BooleanSupplier cancelled + ) throws InterruptedException { + if (timeout <= 0L) { + throw new IllegalArgumentException("Exclusive generation control timeout must be positive."); + } + TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive generation control timeout unit"); + BooleanSupplier cancellation = Objects.requireNonNull(cancelled, "Exclusive generation control cancellation"); + long timeoutNanos = activeUnit.toNanos(timeout); + long started = System.nanoTime(); + long remainingNanos = timeoutNanos; + long cancellationPollNanos = TimeUnit.MILLISECONDS.toNanos(50L); + while (!cancellation.getAsBoolean() && remainingNanos > 0L) { + if (permits.tryAcquire( + permitCount, + Math.min(remainingNanos, cancellationPollNanos), + TimeUnit.NANOSECONDS)) { + return true; + } + remainingNanos = timeoutNanos - (System.nanoTime() - started); + } + return false; + } + + void releaseExclusive() { + permits.release(permitCount); + } + + int queueLength() { + return permits.getQueueLength(); + } + + int availablePermits() { + return permits.availablePermits(); + } + + private IllegalStateException rejected(String operation) { + return new IllegalStateException("Iris generation stage " + operation + + " was rejected while the generator is closing."); + } + } + + static final class GenerationStageExclusivePermit implements AutoCloseable { + private final Semaphore permits; + private final int permitCount; + private final AtomicBoolean released; + + private GenerationStageExclusivePermit(Semaphore permits, int permitCount) { + this.permits = permits; + this.permitCount = permitCount; + this.released = new AtomicBoolean(false); + } + + GenerationStagePermit downgradeToStage() { + if (!released.compareAndSet(false, true)) { + throw new IllegalStateException("Exclusive Iris generation stage was already released."); + } + GenerationStagePermit stage = new GenerationStagePermit(permits); + if (permitCount > 1) { + permits.release(permitCount - 1); + } + return stage; + } + + @Override + public void close() { + if (released.compareAndSet(false, true)) { + permits.release(permitCount); + } + } + } + + public static final class GenerationStagePermit implements AutoCloseable { + private static final GenerationStagePermit NOOP = new GenerationStagePermit(null); + + private final Semaphore permits; + private final AtomicBoolean released; + + private GenerationStagePermit(@Nullable Semaphore permits) { + this.permits = permits; + this.released = new AtomicBoolean(permits == null); + } + + public static GenerationStagePermit noop() { + return NOOP; + } + + @Override + public void close() { + if (permits != null && released.compareAndSet(false, true)) { + permits.release(); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/DummyBiomeProvider.java b/core/src/main/java/art/arcane/iris/engine/platform/DummyBiomeProvider.java new file mode 100644 index 000000000..605b42b51 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/platform/DummyBiomeProvider.java @@ -0,0 +1,36 @@ +package art.arcane.iris.engine.platform; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBiome; +import org.bukkit.block.Biome; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.WorldInfo; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class DummyBiomeProvider extends BiomeProvider { + private final List ALL = resolveAll(); + + private static List resolveAll() { + List platformBiomes = IrisPlatforms.get().biomeWriter().allBiomes(); + List biomes = new ArrayList<>(platformBiomes.size()); + for (PlatformBiome biome : platformBiomes) { + biomes.add((Biome) biome.nativeHandle()); + } + return biomes; + } + + @NotNull + @Override + public Biome getBiome(@NotNull WorldInfo worldInfo, int x, int y, int z) { + return Biome.PLAINS; + } + + @NotNull + @Override + public List getBiomes(@NotNull WorldInfo worldInfo) { + return ALL; + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/platform/DummyChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/DummyChunkGenerator.java similarity index 95% rename from core/src/main/java/com/volmit/iris/engine/platform/DummyChunkGenerator.java rename to core/src/main/java/art/arcane/iris/engine/platform/DummyChunkGenerator.java index b78340ba4..04fb8817a 100644 --- a/core/src/main/java/com/volmit/iris/engine/platform/DummyChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/DummyChunkGenerator.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.platform; +package art.arcane.iris.engine.platform; import org.bukkit.generator.ChunkGenerator; diff --git a/core/src/main/java/art/arcane/iris/engine/platform/EngineBukkitOps.java b/core/src/main/java/art/arcane/iris/engine/platform/EngineBukkitOps.java new file mode 100644 index 000000000..dd3eba0ad --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/platform/EngineBukkitOps.java @@ -0,0 +1,705 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.platform; + +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.core.events.IrisLootEvent; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.framework.Locator; +import art.arcane.iris.engine.framework.LootResolver; +import art.arcane.iris.engine.framework.PlacedObject; +import art.arcane.iris.engine.framework.WrongEngineBroException; +import art.arcane.iris.engine.object.InventorySlotType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +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.VolmitSender; +import art.arcane.iris.util.common.reflect.KeyedType; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.iris.util.common.scheduling.jobs.SingleJob; +import art.arcane.iris.util.project.matter.TileWrapper; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterUpdate; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import io.papermc.lib.PaperLib; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Chest; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.loot.Lootable; + +import java.lang.reflect.Method; +import java.util.UUID; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.volmlib.util.localization.MessageArgument; +public final class EngineBukkitOps { + private static final ConcurrentHashMap> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>(); + + private EngineBukkitOps() { + } + + public static void updateChunk(Engine engine, Chunk c) { + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + if (c.getWorld().isChunkLoaded(c.getX() + x, c.getZ() + z)) + continue; + String msg = "Chunk %s, %s [%s, %s] is not loaded".formatted(c.getX() + x, c.getZ() + z, x, z); + IrisLogging.debug(msg); + return; + } + } + Mantle mantle = engine.getMantle().getMantle(); + if (!mantle.isLoaded(c)) { + String msg = "Mantle Chunk " + c.getX() + "," + c.getZ() + " is not loaded"; + IrisLogging.debug(msg); + return; + } + + if (!J.isFolia() && !J.isPrimaryThread()) { + CompletableFuture scheduled = J.sfut(() -> updateChunk(engine, c)); + if (scheduled != null) { + try { + scheduled.join(); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + return; + } + + MantleChunk chunk = mantle.getChunk(c).use(); + try { + Runnable tileTask = () -> materializeTiles(engine, c, chunk); + Runnable customTask = () -> materializeCustomBlocks(engine, c, chunk); + + Runnable updateTask = () -> { + PrecisionStopwatch p = PrecisionStopwatch.start(); + int[][] grid = new int[16][16]; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + grid[x][z] = Integer.MIN_VALUE; + } + } + + chunk.iterate(MatterCavern.class, (x, yf, z, v) -> { + int y = yf + engine.getWorld().minHeight(); + x &= 15; + z &= 15; + Block block = c.getBlock(x, y, z); + if (!BukkitBlockResolution.isFluid(block.getBlockData())) { + return; + } + boolean u = BukkitBlockResolution.isAir(block.getRelative(BlockFace.DOWN).getBlockData()) + || BukkitBlockResolution.isAir(block.getRelative(BlockFace.WEST).getBlockData()) + || BukkitBlockResolution.isAir(block.getRelative(BlockFace.EAST).getBlockData()) + || BukkitBlockResolution.isAir(block.getRelative(BlockFace.SOUTH).getBlockData()) + || BukkitBlockResolution.isAir(block.getRelative(BlockFace.NORTH).getBlockData()); + + if (u) grid[x][z] = Math.max(grid[x][z], y); + }); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (grid[x][z] == Integer.MIN_VALUE) { + continue; + } + update(engine, x, grid[x][z], z, c, chunk); + } + } + + chunk.iterate(MatterUpdate.class, (x, yf, z, v) -> { + int y = yf + engine.getWorld().minHeight(); + if (v != null && v.isUpdate()) { + update(engine, x, y, z, c, chunk); + } + }); + chunk.deleteSlices(MatterUpdate.class); + engine.getMetrics().getUpdates().put(p.getMilliseconds()); + }; + + if (shouldRunChunkUpdateInline(c)) { + runMaterializationPasses(chunk, tileTask, customTask, updateTask); + return; + } + + runScheduledMaterializationPasses(c, () -> runMaterializationPasses(chunk, tileTask, customTask, updateTask)); + } finally { + chunk.release(); + } + } + + static void materializeTiles(Engine engine, Chunk chunk, MantleChunk mantleChunk) { + mantleChunk.iterate(TileWrapper.class, (x, y, z, value) -> { + int worldY = y + engine.getWorld().minHeight(); + if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) { + return; + } + Block block = chunk.getBlock(x & 15, worldY, z & 15); + if (!TileData.setTileState(block, value.getData())) { + NamespacedKey blockTypeKey = KeyedType.getKey(block.getType()); + String blockType = blockTypeKey == null ? block.getType().name() : blockTypeKey.toString(); + String tileType = value.getData().getMaterialKey(); + IrisLogging.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", block.getX(), block.getY(), block.getZ(), blockType, tileType); + } + }); + mantleChunk.deleteSlices(TileWrapper.class); + } + + static void materializeCustomBlocks(Engine engine, Chunk chunk, MantleChunk mantleChunk) { + mantleChunk.iterate(Identifier.class, (x, y, z, value) -> { + int worldY = y + engine.getWorld().minHeight(); + if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) { + return; + } + IrisServices.get(ExternalDataSVC.class).processUpdate( + engine, + chunk.getBlock(x & 15, worldY, z & 15), + value + ); + }); + mantleChunk.deleteSlices(Identifier.class); + } + + static void runMaterializationPasses( + MantleChunk chunk, + Runnable tileTask, + Runnable customTask, + Runnable updateTask + ) { + chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> { + chunk.raiseFlagUnchecked(MantleFlag.TILE, tileTask); + chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, customTask); + chunk.raiseFlagUnchecked(MantleFlag.UPDATE, updateTask); + }); + } + + private static boolean shouldRunChunkUpdateInline(Chunk chunk) { + if (chunk == null) { + return false; + } + + if (!J.isFolia()) { + return true; + } + + return J.isOwnedByCurrentRegion(chunk.getWorld(), chunk.getX(), chunk.getZ()); + } + + private static void runScheduledMaterializationPasses(Chunk contextChunk, Runnable runnable) { + CompletableFuture completion = new CompletableFuture<>(); + boolean scheduled = J.runRegion(contextChunk.getWorld(), contextChunk.getX(), contextChunk.getZ(), () -> { + try { + runnable.run(); + completion.complete(null); + } catch (Throwable error) { + completion.completeExceptionally(error); + } + }); + + if (!scheduled) { + if (J.isPrimaryThread()) { + runnable.run(); + } + return; + } + + try { + completion.join(); + } catch (CompletionException error) { + Throwable cause = error.getCause(); + IrisLogging.reportError( + "Failed deferred chunk materialization for " + contextChunk.getWorld().getName() + + " at chunk " + contextChunk.getX() + "," + contextChunk.getZ() + ".", + cause == null ? error : cause + ); + } + } + + public static void update(Engine engine, int x, int y, int z, Chunk c, MantleChunk mc) { + World world = c.getWorld(); + if (y < world.getMinHeight() || y >= world.getMaxHeight()) { + return; + } + Block block = c.getBlock(x & 15, y, z & 15); + BlockData data = block.getBlockData(); + engine.blockUpdatedMetric(); + if (BukkitBlockResolution.isStorage(data)) { + InventorySlotType slot = null; + + if (BukkitBlockResolution.isStorageChest(data)) { + slot = InventorySlotType.STORAGE; + } + + if (slot != null) { + if (!isCanonicalContainer(block)) { + return; + } + int worldX = block.getX(); + int worldZ = block.getZ(); + RNG rx = LootResolver.containerRng(engine.getSeedManager().getLoot(), worldX, y, worldZ); + KList tables = getLootTables(engine, rx, block, mc, hasNativeLootTable(block)); + + try { + Bukkit.getPluginManager().callEvent(new IrisLootEvent(engine, block, slot, tables)); + if (tables.isEmpty()) return; + InventoryHolder m = (InventoryHolder) block.getState(); + addItems(engine, false, m.getInventory(), tables, slot, c.getWorld(), worldX, y, worldZ); + + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } else { + block.setType(Material.AIR, false); + block.setBlockData(data, true); + } + } + + public static boolean isCanonicalContainer(Block block) { + if (!(block.getBlockData() instanceof Chest chest) || chest.getType() == Chest.Type.SINGLE) { + return true; + } + BlockFace connectedFace = connectedFace(chest); + int connectedX = block.getX() + connectedFace.getModX(); + int connectedZ = block.getZ() + connectedFace.getModZ(); + return block.getX() < connectedX || block.getX() == connectedX && block.getZ() <= connectedZ; + } + + public static boolean hasNativeLootTable(Block block) { + if (hasNativeLootTableAt(block)) { + return true; + } + if (!(block.getBlockData() instanceof Chest chest) || chest.getType() == Chest.Type.SINGLE) { + return false; + } + BlockFace connectedFace = connectedFace(chest); + int connectedX = block.getX() + connectedFace.getModX(); + int connectedZ = block.getZ() + connectedFace.getModZ(); + if (!block.getWorld().isChunkLoaded(connectedX >> 4, connectedZ >> 4)) { + return false; + } + return hasNativeLootTableAt(block.getWorld().getBlockAt(connectedX, block.getY(), connectedZ)); + } + + private static boolean hasNativeLootTableAt(Block block) { + BlockState state = block.getState(); + return state instanceof Lootable lootable && lootable.getLootTable() != null; + } + + private static BlockFace connectedFace(Chest chest) { + return chest.getType() == Chest.Type.LEFT + ? clockwise(chest.getFacing()) + : counterClockwise(chest.getFacing()); + } + + private static BlockFace clockwise(BlockFace face) { + return switch (face) { + case NORTH -> BlockFace.EAST; + case EAST -> BlockFace.SOUTH; + case SOUTH -> BlockFace.WEST; + case WEST -> BlockFace.NORTH; + default -> BlockFace.SELF; + }; + } + + private static BlockFace counterClockwise(BlockFace face) { + return switch (face) { + case NORTH -> BlockFace.WEST; + case WEST -> BlockFace.SOUTH; + case SOUTH -> BlockFace.EAST; + case EAST -> BlockFace.NORTH; + default -> BlockFace.SELF; + }; + } + + public static void scramble(Engine engine, Inventory inventory, RNG rng) { + org.bukkit.inventory.ItemStack[] items = inventory.getContents(); + org.bukkit.inventory.ItemStack[] nitems = new org.bukkit.inventory.ItemStack[inventory.getSize()]; + System.arraycopy(items, 0, nitems, 0, items.length); + boolean packedFull = false; + + splitting: + for (int i = 0; i < nitems.length; i++) { + ItemStack is = nitems[i]; + + if (is != null && is.getAmount() > 1 && !packedFull) { + for (int j = 0; j < nitems.length; j++) { + if (nitems[j] == null) { + int take = rng.nextInt(is.getAmount()); + take = take == 0 ? 1 : take; + is.setAmount(is.getAmount() - take); + nitems[j] = is.clone(); + nitems[j].setAmount(take); + continue splitting; + } + } + + packedFull = true; + } + } + + for (int i = nitems.length; i > 1; i--) { + int j = rng.nextInt(i); + ItemStack tmp = nitems[i - 1]; + nitems[i - 1] = nitems[j]; + nitems[j] = tmp; + } + + inventory.setContents(nitems); + } + + public static KList getLootTables(Engine engine, RNG rng, Block b) { + MantleChunk mc = engine.getMantle().getMantle().getChunk(b.getChunk()).use(); + try { + return getLootTables(engine, rng, b, mc, hasNativeLootTable(b)); + } finally { + mc.release(); + } + } + + public static KList getLootTables(Engine engine, RNG rng, Block b, MantleChunk mc) { + return getLootTables(engine, rng, b, mc, hasNativeLootTable(b)); + } + + public static KList getLootTables(Engine engine, RNG rng, Block b, MantleChunk mc, boolean nativeLootDefined) { + int rx = b.getX(); + int rz = b.getZ(); + int ry = b.getY() - engine.getWorld().minHeight(); + KList tables = new KList<>(); + boolean objectLootDefined = nativeLootDefined; + + PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc); + if (po != null && po.getPlacement() != null) { + if (BukkitBlockResolution.isStorageChest(b.getBlockData())) { + objectLootDefined = objectLootDefined + || po.getPlacement().getLoot().isNotEmpty() + || po.getPlacement().getVanillaLoot().isNotEmpty(); + IrisLootTable table = po.getPlacement().getTable( + BukkitBlockState.of(b.getBlockData()), engine.getData(), rng); + if (table != null) { + tables.add(table); + if (po.getPlacement().isOverrideGlobalLoot()) { + return new KList<>(table); + } + } + } + } + + LootResolver.resolveEnvironmentSources( + tables, + engine, + rng, + rx, + ry, + rz, + objectLootDefined, + table -> table + ); + + return tables; + } + + public static void addItems(Engine engine, boolean debug, Inventory inv, KList tables, InventorySlotType slot, World world, int x, int y, int z) { + KList items = new KList<>(); + + for (IrisLootTable i : tables) { + if (i == null) + continue; + items.addAll(i.getLoot(debug, engine.getSeedManager().getLoot(), slot, world, x, y, z)); + } + if (IrisLootEvent.callLootEvent(items, inv, world, x, y, z)) + return; + + if (PaperLib.isPaper() && engine.getWorld().hasPlatformWorld()) { + PaperLib.getChunkAtAsync(BukkitWorldBinding.world(engine.getWorld()), x >> 4, z >> 4).thenAccept((c) -> { + Runnable mutation = () -> { + for (ItemStack i : items) { + inv.addItem(i); + } + + scramble(engine, inv, LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z)); + }; + + LootMutationTask task = new LootMutationTask(world, x >> 4, z >> 4, mutation); + boolean scheduled = dispatchLootMutation( + task, + J.isFolia(), + Bukkit::isPrimaryThread, + regionTask -> J.runRegion( + regionTask.world(), + regionTask.chunkX(), + regionTask.chunkZ(), + regionTask.mutation()), + J::s + ); + if (!scheduled) { + IrisLogging.error("Failed to schedule loot inventory mutation for " + world.getName() + "@" + (x >> 4) + "," + (z >> 4) + "."); + } + }); + } else { + for (ItemStack i : items) { + inv.addItem(i); + } + + scramble(engine, inv, LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z)); + } + } + + static boolean dispatchLootMutation( + LootMutationTask task, + boolean folia, + BooleanSupplier primaryThread, + Predicate regionScheduler, + Consumer syncScheduler) { + if (folia) { + return regionScheduler.test(task); + } + + if (primaryThread.getAsBoolean()) { + task.mutation().run(); + } else { + syncScheduler.accept(task.mutation()); + } + return true; + } + + public static IrisBiome getBiome(Engine engine, Location l) { + return engine.getBiome(l.getBlockX(), l.getBlockY() - engine.getWorld().minHeight(), l.getBlockZ()); + } + + public static IrisRegion getRegion(Engine engine, Location l) { + return engine.getRegion(l.getBlockX(), l.getBlockZ()); + } + + public static IrisBiome getBiomeOrMantle(Engine engine, Location l) { + return engine.getBiomeOrMantle(l.getBlockX(), l.getBlockY(), l.getBlockZ()); + } + + public static IrisBiome getSurfaceBiome(Engine engine, Chunk c) { + return engine.getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8); + } + + public static IrisRegion getRegion(Engine engine, Chunk c) { + return engine.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8); + } + + public static void gotoBiome(Engine engine, IrisBiome biome, Player player, boolean teleport) { + find(engine, Locator.surfaceBiome(biome.getLoadKey()), player, teleport, "Biome " + biome.getName()); + } + + public static void gotoObject(Engine engine, String s, Player player, boolean teleport) { + find(engine, Locator.object(s), player, teleport, "Object " + s); + } + + public static void gotoRegion(Engine engine, IrisRegion r, Player player, boolean teleport) { + if (!engine.getDimension().getRegions().contains(r.getLoadKey())) { + player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("name", String.valueOf(r.getName())))); + return; + } + + find(engine, Locator.region(r.getLoadKey()), player, teleport, "Region " + r.getName()); + } + + public static void gotoPOI(Engine engine, String type, Player p, boolean teleport) { + find(engine, Locator.poi(type), p, teleport, "POI " + type); + } + + public static void gotoStructure(Engine engine, String key, Player player, boolean teleport) { + find(engine, Locator.structure(key), player, teleport, "Structure " + key); + } + + private static void find(Engine engine, Locator locator, Player player, boolean teleport, String message) { + find(engine, locator, player, 120_000, location -> { + if (location == null) { + player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_COULD_NOT_FIND_WITHIN_SEARCH_RANGE, MessageArgument.untrusted("message", String.valueOf(message)))); + return; + } + if (teleport) { + J.runEntity(player, () -> teleportAsyncSafely(player, location)); + player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_TELEPORTING, MessageArgument.untrusted("message", String.valueOf(message)))); + } else { + player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_AT, MessageArgument.untrusted("message", String.valueOf(message)), MessageArgument.untrusted("blockX", String.valueOf(location.getBlockX())), MessageArgument.untrusted("blockY", String.valueOf(location.getBlockY())), MessageArgument.untrusted("blockZ", String.valueOf(location.getBlockZ())))); + } + }); + } + + private static void find(Engine engine, Locator locator, Player player, long timeout, Consumer consumer) { + AtomicLong checks = new AtomicLong(); + long ms = M.ms(); + World world = player.getWorld(); + Location origin = player.getLocation(); + int originChunkX = origin.getBlockX() >> 4; + int originChunkZ = origin.getBlockZ() >> 4; + new SingleJob(IrisLanguage.text(RuntimeUiMessages.JOB_SEARCHED_CHUNKS, MessageArgument.trusted("chunks", 0)), () -> { + CompletableFuture search = null; + boolean resultDispatched = false; + UUID playerId = player.getUniqueId(); + try { + search = locator.find(engine, new Position2(originChunkX, originChunkZ), timeout, checks::set); + CompletableFuture previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search); + if (previous != null && previous != search) { + previous.cancel(true); + } + Position2 at = search.get(); + if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) { + return; + } + + if (at != null) { + int bx = (at.getX() << 4) + 8; + int bz = (at.getZ() << 4) + 8; + try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_locator_result"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + int by = engine.getMinHeight() + engine.getHeight(bx, bz, false) + 2; + resultDispatched = dispatchLocateResult( + player, world, consumer, new Location(world, bx, by, bz), playerId, search); + } + } else { + resultDispatched = dispatchLocateResult(player, world, consumer, null, playerId, search); + } + } catch (CancellationException ignored) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (WrongEngineBroException | ExecutionException e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } finally { + if (search != null && !resultDispatched) { + ACTIVE_LOCATE_REQUESTS.remove(playerId, search); + } + } + }) { + @Override + public String getName() { + return IrisLanguage.text( + RuntimeUiMessages.JOB_SEARCHED_CHUNKS, + MessageArgument.trusted("chunks", Form.f(checks.get())) + ); + } + + @Override + public int getTotalWork() { + return (int) timeout; + } + + @Override + public int getWorkCompleted() { + return (int) Math.min(M.ms() - ms, timeout - 1); + } + }.execute(new VolmitSender(player)); + } + + private static boolean dispatchLocateResult(Player player, World world, Consumer consumer, + Location location, UUID playerId, + CompletableFuture search) { + boolean scheduled = J.runEntity(player, () -> { + if (!ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { + return; + } + if (!player.isOnline() || !world.equals(player.getWorld())) { + return; + } + consumer.accept(location); + }); + if (!scheduled) { + IrisLogging.warn("Could not schedule an Iris locator result for " + player.getName() + "."); + } + return scheduled; + } + + private static void teleportAsyncSafely(Player player, Location location) { + if (player == null || location == null) { + return; + } + + if (invokeNativeTeleportAsync(player, location)) { + return; + } + + try { + CompletableFuture teleportFuture = PaperLib.teleportAsync(player, location); + if (teleportFuture != null) { + teleportFuture.exceptionally(throwable -> { + IrisLogging.reportError(throwable); + return false; + }); + } + } catch (Throwable throwable) { + IrisLogging.reportError(throwable); + } + } + + private static boolean invokeNativeTeleportAsync(Player player, Location location) { + try { + Method teleportAsyncMethod = player.getClass().getMethod("teleportAsync", Location.class); + teleportAsyncMethod.invoke(player, location); + return true; + } catch (NoSuchMethodException ignored) { + return false; + } catch (Throwable throwable) { + IrisLogging.reportError(throwable); + return false; + } + } + + record LootMutationTask(World world, int chunkX, int chunkZ, Runnable mutation) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/PlatformChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/PlatformChunkGenerator.java new file mode 100644 index 000000000..6ed15e9ed --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/platform/PlatformChunkGenerator.java @@ -0,0 +1,79 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.platform; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.Hotloadable; +import art.arcane.iris.util.common.data.DataProvider; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public interface PlatformChunkGenerator extends Hotloadable, DataProvider { + @Nullable + Engine getEngine(); + + @Override + default IrisData getData() { + return getTarget().getData(); + } + + @NotNull + EngineTarget getTarget(); + + void close(); + + default CompletableFuture closeAsync() { + close(); + return CompletableFuture.completedFuture(null); + } + + default CompletableFuture hotloadComplexAsync(long acquisitionTimeout, TimeUnit unit) { + if (acquisitionTimeout <= 0L) { + throw new IllegalArgumentException("Complex hotload acquisition timeout must be positive."); + } + Objects.requireNonNull(unit, "Complex hotload acquisition timeout unit"); + Engine activeEngine = getEngine(); + if (activeEngine == null) { + return CompletableFuture.completedFuture(null); + } + try { + activeEngine.hotloadComplex(); + return CompletableFuture.completedFuture(null); + } catch (Throwable failure) { + return CompletableFuture.failedFuture(failure); + } + } + + default void quiesceForServerShutdown() { + } + + boolean isStudio(); + + default boolean isClosing() { + return false; + } + + CompletableFuture getSpawnChunks(); +} diff --git a/core/src/main/java/com/volmit/iris/engine/platform/studio/EnginedStudioGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/studio/EnginedStudioGenerator.java similarity index 83% rename from core/src/main/java/com/volmit/iris/engine/platform/studio/EnginedStudioGenerator.java rename to core/src/main/java/art/arcane/iris/engine/platform/studio/EnginedStudioGenerator.java index cd966c158..4d36201fd 100644 --- a/core/src/main/java/com/volmit/iris/engine/platform/studio/EnginedStudioGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/studio/EnginedStudioGenerator.java @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.engine.platform.studio; +package art.arcane.iris.engine.platform.studio; -import com.volmit.iris.engine.data.chunk.TerrainChunk; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.WrongEngineBroException; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.WrongEngineBroException; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor diff --git a/core/src/main/java/art/arcane/iris/engine/platform/studio/StudioGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/studio/StudioGenerator.java new file mode 100644 index 000000000..13fc9bb8b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/platform/studio/StudioGenerator.java @@ -0,0 +1,34 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.platform.studio; + +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.WrongEngineBroException; + +public interface StudioGenerator { + default boolean requiresPreSessionPreparation() { + return false; + } + + default void prepareChunkBeforeSession(Engine engine, int x, int z) throws WrongEngineBroException { + } + + void generateChunk(Engine engine, TerrainChunk tc, int x, int z) throws WrongEngineBroException; +} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/studio/generators/BiomeBuffetGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/studio/generators/BiomeBuffetGenerator.java new file mode 100644 index 000000000..39f58d409 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/platform/studio/generators/BiomeBuffetGenerator.java @@ -0,0 +1,86 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.platform.studio.generators; + +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.framework.WrongEngineBroException; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.platform.studio.EnginedStudioGenerator; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.context.IrisContext; + +import java.util.Objects; + +public class BiomeBuffetGenerator extends EnginedStudioGenerator { + private static final PlatformBlockState FLOOR = B.getState("BARRIER"); + private final IrisBiome[] biomes; + private final int width; + private final int biomeSize; + + public BiomeBuffetGenerator(Engine engine, int biomeSize) { + super(engine); + this.biomeSize = biomeSize; + biomes = engine.getDimension().getAllBiomes(engine).toArray(new IrisBiome[0]); + width = Math.max((int) Math.sqrt(biomes.length), 1); + } + + @Override + public boolean requiresPreSessionPreparation() { + return true; + } + + @Override + public synchronized void prepareChunkBeforeSession(Engine engine, int x, int z) { + IrisBiome biome = biomeAt(x, z); + if (biome == null || Objects.equals(engine.getDimension().getFocus(), biome.getLoadKey())) { + return; + } + + engine.getDimension().setFocus(biome.getLoadKey()); + engine.hotloadComplex(); + } + + @Override + public synchronized void generateChunk(Engine engine, TerrainChunk tc, int x, int z) throws WrongEngineBroException { + IrisBiome biome = biomeAt(x, z); + if (biome == null) { + try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_biome_buffet_stage"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + tc.setRegion(0, 0, 0, 16, 1, 16, FLOOR); + } + return; + } + + prepareChunkBeforeSession(engine, x, z); + + try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_biome_buffet_stage"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + engine.generate(x << 4, z << 4, tc, true); + } + } + + private IrisBiome biomeAt(int x, int z) { + int id = Cache.to1D(x / biomeSize, 0, z / biomeSize, width, 1); + return id < 0 || id >= biomes.length ? null : biomes[id]; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/studio/generators/JigsawStudioGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/studio/generators/JigsawStudioGenerator.java new file mode 100644 index 000000000..d57e0cdc0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/platform/studio/generators/JigsawStudioGenerator.java @@ -0,0 +1,931 @@ +package art.arcane.iris.engine.platform.studio.generators; + +import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarDirection; +import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarTopology; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioControlPosition; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant; +import art.arcane.iris.core.service.JigsawStudioService; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.framework.WrongEngineBroException; +import art.arcane.iris.engine.object.IrisDirection; +import art.arcane.iris.engine.object.IrisJigsawConnector; +import art.arcane.iris.engine.object.IrisJigsawPiece; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectRotation; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.JigsawJoint; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.engine.platform.studio.EnginedStudioGenerator; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.collection.KList; +import org.bukkit.World; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class JigsawStudioGenerator extends EnginedStudioGenerator { + private static final int CHECKER_SIZE = 4; + + private final JigsawStudioActivation.Request request; + private final JigsawStudioSession session; + private final PlatformBlockState lightFloor; + private final PlatformBlockState darkFloor; + private final PlatformBlockState frame; + private final PlatformBlockState topologyBase; + private final PlatformBlockState topologyPath; + private final PlatformBlockState connectorCap; + private final PlatformBlockState invalidMarker; + private final PlatformBlockState controlChest; + private final FinalStateRenderer finalStateRenderer; + private final AtomicBoolean serviceRegistered = new AtomicBoolean(false); + private final ConcurrentMap renderedBays = new ConcurrentHashMap<>(); + private final ConcurrentMap jigsawStates = new ConcurrentHashMap<>(); + + public JigsawStudioGenerator( + Engine engine, + JigsawStudioActivation.Request request, + JigsawStudioSession session + ) { + this( + engine, + request, + session, + B.getState("minecraft:smooth_stone"), + B.getState("minecraft:polished_deepslate"), + B.getState("minecraft:white_concrete"), + B.getState("minecraft:light_gray_wool"), + B.getState("minecraft:red_wool"), + B.getState("minecraft:sea_lantern"), + B.getState("minecraft:red_stained_glass"), + B.getState("minecraft:chest[facing=south,type=single,waterlogged=false]"), + JigsawStudioGenerator::rotatedFinalState + ); + } + + JigsawStudioGenerator( + Engine engine, + JigsawStudioActivation.Request request, + JigsawStudioSession session, + PlatformBlockState lightFloor, + PlatformBlockState darkFloor, + PlatformBlockState frame, + PlatformBlockState topologyBase, + PlatformBlockState topologyPath, + PlatformBlockState connectorCap, + PlatformBlockState invalidMarker, + PlatformBlockState controlChest, + FinalStateRenderer finalStateRenderer + ) { + super(engine); + this.request = Objects.requireNonNull(request, "Jigsaw Studio request"); + this.session = Objects.requireNonNull(session, "Jigsaw Studio session"); + this.lightFloor = Objects.requireNonNull(lightFloor, "Jigsaw Studio light floor block"); + this.darkFloor = Objects.requireNonNull(darkFloor, "Jigsaw Studio dark floor block"); + this.frame = Objects.requireNonNull(frame, "Jigsaw Studio frame block"); + this.topologyBase = Objects.requireNonNull(topologyBase, "Jigsaw Studio topology base block"); + this.topologyPath = Objects.requireNonNull(topologyPath, "Jigsaw Studio topology path block"); + this.connectorCap = Objects.requireNonNull(connectorCap, "Jigsaw Studio connector cap block"); + this.invalidMarker = Objects.requireNonNull(invalidMarker, "Jigsaw Studio invalid marker block"); + this.controlChest = Objects.requireNonNull(controlChest, "Jigsaw Studio control chest block"); + this.finalStateRenderer = Objects.requireNonNull( + finalStateRenderer, + "Jigsaw Studio connector final-state renderer"); + if (!request.requestId().equals(session.sessionId())) { + throw new IllegalArgumentException("Jigsaw Studio request and session IDs do not match"); + } + if (request.mode() != session.layout().mode()) { + throw new IllegalArgumentException("Jigsaw Studio request and session modes do not match"); + } + } + + public JigsawStudioActivation.Request getRequest() { + return request; + } + + public JigsawStudioSession getSession() { + return session; + } + + public JigsawStudioLayout getLayout() { + return session.layout(); + } + + public RenderedBay renderBay(JigsawStudioBay workcell) { + JigsawStudioBay activeWorkcell = requireWorkcell(workcell); + JigsawStudioSession.WorkcellSnapshot snapshot = session.workcellSnapshot(activeWorkcell.stableId()); + if (snapshot.activeVariantKey().isEmpty()) { + return RenderedBay.empty(activeWorkcell.bounds().dimensions()); + } + JigsawStudioVariant variant = session.activeVariant(activeWorkcell.stableId()).orElse(null); + if (variant == null) { + return RenderedBay.invalid( + activeWorkcell.bounds().dimensions(), + "active variant '" + snapshot.activeVariantKey() + "' is missing from the Studio catalog"); + } + return renderVariant(activeWorkcell, variant, snapshot.loadGeneration()); + } + + public RenderedBay renderVariant(JigsawStudioBay workcell, JigsawStudioVariant variant) { + return buildRenderedBay(requireAcceptedVariant(workcell, variant), variant); + } + + public RenderedBay renderVariant( + JigsawStudioBay workcell, + JigsawStudioVariant variant, + long loadGeneration + ) { + JigsawStudioBay activeWorkcell = requireAcceptedVariant(workcell, variant); + RenderKey key = new RenderKey(activeWorkcell.stableId(), variant.pieceKey(), loadGeneration); + return renderedBays.computeIfAbsent(key, ignored -> buildRenderedBay(activeWorkcell, variant)); + } + + public void invalidateRender(String workcellId) { + if (workcellId != null) { + renderedBays.keySet().removeIf(key -> key.workcellId().equals(workcellId)); + } + } + + public void invalidateRender(String workcellId, String pieceKey) { + if (workcellId != null && pieceKey != null) { + renderedBays.keySet().removeIf(key -> key.workcellId().equals(workcellId) + && key.pieceKey().equals(pieceKey)); + } + } + + @Override + public void generateChunk(Engine engine, TerrainChunk terrainChunk, int chunkX, int chunkZ) + throws WrongEngineBroException { + try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_jigsaw_studio_stage"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + paintChunk(terrainChunk, chunkX, chunkZ); + ensureServiceRegistered(engine); + if (serviceRegistered.get()) { + JigsawStudioService.get().markChunkGenerated(engine, this, chunkX, chunkZ); + } + } + } + + public void paintChunk(TerrainChunk terrainChunk, int chunkX, int chunkZ) { + Objects.requireNonNull(terrainChunk, "Jigsaw Studio terrain chunk"); + int floorY = Math.max(terrainChunk.getMinHeight(), JigsawStudioLayout.FLOOR_Y); + if (floorY >= terrainChunk.getMaxHeight()) { + return; + } + + int chunkWorldX = chunkX << 4; + int chunkWorldZ = chunkZ << 4; + paintFloor(terrainChunk, floorY, chunkWorldX, chunkWorldZ); + paintControlChest(terrainChunk, chunkWorldX, chunkWorldZ); + + int chunkMaxX = chunkWorldX + 15; + int chunkMaxZ = chunkWorldZ + 15; + for (JigsawStudioBay workcell : getLayout().bays()) { + JigsawStudioBounds bounds = workcell.bounds(); + if (!bounds.intersectsHorizontal( + chunkWorldX - 1, + chunkWorldZ - 1, + chunkMaxX + 1, + chunkMaxZ + 1)) { + continue; + } + paintCage(terrainChunk, workcell, chunkWorldX, chunkWorldZ); + if (workcell.archetype().isPresent()) { + paintTopologyGlyph(terrainChunk, workcell, chunkWorldX, chunkWorldZ); + } + RenderedBay renderedBay = renderBay(workcell); + if (!renderedBay.valid()) { + paintInvalidBay(terrainChunk, workcell, chunkWorldX, chunkWorldZ); + continue; + } + paintObject(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ); + if (session.workcellSnapshot(workcell.stableId()).connectorsVisible()) { + paintConnectors(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ); + } else { + paintHiddenConnectorFinalStates( + terrainChunk, + workcell, + renderedBay, + chunkWorldX, + chunkWorldZ); + } + } + } + + private void ensureServiceRegistered(Engine engine) { + if (serviceRegistered.get()) { + return; + } + World world = BukkitWorldBinding.world(engine.getTarget().getWorld()); + if (world == null) { + return; + } + publishServiceRegistration(() -> JigsawStudioService.get().register(engine, this)); + } + + void publishServiceRegistration(Runnable registration) { + Objects.requireNonNull(registration, "Jigsaw Studio service registration"); + if (serviceRegistered.get()) { + return; + } + synchronized (serviceRegistered) { + if (serviceRegistered.get()) { + return; + } + registration.run(); + serviceRegistered.set(true); + } + } + + private void paintFloor(TerrainChunk terrainChunk, int floorY, int chunkWorldX, int chunkWorldZ) { + for (int localX = 0; localX < 16; localX++) { + for (int localZ = 0; localZ < 16; localZ++) { + int worldX = chunkWorldX + localX; + int worldZ = chunkWorldZ + localZ; + int tileX = Math.floorDiv(worldX, CHECKER_SIZE); + int tileZ = Math.floorDiv(worldZ, CHECKER_SIZE); + PlatformBlockState block = ((tileX + tileZ) & 1) == 0 ? lightFloor : darkFloor; + terrainChunk.setBlock(localX, floorY, localZ, block); + } + } + } + + private void paintControlChest(TerrainChunk terrainChunk, int chunkWorldX, int chunkWorldZ) { + JigsawStudioControlPosition control = getLayout().controlPosition(); + setWorldBlock( + terrainChunk, + control.worldX(), + control.worldY(), + control.worldZ(), + controlChest, + chunkWorldX, + chunkWorldZ); + } + + private void paintCage( + TerrainChunk terrainChunk, + JigsawStudioBay workcell, + int chunkWorldX, + int chunkWorldZ + ) { + JigsawStudioBounds bounds = workcell.bounds(); + int minimumX = bounds.originX() - 1; + int maximumX = bounds.maxX() + 1; + int minimumZ = bounds.originZ() - 1; + int maximumZ = bounds.maxZ() + 1; + int bottomY = bounds.originY(); + int topY = bounds.maxY() + 1; + + paintRectangle( + terrainChunk, + minimumX, + maximumX, + bottomY, + minimumZ, + maximumZ, + chunkWorldX, + chunkWorldZ); + paintRectangle( + terrainChunk, + minimumX, + maximumX, + topY, + minimumZ, + maximumZ, + chunkWorldX, + chunkWorldZ); + for (int y = bottomY + 1; y < topY; y++) { + setWorldBlock(terrainChunk, minimumX, y, minimumZ, frame, chunkWorldX, chunkWorldZ); + setWorldBlock(terrainChunk, maximumX, y, minimumZ, frame, chunkWorldX, chunkWorldZ); + setWorldBlock(terrainChunk, minimumX, y, maximumZ, frame, chunkWorldX, chunkWorldZ); + setWorldBlock(terrainChunk, maximumX, y, maximumZ, frame, chunkWorldX, chunkWorldZ); + } + } + + private void paintRectangle( + TerrainChunk terrainChunk, + int minimumX, + int maximumX, + int y, + int minimumZ, + int maximumZ, + int chunkWorldX, + int chunkWorldZ + ) { + for (int x = minimumX; x <= maximumX; x++) { + setWorldBlock(terrainChunk, x, y, minimumZ, frame, chunkWorldX, chunkWorldZ); + setWorldBlock(terrainChunk, x, y, maximumZ, frame, chunkWorldX, chunkWorldZ); + } + for (int z = minimumZ + 1; z < maximumZ; z++) { + setWorldBlock(terrainChunk, minimumX, y, z, frame, chunkWorldX, chunkWorldZ); + setWorldBlock(terrainChunk, maximumX, y, z, frame, chunkWorldX, chunkWorldZ); + } + } + + private void paintTopologyGlyph( + TerrainChunk terrainChunk, + JigsawStudioBay workcell, + int chunkWorldX, + int chunkWorldZ + ) { + JigsawPlanarTopology topology = workcell.topology().orElseThrow(); + JigsawStudioBounds bounds = workcell.bounds(); + int minimumX = Math.max(bounds.originX(), chunkWorldX); + int maximumX = Math.min(bounds.maxX(), chunkWorldX + 15); + int minimumZ = Math.max(bounds.originZ(), chunkWorldZ); + int maximumZ = Math.min(bounds.maxZ(), chunkWorldZ + 15); + if (minimumX > maximumX || minimumZ > maximumZ) { + return; + } + int glyphY = bounds.originY() - 1; + int centerX = bounds.dimensions().width() / 2; + int centerZ = bounds.dimensions().depth() / 2; + for (int worldX = minimumX; worldX <= maximumX; worldX++) { + int localX = worldX - bounds.originX(); + for (int worldZ = minimumZ; worldZ <= maximumZ; worldZ++) { + int localZ = worldZ - bounds.originZ(); + PlatformBlockState state = topologyGlyphState(topology, bounds.dimensions(), localX, localZ, + centerX, centerZ); + setWorldBlock(terrainChunk, worldX, glyphY, worldZ, state, chunkWorldX, chunkWorldZ); + } + } + } + + private PlatformBlockState topologyGlyphState( + JigsawPlanarTopology topology, + JigsawStudioCellDimensions dimensions, + int localX, + int localZ, + int centerX, + int centerZ + ) { + if (isConnectorCap(topology, dimensions, localX, localZ, centerX, centerZ)) { + return connectorCap; + } + if (isTopologyPath(topology, localX, localZ, centerX, centerZ)) { + return topologyPath; + } + return topologyBase; + } + + static boolean isConnectorCap( + JigsawPlanarTopology topology, + JigsawStudioCellDimensions dimensions, + int localX, + int localZ, + int centerX, + int centerZ + ) { + return topology.connects(JigsawPlanarDirection.NORTH) && localX == centerX && localZ == 0 + || topology.connects(JigsawPlanarDirection.EAST) + && localX == dimensions.width() - 1 && localZ == centerZ + || topology.connects(JigsawPlanarDirection.SOUTH) + && localX == centerX && localZ == dimensions.depth() - 1 + || topology.connects(JigsawPlanarDirection.WEST) && localX == 0 && localZ == centerZ; + } + + static boolean isTopologyPath( + JigsawPlanarTopology topology, + int localX, + int localZ, + int centerX, + int centerZ + ) { + if (topology == JigsawPlanarTopology.BLANK) { + return false; + } + boolean vertical = localX == centerX + && (topology.connects(JigsawPlanarDirection.NORTH) && localZ <= centerZ + || topology.connects(JigsawPlanarDirection.SOUTH) && localZ >= centerZ); + boolean horizontal = localZ == centerZ + && (topology.connects(JigsawPlanarDirection.WEST) && localX <= centerX + || topology.connects(JigsawPlanarDirection.EAST) && localX >= centerX); + return vertical || horizontal; + } + + private RenderedBay buildRenderedBay(JigsawStudioBay workcell, JigsawStudioVariant variant) { + IrisJigsawPiece piece = request.source().getJigsawPieceLoader().load(variant.pieceKey(), false); + if (piece == null) { + return RenderedBay.invalid( + workcell.bounds().dimensions(), + "piece '" + variant.pieceKey() + "' is missing"); + } + if (piece.getObject() == null || piece.getObject().isBlank()) { + return RenderedBay.invalid( + workcell.bounds().dimensions(), + "piece '" + variant.pieceKey() + "' has no object"); + } + if (!piece.getObject().equals(variant.objectKey())) { + return RenderedBay.invalid( + workcell.bounds().dimensions(), + "piece '" + variant.pieceKey() + "' changed its object from '" + variant.objectKey() + + "' to '" + piece.getObject() + "'"); + } + IrisObject object = request.source().getObjectLoader().load(variant.objectKey(), false); + if (object == null) { + return RenderedBay.invalid( + workcell.bounds().dimensions(), + "object '" + variant.objectKey() + "' is missing"); + } + if (object.getW() < 1 || object.getH() < 1 || object.getD() < 1) { + return RenderedBay.invalid( + workcell.bounds().dimensions(), + "object '" + variant.objectKey() + "' has invalid dimensions"); + } + + int quarterTurns = variant.sourceToCanonicalQuarterTurns(); + JigsawStudioCellDimensions dimensions = rotatedDimensions(object, quarterTurns); + if (!fits(dimensions, workcell.bounds().dimensions())) { + return RenderedBay.invalid( + dimensions, + "object '" + variant.objectKey() + "' does not fit workcell '" + workcell.stableId() + "'"); + } + + IrisObjectRotation rotation = IrisObjectRotation.of(0, -90.0D * quarterTurns, 0); + List blocks = renderBlocks(object, dimensions, quarterTurns, rotation); + if (blocks == null) { + return RenderedBay.invalid( + dimensions, + "object '" + variant.objectKey() + "' contains an invalid or out-of-bounds block"); + } + + ConnectorRenderResult connectors = renderConnectors(piece, object, dimensions, quarterTurns, rotation); + if (!connectors.failure().isEmpty()) { + return RenderedBay.invalid(dimensions, connectors.failure()); + } + return RenderedBay.valid(dimensions, blocks, connectors.connectors()); + } + + private List renderBlocks( + IrisObject object, + JigsawStudioCellDimensions dimensions, + int quarterTurns, + IrisObjectRotation rotation + ) { + VectorMap sourceBlocks = object.getBlocks(); + VectorMap sourceTiles = object.getStates(); + Vector3i center = object.getCenter(); + List blocks = new ArrayList<>(sourceBlocks.size()); + for (Map.Entry entry : sourceBlocks) { + IrisBlockVector signed = entry.getKey(); + int sourceX = signed.getBlockX() + center.getBlockX(); + int sourceY = signed.getBlockY() + center.getBlockY(); + int sourceZ = signed.getBlockZ() + center.getBlockZ(); + if (!inside(sourceX, sourceY, sourceZ, object.getW(), object.getH(), object.getD())) { + return null; + } + RotatedPosition position = rotatePosition( + sourceX, + sourceY, + sourceZ, + object.getW(), + object.getD(), + quarterTurns); + if (!inside( + position.x(), + position.y(), + position.z(), + dimensions.width(), + dimensions.height(), + dimensions.depth())) { + return null; + } + PlatformBlockState blockState = quarterTurns == 0 + ? entry.getValue() + : rotation.rotate(entry.getValue(), 0, 0, 0); + if (blockState == null) { + return null; + } + TileData tileData = sourceTiles.get(signed); + blocks.add(new RenderedBlock( + position.x(), + position.y(), + position.z(), + blockState, + tileData == null ? null : tileData.clone())); + } + return List.copyOf(blocks); + } + + private ConnectorRenderResult renderConnectors( + IrisJigsawPiece piece, + IrisObject object, + JigsawStudioCellDimensions dimensions, + int quarterTurns, + IrisObjectRotation rotation + ) { + KList sourceConnectors = piece.getConnectors(); + if (sourceConnectors == null) { + return ConnectorRenderResult.invalid("piece '" + piece.getLoadKey() + "' has no connector list"); + } + List connectors = new ArrayList<>(sourceConnectors.size()); + for (int index = 0; index < sourceConnectors.size(); index++) { + IrisJigsawConnector source = sourceConnectors.get(index); + if (!validConnectorMetadata(source)) { + return ConnectorRenderResult.invalid("connector " + index + " has incomplete metadata"); + } + IrisPosition sourcePosition = source.getPosition(); + if (!inside( + sourcePosition.getX(), + sourcePosition.getY(), + sourcePosition.getZ(), + object.getW(), + object.getH(), + object.getD())) { + return ConnectorRenderResult.invalid("connector " + index + " is outside its object bounds"); + } + RotatedPosition position = rotatePosition( + sourcePosition.getX(), + sourcePosition.getY(), + sourcePosition.getZ(), + object.getW(), + object.getD(), + quarterTurns); + IrisDirection front = quarterTurns == 0 + ? source.getDirection() + : rotation.rotate(source.getDirection()); + IrisDirection top = quarterTurns == 0 + ? source.getTop() + : rotation.rotate(source.getTop()); + String orientation; + try { + orientation = orientation(front, top); + } catch (IllegalArgumentException exception) { + return ConnectorRenderResult.invalid("connector " + index + " has an invalid orientation: " + + exception.getMessage()); + } + String finalState = finalStateRenderer.render(source.getFinalState(), rotation, quarterTurns); + if (finalState == null) { + return ConnectorRenderResult.invalid("connector " + index + " final state '" + + source.getFinalState() + "' cannot be parsed and rotated"); + } + IrisJigsawConnector connector = copyConnector(source) + .setPosition(new IrisPosition(position.x(), position.y(), position.z())) + .setDirection(front) + .setTop(top) + .setFinalState(finalState); + if (!inside( + position.x(), + position.y(), + position.z(), + dimensions.width(), + dimensions.height(), + dimensions.depth())) { + return ConnectorRenderResult.invalid("connector " + index + " rotated outside its object bounds"); + } + connectors.add(new RenderedConnector( + position.x(), position.y(), position.z(), connector, orientation)); + } + return ConnectorRenderResult.valid(connectors); + } + + private static String rotatedFinalState( + String finalState, + IrisObjectRotation rotation, + int quarterTurns + ) { + PlatformBlockState state = B.getStateOrNull(finalState, false); + if (state == null) { + return null; + } + if (quarterTurns == 0) { + return state.key(); + } + PlatformBlockState rotated = rotation.rotate(state, 0, 0, 0); + return rotated == null ? null : rotated.key(); + } + + private void paintObject( + TerrainChunk terrainChunk, + JigsawStudioBay workcell, + RenderedBay renderedBay, + int chunkWorldX, + int chunkWorldZ + ) { + JigsawStudioBounds bounds = workcell.bounds(); + for (RenderedBlock block : renderedBay.blocks()) { + setWorldBlock( + terrainChunk, + bounds.originX() + block.x(), + bounds.originY() + block.y(), + bounds.originZ() + block.z(), + block.state(), + chunkWorldX, + chunkWorldZ); + } + } + + private void paintConnectors( + TerrainChunk terrainChunk, + JigsawStudioBay workcell, + RenderedBay renderedBay, + int chunkWorldX, + int chunkWorldZ + ) { + JigsawStudioBounds bounds = workcell.bounds(); + for (RenderedConnector connector : renderedBay.connectors()) { + PlatformBlockState state = jigsawStates.computeIfAbsent( + connector.orientation(), + key -> B.getState("minecraft:jigsaw[orientation=" + key + "]")); + setWorldBlock( + terrainChunk, + bounds.originX() + connector.x(), + bounds.originY() + connector.y(), + bounds.originZ() + connector.z(), + state == null ? invalidMarker : state, + chunkWorldX, + chunkWorldZ); + } + } + + private void paintHiddenConnectorFinalStates( + TerrainChunk terrainChunk, + JigsawStudioBay workcell, + RenderedBay renderedBay, + int chunkWorldX, + int chunkWorldZ + ) { + Set occupied = new HashSet<>(renderedBay.blocks().size()); + for (RenderedBlock block : renderedBay.blocks()) { + occupied.add(new RenderedPosition(block.x(), block.y(), block.z())); + } + JigsawStudioBounds bounds = workcell.bounds(); + for (RenderedConnector connector : renderedBay.connectors()) { + if (occupied.contains(new RenderedPosition(connector.x(), connector.y(), connector.z()))) { + continue; + } + PlatformBlockState finalState = B.getStateOrNull(connector.connector().getFinalState(), false); + setWorldBlock( + terrainChunk, + bounds.originX() + connector.x(), + bounds.originY() + connector.y(), + bounds.originZ() + connector.z(), + finalState == null ? invalidMarker : finalState, + chunkWorldX, + chunkWorldZ); + } + } + + private void paintInvalidBay( + TerrainChunk terrainChunk, + JigsawStudioBay workcell, + int chunkWorldX, + int chunkWorldZ + ) { + JigsawStudioBounds bounds = workcell.bounds(); + int y = bounds.originY(); + int length = Math.min(bounds.dimensions().width(), bounds.dimensions().depth()); + for (int offset = 0; offset < length; offset++) { + setWorldBlock( + terrainChunk, + bounds.originX() + offset, + y, + bounds.originZ() + offset, + invalidMarker, + chunkWorldX, + chunkWorldZ); + setWorldBlock( + terrainChunk, + bounds.maxX() - offset, + y, + bounds.originZ() + offset, + invalidMarker, + chunkWorldX, + chunkWorldZ); + } + } + + private JigsawStudioBay requireWorkcell(JigsawStudioBay workcell) { + JigsawStudioBay activeWorkcell = Objects.requireNonNull(workcell, "Jigsaw Studio workcell"); + if (getLayout().get(activeWorkcell.stableId()) != activeWorkcell) { + throw new IllegalArgumentException("Workcell does not belong to this Jigsaw Studio layout"); + } + return activeWorkcell; + } + + private JigsawStudioBay requireAcceptedVariant(JigsawStudioBay workcell, JigsawStudioVariant variant) { + JigsawStudioBay activeWorkcell = requireWorkcell(workcell); + if (!getLayout().accepts(activeWorkcell, Objects.requireNonNull(variant, "Jigsaw Studio variant"))) { + throw new IllegalArgumentException("Variant '" + variant.pieceKey() + + "' does not belong to workcell '" + activeWorkcell.stableId() + "'"); + } + return activeWorkcell; + } + + static RotatedPosition rotatePosition( + int x, + int y, + int z, + int width, + int depth, + int quarterTurns + ) { + return switch (Math.floorMod(quarterTurns, 4)) { + case 0 -> new RotatedPosition(x, y, z); + case 1 -> new RotatedPosition(depth - 1 - z, y, x); + case 2 -> new RotatedPosition(width - 1 - x, y, depth - 1 - z); + case 3 -> new RotatedPosition(z, y, width - 1 - x); + default -> throw new IllegalStateException("Unreachable Jigsaw Studio rotation"); + }; + } + + static String orientation(IrisDirection front, IrisDirection top) { + IrisDirection activeFront = Objects.requireNonNull(front, "Jigsaw connector front"); + IrisDirection activeTop = Objects.requireNonNull(top, "Jigsaw connector top"); + if (activeFront == IrisDirection.UP_POSITIVE_Y || activeFront == IrisDirection.DOWN_NEGATIVE_Y) { + if (activeTop.isVertical()) { + throw new IllegalArgumentException("Vertical jigsaw fronts require a horizontal top direction"); + } + return directionName(activeFront) + "_" + directionName(activeTop); + } + if (activeTop != IrisDirection.UP_POSITIVE_Y) { + throw new IllegalArgumentException("Horizontal jigsaw fronts require an upward top direction"); + } + return directionName(activeFront) + "_up"; + } + + private static JigsawStudioCellDimensions rotatedDimensions(IrisObject object, int quarterTurns) { + return Math.floorMod(quarterTurns, 2) == 0 + ? new JigsawStudioCellDimensions(object.getW(), object.getH(), object.getD()) + : new JigsawStudioCellDimensions(object.getD(), object.getH(), object.getW()); + } + + private static boolean fits(JigsawStudioCellDimensions object, JigsawStudioCellDimensions bay) { + return object.width() <= bay.width() + && object.height() <= bay.height() + && object.depth() <= bay.depth(); + } + + private static boolean inside(int x, int y, int z, int width, int height, int depth) { + return x >= 0 && x < width + && y >= 0 && y < height + && z >= 0 && z < depth; + } + + private static boolean validConnectorMetadata(IrisJigsawConnector connector) { + return connector != null + && connector.getPosition() != null + && connector.getDirection() != null + && connector.getTop() != null + && connector.getJoint() != null + && connector.getPool() != null + && !connector.getPool().isBlank() + && connector.getName() != null + && !connector.getName().isBlank() + && connector.getTargetName() != null + && !connector.getTargetName().isBlank() + && connector.getFinalState() != null + && !connector.getFinalState().isBlank(); + } + + private static IrisJigsawConnector copyConnector(IrisJigsawConnector source) { + return new IrisJigsawConnector() + .setPosition(new IrisPosition( + source.getPosition().getX(), + source.getPosition().getY(), + source.getPosition().getZ())) + .setDirection(source.getDirection()) + .setTop(source.getTop()) + .setPool(source.getPool()) + .setName(source.getName()) + .setTargetName(source.getTargetName()) + .setChannel(source.getChannel() == null ? "" : source.getChannel()) + .setJoint(source.getJoint() == null ? JigsawJoint.ROLLABLE : source.getJoint()) + .setFinalState(source.getFinalState()) + .setSelectionPriority(source.getSelectionPriority()) + .setPlacementPriority(source.getPlacementPriority()); + } + + private static String directionName(IrisDirection direction) { + return switch (direction) { + case UP_POSITIVE_Y -> "up"; + case DOWN_NEGATIVE_Y -> "down"; + case NORTH_NEGATIVE_Z -> "north"; + case SOUTH_POSITIVE_Z -> "south"; + case EAST_POSITIVE_X -> "east"; + case WEST_NEGATIVE_X -> "west"; + }; + } + + private void setWorldBlock( + TerrainChunk terrainChunk, + int worldX, + int worldY, + int worldZ, + PlatformBlockState block, + int chunkWorldX, + int chunkWorldZ + ) { + int localX = worldX - chunkWorldX; + int localZ = worldZ - chunkWorldZ; + if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16) { + return; + } + if (worldY < terrainChunk.getMinHeight() || worldY >= terrainChunk.getMaxHeight()) { + return; + } + terrainChunk.setBlock(localX, worldY, localZ, block); + } + + public record RenderedBay( + JigsawStudioCellDimensions dimensions, + List blocks, + List connectors, + String failure + ) { + public RenderedBay { + dimensions = Objects.requireNonNull(dimensions, "Jigsaw Studio rendered dimensions"); + blocks = List.copyOf(blocks); + connectors = List.copyOf(connectors); + failure = failure == null ? "" : failure.trim(); + } + + public static RenderedBay valid( + JigsawStudioCellDimensions dimensions, + List blocks, + List connectors + ) { + return new RenderedBay(dimensions, blocks, connectors, ""); + } + + public static RenderedBay empty(JigsawStudioCellDimensions dimensions) { + return valid(dimensions, List.of(), List.of()); + } + + public static RenderedBay invalid(JigsawStudioCellDimensions dimensions, String failure) { + return new RenderedBay(dimensions, List.of(), List.of(), failure); + } + + public boolean valid() { + return failure.isEmpty(); + } + } + + public record RenderedBlock(int x, int y, int z, PlatformBlockState state, TileData tileData) { + public RenderedBlock { + state = Objects.requireNonNull(state, "Jigsaw Studio rendered block state"); + } + } + + public record RenderedConnector( + int x, + int y, + int z, + IrisJigsawConnector connector, + String orientation + ) { + public RenderedConnector { + connector = Objects.requireNonNull(connector, "Jigsaw Studio rendered connector"); + orientation = Objects.requireNonNull(orientation, "Jigsaw Studio connector orientation"); + } + } + + private record RenderKey(String workcellId, String pieceKey, long loadGeneration) { + } + + private record ConnectorRenderResult(List connectors, String failure) { + private ConnectorRenderResult { + connectors = List.copyOf(connectors); + failure = failure == null ? "" : failure; + } + + private static ConnectorRenderResult valid(List connectors) { + return new ConnectorRenderResult(connectors, ""); + } + + private static ConnectorRenderResult invalid(String failure) { + return new ConnectorRenderResult(List.of(), failure); + } + } + + @FunctionalInterface + interface FinalStateRenderer { + String render(String finalState, IrisObjectRotation rotation, int quarterTurns); + } + + record RotatedPosition(int x, int y, int z) { + } + + private record RenderedPosition(int x, int y, int z) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/studio/generators/ObjectStudioGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/studio/generators/ObjectStudioGenerator.java new file mode 100644 index 000000000..db25412dc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/platform/studio/generators/ObjectStudioGenerator.java @@ -0,0 +1,301 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.platform.studio.generators; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.runtime.ObjectStudioActivation; +import art.arcane.iris.core.runtime.ObjectStudioLayout; +import art.arcane.iris.core.runtime.ObjectStudioLayout.GridCell; +import art.arcane.iris.core.service.ObjectStudioSaveService; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.framework.WrongEngineBroException; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.platform.studio.EnginedStudioGenerator; +import art.arcane.iris.platform.bukkit.BukkitBiome; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.Vector3i; +import org.bukkit.Material; +import org.bukkit.block.Biome; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.project.context.IrisContext; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ObjectStudioGenerator extends EnginedStudioGenerator { + public static final int DEFAULT_PADDING = 2; + private static final PlatformBlockState FLOOR = BukkitBlockState.of(Material.POLISHED_DEEPSLATE.createBlockData()); + private static final PlatformBlockState FRAME = BukkitBlockState.of(Material.SMOOTH_QUARTZ.createBlockData()); + private static final PlatformBlockState MARKER = BukkitBlockState.of(Material.END_ROD.createBlockData()); + private static final PlatformBiome DEFAULT_BIOME = BukkitBiome.of(Biome.PLAINS); + + private final int padding; + private final PlatformBlockState floor; + private final PlatformBlockState frame; + private final PlatformBlockState marker; + private final AtomicBoolean layoutBuilt = new AtomicBoolean(false); + private final Object layoutLock = new Object(); + private final Map objectCache = new ConcurrentHashMap<>(); + private final Map packData = new ConcurrentHashMap<>(); + private volatile ObjectStudioLayout layout; + + public ObjectStudioGenerator(Engine engine) { + this(engine, DEFAULT_PADDING, FLOOR, FRAME, MARKER); + } + + public ObjectStudioGenerator(Engine engine, int padding, PlatformBlockState floor, PlatformBlockState frame, PlatformBlockState marker) { + super(engine); + this.padding = padding; + this.floor = floor; + this.frame = frame; + this.marker = marker; + } + + public ObjectStudioLayout getLayout() { + return layout; + } + + public int getPadding() { + return padding; + } + + @Override + public void generateChunk(Engine engine, TerrainChunk tc, int x, int z) throws WrongEngineBroException { + try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_object_studio_stage"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + generateChunkWithinSession(engine, tc, x, z); + } + } + + private void generateChunkWithinSession(Engine engine, TerrainChunk tc, int x, int z) { + ensureLayout(engine); + + int floorY = Math.max(engine.getMinHeight(), ObjectStudioLayout.FLOOR_Y); + for (int bx = 0; bx < 16; bx++) { + for (int bz = 0; bz < 16; bz++) { + tc.setBiome(bx, floorY, bz, DEFAULT_BIOME); + tc.setBlock(bx, floorY, bz, floor); + } + } + + ObjectStudioLayout currentLayout = layout; + if (currentLayout == null) { + return; + } + + int chunkWorldX = x << 4; + int chunkWorldZ = z << 4; + int chunkMaxX = chunkWorldX + 15; + int chunkMaxZ = chunkWorldZ + 15; + int minHeight = engine.getMinHeight(); + int maxHeight = engine.getMaxHeight(); + + int plinthY = floorY + 1; + + for (GridCell cell : currentLayout.cells()) { + int frameMinX = cell.originX() - 1; + int frameMaxX = cell.originX() + cell.w(); + int frameMinZ = cell.originZ() - 1; + int frameMaxZ = cell.originZ() + cell.d(); + + if (frameMaxX < chunkWorldX || frameMinX > chunkMaxX) continue; + if (frameMaxZ < chunkWorldZ || frameMinZ > chunkMaxZ) continue; + + paintFrame(cell, tc, plinthY, chunkWorldX, chunkWorldZ, minHeight, maxHeight); + + IrisObject object = loadObject(cell); + if (object != null) { + placeSlice(object, cell, tc, chunkWorldX, chunkWorldZ, minHeight, maxHeight); + } + } + } + + private void paintFrame(GridCell cell, TerrainChunk tc, int plinthY, int chunkWorldX, int chunkWorldZ, int minHeight, int maxHeight) { + int x0 = cell.originX() - 1; + int x1 = cell.originX() + cell.w(); + int z0 = cell.originZ() - 1; + int z1 = cell.originZ() + cell.d(); + int topY = Math.min(maxHeight - 1, plinthY + cell.h() + 1); + + if (plinthY >= minHeight && plinthY < maxHeight) { + paintFrameRowX(tc, x0, x1, plinthY, z0, chunkWorldX, chunkWorldZ); + paintFrameRowX(tc, x0, x1, plinthY, z1, chunkWorldX, chunkWorldZ); + paintFrameRowZ(tc, x0, plinthY, z0, z1, chunkWorldX, chunkWorldZ); + paintFrameRowZ(tc, x1, plinthY, z0, z1, chunkWorldX, chunkWorldZ); + } + + if (topY > plinthY && topY < maxHeight) { + paintFrameRowX(tc, x0, x1, topY, z0, chunkWorldX, chunkWorldZ); + paintFrameRowX(tc, x0, x1, topY, z1, chunkWorldX, chunkWorldZ); + paintFrameRowZ(tc, x0, topY, z0, z1, chunkWorldX, chunkWorldZ); + paintFrameRowZ(tc, x1, topY, z0, z1, chunkWorldX, chunkWorldZ); + } + + int edgeLo = plinthY + 1; + int edgeHi = Math.min(topY - 1, maxHeight - 1); + if (edgeHi >= edgeLo) { + paintEdgePillar(tc, x0, edgeLo, edgeHi, z0, chunkWorldX, chunkWorldZ); + paintEdgePillar(tc, x1, edgeLo, edgeHi, z0, chunkWorldX, chunkWorldZ); + paintEdgePillar(tc, x0, edgeLo, edgeHi, z1, chunkWorldX, chunkWorldZ); + paintEdgePillar(tc, x1, edgeLo, edgeHi, z1, chunkWorldX, chunkWorldZ); + } + } + + private void paintEdgePillar(TerrainChunk tc, int x, int yMin, int yMax, int z, int chunkWorldX, int chunkWorldZ) { + if (x < chunkWorldX || x > chunkWorldX + 15) return; + if (z < chunkWorldZ || z > chunkWorldZ + 15) return; + int localX = x - chunkWorldX; + int localZ = z - chunkWorldZ; + for (int y = yMin; y <= yMax; y++) { + tc.setBlock(localX, y, localZ, marker); + } + } + + private void paintFrameRowX(TerrainChunk tc, int xMin, int xMax, int y, int z, int chunkWorldX, int chunkWorldZ) { + if (z < chunkWorldZ || z > chunkWorldZ + 15) return; + int lo = Math.max(xMin, chunkWorldX); + int hi = Math.min(xMax, chunkWorldX + 15); + for (int x = lo; x <= hi; x++) { + tc.setBlock(x - chunkWorldX, y, z - chunkWorldZ, frame); + } + } + + private void paintFrameRowZ(TerrainChunk tc, int x, int y, int zMin, int zMax, int chunkWorldX, int chunkWorldZ) { + if (x < chunkWorldX || x > chunkWorldX + 15) return; + int lo = Math.max(zMin, chunkWorldZ); + int hi = Math.min(zMax, chunkWorldZ + 15); + for (int z = lo; z <= hi; z++) { + tc.setBlock(x - chunkWorldX, y, z - chunkWorldZ, frame); + } + } + + private void placeSlice(IrisObject object, GridCell cell, TerrainChunk tc, int chunkWorldX, int chunkWorldZ, int minHeight, int maxHeight) { + VectorMap blocks = object.getBlocks(); + if (blocks == null || blocks.isEmpty()) return; + + Vector3i center = object.getCenter(); + int centerX = center == null ? 0 : center.getBlockX(); + int centerY = center == null ? 0 : center.getBlockY(); + int centerZ = center == null ? 0 : center.getBlockZ(); + + int originX = cell.originX(); + int originY = cell.originY(); + int originZ = cell.originZ(); + + for (Map.Entry entry : blocks) { + IrisBlockVector signed = entry.getKey(); + int worldX = originX + signed.getBlockX() + centerX; + int worldY = originY + signed.getBlockY() + centerY; + int worldZ = originZ + signed.getBlockZ() + centerZ; + + if (worldX < chunkWorldX || worldX > chunkWorldX + 15) continue; + if (worldZ < chunkWorldZ || worldZ > chunkWorldZ + 15) continue; + if (worldY < minHeight || worldY >= maxHeight) continue; + + PlatformBlockState data = entry.getValue(); + if (data == null) continue; + + tc.setBlock(worldX - chunkWorldX, worldY, worldZ - chunkWorldZ, data); + } + } + + private IrisObject loadObject(GridCell cell) { + String cacheKey = cell.pack() + "/" + cell.key(); + IrisObject cached = objectCache.get(cacheKey); + if (cached != null) return cached; + IrisData data = packData.get(cell.pack()); + if (data == null) return null; + IrisObject loaded = data.getObjectLoader().load(cell.key()); + if (loaded != null) { + objectCache.put(cacheKey, loaded); + } + return loaded; + } + + public Map getPackData() { + return packData; + } + + private void ensureLayout(Engine engine) { + if (layoutBuilt.get()) return; + synchronized (layoutLock) { + if (layoutBuilt.get()) return; + + Map sources = resolveSources(engine); + packData.putAll(sources); + + File layoutFile = layoutFile(engine); + ObjectStudioLayout resumed = ObjectStudioLayout.load(layoutFile, sources, padding); + if (resumed != null) { + layout = resumed; + } else { + layout = ObjectStudioLayout.build(sources, padding); + layout.save(layoutFile); + } + layoutBuilt.set(true); + + try { + ObjectStudioSaveService.get().register(engine, this); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + int cellCount = layout.cells().size(); + IrisBlockVector worldExtent = computeExtent(layout); + IrisLogging.info("Object Studio layout built: %d cells from %d pack(s), extent %d x %d blocks", + cellCount, sources.size(), worldExtent.getBlockX(), worldExtent.getBlockZ()); + } + } + + private Map resolveSources(Engine engine) { + String packKey = engine.getDimension() == null ? null : engine.getDimension().getLoadKey(); + Map registered = packKey == null ? null : ObjectStudioActivation.getSources(packKey); + if (registered != null && !registered.isEmpty()) { + return registered; + } + Map fallback = new LinkedHashMap<>(); + IrisData data = engine.getData(); + fallback.put(data.getDataFolder().getName(), data); + return fallback; + } + + private File layoutFile(Engine engine) { + File worldFolder = engine.getTarget().getWorld().worldFolder(); + return new File(new File(worldFolder, ".iris"), "object-studio-layout.json"); + } + + private static IrisBlockVector computeExtent(ObjectStudioLayout layout) { + int maxX = 0; + int maxZ = 0; + for (GridCell cell : layout.cells()) { + maxX = Math.max(maxX, cell.originX() + cell.w()); + maxZ = Math.max(maxZ, cell.originZ() + cell.d()); + } + return new IrisBlockVector(maxX, 0, maxZ); + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiome.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiome.java new file mode 100644 index 000000000..245ffe4a6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiome.java @@ -0,0 +1,75 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.spi.PlatformBiome; +import org.bukkit.block.Biome; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Interned Bukkit adapter for a neutral biome handle. + */ +public final class BukkitBiome implements PlatformBiome { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + + private final Biome biome; + private final String key; + private final String namespace; + + private BukkitBiome(Biome biome, String key) { + this.biome = biome; + this.key = key; + int colon = key.indexOf(':'); + this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft"; + } + + public static BukkitBiome of(Biome biome) { + String key = biomeKey(biome); + return CACHE.computeIfAbsent(key, (String k) -> new BukkitBiome(biome, k)); + } + + private static String biomeKey(Biome biome) { + for (String method : new String[]{"getKeyOrNull", "getKeyOrThrow", "getKey"}) { + try { + Object key = Biome.class.getMethod(method).invoke(biome); + if (key != null) { + return key.toString(); + } + } catch (Throwable ignored) { + } + } + return biome.toString(); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public Object nativeHandle() { + return biome; + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiomeWriter.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiomeWriter.java new file mode 100644 index 000000000..866b4584b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiomeWriter.java @@ -0,0 +1,46 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBiomeWriter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Bukkit adapter for biome resolution backed by the active NMS binding. + */ +public final class BukkitBiomeWriter implements PlatformBiomeWriter { + @Override + public int biomeIdFor(String key) { + return INMS.get().getBiomeBaseIdForKey(key); + } + + @Override + public List allBiomes() { + List natives = INMS.get().getBiomes(); + List biomes = new ArrayList<>(natives.size()); + for (Object biome : natives) { + biomes.add(BukkitPlatform.wrapBiome(biome)); + } + return biomes; + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockResolution.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockResolution.java new file mode 100644 index 000000000..bf6bdf9ee --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockResolution.java @@ -0,0 +1,297 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.link.data.DataType; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.engine.object.IrisCompat; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.common.data.registry.Materials; +import art.arcane.iris.util.common.reflect.KeyedType; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.data.BSupport; +import it.unimi.dsi.fastutil.ints.IntSet; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.data.BlockData; + +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +public final class BukkitBlockResolution { + private static final BSupportImpl BASE = new BSupportImpl(); + + private BukkitBlockResolution() { + } + + private static final class BSupportImpl extends BSupport { + @Override + protected void warn(String message) { + IrisLogging.warn(message); + } + + @Override + protected void debug(String message) { + IrisLogging.debug(message); + } + + @Override + protected void reportError(Throwable throwable) { + IrisLogging.reportError(throwable); + } + + @Override + protected void error(String message) { + IrisLogging.error(message); + } + + @Override + protected Material shortGrassMaterial() { + return Materials.GRASS; + } + + @Override + protected void appendExtraFoliageMaterials(IntSet foliage) { + } + + @Override + protected boolean includeLitInUpdatable() { + return false; + } + + @Override + protected BlockData resolveCompatBlock(String bdxf) { + if (bdxf.contains(":")) { + if (bdxf.startsWith("minecraft:")) { + return IrisServices.get(IrisCompat.class).getBlock(bdxf); + } + return null; + } + return IrisServices.get(IrisCompat.class).getBlock(bdxf); + } + + @Override + protected BlockData resolveExternalBlockData(String ix) { + if (ix.startsWith("minecraft:") || !ix.contains(":")) { + return null; + } + + Identifier key = Identifier.fromString(ix); + Optional bd = IrisServices.get(ExternalDataSVC.class).getBlockData(key); + debug("Loading block data " + key); + return bd.orElse(null); + } + + @Override + protected boolean shouldPreventLeafDecay() { + return IrisSettings.get().getGenerator().isPreventLeafDecay(); + } + + @Override + protected void appendExternalBlockTypes(KList blockTypes) { + for (Identifier id : IrisServices.get(ExternalDataSVC.class).getAllIdentifiers(DataType.BLOCK)) { + blockTypes.add(id.toString()); + } + } + + @Override + protected void appendExternalItemTypes(KList itemTypes) { + for (Identifier id : IrisServices.get(ExternalDataSVC.class).getAllIdentifiers(DataType.ITEM)) { + itemTypes.add(id.toString()); + } + } + + @Override + protected KMap, List> loadExternalBlockStates() { + KMap, List> flipped = new KMap<>(); + INMS.get().getBlockProperties().forEach((k, v) -> { + NamespacedKey key = KeyedType.getKey(k); + String serialized = key == null ? k.name().toLowerCase(Locale.ROOT) : key.toString(); + flipped.computeIfAbsent(v, $ -> new KList<>()).add(serialized); + }); + + var emptyStates = flipped.computeIfAbsent(new KList<>(0), $ -> new KList<>()); + for (var pair : IrisServices.get(ExternalDataSVC.class).getAllBlockProperties()) { + if (pair.getB().isEmpty()) { + emptyStates.add(pair.getA().toString()); + } else { + flipped.computeIfAbsent(pair.getB(), $ -> new KList<>()).add(pair.getA().toString()); + } + } + + KMap, List> states = new KMap<>(); + flipped.forEach((k, v) -> { + var old = states.put(v, k); + if (old != null) { + error("Duplicate block state: " + v + " (" + old + " and " + k + ")"); + } + }); + + return states; + } + } + + public static BlockData toDeepSlateOre(BlockData block, BlockData ore) { + return BASE.toDeepSlateOre(block, ore); + } + + public static boolean isDeepSlate(BlockData blockData) { + return BASE.isDeepSlate(blockData); + } + + public static boolean isOre(BlockData blockData) { + return BASE.isOre(blockData); + } + + public static boolean canPlaceOnto(Material mat, Material onto) { + return BASE.canPlaceOnto(mat, onto); + } + + public static boolean isFoliagePlantable(BlockData d) { + return BASE.isFoliagePlantable(d); + } + + public static boolean isFoliagePlantable(Material d) { + return BASE.isFoliagePlantable(d); + } + + public static boolean isWater(BlockData b) { + return BASE.isWater(b); + } + + public static BlockData getAir() { + return BASE.getAir(); + } + + public static Material getMaterialOrNull(String bdx) { + return BASE.getMaterialOrNull(bdx); + } + + public static Material getMaterial(String bdx) { + return BASE.getMaterial(bdx); + } + + public static boolean isSolid(BlockData mat) { + return BASE.isSolid(mat); + } + + public static BlockData getOrNull(String bdxf) { + return BASE.getOrNull(bdxf); + } + + public static BlockData getOrNull(String bdxf, boolean warn) { + return BASE.getOrNull(bdxf, warn); + } + + /** + * Strict lookup: null when nothing claims the key, never an air substitute. Unlike {@link #getOrNull(String)} this + * never reaches the {@link art.arcane.iris.engine.object.IrisCompat} legacy rewrite table, which is a Bukkit-only + * layer and must stay off the generation path. + */ + public static BlockData resolveOrNull(String bdxf) { + return BASE.resolveOrNull(bdxf, false); + } + + public static BlockData resolveOrNull(String bdxf, boolean warn) { + return BASE.resolveOrNull(bdxf, warn); + } + + public static BlockData getNoCompat(String bdxf) { + return BASE.getNoCompat(bdxf); + } + + public static BlockData get(String bdxf) { + return BASE.get(bdxf); + } + + public static boolean isStorage(BlockData mat) { + return BASE.isStorage(mat); + } + + public static boolean isStorageChest(BlockData mat) { + return BASE.isStorageChest(mat); + } + + public static boolean isLit(BlockData mat) { + return BASE.isLit(mat); + } + + public static boolean isUpdatable(BlockData mat) { + return BASE.isUpdatable(mat); + } + + public static boolean isFoliage(Material d) { + return BASE.isFoliage(d); + } + + public static boolean isFoliage(BlockData d) { + return BASE.isFoliage(d); + } + + public static boolean isDecorant(BlockData m) { + return BASE.isDecorant(m); + } + + public static KList get(KList find) { + return BASE.get(find); + } + + public static boolean isFluid(BlockData d) { + return BASE.isFluid(d); + } + + public static boolean isAirOrFluid(BlockData d) { + return BASE.isAirOrFluid(d); + } + + public static boolean isAir(BlockData d) { + return BASE.isAir(d); + } + + public static synchronized String[] getBlockTypes() { + return BASE.getBlockTypes(); + } + + public static synchronized KMap, List> getBlockStates() { + return BASE.getBlockStates(); + } + + public static String[] getItemTypes() { + return BASE.getItemTypes(); + } + + public static boolean isWaterLogged(BlockData b) { + return BASE.isWaterLogged(b); + } + + public static void registerCustomBlockData(String namespace, String key, BlockData blockData) { + BASE.registerCustomBlockData(namespace, key, blockData); + } + + public static boolean isVineBlock(BlockData data) { + return BASE.isVineBlock(data); + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java new file mode 100644 index 000000000..a4631511f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java @@ -0,0 +1,387 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.IrisCustomData; +import org.bukkit.Bukkit; +import org.bukkit.Tag; +import org.bukkit.block.data.BlockData; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Interned Bukkit adapter for a neutral block state backed by BlockData. + */ +public final class BukkitBlockState implements PlatformBlockState { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + // Front cache keyed on the BlockData itself (CraftBlockData equals/hashCode delegate to + // the canonical NMS state): a hit skips getAsString(), which built the full property + // string on EVERY of() call — the dominant cost of the 99.9% hit case. + private static final ConcurrentHashMap DATA_CACHE = new ConcurrentHashMap<>(); + + private final BlockData data; + private final String key; + private final String namespace; + private volatile String materialKey; + private volatile Boolean air; + private volatile Boolean solid; + private volatile Boolean occluding; + private volatile Boolean fluid; + private volatile Boolean water; + private volatile Boolean waterLogged; + private volatile Boolean lit; + private volatile Boolean updatable; + private volatile Boolean foliage; + private volatile Boolean treeBlock; + private volatile Boolean foliagePlantable; + private volatile Boolean decorant; + private volatile Boolean storage; + private volatile Boolean storageChest; + private volatile Boolean ore; + private volatile Boolean deepSlate; + private volatile Boolean vineBlock; + private volatile Boolean tileEntity; + + private BukkitBlockState(BlockData data, String key) { + this.data = data; + this.key = key; + this.namespace = parseNamespace(key); + } + + public static BukkitBlockState of(BlockData data) { + if (data instanceof IrisCustomData custom) { + return new BukkitBlockState(data, custom.getAsString()); + } + BukkitBlockState fast = DATA_CACHE.get(data); + if (fast != null) { + return fast; + } + String key = data.getAsString(); + BukkitBlockState state = CACHE.computeIfAbsent(key, (String k) -> new BukkitBlockState(data, k)); + DATA_CACHE.putIfAbsent(data, state); + return state; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BukkitBlockState state)) { + return false; + } + return data.equals(state.data); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + private static String parseNamespace(String key) { + String base = key; + int bracket = base.indexOf('['); + if (bracket >= 0) { + base = base.substring(0, bracket); + } + int colon = base.indexOf(':'); + return colon >= 0 ? base.substring(0, colon) : "minecraft"; + } + + private static String mergeProperty(String key, String name, String value) { + int bracket = key.indexOf('['); + if (bracket < 0) { + return key + "[" + name + "=" + value + "]"; + } + String base = key.substring(0, bracket); + String body = key.substring(bracket + 1, key.lastIndexOf(']')); + LinkedHashMap properties = new LinkedHashMap<>(); + for (String entry : body.split(",")) { + int equals = entry.indexOf('='); + if (equals < 0) { + continue; + } + properties.put(entry.substring(0, equals).trim(), entry.substring(equals + 1).trim()); + } + properties.put(name, value); + StringBuilder merged = new StringBuilder(base).append('['); + boolean first = true; + for (Map.Entry property : properties.entrySet()) { + if (!first) { + merged.append(','); + } + merged.append(property.getKey()).append('=').append(property.getValue()); + first = false; + } + return merged.append(']').toString(); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public String materialKey() { + String cached = materialKey; + if (cached == null) { + int bracket = key.indexOf('['); + cached = bracket < 0 ? key : key.substring(0, bracket); + materialKey = cached; + } + return cached; + } + + @Override + public boolean isAir() { + Boolean cached = air; + if (cached == null) { + cached = BukkitBlockResolution.isAir(data); + air = cached; + } + return cached; + } + + @Override + public boolean isSolid() { + Boolean cached = solid; + if (cached == null) { + cached = BukkitBlockResolution.isSolid(data); + solid = cached; + } + return cached; + } + + @Override + public boolean isOccluding() { + Boolean cached = occluding; + if (cached == null) { + cached = data.getMaterial().isOccluding(); + occluding = cached; + } + return cached; + } + + @Override + public boolean isCustom() { + return data instanceof IrisCustomData; + } + + @Override + public String deferredPlacementKey() { + return data instanceof IrisCustomData custom ? custom.getCustom().toString() : null; + } + + @Override + public PlatformBlockState placementBaseState() { + return data instanceof IrisCustomData custom ? of(custom.getBase()) : this; + } + + @Override + public boolean isFluid() { + Boolean cached = fluid; + if (cached == null) { + cached = BukkitBlockResolution.isFluid(data); + fluid = cached; + } + return cached; + } + + @Override + public boolean isWater() { + Boolean cached = water; + if (cached == null) { + cached = BukkitBlockResolution.isWater(data); + water = cached; + } + return cached; + } + + @Override + public boolean isWaterLogged() { + Boolean cached = waterLogged; + if (cached == null) { + cached = BukkitBlockResolution.isWaterLogged(data); + waterLogged = cached; + } + return cached; + } + + @Override + public boolean isLit() { + Boolean cached = lit; + if (cached == null) { + cached = BukkitBlockResolution.isLit(data); + lit = cached; + } + return cached; + } + + @Override + public boolean isUpdatable() { + Boolean cached = updatable; + if (cached == null) { + cached = BukkitBlockResolution.isUpdatable(data); + updatable = cached; + } + return cached; + } + + @Override + public boolean isFoliage() { + Boolean cached = foliage; + if (cached == null) { + cached = BukkitBlockResolution.isFoliage(data); + foliage = cached; + } + return cached; + } + + @Override + public boolean isTreeBlock() { + Boolean cached = treeBlock; + if (cached == null) { + cached = Tag.LOGS.isTagged(data.getMaterial()) || Tag.LEAVES.isTagged(data.getMaterial()); + treeBlock = cached; + } + return cached; + } + + @Override + public boolean isFoliagePlantable() { + Boolean cached = foliagePlantable; + if (cached == null) { + cached = BukkitBlockResolution.isFoliagePlantable(data); + foliagePlantable = cached; + } + return cached; + } + + @Override + public boolean isDecorant() { + Boolean cached = decorant; + if (cached == null) { + cached = BukkitBlockResolution.isDecorant(data); + decorant = cached; + } + return cached; + } + + @Override + public boolean isStorage() { + Boolean cached = storage; + if (cached == null) { + cached = BukkitBlockResolution.isStorage(data); + storage = cached; + } + return cached; + } + + @Override + public boolean isStorageChest() { + Boolean cached = storageChest; + if (cached == null) { + cached = BukkitBlockResolution.isStorageChest(data); + storageChest = cached; + } + return cached; + } + + @Override + public boolean isOre() { + Boolean cached = ore; + if (cached == null) { + cached = BukkitBlockResolution.isOre(data); + ore = cached; + } + return cached; + } + + @Override + public boolean isDeepSlate() { + Boolean cached = deepSlate; + if (cached == null) { + cached = BukkitBlockResolution.isDeepSlate(data); + deepSlate = cached; + } + return cached; + } + + @Override + public boolean isVineBlock() { + Boolean cached = vineBlock; + if (cached == null) { + cached = BukkitBlockResolution.isVineBlock(data); + vineBlock = cached; + } + return cached; + } + + @Override + public boolean canPlaceOnto(PlatformBlockState onto) { + return BukkitBlockResolution.canPlaceOnto(data.getMaterial(), ((BlockData) onto.nativeHandle()).getMaterial()); + } + + @Override + public boolean matches(PlatformBlockState state) { + return data.matches((BlockData) state.nativeHandle()); + } + + @Override + public boolean hasTileEntity() { + Boolean cached = tileEntity; + if (cached == null) { + cached = INMS.get().hasTile(data.getMaterial()); + tileEntity = cached; + } + return cached; + } + + @Override + public boolean isAirOrFluid() { + return isAir() || isFluid(); + } + + @Override + public PlatformBlockState withProperty(String name, String value) { + String merged = mergeProperty(key, name, value); + BlockData resolved = Bukkit.createBlockData(merged); + // Re-attach the custom identity (as the proxy's own merge/clone cases do): the key of + // a custom state is the BASE block's string, so rebuilding from it alone silently + // downgraded custom blocks to vanilla on e.g. auto-waterlogging. + if (data instanceof IrisCustomData custom) { + return of(IrisCustomData.of(resolved, custom.getCustom())); + } + return of(resolved); + } + + @Override + public Object nativeHandle() { + return data; + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitEntityType.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitEntityType.java new file mode 100644 index 000000000..b08080711 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitEntityType.java @@ -0,0 +1,70 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.spi.PlatformEntityType; +import org.bukkit.entity.EntityType; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Interned Bukkit adapter for a neutral entity type handle. + */ +public final class BukkitEntityType implements PlatformEntityType { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + + private final EntityType type; + private final String key; + private final String namespace; + private final String spawnCategory; + + private BukkitEntityType(EntityType type, String key) { + this.type = type; + this.key = key; + int colon = key.indexOf(':'); + this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft"; + this.spawnCategory = INMS.get().getEntitySpawnCategory(key); + } + + public static BukkitEntityType of(EntityType type) { + String key = type.getKey().toString(); + return CACHE.computeIfAbsent(key, (String k) -> new BukkitEntityType(type, k)); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public String spawnCategory() { + return spawnCategory; + } + + @Override + public Object nativeHandle() { + return type; + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitEnvironment.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitEnvironment.java new file mode 100644 index 000000000..602e0cfca --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitEnvironment.java @@ -0,0 +1,39 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.engine.object.IrisEnvironment; +import org.bukkit.World; + +public final class BukkitEnvironment { + private BukkitEnvironment() { + } + + public static World.Environment from(IrisEnvironment environment) { + if (environment == null) { + return World.Environment.NORMAL; + } + return switch (environment) { + case NORMAL -> World.Environment.NORMAL; + case NETHER -> World.Environment.NETHER; + case THE_END -> World.Environment.THE_END; + case CUSTOM -> World.Environment.CUSTOM; + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitItem.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitItem.java new file mode 100644 index 000000000..f98b0ed1b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitItem.java @@ -0,0 +1,62 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.spi.PlatformItem; +import org.bukkit.Material; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Interned Bukkit adapter for a neutral item handle backed by Material. + */ +public final class BukkitItem implements PlatformItem { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + + private final Material material; + private final String key; + private final String namespace; + + private BukkitItem(Material material, String key) { + this.material = material; + this.key = key; + int colon = key.indexOf(':'); + this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft"; + } + + public static BukkitItem of(Material material) { + String key = material.getKey().toString(); + return CACHE.computeIfAbsent(key, (String k) -> new BukkitItem(material, k)); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public Object nativeHandle() { + return material; + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java new file mode 100644 index 000000000..ca9587a30 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java @@ -0,0 +1,362 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.MinecraftVersion; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.LogLevel; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBiomeWriter; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.spi.PlatformScheduler; +import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.iris.spi.PlatformWorld; +import art.arcane.iris.util.common.misc.Bindings; +import art.arcane.iris.util.common.plugin.VolmitPlugin; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.hud.HudActionBar; +import art.arcane.volmlib.util.hud.HudBossBarLane; +import art.arcane.volmlib.util.math.Vector3d; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.bukkit.util.Vector; + +import java.io.File; +import java.util.function.Supplier; + +/** + * Bukkit implementation of the root Iris platform service. + */ +public final class BukkitPlatform implements IrisPlatform { + private static volatile Plugin PLUGIN; + private static volatile Bindings.Adventure AUDIENCES; + private static volatile HudActionBar HUD_BAR; + private static volatile HudBossBarLane HUD_LANES; + private static volatile Supplier CONSOLE; + private static volatile HostBridge BRIDGE; + + public record HostBridge( + java.util.function.BiConsumer logSink, + java.util.function.Consumer msgSink, + java.util.function.Consumer errorSink, + java.util.function.Consumer eventSink, + Supplier dataFolder, + java.util.function.Function dataFile, + Supplier pluginJar, + java.util.function.IntSupplier irisVersion, + java.util.function.IntSupplier minecraftVersion) { + } + + public static void hostBridge(HostBridge bridge) { + BRIDGE = bridge; + } + + private static HostBridge bridge() { + HostBridge bridge = BRIDGE; + if (bridge == null) { + throw new IllegalStateException("No Iris host bridge is hosted"); + } + return bridge; + } + + private final BukkitRegistries registries = new BukkitRegistries(); + private final BukkitScheduler scheduler = new BukkitScheduler(); + private final BukkitStructureHooks structureHooks = new BukkitStructureHooks(); + private final BukkitBiomeWriter biomeWriter = new BukkitBiomeWriter(); + + public static void hostPlugin(Plugin plugin) { + PLUGIN = plugin; + } + + public static Plugin plugin() { + Plugin plugin = PLUGIN; + if (plugin == null) { + throw new IllegalStateException("No Iris plugin is hosted"); + } + return plugin; + } + + public static boolean hasPlugin() { + return PLUGIN != null; + } + + public static VolmitPlugin volmitPlugin() { + Plugin plugin = plugin(); + if (plugin instanceof VolmitPlugin host) { + return host; + } + throw new IllegalStateException("Hosted Iris plugin is not a VolmitPlugin"); + } + + public static void hostAudiences(Bindings.Adventure adventure) { + AUDIENCES = adventure; + } + + public static Bindings.Adventure audiences() { + Bindings.Adventure adventure = AUDIENCES; + if (adventure == null) { + throw new IllegalStateException("No Iris adventure audiences are hosted"); + } + return adventure; + } + + public static void hostHud(HudActionBar hudBar, HudBossBarLane hudLanes) { + HUD_BAR = hudBar; + HUD_LANES = hudLanes; + } + + public static boolean hasHud() { + return HUD_BAR != null && HUD_LANES != null; + } + + public static HudActionBar hudBar() { + HudActionBar hudBar = HUD_BAR; + if (hudBar == null) { + throw new IllegalStateException("No Iris HUD action bar is hosted"); + } + return hudBar; + } + + public static HudBossBarLane hudLanes() { + HudBossBarLane hudLanes = HUD_LANES; + if (hudLanes == null) { + throw new IllegalStateException("No Iris HUD boss bar lanes are hosted"); + } + return hudLanes; + } + + public static void showProgressLane(Player player, String laneId, String title, double progress, long staleMillis) { + if (!IrisSettings.get().getGeneral().isProgressBossBar()) { + return; + } + hudLanes().show(player, laneId, title, progress, BarColor.BLUE, BarStyle.SOLID, staleMillis); + } + + public static void hostConsoleSender(Supplier supplier) { + CONSOLE = supplier; + } + + public static VolmitSender console() { + Supplier supplier = CONSOLE; + if (supplier == null) { + throw new IllegalStateException("No Iris console sender is hosted"); + } + return supplier.get(); + } + + public static World unwrapWorld(PlatformWorld world) { + return (World) world.nativeHandle(); + } + + public static PlatformBiome wrapBiome(Object biome) { + return BukkitBiome.of((Biome) biome); + } + + public static Class classifyMantleValue(Object value) { + if (value instanceof World) { + return World.class; + } + + if (value instanceof BlockData) { + return BlockData.class; + } + + if (value instanceof Entity) { + return Entity.class; + } + + return value.getClass(); + } + + public static void unregisterListener(Object candidate) { + if (candidate instanceof Listener listener) { + volmitPlugin().unregisterListener(listener); + } + } + + public static IrisPosition positionOf(Location location) { + return new IrisPosition(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + } + + public static IrisPosition positionOf(Vector vector) { + return new IrisPosition(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ()); + } + + public static Location toLocation(IrisPosition position, World world) { + return new Location(world, position.getX(), position.getY(), position.getZ()); + } + + public static Entity spawnEntity(Location at, EntityType type, CreatureSpawnEvent.SpawnReason reason) { + return INMS.get().spawnEntity(at, type, reason); + } + + public static Vector3d entityBoundingBox(EntityType type) { + return INMS.get().getBoundingbox(type); + } + + public static boolean hasTile(Material material) { + return INMS.get().hasTile(material); + } + + public static KMap serializeTile(Location location) { + return INMS.get().serializeTile(location); + } + + public static void deserializeTile(KMap properties, Location location) { + INMS.get().deserializeTile(properties, location); + } + + public static ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) { + return INMS.get().applyCustomNbt(itemStack, customNbt); + } + + public static java.util.concurrent.CompletableFuture teleportAsync(Entity entity, Location destination) { + return io.papermc.lib.PaperLib.teleportAsync(entity, destination); + } + + public static boolean isPaperServer() { + return io.papermc.lib.PaperLib.isPaper(); + } + + public static java.util.concurrent.CompletableFuture chunkAtAsync(World world, int x, int z, boolean generate) { + return io.papermc.lib.PaperLib.getChunkAtAsync(world, x, z, generate); + } + + @Override + public String platformName() { + return "bukkit"; + } + + @Override + public String minecraftVersion() { + return minecraftVersion(Bukkit.getServer()); + } + + static String minecraftVersion(Server server) { + MinecraftVersion detected = MinecraftVersion.detect(server); + return detected == null ? null : detected.value(); + } + + @Override + public PlatformRegistries registries() { + return registries; + } + + @Override + public PlatformScheduler scheduler() { + return scheduler; + } + + @Override + public PlatformStructureHooks structureHooks() { + return structureHooks; + } + + @Override + public PlatformBiomeWriter biomeWriter() { + return biomeWriter; + } + + @Override + public File dataFolder() { + return bridge().dataFolder().get(); + } + + @Override + public File dataFile(String... path) { + return bridge().dataFile().apply(path); + } + + @Override + public File pluginJar() { + return bridge().pluginJar().get(); + } + + @Override + public int irisVersionNumber() { + return bridge().irisVersion().getAsInt(); + } + + @Override + public int minecraftVersionNumber() { + return bridge().minecraftVersion().getAsInt(); + } + + @Override + public void callEvent(Object event) { + bridge().eventSink().accept(event); + } + + @Override + public void dispatchConsoleCommand(String command) { + Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), command); + } + + @Override + public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) { + if (world == null || entityKey == null || !(world.nativeHandle() instanceof World bukkitWorld)) { + return false; + } + NamespacedKey namespacedKey = NamespacedKey.fromString(entityKey); + if (namespacedKey == null) { + return false; + } + EntityType type = Registry.ENTITY_TYPE.get(namespacedKey); + if (type == null) { + return false; + } + Entity entity = spawnEntity(new Location(bukkitWorld, x, y, z), type, CreatureSpawnEvent.SpawnReason.COMMAND); + return entity != null; + } + + @Override + public void log(LogLevel level, String message) { + bridge().logSink().accept(level, message); + } + + @Override + public void msg(String message) { + bridge().msgSink().accept(message); + } + + @Override + public void reportError(Throwable error) { + bridge().errorSink().accept(error); + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitRegistries.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitRegistries.java new file mode 100644 index 000000000..f12a978be --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitRegistries.java @@ -0,0 +1,254 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.link.data.DataType; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockProperty; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformEntityType; +import art.arcane.iris.spi.PlatformItem; +import art.arcane.iris.spi.PlatformNumericRange; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.util.common.reflect.KeyedType; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.EntityType; +import org.bukkit.loot.LootTables; +import org.bukkit.potion.PotionEffectType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Bukkit adapter resolving namespaced keys against the live Bukkit registries. + */ +public final class BukkitRegistries implements PlatformRegistries { + @Override + public PlatformBlockState block(String key) { + BlockData data = BukkitBlockResolution.get(key); + return data == null ? null : BukkitBlockState.of(data); + } + + // blockOrNull must stay null-honest to match the modded adapters, so it uses the strict lookup rather than + // BukkitBlockResolution.getOrNull, which substitutes air for an unregistered key and feeds the Bukkit-only + // IrisCompat rewrite table used by block(). + @Override + public PlatformBlockState blockOrNull(String key) { + BlockData data = BukkitBlockResolution.resolveOrNull(key); + return data == null ? null : BukkitBlockState.of(data); + } + + @Override + public PlatformBlockState blockOrNull(String key, boolean warn) { + BlockData data = BukkitBlockResolution.resolveOrNull(key, warn); + return data == null ? null : BukkitBlockState.of(data); + } + + @Override + public PlatformBlockState air() { + return BukkitBlockState.of(BukkitBlockResolution.getAir()); + } + + @Override + public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) { + return BukkitBlockState.of(BukkitBlockResolution.toDeepSlateOre((BlockData) block.nativeHandle(), (BlockData) ore.nativeHandle())); + } + + @Override + public PlatformBiome biome(String key) { + NamespacedKey namespacedKey = NamespacedKey.fromString(key); + if (namespacedKey == null) { + return null; + } + Biome biome = Registry.BIOME.get(namespacedKey); + return biome == null ? null : BukkitBiome.of(biome); + } + + @Override + public PlatformItem item(String key) { + Material material = Material.matchMaterial(key); + return material == null ? null : BukkitItem.of(material); + } + + @Override + public PlatformEntityType entity(String key) { + NamespacedKey namespacedKey = NamespacedKey.fromString(key); + if (namespacedKey == null) { + return null; + } + EntityType type = Registry.ENTITY_TYPE.get(namespacedKey); + return type == null ? null : BukkitEntityType.of(type); + } + + @Override + public List blockKeys() { + List keys = new ArrayList<>(); + for (Material material : Registry.MATERIAL) { + if (material.isBlock()) { + keys.add(material.getKey().toString()); + } + } + return keys; + } + + @Override + public List biomeKeys() { + List keys = new ArrayList<>(); + for (Biome biome : Registry.BIOME) { + keys.add(BukkitBiome.of(biome).key()); + } + return keys; + } + + @Override + public List structureKeys() { + return new ArrayList<>(INMS.get().getStructureKeys()); + } + + @Override + public List itemKeys() { + List keys = new ArrayList<>(); + for (Material material : Registry.MATERIAL) { + if (material.isItem()) { + keys.add(material.getKey().toString()); + } + } + ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class); + if (external != null) { + for (Identifier identifier : external.getAllIdentifiers(DataType.ITEM)) { + keys.add(identifier.toString()); + } + } + return keys; + } + + @Override + public List entityKeys() { + List keys = new ArrayList<>(); + for (EntityType type : Registry.ENTITY_TYPE) { + NamespacedKey key = KeyedType.getKey(type); + if (key != null) { + keys.add(key.toString()); + } + } + return keys; + } + + @Override + public List blockTypeKeys() { + return new ArrayList<>(Arrays.asList(BukkitBlockResolution.getBlockTypes())); + } + + @Override + public List specialEntityKeys() { + ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class); + if (external == null) { + return List.of(); + } + List keys = new ArrayList<>(); + for (Identifier identifier : external.getAllIdentifiers(DataType.ENTITY)) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List enchantmentKeys() { + List keys = new ArrayList<>(); + for (Enchantment enchantment : Registry.ENCHANTMENT) { + NamespacedKey key = KeyedType.getKey(enchantment); + if (key != null) { + keys.add(key.toString()); + } + } + return keys; + } + + @Override + public List potionEffectKeys() { + List keys = new ArrayList<>(); + for (PotionEffectType effect : Registry.EFFECT) { + NamespacedKey key = KeyedType.getKey(effect); + if (key != null) { + keys.add(key.toString()); + } + } + return keys; + } + + @Override + public List lootTableKeys() { + List keys = new ArrayList<>(); + for (LootTables table : LootTables.values()) { + keys.add(table.getKey().toString()); + } + return keys; + } + + @Override + public Map> blockStateProperties() { + Map> properties = new LinkedHashMap<>(); + BukkitBlockResolution.getBlockStates().forEach((blocks, blockProperties) -> { + if (blocks.isEmpty()) { + return; + } + List converted = new ArrayList<>(blockProperties.size()); + for (BlockProperty blockProperty : blockProperties) { + converted.add(toPlatformProperty(blockProperty)); + } + List shared = List.copyOf(converted); + for (String block : blocks) { + properties.put(block, shared); + } + }); + return properties; + } + + private static PlatformBlockProperty toPlatformProperty(BlockProperty property) { + JSONObject json = property.buildJson(); + Object defaultValue = json.has("default") ? json.get("default") : null; + List allowedValues = new ArrayList<>(); + if (json.has("enum")) { + JSONArray values = json.getJSONArray("enum"); + for (int index = 0; index < values.length(); index++) { + allowedValues.add(values.get(index)); + } + } + PlatformNumericRange range = null; + if (json.has("minimum")) { + range = new PlatformNumericRange(json.getDouble("minimum"), json.getDouble("maximum"), json.getBoolean("exclusiveMinimum"), json.getBoolean("exclusiveMaximum")); + } + return new PlatformBlockProperty(property.name(), json.getString("type"), defaultValue, List.copyOf(allowedValues), range); + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitScheduler.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitScheduler.java new file mode 100644 index 000000000..70766410f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitScheduler.java @@ -0,0 +1,60 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.spi.PlatformScheduler; +import art.arcane.iris.spi.PlatformWorld; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.World; + +/** + * Bukkit adapter dispatching platform tasks through the Folia-aware J scheduler. + */ +public final class BukkitScheduler implements PlatformScheduler { + @Override + public void global(Runnable task) { + J.s(task); + } + + @Override + public void region(PlatformWorld world, int chunkX, int chunkZ, Runnable task) { + World bukkitWorld = (World) world.nativeHandle(); + if (!J.runRegion(bukkitWorld, chunkX, chunkZ, task)) { + J.s(task); + } + } + + @Override + public void async(Runnable task) { + J.a(task); + } + + @Override + public void laterGlobal(Runnable task, int ticks) { + J.s(task, ticks); + } + + @Override + public void laterRegion(PlatformWorld world, int chunkX, int chunkZ, Runnable task, int ticks) { + World bukkitWorld = (World) world.nativeHandle(); + if (!J.runRegion(bukkitWorld, chunkX, chunkZ, task, ticks)) { + J.s(task, ticks); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitStructureHooks.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitStructureHooks.java new file mode 100644 index 000000000..e8f255c88 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitStructureHooks.java @@ -0,0 +1,111 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.iris.spi.PlatformWorld; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * Bukkit adapter for structure and feature hooks backed by the active NMS binding. + */ +public final class BukkitStructureHooks implements PlatformStructureHooks { + @Override + public List structureKeys() { + return new ArrayList<>(INMS.get().getStructureKeys()); + } + + @Override + public List jigsawStructureKeys() { + return new ArrayList<>(INMS.get().getJigsawStructureKeys()); + } + + @Override + public List templatePoolKeys() { + return new ArrayList<>(INMS.get().getTemplatePoolKeys()); + } + + @Override + public JigsawSourceMetadata jigsawSourceMetadata(String structureKey) { + return INMS.get().getJigsawSourceMetadata(structureKey); + } + + @Override + public int templatePoolHorizontalSpan(String templatePoolKey) { + return INMS.get().getTemplatePoolHorizontalSpan(templatePoolKey); + } + + @Override + public int jigsawStartPoolHorizontalSpan(String structureKey, String templatePoolKey) { + return INMS.get().getJigsawStartPoolHorizontalSpan(structureKey, templatePoolKey); + } + + @Override + public List structureSetKeys() { + return new ArrayList<>(INMS.get().getStructureSetKeys()); + } + + @Override + public List structureBiomeKeys(String structureKey) { + return new ArrayList<>(INMS.get().getStructureBiomeKeys(structureKey)); + } + + @Override + public List objectFeatureKeys() { + return new ArrayList<>(INMS.get().getObjectFeatureKeys()); + } + + @Override + public List reachableStructureKeys(PlatformWorld world) { + return new ArrayList<>(INMS.get().getReachableStructureKeys(BukkitPlatform.unwrapWorld(world))); + } + + @Override + public List possibleBiomeKeys(PlatformWorld world) { + return new ArrayList<>(INMS.get().getPossibleBiomeKeys(BukkitPlatform.unwrapWorld(world))); + } + + @Override + public boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed) { + return INMS.get().placeFeature(BukkitPlatform.unwrapWorld(world), x, y, z, featureKey, seed); + } + + @Override + public int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) { + return INMS.get().placeStructure(BukkitPlatform.unwrapWorld(world), chunkX, chunkZ, structureKey, seed, maxSpan); + } + + @Override + public boolean supportsStructurePlacement() { + Class type = INMS.get().getClass(); + while (type != null && type != Object.class) { + for (Method method : type.getDeclaredMethods()) { + if ("placeStructure".equals(method.getName())) { + return true; + } + } + type = type.getSuperclass(); + } + return false; + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitWorld.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitWorld.java new file mode 100644 index 000000000..0a570ae54 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitWorld.java @@ -0,0 +1,96 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformWorld; +import org.bukkit.World; +import org.bukkit.block.data.BlockData; + +/** + * Bukkit adapter for a neutral world view backed by org.bukkit.World. + */ +public final class BukkitWorld implements PlatformWorld { + private final World world; + + public BukkitWorld(World world) { + this.world = world; + } + + @Override + public String name() { + return world.getName(); + } + + @Override + public long seed() { + return world.getSeed(); + } + + @Override + public int minHeight() { + return world.getMinHeight(); + } + + @Override + public int maxHeight() { + return world.getMaxHeight(); + } + + @Override + public PlatformBlockState getBlock(int x, int y, int z) { + return BukkitBlockState.of(world.getBlockAt(x, y, z).getBlockData()); + } + + @Override + public void setBlock(int x, int y, int z, PlatformBlockState block, int flags) { + world.getBlockAt(x, y, z).setBlockData((BlockData) block.nativeHandle(), (flags & 1) != 0); + } + + @Override + public PlatformBiome getBiome(int x, int y, int z) { + return BukkitBiome.of(world.getBiome(x, y, z)); + } + + @Override + public boolean isChunkLoaded(int chunkX, int chunkZ) { + return world.isChunkLoaded(chunkX, chunkZ); + } + + @Override + public long getTime() { + return world.getTime(); + } + + @Override + public boolean isStorming() { + return world.hasStorm(); + } + + @Override + public boolean isThundering() { + return world.isThundering(); + } + + @Override + public Object nativeHandle() { + return world; + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitWorldBinding.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitWorldBinding.java new file mode 100644 index 000000000..c874849bc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitWorldBinding.java @@ -0,0 +1,92 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.platform.bukkit; + +import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.generator.WorldInfo; + +import java.util.Collection; +import java.util.List; + +public final class BukkitWorldBinding { + private BukkitWorldBinding() { + } + + public static IrisWorld bind(IrisWorld target, WorldInfo worldInfo) { + target.platformIdentity(WorldIdentity.key(worldInfo).toString()) + .name(worldInfo.getName()) + .worldFolder(IrisWorldStorage.dimensionRoot(worldInfo)) + .minHeight(worldInfo.getMinHeight()) + .maxHeight(worldInfo.getMaxHeight()); + if (worldInfo instanceof World world) { + target.platformWorld(new BukkitWorld(world)); + } + return target; + } + + public static boolean tryBind(IrisWorld target) { + if (target.hasPlatformWorld()) { + return true; + } + NamespacedKey key = NamespacedKey.fromString(target.identity()); + if (key == null) { + return false; + } + World world = WorldIdentity.resolve(key).orElse(null); + if (world == null) { + return false; + } + bind(target, world); + return true; + } + + public static World world(IrisWorld target) { + if (target == null || !target.hasPlatformWorld()) { + return null; + } + return BukkitPlatform.unwrapWorld(target.platformWorld()); + } + + public static List players(IrisWorld target) { + World world = world(target); + return world == null ? List.of() : world.getPlayers(); + } + + public static Collection entities(IrisWorld target, Class type) { + World world = world(target); + return world == null ? List.of() : world.getEntitiesByClass(type); + } + + public static Location spawnLocation(IrisWorld target) { + World world = world(target); + if (world != null) { + return world.getSpawnLocation(); + } + IrisLogging.error("This world is not real yet, cannot get spawn location! HEADLESS!"); + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/board/BoardSettings.java b/core/src/main/java/art/arcane/iris/util/common/board/BoardSettings.java new file mode 100644 index 000000000..07d27d748 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/board/BoardSettings.java @@ -0,0 +1,51 @@ +package art.arcane.iris.util.common.board; + +import art.arcane.volmlib.util.board.BoardProvider; + +public class BoardSettings { + private final BoardProvider boardProvider; + private final ScoreDirection scoreDirection; + + private BoardSettings(BoardProvider boardProvider, ScoreDirection scoreDirection) { + this.boardProvider = boardProvider; + this.scoreDirection = scoreDirection; + } + + public BoardProvider getBoardProvider() { + return boardProvider; + } + + public ScoreDirection getScoreDirection() { + return scoreDirection; + } + + public art.arcane.volmlib.util.board.BoardSettings toShared() { + return art.arcane.volmlib.util.board.BoardSettings.builder() + .boardProvider(boardProvider) + .scoreDirection(scoreDirection.toShared()) + .build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private BoardProvider boardProvider; + private ScoreDirection scoreDirection; + + public Builder boardProvider(BoardProvider boardProvider) { + this.boardProvider = boardProvider; + return this; + } + + public Builder scoreDirection(ScoreDirection scoreDirection) { + this.scoreDirection = scoreDirection; + return this; + } + + public BoardSettings build() { + return new BoardSettings(boardProvider, scoreDirection); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/board/ScoreDirection.java b/core/src/main/java/art/arcane/iris/util/common/board/ScoreDirection.java new file mode 100644 index 000000000..20119cde6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/board/ScoreDirection.java @@ -0,0 +1,16 @@ +package art.arcane.iris.util.common.board; + +public enum ScoreDirection { + UP, + DOWN; + + public art.arcane.volmlib.util.board.ScoreDirection toShared() { + return this == UP + ? art.arcane.volmlib.util.board.ScoreDirection.UP + : art.arcane.volmlib.util.board.ScoreDirection.DOWN; + } + + public static ScoreDirection fromShared(art.arcane.volmlib.util.board.ScoreDirection direction) { + return direction == art.arcane.volmlib.util.board.ScoreDirection.UP ? UP : DOWN; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/B.java b/core/src/main/java/art/arcane/iris/util/common/data/B.java new file mode 100644 index 000000000..6959b57c8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/data/B.java @@ -0,0 +1,129 @@ +package art.arcane.iris.util.common.data; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.volmlib.util.collection.KList; + +public class B { + private static volatile PlatformRegistries registries; + + public static PlatformBlockState getState(String bdxf) { + return registries().block(bdxf); + } + + public static PlatformBlockState getStateOrNull(String bdxf) { + return registries().blockOrNull(bdxf); + } + + public static PlatformBlockState getStateOrNull(String bdxf, boolean warn) { + return registries().blockOrNull(bdxf, warn); + } + + public static KList getStates(KList find) { + KList states = new KList<>(find.size()); + for (String key : find) { + states.add(getState(key)); + } + return states; + } + + public static PlatformBlockState getAirState() { + return registries().air(); + } + + public static PlatformBlockState toDeepSlateOre(PlatformBlockState block, PlatformBlockState ore) { + return registries().deepSlateOre(block, ore); + } + + public static boolean isAir(PlatformBlockState state) { + return state == null || state.isAir(); + } + + public static boolean isSolid(PlatformBlockState state) { + return state != null && state.isSolid(); + } + + public static boolean isOccluding(PlatformBlockState state) { + return state != null && state.isOccluding(); + } + + public static boolean isFluid(PlatformBlockState state) { + return state != null && state.isFluid(); + } + + public static boolean isAirOrFluid(PlatformBlockState state) { + return state == null || state.isAirOrFluid(); + } + + public static boolean isWater(PlatformBlockState state) { + return state != null && state.isWater(); + } + + public static boolean isWaterLogged(PlatformBlockState state) { + return state != null && state.isWaterLogged(); + } + + public static boolean isLit(PlatformBlockState state) { + return state != null && state.isLit(); + } + + public static boolean isUpdatable(PlatformBlockState state) { + return state != null && state.isUpdatable(); + } + + public static boolean isFoliage(PlatformBlockState state) { + return state != null && state.isFoliage(); + } + + public static boolean isTreeBlock(PlatformBlockState state) { + return state != null && state.isTreeBlock(); + } + + public static boolean isFoliagePlantable(PlatformBlockState state) { + return state != null && state.isFoliagePlantable(); + } + + public static boolean isDecorant(PlatformBlockState state) { + return state != null && state.isDecorant(); + } + + public static boolean isStorage(PlatformBlockState state) { + return state != null && state.isStorage(); + } + + public static boolean isStorageChest(PlatformBlockState state) { + return state != null && state.isStorageChest(); + } + + public static boolean isOre(PlatformBlockState state) { + return state != null && state.isOre(); + } + + public static boolean isDeepSlate(PlatformBlockState state) { + return state != null && state.isDeepSlate(); + } + + public static boolean isVineBlock(PlatformBlockState state) { + return state != null && state.isVineBlock(); + } + + public static boolean canPlaceOnto(PlatformBlockState mat, PlatformBlockState onto) { + return mat != null && onto != null && mat.canPlaceOnto(onto); + } + + public static boolean matches(PlatformBlockState filter, PlatformBlockState state) { + return filter != null && state != null && filter.matches(state); + } + + private static PlatformRegistries registries() { + PlatformRegistries cached = registries; + if (cached != null) { + return cached; + } + + PlatformRegistries resolved = IrisPlatforms.get().registries(); + registries = resolved; + return resolved; + } +} diff --git a/core/src/main/java/com/volmit/iris/util/data/DataProvider.java b/core/src/main/java/art/arcane/iris/util/common/data/DataProvider.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/data/DataProvider.java rename to core/src/main/java/art/arcane/iris/util/common/data/DataProvider.java index cf1146e51..5575c8157 100644 --- a/core/src/main/java/com/volmit/iris/util/data/DataProvider.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/DataProvider.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.data; +package art.arcane.iris.util.common.data; -import com.volmit.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisData; public interface DataProvider { IrisData getData(); diff --git a/core/src/main/java/com/volmit/iris/util/data/IrisCustomData.java b/core/src/main/java/art/arcane/iris/util/common/data/IrisCustomData.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/data/IrisCustomData.java rename to core/src/main/java/art/arcane/iris/util/common/data/IrisCustomData.java index 062cb50a1..67f13c6ef 100644 --- a/core/src/main/java/com/volmit/iris/util/data/IrisCustomData.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/IrisCustomData.java @@ -1,14 +1,16 @@ -package com.volmit.iris.util.data; +package art.arcane.iris.util.common.data; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.util.collection.KMap; +import art.arcane.iris.core.link.Identifier; +import art.arcane.volmlib.util.collection.KMap; import lombok.NonNull; import org.bukkit.block.data.BlockData; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Proxy; -import java.util.*; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; public interface IrisCustomData extends BlockData { @NonNull BlockData getBase(); diff --git a/core/src/main/java/art/arcane/iris/util/common/data/VectorMap.java b/core/src/main/java/art/arcane/iris/util/common/data/VectorMap.java new file mode 100644 index 000000000..770f8753a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/data/VectorMap.java @@ -0,0 +1,331 @@ +package art.arcane.iris.util.common.data; + +import art.arcane.volmlib.util.collection.KMap; +import lombok.NonNull; +import art.arcane.iris.util.common.math.IrisBlockVector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.BiConsumer; +import java.util.function.Function; + +public class VectorMap implements Iterable> { + private final Map> map = new KMap<>(); + private transient volatile long modificationRevision; + + public long modificationRevision() { + return modificationRevision; + } + + public int size() { + return map.values().stream().mapToInt(Map::size).sum(); + } + + public boolean isEmpty() { + return map.values().stream().allMatch(Map::isEmpty); + } + + public boolean containsKey(@NonNull IrisBlockVector vector) { + if (map.isEmpty()) return false; + var chunk = map.get(chunk(vector)); + return chunk != null && chunk.containsKey(relative(vector)); + } + + public boolean containsValue(@NonNull T value) { + return map.values().stream().anyMatch(m -> m.containsValue(value)); + } + + public @Nullable T get(@NonNull IrisBlockVector vector) { + if (map.isEmpty()) return null; + var chunk = map.get(chunk(vector)); + return chunk == null ? null : chunk.get(relative(vector)); + } + + public @Nullable T put(@NonNull IrisBlockVector vector, @NonNull T value) { + T previous = map.computeIfAbsent(chunk(vector), k -> new KMap<>()) + .put(relative(vector), value); + modificationRevision++; + return previous; + } + + public @Nullable T computeIfAbsent(@NonNull IrisBlockVector vector, @NonNull Function<@NonNull IrisBlockVector, @NonNull T> mappingFunction) { + boolean[] inserted = new boolean[1]; + T value = map.computeIfAbsent(chunk(vector), key -> new KMap<>()) + .computeIfAbsent(relative(vector), key -> { + inserted[0] = true; + return mappingFunction.apply(vector); + }); + if (inserted[0]) { + modificationRevision++; + } + return value; + } + + @SuppressWarnings("unchecked") + public @Nullable T remove(@NonNull IrisBlockVector vector) { + if (map.isEmpty()) return null; + Key relative = relative(vector); + Object[] removed = new Object[1]; + + // computeIfPresent so the emptied bucket is pruned atomically against a concurrent put. + map.computeIfPresent(chunk(vector), (key, chunk) -> { + removed[0] = chunk.remove(relative); + return chunk.isEmpty() ? null : chunk; + }); + + if (removed[0] != null) { + modificationRevision++; + } + + return (T) removed[0]; + } + + public void putAll(@NonNull VectorMap map) { + map.forEach(this::put); + } + + public void clear() { + if (!map.isEmpty()) { + modificationRevision++; + } + map.clear(); + } + + public void forEach(@NonNull BiConsumer<@NonNull IrisBlockVector, @NonNull T> consumer) { + map.forEach((chunk, values) -> { + int rX = chunk.x << 10; + int rY = chunk.y << 10; + int rZ = chunk.z << 10; + + values.forEach((relative, value) -> consumer.accept( + relative.resolve(rX, rY, rZ), + value + )); + }); + } + + private static Key chunk(IrisBlockVector vector) { + return new Key(vector.getBlockX() >> 10, vector.getBlockY() >> 10, vector.getBlockZ() >> 10); + } + + private static Key relative(IrisBlockVector vector) { + return new Key(vector.getBlockX() & 0x3FF, vector.getBlockY() & 0x3FF, vector.getBlockZ() & 0x3FF); + } + + @Override + public @NotNull EntryIterator iterator() { + return new EntryIterator(); + } + + /** + * Allocation-free entry walk. {@link EntryIterator} allocates a resolved vector plus a Map.Entry per element; + * the cursor resolves into one reused vector instead. Only valid for callers that do not retain the vector + * returned by {@link Cursor#key()} beyond the current step - clone it if it must outlive the next + * {@link Cursor#next()}. + */ + public @NotNull Cursor cursor() { + return new Cursor(); + } + + public @NotNull KeyIterator keys() { + return new KeyIterator(); + } + + public @NotNull ValueIterator values() { + return new ValueIterator(); + } + + public final class Cursor { + private final Iterator>> chunkIterator = map.entrySet().iterator(); + private final IrisBlockVector position = new IrisBlockVector(0, 0, 0); + private Iterator> relativeIterator; + private int rX, rY, rZ; + private T value; + + public boolean next() { + while (relativeIterator == null || !relativeIterator.hasNext()) { + if (!chunkIterator.hasNext()) { + value = null; + return false; + } + + Map.Entry> chunk = chunkIterator.next(); + rX = chunk.getKey().x << 10; + rY = chunk.getKey().y << 10; + rZ = chunk.getKey().z << 10; + relativeIterator = chunk.getValue().entrySet().iterator(); + } + + Map.Entry entry = relativeIterator.next(); + Key relative = entry.getKey(); + position.setX(rX + relative.x); + position.setY(rY + relative.y); + position.setZ(rZ + relative.z); + value = entry.getValue(); + return true; + } + + /** + * The position of the current element. The same instance is returned every step. + */ + public @NotNull IrisBlockVector key() { + return position; + } + + public T value() { + return value; + } + } + + public class EntryIterator implements Iterator> { + private final Iterator>> chunkIterator = map.entrySet().iterator(); + private Iterator> relativeIterator; + private int rX, rY, rZ; + + @Override + public boolean hasNext() { + return advance(); + } + + @Override + public Map.Entry next() { + if (!advance()) throw new NoSuchElementException(); + + var entry = relativeIterator.next(); + return Map.entry(entry.getKey().resolve(rX, rY, rZ), entry.getValue()); + } + + private boolean advance() { + while (relativeIterator == null || !relativeIterator.hasNext()) { + if (!chunkIterator.hasNext()) return false; + var chunk = chunkIterator.next(); + rX = chunk.getKey().x << 10; + rY = chunk.getKey().y << 10; + rZ = chunk.getKey().z << 10; + relativeIterator = chunk.getValue().entrySet().iterator(); + } + + return true; + } + + @Override + public void remove() { + if (relativeIterator == null) throw new IllegalStateException("No element to remove"); + relativeIterator.remove(); + modificationRevision++; + } + } + + public class KeyIterator implements Iterator, Iterable { + private final Iterator>> chunkIterator = map.entrySet().iterator(); + private Iterator relativeIterator; + private int rX, rY, rZ; + + @Override + public boolean hasNext() { + return advance(); + } + + @Override + public IrisBlockVector next() { + if (!advance()) throw new NoSuchElementException(); + + return relativeIterator.next().resolve(rX, rY, rZ); + } + + private boolean advance() { + while (relativeIterator == null || !relativeIterator.hasNext()) { + if (!chunkIterator.hasNext()) return false; + var chunk = chunkIterator.next(); + rX = chunk.getKey().x << 10; + rY = chunk.getKey().y << 10; + rZ = chunk.getKey().z << 10; + relativeIterator = chunk.getValue().keySet().iterator(); + } + + return true; + } + + @Override + public void remove() { + if (relativeIterator == null) throw new IllegalStateException("No element to remove"); + relativeIterator.remove(); + modificationRevision++; + } + + @Override + public @NotNull Iterator iterator() { + return this; + } + } + + public class ValueIterator implements Iterator, Iterable { + private final Iterator> chunkIterator = map.values().iterator(); + private Iterator relativeIterator; + + @Override + public boolean hasNext() { + return advance(); + } + + @Override + public T next() { + if (!advance()) throw new NoSuchElementException(); + + return relativeIterator.next(); + } + + private boolean advance() { + while (relativeIterator == null || !relativeIterator.hasNext()) { + if (!chunkIterator.hasNext()) return false; + relativeIterator = chunkIterator.next().values().iterator(); + } + + return true; + } + + @Override + public void remove() { + if (relativeIterator == null) throw new IllegalStateException("No element to remove"); + relativeIterator.remove(); + modificationRevision++; + } + + @Override + public @NotNull Iterator iterator() { + return this; + } + } + + private static final class Key { + private final int x; + private final int y; + private final int z; + private final int hashCode; + + private Key(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + this.hashCode = (x << 20) | (y << 10) | z; + } + + private IrisBlockVector resolve(int rX, int rY, int rZ) { + return new IrisBlockVector(rX + x, rY + y, rZ + z); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Key key)) return false; + return x == key.x && y == key.y && z == key.z; + } + } +} diff --git a/core/src/main/java/com/volmit/iris/util/data/registry/Attributes.java b/core/src/main/java/art/arcane/iris/util/common/data/registry/Attributes.java similarity index 78% rename from core/src/main/java/com/volmit/iris/util/data/registry/Attributes.java rename to core/src/main/java/art/arcane/iris/util/common/data/registry/Attributes.java index a31d11b89..8400d7b90 100644 --- a/core/src/main/java/com/volmit/iris/util/data/registry/Attributes.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/registry/Attributes.java @@ -1,4 +1,4 @@ -package com.volmit.iris.util.data.registry; +package art.arcane.iris.util.common.data.registry; import org.bukkit.attribute.Attribute; diff --git a/core/src/main/java/com/volmit/iris/util/data/registry/KeyedRegistry.java b/core/src/main/java/art/arcane/iris/util/common/data/registry/KeyedRegistry.java similarity index 96% rename from core/src/main/java/com/volmit/iris/util/data/registry/KeyedRegistry.java rename to core/src/main/java/art/arcane/iris/util/common/data/registry/KeyedRegistry.java index 960ff8ca2..c6543f258 100644 --- a/core/src/main/java/com/volmit/iris/util/data/registry/KeyedRegistry.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/registry/KeyedRegistry.java @@ -1,6 +1,6 @@ -package com.volmit.iris.util.data.registry; +package art.arcane.iris.util.common.data.registry; -import com.volmit.iris.util.collection.KMap; +import art.arcane.volmlib.util.collection.KMap; import lombok.NonNull; import org.bukkit.Keyed; import org.bukkit.NamespacedKey; @@ -8,7 +8,9 @@ import org.bukkit.Registry; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; diff --git a/core/src/main/java/art/arcane/iris/util/common/data/registry/Materials.java b/core/src/main/java/art/arcane/iris/util/common/data/registry/Materials.java new file mode 100644 index 000000000..86fe59606 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/data/registry/Materials.java @@ -0,0 +1,9 @@ +package art.arcane.iris.util.common.data.registry; + +import org.bukkit.Material; + +import static art.arcane.iris.util.common.data.registry.RegistryUtil.find; + +public class Materials { + public static final Material GRASS = find(Material.class, "grass", "short_grass"); +} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/registry/Particles.java b/core/src/main/java/art/arcane/iris/util/common/data/registry/Particles.java new file mode 100644 index 000000000..0ba2c2fd5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/data/registry/Particles.java @@ -0,0 +1,30 @@ +package art.arcane.iris.util.common.data.registry; + +import org.bukkit.Particle; + +import static art.arcane.iris.util.common.data.registry.RegistryUtil.find; + +/** + * Bukkit particle constants. Statically imported by {@code IrisEntity} (a Gson-registered pack + * type), so this class is reachable from core on the modded loaders. The resolution below is + * therefore guarded against the absent Bukkit class: without the guard the class initializer dies + * with a NoClassDefFoundError and the class stays permanently erroneous for the rest of the JVM's + * life. On Bukkit a genuinely missing registry key still throws, exactly as before. + */ +public class Particles { + public static final Particle CRIT_MAGIC = resolve("crit_magic", "crit"); + public static final Particle REDSTONE = resolve("redstone", "dust"); + public static final Particle ITEM = resolve("item_crack", "item"); + + private static Particle resolve(String... keys) { + try { + return find(Particle.class, keys); + } catch (NoClassDefFoundError e) { + // No org.bukkit.Particle on this platform. Every read of these constants is Bukkit-only, so null is + // correct here. Narrower than LinkageError on purpose: a VerifyError, an IncompatibleClassChangeError or + // an ExceptionInInitializerError from the registry itself is a real defect on a Bukkit server and must + // not be silently turned into a null constant. + return null; + } + } +} diff --git a/core/src/main/java/com/volmit/iris/util/data/registry/RegistryTypeAdapter.java b/core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryTypeAdapter.java similarity index 95% rename from core/src/main/java/com/volmit/iris/util/data/registry/RegistryTypeAdapter.java rename to core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryTypeAdapter.java index c328781b7..89523a1c5 100644 --- a/core/src/main/java/com/volmit/iris/util/data/registry/RegistryTypeAdapter.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryTypeAdapter.java @@ -1,4 +1,4 @@ -package com.volmit.iris.util.data.registry; +package art.arcane.iris.util.common.data.registry; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; diff --git a/core/src/main/java/com/volmit/iris/util/data/registry/RegistryUtil.java b/core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryUtil.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/data/registry/RegistryUtil.java rename to core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryUtil.java index 6119cbdad..3b1eb553b 100644 --- a/core/src/main/java/com/volmit/iris/util/data/registry/RegistryUtil.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryUtil.java @@ -1,9 +1,9 @@ -package com.volmit.iris.util.data.registry; +package art.arcane.iris.util.common.data.registry; -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; +import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; import lombok.NonNull; import org.bukkit.Bukkit; import org.bukkit.Keyed; @@ -15,11 +15,12 @@ import org.jetbrains.annotations.Nullable; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.*; -import java.util.function.Function; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; -import static com.volmit.iris.util.data.registry.KeyedRegistry.wrapped; +import static art.arcane.iris.util.common.data.registry.KeyedRegistry.wrapped; @SuppressWarnings("unchecked") public class RegistryUtil { diff --git a/core/src/main/java/art/arcane/iris/util/common/director/DirectorContext.java b/core/src/main/java/art/arcane/iris/util/common/director/DirectorContext.java new file mode 100644 index 000000000..bfba53c03 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/DirectorContext.java @@ -0,0 +1,20 @@ +package art.arcane.iris.util.common.director; + +import art.arcane.volmlib.util.director.context.DirectorContextBase; +import art.arcane.iris.util.common.plugin.VolmitSender; + +public class DirectorContext { + private static final DirectorContextBase context = new DirectorContextBase<>(); + + public static VolmitSender get() { + return context.get(); + } + + public static void touch(VolmitSender c) { + context.touch(c); + } + + public static void remove() { + context.remove(); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/DirectorContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/DirectorContextHandler.java new file mode 100644 index 000000000..94dbe65a1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/DirectorContextHandler.java @@ -0,0 +1,23 @@ +package art.arcane.iris.util.common.director; + +import art.arcane.volmlib.util.director.context.DirectorContextHandlers; +import art.arcane.volmlib.util.director.context.DirectorContextHandlerType; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.VolmitSender; + +import java.util.Map; + +public interface DirectorContextHandler extends DirectorContextHandlerType { + Map, DirectorContextHandler> contextHandlers = buildContextHandlers(); + + static Map, DirectorContextHandler> buildContextHandlers() { + return DirectorContextHandlers.buildOrEmpty( + DirectorSystem.initializePackage("art.arcane.iris.util.common.director.context"), + DirectorContextHandler.class, + h -> ((DirectorContextHandler) h).getType(), + e -> { + IrisLogging.reportError(e); + e.printStackTrace(); + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java b/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java new file mode 100644 index 000000000..754e3c263 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java @@ -0,0 +1,71 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director; + +import art.arcane.volmlib.util.director.DirectorExecutorBase; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.common.plugin.VolmitSender; +import org.bukkit.entity.Player; + +public interface DirectorExecutor extends DirectorExecutorBase { + default VolmitSender sender() { + return DirectorContext.get(); + } + + default Player player() { + VolmitSender sender = sender(); + return sender == null ? null : sender.player(); + } + + default boolean playerWorldGeneratesStructures() { + Player activePlayer = player(); + return activePlayer != null && activePlayer.getWorld().canGenerateStructures(); + } + + default IrisData data() { + var access = access(); + if (access != null) { + return access.getData(); + } + return null; + } + + default Engine engine() { + VolmitSender sender = sender(); + if (sender != null && sender.isPlayer() && IrisToolbelt.access(sender.player().getWorld()) != null) { + PlatformChunkGenerator gen = IrisToolbelt.access(sender.player().getWorld()); + if (gen != null) { + return gen.getEngine(); + } + } + + return null; + } + + default PlatformChunkGenerator access() { + VolmitSender sender = sender(); + if (sender != null && sender.isPlayer()) { + return IrisToolbelt.access(world()); + } + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/DirectorParameterHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/DirectorParameterHandler.java new file mode 100644 index 000000000..ead5defa8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/DirectorParameterHandler.java @@ -0,0 +1,5 @@ +package art.arcane.iris.util.common.director; + +public interface DirectorParameterHandler + extends DirectorExecutor, art.arcane.volmlib.util.director.DirectorParameterHandler { +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/DirectorSystem.java b/core/src/main/java/art/arcane/iris/util/common/director/DirectorSystem.java new file mode 100644 index 000000000..78ffa58f0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/DirectorSystem.java @@ -0,0 +1,67 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.director.DirectorSystemSupport; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.io.JarScanner; +public final class DirectorSystem { + public static final KList> handlers = initializePackage("art.arcane.iris.util.common.director.handlers").convert((i) -> (art.arcane.volmlib.util.director.DirectorParameterHandler) i); + + private DirectorSystem() { + } + + public static KList initializePackage(String packageName) { + JarScanner js = new JarScanner(IrisPlatforms.get().pluginJar(), packageName); + KList v = new KList<>(); + J.attempt(js::scan); + for (Class i : js.getClasses()) { + try { + v.add(i.getDeclaredConstructor().newInstance()); + } catch (Throwable ex) { + IrisLogging.warn("Skipped class initialization for %s: %s%s", + i.getName(), + ex.getClass().getSimpleName(), + ex.getMessage() == null ? "" : " - " + ex.getMessage()); + IrisLogging.reportError(ex); + } + } + + return v; + } + + /** + * Get the handler for the specified type + * + * @param type The type to handle + * @return The corresponding {@link DirectorParameterHandler}, or null + */ + public static art.arcane.volmlib.util.director.DirectorParameterHandler getHandler(Class type) { + art.arcane.volmlib.util.director.DirectorParameterHandler handler = DirectorSystemSupport.getHandler(handlers, type, (h, t) -> h.supports(t)); + if (handler != null) { + return handler; + } + + IrisLogging.error("Unhandled type in Director Parameter: " + type.getName() + ". This is bad!"); + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/context/BiomeContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/context/BiomeContextHandler.java new file mode 100644 index 000000000..d5d90fd32 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/context/BiomeContextHandler.java @@ -0,0 +1,41 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director.context; + +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.util.common.director.DirectorContextHandler; +import art.arcane.iris.util.common.plugin.VolmitSender; + +public class BiomeContextHandler implements DirectorContextHandler { + public Class getType() { + return IrisBiome.class; + } + + public IrisBiome handle(VolmitSender sender) { + if (sender.isPlayer() + && IrisToolbelt.isIrisWorld(sender.player().getWorld()) + && IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) { + return EngineBukkitOps.getBiomeOrMantle(IrisToolbelt.access(sender.player().getWorld()).getEngine(), sender.player().getLocation()); + } + + return null; + } +} diff --git a/core/src/main/java/com/volmit/iris/util/decree/context/DimensionContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/context/DimensionContextHandler.java similarity index 77% rename from core/src/main/java/com/volmit/iris/util/decree/context/DimensionContextHandler.java rename to core/src/main/java/art/arcane/iris/util/common/director/context/DimensionContextHandler.java index 1ea77b6d9..cb6ab3b81 100644 --- a/core/src/main/java/com/volmit/iris/util/decree/context/DimensionContextHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/context/DimensionContextHandler.java @@ -16,14 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.decree.context; +package art.arcane.iris.util.common.director.context; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.util.decree.DecreeContextHandler; -import com.volmit.iris.util.plugin.VolmitSender; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.util.common.director.DirectorContextHandler; +import art.arcane.iris.util.common.plugin.VolmitSender; -public class DimensionContextHandler implements DecreeContextHandler { +public class DimensionContextHandler implements DirectorContextHandler { public Class getType() { return IrisDimension.class; } diff --git a/core/src/main/java/art/arcane/iris/util/common/director/context/GeneratorContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/context/GeneratorContextHandler.java new file mode 100644 index 000000000..71303993c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/context/GeneratorContextHandler.java @@ -0,0 +1,45 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director.context; + +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.util.common.director.DirectorContextHandler; +import art.arcane.iris.util.common.plugin.VolmitSender; + +public class GeneratorContextHandler implements DirectorContextHandler { + @Override + public Class getType() { + return IrisGenerator.class; + } + + @Override + public IrisGenerator handle(VolmitSender sender) { + if (sender.isPlayer() + && IrisToolbelt.isIrisWorld(sender.player().getWorld()) + && IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) { + Engine engine = IrisToolbelt.access(sender.player().getWorld()).getEngine(); + return engine.getData().getGeneratorLoader().load(EngineBukkitOps.getBiome(engine, sender.player().getLocation()).getGenerators().getRandom().getGenerator()); + } + + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/context/RegionContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/context/RegionContextHandler.java new file mode 100644 index 000000000..3ac4a7c02 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/context/RegionContextHandler.java @@ -0,0 +1,41 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director.context; + +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.util.common.director.DirectorContextHandler; +import art.arcane.iris.util.common.plugin.VolmitSender; + +public class RegionContextHandler implements DirectorContextHandler { + public Class getType() { + return IrisRegion.class; + } + + public IrisRegion handle(VolmitSender sender) { + if (sender.isPlayer() + && IrisToolbelt.isIrisWorld(sender.player().getWorld()) + && IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) { + return EngineBukkitOps.getRegion(IrisToolbelt.access(sender.player().getWorld()).getEngine(), sender.player().getLocation()); + } + + return null; + } +} diff --git a/core/src/main/java/com/volmit/iris/util/decree/context/VectorContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/context/VectorContextHandler.java similarity index 78% rename from core/src/main/java/com/volmit/iris/util/decree/context/VectorContextHandler.java rename to core/src/main/java/art/arcane/iris/util/common/director/context/VectorContextHandler.java index 84247f326..2fa2bdfa6 100644 --- a/core/src/main/java/com/volmit/iris/util/decree/context/VectorContextHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/context/VectorContextHandler.java @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.decree.context; +package art.arcane.iris.util.common.director.context; -import com.volmit.iris.util.decree.DecreeContextHandler; -import com.volmit.iris.util.plugin.VolmitSender; +import art.arcane.iris.util.common.director.DirectorContextHandler; +import art.arcane.iris.util.common.plugin.VolmitSender; import org.bukkit.util.Vector; -public class VectorContextHandler implements DecreeContextHandler { +public class VectorContextHandler implements DirectorContextHandler { public Class getType() { return Vector.class; } diff --git a/core/src/main/java/art/arcane/iris/util/common/director/context/WorldContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/context/WorldContextHandler.java new file mode 100644 index 000000000..7bbf082ca --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/context/WorldContextHandler.java @@ -0,0 +1,18 @@ +package art.arcane.iris.util.common.director.context; + +import art.arcane.volmlib.util.director.context.WorldContextHandlerBase; +import art.arcane.iris.util.common.director.DirectorContextHandler; +import art.arcane.iris.util.common.plugin.VolmitSender; +import org.bukkit.World; + +public class WorldContextHandler extends WorldContextHandlerBase implements DirectorContextHandler { + @Override + protected boolean isPlayer(VolmitSender sender) { + return sender.isPlayer(); + } + + @Override + protected World getWorld(VolmitSender sender) { + return sender.player().getWorld(); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/BiomeHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/BiomeHandler.java similarity index 84% rename from core/src/main/java/com/volmit/iris/util/decree/handlers/BiomeHandler.java rename to core/src/main/java/art/arcane/iris/util/common/director/handlers/BiomeHandler.java index 36f78ee16..dd89b2c7d 100644 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/BiomeHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/BiomeHandler.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.decree.handlers; +package art.arcane.iris.util.common.director.handlers; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.util.common.director.specialhandlers.RegistrantHandler; public class BiomeHandler extends RegistrantHandler { public BiomeHandler() { diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/BlockVectorHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/BlockVectorHandler.java new file mode 100644 index 000000000..60c74cb2a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/BlockVectorHandler.java @@ -0,0 +1,39 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.iris.util.common.director.DirectorContext; +import art.arcane.volmlib.util.director.DirectorParameterHandler; +import art.arcane.iris.util.common.director.DirectorSystem; +import art.arcane.volmlib.util.director.handlers.base.BlockVectorHandlerBase; +import art.arcane.volmlib.util.format.Form; +import org.bukkit.FluidCollisionMode; +import org.bukkit.entity.Player; +import org.bukkit.util.BlockVector; + +import java.util.List; + +public class BlockVectorHandler extends BlockVectorHandlerBase implements DirectorParameterHandler { + @Override + protected boolean isSenderPlayer() { + return DirectorContext.get().isPlayer(); + } + + @Override + protected BlockVector getSenderBlockVector() { + return DirectorContext.get().player().getLocation().toVector().toBlockVector(); + } + + @Override + protected BlockVector getLookBlockVector() { + return DirectorContext.get().player().getTargetBlockExact(256, FluidCollisionMode.NEVER).getLocation().toVector().toBlockVector(); + } + + @Override + protected List playerPossibilities(String query) { + return DirectorSystem.getHandler(Player.class).getPossibilities(query); + } + + @Override + protected String format(double value) { + return Form.f(value, 2); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/BooleanHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/BooleanHandler.java new file mode 100644 index 000000000..24ae4ce7c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/BooleanHandler.java @@ -0,0 +1,7 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.base.BooleanHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; + +public class BooleanHandler extends BooleanHandlerBase implements DirectorParameterHandler { +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/ByteHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/ByteHandler.java new file mode 100644 index 000000000..f8886be55 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/ByteHandler.java @@ -0,0 +1,7 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.base.ByteHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; + +public class ByteHandler extends ByteHandlerBase implements DirectorParameterHandler { +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/DataVersionHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/DataVersionHandler.java new file mode 100644 index 000000000..71f740502 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/DataVersionHandler.java @@ -0,0 +1,36 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.director.DirectorParameterHandler; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; + +public class DataVersionHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + return new KList<>(DataVersion.values()).qdel(DataVersion.UNSUPPORTED); + } + + @Override + public String toString(DataVersion version) { + return version.getVersion(); + } + + @Override + public DataVersion parse(String in, boolean force) throws DirectorParsingException { + if (in.equalsIgnoreCase("latest")) { + return DataVersion.getLatest(); + } + for (DataVersion v : DataVersion.values()) { + if (v.getVersion().equalsIgnoreCase(in)) { + return v; + } + } + throw new DirectorParsingException("Unable to parse data version \"" + in + "\""); + } + + @Override + public boolean supports(Class type) { + return DataVersion.class.equals(type); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/DimensionHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/DimensionHandler.java new file mode 100644 index 000000000..27c0b80b8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/DimensionHandler.java @@ -0,0 +1,87 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; +import art.arcane.iris.util.common.director.specialhandlers.RegistrantHandler; + +import java.util.Locale; + +public class DimensionHandler extends RegistrantHandler { + public DimensionHandler() { + super(IrisDimension.class, false); + } + + @Override + public IrisDimension parse(String in, boolean force) throws DirectorParsingException { + String key = in.trim(); + if (key.equalsIgnoreCase("default")) { + key = IrisSettings.get().getGenerator().getDefaultWorldType(); + } + + try { + return super.parse(key, force); + } catch (DirectorParsingException ignored) { + String normalized = key.toLowerCase(Locale.ROOT); + IrisDimension resolved = IrisToolbelt.getDimension(normalized); + if (resolved != null) { + return resolved; + } + + if (!normalized.equals(key)) { + resolved = IrisToolbelt.getDimension(key); + if (resolved != null) { + return resolved; + } + } + + throw ignored; + } + } + + @Override + public KList getPossibilities(String input) { + KList possibilities = super.getPossibilities(); + String normalizedInput = input == null ? "" : input.trim().toLowerCase(Locale.ROOT); + if (normalizedInput.isEmpty()) { + return possibilities; + } + + KList filtered = new KList<>(); + for (IrisDimension dimension : possibilities) { + if (dimension != null && dimension.getLoadKey() != null) { + String key = dimension.getLoadKey().toLowerCase(Locale.ROOT); + if (key.startsWith(normalizedInput)) { + filtered.add(dimension); + } + } + } + + return filtered; + } + + @Override + public String getRandomDefault() { + return "dimension"; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/DoubleHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/DoubleHandler.java new file mode 100644 index 000000000..dc82f8c55 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/DoubleHandler.java @@ -0,0 +1,7 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.base.DoubleHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; + +public class DoubleHandler extends DoubleHandlerBase implements DirectorParameterHandler { +} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/EntityHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/EntityHandler.java similarity index 84% rename from core/src/main/java/com/volmit/iris/util/decree/handlers/EntityHandler.java rename to core/src/main/java/art/arcane/iris/util/common/director/handlers/EntityHandler.java index 3cf4b4e08..53c831a47 100644 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/EntityHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/EntityHandler.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.decree.handlers; +package art.arcane.iris.util.common.director.handlers; -import com.volmit.iris.engine.object.IrisEntity; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; +import art.arcane.iris.engine.object.IrisEntity; +import art.arcane.iris.util.common.director.specialhandlers.RegistrantHandler; public class EntityHandler extends RegistrantHandler { diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/FloatHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/FloatHandler.java new file mode 100644 index 000000000..ba3d1b4e5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/FloatHandler.java @@ -0,0 +1,7 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.base.FloatHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; + +public class FloatHandler extends FloatHandlerBase implements DirectorParameterHandler { +} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/GeneratorHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/GeneratorHandler.java similarity index 84% rename from core/src/main/java/com/volmit/iris/util/decree/handlers/GeneratorHandler.java rename to core/src/main/java/art/arcane/iris/util/common/director/handlers/GeneratorHandler.java index 8b1baf0ae..8d26677b9 100644 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/GeneratorHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/GeneratorHandler.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.decree.handlers; +package art.arcane.iris.util.common.director.handlers; -import com.volmit.iris.engine.object.IrisGenerator; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.util.common.director.specialhandlers.RegistrantHandler; public class GeneratorHandler extends RegistrantHandler { public GeneratorHandler() { diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/IntegerHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/IntegerHandler.java new file mode 100644 index 000000000..e845710e0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/IntegerHandler.java @@ -0,0 +1,7 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.base.IntegerHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; + +public class IntegerHandler extends IntegerHandlerBase implements DirectorParameterHandler { +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/LongHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/LongHandler.java new file mode 100644 index 000000000..e78712f22 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/LongHandler.java @@ -0,0 +1,7 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.base.LongHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; + +public class LongHandler extends LongHandlerBase implements DirectorParameterHandler { +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/PlayerHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/PlayerHandler.java new file mode 100644 index 000000000..ae4db26b5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/PlayerHandler.java @@ -0,0 +1,26 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.base.BukkitPlayerHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; +import org.bukkit.entity.Player; + +public class PlayerHandler extends BukkitPlayerHandlerBase implements DirectorParameterHandler { +} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/RegionHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/RegionHandler.java similarity index 84% rename from core/src/main/java/com/volmit/iris/util/decree/handlers/RegionHandler.java rename to core/src/main/java/art/arcane/iris/util/common/director/handlers/RegionHandler.java index 5b47af457..42e8d34c4 100644 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/RegionHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/RegionHandler.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.decree.handlers; +package art.arcane.iris.util.common.director.handlers; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.util.common.director.specialhandlers.RegistrantHandler; public class RegionHandler extends RegistrantHandler { public RegionHandler() { diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/ShortHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/ShortHandler.java new file mode 100644 index 000000000..292ecbad2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/ShortHandler.java @@ -0,0 +1,7 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.base.ShortHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; + +public class ShortHandler extends ShortHandlerBase implements DirectorParameterHandler { +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/StringHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/StringHandler.java new file mode 100644 index 000000000..477f3b60c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/StringHandler.java @@ -0,0 +1,7 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.StringHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; + +public class StringHandler extends StringHandlerBase implements DirectorParameterHandler { +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/VectorHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/VectorHandler.java new file mode 100644 index 000000000..45f31491d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/VectorHandler.java @@ -0,0 +1,39 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.iris.util.common.director.DirectorContext; +import art.arcane.volmlib.util.director.DirectorParameterHandler; +import art.arcane.iris.util.common.director.DirectorSystem; +import art.arcane.volmlib.util.director.handlers.base.VectorHandlerBase; +import art.arcane.volmlib.util.format.Form; +import org.bukkit.FluidCollisionMode; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.List; + +public class VectorHandler extends VectorHandlerBase implements DirectorParameterHandler { + @Override + protected boolean isSenderPlayer() { + return DirectorContext.get().isPlayer(); + } + + @Override + protected Vector getSenderVector() { + return DirectorContext.get().player().getLocation().toVector(); + } + + @Override + protected Vector getLookVector() { + return DirectorContext.get().player().getTargetBlockExact(256, FluidCollisionMode.NEVER).getLocation().toVector(); + } + + @Override + protected List playerPossibilities(String query) { + return DirectorSystem.getHandler(Player.class).getPossibilities(query); + } + + @Override + protected String format(double value) { + return Form.f(value, 2); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/WorldHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/WorldHandler.java new file mode 100644 index 000000000..5516894af --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/handlers/WorldHandler.java @@ -0,0 +1,12 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.handlers.base.WorldHandlerBase; +import art.arcane.volmlib.util.director.DirectorParameterHandler; +import org.bukkit.World; + +public class WorldHandler extends WorldHandlerBase implements DirectorParameterHandler { + @Override + protected String excludedPrefix() { + return "iris/"; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/IrisStructureHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/IrisStructureHandler.java new file mode 100644 index 000000000..45526850f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/IrisStructureHandler.java @@ -0,0 +1,54 @@ +package art.arcane.iris.util.common.director.specialhandlers; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.util.common.director.DirectorParameterHandler; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; + +import java.io.File; +import java.util.LinkedHashSet; +import java.util.Set; + +public final class IrisStructureHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + Set keys = new LinkedHashSet<>(); + IrisData activeData = data(); + if (activeData != null) { + addStructureKeys(keys, activeData); + } + for (File pack : PackDirectoryResolver.listVisiblePackDirectories( + IrisPlatforms.get().packsFolder())) { + addStructureKeys(keys, IrisData.get(pack)); + } + return new KList<>(keys); + } + + @Override + public String toString(String value) { + return value == null ? "" : value; + } + + @Override + public String parse(String input, boolean force) throws DirectorParsingException { + for (String option : getPossibilities(input)) { + if (option.equalsIgnoreCase(input)) { + return option; + } + } + throw new DirectorParsingException("Unable to find Iris structure \"" + input + "\""); + } + + @Override + public boolean supports(Class type) { + return type == String.class; + } + + private static void addStructureKeys(Set keys, IrisData data) { + for (String key : data.getStructureLoader().getPossibleKeys()) { + keys.add(key); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/NullableDimensionHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/NullableDimensionHandler.java new file mode 100644 index 000000000..fcfa38d80 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/NullableDimensionHandler.java @@ -0,0 +1,86 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director.specialhandlers; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; + +import java.util.Locale; + +public class NullableDimensionHandler extends RegistrantHandler { + public NullableDimensionHandler() { + super(IrisDimension.class, true); + } + + @Override + public IrisDimension parse(String in, boolean force) throws DirectorParsingException { + String key = in.trim(); + if (key.equalsIgnoreCase("default")) { + key = IrisSettings.get().getGenerator().getDefaultWorldType(); + } + + try { + return super.parse(key, force); + } catch (DirectorParsingException ignored) { + String normalized = key.toLowerCase(Locale.ROOT); + IrisDimension resolved = IrisToolbelt.getDimension(normalized); + if (resolved != null) { + return resolved; + } + + if (!normalized.equals(key)) { + resolved = IrisToolbelt.getDimension(key); + if (resolved != null) { + return resolved; + } + } + + throw ignored; + } + } + + @Override + public KList getPossibilities(String input) { + KList possibilities = super.getPossibilities(); + String normalizedInput = input == null ? "" : input.trim().toLowerCase(Locale.ROOT); + if (normalizedInput.isEmpty()) { + return possibilities; + } + + KList filtered = new KList<>(); + for (IrisDimension dimension : possibilities) { + if (dimension != null && dimension.getLoadKey() != null) { + String key = dimension.getLoadKey().toLowerCase(Locale.ROOT); + if (key.startsWith(normalizedInput)) { + filtered.add(dimension); + } + } + } + + return filtered; + } + + @Override + public String getRandomDefault() { + return "dimension"; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/NullablePlayerHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/NullablePlayerHandler.java new file mode 100644 index 000000000..7549c71e4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/NullablePlayerHandler.java @@ -0,0 +1,14 @@ +package art.arcane.iris.util.common.director.specialhandlers; + +import art.arcane.volmlib.util.director.handlers.base.NullablePlayerHandlerBase; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; +import art.arcane.volmlib.util.director.DirectorParameterHandler; +import art.arcane.iris.util.common.director.handlers.PlayerHandler; +import org.bukkit.entity.Player; + +public class NullablePlayerHandler extends PlayerHandler implements DirectorParameterHandler { + @Override + public Player parse(String in, boolean force) throws DirectorParsingException { + return NullablePlayerHandlerBase.parseNullable(this, in); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectHandler.java new file mode 100644 index 000000000..ec8669a4c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectHandler.java @@ -0,0 +1,79 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director.specialhandlers; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorParameterHandler; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; + +import java.io.File; +import java.util.stream.Collectors; + +public class ObjectHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + KList p = new KList<>(); + IrisData data = data(); + if (data != null) { + return new KList<>(data.getObjectLoader().getPossibleKeys()); + } + + for (File i : PackDirectoryResolver.listVisiblePackDirectories( + IrisPlatforms.get().packsFolder())) { + data = IrisData.get(i); + p.add(data.getObjectLoader().getPossibleKeys()); + } + + return p; + } + + @Override + public String toString(String irisObject) { + return irisObject; + } + + @Override + public String parse(String in, boolean force) throws DirectorParsingException { + KList options = getPossibilities(in); + + if (options.isEmpty()) { + throw new DirectorParsingException("Unable to find Object \"" + in + "\""); + } + try { + return options.stream().filter((i) -> toString(i).equalsIgnoreCase(in)).collect(Collectors.toList()).get(0); + } catch (Throwable e) { + throw new DirectorParsingException("Unable to filter which Object \"" + in + "\""); + } + } + + @Override + public boolean supports(Class type) { + return type.equals(String.class); + } + + @Override + public String getRandomDefault() { + String f = getPossibilities().getRandom(); + + return f == null ? "object" : f; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectTargetHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectTargetHandler.java new file mode 100644 index 000000000..665c01533 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectTargetHandler.java @@ -0,0 +1,89 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director.specialhandlers; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorParameterHandler; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +public class ObjectTargetHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + KList out = new KList<>(); + Set prefixes = new HashSet<>(); + + IrisData data = data(); + if (data != null) { + for (String k : data.getObjectLoader().getPossibleKeys()) { + out.add(k); + collectPrefixes(k, prefixes); + } + } else { + File packsFolder = IrisPlatforms.get().packsFolder(); + for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsFolder)) { + IrisData d = IrisData.get(pack); + for (String k : d.getObjectLoader().getPossibleKeys()) { + out.add(k); + collectPrefixes(k, prefixes); + } + } + } + + for (String p : prefixes) { + out.add(p); + } + return out; + } + + private static void collectPrefixes(String key, Set prefixes) { + int idx = 0; + while ((idx = key.indexOf('/', idx)) >= 0) { + prefixes.add(key.substring(0, idx + 1)); + idx++; + } + } + + @Override + public String toString(String irisObject) { + return irisObject; + } + + @Override + public String parse(String in, boolean force) throws DirectorParsingException { + return in; + } + + @Override + public boolean supports(Class type) { + return type.equals(String.class); + } + + @Override + public String getRandomDefault() { + String f = getPossibilities().getRandom(); + return f == null ? "trees/" : f; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/RegistrantHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/RegistrantHandler.java new file mode 100644 index 000000000..c11a99aff --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/RegistrantHandler.java @@ -0,0 +1,75 @@ +package art.arcane.iris.util.common.director.specialhandlers; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.pack.PackDirectoryResolver; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorParameterHandler; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +public abstract class RegistrantHandler implements DirectorParameterHandler { + private final Class type; + private final String name; + private final boolean nullable; + + public RegistrantHandler(Class type, boolean nullable) { + this.type = type; + this.name = type.getSimpleName().replaceFirst("Iris", ""); + this.nullable = nullable; + } + + @Override + public KList getPossibilities() { + KList p = new KList<>(); + Set known = new HashSet<>(); + IrisData data = data(); + if (data != null) { + for (T j : data.getLoader(type).loadAll(data.getLoader(type).getPossibleKeys())) { + known.add(j.getLoadKey()); + p.add(j); + } + } + + for (File i : PackDirectoryResolver.listVisiblePackDirectories( + IrisPlatforms.get().packsFolder())) { + data = IrisData.get(i); + for (T j : data.getLoader(type).loadAll(data.getLoader(type).getPossibleKeys())) { + if (known.add(j.getLoadKey())) + p.add(j); + } + } + + return p; + } + + @Override + public String toString(T t) { + return t != null ? t.getLoadKey() : "null"; + } + + @Override + public T parse(String in, boolean force) throws DirectorParsingException { + if (in.equals("null") && nullable) { + return null; + } + KList options = getPossibilities(in); + if (options.isEmpty()) { + throw new DirectorParsingException("Unable to find " + name + " \"" + in + "\""); + } + + return options.stream() + .filter((i) -> toString(i).equalsIgnoreCase(in)) + .findFirst() + .orElseThrow(() -> new DirectorParsingException("Unable to filter which " + name + " \"" + in + "\"")); + } + + @Override + public boolean supports(Class type) { + return type.equals(this.type); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java new file mode 100644 index 000000000..3484f99c3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java @@ -0,0 +1,145 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.director.specialhandlers; + +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.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.iris.util.common.director.DirectorParameterHandler; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +public class StructureHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + Engine activeEngine = engine(); + if (activeEngine == null) { + return new KList<>(); + } + + boolean nativeGenerationEnabled = nativeStructureGenerationEnabled(); + PlatformStructureHooks structureHooks = IrisPlatforms.get().structureHooks(); + Map registeredKeys = distinctKeys(structureHooks.structureKeys()); + Set reachableKeys = StructureReachability.reachableKeys(activeEngine); + Map suggestions = new LinkedHashMap<>(); + + for (Map.Entry entry : registeredKeys.entrySet()) { + if (isEligibleRegisteredKey(activeEngine, entry.getValue(), entry.getKey(), reachableKeys, + nativeGenerationEnabled)) { + suggestions.put(entry.getKey(), entry.getValue()); + } + } + + Set locatableKeys = IrisStructureLocator.locatableEditableKeys(activeEngine); + for (String key : locatableKeys) { + String normalizedKey = normalizeKey(key); + if (!normalizedKey.isEmpty() + && !registeredKeys.containsKey(normalizedKey) + && !IrisStructureLocator.hasNativePlacement(activeEngine, key)) { + suggestions.putIfAbsent(normalizedKey, key.trim()); + } + } + + return new KList<>(suggestions.values()); + } + + @Override + public String toString(String structure) { + return structure; + } + + @Override + public String parse(String in, boolean force) throws DirectorParsingException { + KList options = getPossibilities(in); + for (String option : options) { + if (option.equalsIgnoreCase(in)) { + return option; + } + } + return in; + } + + @Override + public boolean supports(Class type) { + return type.equals(String.class); + } + + @Override + public String getRandomDefault() { + String f = getPossibilities().getRandom(); + + return f == null ? "minecraft_ancient_city" : f; + } + + protected boolean nativeStructureGenerationEnabled() { + return playerWorldGeneratesStructures(); + } + + private static Map distinctKeys(List keys) { + Map distinct = new LinkedHashMap<>(); + for (String key : keys) { + String normalizedKey = normalizeKey(key); + if (!normalizedKey.isEmpty()) { + distinct.putIfAbsent(normalizedKey, key.trim()); + } + } + return distinct; + } + + private static boolean isEligibleRegisteredKey(Engine engine, String key, String normalizedKey, + Set reachableKeys, boolean nativeGenerationEnabled) { + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false); + boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key); + boolean locatableNativePlacement = nativePlacement + && IrisStructureLocator.hasLocatableNativePlacement(engine, key); + boolean locatableEditableReplacement = !nativePlacement + && decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS + && IrisStructureLocator.hasLocatableEditablePlacement(engine, key); + return isEligibleRegisteredKey( + decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement, + reachableKeys.contains(normalizedKey), nativeGenerationEnabled); + } + + static boolean isEligibleRegisteredKey(IrisNativeStructureDecision decision, boolean nativePlacement, + boolean locatableNativePlacement, + boolean locatableEditableReplacement, + boolean reachable, boolean nativeGenerationEnabled) { + if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + return locatableEditableReplacement + || nativeGenerationEnabled && nativePlacement && locatableNativePlacement; + } + return nativeGenerationEnabled && decision.generate() + && (reachable || (nativePlacement && locatableNativePlacement)); + } + + private static String normalizeKey(String key) { + return key == null ? "" : key.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/format/C.java b/core/src/main/java/art/arcane/iris/util/common/format/C.java similarity index 89% rename from core/src/main/java/com/volmit/iris/util/format/C.java rename to core/src/main/java/art/arcane/iris/util/common/format/C.java index c33d52032..034972894 100644 --- a/core/src/main/java/com/volmit/iris/util/format/C.java +++ b/core/src/main/java/art/arcane/iris/util/common/format/C.java @@ -16,14 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.format; +package art.arcane.iris.util.common.format; -import com.volmit.iris.Iris; -import com.volmit.iris.util.plugin.VolmitSender; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.VolmitSender; import net.kyori.adventure.text.minimessage.MiniMessage; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; -import org.apache.commons.lang.Validate; +import org.apache.commons.lang3.Validate; import org.bukkit.ChatColor; import org.bukkit.Color; import org.bukkit.DyeColor; @@ -260,9 +260,7 @@ public enum C { @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private final static Map BY_ID = new HashMap<>(); private final static Map BY_CHAR = new HashMap<>(); - private final static Map dyeChatMap = new HashMap<>(); private final static Map chatHexMap = new HashMap<>(); - private final static Map dyeHexMap = new HashMap<>(); static { chatHexMap.put(C.BLACK, "#000000"); @@ -282,38 +280,6 @@ public enum C { chatHexMap.put(C.LIGHT_PURPLE, "#FF55FF"); chatHexMap.put(C.YELLOW, "#FFFF55"); chatHexMap.put(C.WHITE, "#FFFFFF"); - dyeChatMap.put(DyeColor.BLACK, C.DARK_GRAY); - dyeChatMap.put(DyeColor.BLUE, C.DARK_BLUE); - dyeChatMap.put(DyeColor.BROWN, C.GOLD); - dyeChatMap.put(DyeColor.CYAN, C.AQUA); - dyeChatMap.put(DyeColor.GRAY, C.GRAY); - dyeChatMap.put(DyeColor.GREEN, C.DARK_GREEN); - dyeChatMap.put(DyeColor.LIGHT_BLUE, C.BLUE); - dyeChatMap.put(DyeColor.LIME, C.GREEN); - dyeChatMap.put(DyeColor.MAGENTA, C.LIGHT_PURPLE); - dyeChatMap.put(DyeColor.ORANGE, C.GOLD); - dyeChatMap.put(DyeColor.PINK, C.LIGHT_PURPLE); - dyeChatMap.put(DyeColor.PURPLE, C.DARK_PURPLE); - dyeChatMap.put(DyeColor.RED, C.RED); - dyeChatMap.put(DyeColor.LIGHT_GRAY, C.GRAY); - dyeChatMap.put(DyeColor.WHITE, C.WHITE); - dyeChatMap.put(DyeColor.YELLOW, C.YELLOW); - dyeHexMap.put(DyeColor.BLACK, "#181414"); - dyeHexMap.put(DyeColor.BLUE, "#253193"); - dyeHexMap.put(DyeColor.BROWN, "#56331c"); - dyeHexMap.put(DyeColor.CYAN, "#267191"); - dyeHexMap.put(DyeColor.GRAY, "#414141"); - dyeHexMap.put(DyeColor.GREEN, "#364b18"); - dyeHexMap.put(DyeColor.LIGHT_BLUE, "#6387d2"); - dyeHexMap.put(DyeColor.LIME, "#39ba2e"); - dyeHexMap.put(DyeColor.MAGENTA, "#be49c9"); - dyeHexMap.put(DyeColor.ORANGE, "#ea7e35"); - dyeHexMap.put(DyeColor.PINK, "#d98199"); - dyeHexMap.put(DyeColor.PURPLE, "#7e34bf"); - dyeHexMap.put(DyeColor.RED, "#9e2b27"); - dyeHexMap.put(DyeColor.LIGHT_GRAY, "#a0a7a7"); - dyeHexMap.put(DyeColor.WHITE, "#a4a4a4"); - dyeHexMap.put(DyeColor.YELLOW, "#c2b51c"); } static { @@ -323,6 +289,46 @@ public enum C { } } + private static final class DyeMaps { + private static final Map CHAT = new HashMap<>(); + private static final Map HEX = new HashMap<>(); + + static { + CHAT.put(DyeColor.BLACK, C.DARK_GRAY); + CHAT.put(DyeColor.BLUE, C.DARK_BLUE); + CHAT.put(DyeColor.BROWN, C.GOLD); + CHAT.put(DyeColor.CYAN, C.AQUA); + CHAT.put(DyeColor.GRAY, C.GRAY); + CHAT.put(DyeColor.GREEN, C.DARK_GREEN); + CHAT.put(DyeColor.LIGHT_BLUE, C.BLUE); + CHAT.put(DyeColor.LIME, C.GREEN); + CHAT.put(DyeColor.MAGENTA, C.LIGHT_PURPLE); + CHAT.put(DyeColor.ORANGE, C.GOLD); + CHAT.put(DyeColor.PINK, C.LIGHT_PURPLE); + CHAT.put(DyeColor.PURPLE, C.DARK_PURPLE); + CHAT.put(DyeColor.RED, C.RED); + CHAT.put(DyeColor.LIGHT_GRAY, C.GRAY); + CHAT.put(DyeColor.WHITE, C.WHITE); + CHAT.put(DyeColor.YELLOW, C.YELLOW); + HEX.put(DyeColor.BLACK, "#181414"); + HEX.put(DyeColor.BLUE, "#253193"); + HEX.put(DyeColor.BROWN, "#56331c"); + HEX.put(DyeColor.CYAN, "#267191"); + HEX.put(DyeColor.GRAY, "#414141"); + HEX.put(DyeColor.GREEN, "#364b18"); + HEX.put(DyeColor.LIGHT_BLUE, "#6387d2"); + HEX.put(DyeColor.LIME, "#39ba2e"); + HEX.put(DyeColor.MAGENTA, "#be49c9"); + HEX.put(DyeColor.ORANGE, "#ea7e35"); + HEX.put(DyeColor.PINK, "#d98199"); + HEX.put(DyeColor.PURPLE, "#7e34bf"); + HEX.put(DyeColor.RED, "#9e2b27"); + HEX.put(DyeColor.LIGHT_GRAY, "#a0a7a7"); + HEX.put(DyeColor.WHITE, "#a4a4a4"); + HEX.put(DyeColor.YELLOW, "#c2b51c"); + } + } + private final int intCode; private final char code; private final String token; @@ -443,7 +449,7 @@ public enum C { } public static String compress(String c) { - return BaseComponent.toLegacyText(TextComponent.fromLegacyText(c)); + return BaseComponent.toLegacyText(TextComponent.fromLegacy(c)); } /** @@ -458,7 +464,7 @@ public enum C { C c = BY_CHAR.get(code); return c == null ? C.WHITE : c; } catch (Exception e) { - Iris.reportError(e); + IrisLogging.reportError(e); return C.WHITE; } } @@ -477,7 +483,7 @@ public enum C { return BY_CHAR.get(code.charAt(0)); } catch (Exception e) { - Iris.reportError(e); + IrisLogging.reportError(e); return C.WHITE; } } @@ -511,15 +517,15 @@ public enum C { * @return the color */ public static C dyeToChat(DyeColor dclr) { - if (dyeChatMap.containsKey(dclr)) { - return dyeChatMap.get(dclr); + if (DyeMaps.CHAT.containsKey(dclr)) { + return DyeMaps.CHAT.get(dclr); } return C.MAGIC; } public static DyeColor chatToDye(ChatColor color) { - for (Map.Entry entry : dyeChatMap.entrySet()) { + for (Map.Entry entry : DyeMaps.CHAT.entrySet()) { if (entry.getValue().toString().equals(color.toString())) { return entry.getKey(); } @@ -538,8 +544,8 @@ public enum C { } public static String dyeToHex(DyeColor clr) { - if (dyeHexMap.containsKey(clr)) { - return dyeHexMap.get(clr); + if (DyeMaps.HEX.containsKey(clr)) { + return DyeMaps.HEX.get(clr); } return "#000000"; @@ -592,7 +598,7 @@ public enum C { str.append(""); str.append(""); - for (Map.Entry e : dyeHexMap.entrySet()) { + for (Map.Entry e : DyeMaps.HEX.entrySet()) { str.append(String.format("" + "", e.getKey().name(), e.getValue())); } @@ -768,4 +774,4 @@ public enum C { default -> (byte) 15; }; } -} \ No newline at end of file +} diff --git a/core/src/main/java/art/arcane/iris/util/common/inventorygui/UIStaticDecorator.java b/core/src/main/java/art/arcane/iris/util/common/inventorygui/UIStaticDecorator.java new file mode 100644 index 000000000..4bf5da6b0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/inventorygui/UIStaticDecorator.java @@ -0,0 +1,40 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.inventorygui; + +import art.arcane.volmlib.util.inventorygui.WindowDecorator; + +import art.arcane.volmlib.util.inventorygui.Element; + +import art.arcane.volmlib.util.data.MaterialBlock; +import org.bukkit.Material; + +@SuppressWarnings("ClassCanBeRecord") +public class UIStaticDecorator implements WindowDecorator { + private final Element element; + + public UIStaticDecorator(Element element) { + this.element = element == null ? new art.arcane.volmlib.util.inventorygui.UIElement("bg").setMaterial(new MaterialBlock(Material.AIR)) : element; + } + + @Override + public Element onDecorateBackground(art.arcane.volmlib.util.inventorygui.Window window, int position, int row) { + return element; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/inventorygui/Window.java b/core/src/main/java/art/arcane/iris/util/common/inventorygui/Window.java new file mode 100644 index 000000000..fa88b247d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/inventorygui/Window.java @@ -0,0 +1,84 @@ +package art.arcane.iris.util.common.inventorygui; + +import art.arcane.volmlib.util.inventorygui.WindowDecorator; + +import art.arcane.volmlib.util.inventorygui.Element; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public interface Window { + WindowDecorator getDecorator(); + + Window setDecorator(WindowDecorator decorator); + + art.arcane.volmlib.util.inventorygui.WindowResolution getResolution(); + + Window setResolution(art.arcane.volmlib.util.inventorygui.WindowResolution resolution); + + default Window setResolution(WindowResolution resolution) { + return setResolution(resolution.toShared()); + } + + default WindowResolution getLocalResolution() { + return WindowResolution.fromShared(getResolution()); + } + + Window clearElements(); + + Window close(); + + Window open(); + + Window callClosed(); + + Window updateInventory(); + + ItemStack computeItemStack(int viewportSlot); + + int getLayoutRow(int viewportSlottedPosition); + + int getLayoutPosition(int viewportSlottedPosition); + + int getRealLayoutPosition(int viewportSlottedPosition); + + int getRealPosition(int position, int row); + + int getRow(int realPosition); + + int getPosition(int realPosition); + + boolean isVisible(); + + Window setVisible(boolean visible); + + int getViewportPosition(); + + Window setViewportPosition(int position); + + int getViewportSlots(); + + int getMaxViewportPosition(); + + Window scroll(int direction); + + int getViewportHeight(); + + Window setViewportHeight(int height); + + String getTitle(); + + Window setTitle(String title); + + boolean hasElement(int position, int row); + + Window setElement(int position, int row, Element e); + + Element getElement(int position, int row); + + Player getViewer(); + + Window reopen(); + + Window onClosed(art.arcane.volmlib.util.scheduling.Callback window); +} diff --git a/core/src/main/java/art/arcane/iris/util/common/inventorygui/WindowResolution.java b/core/src/main/java/art/arcane/iris/util/common/inventorygui/WindowResolution.java new file mode 100644 index 000000000..5ddf3595e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/inventorygui/WindowResolution.java @@ -0,0 +1,43 @@ +package art.arcane.iris.util.common.inventorygui; + +import org.bukkit.event.inventory.InventoryType; + +public enum WindowResolution { + W9_H6(9, 6, InventoryType.CHEST), + W5_H1(5, 1, InventoryType.HOPPER), + W3_H3(3, 3, InventoryType.DROPPER); + + private final int width; + private final int maxHeight; + private final InventoryType type; + + WindowResolution(int w, int h, InventoryType type) { + this.width = w; + this.maxHeight = h; + this.type = type; + } + + public int getMaxWidthOffset() { + return (getWidth() - 1) / 2; + } + + public int getWidth() { + return width; + } + + public int getMaxHeight() { + return maxHeight; + } + + public InventoryType getType() { + return type; + } + + public art.arcane.volmlib.util.inventorygui.WindowResolution toShared() { + return art.arcane.volmlib.util.inventorygui.WindowResolution.valueOf(name()); + } + + public static WindowResolution fromShared(art.arcane.volmlib.util.inventorygui.WindowResolution resolution) { + return valueOf(resolution.name()); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/math/AxisAlignedBB.java b/core/src/main/java/art/arcane/iris/util/common/math/AxisAlignedBB.java similarity index 81% rename from core/src/main/java/com/volmit/iris/util/math/AxisAlignedBB.java rename to core/src/main/java/art/arcane/iris/util/common/math/AxisAlignedBB.java index c7fe7a006..a17c01b37 100644 --- a/core/src/main/java/com/volmit/iris/util/math/AxisAlignedBB.java +++ b/core/src/main/java/art/arcane/iris/util/common/math/AxisAlignedBB.java @@ -16,10 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.math; +package art.arcane.iris.util.common.math; -import com.volmit.iris.engine.object.IrisPosition; -import com.volmit.iris.util.data.Cuboid; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.volmlib.util.math.AlignedPoint; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.volmlib.util.data.Cuboid; import org.bukkit.World; import org.bukkit.util.BlockVector; @@ -56,14 +58,6 @@ public class AxisAlignedBB { return new AxisAlignedBB(min().add(new IrisPosition((int) x, (int) y, (int) z)), max().add(new IrisPosition((int) x, (int) y, (int) z))); } - public boolean contains(AlignedPoint p) { - return p.getX() >= xa && p.getX() <= xb && p.getY() >= ya && p.getZ() <= yb && p.getZ() >= za && p.getZ() <= zb; - } - - public boolean contains(IrisPosition p) { - return p.getX() >= xa && p.getX() <= xb && p.getY() >= ya && p.getZ() <= yb && p.getZ() >= za && p.getZ() <= zb; - } - public boolean intersects(AxisAlignedBB s) { return this.xb >= s.xa && this.yb >= s.ya && this.zb >= s.za && s.xb >= this.xa && s.yb >= this.ya && s.zb >= this.za; } @@ -86,6 +80,6 @@ public class AxisAlignedBB { } public Cuboid toCuboid(World world) { - return new Cuboid(min().toLocation(world), max().toLocation(world)); + return new Cuboid(BukkitPlatform.toLocation(min(), world), BukkitPlatform.toLocation(max(), world)); } -} \ No newline at end of file +} diff --git a/core/src/main/java/art/arcane/iris/util/common/math/ChunkSpiral.java b/core/src/main/java/art/arcane/iris/util/common/math/ChunkSpiral.java new file mode 100644 index 000000000..a24a75d47 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/math/ChunkSpiral.java @@ -0,0 +1,44 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.math; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public final class ChunkSpiral { + private ChunkSpiral() { + } + + public static List centerOut(int centerX, int centerZ, int radius) { + List targets = new ArrayList<>(); + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + targets.add(new int[]{centerX + dx, centerZ + dz}); + } + } + + targets.sort(Comparator.comparingInt((int[] t) -> { + int ox = t[0] - centerX; + int oz = t[1] - centerZ; + return ox * ox + oz * oz; + })); + return targets; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/math/Direction.java b/core/src/main/java/art/arcane/iris/util/common/math/Direction.java new file mode 100644 index 000000000..d33ab9f5a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/math/Direction.java @@ -0,0 +1,158 @@ +package art.arcane.iris.util.common.math; + +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.data.Cuboid.CuboidDirection; +import org.bukkit.Axis; +import org.bukkit.block.BlockFace; +import org.bukkit.util.Vector; + +public enum Direction { + U(art.arcane.volmlib.util.math.DirectionBasis.U, CuboidDirection.Up), + D(art.arcane.volmlib.util.math.DirectionBasis.D, CuboidDirection.Down), + N(art.arcane.volmlib.util.math.DirectionBasis.N, CuboidDirection.North), + S(art.arcane.volmlib.util.math.DirectionBasis.S, CuboidDirection.South), + E(art.arcane.volmlib.util.math.DirectionBasis.E, CuboidDirection.East), + W(art.arcane.volmlib.util.math.DirectionBasis.W, CuboidDirection.West); + + private final art.arcane.volmlib.util.math.DirectionBasis basis; + private final CuboidDirection f; + + Direction(art.arcane.volmlib.util.math.DirectionBasis basis, CuboidDirection f) { + this.basis = basis; + this.f = f; + } + + private static Direction fromBasis(art.arcane.volmlib.util.math.DirectionBasis b) { + if (b == null) { + return null; + } + + return switch (b) { + case U -> U; + case D -> D; + case N -> N; + case S -> S; + case E -> E; + case W -> W; + }; + } + + public static Direction getDirection(BlockFace f) { + return fromBasis(art.arcane.volmlib.util.math.DirectionBasis.getDirection(f)); + } + + public static Direction closest(Vector v) { + return fromBasis(art.arcane.volmlib.util.math.DirectionBasis.closest(v)); + } + + public static Direction closest(Vector v, Direction... d) { + double m = Double.MAX_VALUE; + Direction s = null; + + for (Direction i : d) { + Vector x = i.toVector(); + double g = x.distance(v); + + if (g < m) { + m = g; + s = i; + } + } + + return s; + } + + public static Direction closest(Vector v, KList d) { + double m = Double.MAX_VALUE; + Direction s = null; + + for (Direction i : d) { + Vector x = i.toVector(); + double g = x.distance(v); + + if (g < m) { + m = g; + s = i; + } + } + + return s; + } + + public static KList news() { + return new KList().add(N, E, W, S); + } + + public static Direction getDirection(Vector v) { + return fromBasis(art.arcane.volmlib.util.math.DirectionBasis.getDirection(v)); + } + + public static KList udnews() { + return new KList().add(U, D, N, E, W, S); + } + + public static Direction fromByte(byte b) { + return fromBasis(art.arcane.volmlib.util.math.DirectionBasis.fromByte(b)); + } + + public static void calculatePermutations() { + art.arcane.volmlib.util.math.DirectionBasis.calculatePermutations(); + } + + @Override + public String toString() { + return basis.toString(); + } + + public boolean isVertical() { + return basis.isVertical(); + } + + public Vector toVector() { + return basis.toVector(); + } + + public boolean isCrooked(Direction to) { + if (equals(to.reverse())) { + return false; + } + + return !equals(to); + } + + public Vector angle(Vector initial, Direction d) { + return basis.angle(initial, d.basis); + } + + public Direction reverse() { + return fromBasis(basis.reverse()); + } + + public int x() { + return basis.x(); + } + + public int y() { + return basis.y(); + } + + public int z() { + return basis.z(); + } + + public CuboidDirection f() { + return f; + } + + public byte byteValue() { + return basis.byteValue(); + } + + public BlockFace getFace() { + return basis.getFace(); + } + + public Axis getAxis() { + return basis.getAxis(); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/math/IrisBlockVector.java b/core/src/main/java/art/arcane/iris/util/common/math/IrisBlockVector.java new file mode 100644 index 000000000..288fb8e87 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/math/IrisBlockVector.java @@ -0,0 +1,34 @@ +package art.arcane.iris.util.common.math; + +public class IrisBlockVector extends IrisVector { + public IrisBlockVector() { + super(); + } + + public IrisBlockVector(int x, int y, int z) { + super(x, y, z); + } + + public IrisBlockVector(double x, double y, double z) { + super(x, y, z); + } + + @Override + public IrisBlockVector clone() { + return (IrisBlockVector) super.clone(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IrisBlockVector other)) { + return false; + } + + return (int) other.getX() == (int) this.x && (int) other.getY() == (int) this.y && (int) other.getZ() == (int) this.z; + } + + @Override + public int hashCode() { + return (Integer.hashCode((int) x) >> 13) ^ (Integer.hashCode((int) y) >> 7) ^ Integer.hashCode((int) z); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/math/IrisVector.java b/core/src/main/java/art/arcane/iris/util/common/math/IrisVector.java new file mode 100644 index 000000000..49c125ac4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/math/IrisVector.java @@ -0,0 +1,163 @@ +package art.arcane.iris.util.common.math; + +public class IrisVector implements Cloneable { + private static final double EPSILON = 0.000001; + protected double x; + protected double y; + protected double z; + + public IrisVector() { + this.x = 0; + this.y = 0; + this.z = 0; + } + + public IrisVector(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public IrisVector(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public int getBlockX() { + return floor(x); + } + + public int getBlockY() { + return floor(y); + } + + public int getBlockZ() { + return floor(z); + } + + public IrisVector setX(double x) { + this.x = x; + return this; + } + + public IrisVector setY(double y) { + this.y = y; + return this; + } + + public IrisVector setZ(double z) { + this.z = z; + return this; + } + + public IrisVector add(IrisVector vec) { + x += vec.x; + y += vec.y; + z += vec.z; + return this; + } + + public IrisVector subtract(IrisVector vec) { + x -= vec.x; + y -= vec.y; + z -= vec.z; + return this; + } + + public IrisVector multiply(double m) { + x *= m; + y *= m; + z *= m; + return this; + } + + public double distance(IrisVector o) { + return Math.sqrt(square(x - o.x) + square(y - o.y) + square(z - o.z)); + } + + public double distanceSquared(IrisVector o) { + return square(x - o.x) + square(y - o.y) + square(z - o.z); + } + + public IrisVector rotateAroundX(double angle) { + double angleCos = Math.cos(angle); + double angleSin = Math.sin(angle); + double y = angleCos * this.y - angleSin * this.z; + double z = angleSin * this.y + angleCos * this.z; + return setY(y).setZ(z); + } + + public IrisVector rotateAroundY(double angle) { + double angleCos = Math.cos(angle); + double angleSin = Math.sin(angle); + double x = angleCos * this.x + angleSin * this.z; + double z = -angleSin * this.x + angleCos * this.z; + return setX(x).setZ(z); + } + + public IrisVector rotateAroundZ(double angle) { + double angleCos = Math.cos(angle); + double angleSin = Math.sin(angle); + double x = angleCos * this.x - angleSin * this.y; + double y = angleSin * this.x + angleCos * this.y; + return setX(x).setY(y); + } + + public IrisBlockVector toBlockVector() { + return new IrisBlockVector(x, y, z); + } + + @Override + public IrisVector clone() { + try { + return (IrisVector) super.clone(); + } catch (CloneNotSupportedException e) { + throw new Error(e); + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IrisVector other)) { + return false; + } + + return Math.abs(x - other.x) < EPSILON && Math.abs(y - other.y) < EPSILON && Math.abs(z - other.z) < EPSILON && this.getClass().equals(obj.getClass()); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 79 * hash + (int) (Double.doubleToLongBits(this.x) ^ (Double.doubleToLongBits(this.x) >>> 32)); + hash = 79 * hash + (int) (Double.doubleToLongBits(this.y) ^ (Double.doubleToLongBits(this.y) >>> 32)); + hash = 79 * hash + (int) (Double.doubleToLongBits(this.z) ^ (Double.doubleToLongBits(this.z) >>> 32)); + return hash; + } + + @Override + public String toString() { + return x + "," + y + "," + z; + } + + protected static int floor(double num) { + int floor = (int) num; + return floor == num ? floor : floor - (int) (Double.doubleToRawLongBits(num) >>> 63); + } + + private static double square(double num) { + return num * num; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/math/Vector3i.java b/core/src/main/java/art/arcane/iris/util/common/math/Vector3i.java new file mode 100644 index 000000000..9051e0c07 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/math/Vector3i.java @@ -0,0 +1,65 @@ +package art.arcane.iris.util.common.math; + +public class Vector3i implements Cloneable { + private final int x; + private final int y; + private final int z; + + public Vector3i(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getZ() { + return z; + } + + public int getBlockX() { + return x; + } + + public int getBlockY() { + return y; + } + + public int getBlockZ() { + return z; + } + + @Override + public Vector3i clone() { + try { + return (Vector3i) super.clone(); + } catch (CloneNotSupportedException e) { + throw new Error(e); + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Vector3i other)) { + return false; + } + + return x == other.x && y == other.y && z == other.z; + } + + @Override + public int hashCode() { + return x ^ (z << 12) ^ (y << 24); + } + + @Override + public String toString() { + return x + "," + y + "," + z; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/math/VectorMath.java b/core/src/main/java/art/arcane/iris/util/common/math/VectorMath.java new file mode 100644 index 000000000..df7425321 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/math/VectorMath.java @@ -0,0 +1,78 @@ +package art.arcane.iris.util.common.math; + +import org.bukkit.util.Vector; + +public class VectorMath extends art.arcane.volmlib.util.math.VectorMath { + public static Vector rotate(Direction current, Direction to, Vector v) { + if (current.equals(to)) { + return v; + } else if (current.equals(to.reverse())) { + if (current.isVertical()) { + return new Vector(v.getX(), -v.getY(), v.getZ()); + } else { + return new Vector(-v.getX(), v.getY(), -v.getZ()); + } + } else { + Vector c = current.toVector().clone().add(to.toVector()); + + if (c.getX() == 0) { + if (c.getY() != c.getZ()) { + return rotate90CX(v); + } + + return rotate90CCX(v); + } else if (c.getY() == 0) { + if (c.getX() != c.getZ()) { + return rotate90CY(v); + } + + return rotate90CCY(v); + } else if (c.getZ() == 0) { + if (c.getX() != c.getY()) { + return rotate90CZ(v); + } + + return rotate90CCZ(v); + } + } + + return v; + } + + public static Vector rotate(Direction current, Direction to, Vector v, int w, int h, int d) { + if (current.equals(to)) { + return v; + } else if (current.equals(to.reverse())) { + if (current.isVertical()) { + return new Vector(v.getX(), -v.getY() + h, v.getZ()); + } else { + return new Vector(-v.getX() + w, v.getY(), -v.getZ() + d); + } + } else { + Vector c = current.toVector().clone().add(to.toVector()); + + if (c.getX() == 0) { + if (c.getY() != c.getZ()) { + return rotate90CX(v, d); + } + + return rotate90CCX(v, h); + } else if (c.getY() == 0) { + if (c.getX() != c.getZ()) { + return rotate90CY(v, d); + } + + return rotate90CCY(v, w); + } else if (c.getZ() == 0) { + if (c.getX() != c.getY()) { + return rotate90CZ(v, w); + } + + return rotate90CCZ(v, h); + } + } + + return v; + } + +} diff --git a/core/src/main/java/com/volmit/iris/util/misc/Bindings.java b/core/src/main/java/art/arcane/iris/util/common/misc/Bindings.java similarity index 75% rename from core/src/main/java/com/volmit/iris/util/misc/Bindings.java rename to core/src/main/java/art/arcane/iris/util/common/misc/Bindings.java index d26e924dc..7c5c376f4 100644 --- a/core/src/main/java/com/volmit/iris/util/misc/Bindings.java +++ b/core/src/main/java/art/arcane/iris/util/common/misc/Bindings.java @@ -1,20 +1,23 @@ -package com.volmit.iris.util.misc; +package art.arcane.iris.util.common.misc; +import art.arcane.iris.util.project.sentry.Attachments; +import art.arcane.iris.util.project.sentry.IrisLogger; +import art.arcane.iris.util.project.sentry.ServerID; import com.google.gson.JsonSyntaxException; -import com.volmit.iris.BuildConstants; -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.safeguard.IrisSafeguard; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.json.JSONException; -import com.volmit.iris.util.reflect.ShadeFix; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.sentry.Attachments; -import com.volmit.iris.util.sentry.IrisLogger; -import com.volmit.iris.util.sentry.ServerID; +import art.arcane.iris.BuildConstants; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.safeguard.IrisSafeguard; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.json.JSONException; +import art.arcane.volmlib.util.reflect.ShadeFix; +import art.arcane.iris.util.common.plugin.VolmitPlugin; +import art.arcane.iris.util.common.scheduling.J; + import io.sentry.Sentry; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.platform.bukkit.BukkitAudiences; @@ -25,6 +28,7 @@ import org.bstats.charts.SingleLineChart; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; import java.util.HashMap; import java.util.Map; @@ -40,8 +44,8 @@ public class Bindings { public static void setupSentry() { var settings = IrisSettings.get().getSentry(); if (settings.disableAutoReporting || Sentry.isEnabled() || Boolean.getBoolean("iris.suppressReporting")) return; - Iris.info("Enabling Sentry for anonymous error reporting. You can disable this in the settings."); - Iris.info("Your server ID is: " + ServerID.ID); + IrisLogging.info("Enabling Sentry for anonymous error reporting. You can disable this in the settings."); + IrisLogging.info("Your server ID is: " + ServerID.getId()); Sentry.init(options -> { options.setDsn("http://4cdbb9ac953306529947f4ca1e8e6b26@sentry.volmit.com:8080/2"); @@ -52,7 +56,7 @@ public class Bindings { options.setAttachServerName(false); options.setEnableUncaughtExceptionHandler(false); - options.setRelease(Iris.instance.getDescription().getVersion()); + options.setRelease(BukkitPlatform.plugin().getDescription().getVersion()); options.setEnvironment(BuildConstants.ENVIRONMENT); options.setBeforeSend((event, hint) -> { if (suppress(event.getThrowable())) return null; @@ -82,9 +86,15 @@ public class Bindings { } - public static void setupBstats(Iris plugin) { + // bstats.org plugin id + private static final int BSTATS_PLUGIN_ID = 24220; + + public static void setupBstats(VolmitPlugin plugin) { + if (BSTATS_PLUGIN_ID <= 0) { + return; + } J.s(() -> { - var metrics = new Metrics(plugin, 24220); + var metrics = new Metrics(plugin, BSTATS_PLUGIN_ID); metrics.addCustomChart(new SingleLineChart("custom_dimensions", () -> Bukkit.getWorlds() .stream() .filter(IrisToolbelt::isIrisWorld) @@ -117,7 +127,7 @@ public class Bindings { public static class Adventure { private final BukkitAudiences audiences; - public Adventure(Iris plugin) { + public Adventure(Plugin plugin) { ShadeFix.fix(ComponentSerializer.class); this.audiences = BukkitAudiences.create(plugin); } diff --git a/core/src/main/java/art/arcane/iris/util/common/misc/ServerProperties.java b/core/src/main/java/art/arcane/iris/util/common/misc/ServerProperties.java new file mode 100644 index 000000000..6f499eea8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/misc/ServerProperties.java @@ -0,0 +1,33 @@ +package art.arcane.iris.util.common.misc; + +import art.arcane.iris.core.lifecycle.BukkitStartupPaths; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +public class ServerProperties { + public static final Properties DATA = new Properties(); + public static final File SERVER_PROPERTIES; + public static final File BUKKIT_YML; + + public static final String LEVEL_NAME; + + static { + BukkitStartupPaths startupPaths; + try { + startupPaths = BukkitStartupPaths.resolveCurrent(); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + SERVER_PROPERTIES = startupPaths.serverProperties().toFile(); + BUKKIT_YML = startupPaths.bukkitConfiguration().toFile(); + try (FileInputStream in = new FileInputStream(SERVER_PROPERTIES)) { + DATA.load(in); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + LEVEL_NAME = startupPaths.levelName(); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/misc/SlimJar.java b/core/src/main/java/art/arcane/iris/util/common/misc/SlimJar.java new file mode 100644 index 000000000..00d18bbc5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/misc/SlimJar.java @@ -0,0 +1,72 @@ +package art.arcane.iris.util.common.misc; + +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.VolmitPlugin; +import io.github.slimjar.app.builder.ApplicationBuilder; +import io.github.slimjar.app.builder.SpigotApplicationBuilder; +import io.github.slimjar.injector.loader.factory.InjectableFactory; +import io.github.slimjar.logging.ProcessLogger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Logger; + +public class SlimJar { + private static final boolean DEBUG = Boolean.getBoolean("iris.debug-slimjar"); + + private static final ReentrantLock lock = new ReentrantLock(); + private static final AtomicBoolean loaded = new AtomicBoolean(); + + public static void load() { + if (loaded.get()) return; + lock.lock(); + + try { + if (loaded.getAndSet(true)) return; + VolmitPlugin plugin = BukkitPlatform.volmitPlugin(); + Path downloadPath = plugin.getDataFolder("cache", "libraries").toPath(); + Logger logger = plugin.getLogger(); + + logger.info("Loading libraries..."); + try { + new SpigotApplicationBuilder(plugin) + .downloadDirectoryPath(downloadPath) + .debug(DEBUG) + .build(); + } catch (Throwable e) { + // The Spigot builder is a probe: not every server exposes it, and the fallback is the + // supported path on the ones that do not. + IrisLogging.info("Failed to inject the library loader, falling back to application builder"); + ApplicationBuilder.appending(plugin.getName()) + .injectableFactory(InjectableFactory.selecting(InjectableFactory.ERROR, InjectableFactory.INJECTABLE, InjectableFactory.WRAPPED, InjectableFactory.UNSAFE)) + .downloadDirectoryPath(downloadPath) + .logger(new ProcessLogger() { + @Override + public void info(@NotNull String message, @Nullable Object... args) { + if (!DEBUG) return; + plugin.getLogger().info(message.formatted(args)); + } + + @Override + public void error(@NotNull String message, @Nullable Object... args) { + plugin.getLogger().severe(message.formatted(args)); + } + + @Override + public void debug(@NotNull String message, @Nullable Object... args) { + if (!DEBUG) return; + plugin.getLogger().info(message.formatted(args)); + } + }) + .build(); + } + logger.info("Libraries loaded successfully!"); + } finally { + lock.unlock(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/misc/WebCache.java b/core/src/main/java/art/arcane/iris/util/common/misc/WebCache.java new file mode 100644 index 000000000..91e7013b4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/misc/WebCache.java @@ -0,0 +1,275 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.misc; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.io.IO; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * Download cache helpers over the platform data folder. + * + *

Every request is bounded: URL.openStream had no connect or read timeout, so a hung mirror parked the + * calling thread (a command thread, or the boot pack prefetch) forever. + */ +public final class WebCache { + private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10L); + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(120L); + private static final int BUFFER_SIZE = 8192; + private static final long PROGRESS_INTERVAL_NANOS = Duration.ofMillis(250L).toNanos(); + private static final TransferProgressListener NO_TRANSFER_PROGRESS = progress -> { + }; + + private static volatile HttpClient client; + + private WebCache() { + } + + public static File getTemp() { + return IrisPlatforms.get().dataFolder("cache", "temp"); + } + + public static File getCached(String name, String url) { + String h = IO.hash(name + "@" + url); + File f = IrisPlatforms.get().dataFile("cache", h.substring(0, 2), h.substring(3, 5), h); + + if (!f.exists()) { + download(name, url, f); + } + + return f.exists() ? f : null; + } + + public static String getNonCached(String name, String url) { + String h = IO.hash(name + "*" + url); + File f = IrisPlatforms.get().dataFile("cache", h.substring(0, 2), h.substring(3, 5), h); + + if (!download(name, url, f)) { + return ""; + } + try { + return Files.readString(f.toPath(), StandardCharsets.UTF_8); + } catch (IOException e) { + IrisLogging.reportError(e); + return ""; + } + } + + public static File getNonCachedFile(String name, String url) { + return getNonCachedFile(name, url, Long.MAX_VALUE); + } + + public static File getNonCachedFile(String name, String url, long maxBytes) { + return getNonCachedFile(name, url, maxBytes, NO_TRANSFER_PROGRESS); + } + + public static File getNonCachedFile(String name, String url, TransferProgressListener progressListener) { + return getNonCachedFile(name, url, Long.MAX_VALUE, progressListener); + } + + public static File getNonCachedFile(String name, String url, long maxBytes, + TransferProgressListener progressListener) { + String h = IO.hash(name + "*" + url); + File f = IrisPlatforms.get().dataFile("cache", h.substring(0, 2), h.substring(3, 5), h); + IrisLogging.debug("Download " + name); + return download(name, url, f, maxBytes, progressListener) ? f : null; + } + + private static boolean download(String name, String url, File target) { + return download(name, url, target, Long.MAX_VALUE, NO_TRANSFER_PROGRESS); + } + + private static boolean download(String name, String url, File target, long maxBytes) { + return download(name, url, target, maxBytes, NO_TRANSFER_PROGRESS); + } + + private static boolean download(String name, String url, File target, long maxBytes, + TransferProgressListener progressListener) { + if (maxBytes < 1L) { + throw new IllegalArgumentException("Download size limit must be positive."); + } + TransferProgressListener progress = progressListener == null ? NO_TRANSFER_PROGRESS : progressListener; + HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + .timeout(REQUEST_TIMEOUT) + .GET() + .build(); + Path staged = null; + try { + checkInterrupted(); + HttpResponse response = client() + .send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() / 100 != 2) { + response.body().close(); + IrisLogging.reportError(new IOException("HTTP " + response.statusCode() + + " downloading " + name)); + return false; + } + long declaredBytes = response.headers().firstValueAsLong("Content-Length").orElse(-1L); + if (declaredBytes > maxBytes) { + response.body().close(); + throw new IOException("Download exceeds the size limit for " + name + "."); + } + Path destination = target.toPath().toAbsolutePath().normalize(); + Path parent = destination.getParent(); + if (parent == null) { + response.body().close(); + throw new IOException("Download target has no parent: " + destination); + } + Files.createDirectories(parent); + staged = Files.createTempFile(parent, ".download-", ".tmp"); + long startedNanos = System.nanoTime(); + long lastProgressNanos = startedNanos; + sendProgress(progress, new TransferProgress(0L, declaredBytes, 0L, false)); + long downloadedBytes = 0L; + try (InputStream in = response.body(); + OutputStream out = Files.newOutputStream(staged, StandardOpenOption.WRITE)) { + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + while (true) { + checkInterrupted(); + read = in.read(buffer); + if (read == -1) { + break; + } + checkInterrupted(); + if (read > maxBytes - downloadedBytes) { + throw new IOException("Download exceeds the size limit for " + name + "."); + } + out.write(buffer, 0, read); + downloadedBytes += read; + long currentNanos = System.nanoTime(); + if (currentNanos - lastProgressNanos >= PROGRESS_INTERVAL_NANOS) { + sendProgress(progress, new TransferProgress( + downloadedBytes, + declaredBytes, + elapsedMillis(startedNanos, currentNanos), + false + )); + lastProgressNanos = currentNanos; + } + } + out.flush(); + } + sendProgress(progress, new TransferProgress( + downloadedBytes, + declaredBytes, + elapsedMillis(startedNanos), + true + )); + checkInterrupted(); + try { + Files.move(staged, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + Files.move(staged, destination, StandardCopyOption.REPLACE_EXISTING); + } + staged = null; + return true; + } catch (InterruptedIOException e) { + if (Thread.currentThread().isInterrupted()) { + IrisLogging.debug("Download interrupted for " + name); + } else { + IrisLogging.reportError(e); + } + return false; + } catch (IOException e) { + if (Thread.currentThread().isInterrupted()) { + IrisLogging.debug("Download interrupted for " + name); + return false; + } + IrisLogging.reportError(e); + return false; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + IrisLogging.debug("Download interrupted for " + name); + return false; + } finally { + if (staged != null) { + try { + Files.deleteIfExists(staged); + } catch (IOException cleanupFailure) { + IrisLogging.reportError("Failed to clean incomplete download " + staged + ".", cleanupFailure); + } + } + } + } + + private static void checkInterrupted() throws InterruptedIOException { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedIOException("Download interrupted."); + } + } + + private static long elapsedMillis(long startedNanos) { + return elapsedMillis(startedNanos, System.nanoTime()); + } + + private static long elapsedMillis(long startedNanos, long currentNanos) { + return TimeUnit.NANOSECONDS.toMillis(Math.max(0L, currentNanos - startedNanos)); + } + + private static void sendProgress(TransferProgressListener listener, TransferProgress progress) { + try { + listener.onProgress(progress); + } catch (RuntimeException exception) { + IrisLogging.reportError("Download progress delivery failed", exception); + } + } + + private static HttpClient client() { + HttpClient current = client; + if (current != null) { + return current; + } + synchronized (WebCache.class) { + if (client == null) { + client = HttpClient.newBuilder() + .connectTimeout(CONNECT_TIMEOUT) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + } + return client; + } + } + + public record TransferProgress(long transferredBytes, long contentLength, long elapsedMillis, boolean complete) { + } + + @FunctionalInterface + public interface TransferProgressListener { + void onProgress(TransferProgress progress); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/misc/getHardware.java b/core/src/main/java/art/arcane/iris/util/common/misc/getHardware.java similarity index 93% rename from core/src/main/java/com/volmit/iris/util/misc/getHardware.java rename to core/src/main/java/art/arcane/iris/util/common/misc/getHardware.java index 0e494e27c..0c470beaf 100644 --- a/core/src/main/java/com/volmit/iris/util/misc/getHardware.java +++ b/core/src/main/java/art/arcane/iris/util/common/misc/getHardware.java @@ -1,16 +1,18 @@ -package com.volmit.iris.util.misc; +package art.arcane.iris.util.common.misc; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import org.jetbrains.annotations.Nullable; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.format.Form; import oshi.SystemInfo; -import oshi.hardware.*; -import oshi.software.os.FileSystem; +import oshi.hardware.CentralProcessor; +import oshi.hardware.Display; +import oshi.hardware.GraphicsCard; +import oshi.hardware.HWDiskStore; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.PowerSource; import oshi.software.os.OSFileStore; import oshi.software.os.OperatingSystem; -import oshi.util.EdidUtil; import java.net.InetAddress; import java.net.NetworkInterface; @@ -18,7 +20,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.List; -import java.util.stream.Collectors; public class getHardware { public static String getServerOS() { @@ -153,7 +154,7 @@ public class getHardware { for (Display display : displays) { systemEDID.add("Display: " + display.getEdid()); } - if (!systemEDID.isEmpty()) { + if (systemEDID.isEmpty()) { systemEDID.add("No displays"); } return systemEDID.copy(); @@ -173,8 +174,8 @@ public class getHardware { for (InetAddress ia : Collections.list(inetAddresses)) { interfaces.add("IP: %s", ia.getHostAddress()); } - return interfaces.copy(); } + return interfaces.copy(); } catch (Exception e) { e.printStackTrace(); } diff --git a/core/src/main/java/art/arcane/iris/util/common/nbt/mca/Chunk.java b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/Chunk.java new file mode 100644 index 000000000..4c0f853eb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/Chunk.java @@ -0,0 +1,60 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.nbt.common.mca; + +import art.arcane.volmlib.util.nbt.mca.MCAChunkSupport; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.nms.INMS; + +import static org.bukkit.Bukkit.getServer; + +public class Chunk extends MCAChunkSupport

{ + public static final int DEFAULT_DATA_VERSION = 2730; + + private static final Config
CONFIG = new Config<>( + DEFAULT_DATA_VERSION, + () -> getServer().getWorlds().get(0).getMinHeight(), + () -> getServer().getWorlds().get(0).getMaxHeight(), + (min, max) -> INMS.get().newBiomeContainer(min, max), + (min, max, data) -> INMS.get().newBiomeContainer(min, max, data), + (sectionRoot, dataVersion, loadFlags) -> new Section(sectionRoot, dataVersion, loadFlags), + Section::newSection, + () -> "Iris Headless " + BukkitPlatform.plugin().getDescription().getVersion(), + () -> "Iris " + BukkitPlatform.plugin().getDescription().getVersion() + ); + + Chunk(int lastMCAUpdate) { + super(lastMCAUpdate, CONFIG); + } + + public Chunk(CompoundTag data) { + super(data, CONFIG); + } + + public static Chunk newChunk() { + Chunk c = new Chunk(0); + c.initializeNewChunk(); + return c; + } + + public static void injectIrisData(Chunk c) { + c.injectNativeData("Iris"); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/nbt/mca/MCAFile.java b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/MCAFile.java new file mode 100644 index 000000000..b8970740f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/MCAFile.java @@ -0,0 +1,89 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.nbt.common.mca; + +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.nbt.mca.MCAFileSupport; +import art.arcane.volmlib.util.nbt.mca.LoadFlags; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.iris.util.common.scheduling.J; + +import java.io.IOException; +import java.io.RandomAccessFile; + +@SuppressWarnings("ALL") +public class MCAFile extends MCAFileSupport { + + /** + * The default chunk data version used when no custom version is supplied. + */ + public static final int DEFAULT_DATA_VERSION = 1628; + + /** + * MCAFile represents a world save file used by Minecraft to store world + * data on the hard drive. + * This constructor needs the x- and z-coordinates of the stored region, + * which can usually be taken from the file name {@code r.x.z.mca} + * + * @param regionX The x-coordinate of this region. + * @param regionZ The z-coordinate of this region. + */ + public MCAFile(int regionX, int regionZ) { + super(regionX, regionZ, Chunk::newChunk, task -> J.a(task, 20)); + } + + /** + * Calculates the index of a chunk from its x- and z-coordinates in this region. + * This works with absolute and relative coordinates. + * + * @param chunkX The x-coordinate of the chunk. + * @param chunkZ The z-coordinate of the chunk. + * @return The index of this chunk. + */ + public static int getChunkIndex(int chunkX, int chunkZ) { + return MCAFileSupport.getChunkIndex(chunkX, chunkZ); + } + + /** + * Reads an .mca file from a {@code RandomAccessFile} into this object. + * This method does not perform any cleanups on the data. + * + * @param raf The {@code RandomAccessFile} to read from. + * @throws IOException If something went wrong during deserialization. + */ + public void deserialize(RandomAccessFile raf) throws IOException { + deserialize(raf, LoadFlags.ALL_DATA); + } + + /** + * Reads an .mca file from a {@code RandomAccessFile} into this object. + * This method does not perform any cleanups on the data. + * + * @param raf The {@code RandomAccessFile} to read from. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException If something went wrong during deserialization. + */ + public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { + super.deserialize(raf, loadFlags, Chunk::new); + } + + public KList samplePositions(RandomAccessFile raf) throws IOException { + return super.samplePositions(raf, Position2::new); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/MCAUtil.java b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/MCAUtil.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/nbt/mca/MCAUtil.java rename to core/src/main/java/art/arcane/iris/util/common/nbt/mca/MCAUtil.java index da97e694b..19ed5119f 100644 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/MCAUtil.java +++ b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/MCAUtil.java @@ -16,18 +16,18 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.nbt.mca; +package art.arcane.iris.util.nbt.common.mca; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.Position2; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.nbt.mca.LoadFlags; +import art.arcane.volmlib.util.nbt.mca.MCAUtilSupport; +import art.arcane.volmlib.util.math.Position2; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.file.Files; import java.nio.file.StandardCopyOption; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Provides main and utility functions to read and writeNodeData .mca files and @@ -35,8 +35,6 @@ import java.util.regex.Pattern; */ public final class MCAUtil { - private static final Pattern mcaFilePattern = Pattern.compile("^.*r\\.(?-?\\d+)\\.(?-?\\d+)\\.mca$"); - private MCAUtil() { } @@ -175,7 +173,7 @@ public final class MCAUtil { * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" */ public static String createNameFromChunkLocation(int chunkX, int chunkZ) { - return createNameFromRegionLocation(chunkToRegion(chunkX), chunkToRegion(chunkZ)); + return MCAUtilSupport.createNameFromChunkLocation(chunkX, chunkZ); } /** @@ -187,7 +185,7 @@ public final class MCAUtil { * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" */ public static String createNameFromBlockLocation(int blockX, int blockZ) { - return createNameFromRegionLocation(blockToRegion(blockX), blockToRegion(blockZ)); + return MCAUtilSupport.createNameFromBlockLocation(blockX, blockZ); } /** @@ -198,7 +196,7 @@ public final class MCAUtil { * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" */ public static String createNameFromRegionLocation(int regionX, int regionZ) { - return "r." + regionX + "." + regionZ + ".mca"; + return MCAUtilSupport.createNameFromRegionLocation(regionX, regionZ); } /** @@ -208,7 +206,7 @@ public final class MCAUtil { * @return The chunk coordinate value. */ public static int blockToChunk(int block) { - return block >> 4; + return MCAUtilSupport.blockToChunk(block); } /** @@ -218,7 +216,7 @@ public final class MCAUtil { * @return The region coordinate value. */ public static int blockToRegion(int block) { - return block >> 9; + return MCAUtilSupport.blockToRegion(block); } /** @@ -228,7 +226,7 @@ public final class MCAUtil { * @return The region coordinate value. */ public static int chunkToRegion(int chunk) { - return chunk >> 5; + return MCAUtilSupport.chunkToRegion(chunk); } /** @@ -238,7 +236,7 @@ public final class MCAUtil { * @return The chunk coordinate value. */ public static int regionToChunk(int region) { - return region << 5; + return MCAUtilSupport.regionToChunk(region); } /** @@ -248,7 +246,7 @@ public final class MCAUtil { * @return The block coordinate value. */ public static int regionToBlock(int region) { - return region << 9; + return MCAUtilSupport.regionToBlock(region); } /** @@ -258,14 +256,11 @@ public final class MCAUtil { * @return The block coordinate value. */ public static int chunkToBlock(int chunk) { - return chunk << 4; + return MCAUtilSupport.chunkToBlock(chunk); } public static MCAFile newMCAFile(File file) { - Matcher m = mcaFilePattern.matcher(file.getName()); - if (m.find()) { - return new MCAFile(Integer.parseInt(m.group("regionX")), Integer.parseInt(m.group("regionZ"))); - } - throw new IllegalArgumentException("invalid mca file name: " + file.getName()); + int[] coords = MCAUtilSupport.parseRegionCoordinates(file); + return new MCAFile(coords[0], coords[1]); } } diff --git a/core/src/main/java/art/arcane/iris/util/common/nbt/mca/NBTWorld.java b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/NBTWorld.java new file mode 100644 index 000000000..9f916ce4f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/NBTWorld.java @@ -0,0 +1,257 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.nbt.common.mca; + +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.nbt.mca.MCAWorldStoreSupport; +import art.arcane.volmlib.util.nbt.mca.MCAWorldRuntimeSupport; +import art.arcane.volmlib.util.nbt.mca.NBTWorldSupport; +import art.arcane.iris.util.common.reflect.KeyedType; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.iris.util.common.parallel.HyperLock; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; + +import java.io.File; +import java.util.Map; +import java.util.Locale; + +public class NBTWorld { + private static final class Holder { + private static final BlockData AIR = BukkitBlockResolution.get("AIR"); + private static final NBTWorldSupport.BlockStateCodec BLOCK_STATE_CODEC = NBTWorldSupport.blockStateCodec( + blockStateString -> BukkitBlockResolution.getOrNull(blockStateString, true), + BukkitBlockResolution::getAir, + blockData -> blockData.getAsString(true), + blockData -> { + NamespacedKey key = KeyedType.getKey(blockData.getMaterial()); + if (key == null) { + return "minecraft:" + blockData.getMaterial().name().toLowerCase(Locale.ROOT); + } + return key.getNamespace() + ":" + key.getKey(); + } + ); + private static final Map BIOME_IDS = computeBiomeIDs(); + } + + private final HyperLock hyperLock = new HyperLock(); + private final MCAWorldStoreSupport regionStore; + private final MCAWorldRuntimeSupport worldRuntime; + + public NBTWorld(File worldFolder) { + this.regionStore = new MCAWorldStoreSupport<>(new MCAWorldStoreSupport.Config<>( + NBTWorldSupport.keyCodec(Cache::key, Cache::keyX, Cache::keyZ), + new MCAWorldStoreSupport.GridLock() { + @Override + public T withResult(int x, int z, java.util.function.Supplier supplier) { + return hyperLock.withResult(x, z, supplier::get); + } + }, + MCAFile::new, + NBTWorldSupport.regionFileResolver(worldFolder, "region"), + MCAUtil::write, + NBTWorldSupport.logger( + IrisLogging::info, + IrisLogging::debug, + (message, error) -> { + IrisLogging.error(message); + if (error != null) { + error.printStackTrace(); + } + } + ), + M::ms, + 60000L, + "Iris MCA Writer" + )); + + this.worldRuntime = new MCAWorldRuntimeSupport<>( + this::getMCA, + new MCAWorldRuntimeSupport.ChunkAccess<>() { + @Override + public Chunk getChunk(MCAFile mca, int chunkX, int chunkZ) { + return mca.getChunk(chunkX, chunkZ); + } + + @Override + public void setChunk(MCAFile mca, int chunkX, int chunkZ, Chunk chunk) { + mca.setChunk(chunkX, chunkZ, chunk); + } + + @Override + public Chunk createChunk() { + return Chunk.newChunk(); + } + }, + new MCAWorldRuntimeSupport.SectionAccess<>() { + @Override + public Section getSection(Chunk chunk, int sectionY) { + return chunk.getSection(sectionY); + } + + @Override + public void setSection(Chunk chunk, int sectionY, Section section) { + chunk.setSection(sectionY, section); + } + + @Override + public Section createSection() { + return Section.newSection(); + } + } + ); + } + + public static BlockData getBlockData(CompoundTag tag) { + return Holder.BLOCK_STATE_CODEC.decode(tag); + } + + public static CompoundTag getCompound(BlockData bd) { + return Holder.BLOCK_STATE_CODEC.encode(bd); + } + + private static Map computeBiomeIDs() { + Map biomeIds = new KMap<>(); + + for (Biome biome : Registry.BIOME) { + NamespacedKey key = biome.getKey(); + if (!key.getKey().equals("custom")) { + biomeIds.put(biome, INMS.get().getBiomeId(biome)); + } + } + + return biomeIds; + } + + public void close() { + regionStore.close(); + } + + public void flushNow() { + regionStore.flushNow(); + } + + public void queueSaveUnload(int x, int z) { + regionStore.queueSaveUnload(x, z); + } + + public void doSaveUnload(int x, int z) { + regionStore.doSaveUnload(x, z); + } + + public void save() { + regionStore.save(); + } + + public void queueSave() { + + } + + public synchronized void unloadRegion(int x, int z) { + regionStore.unloadRegion(x, z); + } + + public void saveRegion(int x, int z) { + regionStore.saveRegion(x, z); + } + + public void saveRegion(int x, int z, MCAFile mca) { + regionStore.saveRegion(x, z, mca); + } + + public boolean shouldUnload(int x, int z) { + return regionStore.shouldUnload(x, z); + } + + public File getRegionFile(int x, int z) { + return regionStore.getRegionFile(x, z); + } + + public BlockData getBlockData(int x, int y, int z) { + try { + CompoundTag tag = worldRuntime.getBlockStateTag(x, y, z); + + if (tag == null) { + return Holder.AIR; + } + + return getBlockData(tag); + } catch (Throwable e) { + IrisLogging.reportError(e); + + } + return Holder.AIR; + } + + public void setBlockData(int x, int y, int z, BlockData data) { + worldRuntime.setBlockStateTag(x, y, z, getCompound(data), false); + } + + public int getBiomeId(Biome b) { + return Holder.BIOME_IDS.get(b); + } + + public void setBiome(int x, int y, int z, Biome biome) { + worldRuntime.setBiomeId(x, y, z, Holder.BIOME_IDS.get(biome)); + } + + public Section getChunkSection(int x, int y, int z) { + return worldRuntime.getChunkSection(x, y, z); + } + + public Chunk getChunk(int x, int z) { + return worldRuntime.getChunk(x, z); + } + + public Chunk getChunk(MCAFile mca, int x, int z) { + return worldRuntime.getChunk(mca, x, z); + } + + public Chunk getNewChunk(MCAFile mca, int x, int z) { + return worldRuntime.getNewChunk(mca, x, z); + } + + public long getIdleDuration(int x, int z) { + return regionStore.getIdleDuration(x, z); + } + + public MCAFile getMCA(int x, int z) { + return regionStore.getMCA(x, z); + } + + public MCAFile getMCAOrNull(int x, int z) { + return regionStore.getMCAOrNull(x, z); + } + + public int size() { + return regionStore.size(); + } + + public boolean isLoaded(int x, int z) { + return regionStore.isLoaded(x, z); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/nbt/mca/Section.java b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/Section.java new file mode 100644 index 000000000..cf20bb3ce --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/nbt/mca/Section.java @@ -0,0 +1,138 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.nbt.common.mca; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.volmlib.util.nbt.mca.LoadFlags; +import art.arcane.volmlib.util.nbt.mca.MCASectionLike; +import art.arcane.volmlib.util.nbt.mca.MCASectionSupport; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +public class Section implements MCASectionLike { + private final MCASectionSupport support; + + public Section(CompoundTag sectionRoot, int dataVersion) { + this(sectionRoot, dataVersion, LoadFlags.ALL_DATA); + } + + public Section(CompoundTag sectionRoot, int dataVersion, long loadFlags) { + support = new MCASectionSupport(sectionRoot, loadFlags, () -> INMS.get().createPalette()); + } + + private Section(MCASectionSupport support) { + this.support = support; + } + + /** + * Creates an empty Section with base values. + * + * @return An empty Section + */ + public static Section newSection() { + return new Section(MCASectionSupport.createNew(() -> INMS.get().createPalette())); + } + + /** + * Checks whether the data of this Section is empty. + * + * @return true if empty + */ + public boolean isEmpty() { + return support.isEmpty(); + } + + /** + * Fetches a block state based on a block location from this section. + * The coordinates represent the location of the block inside of this Section. + * + * @param blockX The x-coordinate of the block in this Section + * @param blockY The y-coordinate of the block in this Section + * @param blockZ The z-coordinate of the block in this Section + * @return The block state data of this block. + */ + public synchronized CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { + return support.getBlockStateAt(blockX, blockY, blockZ); + } + + /** + * Attempts to add a block state for a specific block location in this Section. + * + * @param blockX The x-coordinate of the block in this Section + * @param blockY The y-coordinate of the block in this Section + * @param blockZ The z-coordinate of the block in this Section + * @param state The block state to be set + */ + public synchronized void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + support.setBlockStateAt(blockX, blockY, blockZ, state, cleanup); + } + + /** + * This method recalculates the palette and its indices. + * This should only be used moderately to avoid unnecessary recalculation of the palette indices. + * Recalculating the Palette should only be executed once right before saving the Section to file. + */ + public void cleanupPaletteAndBlockStates() { + support.cleanupPaletteAndBlockStates(); + } + + /** + * @return The block light array of this Section + */ + public synchronized byte[] getBlockLight() { + return support.getBlockLight(); + } + + /** + * Sets the block light array for this section. + * + * @param blockLight The block light array + * @throws IllegalArgumentException When the length of the array is not 2048 + */ + public synchronized void setBlockLight(byte[] blockLight) { + support.setBlockLight(blockLight); + } + + /** + * @return The sky light values of this Section + */ + public synchronized byte[] getSkyLight() { + return support.getSkyLight(); + } + + /** + * Sets the sky light values of this section. + * + * @param skyLight The custom sky light values + * @throws IllegalArgumentException If the length of the array is not 2048 + */ + public synchronized void setSkyLight(byte[] skyLight) { + support.setSkyLight(skyLight); + } + + /** + * Updates the raw CompoundTag that this Section is based on. + * This must be called before saving a Section to disk if the Section was manually created + * to set the Y of this Section. + * + * @param y The Y-value of this Section + * @return A reference to the raw CompoundTag this Section is based on + */ + public synchronized CompoundTag updateHandle(int y) { + return support.updateHandle(y); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/parallel/BurstExecutor.java b/core/src/main/java/art/arcane/iris/util/common/parallel/BurstExecutor.java new file mode 100644 index 000000000..534cbc169 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/parallel/BurstExecutor.java @@ -0,0 +1,16 @@ +package art.arcane.iris.util.common.parallel; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.parallel.BurstExecutorSupport; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +public class BurstExecutor extends BurstExecutorSupport { + public BurstExecutor(ExecutorService executor, int burstSizeEstimate) { + super(executor, burstSizeEstimate, IrisLogging::reportError); + } + + public BurstExecutor(Supplier executorSource, int burstSizeEstimate) { + super(executorSource, burstSizeEstimate, IrisLogging::reportError); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/parallel/GridLock.java b/core/src/main/java/art/arcane/iris/util/common/parallel/GridLock.java new file mode 100644 index 000000000..d4121b0c5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/parallel/GridLock.java @@ -0,0 +1,10 @@ +package art.arcane.iris.util.common.parallel; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.parallel.GridLockSupport; + +public class GridLock extends GridLockSupport { + public GridLock(int x, int z) { + super(x, z, IrisLogging::reportError); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/parallel/HyperLock.java b/core/src/main/java/art/arcane/iris/util/common/parallel/HyperLock.java new file mode 100644 index 000000000..45b91a02b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/parallel/HyperLock.java @@ -0,0 +1,18 @@ +package art.arcane.iris.util.common.parallel; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.parallel.HyperLockSupport; + +public class HyperLock extends HyperLockSupport { + public HyperLock() { + this(1024, false); + } + + public HyperLock(int capacity) { + this(capacity, false); + } + + public HyperLock(int capacity, boolean fair) { + super(capacity, fair, IrisLogging::warn, IrisLogging::reportError); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/parallel/MultiBurst.java b/core/src/main/java/art/arcane/iris/util/common/parallel/MultiBurst.java new file mode 100644 index 000000000..221cec69d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/parallel/MultiBurst.java @@ -0,0 +1,93 @@ +package art.arcane.iris.util.common.parallel; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; +import art.arcane.volmlib.util.parallel.MultiBurstSupport; +import art.arcane.volmlib.util.math.M; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; +import java.util.concurrent.Future; +import java.util.function.IntSupplier; + +public class MultiBurst extends MultiBurstSupport { + private static final long TIMEOUT = Long.getLong("iris.burst.timeout", 15000); + public static final MultiBurst burst = new MultiBurst(); + public static final MultiBurst ioBurst = new MultiBurst("Iris IO", () -> IrisSettings.get().getConcurrency().getIoParallelism()); + + public MultiBurst() { + this("Iris"); + } + + public MultiBurst(String name) { + this(name, Thread.MIN_PRIORITY, () -> IrisSettings.get().getConcurrency().getParallelism()); + } + + public MultiBurst(String name, IntSupplier parallelism) { + this(name, Thread.MIN_PRIORITY, parallelism); + } + + public MultiBurst(String name, int priority, IntSupplier parallelism) { + super(name, priority, parallelism, IrisSettings::getThreadCount, M::ms, IrisLogging::reportError, IrisLogging::info, IrisLogging::warn, TIMEOUT); + } + + public boolean ownsCurrentThread() { + Thread thread = Thread.currentThread(); + if (!(thread instanceof ForkJoinWorkerThread worker)) { + return false; + } + + ExecutorService service = service(); + if (!(service instanceof ForkJoinPool pool)) { + return false; + } + + return worker.getPool() == pool; + } + + public CompletableFuture completeValueAsync(Callable task) { + CompletableFuture completion = new CompletableFuture<>(); + Future submitted; + try { + submitted = service().submit(() -> { + try { + completion.complete(task.call()); + } catch (Throwable exception) { + completion.completeExceptionally(exception); + } + }); + } catch (Throwable exception) { + completion.completeExceptionally(exception); + return completion; + } + completion.whenComplete((value, exception) -> { + if (completion.isCancelled()) { + submitted.cancel(true); + } + }); + return completion; + } + + @Override + public BurstExecutor burst(int estimate) { + return new BurstExecutor(this::service, estimate); + } + + @Override + public BurstExecutor burst() { + return burst(16); + } + + @Override + public BurstExecutor burst(boolean multicore) { + BurstExecutor b = burst(); + b.setMulticore(multicore); + return b; + } + + public static void close(ExecutorService service) { + MultiBurstSupport.close(service, M::ms, IrisLogging::info, IrisLogging::warn, IrisLogging::reportError, TIMEOUT); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/Chunks.java b/core/src/main/java/art/arcane/iris/util/common/plugin/Chunks.java similarity index 97% rename from core/src/main/java/com/volmit/iris/util/plugin/Chunks.java rename to core/src/main/java/art/arcane/iris/util/common/plugin/Chunks.java index f6454dafd..0cbdc9056 100644 --- a/core/src/main/java/com/volmit/iris/util/plugin/Chunks.java +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/Chunks.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.plugin; +package art.arcane.iris.util.common.plugin; import org.bukkit.Location; import org.bukkit.World; diff --git a/core/src/main/java/com/volmit/iris/util/plugin/IrisService.java b/core/src/main/java/art/arcane/iris/util/common/plugin/IrisService.java similarity index 86% rename from core/src/main/java/com/volmit/iris/util/plugin/IrisService.java rename to core/src/main/java/art/arcane/iris/util/common/plugin/IrisService.java index a0850418c..43c69f484 100644 --- a/core/src/main/java/com/volmit/iris/util/plugin/IrisService.java +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/IrisService.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.plugin; +package art.arcane.iris.util.common.plugin; -import com.volmit.iris.Iris; +import art.arcane.iris.platform.bukkit.BukkitPlatform; import org.bukkit.event.Listener; public interface IrisService extends Listener { @@ -27,6 +27,6 @@ public interface IrisService extends Listener { void onDisable(); default void postShutdown(Runnable r) { - Iris.instance.postShutdown(r); + BukkitPlatform.volmitPlugin().postShutdown(r); } } diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java new file mode 100644 index 000000000..c3e74dc88 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java @@ -0,0 +1,180 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.plugin; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.io.IO; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; + +@SuppressWarnings("EmptyMethod") +public abstract class VolmitPlugin extends JavaPlugin implements Listener { + public static final boolean bad = false; + private final KList postShutdown = new KList<>(); + + public File getJarFile() { + return getFile(); + } + + public void postShutdown(Runnable r) { + postShutdown.add(r); + } + + protected void runPostShutdown() { + postShutdown.forEach(Runnable::run); + } + + public void l(Object l) { + IrisLogging.info("[" + getName() + "]: " + l); + } + + public void w(Object l) { + IrisLogging.warn("[" + getName() + "]: " + l); + } + + public void f(Object l) { + IrisLogging.error("[" + getName() + "]: " + l); + } + + public void v(Object l) { + IrisLogging.debug("[" + getName() + "]: " + l); + } + + public void onEnable() { + J.a(this::outputInfo); + registerListener(this); + start(); + } + + public void unregisterAll() { + unregisterListeners(); + } + + private void outputInfo() { + try { + IO.delete(getDataFolder("info")); + getDataFolder("info").mkdirs(); + outputPluginInfo(); + } catch (Throwable e) { + IrisLogging.reportError(e); + + } + } + + private void outputPluginInfo() throws IOException { + FileConfiguration fc = new YamlConfiguration(); + fc.set("version", getDescription().getVersion()); + fc.set("name", getDescription().getName()); + fc.save(getDataFile("info", "plugin.yml")); + } + + @Override + public void onDisable() { + stop(); + J.cancelPluginTasks(); + unregisterListener(this); + unregisterAll(); + } + + public String getTag() { + if (bad) { + return ""; + } + return getTag(""); + } + + public void registerListener(Listener l) { + IrisLogging.debug("Register Listener " + l.getClass().getSimpleName()); + Bukkit.getPluginManager().registerEvents(l, this); + } + + public void unregisterListener(Listener l) { + IrisLogging.debug("Register Listener " + l.getClass().getSimpleName()); + HandlerList.unregisterAll(l); + } + + public void unregisterListeners() { + if (bad) { + return; + } + HandlerList.unregisterAll((Listener) this); + } + + public File getDataFile(String... strings) { + File f = new File(getDataFolder(), new KList<>(strings).toString(File.separator)); + f.getParentFile().mkdirs(); + return f; + } + + public File getDataFileList(String pre, String[] strings) { + KList v = new KList<>(strings); + v.add(0, pre); + File f = new File(getDataFolder(), v.toString(File.separator)); + f.getParentFile().mkdirs(); + return f; + } + + public File getDataFolder(String... strings) { + if (strings.length == 0) { + return super.getDataFolder(); + } + + File f = new File(getDataFolder(), new KList<>(strings).toString(File.separator)); + f.mkdirs(); + + return f; + } + + public File getDataFolderNoCreate(String... strings) { + if (strings.length == 0) { + return super.getDataFolder(); + } + + File f = new File(getDataFolder(), new KList<>(strings).toString(File.separator)); + + return f; + } + + public File getDataFolderList(String pre, String[] strings) { + KList v = new KList<>(strings); + v.add(0, pre); + if (v.size() == 0) { + return super.getDataFolder(); + } + File f = new File(getDataFolder(), v.toString(File.separator)); + f.mkdirs(); + + return f; + } + + public abstract void start(); + + public abstract void stop(); + + public abstract String getTag(String subTag); +} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java new file mode 100644 index 000000000..fb46fed72 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java @@ -0,0 +1,467 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.plugin; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.IrisSettings; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.hud.HudPriority; +import art.arcane.volmlib.util.hud.HudSegment; +import art.arcane.volmlib.util.hud.HudSlot; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.math.M; +import lombok.Getter; +import lombok.Setter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Server; +import org.bukkit.Sound; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.jetbrains.annotations.NotNull; +import org.bukkit.plugin.Plugin; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Represents a volume sender. A command sender with extra crap in it + * + * @author cyberpwn + */ +public class VolmitSender implements CommandSender { + private final CommandSender s; + private String tag; + @Getter + @Setter + private String command; + + /** + * Wrap a command sender + * + * @param s the command sender + */ + public VolmitSender(CommandSender s) { + tag = ""; + this.s = s; + } + + public VolmitSender(CommandSender s, String tag) { + this.tag = tag; + this.s = s; + } + + public static long getTick() { + return M.ms() / 16; + } + + public static String pulse(String colorA, String colorB, double speed) { + return ""; + } + + public static String pulse(double speed) { + return Form.f(invertSpread((((getTick() * 15D * speed) % 1000D) / 1000D)), 3).replaceAll("\\Q,\\E", ".").replaceAll("\\Q?\\E", "-"); + } + + public static double invertSpread(double v) { + return ((1D - v) * 2D) - 1D; + } + + public static KList paginate(KList all, int linesPerPage, int page, AtomicBoolean hasNext) { + int totalPages = (int) Math.ceil((double) all.size() / linesPerPage); + page = page < 0 ? 0 : page >= totalPages ? totalPages - 1 : page; + hasNext.set(page < totalPages - 1); + KList d = new KList<>(); + + for (int i = linesPerPage * page; i < Math.min(all.size(), linesPerPage * (page + 1)); i++) { + d.add(all.get(i)); + } + + return d; + } + + /** + * Get the command tag + * + * @return the command tag + */ + public String getTag() { + return tag; + } + + /** + * Set a command tag (prefix for sendMessage) + * + * @param tag the tag + */ + public void setTag(String tag) { + this.tag = tag; + } + + /** + * Is this sender a player? + * + * @return true if it is + */ + public boolean isPlayer() { + return getS() instanceof Player; + } + + /** + * Force cast to player (be sure to check first) + * + * @return a casted player + */ + public Player player() { + return (Player) getS(); + } + + /** + * Get the origin sender this object is wrapping + * + * @return the command sender + */ + public CommandSender getS() { + return s; + } + + @Override + public boolean isPermissionSet(String name) { + return s.isPermissionSet(name); + } + + @Override + public boolean isPermissionSet(Permission perm) { + return s.isPermissionSet(perm); + } + + @Override + public boolean hasPermission(String name) { + return s.hasPermission(name); + } + + @Override + public boolean hasPermission(Permission perm) { + return s.hasPermission(perm); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) { + return s.addAttachment(plugin, name, value); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin) { + return s.addAttachment(plugin); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) { + return s.addAttachment(plugin, name, value, ticks); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin, int ticks) { + return s.addAttachment(plugin, ticks); + } + + @Override + public void removeAttachment(PermissionAttachment attachment) { + s.removeAttachment(attachment); + } + + @Override + public void recalculatePermissions() { + s.recalculatePermissions(); + } + + @Override + public Set getEffectivePermissions() { + return s.getEffectivePermissions(); + } + + @Override + public boolean isOp() { + return s.isOp(); + } + + @Override + public void setOp(boolean value) { + s.setOp(value); + } + + public void hr() { + s.sendMessage("========================================================"); + } + + public void sendProgress(double percent, String thing) { + int l = 44; + int g = (int) ((percent < 0 ? 1D : percent) * l); + sendActionNoProcessing("" + "" + pulse("#00ff80", "#00373d", 1D) + " " + Form.repeat(" ", g) + "" + Form.repeat(" ", l - g)); + } + + public void sendAction(String action) { + try { + deliverAction(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponent(action))); + } catch (Throwable ignored) { + } + } + + public void sendActionNoProcessing(String action) { + try { + deliverAction(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponentNoProcessing(action))); + } catch (Throwable ignored) { + } + } + + private void deliverAction(String legacy) { + Player player = player(); + if (BukkitPlatform.hasHud()) { + if (legacy.isBlank()) { + BukkitPlatform.hudBar().clear(player, "iris:action"); + } else { + BukkitPlatform.hudBar().publish(player, new HudSegment("iris:action", HudPriority.PROGRESS, 3000L, java.util.List.of(HudSlot.CENTER, HudSlot.LEFT), legacy)); + } + return; + } + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(legacy)); + } + + private Component createNoPrefixComponent(String message) { + if (!IrisSettings.get().getGeneral().canUseCustomColors(this)) { + String t = C.translateAlternateColorCodes('&', MiniMessage.miniMessage().stripTags(message)); + return MiniMessage.miniMessage().deserialize(C.mini(t)); + } + + String t = C.translateAlternateColorCodes('&', message); + String a = C.aura(t, IrisSettings.get().getGeneral().getSpinh(), IrisSettings.get().getGeneral().getSpins(), IrisSettings.get().getGeneral().getSpinb(), 0.36); + return MiniMessage.miniMessage().deserialize(a); + } + + private Component createNoPrefixComponentNoProcessing(String message) { + return MiniMessage.builder().postProcessor(c -> c).build().deserialize(C.mini(message)); + } + + private Component createComponent(String message) { + if (!IrisSettings.get().getGeneral().canUseCustomColors(this)) { + String t = C.translateAlternateColorCodes('&', MiniMessage.miniMessage().stripTags(getTag() + message)); + return MiniMessage.miniMessage().deserialize(C.mini(t)); + } + + String t = C.translateAlternateColorCodes('&', getTag() + message); + String a = C.aura(t, IrisSettings.get().getGeneral().getSpinh(), IrisSettings.get().getGeneral().getSpins(), IrisSettings.get().getGeneral().getSpinb()); + return MiniMessage.miniMessage().deserialize(a); + } + + private Component createComponentRaw(String message) { + if (!IrisSettings.get().getGeneral().canUseCustomColors(this)) { + String t = C.translateAlternateColorCodes('&', MiniMessage.miniMessage().stripTags(getTag() + message)); + return MiniMessage.miniMessage().deserialize(C.mini(t)); + } + + String t = C.translateAlternateColorCodes('&', getTag() + message); + return MiniMessage.miniMessage().deserialize(C.mini(t)); + } + + @Override + public void sendMessage(String message) { + if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { + s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message)); + return; + } + + if (message.contains("")) { + s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message.replaceAll("\\Q\\E", ""))); + return; + } + + deliver(createComponent(message)); + } + + public void sendMessageBasic(String message) { + s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message)); + } + + public void sendMessageRaw(String message) { + if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { + s.sendMessage(C.translateAlternateColorCodes('&', message)); + return; + } + + if (message.contains("")) { + s.sendMessage(message.replaceAll("\\Q\\E", "")); + return; + } + + deliver(createComponentRaw(message)); + } + + public void sendComponent(Component component) { + deliver(component); + } + + private void deliver(Component component) { + if (sendNative(s, component)) { + return; + } + s.sendMessage(LegacyComponentSerializer.legacySection().serialize(component)); + } + + private static volatile boolean nativeProbed = false; + private static Object nativeGson; + private static java.lang.reflect.Method nativeDeserialize; + private static java.lang.reflect.Method nativeSendMessage; + private static final java.util.concurrent.atomic.AtomicBoolean SEND_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false); + + private static void adventureDebug(String message) { + try { + java.io.File f = IrisPlatforms.get().dataFile("adventure-debug.txt"); + java.nio.file.Files.writeString(f.toPath(), message + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); + } catch (Throwable ignored) { + } + } + + private static String nativeAdventure(String suffix) { + return new String(new char[]{'n', 'e', 't', '.', 'k', 'y', 'o', 'r', 'i', '.', 'a', 'd', 'v', 'e', 'n', 't', 'u', 'r', 'e', '.', 't', 'e', 'x', 't', '.'}) + suffix; + } + + private static void probeNative() { + String step = "start"; + try { + String[] candidates = {nativeAdventure("serializer.gson.GsonComponentSerializer"), nativeAdventure("serializer.json.JSONComponentSerializer")}; + String[] accessors = {"gson", "json"}; + Class serializer = null; + for (int i = 0; i < candidates.length; i++) { + try { + step = "forName " + candidates[i]; + Class c = Class.forName(candidates[i]); + step = "accessor " + accessors[i] + " on " + candidates[i]; + nativeGson = c.getMethod(accessors[i]).invoke(null); + serializer = c; + break; + } catch (Throwable ignore) { + } + } + if (serializer == null) { + throw new ClassNotFoundException("no native adventure json serializer exposed"); + } + step = "find deserialize on " + serializer.getName(); + java.lang.reflect.Method exact = null; + java.lang.reflect.Method loose = null; + for (java.lang.reflect.Method m : serializer.getMethods()) { + if (m.getName().equals("deserialize") && m.getParameterCount() == 1) { + Class p = m.getParameterTypes()[0]; + if (p == String.class) { + exact = m; + break; + } + if (p.isAssignableFrom(String.class)) { + loose = m; + } + } + } + nativeDeserialize = exact != null ? exact : loose; + if (nativeDeserialize == null) { + throw new NoSuchMethodException("deserialize(String-compatible) not found"); + } + step = "forName Component"; + Class componentType = Class.forName(nativeAdventure("Component")); + step = "getMethod CommandSender.sendMessage(Component)"; + nativeSendMessage = CommandSender.class.getMethod("sendMessage", componentType); + } catch (Throwable e) { + nativeSendMessage = null; + adventureDebug("PROBE FAIL at [" + step + "]: " + e.getClass().getName() + ": " + e.getMessage()); + } + nativeProbed = true; + } + + private static boolean sendNative(CommandSender target, Component component) { + if (!nativeProbed) { + probeNative(); + } + if (nativeSendMessage == null) { + return false; + } + try { + String json = GsonComponentSerializer.gson().serialize(component); + Object nativeComponent = nativeDeserialize.invoke(nativeGson, json); + nativeSendMessage.invoke(target, nativeComponent); + return true; + } catch (Throwable e) { + if (SEND_LOGGED.compareAndSet(false, true)) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + adventureDebug("SEND FAIL: " + cause.getClass().getName() + ": " + cause.getMessage()); + } + return false; + } + } + + @Override + public void sendMessage(String[] messages) { + for (String str : messages) + sendMessage(str); + } + + @Override + public void sendMessage(UUID uuid, String message) { + sendMessage(message); + } + + @Override + public void sendMessage(UUID uuid, String[] messages) { + sendMessage(messages); + } + + @Override + public Server getServer() { + return s.getServer(); + } + + @Override + public String getName() { + return s.getName(); + } + + @NotNull + @Override + public Component name() { + return s.name(); + } + + @Override + public Spigot spigot() { + return s.spigot(); + } + + public void playSound(Sound sound, float volume, float pitch) { + if (isPlayer()) { + player().playSound(player().getLocation(), sound, volume, pitch); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/chunk/ChunkTickets.java b/core/src/main/java/art/arcane/iris/util/common/plugin/chunk/ChunkTickets.java new file mode 100644 index 000000000..4e17412cf --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/chunk/ChunkTickets.java @@ -0,0 +1,62 @@ +package art.arcane.iris.util.common.plugin.chunk; + +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import lombok.NonNull; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.event.world.WorldUnloadEvent; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ChunkTickets implements Listener { + // Concurrent + self-healing: mutated from event handlers and read from command threads, + // and a holder dropped by an aborted unload must recreate on next use, not NPE forever. + private final Map holders = new ConcurrentHashMap<>(); + + public ChunkTickets() { + BukkitPlatform.volmitPlugin().registerListener(this); + Bukkit.getWorlds().forEach(w -> holders.put(w, new TicketHolder(w))); + } + + public TicketHolder getHolder(@NonNull World world) { + return holders.computeIfAbsent(world, TicketHolder::new); + } + + public void addTicket(@NonNull Chunk chunk) { + addTicket(chunk.getWorld(), chunk.getX(), chunk.getZ()); + } + + public void addTicket(@NonNull World world, int x, int z) { + var holder = getHolder(world); + if (holder != null) holder.addTicket(x, z); + } + + public boolean removeTicket(@NonNull Chunk chunk) { + return removeTicket(chunk.getWorld(), chunk.getX(), chunk.getZ()); + } + + public boolean removeTicket(@NonNull World world, int x, int z) { + var holder = getHolder(world); + if (holder != null) return holder.removeTicket(x, z); + return false; + } + + @EventHandler(priority = EventPriority.LOWEST) + public void on(@NonNull WorldLoadEvent event) { + holders.put(event.getWorld(), new TicketHolder(event.getWorld())); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(@NonNull WorldUnloadEvent event) { + TicketHolder holder = holders.remove(event.getWorld()); + if (holder != null) { + holder.releaseAll(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/chunk/TicketHolder.java b/core/src/main/java/art/arcane/iris/util/common/plugin/chunk/TicketHolder.java new file mode 100644 index 000000000..29d3cfde4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/chunk/TicketHolder.java @@ -0,0 +1,60 @@ +package art.arcane.iris.util.common.plugin.chunk; + +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.volmlib.util.collection.KMap; +import lombok.NonNull; +import org.bukkit.Chunk; +import org.bukkit.World; + +public class TicketHolder { + private final World world; + private final KMap tickets = new KMap<>(); + + public TicketHolder(@NonNull World world) { + this.world = world; + } + + public void addTicket(@NonNull Chunk chunk) { + if (chunk.getWorld() != world) return; + addTicket(chunk.getX(), chunk.getZ()); + } + + public void addTicket(int x, int z) { + tickets.compute(Cache.key(x, z), ($, ref) -> { + if (ref == null) { + world.addPluginChunkTicket(x, z, BukkitPlatform.plugin()); + return 1L; + } + return ++ref; + }); + } + + public boolean removeTicket(@NonNull Chunk chunk) { + if (chunk.getWorld() != world) return false; + return removeTicket(chunk.getX(), chunk.getZ()); + } + + public boolean removeTicket(int x, int z) { + return tickets.compute(Cache.key(x, z), ($, ref) -> { + if (ref == null) return null; + if (--ref <= 0) { + world.removePluginChunkTicket(x, z, BukkitPlatform.plugin()); + return null; + } + return ref; + }) == null; + } + + public void releaseAll() { + tickets.forEach((key, refs) -> { + int x = Cache.keyX(key); + int z = Cache.keyZ(key); + try { + world.removePluginChunkTicket(x, z, BukkitPlatform.plugin()); + } catch (Throwable ignored) { + } + }); + tickets.clear(); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/reflect/KeyedType.java b/core/src/main/java/art/arcane/iris/util/common/reflect/KeyedType.java new file mode 100644 index 000000000..c712cd0fd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/reflect/KeyedType.java @@ -0,0 +1,85 @@ +package art.arcane.iris.util.common.reflect; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import art.arcane.iris.util.common.data.registry.RegistryTypeAdapter; +import art.arcane.iris.util.common.data.registry.RegistryUtil; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; + +public class KeyedType { + private static final boolean KEYED_LENIENT = Boolean.getBoolean("iris.keyed-lenient"); + private static final boolean KEYED_PRESENT = detectKeyed(); + + private static boolean detectKeyed() { + try { + Class.forName("org.bukkit.Keyed", false, KeyedType.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static String[] values(Class type) { + if (!isKeyed(type)) return new String[0]; + return RegistryUtil.lookup(type) + .map() + .keySet() + .stream() + .map(Object::toString) + .toArray(String[]::new); + } + + public static boolean isKeyed(Class type) { + if (!KEYED_PRESENT) return false; + if (KEYED_LENIENT) return !RegistryUtil.lookup(type).isEmpty(); + return Keyed.class.isAssignableFrom(type); + } + + @SuppressWarnings("unchecked") + public static TypeAdapter createTypeAdapter(Gson gson, TypeToken type) { + if (!isKeyed(type.getRawType())) return null; + return (TypeAdapter) RegistryTypeAdapter.of(type.getRawType()); + } + + @Nullable + public static NamespacedKey getKey(Object value) { + // KEYED_PRESENT first: without it the instanceof below resolves org.bukkit.Keyed and + // NoClassDefFoundErrors on the modded loaders instead of degrading to null. + if (!KEYED_PRESENT || value == null) { + return null; + } + + if (value instanceof Keyed keyed) { + NamespacedKey key = keyed.getKey(); + if (key != null) { + return key; + } + } + + NamespacedKey keyOrThrow = invokeKey(value, "getKeyOrThrow"); + if (keyOrThrow != null) { + return keyOrThrow; + } + + return invokeKey(value, "getKey"); + } + + @Nullable + private static NamespacedKey invokeKey(Object value, String methodName) { + try { + Method method = value.getClass().getMethod(methodName); + Object result = method.invoke(value); + if (result instanceof NamespacedKey namespacedKey) { + return namespacedKey; + } + return null; + } catch (Throwable ignored) { + return null; + } + } +} diff --git a/core/src/main/java/com/volmit/iris/util/reflect/W.java b/core/src/main/java/art/arcane/iris/util/common/reflect/W.java similarity index 78% rename from core/src/main/java/com/volmit/iris/util/reflect/W.java rename to core/src/main/java/art/arcane/iris/util/common/reflect/W.java index 7febb0611..7d04f895c 100644 --- a/core/src/main/java/com/volmit/iris/util/reflect/W.java +++ b/core/src/main/java/art/arcane/iris/util/common/reflect/W.java @@ -1,4 +1,4 @@ -package com.volmit.iris.util.reflect; +package art.arcane.iris.util.common.reflect; import lombok.Getter; diff --git a/core/src/main/java/art/arcane/iris/util/common/reflect/WrappedField.java b/core/src/main/java/art/arcane/iris/util/common/reflect/WrappedField.java new file mode 100644 index 000000000..7fd8e13c4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/reflect/WrappedField.java @@ -0,0 +1,54 @@ +package art.arcane.iris.util.common.reflect; + +import art.arcane.iris.spi.IrisLogging; + +import java.lang.reflect.Field; + +public class WrappedField { + + private final Field field; + + public WrappedField(Class origin, String methodName) { + Field f = null; + try { + f = origin.getDeclaredField(methodName); + f.setAccessible(true); + } catch (NoSuchFieldException e) { + IrisLogging.error("Failed to created WrappedField %s#%s: %s%s", origin.getSimpleName(), methodName, e.getClass().getSimpleName(), e.getMessage().equals("") ? "" : " | " + e.getMessage()); + } + this.field = f; + } + + public T get() { + return get(null); + } + + public T get(C instance) { + if (field == null) { + return null; + } + + try { + return (T) field.get(instance); + } catch (IllegalAccessException e) { + IrisLogging.error("Failed to get WrappedField %s#%s: %s%s", field.getDeclaringClass().getSimpleName(), field.getName(), e.getClass().getSimpleName(), e.getMessage().equals("") ? "" : " | " + e.getMessage()); + return null; + } + } + + public void set(T value) throws IllegalAccessException { + set(null, value); + } + + public void set(C instance, T value) throws IllegalAccessException { + if (field == null) { + return; + } + + field.set(instance, value); + } + + public boolean hasFailed() { + return field == null; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/reflect/WrappedReturningMethod.java b/core/src/main/java/art/arcane/iris/util/common/reflect/WrappedReturningMethod.java new file mode 100644 index 000000000..02b220921 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/reflect/WrappedReturningMethod.java @@ -0,0 +1,39 @@ +package art.arcane.iris.util.common.reflect; + +import art.arcane.iris.spi.IrisLogging; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public final class WrappedReturningMethod { + + private final Method method; + + public WrappedReturningMethod(Class origin, String methodName, Class... paramTypes) { + Method m = null; + try { + m = origin.getDeclaredMethod(methodName, paramTypes); + m.setAccessible(true); + } catch (NoSuchMethodException e) { + IrisLogging.error("Failed to created WrappedMethod %s#%s: %s%s", origin.getSimpleName(), methodName, e.getClass().getSimpleName(), e.getMessage().equals("") ? "" : " | " + e.getMessage()); + } + this.method = m; + } + + public R invoke(Object... args) { + return invoke(null, args); + } + + public R invoke(C instance, Object... args) { + if (method == null) { + return null; + } + + try { + return (R) method.invoke(instance, args); + } catch (InvocationTargetException | IllegalAccessException e) { + IrisLogging.error("Failed to invoke WrappedMethod %s#%s: %s%s", method.getDeclaringClass().getSimpleName(), method.getName(), e.getClass().getSimpleName(), e.getMessage().equals("") ? "" : " | " + e.getMessage()); + return null; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java new file mode 100644 index 000000000..69bce7cbd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java @@ -0,0 +1,831 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.scheduling; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.volmlib.util.function.NastyFunction; +import art.arcane.volmlib.util.function.NastyFuture; +import art.arcane.volmlib.util.function.NastyRunnable; +import art.arcane.volmlib.util.function.NastySupplier; +import art.arcane.volmlib.util.math.FinalInteger; +import art.arcane.volmlib.util.scheduling.AR; +import art.arcane.volmlib.util.scheduling.FoliaScheduler; +import art.arcane.volmlib.util.scheduling.JSupport; +import art.arcane.volmlib.util.scheduling.SR; +import art.arcane.volmlib.util.scheduling.SchedulerBridge; +import art.arcane.volmlib.util.scheduling.StartupQueueSupport; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; + +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +@SuppressWarnings("ALL") +public class J { + private static final long TICK_MS = 50L; + private static final AtomicInteger TASK_IDS = new AtomicInteger(1); + private static final Map REPEATING_CANCELLERS = new ConcurrentHashMap<>(); + private static final StartupQueueSupport STARTUP_QUEUE = new StartupQueueSupport(); + private static final boolean BUKKIT_PRESENT = detectBukkit(); + + private static boolean detectBukkit() { + try { + Class.forName("org.bukkit.Bukkit", false, J.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + static { + SchedulerBridge.setSyncScheduler(J::s); + SchedulerBridge.setDelayedSyncScheduler(J::s); + SchedulerBridge.setAsyncScheduler(J::a); + SchedulerBridge.setDelayedAsyncScheduler(J::a); + SchedulerBridge.setSyncRepeatingScheduler(J::sr); + SchedulerBridge.setAsyncRepeatingScheduler(J::ar); + SchedulerBridge.setCancelScheduler(J::car); + SchedulerBridge.setErrorHandler(e -> { + IrisLogging.reportError(e); + e.printStackTrace(); + }); + SchedulerBridge.setInfoLogger(IrisLogging::debug); + SchedulerBridge.setThreadRegistrar(thread -> { + try { + IrisServices.get(PreservationRegistry.class).register(thread); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + }); + } + + public static void dofor(int a, Function c, int ch, Consumer d) { + JSupport.dofor(a, c, ch, d); + } + + public static boolean doif(Supplier c, Runnable g) { + return JSupport.doif(c, g, IrisLogging::reportError); + } + + public static void arun(Runnable a) { + MultiBurst.burst.lazy(() -> { + try { + a.run(); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Failed to run async task"); + e.printStackTrace(); + } + }); + } + + public static void a(Runnable a) { + MultiBurst.burst.lazy(() -> { + try { + a.run(); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Failed to run async task"); + e.printStackTrace(); + } + }); + } + + public static void aBukkit(Runnable a) { + if (!BUKKIT_PRESENT) { + if (IrisPlatforms.isBound()) { + IrisPlatforms.get().scheduler().async(a); + } + return; + } + + if (!isPluginEnabled()) { + return; + } + + if (!runAsyncImmediate(a)) { + a(a, 0); + } + } + + public static Future a(Callable a) { + return MultiBurst.burst.lazySubmit(a); + } + + public static void attemptAsync(NastyRunnable r) { + JSupport.attemptAsync(r::run, J::a); + } + + public static R attemptResult(NastyFuture r, R onError) { + return JSupport.attemptResult(r::run, onError, IrisLogging::reportError); + } + + public static R attemptFunction(NastyFunction r, T param, R onError) { + return JSupport.attemptFunction(r::run, param, onError, IrisLogging::reportError); + } + + public static boolean sleep(long ms) { + return JSupport.sleep(ms); + } + + public static boolean attempt(NastyRunnable r) { + return JSupport.attempt(r::run); + } + + public static T attemptResult(NastySupplier r) { + return JSupport.attemptNullable(r::get); + } + + public static Throwable attemptCatch(NastyRunnable r) { + return JSupport.attemptCatch(r::run); + } + + public static T attempt(Supplier t, T i) { + return JSupport.attempt(t::get, i, IrisLogging::reportError); + } + + public static void executeAfterStartupQueue() { + JSupport.executeAfterStartupQueue(STARTUP_QUEUE, J::s, J::a); + } + + public static void ass(Runnable r) { + JSupport.enqueueAfterStartupSync(STARTUP_QUEUE, r, J::s); + } + + public static void asa(Runnable r) { + JSupport.enqueueAfterStartupAsync(STARTUP_QUEUE, r, J::a); + } + + public static boolean isFolia() { + return BUKKIT_PRESENT && FoliaScheduler.isFolia(Bukkit.getServer()); + } + + public static boolean isPrimaryThread() { + return FoliaScheduler.isPrimaryThread(); + } + + public static boolean isOwnedByCurrentRegion(Entity entity) { + if (entity == null) { + return false; + } + + if (!isFolia()) { + return isPrimaryThread(); + } + + return FoliaScheduler.isOwnedByCurrentRegion(entity); + } + + public static boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ) { + if (world == null) { + return false; + } + + if (!isFolia()) { + return isPrimaryThread(); + } + + return FoliaScheduler.isOwnedByCurrentRegion(world, chunkX, chunkZ); + } + + public static boolean runEntity(Entity entity, Runnable runnable) { + if (entity == null || runnable == null) { + return false; + } + + if (isFolia()) { + if (isOwnedByCurrentRegion(entity)) { + runnable.run(); + return true; + } + + return runEntityImmediate(entity, runnable); + } + + if (isPrimaryThread()) { + runnable.run(); + return true; + } + + s(runnable); + return true; + } + + public static boolean runEntity(Entity entity, Runnable runnable, int delayTicks) { + if (entity == null || runnable == null) { + return false; + } + + if (delayTicks <= 0) { + return runEntity(entity, runnable); + } + + if (isFolia() && runEntityDelayed(entity, runnable, delayTicks)) { + return true; + } + + s(() -> runEntity(entity, runnable), delayTicks); + return true; + } + + public static boolean runEntity(Entity entity, Runnable runnable, int delayTicks, Runnable retired) { + if (retired == null) { + return runEntity(entity, runnable, delayTicks); + } + + if (entity == null || runnable == null) { + return false; + } + + if (isFolia()) { + return FoliaScheduler.runEntity(BukkitPlatform.plugin(), entity, runnable, Math.max(0, delayTicks), retired); + } + + Runnable guarded = () -> { + if (entity.isValid()) { + runnable.run(); + } else { + retired.run(); + } + }; + return runEntity(entity, guarded, delayTicks); + } + + public static boolean runRegion(World world, int chunkX, int chunkZ, Runnable runnable) { + if (world == null || runnable == null) { + return false; + } + + if (isFolia() && isOwnedByCurrentRegion(world, chunkX, chunkZ)) { + runnable.run(); + return true; + } + + if (runRegionImmediate(world, chunkX, chunkZ, runnable)) { + return true; + } + + if (isFolia()) { + IrisLogging.debug("Failed to schedule immediate region task for " + world.getName() + "@" + chunkX + "," + chunkZ + " on Folia."); + return false; + } + + s(runnable); + return true; + } + + public static boolean runGlobal(Runnable runnable) { + if (runnable == null) { + return false; + } + + if (!BUKKIT_PRESENT) { + if (!IrisPlatforms.isBound()) { + return false; + } + IrisPlatforms.get().scheduler().global(runnable); + return true; + } + + if (!isPluginEnabled()) { + return false; + } + + if (isFolia()) { + return FoliaScheduler.runGlobal(BukkitPlatform.plugin(), runnable); + } + + if (Bukkit.isPrimaryThread()) { + runnable.run(); + return true; + } + + try { + Bukkit.getScheduler().runTask(BukkitPlatform.plugin(), runnable); + return true; + } catch (UnsupportedOperationException e) { + FoliaScheduler.forceFoliaThreading(Bukkit.getServer()); + return FoliaScheduler.runGlobal(BukkitPlatform.plugin(), runnable); + } + } + + public static boolean runRegion(World world, int chunkX, int chunkZ, Runnable runnable, int delayTicks) { + if (world == null || runnable == null) { + return false; + } + + if (delayTicks <= 0) { + return runRegion(world, chunkX, chunkZ, runnable); + } + + if (runRegionDelayed(world, chunkX, chunkZ, runnable, delayTicks)) { + return true; + } + + if (isFolia()) { + IrisLogging.debug("Failed to schedule delayed region task for " + world.getName() + "@" + chunkX + "," + chunkZ + + " (" + delayTicks + "t) on Folia."); + return false; + } + + s(runnable, delayTicks); + return true; + } + + public static boolean runAt(Location location, Runnable runnable) { + if (location == null || runnable == null || location.getWorld() == null) { + return false; + } + + return runRegion(location.getWorld(), location.getBlockX() >> 4, location.getBlockZ() >> 4, runnable); + } + + public static boolean runAt(Location location, Runnable runnable, int delayTicks) { + if (location == null || runnable == null || location.getWorld() == null) { + return false; + } + + return runRegion(location.getWorld(), location.getBlockX() >> 4, location.getBlockZ() >> 4, runnable, delayTicks); + } + + public static void cancelPluginTasks() { + if (!BukkitPlatform.hasPlugin()) { + return; + } + + FoliaScheduler.cancelTasks(BukkitPlatform.plugin()); + + try { + Bukkit.getScheduler().cancelTasks(BukkitPlatform.plugin()); + } catch (UnsupportedOperationException ex) { + // Folia blocks BukkitScheduler usage. + IrisLogging.debug("Skipping BukkitScheduler#cancelTasks for Iris on this server."); + } + } + + public static void s(Runnable r) { + if (!BUKKIT_PRESENT) { + if (IrisPlatforms.isBound()) { + IrisPlatforms.get().scheduler().global(r); + } + return; + } + + if (!isPluginEnabled()) { + return; + } + + if (isFolia()) { + if (runGlobalImmediate(r)) { + return; + } + + throw new IllegalStateException("Failed to schedule sync task on Folia runtime."); + } + + try { + Bukkit.getScheduler().scheduleSyncDelayedTask(BukkitPlatform.plugin(), r); + } catch (UnsupportedOperationException e) { + FoliaScheduler.forceFoliaThreading(Bukkit.getServer()); + if (runGlobalImmediate(r)) { + return; + } + + throw new IllegalStateException("Failed to schedule sync task (Folia scheduler unavailable, BukkitScheduler unsupported).", e); + } + } + + /** + * Never returns null; the future always completes, exceptionally if the task throws or cannot be scheduled. + */ + public static CompletableFuture sfut(Runnable r) { + CompletableFuture f = new CompletableFuture<>(); + + if (!canSchedule()) { + f.completeExceptionally(new IllegalStateException("Cannot schedule sync task, no scheduler available.")); + return f; + } + + try { + s(() -> settle(f, r)); + } catch (Throwable e) { + f.completeExceptionally(e); + } + + return f; + } + + /** + * Never returns null; the future always completes, exceptionally if the supplier throws or cannot be scheduled. + */ + public static CompletableFuture sfut(Supplier r) { + CompletableFuture f = new CompletableFuture<>(); + + if (!canSchedule()) { + f.completeExceptionally(new IllegalStateException("Cannot schedule sync task, no scheduler available.")); + return f; + } + + try { + s(() -> settleSupplied(f, r)); + } catch (Throwable e) { + f.completeExceptionally(e); + } + + return f; + } + + /** + * Never returns null; the future always completes, exceptionally if the task throws or cannot be scheduled. + */ + public static CompletableFuture sfut(Runnable r, int delay) { + CompletableFuture f = new CompletableFuture<>(); + + if (!canSchedule()) { + f.completeExceptionally(new IllegalStateException("Cannot schedule delayed sync task, no scheduler available.")); + return f; + } + + try { + s(() -> settle(f, r), delay); + } catch (Throwable e) { + f.completeExceptionally(e); + } + + return f; + } + + /** + * Never returns null; the future always completes, exceptionally if the task throws or cannot be scheduled. + */ + public static CompletableFuture afut(Runnable r) { + CompletableFuture f = new CompletableFuture<>(); + + try { + J.a(() -> settle(f, r)); + } catch (Throwable e) { + f.completeExceptionally(e); + } + + return f; + } + + private static void settle(CompletableFuture f, Runnable r) { + try { + r.run(); + f.complete(null); + } catch (Throwable e) { + f.completeExceptionally(e); + } + } + + private static void settleSupplied(CompletableFuture f, Supplier r) { + try { + f.complete(r.get()); + } catch (Throwable e) { + f.completeExceptionally(e); + } + } + + public static void s(Runnable r, int delay) { + if (!BUKKIT_PRESENT) { + if (IrisPlatforms.isBound()) { + IrisPlatforms.get().scheduler().laterGlobal(r, delay); + } + return; + } + + if (!isPluginEnabled()) { + return; + } + + if (delay <= 0) { + s(r); + return; + } + + if (isFolia()) { + if (runGlobalDelayed(r, delay)) { + return; + } + + a(() -> { + if (sleep(ticksToMilliseconds(delay))) { + s(r); + } + }); + return; + } + + try { + Bukkit.getScheduler().scheduleSyncDelayedTask(BukkitPlatform.plugin(), r, delay); + } catch (UnsupportedOperationException e) { + FoliaScheduler.forceFoliaThreading(Bukkit.getServer()); + if (runGlobalDelayed(r, delay)) { + return; + } + + throw new IllegalStateException("Failed to schedule delayed sync task (Folia scheduler unavailable, BukkitScheduler unsupported).", e); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + public static void csr(int id) { + cancelRepeatingTask(id); + } + + public static int sr(Runnable r, int interval) { + if (!canSchedule()) { + return -1; + } + + int safeInterval = Math.max(1, interval); + RepeatingState state = new RepeatingState(); + int taskId = trackRepeatingTask(() -> state.cancelled = true); + + Runnable[] loop = new Runnable[1]; + loop[0] = () -> { + if (state.cancelled || !canSchedule()) { + REPEATING_CANCELLERS.remove(taskId); + return; + } + + // A throwing body must not kill the loop (Bukkit's own runTaskTimer survives + // throws) or leak the canceller entry; the re-arm is unconditional. + try { + r.run(); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + if (state.cancelled || !canSchedule()) { + REPEATING_CANCELLERS.remove(taskId); + return; + } + + s(loop[0], safeInterval); + }; + + // Delay 1, never inline: an immediate first run can execute before the caller has + // stored the returned id, making the body's own cancel() target a nonexistent id. + s(loop[0], 1); + return taskId; + } + + public static void sr(Runnable r, int interval, int intervals) { + FinalInteger fi = new FinalInteger(0); + + new SR(interval) { + @Override + public void run() { + fi.add(1); + r.run(); + + if (fi.get() >= intervals) { + cancel(); + } + } + }; + } + + public static void a(Runnable r, int delay) { + if (!BUKKIT_PRESENT) { + if (IrisPlatforms.isBound()) { + if (delay <= 0) { + IrisPlatforms.get().scheduler().async(r); + } else { + IrisPlatforms.get().scheduler().laterGlobal(() -> IrisPlatforms.get().scheduler().async(r), delay); + } + } + return; + } + + if (!isPluginEnabled()) { + return; + } + + if (delay <= 0) { + if (!runAsyncImmediate(r)) { + a(r); + } + return; + } + + if (!runAsyncDelayed(r, delay)) { + a(() -> { + if (sleep(ticksToMilliseconds(delay))) { + r.run(); + } + }); + } + } + + public static void car(int id) { + cancelRepeatingTask(id); + } + + public static int ar(Runnable r, int interval) { + if (!canSchedule()) { + return -1; + } + + int safeInterval = Math.max(1, interval); + RepeatingState state = new RepeatingState(); + int taskId = trackRepeatingTask(() -> state.cancelled = true); + + Runnable[] loop = new Runnable[1]; + loop[0] = () -> { + if (state.cancelled || !canSchedule()) { + REPEATING_CANCELLERS.remove(taskId); + return; + } + + // A throwing body must not kill the loop (Bukkit's own runTaskTimer survives + // throws) or leak the canceller entry; the re-arm is unconditional. + try { + r.run(); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + if (state.cancelled || !canSchedule()) { + REPEATING_CANCELLERS.remove(taskId); + return; + } + + a(loop[0], safeInterval); + }; + + a(loop[0], 1); + return taskId; + } + + public static void ar(Runnable r, int interval, int intervals) { + FinalInteger fi = new FinalInteger(0); + + new AR(interval) { + @Override + public void run() { + fi.add(1); + r.run(); + + if (fi.get() >= intervals) { + cancel(); + } + } + }; + } + + private static int trackRepeatingTask(Runnable cancelAction) { + int id = TASK_IDS.getAndIncrement(); + REPEATING_CANCELLERS.put(id, cancelAction); + return id; + } + + private static void cancelRepeatingTask(int id) { + Runnable cancelAction = REPEATING_CANCELLERS.remove(id); + if (cancelAction != null) { + cancelAction.run(); + } + } + + private static boolean isPluginEnabled() { + return BUKKIT_PRESENT && BukkitPlatform.hasPlugin() && Bukkit.getPluginManager().isPluginEnabled(BukkitPlatform.plugin()); + } + + private static boolean canSchedule() { + return BUKKIT_PRESENT ? isPluginEnabled() : IrisPlatforms.isBound(); + } + + private static long ticksToMilliseconds(int ticks) { + return Math.max(0L, ticks) * TICK_MS; + } + + private static boolean runGlobalImmediate(Runnable runnable) { + if (!isFolia()) { + return false; + } + + if (isPrimaryThread()) { + runnable.run(); + return true; + } + + return FoliaScheduler.runGlobal(BukkitPlatform.plugin(), runnable); + } + + private static boolean runGlobalDelayed(Runnable runnable, int delayTicks) { + if (!isFolia()) { + return false; + } + + if (delayTicks <= 0) { + return runGlobalImmediate(runnable); + } + + return FoliaScheduler.runGlobal(BukkitPlatform.plugin(), runnable, Math.max(0, delayTicks)); + } + + private static boolean runRegionImmediate(World world, int chunkX, int chunkZ, Runnable runnable) { + if (!isFolia()) { + return false; + } + + return FoliaScheduler.runRegion(BukkitPlatform.plugin(), world, chunkX, chunkZ, runnable); + } + + private static boolean runRegionDelayed(World world, int chunkX, int chunkZ, Runnable runnable, int delayTicks) { + if (!isFolia()) { + return false; + } + + return FoliaScheduler.runRegion(BukkitPlatform.plugin(), world, chunkX, chunkZ, runnable, Math.max(0, delayTicks)); + } + + private static boolean runAsyncImmediate(Runnable runnable) { + if (!isPluginEnabled()) { + return false; + } + + if (!isFolia()) { + try { + Bukkit.getScheduler().runTaskAsynchronously(BukkitPlatform.plugin(), runnable); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + return FoliaScheduler.runAsync(BukkitPlatform.plugin(), runnable); + } + + private static boolean runAsyncDelayed(Runnable runnable, int delayTicks) { + if (!isPluginEnabled()) { + return false; + } + + if (!isFolia()) { + try { + Bukkit.getScheduler().runTaskLaterAsynchronously(BukkitPlatform.plugin(), runnable, Math.max(0, delayTicks)); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + return FoliaScheduler.runAsync(BukkitPlatform.plugin(), runnable, Math.max(0, delayTicks)); + } + + private static boolean runEntityImmediate(Entity entity, Runnable runnable) { + if (!isFolia()) { + return false; + } + + return FoliaScheduler.runEntity(BukkitPlatform.plugin(), entity, runnable); + } + + private static boolean runEntityDelayed(Entity entity, Runnable runnable, int delayTicks) { + if (!isFolia()) { + return false; + } + + return FoliaScheduler.runEntity(BukkitPlatform.plugin(), entity, runnable, Math.max(0, delayTicks)); + } + + private static final class RepeatingState { + private volatile boolean cancelled; + } +} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/DownloadJob.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/DownloadJob.java similarity index 80% rename from core/src/main/java/com/volmit/iris/util/scheduling/jobs/DownloadJob.java rename to core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/DownloadJob.java index e4b922529..3fe90c64b 100644 --- a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/DownloadJob.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/DownloadJob.java @@ -16,25 +16,27 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.scheduling.jobs; +package art.arcane.iris.util.common.scheduling.jobs; -import com.volmit.iris.util.network.DL; -import com.volmit.iris.util.network.DownloadMonitor; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.volmlib.util.network.DL; +import art.arcane.volmlib.util.network.DownloadMonitor; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; public class DownloadJob implements Job { private final DL.Download download; - private int tw; - private int cw; + private volatile int tw; + private volatile int cw; public DownloadJob(String url, File destination) throws MalformedURLException { tw = 1; cw = 0; - download = new DL.Download(new URL(url), destination, DL.DownloadFlag.CALCULATE_SIZE); + download = new DL.Download(URI.create(url).toURL(), destination, DL.DownloadFlag.CALCULATE_SIZE); download.monitor(new DownloadMonitor() { @Override public void onUpdate(DL.DownloadState state, double progress, long elapsed, long estimated, long bps, long iobps, long size, long downloaded, long buffer, double bufferuse) { @@ -50,7 +52,7 @@ public class DownloadJob implements Job { @Override public String getName() { - return "Downloading"; + return IrisLanguage.text(RuntimeUiMessages.JOB_DOWNLOADING); } @Override diff --git a/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/Job.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/Job.java new file mode 100644 index 000000000..91618e88a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/Job.java @@ -0,0 +1,94 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.common.scheduling.jobs; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.util.concurrent.CompletableFuture; + +public interface Job { + String getName(); + + void execute(); + + void completeWork(); + + int getTotalWork(); + + default int getWorkRemaining() { + return getTotalWork() - getWorkCompleted(); + } + + int getWorkCompleted(); + + default String getProgressString() { + return Form.pc(getProgress(), 0); + } + + default double getProgress() { + return (double) getWorkCompleted() / (double) getTotalWork(); + } + + + default void execute(VolmitSender sender) { + execute(sender, () -> { + }); + } + + + default void execute(VolmitSender sender, Runnable whenComplete) { + execute(sender, false, whenComplete); + } + + default void execute(VolmitSender sender, boolean silentMsg, Runnable whenComplete) { + PrecisionStopwatch p = PrecisionStopwatch.start(); + CompletableFuture f = J.afut(this::execute); + int c = J.ar(() -> { + if (sender.isPlayer()) { + sender.sendProgress(getProgress(), getName()); + BukkitPlatform.showProgressLane(sender.player(), "iris:job", getName() + " " + getProgressString(), getProgress(), 4000L); + } else { + sender.sendMessage(getName() + ": " + getProgressString()); + } + }, sender.isPlayer() ? 0 : 20); + f.whenComplete((fs, ff) -> { + J.car(c); + if (sender.isPlayer()) { + sender.sendAction(" "); + BukkitPlatform.hudLanes().hide(sender.player(), "iris:job"); + } + if (!silentMsg) { + sender.sendMessage(C.AQUA + IrisLanguage.text( + RuntimeUiMessages.JOB_COMPLETED, + MessageArgument.untrusted("job", getName()), + MessageArgument.trusted("duration", Form.duration(p.getMilliseconds(), 1)) + )); + } + whenComplete.run(); + }); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/JobCollection.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/JobCollection.java similarity index 92% rename from core/src/main/java/com/volmit/iris/util/scheduling/jobs/JobCollection.java rename to core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/JobCollection.java index 6b8fde875..b24358b37 100644 --- a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/JobCollection.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/JobCollection.java @@ -16,14 +16,14 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.scheduling.jobs; +package art.arcane.iris.util.common.scheduling.jobs; -import com.volmit.iris.util.collection.KList; +import art.arcane.volmlib.util.collection.KList; public class JobCollection implements Job { private final String name; private final KList jobs; - private String status; + private volatile String status; public JobCollection(String name, Job... jobs) { this(name, new KList<>(jobs)); diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/ParallelQueueJob.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/ParallelQueueJob.java similarity index 85% rename from core/src/main/java/com/volmit/iris/util/scheduling/jobs/ParallelQueueJob.java rename to core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/ParallelQueueJob.java index 8fd80e9c2..8999d9d7c 100644 --- a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/ParallelQueueJob.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/ParallelQueueJob.java @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.scheduling.jobs; +package art.arcane.iris.util.common.scheduling.jobs; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; public abstract class ParallelQueueJob extends QueueJob { @Override diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/QueueJob.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/QueueJob.java similarity index 92% rename from core/src/main/java/com/volmit/iris/util/scheduling/jobs/QueueJob.java rename to core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/QueueJob.java index f62cb2d0a..80db9c682 100644 --- a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/QueueJob.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/QueueJob.java @@ -16,16 +16,16 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.scheduling.jobs; +package art.arcane.iris.util.common.scheduling.jobs; -import com.volmit.iris.util.collection.KList; +import art.arcane.volmlib.util.collection.KList; import java.util.concurrent.atomic.AtomicInteger; public abstract class QueueJob implements Job { final KList queue; private final AtomicInteger completed; - protected int totalWork; + protected volatile int totalWork; public QueueJob() { totalWork = 0; diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/SingleJob.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/SingleJob.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/scheduling/jobs/SingleJob.java rename to core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/SingleJob.java index 43c4c2d81..13180feea 100644 --- a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/SingleJob.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/SingleJob.java @@ -16,12 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.scheduling.jobs; +package art.arcane.iris.util.common.scheduling.jobs; public class SingleJob implements Job { private final String name; private final Runnable runnable; - private boolean done; + private volatile boolean done; public SingleJob(String name, Runnable runnable) { this.name = name; diff --git a/core/src/main/java/art/arcane/iris/util/project/agent/Agent.java b/core/src/main/java/art/arcane/iris/util/project/agent/Agent.java new file mode 100644 index 000000000..a02243ac4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/agent/Agent.java @@ -0,0 +1,88 @@ +package art.arcane.iris.util.project.agent; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisLogging; +import net.bytebuddy.agent.ByteBuddyAgent; +import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; + +import java.io.File; +import java.io.InputStream; +import java.lang.instrument.Instrumentation; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; + +public class Agent { + private static final String NAME = "art.arcane.iris.util.project.agent.Installer"; + public static final File AGENT_JAR = new File(IrisPlatforms.get().dataFolder(), "agent.jar"); + + public static ClassReloadingStrategy installed() { + return ClassReloadingStrategy.of(getInstrumentation()); + } + + public static boolean isInstalled() { + return doGetInstrumentation() != null; + } + + public static Instrumentation getInstrumentation() { + Instrumentation instrumentation = doGetInstrumentation(); + if (instrumentation == null) throw new IllegalStateException("The agent is not initialized or unavailable"); + return instrumentation; + } + + public static boolean install() { + if (isInstalled()) + return true; + + if (!ensureAgentJar()) + return false; + + try { + IrisLogging.info("Installing Java Agent..."); + IrisLogging.info("Note: JVM [Attach Listener/ERROR] [STDERR] warning lines during this step are expected and not Iris errors."); + ByteBuddyAgent.attach(AGENT_JAR, ByteBuddyAgent.ProcessProvider.ForCurrentVm.INSTANCE); + } catch (Throwable e) { + IrisLogging.error("Failed to install Java Agent: " + e.getMessage()); + IrisLogging.reportError(e); + } + return doGetInstrumentation() != null; + } + + private static boolean ensureAgentJar() { + File parent = AGENT_JAR.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs() && !parent.exists()) { + IrisLogging.error("Failed to create Iris plugin data folder for Java agent: " + parent.getAbsolutePath()); + return false; + } + + try (InputStream in = openBundledAgentJar()) { + if (in == null) { + if (AGENT_JAR.isFile() && AGENT_JAR.length() > 0) { + IrisLogging.warn("Bundled agent.jar not found in Iris plugin jar. Reusing existing " + AGENT_JAR.getAbsolutePath()); + return true; + } + + IrisLogging.error("Bundled agent.jar was not found in Iris plugin jar. Rebuild/deploy Iris with embedded agent.jar."); + return false; + } + + Files.copy(in, AGENT_JAR.toPath(), StandardCopyOption.REPLACE_EXISTING); + return true; + } catch (Throwable e) { + IrisLogging.error("Failed to prepare Java agent jar: " + e.getMessage()); + IrisLogging.reportError(e); + return false; + } + } + + private static InputStream openBundledAgentJar() { + return Agent.class.getClassLoader().getResourceAsStream("agent.jar"); + } + + private static Instrumentation doGetInstrumentation() { + try { + return (Instrumentation) Class.forName(NAME, true, ClassLoader.getSystemClassLoader()).getMethod("getInstrumentation").invoke(null); + } catch (Exception ex) { + return null; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java new file mode 100644 index 000000000..66dd864ee --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java @@ -0,0 +1,197 @@ +package art.arcane.iris.util.project.context; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.EngineMetrics; +import art.arcane.iris.engine.object.FloatingIslandBoundarySampler; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.parallel.MultiBurst; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class ChunkContext { + private final int x; + private final int z; + private final IrisComplex complex; + private final long generationSessionId; + private final ChunkedDoubleDataCache height; + private final int[] roundedHeight; + private final ChunkedDataCache biome; + private final ChunkedDataCache cave; + private final ChunkedDataCache rock; + private final ChunkedDataCache fluid; + private final ChunkedDataCache region; + private final FloatingIslandBoundarySampler floatingIslandBoundarySampler; + + public ChunkContext(int x, int z, IrisComplex complex) { + this(x, z, complex, 0L, true, PrefillPlan.NO_CAVE, null); + } + + public ChunkContext(int x, int z, IrisComplex complex, boolean cache) { + this(x, z, complex, 0L, cache, PrefillPlan.NO_CAVE, null); + } + + public ChunkContext(int x, int z, IrisComplex complex, boolean cache, EngineMetrics metrics) { + this(x, z, complex, 0L, cache, PrefillPlan.NO_CAVE, metrics); + } + + public ChunkContext(int x, int z, IrisComplex complex, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) { + this(x, z, complex, 0L, cache, prefillPlan, metrics); + } + + public ChunkContext(int x, int z, IrisComplex complex, long generationSessionId, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) { + this.x = x; + this.z = z; + this.complex = complex; + this.generationSessionId = generationSessionId; + this.height = new ChunkedDoubleDataCache(complex.getHeightStream(), x, z, cache); + this.roundedHeight = new int[cache ? 256 : 0]; + this.biome = new ChunkedDataCache<>(complex.getTrueBiomeStream(), x, z, cache); + this.cave = new ChunkedDataCache<>(complex.getCaveBiomeStream(), x, z, cache); + this.rock = new ChunkedDataCache<>(complex.getRockStream(), x, z, cache); + this.fluid = new ChunkedDataCache<>(complex.getFluidStream(), x, z, cache); + this.region = new ChunkedDataCache<>(complex.getRegionStream(), x, z, cache); + this.floatingIslandBoundarySampler = new FloatingIslandBoundarySampler((wx, wz) -> complex.getTrueBiomeStream().get(wx, wz)); + + if (cache) { + PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan; + boolean capturePrefillMetric = metrics != null; + long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L; + List fillTasks = new ArrayList<>(6); + if (resolvedPlan.height) { + fillTasks.add(() -> height.fillRounded(roundedHeight)); + } + if (resolvedPlan.biome) { + fillTasks.add(new PrefillFillTask(biome)); + } + if (resolvedPlan.rock) { + fillTasks.add(new PrefillFillTask(rock)); + } + if (resolvedPlan.fluid) { + fillTasks.add(new PrefillFillTask(fluid)); + } + if (resolvedPlan.region) { + fillTasks.add(new PrefillFillTask(region)); + } + if (resolvedPlan.cave) { + fillTasks.add(new PrefillFillTask(cave)); + } + + if (!shouldPrefillAsync(fillTasks.size())) { + for (Runnable fillTask : fillTasks) { + fillTask.run(); + } + } else { + List> futures = new ArrayList<>(fillTasks.size()); + for (Runnable fillTask : fillTasks) { + futures.add(CompletableFuture.runAsync(fillTask, MultiBurst.burst)); + } + for (CompletableFuture future : futures) { + future.join(); + } + } + if (capturePrefillMetric) { + metrics.getContextPrefill().put((System.nanoTime() - totalStartNanos) / 1_000_000D); + } + } + } + + static boolean shouldPrefillAsync(int fillTaskCount) { + if (fillTaskCount <= 1 || !IrisPlatforms.isBound()) { + return false; + } + + return !MultiBurst.burst.ownsCurrentThread(); + } + + public int getX() { + return x; + } + + public int getZ() { + return z; + } + + public IrisComplex getComplex() { + return complex; + } + + public FloatingIslandBoundarySampler getFloatingIslandBoundarySampler() { + return floatingIslandBoundarySampler; + } + + public long getGenerationSessionId() { + return generationSessionId; + } + + public ChunkedDoubleDataCache getHeight() { + return height; + } + + public int getRoundedHeight(int x, int z) { + if (roundedHeight.length == 0) { + return (int) Math.round(height.getDouble(x, z)); + } + + return roundedHeight[(z << 4) + x]; + } + + public ChunkedDataCache getBiome() { + return biome; + } + + public ChunkedDataCache getCave() { + return cave; + } + + public ChunkedDataCache getRock() { + return rock; + } + + public ChunkedDataCache getFluid() { + return fluid; + } + + public ChunkedDataCache getRegion() { + return region; + } + + public enum PrefillPlan { + ALL(true, true, true, true, true, true), + NO_CAVE(true, true, false, true, true, true), + NONE(false, false, false, false, false, false); + + private final boolean height; + private final boolean biome; + private final boolean cave; + private final boolean rock; + private final boolean fluid; + private final boolean region; + + PrefillPlan(boolean height, boolean biome, boolean cave, boolean rock, boolean fluid, boolean region) { + this.height = height; + this.biome = biome; + this.cave = cave; + this.rock = rock; + this.fluid = fluid; + this.region = region; + } + } + + private static final class PrefillFillTask implements Runnable { + private final ChunkedDataCache dataCache; + + private PrefillFillTask(ChunkedDataCache dataCache) { + this.dataCache = dataCache; + } + + @Override + public void run() { + dataCache.fill(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDataCache.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDataCache.java new file mode 100644 index 000000000..2c578e75e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDataCache.java @@ -0,0 +1,69 @@ +package art.arcane.iris.util.project.context; + +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.utility.ChunkFillableStream2D; +import art.arcane.volmlib.util.documentation.BlockCoordinates; +import java.util.concurrent.Executor; + +public class ChunkedDataCache { + private final int x; + private final int z; + private final ProceduralStream stream; + private final boolean cache; + private final Object[] data; + + @BlockCoordinates + public ChunkedDataCache(ProceduralStream stream, int x, int z) { + this(stream, x, z, true); + } + + @BlockCoordinates + public ChunkedDataCache(ProceduralStream stream, int x, int z, boolean cache) { + this.x = x; + this.z = z; + this.stream = stream; + this.cache = cache; + this.data = new Object[cache ? 256 : 0]; + } + + public void fill() { + fill(null); + } + + public void fill(Executor executor) { + if (!cache) { + return; + } + + if (stream instanceof ChunkFillableStream2D cachedStream) { + cachedStream.fillChunkRaw(x, z, data); + return; + } + + for (int row = 0; row < 16; row++) { + int rowOffset = row * 16; + int worldZ = z + row; + for (int column = 0; column < 16; column++) { + data[rowOffset + column] = stream.get(x + column, worldZ); + } + } + } + + @BlockCoordinates + @SuppressWarnings("unchecked") + public T get(int x, int z) { + if (!cache) { + return stream.get(this.x + x, this.z + z); + } + + int index = (z * 16) + x; + T value = (T) data[index]; + if (value != null) { + return value; + } + + T sampled = stream.get(this.x + x, this.z + z); + data[index] = sampled; + return sampled; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java new file mode 100644 index 000000000..2f5a2c9a5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java @@ -0,0 +1,94 @@ +package art.arcane.iris.util.project.context; + +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D; +import art.arcane.iris.util.simd.SimdSupport; +import art.arcane.volmlib.util.documentation.BlockCoordinates; + +import java.util.Arrays; +import java.util.concurrent.Executor; + +public class ChunkedDoubleDataCache { + private final int x; + private final int z; + private final ProceduralStream stream; + private final boolean cache; + private final double[] data; + + @BlockCoordinates + public ChunkedDoubleDataCache(ProceduralStream stream, int x, int z) { + this(stream, x, z, true); + } + + @BlockCoordinates + public ChunkedDoubleDataCache(ProceduralStream stream, int x, int z, boolean cache) { + this.x = x; + this.z = z; + this.stream = stream; + this.cache = cache; + this.data = new double[cache ? 256 : 0]; + if (cache) { + Arrays.fill(this.data, Double.NaN); + } + } + + public void fill() { + fill(null); + } + + public void fill(Executor executor) { + fillRounded(null); + } + + public void fillRounded(int[] roundedTarget) { + if (!cache) { + if (roundedTarget != null) { + for (int row = 0; row < 16; row++) { + int rowOffset = row << 4; + int worldZ = z + row; + for (int column = 0; column < 16; column++) { + roundedTarget[rowOffset + column] = (int) Math.round(stream.getDouble(x + column, worldZ)); + } + } + } + return; + } + + if (stream instanceof ChunkFillableDoubleStream2D cachedStream) { + cachedStream.fillChunkDoubles(x, z, data); + if (roundedTarget != null) { + SimdSupport.kernels().roundToInt(data, roundedTarget, 256); + } + return; + } + + for (int row = 0; row < 16; row++) { + int rowOffset = row << 4; + int worldZ = z + row; + for (int column = 0; column < 16; column++) { + double sampled = stream.getDouble(x + column, worldZ); + data[rowOffset + column] = sampled; + if (roundedTarget != null) { + roundedTarget[rowOffset + column] = (int) Math.round(sampled); + } + } + } + } + + @BlockCoordinates + public double getDouble(int x, int z) { + if (!cache) { + return stream.getDouble(this.x + x, this.z + z); + } + + int index = (z << 4) + x; + double value = data[index]; + if (!Double.isNaN(value)) { + return value; + } + + double sampled = stream.getDouble(this.x + x, this.z + z); + data[index] = sampled; + return sampled; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/context/IrisContext.java b/core/src/main/java/art/arcane/iris/util/project/context/IrisContext.java new file mode 100644 index 000000000..4f3b46def --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/context/IrisContext.java @@ -0,0 +1,132 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.context; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.volmlib.util.collection.KMap; + +import java.util.Objects; + +public final class IrisContext { + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + private final Engine engine; + private final ChunkContext chunkContext; + private final long generationSessionId; + + private IrisContext(Engine engine, long generationSessionId, ChunkContext chunkContext) { + this.engine = Objects.requireNonNull(engine); + this.generationSessionId = generationSessionId; + this.chunkContext = chunkContext; + } + + public static IrisContext get() { + return CONTEXT.get(); + } + + public static IrisContext require() { + IrisContext current = get(); + if (current == null) { + throw new IllegalStateException("No Iris execution context is bound to thread " + Thread.currentThread().getName() + "."); + } + return current; + } + + public static Scope open(Engine engine, long generationSessionId, ChunkContext chunkContext) { + Thread thread = Thread.currentThread(); + IrisContext previous = CONTEXT.get(); + IrisContext installed = new IrisContext(engine, generationSessionId, chunkContext); + CONTEXT.set(installed); + return new Scope(thread, previous, installed); + } + + public Engine getEngine() { + return engine; + } + + public ChunkContext getChunkContext() { + return chunkContext; + } + + public long getGenerationSessionId() { + return generationSessionId; + } + + public IrisData getData() { + return engine.getData(); + } + + public IrisComplex getComplex() { + return engine.getComplex(); + } + + public KMap asContext() { + Long hash32 = engine.getHash32().getNow(null); + IrisDimension dimension = engine.getDimension(); + EngineMantle mantle = engine.getMantle(); + return new KMap() + .qput("studio", engine.isStudio()) + .qput("closed", engine.isClosed()) + .qput("generationSessionId", generationSessionId) + .qput("pack", new KMap<>() + .qput("key", dimension == null ? "" : dimension.getLoadKey()) + .qput("version", dimension == null ? "" : dimension.getVersion()) + .qput("hash", hash32 == null ? "" : Long.toHexString(hash32))) + .qput("mantle", new KMap<>() + .qput("idle", mantle.getAdjustedIdleDuration()) + .qput("loaded", mantle.getLoadedRegionCount()) + .qput("queued", mantle.getUnloadRegionCount())); + } + + public static final class Scope implements AutoCloseable { + private final Thread owner; + private final IrisContext previous; + private final IrisContext installed; + private boolean closed; + + private Scope(Thread owner, IrisContext previous, IrisContext installed) { + this.owner = owner; + this.previous = previous; + this.installed = installed; + } + + @Override + public void close() { + if (closed) { + return; + } + if (Thread.currentThread() != owner) { + throw new IllegalStateException("Iris execution context scope closed from a different thread."); + } + IrisContext current = CONTEXT.get(); + if (current != installed) { + throw new IllegalStateException("Iris execution context scopes must close in LIFO order."); + } + if (previous == null) { + CONTEXT.remove(); + } else { + CONTEXT.set(previous); + } + closed = true; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java new file mode 100644 index 000000000..57e10df6f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java @@ -0,0 +1,1205 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.hunk; + +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.function.Consumer2; +import art.arcane.volmlib.util.function.Consumer3; +import art.arcane.volmlib.util.function.Consumer4; +import art.arcane.volmlib.util.function.Consumer4IO; +import art.arcane.volmlib.util.function.Consumer5; +import art.arcane.volmlib.util.function.Consumer6; +import art.arcane.volmlib.util.function.Consumer8; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.volmlib.util.function.Function4; +import art.arcane.volmlib.util.function.Supplier3R; +import art.arcane.volmlib.util.hunk.HunkComputeSupport; +import art.arcane.volmlib.util.hunk.HunkCoreSupport; +import art.arcane.volmlib.util.hunk.HunkInterpolationSupport; +import art.arcane.volmlib.util.hunk.HunkMutationSupport; +import art.arcane.volmlib.util.hunk.HunkSectionSupport; +import art.arcane.volmlib.util.hunk.HunkLike; +import art.arcane.iris.util.project.hunk.storage.ArrayHunk; +import art.arcane.iris.util.project.hunk.storage.AtomicDoubleHunk; +import art.arcane.iris.util.project.hunk.storage.AtomicHunk; +import art.arcane.iris.util.project.hunk.storage.AtomicIntegerHunk; +import art.arcane.iris.util.project.hunk.storage.AtomicLongHunk; +import art.arcane.iris.util.project.hunk.storage.SynchronizedArrayHunk; +import art.arcane.iris.util.project.hunk.view.ChunkBiomeHunkView; +import art.arcane.iris.util.project.hunk.view.ChunkDataHunkView; +import art.arcane.iris.util.project.hunk.view.ChunkHunkView; +import art.arcane.iris.util.project.hunk.view.TerrainChunkBiomeHunkView; +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +import art.arcane.iris.util.project.interpolation.InterpolationMethod3D; +import art.arcane.iris.util.project.interpolation.Interpolation3D; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; +import org.bukkit.Chunk; +import org.bukkit.generator.ChunkGenerator.ChunkData; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +@SuppressWarnings("ALL") +public interface Hunk extends HunkLike { + static Hunk adapt(HunkLike hunk) { + if (hunk == null) { + return null; + } + + if (hunk instanceof Hunk existing) { + return (Hunk) existing; + } + + return new SharedHunkAdapter<>(hunk); + } + + static HunkLike unwrap(HunkLike hunk) { + if (hunk instanceof SharedHunkAdapter adapter) { + return (HunkLike) adapter.delegate(); + } + + return hunk; + } + + /** + * Create a hunk view from a source hunk. This view reads and writes through to + * the source hunk. Its is not a copy. + * + * @param the type + * @param src the source hunk + * @return the hunk view + */ + static Hunk view(Hunk src) { + return adapt(new art.arcane.volmlib.util.hunk.view.HunkView<>(unwrap(src))); + } + + static Hunk convertedReadView(Hunk src, Function reader) { + return adapt(new art.arcane.volmlib.util.hunk.view.FunctionalHunkView<>(unwrap(src), reader, null)); + } + + static Hunk convertedWriteView(Hunk src, Function writer) { + return adapt(new art.arcane.volmlib.util.hunk.view.FunctionalHunkView<>(unwrap(src), null, writer)); + } + + static Hunk convertedReadWriteView(Hunk src, Function reader, Function writer) { + return adapt(new art.arcane.volmlib.util.hunk.view.FunctionalHunkView<>(unwrap(src), reader, writer)); + } + + static Hunk fringe(Hunk i, Hunk o) { + return adapt(new art.arcane.volmlib.util.hunk.view.FringedHunkView<>(unwrap(i), unwrap(o))); + } + + static Hunk view(TerrainChunk src) { + return new ChunkDataHunkView(src.getChunkData()); + } + + static Hunk viewBiomes(TerrainChunk src) { + return new TerrainChunkBiomeHunkView(src); + } + + static Hunk view(ChunkData src) { + return new ChunkDataHunkView(src); + } + + static Hunk viewBlocks(Chunk src) { + return new ChunkHunkView(src); + } + + static Hunk viewBiomes(Chunk src) { + return new ChunkBiomeHunkView(src); + } + + static Hunk newHunk(int w, int h, int d) { + return newArrayHunk(w, h, d); + } + + @SafeVarargs + static Hunk newCombinedHunk(Hunk... hunks) { + return newCombinedArrayHunk(hunks); + } + + static Hunk newArrayHunk(int w, int h, int d) { + return new ArrayHunk<>(w, h, d); + } + + @SafeVarargs + static Hunk newCombinedArrayHunk(Hunk... hunks) { + return combined(Hunk::newArrayHunk, hunks); + } + + static Hunk newSynchronizedArrayHunk(int w, int h, int d) { + return new SynchronizedArrayHunk<>(w, h, d); + } + + @SafeVarargs + static Hunk newCombinedSynchronizedArrayHunk(Hunk... hunks) { + return combined(Hunk::newSynchronizedArrayHunk, hunks); + } + + static Hunk newMappedHunk(int w, int h, int d) { + return adapt(new art.arcane.volmlib.util.hunk.storage.MappedHunk<>(w, h, d)); + } + + static Hunk newMappedHunkSynced(int w, int h, int d) { + return adapt(new art.arcane.volmlib.util.hunk.storage.MappedSyncHunk<>(w, h, d)); + } + + @SafeVarargs + static Hunk newCombinedMappedHunk(Hunk... hunks) { + return combined(Hunk::newMappedHunk, hunks); + } + + static Hunk newAtomicHunk(int w, int h, int d) { + return new AtomicHunk<>(w, h, d); + } + + @SafeVarargs + static Hunk newCombinedAtomicHunk(Hunk... hunks) { + return combined(Hunk::newAtomicHunk, hunks); + } + + static Hunk newAtomicDoubleHunk(int w, int h, int d) { + return new AtomicDoubleHunk(w, h, d); + } + + @SafeVarargs + static Hunk newCombinedAtomicDoubleHunk(Hunk... hunks) { + return combined(Hunk::newAtomicDoubleHunk, hunks); + } + + static Hunk newAtomicLongHunk(int w, int h, int d) { + return new AtomicLongHunk(w, h, d); + } + + @SafeVarargs + static Hunk newCombinedAtomicLongHunk(Hunk... hunks) { + return combined(Hunk::newAtomicLongHunk, hunks); + } + + static Hunk newAtomicIntegerHunk(int w, int h, int d) { + return new AtomicIntegerHunk(w, h, d); + } + + @SafeVarargs + static Hunk newCombinedAtomicIntegerHunk(Hunk... hunks) { + return combined(Hunk::newAtomicIntegerHunk, hunks); + } + + /** + * Creates a new bounding hunk from the given hunks + * + * @param the type + * @param factory the factory that creates a hunk + * @param hunks the hunks + * @return the new bounding hunk + */ + @SafeVarargs + static Hunk combined(Function3> factory, Hunk... hunks) { + int w = 0; + int h = 0; + int d = 0; + + for (Hunk i : hunks) { + w = Math.max(w, i.getWidth()); + h = Math.max(h, i.getHeight()); + d = Math.max(d, i.getDepth()); + } + + Hunk b = factory.apply(w, h, d); + + for (Hunk i : hunks) { + b.insert(i); + } + + return b; + } + + static void computeDual2D(int parallelism, Hunk a, Hunk b, Consumer5, Hunk> v) { + if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight() || a.getDepth() != b.getDepth()) { + throw new RuntimeException("Hunk sizes must match!"); + } + + if (a.get2DDimension(parallelism) == 1) { + v.accept(0, 0, 0, a, b); + return; + } + + HunkComputeSupport.computeDualMerged( + parallelism, + p -> MultiBurst.burst.burst(p), + consumer -> getDualSections2D(parallelism, a, b, consumer, (x, y, z, hax, hbx) -> + { + a.insert(x, y, z, hax); + b.insert(x, y, z, hbx); + }), + v); + } + + static void getDualSections2D(int sections, Hunk a, Hunk b, Consumer6, Hunk, Runnable> v, Consumer5, Hunk> inserterAB) { + if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight() || a.getDepth() != b.getDepth()) { + throw new RuntimeException("Hunk sizes must match!"); + } + + HunkSectionSupport.forEach2DSectionBounds(a, sections, (x, y, z, x1, y1, z1) -> + getDualSection(x, y, z, x1, y1, z1, a, b, (ha, hr, r) -> v.accept(x, y, z, ha, hr, r), inserterAB)); + } + + static void getDualSection(int x, int y, int z, int x1, int y1, int z1, Hunk a, Hunk b, Consumer3, Hunk, Runnable> v, Consumer5, Hunk> inserter) { + Hunk copya = a.crop(x, y, z, x1, y1, z1); + Hunk copyb = b.crop(x, y, z, x1, y1, z1); + v.accept(copya, copyb, () -> inserter.accept(x, y, z, copya, copyb)); + } + + /** + * Create a hunk that is optimized for specific uses + * + * @param w width + * @param h height + * @param d depth + * @param type the class type + * @param packed if the hunk is generally more than 50% full (non-null nodes) + * @param concurrent if this hunk must be thread safe + * @param the type + * @return the hunk + */ + static Hunk newHunk(int w, int h, int d, Class type, boolean packed, boolean concurrent) { + if (type.equals(Double.class)) { + return concurrent ? + packed ? (Hunk) newAtomicDoubleHunk(w, h, d) : newMappedHunk(w, h, d) + : packed ? newArrayHunk(w, h, d) : newMappedHunkSynced(w, h, d); + } + + if (type.equals(Integer.class)) { + return concurrent ? + packed ? (Hunk) newAtomicIntegerHunk(w, h, d) : newMappedHunk(w, h, d) + : packed ? newArrayHunk(w, h, d) : newMappedHunkSynced(w, h, d); + } + + if (type.equals(Long.class)) { + return concurrent ? + packed ? (Hunk) newAtomicLongHunk(w, h, d) : newMappedHunk(w, h, d) + : packed ? newArrayHunk(w, h, d) : newMappedHunkSynced(w, h, d); + } + + return concurrent ? + packed ? newAtomicHunk(w, h, d) : newMappedHunk(w, h, d) + : packed ? newArrayHunk(w, h, d) : newMappedHunkSynced(w, h, d); + } + + static IrisPosition rotatedBounding(int w, int h, int d, double x, double y, double z) { + int[] bounds = HunkCoreSupport.rotatedBoundsSize(w, h, d, x, y, z); + return new IrisPosition(bounds[0], bounds[1], bounds[2]); + } + + static int max(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) { + return HunkCoreSupport.max8(a1, a2, a3, a4, a5, a6, a7, a8); + } + + static int min(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) { + return HunkCoreSupport.min8(a1, a2, a3, a4, a5, a6, a7, a8); + } + + static void rotate(double x, double y, double z, int[] c) { + HunkCoreSupport.rotate(x, y, z, c); + } + + static void rotateAroundX(double a, int[] c) { + HunkCoreSupport.rotateAroundX(a, c); + } + + static void rotateAroundX(double cos, double sin, int[] c) { + HunkCoreSupport.rotateAroundX(cos, sin, c); + } + + static void rotateAroundY(double a, int[] c) { + HunkCoreSupport.rotateAroundY(a, c); + } + + static void rotateAroundY(double cos, double sin, int[] c) { + HunkCoreSupport.rotateAroundY(cos, sin, c); + } + + static void rotateAroundZ(double a, int[] c) { + HunkCoreSupport.rotateAroundZ(a, c); + } + + static void rotateAroundZ(double cos, double sin, int[] c) { + HunkCoreSupport.rotateAroundZ(cos, sin, c); + } + + default boolean isMapped() { + return false; + } + + default int getEntryCount() { + return getWidth() * getHeight() * getDepth(); + } + + default Hunk listen(Consumer4 l) { + return adapt(new art.arcane.volmlib.util.hunk.view.ListeningHunk<>(unwrap(this), l)); + } + + default Hunk synchronize() { + return adapt(new art.arcane.volmlib.util.hunk.view.SynchronizedHunkView<>(unwrap(this))); + } + + default Hunk trackWrite(AtomicBoolean b) { + return adapt(new art.arcane.volmlib.util.hunk.view.WriteTrackHunk<>(unwrap(this), b)); + } + + default Hunk readOnly() { + return adapt(new art.arcane.volmlib.util.hunk.view.ReadOnlyHunk<>(unwrap(this))); + } + + default int getNonNullEntries() { + AtomicInteger count = new AtomicInteger(); + iterate((x, y, z, v) -> count.getAndAdd(1)); + + return count.get(); + } + + default boolean isAtomic() { + return false; + } + + default Hunk invertY() { + return adapt(new art.arcane.volmlib.util.hunk.view.InvertedHunkView<>(unwrap(this))); + } + + default int getMaximumDimension() { + return HunkCoreSupport.getMaximumDimension(this); + } + + default int getIdeal2DParallelism() { + return getMax2DParallelism() / 4; + } + + default int getIdeal3DParallelism() { + return getMax3DParallelism() / 8; + } + + default int getMinimumDimension() { + return HunkCoreSupport.getMinimumDimension(this); + } + + default int getMax2DParallelism() { + return HunkCoreSupport.getMax2DParallelism(this); + } + + default int getMax3DParallelism() { + return HunkCoreSupport.getMax3DParallelism(this); + } + + default int filterDimension(int dim) { + return HunkCoreSupport.filterDimension(dim, getMinimumDimension()); + } + + default int get2DDimension(int sections) { + return HunkCoreSupport.get2DDimension(sections, getMinimumDimension()); + } + + default int get3DDimension(int sections) { + return HunkCoreSupport.get3DDimension(sections, getMinimumDimension()); + } + + /** + * Iterate surfaces on 2d. Raytraces with a front and a back which stretches + * through surfaces. Essentially what is returned is the following (in + * order)
+ *
+ *

+ * The predicate is used to determine if the given block type is solid or not. + * + *
+ *
+ * ================================================
+ * AX, AZ: Hunk Relative X and Z + * + *
+ *
+ * HX, HZ: Hunk Positional X and Z (in its parent hunk) + * + *
+ *
+ * TOP: The top of this surface (top+1 is air above a surface) + * + *
+ *
+ * BOTTOM: The bottom of this surface (bottom is the lowest SOLID surface before + * either air or bedrock going down further) + * + *
+ *
+ * LAST_BOTTOM: The previous bottom. If your surface is the top surface, this + * will be -1 as there is no bottom-of-surface above you. However if you are not + * the top surface, this value is equal to the next solid layer above TOP, such + * that ((LAST_BOTTOM - 1) - (TOP + 1)) is how many air blocks are between your + * surface and the surface above you + * + *
+ *
+ * HUNK: The hunk to set data to.
+ * ================================================
+ *
+ * If we assume your chunk coordinates are x and z, then
+ *
+ * bX = (x * 16)
+ * bZ = (z * 16)
+ *
+ * (ax, az, hx, hz, top, bottom, lastBottom, hunk) {
+ * actualBlockX = ax+hx;
+ * actualBlockZ = az+hz;
+ *
+ * hunkX = ax;
+ * hunkZ = az;
+ *
+ * hunk.set(hunkX, ?, hunkZ, noise(actualBlockX, ?, actualBlockZ));
+ * }
+ * + * @param p the predicate + * @param c the consumer + * @return this + */ + default Hunk iterateSurfaces2D(Predicate p, Consumer8> c) { + return iterateSurfaces2D(getIdeal2DParallelism(), p, c); + } + + /** + * Iterate surfaces on 2d. Raytraces with a front and a back which stretches + * through surfaces. Essentially what is returned is the following (in + * order)
+ *
+ *

+ * The predicate is used to determine if the given block type is solid or not. + * + *
+ *
+ * ================================================
+ * AX, AZ: Hunk Relative X and Z + * + *
+ *
+ * HX, HZ: Hunk Positional X and Z (in its parent hunk) + * + *
+ *
+ * TOP: The top of this surface (top+1 is air above a surface) + * + *
+ *
+ * BOTTOM: The bottom of this surface (bottom is the lowest SOLID surface before + * either air or bedrock going down further) + * + *
+ *
+ * LAST_BOTTOM: The previous bottom. If your surface is the top surface, this + * will be -1 as there is no bottom-of-surface above you. However if you are not + * the top surface, this value is equal to the next solid layer above TOP, such + * that ((LAST_BOTTOM - 1) - (TOP + 1)) is how many air blocks are between your + * surface and the surface above you + * + *
+ *
+ * HUNK: The hunk to set data to.
+ * ================================================
+ *
+ * If we assume your chunk coordinates are x and z, then
+ *
+ * bX = (x * 16)
+ * bZ = (z * 16)
+ *
+ * (ax, az, hx, hz, top, bottom, lastBottom, hunk) {
+ * actualBlockX = ax+hx;
+ * actualBlockZ = az+hz;
+ *
+ * hunkX = ax;
+ * hunkZ = az;
+ *
+ * hunk.set(hunkX, ?, hunkZ, noise(actualBlockX, ?, actualBlockZ));
+ * }
+ * + * @param parallelism the ideal threads to use on this + * @param p the predicate + * @param c the consumer + * @return this + */ + default Hunk iterateSurfaces2D(int parallelism, Predicate p, Consumer8> c) { + iterate2DTop(parallelism, (ax, az, hox, hoz, h) -> + { + int last = -1; + int in = getHeight() - 1; + boolean hitting = false; + for (int i = getHeight() - 1; i >= 0; i--) { + boolean solid = p.test(h.get(ax, i, az)); + + if (!hitting && solid) { + in = i; + hitting = true; + } else if (hitting && !solid) { + hitting = false; + c.accept(ax, az, hox, hoz, in, i - 1, last, h); + last = i - 1; + } + } + + if (hitting) { + c.accept(ax, az, hox, hoz, in, 0, last, h); + } + }); + + return this; + } + + /** + * Iterate on the xz top of this hunk. When using this consumer, given + *

+ * consumer: (ax, az, hx, hz, hunk) + *

+ * hunk.set(ax, ?, az, NOISE.get(ax+hx, az+hz)); + * + * @param c the consumer hunkX, hunkZ, hunkOffsetX, hunkOffsetZ. + * @return this + */ + default Hunk iterate2DTop(Consumer5> c) { + return iterate2DTop(getIdeal2DParallelism(), c); + } + + default Hunk drift(int x, int y, int z) { + return adapt(new art.arcane.volmlib.util.hunk.view.DriftHunkView<>(unwrap(this), x, y, z)); + } + + /** + * Iterate on the xz top of this hunk. When using this consumer, given + *

+ * consumer: (ax, az, hx, hz, hunk) + *

+ * hunk.set(ax, ?, az, NOISE.get(ax+hx, az+hz)); + * + * @param parallelism the target parallelism value or 0 to disable + * @param c the consumer hunkX, hunkZ, hunkOffsetX, hunkOffsetZ. + * @return this + */ + default Hunk iterate2DTop(int parallelism, Consumer5> c) { + compute2D(parallelism, (x, y, z, h) -> + { + for (int i = 0; i < h.getWidth(); i++) { + for (int k = 0; k < h.getDepth(); k++) { + c.accept(i, k, x, z, h); + } + } + }); + + return this; + } + + default Hunk iterate(Predicate p, Consumer3 c) { + return iterate(getIdeal3DParallelism(), p, c); + } + + default Hunk iterate(int parallelism, Predicate p, Consumer3 c) { + iterate(parallelism, (x, y, z, t) -> + { + if (p.test(t)) { + c.accept(x, y, z); + } + }); + + return this; + } + + default Hunk iterate(Predicate p, Consumer4 c) { + return iterate(getIdeal3DParallelism(), p, c); + } + + default Hunk iterate(int parallelism, Predicate p, Consumer4 c) { + iterate(parallelism, (x, y, z, t) -> + { + if (p.test(t)) { + c.accept(x, y, z, t); + } + }); + + return this; + } + + default Hunk iterate(Consumer3 c) { + return iterate(getIdeal3DParallelism(), c); + } + + default Hunk iterateSync(Consumer3 c) { + for (int i = 0; i < getWidth(); i++) { + for (int j = 0; j < getHeight(); j++) { + for (int k = 0; k < getDepth(); k++) { + c.accept(i, j, k); + } + } + } + + return this; + } + + default Hunk iterateSync(Consumer4 c) { + for (int i = 0; i < getWidth(); i++) { + for (int j = 0; j < getHeight(); j++) { + for (int k = 0; k < getDepth(); k++) { + c.accept(i, j, k, get(i, j, k)); + } + } + } + + return this; + } + + default Hunk updateSync(Function4 c) { + for (int i = 0; i < getWidth(); i++) { + for (int j = 0; j < getHeight(); j++) { + for (int k = 0; k < getDepth(); k++) { + set(i, j, k, c.apply(i, j, k, get(i, j, k))); + } + } + } + + return this; + } + + default Hunk iterateSyncIO(Consumer4IO c) throws IOException { + for (int i = 0; i < getWidth(); i++) { + for (int j = 0; j < getHeight(); j++) { + for (int k = 0; k < getDepth(); k++) { + c.accept(i, j, k, get(i, j, k)); + } + } + } + + return this; + } + + default Hunk iterate(int parallelism, Consumer3 c) { + compute3D(parallelism, (x, y, z, h) -> + { + for (int i = 0; i < h.getWidth(); i++) { + for (int j = 0; j < h.getHeight(); j++) { + for (int k = 0; k < h.getDepth(); k++) { + c.accept(i + x, j + y, k + z); + } + } + } + }); + + return this; + } + + default Hunk iterate(Consumer4 c) { + return iterate(getIdeal3DParallelism(), c); + } + + default Hunk iterate(int parallelism, Consumer4 c) { + compute3D(parallelism, (x, y, z, h) -> + { + for (int i = 0; i < h.getWidth(); i++) { + for (int j = 0; j < h.getHeight(); j++) { + for (int k = 0; k < h.getDepth(); k++) { + c.accept(i + x, j + y, k + z, h.get(i, j, k)); + } + } + } + }); + + return this; + } + + default Hunk compute2D(Consumer4> v) { + return compute2D(getIdeal2DParallelism(), v); + } + + default Hunk compute2D(int parallelism, Consumer4> v) { + if (get2DDimension(parallelism) == 1) { + v.accept(0, 0, 0, this); + return this; + } + + if (isAtomic()) { + HunkComputeSupport.computeAtomic( + parallelism, + p -> MultiBurst.burst.burst(p), + sections -> getSectionsAtomic2D(parallelism, sections), + v); + } else { + HunkComputeSupport.computeMerged( + parallelism, + p -> MultiBurst.burst.burst(p), + sections -> getSections2D(parallelism, sections, this::insert), + v); + } + + return this; + } + + default Hunk compute2DYRange(int parallelism, int ymin, int ymax, Consumer4> v) { + if (get2DDimension(parallelism) == 1) { + v.accept(0, 0, 0, this); + return this; + } + + HunkComputeSupport.computeMerged( + parallelism, + p -> MultiBurst.burst.burst(p), + sections -> getSections2DYLimit(parallelism, ymin, ymax, sections, this::insert), + v); + return this; + } + + default Hunk compute3D(Consumer4> v) { + return compute3D(getIdeal3DParallelism(), v); + } + + default Hunk compute3D(int parallelism, Consumer4> v) { + if (get3DDimension(parallelism) == 1) { + v.accept(0, 0, 0, this); + return this; + } + + HunkComputeSupport.computeMerged( + parallelism, + p -> MultiBurst.burst.burst(p), + sections -> getSections3D(parallelism, sections, this::insert), + v); + return this; + } + + default Hunk getSections2D(int sections, Consumer5, Runnable> v) { + return getSections2D(sections, v, this::insert); + } + + default Hunk getSectionsAtomic2D(int sections, Consumer4> v) { + HunkSectionSupport.forEachAtomic2DSectionBounds(this, sections, (x, y, z, x1, y1, z1) -> + getAtomicSection(x, y, z, x1, y1, z1, (h) -> v.accept(x, y, z, h))); + return this; + } + + default Hunk getSections2D(int sections, Consumer5, Runnable> v, Consumer4> inserter) { + HunkSectionSupport.forEach2DSectionBounds(this, sections, (x, y, z, x1, y1, z1) -> + getSection(x, y, z, x1, y1, z1, (h, r) -> v.accept(x, y, z, h, r), inserter)); + return this; + } + + default Hunk getSections2DYLimit(int sections, int ymin, int ymax, Consumer5, Runnable> v, Consumer4> inserter) { + HunkSectionSupport.forEach2DYRangeSectionBounds(this, sections, ymin, ymax, (x, y, z, x1, y1, z1) -> + getSection(x, y, z, x1, y1, z1, (h, r) -> v.accept(x, y, z, h, r), inserter)); + return this; + } + + default Hunk getSections3D(int sections, Consumer5, Runnable> v) { + return getSections3D(sections, v, (xx, yy, zz, c) -> insert(xx, yy, zz, c)); + } + + default Hunk getSections3D(int sections, Consumer5, Runnable> v, Consumer4> inserter) { + HunkSectionSupport.forEach3DSectionBounds(this, sections, (x, y, z, x1, y1, z1) -> + getSection(x, y, z, x1, y1, z1, (h, r) -> v.accept(x, y, z, h, r), inserter)); + return this; + } + + default Hunk getSection(int x, int y, int z, int x1, int y1, int z1, Consumer2, Runnable> v) { + return getSection(x, y, z, x1, y1, z1, v, (xx, yy, zz, c) -> insert(xx, yy, zz, c)); + } + + default Hunk getSection(int x, int y, int z, int x1, int y1, int z1, Consumer2, Runnable> v, Consumer4> inserter) { + Hunk copy = crop(x, y, z, x1, y1, z1); + v.accept(copy, () -> inserter.accept(x, y, z, copy)); + return this; + } + + default Hunk getAtomicSection(int x, int y, int z, int x1, int y1, int z1, Consumer> v) { + Hunk copy = croppedView(x, y, z, x1, y1, z1); + v.accept(copy); + return this; + } + + /** + * Create a new hunk from a section of this hunk. + * + * @param x1 The min x (inclusive) + * @param y1 The min y (inclusive) + * @param z1 The min z (inclusive) + * @param x2 The max x (exclusive) + * @param y2 The max y (exclusive) + * @param z2 The max z (exclusive) + * @return the new hunk (x2-x1, y2-y1, z2-z1) + */ + default ArrayHunk crop(int x1, int y1, int z1, int x2, int y2, int z2) { + ArrayHunk h = new ArrayHunk(x2 - x1, y2 - y1, z2 - z1); + + for (int i = x1; i < x2; i++) { + for (int j = y1; j < y2; j++) { + for (int k = z1; k < z2; k++) { + h.setRaw(i - x1, j - y1, k - z1, getRaw(i, j, k)); + } + } + } + + return h; + } + + /** + * Create a new view of this same hunk from a section of this hunk. + * Modifications are routed to this hunk! + * + * @param x1 The min x (inclusive) + * @param y1 The min y (inclusive) + * @param z1 The min z (inclusive) + * @param x2 The max x (exclusive) + * @param y2 The max y (exclusive) + * @param z2 The max z (exclusive) + * @return the cropped view of this hunk (x2-x1, y2-y1, z2-z1) + */ + default Hunk croppedView(int x1, int y1, int z1, int x2, int y2, int z2) { + return adapt(new art.arcane.volmlib.util.hunk.view.HunkView<>(unwrap(this), x2 - x1, y2 - y1, z2 - z1, x1, y1, z1)); + } + + /** + * @return The X length + */ + int getWidth(); + + /** + * @return The Z length + */ + int getDepth(); + + /** + * @return The Y length + */ + int getHeight(); + + /** + * Set a region + * + * @param x1 inclusive 1st x + * @param y1 inclusive 1st y + * @param z1 inclusive 1st z + * @param x2 inclusive 2nd x + * @param y2 inclusive 2nd y + * @param z2 inclusive 2nd z + * @param t the value to set + */ + default void set(int x1, int y1, int z1, int x2, int y2, int z2, T t) { + HunkMutationSupport.setRangeInclusive(this, x1, y1, z1, x2, y2, z2, t); + } + + /** + * Get the value to the closest valid position + * + * @param x the x + * @param y the y + * @param z the z + * @return the value closest to the border of the hunk + */ + default T getClosest(int x, int y, int z) { + return HunkMutationSupport.getClosest(this, x, y, z); + } + + default BlockPosition getCenter() { + return new BlockPosition(getCenterX(), getCenterY(), getCenterZ()); + } + + default int getCenterX() { + return (int) Math.floor(getWidth() / 2); + } + + default int getCenterY() { + return (int) Math.floor(getHeight() / 2); + } + + default int getCenterZ() { + return (int) Math.floor(getDepth() / 2); + } + + default void fill(T t) { + HunkMutationSupport.fill(this, t); + } + + /** + * Get a 1 node thick hunk representing the face of this hunk + * + * @param f the face + * @return the hunk view of this hunk + */ + default Hunk viewFace(HunkFace f) { + switch (f) { + case BOTTOM: + return croppedView(0, 0, 0, getWidth() - 1, 0, getDepth() - 1); + case EAST: + return croppedView(getWidth() - 1, 0, 0, getWidth() - 1, getHeight() - 1, getDepth() - 1); + case NORTH: + return croppedView(0, 0, 0, getWidth() - 1, getHeight() - 1, 0); + case SOUTH: + return croppedView(0, 0, 0, 0, getHeight() - 1, getDepth() - 1); + case TOP: + return croppedView(0, getHeight() - 1, 0, getWidth() - 1, getHeight() - 1, getDepth() - 1); + case WEST: + return croppedView(0, 0, getDepth() - 1, getWidth() - 1, getHeight() - 1, getDepth() - 1); + default: + break; + } + + return null; + } + + /** + * Crop (copy) a 1 node thick hunk representing the face of this hunk + * + * @param f the face + * @return the hunk copy (face) of this hunk + */ + default Hunk cropFace(HunkFace f) { + switch (f) { + case BOTTOM: + return crop(0, 0, 0, getWidth() - 1, 0, getDepth() - 1); + case EAST: + return crop(getWidth() - 1, 0, 0, getWidth() - 1, getHeight() - 1, getDepth() - 1); + case NORTH: + return crop(0, 0, 0, getWidth() - 1, getHeight() - 1, 0); + case SOUTH: + return crop(0, 0, 0, 0, getHeight() - 1, getDepth() - 1); + case TOP: + return crop(0, getHeight() - 1, 0, getWidth() - 1, getHeight() - 1, getDepth() - 1); + case WEST: + return crop(0, 0, getDepth() - 1, getWidth() - 1, getHeight() - 1, getDepth() - 1); + default: + break; + } + + return null; + } + + /** + * Set a value at the given position + * + * @param x the x + * @param y the y + * @param z the z + * @param t the value + */ + default void set(int x, int y, int z, T t) { + if (!contains(x, y, z)) { + // The clamp on the default write path: callers write past the edge of a hunk by design and + // rely on the write being dropped, so this is a trace of a normal event, not a problem. + IrisLogging.debug("OUT OF BOUNDS " + x + " " + y + " " + z + " in bounds " + getWidth() + " " + getHeight() + " " + getDepth()); + return; + } + + setRaw(x, y, z, t); + } + + default void setIfExists(int x, int y, int z, T t) { + HunkMutationSupport.setIfExists(this, x, y, z, t); + } + + default T getIfExists(int x, int y, int z, T t) { + return HunkMutationSupport.getIfExists(this, x, y, z, t); + } + + default T getIfExists(int x, int y, int z) { + return getIfExists(x, y, z, null); + } + + /** + * Set a value at the given position without checking coordinate bounds + * + * @param x the x + * @param y the y + * @param z the z + * @param t the value + */ + void setRaw(int x, int y, int z, T t); + + /** + * Get a value at the given position without checking coordinate bounds + * + * @param x the x + * @param y the y + * @param z the z + * @return the value or null + */ + T getRaw(int x, int y, int z); + + /** + * Get a value at the given position + * + * @param x the x + * @param y the y + * @param z the z + * @return the value or null + */ + default T get(int x, int y, int z) { + return getRaw(x, y, z); + } + + default T getOr(int x, int y, int z, T t) { + return HunkMutationSupport.getOr(this, x, y, z, t); + } + + /** + * Insert a hunk into this one with an offset the inserted hunk + * + * @param offX the offset from zero for x + * @param offY the offset from zero for y + * @param offZ the offset from zero for z + * @param hunk the hunk to insert + */ + default void insert(int offX, int offY, int offZ, Hunk hunk) { + insert(offX, offY, offZ, hunk, false); + } + + default void insertSoftly(int offX, int offY, int offZ, Hunk hunk, Predicate shouldOverwrite) { + insertSoftly(offX, offY, offZ, hunk, false, shouldOverwrite); + } + + /** + * Insert a hunk into this one + * + * @param hunk the hunk to insert + */ + default void insert(Hunk hunk) { + insert(0, 0, 0, hunk, false); + } + + /** + * Returns the source of this hunk view. This could return another hunk view, + * not an actual source, however it does return it's underlying data source. If + * this hunk is a data source and not a view, it will return null. + * + * @return the source or null if this is already the source + */ + default Hunk getSource() { + return null; + } + + /** + * Insert a hunk into this one + * + * @param hunk the hunk to insert + * @param inverted invert the inserted hunk or not + */ + default void insert(Hunk hunk, boolean inverted) { + insert(0, 0, 0, hunk, inverted); + } + + /** + * Insert a hunk into this one with an offset and possibly inverting the y of + * the inserted hunk + * + * @param offX the offset from zero for x + * @param offY the offset from zero for y + * @param offZ the offset from zero for z + * @param hunk the hunk to insert + * @param invertY should the inserted hunk be inverted + */ + default void insert(int offX, int offY, int offZ, Hunk hunk, boolean invertY) { + HunkMutationSupport.insert(this, offX, offY, offZ, hunk); + } + + /** + * Insert a hunk into this one with an offset and possibly inverting the y of. Will never insert a node if its + * already used + * the inserted hunk + * + * @param offX the offset from zero for x + * @param offY the offset from zero for y + * @param offZ the offset from zero for z + * @param hunk the hunk to insert + * @param invertY should the inserted hunk be inverted + */ + default void insertSoftly(int offX, int offY, int offZ, Hunk hunk, boolean invertY, Predicate shouldOverwrite) { + HunkMutationSupport.insertSoftly(this, offX, offY, offZ, hunk, shouldOverwrite); + } + + /** + * Acts like fill, however if used by a mapped hunk, will simply clear it + * + * @param b the data to use for fill + */ + default void empty(T b) { + fill(b); + } + + /** + * Take a hunk and scale it up using interpolation + * + * @param scale the scale + * @param d the interpolation method + * @param interpolated the interpolated value converter + * @return the new hunk + */ + default Hunk interpolate3D(double scale, InterpolationMethod3D d, Interpolated interpolated) { + return HunkInterpolationSupport.interpolate3D( + this, + scale, + Hunk::newArrayHunk, + interpolated::toDouble, + interpolated::fromDouble, + (x, y, z, s, noise) -> Interpolation3D.getNoise3D(d, x, y, z, s, noise)); + } + + /** + * Take a hunk and scale it up using interpolation + * 2D, (using only x and z) assumes the height is 1 + * + * @param scale the scale + * @param d the interpolation method + * @param interpolated the interpolated value converter + * @return the new hunk + */ + default Hunk interpolate2D(double scale, InterpolationMethod d, Interpolated interpolated) { + return HunkInterpolationSupport.interpolate2D( + this, + scale, + Hunk::newArrayHunk, + interpolated::toDouble, + interpolated::fromDouble, + (x, z, s, noise) -> IrisInterpolation.getNoise(d, x, z, s, noise)); + } + + default Hunk rotate(double x, double y, double z, Supplier3R> builder) { + return HunkCoreSupport.rotate(this, x, y, z, builder::get); + } + + default boolean isEmpty() { + return false; + } + + default boolean contains(int x, int y, int z) { + return HunkCoreSupport.contains(this, x, y, z); + } + + default int volume() { + return HunkCoreSupport.volume(this); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/HunkFace.java b/core/src/main/java/art/arcane/iris/util/project/hunk/HunkFace.java new file mode 100644 index 000000000..ec8e58a21 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/HunkFace.java @@ -0,0 +1,42 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.hunk; + +public enum HunkFace { + TOP(art.arcane.volmlib.util.hunk.HunkFace.TOP), + BOTTOM(art.arcane.volmlib.util.hunk.HunkFace.BOTTOM), + EAST(art.arcane.volmlib.util.hunk.HunkFace.EAST), + WEST(art.arcane.volmlib.util.hunk.HunkFace.WEST), + NORTH(art.arcane.volmlib.util.hunk.HunkFace.NORTH), + SOUTH(art.arcane.volmlib.util.hunk.HunkFace.SOUTH); + + private final art.arcane.volmlib.util.hunk.HunkFace shared; + + HunkFace(art.arcane.volmlib.util.hunk.HunkFace shared) { + this.shared = shared; + } + + public art.arcane.volmlib.util.hunk.HunkFace shared() { + return shared; + } + + public static HunkFace fromShared(art.arcane.volmlib.util.hunk.HunkFace shared) { + return HunkFace.valueOf(shared.name()); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/SharedHunkAdapter.java b/core/src/main/java/art/arcane/iris/util/project/hunk/SharedHunkAdapter.java new file mode 100644 index 000000000..2239243c5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/SharedHunkAdapter.java @@ -0,0 +1,63 @@ +package art.arcane.iris.util.project.hunk; + +import art.arcane.volmlib.util.hunk.HunkLike; +final class SharedHunkAdapter implements Hunk { + private final HunkLike delegate; + + SharedHunkAdapter(HunkLike delegate) { + this.delegate = delegate; + } + + HunkLike delegate() { + return delegate; + } + + @Override + public int getWidth() { + return delegate.getWidth(); + } + + @Override + public int getDepth() { + return delegate.getDepth(); + } + + @Override + public int getHeight() { + return delegate.getHeight(); + } + + @Override + public void setRaw(int x, int y, int z, T t) { + delegate.setRaw(x, y, z, t); + } + + @Override + public T getRaw(int x, int y, int z) { + return delegate.getRaw(x, y, z); + } + + @Override + public boolean isMapped() { + return delegate instanceof art.arcane.volmlib.util.hunk.storage.MappedHunk + || delegate instanceof art.arcane.volmlib.util.hunk.storage.MappedSyncHunk; + } + + @Override + public int getEntryCount() { + if (delegate instanceof art.arcane.volmlib.util.hunk.storage.MappedHunk mapped) { + return mapped.getEntryCount(); + } + + if (delegate instanceof art.arcane.volmlib.util.hunk.storage.MappedSyncHunk mapped) { + return mapped.getEntryCount(); + } + + return Hunk.super.getEntryCount(); + } + + @Override + public boolean isAtomic() { + return delegate instanceof art.arcane.volmlib.util.hunk.storage.AtomicHunk; + } +} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/ArrayHunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/ArrayHunk.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/hunk/storage/ArrayHunk.java rename to core/src/main/java/art/arcane/iris/util/project/hunk/storage/ArrayHunk.java index fa213ef5f..57a418f6d 100644 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/ArrayHunk.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/ArrayHunk.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.hunk.storage; +package art.arcane.iris.util.project.hunk.storage; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.util.hunk.Hunk; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.util.project.hunk.Hunk; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicDoubleHunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicDoubleHunk.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicDoubleHunk.java rename to core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicDoubleHunk.java index 7df375a1d..10b395a59 100644 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicDoubleHunk.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicDoubleHunk.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.hunk.storage; +package art.arcane.iris.util.project.hunk.storage; import com.google.common.util.concurrent.AtomicDoubleArray; -import com.volmit.iris.util.hunk.Hunk; +import art.arcane.iris.util.project.hunk.Hunk; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicHunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicHunk.java similarity index 78% rename from core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicHunk.java rename to core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicHunk.java index b2297fa98..2df03cf79 100644 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicHunk.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicHunk.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.hunk.storage; +package art.arcane.iris.util.project.hunk.storage; -import com.volmit.iris.util.hunk.Hunk; +import art.arcane.iris.util.project.hunk.Hunk; import lombok.Data; import lombok.EqualsAndHashCode; @@ -29,10 +29,16 @@ import java.util.concurrent.atomic.AtomicReferenceArray; @EqualsAndHashCode(callSuper = false) public class AtomicHunk extends StorageHunk implements Hunk { private final AtomicReferenceArray data; + // index() runs per block access on the hottest paths; two final-field multiplies beat + // re-deriving the strides through the accessors every call. + private final int xStride; + private final int zStride; public AtomicHunk(int w, int h, int d) { super(w, h, d); data = new AtomicReferenceArray<>(w * h * d); + xStride = w; + zStride = w * h; } @Override @@ -51,6 +57,6 @@ public class AtomicHunk extends StorageHunk implements Hunk { } private int index(int x, int y, int z) { - return (z * getWidth() * getHeight()) + (y * getWidth()) + x; + return (z * zStride) + (y * xStride) + x; } } diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicIntegerHunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicIntegerHunk.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicIntegerHunk.java rename to core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicIntegerHunk.java index 833d3c50e..24672403b 100644 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicIntegerHunk.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicIntegerHunk.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.hunk.storage; +package art.arcane.iris.util.project.hunk.storage; -import com.volmit.iris.util.hunk.Hunk; +import art.arcane.iris.util.project.hunk.Hunk; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicLongHunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicLongHunk.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicLongHunk.java rename to core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicLongHunk.java index 98b452584..89c76128f 100644 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/AtomicLongHunk.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/AtomicLongHunk.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.hunk.storage; +package art.arcane.iris.util.project.hunk.storage; -import com.volmit.iris.util.hunk.Hunk; +import art.arcane.iris.util.project.hunk.Hunk; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/StorageHunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/StorageHunk.java similarity index 93% rename from core/src/main/java/com/volmit/iris/util/hunk/storage/StorageHunk.java rename to core/src/main/java/art/arcane/iris/util/project/hunk/storage/StorageHunk.java index 52764ca42..feac57805 100644 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/StorageHunk.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/StorageHunk.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.hunk.storage; +package art.arcane.iris.util.project.hunk.storage; -import com.volmit.iris.util.hunk.Hunk; +import art.arcane.iris.util.project.hunk.Hunk; import lombok.Data; @Data diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/SynchronizedArrayHunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/SynchronizedArrayHunk.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/hunk/storage/SynchronizedArrayHunk.java rename to core/src/main/java/art/arcane/iris/util/project/hunk/storage/SynchronizedArrayHunk.java index bf0ebe3d6..4395a1b5a 100644 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/SynchronizedArrayHunk.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/SynchronizedArrayHunk.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.hunk.storage; +package art.arcane.iris.util.project.hunk.storage; -import com.volmit.iris.util.hunk.Hunk; +import art.arcane.iris.util.project.hunk.Hunk; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkBiomeHunkView.java b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkBiomeHunkView.java new file mode 100644 index 000000000..5e0a6fe19 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkBiomeHunkView.java @@ -0,0 +1,44 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.hunk.view; + +import art.arcane.iris.engine.framework.BlockEditAccess; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.platform.bukkit.BukkitBiome; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.util.project.hunk.Hunk; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; + +@SuppressWarnings("ClassCanBeRecord") +public class ChunkBiomeHunkView extends art.arcane.volmlib.util.hunk.view.ChunkWorldHunkView implements Hunk { + public ChunkBiomeHunkView(Chunk chunk) { + super(chunk, + chunk.getWorld().getMaxHeight(), + (wx, y, wz, t) -> edits().setBiome(chunk.getWorld(), wx, y, wz, (Biome) t.nativeHandle()), + (wx, y, wz) -> BukkitBiome.of(edits().getBiome(chunk.getWorld(), wx, y, wz))); + } + + @SuppressWarnings("unchecked") + private static BlockEditAccess edits() { + return (BlockEditAccess) IrisServices.get(BlockEditAccess.class); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkHolder.java b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkHolder.java new file mode 100644 index 000000000..c2f4fb608 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkHolder.java @@ -0,0 +1,131 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.hunk.view; + +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.data.IrisCustomData; +import art.arcane.iris.util.project.hunk.storage.AtomicHunk; +import org.bukkit.block.data.BlockData; +import org.bukkit.generator.ChunkGenerator.ChunkData; + +@SuppressWarnings("ClassCanBeRecord") +public class ChunkDataHunkHolder extends AtomicHunk { + private static final class States { + private static final PlatformBlockState AIR = B.getState("AIR"); + } + + private final ChunkData chunk; + + public ChunkDataHunkHolder(ChunkData chunk) { + super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16); + this.chunk = chunk; + } + + @Override + public int getWidth() { + return 16; + } + + @Override + public int getDepth() { + return 16; + } + + @Override + public void setRaw(int x, int y, int z, PlatformBlockState t) { + // Block-hunk contract: null means "no write", never "erase" — ChunkDataHunkView and + // the modded ModdedBlockBuffer already discard nulls, and storing one here made the + // Bukkit output diverge from the modded loaders for the same engine emission. + if (t == null) { + return; + } + super.setRaw(x, y, z, t); + } + + @Override + public PlatformBlockState getRaw(int x, int y, int z) { + if (y < 0 || y >= getHeight()) { + return States.AIR; + } + + PlatformBlockState b = super.getRaw(x, y, z); + + return b != null ? b : States.AIR; + } + + public PlatformBlockState getStoredRaw(int x, int y, int z) { + return super.getRaw(x, y, z); + } + + public void apply() { + if (INMS.get().applyChunkDataBlocks(chunk, this)) { + return; + } + + int height = getHeight(); + for (int x = 0; x < getWidth(); x++) { + for (int z = 0; z < getDepth(); z++) { + BlockData activeBlock = null; + int runStart = -1; + + for (int y = 0; y < height; y++) { + PlatformBlockState state = super.getRaw(x, y, z); + BlockData block = state == null ? null : (BlockData) state.nativeHandle(); + // Custom wrappers are not real Bukkit data; write the vanilla base like the + // NMS fast path (NMSBinding.applyChunkDataBlocks) does. + if (block instanceof IrisCustomData custom) { + block = custom.getBase(); + } + if (block == null) { + flushRun(x, z, runStart, y, activeBlock); + activeBlock = null; + runStart = -1; + continue; + } + + if (activeBlock != null && activeBlock.equals(block)) { + continue; + } + + flushRun(x, z, runStart, y, activeBlock); + activeBlock = block; + runStart = y; + } + + flushRun(x, z, runStart, height, activeBlock); + } + } + } + + private void flushRun(int x, int z, int startY, int endY, BlockData block) { + if (block == null || startY < 0 || endY <= startY) { + return; + } + + int minY = chunk.getMinHeight(); + if (endY - startY == 1) { + chunk.setBlock(x, startY + minY, z, block); + return; + } + + chunk.setRegion(x, startY + minY, z, x + 1, endY + minY, z + 1, block); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkView.java b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkView.java new file mode 100644 index 000000000..6d51d3d76 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkView.java @@ -0,0 +1,89 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.hunk.view; + +import art.arcane.iris.platform.bukkit.BukkitBlockResolution; + +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.IrisCustomData; +import art.arcane.iris.util.project.hunk.Hunk; +import org.bukkit.block.data.BlockData; +import org.bukkit.generator.ChunkGenerator.ChunkData; + +@SuppressWarnings("ClassCanBeRecord") +public class ChunkDataHunkView implements Hunk { + private final art.arcane.volmlib.util.hunk.view.ChunkDataHunkView view; + + public ChunkDataHunkView(ChunkData chunk) { + this.view = new art.arcane.volmlib.util.hunk.view.ChunkDataHunkView(chunk, BukkitBlockResolution.getAir(), (data) -> data instanceof IrisCustomData d ? d.getBase() : data); + } + + @Override + public int getWidth() { + return view.getWidth(); + } + + @Override + public int getDepth() { + return view.getDepth(); + } + + @Override + public int getHeight() { + return view.getHeight(); + } + + @Override + public void set(int x1, int y1, int z1, int x2, int y2, int z2, PlatformBlockState t) { + if (t == null) { + return; + } + + view.setRegion(x1, y1, z1, x2, y2, z2, (BlockData) t.nativeHandle()); + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + return BukkitBlockState.of(view.get(x, y, z)); + } + + @Override + public void set(int x, int y, int z, PlatformBlockState t) { + if (t == null) { + return; + } + + view.set(x, y, z, (BlockData) t.nativeHandle()); + } + + @Override + public void setRaw(int x, int y, int z, PlatformBlockState t) { + if (t == null) { + return; + } + + view.setRaw(x, y, z, (BlockData) t.nativeHandle()); + } + + @Override + public PlatformBlockState getRaw(int x, int y, int z) { + return BukkitBlockState.of(view.getRaw(x, y, z)); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkHunkView.java b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkHunkView.java new file mode 100644 index 000000000..172e3c518 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkHunkView.java @@ -0,0 +1,44 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.hunk.view; + +import art.arcane.iris.engine.framework.BlockEditAccess; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; + +@SuppressWarnings("ClassCanBeRecord") +public class ChunkHunkView extends art.arcane.volmlib.util.hunk.view.ChunkWorldHunkView implements Hunk { + public ChunkHunkView(Chunk chunk) { + super(chunk, + chunk.getWorld().getMaxHeight(), + (wx, y, wz, t) -> edits().set(chunk.getWorld(), wx, y, wz, (BlockData) t.nativeHandle()), + (wx, y, wz) -> BukkitBlockState.of(edits().get(chunk.getWorld(), wx, y, wz))); + } + + @SuppressWarnings("unchecked") + private static BlockEditAccess edits() { + return (BlockEditAccess) IrisServices.get(BlockEditAccess.class); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/view/TerrainChunkBiomeHunkView.java b/core/src/main/java/art/arcane/iris/util/project/hunk/view/TerrainChunkBiomeHunkView.java new file mode 100644 index 000000000..010e192f7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/view/TerrainChunkBiomeHunkView.java @@ -0,0 +1,60 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.hunk.view; + +import art.arcane.iris.engine.data.chunk.LinkedTerrainChunk; +import art.arcane.iris.engine.data.chunk.TerrainChunk; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.project.hunk.storage.StorageHunk; +import art.arcane.volmlib.util.hunk.HunkMutationSupport; + +public class TerrainChunkBiomeHunkView extends StorageHunk implements Hunk { + private final TerrainChunk chunk; + + public TerrainChunkBiomeHunkView(TerrainChunk chunk) { + super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16); + this.chunk = chunk; + } + + /** + * A full height single column is the shape the biome actuator writes for every column of the chunk, so + * stride the backing biome array in one pass instead of dispatching per block. Any other region falls + * through to the generic element wise write. + */ + @Override + public void set(int x1, int y1, int z1, int x2, int y2, int z2, PlatformBiome biome) { + if (x1 == x2 && z1 == z2 && y1 == 0 && y2 == getHeight() - 1 && chunk instanceof LinkedTerrainChunk linked) { + linked.fillBiomeColumn(x1, z1, biome); + return; + } + + HunkMutationSupport.setRangeInclusive(this, x1, y1, z1, x2, y2, z2, biome); + } + + @Override + public void setRaw(int x, int y, int z, PlatformBiome biome) { + chunk.setBiome(x, y + chunk.getMinHeight(), z, biome); + } + + @Override + public PlatformBiome getRaw(int x, int y, int z) { + return chunk.getBiome(x, y + chunk.getMinHeight(), z); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/Interpolation3D.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/Interpolation3D.java new file mode 100644 index 000000000..583be20bf --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/Interpolation3D.java @@ -0,0 +1,322 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.interpolation; + +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.function.NoiseProvider3; +import art.arcane.volmlib.util.interpolation.Starcast; + +/** + * Volumetric field samplers. The scalar kernels these compose (trilerp, tricubic, trihermite) live + * in {@link IrisInterpolation}; this class only owns the 3D sampling grids and the + * {@link InterpolationMethod3D} dispatch. + */ +public final class Interpolation3D { + private Interpolation3D() { + } + + public static double getStarcast3D(int x, int y, int z, double rad, double checks, NoiseProvider3 n) { + return (Starcast.starcast(x, z, rad, checks, (xx, zz) -> n.noise(xx, y, zz)) + + Starcast.starcast(x, y, rad, checks, (xx, yy) -> n.noise(xx, yy, z)) + + Starcast.starcast(y, z, rad, checks, (yy, zz) -> n.noise(x, yy, zz))) / 3D; + } + + public static double getTrilinear(int x, int y, int z, double rad, NoiseProvider3 n) { + return getTrilinear(x, y, z, rad, rad, rad, n); + } + + public static double getTrilinear(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { + int fx = IrisInterpolation.getRadiusFactor(x, radx); + int fy = IrisInterpolation.getRadiusFactor(y, rady); + int fz = IrisInterpolation.getRadiusFactor(z, radz); + int x1 = (int) Math.round(fx * radx); + int y1 = (int) Math.round(fy * rady); + int z1 = (int) Math.round(fz * radz); + int x2 = (int) Math.round((fx + 1) * radx); + int y2 = (int) Math.round((fy + 1) * rady); + int z2 = (int) Math.round((fz + 1) * radz); + double px = IrisInterpolation.rangeScale(0, 1, x1, x2, x); + double py = IrisInterpolation.rangeScale(0, 1, y1, y2, y); + double pz = IrisInterpolation.rangeScale(0, 1, z1, z2, z); + //@builder + return IrisInterpolation.trilerp( + n.noise(x1, y1, z1), + n.noise(x2, y1, z1), + n.noise(x1, y2, z1), + n.noise(x2, y2, z1), + n.noise(x1, y1, z2), + n.noise(x2, y1, z2), + n.noise(x1, y2, z2), + n.noise(x2, y2, z2), + px, py, pz); + //@done + } + + public static double getTricubic(int x, int y, int z, double rad, NoiseProvider3 n) { + return getTricubic(x, y, z, rad, rad, rad, n); + } + + public static double getTricubic(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { + int fx = IrisInterpolation.getRadiusFactor(x, radx); + int fy = IrisInterpolation.getRadiusFactor(y, rady); + int fz = IrisInterpolation.getRadiusFactor(z, radz); + int x0 = (int) Math.round((fx - 1) * radx); + int y0 = (int) Math.round((fy - 1) * rady); + int z0 = (int) Math.round((fz - 1) * radz); + int x1 = (int) Math.round(fx * radx); + int y1 = (int) Math.round(fy * rady); + int z1 = (int) Math.round(fz * radz); + int x2 = (int) Math.round((fx + 1) * radx); + int y2 = (int) Math.round((fy + 1) * rady); + int z2 = (int) Math.round((fz + 1) * radz); + int x3 = (int) Math.round((fx + 2) * radx); + int y3 = (int) Math.round((fy + 2) * rady); + int z3 = (int) Math.round((fz + 2) * radz); + double px = IrisInterpolation.rangeScale(0, 1, x1, x2, x); + double py = IrisInterpolation.rangeScale(0, 1, y1, y2, y); + double pz = IrisInterpolation.rangeScale(0, 1, z1, z2, z); + //@builder + //!!!!!!!!!!!!!!!!!! 2 1 3 + + return IrisInterpolation.tricubic( + n.noise(x0, y0, z0), + n.noise(x0, y1, z0), + n.noise(x0, y2, z0), + n.noise(x0, y3, z0), + n.noise(x1, y0, z0), + n.noise(x1, y1, z0), + n.noise(x1, y2, z0), + n.noise(x1, y3, z0), + n.noise(x2, y0, z0), + n.noise(x2, y1, z0), + n.noise(x2, y2, z0), + n.noise(x2, y3, z0), + n.noise(x3, y0, z0), + n.noise(x3, y1, z0), + n.noise(x3, y2, z0), + n.noise(x3, y3, z0), + n.noise(x0, y0, z1), + n.noise(x0, y1, z1), + n.noise(x0, y2, z1), + n.noise(x0, y3, z1), + n.noise(x1, y0, z1), + n.noise(x1, y1, z1), + n.noise(x1, y2, z1), + n.noise(x1, y3, z1), + n.noise(x2, y0, z1), + n.noise(x2, y1, z1), + n.noise(x2, y2, z1), + n.noise(x2, y3, z1), + n.noise(x3, y0, z1), + n.noise(x3, y1, z1), + n.noise(x3, y2, z1), + n.noise(x3, y3, z1), + n.noise(x0, y0, z2), + n.noise(x0, y1, z2), + n.noise(x0, y2, z2), + n.noise(x0, y3, z2), + n.noise(x1, y0, z2), + n.noise(x1, y1, z2), + n.noise(x1, y2, z2), + n.noise(x1, y3, z2), + n.noise(x2, y0, z2), + n.noise(x2, y1, z2), + n.noise(x2, y2, z2), + n.noise(x2, y3, z2), + n.noise(x3, y0, z2), + n.noise(x3, y1, z2), + n.noise(x3, y2, z2), + n.noise(x3, y3, z2), + n.noise(x0, y0, z3), + n.noise(x0, y1, z3), + n.noise(x0, y2, z3), + n.noise(x0, y3, z3), + n.noise(x1, y0, z3), + n.noise(x1, y1, z3), + n.noise(x1, y2, z3), + n.noise(x1, y3, z3), + n.noise(x2, y0, z3), + n.noise(x2, y1, z3), + n.noise(x2, y2, z3), + n.noise(x2, y3, z3), + n.noise(x3, y0, z3), + n.noise(x3, y1, z3), + n.noise(x3, y2, z3), + n.noise(x3, y3, z3), + px, py, pz); + //@done + } + + public static double getTrihermite(int x, int y, int z, double rad, NoiseProvider3 n, double tension, double bias) { + return getTrihermite(x, y, z, rad, rad, rad, n, tension, bias); + } + + public static double getTrihermite(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { + return getTrihermite(x, y, z, radx, rady, radz, n, 0D, 0D); + } + + public static double getTrihermite(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n, double tension, double bias) { + int fx = IrisInterpolation.getRadiusFactor(x, radx); + int fy = IrisInterpolation.getRadiusFactor(y, rady); + int fz = IrisInterpolation.getRadiusFactor(z, radz); + int x0 = (int) Math.round((fx - 1) * radx); + int y0 = (int) Math.round((fy - 1) * rady); + int z0 = (int) Math.round((fz - 1) * radz); + int x1 = (int) Math.round(fx * radx); + int y1 = (int) Math.round(fy * rady); + int z1 = (int) Math.round(fz * radz); + int x2 = (int) Math.round((fx + 1) * radx); + int y2 = (int) Math.round((fy + 1) * rady); + int z2 = (int) Math.round((fz + 1) * radz); + int x3 = (int) Math.round((fx + 2) * radx); + int y3 = (int) Math.round((fy + 2) * rady); + int z3 = (int) Math.round((fz + 2) * radz); + double px = IrisInterpolation.rangeScale(0, 1, x1, x2, x); + double py = IrisInterpolation.rangeScale(0, 1, y1, y2, y); + double pz = IrisInterpolation.rangeScale(0, 1, z1, z2, z); + //@builder + //!!!!!!!!!!!!!!!!!! 2 1 3 + + return IrisInterpolation.trihermite( + n.noise(x0, y0, z0), + n.noise(x0, y1, z0), + n.noise(x0, y2, z0), + n.noise(x0, y3, z0), + n.noise(x1, y0, z0), + n.noise(x1, y1, z0), + n.noise(x1, y2, z0), + n.noise(x1, y3, z0), + n.noise(x2, y0, z0), + n.noise(x2, y1, z0), + n.noise(x2, y2, z0), + n.noise(x2, y3, z0), + n.noise(x3, y0, z0), + n.noise(x3, y1, z0), + n.noise(x3, y2, z0), + n.noise(x3, y3, z0), + n.noise(x0, y0, z1), + n.noise(x0, y1, z1), + n.noise(x0, y2, z1), + n.noise(x0, y3, z1), + n.noise(x1, y0, z1), + n.noise(x1, y1, z1), + n.noise(x1, y2, z1), + n.noise(x1, y3, z1), + n.noise(x2, y0, z1), + n.noise(x2, y1, z1), + n.noise(x2, y2, z1), + n.noise(x2, y3, z1), + n.noise(x3, y0, z1), + n.noise(x3, y1, z1), + n.noise(x3, y2, z1), + n.noise(x3, y3, z1), + n.noise(x0, y0, z2), + n.noise(x0, y1, z2), + n.noise(x0, y2, z2), + n.noise(x0, y3, z2), + n.noise(x1, y0, z2), + n.noise(x1, y1, z2), + n.noise(x1, y2, z2), + n.noise(x1, y3, z2), + n.noise(x2, y0, z2), + n.noise(x2, y1, z2), + n.noise(x2, y2, z2), + n.noise(x2, y3, z2), + n.noise(x3, y0, z2), + n.noise(x3, y1, z2), + n.noise(x3, y2, z2), + n.noise(x3, y3, z2), + n.noise(x0, y0, z3), + n.noise(x0, y1, z3), + n.noise(x0, y2, z3), + n.noise(x0, y3, z3), + n.noise(x1, y0, z3), + n.noise(x1, y1, z3), + n.noise(x1, y2, z3), + n.noise(x1, y3, z3), + n.noise(x2, y0, z3), + n.noise(x2, y1, z3), + n.noise(x2, y2, z3), + n.noise(x2, y3, z3), + n.noise(x3, y0, z3), + n.noise(x3, y1, z3), + n.noise(x3, y2, z3), + n.noise(x3, y3, z3), + px, py, pz, tension, bias); + //@done + } + + public static double getNoise3D(InterpolationMethod3D method, int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { + return switch (method) { + case TRILINEAR -> getTrilinear(x, y, z, radx, rady, radz, n); + case TRICUBIC -> getTricubic(x, y, z, radx, rady, radz, n); + case TRIHERMITE -> getTrihermite(x, y, z, radx, rady, radz, n); + case TRISTARCAST_3 -> getStarcast3D(x, y, z, radx, 3D, n); + case TRISTARCAST_6 -> getStarcast3D(x, y, z, radx, 6D, n); + case TRISTARCAST_9 -> getStarcast3D(x, y, z, radx, 9D, n); + case TRISTARCAST_12 -> getStarcast3D(x, y, z, radx, 12D, n); + case TRILINEAR_TRISTARCAST_3 -> + getStarcast3D(x, y, z, radx, 3D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); + case TRILINEAR_TRISTARCAST_6 -> + getStarcast3D(x, y, z, radx, 6D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); + case TRILINEAR_TRISTARCAST_9 -> + getStarcast3D(x, y, z, radx, 9D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); + case TRILINEAR_TRISTARCAST_12 -> + getStarcast3D(x, y, z, radx, 12D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); + case NONE -> n.noise(x, y, z); + }; + } + + public static Hunk getNoise3D(InterpolationMethod3D method, int xo, int yo, int zo, int w, int h, int d, double rad, NoiseProvider3 n) { + return getNoise3D(method, xo, yo, zo, w, h, d, rad, rad, rad, n); + } + + /** + * Get the interpolated 3D noise within a given cuboid size with offsets + * + * @param method the interpolation method to use + * @param xo the x offset for noise + * @param yo the y offset for noise + * @param zo the z offset for noise + * @param w the width of the result + * @param h the height of the result + * @param d the depth of the result + * @param radX the interpolation radius for the x axis + * @param radY the interpolation radius for the y axis + * @param radZ the interpolation radius for the z axis + * @param n the noise provider + * @return the resulting hunk of noise + */ + public static Hunk getNoise3D(InterpolationMethod3D method, int xo, int yo, int zo, int w, int h, int d, double radX, double radY, double radZ, NoiseProvider3 n) { + Hunk hunk = Hunk.newAtomicDoubleHunk(w, h, d); + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + for (int k = 0; k < d; k++) { + hunk.set(i, j, k, getNoise3D(method, i + xo, j + yo, k + zo, radX, radY, radZ, n)); + } + } + } + + return hunk; + } + + public static double getNoise3D(InterpolationMethod3D method, int x, int y, int z, double rad, NoiseProvider3 n) { + return getNoise3D(method, x, y, z, rad, rad, rad, n); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/interpolation/InterpolationMethod.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod.java similarity index 98% rename from core/src/main/java/com/volmit/iris/util/interpolation/InterpolationMethod.java rename to core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod.java index 715a094f0..167449f64 100644 --- a/core/src/main/java/com/volmit/iris/util/interpolation/InterpolationMethod.java +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.interpolation; +package art.arcane.iris.util.project.interpolation; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("An interpolation method (or function) is simply a method of smoothing a position based on surrounding points on a grid. Bicubic for example is smoother, but has 4 times the checks than Bilinear for example. Try using BILINEAR_STARCAST_9 for beautiful results.") public enum InterpolationMethod { diff --git a/core/src/main/java/com/volmit/iris/util/interpolation/InterpolationMethod3D.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod3D.java similarity index 92% rename from core/src/main/java/com/volmit/iris/util/interpolation/InterpolationMethod3D.java rename to core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod3D.java index 08c36729f..57251bca8 100644 --- a/core/src/main/java/com/volmit/iris/util/interpolation/InterpolationMethod3D.java +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod3D.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.interpolation; +package art.arcane.iris.util.project.interpolation; -import com.volmit.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Desc; @Desc("An interpolation method (or function) is simply a method of smoothing a position based on surrounding points on a grid. Bicubic for example is smoother, but has 4 times the checks than Bilinear for example. Try using BILINEAR_STARCAST_9 for beautiful results.") public enum InterpolationMethod3D { diff --git a/core/src/main/java/com/volmit/iris/util/interpolation/InterpolationType.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationType.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/interpolation/InterpolationType.java rename to core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationType.java index c7aa23e09..03a3b8279 100644 --- a/core/src/main/java/com/volmit/iris/util/interpolation/InterpolationType.java +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationType.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.interpolation; +package art.arcane.iris.util.project.interpolation; public enum InterpolationType { LINEAR, diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java new file mode 100644 index 000000000..2bfaea3aa --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java @@ -0,0 +1,699 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.interpolation; + +import com.google.common.util.concurrent.AtomicDouble; +import art.arcane.volmlib.util.interpolation.Starcast; +import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.volmlib.util.function.NoiseProvider; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; + +public class IrisInterpolation { + public static CNG cng = NoiseStyle.SIMPLEX.create(new RNG()); + private static final ThreadLocal NOISE_SAMPLE_CACHE_2D = ThreadLocal.withInitial(() -> new NoiseSampleCache2D(64)); + private static final ThreadLocal NOISE_BOUNDS_SAMPLE_CACHE_2D = ThreadLocal.withInitial(() -> new NoiseBoundsSampleCache2D(64)); + + public static double bezier(double t) { + return t * t * (3.0d - 2.0d * t); + } + + public static double parametric(double t, double alpha) { + double sqt = Math.pow(t, alpha); + return sqt / (alpha * (sqt - Math.pow(t, alpha - 1)) + 1.0d); + } + + public static float lerpf(float a, float b, float f) { + return a + (f * (b - a)); + } + + public static double lerpBezier(double a, double b, double f) { + return a + (bezier(f) * (b - a)); + } + + public static double sinCenter(double f) { + return Math.sin(f * Math.PI); + } + + public static double lerpCenterSinBezier(double a, double b, double f) { + return lerpBezier(a, b, sinCenter(f)); + } + + public static double lerpCenterSin(double a, double b, double f) { + return lerpBezier(a, b, sinCenter(f)); + } + + public static double lerpParametric(double a, double b, double f, double v) { + return a + (parametric(f, v) * (b - a)); + } + + public static double blerp(double a, double b, double c, double d, double tx, double ty, InterpolationType type) { + if (type.equals(InterpolationType.LINEAR)) { + return blerp(a, b, c, d, tx, ty); + } + + if (type.equals(InterpolationType.BEZIER)) { + return blerpBezier(a, b, c, d, tx, ty); + } + + if (type.equals(InterpolationType.PARAMETRIC_2)) { + return blerpParametric(a, b, c, d, tx, ty, 2); + } + + if (type.equals(InterpolationType.PARAMETRIC_4)) { + return blerpParametric(a, b, c, d, tx, ty, 4); + } + + return 0; + } + + public static double blerpBezier(double a, double b, double c, double d, double tx, double ty) { + return lerpBezier(lerpBezier(a, b, tx), lerpBezier(c, d, tx), ty); + } + + public static double blerpSinCenter(double a, double b, double c, double d, double tx, double ty) { + return lerpCenterSin(lerpCenterSin(a, b, tx), lerpCenterSin(c, d, tx), ty); + } + + public static double blerpParametric(double a, double b, double c, double d, double tx, double ty, double v) { + return lerpParametric(lerpParametric(a, b, tx, v), lerpParametric(c, d, tx, v), ty, v); + } + + public static double hermiteBezier(double p0, double p1, double p2, double p3, double mu, double tension, double bias) { + return bezier(hermite(p0, p1, p2, p3, mu, tension, bias)); + } + + public static double hermiteParametric(double p0, double p1, double p2, double p3, double mu, double tension, double bias, double a) { + return parametric(hermite(p0, p1, p2, p3, mu, tension, bias), a); + } + + public static double bihermiteBezier(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy, double tension, double bias) { + //@builder + return hermiteBezier( + hermiteBezier(p00, p01, p02, p03, muy, tension, bias), + hermiteBezier(p10, p11, p12, p13, muy, tension, bias), + hermiteBezier(p20, p21, p22, p23, muy, tension, bias), + hermiteBezier(p30, p31, p32, p33, muy, tension, bias), + mux, tension, bias); + //@done + } + + public static double bihermiteParametric(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy, double tension, double bias, double a) { + //@builder + return hermiteParametric( + hermiteParametric(p00, p01, p02, p03, muy, tension, bias, a), + hermiteParametric(p10, p11, p12, p13, muy, tension, bias, a), + hermiteParametric(p20, p21, p22, p23, muy, tension, bias, a), + hermiteParametric(p30, p31, p32, p33, muy, tension, bias, a), + mux, tension, bias, a); + //@done + } + + public static double cubicBezier(double p0, double p1, double p2, double p3, double mu) { + return bezier(cubic(p0, p1, p2, p3, mu)); + } + + public static double cubicParametric(double p0, double p1, double p2, double p3, double mu, double a) { + return parametric(cubic(p0, p1, p2, p3, mu), a); + } + + public static double hermite(double p0, double p1, double p2, double p3, double mu, double tension, double bias) { + double m0, m1, mu2, mu3; + double a0, a1, a2, a3; + + mu2 = mu * mu; + mu3 = mu2 * mu; + m0 = (p1 - p0) * (1 + bias) * (1 - tension) / 2; + m0 += (p2 - p1) * (1 - bias) * (1 - tension) / 2; + m1 = (p2 - p1) * (1 + bias) * (1 - tension) / 2; + m1 += (p3 - p2) * (1 - bias) * (1 - tension) / 2; + a0 = 2 * mu3 - 3 * mu2 + 1; + a1 = mu3 - 2 * mu2 + mu; + a2 = mu3 - mu2; + a3 = -2 * mu3 + 3 * mu2; + + return (a0 * p1 + a1 * m0 + a2 * m1 + a3 * p2); + } + + public static double cubic(double p0, double p1, double p2, double p3, double mu) { + double a0, a1, a2, a3, mu2; + + mu2 = mu * mu; + a0 = p3 - p2 - p0 + p1; + a1 = p0 - p1 - a0; + a2 = p2 - p0; + a3 = p1; + + return a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3; + } + + public static double bicubic(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy) { + //@builder + return cubic( + cubic(p00, p01, p02, p03, muy), + cubic(p10, p11, p12, p13, muy), + cubic(p20, p21, p22, p23, muy), + cubic(p30, p31, p32, p33, muy), + mux); + //@done + } + + public static double bihermite(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy, double tension, double bias) { + //@builder + return hermite( + hermite(p00, p01, p02, p03, muy, tension, bias), + hermite(p10, p11, p12, p13, muy, tension, bias), + hermite(p20, p21, p22, p23, muy, tension, bias), + hermite(p30, p31, p32, p33, muy, tension, bias), + mux, tension, bias); + //@done + } + + public static double trihermite(double p000, double p001, double p002, double p003, double p010, double p011, double p012, double p013, double p020, double p021, double p022, double p023, double p030, double p031, double p032, double p033, double p100, double p101, double p102, double p103, double p110, double p111, double p112, double p113, double p120, double p121, double p122, double p123, double p130, double p131, double p132, double p133, double p200, double p201, double p202, double p203, double p210, double p211, double p212, double p213, double p220, double p221, double p222, double p223, double p230, double p231, double p232, double p233, double p300, double p301, double p302, double p303, double p310, double p311, double p312, double p313, double p320, double p321, double p322, double p323, double p330, double p331, double p332, double p333, double mux, double muy, double muz, double tension, double bias) { + //@builder + return hermite( + bihermite(p000, p001, p002, p003, + p010, p011, p012, p013, + p020, p021, p022, p023, + p030, p031, p032, p033, + mux, muy, tension, bias), + bihermite(p100, p101, p102, p103, + p110, p111, p112, p113, + p120, p121, p122, p123, + p130, p131, p132, p133, + mux, muy, tension, bias), + bihermite(p200, p201, p202, p203, + p210, p211, p212, p213, + p220, p221, p222, p223, + p230, p231, p232, p233, + mux, muy, tension, bias), + bihermite(p300, p301, p302, p303, + p310, p311, p312, p313, + p320, p321, p322, p323, + p330, p331, p332, p333, + mux, muy, tension, bias), + muz, tension, bias); + //@done + } + + public static double tricubic(double p000, + double p001, + double p002, + double p003, + double p010, + double p011, + double p012, double p013, double p020, double p021, double p022, double p023, double p030, double p031, double p032, double p033, double p100, double p101, double p102, double p103, double p110, double p111, double p112, double p113, double p120, double p121, double p122, double p123, double p130, double p131, double p132, double p133, double p200, double p201, double p202, double p203, double p210, double p211, double p212, double p213, double p220, double p221, double p222, double p223, double p230, double p231, double p232, double p233, double p300, double p301, double p302, double p303, double p310, double p311, double p312, double p313, double p320, double p321, double p322, double p323, double p330, double p331, double p332, double p333, double mux, double muy, double muz) { + //@builder + return cubic( + bicubic(p000, p001, p002, p003, + p010, p011, p012, p013, + p020, p021, p022, p023, + p030, p031, p032, p033, + mux, muy), + bicubic(p100, p101, p102, p103, + p110, p111, p112, p113, + p120, p121, p122, p123, + p130, p131, p132, p133, + mux, muy), + bicubic(p200, p201, p202, p203, + p210, p211, p212, p213, + p220, p221, p222, p223, + p230, p231, p232, p233, + mux, muy), + bicubic(p300, p301, p302, p303, + p310, p311, p312, p313, + p320, p321, p322, p323, + p330, p331, p332, p333, + mux, muy), + muz); + //@done + } + + public static double lerp(double a, double b, double f) { + return a + (f * (b - a)); + } + + public static double blerp(double a, double b, double c, double d, double tx, double ty) { + return lerp(lerp(a, b, tx), lerp(c, d, tx), ty); + } + + public static double trilerp(double v1, double v2, double v3, double v4, double v5, double v6, double v7, double v8, double x, double y, double z) { + return lerp(blerp(v1, v2, v3, v4, x, y), blerp(v5, v6, v7, v8, x, y), z); + } + + public static double bicubicBezier(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy) { + //@builder + return cubicBezier( + cubicBezier(p00, p01, p02, p03, muy), + cubicBezier(p10, p11, p12, p13, muy), + cubicBezier(p20, p21, p22, p23, muy), + cubicBezier(p30, p31, p32, p33, muy), + mux); + //@done + } + + public static double bicubicParametric(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy, double a) { + //@builder + return cubicParametric( + cubicParametric(p00, p01, p02, p03, muy, a), + cubicParametric(p10, p11, p12, p13, muy, a), + cubicParametric(p20, p21, p22, p23, muy, a), + cubicParametric(p30, p31, p32, p33, muy, a), + mux, a); + //@done + } + + public static int getRadiusFactor(int coord, double radius) { + int radiusInt = (int) radius; + if (radius == radiusInt && radiusInt > 0 && (radiusInt & (radiusInt - 1)) == 0) { + return coord >> Integer.numberOfTrailingZeros(radiusInt); + } + return (int) Math.floor(coord / radius); + } + + public static double getBilinearNoise(int x, int z, double rad, NoiseProvider n) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return blerp( + n.noise(x1, z1), + n.noise(x2, z1), + n.noise(x1, z2), + n.noise(x2, z2), + px, pz); + //@done + } + + public static double getBilinearBezierNoise(int x, int z, double rad, NoiseProvider n) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return blerpBezier( + n.noise(x1, z1), + n.noise(x2, z1), + n.noise(x1, z2), + n.noise(x2, z2), + px, pz); + //@done + } + + public static double getBilinearParametricNoise(int x, int z, double rad, NoiseProvider n, double a) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return blerpParametric( + n.noise(x1, z1), + n.noise(x2, z1), + n.noise(x1, z2), + n.noise(x2, z2), + px, pz, a); + //@done + } + + public static double getBilinearCenterSineNoise(int x, int z, double rad, NoiseProvider n) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return blerpSinCenter( + n.noise(x1, z1), + n.noise(x2, z1), + n.noise(x1, z2), + n.noise(x2, z2), + px, pz); + //@done + } + + public static double getBicubicNoise(int x, int z, double rad, NoiseProvider n) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x0 = (int) Math.round((fx - 1) * rad); + int z0 = (int) Math.round((fz - 1) * rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + int x3 = (int) Math.round((fx + 2) * rad); + int z3 = (int) Math.round((fz + 2) * rad); + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return bicubic( + n.noise(x0, z0), + n.noise(x0, z1), + n.noise(x0, z2), + n.noise(x0, z3), + n.noise(x1, z0), + n.noise(x1, z1), + n.noise(x1, z2), + n.noise(x1, z3), + n.noise(x2, z0), + n.noise(x2, z1), + n.noise(x2, z2), + n.noise(x2, z3), + n.noise(x3, z0), + n.noise(x3, z1), + n.noise(x3, z2), + n.noise(x3, z3), + px, pz); + //@done + } + + public static double getBicubicBezierNoise(int x, int z, double rad, NoiseProvider n) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x0 = (int) Math.round((fx - 1) * rad); + int z0 = (int) Math.round((fz - 1) * rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + int x3 = (int) Math.round((fx + 2) * rad); + int z3 = (int) Math.round((fz + 2) * rad); + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return bicubicBezier( + n.noise(x0, z0), + n.noise(x0, z1), + n.noise(x0, z2), + n.noise(x0, z3), + n.noise(x1, z0), + n.noise(x1, z1), + n.noise(x1, z2), + n.noise(x1, z3), + n.noise(x2, z0), + n.noise(x2, z1), + n.noise(x2, z2), + n.noise(x2, z3), + n.noise(x3, z0), + n.noise(x3, z1), + n.noise(x3, z2), + n.noise(x3, z3), + px, pz); + //@done + } + + public static double getBicubicParametricNoise(int x, int z, double rad, NoiseProvider n, double a) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x0 = (int) Math.round((fx - 1) * rad); + int z0 = (int) Math.round((fz - 1) * rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + int x3 = (int) Math.round((fx + 2) * rad); + int z3 = (int) Math.round((fz + 2) * rad); + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return bicubicParametric( + n.noise(x0, z0), + n.noise(x0, z1), + n.noise(x0, z2), + n.noise(x0, z3), + n.noise(x1, z0), + n.noise(x1, z1), + n.noise(x1, z2), + n.noise(x1, z3), + n.noise(x2, z0), + n.noise(x2, z1), + n.noise(x2, z2), + n.noise(x2, z3), + n.noise(x3, z0), + n.noise(x3, z1), + n.noise(x3, z2), + n.noise(x3, z3), + px, pz, a); + //@done + } + + public static double getHermiteNoise(int x, int z, double rad, NoiseProvider n) { + return getHermiteNoise(x, z, rad, n, 0.5, 0); + } + + public static double getHermiteBezierNoise(int x, int z, double rad, NoiseProvider n) { + return getHermiteBezierNoise(x, z, rad, n, 0.5, 0); + } + + public static double getHermiteParametricNoise(int x, int z, double rad, NoiseProvider n, double a) { + return getHermiteParametricNoise(x, z, rad, n, 0.5, 0, a); + } + + public static double getHermiteNoise(int x, int z, double rad, NoiseProvider n, double t, double b) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x0 = (int) Math.round((fx - 1) * rad); + int z0 = (int) Math.round((fz - 1) * rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + int x3 = (int) Math.round((fx + 2) * rad); + int z3 = (int) Math.round((fz + 2) * rad); + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return bihermite( + n.noise(x0, z0), + n.noise(x0, z1), + n.noise(x0, z2), + n.noise(x0, z3), + n.noise(x1, z0), + n.noise(x1, z1), + n.noise(x1, z2), + n.noise(x1, z3), + n.noise(x2, z0), + n.noise(x2, z1), + n.noise(x2, z2), + n.noise(x2, z3), + n.noise(x3, z0), + n.noise(x3, z1), + n.noise(x3, z2), + n.noise(x3, z3), + px, pz, t, b); + //@done + } + + public static double getHermiteBezierNoise(int x, int z, double rad, NoiseProvider n, double t, double b) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x0 = (int) Math.round((fx - 1) * rad); + int z0 = (int) Math.round((fz - 1) * rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + int x3 = (int) Math.round((fx + 2) * rad); + int z3 = (int) Math.round((fz + 2) * rad); + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return bihermiteBezier( + n.noise(x0, z0), + n.noise(x0, z1), + n.noise(x0, z2), + n.noise(x0, z3), + n.noise(x1, z0), + n.noise(x1, z1), + n.noise(x1, z2), + n.noise(x1, z3), + n.noise(x2, z0), + n.noise(x2, z1), + n.noise(x2, z2), + n.noise(x2, z3), + n.noise(x3, z0), + n.noise(x3, z1), + n.noise(x3, z2), + n.noise(x3, z3), + px, pz, t, b); + //@done + } + + public static double getHermiteParametricNoise(int x, int z, double rad, NoiseProvider n, double t, double b, double a) { + int fx = getRadiusFactor(x, rad); + int fz = getRadiusFactor(z, rad); + int x0 = (int) Math.round((fx - 1) * rad); + int z0 = (int) Math.round((fz - 1) * rad); + int x1 = (int) Math.round(fx * rad); + int z1 = (int) Math.round(fz * rad); + int x2 = (int) Math.round((fx + 1) * rad); + int z2 = (int) Math.round((fz + 1) * rad); + int x3 = (int) Math.round((fx + 2) * rad); + int z3 = (int) Math.round((fz + 2) * rad); + double px = rangeScale(0, 1, x1, x2, x); + double pz = rangeScale(0, 1, z1, z2, z); + //@builder + return bihermiteParametric( + n.noise(x0, z0), + n.noise(x0, z1), + n.noise(x0, z2), + n.noise(x0, z3), + n.noise(x1, z0), + n.noise(x1, z1), + n.noise(x1, z2), + n.noise(x1, z3), + n.noise(x2, z0), + n.noise(x2, z1), + n.noise(x2, z2), + n.noise(x2, z3), + n.noise(x3, z0), + n.noise(x3, z1), + n.noise(x3, z2), + n.noise(x3, z3), + px, pz, t, b, a); + //@done + } + + public static double getRealRadius(InterpolationMethod method, double h) { + AtomicDouble rad = new AtomicDouble(h); + AtomicDouble accessX = new AtomicDouble(); + AtomicDouble accessZ = new AtomicDouble(); + NoiseProvider np = (x1, z1) -> { + double d = Math.max(Math.abs(x1), Math.abs(z1)); + if (d > rad.get()) { + rad.set(d); + } + return 0; + }; + getNoise(method, 0, 0, h, np); + return rad.get(); + } + + public static double getNoise(InterpolationMethod method, int x, int z, double h, NoiseProvider noise) { + if (usesSampleCache(method)) { + NoiseSampleCache2D cache = NOISE_SAMPLE_CACHE_2D.get(); + if (!cache.isInUse()) { + cache.beginUse(); + try { + return dispatch(method, x, z, h, (x1, z1) -> cache.getOrSample(x1 - x, z1 - z, x1, z1, noise)); + } finally { + cache.endUse(); + } + } + // Nested interpolation on this thread (image maps, interpolated noise styles): + // run unmemoized. Sharing the outer table would clear it mid-flight and serve + // this provider's samples to the outer pass. Providers are pure, so the + // unmemoized result is bit-identical. + } + return dispatch(method, x, z, h, noise); + } + + private static double dispatch(InterpolationMethod method, int x, int z, double h, NoiseProvider n) { + return switch (method) { + case BILINEAR -> getBilinearNoise(x, z, h, n); + case STARCAST_3 -> Starcast.starcast(x, z, h, 3D, n); + case STARCAST_6 -> Starcast.starcast(x, z, h, 6D, n); + case STARCAST_9 -> Starcast.starcast(x, z, h, 9D, n); + case STARCAST_12 -> Starcast.starcast(x, z, h, 12D, n); + case BILINEAR_STARCAST_3 -> + Starcast.starcast(x, z, h, 3D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); + case BILINEAR_STARCAST_6 -> + Starcast.starcast(x, z, h, 6D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); + case BILINEAR_STARCAST_9 -> + Starcast.starcast(x, z, h, 9D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); + case BILINEAR_STARCAST_12 -> + Starcast.starcast(x, z, h, 12D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); + case HERMITE_STARCAST_3 -> + Starcast.starcast(x, z, h, 3D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); + case HERMITE_STARCAST_6 -> + Starcast.starcast(x, z, h, 6D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); + case HERMITE_STARCAST_9 -> + Starcast.starcast(x, z, h, 9D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); + case HERMITE_STARCAST_12 -> + Starcast.starcast(x, z, h, 12D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); + case BILINEAR_BEZIER -> getBilinearBezierNoise(x, z, h, n); + case BILINEAR_PARAMETRIC_2 -> getBilinearParametricNoise(x, z, h, n, 2); + case BILINEAR_PARAMETRIC_4 -> getBilinearParametricNoise(x, z, h, n, 4); + case BILINEAR_PARAMETRIC_1_5 -> getBilinearParametricNoise(x, z, h, n, 1.5); + case BICUBIC -> getBilinearNoise(x, z, h, n); + case HERMITE -> getHermiteNoise(x, z, h, n); + case HERMITE_TENSE -> getHermiteNoise(x, z, h, n, 0.8D, 0D); + case CATMULL_ROM_SPLINE -> getHermiteNoise(x, z, h, n, 1D, 0D); + case HERMITE_LOOSE -> getHermiteNoise(x, z, h, n, 0D, 0D); + case HERMITE_LOOSE_HALF_NEGATIVE_BIAS -> getHermiteNoise(x, z, h, n, 0D, -0.5D); + case HERMITE_LOOSE_HALF_POSITIVE_BIAS -> getHermiteNoise(x, z, h, n, 0D, 0.5D); + case HERMITE_LOOSE_FULL_NEGATIVE_BIAS -> getHermiteNoise(x, z, h, n, 0D, -1D); + case HERMITE_LOOSE_FULL_POSITIVE_BIAS -> getHermiteNoise(x, z, h, n, 0D, 1D); + case NONE -> n.noise(x, z); + }; + } + + public static NoiseBounds getNoiseBounds(InterpolationMethod method, int x, int z, double h, NoiseBoundsProvider noise) { + NoiseBoundsSampleCache2D cache = NOISE_BOUNDS_SAMPLE_CACHE_2D.get(); + if (cache.isInUse()) { + // Nested bounds interpolation: a fresh table on the rare nested path, never the + // outer pass's table — its entries belong to a different bound provider. + cache = new NoiseBoundsSampleCache2D(64); + } + cache.beginUse(); + NoiseBoundsProvider previous = cache.bindProvider(noise); + try { + double min = getNoise(method, x, z, h, cache.minView()); + double max = getNoise(method, x, z, h, cache.maxView()); + return new NoiseBounds(min, max); + } finally { + cache.bindProvider(previous); + cache.endUse(); + } + } + + private static boolean usesSampleCache(InterpolationMethod method) { + return switch (method) { + case BILINEAR_STARCAST_3, + BILINEAR_STARCAST_6, + BILINEAR_STARCAST_9, + BILINEAR_STARCAST_12, + HERMITE_STARCAST_3, + HERMITE_STARCAST_6, + HERMITE_STARCAST_9, + HERMITE_STARCAST_12 -> true; + default -> false; + }; + } + + public static double rangeScale(double amin, double amax, double bmin, double bmax, double b) { + return amin + ((amax - amin) * ((b - bmin) / (bmax - bmin))); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBounds.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBounds.java new file mode 100644 index 000000000..c1a1e0c5f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBounds.java @@ -0,0 +1,25 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.interpolation; + +/** + * An immutable minimum/maximum noise pair produced by bounds interpolation. + */ +public record NoiseBounds(double min, double max) { +} diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsProvider.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsProvider.java new file mode 100644 index 000000000..ee0952ca1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsProvider.java @@ -0,0 +1,24 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.interpolation; + +@FunctionalInterface +public interface NoiseBoundsProvider { + NoiseBounds noise(double x, double z); +} diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java new file mode 100644 index 000000000..7697153bc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java @@ -0,0 +1,221 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.interpolation; + +import art.arcane.volmlib.util.function.NoiseProvider; + +import java.util.Arrays; + +/** + * Open-addressed memo table that samples a {@link NoiseBoundsProvider} once per column and serves + * both the min and max side from the same entry. Single threaded by contract; instances are held in + * thread locals. + *

+ * The min/max {@link NoiseProvider} views are allocated once per cache instance and read the + * currently bound provider, so a bounds interpolation pass allocates no lambdas per column. + */ +final class NoiseBoundsSampleCache2D { + private final NoiseProvider minView = this::sampleMin; + private final NoiseProvider maxView = this::sampleMax; + private NoiseBoundsProvider boundProvider; + private long[] xBits; + private long[] zBits; + private double[] minValues; + private double[] maxValues; + private byte[] states; + private int mask; + private int resizeThreshold; + private int size; + private boolean inUse; + + public NoiseBoundsSampleCache2D(int initialCapacity) { + int minimumCapacity = Math.max(8, initialCapacity); + int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); + xBits = new long[tableSize]; + zBits = new long[tableSize]; + minValues = new double[tableSize]; + maxValues = new double[tableSize]; + states = new byte[tableSize]; + mask = tableSize - 1; + resizeThreshold = Math.max(1, (tableSize * 3) >> 2); + size = 0; + } + + public void clear() { + if (size == 0) { + return; + } + Arrays.fill(states, (byte) 0); + size = 0; + } + + /** + * Bounds interpolation nests on one thread; a nested pass must never reuse the outer + * pass's table, since entries belong to one specific bound provider. + */ + public boolean isInUse() { + return inUse; + } + + public void beginUse() { + inUse = true; + clear(); + } + + public void endUse() { + inUse = false; + } + + /** + * Binds the provider that {@link #minView()} and {@link #maxView()} delegate to, returning the + * previously bound provider so callers can restore it. + */ + public NoiseBoundsProvider bindProvider(NoiseBoundsProvider provider) { + NoiseBoundsProvider previous = boundProvider; + boundProvider = provider; + return previous; + } + + public NoiseProvider minView() { + return minView; + } + + public NoiseProvider maxView() { + return maxView; + } + + private double sampleMin(double sampleX, double sampleZ) { + return getOrSampleMin(sampleX, sampleZ, boundProvider); + } + + private double sampleMax(double sampleX, double sampleZ) { + return getOrSampleMax(sampleX, sampleZ, boundProvider); + } + + public double getOrSampleMin(double sampleX, double sampleZ, NoiseBoundsProvider provider) { + long xBitsValue = Double.doubleToLongBits(sampleX); + long zBitsValue = Double.doubleToLongBits(sampleZ); + int slot = findSlot(xBitsValue, zBitsValue); + if (states[slot] != 0) { + return minValues[slot]; + } + + NoiseBounds bounds = provider.noise(sampleX, sampleZ); + // Recompute the slot: the provider call can mutate the table (clear/grow) via nesting. + insert(findSlot(xBitsValue, zBitsValue), xBitsValue, zBitsValue, bounds.min(), bounds.max()); + return bounds.min(); + } + + public double getOrSampleMax(double sampleX, double sampleZ, NoiseBoundsProvider provider) { + long xBitsValue = Double.doubleToLongBits(sampleX); + long zBitsValue = Double.doubleToLongBits(sampleZ); + int slot = findSlot(xBitsValue, zBitsValue); + if (states[slot] != 0) { + return maxValues[slot]; + } + + NoiseBounds bounds = provider.noise(sampleX, sampleZ); + // Recompute the slot: the provider call can mutate the table (clear/grow) via nesting. + insert(findSlot(xBitsValue, zBitsValue), xBitsValue, zBitsValue, bounds.min(), bounds.max()); + return bounds.max(); + } + + private int findSlot(long xb, long zb) { + int slot = mix(xb, zb) & mask; + while (states[slot] != 0) { + if (xBits[slot] == xb && zBits[slot] == zb) { + break; + } + slot = (slot + 1) & mask; + } + return slot; + } + + private void insert(int slot, long xb, long zb, double min, double max) { + xBits[slot] = xb; + zBits[slot] = zb; + minValues[slot] = min; + maxValues[slot] = max; + states[slot] = 1; + size++; + if (size >= resizeThreshold) { + grow(); + } + } + + private int mix(long xb, long zb) { + long hash = xb * 0x9E3779B97F4A7C15L; + hash ^= Long.rotateLeft(zb * 0xC2B2AE3D27D4EB4FL, 32); + hash ^= (hash >>> 33); + hash *= 0xff51afd7ed558ccdL; + hash ^= (hash >>> 33); + return (int) hash; + } + + private void grow() { + long[] previousXBits = xBits; + long[] previousZBits = zBits; + double[] previousMin = minValues; + double[] previousMax = maxValues; + byte[] previousStates = states; + + int nextLength = xBits.length << 1; + long[] nextXBits = new long[nextLength]; + long[] nextZBits = new long[nextLength]; + double[] nextMin = new double[nextLength]; + double[] nextMax = new double[nextLength]; + byte[] nextStates = new byte[nextLength]; + + xBits = nextXBits; + zBits = nextZBits; + minValues = nextMin; + maxValues = nextMax; + states = nextStates; + mask = nextLength - 1; + resizeThreshold = Math.max(1, (nextLength * 3) >> 2); + size = 0; + + for (int i = 0; i < previousStates.length; i++) { + if (previousStates[i] == 0) { + continue; + } + int slot = findSlot(previousXBits[i], previousZBits[i]); + xBits[slot] = previousXBits[i]; + zBits[slot] = previousZBits[i]; + minValues[slot] = previousMin[i]; + maxValues[slot] = previousMax[i]; + states[slot] = 1; + size++; + } + } + + private int tableSizeFor(int value) { + int n = value - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + int tableSize = n + 1; + if (tableSize < 8) { + return 8; + } + return tableSize; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseSampleCache2D.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseSampleCache2D.java new file mode 100644 index 000000000..e88b168ee --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseSampleCache2D.java @@ -0,0 +1,170 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.interpolation; + +import art.arcane.volmlib.util.function.NoiseProvider; + +import java.util.Arrays; + +/** + * Open-addressed memo table keyed on a relative sample offset, used to collapse the duplicate + * noise probes that starcast composites issue for the same column. Single threaded by contract; + * instances are held in thread locals. + */ +final class NoiseSampleCache2D { + private long[] xBits; + private long[] zBits; + private double[] values; + private byte[] states; + private int mask; + private int resizeThreshold; + private int size; + private boolean inUse; + + public NoiseSampleCache2D(int initialCapacity) { + int minimumCapacity = Math.max(8, initialCapacity); + int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); + xBits = new long[tableSize]; + zBits = new long[tableSize]; + values = new double[tableSize]; + states = new byte[tableSize]; + mask = tableSize - 1; + resizeThreshold = Math.max(1, (tableSize * 3) >> 2); + size = 0; + } + + public void clear() { + if (size == 0) { + return; + } + Arrays.fill(states, (byte) 0); + size = 0; + } + + /** + * Interpolation nests on one thread (image maps and interpolated noise styles re-enter + * {@code IrisInterpolation.getNoise} from inside a provider), and a nested pass sharing + * this table would clear it mid-flight and cross-serve another provider's samples. + */ + public boolean isInUse() { + return inUse; + } + + public void beginUse() { + inUse = true; + clear(); + } + + public void endUse() { + inUse = false; + } + + public double getOrSample(double relativeX, double relativeZ, double sampleX, double sampleZ, NoiseProvider provider) { + long rx = Double.doubleToLongBits(relativeX); + long rz = Double.doubleToLongBits(relativeZ); + int slot = findSlot(rx, rz); + if (states[slot] != 0) { + return values[slot]; + } + + double value = provider.noise(sampleX, sampleZ); + // Recompute the slot: the provider call may have mutated the table (clear/grow), so + // the pre-call index can point into stale or reallocated arrays. + insert(findSlot(rx, rz), rx, rz, value); + return value; + } + + private int findSlot(long rx, long rz) { + int slot = mix(rx, rz) & mask; + while (states[slot] != 0) { + if (xBits[slot] == rx && zBits[slot] == rz) { + break; + } + slot = (slot + 1) & mask; + } + return slot; + } + + private void insert(int slot, long rx, long rz, double value) { + xBits[slot] = rx; + zBits[slot] = rz; + values[slot] = value; + states[slot] = 1; + size++; + if (size >= resizeThreshold) { + grow(); + } + } + + private int mix(long rx, long rz) { + long hash = rx * 0x9E3779B97F4A7C15L; + hash ^= Long.rotateLeft(rz * 0xC2B2AE3D27D4EB4FL, 32); + hash ^= (hash >>> 33); + hash *= 0xff51afd7ed558ccdL; + hash ^= (hash >>> 33); + return (int) hash; + } + + private void grow() { + long[] previousXBits = xBits; + long[] previousZBits = zBits; + double[] previousValues = values; + byte[] previousStates = states; + + int nextLength = xBits.length << 1; + long[] nextXBits = new long[nextLength]; + long[] nextZBits = new long[nextLength]; + double[] nextValues = new double[nextLength]; + byte[] nextStates = new byte[nextLength]; + + xBits = nextXBits; + zBits = nextZBits; + values = nextValues; + states = nextStates; + mask = nextLength - 1; + resizeThreshold = Math.max(1, (nextLength * 3) >> 2); + size = 0; + + for (int i = 0; i < previousStates.length; i++) { + if (previousStates[i] == 0) { + continue; + } + int slot = findSlot(previousXBits[i], previousZBits[i]); + xBits[slot] = previousXBits[i]; + zBits[slot] = previousZBits[i]; + values[slot] = previousValues[i]; + states[slot] = 1; + size++; + } + } + + private int tableSizeFor(int value) { + int n = value - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + int size = n + 1; + if (size < 8) { + return 8; + } + return size; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterContext.java b/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterContext.java new file mode 100644 index 000000000..aa16398d8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterContext.java @@ -0,0 +1,60 @@ +package art.arcane.iris.util.project.matter; + +import art.arcane.iris.core.loader.IrisData; + +import java.util.Objects; + +public final class IrisMatterContext { + private static final ThreadLocal DATA = new ThreadLocal<>(); + + private IrisMatterContext() { + } + + public static Scope open(IrisData data) { + Thread owner = Thread.currentThread(); + IrisData previous = DATA.get(); + IrisData installed = Objects.requireNonNull(data); + DATA.set(installed); + return new Scope(owner, previous, installed); + } + + public static IrisData require() { + IrisData data = DATA.get(); + if (data == null) { + throw new IllegalStateException("No Iris matter data context is bound to thread " + Thread.currentThread().getName() + "."); + } + return data; + } + + public static final class Scope implements AutoCloseable { + private final Thread owner; + private final IrisData previous; + private final IrisData installed; + private boolean closed; + + private Scope(Thread owner, IrisData previous, IrisData installed) { + this.owner = owner; + this.previous = previous; + this.installed = installed; + } + + @Override + public void close() { + if (closed) { + return; + } + if (Thread.currentThread() != owner) { + throw new IllegalStateException("Iris matter data scope closed from a different thread."); + } + if (DATA.get() != installed) { + throw new IllegalStateException("Iris matter data scopes must close in LIFO order."); + } + if (previous == null) { + DATA.remove(); + } else { + DATA.set(previous); + } + closed = true; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java b/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java new file mode 100644 index 000000000..8d50fffaa --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java @@ -0,0 +1,121 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.matter; + +import art.arcane.iris.engine.object.IrisObject; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.project.matter.slices.EntityMatter; +import art.arcane.iris.util.project.matter.slices.IdentifierMatter; +import art.arcane.iris.util.project.matter.slices.NativeStructureOwnershipMatter; +import art.arcane.iris.util.project.matter.slices.PlatformBlockMatter; +import art.arcane.iris.util.project.matter.slices.SpawnerMatter; +import art.arcane.iris.util.project.matter.slices.TileMatter; +import art.arcane.iris.util.project.matter.slices.TreeBlockMaterialMatter; +import art.arcane.volmlib.util.matter.IrisMatter; +import art.arcane.volmlib.util.matter.Matter; +import org.bukkit.block.data.BlockData; +import art.arcane.iris.util.common.math.IrisBlockVector; + +import java.io.File; + +public final class IrisMatterSupport { + private static final boolean BUKKIT_PRESENT = detectBukkit(); + private static boolean registered; + + private IrisMatterSupport() { + } + + public static boolean isBukkitPresent() { + return BUKKIT_PRESENT; + } + + private static boolean detectBukkit() { + try { + Class.forName("org.bukkit.World", false, IrisMatterSupport.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static synchronized void ensureRegistered() { + if (registered) { + return; + } + + IrisMatter.registerSliceType(new EntityMatter()); + IrisMatter.registerSliceType(new IdentifierMatter()); + IrisMatter.registerSliceType(new NativeStructureOwnershipMatter()); + IrisMatter.registerSliceType(new PlatformBlockMatter()); + IrisMatter.registerSliceType(new SpawnerMatter()); + IrisMatter.registerSliceType(new TileMatter()); + IrisMatter.registerSliceType(new TreeBlockMaterialMatter()); + registered = true; + } + + public static Matter from(IrisObject object) { + ensureRegistered(); + object.clean(); + object.shrinkwrap(); + IrisBlockVector min = new IrisBlockVector(); + Matter matter = new IrisMatter(Math.max(object.getW(), 1) + 1, Math.max(object.getH(), 1) + 1, Math.max(object.getD(), 1) + 1); + + for (IrisBlockVector i : object.getBlocks().keys()) { + min.setX(Math.min(min.getX(), i.getX())); + min.setY(Math.min(min.getY(), i.getY())); + min.setZ(Math.min(min.getZ(), i.getZ())); + } + + for (IrisBlockVector i : object.getBlocks().keys()) { + matter.slice(BlockData.class).set(i.getBlockX() - min.getBlockX(), i.getBlockY() - min.getBlockY(), i.getBlockZ() - min.getBlockZ(), (BlockData) object.getBlocks().get(i).nativeHandle()); + } + + return matter; + } + + public static long convert(File folder) { + if (folder.isDirectory()) { + long total = 0; + File[] files = folder.listFiles(); + if (files == null) { + return total; + } + + for (File file : files) { + total += convert(file); + } + + return total; + } + + IrisObject object = new IrisObject(1, 1, 1); + try { + long oldSize = folder.length(); + object.read(folder); + from(object).write(folder); + IrisLogging.info("Converted " + folder.getPath() + " Saved " + (oldSize - folder.length())); + } catch (Throwable e) { + IrisLogging.error("Failed to convert " + folder.getPath()); + e.printStackTrace(); + } + + return 0; + } +} diff --git a/core/src/main/java/com/volmit/iris/util/matter/TileWrapper.java b/core/src/main/java/art/arcane/iris/util/project/matter/TileWrapper.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/matter/TileWrapper.java rename to core/src/main/java/art/arcane/iris/util/project/matter/TileWrapper.java index 0fa14eab6..dc9082c1d 100644 --- a/core/src/main/java/com/volmit/iris/util/matter/TileWrapper.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/TileWrapper.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.matter; +package art.arcane.iris.util.project.matter; -import com.volmit.iris.engine.object.TileData; +import art.arcane.iris.engine.object.TileData; import lombok.Data; @Data diff --git a/core/src/main/java/com/volmit/iris/util/matter/WorldMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/WorldMatter.java similarity index 79% rename from core/src/main/java/com/volmit/iris/util/matter/WorldMatter.java rename to core/src/main/java/art/arcane/iris/util/project/matter/WorldMatter.java index 40ff187d0..f59f8065c 100644 --- a/core/src/main/java/com/volmit/iris/util/matter/WorldMatter.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/WorldMatter.java @@ -16,16 +16,19 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.matter; +package art.arcane.iris.util.project.matter; -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.TileData; -import com.volmit.iris.util.data.Cuboid; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.data.Cuboid; +import art.arcane.volmlib.util.matter.IrisMatter; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterEntityGroup; import org.bukkit.Location; import org.bukkit.block.data.BlockData; public class WorldMatter { public static void placeMatter(Matter matter, Location at) { + IrisMatterSupport.ensureRegistered(); if (matter.hasSlice(BlockData.class)) { matter.slice(BlockData.class).writeInto(at); } @@ -40,9 +43,10 @@ public class WorldMatter { } public static Matter createMatter(String author, Location a, Location b) { + IrisMatterSupport.ensureRegistered(); Cuboid c = new Cuboid(a, b); Matter s = new IrisMatter(c.getSizeX(), c.getSizeY(), c.getSizeZ()); - Iris.info(s.getWidth() + " " + s.getHeight() + " " + s.getDepth()); + IrisLogging.info(s.getWidth() + " " + s.getHeight() + " " + s.getDepth()); s.getHeader().setAuthor(author); s.slice(BlockData.class).readFrom(c.getLowerNE()); s.slice(MatterEntityGroup.class).readFrom(c.getLowerNE()); diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/EntityMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/EntityMatter.java similarity index 83% rename from core/src/main/java/com/volmit/iris/util/matter/slices/EntityMatter.java rename to core/src/main/java/art/arcane/iris/util/project/matter/slices/EntityMatter.java index 23c0e0495..6a89092cc 100644 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/EntityMatter.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/EntityMatter.java @@ -16,20 +16,23 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.matter.slices; +package art.arcane.iris.util.project.matter.slices; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.engine.object.IrisPosition; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.Varint; -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.MatterEntity; -import com.volmit.iris.util.matter.MatterEntityGroup; -import com.volmit.iris.util.matter.MatterReader; -import com.volmit.iris.util.matter.Sliced; -import com.volmit.iris.util.nbt.io.NBTUtil; -import com.volmit.iris.util.nbt.tag.CompoundTag; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.util.project.matter.IrisMatterSupport; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.data.Varint; +import art.arcane.volmlib.util.data.palette.Palette; +import art.arcane.volmlib.util.matter.MatterEntity; +import art.arcane.volmlib.util.matter.MatterEntityGroup; +import art.arcane.volmlib.util.matter.MatterReader; +import art.arcane.volmlib.util.matter.Sliced; +import art.arcane.volmlib.util.matter.slices.RawMatter; +import art.arcane.volmlib.util.nbt.io.NBTUtil; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Entity; @@ -50,6 +53,12 @@ public class EntityMatter extends RawMatter { public EntityMatter(int width, int height, int depth) { super(width, height, depth, MatterEntityGroup.class); + if (IrisMatterSupport.isBukkitPresent()) { + registerBukkitIO(); + } + } + + private void registerBukkitIO() { registerWriter(World.class, ((w, d, x, y, z) -> { for (MatterEntity i : d.getEntities()) { Location realPosition = new Location(w, x + i.getXOff(), y + i.getYOff(), z + i.getZOff()); @@ -110,7 +119,7 @@ public class EntityMatter extends RawMatter { entityCache = new KMap<>(); for (Entity i : ((World) w).getNearbyEntities(new BoundingBox(x, y, z, x + getWidth(), y + getHeight(), z + getHeight()))) { - entityCache.computeIfAbsent(new IrisPosition(i.getLocation()), + entityCache.computeIfAbsent(BukkitPlatform.positionOf(i.getLocation()), k -> new KList<>()).add(i); } diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/slices/IdentifierMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/IdentifierMatter.java new file mode 100644 index 000000000..fbbdb254a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/IdentifierMatter.java @@ -0,0 +1,37 @@ +package art.arcane.iris.util.project.matter.slices; + +import art.arcane.iris.core.link.Identifier; +import art.arcane.volmlib.util.data.palette.Palette; +import art.arcane.volmlib.util.matter.Sliced; +import art.arcane.volmlib.util.matter.slices.RawMatter; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +@Sliced +public class IdentifierMatter extends RawMatter { + + public IdentifierMatter() { + this(1, 1, 1); + } + + public IdentifierMatter(int width, int height, int depth) { + super(width, height, depth, Identifier.class); + } + + @Override + public Palette getGlobalPalette() { + return null; + } + + @Override + public void writeNode(Identifier b, DataOutputStream dos) throws IOException { + dos.writeUTF(b.toString()); + } + + @Override + public Identifier readNode(DataInputStream din) throws IOException { + return Identifier.fromString(din.readUTF()); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/slices/NativeStructureOwnershipMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/NativeStructureOwnershipMatter.java new file mode 100644 index 000000000..7a7f3c830 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/NativeStructureOwnershipMatter.java @@ -0,0 +1,36 @@ +package art.arcane.iris.util.project.matter.slices; + +import art.arcane.iris.engine.framework.NativeStructureOwnershipBundle; +import art.arcane.volmlib.util.data.palette.Palette; +import art.arcane.volmlib.util.matter.Sliced; +import art.arcane.volmlib.util.matter.slices.RawMatter; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +@Sliced +public final class NativeStructureOwnershipMatter extends RawMatter { + public NativeStructureOwnershipMatter() { + this(1, 1, 1); + } + + public NativeStructureOwnershipMatter(int width, int height, int depth) { + super(width, height, depth, NativeStructureOwnershipBundle.class); + } + + @Override + public Palette getGlobalPalette() { + return null; + } + + @Override + public void writeNode(NativeStructureOwnershipBundle bundle, DataOutputStream output) throws IOException { + bundle.write(output); + } + + @Override + public NativeStructureOwnershipBundle readNode(DataInputStream input) throws IOException { + return NativeStructureOwnershipBundle.read(input); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/slices/PlatformBlockMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/PlatformBlockMatter.java new file mode 100644 index 000000000..fc939a5cb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/PlatformBlockMatter.java @@ -0,0 +1,39 @@ +package art.arcane.iris.util.project.matter.slices; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.volmlib.util.data.palette.Palette; +import art.arcane.volmlib.util.matter.Sliced; +import art.arcane.volmlib.util.matter.slices.RawMatter; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +@Sliced +public class PlatformBlockMatter extends RawMatter { + + public PlatformBlockMatter() { + this(1, 1, 1); + } + + public PlatformBlockMatter(int width, int height, int depth) { + super(width, height, depth, PlatformBlockState.class); + } + + @Override + public Palette getGlobalPalette() { + return null; + } + + @Override + public void writeNode(PlatformBlockState b, DataOutputStream dos) throws IOException { + dos.writeUTF(b.key()); + } + + @Override + public PlatformBlockState readNode(DataInputStream din) throws IOException { + PlatformBlockState state = B.getState(din.readUTF()); + return state == null ? B.getAirState() : state; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/slices/RegistryMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/RegistryMatter.java new file mode 100644 index 000000000..780e55617 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/RegistryMatter.java @@ -0,0 +1,57 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.matter.slices; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.util.project.matter.IrisMatterContext; +import art.arcane.volmlib.util.data.palette.Palette; +import art.arcane.volmlib.util.matter.slices.RawMatter; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class RegistryMatter extends RawMatter { + public RegistryMatter(int width, int height, int depth, Class c, T e) { + super(width, height, depth, c); + } + + @Override + public Palette getGlobalPalette() { + return null; + } + + @Override + public void writeNode(T b, DataOutputStream dos) throws IOException { + dos.writeUTF(b.getLoadKey()); + } + + @Override + public T readNode(DataInputStream din) throws IOException { + String key = din.readUTF(); + IrisData data = IrisMatterContext.require(); + ResourceLoader loader = data.getLoaders().get(getType()); + if (loader == null) { + throw new IOException("No Iris registry loader is available for matter type " + getType().getName() + " while reading key " + key + "."); + } + return (T) loader.load(key); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/SpawnerMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/SpawnerMatter.java similarity index 87% rename from core/src/main/java/com/volmit/iris/util/matter/slices/SpawnerMatter.java rename to core/src/main/java/art/arcane/iris/util/project/matter/slices/SpawnerMatter.java index 75ec22257..93bcbe9cb 100644 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/SpawnerMatter.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/SpawnerMatter.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.matter.slices; +package art.arcane.iris.util.project.matter.slices; -import com.volmit.iris.engine.object.IrisSpawner; -import com.volmit.iris.util.matter.Sliced; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.volmlib.util.matter.Sliced; @Sliced public class SpawnerMatter extends RegistryMatter { diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/TileMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/TileMatter.java similarity index 78% rename from core/src/main/java/com/volmit/iris/util/matter/slices/TileMatter.java rename to core/src/main/java/art/arcane/iris/util/project/matter/slices/TileMatter.java index c5febf50b..71dee5144 100644 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/TileMatter.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/TileMatter.java @@ -16,12 +16,15 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.matter.slices; +package art.arcane.iris.util.project.matter.slices; -import com.volmit.iris.engine.object.TileData; -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; -import com.volmit.iris.util.matter.TileWrapper; +import art.arcane.iris.engine.object.TileData; + +import art.arcane.iris.util.project.matter.IrisMatterSupport; +import art.arcane.iris.util.project.matter.TileWrapper; +import art.arcane.volmlib.util.data.palette.Palette; +import art.arcane.volmlib.util.matter.Sliced; +import art.arcane.volmlib.util.matter.slices.RawMatter; import org.bukkit.Location; import org.bukkit.World; @@ -39,6 +42,12 @@ public class TileMatter extends RawMatter { public TileMatter(int width, int height, int depth) { super(width, height, depth, TileWrapper.class); + if (IrisMatterSupport.isBukkitPresent()) { + registerBukkitIO(); + } + } + + private void registerBukkitIO() { registerWriter(World.class, (w, d, x, y, z) -> TileData.setTileState(w.getBlockAt(new Location(w, x, y, z)), d.getData())); registerReader(World.class, (w, x, y, z) -> new TileWrapper(TileData.getTileState(w.getBlockAt(new Location(w, x, y, z)), false))); } diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/slices/TreeBlockMaterialMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/TreeBlockMaterialMatter.java new file mode 100644 index 000000000..889126c73 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/TreeBlockMaterialMatter.java @@ -0,0 +1,36 @@ +package art.arcane.iris.util.project.matter.slices; + +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.volmlib.util.data.palette.Palette; +import art.arcane.volmlib.util.matter.Sliced; +import art.arcane.volmlib.util.matter.slices.RawMatter; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +@Sliced +public class TreeBlockMaterialMatter extends RawMatter { + public TreeBlockMaterialMatter() { + this(1, 1, 1); + } + + public TreeBlockMaterialMatter(int width, int height, int depth) { + super(width, height, depth, TreeBlockMaterial.class); + } + + @Override + public Palette getGlobalPalette() { + return null; + } + + @Override + public void writeNode(TreeBlockMaterial material, DataOutputStream output) throws IOException { + output.writeUTF(material.materialKey()); + } + + @Override + public TreeBlockMaterial readNode(DataInputStream input) throws IOException { + return new TreeBlockMaterial(input.readUTF()); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java new file mode 100644 index 000000000..a4b5eea2e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java @@ -0,0 +1,1152 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.noise; + +import art.arcane.iris.engine.object.IRare; +import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.cache.FloatCache; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.function.NoiseInjector; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.arithmetic.FittedStream; +import art.arcane.iris.util.project.stream.sources.CNGStream; +import lombok.Data; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.List; + +@Data +public class CNG { + public static final NoiseInjector ADD = (s, v) -> new double[]{s + v, 1}; + public static final NoiseInjector SRC_SUBTRACT = (s, v) -> new double[]{s - v < 0 ? 0 : s - v, -1}; + public static final NoiseInjector DST_SUBTRACT = (s, v) -> new double[]{v - s < 0 ? 0 : s - v, -1}; + public static final NoiseInjector MULTIPLY = (s, v) -> new double[]{s * v, 0}; + public static final NoiseInjector MAX = (s, v) -> new double[]{Math.max(s, v), 0}; + public static final NoiseInjector MIN = (s, v) -> new double[]{Math.min(s, v), 0}; + public static final NoiseInjector SRC_MOD = (s, v) -> new double[]{s % v, 0}; + public static final NoiseInjector SRC_POW = (s, v) -> new double[]{Math.pow(s, v), 0}; + public static final NoiseInjector DST_MOD = (s, v) -> new double[]{v % s, 0}; + public static final NoiseInjector DST_POW = (s, v) -> new double[]{Math.pow(v, s), 0}; + private final double opacity; + private double scale; + private double bakedScale; + private double fscale; + private KList children; + private CNG fracture; + private FloatCache cache; + private NoiseGenerator generator; + private NoiseInjector injector; + private InjectorMode injectorMode; + private RNG rng; + private boolean noscale; + private int oct; + private double patch; + private double up; + private double down; + private double power; + private NoiseStyle leakStyle; + private ProceduralStream customGenerator; + private transient boolean identityPostFastPath; + private transient boolean identitySignedFastPath; + private transient boolean unityOpacity; + private transient double effectiveScale; + private transient double signedFractureScale; + private transient boolean fastPathStateDirty = true; + private transient int coordCacheSalt; + + public CNG(RNG random) { + this(random, 1); + } + + public CNG(RNG random, int octaves) { + this(random, 1D, octaves); + } + + public CNG(RNG random, double opacity, int octaves) { + this(random, NoiseType.SIMPLEX, opacity, octaves); + } + + public CNG(RNG random, NoiseType type, double opacity, int octaves) { + this(random, type.create(random.nextParallelRNG((long) ((1113334944L * opacity) + 12922 + octaves)).lmax()), opacity, octaves); + } + + public CNG(RNG random, NoiseGenerator generator, double opacity, int octaves) { + customGenerator = null; + noscale = generator.isNoScale(); + this.oct = octaves; + this.rng = random; + power = 1; + scale = 1; + patch = 1; + bakedScale = 1; + fscale = 1; + down = 0; + up = 0; + fracture = null; + this.generator = generator; + this.opacity = opacity; + this.injector = ADD; + this.injectorMode = InjectorMode.ADD; + coordCacheSalt = createCoordCacheSalt(); + + if (generator instanceof OctaveNoise) { + ((OctaveNoise) generator).setOctaves(octaves); + } + + refreshFastPathState(); + } + + public static CNG signature(RNG rng) { + return signature(rng, NoiseType.SIMPLEX); + } + + public static CNG signatureHalf(RNG rng) { + return signatureHalf(rng, NoiseType.SIMPLEX); + } + + public static CNG signatureThick(RNG rng) { + return signatureThick(rng, NoiseType.SIMPLEX); + } + + public static CNG signatureDouble(RNG rng) { + return signatureDouble(rng, NoiseType.SIMPLEX); + } + + public static CNG signatureDouble(RNG rng, NoiseType t) { + return signatureThick(rng, t).fractureWith(signature(rng.nextParallelRNG(4956)), 93); + } + + public static CNG signatureDoubleFast(RNG rng, NoiseType t, NoiseType f) { + return signatureThickFast(rng, t, f) + .fractureWith(signatureFast(rng.nextParallelRNG(4956), t, f), 93); + } + + public static CNG signature(RNG rng, NoiseType t) { + // @NoArgsConstructor + return new CNG(rng.nextParallelRNG(17), t, 1D, 1).fractureWith(new CNG(rng.nextParallelRNG(18), 1, 1).scale(0.9).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.21).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.9), 620), 145), 44).bake(); + // @done + } + + public static CNG signaturePerlin(RNG rng) { + return signaturePerlin(rng, NoiseType.PERLIN); + } + + public static CNG signaturePerlin(RNG rng, NoiseType t) { + // @NoArgsConstructor + return new CNG(rng.nextParallelRNG(124996), t, 1D, 1) + .fractureWith(new CNG(rng.nextParallelRNG(18), NoiseType.PERLIN, 1, 1).scale(1.25), 250) + .bake(); + // @done + } + + public static CNG signatureFast(RNG rng, NoiseType t, NoiseType f) { + // @NoArgsConstructor + return new CNG(rng.nextParallelRNG(17), t, 1D, 1) + .fractureWith(new CNG(rng.nextParallelRNG(18), f, 1, 1) + .scale(0.9) + .fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1) + .scale(0.21) + .fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1).scale(0.9), 620), 145), 44) + .bake(); + // @done + } + + public static CNG signatureThick(RNG rng, NoiseType t) { + // @NoArgsConstructor + return new CNG(rng.nextParallelRNG(133), t, 1D, 1).fractureWith(new CNG(rng.nextParallelRNG(18), 1, 1).scale(0.5).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.11).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.4), 620), 145), 44).bake(); + // @done + } + + public static CNG signatureThickFast(RNG rng, NoiseType t, NoiseType f) { + // @NoArgsConstructor + return new CNG(rng.nextParallelRNG(133), t, 1D, 1) + .fractureWith(new CNG(rng.nextParallelRNG(18), f, 1, 1) + .scale(0.5).fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1) + .scale(0.11).fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1) + .scale(0.4), 620), 145), 44).bake(); + // @done + } + + public static CNG signatureHalf(RNG rng, NoiseType t) { + // @NoArgsConstructor + return new CNG(rng.nextParallelRNG(127), t, 1D, 1).fractureWith(new CNG(rng.nextParallelRNG(18), 1, 1).scale(0.9).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.21).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.9), 420), 99), 22).bake(); + // @done + } + + public static CNG signatureHalfFast(RNG rng, NoiseType t, NoiseType f) { + // @NoArgsConstructor + return new CNG(rng.nextParallelRNG(127), t, 1D, 1) + .fractureWith(new CNG(rng.nextParallelRNG(18), f, 1, 1).scale(0.9) + .fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1).scale(0.21) + .fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1).scale(0.9), 420), 99), 22).bake(); + // @done + } + + public static void main(String[] a) { + CNG cng = NoiseStyle.SIMPLEX.create(new RNG(1234)); + PrecisionStopwatch p = PrecisionStopwatch.start(); + double r = 0; + + for (int i = 0; i < 30000000 * 10; i++) { + r += cng.fit(-1000, 1000, i, i); + } + + System.out.println(Form.duration(p.getMilliseconds(), 10) + " merged = " + r); + } + + public CNG cellularize(RNG seed, double freq) { + FastNoise cellularFilter = new FastNoise(seed.imax()); + cellularFilter.SetNoiseType(FastNoise.NoiseType.Cellular); + cellularFilter.SetCellularReturnType(FastNoise.CellularReturnType.CellValue); + cellularFilter.SetCellularDistanceFunction(FastNoise.CellularDistanceFunction.Manhattan); + cellularFilter.SetFrequency((float) freq * 0.01f); + + ProceduralStream str = stream(); + + return new CNG(seed, new NoiseGenerator() { + @Override + public double noise(double x) { + return noise(x, 0); + } + + @Override + public double noise(double x, double z) { + return (cellularFilter.GetCellular((float) x, (float) z, str, 1) * 0.5) + 0.5D; + } + + @Override + public double noise(double x, double y, double z) { + return noise(x, y + z); + } + }, 1D, 1); + } + + public CNG cached(int size, String key, File cacheFolder) { + return cached(size, key, cacheFolder, false); + } + + public CNG cached(int size, String key, File cacheFolder, boolean quiet) { + if (size <= 0) { + return this; + } + + cache = null; + + File f = new File(new File(cacheFolder, ".cache"), key + ".cnm"); + FloatCache fbc; + boolean cached = false; + if (f.exists()) { + try { + fbc = new FloatCache(f); + cached = true; + } catch (IOException e) { + fbc = new FloatCache(size, size); + } + } else { + fbc = new FloatCache(size, size); + } + + if (!cached) { + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + fbc.set(i, j, (float) noise(i, j)); + } + } + + // Write to a unique temp name (excluded from the *.cnm stale-entry pruner) and + // move atomically: a throw mid-write must never leak the descriptor or leave a + // truncated .cnm that the read path would pick up by existence alone. The name is + // per-writer unique so two engines caching the same style key cannot interleave + // into one temp file. + File tmp = new File(f.getParentFile(), f.getName() + "." + Long.toUnsignedString(System.nanoTime(), 36) + + "-" + Long.toUnsignedString(Thread.currentThread().threadId(), 36) + ".tmp"); + try { + f.getParentFile().mkdirs(); + try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(tmp))) { + fbc.writeCache(dos); + } + Files.move(tmp.toPath(), f.toPath(), + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + if (!quiet) { + IrisLogging.info("Saved Noise Cache " + f.getName()); + } + } catch (IOException e) { + try { + Files.deleteIfExists(tmp.toPath()); + } catch (IOException ignored) { + } + throw new RuntimeException(e); + } + } + + cache = fbc; + return this; + } + + public NoiseGenerator getGen() { + return generator; + } + + public ProceduralStream stream() { + return new CNGStream(this); + } + + public ProceduralStream stream(double min, double max) { + return new FittedStream<>(stream(), min, max); + } + + public CNG bake() { + bakedScale *= scale; + scale = 1; + markFastPathStateDirty(); + return this; + } + + public CNG child(CNG c) { + if (children == null) { + children = new KList<>(); + } + + children.add(c); + markFastPathStateDirty(); + return this; + } + + public RNG getRNG() { + return rng; + } + + public CNG fractureWith(CNG c, double scale) { + fracture = c; + fscale = scale; + markFastPathStateDirty(); + return this; + } + + public CNG scale(double c) { + scale = c; + markFastPathStateDirty(); + return this; + } + + public CNG patch(double c) { + patch = c; + markFastPathStateDirty(); + return this; + } + + public CNG up(double c) { + up = c; + markFastPathStateDirty(); + return this; + } + + public CNG down(double c) { + down = c; + markFastPathStateDirty(); + return this; + } + + public CNG injectWith(NoiseInjector i) { + injector = i == null ? ADD : i; + injectorMode = resolveInjectorMode(injector); + markFastPathStateDirty(); + return this; + } + + private InjectorMode resolveInjectorMode(NoiseInjector i) { + if (i == ADD) { + return InjectorMode.ADD; + } + + if (i == SRC_SUBTRACT) { + return InjectorMode.SRC_SUBTRACT; + } + + if (i == DST_SUBTRACT) { + return InjectorMode.DST_SUBTRACT; + } + + if (i == MULTIPLY) { + return InjectorMode.MULTIPLY; + } + + if (i == MAX) { + return InjectorMode.MAX; + } + + if (i == MIN) { + return InjectorMode.MIN; + } + + if (i == SRC_MOD) { + return InjectorMode.SRC_MOD; + } + + if (i == SRC_POW) { + return InjectorMode.SRC_POW; + } + + if (i == DST_MOD) { + return InjectorMode.DST_MOD; + } + + if (i == DST_POW) { + return InjectorMode.DST_POW; + } + + return InjectorMode.CUSTOM; + } + + public T fitRarity(KList b, double... dim) { + if (dim.length == 2) { + return fitRarity2D(b, dim[0], dim[1]); + } + + if (b.size() == 0) { + return null; + } + + if (b.size() == 1) { + return b.get(0); + } + + KList rarityMapped = buildRarityMapped(b); + + if (rarityMapped.size() == 1) { + return rarityMapped.get(0); + } + + if (rarityMapped.isEmpty()) { + throw new RuntimeException("BAD RARITY MAP! RELATED TO: " + b.toString(", or possibly ")); + } + + return fit(rarityMapped, dim); + } + + public T fitRarity2D(KList b, double x, double z) { + if (b.size() == 0) { + return null; + } + + if (b.size() == 1) { + return b.get(0); + } + + KList rarityMapped = buildRarityMapped(b); + if (rarityMapped.size() == 1) { + return rarityMapped.get(0); + } + + if (rarityMapped.isEmpty()) { + throw new RuntimeException("BAD RARITY MAP! RELATED TO: " + b.toString(", or possibly ")); + } + + return fit2D(rarityMapped, x, z); + } + + private KList buildRarityMapped(KList values) { + KList rarityMapped = new KList<>(); + boolean flip = false; + int max = 1; + for (T value : values) { + if (value.getRarity() > max) { + max = value.getRarity(); + } + } + + max++; + for (T value : values) { + int count = max - value.getRarity(); + for (int j = 0; j < count; j++) { + flip = !flip; + if (flip) { + rarityMapped.add(value); + } else { + rarityMapped.add(0, value); + } + } + } + + return rarityMapped; + } + + public T fit2D(T[] values, double x, double z) { + if (values.length == 0) { + return null; + } + + if (values.length == 1) { + return values[0]; + } + + return values[fit2D(0, values.length - 1, x, z)]; + } + + public T fit2D(List values, double x, double z) { + if (values.isEmpty()) { + return null; + } + + if (values.size() == 1) { + return values.get(0); + } + + try { + return values.get(fit2D(0, values.size() - 1, x, z)); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + return values.get(0); + } + + public int fit2D(int min, int max, double x, double z) { + if (min == max) { + return min; + } + + double noise = noiseFast2D(x, z); + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + + public T fit(T[] v, double... dim) { + if (v.length == 0) { + return null; + } + + if (v.length == 1) { + return v[0]; + } + + return v[fit(0, v.length - 1, dim)]; + } + + public T fit(List v, double... dim) { + if (v.size() == 0) { + return null; + } + + if (v.size() == 1) { + return v.get(0); + } + + try { + return v.get(fit(0, v.size() - 1, dim)); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + + return v.get(0); + } + + public int fit(int min, int max, double... dim) { + if (min == max) { + return min; + } + + double noise = noise(dim); + + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + + public int fit(int min, int max, double x, double z) { + return fit2D(min, max, x, z); + } + + public int fit(int min, int max, double x, double y, double z) { + if (min == max) { + return min; + } + + double noise = noise(x, y, z); + + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + + public int fit(double min, double max, double... dim) { + if (min == max) { + return (int) Math.round(min); + } + + double noise = noise(dim); + + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + + public int fit(double min, double max, double x, double z) { + if (min == max) { + return (int) Math.round(min); + } + + double noise = noiseFast2D(x, z); + + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + + public int fit(double min, double max, double x, double y, double z) { + if (min == max) { + return (int) Math.round(min); + } + + double noise = noise(x, y, z); + + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + + public double fitDouble(double min, double max, double... dim) { + if (min == max) { + return min; + } + + double noise = noise(dim); + + return IrisInterpolation.lerp(min, max, noise); + } + + public double fitDouble(double min, double max, double x, double z) { + if (min == max) { + return min; + } + + double noise = noise(x, z); + + return IrisInterpolation.lerp(min, max, noise); + } + + public double fitDouble(double min, double max, double x, double y, double z) { + if (min == max) { + return min; + } + + double noise = noise(x, y, z); + + return IrisInterpolation.lerp(min, max, noise); + } + + private double getNoise(double... dim) { + ensureFastPathState(); + double scale = effectiveScale; + + if (fracture == null || noscale) { + return generator.noise( + (dim.length > 0 ? dim[0] : 0D) * scale, + (dim.length > 1 ? dim[1] : 0D) * scale, + (dim.length > 2 ? dim[2] : 0D) * scale) * opacity; + } + + double x = dim.length > 0 ? dim[0] : 0D; + double y = dim.length > 1 ? dim[1] : 0D; + double z = dim.length > 2 ? dim[2] : 0D; + + if (dim.length > 0) { + x += (fracture.noise(dim) - 0.5) * fscale; + } + + if (dim.length > 1) { + y += (fracture.noise(dim[1], dim[0]) - 0.5) * fscale; + } + + if (dim.length > 2) { + z += (fracture.noise(dim[2], dim[0], dim[1]) - 0.5) * fscale; + } + + return generator.noise(x * scale, y * scale, z * scale) * opacity; + } + + private double getNoise(double x) { + ensureFastPathState(); + double scl = effectiveScale; + + if (fracture == null || noscale) { + return generator.noise(x * scl, 0D, 0D) * opacity; + } + + double fx = x + ((fracture.noiseFast1D(x) - 0.5D) * fscale); + return generator.noise(fx * scl, 0D, 0D) * opacity; + } + + private double getNoise(double x, double z) { + ensureFastPathState(); + double scl = effectiveScale; + + if (fracture == null || noscale) { + return generator.noise(x * scl, z * scl, 0D) * opacity; + } + + double fx = x + ((fracture.noiseFast2D(x, z) - 0.5D) * fscale); + double fz = z + ((fracture.noiseFast2D(z, x) - 0.5D) * fscale); + return generator.noise(fx * scl, fz * scl, 0D) * opacity; + } + + private double getNoise(double x, double y, double z) { + ensureFastPathState(); + double scl = effectiveScale; + + if (fracture == null || noscale) { + return generator.noise(x * scl, y * scl, z * scl) * opacity; + } + + double fx = x + ((fracture.noiseFast3D(x, y, z) - 0.5D) * fscale); + double fy = y + ((fracture.noiseFast2D(y, x) - 0.5D) * fscale); + double fz = z + ((fracture.noiseFast3D(z, x, y) - 0.5D) * fscale); + return generator.noise(fx * scl, fy * scl, fz * scl) * opacity; + } + + public double invertNoise(double... dim) { + if (dim.length == 1) { + return noise(-dim[0]); + } else if (dim.length == 2) { + return noise(dim[1], dim[0]); + } else if (dim.length == 3) { + return noise(dim[1], dim[2], dim[0]); + } + + return noise(dim); + } + + public double noiseSym(double... dim) { + return (noise(dim) * 2) - 1; + } + + private static boolean isWholeCoordinate(double value) { + return value == Math.rint(value); + } + + private double applyPost(double n, double x) { + n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; + double m = 1; + + if (children != null) { + for (CNG i : children) { + double source = n; + double value = i.noise(x); + switch (injectorMode) { + case ADD -> { + n = source + value; + m += 1D; + } + case SRC_SUBTRACT -> { + n = source - value < 0D ? 0D : source - value; + m -= 1D; + } + case DST_SUBTRACT -> { + n = value - source < 0D ? 0D : source - value; + m -= 1D; + } + case MULTIPLY -> n = source * value; + case MAX -> n = Math.max(source, value); + case MIN -> n = Math.min(source, value); + case SRC_MOD -> n = source % value; + case SRC_POW -> n = Math.pow(source, value); + case DST_MOD -> n = value % source; + case DST_POW -> n = Math.pow(value, source); + case CUSTOM -> { + double[] combined = injector.combine(source, value); + n = combined[0]; + m += combined[1]; + } + } + } + } + + return ((n / m) - down + up) * patch; + } + + private double applyPost(double n, double x, double z) { + n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; + double m = 1; + + if (children != null) { + for (CNG i : children) { + double source = n; + double value = i.noise(x, z); + switch (injectorMode) { + case ADD -> { + n = source + value; + m += 1D; + } + case SRC_SUBTRACT -> { + n = source - value < 0D ? 0D : source - value; + m -= 1D; + } + case DST_SUBTRACT -> { + n = value - source < 0D ? 0D : source - value; + m -= 1D; + } + case MULTIPLY -> n = source * value; + case MAX -> n = Math.max(source, value); + case MIN -> n = Math.min(source, value); + case SRC_MOD -> n = source % value; + case SRC_POW -> n = Math.pow(source, value); + case DST_MOD -> n = value % source; + case DST_POW -> n = Math.pow(value, source); + case CUSTOM -> { + double[] combined = injector.combine(source, value); + n = combined[0]; + m += combined[1]; + } + } + } + } + + return ((n / m) - down + up) * patch; + } + + private double applyPost(double n, double x, double y, double z) { + n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; + double m = 1; + + if (children != null) { + for (CNG i : children) { + double source = n; + double value = i.noise(x, y, z); + switch (injectorMode) { + case ADD -> { + n = source + value; + m += 1D; + } + case SRC_SUBTRACT -> { + n = source - value < 0D ? 0D : source - value; + m -= 1D; + } + case DST_SUBTRACT -> { + n = value - source < 0D ? 0D : source - value; + m -= 1D; + } + case MULTIPLY -> n = source * value; + case MAX -> n = Math.max(source, value); + case MIN -> n = Math.min(source, value); + case SRC_MOD -> n = source % value; + case SRC_POW -> n = Math.pow(source, value); + case DST_MOD -> n = value % source; + case DST_POW -> n = Math.pow(value, source); + case CUSTOM -> { + double[] combined = injector.combine(source, value); + n = combined[0]; + m += combined[1]; + } + } + } + } + + return ((n / m) - down + up) * patch; + } + + public double noise(double... dim) { + if (dim.length == 1) { + return noise(dim[0]); + } + + if (dim.length == 2) { + return noise(dim[0], dim[1]); + } + + if (dim.length == 3) { + return noise(dim[0], dim[1], dim[2]); + } + + double n = getNoise(dim); + n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; + double m = 1; + if (children == null) { + return (n - down + up) * patch; + } + + for (CNG i : children) { + double source = n; + double value = i.noise(dim); + switch (injectorMode) { + case ADD -> { + n = source + value; + m += 1D; + } + case SRC_SUBTRACT -> { + n = source - value < 0D ? 0D : source - value; + m -= 1D; + } + case DST_SUBTRACT -> { + n = value - source < 0D ? 0D : source - value; + m -= 1D; + } + case MULTIPLY -> n = source * value; + case MAX -> n = Math.max(source, value); + case MIN -> n = Math.min(source, value); + case SRC_MOD -> n = source % value; + case SRC_POW -> n = Math.pow(source, value); + case DST_MOD -> n = value % source; + case DST_POW -> n = Math.pow(value, source); + case CUSTOM -> { + double[] combined = injector.combine(source, value); + n = combined[0]; + m += combined[1]; + } + } + } + + return ((n / m) - down + up) * patch; + } + + public double noise(double x) { + return applyPost(getNoise(x), x); + } + + public double noiseFast1D(double x) { + return applyPost(getNoise(x), x); + } + + public double noise(double x, double z) { + if (cache != null && isWholeCoordinate(x) && isWholeCoordinate(z)) { + return cache.get((int) x, (int) z); + } + + return applyPost(getNoise(x, z), x, z); + } + + public double noiseFast2D(double x, double z) { + if (cache != null && isWholeCoordinate(x) && isWholeCoordinate(z)) { + return cache.get((int) x, (int) z); + } + + if (isIdentityPostFastPath()) { + return getNoise(x, z); + } + + return applyPost(getNoise(x, z), x, z); + } + + private static final int COORD_CACHE_SIZE = 1 << 16; + private static final int COORD_CACHE_MASK = COORD_CACHE_SIZE - 1; + + private static final class CoordCache { + private final Object[] owner = new Object[COORD_CACHE_SIZE]; + private final long[] kx = new long[COORD_CACHE_SIZE]; + private final long[] kz = new long[COORD_CACHE_SIZE]; + private final double[] value = new double[COORD_CACHE_SIZE]; + } + + private static final ThreadLocal COORD_CACHE_2D = ThreadLocal.withInitial(CoordCache::new); + + private static int coordSlot(long a, long b, int salt) { + long h = (a * 0x9E3779B97F4A7C15L) ^ (b * 0xC2B2AE3D27D4EB4FL) ^ (salt * 0x165667B19E3779F9L); + h ^= (h >>> 32); + return (int) (h & COORD_CACHE_MASK); + } + + public double noiseFastSigned2D(double x, double z) { + long kx = Double.doubleToRawLongBits(x); + long kz = Double.doubleToRawLongBits(z); + CoordCache cc = COORD_CACHE_2D.get(); + int slot = coordSlot(kx, kz, coordCacheSalt()); + if (cc.owner[slot] == this && cc.kx[slot] == kx && cc.kz[slot] == kz) { + return cc.value[slot]; + } + double computed = computeNoiseFastSigned2D(x, z); + cc.owner[slot] = this; + cc.kx[slot] = kx; + cc.kz[slot] = kz; + cc.value[slot] = computed; + return computed; + } + + private double computeNoiseFastSigned2D(double x, double z) { + if (cache != null && isWholeCoordinate(x) && isWholeCoordinate(z)) { + return (cache.get((int) x, (int) z) * 2D) - 1D; + } + + if (hasIdentitySignedFastPath()) { + return getSignedNoise(x, z); + } + + return (noiseFast2D(x, z) * 2D) - 1D; + } + + public double noise(double x, double y, double z) { + return applyPost(getNoise(x, y, z), x, y, z); + } + + public double noiseFast3D(double x, double y, double z) { + if (isIdentityPostFastPath()) { + return getNoise(x, y, z); + } + + return applyPost(getNoise(x, y, z), x, y, z); + } + + private static final class CoordCache3D { + private final Object[] owner = new Object[COORD_CACHE_SIZE]; + private final long[] kx = new long[COORD_CACHE_SIZE]; + private final long[] ky = new long[COORD_CACHE_SIZE]; + private final long[] kz = new long[COORD_CACHE_SIZE]; + private final double[] value = new double[COORD_CACHE_SIZE]; + } + + private static final ThreadLocal COORD_CACHE_3D = ThreadLocal.withInitial(CoordCache3D::new); + + public double noiseFastSigned3D(double x, double y, double z) { + long kx = Double.doubleToRawLongBits(x); + long ky = Double.doubleToRawLongBits(y); + long kz = Double.doubleToRawLongBits(z); + CoordCache3D cc = COORD_CACHE_3D.get(); + int slot = coordSlot(kx ^ Long.rotateLeft(ky, 21), kz, coordCacheSalt()); + if (cc.owner[slot] == this && cc.kx[slot] == kx && cc.ky[slot] == ky && cc.kz[slot] == kz) { + return cc.value[slot]; + } + double computed = hasIdentitySignedFastPath() ? getSignedNoise(x, y, z) : (noiseFast3D(x, y, z) * 2D) - 1D; + cc.owner[slot] = this; + cc.kx[slot] = kx; + cc.ky[slot] = ky; + cc.kz[slot] = kz; + cc.value[slot] = computed; + return computed; + } + + public CNG pow(double power) { + this.power = power; + markFastPathStateDirty(); + return this; + } + + public CNG oct(int octaves) { + oct = octaves; + return this; + } + + public double getScale() { + return scale; + } + + public boolean isStatic() { + return generator != null && generator.isStatic(); + } + + private boolean isIdentityPostFastPath() { + ensureFastPathState(); + return identityPostFastPath; + } + + private boolean hasIdentitySignedFastPath() { + ensureFastPathState(); + return identitySignedFastPath; + } + + private void ensureFastPathState() { + if (fastPathStateDirty) { + refreshFastPathState(); + } + } + + private void markFastPathStateDirty() { + fastPathStateDirty = true; + } + + private int coordCacheSalt() { + if (coordCacheSalt == 0) { + coordCacheSalt = createCoordCacheSalt(); + } + + return coordCacheSalt; + } + + private int createCoordCacheSalt() { + int salt = System.identityHashCode(this); + return salt == 0 ? 1 : salt; + } + + private void refreshFastPathState() { + effectiveScale = noscale ? 1D : bakedScale * scale; + identityPostFastPath = power == 1D + && children == null + && fracture == null + && down == 0D + && up == 0D + && patch == 1D; + identitySignedFastPath = power == 1D + && children == null + && down == 0D + && up == 0D + && patch == 1D; + unityOpacity = opacity == 1D; + signedFractureScale = 0.5D * fscale; + fastPathStateDirty = false; + } + + private double signedWithOpacity(double signedNoise) { + if (unityOpacity) { + return signedNoise; + } + + return ((signedNoise + 1D) * opacity) - 1D; + } + + private double getSignedNoise(double x, double z) { + ensureFastPathState(); + double scl = effectiveScale; + NoiseGenerator localGenerator = generator; + CNG localFracture = fracture; + + if (localFracture == null || noscale) { + return signedWithOpacity(localGenerator.noiseSigned(x * scl, z * scl, 0D)); + } + + double fx = x + (localFracture.noiseFastSigned2D(x, z) * signedFractureScale); + double fz = z + (localFracture.noiseFastSigned2D(z, x) * signedFractureScale); + return signedWithOpacity(localGenerator.noiseSigned(fx * scl, fz * scl, 0D)); + } + + private double getSignedNoise(double x, double y, double z) { + ensureFastPathState(); + double scl = effectiveScale; + NoiseGenerator localGenerator = generator; + CNG localFracture = fracture; + + if (localFracture == null || noscale) { + return signedWithOpacity(localGenerator.noiseSigned(x * scl, y * scl, z * scl)); + } + + double fx = x + (localFracture.noiseFastSigned3D(x, y, z) * signedFractureScale); + double fy = y + (localFracture.noiseFastSigned2D(y, x) * signedFractureScale); + double fz = z + (localFracture.noiseFastSigned3D(z, x, y) * signedFractureScale); + return signedWithOpacity(localGenerator.noiseSigned(fx * scl, fy * scl, fz * scl)); + } + + private enum InjectorMode { + ADD, + SRC_SUBTRACT, + DST_SUBTRACT, + MULTIPLY, + MAX, + MIN, + SRC_MOD, + SRC_POW, + DST_MOD, + DST_POW, + CUSTOM + } +} diff --git a/core/src/main/java/com/volmit/iris/util/noise/CNGFactory.java b/core/src/main/java/art/arcane/iris/util/project/noise/CNGFactory.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/noise/CNGFactory.java rename to core/src/main/java/art/arcane/iris/util/project/noise/CNGFactory.java index ccb9f249e..058883445 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/CNGFactory.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CNGFactory.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.util.math.RNG; +import art.arcane.volmlib.util.math.RNG; @FunctionalInterface public interface CNGFactory { diff --git a/core/src/main/java/com/volmit/iris/util/noise/CellGenerator.java b/core/src/main/java/art/arcane/iris/util/project/noise/CellGenerator.java similarity index 97% rename from core/src/main/java/com/volmit/iris/util/noise/CellGenerator.java rename to core/src/main/java/art/arcane/iris/util/project/noise/CellGenerator.java index 9cd0fbc7e..a6a354e1b 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/CellGenerator.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CellGenerator.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.util.math.RNG; +import art.arcane.volmlib.util.math.RNG; import lombok.Getter; import lombok.Setter; diff --git a/core/src/main/java/com/volmit/iris/util/noise/CellHeightNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/CellHeightNoise.java similarity index 93% rename from core/src/main/java/com/volmit/iris/util/noise/CellHeightNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/CellHeightNoise.java index 62e3d1761..f478bfc90 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/CellHeightNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CellHeightNoise.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; public class CellHeightNoise implements NoiseGenerator { private final FastNoiseDouble n; diff --git a/core/src/main/java/com/volmit/iris/util/noise/CellularNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/CellularNoise.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/noise/CellularNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/CellularNoise.java index 740c1c4c4..2218229a5 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/CellularNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CellularNoise.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.util.math.RNG; +import art.arcane.volmlib.util.math.RNG; public class CellularNoise implements NoiseGenerator { private final FastNoise n; diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/CloverNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/CloverNoise.java new file mode 100644 index 000000000..170cf6d5c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CloverNoise.java @@ -0,0 +1,1140 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.noise; + +public class CloverNoise implements NoiseGenerator { + private final Noise2D n2; + private final Noise3D n3; + + public CloverNoise(long seed) { + n2 = new CloverNoise.Noise2D(seed); + n3 = new CloverNoise.Noise3D(seed); + } + + @Override + public double noise(double x) { + return n2.noise(x, 0); + } + + @Override + public double noise(double x, double z) { + return n2.noise(x, z); + } + + @Override + public double noise(double x, double y, double z) { + if (z == 0) { + return n2.noise(x, y); + } + + return n3.noise(x, y, z); + } + + /** + * Java implementation of 2D Clover Noise. See https://github.com/ValgoBoi/clover-noise + */ + public static class Noise2D { + private static final long HASH_A = 25214903917L; + private static final long HASH_C = 11L; + private static final long HASH_M = 0x1000000000000L; + private static final double POINT_SPREAD = 0.3; + private static final double CURL_DX = 0.0001; + private final long seed; + + /** + * Constructs a new 2D Clover Noise generator with a specific seed. + * + * @param seed The seed for the noise generator. + */ + public Noise2D(long seed) { + this.seed = seed; + } + + /** + * Constructs a new 2D Clover Noise generator, with the seed automatically set to the system time. + */ + public Noise2D() { + this(System.currentTimeMillis()); + } + + private long doHash(long input, long seed) { + input += seed; + + if (input < 0) { + input += HASH_M; + } + + input *= HASH_A; + input += HASH_C; + input %= HASH_M; + + return input; + } + + private long mix(long input) { + input = (input ^ (input >>> 33)) * 0xff51afd7ed558ccdL; + input = (input ^ (input >>> 33)) * 0xc4ceb9fe1a85ec53L; + return input ^ (input >>> 33); + } + + private double hash(Vector2 position) { + long x = (long) Math.floor(position.getX()); + long y = (long) Math.floor(position.getY()); + long hash = seed; + hash ^= mix(x + 0x9E3779B97F4A7C15L); + hash ^= mix(y + 0xC2B2AE3D27D4EB4FL); + hash = mix(hash); + hash &= (HASH_M - 1); + if (hash < 0) { + hash += HASH_M; + } + + return (double) hash / HASH_M; + } + + private Vector2 offset(Vector2 position) { + double hash = hash(position); + double scale = Math.floor(hash * 50 + 1) / 100; + double angle = hash * Math.PI * 100; + double offsetX = Math.sin(angle) * scale + 0.5; + double offsetY = Math.cos(angle) * scale + 0.5; + return new Vector2( + position.x + offsetX * (POINT_SPREAD * 2) + (0.5 - POINT_SPREAD), + position.y + offsetY * (POINT_SPREAD * 2) + (0.5 - POINT_SPREAD)); + } + + /** + * Generates 2D Clover Noise at a specific point. + * + * @param p The point to generate noise at. + * @return The value of noise, from 0 to 1. + */ + public double noise(Vector2 p) { + Vector2 p_floor = p.floor(); + + Vector2 c_11 = offset(p_floor); + Vector2 c_10 = offset(p_floor.add(0, -1)); + Vector2 c_12 = offset(p_floor.add(0, 1)); + Vector2 c_01 = offset(p_floor.add(-1, 0)); + Vector2 c_21 = offset(p_floor.add(1, 0)); + + Vector2 d_p_c11 = p.sub(c_11).yx(); + Vector2 m_p_c11 = d_p_c11.mult(c_11); + + double side_nx = m_p_c11.sub(d_p_c11.mult(c_01)).ymx(); + double side_px = m_p_c11.sub(d_p_c11.mult(c_21)).ymx(); + + Vector2 a, c, d; + + if (side_nx < 0 && p.x < c_11.x || side_px > 0 && p.x > c_11.x) { + double side_py = m_p_c11.sub(d_p_c11.mult(c_12)).ymx(); + + if (side_py > 0) { + a = c_12; + c = c_01; + d = new Vector2(-1, 1); + } else { + a = c_21; + c = c_12; + d = new Vector2(1, 1); + } + } else { + double side_ny = m_p_c11.sub(d_p_c11.mult(c_10)).ymx(); + + if (side_ny > 0) { + a = c_10; + c = c_21; + d = new Vector2(1, -1); + } else { + a = c_01; + c = c_10; + d = new Vector2(-1, -1); + } + } + + d = offset(p_floor.add(d)); + + Vector2 f = a; + Vector2 g = c; + Vector2 h = d; + + Vector2 ac = a.sub(c); + Vector2 bd = c_11.sub(d); + + if (ac.x * ac.x + ac.y * ac.y < bd.x * bd.x + bd.y * bd.y) { + Vector2 pa = p.sub(a); + + if (pa.x * ac.y - pa.y * ac.x > 0.) { + h = c_11; + } + } else { + Vector2 pb = p.sub(c_11); + + if (pb.x * bd.y - pb.y * bd.x > 0) { + f = c_11; + } else { + g = c_11; + } + } + + Vector2 bc_v0 = g.sub(f); + Vector2 bc_v1 = h.sub(f); + Vector2 bc_v2 = p.sub(f); + double den = 1 / (bc_v0.x * bc_v1.y - bc_v1.x * bc_v0.y); + double v = (bc_v2.x * bc_v1.y - bc_v1.x * bc_v2.y) * den; + double w = (bc_v0.x * bc_v2.y - bc_v2.x * bc_v0.y) * den; + double u = 1 - v - w; + + v = v * v * v; + w = w * w * w; + u = u * u * u; + double s = 1 / (u + v + w); + v *= s; + w *= s; + u *= s; + + double fv = hash(f); + double gv = hash(g); + double hv = hash(h); + + return u * fv + v * gv + w * hv; + } + + /** + * Generates 2D Clover Noise at a specific point. + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @return The value of noise, from 0 to 1. + */ + public double noise(double x, double y) { + return noise(new Vector2(x, y)); + } + + /** + * Generates fractal 2D Clover Noise at a specific point. + * + * @param p The point to generate noise at. + * @param iterations The number of iterations for the fractal noise. + * @return The value of noise, from 0 to 1. + */ + public double fractalNoise(Vector2 p, int iterations) { + double total = 0; + double weight = 1; + double div = 0; + + for (int i = 0; i < iterations; i++) { + total += noise(p.mult(1 / weight)) * weight; + div += weight; + + weight *= 0.4; + } + + return total / div; + } + + /** + * Generates fractal 2D Clover Noise at a specific point. + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @param iterations The number of iterations for the fractal noise. + * @return The value of noise, from 0 to 1. + */ + public double fractalNoise(double x, double y, int iterations) { + return fractalNoise(new Vector2(x, y), iterations); + } + + /** + * Generates curl 2D Clover Noise at a specific point. + * + * @param p The point to generate noise at. + * @return The value of curl noise, a normalized 2D vector. + */ + public Vector2 curlNoise(Vector2 p) { + double v = noise(p); + double x = noise(p.add(CURL_DX, 0)); + double y = noise(p.add(0, CURL_DX)); + + return new Vector2(v - x, v - y).normalize(); + } + + /** + * Generates curl 2D Clover Noise at a specific point. + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @return The value of curl noise, a normalized 2D vector. + */ + public Vector2 curlNoise(double x, double y) { + return curlNoise(new Vector2(x, y)); + } + + /** + * Generates fractal curl 2D Clover Noise at a specific point. + * + * @param p The point to generate noise at. + * @param iterations The number of iterations for the fractal noise. + * @return The value of curl noise, a normalized 2D vector. + */ + public Vector2 fractalCurlNoise(Vector2 p, int iterations) { + double v = fractalNoise(p, iterations); + double x = fractalNoise(p.add(CURL_DX, 0), iterations); + double y = fractalNoise(p.add(0, CURL_DX), iterations); + + return new Vector2(v - x, v - y).normalize(); + } + + /** + * Generates fractal curl 2D Clover Noise at a specific point. + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @param iterations The number of iterations for the fractal noise. + * @return The value of curl noise, a normalized 2D vector. + */ + public Vector2 fractalCurlNoise(double x, double y, int iterations) { + return fractalCurlNoise(new Vector2(x, y), iterations); + } + + /** + * Generates 2D Frost Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/frost.md + * + * @param p The point to generate noise at. + * @return The value of noise, from 0 to 1. + */ + public double frostNoise(Vector2 p) { + Vector2 curl_1 = fractalCurlNoise(p, 3).mult(fractalNoise(p, 2) * 0.4 + 0.3); + Vector2 p_1 = p.add(curl_1); + Vector2 curl_2 = fractalCurlNoise(p_1, 4).mult(fractalNoise(p_1, 3) * 0.1 + 0.05); + Vector2 p_2 = p_1.add(curl_2); + + return fractalNoise(p_2, 5) - fractalNoise(p_1, 3) * 0.5 + fractalNoise(p, 2) * 0.3; + } + + /** + * Generates 2D Frost Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/frost.md + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @return The value of noise, from 0 to 1. + */ + public double frostNoise(double x, double y) { + return frostNoise(new Vector2(x, y)); + } + + /** + * Generates 2D Marble Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/marble.md + * + * @param p The point to generate noise at. + * @return The value of noise, from 0 to 1. + */ + public double marbleNoise(Vector2 p) { + Vector2 p_2 = p.mult(0.6); + double d1 = Math.max(1 - Math.abs(fractalNoise(p_2.add(100), 4) - fractalNoise(p_2.add(200), 3)) * 3, 0); + d1 = d1 * d1 * d1; + d1 *= fractalNoise(p_2.add(300), 2) * 0.3; + + Vector2 curl_1 = fractalCurlNoise(p.add(400), 3).mult(fractalNoise(p.add(500), 2) * 0.05); + + Vector2 p_3 = p.mult(1.2); + double d2 = Math.max(1 - Math.abs(fractalNoise(p_3.add(curl_1).add(600), 4) - fractalNoise(p_3.add(curl_1).add(700), 3)) * 2, 0); + d2 = d2 * d2 * d2; + d2 *= fractalNoise(p_3.add(800), 2) * 0.5; + + double v = 1 - fractalNoise(p.add(900), 5); + v = 1 - v * v * v; + + return Math.min(Math.max(v - d1 - d2, 0), 1); + } + + /** + * Generates 2D Marble Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/marble.md + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @return The value of noise, from 0 to 1. + */ + public double marbleNoise(double x, double y) { + return marbleNoise(new Vector2(x, y)); + } + } + + public static class Noise3D { + private static final long HASH_A = 25214903917L; + private static final long HASH_C = 11L; + private static final long HASH_M = 0x1000000000000L; + private static final double POINT_SPREAD = 0.2; + private static final double CURL_DX = 0.0001; + private final long seed; + + public Noise3D(long seed) { + this.seed = seed; + } + + public Noise3D() { + this(System.currentTimeMillis()); + } + + private long doHash(long input, long seed) { + input += seed; + + if (input < 0) { + input += HASH_M; + } + + input *= HASH_A; + input += HASH_C; + input %= HASH_M; + + return input; + } + + private long mix(long input) { + input = (input ^ (input >>> 33)) * 0xff51afd7ed558ccdL; + input = (input ^ (input >>> 33)) * 0xc4ceb9fe1a85ec53L; + return input ^ (input >>> 33); + } + + private double hash(Vector3 position) { + return hash(position.x, position.y, position.z); + } + + private double hash(double positionX, double positionY, double positionZ) { + long x = (long) Math.floor(positionX); + long y = (long) Math.floor(positionY); + long z = (long) Math.floor(positionZ); + long hash = seed; + hash ^= mix(x + 0x9E3779B97F4A7C15L); + hash ^= mix(y + 0xC2B2AE3D27D4EB4FL); + hash ^= mix(z + 0x165667B19E3779F9L); + hash = mix(hash); + hash &= (HASH_M - 1); + if (hash < 0) { + hash += HASH_M; + } + + return (double) hash / HASH_M; + } + + private Vector3 offset(Vector3 position) { + return offset(position.x, position.y, position.z); + } + + private Vector3 offset(Vector3 position, double xOffset, double yOffset, double zOffset) { + return offset(position.x + xOffset, position.y + yOffset, position.z + zOffset); + } + + private Vector3 offset(double positionX, double positionY, double positionZ) { + double hash = hash(positionX, positionY, positionZ); + double theta = hash * Math.PI * 2000; + double height = (((Math.floor(hash * 1000) + 0.5) / 100) % 1 - 0.5) * Math.PI / 2; + double layer = Math.floor(hash * 10 + 1) / 10; + double cosHeight = Math.cos(height); + double offsetX = Math.sin(theta) * cosHeight * layer + 0.5; + double offsetY = Math.sin(height) * layer + 0.5; + double offsetZ = Math.cos(theta) * cosHeight * layer + 0.5; + return new Vector3( + positionX + (offsetX * (POINT_SPREAD * 2) + (0.5 - POINT_SPREAD)), + positionY + (offsetY * (POINT_SPREAD * 2) + (0.5 - POINT_SPREAD)), + positionZ + (offsetZ * (POINT_SPREAD * 2) + (0.5 - POINT_SPREAD))); + } + + private boolean boundary(int permutation, Vector3 p, Vector3 c_00, Vector3 c_10, Vector3 c_20, Vector3 c_01, Vector3 c_11, Vector3 c_21, Vector3 c_02, Vector3 c_12, Vector3 c_22) { + double px = permutedX(p, permutation); + double py = permutedY(p, permutation); + double pz = permutedZ(p, permutation); + double c11x = permutedX(c_11, permutation); + double c11y = permutedY(c_11, permutation); + double deltaX = py - c11y; + double deltaY = px - c11x; + double multipliedX = deltaX * c11x; + double multipliedY = deltaY * c11y; + + double side_nx = side(multipliedX, multipliedY, deltaX, deltaY, c_01, permutation); + double side_px = side(multipliedX, multipliedY, deltaX, deltaY, c_21, permutation); + + Vector3 a, b, c, d; + + if (side_nx < 0 && px < c11x || side_px > 0 && px >= c11x) { + double side_py = side(multipliedX, multipliedY, deltaX, deltaY, c_12, permutation); + + if (side_py > 0.) { + a = c_01; + b = c_02; + c = c_12; + d = c_11; + } else { + a = c_11; + b = c_12; + c = c_22; + d = c_21; + } + } else { + double side_ny = side(multipliedX, multipliedY, deltaX, deltaY, c_10, permutation); + + if (side_ny > 0.) { + a = c_10; + b = c_11; + c = c_21; + d = c_20; + } else { + a = c_00; + b = c_01; + c = c_11; + d = c_10; + } + } + + Vector3 f = a; + Vector3 g = c; + Vector3 h = d; + + double acx = permutedX(a, permutation) - permutedX(c, permutation); + double acy = permutedY(a, permutation) - permutedY(c, permutation); + double pax = px - permutedX(a, permutation); + double pay = py - permutedY(a, permutation); + + if (pax * acy - pay * acx > 0) { + h = b; + } + + double fx = permutedX(f, permutation); + double fy = permutedY(f, permutation); + double v0x = permutedX(g, permutation) - fx; + double v0y = permutedY(g, permutation) - fy; + double v1x = permutedX(h, permutation) - fx; + double v1y = permutedY(h, permutation) - fy; + double v2x = px - fx; + double v2y = py - fy; + double den = 1 / (v0x * v1y - v1x * v0y); + double v = (v2x * v1y - v1x * v2y) * den; + double w = (v0x * v2y - v2x * v0y) * den; + double u = 1 - v - w; + + return pz < u * permutedZ(f, permutation) + + v * permutedZ(g, permutation) + + w * permutedZ(h, permutation); + } + + private static double side(double multipliedX, double multipliedY, double deltaX, double deltaY, Vector3 candidate, int permutation) { + double x = multipliedX - deltaX * permutedX(candidate, permutation); + double y = multipliedY - deltaY * permutedY(candidate, permutation); + return y - x; + } + + private static double permutedX(Vector3 vector, int permutation) { + return switch (permutation) { + case 1 -> vector.y; + default -> vector.x; + }; + } + + private static double permutedY(Vector3 vector, int permutation) { + return permutation == 0 ? vector.y : vector.z; + } + + private static double permutedZ(Vector3 vector, int permutation) { + return switch (permutation) { + case 1 -> vector.x; + case 2 -> vector.y; + default -> vector.z; + }; + } + + /** + * Generates 3D Clover Noise at a specific point. + * + * @param p The point to generate noise at. + * @return The value of noise, from 0 to 1. + */ + public double noise(Vector3 p) { + Vector3 p_floor = p.floor(); + + Vector3 c_111 = offset(p_floor); + Vector3 c_100 = offset(p_floor, 0, -1, -1); + Vector3 c_010 = offset(p_floor, -1, 0, -1); + Vector3 c_110 = offset(p_floor, 0, 0, -1); + Vector3 c_210 = offset(p_floor, 1, 0, -1); + Vector3 c_120 = offset(p_floor, 0, 1, -1); + Vector3 c_001 = offset(p_floor, -1, -1, 0); + Vector3 c_101 = offset(p_floor, 0, -1, 0); + Vector3 c_201 = offset(p_floor, 1, -1, 0); + Vector3 c_011 = offset(p_floor, -1, 0, 0); + Vector3 c_211 = offset(p_floor, 1, 0, 0); + Vector3 c_021 = offset(p_floor, -1, 1, 0); + Vector3 c_121 = offset(p_floor, 0, 1, 0); + Vector3 c_221 = offset(p_floor, 1, 1, 0); + Vector3 c_102 = offset(p_floor, 0, -1, 1); + Vector3 c_012 = offset(p_floor, -1, 0, 1); + Vector3 c_112 = offset(p_floor, 0, 0, 1); + Vector3 c_212 = offset(p_floor, 1, 0, 1); + Vector3 c_122 = offset(p_floor, 0, 1, 1); + + boolean x_bound = boundary(1, p, c_100, c_110, c_120, c_101, c_111, c_121, c_102, c_112, c_122); + boolean y_bound = boundary(2, p, c_010, c_110, c_210, c_011, c_111, c_211, c_012, c_112, c_212); + boolean z_bound = boundary(0, p, c_001, c_101, c_201, c_011, c_111, c_211, c_021, c_121, c_221); + + Vector3 a, b, c, d, e, f, g, h; + + if (x_bound) { + if (y_bound) { + if (z_bound) { + a = offset(p_floor, -1, -1, -1); + b = c_001; + c = c_010; + d = c_011; + e = c_100; + f = c_101; + g = c_110; + h = c_111; + } else { + a = c_001; + b = offset(p_floor, -1, -1, 1); + c = c_011; + d = c_012; + e = c_101; + f = c_102; + g = c_111; + h = c_112; + } + } else { + if (z_bound) { + a = c_010; + b = c_011; + c = offset(p_floor, -1, 1, -1); + d = c_021; + e = c_110; + f = c_111; + g = c_120; + h = c_121; + } else { + a = c_011; + b = c_012; + c = c_021; + d = offset(p_floor, -1, 1, 1); + e = c_111; + f = c_112; + g = c_121; + h = c_122; + } + } + } else { + if (y_bound) { + if (z_bound) { + a = c_100; + b = c_101; + c = c_110; + d = c_111; + e = offset(p_floor, 1, -1, -1); + f = c_201; + g = c_210; + h = c_211; + } else { + a = c_101; + b = c_102; + c = c_111; + d = c_112; + e = c_201; + f = offset(p_floor, 1, -1, 1); + g = c_211; + h = c_212; + } + } else { + if (z_bound) { + a = c_110; + b = c_111; + c = c_120; + d = c_121; + e = c_210; + f = c_211; + g = offset(p_floor, 1, 1, -1); + h = c_221; + } else { + a = c_111; + b = c_112; + c = c_121; + d = c_122; + e = c_211; + f = c_212; + g = c_221; + h = offset(p_floor, 1, 1, 1); + } + } + } + + double plane_b = dotDifferenceCrossDifferences(p, a, a, h, b, h); + double plane_c = dotDifferenceCrossDifferences(p, a, a, h, c, h); + double plane_d = dotDifferenceCrossDifferences(p, a, a, h, d, h); + double plane_e = dotDifferenceCrossDifferences(p, a, a, h, e, h); + double plane_f = dotDifferenceCrossDifferences(p, a, a, h, f, h); + double plane_g = dotDifferenceCrossDifferences(p, a, a, h, g, h); + + Vector3 i, j, k, l; + + i = a; + j = h; + + if (plane_b > 0 && plane_d <= 0) { + k = b; + l = d; + } else if (plane_d > 0 && plane_c <= 0) { + k = d; + l = c; + } else if (plane_c > 0 && plane_g <= 0) { + k = c; + l = g; + } else if (plane_g > 0 && plane_e <= 0) { + k = g; + l = e; + } else if (plane_e > 0 && plane_f <= 0) { + k = e; + l = f; + } else { + k = f; + l = b; + } + + double bc_va6 = dotDifferenceCrossDifferences(p, j, l, j, k, j); + double bc_vb6 = dotDifferenceCrossDifferences(p, i, k, i, l, i); + double bc_vc6 = dotDifferenceCrossDifferences(p, i, l, i, j, i); + double bc_vd6 = dotDifferenceCrossDifferences(p, i, j, i, k, i); + double bc_v6 = 1 / dotDifferenceCrossDifferences(j, i, k, i, l, i); + + double v = bc_va6 * bc_v6; + double w = bc_vb6 * bc_v6; + double t = bc_vc6 * bc_v6; + double u = bc_vd6 * bc_v6; + + double fiu = u * u * u * (1 - v * w * t); + double fiv = v * v * v * (1 - u * w * t); + double fiw = w * w * w * (1 - v * u * t); + double fit = t * t * t * (1 - v * w * u); + double s = fiu + fiv + fiw + fit; + fiu /= s; + fiv /= s; + fiw /= s; + fit /= s; + + double iv = hash(i); + double jv = hash(j); + double kv = hash(k); + double lv = hash(l); + + return fiv * iv + fiw * jv + fit * kv + fiu * lv; + } + + private static double dotDifferenceCrossDifferences( + Vector3 dotValue, + Vector3 dotOrigin, + Vector3 crossLeftValue, + Vector3 crossLeftOrigin, + Vector3 crossRightValue, + Vector3 crossRightOrigin + ) { + double dotX = dotValue.x - dotOrigin.x; + double dotY = dotValue.y - dotOrigin.y; + double dotZ = dotValue.z - dotOrigin.z; + double leftX = crossLeftValue.x - crossLeftOrigin.x; + double leftY = crossLeftValue.y - crossLeftOrigin.y; + double leftZ = crossLeftValue.z - crossLeftOrigin.z; + double rightX = crossRightValue.x - crossRightOrigin.x; + double rightY = crossRightValue.y - crossRightOrigin.y; + double rightZ = crossRightValue.z - crossRightOrigin.z; + double crossX = leftY * rightZ - leftZ * rightY; + double crossY = leftZ * rightX - leftX * rightZ; + double crossZ = leftX * rightY - leftY * rightX; + return dotX * crossX + dotY * crossY + dotZ * crossZ; + } + + /** + * Generates 3D Clover Noise at a specific point. + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @param z The z coordinate of the point. + * @return The value of noise, from 0 to 1. + */ + public double noise(double x, double y, double z) { + return noise(new Vector3(x, y, z)); + } + + /** + * Generates fractal 3D Clover Noise at a specific point. + * + * @param p The point to generate noise at. + * @param iterations The number of iterations for the fractal noise. + * @return The value of noise, from 0 to 1. + */ + public double fractalNoise(Vector3 p, int iterations) { + double total = 0; + double weight = 1; + double div = 0; + + for (int i = 0; i < iterations; i++) { + total += noise(p.mult(1 / weight)) * weight; + div += weight; + + weight *= 0.4; + } + + return total / div; + } + + /** + * Generates fractal 3D Clover Noise at a specific point. + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @param z The z coordinate of the point. + * @param iterations The number of iterations for the fractal noise. + * @return The value of noise, from 0 to 1. + */ + public double fractalNoise(double x, double y, double z, int iterations) { + return fractalNoise(new Vector3(x, y, z), iterations); + } + + /** + * Generates curl 3D Clover Noise at a specific point. + * + * @param p The point to generate noise at. + * @return The value of curl noise, a normalized 3D vector. + */ + public Vector3 curlNoise(Vector3 p) { + double v = noise(p); + double x = noise(p.add(CURL_DX, 0, 0)); + double y = noise(p.add(0, CURL_DX, 0)); + double z = noise(p.add(0, 0, CURL_DX)); + + return new Vector3(v - x, v - y, v - z).normalize(); + } + + /** + * Generates curl 3D Clover Noise at a specific point. + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @param z The z coordinate of the point. + * @return The value of curl noise, a normalized 3D vector. + */ + public Vector3 curlNoise(double x, double y, double z) { + return curlNoise(new Vector3(x, y, z)); + } + + /** + * Generates fractal curl 3D Clover Noise at a specific point. + * + * @param p The point to generate noise at. + * @param iterations The number of iterations for the fractal noise. + * @return The value of curl noise, a normalized 3D vector. + */ + public Vector3 fractalCurlNoise(Vector3 p, int iterations) { + double v = fractalNoise(p, iterations); + double x = fractalNoise(p.add(CURL_DX, 0, 0), iterations); + double y = fractalNoise(p.add(0, CURL_DX, 0), iterations); + double z = fractalNoise(p.add(0, 0, CURL_DX), iterations); + + return new Vector3(v - x, v - y, v - z).normalize(); + } + + /** + * Generates fractal curl 3D Clover Noise at a specific point. + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @param z The z coordinate of the point. + * @param iterations The number of iterations for the fractal noise. + * @return The value of curl noise, a normalized 3D vector. + */ + public Vector3 fractalCurlNoise(double x, double y, double z, int iterations) { + return fractalCurlNoise(new Vector3(x, y, z), iterations); + } + + /** + * Generates 3D Frost Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/frost.md + * + * @param p The point to generate noise at. + * @return The value of noise, from 0 to 1. + */ + public double frostNoise(Vector3 p) { + Vector3 curl_1 = fractalCurlNoise(p, 3).mult(fractalNoise(p, 2) * 0.4 + 0.3); + Vector3 p_1 = p.add(curl_1); + Vector3 curl_2 = fractalCurlNoise(p_1, 4).mult(fractalNoise(p_1, 3) * 0.1 + 0.05); + Vector3 p_2 = p_1.add(curl_2); + + return fractalNoise(p_2, 5) - fractalNoise(p_1, 3) * 0.5 + fractalNoise(p, 2) * 0.3; + } + + /** + * Generates 3D Frost Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/frost.md + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @param z The z coordinate of the point. + * @return The value of noise, from 0 to 1. + */ + public double frostNoise(double x, double y, double z) { + return frostNoise(new Vector3(x, y, z)); + } + + /** + * Generates 3D Marble Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/marble.md + * + * @param p The point to generate noise at. + * @return The value of noise, from 0 to 1. + */ + public double marbleNoise(Vector3 p) { + Vector3 p_2 = p.mult(0.6); + double d1 = Math.max(1 - Math.abs(fractalNoise(p_2.add(100), 4) - fractalNoise(p_2.add(200), 3)) * 3, 0); + d1 = d1 * d1 * d1; + d1 *= fractalNoise(p_2.add(300), 2) * 0.3; + + Vector3 curl_1 = fractalCurlNoise(p.add(400), 3).mult(fractalNoise(p.add(500), 2) * 0.05); + + Vector3 p_3 = p.mult(1.2); + double d2 = Math.max(1 - Math.abs(fractalNoise(p_3.add(curl_1).add(600), 4) - fractalNoise(p_3.add(curl_1).add(700), 3)) * 2, 0); + d2 = d2 * d2 * d2; + d2 *= fractalNoise(p_3.add(800), 2) * 0.5; + + double v = 1 - fractalNoise(p.add(900), 5); + v = 1 - v * v * v; + + return Math.min(Math.max(v - d1 - d2, 0), 1); + } + + /** + * Generates 3D Marble Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/marble.md + * + * @param x The x coordinate of the point. + * @param y The y coordinate of the point. + * @param z The z coordinate of the point. + * @return The value of noise, from 0 to 1. + */ + public double marbleNoise(double x, double y, double z) { + return marbleNoise(new Vector3(x, y, z)); + } + } + + /** + * A 2-dimensional Vector, with methods relevant to the Clover Noise algorithm. + */ + public static class Vector2 { + private double x, y; + + public Vector2(double x, double y) { + this.x = x; + this.y = y; + } + + public Vector2() { + this(0, 0); + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double ymx() { + return y - x; + } + + public Vector2 yx() { + return new Vector2(y, x); + } + + public Vector2 negate() { + return new Vector2(-x, -y); + } + + public Vector2 floor() { + return new Vector2(Math.floor(x), Math.floor(y)); + } + + public Vector2 normalize() { + double len2 = x * x + y * y; + double len = Math.sqrt(len2); + + return new Vector2(x / len, y / len); + } + + public Vector2 add(Vector2 a) { + return new Vector2(x + a.x, y + a.y); + } + + public Vector2 add(double xa, double ya) { + return new Vector2(x + xa, y + ya); + } + + public Vector2 add(double a) { + return add(a, a); + } + + public Vector2 sub(Vector2 s) { + return new Vector2(x - s.x, y - s.y); + } + + public Vector2 sub(double xs, double ys) { + return new Vector2(x - xs, y - ys); + } + + public Vector2 sub(double s) { + return sub(s, s); + } + + public Vector2 mult(Vector2 m) { + return new Vector2(x * m.x, y * m.y); + } + + public Vector2 mult(double xm, double ym) { + return new Vector2(x * xm, y * ym); + } + + public Vector2 mult(double m) { + return mult(m, m); + } + } + + /** + * A 3-dimensional Vector, with methods relevant to the Clover Noise algorithm. + */ + public static class Vector3 { + double x, y, z; + + public Vector3(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector3() { + this(0, 0, 0); + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getZ() { + return z; + } + + public void setZ(double z) { + this.z = z; + } + + public Vector2 xy() { + return new Vector2(x, y); + } + + public Vector2 yx() { + return new Vector2(y, x); + } + + public Vector3 yzx() { + return new Vector3(y, z, x); + } + + public Vector3 xzy() { + return new Vector3(x, z, y); + } + + public double xpypz() { + return x + y + z; + } + + public Vector3 negate() { + return new Vector3(-x, -y, -z); + } + + public Vector3 floor() { + return new Vector3(Math.floor(x), Math.floor(y), Math.floor(z)); + } + + public Vector3 normalize() { + double len2 = x * x + y * y + z * z; + double len = Math.sqrt(len2); + + return new Vector3(x / len, y / len, z / len); + } + + public Vector3 add(Vector3 a) { + return new Vector3(x + a.x, y + a.y, z + a.z); + } + + public Vector3 add(double xa, double ya, double za) { + return new Vector3(x + xa, y + ya, z + za); + } + + public Vector3 add(double a) { + return add(a, a, a); + } + + public Vector3 sub(Vector3 s) { + return new Vector3(x - s.x, y - s.y, z - s.z); + } + + public Vector3 sub(double xs, double ys, double zs) { + return new Vector3(x - xs, y - ys, z - zs); + } + + public Vector3 sub(double s) { + return sub(s, s, s); + } + + public Vector3 mult(Vector3 m) { + return new Vector3(x * m.x, y * m.y, z * m.z); + } + + public Vector3 mult(double mx, double my, double mz) { + return new Vector3(x * mx, y * my, z * mz); + } + + public Vector3 mult(double m) { + return mult(m, m, m); + } + + public Vector3 cross(Vector3 c) { + return new Vector3(y * c.z - z * c.y, z * c.x - x * c.z, x * c.y - y * c.x); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/util/noise/CubicNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/CubicNoise.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/noise/CubicNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/CubicNoise.java index 485ea08b8..eef6afae9 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/CubicNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CubicNoise.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.util.math.RNG; +import art.arcane.volmlib.util.math.RNG; public class CubicNoise implements NoiseGenerator { private final FastNoiseDouble n; diff --git a/core/src/main/java/com/volmit/iris/util/noise/ExpressionNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/ExpressionNoise.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/noise/ExpressionNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/ExpressionNoise.java index 7052d9dd2..3327bf871 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/ExpressionNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/ExpressionNoise.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.engine.object.IrisExpression; -import com.volmit.iris.util.math.RNG; +import art.arcane.iris.engine.object.IrisExpression; +import art.arcane.volmlib.util.math.RNG; public class ExpressionNoise implements NoiseGenerator { private final RNG rng; diff --git a/core/src/main/java/com/volmit/iris/util/noise/FastNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/FastNoise.java similarity index 99% rename from core/src/main/java/com/volmit/iris/util/noise/FastNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/FastNoise.java index 4538c772c..3e97fe38a 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/FastNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/FastNoise.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.math.Vector2f; -import com.volmit.iris.util.math.Vector3f; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.noise; +import art.arcane.volmlib.util.math.Vector2f; +import art.arcane.volmlib.util.math.Vector3f; +import art.arcane.iris.util.project.stream.ProceduralStream; public class FastNoise { private static final Float2[] GRAD_2D = {new Float2(-1, -1), new Float2(1, -1), new Float2(-1, 1), new Float2(1, 1), new Float2(0, -1), new Float2(-1, 0), new Float2(0, 1), new Float2(1, 0), }; diff --git a/core/src/main/java/com/volmit/iris/util/noise/FastNoiseDouble.java b/core/src/main/java/art/arcane/iris/util/project/noise/FastNoiseDouble.java similarity index 88% rename from core/src/main/java/com/volmit/iris/util/noise/FastNoiseDouble.java rename to core/src/main/java/art/arcane/iris/util/project/noise/FastNoiseDouble.java index 6bc67528c..0a0d53c56 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/FastNoiseDouble.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/FastNoiseDouble.java @@ -15,20 +15,28 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.util.math.Vector2f; -import com.volmit.iris.util.math.Vector3f; +import art.arcane.volmlib.util.math.Vector2f; +import art.arcane.volmlib.util.math.Vector3f; public class FastNoiseDouble { - private static final Double2[] GRAD_2D = {new Double2(-1, -1), new Double2(1, -1), new Double2(-1, 1), new Double2(1, 1), new Double2(0, -1), new Double2(-1, 0), new Double2(0, 1), new Double2(1, 0), - }; - private static final Double3[] GRAD_3D = {new Double3(1, 1, 0), new Double3(-1, 1, 0), new Double3(1, -1, 0), new Double3(-1, -1, 0), new Double3(1, 0, 1), new Double3(-1, 0, 1), new Double3(1, 0, -1), new Double3(-1, 0, -1), new Double3(0, 1, 1), new Double3(0, -1, 1), new Double3(0, 1, -1), new Double3(0, -1, -1), new Double3(1, 1, 0), new Double3(0, -1, 1), new Double3(-1, 1, 0), new Double3(0, -1, -1), + private static final double[] GRAD_2D = {-1D, -1D, 1D, -1D, -1D, 1D, 1D, 1D, 0D, -1D, -1D, 0D, 0D, 1D, 1D, 0D}; + private static final double[] GRAD_3D = { + 1D, 1D, 0D, -1D, 1D, 0D, 1D, -1D, 0D, -1D, -1D, 0D, + 1D, 0D, 1D, -1D, 0D, 1D, 1D, 0D, -1D, -1D, 0D, -1D, + 0D, 1D, 1D, 0D, -1D, 1D, 0D, 1D, -1D, 0D, -1D, -1D, + 1D, 1D, 0D, 0D, -1D, 1D, -1D, 1D, 0D, 0D, -1D, -1D }; private static final Double2[] CELL_2D = {new Double2(-0.4313539279f, 0.1281943404f), new Double2(-0.1733316799f, 0.415278375f), new Double2(-0.2821957395f, -0.3505218461f), new Double2(-0.2806473808f, 0.3517627718f), new Double2(0.3125508975f, -0.3237467165f), new Double2(0.3383018443f, -0.2967353402f), new Double2(-0.4393982022f, -0.09710417025f), new Double2(-0.4460443703f, -0.05953502905f), new Double2(-0.302223039f, 0.3334085102f), new Double2(-0.212681052f, -0.3965687458f), new Double2(-0.2991156529f, 0.3361990872f), new Double2(0.2293323691f, 0.3871778202f), new Double2(0.4475439151f, -0.04695150755f), new Double2(0.1777518f, 0.41340573f), new Double2(0.1688522499f, -0.4171197882f), new Double2(-0.0976597166f, 0.4392750616f), new Double2(0.08450188373f, 0.4419948321f), new Double2(-0.4098760448f, -0.1857461384f), new Double2(0.3476585782f, -0.2857157906f), new Double2(-0.3350670039f, -0.30038326f), new Double2(0.2298190031f, -0.3868891648f), new Double2(-0.01069924099f, 0.449872789f), new Double2(-0.4460141246f, -0.05976119672f), new Double2(0.3650293864f, 0.2631606867f), new Double2(-0.349479423f, 0.2834856838f), new Double2(-0.4122720642f, 0.1803655873f), new Double2(-0.267327811f, 0.3619887311f), new Double2(0.322124041f, -0.3142230135f), new Double2(0.2880445931f, -0.3457315612f), new Double2(0.3892170926f, -0.2258540565f), new Double2(0.4492085018f, -0.02667811596f), new Double2(-0.4497724772f, 0.01430799601f), new Double2(0.1278175387f, -0.4314657307f), new Double2(-0.03572100503f, 0.4485799926f), new Double2(-0.4297407068f, -0.1335025276f), new Double2(-0.3217817723f, 0.3145735065f), new Double2(-0.3057158873f, 0.3302087162f), new Double2(-0.414503978f, 0.1751754899f), new Double2(-0.3738139881f, 0.2505256519f), new Double2(0.2236891408f, -0.3904653228f), new Double2(0.002967775577f, -0.4499902136f), new Double2(0.1747128327f, -0.4146991995f), new Double2(-0.4423772489f, -0.08247647938f), new Double2(-0.2763960987f, -0.355112935f), new Double2(-0.4019385906f, -0.2023496216f), new Double2(0.3871414161f, -0.2293938184f), new Double2(-0.430008727f, 0.1326367019f), new Double2(-0.03037574274f, -0.4489736231f), new Double2(-0.3486181573f, 0.2845441624f), new Double2(0.04553517144f, -0.4476902368f), new Double2(-0.0375802926f, 0.4484280562f), new Double2(0.3266408905f, 0.3095250049f), new Double2(0.06540017593f, -0.4452222108f), new Double2(0.03409025829f, 0.448706869f), new Double2(-0.4449193635f, 0.06742966669f), new Double2(-0.4255936157f, -0.1461850686f), new Double2(0.449917292f, 0.008627302568f), new Double2(0.05242606404f, 0.4469356864f), new Double2(-0.4495305179f, -0.02055026661f), new Double2(-0.1204775703f, 0.4335725488f), new Double2(-0.341986385f, -0.2924813028f), new Double2(0.3865320182f, 0.2304191809f), new Double2(0.04506097811f, -0.447738214f), new Double2(-0.06283465979f, 0.4455915232f), new Double2(0.3932600341f, -0.2187385324f), new Double2(0.4472261803f, -0.04988730975f), new Double2(0.3753571011f, -0.2482076684f), new Double2(-0.273662295f, 0.357223947f), new Double2(0.1700461538f, 0.4166344988f), new Double2(0.4102692229f, 0.1848760794f), new Double2(0.323227187f, -0.3130881435f), new Double2(-0.2882310238f, -0.3455761521f), new Double2(0.2050972664f, 0.4005435199f), new Double2(0.4414085979f, -0.08751256895f), new Double2(-0.1684700334f, 0.4172743077f), new Double2(-0.003978032396f, 0.4499824166f), new Double2(-0.2055133639f, 0.4003301853f), new Double2(-0.006095674897f, -0.4499587123f), new Double2(-0.1196228124f, -0.4338091548f), new Double2(0.3901528491f, -0.2242337048f), new Double2(0.01723531752f, 0.4496698165f), new Double2(-0.3015070339f, 0.3340561458f), new Double2(-0.01514262423f, -0.4497451511f), new Double2(-0.4142574071f, -0.1757577897f), new Double2(-0.1916377265f, -0.4071547394f), new Double2(0.3749248747f, 0.2488600778f), new Double2(-0.2237774255f, 0.3904147331f), new Double2(-0.4166343106f, -0.1700466149f), new Double2(0.3619171625f, 0.267424695f), new Double2(0.1891126846f, -0.4083336779f), new Double2(-0.3127425077f, 0.323561623f), new Double2(-0.3281807787f, 0.307891826f), new Double2(-0.2294806661f, 0.3870899429f), new Double2(-0.3445266136f, 0.2894847362f), new Double2(-0.4167095422f, -0.1698621719f), new Double2(-0.257890321f, -0.3687717212f), new Double2(-0.3612037825f, 0.2683874578f), new Double2(0.2267996491f, 0.3886668486f), new Double2(0.207157062f, 0.3994821043f), new Double2(0.08355176718f, -0.4421754202f), new Double2(-0.4312233307f, 0.1286329626f), new Double2(0.3257055497f, 0.3105090899f), new Double2(0.177701095f, -0.4134275279f), new Double2(-0.445182522f, 0.06566979625f), new Double2(0.3955143435f, 0.2146355146f), new Double2(-0.4264613988f, 0.1436338239f), new Double2(-0.3793799665f, -0.2420141339f), new Double2(0.04617599081f, -0.4476245948f), new Double2(-0.371405428f, -0.2540826796f), new Double2(0.2563570295f, -0.3698392535f), new Double2(0.03476646309f, 0.4486549822f), new Double2(-0.3065454405f, 0.3294387544f), new Double2(-0.2256979823f, 0.3893076172f), new Double2(0.4116448463f, -0.1817925206f), new Double2(-0.2907745828f, -0.3434387019f), new Double2(0.2842278468f, -0.348876097f), new Double2(0.3114589359f, -0.3247973695f), new Double2(0.4464155859f, -0.0566844308f), new Double2(-0.3037334033f, -0.3320331606f), new Double2(0.4079607166f, 0.1899159123f), new Double2(-0.3486948919f, -0.2844501228f), new Double2(0.3264821436f, 0.3096924441f), new Double2(0.3211142406f, 0.3152548881f), new Double2(0.01183382662f, 0.4498443737f), new Double2(0.4333844092f, 0.1211526057f), new Double2(0.3118668416f, 0.324405723f), new Double2(-0.272753471f, 0.3579183483f), new Double2(-0.422228622f, -0.1556373694f), new Double2(-0.1009700099f, -0.4385260051f), new Double2(-0.2741171231f, -0.3568750521f), new Double2(-0.1465125133f, 0.4254810025f), new Double2(0.2302279044f, -0.3866459777f), new Double2(-0.3699435608f, 0.2562064828f), new Double2(0.105700352f, -0.4374099171f), new Double2(-0.2646713633f, 0.3639355292f), new Double2(0.3521828122f, 0.2801200935f), new Double2(-0.1864187807f, -0.4095705534f), new Double2(0.1994492955f, -0.4033856449f), new Double2(0.3937065066f, 0.2179339044f), new Double2(-0.3226158377f, 0.3137180602f), new Double2(0.3796235338f, 0.2416318948f), new Double2(0.1482921929f, 0.4248640083f), new Double2(-0.407400394f, 0.1911149365f), new Double2(0.4212853031f, 0.1581729856f), new Double2(-0.2621297173f, 0.3657704353f), new Double2(-0.2536986953f, -0.3716678248f), new Double2(-0.2100236383f, 0.3979825013f), new Double2(0.3624152444f, 0.2667493029f), new Double2(-0.3645038479f, -0.2638881295f), new Double2(0.2318486784f, 0.3856762766f), new Double2(-0.3260457004f, 0.3101519002f), new Double2(-0.2130045332f, -0.3963950918f), new Double2(0.3814998766f, -0.2386584257f), new Double2(-0.342977305f, 0.2913186713f), new Double2(-0.4355865605f, 0.1129794154f), new Double2(-0.2104679605f, 0.3977477059f), new Double2(0.3348364681f, -0.3006402163f), new Double2(0.3430468811f, 0.2912367377f), new Double2(-0.2291836801f, -0.3872658529f), new Double2(0.2547707298f, -0.3709337882f), new Double2(0.4236174945f, -0.151816397f), new Double2(-0.15387742f, 0.4228731957f), new Double2(-0.4407449312f, 0.09079595574f), new Double2(-0.06805276192f, -0.444824484f), new Double2(0.4453517192f, -0.06451237284f), new Double2(0.2562464609f, -0.3699158705f), new Double2(0.3278198355f, -0.3082761026f), new Double2(-0.4122774207f, -0.1803533432f), new Double2(0.3354090914f, -0.3000012356f), new Double2(0.446632869f, -0.05494615882f), new Double2(-0.1608953296f, 0.4202531296f), new Double2(-0.09463954939f, 0.4399356268f), new Double2(-0.02637688324f, -0.4492262904f), new Double2(0.447102804f, -0.05098119915f), new Double2(-0.4365670908f, 0.1091291678f), new Double2(-0.3959858651f, 0.2137643437f), new Double2(-0.4240048207f, -0.1507312575f), new Double2(-0.3882794568f, 0.2274622243f), new Double2(-0.4283652566f, -0.1378521198f), new Double2(0.3303888091f, 0.305521251f), new Double2(0.3321434919f, -0.3036127481f), new Double2(-0.413021046f, -0.1786438231f), new Double2(0.08403060337f, -0.4420846725f), new Double2(-0.3822882919f, 0.2373934748f), new Double2(-0.3712395594f, -0.2543249683f), new Double2(0.4472363971f, -0.04979563372f), new Double2(-0.4466591209f, 0.05473234629f), new Double2(0.0486272539f, -0.4473649407f), new Double2(-0.4203101295f, -0.1607463688f), new Double2(0.2205360833f, 0.39225481f), new Double2(-0.3624900666f, 0.2666476169f), new Double2(-0.4036086833f, -0.1989975647f), new Double2(0.2152727807f, 0.3951678503f), new Double2(-0.4359392962f, -0.1116106179f), new Double2(0.4178354266f, 0.1670735057f), new Double2(0.2007630161f, 0.4027334247f), new Double2(-0.07278067175f, -0.4440754146f), new Double2(0.3644748615f, -0.2639281632f), new Double2(-0.4317451775f, 0.126870413f), new Double2(-0.297436456f, 0.3376855855f), new Double2(-0.2998672222f, 0.3355289094f), new Double2(-0.2673674124f, 0.3619594822f), new Double2(0.2808423357f, 0.3516071423f), new Double2(0.3498946567f, 0.2829730186f), new Double2(-0.2229685561f, 0.390877248f), new Double2(0.3305823267f, 0.3053118493f), new Double2(-0.2436681211f, -0.3783197679f), new Double2(-0.03402776529f, 0.4487116125f), new Double2(-0.319358823f, 0.3170330301f), new Double2(0.4454633477f, -0.06373700535f), new Double2(0.4483504221f, 0.03849544189f), new Double2(-0.4427358436f, -0.08052932871f), new Double2(0.05452298565f, 0.4466847255f), new Double2(-0.2812560807f, 0.3512762688f), new Double2(0.1266696921f, 0.4318041097f), new Double2(-0.3735981243f, 0.2508474468f), new Double2(0.2959708351f, -0.3389708908f), new Double2(-0.3714377181f, 0.254035473f), new Double2(-0.404467102f, -0.1972469604f), new Double2(0.1636165687f, -0.419201167f), new Double2(0.3289185495f, -0.3071035458f), new Double2(-0.2494824991f, -0.3745109914f), new Double2(0.03283133272f, 0.4488007393f), new Double2(-0.166306057f, -0.4181414777f), new Double2(-0.106833179f, 0.4371346153f), new Double2(0.06440260376f, -0.4453676062f), new Double2(-0.4483230967f, 0.03881238203f), new Double2(-0.421377757f, -0.1579265206f), new Double2(0.05097920662f, -0.4471030312f), new Double2(0.2050584153f, -0.4005634111f), new Double2(0.4178098529f, -0.167137449f), new Double2(-0.3565189504f, -0.2745801121f), new Double2(0.4478398129f, 0.04403977727f), new Double2(-0.3399999602f, -0.2947881053f), new Double2(0.3767121994f, 0.2461461331f), new Double2(-0.3138934434f, 0.3224451987f), new Double2(-0.1462001792f, -0.4255884251f), new Double2(0.3970290489f, -0.2118205239f), new Double2(0.4459149305f, -0.06049689889f), new Double2(-0.4104889426f, -0.1843877112f), new Double2(0.1475103971f, -0.4251360756f), new Double2(0.09258030352f, 0.4403735771f), new Double2(-0.1589664637f, -0.4209865359f), new Double2(0.2482445008f, 0.3753327428f), new Double2(0.4383624232f, -0.1016778537f), new Double2(0.06242802956f, 0.4456486745f), new Double2(0.2846591015f, -0.3485243118f), new Double2(-0.344202744f, -0.2898697484f), new Double2(0.1198188883f, -0.4337550392f), new Double2(-0.243590703f, 0.3783696201f), new Double2(0.2958191174f, -0.3391033025f), new Double2(-0.1164007991f, 0.4346847754f), new Double2(0.1274037151f, -0.4315881062f), new Double2(0.368047306f, 0.2589231171f), new Double2(0.2451436949f, 0.3773652989f), new Double2(-0.4314509715f, 0.12786735f), }; private static final Double3[] CELL_3D = {new Double3(0.1453787434f, -0.4149781685f, -0.0956981749f), new Double3(-0.01242829687f, -0.1457918398f, -0.4255470325f), new Double3(0.2877979582f, -0.02606483451f, -0.3449535616f), new Double3(-0.07732986802f, 0.2377094325f, 0.3741848704f), new Double3(0.1107205875f, -0.3552302079f, -0.2530858567f), new Double3(0.2755209141f, 0.2640521179f, -0.238463215f), new Double3(0.294168941f, 0.1526064594f, 0.3044271714f), new Double3(0.4000921098f, -0.2034056362f, 0.03244149937f), new Double3(-0.1697304074f, 0.3970864695f, -0.1265461359f), new Double3(-0.1483224484f, -0.3859694688f, 0.1775613147f), new Double3(0.2623596946f, -0.2354852944f, 0.2796677792f), new Double3(-0.2709003183f, 0.3505271138f, -0.07901746678f), new Double3(-0.03516550699f, 0.3885234328f, 0.2243054374f), new Double3(-0.1267712655f, 0.1920044036f, 0.3867342179f), new Double3(0.02952021915f, 0.4409685861f, 0.08470692262f), new Double3(-0.2806854217f, -0.266996757f, 0.2289725438f), new Double3(-0.171159547f, 0.2141185563f, 0.3568720405f), new Double3(0.2113227183f, 0.3902405947f, -0.07453178509f), new Double3(-0.1024352839f, 0.2128044156f, -0.3830421561f), new Double3(-0.3304249877f, -0.1566986703f, 0.2622305365f), new Double3(0.2091111325f, 0.3133278055f, -0.2461670583f), new Double3(0.344678154f, -0.1944240454f, -0.2142341261f), new Double3(0.1984478035f, -0.3214342325f, -0.2445373252f), new Double3(-0.2929008603f, 0.2262915116f, 0.2559320961f), new Double3(-0.1617332831f, 0.006314769776f, -0.4198838754f), new Double3(-0.3582060271f, -0.148303178f, -0.2284613961f), new Double3(-0.1852067326f, -0.3454119342f, -0.2211087107f), new Double3(0.3046301062f, 0.1026310383f, 0.314908508f), new Double3(-0.03816768434f, -0.2551766358f, -0.3686842991f), new Double3(-0.4084952196f, 0.1805950793f, 0.05492788837f), new Double3(-0.02687443361f, -0.2749741471f, 0.3551999201f), new Double3(-0.03801098351f, 0.3277859044f, 0.3059600725f), new Double3(0.2371120802f, 0.2900386767f, -0.2493099024f), new Double3(0.4447660503f, 0.03946930643f, 0.05590469027f), new Double3(0.01985147278f, -0.01503183293f, -0.4493105419f), new Double3(0.4274339143f, 0.03345994256f, -0.1366772882f), new Double3(-0.2072988631f, 0.2871414597f, -0.2776273824f), new Double3(-0.3791240978f, 0.1281177671f, 0.2057929936f), new Double3(-0.2098721267f, -0.1007087278f, -0.3851122467f), new Double3(0.01582798878f, 0.4263894424f, 0.1429738373f), new Double3(-0.1888129464f, -0.3160996813f, -0.2587096108f), new Double3(0.1612988974f, -0.1974805082f, -0.3707885038f), new Double3(-0.08974491322f, 0.229148752f, -0.3767448739f), new Double3(0.07041229526f, 0.4150230285f, -0.1590534329f), new Double3(-0.1082925611f, -0.1586061639f, 0.4069604477f), new Double3(0.2474100658f, -0.3309414609f, 0.1782302128f), new Double3(-0.1068836661f, -0.2701644537f, -0.3436379634f), new Double3(0.2396452163f, 0.06803600538f, -0.3747549496f), new Double3(-0.3063886072f, 0.2597428179f, 0.2028785103f), new Double3(0.1593342891f, -0.3114350249f, -0.2830561951f), new Double3(0.2709690528f, 0.1412648683f, -0.3303331794f), new Double3(-0.1519780427f, 0.3623355133f, 0.2193527988f), new Double3(0.1699773681f, 0.3456012883f, 0.2327390037f), new Double3(-0.1986155616f, 0.3836276443f, -0.1260225743f), new Double3(-0.1887482106f, -0.2050154888f, -0.353330953f), new Double3(0.2659103394f, 0.3015631259f, -0.2021172246f), new Double3(-0.08838976154f, -0.4288819642f, -0.1036702021f), new Double3(-0.04201869311f, 0.3099592485f, 0.3235115047f), new Double3(-0.3230334656f, 0.201549922f, -0.2398478873f), new Double3(0.2612720941f, 0.2759854499f, -0.2409749453f), new Double3(0.385713046f, 0.2193460345f, 0.07491837764f), new Double3(0.07654967953f, 0.3721732183f, 0.241095919f), new Double3(0.4317038818f, -0.02577753072f, 0.1243675091f), new Double3(-0.2890436293f, -0.3418179959f, -0.04598084447f), new Double3(-0.2201947582f, 0.383023377f, -0.08548310451f), new Double3(0.4161322773f, -0.1669634289f, -0.03817251927f), new Double3(0.2204718095f, 0.02654238946f, -0.391391981f), new Double3(-0.1040307469f, 0.3890079625f, -0.2008741118f), new Double3(-0.1432122615f, 0.371614387f, -0.2095065525f), new Double3(0.3978380468f, -0.06206669342f, 0.2009293758f), new Double3(-0.2599274663f, 0.2616724959f, -0.2578084893f), new Double3(0.4032618332f, -0.1124593585f, 0.1650235939f), new Double3(-0.08953470255f, -0.3048244735f, 0.3186935478f), new Double3(0.118937202f, -0.2875221847f, 0.325092195f), new Double3(0.02167047076f, -0.03284630549f, -0.4482761547f), new Double3(-0.3411343612f, 0.2500031105f, 0.1537068389f), new Double3(0.3162964612f, 0.3082064153f, -0.08640228117f), new Double3(0.2355138889f, -0.3439334267f, -0.1695376245f), new Double3(-0.02874541518f, -0.3955933019f, 0.2125550295f), new Double3(-0.2461455173f, 0.02020282325f, -0.3761704803f), new Double3(0.04208029445f, -0.4470439576f, 0.02968078139f), new Double3(0.2727458746f, 0.2288471896f, -0.2752065618f), new Double3(-0.1347522818f, -0.02720848277f, -0.4284874806f), new Double3(0.3829624424f, 0.1231931484f, -0.2016512234f), new Double3(-0.3547613644f, 0.1271702173f, 0.2459107769f), new Double3(0.2305790207f, 0.3063895591f, 0.2354968222f), new Double3(-0.08323845599f, -0.1922245118f, 0.3982726409f), new Double3(0.2993663085f, -0.2619918095f, -0.2103333191f), new Double3(-0.2154865723f, 0.2706747713f, 0.287751117f), new Double3(0.01683355354f, -0.2680655787f, -0.3610505186f), new Double3(0.05240429123f, 0.4335128183f, -0.1087217856f), new Double3(0.00940104872f, -0.4472890582f, 0.04841609928f), new Double3(0.3465688735f, 0.01141914583f, -0.2868093776f), new Double3(-0.3706867948f, -0.2551104378f, 0.003156692623f), new Double3(0.2741169781f, 0.2139972417f, -0.2855959784f), new Double3(0.06413433865f, 0.1708718512f, 0.4113266307f), new Double3(-0.388187972f, -0.03973280434f, -0.2241236325f), new Double3(0.06419469312f, -0.2803682491f, 0.3460819069f), new Double3(-0.1986120739f, -0.3391173584f, 0.2192091725f), new Double3(-0.203203009f, -0.3871641506f, 0.1063600375f), new Double3(-0.1389736354f, -0.2775901578f, -0.3257760473f), new Double3(-0.06555641638f, 0.342253257f, -0.2847192729f), new Double3(-0.2529246486f, -0.2904227915f, 0.2327739768f), new Double3(0.1444476522f, 0.1069184044f, 0.4125570634f), new Double3(-0.3643780054f, -0.2447099973f, -0.09922543227f), new Double3(0.4286142488f, -0.1358496089f, -0.01829506817f), new Double3(0.165872923f, -0.3136808464f, -0.2767498872f), new Double3(0.2219610524f, -0.3658139958f, 0.1393320198f), new Double3(0.04322940318f, -0.3832730794f, 0.2318037215f), new Double3(-0.08481269795f, -0.4404869674f, -0.03574965489f), new Double3(0.1822082075f, -0.3953259299f, 0.1140946023f), new Double3(-0.3269323334f, 0.3036542563f, 0.05838957105f), new Double3(-0.4080485344f, 0.04227858267f, -0.184956522f), new Double3(0.2676025294f, -0.01299671652f, 0.36155217f), new Double3(0.3024892441f, -0.1009990293f, -0.3174892964f), new Double3(0.1448494052f, 0.425921681f, -0.0104580805f), new Double3(0.4198402157f, 0.08062320474f, 0.1404780841f), new Double3(-0.3008872161f, -0.333040905f, -0.03241355801f), new Double3(0.3639310428f, -0.1291284382f, -0.2310412139f), new Double3(0.3295806598f, 0.0184175994f, -0.3058388149f), new Double3(0.2776259487f, -0.2974929052f, -0.1921504723f), new Double3(0.4149000507f, -0.144793182f, -0.09691688386f), new Double3(0.145016715f, -0.0398992945f, 0.4241205002f), new Double3(0.09299023471f, -0.299732164f, -0.3225111565f), new Double3(0.1028907093f, -0.361266869f, 0.247789732f), new Double3(0.2683057049f, -0.07076041213f, -0.3542668666f), new Double3(-0.4227307273f, -0.07933161816f, -0.1323073187f), new Double3(-0.1781224702f, 0.1806857196f, -0.3716517945f), new Double3(0.4390788626f, -0.02841848598f, -0.09435116353f), new Double3(0.2972583585f, 0.2382799621f, -0.2394997452f), new Double3(-0.1707002821f, 0.2215845691f, 0.3525077196f), new Double3(0.3806686614f, 0.1471852559f, -0.1895464869f), new Double3(-0.1751445661f, -0.274887877f, 0.3102596268f), new Double3(-0.2227237566f, -0.2316778837f, 0.3149912482f), new Double3(0.1369633021f, 0.1341343041f, -0.4071228836f), new Double3(-0.3529503428f, -0.2472893463f, -0.129514612f), new Double3(-0.2590744185f, -0.2985577559f, -0.2150435121f), new Double3(-0.3784019401f, 0.2199816631f, -0.1044989934f), new Double3(-0.05635805671f, 0.1485737441f, 0.4210102279f), new Double3(0.3251428613f, 0.09666046873f, -0.2957006485f), new Double3(-0.4190995804f, 0.1406751354f, -0.08405978803f), new Double3(-0.3253150961f, -0.3080335042f, -0.04225456877f), new Double3(0.2857945863f, -0.05796152095f, 0.3427271751f), new Double3(-0.2733604046f, 0.1973770973f, -0.2980207554f), new Double3(0.219003657f, 0.2410037886f, -0.3105713639f), new Double3(0.3182767252f, -0.271342949f, 0.1660509868f), new Double3(-0.03222023115f, -0.3331161506f, -0.300824678f), new Double3(-0.3087780231f, 0.1992794134f, -0.2596995338f), new Double3(-0.06487611647f, -0.4311322747f, 0.1114273361f), new Double3(0.3921171432f, -0.06294284106f, -0.2116183942f), new Double3(-0.1606404506f, -0.358928121f, -0.2187812825f), new Double3(-0.03767771199f, -0.2290351443f, 0.3855169162f), new Double3(0.1394866832f, -0.3602213994f, 0.2308332918f), new Double3(-0.4345093872f, 0.005751117145f, 0.1169124335f), new Double3(-0.1044637494f, 0.4168128432f, -0.1336202785f), new Double3(0.2658727501f, 0.2551943237f, 0.2582393035f), new Double3(0.2051461999f, 0.1975390727f, 0.3484154868f), new Double3(-0.266085566f, 0.23483312f, 0.2766800993f), new Double3(0.07849405464f, -0.3300346342f, -0.2956616708f), new Double3(-0.2160686338f, 0.05376451292f, -0.3910546287f), new Double3(-0.185779186f, 0.2148499206f, 0.3490352499f), new Double3(0.02492421743f, -0.3229954284f, -0.3123343347f), new Double3(-0.120167831f, 0.4017266681f, 0.1633259825f), new Double3(-0.02160084693f, -0.06885389554f, 0.4441762538f), new Double3(0.2597670064f, 0.3096300784f, 0.1978643903f), new Double3(-0.1611553854f, -0.09823036005f, 0.4085091653f), new Double3(-0.3278896792f, 0.1461670309f, 0.2713366126f), new Double3(0.2822734956f, 0.03754421121f, -0.3484423997f), new Double3(0.03169341113f, 0.347405252f, -0.2842624114f), new Double3(0.2202613604f, -0.3460788041f, -0.1849713341f), new Double3(0.2933396046f, 0.3031973659f, 0.1565989581f), new Double3(-0.3194922995f, 0.2453752201f, -0.200538455f), new Double3(-0.3441586045f, -0.1698856132f, -0.2349334659f), new Double3(0.2703645948f, -0.3574277231f, 0.04060059933f), new Double3(0.2298568861f, 0.3744156221f, 0.0973588921f), new Double3(0.09326603877f, -0.3170108894f, 0.3054595587f), new Double3(-0.1116165319f, -0.2985018719f, 0.3177080142f), new Double3(0.2172907365f, -0.3460005203f, -0.1885958001f), new Double3(0.1991339479f, 0.3820341668f, -0.1299829458f), new Double3(-0.0541918155f, -0.2103145071f, 0.39412061f), new Double3(0.08871336998f, 0.2012117383f, 0.3926114802f), new Double3(0.2787673278f, 0.3505404674f, 0.04370535101f), new Double3(-0.322166438f, 0.3067213525f, 0.06804996813f), new Double3(-0.4277366384f, 0.132066775f, 0.04582286686f), new Double3(0.240131882f, -0.1612516055f, 0.344723946f), new Double3(0.1448607981f, -0.2387819045f, 0.3528435224f), new Double3(-0.3837065682f, -0.2206398454f, 0.08116235683f), new Double3(-0.4382627882f, -0.09082753406f, -0.04664855374f), new Double3(-0.37728353f, 0.05445141085f, 0.2391488697f), new Double3(0.1259579313f, 0.348394558f, 0.2554522098f), new Double3(-0.1406285511f, -0.270877371f, -0.3306796947f), new Double3(-0.1580694418f, 0.4162931958f, -0.06491553533f), new Double3(0.2477612106f, -0.2927867412f, -0.2353514536f), new Double3(0.2916132853f, 0.3312535401f, 0.08793624968f), new Double3(0.07365265219f, -0.1666159848f, 0.411478311f), new Double3(-0.26126526f, -0.2422237692f, 0.2748965434f), new Double3(-0.3721862032f, 0.252790166f, 0.008634938242f), new Double3(-0.3691191571f, -0.255281188f, 0.03290232422f), new Double3(0.2278441737f, -0.3358364886f, 0.1944244981f), new Double3(0.363398169f, -0.2310190248f, 0.1306597909f), new Double3(-0.304231482f, -0.2698452035f, 0.1926830856f), new Double3(-0.3199312232f, 0.316332536f, -0.008816977938f), new Double3(0.2874852279f, 0.1642275508f, -0.304764754f), new Double3(-0.1451096801f, 0.3277541114f, -0.2720669462f), new Double3(0.3220090754f, 0.0511344108f, 0.3101538769f), new Double3(-0.1247400865f, -0.04333605335f, -0.4301882115f), new Double3(-0.2829555867f, -0.3056190617f, -0.1703910946f), new Double3(0.1069384374f, 0.3491024667f, -0.2630430352f), new Double3(-0.1420661144f, -0.3055376754f, -0.2982682484f), new Double3(-0.250548338f, 0.3156466809f, -0.2002316239f), new Double3(0.3265787872f, 0.1871229129f, 0.2466400438f), new Double3(0.07646097258f, -0.3026690852f, 0.324106687f), new Double3(0.3451771584f, 0.2757120714f, -0.0856480183f), new Double3(0.298137964f, 0.2852657134f, 0.179547284f), new Double3(0.2812250376f, 0.3466716415f, 0.05684409612f), new Double3(0.4390345476f, -0.09790429955f, -0.01278335452f), new Double3(0.2148373234f, 0.1850172527f, 0.3494474791f), new Double3(0.2595421179f, -0.07946825393f, 0.3589187731f), new Double3(0.3182823114f, -0.307355516f, -0.08203022006f), new Double3(-0.4089859285f, -0.04647718411f, 0.1818526372f), new Double3(-0.2826749061f, 0.07417482322f, 0.3421885344f), new Double3(0.3483864637f, 0.225442246f, -0.1740766085f), new Double3(-0.3226415069f, -0.1420585388f, -0.2796816575f), new Double3(0.4330734858f, -0.118868561f, -0.02859407492f), new Double3(-0.08717822568f, -0.3909896417f, -0.2050050172f), new Double3(-0.2149678299f, 0.3939973956f, -0.03247898316f), new Double3(-0.2687330705f, 0.322686276f, -0.1617284888f), new Double3(0.2105665099f, -0.1961317136f, -0.3459683451f), new Double3(0.4361845915f, -0.1105517485f, 0.004616608544f), new Double3(0.05333333359f, -0.313639498f, -0.3182543336f), new Double3(-0.05986216652f, 0.1361029153f, -0.4247264031f), new Double3(0.3664988455f, 0.2550543014f, -0.05590974511f), new Double3(-0.2341015558f, -0.182405731f, 0.3382670703f), new Double3(-0.04730947785f, -0.4222150243f, -0.1483114513f), new Double3(-0.2391566239f, -0.2577696514f, -0.2808182972f), new Double3(-0.1242081035f, 0.4256953395f, -0.07652336246f), new Double3(0.2614832715f, -0.3650179274f, 0.02980623099f), new Double3(-0.2728794681f, -0.3499628774f, 0.07458404908f), new Double3(0.007892900508f, -0.1672771315f, 0.4176793787f), new Double3(-0.01730330376f, 0.2978486637f, -0.3368779738f), new Double3(0.2054835762f, -0.3252600376f, -0.2334146693f), new Double3(-0.3231994983f, 0.1564282844f, -0.2712420987f), new Double3(-0.2669545963f, 0.2599343665f, -0.2523278991f), new Double3(-0.05554372779f, 0.3170813944f, -0.3144428146f), new Double3(-0.2083935713f, -0.310922837f, -0.2497981362f), new Double3(0.06989323478f, -0.3156141536f, 0.3130537363f), new Double3(0.3847566193f, -0.1605309138f, -0.1693876312f), new Double3(-0.3026215288f, -0.3001537679f, -0.1443188342f), new Double3(0.3450735512f, 0.08611519592f, 0.2756962409f), new Double3(0.1814473292f, -0.2788782453f, -0.3029914042f), new Double3(-0.03855010448f, 0.09795110726f, 0.4375151083f), new Double3(0.3533670318f, 0.2665752752f, 0.08105160988f), new Double3(-0.007945601311f, 0.140359426f, -0.4274764309f), new Double3(0.4063099273f, -0.1491768253f, -0.1231199324f), new Double3(-0.2016773589f, 0.008816271194f, -0.4021797064f), new Double3(-0.07527055435f, -0.425643481f, -0.1251477955f), }; + private static final double[] CELL_2D_X = extractCell2DComponent(true); + private static final double[] CELL_2D_Y = extractCell2DComponent(false); + private static final double[] CELL_3D_X = extractCell3DComponent(0); + private static final double[] CELL_3D_Y = extractCell3DComponent(1); + private static final double[] CELL_3D_Z = extractCell3DComponent(2); // Hashing private final static long X_PRIME = 1619; private final static long Y_PRIME = 31337; @@ -37,8 +45,8 @@ public class FastNoiseDouble { private final static double F3 = 1.0 / 3.0; private final static double G3 = 1.0 / 6.0; private final static double G33 = G3 * 3 - 1; - private final static double F2 = 1.0 / 2.0; - private final static double G2 = 1.0 / 4.0; + private final static double F2 = 0.5 * (Math.sqrt(3.0) - 1.0); + private final static double G2 = (3.0 - Math.sqrt(3.0)) / 6.0; private static final byte[] SIMPLEX_4D = {0, 1, 2, 3, 0, 1, 3, 2, 0, 0, 0, 0, 0, 2, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 2, 1, 3, 0, 0, 0, 0, 0, 3, 1, 2, 0, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 3, 0, 0, 0, 0, 1, 3, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 1, 2, 3, 1, 0, 1, 0, 2, 3, 1, 0, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 1, 0, 0, 0, 0, 2, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 2, 3, 0, 2, 1, 0, 0, 0, 0, 3, 1, 2, 0, 2, 1, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 0, 2, 0, 0, 0, 0, 3, 2, 0, 1, 3, 2, 1, 0 }; private final static double F4 = (2.23606797 - 1.0) / 4.0; @@ -98,6 +106,24 @@ public class FastNoiseDouble { return t * t * t * p + t * t * ((a - b) - p) + t * (c - a) + b; } + private static double[] extractCell2DComponent(boolean xComponent) { + double[] components = new double[CELL_2D.length]; + for (int i = 0; i < CELL_2D.length; i++) { + Double2 value = CELL_2D[i]; + components[i] = xComponent ? value.x : value.y; + } + return components; + } + + private static double[] extractCell3DComponent(int axis) { + double[] components = new double[CELL_3D.length]; + for (int i = 0; i < CELL_3D.length; i++) { + Double3 value = CELL_3D[i]; + components[i] = axis == 0 ? value.x : axis == 1 ? value.y : value.z; + } + return components; + } + private static long hash2D(long seed, long x, long y) { long hash = seed; hash ^= X_PRIME * x; @@ -169,9 +195,8 @@ public class FastNoiseDouble { hash = hash * hash * hash * 60493; hash = (hash >> 13) ^ hash; - Double2 g = GRAD_2D[(int) hash & 7]; - - return xd * g.x + yd * g.y; + int gradientIndex = ((int) hash & 7) << 1; + return (xd * GRAD_2D[gradientIndex]) + (yd * GRAD_2D[gradientIndex + 1]); } private static double GradCoord3D(long seed, long x, long y, long z, double xd, double yd, double zd) { @@ -183,9 +208,8 @@ public class FastNoiseDouble { hash = hash * hash * hash * 60493; hash = (hash >> 13) ^ hash; - Double3 g = GRAD_3D[(int) (hash & 15)]; - - return xd * g.x + yd * g.y + zd * g.z; + int gradientIndex = ((int) (hash & 15)) * 3; + return (xd * GRAD_3D[gradientIndex]) + (yd * GRAD_3D[gradientIndex + 1]) + (zd * GRAD_3D[gradientIndex + 2]); } private static double GradCoord4D(long seed, long x, long y, long z, long w, double xd, double yd, double zd, double wd) { @@ -231,6 +255,14 @@ public class FastNoiseDouble { m_seed = seed; } + public double getFrequency() { + return m_frequency; + } + + public double getFractalBounding() { + return m_fractalBounding; + } + // Sets frequency for all noise types // Default: 0.01 public void setFrequency(double frequency) { @@ -1132,8 +1164,8 @@ public class FastNoiseDouble { double x1 = x0 - i1 + G2; double y1 = y0 - j1 + G2; - double x2 = x0 - 1 + F2; - double y2 = y0 - 1 + F2; + double x2 = x0 - 1 + (2 * G2); + double y2 = y0 - 1 + (2 * G2); double n0, n1, n2; @@ -1457,6 +1489,10 @@ public class FastNoiseDouble { long xr = fastRound(x); long yr = fastRound(y); long zr = fastRound(z); + long seed = m_seed; + double[] cell3dX = CELL_3D_X; + double[] cell3dY = CELL_3D_Y; + double[] cell3dZ = CELL_3D_Z; double distance = 999999; long xc = 0, yc = 0, zc = 0; @@ -1464,13 +1500,15 @@ public class FastNoiseDouble { switch (m_cellularDistanceFunction) { case Euclidean: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { + double baseVecY = yi - y; for (long zi = zr - 1; zi <= zr + 1; zi++) { - Double3 vec = CELL_3D[(int) hash3D(m_seed, xi, yi, zi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; - double vecZ = zi - z + vec.z; + double baseVecZ = zi - z; + int cellIndex3D = (int) hash3D(seed, xi, yi, zi) & 255; + double vecX = baseVecX + cell3dX[cellIndex3D]; + double vecY = baseVecY + cell3dY[cellIndex3D]; + double vecZ = baseVecZ + cell3dZ[cellIndex3D]; double newDistance = vecX * vecX + vecY * vecY + vecZ * vecZ; @@ -1486,13 +1524,15 @@ public class FastNoiseDouble { break; case Manhattan: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { + double baseVecY = yi - y; for (long zi = zr - 1; zi <= zr + 1; zi++) { - Double3 vec = CELL_3D[(int) hash3D(m_seed, xi, yi, zi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; - double vecZ = zi - z + vec.z; + double baseVecZ = zi - z; + int cellIndex3D = (int) hash3D(seed, xi, yi, zi) & 255; + double vecX = baseVecX + cell3dX[cellIndex3D]; + double vecY = baseVecY + cell3dY[cellIndex3D]; + double vecZ = baseVecZ + cell3dZ[cellIndex3D]; double newDistance = Math.abs(vecX) + Math.abs(vecY) + Math.abs(vecZ); @@ -1508,13 +1548,15 @@ public class FastNoiseDouble { break; case Natural: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { + double baseVecY = yi - y; for (long zi = zr - 1; zi <= zr + 1; zi++) { - Double3 vec = CELL_3D[(int) hash3D(m_seed, xi, yi, zi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; - double vecZ = zi - z + vec.z; + double baseVecZ = zi - z; + int cellIndex3D = (int) hash3D(seed, xi, yi, zi) & 255; + double vecX = baseVecX + cell3dX[cellIndex3D]; + double vecY = baseVecY + cell3dY[cellIndex3D]; + double vecZ = baseVecZ + cell3dZ[cellIndex3D]; double newDistance = (Math.abs(vecX) + Math.abs(vecY) + Math.abs(vecZ)) + (vecX * vecX + vecY * vecY + vecZ * vecZ); @@ -1535,8 +1577,8 @@ public class FastNoiseDouble { return valCoord3D(0, xc, yc, zc); case NoiseLookup: - Double3 vec = CELL_3D[(int) hash3D(m_seed, xc, yc, zc) & 255]; - return m_cellularNoiseLookup.GetNoise(xc + vec.x, yc + vec.y, zc + vec.z); + int lookupIndex3D = (int) hash3D(m_seed, xc, yc, zc) & 255; + return m_cellularNoiseLookup.GetNoise(xc + CELL_3D_X[lookupIndex3D], yc + CELL_3D_Y[lookupIndex3D], zc + CELL_3D_Z[lookupIndex3D]); case Distance: return distance - 1; @@ -1549,6 +1591,10 @@ public class FastNoiseDouble { long xr = fastRound(x); long yr = fastRound(y); long zr = fastRound(z); + long seed = m_seed; + double[] cell3dX = CELL_3D_X; + double[] cell3dY = CELL_3D_Y; + double[] cell3dZ = CELL_3D_Z; double distance = 999999; double distance2 = 999999; @@ -1556,54 +1602,72 @@ public class FastNoiseDouble { switch (m_cellularDistanceFunction) { case Euclidean: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { + double baseVecY = yi - y; for (long zi = zr - 1; zi <= zr + 1; zi++) { - Double3 vec = CELL_3D[(int) hash3D(m_seed, xi, yi, zi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; - double vecZ = zi - z + vec.z; + double baseVecZ = zi - z; + int cellIndex3D = (int) hash3D(seed, xi, yi, zi) & 255; + double vecX = baseVecX + cell3dX[cellIndex3D]; + double vecY = baseVecY + cell3dY[cellIndex3D]; + double vecZ = baseVecZ + cell3dZ[cellIndex3D]; double newDistance = vecX * vecX + vecY * vecY + vecZ * vecZ; - distance2 = Math.max(Math.min(distance2, newDistance), distance); - distance = Math.min(distance, newDistance); + if (newDistance < distance) { + distance2 = distance; + distance = newDistance; + } else if (newDistance < distance2) { + distance2 = newDistance; + } } } } break; case Manhattan: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { + double baseVecY = yi - y; for (long zi = zr - 1; zi <= zr + 1; zi++) { - Double3 vec = CELL_3D[(int) hash3D(m_seed, xi, yi, zi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; - double vecZ = zi - z + vec.z; + double baseVecZ = zi - z; + int cellIndex3D = (int) hash3D(seed, xi, yi, zi) & 255; + double vecX = baseVecX + cell3dX[cellIndex3D]; + double vecY = baseVecY + cell3dY[cellIndex3D]; + double vecZ = baseVecZ + cell3dZ[cellIndex3D]; double newDistance = Math.abs(vecX) + Math.abs(vecY) + Math.abs(vecZ); - distance2 = Math.max(Math.min(distance2, newDistance), distance); - distance = Math.min(distance, newDistance); + if (newDistance < distance) { + distance2 = distance; + distance = newDistance; + } else if (newDistance < distance2) { + distance2 = newDistance; + } } } } break; case Natural: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { + double baseVecY = yi - y; for (long zi = zr - 1; zi <= zr + 1; zi++) { - Double3 vec = CELL_3D[(int) hash3D(m_seed, xi, yi, zi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; - double vecZ = zi - z + vec.z; + double baseVecZ = zi - z; + int cellIndex3D = (int) hash3D(seed, xi, yi, zi) & 255; + double vecX = baseVecX + cell3dX[cellIndex3D]; + double vecY = baseVecY + cell3dY[cellIndex3D]; + double vecZ = baseVecZ + cell3dZ[cellIndex3D]; double newDistance = (Math.abs(vecX) + Math.abs(vecY) + Math.abs(vecZ)) + (vecX * vecX + vecY * vecY + vecZ * vecZ); - distance2 = Math.max(Math.min(distance2, newDistance), distance); - distance = Math.min(distance, newDistance); + if (newDistance < distance) { + distance2 = distance; + distance = newDistance; + } else if (newDistance < distance2) { + distance2 = newDistance; + } } } } @@ -1635,6 +1699,9 @@ public class FastNoiseDouble { private double SingleCellular(double x, double y) { long xr = fastRound(x); long yr = fastRound(y); + long seed = m_seed; + double[] cell2dX = CELL_2D_X; + double[] cell2dY = CELL_2D_Y; double distance = 999999; long xc = 0, yc = 0; @@ -1643,11 +1710,12 @@ public class FastNoiseDouble { default: case Euclidean: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { - Double2 vec = CELL_2D[(int) hash2D(m_seed, xi, yi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; + double baseVecY = yi - y; + int cellIndex2D = (int) hash2D(seed, xi, yi) & 255; + double vecX = baseVecX + cell2dX[cellIndex2D]; + double vecY = baseVecY + cell2dY[cellIndex2D]; double newDistance = vecX * vecX + vecY * vecY; @@ -1661,11 +1729,12 @@ public class FastNoiseDouble { break; case Manhattan: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { - Double2 vec = CELL_2D[(int) hash2D(m_seed, xi, yi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; + double baseVecY = yi - y; + int cellIndex2D = (int) hash2D(seed, xi, yi) & 255; + double vecX = baseVecX + cell2dX[cellIndex2D]; + double vecY = baseVecY + cell2dY[cellIndex2D]; double newDistance = (Math.abs(vecX) + Math.abs(vecY)); @@ -1679,11 +1748,12 @@ public class FastNoiseDouble { break; case Natural: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { - Double2 vec = CELL_2D[(int) hash2D(m_seed, xi, yi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; + double baseVecY = yi - y; + int cellIndex2D = (int) hash2D(seed, xi, yi) & 255; + double vecX = baseVecX + cell2dX[cellIndex2D]; + double vecY = baseVecY + cell2dY[cellIndex2D]; double newDistance = (Math.abs(vecX) + Math.abs(vecY)) + (vecX * vecX + vecY * vecY); @@ -1702,8 +1772,8 @@ public class FastNoiseDouble { return valCoord2D(0, xc, yc); case NoiseLookup: - Double2 vec = CELL_2D[(int) hash2D(m_seed, xc, yc) & 255]; - return m_cellularNoiseLookup.GetNoise(xc + vec.x, yc + vec.y); + int lookupIndex2D = (int) hash2D(m_seed, xc, yc) & 255; + return m_cellularNoiseLookup.GetNoise(xc + CELL_2D_X[lookupIndex2D], yc + CELL_2D_Y[lookupIndex2D]); case Distance: return distance - 1; @@ -1715,6 +1785,9 @@ public class FastNoiseDouble { private double SingleCellular2Edge(double x, double y) { long xr = fastRound(x); long yr = fastRound(y); + long seed = m_seed; + double[] cell2dX = CELL_2D_X; + double[] cell2dY = CELL_2D_Y; double distance = 999999; double distance2 = 999999; @@ -1723,46 +1796,61 @@ public class FastNoiseDouble { default: case Euclidean: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { - Double2 vec = CELL_2D[(int) hash2D(m_seed, xi, yi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; + double baseVecY = yi - y; + int cellIndex2D = (int) hash2D(seed, xi, yi) & 255; + double vecX = baseVecX + cell2dX[cellIndex2D]; + double vecY = baseVecY + cell2dY[cellIndex2D]; double newDistance = vecX * vecX + vecY * vecY; - distance2 = Math.max(Math.min(distance2, newDistance), distance); - distance = Math.min(distance, newDistance); + if (newDistance < distance) { + distance2 = distance; + distance = newDistance; + } else if (newDistance < distance2) { + distance2 = newDistance; + } } } break; case Manhattan: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { - Double2 vec = CELL_2D[(int) hash2D(m_seed, xi, yi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; + double baseVecY = yi - y; + int cellIndex2D = (int) hash2D(seed, xi, yi) & 255; + double vecX = baseVecX + cell2dX[cellIndex2D]; + double vecY = baseVecY + cell2dY[cellIndex2D]; double newDistance = Math.abs(vecX) + Math.abs(vecY); - distance2 = Math.max(Math.min(distance2, newDistance), distance); - distance = Math.min(distance, newDistance); + if (newDistance < distance) { + distance2 = distance; + distance = newDistance; + } else if (newDistance < distance2) { + distance2 = newDistance; + } } } break; case Natural: for (long xi = xr - 1; xi <= xr + 1; xi++) { + double baseVecX = xi - x; for (long yi = yr - 1; yi <= yr + 1; yi++) { - Double2 vec = CELL_2D[(int) hash2D(m_seed, xi, yi) & 255]; - - double vecX = xi - x + vec.x; - double vecY = yi - y + vec.y; + double baseVecY = yi - y; + int cellIndex2D = (int) hash2D(seed, xi, yi) & 255; + double vecX = baseVecX + cell2dX[cellIndex2D]; + double vecY = baseVecY + cell2dY[cellIndex2D]; double newDistance = (Math.abs(vecX) + Math.abs(vecY)) + (vecX * vecX + vecY * vecY); - distance2 = Math.max(Math.min(distance2, newDistance), distance); - distance = Math.min(distance, newDistance); + if (newDistance < distance) { + distance2 = distance; + distance = newDistance; + } else if (newDistance < distance2) { + distance2 = newDistance; + } } } break; @@ -1827,37 +1915,37 @@ public class FastNoiseDouble { } } - Double3 vec0 = CELL_3D[(int) hash3D(seed, x0, y0, z0) & 255]; - Double3 vec1 = CELL_3D[(int) hash3D(seed, x1, y0, z0) & 255]; + int vecIndex000 = (int) hash3D(seed, x0, y0, z0) & 255; + int vecIndex100 = (int) hash3D(seed, x1, y0, z0) & 255; - double lx0x = lerp(vec0.x, vec1.x, xs); - double ly0x = lerp(vec0.y, vec1.y, xs); - double lz0x = lerp(vec0.z, vec1.z, xs); + double lx0x = lerp(CELL_3D_X[vecIndex000], CELL_3D_X[vecIndex100], xs); + double ly0x = lerp(CELL_3D_Y[vecIndex000], CELL_3D_Y[vecIndex100], xs); + double lz0x = lerp(CELL_3D_Z[vecIndex000], CELL_3D_Z[vecIndex100], xs); - vec0 = CELL_3D[(int) hash3D(seed, x0, y1, z0) & 255]; - vec1 = CELL_3D[(int) hash3D(seed, x1, y1, z0) & 255]; + int vecIndex010 = (int) hash3D(seed, x0, y1, z0) & 255; + int vecIndex110 = (int) hash3D(seed, x1, y1, z0) & 255; - double lx1x = lerp(vec0.x, vec1.x, xs); - double ly1x = lerp(vec0.y, vec1.y, xs); - double lz1x = lerp(vec0.z, vec1.z, xs); + double lx1x = lerp(CELL_3D_X[vecIndex010], CELL_3D_X[vecIndex110], xs); + double ly1x = lerp(CELL_3D_Y[vecIndex010], CELL_3D_Y[vecIndex110], xs); + double lz1x = lerp(CELL_3D_Z[vecIndex010], CELL_3D_Z[vecIndex110], xs); double lx0y = lerp(lx0x, lx1x, ys); double ly0y = lerp(ly0x, ly1x, ys); double lz0y = lerp(lz0x, lz1x, ys); - vec0 = CELL_3D[(int) hash3D(seed, x0, y0, z1) & 255]; - vec1 = CELL_3D[(int) hash3D(seed, x1, y0, z1) & 255]; + int vecIndex001 = (int) hash3D(seed, x0, y0, z1) & 255; + int vecIndex101 = (int) hash3D(seed, x1, y0, z1) & 255; - lx0x = lerp(vec0.x, vec1.x, xs); - ly0x = lerp(vec0.y, vec1.y, xs); - lz0x = lerp(vec0.z, vec1.z, xs); + lx0x = lerp(CELL_3D_X[vecIndex001], CELL_3D_X[vecIndex101], xs); + ly0x = lerp(CELL_3D_Y[vecIndex001], CELL_3D_Y[vecIndex101], xs); + lz0x = lerp(CELL_3D_Z[vecIndex001], CELL_3D_Z[vecIndex101], xs); - vec0 = CELL_3D[(int) hash3D(seed, x0, y1, z1) & 255]; - vec1 = CELL_3D[(int) hash3D(seed, x1, y1, z1) & 255]; + int vecIndex011 = (int) hash3D(seed, x0, y1, z1) & 255; + int vecIndex111 = (int) hash3D(seed, x1, y1, z1) & 255; - lx1x = lerp(vec0.x, vec1.x, xs); - ly1x = lerp(vec0.y, vec1.y, xs); - lz1x = lerp(vec0.z, vec1.z, xs); + lx1x = lerp(CELL_3D_X[vecIndex011], CELL_3D_X[vecIndex111], xs); + ly1x = lerp(CELL_3D_Y[vecIndex011], CELL_3D_Y[vecIndex111], xs); + lz1x = lerp(CELL_3D_Z[vecIndex011], CELL_3D_Z[vecIndex111], xs); v3.x += lerp(lx0y, lerp(lx0x, lx1x, ys), zs) * perturbAmp; v3.y += lerp(ly0y, lerp(ly0x, ly1x, ys), zs) * perturbAmp; @@ -1907,17 +1995,17 @@ public class FastNoiseDouble { } } - Double2 vec0 = CELL_2D[(int) hash2D(seed, x0, y0) & 255]; - Double2 vec1 = CELL_2D[(int) hash2D(seed, x1, y0) & 255]; + int vecIndex00 = (int) hash2D(seed, x0, y0) & 255; + int vecIndex10 = (int) hash2D(seed, x1, y0) & 255; - double lx0x = lerp(vec0.x, vec1.x, xs); - double ly0x = lerp(vec0.y, vec1.y, xs); + double lx0x = lerp(CELL_2D_X[vecIndex00], CELL_2D_X[vecIndex10], xs); + double ly0x = lerp(CELL_2D_Y[vecIndex00], CELL_2D_Y[vecIndex10], xs); - vec0 = CELL_2D[(int) hash2D(seed, x0, y1) & 255]; - vec1 = CELL_2D[(int) hash2D(seed, x1, y1) & 255]; + int vecIndex01 = (int) hash2D(seed, x0, y1) & 255; + int vecIndex11 = (int) hash2D(seed, x1, y1) & 255; - double lx1x = lerp(vec0.x, vec1.x, xs); - double ly1x = lerp(vec0.y, vec1.y, xs); + double lx1x = lerp(CELL_2D_X[vecIndex01], CELL_2D_X[vecIndex11], xs); + double ly1x = lerp(CELL_2D_Y[vecIndex01], CELL_2D_Y[vecIndex11], xs); v2.x += lerp(lx0x, lx1x, ys) * perturbAmp; v2.y += lerp(ly0x, ly1x, ys) * perturbAmp; @@ -1986,4 +2074,4 @@ public class FastNoiseDouble { } } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/volmit/iris/util/noise/FlatNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/FlatNoise.java similarity index 96% rename from core/src/main/java/com/volmit/iris/util/noise/FlatNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/FlatNoise.java index e7a92b8d8..611192808 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/FlatNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/FlatNoise.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; public class FlatNoise implements NoiseGenerator { public FlatNoise(long seed) { diff --git a/core/src/main/java/com/volmit/iris/util/noise/FractalBillowPerlinNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/FractalBillowPerlinNoise.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/noise/FractalBillowPerlinNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/FractalBillowPerlinNoise.java index 06c92b12f..65e205fbf 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/FractalBillowPerlinNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/FractalBillowPerlinNoise.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.FastNoiseDouble.FractalType; +package art.arcane.iris.util.project.noise; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.FastNoiseDouble.FractalType; public class FractalBillowPerlinNoise implements NoiseGenerator, OctaveNoise { private final FastNoiseDouble n; diff --git a/core/src/main/java/com/volmit/iris/util/noise/FractalBillowSimplexNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/FractalBillowSimplexNoise.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/noise/FractalBillowSimplexNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/FractalBillowSimplexNoise.java index 1641bb4b7..aa05c56df 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/FractalBillowSimplexNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/FractalBillowSimplexNoise.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.FastNoiseDouble.FractalType; +package art.arcane.iris.util.project.noise; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.FastNoiseDouble.FractalType; public class FractalBillowSimplexNoise implements NoiseGenerator, OctaveNoise { private final FastNoiseDouble n; diff --git a/core/src/main/java/com/volmit/iris/util/noise/FractalCubicNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/FractalCubicNoise.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/noise/FractalCubicNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/FractalCubicNoise.java index 76db22118..65ec7e0f3 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/FractalCubicNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/FractalCubicNoise.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.FastNoiseDouble.FractalType; +package art.arcane.iris.util.project.noise; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.FastNoiseDouble.FractalType; public class FractalCubicNoise implements NoiseGenerator { private final FastNoiseDouble n; diff --git a/core/src/main/java/com/volmit/iris/util/noise/FractalFBMSimplexNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/FractalFBMSimplexNoise.java similarity index 75% rename from core/src/main/java/com/volmit/iris/util/noise/FractalFBMSimplexNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/FractalFBMSimplexNoise.java index af3e68d64..961e2612b 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/FractalFBMSimplexNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/FractalFBMSimplexNoise.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.FastNoiseDouble.FractalType; +package art.arcane.iris.util.project.noise; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.FastNoiseDouble.FractalType; public class FractalFBMSimplexNoise implements NoiseGenerator, OctaveNoise { private final FastNoiseDouble n; @@ -39,16 +38,31 @@ public class FractalFBMSimplexNoise implements NoiseGenerator, OctaveNoise { return f(n.GetSimplexFractal(x, 0d)); } + @Override + public double noiseSigned(double x) { + return n.GetSimplexFractal(x, 0D); + } + @Override public double noise(double x, double z) { return f(n.GetSimplexFractal(x, z)); } + @Override + public double noiseSigned(double x, double z) { + return n.GetSimplexFractal(x, z); + } + @Override public double noise(double x, double y, double z) { return f(n.GetSimplexFractal(x, y, z)); } + @Override + public double noiseSigned(double x, double y, double z) { + return n.GetSimplexFractal(x, y, z); + } + @Override public void setOctaves(int o) { n.setFractalOctaves(o); diff --git a/core/src/main/java/com/volmit/iris/util/noise/FractalRigidMultiSimplexNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/FractalRigidMultiSimplexNoise.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/noise/FractalRigidMultiSimplexNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/FractalRigidMultiSimplexNoise.java index 150975da2..896091156 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/FractalRigidMultiSimplexNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/FractalRigidMultiSimplexNoise.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.FastNoiseDouble.FractalType; +package art.arcane.iris.util.project.noise; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.FastNoiseDouble.FractalType; public class FractalRigidMultiSimplexNoise implements NoiseGenerator, OctaveNoise { private final FastNoiseDouble n; diff --git a/core/src/main/java/com/volmit/iris/util/noise/GlobNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/GlobNoise.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/noise/GlobNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/GlobNoise.java index 0443f51c1..aedcfbb1a 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/GlobNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/GlobNoise.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.util.math.RNG; +import art.arcane.volmlib.util.math.RNG; public class GlobNoise implements NoiseGenerator { private final FastNoiseDouble n; diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/HexJamesNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/HexJamesNoise.java new file mode 100644 index 000000000..77542bb50 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/HexJamesNoise.java @@ -0,0 +1,295 @@ +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.math.RNG; + +public class HexJamesNoise implements NoiseGenerator, OctaveNoise { + private static final double SQRT_3_OVER_3 = Math.sqrt(3.0) / 3.0; + private static final double TWO_OVER_THREE = 2.0 / 3.0; + private static final double SQRT_3 = Math.sqrt(3.0); + private static final double THREE_OVER_TWO = 1.5D; + private static final double ONE_THIRD = 1D / 3D; + private static final double TWO_THIRDS = 2D / 3D; + private static final int MAX_DEPTH = 7; + private static final double HEAT_SCALE = 0.31D; + private static final double LARGE_CONTINUE = 0.26D; + private static final double SMALL_CONTINUE = 0.79D; + private static final double CENTER_CONTINUE = 0.58D; + private static final long CONST_X = 0x9E3779B97F4A7C15L; + private static final long CONST_Z = 0xC2B2AE3D27D4EB4FL; + private static final long[][] CHILD_DIRECTIONS = new long[][]{ + {1L, 0L}, + {1L, -1L}, + {0L, -1L}, + {-1L, 0L}, + {-1L, 1L}, + {0L, 1L} + }; + private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(HexScratch::new); + private final long seed; + private final SimplexNoise heatSimplex; + private int octaves; + + public HexJamesNoise(long seed) { + RNG rng = new RNG(seed); + this.seed = rng.lmax(); + this.heatSimplex = new SimplexNoise(rng.nextParallelRNG(877L).lmax()); + this.octaves = 1; + } + + private double clip(double value) { + if (value < 0D) { + return 0D; + } + + if (value > 1D) { + return 1D; + } + + return value; + } + + private long mix(long input) { + input = (input ^ (input >>> 33)) * 0xff51afd7ed558ccdL; + input = (input ^ (input >>> 33)) * 0xc4ceb9fe1a85ec53L; + return input ^ (input >>> 33); + } + + private double hashToUnit(long value) { + long normalized = mix(value) & Long.MAX_VALUE; + return normalized / (double) Long.MAX_VALUE; + } + + private double random01(long nodeHash, int salt) { + long mixed = nodeHash ^ (CONST_X * (salt + 1L)) ^ (CONST_Z * (salt + 7L)); + return hashToUnit(mixed); + } + + private long nextNodeHash(long nodeHash, int childIndex, int level) { + long mixed = nodeHash ^ (CONST_X * (childIndex + 11L)) ^ (CONST_Z * (level + 1L)); + return mix(mixed); + } + + private long[] roundAxial(double q, double r) { + double cubeX = q; + double cubeZ = r; + double cubeY = -cubeX - cubeZ; + long roundedX = Math.round(cubeX); + long roundedY = Math.round(cubeY); + long roundedZ = Math.round(cubeZ); + double xDiff = Math.abs(roundedX - cubeX); + double yDiff = Math.abs(roundedY - cubeY); + double zDiff = Math.abs(roundedZ - cubeZ); + + if (xDiff > yDiff && xDiff > zDiff) { + roundedX = -roundedY - roundedZ; + } else if (yDiff > zDiff) { + roundedY = -roundedX - roundedZ; + } else { + roundedZ = -roundedX - roundedY; + } + + return new long[]{roundedX, roundedZ}; + } + + private double hexDistance(double q, double r, double centerQ, double centerR) { + double deltaQ = q - centerQ; + double deltaR = r - centerR; + double deltaY = -deltaQ - deltaR; + return Math.max(Math.abs(deltaQ), Math.max(Math.abs(deltaR), Math.abs(deltaY))); + } + + private double axialX(double q, double r) { + return SQRT_3 * (q + (r * 0.5D)); + } + + private double axialZ(double r) { + return THREE_OVER_TWO * r; + } + + private double simplexField(double x, double z, double scale) { + if (octaves <= 1) { + return heatSimplex.noise(x * scale, z * scale); + } + + double frequency = 1D; + double amplitude = 1D; + double total = 0D; + double max = 0D; + + for (int i = 0; i < octaves; i++) { + total += heatSimplex.noise((x * scale) * frequency, (z * scale) * frequency) * amplitude; + max += amplitude; + frequency *= 2D; + amplitude *= 0.5D; + } + + if (max == 0D) { + return 0.5D; + } + + return clip(total / max); + } + + private double cellHeat(double centerQ, double centerR, long nodeHash, int level) { + double x = axialX(centerQ, centerR); + double z = axialZ(centerR); + double scale = HEAT_SCALE * (1D + (level * 0.24D)); + double simplexValue = simplexField(x, z, scale); + double randomValue = random01(nodeHash, 31 + level); + return clip((simplexValue * 0.82D) + (randomValue * 0.18D)); + } + + private int rotationForNode(long nodeHash) { + int rotation = (int) Math.floor(random01(nodeHash, 3) * 6D); + if (rotation < 0) { + rotation = 0; + } + + if (rotation > 5) { + rotation = 5; + } + + return rotation; + } + + private int parityForNode(long nodeHash) { + return random01(nodeHash, 5) >= 0.5D ? 1 : 0; + } + + private int ringIndexForDirection(int direction, int rotation) { + int ring = direction - rotation; + + while (ring < 0) { + ring += 6; + } + + while (ring >= 6) { + ring -= 6; + } + + return ring; + } + + private int pickChildIndex(double localQ, double localR, double[] centersQ, double[] centersR) { + int bestIndex = -1; + double bestDistance = Double.POSITIVE_INFINITY; + + centersQ[0] = 0D; + centersR[0] = 0D; + double centerDistance = hexDistance(localQ, localR, 0D, 0D) / ONE_THIRD; + + if (centerDistance <= 1D && centerDistance < bestDistance) { + bestDistance = centerDistance; + bestIndex = 0; + } + + for (int i = 0; i < 6; i++) { + int index = i + 1; + double centerQ = CHILD_DIRECTIONS[i][0] * TWO_THIRDS; + double centerR = CHILD_DIRECTIONS[i][1] * TWO_THIRDS; + centersQ[index] = centerQ; + centersR[index] = centerR; + double distance = hexDistance(localQ, localR, centerQ, centerR) / ONE_THIRD; + + if (distance <= 1D && distance < bestDistance) { + bestDistance = distance; + bestIndex = index; + } + } + + return bestIndex; + } + + private boolean shouldContinue(int level, int childIndex, long nodeHash) { + if (level >= MAX_DEPTH - 1) { + return false; + } + + double gate; + + if (childIndex == 0) { + gate = CENTER_CONTINUE; + } else { + int rotation = rotationForNode(nodeHash); + int parity = parityForNode(nodeHash); + int ringIndex = ringIndexForDirection(childIndex - 1, rotation); + boolean large = (ringIndex % 2) == parity; + gate = large ? LARGE_CONTINUE : SMALL_CONTINUE; + } + + double randomValue = random01(nodeHash, 97 + level); + return randomValue <= gate; + } + + private double sample(double x, double z) { + HexScratch scratch = SCRATCH.get(); + double[] centersQ = scratch.centersQ; + double[] centersR = scratch.centersR; + double qWorld = (SQRT_3_OVER_3 * x) - (z / 3.0); + double rWorld = TWO_OVER_THREE * z; + long[] rounded = roundAxial(qWorld, rWorld); + double centerQ = rounded[0]; + double centerR = rounded[1]; + double radius = 0.5D; + double localQ = (qWorld - centerQ) / radius; + double localR = (rWorld - centerR) / radius; + long nodeHash = mix(seed ^ (rounded[0] * CONST_X) ^ (rounded[1] * CONST_Z)); + double heat = cellHeat(centerQ, centerR, nodeHash, 0); + + for (int level = 0; level < MAX_DEPTH; level++) { + int childIndex = pickChildIndex(localQ, localR, centersQ, centersR); + + if (childIndex < 0) { + return heat; + } + + double childCenterQ = centersQ[childIndex]; + double childCenterR = centersR[childIndex]; + centerQ += childCenterQ * radius; + centerR += childCenterR * radius; + radius *= ONE_THIRD; + localQ = (localQ - childCenterQ) / ONE_THIRD; + localR = (localR - childCenterR) / ONE_THIRD; + nodeHash = nextNodeHash(nodeHash, childIndex, level); + heat = cellHeat(centerQ, centerR, nodeHash, level + 1); + + if (!shouldContinue(level, childIndex, nodeHash)) { + return heat; + } + } + + return heat; + } + + @Override + public double noise(double x) { + return sample(x, 0D); + } + + @Override + public double noise(double x, double z) { + return sample(x, z); + } + + @Override + public double noise(double x, double y, double z) { + if (z == 0D) { + return sample(x, y); + } + + double a = sample(x, z); + double b = sample(y, x); + double c = sample(z, y); + return (a + b + c) / 3D; + } + + @Override + public void setOctaves(int o) { + this.octaves = Math.max(1, o); + } + + private static final class HexScratch { + private final double[] centersQ = new double[7]; + private final double[] centersR = new double[7]; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/HexRandomSizeNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/HexRandomSizeNoise.java new file mode 100644 index 000000000..9b034946f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/HexRandomSizeNoise.java @@ -0,0 +1,260 @@ +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.math.RNG; + +public class HexRandomSizeNoise implements NoiseGenerator, OctaveNoise { + private static final double SQRT_3_OVER_3 = Math.sqrt(3.0) / 3.0; + private static final double TWO_OVER_THREE = 2.0 / 3.0; + private static final double SQRT_3 = Math.sqrt(3.0); + private static final double THREE_OVER_TWO = 1.5D; + private static final double ONE_THIRD = 1D / 3D; + private static final double TWO_THIRDS = 2D / 3D; + private static final int MAX_DEPTH = 8; + private static final double HEAT_SCALE = 0.24D; + private static final double STRUCTURE_SCALE = 0.36D; + private static final double SUBDIVIDE_BASE_THRESHOLD = 0.22D; + private static final double SUBDIVIDE_LEVEL_STEP = 0.11D; + private static final long CONST_X = 0x9E3779B97F4A7C15L; + private static final long CONST_Z = 0xC2B2AE3D27D4EB4FL; + private static final long[][] CHILD_DIRECTIONS = new long[][]{ + {1L, 0L}, + {1L, -1L}, + {0L, -1L}, + {-1L, 0L}, + {-1L, 1L}, + {0L, 1L} + }; + private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(HexScratch::new); + private final long seed; + private final SimplexNoise heatSimplex; + private final SimplexNoise structureSimplex; + private int octaves; + + public HexRandomSizeNoise(long seed) { + RNG rng = new RNG(seed); + this.seed = rng.lmax(); + this.heatSimplex = new SimplexNoise(rng.nextParallelRNG(221L).lmax()); + this.structureSimplex = new SimplexNoise(rng.nextParallelRNG(442L).lmax()); + this.octaves = 1; + } + + private double clip(double value) { + if (value < 0D) { + return 0D; + } + + if (value > 1D) { + return 1D; + } + + return value; + } + + private long mix(long input) { + input = (input ^ (input >>> 33)) * 0xff51afd7ed558ccdL; + input = (input ^ (input >>> 33)) * 0xc4ceb9fe1a85ec53L; + return input ^ (input >>> 33); + } + + private double hashToUnit(long value) { + long normalized = mix(value) & Long.MAX_VALUE; + return normalized / (double) Long.MAX_VALUE; + } + + private double random01(long nodeHash, int salt) { + long mixed = nodeHash ^ (CONST_X * (salt + 1L)) ^ (CONST_Z * (salt + 31L)); + return hashToUnit(mixed); + } + + private long nextNodeHash(long nodeHash, int childIndex, int level) { + long mixed = nodeHash ^ (CONST_Z * (childIndex + 1L)) ^ (CONST_X * (level + 1L)); + return mix(mixed); + } + + private long[] roundAxial(double q, double r) { + double cubeX = q; + double cubeZ = r; + double cubeY = -cubeX - cubeZ; + long roundedX = Math.round(cubeX); + long roundedY = Math.round(cubeY); + long roundedZ = Math.round(cubeZ); + double xDiff = Math.abs(roundedX - cubeX); + double yDiff = Math.abs(roundedY - cubeY); + double zDiff = Math.abs(roundedZ - cubeZ); + + if (xDiff > yDiff && xDiff > zDiff) { + roundedX = -roundedY - roundedZ; + } else if (yDiff > zDiff) { + roundedY = -roundedX - roundedZ; + } else { + roundedZ = -roundedX - roundedY; + } + + return new long[]{roundedX, roundedZ}; + } + + private double hexDistance(double q, double r, double centerQ, double centerR) { + double deltaQ = q - centerQ; + double deltaR = r - centerR; + double deltaY = -deltaQ - deltaR; + return Math.max(Math.abs(deltaQ), Math.max(Math.abs(deltaR), Math.abs(deltaY))); + } + + private double axialX(double q, double r) { + return SQRT_3 * (q + (r * 0.5D)); + } + + private double axialZ(double r) { + return THREE_OVER_TWO * r; + } + + private double simplexField(SimplexNoise simplex, double x, double z, double scale) { + if (octaves <= 1) { + return simplex.noise(x * scale, z * scale); + } + + double frequency = 1D; + double amplitude = 1D; + double total = 0D; + double max = 0D; + + for (int i = 0; i < octaves; i++) { + total += simplex.noise((x * scale) * frequency, (z * scale) * frequency) * amplitude; + max += amplitude; + frequency *= 2D; + amplitude *= 0.5D; + } + + if (max == 0D) { + return 0.5D; + } + + return clip(total / max); + } + + private double cellHeat(double centerQ, double centerR, long nodeHash, int level) { + double x = axialX(centerQ, centerR); + double z = axialZ(centerR); + double scale = HEAT_SCALE * (1D + (level * 0.2D)); + double simplexValue = simplexField(heatSimplex, x, z, scale); + double randomValue = random01(nodeHash, 11 + level); + return clip((simplexValue * 0.74D) + (randomValue * 0.26D)); + } + + private boolean shouldSubdivide(double centerQ, double centerR, long nodeHash, int level) { + if (level >= MAX_DEPTH - 1) { + return false; + } + + double x = axialX(centerQ, centerR); + double z = axialZ(centerR); + double scale = STRUCTURE_SCALE * (1D + (level * 0.31D)); + double simplexValue = simplexField(structureSimplex, x, z, scale); + double randomValue = random01(nodeHash, 71 + level); + double score = clip((simplexValue * 0.68D) + (randomValue * 0.32D)); + double threshold = SUBDIVIDE_BASE_THRESHOLD + (level * SUBDIVIDE_LEVEL_STEP); + return score > threshold; + } + + private int pickChildIndex(double localQ, double localR, double[] centersQ, double[] centersR) { + int bestIndex = -1; + double bestDistance = Double.POSITIVE_INFINITY; + + centersQ[0] = 0D; + centersR[0] = 0D; + double centerDistance = hexDistance(localQ, localR, 0D, 0D) / ONE_THIRD; + + if (centerDistance <= 1D && centerDistance < bestDistance) { + bestDistance = centerDistance; + bestIndex = 0; + } + + for (int i = 0; i < 6; i++) { + int index = i + 1; + double centerQ = CHILD_DIRECTIONS[i][0] * TWO_THIRDS; + double centerR = CHILD_DIRECTIONS[i][1] * TWO_THIRDS; + centersQ[index] = centerQ; + centersR[index] = centerR; + double distance = hexDistance(localQ, localR, centerQ, centerR) / ONE_THIRD; + + if (distance <= 1D && distance < bestDistance) { + bestDistance = distance; + bestIndex = index; + } + } + + return bestIndex; + } + + private double sample(double x, double z) { + HexScratch scratch = SCRATCH.get(); + double[] centersQ = scratch.centersQ; + double[] centersR = scratch.centersR; + double qWorld = (SQRT_3_OVER_3 * x) - (z / 3.0); + double rWorld = TWO_OVER_THREE * z; + long[] rounded = roundAxial(qWorld, rWorld); + double centerQ = rounded[0]; + double centerR = rounded[1]; + double radius = 0.5D; + double localQ = (qWorld - centerQ) / radius; + double localR = (rWorld - centerR) / radius; + long nodeHash = mix(seed ^ (rounded[0] * CONST_X) ^ (rounded[1] * CONST_Z)); + double heat = cellHeat(centerQ, centerR, nodeHash, 0); + + for (int level = 0; level < MAX_DEPTH; level++) { + if (!shouldSubdivide(centerQ, centerR, nodeHash, level)) { + return heat; + } + + int childIndex = pickChildIndex(localQ, localR, centersQ, centersR); + + if (childIndex < 0) { + return heat; + } + + double childCenterQ = centersQ[childIndex]; + double childCenterR = centersR[childIndex]; + centerQ += childCenterQ * radius; + centerR += childCenterR * radius; + radius *= ONE_THIRD; + localQ = (localQ - childCenterQ) / ONE_THIRD; + localR = (localR - childCenterR) / ONE_THIRD; + nodeHash = nextNodeHash(nodeHash, childIndex, level); + heat = cellHeat(centerQ, centerR, nodeHash, level + 1); + } + + return heat; + } + + @Override + public double noise(double x) { + return sample(x, 0D); + } + + @Override + public double noise(double x, double z) { + return sample(x, z); + } + + @Override + public double noise(double x, double y, double z) { + if (z == 0D) { + return sample(x, y); + } + + double a = sample(x, z); + double b = sample(y, x); + double c = sample(z, y); + return (a + b + c) / 3D; + } + + @Override + public void setOctaves(int o) { + this.octaves = Math.max(1, o); + } + + private static final class HexScratch { + private final double[] centersQ = new double[7]; + private final double[] centersR = new double[7]; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/HexSimplexNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/HexSimplexNoise.java new file mode 100644 index 000000000..554896d16 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/HexSimplexNoise.java @@ -0,0 +1,125 @@ +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.math.RNG; + +public class HexSimplexNoise implements NoiseGenerator, OctaveNoise { + private static final double SQRT_3_OVER_3 = Math.sqrt(3.0) / 3.0; + private static final double TWO_OVER_THREE = 2.0 / 3.0; + private static final double SQRT_3 = Math.sqrt(3.0); + private static final double THREE_OVER_TWO = 1.5D; + private static final double CELL_HEAT_SCALE = 2.7D; + private final SimplexNoise cellHeatSimplex; + private int octaves; + + public HexSimplexNoise(long seed) { + RNG rng = new RNG(seed); + this.cellHeatSimplex = new SimplexNoise(rng.nextParallelRNG(811L).lmax()); + this.octaves = 1; + } + + private double clip(double value) { + if (value < 0D) { + return 0D; + } + + if (value > 1D) { + return 1D; + } + + return value; + } + + private long[] roundAxial(double q, double r) { + double cubeX = q; + double cubeZ = r; + double cubeY = -cubeX - cubeZ; + long roundedX = Math.round(cubeX); + long roundedY = Math.round(cubeY); + long roundedZ = Math.round(cubeZ); + double xDiff = Math.abs(roundedX - cubeX); + double yDiff = Math.abs(roundedY - cubeY); + double zDiff = Math.abs(roundedZ - cubeZ); + + if (xDiff > yDiff && xDiff > zDiff) { + roundedX = -roundedY - roundedZ; + } else if (yDiff > zDiff) { + roundedY = -roundedX - roundedZ; + } else { + roundedZ = -roundedX - roundedY; + } + + return new long[]{roundedX, roundedZ}; + } + + private double axialX(long q, long r) { + return SQRT_3 * (q + (r * 0.5D)); + } + + private double axialZ(long r) { + return THREE_OVER_TWO * r; + } + + private double cellHeat(long q, long r, double frequency) { + double x = axialX(q, r); + double z = axialZ(r); + return cellHeatSimplex.noise((x * CELL_HEAT_SCALE) * frequency, (z * CELL_HEAT_SCALE) * frequency); + } + + private double sampleCell(long q, long r) { + if (octaves <= 1) { + return cellHeat(q, r, 1D); + } + + double frequency = 1D; + double amplitude = 1D; + double total = 0D; + double max = 0D; + + for (int i = 0; i < octaves; i++) { + total += cellHeat(q, r, frequency) * amplitude; + max += amplitude; + frequency *= 2D; + amplitude *= 0.5D; + } + + if (max == 0D) { + return 0.5D; + } + + return clip(total / max); + } + + private double sample(double x, double z) { + double q = (SQRT_3_OVER_3 * x) - (z / 3.0); + double r = TWO_OVER_THREE * z; + long[] rounded = roundAxial(q, r); + return sampleCell(rounded[0], rounded[1]); + } + + @Override + public double noise(double x) { + return sample(x, 0D); + } + + @Override + public double noise(double x, double z) { + return sample(x, z); + } + + @Override + public double noise(double x, double y, double z) { + if (z == 0D) { + return sample(x, y); + } + + double a = sample(x, z); + double b = sample(y, x); + double c = sample(z, y); + return (a + b + c) / 3D; + } + + @Override + public void setOctaves(int o) { + this.octaves = Math.max(1, o); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/HexagonNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/HexagonNoise.java new file mode 100644 index 000000000..0f3181d3d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/HexagonNoise.java @@ -0,0 +1,83 @@ +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.math.RNG; + +public class HexagonNoise implements NoiseGenerator { + private static final double SQRT_3_OVER_3 = Math.sqrt(3.0) / 3.0; + private static final double TWO_OVER_THREE = 2.0 / 3.0; + private static final long CONST_X = 0x9E3779B97F4A7C15L; + private static final long CONST_Z = 0xC2B2AE3D27D4EB4FL; + private final long seed; + + public HexagonNoise(long seed) { + this.seed = new RNG(seed).lmax(); + } + + private long mix(long input) { + input = (input ^ (input >>> 33)) * 0xff51afd7ed558ccdL; + input = (input ^ (input >>> 33)) * 0xc4ceb9fe1a85ec53L; + return input ^ (input >>> 33); + } + + private long hash(long q, long r) { + long hash = seed; + hash ^= mix(q + CONST_X); + hash ^= mix(r + CONST_Z); + return mix(hash); + } + + private long[] roundAxial(double q, double r) { + double cubeX = q; + double cubeZ = r; + double cubeY = -cubeX - cubeZ; + + long rx = Math.round(cubeX); + long ry = Math.round(cubeY); + long rz = Math.round(cubeZ); + + double xDiff = Math.abs(rx - cubeX); + double yDiff = Math.abs(ry - cubeY); + double zDiff = Math.abs(rz - cubeZ); + + if (xDiff > yDiff && xDiff > zDiff) { + rx = -ry - rz; + } else if (yDiff > zDiff) { + ry = -rx - rz; + } else { + rz = -rx - ry; + } + + return new long[]{rx, rz}; + } + + private double sampleCell(double x, double z) { + double q = (SQRT_3_OVER_3 * x) - (z / 3.0); + double r = TWO_OVER_THREE * z; + long[] axial = roundAxial(q, r); + long hashed = hash(axial[0], axial[1]); + long normalized = hashed & Long.MAX_VALUE; + return normalized / (double) Long.MAX_VALUE; + } + + @Override + public double noise(double x) { + return sampleCell(x, 0D); + } + + @Override + public double noise(double x, double z) { + return sampleCell(x, z); + } + + @Override + public double noise(double x, double y, double z) { + if (z == 0D) { + return sampleCell(x, y); + } + + double a = sampleCell(x, z); + double b = sampleCell(y, x); + double c = sampleCell(z, y); + return (a + b + c) / 3D; + } +} diff --git a/core/src/main/java/com/volmit/iris/util/noise/ImageNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/ImageNoise.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/noise/ImageNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/ImageNoise.java index 2bd790fd5..31a66571e 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/ImageNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/ImageNoise.java @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.object.IrisImageMap; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisImageMap; public class ImageNoise implements NoiseGenerator { private final IrisImageMap expression; diff --git a/core/src/main/java/com/volmit/iris/util/noise/InterpolatedNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/InterpolatedNoise.java similarity index 86% rename from core/src/main/java/com/volmit/iris/util/noise/InterpolatedNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/InterpolatedNoise.java index f146fa483..0991b458a 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/InterpolatedNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/InterpolatedNoise.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.function.NoiseProvider; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.interpolation.IrisInterpolation; +package art.arcane.iris.util.project.noise; +import art.arcane.volmlib.util.function.NoiseProvider; +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; public class InterpolatedNoise implements NoiseGenerator { private final InterpolationMethod method; private final NoiseProvider p; diff --git a/core/src/main/java/com/volmit/iris/util/noise/NoiseFactory.java b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseFactory.java similarity index 95% rename from core/src/main/java/com/volmit/iris/util/noise/NoiseFactory.java rename to core/src/main/java/art/arcane/iris/util/project/noise/NoiseFactory.java index fac5399d4..222669cf8 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/NoiseFactory.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseFactory.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; @FunctionalInterface public interface NoiseFactory { diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/NoiseGenerator.java b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseGenerator.java new file mode 100644 index 000000000..cf01f4e7a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseGenerator.java @@ -0,0 +1,57 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.noise; + +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; +public interface NoiseGenerator { + double noise(double x); + + double noise(double x, double z); + + double noise(double x, double y, double z); + + default double noiseSigned(double x) { + return (noise(x) * 2D) - 1D; + } + + default double noiseSigned(double x, double z) { + return (noise(x, z) * 2D) - 1D; + } + + default double noiseSigned(double x, double y, double z) { + return (noise(x, y, z) * 2D) - 1D; + } + + default boolean isStatic() { + return false; + } + + default boolean isNoScale() { + return false; + } + + default ProceduralStream stream() { + return ProceduralStream.of(this::noise, this::noise, Interpolated.DOUBLE); + } + + default OffsetNoiseGenerator offset(long seed) { + return new OffsetNoiseGenerator(this, seed); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java new file mode 100644 index 000000000..1d0cc2a78 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java @@ -0,0 +1,101 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.noise; + +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +public enum NoiseType { + WHITE(WhiteNoise::new), + WHITE_BILINEAR((s) -> new InterpolatedNoise(s, WHITE, InterpolationMethod.BILINEAR)), + WHITE_BICUBIC((s) -> new InterpolatedNoise(s, WHITE, InterpolationMethod.BICUBIC)), + WHITE_HERMITE((s) -> new InterpolatedNoise(s, WHITE, InterpolationMethod.HERMITE)), + SIMPLEX(SimplexNoise::new), + PERLIN(seed -> new PerlinNoise(seed).hermite()), + FRACTAL_BILLOW_SIMPLEX(FractalBillowSimplexNoise::new), + FRACTAL_BILLOW_PERLIN(FractalBillowPerlinNoise::new), + FRACTAL_FBM_SIMPLEX(FractalFBMSimplexNoise::new), + FRACTAL_RIGID_MULTI_SIMPLEX(FractalRigidMultiSimplexNoise::new), + FLAT(FlatNoise::new), + CELLULAR(CellularNoise::new), + CELLULAR_BILINEAR((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR)), + CELLULAR_BILINEAR_STARCAST_3((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR_STARCAST_3)), + CELLULAR_BILINEAR_STARCAST_6((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR_STARCAST_6)), + CELLULAR_BILINEAR_STARCAST_9((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR_STARCAST_9)), + CELLULAR_BILINEAR_STARCAST_12((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR_STARCAST_12)), + CELLULAR_BICUBIC((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BICUBIC)), + CELLULAR_HERMITE((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE)), + CELLULAR_STARCAST_3((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.STARCAST_3)), + CELLULAR_STARCAST_6((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.STARCAST_6)), + CELLULAR_STARCAST_9((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.STARCAST_9)), + CELLULAR_STARCAST_12((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.STARCAST_12)), + CELLULAR_HERMITE_STARCAST_3((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE_STARCAST_3)), + CELLULAR_HERMITE_STARCAST_6((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE_STARCAST_6)), + CELLULAR_HERMITE_STARCAST_9((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE_STARCAST_9)), + CELLULAR_HERMITE_STARCAST_12((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE_STARCAST_12)), + GLOB(GlobNoise::new), + CUBIC(CubicNoise::new), + FRACTAL_CUBIC(FractalCubicNoise::new), + CELLULAR_HEIGHT(CellHeightNoise::new), + CLOVER(CloverNoise::new), + CLOVER_BILINEAR((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR)), + CLOVER_BILINEAR_STARCAST_3((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR_STARCAST_3)), + CLOVER_BILINEAR_STARCAST_6((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR_STARCAST_6)), + CLOVER_BILINEAR_STARCAST_9((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR_STARCAST_9)), + CLOVER_BILINEAR_STARCAST_12((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR_STARCAST_12)), + CLOVER_BICUBIC((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BICUBIC)), + CLOVER_HERMITE((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE)), + CLOVER_STARCAST_3((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.STARCAST_3)), + CLOVER_STARCAST_6((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.STARCAST_6)), + CLOVER_STARCAST_9((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.STARCAST_9)), + CLOVER_STARCAST_12((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.STARCAST_12)), + CLOVER_HERMITE_STARCAST_3((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE_STARCAST_3)), + CLOVER_HERMITE_STARCAST_6((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE_STARCAST_6)), + CLOVER_HERMITE_STARCAST_9((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE_STARCAST_9)), + CLOVER_HERMITE_STARCAST_12((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE_STARCAST_12)), + HEXAGON(HexagonNoise::new), + HEXAGON_BILINEAR((s) -> new InterpolatedNoise(s, HEXAGON, InterpolationMethod.BILINEAR)), + HEXAGON_BICUBIC((s) -> new InterpolatedNoise(s, HEXAGON, InterpolationMethod.BICUBIC)), + HEXAGON_HERMITE((s) -> new InterpolatedNoise(s, HEXAGON, InterpolationMethod.HERMITE)), + HEX_JAMES(HexJamesNoise::new), + HEX_JAMES_BILINEAR((s) -> new InterpolatedNoise(s, HEX_JAMES, InterpolationMethod.BILINEAR)), + HEX_JAMES_BICUBIC((s) -> new InterpolatedNoise(s, HEX_JAMES, InterpolationMethod.BICUBIC)), + HEX_JAMES_HERMITE((s) -> new InterpolatedNoise(s, HEX_JAMES, InterpolationMethod.HERMITE)), + HEX_SIMPLEX(HexSimplexNoise::new), + HEX_SIMPLEX_BILINEAR((s) -> new InterpolatedNoise(s, HEX_SIMPLEX, InterpolationMethod.BILINEAR)), + HEX_SIMPLEX_BICUBIC((s) -> new InterpolatedNoise(s, HEX_SIMPLEX, InterpolationMethod.BICUBIC)), + HEX_SIMPLEX_HERMITE((s) -> new InterpolatedNoise(s, HEX_SIMPLEX, InterpolationMethod.HERMITE)), + HEX_RANDOM_SIZE(HexRandomSizeNoise::new), + HEX_RANDOM_SIZE_BILINEAR((s) -> new InterpolatedNoise(s, HEX_RANDOM_SIZE, InterpolationMethod.BILINEAR)), + HEX_RANDOM_SIZE_BICUBIC((s) -> new InterpolatedNoise(s, HEX_RANDOM_SIZE, InterpolationMethod.BICUBIC)), + HEX_RANDOM_SIZE_HERMITE((s) -> new InterpolatedNoise(s, HEX_RANDOM_SIZE, InterpolationMethod.HERMITE)), + SIERPINSKI_TRIANGLE(SierpinskiTriangleNoise::new), + SIERPINSKI_TRIANGLE_BILINEAR((s) -> new InterpolatedNoise(s, SIERPINSKI_TRIANGLE, InterpolationMethod.BILINEAR)), + SIERPINSKI_TRIANGLE_BICUBIC((s) -> new InterpolatedNoise(s, SIERPINSKI_TRIANGLE, InterpolationMethod.BICUBIC)), + SIERPINSKI_TRIANGLE_HERMITE((s) -> new InterpolatedNoise(s, SIERPINSKI_TRIANGLE, InterpolationMethod.HERMITE)), + VASCULAR(VascularNoise::new); + + private final NoiseFactory f; + + NoiseType(NoiseFactory f) { + this.f = f; + } + + public NoiseGenerator create(long seed) { + return f.create(seed).offset(seed); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/noise/OctaveNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/OctaveNoise.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/noise/OctaveNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/OctaveNoise.java index a868f0738..6f3ebd8bd 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/OctaveNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/OctaveNoise.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; public interface OctaveNoise { void setOctaves(int o); diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/OffsetNoiseGenerator.java b/core/src/main/java/art/arcane/iris/util/project/noise/OffsetNoiseGenerator.java new file mode 100644 index 000000000..fb3185172 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/OffsetNoiseGenerator.java @@ -0,0 +1,119 @@ +package art.arcane.iris.util.project.noise; + + +import art.arcane.volmlib.util.math.RNG; +import org.jetbrains.annotations.NotNull; + + +public class OffsetNoiseGenerator implements NoiseGenerator { + private static final int GENERIC = 0; + private static final int SIMPLEX = 1; + private static final int PERLIN = 2; + private static final int FRACTAL_FBM_SIMPLEX = 3; + private static final int VASCULAR = 4; + private final NoiseGenerator base; + private final SimplexNoise simplexBase; + private final PerlinNoise perlinBase; + private final FractalFBMSimplexNoise fractalFbmSimplexBase; + private final VascularNoise vascularBase; + private final int dispatchKind; + private final double ox; + private final double oz; + + public OffsetNoiseGenerator(NoiseGenerator base, long seed) { + this.base = base; + this.simplexBase = base instanceof SimplexNoise simplex ? simplex : null; + this.perlinBase = base instanceof PerlinNoise perlin ? perlin : null; + this.fractalFbmSimplexBase = base instanceof FractalFBMSimplexNoise fractal ? fractal : null; + this.vascularBase = base instanceof VascularNoise vascular ? vascular : null; + this.dispatchKind = simplexBase != null ? SIMPLEX + : perlinBase != null ? PERLIN + : fractalFbmSimplexBase != null ? FRACTAL_FBM_SIMPLEX + : vascularBase != null ? VASCULAR + : GENERIC; + RNG rng = new RNG(seed); + ox = rng.nextInt(Short.MIN_VALUE, Short.MAX_VALUE); + oz = rng.nextInt(Short.MIN_VALUE, Short.MAX_VALUE); + } + + @Override + public double noise(double x) { + return switch (dispatchKind) { + case SIMPLEX -> simplexBase.noise(x + ox); + case PERLIN -> perlinBase.noise(x + ox); + case FRACTAL_FBM_SIMPLEX -> fractalFbmSimplexBase.noise(x + ox); + case VASCULAR -> vascularBase.noise(x + ox); + default -> base.noise(x + ox); + }; + } + + @Override + public double noiseSigned(double x) { + return switch (dispatchKind) { + case SIMPLEX -> simplexBase.noiseSigned(x + ox); + case PERLIN -> perlinBase.noiseSigned(x + ox); + case FRACTAL_FBM_SIMPLEX -> fractalFbmSimplexBase.noiseSigned(x + ox); + case VASCULAR -> vascularBase.noiseSigned(x + ox); + default -> base.noiseSigned(x + ox); + }; + } + + @Override + public double noise(double x, double z) { + return switch (dispatchKind) { + case SIMPLEX -> simplexBase.noise(x + ox, z + oz); + case PERLIN -> perlinBase.noise(x + ox, z + oz); + case FRACTAL_FBM_SIMPLEX -> fractalFbmSimplexBase.noise(x + ox, z + oz); + case VASCULAR -> vascularBase.noise(x + ox, z + oz); + default -> base.noise(x + ox, z + oz); + }; + } + + @Override + public double noiseSigned(double x, double z) { + return switch (dispatchKind) { + case SIMPLEX -> simplexBase.noiseSigned(x + ox, z + oz); + case PERLIN -> perlinBase.noiseSigned(x + ox, z + oz); + case FRACTAL_FBM_SIMPLEX -> fractalFbmSimplexBase.noiseSigned(x + ox, z + oz); + case VASCULAR -> vascularBase.noiseSigned(x + ox, z + oz); + default -> base.noiseSigned(x + ox, z + oz); + }; + } + + @Override + public double noise(double x, double y, double z) { + return switch (dispatchKind) { + case SIMPLEX -> simplexBase.noise(x + ox, y, z + oz); + case PERLIN -> perlinBase.noise(x + ox, y, z + oz); + case FRACTAL_FBM_SIMPLEX -> fractalFbmSimplexBase.noise(x + ox, y, z + oz); + case VASCULAR -> vascularBase.noise(x + ox, y, z + oz); + default -> base.noise(x + ox, y, z + oz); + }; + } + + @Override + public double noiseSigned(double x, double y, double z) { + return switch (dispatchKind) { + case SIMPLEX -> simplexBase.noiseSigned(x + ox, y, z + oz); + case PERLIN -> perlinBase.noiseSigned(x + ox, y, z + oz); + case FRACTAL_FBM_SIMPLEX -> fractalFbmSimplexBase.noiseSigned(x + ox, y, z + oz); + case VASCULAR -> vascularBase.noiseSigned(x + ox, y, z + oz); + default -> base.noiseSigned(x + ox, y, z + oz); + }; + } + + @Override + public boolean isNoScale() { + return base.isNoScale(); + } + + @Override + public boolean isStatic() { + return base.isStatic(); + } + + @NotNull + public NoiseGenerator getBase() { + return base; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/PerlinNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/PerlinNoise.java new file mode 100644 index 000000000..6cd80cefa --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/PerlinNoise.java @@ -0,0 +1,171 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.math.RNG; + +public class PerlinNoise implements NoiseGenerator, OctaveNoise { + private final FastNoiseDouble n; + private int octaves; + + public PerlinNoise(long seed) { + this.n = new FastNoiseDouble(new RNG(seed).lmax()); + octaves = 1; + } + + public double f(double v) { + return (v / 2D) + 0.5D; + } + + @Override + public double noise(double x) { + if (octaves <= 1) { + return f(n.GetPerlin(x, 0)); + } + + double f = 1; + double m = 0; + double v = 0; + + for (int i = 0; i < octaves; i++) { + v += n.GetPerlin((x * (f == 1 ? f++ : (f *= 2))), 0) * f; + m += f; + } + + return f(v / m); + } + + @Override + public double noiseSigned(double x) { + if (octaves <= 1) { + return n.GetPerlin(x, 0D); + } + + double frequency = 1D; + double magnitude = 0D; + double value = 0D; + + for (int i = 0; i < octaves; i++) { + double sampleFrequency; + if (frequency == 1D) { + sampleFrequency = frequency; + frequency = 2D; + } else { + frequency *= 2D; + sampleFrequency = frequency; + } + + value += n.GetPerlin(x * sampleFrequency, 0D) * frequency; + magnitude += frequency; + } + + return value / magnitude; + } + + @Override + public double noise(double x, double z) { + if (octaves <= 1) { + return f(n.GetPerlin(x, z)); + } + double f = 1; + double m = 0; + double v = 0; + + for (int i = 0; i < octaves; i++) { + f = f == 1 ? f + 1 : f * 2; + v += n.GetPerlin((x * f), (z * f)) * f; + m += f; + } + + return f(v / m); + } + + @Override + public double noiseSigned(double x, double z) { + if (octaves <= 1) { + return n.GetPerlin(x, z); + } + + double frequency = 1D; + double magnitude = 0D; + double value = 0D; + + for (int i = 0; i < octaves; i++) { + double octaveFrequency = frequency == 1D ? frequency + 1D : frequency * 2D; + value += n.GetPerlin(x * octaveFrequency, z * octaveFrequency) * octaveFrequency; + magnitude += octaveFrequency; + frequency = octaveFrequency; + } + + return value / magnitude; + } + + @Override + public double noise(double x, double y, double z) { + if (octaves <= 1) { + return f(n.GetPerlin(x, y, z)); + } + double f = 1; + double m = 0; + double v = 0; + + for (int i = 0; i < octaves; i++) { + f = f == 1 ? f + 1 : f * 2; + v += n.GetPerlin((x * f), (y * f), (z * f)) * f; + m += f; + } + + return f(v / m); + } + + @Override + public double noiseSigned(double x, double y, double z) { + if (octaves <= 1) { + return n.GetPerlin(x, y, z); + } + + double frequency = 1D; + double magnitude = 0D; + double value = 0D; + + for (int i = 0; i < octaves; i++) { + double octaveFrequency = frequency == 1D ? frequency + 1D : frequency * 2D; + value += n.GetPerlin(x * octaveFrequency, y * octaveFrequency, z * octaveFrequency) * octaveFrequency; + magnitude += octaveFrequency; + frequency = octaveFrequency; + } + + return value / magnitude; + } + + @Override + public void setOctaves(int o) { + octaves = o; + } + + public NoiseGenerator hermite() { + n.m_longerp = FastNoiseDouble.Longerp.Hermite; + return this; + } + + public NoiseGenerator quad() { + n.m_longerp = FastNoiseDouble.Longerp.Qulongic; + return this; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/SierpinskiTriangleNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/SierpinskiTriangleNoise.java new file mode 100644 index 000000000..fbf0fa6f6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/SierpinskiTriangleNoise.java @@ -0,0 +1,98 @@ +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.math.RNG; + +public class SierpinskiTriangleNoise implements NoiseGenerator, OctaveNoise { + private static final double TRI_SCALE = 0.18D; + private static final double TRI_PROJECTION = 0.8660254037844386D; + private static final double HEAT_SCALE = 0.33D; + private final SimplexNoise heatSimplex; + private int octaves; + + public SierpinskiTriangleNoise(long seed) { + RNG rng = new RNG(seed); + this.heatSimplex = new SimplexNoise(rng.nextParallelRNG(177L).lmax()); + this.octaves = 1; + } + + private double clip(double value) { + if (value < 0D) { + return 0D; + } + + if (value > 1D) { + return 1D; + } + + return value; + } + + private double heat(double x, double z) { + if (octaves <= 1) { + return heatSimplex.noise(x * HEAT_SCALE, z * HEAT_SCALE); + } + + double frequency = 1D; + double amplitude = 1D; + double total = 0D; + double max = 0D; + + for (int i = 0; i < octaves; i++) { + total += heatSimplex.noise((x * HEAT_SCALE) * frequency, (z * HEAT_SCALE) * frequency) * amplitude; + max += amplitude; + frequency *= 2D; + amplitude *= 0.5D; + } + + if (max == 0D) { + return 0.5D; + } + + return clip(total / max); + } + + private double mask(double x, double z) { + double sx = (x * TRI_SCALE) + ((z * TRI_SCALE) * 0.5D); + double sz = (z * TRI_SCALE) * TRI_PROJECTION; + long ix = (long) Math.floor(Math.abs(sx)); + long iz = (long) Math.floor(Math.abs(sz)); + return ((ix & iz) == 0L) ? 1D : 0D; + } + + private double sample(double x, double z) { + double m = mask(x, z); + double h = heat(x, z); + if (m >= 0.5D) { + return clip((h * 0.65D) + 0.35D); + } + + return clip(h * 0.12D); + } + + @Override + public double noise(double x) { + return sample(x, 0D); + } + + @Override + public double noise(double x, double z) { + return sample(x, z); + } + + @Override + public double noise(double x, double y, double z) { + if (z == 0D) { + return sample(x, y); + } + + double a = sample(x, z); + double b = sample(y, x); + double c = sample(z, y); + return (a + b + c) / 3D; + } + + @Override + public void setOctaves(int o) { + this.octaves = Math.max(1, o); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/SimplexNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/SimplexNoise.java new file mode 100644 index 000000000..5c65a0fa2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/SimplexNoise.java @@ -0,0 +1,161 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.math.RNG; + +public class SimplexNoise implements NoiseGenerator, OctaveNoise { + private final FastNoiseDouble n; + private int octaves; + + public SimplexNoise(long seed) { + this.n = new FastNoiseDouble(new RNG(seed).lmax()); + octaves = 1; + } + + public double f(double v) { + return (v / 2D) + 0.5D; + } + + @Override + public double noise(double x) { + if (octaves <= 1) { + return f(n.GetSimplex(x, 0d)); + } + + double f = 1; + double m = 0; + double v = 0; + + for (int i = 0; i < octaves; i++) { + v += n.GetSimplex((x * (f == 1 ? f++ : (f *= 2))), 0d) * f; + m += f; + } + + return f(v / m); + } + + @Override + public double noiseSigned(double x) { + if (octaves <= 1) { + return n.GetSimplex(x, 0D); + } + + double frequency = 1D; + double magnitude = 0D; + double value = 0D; + + for (int i = 0; i < octaves; i++) { + double sampleFrequency; + if (frequency == 1D) { + sampleFrequency = frequency; + frequency = 2D; + } else { + frequency *= 2D; + sampleFrequency = frequency; + } + + value += n.GetSimplex(x * sampleFrequency, 0D) * frequency; + magnitude += frequency; + } + + return value / magnitude; + } + + @Override + public double noise(double x, double z) { + if (octaves <= 1) { + return f(n.GetSimplex(x, z)); + } + double f = 1; + double m = 0; + double v = 0; + + for (int i = 0; i < octaves; i++) { + f = f == 1 ? f + 1 : f * 2; + v += n.GetSimplex((x * f), (z * f)) * f; + m += f; + } + + return f(v / m); + } + + @Override + public double noiseSigned(double x, double z) { + if (octaves <= 1) { + return n.GetSimplex(x, z); + } + + double frequency = 1D; + double magnitude = 0D; + double value = 0D; + + for (int i = 0; i < octaves; i++) { + double octaveFrequency = frequency == 1D ? frequency + 1D : frequency * 2D; + value += n.GetSimplex(x * octaveFrequency, z * octaveFrequency) * octaveFrequency; + magnitude += octaveFrequency; + frequency = octaveFrequency; + } + + return value / magnitude; + } + + @Override + public double noise(double x, double y, double z) { + if (octaves <= 1) { + return f(n.GetSimplex(x, y, z)); + } + double f = 1; + double m = 0; + double v = 0; + + for (int i = 0; i < octaves; i++) { + f = f == 1 ? f + 1 : f * 2; + v += n.GetSimplex((x * f), (y * f), (z * f)) * f; + m += f; + } + + return f(v / m); + } + + @Override + public double noiseSigned(double x, double y, double z) { + if (octaves <= 1) { + return n.GetSimplex(x, y, z); + } + + double frequency = 1D; + double magnitude = 0D; + double value = 0D; + + for (int i = 0; i < octaves; i++) { + double octaveFrequency = frequency == 1D ? frequency + 1D : frequency * 2D; + value += n.GetSimplex(x * octaveFrequency, y * octaveFrequency, z * octaveFrequency) * octaveFrequency; + magnitude += octaveFrequency; + frequency = octaveFrequency; + } + + return value / magnitude; + } + + @Override + public void setOctaves(int o) { + octaves = o; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/VascularNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/VascularNoise.java new file mode 100644 index 000000000..f58119c51 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/VascularNoise.java @@ -0,0 +1,79 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.math.RNG; + +public class VascularNoise implements NoiseGenerator { + private final FastNoiseDouble n; + + public VascularNoise(long seed) { + this.n = new FastNoiseDouble(new RNG(seed).lmax()); + n.setNoiseType(FastNoiseDouble.NoiseType.Cellular); + n.setCellularReturnType(FastNoiseDouble.CellularReturnType.Distance2Sub); + n.setCellularDistanceFunction(FastNoiseDouble.CellularDistanceFunction.Natural); + } + + private double filter(double noise) { + double normalized = (noise * 0.5D) + 0.5D; + if (normalized < 0D) { + return 0D; + } + + return normalized > 1D ? 1D : normalized; + } + + private double filterSigned(double noise) { + if (noise < -1D) { + return -1D; + } + + return noise > 1D ? 1D : noise; + } + + @Override + public double noise(double x) { + return filter(n.GetCellular(x, 0)); + } + + @Override + public double noiseSigned(double x) { + return filterSigned(n.GetCellular(x, 0D)); + } + + @Override + public double noise(double x, double z) { + return filter(n.GetCellular(x, z)); + } + + @Override + public double noiseSigned(double x, double z) { + return filterSigned(n.GetCellular(x, z)); + } + + @Override + public double noise(double x, double y, double z) { + return filter(n.GetCellular(x, y, z)); + } + + @Override + public double noiseSigned(double x, double y, double z) { + return filterSigned(n.GetCellular(x, y, z)); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/noise/WhiteNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/WhiteNoise.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/noise/WhiteNoise.java rename to core/src/main/java/art/arcane/iris/util/project/noise/WhiteNoise.java index 3ce0bccc4..f929682a3 100644 --- a/core/src/main/java/com/volmit/iris/util/noise/WhiteNoise.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/WhiteNoise.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.noise; +package art.arcane.iris.util.project.noise; -import com.volmit.iris.util.math.RNG; +import art.arcane.volmlib.util.math.RNG; public class WhiteNoise implements NoiseGenerator { private final FastNoise n; diff --git a/core/src/main/java/com/volmit/iris/util/sentry/Attachments.java b/core/src/main/java/art/arcane/iris/util/project/sentry/Attachments.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/sentry/Attachments.java rename to core/src/main/java/art/arcane/iris/util/project/sentry/Attachments.java index d6dbcccfa..026908c5c 100644 --- a/core/src/main/java/com/volmit/iris/util/sentry/Attachments.java +++ b/core/src/main/java/art/arcane/iris/util/project/sentry/Attachments.java @@ -1,9 +1,9 @@ -package com.volmit.iris.util.sentry; +package art.arcane.iris.util.project.sentry; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.volmit.iris.core.safeguard.IrisSafeguard; -import com.volmit.iris.util.collection.KMap; +import art.arcane.iris.core.safeguard.IrisSafeguard; +import art.arcane.volmlib.util.collection.KMap; import io.sentry.Attachment; import org.bukkit.Bukkit; diff --git a/core/src/main/java/art/arcane/iris/util/project/sentry/IrisLogger.java b/core/src/main/java/art/arcane/iris/util/project/sentry/IrisLogger.java new file mode 100644 index 000000000..36b789711 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/sentry/IrisLogger.java @@ -0,0 +1,47 @@ +package art.arcane.iris.util.project.sentry; + +import art.arcane.iris.spi.IrisLogging; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.PrintWriter; +import java.io.StringWriter; + +public class IrisLogger implements ILogger { + @Override + public void log(@NotNull SentryLevel level, @NotNull String message, @Nullable Object... args) { + IrisLogging.msg(String.format("%s: %s", level, String.format(message, args))); + } + + @Override + public void log(@NotNull SentryLevel level, @NotNull String message, @Nullable Throwable throwable) { + if (throwable == null) { + log(level, message); + } else { + IrisLogging.msg(String.format("%s: %s\n%s", level, String.format(message, throwable), captureStackTrace(throwable))); + } + } + + @Override + public void log(@NotNull SentryLevel level, @Nullable Throwable throwable, @NotNull String message, @Nullable Object... args) { + if (throwable == null) { + log(level, message, args); + } else { + IrisLogging.msg(String.format("%s: %s\n%s", level, String.format(message, throwable), captureStackTrace(throwable))); + } + } + + @Override + public boolean isEnabled(@Nullable SentryLevel level) { + return true; + } + + private @NotNull String captureStackTrace(@NotNull Throwable throwable) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + throwable.printStackTrace(printWriter); + return stringWriter.toString(); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/sentry/ServerID.java b/core/src/main/java/art/arcane/iris/util/project/sentry/ServerID.java similarity index 84% rename from core/src/main/java/com/volmit/iris/util/sentry/ServerID.java rename to core/src/main/java/art/arcane/iris/util/project/sentry/ServerID.java index da766574e..69b99e06f 100644 --- a/core/src/main/java/com/volmit/iris/util/sentry/ServerID.java +++ b/core/src/main/java/art/arcane/iris/util/project/sentry/ServerID.java @@ -1,6 +1,6 @@ -package com.volmit.iris.util.sentry; +package art.arcane.iris.util.project.sentry; -import com.volmit.iris.util.io.IO; +import art.arcane.volmlib.util.io.IO; import io.sentry.protocol.User; import lombok.SneakyThrows; import org.bukkit.Bukkit; @@ -12,11 +12,17 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class ServerID { - public static final String ID = calculate(); + private static final class Holder { + private static final String ID = calculate(); + } + + public static String getId() { + return Holder.ID; + } public static User asUser() { User u = new User(); - u.setId(ID); + u.setId(Holder.ID); return u; } diff --git a/core/src/main/java/com/volmit/iris/util/stream/BasicStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/BasicStream.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/stream/BasicStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/BasicStream.java index b23ba9801..bbb47757a 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/BasicStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/BasicStream.java @@ -16,8 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream; +package art.arcane.iris.util.project.stream; +import art.arcane.volmlib.util.stream.BasicLayer; public abstract class BasicStream extends BasicLayer implements ProceduralStream { private final ProceduralStream source; diff --git a/core/src/main/java/com/volmit/iris/util/stream/ProceduralStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java similarity index 81% rename from core/src/main/java/com/volmit/iris/util/stream/ProceduralStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java index 539a0f224..9d7f90946 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/ProceduralStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java @@ -16,28 +16,58 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream; +package art.arcane.iris.util.project.stream; -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IRare; -import com.volmit.iris.engine.object.IrisStyledRange; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.function.Function4; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.stream.arithmetic.*; -import com.volmit.iris.util.stream.convert.*; -import com.volmit.iris.util.stream.interpolation.Interpolated; -import com.volmit.iris.util.stream.sources.FunctionStream; -import com.volmit.iris.util.stream.utility.*; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IRare; +import art.arcane.iris.engine.object.IrisStyledRange; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.stream.ProceduralLayer; +import art.arcane.volmlib.util.stream.Significance; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.volmlib.util.function.Function4; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.stream.arithmetic.AddingStream; +import art.arcane.iris.util.project.stream.arithmetic.ClampedStream; +import art.arcane.iris.util.project.stream.arithmetic.CoordinateBitShiftLeftStream; +import art.arcane.iris.util.project.stream.arithmetic.CoordinateBitShiftRightStream; +import art.arcane.iris.util.project.stream.arithmetic.DividingStream; +import art.arcane.iris.util.project.stream.arithmetic.FittedStream; +import art.arcane.iris.util.project.stream.arithmetic.MaxingStream; +import art.arcane.iris.util.project.stream.arithmetic.MinningStream; +import art.arcane.iris.util.project.stream.arithmetic.ModuloStream; +import art.arcane.iris.util.project.stream.arithmetic.MultiplyingStream; +import art.arcane.iris.util.project.stream.arithmetic.OffsetStream; +import art.arcane.iris.util.project.stream.arithmetic.RadialStream; +import art.arcane.iris.util.project.stream.arithmetic.RoundingDoubleStream; +import art.arcane.iris.util.project.stream.arithmetic.SlopeStream; +import art.arcane.iris.util.project.stream.arithmetic.SubtractingStream; +import art.arcane.iris.util.project.stream.arithmetic.ZoomStream; +import art.arcane.iris.util.project.stream.convert.AwareConversionStream2D; +import art.arcane.iris.util.project.stream.convert.AwareConversionStream3D; +import art.arcane.iris.util.project.stream.convert.CachedConversionStream; +import art.arcane.iris.util.project.stream.convert.ConversionStream; +import art.arcane.iris.util.project.stream.convert.ForceDoubleStream; +import art.arcane.iris.util.project.stream.convert.RoundingStream; +import art.arcane.iris.util.project.stream.convert.SelectionStream; +import art.arcane.iris.util.project.stream.convert.SignificanceStream; +import art.arcane.iris.util.project.stream.convert.To3DStream; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; +import art.arcane.iris.util.project.stream.sources.FunctionStream; +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.util.project.stream.utility.ContextInjectingStream; +import art.arcane.iris.util.project.stream.utility.NullSafeStream; +import art.arcane.iris.util.project.stream.utility.SynchronizedStream; import java.util.ArrayList; import java.util.List; @@ -49,8 +79,8 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { try { return of(f, Interpolated.DOUBLE); } catch (IncompatibleClassChangeError e) { - Iris.warn(f.toString()); - Iris.reportError(e); + IrisLogging.warn(f.toString()); + IrisLogging.reportError(e); e.printStackTrace(); return null; } @@ -85,14 +115,6 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { }, Interpolated.BOOLEAN); } - default ProceduralStream profile() { - return profile(256); - } - - default ProceduralStream profile(int memory) { - return new ProfiledStream<>(this, memory); - } - default ProceduralStream onNull(T v) { return new NullSafeStream<>(this, v); } @@ -110,13 +132,12 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { return new ContextInjectingStream<>(this, contextAccessor); } - default ProceduralStream add(ProceduralStream a) { - return add2D((x, z) -> a.get(x, z)); + default ProceduralStream contextInjecting(Engine engine, Function3 contextAccessor) { + return new ContextInjectingStream<>(this, engine, contextAccessor); } - default ProceduralStream waste(String name) { - return this; - //return new WasteDetector(this, name); + default ProceduralStream add(ProceduralStream a) { + return add2D((x, z) -> a.get(x, z)); } default ProceduralStream subtract(ProceduralStream a) { @@ -167,10 +188,6 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { return new SynchronizedStream<>(this); } - default ProceduralStream semaphore(int permits) { - return new SemaphoreStream<>(this, permits); - } - default ProceduralStream bitShiftCoordsLeft(int a) { return new CoordinateBitShiftLeftStream<>(this, a); } @@ -279,6 +296,11 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { return new CachedStream2D(name, engine, this, size); } + @SuppressWarnings("unchecked") + default ProceduralStream cache2DDouble(String name, Engine engine, int size) { + return new CachedDoubleStream2D(name, engine, (ProceduralStream) this, size); + } + default ProceduralStream cache3D(String name, Engine engine, int maxSize) { return new CachedStream3D(name, engine, this, maxSize); } @@ -336,7 +358,6 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { } @SuppressWarnings("unchecked") - @Deprecated(forRemoval = true) default ProceduralStream selectRarity(V... types) { KList rarityTypes = new KList<>(); int totalRarity = 0; @@ -351,16 +372,16 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { return new SelectionStream(this, rarityTypes); } - default ProceduralStream selectRarity(List types, boolean legacy) { - return IRare.stream(this.forceDouble(), types, legacy); + default ProceduralStream selectRarity(List types) { + return IRare.stream(this.forceDouble(), types); } - default ProceduralStream selectRarity(List types, Function loader, boolean legacy) { + default ProceduralStream selectRarity(List types, Function loader) { List r = new ArrayList<>(); for (V f : types) { r.add(loader.apply(f)); } - return selectRarity(r, legacy); + return selectRarity(r); } default int countPossibilities(List types, Function loader) { @@ -521,7 +542,7 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { } default void fill(Hunk h, double x, double y, double z) { - fill(h, x, z, 4); + fill(h, x, y, z, 4); } default void fill2D(Hunk h, double x, double z, V v) { @@ -570,7 +591,7 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { for (int i = 0; i < 16; i++) { for (int j = 0; j < 16; j++) { - c[Cache.to1D(i + xs, j + zs, 0, 16, 16)] = get(i + xs, j + zs); + c[Cache.to1D(i, j, 0, 16, 16)] = get(i + xs, j + zs); } } } diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/AddingStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/AddingStream.java similarity index 86% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/AddingStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/AddingStream.java index 273e107ca..d54560698 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/AddingStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/AddingStream.java @@ -16,13 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class AddingStream extends BasicStream { private final Function3 add; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/ClampedStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ClampedStream.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/ClampedStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ClampedStream.java index 34f5a5f8a..2e8f2f5fd 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/ClampedStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ClampedStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class ClampedStream extends BasicStream implements ProceduralStream { private final double min; private final double max; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/CoordinateBitShiftLeftStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/CoordinateBitShiftLeftStream.java similarity index 89% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/CoordinateBitShiftLeftStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/CoordinateBitShiftLeftStream.java index 14925fe75..2ea25f89f 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/CoordinateBitShiftLeftStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/CoordinateBitShiftLeftStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class CoordinateBitShiftLeftStream extends BasicStream implements ProceduralStream { private final int amount; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/CoordinateBitShiftRightStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/CoordinateBitShiftRightStream.java similarity index 89% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/CoordinateBitShiftRightStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/CoordinateBitShiftRightStream.java index 948a690e8..b5a68edc0 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/CoordinateBitShiftRightStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/CoordinateBitShiftRightStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class CoordinateBitShiftRightStream extends BasicStream implements ProceduralStream { private final int amount; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/DividingStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/DividingStream.java similarity index 87% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/DividingStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/DividingStream.java index 881af02e8..3c897710e 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/DividingStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/DividingStream.java @@ -16,13 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class DividingStream extends BasicStream implements ProceduralStream { private final Function3 add; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/FittedStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/FittedStream.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/FittedStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/FittedStream.java index 045039e9f..7028a91e4 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/FittedStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/FittedStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class FittedStream extends BasicStream implements ProceduralStream { private final double min; private final double max; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/MaxingStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MaxingStream.java similarity index 86% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/MaxingStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MaxingStream.java index 513f09ea2..f18e4950b 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/MaxingStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MaxingStream.java @@ -16,13 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class MaxingStream extends BasicStream { private final Function3 add; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/MinningStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MinningStream.java similarity index 86% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/MinningStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MinningStream.java index 369856432..6d704fc50 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/MinningStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MinningStream.java @@ -16,13 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class MinningStream extends BasicStream { private final Function3 add; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/ModuloStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ModuloStream.java similarity index 86% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/ModuloStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ModuloStream.java index c8c0664b5..98d0c19e6 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/ModuloStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ModuloStream.java @@ -16,13 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class ModuloStream extends BasicStream { private final Function3 add; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/MultiplyingStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MultiplyingStream.java similarity index 86% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/MultiplyingStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MultiplyingStream.java index 4e1a8e4fc..0fa4b56b1 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/MultiplyingStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/MultiplyingStream.java @@ -16,13 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class MultiplyingStream extends BasicStream { private final Function3 add; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/OffsetStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/OffsetStream.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/OffsetStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/OffsetStream.java index ace5be403..6ddb74011 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/OffsetStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/OffsetStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class OffsetStream extends BasicStream implements ProceduralStream { private final double ox; private final double oy; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/RadialStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/RadialStream.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/RadialStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/RadialStream.java index be20cdc40..4fe304165 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/RadialStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/RadialStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class RadialStream extends BasicStream implements ProceduralStream { private final double scale; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/RoundingDoubleStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/RoundingDoubleStream.java similarity index 89% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/RoundingDoubleStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/RoundingDoubleStream.java index 7b24faf0e..993fe75ac 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/RoundingDoubleStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/RoundingDoubleStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class RoundingDoubleStream extends BasicStream { private final ProceduralStream stream; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/SlopeStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/SlopeStream.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/SlopeStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/SlopeStream.java index eb60f804b..4e8c362e3 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/SlopeStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/SlopeStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class SlopeStream extends BasicStream { private final int range; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/SubtractingStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/SubtractingStream.java similarity index 86% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/SubtractingStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/SubtractingStream.java index 8dd76be80..c48cb1faf 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/SubtractingStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/SubtractingStream.java @@ -16,13 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class SubtractingStream extends BasicStream { private final Function3 add; diff --git a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/ZoomStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ZoomStream.java similarity index 89% rename from core/src/main/java/com/volmit/iris/util/stream/arithmetic/ZoomStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ZoomStream.java index ea2e5c5b0..4e804a69f 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/arithmetic/ZoomStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/arithmetic/ZoomStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.arithmetic; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.arithmetic; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class ZoomStream extends BasicStream { private final double ox; private final double oy; diff --git a/core/src/main/java/com/volmit/iris/util/stream/convert/AwareConversionStream2D.java b/core/src/main/java/art/arcane/iris/util/project/stream/convert/AwareConversionStream2D.java similarity index 88% rename from core/src/main/java/com/volmit/iris/util/stream/convert/AwareConversionStream2D.java rename to core/src/main/java/art/arcane/iris/util/project/stream/convert/AwareConversionStream2D.java index 2ddecb93b..811b38efb 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/convert/AwareConversionStream2D.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/convert/AwareConversionStream2D.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.convert; - -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.convert; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class AwareConversionStream2D extends BasicStream { private final ProceduralStream stream; private final Function3 converter; diff --git a/core/src/main/java/com/volmit/iris/util/stream/convert/AwareConversionStream3D.java b/core/src/main/java/art/arcane/iris/util/project/stream/convert/AwareConversionStream3D.java similarity index 88% rename from core/src/main/java/com/volmit/iris/util/stream/convert/AwareConversionStream3D.java rename to core/src/main/java/art/arcane/iris/util/project/stream/convert/AwareConversionStream3D.java index fefbc2a6c..4e14c42c3 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/convert/AwareConversionStream3D.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/convert/AwareConversionStream3D.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.convert; - -import com.volmit.iris.util.function.Function4; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.convert; +import art.arcane.volmlib.util.function.Function4; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class AwareConversionStream3D extends BasicStream { private final ProceduralStream stream; private final Function4 converter; diff --git a/core/src/main/java/com/volmit/iris/util/stream/convert/CachedConversionStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/convert/CachedConversionStream.java similarity index 89% rename from core/src/main/java/com/volmit/iris/util/stream/convert/CachedConversionStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/convert/CachedConversionStream.java index b056a9cce..f857bca3c 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/convert/CachedConversionStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/convert/CachedConversionStream.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.convert; - -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.stream.BasicLayer; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.convert; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.stream.BasicLayer; +import art.arcane.iris.util.project.stream.ProceduralStream; import java.util.function.Function; public class CachedConversionStream extends BasicLayer implements ProceduralStream { diff --git a/core/src/main/java/com/volmit/iris/util/stream/convert/ConversionStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/convert/ConversionStream.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/stream/convert/ConversionStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/convert/ConversionStream.java index 9aae8e741..0fa13d033 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/convert/ConversionStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/convert/ConversionStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.convert; - -import com.volmit.iris.util.stream.BasicLayer; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.convert; +import art.arcane.volmlib.util.stream.BasicLayer; +import art.arcane.iris.util.project.stream.ProceduralStream; import java.util.function.Function; public class ConversionStream extends BasicLayer implements ProceduralStream { diff --git a/core/src/main/java/com/volmit/iris/util/stream/convert/ForceDoubleStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/convert/ForceDoubleStream.java similarity index 88% rename from core/src/main/java/com/volmit/iris/util/stream/convert/ForceDoubleStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/convert/ForceDoubleStream.java index f9c28cb10..fb6685c30 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/convert/ForceDoubleStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/convert/ForceDoubleStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.convert; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.convert; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class ForceDoubleStream extends BasicStream { private final ProceduralStream stream; diff --git a/core/src/main/java/com/volmit/iris/util/stream/convert/RoundingStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/convert/RoundingStream.java similarity index 89% rename from core/src/main/java/com/volmit/iris/util/stream/convert/RoundingStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/convert/RoundingStream.java index 272a3d691..abc364eee 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/convert/RoundingStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/convert/RoundingStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.convert; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.convert; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class RoundingStream extends BasicStream { private final ProceduralStream stream; diff --git a/core/src/main/java/com/volmit/iris/util/stream/convert/SelectionStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/convert/SelectionStream.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/stream/convert/SelectionStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/convert/SelectionStream.java index d4ee0038a..178974850 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/convert/SelectionStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/convert/SelectionStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.convert; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.convert; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; import java.util.List; public class SelectionStream extends BasicStream { diff --git a/core/src/main/java/com/volmit/iris/util/stream/convert/SignificanceStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/convert/SignificanceStream.java similarity index 87% rename from core/src/main/java/com/volmit/iris/util/stream/convert/SignificanceStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/convert/SignificanceStream.java index 2353cb641..2c89af41d 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/convert/SignificanceStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/convert/SignificanceStream.java @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.convert; +package art.arcane.iris.util.project.stream.convert; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.stream.ArraySignificance; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; -import com.volmit.iris.util.stream.Significance; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.stream.ArraySignificance; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.volmlib.util.stream.Significance; public class SignificanceStream, T> extends BasicStream { private final ProceduralStream stream; diff --git a/core/src/main/java/com/volmit/iris/util/stream/convert/To3DStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/convert/To3DStream.java similarity index 88% rename from core/src/main/java/com/volmit/iris/util/stream/convert/To3DStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/convert/To3DStream.java index 969211ae4..32e94f1e6 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/convert/To3DStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/convert/To3DStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.convert; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.convert; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class To3DStream extends BasicStream { public To3DStream(ProceduralStream stream) { super(stream); diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/BiHermiteStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BiHermiteStream.java similarity index 93% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/BiHermiteStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BiHermiteStream.java index 97350b912..54e85a7d7 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/BiHermiteStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BiHermiteStream.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class BiHermiteStream extends BasicStream implements Interpolator { private final int rx; private final int ry; diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/BiStarcastStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BiStarcastStream.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/BiStarcastStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BiStarcastStream.java index d9529a9e9..5c87d21d3 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/BiStarcastStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BiStarcastStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class BiStarcastStream extends BasicStream implements Interpolator { private final int rad; private final int checks; diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/BicubicStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BicubicStream.java similarity index 92% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/BicubicStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BicubicStream.java index 3b9302bcb..998ae6ad6 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/BicubicStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BicubicStream.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class BicubicStream extends BasicStream implements Interpolator { private final int rx; private final int ry; diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/BilinearStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BilinearStream.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/BilinearStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BilinearStream.java index e92d9f53a..00277addf 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/BilinearStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/BilinearStream.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class BilinearStream extends BasicStream implements Interpolator { private final int rx; private final int ry; diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/Interpolated.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/Interpolated.java similarity index 80% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/Interpolated.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/Interpolated.java index 9833c58ce..0fcd6fd27 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/Interpolated.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/Interpolated.java @@ -16,20 +16,20 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; +package art.arcane.iris.util.project.stream.interpolation; -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.CaveResult; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.stream.ProceduralStream; -import org.bukkit.block.data.BlockData; +import art.arcane.iris.engine.object.CaveResult; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.stream.ProceduralStream; import java.util.UUID; import java.util.function.Function; public interface Interpolated { - Interpolated BLOCK_DATA = of((t) -> 0D, (t) -> null); + Interpolated BLOCK_DATA = of((t) -> 0D, (t) -> null); Interpolated> CAVE_RESULTS = of((t) -> 0D, (t) -> null); Interpolated RNG = of((t) -> 0D, (t) -> null); Interpolated DOUBLE = of((t) -> t, (t) -> t); @@ -62,7 +62,7 @@ public interface Interpolated { return new InterpolatorFactory<>((ProceduralStream) this); } - Iris.warn("Cannot interpolate " + this.getClass().getCanonicalName() + "!"); + IrisLogging.warn("Cannot interpolate " + this.getClass().getCanonicalName() + "!"); return null; } } diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/InterpolatingStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/InterpolatingStream.java similarity index 82% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/InterpolatingStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/InterpolatingStream.java index 12ef7a078..5dd0d8a92 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/InterpolatingStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/InterpolatingStream.java @@ -16,14 +16,13 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.function.NoiseProvider; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.volmlib.util.function.NoiseProvider; +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class InterpolatingStream extends BasicStream implements Interpolator { private final InterpolationMethod type; private final NoiseProvider np; diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/Interpolator.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/Interpolator.java similarity index 89% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/Interpolator.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/Interpolator.java index 22e600a93..f8f2c55de 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/Interpolator.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/Interpolator.java @@ -16,10 +16,9 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.iris.util.project.stream.ProceduralStream; public interface Interpolator { @SuppressWarnings("unchecked") default InterpolatorFactory into() { diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/InterpolatorFactory.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/InterpolatorFactory.java similarity index 94% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/InterpolatorFactory.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/InterpolatorFactory.java index 8ed1ee315..ac177d118 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/InterpolatorFactory.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/InterpolatorFactory.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +import art.arcane.iris.util.project.stream.ProceduralStream; @SuppressWarnings("ClassCanBeRecord") public class InterpolatorFactory { private final ProceduralStream stream; diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/TriStarcastStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TriStarcastStream.java similarity index 92% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/TriStarcastStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TriStarcastStream.java index 5c91ef598..d1bdf9c5c 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/TriStarcastStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TriStarcastStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class TriStarcastStream extends BasicStream implements Interpolator { private final int rad; private final int checks; diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/TricubicStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TricubicStream.java similarity index 96% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/TricubicStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TricubicStream.java index bbb007aba..b994262f9 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/TricubicStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TricubicStream.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class TricubicStream extends BasicStream implements Interpolator { private final int rx; private final int ry; diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/TrilinearStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TrilinearStream.java similarity index 91% rename from core/src/main/java/com/volmit/iris/util/stream/interpolation/TrilinearStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TrilinearStream.java index bfbd78110..18a9f1595 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/TrilinearStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TrilinearStream.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.interpolation; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class TrilinearStream extends BasicStream implements Interpolator { private final int rx; private final int ry; diff --git a/core/src/main/java/com/volmit/iris/util/stream/sources/CNGStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/sources/CNGStream.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/stream/sources/CNGStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/sources/CNGStream.java index 4878a5e58..0227ce003 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/sources/CNGStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/sources/CNGStream.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.sources; - -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.stream.BasicLayer; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.sources; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.stream.BasicLayer; +import art.arcane.iris.util.project.stream.ProceduralStream; public class CNGStream extends BasicLayer implements ProceduralStream { private final CNG cng; diff --git a/core/src/main/java/com/volmit/iris/util/stream/sources/FunctionStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/sources/FunctionStream.java similarity index 84% rename from core/src/main/java/com/volmit/iris/util/stream/sources/FunctionStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/sources/FunctionStream.java index 66a1ce506..a5916ac7c 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/sources/FunctionStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/sources/FunctionStream.java @@ -16,13 +16,12 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.sources; - -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.interpolation.Interpolated; +package art.arcane.iris.util.project.stream.sources; +import art.arcane.volmlib.util.function.Function2; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; public class FunctionStream extends BasicStream { private final Function2 f2; private final Function3 f3; diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedDoubleStream2D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedDoubleStream2D.java new file mode 100644 index 000000000..bd7971a00 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedDoubleStream2D.java @@ -0,0 +1,83 @@ +package art.arcane.iris.util.project.stream.utility; + +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.volmlib.util.cache.WorldCache2DDouble; +import art.arcane.volmlib.util.data.KCache; + +public class CachedDoubleStream2D extends BasicStream implements ProceduralStream, MeteredCache, ChunkFillableStream2D, ChunkFillableDoubleStream2D { + private final ProceduralStream stream; + private final WorldCache2DDouble cache; + private final Engine engine; + + public CachedDoubleStream2D(String name, Engine engine, ProceduralStream stream, int size) { + super(); + this.stream = stream; + this.engine = engine; + this.cache = new WorldCache2DDouble((x, z) -> stream.getDouble(x, z), size); + IrisServices.get(PreservationRegistry.class).registerCache(this); + } + + @Override + public double toDouble(Double t) { + return t; + } + + @Override + public Double fromDouble(double d) { + return d; + } + + @Override + public Double get(double x, double z) { + return cache.get((int) x, (int) z); + } + + @Override + public Double get(double x, double y, double z) { + return stream.get(x, y, z); + } + + @Override + public double getDouble(double x, double z) { + return cache.get((int) x, (int) z); + } + + @Override + public long getSize() { + return cache.getSize(); + } + + @Override + public KCache getRawCache() { + return null; + } + + @Override + public long getMaxSize() { + return cache.getMaxSize(); + } + + @Override + public boolean isClosed() { + return engine.isClosed(); + } + + @Override + public void fillChunkRaw(int worldX, int worldZ, Object[] target) { + int chunkX = worldX >> 4; + int chunkZ = worldZ >> 4; + cache.fillChunk(chunkX, chunkZ, target); + } + + @Override + public void fillChunkDoubles(int worldX, int worldZ, double[] target) { + int chunkX = worldX >> 4; + int chunkZ = worldZ >> 4; + cache.fillChunk(chunkX, chunkZ, target); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java new file mode 100644 index 000000000..4c074919e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java @@ -0,0 +1,95 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.stream.utility; + +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.volmlib.util.cache.ChunkCache2D; +import art.arcane.volmlib.util.cache.WorldCache2D; +import art.arcane.volmlib.util.data.KCache; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; +public class CachedStream2D extends BasicStream implements ProceduralStream, MeteredCache, ChunkFillableStream2D { + private final ProceduralStream stream; + private final WorldCache2D cache; + private final Engine engine; + private final boolean fastCache; + private final boolean chunked = true; + + public CachedStream2D(String name, Engine engine, ProceduralStream stream, int size) { + super(); + this.stream = stream; + this.engine = engine; + this.fastCache = Boolean.getBoolean("iris.cache.fast"); + cache = WorldCache2D.ofInts(stream::get, size, () -> new ChunkCache2D<>("iris")); + IrisServices.get(PreservationRegistry.class).registerCache(this); + } + + @Override + public double toDouble(T t) { + return stream.toDouble(t); + } + + @Override + public T fromDouble(double d) { + return stream.fromDouble(d); + } + + @Override + public T get(double x, double z) { + return cache.get((int) x, (int) z); + } + + @Override + public T get(double x, double y, double z) { + return stream.get(x, y, z); + } + + @Override + public long getSize() { + return cache.getSize(); + } + + @Override + public KCache getRawCache() { + return null; + } + + @Override + public long getMaxSize() { + return cache.getMaxSize(); + } + + public boolean usesFastCache() { + return fastCache; + } + + @Override + public boolean isClosed() { + return engine.isClosed(); + } + + public void fillChunkRaw(int worldX, int worldZ, Object[] target) { + int chunkX = worldX >> 4; + int chunkZ = worldZ >> 4; + cache.fillChunk(chunkX, chunkZ, target); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/stream/utility/CachedStream3D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream3D.java similarity index 79% rename from core/src/main/java/com/volmit/iris/util/stream/utility/CachedStream3D.java rename to core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream3D.java index 7c1b66407..35593bf93 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/utility/CachedStream3D.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream3D.java @@ -16,17 +16,16 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.utility; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.service.PreservationSVC; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.MeteredCache; -import com.volmit.iris.util.data.KCache; -import com.volmit.iris.util.math.BlockPosition; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.utility; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.volmlib.util.data.KCache; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class CachedStream3D extends BasicStream implements ProceduralStream, MeteredCache { private final ProceduralStream stream; private final KCache cache; @@ -37,7 +36,7 @@ public class CachedStream3D extends BasicStream implements ProceduralStrea this.stream = stream; this.engine = engine; cache = new KCache<>((k) -> stream.get(k.getX(), k.getY(), k.getZ()), size); - Iris.service(PreservationSVC.class).registerCache(this); + IrisServices.get(PreservationRegistry.class).registerCache(this); } @Override diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/ChunkFillableDoubleStream2D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/ChunkFillableDoubleStream2D.java new file mode 100644 index 000000000..ed88a1f62 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/ChunkFillableDoubleStream2D.java @@ -0,0 +1,5 @@ +package art.arcane.iris.util.project.stream.utility; + +public interface ChunkFillableDoubleStream2D { + void fillChunkDoubles(int worldX, int worldZ, double[] target); +} diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/ChunkFillableStream2D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/ChunkFillableStream2D.java new file mode 100644 index 000000000..e63eea603 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/ChunkFillableStream2D.java @@ -0,0 +1,5 @@ +package art.arcane.iris.util.project.stream.utility; + +public interface ChunkFillableStream2D { + void fillChunkRaw(int worldX, int worldZ, Object[] target); +} diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/ContextInjectingStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/ContextInjectingStream.java new file mode 100644 index 000000000..0a0d9e803 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/ContextInjectingStream.java @@ -0,0 +1,63 @@ +package art.arcane.iris.util.project.stream.utility; + +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; +public class ContextInjectingStream extends BasicStream { + private final Engine engine; + private final Function3 contextAccessor; + + public ContextInjectingStream(ProceduralStream stream, Function3 contextAccessor) { + this(stream, null, contextAccessor); + } + + public ContextInjectingStream(ProceduralStream stream, Engine engine, Function3 contextAccessor) { + super(stream); + this.engine = engine; + this.contextAccessor = contextAccessor; + } + + @Override + public T get(double x, double z) { + IrisContext context = IrisContext.get(); + + if (context != null) { + ChunkContext chunkContext = context.getChunkContext(); + int blockX = (int) x; + int blockZ = (int) z; + + if (chunkContext != null + && (engine == null || context.getEngine() == engine) + && context.getGenerationSessionId() == chunkContext.getGenerationSessionId() + && (engine == null || context.getGenerationSessionId() == engine.getGenerationSessionId()) + && blockX >> 4 == chunkContext.getX() >> 4 + && blockZ >> 4 == chunkContext.getZ() >> 4) { + T t = contextAccessor.apply(chunkContext, blockX & 15, blockZ & 15); + + if (t != null) { + return t; + } + } + } + + return getTypedSource().get(x, z); + } + + @Override + public T get(double x, double y, double z) { + return getTypedSource().get(x, y, z); + } + + @Override + public double toDouble(T t) { + return getTypedSource().toDouble(t); + } + + @Override + public T fromDouble(double d) { + return getTypedSource().fromDouble(d); + } +} diff --git a/core/src/main/java/com/volmit/iris/util/stream/utility/NullSafeStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/NullSafeStream.java similarity index 90% rename from core/src/main/java/com/volmit/iris/util/stream/utility/NullSafeStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/utility/NullSafeStream.java index b17dd0076..97ce20903 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/utility/NullSafeStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/NullSafeStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.utility; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.utility; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class NullSafeStream extends BasicStream implements ProceduralStream { private final ProceduralStream stream; private final T ifNull; diff --git a/core/src/main/java/com/volmit/iris/util/stream/utility/SynchronizedStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/SynchronizedStream.java similarity index 89% rename from core/src/main/java/com/volmit/iris/util/stream/utility/SynchronizedStream.java rename to core/src/main/java/art/arcane/iris/util/project/stream/utility/SynchronizedStream.java index ff0c4a0be..2acbf6ee1 100644 --- a/core/src/main/java/com/volmit/iris/util/stream/utility/SynchronizedStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/SynchronizedStream.java @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package com.volmit.iris.util.stream.utility; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; +package art.arcane.iris.util.project.stream.utility; +import art.arcane.iris.util.project.stream.BasicStream; +import art.arcane.iris.util.project.stream.ProceduralStream; public class SynchronizedStream extends BasicStream { public SynchronizedStream(ProceduralStream stream) { super(stream); diff --git a/core/src/main/java/art/arcane/iris/util/simd/NoiseKernels2D.java b/core/src/main/java/art/arcane/iris/util/simd/NoiseKernels2D.java new file mode 100644 index 000000000..b781cd784 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/simd/NoiseKernels2D.java @@ -0,0 +1,8 @@ +package art.arcane.iris.util.simd; + +public interface NoiseKernels2D { + String describe(); + + void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain, + double fractalBounding, double[] xs, double[] zs, double[] out, int length); +} diff --git a/core/src/main/java/art/arcane/iris/util/simd/ScalarNoiseKernels2D.java b/core/src/main/java/art/arcane/iris/util/simd/ScalarNoiseKernels2D.java new file mode 100644 index 000000000..e2b592d0b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/simd/ScalarNoiseKernels2D.java @@ -0,0 +1,99 @@ +package art.arcane.iris.util.simd; + +public final class ScalarNoiseKernels2D implements NoiseKernels2D { + static final double F2 = 0.5D * (Math.sqrt(3.0D) - 1.0D); + static final double G2 = (3.0D - Math.sqrt(3.0D)) / 6.0D; + static final long X_PRIME = 1619L; + static final long Y_PRIME = 31337L; + static final double[] GRAD_2D = {-1D, -1D, 1D, -1D, -1D, 1D, 1D, 1D, 0D, -1D, -1D, 0D, 0D, 1D, 1D, 0D}; + + @Override + public String describe() { + return "scalar"; + } + + static long fastFloor(double f) { + return f >= 0D ? (long) f : (long) f - 1L; + } + + static double gradCoord2D(long seed, long x, long y, double xd, double yd) { + long hash = seed; + hash ^= X_PRIME * x; + hash ^= Y_PRIME * y; + hash = hash * hash * hash * 60493L; + hash = (hash >> 13) ^ hash; + int gradientIndex = ((int) hash & 7) << 1; + return (xd * GRAD_2D[gradientIndex]) + (yd * GRAD_2D[gradientIndex + 1]); + } + + static double singleSimplex(long seed, double x, double y) { + double t = (x + y) * F2; + long i = fastFloor(x + t); + long j = fastFloor(y + t); + t = (i + j) * G2; + double x0 = x - (i - t); + double y0 = y - (j - t); + long i1; + long j1; + if (x0 > y0) { + i1 = 1L; + j1 = 0L; + } else { + i1 = 0L; + j1 = 1L; + } + double x1 = x0 - i1 + G2; + double y1 = y0 - j1 + G2; + double x2 = x0 - 1D + (2D * G2); + double y2 = y0 - 1D + (2D * G2); + double n0; + double n1; + double n2; + double a = 0.5D - x0 * x0 - y0 * y0; + if (a < 0D) { + n0 = 0D; + } else { + a *= a; + n0 = a * a * gradCoord2D(seed, i, j, x0, y0); + } + double b = 0.5D - x1 * x1 - y1 * y1; + if (b < 0D) { + n1 = 0D; + } else { + b *= b; + n1 = b * b * gradCoord2D(seed, i + i1, j + j1, x1, y1); + } + double c = 0.5D - x2 * x2 - y2 * y2; + if (c < 0D) { + n2 = 0D; + } else { + c *= c; + n2 = c * c * gradCoord2D(seed, i + 1L, j + 1L, x2, y2); + } + return 50D * (n0 + n1 + n2); + } + + static double simplexFractalFBMScalar(long seed, int octaves, double frequency, double lacunarity, double gain, + double fractalBounding, double xIn, double yIn) { + double x = xIn * frequency; + double y = yIn * frequency; + long s = seed; + double sum = singleSimplex(s, x, y); + double amp = 1D; + for (int o = 1; o < octaves; o++) { + x *= lacunarity; + y *= lacunarity; + amp *= gain; + sum += singleSimplex(++s, x, y) * amp; + } + return sum * fractalBounding; + } + + @Override + public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain, + double fractalBounding, double[] xs, double[] zs, double[] out, int length) { + for (int k = 0; k < length; k++) { + out[k] = simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs[k], zs[k]); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/simd/ScalarSimdKernels.java b/core/src/main/java/art/arcane/iris/util/simd/ScalarSimdKernels.java new file mode 100644 index 000000000..85a4ef275 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/simd/ScalarSimdKernels.java @@ -0,0 +1,37 @@ +package art.arcane.iris.util.simd; + +public final class ScalarSimdKernels implements SimdKernels { + @Override + public String describe() { + return "scalar"; + } + + @Override + public void roundToInt(double[] source, int[] target, int length) { + for (int index = 0; index < length; index++) { + target[index] = (int) Math.round(source[index]); + } + } + + @Override + public double sum(double[] values, int length) { + double total = 0D; + for (int index = 0; index < length; index++) { + total += values[index]; + } + + return total; + } + + @Override + public double max(double[] values, int length) { + double best = Double.NEGATIVE_INFINITY; + for (int index = 0; index < length; index++) { + if (values[index] > best) { + best = values[index]; + } + } + + return best; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/simd/SimdKernels.java b/core/src/main/java/art/arcane/iris/util/simd/SimdKernels.java new file mode 100644 index 000000000..243dd59ee --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/simd/SimdKernels.java @@ -0,0 +1,11 @@ +package art.arcane.iris.util.simd; + +public interface SimdKernels { + String describe(); + + void roundToInt(double[] source, int[] target, int length); + + double sum(double[] values, int length); + + double max(double[] values, int length); +} diff --git a/core/src/main/java/art/arcane/iris/util/simd/SimdSupport.java b/core/src/main/java/art/arcane/iris/util/simd/SimdSupport.java new file mode 100644 index 000000000..de41f882b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/simd/SimdSupport.java @@ -0,0 +1,94 @@ +package art.arcane.iris.util.simd; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.core.IrisSettings; + +public final class SimdSupport { + private static final String VECTOR_MODULE = "jdk.incubator.vector"; + private static final boolean MODULE_PRESENT = ModuleLayer.boot().findModule(VECTOR_MODULE).isPresent(); + private static final SimdKernels KERNELS = selectKernels(); + + private SimdSupport() { + } + + public static SimdKernels kernels() { + return KERNELS; + } + + public static boolean isVectorModulePresent() { + return MODULE_PRESENT; + } + + public static boolean isVectorized() { + return !(KERNELS instanceof ScalarSimdKernels); + } + + public static void install() { + if (isVectorized()) { + IrisLogging.info("SIMD: vector kernels enabled (" + KERNELS.describe() + ")"); + } else if (!MODULE_PRESENT) { + IrisLogging.info("SIMD: scalar kernels active; add --add-modules " + VECTOR_MODULE + " to JVM flags to enable vectorized generation kernels"); + } else { + IrisLogging.info("SIMD: vector kernels disabled (performance.simdKernels=false)"); + } + } + + public static SimdKernels createVectorKernels() { + if (!MODULE_PRESENT) { + return null; + } + + try { + return (SimdKernels) Class.forName("art.arcane.iris.util.simd.VectorSimdKernels").getDeclaredConstructor().newInstance(); + } catch (Throwable e) { + return null; + } + } + + private static SimdKernels selectKernels() { + if (!simdEnabledInSettings()) { + return new ScalarSimdKernels(); + } + + SimdKernels vector = createVectorKernels(); + return vector == null ? new ScalarSimdKernels() : vector; + } + + private static final NoiseKernels2D NOISE_KERNELS_2D = selectNoiseKernels2D(); + + public static NoiseKernels2D noiseKernels2D() { + return NOISE_KERNELS_2D; + } + + public static NoiseKernels2D createVectorNoiseKernels2D() { + if (!MODULE_PRESENT) { + return null; + } + try { + Class cls = Class.forName("art.arcane.iris.util.simd.VectorNoiseKernels2D"); + boolean profitable = (boolean) cls.getMethod("profitable").invoke(null); + if (!profitable) { + return null; + } + return (NoiseKernels2D) cls.getDeclaredConstructor().newInstance(); + } catch (Throwable e) { + return null; + } + } + + private static NoiseKernels2D selectNoiseKernels2D() { + if (!simdEnabledInSettings()) { + return new ScalarNoiseKernels2D(); + } + NoiseKernels2D vector = createVectorNoiseKernels2D(); + return vector == null ? new ScalarNoiseKernels2D() : vector; + } + + private static boolean simdEnabledInSettings() { + try { + return IrisSettings.get().getPerformance().isSimdKernels(); + } catch (Throwable e) { + return true; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/simd/VectorNoiseKernels2D.java b/core/src/main/java/art/arcane/iris/util/simd/VectorNoiseKernels2D.java new file mode 100644 index 000000000..5db19a260 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/simd/VectorNoiseKernels2D.java @@ -0,0 +1,129 @@ +package art.arcane.iris.util.simd; + +import jdk.incubator.vector.DoubleVector; +import jdk.incubator.vector.LongVector; +import jdk.incubator.vector.VectorMask; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; + +public final class VectorNoiseKernels2D implements NoiseKernels2D { + private static final VectorSpecies DS = DoubleVector.SPECIES_PREFERRED; + private static final VectorSpecies LS = LongVector.SPECIES_PREFERRED; + private static final boolean ALIGNED = DS.length() == LS.length(); + private static final int MIN_PROFITABLE_LANES = 4; + private static final boolean PROFITABLE = ALIGNED && DS.length() >= MIN_PROFITABLE_LANES; + private static final double[] GRAD_X = {-1D, 1D, -1D, 1D, 0D, -1D, 0D, 1D}; + private static final double[] GRAD_Y = {-1D, -1D, 1D, 1D, -1D, 0D, 1D, 0D}; + private static final double F2 = ScalarNoiseKernels2D.F2; + private static final double G2 = ScalarNoiseKernels2D.G2; + private static final long X_PRIME = ScalarNoiseKernels2D.X_PRIME; + private static final long Y_PRIME = ScalarNoiseKernels2D.Y_PRIME; + private static final ThreadLocal LONG_SCRATCH = ThreadLocal.withInitial(() -> new long[LS.length()]); + + public static boolean lanesAligned() { + return ALIGNED; + } + + public static boolean profitable() { + return PROFITABLE; + } + + @Override + public String describe() { + return DS.length() + "x64 lanes, " + DS.vectorShape(); + } + + private static LongVector floorToLong(DoubleVector f) { + LongVector truncated = (LongVector) f.convertShape(VectorOperators.D2L, LS, 0); + VectorMask negative = f.compare(VectorOperators.LT, 0D).cast(LS); + return truncated.sub(1L, negative); + } + + private static DoubleVector toDouble(LongVector v) { + return (DoubleVector) v.convertShape(VectorOperators.L2D, DS, 0); + } + + private static DoubleVector gradCoord(long seed, LongVector i, LongVector j, DoubleVector xd, DoubleVector yd, + int[] idxScratch) { + LongVector hash = LongVector.broadcast(LS, seed) + .lanewise(VectorOperators.XOR, i.mul(X_PRIME)) + .lanewise(VectorOperators.XOR, j.mul(Y_PRIME)); + hash = hash.mul(hash).mul(hash).mul(60493L); + LongVector shifted = hash.lanewise(VectorOperators.ASHR, 13); + hash = shifted.lanewise(VectorOperators.XOR, hash); + LongVector idx = hash.lanewise(VectorOperators.AND, 7L); + long[] tmp = LONG_SCRATCH.get(); + idx.intoArray(tmp, 0); + for (int l = 0; l < idxScratch.length; l++) { + idxScratch[l] = (int) tmp[l]; + } + DoubleVector gx = DoubleVector.fromArray(DS, GRAD_X, 0, idxScratch, 0); + DoubleVector gy = DoubleVector.fromArray(DS, GRAD_Y, 0, idxScratch, 0); + return xd.mul(gx).add(yd.mul(gy)); + } + + private static DoubleVector corner(DoubleVector xk, DoubleVector yk, DoubleVector grad) { + DoubleVector t = DoubleVector.broadcast(DS, 0.5D).sub(xk.mul(xk)).sub(yk.mul(yk)); + VectorMask negative = t.compare(VectorOperators.LT, 0D); + DoubleVector t2 = t.mul(t); + DoubleVector t4 = t2.mul(t2); + return t4.mul(grad).blend(0D, negative); + } + + private static DoubleVector singleSimplexVector(long seed, DoubleVector x, DoubleVector y, int[] idxScratch) { + DoubleVector t = x.add(y).mul(F2); + LongVector i = floorToLong(x.add(t)); + LongVector j = floorToLong(y.add(t)); + DoubleVector skew = toDouble(i.add(j)).mul(G2); + DoubleVector x0 = x.sub(toDouble(i).sub(skew)); + DoubleVector y0 = y.sub(toDouble(j).sub(skew)); + VectorMask xGreater = x0.compare(VectorOperators.GT, y0); + VectorMask xGreaterL = xGreater.cast(LS); + LongVector i1 = LongVector.zero(LS).blend(1L, xGreaterL); + LongVector j1 = LongVector.broadcast(LS, 1L).blend(0L, xGreaterL); + DoubleVector x1 = x0.sub(toDouble(i1)).add(G2); + DoubleVector y1 = y0.sub(toDouble(j1)).add(G2); + DoubleVector x2 = x0.sub(1D).add(2D * G2); + DoubleVector y2 = y0.sub(1D).add(2D * G2); + DoubleVector n0 = corner(x0, y0, gradCoord(seed, i, j, x0, y0, idxScratch)); + DoubleVector n1 = corner(x1, y1, gradCoord(seed, i.add(i1), j.add(j1), x1, y1, idxScratch)); + DoubleVector n2 = corner(x2, y2, gradCoord(seed, i.add(1L), j.add(1L), x2, y2, idxScratch)); + return n0.add(n1).add(n2).mul(50D); + } + + @Override + public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain, + double fractalBounding, double[] xs, double[] zs, double[] out, int length) { + if (!ALIGNED) { + tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, 0, length); + return; + } + int lanes = DS.length(); + int bound = DS.loopBound(length); + int[] idxScratch = new int[lanes]; + int k = 0; + for (; k < bound; k += lanes) { + DoubleVector x = DoubleVector.fromArray(DS, xs, k).mul(frequency); + DoubleVector y = DoubleVector.fromArray(DS, zs, k).mul(frequency); + long s = seed; + DoubleVector sum = singleSimplexVector(s, x, y, idxScratch); + double amp = 1D; + for (int o = 1; o < octaves; o++) { + x = x.mul(lacunarity); + y = y.mul(lacunarity); + amp *= gain; + sum = sum.add(singleSimplexVector(++s, x, y, idxScratch).mul(amp)); + } + sum.mul(fractalBounding).intoArray(out, k); + } + tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, k, length); + } + + private static void tailScalar(long seed, int octaves, double frequency, double lacunarity, double gain, + double fractalBounding, double[] xs, double[] zs, double[] out, int from, int length) { + for (int k = from; k < length; k++) { + out[k] = ScalarNoiseKernels2D.simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain, + fractalBounding, xs[k], zs[k]); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/util/simd/VectorSimdKernels.java b/core/src/main/java/art/arcane/iris/util/simd/VectorSimdKernels.java new file mode 100644 index 000000000..3d2c4605b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/simd/VectorSimdKernels.java @@ -0,0 +1,69 @@ +package art.arcane.iris.util.simd; + +import jdk.incubator.vector.DoubleVector; +import jdk.incubator.vector.IntVector; +import jdk.incubator.vector.VectorMask; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorShape; +import jdk.incubator.vector.VectorSpecies; + +public final class VectorSimdKernels implements SimdKernels { + private static final VectorSpecies DOUBLE_SPECIES = DoubleVector.SPECIES_PREFERRED; + private static final VectorSpecies INT_SPECIES = VectorSpecies.of(int.class, VectorShape.forBitSize(DOUBLE_SPECIES.length() * Integer.SIZE)); + + @Override + public String describe() { + return DOUBLE_SPECIES.length() + "x64-bit lanes, " + DOUBLE_SPECIES.vectorShape(); + } + + @Override + public void roundToInt(double[] source, int[] target, int length) { + int lanes = DOUBLE_SPECIES.length(); + int bound = DOUBLE_SPECIES.loopBound(length); + int index = 0; + for (; index < bound; index += lanes) { + DoubleVector shifted = DoubleVector.fromArray(DOUBLE_SPECIES, source, index).add(0.5D); + IntVector truncated = (IntVector) shifted.convertShape(VectorOperators.D2I, INT_SPECIES, 0); + DoubleVector truncatedBack = (DoubleVector) truncated.convertShape(VectorOperators.I2D, DOUBLE_SPECIES, 0); + VectorMask needsDecrement = shifted.lt(truncatedBack).cast(INT_SPECIES); + truncated.sub(1, needsDecrement).intoArray(target, index); + } + + for (; index < length; index++) { + target[index] = (int) Math.round(source[index]); + } + } + + @Override + public double sum(double[] values, int length) { + // Deliberately scalar: lane-wise accumulation reassociates FP addition, so the vector + // and scalar kernels returned different low bits for the same input. Generation-path + // reductions must be bit-exact regardless of which kernel set is installed. + double total = 0D; + for (int index = 0; index < length; index++) { + total += values[index]; + } + + return total; + } + + @Override + public double max(double[] values, int length) { + int lanes = DOUBLE_SPECIES.length(); + int bound = DOUBLE_SPECIES.loopBound(length); + DoubleVector accumulator = DoubleVector.broadcast(DOUBLE_SPECIES, Double.NEGATIVE_INFINITY); + int index = 0; + for (; index < bound; index += lanes) { + accumulator = accumulator.max(DoubleVector.fromArray(DOUBLE_SPECIES, values, index)); + } + + double best = accumulator.reduceLanes(VectorOperators.MAX); + for (; index < length; index++) { + if (values[index] > best) { + best = values[index]; + } + } + + return best; + } +} diff --git a/core/src/main/java/com/volmit/iris/Iris.java b/core/src/main/java/com/volmit/iris/Iris.java deleted file mode 100644 index fc9b4e55e..000000000 --- a/core/src/main/java/com/volmit/iris/Iris.java +++ /dev/null @@ -1,750 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.IrisWorlds; -import com.volmit.iris.core.ServerConfigurator; -import com.volmit.iris.core.link.IrisPapiExpansion; -import com.volmit.iris.core.link.MultiverseCoreLink; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.pregenerator.LazyPregenerator; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.EnginePanic; -import com.volmit.iris.engine.object.IrisCompat; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.engine.object.IrisWorld; -import com.volmit.iris.engine.platform.BukkitChunkGenerator; -import com.volmit.iris.core.safeguard.IrisSafeguard; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.exceptions.IrisException; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.function.NastyRunnable; -import com.volmit.iris.util.io.FileWatcher; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.io.InstanceState; -import com.volmit.iris.util.io.JarScanner; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.misc.Bindings; -import com.volmit.iris.util.misc.SlimJar; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.plugin.VolmitPlugin; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.plugin.chunk.ChunkTickets; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.Queue; -import com.volmit.iris.util.scheduling.ShurikenQueue; -import lombok.NonNull; -import org.bukkit.*; -import org.bukkit.block.data.BlockData; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.event.*; -import org.bukkit.generator.BiomeProvider; -import org.bukkit.generator.ChunkGenerator; -import org.bukkit.plugin.IllegalPluginAccessException; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.*; -import java.lang.annotation.Annotation; -import java.net.URL; -import java.util.*; -import java.util.function.Predicate; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@SuppressWarnings("CanBeFinal") -public class Iris extends VolmitPlugin implements Listener { - private static final Queue syncJobs = new ShurikenQueue<>(); - - public static Iris instance; - public static Bindings.Adventure audiences; - public static MultiverseCoreLink linkMultiverseCore; - public static IrisCompat compat; - public static FileWatcher configWatcher; - public static ChunkTickets tickets; - private static VolmitSender sender; - private static Thread shutdownHook; - - static { - try { - InstanceState.updateInstanceId(); - } catch (Throwable ignored) { - - } - } - - private final KList postShutdown = new KList<>(); - private KMap, IrisService> services; - - public static VolmitSender getSender() { - if (sender == null) { - sender = new VolmitSender(Bukkit.getConsoleSender()); - sender.setTag(instance.getTag()); - } - return sender; - } - - @SuppressWarnings("unchecked") - public static T service(Class c) { - return (T) instance.services.get(c); - } - - public static void callEvent(Event e) { - if (!e.isAsynchronous()) { - J.s(() -> Bukkit.getPluginManager().callEvent(e)); - } else { - Bukkit.getPluginManager().callEvent(e); - } - } - - public static KList initialize(String s, Class slicedClass) { - JarScanner js = new JarScanner(instance.getJarFile(), s); - KList v = new KList<>(); - J.attempt(js::scan); - for (Class i : js.getClasses()) { - if (slicedClass == null || i.isAnnotationPresent(slicedClass)) { - try { - v.add(i.getDeclaredConstructor().newInstance()); - } catch (Throwable ignored) { - - } - } - } - - return v; - } - - public static KList> getClasses(String s, Class slicedClass) { - JarScanner js = new JarScanner(instance.getJarFile(), s); - KList> v = new KList<>(); - J.attempt(js::scan); - for (Class i : js.getClasses()) { - if (slicedClass == null || i.isAnnotationPresent(slicedClass)) { - try { - v.add(i); - } catch (Throwable ignored) { - - } - } - } - - return v; - } - - public static KList initialize(String s) { - return initialize(s, null); - } - - public static void sq(Runnable r) { - synchronized (syncJobs) { - syncJobs.queue(r); - } - } - - public static File getTemp() { - return instance.getDataFolder("cache", "temp"); - } - - public static void msg(String string) { - try { - getSender().sendMessage(string); - } catch (Throwable e) { - try { - instance.getLogger().info(instance.getTag() + string.replaceAll("(<([^>]+)>)", "")); - } catch (Throwable ignored1) { - - } - } - } - - public static File getCached(String name, String url) { - String h = IO.hash(name + "@" + url); - File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h); - - if (!f.exists()) { - try (BufferedInputStream in = new BufferedInputStream(new URL(url).openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - fileOutputStream.write(dataBuffer, 0, bytesRead); - Iris.verbose("Aquiring " + name); - } - } catch (IOException e) { - Iris.reportError(e); - } - } - - return f.exists() ? f : null; - } - - public static String getNonCached(String name, String url) { - String h = IO.hash(name + "*" + url); - File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h); - - try (BufferedInputStream in = new BufferedInputStream(new URL(url).openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - fileOutputStream.write(dataBuffer, 0, bytesRead); - } - } catch (IOException e) { - Iris.reportError(e); - } - - try { - return IO.readAll(f); - } catch (IOException e) { - Iris.reportError(e); - } - - return ""; - } - - public static File getNonCachedFile(String name, String url) { - String h = IO.hash(name + "*" + url); - File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h); - Iris.verbose("Download " + name + " -> " + url); - try (BufferedInputStream in = new BufferedInputStream(new URL(url).openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - fileOutputStream.write(dataBuffer, 0, bytesRead); - } - - fileOutputStream.flush(); - } catch (IOException e) { - e.printStackTrace(); - Iris.reportError(e); - } - - return f; - } - - public static void warn(String format, Object... objs) { - msg(C.YELLOW + String.format(format, objs)); - } - - public static void error(String format, Object... objs) { - msg(C.RED + String.format(format, objs)); - } - - public static void debug(String string) { - if (!IrisSettings.get().getGeneral().isDebug()) { - return; - } - - try { - throw new RuntimeException(); - } catch (Throwable e) { - try { - String[] cc = e.getStackTrace()[1].getClassName().split("\\Q.\\E"); - - if (cc.length > 5) { - debug(cc[3] + "/" + cc[4] + "/" + cc[cc.length - 1], e.getStackTrace()[1].getLineNumber(), string); - } else { - debug(cc[3] + "/" + cc[4], e.getStackTrace()[1].getLineNumber(), string); - } - } catch (Throwable ex) { - debug("Origin", -1, string); - } - } - } - - public static void debug(String category, int line, String string) { - if (!IrisSettings.get().getGeneral().isDebug()) { - return; - } - if (IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { - msg("" + category + " <#bf3b76>" + line + " " + C.LIGHT_PURPLE + string.replaceAll("\\Q<\\E", "[").replaceAll("\\Q>\\E", "]")); - } else { - msg(C.BLUE + category + ":" + C.AQUA + line + C.RESET + C.LIGHT_PURPLE + " " + string.replaceAll("\\Q<\\E", "[").replaceAll("\\Q>\\E", "]")); - - } - } - - public static void verbose(String string) { - debug(string); - } - - public static void success(String string) { - msg(C.IRIS + string); - } - - public static void info(String format, Object... args) { - msg(C.WHITE + String.format(format, args)); - } - - @SuppressWarnings("deprecation") - public static void later(NastyRunnable object) { - try { - Bukkit.getScheduler().scheduleAsyncDelayedTask(instance, () -> - { - try { - object.run(); - } catch (Throwable e) { - e.printStackTrace(); - Iris.reportError(e); - } - }, RNG.r.i(100, 1200)); - } catch (IllegalPluginAccessException ignored) { - - } - } - - public static int jobCount() { - return syncJobs.size(); - } - - public static void clearQueues() { - synchronized (syncJobs) { - syncJobs.clear(); - } - } - - public static int getJavaVersion() { - String version = System.getProperty("java.version"); - if (version.startsWith("1.")) { - version = version.substring(2, 3); - } else { - int dot = version.indexOf("."); - if (dot != -1) { - version = version.substring(0, dot); - } - } - return Integer.parseInt(version); - } - - public static String getJava() { - String javaRuntimeName = System.getProperty("java.vm.name"); - String javaRuntimeVendor = System.getProperty("java.vendor"); - String javaRuntimeVersion = System.getProperty("java.vm.version"); - return String.format("%s %s (build %s)", javaRuntimeName, javaRuntimeVendor, javaRuntimeVersion); - } - - public static void reportErrorChunk(int x, int z, Throwable e, String extra) { - if (IrisSettings.get().getGeneral().isDebug()) { - File f = instance.getDataFile("debug", "chunk-errors", "chunk." + x + "." + z + ".txt"); - - if (!f.exists()) { - J.attempt(() -> { - PrintWriter pw = new PrintWriter(f); - pw.println("Thread: " + Thread.currentThread().getName()); - pw.println("First: " + new Date(M.ms())); - e.printStackTrace(pw); - pw.close(); - }); - } - - Iris.debug("Chunk " + x + "," + z + " Exception Logged: " + e.getClass().getSimpleName() + ": " + C.RESET + "" + C.LIGHT_PURPLE + e.getMessage()); - } - } - - public static void reportError(Throwable e) { - Bindings.capture(e); - if (IrisSettings.get().getGeneral().isDebug()) { - String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber(); - - if (e.getCause() != null) { - n += "-" + e.getCause().getStackTrace()[0].getClassName() + "-" + e.getCause().getStackTrace()[0].getLineNumber(); - } - - File f = instance.getDataFile("debug", "caught-exceptions", n + ".txt"); - - if (!f.exists()) { - J.attempt(() -> { - PrintWriter pw = new PrintWriter(f); - pw.println("Thread: " + Thread.currentThread().getName()); - pw.println("First: " + new Date(M.ms())); - e.printStackTrace(pw); - pw.close(); - }); - } - - Iris.debug("Exception Logged: " + e.getClass().getSimpleName() + ": " + C.RESET + "" + C.LIGHT_PURPLE + e.getMessage()); - } - } - - public static void dump() { - try { - File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt"); - FileOutputStream fos = new FileOutputStream(fi); - Map f = Thread.getAllStackTraces(); - PrintWriter pw = new PrintWriter(fos); - for (Thread i : f.keySet()) { - pw.println("========================================"); - pw.println("Thread: '" + i.getName() + "' ID: " + i.getId() + " STATUS: " + i.getState().name()); - - for (StackTraceElement j : f.get(i)) { - pw.println(" @ " + j.toString()); - } - - pw.println("========================================"); - pw.println(); - pw.println(); - } - pw.println("[%%__USER__%%,%%__RESOURCE__%%,%%__PRODUCT__%%,%%__BUILTBYBIT__%%]"); - - pw.close(); - Iris.info("DUMPED! See " + fi.getAbsolutePath()); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - public static void panic() { - EnginePanic.panic(); - } - - public static void addPanic(String s, String v) { - EnginePanic.add(s, v); - } - - public Iris() { - instance = this; - SlimJar.load(); - } - - private void enable() { - services = new KMap<>(); - setupAudience(); - Bindings.setupSentry(); - initialize("com.volmit.iris.core.service").forEach((i) -> services.put((Class) i.getClass(), (IrisService) i)); - IO.delete(new File("iris")); - compat = IrisCompat.configured(getDataFile("compat.json")); - ServerConfigurator.configure(); - IrisSafeguard.execute(); - getSender().setTag(getTag()); - IrisSafeguard.splash(); - tickets = new ChunkTickets(); - linkMultiverseCore = new MultiverseCoreLink(); - configWatcher = new FileWatcher(getDataFile("settings.json")); - services.values().forEach(IrisService::onEnable); - services.values().forEach(this::registerListener); - addShutdownHook(); - J.s(() -> { - J.a(() -> IO.delete(getTemp())); - J.a(LazyPregenerator::loadLazyGenerators, 100); - J.a(this::bstats); - J.ar(this::checkConfigHotload, 60); - J.sr(this::tickQueue, 0); - J.s(this::setupPapi); - J.a(ServerConfigurator::configure, 20); - - autoStartStudio(); - checkForBukkitWorlds(s -> true); - IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName()); - IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName()); - }); - } - - public void addShutdownHook() { - if (shutdownHook != null) { - Runtime.getRuntime().removeShutdownHook(shutdownHook); - } - shutdownHook = new Thread(() -> { - Bukkit.getWorlds() - .stream() - .map(IrisToolbelt::access) - .filter(Objects::nonNull) - .forEach(PlatformChunkGenerator::close); - - MultiBurst.burst.close(); - MultiBurst.ioBurst.close(); - services.clear(); - }); - Runtime.getRuntime().addShutdownHook(shutdownHook); - } - - public void checkForBukkitWorlds(Predicate filter) { - try { - IrisWorlds.readBukkitWorlds().forEach((s, generator) -> { - try { - if (Bukkit.getWorld(s) != null || !filter.test(s)) return; - - Iris.info("Loading World: %s | Generator: %s", s, generator); - var gen = getDefaultWorldGenerator(s, generator); - var dim = loadDimension(s, generator); - assert dim != null && gen != null; - - Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "..."); - WorldCreator c = new WorldCreator(s) - .generator(gen) - .environment(dim.getEnvironment()); - INMS.get().createWorld(c); - Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!"); - } catch (Throwable e) { - Iris.error("Failed to load world " + s + "!"); - e.printStackTrace(); - } - }); - } catch (Throwable e) { - e.printStackTrace(); - reportError(e); - } - } - - private void autoStartStudio() { - if (IrisSettings.get().getStudio().isAutoStartDefaultStudio()) { - Iris.info("Starting up auto Studio!"); - try { - Player r = new KList<>(getServer().getOnlinePlayers()).getRandom(); - Iris.service(StudioSVC.class).open(r != null ? new VolmitSender(r) : getSender(), 1337, IrisSettings.get().getGenerator().getDefaultWorldType(), (w) -> { - J.s(() -> { - var spawn = w.getSpawnLocation(); - for (Player i : getServer().getOnlinePlayers()) { - i.setGameMode(GameMode.SPECTATOR); - i.teleport(spawn); - } - }); - }); - } catch (IrisException e) { - reportError(e); - } - } - } - - private void setupAudience() { - try { - audiences = new Bindings.Adventure(this); - } catch (Throwable e) { - e.printStackTrace(); - IrisSettings.get().getGeneral().setUseConsoleCustomColors(false); - IrisSettings.get().getGeneral().setUseCustomColorsIngame(false); - Iris.error("Failed to setup Adventure API... No custom colors :("); - } - } - - public void postShutdown(Runnable r) { - postShutdown.add(r); - } - - public void onEnable() { - enable(); - super.onEnable(); - Bukkit.getPluginManager().registerEvents(this, this); - } - - public void onDisable() { - if (IrisSafeguard.isForceShutdown()) return; - services.values().forEach(IrisService::onDisable); - Bukkit.getScheduler().cancelTasks(this); - HandlerList.unregisterAll((Plugin) this); - postShutdown.forEach(Runnable::run); - super.onDisable(); - - J.attempt(new JarScanner(instance.getJarFile(), "", false)::scanAll); - } - - private void setupPapi() { - if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { - new IrisPapiExpansion().register(); - } - } - - @Override - public void start() { - - } - - @Override - public void stop() { - - } - - @Override - public String getTag(String subTag) { - return IrisSafeguard.mode().tag(subTag); - } - - private void checkConfigHotload() { - if (configWatcher.checkModified()) { - IrisSettings.invalidate(); - IrisSettings.get(); - configWatcher.checkModified(); - Iris.info("Hotloaded settings.json "); - } - } - - private void tickQueue() { - synchronized (Iris.syncJobs) { - if (!Iris.syncJobs.hasNext()) { - return; - } - - long ms = M.ms(); - - while (Iris.syncJobs.hasNext() && M.ms() - ms < 25) { - try { - Iris.syncJobs.next().run(); - } catch (Throwable e) { - e.printStackTrace(); - Iris.reportError(e); - } - } - } - } - - private void bstats() { - if (IrisSettings.get().getGeneral().isPluginMetrics()) { - Bindings.setupBstats(this); - } - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - return super.onCommand(sender, command, label, args); - } - - public void imsg(CommandSender s, String msg) { - s.sendMessage(C.IRIS + "[" + C.DARK_GRAY + "Iris" + C.IRIS + "]" + C.GRAY + ": " + msg); - } - - @Nullable - @Override - public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) { - Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id); - return super.getDefaultBiomeProvider(worldName, id); - } - - @Override - public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { - Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id); - if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType(); - Iris.debug("Generator ID: " + id + " requested by bukkit/plugin"); - IrisDimension dim = loadDimension(worldName, id); - if (dim == null) { - throw new RuntimeException("Can't find dimension " + id + "!"); - } - - Iris.debug("Assuming IrisDimension: " + dim.getName()); - - IrisWorld w = IrisWorld.builder() - .name(worldName) - .seed(1337) - .environment(dim.getEnvironment()) - .worldFolder(new File(Bukkit.getWorldContainer(), worldName)) - .minHeight(dim.getMinHeight()) - .maxHeight(dim.getMaxHeight()) - .build(); - - Iris.debug("Generator Config: " + w.toString()); - - File ff = new File(w.worldFolder(), "iris/pack"); - var files = ff.listFiles(); - if (files == null || files.length == 0) - IO.delete(ff); - - if (!ff.exists()) { - ff.mkdirs(); - service(StudioSVC.class).installIntoWorld(getSender(), dim.getLoadKey(), w.worldFolder()); - } - - return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey()); - } - - @Nullable - public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) { - File pack = new File(Bukkit.getWorldContainer(), String.join(File.separator, worldName, "iris", "pack")); - var dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null; - if (dimension == null) dimension = IrisData.loadAnyDimension(id, null); - if (dimension == null) { - Iris.warn("Unable to find dimension type " + id + " Looking for online packs..."); - Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false); - dimension = IrisData.loadAnyDimension(id, null); - - if (dimension != null) { - Iris.info("Resolved missing dimension, proceeding."); - } - } - - return dimension; - } - - public void splash() { - Iris.info("Server type & version: " + Bukkit.getName() + " v" + Bukkit.getVersion()); - Iris.info("Custom Biomes: " + INMS.get().countCustomBiomes()); - printPacks(); - - IrisSafeguard.mode().trySplash(); - } - - private void printPacks() { - File packFolder = Iris.service(StudioSVC.class).getWorkspaceFolder(); - File[] packs = packFolder.listFiles(File::isDirectory); - if (packs == null || packs.length == 0) - return; - Iris.info("Custom Dimensions: " + packs.length); - for (File f : packs) - printPack(f); - } - - private void printPack(File pack) { - String dimName = pack.getName(); - String version = "???"; - try (FileReader r = new FileReader(new File(pack, "dimensions/" + dimName + ".json"))) { - JsonObject json = JsonParser.parseReader(r).getAsJsonObject(); - if (json.has("version")) - version = json.get("version").getAsString(); - } catch (IOException | JsonParseException ignored) { - } - Iris.info(" " + dimName + " v" + version); - } - - public int getIrisVersion() { - String input = Iris.instance.getDescription().getVersion(); - int hyphenIndex = input.indexOf('-'); - if (hyphenIndex != -1) { - String result = input.substring(0, hyphenIndex); - result = result.replaceAll("\\.", ""); - return Integer.parseInt(result); - } - return -1; - } - - public int getMCVersion() { - try { - String version = Bukkit.getVersion(); - Matcher matcher = Pattern.compile("\\(MC: ([\\d.]+)\\)").matcher(version); - if (matcher.find()) { - version = matcher.group(1).replaceAll("\\.", ""); - long versionNumber = Long.parseLong(version); - if (versionNumber > Integer.MAX_VALUE) { - return -1; - } - return (int) versionNumber; - } - return -1; - } catch (Exception e) { - return -1; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/IrisSettings.java b/core/src/main/java/com/volmit/iris/core/IrisSettings.java deleted file mode 100644 index c2c394c32..000000000 --- a/core/src/main/java/com/volmit/iris/core/IrisSettings.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core; - -import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONException; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.misc.getHardware; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.File; -import java.io.IOException; - -@SuppressWarnings("SynchronizeOnNonFinalField") -@Data -public class IrisSettings { - public static IrisSettings settings; - private IrisSettingsGeneral general = new IrisSettingsGeneral(); - private IrisSettingsWorld world = new IrisSettingsWorld(); - private IrisSettingsGUI gui = new IrisSettingsGUI(); - private IrisSettingsAutoconfiguration autoConfiguration = new IrisSettingsAutoconfiguration(); - private IrisSettingsGenerator generator = new IrisSettingsGenerator(); - private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency(); - private IrisSettingsStudio studio = new IrisSettingsStudio(); - private IrisSettingsPerformance performance = new IrisSettingsPerformance(); - private IrisSettingsUpdater updater = new IrisSettingsUpdater(); - private IrisSettingsPregen pregen = new IrisSettingsPregen(); - private IrisSettingsSentry sentry = new IrisSettingsSentry(); - - public static int getThreadCount(int c) { - return Math.max(switch (c) { - case -1, -2, -4 -> Runtime.getRuntime().availableProcessors() / -c; - default -> Math.max(c, 2); - }, 1); - } - - public static IrisSettings get() { - if (settings != null) { - return settings; - } - - settings = new IrisSettings(); - - File s = Iris.instance.getDataFile("settings.json"); - - if (!s.exists()) { - try { - IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4)); - } catch (JSONException | IOException e) { - e.printStackTrace(); - Iris.reportError(e); - } - } else { - try { - String ss = IO.readAll(s); - settings = new Gson().fromJson(ss, IrisSettings.class); - try { - IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4)); - } catch (IOException e) { - e.printStackTrace(); - } - } catch (Throwable ee) { - // Iris.reportError(ee); causes a self-reference & stackoverflow - Iris.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage()); - } - } - - return settings; - } - - public static void invalidate() { - synchronized (settings) { - settings = null; - } - } - - public void forceSave() { - File s = Iris.instance.getDataFile("settings.json"); - - try { - IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4)); - } catch (JSONException | IOException e) { - e.printStackTrace(); - Iris.reportError(e); - } - } - - @Data - public static class IrisSettingsAutoconfiguration { - public boolean configureSpigotTimeoutTime = true; - public boolean configurePaperWatchdogDelay = true; - public boolean autoRestartOnCustomBiomeInstall = true; - } - - @Data - public static class IrisAsyncTeleport { - public boolean enabled = false; - public int loadViewDistance = 2; - public boolean urgent = false; - } - - @Data - public static class IrisSettingsWorld { - public IrisAsyncTeleport asyncTeleport = new IrisAsyncTeleport(); - public boolean postLoadBlockUpdates = true; - public boolean forcePersistEntities = true; - public boolean anbientEntitySpawningSystem = true; - public long asyncTickIntervalMS = 700; - public double targetSpawnEntitiesPerChunk = 0.95; - public boolean markerEntitySpawningSystem = true; - public boolean effectSystem = true; - public boolean worldEditWandCUI = true; - public boolean globalPregenCache = false; - } - - @Data - public static class IrisSettingsConcurrency { - public int parallelism = -1; - public int ioParallelism = -2; - public int worldGenParallelism = -1; - - public int getWorldGenThreads() { - return getThreadCount(worldGenParallelism); - } - } - - @Data - public static class IrisSettingsPregen { - public boolean useCacheByDefault = true; - public boolean useHighPriority = false; - public boolean useVirtualThreads = false; - public boolean useTicketQueue = true; - public int maxConcurrency = 256; - } - - @Data - public static class IrisSettingsPerformance { - private IrisSettingsEngineSVC engineSVC = new IrisSettingsEngineSVC(); - public boolean trimMantleInStudio = false; - public int mantleKeepAlive = 30; - public int noiseCacheSize = 1_024; - public int resourceLoaderCacheSize = 1_024; - public int objectLoaderCacheSize = 4_096; - public int scriptLoaderCacheSize = 512; - public int tectonicPlateSize = -1; - public int mantleCleanupDelay = 200; - - public int getTectonicPlateSize() { - if (tectonicPlateSize > 0) - return tectonicPlateSize; - - return (int) (getHardware.getProcessMemory() / 512L); - } - } - - @Data - public static class IrisSettingsUpdater { - public int maxConcurrency = 256; - public boolean nativeThreads = false; - public double threadMultiplier = 2; - - public double chunkLoadSensitivity = 0.7; - public MsRange emptyMsRange = new MsRange(80, 100); - public MsRange defaultMsRange = new MsRange(20, 40); - - public int getMaxConcurrency() { - return Math.max(Math.abs(maxConcurrency), 1); - } - - public double getThreadMultiplier() { - return Math.min(Math.abs(threadMultiplier), 0.1); - } - - public double getChunkLoadSensitivity() { - return Math.min(chunkLoadSensitivity, 0.9); - } - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class MsRange { - public int min = 20; - public int max = 40; - } - - @Data - public static class IrisSettingsGeneral { - public boolean DoomsdayAnnihilationSelfDestructMode = false; - public boolean commandSounds = true; - public boolean debug = false; - public boolean dumpMantleOnError = false; - public boolean disableNMS = false; - public boolean pluginMetrics = true; - public boolean splashLogoStartup = true; - public boolean useConsoleCustomColors = true; - public boolean useCustomColorsIngame = true; - public boolean adjustVanillaHeight = false; - public String forceMainWorld = ""; - public int spinh = -20; - public int spins = 7; - public int spinb = 8; - public String cartographerMessage = "Iris does not allow cartographers in its world due to crashes."; - - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean canUseCustomColors(VolmitSender volmitSender) { - return volmitSender.isPlayer() ? useCustomColorsIngame : useConsoleCustomColors; - } - } - - @Data - public static class IrisSettingsSentry { - public boolean includeServerId = true; - public boolean disableAutoReporting = false; - public boolean debug = false; - } - - @Data - public static class IrisSettingsGUI { - public boolean useServerLaunchedGuis = true; - public boolean maximumPregenGuiFPS = false; - public boolean colorMode = true; - } - - @Data - public static class IrisSettingsGenerator { - public String defaultWorldType = "overworld"; - public int maxBiomeChildDepth = 4; - public boolean preventLeafDecay = true; - public boolean useMulticore = false; - public boolean useMulticoreMantle = false; - public boolean offsetNoiseTypes = false; - public boolean earlyCustomBlocks = false; - } - - @Data - public static class IrisSettingsStudio { - public boolean studio = true; - public boolean openVSCode = true; - public boolean disableTimeAndWeather = true; - public boolean autoStartDefaultStudio = false; - } - - @Data - public static class IrisSettingsEngineSVC { - public boolean useVirtualThreads = true; - public boolean forceMulticoreWrite = false; - public int priority = Thread.NORM_PRIORITY; - - public int getPriority() { - return Math.max(Math.min(priority, Thread.MAX_PRIORITY), Thread.MIN_PRIORITY); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/IrisWorlds.java b/core/src/main/java/com/volmit/iris/core/IrisWorlds.java deleted file mode 100644 index 72dc0b5cd..000000000 --- a/core/src/main/java/com/volmit/iris/core/IrisWorlds.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.volmit.iris.core; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.misc.ServerProperties; -import org.bukkit.Bukkit; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.lang.reflect.Type; -import java.util.Objects; -import java.util.stream.Stream; - -public class IrisWorlds { - private static final AtomicCache cache = new AtomicCache<>(); - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - private static final Type TYPE = TypeToken.getParameterized(KMap.class, String.class, String.class).getType(); - private final KMap worlds; - private volatile boolean dirty = false; - - private IrisWorlds(KMap worlds) { - this.worlds = worlds; - readBukkitWorlds().forEach(this::put0); - save(); - } - - public static IrisWorlds get() { - return cache.aquire(() -> { - File file = Iris.instance.getDataFile("worlds.json"); - if (!file.exists()) { - return new IrisWorlds(new KMap<>()); - } - - try { - String json = IO.readAll(file); - KMap worlds = GSON.fromJson(json, TYPE); - return new IrisWorlds(Objects.requireNonNullElseGet(worlds, KMap::new)); - } catch (Throwable e) { - Iris.error("Failed to load worlds.json!"); - e.printStackTrace(); - Iris.reportError(e); - } - - return new IrisWorlds(new KMap<>()); - }); - } - - public void put(String name, String type) { - put0(name, type); - save(); - } - - private void put0(String name, String type) { - String old = worlds.put(name, type); - if (!type.equals(old)) - dirty = true; - } - - public KMap getWorlds() { - clean(); - return readBukkitWorlds().put(worlds); - } - - public Stream getPacks() { - return getDimensions() - .map(IrisDimension::getLoader) - .filter(Objects::nonNull); - } - - public Stream getDimensions() { - return getWorlds() - .entrySet() - .stream() - .map(entry -> Iris.loadDimension(entry.getKey(), entry.getValue())) - .filter(Objects::nonNull); - } - - public void clean() { - dirty = worlds.entrySet().removeIf(entry -> !new File(Bukkit.getWorldContainer(), entry.getKey() + "/iris/pack/dimensions/" + entry.getValue() + ".json").exists()); - } - - public synchronized void save() { - clean(); - if (!dirty) return; - try { - IO.write(Iris.instance.getDataFile("worlds.json"), OutputStreamWriter::new, writer -> GSON.toJson(worlds, TYPE, writer)); - dirty = false; - } catch (IOException e) { - Iris.error("Failed to save worlds.json!"); - e.printStackTrace(); - Iris.reportError(e); - } - } - - public static KMap readBukkitWorlds() { - var bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML); - var worlds = bukkit.getConfigurationSection("worlds"); - if (worlds == null) return new KMap<>(); - - var result = new KMap(); - for (String world : worlds.getKeys(false)) { - var gen = worlds.getString(world + ".generator"); - if (gen == null) continue; - - String loadKey; - if (gen.equalsIgnoreCase("iris")) { - loadKey = IrisSettings.get().getGenerator().getDefaultWorldType(); - } else if (gen.startsWith("Iris:")) { - loadKey = gen.substring(5); - } else continue; - - result.put(world, loadKey); - } - - return result; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/ServerConfigurator.java b/core/src/main/java/com/volmit/iris/core/ServerConfigurator.java deleted file mode 100644 index b4bd3e083..000000000 --- a/core/src/main/java/com/volmit/iris/core/ServerConfigurator.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.nms.datapack.DataVersion; -import com.volmit.iris.core.nms.datapack.IDataFixer; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.misc.ServerProperties; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import lombok.NonNull; -import lombok.SneakyThrows; -import org.bukkit.Bukkit; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicIntegerArray; -import java.util.stream.Stream; - -public class ServerConfigurator { - public static void configure() { - IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration(); - if (s.isConfigureSpigotTimeoutTime()) { - J.attempt(ServerConfigurator::increaseKeepAliveSpigot); - } - - if (s.isConfigurePaperWatchdogDelay()) { - J.attempt(ServerConfigurator::increasePaperWatchdog); - } - - installDataPacks(true); - } - - private static void increaseKeepAliveSpigot() throws IOException, InvalidConfigurationException { - File spigotConfig = new File("spigot.yml"); - FileConfiguration f = new YamlConfiguration(); - f.load(spigotConfig); - long tt = f.getLong("settings.timeout-time"); - - long spigotTimeout = TimeUnit.MINUTES.toSeconds(5); - - if (tt < spigotTimeout) { - Iris.warn("Updating spigot.yml timeout-time: " + tt + " -> " + spigotTimeout + " (5 minutes)"); - Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value."); - f.set("settings.timeout-time", spigotTimeout); - f.save(spigotConfig); - } - } - private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException { - File spigotConfig = new File("config/paper-global.yml"); - FileConfiguration f = new YamlConfiguration(); - f.load(spigotConfig); - long tt = f.getLong("watchdog.early-warning-delay"); - - long watchdog = TimeUnit.MINUTES.toMillis(3); - if (tt < watchdog) { - Iris.warn("Updating paper.yml watchdog early-warning-delay: " + tt + " -> " + watchdog + " (3 minutes)"); - Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value."); - f.set("watchdog.early-warning-delay", watchdog); - f.save(spigotConfig); - } - } - - private static KList getDatapacksFolder() { - if (!IrisSettings.get().getGeneral().forceMainWorld.isEmpty()) { - return new KList().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks")); - } - KList worlds = new KList<>(); - Bukkit.getServer().getWorlds().forEach(w -> worlds.add(new File(w.getWorldFolder(), "datapacks"))); - if (worlds.isEmpty()) worlds.add(new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME + "/datapacks")); - return worlds; - } - - public static boolean installDataPacks(boolean fullInstall) { - return installDataPacks(DataVersion.getDefault(), fullInstall); - } - - public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall) { - if (fixer == null) { - Iris.error("Unable to install datapacks, fixer is null!"); - return false; - } - Iris.info("Checking Data Packs..."); - DimensionHeight height = new DimensionHeight(fixer); - KList folders = getDatapacksFolder(); - KMap> biomes = new KMap<>(); - - try (Stream stream = allPacks()) { - stream.flatMap(height::merge) - .parallel() - .forEach(dim -> { - Iris.verbose(" Checking Dimension " + dim.getLoadFile().getPath()); - dim.installBiomes(fixer, dim::getLoader, folders, biomes.computeIfAbsent(dim.getLoadKey(), k -> new KSet<>())); - dim.installDimensionType(fixer, folders); - }); - } - IrisDimension.writeShared(folders, height); - Iris.info("Data Packs Setup!"); - - return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall()); - } - - private static boolean verifyDataPacksPost(boolean allowRestarting) { - try (Stream stream = allPacks()) { - boolean bad = stream - .map(data -> { - Iris.verbose("Checking Pack: " + data.getDataFolder().getPath()); - var loader = data.getDimensionLoader(); - return loader.loadAll(loader.getPossibleKeys()) - .stream() - .filter(Objects::nonNull) - .map(ServerConfigurator::verifyDataPackInstalled) - .toList() - .contains(false); - }) - .toList() - .contains(true); - if (!bad) return false; - } - - - if (allowRestarting) { - restart(); - } else if (INMS.get().supportsDataPacks()) { - Iris.error("============================================================================"); - Iris.error(C.ITALIC + "You need to restart your server to properly generate custom biomes."); - Iris.error(C.ITALIC + "By continuing, Iris will use backup biomes in place of the custom biomes."); - Iris.error("----------------------------------------------------------------------------"); - Iris.error(C.UNDERLINE + "IT IS HIGHLY RECOMMENDED YOU RESTART THE SERVER BEFORE GENERATING!"); - Iris.error("============================================================================"); - - for (Player i : Bukkit.getOnlinePlayers()) { - if (i.isOp() || i.hasPermission("iris.all")) { - VolmitSender sender = new VolmitSender(i, Iris.instance.getTag("WARNING")); - sender.sendMessage("There are some Iris Packs that have custom biomes in them"); - sender.sendMessage("You need to restart your server to use these packs."); - } - } - - J.sleep(3000); - } - return true; - } - - public static void restart() { - J.s(() -> { - Iris.warn("New data pack entries have been installed in Iris! Restarting server!"); - Iris.warn("This will only happen when your pack changes (updates/first time setup)"); - Iris.warn("(You can disable this auto restart in iris settings)"); - J.s(() -> { - Iris.warn("Looks like the restart command didn't work. Stopping the server instead!"); - Bukkit.shutdown(); - }, 100); - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "restart"); - }); - } - - public static boolean verifyDataPackInstalled(IrisDimension dimension) { - KSet keys = new KSet<>(); - boolean warn = false; - - for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) { - if (i.isCustom()) { - for (IrisBiomeCustom j : i.getCustomDerivitives()) { - keys.add(dimension.getLoadKey() + ":" + j.getId()); - } - } - } - String key = getWorld(dimension.getLoader()); - if (key == null) key = dimension.getLoadKey(); - else key += "/" + dimension.getLoadKey(); - - if (!INMS.get().supportsDataPacks()) { - if (!keys.isEmpty()) { - Iris.warn("==================================================================================="); - Iris.warn("Pack " + key + " has " + keys.size() + " custom biome(s). "); - Iris.warn("Your server version does not yet support datapacks for iris."); - Iris.warn("The world will generate these biomes as backup biomes."); - Iris.warn("===================================================================================="); - } - - return true; - } - - for (String i : keys) { - Object o = INMS.get().getCustomBiomeBaseFor(i); - - if (o == null) { - Iris.warn("The Biome " + i + " is not registered on the server."); - warn = true; - } - } - - if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) { - Iris.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server."); - warn = true; - } - - if (warn) { - Iris.error("The Pack " + key + " is INCAPABLE of generating custom biomes"); - Iris.error("If not done automatically, restart your server before generating with this pack!"); - } - - return !warn; - } - - public static Stream allPacks() { - return Stream.concat(listFiles(Iris.instance.getDataFolder("packs")) - .filter(File::isDirectory) - .filter( base -> { - var content = new File(base, "dimensions").listFiles(); - return content != null && content.length > 0; - }) - .map(IrisData::get), IrisWorlds.get().getPacks()); - } - - @Nullable - public static String getWorld(@NonNull IrisData data) { - String worldContainer = Bukkit.getWorldContainer().getAbsolutePath(); - if (!worldContainer.endsWith(File.separator)) worldContainer += File.separator; - - String path = data.getDataFolder().getAbsolutePath(); - if (!path.startsWith(worldContainer)) return null; - int l = path.endsWith(File.separator) ? 11 : 10; - return path.substring(worldContainer.length(), path.length() - l); - } - - @SneakyThrows - private static Stream listFiles(File parent) { - if (!parent.isDirectory()) return Stream.empty(); - return Files.walk(parent.toPath()).map(Path::toFile); - } - - public static class DimensionHeight { - private final IDataFixer fixer; - private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3]; - - public DimensionHeight(IDataFixer fixer) { - this.fixer = fixer; - for (int i = 0; i < 3; i++) { - dimensions[i] = new AtomicIntegerArray(new int[]{ - Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE - }); - } - } - - public Stream merge(IrisData data) { - Iris.verbose("Checking Pack: " + data.getDataFolder().getPath()); - var loader = data.getDimensionLoader(); - return loader.loadAll(loader.getPossibleKeys()) - .stream() - .filter(Objects::nonNull) - .peek(this::merge); - } - - public void merge(IrisDimension dimension) { - AtomicIntegerArray array = dimensions[dimension.getBaseDimension().ordinal()]; - array.updateAndGet(0, min -> Math.min(min, dimension.getMinHeight())); - array.updateAndGet(1, max -> Math.max(max, dimension.getMaxHeight())); - array.updateAndGet(2, logical -> Math.max(logical, dimension.getLogicalHeight())); - } - - public String[] jsonStrings() { - var dims = IDataFixer.Dimension.values(); - var arr = new String[3]; - for (int i = 0; i < 3; i++) { - arr[i] = jsonString(dims[i]); - } - return arr; - } - - public String jsonString(IDataFixer.Dimension dimension) { - var data = dimensions[dimension.ordinal()]; - int minY = data.get(0); - int maxY = data.get(1); - int logicalHeight = data.get(2); - if (minY == Integer.MAX_VALUE || maxY == Integer.MIN_VALUE || Integer.MIN_VALUE == logicalHeight) - return null; - return fixer.createDimension(dimension, minY, maxY - minY, logicalHeight, null).toString(4); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java b/core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java deleted file mode 100644 index d54635c2a..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java +++ /dev/null @@ -1,580 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.volmit.iris.Iris; -import com.volmit.iris.core.ServerConfigurator; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.nms.datapack.DataVersion; -import com.volmit.iris.core.service.IrisEngineSVC; -import com.volmit.iris.core.tools.IrisPackBenchmarking; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.engine.object.IrisPosition; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.engine.object.IrisJigsawStructurePlacement; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.DecreeOrigin; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.decree.specialhandlers.NullableDimensionHandler; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.CountingDataInputStream; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.mantle.TectonicPlate; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.matter.Matter; -import com.volmit.iris.util.nbt.mca.MCAFile; -import com.volmit.iris.util.nbt.mca.MCAUtil; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.jobs.Job; -import lombok.SneakyThrows; -import net.jpountz.lz4.LZ4BlockInputStream; -import net.jpountz.lz4.LZ4BlockOutputStream; -import net.jpountz.lz4.LZ4FrameInputStream; -import net.jpountz.lz4.LZ4FrameOutputStream; -import org.apache.commons.lang.RandomStringUtils; -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.World; - -import java.io.*; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.nio.file.Files; -import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -@Decree(name = "Developer", origin = DecreeOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"}) -public class CommandDeveloper implements DecreeExecutor { - private CommandTurboPregen turboPregen; - private CommandLazyPregen lazyPregen; - - @Decree(description = "Get Loaded TectonicPlates Count", origin = DecreeOrigin.BOTH, sync = true) - public void EngineStatus() { - Iris.service(IrisEngineSVC.class) - .engineStatus(sender()); - } - - @Decree(description = "Send a test exception to sentry") - public void Sentry() { - Engine engine = engine(); - if (engine != null) IrisContext.getOr(engine); - Iris.reportError(new Exception("This is a test")); - } - - @Decree(description = "Dev cmd to fix all the broken objects caused by faulty shrinkwarp") - public void fixObjects( - @Param(aliases = "dimension", description = "The dimension type to create the world with") - IrisDimension type - ) { - if (type == null) { - sender().sendMessage("Type cant be null?"); - return; - } - - IrisData dm = IrisData.get(Iris.instance.getDataFolder("packs", type.getLoadKey())); - var loader = dm.getObjectLoader(); - var processed = new KMap(); - - var objects = loader.getPossibleKeys(); - var pieces = dm.getJigsawPieceLoader().getPossibleKeys(); - var sender = sender(); - - sender.sendMessage(C.IRIS + "Found " + objects.length + " objects in " + type.getLoadKey()); - sender.sendMessage(C.IRIS + "Found " + pieces.length + " jigsaw pieces in " + type.getLoadKey()); - - final int total = objects.length; - final AtomicInteger completed = new AtomicInteger(); - final AtomicInteger changed = new AtomicInteger(); - - new Job() { - @Override - public String getName() { - return "Fixing Objects"; - } - - @Override - public void execute() { - Arrays.stream(pieces).parallel() - .map(dm.getJigsawPieceLoader()::load) - .filter(Objects::nonNull) - .forEach(piece -> { - var offset = processed.compute(piece.getObject(), (key, o) -> { - if (o != null) return o; - var obj = loader.load(key); - if (obj == null) return new IrisPosition(); - - obj.shrinkwrap(); - try { - if (!obj.getShrinkOffset().isZero()) { - changed.incrementAndGet(); - obj.write(obj.getLoadFile()); - } - completeWork(); - } catch (IOException e) { - Iris.error("Failed to write object " + obj.getLoadKey()); - e.printStackTrace(); - return new IrisPosition(); - } - - return new IrisPosition(obj.getShrinkOffset()); - }); - if (offset.getX() == 0 && offset.getY() == 0 && offset.getZ() == 0) - return; - - piece.getConnectors().forEach(connector -> connector.setPosition(connector.getPosition().add(offset))); - - try { - IO.writeAll(piece.getLoadFile(), dm.getGson().toJson(piece)); - } catch (IOException e) { - Iris.error("Failed to write jigsaw piece " + piece.getLoadKey()); - e.printStackTrace(); - } - }); - - Arrays.stream(loader.getPossibleKeys()).parallel() - .filter(key -> !processed.containsKey(key)) - .map(loader::load) - .forEach(obj -> { - if (obj == null) { - completeWork(); - return; - } - - obj.shrinkwrap(); - if (obj.getShrinkOffset().isZero()) { - completeWork(); - return; - } - - try { - obj.write(obj.getLoadFile()); - completeWork(); - changed.incrementAndGet(); - } catch (IOException e) { - Iris.error("Failed to write object " + obj.getLoadKey()); - e.printStackTrace(); - } - }); - } - - @Override - public void completeWork() { - completed.incrementAndGet(); - } - - @Override - public int getTotalWork() { - return total; - } - - @Override - public int getWorkCompleted() { - return completed.get(); - } - }.execute(sender, () -> { - var failed = total - completed.get(); - if (failed != 0) sender.sendMessage(C.IRIS + "" + failed + " objects failed!"); - if (changed.get() != 0) sender.sendMessage(C.IRIS + "" + changed.get() + " objects had their offsets changed!"); - else sender.sendMessage(C.IRIS + "No objects had their offsets changed!"); - }); - } - - @Decree(description = "Test") - public void mantle(@Param(defaultValue = "false") boolean plate, @Param(defaultValue = "21474836474") String name) throws Throwable { - var base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin"); - var section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin"); - - //extractSection(base, section, 5604930, 4397); - - if (plate) { - try (var in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) { - new TectonicPlate(1088, in, true); - } catch (Throwable e) { - e.printStackTrace(); - } - } else Matter.read(section); - if (!TectonicPlate.hasError()) - Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath()); - } - - private void extractSection(File source, File target, long offset, int length) throws IOException { - var raf = new RandomAccessFile(source, "r"); - var bytes = new byte[length]; - raf.seek(offset); - raf.readFully(bytes); - raf.close(); - Files.write(target.toPath(), bytes); - } - - @Decree(description = "Test") - public void dumpThreads() { - try { - File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt"); - FileOutputStream fos = new FileOutputStream(fi); - Map f = Thread.getAllStackTraces(); - PrintWriter pw = new PrintWriter(fos); - - pw.println(Thread.activeCount() + "/" + f.size()); - var run = Runtime.getRuntime(); - pw.println("Memory:"); - pw.println("\tMax: " + run.maxMemory()); - pw.println("\tTotal: " + run.totalMemory()); - pw.println("\tFree: " + run.freeMemory()); - pw.println("\tUsed: " + (run.totalMemory() - run.freeMemory())); - - for (Thread i : f.keySet()) { - pw.println("========================================"); - pw.println("Thread: '" + i.getName() + "' ID: " + i.getId() + " STATUS: " + i.getState().name()); - - for (StackTraceElement j : f.get(i)) { - pw.println(" @ " + j.toString()); - } - - pw.println("========================================"); - pw.println(); - pw.println(); - } - - pw.close(); - Iris.info("DUMPED! See " + fi.getAbsolutePath()); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - @SneakyThrows - @Decree(description = "Generate Iris structures for all loaded datapack structures") - public void generateStructures( - @Param(description = "The pack to add the generated structures to", aliases = "pack", defaultValue = "null", customHandler = NullableDimensionHandler.class) - IrisDimension dimension, - @Param(description = "Ignore existing structures", defaultValue = "false") - boolean force - ) { - var map = INMS.get().collectStructures(); - if (map.isEmpty()) { - sender().sendMessage(C.IRIS + "No structures found"); - return; - } - - sender().sendMessage(C.IRIS + "Found " + map.size() + " structures"); - - final File dataDir; - final IrisData data; - final Set existingStructures; - final Map> snippets; - final File dimensionFile; - final File structuresFolder; - final File snippetsFolder; - - var dimensionObj = new JsonObject(); - - if (dimension == null) { - dataDir = Iris.instance.getDataFolder("structures"); - IO.delete(dataDir); - data = IrisData.get(dataDir); - existingStructures = Set.of(); - snippets = Map.of(); - dimensionFile = new File(dataDir, "structures.json"); - } else { - data = dimension.getLoader(); - dataDir = data.getDataFolder(); - existingStructures = new KSet<>(data.getJigsawStructureLoader().getPossibleKeys()); - - dimensionObj = data.getGson().fromJson(IO.readAll(dimension.getLoadFile()), JsonObject.class); - snippets = Optional.ofNullable(dimensionObj.getAsJsonArray("jigsawStructures")) - .map(array -> array.asList() - .stream() - .filter(JsonElement::isJsonPrimitive) - .collect(Collectors.toMap(element -> data.getGson() - .fromJson(element, IrisJigsawStructurePlacement.class) - .getStructure(), - element -> Set.of(element.getAsString()), - KSet::merge))) - .orElse(Map.of()); - - dimensionFile = dimension.getLoadFile(); - } - structuresFolder = new File(dataDir, "jigsaw-structures"); - snippetsFolder = new File(dataDir, "snippet" + "/" + IrisJigsawStructurePlacement.class.getAnnotation(Snippet.class).value()); - - var gson = data.getGson(); - var jigsawStructures = Optional.ofNullable(dimensionObj.getAsJsonArray("jigsawStructures")) - .orElse(new JsonArray(map.size())); - - map.forEach((key, placement) -> { - String loadKey = "datapack/" + key.namespace() + "/" + key.key(); - if (existingStructures.contains(loadKey) && !force) - return; - - var structures = placement.structures(); - var obj = placement.toJson(loadKey); - if (obj == null || structures.isEmpty()) { - sender().sendMessage(C.RED + "Failed to generate hook for " + key); - return; - } - File snippetFile = new File(snippetsFolder, loadKey + ".json"); - try { - IO.writeAll(snippetFile, gson.toJson(obj)); - } catch (IOException e) { - sender().sendMessage(C.RED + "Failed to generate snippet for " + key); - e.printStackTrace(); - return; - } - - Set loadKeys = snippets.getOrDefault(loadKey, Set.of(loadKey)); - jigsawStructures.asList().removeIf(e -> loadKeys.contains((e.isJsonObject() ? e.getAsJsonObject().get("structure") : e).getAsString())); - jigsawStructures.add("snippet/" + loadKey); - - String structureKey; - if (structures.size() > 1) { - KList common = new KList<>(); - for (int i = 0; i < structures.size(); i++) { - var tags = structures.get(i).tags(); - if (i == 0) common.addAll(tags); - else common.removeIf(tag -> !tags.contains(tag)); - } - structureKey = common.isNotEmpty() ? "#" + common.getFirst() : structures.getFirst().key(); - } else structureKey = structures.getFirst().key(); - - JsonArray array = new JsonArray(); - if (structures.size() > 1) { - structures.stream() - .flatMap(structure -> { - String[] arr = new String[structure.weight()]; - Arrays.fill(arr, structure.key()); - return Arrays.stream(arr); - }) - .forEach(array::add); - } else array.add(structureKey); - - obj = new JsonObject(); - obj.addProperty("structureKey", structureKey); - obj.add("datapackStructures", array); - - File out = new File(structuresFolder, loadKey + ".json"); - out.getParentFile().mkdirs(); - try { - IO.writeAll(out, gson.toJson(obj)); - } catch (IOException e) { - e.printStackTrace(); - } - }); - - dimensionObj.add("jigsawStructures", jigsawStructures); - IO.writeAll(dimensionFile, gson.toJson(dimensionObj)); - - data.hotloaded(); - } - - @Decree(description = "Test") - public void packBenchmark( - @Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld") - IrisDimension dimension, - @Param(description = "Radius in regions", defaultValue = "2048") - int radius, - @Param(description = "Open GUI while benchmarking", defaultValue = "false") - boolean gui - ) { - new IrisPackBenchmarking(dimension, radius, gui); - } - - @Decree(description = "Upgrade to another Minecraft version") - public void upgrade( - @Param(description = "The version to upgrade to", defaultValue = "latest") DataVersion version) { - sender().sendMessage(C.GREEN + "Upgrading to " + version.getVersion() + "..."); - ServerConfigurator.installDataPacks(version.get(), false); - sender().sendMessage(C.GREEN + "Done upgrading! You can now update your server version to " + version.getVersion()); - } - - @Decree(description = "test") - public void mca ( - @Param(description = "String") String world) { - try { - File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca")); - for (File mca : McaFiles) { - MCAFile MCARegion = MCAUtil.read(mca); - } - } catch (Exception e) { - e.printStackTrace(); - } - - } - - @Decree(description = "UnloadChunks for good reasons.") - public void unloadchunks() { - List IrisWorlds = new ArrayList<>(); - int chunksUnloaded = 0; - for (World world : Bukkit.getWorlds()) { - try { - if (IrisToolbelt.access(world).getEngine() != null) { - IrisWorlds.add(world); - } - } catch (Exception e) { - // no - } - } - - for (World world : IrisWorlds) { - for (Chunk chunk : world.getLoadedChunks()) { - if (chunk.isLoaded()) { - chunk.unload(); - chunksUnloaded++; - } - } - } - Iris.info(C.IRIS + "Chunks Unloaded: " + chunksUnloaded); - - } - - @Decree - public void objects(@Param(defaultValue = "overworld") IrisDimension dimension) { - var loader = dimension.getLoader().getObjectLoader(); - var sender = sender(); - var keys = loader.getPossibleKeys(); - var burst = MultiBurst.burst.burst(keys.length); - AtomicInteger failed = new AtomicInteger(); - for (String key : keys) { - burst.queue(() -> { - if (loader.load(key) == null) - failed.incrementAndGet(); - }); - } - burst.complete(); - sender.sendMessage(C.RED + "Failed to load " + failed.get() + " of " + keys.length + " objects"); - } - - @Decree(description = "Test", aliases = {"ip"}) - public void network() { - try { - Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); - for (NetworkInterface ni : Collections.list(networkInterfaces)) { - Iris.info("Display Name: %s", ni.getDisplayName()); - Enumeration inetAddresses = ni.getInetAddresses(); - for (InetAddress ia : Collections.list(inetAddresses)) { - Iris.info("IP: %s", ia.getHostAddress()); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Decree(description = "Test the compression algorithms") - public void compression( - @Param(description = "base IrisWorld") World world, - @Param(description = "raw TectonicPlate File") String path, - @Param(description = "Algorithm to Test") String algorithm, - @Param(description = "Amount of Tests") int amount, - @Param(description = "Is versioned", defaultValue = "false") boolean versioned) { - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", Bukkit.getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList())); - return; - } - - File file = new File(path); - if (!file.exists()) return; - - Engine engine = IrisToolbelt.access(world).getEngine(); - if(engine != null) { - int height = engine.getTarget().getHeight(); - ExecutorService service = Executors.newFixedThreadPool(1); - VolmitSender sender = sender(); - service.submit(() -> { - try { - CountingDataInputStream raw = CountingDataInputStream.wrap(new FileInputStream(file)); - TectonicPlate plate = new TectonicPlate(height, raw, versioned); - raw.close(); - - double d1 = 0; - double d2 = 0; - long size = 0; - File folder = new File("tmp"); - folder.mkdirs(); - for (int i = 0; i < amount; i++) { - File tmp = new File(folder, RandomStringUtils.randomAlphanumeric(10) + "." + algorithm + ".bin"); - DataOutputStream dos = createOutput(tmp, algorithm); - long start = System.currentTimeMillis(); - plate.write(dos); - dos.close(); - d1 += System.currentTimeMillis() - start; - if (size == 0) - size = tmp.length(); - start = System.currentTimeMillis(); - CountingDataInputStream din = createInput(tmp, algorithm); - new TectonicPlate(height, din, true); - din.close(); - d2 += System.currentTimeMillis() - start; - tmp.delete(); - } - IO.delete(folder); - sender.sendMessage(algorithm + " is " + Form.fileSize(size) + " big after compression"); - sender.sendMessage(algorithm + " Took " + d2/amount + "ms to read"); - sender.sendMessage(algorithm + " Took " + d1/amount + "ms to write"); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - service.shutdown(); - } else { - Iris.info(C.RED + "Engine is null!"); - } - } - - private CountingDataInputStream createInput(File file, String algorithm) throws Throwable { - FileInputStream in = new FileInputStream(file); - - return CountingDataInputStream.wrap(switch (algorithm) { - case "gzip" -> new GZIPInputStream(in); - case "lz4f" -> new LZ4FrameInputStream(in); - case "lz4b" -> new LZ4BlockInputStream(in); - default -> throw new IllegalStateException("Unexpected value: " + algorithm); - }); - } - - private DataOutputStream createOutput(File file, String algorithm) throws Throwable { - FileOutputStream out = new FileOutputStream(file); - - return new DataOutputStream(switch (algorithm) { - case "gzip" -> new GZIPOutputStream(out); - case "lz4f" -> new LZ4FrameOutputStream(out); - case "lz4b" -> new LZ4BlockOutputStream(out); - default -> throw new IllegalStateException("Unexpected value: " + algorithm); - }); - } -} - - diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandEdit.java b/core/src/main/java/com/volmit/iris/core/commands/CommandEdit.java deleted file mode 100644 index dc00adf7d..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandEdit.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.DecreeOrigin; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.format.C; - -import java.awt.*; - - -@Decree(name = "edit", origin = DecreeOrigin.PLAYER, studio = true, description = "Edit something") -public class CommandEdit implements DecreeExecutor { - - private boolean noStudio() { - if (!sender().isPlayer()) { - sender().sendMessage(C.RED + "Players only!"); - return true; - } - if (!Iris.service(StudioSVC.class).isProjectOpen()) { - sender().sendMessage(C.RED + "No studio world is open!"); - return true; - } - if (!engine().isStudio()) { - sender().sendMessage(C.RED + "You must be in a studio world!"); - return true; - } - - if (GraphicsEnvironment.isHeadless()) { - sender().sendMessage(C.RED + "Cannot open files in headless environments!"); - return true; - } - - if (!Desktop.isDesktopSupported()) { - sender().sendMessage(C.RED + "Desktop is not supported by this environment!"); - return true; - } - return false; - } - - - @Decree(description = "Edit the biome you specified", aliases = {"b"}, origin = DecreeOrigin.PLAYER) - public void biome(@Param(contextual = false, description = "The biome to edit") IrisBiome biome) { - if (noStudio()) { - return; - } - try { - if (biome == null || biome.getLoadFile() == null) { - sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?"); - return; - } - Desktop.getDesktop().open(biome.getLoadFile()); - sender().sendMessage(C.GREEN + "Opening " + biome.getTypeName() + " " + biome.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! "); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist"); - } - } - - @Decree(description = "Edit the region you specified", aliases = {"r"}, origin = DecreeOrigin.PLAYER) - public void region(@Param(contextual = false, description = "The region to edit") IrisRegion region) { - if (noStudio()) { - return; - } - try { - if (region == null || region.getLoadFile() == null) { - sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?"); - return; - } - Desktop.getDesktop().open(region.getLoadFile()); - sender().sendMessage(C.GREEN + "Opening " + region.getTypeName() + " " + region.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! "); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist"); - } - } - - @Decree(description = "Edit the dimension you specified", aliases = {"d"}, origin = DecreeOrigin.PLAYER) - public void dimension(@Param(contextual = false, description = "The dimension to edit") IrisDimension dimension) { - if (noStudio()) { - return; - } - try { - if (dimension == null || dimension.getLoadFile() == null) { - sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?"); - return; - } - Desktop.getDesktop().open(dimension.getLoadFile()); - sender().sendMessage(C.GREEN + "Opening " + dimension.getTypeName() + " " + dimension.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! "); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist"); - } - } - - @Decree(description = "Edit the cave file you specified", aliases = {"c"}, origin = DecreeOrigin.PLAYER) - public void cave(@Param(contextual = false, description = "The cave to edit") IrisCave cave) { - if (noStudio()) { - return; - } - try { - if (cave == null || cave.getLoadFile() == null) { - sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?"); - return; - } - Desktop.getDesktop().open(cave.getLoadFile()); - sender().sendMessage(C.GREEN + "Opening " + cave.getTypeName() + " " + cave.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! "); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist"); - } - } - - @Decree(description = "Edit the structure file you specified", aliases = {"jigsawstructure", "structure"}, origin = DecreeOrigin.PLAYER) - public void jigsaw(@Param(contextual = false, description = "The jigsaw structure to edit") IrisJigsawStructure jigsaw) { - if (noStudio()) { - return; - } - try { - if (jigsaw == null || jigsaw.getLoadFile() == null) { - sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?"); - return; - } - Desktop.getDesktop().open(jigsaw.getLoadFile()); - sender().sendMessage(C.GREEN + "Opening " + jigsaw.getTypeName() + " " + jigsaw.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! "); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist"); - } - } - - @Decree(description = "Edit the pool file you specified", aliases = {"jigsawpool", "pool"}, origin = DecreeOrigin.PLAYER) - public void jigsawPool(@Param(contextual = false, description = "The jigsaw pool to edit") IrisJigsawPool pool) { - if (noStudio()) { - return; - } - try { - if (pool == null || pool.getLoadFile() == null) { - sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?"); - return; - } - Desktop.getDesktop().open(pool.getLoadFile()); - sender().sendMessage(C.GREEN + "Opening " + pool.getTypeName() + " " + pool.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! "); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist"); - } - } - - @Decree(description = "Edit the jigsaw piece file you specified", aliases = {"jigsawpiece", "piece"}, origin = DecreeOrigin.PLAYER) - public void jigsawPiece(@Param(contextual = false, description = "The jigsaw piece to edit") IrisJigsawPiece piece) { - if (noStudio()) { - return; - } - try { - if (piece == null || piece.getLoadFile() == null) { - sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?"); - return; - } - Desktop.getDesktop().open(piece.getLoadFile()); - sender().sendMessage(C.GREEN + "Opening " + piece.getTypeName() + " " + piece.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! "); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist"); - } - } - -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandFind.java b/core/src/main/java/com/volmit/iris/core/commands/CommandFind.java deleted file mode 100644 index d0bd97c68..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandFind.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisJigsawStructure; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.DecreeOrigin; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.decree.specialhandlers.ObjectHandler; -import com.volmit.iris.util.format.C; - -@Decree(name = "find", origin = DecreeOrigin.PLAYER, description = "Iris Find commands", aliases = "goto") -public class CommandFind implements DecreeExecutor { - @Decree(description = "Find a biome") - public void biome( - @Param(description = "The biome to look for") - IrisBiome biome, - @Param(description = "Should you be teleported", defaultValue = "true") - boolean teleport - ) { - Engine e = engine(); - - if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); - return; - } - - e.gotoBiome(biome, player(), teleport); - } - - @Decree(description = "Find a region") - public void region( - @Param(description = "The region to look for") - IrisRegion region, - @Param(description = "Should you be teleported", defaultValue = "true") - boolean teleport - ) { - Engine e = engine(); - - if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); - return; - } - - e.gotoRegion(region, player(), teleport); - } - - @Decree(description = "Find a structure") - public void structure( - @Param(description = "The structure to look for") - IrisJigsawStructure structure, - @Param(description = "Should you be teleported", defaultValue = "true") - boolean teleport - ) { - Engine e = engine(); - - if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); - return; - } - - e.gotoJigsaw(structure, player(), teleport); - } - - @Decree(description = "Find a point of interest.") - public void poi( - @Param(description = "The type of PoI to look for.") - String type, - @Param(description = "Should you be teleported", defaultValue = "true") - boolean teleport - ) { - Engine e = engine(); - if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); - return; - } - - e.gotoPOI(type, player(), teleport); - } - - @Decree(description = "Find an object") - public void object( - @Param(description = "The object to look for", customHandler = ObjectHandler.class) - String object, - @Param(description = "Should you be teleported", defaultValue = "true") - boolean teleport - ) { - Engine e = engine(); - - if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); - return; - } - - e.gotoObject(object, player(), teleport); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandIris.java b/core/src/main/java/com/volmit/iris/core/commands/CommandIris.java deleted file mode 100644 index 75448c8dc..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandIris.java +++ /dev/null @@ -1,540 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.DecreeOrigin; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.decree.specialhandlers.NullablePlayerHandler; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.misc.ServerProperties; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import lombok.SneakyThrows; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; - -import java.io.*; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - -import static com.volmit.iris.core.service.EditSVC.deletingWorld; -import static com.volmit.iris.util.misc.ServerProperties.BUKKIT_YML; -import static org.bukkit.Bukkit.getServer; - -@Decree(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command") -public class CommandIris implements DecreeExecutor { - private CommandUpdater updater; - private CommandStudio studio; - private CommandPregen pregen; - private CommandSettings settings; - private CommandObject object; - private CommandJigsaw jigsaw; - private CommandWhat what; - private CommandEdit edit; - private CommandFind find; - private CommandDeveloper developer; - public static boolean worldCreation = false; - private static final AtomicReference mainWorld = new AtomicReference<>(); - String WorldEngine; - String worldNameToCheck = "YourWorldName"; - VolmitSender sender = Iris.getSender(); - - @Decree(description = "Create a new world", aliases = {"+", "c"}) - public void create( - @Param(aliases = "world-name", description = "The name of the world to create") - String name, - @Param(aliases = "dimension", description = "The dimension type to create the world with", defaultValue = "default") - IrisDimension type, - @Param(description = "The seed to generate the world with", defaultValue = "1337") - long seed, - @Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false") - boolean main - ) { - if (name.equalsIgnoreCase("iris")) { - sender().sendMessage(C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds."); - sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?"); - return; - } - - if (name.equalsIgnoreCase("benchmark")) { - sender().sendMessage(C.RED + "You cannot use the world name \"benchmark\" for creating worlds as Iris uses this directory for Benchmarking Packs."); - sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?"); - return; - } - - if (new File(Bukkit.getWorldContainer(), name).exists()) { - sender().sendMessage(C.RED + "That folder already exists!"); - return; - } - - try { - worldCreation = true; - IrisToolbelt.createWorld() - .dimension(type.getLoadKey()) - .name(name) - .seed(seed) - .sender(sender()) - .studio(false) - .create(); - if (main) { - Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> { - if (old != null) Runtime.getRuntime().removeShutdownHook(old); - return new Thread(() -> updateMainWorld(name)); - })); - } - } catch (Throwable e) { - sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details."); - Iris.error("Exception raised during world creation: " + e.getMessage()); - Iris.reportError(e); - worldCreation = false; - return; - } - worldCreation = false; - sender().sendMessage(C.GREEN + "Successfully created your world!"); - if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts."); - } - - @SneakyThrows - private void updateMainWorld(String newName) { - File worlds = Bukkit.getWorldContainer(); - var data = ServerProperties.DATA; - try (var in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) { - data.load(in); - } - for (String sub : List.of("datapacks", "playerdata", "advancements", "stats")) { - IO.copyDirectory(new File(worlds, ServerProperties.LEVEL_NAME + "/" + sub).toPath(), new File(worlds, newName + "/" + sub).toPath()); - } - - data.setProperty("level-name", newName); - try (var out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) { - data.store(out, null); - } - } - - @Decree(description = "Teleport to another world", aliases = {"tp"}, sync = true) - public void teleport( - @Param(description = "World to teleport to") - World world, - @Param(description = "Player to teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class) - Player player - ) { - if (player == null && sender().isPlayer()) - player = sender().player(); - - final Player target = player; - if (target == null) { - sender().sendMessage(C.RED + "The specified player does not exist."); - return; - } - - new BukkitRunnable() { - @Override - public void run() { - target.teleport(world.getSpawnLocation()); - new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + "."); - } - }.runTask(Iris.instance); - } - - @Decree(description = "Print version information") - public void version() { - sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software"); - } - - /* - /todo - @Decree(description = "Benchmark a pack", origin = DecreeOrigin.CONSOLE) - public void packbenchmark( - @Param(description = "Dimension to benchmark") - IrisDimension type - ) throws InterruptedException { - - BenchDimension = type.getLoadKey(); - - IrisPackBenchmarking.runBenchmark(); - } */ - - @Decree(description = "Print world height information", origin = DecreeOrigin.PLAYER) - public void height() { - if (sender().isPlayer()) { - sender().sendMessage(C.GREEN + "" + sender().player().getWorld().getMinHeight() + " to " + sender().player().getWorld().getMaxHeight()); - sender().sendMessage(C.GREEN + "Total Height: " + (sender().player().getWorld().getMaxHeight() - sender().player().getWorld().getMinHeight())); - } else { - World mainWorld = getServer().getWorlds().get(0); - Iris.info(C.GREEN + "" + mainWorld.getMinHeight() + " to " + mainWorld.getMaxHeight()); - Iris.info(C.GREEN + "Total Height: " + (mainWorld.getMaxHeight() - mainWorld.getMinHeight())); - } - } - - @Decree(description = "QOL command to open a overworld studio world.", sync = true) - public void so() { - sender().sendMessage(C.GREEN + "Opening studio for the \"Overworld\" pack (seed: 1337)"); - Iris.service(StudioSVC.class).open(sender(), 1337, "overworld"); - } - - @Decree(description = "Check access of all worlds.", aliases = {"accesslist"}) - public void worlds() { - KList IrisWorlds = new KList<>(); - KList BukkitWorlds = new KList<>(); - - for (World w : Bukkit.getServer().getWorlds()) { - try { - Engine engine = IrisToolbelt.access(w).getEngine(); - if (engine != null) { - IrisWorlds.add(w); - } - } catch (Exception e) { - BukkitWorlds.add(w); - } - } - - if (sender().isPlayer()) { - sender().sendMessage(C.BLUE + "Iris Worlds: "); - for (World IrisWorld : IrisWorlds.copy()) { - sender().sendMessage(C.IRIS + "- " +IrisWorld.getName()); - } - sender().sendMessage(C.GOLD + "Bukkit Worlds: "); - for (World BukkitWorld : BukkitWorlds.copy()) { - sender().sendMessage(C.GRAY + "- " +BukkitWorld.getName()); - } - } else { - Iris.info(C.BLUE + "Iris Worlds: "); - for (World IrisWorld : IrisWorlds.copy()) { - Iris.info(C.IRIS + "- " +IrisWorld.getName()); - } - Iris.info(C.GOLD + "Bukkit Worlds: "); - for (World BukkitWorld : BukkitWorlds.copy()) { - Iris.info(C.GRAY + "- " +BukkitWorld.getName()); - } - - } - } - - @Decree(description = "Remove an Iris world", aliases = {"del", "rm", "delete"}, sync = true) - public void remove( - @Param(description = "The world to remove") - World world, - @Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", defaultValue = "true") - boolean delete - ) { - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList())); - return; - } - sender().sendMessage(C.GREEN + "Removing world: " + world.getName()); - - if (!IrisToolbelt.evacuate(world)) { - sender().sendMessage(C.RED + "Failed to evacuate world: " + world.getName()); - return; - } - - if (!Bukkit.unloadWorld(world, false)) { - sender().sendMessage(C.RED + "Failed to unload world: " + world.getName()); - return; - } - - try { - if (IrisToolbelt.removeWorld(world)) { - sender().sendMessage(C.GREEN + "Successfully removed " + world.getName() + " from bukkit.yml"); - } else { - sender().sendMessage(C.YELLOW + "Looks like the world was already removed from bukkit.yml"); - } - } catch (IOException e) { - sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage()); - e.printStackTrace(); - } - IrisToolbelt.evacuate(world, "Deleting world"); - deletingWorld = true; - if (!delete) { - deletingWorld = false; - return; - } - VolmitSender sender = sender(); - J.a(() -> { - int retries = 12; - - if (deleteDirectory(world.getWorldFolder())) { - sender.sendMessage(C.GREEN + "Successfully removed world folder"); - } else { - while(true){ - if (deleteDirectory(world.getWorldFolder())){ - sender.sendMessage(C.GREEN + "Successfully removed world folder"); - break; - } - retries--; - if (retries == 0){ - sender.sendMessage(C.RED + "Failed to remove world folder"); - break; - } - J.sleep(3000); - } - } - deletingWorld = false; - }); - } - - public static boolean deleteDirectory(File dir) { - if (dir.isDirectory()) { - File[] children = dir.listFiles(); - for (int i = 0; i < children.length; i++) { - boolean success = deleteDirectory(children[i]); - if (!success) { - return false; - } - } - } - return dir.delete(); - } - - @Decree(description = "Set aura spins") - public void aura( - @Param(description = "The h color value", defaultValue = "-20") - int h, - @Param(description = "The s color value", defaultValue = "7") - int s, - @Param(description = "The b color value", defaultValue = "8") - int b - ) { - IrisSettings.get().getGeneral().setSpinh(h); - IrisSettings.get().getGeneral().setSpins(s); - IrisSettings.get().getGeneral().setSpinb(b); - IrisSettings.get().forceSave(); - sender().sendMessage("Aura Spins updated to " + h + " " + s + " " + b); - } - - @Decree(description = "Bitwise calculations") - public void bitwise( - @Param(description = "The first value to run calculations on") - int value1, - @Param(description = "The operator: | & ^ ≺≺ ≻≻ %") - String operator, - @Param(description = "The second value to run calculations on") - int value2 - ) { - Integer v = null; - switch (operator) { - case "|" -> v = value1 | value2; - case "&" -> v = value1 & value2; - case "^" -> v = value1 ^ value2; - case "%" -> v = value1 % value2; - case ">>" -> v = value1 >> value2; - case "<<" -> v = value1 << value2; - } - if (v == null) { - sender().sendMessage(C.RED + "The operator you entered: (" + operator + ") is invalid!"); - return; - } - sender().sendMessage(C.GREEN + "" + value1 + " " + C.GREEN + operator.replaceAll("<", "≺").replaceAll(">", "≻").replaceAll("%", "%") + " " + C.GREEN + value2 + C.GREEN + " returns " + C.GREEN + v); - } - - @Decree(description = "Toggle debug") - public void debug( - @Param(name = "on", description = "Whether or not debug should be on", defaultValue = "other") - Boolean on - ) { - boolean to = on == null ? !IrisSettings.get().getGeneral().isDebug() : on; - IrisSettings.get().getGeneral().setDebug(to); - IrisSettings.get().forceSave(); - sender().sendMessage(C.GREEN + "Set debug to: " + to); - } - - //TODO fix pack trimming - @Decree(description = "Download a project.", aliases = "dl") - public void download( - @Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project") - String pack, - @Param(name = "branch", description = "The branch to download from", defaultValue = "main") - String branch, - //@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false") - //boolean trim, - @Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false") - boolean overwrite - ) { - boolean trim = false; - sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (trim ? " trimmed" : "") + (overwrite ? " overwriting" : "")); - if (pack.equals("overworld")) { - String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip"; - Iris.service(StudioSVC.class).downloadRelease(sender(), url, trim, overwrite); - } else { - Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, trim, overwrite); - } - } - - @Decree(description = "Get metrics for your world", aliases = "measure", origin = DecreeOrigin.PLAYER) - public void metrics() { - if (!IrisToolbelt.isIrisWorld(world())) { - sender().sendMessage(C.RED + "You must be in an Iris world"); - return; - } - sender().sendMessage(C.GREEN + "Sending metrics..."); - engine().printMetrics(sender()); - } - - @Decree(description = "Reload configuration file (this is also done automatically)") - public void reload() { - IrisSettings.invalidate(); - IrisSettings.get(); - sender().sendMessage(C.GREEN + "Hotloaded settings"); - } - - @Decree(description = "Update the pack of a world (UNSAFE!)", name = "^world", aliases = "update-world") - public void updateWorld( - @Param(description = "The world to update", contextual = true) - World world, - @Param(description = "The pack to install into the world", contextual = true, aliases = "dimension") - IrisDimension pack, - @Param(description = "Make sure to make a backup & read the warnings first!", defaultValue = "false", aliases = "c") - boolean confirm, - @Param(description = "Should Iris download the pack again for you", defaultValue = "false", name = "fresh-download", aliases = {"fresh", "new"}) - boolean freshDownload - ) { - if (!confirm) { - sender().sendMessage(new String[]{ - C.RED + "You should always make a backup before using this", - C.YELLOW + "Issues caused by this can be, but are not limited to:", - C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)", - C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks", - C.YELLOW + " - Structures not spawning again when regenerating", - C.YELLOW + " - Caves not lining up", - C.YELLOW + " - Terrain layers not lining up", - C.RED + "Now that you are aware of the risks, and have made a back-up:", - C.RED + "/iris ^world " + world.getName() + " " + pack.getLoadKey() + " confirm=true" - }); - return; - } - - File folder = world.getWorldFolder(); - folder.mkdirs(); - - if (freshDownload) { - Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), false, true); - } - - Iris.service(StudioSVC.class).installIntoWorld(sender(), pack.getLoadKey(), folder); - } - - @Decree(description = "Unload an Iris World", origin = DecreeOrigin.PLAYER, sync = true) - public void unloadWorld( - @Param(description = "The world to unload") - World world - ) { - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList())); - return; - } - sender().sendMessage(C.GREEN + "Unloading world: " + world.getName()); - try { - IrisToolbelt.evacuate(world); - Bukkit.unloadWorld(world, false); - sender().sendMessage(C.GREEN + "World unloaded successfully."); - } catch (Exception e) { - sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage()); - e.printStackTrace(); - } - } - - @Decree(description = "Load an Iris World", origin = DecreeOrigin.PLAYER, sync = true, aliases = {"import"}) - public void loadWorld( - @Param(description = "The name of the world to load") - String world - ) { - World worldloaded = Bukkit.getWorld(world); - worldNameToCheck = world; - boolean worldExists = doesWorldExist(worldNameToCheck); - WorldEngine = world; - - if (!worldExists) { - sender().sendMessage(C.YELLOW + world + " Doesnt exist on the server."); - return; - } - - String pathtodim = world + File.separator +"iris"+File.separator +"pack"+File.separator +"dimensions"+File.separator; - File directory = new File(Bukkit.getWorldContainer(), pathtodim); - - String dimension = null; - if (directory.exists() && directory.isDirectory()) { - File[] files = directory.listFiles(); - if (files != null) { - for (File file : files) { - if (file.isFile()) { - String fileName = file.getName(); - if (fileName.endsWith(".json")) { - dimension = fileName.substring(0, fileName.length() - 5); - sender().sendMessage(C.BLUE + "Generator: " + dimension); - } - } - } - } - } else { - sender().sendMessage(C.GOLD + world + " is not an iris world."); - return; - } - sender().sendMessage(C.GREEN + "Loading world: " + world); - - YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); - String gen = "Iris:" + dimension; - ConfigurationSection section = yml.contains("worlds") ? yml.getConfigurationSection("worlds") : yml.createSection("worlds"); - if (!section.contains(world)) { - section.createSection(world).set("generator", gen); - try { - yml.save(BUKKIT_YML); - Iris.info("Registered \"" + world + "\" in bukkit.yml"); - } catch (IOException e) { - Iris.error("Failed to update bukkit.yml!"); - e.printStackTrace(); - return; - } - } - Iris.instance.checkForBukkitWorlds(world::equals); - sender().sendMessage(C.GREEN + world + " loaded successfully."); - } - @Decree(description = "Evacuate an iris world", origin = DecreeOrigin.PLAYER, sync = true) - public void evacuate( - @Param(description = "Evacuate the world") - World world - ) { - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList())); - return; - } - sender().sendMessage(C.GREEN + "Evacuating world" + world.getName()); - IrisToolbelt.evacuate(world); - } - - boolean doesWorldExist(String worldName) { - File worldContainer = Bukkit.getWorldContainer(); - File worldDirectory = new File(worldContainer, worldName); - return worldDirectory.exists() && worldDirectory.isDirectory(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandJigsaw.java b/core/src/main/java/com/volmit/iris/core/commands/CommandJigsaw.java deleted file mode 100644 index 7777f1764..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandJigsaw.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.edit.JigsawEditor; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.placer.WorldObjectPlacer; -import com.volmit.iris.engine.jigsaw.PlannedStructure; -import com.volmit.iris.engine.object.IrisJigsawPiece; -import com.volmit.iris.engine.object.IrisJigsawStructure; -import com.volmit.iris.engine.object.IrisObject; -import com.volmit.iris.engine.object.IrisPosition; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.DecreeOrigin; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.decree.specialhandlers.ObjectHandler; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; - -import java.io.File; - -@Decree(name = "jigsaw", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris jigsaw commands") -public class CommandJigsaw implements DecreeExecutor { - @Decree(description = "Edit a jigsaw piece") - public void edit( - @Param(description = "The jigsaw piece to edit") - IrisJigsawPiece piece - ) { - File dest = piece.getLoadFile(); - new JigsawEditor(player(), piece, IrisData.loadAnyObject(piece.getObject(), data()), dest); - } - - @Decree(description = "Place a jigsaw structure") - public void place( - @Param(description = "The jigsaw structure to place") - IrisJigsawStructure structure - ) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - try { - var world = world(); - WorldObjectPlacer placer = new WorldObjectPlacer(world); - PlannedStructure ps = new PlannedStructure(structure, new IrisPosition(player().getLocation().add(0, world.getMinHeight(), 0)), new RNG(), true); - VolmitSender sender = sender(); - sender.sendMessage(C.GREEN + "Generated " + ps.getPieces().size() + " pieces in " + Form.duration(p.getMilliseconds(), 2)); - ps.place(placer, failed -> sender.sendMessage(failed ? C.GREEN + "Placed the structure!" : C.RED + "Failed to place the structure!")); - } catch (IllegalArgumentException e) { - sender().sendMessage(C.RED + "Failed to place the structure: " + e.getMessage()); - } - } - - @Decree(description = "Create a jigsaw piece") - public void create( - @Param(description = "The name of the jigsaw piece") - String piece, - @Param(description = "The project to add the jigsaw piece to") - String project, - @Param(description = "The object to use for this piece", customHandler = ObjectHandler.class) - String object - ) { - IrisObject o = IrisData.loadAnyObject(object, data()); - - if (object == null) { - sender().sendMessage(C.RED + "Failed to find existing object"); - return; - } - - File dest = Iris.instance.getDataFile("packs", project, "jigsaw-pieces", piece + ".json"); - new JigsawEditor(player(), null, o, dest); - sender().sendMessage(C.GRAY + "* Right Click blocks to make them connectors"); - sender().sendMessage(C.GRAY + "* Right Click connectors to orient them"); - sender().sendMessage(C.GRAY + "* Shift + Right Click connectors to remove them"); - sender().sendMessage(C.GREEN + "Remember to use /iris jigsaw save"); - } - - @Decree(description = "Exit the current jigsaw editor") - public void exit() { - JigsawEditor editor = JigsawEditor.editors.get(player()); - - if (editor == null) { - sender().sendMessage(C.GOLD + "You don't have any pieces open to exit!"); - return; - } - - editor.exit(); - sender().sendMessage(C.GREEN + "Exited Jigsaw Editor"); - } - - @Decree(description = "Save & Exit the current jigsaw editor") - public void save() { - JigsawEditor editor = JigsawEditor.editors.get(player()); - - if (editor == null) { - sender().sendMessage(C.GOLD + "You don't have any pieces open to save!"); - return; - } - - editor.close(); - sender().sendMessage(C.GREEN + "Saved & Exited Jigsaw Editor"); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandLazyPregen.java b/core/src/main/java/com/volmit/iris/core/commands/CommandLazyPregen.java deleted file mode 100644 index a3d4c622c..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandLazyPregen.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.gui.PregeneratorJob; -import com.volmit.iris.core.pregenerator.LazyPregenerator; -import com.volmit.iris.core.pregenerator.PregenTask; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.math.Position2; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.util.Vector; - -import java.io.File; -import java.io.IOException; - -@Decree(name = "lazypregen", aliases = "lazy", description = "Pregenerate your Iris worlds!") -public class CommandLazyPregen implements DecreeExecutor { - public String worldName; - @Decree(description = "Pregenerate a world") - public void start( - @Param(description = "The radius of the pregen in blocks", aliases = "size") - int radius, - @Param(description = "The world to pregen", contextual = true) - World world, - @Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0") - Vector center, - @Param(aliases = "maxcpm", description = "Limit the chunks per minute the pregen will generate", defaultValue = "999999999") - int cpm, - @Param(aliases = "silent", description = "Silent generation", defaultValue = "false") - boolean silent - ) { - - worldName = world.getName(); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File lazyFile = new File(worldDirectory, "lazygen.json"); - if (lazyFile.exists()) { - sender().sendMessage(C.BLUE + "Lazy pregen is already in progress"); - Iris.info(C.YELLOW + "Lazy pregen is already in progress"); - return; - } - - try { - if (sender().isPlayer() && access() == null) { - sender().sendMessage(C.RED + "The engine access for this world is null!"); - sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example."); - } - - LazyPregenerator.LazyPregenJob pregenJob = LazyPregenerator.LazyPregenJob.builder() - .world(worldName) - .healingPosition(0) - .healing(false) - .chunksPerMinute(cpm) - .radiusBlocks(radius) - .position(0) - .silent(silent) - .build(); - - File lazyGenFile = new File(worldDirectory, "lazygen.json"); - LazyPregenerator pregenerator = new LazyPregenerator(pregenJob, lazyGenFile); - pregenerator.start(); - - String msg = C.GREEN + "LazyPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ(); - sender().sendMessage(msg); - Iris.info(msg); - } catch (Throwable e) { - sender().sendMessage(C.RED + "Epic fail. See console."); - Iris.reportError(e); - e.printStackTrace(); - } - } - - @Decree(description = "Stop the active pregeneration task", aliases = "x") - public void stop( - @Param(aliases = "world", description = "The world to pause") - World world - ) throws IOException { - if (LazyPregenerator.getInstance() != null) { - LazyPregenerator.getInstance().shutdownInstance(world); - sender().sendMessage(C.LIGHT_PURPLE + "Closed lazygen instance for " + world.getName()); - } else { - sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop"); - } - } - - @Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) - public void pause( - @Param(aliases = "world", description = "The world to pause") - World world - ) { - if (LazyPregenerator.getInstance() != null) { - LazyPregenerator.getInstance().setPausedLazy(world); - sender().sendMessage(C.GREEN + "Paused/unpaused Lazy Pregen, now: " + (LazyPregenerator.getInstance().isPausedLazy(world) ? "Paused" : "Running") + "."); - } else { - sender().sendMessage(C.YELLOW + "No active Lazy Pregen tasks to pause/unpause."); - - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandObject.java b/core/src/main/java/com/volmit/iris/core/commands/CommandObject.java deleted file mode 100644 index 9786c3ec4..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandObject.java +++ /dev/null @@ -1,577 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.WorldEditLink; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.service.ObjectSVC; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.core.service.WandSVC; -import com.volmit.iris.core.tools.IrisConverter; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.data.Cuboid; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.data.registry.Materials; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.DecreeOrigin; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.decree.specialhandlers.ObjectHandler; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.math.Direction; -import com.volmit.iris.util.math.RNG; -import org.bukkit.*; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.Vector; - -import java.io.File; -import java.io.IOException; -import java.text.NumberFormat; -import java.util.*; - -@Decree(name = "object", aliases = "o", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris object manipulation") -public class CommandObject implements DecreeExecutor { - - private static final Set skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH, - Material.POPPY, Material.DANDELION); - - public static IObjectPlacer createPlacer(World world, Map futureBlockChanges) { - - return new IObjectPlacer() { - @Override - public int getHighest(int x, int z, IrisData data) { - return world.getHighestBlockYAt(x, z); - } - - @Override - public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { - return world.getHighestBlockYAt(x, z, ignoreFluid ? HeightMap.OCEAN_FLOOR : HeightMap.MOTION_BLOCKING); - } - - @Override - public void set(int x, int y, int z, BlockData d) { - Block block = world.getBlockAt(x, y, z); - - //Prevent blocks being set in or bellow bedrock - if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return; - - futureBlockChanges.put(block, block.getBlockData()); - - if (d instanceof IrisCustomData data) { - block.setBlockData(data.getBase(), false); - Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!"); - } else block.setBlockData(d, false); - } - - @Override - public BlockData get(int x, int y, int z) { - return world.getBlockAt(x, y, z).getBlockData(); - } - - @Override - public boolean isPreventingDecay() { - return false; - } - - @Override - public boolean isCarved(int x, int y, int z) { - return false; - } - - @Override - public boolean isSolid(int x, int y, int z) { - return world.getBlockAt(x, y, z).getType().isSolid(); - } - - @Override - public boolean isUnderwater(int x, int z) { - return false; - } - - @Override - public int getFluidHeight() { - return 63; - } - - @Override - public boolean isDebugSmartBore() { - return false; - } - - @Override - public void setTile(int xx, int yy, int zz, TileData tile) { - tile.toBukkitTry(world.getBlockAt(xx, yy, zz)); - } - - @Override - public void setData(int xx, int yy, int zz, T data) { - - } - - @Override - public T getData(int xx, int yy, int zz, Class t) { - return null; - } - - @Override - public Engine getEngine() { - return null; - } - }; - } - - @Decree(description = "Check the composition of an object") - public void analyze( - @Param(description = "The object to analyze", customHandler = ObjectHandler.class) - String object - ) { - IrisObject o = IrisData.loadAnyObject(object, data()); - sender().sendMessage("Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD() + ""); - sender().sendMessage("Blocks Used: " + NumberFormat.getIntegerInstance().format(o.getBlocks().size())); - - var queue = o.getBlocks().values(); - Map> unsorted = new HashMap<>(); - Map amounts = new HashMap<>(); - Map materials = new HashMap<>(); - while (queue.hasNext()) { - BlockData block = queue.next(); - - //unsorted.put(block.getMaterial(), block); - - if (!amounts.containsKey(block)) { - amounts.put(block, 1); - - - } else - amounts.put(block, amounts.get(block) + 1); - - if (!materials.containsKey(block.getMaterial())) { - materials.put(block.getMaterial(), 1); - unsorted.put(block.getMaterial(), new HashSet<>()); - unsorted.get(block.getMaterial()).add(block); - } else { - materials.put(block.getMaterial(), materials.get(block.getMaterial()) + 1); - unsorted.get(block.getMaterial()).add(block); - } - - } - - List sortedMatsList = amounts.keySet().stream().map(BlockData::getMaterial) - .sorted().toList(); - Set sortedMats = new TreeSet<>(Comparator.comparingInt(materials::get).reversed()); - sortedMats.addAll(sortedMatsList); - sender().sendMessage("== Blocks in object =="); - - int n = 0; - for (Material mat : sortedMats) { - int amount = materials.get(mat); - List set = new ArrayList<>(unsorted.get(mat)); - set.sort(Comparator.comparingInt(amounts::get).reversed()); - BlockData data = set.get(0); - int dataAmount = amounts.get(data); - - String string = " - " + mat.toString() + "*" + amount; - if (data.getAsString(true).contains("[")) { - string = string + " --> [" + data.getAsString(true).split("\\[")[1] - .replaceAll("true", ChatColor.GREEN + "true" + ChatColor.GRAY) - .replaceAll("false", ChatColor.RED + "false" + ChatColor.GRAY) + "*" + dataAmount; - } - - sender().sendMessage(string); - - n++; - - if (n >= 10) { - sender().sendMessage(" + " + (sortedMats.size() - n) + " other block types"); - return; - } - } - } - - @Decree(description = "Shrink an object to its minimum size") - public void shrink(@Param(description = "The object to shrink", customHandler = ObjectHandler.class) String object) { - IrisObject o = IrisData.loadAnyObject(object, data()); - sender().sendMessage("Current Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD()); - o.shrinkwrap(); - sender().sendMessage("New Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD()); - try { - o.write(o.getLoadFile()); - } catch (IOException e) { - sender().sendMessage("Failed to save object " + o.getLoadFile() + ": " + e.getMessage()); - e.printStackTrace(); - } - } - - @Decree(description = "Convert .schem files in the 'convert' folder to .iob files.") - public void convert () { - try { - IrisConverter.convertSchematics(sender()); - } catch (Exception e) { - e.printStackTrace(); - } - - } - - @Decree(description = "Get a powder that reveals objects", studio = true, aliases = "d") - public void dust() { - player().getInventory().addItem(WandSVC.createDust()); - sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f); - } - - @Decree(description = "Contract a selection based on your looking direction", aliases = "-") - public void contract( - @Param(description = "The amount to inset by", defaultValue = "1") - int amount - ) { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage("Hold your wand."); - return; - } - - - Location[] b = WandSVC.getCuboid(player()); - if (b == null || b[0] == null || b[1] == null) { - sender().sendMessage("No area selected."); - return; - } - Location a1 = b[0].clone(); - Location a2 = b[1].clone(); - Cuboid cursor = new Cuboid(a1, a2); - Direction d = Direction.closest(player().getLocation().getDirection()).reverse(); - assert d != null; - cursor = cursor.expand(d, -amount); - b[0] = cursor.getLowerNE(); - b[1] = cursor.getUpperSW(); - player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); - player().updateInventory(); - sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); - } - - @Decree(description = "Set point 1 to look", aliases = "p1") - public void position1( - @Param(description = "Whether to use your current position, or where you look", defaultValue = "true") - boolean here - ) { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage("Ready your Wand."); - return; - } - - if (WandSVC.isHoldingWand(player())) { - Location[] g = WandSVC.getCuboid(player()); - - if (g == null) { - return; - } - if (!here) { - // TODO: WARNING HEIGHT - g[1] = player().getTargetBlock(null, 256).getLocation().clone(); - } else { - g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0); - } - player().setItemInHand(WandSVC.createWand(g[0], g[1])); - } - } - - @Decree(description = "Set point 2 to look", aliases = "p2") - public void position2( - @Param(description = "Whether to use your current position, or where you look", defaultValue = "true") - boolean here - ) { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage("Ready your Wand."); - return; - } - - if (WandSVC.isHoldingIrisWand(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().setItemInHand(WandSVC.createWand(g[0], g[1])); - } - } - - @Decree(description = "Paste an object", sync = true) - public void paste( - @Param(description = "The object to paste", customHandler = ObjectHandler.class) - String object, - @Param(description = "Whether or not to edit the object (need to hold wand)", defaultValue = "false") - boolean edit, - @Param(description = "The amount of degrees to rotate by", defaultValue = "0") - int rotate, - @Param(description = "The factor by which to scale the object placement", defaultValue = "1") - double scale -// , -// @Param(description = "The scale interpolator to use", defaultValue = "none") -// IrisObjectPlacementScaleInterpolator interpolator - ) { - IrisObject o = IrisData.loadAnyObject(object, data()); - double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1); - if (scale > maxScale) { - sender().sendMessage(C.YELLOW + "Indicated scale exceeds maximum. Downscaled to maximum: " + maxScale); - scale = maxScale; - } - - sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f); - - IrisObjectPlacement placement = new IrisObjectPlacement(); - placement.setRotation(IrisObjectRotation.of(0, rotate, 0)); - - ItemStack wand = player().getInventory().getItemInMainHand(); - Location block = player().getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0); - - Map futureChanges = new HashMap<>(); - - if (scale != 1) { - o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC); - } - - o.place(block.getBlockX(), block.getBlockY() + (int) o.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null); - - Iris.service(ObjectSVC.class).addChanges(futureChanges); - - if (edit) { - ItemStack newWand = WandSVC.createWand(block.clone().subtract(o.getCenter()).add(o.getW() - 1, - o.getH() + o.getCenter().clone().getY() - 1, o.getD() - 1), block.clone().subtract(o.getCenter().clone().setY(0))); - if (WandSVC.isWand(wand)) { - wand = newWand; - player().getInventory().setItemInMainHand(wand); - sender().sendMessage("Updated wand for " + "objects/" + o.getLoadKey() + ".iob "); - } else { - int slot = WandSVC.findWand(player().getInventory()); - if (slot == -1) { - player().getInventory().addItem(newWand); - sender().sendMessage("Given new wand for " + "objects/" + o.getLoadKey() + ".iob "); - } else { - player().getInventory().setItem(slot, newWand); - sender().sendMessage("Updated wand for " + "objects/" + o.getLoadKey() + ".iob "); - } - } - } else { - sender().sendMessage(C.IRIS + "Placed " + object); - } - } - - @Decree(description = "Save an object") - public void save( - @Param(description = "The dimension to store the object in", contextual = true) - IrisDimension dimension, - @Param(description = "The file to store it in, can use / for subfolders") - String name, - @Param(description = "Overwrite existing object files", defaultValue = "false", aliases = "force") - boolean overwrite, - @Param(description = "Use legacy TileState serialization if possible", defaultValue = "true") - boolean legacy - ) { - IrisObject o = WandSVC.createSchematic(player(), legacy); - - if (o == null) { - sender().sendMessage(C.YELLOW + "You need to hold your wand!"); - return; - } - - File file = Iris.service(StudioSVC.class).getWorkspaceFile(dimension.getLoadKey(), "objects", name + ".iob"); - - if (file.exists() && !overwrite) { - sender().sendMessage(C.RED + "File already exists. Set overwrite=true to overwrite it."); - return; - } - try { - o.write(file, sender()); - } catch (IOException e) { - sender().sendMessage(C.RED + "Failed to save object because of an IOException: " + e.getMessage()); - Iris.reportError(e); - } - - sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f); - sender().sendMessage(C.GREEN + "Successfully object to saved: " + dimension.getLoadKey() + "/objects/" + name); - } - - @Decree(description = "Shift a selection in your looking direction", aliases = "-") - public void shift( - @Param(description = "The amount to shift by", defaultValue = "1") - int amount - ) { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage("Hold your wand."); - return; - } - - Location[] b = WandSVC.getCuboid(player()); - if (b == null || b[0] == null || b[1] == null) { - sender().sendMessage("No area selected."); - return; - } - Location a1 = b[0].clone(); - Location a2 = b[1].clone(); - Direction d = Direction.closest(player().getLocation().getDirection()).reverse(); - if (d == null) { - return; // HOW DID THIS HAPPEN - } - a1.add(d.toVector().multiply(amount)); - a2.add(d.toVector().multiply(amount)); - Cuboid cursor = new Cuboid(a1, a2); - b[0] = cursor.getLowerNE(); - b[1] = cursor.getUpperSW(); - player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); - player().updateInventory(); - sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); - } - - @Decree(description = "Undo a number of pastes", aliases = "-") - public void undo( - @Param(description = "The amount of pastes to undo", defaultValue = "1") - int amount - ) { - ObjectSVC service = Iris.service(ObjectSVC.class); - int actualReverts = Math.min(service.getUndos().size(), amount); - service.revertChanges(actualReverts); - sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!"); - } - - @Decree(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DecreeOrigin.PLAYER, studio = true) - public void we() { - if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) { - sender().sendMessage(C.RED + "You can't get a WorldEdit selection without WorldEdit, you know."); - return; - } - - Cuboid locs = WorldEditLink.getSelection(sender().player()); - - if (locs == null) { - sender().sendMessage(C.RED + "You don't have a WorldEdit selection in this world."); - return; - } - - sender().player().getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW())); - sender().sendMessage(C.GREEN + "A fresh wand with your current WorldEdit selection on it!"); - } - - @Decree(description = "Get an object wand", sync = true) - public void wand() { - player().getInventory().addItem(WandSVC.createWand()); - sender().playSound(Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f, 1.5f); - sender().sendMessage(C.GREEN + "Poof! Good luck building!"); - } - - @Decree(name = "x&y", description = "Autoselect up, down & out", sync = true) - public void xay() { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage(C.YELLOW + "Hold your wand!"); - return; - } - - Location[] b = WandSVC.getCuboid(player()); - if (b == null || b[0] == null || b[1] == null) { - sender().sendMessage("No area selected."); - return; - } - Location a1 = b[0].clone(); - Location a2 = b[1].clone(); - Location a1x = b[0].clone(); - Location a2x = b[1].clone(); - Cuboid cursor = new Cuboid(a1, a2); - Cuboid cursorx = new Cuboid(a1, a2); - - while (!cursor.containsOnly(Material.AIR)) { - a1.add(new org.bukkit.util.Vector(0, 1, 0)); - a2.add(new org.bukkit.util.Vector(0, 1, 0)); - cursor = new Cuboid(a1, a2); - } - - a1.add(new org.bukkit.util.Vector(0, -1, 0)); - a2.add(new org.bukkit.util.Vector(0, -1, 0)); - - while (!cursorx.containsOnly(Material.AIR)) { - a1x.add(new org.bukkit.util.Vector(0, -1, 0)); - a2x.add(new org.bukkit.util.Vector(0, -1, 0)); - cursorx = new Cuboid(a1x, a2x); - } - - a1x.add(new org.bukkit.util.Vector(0, 1, 0)); - a2x.add(new Vector(0, 1, 0)); - b[0] = a1; - b[1] = a2x; - cursor = new Cuboid(b[0], b[1]); - cursor = cursor.contract(Cuboid.CuboidDirection.North); - cursor = cursor.contract(Cuboid.CuboidDirection.South); - cursor = cursor.contract(Cuboid.CuboidDirection.East); - cursor = cursor.contract(Cuboid.CuboidDirection.West); - b[0] = cursor.getLowerNE(); - b[1] = cursor.getUpperSW(); - player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); - player().updateInventory(); - sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); - sender().sendMessage(C.GREEN + "Auto-select complete!"); - } - - @Decree(name = "x+y", description = "Autoselect up & out", sync = true) - public void xpy() { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage(C.YELLOW + "Hold your wand!"); - return; - } - - Location[] b = WandSVC.getCuboid(player()); - if (b == null || b[0] == null || b[1] == null) { - sender().sendMessage("No area selected."); - return; - } - b[0].add(new Vector(0, 1, 0)); - b[1].add(new Vector(0, 1, 0)); - Location a1 = b[0].clone(); - Location a2 = b[1].clone(); - Cuboid cursor = new Cuboid(a1, a2); - - while (!cursor.containsOnly(Material.AIR)) { - a1.add(new Vector(0, 1, 0)); - a2.add(new Vector(0, 1, 0)); - cursor = new Cuboid(a1, a2); - } - - a1.add(new Vector(0, -1, 0)); - a2.add(new Vector(0, -1, 0)); - b[0] = a1; - a2 = b[1]; - cursor = new Cuboid(a1, a2); - cursor = cursor.contract(Cuboid.CuboidDirection.North); - cursor = cursor.contract(Cuboid.CuboidDirection.South); - cursor = cursor.contract(Cuboid.CuboidDirection.East); - cursor = cursor.contract(Cuboid.CuboidDirection.West); - b[0] = cursor.getLowerNE(); - b[1] = cursor.getUpperSW(); - player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); - player().updateInventory(); - sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); - sender().sendMessage(C.GREEN + "Auto-select complete!"); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandPregen.java b/core/src/main/java/com/volmit/iris/core/commands/CommandPregen.java deleted file mode 100644 index 2de5cef25..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandPregen.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.gui.PregeneratorJob; -import com.volmit.iris.core.pregenerator.PregenTask; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.math.Position2; -import org.bukkit.World; -import org.bukkit.util.Vector; - -@Decree(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!") -public class CommandPregen implements DecreeExecutor { - @Decree(description = "Pregenerate a world") - public void start( - @Param(description = "The radius of the pregen in blocks", aliases = "size") - int radius, - @Param(description = "The world to pregen", contextual = true) - World world, - @Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0") - Vector center, - @Param(description = "Open the Iris pregen gui", defaultValue = "true") - boolean gui - ) { - try { - if (sender().isPlayer() && access() == null) { - sender().sendMessage(C.RED + "The engine access for this world is null!"); - sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example."); - } - radius = Math.max(radius, 1024); - IrisToolbelt.pregenerate(PregenTask - .builder() - .center(new Position2(center.getBlockX(), center.getBlockZ())) - .gui(gui) - .radiusX(radius) - .radiusZ(radius) - .build(), world); - String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ(); - sender().sendMessage(msg); - Iris.info(msg); - } catch (Throwable e) { - sender().sendMessage(C.RED + "Epic fail. See console."); - Iris.reportError(e); - e.printStackTrace(); - } - } - - @Decree(description = "Stop the active pregeneration task", aliases = "x") - public void stop() { - if (PregeneratorJob.shutdownInstance()) { - Iris.info( C.BLUE + "Finishing up mca region..."); - } else { - sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop"); - } - } - - @Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) - public void pause() { - if (PregeneratorJob.pauseResume()) { - sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + "."); - } else { - sender().sendMessage(C.YELLOW + "No active pregeneration tasks to pause/unpause."); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandSettings.java b/core/src/main/java/com/volmit/iris/core/commands/CommandSettings.java deleted file mode 100644 index e1da7efd7..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandSettings.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.util.decree.DecreeExecutor; - -public class CommandSettings implements DecreeExecutor { - -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java b/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java deleted file mode 100644 index 467427681..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java +++ /dev/null @@ -1,947 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.gui.NoiseExplorerGUI; -import com.volmit.iris.core.gui.VisionGUI; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.project.IrisProject; -import com.volmit.iris.core.service.ConversionSVC; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.decree.DecreeContext; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.DecreeOrigin; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.function.NoiseProvider; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.mantle.MantleChunk; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.math.Spiraler; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.parallel.SyncExecutor; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.O; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import com.volmit.iris.util.scheduling.jobs.ParallelRadiusJob; -import io.papermc.lib.PaperLib; -import org.bukkit.*; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.inventory.Inventory; -import org.bukkit.util.BlockVector; -import org.bukkit.util.Vector; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.file.Files; -import java.nio.file.attribute.FileTime; -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; -import java.util.Date; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Supplier; - -@Decree(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true) -public class CommandStudio implements DecreeExecutor { - private CommandFind find; - private CommandEdit edit; - //private CommandDeepSearch deepSearch; - - public static String hrf(Duration duration) { - return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase(); - } - - //TODO fix pack trimming - @Decree(description = "Download a project.", aliases = "dl") - public void download( - @Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project") - String pack, - @Param(name = "branch", description = "The branch to download from", defaultValue = "master") - String branch, - //@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false") - //boolean trim, - @Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false") - boolean overwrite - ) { - new CommandIris().download(pack, branch, overwrite); - } - - @Decree(description = "Open a new studio world", aliases = "o", sync = true) - public void open( - @Param(defaultValue = "default", description = "The dimension to open a studio for", aliases = "dim") - IrisDimension dimension, - @Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s") - long seed) { - sender().sendMessage(C.GREEN + "Opening studio for the \"" + dimension.getName() + "\" pack (seed: " + seed + ")"); - Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey()); - } - - @Decree(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"}) - public void vscode( - @Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim") - IrisDimension dimension - ) { - sender().sendMessage(C.GREEN + "Opening VSCode for the \"" + dimension.getName() + "\" pack"); - Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey()); - } - - @Decree(description = "Close an open studio project", aliases = {"x", "c"}, sync = true) - public void close() { - if (!Iris.service(StudioSVC.class).isProjectOpen()) { - sender().sendMessage(C.RED + "No open studio projects."); - return; - } - - Iris.service(StudioSVC.class).close(); - sender().sendMessage(C.GREEN + "Project Closed."); - } - - @Decree(description = "Create a new studio project", aliases = "+", sync = true) - public void create( - @Param(description = "The name of this new Iris Project.") - String name, - @Param(description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.", contextual = true) - IrisDimension template) { - if (template != null) { - Iris.service(StudioSVC.class).create(sender(), name, template.getLoadKey()); - } else { - Iris.service(StudioSVC.class).create(sender(), name); - } - } - - @Decree(description = "Get the version of a pack") - public void version( - @Param(defaultValue = "default", description = "The dimension get the version of", aliases = "dim", contextual = true) - IrisDimension dimension - ) { - sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion()); - } - - @Decree(name = "regen", description = "Regenerate nearby chunks.", aliases = "rg", sync = true, origin = DecreeOrigin.PLAYER) - public void regen( - @Param(name = "radius", description = "The radius of nearby cunks", defaultValue = "5") - int radius - ) { - World world = player().getWorld(); - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "You must be in an Iris World to use regen!"); - } - - VolmitSender sender = sender(); - var loc = player().getLocation().clone(); - - J.a(() -> { - PlatformChunkGenerator plat = IrisToolbelt.access(world); - Engine engine = plat.getEngine(); - DecreeContext.touch(sender); - try (SyncExecutor executor = new SyncExecutor(20); - var service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) - ) { - int x = loc.getBlockX() >> 4; - int z = loc.getBlockZ() >> 4; - - int rad = engine.getMantle().getRadius(); - var mantle = engine.getMantle().getMantle(); - var chunkMap = new KMap(); - ParallelRadiusJob prep = new ParallelRadiusJob(Integer.MAX_VALUE, service) { - @Override - protected void execute(int rX, int rZ) { - if (Math.abs(rX) <= radius && Math.abs(rZ) <= radius) { - mantle.deleteChunk(rX + x, rZ + z); - return; - } - rX += x; - rZ += z; - chunkMap.put(new Position2(rX, rZ), mantle.getChunk(rX, rZ)); - mantle.deleteChunk(rX, rZ); - } - - @Override - public String getName() { - return "Preparing Mantle"; - } - }.retarget(radius + rad, 0, 0); - CountDownLatch pLatch = new CountDownLatch(1); - prep.execute(sender(), pLatch::countDown); - pLatch.await(); - - - ParallelRadiusJob job = new ParallelRadiusJob(Integer.MAX_VALUE, service) { - @Override - protected void execute(int x, int z) { - plat.injectChunkReplacement(world, x, z, executor); - } - - @Override - public String getName() { - return "Regenerating"; - } - }.retarget(radius, x, z); - CountDownLatch latch = new CountDownLatch(1); - job.execute(sender(), latch::countDown); - latch.await(); - - chunkMap.forEach((pos, chunk) -> - mantle.getChunk(pos.getX(), pos.getZ()).copyFrom(chunk)); - } catch (Throwable e) { - sender().sendMessage("Error while regenerating chunks"); - e.printStackTrace(); - } finally { - DecreeContext.remove(); - } - }); - } - - @Decree(description = "Convert objects in the \"convert\" folder") - public void convert() { - Iris.service(ConversionSVC.class).check(sender()); - //IrisConverter.convertSchematics(sender()); - } - - @Decree(description = "Execute a script", aliases = "run", origin = DecreeOrigin.PLAYER) - public void execute( - @Param(description = "The script to run") - IrisScript script - ) { - engine().getExecution().execute(script.getLoadKey()); - } - - @Decree(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"}) - public void noise() { - if (noGUI()) return; - sender().sendMessage(C.GREEN + "Opening Noise Explorer!"); - NoiseExplorerGUI.launch(); - } - - @Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER) - public void charge() { - if (!IrisToolbelt.isIrisWorld(world())) { - sender().sendMessage(C.RED + "You must be in an Iris world to charge spawners!"); - return; - } - sender().sendMessage(C.GREEN + "Charging spawners!"); - engine().getWorldManager().chargeEnergy(); - } - - @Decree(description = "Preview noise gens (External GUI)", aliases = {"generator", "gen"}) - public void explore( - @Param(description = "The generator to explore", contextual = true) - IrisGenerator generator, - @Param(description = "The seed to generate with", defaultValue = "12345") - long seed - ) { - if (noGUI()) return; - sender().sendMessage(C.GREEN + "Opening Noise Explorer!"); - - Supplier> l = () -> { - - if (generator == null) { - return (x, z) -> 0D; - } - - return (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax()); - }; - NoiseExplorerGUI.launch(l, "Custom Generator"); - } - - @Decree(description = "Hotload a studio", aliases = {"reload", "h"}) - public void hotload() { - if (!Iris.service(StudioSVC.class).isProjectOpen()) { - sender().sendMessage(C.RED + "No studio world open!"); - return; - } - Iris.service(StudioSVC.class).getActiveProject().getActiveProvider().getEngine().hotload(); - sender().sendMessage(C.GREEN + "Hotloaded"); - } - - @Decree(description = "Show loot if a chest were right here", origin = DecreeOrigin.PLAYER, sync = true) - public void loot( - @Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", defaultValue = "false") - boolean fast, - @Param(description = "Whether or not to append to the inventory currently open (if false, clears opened inventory)", defaultValue = "true") - boolean add - ) { - if (noStudio()) return; - - KList tables = engine().getLootTables(RNG.r, player().getLocation().getBlock()); - Inventory inv = Bukkit.createInventory(null, 27 * 2); - - try { - engine().addItems(true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage()); - return; - } - - - O ta = new O<>(); - ta.set(-1); - - var sender = sender(); - var player = player(); - var engine = engine(); - - ta.set(Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () -> - { - if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) { - Bukkit.getScheduler().cancelTask(ta.get()); - sender.sendMessage(C.GREEN + "Opened inventory!"); - return; - } - - if (!add) { - inv.clear(); - } - - engine.addItems(true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ(), 1); - }, 0, fast ? 5 : 35)); - - sender().sendMessage(C.GREEN + "Opening inventory now!"); - player().openInventory(inv); - } - - @Decree(description = "Calculate the chance for each region to generate", origin = DecreeOrigin.PLAYER) - public void regions(@Param(description = "The radius in chunks", defaultValue = "500") int radius) { - var engine = engine(); - if (engine == null) { - sender().sendMessage(C.RED + "Only works in an Iris world!"); - return; - } - var sender = sender(); - var player = player(); - Thread.ofVirtual() - .start(() -> { - int d = radius * 2; - KMap data = new KMap<>(); - engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0))); - var multiBurst = new MultiBurst("Region Sampler"); - var executor = multiBurst.burst(radius * radius); - sender.sendMessage(C.GRAY + "Generating data..."); - var loc = player.getLocation(); - int totalTasks = d * d; - AtomicInteger completedTasks = new AtomicInteger(0); - int c = J.ar(() -> sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding regions"), 0); - new Spiraler(d, d, (x, z) -> executor.queue(() -> { - var region = engine.getRegion((x << 4) + 8, (z << 4) + 8); - data.computeIfAbsent(region.getLoadKey(), (k) -> new AtomicInteger(0)) - .incrementAndGet(); - completedTasks.incrementAndGet(); - })).setOffset(loc.getBlockX(), loc.getBlockZ()).drain(); - executor.complete(); - multiBurst.close(); - J.car(c); - - sender.sendMessage(C.GREEN + "Done!"); - var loader = engine.getData().getRegionLoader(); - data.forEach((k, v) -> sender.sendMessage(C.GREEN + k + ": " + loader.load(k).getRarity() + " / " + Form.f((double) v.get() / totalTasks * 100, 2) + "%")); - }); - } - - @Decree(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DecreeOrigin.PLAYER) - public void distances(@Param(description = "The radius in chunks") int radius) { - var engine = engine(); - if (engine == null) { - sender().sendMessage(C.RED + "Only works in an Iris world!"); - return; - } - var sender = sender(); - int d = radius * 2; - KMap> data = new KMap<>(); - var multiBurst = new MultiBurst("Distance Sampler"); - var executor = multiBurst.burst(radius * radius); - - sender.sendMessage(C.GRAY + "Generating data..."); - var loc = player().getLocation(); - int totalTasks = d * d; - AtomicInteger completedTasks = new AtomicInteger(0); - int c = J.ar(() -> { - sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding structures"); - }, 0); - - new Spiraler(d, d, (x, z) -> executor.queue(() -> { - var struct = engine.getStructureAt(x, z); - if (struct != null) { - data.computeIfAbsent(struct.getLoadKey(), (k) -> new KList<>()).add(new Position2(x, z)); - } - completedTasks.incrementAndGet(); - })).setOffset(loc.getBlockX(), loc.getBlockZ()).drain(); - - executor.complete(); - multiBurst.close(); - J.car(c); - - for (var key : data.keySet()) { - var list = data.get(key); - KList distances = new KList<>(list.size() - 1); - for (int i = 0; i < list.size(); i++) { - var pos = list.get(i); - double dist = Integer.MAX_VALUE; - for (var p : list) { - if (p.equals(pos)) continue; - dist = Math.min(dist, Math.sqrt(Math.pow(pos.getX() - p.getX(), 2) + Math.pow(pos.getZ() - p.getZ(), 2))); - } - if (dist == Integer.MAX_VALUE) continue; - distances.add(Math.round(dist * 16)); - } - long[] array = new long[distances.size()]; - for (int i = 0; i < distances.size(); i++) { - array[i] = distances.get(i); - } - Arrays.sort(array); - long min = array.length > 0 ? array[0] : 0; - long max = array.length > 0 ? array[array.length - 1] : 0; - long sum = Arrays.stream(array).sum(); - long avg = array.length > 0 ? Math.round(sum / (double) array.length) : 0; - String msg = "%s: %s => min: %s/max: %s -> avg: %s".formatted(key, list.size(), min, max, avg); - sender.sendMessage(msg); - } - if (data.isEmpty()) { - sender.sendMessage(C.RED + "No data found!"); - } else { - sender.sendMessage(C.GREEN + "Done!"); - } - } - - - @Decree(description = "Render a world map (External GUI)", aliases = "render") - public void map( - @Param(name = "world", description = "The world to open the generator for", contextual = true) - World world - ) { - if (noGUI()) return; - - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "You need to be in or specify an Iris-generated world!"); - return; - } - - VisionGUI.launch(IrisToolbelt.access(world).getEngine(), 0); - sender().sendMessage(C.GREEN + "Opening map!"); - } - - @Decree(description = "Package a dimension into a compressed format", aliases = "package") - public void pkg( - @Param(name = "dimension", description = "The dimension pack to compress", contextual = true, defaultValue = "default") - IrisDimension dimension, - @Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", defaultValue = "false") - boolean obfuscate, - @Param(name = "minify", description = "Whether or not to minify the pack", defaultValue = "true") - boolean minify - ) { - Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify); - } - - @Decree(description = "Profiles the performance of a dimension", origin = DecreeOrigin.PLAYER) - public void profile( - @Param(description = "The dimension to profile", contextual = true, defaultValue = "default") - IrisDimension dimension - ) { - // Todo: Make this more accurate - File pack = dimension.getLoadFile().getParentFile().getParentFile(); - File report = Iris.instance.getDataFile("profile.txt"); - IrisProject project = new IrisProject(pack); - IrisData data = IrisData.get(pack); - - KList fileText = new KList<>(); - - KMap styleTimings = new KMap<>(); - KMap interpolatorTimings = new KMap<>(); - KMap generatorTimings = new KMap<>(); - KMap biomeTimings = new KMap<>(); - KMap regionTimings = new KMap<>(); - - sender().sendMessage("Calculating Performance Metrics for Noise generators"); - - for (NoiseStyle i : NoiseStyle.values()) { - CNG c = i.create(new RNG(i.hashCode())); - - for (int j = 0; j < 3000; j++) { - c.noise(j, j + 1000, j * j); - c.noise(j, -j); - } - - PrecisionStopwatch px = PrecisionStopwatch.start(); - - for (int j = 0; j < 100000; j++) { - c.noise(j, j + 1000, j * j); - c.noise(j, -j); - } - - styleTimings.put(i, px.getMilliseconds()); - } - - fileText.add("Noise Style Performance Impacts: "); - - for (NoiseStyle i : styleTimings.sortKNumber()) { - fileText.add(i.name() + ": " + styleTimings.get(i)); - } - - fileText.add(""); - - sender().sendMessage("Calculating Interpolator Timings..."); - - for (InterpolationMethod i : InterpolationMethod.values()) { - IrisInterpolator in = new IrisInterpolator(); - in.setFunction(i); - in.setHorizontalScale(8); - - NoiseProvider np = (x, z) -> Math.random(); - - for (int j = 0; j < 3000; j++) { - in.interpolate(j, -j, np); - } - - PrecisionStopwatch px = PrecisionStopwatch.start(); - - for (int j = 0; j < 100000; j++) { - in.interpolate(j + 10000, -j - 100000, np); - } - - interpolatorTimings.put(i, px.getMilliseconds()); - } - - fileText.add("Noise Interpolator Performance Impacts: "); - - for (InterpolationMethod i : interpolatorTimings.sortKNumber()) { - fileText.add(i.name() + ": " + interpolatorTimings.get(i)); - } - - fileText.add(""); - - sender().sendMessage("Processing Generator Scores: "); - - KMap> btx = new KMap<>(); - - for (String i : data.getGeneratorLoader().getPossibleKeys()) { - KList vv = new KList<>(); - IrisGenerator g = data.getGeneratorLoader().load(i); - KList composites = g.getAllComposites(); - double score = 0; - int m = 0; - for (IrisNoiseGenerator j : composites) { - m++; - score += styleTimings.get(j.getStyle().getStyle()); - vv.add("Composite Noise Style " + m + " " + j.getStyle().getStyle().name() + ": " + styleTimings.get(j.getStyle().getStyle())); - } - - score += interpolatorTimings.get(g.getInterpolator().getFunction()); - vv.add("Interpolator " + g.getInterpolator().getFunction().name() + ": " + interpolatorTimings.get(g.getInterpolator().getFunction())); - generatorTimings.put(i, score); - btx.put(i, vv); - } - - fileText.add("Project Generator Performance Impacts: "); - - for (String i : generatorTimings.sortKNumber()) { - fileText.add(i + ": " + generatorTimings.get(i)); - - btx.get(i).forEach((ii) -> fileText.add(" " + ii)); - } - - fileText.add(""); - - KMap> bt = new KMap<>(); - - for (String i : data.getBiomeLoader().getPossibleKeys()) { - KList vv = new KList<>(); - IrisBiome b = data.getBiomeLoader().load(i); - double score = 0; - - int m = 0; - for (IrisBiomePaletteLayer j : b.getLayers()) { - m++; - score += styleTimings.get(j.getStyle().getStyle()); - vv.add("Palette Layer " + m + ": " + styleTimings.get(j.getStyle().getStyle())); - } - - score += styleTimings.get(b.getBiomeStyle().getStyle()); - vv.add("Biome Style: " + styleTimings.get(b.getBiomeStyle().getStyle())); - score += styleTimings.get(b.getChildStyle().getStyle()); - vv.add("Child Style: " + styleTimings.get(b.getChildStyle().getStyle())); - biomeTimings.put(i, score); - bt.put(i, vv); - } - - fileText.add("Project Biome Performance Impacts: "); - - for (String i : biomeTimings.sortKNumber()) { - fileText.add(i + ": " + biomeTimings.get(i)); - - bt.get(i).forEach((ff) -> fileText.add(" " + ff)); - } - - fileText.add(""); - - for (String i : data.getRegionLoader().getPossibleKeys()) { - IrisRegion b = data.getRegionLoader().load(i); - double score = 0; - - score += styleTimings.get(b.getLakeStyle().getStyle()); - score += styleTimings.get(b.getRiverStyle().getStyle()); - regionTimings.put(i, score); - } - - fileText.add("Project Region Performance Impacts: "); - - for (String i : regionTimings.sortKNumber()) { - fileText.add(i + ": " + regionTimings.get(i)); - } - - fileText.add(""); - - double m = 0; - for (double i : biomeTimings.v()) { - m += i; - } - m /= biomeTimings.size(); - double mm = 0; - for (double i : generatorTimings.v()) { - mm += i; - } - mm /= generatorTimings.size(); - m += mm; - double mmm = 0; - for (double i : regionTimings.v()) { - mmm += i; - } - mmm /= regionTimings.size(); - m += mmm; - - fileText.add("Average Score: " + m); - sender().sendMessage("Score: " + Form.duration(m, 0)); - - try { - IO.writeAll(report, fileText.toString("\n")); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - - sender().sendMessage(C.GREEN + "Done! " + report.getPath()); - } - - @Decree(description = "Spawn an Iris entity", aliases = "summon", origin = DecreeOrigin.PLAYER) - public void spawn( - @Param(description = "The entity to spawn") - IrisEntity entity, - @Param(description = "The location to spawn the entity at", contextual = true) - Vector location - ) { - if (!IrisToolbelt.isIrisWorld(player().getWorld())) { - sender().sendMessage(C.RED + "You have to be in an Iris world to spawn entities properly. Trying to spawn the best we can do."); - } - entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ())); - } - - @Decree(description = "Teleport to the active studio world", aliases = "stp", origin = DecreeOrigin.PLAYER, sync = true) - public void tpstudio() { - if (!Iris.service(StudioSVC.class).isProjectOpen()) { - sender().sendMessage(C.RED + "No studio world is open!"); - return; - } - - if (IrisToolbelt.isIrisWorld(world()) && engine().isStudio()) { - sender().sendMessage(C.RED + "You are already in a studio world!"); - return; - } - - sender().sendMessage(C.GREEN + "Sending you to the studio world!"); - var player = player(); - PaperLib.teleportAsync(player(), Iris.service(StudioSVC.class) - .getActiveProject() - .getActiveProvider() - .getTarget() - .getWorld() - .spawnLocation() - ).thenRun(() -> player.setGameMode(GameMode.SPECTATOR)); - } - - @Decree(description = "Update your dimension projects VSCode workspace") - public void update( - @Param(description = "The dimension to update the workspace of", contextual = true, defaultValue = "default") - IrisDimension dimension - ) { - sender().sendMessage(C.GOLD + "Updating Code Workspace for " + dimension.getName() + "..."); - if (new IrisProject(dimension.getLoader().getDataFolder()).updateWorkspace()) { - sender().sendMessage(C.GREEN + "Updated Code Workspace for " + dimension.getName()); - } else { - sender().sendMessage(C.RED + "Invalid project: " + dimension.getName() + ". Try deleting the code-workspace file and try again."); - } - } - - @Decree(aliases = "find-objects", description = "Get information about nearby structures") - public void objects() { - if (!IrisToolbelt.isIrisWorld(player().getWorld())) { - sender().sendMessage(C.RED + "You must be in an Iris world"); - return; - } - - World world = player().getWorld(); - - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage("You must be in an iris world."); - return; - } - KList chunks = new KList<>(); - int bx = player().getLocation().getChunk().getX(); - int bz = player().getLocation().getChunk().getZ(); - - try { - Location l = player().getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation(); - - int cx = l.getChunk().getX(); - int cz = l.getChunk().getZ(); - new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain(); - } catch (Throwable e) { - Iris.reportError(e); - } - - new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain(); - sender().sendMessage("Capturing IGenData from " + chunks.size() + " nearby chunks."); - try { - File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt"); - PrintWriter pw = new PrintWriter(ff); - pw.println("=== Iris Chunk Report ==="); - pw.println("== General Info =="); - pw.println("Iris Version: " + Iris.instance.getDescription().getVersion()); - pw.println("Bukkit Version: " + Bukkit.getBukkitVersion()); - pw.println("MC Version: " + Bukkit.getVersion()); - pw.println("PaperSpigot: " + (PaperLib.isPaper() ? "Yup!" : "Nope!")); - pw.println("Report Captured At: " + new Date()); - pw.println("Chunks: (" + chunks.size() + "): "); - - for (Chunk i : chunks) { - pw.println("- [" + i.getX() + ", " + i.getZ() + "]"); - } - - int regions = 0; - long size = 0; - String age = "No idea..."; - - try { - for (File i : Objects.requireNonNull(new File(world.getWorldFolder(), "region").listFiles())) { - if (i.isFile()) { - size += i.length(); - } - } - } catch (Throwable e) { - Iris.reportError(e); - } - - try { - FileTime creationTime = (FileTime) Files.getAttribute(world.getWorldFolder().toPath(), "creationTime"); - age = hrf(Duration.of(M.ms() - creationTime.toMillis(), ChronoUnit.MILLIS)); - } catch (IOException e) { - Iris.reportError(e); - } - - KList biomes = new KList<>(); - KList caveBiomes = new KList<>(); - KMap>> objects = new KMap<>(); - - for (Chunk i : chunks) { - for (int j = 0; j < 16; j += 3) { - - for (int k = 0; k < 16; k += 3) { - - assert engine() != null; - IrisBiome bb = engine().getSurfaceBiome((i.getX() * 16) + j, (i.getZ() * 16) + k); - IrisBiome bxf = engine().getCaveBiome((i.getX() * 16) + j, (i.getZ() * 16) + k); - biomes.addIfMissing(bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")"); - caveBiomes.addIfMissing(bxf.getName() + " (" + bxf.getLoadFile().getName() + ")"); - exportObjects(bb, pw, engine(), objects); - exportObjects(bxf, pw, engine(), objects); - } - } - } - - regions = Objects.requireNonNull(new File(world.getWorldFolder().getPath() + "/region").list()).length; - - pw.println(); - pw.println("== World Info =="); - pw.println("World Name: " + world.getName()); - pw.println("Age: " + age); - pw.println("Folder: " + world.getWorldFolder().getPath()); - pw.println("Regions: " + Form.f(regions)); - pw.println("Chunks: max. " + Form.f(regions * 32 * 32)); - pw.println("World Size: min. " + Form.fileSize(size)); - pw.println(); - pw.println("== Biome Info =="); - pw.println("Found " + biomes.size() + " Biome(s): "); - - for (String i : biomes) { - pw.println("- " + i); - } - pw.println(); - - pw.println("== Object Info =="); - - for (String i : objects.k()) { - pw.println("- " + i); - - for (String j : objects.get(i).k()) { - pw.println(" @ " + j); - - for (String k : objects.get(i).get(j)) { - pw.println(" * " + k); - } - } - } - - pw.println(); - pw.close(); - - sender().sendMessage("Reported to: " + ff.getPath()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - Iris.reportError(e); - } - } - - private void exportObjects(IrisBiome bb, PrintWriter pw, Engine g, KMap>> objects) { - String n1 = bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")"; - int m = 0; - KSet stop = new KSet<>(); - for (IrisObjectPlacement f : bb.getObjects()) { - m++; - String n2 = "Placement #" + m + " (" + f.getPlace().size() + " possible objects)"; - - for (String i : f.getPlace()) { - String nn3 = i + ": [ERROR] Failed to find object!"; - - try { - if (stop.contains(i)) { - continue; - } - - File ff = g.getData().getObjectLoader().findFile(i); - BlockVector sz = IrisObject.sampleSize(ff); - nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]"; - stop.add(i); - } catch (Throwable e) { - Iris.reportError(e); - } - - String n3 = nn3; - objects.computeIfAbsent(n1, (k1) -> new KMap<>()) - .computeIfAbsent(n2, (k) -> new KList<>()).addIfMissing(n3); - } - } - } - - /** - * @return true if server GUIs are not enabled - */ - private boolean noGUI() { - if (!IrisSettings.get().getGui().isUseServerLaunchedGuis()) { - sender().sendMessage(C.RED + "You must have server launched GUIs enabled in the settings!"); - return true; - } - return false; - } - - /** - * @return true if no studio is open or the player is not in one - */ - private boolean noStudio() { - if (!sender().isPlayer()) { - sender().sendMessage(C.RED + "Players only!"); - return true; - } - if (!Iris.service(StudioSVC.class).isProjectOpen()) { - sender().sendMessage(C.RED + "No studio world is open!"); - return true; - } - if (!engine().isStudio()) { - sender().sendMessage(C.RED + "You must be in a studio world!"); - return true; - } - return false; - } - - - public void files(File clean, KList files) { - if (clean.isDirectory()) { - for (File i : clean.listFiles()) { - files(i, files); - } - } else if (clean.getName().endsWith(".json")) { - try { - files.add(clean); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!"); - } - } - } - - private void fixBlocks(JSONObject obj) { - for (String i : obj.keySet()) { - Object o = obj.get(i); - - if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) { - obj.put(i, "minecraft:" + o); - } - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o); - } - } - } - - private void fixBlocks(JSONArray obj) { - for (int i = 0; i < obj.length(); i++) { - Object o = obj.get(i); - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o); - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandTurboPregen.java b/core/src/main/java/com/volmit/iris/core/commands/CommandTurboPregen.java deleted file mode 100644 index 18a7be8d7..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandTurboPregen.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.pregenerator.LazyPregenerator; -import com.volmit.iris.core.pregenerator.TurboPregenerator; -import com.volmit.iris.core.pregenerator.TurboPregenerator; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.format.C; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.util.Vector; - -import java.io.File; -import java.io.IOException; - -@Decree(name = "turbopregen", aliases = "turbo", description = "Pregenerate your Iris worlds!") -public class CommandTurboPregen implements DecreeExecutor { - public String worldName; - @Decree(description = "Pregenerate a world") - public void start( - @Param(description = "The radius of the pregen in blocks", aliases = "size") - int radius, - @Param(description = "The world to pregen", contextual = true) - World world, - @Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0") - Vector center - ) { - - worldName = world.getName(); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File TurboFile = new File(worldDirectory, "turbogen.json"); - if (TurboFile.exists()) { - if (TurboPregenerator.getInstance() != null) { - sender().sendMessage(C.BLUE + "Turbo pregen is already in progress"); - Iris.info(C.YELLOW + "Turbo pregen is already in progress"); - return; - } else { - try { - TurboFile.delete(); - } catch (Exception e){ - Iris.error("Failed to delete the old instance file of Turbo Pregen!"); - return; - } - } - } - - try { - if (sender().isPlayer() && access() == null) { - sender().sendMessage(C.RED + "The engine access for this world is null!"); - sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example."); - } - - TurboPregenerator.TurboPregenJob pregenJob = TurboPregenerator.TurboPregenJob.builder() - .world(worldName) - .radiusBlocks(radius) - .position(0) - .build(); - - File TurboGenFile = new File(worldDirectory, "turbogen.json"); - TurboPregenerator pregenerator = new TurboPregenerator(pregenJob, TurboGenFile); - pregenerator.start(); - - String msg = C.GREEN + "TurboPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ(); - sender().sendMessage(msg); - Iris.info(msg); - } catch (Throwable e) { - sender().sendMessage(C.RED + "Epic fail. See console."); - Iris.reportError(e); - e.printStackTrace(); - } - } - - @Decree(description = "Stop the active pregeneration task", aliases = "x") - public void stop(@Param(aliases = "world", description = "The world to pause") World world) throws IOException { - TurboPregenerator turboPregenInstance = TurboPregenerator.getInstance(); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File turboFile = new File(worldDirectory, "turbogen.json"); - - if (turboPregenInstance != null) { - turboPregenInstance.shutdownInstance(world); - sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName()); - } else if (turboFile.exists() && turboFile.delete()) { - sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName()); - } else if (turboFile.exists()) { - Iris.error("Failed to delete the old instance file of Turbo Pregen!"); - } else { - sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop"); - } - } - - @Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) - public void pause( - @Param(aliases = "world", description = "The world to pause") - World world - ) { - if (TurboPregenerator.getInstance() != null) { - TurboPregenerator.setPausedTurbo(world); - sender().sendMessage(C.GREEN + "Paused/unpaused Turbo Pregen, now: " + (TurboPregenerator.isPausedTurbo(world) ? "Paused" : "Running") + "."); - } else { - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File TurboFile = new File(worldDirectory, "turbogen.json"); - if (TurboFile.exists()){ - TurboPregenerator.loadTurboGenerator(world.getName()); - sender().sendMessage(C.YELLOW + "Started Turbo Pregen back up!"); - } else { - sender().sendMessage(C.YELLOW + "No active Turbo Pregen tasks to pause/unpause."); - } - - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandUpdater.java b/core/src/main/java/com/volmit/iris/core/commands/CommandUpdater.java deleted file mode 100644 index e19fc61ba..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandUpdater.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import lombok.Synchronized; -import org.bukkit.World; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.pregenerator.ChunkUpdater; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.DecreeOrigin; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; - -@Decree(name = "updater", origin = DecreeOrigin.BOTH, description = "Iris World Updater") -public class CommandUpdater implements DecreeExecutor { - private final Object lock = new Object(); - private transient ChunkUpdater chunkUpdater; - - @Decree(description = "Updates all chunk in the specified world") - public void start( - @Param(description = "World to update chunks at", contextual = true) - World world - ) { - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.GOLD + "This is not an Iris world"); - return; - } - synchronized (lock) { - if (chunkUpdater != null) { - chunkUpdater.stop(); - } - - chunkUpdater = new ChunkUpdater(world); - if (sender().isPlayer()) { - sender().sendMessage(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks())); - } else { - Iris.info(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks())); - } - chunkUpdater.start(); - } - } - - @Synchronized("lock") - @Decree(description = "Pause the updater") - public void pause( ) { - if (chunkUpdater == null) { - sender().sendMessage(C.GOLD + "You cant pause something that doesnt exist?"); - return; - } - boolean status = chunkUpdater.pause(); - if (sender().isPlayer()) { - if (status) { - sender().sendMessage(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName()); - } else { - sender().sendMessage(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName()); - } - } else { - if (status) { - Iris.info(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName()); - } else { - Iris.info(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName()); - } - } - } - - @Synchronized("lock") - @Decree(description = "Stops the updater") - public void stop() { - if (chunkUpdater == null) { - sender().sendMessage(C.GOLD + "You cant stop something that doesnt exist?"); - return; - } - if (sender().isPlayer()) { - sender().sendMessage("Stopping Updater for: " + C.GRAY + chunkUpdater.getName()); - } else { - Iris.info("Stopping Updater for: " + C.GRAY + chunkUpdater.getName()); - } - chunkUpdater.stop(); - } -} - - diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandWhat.java b/core/src/main/java/com/volmit/iris/core/commands/CommandWhat.java deleted file mode 100644 index 48d8c726e..000000000 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandWhat.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.commands; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.edit.BlockSignal; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.decree.DecreeExecutor; -import com.volmit.iris.util.decree.DecreeOrigin; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.matter.MatterMarker; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.Chunk; -import org.bukkit.FluidCollisionMode; -import org.bukkit.Material; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; - -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; - -@Decree(name = "what", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris What?") -public class CommandWhat implements DecreeExecutor { - @Decree(description = "What is in my hand?", origin = DecreeOrigin.PLAYER) - public void hand() { - try { - BlockData bd = player().getInventory().getItemInMainHand().getType().createBlockData(); - if (!bd.getMaterial().equals(Material.AIR)) { - sender().sendMessage("Material: " + C.GREEN + bd.getMaterial().name()); - sender().sendMessage("Full: " + C.WHITE + bd.getAsString(true)); - } else { - sender().sendMessage("Please hold a block/item"); - } - } catch (Throwable e) { - Iris.reportError(e); - Material bd = player().getInventory().getItemInMainHand().getType(); - if (!bd.equals(Material.AIR)) { - sender().sendMessage("Material: " + C.GREEN + bd.name()); - } else { - sender().sendMessage("Please hold a block/item"); - } - } - } - - @Decree(description = "What biome am i in?", origin = DecreeOrigin.PLAYER) - public void biome() { - try { - IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ()); - sender().sendMessage("IBiome: " + b.getLoadKey() + " (" + b.getDerivative().name() + ")"); - - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage("Non-Iris Biome: " + player().getLocation().getBlock().getBiome().name()); - - if (player().getLocation().getBlock().getBiome().equals(Biome.CUSTOM)) { - try { - sender().sendMessage("Data Pack Biome: " + INMS.get().getTrueBiomeBaseKey(player().getLocation()) + " (ID: " + INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(player().getLocation())) + ")"); - } catch (Throwable ee) { - Iris.reportError(ee); - } - } - } - } - - @Decree(description = "What region am i in?", origin = DecreeOrigin.PLAYER) - public void region() { - try { - Chunk chunk = world().getChunkAt(player().getLocation().getBlockZ() / 16, player().getLocation().getBlockZ() / 16); - IrisRegion r = engine().getRegion(chunk); - sender().sendMessage("IRegion: " + r.getLoadKey() + " (" + r.getName() + ")"); - - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.IRIS + "Iris worlds only."); - } - } - - @Decree(description = "What block am i looking at?", origin = DecreeOrigin.PLAYER) - public void block() { - BlockData bd; - try { - bd = player().getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData(); - } catch (NullPointerException e) { - Iris.reportError(e); - sender().sendMessage("Please look at any block, not at the sky"); - bd = null; - } - - if (bd != null) { - sender().sendMessage("Material: " + C.GREEN + bd.getMaterial().name()); - sender().sendMessage("Full: " + C.WHITE + bd.getAsString(true)); - - if (B.isStorage(bd)) { - sender().sendMessage(C.YELLOW + "* Storage Block (Loot Capable)"); - } - - if (B.isLit(bd)) { - sender().sendMessage(C.YELLOW + "* Lit Block (Light Capable)"); - } - - if (B.isFoliage(bd)) { - sender().sendMessage(C.YELLOW + "* Foliage Block"); - } - - if (B.isDecorant(bd)) { - sender().sendMessage(C.YELLOW + "* Decorant Block"); - } - - if (B.isFluid(bd)) { - sender().sendMessage(C.YELLOW + "* Fluid Block"); - } - - if (B.isFoliagePlantable(bd)) { - sender().sendMessage(C.YELLOW + "* Plantable Foliage Block"); - } - - if (B.isSolid(bd)) { - sender().sendMessage(C.YELLOW + "* Solid Block"); - } - } - } - - @Decree(description = "Show markers in chunk", origin = DecreeOrigin.PLAYER) - public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling") String marker) { - Chunk c = player().getLocation().getChunk(); - - if (IrisToolbelt.isIrisWorld(c.getWorld())) { - int m = 1; - AtomicInteger v = new AtomicInteger(0); - - for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) { - for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) { - IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker)) - .convert((i) -> i.toLocation(c.getWorld())).forEach((i) -> { - J.s(() -> BlockSignal.of(i.getBlock(), 100)); - v.incrementAndGet(); - }); - } - } - - sender().sendMessage("Found " + v.get() + " Nearby Markers (" + marker + ")"); - } else { - sender().sendMessage(C.IRIS + "Iris worlds only."); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/edit/BlockSignal.java b/core/src/main/java/com/volmit/iris/core/edit/BlockSignal.java deleted file mode 100644 index 65526cd87..000000000 --- a/core/src/main/java/com/volmit/iris/core/edit/BlockSignal.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.edit; - -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.SR; -import org.bukkit.Location; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.bukkit.entity.FallingBlock; -import org.bukkit.entity.Player; -import org.bukkit.util.Vector; - -import java.util.concurrent.atomic.AtomicInteger; - -@SuppressWarnings("InstantiationOfUtilityClass") -public class BlockSignal { - public static final AtomicInteger active = new AtomicInteger(0); - - public BlockSignal(Block block, int ticks) { - active.incrementAndGet(); - Location tg = block.getLocation().clone().add(0.5, 0, 0.5); - FallingBlock e = block.getWorld().spawnFallingBlock(tg, block.getBlockData()); - e.setGravity(false); - e.setInvulnerable(true); - e.setGlowing(true); - e.setDropItem(false); - e.setHurtEntities(false); - e.setSilent(true); - e.setTicksLived(1); - e.setVelocity(new Vector(0, 0, 0)); - J.s(() -> { - e.remove(); - active.decrementAndGet(); - BlockData type = block.getBlockData(); - MultiBurst.burst.lazy(() -> { - for (Player i : block.getWorld().getPlayers()) { - i.sendBlockChange(block.getLocation(), block.getBlockData()); - } - }); - }, ticks); - } - - public static void of(Block block, int ticks) { - new BlockSignal(block, ticks); - } - - public static void of(Block block) { - of(block, 100); - } - - public static Runnable forever(Block block) { - Location tg = block.getLocation().clone().add(0.5, 0, 0.5).clone(); - FallingBlock e = block.getWorld().spawnFallingBlock(tg.clone(), block.getBlockData()); - e.setGravity(false); - e.setInvulnerable(true); - e.setGlowing(true); - e.teleport(tg.clone()); - e.setDropItem(false); - e.setHurtEntities(false); - e.setSilent(true); - e.setTicksLived(1); - e.setVelocity(new Vector(0, 0, 0)); - - new SR(20) { - @Override - public void run() { - if (e.isDead()) { - cancel(); - return; - } - - e.setTicksLived(1); - e.teleport(tg.clone()); - e.setVelocity(new Vector(0, 0, 0)); - } - }; - - return () -> { - e.remove(); - BlockData type = block.getBlockData(); - - MultiBurst.burst.lazy(() -> { - for (Player i : block.getWorld().getPlayers()) { - i.sendBlockChange(block.getLocation(), block.getBlockData()); - } - }); - }; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/edit/DustRevealer.java b/core/src/main/java/com/volmit/iris/core/edit/DustRevealer.java deleted file mode 100644 index 35353c771..000000000 --- a/core/src/main/java/com/volmit/iris/core/edit/DustRevealer.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.edit; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.BlockPosition; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import lombok.Data; -import org.bukkit.Sound; -import org.bukkit.World; -import org.bukkit.block.Block; - -@SuppressWarnings("ALL") -@Data -public class DustRevealer { - private final Engine engine; - private final World world; - private final BlockPosition block; - private final String key; - private final KList hits; - - public DustRevealer(Engine engine, World world, BlockPosition block, String key, KList hits) { - this.engine = engine; - this.world = world; - this.block = block; - this.key = key; - this.hits = hits; - - J.s(() -> { - new BlockSignal(world.getBlockAt(block.getX(), block.getY(), block.getZ()), 10); - if (M.r(0.25)) { - world.playSound(block.toBlock(world).getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1f, RNG.r.f(0.2f, 2f)); - } - J.a(() -> { - while (BlockSignal.active.get() > 128) { - J.sleep(5); - } - - try { - is(new BlockPosition(block.getX() + 1, block.getY(), block.getZ())); - is(new BlockPosition(block.getX() - 1, block.getY(), block.getZ())); - is(new BlockPosition(block.getX(), block.getY() + 1, block.getZ())); - is(new BlockPosition(block.getX(), block.getY() - 1, block.getZ())); - is(new BlockPosition(block.getX(), block.getY(), block.getZ() + 1)); - is(new BlockPosition(block.getX(), block.getY(), block.getZ() - 1)); - is(new BlockPosition(block.getX() + 1, block.getY(), block.getZ() + 1)); - is(new BlockPosition(block.getX() + 1, block.getY(), block.getZ() - 1)); - is(new BlockPosition(block.getX() - 1, block.getY(), block.getZ() + 1)); - is(new BlockPosition(block.getX() - 1, block.getY(), block.getZ() - 1)); - is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ())); - is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ())); - is(new BlockPosition(block.getX() - 1, block.getY() + 1, block.getZ())); - is(new BlockPosition(block.getX() - 1, block.getY() - 1, block.getZ())); - is(new BlockPosition(block.getX(), block.getY() + 1, block.getZ() - 1)); - is(new BlockPosition(block.getX(), block.getY() + 1, block.getZ() + 1)); - is(new BlockPosition(block.getX(), block.getY() - 1, block.getZ() - 1)); - is(new BlockPosition(block.getX(), block.getY() - 1, block.getZ() + 1)); - is(new BlockPosition(block.getX() - 1, block.getY() + 1, block.getZ() - 1)); - is(new BlockPosition(block.getX() - 1, block.getY() + 1, block.getZ() + 1)); - is(new BlockPosition(block.getX() - 1, block.getY() - 1, block.getZ() - 1)); - is(new BlockPosition(block.getX() - 1, block.getY() - 1, block.getZ() + 1)); - is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ() - 1)); - is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ() + 1)); - is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ() - 1)); - is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ() + 1)); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - }); - }, RNG.r.i(2, 8)); - } - - public static void spawn(Block block, VolmitSender sender) { - World world = block.getWorld(); - Engine access = IrisToolbelt.access(world).getEngine(); - - if (access != null) { - String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ()); - if (a != null) { - world.playSound(block.getLocation(), Sound.ITEM_LODESTONE_COMPASS_LOCK, 1f, 0.1f); - - sender.sendMessage("Found object " + a); - J.a(() -> { - new DustRevealer(access, world, new BlockPosition(block.getX(), block.getY(), block.getZ()), a, new KList<>()); - }); - } - } - } - - private boolean is(BlockPosition a) { - int betterY = a.getY() - world.getMinHeight(); - if (isValidTry(a) && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()) != null && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()).equals(key)) { - hits.add(a); - new DustRevealer(engine, world, a, key, hits); - return true; - } - - return false; - } - - private boolean isValidTry(BlockPosition b) { - return !hits.contains(b); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/edit/JigsawEditor.java b/core/src/main/java/com/volmit/iris/core/edit/JigsawEditor.java deleted file mode 100644 index 17655e1c5..000000000 --- a/core/src/main/java/com/volmit/iris/core/edit/JigsawEditor.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.edit; - -import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.core.service.WandSVC; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.Cuboid; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.Particle; -import org.bukkit.Sound; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.event.player.PlayerMoveEvent; -import org.bukkit.util.Vector; - -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.ExecutionException; - -public class JigsawEditor implements Listener { - public static final KMap editors = new KMap<>(); - private final Player player; - private final IrisObject object; - private final File targetSaveLocation; - private final IrisJigsawPiece piece; - private final Location origin; - private final Cuboid cuboid; - private final int ticker; - private final KMap falling = new KMap<>(); - private final ChronoLatch cl = new ChronoLatch(100); - private Location target; - - public JigsawEditor(Player player, IrisJigsawPiece piece, IrisObject object, File saveLocation) { - if (object == null) throw new RuntimeException("Object is null! " + piece.getObject()); - editors.compute(player, ($, current) -> { - if (current != null) { - current.exit(); - } - return this; - }); - - this.object = object; - this.player = player; - origin = player.getLocation().clone().add(0, 7, 0); - target = origin; - this.targetSaveLocation = saveLocation; - this.piece = piece == null ? new IrisJigsawPiece() : piece; - this.piece.setObject(object.getLoadKey()); - cuboid = new Cuboid(origin.clone(), origin.clone().add(object.getW() - 1, object.getH() - 1, object.getD() - 1)); - ticker = J.sr(this::onTick, 0); - J.s(() -> object.placeCenterY(origin)); - Iris.instance.registerListener(this); - } - - @EventHandler - public void on(PlayerMoveEvent e) { - if (e.getPlayer().equals(player)) { - try { - target = player.getTargetBlockExact(7).getLocation(); - } catch (Throwable ex) { - Iris.reportError(ex); - target = player.getLocation(); - return; - } - - if (cuboid.contains(target)) { - for (IrisPosition i : falling.k()) { - Location at = toLocation(i); - - if (at.equals(target)) { - falling.remove(i).run(); - } - } - } - } - } - - public Location toLocation(IrisPosition i) { - return origin.clone() - .add(new Vector(i.getX(), i.getY(), i.getZ())) - .add(object.getCenter()) - .getBlock() - .getLocation(); - } - - public IrisPosition toPosition(Location l) { - return new IrisPosition(l.clone().getBlock().getLocation() - .subtract(origin.clone()) - .subtract(object.getCenter()) - .add(1, 1, 1) - .toVector()); - } - - @EventHandler - public void on(PlayerInteractEvent e) { - if (e.getAction().equals(Action.RIGHT_CLICK_BLOCK)) { - if (e.getClickedBlock() != null && cuboid.contains(e.getClickedBlock().getLocation()) && e.getPlayer().equals(player)) { - IrisPosition pos = toPosition(e.getClickedBlock().getLocation()); - IrisJigsawPieceConnector connector = null; - for (IrisJigsawPieceConnector i : piece.getConnectors()) { - if (i.getPosition().equals(pos)) { - connector = i; - break; - } - } - - if (!player.isSneaking() && connector == null) { - connector = new IrisJigsawPieceConnector(); - connector.setDirection(IrisDirection.getDirection(e.getBlockFace())); - connector.setPosition(pos); - piece.getConnectors().add(connector); - player.playSound(e.getClickedBlock().getLocation(), Sound.ENTITY_ITEM_FRAME_ADD_ITEM, 1f, 1f); - } else if (player.isSneaking() && connector != null) { - piece.getConnectors().remove(connector); - player.playSound(e.getClickedBlock().getLocation(), Sound.ENTITY_ITEM_FRAME_REMOVE_ITEM, 1f, 1f); - } else if (connector != null && !player.isSneaking()) { - connector.setDirection(IrisDirection.getDirection(e.getBlockFace())); - player.playSound(e.getClickedBlock().getLocation(), Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 1f); - } - } - } - } - - private void removeKey(JSONObject o, String... path) { - if (path.length == 1) { - o.remove(path[0]); - return; - } - - List s = new java.util.ArrayList<>(List.of(path)); - s.remove(0); - removeKey(o.getJSONObject(path[0]), s.toArray(new String[0])); - } - - private List getObjectsInArray(JSONObject a) { // This gets all the objects in an array that are connectors - KList o = new KList<>(); - - for (int i = 0; i < a.getJSONArray("connectors").length(); i++) { - o.add(a.getJSONArray("connectors").getJSONObject(i)); - } - - return o; - } - - public void close() { - exit(); - try { - JSONObject j = new JSONObject(new Gson().toJson(piece)); - // Remove sub-key - removeKey(j, "placementOptions", "translateCenter"); // should work - J.attempt(() -> j.getJSONObject("placementOptions").remove("translateCenter")); // otherwise - - // remove root key - removeKey(j, "placementOptions"); // should work - j.remove("placementOptions"); // otherwise - - // Remove key in all objects in array - for (JSONObject i : getObjectsInArray(j)) { - removeKey(i, "rotateConnector"); - } - - IO.writeAll(targetSaveLocation, j.toString(4)); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - public void exit() { - J.car(ticker); - Iris.instance.unregisterListener(this); - try { - J.sfut(() -> { - object.unplaceCenterY(origin); - falling.v().forEach(Runnable::run); - }).get(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - editors.remove(player); - } - - public void onTick() { - if (cl.flip()) { - Iris.service(WandSVC.class).draw(cuboid, player); - - f: - for (IrisPosition i : falling.k()) { - for (IrisJigsawPieceConnector j : piece.getConnectors()) { - if (j.getPosition().equals(i)) { - continue f; - } - } - - falling.remove(i).run(); - } - - for (IrisJigsawPieceConnector i : piece.getConnectors()) { - IrisPosition pos = i.getPosition(); - Location at = toLocation(pos); - - Vector dir = i.getDirection().toVector().clone(); - - - for (int ix = 0; ix < RNG.r.i(1, 3); ix++) { - at.getWorld().spawnParticle(Particle.SOUL_FIRE_FLAME, at.clone().getBlock().getLocation().add(0.25, 0.25, 0.25).add(RNG.r.d(0.5), RNG.r.d(0.5), RNG.r.d(0.5)), 0, dir.getX(), dir.getY(), dir.getZ(), 0.092 + RNG.r.d(-0.03, 0.08)); - } - - if (at.getBlock().getLocation().equals(target)) { - continue; - } - - if (!falling.containsKey(pos)) { - if (at.getBlock().getType().isAir()) { - at.getBlock().setType(Material.STONE); - } - - falling.put(pos, BlockSignal.forever(at.getBlock())); - } - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/events/IrisLootEvent.java b/core/src/main/java/com/volmit/iris/core/events/IrisLootEvent.java deleted file mode 100644 index 067e874ce..000000000 --- a/core/src/main/java/com/volmit/iris/core/events/IrisLootEvent.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.volmit.iris.core.events; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.InventorySlotType; -import com.volmit.iris.engine.object.IrisLootTable; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.scheduling.J; -import lombok.Getter; -import org.bukkit.*; -import org.bukkit.block.Block; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.bukkit.event.world.LootGenerateEvent; - -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.bukkit.loot.LootContext; -import org.bukkit.loot.LootTable; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Collection; -import java.util.List; -import java.util.Random; - -@Getter -public class IrisLootEvent extends Event { - private static final HandlerList handlers = new HandlerList(); - private static final LootTable EMPTY = new LootTable() { - @NotNull - @Override - public NamespacedKey getKey() { - return new NamespacedKey(Iris.instance, "empty"); - } - - @NotNull - @Override - public Collection populateLoot(@Nullable Random random, @NotNull LootContext context) { - return List.of(); - } - - @Override - public void fillInventory(@NotNull Inventory inventory, @Nullable Random random, @NotNull LootContext context) { - } - }; - - private final Engine engine; - private final Block block; - private final InventorySlotType slot; - private final KList tables; - - /** - * Constructor for IrisLootEvent with mode selection. - * - * @param engine The engine instance. - * @param block The block associated with the event. - * @param slot The inventory slot type. - * @param tables The list of IrisLootTables. (mutable*) - */ - public IrisLootEvent(Engine engine, Block block, InventorySlotType slot, KList tables) { - this.engine = engine; - this.block = block; - this.slot = slot; - this.tables = tables; - } - - @Override - public HandlerList getHandlers() { - return handlers; - } - - /** - * Required method to get the HandlerList for this event. - * - * @return The HandlerList. - */ - public static HandlerList getHandlerList() { - return handlers; - } - - /** - * Triggers the corresponding Bukkit loot event. - * This method integrates your custom IrisLootTables with Bukkit's LootGenerateEvent, - * allowing other plugins to modify or cancel the loot generation. - * - * @return true when the event was canceled - */ - public static boolean callLootEvent(KList loot, Inventory inv, World world, int x, int y, int z) { - InventoryHolder holder = inv.getHolder(); - Location loc = new Location(world, x, y, z); - if (holder == null) { - holder = new InventoryHolder() { - @NotNull - @Override - public Inventory getInventory() { - return inv; - } - }; - } - - LootContext context = new LootContext.Builder(loc).build(); - LootGenerateEvent event = new LootGenerateEvent(world, null, holder, EMPTY, context, loot, true); - if (!Bukkit.isPrimaryThread()) { - Iris.warn("LootGenerateEvent was not called on the main thread, please report this issue."); - Thread.dumpStack(); - J.sfut(() -> Bukkit.getPluginManager().callEvent(event)).join(); - } else Bukkit.getPluginManager().callEvent(event); - - return event.isCancelled(); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java b/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java deleted file mode 100644 index 19fa5cd71..000000000 --- a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.gui; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.events.IrisEngineHotloadEvent; -import com.volmit.iris.engine.object.NoiseStyle; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; - -import javax.imageio.ImageIO; -import javax.swing.*; -import java.awt.*; -import java.awt.event.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Supplier; - -public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, Listener { - - private static final long serialVersionUID = 2094606939770332040L; - - static JComboBox combo; - @SuppressWarnings("CanBeFinal") - static boolean hd = false; - static double ascale = 10; - static double oxp = 0; - static double ozp = 0; - static double mxx = 0; - static double mzz = 0; - @SuppressWarnings("CanBeFinal") - static boolean down = false; - @SuppressWarnings("CanBeFinal") - RollingSequence r = new RollingSequence(20); - @SuppressWarnings("CanBeFinal") - boolean colorMode = IrisSettings.get().getGui().colorMode; - double scale = 1; - CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong())); - @SuppressWarnings("CanBeFinal") - MultiBurst gx = MultiBurst.burst; - ReentrantLock l = new ReentrantLock(); - BufferedImage img; - int w = 0; - int h = 0; - Function2 generator; - Supplier> loader; - double ox = 0; //Offset X - double oz = 0; //Offset Y - double mx = 0; - double mz = 0; - double lx = Double.MAX_VALUE; //MouseX - double lz = Double.MAX_VALUE; //MouseY - double t; - double tz; - - public NoiseExplorerGUI() { - Iris.instance.registerListener(this); - addMouseWheelListener(this); - addMouseMotionListener(new MouseMotionListener() { - @Override - public void mouseMoved(MouseEvent e) { - Point cp = e.getPoint(); - - lx = (cp.getX()); - lz = (cp.getY()); - mx = lx; - mz = lz; - } - - @Override - public void mouseDragged(MouseEvent e) { - Point cp = e.getPoint(); - ox += (lx - cp.getX()) * scale; - oz += (lz - cp.getY()) * scale; - lx = cp.getX(); - lz = cp.getY(); - } - }); - } - - private static void createAndShowGUI(Supplier> loader, String genName) { - JFrame frame = new JFrame("Noise Explorer: " + genName); - NoiseExplorerGUI nv = new NoiseExplorerGUI(); - frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); - JLayeredPane pane = new JLayeredPane(); - nv.setSize(new Dimension(1440, 820)); - pane.add(nv, 1, 0); - nv.loader = loader; - nv.generator = loader.get(); - frame.add(pane); - File file = Iris.getCached("Iris Icon", "https://raw.githubusercontent.com/VolmitSoftware/Iris/master/icon.png"); - - if (file != null) { - try { - frame.setIconImage(ImageIO.read(file)); - } catch (IOException e) { - Iris.reportError(e); - } - } - frame.setSize(1440, 820); - frame.setVisible(true); - } - - private static void createAndShowGUI() { - JFrame frame = new JFrame("Noise Explorer"); - NoiseExplorerGUI nv = new NoiseExplorerGUI(); - frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); - KList li = new KList<>(NoiseStyle.values()).toStringList().sort(); - combo = new JComboBox<>(li.toArray(new String[0])); - combo.setSelectedItem("STATIC"); - combo.setFocusable(false); - combo.addActionListener(e -> { - @SuppressWarnings("unchecked") - String b = (String) (((JComboBox) e.getSource()).getSelectedItem()); - NoiseStyle s = NoiseStyle.valueOf(b); - nv.cng = s.create(RNG.r.nextParallelRNG(RNG.r.imax())); - }); - - combo.setSize(500, 30); - JLayeredPane pane = new JLayeredPane(); - nv.setSize(new Dimension(1440, 820)); - pane.add(nv, 1, 0); - pane.add(combo, 2, 0); - frame.add(pane); - File file = Iris.getCached("Iris Icon", "https://raw.githubusercontent.com/VolmitSoftware/Iris/master/icon.png"); - - if (file != null) { - try { - frame.setIconImage(ImageIO.read(file)); - } catch (IOException e) { - Iris.reportError(e); - } - } - frame.setSize(1440, 820); - frame.setVisible(true); - frame.addWindowListener(new java.awt.event.WindowAdapter() { - @Override - public void windowClosing(java.awt.event.WindowEvent windowEvent) { - Iris.instance.unregisterListener(nv); - } - }); - } - - public static void launch(Supplier> gen, String genName) { - EventQueue.invokeLater(() -> createAndShowGUI(gen, genName)); - } - - public static void launch() { - EventQueue.invokeLater(NoiseExplorerGUI::createAndShowGUI); - } - - @EventHandler - public void on(IrisEngineHotloadEvent e) { - if (generator != null) - generator = loader.get(); - } - - public void mouseWheelMoved(MouseWheelEvent e) { - - int notches = e.getWheelRotation(); - if (e.isControlDown()) { - t = t + ((0.0025 * t) * notches); - return; - } - - scale = scale + ((0.044 * scale) * notches); - scale = Math.max(scale, 0.00001); - } - - @Override - public void paint(Graphics g) { - if (scale < ascale) { - ascale -= Math.abs(scale - ascale) * 0.16; - } - - if (scale > ascale) { - ascale += Math.abs(ascale - scale) * 0.16; - } - - if (t < tz) { - tz -= Math.abs(t - tz) * 0.29; - } - - if (t > tz) { - tz += Math.abs(tz - t) * 0.29; - } - - if (ox < oxp) { - oxp -= Math.abs(ox - oxp) * 0.16; - } - - if (ox > oxp) { - oxp += Math.abs(oxp - ox) * 0.16; - } - - if (oz < ozp) { - ozp -= Math.abs(oz - ozp) * 0.16; - } - - if (oz > ozp) { - ozp += Math.abs(ozp - oz) * 0.16; - } - - if (mx < mxx) { - mxx -= Math.abs(mx - mxx) * 0.16; - } - - if (mx > mxx) { - mxx += Math.abs(mxx - mx) * 0.16; - } - - if (mz < mzz) { - mzz -= Math.abs(mz - mzz) * 0.16; - } - - if (mz > mzz) { - mzz += Math.abs(mzz - mz) * 0.16; - } - - PrecisionStopwatch p = PrecisionStopwatch.start(); - int accuracy = hd ? 1 : M.clip((r.getAverage() / 12D), 2D, 128D).intValue(); - accuracy = down ? accuracy * 4 : accuracy; - int v = 1000; - - if (g instanceof Graphics2D gg) { - - if (getParent().getWidth() != w || getParent().getHeight() != h) { - w = getParent().getWidth(); - h = getParent().getHeight(); - img = null; - } - - if (img == null) { - img = new BufferedImage(w / accuracy, h / accuracy, BufferedImage.TYPE_INT_RGB); - } - - BurstExecutor e = gx.burst(w); - - for (int x = 0; x < w / accuracy; x++) { - int xx = x; - - int finalAccuracy = accuracy; - e.queue(() -> { - for (int z = 0; z < h / finalAccuracy; z++) { - double n = generator != null ? generator.apply(((xx * finalAccuracy) * ascale) + oxp, ((z * finalAccuracy) * ascale) + ozp) : cng.noise(((xx * finalAccuracy) * ascale) + oxp, ((z * finalAccuracy) * ascale) + ozp); - n = n > 1 ? 1 : n < 0 ? 0 : n; - - try { - //Color color = colorMode ? Color.getHSBColor((float) (n), 1f - (float) (n * n * n * n * n * n), 1f - (float) n) : Color.getHSBColor(0f, 0f, (float) n); - Color color = colorMode ? Color.getHSBColor((float) (0.666f - n * 0.666f), 1f, (float) (1f - n * 0.8f)) : Color.getHSBColor(0f, 0f, (float) n); - int rgb = color.getRGB(); - img.setRGB(xx, z, rgb); - } catch (Throwable ignored) { - } - } - }); - } - - e.complete(); - gg.drawImage(img, 0, 0, getParent().getWidth() * accuracy, getParent().getHeight() * accuracy, (img, infoflags, x, y, width, height) -> true); - } - - p.end(); - - t += 1D; - r.put(p.getMilliseconds()); - - if (!isVisible()) { - return; - } - - if (!getParent().isVisible()) { - return; - } - - if (!getParent().getParent().isVisible()) { - return; - } - - EventQueue.invokeLater(() -> - { - J.sleep((long) Math.max(0, 32 - r.getAverage())); - repaint(); - }); - } - - static class HandScrollListener extends MouseAdapter { - private static final Point pp = new Point(); - - @Override - public void mouseDragged(MouseEvent e) { - JViewport vport = (JViewport) e.getSource(); - JComponent label = (JComponent) vport.getView(); - Point cp = e.getPoint(); - Point vp = vport.getViewPosition(); - vp.translate(pp.x - cp.x, pp.y - cp.y); - label.scrollRectToVisible(new Rectangle(vp, vport.getSize())); - - pp.setLocation(cp); - } - - @Override - public void mousePressed(MouseEvent e) { - pp.setLocation(e.getPoint()); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java b/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java deleted file mode 100644 index 348b42349..000000000 --- a/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java +++ /dev/null @@ -1,452 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.gui; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.pregenerator.IrisPregenerator; -import com.volmit.iris.core.pregenerator.PregenListener; -import com.volmit.iris.core.pregenerator.PregenTask; -import com.volmit.iris.core.pregenerator.PregeneratorMethod; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.format.MemoryMonitor; -import com.volmit.iris.util.function.Consumer2; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.image.BufferedImage; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Consumer; - -public class PregeneratorJob implements PregenListener { - private static final Color COLOR_EXISTS = parseColor("#4d7d5b"); - private static final Color COLOR_BLACK = parseColor("#4d7d5b"); - private static final Color COLOR_MANTLE = parseColor("#3c2773"); - private static final Color COLOR_GENERATING = parseColor("#66967f"); - private static final Color COLOR_NETWORK = parseColor("#a863c2"); - private static final Color COLOR_NETWORK_GENERATING = parseColor("#836b8c"); - private static final Color COLOR_GENERATED = parseColor("#65c295"); - private static final Color COLOR_CLEANED = parseColor("#34eb93"); - private static final AtomicReference instance = new AtomicReference<>(); - private final MemoryMonitor monitor; - private final PregenTask task; - private final boolean saving; - private final KList> onProgress = new KList<>(); - private final KList whenDone = new KList<>(); - private final IrisPregenerator pregenerator; - private final Position2 min; - private final Position2 max; - private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1)); - private final Engine engine; - private final ExecutorService service; - private JFrame frame; - private PregenRenderer renderer; - private int rgc = 0; - private String[] info; - - public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) { - instance.updateAndGet(old -> { - if (old != null) { - old.pregenerator.close(); - old.close(); - } - return this; - }); - this.engine = engine; - monitor = new MemoryMonitor(50); - saving = false; - info = new String[]{"Initializing..."}; - this.task = task; - this.pregenerator = new IrisPregenerator(task, method, this); - max = new Position2(0, 0); - min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE); - task.iterateAllChunks((xx, zz) -> { - min.setX(Math.min(xx, min.getX())); - min.setZ(Math.min(zz, min.getZ())); - max.setX(Math.max(xx, max.getX())); - max.setZ(Math.max(zz, max.getZ())); - }); - - if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) { - open(); - } - - var t = new Thread(() -> { - J.sleep(1000); - this.pregenerator.start(); - }, "Iris Pregenerator"); - t.setPriority(Thread.MIN_PRIORITY); - t.start(); - service = Executors.newVirtualThreadPerTaskExecutor(); - } - - public static boolean shutdownInstance() { - PregeneratorJob inst = instance.get(); - if (inst == null) { - return false; - } - - J.a(inst.pregenerator::close); - return true; - } - - public static PregeneratorJob getInstance() { - return instance.get(); - } - - public static boolean pauseResume() { - PregeneratorJob inst = instance.get(); - if (inst == null) { - return false; - } - - if (isPaused()) { - inst.pregenerator.resume(); - } else { - inst.pregenerator.pause(); - } - return true; - } - - public static boolean isPaused() { - PregeneratorJob inst = instance.get(); - if (inst == null) { - return true; - } - - return inst.paused(); - } - - private static Color parseColor(String c) { - String v = (c.startsWith("#") ? c : "#" + c).trim(); - try { - return Color.decode(v); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Error Parsing 'color', (" + c + ")"); - } - - return Color.RED; - } - - public Mantle getMantle() { - return pregenerator.getMantle(); - } - - public PregeneratorJob onProgress(Consumer c) { - onProgress.add(c); - return this; - } - - public PregeneratorJob whenDone(Runnable r) { - whenDone.add(r); - return this; - } - - public void drawRegion(int x, int z, Color color) { - J.a(() -> task.iterateChunks(x, z, (xx, zz) -> { - draw(xx, zz, color); - J.sleep(3); - })); - } - - public void draw(int x, int z, Color color) { - try { - if (renderer != null && frame != null && frame.isVisible()) { - renderer.func.accept(new Position2(x, z), color); - } - } catch (Throwable ignored) { - Iris.error("Failed to draw pregen"); - } - } - - public void stop() { - J.a(() -> { - pregenerator.close(); - close(); - instance.compareAndSet(this, null); - }); - } - - public void close() { - J.a(() -> { - try { - monitor.close(); - J.sleep(3000); - frame.setVisible(false); - } catch (Throwable ignored) { - Iris.error("Error closing pregen gui"); - } - }); - } - - public void open() { - J.a(() -> { - try { - frame = new JFrame("Pregen View"); - renderer = new PregenRenderer(); - frame.addKeyListener(renderer); - renderer.l = new ReentrantLock(); - renderer.frame = frame; - renderer.job = this; - renderer.func = (c, b) -> { - renderer.l.lock(); - renderer.order.add(() -> renderer.draw(c, b, renderer.bg)); - renderer.l.unlock(); - }; - frame.add(renderer); - frame.setSize(1000, 1000); - frame.setVisible(true); - } catch (Throwable ignored) { - Iris.error("Error opening pregen gui"); - } - }); - } - - @Override - public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) { - info = new String[]{ - (paused() ? "PAUSED" : (saving ? "Saving... " : "Generating")) + " " + Form.f(generated) + " of " + Form.f(totalChunks) + " (" + Form.pc(percent, 0) + " Complete)", - "Speed: " + (cached ? "Cached " : "") + Form.f(chunksPerSecond, 0) + " Chunks/s, " + Form.f(regionsPerMinute, 1) + " Regions/m, " + Form.f(chunksPerMinute, 0) + " Chunks/m", - Form.duration(eta, 2) + " Remaining " + " (" + Form.duration(elapsed, 2) + " Elapsed)", - "Generation Method: " + method, - "Memory: " + Form.memSize(monitor.getUsedBytes(), 2) + " (" + Form.pc(monitor.getUsagePercent(), 0) + ") Pressure: " + Form.memSize(monitor.getPressure(), 0) + "/s", - - }; - - for (Consumer i : onProgress) { - i.accept(percent); - } - } - - @Override - public void onChunkGenerating(int x, int z) { - draw(x, z, COLOR_GENERATING); - } - - @Override - public void onChunkGenerated(int x, int z, boolean cached) { - if (renderer == null || frame == null || !frame.isVisible()) return; - service.submit(() -> { - if (engine != null) { - draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8)); - return; - } - - draw(x, z, COLOR_GENERATED); - }); - } - - @Override - public void onRegionGenerated(int x, int z) { - shouldGc(); - rgc++; - } - - private void shouldGc() { - if (cl.flip() && rgc > 16) { - System.gc(); - } - } - - @Override - public void onRegionGenerating(int x, int z) { - - } - - @Override - public void onChunkCleaned(int x, int z) { - //draw(x, z, COLOR_CLEANED); - } - - @Override - public void onRegionSkipped(int x, int z) { - - } - - @Override - public void onNetworkStarted(int x, int z) { - drawRegion(x, z, COLOR_NETWORK); - } - - @Override - public void onNetworkFailed(int x, int z) { - - } - - @Override - public void onNetworkReclaim(int revert) { - - } - - @Override - public void onNetworkGeneratedChunk(int x, int z) { - draw(x, z, COLOR_NETWORK_GENERATING); - } - - @Override - public void onNetworkDownloaded(int x, int z) { - drawRegion(x, z, COLOR_NETWORK); - } - - @Override - public void onClose() { - close(); - instance.compareAndSet(this, null); - whenDone.forEach(Runnable::run); - service.shutdownNow(); - } - - @Override - public void onSaving() { - - } - - @Override - public void onChunkExistsInRegionGen(int x, int z) { - if (engine != null) { - draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8)); - return; - } - - draw(x, z, COLOR_EXISTS); - } - - private Position2 getMax() { - return max; - } - - private Position2 getMin() { - return min; - } - - private boolean paused() { - return pregenerator.paused(); - } - - private String[] getProgress() { - return info; - } - - public static class PregenRenderer extends JPanel implements KeyListener { - private static final long serialVersionUID = 2094606939770332040L; - private final KList order = new KList<>(); - private final int res = 512; - private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB); - Graphics2D bg; - private PregeneratorJob job; - private ReentrantLock l; - private Consumer2 func; - private JFrame frame; - - public PregenRenderer() { - - } - - public void paint(int x, int z, Color c) { - func.accept(new Position2(x, z), c); - } - - @Override - public void paint(Graphics gx) { - Graphics2D g = (Graphics2D) gx; - bg = (Graphics2D) image.getGraphics(); - l.lock(); - - while (order.isNotEmpty()) { - try { - order.pop().run(); - } catch (Throwable e) { - Iris.reportError(e); - - } - } - - l.unlock(); - g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true); - g.setColor(Color.WHITE); - g.setFont(new Font("Hevetica", Font.BOLD, 13)); - String[] prog = job.getProgress(); - int h = g.getFontMetrics().getHeight() + 5; - int hh = 20; - - if (job.paused()) { - g.drawString("PAUSED", 20, hh += h); - - g.drawString("Press P to Resume", 20, hh += h); - } else { - for (String i : prog) { - g.drawString(i, 20, hh += h); - } - - g.drawString("Press P to Pause", 20, hh += h); - } - - J.sleep(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250); - repaint(); - } - - private void draw(Position2 p, Color c, Graphics2D bg) { - double pw = M.lerpInverse(job.getMin().getX(), job.getMax().getX(), p.getX()); - double ph = M.lerpInverse(job.getMin().getZ(), job.getMax().getZ(), p.getZ()); - double pwa = M.lerpInverse(job.getMin().getX(), job.getMax().getX(), p.getX() + 1); - double pha = M.lerpInverse(job.getMin().getZ(), job.getMax().getZ(), p.getZ() + 1); - int x = (int) M.lerp(0, res, pw); - int z = (int) M.lerp(0, res, ph); - int xa = (int) M.lerp(0, res, pwa); - int za = (int) M.lerp(0, res, pha); - bg.setColor(c); - bg.fillRect(x, z, xa - x, za - z); - } - - @Override - public void keyTyped(KeyEvent e) { - - } - - @Override - public void keyPressed(KeyEvent e) { - - } - - @Override - public void keyReleased(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_P) { - PregeneratorJob.pauseResume(); - } - } - - public void close() { - frame.setVisible(false); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/gui/VisionGUI.java b/core/src/main/java/com/volmit/iris/core/gui/VisionGUI.java deleted file mode 100644 index 32bc1f79d..000000000 --- a/core/src/main/java/com/volmit/iris/core/gui/VisionGUI.java +++ /dev/null @@ -1,870 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.gui; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.gui.components.IrisRenderer; -import com.volmit.iris.core.gui.components.RenderType; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.IrisComplex; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.engine.object.IrisWorld; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.BlockPosition; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.O; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.bukkit.Location; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; - -import javax.imageio.ImageIO; -import javax.swing.*; -import javax.swing.event.MouseInputListener; -import java.awt.*; -import java.awt.event.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.Locale; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.function.BiFunction; - -import static com.volmit.iris.util.data.registry.Attributes.MAX_HEALTH; - -public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener { - private static final long serialVersionUID = 2094606939770332040L; - private final KList lastEntities = new KList<>(); - private final KMap notifications = new KMap<>(); - private final ChronoLatch centities = new ChronoLatch(1000); - private final RollingSequence rs = new RollingSequence(512); - private final O m = new O<>(); - private final KMap positions = new KMap<>(); - private final KMap fastpositions = new KMap<>(); - private final KSet working = new KSet<>(); - private final KSet workingfast = new KSet<>(); - double tfps = 240D; - int ltc = 3; - private RenderType currentType = RenderType.BIOME; - private boolean help = true; - private boolean helpIgnored = false; - private boolean shift = false; - private Player player = null; - private boolean debug = false; - private boolean control = false; - private boolean eco = false; - private boolean lowtile = false; - private boolean follow = false; - private boolean alt = false; - private IrisRenderer renderer; - private IrisWorld world; - private double velocity = 0; - private int lowq = 12; - private double scale = 128; - private double mscale = 4D; - private int w = 0; - private int h = 0; - private double lx = 0; - private double lz = 0; - private double ox = 0; - private double oz = 0; - private double hx = 0; - private double hz = 0; - private double oxp = 0; - private double ozp = 0; - private Engine engine; - private int tid = 0; - private final ExecutorService e = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> { - tid++; - Thread t = new Thread(r); - t.setName("Iris HD Renderer " + tid); - t.setPriority(Thread.MIN_PRIORITY); - t.setUncaughtExceptionHandler((et, e) -> - { - Iris.info("Exception encountered in " + et.getName()); - e.printStackTrace(); - }); - - return t; - }); - - private final ExecutorService eh = Executors.newFixedThreadPool(ltc, r -> { - tid++; - Thread t = new Thread(r); - t.setName("Iris Renderer " + tid); - t.setPriority(Thread.NORM_PRIORITY); - t.setUncaughtExceptionHandler((et, e) -> - { - Iris.info("Exception encountered in " + et.getName()); - e.printStackTrace(); - }); - - return t; - }); - private BufferedImage texture; - - public VisionGUI(JFrame frame) { - m.set(8); - rs.put(1); - addMouseWheelListener(this); - addMouseMotionListener(this); - addMouseListener(this); - frame.addKeyListener(this); - J.a(() -> { - J.sleep(10000); - - if (!helpIgnored && help) { - help = false; - } - }); - frame.addWindowListener(new java.awt.event.WindowAdapter() { - @Override - public void windowClosing(java.awt.event.WindowEvent windowEvent) { - e.shutdown(); - eh.shutdown(); - } - }); - } - - private static void createAndShowGUI(Engine r, int s, IrisWorld world) { - JFrame frame = new JFrame("Vision"); - VisionGUI nv = new VisionGUI(frame); - nv.world = world; - nv.engine = r; - nv.renderer = new IrisRenderer(r); - frame.add(nv); - frame.setSize(1440, 820); - frame.setVisible(true); - File file = Iris.getCached("Iris Icon", "https://raw.githubusercontent.com/VolmitSoftware/Iris/master/icon.png"); - - if (file != null) { - try { - nv.texture = ImageIO.read(file); - frame.setIconImage(ImageIO.read(file)); - } catch (IOException e) { - Iris.reportError(e); - - } - } - } - - public static void launch(Engine g, int i) { - J.a(() -> - createAndShowGUI(g, i, g.getWorld())); - } - - public boolean updateEngine() { - if (engine.isClosed()) { - if (world.hasRealWorld()) { - try { - engine = IrisToolbelt.access(world.realWorld()).getEngine(); - return !engine.isClosed(); - } catch (Throwable e) { - - } - } - } - - return false; - } - - @Override - public void mouseMoved(MouseEvent e) { - Point cp = e.getPoint(); - lx = (cp.getX()); - lz = (cp.getY()); - } - - @Override - public void mouseDragged(MouseEvent e) { - Point cp = e.getPoint(); - ox += (lx - cp.getX()) * scale; - oz += (lz - cp.getY()) * scale; - lx = cp.getX(); - lz = cp.getY(); - } - - public int getColor(double wx, double wz) { - BiFunction colorFunction = (d, dx) -> Color.black.getRGB(); - - switch (currentType) { - case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> - colorFunction = (x, z) -> engine.getComplex().getTrueBiomeStream().get(x, z).getColor(engine, currentType).getRGB(); - case BIOME_LAND -> - colorFunction = (x, z) -> engine.getComplex().getLandBiomeStream().get(x, z).getColor(engine, currentType).getRGB(); - case BIOME_SEA -> - colorFunction = (x, z) -> engine.getComplex().getSeaBiomeStream().get(x, z).getColor(engine, currentType).getRGB(); - case REGION -> - colorFunction = (x, z) -> engine.getComplex().getRegionStream().get(x, z).getColor(engine.getComplex(), currentType).getRGB(); - case CAVE_LAND -> - colorFunction = (x, z) -> engine.getComplex().getCaveBiomeStream().get(x, z).getColor(engine, currentType).getRGB(); - case HEIGHT -> - colorFunction = (x, z) -> Color.getHSBColor(engine.getComplex().getHeightStream().get(x, z).floatValue(), 100, 100).getRGB(); - } - - return colorFunction.apply(wx, wz); - } - - public void notify(String s) { - notifications.put(s, M.ms() + 2500); - } - - @Override - public void keyTyped(KeyEvent e) { - - } - - @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_SHIFT) { - shift = true; - } - if (e.getKeyCode() == KeyEvent.VK_CONTROL) { - control = true; - } - if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) { - debug = true; - } - if (e.getKeyCode() == KeyEvent.VK_SLASH) { - help = true; - helpIgnored = true; - } - if (e.getKeyCode() == KeyEvent.VK_ALT) { - alt = true; - } - } - - @Override - public void keyReleased(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) { - debug = false; - } - if (e.getKeyCode() == KeyEvent.VK_SHIFT) { - shift = false; - } - - if (e.getKeyCode() == KeyEvent.VK_CONTROL) { - control = false; - } - if (e.getKeyCode() == KeyEvent.VK_SLASH) { - help = false; - helpIgnored = true; - } - if (e.getKeyCode() == KeyEvent.VK_ALT) { - alt = false; - } - - // Pushes - if (e.getKeyCode() == KeyEvent.VK_F) { - follow = !follow; - - if (player != null && follow) { - notify("Following " + player.getName() + ". Press F to disable"); - } else if (follow) { - notify("Can't follow, no one is in the world"); - follow = false; - } else { - notify("Follow Off"); - } - - return; - } - - if (e.getKeyCode() == KeyEvent.VK_R) { - dump(); - notify("Refreshing Chunks"); - return; - } - - if (e.getKeyCode() == KeyEvent.VK_P) { - lowtile = !lowtile; - dump(); - notify("Rendering " + (lowtile ? "Low" : "High") + " Quality Tiles"); - return; - } - if (e.getKeyCode() == KeyEvent.VK_E) { - eco = !eco; - dump(); - notify("Using " + (eco ? "60" : "Uncapped") + " FPS Limit"); - return; - } - if (e.getKeyCode() == KeyEvent.VK_EQUALS) { - mscale = mscale + ((0.044 * mscale) * -3); - mscale = Math.max(mscale, 0.00001); - dump(); - return; - } - if (e.getKeyCode() == KeyEvent.VK_MINUS) { - mscale = mscale + ((0.044 * mscale) * 3); - mscale = Math.max(mscale, 0.00001); - dump(); - return; - } - - if (e.getKeyCode() == KeyEvent.VK_BACK_SLASH) { - mscale = 1D; - dump(); - notify("Zoom Reset"); - return; - } - - int currentMode = currentType.ordinal(); - - for (RenderType i : RenderType.values()) { - if (e.getKeyChar() == String.valueOf(i.ordinal() + 1).charAt(0)) { - if (i.ordinal() != currentMode) { - currentType = i; - dump(); - notify("Rendering " + Form.capitalizeWords(currentType.name().toLowerCase().replaceAll("\\Q_\\E", " "))); - return; - } - } - } - - if (e.getKeyCode() == KeyEvent.VK_M) { - currentType = RenderType.values()[(currentMode + 1) % RenderType.values().length]; - notify("Rendering " + Form.capitalizeWords(currentType.name().toLowerCase().replaceAll("\\Q_\\E", " "))); - dump(); - } - } - - private void dump() { - positions.clear(); - fastpositions.clear(); - } - - public BufferedImage getTile(KSet fg, int div, int x, int z, O m) { - BlockPosition key = new BlockPosition((int) mscale, Math.floorDiv(x, div), Math.floorDiv(z, div)); - fg.add(key); - - if (positions.containsKey(key)) { - return positions.get(key); - } - - if (fastpositions.containsKey(key)) { - if (!working.contains(key) && working.size() < 9) { - m.set(m.get() - 1); - - if (m.get() >= 0 && velocity < 50) { - working.add(key); - double mk = mscale; - double mkd = scale; - e.submit(() -> - { - PrecisionStopwatch ps = PrecisionStopwatch.start(); - BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / (lowtile ? 3 : 1), currentType); - rs.put(ps.getMilliseconds()); - working.remove(key); - - if (mk == mscale && mkd == scale) { - positions.put(key, b); - } - }); - } - } - - return fastpositions.get(key); - } - - if (workingfast.contains(key) || workingfast.size() > Runtime.getRuntime().availableProcessors()) { - return null; - } - - workingfast.add(key); - double mk = mscale; - double mkd = scale; - eh.submit(() -> - { - PrecisionStopwatch ps = PrecisionStopwatch.start(); - BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / lowq, currentType); - rs.put(ps.getMilliseconds()); - workingfast.remove(key); - - if (mk == mscale && mkd == scale) { - fastpositions.put(key, b); - } - }); - return null; - } - - private double getWorldX(double screenX) { - return (mscale * screenX) + ((oxp / scale) * mscale); - } - - private double getWorldZ(double screenZ) { - return (mscale * screenZ) + ((ozp / scale) * mscale); - } - - private double getScreenX(double x) { - return (x / mscale) - ((oxp / scale)); - } - - private double getScreenZ(double z) { - return (z / mscale) - ((ozp / scale)); - } - - @Override - public void paint(Graphics gx) { - - if (engine.isClosed()) { - EventQueue.invokeLater(() -> { - try { - setVisible(false); - } catch (Throwable e) { - - } - }); - - return; - } - - if (updateEngine()) { - dump(); - } - - if (ox < oxp) { - velocity = Math.abs(ox - oxp) * 0.36; - oxp -= velocity; - } - - if (ox > oxp) { - velocity = Math.abs(oxp - ox) * 0.36; - oxp += velocity; - } - - if (oz < ozp) { - velocity = Math.abs(oz - ozp) * 0.36; - ozp -= velocity; - } - - if (oz > ozp) { - velocity = Math.abs(ozp - oz) * 0.36; - ozp += velocity; - } - - if (lx < hx) { - hx -= Math.abs(lx - hx) * 0.36; - } - - if (lx > hx) { - hx += Math.abs(hx - lx) * 0.36; - } - - if (lz < hz) { - hz -= Math.abs(lz - hz) * 0.36; - } - - if (lz > hz) { - hz += Math.abs(hz - lz) * 0.36; - } - - if (Math.abs(lx - hx) < 0.5) { - hx = lx; - } - if (Math.abs(lz - hz) < 0.5) { - hz = lz; - } - - if (centities.flip()) { - J.s(() -> { - synchronized (lastEntities) { - lastEntities.clear(); - lastEntities.addAll(world.getEntitiesByClass(LivingEntity.class)); - } - }); - } - lowq = Math.max(Math.min((int) M.lerp(8, 28, velocity / 1000D), 28), 8); - PrecisionStopwatch p = PrecisionStopwatch.start(); - Graphics2D g = (Graphics2D) gx; - w = getWidth(); - h = getHeight(); - double vscale = scale; - scale = w / 12D; - - if (scale != vscale) { - positions.clear(); - } - - KSet gg = new KSet<>(); - int iscale = (int) scale; - g.setColor(Color.white); - g.clearRect(0, 0, w, h); - double offsetX = oxp / scale; - double offsetZ = ozp / scale; - m.set(3); - - for (int r = 0; r < Math.max(w, h); r += iscale) { - for (int i = -iscale; i < w + iscale; i += iscale) { - for (int j = -iscale; j < h + iscale; j += iscale) { - int a = i - (w / 2); - int b = j - (h / 2); - if (a * a + b * b <= r * r) { - int tx = (int) (Math.floor((offsetX + i) / iscale) * iscale); - int tz = (int) (Math.floor((offsetZ + j) / iscale) * iscale); - BufferedImage t = getTile(gg, iscale, tx, tz, m); - - if (t != null) { - int rx = Math.floorMod((int) Math.floor(offsetX), iscale); - int rz = Math.floorMod((int) Math.floor(offsetZ), iscale); - g.drawImage(t, i - rx, j - rz, iscale, iscale, (img, infoflags, x, y, width, height) -> true); - } - } - } - } - } - - p.end(); - - for (BlockPosition i : positions.k()) { - if (!gg.contains(i)) { - positions.remove(i); - } - } - - hanleFollow(); - renderOverlays(g); - - if (!isVisible()) { - return; - } - - if (!getParent().isVisible()) { - return; - } - - if (!getParent().getParent().isVisible()) { - return; - } - - J.a(() -> - { - J.sleep(eco ? 15 : 1); - repaint(); - }); - } - - private void hanleFollow() { - if (follow && player != null) { - animateTo(player.getLocation().getX(), player.getLocation().getZ()); - } - } - - private void renderOverlays(Graphics2D g) { - renderPlayer(g); - - if (help) { - renderOverlayHelp(g); - } else if (debug) { - renderOverlayDebug(g); - } - - renderOverlayLegend(g); - - renderHoverOverlay(g, shift); - if (!notifications.isEmpty()) { - renderNotification(g); - } - } - - private void renderOverlayLegend(Graphics2D g) { - KList l = new KList<>(); - l.add("Zoom: " + Form.pc(mscale, 0)); - l.add("Blocks: " + Form.f((int) mscale * w) + " by " + Form.f((int) mscale * h)); - l.add("BPP: " + Form.f(mscale, 1)); - l.add("Render Mode: " + Form.capitalizeWords(currentType.name().toLowerCase().replaceAll("\\Q_\\E", " "))); - - drawCardBR(g, l); - } - - private void renderNotification(Graphics2D g) { - drawCardCB(g, notifications.k()); - - for (String i : notifications.k()) { - if (M.ms() > notifications.get(i)) { - notifications.remove(i); - } - } - } - - private void renderPlayer(Graphics2D g) { - Player b = null; - - for (Player i : world.getPlayers()) { - b = i; - renderPosition(g, i.getLocation().getX(), i.getLocation().getZ()); - } - - synchronized (lastEntities) { - double dist = Double.MAX_VALUE; - LivingEntity h = null; - - for (LivingEntity i : lastEntities) { - if (i instanceof Player) { - continue; - } - - renderMobPosition(g, i, i.getLocation().getX(), i.getLocation().getZ()); - if (shift) { - double d = i.getLocation().distanceSquared(new Location(i.getWorld(), getWorldX(hx), i.getLocation().getY(), getWorldZ(hz))); - - if (d < dist) { - dist = d; - h = i; - } - } - } - - if (h != null && shift) { - g.setColor(Color.red); - g.fillRoundRect((int) getScreenX(h.getLocation().getX()) - 10, (int) getScreenZ(h.getLocation().getZ()) - 10, 20, 20, 20, 20); - KList k = new KList<>(); - k.add(Form.capitalizeWords(h.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")) + h.getEntityId()); - - k.add("Pos: " + h.getLocation().getBlockX() + ", " + h.getLocation().getBlockY() + ", " + h.getLocation().getBlockZ()); - k.add("UUID: " + h.getUniqueId()); - k.add("HP: " + h.getHealth() + " / " + h.getAttribute(MAX_HEALTH).getValue()); - - drawCardTR(g, k); - } - } - - player = b; - } - - private void animateTo(double wx, double wz) { - double cx = getWorldX(getWidth() / 2); - double cz = getWorldZ(getHeight() / 2); - ox += ((wx - cx) / mscale) * scale; - oz += ((wz - cz) / mscale) * scale; - } - - private void renderPosition(Graphics2D g, double x, double z) { - if (texture != null) { - g.drawImage(texture, (int) getScreenX(x), (int) getScreenZ(z), 66, 66, (img, infoflags, xx, xy, width, height) -> true); - } else { - g.setColor(Color.darkGray); - g.fillRoundRect((int) getScreenX(x) - 15, (int) getScreenZ(z) - 15, 30, 30, 15, 15); - g.setColor(Color.cyan.darker().darker()); - g.fillRoundRect((int) getScreenX(x) - 10, (int) getScreenZ(z) - 10, 20, 20, 10, 10); - } - } - - private void renderMobPosition(Graphics2D g, LivingEntity e, double x, double z) { - g.setColor(Color.red.darker().darker()); - g.fillRoundRect((int) getScreenX(x) - 2, (int) getScreenZ(z) - 2, 4, 4, 4, 4); - } - - private void renderHoverOverlay(Graphics2D g, boolean detailed) { - IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)); - IrisRegion region = engine.getComplex().getRegionStream().get(getWorldX(hx), getWorldZ(hz)); - KList l = new KList<>(); - l.add("Biome: " + biome.getName()); - l.add("Region: " + region.getName() + "(" + region.getLoadKey() + ")"); - l.add("Block " + (int) getWorldX(hx) + ", " + (int) getWorldZ(hz)); - if (detailed) { - l.add("Chunk " + ((int) getWorldX(hx) >> 4) + ", " + ((int) getWorldZ(hz) >> 4)); - l.add("Region " + (((int) getWorldX(hx) >> 4) >> 5) + ", " + (((int) getWorldZ(hz) >> 4) >> 5)); - l.add("Key: " + biome.getLoadKey()); - l.add("File: " + biome.getLoadFile()); - } - - drawCardAt((float) hx, (float) hz, 0, 0, g, l); - } - - private void renderOverlayDebug(Graphics2D g) { - KList l = new KList<>(); - l.add("Velocity: " + (int) velocity); - l.add("Center Pos: " + Form.f((int) getWorldX(getWidth() / 2)) + ", " + Form.f((int) getWorldZ(getHeight() / 2))); - drawCardBL(g, l); - } - - private void renderOverlayHelp(Graphics2D g) { - KList l = new KList<>(); - l.add("/ to show this help screen"); - l.add("R to repaint the screen"); - l.add("F to follow first player"); - l.add("+/- to Change Zoom"); - l.add("\\ to reset zoom to 1"); - l.add("M to cycle render modes"); - l.add("P to toggle Tile Quality Mode"); - l.add("E to toggle Eco FPS Mode"); - - int ff = 0; - for (RenderType i : RenderType.values()) { - ff++; - l.add(ff + " to view " + Form.capitalizeWords(i.name().toLowerCase().replaceAll("\\Q_\\E", " "))); - } - - l.add("Shift for additional biome details (at cursor)"); - l.add("CTRL + Click to teleport to location"); - l.add("ALT + Click to open biome in VSCode"); - drawCardTL(g, l); - } - - private void drawCardTL(Graphics2D g, KList text) { - drawCardAt(0, 0, 0, 0, g, text); - } - - private void drawCardBR(Graphics2D g, KList text) { - drawCardAt(getWidth(), getHeight(), 1, 1, g, text); - } - - private void drawCardBL(Graphics2D g, KList text) { - drawCardAt(0, getHeight(), 0, 1, g, text); - } - - private void drawCardTR(Graphics2D g, KList text) { - drawCardAt(getWidth(), 0, 1, 0, g, text); - } - - private void open() { - IrisComplex complex = engine.getComplex(); - File r = null; - switch (currentType) { - case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT -> - r = complex.getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); - case BIOME_LAND -> r = complex.getLandBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); - case BIOME_SEA -> r = complex.getSeaBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); - case REGION -> r = complex.getRegionStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); - case CAVE_LAND -> r = complex.getCaveBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); - } - - notify("Opening " + r.getPath() + " in VSCode"); - } - - private void teleport() { - J.s(() -> { - if (player != null) { - int xx = (int) getWorldX(hx); - int zz = (int) getWorldZ(hz); - int h = engine.getComplex().getRoundedHeighteightStream().get(xx, zz); - player.teleport(new Location(player.getWorld(), xx, h, zz)); - notify("Teleporting to " + xx + ", " + h + ", " + zz); - } else { - notify("No player in world, can't teleport."); - } - }); - } - - private void drawCardCB(Graphics2D g, KList text) { - drawCardAt(getWidth() / 2, getHeight(), 0.5, 1, g, text); - } - - private void drawCardCT(Graphics2D g, KList text) { - drawCardAt(getWidth() / 2, 0, 0.5, 0, g, text); - } - - private void drawCardAt(float x, float y, double pushX, double pushZ, Graphics2D g, KList text) { - g.setFont(new Font("Hevetica", Font.BOLD, 16)); - int h = 0; - int w = 0; - - for (String i : text) { - h += g.getFontMetrics().getHeight(); - w = Math.max(w, g.getFontMetrics().stringWidth(i)); - } - - w += 28; - h += 14; - - int cw = (int) ((w + 26) * pushX); - int ch = (int) ((h + 26) * pushZ); - - g.setColor(Color.darkGray); - g.fillRect((int) x + 7 + 2 - cw, (int) y + 12 + 2 - ch, w + 7, h); // Shadow - g.setColor(Color.gray); - g.fillRect((int) x + 7 + 1 - cw, (int) y + 12 + 1 - ch, w + 7, h); // Shadow - g.setColor(Color.white); - g.fillRect((int) x + 7 - cw, (int) y + 12 - ch, w + 7, h); - - g.setColor(Color.black); - int m = 0; - for (String i : text) { - g.drawString(i, x + 14 - cw, y + 14 - ch + (++m * g.getFontMetrics().getHeight())); - } - } - - public void mouseWheelMoved(MouseWheelEvent e) { - int notches = e.getWheelRotation(); - if (e.isControlDown()) { - return; - } - - double m0 = mscale; - double m1 = m0 + ((0.25 * m0) * notches); - m1 = Math.max(m1, 0.00001); - if (m1 == m0) { - return; - } - - positions.clear(); - fastpositions.clear(); - - Point p = e.getPoint(); - double sx = p.getX(); - double sz = p.getY(); - - double newOxp = scale * ((m0 / m1) * (sx + (oxp / scale)) - sx); - double newOzp = scale * ((m0 / m1) * (sz + (ozp / scale)) - sz); - - mscale = m1; - oxp = newOxp; - ozp = newOzp; - ox = oxp; - oz = ozp; - } - - @Override - public void mouseClicked(MouseEvent e) { - if (control) { - teleport(); - } else if (alt) { - open(); - } - } - - @Override - public void mousePressed(MouseEvent e) { - - } - - @Override - public void mouseReleased(MouseEvent e) { - - } - - @Override - public void mouseEntered(MouseEvent e) { - - } - - @Override - public void mouseExited(MouseEvent e) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/core/gui/components/IrisRenderer.java b/core/src/main/java/com/volmit/iris/core/gui/components/IrisRenderer.java deleted file mode 100644 index 2820b8999..000000000 --- a/core/src/main/java/com/volmit/iris/core/gui/components/IrisRenderer.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.gui.components; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisBiomeGeneratorLink; -import com.volmit.iris.util.interpolation.IrisInterpolation; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.function.BiFunction; - -public class IrisRenderer { - private final Engine renderer; - - public IrisRenderer(Engine renderer) { - this.renderer = renderer; - } - - public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) { - BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB); - BiFunction colorFunction = (d, dx) -> Color.black.getRGB(); - - switch (currentType) { - case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> - colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); - case BIOME_LAND -> - colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); - case BIOME_SEA -> - colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); - case REGION -> - colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB(); - case CAVE_LAND -> - colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); - case HEIGHT -> - colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 100, 100).getRGB(); - case CONTINENT -> colorFunction = (x, z) -> { - IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z)); - IrisBiomeGeneratorLink g = b.getGenerators().get(0); - Color c; - if (g.getMax() <= 0) { - // Max is below water level, so it is most likely an ocean biome - c = Color.BLUE; - } else if (g.getMin() < 0) { - // Min is below water level, but max is not, so it is most likely a shore biome - c = Color.YELLOW; - } else { - // Both min and max are above water level, so it is most likely a land biome - c = Color.GREEN; - } - return c.getRGB(); - }; - } - - double x, z; - int i, j; - for (i = 0; i < resolution; i++) { - x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) (resolution)); - - for (j = 0; j < resolution; j++) { - z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) (resolution)); - image.setRGB(i, j, colorFunction.apply(x, z)); - } - } - - return image; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/gui/components/TileRender.java b/core/src/main/java/com/volmit/iris/core/gui/components/TileRender.java deleted file mode 100644 index 54fa5481d..000000000 --- a/core/src/main/java/com/volmit/iris/core/gui/components/TileRender.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.gui.components; - -import lombok.Builder; -import lombok.Data; - -import java.awt.image.BufferedImage; - -@Builder -@Data -public class TileRender { - private BufferedImage image; - private int quality; -} diff --git a/core/src/main/java/com/volmit/iris/core/link/CustomItemsDataProvider.java b/core/src/main/java/com/volmit/iris/core/link/CustomItemsDataProvider.java deleted file mode 100644 index 216e94e91..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/CustomItemsDataProvider.java +++ /dev/null @@ -1,102 +0,0 @@ -//package com.volmit.iris.core.link; -// -//import com.jojodmo.customitems.api.CustomItemsAPI; -//import com.jojodmo.customitems.item.custom.CustomItem; -//import com.jojodmo.customitems.item.custom.block.CustomMushroomBlock; -//import com.jojodmo.customitems.version.SafeMaterial; -//import com.volmit.iris.util.collection.KList; -//import com.volmit.iris.util.reflect.WrappedField; -//import com.volmit.iris.util.reflect.WrappedReturningMethod; -//import org.bukkit.block.BlockFace; -//import org.bukkit.block.data.BlockData; -//import org.bukkit.block.data.MultipleFacing; -//import org.bukkit.inventory.ItemStack; -// -//import java.util.Map; -//import java.util.MissingResourceException; -// -//public class CustomItemsDataProvider extends ExternalDataProvider { -// -// private static final String FIELD_FACES = "faces"; -// private static final String METHOD_GET_MATERIAL = "getMaterial"; -// -// private WrappedField> mushroomFaces; -// private WrappedReturningMethod mushroomMaterial; -// -// public CustomItemsDataProvider() { -// super("CustomItems"); -// } -// -// @Override -// public void init() { -// this.mushroomFaces = new WrappedField<>(CustomMushroomBlock.class, FIELD_FACES); -// this.mushroomMaterial = new WrappedReturningMethod<>(CustomMushroomBlock.class, METHOD_GET_MATERIAL); -// } -// -// @Override -// public BlockData getBlockData(Identifier blockId) throws MissingResourceException { -// CustomItem item = CustomItem.get(blockId.key()); -// if(item == null) { -// throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); -// } else if(item.getBlockTexture().isSpawner()) { -// throw new MissingResourceException("Iris does not yet support SpawnerBlocks from CustomItems.", blockId.namespace(), blockId.key()); -// } else if(item.getBlockTexture() != null && item.getBlockTexture().isValid()) { -// throw new MissingResourceException("Tried to fetch BlockData for a CustomItem that is not placeable!", blockId.namespace(), blockId.key()); -// } -// return getMushroomData(item); -// } -// -// @Override -// public ItemStack getItemStack(Identifier itemId) throws MissingResourceException { -// ItemStack stack = CustomItemsAPI.getCustomItem(itemId.key()); -// if(stack == null) { -// throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); -// } -// return stack; -// } -// -// @Override -// public Identifier[] getBlockTypes() { -// KList names = new KList<>(); -// for (String name : CustomItemsAPI.listBlockCustomItemIDs()) { -// try { -// Identifier key = new Identifier("cui", name); -// if (getItemStack(key) != null) -// names.add(key); -// } catch (MissingResourceException ignored) { } -// } -// -// return names.toArray(new Identifier[0]); -// } -// -// @Override -// public Identifier[] getItemTypes() { -// KList names = new KList<>(); -// for (String name : CustomItemsAPI.listCustomItemIDs()) { -// try { -// Identifier key = new Identifier("cui", name); -// if (getItemStack(key) != null) -// names.add(key); -// } catch (MissingResourceException ignored) { } -// } -// -// return names.toArray(new Identifier[0]); -// } -// -// @Override -// public boolean isValidProvider(Identifier key, boolean isItem) { -// return key.namespace().equalsIgnoreCase("cui"); -// } -// -// private BlockData getMushroomData(CustomItem item) { -// MultipleFacing data = (MultipleFacing)mushroomMaterial.invoke(item.getBlockTexture().getMushroomId()).parseMaterial().createBlockData(); -// boolean[] values = mushroomFaces.get().get(item.getBlockTexture().getMushroomId()); -// data.setFace(BlockFace.DOWN, values[0]); -// data.setFace(BlockFace.EAST, values[1]); -// data.setFace(BlockFace.NORTH, values[2]); -// data.setFace(BlockFace.SOUTH, values[3]); -// data.setFace(BlockFace.UP, values[4]); -// data.setFace(BlockFace.WEST, values[5]); -// return data; -// } -//} diff --git a/core/src/main/java/com/volmit/iris/core/link/Identifier.java b/core/src/main/java/com/volmit/iris/core/link/Identifier.java deleted file mode 100644 index 3969b174b..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/Identifier.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.volmit.iris.core.link; - -import org.bukkit.NamespacedKey; - -public record Identifier(String namespace, String key) { - - private static final String DEFAULT_NAMESPACE = "minecraft"; - - public static Identifier fromNamespacedKey(NamespacedKey key) { - return new Identifier(key.getNamespace(), key.getKey()); - } - - public static Identifier fromString(String id) { - String[] strings = id.split(":", 2); - if (strings.length == 1) { - return new Identifier(DEFAULT_NAMESPACE, strings[0]); - } else { - return new Identifier(strings[0], strings[1]); - } - } - - @Override - public String toString() { - return namespace + ":" + key; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof Identifier i) { - return i.namespace().equals(this.namespace) && i.key().equals(this.key); - } else if (obj instanceof NamespacedKey i) { - return i.getNamespace().equals(this.namespace) && i.getKey().equals(this.key); - } else { - return false; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/link/IrisPapiExpansion.java b/core/src/main/java/com/volmit/iris/core/link/IrisPapiExpansion.java deleted file mode 100644 index 666573f14..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/IrisPapiExpansion.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.link; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import me.clip.placeholderapi.expansion.PlaceholderExpansion; -import org.bukkit.Location; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -// See/update https://app.gitbook.com/@volmitsoftware/s/iris/compatability/papi/ -public class IrisPapiExpansion extends PlaceholderExpansion { - @Override - public @NotNull String getIdentifier() { - return "iris"; - } - - @Override - public @NotNull String getAuthor() { - return "Volmit Software"; - } - - @Override - public @NotNull String getVersion() { - return Iris.instance.getDescription().getVersion(); - } - - @Override - public boolean persist() { - return true; - } - - @Override - public String onRequest(OfflinePlayer player, String p) { - Location l = null; - PlatformChunkGenerator a = null; - - if (player.isOnline() && player.getPlayer() != null) { - l = player.getPlayer().getLocation().add(0, 2, 0); - a = IrisToolbelt.access(l.getWorld()); - } - - if (p.equalsIgnoreCase("biome_name")) { - if (a != null) { - return getBiome(a, l).getName(); - } - } else if (p.equalsIgnoreCase("biome_id")) { - if (a != null) { - return getBiome(a, l).getLoadKey(); - } - } else if (p.equalsIgnoreCase("biome_file")) { - if (a != null) { - return getBiome(a, l).getLoadFile().getPath(); - } - } else if (p.equalsIgnoreCase("region_name")) { - if (a != null) { - return a.getEngine().getRegion(l).getName(); - } - } else if (p.equalsIgnoreCase("region_id")) { - if (a != null) { - return a.getEngine().getRegion(l).getLoadKey(); - } - } else if (p.equalsIgnoreCase("region_file")) { - if (a != null) { - return a.getEngine().getRegion(l).getLoadFile().getPath(); - } - } else if (p.equalsIgnoreCase("terrain_slope")) { - if (a != null) { - return (a.getEngine()) - .getComplex().getSlopeStream() - .get(l.getX(), l.getZ()) + ""; - } - } else if (p.equalsIgnoreCase("terrain_height")) { - if (a != null) { - return Math.round(a.getEngine().getHeight(l.getBlockX(), l.getBlockZ())) + ""; - } - } else if (p.equalsIgnoreCase("world_mode")) { - if (a != null) { - return a.isStudio() ? "Studio" : "Production"; - } - } else if (p.equalsIgnoreCase("world_seed")) { - if (a != null) { - return a.getEngine().getSeedManager().getSeed() + ""; - } - } else if (p.equalsIgnoreCase("world_speed")) { - if (a != null) { - return a.getEngine().getGeneratedPerSecond() + "/s"; - } - } - - return null; - } - - private IrisBiome getBiome(PlatformChunkGenerator a, Location l) { - return a.getEngine().getBiome(l.getBlockX(), l.getBlockY() - l.getWorld().getMinHeight(), l.getBlockZ()); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/link/MultiverseCoreLink.java b/core/src/main/java/com/volmit/iris/core/link/MultiverseCoreLink.java deleted file mode 100644 index 920abeb5c..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/MultiverseCoreLink.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.link; - -import lombok.SneakyThrows; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.mvplugins.multiverse.core.MultiverseCoreApi; -import org.mvplugins.multiverse.core.world.MultiverseWorld; -import org.mvplugins.multiverse.core.world.WorldManager; -import org.mvplugins.multiverse.core.world.options.ImportWorldOptions; - -public class MultiverseCoreLink { - private final boolean active; - - public MultiverseCoreLink() { - active = Bukkit.getPluginManager().getPlugin("Multiverse-Core") != null; - } - - public void removeFromConfig(World world) { - removeFromConfig(world.getName()); - } - - public void removeFromConfig(String world) { - if (!active) return; - var manager = worldManager(); - manager.removeWorld(world).onSuccess(manager::saveWorldsConfig); - } - - @SneakyThrows - public void updateWorld(World bukkitWorld, String pack) { - if (!active) return; - var generator = "Iris:" + pack; - var manager = worldManager(); - var world = manager.getWorld(bukkitWorld).getOrElse(() -> { - var options = ImportWorldOptions.worldName(bukkitWorld.getName()) - .generator(generator) - .environment(bukkitWorld.getEnvironment()) - .useSpawnAdjust(false); - return manager.importWorld(options).get(); - }); - - world.setAutoLoad(false); - if (!generator.equals(world.getGenerator())) { - var field = MultiverseWorld.class.getDeclaredField("worldConfig"); - field.setAccessible(true); - - var config = field.get(world); - config.getClass() - .getDeclaredMethod("setGenerator", String.class) - .invoke(config, generator); - } - - manager.saveWorldsConfig(); - } - - private WorldManager worldManager() { - var api = MultiverseCoreApi.get(); - return api.getWorldManager(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/link/WorldEditLink.java b/core/src/main/java/com/volmit/iris/core/link/WorldEditLink.java deleted file mode 100644 index 638c66cc2..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/WorldEditLink.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.volmit.iris.core.link; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.util.data.Cuboid; -import com.volmit.iris.util.data.KCache; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.entity.Player; - -import java.lang.reflect.InvocationTargetException; -import java.time.Duration; -import java.util.UUID; - -public class WorldEditLink { - private static final AtomicCache active = new AtomicCache<>(); - - public static Cuboid getSelection(Player p) { - if (!hasWorldEdit()) - return null; - - try { - Object instance = Class.forName("com.sk89q.worldedit.WorldEdit").getDeclaredMethod("getInstance").invoke(null); - Object sessionManager = instance.getClass().getDeclaredMethod("getSessionManager").invoke(instance); - Class bukkitAdapter = Class.forName("com.sk89q.worldedit.bukkit.BukkitAdapter"); - Object world = bukkitAdapter.getDeclaredMethod("adapt", World.class).invoke(null, p.getWorld()); - Object player = bukkitAdapter.getDeclaredMethod("adapt", Player.class).invoke(null, p); - Object localSession = sessionManager.getClass().getDeclaredMethod("getIfPresent", Class.forName("com.sk89q.worldedit.session.SessionOwner")).invoke(sessionManager, player); - if (localSession == null) return null; - - Object region = null; - try { - region = localSession.getClass().getDeclaredMethod("getSelection", Class.forName("com.sk89q.worldedit.world.World")).invoke(localSession, world); - } catch (InvocationTargetException ignored) {} - if (region == null) return null; - - Object min = region.getClass().getDeclaredMethod("getMinimumPoint").invoke(region); - Object max = region.getClass().getDeclaredMethod("getMaximumPoint").invoke(region); - return new Cuboid(p.getWorld(), - (int) min.getClass().getDeclaredMethod("x").invoke(min), - (int) min.getClass().getDeclaredMethod("y").invoke(min), - (int) min.getClass().getDeclaredMethod("z").invoke(min), - (int) min.getClass().getDeclaredMethod("x").invoke(max), - (int) min.getClass().getDeclaredMethod("y").invoke(max), - (int) min.getClass().getDeclaredMethod("z").invoke(max) - ); - } catch (Throwable e) { - Iris.error("Could not get selection"); - e.printStackTrace(); - Iris.reportError(e); - active.reset(); - active.aquire(() -> false); - } - return null; - } - - public static boolean hasWorldEdit() { - return active.aquire(() -> Bukkit.getPluginManager().isPluginEnabled("WorldEdit")); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/link/data/CraftEngineDataProvider.java b/core/src/main/java/com/volmit/iris/core/link/data/CraftEngineDataProvider.java deleted file mode 100644 index b9650a33e..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/data/CraftEngineDataProvider.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.volmit.iris.core.link.data; - -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.nms.container.BlockProperty; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.math.RNG; -import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks; -import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture; -import net.momirealms.craftengine.bukkit.api.CraftEngineItems; -import net.momirealms.craftengine.core.block.ImmutableBlockState; -import net.momirealms.craftengine.core.block.properties.BooleanProperty; -import net.momirealms.craftengine.core.block.properties.IntegerProperty; -import net.momirealms.craftengine.core.block.properties.Property; -import net.momirealms.craftengine.core.util.Key; -import org.bukkit.Location; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.function.Function; -import java.util.stream.Stream; - -public class CraftEngineDataProvider extends ExternalDataProvider { - - public CraftEngineDataProvider() { - super("CraftEngine"); - } - - private static final BlockProperty[] FURNITURE_PROPERTIES = new BlockProperty[] { - BlockProperty.ofBoolean("randomYaw", false), - BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true), - BlockProperty.ofBoolean("randomPitch", false), - BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true), - }; - - @Override - public void init() { - } - - @Override - public @NotNull List getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException { - var block = CraftEngineBlocks.byId(Key.of(blockId.namespace(), blockId.key())); - if (block != null) return block.properties() - .stream() - .map(CraftEngineDataProvider::convert) - .toList(); - var furniture = CraftEngineFurniture.byId(Key.of(blockId.namespace(), blockId.key())); - if (furniture != null) { - BlockProperty[] properties = Arrays.copyOf(FURNITURE_PROPERTIES, 5); - properties[4] = new BlockProperty( - "variant", - String.class, - furniture.anyVariantName(), - furniture.variants().keySet(), - Function.identity() - ); - return List.of(properties); - } - throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); - } - - @Override - public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) throws MissingResourceException { - var item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key())); - if (item == null) throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); - return item.buildItemStack(); - } - - @Override - public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap state) throws MissingResourceException { - var key = Key.of(blockId.namespace(), blockId.key()); - if (CraftEngineBlocks.byId(key) == null && CraftEngineFurniture.byId(key) == null) - throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); - return IrisCustomData.of(B.getAir(), ExternalDataSVC.buildState(blockId, state)); - } - - @Override - public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) { - var pair = ExternalDataSVC.parseState(blockId); - var key = Key.of(blockId.namespace(), blockId.key()); - var state = pair.getB(); - - var customBlock = CraftEngineBlocks.byId(key); - if (customBlock != null) { - ImmutableBlockState blockState = customBlock.defaultState(); - - for (var entry : state.entrySet()) { - var property = customBlock.getProperty(entry.getKey()); - if (property == null) continue; - var tag = property.optional(entry.getValue()).orElse(null); - if (tag == null) continue; - blockState = ImmutableBlockState.with(blockState, property, tag); - } - CraftEngineBlocks.place(block.getLocation(), blockState, false); - return; - } - - var furniture = CraftEngineFurniture.byId(key); - if (furniture == null) return; - var location = parseYawAndPitch(engine, block, state); - String variant = state.getOrDefault("variant", furniture.anyVariantName()); - CraftEngineFurniture.place(location, furniture, variant, false); - } - - private static Location parseYawAndPitch(@NotNull Engine engine, @NotNull Block block, @NotNull Map state) { - Location location = block.getLocation(); - long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY(); - RNG rng = new RNG(seed); - if ("true".equals(state.get("randomYaw"))) { - location.setYaw(rng.f(0, 360)); - } else if (state.containsKey("yaw")) { - location.setYaw(Float.parseFloat(state.get("yaw"))); - } - - if ("true".equals(state.get("randomPitch"))) { - location.setPitch(rng.f(0, 360)); - } else if (state.containsKey("pitch")) { - location.setPitch(Float.parseFloat(state.get("pitch"))); - } - - return location; - } - - @Override - public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { - return (switch (dataType) { - case ENTITY -> Stream.empty(); - case ITEM -> CraftEngineItems.loadedItems().keySet().stream(); - case BLOCK -> Stream.concat(CraftEngineBlocks.loadedBlocks().keySet().stream(), - CraftEngineFurniture.loadedFurniture().keySet().stream()); - }).map(key -> new Identifier(key.namespace(), key.value())).toList(); - } - - @Override - public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { - Key key = Key.of(id.namespace(), id.key()); - return switch (dataType) { - case ENTITY -> false; - case ITEM -> CraftEngineItems.byId(key) != null; - case BLOCK -> (CraftEngineBlocks.byId(key) != null || CraftEngineFurniture.byId(key) != null); - }; - } - - private static > BlockProperty convert(Property raw) { - return switch (raw) { - case BooleanProperty property -> BlockProperty.ofBoolean(property.name(), property.defaultValue()); - case IntegerProperty property -> BlockProperty.ofLong(property.name(), property.defaultValue(), property.min, property.max, false, false); - default -> new BlockProperty(raw.name(), raw.valueClass(), raw.defaultValue(), raw.possibleValues(), raw::valueName); - }; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/link/data/EcoItemsDataProvider.java b/core/src/main/java/com/volmit/iris/core/link/data/EcoItemsDataProvider.java deleted file mode 100644 index 05e9cd221..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/data/EcoItemsDataProvider.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.volmit.iris.core.link.data; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.reflect.WrappedField; -import com.willfp.ecoitems.items.EcoItem; -import com.willfp.ecoitems.items.EcoItems; -import org.bukkit.NamespacedKey; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.List; -import java.util.MissingResourceException; - -public class EcoItemsDataProvider extends ExternalDataProvider { - private WrappedField itemStack; - private WrappedField id; - - public EcoItemsDataProvider() { - super("EcoItems"); - } - - @Override - public void init() { - Iris.info("Setting up EcoItems Link..."); - itemStack = new WrappedField<>(EcoItem.class, "_itemStack"); - if (this.itemStack.hasFailed()) { - Iris.error("Failed to set up EcoItems Link: Unable to fetch ItemStack field!"); - } - id = new WrappedField<>(EcoItem.class, "id"); - if (this.id.hasFailed()) { - Iris.error("Failed to set up EcoItems Link: Unable to fetch id field!"); - } - } - - @NotNull - @Override - public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) throws MissingResourceException { - EcoItem item = EcoItems.INSTANCE.getByID(itemId.key()); - if (item == null) throw new MissingResourceException("Failed to find Item!", itemId.namespace(), itemId.key()); - return itemStack.get(item).clone(); - } - - @Override - public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { - if (dataType != DataType.ITEM) return List.of(); - return EcoItems.INSTANCE.values() - .stream() - .map(x -> Identifier.fromNamespacedKey(id.get(x))) - .filter(dataType.asPredicate(this)) - .toList(); - } - - @Override - public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { - return id.namespace().equalsIgnoreCase("ecoitems") && dataType == DataType.ITEM; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/link/data/HMCLeavesDataProvider.java b/core/src/main/java/com/volmit/iris/core/link/data/HMCLeavesDataProvider.java deleted file mode 100644 index eb17801d7..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/data/HMCLeavesDataProvider.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.volmit.iris.core.link.data; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.reflect.WrappedField; -import com.volmit.iris.util.reflect.WrappedReturningMethod; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.Leaves; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.MissingResourceException; -import java.util.function.Supplier; - -public class HMCLeavesDataProvider extends ExternalDataProvider { - private Object apiInstance; - private WrappedReturningMethod worldBlockType; - private WrappedReturningMethod setCustomBlock; - private Map blockDataMap = Map.of(); - private Map> itemDataField = Map.of(); - - public HMCLeavesDataProvider() { - super("HMCLeaves"); - } - - @Override - public String getPluginId() { - return "HMCLeaves"; - } - - @Override - public void init() { - try { - worldBlockType = new WrappedReturningMethod<>((Class) Class.forName("io.github.fisher2911.hmcleaves.data.BlockData"), "worldBlockType"); - apiInstance = getApiInstance(Class.forName("io.github.fisher2911.hmcleaves.api.HMCLeavesAPI")); - setCustomBlock = new WrappedReturningMethod<>((Class) apiInstance.getClass(), "setCustomBlock", Location.class, String.class, boolean.class); - Object config = getLeavesConfig(apiInstance.getClass()); - blockDataMap = getMap(config, "blockDataMap"); - itemDataField = getMap(config, "itemSupplierMap"); - } catch (Throwable e) { - Iris.error("Failed to initialize HMCLeavesDataProvider: " + e.getMessage()); - } - } - - @NotNull - @Override - public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap state) throws MissingResourceException { - Object o = blockDataMap.get(blockId.key()); - if (o == null) - throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); - Material material = worldBlockType.invoke(o, new Object[0]); - if (material == null) - throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); - BlockData blockData = Bukkit.createBlockData(material); - if (IrisSettings.get().getGenerator().preventLeafDecay && blockData instanceof Leaves leaves) - leaves.setPersistent(true); - return IrisCustomData.of(blockData, ExternalDataSVC.buildState(blockId, state)); - } - - @NotNull - @Override - public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) throws MissingResourceException { - if (!itemDataField.containsKey(itemId.key())) - throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); - return itemDataField.get(itemId.key()).get(); - } - - @Override - public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) { - var pair = ExternalDataSVC.parseState(blockId); - blockId = pair.getA(); - Boolean result = setCustomBlock.invoke(apiInstance, new Object[]{block.getLocation(), blockId.key(), false}); - if (result == null || !result) - Iris.warn("Failed to set custom block! " + blockId.key() + " " + block.getX() + " " + block.getY() + " " + block.getZ()); - else if (IrisSettings.get().getGenerator().preventLeafDecay) { - BlockData blockData = block.getBlockData(); - if (blockData instanceof Leaves leaves) - leaves.setPersistent(true); - } - } - - @Override - public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { - if (dataType == DataType.ENTITY) return List.of(); - return (dataType == DataType.BLOCK ? blockDataMap.keySet() : itemDataField.keySet()) - .stream() - .map(x -> new Identifier("hmcleaves", x)) - .filter(dataType.asPredicate(this)) - .toList(); - } - - @Override - public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { - if (dataType == DataType.ENTITY) return false; - return (dataType == DataType.ITEM ? itemDataField.keySet() : blockDataMap.keySet()).contains(id.key()); - } - - private Map getMap(C config, String name) { - WrappedField> field = new WrappedField<>((Class) config.getClass(), name); - return field.get(config); - } - - private A getApiInstance(Class apiClass) { - WrappedReturningMethod instance = new WrappedReturningMethod<>(apiClass, "getInstance"); - return instance.invoke(); - } - - private C getLeavesConfig(Class apiClass) { - WrappedReturningMethod instance = new WrappedReturningMethod<>(apiClass, "getInstance"); - WrappedField config = new WrappedField<>(apiClass, "config"); - return config.get(instance.invoke()); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/link/data/KGeneratorsDataProvider.java b/core/src/main/java/com/volmit/iris/core/link/data/KGeneratorsDataProvider.java deleted file mode 100644 index acb2b47d6..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/data/KGeneratorsDataProvider.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.volmit.iris.core.link.data; - -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.IrisCustomData; -import me.kryniowesegryderiusz.kgenerators.Main; -import me.kryniowesegryderiusz.kgenerators.api.KGeneratorsAPI; -import me.kryniowesegryderiusz.kgenerators.generators.locations.objects.GeneratorLocation; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.List; -import java.util.MissingResourceException; - -public class KGeneratorsDataProvider extends ExternalDataProvider { - public KGeneratorsDataProvider() { - super("KGenerators"); - } - - @Override - public void init() { - - } - - @Override - public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap state) throws MissingResourceException { - if (Main.getGenerators().get(blockId.key()) == null) throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); - return IrisCustomData.of(Material.STRUCTURE_VOID.createBlockData(), ExternalDataSVC.buildState(blockId, state)); - } - - @Override - public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) throws MissingResourceException { - var gen = Main.getGenerators().get(itemId.key()); - if (gen == null) throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); - return gen.getGeneratorItem(); - } - - @Override - public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) { - if (block.getType() != Material.STRUCTURE_VOID) return; - var existing = KGeneratorsAPI.getLoadedGeneratorLocation(block.getLocation()); - if (existing != null) return; - block.setBlockData(B.getAir(), false); - var gen = Main.getGenerators().get(blockId.key()); - if (gen == null) return; - var loc = new GeneratorLocation(-1, gen, block.getLocation(), Main.getPlacedGenerators().getChunkInfo(block.getChunk()), null, null); - Main.getDatabases().getDb().saveGenerator(loc); - Main.getPlacedGenerators().addLoaded(loc); - Main.getSchedules().schedule(loc, true); - } - - @Override - public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { - if (dataType == DataType.ENTITY) return List.of(); - return Main.getGenerators().getAll().stream() - .map(gen -> new Identifier("kgenerators", gen.getId())) - .filter(dataType.asPredicate(this)) - .toList(); - } - - @Override - public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { - if (dataType == DataType.ENTITY) return false; - return "kgenerators".equalsIgnoreCase(id.namespace()); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/link/data/MythicMobsDataProvider.java b/core/src/main/java/com/volmit/iris/core/link/data/MythicMobsDataProvider.java deleted file mode 100644 index b87963345..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/data/MythicMobsDataProvider.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.volmit.iris.core.link.data; - -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.tools.IrisToolbelt; -import io.lumine.mythic.api.adapters.AbstractLocation; -import io.lumine.mythic.api.config.MythicLineConfig; -import io.lumine.mythic.api.mobs.entities.SpawnReason; -import io.lumine.mythic.api.skills.conditions.ILocationCondition; -import io.lumine.mythic.bukkit.BukkitAdapter; -import io.lumine.mythic.bukkit.MythicBukkit; -import io.lumine.mythic.bukkit.adapters.BukkitWorld; -import io.lumine.mythic.bukkit.events.MythicConditionLoadEvent; -import io.lumine.mythic.core.mobs.ActiveMob; -import io.lumine.mythic.core.mobs.MobStack; -import io.lumine.mythic.core.skills.SkillCondition; -import io.lumine.mythic.core.utils.annotations.MythicCondition; -import io.lumine.mythic.core.utils.annotations.MythicField; -import org.bukkit.Location; -import org.bukkit.entity.Entity; -import org.bukkit.event.EventHandler; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; - -public class MythicMobsDataProvider extends ExternalDataProvider { - public MythicMobsDataProvider() { - super("MythicMobs"); - } - - @Override - public void init() { - } - - @Override - public @Nullable Entity spawnMob(@NotNull Location location, @NotNull Identifier entityId) throws MissingResourceException { - var mm = spawnMob(BukkitAdapter.adapt(location), entityId); - return mm == null ? null : mm.getEntity().getBukkitEntity(); - } - - private ActiveMob spawnMob(AbstractLocation location, Identifier entityId) throws MissingResourceException { - var manager = MythicBukkit.inst().getMobManager(); - var mm = manager.getMythicMob(entityId.key()).orElse(null); - if (mm == null) { - var stack = manager.getMythicMobStack(entityId.key()); - if (stack == null) throw new MissingResourceException("Failed to find Mob!", entityId.namespace(), entityId.key()); - return stack.spawn(location, 1d, SpawnReason.OTHER, null); - } - return mm.spawn(location, 1d, SpawnReason.OTHER, null, null); - } - - @Override - public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { - if (dataType != DataType.ENTITY) return List.of(); - var manager = MythicBukkit.inst().getMobManager(); - return Stream.concat(manager.getMobNames().stream(), - manager.getMobStacks() - .stream() - .map(MobStack::getName) - ) - .distinct() - .map(name -> new Identifier("mythicmobs", name)) - .toList(); - } - - @Override - public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { - return id.namespace().equalsIgnoreCase("mythicmobs") && dataType == DataType.ENTITY; - } - - @EventHandler - public void on(MythicConditionLoadEvent event) { - switch (event.getConditionName()) { - case "irisbiome" -> event.register(new IrisBiomeCondition(event.getConditionName(), event.getConfig())); - case "irisregion" -> event.register(new IrisRegionCondition(event.getConditionName(), event.getConfig())); - } - } - - @MythicCondition(author = "CrazyDev22", name = "irisbiome", description = "Tests if the target is within the given list of biomes") - public static class IrisBiomeCondition extends SkillCondition implements ILocationCondition { - @MythicField(name = "biome", aliases = {"b"}, description = "A list of biomes to check") - private Set biomes = ConcurrentHashMap.newKeySet(); - @MythicField(name = "surface", aliases = {"s"}, description = "If the biome check should only be performed on the surface") - private boolean surface; - - public IrisBiomeCondition(String line, MythicLineConfig mlc) { - super(line); - String b = mlc.getString(new String[]{"biome", "b"}, ""); - biomes.addAll(Arrays.asList(b.split(","))); - surface = mlc.getBoolean(new String[]{"surface", "s"}, false); - } - - @Override - public boolean check(AbstractLocation target) { - var access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld()); - if (access == null) return false; - var engine = access.getEngine(); - if (engine == null) return false; - var biome = surface ? - engine.getSurfaceBiome(target.getBlockX(), target.getBlockZ()) : - engine.getBiomeOrMantle(target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ()); - return biomes.contains(biome.getLoadKey()); - } - } - - @MythicCondition(author = "CrazyDev22", name = "irisregion", description = "Tests if the target is within the given list of biomes") - public static class IrisRegionCondition extends SkillCondition implements ILocationCondition { - @MythicField(name = "region", aliases = {"r"}, description = "A list of regions to check") - private Set regions = ConcurrentHashMap.newKeySet(); - - public IrisRegionCondition(String line, MythicLineConfig mlc) { - super(line); - String b = mlc.getString(new String[]{"region", "r"}, ""); - regions.addAll(Arrays.asList(b.split(","))); - } - - @Override - public boolean check(AbstractLocation target) { - var access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld()); - if (access == null) return false; - var engine = access.getEngine(); - if (engine == null) return false; - var region = engine.getRegion(target.getBlockX(), target.getBlockZ()); - return regions.contains(region.getLoadKey()); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/link/data/NexoDataProvider.java b/core/src/main/java/com/volmit/iris/core/link/data/NexoDataProvider.java deleted file mode 100644 index 4eeff9a31..000000000 --- a/core/src/main/java/com/volmit/iris/core/link/data/NexoDataProvider.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.volmit.iris.core.link.data; - -import com.nexomc.nexo.api.NexoBlocks; -import com.nexomc.nexo.api.NexoFurniture; -import com.nexomc.nexo.api.NexoItems; -import com.nexomc.nexo.items.ItemBuilder; -import com.volmit.iris.core.link.ExternalDataProvider; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.nms.container.BiomeColor; -import com.volmit.iris.core.nms.container.BlockProperty; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.IrisCustomData; -import org.bukkit.Color; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.bukkit.entity.ItemDisplay; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.LeatherArmorMeta; -import org.bukkit.inventory.meta.MapMeta; -import org.bukkit.inventory.meta.PotionMeta; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.List; -import java.util.MissingResourceException; - -public class NexoDataProvider extends ExternalDataProvider { - public NexoDataProvider() { - super("Nexo"); - } - - @Override - public void init() { - } - - @NotNull - @Override - public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap state) throws MissingResourceException { - if (!NexoItems.exists(blockId.key())) { - throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); - } - - Identifier blockState = ExternalDataSVC.buildState(blockId, state); - if (NexoBlocks.isCustomBlock(blockId.key())) { - BlockData data = NexoBlocks.blockData(blockId.key()); - if (data == null) - throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); - return IrisCustomData.of(data, blockState); - } else if (NexoFurniture.isFurniture(blockId.key())) { - return IrisCustomData.of(B.getAir(), blockState); - } - - throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); - } - - @Override - public @NotNull List getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException { - if (!NexoItems.exists(blockId.key())) { - throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()); - } - - return NexoFurniture.isFurniture(blockId.key()) ? YAW_FACE_BIOME_PROPERTIES : List.of(); - } - - @NotNull - @Override - public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) throws MissingResourceException { - ItemBuilder builder = NexoItems.itemFromId(itemId.key()); - if (builder == null) { - throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); - } - try { - return builder.build(); - } catch (Exception e) { - e.printStackTrace(); - throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()); - } - } - - @Override - public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) { - var statePair = ExternalDataSVC.parseState(blockId); - var state = statePair.getB(); - blockId = statePair.getA(); - - if (NexoBlocks.isCustomBlock(blockId.key())) { - NexoBlocks.place(blockId.key(), block.getLocation()); - return; - } - - if (!NexoFurniture.isFurniture(blockId.key())) - return; - - var pair = parseYawAndFace(engine, block, state); - ItemDisplay display = NexoFurniture.place(blockId.key(), block.getLocation(), pair.getA(), pair.getB()); - if (display == null) return; - ItemStack itemStack = display.getItemStack(); - if (itemStack == null) return; - - BiomeColor type = null; - try { - type = BiomeColor.valueOf(state.get("matchBiome").toUpperCase()); - } catch (NullPointerException | IllegalArgumentException ignored) {} - - if (type != null) { - var biomeColor = INMS.get().getBiomeColor(block.getLocation(), type); - if (biomeColor == null) return; - var potionColor = Color.fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue()); - var meta = itemStack.getItemMeta(); - switch (meta) { - case LeatherArmorMeta armor -> armor.setColor(potionColor); - case PotionMeta potion -> potion.setColor(potionColor); - case MapMeta map -> map.setColor(potionColor); - case null, default -> {} - } - itemStack.setItemMeta(meta); - } - display.setItemStack(itemStack); - } - - @Override - public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) { - if (dataType == DataType.ENTITY) return List.of(); - return NexoItems.itemNames() - .stream() - .map(i -> new Identifier("nexo", i)) - .filter(dataType.asPredicate(this)) - .toList(); - } - - @Override - public boolean isValidProvider(@NotNull Identifier id, DataType dataType) { - if (dataType == DataType.ENTITY) return false; - return "nexo".equalsIgnoreCase(id.namespace()); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/loader/ImageResourceLoader.java b/core/src/main/java/com/volmit/iris/core/loader/ImageResourceLoader.java deleted file mode 100644 index 877c08227..000000000 --- a/core/src/main/java/com/volmit/iris/core/loader/ImageResourceLoader.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.loader; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.engine.object.IrisImage; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.data.KCache; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.util.Set; - -public class ImageResourceLoader extends ResourceLoader { - public ImageResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName) { - super(root, idm, folderName, resourceTypeName, IrisImage.class); - loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize()); - } - - public boolean supportsSchemas() { - return false; - } - - public long getSize() { - return loadCache.getSize(); - } - - public long getTotalStorage() { - return getSize(); - } - - protected IrisImage loadFile(File j, String name) { - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - BufferedImage bu = ImageIO.read(j); - IrisImage img = new IrisImage(bu); - img.setLoadFile(j); - img.setLoader(manager); - img.setLoadKey(name); - logLoad(j, img); - tlt.addAndGet(p.getMilliseconds()); - return img; - } catch (Throwable e) { - Iris.reportError(e); - Iris.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + ": " + e.getMessage()); - return null; - } - } - - void getPNGFiles(File directory, Set m) { - for (File file : directory.listFiles()) { - if (file.isFile() && file.getName().endsWith(".png")) { - m.add(file.getName().replaceAll("\\Q.png\\E", "")); - } else if (file.isDirectory()) { - getPNGFiles(file, m); - } - } - } - - - public String[] getPossibleKeys() { - if (possibleKeys != null) { - return possibleKeys; - } - - Iris.debug("Building " + resourceTypeName + " Possibility Lists"); - KSet m = new KSet<>(); - - - for (File i : getFolders()) { - getPNGFiles(i, m); - } - -// for (File i : getFolders()) { -// for (File j : i.listFiles()) { -// if (j.isFile() && j.getName().endsWith(".png")) { -// m.add(j.getName().replaceAll("\\Q.png\\E", "")); -// } else if (j.isDirectory()) { -// for (File k : j.listFiles()) { -// if (k.isFile() && k.getName().endsWith(".png")) { -// m.add(j.getName() + "/" + k.getName().replaceAll("\\Q.png\\E", "")); -// } else if (k.isDirectory()) { -// for (File l : k.listFiles()) { -// if (l.isFile() && l.getName().endsWith(".png")) { -// m.add(j.getName() + "/" + k.getName() + "/" + l.getName().replaceAll("\\Q.png\\E", "")); -// } -// } -// } -// } -// } -// } -// } - - KList v = new KList<>(m); - possibleKeys = v.toArray(new String[0]); - return possibleKeys; - } - - public File findFile(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return j; - } - } - - File file = new File(i, name + ".png"); - - if (file.exists()) { - return file; - } - } - - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - - return null; - } - - public IrisImage load(String name) { - return load(name, true); - } - - private IrisImage loadRaw(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return loadFile(j, name); - } - } - - File file = new File(i, name + ".png"); - - if (file.exists()) { - return loadFile(file, name); - } - } - - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - - return null; - } - - public IrisImage load(String name, boolean warn) { - return loadCache.get(name); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/loader/IrisData.java b/core/src/main/java/com/volmit/iris/core/loader/IrisData.java deleted file mode 100644 index b43f3d639..000000000 --- a/core/src/main/java/com/volmit/iris/core/loader/IrisData.java +++ /dev/null @@ -1,593 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.loader; - -import com.google.gson.*; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import com.volmit.iris.Iris; -import com.volmit.iris.core.scripting.environment.PackEnvironment; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.engine.object.matter.IrisMatterObject; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.mantle.flag.MantleFlagAdapter; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.reflect.KeyedType; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import lombok.Data; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; - -@Data -public class IrisData implements ExclusionStrategy, TypeAdapterFactory { - private static final KMap dataLoaders = new KMap<>(); - private final File dataFolder; - private final int id; - private boolean closed = false; - private PackEnvironment environment; - private ResourceLoader biomeLoader; - private ResourceLoader lootLoader; - private ResourceLoader regionLoader; - private ResourceLoader dimensionLoader; - private ResourceLoader generatorLoader; - private ResourceLoader jigsawPieceLoader; - private ResourceLoader jigsawPoolLoader; - private ResourceLoader jigsawStructureLoader; - private ResourceLoader entityLoader; - private ResourceLoader markerLoader; - private ResourceLoader spawnerLoader; - private ResourceLoader modLoader; - private ResourceLoader blockLoader; - private ResourceLoader expressionLoader; - private ResourceLoader objectLoader; - private ResourceLoader matterLoader; - private ResourceLoader imageLoader; - private ResourceLoader scriptLoader; - private ResourceLoader caveLoader; - private ResourceLoader ravineLoader; - private ResourceLoader matterObjectLoader; - private KMap> possibleSnippets; - private Gson gson; - private Gson snippetLoader; - private GsonBuilder builder; - private KMap, ResourceLoader> loaders = new KMap<>(); - private Engine engine; - - private IrisData(File dataFolder) { - this.engine = null; - this.dataFolder = dataFolder; - this.id = RNG.r.imax(); - hotloaded(); - } - - public static IrisData get(File dataFolder) { - return dataLoaders.computeIfAbsent(dataFolder, IrisData::new); - } - - public static Optional getLoaded(File dataFolder) { - return Optional.ofNullable(dataLoaders.get(dataFolder)); - } - - public static void dereference() { - dataLoaders.values().forEach(IrisData::cleanupEngine); - } - - public static int cacheSize() { - int m = 0; - for (IrisData i : dataLoaders.values()) { - for (ResourceLoader j : i.getLoaders().values()) { - m += j.getLoadCache().getSize(); - } - } - - return m; - } - - private static void printData(ResourceLoader rl) { - Iris.warn(" " + rl.getResourceTypeName() + " @ /" + rl.getFolderName() + ": Cache=" + rl.getLoadCache().getSize() + " Folders=" + rl.getFolders().size()); - } - - public static IrisObject loadAnyObject(String key, @Nullable IrisData nearest) { - return loadAny(IrisObject.class, key, nearest); - } - - public static IrisMatterObject loadAnyMatter(String key, @Nullable IrisData nearest) { - return loadAny(IrisMatterObject.class, key, nearest); - } - - public static IrisBiome loadAnyBiome(String key, @Nullable IrisData nearest) { - return loadAny(IrisBiome.class, key, nearest); - } - - public static IrisExpression loadAnyExpression(String key, @Nullable IrisData nearest) { - return loadAny(IrisExpression.class, key, nearest); - } - - public static IrisMod loadAnyMod(String key, @Nullable IrisData nearest) { - return loadAny(IrisMod.class, key, nearest); - } - - public static IrisJigsawPiece loadAnyJigsawPiece(String key, @Nullable IrisData nearest) { - return loadAny(IrisJigsawPiece.class, key, nearest); - } - - public static IrisJigsawPool loadAnyJigsawPool(String key, @Nullable IrisData nearest) { - return loadAny(IrisJigsawPool.class, key, nearest); - } - - public static IrisEntity loadAnyEntity(String key, @Nullable IrisData nearest) { - return loadAny(IrisEntity.class, key, nearest); - } - - public static IrisLootTable loadAnyLootTable(String key, @Nullable IrisData nearest) { - return loadAny(IrisLootTable.class, key, nearest); - } - - public static IrisBlockData loadAnyBlock(String key, @Nullable IrisData nearest) { - return loadAny(IrisBlockData.class, key, nearest); - } - - public static IrisSpawner loadAnySpaner(String key, @Nullable IrisData nearest) { - return loadAny(IrisSpawner.class, key, nearest); - } - - public static IrisScript loadAnyScript(String key, @Nullable IrisData nearest) { - return loadAny(IrisScript.class, key, nearest); - } - - public static IrisRavine loadAnyRavine(String key, @Nullable IrisData nearest) { - return loadAny(IrisRavine.class, key, nearest); - } - - public static IrisRegion loadAnyRegion(String key, @Nullable IrisData nearest) { - return loadAny(IrisRegion.class, key, nearest); - } - - public static IrisMarker loadAnyMarker(String key, @Nullable IrisData nearest) { - return loadAny(IrisMarker.class, key, nearest); - } - - public static IrisCave loadAnyCave(String key, @Nullable IrisData nearest) { - return loadAny(IrisCave.class, key, nearest); - } - - public static IrisImage loadAnyImage(String key, @Nullable IrisData nearest) { - return loadAny(IrisImage.class, key, nearest); - } - - public static IrisDimension loadAnyDimension(String key, @Nullable IrisData nearest) { - return loadAny(IrisDimension.class, key, nearest); - } - - public static IrisJigsawStructure loadAnyJigsawStructure(String key, @Nullable IrisData nearest) { - return loadAny(IrisJigsawStructure.class, key, nearest); - } - - public static IrisGenerator loadAnyGenerator(String key, @Nullable IrisData nearest) { - return loadAny(IrisGenerator.class, key, nearest); - } - - public static T loadAny(Class type, String key, @Nullable IrisData nearest) { - try { - if (nearest != null) { - T t = nearest.load(type, key, false); - if (t != null) { - return t; - } - } - - for (File i : Objects.requireNonNull(Iris.instance.getDataFolder("packs").listFiles())) { - if (i.isDirectory()) { - IrisData dm = get(i); - if (dm == nearest) continue; - T t = dm.load(type, key, false); - - if (t != null) { - return t; - } - } - } - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return null; - } - - public T load(Class type, String key, boolean warn) { - var loader = getLoader(type); - if (loader == null) return null; - return loader.load(key, warn); - } - - @SuppressWarnings("unchecked") - public ResourceLoader getLoader(Class type) { - return (ResourceLoader) loaders.get(type); - } - - public ResourceLoader getTypedLoaderFor(File f) { - String[] k = f.getPath().split("\\Q" + File.separator + "\\E"); - - for (String i : k) { - for (ResourceLoader j : loaders.values()) { - if (j.getFolderName().equals(i)) { - return j; - } - } - } - - return null; - } - - public void cleanupEngine() { - if (engine != null && engine.isClosed()) { - engine = null; - Iris.debug("Dereferenced Data " + getId() + " " + getDataFolder()); - } - } - - public void preprocessObject(IrisRegistrant t) { - try { - IrisContext ctx = IrisContext.get(); - Engine engine = this.engine; - - if (engine == null && ctx != null && ctx.getEngine() != null) { - engine = ctx.getEngine(); - } - - if (engine == null && t.getPreprocessors().isNotEmpty()) { - Iris.error("Failed to preprocess object " + t.getLoadKey() + " because there is no engine context here. (See stack below)"); - try { - throw new RuntimeException(); - } catch (Throwable ex) { - ex.printStackTrace(); - } - } - - if (engine == null) return; - var global = engine.getDimension().getPreProcessors(t.getFolderName()); - var local = t.getPreprocessors(); - if ((global != null && global.isNotEmpty()) || local.isNotEmpty()) { - synchronized (this) { - if (global != null) { - for (String i : global) { - engine.getExecution().preprocessObject(i, t); - Iris.debug("Loader<" + C.GREEN + t.getTypeName() + C.LIGHT_PURPLE + "> iprocess " + C.YELLOW + t.getLoadKey() + C.LIGHT_PURPLE + " in " + i); - } - } - - for (String i : local) { - engine.getExecution().preprocessObject(i, t); - Iris.debug("Loader<" + C.GREEN + t.getTypeName() + C.LIGHT_PURPLE + "> iprocess " + C.YELLOW + t.getLoadKey() + C.LIGHT_PURPLE + " in " + i); - } - } - } - } catch (Throwable e) { - Iris.error("Failed to preprocess object!"); - e.printStackTrace(); - } - } - - public void close() { - closed = true; - dump(); - dataLoaders.remove(dataFolder); - } - - public IrisData copy() { - return IrisData.get(dataFolder); - } - - private ResourceLoader registerLoader(Class registrant) { - try { - IrisRegistrant rr = registrant.getConstructor().newInstance(); - ResourceLoader r = null; - if (registrant.equals(IrisObject.class)) { - r = (ResourceLoader) new ObjectResourceLoader(dataFolder, this, rr.getFolderName(), - rr.getTypeName()); - } else if (registrant.equals(IrisMatterObject.class)) { - r = (ResourceLoader) new MatterObjectResourceLoader(dataFolder, this, rr.getFolderName(), - rr.getTypeName()); - } else if (registrant.equals(IrisScript.class)) { - r = (ResourceLoader) new ScriptResourceLoader(dataFolder, this, rr.getFolderName(), - rr.getTypeName()); - } else if (registrant.equals(IrisImage.class)) { - r = (ResourceLoader) new ImageResourceLoader(dataFolder, this, rr.getFolderName(), - rr.getTypeName()); - } else { - J.attempt(() -> registrant.getConstructor().newInstance().registerTypeAdapters(builder)); - r = new ResourceLoader<>(dataFolder, this, rr.getFolderName(), rr.getTypeName(), registrant); - } - - loaders.put(registrant, r); - - return r; - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - Iris.error("Failed to create loader! " + registrant.getCanonicalName()); - } - - return null; - } - - public synchronized void hotloaded() { - closed = false; - possibleSnippets = new KMap<>(); - builder = new GsonBuilder() - .addDeserializationExclusionStrategy(this) - .addSerializationExclusionStrategy(this) - .setLenient() - .registerTypeAdapterFactory(this) - .registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter()) - .setPrettyPrinting(); - loaders.clear(); - File packs = dataFolder; - packs.mkdirs(); - this.lootLoader = registerLoader(IrisLootTable.class); - this.spawnerLoader = registerLoader(IrisSpawner.class); - this.entityLoader = registerLoader(IrisEntity.class); - this.regionLoader = registerLoader(IrisRegion.class); - this.biomeLoader = registerLoader(IrisBiome.class); - this.modLoader = registerLoader(IrisMod.class); - this.dimensionLoader = registerLoader(IrisDimension.class); - this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class); - this.jigsawStructureLoader = registerLoader(IrisJigsawStructure.class); - this.jigsawPieceLoader = registerLoader(IrisJigsawPiece.class); - this.generatorLoader = registerLoader(IrisGenerator.class); - this.caveLoader = registerLoader(IrisCave.class); - this.markerLoader = registerLoader(IrisMarker.class); - this.ravineLoader = registerLoader(IrisRavine.class); - this.blockLoader = registerLoader(IrisBlockData.class); - this.expressionLoader = registerLoader(IrisExpression.class); - this.objectLoader = registerLoader(IrisObject.class); - this.imageLoader = registerLoader(IrisImage.class); - this.scriptLoader = registerLoader(IrisScript.class); - this.matterObjectLoader = registerLoader(IrisMatterObject.class); - this.environment = PackEnvironment.create(this); - builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter); - - gson = builder.create(); - dimensionLoader.streamAll() - .map(IrisDimension::getDataScripts) - .flatMap(KList::stream) - .forEach(environment::execute); - - if (engine != null) { - engine.hotload(); - } - } - - public void dump() { - for (ResourceLoader i : loaders.values()) { - i.clearCache(); - } - } - - public void clearLists() { - for (ResourceLoader i : loaders.values()) { - i.clearList(); - } - possibleSnippets.clear(); - } - - public Set> resolveSnippets() { - var result = new HashSet>(); - var processed = new HashSet>(); - var excluder = gson.excluder(); - - var queue = new LinkedList>(loaders.keySet()); - while (!queue.isEmpty()) { - var type = queue.poll(); - if (excluder.excludeClass(type, false) || !processed.add(type)) - continue; - if (type.isAnnotationPresent(Snippet.class)) - result.add(type); - - try { - for (var field : type.getDeclaredFields()) { - if (excluder.excludeField(field, false)) - continue; - - queue.add(field.getType()); - } - } catch (Throwable ignored) { - } - } - - return result; - } - - public String toLoadKey(File f) { - if (f.getPath().startsWith(getDataFolder().getPath())) { - String[] full = f.getPath().split("\\Q" + File.separator + "\\E"); - String[] df = getDataFolder().getPath().split("\\Q" + File.separator + "\\E"); - StringBuilder g = new StringBuilder(); - boolean m = true; - for (int i = 0; i < full.length; i++) { - if (i >= df.length) { - if (m) { - m = false; - continue; - } - - g.append("/").append(full[i]); - } - } - - return g.substring(1).split("\\Q.\\E")[0]; - } else { - Iris.error("Forign file from loader " + f.getPath() + " (loader realm: " + getDataFolder().getPath() + ")"); - } - - Iris.error("Failed to load " + f.getPath() + " (loader realm: " + getDataFolder().getPath() + ")"); - - return null; - } - - @Override - public boolean shouldSkipField(FieldAttributes f) { - return false; - } - - @Override - public boolean shouldSkipClass(Class c) { - if (c.equals(AtomicCache.class)) { - return true; - } else return c.equals(ChronoLatch.class); - } - - @Override - public TypeAdapter create(Gson gson, TypeToken typeToken) { - if (!typeToken.getRawType().isAnnotationPresent(Snippet.class)) { - return null; - } - - String snippetType = typeToken.getRawType().getDeclaredAnnotation(Snippet.class).value(); - String snippedBase = "snippet/" + snippetType + "/"; - - return new TypeAdapter<>() { - @Override - public void write(JsonWriter jsonWriter, T t) throws IOException { - gson.getDelegateAdapter(IrisData.this, typeToken).write(jsonWriter, t); - } - - @Override - public T read(JsonReader reader) throws IOException { - TypeAdapter adapter = gson.getDelegateAdapter(IrisData.this, typeToken); - - if (reader.peek().equals(JsonToken.STRING)) { - String r = reader.nextString(); - if (!r.startsWith("snippet/")) - return null; - if (!r.startsWith(snippedBase)) - r = snippedBase + r.substring(8); - - File f = new File(getDataFolder(), r + ".json"); - if (f.exists()) { - try (JsonReader snippetReader = new JsonReader(new FileReader(f))){ - return adapter.read(snippetReader); - } catch (Throwable e) { - Iris.error("Couldn't read snippet " + r + " in " + reader.getPath() + " (" + e.getMessage() + ")"); - } - } else { - Iris.error("Couldn't find snippet " + r + " in " + reader.getPath()); - } - - return null; - } - - try { - return adapter.read(reader); - } catch (Throwable e) { - Iris.error("Failed to read " + typeToken.getRawType().getCanonicalName() + "... faking objects a little to load the file at least."); - Iris.reportError(e); - try { - return (T) typeToken.getRawType().getConstructor().newInstance(); - } catch (Throwable ignored) { - - } - } - return null; - } - }; - } - - public KList getPossibleSnippets(String f) { - return possibleSnippets.computeIfAbsent(f, (k) -> { - KList l = new KList<>(); - - File snippetFolder = new File(getDataFolder(), "snippet/" + f); - if (!snippetFolder.exists()) return l; - - String absPath = snippetFolder.getAbsolutePath(); - try (var stream = Files.walk(snippetFolder.toPath())) { - stream.filter(Files::isRegularFile) - .map(Path::toAbsolutePath) - .map(Path::toString) - .filter(s -> s.endsWith(".json")) - .map(s -> s.substring(absPath.length() + 1)) - .map(s -> s.replace("\\", "/")) - .map(s -> s.split("\\Q.\\E")[0]) - .forEach(s -> l.add("snippet/" + s)); - } catch (Throwable e) { - e.printStackTrace(); - } - - return l; - }); - } - - public boolean isClosed() { - return closed; - } - - public void savePrefetch(Engine engine) { - BurstExecutor b = MultiBurst.ioBurst.burst(loaders.size()); - - for (ResourceLoader i : loaders.values()) { - b.queue(() -> { - try { - i.saveFirstAccess(engine); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - b.complete(); - Iris.info("Saved Prefetch Cache to speed up future world startups"); - } - - public void loadPrefetch(Engine engine) { - BurstExecutor b = MultiBurst.ioBurst.burst(loaders.size()); - - for (ResourceLoader i : loaders.values()) { - b.queue(() -> { - try { - i.loadFirstAccess(engine); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - b.complete(); - Iris.info("Loaded Prefetch Cache to reduce generation disk use."); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/core/loader/IrisRegistrant.java b/core/src/main/java/com/volmit/iris/core/loader/IrisRegistrant.java deleted file mode 100644 index 123fd6f2e..000000000 --- a/core/src/main/java/com/volmit/iris/core/loader/IrisRegistrant.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.loader; - -import com.google.gson.GsonBuilder; -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.IrisScript; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.RegistryListResource; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.awt.*; -import java.io.File; - -@Data -public abstract class IrisRegistrant { - @Desc("Preprocess this object in-memory when it's loaded, run scripts using the variable 'object' and modify properties about this object before it's used.\nFile extension: .proc.kts") - @RegistryListResource(IrisScript.class) - @ArrayType(min = 1, type = String.class) - private KList preprocessors = new KList<>(); - - @EqualsAndHashCode.Exclude - private transient IrisData loader; - - private transient String loadKey; - - private transient File loadFile; - - public abstract String getFolderName(); - - public abstract String getTypeName(); - - public void registerTypeAdapters(GsonBuilder builder) { - - } - - public File openInVSCode() { - try { - Desktop.getDesktop().open(getLoadFile()); - } catch (Throwable e) { - Iris.reportError(e); - } - - return getLoadFile(); - } - - public abstract void scanForErrors(JSONObject p, VolmitSender sender); -} diff --git a/core/src/main/java/com/volmit/iris/core/loader/MatterObjectResourceLoader.java b/core/src/main/java/com/volmit/iris/core/loader/MatterObjectResourceLoader.java deleted file mode 100644 index e09dc64fc..000000000 --- a/core/src/main/java/com/volmit/iris/core/loader/MatterObjectResourceLoader.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.loader; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.engine.object.matter.IrisMatterObject; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.data.KCache; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; - -import java.io.File; - -public class MatterObjectResourceLoader extends ResourceLoader { - private String[] possibleKeys; - - public MatterObjectResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName) { - super(root, idm, folderName, resourceTypeName, IrisMatterObject.class); - loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize()); - } - - public boolean supportsSchemas() { - return false; - } - - public long getSize() { - return loadCache.getSize(); - } - - public long getTotalStorage() { - return getSize(); - } - - protected IrisMatterObject loadFile(File j, String name) { - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - IrisMatterObject t = IrisMatterObject.from(j); - t.setLoadKey(name); - t.setLoader(manager); - t.setLoadFile(j); - logLoad(j, t); - tlt.addAndGet(p.getMilliseconds()); - return t; - } catch (Throwable e) { - Iris.reportError(e); - Iris.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + ": " + e.getMessage()); - return null; - } - } - - private void findMatFiles(File dir, KSet m) { - for (File file : dir.listFiles()) { - if (file.isFile() && file.getName().endsWith(".mat")) { - m.add(file.getName().replaceAll("\\Q.mat\\E", "")); - } else if (file.isDirectory()) { - findMatFiles(file, m); - } - } - } - - public String[] getPossibleKeys() { - if (possibleKeys != null) { - return possibleKeys; - } - - Iris.debug("Building " + resourceTypeName + " Possibility Lists"); - KSet m = new KSet<>(); - - for (File folder : getFolders()) { - findMatFiles(folder, m); - } - - KList v = new KList<>(m); - possibleKeys = v.toArray(new String[0]); - return possibleKeys; - } - - -// public String[] getPossibleKeys() { -// if (possibleKeys != null) { -// return possibleKeys; -// } -// -// Iris.debug("Building " + resourceTypeName + " Possibility Lists"); -// KSet m = new KSet<>(); -// -// for (File i : getFolders()) { -// for (File j : i.listFiles()) { -// if (j.isFile() && j.getName().endsWith(".mat")) { -// m.add(j.getName().replaceAll("\\Q.mat\\E", "")); -// } else if (j.isDirectory()) { -// for (File k : j.listFiles()) { -// if (k.isFile() && k.getName().endsWith(".mat")) { -// m.add(j.getName() + "/" + k.getName().replaceAll("\\Q.mat\\E", "")); -// } else if (k.isDirectory()) { -// for (File l : k.listFiles()) { -// if (l.isFile() && l.getName().endsWith(".mat")) { -// m.add(j.getName() + "/" + k.getName() + "/" + l.getName().replaceAll("\\Q.mat\\E", "")); -// } -// } -// } -// } -// } -// } -// } -// -// KList v = new KList<>(m); -// possibleKeys = v.toArray(new String[0]); -// return possibleKeys; -// } - - public File findFile(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return j; - } - } - - File file = new File(i, name + ".mat"); - - if (file.exists()) { - return file; - } - } - - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - - return null; - } - - public IrisMatterObject load(String name) { - return load(name, true); - } - - private IrisMatterObject loadRaw(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return loadFile(j, name); - } - } - - File file = new File(i, name + ".mat"); - - if (file.exists()) { - return loadFile(file, name); - } - } - - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - - return null; - } - - public IrisMatterObject load(String name, boolean warn) { - return loadCache.get(name); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/loader/ObjectResourceLoader.java b/core/src/main/java/com/volmit/iris/core/loader/ObjectResourceLoader.java deleted file mode 100644 index 3fa92bc46..000000000 --- a/core/src/main/java/com/volmit/iris/core/loader/ObjectResourceLoader.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.loader; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.engine.object.IrisObject; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.data.KCache; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; - -import java.io.File; - -public class ObjectResourceLoader extends ResourceLoader { - public ObjectResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName) { - super(root, idm, folderName, resourceTypeName, IrisObject.class); - loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize()); - } - - public boolean supportsSchemas() { - return false; - } - - public long getSize() { - return loadCache.getSize(); - } - - public long getTotalStorage() { - return getSize(); - } - - protected IrisObject loadFile(File j, String name) { - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - IrisObject t = new IrisObject(0, 0, 0); - t.setLoadKey(name); - t.setLoader(manager); - t.setLoadFile(j); - t.read(j); - logLoad(j, t); - tlt.addAndGet(p.getMilliseconds()); - return t; - } catch (Throwable e) { - Iris.reportError(e); - Iris.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + ": " + e.getMessage()); - return null; - } - } - - public String[] getPossibleKeys() { - if (possibleKeys != null) { - return possibleKeys; - } - Iris.debug("Building " + resourceTypeName + " Possibility Lists"); - KSet m = new KSet<>(); - for (File i : getFolders()) { - m.addAll(getFiles(i, ".iob", true)); - } - possibleKeys = m.toArray(new String[0]); - return possibleKeys; - } - - private KList getFiles(File dir, String ext, boolean skipDirName) { - KList paths = new KList<>(); - String name = skipDirName ? "" : dir.getName() + "/"; - for (File f : dir.listFiles()) { - if (f.isFile() && f.getName().endsWith(ext)) { - paths.add(name + f.getName().replaceAll("\\Q" + ext + "\\E", "")); - } else if (f.isDirectory()) { - getFiles(f, ext, false).forEach(e -> paths.add(name + e)); - } - } - return paths; - } - - public File findFile(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return j; - } - } - - File file = new File(i, name + ".iob"); - - if (file.exists()) { - return file; - } - } - - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - - return null; - } - - public IrisObject load(String name) { - return load(name, true); - } - - private IrisObject loadRaw(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return loadFile(j, name); - } - } - - File file = new File(i, name + ".iob"); - - if (file.exists()) { - return loadFile(file, name); - } - } - - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - - return null; - } - - public IrisObject load(String name, boolean warn) { - return loadCache.get(name); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/loader/ResourceLoader.java b/core/src/main/java/com/volmit/iris/core/loader/ResourceLoader.java deleted file mode 100644 index 6e06a9772..000000000 --- a/core/src/main/java/com/volmit/iris/core/loader/ResourceLoader.java +++ /dev/null @@ -1,482 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.loader; - -import com.google.common.util.concurrent.AtomicDouble; -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.project.SchemaBuilder; -import com.volmit.iris.core.service.PreservationSVC; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.MeteredCache; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.data.KCache; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.CustomOutputStream; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; - -import java.io.*; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Locale; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.stream.Stream; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -@Data -@EqualsAndHashCode(exclude = "manager") -@ToString(exclude = "manager") -public class ResourceLoader implements MeteredCache { - public static final AtomicDouble tlt = new AtomicDouble(0); - private static final int CACHE_SIZE = 100000; - protected final AtomicCache> folderCache; - protected KSet firstAccess; - protected File root; - protected String folderName; - protected String resourceTypeName; - protected KCache loadCache; - protected Class objectClass; - protected String cname; - protected String[] possibleKeys = null; - protected IrisData manager; - protected AtomicInteger loads; - protected ChronoLatch sec; - - public ResourceLoader(File root, IrisData manager, String folderName, String resourceTypeName, Class objectClass) { - this.manager = manager; - firstAccess = new KSet<>(); - folderCache = new AtomicCache<>(); - sec = new ChronoLatch(5000); - loads = new AtomicInteger(); - this.objectClass = objectClass; - cname = objectClass.getCanonicalName(); - this.resourceTypeName = resourceTypeName; - this.root = root; - this.folderName = folderName; - loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getResourceLoaderCacheSize()); - Iris.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> created in " + C.RED + "IDM/" + manager.getId() + C.LIGHT_PURPLE + " on " + C.GRAY + manager.getDataFolder().getPath()); - Iris.service(PreservationSVC.class).registerCache(this); - } - - public JSONObject buildSchema() { - Iris.debug("Building Schema " + objectClass.getSimpleName() + " " + root.getPath()); - JSONObject o = new JSONObject(); - KList fm = new KList<>(); - - for (int g = 1; g < 8; g++) { - fm.add("/" + folderName + Form.repeat("/*", g) + ".json"); - } - - o.put("fileMatch", new JSONArray(fm.toArray())); - o.put("url", "./.iris/schema/" + getFolderName() + "-schema.json"); - File a = new File(getManager().getDataFolder(), ".iris/schema/" + getFolderName() + "-schema.json"); - J.attemptAsync(() -> IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4))); - - return o; - } - - public File findFile(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return j; - } - } - - File file = new File(i, name + ".json"); - - if (file.exists()) { - return file; - } - } - - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - - return null; - } - - public void logLoad(File path, T t) { - loads.getAndIncrement(); - - if (loads.get() == 1) { - sec.flip(); - } - - if (sec.flip()) { - J.a(() -> { - Iris.verbose("Loaded " + C.WHITE + loads.get() + " " + resourceTypeName + (loads.get() == 1 ? "" : "s") + C.GRAY + " (" + Form.f(getLoadCache().getSize()) + " " + resourceTypeName + (loadCache.getSize() == 1 ? "" : "s") + " Loaded)"); - loads.set(0); - }); - } - - Iris.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> iload " + C.YELLOW + t.getLoadKey() + C.LIGHT_PURPLE + " in " + C.GRAY + t.getLoadFile().getPath() + C.LIGHT_PURPLE + " TLT: " + C.RED + Form.duration(tlt.get(), 2)); - } - - public void failLoad(File path, Throwable e) { - J.a(() -> Iris.warn("Couldn't Load " + resourceTypeName + " file: " + path.getPath() + ": " + e.getMessage())); - } - - private KList matchAllFiles(File root, Predicate f) { - KList fx = new KList<>(); - matchFiles(root, fx, f); - return fx; - } - - private void matchFiles(File at, KList files, Predicate f) { - if (at.isDirectory()) { - for (File i : at.listFiles()) { - matchFiles(i, files, f); - } - } else { - if (f.test(at)) { - files.add(at); - } - } - } - - public String[] getPossibleKeys() { - if (possibleKeys != null) { - return possibleKeys; - } - - KList files = getFolders(); - - if (files == null) { - possibleKeys = new String[0]; - return possibleKeys; - } - - HashSet m = new HashSet<>(); - for (File i : files) { - for (File j : matchAllFiles(i, (f) -> f.getName().endsWith(".json"))) { - m.add(i.toURI().relativize(j.toURI()).getPath().replaceAll("\\Q.json\\E", "")); - } - } - - KList v = new KList<>(m); - possibleKeys = v.toArray(new String[0]); - return possibleKeys; - } - - public long count() { - return loadCache.getSize(); - } - - protected T loadFile(File j, String name) { - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - T t = getManager().getGson() - .fromJson(preprocess(new JSONObject(IO.readAll(j))).toString(0), objectClass); - t.setLoadKey(name); - t.setLoadFile(j); - t.setLoader(manager); - getManager().preprocessObject(t); - logLoad(j, t); - tlt.addAndGet(p.getMilliseconds()); - return t; - } catch (Throwable e) { - Iris.reportError(e); - failLoad(j, e); - return null; - } - } - - protected JSONObject preprocess(JSONObject j) { - return j; - } - - public Stream streamAll() { - return streamAll(Arrays.stream(getPossibleKeys())); - } - - public Stream streamAll(Stream s) { - return s.map(this::load); - } - - public KList loadAll(KList s) { - KList m = new KList<>(); - - for (String i : s) { - T t = load(i); - - if (t != null) { - m.add(t); - } - } - - return m; - } - - public KList loadAllParallel(KList s) { - KList m = new KList<>(); - BurstExecutor burst = MultiBurst.ioBurst.burst(s.size()); - - for (String i : s) { - burst.queue(() -> { - T t = load(i); - if (t == null) - return; - - synchronized (m) { - m.add(t); - } - }); - } - - burst.complete(); - return m; - } - - public KList loadAll(KList s, Consumer postLoad) { - KList m = new KList<>(); - - for (String i : s) { - T t = load(i); - - if (t != null) { - m.add(t); - postLoad.accept(t); - } - } - - return m; - } - - public KList loadAll(String[] s) { - KList m = new KList<>(); - - for (String i : s) { - T t = load(i); - - if (t != null) { - m.add(t); - } - } - - return m; - } - - public T load(String name) { - return load(name, true); - } - - private T loadRaw(String name) { - for (File i : getFolders(name)) { - //noinspection ConstantConditions - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return loadFile(j, name); - } - } - - File file = new File(i, name + ".json"); - - if (file.exists()) { - return loadFile(file, name); - } - } - - return null; - } - - public T load(String name, boolean warn) { - if (name == null) { - return null; - } - - if (name.trim().isEmpty()) { - return null; - } - - var set = firstAccess; - if (set != null) firstAccess.add(name); - return loadCache.get(name); - } - - public void loadFirstAccess(Engine engine) throws IOException { - String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode()); - File file = Iris.instance.getDataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch"); - - if (!file.exists()) { - return; - } - - FileInputStream fin = new FileInputStream(file); - GZIPInputStream gzi = new GZIPInputStream(fin); - DataInputStream din = new DataInputStream(gzi); - int m = din.readInt(); - KList s = new KList<>(); - - for (int i = 0; i < m; i++) { - s.add(din.readUTF()); - } - - din.close(); - Iris.info("Loading " + s.size() + " prefetch " + getFolderName()); - firstAccess = null; - loadAllParallel(s); - } - - public void saveFirstAccess(Engine engine) throws IOException { - if (firstAccess == null) return; - String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode()); - File file = Iris.instance.getDataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch"); - file.getParentFile().mkdirs(); - FileOutputStream fos = new FileOutputStream(file); - GZIPOutputStream gzo = new CustomOutputStream(fos, 9); - DataOutputStream dos = new DataOutputStream(gzo); - var set = firstAccess; - firstAccess = null; - dos.writeInt(set.size()); - - for (String i : set) { - dos.writeUTF(i); - } - - dos.flush(); - dos.close(); - } - - public KList getFolders() { - return folderCache.aquire(() -> { - KList fc = new KList<>(); - - File[] files = root.listFiles(); - if (files == null) { - throw new IllegalStateException("Failed to list files in " + root); - } - - for (File i : files) { - if (i.isDirectory()) { - if (i.getName().equals(folderName)) { - fc.add(i); - break; - } - } - } - return fc; - }); - } - - public KList getFolders(String rc) { - KList folders = getFolders().copy(); - - if (rc.contains(":")) { - for (File i : folders.copy()) { - if (!rc.startsWith(i.getName() + ":")) { - folders.remove(i); - } - } - } - - return folders; - } - - public void clearCache() { - possibleKeys = null; - loadCache.invalidate(); - folderCache.reset(); - } - - public File fileFor(T b) { - for (File i : getFolders()) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(b.getLoadKey())) { - return j; - } - } - - File file = new File(i, b.getLoadKey() + ".json"); - - if (file.exists()) { - return file; - } - } - - return null; - } - - public boolean isLoaded(String next) { - return loadCache.contains(next); - } - - public void clearList() { - folderCache.reset(); - possibleKeys = null; - } - - public KList getPossibleKeys(String arg) { - KList f = new KList<>(); - - for (String i : getPossibleKeys()) { - if (i.equalsIgnoreCase(arg) || i.toLowerCase(Locale.ROOT).startsWith(arg.toLowerCase(Locale.ROOT)) || i.toLowerCase(Locale.ROOT).contains(arg.toLowerCase(Locale.ROOT)) || arg.toLowerCase(Locale.ROOT).contains(i.toLowerCase(Locale.ROOT))) { - f.add(i); - } - } - - return f; - } - - public boolean supportsSchemas() { - return true; - } - - public void clean() { - - } - - public long getSize() { - return loadCache.getSize(); - } - - @Override - public KCache getRawCache() { - return loadCache; - } - - @Override - public long getMaxSize() { - return loadCache.getMaxSize(); - } - - @Override - public boolean isClosed() { - return getManager().isClosed(); - } - - public long getTotalStorage() { - return getSize(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/loader/ScriptResourceLoader.java b/core/src/main/java/com/volmit/iris/core/loader/ScriptResourceLoader.java deleted file mode 100644 index 793a0aa1b..000000000 --- a/core/src/main/java/com/volmit/iris/core/loader/ScriptResourceLoader.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.loader; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.engine.object.IrisScript; -import com.volmit.iris.util.data.KCache; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; - -import java.io.File; -import java.util.HashSet; -import java.util.Set; - -public class ScriptResourceLoader extends ResourceLoader { - public ScriptResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName) { - super(root, idm, folderName, resourceTypeName, IrisScript.class); - loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getScriptLoaderCacheSize()); - } - - public boolean supportsSchemas() { - return false; - } - - public long getSize() { - return loadCache.getSize(); - } - - protected IrisScript loadFile(File j, String name) { - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - IrisScript t = new IrisScript(IO.readAll(j)); - t.setLoadKey(name); - t.setLoader(manager); - t.setLoadFile(j); - logLoad(j, t); - tlt.addAndGet(p.getMilliseconds()); - return t; - } catch (Throwable e) { - Iris.reportError(e); - Iris.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + ": " + e.getMessage()); - return null; - } - } - - - public String[] getPossibleKeys() { - if (possibleKeys != null) { - return possibleKeys; - } - - Iris.debug("Building " + resourceTypeName + " Possibility Lists"); - Set keys = new HashSet<>(); - - for (File i : getFolders()) { - if (i.isDirectory()) { - keys.addAll(getKeysInDirectory(i)); - } - } - - possibleKeys = keys.toArray(new String[0]); - return possibleKeys; - } - - private Set getKeysInDirectory(File directory) { - Set keys = new HashSet<>(); - for (File file : directory.listFiles()) { - if (file.isFile() && file.getName().endsWith(".kts")) { - keys.add(file.getName().replaceAll("\\Q.kts\\E", "")); - } else if (file.isDirectory()) { - keys.addAll(getKeysInDirectory(file)); - } - } - return keys; - } - -// public String[] getPossibleKeys() { -// if (possibleKeys != null) { -// return possibleKeys; -// } -// -// Iris.debug("Building " + resourceTypeName + " Possibility Lists"); -// KSet m = new KSet<>(); -// -// for (File i : getFolders()) { -// for (File j : i.listFiles()) { -// if (j.isFile() && j.getName().endsWith(".js")) { -// m.add(j.getName().replaceAll("\\Q.js\\E", "")); -// } else if (j.isDirectory()) { -// for (File k : j.listFiles()) { -// if (k.isFile() && k.getName().endsWith(".js")) { -// m.add(j.getName() + "/" + k.getName().replaceAll("\\Q.js\\E", "")); -// } else if (k.isDirectory()) { -// for (File l : k.listFiles()) { -// if (l.isFile() && l.getName().endsWith(".js")) { -// m.add(j.getName() + "/" + k.getName() + "/" + l.getName().replaceAll("\\Q.js\\E", "")); -// } -// } -// } -// } -// } -// } -// } -// -// KList v = new KList<>(m); -// possibleKeys = v.toArray(new String[0]); -// return possibleKeys; -// } - - public File findFile(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".kts") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return j; - } - } - - File file = new File(i, name + ".kts"); - - if (file.exists()) { - return file; - } - } - - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - - return null; - } - - private IrisScript loadRaw(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".kts") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return loadFile(j, name); - } - } - - File file = new File(i, name + ".kts"); - - if (file.exists()) { - return loadFile(file, name); - } - } - - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - - return null; - } - - public IrisScript load(String name, boolean warn) { - return loadCache.get(name); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/BiomeBaseInjector.java b/core/src/main/java/com/volmit/iris/core/nms/BiomeBaseInjector.java deleted file mode 100644 index 8f7062f99..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/BiomeBaseInjector.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.nms; - -@FunctionalInterface -public interface BiomeBaseInjector { - default void setBiome(int x, int z, Object biomeBase) { - setBiome(x, 0, z, biomeBase); - } - - void setBiome(int x, int y, int z, Object biomeBase); -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/INMS.java b/core/src/main/java/com/volmit/iris/core/nms/INMS.java deleted file mode 100644 index a5ed8ba11..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/INMS.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.nms; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.nms.v1X.NMSBinding1X; -import org.bukkit.Bukkit; - -import java.util.List; - -public class INMS { - private static final Version CURRENT = Boolean.getBoolean("iris.no-version-limit") ? - new Version(Integer.MAX_VALUE, Integer.MAX_VALUE, null) : - new Version(21, 11, null); - - private static final List REVISION = List.of( - new Version(21, 11, "v1_21_R7"), - new Version(21, 9, "v1_21_R6"), - new Version(21, 6, "v1_21_R5"), - new Version(21, 5, "v1_21_R4"), - new Version(21, 4, "v1_21_R3"), - new Version(21, 2, "v1_21_R2"), - new Version(21, 0, "v1_21_R1"), - new Version(20, 5, "v1_20_R4") - ); - - private static final List PACKS = List.of( - new Version(21, 5, "31100"), - new Version(21, 4, "31020"), - new Version(21, 2, "31000"), - new Version(20, 1, "3910") - ); - - //@done - private static final INMSBinding binding = bind(); - public static final String OVERWORLD_TAG = getTag(PACKS, "3910"); - - public static INMSBinding get() { - return binding; - } - - public static String getNMSTag() { - if (IrisSettings.get().getGeneral().isDisableNMS()) { - return "BUKKIT"; - } - - try { - String name = Bukkit.getServer().getClass().getCanonicalName(); - if (name.equals("org.bukkit.craftbukkit.CraftServer")) { - return getTag(REVISION, "BUKKIT"); - } else { - return name.split("\\Q.\\E")[3]; - } - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Failed to determine server nms version!"); - e.printStackTrace(); - } - - return "BUKKIT"; - } - - private static INMSBinding bind() { - String code = getNMSTag(); - Iris.info("Locating NMS Binding for " + code); - - try { - Class clazz = Class.forName("com.volmit.iris.core.nms."+code+".NMSBinding"); - try { - Object b = clazz.getConstructor().newInstance(); - if (b instanceof INMSBinding binding) { - Iris.info("Craftbukkit " + code + " <-> " + b.getClass().getSimpleName() + " Successfully Bound"); - return binding; - } - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } catch (ClassNotFoundException|NoClassDefFoundError classNotFoundException) {} - - Iris.info("Craftbukkit " + code + " <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound"); - Iris.warn("Note: Some features of Iris may not work the same since you are on an unsupported version of Minecraft."); - Iris.warn("Note: If this is a new version, expect an update soon."); - - return new NMSBinding1X(); - } - - private static String getTag(List versions, String def) { - var version = Bukkit.getServer().getBukkitVersion().split("-")[0].split("\\.", 3); - int major = 0; - int minor = 0; - - if (version.length > 2) { - major = Integer.parseInt(version[1]); - minor = Integer.parseInt(version[2]); - } else if (version.length == 2) { - major = Integer.parseInt(version[1]); - } - if (CURRENT.major < major || CURRENT.minor < minor) { - return versions.getFirst().tag; - } - - for (var p : versions) { - if (p.major > major || p.minor > minor) - continue; - return p.tag; - } - return def; - } - - private record Version(int major, int minor, String tag) {} -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java b/core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java deleted file mode 100644 index 9e62bc4de..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.nms; - -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.nms.container.BiomeColor; -import com.volmit.iris.core.nms.container.BlockProperty; -import com.volmit.iris.core.nms.container.StructurePlacement; -import com.volmit.iris.core.nms.datapack.DataVersion; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.math.Vector3d; -import com.volmit.iris.util.nbt.mca.palette.MCABiomeContainer; -import com.volmit.iris.util.nbt.mca.palette.MCAPaletteAccess; -import com.volmit.iris.util.nbt.tag.CompoundTag; -import org.bukkit.*; -import org.bukkit.block.Biome; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.generator.ChunkGenerator; -import org.bukkit.inventory.ItemStack; - -import java.awt.Color; -import java.util.List; - -public interface INMSBinding { - boolean hasTile(Material material); - - boolean hasTile(Location l); - - KMap serializeTile(Location location); - - void deserializeTile(KMap s, Location newPosition); - - CompoundTag serializeEntity(Entity location); - - Entity deserializeEntity(CompoundTag s, Location newPosition); - - boolean supportsCustomHeight(); - - Object getBiomeBaseFromId(int id); - - int getMinHeight(World world); - - boolean supportsCustomBiomes(); - - int getTrueBiomeBaseId(Object biomeBase); - - Object getTrueBiomeBase(Location location); - - String getTrueBiomeBaseKey(Location location); - - Object getCustomBiomeBaseFor(String mckey); - - Object getCustomBiomeBaseHolderFor(String mckey); - - int getBiomeBaseIdForKey(String key); - - String getKeyForBiomeBase(Object biomeBase); - - Object getBiomeBase(World world, Biome biome); - - Object getBiomeBase(Object registry, Biome biome); - - KList getBiomes(); - - boolean isBukkit(); - - int getBiomeId(Biome biome); - - MCABiomeContainer newBiomeContainer(int min, int max, int[] data); - - MCABiomeContainer newBiomeContainer(int min, int max); - - default World createWorld(WorldCreator c) { - if (c.generator() instanceof PlatformChunkGenerator gen - && missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) - throw new IllegalStateException("Missing dimension types to create world"); - return c.createWorld(); - } - - int countCustomBiomes(); - - void forceBiomeInto(int x, int y, int z, Object somethingVeryDirty, ChunkGenerator.BiomeGrid chunk); - - default boolean supportsDataPacks() { - return false; - } - - MCAPaletteAccess createPalette(); - - void injectBiomesFromMantle(Chunk e, Mantle mantle); - - ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException; - - void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException; - - Vector3d getBoundingbox(org.bukkit.entity.EntityType entity); - - Entity spawnEntity(Location location, EntityType type, CreatureSpawnEvent.SpawnReason reason); - - Color getBiomeColor(Location location, BiomeColor type); - - default DataVersion getDataVersion() { - return DataVersion.V1_19_2; - } - - default int getSpawnChunkCount(World world) { - return 441; - } - - KList getStructureKeys(); - - boolean missingDimensionTypes(String... keys); - - default boolean injectBukkit() { - return true; - } - - KMap> getBlockProperties(); - - void placeStructures(Chunk chunk); - - KMap collectStructures(); -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/container/AutoClosing.java b/core/src/main/java/com/volmit/iris/core/nms/container/AutoClosing.java deleted file mode 100644 index a4771ca84..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/container/AutoClosing.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.volmit.iris.core.nms.container; - -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.function.NastyRunnable; -import lombok.AllArgsConstructor; - -import java.util.concurrent.atomic.AtomicBoolean; - -@AllArgsConstructor -public class AutoClosing implements AutoCloseable { - private static final KMap CONTEXTS = new KMap<>(); - private final AtomicBoolean closed = new AtomicBoolean(); - private final NastyRunnable action; - - @Override - public void close() { - if (closed.getAndSet(true)) return; - try { - removeContext(); - action.run(); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - - public void storeContext() { - CONTEXTS.put(Thread.currentThread(), this); - } - - public void removeContext() { - CONTEXTS.values().removeIf(c -> c == this); - } - - public static void closeContext() { - AutoClosing closing = CONTEXTS.remove(Thread.currentThread()); - if (closing == null) return; - closing.close(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/container/BiomeColor.java b/core/src/main/java/com/volmit/iris/core/nms/container/BiomeColor.java deleted file mode 100644 index 58bc4a5a1..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/container/BiomeColor.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.volmit.iris.core.nms.container; - -public enum BiomeColor { - FOG, - WATER, - WATER_FOG, - SKY, - FOLIAGE, - GRASS -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/container/StructurePlacement.java b/core/src/main/java/com/volmit/iris/core/nms/container/StructurePlacement.java deleted file mode 100644 index 173251ddc..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/container/StructurePlacement.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.volmit.iris.core.nms.container; - -import com.google.gson.JsonObject; -import com.volmit.iris.engine.object.IrisJigsawStructurePlacement.SpreadType; -import lombok.*; -import lombok.experimental.Accessors; -import lombok.experimental.SuperBuilder; -import org.apache.commons.math3.fraction.Fraction; - -import java.util.List; - -@Data -@SuperBuilder -@Accessors(fluent = true, chain = true) -public abstract class StructurePlacement { - private final int salt; - private final float frequency; - private final List structures; - - public abstract JsonObject toJson(String structure); - - protected JsonObject createBase(String structure) { - JsonObject object = new JsonObject(); - object.addProperty("structure", structure); - object.addProperty("salt", salt); - return object; - } - - public int frequencyToSpacing() { - var frac = new Fraction(Math.max(Math.min(frequency, 1), 0.000000001f)); - return (int) Math.round(Math.sqrt((double) frac.getDenominator() / frac.getNumerator())); - } - - @Getter - @Accessors(chain = true, fluent = true) - @EqualsAndHashCode(callSuper = true) - @SuperBuilder - public static class RandomSpread extends StructurePlacement { - private final int spacing; - private final int separation; - private final SpreadType spreadType; - - @Override - public JsonObject toJson(String structure) { - JsonObject object = createBase(structure); - object.addProperty("spacing", Math.max(spacing, frequencyToSpacing())); - object.addProperty("separation", separation); - object.addProperty("spreadType", spreadType.name()); - return object; - } - } - - @Getter - @EqualsAndHashCode(callSuper = true) - @SuperBuilder - public static class ConcentricRings extends StructurePlacement { - private final int distance; - private final int spread; - private final int count; - - @Override - public JsonObject toJson(String structure) { - return null; - } - } - - public record Structure( - int weight, - String key, - List tags - ) { - - public boolean isValid() { - return weight > 0 && key != null; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/datapack/DataVersion.java b/core/src/main/java/com/volmit/iris/core/nms/datapack/DataVersion.java deleted file mode 100644 index 205ad5e31..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/datapack/DataVersion.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.volmit.iris.core.nms.datapack; - -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.nms.datapack.v1192.DataFixerV1192; -import com.volmit.iris.core.nms.datapack.v1206.DataFixerV1206; -import com.volmit.iris.core.nms.datapack.v1213.DataFixerV1213; -import com.volmit.iris.core.nms.datapack.v1217.DataFixerV1217; -import com.volmit.iris.util.collection.KMap; -import lombok.AccessLevel; -import lombok.Getter; - -import java.util.function.Supplier; - -//https://minecraft.wiki/w/Pack_format -@Getter -public enum DataVersion { - UNSUPPORTED("0.0.0", 0, () -> null), - V1_19_2("1.19.2", 10, DataFixerV1192::new), - V1_20_5("1.20.6", 41, DataFixerV1206::new), - V1_21_3("1.21.3", 57, DataFixerV1213::new), - V1_21_11("1.21.11", 75, DataFixerV1217::new); - private static final KMap cache = new KMap<>(); - @Getter(AccessLevel.NONE) - private final Supplier constructor; - private final String version; - private final int packFormat; - - DataVersion(String version, int packFormat, Supplier constructor) { - this.constructor = constructor; - this.packFormat = packFormat; - this.version = version; - } - - public IDataFixer get() { - return cache.computeIfAbsent(this, k -> constructor.get()); - } - - public static IDataFixer getDefault() { - return INMS.get().getDataVersion().get(); - } - - public static DataVersion getLatest() { - return values()[values().length - 1]; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/datapack/v1206/DataFixerV1206.java b/core/src/main/java/com/volmit/iris/core/nms/datapack/v1206/DataFixerV1206.java deleted file mode 100644 index eb8b59c28..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/datapack/v1206/DataFixerV1206.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.volmit.iris.core.nms.datapack.v1206; - -import com.volmit.iris.core.nms.datapack.v1192.DataFixerV1192; -import com.volmit.iris.engine.object.IrisBiomeCustom; -import com.volmit.iris.engine.object.IrisBiomeCustomSpawn; -import com.volmit.iris.engine.object.IrisBiomeCustomSpawnType; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; - -import java.util.Locale; - -public class DataFixerV1206 extends DataFixerV1192 { - @Override - public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) { - int spawnRarity = biome.getSpawnRarity(); - if (spawnRarity > 0) { - json.put("creature_spawn_probability", Math.min(spawnRarity/20d, 0.9999999)); - } else { - json.remove("creature_spawn_probability"); - } - - var spawns = biome.getSpawns(); - if (spawns != null && spawns.isNotEmpty()) { - JSONObject spawners = new JSONObject(); - KMap groups = new KMap<>(); - - for (IrisBiomeCustomSpawn i : spawns) { - JSONArray g = groups.computeIfAbsent(i.getGroup(), (k) -> new JSONArray()); - JSONObject o = new JSONObject(); - o.put("type", i.getType().getKey()); - o.put("weight", i.getWeight()); - o.put("minCount", i.getMinCount()); - o.put("maxCount", i.getMaxCount()); - g.put(o); - } - - for (IrisBiomeCustomSpawnType i : groups.k()) { - spawners.put(i.name().toLowerCase(Locale.ROOT), groups.get(i)); - } - - json.put("spawners", spawners); - } - return json; - } - - @Override - public void fixDimension(Dimension dimension, JSONObject json) { - super.fixDimension(dimension, json); - if (!(json.get("monster_spawn_light_level") instanceof JSONObject lightLevel)) - return; - var value = (JSONObject) lightLevel.remove("value"); - lightLevel.put("max_inclusive", value.get("max_inclusive")); - lightLevel.put("min_inclusive", value.get("min_inclusive")); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/datapack/v1213/DataFixerV1213.java b/core/src/main/java/com/volmit/iris/core/nms/datapack/v1213/DataFixerV1213.java deleted file mode 100644 index 7a762e77d..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/datapack/v1213/DataFixerV1213.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.volmit.iris.core.nms.datapack.v1213; - -import com.volmit.iris.core.nms.datapack.v1206.DataFixerV1206; -import com.volmit.iris.engine.object.IrisBiomeCustom; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; - -public class DataFixerV1213 extends DataFixerV1206 { - - @Override - public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) { - json = super.fixCustomBiome(biome, json); - json.put("carvers", new JSONArray()); - return json; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/datapack/v1217/DataFixerV1217.java b/core/src/main/java/com/volmit/iris/core/nms/datapack/v1217/DataFixerV1217.java deleted file mode 100644 index 3ea6947ae..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/datapack/v1217/DataFixerV1217.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.volmit.iris.core.nms.datapack.v1217; - -import com.volmit.iris.core.nms.datapack.v1213.DataFixerV1213; -import com.volmit.iris.engine.object.IrisBiomeCustom; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; - -import java.util.Map; - -public class DataFixerV1217 extends DataFixerV1213 { - private static final Map DIMENSIONS = Map.of( - Dimension.OVERWORLD, """ - { - "ambient_light": 0.0, - "attributes": { - "minecraft:audio/ambient_sounds": { - "mood": { - "block_search_extent": 8, - "offset": 2.0, - "sound": "minecraft:ambient.cave", - "tick_delay": 6000 - } - }, - "minecraft:audio/background_music": { - "creative": { - "max_delay": 24000, - "min_delay": 12000, - "sound": "minecraft:music.creative" - }, - "default": { - "max_delay": 24000, - "min_delay": 12000, - "sound": "minecraft:music.game" - } - }, - "minecraft:visual/cloud_color": "#ccffffff", - "minecraft:visual/fog_color": "#c0d8ff", - "minecraft:visual/sky_color": "#78a7ff" - }, - "timelines": "#minecraft:in_overworld" - }""", - Dimension.NETHER, """ - { - "ambient_light": 0.1, - "attributes": { - "minecraft:gameplay/sky_light_level": 4.0, - "minecraft:gameplay/snow_golem_melts": true, - "minecraft:visual/fog_end_distance": 96.0, - "minecraft:visual/fog_start_distance": 10.0, - "minecraft:visual/sky_light_color": "#7a7aff", - "minecraft:visual/sky_light_factor": 0.0 - }, - "cardinal_light": "nether", - "skybox": "none", - "timelines": "#minecraft:in_nether" - }""", - Dimension.END, """ - { - "ambient_light": 0.25, - "attributes": { - "minecraft:audio/ambient_sounds": { - "mood": { - "block_search_extent": 8, - "offset": 2.0, - "sound": "minecraft:ambient.cave", - "tick_delay": 6000 - } - }, - "minecraft:audio/background_music": { - "default": { - "max_delay": 24000, - "min_delay": 6000, - "replace_current_music": true, - "sound": "minecraft:music.end" - } - }, - "minecraft:visual/fog_color": "#181318", - "minecraft:visual/sky_color": "#000000", - "minecraft:visual/sky_light_color": "#e580ff", - "minecraft:visual/sky_light_factor": 0.0 - }, - "skybox": "end", - "timelines": "#minecraft:in_end" - }""" - ); - - @Override - public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) { - json = super.fixCustomBiome(biome, json); - var effects = json.getJSONObject("effects"); - var attributes = new JSONObject(); - - attributes.put("minecraft:visual/fog_color", effects.remove("fog_color")); - attributes.put("minecraft:visual/sky_color", effects.remove("sky_color")); - attributes.put("minecraft:visual/water_fog_color", effects.remove("water_fog_color")); - - JSONObject particle = (JSONObject) effects.remove("particle"); - if (particle != null) { - particle.put("particle", particle.remove("options")); - attributes.put("minecraft:visual/ambient_particles", new JSONArray() - .put(particle)); - } - json.put("attributes", attributes); - - return json; - } - - @Override - public void fixDimension(Dimension dimension, JSONObject json) { - super.fixDimension(dimension, json); - - var attributes = new JSONObject(); - if ((Boolean) json.remove("ultrawarm")) { - attributes.put("minecraft:gameplay/water_evaporates", true); - attributes.put("minecraft:gameplay/fast_lava", true); - attributes.put("minecraft:gameplay/snow_golem_melts", true); - attributes.put("minecraft:visual/default_dripstone_particle", new JSONObject() - .put("type", "minecraft:dripping_dripstone_lava")); - } - - if ((Boolean) json.remove("bed_works")) { - attributes.put("minecraft:gameplay/bed_rule", new JSONObject() - .put("can_set_spawn", "always") - .put("can_sleep", "when_dark") - .put("error_message", new JSONObject() - .put("translate", "block.minecraft.bed.no_sleep"))); - } else { - attributes.put("minecraft:gameplay/bed_rule", new JSONObject() - .put("can_set_spawn", "never") - .put("can_sleep", "never") - .put("explodes", true)); - } - - attributes.put("minecraft:gameplay/respawn_anchor_works", json.remove("respawn_anchor_works")); - attributes.put("minecraft:gameplay/piglins_zombify", json.remove("piglin_safe")); - attributes.put("minecraft:gameplay/can_start_raid", json.remove("has_raids")); - - var cloud_height = json.remove("cloud_height"); - if (cloud_height != null) attributes.put("minecraft:visual/cloud_height", cloud_height); - - boolean natural = (Boolean) json.remove("natural"); - attributes.put("minecraft:gameplay/nether_portal_spawns_piglin", natural); - if (natural != (dimension == Dimension.OVERWORLD)) { - attributes.put("minecraft:gameplay/eyeblossom_open", natural); - attributes.put("minecraft:gameplay/creaking_active", natural); - } - - //json.put("has_fixed_time", json.remove("fixed_time") != null); //TODO investigate - json.put("attributes", attributes); - - json.remove("effects"); - var defaults = new JSONObject(DIMENSIONS.get(dimension)); - merge(json, defaults); - } - - private void merge(JSONObject base, JSONObject override) { - for (String key : override.keySet()) { - switch (base.opt(key)) { - case null -> base.put(key, override.opt(key)); - case JSONObject base1 when override.opt(key) instanceof JSONObject override1 -> merge(base1, override1); - case JSONArray base1 when override.opt(key) instanceof JSONArray override1 -> { - for (Object o : override1) { - base1.put(o); - } - } - default -> {} - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java b/core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java deleted file mode 100644 index 40835b851..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.nms.v1X; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.nms.INMSBinding; -import com.volmit.iris.core.nms.container.BiomeColor; -import com.volmit.iris.core.nms.container.BlockProperty; -import com.volmit.iris.core.nms.datapack.DataVersion; -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.core.nms.container.StructurePlacement; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.math.Vector3d; -import com.volmit.iris.util.nbt.mca.palette.MCABiomeContainer; -import com.volmit.iris.util.nbt.mca.palette.MCAPaletteAccess; -import com.volmit.iris.util.nbt.tag.CompoundTag; -import org.bukkit.*; -import org.bukkit.block.Biome; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.generator.ChunkGenerator; -import org.bukkit.generator.structure.Structure; -import org.bukkit.inventory.ItemStack; - -import java.awt.Color; -import java.util.List; -import java.util.stream.StreamSupport; - -public class NMSBinding1X implements INMSBinding { - private static final boolean supportsCustomHeight = testCustomHeight(); - - @SuppressWarnings("ConstantConditions") - private static boolean testCustomHeight() { - try { - if (World.class.getDeclaredMethod("getMaxHeight") != null && World.class.getDeclaredMethod("getMinHeight") != null) - ; - { - return true; - } - } catch (Throwable ignored) { - - } - - return false; - } - - @Override - public boolean hasTile(Material material) { - return false; - } - - @Override - public boolean hasTile(Location l) { - return false; - } - - @Override - public KMap serializeTile(Location location) { - return null; - } - - @Override - public void deserializeTile(KMap s, Location newPosition) { - - } - - - @Override - public void injectBiomesFromMantle(Chunk e, Mantle mantle) { - - } - - @Override - public ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException { - return itemStack; - } - - @Override - public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException { - - } - - public Vector3d getBoundingbox() { - return null; - } - - @Override - public Entity spawnEntity(Location location, EntityType type, CreatureSpawnEvent.SpawnReason reason) { - return location.getWorld().spawnEntity(location, type); - } - - @Override - public Color getBiomeColor(Location location, BiomeColor type) { - return Color.GREEN; - } - - @Override - public KList getStructureKeys() { - var list = StreamSupport.stream(Registry.STRUCTURE.spliterator(), false) - .map(Structure::getKey) - .map(NamespacedKey::toString) - .toList(); - return new KList<>(list); - } - - @Override - public boolean missingDimensionTypes(String... keys) { - return false; - } - - @Override - public KMap> getBlockProperties() { - KMap> map = new KMap<>(); - for (Material m : Material.values()) { - if (m.isBlock()) map.put(m, List.of()); - } - return map; - } - - @Override - public void placeStructures(Chunk chunk) { - - } - - @Override - public KMap collectStructures() { - return new KMap<>(); - } - - @Override - public CompoundTag serializeEntity(Entity location) { - return null; - } - - @Override - public Entity deserializeEntity(CompoundTag s, Location newPosition) { - return null; - } - - @Override - public boolean supportsCustomHeight() { - return supportsCustomHeight; - } - - @Override - public Object getBiomeBaseFromId(int id) { - return null; - } - - @Override - public int getMinHeight(World world) { - return supportsCustomHeight ? world.getMinHeight() : 0; - } - - @Override - public boolean supportsCustomBiomes() { - return false; - } - - @Override - public int getTrueBiomeBaseId(Object biomeBase) { - return 0; - } - - @Override - public Object getTrueBiomeBase(Location location) { - return null; - } - - @Override - public String getTrueBiomeBaseKey(Location location) { - return null; - } - - @Override - public Object getCustomBiomeBaseFor(String mckey) { - return null; - } - - @Override - public Object getCustomBiomeBaseHolderFor(String mckey) { - return null; - } - - @Override - public int getBiomeBaseIdForKey(String key) { - return 0; - } - - @Override - public String getKeyForBiomeBase(Object biomeBase) { - return null; - } - - public Object getBiomeBase(World world, Biome biome) { - return null; - } - - @Override - public Object getBiomeBase(Object registry, Biome biome) { - return null; - } - - @Override - public KList getBiomes() { - return new KList<>(Biome.values()).qdel(Biome.CUSTOM); - } - - @Override - public boolean isBukkit() { - return true; - } - - @Override - public DataVersion getDataVersion() { - return DataVersion.UNSUPPORTED; - } - - @Override - public int getBiomeId(Biome biome) { - return biome.ordinal(); - } - - @Override - public MCABiomeContainer newBiomeContainer(int min, int max) { - Iris.error("Cannot use the custom biome data! Iris is incapable of using MCA generation on this version of minecraft!"); - - return null; - } - - @Override - public MCABiomeContainer newBiomeContainer(int min, int max, int[] v) { - Iris.error("Cannot use the custom biome data! Iris is incapable of using MCA generation on this version of minecraft!"); - - return null; - } - - @Override - public int countCustomBiomes() { - return 0; - } - - @Override - public void forceBiomeInto(int x, int y, int z, Object somethingVeryDirty, ChunkGenerator.BiomeGrid chunk) { - - } - - @Override - public Vector3d getBoundingbox(org.bukkit.entity.EntityType entity) { - return null; - } - - @Override - public MCAPaletteAccess createPalette() { - Iris.error("Cannot use the global data palette! Iris is incapable of using MCA generation on this version of minecraft!"); - return null; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/pack/IrisPackRepository.java b/core/src/main/java/com/volmit/iris/core/pack/IrisPackRepository.java deleted file mode 100644 index 71a68908b..000000000 --- a/core/src/main/java/com/volmit/iris/core/pack/IrisPackRepository.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.pack; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.jobs.DownloadJob; -import com.volmit.iris.util.scheduling.jobs.JobCollection; -import com.volmit.iris.util.scheduling.jobs.SingleJob; -import lombok.Builder; -import lombok.Data; -import org.zeroturnaround.zip.ZipUtil; -import org.zeroturnaround.zip.commons.FileUtils; - -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.util.UUID; - -@Data -@Builder -public class IrisPackRepository { - @Builder.Default - private String user = "IrisDimensions"; - - @Builder.Default - private String repo = "overworld"; - - @Builder.Default - private String branch = "master"; - - @Builder.Default - private String tag = ""; - - /** - * - */ - public static IrisPackRepository from(String g) { - // https://github.com/IrisDimensions/overworld - if (g.startsWith("https://github.com/")) { - String sub = g.split("\\Qgithub.com/\\E")[1]; - IrisPackRepository r = IrisPackRepository.builder() - .user(sub.split("\\Q/\\E")[0]) - .repo(sub.split("\\Q/\\E")[1]).build(); - - if (g.contains("/tree/")) { - r.setBranch(g.split("/tree/")[1]); - } - - return r; - } else if (g.contains("/")) { - String[] f = g.split("\\Q/\\E"); - - if (f.length == 1) { - return from(g); - } else if (f.length == 2) { - return IrisPackRepository.builder() - .user(f[0]) - .repo(f[1]) - .build(); - } else if (f.length >= 3) { - IrisPackRepository r = IrisPackRepository.builder() - .user(f[0]) - .repo(f[1]) - .build(); - - if (f[2].startsWith("#")) { - r.setTag(f[2].substring(1)); - } else { - r.setBranch(f[2]); - } - - return r; - } - } else { - return IrisPackRepository.builder() - .user("IrisDimensions") - .repo(g) - .branch(g.equals("overworld") ? "stable" : "master") - .build(); - } - - return null; - } - - public String toURL() { - if (!tag.trim().isEmpty()) { - return "https://codeload.github.com/" + user + "/" + repo + "/zip/refs/tags/" + tag; - } - - return "https://codeload.github.com/" + user + "/" + repo + "/zip/refs/heads/" + branch; - } - - public void install(VolmitSender sender, Runnable whenComplete) throws MalformedURLException { - File pack = Iris.instance.getDataFolderNoCreate(StudioSVC.WORKSPACE_NAME, getRepo()); - - if (!pack.exists()) { - File dl = new File(Iris.getTemp(), "dltk-" + UUID.randomUUID() + ".zip"); - File work = new File(Iris.getTemp(), "extk-" + UUID.randomUUID()); - new JobCollection(Form.capitalize(getRepo()), - new DownloadJob(toURL(), pack), - new SingleJob("Extracting", () -> ZipUtil.unpack(dl, work)), - new SingleJob("Installing", () -> { - try { - FileUtils.copyDirectory(work.listFiles()[0], pack); - } catch (IOException e) { - e.printStackTrace(); - } - })).execute(sender, whenComplete); - } else { - sender.sendMessage("Pack already exists!"); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/ChunkUpdater.java b/core/src/main/java/com/volmit/iris/core/pregenerator/ChunkUpdater.java deleted file mode 100644 index 1dbd26172..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/ChunkUpdater.java +++ /dev/null @@ -1,320 +0,0 @@ -package com.volmit.iris.core.pregenerator; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.service.PreservationSVC; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.plugin.chunk.TicketHolder; -import com.volmit.iris.util.profile.LoadBalancer; -import com.volmit.iris.util.scheduling.J; -import io.papermc.lib.PaperLib; -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.World; - -import java.io.File; - -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -public class ChunkUpdater { - private static final String REGION_PATH = "region" + File.separator + "r."; - private final AtomicBoolean paused = new AtomicBoolean(); - private final AtomicBoolean cancelled = new AtomicBoolean(); - private final TicketHolder holder; - private final RollingSequence chunksPerSecond = new RollingSequence(5); - private final AtomicInteger totalMaxChunks = new AtomicInteger(); - private final AtomicInteger chunksProcessed = new AtomicInteger(); - private final AtomicInteger chunksProcessedLast = new AtomicInteger(); - private final AtomicInteger chunksUpdated = new AtomicInteger(); - private final AtomicBoolean serverEmpty = new AtomicBoolean(true); - private final AtomicLong lastCpsTime = new AtomicLong(M.ms()); - private final int maxConcurrency = IrisSettings.get().getUpdater().getMaxConcurrency(); - private final int coreLimit = (int) Math.max(Runtime.getRuntime().availableProcessors() * IrisSettings.get().getUpdater().getThreadMultiplier(), 1); - private final Semaphore semaphore = new Semaphore(maxConcurrency); - private final LoadBalancer loadBalancer = new LoadBalancer(semaphore, maxConcurrency, IrisSettings.get().getUpdater().emptyMsRange); - private final AtomicLong startTime = new AtomicLong(); - private final Dimensions dimensions; - private final PregenTask task; - private final ExecutorService chunkExecutor = IrisSettings.get().getUpdater().isNativeThreads() ? Executors.newFixedThreadPool(coreLimit) : Executors.newVirtualThreadPerTaskExecutor(); - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - private final CountDownLatch latch; - private final Engine engine; - private final World world; - - public ChunkUpdater(World world) { - this.engine = IrisToolbelt.access(world).getEngine(); - this.world = world; - this.holder = Iris.tickets.getHolder(world); - this.dimensions = calculateWorldDimensions(new File(world.getWorldFolder(), "region")); - this.task = dimensions.task(); - this.totalMaxChunks.set(dimensions.count * 1024); - this.latch = new CountDownLatch(totalMaxChunks.get()); - } - - public String getName() { - return world.getName(); - } - - public int getChunks() { - return totalMaxChunks.get(); - } - - public void start() { - unloadAndSaveAllChunks(); - update(); - } - - public boolean pause() { - unloadAndSaveAllChunks(); - if (paused.get()) { - paused.set(false); - return false; - } else { - paused.set(true); - return true; - } - } - - public void stop() { - unloadAndSaveAllChunks(); - cancelled.set(true); - } - - private void update() { - Iris.info("Updating.."); - try { - startTime.set(System.currentTimeMillis()); - scheduler.scheduleAtFixedRate(() -> { - try { - if (!paused.get()) { - long eta = computeETA(); - int processed = chunksProcessed.get(); - double last = processed - chunksProcessedLast.getAndSet(processed); - double cps = last / ((M.ms() - lastCpsTime.getAndSet(M.ms())) / 1000d); - chunksPerSecond.put(cps); - double percentage = ((double) processed / (double) totalMaxChunks.get()) * 100; - if (!cancelled.get()) { - Iris.info("Updated: " + Form.f(processed) + " of " + Form.f(totalMaxChunks.get()) + " (%.0f%%) " + Form.f(chunksPerSecond.getAverage()) + "/s, ETA: " + Form.duration(eta, - 2), percentage); - } - } - } catch (Exception e) { - Iris.reportError(e); - e.printStackTrace(); - } - }, 0, 3, TimeUnit.SECONDS); - scheduler.scheduleAtFixedRate(() -> { - boolean empty = Bukkit.getOnlinePlayers().isEmpty(); - if (serverEmpty.getAndSet(empty) == empty) - return; - loadBalancer.setRange(empty ? IrisSettings.get().getUpdater().emptyMsRange : IrisSettings.get().getUpdater().defaultMsRange); - }, 0, 10, TimeUnit.SECONDS); - - var t = new Thread(() -> { - run(); - close(); - }, "Iris Chunk Updater - " + world.getName()); - t.setPriority(Thread.MAX_PRIORITY); - t.start(); - - Iris.service(PreservationSVC.class).register(t); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void close() { - try { - loadBalancer.close(); - semaphore.acquire(256); - - chunkExecutor.shutdown(); - chunkExecutor.awaitTermination(5, TimeUnit.SECONDS); - scheduler.shutdownNow(); - unloadAndSaveAllChunks(); - } catch (Exception ignored) {} - if (cancelled.get()) { - Iris.info("Updated: " + Form.f(chunksUpdated.get()) + " Chunks"); - Iris.info("Irritated: " + Form.f(chunksProcessed.get()) + " of " + Form.f(totalMaxChunks.get())); - Iris.info("Stopped updater."); - } else { - Iris.info("Processed: " + Form.f(chunksProcessed.get()) + " Chunks"); - Iris.info("Finished Updating: " + Form.f(chunksUpdated.get()) + " Chunks"); - } - } - - private void run() { - task.iterateRegions((rX, rZ) -> { - if (cancelled.get()) - return; - - while (paused.get()) { - J.sleep(50); - } - - if (rX < dimensions.min.getX() || - rX > dimensions.max.getX() || - rZ < dimensions.min.getZ() || - rZ > dimensions.max.getZ() || - !new File(world.getWorldFolder(), REGION_PATH + rX + "." + rZ + ".mca").exists() - ) return; - - task.iterateChunks(rX, rZ, (x, z) -> { - while (paused.get() && !cancelled.get()) { - J.sleep(50); - } - - try { - semaphore.acquire(); - } catch (InterruptedException ignored) { - return; - } - chunkExecutor.submit(() -> { - try { - if (!cancelled.get()) - processChunk(x, z); - } finally { - latch.countDown(); - semaphore.release(); - } - }); - }); - }); - } - - private void processChunk(int x, int z) { - if (!loadChunksIfGenerated(x, z)) { - chunksProcessed.getAndIncrement(); - return; - } - - var mc = engine.getMantle().getMantle().getChunk(x, z).use(); - try { - Chunk c = world.getChunkAt(x, z); - engine.updateChunk(c); - - removeTickets(x, z); - } finally { - chunksUpdated.incrementAndGet(); - chunksProcessed.getAndIncrement(); - mc.release(); - } - } - - private boolean loadChunksIfGenerated(int x, int z) { - if (engine.getMantle().getMantle().hasFlag(x, z, MantleFlag.ETCHED)) - return false; - - for (int dx = -1; dx <= 1; dx++) { - for (int dz = -1; dz <= 1; dz++) { - if (!PaperLib.isChunkGenerated(world, x + dx, z + dz)) { - return false; - } - } - } - - AtomicBoolean generated = new AtomicBoolean(true); - CountDownLatch latch = new CountDownLatch(9); - for (int dx = -1; dx <= 1; dx++) { - for (int dz = -1; dz <= 1; dz++) { - int xx = x + dx; - int zz = z + dz; - PaperLib.getChunkAtAsync(world, xx, zz, false, true) - .thenAccept(chunk -> { - if (chunk == null || !chunk.isGenerated()) { - latch.countDown(); - generated.set(false); - return; - } - holder.addTicket(chunk); - latch.countDown(); - }); - } - } - - try { - latch.await(); - } catch (InterruptedException e) { - Iris.info("Interrupted while waiting for chunks to load"); - } - - if (generated.get()) return true; - removeTickets(x, z); - return false; - } - - private void removeTickets(int x, int z) { - for (int xx = -1; xx <= 1; xx++) { - for (int zz = -1; zz <= 1; zz++) { - holder.removeTicket(x + xx, z + zz); - } - } - } - - private void unloadAndSaveAllChunks() { - try { - J.sfut(() -> { - if (world == null) { - Iris.warn("World was null somehow..."); - return; - } - - world.save(); - }).get(); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - private long computeETA() { - return (long) (totalMaxChunks.get() > 1024 ? // Generated chunks exceed 1/8th of total? - // If yes, use smooth function (which gets more accurate over time since its less sensitive to outliers) - ((totalMaxChunks.get() - chunksProcessed.get()) * ((double) (M.ms() - startTime.get()) / (double) chunksProcessed.get())) : - // If no, use quick function (which is less accurate over time but responds better to the initial delay) - ((totalMaxChunks.get() - chunksProcessed.get()) / chunksPerSecond.getAverage()) * 1000 - ); - } - - private Dimensions calculateWorldDimensions(File regionDir) { - File[] files = regionDir.listFiles((dir, name) -> name.endsWith(".mca")); - - int minX = Integer.MAX_VALUE; - int maxX = Integer.MIN_VALUE; - int minZ = Integer.MAX_VALUE; - int maxZ = Integer.MIN_VALUE; - - for (File file : files) { - String[] parts = file.getName().split("\\."); - int x = Integer.parseInt(parts[1]); - int z = Integer.parseInt(parts[2]); - - minX = Math.min(minX, x); - maxX = Math.max(maxX, x); - minZ = Math.min(minZ, z); - maxZ = Math.max(maxZ, z); - } - int oX = minX + ((maxX - minX) / 2); - int oZ = minZ + ((maxZ - minZ) / 2); - - int height = maxX - minX + 1; - int width = maxZ - minZ + 1; - - return new Dimensions(new Position2(minX, minZ), new Position2(maxX, maxZ), height * width, PregenTask.builder() - .radiusZ((int) Math.ceil(width / 2d * 512)) - .radiusX((int) Math.ceil(height / 2d * 512)) - .center(new Position2(oX, oZ)) - .build()); - } - - private record Dimensions(Position2 min, Position2 max, int count, PregenTask task) { } -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/DeepSearchPregenerator.java b/core/src/main/java/com/volmit/iris/core/pregenerator/DeepSearchPregenerator.java deleted file mode 100644 index e9d2b8f74..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/DeepSearchPregenerator.java +++ /dev/null @@ -1,274 +0,0 @@ -package com.volmit.iris.core.pregenerator; - -import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.math.Spiraler; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import lombok.Data; -import lombok.Getter; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.world.WorldUnloadEvent; -import org.bukkit.scheduler.BukkitRunnable; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantLock; - -public class DeepSearchPregenerator extends Thread implements Listener { - @Getter - private static DeepSearchPregenerator instance; - private final DeepSearchJob job; - private final File destination; - private final int maxPosition; - private World world; - private final ChronoLatch latch; - private static AtomicInteger foundChunks; - private final AtomicInteger foundLast; - private final AtomicInteger foundTotalChunks; - private final AtomicLong startTime; - private final RollingSequence chunksPerSecond; - private final RollingSequence chunksPerMinute; - private final AtomicInteger chunkCachePos; - private final AtomicInteger chunkCacheSize; - private int pos; - private final AtomicInteger foundCacheLast; - private final AtomicInteger foundCache; - private LinkedHashMap chunkCache; - private KList chunkQueue; - private final ReentrantLock cacheLock; - - private static final Map jobs = new HashMap<>(); - - public DeepSearchPregenerator(DeepSearchJob job, File destination) { - this.job = job; - this.chunkCacheSize = new AtomicInteger(); // todo - this.chunkCachePos = new AtomicInteger(1000); - this.foundCacheLast = new AtomicInteger(); - this.foundCache = new AtomicInteger(); - this.cacheLock = new ReentrantLock(); - this.destination = destination; - this.chunkCache = new LinkedHashMap<>(); - this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - }).count(); - this.world = Bukkit.getWorld(job.getWorld().getUID()); - this.chunkQueue = new KList<>(); - this.latch = new ChronoLatch(3000); - this.startTime = new AtomicLong(M.ms()); - this.chunksPerSecond = new RollingSequence(10); - this.chunksPerMinute = new RollingSequence(10); - foundChunks = new AtomicInteger(0); - this.foundLast = new AtomicInteger(0); - this.foundTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2))); - - this.pos = 0; - jobs.put(job.getWorld().getName(), job); - DeepSearchPregenerator.instance = this; - } - - @EventHandler - public void on(WorldUnloadEvent e) { - if (e.getWorld().equals(world)) { - interrupt(); - } - } - - public void run() { - while (!interrupted()) { - tick(); - } - try { - saveNow(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public void tick() { - DeepSearchJob job = jobs.get(world.getName()); - // chunkCache(); //todo finish this - if (latch.flip() && !job.paused) { - if (cacheLock.isLocked()) { - Iris.info("DeepFinder: Caching: " + chunkCachePos.get() + " Of " + chunkCacheSize.get()); - } else { - long eta = computeETA(); - save(); - int secondGenerated = foundChunks.get() - foundLast.get(); - foundLast.set(foundChunks.get()); - secondGenerated = secondGenerated / 3; - chunksPerSecond.put(secondGenerated); - chunksPerMinute.put(secondGenerated * 60); - Iris.info("DeepFinder: " + C.IRIS + world.getName() + C.RESET + " Searching: " + Form.f(foundChunks.get()) + " of " + Form.f(foundTotalChunks.get()) + " " + Form.f((int) chunksPerSecond.getAverage()) + "/s ETA: " + Form.duration((double) eta, 2)); - } - - } - if (foundChunks.get() >= foundTotalChunks.get()) { - Iris.info("Completed DeepSearch!"); - interrupt(); - } - } - - private long computeETA() { - return (long) ((foundTotalChunks.get() - foundChunks.get()) / chunksPerSecond.getAverage()) * 1000; - // todo broken - } - - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - private void queueSystem(Position2 chunk) { - if (chunkQueue.isEmpty()) { - for (int limit = 512; limit != 0; limit--) { - pos = job.getPosition() + 1; - chunkQueue.add(getChunk(pos)); - } - } else { - //MCAUtil.read(); - - } - - - } - - private void findInChunk(World world, int x, int z) throws IOException { - int xx = x * 16; - int zz = z * 16; - Engine engine = IrisToolbelt.access(world).getEngine(); - for (int i = 0; i < 16; i++) { - for (int j = 0; j < 16; j++) { - int height = engine.getHeight(xx + i, zz + j); - if (height > 300) { - File found = new File("plugins" + "iris" + "found.txt"); - FileWriter writer = new FileWriter(found); - if (!found.exists()) { - found.createNewFile(); - } - IrisBiome biome = engine.getBiome(xx, engine.getHeight(), zz); - Iris.info("Found at! " + xx + ", " + zz + "Biome ID: " + biome.getName() + ", "); - writer.write("Biome at: X: " + xx + " Z: " + zz + "Biome ID: " + biome.getName() + ", "); - return; - } - } - } - } - - public Position2 getChunk(int position) { - int p = -1; - AtomicInteger xx = new AtomicInteger(); - AtomicInteger zz = new AtomicInteger(); - Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - xx.set(x); - zz.set(z); - }); - - while (s.hasNext() && p++ < position) { - s.next(); - } - - return new Position2(xx.get(), zz.get()); - } - - public void save() { - J.a(() -> { - try { - saveNow(); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } - - public static void setPausedDeep(World world) { - DeepSearchJob job = jobs.get(world.getName()); - if (isPausedDeep(world)){ - job.paused = false; - } else { - job.paused = true; - } - - if ( job.paused) { - Iris.info(C.BLUE + "DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Paused"); - } else { - Iris.info(C.BLUE + "DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Resumed"); - } - } - - public static boolean isPausedDeep(World world) { - DeepSearchJob job = jobs.get(world.getName()); - return job != null && job.isPaused(); - } - - public void shutdownInstance(World world) throws IOException { - Iris.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Shutting down.."); - DeepSearchJob job = jobs.get(world.getName()); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File deepFile = new File(worldDirectory, "DeepSearch.json"); - - if (job == null) { - Iris.error("No DeepSearch job found for world: " + world.getName()); - return; - } - - try { - if (!job.isPaused()) { - job.setPaused(true); - } - save(); - jobs.remove(world.getName()); - new BukkitRunnable() { - @Override - public void run() { - while (deepFile.exists()){ - deepFile.delete(); - J.sleep(1000); - } - Iris.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); - } - }.runTaskLater(Iris.instance, 20L); - } catch (Exception e) { - Iris.error("Failed to shutdown DeepSearch for " + world.getName()); - e.printStackTrace(); - } finally { - saveNow(); - interrupt(); - } - } - - - public void saveNow() throws IOException { - IO.writeAll(this.destination, new Gson().toJson(job)); - } - - @Data - @lombok.Builder - public static class DeepSearchJob { - private World world; - @lombok.Builder.Default - private int radiusBlocks = 5000; - @lombok.Builder.Default - private int position = 0; - @lombok.Builder.Default - boolean paused = false; - } -} - diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/com/volmit/iris/core/pregenerator/IrisPregenerator.java deleted file mode 100644 index e90307dc9..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/IrisPregenerator.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.pregenerator; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.tools.IrisPackBenchmarking; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.Looper; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - - -public class IrisPregenerator { - private static final double INVALID = 9223372036854775807d; - private final PregenTask task; - private final PregeneratorMethod generator; - private final PregenListener listener; - private final Looper ticker; - private final AtomicBoolean paused; - private final AtomicBoolean shutdown; - private final RollingSequence cachedPerSecond; - private final RollingSequence chunksPerSecond; - private final RollingSequence chunksPerMinute; - private final RollingSequence regionsPerMinute; - private final KList chunksPerSecondHistory; - private final AtomicLong generated; - private final AtomicLong generatedLast; - private final AtomicLong generatedLastMinute; - private final AtomicLong cached; - private final AtomicLong cachedLast; - private final AtomicLong cachedLastMinute; - private final AtomicLong totalChunks; - private final AtomicLong startTime; - private final ChronoLatch minuteLatch; - private final AtomicReference currentGeneratorMethod; - private final KSet generatedRegions; - private final KSet retry; - private final KSet net; - private final ChronoLatch cl; - private final ChronoLatch saveLatch = new ChronoLatch(30000); - private final IrisPackBenchmarking benchmarking; - - public IrisPregenerator(PregenTask task, PregeneratorMethod generator, PregenListener listener) { - benchmarking = IrisPackBenchmarking.getInstance(); - this.listener = listenify(listener); - cl = new ChronoLatch(5000); - generatedRegions = new KSet<>(); - this.shutdown = new AtomicBoolean(false); - this.paused = new AtomicBoolean(false); - this.task = task; - this.generator = generator; - retry = new KSet<>(); - net = new KSet<>(); - currentGeneratorMethod = new AtomicReference<>("Void"); - minuteLatch = new ChronoLatch(60000, false); - cachedPerSecond = new RollingSequence(5); - chunksPerSecond = new RollingSequence(10); - chunksPerMinute = new RollingSequence(10); - regionsPerMinute = new RollingSequence(10); - chunksPerSecondHistory = new KList<>(); - generated = new AtomicLong(0); - generatedLast = new AtomicLong(0); - generatedLastMinute = new AtomicLong(0); - cached = new AtomicLong(); - cachedLast = new AtomicLong(0); - cachedLastMinute = new AtomicLong(0); - totalChunks = new AtomicLong(0); - task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet()); - startTime = new AtomicLong(M.ms()); - ticker = new Looper() { - @Override - protected long loop() { - long eta = computeETA(); - - long secondCached = cached.get() - cachedLast.get(); - cachedLast.set(cached.get()); - cachedPerSecond.put(secondCached); - - long secondGenerated = generated.get() - generatedLast.get() - secondCached; - generatedLast.set(generated.get()); - if (secondCached == 0 || secondGenerated != 0) { - chunksPerSecond.put(secondGenerated); - chunksPerSecondHistory.add((int) secondGenerated); - } - - if (minuteLatch.flip()) { - long minuteCached = cached.get() - cachedLastMinute.get(); - cachedLastMinute.set(cached.get()); - - long minuteGenerated = generated.get() - generatedLastMinute.get() - minuteCached; - generatedLastMinute.set(generated.get()); - if (minuteCached == 0 || minuteGenerated != 0) { - chunksPerMinute.put(minuteGenerated); - regionsPerMinute.put((double) minuteGenerated / 1024D); - } - } - boolean cached = cachedPerSecond.getAverage() != 0; - - listener.onTick( - cached ? cachedPerSecond.getAverage() : chunksPerSecond.getAverage(), - chunksPerMinute.getAverage(), - regionsPerMinute.getAverage(), - (double) generated.get() / (double) totalChunks.get(), generated.get(), - totalChunks.get(), - totalChunks.get() - generated.get(), eta, M.ms() - startTime.get(), currentGeneratorMethod.get(), - cached); - - if (cl.flip()) { - double percentage = ((double) generated.get() / (double) totalChunks.get()) * 100; - - Iris.info("%s: %s of %s (%.0f%%), %s/s ETA: %s", - benchmarking != null ? "Benchmarking" : "Pregen", - Form.f(generated.get()), - Form.f(totalChunks.get()), - percentage, - cached ? - "Cached " + Form.f((int) cachedPerSecond.getAverage()) : - Form.f((int) chunksPerSecond.getAverage()), - Form.duration(eta, 2) - ); - } - return 1000; - } - }; - } - - private long computeETA() { - double d = (long) (generated.get() > 1024 ? // Generated chunks exceed 1/8th of total? - // If yes, use smooth function (which gets more accurate over time since its less sensitive to outliers) - ((totalChunks.get() - generated.get()) * ((double) (M.ms() - startTime.get()) / (double) generated.get())) : - // If no, use quick function (which is less accurate over time but responds better to the initial delay) - ((totalChunks.get() - generated.get()) / chunksPerSecond.getAverage()) * 1000); - return Double.isFinite(d) && d != INVALID ? (long) d : 0; - } - - - public void close() { - shutdown.set(true); - } - - public void start() { - init(); - ticker.start(); - checkRegions(); - var p = PrecisionStopwatch.start(); - task.iterateRegions((x, z) -> visitRegion(x, z, true)); - task.iterateRegions((x, z) -> visitRegion(x, z, false)); - Iris.info("Pregen took " + Form.duration((long) p.getMilliseconds())); - shutdown(); - if (benchmarking == null) { - Iris.info(C.IRIS + "Pregen stopped."); - } else { - benchmarking.finishedBenchmark(chunksPerSecondHistory); - } - } - - private void checkRegions() { - task.iterateRegions(this::checkRegion); - } - - private void init() { - generator.init(); - generator.save(); - } - - private void shutdown() { - listener.onSaving(); - generator.close(); - ticker.interrupt(); - listener.onClose(); - Mantle mantle = getMantle(); - if (mantle != null) { - mantle.trim(0, 0); - } - } - - private void visitRegion(int x, int z, boolean regions) { - while (paused.get() && !shutdown.get()) { - J.sleep(50); - } - - if (shutdown.get()) { - listener.onRegionSkipped(x, z); - return; - } - - Position2 pos = new Position2(x, z); - - if (generatedRegions.contains(pos)) { - return; - } - - currentGeneratorMethod.set(generator.getMethod(x, z)); - boolean hit = false; - if (generator.supportsRegions(x, z, listener) && regions) { - hit = true; - listener.onRegionGenerating(x, z); - generator.generateRegion(x, z, listener); - } else if (!regions) { - hit = true; - listener.onRegionGenerating(x, z); - task.iterateChunks(x, z, (xx, zz) -> { - while (paused.get() && !shutdown.get()) { - J.sleep(50); - } - - generator.generateChunk(xx, zz, listener); - }); - } - - if (hit) { - listener.onRegionGenerated(x, z); - - if (saveLatch.flip()) { - listener.onSaving(); - generator.save(); - } - - generatedRegions.add(pos); - checkRegions(); - } - } - - private void checkRegion(int x, int z) { - if (generatedRegions.contains(new Position2(x, z))) { - return; - } - - generator.supportsRegions(x, z, listener); - } - - public void pause() { - paused.set(true); - } - - public void resume() { - paused.set(false); - } - - private PregenListener listenify(PregenListener listener) { - return new PregenListener() { - @Override - public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) { - listener.onTick(chunksPerSecond, chunksPerMinute, regionsPerMinute, percent, generated, totalChunks, chunksRemaining, eta, elapsed, method, cached); - } - - @Override - public void onChunkGenerating(int x, int z) { - listener.onChunkGenerating(x, z); - } - - @Override - public void onChunkGenerated(int x, int z, boolean c) { - listener.onChunkGenerated(x, z, c); - generated.addAndGet(1); - if (c) cached.addAndGet(1); - } - - @Override - public void onRegionGenerated(int x, int z) { - listener.onRegionGenerated(x, z); - } - - @Override - public void onRegionGenerating(int x, int z) { - listener.onRegionGenerating(x, z); - } - - @Override - public void onChunkCleaned(int x, int z) { - listener.onChunkCleaned(x, z); - } - - @Override - public void onRegionSkipped(int x, int z) { - listener.onRegionSkipped(x, z); - } - - @Override - public void onNetworkStarted(int x, int z) { - net.add(new Position2(x, z)); - } - - @Override - public void onNetworkFailed(int x, int z) { - retry.add(new Position2(x, z)); - } - - @Override - public void onNetworkReclaim(int revert) { - generated.addAndGet(-revert); - } - - @Override - public void onNetworkGeneratedChunk(int x, int z) { - generated.addAndGet(1); - } - - @Override - public void onNetworkDownloaded(int x, int z) { - net.remove(new Position2(x, z)); - } - - @Override - public void onClose() { - listener.onClose(); - } - - @Override - public void onSaving() { - listener.onSaving(); - } - - @Override - public void onChunkExistsInRegionGen(int x, int z) { - listener.onChunkExistsInRegionGen(x, z); - } - }; - } - - public boolean paused() { - return paused.get(); - } - - public Mantle getMantle() { - return generator.getMantle(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/LazyPregenerator.java b/core/src/main/java/com/volmit/iris/core/pregenerator/LazyPregenerator.java deleted file mode 100644 index dc58f2ed5..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/LazyPregenerator.java +++ /dev/null @@ -1,285 +0,0 @@ -package com.volmit.iris.core.pregenerator; - -import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.math.Spiraler; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import io.papermc.lib.PaperLib; -import lombok.Data; -import lombok.Getter; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.world.WorldUnloadEvent; -import org.bukkit.scheduler.BukkitRunnable; - -import java.io.File; -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -import java.util.HashMap; -import java.util.Map; - -public class LazyPregenerator extends Thread implements Listener { - @Getter - private static LazyPregenerator instance; - private final LazyPregenJob job; - private final File destination; - private final int maxPosition; - private World world; - private final long rate; - private final ChronoLatch latch; - private static AtomicInteger lazyGeneratedChunks; - private final AtomicInteger generatedLast; - private final AtomicInteger lazyTotalChunks; - private final AtomicLong startTime; - private final RollingSequence chunksPerSecond; - private final RollingSequence chunksPerMinute; - - private static final Map jobs = new HashMap<>(); - - public LazyPregenerator(LazyPregenJob job, File destination) { - this.job = job; - this.destination = destination; - this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - }).count(); - this.world = Bukkit.getWorld(job.getWorld()); - this.rate = Math.round((1D / (job.getChunksPerMinute() / 60D)) * 1000D); - this.latch = new ChronoLatch(15000); - this.startTime = new AtomicLong(M.ms()); - this.chunksPerSecond = new RollingSequence(10); - this.chunksPerMinute = new RollingSequence(10); - lazyGeneratedChunks = new AtomicInteger(0); - this.generatedLast = new AtomicInteger(0); - this.lazyTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2))); - jobs.put(job.getWorld(), job); - LazyPregenerator.instance = this; - } - - public LazyPregenerator(File file) throws IOException { - this(new Gson().fromJson(IO.readAll(file), LazyPregenJob.class), file); - } - - public static void loadLazyGenerators() { - for (World i : Bukkit.getWorlds()) { - File lazygen = new File(i.getWorldFolder(), "lazygen.json"); - if (lazygen.exists()) { - try { - LazyPregenerator p = new LazyPregenerator(lazygen); - p.start(); - Iris.info("Started Lazy Pregenerator: " + p.job); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - } - - @EventHandler - public void on(WorldUnloadEvent e) { - if (e.getWorld().equals(world)) { - interrupt(); - } - } - - public void run() { - while (!interrupted()) { - J.sleep(rate); - tick(); - } - - try { - saveNow(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public void tick() { - LazyPregenJob job = jobs.get(world.getName()); - if (latch.flip() && !job.paused) { - long eta = computeETA(); - save(); - int secondGenerated = lazyGeneratedChunks.get() - generatedLast.get(); - generatedLast.set(lazyGeneratedChunks.get()); - secondGenerated = secondGenerated / 15; - chunksPerSecond.put(secondGenerated); - chunksPerMinute.put(secondGenerated * 60); - if (!job.isSilent()) { - Iris.info("LazyGen: " + C.IRIS + world.getName() + C.RESET + " RTT: " + Form.f(lazyGeneratedChunks.get()) + " of " + Form.f(lazyTotalChunks.get()) + " " + Form.f((int) chunksPerMinute.getAverage()) + "/m ETA: " + Form.duration((double) eta, 2)); - } - } - - if (lazyGeneratedChunks.get() >= lazyTotalChunks.get()) { - if (job.isHealing()) { - int pos = (job.getHealingPosition() + 1) % maxPosition; - job.setHealingPosition(pos); - tickRegenerate(getChunk(pos)); - } else { - Iris.info("Completed Lazy Gen!"); - interrupt(); - } - } else { - int pos = job.getPosition() + 1; - job.setPosition(pos); - if (!job.paused) { - tickGenerate(getChunk(pos)); - } - } - } - - private long computeETA() { - return (long) ((lazyTotalChunks.get() - lazyGeneratedChunks.get()) / chunksPerMinute.getAverage()) * 1000; - // todo broken - } - - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - private void tickGenerate(Position2 chunk) { - executorService.submit(() -> { - CountDownLatch latch = new CountDownLatch(1); - if (PaperLib.isPaper()) { - PaperLib.getChunkAtAsync(world, chunk.getX(), chunk.getZ(), true) - .thenAccept((i) -> { - Iris.verbose("Generated Async " + chunk); - latch.countDown(); - }); - } else { - J.s(() -> { - world.getChunkAt(chunk.getX(), chunk.getZ()); - Iris.verbose("Generated " + chunk); - latch.countDown(); - }); - } - try { - latch.await(); - } catch (InterruptedException ignored) {} - lazyGeneratedChunks.addAndGet(1); - }); - } - - private void tickRegenerate(Position2 chunk) { - J.s(() -> world.regenerateChunk(chunk.getX(), chunk.getZ())); - Iris.verbose("Regenerated " + chunk); - } - - public Position2 getChunk(int position) { - int p = -1; - AtomicInteger xx = new AtomicInteger(); - AtomicInteger zz = new AtomicInteger(); - Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - xx.set(x); - zz.set(z); - }); - - while (s.hasNext() && p++ < position) { - s.next(); - } - - return new Position2(xx.get(), zz.get()); - } - - public void save() { - J.a(() -> { - try { - saveNow(); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } - - public static void setPausedLazy(World world) { - LazyPregenJob job = jobs.get(world.getName()); - if (isPausedLazy(world)){ - job.paused = false; - } else { - job.paused = true; - } - - if ( job.paused) { - Iris.info(C.BLUE + "LazyGen: " + C.IRIS + world.getName() + C.BLUE + " Paused"); - } else { - Iris.info(C.BLUE + "LazyGen: " + C.IRIS + world.getName() + C.BLUE + " Resumed"); - } - } - - public static boolean isPausedLazy(World world) { - LazyPregenJob job = jobs.get(world.getName()); - return job != null && job.isPaused(); - } - - public void shutdownInstance(World world) throws IOException { - Iris.info("LazyGen: " + C.IRIS + world.getName() + C.BLUE + " Shutting down.."); - LazyPregenJob job = jobs.get(world.getName()); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File lazyFile = new File(worldDirectory, "lazygen.json"); - - if (job == null) { - Iris.error("No Lazygen job found for world: " + world.getName()); - return; - } - - try { - if (!job.isPaused()) { - job.setPaused(true); - } - save(); - jobs.remove(world.getName()); - new BukkitRunnable() { - @Override - public void run() { - while (lazyFile.exists()){ - lazyFile.delete(); - J.sleep(1000); - } - Iris.info("LazyGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); - } - }.runTaskLater(Iris.instance, 20L); - } catch (Exception e) { - Iris.error("Failed to shutdown Lazygen for " + world.getName()); - e.printStackTrace(); - } finally { - saveNow(); - interrupt(); - } - } - - - public void saveNow() throws IOException { - IO.writeAll(this.destination, new Gson().toJson(job)); - } - - @Data - @lombok.Builder - public static class LazyPregenJob { - private String world; - @lombok.Builder.Default - private int healingPosition = 0; - @lombok.Builder.Default - private boolean healing = false; - @lombok.Builder.Default - private int chunksPerMinute = 32; - @lombok.Builder.Default - private int radiusBlocks = 5000; - @lombok.Builder.Default - private int position = 0; - @lombok.Builder.Default - boolean silent = false; - @lombok.Builder.Default - boolean paused = false; - } -} - diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenTask.java b/core/src/main/java/com/volmit/iris/core/pregenerator/PregenTask.java deleted file mode 100644 index db8d76e7a..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenTask.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.pregenerator; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.Spiraled; -import com.volmit.iris.util.math.Spiraler; -import lombok.Builder; -import lombok.Data; - -import java.util.Comparator; - -@Builder -@Data -public class PregenTask { - private static final Position2 ZERO = new Position2(0, 0); - private static final KMap> ORDERS = new KMap<>(); - - @Builder.Default - private final boolean gui = false; - @Builder.Default - private final Position2 center = new Position2(0, 0); - @Builder.Default - private final int radiusX = 1; - @Builder.Default - private final int radiusZ = 1; - - private final Bounds bounds = new Bounds(); - - protected PregenTask(boolean gui, Position2 center, int radiusX, int radiusZ) { - this.gui = gui; - this.center = new ProxiedPos(center); - this.radiusX = radiusX; - this.radiusZ = radiusZ; - bounds.update(); - } - - public static void iterateRegion(int xr, int zr, Spiraled s, Position2 pull) { - for (Position2 i : ORDERS.computeIfAbsent(pull, PregenTask::computeOrder)) { - s.on(i.getX() + (xr << 5), i.getZ() + (zr << 5)); - } - } - - public static void iterateRegion(int xr, int zr, Spiraled s) { - iterateRegion(xr, zr, s, new Position2(-(xr << 5), -(zr << 5))); - } - - private static KList computeOrder(Position2 pull) { - KList p = new KList<>(); - new Spiraler(33, 33, (x, z) -> { - int xx = (x + 15); - int zz = (z + 15); - if (xx < 0 || xx > 31 || zz < 0 || zz > 31) { - return; - } - - p.add(new Position2(xx, zz)); - }).drain(); - p.sort(Comparator.comparing((i) -> i.distance(pull))); - - return p; - } - - public void iterateRegions(Spiraled s) { - var bound = bounds.region(); - new Spiraler(bound.sizeX, bound.sizeZ, ((x, z) -> { - if (bound.check(x, z)) s.on(x, z); - })).setOffset(center.getX() >> 9, center.getZ() >> 9).drain(); - } - - public void iterateChunks(int rX, int rZ, Spiraled s) { - var bound = bounds.chunk(); - iterateRegion(rX, rZ, ((x, z) -> { - if (bound.check(x, z)) s.on(x, z); - })); - } - - public void iterateAllChunks(Spiraled s) { - iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s))); - } - - private class Bounds { - private Bound chunk = null; - private Bound region = null; - - public void update() { - int maxX = center.getX() + radiusX; - int maxZ = center.getZ() + radiusZ; - int minX = center.getX() - radiusX; - int minZ = center.getZ() - radiusZ; - - chunk = new Bound(minX >> 4, minZ >> 4, Math.ceilDiv(maxX, 16), Math.ceilDiv(maxZ, 16)); - region = new Bound(minX >> 9, minZ >> 9, Math.ceilDiv(maxX, 512), Math.ceilDiv(maxZ, 512)); - } - - public Bound chunk() { - if (chunk == null) update(); - return chunk; - } - - public Bound region() { - if (region == null) update(); - return region; - } - } - - private record Bound(int minX, int maxX, int minZ, int maxZ, int sizeX, int sizeZ) { - private Bound(int minX, int minZ, int maxX, int maxZ) { - this(minX, maxX, minZ, maxZ, maxZ - minZ + 1, maxZ - minZ + 1); - } - - boolean check(int x, int z) { - return x >= minX && x <= maxX && z >= minZ && z <= maxZ; - } - } - - private static class ProxiedPos extends Position2 { - public ProxiedPos(Position2 p) { - super(p.getX(), p.getZ()); - } - - @Override - public void setX(int x) { - throw new IllegalStateException("This Position2 may not be modified"); - } - - @Override - public void setZ(int z) { - throw new IllegalStateException("This Position2 may not be modified"); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/TurboPregenerator.java b/core/src/main/java/com/volmit/iris/core/pregenerator/TurboPregenerator.java deleted file mode 100644 index 4588597c3..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/TurboPregenerator.java +++ /dev/null @@ -1,341 +0,0 @@ -package com.volmit.iris.core.pregenerator; - -import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.math.Spiraler; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.HyperLock; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import io.papermc.lib.PaperLib; -import lombok.Data; -import lombok.Getter; -import org.apache.logging.log4j.core.util.ExecutorServices; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.world.WorldUnloadEvent; -import org.bukkit.scheduler.BukkitRunnable; -import org.checkerframework.checker.units.qual.N; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Array; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.IntStream; - -public class TurboPregenerator extends Thread implements Listener { - @Getter - private static TurboPregenerator instance; - private final TurboPregenJob job; - private final File destination; - private final int maxPosition; - private World world; - private final ChronoLatch latch; - private static AtomicInteger turboGeneratedChunks; - private final AtomicInteger generatedLast; - private final AtomicLong cachedLast; - private final RollingSequence cachePerSecond; - private final AtomicInteger turboTotalChunks; - private final AtomicLong startTime; - private final RollingSequence chunksPerSecond; - private final RollingSequence chunksPerMinute; - private KList queue; - private ConcurrentHashMap cache; - private AtomicInteger maxWaiting; - private ReentrantLock cachinglock; - private AtomicBoolean caching; - private final HyperLock hyperLock; - private MultiBurst burst; - private static final Map jobs = new HashMap<>(); - - public TurboPregenerator(TurboPregenJob job, File destination) { - this.job = job; - queue = new KList<>(512); - this.maxWaiting = new AtomicInteger(128); - this.destination = destination; - this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - }).count(); - this.world = Bukkit.getWorld(job.getWorld()); - this.latch = new ChronoLatch(3000); - this.burst = MultiBurst.burst; - this.hyperLock = new HyperLock(); - this.startTime = new AtomicLong(M.ms()); - this.cachePerSecond = new RollingSequence(10); - this.chunksPerSecond = new RollingSequence(10); - this.chunksPerMinute = new RollingSequence(10); - turboGeneratedChunks = new AtomicInteger(0); - this.generatedLast = new AtomicInteger(0); - this.cachedLast = new AtomicLong(0); - this.caching = new AtomicBoolean(false); - this.turboTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2))); - cache = new ConcurrentHashMap<>(turboTotalChunks.get()); - this.cachinglock = new ReentrantLock(); - jobs.put(job.getWorld(), job); - TurboPregenerator.instance = this; - } - - public TurboPregenerator(File file) throws IOException { - this(new Gson().fromJson(IO.readAll(file), TurboPregenerator.TurboPregenJob.class), file); - } - - public static void loadTurboGenerator(String i) { - World x = Bukkit.getWorld(i); - File turbogen = new File(x.getWorldFolder(), "turbogen.json"); - if (turbogen.exists()) { - try { - TurboPregenerator p = new TurboPregenerator(turbogen); - p.start(); - Iris.info("Started Turbo Pregenerator: " + p.job); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - } - - @EventHandler - public void on(WorldUnloadEvent e) { - if (e.getWorld().equals(world)) { - interrupt(); - } - } - - public void run() { - while (!interrupted()) { - tick(); - } - - try { - saveNow(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public void tick() { - TurboPregenJob job = jobs.get(world.getName()); - if (!cachinglock.isLocked() && cache.isEmpty() && !caching.get()) { - ExecutorService cache = Executors.newFixedThreadPool(1); - cache.submit(this::cache); - } - - if (latch.flip() && caching.get()) { - long secondCached = cache.mappingCount() - cachedLast.get(); - cachedLast.set(cache.mappingCount()); - secondCached = secondCached / 3; - cachePerSecond.put(secondCached); - Iris.info("TurboGen: " + C.IRIS + world.getName() + C.RESET + C.BLUE + " Caching: " + Form.f(cache.mappingCount()) + " of " + Form.f(turboTotalChunks.get()) + " " + Form.f((int) cachePerSecond.getAverage()) + "/s"); - } - - if (latch.flip() && !job.paused && !cachinglock.isLocked()) { - long eta = computeETA(); - save(); - int secondGenerated = turboGeneratedChunks.get() - generatedLast.get(); - generatedLast.set(turboGeneratedChunks.get()); - secondGenerated = secondGenerated / 3; - chunksPerSecond.put(secondGenerated); - chunksPerMinute.put(secondGenerated * 60); - Iris.info("TurboGen: " + C.IRIS + world.getName() + C.RESET + " RTT: " + Form.f(turboGeneratedChunks.get()) + " of " + Form.f(turboTotalChunks.get()) + " " + Form.f((int) chunksPerSecond.getAverage()) + "/s ETA: " + Form.duration((double) eta, 2)); - - } - if (turboGeneratedChunks.get() >= turboTotalChunks.get()) { - Iris.info("Completed Turbo Gen!"); - interrupt(); - } else { - if (!cachinglock.isLocked()) { - int pos = job.getPosition() + 1; - job.setPosition(pos); - if (!job.paused) { - if (queue.size() < maxWaiting.get()) { - Position2 chunk = cache.get(pos); - queue.add(chunk); - } - waitForChunksPartial(); - } - } - } - } - - private void cache() { - if (!cachinglock.isLocked()) { - cachinglock.lock(); - caching.set(true); - PrecisionStopwatch p = PrecisionStopwatch.start(); - BurstExecutor b = MultiBurst.burst.burst(turboTotalChunks.get()); - b.setMulticore(true); - int[] list = IntStream.rangeClosed(0, turboTotalChunks.get()).toArray(); - AtomicInteger order = new AtomicInteger(turboTotalChunks.get()); - - int threads = Runtime.getRuntime().availableProcessors(); - if (threads > 1) threads--; - ExecutorService process = Executors.newFixedThreadPool(threads); - - for (int id : list) { - b.queue(() -> { - cache.put(id, getChunk(id)); - order.addAndGet(-1); - }); - } - b.complete(); - - if (order.get() < 0) { - cachinglock.unlock(); - caching.set(false); - Iris.info("Completed Caching in: " + Form.duration(p.getMilliseconds(), 2)); - } - } else { - Iris.error("TurboCache is locked!"); - } - } - - private void waitForChunksPartial() { - while (!queue.isEmpty() && maxWaiting.get() > queue.size()) { - try { - for (Position2 c : new KList<>(queue)) { - tickGenerate(c); - queue.remove(c); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - private long computeETA() { - return (long) ((turboTotalChunks.get() - turboGeneratedChunks.get()) / chunksPerMinute.getAverage()) * 1000; - // todo broken - } - - private final ExecutorService executorService = Executors.newFixedThreadPool(10); - private void tickGenerate(Position2 chunk) { - executorService.submit(() -> { - CountDownLatch latch = new CountDownLatch(1); - PaperLib.getChunkAtAsync(world, chunk.getX(), chunk.getZ(), true) - .thenAccept((i) -> { - latch.countDown(); - }); - try { - latch.await(); - } catch (InterruptedException ignored) { - } - turboGeneratedChunks.addAndGet(1); - }); - } - - public Position2 getChunk(int position) { - int p = -1; - AtomicInteger xx = new AtomicInteger(); - AtomicInteger zz = new AtomicInteger(); - Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - xx.set(x); - zz.set(z); - }); - - while (s.hasNext() && p++ < position) { - s.next(); - } - - return new Position2(xx.get(), zz.get()); - } - - public void save() { - J.a(() -> { - try { - saveNow(); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } - - public static void setPausedTurbo(World world) { - TurboPregenJob job = jobs.get(world.getName()); - if (isPausedTurbo(world)) { - job.paused = false; - } else { - job.paused = true; - } - - if (job.paused) { - Iris.info(C.BLUE + "TurboGen: " + C.IRIS + world.getName() + C.BLUE + " Paused"); - } else { - Iris.info(C.BLUE + "TurboGen: " + C.IRIS + world.getName() + C.BLUE + " Resumed"); - } - } - - public static boolean isPausedTurbo(World world) { - TurboPregenJob job = jobs.get(world.getName()); - return job != null && job.isPaused(); - } - - public void shutdownInstance(World world) throws IOException { - Iris.info("turboGen: " + C.IRIS + world.getName() + C.BLUE + " Shutting down.."); - TurboPregenJob job = jobs.get(world.getName()); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File turboFile = new File(worldDirectory, "turbogen.json"); - - if (job == null) { - Iris.error("No turbogen job found for world: " + world.getName()); - return; - } - - try { - if (!job.isPaused()) { - job.setPaused(true); - } - save(); - jobs.remove(world.getName()); - new BukkitRunnable() { - @Override - public void run() { - while (turboFile.exists()) { - turboFile.delete(); - J.sleep(1000); - } - Iris.info("turboGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); - } - }.runTaskLater(Iris.instance, 20L); - } catch (Exception e) { - Iris.error("Failed to shutdown turbogen for " + world.getName()); - e.printStackTrace(); - } finally { - saveNow(); - interrupt(); - } - } - - - public void saveNow() throws IOException { - IO.writeAll(this.destination, new Gson().toJson(job)); - } - - @Data - @lombok.Builder - public static class TurboPregenJob { - private String world; - @lombok.Builder.Default - private int radiusBlocks = 5000; - @lombok.Builder.Default - private int position = 0; - @lombok.Builder.Default - boolean paused = false; - } -} - diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java deleted file mode 100644 index f33fe2cdc..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.pregenerator.methods; - -import com.volmit.iris.core.pregenerator.PregenListener; -import com.volmit.iris.core.pregenerator.PregeneratorMethod; -import com.volmit.iris.util.mantle.Mantle; -import io.papermc.lib.PaperLib; -import org.bukkit.World; - -public class AsyncOrMedievalPregenMethod implements PregeneratorMethod { - private final PregeneratorMethod method; - - public AsyncOrMedievalPregenMethod(World world, int threads) { - method = PaperLib.isPaper() ? new AsyncPregenMethod(world, threads) : new MedievalPregenMethod(world); - } - - @Override - public void init() { - method.init(); - } - - @Override - public void close() { - method.close(); - } - - @Override - public void save() { - method.save(); - } - - @Override - public String getMethod(int x, int z) { - return method.getMethod(x, z); - } - - @Override - public boolean supportsRegions(int x, int z, PregenListener listener) { - return false; - } - - @Override - public void generateRegion(int x, int z, PregenListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public void generateChunk(int x, int z, PregenListener listener) { - method.generateChunk(x, z, listener); - } - - @Override - public Mantle getMantle() { - return method.getMantle(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java deleted file mode 100644 index 7e92807bf..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.pregenerator.methods; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.pregenerator.PregenListener; -import com.volmit.iris.core.pregenerator.PregeneratorMethod; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.scheduling.J; -import io.papermc.lib.PaperLib; -import org.bukkit.Chunk; -import org.bukkit.World; - -import java.lang.reflect.InvocationTargetException; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicInteger; - -public class AsyncPregenMethod implements PregeneratorMethod { - private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); - private final World world; - private final Executor executor; - private final Semaphore semaphore; - private final int threads; - private final boolean urgent; - private final Map lastUse; - - public AsyncPregenMethod(World world, int unusedThreads) { - if (!PaperLib.isPaper()) { - throw new UnsupportedOperationException("Cannot use PaperAsync on non paper!"); - } - - this.world = world; - this.executor = IrisSettings.get().getPregen().isUseTicketQueue() ? new TicketExecutor() : new ServiceExecutor(); - this.threads = IrisSettings.get().getPregen().getMaxConcurrency(); - this.semaphore = new Semaphore(this.threads, true); - this.urgent = IrisSettings.get().getPregen().useHighPriority; - this.lastUse = new KMap<>(); - } - - private void unloadAndSaveAllChunks() { - try { - J.sfut(() -> { - if (world == null) { - Iris.warn("World was null somehow..."); - return; - } - - long minTime = M.ms() - 10_000; - lastUse.entrySet().removeIf(i -> { - final Chunk chunk = i.getKey(); - final Long lastUseTime = i.getValue(); - if (!chunk.isLoaded() || lastUseTime == null) - return true; - if (lastUseTime < minTime) { - chunk.unload(); - return true; - } - return false; - }); - world.save(); - }).get(); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - @Override - public void init() { - unloadAndSaveAllChunks(); - increaseWorkerThreads(); - } - - @Override - public String getMethod(int x, int z) { - return "Async"; - } - - @Override - public void close() { - semaphore.acquireUninterruptibly(threads); - unloadAndSaveAllChunks(); - executor.shutdown(); - resetWorkerThreads(); - } - - @Override - public void save() { - unloadAndSaveAllChunks(); - } - - @Override - public boolean supportsRegions(int x, int z, PregenListener listener) { - return false; - } - - @Override - public void generateRegion(int x, int z, PregenListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public void generateChunk(int x, int z, PregenListener listener) { - listener.onChunkGenerating(x, z); - try { - semaphore.acquire(); - } catch (InterruptedException e) { - return; - } - executor.generate(x, z, listener); - } - - @Override - public Mantle getMantle() { - if (IrisToolbelt.isIrisWorld(world)) { - return IrisToolbelt.access(world).getEngine().getMantle().getMantle(); - } - - return null; - } - - public static void increaseWorkerThreads() { - THREAD_COUNT.updateAndGet(i -> { - if (i > 0) return 1; - var adjusted = IrisSettings.get().getConcurrency().getWorldGenThreads(); - try { - var field = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon").getDeclaredField("WORKER_POOL"); - var pool = field.get(null); - var threads = ((Thread[]) pool.getClass().getDeclaredMethod("getCoreThreads").invoke(pool)).length; - if (threads >= adjusted) return 0; - - pool.getClass().getDeclaredMethod("adjustThreadCount", int.class).invoke(pool, adjusted); - return threads; - } catch (Throwable e) { - Iris.warn("Failed to increase worker threads, if you are on paper or a fork of it please increase it manually to " + adjusted); - Iris.warn("For more information see https://docs.papermc.io/paper/reference/global-configuration#chunk_system_worker_threads"); - if (e instanceof InvocationTargetException) { - Iris.reportError(e); - e.printStackTrace(); - } - } - return 0; - }); - } - - public static void resetWorkerThreads() { - THREAD_COUNT.updateAndGet(i -> { - if (i == 0) return 0; - try { - var field = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon").getDeclaredField("WORKER_POOL"); - var pool = field.get(null); - var method = pool.getClass().getDeclaredMethod("adjustThreadCount", int.class); - method.invoke(pool, i); - return 0; - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Failed to reset worker threads"); - e.printStackTrace(); - } - return i; - }); - } - - private interface Executor { - void generate(int x, int z, PregenListener listener); - default void shutdown() {} - } - - private class ServiceExecutor implements Executor { - private final ExecutorService service = IrisSettings.get().getPregen().isUseVirtualThreads() ? - Executors.newVirtualThreadPerTaskExecutor() : - new MultiBurst("Iris Async Pregen"); - - public void generate(int x, int z, PregenListener listener) { - service.submit(() -> { - try { - PaperLib.getChunkAtAsync(world, x, z, true, urgent).thenAccept((i) -> { - listener.onChunkGenerated(x, z); - listener.onChunkCleaned(x, z); - if (i == null) return; - lastUse.put(i, M.ms()); - }).get(); - } catch (InterruptedException ignored) { - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } finally { - semaphore.release(); - } - }); - } - - @Override - public void shutdown() { - service.shutdown(); - } - } - - private class TicketExecutor implements Executor { - @Override - public void generate(int x, int z, PregenListener listener) { - PaperLib.getChunkAtAsync(world, x, z, true, urgent) - .exceptionally(e -> { - Iris.reportError(e); - e.printStackTrace(); - return null; - }) - .thenAccept(i -> { - semaphore.release(); - listener.onChunkGenerated(x, z); - listener.onChunkCleaned(x, z); - if (i == null) return; - lastUse.put(i, M.ms()); - }); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/CachedPregenMethod.java b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/CachedPregenMethod.java deleted file mode 100644 index 91c4ddb87..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/CachedPregenMethod.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.volmit.iris.core.pregenerator.methods; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.pregenerator.PregenListener; -import com.volmit.iris.core.pregenerator.PregeneratorMethod; -import com.volmit.iris.core.pregenerator.cache.PregenCache; -import com.volmit.iris.core.service.GlobalCacheSVC; -import com.volmit.iris.util.mantle.Mantle; -import lombok.AllArgsConstructor; - -@AllArgsConstructor -public class CachedPregenMethod implements PregeneratorMethod { - private final PregeneratorMethod method; - private final PregenCache cache; - - public CachedPregenMethod(PregeneratorMethod method, String worldName) { - this.method = method; - var cache = Iris.service(GlobalCacheSVC.class).get(worldName); - if (cache == null) { - Iris.debug("Could not find existing cache for " + worldName + " creating fallback"); - cache = GlobalCacheSVC.createDefault(worldName); - } - this.cache = cache; - } - - @Override - public void init() { - method.init(); - } - - @Override - public void close() { - method.close(); - cache.write(); - } - - @Override - public void save() { - method.save(); - cache.write(); - } - - @Override - public boolean supportsRegions(int x, int z, PregenListener listener) { - return cache.isRegionCached(x, z) || method.supportsRegions(x, z, listener); - } - - @Override - public String getMethod(int x, int z) { - return method.getMethod(x, z); - } - - @Override - public void generateRegion(int x, int z, PregenListener listener) { - if (cache.isRegionCached(x, z)) { - listener.onRegionGenerated(x, z); - - int rX = x << 5, rZ = z << 5; - for (int cX = 0; cX < 32; cX++) { - for (int cZ = 0; cZ < 32; cZ++) { - listener.onChunkGenerated(rX + cX, rZ + cZ, true); - listener.onChunkCleaned(rX + cX, rZ + cZ); - } - } - return; - } - method.generateRegion(x, z, listener); - cache.cacheRegion(x, z); - } - - @Override - public void generateChunk(int x, int z, PregenListener listener) { - if (cache.isChunkCached(x, z)) { - listener.onChunkGenerated(x, z, true); - listener.onChunkCleaned(x, z); - return; - } - method.generateChunk(x, z, listener); - cache.cacheChunk(x, z); - } - - @Override - public Mantle getMantle() { - return method.getMantle(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/DummyPregenMethod.java b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/DummyPregenMethod.java deleted file mode 100644 index 9ff345fc3..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/DummyPregenMethod.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.pregenerator.methods; - -import com.volmit.iris.core.pregenerator.PregenListener; -import com.volmit.iris.core.pregenerator.PregeneratorMethod; -import com.volmit.iris.util.mantle.Mantle; - -public class DummyPregenMethod implements PregeneratorMethod { - @Override - public void init() { - - } - - @Override - public void close() { - - } - - @Override - public String getMethod(int x, int z) { - return "Dummy"; - } - - @Override - public void save() { - - } - - @Override - public boolean supportsRegions(int x, int z, PregenListener listener) { - return false; - } - - @Override - public void generateRegion(int x, int z, PregenListener listener) { - - } - - @Override - public void generateChunk(int x, int z, PregenListener listener) { - - } - - @Override - public Mantle getMantle() { - return null; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/HybridPregenMethod.java b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/HybridPregenMethod.java deleted file mode 100644 index 8ae773641..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/HybridPregenMethod.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.pregenerator.methods; - -import com.volmit.iris.core.pregenerator.PregenListener; -import com.volmit.iris.core.pregenerator.PregeneratorMethod; -import com.volmit.iris.util.mantle.Mantle; -import org.bukkit.World; - -public class HybridPregenMethod implements PregeneratorMethod { - private final PregeneratorMethod inWorld; - private final World world; - - public HybridPregenMethod(World world, int threads) { - this.world = world; - inWorld = new AsyncOrMedievalPregenMethod(world, threads); - } - - @Override - public String getMethod(int x, int z) { - return "Hybrid<" + inWorld.getMethod(x, z) + ">"; - } - - @Override - public void init() { - inWorld.init(); - } - - @Override - public void close() { - inWorld.close(); - } - - @Override - public void save() { - inWorld.save(); - } - - @Override - public boolean supportsRegions(int x, int z, PregenListener listener) { - return inWorld.supportsRegions(x, z, listener); - } - - @Override - public void generateRegion(int x, int z, PregenListener listener) { - inWorld.generateRegion(x, z, listener); - } - - @Override - public void generateChunk(int x, int z, PregenListener listener) { - inWorld.generateChunk(x, z, listener); - } - - @Override - public Mantle getMantle() { - return inWorld.getMantle(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/MedievalPregenMethod.java b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/MedievalPregenMethod.java deleted file mode 100644 index 2ebfb64b2..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/MedievalPregenMethod.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.pregenerator.methods; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.pregenerator.PregenListener; -import com.volmit.iris.core.pregenerator.PregeneratorMethod; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.World; - -import java.util.ArrayList; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -public class MedievalPregenMethod implements PregeneratorMethod { - private final World world; - private final KList> futures; - private final Map lastUse; - - public MedievalPregenMethod(World world) { - this.world = world; - futures = new KList<>(); - this.lastUse = new KMap<>(); - } - - private void waitForChunks() { - for (CompletableFuture i : futures) { - try { - i.get(); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - futures.clear(); - } - - private void unloadAndSaveAllChunks() { - try { - J.sfut(() -> { - if (world == null) { - Iris.warn("World was null somehow..."); - return; - } - - for (Chunk i : new ArrayList<>(lastUse.keySet())) { - Long lastUseTime = lastUse.get(i); - if (lastUseTime != null && M.ms() - lastUseTime >= 10) { - i.unload(); - lastUse.remove(i); - } - } - world.save(); - }).get(); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - @Override - public void init() { - unloadAndSaveAllChunks(); - } - - @Override - public void close() { - unloadAndSaveAllChunks(); - } - - @Override - public void save() { - unloadAndSaveAllChunks(); - } - - @Override - public boolean supportsRegions(int x, int z, PregenListener listener) { - return false; - } - - @Override - public void generateRegion(int x, int z, PregenListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public String getMethod(int x, int z) { - return "Medieval"; - } - - @Override - public void generateChunk(int x, int z, PregenListener listener) { - if (futures.size() > IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())) { - waitForChunks(); - } - - listener.onChunkGenerating(x, z); - futures.add(J.sfut(() -> { - world.getChunkAt(x, z); - Chunk c = Bukkit.getWorld(world.getUID()).getChunkAt(x, z); - lastUse.put(c, M.ms()); - listener.onChunkGenerated(x, z); - listener.onChunkCleaned(x, z); - })); - } - - @Override - public Mantle getMantle() { - if (IrisToolbelt.isIrisWorld(world)) { - return IrisToolbelt.access(world).getEngine().getMantle().getMantle(); - } - - return null; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/project/Gradle.java b/core/src/main/java/com/volmit/iris/core/project/Gradle.java deleted file mode 100644 index 565e7d39f..000000000 --- a/core/src/main/java/com/volmit/iris/core/project/Gradle.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.volmit.iris.core.project; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.io.IO; -import org.zeroturnaround.zip.ZipUtil; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.*; - -public class Gradle { - private static final boolean WINDOWS = System.getProperty("os.name").toLowerCase().contains("win"); - private static final String[] ENVIRONMENT = createEnvironment(); - private static final String VERSION = "8.14.2"; - private static final String DISTRIBUTION_URL = "https://services.gradle.org/distributions/gradle-" + VERSION + "-bin.zip"; - private static final String HASH = IO.hash(DISTRIBUTION_URL); - - public static synchronized void wrapper(File projectDir) { - try { - File settings = new File(projectDir, "settings.gradle.kts"); - if (!settings.exists()) settings.createNewFile(); - runGradle(projectDir, "wrapper"); - } catch (Throwable e) { - Iris.error("Failed to install gradle wrapper!"); - e.printStackTrace(); - Iris.reportError(e); - } - } - - public static void runGradle(File projectDir, String... args) throws IOException, InterruptedException { - File gradle = downloadGradle(false); - String[] cmd = new String[args.length + 1]; - cmd[0] = gradle.getAbsolutePath(); - System.arraycopy(args, 0, cmd, 1, args.length); - var process = Runtime.getRuntime().exec(cmd, ENVIRONMENT, projectDir); - var lines = Collections.synchronizedList(new ArrayList()); - attach(process.getInputStream(), lines); - attach(process.getErrorStream(), lines); - var code = process.waitFor(); - if (code == 0) { - lines.forEach(Iris::debug); - return; - } - lines.forEach(Iris::error); - throw new RuntimeException("Gradle exited with code " + code); - } - - private static synchronized File downloadGradle(boolean force) { - var folder = Iris.instance.getDataFolder("cache", HASH.substring(0, 2), HASH); - if (force) { - IO.delete(folder); - folder.mkdirs(); - } - - var bin = new File(folder, "gradle-" + VERSION + "/bin/gradle" + (WINDOWS ? ".bat" : "")); - if (bin.exists()) { - bin.setExecutable(true); - return bin; - } - - try (var input = new BufferedInputStream(URI.create(DISTRIBUTION_URL).toURL().openStream())) { - ZipUtil.unpack(input, folder); - } catch (Throwable e) { - throw new RuntimeException("Failed to download gradle", e); - } - - bin.setExecutable(true); - return bin; - } - - private static String[] createEnvironment() { - var env = new HashMap<>(System.getenv()); - env.put("JAVA_HOME", findJavaHome()); - return env.entrySet() - .stream() - .map(e -> e.getKey() + "=" + e.getValue()) - .toArray(String[]::new); - } - - private static String findJavaHome() { - String javaHome = System.getProperty("java.home"); - if (javaHome != null && new File(javaHome + "/bin/java" + (WINDOWS ? ".exe" : "")).exists()) { - return javaHome; - } - - return ProcessHandle.current() - .info() - .command() - .map(s -> new File(s).getAbsoluteFile().getParentFile().getParentFile()) - .flatMap(f -> f.exists() ? Optional.of(f.getAbsolutePath()) : Optional.empty()) - .orElseThrow(() -> new RuntimeException("Failed to find java home, please set java.home system property")); - } - - private static void attach(InputStream stream, List list) { - Thread.ofPlatform().start(() -> { - try (var in = new Scanner(stream)) { - while (in.hasNextLine()) { - String line = in.nextLine(); - list.add(line); - } - } - }); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/project/IrisProject.java b/core/src/main/java/com/volmit/iris/core/project/IrisProject.java deleted file mode 100644 index ec9578b24..000000000 --- a/core/src/main/java/com/volmit/iris/core/project/IrisProject.java +++ /dev/null @@ -1,719 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.project; - -import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.core.loader.ResourceLoader; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.exceptions.IrisException; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.O; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import com.volmit.iris.util.scheduling.jobs.Job; -import com.volmit.iris.util.scheduling.jobs.JobCollection; -import com.volmit.iris.util.scheduling.jobs.ParallelQueueJob; -import lombok.Data; -import org.bukkit.Bukkit; -import org.bukkit.GameMode; -import org.bukkit.World; -import org.dom4j.Document; -import org.dom4j.Element; -import org.zeroturnaround.zip.ZipUtil; - -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.util.Objects; -import java.util.UUID; -import java.util.function.Consumer; - -@SuppressWarnings("ALL") -@Data -public class IrisProject { - private File path; - private String name; - private PlatformChunkGenerator activeProvider; - - public IrisProject(File path) { - this.path = path; - this.name = path.getName(); - } - - public static int clean(VolmitSender s, File clean) { - int c = 0; - if (clean.isDirectory()) { - for (File i : clean.listFiles()) { - c += clean(s, i); - } - } else if (clean.getName().endsWith(".json")) { - try { - clean(clean); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!"); - } - - c++; - } - - return c; - } - - public static void clean(File clean) throws IOException { - JSONObject obj = new JSONObject(IO.readAll(clean)); - fixBlocks(obj, clean); - - IO.writeAll(clean, obj.toString(4)); - } - - public static void fixBlocks(JSONObject obj, File f) { - for (String i : obj.keySet()) { - Object o = obj.get(i); - - if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) { - obj.put(i, "minecraft:" + o); - Iris.debug("Updated Block Key: " + o + " to " + obj.getString(i) + " in " + f.getPath()); - } - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o, f); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o, f); - } - } - } - - public static void fixBlocks(JSONArray obj, File f) { - for (int i = 0; i < obj.length(); i++) { - Object o = obj.get(i); - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o, f); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o, f); - } - } - } - - public boolean isOpen() { - return activeProvider != null; - } - - public KList collectFiles(File f, String fileExtension) { - KList l = new KList<>(); - - if (f.isDirectory()) { - for (File i : f.listFiles()) { - l.addAll(collectFiles(i, fileExtension)); - } - } else if (f.getName().endsWith("." + fileExtension)) { - l.add(f); - } - - return l; - } - - public KList collectFiles(String json) { - return collectFiles(path, json); - } - - public void open(VolmitSender sender) throws IrisException { - open(sender, 1337, (w) -> - { - }); - } - - public void openVSCode(VolmitSender sender) { - - IrisDimension d = IrisData.loadAnyDimension(getName(), null); - J.attemptAsync(() -> - { - try { - if (d.getLoader() == null) { - sender.sendMessage("Could not get dimension loader"); - return; - } - File f = d.getLoader().getDataFolder(); - - if (!doOpenVSCode(f)) { - File ff = new File(d.getLoader().getDataFolder(), d.getLoadKey() + ".code-workspace"); - Iris.warn("Project missing code-workspace: " + ff.getAbsolutePath() + " Re-creating code workspace."); - - try { - IO.writeAll(ff, createCodeWorkspaceConfig()); - } catch (IOException e1) { - Iris.reportError(e1); - e1.printStackTrace(); - } - updateWorkspace(); - if (!doOpenVSCode(f)) { - Iris.warn("Tried creating code workspace but failed a second time. Your project is likely corrupt."); - } - } - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - }); - } - - private boolean doOpenVSCode(File f) throws IOException { - boolean foundWork = false; - for (File i : Objects.requireNonNull(f.listFiles())) { - if (i.getName().endsWith(".code-workspace")) { - foundWork = true; - J.a(() -> - { - updateWorkspace(); - }); - - if (IrisSettings.get().getStudio().isOpenVSCode()) { - if (!GraphicsEnvironment.isHeadless()) { - Iris.msg("Opening VSCode. You may see the output from VSCode."); - Iris.msg("VSCode output always starts with: '(node:#####) electron'"); - Desktop.getDesktop().open(i); - } - } - - break; - } - } - return foundWork; - } - - public void open(VolmitSender sender, long seed, Consumer onDone) throws IrisException { - if (isOpen()) { - close(); - } - - J.a(() -> { - IrisDimension d = IrisData.loadAnyDimension(getName(), null); - if (d == null) { - sender.sendMessage("Can't find dimension: " + getName()); - return; - } else if (sender.isPlayer()) { - J.s(() -> sender.player().setGameMode(GameMode.SPECTATOR)); - } - - try { - activeProvider = (PlatformChunkGenerator) IrisToolbelt.createWorld() - .seed(seed) - .sender(sender) - .studio(true) - .name("iris/" + UUID.randomUUID()) - .dimension(d.getLoadKey()) - .create().getGenerator(); - onDone.accept(activeProvider.getTarget().getWorld().realWorld()); - } catch (IrisException e) { - e.printStackTrace(); - } - - openVSCode(sender); - }); - } - - public void close() { - Iris.debug("Closing Active Provider"); - IrisToolbelt.evacuate(activeProvider.getTarget().getWorld().realWorld()); - activeProvider.close(); - File folder = activeProvider.getTarget().getWorld().worldFolder(); - Iris.linkMultiverseCore.removeFromConfig(activeProvider.getTarget().getWorld().name()); - Bukkit.unloadWorld(activeProvider.getTarget().getWorld().name(), false); - J.attemptAsync(() -> IO.delete(folder)); - Iris.debug("Closed Active Provider " + activeProvider.getTarget().getWorld().name()); - activeProvider = null; - } - - public File getCodeWorkspaceFile() { - return new File(path, getName() + ".code-workspace"); - } - - public boolean updateWorkspace() { - getPath().mkdirs(); - File ws = getCodeWorkspaceFile(); - - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - JSONObject j = createCodeWorkspaceConfig(); - IO.writeAll(ws, j.toString(4)); - p.end(); - return true; - } catch (Throwable e) { - Iris.reportError(e); - Iris.warn("Project invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!"); - ws.delete(); - try { - IO.writeAll(ws, createCodeWorkspaceConfig()); - } catch (IOException e1) { - Iris.reportError(e1); - e1.printStackTrace(); - } - } - - return false; - } - - public JSONObject createCodeWorkspaceConfig() { - JSONObject ws = new JSONObject(); - JSONArray folders = new JSONArray(); - JSONObject folder = new JSONObject(); - folder.put("path", "."); - folders.put(folder); - ws.put("folders", folders); - JSONObject settings = new JSONObject(); - settings.put("workbench.colorTheme", "Monokai"); - settings.put("workbench.preferredDarkColorTheme", "Solarized Dark"); - settings.put("workbench.tips.enabled", false); - settings.put("workbench.tree.indent", 24); - settings.put("files.autoSave", "onFocusChange"); - JSONObject jc = new JSONObject(); - jc.put("editor.autoIndent", "brackets"); - jc.put("editor.acceptSuggestionOnEnter", "smart"); - jc.put("editor.cursorSmoothCaretAnimation", true); - jc.put("editor.dragAndDrop", false); - jc.put("files.trimTrailingWhitespace", true); - jc.put("diffEditor.ignoreTrimWhitespace", true); - jc.put("files.trimFinalNewlines", true); - jc.put("editor.suggest.showKeywords", false); - jc.put("editor.suggest.showSnippets", false); - jc.put("editor.suggest.showWords", false); - JSONObject st = new JSONObject(); - st.put("strings", true); - jc.put("editor.quickSuggestions", st); - jc.put("editor.suggest.insertMode", "replace"); - settings.put("[json]", jc); - settings.put("json.maxItemsComputed", 30000); - JSONArray schemas = new JSONArray(); - IrisData dm = IrisData.get(getPath()); - - for (ResourceLoader r : dm.getLoaders().v()) { - if (r.supportsSchemas()) { - schemas.put(r.buildSchema()); - } - } - - for (Class i : dm.resolveSnippets()) { - try { - String snipType = i.getDeclaredAnnotation(Snippet.class).value(); - JSONObject o = new JSONObject(); - KList fm = new KList<>(); - - for (int g = 1; g < 8; g++) { - fm.add("/snippet/" + snipType + Form.repeat("/*", g) + ".json"); - } - - o.put("fileMatch", new JSONArray(fm.toArray())); - o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json"); - schemas.put(o); - File a = new File(dm.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json"); - J.attemptAsync(() -> { - try { - IO.writeAll(a, new SchemaBuilder(i, dm).construct().toString(4)); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - settings.put("json.schemas", schemas); - ws.put("settings", settings); - - dm.getEnvironment().configureProject(); - File schemasFile = new File(path, ".idea" + File.separator + "jsonSchemas.xml"); - Document doc = IO.read(schemasFile); - Element mappings = (Element) doc.selectSingleNode("//component[@name='JsonSchemaMappingsProjectConfiguration']"); - if (mappings == null) { - mappings = doc.getRootElement() - .addElement("component") - .addAttribute("name", "JsonSchemaMappingsProjectConfiguration"); - } - - Element state = (Element) mappings.selectSingleNode("state"); - if (state == null) state = mappings.addElement("state"); - - Element map = (Element) state.selectSingleNode("map"); - if (map == null) map = state.addElement("map"); - var schemaMap = new KMap(); - schemas.forEach(element -> { - if (!(element instanceof JSONObject obj)) - return; - - String url = obj.getString("url"); - String dir = obj.getJSONArray("fileMatch").getString(0); - schemaMap.put(url, dir.substring(1, dir.indexOf("/*"))); - }); - - map.selectNodes("entry/value/SchemaInfo/option[@name='relativePathToSchema']") - .stream() - .map(node -> node.valueOf("@value")) - .forEach(schemaMap::remove); - - var ideaSchemas = map; - schemaMap.forEach((url, dir) -> { - var genName = UUID.randomUUID().toString(); - - var info = ideaSchemas.addElement("entry") - .addAttribute("key", genName) - .addElement("value") - .addElement("SchemaInfo"); - info.addElement("option") - .addAttribute("name", "generatedName") - .addAttribute("value", genName); - info.addElement("option") - .addAttribute("name", "name") - .addAttribute("value", dir); - info.addElement("option") - .addAttribute("name", "relativePathToSchema") - .addAttribute("value", url); - - - var item = info.addElement("option") - .addAttribute("name", "patterns") - .addElement("list") - .addElement("Item"); - item.addElement("option") - .addAttribute("name", "directory") - .addAttribute("value", "true"); - item.addElement("option") - .addAttribute("name", "path") - .addAttribute("value", dir); - item.addElement("option") - .addAttribute("name", "mappingKind") - .addAttribute("value", "Directory"); - }); - if (!schemaMap.isEmpty()) { - IO.write(schemasFile, doc); - } - Gradle.wrapper(path); - - return ws; - } - - public File compilePackage(VolmitSender sender, boolean obfuscate, boolean minify) { - String dimm = getName(); - IrisData dm = IrisData.get(path); - IrisDimension dimension = dm.getDimensionLoader().load(dimm); - File folder = new File(Iris.instance.getDataFolder(), "exports/" + dimension.getLoadKey()); - folder.mkdirs(); - Iris.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : "")); - KSet regions = new KSet<>(); - KSet biomes = new KSet<>(); - KSet entities = new KSet<>(); - KSet spawners = new KSet<>(); - KSet generators = new KSet<>(); - KSet loot = new KSet<>(); - KSet blocks = new KSet<>(); - - for (String i : dm.getBlockLoader().getPossibleKeys()) { - blocks.add(dm.getBlockLoader().load(i)); - } - - dimension.getRegions().forEach((i) -> regions.add(dm.getRegionLoader().load(i))); - dimension.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))); - regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm))); - regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i)))); - regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp)))); - dimension.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))); - biomes.forEach((i) -> i.getGenerators().forEach((j) -> generators.add(j.getCachedGenerator(() -> dm)))); - biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i)))); - biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp)))); - spawners.forEach((i) -> i.getSpawns().forEach((j) -> entities.add(dm.getEntityLoader().load(j.getEntity())))); - KMap renameObjects = new KMap<>(); - String a; - StringBuilder b = new StringBuilder(); - StringBuilder c = new StringBuilder(); - sender.sendMessage("Serializing Objects"); - - for (IrisBiome i : biomes) { - for (IrisObjectPlacement j : i.getObjects()) { - b.append(j.hashCode()); - KList newNames = new KList<>(); - - for (String k : j.getPlace()) { - if (renameObjects.containsKey(k)) { - newNames.add(renameObjects.get(k)); - continue; - } - - String name = !obfuscate ? k : UUID.randomUUID().toString().replaceAll("-", ""); - b.append(name); - newNames.add(name); - renameObjects.put(k, name); - } - - j.setPlace(newNames); - } - } - - KMap> lookupObjects = renameObjects.flip(); - StringBuilder gb = new StringBuilder(); - ChronoLatch cl = new ChronoLatch(1000); - O ggg = new O<>(); - ggg.set(0); - biomes.forEach((i) -> i.getObjects().forEach((j) -> j.getPlace().forEach((k) -> - { - try { - File f = dm.getObjectLoader().findFile(lookupObjects.get(k).get(0)); - IO.copyFile(f, new File(folder, "objects/" + k + ".iob")); - gb.append(IO.hash(f)); - ggg.set(ggg.get() + 1); - - if (cl.flip()) { - int g = ggg.get(); - ggg.set(0); - sender.sendMessage("Wrote another " + g + " Objects"); - } - } catch (Throwable e) { - Iris.reportError(e); - } - }))); - - b.append(IO.hash(gb.toString())); - c.append(IO.hash(b.toString())); - b = new StringBuilder(); - - Iris.info("Writing Dimensional Scaffold"); - - try { - a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - - for (IrisGenerator i : generators) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "generators/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - c.append(IO.hash(b.toString())); - b = new StringBuilder(); - - for (IrisRegion i : regions) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "regions/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - for (IrisBlockData i : blocks) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "blocks/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - for (IrisBiome i : biomes) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "biomes/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - for (IrisEntity i : entities) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "entities/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - for (IrisLootTable i : loot) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "loot/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - c.append(IO.hash(b.toString())); - String finalHash = IO.hash(c.toString()); - JSONObject meta = new JSONObject(); - meta.put("hash", finalHash); - meta.put("time", M.ms()); - meta.put("version", dimension.getVersion()); - IO.writeAll(new File(folder, "package.json"), meta.toString(minify ? 0 : 4)); - File p = new File(Iris.instance.getDataFolder(), "exports/" + dimension.getLoadKey() + ".iris"); - Iris.info("Compressing Package"); - ZipUtil.pack(folder, p, 9); - IO.delete(folder); - - sender.sendMessage("Package Compiled!"); - return p; - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - sender.sendMessage("Failed!"); - return null; - } - - public void compile(VolmitSender sender) { - IrisData data = IrisData.get(getPath()); - KList jobs = new KList<>(); - KList files = new KList<>(); - KList objects = new KList<>(); - files(getPath(), files); - filesObjects(getPath(), objects); - - jobs.add(new ParallelQueueJob() { - @Override - public void execute(File f) { - try { - IrisObject o = new IrisObject(0, 0, 0); - o.read(f); - - if (o.getBlocks().isEmpty()) { - sender.sendMessageRaw("" + f.getPath() + - "'>- IOB " + f.getName() + " has 0 blocks!"); - } - - if (o.getW() == 0 || o.getH() == 0 || o.getD() == 0) { - sender.sendMessageRaw("" + f.getPath() + "\nThe width height or depth has a zero in it (bad format)" + - "'>- IOB " + f.getName() + " is not 3D!"); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public String getName() { - return "IOB"; - } - }.queue(objects)); - - jobs.add(new ParallelQueueJob() { - @Override - public void execute(File f) { - try { - JSONObject p = new JSONObject(IO.readAll(f)); - fixBlocks(p); - scanForErrors(data, f, p, sender); - IO.writeAll(f, p.toString(4)); - - } catch (Throwable e) { - sender.sendMessageRaw("" + f.getPath() + - "\n" + e.getMessage() + - "'>- JSON Error " + f.getName()); - } - } - - @Override - public String getName() { - return "JSON"; - } - }.queue(files)); - - new JobCollection("Compile", jobs).execute(sender); - } - - private void scanForErrors(IrisData data, File f, JSONObject p, VolmitSender sender) { - String key = data.toLoadKey(f); - ResourceLoader loader = data.getTypedLoaderFor(f); - - if (loader == null) { - sender.sendMessageBasic("Can't find loader for " + f.getPath()); - return; - } - - IrisRegistrant load = loader.load(key); - compare(load.getClass(), p, sender, new KList<>()); - load.scanForErrors(p, sender); - } - - public void compare(Class c, JSONObject j, VolmitSender sender, KList path) { - try { - Object o = c.getClass().getConstructor().newInstance(); - } catch (Throwable e) { - - } - } - - public void files(File clean, KList files) { - if (clean.isDirectory()) { - for (File i : clean.listFiles()) { - files(i, files); - } - } else if (clean.getName().endsWith(".json")) { - try { - files.add(clean); - } catch (Throwable e) { - Iris.reportError(e); - } - } - } - - public void filesObjects(File clean, KList files) { - if (clean.isDirectory()) { - for (File i : clean.listFiles()) { - filesObjects(i, files); - } - } else if (clean.getName().endsWith(".iob")) { - try { - files.add(clean); - } catch (Throwable e) { - Iris.reportError(e); - } - } - } - - private void fixBlocks(JSONObject obj) { - for (String i : obj.keySet()) { - Object o = obj.get(i); - - if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) { - obj.put(i, "minecraft:" + o); - } - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o); - } - } - } - - private void fixBlocks(JSONArray obj) { - for (int i = 0; i < obj.length(); i++) { - Object o = obj.get(i); - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o); - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/project/SchemaBuilder.java b/core/src/main/java/com/volmit/iris/core/project/SchemaBuilder.java deleted file mode 100644 index 5c4373452..000000000 --- a/core/src/main/java/com/volmit/iris/core/project/SchemaBuilder.java +++ /dev/null @@ -1,767 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.project; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.link.data.DataType; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.core.loader.ResourceLoader; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.reflect.KeyedType; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.potion.PotionEffectType; -import org.jetbrains.annotations.NotNull; - -import java.awt.*; -import java.lang.reflect.Field; -import java.lang.reflect.InaccessibleObjectException; -import java.lang.reflect.Modifier; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -public class SchemaBuilder { - private static final String SYMBOL_LIMIT__N = "*"; - private static final String SYMBOL_TYPE__N = ""; - private static final JSONArray POTION_TYPES = getPotionTypes(); - private static final JSONArray ENCHANT_TYPES = getEnchantTypes(); - private static final JSONArray FONT_TYPES = new JSONArray(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()); - private final KMap definitions; - private final Class root; - private final KList warnings; - private final IrisData data; - - public SchemaBuilder(Class root, IrisData data) { - this.data = data; - warnings = new KList<>(); - this.definitions = new KMap<>(); - this.root = root; - } - - private static JSONArray getPotionTypes() { - JSONArray a = new JSONArray(); - - for (PotionEffectType gg : PotionEffectType.values()) { - a.put(gg.getName().toUpperCase().replaceAll("\\Q \\E", "_")); - } - - return a; - } - - private static JSONArray getEnchantTypes() { - JSONArray array = new JSONArray(); - for (Enchantment e : Enchantment.values()) { - array.put(e.getKey().getKey()); - } - return array; - } - - public JSONObject construct() { - JSONObject schema = new JSONObject(); - schema.put("$schema", "http://json-schema.org/draft-07/schema#"); - schema.put("$id", "https://volmit.com/iris-schema/" + root.getSimpleName().toLowerCase() + ".json"); - - JSONObject props = buildProperties(root); - - for (String i : props.keySet()) { - if (!schema.has(i)) { - schema.put(i, props.get(i)); - } - } - - JSONObject defs = new JSONObject(); - - for (Map.Entry entry : definitions.entrySet()) { - defs.put(entry.getKey(), entry.getValue()); - } - - schema.put("definitions", defs); - - for (String i : warnings) { - Iris.warn(root.getSimpleName() + ": " + i); - } - - return schema; - } - - private JSONObject buildProperties(Class c) { - JSONObject o = new JSONObject(); - JSONObject properties = new JSONObject(); - String desc = getDescription(c); - o.put("description", desc); - o.put("x-intellij-html-description", desc.replace("\n", "
")); - o.put("type", getType(c)); - JSONArray required = new JSONArray(); - JSONArray extended = new JSONArray(); - - var parent = c.getSuperclass(); - while (parent != null && IrisRegistrant.class.isAssignableFrom(parent)) { - buildProperties(properties, required, extended, parent); - parent = parent.getSuperclass(); - } - - buildProperties(properties, required, extended, c); - - if (required.length() > 0) { - o.put("required", required); - } - if (extended.length() > 0) { - o.put("allOf", extended); - } - - o.put("properties", properties); - - - return buildSnippet(o, c); - } - - private void buildProperties(JSONObject properties, JSONArray required, JSONArray extended, Class c) { - for (Field k : c.getDeclaredFields()) { - if (Modifier.isStatic(k.getModifiers()) || Modifier.isFinal(k.getModifiers()) || Modifier.isTransient(k.getModifiers())) { - continue; - } - - try { - k.setAccessible(true); - } catch (InaccessibleObjectException e) { - continue; - } - - JSONObject property = buildProperty(k, c); - - if (Boolean.TRUE == property.remove("!top")) { - extended.put(property); - continue; - } - - if (Boolean.TRUE == property.remove("!required")) { - required.put(k.getName()); - } - - properties.put(k.getName(), property); - } - } - - private JSONObject buildProperty(Field k, Class cl) { - JSONObject prop = new JSONObject(); - String type = getType(k.getType()); - KList description = new KList<>(); - prop.put("!required", k.isAnnotationPresent(Required.class)); - prop.put("type", type); - String fancyType = "Unknown Type"; - - switch (type) { - case "boolean" -> fancyType = "Boolean"; - case "integer" -> { - fancyType = "Integer"; - if (k.isAnnotationPresent(MinNumber.class)) { - int min = (int) k.getDeclaredAnnotation(MinNumber.class).value(); - prop.put("minimum", min); - description.add(SYMBOL_LIMIT__N + " Minimum allowed is " + min); - } - if (k.isAnnotationPresent(MaxNumber.class)) { - int max = (int) k.getDeclaredAnnotation(MaxNumber.class).value(); - prop.put("maximum", max); - description.add(SYMBOL_LIMIT__N + " Maximum allowed is " + max); - } - } - case "number" -> { - fancyType = "Number"; - if (k.isAnnotationPresent(MinNumber.class)) { - double min = k.getDeclaredAnnotation(MinNumber.class).value(); - prop.put("minimum", min); - description.add(SYMBOL_LIMIT__N + " Minimum allowed is " + min); - } - if (k.isAnnotationPresent(MaxNumber.class)) { - double max = k.getDeclaredAnnotation(MaxNumber.class).value(); - prop.put("maximum", max); - description.add(SYMBOL_LIMIT__N + " Maximum allowed is " + max); - } - } - case "string" -> { - fancyType = "Text"; - if (k.isAnnotationPresent(MinNumber.class)) { - int min = (int) k.getDeclaredAnnotation(MinNumber.class).value(); - prop.put("minLength", min); - description.add(SYMBOL_LIMIT__N + " Minimum Length allowed is " + min); - } - if (k.isAnnotationPresent(MaxNumber.class)) { - int max = (int) k.getDeclaredAnnotation(MaxNumber.class).value(); - prop.put("maxLength", max); - description.add(SYMBOL_LIMIT__N + " Maximum Length allowed is " + max); - } - - - if (k.isAnnotationPresent(RegistryListResource.class)) { - RegistryListResource rr = k.getDeclaredAnnotation(RegistryListResource.class); - ResourceLoader loader = data.getLoaders().get(rr.value()); - - if (loader != null) { - String key = "erz" + loader.getFolderName(); - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", new JSONArray(loader.getPossibleKeys())); - definitions.put(key, j); - } - - fancyType = "Iris " + loader.getResourceTypeName(); - prop.put("$ref", "#/definitions/" + key); - description.add(SYMBOL_TYPE__N + " Must be a valid " + loader.getFolderName() + " (use ctrl+space for auto complete!)"); - } else { - Iris.error("Cannot find Registry Loader for type " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName()); - } - } else if (k.isAnnotationPresent(RegistryListBlockType.class)) { - String key = "enum-block-type"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - JSONArray ja = new JSONArray(); - - for (String i : data.getBlockLoader().getPossibleKeys()) { - ja.put(i); - } - - for (String i : B.getBlockTypes()) { - ja.put(i); - } - - j.put("enum", ja); - definitions.put(key, j); - } - - fancyType = "Block Type"; - prop.put("$ref", "#/definitions/" + key); - description.add(SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)"); - - } else if (k.isAnnotationPresent(RegistryListItemType.class)) { - String key = "enum-item-type"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", B.getItemTypes()); - definitions.put(key, j); - } - - fancyType = "Item Type"; - prop.put("$ref", "#/definitions/" + key); - description.add(SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)"); - - } else if (k.isAnnotationPresent(RegistryListSpecialEntity.class)) { - String key = "enum-reg-specialentity"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - KList list = Iris.service(ExternalDataSVC.class) - .getAllIdentifiers(DataType.ENTITY) - .stream() - .map(Identifier::toString) - .collect(KList.collector()); - j.put("enum", list.toJSONStringArray()); - definitions.put(key, j); - } - - fancyType = "Custom Mob Type"; - prop.put("$ref", "#/definitions/" + key); - description.add(SYMBOL_TYPE__N + " Must be a valid Custom Mob Type (use ctrl+space for auto complete!)"); - } else if (k.isAnnotationPresent(RegistryListFont.class)) { - String key = "enum-font"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", FONT_TYPES); - definitions.put(key, j); - } - - fancyType = "Font Family"; - prop.put("$ref", "#/definitions/" + key); - description.add(SYMBOL_TYPE__N + " Must be a valid Font Family (use ctrl+space for auto complete!)"); - - } else if (k.isAnnotationPresent(RegistryListEnchantment.class)) { - String key = "enum-enchantment"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", ENCHANT_TYPES); - definitions.put(key, j); - } - - fancyType = "Enchantment Type"; - prop.put("$ref", "#/definitions/" + key); - description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)"); - } else if (k.isAnnotationPresent(RegistryListFunction.class)) { - var functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value(); - try { - var instance = functionClass.getDeclaredConstructor().newInstance(); - String key = instance.key(); - fancyType = instance.fancyName(); - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", instance.apply(data)); - definitions.put(key, j); - } - - prop.put("$ref", "#/definitions/" + key); - description.add(SYMBOL_TYPE__N + " Must be a valid " + fancyType + " (use ctrl+space for auto complete!)"); - } catch (Throwable e) { - Iris.error("Could not execute apply method in " + functionClass.getName()); - } - } else if (k.getType().equals(PotionEffectType.class)) { - String key = "enum-potion-effect-type"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", POTION_TYPES); - definitions.put(key, j); - } - - fancyType = "Potion Effect Type"; - prop.put("$ref", "#/definitions/" + key); - description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)"); - - } else if (KeyedType.isKeyed(k.getType())) { - fancyType = addEnum(k.getType(), prop, description, KeyedType.values(k.getType()), Function.identity()); - } else if (k.getType().isEnum()) { - fancyType = addEnum(k.getType(), prop, description, k.getType().getEnumConstants(), o -> ((Enum) o).name()); - } - } - case "object" -> { - //TODO add back descriptions - if (k.isAnnotationPresent(RegistryMapBlockState.class)) { - String blockType = k.getDeclaredAnnotation(RegistryMapBlockState.class).value(); - fancyType = "Block State"; - prop.put("!top", true); - JSONArray any = new JSONArray(); - prop.put("anyOf", any); - - B.getBlockStates().forEach((blocks, properties) -> { - if (blocks.isEmpty()) return; - - String raw = blocks.getFirst().replace(':', '_'); - String enumKey = "enum-block-state-" + raw; - String propertiesKey = "obj-block-state-" + raw; - - any.put(new JSONObject() - .put("if", new JSONObject() - .put("properties", new JSONObject() - .put(blockType, new JSONObject() - .put("type", "string") - .put("$ref", "#/definitions/" + enumKey)))) - .put("then", new JSONObject() - .put("properties", new JSONObject() - .put(k.getName(), new JSONObject() - .put("type", "object") - .put("$ref", "#/definitions/" + propertiesKey)))) - .put("else", false)); - - if (!definitions.containsKey(enumKey)) { - JSONArray filters = new JSONArray(); - blocks.forEach(filters::put); - - definitions.put(enumKey, new JSONObject() - .put("type", "string") - .put("enum", filters)); - } - - if (!definitions.containsKey(propertiesKey)) { - JSONObject props = new JSONObject(); - properties.forEach(property -> { - props.put(property.name(), property.buildJson()); - }); - - definitions.put(propertiesKey, new JSONObject() - .put("type", "object") - .put("properties", props)); - } - }); - } else { - fancyType = k.getType().getSimpleName().replaceAll("\\QIris\\E", "") + " (Object)"; - String key = "obj-" + k.getType().getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase(); - if (!definitions.containsKey(key)) { - definitions.put(key, new JSONObject()); - definitions.put(key, buildProperties(k.getType())); - } - prop.put("$ref", "#/definitions/" + key); - } - } - case "array" -> { - fancyType = "List of Something...?"; - ArrayType t = k.getDeclaredAnnotation(ArrayType.class); - if (t != null) { - if (t.min() > 0) { - prop.put("minItems", t.min()); - if (t.min() == 1) { - description.add(SYMBOL_LIMIT__N + " At least one entry must be defined, or just remove this list."); - } else { - description.add(SYMBOL_LIMIT__N + " Requires at least " + t.min() + " entries."); - } - } - - String arrayType = getType(t.type()); - - switch (arrayType) { - case "integer" -> fancyType = "List of Integers"; - case "number" -> fancyType = "List of Numbers"; - case "object" -> { - fancyType = "List of " + t.type().getSimpleName().replaceAll("\\QIris\\E", "") + "s (Objects)"; - String key = "obj-" + t.type().getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase(); - if (!definitions.containsKey(key)) { - definitions.put(key, new JSONObject()); - definitions.put(key, buildProperties(t.type())); - } - JSONObject items = new JSONObject(); - items.put("$ref", "#/definitions/" + key); - prop.put("items", items); - } - case "string" -> { - fancyType = "List of Text"; - - if (k.isAnnotationPresent(RegistryListResource.class)) { - RegistryListResource rr = k.getDeclaredAnnotation(RegistryListResource.class); - ResourceLoader loader = data.getLoaders().get(rr.value()); - - if (loader != null) { - fancyType = "List<" + loader.getResourceTypeName() + ">"; - String key = "erz" + loader.getFolderName(); - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", new JSONArray(loader.getPossibleKeys())); - definitions.put(key, j); - } - - JSONObject items = new JSONObject(); - items.put("$ref", "#/definitions/" + key); - prop.put("items", items); - description.add(SYMBOL_TYPE__N + " Must be a valid " + loader.getResourceTypeName() + " (use ctrl+space for auto complete!)"); - } else { - Iris.error("Cannot find Registry Loader for type (list schema) " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName()); - } - } else if (k.isAnnotationPresent(RegistryListBlockType.class)) { - fancyType = "List of Block Types"; - String key = "enum-block-type"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - JSONArray ja = new JSONArray(); - - for (String i : data.getBlockLoader().getPossibleKeys()) { - ja.put(i); - } - - for (String i : B.getBlockTypes()) { - ja.put(i); - } - - j.put("enum", ja); - definitions.put(key, j); - } - - JSONObject items = new JSONObject(); - items.put("$ref", "#/definitions/" + key); - prop.put("items", items); - description.add(SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)"); - } else if (k.isAnnotationPresent(RegistryListItemType.class)) { - fancyType = "List of Item Types"; - String key = "enum-item-type"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", B.getItemTypes()); - definitions.put(key, j); - } - - JSONObject items = new JSONObject(); - items.put("$ref", "#/definitions/" + key); - prop.put("items", items); - description.add(SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)"); - } else if (k.isAnnotationPresent(RegistryListFont.class)) { - String key = "enum-font"; - fancyType = "List of Font Families"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", FONT_TYPES); - definitions.put(key, j); - } - - JSONObject items = new JSONObject(); - items.put("$ref", "#/definitions/" + key); - prop.put("items", items); - description.add(SYMBOL_TYPE__N + " Must be a valid Font Family (use ctrl+space for auto complete!)"); - } else if (k.isAnnotationPresent(RegistryListEnchantment.class)) { - fancyType = "List of Enchantment Types"; - String key = "enum-enchantment"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", ENCHANT_TYPES); - definitions.put(key, j); - } - - JSONObject items = new JSONObject(); - items.put("$ref", "#/definitions/" + key); - prop.put("items", items); - description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)"); - } else if (k.isAnnotationPresent(RegistryListFunction.class)) { - var functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value(); - try { - var instance = functionClass.getDeclaredConstructor().newInstance(); - String key = instance.key(); - fancyType = instance.fancyName(); - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", instance.apply(data)); - definitions.put(key, j); - } - - JSONObject items = new JSONObject(); - items.put("$ref", "#/definitions/" + key); - prop.put("items", items); - description.add(SYMBOL_TYPE__N + " Must be a valid " + fancyType + " (use ctrl+space for auto complete!)"); - } catch (Throwable e) { - Iris.error("Could not execute apply method in " + functionClass.getName()); - } - } else if (t.type().equals(PotionEffectType.class)) { - fancyType = "List of Potion Effect Types"; - String key = "enum-potion-effect-type"; - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put("enum", POTION_TYPES); - definitions.put(key, j); - } - - JSONObject items = new JSONObject(); - items.put("$ref", "#/definitions/" + key); - prop.put("items", items); - description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)"); - } else if (KeyedType.isKeyed(t.type())) { - fancyType = addEnumList(prop, description, t, KeyedType.values(t.type()), Function.identity()); - } else if (t.type().isEnum()) { - fancyType = addEnumList(prop, description, t, t.type().getEnumConstants(), o -> ((Enum) o).name()); - } - } - } - } else { - warnings.add("Undefined array type for field " + k.getName() + " (" + k.getType().getSimpleName() + ") in class " + cl.getSimpleName()); - } - } - default -> - warnings.add("Unexpected Schema Type: " + type + " for field " + k.getName() + " (" + k.getType().getSimpleName() + ") in class " + cl.getSimpleName()); - } - - KList d = new KList<>(); - d.add("" + k.getName() + ""); - d.add(getFieldDescription(k) + "
"); - d.add("" + fancyType + ""); - String typeDesc = getDescription(k.getType()); - boolean present = !typeDesc.isBlank(); - if (present) d.add(typeDesc); - - Snippet snippet = k.getType().getDeclaredAnnotation(Snippet.class); - if (snippet == null) { - ArrayType array = k.getType().getDeclaredAnnotation(ArrayType.class); - if (array != null) { - snippet = array.type().getDeclaredAnnotation(Snippet.class); - } - } - - if (snippet != null) { - String sm = snippet.value(); - if (present) d.add(" "); - d.add("You can instead specify \"snippet/" + sm + "/some-name.json\" to use a snippet file instead of specifying it here."); - present = false; - } - - try { - k.setAccessible(true); - Object value = k.get(cl.newInstance()); - - if (value != null) { - if (present) d.add(" "); - if (value instanceof List) { - d.add(SYMBOL_LIMIT__N + " Default Value is an empty list"); - } else if (!k.getType().isPrimitive() && !(value instanceof Number) && !(value instanceof String) && !(value instanceof Enum) && !KeyedType.isKeyed(k.getType())) { - d.add(SYMBOL_LIMIT__N + " Default Value is a default object (create this object to see default properties)"); - } else { - d.add(SYMBOL_LIMIT__N + " Default Value is " + value); - } - } - } catch (Throwable ignored) { - - } - - description.forEach((g) -> d.add(g.trim())); - String desc = d.toString("\n") - .replace("
", "\n") - .replace("", "") - .replace("", ""); - String hDesc = d.toString("
"); - prop.put("type", type); - prop.put("description", desc); - prop.put("x-intellij-html-description", hDesc); - return buildSnippet(prop, k.getType()); - } - - private JSONObject buildSnippet(JSONObject prop, Class type) { - Snippet snippet = type.getDeclaredAnnotation(Snippet.class); - if (snippet == null) return prop; - - JSONObject anyOf = new JSONObject(); - JSONArray arr = new JSONArray(); - JSONObject str = new JSONObject(); - str.put("type", "string"); - String key = "enum-snippet-" + snippet.value(); - str.put("$ref", "#/definitions/" + key); - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - JSONArray snl = new JSONArray(); - data.getPossibleSnippets(snippet.value()).forEach(snl::put); - j.put("enum", snl); - definitions.put(key, j); - } - - arr.put(prop); - arr.put(str); - str.put("description", prop.getString("description")); - str.put("x-intellij-html-description", prop.getString("x-intellij-html-description")); - anyOf.put("anyOf", arr); - anyOf.put("description", prop.getString("description")); - anyOf.put("x-intellij-html-description", prop.getString("x-intellij-html-description")); - anyOf.put("!required", type.isAnnotationPresent(Required.class)); - - return anyOf; - } - - @NotNull - private String addEnumList(JSONObject prop, KList description, ArrayType t, T[] values, Function function) { - JSONObject items = new JSONObject(); - var s = addEnum(t.type(), items, description, values, function); - prop.put("items", items); - - return "List of " + s + "s"; - } - - @NotNull - private String addEnum(Class type, JSONObject prop, KList description, T[] values, Function function) { - JSONArray a = new JSONArray(); - boolean advanced = type.isAnnotationPresent(Desc.class); - for (T gg : values) { - if (advanced) { - try { - JSONObject j = new JSONObject(); - String name = function.apply(gg); - j.put("const", name); - Desc dd = type.getField(name).getAnnotation(Desc.class); - String desc = dd == null ? ("No Description for " + name) : dd.value(); - j.put("description", desc); - j.put("x-intellij-html-description", desc.replace("\n", "
")); - a.put(j); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } else { - a.put(function.apply(gg)); - } - } - - String key = (advanced ? "oneof-" : "") + "enum-" + type.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase(); - - if (!definitions.containsKey(key)) { - JSONObject j = new JSONObject(); - j.put(advanced ? "oneOf" : "enum", a); - definitions.put(key, j); - } - - prop.put("$ref", "#/definitions/" + key); - description.add(SYMBOL_TYPE__N + " Must be a valid " + type.getSimpleName().replaceAll("\\QIris\\E", "") + " (use ctrl+space for auto complete!)"); - return type.getSimpleName().replaceAll("\\QIris\\E", ""); - } - - private String getType(Class c) { - if (c.equals(int.class) || c.equals(Integer.class) || c.equals(long.class) || c.equals(Long.class)) { - return "integer"; - } - - if (c.equals(float.class) || c.equals(double.class) || c.equals(Float.class) || c.equals(Double.class)) { - return "number"; - } - - if (c.equals(boolean.class) || c.equals(Boolean.class)) { - return "boolean"; - } - - if (c.equals(String.class) || c.isEnum() || KeyedType.isKeyed(c)) { - return "string"; - } - - if (c.equals(KList.class)) { - return "array"; - } - - if (c.equals(KMap.class)) { - return "object"; - } - - if (!c.isAnnotationPresent(Desc.class) && c.getCanonicalName().startsWith("com.volmit.iris.")) { - warnings.addIfMissing("Unsupported Type: " + c.getCanonicalName() + " Did you forget @Desc?"); - } - - return "object"; - } - - private String getFieldDescription(Field r) { - - if (r.isAnnotationPresent(Desc.class)) { - return r.getDeclaredAnnotation(Desc.class).value(); - } - - // suppress warnings on bukkit classes - if (r.getDeclaringClass().getName().startsWith("org.bukkit.")) { - return "Bukkit package classes and enums have no descriptions"; - } - - warnings.addIfMissing("Missing @Desc on field " + r.getName() + " (" + r.getType() + ") in " + r.getDeclaringClass().getCanonicalName()); - return "No Field Description"; - } - - private String getDescription(Class r) { - if (r.isAnnotationPresent(Desc.class)) { - return r.getDeclaredAnnotation(Desc.class).value(); - } - - if (!r.isPrimitive() && !r.equals(KList.class) && !r.equals(KMap.class) && r.getCanonicalName().startsWith("com.volmit.")) { - warnings.addIfMissing("Missing @Desc on " + r.getSimpleName() + " in " + (r.getDeclaringClass() != null ? r.getDeclaringClass().getCanonicalName() : " NOSRC")); - } - return ""; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/scripting/environment/EngineEnvironment.java b/core/src/main/java/com/volmit/iris/core/scripting/environment/EngineEnvironment.java deleted file mode 100644 index efed2f8e6..000000000 --- a/core/src/main/java/com/volmit/iris/core/scripting/environment/EngineEnvironment.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.volmit.iris.core.scripting.environment; - -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.core.scripting.func.UpdateExecutor; -import com.volmit.iris.core.scripting.kotlin.environment.IrisExecutionEnvironment; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.mantle.MantleChunk; -import lombok.NonNull; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.entity.Entity; -import org.jetbrains.annotations.Nullable; - -public interface EngineEnvironment extends PackEnvironment { - static EngineEnvironment create(@NonNull Engine engine) { - return new IrisExecutionEnvironment(engine); - } - - @NonNull - Engine getEngine(); - - @Nullable - Object spawnMob(@NonNull String script, @NonNull Location location); - - void postSpawnMob(@NonNull String script, @NonNull Location location, @NonNull Entity mob); - - void preprocessObject(@NonNull String script, @NonNull IrisRegistrant object); - - void updateChunk(@NonNull String script, @NonNull MantleChunk mantleChunk, @NonNull Chunk chunk, @NonNull UpdateExecutor executor); -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/core/scripting/environment/PackEnvironment.java b/core/src/main/java/com/volmit/iris/core/scripting/environment/PackEnvironment.java deleted file mode 100644 index 4f0dfd6bb..000000000 --- a/core/src/main/java/com/volmit/iris/core/scripting/environment/PackEnvironment.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.volmit.iris.core.scripting.environment; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.scripting.kotlin.environment.IrisPackExecutionEnvironment; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.math.RNG; -import lombok.NonNull; -import org.jetbrains.annotations.Nullable; - -public interface PackEnvironment extends SimpleEnvironment { - static PackEnvironment create(@NonNull IrisData data) { - return new IrisPackExecutionEnvironment(data); - } - - @NonNull - IrisData getData(); - - @Nullable - Object createNoise(@NonNull String script, @NonNull RNG rng); - - EngineEnvironment with(@NonNull Engine engine); -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/core/scripting/environment/SimpleEnvironment.java b/core/src/main/java/com/volmit/iris/core/scripting/environment/SimpleEnvironment.java deleted file mode 100644 index 6d543cdca..000000000 --- a/core/src/main/java/com/volmit/iris/core/scripting/environment/SimpleEnvironment.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.volmit.iris.core.scripting.environment; - -import com.volmit.iris.core.scripting.kotlin.environment.IrisSimpleExecutionEnvironment; -import lombok.NonNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.util.Map; - -public interface SimpleEnvironment { - static SimpleEnvironment create() { - return new IrisSimpleExecutionEnvironment(); - } - - static SimpleEnvironment create(@NonNull File projectDir) { - return new IrisSimpleExecutionEnvironment(projectDir); - } - - void configureProject(); - - void execute(@NonNull String script); - - void execute(@NonNull String script, @NonNull Class type, @Nullable Map<@NonNull String, Object> vars); - - @Nullable - Object evaluate(@NonNull String script); - - @Nullable - Object evaluate(@NonNull String script, @NonNull Class type, @Nullable Map<@NonNull String, Object> vars); -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/core/scripting/func/BiomeLookup.java b/core/src/main/java/com/volmit/iris/core/scripting/func/BiomeLookup.java deleted file mode 100644 index d27247b1a..000000000 --- a/core/src/main/java/com/volmit/iris/core/scripting/func/BiomeLookup.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.volmit.iris.core.scripting.func; - -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.util.documentation.BlockCoordinates; - -@FunctionalInterface -public interface BiomeLookup { - @BlockCoordinates - IrisBiome at(int x, int z); -} diff --git a/core/src/main/java/com/volmit/iris/core/scripting/func/UpdateExecutor.java b/core/src/main/java/com/volmit/iris/core/scripting/func/UpdateExecutor.java deleted file mode 100644 index cf420733b..000000000 --- a/core/src/main/java/com/volmit/iris/core/scripting/func/UpdateExecutor.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.volmit.iris.core.scripting.func; - -import org.jetbrains.annotations.NotNull; - -@FunctionalInterface -public interface UpdateExecutor { - - @NotNull Runnable wrap(int delay, @NotNull Runnable runnable); - - @NotNull - default Runnable wrap(@NotNull Runnable runnable) { - return wrap(1, runnable); - } - - default void execute(@NotNull Runnable runnable) { - execute(1, runnable); - } - - default void execute(int delay, @NotNull Runnable runnable) { - wrap(delay, runnable).run(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/service/BoardSVC.java b/core/src/main/java/com/volmit/iris/core/service/BoardSVC.java deleted file mode 100644 index e8eadc558..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/BoardSVC.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.service; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.util.board.BoardManager; -import com.volmit.iris.util.board.BoardProvider; -import com.volmit.iris.util.board.BoardSettings; -import com.volmit.iris.util.board.ScoreDirection; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.scheduling.J; -import lombok.Data; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -public class BoardSVC implements IrisService, BoardProvider { - private final KMap boards = new KMap<>(); - private ScheduledExecutorService executor; - private BoardManager manager; - - @Override - public void onEnable() { - executor = Executors.newScheduledThreadPool(0, Thread.ofVirtual().factory()); - manager = new BoardManager(Iris.instance, BoardSettings.builder() - .boardProvider(this) - .scoreDirection(ScoreDirection.DOWN) - .build()); - } - - @Override - public void onDisable() { - executor.shutdownNow(); - manager.onDisable(); - boards.clear(); - } - - @EventHandler - public void on(PlayerChangedWorldEvent e) { - J.s(() -> updatePlayer(e.getPlayer())); - } - - @EventHandler - public void on(PlayerJoinEvent e) { - J.s(() -> updatePlayer(e.getPlayer())); - } - - @EventHandler - public void on(PlayerQuitEvent e) { - remove(e.getPlayer()); - } - - public void updatePlayer(Player p) { - if (IrisToolbelt.isIrisStudioWorld(p.getWorld())) { - manager.remove(p); - manager.setup(p); - } else remove(p); - } - - private void remove(Player player) { - manager.remove(player); - var board = boards.remove(player); - if (board != null) board.task.cancel(true); - } - - @Override - public String getTitle(Player player) { - return C.GREEN + "Iris"; - } - - @Override - public List getLines(Player player) { - return boards.computeIfAbsent(player, PlayerBoard::new).lines; - } - - @Data - public class PlayerBoard { - private final Player player; - private final ScheduledFuture task; - private volatile List lines; - - public PlayerBoard(Player player) { - this.player = player; - this.lines = new ArrayList<>(); - this.task = executor.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.SECONDS); - } - - private void tick() { - if (!Iris.service(StudioSVC.class).isProjectOpen()) { - return; - } - - update(); - } - - public void update() { - final World world = player.getWorld(); - final Location loc = player.getLocation(); - - final var access = IrisToolbelt.access(world); - if (access == null) return; - - final var engine = access.getEngine(); - if (engine == null) return; - - int x = loc.getBlockX(); - int y = loc.getBlockY() - world.getMinHeight(); - int z = loc.getBlockZ(); - - List lines = new ArrayList<>(this.lines.size()); - lines.add("&7&m "); - lines.add(C.GREEN + "Speed" + C.GRAY + ": " + Form.f(engine.getGeneratedPerSecond(), 0) + "/s " + Form.duration(1000D / engine.getGeneratedPerSecond(), 0)); - lines.add(C.AQUA + "Cache" + C.GRAY + ": " + Form.f(IrisData.cacheSize())); - lines.add(C.AQUA + "Mantle" + C.GRAY + ": " + engine.getMantle().getLoadedRegionCount()); - - if (IrisSettings.get().getGeneral().debug) { - lines.add(C.LIGHT_PURPLE + "Carving" + C.GRAY + ": " + engine.getMantle().isCarved(x,y,z)); - } - - lines.add("&7&m "); - lines.add(C.AQUA + "Region" + C.GRAY + ": " + engine.getRegion(x, z).getName()); - lines.add(C.AQUA + "Biome" + C.GRAY + ": " + engine.getBiomeOrMantle(x, y, z).getName()); - lines.add(C.AQUA + "Height" + C.GRAY + ": " + Math.round(engine.getHeight(x, z))); - lines.add(C.AQUA + "Slope" + C.GRAY + ": " + Form.f(engine.getComplex().getSlopeStream().get(x, z), 2)); - lines.add(C.AQUA + "BUD/s" + C.GRAY + ": " + Form.f(engine.getBlockUpdatesPerSecond())); - lines.add("&7&m "); - this.lines = lines; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/service/CommandSVC.java b/core/src/main/java/com/volmit/iris/core/service/CommandSVC.java deleted file mode 100644 index 7ca3ba94e..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/CommandSVC.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.service; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.commands.CommandIris; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.decree.DecreeContext; -import com.volmit.iris.util.decree.DecreeSystem; -import com.volmit.iris.util.decree.virtual.VirtualDecreeCommand; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.event.EventHandler; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.server.ServerCommandEvent; - -import java.util.Locale; -import java.util.concurrent.CompletableFuture; - -public class CommandSVC implements IrisService, DecreeSystem { - private final KMap> futures = new KMap<>(); - private final transient AtomicCache commandCache = new AtomicCache<>(); - private CompletableFuture consoleFuture = null; - - @Override - public void onEnable() { - Iris.instance.getCommand("iris").setExecutor(this); - J.a(() -> { - DecreeContext.touch(Iris.getSender()); - try { - getRoot().cacheAll(); - } finally { - DecreeContext.remove(); - } - }); - } - - @Override - public void onDisable() { - - } - - @EventHandler - public void on(PlayerCommandPreprocessEvent e) { - String msg = e.getMessage().startsWith("/") ? e.getMessage().substring(1) : e.getMessage(); - - if (msg.startsWith("irisdecree ")) { - String[] args = msg.split("\\Q \\E"); - CompletableFuture future = futures.get(args[1]); - - if (future != null) { - future.complete(args[2]); - e.setCancelled(true); - return; - } - } - - if ((msg.startsWith("locate ") || msg.startsWith("locatebiome ")) && IrisToolbelt.isIrisWorld(e.getPlayer().getWorld())) { - new VolmitSender(e.getPlayer()).sendMessage(C.RED + "Locating biomes & objects is disabled in Iris Worlds. Use /iris studio goto "); - e.setCancelled(true); - } - } - - @EventHandler - public void on(ServerCommandEvent e) { - if (consoleFuture != null && !consoleFuture.isCancelled() && !consoleFuture.isDone()) { - if (!e.getCommand().contains(" ")) { - String pick = e.getCommand().trim().toLowerCase(Locale.ROOT); - consoleFuture.complete(pick); - e.setCancelled(true); - } - } - } - - @Override - public VirtualDecreeCommand getRoot() { - return commandCache.aquireNastyPrint(() -> VirtualDecreeCommand.createRoot(new CommandIris())); - } - - public void post(String password, CompletableFuture future) { - futures.put(password, future); - } - - public void postConsole(CompletableFuture future) { - consoleFuture = future; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/service/ConversionSVC.java b/core/src/main/java/com/volmit/iris/core/service/ConversionSVC.java deleted file mode 100644 index 7786e5aed..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/ConversionSVC.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.service; - -import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.function.Consumer2; -import com.volmit.iris.util.io.Converter; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.nbt.io.NBTUtil; -import com.volmit.iris.util.nbt.io.NamedTag; -import com.volmit.iris.util.nbt.mca.NBTWorld; -import com.volmit.iris.util.nbt.tag.CompoundTag; -import com.volmit.iris.util.nbt.tag.IntTag; -import com.volmit.iris.util.nbt.tag.ListTag; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.Jigsaw; - -import java.io.File; -import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; - -public class ConversionSVC implements IrisService { - private KList converters; - private File folder; - - @Override - public void onEnable() { - folder = Iris.instance.getDataFolder("convert"); - converters = new KList<>(); - - J.s(() -> - J.attemptAsync(() -> - { - - }), 5); - } - - @Override - public void onDisable() { - - } - - private String toPoolName(String poolReference) { - return poolReference.split("\\Q:\\E")[1]; - } - - public void convertStructures(File in, File out, VolmitSender s) { - KMap pools = new KMap<>(); - KList roots = new KList<>(); - AtomicInteger total = new AtomicInteger(0); - AtomicInteger at = new AtomicInteger(0); - File destPools = new File(out.getAbsolutePath() + "/jigsaw-pools"); - destPools.mkdirs(); - findAllNBT(in, (folder, file) -> { - total.getAndIncrement(); - if (roots.addIfMissing(folder)) { - String b = in.toURI().relativize(folder.toURI()).getPath(); - if (b.startsWith("/")) { - b = b.substring(1); - } - - if (b.endsWith("/")) { - b = b.substring(0, b.length() - 1); - } - - pools.put(b, new IrisJigsawPool()); - } - }); - findAllNBT(in, (folder, file) -> { - at.getAndIncrement(); - String b = in.toURI().relativize(folder.toURI()).getPath(); - if (b.startsWith("/")) { - b = b.substring(1); - } - - if (b.endsWith("/")) { - b = b.substring(0, b.length() - 1); - } - IrisJigsawPool jpool = pools.get(b); - File destObjects = new File(out.getAbsolutePath() + "/objects/" + in.toURI().relativize(folder.toURI()).getPath()); - File destPieces = new File(out.getAbsolutePath() + "/jigsaw-pieces/" + in.toURI().relativize(folder.toURI()).getPath()); - destObjects.mkdirs(); - destPieces.mkdirs(); - - try { - NamedTag tag = NBTUtil.read(file); - CompoundTag compound = (CompoundTag) tag.getTag(); - - if (compound.containsKey("blocks") && compound.containsKey("palette") && compound.containsKey("size")) { - String id = in.toURI().relativize(folder.toURI()).getPath() + file.getName().split("\\Q.\\E")[0]; - @SuppressWarnings("unchecked") ListTag size = (ListTag) compound.getListTag("size"); - int w = size.get(0).asInt(); - int h = size.get(1).asInt(); - int d = size.get(2).asInt(); - KList palette = new KList<>(); - @SuppressWarnings("unchecked") ListTag paletteList = (ListTag) compound.getListTag("palette"); - for (int i = 0; i < paletteList.size(); i++) { - CompoundTag cp = paletteList.get(i); - palette.add(NBTWorld.getBlockData(cp)); - } - IrisJigsawPiece piece = new IrisJigsawPiece(); - IrisObject object = new IrisObject(w, h, d); - @SuppressWarnings("unchecked") ListTag blockList = (ListTag) compound.getListTag("blocks"); - for (int i = 0; i < blockList.size(); i++) { - CompoundTag cp = blockList.get(i); - @SuppressWarnings("unchecked") ListTag pos = (ListTag) cp.getListTag("pos"); - int x = pos.get(0).asInt(); - int y = pos.get(1).asInt(); - int z = pos.get(2).asInt(); - BlockData bd = palette.get(cp.getInt("state")).clone(); - - if (bd.getMaterial().equals(Material.JIGSAW) && cp.containsKey("nbt")) { - piece.setObject(in.toURI().relativize(folder.toURI()).getPath() + file.getName().split("\\Q.\\E")[0]); - IrisPosition spos = new IrisPosition(object.getSigned(x, y, z)); - CompoundTag nbt = cp.getCompoundTag("nbt"); - CompoundTag finalState = new CompoundTag(); - finalState.putString("Name", nbt.getString("final_state")); - BlockData jd = bd.clone(); - bd = NBTWorld.getBlockData(finalState); - String joint = nbt.getString("joint"); - String pool = nbt.getString("pool"); - String poolId = toPoolName(pool); - String name = nbt.getString("name"); - String target = nbt.getString("target"); - pools.computeIfAbsent(poolId, (k) -> new IrisJigsawPool()); - IrisJigsawPieceConnector connector = new IrisJigsawPieceConnector(); - connector.setName(name); - connector.setTargetName(target); - connector.setRotateConnector(false); - connector.setPosition(spos); - connector.getPools().add(poolId); - connector.setDirection(IrisDirection.getDirection(((Jigsaw) jd).getOrientation())); - - if (target.equals("minecraft:building_entrance")) { - connector.setInnerConnector(true); - } - - piece.getConnectors().add(connector); - } - - if (!bd.getMaterial().equals(Material.STRUCTURE_VOID) && !bd.getMaterial().equals(Material.AIR)) { - object.setUnsigned(x, y, z, bd); - } - } - - jpool.getPieces().addIfMissing(id); - object.write(new File(destObjects, file.getName().split("\\Q.\\E")[0] + ".iob")); - IO.writeAll(new File(destPieces, file.getName().split("\\Q.\\E")[0] + ".json"), new JSONObject(new Gson().toJson(piece)).toString(4)); - Iris.info("[Jigsaw]: (" + Form.pc((double) at.get() / (double) total.get(), 0) + ") Exported Piece: " + id); - } - } catch (Throwable e) { - e.printStackTrace(); - Iris.reportError(e); - } - }); - - for (String i : pools.k()) { - try { - IO.writeAll(new File(destPools, i + ".json"), new JSONObject(new Gson().toJson(pools.get(i))).toString(4)); - } catch (IOException e) { - e.printStackTrace(); - Iris.reportError(e); - } - } - - Iris.info("Done! Exported " + Form.f((total.get() * 2) + pools.size()) + " Files!"); - } - - public void findAllNBT(File path, Consumer2 inFile) { - if (path == null) { - return; - } - - if (path.isFile() && path.getName().endsWith(".nbt")) { - inFile.accept(path.getParentFile(), path); - return; - } - - for (File i : path.listFiles()) { - if (i.isDirectory()) { - findAllNBT(i, inFile); - } else if (i.isFile() && i.getName().endsWith(".nbt")) { - inFile.accept(path, i); - } - } - } - - public void check(VolmitSender s) { - int m = 0; - Iris.instance.getDataFolder("convert"); - - for (File i : folder.listFiles()) { - for (Converter j : converters) { - if (i.getName().endsWith("." + j.getInExtension())) { - File out = new File(folder, i.getName().replaceAll("\\Q." + j.getInExtension() + "\\E", "." + j.getOutExtension())); - m++; - j.convert(i, out); - s.sendMessage("Converted " + i.getName() + " -> " + out.getName()); - } - } - - if (i.isDirectory() && i.getName().equals("structures")) { - File f = new File(folder, "jigsaw"); - - if (!f.exists()) { - s.sendMessage("Converting NBT Structures into Iris Jigsaw Structures..."); - f.mkdirs(); - J.a(() -> convertStructures(i, f, s)); - } - } - } - - s.sendMessage("Converted " + m + " File" + (m == 1 ? "" : "s")); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/service/EditSVC.java b/core/src/main/java/com/volmit/iris/core/service/EditSVC.java deleted file mode 100644 index dc944f1f2..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/EditSVC.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.service; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.edit.BlockEditor; -import com.volmit.iris.core.edit.BukkitBlockEditor; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.plugin.IrisService; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; -import org.bukkit.event.EventHandler; -import org.bukkit.event.world.WorldUnloadEvent; - -public class EditSVC implements IrisService { - private KMap editors; - public static boolean deletingWorld = false; - - @Override - public void onEnable() { - this.editors = new KMap<>(); - Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, this::update, 1000, 1000); - } - - @Override - public void onDisable() { - flushNow(); - } - - public BlockData get(World world, int x, int y, int z) { - return open(world).get(x, y, z); - } - - public void set(World world, int x, int y, int z, BlockData d) { - open(world).set(x, y, z, d); - } - - public void setBiome(World world, int x, int y, int z, Biome d) { - open(world).setBiome(x, y, z, d); - } - - public void setBiome(World world, int x, int z, Biome d) { - open(world).setBiome(x, z, d); - } - - public Biome getBiome(World world, int x, int y, int z) { - return open(world).getBiome(x, y, z); - } - - public Biome getBiome(World world, int x, int z) { - return open(world).getBiome(x, z); - } - - @EventHandler - public void on(WorldUnloadEvent e) { - if (editors.containsKey(e.getWorld()) && !deletingWorld) { - editors.remove(e.getWorld()).close(); - } - } - - - public void update() { - for (World i : editors.k()) { - if (M.ms() - editors.get(i).last() > 1000) { - editors.remove(i).close(); - } - } - } - - public void flushNow() { - for (World i : editors.k()) { - editors.remove(i).close(); - } - } - - public BlockEditor open(World world) { - if (editors.containsKey(world)) { - return editors.get(world); - } - - BlockEditor e = new BukkitBlockEditor(world); - editors.put(world, e); - - return e; - } - -} diff --git a/core/src/main/java/com/volmit/iris/core/service/ExternalDataSVC.java b/core/src/main/java/com/volmit/iris/core/service/ExternalDataSVC.java deleted file mode 100644 index 528e41869..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/ExternalDataSVC.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.service; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.*; -import com.volmit.iris.core.link.data.DataType; -import com.volmit.iris.core.nms.container.BlockProperty; -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.io.JarScanner; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.scheduling.J; -import lombok.Data; -import lombok.NonNull; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.bukkit.entity.Entity; -import org.bukkit.event.EventHandler; -import org.bukkit.event.server.PluginEnableEvent; -import org.bukkit.inventory.ItemStack; - -import java.util.*; -import java.util.stream.Collectors; - -@Data -public class ExternalDataSVC implements IrisService { - - private KList providers = new KList<>(), activeProviders = new KList<>(); - - @Override - public void onEnable() { - Iris.info("Loading ExternalDataProvider..."); - Bukkit.getPluginManager().registerEvents(this, Iris.instance); - - providers.addAll(createProviders()); - for (ExternalDataProvider p : providers) { - if (p.isReady()) { - activeProviders.add(p); - p.init(); - Iris.instance.registerListener(p); - Iris.info("Enabled ExternalDataProvider for %s.", p.getPluginId()); - } - } - } - - @Override - public void onDisable() { - } - - @EventHandler - public void onPluginEnable(PluginEnableEvent e) { - if (activeProviders.stream().noneMatch(p -> e.getPlugin().equals(p.getPlugin()))) { - providers.stream().filter(p -> p.isReady() && e.getPlugin().equals(p.getPlugin())).findFirst().ifPresent(edp -> { - activeProviders.add(edp); - edp.init(); - Iris.instance.registerListener(edp); - Iris.info("Enabled ExternalDataProvider for %s.", edp.getPluginId()); - }); - } - } - - public void registerProvider(@NonNull ExternalDataProvider provider) { - String plugin = provider.getPluginId(); - if (providers.stream().map(ExternalDataProvider::getPluginId).anyMatch(plugin::equals)) - throw new IllegalArgumentException("A provider with the same plugin id already exists."); - - providers.add(provider); - if (provider.isReady()) { - activeProviders.add(provider); - provider.init(); - Iris.instance.registerListener(provider); - } - } - - public Optional getBlockData(final Identifier key) { - var pair = parseState(key); - Identifier mod = pair.getA(); - - Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(mod, DataType.BLOCK)).findFirst(); - if (provider.isEmpty()) - return Optional.empty(); - try { - return Optional.of(provider.get().getBlockData(mod, pair.getB())); - } catch (MissingResourceException e) { - Iris.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]"); - return Optional.empty(); - } - } - - public Optional> getBlockProperties(final Identifier key) { - Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(key, DataType.BLOCK)).findFirst(); - if (provider.isEmpty()) - return Optional.empty(); - try { - return Optional.of(provider.get().getBlockProperties(key)); - } catch (MissingResourceException e) { - Iris.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]"); - return Optional.empty(); - } - } - - public Optional getItemStack(Identifier key, KMap customNbt) { - Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(key, DataType.ITEM)).findFirst(); - if (provider.isEmpty()) { - Iris.warn("No matching Provider found for modded material \"%s\"!", key); - return Optional.empty(); - } - try { - return Optional.of(provider.get().getItemStack(key, customNbt)); - } catch (MissingResourceException e) { - Iris.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]"); - return Optional.empty(); - } - } - - public void processUpdate(Engine engine, Block block, Identifier blockId) { - Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(blockId, DataType.BLOCK)).findFirst(); - if (provider.isEmpty()) { - Iris.warn("No matching Provider found for modded material \"%s\"!", blockId); - return; - } - provider.get().processUpdate(engine, block, blockId); - } - - public Entity spawnMob(Location location, Identifier mobId) { - Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(mobId, DataType.ENTITY)).findFirst(); - if (provider.isEmpty()) { - Iris.warn("No matching Provider found for modded mob \"%s\"!", mobId); - return null; - } - try { - return provider.get().spawnMob(location, mobId); - } catch (MissingResourceException e) { - Iris.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]"); - return null; - } - } - - public Collection getAllIdentifiers(DataType dataType) { - return activeProviders.stream() - .flatMap(p -> p.getTypes(dataType).stream()) - .toList(); - } - - public Collection>> getAllBlockProperties() { - return activeProviders.stream() - .flatMap(p -> p.getTypes(DataType.BLOCK) - .stream() - .map(id -> new Pair<>(id, p.getBlockProperties(id)))) - .toList(); - } - - public static Pair> parseState(Identifier key) { - if (!key.key().contains("[") || !key.key().contains("]")) { - return new Pair<>(key, new KMap<>()); - } - String state = key.key().split("\\Q[\\E")[1].split("\\Q]\\E")[0]; - KMap stateMap = new KMap<>(); - if (!state.isEmpty()) { - Arrays.stream(state.split(",")).forEach(s -> stateMap.put(s.split("=")[0], s.split("=")[1])); - } - return new Pair<>(new Identifier(key.namespace(), key.key().split("\\Q[\\E")[0]), stateMap); - } - - public static Identifier buildState(Identifier key, KMap state) { - if (state.isEmpty()) { - return key; - } - String path = state.entrySet() - .stream() - .map(e -> e.getKey() + "=" + e.getValue()) - .collect(Collectors.joining(",", key.key() + "[", "]")); - return new Identifier(key.namespace(), path); - } - - private static KList createProviders() { - JarScanner jar = new JarScanner(Iris.instance.getJarFile(), "com.volmit.iris.core.link.data", false); - J.attempt(jar::scan); - KList providers = new KList<>(); - - for (Class c : jar.getClasses()) { - if (ExternalDataProvider.class.isAssignableFrom(c)) { - try { - ExternalDataProvider p = (ExternalDataProvider) c.getDeclaredConstructor().newInstance(); - if (p.getPlugin() != null) Iris.info(p.getPluginId() + " found, loading " + c.getSimpleName() + "..."); - providers.add(p); - } catch (Throwable ignored) {} - } - } - return providers; - } -} diff --git a/core/src/main/java/com/volmit/iris/core/service/GlobalCacheSVC.java b/core/src/main/java/com/volmit/iris/core/service/GlobalCacheSVC.java deleted file mode 100644 index 4d6e2db76..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/GlobalCacheSVC.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.volmit.iris.core.service; - -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.pregenerator.cache.PregenCache; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.scheduling.Looper; -import lombok.NonNull; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.world.ChunkLoadEvent; -import org.bukkit.event.world.WorldInitEvent; -import org.bukkit.event.world.WorldUnloadEvent; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.lang.ref.Reference; -import java.lang.ref.WeakReference; -import java.util.function.Function; - -public class GlobalCacheSVC implements IrisService { - private static final KMap> REFERENCE_CACHE = new KMap<>(); - private final KMap globalCache = new KMap<>(); - private transient boolean lastState; - private static boolean disabled = true; - private Looper trimmer; - - @Override - public void onEnable() { - disabled = false; - trimmer = new Looper() { - @Override - protected long loop() { - var it = REFERENCE_CACHE.values().iterator(); - while (it.hasNext()) { - var cache = it.next().get(); - if (cache == null) it.remove(); - else cache.trim(10_000); - } - return disabled ? -1 : 2_000; - } - }; - trimmer.start(); - lastState = !IrisSettings.get().getWorld().isGlobalPregenCache(); - if (lastState) return; - Bukkit.getWorlds().forEach(this::createCache); - } - - @Override - public void onDisable() { - disabled = true; - try { - trimmer.join(); - } catch (InterruptedException ignored) {} - globalCache.qclear((world, cache) -> cache.write()); - } - - @Nullable - public PregenCache get(@NonNull World world) { - return globalCache.get(world.getName()); - } - - @Nullable - public PregenCache get(@NonNull String world) { - return globalCache.get(world); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void on(WorldInitEvent event) { - if (isDisabled()) return; - createCache(event.getWorld()); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void on(WorldUnloadEvent event) { - var cache = globalCache.remove(event.getWorld().getName()); - if (cache == null) return; - cache.write(); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void on(ChunkLoadEvent event) { - var cache = get(event.getWorld()); - if (cache == null) return; - cache.cacheChunk(event.getChunk().getX(), event.getChunk().getZ()); - } - - private void createCache(World world) { - if (!IrisToolbelt.isIrisWorld(world)) return; - globalCache.computeIfAbsent(world.getName(), GlobalCacheSVC::createDefault); - } - - private boolean isDisabled() { - boolean conf = IrisSettings.get().getWorld().isGlobalPregenCache(); - if (lastState != conf) - return lastState; - - if (conf) { - Bukkit.getWorlds().forEach(this::createCache); - } else { - globalCache.values().removeIf(cache -> { - cache.write(); - return true; - }); - } - - return lastState = !conf; - } - - - @NonNull - public static PregenCache createCache(@NonNull String worldName, @NonNull Function provider) { - PregenCache[] holder = new PregenCache[1]; - REFERENCE_CACHE.compute(worldName, (name, ref) -> { - if (ref != null) { - if ((holder[0] = ref.get()) != null) - return ref; - } - return new WeakReference<>(holder[0] = provider.apply(worldName)); - }); - return holder[0]; - } - - @NonNull - public static PregenCache createDefault(@NonNull String worldName) { - return createCache(worldName, GlobalCacheSVC::createDefault0); - } - - private static PregenCache createDefault0(String worldName) { - if (disabled) return PregenCache.EMPTY; - return PregenCache.create(new File(Bukkit.getWorldContainer(), String.join(File.separator, worldName, "iris", "pregen"))).sync(); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/service/IrisEngineSVC.java b/core/src/main/java/com/volmit/iris/core/service/IrisEngineSVC.java deleted file mode 100644 index 36e4b6d98..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/IrisEngineSVC.java +++ /dev/null @@ -1,277 +0,0 @@ -package com.volmit.iris.core.service; - -import com.google.common.util.concurrent.AtomicDouble; -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.loader.ResourceLoader; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.Looper; -import com.volmit.iris.util.stream.utility.CachedStream2D; -import com.volmit.iris.util.stream.utility.CachedStream3D; -import lombok.Synchronized; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.world.WorldLoadEvent; -import org.bukkit.event.world.WorldUnloadEvent; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -public class IrisEngineSVC implements IrisService { - private static final int TRIM_PERIOD = 2_000; - private final AtomicInteger tectonicLimit = new AtomicInteger(30); - private final AtomicInteger tectonicPlates = new AtomicInteger(); - private final AtomicInteger queuedTectonicPlates = new AtomicInteger(); - private final AtomicInteger trimmerAlive = new AtomicInteger(); - private final AtomicInteger unloaderAlive = new AtomicInteger(); - private final AtomicInteger totalWorlds = new AtomicInteger(); - private final AtomicDouble maxIdleDuration = new AtomicDouble(); - private final AtomicDouble minIdleDuration = new AtomicDouble(); - private final AtomicLong loadedChunks = new AtomicLong(); - private final KMap worlds = new KMap<>(); - private ScheduledExecutorService service; - private Looper updateTicker; - - @Override - public void onEnable() { - var settings = IrisSettings.get().getPerformance(); - var engine = settings.getEngineSVC(); - service = Executors.newScheduledThreadPool(0, - (engine.isUseVirtualThreads() - ? Thread.ofVirtual() - : Thread.ofPlatform().priority(engine.getPriority())) - .name("Iris EngineSVC-", 0) - .factory()); - tectonicLimit.set(settings.getTectonicPlateSize()); - Bukkit.getWorlds().forEach(this::add); - setup(); - } - - @Override - public void onDisable() { - service.shutdown(); - updateTicker.interrupt(); - worlds.keySet().forEach(this::remove); - worlds.clear(); - } - - public void engineStatus(VolmitSender sender) { - long[] sizes = new long[4]; - long[] count = new long[4]; - - for (var cache : Iris.service(PreservationSVC.class).getCaches()) { - var type = switch (cache) { - case ResourceLoader ignored -> 0; - case CachedStream2D ignored -> 1; - case CachedStream3D ignored -> 2; - default -> 3; - }; - - sizes[type] += cache.getSize(); - count[type]++; - } - - sender.sendMessage(C.DARK_PURPLE + "-------------------------"); - sender.sendMessage(C.DARK_PURPLE + "Status:"); - sender.sendMessage(C.DARK_PURPLE + "- Service: " + C.LIGHT_PURPLE + (service.isShutdown() ? "Shutdown" : "Running")); - sender.sendMessage(C.DARK_PURPLE + "- Updater: " + C.LIGHT_PURPLE + (updateTicker.isAlive() ? "Running" : "Stopped")); - sender.sendMessage(C.DARK_PURPLE + "- Period: " + C.LIGHT_PURPLE + Form.duration(TRIM_PERIOD)); - sender.sendMessage(C.DARK_PURPLE + "- Trimmers: " + C.LIGHT_PURPLE + trimmerAlive.get()); - sender.sendMessage(C.DARK_PURPLE + "- Unloaders: " + C.LIGHT_PURPLE + unloaderAlive.get()); - sender.sendMessage(C.DARK_PURPLE + "Tectonic Plates:"); - sender.sendMessage(C.DARK_PURPLE + "- Limit: " + C.LIGHT_PURPLE + tectonicLimit.get()); - sender.sendMessage(C.DARK_PURPLE + "- Total: " + C.LIGHT_PURPLE + tectonicPlates.get()); - sender.sendMessage(C.DARK_PURPLE + "- Queued: " + C.LIGHT_PURPLE + queuedTectonicPlates.get()); - sender.sendMessage(C.DARK_PURPLE + "- Max Idle Duration: " + C.LIGHT_PURPLE + Form.duration(maxIdleDuration.get(), 2)); - sender.sendMessage(C.DARK_PURPLE + "- Min Idle Duration: " + C.LIGHT_PURPLE + Form.duration(minIdleDuration.get(), 2)); - sender.sendMessage(C.DARK_PURPLE + "Caches:"); - sender.sendMessage(C.DARK_PURPLE + "- Resource: " + C.LIGHT_PURPLE + sizes[0] + " (" + count[0] + ")"); - sender.sendMessage(C.DARK_PURPLE + "- 2D Stream: " + C.LIGHT_PURPLE + sizes[1] + " (" + count[1] + ")"); - sender.sendMessage(C.DARK_PURPLE + "- 3D Stream: " + C.LIGHT_PURPLE + sizes[2] + " (" + count[2] + ")"); - sender.sendMessage(C.DARK_PURPLE + "- Other: " + C.LIGHT_PURPLE + sizes[3] + " (" + count[3] + ")"); - sender.sendMessage(C.DARK_PURPLE + "Other:"); - sender.sendMessage(C.DARK_PURPLE + "- Iris Worlds: " + C.LIGHT_PURPLE + totalWorlds.get()); - sender.sendMessage(C.DARK_PURPLE + "- Loaded Chunks: " + C.LIGHT_PURPLE + loadedChunks.get()); - sender.sendMessage(C.DARK_PURPLE + "-------------------------"); - } - - @EventHandler - public void onWorldUnload(WorldUnloadEvent event) { - remove(event.getWorld()); - } - - @EventHandler - public void onWorldLoad(WorldLoadEvent event) { - add(event.getWorld()); - } - - private void remove(World world) { - var entry = worlds.remove(world); - if (entry == null) return; - entry.close(); - } - - private void add(World world) { - var access = IrisToolbelt.access(world); - if (access == null) return; - worlds.put(world, new Registered(world.getName(), access)); - } - - private synchronized void setup() { - if (updateTicker != null && updateTicker.isAlive()) - return; - - updateTicker = new Looper() { - @Override - protected long loop() { - try { - int queuedPlates = 0; - int totalPlates = 0; - long chunks = 0; - int unloaders = 0; - int trimmers = 0; - int iris = 0; - - double maxDuration = Long.MIN_VALUE; - double minDuration = Long.MAX_VALUE; - for (var entry : worlds.entrySet()) { - var registered = entry.getValue(); - if (registered.closed) continue; - - iris++; - if (registered.unloaderAlive()) unloaders++; - if (registered.trimmerAlive()) trimmers++; - - var engine = registered.getEngine(); - if (engine == null) continue; - - queuedPlates += engine.getMantle().getUnloadRegionCount(); - totalPlates += engine.getMantle().getLoadedRegionCount(); - chunks += entry.getKey().getLoadedChunks().length; - - double duration = engine.getMantle().getAdjustedIdleDuration(); - if (duration > maxDuration) maxDuration = duration; - if (duration < minDuration) minDuration = duration; - } - - trimmerAlive.set(trimmers); - unloaderAlive.set(unloaders); - tectonicPlates.set(totalPlates); - queuedTectonicPlates.set(queuedPlates); - maxIdleDuration.set(maxDuration); - minIdleDuration.set(minDuration); - loadedChunks.set(chunks); - totalWorlds.set(iris); - - worlds.values().forEach(Registered::update); - } catch (Throwable e) { - e.printStackTrace(); - } - return 1000; - } - }; - updateTicker.start(); - } - - private final class Registered { - private final String name; - private final PlatformChunkGenerator access; - private final int offset = RNG.r.nextInt(TRIM_PERIOD); - private transient ScheduledFuture trimmer; - private transient ScheduledFuture unloader; - private transient boolean closed; - - private Registered(String name, PlatformChunkGenerator access) { - this.name = name; - this.access = access; - update(); - } - - private boolean unloaderAlive() { - return unloader != null && !unloader.isDone() && !unloader.isCancelled(); - } - - private boolean trimmerAlive() { - return trimmer != null && !trimmer.isDone() && !trimmer.isCancelled(); - } - - @Synchronized - private void update() { - if (closed || service == null || service.isShutdown()) - return; - - if (trimmer == null || trimmer.isDone() || trimmer.isCancelled()) { - trimmer = service.scheduleAtFixedRate(() -> { - Engine engine = getEngine(); - if (engine == null || !engine.getMantle().getMantle().shouldReduce(engine)) - return; - - try { - engine.getMantle().trim(tectonicLimit()); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("EngineSVC: Failed to trim for " + name); - e.printStackTrace(); - } - }, offset, TRIM_PERIOD, TimeUnit.MILLISECONDS); - } - - if (unloader == null || unloader.isDone() || unloader.isCancelled()) { - unloader = service.scheduleAtFixedRate(() -> { - Engine engine = getEngine(); - if (engine == null || !engine.getMantle().getMantle().shouldReduce(engine)) - return; - - try { - long unloadStart = System.currentTimeMillis(); - int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : tectonicLimit()); - if (count > 0) { - Iris.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2)); - } - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("EngineSVC: Failed to unload for " + name); - e.printStackTrace(); - } - }, offset + TRIM_PERIOD / 2, TRIM_PERIOD, TimeUnit.MILLISECONDS); - } - } - - private int tectonicLimit() { - return tectonicLimit.get() / Math.max(worlds.size(), 1); - } - - @Synchronized - private void close() { - if (closed) return; - closed = true; - - if (trimmer != null) { - trimmer.cancel(false); - trimmer = null; - } - - if (unloader != null) { - unloader.cancel(false); - unloader = null; - } - } - - @Nullable - private Engine getEngine() { - if (closed) return null; - return access.getEngine(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/service/ObjectSVC.java b/core/src/main/java/com/volmit/iris/core/service/ObjectSVC.java deleted file mode 100644 index 5fadf8b4a..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/ObjectSVC.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.service; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.scheduling.J; -import lombok.Getter; -import org.bukkit.Bukkit; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Iterator; -import java.util.Map; - -public class ObjectSVC implements IrisService { - - @Getter - private final Deque> undos = new ArrayDeque<>(); - - - @Override - public void onEnable() { - - } - - @Override - public void onDisable() { - - } - - public void addChanges(Map oldBlocks) { - undos.add(oldBlocks); - } - - public void revertChanges(int amount) { - loopChange(amount); - } - - private void loopChange(int amount) { - if (undos.size() > 0) { - revert(undos.pollLast()); - if (amount > 1) { - J.s(() -> loopChange(amount - 1), 2); - } - } - } - - /** - * Reverts all the block changes provided, 200 blocks per tick - * - * @param blocks The blocks to remove - */ - private void revert(Map blocks) { - Iterator> it = blocks.entrySet().iterator(); - Bukkit.getScheduler().runTask(Iris.instance, () -> { - int amount = 0; - while (it.hasNext()) { - Map.Entry entry = it.next(); - BlockData data = entry.getValue(); - entry.getKey().setBlockData(data, false); - - it.remove(); - - amount++; - - if (amount > 200) { - J.s(() -> revert(blocks), 1); - } - } - }); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/core/service/PreservationSVC.java b/core/src/main/java/com/volmit/iris/core/service/PreservationSVC.java deleted file mode 100644 index 2fb0e8d74..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/PreservationSVC.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.service; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.MeteredCache; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.data.KCache; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.scheduling.Looper; -import org.jetbrains.annotations.Unmodifiable; - -import java.lang.ref.WeakReference; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class PreservationSVC implements IrisService { - private final List threads = new CopyOnWriteArrayList<>(); - private final List services = new CopyOnWriteArrayList<>(); - private final List> caches = new CopyOnWriteArrayList<>(); - private Looper dereferencer; - - public void register(Thread t) { - threads.add(t); - } - - public void register(ExecutorService service) { - services.add(service); - } - - public void printCaches() { - var c = getCaches(); - long s = 0; - long m = 0; - double p = 0; - double mf = Math.max(c.size(), 1); - - for (MeteredCache i : c) { - s += i.getSize(); - m += i.getMaxSize(); - p += i.getUsage(); - } - - Iris.info("Cached " + Form.f(s) + " / " + Form.f(m) + " (" + Form.pc(p / mf) + ") from " + caches.size() + " Caches"); - } - - public void dereference() { - IrisContext.dereference(); - IrisData.dereference(); - threads.removeIf((i) -> !i.isAlive()); - services.removeIf(ExecutorService::isShutdown); - updateCaches(); - } - - @Override - public void onEnable() { - /* - * Dereferences copies of Engine instances that are closed to prevent memory from - * hanging around and keeping copies of complex, caches and other dead objects. - */ - dereferencer = new Looper() { - @Override - protected long loop() { - dereference(); - return 60000; - } - }; - } - - @Override - public void onDisable() { - dereferencer.interrupt(); - dereference(); - - postShutdown(() -> { - for (Thread i : threads) { - if (i.isAlive()) { - try { - i.interrupt(); - Iris.info("Shutdown Thread " + i.getName()); - } catch (Throwable e) { - Iris.reportError(e); - } - } - } - - for (ExecutorService i : services) { - try { - i.shutdownNow(); - Iris.info("Shutdown Executor Service " + i); - } catch (Throwable e) { - Iris.reportError(e); - } - } - }); - } - - public void updateCaches() { - caches.removeIf(ref -> { - var c = ref.get(); - return c == null || c.isClosed(); - }); - } - - public void registerCache(MeteredCache cache) { - caches.add(new WeakReference<>(cache)); - } - - public List> caches() { - return cacheStream().map(MeteredCache::getRawCache).collect(Collectors.toList()); - } - - @Unmodifiable - public List getCaches() { - return cacheStream().toList(); - } - - private Stream cacheStream() { - return caches.stream() - .map(WeakReference::get) - .filter(Objects::nonNull) - .filter(cache -> !cache.isClosed()); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/service/StudioSVC.java b/core/src/main/java/com/volmit/iris/core/service/StudioSVC.java deleted file mode 100644 index fb2be0f33..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/StudioSVC.java +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.service; - -import com.google.gson.JsonSyntaxException; -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.ServerConfigurator; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.pack.IrisPack; -import com.volmit.iris.core.project.IrisProject; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.exceptions.IrisException; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONException; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.zeroturnaround.zip.ZipUtil; -import org.zeroturnaround.zip.commons.FileUtils; - -import java.io.File; -import java.io.IOException; -import java.util.UUID; -import java.util.function.Consumer; - -public class StudioSVC implements IrisService { - public static final String LISTING = "https://raw.githubusercontent.com/IrisDimensions/_listing/main/listing-v2.json"; - public static final String WORKSPACE_NAME = "packs"; - private static final AtomicCache counter = new AtomicCache<>(); - private final KMap cacheListing = null; - private IrisProject activeProject; - - @Override - public void onEnable() { - J.s(() -> { - String pack = IrisSettings.get().getGenerator().getDefaultWorldType(); - File f = IrisPack.packsPack(pack); - - if (!f.exists()) { - Iris.info("Downloading Default Pack " + pack); - if (pack.equals("overworld")) { - String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip"; - Iris.service(StudioSVC.class).downloadRelease(Iris.getSender(), url, false, false); - } else { - downloadSearch(Iris.getSender(), pack, false); - } - } - }); - } - - @Override - public void onDisable() { - Iris.debug("Studio Mode Active: Closing Projects"); - - for (World i : Bukkit.getWorlds()) { - if (IrisToolbelt.isIrisWorld(i)) { - if (IrisToolbelt.isStudio(i)) { - IrisToolbelt.evacuate(i); - IrisToolbelt.access(i).close(); - } - } - } - } - - public IrisDimension installIntoWorld(VolmitSender sender, String type, File folder) { - return installInto(sender, type, new File(folder, "iris/pack")); - } - - public IrisDimension installInto(VolmitSender sender, String type, File folder) { - sender.sendMessage("Looking for Package: " + type); - IrisDimension dim = IrisData.loadAnyDimension(type, null); - - if (dim == null) { - for (File i : getWorkspaceFolder().listFiles()) { - if (i.isFile() && i.getName().equals(type + ".iris")) { - sender.sendMessage("Found " + type + ".iris in " + WORKSPACE_NAME + " folder"); - ZipUtil.unpack(i, folder); - break; - } - } - } else { - sender.sendMessage("Found " + type + " dimension in " + WORKSPACE_NAME + " folder. Repackaging"); - File f = new IrisProject(new File(getWorkspaceFolder(), type)).getPath(); - - try { - FileUtils.copyDirectory(f, folder); - } catch (IOException e) { - Iris.reportError(e); - } - } - - File dimensionFile = new File(folder, "dimensions/" + type + ".json"); - - if (!dimensionFile.exists() || !dimensionFile.isFile()) { - downloadSearch(sender, type, false); - File downloaded = getWorkspaceFolder(type); - - for (File i : downloaded.listFiles()) { - if (i.isFile()) { - try { - FileUtils.copyFile(i, new File(folder, i.getName())); - } catch (IOException e) { - e.printStackTrace(); - Iris.reportError(e); - } - } else { - try { - FileUtils.copyDirectory(i, new File(folder, i.getName())); - } catch (IOException e) { - e.printStackTrace(); - Iris.reportError(e); - } - } - } - - IO.delete(downloaded); - } - - if (!dimensionFile.exists() || !dimensionFile.isFile()) { - sender.sendMessage("Can't find the " + dimensionFile.getName() + " in the dimensions folder of this pack! Failed!"); - return null; - } - - IrisData dm = IrisData.get(folder); - dm.hotloaded(); - dim = dm.getDimensionLoader().load(type); - - if (dim == null) { - sender.sendMessage("Can't load the dimension! Failed!"); - return null; - } - - sender.sendMessage(folder.getName() + " type installed. "); - return dim; - } - - public void downloadSearch(VolmitSender sender, String key, boolean trim) { - downloadSearch(sender, key, trim, false); - } - - public void downloadSearch(VolmitSender sender, String key, boolean trim, boolean forceOverwrite) { - String url = "?"; - - try { - url = getListing(false).get(key); - - if (url == null) { - Iris.warn("ITS ULL for " + key); - } - - url = url == null ? key : url; - Iris.info("Assuming URL " + url); - String branch = "master"; - String[] nodes = url.split("\\Q/\\E"); - String repo = nodes.length == 1 ? "IrisDimensions/" + nodes[0] : nodes[0] + "/" + nodes[1]; - branch = nodes.length > 2 ? nodes[2] : branch; - download(sender, repo, branch, trim, forceOverwrite, false); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - sender.sendMessage("Failed to download '" + key + "' from " + url + "."); - } - } - - public void downloadRelease(VolmitSender sender, String url, boolean trim, boolean forceOverwrite) { - try { - download(sender, "IrisDimensions", url, trim, forceOverwrite, true); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - sender.sendMessage("Failed to download 'IrisDimensions/overworld' from " + url + "."); - } - } - - public void download(VolmitSender sender, String repo, String branch, boolean trim) throws JsonSyntaxException, IOException { - download(sender, repo, branch, trim, false, false); - } - - public void download(VolmitSender sender, String repo, String branch, boolean trim, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException { - String url = directUrl ? branch : "https://codeload.github.com/" + repo + "/zip/refs/heads/" + branch; - sender.sendMessage("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL - File zip = Iris.getNonCachedFile("pack-" + trim + "-" + repo, url); - File temp = Iris.getTemp(); - File work = new File(temp, "dl-" + UUID.randomUUID()); - File packs = getWorkspaceFolder(); - - if (zip == null || !zip.exists()) { - sender.sendMessage("Failed to find pack at " + url); - sender.sendMessage("Make sure you specified the correct repo and branch!"); - sender.sendMessage("For example: /iris download IrisDimensions/overworld branch=master"); - return; - } - sender.sendMessage("Unpacking " + repo); - try { - ZipUtil.unpack(zip, work); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - sender.sendMessage( - """ - Issue when unpacking. Please check/do the following: - 1. Do you have a functioning internet connection? - 2. Did the download corrupt? - 3. Try deleting the */plugins/iris/packs folder and re-download. - 4. Download the pack from the GitHub repo: https://github.com/IrisDimensions/overworld - 5. Contact support (if all other options do not help)""" - ); - } - File dir = null; - File[] zipFiles = work.listFiles(); - - if (zipFiles == null) { - sender.sendMessage("No files were extracted from the zip file."); - return; - } - - try { - dir = zipFiles.length > 1 ? work : zipFiles[0].isDirectory() ? zipFiles[0] : null; - } catch (NullPointerException e) { - Iris.reportError(e); - sender.sendMessage("Error when finding home directory. Are there any non-text characters in the file name?"); - return; - } - - if (dir == null) { - sender.sendMessage("Invalid Format. Missing root folder or too many folders!"); - return; - } - - IrisData data = IrisData.get(dir); - String[] dimensions = data.getDimensionLoader().getPossibleKeys(); - - if (dimensions == null || dimensions.length == 0) { - sender.sendMessage("No dimension file found in the extracted zip file."); - sender.sendMessage("Check it is there on GitHub and report this to staff!"); - } else if (dimensions.length != 1) { - sender.sendMessage("Dimensions folder must have 1 file in it"); - return; - } - - IrisDimension d = data.getDimensionLoader().load(dimensions[0]); - data.close(); - - if (d == null) { - sender.sendMessage("Invalid dimension (folder) in dimensions folder"); - return; - } - - String key = d.getLoadKey(); - sender.sendMessage("Importing " + d.getName() + " (" + key + ")"); - File packEntry = new File(packs, key); - - if (forceOverwrite) { - IO.delete(packEntry); - } - - if (IrisData.loadAnyDimension(key, null) != null) { - sender.sendMessage("Another dimension in the packs folder is already using the key " + key + " IMPORT FAILED!"); - return; - } - - if (packEntry.exists() && packEntry.listFiles().length > 0) { - sender.sendMessage("Another pack is using the key " + key + ". IMPORT FAILED!"); - return; - } - - FileUtils.copyDirectory(dir, packEntry); - - if (trim) { - sender.sendMessage("Trimming " + key); - File cp = compilePackage(sender, key, false, false); - IO.delete(packEntry); - packEntry.mkdirs(); - ZipUtil.unpack(cp, packEntry); - } - IrisData.getLoaded(packEntry) - .ifPresent(IrisData::hotloaded); - - sender.sendMessage("Successfully Aquired " + d.getName()); - ServerConfigurator.installDataPacks(true); - } - - public KMap getListing(boolean cached) { - JSONObject a; - - if (cached) { - a = new JSONObject(Iris.getCached("cachedlisting", LISTING)); - } else { - a = new JSONObject(Iris.getNonCached(true + "listing", LISTING)); - } - - KMap l = new KMap<>(); - - for (String i : a.keySet()) { - if (a.get(i) instanceof String) - l.put(i, a.getString(i)); - } - - // TEMP FIX - l.put("IrisDimensions/overworld/master", "IrisDimensions/overworld/stable"); - l.put("overworld", "IrisDimensions/overworld/stable"); - return l; - } - - public boolean isProjectOpen() { - return activeProject != null && activeProject.isOpen(); - } - - public void open(VolmitSender sender, String dimm) { - open(sender, 1337, dimm); - } - - public void open(VolmitSender sender, long seed, String dimm) { - try { - open(sender, seed, dimm, (w) -> { - }); - } catch (Exception e) { - Iris.reportError(e); - sender.sendMessage("Error when creating studio world:"); - e.printStackTrace(); - } - } - - public void open(VolmitSender sender, long seed, String dimm, Consumer onDone) throws IrisException { - if (isProjectOpen()) { - close(); - } - - IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimm)); - activeProject = project; - project.open(sender, seed, onDone); - } - - public void openVSCode(VolmitSender sender, String dim) { - new IrisProject(new File(getWorkspaceFolder(), dim)).openVSCode(sender); - } - - public File getWorkspaceFolder(String... sub) { - return Iris.instance.getDataFolderList(WORKSPACE_NAME, sub); - } - - public File getWorkspaceFile(String... sub) { - return Iris.instance.getDataFileList(WORKSPACE_NAME, sub); - } - - public void close() { - if (isProjectOpen()) { - Iris.debug("Closing Active Project"); - activeProject.close(); - activeProject = null; - } - } - - public File compilePackage(VolmitSender sender, String d, boolean obfuscate, boolean minify) { - return new IrisProject(new File(getWorkspaceFolder(), d)).compilePackage(sender, obfuscate, minify); - } - - public void createFrom(String existingPack, String newName) { - File importPack = getWorkspaceFolder(existingPack); - File newPack = getWorkspaceFolder(newName); - - if (importPack.listFiles().length == 0) { - Iris.warn("Couldn't find the pack to create a new dimension from."); - return; - } - - try { - FileUtils.copyDirectory(importPack, newPack, pathname -> !pathname.getAbsolutePath().contains(".git"), false); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - - new File(importPack, existingPack + ".code-workspace").delete(); - File dimFile = new File(importPack, "dimensions/" + existingPack + ".json"); - File newDimFile = new File(newPack, "dimensions/" + newName + ".json"); - - try { - FileUtils.copyFile(dimFile, newDimFile); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - - new File(newPack, "dimensions/" + existingPack + ".json").delete(); - - try { - JSONObject json = new JSONObject(IO.readAll(newDimFile)); - - if (json.has("name")) { - json.put("name", Form.capitalizeWords(newName.replaceAll("\\Q-\\E", " "))); - IO.writeAll(newDimFile, json.toString(4)); - } - } catch (JSONException | IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - - try { - IrisProject p = new IrisProject(getWorkspaceFolder(newName)); - JSONObject ws = p.createCodeWorkspaceConfig(); - IO.writeAll(getWorkspaceFile(newName, newName + ".code-workspace"), ws.toString(0)); - } catch (JSONException | IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - public void create(VolmitSender sender, String s, String downloadable) { - boolean shouldDelete = false; - File importPack = getWorkspaceFolder(downloadable); - - if (importPack.listFiles().length == 0) { - downloadSearch(sender, downloadable, false); - - if (importPack.listFiles().length > 0) { - shouldDelete = true; - } - } - - if (importPack.listFiles().length == 0) { - sender.sendMessage("Couldn't find the pack to create a new dimension from."); - return; - } - - File importDimensionFile = new File(importPack, "dimensions/" + downloadable + ".json"); - - if (!importDimensionFile.exists()) { - sender.sendMessage("Missing Imported Dimension File"); - return; - } - - sender.sendMessage("Importing " + downloadable + " into new Project " + s); - createFrom(downloadable, s); - if (shouldDelete) { - importPack.delete(); - } - open(sender, s); - } - - public void create(VolmitSender sender, String s) { - create(sender, s, "example"); - } - - public IrisProject getActiveProject() { - return activeProject; - } - - public void updateWorkspace() { - if (isProjectOpen()) { - activeProject.updateWorkspace(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/service/TreeSVC.java b/core/src/main/java/com/volmit/iris/core/service/TreeSVC.java deleted file mode 100644 index d11cd911a..000000000 --- a/core/src/main/java/com/volmit/iris/core/service/TreeSVC.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.service; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.Cuboid; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.math.BlockPosition; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.plugin.IrisService; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.*; -import org.bukkit.block.Block; -import org.bukkit.block.BlockState; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.Sapling; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.world.StructureGrowEvent; - -import java.util.List; -import java.util.Objects; -import java.util.function.Predicate; - -public class TreeSVC implements IrisService { - private boolean block = false; - - @Override - public void onEnable() { - - } - - @Override - public void onDisable() { - - } - - /** - * This function does the following - *
1. Is the sapling growing in an Iris world? No -> exit
- *
2. Is the Iris world accessible? No -> exit
- *
3. Is the sapling overwriting setting on in that dimension? No -> exit
- *
4. Check biome, region and dimension for overrides for that sapling type -> Found -> use
- *
5. Exit if none are found, cancel event if one or more are.
- * - * @param event Checks the given event for sapling overrides - */ - @EventHandler(priority = EventPriority.HIGHEST) - public void on(StructureGrowEvent event) { - if (block || event.isCancelled()) { - return; - } - Iris.debug(this.getClass().getName() + " received a structure grow event"); - - if (!IrisToolbelt.isIrisWorld(event.getWorld())) { - Iris.debug(this.getClass().getName() + " passed grow event off to vanilla since not an Iris world"); - return; - } - - PlatformChunkGenerator worldAccess = IrisToolbelt.access(event.getWorld()); - if (worldAccess == null) { - Iris.debug(this.getClass().getName() + " passed it off to vanilla because could not get IrisAccess for this world"); - Iris.reportError(new NullPointerException(event.getWorld().getName() + " could not be accessed despite being an Iris world")); - return; - } - - Engine engine = worldAccess.getEngine(); - - if (engine == null) { - Iris.debug(this.getClass().getName() + " passed it off to vanilla because could not get Engine for this world"); - Iris.reportError(new NullPointerException(event.getWorld().getName() + " could not be accessed despite being an Iris world")); - return; - } - - IrisDimension dimension = engine.getDimension(); - - if (dimension == null) { - Iris.debug(this.getClass().getName() + " passed it off to vanilla because could not get Dimension for this world"); - Iris.reportError(new NullPointerException(event.getWorld().getName() + " could not be accessed despite being an Iris world")); - return; - } - - if (!dimension.getTreeSettings().isEnabled()) { - Iris.debug(this.getClass().getName() + " cancelled because tree overrides are disabled"); - return; - } - - BlockData first = event.getLocation().getBlock().getBlockData().clone(); - Cuboid saplingPlane = getSaplings(event.getLocation(), blockData -> blockData instanceof Sapling && blockData.getMaterial().equals(first.getMaterial()), event.getWorld()); - - Iris.debug("Sapling grew @ " + event.getLocation() + " for " + event.getSpecies().name() + " usedBoneMeal is " + event.isFromBonemeal()); - Iris.debug("Sapling plane is: " + saplingPlane.getSizeX() + " by " + saplingPlane.getSizeZ()); - IrisObjectPlacement placement = getObjectPlacement(worldAccess, event.getLocation(), event.getSpecies(), new IrisTreeSize(1, 1)); - - if (placement == null) { - Iris.debug(this.getClass().getName() + " had options but did not manage to find objectPlacements for them"); - return; - } - - saplingPlane.forEach(block -> block.setType(Material.AIR)); - IrisObject object = worldAccess.getData().getObjectLoader().load(placement.getPlace().getRandom(RNG.r)); - List blockStateList = new KList<>(); - KMap dataCache = new KMap<>(); - // TODO: REAL CLASSES!!!! - - IObjectPlacer placer = new IObjectPlacer() { - - @Override - public int getHighest(int x, int z, IrisData data) { - return event.getWorld().getHighestBlockYAt(x, z); - } - - @Override - public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { - return event.getWorld().getHighestBlockYAt(x, z, ignoreFluid ? HeightMap.OCEAN_FLOOR : HeightMap.WORLD_SURFACE); - } - - @Override - public void set(int x, int y, int z, BlockData d) { - Block b = event.getWorld().getBlockAt(x, y, z); - BlockState state = b.getState(); - if (d instanceof IrisCustomData data) - state.setBlockData(data.getBase()); - else state.setBlockData(d); - blockStateList.add(b.getState()); - dataCache.put(new Location(event.getWorld(), x, y, z), d); - } - - @Override - public BlockData get(int x, int y, int z) { - return event.getWorld().getBlockAt(x, y, z).getBlockData(); - } - - @Override - public boolean isPreventingDecay() { - return true; - } - - @Override - public boolean isCarved(int x, int y, int z) { - return false; - } - - @Override - public boolean isSolid(int x, int y, int z) { - return get(x, y, z).getMaterial().isSolid(); - } - - @Override - public boolean isUnderwater(int x, int z) { - return false; - } - - @Override - public int getFluidHeight() { - return worldAccess.getEngine().getDimension().getFluidHeight(); - } - - @Override - public boolean isDebugSmartBore() { - return false; - } - - @Override - public void setTile(int xx, int yy, int zz, TileData tile) { - - } - - @Override - public void setData(int xx, int yy, int zz, T data) { - - } - - @Override - public T getData(int xx, int yy, int zz, Class t) { - return null; - } - - @Override - public Engine getEngine() { - return engine; - } - }; - - object.place( - saplingPlane.getCenter().getBlockX(), - (saplingPlane.getCenter().getBlockY() + object.getH() / 2), - saplingPlane.getCenter().getBlockZ(), - placer, - placement, - RNG.r, - Objects.requireNonNull(worldAccess).getData() - ); - - event.setCancelled(true); - - J.s(() -> { - - StructureGrowEvent iGrow = new StructureGrowEvent(event.getLocation(), event.getSpecies(), event.isFromBonemeal(), event.getPlayer(), blockStateList); - block = true; - Bukkit.getServer().getPluginManager().callEvent(iGrow); - block = false; - - if (!iGrow.isCancelled()) { - for (BlockState state : iGrow.getBlocks()) { - Location l = state.getLocation(); - - BlockData d = dataCache.get(l); - if (d == null) continue; - Block block = l.getBlock(); - - if (d instanceof IrisCustomData data) { - block.setBlockData(data.getBase(), false); - Iris.service(ExternalDataSVC.class).processUpdate(engine, block, data.getCustom()); - } else block.setBlockData(d, false); - } - } - }); - } - - /** - * Finds a single object placement (which may contain more than one object) for the requirements species, location & - * size - * - * @param worldAccess The world to access (check for biome, region, dimension, etc) - * @param location The location of the growth event (For biome/region finding) - * @param type The bukkit TreeType to match - * @param size The size of the sapling area - * @return An object placement which contains the matched tree, or null if none were found / it's disabled. - */ - private IrisObjectPlacement getObjectPlacement(PlatformChunkGenerator worldAccess, Location location, TreeType type, IrisTreeSize size) { - - KList placements = new KList<>(); - boolean isUseAll = worldAccess.getEngine().getDimension().getTreeSettings().getMode().equals(IrisTreeModes.ALL); - - // Retrieve objectPlacements of type `species` from biome - IrisBiome biome = worldAccess.getEngine().getBiome(location.getBlockX(), location.getBlockY()-worldAccess.getTarget().getWorld().minHeight(), location.getBlockZ()); - placements.addAll(matchObjectPlacements(biome.getObjects(), size, type)); - - // Add more or find any in the region - if (isUseAll || placements.isEmpty()) { - IrisRegion region = worldAccess.getEngine().getRegion(location.getBlockX(), location.getBlockZ()); - placements.addAll(matchObjectPlacements(region.getObjects(), size, type)); - } - - // Check if no matches were found, return a random one if they are - return placements.isNotEmpty() ? placements.getRandom() : null; - } - - /** - * Filters out mismatches and returns matches - * - * @param objects The object placements to check - * @param size The size of the sapling area to filter with - * @param type The type of the tree to filter with - * @return A list of objectPlacements that matched. May be empty. - */ - private KList matchObjectPlacements(KList objects, IrisTreeSize size, TreeType type) { - - KList p = new KList<>(); - - for (IrisObjectPlacement i : objects) { - if (i.matches(size, type)) { - p.add(i); - } - } - - return p; - } - - /** - * Get the Cuboid of sapling sizes at a location & blockData predicate - * - * @param at this location - * @param valid with this blockData predicate - * @param world the world to check in - * @return A cuboid containing only saplings - */ - public Cuboid getSaplings(Location at, Predicate valid, World world) { - KList blockPositions = new KList<>(); - grow(at.getWorld(), new BlockPosition(at.getBlockX(), at.getBlockY(), at.getBlockZ()), valid, blockPositions); - BlockPosition a = new BlockPosition(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); - BlockPosition b = new BlockPosition(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); - - // Maximise the block position in x and z to get max cuboid bounds - for (BlockPosition blockPosition : blockPositions) { - a.max(blockPosition); - b.min(blockPosition); - } - - Iris.debug("Blocks: " + blockPositions.size()); - Iris.debug("Min: " + a + " Max: " + b); - - // Create a cuboid with the size calculated before - Cuboid cuboid = new Cuboid(a.toBlock(world).getLocation(), b.toBlock(world).getLocation()); - boolean cuboidIsValid = true; - - // Loop while the cuboid is larger than 2 - while (Math.min(cuboid.getSizeX(), cuboid.getSizeZ()) > 0) { - checking: - for (int i = cuboid.getLowerX(); i < cuboid.getUpperX(); i++) { - for (int j = cuboid.getLowerY(); j < cuboid.getUpperY(); j++) { - for (int k = cuboid.getLowerZ(); k < cuboid.getUpperZ(); k++) { - if (!blockPositions.contains(new BlockPosition(i, j, k))) { - cuboidIsValid = false; - break checking; - } - } - } - } - - // Return this cuboid if it's valid - if (cuboidIsValid) { - return cuboid; - } - - // Inset the cuboid and try again (revalidate) - cuboid = cuboid.inset(Cuboid.CuboidDirection.Horizontal, 1); - cuboidIsValid = true; - } - - return new Cuboid(at, at); - } - - /** - * Grows the blockPosition list by means of checking neighbours in - * - * @param world the world to check in - * @param center the location of this position - * @param valid validation on blockData to check block with - * @param l list of block positions to add new neighbors too - */ - private void grow(World world, BlockPosition center, Predicate valid, KList l) { - // Make sure size is less than 50, the block to check isn't already in, and make sure the blockData still matches - if (l.size() <= 50 && !l.contains(center) && valid.test(center.toBlock(world).getBlockData())) { - l.add(center); - grow(world, center.add(1, 0, 0), valid, l); - grow(world, center.add(-1, 0, 0), valid, l); - grow(world, center.add(0, 0, 1), valid, l); - grow(world, center.add(0, 0, -1), valid, l); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/tools/IrisConverter.java b/core/src/main/java/com/volmit/iris/core/tools/IrisConverter.java deleted file mode 100644 index 66f9c2d01..000000000 --- a/core/src/main/java/com/volmit/iris/core/tools/IrisConverter.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.volmit.iris.core.tools; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.data.Varint; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.nbt.io.NBTUtil; -import com.volmit.iris.util.nbt.io.NamedTag; -import com.volmit.iris.util.nbt.tag.*; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.apache.commons.io.FileUtils; -import org.bukkit.Bukkit; -import org.bukkit.block.data.BlockData; - -import java.io.*; -import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; - -public class IrisConverter { - public static void convertSchematics(VolmitSender sender) { - File folder = Iris.instance.getDataFolder("convert"); - - FilenameFilter filter = (dir, name) -> name.endsWith(".schem"); - File[] fileList = folder.listFiles(filter); - if (fileList == null) { - sender.sendMessage("No schematic files to convert found in " + folder.getAbsolutePath()); - return; - } - - AtomicInteger counter = new AtomicInteger(0); - var stopwatch = PrecisionStopwatch.start(); - ExecutorService executorService = Executors.newFixedThreadPool(1); - executorService.submit(() -> { - for (File schem : fileList) { - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - IrisObject object; - boolean largeObject = false; - NamedTag tag; - try { - tag = NBTUtil.read(schem); - } catch (IOException e) { - Iris.info(C.RED + "Failed to read: " + schem.getName()); - throw new RuntimeException(e); - } - CompoundTag compound = (CompoundTag) tag.getTag(); - int version = resolveVersion(compound); - if (!(version == 2 || version == 3)) - throw new RuntimeException(C.RED + "Unsupported schematic version: " + version); - - compound = version == 3 ? (CompoundTag) compound.get("Schematic") : compound; - int objW = ((ShortTag) compound.get("Width")).getValue(); - int objH = ((ShortTag) compound.get("Height")).getValue(); - int objD = ((ShortTag) compound.get("Length")).getValue(); - int i = -1; - int mv = objW * objH * objD; - AtomicInteger v = new AtomicInteger(0); - if (mv > 2_000_000) { - largeObject = true; - Iris.info(C.GRAY + "Converting.. " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); - Iris.info(C.GRAY + "- It may take a while"); - if (sender.isPlayer()) { - i = J.ar(() -> { - sender.sendProgress((double) v.get() / mv, "Converting"); - }, 0); - } - } - - compound = version == 3 ? (CompoundTag) compound.get("Blocks") : compound; - CompoundTag paletteTag = (CompoundTag) compound.get("Palette"); - Map blockmap = new HashMap<>(paletteTag.size(), 0.9f); - for (Map.Entry> entry : paletteTag.getValue().entrySet()) { - String blockName = entry.getKey(); - BlockData bd = Bukkit.createBlockData(blockName); - Tag blockTag = entry.getValue(); - int blockId = ((IntTag) blockTag).getValue(); - blockmap.put(blockId, bd); - } - - boolean isBytes = version == 3 ? compound.getByteArrayTag("Data").length() < 128 : ((IntTag) compound.get("PaletteMax")).getValue() < 128; - ByteArrayTag byteArray = version == 3 ? (ByteArrayTag) compound.get("Data") : (ByteArrayTag) compound.get("BlockData"); - byte[] originalBlockArray = byteArray.getValue(); - var din = new DataInputStream(new ByteArrayInputStream(originalBlockArray)); - object = new IrisObject(objW, objH, objD); - for (int h = 0; h < objH; h++) { - for (int d = 0; d < objD; d++) { - for (int w = 0; w < objW; w++) { - int blockIndex = isBytes ? din.read() & 0xFF : Varint.readUnsignedVarInt(din); - BlockData bd = blockmap.get(blockIndex); - if (!bd.getMaterial().isAir()) { - object.setUnsigned(w, h, d, bd); - } - v.getAndAdd(1); - } - } - } - - if (i != -1) J.car(i); - try { - object.shrinkwrap(); - object.write(new File(folder, schem.getName().replace(".schem", ".iob"))); - counter.incrementAndGet(); - if (sender.isPlayer()) { - if (largeObject) { - sender.sendMessage(C.IRIS + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob") + " in " + Form.duration(p.getMillis())); - } else { - sender.sendMessage(C.IRIS + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); - } - } - if (largeObject) { - Iris.info(C.GRAY + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob") + " in " + Form.duration(p.getMillis())); - } else { - Iris.info(C.GRAY + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); - } - FileUtils.delete(schem); - } catch (IOException e) { - sender.sendMessage(C.RED + "Failed to save: " + schem.getName()); - throw new IOException(e); - } - - - } catch (Exception e) { - sender.sendMessage(C.RED + "Failed to convert: " + schem.getName()); - e.printStackTrace(); - } - } - stopwatch.end(); - if (counter.get() != 0) { - sender.sendMessage(C.GRAY + "Converted: " + counter.get() + " in " + Form.duration(stopwatch.getMillis())); - } - if (counter.get() < fileList.length) { - sender.sendMessage(C.RED + "Some schematics failed to convert. Check the console for details."); - } - }); - } - - private static int resolveVersion(CompoundTag compound) throws Exception { - try { - - IntTag root = compound.getIntTag("Version"); - if (root != null) { - return root.getValue(); - } - CompoundTag schematic = (CompoundTag) compound.get("Schematic"); - return schematic.getIntTag("Version").getValue(); - } catch (NullPointerException e) { - throw new Exception("Cannot resolve schematic version", e); - } - } -} - - diff --git a/core/src/main/java/com/volmit/iris/core/tools/IrisCreator.java b/core/src/main/java/com/volmit/iris/core/tools/IrisCreator.java deleted file mode 100644 index b112ed779..000000000 --- a/core/src/main/java/com/volmit/iris/core/tools/IrisCreator.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.tools; - -import com.google.common.util.concurrent.AtomicDouble; -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.ServerConfigurator; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.pregenerator.PregenTask; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.exceptions.IrisException; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.O; -import lombok.Data; -import lombok.experimental.Accessors; -import org.bukkit.*; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.IntSupplier; - -import static com.volmit.iris.util.misc.ServerProperties.BUKKIT_YML; - -/** - * Makes it a lot easier to setup an engine, world, studio or whatever - */ -@Data -@Accessors(fluent = true, chain = true) -public class IrisCreator { - /** - * Specify an area to pregenerate during creation - */ - private PregenTask pregen; - /** - * Specify a sender to get updates & progress info + tp when world is created. - */ - private VolmitSender sender; - /** - * The seed to use for this generator - */ - private long seed = 1337; - /** - * The dimension to use. This can be any online dimension, or a dimension in the - * packs folder - */ - private String dimension = IrisSettings.get().getGenerator().getDefaultWorldType(); - /** - * The name of this world. - */ - private String name = "irisworld"; - /** - * Studio mode makes the engine hotloadable and uses the dimension in - * your Iris/packs folder instead of copying the dimension files into - * the world itself. Studio worlds are deleted when they are unloaded. - */ - private boolean studio = false; - /** - * Benchmark mode - */ - private boolean benchmark = false; - - public static boolean removeFromBukkitYml(String name) throws IOException { - YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); - ConfigurationSection section = yml.getConfigurationSection("worlds"); - if (section == null) { - return false; - } - section.set(name, null); - if (section.getValues(false).keySet().stream().noneMatch(k -> section.get(k) != null)) { - yml.set("worlds", null); - } - yml.save(BUKKIT_YML); - return true; - } - public static boolean worldLoaded(){ - return true; - } - - /** - * Create the IrisAccess (contains the world) - * - * @return the IrisAccess - * @throws IrisException shit happens - */ - - public World create() throws IrisException { - if (Bukkit.isPrimaryThread()) { - throw new IrisException("You cannot invoke create() on the main thread."); - } - - IrisDimension d = IrisToolbelt.getDimension(dimension()); - - if (d == null) { - throw new IrisException("Dimension cannot be found null for id " + dimension()); - } - - if (sender == null) - sender = Iris.getSender(); - - if (!studio() || benchmark) { - Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name())); - } - - AtomicDouble pp = new AtomicDouble(0); - O done = new O<>(); - done.set(false); - WorldCreator wc = new IrisWorldCreator() - .dimension(dimension) - .name(name) - .seed(seed) - .studio(studio) - .create(); - if (ServerConfigurator.installDataPacks(true)) { - throw new IrisException("Datapacks were missing!"); - } - - PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator(); - if (access == null) throw new IrisException("Access is null. Something bad happened."); - - J.a(() -> { - IntSupplier g = () -> { - if (access.getEngine() == null) { - return 0; - } - return access.getEngine().getGenerated(); - }; - if(!benchmark) { - int req = access.getSpawnChunks().join(); - for (int c = 0; c < req && !done.get(); c = g.getAsInt()) { - double v = (double) c / req; - if (sender.isPlayer()) { - sender.sendProgress(v, "Generating"); - J.sleep(16); - } else { - sender.sendMessage(C.WHITE + "Generating " + Form.pc(v) + ((C.GRAY + " (" + (req - c) + " Left)"))); - J.sleep(1000); - } - } - } - }); - - - World world; - try { - world = J.sfut(() -> INMS.get().createWorld(wc)).get(); - } catch (Throwable e) { - done.set(true); - throw new IrisException("Failed to create world!", e); - } - - done.set(true); - - if (sender.isPlayer() && !benchmark) { - J.s(() -> sender.player().teleport(new Location(world, 0, world.getHighestBlockYAt(0, 0) + 1, 0))); - } - - if (studio || benchmark) { - J.s(() -> { - Iris.linkMultiverseCore.removeFromConfig(world); - - if (IrisSettings.get().getStudio().isDisableTimeAndWeather()) { - world.setGameRule(GameRule.DO_WEATHER_CYCLE, false); - world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false); - world.setTime(6000); - } - }); - } else { - addToBukkitYml(); - J.s(() -> Iris.linkMultiverseCore.updateWorld(world, dimension)); - } - - if (pregen != null) { - CompletableFuture ff = new CompletableFuture<>(); - - IrisToolbelt.pregenerate(pregen, access) - .onProgress(pp::set) - .whenDone(() -> ff.complete(true)); - - try { - AtomicBoolean dx = new AtomicBoolean(false); - - J.a(() -> { - while (!dx.get()) { - if (sender.isPlayer()) { - sender.sendProgress(pp.get(), "Pregenerating"); - J.sleep(16); - } else { - sender.sendMessage(C.WHITE + "Pregenerating " + Form.pc(pp.get())); - J.sleep(1000); - } - } - }); - - ff.get(); - dx.set(true); - } catch (Throwable e) { - e.printStackTrace(); - } - } - return world; - } - - private void addToBukkitYml() { - YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); - String gen = "Iris:" + dimension; - ConfigurationSection section = yml.contains("worlds") ? yml.getConfigurationSection("worlds") : yml.createSection("worlds"); - if (!section.contains(name)) { - section.createSection(name).set("generator", gen); - try { - yml.save(BUKKIT_YML); - Iris.info("Registered \"" + name + "\" in bukkit.yml"); - } catch (IOException e) { - Iris.error("Failed to update bukkit.yml!"); - e.printStackTrace(); - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/core/tools/IrisPackBenchmarking.java b/core/src/main/java/com/volmit/iris/core/tools/IrisPackBenchmarking.java deleted file mode 100644 index 4f39a7527..000000000 --- a/core/src/main/java/com/volmit/iris/core/tools/IrisPackBenchmarking.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.volmit.iris.core.tools; - - -import com.volmit.iris.Iris; -import com.volmit.iris.core.pregenerator.PregenTask; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.exceptions.IrisException; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.bukkit.Bukkit; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.time.Clock; -import java.time.LocalDateTime; -import java.util.Collections; - - -public class IrisPackBenchmarking { - private static final ThreadLocal instance = new ThreadLocal<>(); - private final PrecisionStopwatch stopwatch = new PrecisionStopwatch(); - private final IrisDimension dimension; - private final int radius; - private final boolean gui; - - public IrisPackBenchmarking(IrisDimension dimension, int radius, boolean gui) { - this.dimension = dimension; - this.radius = radius; - this.gui = gui; - runBenchmark(); - } - - public static IrisPackBenchmarking getInstance() { - return instance.get(); - } - - private void runBenchmark() { - Thread.ofVirtual() - .name("PackBenchmarking") - .start(() -> { - Iris.info("Setting up benchmark environment "); - IO.delete(new File(Bukkit.getWorldContainer(), "benchmark")); - createBenchmark(); - while (!IrisToolbelt.isIrisWorld(Bukkit.getWorld("benchmark"))) { - J.sleep(1000); - Iris.debug("Iris PackBenchmark: Waiting..."); - } - Iris.info("Starting Benchmark!"); - stopwatch.begin(); - startBenchmark(); - }); - - } - - public void finishedBenchmark(KList cps) { - try { - String time = Form.duration((long) stopwatch.getMilliseconds()); - Engine engine = IrisToolbelt.access(Bukkit.getWorld("benchmark")).getEngine(); - Iris.info("-----------------"); - Iris.info("Results:"); - Iris.info("- Total time: " + time); - Iris.info("- Average CPS: " + calculateAverage(cps)); - Iris.info(" - Median CPS: " + calculateMedian(cps)); - Iris.info(" - Highest CPS: " + findHighest(cps)); - Iris.info(" - Lowest CPS: " + findLowest(cps)); - Iris.info("-----------------"); - Iris.info("Creating a report.."); - File results = Iris.instance.getDataFile("packbenchmarks", dimension.getName() + " " + LocalDateTime.now(Clock.systemDefaultZone()).toString().replace(':', '-') + ".txt"); - KMap metrics = engine.getMetrics().pull(); - try (FileWriter writer = new FileWriter(results)) { - writer.write("-----------------\n"); - writer.write("Results:\n"); - writer.write("Dimension: " + dimension.getName() + "\n"); - writer.write("- Date of Benchmark: " + LocalDateTime.now(Clock.systemDefaultZone()) + "\n"); - writer.write("\n"); - writer.write("Metrics"); - for (String m : metrics.k()) { - double i = metrics.get(m); - writer.write("- " + m + ": " + i); - } - writer.write("- " + metrics); - writer.write("Benchmark: " + LocalDateTime.now(Clock.systemDefaultZone()) + "\n"); - writer.write("- Total time: " + time + "\n"); - writer.write("- Average CPS: " + calculateAverage(cps) + "\n"); - writer.write(" - Median CPS: " + calculateMedian(cps) + "\n"); - writer.write(" - Highest CPS: " + findHighest(cps) + "\n"); - writer.write(" - Lowest CPS: " + findLowest(cps) + "\n"); - writer.write("-----------------\n"); - Iris.info("Finished generating a report!"); - } catch (IOException e) { - Iris.error("An error occurred writing to the file."); - e.printStackTrace(); - } - - J.s(() -> { - var world = Bukkit.getWorld("benchmark"); - if (world == null) return; - IrisToolbelt.evacuate(world); - Bukkit.unloadWorld(world, true); - }); - - stopwatch.end(); - } catch (Exception e) { - Iris.error("Something has gone wrong!"); - e.printStackTrace(); - } - } - - private void createBenchmark() { - try { - IrisToolbelt.createWorld() - .dimension(dimension.getLoadKey()) - .name("benchmark") - .seed(1337) - .studio(false) - .benchmark(true) - .create(); - } catch (IrisException e) { - throw new RuntimeException(e); - } - } - - private void startBenchmark() { - try { - instance.set(this); - IrisToolbelt.pregenerate(PregenTask - .builder() - .gui(gui) - .radiusX(radius) - .radiusZ(radius) - .build(), Bukkit.getWorld("benchmark") - ); - } finally { - instance.remove(); - } - } - - private double calculateAverage(KList list) { - double sum = 0; - for (int num : list) { - sum += num; - } - return sum / list.size(); - } - - private double calculateMedian(KList list) { - Collections.sort(list); - int middle = list.size() / 2; - - if (list.size() % 2 == 1) { - return list.get(middle); - } else { - return (list.get(middle - 1) + list.get(middle)) / 2.0; - } - } - - private int findLowest(KList list) { - return Collections.min(list); - } - - private int findHighest(KList list) { - return Collections.max(list); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/core/tools/IrisReflectiveAPI.java b/core/src/main/java/com/volmit/iris/core/tools/IrisReflectiveAPI.java deleted file mode 100644 index c52076f7c..000000000 --- a/core/src/main/java/com/volmit/iris/core/tools/IrisReflectiveAPI.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.volmit.iris.core.tools; - -import com.volmit.iris.util.data.B; -import org.bukkit.World; -import org.bukkit.block.data.BlockData; - -/** - * This class is used by an external IrisLib for other plugins to interact with Iris. Do not change - * existing methods or their parameters as it will break the library that uses these methods - * feel free to add more methods so long as you also add the reflective methods to the library - */ -public class IrisReflectiveAPI { - public static boolean isIrisWorld(World world) { - return IrisToolbelt.isIrisWorld(world); - } - - public static boolean isIrisStudioWorld(World world) { - return IrisToolbelt.isIrisStudioWorld(world); - } - - public static void registerCustomBlockData(String namespace, String key, BlockData blockData) { - B.registerCustomBlockData(namespace, key, blockData); - } - - public static void retainMantleData(String classname) { - IrisToolbelt.retainMantleDataForSlice(classname); - } - - public static void setMantleData(World world, int x, int y, int z, Object data) { - IrisToolbelt.access(world).getEngine().getMantle().getMantle().set(x, y, z, data); - } - - public static void deleteMantleData(World world, int x, int y, int z, Class c) { - IrisToolbelt.access(world).getEngine().getMantle().getMantle().remove(x, y, z, c); - } - - public static Object getMantleData(World world, int x, int y, int z, Class c) { - return IrisToolbelt.access(world).getEngine().getMantle().getMantle().get(x, y, z, c); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java b/core/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java deleted file mode 100644 index 68812036f..000000000 --- a/core/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.tools; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.gui.PregeneratorJob; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.pregenerator.PregenTask; -import com.volmit.iris.core.pregenerator.PregeneratorMethod; -import com.volmit.iris.core.pregenerator.methods.CachedPregenMethod; -import com.volmit.iris.core.pregenerator.methods.HybridPregenMethod; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.plugin.VolmitSender; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.ApiStatus; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -/** - * Something you really want to wear if working on Iris. Shit gets pretty hectic down there. - * Hope you packed snacks & road sodas. - */ -public class IrisToolbelt { - @ApiStatus.Internal - public static Map toolbeltConfiguration = new HashMap<>(); - - /** - * Will find / download / search for the dimension or return null - *

- * - You can provide a dimenson in the packs folder by the folder name - * - You can provide a github repo by using (assumes branch is master unless specified) - * - GithubUsername/repository - * - GithubUsername/repository/branch - * - * @param dimension the dimension id such as overworld or flat - * @return the IrisDimension or null - */ - public static IrisDimension getDimension(String dimension) { - File pack = Iris.instance.getDataFolder("packs", dimension); - - if (!pack.exists()) { - Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender(), Iris.instance.getTag()), dimension, false, false); - } - - if (!pack.exists()) { - return null; - } - - return IrisData.get(pack).getDimensionLoader().load(dimension); - } - - /** - * Create a world with plenty of options - * - * @return the creator builder - */ - public static IrisCreator createWorld() { - return new IrisCreator(); - } - - /** - * Checks if the given world is an Iris World (same as access(world) != null) - * - * @param world the world - * @return true if it is an Iris Access world - */ - public static boolean isIrisWorld(World world) { - if (world == null) { - return false; - } - - if (world.getGenerator() instanceof PlatformChunkGenerator f) { - f.touch(world); - return true; - } - - return false; - } - - public static boolean isIrisStudioWorld(World world) { - return isIrisWorld(world) && access(world).isStudio(); - } - - /** - * Get the Iris generator for the given world - * - * @param world the given world - * @return the IrisAccess or null if it's not an Iris World - */ - public static PlatformChunkGenerator access(World world) { - if (isIrisWorld(world)) { - return ((PlatformChunkGenerator) world.getGenerator()); - } /*else { - Iris.warn(""" - "---------- No World? --------------- - ⠀⣞⢽⢪⢣⢣⢣⢫⡺⡵⣝⡮⣗⢷⢽⢽⢽⣮⡷⡽⣜⣜⢮⢺⣜⢷⢽⢝⡽⣝ - ⠸⡸⠜⠕⠕⠁⢁⢇⢏⢽⢺⣪⡳⡝⣎⣏⢯⢞⡿⣟⣷⣳⢯⡷⣽⢽⢯⣳⣫⠇ - ⠀⠀⢀⢀⢄⢬⢪⡪⡎⣆⡈⠚⠜⠕⠇⠗⠝⢕⢯⢫⣞⣯⣿⣻⡽⣏⢗⣗⠏⠀ - ⠀⠪⡪⡪⣪⢪⢺⢸⢢⢓⢆⢤⢀⠀⠀⠀⠀⠈⢊⢞⡾⣿⡯⣏⢮⠷⠁⠀⠀ - ⠀⠀⠀⠈⠊⠆⡃⠕⢕⢇⢇⢇⢇⢇⢏⢎⢎⢆⢄⠀⢑⣽⣿⢝⠲⠉⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⡿⠂⠠⠀⡇⢇⠕⢈⣀⠀⠁⠡⠣⡣⡫⣂⣿⠯⢪⠰⠂⠀⠀⠀⠀ - ⠀⠀⠀⠀⡦⡙⡂⢀⢤⢣⠣⡈⣾⡃⠠⠄⠀⡄⢱⣌⣶⢏⢊⠂⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⢝⡲⣜⡮⡏⢎⢌⢂⠙⠢⠐⢀⢘⢵⣽⣿⡿⠁⠁⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠨⣺⡺⡕⡕⡱⡑⡆⡕⡅⡕⡜⡼⢽⡻⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⣼⣳⣫⣾⣵⣗⡵⡱⡡⢣⢑⢕⢜⢕⡝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⣴⣿⣾⣿⣿⣿⡿⡽⡑⢌⠪⡢⡣⣣⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⡟⡾⣿⢿⢿⢵⣽⣾⣼⣘⢸⢸⣞⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠁⠇⠡⠩⡫⢿⣝⡻⡮⣒⢽⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - """); - }*/ - return null; - } - - /** - * Start a pregenerator task - * - * @param task the scheduled task - * @param method the method to execute the task - * @return the pregenerator job (already started) - */ - public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine) { - return pregenerate(task, method, engine, IrisSettings.get().getPregen().useCacheByDefault); - } - - /** - * Start a pregenerator task - * - * @param task the scheduled task - * @param method the method to execute the task - * @return the pregenerator job (already started) - */ - public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) { - return new PregeneratorJob(task, cached && engine != null ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine); - } - - /** - * Start a pregenerator task. If the supplied generator is headless, headless mode is used, - * otherwise Hybrid mode is used. - * - * @param task the scheduled task - * @param gen the Iris Generator - * @return the pregenerator job (already started) - */ - public static PregeneratorJob pregenerate(PregenTask task, PlatformChunkGenerator gen) { - return pregenerate(task, new HybridPregenMethod(gen.getEngine().getWorld().realWorld(), - IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())), gen.getEngine()); - } - - /** - * Start a pregenerator task. If the supplied generator is headless, headless mode is used, - * otherwise Hybrid mode is used. - * - * @param task the scheduled task - * @param world the World - * @return the pregenerator job (already started) - */ - public static PregeneratorJob pregenerate(PregenTask task, World world) { - if (isIrisWorld(world)) { - return pregenerate(task, access(world)); - } - - return pregenerate(task, new HybridPregenMethod(world, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())), null); - } - - /** - * Evacuate all players from the world into literally any other world. - * If there are no other worlds, kick them! Not the best but what's mine is mine sometimes... - * - * @param world the world to evac - */ - public static boolean evacuate(World world) { - for (World i : Bukkit.getWorlds()) { - if (!i.getName().equals(world.getName())) { - for (Player j : world.getPlayers()) { - new VolmitSender(j, Iris.instance.getTag()).sendMessage("You have been evacuated from this world."); - j.teleport(i.getSpawnLocation()); - } - - return true; - } - } - - return false; - } - - /** - * Evacuate all players from the world - * - * @param world the world to leave - * @param m the message - * @return true if it was evacuated. - */ - public static boolean evacuate(World world, String m) { - for (World i : Bukkit.getWorlds()) { - if (!i.getName().equals(world.getName())) { - for (Player j : world.getPlayers()) { - new VolmitSender(j, Iris.instance.getTag()).sendMessage("You have been evacuated from this world. " + m); - j.teleport(i.getSpawnLocation()); - } - return true; - } - } - - return false; - } - - public static boolean isStudio(World i) { - return isIrisWorld(i) && access(i).isStudio(); - } - - public static void retainMantleDataForSlice(String className) { - toolbeltConfiguration.put("retain.mantle." + className, Boolean.TRUE); - } - - public static boolean isRetainingMantleDataForSlice(String className) { - return !toolbeltConfiguration.isEmpty() && toolbeltConfiguration.get("retain.mantle." + className) == Boolean.TRUE; - } - - public static T getMantleData(World world, int x, int y, int z, Class of) { - PlatformChunkGenerator e = access(world); - if (e == null) { - return null; - } - return e.getEngine().getMantle().getMantle().get(x, y - world.getMinHeight(), z, of); - } - - public static void deleteMantleData(World world, int x, int y, int z, Class of) { - PlatformChunkGenerator e = access(world); - if (e == null) { - return; - } - e.getEngine().getMantle().getMantle().remove(x, y - world.getMinHeight(), z, of); - } - - public static boolean removeWorld(World world) throws IOException { - return IrisCreator.removeFromBukkitYml(world.getName()); - } -} diff --git a/core/src/main/java/com/volmit/iris/core/tools/IrisWorldCreator.java b/core/src/main/java/com/volmit/iris/core/tools/IrisWorldCreator.java deleted file mode 100644 index a6fd79a83..000000000 --- a/core/src/main/java/com/volmit/iris/core/tools/IrisWorldCreator.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.core.tools; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.engine.platform.BukkitChunkGenerator; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.WorldCreator; -import org.bukkit.generator.ChunkGenerator; - -import java.io.File; - -public class IrisWorldCreator { - private String name; - private boolean studio = false; - private String dimensionName = null; - private long seed = 1337; - - public IrisWorldCreator() { - - } - - public IrisWorldCreator dimension(String loadKey) { - this.dimensionName = loadKey; - return this; - } - - public IrisWorldCreator name(String name) { - this.name = name; - return this; - } - - public IrisWorldCreator seed(long seed) { - this.seed = seed; - return this; - } - - public IrisWorldCreator studioMode() { - this.studio = true; - return this; - } - - public IrisWorldCreator productionMode() { - this.studio = false; - return this; - } - - public WorldCreator create() { - IrisDimension dim = IrisData.loadAnyDimension(dimensionName, null); - - IrisWorld w = IrisWorld.builder() - .name(name) - .minHeight(dim.getMinHeight()) - .maxHeight(dim.getMaxHeight()) - .seed(seed) - .worldFolder(new File(Bukkit.getWorldContainer(), name)) - .environment(findEnvironment()) - .build(); - ChunkGenerator g = new BukkitChunkGenerator(w, studio, studio - ? dim.getLoader().getDataFolder() : - new File(w.worldFolder(), "iris/pack"), dimensionName); - - - return new WorldCreator(name) - .environment(w.environment()) - .generateStructures(true) - .generator(g).seed(seed); - } - - private World.Environment findEnvironment() { - IrisDimension dim = IrisData.loadAnyDimension(dimensionName, null); - if (dim == null || dim.getEnvironment() == null) { - return World.Environment.NORMAL; - } else { - return dim.getEnvironment(); - } - } - - public IrisWorldCreator studio(boolean studio) { - this.studio = studio; - return this; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/EnginePanic.java b/core/src/main/java/com/volmit/iris/engine/EnginePanic.java deleted file mode 100644 index 582fa3d55..000000000 --- a/core/src/main/java/com/volmit/iris/engine/EnginePanic.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KMap; - -public class EnginePanic { - private static final KMap stuff = new KMap<>(); - private static KMap last = new KMap<>(); - - public static void add(String key, String value) { - stuff.put(key, value); - } - - public static void saveLast() { - last = stuff.copy(); - } - - public static void lastPanic() { - for (String i : last.keySet()) { - Iris.error("Last Panic " + i + ": " + stuff.get(i)); - } - } - - public static void panic() { - lastPanic(); - for (String i : stuff.keySet()) { - Iris.error("Engine Panic " + i + ": " + stuff.get(i)); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/IrisComplex.java b/core/src/main/java/com/volmit/iris/engine/IrisComplex.java deleted file mode 100644 index 98e73444e..000000000 --- a/core/src/main/java/com/volmit/iris/engine/IrisComplex.java +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.data.DataProvider; -import com.volmit.iris.util.interpolation.IrisInterpolation.NoiseKey; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.stream.ProceduralStream; -import com.volmit.iris.util.stream.interpolation.Interpolated; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.bukkit.Material; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; - -import java.io.File; -import java.util.*; - -@Data -@EqualsAndHashCode(exclude = "data") -@ToString(exclude = "data") -public class IrisComplex implements DataProvider { - private static final BlockData AIR = Material.AIR.createBlockData(); - private RNG rng; - private double fluidHeight; - private IrisData data; - private Map> generators; - private ProceduralStream regionStream; - private ProceduralStream regionStyleStream; - private ProceduralStream regionIdentityStream; - private ProceduralStream regionIDStream; - private ProceduralStream bridgeStream; - private ProceduralStream landBiomeStream; - private ProceduralStream caveBiomeStream; - private ProceduralStream seaBiomeStream; - private ProceduralStream shoreBiomeStream; - private ProceduralStream baseBiomeStream; - private ProceduralStream baseBiomeIDStream; - private ProceduralStream trueBiomeStream; - private ProceduralStream trueBiomeDerivativeStream; - private ProceduralStream heightStream; - private ProceduralStream roundedHeighteightStream; - private ProceduralStream maxHeightStream; - private ProceduralStream overlayStream; - private ProceduralStream heightFluidStream; - private ProceduralStream slopeStream; - private ProceduralStream topSurfaceStream; - private ProceduralStream terrainSurfaceDecoration; - private ProceduralStream terrainCeilingDecoration; - private ProceduralStream terrainCaveSurfaceDecoration; - private ProceduralStream terrainCaveCeilingDecoration; - private ProceduralStream seaSurfaceDecoration; - private ProceduralStream seaFloorDecoration; - private ProceduralStream shoreSurfaceDecoration; - private ProceduralStream rockStream; - private ProceduralStream fluidStream; - private IrisBiome focusBiome; - private IrisRegion focusRegion; - - public IrisComplex(Engine engine) { - this(engine, false); - } - - public IrisComplex(Engine engine, boolean simple) { - int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize(); - IrisBiome emptyBiome = new IrisBiome(); - UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes()); - this.rng = new RNG(engine.getSeedManager().getComplex()); - this.data = engine.getData(); - double height = engine.getMaxHeight(); - fluidHeight = engine.getDimension().getFluidHeight(); - generators = new HashMap<>(); - focusBiome = engine.getFocus(); - focusRegion = engine.getFocusRegion(); - Map> inferredStreams = new HashMap<>(); - - if (focusBiome != null) { - focusBiome.setInferredType(InferredType.LAND); - focusRegion = findRegion(focusBiome, engine); - } - - //@builder - if (focusRegion != null) { - focusRegion.getAllBiomes(this).forEach(this::registerGenerators); - } else { - engine.getDimension() - .getRegions() - .forEach(i -> data.getRegionLoader().load(i) - .getAllBiomes(this) - .forEach(this::registerGenerators)); - } - boolean legacy = engine.getDimension().isLegacyRarity(); - overlayStream = ProceduralStream.ofDouble((x, z) -> 0.0D).waste("Overlay Stream"); - engine.getDimension().getOverlayNoise().forEach(i -> overlayStream = overlayStream.add((x, z) -> i.get(rng, getData(), x, z))); - rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() - .select(engine.getDimension().getRockPalette().getBlockData(data)).waste("Rock Stream"); - fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream() - .select(engine.getDimension().getFluidPalette().getBlockData(data)).waste("Fluid Stream"); - regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream() - .zoom(engine.getDimension().getRegionZoom()).waste("Region Style"); - regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE).waste("Region Identity Stream"); - regionStream = focusRegion != null ? - ProceduralStream.of((x, z) -> focusRegion, - Interpolated.of(a -> 0D, a -> focusRegion)) - : regionStyleStream - .selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions()), legacy) - .cache2D("regionStream", engine, cacheSize).waste("Region Stream"); - regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i), - String.valueOf(i * 38445).hashCode() * 3245556666L)).waste("Region ID Stream"); - caveBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z)) - .convert((r) - -> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream() - .zoom(engine.getDimension().getBiomeZoom()) - .zoom(r.getCaveBiomeZoom()) - .selectRarity(data.getBiomeLoader().loadAll(r.getCaveBiomes()), legacy) - .onNull(emptyBiome) - ).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream"); - inferredStreams.put(InferredType.CAVE, caveBiomeStream); - landBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z)) - .convert((r) - -> engine.getDimension().getLandBiomeStyle().create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream() - .zoom(engine.getDimension().getBiomeZoom()) - .zoom(engine.getDimension().getLandZoom()) - .zoom(r.getLandBiomeZoom()) - .selectRarity(data.getBiomeLoader().loadAll(r.getLandBiomes(), (t) -> t.setInferredType(InferredType.LAND)), legacy) - ).convertAware2D(ProceduralStream::get) - .cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream"); - inferredStreams.put(InferredType.LAND, landBiomeStream); - seaBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z)) - .convert((r) - -> engine.getDimension().getSeaBiomeStyle().create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream() - .zoom(engine.getDimension().getBiomeZoom()) - .zoom(engine.getDimension().getSeaZoom()) - .zoom(r.getSeaBiomeZoom()) - .selectRarity(data.getBiomeLoader().loadAll(r.getSeaBiomes(), (t) -> t.setInferredType(InferredType.SEA)), legacy) - ).convertAware2D(ProceduralStream::get) - .cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream"); - inferredStreams.put(InferredType.SEA, seaBiomeStream); - shoreBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z)) - .convert((r) - -> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream() - .zoom(engine.getDimension().getBiomeZoom()) - .zoom(r.getShoreBiomeZoom()) - .selectRarity(data.getBiomeLoader().loadAll(r.getShoreBiomes(), (t) -> t.setInferredType(InferredType.SHORE)), legacy) - ).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize).waste("Shore Biome Stream"); - inferredStreams.put(InferredType.SHORE, shoreBiomeStream); - bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(), - Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) : - engine.getDimension().getContinentalStyle().create(rng.nextParallelRNG(234234565), getData()) - .bake().scale(1D / engine.getDimension().getContinentZoom()).bake().stream() - .convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND) - .cache2D("bridgeStream", engine, cacheSize).waste("Bridge Stream"); - baseBiomeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome, - Interpolated.of(a -> 0D, a -> focusBiome)) : - bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z)) - .convertAware2D(this::implode) - .cache2D("baseBiomeStream", engine, cacheSize).waste("Base Biome Stream"); - heightStream = ProceduralStream.of((x, z) -> { - IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z); - return getHeight(engine, b, x, z, engine.getSeedManager().getHeight()); - }, Interpolated.DOUBLE).cache2D("heightStream", engine, cacheSize).waste("Height Stream"); - roundedHeighteightStream = heightStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getHeight().get(x, z)) - .round().waste("Rounded Height Stream"); - slopeStream = heightStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getHeight().get(x, z)) - .slope(3).cache2D("slopeStream", engine, cacheSize).waste("Slope Stream"); - trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D, - b -> focusBiome)) - .cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream - .convertAware2D((h, x, z) -> - fixBiomeType(h, baseBiomeStream.get(x, z), - regionStream.contextInjecting((c, xx, zz) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight)) - .cache2D("trueBiomeStream", engine, cacheSize).waste("True Biome Stream"); - trueBiomeDerivativeStream = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z)) - .convert(IrisBiome::getDerivative).cache2D("trueBiomeDerivativeStream", engine, cacheSize).waste("True Biome Derivative Stream"); - heightFluidStream = heightStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getHeight().get(x, z)) - .max(fluidHeight).cache2D("heightFluidStream", engine, cacheSize).waste("Height Fluid Stream"); - maxHeightStream = ProceduralStream.ofDouble((x, z) -> height).waste("Max Height Stream"); - terrainSurfaceDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize).waste("Surface Decoration Stream"); - terrainCeilingDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCeilingDecoration", engine, cacheSize).waste("Ceiling Decoration Stream"); - terrainCaveSurfaceDecoration = caveBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getCave().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainCaveSurfaceDecoration", engine, cacheSize).waste("Cave Surface Stream"); - terrainCaveCeilingDecoration = caveBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getCave().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCaveCeilingDecoration", engine, cacheSize).waste("Cave Ceiling Stream"); - shoreSurfaceDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SHORE_LINE)).cache2D("shoreSurfaceDecoration", engine, cacheSize).waste("Shore Surface Stream"); - seaSurfaceDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_SURFACE)).cache2D("seaSurfaceDecoration", engine, cacheSize).waste("Sea Surface Stream"); - seaFloorDecoration = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_FLOOR)).cache2D("seaFloorDecoration", engine, cacheSize).waste("Sea Floor Stream"); - baseBiomeIDStream = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z)) - .convertAware2D((b, x, z) -> { - UUID d = regionIDStream.get(x, z); - return new UUID(b.getLoadKey().hashCode() * 818223L, - d.hashCode()); - }) - .cache2D("", engine, cacheSize).waste("Biome ID Stream"); - //@done - } - - public ProceduralStream getBiomeStream(InferredType type) { - switch (type) { - case CAVE: - return caveBiomeStream; - case LAND: - return landBiomeStream; - case SEA: - return seaBiomeStream; - case SHORE: - return shoreBiomeStream; - default: - break; - } - - return null; - } - - private IrisRegion findRegion(IrisBiome focus, Engine engine) { - for (IrisRegion i : engine.getDimension().getAllRegions(engine)) { - if (i.getAllBiomeIds().contains(focus.getLoadKey())) { - return i; - } - } - - String key = UUID.randomUUID().toString(); - IrisRegion region = new IrisRegion(); - region.getLandBiomes().add(focus.getLoadKey()); - region.getSeaBiomes().add(focus.getLoadKey()); - region.getShoreBiomes().add(focus.getLoadKey()); - region.setLoadKey(key); - region.setLoader(data); - region.setLoadFile(new File(data.getDataFolder(), data.getRegionLoader().getFolderName() + "/" + key + ".json")); - return region; - } - - private IrisDecorator decorateFor(IrisBiome b, double x, double z, IrisDecorationPart part) { - RNG rngc = new RNG(Cache.key(((int) x), ((int) z))); - - for (IrisDecorator i : b.getDecorators()) { - if (!i.getPartOf().equals(part)) { - continue; - } - - BlockData block = i.getBlockData(b, rngc, x, z, data); - - if (block != null) { - return i; - } - } - - return null; - } - - private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) { - double sh = region.getShoreHeight(x, z); - - if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) { - return shoreBiomeStream.get(x, z); - } - - if (height > fluidHeight + sh && !biome.isLand()) { - return landBiomeStream.get(x, z); - } - - if (height < fluidHeight && !biome.isAquatic()) { - return seaBiomeStream.get(x, z); - } - - if (height == fluidHeight && !biome.isShore()) { - return shoreBiomeStream.get(x, z); - } - - return biome; - } - - private double interpolateGenerators(Engine engine, IrisInterpolator interpolator, Set generators, double x, double z, long seed) { - if (generators.isEmpty()) { - return 0; - } - - HashMap cache = new HashMap<>(64); - double hi = interpolator.interpolate(x, z, (xx, zz) -> { - try { - IrisBiome bx = baseBiomeStream.get(xx, zz); - cache.put(new NoiseKey(xx, zz), bx); - double b = 0; - - for (IrisGenerator gen : generators) { - b += bx.getGenLinkMax(gen.getLoadKey(), engine); - } - - return b; - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - Iris.error("Failed to sample hi biome at " + xx + " " + zz + "..."); - } - - return 0; - }); - - double lo = interpolator.interpolate(x, z, (xx, zz) -> { - try { - IrisBiome bx = cache.get(new NoiseKey(xx, zz)); - if (bx == null) { - bx = baseBiomeStream.get(xx, zz); - cache.put(new NoiseKey(xx, zz), bx); - } - double b = 0; - - for (IrisGenerator gen : generators) { - b += bx.getGenLinkMin(gen.getLoadKey(), engine); - } - - return b; - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - Iris.error("Failed to sample lo biome at " + xx + " " + zz + "..."); - } - - return 0; - }); - - double d = 0; - - for (IrisGenerator i : generators) { - d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945)); - } - - return d / generators.size(); - } - - private double getInterpolatedHeight(Engine engine, double x, double z, long seed) { - double h = 0; - - for (IrisInterpolator i : generators.keySet()) { - h += interpolateGenerators(engine, i, generators.get(i), x, z, seed); - } - - return h; - } - - private double getHeight(Engine engine, IrisBiome b, double x, double z, long seed) { - return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0); - } - - private void registerGenerators(IrisBiome biome) { - biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this))); - } - - private void registerGenerator(IrisGenerator cachedGenerator) { - generators.computeIfAbsent(cachedGenerator.getInterpolator(), (k) -> new HashSet<>()).add(cachedGenerator); - } - - private IrisBiome implode(IrisBiome b, Double x, Double z) { - if (b.getChildren().isEmpty()) { - return b; - } - - return implode(b, x, z, 3); - } - - private IrisBiome implode(IrisBiome b, Double x, Double z, int max) { - if (max < 0) { - return b; - } - - if (b.getChildren().isEmpty()) { - return b; - } - - CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor()); - KList chx = b.getRealChildren(this).copy(); - chx.add(b); - IrisBiome biome = childCell.fitRarity(chx, x, z); - biome.setInferredType(b.getInferredType()); - return implode(biome, x, z, max - 1); - } - - public void close() { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/IrisEngine.java b/core/src/main/java/com/volmit/iris/engine/IrisEngine.java deleted file mode 100644 index 9f570d642..000000000 --- a/core/src/main/java/com/volmit/iris/engine/IrisEngine.java +++ /dev/null @@ -1,583 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine; - -import com.google.common.util.concurrent.AtomicDouble; -import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.core.ServerConfigurator; -import com.volmit.iris.core.events.IrisEngineHotloadEvent; -import com.volmit.iris.core.gui.PregeneratorJob; -import com.volmit.iris.core.loader.ResourceLoader; -import com.volmit.iris.core.nms.container.BlockPos; -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.core.project.IrisProject; -import com.volmit.iris.core.scripting.environment.EngineEnvironment; -import com.volmit.iris.core.service.PreservationSVC; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.*; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.atomics.AtomicRollingSequence; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterStructurePOI; -import com.volmit.iris.util.matter.slices.container.JigsawStructureContainer; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.bukkit.Material; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; -import org.bukkit.command.CommandSender; - -import java.io.File; -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -@Data -@EqualsAndHashCode(exclude = "context") -@ToString(exclude = "context") -public class IrisEngine implements Engine { - private final AtomicInteger bud; - private final AtomicInteger buds; - private final AtomicInteger generated; - private final AtomicInteger generatedLast; - private final AtomicDouble perSecond; - private final AtomicLong lastGPS; - private final EngineTarget target; - private final IrisContext context; - private final EngineMantle mantle; - private final ChronoLatch perSecondLatch; - private final ChronoLatch perSecondBudLatch; - private final EngineMetrics metrics; - private final boolean studio; - private final AtomicRollingSequence wallClock; - private final int art; - private final AtomicCache engineData = new AtomicCache<>(); - private final AtomicBoolean cleaning; - private final ChronoLatch cleanLatch; - private final SeedManager seedManager; - private CompletableFuture hash32; - private EngineMode mode; - private EngineEffects effects; - private EngineEnvironment execution; - private EngineWorldManager worldManager; - private volatile int parallelism; - private boolean failing; - private boolean closed; - private int cacheId; - private double maxBiomeObjectDensity; - private double maxBiomeLayerDensity; - private double maxBiomeDecoratorDensity; - private IrisComplex complex; - - public IrisEngine(EngineTarget target, boolean studio) { - this.studio = studio; - this.target = target; - getEngineData(); - verifySeed(); - this.seedManager = new SeedManager(target.getWorld().getRawWorldSeed()); - bud = new AtomicInteger(0); - buds = new AtomicInteger(0); - metrics = new EngineMetrics(32); - cleanLatch = new ChronoLatch(10000); - generatedLast = new AtomicInteger(0); - perSecond = new AtomicDouble(0); - perSecondLatch = new ChronoLatch(1000, false); - perSecondBudLatch = new ChronoLatch(1000, false); - wallClock = new AtomicRollingSequence(32); - lastGPS = new AtomicLong(M.ms()); - generated = new AtomicInteger(0); - mantle = new IrisEngineMantle(this); - context = new IrisContext(this); - cleaning = new AtomicBoolean(false); - execution = getData().getEnvironment().with(this); - if (studio) { - getData().dump(); - getData().clearLists(); - getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey())); - } - context.touch(); - getData().setEngine(this); - getData().loadPrefetch(this); - Iris.info("Initializing Engine: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " (" + target.getDimension().getDimensionHeight() + " height) Seed: " + getSeedManager().getSeed()); - failing = false; - closed = false; - art = J.ar(this::tickRandomPlayer, 0); - setupEngine(); - Iris.debug("Engine Initialized " + getCacheID()); - } - - private void verifySeed() { - if (getEngineData().getSeed() != null && getEngineData().getSeed() != target.getWorld().getRawWorldSeed()) { - target.getWorld().setRawWorldSeed(getEngineData().getSeed()); - } - } - - private void tickRandomPlayer() { - recycle(); - if (perSecondBudLatch.flip()) { - buds.set(bud.get()); - bud.set(0); - } - - if (effects != null) { - effects.tickRandomPlayer(); - } - } - - private void prehotload() { - worldManager.close(); - complex.close(); - effects.close(); - mode.close(); - execution = getData().getEnvironment().with(this); - - J.a(() -> new IrisProject(getData().getDataFolder()).updateWorkspace()); - } - - private void setupEngine() { - try { - Iris.debug("Setup Engine " + getCacheID()); - cacheId = RNG.r.nextInt(); - worldManager = new IrisWorldManager(this); - complex = new IrisComplex(this); - effects = new IrisEngineEffects(this); - hash32 = new CompletableFuture<>(); - mantle.hotload(); - setupMode(); - getDimension().getEngineScripts().forEach(execution::execute); - J.a(this::computeBiomeMaxes); - J.a(() -> { - File[] roots = getData().getLoaders() - .values() - .stream() - .map(ResourceLoader::getFolderName) - .map(n -> new File(getData().getDataFolder(), n)) - .filter(File::exists) - .filter(File::isDirectory) - .toArray(File[]::new); - hash32.complete(IO.hashRecursive(roots)); - }); - } catch (Throwable e) { - Iris.error("FAILED TO SETUP ENGINE!"); - e.printStackTrace(); - } - - Iris.debug("Engine Setup Complete " + getCacheID()); - } - - private void setupMode() { - if (mode != null) { - mode.close(); - } - - mode = getDimension().getMode().create(this); - } - - @Override - public void generateMatter(int x, int z, boolean multicore, ChunkContext context) { - getMantle().generateMatter(x, z, multicore, context); - } - - @Override - public Set getObjectsAt(int x, int z) { - return getMantle().getObjectComponent().guess(x, z); - } - - @Override - public Set> getPOIsAt(int chunkX, int chunkY) { - Set> pois = new HashSet<>(); - getMantle().getMantle().iterateChunk(chunkX, chunkY, MatterStructurePOI.class, (x, y, z, d) -> pois.add(new Pair<>(d.getType(), new BlockPos(x, y, z)))); - return pois; - } - - @Override - public IrisJigsawStructure getStructureAt(int x, int z) { - return getMantle().getJigsawComponent().guess(x, z); - } - - @Override - public IrisJigsawStructure getStructureAt(int x, int y, int z) { - var container = getMantle().getMantle().get(x, y, z, JigsawStructureContainer.class); - return container == null ? null : container.load(getData()); - } - - private void warmupChunk(int x, int z) { - for (int i = 0; i < 16; i++) { - for (int j = 0; j < 16; j++) { - int xx = x + (i << 4); - int zz = z + (z << 4); - getComplex().getTrueBiomeStream().get(xx, zz); - getComplex().getHeightStream().get(xx, zz); - } - } - } - - @Override - public void hotload() { - hotloadSilently(); - Iris.callEvent(new IrisEngineHotloadEvent(this)); - } - - public void hotloadComplex() { - complex.close(); - complex = new IrisComplex(this); - } - - public void hotloadSilently() { - getData().dump(); - getData().clearLists(); - getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey())); - prehotload(); - setupEngine(); - J.a(() -> { - synchronized (ServerConfigurator.class) { - ServerConfigurator.installDataPacks(false); - } - }); - } - - @Override - public IrisEngineData getEngineData() { - return engineData.aquire(() -> { - //TODO: Method this file - File f = new File(getWorld().worldFolder(), "iris/engine-data/" + getDimension().getLoadKey() + ".json"); - IrisEngineData data = null; - - if (f.exists()) { - try { - data = new Gson().fromJson(IO.readAll(f), IrisEngineData.class); - if (data == null) { - Iris.error("Failed to read Engine Data! Corrupted File? recreating..."); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - if (data == null) { - data = new IrisEngineData(); - data.getStatistics().setVersion(Iris.instance.getIrisVersion()); - data.getStatistics().setMCVersion(Iris.instance.getMCVersion()); - data.getStatistics().setUpgradedVersion(Iris.instance.getIrisVersion()); - if (data.getStatistics().getVersion() == -1 || data.getStatistics().getMCVersion() == -1 ) { - Iris.error("Failed to setup Engine Data!"); - } - - if (f.getParentFile().exists() || f.getParentFile().mkdirs()) { - try { - IO.writeAll(f, new Gson().toJson(data)); - } catch (IOException e) { - e.printStackTrace(); - } - } else { - Iris.error("Failed to setup Engine Data!"); - } - } - - return data; - }); - } - - @Override - public int getGenerated() { - return generated.get(); - } - - @Override - public double getGeneratedPerSecond() { - if (perSecondLatch.flip()) { - double g = generated.get() - generatedLast.get(); - generatedLast.set(generated.get()); - - if (g == 0) { - return 0; - } - - long dur = M.ms() - lastGPS.get(); - lastGPS.set(M.ms()); - perSecond.set(g / ((double) (dur) / 1000D)); - } - - return perSecond.get(); - } - - @Override - public boolean isStudio() { - return studio; - } - - private void computeBiomeMaxes() { - for (IrisBiome i : getDimension().getAllBiomes(this)) { - double density = 0; - - for (IrisObjectPlacement j : i.getObjects()) { - density += j.getDensity() * j.getChance(); - } - - maxBiomeObjectDensity = Math.max(maxBiomeObjectDensity, density); - density = 0; - - for (IrisDecorator j : i.getDecorators()) { - density += Math.max(j.getStackMax(), 1) * j.getChance(); - } - - maxBiomeDecoratorDensity = Math.max(maxBiomeDecoratorDensity, density); - density = 0; - - for (IrisBiomePaletteLayer j : i.getLayers()) { - density++; - } - - maxBiomeLayerDensity = Math.max(maxBiomeLayerDensity, density); - } - } - - @Override - public int getBlockUpdatesPerSecond() { - return buds.get(); - } - - public void printMetrics(CommandSender sender) { - KMap totals = new KMap<>(); - KMap weights = new KMap<>(); - double masterWallClock = wallClock.getAverage(); - KMap timings = getMetrics().pull(); - double totalWeight = 0; - double wallClock = getMetrics().getTotal().getAverage(); - - for (double j : timings.values()) { - totalWeight += j; - } - - for (String j : timings.k()) { - weights.put(getName() + "." + j, (wallClock / totalWeight) * timings.get(j)); - } - - totals.put(getName(), wallClock); - - double mtotals = 0; - - for (double i : totals.values()) { - mtotals += i; - } - - for (String i : totals.k()) { - totals.put(i, (masterWallClock / mtotals) * totals.get(i)); - } - - double v = 0; - - for (double i : weights.values()) { - v += i; - } - - for (String i : weights.k()) { - weights.put(i, weights.get(i) / v); - } - - sender.sendMessage("Total: " + C.BOLD + C.WHITE + Form.duration(masterWallClock, 0)); - - for (String i : totals.k()) { - sender.sendMessage(" Engine " + C.UNDERLINE + C.GREEN + i + C.RESET + ": " + C.BOLD + C.WHITE + Form.duration(totals.get(i), 0)); - } - - sender.sendMessage("Details: "); - - for (String i : weights.sortKNumber().reverse()) { - String befb = C.UNDERLINE + "" + C.GREEN + "" + i.split("\\Q[\\E")[0] + C.RESET + C.GRAY + "["; - String num = C.GOLD + i.split("\\Q[\\E")[1].split("]")[0] + C.RESET + C.GRAY + "]."; - String afb = C.ITALIC + "" + C.AQUA + i.split("\\Q]\\E")[1].substring(1) + C.RESET + C.GRAY; - - sender.sendMessage(" " + befb + num + afb + ": " + C.BOLD + C.WHITE + Form.pc(weights.get(i), 0)); - } - } - - @Override - public void close() { - PregeneratorJob.shutdownInstance(); - closed = true; - J.car(art); - getWorldManager().close(); - getTarget().close(); - saveEngineData(); - getMantle().close(); - getComplex().close(); - mode.close(); - getData().dump(); - getData().clearLists(); - Iris.service(PreservationSVC.class).dereference(); - Iris.debug("Engine Fully Shutdown!"); - complex = null; - } - - @Override - public boolean isClosed() { - return closed; - } - - @Override - public void recycle() { - if (!cleanLatch.flip()) { - return; - } - - if (cleaning.get()) { - cleanLatch.flipDown(); - return; - } - - cleaning.set(true); - - J.a(() -> { - try { - getData().getObjectLoader().clean(); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Cleanup failed! Enable debug to see stacktrace."); - } - - cleaning.lazySet(false); - }); - } - - @BlockCoordinates - @Override - public void generate(int x, int z, Hunk vblocks, Hunk vbiomes, boolean multicore) throws WrongEngineBroException { - if (closed) { - throw new WrongEngineBroException(); - } - - context.touch(); - getEngineData().getStatistics().generatedChunk(); - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - Hunk blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t)); - - if (getDimension().isDebugChunkCrossSections() && ((x >> 4) % getDimension().getDebugCrossSectionsMod() == 0 || (z >> 4) % getDimension().getDebugCrossSectionsMod() == 0)) { - for (int i = 0; i < 16; i++) { - for (int j = 0; j < 16; j++) { - blocks.set(i, 0, j, Material.CRYING_OBSIDIAN.createBlockData()); - } - } - } else { - mode.generate(x, z, blocks, vbiomes, multicore); - } - - getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true); - getMetrics().getTotal().put(p.getMilliseconds()); - generated.incrementAndGet(); - - if (generated.get() == 661) { - J.a(() -> getData().savePrefetch(this)); - } - } catch (Throwable e) { - Iris.reportError(e); - fail("Failed to generate " + x + ", " + z, e); - } - } - - @Override - public void saveEngineData() { - //TODO: Method this file - File f = new File(getWorld().worldFolder(), "iris/engine-data/" + getDimension().getLoadKey() + ".json"); - f.getParentFile().mkdirs(); - try { - IO.writeAll(f, new Gson().toJson(getEngineData())); - Iris.debug("Saved Engine Data"); - } catch (IOException e) { - Iris.error("Failed to save Engine Data"); - e.printStackTrace(); - } - } - - @Override - public void blockUpdatedMetric() { - bud.incrementAndGet(); - } - - @Override - public IrisBiome getFocus() { - if (getDimension().getFocus() == null || getDimension().getFocus().trim().isEmpty()) { - return null; - } - - return getData().getBiomeLoader().load(getDimension().getFocus()); - } - - @Override - public IrisRegion getFocusRegion() { - if (getDimension().getFocusRegion() == null || getDimension().getFocusRegion().trim().isEmpty()) { - return null; - } - - return getData().getRegionLoader().load(getDimension().getFocusRegion()); - } - - @Override - public void fail(String error, Throwable e) { - failing = true; - Iris.error(error); - e.printStackTrace(); - } - - @Override - public boolean hasFailed() { - return failing; - } - - @Override - public int getCacheID() { - return cacheId; - } - - private boolean EngineSafe() { - // Todo: this has potential if done right - int EngineMCVersion = getEngineData().getStatistics().getMCVersion(); - int EngineIrisVersion = getEngineData().getStatistics().getVersion(); - int MinecraftVersion = Iris.instance.getMCVersion(); - int IrisVersion = Iris.instance.getIrisVersion(); - if (EngineIrisVersion != IrisVersion) { - return false; - } - if (EngineMCVersion != MinecraftVersion) { - return false; - } - return true; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/IrisEngineEffects.java b/core/src/main/java/com/volmit/iris/engine/IrisEngineEffects.java deleted file mode 100644 index 6d0804892..000000000 --- a/core/src/main/java/com/volmit/iris/engine/IrisEngineEffects.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedComponent; -import com.volmit.iris.engine.framework.EngineEffects; -import com.volmit.iris.engine.framework.EnginePlayer; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.bukkit.entity.Player; - -import java.util.List; -import java.util.UUID; -import java.util.concurrent.Semaphore; - -public class IrisEngineEffects extends EngineAssignedComponent implements EngineEffects { - private final KMap players; - private final Semaphore limit; - - public IrisEngineEffects(Engine engine) { - super(engine, "FX"); - players = new KMap<>(); - limit = new Semaphore(1); - } - - @Override - public void updatePlayerMap() { - List pr = getEngine().getWorld().getPlayers(); - - if (pr == null) { - return; - } - - for (Player i : pr) { - boolean pcc = players.containsKey(i.getUniqueId()); - if (!pcc) { - players.put(i.getUniqueId(), new EnginePlayer(getEngine(), i)); - } - } - - for (UUID i : players.k()) { - if (!pr.contains(players.get(i).getPlayer())) { - players.remove(i); - } - } - } - - @Override - public void tickRandomPlayer() { - if (limit.tryAcquire()) { - if (M.r(0.02)) { - updatePlayerMap(); - limit.release(); - return; - } - - if (players.isEmpty()) { - limit.release(); - return; - } - - double limitms = 1.5; - int max = players.size(); - PrecisionStopwatch p = new PrecisionStopwatch(); - - while (max-- > 0 && M.ms() - p.getMilliseconds() < limitms) { - players.v().getRandom().tick(); - } - - limit.release(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/IrisEngineMantle.java b/core/src/main/java/com/volmit/iris/engine/IrisEngineMantle.java deleted file mode 100644 index 71a412290..000000000 --- a/core/src/main/java/com/volmit/iris/engine/IrisEngineMantle.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine; - -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.engine.mantle.MantleComponent; -import com.volmit.iris.engine.mantle.components.MantleCarvingComponent; -import com.volmit.iris.engine.mantle.components.MantleFluidBodyComponent; -import com.volmit.iris.engine.mantle.components.MantleJigsawComponent; -import com.volmit.iris.engine.mantle.components.MantleObjectComponent; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import lombok.*; - -import java.io.File; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -@Data -@EqualsAndHashCode(exclude = "engine") -@ToString(exclude = "engine") -public class IrisEngineMantle implements EngineMantle { - private final Engine engine; - private final Mantle mantle; - @Getter(AccessLevel.NONE) - private final KMap> components; - private final KMap registeredComponents = new KMap<>(); - private final AtomicCache, Integer>>> componentsCache = new AtomicCache<>(); - private final AtomicCache> disabledFlags = new AtomicCache<>(); - private final MantleObjectComponent object; - private final MantleJigsawComponent jigsaw; - - public IrisEngineMantle(Engine engine) { - this.engine = engine; - this.mantle = new Mantle(new File(engine.getWorld().worldFolder(), "mantle"), engine.getTarget().getHeight()); - components = new KMap<>(); - registerComponent(new MantleCarvingComponent(this)); - registerComponent(new MantleFluidBodyComponent(this)); - jigsaw = new MantleJigsawComponent(this); - registerComponent(jigsaw); - object = new MantleObjectComponent(this); - registerComponent(object); - } - - @Override - public int getRadius() { - if (components.isEmpty()) return 0; - return getComponents().getFirst().getB(); - } - - @Override - public int getRealRadius() { - if (components.isEmpty()) return 0; - return getComponents().getLast().getB(); - } - - @Override - public List, Integer>> getComponents() { - return componentsCache.aquire(() -> { - var list = components.keySet() - .stream() - .sorted() - .map(components::get) - .map(components -> { - int radius = components.stream() - .filter(MantleComponent::isEnabled) - .mapToInt(MantleComponent::getRadius) - .max() - .orElse(0); - return new Pair<>(List.copyOf(components), radius); - }) - .filter(pair -> !pair.getA().isEmpty()) - .toList(); - - int radius = 0; - for (var pair : list.reversed()) { - radius += pair.getB(); - pair.setB(Math.ceilDiv(radius, 16)); - } - - return list; - }); - } - - @Override - public Map getRegisteredComponents() { - return Collections.unmodifiableMap(registeredComponents); - } - - @Override - public boolean registerComponent(MantleComponent c) { - if (registeredComponents.putIfAbsent(c.getFlag(), c) != null) return false; - c.setEnabled(!getDisabledFlags().contains(c.getFlag())); - components.computeIfAbsent(c.getPriority(), k -> new KList<>()).add(c); - componentsCache.reset(); - return true; - } - - @Override - public KList getComponentFlags() { - return new KList<>(registeredComponents.keySet()); - } - - @Override - public void hotload() { - disabledFlags.reset(); - for (var component : registeredComponents.values()) { - component.hotload(); - component.setEnabled(!getDisabledFlags().contains(component.getFlag())); - } - componentsCache.reset(); - } - - private Set getDisabledFlags() { - return disabledFlags.aquire(() -> Set.copyOf(getDimension().getDisabledComponents())); - } - - @Override - public MantleJigsawComponent getJigsawComponent() { - return jigsaw; - } - - @Override - public MantleObjectComponent getObjectComponent() { - return object; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/IrisWorldManager.java b/core/src/main/java/com/volmit/iris/engine/IrisWorldManager.java deleted file mode 100644 index c6233001e..000000000 --- a/core/src/main/java/com/volmit/iris/engine/IrisWorldManager.java +++ /dev/null @@ -1,699 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedWorldManager; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterMarker; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.plugin.Chunks; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.Looper; -import com.volmit.iris.util.scheduling.jobs.QueueJob; -import io.papermc.lib.PaperLib; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.inventory.ItemStack; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@EqualsAndHashCode(callSuper = true) -@Data -public class IrisWorldManager extends EngineAssignedWorldManager { - private final Looper looper; - private final int id; - private final KList updateQueue = new KList<>(); - private final ChronoLatch cl; - private final ChronoLatch clw; - private final ChronoLatch ecl; - private final ChronoLatch cln; - private final ChronoLatch chunkUpdater; - private final ChronoLatch chunkDiscovery; - private final KMap> cleanup = new KMap<>(); - private final ScheduledExecutorService cleanupService; - private double energy = 25; - private int entityCount = 0; - private long charge = 0; - private int actuallySpawned = 0; - private int cooldown = 0; - private List precount = new KList<>(); - private KSet injectBiomes = new KSet<>(); - - public IrisWorldManager() { - super(null); - cl = null; - ecl = null; - cln = null; - clw = null; - looper = null; - chunkUpdater = null; - chunkDiscovery = null; - cleanupService = null; - id = -1; - } - - public IrisWorldManager(Engine engine) { - super(engine); - chunkUpdater = new ChronoLatch(3000); - chunkDiscovery = new ChronoLatch(5000); - cln = new ChronoLatch(60000); - cl = new ChronoLatch(3000); - ecl = new ChronoLatch(250); - clw = new ChronoLatch(1000, true); - cleanupService = Executors.newSingleThreadScheduledExecutor(runnable -> { - var thread = new Thread(runnable, "Iris Mantle Cleanup " + getTarget().getWorld().name()); - thread.setPriority(Thread.MIN_PRIORITY); - return thread; - }); - id = engine.getCacheID(); - energy = 25; - looper = new Looper() { - @Override - protected long loop() { - if (getEngine().isClosed() || getEngine().getCacheID() != id) { - interrupt(); - } - - if (!getEngine().getWorld().hasRealWorld() && clw.flip()) { - getEngine().getWorld().tryGetRealWorld(); - } - - if (getEngine().getWorld().hasRealWorld()) { - if (getEngine().getWorld().getPlayers().isEmpty()) { - return 5000; - } - - if (chunkUpdater.flip()) { - updateChunks(); - } - - if (chunkDiscovery.flip()) { - discoverChunks(); - } - - if (cln.flip()) { - engine.getEngineData().cleanup(getEngine()); - } - - if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem() && !IrisSettings.get().getWorld().isAnbientEntitySpawningSystem()) { - return 3000; - } - - if (getDimension().isInfiniteEnergy()) { - energy += 1000; - fixEnergy(); - } - - if (M.ms() < charge) { - energy += 70; - fixEnergy(); - } - - if (precount != null) { - entityCount = 0; - for (Entity i : precount) { - if (i instanceof LivingEntity) { - if (!i.isDead()) { - entityCount++; - } - } - } - - precount = null; - } - - if (energy < 650) { - if (ecl.flip()) { - energy *= 1 + (0.02 * M.clip((1D - getEntitySaturation()), 0D, 1D)); - fixEnergy(); - } - } - - onAsyncTick(); - } - - return IrisSettings.get().getWorld().getAsyncTickIntervalMS(); - } - }; - looper.setPriority(Thread.MIN_PRIORITY); - looper.setName("Iris World Manager " + getTarget().getWorld().name()); - looper.start(); - } - - private void discoverChunks() { - var mantle = getEngine().getMantle().getMantle(); - for (Player i : getEngine().getWorld().realWorld().getPlayers()) { - int r = 1; - - for (int x = -r; x <= r; x++) { - for (int z = -r; z <= r; z++) { - mantle.getChunk(i.getLocation().getChunk()).flag(MantleFlag.DISCOVERED, true); - } - } - } - } - - private void updateChunks() { - for (Player i : getEngine().getWorld().realWorld().getPlayers()) { - int r = 1; - - Chunk c = i.getLocation().getChunk(); - for (int x = -r; x <= r; x++) { - for (int z = -r; z <= r; z++) { - if (c.getWorld().isChunkLoaded(c.getX() + x, c.getZ() + z) && Chunks.isSafe(getEngine().getWorld().realWorld(), c.getX() + x, c.getZ() + z)) { - - if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) { - getEngine().updateChunk(c.getWorld().getChunkAt(c.getX() + x, c.getZ() + z)); - } - - if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { - Chunk cx = getEngine().getWorld().realWorld().getChunkAt(c.getX() + x, c.getZ() + z); - int finalX = c.getX() + x; - int finalZ = c.getZ() + z; - J.a(() -> getMantle().raiseFlag(finalX, finalZ, MantleFlag.INITIAL_SPAWNED_MARKER, - () -> { - J.a(() -> spawnIn(cx, true), RNG.r.i(5, 200)); - getSpawnersFromMarkers(cx).forEach((blockf, spawners) -> { - if (spawners.isEmpty()) { - return; - } - - IrisPosition block = new IrisPosition(blockf.getX(), blockf.getY() + getEngine().getWorld().minHeight(), blockf.getZ()); - IrisSpawner s = new KList<>(spawners).getRandom(); - spawn(block, s, true); - }); - })); - } - } - } - } - } - } - - private boolean onAsyncTick() { - if (getEngine().isClosed()) { - return false; - } - - actuallySpawned = 0; - - if (energy < 100) { - J.sleep(200); - return false; - } - - if (!getEngine().getWorld().hasRealWorld()) { - Iris.debug("Can't spawn. No real world"); - J.sleep(5000); - return false; - } - - double epx = getEntitySaturation(); - if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) { - Iris.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + entityCount + ")"); - J.sleep(5000); - return false; - } - - if (cl.flip()) { - try { - J.s(() -> precount = getEngine().getWorld().realWorld().getEntities()); - } catch (Throwable e) { - close(); - } - } - - int spawnBuffer = RNG.r.i(2, 12); - - Chunk[] cc = getEngine().getWorld().realWorld().getLoadedChunks(); - while (spawnBuffer-- > 0) { - if (cc.length == 0) { - Iris.debug("Can't spawn. No chunks!"); - return false; - } - - Chunk c = cc[RNG.r.nextInt(cc.length)]; - - if (!c.isLoaded() || !Chunks.isSafe(c.getWorld(), c.getX(), c.getZ())) { - continue; - } - - spawnIn(c, false); - } - - energy -= (actuallySpawned / 2D); - return actuallySpawned > 0; - } - - private void fixEnergy() { - energy = M.clip(energy, 1D, getDimension().getMaximumEnergy()); - } - - private void spawnIn(Chunk c, boolean initial) { - if (getEngine().isClosed()) { - return; - } - - if (initial) { - energy += 1.2; - } - - if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { - getSpawnersFromMarkers(c).forEach((blockf, spawners) -> { - if (spawners.isEmpty()) { - return; - } - - IrisPosition block = new IrisPosition(blockf.getX(), blockf.getY() + getEngine().getWorld().minHeight(), blockf.getZ()); - IrisSpawner s = new KList<>(spawners).getRandom(); - spawn(block, s, false); - J.a(() -> getMantle().raiseFlag(c.getX(), c.getZ(), MantleFlag.INITIAL_SPAWNED_MARKER, - () -> spawn(block, s, true))); - }); - } - - if (!IrisSettings.get().getWorld().isAnbientEntitySpawningSystem()) { - return; - } - - //@builder - Predicate filter = i -> i.canSpawn(getEngine(), c.getX(), c.getZ()); - ChunkCounter counter = new ChunkCounter(c.getEntities()); - - IrisBiome biome = getEngine().getSurfaceBiome(c); - IrisEntitySpawn v = spawnRandomly(Stream.concat(getData().getSpawnerLoader() - .loadAll(getDimension().getEntitySpawners()) - .shuffleCopy(RNG.r) - .stream() - .filter(filter) - .filter((i) -> i.isValid(biome)), - Stream.concat(getData() - .getSpawnerLoader() - .loadAll(getEngine().getRegion(c.getX() << 4, c.getZ() << 4).getEntitySpawners()) - .shuffleCopy(RNG.r) - .stream() - .filter(filter), - getData().getSpawnerLoader() - .loadAll(getEngine().getSurfaceBiome(c.getX() << 4, c.getZ() << 4).getEntitySpawners()) - .shuffleCopy(RNG.r) - .stream() - .filter(filter))) - .filter(counter) - .flatMap((i) -> stream(i, initial)) - .collect(Collectors.toList())) - .getRandom(); - //@done - if (v == null || v.getReferenceSpawner() == null) - return; - - try { - spawn(c, v); - } catch (Throwable e) { - J.s(() -> spawn(c, v)); - } - } - - private void spawn(Chunk c, IrisEntitySpawn i) { - IrisSpawner ref = i.getReferenceSpawner(); - int s = i.spawn(getEngine(), c, RNG.r); - actuallySpawned += s; - if (s > 0) { - ref.spawn(getEngine(), c.getX(), c.getZ()); - energy -= s * ((i.getEnergyMultiplier() * ref.getEnergyMultiplier() * 1)); - } - } - - private void spawn(IrisPosition pos, IrisEntitySpawn i) { - IrisSpawner ref = i.getReferenceSpawner(); - if (!ref.canSpawn(getEngine(), pos.getX() >> 4, pos.getZ() >> 4)) - return; - - int s = i.spawn(getEngine(), pos, RNG.r); - actuallySpawned += s; - if (s > 0) { - ref.spawn(getEngine(), pos.getX() >> 4, pos.getZ() >> 4); - energy -= s * ((i.getEnergyMultiplier() * ref.getEnergyMultiplier() * 1)); - } - } - - private Stream stream(IrisSpawner s, boolean initial) { - for (IrisEntitySpawn i : initial ? s.getInitialSpawns() : s.getSpawns()) { - i.setReferenceSpawner(s); - i.setReferenceMarker(s.getReferenceMarker()); - } - - return (initial ? s.getInitialSpawns() : s.getSpawns()).stream(); - } - - private KList spawnRandomly(List types) { - KList rarityTypes = new KList<>(); - int totalRarity = 0; - - for (IrisEntitySpawn i : types) { - totalRarity += IRare.get(i); - } - - for (IrisEntitySpawn i : types) { - rarityTypes.addMultiple(i, totalRarity / IRare.get(i)); - } - - return rarityTypes; - } - - @Override - public void onTick() { - - } - - @Override - public void onSave() { - getEngine().getMantle().save(); - } - - public void requestBiomeInject(Position2 p) { - injectBiomes.add(p); - } - - @Override - public void onChunkLoad(Chunk e, boolean generated) { - if (getEngine().isClosed()) { - return; - } - - int cX = e.getX(), cZ = e.getZ(); - Long key = Cache.key(e); - cleanup.put(key, cleanupService.schedule(() -> { - cleanup.remove(key); - energy += 0.3; - fixEnergy(); - getEngine().cleanupMantleChunk(cX, cZ); - }, Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS)); - - if (generated) { - //INMS.get().injectBiomesFromMantle(e, getMantle()); - - if (!IrisSettings.get().getGenerator().earlyCustomBlocks) return; - Iris.tickets.addTicket(e); - J.s(() -> { - var chunk = getMantle().getChunk(e).use(); - int minY = getTarget().getWorld().minHeight(); - try { - chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, () -> { - chunk.iterate(Identifier.class, (x, y, z, v) -> { - Iris.service(ExternalDataSVC.class).processUpdate(getEngine(), e.getBlock(x & 15, y + minY, z & 15), v); - }); - }); - } finally { - chunk.release(); - Iris.tickets.removeTicket(e); - } - }, RNG.r.i(20, 60)); - } - } - - @Override - public void onChunkUnload(Chunk e) { - final var future = cleanup.remove(Cache.key(e)); - if (future != null) { - future.cancel(false); - } - } - - private void spawn(IrisPosition block, IrisSpawner spawner, boolean initial) { - if (getEngine().isClosed()) { - return; - } - - if (spawner == null) { - return; - } - - KList s = initial ? spawner.getInitialSpawns() : spawner.getSpawns(); - if (s.isEmpty()) { - return; - } - - IrisEntitySpawn ss = spawnRandomly(s).getRandom(); - ss.setReferenceSpawner(spawner); - ss.setReferenceMarker(spawner.getReferenceMarker()); - spawn(block, ss); - } - - public Mantle getMantle() { - return getEngine().getMantle().getMantle(); - } - - @Override - public void chargeEnergy() { - charge = M.ms() + 3000; - } - - @Override - public void teleportAsync(PlayerTeleportEvent e) { - if (IrisSettings.get().getWorld().getAsyncTeleport().isEnabled()) { - e.setCancelled(true); - warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.s(() -> { - ignoreTP.set(true); - e.getPlayer().teleport(e.getTo(), e.getCause()); - ignoreTP.set(false); - })); - } - } - - private void warmupAreaAsync(Player player, Location to, Runnable r) { - J.a(() -> { - int viewDistance = IrisSettings.get().getWorld().getAsyncTeleport().getLoadViewDistance(); - KList> futures = new KList<>(); - for (int i = -viewDistance; i <= viewDistance; i++) { - for (int j = -viewDistance; j <= viewDistance; j++) { - int finalJ = j; - int finalI = i; - - if (to.getWorld().isChunkLoaded((to.getBlockX() >> 4) + i, (to.getBlockZ() >> 4) + j)) { - futures.add(CompletableFuture.completedFuture(null)); - continue; - } - - futures.add(MultiBurst.burst.completeValue(() - -> PaperLib.getChunkAtAsync(to.getWorld(), - (to.getBlockX() >> 4) + finalI, - (to.getBlockZ() >> 4) + finalJ, - true, IrisSettings.get().getWorld().getAsyncTeleport().isUrgent()).get())); - } - } - - new QueueJob>() { - @Override - public void execute(Future chunkFuture) { - try { - chunkFuture.get(); - } catch (InterruptedException | ExecutionException ignored) { - - } - } - - @Override - public String getName() { - return "Loading Chunks"; - } - }.queue(futures).execute(new VolmitSender(player), true, r); - }); - } - - public Map> getSpawnersFromMarkers(Chunk c) { - Map> p = new KMap<>(); - Set b = new KSet<>(); - getMantle().iterateChunk(c.getX(), c.getZ(), MatterMarker.class, (x, y, z, t) -> { - if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) { - return; - } - - IrisMarker mark = getData().getMarkerLoader().load(t.getTag()); - IrisPosition pos = new IrisPosition((c.getX() << 4) + x, y, (c.getZ() << 4) + z); - - if (mark.isEmptyAbove()) { - AtomicBoolean remove = new AtomicBoolean(false); - - try { - J.sfut(() -> { - if (c.getBlock(x, y + 1, z).getBlockData().getMaterial().isSolid() || c.getBlock(x, y + 2, z).getBlockData().getMaterial().isSolid()) { - remove.set(true); - } - }).get(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - - if (remove.get()) { - b.add(pos); - return; - } - } - - for (String i : mark.getSpawners()) { - IrisSpawner m = getData().getSpawnerLoader().load(i); - if (m == null) { - Iris.error("Cannot load spawner: " + i + " for marker on " + getName()); - continue; - } - m.setReferenceMarker(mark); - - // This is so fucking incorrect its a joke - //noinspection ConstantConditions - if (m != null) { - p.computeIfAbsent(pos, (k) -> new KSet<>()).add(m); - } - } - }); - - for (IrisPosition i : b) { - getEngine().getMantle().getMantle().remove(i.getX(), i.getY(), i.getZ(), MatterMarker.class); - } - - return p; - } - - @Override - public void onBlockBreak(BlockBreakEvent e) { - if (e.getBlock().getWorld().equals(getTarget().getWorld().realWorld())) { - J.a(() -> { - MatterMarker marker = getMantle().get(e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ(), MatterMarker.class); - - if (marker != null) { - if (marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) { - return; - } - - IrisMarker mark = getData().getMarkerLoader().load(marker.getTag()); - - if (mark == null || mark.isRemoveOnChange()) { - getMantle().remove(e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ(), MatterMarker.class); - } - } - }); - - KList d = new KList<>(); - IrisBiome b = getEngine().getBiome(e.getBlock().getLocation().clone().subtract(0, getEngine().getWorld().minHeight(), 0)); - List dropProviders = filterDrops(b.getBlockDrops(), e, getData()); - - if (dropProviders.stream().noneMatch(IrisBlockDrops::isSkipParents)) { - IrisRegion r = getEngine().getRegion(e.getBlock().getLocation()); - dropProviders.addAll(filterDrops(r.getBlockDrops(), e, getData())); - dropProviders.addAll(filterDrops(getEngine().getDimension().getBlockDrops(), e, getData())); - } - - dropProviders.forEach(provider -> provider.fillDrops(false, d)); - - if (dropProviders.stream().anyMatch(IrisBlockDrops::isReplaceVanillaDrops)) { - e.setDropItems(false); - } - - if (d.isNotEmpty()) { - World w = e.getBlock().getWorld(); - J.s(() -> d.forEach(item -> w.dropItemNaturally(e.getBlock().getLocation().clone().add(.5, .5, .5), item))); - } - } - } - - private List filterDrops(KList drops, BlockBreakEvent e, IrisData data) { - return new KList<>(drops.stream().filter(d -> d.shouldDropFor(e.getBlock().getBlockData(), data)).toList()); - } - - @Override - public void onBlockPlace(BlockPlaceEvent e) { - - } - - @Override - public void close() { - super.close(); - looper.interrupt(); - } - - @Override - public int getChunkCount() { - return getEngine().getWorld().realWorld().getLoadedChunks().length; - } - - @Override - public double getEntitySaturation() { - if (!getEngine().getWorld().hasRealWorld()) { - return 1; - } - - return (double) entityCount / (getEngine().getWorld().realWorld().getLoadedChunks().length + 1) * 1.28; - } - - @Data - private static class ChunkCounter implements Predicate { - private final Entity[] entities; - private transient int index = 0; - private transient int count = 0; - - @Override - public boolean test(IrisSpawner spawner) { - int max = spawner.getMaxEntitiesPerChunk(); - if (max <= count) - return false; - - while (index < entities.length) { - if (entities[index++] instanceof LivingEntity) { - if (++count >= max) - return false; - } - } - - return true; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/actuator/IrisBiomeActuator.java b/core/src/main/java/com/volmit/iris/engine/actuator/IrisBiomeActuator.java deleted file mode 100644 index 584609e77..000000000 --- a/core/src/main/java/com/volmit/iris/engine/actuator/IrisBiomeActuator.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.actuator; - -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedActuator; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisBiomeCustom; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterBiomeInject; -import com.volmit.iris.util.matter.slices.BiomeInjectMatter; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.bukkit.block.Biome; - -public class IrisBiomeActuator extends EngineAssignedActuator { - private final RNG rng; - private final ChronoLatch cl = new ChronoLatch(5000); - - public IrisBiomeActuator(Engine engine) { - super(engine, "Biome"); - rng = new RNG(engine.getSeedManager().getBiome()); - } - - @BlockCoordinates - @Override - public void onActuate(int x, int z, Hunk h, boolean multicore, ChunkContext context) { - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - for (int xf = 0; xf < h.getWidth(); xf++) { - IrisBiome ib; - for (int zf = 0; zf < h.getDepth(); zf++) { - ib = context.getBiome().get(xf, zf); - MatterBiomeInject matter; - - if (ib.isCustom()) { - IrisBiomeCustom custom = ib.getCustomBiome(rng, x, 0, z); - matter = BiomeInjectMatter.get(INMS.get().getBiomeBaseIdForKey(getDimension().getLoadKey() + ":" + custom.getId())); - } else { - Biome v = ib.getSkyBiome(rng, x, 0, z); - matter = BiomeInjectMatter.get(v); - } - - getEngine().getMantle().getMantle().set(x + xf, 0, z + zf, matter); - } - } - getEngine().getMetrics().getBiome().put(p.getMilliseconds()); - } catch (Throwable e) { - e.printStackTrace(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/actuator/IrisDecorantActuator.java b/core/src/main/java/com/volmit/iris/engine/actuator/IrisDecorantActuator.java deleted file mode 100644 index c53a70a6d..000000000 --- a/core/src/main/java/com/volmit/iris/engine/actuator/IrisDecorantActuator.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.actuator; - -import com.volmit.iris.engine.decorator.*; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedActuator; -import com.volmit.iris.engine.framework.EngineDecorator; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import lombok.Getter; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; - -import java.util.function.Predicate; - -public class IrisDecorantActuator extends EngineAssignedActuator { - private static final Predicate PREDICATE_SOLID = (b) -> b != null && !b.getMaterial().isAir() && !b.getMaterial().equals(Material.WATER) && !b.getMaterial().equals(Material.LAVA); - private final RNG rng; - @Getter - private final EngineDecorator surfaceDecorator; - @Getter - private final EngineDecorator ceilingDecorator; - @Getter - private final EngineDecorator seaSurfaceDecorator; - @Getter - private final EngineDecorator seaFloorDecorator; - @Getter - private final EngineDecorator shoreLineDecorator; - private final boolean shouldRay; - - public IrisDecorantActuator(Engine engine) { - super(engine, "Decorant"); - shouldRay = shouldRayDecorate(); - this.rng = new RNG(engine.getSeedManager().getDecorator()); - surfaceDecorator = new IrisSurfaceDecorator(getEngine()); - ceilingDecorator = new IrisCeilingDecorator(getEngine()); - seaSurfaceDecorator = new IrisSeaSurfaceDecorator(getEngine()); - shoreLineDecorator = new IrisShoreLineDecorator(getEngine()); - seaFloorDecorator = new IrisSeaFloorDecorator(getEngine()); - } - - @BlockCoordinates - @Override - public void onActuate(int x, int z, Hunk output, boolean multicore, ChunkContext context) { - if (!getEngine().getDimension().isDecorate()) { - return; - } - - PrecisionStopwatch p = PrecisionStopwatch.start(); - - for (int i = 0; i < output.getWidth(); i++) { - int height; - int realX = Math.round(x + i); - int realZ; - IrisBiome biome, cave; - for (int j = 0; j < output.getDepth(); j++) { - boolean solid; - int emptyFor = 0; - int lastSolid = 0; - realZ = Math.round(z + j); - height = (int) Math.round(context.getHeight().get(i, j)); - biome = context.getBiome().get(i, j); - cave = shouldRay ? context.getCave().get(i, j) : null; - - if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) { - continue; - } - - if (height < getDimension().getFluidHeight()) { - getSeaSurfaceDecorator().decorate(i, j, - realX, Math.round(i + 1), Math.round(x + i - 1), - realZ, Math.round(z + j + 1), Math.round(z + j - 1), - output, biome, getDimension().getFluidHeight(), getEngine().getHeight()); - getSeaFloorDecorator().decorate(i, j, - realX, realZ, output, biome, height + 1, - getDimension().getFluidHeight() + 1); - } - - if (height == getDimension().getFluidHeight()) { - getShoreLineDecorator().decorate(i, j, - realX, Math.round(x + i + 1), Math.round(x + i - 1), - realZ, Math.round(z + j + 1), Math.round(z + j - 1), - output, biome, height, getEngine().getHeight()); - } - - getSurfaceDecorator().decorate(i, j, realX, realZ, output, biome, height, getEngine().getHeight() - height); - - - if (cave != null && cave.getDecorators().isNotEmpty()) { - for (int k = height; k > 0; k--) { - solid = PREDICATE_SOLID.test(output.get(i, k, j)); - - if (solid) { - if (emptyFor > 0) { - getSurfaceDecorator().decorate(i, j, realX, realZ, output, cave, k, lastSolid); - getCeilingDecorator().decorate(i, j, realX, realZ, output, cave, lastSolid - 1, emptyFor); - emptyFor = 0; - } - lastSolid = k; - } else { - emptyFor++; - } - } - } - } - } - - getEngine().getMetrics().getDecoration().put(p.getMilliseconds()); - - } - - private boolean shouldRayDecorate() { - return false; // TODO CAVES - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/actuator/IrisTerrainNormalActuator.java b/core/src/main/java/com/volmit/iris/engine/actuator/IrisTerrainNormalActuator.java deleted file mode 100644 index 646ca0773..000000000 --- a/core/src/main/java/com/volmit/iris/engine/actuator/IrisTerrainNormalActuator.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.actuator; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedActuator; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import lombok.Getter; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; - -public class IrisTerrainNormalActuator extends EngineAssignedActuator { - private static final BlockData AIR = Material.AIR.createBlockData(); - private static final BlockData BEDROCK = Material.BEDROCK.createBlockData(); - private static final BlockData LAVA = Material.LAVA.createBlockData(); - private static final BlockData GLASS = Material.GLASS.createBlockData(); - private static final BlockData CAVE_AIR = Material.CAVE_AIR.createBlockData(); - @Getter - private final RNG rng; - @Getter - private int lastBedrock = -1; - - public IrisTerrainNormalActuator(Engine engine) { - super(engine, "Terrain"); - rng = new RNG(engine.getSeedManager().getTerrain()); - } - - @BlockCoordinates - @Override - public void onActuate(int x, int z, Hunk h, boolean multicore, ChunkContext context) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - - for (int xf = 0; xf < h.getWidth(); xf++) { - terrainSliver(x, z, xf, h, context); - } - - getEngine().getMetrics().getTerrain().put(p.getMilliseconds()); - } - - private int fluidOrHeight(int height) { - return Math.max(getDimension().getFluidHeight(), height); - } - - /** - * This is calling 1/16th of a chunk x/z slice. It is a plane from sky to bedrock 1 thick in the x direction. - * - * @param x the chunk x in blocks - * @param z the chunk z in blocks - * @param xf the current x slice - * @param h the blockdata - */ - @BlockCoordinates - public void terrainSliver(int x, int z, int xf, Hunk h, ChunkContext context) { - int zf, realX, realZ, hf, he; - IrisBiome biome; - IrisRegion region; - - for (zf = 0; zf < h.getDepth(); zf++) { - realX = xf + x; - realZ = zf + z; - biome = context.getBiome().get(xf, zf); - region = context.getRegion().get(xf, zf); - he = (int) Math.round(Math.min(h.getHeight(), context.getHeight().get(xf, zf))); - hf = Math.round(Math.max(Math.min(h.getHeight(), getDimension().getFluidHeight()), he)); - - if (hf < 0) { - continue; - } - - KList blocks = null; - KList fblocks = null; - int depth, fdepth; - for (int i = hf; i >= 0; i--) { - if (i >= h.getHeight()) { - continue; - } - - if (i == 0) { - if (getDimension().isBedrock()) { - h.set(xf, i, zf, BEDROCK); - lastBedrock = i; - continue; - } - } - - BlockData ore = biome.generateOres(realX, i, realZ, rng, getData(), true); - ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), true) : ore; - ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), true) : ore; - if (ore != null) { - h.set(xf, i, zf, ore); - continue; - } - - if (i > he && i <= hf) { - fdepth = hf - i; - - if (fblocks == null) { - fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, getData()); - } - - if (fblocks.hasIndex(fdepth)) { - h.set(xf, i, zf, fblocks.get(fdepth)); - continue; - } - - h.set(xf, i, zf, context.getFluid().get(xf, zf)); - continue; - } - - if (i <= he) { - depth = he - i; - if (blocks == null) { - blocks = biome.generateLayers(getDimension(), realX, realZ, rng, - he, - he, - getData(), - getComplex()); - } - - - if (blocks.hasIndex(depth)) { - h.set(xf, i, zf, blocks.get(depth)); - continue; - } - - ore = biome.generateOres(realX, i, realZ, rng, getData(), false); - ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), false) : ore; - ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), false) : ore; - - if (ore != null) { - h.set(xf, i, zf, ore); - } else { - h.set(xf, i, zf, context.getRock().get(xf, zf)); - } - } - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/cache/AtomicCache.java b/core/src/main/java/com/volmit/iris/engine/data/cache/AtomicCache.java deleted file mode 100644 index 4757bcfb6..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/cache/AtomicCache.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.cache; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.function.NastySupplier; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Supplier; - -public class AtomicCache { - private transient final AtomicReference t; - private transient final AtomicBoolean set; - private transient final ReentrantLock lock; - private transient final boolean nullSupport; - - public AtomicCache() { - this(false); - } - - public AtomicCache(boolean nullSupport) { - set = nullSupport ? new AtomicBoolean() : null; - t = new AtomicReference<>(); - lock = new ReentrantLock(); - this.nullSupport = nullSupport; - } - - public void reset() { - t.set(null); - - if (nullSupport) { - set.set(false); - } - } - - public T aquireNasty(NastySupplier t) { - return aquire(() -> { - try { - return t.get(); - } catch (Throwable e) { - return null; - } - }); - } - - public T aquireNastyPrint(NastySupplier t) { - return aquire(() -> { - try { - return t.get(); - } catch (Throwable e) { - e.printStackTrace(); - return null; - } - }); - } - - public T aquire(Supplier t) { - if (this.t.get() != null) { - return this.t.get(); - } else if (nullSupport && set.get()) { - return null; - } - - lock.lock(); - - if (this.t.get() != null) { - lock.unlock(); - return this.t.get(); - } else if (nullSupport && set.get()) { - lock.unlock(); - return null; - } - - try { - this.t.set(t.get()); - - if (nullSupport) { - set.set(true); - } - } catch (Throwable e) { - Iris.error("Atomic cache failure!"); - e.printStackTrace(); - } - - lock.unlock(); - - return this.t.get(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/cache/Cache.java b/core/src/main/java/com/volmit/iris/engine/data/cache/Cache.java deleted file mode 100644 index 9e36ef704..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/cache/Cache.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.cache; - -import org.bukkit.Chunk; - -public interface Cache { - static long key(Chunk chunk) { - return key(chunk.getX(), chunk.getZ()); - } - - static long key(int x, int z) { - return (((long) x) << 32) | (z & 0xffffffffL); - } - - static int keyX(long key) { - return (int) (key >> 32); - } - - static int keyZ(long key) { - return (int) key; - } - - static int to1D(int x, int y, int z, int w, int h) { - return (z * w * h) + (y * w) + x; - } - - static int[] to3D(int idx, int w, int h) { - final int z = idx / (w * h); - idx -= (z * w * h); - final int y = idx / w; - final int x = idx % w; - return new int[]{x, y, z}; - } - - int getId(); - - V get(int x, int z); -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/cache/Multicache.java b/core/src/main/java/com/volmit/iris/engine/data/cache/Multicache.java deleted file mode 100644 index e81b63ac4..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/cache/Multicache.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.cache; - -public interface Multicache { - Cache getCache(int id); - - Cache createCache(); -} - \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/engine/data/chunk/LinkedTerrainChunk.java b/core/src/main/java/com/volmit/iris/engine/data/chunk/LinkedTerrainChunk.java deleted file mode 100644 index b726610cb..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/chunk/LinkedTerrainChunk.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.chunk; - -import com.volmit.iris.core.nms.BiomeBaseInjector; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.util.data.IrisBiomeStorage; -import com.volmit.iris.util.data.IrisCustomData; -import lombok.Setter; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; -import org.bukkit.generator.ChunkGenerator.BiomeGrid; -import org.bukkit.generator.ChunkGenerator.ChunkData; -import org.bukkit.material.MaterialData; - -@SuppressWarnings("deprecation") -public class LinkedTerrainChunk implements TerrainChunk { - private final IrisBiomeStorage biome3D; - private final BiomeGrid storage; - private ChunkData rawChunkData; - @Setter - private boolean unsafe = true; - - public LinkedTerrainChunk(World world) { - this(null, Bukkit.createChunkData(world)); - } - - public LinkedTerrainChunk(World world, BiomeGrid storage) { - this(storage, Bukkit.createChunkData(world)); - } - - public LinkedTerrainChunk(BiomeGrid storage, ChunkData data) { - this.storage = storage; - rawChunkData = data; - biome3D = storage != null ? null : new IrisBiomeStorage(); - } - - @Override - public BiomeBaseInjector getBiomeBaseInjector() { - - if (unsafe) { - return (a, b, c, d) -> { - }; - } - - return (x, y, z, bb) -> INMS.get().forceBiomeInto(x, y, z, bb, storage); - } - - - @Override - public Biome getBiome(int x, int z) { - if (storage != null) { - return storage.getBiome(x, z); - } - - return biome3D.getBiome(x, 0, z); - } - - - @Override - public Biome getBiome(int x, int y, int z) { - if (storage != null) { - return storage.getBiome(x, y, z); - } - - return biome3D.getBiome(x, y, z); - } - - @Override - public void setBiome(int x, int z, Biome bio) { - if (storage != null) { - storage.setBiome(x, z, bio); - return; - } - - biome3D.setBiome(x, 0, z, bio); - } - - public BiomeGrid getRawBiome() { - return storage; - } - - @Override - public void setBiome(int x, int y, int z, Biome bio) { - if (storage != null) { - storage.setBiome(x, y, z, bio); - return; - } - - biome3D.setBiome(x, y, z, bio); - } - - @Override - public int getMinHeight() { - return rawChunkData.getMinHeight(); - } - - @Override - public int getMaxHeight() { - return rawChunkData.getMaxHeight(); - } - - @Override - public synchronized void setBlock(int x, int y, int z, BlockData blockData) { - if (blockData instanceof IrisCustomData d) - blockData = d.getBase(); - rawChunkData.setBlock(x, y, z, blockData); - } - - - @Override - public BlockData getBlockData(int x, int y, int z) { - return rawChunkData.getBlockData(x, y, z); - } - - @Deprecated - @Override - public synchronized void setBlock(int x, int y, int z, Material material) { - rawChunkData.setBlock(x, y, z, material); - } - - @Deprecated - @Override - public synchronized void setBlock(int x, int y, int z, MaterialData material) { - rawChunkData.setBlock(x, y, z, material); - } - - @Deprecated - @Override - public synchronized void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) { - rawChunkData.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material); - } - - @Deprecated - @Override - public synchronized void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) { - rawChunkData.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material); - } - - @Override - public synchronized void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) { - rawChunkData.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, blockData); - } - - - @Deprecated - @Override - public synchronized Material getType(int x, int y, int z) { - return rawChunkData.getType(x, y, z); - } - - - @Deprecated - @Override - public MaterialData getTypeAndData(int x, int y, int z) { - return rawChunkData.getTypeAndData(x, y, z); - } - - @Deprecated - @Override - public byte getData(int x, int y, int z) { - return rawChunkData.getData(x, y, z); - } - - @Override - public ChunkData getRaw() { - return rawChunkData; - } - - @Override - public void setRaw(ChunkData data) { - rawChunkData = data; - } - - @Override - public void inject(BiomeGrid biome) { - if (biome3D != null) { - biome3D.inject(biome); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/chunk/MCATerrainChunk.java b/core/src/main/java/com/volmit/iris/engine/data/chunk/MCATerrainChunk.java deleted file mode 100644 index d4c1cbf8d..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/chunk/MCATerrainChunk.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.chunk; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.nms.BiomeBaseInjector; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.nbt.mca.Chunk; -import com.volmit.iris.util.nbt.mca.NBTWorld; -import lombok.AllArgsConstructor; -import lombok.Builder; -import org.bukkit.Material; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; -import org.bukkit.generator.ChunkGenerator; -import org.bukkit.material.MaterialData; - -@Builder -@AllArgsConstructor -public class MCATerrainChunk implements TerrainChunk { - private final NBTWorld writer; - private final BiomeBaseInjector injector; - private final int ox; - private final int oz; - private final int minHeight; - private final int maxHeight; - private final Chunk mcaChunk; - - @Override - public BiomeBaseInjector getBiomeBaseInjector() { - return injector; - } - - @Override - public Biome getBiome(int x, int z) { - return Biome.THE_VOID; - } - - @Override - public Biome getBiome(int x, int y, int z) { - return Biome.THE_VOID; - } - - @Override - public void setBiome(int x, int z, Biome bio) { - setBiome(ox + x, 0, oz + z, bio); - } - - @Override - public void setBiome(int x, int y, int z, Biome bio) { - mcaChunk.setBiomeAt((ox + x) & 15, y, (oz + z) & 15, writer.getBiomeId(bio)); - } - - @Override - public int getMinHeight() { - return minHeight; - } - - @Override - public int getMaxHeight() { - return maxHeight; - } - - @Override - public void setBlock(int x, int y, int z, BlockData blockData) { - int xx = (x + ox) & 15; - int zz = (z + oz) & 15; - - if (y > getMaxHeight() || y < getMinHeight()) { - return; - } - - if (blockData == null) { - Iris.error("NULL BD"); - } - if (blockData instanceof IrisCustomData data) - blockData = data.getBase(); - - mcaChunk.setBlockStateAt(xx, y, zz, NBTWorld.getCompound(blockData), false); - } - - @Override - public org.bukkit.block.data.BlockData getBlockData(int x, int y, int z) { - if (y > getMaxHeight()) { - y = getMaxHeight(); - } - - if (y < getMinHeight()) { - y = getMinHeight(); - } - - return NBTWorld.getBlockData(mcaChunk.getBlockStateAt((x + ox) & 15, y, (z + oz) & 15)); - } - - @Override - public ChunkGenerator.ChunkData getRaw() { - return null; - } - - @Override - public void setRaw(ChunkGenerator.ChunkData data) { - - } - - @Override - public void inject(ChunkGenerator.BiomeGrid biome) { - - } - - @Override - public void setBlock(int x, int y, int z, Material material) { - - } - - @Override - public void setBlock(int x, int y, int z, MaterialData material) { - - } - - @Override - public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) { - - } - - @Override - public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) { - - } - - @Override - public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) { - - } - - - @Override - public Material getType(int x, int y, int z) { - return null; - } - - - @Override - public MaterialData getTypeAndData(int x, int y, int z) { - return null; - } - - @Override - public byte getData(int x, int y, int z) { - return 0; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/chunk/TerrainChunk.java b/core/src/main/java/com/volmit/iris/engine/data/chunk/TerrainChunk.java deleted file mode 100644 index 16c93da85..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/chunk/TerrainChunk.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.chunk; - -import com.volmit.iris.core.nms.BiomeBaseInjector; -import org.bukkit.World; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; -import org.bukkit.generator.ChunkGenerator.BiomeGrid; -import org.bukkit.generator.ChunkGenerator.ChunkData; - -public interface TerrainChunk extends BiomeGrid, ChunkData { - static TerrainChunk create(World world) { - return new LinkedTerrainChunk(world); - } - - static TerrainChunk create(World world, BiomeGrid grid) { - return new LinkedTerrainChunk(world, grid); - } - - static TerrainChunk createUnsafe(World world, BiomeGrid grid) { - LinkedTerrainChunk ltc = new LinkedTerrainChunk(world, grid); - ltc.setUnsafe(true); - return ltc; - } - - static TerrainChunk create(ChunkData raw, BiomeGrid grid) { - return new LinkedTerrainChunk(grid, raw); - } - - BiomeBaseInjector getBiomeBaseInjector(); - - /** - * Get biome at x, z within chunk being generated - * - * @param x - 0-15 - * @param z - 0-15 - * @return Biome value - * @deprecated biomes are now 3-dimensional - */ - - @Deprecated - Biome getBiome(int x, int z); - - /** - * Get biome at x, z within chunk being generated - * - * @param x - 0-15 - * @param y - 0-255 - * @param z - 0-15 - * @return Biome value - */ - Biome getBiome(int x, int y, int z); - - /** - * Set biome at x, z within chunk being generated - * - * @param x - 0-15 - * @param z - 0-15 - * @param bio - Biome value - * @deprecated biomes are now 3-dimensional - */ - @Deprecated - void setBiome(int x, int z, Biome bio); - - /** - * Set biome at x, z within chunk being generated - * - * @param x - 0-15 - * @param y - 0-255 - * @param z - 0-15 - * @param bio - Biome value - */ - void setBiome(int x, int y, int z, Biome bio); - - /** - * Get the maximum height for the chunk. - *

- * Setting blocks at or above this height will do nothing. - * - * @return the maximum height - */ - int getMaxHeight(); - - /** - * Set the block at x,y,z in the chunk data to material. - *

- * Setting blocks outside the chunk's bounds does nothing. - * - * @param x the x location in the chunk from 0-15 inclusive - * @param y the y location in the chunk from 0 (inclusive) - maxHeight - * (exclusive) - * @param z the z location in the chunk from 0-15 inclusive - * @param blockData the type to set the block to - */ - void setBlock(int x, int y, int z, BlockData blockData); - - /** - * Get the type and data of the block at x, y, z. - *

- * Getting blocks outside the chunk's bounds returns air. - * - * @param x the x location in the chunk from 0-15 inclusive - * @param y the y location in the chunk from 0 (inclusive) - maxHeight - * (exclusive) - * @param z the z location in the chunk from 0-15 inclusive - * @return the data of the block or the BlockData for air if x, y or z are - * outside the chunk's bounds - */ - BlockData getBlockData(int x, int y, int z); - - ChunkData getRaw(); - - void setRaw(ChunkData data); - - void inject(BiomeGrid biome); -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/io/Deserializer.java b/core/src/main/java/com/volmit/iris/engine/data/io/Deserializer.java deleted file mode 100644 index 3b2dfbd7b..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/io/Deserializer.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.io; - -import java.io.*; -import java.net.URL; - -public interface Deserializer { - - T fromStream(InputStream stream) throws IOException; - - default T fromFile(File file) throws IOException { - try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { - return fromStream(bis); - } - } - - default T fromBytes(byte[] data) throws IOException { - ByteArrayInputStream stream = new ByteArrayInputStream(data); - return fromStream(stream); - } - - default T fromResource(Class clazz, String path) throws IOException { - try (InputStream stream = clazz.getClassLoader().getResourceAsStream(path)) { - if (stream == null) { - throw new IOException("resource \"" + path + "\" not found"); - } - return fromStream(stream); - } - } - - default T fromURL(URL url) throws IOException { - try (InputStream stream = url.openStream()) { - return fromStream(stream); - } - } - - -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/io/ExceptionBiFunction.java b/core/src/main/java/com/volmit/iris/engine/data/io/ExceptionBiFunction.java deleted file mode 100644 index 1ac520d59..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/io/ExceptionBiFunction.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.io; - -@FunctionalInterface -public interface ExceptionBiFunction { - - R accept(T t, U u) throws E; -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/io/ExceptionTriConsumer.java b/core/src/main/java/com/volmit/iris/engine/data/io/ExceptionTriConsumer.java deleted file mode 100644 index 047fde648..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/io/ExceptionTriConsumer.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.io; - -@FunctionalInterface -public interface ExceptionTriConsumer { - - void accept(T t, U u, V v) throws E; -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/io/MaxDepthIO.java b/core/src/main/java/com/volmit/iris/engine/data/io/MaxDepthIO.java deleted file mode 100644 index 5ca04f4bd..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/io/MaxDepthIO.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.io; - -public interface MaxDepthIO { - - default int decrementMaxDepth(int maxDepth) { - if (maxDepth < 0) { - throw new IllegalArgumentException("negative maximum depth is not allowed"); - } else if (maxDepth == 0) { - throw new MaxDepthReachedException("reached maximum depth of NBT structure"); - } - return --maxDepth; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/io/MaxDepthReachedException.java b/core/src/main/java/com/volmit/iris/engine/data/io/MaxDepthReachedException.java deleted file mode 100644 index c7a1a8d34..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/io/MaxDepthReachedException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.io; - -/** - * Exception indicating that the maximum (de-)serialization depth has been reached. - */ -@SuppressWarnings("serial") -public class MaxDepthReachedException extends RuntimeException { - - public MaxDepthReachedException(String msg) { - super(msg); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/io/Serializer.java b/core/src/main/java/com/volmit/iris/engine/data/io/Serializer.java deleted file mode 100644 index b6e248008..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/io/Serializer.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.io; - -import java.io.*; - -public interface Serializer { - - void toStream(T object, OutputStream out) throws IOException; - - default void toFile(T object, File file) throws IOException { - try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) { - toStream(object, bos); - } - } - - default byte[] toBytes(T object) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - toStream(object, bos); - bos.close(); - return bos.toByteArray(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/io/StringDeserializer.java b/core/src/main/java/com/volmit/iris/engine/data/io/StringDeserializer.java deleted file mode 100644 index 1cb4240a3..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/io/StringDeserializer.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.io; - -import java.io.*; - -public interface StringDeserializer extends Deserializer { - - T fromReader(Reader reader) throws IOException; - - default T fromString(String s) throws IOException { - return fromReader(new StringReader(s)); - } - - @Override - default T fromStream(InputStream stream) throws IOException { - try (Reader reader = new InputStreamReader(stream)) { - return fromReader(reader); - } - } - - @Override - default T fromFile(File file) throws IOException { - try (Reader reader = new FileReader(file)) { - return fromReader(reader); - } - } - - @Override - default T fromBytes(byte[] data) throws IOException { - return fromReader(new StringReader(new String(data))); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/data/io/StringSerializer.java b/core/src/main/java/com/volmit/iris/engine/data/io/StringSerializer.java deleted file mode 100644 index 186b56ad0..000000000 --- a/core/src/main/java/com/volmit/iris/engine/data/io/StringSerializer.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.data.io; - -import java.io.*; - -public interface StringSerializer extends Serializer { - - void toWriter(T object, Writer writer) throws IOException; - - default String toString(T object) throws IOException { - Writer writer = new StringWriter(); - toWriter(object, writer); - writer.flush(); - return writer.toString(); - } - - @Override - default void toStream(T object, OutputStream stream) throws IOException { - Writer writer = new OutputStreamWriter(stream); - toWriter(object, writer); - writer.flush(); - } - - @Override - default void toFile(T object, File file) throws IOException { - try (Writer writer = new FileWriter(file)) { - toWriter(object, writer); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/decorator/IrisCeilingDecorator.java b/core/src/main/java/com/volmit/iris/engine/decorator/IrisCeilingDecorator.java deleted file mode 100644 index 134e93371..000000000 --- a/core/src/main/java/com/volmit/iris/engine/decorator/IrisCeilingDecorator.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.decorator; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisDecorationPart; -import com.volmit.iris.engine.object.IrisDecorator; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import org.bukkit.Material; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.PointedDripstone; - -public class IrisCeilingDecorator extends IrisEngineDecorator { - public IrisCeilingDecorator(Engine engine) { - super(engine, "Ceiling", IrisDecorationPart.CEILING); - } - - @BlockCoordinates - @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { - RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); - - if (decorator != null) { - if (!decorator.isStacking()) { - data.set(x, height, z, fixFaces(decorator.getBlockData100(biome, rng, realX, height, realZ, getData()), data, x, z, realX, height, realZ)); - } else { - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack()); - } else { - stack = Math.min(max, stack); - } - - if (stack == 1) { - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - for (int i = 0; i < stack; i++) { - int h = height - i; - if (h < getEngine().getMinHeight()) { - continue; - } - - double threshold = (((double) i) / (double) (stack - 1)); - - BlockData bd = threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData()); - - if (bd instanceof PointedDripstone) { - PointedDripstone.Thickness th = PointedDripstone.Thickness.BASE; - - if (stack == 2) { - th = PointedDripstone.Thickness.FRUSTUM; - - if (i == stack - 1) { - th = PointedDripstone.Thickness.TIP; - } - } else { - if (i == stack - 1) { - th = PointedDripstone.Thickness.TIP; - } else if (i == stack - 2) { - th = PointedDripstone.Thickness.FRUSTUM; - } - } - - - bd = Material.POINTED_DRIPSTONE.createBlockData(); - ((PointedDripstone) bd).setThickness(th); - ((PointedDripstone) bd).setVerticalDirection(BlockFace.DOWN); - } - - data.set(x, h, z, bd); - } - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/decorator/IrisEngineDecorator.java b/core/src/main/java/com/volmit/iris/engine/decorator/IrisEngineDecorator.java deleted file mode 100644 index 68660c955..000000000 --- a/core/src/main/java/com/volmit/iris/engine/decorator/IrisEngineDecorator.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.decorator; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedComponent; -import com.volmit.iris.engine.framework.EngineDecorator; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisDecorationPart; -import com.volmit.iris.engine.object.IrisDecorator; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.math.RNG; -import lombok.Getter; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockSupport; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.MultipleFacing; - -public abstract class IrisEngineDecorator extends EngineAssignedComponent implements EngineDecorator { - @Getter - private final IrisDecorationPart part; - private final long seed; - private final long modX, modZ; - - public IrisEngineDecorator(Engine engine, String name, IrisDecorationPart part) { - super(engine, name + " Decorator"); - this.part = part; - this.seed = getSeed() + 29356788 - (part.ordinal() * 10439677L); - this.modX = 29356788 ^ (part.ordinal() + 6); - this.modZ = 10439677 ^ (part.ordinal() + 1); - } - - @BlockCoordinates - protected RNG getRNG(int x, int z) { - return new RNG(x * modX + z * modZ + seed); - } - - protected IrisDecorator getDecorator(RNG rng, IrisBiome biome, double realX, double realZ) { - KList v = new KList<>(); - - RNG gRNG = new RNG(seed); - for (IrisDecorator i : biome.getDecorators()) { - try { - if (i.getPartOf().equals(part) && i.getBlockData(biome, gRNG, realX, realZ, getData()) != null) { - v.add(i); - } - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("PART OF: " + biome.getLoadFile().getAbsolutePath() + " HAS AN INVALID DECORATOR near 'partOf'!!!"); - } - } - - if (v.isNotEmpty()) { - return v.get(rng.nextInt(v.size())); - } - - return null; - } - - protected BlockData fixFaces(BlockData b, Hunk hunk, int rX, int rZ, int x, int y, int z) { - if (B.isVineBlock(b)) { - MultipleFacing data = (MultipleFacing) b.clone(); - data.getFaces().forEach(f -> data.setFace(f, false)); - - boolean found = false; - for (BlockFace f : BlockFace.values()) { - if (!f.isCartesian()) - continue; - int yy = y + f.getModY(); - - BlockData r = getEngine().getMantle().get(x + f.getModX(), yy, z + f.getModZ()); - if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { - found = true; - data.setFace(f, true); - continue; - } - - int xx = rX + f.getModX(); - int zz = rZ + f.getModZ(); - if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) - continue; - - r = hunk.get(xx, yy, zz); - if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) { - found = true; - data.setFace(f, true); - } - } - if (!found) - data.setFace(BlockFace.DOWN, true); - return data; - } - return b; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/decorator/IrisSeaFloorDecorator.java b/core/src/main/java/com/volmit/iris/engine/decorator/IrisSeaFloorDecorator.java deleted file mode 100644 index 611634310..000000000 --- a/core/src/main/java/com/volmit/iris/engine/decorator/IrisSeaFloorDecorator.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.decorator; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisDecorationPart; -import com.volmit.iris.engine.object.IrisDecorator; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import org.bukkit.block.data.BlockData; - -public class IrisSeaFloorDecorator extends IrisEngineDecorator { - public IrisSeaFloorDecorator(Engine engine) { - super(engine, "Sea Floor", IrisDecorationPart.SEA_FLOOR); - } - - @BlockCoordinates - @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { - RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); - - if (decorator != null) { - if (!decorator.isStacking()) { - if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() - && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { - return; - } - if (height >= 0 || height < getEngine().getHeight()) { - data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); - } - } else { - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - int maxStack = max - height; - stack = (int) Math.ceil((double) maxStack * ((double) stack / 100)); - } else stack = Math.min(stack, max - height); - - if (stack == 1) { - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - for (int i = 0; i < stack; i++) { - int h = height + i; - if (h > max || h > getEngine().getHeight()) { - continue; - } - - double threshold = ((double) i) / (stack - 1); - data.set(x, h, z, threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); - } - } - } - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/decorator/IrisSeaSurfaceDecorator.java b/core/src/main/java/com/volmit/iris/engine/decorator/IrisSeaSurfaceDecorator.java deleted file mode 100644 index 0096d75d2..000000000 --- a/core/src/main/java/com/volmit/iris/engine/decorator/IrisSeaSurfaceDecorator.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.decorator; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisDecorationPart; -import com.volmit.iris.engine.object.IrisDecorator; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import org.bukkit.block.data.BlockData; - -public class IrisSeaSurfaceDecorator extends IrisEngineDecorator { - public IrisSeaSurfaceDecorator(Engine engine) { - super(engine, "Sea Surface", IrisDecorationPart.SEA_SURFACE); - } - - @BlockCoordinates - @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { - RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); - - if (decorator != null) { - if (!decorator.isStacking()) { - if (height >= 0 || height < getEngine().getHeight()) { - data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); - } - } else { - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - int maxStack = max - height; - stack = (int) Math.ceil((double) maxStack * ((double) stack / 100)); - } - - if (stack == 1) { - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - for (int i = 0; i < stack; i++) { - int h = height + i; - if (h >= max || h >= getEngine().getHeight()) { - continue; - } - - double threshold = ((double) i) / (stack - 1); - data.set(x, h + 1, z, threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); - } - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/decorator/IrisShoreLineDecorator.java b/core/src/main/java/com/volmit/iris/engine/decorator/IrisShoreLineDecorator.java deleted file mode 100644 index dfeaad7fc..000000000 --- a/core/src/main/java/com/volmit/iris/engine/decorator/IrisShoreLineDecorator.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.decorator; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisDecorationPart; -import com.volmit.iris.engine.object.IrisDecorator; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import org.bukkit.block.data.BlockData; - -public class IrisShoreLineDecorator extends IrisEngineDecorator { - public IrisShoreLineDecorator(Engine engine) { - super(engine, "Shore Line", IrisDecorationPart.SHORE_LINE); - } - - @BlockCoordinates - @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { - - if (height == getDimension().getFluidHeight()) { - if (Math.round(getComplex().getHeightStream().get(realX1, realZ)) < getComplex().getFluidHeight() || - Math.round(getComplex().getHeightStream().get(realX_1, realZ)) < getComplex().getFluidHeight() || - Math.round(getComplex().getHeightStream().get(realX, realZ1)) < getComplex().getFluidHeight() || - Math.round(getComplex().getHeightStream().get(realX, realZ_1)) < getComplex().getFluidHeight() - ) { - RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); - - if (decorator != null) { - if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() - && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { - return; - } - - if (!decorator.isStacking()) { - data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); - } else { - int stack = decorator.getHeight(rng, realX, realZ, getData()); - if (decorator.isScaleStack()) { - int maxStack = max - height; - stack = (int) Math.ceil((double) maxStack * ((double) stack / 100)); - } else stack = Math.min(max - height, stack); - - if (stack == 1) { - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - for (int i = 0; i < stack; i++) { - int h = height + i; - double threshold = ((double) i) / (stack - 1); - data.set(x, h + 1, z, threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData())); - } - } - } - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/decorator/IrisSurfaceDecorator.java b/core/src/main/java/com/volmit/iris/engine/decorator/IrisSurfaceDecorator.java deleted file mode 100644 index 3bc6cc861..000000000 --- a/core/src/main/java/com/volmit/iris/engine/decorator/IrisSurfaceDecorator.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.decorator; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.InferredType; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisDecorationPart; -import com.volmit.iris.engine.object.IrisDecorator; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import org.bukkit.Material; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.Bisected; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.PointedDripstone; - -public class IrisSurfaceDecorator extends IrisEngineDecorator { - public IrisSurfaceDecorator(Engine engine) { - super(engine, "Surface", IrisDecorationPart.NONE); - } - - @BlockCoordinates - @Override - public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { - if (biome.getInferredType().equals(InferredType.SHORE) && height < getDimension().getFluidHeight()) { - return; - } - - BlockData bd, bdx; - RNG rng = getRNG(realX, realZ); - IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); - bdx = data.get(x, height, z); - boolean underwater = height < getDimension().getFluidHeight(); - - if (decorator != null) { - if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() - && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { - return; - } - - if (!decorator.isStacking()) { - bd = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); - - if (!underwater) { - if (!canGoOn(bd, bdx) && (!decorator.isForcePlace() && decorator.getForceBlock() == null)) { - return; - } - } - - if (decorator.getForceBlock() != null) { - data.set(x, height, z, fixFaces(decorator.getForceBlock().getBlockData(getData()), data, x, z, realX, height, realZ)); - } else if (!decorator.isForcePlace()) { - if (decorator.getWhitelist() != null && decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(getData()).equals(bdx))) { - return; - } - if (decorator.getBlacklist() != null && decorator.getBlacklist().stream().anyMatch(d -> d.getBlockData(getData()).equals(bdx))) { - return; - } - } - - if (bd instanceof Bisected) { - bd = bd.clone(); - ((Bisected) bd).setHalf(Bisected.Half.TOP); - try { - data.set(x, height + 2, z, bd); - } catch (Throwable e) { - Iris.reportError(e); - } - bd = bd.clone(); - ((Bisected) bd).setHalf(Bisected.Half.BOTTOM); - } - - if (B.isAir(data.get(x, height + 1, z))) { - data.set(x, height + 1, z, fixFaces(bd, data, x, z, realX, height + 1, realZ)); - } - } else { - if (height < getDimension().getFluidHeight()) { - max = getDimension().getFluidHeight(); - } - - int stack = decorator.getHeight(rng, realX, realZ, getData()); - - if (decorator.isScaleStack()) { - stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack()); - } else { - stack = Math.min(max, stack); - } - - if (stack == 1) { - data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); - return; - } - - for (int i = 0; i < stack; i++) { - int h = height + i; - double threshold = ((double) i) / (stack - 1); - bd = threshold >= decorator.getTopThreshold() ? - decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) : - decorator.getBlockData100(biome, rng, realX, h, realZ, getData()); - - if (bd == null) { - break; - } - - if (i == 0 && !underwater && !canGoOn(bd, bdx)) { - break; - } - - if (underwater && height + 1 + i > getDimension().getFluidHeight()) { - break; - } - - if (bd instanceof PointedDripstone) { - PointedDripstone.Thickness th = PointedDripstone.Thickness.BASE; - - if (stack == 2) { - th = PointedDripstone.Thickness.FRUSTUM; - - if (i == stack - 1) { - th = PointedDripstone.Thickness.TIP; - } - } else { - if (i == stack - 1) { - th = PointedDripstone.Thickness.TIP; - } else if (i == stack - 2) { - th = PointedDripstone.Thickness.FRUSTUM; - } - } - - - bd = Material.POINTED_DRIPSTONE.createBlockData(); - ((PointedDripstone) bd).setThickness(th); - ((PointedDripstone) bd).setVerticalDirection(BlockFace.UP); - } - - data.set(x, height + 1 + i, z, bd); - } - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/BlockUpdater.java b/core/src/main/java/com/volmit/iris/engine/framework/BlockUpdater.java deleted file mode 100644 index 1ba9e7a18..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/BlockUpdater.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.util.mantle.MantleChunk; -import com.volmit.iris.util.math.RNG; -import org.bukkit.Chunk; -import org.bukkit.block.data.BlockData; - -public interface BlockUpdater { - - void catchBlockUpdates(int x, int y, int z, BlockData data); - - void updateChunk(Chunk c); - - void update(int x, int y, int z, Chunk c, MantleChunk mc, RNG rf); -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/Engine.java b/core/src/main/java/com/volmit/iris/engine/framework/Engine.java deleted file mode 100644 index fd972ff1d..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/Engine.java +++ /dev/null @@ -1,1006 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.events.IrisLootEvent; -import com.volmit.iris.core.gui.components.RenderType; -import com.volmit.iris.core.gui.components.Renderer; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.core.nms.container.BlockPos; -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.core.pregenerator.ChunkUpdater; -import com.volmit.iris.core.scripting.environment.EngineEnvironment; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.IrisComplex; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.data.chunk.TerrainChunk; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.DataProvider; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.function.Function2; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.mantle.MantleChunk; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.math.BlockPosition; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.matter.MatterUpdate; -import com.volmit.iris.util.matter.TileWrapper; -import com.volmit.iris.util.matter.slices.container.JigsawPieceContainer; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.reflect.W; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import com.volmit.iris.util.stream.ProceduralStream; -import io.papermc.lib.PaperLib; -import org.bukkit.*; -import org.bukkit.block.Biome; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.BlockData; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.Nullable; - -import java.awt.Color; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdater, Renderer, Hotloadable { - IrisComplex getComplex(); - - EngineMode getMode(); - - int getBlockUpdatesPerSecond(); - - void printMetrics(CommandSender sender); - - EngineMantle getMantle(); - - void hotloadSilently(); - - void hotloadComplex(); - - void recycle(); - - void close(); - - IrisContext getContext(); - - EngineEnvironment getExecution(); - - double getMaxBiomeObjectDensity(); - - double getMaxBiomeDecoratorDensity(); - - double getMaxBiomeLayerDensity(); - - boolean isClosed(); - - EngineWorldManager getWorldManager(); - - default UUID getBiomeID(int x, int z) { - return getComplex().getBaseBiomeIDStream().get(x, z); - } - - int getParallelism(); - - void setParallelism(int parallelism); - - EngineTarget getTarget(); - - default int getMaxHeight() { - return getTarget().getWorld().maxHeight(); - } - - default int getMinHeight() { - return getTarget().getWorld().minHeight(); - } - - default void setMinHeight(int min) { - getTarget().getWorld().minHeight(min); - } - - @BlockCoordinates - default void generate(int x, int z, TerrainChunk tc, boolean multicore) throws WrongEngineBroException { - generate(x, z, Hunk.view(tc), Hunk.view(tc, tc.getMinHeight(), tc.getMaxHeight()), multicore); - } - - @BlockCoordinates - void generate(int x, int z, Hunk blocks, Hunk biomes, boolean multicore) throws WrongEngineBroException; - - EngineMetrics getMetrics(); - - default void save() { - getMantle().save(); - getWorldManager().onSave(); - saveEngineData(); - } - - default void saveNow() { - getMantle().saveAllNow(); - saveEngineData(); - } - - SeedManager getSeedManager(); - - void saveEngineData(); - - default String getName() { - return getDimension().getName(); - } - - default IrisData getData() { - return getTarget().getData(); - } - - default IrisWorld getWorld() { - return getTarget().getWorld(); - } - - default IrisDimension getDimension() { - return getTarget().getDimension(); - } - - @BlockCoordinates - default Color draw(double x, double z) { - IrisRegion region = getRegion((int) x, (int) z); - IrisBiome biome = getSurfaceBiome((int) x, (int) z); - int height = getHeight((int) x, (int) z); - double heightFactor = M.lerpInverse(0, getTarget().getHeight(), height); - Color irc = region.getColor(this.getComplex(), RenderType.BIOME); - Color ibc = biome.getColor(this, RenderType.BIOME); - Color rc = irc != null ? irc : Color.GREEN.darker(); - Color bc = ibc != null ? ibc : biome.isAquatic() ? Color.BLUE : Color.YELLOW; - Color f = IrisColor.blend(rc, bc, bc, Color.getHSBColor(0, 0, (float) heightFactor)); - - return IrisColor.blend(rc, bc, bc, Color.getHSBColor(0, 0, (float) heightFactor)); - } - - @BlockCoordinates - default IrisRegion getRegion(int x, int z) { - return getComplex().getRegionStream().get(x, z); - } - - void generateMatter(int x, int z, boolean multicore, ChunkContext context); - - @BlockCoordinates - default IrisBiome getCaveOrMantleBiome(int x, int y, int z) { - MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class); - - if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) { - IrisBiome biome = getData().getBiomeLoader().load(m.getCustomBiome()); - - if (biome != null) { - return biome; - } - } - - return getCaveBiome(x, z); - } - - @ChunkCoordinates - Set getObjectsAt(int x, int z); - - @ChunkCoordinates - Set> getPOIsAt(int x, int z); - - @ChunkCoordinates - IrisJigsawStructure getStructureAt(int x, int z); - - @BlockCoordinates - IrisJigsawStructure getStructureAt(int x, int y, int z); - - @BlockCoordinates - default IrisBiome getCaveBiome(int x, int z) { - return getComplex().getCaveBiomeStream().get(x, z); - } - - @BlockCoordinates - default IrisBiome getSurfaceBiome(int x, int z) { - return getComplex().getTrueBiomeStream().get(x, z); - } - - @BlockCoordinates - default int getHeight(int x, int z) { - return getHeight(x, z, true); - } - - @BlockCoordinates - default int getHeight(int x, int z, boolean ignoreFluid) { - return getMantle().getHighest(x, z, getData(), ignoreFluid); - } - - @BlockCoordinates - @Override - default void catchBlockUpdates(int x, int y, int z, BlockData data) { - if (data == null) { - return; - } - - if (B.isUpdatable(data)) { - getMantle().updateBlock(x, y, z); - } - if (data instanceof IrisCustomData) { - getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.CUSTOM_ACTIVE, true); - } - } - - void blockUpdatedMetric(); - - @ChunkCoordinates - @Override - default void updateChunk(Chunk c) { - for (int x = -1; x <= 1; x++) { - for (int z = -1; z <= 1; z++) { - if (c.getWorld().isChunkLoaded(c.getX() + x, c.getZ() + z)) - continue; - var msg = "Chunk %s, %s [%s, %s] is not loaded".formatted(c.getX() + x, c.getZ() + z, x, z); - if (W.getStack().getCallerClass().equals(ChunkUpdater.class)) Iris.warn(msg); - else Iris.debug(msg); - return; - } - } - var mantle = getMantle().getMantle(); - if (!mantle.isLoaded(c)) { - var msg = "Mantle Chunk " + c.getX() + c.getX() + " is not loaded"; - if (W.getStack().getCallerClass().equals(ChunkUpdater.class)) Iris.warn(msg); - else Iris.debug(msg); - return; - } - - var chunk = mantle.getChunk(c).use(); - try { - Semaphore semaphore = new Semaphore(1024); - chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> { - chunk.raiseFlagUnchecked(MantleFlag.TILE, run(semaphore, () -> { - chunk.iterate(TileWrapper.class, (x, y, z, v) -> { - Block block = c.getBlock(x & 15, y + getWorld().minHeight(), z & 15); - if (!TileData.setTileState(block, v.getData())) - Iris.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", block.getX(), block.getY(), block.getZ(), block.getType().getKey(), v.getData().getMaterial().getKey()); - }); - }, 0)); - chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, run(semaphore, () -> { - chunk.iterate(Identifier.class, (x, y, z, v) -> { - Iris.service(ExternalDataSVC.class).processUpdate(this, c.getBlock(x & 15, y + getWorld().minHeight(), z & 15), v); - }); - }, 0)); - - chunk.raiseFlagUnchecked(MantleFlag.UPDATE, run(semaphore, () -> { - PrecisionStopwatch p = PrecisionStopwatch.start(); - int[][] grid = new int[16][16]; - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - grid[x][z] = Integer.MIN_VALUE; - } - } - - RNG rng = new RNG(Cache.key(c.getX(), c.getZ())); - chunk.iterate(MatterCavern.class, (x, yf, z, v) -> { - int y = yf + getWorld().minHeight(); - x &= 15; - z &= 15; - Block block = c.getBlock(x, y, z); - if (!B.isFluid(block.getBlockData())) { - return; - } - boolean u = B.isAir(block.getRelative(BlockFace.DOWN).getBlockData()) - || B.isAir(block.getRelative(BlockFace.WEST).getBlockData()) - || B.isAir(block.getRelative(BlockFace.EAST).getBlockData()) - || B.isAir(block.getRelative(BlockFace.SOUTH).getBlockData()) - || B.isAir(block.getRelative(BlockFace.NORTH).getBlockData()); - - if (u) grid[x][z] = Math.max(grid[x][z], y); - }); - - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - if (grid[x][z] == Integer.MIN_VALUE) - continue; - update(x, grid[x][z], z, c, chunk, rng); - } - } - - chunk.iterate(MatterUpdate.class, (x, yf, z, v) -> { - int y = yf + getWorld().minHeight(); - if (v != null && v.isUpdate()) { - update(x, y, z, c, chunk, rng); - } - }); - chunk.deleteSlices(MatterUpdate.class); - getMetrics().getUpdates().put(p.getMilliseconds()); - }, RNG.r.i(1, 20))); //Why is there a random delay here? - }); - - chunk.raiseFlagUnchecked(MantleFlag.SCRIPT, () -> { - var scripts = getDimension().getChunkUpdateScripts(); - if (scripts == null || scripts.isEmpty()) - return; - - for (var script : scripts) { - getExecution().updateChunk(script, chunk, c, (delay, task) -> run(semaphore, task, delay)); - } - }); - - try { - semaphore.acquire(1024); - } catch (InterruptedException ignored) {} - } finally { - chunk.release(); - } - } - - private static Runnable run(Semaphore semaphore, Runnable runnable, int delay) { - return () -> { - try { - semaphore.acquire(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - J.s(() -> { - try { - runnable.run(); - } finally { - semaphore.release(); - } - }, delay); - }; - } - - @BlockCoordinates - @Override - - default void update(int x, int y, int z, Chunk c, MantleChunk mc, RNG rf) { - Block block = c.getBlock(x, y, z); - BlockData data = block.getBlockData(); - blockUpdatedMetric(); - if (B.isStorage(data)) { - RNG rx = rf.nextParallelRNG(BlockPosition.toLong(x, y, z)); - InventorySlotType slot = null; - - if (B.isStorageChest(data)) { - slot = InventorySlotType.STORAGE; - } - - if (slot != null) { - KList tables = getLootTables(rx, block, mc); - - try { - Bukkit.getPluginManager().callEvent(new IrisLootEvent(this, block, slot, tables)); - if (tables.isEmpty()) return; - InventoryHolder m = (InventoryHolder) block.getState(); - addItems(false, m.getInventory(), rx, tables, slot, c.getWorld(), x, y, z, 15); - - } catch (Throwable e) { - Iris.reportError(e); - } - } - } else { - block.setType(Material.AIR, false); - block.setBlockData(data, true); - } - } - - @Override - default void scramble(Inventory inventory, RNG rng) { - org.bukkit.inventory.ItemStack[] items = inventory.getContents(); - org.bukkit.inventory.ItemStack[] nitems = new org.bukkit.inventory.ItemStack[inventory.getSize()]; - System.arraycopy(items, 0, nitems, 0, items.length); - boolean packedFull = false; - - splitting: - for (int i = 0; i < nitems.length; i++) { - ItemStack is = nitems[i]; - - if (is != null && is.getAmount() > 1 && !packedFull) { - for (int j = 0; j < nitems.length; j++) { - if (nitems[j] == null) { - int take = rng.nextInt(is.getAmount()); - take = take == 0 ? 1 : take; - is.setAmount(is.getAmount() - take); - nitems[j] = is.clone(); - nitems[j].setAmount(take); - continue splitting; - } - } - - packedFull = true; - } - } - - for (int i = nitems.length; i > 1; i--) { - int j = rng.nextInt(i); - ItemStack tmp = nitems[i - 1]; - nitems[i - 1] = nitems[j]; - nitems[j] = tmp; - } - - inventory.setContents(nitems); - } - - @Override - default void injectTables(KList list, IrisLootReference r, boolean fallback) { - if (r.getMode().equals(IrisLootMode.FALLBACK) && !fallback) - return; - - if (r.getMode().equals(IrisLootMode.CLEAR) || r.getMode().equals(IrisLootMode.REPLACE)) { - list.clear(); - } - - list.addAll(r.getLootTables(getComplex())); - } - - @BlockCoordinates - @Override - default KList getLootTables(RNG rng, Block b) { - MantleChunk mc = getMantle().getMantle().getChunk(b.getChunk()).use(); - try { - return getLootTables(rng, b, mc); - } finally { - mc.release(); - } - } - - @BlockCoordinates - default KList getLootTables(RNG rng, Block b, MantleChunk mc) { - int rx = b.getX(); - int rz = b.getZ(); - int ry = b.getY() - getWorld().minHeight(); - double he = getComplex().getHeightStream().get(rx, rz); - KList tables = new KList<>(); - - PlacedObject po = getObjectPlacement(rx, ry, rz, mc); - if (po != null && po.getPlacement() != null) { - if (B.isStorageChest(b.getBlockData())) { - IrisLootTable table = po.getPlacement().getTable(b.getBlockData(), getData()); - if (table != null) { - tables.add(table); - if (po.getPlacement().isOverrideGlobalLoot()) { - return new KList<>(table); - } - } - } - } - - IrisRegion region = getComplex().getRegionStream().get(rx, rz); - IrisBiome biomeSurface = getComplex().getTrueBiomeStream().get(rx, rz); - IrisBiome biomeUnder = ry < he ? getComplex().getCaveBiomeStream().get(rx, rz) : biomeSurface; - - double multiplier = 1D * getDimension().getLoot().getMultiplier() * region.getLoot().getMultiplier() * biomeSurface.getLoot().getMultiplier() * biomeUnder.getLoot().getMultiplier(); - boolean fallback = tables.isEmpty(); - injectTables(tables, getDimension().getLoot(), fallback); - injectTables(tables, region.getLoot(), fallback); - injectTables(tables, biomeSurface.getLoot(), fallback); - injectTables(tables, biomeUnder.getLoot(), fallback); - - if (tables.isNotEmpty()) { - int target = (int) Math.round(tables.size() * multiplier); - - while (tables.size() < target && tables.isNotEmpty()) { - tables.add(tables.get(rng.i(tables.size() - 1))); - } - - while (tables.size() > target && tables.isNotEmpty()) { - tables.remove(rng.i(tables.size() - 1)); - } - } - - return tables; - } - - @Override - default void addItems(boolean debug, Inventory inv, RNG rng, KList tables, InventorySlotType slot, World world, int x, int y, int z, int mgf) { - KList items = new KList<>(); - - for (IrisLootTable i : tables) { - if (i == null) - continue; - items.addAll(i.getLoot(debug, rng, slot, world, x, y, z)); - } - if (IrisLootEvent.callLootEvent(items, inv, world, x, y, z)) - return; - - if (PaperLib.isPaper() && getWorld().hasRealWorld()) { - PaperLib.getChunkAtAsync(getWorld().realWorld(), x >> 4, z >> 4).thenAccept((c) -> { - Runnable r = () -> { - for (ItemStack i : items) { - inv.addItem(i); - } - - scramble(inv, rng); - }; - - if (Bukkit.isPrimaryThread()) { - r.run(); - } else { - J.s(r); - } - }); - } else { - for (ItemStack i : items) { - inv.addItem(i); - } - - scramble(inv, rng); - } - } - - EngineEffects getEffects(); - - default MultiBurst burst() { - return getTarget().getBurster(); - } - - @Deprecated - default void clean() { - burst().lazy(() -> getMantle().trim(10)); - } - - @BlockCoordinates - default IrisBiome getBiome(Location l) { - return getBiome(l.getBlockX(), l.getBlockY() - getWorld().minHeight(), l.getBlockZ()); - } - - @BlockCoordinates - default IrisRegion getRegion(Location l) { - return getRegion(l.getBlockX(), l.getBlockZ()); - } - - IrisBiome getFocus(); - - IrisRegion getFocusRegion(); - - - IrisEngineData getEngineData(); - - default IrisBiome getSurfaceBiome(Chunk c) { - return getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8); - } - - default IrisRegion getRegion(Chunk c) { - return getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8); - } - - default KList getAllBiomes() { - KMap v = new KMap<>(); - - IrisDimension dim = getDimension(); - dim.getAllBiomes(this).forEach((i) -> v.put(i.getLoadKey(), i)); - - return v.v(); - } - - int getGenerated(); - - CompletableFuture getHash32(); - - default IrisPosition lookForStreamResult(T find, ProceduralStream stream, Function2 matcher, long timeout) { - AtomicInteger checked = new AtomicInteger(); - AtomicLong time = new AtomicLong(M.ms()); - AtomicReference r = new AtomicReference<>(); - BurstExecutor b = burst().burst(); - - while (M.ms() - time.get() < timeout && r.get() == null) { - b.queue(() -> { - for (int i = 0; i < 1000; i++) { - if (M.ms() - time.get() > timeout) { - return; - } - - int x = RNG.r.i(-29999970, 29999970); - int z = RNG.r.i(-29999970, 29999970); - checked.incrementAndGet(); - if (matcher.apply(stream.get(x, z), find)) { - r.set(new IrisPosition(x, 120, z)); - time.set(0); - } - } - }); - } - - return r.get(); - } - - default IrisPosition lookForBiome(IrisBiome biome, long timeout, Consumer triesc) { - if (!getWorld().hasRealWorld()) { - Iris.error("Cannot GOTO without a bound world (headless mode)"); - return null; - } - - ChronoLatch cl = new ChronoLatch(250, false); - long s = M.ms(); - int cpus = (Runtime.getRuntime().availableProcessors()); - - if (!getDimension().getAllBiomes(this).contains(biome)) { - return null; - } - - AtomicInteger tries = new AtomicInteger(0); - AtomicBoolean found = new AtomicBoolean(false); - AtomicBoolean running = new AtomicBoolean(true); - AtomicReference location = new AtomicReference<>(); - for (int i = 0; i < cpus; i++) { - J.a(() -> { - try { - Engine e; - IrisBiome b; - int x, z; - - while (!found.get() && running.get()) { - try { - x = RNG.r.i(-29999970, 29999970); - z = RNG.r.i(-29999970, 29999970); - b = getSurfaceBiome(x, z); - - if (b != null && b.getLoadKey() == null) { - continue; - } - - if (b != null && b.getLoadKey().equals(biome.getLoadKey())) { - found.lazySet(true); - location.lazySet(new IrisPosition(x, getHeight(x, z), z)); - } - - tries.getAndIncrement(); - } catch (Throwable ex) { - Iris.reportError(ex); - ex.printStackTrace(); - return; - } - } - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - }); - } - - while (!found.get() || location.get() == null) { - J.sleep(50); - - if (cl.flip()) { - triesc.accept(tries.get()); - } - - if (M.ms() - s > timeout) { - running.set(false); - return null; - } - } - - running.set(false); - return location.get(); - } - - default IrisPosition lookForRegion(IrisRegion reg, long timeout, Consumer triesc) { - if (!getWorld().hasRealWorld()) { - Iris.error("Cannot GOTO without a bound world (headless mode)"); - return null; - } - - ChronoLatch cl = new ChronoLatch(3000, false); - long s = M.ms(); - int cpus = (Runtime.getRuntime().availableProcessors()); - - if (!getDimension().getRegions().contains(reg.getLoadKey())) { - return null; - } - - AtomicInteger tries = new AtomicInteger(0); - AtomicBoolean found = new AtomicBoolean(false); - AtomicBoolean running = new AtomicBoolean(true); - AtomicReference location = new AtomicReference<>(); - - for (int i = 0; i < cpus; i++) { - J.a(() -> { - Engine e; - IrisRegion b; - int x, z; - - while (!found.get() && running.get()) { - try { - x = RNG.r.i(-29999970, 29999970); - z = RNG.r.i(-29999970, 29999970); - b = getRegion(x, z); - - if (b != null && b.getLoadKey() != null && b.getLoadKey().equals(reg.getLoadKey())) { - found.lazySet(true); - location.lazySet(new IrisPosition(x, getHeight(x, z), z)); - } - - tries.getAndIncrement(); - } catch (Throwable xe) { - Iris.reportError(xe); - xe.printStackTrace(); - return; - } - } - }); - } - - while (!found.get() || location.get() != null) { - J.sleep(50); - - if (cl.flip()) { - triesc.accept(tries.get()); - } - - if (M.ms() - s > timeout) { - triesc.accept(tries.get()); - running.set(false); - return null; - } - } - - triesc.accept(tries.get()); - running.set(false); - return location.get(); - } - - double getGeneratedPerSecond(); - - default int getHeight() { - return getWorld().getHeight(); - } - - boolean isStudio(); - - default IrisBiome getBiome(int x, int y, int z) { - if (y <= getHeight(x, z) - 2) { - return getCaveBiome(x, z); - } - - return getSurfaceBiome(x, z); - } - - default IrisBiome getBiomeOrMantle(int x, int y, int z) { - if (y <= getHeight(x, z) - 2) { - return getCaveOrMantleBiome(x, y, z); - } - - return getSurfaceBiome(x, z); - } - - default String getObjectPlacementKey(int x, int y, int z) { - PlacedObject o = getObjectPlacement(x, y, z); - - if (o != null && o.getObject() != null) { - return o.getObject().getLoadKey() + "@" + o.getId(); - } - - return null; - } - - default PlacedObject getObjectPlacement(int x, int y, int z) { - MantleChunk chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use(); - try { - return getObjectPlacement(x, y, z, chunk); - } finally { - chunk.release(); - } - } - - default PlacedObject getObjectPlacement(int x, int y, int z, MantleChunk chunk) { - String objectAt = chunk.get(x & 15, y, z & 15, String.class); - if (objectAt == null || objectAt.isEmpty()) { - return null; - } - - String[] v = objectAt.split("\\Q@\\E"); - String object = v[0]; - int id = Integer.parseInt(v[1]); - - - JigsawPieceContainer container = chunk.get(x & 15, y, z & 15, JigsawPieceContainer.class); - if (container != null) { - IrisJigsawPiece piece = container.load(getData()); - if (piece.getObject().equals(object)) - return new PlacedObject(piece.getPlacementOptions(), getData().getObjectLoader().load(object), id, x, z); - } - - IrisRegion region = getRegion(x, z); - - for (IrisObjectPlacement i : region.getObjects()) { - if (i.getPlace().contains(object)) { - return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z); - } - } - - IrisBiome biome = getSurfaceBiome(x, z); - - for (IrisObjectPlacement i : biome.getObjects()) { - if (i.getPlace().contains(object)) { - return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z); - } - } - - return new PlacedObject(null, getData().getObjectLoader().load(object), id, x, z); - } - - int getCacheID(); - - default IrisBiome getBiomeOrMantle(Location l) { - return getBiomeOrMantle(l.getBlockX(), l.getBlockY(), l.getBlockZ()); - } - - @Nullable - @BlockCoordinates - default Position2 getNearestStronghold(Position2 pos) { - KList p = getDimension().getStrongholds(getSeedManager().getMantle()); - if (p.isEmpty()) return null; - - Position2 pr = null; - double d = Double.MAX_VALUE; - - for (Position2 i : p) { - double dx = i.distance(pos); - if (dx < d) { - d = dx; - pr = i; - } - } - return pr; - } - - default void gotoBiome(IrisBiome biome, Player player, boolean teleport) { - Set regionKeys = getDimension() - .getAllRegions(this).stream() - .filter((i) -> i.getAllBiomeIds().contains(biome.getLoadKey())) - .map(IrisRegistrant::getLoadKey) - .collect(Collectors.toSet()); - Locator lb = Locator.surfaceBiome(biome.getLoadKey()); - Locator locator = (engine, chunk) - -> regionKeys.contains(getRegion((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey()) - && lb.matches(engine, chunk); - - if (!regionKeys.isEmpty()) { - locator.find(player, teleport, "Biome " + biome.getName()); - } else { - player.sendMessage(C.RED + biome.getName() + " is not in any defined regions!"); - } - } - - default void gotoJigsaw(IrisJigsawStructure s, Player player, boolean teleport) { - if (s.getLoadKey().equals(getDimension().getStronghold())) { - Position2 pr = getNearestStronghold(new Position2(player.getLocation().getBlockX(), player.getLocation().getBlockZ())); - if (pr == null) { - player.sendMessage(C.GOLD + "No strongholds in world."); - } else { - Location ll = new Location(player.getWorld(), pr.getX(), 40, pr.getZ()); - J.s(() -> player.teleport(ll)); - } - - return; - } - - if (getDimension().getJigsawStructures().stream() - .map(IrisJigsawStructurePlacement::getStructure) - .collect(Collectors.toSet()).contains(s.getLoadKey())) { - Locator.jigsawStructure(s.getLoadKey()).find(player, teleport, "Structure " + s.getLoadKey()); - } else { - Set biomeKeys = getDimension().getAllBiomes(this).stream() - .filter((i) -> i.getJigsawStructures() - .stream() - .anyMatch((j) -> j.getStructure().equals(s.getLoadKey()))) - .map(IrisRegistrant::getLoadKey) - .collect(Collectors.toSet()); - Set regionKeys = getDimension().getAllRegions(this).stream() - .filter((i) -> i.getAllBiomeIds().stream().anyMatch(biomeKeys::contains) - || i.getJigsawStructures() - .stream() - .anyMatch((j) -> j.getStructure().equals(s.getLoadKey()))) - .map(IrisRegistrant::getLoadKey) - .collect(Collectors.toSet()); - - Locator sl = Locator.jigsawStructure(s.getLoadKey()); - Locator locator = (engine, chunk) -> { - if (biomeKeys.contains(getSurfaceBiome((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey())) { - return sl.matches(engine, chunk); - } else if (regionKeys.contains(getRegion((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey())) { - return sl.matches(engine, chunk); - } - return false; - }; - - if (!regionKeys.isEmpty()) { - locator.find(player, teleport, "Structure " + s.getLoadKey()); - } else { - player.sendMessage(C.RED + s.getLoadKey() + " is not in any defined regions, biomes or dimensions!"); - } - } - - } - - default void gotoObject(String s, Player player, boolean teleport) { - Set biomeKeys = getDimension().getAllBiomes(this).stream() - .filter((i) -> i.getObjects().stream().anyMatch((f) -> f.getPlace().contains(s))) - .map(IrisRegistrant::getLoadKey) - .collect(Collectors.toSet()); - Set regionKeys = getDimension().getAllRegions(this).stream() - .filter((i) -> i.getAllBiomeIds().stream().anyMatch(biomeKeys::contains) - || i.getObjects().stream().anyMatch((f) -> f.getPlace().contains(s))) - .map(IrisRegistrant::getLoadKey) - .collect(Collectors.toSet()); - - Locator sl = Locator.object(s); - Locator locator = (engine, chunk) -> { - if (biomeKeys.contains(getSurfaceBiome((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey())) { - return sl.matches(engine, chunk); - } else if (regionKeys.contains(getRegion((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey())) { - return sl.matches(engine, chunk); - } - - return false; - }; - - if (!regionKeys.isEmpty()) { - locator.find(player, teleport, "Object " + s); - } else { - player.sendMessage(C.RED + s + " is not in any defined regions or biomes!"); - } - } - - default void gotoRegion(IrisRegion r, Player player, boolean teleport) { - if (!getDimension().getRegions().contains(r.getLoadKey())) { - player.sendMessage(C.RED + r.getName() + " is not defined in the dimension!"); - return; - } - - Locator.region(r.getLoadKey()).find(player, teleport, "Region " + r.getName()); - } - - default void gotoPOI(String type, Player p, boolean teleport) { - Locator.poi(type).find(p, teleport, "POI " + type); - } - - default void cleanupMantleChunk(int x, int z) { - if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) { - getMantle().cleanupChunk(x, z); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedActuator.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedActuator.java deleted file mode 100644 index 792ae1877..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedActuator.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; - -public abstract class EngineAssignedActuator extends EngineAssignedComponent implements EngineActuator { - public EngineAssignedActuator(Engine engine, String name) { - super(engine, name); - } - - public abstract void onActuate(int x, int z, Hunk output, boolean multicore, ChunkContext context); - - @BlockCoordinates - @Override - public void actuate(int x, int z, Hunk output, boolean multicore, ChunkContext context) { - onActuate(x, z, output, multicore, context); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedBiModifier.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedBiModifier.java deleted file mode 100644 index 0f29d67c6..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedBiModifier.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - - -import com.volmit.iris.util.hunk.Hunk; - -public abstract class EngineAssignedBiModifier extends EngineAssignedComponent implements EngineBiModifier { - public EngineAssignedBiModifier(Engine engine, String name) { - super(engine, name); - } - - public abstract void onModify(int x, int z, Hunk a, Hunk b); - - @Override - public void modify(int x, int z, Hunk a, Hunk b) { - onModify(x, z, a, b); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedModifier.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedModifier.java deleted file mode 100644 index b3a83e53e..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedModifier.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; - -public abstract class EngineAssignedModifier extends EngineAssignedComponent implements EngineModifier { - public EngineAssignedModifier(Engine engine, String name) { - super(engine, name); - } - - @BlockCoordinates - public abstract void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context); - - @BlockCoordinates - @Override - public void modify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { - try { - onModify(x, z, output, multicore, context); - } catch (Throwable e) { - Iris.error("Modifier Failure: " + getName()); - e.printStackTrace(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedWorldManager.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedWorldManager.java deleted file mode 100644 index 7dabc01de..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineAssignedWorldManager.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.events.IrisEngineHotloadEvent; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.plugin.VolmitSender; -import org.bukkit.*; -import org.bukkit.entity.EnderSignal; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.event.world.ChunkLoadEvent; -import org.bukkit.event.world.ChunkUnloadEvent; -import org.bukkit.event.world.WorldSaveEvent; -import org.bukkit.event.world.WorldUnloadEvent; -import org.bukkit.inventory.EquipmentSlot; - -import java.util.concurrent.atomic.AtomicBoolean; - -public abstract class EngineAssignedWorldManager extends EngineAssignedComponent implements EngineWorldManager, Listener { - private final int taskId; - protected AtomicBoolean ignoreTP = new AtomicBoolean(false); - - public EngineAssignedWorldManager() { - super(null, null); - taskId = -1; - } - - public EngineAssignedWorldManager(Engine engine) { - super(engine, "World"); - Iris.instance.registerListener(this); - taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, this::onTick, 0, 0); - } - - @EventHandler - public void on(IrisEngineHotloadEvent e) { - for (Player i : e.getEngine().getWorld().getPlayers()) { - i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f); - VolmitSender s = new VolmitSender(i); - s.sendTitle(C.IRIS + "Engine " + C.AQUA + "Hotloaded", 70, 60, 410); - } - } - -// @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) -// public void on(PlayerTeleportEvent e) { -// if(ignoreTP.get()) { -// System.out.println("IgTP1"); -// return; -// } -// -// if(!PaperLib.isPaper() || e.getTo() == null) { -// System.out.println("IgTP2"); -// -//// return; -// } -// -//// try { -//// System.out.println("IgTP3"); -//// -//// if(e.getTo().getWorld().equals(getTarget().getWorld().realWorld())) { -//// System.out.println("IgTP4"); -//// -//// getEngine().getWorldManager().teleportAsync(e); -//// } -//// } catch(Throwable ex) { -//// -//// } -// } - - @EventHandler - public void on(WorldSaveEvent e) { - if (e.getWorld().equals(getTarget().getWorld().realWorld())) { - getEngine().save(); - } - } - - @EventHandler - public void on(WorldUnloadEvent e) { - if (e.getWorld().equals(getTarget().getWorld().realWorld())) { - getEngine().close(); - } - } - - @EventHandler - public void on(BlockBreakEvent e) { - if (e.getPlayer().getWorld().equals(getTarget().getWorld().realWorld())) { - onBlockBreak(e); - } - } - - @EventHandler - public void on(BlockPlaceEvent e) { - if (e.getPlayer().getWorld().equals(getTarget().getWorld().realWorld())) { - onBlockPlace(e); - } - } - - @EventHandler - public void on(ChunkLoadEvent e) { - if (e.getChunk().getWorld().equals(getTarget().getWorld().realWorld())) { - onChunkLoad(e.getChunk(), e.isNewChunk()); - } - } - - @EventHandler - public void on(ChunkUnloadEvent e) { - if (e.getChunk().getWorld().equals(getTarget().getWorld().realWorld())) { - onChunkUnload(e.getChunk()); - } - } - - @Override - public void close() { - super.close(); - Iris.instance.unregisterListener(this); - Bukkit.getScheduler().cancelTask(taskId); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineBiModifier.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineBiModifier.java deleted file mode 100644 index ecd9b894d..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineBiModifier.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - - -import com.volmit.iris.util.hunk.Hunk; - -public interface EngineBiModifier extends EngineComponent { - void modify(int x, int z, Hunk a, Hunk b); -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineComponent.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineComponent.java deleted file mode 100644 index 74cdfd6f1..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineComponent.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.IrisComplex; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.parallel.MultiBurst; -import org.bukkit.event.Listener; - -public interface EngineComponent { - Engine getEngine(); - - RollingSequence getMetrics(); - - String getName(); - - default MultiBurst burst() { - return getEngine().burst(); - } - - default void close() { - try { - if (this instanceof Listener) { - Iris.instance.unregisterListener((Listener) this); - } - } catch (Throwable e) { - Iris.reportError(e); - - } - } - - default IrisData getData() { - return getEngine().getData(); - } - - default EngineTarget getTarget() { - return getEngine().getTarget(); - } - - default IrisDimension getDimension() { - return getEngine().getDimension(); - } - - default long getSeed() { - return getEngine().getSeedManager().getComponent(); - } - - default int getParallelism() { - return getEngine().getParallelism(); - } - - default IrisComplex getComplex() { - return getEngine().getComplex(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineData.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineData.java deleted file mode 100644 index 1364a86d8..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineData.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.google.gson.Gson; -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.IrisPosition; -import com.volmit.iris.util.io.IO; -import lombok.Data; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -@Data -public class EngineData { - private String dimension; - private String lastVersion; - private List strongholdPositions; - - public static EngineData load(File f) { - try { - f.getParentFile().mkdirs(); - return new Gson().fromJson(IO.readAll(f), EngineData.class); - } catch (Throwable e) { - Iris.reportError(e); - - } - - return new EngineData(); - } - - public void save(File f) { - try { - f.getParentFile().mkdirs(); - IO.writeAll(f, new Gson().toJson(this)); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineDecorator.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineDecorator.java deleted file mode 100644 index c0e3898b5..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineDecorator.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import org.bukkit.block.data.BlockData; - -public interface EngineDecorator extends EngineComponent { - - @BlockCoordinates - void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max); - - @BlockCoordinates - default void decorate(int x, int z, int realX, int realZ, Hunk data, IrisBiome biome, int height, int max) { - decorate(x, z, realX, realX, realX, realZ, realZ, realZ, data, biome, height, max); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - default boolean canGoOn(BlockData decorant, BlockData atop) { - if (atop == null || B.isAir(atop)) { - return false; - } - - return B.canPlaceOnto(decorant.getMaterial(), atop.getMaterial()); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineMetrics.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineMetrics.java deleted file mode 100644 index 691ef5d46..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineMetrics.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.util.atomics.AtomicRollingSequence; -import com.volmit.iris.util.collection.KMap; -import lombok.Data; - -@Data -public class EngineMetrics { - private final AtomicRollingSequence total; - private final AtomicRollingSequence updates; - private final AtomicRollingSequence terrain; - private final AtomicRollingSequence biome; - private final AtomicRollingSequence parallax; - private final AtomicRollingSequence parallaxInsert; - private final AtomicRollingSequence post; - private final AtomicRollingSequence perfection; - private final AtomicRollingSequence api; - private final AtomicRollingSequence decoration; - private final AtomicRollingSequence cave; - private final AtomicRollingSequence ravine; - private final AtomicRollingSequence deposit; - - public EngineMetrics(int mem) { - this.total = new AtomicRollingSequence(mem); - this.terrain = new AtomicRollingSequence(mem); - this.api = new AtomicRollingSequence(mem); - this.biome = new AtomicRollingSequence(mem); - this.perfection = new AtomicRollingSequence(mem); - this.parallax = new AtomicRollingSequence(mem); - this.parallaxInsert = new AtomicRollingSequence(mem); - this.post = new AtomicRollingSequence(mem); - this.decoration = new AtomicRollingSequence(mem); - this.updates = new AtomicRollingSequence(mem); - this.cave = new AtomicRollingSequence(mem); - this.ravine = new AtomicRollingSequence(mem); - this.deposit = new AtomicRollingSequence(mem); - } - - public KMap pull() { - KMap v = new KMap<>(); - v.put("total", total.getAverage()); - v.put("terrain", terrain.getAverage()); - v.put("biome", biome.getAverage()); - v.put("parallax", parallax.getAverage()); - v.put("parallax.insert", parallaxInsert.getAverage()); - v.put("post", post.getAverage()); - v.put("perfection", perfection.getAverage()); - v.put("decoration", decoration.getAverage()); - v.put("api", api.getAverage()); - v.put("updates", updates.getAverage()); - v.put("cave", cave.getAverage()); - v.put("ravine", ravine.getAverage()); - v.put("deposit", deposit.getAverage()); - - return v; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineMode.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineMode.java deleted file mode 100644 index 6f933f58f..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineMode.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.engine.IrisComplex; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; - -public interface EngineMode extends Staged { - RollingSequence r = new RollingSequence(64); - RollingSequence r2 = new RollingSequence(256); - - void close(); - - Engine getEngine(); - - default MultiBurst burst() { - return getEngine().burst(); - } - - default EngineStage burst(EngineStage... stages) { - return (x, z, blocks, biomes, multicore, ctx) -> { - BurstExecutor e = burst().burst(stages.length); - e.setMulticore(multicore); - - for (EngineStage i : stages) { - e.queue(() -> i.generate(x, z, blocks, biomes, multicore, ctx)); - } - - e.complete(); - }; - } - - default IrisComplex getComplex() { - return getEngine().getComplex(); - } - - default EngineMantle getMantle() { - return getEngine().getMantle(); - } - - default void generateMatter(int x, int z, boolean multicore, ChunkContext context) { - getMantle().generateMatter(x, z, multicore, context); - } - - @BlockCoordinates - default void generate(int x, int z, Hunk blocks, Hunk biomes, boolean multicore) { - ChunkContext ctx = new ChunkContext(x, z, getComplex()); - IrisContext.getOr(getEngine()).setChunkContext(ctx); - - for (EngineStage i : getStages()) { - i.generate(x, z, blocks, biomes, multicore, ctx); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EnginePlayer.java b/core/src/main/java/com/volmit/iris/engine/framework/EnginePlayer.java deleted file mode 100644 index 2350b3140..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EnginePlayer.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisEffect; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.scheduling.J; -import lombok.Data; -import org.bukkit.Location; -import org.bukkit.entity.Player; - -@Data -public class EnginePlayer { - private final Engine engine; - private final Player player; - private IrisBiome biome; - private IrisRegion region; - private Location lastLocation; - private long lastSample; - - public EnginePlayer(Engine engine, Player player) { - this.engine = engine; - this.player = player; - lastLocation = player.getLocation().clone(); - lastSample = -1; - sample(); - } - - public void tick() { - if (sample() || !IrisSettings.get().getWorld().isEffectSystem()) - return; - - J.a(() -> { - if (region != null) { - for (IrisEffect j : region.getEffects()) { - try { - j.apply(player, getEngine()); - } catch (Throwable e) { - Iris.reportError(e); - - } - } - } - - if (biome != null) { - for (IrisEffect j : biome.getEffects()) { - try { - j.apply(player, getEngine()); - } catch (Throwable e) { - Iris.reportError(e); - - } - } - } - }); - } - - public long ticksSinceLastSample() { - return M.ms() - lastSample; - } - - public boolean sample() { - Location current = player.getLocation().clone(); - if (current.getWorld() != engine.getWorld().realWorld()) - return true; - try { - if (ticksSinceLastSample() > 55 && current.distanceSquared(lastLocation) > 9 * 9) { - lastLocation = current; - lastSample = M.ms(); - biome = engine.getBiome(current); - region = engine.getRegion(current); - } - return false; - } catch (Throwable e) { - Iris.reportError(e); - - } - return true; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineStage.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineStage.java deleted file mode 100644 index c863c8234..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineStage.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; - -public interface EngineStage { - @BlockCoordinates - void generate(int x, int z, Hunk blocks, Hunk biomes, boolean multicore, ChunkContext context); - - default void close() { - if (this instanceof EngineComponent c) { - c.close(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/EngineWorldManager.java b/core/src/main/java/com/volmit/iris/engine/framework/EngineWorldManager.java deleted file mode 100644 index 36a31b1f2..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/EngineWorldManager.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import org.bukkit.Chunk; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.player.PlayerTeleportEvent; - -@SuppressWarnings("EmptyMethod") -public interface EngineWorldManager { - void close(); - - double getEnergy(); - - int getEntityCount(); - - int getChunkCount(); - - double getEntitySaturation(); - - void onTick(); - - void onSave(); - - void onBlockBreak(BlockBreakEvent e); - - void onBlockPlace(BlockPlaceEvent e); - - void onChunkLoad(Chunk e, boolean generated); - - void onChunkUnload(Chunk e); - - void chargeEnergy(); - - void teleportAsync(PlayerTeleportEvent e); -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/ListFunction.java b/core/src/main/java/com/volmit/iris/engine/framework/ListFunction.java deleted file mode 100644 index a99341603..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/ListFunction.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.volmit.iris.engine.framework; - -import com.volmit.iris.core.loader.IrisData; - -import java.util.function.Function; - -public interface ListFunction extends Function { - String key(); - String fancyName(); -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/Locator.java b/core/src/main/java/com/volmit/iris/engine/framework/Locator.java deleted file mode 100644 index 0c05c068b..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/Locator.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.nms.container.BlockPos; -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisJigsawStructure; -import com.volmit.iris.engine.object.IrisObject; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.Spiraler; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import com.volmit.iris.util.scheduling.jobs.SingleJob; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Player; - -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -@FunctionalInterface -public interface Locator { - static void cancelSearch() { - if (LocatorCanceller.cancel != null) { - LocatorCanceller.cancel.run(); - LocatorCanceller.cancel = null; - } - } - - static Locator region(String loadKey) { - return (e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey); - } - - static Locator jigsawStructure(String loadKey) { - return (e, c) -> { - IrisJigsawStructure s = e.getStructureAt(c.getX(), c.getZ()); - return s != null && s.getLoadKey().equals(loadKey); - }; - } - - static Locator object(String loadKey) { - return (e, c) -> e.getObjectsAt(c.getX(), c.getZ()).contains(loadKey); - } - - static Locator surfaceBiome(String loadKey) { - return (e, c) -> e.getSurfaceBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey); - } - - static Locator poi(String type) { - return (e, c) -> { - Set> pos = e.getPOIsAt((c.getX() << 4) + 8, (c.getZ() << 4) + 8); - return pos.stream().anyMatch(p -> p.getA().equals(type)); - }; - } - - static Locator caveBiome(String loadKey) { - return (e, c) -> e.getCaveBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey); - } - - static Locator caveOrMantleBiome(String loadKey) { - return (e, c) -> { - AtomicBoolean found = new AtomicBoolean(false); - e.generateMatter(c.getX(), c.getZ(), true, new ChunkContext(c.getX() << 4, c.getZ() << 4, e.getComplex(), false)); - e.getMantle().getMantle().iterateChunk(c.getX(), c.getZ(), MatterCavern.class, (x, y, z, t) -> { - if (found.get()) { - return; - } - - if (t != null && t.getCustomBiome().equals(loadKey)) { - found.set(true); - } - }); - - return found.get(); - }; - } - - boolean matches(Engine engine, Position2 chunk); - - default void find(Player player, boolean teleport, String message) { - find(player, location -> { - if (teleport) { - J.s(() -> player.teleport(location)); - } else { - player.sendMessage(C.GREEN + message + " at: " + location.getBlockX() + " " + location.getBlockY() + " " + location.getBlockZ()); - } - }); - } - - default void find(Player player, Consumer consumer) { - find(player, 30_000, consumer); - } - - default void find(Player player, long timeout, Consumer consumer) { - AtomicLong checks = new AtomicLong(); - long ms = M.ms(); - new SingleJob("Searching", () -> { - try { - World world = player.getWorld(); - Engine engine = IrisToolbelt.access(world).getEngine(); - Position2 at = find(engine, new Position2(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4), timeout, checks::set).get(); - - if (at != null) { - consumer.accept(new Location(world, (at.getX() << 4) + 8, - engine.getHeight( - (at.getX() << 4) + 8, - (at.getZ() << 4) + 8, false), - (at.getZ() << 4) + 8)); - } - } catch (WrongEngineBroException | InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - }) { - @Override - public String getName() { - return "Searched " + Form.f(checks.get()) + " Chunks"; - } - - @Override - public int getTotalWork() { - return (int) timeout; - } - - @Override - public int getWorkCompleted() { - return (int) Math.min(M.ms() - ms, timeout - 1); - } - }.execute(new VolmitSender(player)); - } - - default Future find(Engine engine, Position2 pos, long timeout, Consumer checks) throws WrongEngineBroException { - if (engine.isClosed()) { - throw new WrongEngineBroException(); - } - - cancelSearch(); - - return MultiBurst.burst.completeValue(() -> { - int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 17; - MultiBurst burst = MultiBurst.burst; - AtomicBoolean found = new AtomicBoolean(false); - Position2 cursor = pos; - AtomicInteger searched = new AtomicInteger(); - AtomicBoolean stop = new AtomicBoolean(false); - AtomicReference foundPos = new AtomicReference<>(); - PrecisionStopwatch px = PrecisionStopwatch.start(); - LocatorCanceller.cancel = () -> stop.set(true); - AtomicReference next = new AtomicReference<>(cursor); - Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z))); - s.setOffset(cursor.getX(), cursor.getZ()); - s.next(); - while (!found.get() && !stop.get() && px.getMilliseconds() < timeout) { - BurstExecutor e = burst.burst(tc); - - for (int i = 0; i < tc; i++) { - Position2 p = next.get(); - s.next(); - e.queue(() -> { - if (matches(engine, p)) { - if (foundPos.get() == null) { - foundPos.set(p); - } - - found.set(true); - } - searched.incrementAndGet(); - }); - } - - e.complete(); - checks.accept(searched.get()); - } - - LocatorCanceller.cancel = null; - - if (found.get() && foundPos.get() != null) { - return foundPos.get(); - } - - return null; - }); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/LocatorCanceller.java b/core/src/main/java/com/volmit/iris/engine/framework/LocatorCanceller.java deleted file mode 100644 index 53ccaf2a8..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/LocatorCanceller.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -public class LocatorCanceller { - protected static Runnable cancel = null; -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/LootProvider.java b/core/src/main/java/com/volmit/iris/engine/framework/LootProvider.java deleted file mode 100644 index 3192748b4..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/LootProvider.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.engine.object.InventorySlotType; -import com.volmit.iris.engine.object.IrisLootReference; -import com.volmit.iris.engine.object.IrisLootTable; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.inventory.Inventory; - -public interface LootProvider { - void scramble(Inventory inventory, RNG rng); - - void injectTables(KList list, IrisLootReference r, boolean fallback); - - KList getLootTables(RNG rng, Block b); - - void addItems(boolean debug, Inventory inv, RNG rng, KList tables, InventorySlotType slot, World world, int x, int y, int z, int mgf); -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/PregeneratedData.java b/core/src/main/java/com/volmit/iris/engine/framework/PregeneratedData.java deleted file mode 100644 index 8b8580227..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/PregeneratedData.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -import com.volmit.iris.engine.data.chunk.TerrainChunk; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.hunk.Hunk; -import lombok.Data; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; - -import java.util.concurrent.atomic.AtomicBoolean; - -@Data -public class PregeneratedData { - private final Hunk blocks; - private final Hunk post; - private final Hunk biomes; - private final AtomicBoolean postMod; - - public PregeneratedData(int height) { - postMod = new AtomicBoolean(false); - blocks = Hunk.newAtomicHunk(16, height, 16); - biomes = Hunk.newAtomicHunk(16, height, 16); - Hunk p = Hunk.newMappedHunkSynced(16, height, 16); - post = p.trackWrite(postMod); - } - - public Runnable inject(TerrainChunk tc) { - blocks.iterateSync((x, y, z, b) -> { - if (b != null) { - tc.setBlock(x, y, z, b); - } - - Biome bf = biomes.get(x, y, z); - if (bf != null) { - tc.setBiome(x, y, z, bf); - } - }); - - if (postMod.get()) { - return () -> Hunk.view(tc).insertSoftly(0, 0, 0, post, (b) -> b == null || B.isAirOrFluid(b)); - } - - return () -> { - }; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/ResultLocator.java b/core/src/main/java/com/volmit/iris/engine/framework/ResultLocator.java deleted file mode 100644 index 93d698347..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/ResultLocator.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.volmit.iris.engine.framework; - -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.engine.object.IrisJigsawStructure; -import com.volmit.iris.engine.object.IrisObject; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.Spiraler; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.apache.commons.lang3.function.TriFunction; - -import java.util.Collection; -import java.util.Set; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -@FunctionalInterface -public interface ResultLocator { - static void cancelSearch() { - if (LocatorCanceller.cancel != null) { - LocatorCanceller.cancel.run(); - LocatorCanceller.cancel = null; - } - } - - static ResultLocator locateStructure(Collection keys) { - return (e, pos) -> { - var structure = e.getStructureAt(pos.getX(), pos.getZ()); - return structure != null && keys.contains(structure.getLoadKey()) ? structure : null; - }; - } - - static ResultLocator locateObject(Collection keys) { - return (e, pos) -> { - Set objects = e.getObjectsAt(pos.getX(), pos.getZ()); - for (String object : objects) { - if (!keys.contains(object)) continue; - return e.getData().getObjectLoader().load(object); - } - return null; - }; - } - - T find(Engine e, Position2 chunkPos); - - default ResultLocator then(TriFunction filter) { - return (e, pos) -> { - var t = find(e, pos); - return t != null ? filter.apply(e, pos, t) : null; - }; - } - - default Future> find(Engine engine, Position2 pos, long timeout, Consumer checks, boolean cancelable) throws WrongEngineBroException { - if (engine.isClosed()) { - throw new WrongEngineBroException(); - } - - cancelSearch(); - - return MultiBurst.burst.completeValue(() -> { - int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 17; - MultiBurst burst = MultiBurst.burst; - AtomicBoolean found = new AtomicBoolean(false); - AtomicReference> foundObj = new AtomicReference<>(); - Position2 cursor = pos; - AtomicInteger searched = new AtomicInteger(); - AtomicBoolean stop = new AtomicBoolean(false); - PrecisionStopwatch px = PrecisionStopwatch.start(); - if (cancelable) LocatorCanceller.cancel = () -> stop.set(true); - AtomicReference next = new AtomicReference<>(cursor); - Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z))); - s.setOffset(cursor.getX(), cursor.getZ()); - s.next(); - while (!found.get() && !stop.get() && px.getMilliseconds() < timeout) { - BurstExecutor e = burst.burst(tc); - - for (int i = 0; i < tc; i++) { - Position2 p = next.get(); - s.next(); - e.queue(() -> { - var o = find(engine, p); - if (o != null) { - if (foundObj.get() == null) { - foundObj.set(new Result<>(o, p)); - } - - found.set(true); - } - searched.incrementAndGet(); - }); - } - - e.complete(); - checks.accept(searched.get()); - } - - LocatorCanceller.cancel = null; - - if (found.get() && foundObj.get() != null) { - return foundObj.get(); - } - - return null; - }); - } - - record Result(T obj, Position2 pos) { - public int getBlockX() { - return (pos.getX() << 4) + 8; - } - - public int getBlockZ() { - return (pos.getZ() << 4) + 8; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/WrongEngineBroException.java b/core/src/main/java/com/volmit/iris/engine/framework/WrongEngineBroException.java deleted file mode 100644 index 63db6f72f..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/WrongEngineBroException.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.framework; - -public class WrongEngineBroException extends Exception { -} diff --git a/core/src/main/java/com/volmit/iris/engine/framework/placer/WorldObjectPlacer.java b/core/src/main/java/com/volmit/iris/engine/framework/placer/WorldObjectPlacer.java deleted file mode 100644 index e75f00c6b..000000000 --- a/core/src/main/java/com/volmit/iris/engine/framework/placer/WorldObjectPlacer.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.volmit.iris.engine.framework.placer; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.core.events.IrisLootEvent; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.engine.object.IObjectPlacer; -import com.volmit.iris.engine.object.InventorySlotType; -import com.volmit.iris.engine.object.IrisLootTable; -import com.volmit.iris.engine.object.TileData; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.math.RNG; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.bukkit.inventory.InventoryHolder; - -@Getter -@EqualsAndHashCode(exclude = {"engine", "mantle"}) -public class WorldObjectPlacer implements IObjectPlacer { - private final World world; - private final Engine engine; - private final EngineMantle mantle; - - public WorldObjectPlacer(World world) { - var a = IrisToolbelt.access(world); - if (a == null || a.getEngine() == null) throw new IllegalStateException(world.getName() + " is not an Iris World!"); - this.world = world; - this.engine = a.getEngine(); - this.mantle = engine.getMantle(); - } - - @Override - public int getHighest(int x, int z, IrisData data) { - return mantle.getHighest(x, z, data); - } - - @Override - public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { - return mantle.getHighest(x, z, data, ignoreFluid); - } - - @Override - public void set(int x, int y, int z, BlockData d) { - Block block = world.getBlockAt(x, y + world.getMinHeight(), z); - - if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return; - InventorySlotType slot = null; - if (B.isStorageChest(d)) { - slot = InventorySlotType.STORAGE; - } - - if (d instanceof IrisCustomData data) { - block.setBlockData(data.getBase(), false); - Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!"); - } else block.setBlockData(d, false); - - if (slot != null) { - RNG rx = new RNG(Cache.key(x, z)); - KList tables = engine.getLootTables(rx, block); - - try { - Bukkit.getPluginManager().callEvent(new IrisLootEvent(engine, block, slot, tables)); - - if (!tables.isEmpty()){ - Iris.debug("IrisLootEvent has been accessed"); - } - - if (tables.isEmpty()) - return; - InventoryHolder m = (InventoryHolder) block.getState(); - engine.addItems(false, m.getInventory(), rx, tables, slot, world, x, y, z, 15); - } catch (Throwable e) { - Iris.reportError(e); - } - } - } - - @Override - public BlockData get(int x, int y, int z) { - return world.getBlockAt(x, y + world.getMinHeight(), z).getBlockData(); - } - - @Override - public boolean isPreventingDecay() { - return mantle.isPreventingDecay(); - } - - @Override - public boolean isCarved(int x, int y, int z) { - return mantle.isCarved(x, y, z); - } - - @Override - public boolean isSolid(int x, int y, int z) { - return world.getBlockAt(x, y + world.getMinHeight(), z).getType().isSolid(); - } - - @Override - public boolean isUnderwater(int x, int z) { - return mantle.isUnderwater(x, z); - } - - @Override - public int getFluidHeight() { - return mantle.getFluidHeight(); - } - - @Override - public boolean isDebugSmartBore() { - return mantle.isDebugSmartBore(); - } - - @Override - public void setTile(int xx, int yy, int zz, TileData tile) { - tile.toBukkitTry(world.getBlockAt(xx, yy + world.getMinHeight(), zz)); - } - - @Override - public void setData(int xx, int yy, int zz, T data) { - } - - @Override - public T getData(int xx, int yy, int zz, Class t) { - return null; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/jigsaw/PlannedPiece.java b/core/src/main/java/com/volmit/iris/engine/jigsaw/PlannedPiece.java deleted file mode 100644 index fb757a6e4..000000000 --- a/core/src/main/java/com/volmit/iris/engine/jigsaw/PlannedPiece.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.jigsaw; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.math.AxisAlignedBB; -import lombok.AccessLevel; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Setter; -import org.bukkit.util.BlockVector; - -import java.util.ArrayList; -import java.util.List; - -@Data -public class PlannedPiece { - private IrisPosition position; - private IrisObject object; - private IrisObject ogObject; - private IrisJigsawPiece piece; - private IrisObjectRotation rotation; - @EqualsAndHashCode.Exclude - private IrisData data; - private KList connected; - private boolean dead = false; - private AxisAlignedBB box; - @EqualsAndHashCode.Exclude - private PlannedStructure structure; - @EqualsAndHashCode.Exclude - @Setter(AccessLevel.NONE) - private ParentConnection parent = null; - @EqualsAndHashCode.Exclude - @Setter(AccessLevel.NONE) - private KMap realPositions; - - public PlannedPiece(PlannedStructure structure, IrisPosition position, IrisJigsawPiece piece) { - this(structure, position, piece, 0, 0, 0); - } - - public PlannedPiece(PlannedStructure structure, IrisPosition position, IrisJigsawPiece piece, int rx, int ry, int rz) { - this(structure, position, piece, IrisObjectRotation.of(rx * 90D, ry * 90D, rz * 90D)); - } - - public PlannedPiece(PlannedStructure structure, IrisPosition position, IrisJigsawPiece piece, IrisObjectRotation rot) { - this.structure = structure; - this.position = position; - this.data = piece.getLoader(); - this.setRotation(rot); - this.ogObject = data.getObjectLoader().load(piece.getObject()); - this.object = structure.rotated(piece, rotation); - this.piece = rotation.rotateCopy(piece, new IrisPosition(object.getShrinkOffset())); - this.piece.setLoadKey(piece.getLoadKey()); - this.object.setLoadKey(piece.getObject()); - this.ogObject.setLoadKey(piece.getObject()); - this.connected = new KList<>(); - this.realPositions = new KMap<>(); - - } - - public void setPosition(IrisPosition p) { - this.position = p; - box = null; - } - - public String toString() { - return piece.getLoadKey() + "@(" + position.getX() + "," + position.getY() + "," + position.getZ() + ")[rot:" + rotation.toString() + "]"; - } - - public AxisAlignedBB getBox() { - if (box != null) { - return box; - } - - BlockVector v = getObject().getCenter(); - IrisPosition pos = new IrisPosition(); - IrisObjectPlacement options = piece.getPlacementOptions(); - if (options != null && options.getTranslate() != null) { - IrisObjectTranslate translate = options.getTranslate(); - pos.setX(translate.getX()); - pos.setY(translate.getY()); - pos.setZ(translate.getZ()); - } - box = object.getAABB().shifted(position.add(new IrisPosition(object.getCenter())).add(pos)); - return box; - } - - public boolean contains(IrisPosition p) { - return getBox().contains(p); - } - - public boolean collidesWith(PlannedPiece p) { - return getBox().intersects(p.getBox()); - } - - public KList getAvailableConnectors() { - if (connected.isEmpty()) { - return piece.getConnectors().copy(); - } - - if (connected.size() == piece.getConnectors().size()) { - return new KList<>(); - } - - KList c = new KList<>(); - - for (IrisJigsawPieceConnector i : piece.getConnectors()) { - if (!connected.contains(i)) { - c.add(i); - } - } - - return c; - } - - public KList getChildConnectors() { - ParentConnection pc = getParent(); - KList c = getConnected().copy(); - if (pc != null) c.removeIf(i -> i.equals(pc.connector)); - return c; - } - - public boolean connect(IrisJigsawPieceConnector c, PlannedPiece p, IrisJigsawPieceConnector pc) { - if (piece.getConnectors().contains(c) && p.getPiece().getConnectors().contains(pc)) { - if (connected.contains(c) || p.connected.contains(pc)) return false; - connected.add(c); - p.connected.add(pc); - p.parent = new ParentConnection(this, c, p, pc); - return true; - } - return false; - } - - public IrisPosition getWorldPosition(IrisJigsawPieceConnector c) { - return getWorldPosition(c.getPosition()); - } - - public List getConnectorWorldPositions() { - List worldPositions = new ArrayList<>(); - - for (IrisJigsawPieceConnector connector : this.piece.getConnectors()) { - IrisPosition worldPosition = getWorldPosition(connector.getPosition()); - worldPositions.add(worldPosition); - } - - return worldPositions; - } - - public IrisPosition getWorldPosition(IrisPosition position) { - return this.position.add(position).add(new IrisPosition(object.getCenter())); - } - - public void debugPrintConnectorPositions() { - Iris.debug("Connector World Positions for PlannedPiece at " + position + ":"); - List connectorPositions = getConnectorWorldPositions(); - for (IrisPosition pos : connectorPositions) { - Iris.debug(" - Connector at: " + pos); - } - } - - public boolean isFull() { - return connected.size() >= piece.getConnectors().size(); - } - - public void setRealPositions(int x, int y, int z, IObjectPlacer placer) { - boolean isUnderwater = piece.getPlacementOptions().isUnderwater(); - for (IrisJigsawPieceConnector c : piece.getConnectors()) { - var pos = c.getPosition().add(new IrisPosition(x, 0, z)); - if (y < 0) { - pos.setY(pos.getY() + placer.getHighest(pos.getX(), pos.getZ(), getData(), isUnderwater) + (object.getH() / 2)); - } else { - pos.setY(pos.getY() + y); - } - realPositions.put(c, pos); - } - } - - public record ParentConnection(PlannedPiece parent, IrisJigsawPieceConnector parentConnector, PlannedPiece self, IrisJigsawPieceConnector connector) { - public IrisPosition getTargetPosition() { - var pos = parent.realPositions.get(parentConnector); - if (pos == null) return null; - return pos.add(new IrisPosition(parentConnector.getDirection().toVector())) - .sub(connector.getPosition()) - .sub(new IrisPosition(self.object.getCenter())); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/jigsaw/PlannedStructure.java b/core/src/main/java/com/volmit/iris/engine/jigsaw/PlannedStructure.java deleted file mode 100644 index 96be7cbc8..000000000 --- a/core/src/main/java/com/volmit/iris/engine/jigsaw/PlannedStructure.java +++ /dev/null @@ -1,361 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.jigsaw; - -import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.placer.WorldObjectPlacer; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.slices.container.JigsawPieceContainer; -import com.volmit.iris.util.matter.slices.container.JigsawStructureContainer; -import com.volmit.iris.util.matter.slices.container.JigsawStructuresContainer; -import com.volmit.iris.util.scheduling.J; -import lombok.Data; -import org.bukkit.Axis; - -import java.util.function.Consumer; - -@Data -public class PlannedStructure { - private static ConcurrentLinkedHashMap objectRotationCache - = new ConcurrentLinkedHashMap.Builder() - .initialCapacity(64) - .maximumWeightedCapacity(1024) - .concurrencyLevel(32) - .build(); - private KList pieces; - private IrisJigsawStructure structure; - private IrisPosition position; - private IrisData data; - private RNG rng; - private boolean forcePlace; - private boolean verbose; - private boolean terminating; - - public PlannedStructure(IrisJigsawStructure structure, IrisPosition position, RNG rng, boolean forcePlace) { - terminating = false; - verbose = true; - this.pieces = new KList<>(); - this.structure = structure; - this.position = position; - this.rng = rng; - this.forcePlace = forcePlace || structure.isForcePlace(); - this.data = structure.getLoader(); - generateStartPiece(); - - for (int i = 0; i < structure.getMaxDepth(); i++) { - generateOutwards(); - } - - generateTerminators(); - - Iris.debug("JPlace: ROOT @ relative " + position.toString()); - - for (PlannedPiece i : pieces) { - Iris.debug("Place: " + i.getObject().getLoadKey() + " at @ relative " + i.getPosition().toString()); - } - } - - public boolean place(IObjectPlacer placer, Mantle e, Engine eng) { - IrisObjectPlacement options = new IrisObjectPlacement(); - options.setRotation(IrisObjectRotation.of(0,0,0)); - int startHeight = pieces.get(0).getPosition().getY(); - - boolean placed = false; - for (PlannedPiece i : pieces) { - if (place(i, startHeight, options, placer, e, eng)) - placed = true; - } - if (placed) { - Position2 chunkPos = new Position2(position.getX() >> 4, position.getZ() >> 4); - Position2 regionPos = new Position2(chunkPos.getX() >> 5, chunkPos.getZ() >> 5); - JigsawStructuresContainer slice = e.get(regionPos.getX(), 0, regionPos.getZ(), JigsawStructuresContainer.class); - if (slice == null) slice = new JigsawStructuresContainer(); - slice.add(structure, chunkPos); - e.set(regionPos.getX(), 0, regionPos.getZ(), slice); - } - return placed; - } - - public boolean place(PlannedPiece i, int startHeight, IrisObjectPlacement o, IObjectPlacer placer, Mantle e, Engine eng) { - IrisObjectPlacement options = o; - - if (i.getPiece().getPlacementOptions() != null) { - options = i.getPiece().getPlacementOptions(); - options.getRotation().setEnabled(false); - options.setRotateTowardsSlope(false); - options.setWarp(new IrisGeneratorStyle(NoiseStyle.FLAT)); - } else { - options.setMode(i.getPiece().getPlaceMode()); - } - if (forcePlace) { - options.setForcePlace(true); - } - - IrisObject v = i.getObject(); - int sx = (v.getW() / 2); - int sz = (v.getD() / 2); - int xx = i.getPosition().getX() + sx; - int zz = i.getPosition().getZ() + sz; - int offset = i.getPosition().getY() - startHeight; - int height; - - if (i.getStructure().getStructure().getLockY() == -1) { - if (i.getStructure().getStructure().getOverrideYRange() != null) { - height = (int) i.getStructure().getStructure().getOverrideYRange().get(rng, xx, zz, getData()); - } else { - height = placer.getHighest(xx, zz, getData(), options.isUnderwater()); - } - } else { - height = i.getStructure().getStructure().getLockY(); - } - - PlannedPiece.ParentConnection connection = i.getParent(); - if (connection != null && connection.connector().isLockY()) { - var pos = connection.getTargetPosition(); - if (pos != null) { - height = pos.getY(); - offset = 0; - } else { - Iris.warn("Failed to get target position for " + v.getLoadKey()); - } - } - - height += offset + (v.getH() / 2); - - if (options.getMode().equals(ObjectPlaceMode.PAINT)) { - height = -1; - } - - int id = rng.i(0, Integer.MAX_VALUE); - JigsawPieceContainer piece = JigsawPieceContainer.toContainer(i.getPiece()); - JigsawStructureContainer structure = JigsawStructureContainer.toContainer(getStructure()); - i.setRealPositions(xx, height, zz, placer); - return v.place(xx, height, zz, placer, options, rng, (b, data) -> { - placer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id); - placer.setData(b.getX(), b.getY(), b.getZ(), structure); - placer.setData(b.getX(), b.getY(), b.getZ(), piece); - }, null, getData().getEngine() != null ? getData() : eng.getData()) != -1; - } - - public void place(WorldObjectPlacer placer, Consumer consumer) { - J.s(() -> consumer.accept(place(placer, placer.getMantle().getMantle(), placer.getEngine()))); - } - - private void generateOutwards() { - for (PlannedPiece i : getPiecesWithAvailableConnectors().shuffle(rng)) { - if (!generatePieceOutwards(i)) { - i.setDead(true); - } - } - } - - private boolean generatePieceOutwards(PlannedPiece piece) { - boolean b = false; - - for (IrisJigsawPieceConnector i : piece.getAvailableConnectors().shuffleCopy(rng)) { - if (generateConnectorOutwards(piece, i)) { - b = true; - piece.debugPrintConnectorPositions(); - } - } - - return b; - } - - private boolean generateConnectorOutwards(PlannedPiece piece, IrisJigsawPieceConnector pieceConnector) { - for (IrisJigsawPiece i : getShuffledPiecesFor(pieceConnector)) { - if (generateRotatedPiece(piece, pieceConnector, i)) { - return true; - } - } - - return false; - } - - private boolean generateRotatedPiece(PlannedPiece piece, IrisJigsawPieceConnector pieceConnector, IrisJigsawPiece idea) { - if (!piece.getPiece().getPlacementOptions().getRotation().isEnabled()) { - return generateRotatedPiece(piece, pieceConnector, idea, 0, 0, 0); - } - - KList forder1 = new KList().qadd(0).qadd(1).qadd(2).qadd(3).shuffle(rng); - KList forder2 = new KList().qadd(0).qadd(1).qadd(2).qadd(3).shuffle(rng); - - for (Integer i : forder1) { - if (pieceConnector.isRotateConnector()) { - assert pieceConnector.getDirection().getAxis() != null; - if (!pieceConnector.getDirection().getAxis().equals(Axis.Y)) { - for (Integer j : forder2) { - if (pieceConnector.getDirection().getAxis().equals(Axis.X) && generateRotatedPiece(piece, pieceConnector, idea, j, i, 0)) { - return true; - } - - if (pieceConnector.getDirection().getAxis().equals(Axis.Z) && generateRotatedPiece(piece, pieceConnector, idea, 0, i, j)) { - return true; - } - } - } - } - - if (generateRotatedPiece(piece, pieceConnector, idea, 0, i, 0)) { - return true; - } - } - - return false; - } - - private boolean generateRotatedPiece(PlannedPiece piece, IrisJigsawPieceConnector pieceConnector, IrisJigsawPiece idea, IrisObjectRotation rotation) { - if (!idea.getPlacementOptions().getRotation().isEnabled()) { - rotation = piece.getRotation(); - } - - PlannedPiece test = new PlannedPiece(this, piece.getPosition(), idea, rotation); - - for (IrisJigsawPieceConnector j : test.getPiece().getConnectors().shuffleCopy(rng)) { - if (generatePositionedPiece(piece, pieceConnector, test, j)) { - return true; - } - } - - return false; - } - - private boolean generateRotatedPiece(PlannedPiece piece, IrisJigsawPieceConnector pieceConnector, IrisJigsawPiece idea, int x, int y, int z) { - return generateRotatedPiece(piece, pieceConnector, idea, IrisObjectRotation.of(x * 90D, y * 90D, z * 90D)); - } - - private boolean generatePositionedPiece(PlannedPiece piece, IrisJigsawPieceConnector pieceConnector, PlannedPiece test, IrisJigsawPieceConnector testConnector) { - test.setPosition(new IrisPosition(0, 0, 0)); - IrisPosition connector = piece.getWorldPosition(pieceConnector); - IrisDirection desiredDirection = pieceConnector.getDirection().reverse(); - IrisPosition desiredPosition = connector.sub(new IrisPosition(desiredDirection.toVector())); - - if (!pieceConnector.getTargetName().equals("*") && !pieceConnector.getTargetName().equals(testConnector.getName())) { - return false; - } - - if (!testConnector.getDirection().equals(desiredDirection)) { - return false; - } - - IrisPosition shift = test.getWorldPosition(testConnector); - test.setPosition(desiredPosition.sub(shift)); - - if (collidesWith(test, piece)) { - return false; - } - - piece.connect(pieceConnector, test, testConnector); - pieces.add(test); - - return true; - } - - private KList getShuffledPiecesFor(IrisJigsawPieceConnector c) { - KList p = new KList<>(); - - KList pools = terminating && getStructure().getTerminatePool() != null ? new KList<>(getStructure().getTerminatePool()) : c.getPools().shuffleCopy(rng); - for (String i : pools) { - for (String j : getData().getJigsawPoolLoader().load(i).getPieces().shuffleCopy(rng)) { - IrisJigsawPiece pi = getData().getJigsawPieceLoader().load(j); - - if (pi == null || (terminating && !pi.isTerminal())) { - continue; - } - - p.addIfMissing(pi); - } - } - return p.shuffle(rng); - } - - private void generateStartPiece() { - pieces.add(new PlannedPiece(this, position, getData().getJigsawPieceLoader().load(rng.pick(getStructure().getPieces())), 0, getStructure().isDisableInitialRotation() ? 0 : rng.nextInt(4), 0)); - } - - private void generateTerminators() { - if (getStructure().isTerminate()) { - terminating = true; - generateOutwards(); - } - } - - public KList getPiecesWithAvailableConnectors() { - KList available = pieces.copy().removeWhere(PlannedPiece::isFull); - if (!terminating) available.removeIf(PlannedPiece::isDead); - return available; - } - - public int getVolume() { - int v = 0; - - for (PlannedPiece i : pieces) { - v += i.getObject().getH() * i.getObject().getW() * i.getObject().getD(); - } - - return v; - } - - public int getMass() { - int v = 0; - - for (PlannedPiece i : pieces) { - v += i.getObject().getBlocks().size(); - } - - return v; - } - - public boolean collidesWith(PlannedPiece piece, PlannedPiece ignore) { - for (PlannedPiece i : pieces) { - if (i.equals(ignore)) { - continue; - } - - if (i.collidesWith(piece)) { - return true; - } - } - - return false; - } - - public boolean contains(IrisPosition p) { - for (PlannedPiece i : pieces) { - if (i.contains(p)) { - return true; - } - } - - return false; - } - - public IrisObject rotated(IrisJigsawPiece piece, IrisObjectRotation rotation) { - String key = piece.getObject() + "-" + rotation.hashCode(); - - return objectRotationCache.computeIfAbsent(key, (k) -> rotation.rotateCopy(data.getObjectLoader().load(piece.getObject()))); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/EngineMantle.java b/core/src/main/java/com/volmit/iris/engine/mantle/EngineMantle.java deleted file mode 100644 index 0ebe6cc53..000000000 --- a/core/src/main/java/com/volmit/iris/engine/mantle/EngineMantle.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.mantle; - -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.engine.IrisComplex; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineTarget; -import com.volmit.iris.engine.mantle.components.MantleJigsawComponent; -import com.volmit.iris.engine.mantle.components.MantleObjectComponent; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.engine.object.IrisPosition; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.mantle.MantleChunk; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.matter.*; -import com.volmit.iris.util.matter.slices.UpdateMatter; -import com.volmit.iris.util.parallel.MultiBurst; -import org.bukkit.block.data.BlockData; -import org.jetbrains.annotations.UnmodifiableView; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -public interface EngineMantle extends MatterGenerator { - BlockData AIR = B.get("AIR"); - - Mantle getMantle(); - - Engine getEngine(); - - int getRadius(); - - int getRealRadius(); - - @UnmodifiableView - List, Integer>> getComponents(); - - @UnmodifiableView - Map getRegisteredComponents(); - - boolean registerComponent(MantleComponent c); - - @UnmodifiableView - KList getComponentFlags(); - - void hotload(); - - default int getHighest(int x, int z) { - return getHighest(x, z, getData()); - } - - @ChunkCoordinates - default KList findMarkers(int x, int z, MatterMarker marker) { - KList p = new KList<>(); - getMantle().iterateChunk(x, z, MatterMarker.class, (xx, yy, zz, mm) -> { - if (marker.equals(mm)) { - p.add(new IrisPosition(xx + (x << 4), yy, zz + (z << 4))); - } - }); - - return p; - } - - default int getHighest(int x, int z, boolean ignoreFluid) { - return getHighest(x, z, getData(), ignoreFluid); - } - - default int getHighest(int x, int z, IrisData data) { - return getHighest(x, z, data, false); - } - - default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { - return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getEngine().getDimension().getFluidHeight()); - } - - default int trueHeight(int x, int z) { - return getComplex().getRoundedHeighteightStream().get(x, z); - } - - @Deprecated(forRemoval = true) - default boolean isCarved(int x, int h, int z) { - return getMantle().get(x, h, z, MatterCavern.class) != null; - } - - @Deprecated(forRemoval = true) - default BlockData get(int x, int y, int z) { - BlockData block = getMantle().get(x, y, z, BlockData.class); - if (block == null) - return AIR; - return block; - } - - default boolean isPreventingDecay() { - return getEngine().getDimension().isPreventLeafDecay(); - } - - default boolean isUnderwater(int x, int z) { - return getHighest(x, z, true) <= getFluidHeight(); - } - - default int getFluidHeight() { - return getEngine().getDimension().getFluidHeight(); - } - - default boolean isDebugSmartBore() { - return getEngine().getDimension().isDebugSmartBore(); - } - - default void trim(long dur, int limit) { - getMantle().trim(dur, limit); - } - - default IrisData getData() { - return getEngine().getData(); - } - - default EngineTarget getTarget() { - return getEngine().getTarget(); - } - - default IrisDimension getDimension() { - return getEngine().getDimension(); - } - - default IrisComplex getComplex() { - return getEngine().getComplex(); - } - - default void close() { - getMantle().close(); - } - - default void saveAllNow() { - getMantle().saveAll(); - } - - default void save() { - - } - - default void trim(int limit) { - getMantle().trim(TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()), limit); - } - default int unloadTectonicPlate(int tectonicLimit){ - return getMantle().unloadTectonicPlate(tectonicLimit); - } - - default MultiBurst burst() { - return getEngine().burst(); - } - - @ChunkCoordinates - default void insertMatter(int x, int z, Class t, Hunk blocks, boolean multicore) { - if (!getEngine().getDimension().isUseMantle()) { - return; - } - - var chunk = getMantle().getChunk(x, z).use(); - try { - chunk.iterate(t, blocks::set); - } finally { - chunk.release(); - } - } - - @BlockCoordinates - default void updateBlock(int x, int y, int z) { - getMantle().set(x, y, z, UpdateMatter.ON); - } - - @BlockCoordinates - default void dropCavernBlock(int x, int y, int z) { - Matter matter = getMantle().getChunk(x & 15, z & 15).get(y & 15); - - if (matter != null) { - matter.slice(MatterCavern.class).set(x & 15, y & 15, z & 15, null); - } - } - - default boolean queueRegenerate(int x, int z) { - return false; // TODO: - } - - default boolean dequeueRegenerate(int x, int z) { - return false;// TODO: - } - - default int getLoadedRegionCount() { - return getMantle().getLoadedRegionCount(); - } - - MantleJigsawComponent getJigsawComponent(); - - MantleObjectComponent getObjectComponent(); - - default boolean isCovered(int x, int z) { - int s = getRealRadius(); - - for (int i = -s; i <= s; i++) { - for (int j = -s; j <= s; j++) { - int xx = i + x; - int zz = j + z; - if (!getMantle().hasFlag(xx, zz, MantleFlag.REAL)) { - return false; - } - } - } - - return true; - } - - default void cleanupChunk(int x, int z) { - if (!isCovered(x, z)) return; - MantleChunk chunk = getMantle().getChunk(x, z).use(); - try { - chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> { - chunk.deleteSlices(BlockData.class); - chunk.deleteSlices(String.class); - chunk.deleteSlices(MatterCavern.class); - chunk.deleteSlices(MatterFluidBody.class); - }); - } finally { - chunk.release(); - } - } - - default int getUnloadRegionCount() { - return getMantle().getUnloadRegionCount(); - } - - default double getAdjustedIdleDuration() { - return getMantle().getAdjustedIdleDuration(); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/MantleComponent.java b/core/src/main/java/com/volmit/iris/engine/mantle/MantleComponent.java deleted file mode 100644 index b77b095a6..000000000 --- a/core/src/main/java/com/volmit/iris/engine/mantle/MantleComponent.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.mantle; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.IrisComplex; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.parallel.BurstExecutor; -import org.jetbrains.annotations.NotNull; - -public interface MantleComponent extends Comparable { - int getPriority(); - - int getRadius(); - - default IrisData getData() { - return getEngineMantle().getData(); - } - - default IrisDimension getDimension() { - return getEngineMantle().getEngine().getDimension(); - } - - default IrisComplex getComplex() { - return getEngineMantle().getComplex(); - } - - default long seed() { - return getEngineMantle().getEngine().getSeedManager().getMantle(); - } - - default BurstExecutor burst() { - return getEngineMantle().getEngine().burst().burst(); - } - - EngineMantle getEngineMantle(); - - default Mantle getMantle() { - return getEngineMantle().getMantle(); - } - - MantleFlag getFlag(); - - boolean isEnabled(); - - void setEnabled(boolean b); - - void hotload(); - - @ChunkCoordinates - void generateLayer(MantleWriter writer, int x, int z, ChunkContext context); - - @Override - default int compareTo(@NotNull MantleComponent o) { - return Integer.compare(getPriority(), o.getPriority()); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/MantleSized.java b/core/src/main/java/com/volmit/iris/engine/mantle/MantleSized.java deleted file mode 100644 index 724188574..000000000 --- a/core/src/main/java/com/volmit/iris/engine/mantle/MantleSized.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.mantle; - -public interface MantleSized { - int getMaxChunkSize(); -} diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/MantleWriter.java b/core/src/main/java/com/volmit/iris/engine/mantle/MantleWriter.java deleted file mode 100644 index 391bd8eb7..000000000 --- a/core/src/main/java/com/volmit/iris/engine/mantle/MantleWriter.java +++ /dev/null @@ -1,743 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.mantle; - -import com.google.common.collect.ImmutableList; -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IObjectPlacer; -import com.volmit.iris.engine.object.IrisGeneratorStyle; -import com.volmit.iris.engine.object.IrisPosition; -import com.volmit.iris.engine.object.TileData; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.mantle.MantleChunk; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.Matter; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.matter.TileWrapper; -import com.volmit.iris.util.noise.CNG; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import lombok.Data; -import org.bukkit.block.data.BlockData; -import org.bukkit.util.Vector; - -import java.util.*; - -import static com.volmit.iris.engine.mantle.EngineMantle.AIR; - -@Data -public class MantleWriter implements IObjectPlacer, AutoCloseable { - private final EngineMantle engineMantle; - private final Mantle mantle; - private final Map cachedChunks; - private final int radius; - private final int x; - private final int z; - - public MantleWriter(EngineMantle engineMantle, Mantle mantle, int x, int z, int radius, boolean multicore) { - this.engineMantle = engineMantle; - this.mantle = mantle; - this.radius = radius * 2; - final int d = this.radius + 1; - this.cachedChunks = multicore ? new KMap<>(d * d, 0.75f, Math.max(32, Runtime.getRuntime().availableProcessors() * 4)) : new Long2ObjectOpenHashMap<>(d * d); - this.x = x; - this.z = z; - - final int parallelism = multicore ? Runtime.getRuntime().availableProcessors() / 2 : 4; - final var map = multicore ? cachedChunks : new KMap(d * d, 1f, parallelism); - mantle.getChunks( - x - radius, - x + radius, - z - radius, - z + radius, - parallelism, - (i, j, c) -> map.put(Cache.key(i, j), c.use()) - ); - if (!multicore) cachedChunks.putAll(map); - } - - private static Set getBallooned(Set vset, double radius) { - Set returnset = new HashSet<>(); - int ceilrad = (int) Math.ceil(radius); - double r2 = Math.pow(radius, 2); - - for (IrisPosition v : vset) { - int tipx = v.getX(); - int tipy = v.getY(); - int tipz = v.getZ(); - - for (int loopx = tipx - ceilrad; loopx <= tipx + ceilrad; loopx++) { - for (int loopy = tipy - ceilrad; loopy <= tipy + ceilrad; loopy++) { - for (int loopz = tipz - ceilrad; loopz <= tipz + ceilrad; loopz++) { - if (hypot(loopx - tipx, loopy - tipy, loopz - tipz) <= r2) { - returnset.add(new IrisPosition(loopx, loopy, loopz)); - } - } - } - } - } - return returnset; - } - - private static Set getHollowed(Set vset) { - Set returnset = new KSet<>(); - for (IrisPosition v : vset) { - double x = v.getX(); - double y = v.getY(); - double z = v.getZ(); - if (!(vset.contains(new IrisPosition(x + 1, y, z)) - && vset.contains(new IrisPosition(x - 1, y, z)) - && vset.contains(new IrisPosition(x, y + 1, z)) - && vset.contains(new IrisPosition(x, y - 1, z)) - && vset.contains(new IrisPosition(x, y, z + 1)) - && vset.contains(new IrisPosition(x, y, z - 1)))) { - returnset.add(v); - } - } - return returnset; - } - - private static double hypot(double... pars) { - double sum = 0; - for (double d : pars) { - sum += Math.pow(d, 2); - } - return sum; - } - - private static double lengthSq(double x, double y, double z) { - return (x * x) + (y * y) + (z * z); - } - - private static double lengthSq(double x, double z) { - return (x * x) + (z * z); - } - - public void setDataWarped(int x, int y, int z, T t, RNG rng, IrisData data, IrisGeneratorStyle style) { - setData((int) Math.round(style.warp(rng, data, x, x, y, -z)), - (int) Math.round(style.warp(rng, data, y, z, -x, y)), - (int) Math.round(style.warp(rng, data, z, -y, z, x)), t); - } - - public void setData(int x, int y, int z, T t) { - if (t == null) { - return; - } - - int cx = x >> 4; - int cz = z >> 4; - - if (y < 0 || y >= mantle.getWorldHeight()) { - return; - } - - MantleChunk chunk = acquireChunk(cx, cz); - if (chunk == null) return; - - Matter matter = chunk.getOrCreate(y >> 4); - matter.slice(matter.getClass(t)).set(x & 15, y & 15, z & 15, t); - } - - public T getData(int x, int y, int z, Class type) { - int cx = x >> 4; - int cz = z >> 4; - - if (y < 0 || y >= mantle.getWorldHeight()) { - return null; - } - - MantleChunk chunk = acquireChunk(cx, cz); - if (chunk == null) { - return null; - } - - return chunk.getOrCreate(y >> 4) - .slice(type) - .get(x & 15, y & 15, z & 15); - } - - @ChunkCoordinates - public MantleChunk acquireChunk(int cx, int cz) { - if (cx < this.x - radius || cx > this.x + radius - || cz < this.z - radius || cz > this.z + radius) { - Iris.error("Mantle Writer Accessed chunk out of bounds" + cx + "," + cz); - return null; - } - final Long key = Cache.key(cx, cz); - MantleChunk chunk = cachedChunks.get(key); - if (chunk == null) { - chunk = mantle.getChunk(cx, cz).use(); - var old = cachedChunks.put(key, chunk); - if (old != null) old.release(); - } - return chunk; - } - - @Override - public int getHighest(int x, int z, IrisData data) { - return engineMantle.getHighest(x, z, data); - } - - @Override - public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { - return engineMantle.getHighest(x, z, data, ignoreFluid); - } - - @Override - public void set(int x, int y, int z, BlockData d) { - if (d instanceof IrisCustomData data) { - setData(x, y, z, data.getBase()); - setData(x, y, z, data.getCustom()); - } else setData(x, y, z, d); - } - - @Override - public BlockData get(int x, int y, int z) { - BlockData block = getData(x, y, z, BlockData.class); - if (block == null) - return AIR; - return block; - } - - @Override - public boolean isPreventingDecay() { - return getEngineMantle().isPreventingDecay(); - } - - @Override - public boolean isCarved(int x, int y, int z) { - return getData(x, y, z, MatterCavern.class) != null; - } - - @Override - public boolean isSolid(int x, int y, int z) { - return B.isSolid(get(x, y, z)); - } - - @Override - public boolean isUnderwater(int x, int z) { - return getEngineMantle().isUnderwater(x, z); - } - - @Override - public int getFluidHeight() { - return getEngineMantle().getFluidHeight(); - } - - @Override - public boolean isDebugSmartBore() { - return getEngineMantle().isDebugSmartBore(); - } - - @Override - public void setTile(int xx, int yy, int zz, TileData tile) { - setData(xx, yy, zz, new TileWrapper(tile)); - } - - @Override - public Engine getEngine() { - return getEngineMantle().getEngine(); - } - - /** - * Set a sphere into the mantle - * - * @param cx the center x - * @param cy the center y - * @param cz the center z - * @param radius the radius of this sphere - * @param fill should it be filled? or just the outer shell? - * @param data the data to set - * @param the type of data to apply to the mantle - */ - public void setSphere(int cx, int cy, int cz, double radius, boolean fill, T data) { - setElipsoid(cx, cy, cz, radius, radius, radius, fill, data); - } - - public void setElipsoid(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, T data) { - setElipsoidFunction(cx, cy, cz, rx, ry, rz, fill, (a, b, c) -> data); - } - - public void setElipsoidWarped(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, T data, RNG rng, IrisData idata, IrisGeneratorStyle style) { - setElipsoidFunctionWarped(cx, cy, cz, rx, ry, rz, fill, (a, b, c) -> data, rng, idata, style); - } - - /** - * Set an elipsoid into the mantle - * - * @param cx the center x - * @param cy the center y - * @param cz the center z - * @param rx the x radius - * @param ry the y radius - * @param rz the z radius - * @param fill should it be filled or just the outer shell? - * @param data the data to set - * @param the type of data to apply to the mantle - */ - public void setElipsoidFunction(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, Function3 data) { - rx += 0.5; - ry += 0.5; - rz += 0.5; - final double invRadiusX = 1 / rx; - final double invRadiusY = 1 / ry; - final double invRadiusZ = 1 / rz; - final int ceilRadiusX = (int) Math.ceil(rx); - final int ceilRadiusY = (int) Math.ceil(ry); - final int ceilRadiusZ = (int) Math.ceil(rz); - double nextXn = 0; - - forX: - for (int x = 0; x <= ceilRadiusX; ++x) { - final double xn = nextXn; - nextXn = (x + 1) * invRadiusX; - double nextYn = 0; - forY: - for (int y = 0; y <= ceilRadiusY; ++y) { - final double yn = nextYn; - nextYn = (y + 1) * invRadiusY; - double nextZn = 0; - for (int z = 0; z <= ceilRadiusZ; ++z) { - final double zn = nextZn; - nextZn = (z + 1) * invRadiusZ; - - double distanceSq = lengthSq(xn, yn, zn); - if (distanceSq > 1) { - if (z == 0) { - if (y == 0) { - break forX; - } - break forY; - } - break; - } - - if (!fill) { - if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) { - continue; - } - } - - setData(x + cx, y + cy, z + cz, data.apply(x + cx, y + cy, z + cz)); - setData(-x + cx, y + cy, z + cz, data.apply(-x + cx, y + cy, z + cz)); - setData(x + cx, -y + cy, z + cz, data.apply(x + cx, -y + cy, z + cz)); - setData(x + cx, y + cy, -z + cz, data.apply(x + cx, y + cy, -z + cz)); - setData(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz)); - setData(-x + cx, -y + cy, z + cz, data.apply(-x + cx, -y + cy, z + cz)); - setData(x + cx, -y + cy, -z + cz, data.apply(x + cx, -y + cy, -z + cz)); - setData(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz)); - setData(-x + cx, -y + cy, -z + cz, data.apply(-x + cx, -y + cy, -z + cz)); - } - } - } - } - - public void setElipsoidFunctionWarped(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, Function3 data, RNG rng, IrisData idata, IrisGeneratorStyle style) { - rx += 0.5; - ry += 0.5; - rz += 0.5; - final double invRadiusX = 1 / rx; - final double invRadiusY = 1 / ry; - final double invRadiusZ = 1 / rz; - final int ceilRadiusX = (int) Math.ceil(rx); - final int ceilRadiusY = (int) Math.ceil(ry); - final int ceilRadiusZ = (int) Math.ceil(rz); - double nextXn = 0; - - forX: - for (int x = 0; x <= ceilRadiusX; ++x) { - final double xn = nextXn; - nextXn = (x + 1) * invRadiusX; - double nextYn = 0; - forY: - for (int y = 0; y <= ceilRadiusY; ++y) { - final double yn = nextYn; - nextYn = (y + 1) * invRadiusY; - double nextZn = 0; - for (int z = 0; z <= ceilRadiusZ; ++z) { - final double zn = nextZn; - nextZn = (z + 1) * invRadiusZ; - - double distanceSq = lengthSq(xn, yn, zn); - if (distanceSq > 1) { - if (z == 0) { - if (y == 0) { - break forX; - } - break forY; - } - break; - } - - if (!fill) { - if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) { - continue; - } - } - - setDataWarped(x + cx, y + cy, z + cz, data.apply(x + cx, y + cy, z + cz), rng, idata, style); - setDataWarped(-x + cx, y + cy, z + cz, data.apply(-x + cx, y + cy, z + cz), rng, idata, style); - setDataWarped(x + cx, -y + cy, z + cz, data.apply(x + cx, -y + cy, z + cz), rng, idata, style); - setDataWarped(x + cx, y + cy, -z + cz, data.apply(x + cx, y + cy, -z + cz), rng, idata, style); - setDataWarped(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz), rng, idata, style); - setDataWarped(-x + cx, -y + cy, z + cz, data.apply(-x + cx, -y + cy, z + cz), rng, idata, style); - setDataWarped(x + cx, -y + cy, -z + cz, data.apply(x + cx, -y + cy, -z + cz), rng, idata, style); - setDataWarped(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz), rng, idata, style); - setDataWarped(-x + cx, -y + cy, -z + cz, data.apply(-x + cx, -y + cy, -z + cz), rng, idata, style); - } - } - } - } - - /** - * Set a cuboid of data in the mantle - * - * @param x1 the min x - * @param y1 the min y - * @param z1 the min z - * @param x2 the max x - * @param y2 the max y - * @param z2 the max z - * @param data the data to set - * @param the type of data to apply to the mantle - */ - public void setCuboid(int x1, int y1, int z1, int x2, int y2, int z2, T data) { - int j, k; - - for (int i = x1; i <= x2; i++) { - for (j = x1; j <= x2; j++) { - for (k = x1; k <= x2; k++) { - setData(i, j, k, data); - } - } - } - } - - /** - * Set a pyramid of data in the mantle - * - * @param cx the center x - * @param cy the base y - * @param cz the center z - * @param data the data to set - * @param size the size of the pyramid (width of base & height) - * @param filled should it be filled or hollow - * @param the type of data to apply to the mantle - */ - @SuppressWarnings("ConstantConditions") - public void setPyramid(int cx, int cy, int cz, T data, int size, boolean filled) { - int height = size; - - for (int y = 0; y <= height; ++y) { - size--; - for (int x = 0; x <= size; ++x) { - for (int z = 0; z <= size; ++z) { - if ((filled && z <= size && x <= size) || z == size || x == size) { - setData(x + cx, y + cy, z + cz, data); - setData(-x + cx, y + cy, z + cz, data); - setData(x + cx, y + cy, -z + cz, data); - setData(-x + cx, y + cy, -z + cz, data); - } - } - } - } - } - - /** - * Set a 3d line - * - * @param a the first point - * @param b the second point - * @param radius the radius - * @param filled hollow or filled? - * @param data the data - * @param the type of data to apply to the mantle - */ - public void setLine(IrisPosition a, IrisPosition b, double radius, boolean filled, T data) { - setLine(ImmutableList.of(a, b), radius, filled, data); - } - - public void setLine(List vectors, double radius, boolean filled, T data) { - setLineConsumer(vectors, radius, filled, (_x, _y, _z) -> data); - } - - /** - * Set lines for points - * - * @param vectors the points - * @param radius the radius - * @param filled hollow or filled? - * @param data the data to set - * @param the type of data to apply to the mantle - */ - public void setLineConsumer(List vectors, double radius, boolean filled, Function3 data) { - Set vset = cleanup(vectors); - vset = getBallooned(vset, radius); - - if (!filled) { - vset = getHollowed(vset); - } - - setConsumer(vset, data); - } - - /** - * Set lines for points - * - * @param vectors the points - * @param radius the radius - * @param filled hollow or filled? - * @param data the data to set - * @param the type of data to apply to the mantle - */ - public void setNoiseMasked(List vectors, double radius, double threshold, CNG shape, Set masks, boolean filled, Function3 data) { - Set vset = cleanup(vectors); - vset = masks == null ? getBallooned(vset, radius) : getMasked(vset, masks, radius); - vset.removeIf(p -> shape.noise(p.getX(), p.getY(), p.getZ()) < threshold); - - if (!filled) { - vset = getHollowed(vset); - } - - setConsumer(vset, data); - } - - private static Set getMasked(Set vectors, Set masks, double radius) { - Set vset = new KSet<>(); - int ceil = (int) Math.ceil(radius); - double r2 = Math.pow(radius, 2); - - for (IrisPosition v : vectors) { - int tipX = v.getX(); - int tipY = v.getY(); - int tipZ = v.getZ(); - - for (int x = -ceil; x <= ceil; x++) { - for (int y = -ceil; y <= ceil; y++) { - for (int z = -ceil; z <= ceil; z++) { - if (hypot(x, y, z) > r2 || !masks.contains(new IrisPosition(x, y, z))) - continue; - vset.add(new IrisPosition(tipX + x, tipY + y, tipZ + z)); - } - } - } - } - - return vset; - } - - private static Set cleanup(List vectors) { - Set vset = new KSet<>(); - - for (int i = 0; vectors.size() != 0 && i < vectors.size() - 1; i++) { - IrisPosition pos1 = vectors.get(i); - IrisPosition pos2 = vectors.get(i + 1); - int x1 = pos1.getX(); - int y1 = pos1.getY(); - int z1 = pos1.getZ(); - int x2 = pos2.getX(); - int y2 = pos2.getY(); - int z2 = pos2.getZ(); - int tipx = x1; - int tipy = y1; - int tipz = z1; - int dx = Math.abs(x2 - x1); - int dy = Math.abs(y2 - y1); - int dz = Math.abs(z2 - z1); - - if (dx + dy + dz == 0) { - vset.add(new IrisPosition(tipx, tipy, tipz)); - continue; - } - - int dMax = Math.max(Math.max(dx, dy), dz); - if (dMax == dx) { - for (int domstep = 0; domstep <= dx; domstep++) { - tipx = x1 + domstep * (x2 - x1 > 0 ? 1 : -1); - tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dx) * (y2 - y1 > 0 ? 1 : -1)); - tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dx) * (z2 - z1 > 0 ? 1 : -1)); - - vset.add(new IrisPosition(tipx, tipy, tipz)); - } - } else if (dMax == dy) { - for (int domstep = 0; domstep <= dy; domstep++) { - tipy = y1 + domstep * (y2 - y1 > 0 ? 1 : -1); - tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dy) * (x2 - x1 > 0 ? 1 : -1)); - tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dy) * (z2 - z1 > 0 ? 1 : -1)); - - vset.add(new IrisPosition(tipx, tipy, tipz)); - } - } else /* if (dMax == dz) */ { - for (int domstep = 0; domstep <= dz; domstep++) { - tipz = z1 + domstep * (z2 - z1 > 0 ? 1 : -1); - tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dz) * (y2 - y1 > 0 ? 1 : -1)); - tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dz) * (x2 - x1 > 0 ? 1 : -1)); - - vset.add(new IrisPosition(tipx, tipy, tipz)); - } - } - } - - return vset; - } - - /** - * Set a cylinder in the mantle - * - * @param cx the center x - * @param cy the base y - * @param cz the center z - * @param data the data to set - * @param radius the radius - * @param height the height of the cyl - * @param filled filled or not - */ - public void setCylinder(int cx, int cy, int cz, T data, double radius, int height, boolean filled) { - setCylinder(cx, cy, cz, data, radius, radius, height, filled); - } - - /** - * Set a cylinder in the mantle - * - * @param cx the center x - * @param cy the base y - * @param cz the center z - * @param data the data to set - * @param radiusX the x radius - * @param radiusZ the z radius - * @param height the height of this cyl - * @param filled filled or hollow? - */ - public void setCylinder(int cx, int cy, int cz, T data, double radiusX, double radiusZ, int height, boolean filled) { - int affected = 0; - radiusX += 0.5; - radiusZ += 0.5; - - if (height == 0) { - return; - } else if (height < 0) { - height = -height; - cy = cy - height; - } - - if (cy < 0) { - cy = 0; - } else if (cy + height - 1 > getMantle().getWorldHeight()) { - height = getMantle().getWorldHeight() - cy + 1; - } - - final double invRadiusX = 1 / radiusX; - final double invRadiusZ = 1 / radiusZ; - final int ceilRadiusX = (int) Math.ceil(radiusX); - final int ceilRadiusZ = (int) Math.ceil(radiusZ); - double nextXn = 0; - - forX: - for (int x = 0; x <= ceilRadiusX; ++x) { - final double xn = nextXn; - nextXn = (x + 1) * invRadiusX; - double nextZn = 0; - for (int z = 0; z <= ceilRadiusZ; ++z) { - final double zn = nextZn; - nextZn = (z + 1) * invRadiusZ; - double distanceSq = lengthSq(xn, zn); - - if (distanceSq > 1) { - if (z == 0) { - break forX; - } - - break; - } - - if (!filled) { - if (lengthSq(nextXn, zn) <= 1 && lengthSq(xn, nextZn) <= 1) { - continue; - } - } - - for (int y = 0; y < height; ++y) { - setData(cx + x, cy + y, cz + z, data); - setData(cx + -x, cy + y, cz + z, data); - setData(cx + x, cy + y, cz + -z, data); - setData(cx + -x, cy + y, cz + -z, data); - } - } - } - } - - public void set(IrisPosition pos, T data) { - try { - setData(pos.getX(), pos.getY(), pos.getZ(), data); - } catch (Throwable e) { - Iris.error("No set? " + data.toString() + " for " + pos.toString()); - } - } - - public void set(List positions, T data) { - for (IrisPosition i : positions) { - set(i, data); - } - } - - public void set(Set positions, T data) { - for (IrisPosition i : positions) { - set(i, data); - } - } - - public void setConsumer(Set positions, Function3 data) { - for (IrisPosition i : positions) { - set(i, data.apply(i.getX(), i.getY(), i.getZ())); - } - } - - public boolean isWithin(Vector pos) { - return isWithin(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); - } - - public boolean isWithin(int x, int y, int z) { - int cx = x >> 4; - int cz = z >> 4; - - if (y < 0 || y >= mantle.getWorldHeight()) { - return false; - } - - return cx >= this.x - radius && cx <= this.x + radius - && cz >= this.z - radius && cz <= this.z + radius; - } - - @Override - public void close() { - var iterator = cachedChunks.values().iterator(); - while (iterator.hasNext()) { - iterator.next().release(); - iterator.remove(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleCarvingComponent.java deleted file mode 100644 index dcaf50bee..000000000 --- a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleCarvingComponent.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.mantle.components; - -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.mantle.ComponentFlag; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.engine.mantle.IrisMantleComponent; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisCarving; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.mantle.flag.ReservedFlag; -import com.volmit.iris.util.math.RNG; - -@ComponentFlag(ReservedFlag.CARVED) -public class MantleCarvingComponent extends IrisMantleComponent { - public MantleCarvingComponent(EngineMantle engineMantle) { - super(engineMantle, ReservedFlag.CARVED, 0); - } - - @Override - public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { - RNG rng = new RNG(Cache.key(x, z) + seed()); - int xxx = 8 + (x << 4); - int zzz = 8 + (z << 4); - IrisRegion region = getComplex().getRegionStream().get(xxx, zzz); - IrisBiome biome = getComplex().getTrueBiomeStream().get(xxx, zzz); - carve(writer, rng, x, z, region, biome); - } - - @ChunkCoordinates - private void carve(MantleWriter writer, RNG rng, int cx, int cz, IrisRegion region, IrisBiome biome) { - carve(getDimension().getCarving(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz); - carve(biome.getCarving(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz); - carve(region.getCarving(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz); - } - - @ChunkCoordinates - private void carve(IrisCarving carving, MantleWriter writer, RNG rng, int cx, int cz) { - carving.doCarving(writer, rng, getEngineMantle().getEngine(), cx << 4, -1, cz << 4, 0); - } - - protected int computeRadius() { - var dimension = getDimension(); - int max = 0; - - max = Math.max(max, dimension.getCarving().getMaxRange(getData(), 0)); - - for (var i : dimension.getAllRegions(this::getData)) { - max = Math.max(max, i.getCarving().getMaxRange(getData(), 0)); - } - - for (var i : dimension.getAllBiomes(this::getData)) { - max = Math.max(max, i.getCarving().getMaxRange(getData(), 0)); - } - - return max; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleFluidBodyComponent.java b/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleFluidBodyComponent.java deleted file mode 100644 index ee12b0d6d..000000000 --- a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleFluidBodyComponent.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.mantle.components; - -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.mantle.ComponentFlag; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.engine.mantle.IrisMantleComponent; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisFluidBodies; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.mantle.flag.ReservedFlag; -import com.volmit.iris.util.math.RNG; - -@ComponentFlag(ReservedFlag.FLUID_BODIES) -public class MantleFluidBodyComponent extends IrisMantleComponent { - public MantleFluidBodyComponent(EngineMantle engineMantle) { - super(engineMantle, ReservedFlag.FLUID_BODIES, 0); - } - - @Override - public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { - RNG rng = new RNG(Cache.key(x, z) + seed() + 405666); - int xxx = 8 + (x << 4); - int zzz = 8 + (z << 4); - IrisRegion region = getComplex().getRegionStream().get(xxx, zzz); - IrisBiome biome = getComplex().getTrueBiomeStream().get(xxx, zzz); - generate(writer, rng, x, z, region, biome); - } - - @ChunkCoordinates - private void generate(MantleWriter writer, RNG rng, int cx, int cz, IrisRegion region, IrisBiome biome) { - generate(getDimension().getFluidBodies(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz); - generate(biome.getFluidBodies(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz); - generate(region.getFluidBodies(), writer, new RNG((rng.nextLong() * cx) + 490495 + cz), cx, cz); - } - - @ChunkCoordinates - private void generate(IrisFluidBodies bodies, MantleWriter writer, RNG rng, int cx, int cz) { - bodies.generate(writer, rng, getEngineMantle().getEngine(), cx << 4, -1, cz << 4); - } - - protected int computeRadius() { - int max = 0; - - max = Math.max(max, getDimension().getFluidBodies().getMaxRange(getData())); - - for (IrisRegion i : getDimension().getAllRegions(this::getData)) { - max = Math.max(max, i.getFluidBodies().getMaxRange(getData())); - } - - for (IrisBiome i : getDimension().getAllBiomes(this::getData)) { - max = Math.max(max, i.getFluidBodies().getMaxRange(getData())); - } - - return max; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleJigsawComponent.java b/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleJigsawComponent.java deleted file mode 100644 index fe4e45f0a..000000000 --- a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleJigsawComponent.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.mantle.components; - -import com.volmit.iris.engine.jigsaw.PlannedStructure; -import com.volmit.iris.engine.mantle.ComponentFlag; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.engine.mantle.IrisMantleComponent; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.mantle.flag.ReservedFlag; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.slices.container.JigsawStructuresContainer; -import com.volmit.iris.util.noise.CNG; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -@ComponentFlag(ReservedFlag.JIGSAW) -public class MantleJigsawComponent extends IrisMantleComponent { - private final CNG cng; - - public MantleJigsawComponent(EngineMantle engineMantle) { - super(engineMantle, ReservedFlag.JIGSAW, 1); - cng = NoiseStyle.STATIC.create(new RNG(jigsaw())); - } - - @Override - public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { - int xxx = 8 + (x << 4); - int zzz = 8 + (z << 4); - IrisRegion region = getComplex().getRegionStream().get(xxx, zzz); - IrisBiome biome = getComplex().getTrueBiomeStream().get(xxx, zzz); - generateJigsaw(writer, x, z, biome, region); - } - - @ChunkCoordinates - private void generateJigsaw(MantleWriter writer, int x, int z, IrisBiome biome, IrisRegion region) { - long seed = cng.fit(Integer.MIN_VALUE, Integer.MAX_VALUE, x, z); - - if (getDimension().getStronghold() != null) { - List poss = getDimension().getStrongholds(seed()); - - if (poss != null) { - for (Position2 pos : poss) { - if (x == pos.getX() >> 4 && z == pos.getZ() >> 4) { - IrisJigsawStructure structure = getData().getJigsawStructureLoader().load(getDimension().getStronghold()); - place(writer, pos.toIris(), structure, new RNG(seed), true); - return; - } - } - } - } - - KSet cachedRegions = new KSet<>(); - KMap> cache = new KMap<>(); - KMap distanceCache = new KMap<>(); - boolean placed = placeStructures(writer, seed, x, z, biome.getJigsawStructures(), cachedRegions, cache, distanceCache); - if (!placed) - placed = placeStructures(writer, seed, x, z, region.getJigsawStructures(), cachedRegions, cache, distanceCache); - if (!placed) - placeStructures(writer, seed, x, z, getDimension().getJigsawStructures(), cachedRegions, cache, distanceCache); - } - - @ChunkCoordinates - private boolean placeStructures(MantleWriter writer, long seed, int x, int z, KList structures, - KSet cachedRegions, KMap> cache, KMap distanceCache) { - IrisJigsawStructurePlacement i = pick(structures, seed, x, z); - try { - if (i == null || checkMinDistances(i.collectMinDistances(), x, z, cachedRegions, cache, distanceCache)) - return false; - } catch (Throwable ignored) {} - RNG rng = new RNG(seed); - IrisPosition position = new IrisPosition((x << 4) + rng.nextInt(15), 0, (z << 4) + rng.nextInt(15)); - IrisJigsawStructure structure = getData().getJigsawStructureLoader().load(i.getStructure()); - return place(writer, position, structure, rng, false); - } - - @ChunkCoordinates - private boolean checkMinDistances(KMap minDistances, int x, int z, KSet cachedRegions, KMap> cache, KMap distanceCache) { - int range = 0; - for (int d : minDistances.values()) - range = Math.max(range, d); - - for (int xx = -range; xx <= range; xx++) { - for (int zz = -range; zz <= range; zz++) { - Position2 pos = new Position2((xx + x) >> 5, (zz + z) >> 5); - if (cachedRegions.contains(pos)) continue; - cachedRegions.add(pos); - JigsawStructuresContainer container = getMantle().get(pos.getX(), 0, pos.getZ(), JigsawStructuresContainer.class); - if (container == null) continue; - for (String key : container.getStructures()) { - cache.computeIfAbsent(key, k -> new KSet<>()).addAll(container.getPositions(key)); - } - } - } - Position2 pos = new Position2(x, z); - for (String structure : minDistances.keySet()) { - if (!cache.containsKey(structure)) continue; - double minDist = minDistances.get(structure); - minDist = minDist * minDist; - for (Position2 sPos : cache.get(structure)) { - double dist = distanceCache.computeIfAbsent(sPos, position2 -> position2.distance(pos)); - if (minDist > dist) return true; - } - } - return false; - } - - @ChunkCoordinates - public IrisJigsawStructure guess(int x, int z) { - // todo The guess doesnt bring into account that the placer may return -1 - // todo doesnt bring skipped placements into account - long seed = cng.fit(Integer.MIN_VALUE, Integer.MAX_VALUE, x, z); - IrisBiome biome = getEngineMantle().getEngine().getSurfaceBiome((x << 4) + 8, (z << 4) + 8); - IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8); - - if (getDimension().getStronghold() != null) { - List poss = getDimension().getStrongholds(seed()); - - if (poss != null) { - for (Position2 pos : poss) { - if (x == pos.getX() >> 4 && z == pos.getZ() >> 4) { - return getData().getJigsawStructureLoader().load(getDimension().getStronghold()); - } - } - } - } - - IrisJigsawStructurePlacement i = pick(biome.getJigsawStructures(), seed, x, z); - if (i == null) i = pick(region.getJigsawStructures(), seed, x, z); - if (i == null) i = pick(getDimension().getJigsawStructures(), seed, x, z); - return i != null ? getData().getJigsawStructureLoader().load(i.getStructure()) : null; - } - - @Nullable - @ChunkCoordinates - private IrisJigsawStructurePlacement pick(List structures, long seed, int x, int z) { - return IRare.pick(structures.stream() - .filter(p -> p.shouldPlace(getData(), getDimension().getJigsawStructureDivisor(), jigsaw(), x, z)) - .toList(), new RNG(seed).nextDouble()); - } - - @BlockCoordinates - private boolean place(MantleWriter writer, IrisPosition position, IrisJigsawStructure structure, RNG rng, boolean forcePlace) { - if (structure == null || structure.getDatapackStructures().isNotEmpty()) return false; - return new PlannedStructure(structure, position, rng, forcePlace).place(writer, getMantle(), writer.getEngine()); - } - - private long jigsaw() { - return getEngineMantle().getEngine().getSeedManager().getJigsaw(); - } - - protected int computeRadius() { - var dimension = getDimension(); - - KSet structures = new KSet<>(); - if (dimension.getStronghold() != null) { - structures.add(dimension.getStronghold()); - } - - for (var placement : dimension.getJigsawStructures()) { - structures.add(placement.getStructure()); - } - for (var region : dimension.getAllRegions(this::getData)) { - for (var placement : region.getJigsawStructures()) { - structures.add(placement.getStructure()); - } - } - for (var biome : dimension.getAllBiomes(this::getData)) { - for (var placement : biome.getJigsawStructures()) { - structures.add(placement.getStructure()); - } - } - - int max = 0; - for (var structure : structures) { - max = Math.max(max, getData().getJigsawStructureLoader().load(structure).getMaxDimension()); - } - return max; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleObjectComponent.java deleted file mode 100644 index 920bb9b35..000000000 --- a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleObjectComponent.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.mantle.components; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.mantle.ComponentFlag; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.engine.mantle.IrisMantleComponent; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.mantle.flag.ReservedFlag; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterStructurePOI; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.noise.NoiseType; -import com.volmit.iris.util.parallel.BurstExecutor; -import org.bukkit.util.BlockVector; - -import java.io.IOException; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - -@ComponentFlag(ReservedFlag.OBJECT) -public class MantleObjectComponent extends IrisMantleComponent { - - public MantleObjectComponent(EngineMantle engineMantle) { - super(engineMantle, ReservedFlag.OBJECT, 1); - } - - @Override - public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { - RNG rng = applyNoise(x, z, Cache.key(x, z) + seed()); - int xxx = 8 + (x << 4); - int zzz = 8 + (z << 4); - IrisRegion region = getComplex().getRegionStream().get(xxx, zzz); - IrisBiome biome = getComplex().getTrueBiomeStream().get(xxx, zzz); - placeObjects(writer, rng, x, z, biome, region); - } - - private RNG applyNoise(int x, int z, long seed) { - CNG noise = CNG.signatureFast(new RNG(seed), NoiseType.WHITE, NoiseType.GLOB); - return new RNG((long) (seed * noise.noise(x, z))); - } - - @ChunkCoordinates - private void placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome biome, IrisRegion region) { - for (IrisObjectPlacement i : biome.getSurfaceObjects()) { - if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { - try { - placeObject(writer, rng, x << 4, z << 4, i); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Failed to place objects in the following biome: " + biome.getName()); - Iris.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); - Iris.error("Are these objects missing?"); - e.printStackTrace(); - } - } - } - - for (IrisObjectPlacement i : region.getSurfaceObjects()) { - if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { - try { - placeObject(writer, rng, x << 4, z << 4, i); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Failed to place objects in the following region: " + region.getName()); - Iris.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); - Iris.error("Are these objects missing?"); - e.printStackTrace(); - } - } - } - } - - @BlockCoordinates - private void placeObject(MantleWriter writer, RNG rng, int x, int z, IrisObjectPlacement objectPlacement) { - for (int i = 0; i < objectPlacement.getDensity(rng, x, z, getData()); i++) { - IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng)); - if (v == null) { - return; - } - int xx = rng.i(x, x + 15); - int zz = rng.i(z, z + 15); - int id = rng.i(0, Integer.MAX_VALUE); - v.place(xx, -1, zz, writer, objectPlacement, rng, (b, data) -> { - writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id); - if (objectPlacement.isDolphinTarget() && objectPlacement.isUnderwater() && B.isStorageChest(data)) { - writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); - } - }, null, getData()); - } - } - - @BlockCoordinates - private Set guessPlacedKeys(RNG rng, int x, int z, IrisObjectPlacement objectPlacement) { - Set f = new KSet<>(); - for (int i = 0; i < objectPlacement.getDensity(rng, x, z, getData()); i++) { - IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng)); - if (v == null) { - continue; - } - - f.add(v.getLoadKey()); - } - - return f; - } - - public Set guess(int x, int z) { - // todo The guess doesnt bring into account that the placer may return -1 - RNG rng = applyNoise(x, z, Cache.key(x, z) + seed()); - IrisBiome biome = getEngineMantle().getEngine().getSurfaceBiome((x << 4) + 8, (z << 4) + 8); - IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8); - Set v = new KSet<>(); - for (IrisObjectPlacement i : biome.getSurfaceObjects()) { - if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { - v.addAll(guessPlacedKeys(rng, x, z, i)); - } - } - - for (IrisObjectPlacement i : region.getSurfaceObjects()) { - if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { - v.addAll(guessPlacedKeys(rng, x, z, i)); - } - } - - return v; - } - - protected int computeRadius() { - var dimension = getDimension(); - - AtomicInteger xg = new AtomicInteger(); - AtomicInteger zg = new AtomicInteger(); - - KSet objects = new KSet<>(); - KMap> scalars = new KMap<>(); - for (var region : dimension.getAllRegions(this::getData)) { - for (var j : region.getObjects()) { - if (j.getScale().canScaleBeyond()) { - scalars.put(j.getScale(), j.getPlace()); - } else { - objects.addAll(j.getPlace()); - } - } - } - for (var biome : dimension.getAllBiomes(this::getData)) { - for (var j : biome.getObjects()) { - if (j.getScale().canScaleBeyond()) { - scalars.put(j.getScale(), j.getPlace()); - } else { - objects.addAll(j.getPlace()); - } - } - } - - BurstExecutor e = getEngineMantle().getTarget().getBurster().burst(objects.size()); - KMap sizeCache = new KMap<>(); - for (String i : objects) { - e.queue(() -> { - try { - BlockVector bv = sizeCache.computeIfAbsent(i, (k) -> { - try { - return IrisObject.sampleSize(getData().getObjectLoader().findFile(i)); - } catch (IOException ex) { - Iris.reportError(ex); - ex.printStackTrace(); - } - - return null; - }); - - if (bv == null) { - throw new RuntimeException(); - } - - if (Math.max(bv.getBlockX(), bv.getBlockZ()) > 128) { - Iris.warn("Object " + i + " has a large size (" + bv + ") and may increase memory usage!"); - } - - synchronized (xg) { - xg.getAndSet(Math.max(bv.getBlockX(), xg.get())); - } - - synchronized (zg) { - zg.getAndSet(Math.max(bv.getBlockZ(), zg.get())); - } - } catch (Throwable ed) { - Iris.reportError(ed); - - } - }); - } - - for (Map.Entry> entry : scalars.entrySet()) { - double ms = entry.getKey().getMaximumScale(); - for (String j : entry.getValue()) { - e.queue(() -> { - try { - BlockVector bv = sizeCache.computeIfAbsent(j, (k) -> { - try { - return IrisObject.sampleSize(getData().getObjectLoader().findFile(j)); - } catch (IOException ioException) { - Iris.reportError(ioException); - ioException.printStackTrace(); - } - - return null; - }); - - if (bv == null) { - throw new RuntimeException(); - } - - if (Math.max(bv.getBlockX(), bv.getBlockZ()) > 128) { - Iris.warn("Object " + j + " has a large size (" + bv + ") and may increase memory usage! (Object scaled up to " + Form.pc(ms, 2) + ")"); - } - - synchronized (xg) { - xg.getAndSet((int) Math.max(Math.ceil(bv.getBlockX() * ms), xg.get())); - } - - synchronized (zg) { - zg.getAndSet((int) Math.max(Math.ceil(bv.getBlockZ() * ms), zg.get())); - } - } catch (Throwable ee) { - Iris.reportError(ee); - - } - }); - } - } - - e.complete(); - return Math.max(xg.get(), zg.get()); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/mode/ModeOverworld.java b/core/src/main/java/com/volmit/iris/engine/mode/ModeOverworld.java deleted file mode 100644 index c3a04ba73..000000000 --- a/core/src/main/java/com/volmit/iris/engine/mode/ModeOverworld.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.mode; - -import com.volmit.iris.engine.actuator.IrisBiomeActuator; -import com.volmit.iris.engine.actuator.IrisDecorantActuator; -import com.volmit.iris.engine.actuator.IrisTerrainNormalActuator; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineMode; -import com.volmit.iris.engine.framework.EngineStage; -import com.volmit.iris.engine.framework.IrisEngineMode; -import com.volmit.iris.engine.modifier.*; -import org.bukkit.block.data.BlockData; - -public class ModeOverworld extends IrisEngineMode implements EngineMode { - public ModeOverworld(Engine engine) { - super(engine); - var terrain = new IrisTerrainNormalActuator(getEngine()); - var biome = new IrisBiomeActuator(getEngine()); - var decorant = new IrisDecorantActuator(getEngine()); - var cave = new IrisCarveModifier(getEngine()); - var post = new IrisPostModifier(getEngine()); - var deposit = new IrisDepositModifier(getEngine()); - var perfection = new IrisPerfectionModifier(getEngine()); - var custom = new IrisCustomModifier(getEngine()); - EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c); - EngineStage sGenMatter = (x, z, k, p, m, c) -> generateMatter(x >> 4, z >> 4, m, c); - EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c); - EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c); - EngineStage sCave = (x, z, k, p, m, c) -> cave.modify(x >> 4, z >> 4, k, m, c); - EngineStage sDeposit = (x, z, k, p, m, c) -> deposit.modify(x, z, k, m, c); - EngineStage sPost = (x, z, k, p, m, c) -> post.modify(x, z, k, m, c); - EngineStage sInsertMatter = (x, z, K, p, m, c) -> getMantle().insertMatter(x >> 4, z >> 4, BlockData.class, K, m); - EngineStage sPerfection = (x, z, k, p, m, c) -> perfection.modify(x, z, k, m, c); - EngineStage sCustom = (x, z, k, p, m, c) -> custom.modify(x, z, k, m, c); - - registerStage(burst( - sGenMatter, - sTerrain - )); - registerStage(burst( - sCave, - sPost - )); - registerStage(burst( - sDeposit, - sInsertMatter, - sDecorant - )); - registerStage(sPerfection); - registerStage(sCustom); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/com/volmit/iris/engine/modifier/IrisCarveModifier.java deleted file mode 100644 index 678dbcd3b..000000000 --- a/core/src/main/java/com/volmit/iris/engine/modifier/IrisCarveModifier.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.modifier; - -import com.volmit.iris.engine.actuator.IrisDecorantActuator; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedModifier; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.function.Consumer4; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.mantle.MantleChunk; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.matter.slices.MarkerMatter; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import lombok.Data; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; - -public class IrisCarveModifier extends EngineAssignedModifier { - private final RNG rng; - private final BlockData AIR = Material.CAVE_AIR.createBlockData(); - private final BlockData LAVA = Material.LAVA.createBlockData(); - private final IrisDecorantActuator decorant; - - public IrisCarveModifier(Engine engine) { - super(engine, "Carve"); - rng = new RNG(getEngine().getSeedManager().getCarve()); - decorant = new IrisDecorantActuator(engine); - } - - @Override - @ChunkCoordinates - public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - Mantle mantle = getEngine().getMantle().getMantle(); - MantleChunk mc = mantle.getChunk(x, z).use(); - KMap> positions = new KMap<>(); - KMap walls = new KMap<>(); - Consumer4 iterator = (xx, yy, zz, c) -> { - if (c == null) { - return; - } - - if (yy >= getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() || yy <= 0) { // Yes, skip bedrock - return; - } - - int rx = xx & 15; - int rz = zz & 15; - - BlockData current = output.get(rx, yy, rz); - - if (B.isFluid(current)) { - return; - } - - positions.computeIfAbsent(Cache.key(rx, rz), (k) -> new KList<>()).qadd(yy); - - //todo: Fix chunk decoration not working on chunk's border - - if (rz < 15 && mc.get(xx, yy, zz + 1, MatterCavern.class) == null) { - walls.put(new IrisPosition(rx, yy, rz + 1), c); - } - - if (rx < 15 && mc.get(xx + 1, yy, zz, MatterCavern.class) == null) { - walls.put(new IrisPosition(rx + 1, yy, rz), c); - } - - if (rz > 0 && mc.get(xx, yy, zz - 1, MatterCavern.class) == null) { - walls.put(new IrisPosition(rx, yy, rz - 1), c); - } - - if (rx > 0 && mc.get(xx - 1, yy, zz, MatterCavern.class) == null) { - walls.put(new IrisPosition(rx - 1, yy, rz), c); - } - - if (current.getMaterial().isAir()) { - return; - } - - if (c.isWater()) { - output.set(rx, yy, rz, context.getFluid().get(rx, rz)); - } else if (c.isLava()) { - output.set(rx, yy, rz, LAVA); - } else { - if (getEngine().getDimension().getCaveLavaHeight() > yy) { - output.set(rx, yy, rz, LAVA); - } else { - output.set(rx, yy, rz, AIR); - } - } - }; - - mc.iterate(MatterCavern.class, iterator); - - walls.forEach((i, v) -> { - IrisBiome biome = v.getCustomBiome().isEmpty() - ? getEngine().getCaveBiome(i.getX() + (x << 4), i.getZ() + (z << 4)) - : getEngine().getData().getBiomeLoader().load(v.getCustomBiome()); - - if (biome != null) { - biome.setInferredType(InferredType.CAVE); - BlockData d = biome.getWall().get(rng, i.getX() + (x << 4), i.getY(), i.getZ() + (z << 4), getData()); - - if (d != null && B.isSolid(output.get(i.getX(), i.getY(), i.getZ())) && i.getY() <= context.getHeight().get(i.getX(), i.getZ())) { - output.set(i.getX(), i.getY(), i.getZ(), d); - } - } - }); - - positions.forEach((k, v) -> { - if (v.isEmpty()) { - return; - } - - int rx = Cache.keyX(k); - int rz = Cache.keyZ(k); - v.sort(Integer::compare); - CaveZone zone = new CaveZone(); - zone.setFloor(v.get(0)); - int buf = v.get(0) - 1; - - for (Integer i : v) { - if (i < 0 || i > getEngine().getHeight()) { - continue; - } - - if (i == buf + 1) { - buf = i; - zone.ceiling = buf; - } else if (zone.isValid(getEngine())) { - processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4)); - zone = new CaveZone(); - zone.setFloor(i); - buf = i; - } - } - - if (zone.isValid(getEngine())) { - processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4)); - } - }); - - getEngine().getMetrics().getDeposit().put(p.getMilliseconds()); - mc.release(); - } - - private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz) { - boolean decFloor = B.isSolid(output.getClosest(rx, zone.floor - 1, rz)); - boolean decCeiling = B.isSolid(output.getClosest(rx, zone.ceiling + 1, rz)); - int center = (zone.floor + zone.ceiling) / 2; - int thickness = zone.airThickness(); - String customBiome = ""; - - if (B.isDecorant(output.getClosest(rx, zone.ceiling + 1, rz))) { - output.set(rx, zone.ceiling + 1, rz, AIR); - } - - if (B.isDecorant(output.get(rx, zone.ceiling, rz))) { - output.set(rx, zone.ceiling, rz, AIR); - } - - if (M.r(1D / 16D)) { - mantle.set(xx, zone.ceiling, zz, MarkerMatter.CAVE_CEILING); - } - - if (M.r(1D / 16D)) { - mantle.set(xx, zone.floor, zz, MarkerMatter.CAVE_FLOOR); - } - - for (int i = zone.floor; i <= zone.ceiling; i++) { - MatterCavern cavernData = (MatterCavern) mc.getOrCreate(i >> 4).slice(MatterCavern.class) - .get(rx, i & 15, rz); - - if (cavernData != null && !cavernData.getCustomBiome().isEmpty()) { - customBiome = cavernData.getCustomBiome(); - break; - } - } - - IrisBiome biome = customBiome.isEmpty() - ? getEngine().getCaveBiome(xx, zz) - : getEngine().getData().getBiomeLoader().load(customBiome); - - if (biome == null) { - return; - } - - biome.setInferredType(InferredType.CAVE); - - KList blocks = biome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex()); - - for (int i = 0; i < zone.floor - 1; i++) { - if (!blocks.hasIndex(i)) { - break; - } - int y = zone.floor - i - 1; - - BlockData b = blocks.get(i); - BlockData down = output.get(rx, y, rz); - - if (!B.isSolid(down)) { - continue; - } - - if (B.isOre(down)) { - output.set(rx, y, rz, B.toDeepSlateOre(down, b)); - continue; - } - - output.set(rx, y, rz, blocks.get(i)); - } - - blocks = biome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex()); - - if (zone.ceiling + 1 < mantle.getWorldHeight()) { - for (int i = 0; i < zone.ceiling + 1; i++) { - if (!blocks.hasIndex(i)) { - break; - } - - BlockData b = blocks.get(i); - BlockData up = output.get(rx, zone.ceiling + i + 1, rz); - - if (!B.isSolid(up)) { - continue; - } - - if (B.isOre(up)) { - output.set(rx, zone.ceiling + i + 1, rz, B.toDeepSlateOre(up, b)); - continue; - } - - output.set(rx, zone.ceiling + i + 1, rz, b); - } - } - - for (IrisDecorator i : biome.getDecorators()) { - if (i.getPartOf().equals(IrisDecorationPart.NONE) && B.isSolid(output.get(rx, zone.getFloor() - 1, rz))) { - decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getFloor() - 1, zone.airThickness()); - } else if (i.getPartOf().equals(IrisDecorationPart.CEILING) && B.isSolid(output.get(rx, zone.getCeiling() + 1, rz))) { - decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getCeiling(), zone.airThickness()); - } - } - } - - @Data - public static class CaveZone { - private int ceiling = -1; - private int floor = -1; - - public int airThickness() { - return (ceiling - floor) - 1; - } - - public boolean isValid(Engine engine) { - return floor < ceiling && ceiling - floor >= 1 && floor >= 0 && ceiling <= engine.getHeight() && airThickness() > 0; - } - - public String toString() { - return floor + "-" + ceiling; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/modifier/IrisCustomModifier.java b/core/src/main/java/com/volmit/iris/engine/modifier/IrisCustomModifier.java deleted file mode 100644 index eca550cd2..000000000 --- a/core/src/main/java/com/volmit/iris/engine/modifier/IrisCustomModifier.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.volmit.iris.engine.modifier; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedModifier; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import org.bukkit.block.data.BlockData; -public class IrisCustomModifier extends EngineAssignedModifier { - public IrisCustomModifier(Engine engine) { - super(engine, "Custom"); - } - @Override - public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { - var mc = getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4); - if (!mc.isFlagged(MantleFlag.CUSTOM_ACTIVE)) return; - mc.use(); - - BurstExecutor burst = MultiBurst.burst.burst(output.getHeight()); - burst.setMulticore(multicore); - for (int y = 0; y < output.getHeight(); y++) { - int finalY = y; - burst.queue(() -> { - for (int rX = 0; rX < output.getWidth(); rX++) { - for (int rZ = 0; rZ < output.getDepth(); rZ++) { - BlockData b = output.get(rX, finalY, rZ); - if (!(b instanceof IrisCustomData d)) continue; - - mc.getOrCreate(finalY >> 4) - .slice(Identifier.class) - .set(rX, finalY & 15, rZ, d.getCustom()); - output.set(rX, finalY, rZ, d.getBase()); - } - } - }); - } - burst.complete(); - mc.release(); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/engine/modifier/IrisDepositModifier.java b/core/src/main/java/com/volmit/iris/engine/modifier/IrisDepositModifier.java deleted file mode 100644 index f6fcd52c0..000000000 --- a/core/src/main/java/com/volmit/iris/engine/modifier/IrisDepositModifier.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.modifier; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedModifier; -import com.volmit.iris.engine.object.*; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.HeightMap; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.mantle.MantleChunk; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; -import org.bukkit.util.BlockVector; - -public class IrisDepositModifier extends EngineAssignedModifier { - private final RNG rng; - - public IrisDepositModifier(Engine engine) { - super(engine, "Deposit"); - rng = new RNG(getEngine().getSeedManager().getDeposit()); - } - - @Override - public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - generateDeposits(output, Math.floorDiv(x, 16), Math.floorDiv(z, 16), multicore, context); - getEngine().getMetrics().getDeposit().put(p.getMilliseconds()); - } - - public void generateDeposits(Hunk terrain, int x, int z, boolean multicore, ChunkContext context) { - IrisRegion region = context.getRegion().get(7, 7); - IrisBiome biome = context.getBiome().get(7, 7); - BurstExecutor burst = burst().burst(multicore); - - long seed = x * 341873128712L + z * 132897987541L; - long mask = 0; - MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use(); - for (IrisDepositGenerator k : getDimension().getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); - } - - for (IrisDepositGenerator k : region.getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); - } - - for (IrisDepositGenerator k : biome.getDeposits()) { - long finalSeed = seed * ++mask; - burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context)); - } - burst.complete(); - chunk.release(); - } - - public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk data, RNG rng, int cx, int cz, boolean safe, ChunkContext context) { - generate(k, chunk, data, rng, cx, cz, safe, null, context); - } - - public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) { - if (k.getSpawnChance() < rng.d()) - return; - - for (int l = 0; l < rng.i(k.getMinPerChunk(), k.getMaxPerChunk() + 1); l++) { - if (k.getPerClumpSpawnChance() < rng.d()) - continue; - - IrisObject clump = k.getClump(getEngine(), rng, getData()); - - int dim = clump.getW(); - int min = dim / 2; - int max = (int) (16D - dim / 2D); - - if (min > max || min < 0 || max > 15) { - min = 6; - max = 9; - } - - int x = rng.i(min, max + 1); - int z = rng.i(min, max + 1); - int height = (he != null ? he.getHeight((cx << 4) + x, (cz << 4) + z) : (int) (Math.round( - context.getHeight().get(x, z) - ))) - 7; - - if (height <= 0) - continue; - - int minY = Math.max(0, k.getMinHeight()); - // TODO: WARNING HEIGHT - int maxY = Math.min(height, Math.min(getEngine().getHeight(), k.getMaxHeight())); - - if (minY >= maxY) - continue; - - int y = rng.i(minY, maxY + 1); - - if (y > k.getMaxHeight() || y < k.getMinHeight() || y > height - 2) - continue; - - for (BlockVector j : clump.getBlocks().keys()) { - int nx = j.getBlockX() + x; - int ny = j.getBlockY() + y; - int nz = j.getBlockZ() + z; - - if (ny > height || nx > 15 || nx < 0 || ny > getEngine().getHeight() || ny < 0 || nz < 0 || nz > 15) { - continue; - } - if (!k.isReplaceBedrock() && data.get(nx, ny, nz).getMaterial() == Material.BEDROCK) { - continue; - } - - if (chunk.get(nx, ny, nz, MatterCavern.class) == null) { - data.set(nx, ny, nz, B.toDeepSlateOre(data.get(nx, ny, nz), clump.getBlocks().get(j))); - } - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/modifier/IrisPerfectionModifier.java b/core/src/main/java/com/volmit/iris/engine/modifier/IrisPerfectionModifier.java deleted file mode 100644 index 9aa9085e4..000000000 --- a/core/src/main/java/com/volmit/iris/engine/modifier/IrisPerfectionModifier.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.modifier; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedModifier; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.bukkit.block.data.Bisected; -import org.bukkit.block.data.BlockData; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -public class IrisPerfectionModifier extends EngineAssignedModifier { - private static final BlockData AIR = B.get("AIR"); - private static final BlockData WATER = B.get("WATER"); - - public IrisPerfectionModifier(Engine engine) { - super(engine, "Perfection"); - } - - @Override - public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - AtomicBoolean changed = new AtomicBoolean(true); - int passes = 0; - AtomicInteger changes = new AtomicInteger(); - List surfaces = new ArrayList<>(); - List ceilings = new ArrayList<>(); - BurstExecutor burst = burst().burst(multicore); - while (changed.get()) { - passes++; - changed.set(false); - for (int i = 0; i < 16; i++) { - int finalI = i; - burst.queue(() -> { - for (int j = 0; j < 16; j++) { - surfaces.clear(); - ceilings.clear(); - int top = getHeight(output, finalI, j); - boolean inside = true; - surfaces.add(top); - - for (int k = top; k >= 0; k--) { - BlockData b = output.get(finalI, k, j); - boolean now = b != null && !(B.isAir(b) || B.isFluid(b)); - - if (now != inside) { - inside = now; - - if (inside) { - surfaces.add(k); - } else { - ceilings.add(k + 1); - } - } - } - - for (int k : surfaces) { - BlockData tip = output.get(finalI, k, j); - - if (tip == null) { - continue; - } - - boolean remove = false; - boolean remove2 = false; - - if (B.isDecorant(tip)) { - BlockData bel = output.get(finalI, k - 1, j); - - if (bel == null) { - remove = true; - } else if (!B.canPlaceOnto(tip.getMaterial(), bel.getMaterial())) { - remove = true; - } else if (bel instanceof Bisected) { - BlockData bb = output.get(finalI, k - 2, j); - if (bb == null || !B.canPlaceOnto(bel.getMaterial(), bb.getMaterial())) { - remove = true; - remove2 = true; - } - } - - if (remove) { - changed.set(true); - changes.getAndIncrement(); - output.set(finalI, k, j, AIR); - - if (remove2) { - changes.getAndIncrement(); - output.set(finalI, k - 1, j, AIR); - } - } - } - } - } - }); - } - } - - getEngine().getMetrics().getPerfection().put(p.getMilliseconds()); - } - - private int getHeight(Hunk output, int x, int z) { - for (int i = output.getHeight() - 1; i >= 0; i--) { - BlockData b = output.get(x, i, z); - - if (b != null && !B.isAir(b) && !B.isFluid(b)) { - return i; - } - } - - return 0; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/modifier/IrisPostModifier.java b/core/src/main/java/com/volmit/iris/engine/modifier/IrisPostModifier.java deleted file mode 100644 index aa80a05b6..000000000 --- a/core/src/main/java/com/volmit/iris/engine/modifier/IrisPostModifier.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.modifier; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedModifier; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.object.IrisSlopeClip; -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.Levelled; -import org.bukkit.block.data.MultipleFacing; -import org.bukkit.block.data.Waterlogged; -import org.bukkit.block.data.type.Slab; - -import java.util.concurrent.atomic.AtomicInteger; - -public class IrisPostModifier extends EngineAssignedModifier { - private static final BlockData AIR = B.get("AIR"); - private static final BlockData WATER = B.get("WATER"); - private final RNG rng; - - public IrisPostModifier(Engine engine) { - super(engine, "Post"); - rng = new RNG(getEngine().getSeedManager().getPost()); - } - - @Override - public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - AtomicInteger i = new AtomicInteger(); - AtomicInteger j = new AtomicInteger(); - Hunk sync = output.synchronize(); - for (i.set(0); i.get() < output.getWidth(); i.getAndIncrement()) { - for (j.set(0); j.get() < output.getDepth(); j.getAndIncrement()) { - int ii = i.get(); - int jj = j.get(); - post(ii, jj, sync, ii + x, jj + z, context); - } - } - - getEngine().getMetrics().getPost().put(p.getMilliseconds()); - } - - private void post(int currentPostX, int currentPostZ, Hunk currentData, int x, int z, ChunkContext context) { - int h = getEngine().getMantle().trueHeight(x, z); - int ha = getEngine().getMantle().trueHeight(x + 1, z); - int hb = getEngine().getMantle().trueHeight(x, z + 1); - int hc = getEngine().getMantle().trueHeight(x - 1, z); - int hd = getEngine().getMantle().trueHeight(x, z - 1); - - // Floating Nibs - int g = 0; - - if (h < 1) { - return; - } - - g += ha < h - 1 ? 1 : 0; - g += hb < h - 1 ? 1 : 0; - g += hc < h - 1 ? 1 : 0; - g += hd < h - 1 ? 1 : 0; - - if (g == 4 && isAir(x, h - 1, z, currentPostX, currentPostZ, currentData)) { - setPostBlock(x, h, z, AIR, currentPostX, currentPostZ, currentData); - - for (int i = h - 1; i > 0; i--) { - if (!isAir(x, i, z, currentPostX, currentPostZ, currentData)) { - h = i; - break; - } - } - } - - // Nibs - g = 0; - g += ha == h - 1 ? 1 : 0; - g += hb == h - 1 ? 1 : 0; - g += hc == h - 1 ? 1 : 0; - g += hd == h - 1 ? 1 : 0; - - if (g >= 4) { - BlockData bc = getPostBlock(x, h, z, currentPostX, currentPostZ, currentData); - BlockData b = getPostBlock(x, h + 1, z, currentPostX, currentPostZ, currentData); - Material m = bc.getMaterial(); - - if ((b.getMaterial().isOccluding() && b.getMaterial().isSolid())) { - if (m.isSolid()) { - setPostBlock(x, h, z, b, currentPostX, currentPostZ, currentData); - h--; - } - } - } else { - // Potholes - g = 0; - g += ha == h + 1 ? 1 : 0; - g += hb == h + 1 ? 1 : 0; - g += hc == h + 1 ? 1 : 0; - g += hd == h + 1 ? 1 : 0; - - if (g >= 4) { - BlockData ba = getPostBlock(x, ha, z, currentPostX, currentPostZ, currentData); - BlockData bb = getPostBlock(x, hb, z, currentPostX, currentPostZ, currentData); - BlockData bc = getPostBlock(x, hc, z, currentPostX, currentPostZ, currentData); - BlockData bd = getPostBlock(x, hd, z, currentPostX, currentPostZ, currentData); - g = 0; - g = B.isSolid(ba) ? g + 1 : g; - g = B.isSolid(bb) ? g + 1 : g; - g = B.isSolid(bc) ? g + 1 : g; - g = B.isSolid(bd) ? g + 1 : g; - - if (g >= 3) { - setPostBlock(x, h + 1, z, getPostBlock(x, h, z, currentPostX, currentPostZ, currentData), currentPostX, currentPostZ, currentData); - h++; - } - } - } - - // Wall Patcher - IrisBiome biome = context.getBiome().get(currentPostX, currentPostZ); - - if (getDimension().isPostProcessingWalls()) { - if (!biome.getWall().getPalette().isEmpty()) { - if (ha < h - 2 || hb < h - 2 || hc < h - 2 || hd < h - 2) { - boolean brokeGround = false; - int max = Math.abs(Math.max(h - ha, Math.max(h - hb, Math.max(h - hc, h - hd)))); - - for (int i = h; i > h - max; i--) { - BlockData d = biome.getWall().get(rng, x + i, i + h, z + i, getData()); - - if (d != null) { - if (isAirOrWater(x, i, z, currentPostX, currentPostZ, currentData)) { - if (brokeGround) { - break; - } - - continue; - } - - setPostBlock(x, i, z, d, currentPostX, currentPostZ, currentData); - brokeGround = true; - } - } - } - } - } - - // Slab - if (getDimension().isPostProcessingSlabs()) { - //@builder - if ((ha == h + 1 && isSolidNonSlab(x + 1, ha, z, currentPostX, currentPostZ, currentData)) - || (hb == h + 1 && isSolidNonSlab(x, hb, z + 1, currentPostX, currentPostZ, currentData)) - || (hc == h + 1 && isSolidNonSlab(x - 1, hc, z, currentPostX, currentPostZ, currentData)) - || (hd == h + 1 && isSolidNonSlab(x, hd, z - 1, currentPostX, currentPostZ, currentData))) - //@done - { - IrisSlopeClip sc = biome.getSlab().getSlopeCondition(); - BlockData d = sc.isValid(getComplex().getSlopeStream().get(x, z)) ? biome.getSlab().get(rng, x, h, z, getData()) : null; - - if (d != null) { - boolean cancel = B.isAir(d); - - if (d.getMaterial().equals(Material.SNOW) && h + 1 <= getDimension().getFluidHeight()) { - cancel = true; - } - - if (isSnowLayer(x, h, z, currentPostX, currentPostZ, currentData)) { - cancel = true; - } - - if (!cancel && isAirOrWater(x, h + 1, z, currentPostX, currentPostZ, currentData)) { - setPostBlock(x, h + 1, z, d, currentPostX, currentPostZ, currentData); - h++; - } - } - } - } - - // Waterlogging - BlockData b = getPostBlock(x, h, z, currentPostX, currentPostZ, currentData); - - if (b instanceof Waterlogged) { - Waterlogged ww = (Waterlogged) b.clone(); - boolean w = false; - - if (h <= getDimension().getFluidHeight() + 1) { - if (isWaterOrWaterlogged(x, h + 1, z, currentPostX, currentPostZ, currentData)) { - w = true; - } else if ((isWaterOrWaterlogged(x + 1, h, z, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x - 1, h, z, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x, h, z + 1, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x, h, z - 1, currentPostX, currentPostZ, currentData))) { - w = true; - } - } - - if (w != ww.isWaterlogged()) { - ww.setWaterlogged(w); - setPostBlock(x, h, z, ww, currentPostX, currentPostZ, currentData); - } - } else if (b.getMaterial().equals(Material.AIR) && h <= getDimension().getFluidHeight()) { - if ((isWaterOrWaterlogged(x + 1, h, z, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x - 1, h, z, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x, h, z + 1, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x, h, z - 1, currentPostX, currentPostZ, currentData))) { - setPostBlock(x, h, z, WATER, currentPostX, currentPostZ, currentData); - } - } - - // Foliage - b = getPostBlock(x, h + 1, z, currentPostX, currentPostZ, currentData); - - if (B.isVineBlock(b) && b instanceof MultipleFacing f) { - int finalH = h + 1; - - f.getAllowedFaces().forEach(face -> { - BlockData d = getPostBlock(x + face.getModX(), finalH + face.getModY(), z + face.getModZ(), currentPostX, currentPostZ, currentData); - f.setFace(face, !B.isAir(d) && !B.isVineBlock(d)); - }); - setPostBlock(x, h + 1, z, b, currentPostX, currentPostZ, currentData); - } - - if (B.isFoliage(b) || b.getMaterial().equals(Material.DEAD_BUSH)) { - Material onto = getPostBlock(x, h, z, currentPostX, currentPostZ, currentData).getMaterial(); - - if (!B.canPlaceOnto(b.getMaterial(), onto) && !B.isDecorant(b)) { - setPostBlock(x, h + 1, z, AIR, currentPostX, currentPostZ, currentData); - } - } - } - - public boolean isAir(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d.getMaterial().equals(Material.AIR) || d.getMaterial().equals(Material.CAVE_AIR); - } - - public boolean hasGravity(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d.getMaterial().equals(Material.SAND) || d.getMaterial().equals(Material.RED_SAND) || d.getMaterial().equals(Material.BLACK_CONCRETE_POWDER) || d.getMaterial().equals(Material.BLUE_CONCRETE_POWDER) || d.getMaterial().equals(Material.BROWN_CONCRETE_POWDER) || d.getMaterial().equals(Material.CYAN_CONCRETE_POWDER) || d.getMaterial().equals(Material.GRAY_CONCRETE_POWDER) || d.getMaterial().equals(Material.GREEN_CONCRETE_POWDER) || d.getMaterial().equals(Material.LIGHT_BLUE_CONCRETE_POWDER) || d.getMaterial().equals(Material.LIGHT_GRAY_CONCRETE_POWDER) || d.getMaterial().equals(Material.LIME_CONCRETE_POWDER) || d.getMaterial().equals(Material.MAGENTA_CONCRETE_POWDER) || d.getMaterial().equals(Material.ORANGE_CONCRETE_POWDER) || d.getMaterial().equals(Material.PINK_CONCRETE_POWDER) || d.getMaterial().equals(Material.PURPLE_CONCRETE_POWDER) || d.getMaterial().equals(Material.RED_CONCRETE_POWDER) || d.getMaterial().equals(Material.WHITE_CONCRETE_POWDER) || d.getMaterial().equals(Material.YELLOW_CONCRETE_POWDER); - } - - public boolean isSolid(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d.getMaterial().isSolid() && !B.isVineBlock(d); - } - - public boolean isSolidNonSlab(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d.getMaterial().isSolid() && !(d instanceof Slab); - } - - public boolean isAirOrWater(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d.getMaterial().equals(Material.WATER) || d.getMaterial().equals(Material.AIR) || d.getMaterial().equals(Material.CAVE_AIR); - } - - public boolean isSlab(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d instanceof Slab; - } - - public boolean isSnowLayer(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d.getMaterial().equals(Material.SNOW); - } - - public boolean isWater(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d.getMaterial().equals(Material.WATER); - } - - public boolean isWaterOrWaterlogged(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d.getMaterial().equals(Material.WATER) || (d instanceof Waterlogged && ((Waterlogged) d).isWaterlogged()); - } - - public boolean isLiquid(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - BlockData d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); - return d instanceof Levelled; - } - - public void setPostBlock(int x, int y, int z, BlockData d, int currentPostX, int currentPostZ, Hunk currentData) { - if (y < currentData.getHeight()) { - currentData.set(x & 15, y, z & 15, d); - } - } - - public BlockData getPostBlock(int x, int y, int z, int cpx, int cpz, Hunk h) { - BlockData b = h.getClosest(x & 15, y, z & 15); - - return b == null ? AIR : b; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IObjectLoot.java b/core/src/main/java/com/volmit/iris/engine/object/IObjectLoot.java deleted file mode 100644 index 6ee2ec882..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IObjectLoot.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.util.collection.KList; -import org.bukkit.block.data.BlockData; - -public interface IObjectLoot { - KList getFilter(); - KList getFilter(IrisData manager); - boolean isExact(); - String getName(); - int getWeight(); -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IObjectPlacer.java b/core/src/main/java/com/volmit/iris/engine/object/IObjectPlacer.java deleted file mode 100644 index 9e2183b1c..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IObjectPlacer.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.Engine; -import org.bukkit.block.data.BlockData; -import org.jetbrains.annotations.Nullable; - -public interface IObjectPlacer { - int getHighest(int x, int z, IrisData data); - - int getHighest(int x, int z, IrisData data, boolean ignoreFluid); - - void set(int x, int y, int z, BlockData d); - - BlockData get(int x, int y, int z); - - boolean isPreventingDecay(); - - boolean isCarved(int x, int y, int z); - - boolean isSolid(int x, int y, int z); - - boolean isUnderwater(int x, int z); - - int getFluidHeight(); - - boolean isDebugSmartBore(); - - void setTile(int xx, int yy, int zz, TileData tile); - - void setData(int xx, int yy, int zz, T data); - - @Nullable T getData(int xx, int yy, int zz, Class t); - - Engine getEngine(); -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IPostBlockAccess.java b/core/src/main/java/com/volmit/iris/engine/object/IPostBlockAccess.java deleted file mode 100644 index db1c74271..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IPostBlockAccess.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.util.collection.KList; -import org.bukkit.block.data.BlockData; -import org.bukkit.generator.ChunkGenerator.ChunkData; - -public interface IPostBlockAccess { - BlockData getPostBlock(int x, int y, int z, int currentPostX, int currentPostZ, ChunkData currentData); - - void setPostBlock(int x, int y, int z, BlockData d, int currentPostX, int currentPostZ, ChunkData currentData); - - int highestTerrainOrFluidBlock(int x, int z); - - int highestTerrainBlock(int x, int z); - - void updateHeight(int x, int z, int h); - - KList caveFloors(int x, int z); -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IRare.java b/core/src/main/java/com/volmit/iris/engine/object/IRare.java deleted file mode 100644 index f78931830..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IRare.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.stream.ProceduralStream; -import com.volmit.iris.util.stream.interpolation.Interpolated; - -import java.util.List; - -public interface IRare { - static ProceduralStream stream(ProceduralStream noise, List possibilities, boolean legacyRarity) { - return ProceduralStream.of(legacyRarity ? (x, z) -> pickLegacy(possibilities, noise.get(x, z)) : (x, z) -> pick(possibilities, noise.get(x, z)), - legacyRarity ? (x, y, z) -> pickLegacy(possibilities, noise.get(x, y, z)) : (x, y, z) -> pick(possibilities, noise.get(x, y, z)), - new Interpolated() { - @Override - public double toDouble(T t) { - return 0; - } - - @Override - public T fromDouble(double d) { - return null; - } - }); - } - - - static T pickSlowly(List possibilities, double noiseValue) { - if (possibilities.isEmpty()) { - return null; - } - - if (possibilities.size() == 1) { - return possibilities.get(0); - } - - KList rarityTypes = new KList<>(); - int totalRarity = 0; - for (T i : possibilities) { - totalRarity += IRare.get(i); - } - - for (T i : possibilities) { - rarityTypes.addMultiple(i, totalRarity / IRare.get(i)); - } - - return rarityTypes.get((int) (noiseValue * rarityTypes.last())); - } - - static T pick(List possibilities, double noiseValue) { - if (possibilities.isEmpty()) { - return null; - } - - if (possibilities.size() == 1) { - return possibilities.getFirst(); - } - - double total = 0; - for (T i : possibilities) { - total += 1d / i.getRarity(); - } - - double threshold = total * noiseValue; - double buffer = 0; - for (T i : possibilities) { - buffer += 1d / i.getRarity(); - if (buffer >= threshold) { - return i; - } - } - - return possibilities.getLast(); - } - - static T pickLegacy(List possibilities, double noiseValue) { - if (possibilities.isEmpty()) { - return null; - } - - if (possibilities.size() == 1) { - return possibilities.get(0); - } - int totalWeight = 0; // This is he baseline - int buffer = 0; - for (T i : possibilities) { // Im adding all of the rarity together - totalWeight += i.getRarity(); - } - double threshold = totalWeight * (possibilities.size() - 1) * noiseValue; - for (T i : possibilities) { - buffer += totalWeight - i.getRarity(); - - if (buffer >= threshold) { - return i; - } - } - return possibilities.get(possibilities.size() - 1); - } - - - static T pickOld(List possibilities, double noiseValue) { - if (possibilities.isEmpty()) { - return null; - } - - if (possibilities.size() == 1) { - return possibilities.get(0); - } - - double completeWeight = 0.0; - double highestWeight = 0.0; - - for (T item : possibilities) { - double weight = Math.max(item.getRarity(), 1); - highestWeight = Math.max(highestWeight, weight); - completeWeight += weight; - } - - double r = noiseValue * completeWeight; - double countWeight = 0.0; - - for (T item : possibilities) { - double weight = Math.max(highestWeight - Math.max(item.getRarity(), 1), 1); - countWeight += weight; - if (countWeight >= r) { - return item; - } - } - - return possibilities.get(possibilities.size() - 1); - } - - static int get(Object v) { - return v instanceof IRare ? Math.max(1, ((IRare) v).getRarity()) : 1; - } - - int getRarity(); -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisAttributeModifier.java b/core/src/main/java/com/volmit/iris/engine/object/IrisAttributeModifier.java deleted file mode 100644 index 6108680f6..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisAttributeModifier.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.attribute.Attributable; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeModifier; -import org.bukkit.attribute.AttributeModifier.Operation; -import org.bukkit.inventory.meta.ItemMeta; - -@Snippet("attribute-modifier") -@Accessors(chain = true) -@AllArgsConstructor -@NoArgsConstructor -@Desc("Represents an attribute modifier for an item or an entity. This allows you to create modifications to basic game attributes such as MAX_HEALTH or ARMOR_VALUE.") -@Data -public class IrisAttributeModifier { - @Required - @Desc("The Attribute type. This type is pulled from the game attributes. Zombie & Horse attributes will not work on non-zombie/horse entities.\nUsing an attribute on an item will have affects when held, or worn. There is no way to specify further granularity as the game picks this depending on the item type.") - private Attribute attribute = null; - - @MinNumber(2) - @Required - @Desc("The Attribute Name is used internally only for the game. This value should be unique to all other attributes applied to this item/entity. It is not shown in game.") - private String name = ""; - - @Desc("The application operation (add number is default). Add Number adds to the default value. \nAdd scalar_1 will multiply by 1 for example if the health is 20 and you multiply_scalar_1 by 0.5, the health will result in 30, not 10. Use negative values to achieve that.") - private Operation operation = Operation.ADD_NUMBER; - - @Desc("Minimum amount for this modifier. Iris randomly chooses an amount, this is the minimum it can choose randomly for this attribute.") - private double minAmount = 1; - - @Desc("Maximum amount for this modifier Iris randomly chooses an amount, this is the maximum it can choose randomly for this attribute.") - private double maxAmount = 1; - - @MinNumber(0) - @MaxNumber(1) - @Desc("The chance that this attribute is applied (0 to 1). If the chance is 0.5 (50%), then Iris will only apply this attribute 50% of the time.") - private double chance = 1; - - public void apply(RNG rng, ItemMeta meta) { - if (rng.nextDouble() < getChance()) { - meta.addAttributeModifier(getAttribute(), new AttributeModifier(getName(), getAmount(rng), getOperation())); - } - } - - public void apply(RNG rng, Attributable meta) { - if (rng.nextDouble() < getChance()) { - meta.getAttribute(getAttribute()).addModifier(new AttributeModifier(getName(), getAmount(rng), getOperation())); - } - } - - public double getAmount(RNG rng) { - return rng.d(getMinAmount(), getMaxAmount()); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java deleted file mode 100644 index 42caee79e..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java +++ /dev/null @@ -1,718 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.gui.components.RenderType; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.IrisComplex; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.DataProvider; -import com.volmit.iris.util.data.VanillaBiomeMap; -import com.volmit.iris.util.inventorygui.RandomColor; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Material; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; - -import java.awt.*; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a biome in iris. Biomes are placed inside of regions and hold objects.\nA biome consists of layers (block palletes), decorations, objects & generators.") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisBiome extends IrisRegistrant implements IRare { - private static final BlockData BARRIER = Material.BARRIER.createBlockData(); - private final transient AtomicCache> genCache = new AtomicCache<>(); - private final transient AtomicCache> genCacheMax = new AtomicCache<>(); - private final transient AtomicCache> genCacheMin = new AtomicCache<>(); - private final transient AtomicCache> surfaceObjectsCache = new AtomicCache<>(); - private final transient AtomicCache> carveObjectsCache = new AtomicCache<>(); - private final transient AtomicCache cacheColor = new AtomicCache<>(); - private final transient AtomicCache cacheColorObjectDensity = new AtomicCache<>(); - private final transient AtomicCache cacheColorDecoratorLoad = new AtomicCache<>(); - private final transient AtomicCache cacheColorLayerLoad = new AtomicCache<>(); - private final transient AtomicCache cacheColorDepositLoad = new AtomicCache<>(); - private final transient AtomicCache childrenCell = new AtomicCache<>(); - private final transient AtomicCache biomeGenerator = new AtomicCache<>(); - private final transient AtomicCache maxHeight = new AtomicCache<>(); - private final transient AtomicCache maxWithObjectHeight = new AtomicCache<>(); - private final transient AtomicCache realCarveBiome = new AtomicCache<>(); - private final transient AtomicCache> realChildren = new AtomicCache<>(); - private final transient AtomicCache> layerHeightGenerators = new AtomicCache<>(); - private final transient AtomicCache> layerSeaHeightGenerators = new AtomicCache<>(); - @MinNumber(2) - @Required - @Desc("This is the human readable name for this biome. This can and should be different than the file name. This is not used for loading biomes in other objects.") - private String name = "Subterranean Land"; - @ArrayType(min = 1, type = IrisBiomeCustom.class) - @Desc("If the biome type custom is defined, specify this") - private KList customDerivitives; - @Desc("Spawn Entities in this area over time. Iris will continually replenish these mobs just like vanilla does.") - @ArrayType(min = 1, type = String.class) - @RegistryListResource(IrisSpawner.class) - private KList entitySpawners = new KList<>(); - @ArrayType(min = 1, type = IrisEffect.class) - @Desc("Effects are ambient effects such as potion effects, random sounds, or even particles around each player. All of these effects are played via packets so two players won't see/hear each others effects.\nDue to performance reasons, effects will play around the player even if where the effect was played is no longer in the biome the player is in.") - private KList effects = new KList<>(); - @DependsOn({"biomeStyle", "biomeZoom", "biomeScatter"}) - @Desc("This changes the dispersion of the biome colors if multiple derivatives are chosen.") - private IrisGeneratorStyle biomeStyle = NoiseStyle.SIMPLEX.style(); - @ArrayType(min = 1, type = IrisBlockDrops.class) - @Desc("Define custom block drops for this biome") - private KList blockDrops = new KList<>(); - @Desc("Reference loot tables in this area") - private IrisLootReference loot = new IrisLootReference(); - @Desc("Layers no longer descend from the surface block, they descend from the max possible height the biome can produce (constant) creating mesa like layers.") - private boolean lockLayers = false; - @Desc("The max layers to iterate below the surface for locked layer biomes (mesa).") - private int lockLayersMax = 7; - @Desc("Carving configuration for the dimension") - private IrisCarving carving = new IrisCarving(); - @Desc("Configuration of fluid bodies such as rivers & lakes") - private IrisFluidBodies fluidBodies = new IrisFluidBodies(); - @MinNumber(1) - @MaxNumber(512) - @Desc("The rarity of this biome (integer)") - private int rarity = 1; - @Desc("A color for visualizing this biome with a color. I.e. #F13AF5. This will show up on the map.") - private String color = null; - @Required - @Desc("The raw derivative of this biome. This is required or the terrain will not properly generate. Use any vanilla biome type. Look in examples/biome-list.txt") - private Biome derivative = Biome.THE_VOID; - @Required - @Desc("Override the derivative when vanilla places structures to this derivative. This is useful for example if you have an ocean biome, but you have set the derivative to desert to get a brown-ish color. To prevent desert structures from spawning on top of your ocean, you can set your vanillaDerivative to ocean, to allow for vanilla structures. Not defining this value will simply select the derivative.") - private Biome vanillaDerivative = null; - @ArrayType(min = 1, type = Biome.class) - @Desc("You can instead specify multiple biome derivatives to randomly scatter colors in this biome") - private KList biomeScatter = new KList<>(); - @ArrayType(min = 1, type = Biome.class) - @Desc("Since 1.13 supports 3D biomes, you can add different derivative colors for anything above the terrain. (Think swampy tree leaves with a desert looking grass surface)") - private KList biomeSkyScatter = new KList<>(); - @DependsOn({"children"}) - @Desc("If this biome has children biomes, and the gen layer chooses one of this biomes children, how much smaller will it be (inside of this biome). Higher values means a smaller biome relative to this biome's size. Set higher than 1.0 and below 3.0 for best results.") - private double childShrinkFactor = 1.5; - @DependsOn({"children"}) - @Desc("If this biome has children biomes, and the gen layer chooses one of this biomes children, How will it be shaped?") - private IrisGeneratorStyle childStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @RegistryListResource(IrisBiome.class) - @ArrayType(min = 1, type = String.class) - @Desc("List any biome names (file names without.json) here as children. Portions of this biome can sometimes morph into their children. Iris supports cyclic relationships such as A > B > A > B. Iris will stop checking 9 biomes down the tree.") - private KList children = new KList<>(); - @ArrayType(min = 1, type = IrisJigsawStructurePlacement.class) - @Desc("Jigsaw structures") - private KList jigsawStructures = new KList<>(); - @RegistryListResource(IrisBiome.class) - @Desc("The carving biome. If specified the biome will be used when under a carving instead of this current biome.") - private String carvingBiome = ""; - @Desc("The default slab if iris decides to place a slab in this biome. Default is no slab.") - private IrisBiomePaletteLayer slab = new IrisBiomePaletteLayer().zero(); - @Desc("The default wall if iris decides to place a wall higher than 2 blocks (steep hills or possibly cliffs)") - private IrisBiomePaletteLayer wall = new IrisBiomePaletteLayer().zero(); - @Required - @ArrayType(type = IrisBiomePaletteLayer.class) - @Desc("This defines the layers of materials in this biome. Each layer has a palette and min/max height and some other properties. Usually a grassy/sandy layer then a dirt layer then a stone layer. Iris will fill in the remaining blocks below your layers with stone.") - private KList layers = new KList().qadd(new IrisBiomePaletteLayer()); - @Required - @ArrayType(type = IrisBiomePaletteLayer.class) - @Desc("This defines the layers of materials in this biome. Each layer has a palette and min/max height and some other properties. Usually a grassy/sandy layer then a dirt layer then a stone layer. Iris will fill in the remaining blocks below your layers with stone.") - private KList caveCeilingLayers = new KList().qadd(new IrisBiomePaletteLayer()); - @ArrayType(type = IrisBiomePaletteLayer.class) - @Desc("This defines the layers of materials in this biome. Each layer has a palette and min/max height and some other properties. Usually a grassy/sandy layer then a dirt layer then a stone layer. Iris will fill in the remaining blocks below your layers with stone.") - private KList seaLayers = new KList<>(); - @ArrayType(min = 1, type = IrisDecorator.class) - @Desc("Decorators are used for things like tall grass, bisected flowers, and even kelp or cactus (random heights)") - private KList decorators = new KList<>(); - @ArrayType(min = 1, type = IrisObjectPlacement.class) - @Desc("Objects define what schematics (iob files) iris will place in this biome") - private KList objects = new KList<>(); - @Required - @ArrayType(min = 1, type = IrisBiomeGeneratorLink.class) - @Desc("Generators for this biome. Multiple generators with different interpolation sizes will mix with other biomes how you would expect. This defines your biome height relative to the fluid height. Use negative for oceans.") - private KList generators = new KList().qadd(new IrisBiomeGeneratorLink()); - @ArrayType(min = 1, type = IrisDepositGenerator.class) - @Desc("Define biome deposit generators that add onto the existing regional and global deposit generators") - private KList deposits = new KList<>(); - private transient InferredType inferredType; - @Desc("Collection of ores to be generated") - @ArrayType(type = IrisOreGenerator.class, min = 1) - private KList ores = new KList<>(); - - public BlockData generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) { - if (ores.isEmpty()) { - return null; - } - BlockData b = null; - for (IrisOreGenerator i : ores) { - if (i.isGenerateSurface() != surface) - continue; - - b = i.generate(x, y, z, rng, data); - if (b != null) { - return b; - } - } - return null; - } - - public Biome getVanillaDerivative() { - return vanillaDerivative == null ? derivative : vanillaDerivative; - } - - public boolean isCustom() { - return getCustomDerivitives() != null && getCustomDerivitives().isNotEmpty(); - } - - public double getGenLinkMax(String loadKey, Engine engine) { - Integer v = genCacheMax.aquire(() -> - { - KMap l = new KMap<>(); - - for (IrisBiomeGeneratorLink i : getGenerators()) { - l.put(i.getGenerator(), i.getMax()); - - } - - return l; - }).get(loadKey); - - return v == null ? 0 : v; - } - - public double getGenLinkMin(String loadKey, Engine engine) { - Integer v = genCacheMin.aquire(() -> - { - KMap l = new KMap<>(); - - for (IrisBiomeGeneratorLink i : getGenerators()) { - l.put(i.getGenerator(), i.getMin()); - } - - return l; - }).get(loadKey); - - return v == null ? 0 : v; - } - - public IrisBiomeGeneratorLink getGenLink(String loadKey) { - return genCache.aquire(() -> - { - KMap l = new KMap<>(); - - for (IrisBiomeGeneratorLink i : getGenerators()) { - l.put(i.getGenerator(), i); - } - - return l; - }).get(loadKey); - } - - public IrisBiome getRealCarvingBiome(IrisData data) { - return realCarveBiome.aquire(() -> - { - IrisBiome biome = data.getBiomeLoader().load(getCarvingBiome()); - - if (biome == null) { - biome = this; - } - - return biome; - }); - } - - public KList getSurfaceObjects() { - return getSurfaceObjectsCache().aquire(() -> - { - KList o = getObjects().copy(); - - for (IrisObjectPlacement i : o.copy()) { - if (!i.getCarvingSupport().supportsSurface()) { - o.remove(i); - } - } - - return o; - }); - } - - public KList getCarvingObjects() { - return getCarveObjectsCache().aquire(() -> - { - KList o = getObjects().copy(); - - for (IrisObjectPlacement i : o.copy()) { - if (!i.getCarvingSupport().supportsCarving()) { - o.remove(i); - } - } - - return o; - }); - } - - public double getHeight(Engine xg, double x, double z, long seed) { - double height = 0; - - for (IrisBiomeGeneratorLink i : generators) { - height += i.getHeight(xg, x, z, seed); - } - - return Math.max(0, Math.min(height, xg.getHeight())); - } - - public CNG getBiomeGenerator(RNG random) { - return biomeGenerator.aquire(() -> - biomeStyle.create(random.nextParallelRNG(213949 + 228888 + getRarity() + getName().length()), getLoader())); - } - - public CNG getChildrenGenerator(RNG random, int sig, double scale) { - return childrenCell.aquire(() -> getChildStyle().create(random.nextParallelRNG(sig * 2137), getLoader()).bake().scale(scale).bake()); - } - - public KList generateLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { - if (isLockLayers()) { - return generateLockedLayers(wx, wz, random, maxDepth, height, rdata, complex); - } - - KList data = new KList<>(); - - if (maxDepth <= 0) { - return data; - } - - for (int i = 0; i < layers.size(); i++) { - CNG hgen = getLayerHeightGenerators(random, rdata).get(i); - double d = hgen.fit(layers.get(i).getMinHeight(), layers.get(i).getMaxHeight(), wx / layers.get(i).getZoom(), wz / layers.get(i).getZoom()); - - IrisSlopeClip sc = getLayers().get(i).getSlopeCondition(); - - if (!sc.isDefault()) { - if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { - d = 0; - } - } - - if (d <= 0) { - continue; - } - - for (int j = 0; j < d; j++) { - if (data.size() >= maxDepth) { - break; - } - - try { - data.add(getLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / layers.get(i).getZoom(), j, (wz - j) / layers.get(i).getZoom(), rdata)); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - if (data.size() >= maxDepth) { - break; - } - - if (dim.isExplodeBiomePalettes()) { - for (int j = 0; j < dim.getExplodeBiomePaletteSize(); j++) { - data.add(BARRIER); - - if (data.size() >= maxDepth) { - break; - } - } - } - } - - return data; - } - - public KList generateCeilingLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { - KList data = new KList<>(); - - if (maxDepth <= 0) { - return data; - } - - for (int i = 0; i < caveCeilingLayers.size(); i++) { - CNG hgen = getLayerHeightGenerators(random, rdata).get(i); - double d = hgen.fit(caveCeilingLayers.get(i).getMinHeight(), caveCeilingLayers.get(i).getMaxHeight(), wx / caveCeilingLayers.get(i).getZoom(), wz / caveCeilingLayers.get(i).getZoom()); - - if (d <= 0) { - continue; - } - - for (int j = 0; j < d; j++) { - if (data.size() >= maxDepth) { - break; - } - - try { - data.add(getCaveCeilingLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / caveCeilingLayers.get(i).getZoom(), j, (wz - j) / caveCeilingLayers.get(i).getZoom(), rdata)); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - if (data.size() >= maxDepth) { - break; - } - - if (dim.isExplodeBiomePalettes()) { - for (int j = 0; j < dim.getExplodeBiomePaletteSize(); j++) { - data.add(BARRIER); - - if (data.size() >= maxDepth) { - break; - } - } - } - } - - return data; - } - - public KList generateLockedLayers(double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex) { - KList data = new KList<>(); - KList real = new KList<>(); - int maxDepth = Math.min(maxDepthf, getLockLayersMax()); - if (maxDepth <= 0) { - return data; - } - - for (int i = 0; i < layers.size(); i++) { - CNG hgen = getLayerHeightGenerators(random, rdata).get(i); - double d = hgen.fit(layers.get(i).getMinHeight(), layers.get(i).getMaxHeight(), wx / layers.get(i).getZoom(), wz / layers.get(i).getZoom()); - - IrisSlopeClip sc = getLayers().get(i).getSlopeCondition(); - - if (!sc.isDefault()) { - if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { - d = 0; - } - } - - if (d <= 0) { - continue; - } - - for (int j = 0; j < d; j++) { - try { - data.add(getLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / layers.get(i).getZoom(), j, (wz - j) / layers.get(i).getZoom(), rdata)); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - } - - if (data.isEmpty()) { - return real; - } - - for (int i = 0; i < maxDepth; i++) { - int offset = (512 - height) - i; - int index = offset % data.size(); - real.add(data.get(Math.max(index, 0))); - } - - return real; - } - - public int getMaxHeight(Engine engine) { - return maxHeight.aquire(() -> - { - int maxHeight = 0; - - for (IrisBiomeGeneratorLink i : getGenerators()) { - maxHeight += i.getMax(); - } - - return maxHeight; - }); - } - - public int getMaxWithObjectHeight(IrisData data, Engine engine) { - return maxWithObjectHeight.aquire(() -> - { - int maxHeight = 0; - - for (IrisBiomeGeneratorLink i : getGenerators()) { - maxHeight += i.getMax(); - } - - int gg = 0; - - for (IrisObjectPlacement i : getObjects()) { - for (IrisObject j : data.getObjectLoader().loadAll(i.getPlace())) { - gg = Math.max(gg, j.getH()); - } - } - - return maxHeight + gg + 3; - }); - } - - public KList generateSeaLayers(double wx, double wz, RNG random, int maxDepth, IrisData rdata) { - KList data = new KList<>(); - - for (int i = 0; i < seaLayers.size(); i++) { - CNG hgen = getLayerSeaHeightGenerators(random, rdata).get(i); - int d = hgen.fit(seaLayers.get(i).getMinHeight(), seaLayers.get(i).getMaxHeight(), wx / seaLayers.get(i).getZoom(), wz / seaLayers.get(i).getZoom()); - - if (d < 0) { - continue; - } - - for (int j = 0; j < d; j++) { - if (data.size() >= maxDepth) { - break; - } - - try { - data.add(getSeaLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / seaLayers.get(i).getZoom(), j, (wz - j) / seaLayers.get(i).getZoom(), rdata)); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - if (data.size() >= maxDepth) { - break; - } - } - - return data; - } - - public KList getLayerHeightGenerators(RNG rng, IrisData rdata) { - return layerHeightGenerators.aquire(() -> - { - KList layerHeightGenerators = new KList<>(); - - int m = 7235; - - for (IrisBiomePaletteLayer i : getLayers()) { - layerHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), rdata)); - } - - return layerHeightGenerators; - }); - } - - public KList getLayerSeaHeightGenerators(RNG rng, IrisData data) { - return layerSeaHeightGenerators.aquire(() -> - { - KList layerSeaHeightGenerators = new KList<>(); - - int m = 7735; - - for (IrisBiomePaletteLayer i : getSeaLayers()) { - layerSeaHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data)); - } - - return layerSeaHeightGenerators; - }); - } - - public boolean isLand() { - if (inferredType == null) { - return true; - } - - return inferredType.equals(InferredType.LAND); - } - - public boolean isSea() { - if (inferredType == null) { - return false; - } - return inferredType.equals(InferredType.SEA); - } - - public boolean isAquatic() { - return isSea(); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean isShore() { - if (inferredType == null) { - return false; - } - return inferredType.equals(InferredType.SHORE); - } - - public Biome getSkyBiome(RNG rng, double x, double y, double z) { - if (biomeSkyScatter.size() == 1) { - return biomeSkyScatter.get(0); - } - - if (biomeSkyScatter.isEmpty()) { - return getGroundBiome(rng, x, y, z); - } - - return biomeSkyScatter.get(getBiomeGenerator(rng).fit(0, biomeSkyScatter.size() - 1, x, y, z)); - } - - public IrisBiomeCustom getCustomBiome(RNG rng, double x, double y, double z) { - if (customDerivitives.size() == 1) { - return customDerivitives.get(0); - } - - return customDerivitives.get(getBiomeGenerator(rng).fit(0, customDerivitives.size() - 1, x, y, z)); - } - - public KList getRealChildren(DataProvider g) { - return realChildren.aquire(() -> - { - KList realChildren = new KList<>(); - - for (String i : getChildren()) { - realChildren.add(g.getData().getBiomeLoader().load(i)); - } - - return realChildren; - }); - } - - public KList getAllChildren(DataProvider g, int limit) { - KSet m = new KSet<>(); - m.addAll(getChildren()); - limit--; - - if (limit > 0) { - for (String i : getChildren()) { - IrisBiome b = g.getData().getBiomeLoader().load(i); - m.addAll(b.getAllChildren(g, limit)); - } - } - - return new KList<>(m); - } - - //TODO: Test - public Biome getGroundBiome(RNG rng, double x, double y, double z) { - if (biomeScatter.isEmpty()) { - return getDerivative(); - } - - if (biomeScatter.size() == 1) { - return biomeScatter.get(0); - } - - return getBiomeGenerator(rng).fit(biomeScatter, x, y, z); - } - - public BlockData getSurfaceBlock(int x, int z, RNG rng, IrisData idm) { - if (getLayers().isEmpty()) { - return B.get("AIR"); - } - - return getLayers().get(0).get(rng, x, 0, z, idm); - } - - public Color getColor(Engine engine, RenderType type) { - switch (type) { - case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> { - return this.cacheColor.aquire(() -> { - if (this.color == null) { - RandomColor randomColor = new RandomColor(getName().hashCode()); - if (this.getVanillaDerivative() == null) { - Iris.warn("No vanilla biome found for " + getName()); - return new Color(randomColor.randomColor()); - } - RandomColor.Color col = VanillaBiomeMap.getColorType(this.getVanillaDerivative()); - RandomColor.Luminosity lum = VanillaBiomeMap.getColorLuminosity(this.getVanillaDerivative()); - RandomColor.SaturationType sat = VanillaBiomeMap.getColorSaturatiom(this.getVanillaDerivative()); - int newColorI = randomColor.randomColor(col, col == RandomColor.Color.MONOCHROME ? RandomColor.SaturationType.MONOCHROME : sat, lum); - - return new Color(newColorI); - } - - try { - return Color.decode(this.color); - } catch (NumberFormatException e) { - Iris.warn("Could not parse color \"" + this.color + "\" for biome " + getName()); - return new Color(new RandomColor(getName().hashCode()).randomColor()); - } - }); - } - case OBJECT_LOAD -> { - return cacheColorObjectDensity.aquire(() -> { - double density = 0; - - for (IrisObjectPlacement i : getObjects()) { - density += i.getDensity() * i.getChance(); - } - - return Color.getHSBColor(0.225f, (float) (density / engine.getMaxBiomeObjectDensity()), 1f); - }); - } - case DECORATOR_LOAD -> { - return cacheColorDecoratorLoad.aquire(() -> { - double density = 0; - - for (IrisDecorator i : getDecorators()) { - density += i.getChance() * Math.min(1, i.getStackMax()) * 256; - } - - return Color.getHSBColor(0.41f, (float) (density / engine.getMaxBiomeDecoratorDensity()), 1f); - }); - } - case LAYER_LOAD -> { - return cacheColorLayerLoad.aquire(() -> Color.getHSBColor(0.625f, (float) (getLayers().size() / engine.getMaxBiomeLayerDensity()), 1f)); - } - } - - return Color.black; - } - - @Override - public String getFolderName() { - return "biomes"; - } - - @Override - public String getTypeName() { - return "Biome"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustom.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustom.java deleted file mode 100644 index aa3859c83..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustom.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.nms.datapack.IDataFixer; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.json.JSONObject; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.awt.*; -import java.util.Locale; - -@Snippet("custom-biome") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("A custom biome, generated through a datapack") -@Data -public class IrisBiomeCustom { - @Required - @Desc("The resource key of this biome. Just a simple id such as 'plains' or something.") - private String id = ""; - - @MinNumber(-3) - @MaxNumber(3) - @Desc("The biome's temperature") - private double temperature = 0.8; - - @MinNumber(-3) - @MaxNumber(3) - @Desc("The biome's downfall amount (snow / rain), see preci") - private double humidity = 0.4; - - @DependsOn("spawnRarity") - @ArrayType(min = 1, type = IrisBiomeCustomSpawn.class) - @Desc("The biome's mob spawns") - private KList spawns = new KList<>(); - - @Desc("The biome's downfall type") - private IrisBiomeCustomPrecipType downfallType = IrisBiomeCustomPrecipType.rain; - - @Desc("Define an ambient particle to be rendered clientside (no server cost!)") - private IrisBiomeCustomParticle ambientParticle = null; - - @Required - @Desc("The biome's category type") - private IrisBiomeCustomCategory category = IrisBiomeCustomCategory.plains; - - @MinNumber(0) - @MaxNumber(20) - @Desc("The spawn rarity of any defined spawners") - private int spawnRarity = 0; - - @Desc("The color of the sky, top half of sky. (hex format)") - private String skyColor = "#79a8e1"; - - @Desc("The color of the fog, bottom half of sky. (hex format)") - private String fogColor = "#c0d8e1"; - - @Desc("The color of the water (hex format). Leave blank / don't define to not change") - private String waterColor = "#3f76e4"; - - @Desc("The color of water fog (hex format). Leave blank / don't define to not change") - private String waterFogColor = "#050533"; - - @Desc("The color of the grass (hex format). Leave blank / don't define to not change") - private String grassColor = ""; - - @Desc("The color of foliage (hex format). Leave blank / don't define to not change") - private String foliageColor = ""; - - public String generateJson(IDataFixer fixer) { - JSONObject effects = new JSONObject(); - effects.put("sky_color", parseColor(getSkyColor())); - effects.put("fog_color", parseColor(getFogColor())); - effects.put("water_color", parseColor(getWaterColor())); - effects.put("water_fog_color", parseColor(getWaterFogColor())); - - if (ambientParticle != null) { - JSONObject particle = new JSONObject(); - JSONObject po = new JSONObject(); - po.put("type", ambientParticle.getParticle().name().toLowerCase()); - particle.put("options", po); - particle.put("probability", 1f/ambientParticle.getRarity()); - effects.put("particle", particle); - } - - if (!getGrassColor().isEmpty()) { - effects.put("grass_color", parseColor(getGrassColor())); - } - - if (!getFoliageColor().isEmpty()) { - effects.put("foliage_color", parseColor(getFoliageColor())); - } - - JSONObject j = new JSONObject(); - j.put("surface_builder", "minecraft:grass"); - j.put("depth", 0.125); - j.put("scale", 0.05); - j.put("temperature", getTemperature()); - j.put("downfall", getHumidity()); - j.put("creature_spawn_probability", getSpawnRarity()); - j.put("has_precipitation", getDownfallType() != IrisBiomeCustomPrecipType.none); - j.put("precipitation", getDownfallType().toString().toLowerCase()); - j.put("category", getCategory().toString().toLowerCase()); - j.put("effects", effects); - j.put("starts", new JSONArray()); - j.put("spawners", new JSONObject()); - j.put("spawn_costs", new JSONObject()); - j.put("carvers", new JSONObject()); - j.put("features", new JSONArray()); - - if (spawnRarity > 0) { - j.put("creature_spawn_probability", spawnRarity); - } - - if (getSpawns() != null && getSpawns().isNotEmpty()) { - JSONObject spawners = new JSONObject(); - KMap groups = new KMap<>(); - - for (IrisBiomeCustomSpawn i : getSpawns()) { - JSONArray g = groups.computeIfAbsent(i.getGroup(), (k) -> new JSONArray()); - JSONObject o = new JSONObject(); - o.put("type", "minecraft:" + i.getType().name().toLowerCase()); - o.put("weight", i.getWeight()); - o.put("minCount", i.getMinCount()); - o.put("maxCount", i.getMaxCount()); - g.put(o); - } - - for (IrisBiomeCustomSpawnType i : groups.k()) { - spawners.put(i.name().toLowerCase(Locale.ROOT), groups.get(i)); - } - - j.put("spawners", spawners); - } - - return fixer.fixCustomBiome(this, j).toString(4); - } - - private int parseColor(String c) { - String v = (c.startsWith("#") ? c : "#" + c).trim(); - try { - return Color.decode(v).getRGB(); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Error Parsing '''color''', (" + c + ")"); - } - - return 0; - } - - public String getId() { - return id.toLowerCase(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomCategory.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomCategory.java deleted file mode 100644 index 0d638e1cc..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomCategory.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; - -@Desc("The custom biome category. Vanilla asks for this, basically what represents your biome closest?") -public enum IrisBiomeCustomCategory { - beach, - desert, - extreme_hills, - forest, - icy, - jungle, - mesa, - mushroom, - nether, - none, - ocean, - plains, - river, - savanna, - swamp, - taiga, - the_end -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomParticle.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomParticle.java deleted file mode 100644 index 756310b26..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomParticle.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Particle; - -@Snippet("custom-biome-particle") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("A custom biome ambient particle") -@Data -public class IrisBiomeCustomParticle { - @Required - @Desc("The biome's particle type") - private Particle particle = Particle.FLASH; - - @MinNumber(1) - @MaxNumber(10000) - @Desc("The rarity") - private int rarity = 35; -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomSpawn.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomSpawn.java deleted file mode 100644 index fd88885d1..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustomSpawn.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.entity.EntityType; - -@Snippet("custom-biome-spawn") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("A custom biome spawn") -@Data -public class IrisBiomeCustomSpawn { - @Required - @Desc("The biome's entity type") - private EntityType type = EntityType.COW; - - @MinNumber(1) - @Desc("The min to spawn") - private int minCount = 2; - - @MinNumber(1) - @Desc("The max to spawn") - private int maxCount = 5; - - @MinNumber(1) - @MaxNumber(1000) - @Desc("The weight in this group. Higher weight, the more common this type is spawned") - private int weight = 1; - - @Desc("The rarity") - private IrisBiomeCustomSpawnType group = IrisBiomeCustomSpawnType.MISC; -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeGeneratorLink.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeGeneratorLink.java deleted file mode 100644 index 67bd7340f..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeGeneratorLink.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.data.DataProvider; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import lombok.*; -import lombok.experimental.Accessors; - -@Snippet("generator-layer") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("This represents a link to a generator for a biome") -@Data -public class IrisBiomeGeneratorLink { - - private final transient AtomicCache gen = new AtomicCache<>(); - @RegistryListResource(IrisGenerator.class) - @Desc("The generator id") - private String generator = "default"; - @DependsOn({"min", "max"}) - @Required - @MinNumber(-2032) // TODO: WARNING HEIGHT - @MaxNumber(2032) // TODO: WARNING HEIGHT - @Desc("The min block value (value + fluidHeight)") - private int min = 0; - @DependsOn({"min", "max"}) - @Required - @MinNumber(-2032) // TODO: WARNING HEIGHT - @MaxNumber(2032) // TODO: WARNING HEIGHT - @Desc("The max block value (value + fluidHeight)") - private int max = 0; - - public IrisGenerator getCachedGenerator(DataProvider g) { - return gen.aquire(() -> { - IrisGenerator gen = g.getData().getGeneratorLoader().load(getGenerator()); - - if (gen == null) { - gen = new IrisGenerator(); - } - - return gen; - }); - } - - public double getHeight(DataProvider xg, double x, double z, long seed) { - double g = getCachedGenerator(xg).getHeight(x, z, seed); - g = g < 0 ? 0 : g; - g = g > 1 ? 1 : g; - - return IrisInterpolation.lerp(min, max, g); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePaletteLayer.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePaletteLayer.java deleted file mode 100644 index c2fa1b59f..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePaletteLayer.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -@Snippet("biome-palette") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("A layer of surface / subsurface material in biomes") -@Data -public class IrisBiomePaletteLayer { - private final transient AtomicCache> blockData = new AtomicCache<>(); - private final transient AtomicCache layerGenerator = new AtomicCache<>(); - private final transient AtomicCache heightGenerator = new AtomicCache<>(); - @Desc("The style of noise") - private IrisGeneratorStyle style = NoiseStyle.STATIC.style(); - @DependsOn({"minHeight", "maxHeight"}) - @MinNumber(0) - @MaxNumber(2032) // TODO: WARNING HEIGHT - - @Desc("The min thickness of this layer") - private int minHeight = 1; - @DependsOn({"minHeight", "maxHeight"}) - @MinNumber(1) - @MaxNumber(2032) // TODO: WARNING HEIGHT - - @Desc("The max thickness of this layer") - private int maxHeight = 1; - @Desc("If set, this layer will change size depending on the slope. If in bounds, the layer will get larger (taller) the closer to the center of this slope clip it is. If outside of the slipe's bounds, this layer will not show.") - private IrisSlopeClip slopeCondition = new IrisSlopeClip(); - @MinNumber(0.0001) - @Desc("The terrain zoom mostly for zooming in on a wispy palette") - private double zoom = 5; - @Required - @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("The palette of blocks to be used in this layer") - private KList palette = new KList().qadd(new IrisBlockData("GRASS_BLOCK")); - - public CNG getHeightGenerator(RNG rng, IrisData data) { - return heightGenerator.aquire(() -> CNG.signature(rng.nextParallelRNG(minHeight * maxHeight + getBlockData(data).size()))); - } - - public BlockData get(RNG rng, double x, double y, double z, IrisData data) { - if (getBlockData(data).isEmpty()) { - return null; - } - - if (getBlockData(data).size() == 1) { - return getBlockData(data).get(0); - } - - return getLayerGenerator(rng, data).fit(getBlockData(data), x / zoom, y / zoom, z / zoom); - } - - public CNG getLayerGenerator(RNG rng, IrisData data) { - return layerGenerator.aquire(() -> - { - RNG rngx = rng.nextParallelRNG(minHeight + maxHeight + getBlockData(data).size()); - return style.create(rngx, data); - }); - } - - public KList add(String b) { - palette.add(new IrisBlockData(b)); - - return palette; - } - - public KList getBlockData(IrisData data) { - return blockData.aquire(() -> - { - KList blockData = new KList<>(); - for (IrisBlockData ix : palette) { - BlockData bx = ix.getBlockData(data); - if (bx != null) { - for (int i = 0; i < ix.getWeight(); i++) { - blockData.add(bx); - } - } - } - - return blockData; - }); - } - - public IrisBiomePaletteLayer zero() { - palette.clear(); - return this; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBlockData.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBlockData.java deleted file mode 100644 index 1c9cb514d..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBlockData.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; - -import java.util.Map; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents Block Data") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisBlockData extends IrisRegistrant { - private final transient AtomicCache blockdata = new AtomicCache<>(); - private final transient AtomicCache realProperties = new AtomicCache<>(); - @RegistryListBlockType - @Required - @Desc("The block to use") - private String block = "air"; - @Desc("Debug this block by printing it to the console when it's used. Must have debug turned on in settings.") - private boolean debug = false; - @MinNumber(1) - @MaxNumber(1000) - @Desc("The weight is used when this block data is inside of a list of blockdata. A weight of two is just as if you placed two of the same block data values in the same list making it more common when randomly picked.") - private int weight = 1; - @Desc("If the block cannot be created on this version, Iris will attempt to use this backup block data instead.") - private IrisBlockData backup = null; - @RegistryMapBlockState("block") - @Desc("Optional properties for this block data such as 'waterlogged': true") - private KMap data = new KMap<>(); - @Desc("Optional tile data for this block data") - private KMap tileData = new KMap<>(); - - public IrisBlockData(String b) { - this.block = b; - } - - public static IrisBlockData from(String j) { - IrisBlockData b = new IrisBlockData(); - String v = j.toLowerCase().trim(); - - if (v.contains("[")) { - KList props = new KList<>(); - String rp = v.split("\\Q[\\E")[1].replaceAll("\\Q]\\E", ""); - b.setBlock(v.split("\\Q[\\E")[0]); - - if (rp.contains(",")) { - props.add(rp.split("\\Q,\\E")); - } else { - props.add(rp); - } - - for (String i : props) { - Object kg = filter(i.split("\\Q=\\E")[1]); - b.data.put(i.split("\\Q=\\E")[0], kg); - } - } else { - b.setBlock(v); - } - - return b; - } - - private static Object filter(String string) { - if (string.equals("true")) { - return true; - } - - if (string.equals("false")) { - return false; - } - - try { - return Integer.parseInt(string); - } catch (Throwable ignored) { - // Checks - } - - try { - return Double.valueOf(string).intValue(); - } catch (Throwable ignored) { - // Checks - } - - return string; - } - - public String computeProperties(KMap data) { - if (data.isEmpty()) { - return ""; - } - - KList r = new KList<>(); - - for (Map.Entry entry : data.entrySet()) { - r.add(entry.getKey() + "=" + filter(entry.getValue().toString())); - } - - return "[" + r.toString(",") + "]"; - } - - public String computeProperties() { - return computeProperties(getData()); - } - - public BlockData getBlockData(IrisData data) { - return blockdata.aquire(() -> - { - BlockData b = null; - - IrisBlockData customData = data.getBlockLoader().load(getBlock(), false); - - if (customData != null) { - b = customData.getBlockData(data); - - if (b != null) { - b = b.clone(); - - String st = b.getAsString(true); - - if (st.contains("[")) { - st = st.split("\\Q[\\E")[0]; - } - - KMap cdata = customData.getData().copy(); - - for (String i : getData().keySet()) { - cdata.put(i, getData().get(i)); - } - - String sx = keyify(st) + computeProperties(cdata); - - if (debug) { - Iris.debug("Block Data used " + sx + " (CUSTOM)"); - } - - BlockData bx = B.get(sx); - - if (bx != null) { - return bx; - } - - if (b != null) { - return b; - } - } - } - - String ss = keyify(getBlock()) + computeProperties(); - b = B.get(ss); - - if (debug) { - Iris.debug("Block Data used " + ss); - } - - if (b != null) { - return b; - } - - if (backup != null) { - return backup.getBlockData(data); - } - - return B.get("AIR"); - }); - } - - public TileData tryGetTile(IrisData data) { - //TODO Do like a registry thing with the tile data registry. Also update the parsing of data to include **block** entities. - var type = getBlockData(data).getMaterial(); - if (type == Material.SPAWNER && this.data.containsKey("entitySpawn")) { - String id = (String) this.data.get("entitySpawn"); - if (tileData == null) tileData = new KMap<>(); - KMap spawnData = (KMap) tileData.computeIfAbsent("SpawnData", k -> new KMap<>()); - KMap entity = (KMap) spawnData.computeIfAbsent("entity", k -> new KMap<>()); - entity.putIfAbsent("id", Identifier.fromString(id).toString()); - } - - if (!INMS.get().hasTile(type) || tileData == null || tileData.isEmpty()) - return null; - return new TileData(type, this.tileData); - } - - private String keyify(String dat) { - if (dat.contains(":")) { - return dat; - } - - return "minecraft:" + dat; - } - - @Override - public String getFolderName() { - return "blocks"; - } - - @Override - public String getTypeName() { - return "Block"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisCarving.java b/core/src/main/java/com/volmit/iris/engine/object/IrisCarving.java deleted file mode 100644 index 8287db8c4..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisCarving.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("carving") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a carving configuration") -@Data -public class IrisCarving { - @ArrayType(type = IrisCavePlacer.class, min = 1) - @Desc("Define cave placers") - private KList caves = new KList<>(); - - @ArrayType(type = IrisRavinePlacer.class, min = 1) - @Desc("Define ravine placers") - private KList ravines = new KList<>(); - - @ArrayType(type = IrisElipsoid.class, min = 1) - @Desc("Define elipsoids") - private KList elipsoids = new KList<>(); - - @ArrayType(type = IrisSphere.class, min = 1) - @Desc("Define spheres") - private KList spheres = new KList<>(); - - @ArrayType(type = IrisPyramid.class, min = 1) - @Desc("Define pyramids") - private KList pyramids = new KList<>(); - - - @BlockCoordinates - public void doCarving(MantleWriter writer, RNG rng, Engine engine, int x, int y, int z, int depth) { - doCarving(writer, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, depth, -1); - } - - @BlockCoordinates - public void doCarving(MantleWriter writer, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint) { - int nextRecursion = recursion + 1; - - if (caves.isNotEmpty()) { - for (IrisCavePlacer i : caves) { - if (recursion > i.getMaxRecursion()) continue; - i.generateCave(writer, rng, base, engine, x, y, z, nextRecursion, waterHint); - } - } - - if (ravines.isNotEmpty()) { - for (IrisRavinePlacer i : ravines) { - if (recursion > i.getMaxRecursion()) continue; - i.generateRavine(writer, rng, base, engine, x, y, z, nextRecursion, waterHint); - } - } - - if (spheres.isNotEmpty()) { - for (IrisSphere i : spheres) { - if (rng.nextInt(i.getRarity()) == 0) { - i.generate(base, engine, writer, x, y, z); - } - } - } - - if (elipsoids.isNotEmpty()) { - for (IrisElipsoid i : elipsoids) { - if (rng.nextInt(i.getRarity()) == 0) { - i.generate(base, engine, writer, x, y, z); - } - } - } - - if (pyramids.isNotEmpty()) { - for (IrisPyramid i : pyramids) { - if (rng.nextInt(i.getRarity()) == 0) { - i.generate(base, engine, writer, x, y, z); - } - } - } - } - - public int getMaxRange(IrisData data, int recursion) { - int max = 0; - int nextRecursion = recursion + 1; - - for (IrisCavePlacer i : caves) { - if (recursion > i.getMaxRecursion()) continue; - max = Math.max(max, i.getSize(data, nextRecursion)); - } - - for (IrisRavinePlacer i : ravines) { - if (recursion > i.getMaxRecursion()) continue; - max = Math.max(max, i.getSize(data, nextRecursion)); - } - - if (elipsoids.isNotEmpty()) { - max = (int) Math.max(elipsoids.stream().mapToDouble(IrisElipsoid::maxSize).max().getAsDouble(), max); - } - - if (spheres.isNotEmpty()) { - max = (int) Math.max(spheres.stream().mapToDouble(IrisSphere::maxSize).max().getAsDouble(), max); - } - - if (pyramids.isNotEmpty()) { - max = (int) Math.max(pyramids.stream().mapToDouble(IrisPyramid::maxSize).max().getAsDouble(), max); - } - - return max; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisCave.java b/core/src/main/java/com/volmit/iris/engine/object/IrisCave.java deleted file mode 100644 index af20daab7..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisCave.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.RegistryListResource; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@EqualsAndHashCode(callSuper = true) -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisCave extends IrisRegistrant { - @Desc("Define the shape of this cave") - private IrisWorm worm = new IrisWorm(); - - @Desc("Define potential forking features") - private IrisCarving fork = new IrisCarving(); - - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - - @Desc("Limit the worm from ever getting higher or lower than this range") - private IrisRange verticalRange = new IrisRange(3, 255); - - @Desc("Shape of the caves") - private IrisCaveShape shape = new IrisCaveShape(); - - @Override - public String getFolderName() { - return "caves"; - } - - @Override - public String getTypeName() { - return "Cave"; - } - - public void generate(MantleWriter writer, RNG rng, Engine engine, int x, int y, int z) { - generate(writer, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, 0, -1, true); - } - - public void generate(MantleWriter writer, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint, boolean breakSurface) { - double girth = getWorm().getGirth().get(base.nextParallelRNG(465156), x, z, engine.getData()); - KList points = getWorm().generate(base.nextParallelRNG(784684), engine.getData(), writer, verticalRange, x, y, z, breakSurface, girth + 9); - int highestWater = Math.max(waterHint, -1); - - if (highestWater == -1) { - for (IrisPosition i : points) { - double yy = i.getY() + girth; - int th = engine.getHeight(x, z, true); - - if (yy > th && th < engine.getDimension().getFluidHeight()) { - highestWater = Math.max(highestWater, (int) yy); - break; - } - } - } - - - int h = Math.min(highestWater, engine.getDimension().getFluidHeight()); - - for (IrisPosition i : points) { - fork.doCarving(writer, rng, base, engine, i.getX(), i.getY(), i.getZ(), recursion, h); - } - - MatterCavern c = new MatterCavern(true, customBiome, (byte) 0); - MatterCavern w = new MatterCavern(true, customBiome, (byte) 1); - - CNG cng = shape.getNoise(base.nextParallelRNG(8131545), engine); - KSet mask = shape.getMasked(rng, engine); - writer.setNoiseMasked(points, - girth, shape.getNoiseThreshold() < 0 ? cng.noise(x, y, z) : shape.getNoiseThreshold(), cng, mask, true, - (xf, yf, zf) -> yf <= h ? w : c); - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } - - public int getMaxSize(IrisData data, int depth) { - return (int) (Math.ceil(getWorm().getGirth().getMax() * 2) + getWorm().getMaxDistance() + fork.getMaxRange(data, depth)); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisCavePlacer.java b/core/src/main/java/com/volmit/iris/engine/object/IrisCavePlacer.java deleted file mode 100644 index ccdcaafb9..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisCavePlacer.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.util.concurrent.atomic.AtomicBoolean; - -@Snippet("cave-placer") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisCavePlacer implements IRare { - private transient final AtomicCache caveCache = new AtomicCache<>(); - private transient final AtomicBoolean fail = new AtomicBoolean(false); - @Required - @Desc("Typically a 1 in RARITY on a per chunk/fork basis") - @MinNumber(1) - private int rarity = 15; - @MinNumber(1) - @Required - @Desc("The cave to place") - @RegistryListResource(IrisCave.class) - private String cave; - @MinNumber(1) - @MaxNumber(256) - @Desc("The maximum recursion depth") - private int maxRecursion = 16; - @Desc("If set to true, this cave is allowed to break the surface") - private boolean breakSurface = true; - @Desc("The height range this cave can spawn at. If breakSurface is false, the output of this range will be clamped by the current world height to prevent surface breaking.") - private IrisStyledRange caveStartHeight = new IrisStyledRange(13, 120, new IrisGeneratorStyle(NoiseStyle.STATIC)); - - public IrisCave getRealCave(IrisData data) { - return caveCache.aquire(() -> data.getCaveLoader().load(getCave())); - } - - public void generateCave(MantleWriter mantle, RNG rng, Engine engine, int x, int y, int z) { - generateCave(mantle, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, 0, -1); - } - - public void generateCave(MantleWriter mantle, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint) { - if (fail.get()) { - return; - } - - if (rng.nextInt(rarity) != 0) { - return; - } - - IrisData data = engine.getData(); - IrisCave cave = getRealCave(data); - - if (cave == null) { - Iris.warn("Unable to locate cave for generation!"); - fail.set(true); - return; - } - - if (y == -1) { - int h = (int) caveStartHeight.get(base, x, z, data); - int ma = breakSurface ? h : (int) (engine.getComplex().getHeightStream().get(x, z) - 9); - y = Math.min(h, ma); - } - - try { - cave.generate(mantle, rng, base, engine, x + rng.nextInt(15), y, z + rng.nextInt(15), recursion, waterHint, breakSurface); - } catch (Throwable e) { - e.printStackTrace(); - fail.set(true); - } - } - - public int getSize(IrisData data, int depth) { - IrisCave cave = getRealCave(data); - - if (cave != null) { - return cave.getMaxSize(data, depth); - } - - return 32; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisCaveShape.java b/core/src/main/java/com/volmit/iris/engine/object/IrisCaveShape.java deleted file mode 100644 index 3245f2485..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisCaveShape.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("cave-shape") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Cave Shape") -@Data -public class IrisCaveShape { - private transient final KMap> cache = new KMap<>(); - - @Desc("Noise used for the shape of the cave") - private IrisGeneratorStyle noise = new IrisGeneratorStyle(); - @MinNumber(0) - @MaxNumber(1) - @Desc("The threshold for noise mask") - private double noiseThreshold = -1; - - @RegistryListResource(IrisObject.class) - @Desc("Object used as mask for the shape of the cave") - private String object = null; - @Desc("Rotation to apply to objects before using them as mask") - private IrisObjectRotation objectRotation = new IrisObjectRotation(); - - public CNG getNoise(RNG rng, Engine engine) { - return noise.create(rng, engine.getData()); - } - - public KSet getMasked(RNG rng, Engine engine) { - if (object == null) return null; - return cache.computeIfAbsent(randomRotation(rng), pos -> { - var rotated = new KSet(); - engine.getData().getObjectLoader().load(object).getBlocks().forEach((vector, data) -> { - if (data.getMaterial().isAir()) return; - rotated.add(new IrisPosition(objectRotation.rotate(vector, pos.getX(), pos.getY(), pos.getZ()))); - }); - return rotated; - }); - } - - private IrisPosition randomRotation(RNG rng) { - if (objectRotation == null || !objectRotation.canRotate()) - return new IrisPosition(0,0,0); - return new IrisPosition( - randomDegree(rng, objectRotation.getXAxis()), - randomDegree(rng, objectRotation.getYAxis()), - randomDegree(rng, objectRotation.getZAxis()) - ); - } - - private int randomDegree(RNG rng, IrisAxisRotationClamp clamp) { - if (!clamp.isEnabled()) return 0; - if (clamp.isLocked()) return (int) clamp.getMax(); - double interval = clamp.getInterval(); - if (interval < 1) interval = 1; - - double min = clamp.getMin(), max = clamp.getMax(); - double value = (interval * (Math.ceil(Math.abs(rng.d(0, 360) / interval)))) % 360D; - if (clamp.isUnlimited()) return (int) value; - - if (min > max) { - max = clamp.getMin(); - min = clamp.getMax(); - } - return (int) (double) M.clip(value, min, max); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisCommand.java b/core/src/main/java/com/volmit/iris/engine/object/IrisCommand.java deleted file mode 100644 index 0555bb6bb..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisCommand.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KList; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; - -@Snippet("command") -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Represents a set of Iris commands") -@Data -public class IrisCommand { - - @Required - @ArrayType(min = 1, type = String.class) - @Desc("List of commands. Iris replaces {x} {y} and {z} with the location of the entity spawn") - private KList commands = new KList<>(); - - @Desc("The delay for running the command. Instant by default") - private long delay = 0; - - @Desc("If this should be repeated (indefinitely, cannot be cancelled). This does not persist with server-restarts, so it only repeats when the chunk is generated.") - private boolean repeat = false; - - @Desc("The delay between repeats, in server ticks (by default 100, so 5 seconds)") - private long repeatDelay = 100; - - @Desc("The block of 24 hour time in which the command should execute.") - private IrisTimeBlock timeBlock = new IrisTimeBlock(); - - @Desc("The weather that is required for the command to execute.") - private IrisWeather weather = IrisWeather.ANY; - - public boolean isValid(World world) { - return timeBlock.isWithin(world) && weather.is(world); - } - - public void run(Location at) { - if (!isValid(at.getWorld())) { - return; - } - - for (String command : commands) { - command = (command.startsWith("/") ? command.replaceFirst("/", "") : command) - .replaceAll("\\Q{x}\\E", String.valueOf(at.getBlockX())) - .replaceAll("\\Q{y}\\E", String.valueOf(at.getBlockY())) - .replaceAll("\\Q{z}\\E", String.valueOf(at.getBlockZ())); - final String finalCommand = command; - if (repeat) { - Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () -> Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalCommand), delay, repeatDelay); - } else { - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalCommand), delay); - } - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDecorator.java b/core/src/main/java/com/volmit/iris/engine/object/IrisDecorator.java deleted file mode 100644 index 8bdd2ec91..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDecorator.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -@Snippet("decorator") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("A biome decorator is used for placing flowers, grass, cacti and so on") -@Data -public class IrisDecorator { - private final transient AtomicCache layerGenerator = new AtomicCache<>(); - private final transient AtomicCache varianceGenerator = new AtomicCache<>(); - private final transient AtomicCache heightGenerator = new AtomicCache<>(); - private final transient AtomicCache> blockData = new AtomicCache<>(); - private final transient AtomicCache> blockDataTops = new AtomicCache<>(); - @Desc("The varience dispersion is used when multiple blocks are put in the palette. Scatter scrambles them, Wispy shows streak-looking varience") - private IrisGeneratorStyle variance = NoiseStyle.STATIC.style(); - @Desc("Forcefully place this decorant anywhere it is supposed to go even if it should not go on a specific surface block. For example, you could force tallgrass to place on top of stone by using this.") - private boolean forcePlace = false; - @Desc("Forced the surface block of this decorant to be the specified block. Assumes forcePlace.") - private IrisBlockData forceBlock; - @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("When set, the decorator can only place onto any of these blocks.") - private KList whitelist; - @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("When set, the decorator will never place onto any of these blocks.") - private KList blacklist; - @Desc("The slope at which this decorator can be placed. Range from 0 to 10 by default. Calculated from a 3-block radius from the center of the decorator placement.") - private IrisSlopeClip slopeCondition = new IrisSlopeClip(); - @DependsOn({"scaleStack", "stackMin", "stackMax"}) - @Desc("If stackMax is set to true, use this to limit its max height for large caverns") - private int absoluteMaxStack = 30; - @Desc("Dispersion is used to pick places to spawn. Scatter randomly places them (vanilla) or Wispy for a streak like patch system.") - private IrisGeneratorStyle style = NoiseStyle.STATIC.style(); - @DependsOn({"stackMin", "stackMax"}) - @Desc("If this decorator has a height more than 1 this changes how it picks the height between your maxes. Scatter = random, Wispy = wavy heights") - private IrisGeneratorStyle heightVariance = NoiseStyle.STATIC.style(); - @Desc("Tells iris where this decoration is a part of. I.e. SHORE_LINE or SEA_SURFACE") - private IrisDecorationPart partOf = IrisDecorationPart.NONE; - @DependsOn({"stackMin", "stackMax"}) - @MinNumber(1) - @MaxNumber(2032) // TODO: WARNING HEIGHT - @Desc("The minimum repeat stack height (setting to 3 would stack 3 of on top of each other") - private int stackMin = 1; - @DependsOn({"stackMin", "stackMax"}) - @MinNumber(1) - @MaxNumber(2032) // TODO: WARNING HEIGHT - @Desc("The maximum repeat stack height") - private int stackMax = 1; - @DependsOn({"stackMin", "stackMax"}) - @Desc(""" - Changes stackMin and stackMin from being absolute block heights and instead uses them as a percentage to scale the stack based on the cave height - - Within a cave, setting them stackMin/max to 50 would make the stack 50% of the cave height""") - private boolean scaleStack = false; - @Required - @MinNumber(0) - @MaxNumber(1) - @Desc("The chance for this decorator to decorate at a given X,Y coordinate. This is hit 256 times per chunk (per surface block)") - // TODO: WARNING HEIGHT - private double chance = 0.1; - @Required - @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("The palette of blocks to pick from when this decorator needs to place.") - private KList palette = new KList().qadd(new IrisBlockData("grass")); - @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("The palette of blocks used at the very top of a 'stackMax' of higher than 1. For example, bamboo tops.") - private KList topPalette = new KList<>(); - @DependsOn("topPalette") - @MinNumber(0.01) - @MaxNumber(1.0) - @Desc("When the stack passes the top threshold, the top palette will start being used instead of the normal palette.") - private double topThreshold = 1.0; - - public int getHeight(RNG rng, double x, double z, IrisData data) { - if (stackMin == stackMax) { - return stackMin; - } - - return getHeightGenerator(rng, data) - .fit(stackMin, stackMax, - x / heightVariance.getZoom(), - z / heightVariance.getZoom()) + 1; - } - - public CNG getHeightGenerator(RNG rng, IrisData data) { - return heightGenerator.aquire(() -> - heightVariance.create(rng.nextParallelRNG(getBlockData(data).size() + stackMax + stackMin), data)); - } - - public CNG getGenerator(RNG rng, IrisData data) { - return layerGenerator.aquire(() -> style.create(rng.nextParallelRNG(getBlockData(data).size()), data)); - } - - public CNG getVarianceGenerator(RNG rng, IrisData data) { - return varianceGenerator.aquire(() -> - variance.create( - rng.nextParallelRNG(getBlockData(data).size()), data) - .scale(1D / variance.getZoom())); - } - - public KList add(String b) { - palette.add(new IrisBlockData(b)); - return palette; - } - - public BlockData getBlockData(IrisBiome b, RNG rng, double x, double z, IrisData data) { - if (getBlockData(data).isEmpty()) { - Iris.warn("Empty Block Data for " + b.getName()); - return null; - } - - double xx = x / style.getZoom(); - double zz = z / style.getZoom(); - - if (getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance) { - if (getBlockData(data).size() == 1) { - return getBlockData(data).get(0); - } - - return getVarianceGenerator(rng, data).fit(getBlockData(data), z, x); //X and Z must be switched - } - - return null; - } - - public BlockData getBlockData100(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) { - if (getBlockData(data).isEmpty()) { - Iris.warn("Empty Block Data for " + b.getName()); - return null; - } - - double xx = x; - double yy = y; - double zz = z; - - if (!getVarianceGenerator(rng, data).isStatic()) { - xx = x / style.getZoom(); - yy = y / style.getZoom(); - zz = z / style.getZoom(); - } - - if (getBlockData(data).size() == 1) { - return getBlockData(data).get(0); - } - - return getVarianceGenerator(rng, data).fit(getBlockData(data), z, y, x).clone(); //X and Z must be switched - } - - public BlockData getBlockDataForTop(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) { - if (getBlockDataTops(data).isEmpty()) { - return getBlockData100(b, rng, x, y, z, data); - } - - double xx = x / style.getZoom(); - double zz = z / style.getZoom(); - - if (getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance) { //Exclude y from here - if (getBlockData(data).size() == 1) { - return getBlockDataTops(data).get(0); - } - - return getVarianceGenerator(rng, data).fit(getBlockDataTops(data), z, y, x); //X and Z must be switched - } - - return null; - } - - public KList getBlockData(IrisData data) { - return blockData.aquire(() -> - { - KList blockData = new KList<>(); - for (IrisBlockData i : palette) { - BlockData bx = i.getBlockData(data); - if (bx != null) { - for (int n = 0; n < i.getWeight(); n++) { - blockData.add(bx); - } - } - } - - return blockData; - }); - } - - public KList getBlockDataTops(IrisData data) { - return blockDataTops.aquire(() -> - { - KList blockDataTops = new KList<>(); - for (IrisBlockData i : topPalette) { - BlockData bx = i.getBlockData(data); - if (bx != null) { - for (int n = 0; n < i.getWeight(); n++) { - blockDataTops.add(bx); - } - } - } - - return blockDataTops; - }); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean isStacking() { - return getStackMax() > 1; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDepositGenerator.java b/core/src/main/java/com/volmit/iris/engine/object/IrisDepositGenerator.java deleted file mode 100644 index b1e1c609b..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDepositGenerator.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.math.BlockPosition; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -@Snippet("deposit") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Creates ore & other block deposits underground") -@Data -public class IrisDepositGenerator { - private final transient AtomicCache> objects = new AtomicCache<>(); - private final transient AtomicCache> blockData = new AtomicCache<>(); - @Required - @MinNumber(0) - @MaxNumber(8192) // TODO: WARNING HEIGHT - @Desc("The minimum height this deposit can generate at") - private int minHeight = 1; - @Required - @MinNumber(0) - @MaxNumber(8192) // TODO: WARNING HEIGHT - @Desc("The maximum height this deposit can generate at") - private int maxHeight = 75; - @Required - @MinNumber(0) - @MaxNumber(8192) - @Desc("The minimum amount of deposit blocks per clump") - private int minSize = 0; - @Required - @MinNumber(0) - @MaxNumber(8192) - @Desc("The maximum amount of deposit blocks per clump") - private int maxSize = 128; - @Required - @MinNumber(0) - @MaxNumber(2048) - @Desc("The maximum amount of clumps per chunk") - private int maxPerChunk = 3; - @Required - @MinNumber(0) - @MaxNumber(2048) - @Desc("The minimum amount of clumps per chunk") - private int minPerChunk = 0; - @MinNumber(0) - @MaxNumber(1) - @Desc("The change of the deposit spawning in a chunk") - private double spawnChance = 1; - @MinNumber(0) - @MaxNumber(1) - @Desc("The change of the a clump spawning in a chunk") - private double perClumpSpawnChance = 1; - @Required - @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("The palette of blocks to be used in this deposit generator") - private KList palette = new KList<>(); - @MinNumber(1) - @MaxNumber(64) - @Desc("Ore varience is how many different objects clumps iris will create") - private int varience = 3; - @Desc("If set to true, this deposit will replace bedrock") - private boolean replaceBedrock = false; - - public IrisObject getClump(Engine engine, RNG rng, IrisData rdata) { - KList objects = this.objects.aquire(() -> - { - RNG rngv = new RNG(engine.getSeedManager().getDeposit() + hashCode()); - KList objectsf = new KList<>(); - - for (int i = 0; i < varience; i++) { - objectsf.add(generateClumpObject(rngv.nextParallelRNG(2349 * i + 3598), rdata)); - } - - return objectsf; - }); - return objects.get(rng.i(0, objects.size())); - } - - public int getMaxDimension() { - return Math.min(11, (int) Math.ceil(Math.cbrt(maxSize))); - } - - private IrisObject generateClumpObject(RNG rngv, IrisData rdata) { - int s = rngv.i(minSize, maxSize + 1); - if (s == 1) { - IrisObject o = new IrisObject(1, 1, 1); - o.getBlocks().put(o.getCenter(), nextBlock(rngv, rdata)); - return o; - } - - int dim = Math.min(11, (int) Math.ceil(Math.cbrt(s))); - IrisObject o = new IrisObject(dim, dim, dim); - - int volume = dim * dim * dim; - if (s >= volume) { - int x = 0, y = 0, z = 0; - - while (z < dim) { - o.setUnsigned(x++, y, z, nextBlock(rngv, rdata)); - - if (x == dim) { - x = 0; - y++; - } - - if (y == dim) { - y = 0; - z++; - } - } - return o; - } - - KSet set = new KSet<>(); - while (s > 0) { - BlockPosition ang = new BlockPosition( - rngv.i(0, dim), - rngv.i(0, dim), - rngv.i(0, dim) - ); - if (!set.add(ang)) continue; - - s--; - o.setUnsigned(ang.getX(), ang.getY(), ang.getZ(), nextBlock(rngv, rdata)); - } - - return o; - } - - private BlockData nextBlock(RNG rngv, IrisData rdata) { - return getBlockData(rdata).get(rngv.i(0, getBlockData(rdata).size())); - } - - public KList getBlockData(IrisData rdata) { - return blockData.aquire(() -> - { - KList blockData = new KList<>(); - - for (IrisBlockData ix : palette) { - BlockData bx = ix.getBlockData(rdata); - - if (bx != null) { - blockData.add(bx); - } - } - - return blockData; - }); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java b/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java deleted file mode 100644 index ef4842056..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java +++ /dev/null @@ -1,537 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.ServerConfigurator.DimensionHeight; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.nms.datapack.IDataFixer; -import com.volmit.iris.core.nms.datapack.IDataFixer.Dimension; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.engine.object.annotations.functions.ComponentFlagFunction; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.data.DataProvider; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Material; -import org.bukkit.World.Environment; -import org.bukkit.block.data.BlockData; - -import java.io.*; - -@Accessors(chain = true) -@AllArgsConstructor -@NoArgsConstructor -@Desc("Represents a dimension") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisDimension extends IrisRegistrant { - public static final BlockData STONE = Material.STONE.createBlockData(); - public static final BlockData WATER = Material.WATER.createBlockData(); - private final transient AtomicCache parallaxSize = new AtomicCache<>(); - private final transient AtomicCache rockLayerGenerator = new AtomicCache<>(); - private final transient AtomicCache fluidLayerGenerator = new AtomicCache<>(); - private final transient AtomicCache coordFracture = new AtomicCache<>(); - private final transient AtomicCache sinr = new AtomicCache<>(); - private final transient AtomicCache cosr = new AtomicCache<>(); - private final transient AtomicCache rad = new AtomicCache<>(); - private final transient AtomicCache featuresUsed = new AtomicCache<>(); - private final transient AtomicCache> strongholdsCache = new AtomicCache<>(); - private final transient AtomicCache>> cachedPreProcessors = new AtomicCache<>(); - @MinNumber(2) - @Required - @Desc("The human readable name of this dimension") - private String name = "A Dimension"; - @MinNumber(1) - @MaxNumber(2032) - @Desc("Maximum height at which players can be teleported to through gameplay.") - private int logicalHeight = 256; - @RegistryListResource(IrisJigsawStructure.class) - @Desc("If defined, Iris will place the given jigsaw structure where minecraft should place the overworld stronghold.") - private String stronghold; - @Desc("If set to true, Iris will remove chunks to allow visualizing cross sections of chunks easily") - private boolean debugChunkCrossSections = false; - @Desc("Vertically split up the biome palettes with 3 air blocks in between to visualize them") - private boolean explodeBiomePalettes = false; - @Desc("Studio Mode for testing different parts of the world") - private StudioMode studioMode = StudioMode.NORMAL; - @MinNumber(1) - @MaxNumber(16) - @Desc("Customize the palette height explosion") - private int explodeBiomePaletteSize = 3; - @MinNumber(2) - @MaxNumber(16) - @Desc("Every X/Z % debugCrossSectionsMod == 0 cuts the chunk") - private int debugCrossSectionsMod = 3; - @Desc("The average distance between strongholds") - private int strongholdJumpDistance = 1280; - @Desc("Define the maximum strongholds to place") - private int maxStrongholds = 14; - @Desc("Tree growth override settings") - private IrisTreeSettings treeSettings = new IrisTreeSettings(); - @Desc("Spawn Entities in this dimension over time. Iris will continually replenish these mobs just like vanilla does.") - @ArrayType(min = 1, type = String.class) - @RegistryListResource(IrisSpawner.class) - private KList entitySpawners = new KList<>(); - @Desc("Reference loot tables in this area") - private IrisLootReference loot = new IrisLootReference(); - @MinNumber(0) - @Desc("The version of this dimension. Changing this will stop users from accidentally upgrading (and breaking their worlds).") - private int version = 1; - @ArrayType(min = 1, type = IrisBlockDrops.class) - @Desc("Define custom block drops for this dimension") - private KList blockDrops = new KList<>(); - @Desc("Should bedrock be generated or not.") - private boolean bedrock = true; - @MinNumber(0) - @MaxNumber(1) - @Desc("The land chance. Up to 1.0 for total land or 0.0 for total sea") - private double landChance = 0.625; - @Desc("The placement style of regions") - private IrisGeneratorStyle regionStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("The placement style of land/sea") - private IrisGeneratorStyle continentalStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("The placement style of biomes") - private IrisGeneratorStyle landBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("The placement style of biomes") - private IrisGeneratorStyle shoreBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("The placement style of biomes") - private IrisGeneratorStyle seaBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("The placement style of biomes") - private IrisGeneratorStyle caveBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("Instead of filling objects with air, fills them with cobweb so you can see them") - private boolean debugSmartBore = false; - @Desc("Generate decorations or not") - private boolean decorate = true; - @Desc("Use post processing or not") - private boolean postProcessing = true; - @Desc("Add slabs in post processing") - private boolean postProcessingSlabs = true; - @Desc("Add painted walls in post processing") - private boolean postProcessingWalls = true; - @Desc("Carving configuration for the dimension") - private IrisCarving carving = new IrisCarving(); - @Desc("Configuration of fluid bodies such as rivers & lakes") - private IrisFluidBodies fluidBodies = new IrisFluidBodies(); - @Desc("forceConvertTo320Height") - private Boolean forceConvertTo320Height = false; - @Desc("The world environment") - private Environment environment = Environment.NORMAL; - @RegistryListResource(IrisRegion.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("Define all of the regions to include in this dimension. Dimensions -> Regions -> Biomes -> Objects etc") - private KList regions = new KList<>(); - @ArrayType(min = 1, type = IrisJigsawStructurePlacement.class) - @Desc("Jigsaw structures") - private KList jigsawStructures = new KList<>(); - @Desc("The jigsaw structure divisor to use when generating missing jigsaw placement values") - private double jigsawStructureDivisor = 18; - @Required - @MinNumber(0) - @MaxNumber(1024) - @Desc("The fluid height for this dimension") - private int fluidHeight = 63; - @Desc("Define the min and max Y bounds of this dimension. Please keep in mind that Iris internally generates from 0 to (max - min). \n\nFor example at -64 to 320, Iris is internally generating to 0 to 384, then on outputting chunks, it shifts it down by the min height (64 blocks). The default is -64 to 320. \n\nThe fluid height is placed at (fluid height + min height). So a fluid height of 63 would actually show up in the world at 1.") - private IrisRange dimensionHeight = new IrisRange(-64, 320); - @Desc("Define options for this dimension") - private IrisDimensionTypeOptions dimensionOptions = new IrisDimensionTypeOptions(); - @RegistryListResource(IrisBiome.class) - @Desc("Keep this either undefined or empty. Setting any biome name into this will force iris to only generate the specified biome. Great for testing.") - private String focus = ""; - @RegistryListResource(IrisRegion.class) - @Desc("Keep this either undefined or empty. Setting any region name into this will force iris to only generate the specified region. Great for testing.") - private String focusRegion = ""; - @MinNumber(0.0001) - @MaxNumber(512) - @Desc("Zoom in or out the biome size. Higher = bigger biomes") - private double biomeZoom = 1D; - @MinNumber(0) - @MaxNumber(360) - @Desc("You can rotate the input coordinates by an angle. This can make terrain appear more natural (less sharp corners and lines). This literally rotates the entire dimension by an angle. Hint: Try 12 degrees or something not on a 90 or 45 degree angle.") - private double dimensionAngleDeg = 0; - @Required - @Desc("Define the mode of this dimension (required!)") - private IrisDimensionMode mode = new IrisDimensionMode(); - @MinNumber(0) - @MaxNumber(8192) - @Desc("Coordinate fracturing applies noise to the input coordinates. This creates the 'iris swirls' and wavy features. The distance pushes these waves further into places they shouldnt be. This is a block value multiplier.") - private double coordFractureDistance = 20; - @MinNumber(0.0001) - @MaxNumber(512) - @Desc("Coordinate fracturing zoom. Higher = less frequent warping, Lower = more frequent and rapid warping / swirls.") - private double coordFractureZoom = 8; - @MinNumber(0.0001) - @MaxNumber(512) - @Desc("This zooms in the land space") - private double landZoom = 1; - @MinNumber(0.0001) - @MaxNumber(512) - @Desc("This zooms oceanic biomes") - private double seaZoom = 1; - @MinNumber(0.0001) - @MaxNumber(512) - @Desc("Zoom in continents") - private double continentZoom = 1; - @MinNumber(0.0001) - @MaxNumber(512) - @Desc("Change the size of regions") - private double regionZoom = 1; - @Desc("Disable this to stop placing objects, entities, features & updates") - private boolean useMantle = true; - @Desc("Prevent Leaf decay as if placed in creative mode") - private boolean preventLeafDecay = false; - @ArrayType(min = 1, type = IrisDepositGenerator.class) - @Desc("Define global deposit generators") - private KList deposits = new KList<>(); - @ArrayType(min = 1, type = IrisShapedGeneratorStyle.class) - @Desc("Overlay additional noise on top of the interoplated terrain.") - private KList overlayNoise = new KList<>(); - @Desc("If true, the spawner system has infinite energy. This is NOT recommended because it would allow for mobs to keep spawning over and over without a rate limit") - private boolean infiniteEnergy = false; - @MinNumber(0) - @MaxNumber(10000) - @Desc("This is the maximum energy you can have in a dimension") - private double maximumEnergy = 1000; - @MinNumber(0.0001) - @MaxNumber(512) - @Desc("The rock zoom mostly for zooming in on a wispy palette") - private double rockZoom = 5; - @Desc("The palette of blocks for 'stone'") - private IrisMaterialPalette rockPalette = new IrisMaterialPalette().qclear().qadd("stone"); - @Desc("The palette of blocks for 'water'") - private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("water"); - @Desc("Prevent cartographers to generate explorer maps (Iris worlds only)\nONLY TOUCH IF YOUR SERVER CRASHES WHILE GENERATING EXPLORER MAPS") - private boolean disableExplorerMaps = false; - @Desc("Collection of ores to be generated") - @ArrayType(type = IrisOreGenerator.class, min = 1) - private KList ores = new KList<>(); - @MinNumber(0) - @MaxNumber(318) - @Desc("The Subterrain Fluid Layer Height") - private int caveLavaHeight = 8; - @RegistryListFunction(ComponentFlagFunction.class) - @ArrayType(type = String.class) - @Desc("Collection of disabled components") - private KList disabledComponents = new KList<>(); - @Desc("A list of globally applied pre-processors") - @ArrayType(type = IrisPreProcessors.class) - private KList globalPreProcessors = new KList<>(); - @Desc("A list of scripts executed on engine setup\nFile extension: .engine.kts") - @RegistryListResource(IrisScript.class) - @ArrayType(type = String.class, min = 1) - private KList engineScripts = new KList<>(); - @Desc("A list of scripts executed on data setup\nFile extension: .data.kts") - @RegistryListResource(IrisScript.class) - @ArrayType(type = String.class, min = 1) - private KList dataScripts = new KList<>(); - @Desc("A list of scripts executed on chunk update\nFile extension: .update.kts") - @RegistryListResource(IrisScript.class) - @ArrayType(type = String.class, min = 1) - private KList chunkUpdateScripts = new KList<>(); - @Desc("Use legacy rarity instead of modern one\nWARNING: Changing this may break expressions and image maps") - private boolean legacyRarity = true; - - public int getMaxHeight() { - return (int) getDimensionHeight().getMax(); - } - - public int getMinHeight() { - return (int) getDimensionHeight().getMin(); - } - - public BlockData generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) { - if (ores.isEmpty()) { - return null; - } - BlockData b = null; - for (IrisOreGenerator i : ores) { - if (i.isGenerateSurface() != surface) - continue; - - b = i.generate(x, y, z, rng, data); - if (b != null) { - return b; - } - } - return null; - } - - public KList getStrongholds(long seed) { - return strongholdsCache.aquire(() -> { - KList pos = new KList<>(); - int jump = strongholdJumpDistance; - RNG rng = new RNG((seed * 223) + 12945); - for (int i = 0; i < maxStrongholds + 1; i++) { - int m = i + 1; - pos.add(new Position2( - (int) ((rng.i(jump * i) + (jump * i)) * (rng.b() ? -1D : 1D)), - (int) ((rng.i(jump * i) + (jump * i)) * (rng.b() ? -1D : 1D)) - )); - } - - pos.remove(0); - - return pos; - }); - } - - public int getFluidHeight() { - return fluidHeight - (int) dimensionHeight.getMin(); - } - - public CNG getCoordFracture(RNG rng, int signature) { - return coordFracture.aquire(() -> - { - CNG coordFracture = CNG.signature(rng.nextParallelRNG(signature)); - coordFracture.scale(0.012 / coordFractureZoom); - return coordFracture; - }); - } - - public double getDimensionAngle() { - return rad.aquire(() -> Math.toRadians(dimensionAngleDeg)); - } - - public Environment getEnvironment() { - return environment; - } - - public boolean hasFocusRegion() { - return !focusRegion.equals(""); - } - - public String getFocusRegion() { - return focusRegion; - } - - public double sinRotate() { - return sinr.aquire(() -> Math.sin(getDimensionAngle())); - } - - public double cosRotate() { - return cosr.aquire(() -> Math.cos(getDimensionAngle())); - } - - public KList getAllRegions(DataProvider g) { - KList r = new KList<>(); - - for (String i : getRegions()) { - r.add(g.getData().getRegionLoader().load(i)); - } - - return r; - } - - public KList getAllAnyRegions() { - KList r = new KList<>(); - - for (String i : getRegions()) { - r.add(IrisData.loadAnyRegion(i, getLoader())); - } - - return r; - } - - public KList getAllBiomes(DataProvider g) { - return g.getData().getBiomeLoader().loadAll(g.getData().getBiomeLoader().getPossibleKeys()); - } - - public KList getAllAnyBiomes() { - KList r = new KList<>(); - - for (IrisRegion i : getAllAnyRegions()) { - if (i == null) { - continue; - } - - r.addAll(i.getAllAnyBiomes()); - } - - return r; - } - - public KList getPreProcessors(String type) { - return cachedPreProcessors.aquire(() -> { - KMap> preProcessors = new KMap<>(); - for (var entry : globalPreProcessors) { - preProcessors.computeIfAbsent(entry.getType(), k -> new KList<>()) - .addAll(entry.getScripts()); - } - return preProcessors; - }).get(type); - } - - public IrisGeneratorStyle getBiomeStyle(InferredType type) { - switch (type) { - case CAVE: - return caveBiomeStyle; - case LAND: - return landBiomeStyle; - case SEA: - return seaBiomeStyle; - case SHORE: - return shoreBiomeStyle; - default: - break; - } - - return landBiomeStyle; - } - - public void installBiomes(IDataFixer fixer, DataProvider data, KList folders, KSet biomes) { - getAllBiomes(data) - .stream() - .filter(IrisBiome::isCustom) - .map(IrisBiome::getCustomDerivitives) - .flatMap(KList::stream) - .parallel() - .forEach(j -> { - String json = j.generateJson(fixer); - synchronized (biomes) { - if (!biomes.add(j.getId())) { - Iris.verbose("Duplicate Data Pack Biome: " + getLoadKey() + "/" + j.getId()); - return; - } - } - - for (File datapacks : folders) { - File output = new File(datapacks, "iris/data/" + getLoadKey().toLowerCase() + "/worldgen/biome/" + j.getId() + ".json"); - - Iris.verbose(" Installing Data Pack Biome: " + output.getPath()); - output.getParentFile().mkdirs(); - try { - IO.writeAll(output, json); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - }); - } - - public Dimension getBaseDimension() { - return switch (getEnvironment()) { - case NETHER -> Dimension.NETHER; - case THE_END -> Dimension.END; - default -> Dimension.OVERWORLD; - }; - } - - public String getDimensionTypeKey() { - return getDimensionType().key(); - } - - public IrisDimensionType getDimensionType() { - return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight()); - } - - public void installDimensionType(IDataFixer fixer, KList folders) { - IrisDimensionType type = getDimensionType(); - String json = type.toJson(fixer); - - Iris.verbose(" Installing Data Pack Dimension Type: \"iris:" + type.key() + '"'); - for (File datapacks : folders) { - File output = new File(datapacks, "iris/data/iris/dimension_type/" + type.key() + ".json"); - output.getParentFile().mkdirs(); - try { - IO.writeAll(output, json); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - } - - @Override - public String getFolderName() { - return "dimensions"; - } - - @Override - public String getTypeName() { - return "Dimension"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } - - public static void writeShared(KList folders, DimensionHeight height) { - Iris.verbose(" Installing Data Pack Vanilla Dimension Types"); - String[] jsonStrings = height.jsonStrings(); - for (File datapacks : folders) { - write(datapacks, "overworld", jsonStrings[0]); - write(datapacks, "the_nether", jsonStrings[1]); - write(datapacks, "the_end", jsonStrings[2]); - } - - String raw = """ - { - "pack": { - "description": "Iris Data Pack. This pack contains all installed Iris Packs' resources.", - "pack_format": {} - } - } - """.replace("{}", INMS.get().getDataVersion().getPackFormat() + ""); - - for (File datapacks : folders) { - File mcm = new File(datapacks, "iris/pack.mcmeta"); - try { - IO.writeAll(mcm, raw); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - Iris.verbose(" Installing Data Pack MCMeta: " + mcm.getPath()); - } - } - - private static void write(File datapacks, String type, String json) { - if (json == null) return; - File dimTypeVanilla = new File(datapacks, "iris/data/minecraft/dimension_type/" + type + ".json"); - - if (IrisSettings.get().getGeneral().adjustVanillaHeight || dimTypeVanilla.exists()) { - dimTypeVanilla.getParentFile().mkdirs(); - try { - IO.writeAll(dimTypeVanilla, json); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionMode.java b/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionMode.java deleted file mode 100644 index a57e78f08..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionMode.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineMode; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.RegistryListResource; -import com.volmit.iris.engine.object.annotations.Snippet; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("dimension-mode") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a dimensional mode") -@Data -public class IrisDimensionMode { - @Desc("The dimension type") - private IrisDimensionModeType type = IrisDimensionModeType.OVERWORLD; - - @RegistryListResource(IrisScript.class) - @Desc("The script to create the dimension mode instead of using provided types\nFile extension: .engine.kts") - private String script; - - public EngineMode create(Engine engine) { - if (script == null) { - return type.create(engine); - } - Object result = engine.getExecution().evaluate(script); - if (result instanceof EngineMode) { - return (EngineMode) result; - } - - throw new IllegalStateException("The script '" + script + "' did not return an engine mode!"); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionType.java b/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionType.java deleted file mode 100644 index 756e4c538..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDimensionType.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.nms.datapack.IDataFixer; -import com.volmit.iris.util.data.Varint; -import com.volmit.iris.util.io.IO; -import lombok.*; -import lombok.experimental.Accessors; - -import java.io.*; - -@Getter -@ToString -@Accessors(fluent = true, chain = true) -@EqualsAndHashCode -public final class IrisDimensionType { - @NonNull - private final String key; - @NonNull - private final IDataFixer.Dimension base; - @NonNull - private final IrisDimensionTypeOptions options; - private final int logicalHeight; - private final int height; - private final int minY; - - public IrisDimensionType( - @NonNull IDataFixer.Dimension base, - @NonNull IrisDimensionTypeOptions options, - int logicalHeight, - int height, - int minY - ) { - if (logicalHeight > height) throw new IllegalArgumentException("Logical height cannot be greater than height"); - if (logicalHeight < 0) throw new IllegalArgumentException("Logical height cannot be less than zero"); - if (height < 16 || height > 4064 ) throw new IllegalArgumentException("Height must be between 16 and 4064"); - if ((height & 15) != 0) throw new IllegalArgumentException("Height must be a multiple of 16"); - if (minY < -2032 || minY > 2031) throw new IllegalArgumentException("Min Y must be between -2032 and 2031"); - if ((minY & 15) != 0) throw new IllegalArgumentException("Min Y must be a multiple of 16"); - - this.base = base; - this.options = options; - this.logicalHeight = logicalHeight; - this.height = height; - this.minY = minY; - this.key = createKey(); - } - - public static IrisDimensionType fromKey(String key) { - var stream = new ByteArrayInputStream(IO.decode(key.replace(".", "=").toUpperCase())); - try (var din = new DataInputStream(stream)) { - return new IrisDimensionType( - IDataFixer.Dimension.values()[din.readUnsignedByte()], - new IrisDimensionTypeOptions().read(din), - Varint.readUnsignedVarInt(din), - Varint.readUnsignedVarInt(din), - Varint.readSignedVarInt(din) - ); - } catch (IOException e) { - throw new RuntimeException("This is impossible", e); - } - } - - public String toJson(IDataFixer fixer) { - return fixer.createDimension( - base, - minY, - height, - logicalHeight, - options.copy() - ).toString(4); - } - - private String createKey() { - var stream = new ByteArrayOutputStream(41); - try (var dos = new DataOutputStream(stream)) { - dos.writeByte(base.ordinal()); - options.write(dos); - Varint.writeUnsignedVarInt(logicalHeight, dos); - Varint.writeUnsignedVarInt(height, dos); - Varint.writeSignedVarInt(minY, dos); - } catch (IOException e) { - throw new RuntimeException("This is impossible", e); - } - - return IO.encode(stream.toByteArray()).replace("=", ".").toLowerCase(); - } - - public IrisDimensionTypeOptions options() { - return options.copy(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisEffect.java b/core/src/main/java/com/volmit/iris/engine/object/IrisEffect.java deleted file mode 100644 index d2c6eca6b..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisEffect.java +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Location; -import org.bukkit.Particle; -import org.bukkit.Sound; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.util.Vector; - -@Snippet("effect") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("An iris effect") -@Data -public class IrisEffect { - private final transient AtomicCache pt = new AtomicCache<>(); - private final transient AtomicCache latch = new AtomicCache<>(); - @Desc("The potion effect to apply in this area") - private String potionEffect = ""; - @Desc("The particle effect to apply in the area") - private Particle particleEffect = null; - @DependsOn({"particleEffect"}) - @MinNumber(-32) - @MaxNumber(32) - @Desc("Randomly offset from the surface to this surface+value") - private int particleOffset = 0; - @DependsOn({"particleEffect"}) - @MinNumber(-8) - @MaxNumber(8) - @Desc("The alt x, usually represents motion if the particle count is zero. Otherwise an offset.") - private double particleAltX = 0; - @DependsOn({"particleEffect"}) - @MinNumber(-8) - @MaxNumber(8) - @Desc("The alt y, usually represents motion if the particle count is zero. Otherwise an offset.") - private double particleAltY = 0; - @DependsOn({"particleEffect"}) - @MinNumber(-8) - @MaxNumber(8) - @Desc("The alt z, usually represents motion if the particle count is zero. Otherwise an offset.") - private double particleAltZ = 0; - @DependsOn({"particleEffect"}) - @Desc("Randomize the altX by -altX to altX") - private boolean randomAltX = true; - @DependsOn({"particleEffect"}) - @Desc("Randomize the altY by -altY to altY") - private boolean randomAltY = false; - @DependsOn({"particleEffect"}) - @Desc("Randomize the altZ by -altZ to altZ") - private boolean randomAltZ = true; - @Desc("The sound to play") - private Sound sound = null; - @DependsOn({"sound"}) - @MinNumber(0) - @MaxNumber(512) - @Desc("The max distance from the player the sound will play") - private int soundDistance = 12; - @DependsOn({"sound", "maxPitch"}) - @MinNumber(0.01) - @MaxNumber(1.99) - @Desc("The minimum sound pitch") - private double minPitch = 0.5D; - @DependsOn({"sound", "minVolume"}) - @MinNumber(0.01) - @MaxNumber(1.99) - @Desc("The max sound pitch") - private double maxPitch = 1.5D; - @DependsOn({"sound"}) - @MinNumber(0.001) - @MaxNumber(512) - @Desc("The sound volume.") - private double volume = 1.5D; - @DependsOn({"particleEffect"}) - @MinNumber(0) - @MaxNumber(512) - @Desc("The particle count. Try setting to zero for using the alt xyz to a motion value instead of an offset") - private int particleCount = 0; - @DependsOn({"particleEffect"}) - @MinNumber(0) - @MaxNumber(64) - @Desc("How far away from the player particles can play") - private int particleDistance = 20; - @DependsOn({"particleEffect"}) - @MinNumber(0) - @MaxNumber(128) - @Desc("How wide the particles can play (player's view left and right) RADIUS") - private int particleDistanceWidth = 24; - @DependsOn({"particleEffect"}) - @Desc("An extra value for some particles... Which bukkit doesn't even document.") - private double extra = 0; - @DependsOn({"potionEffect"}) - @MinNumber(-1) - @MaxNumber(1024) - @Desc("The Potion Strength or -1 to disable") - private int potionStrength = -1; - @DependsOn({"potionEffect", "potionTicksMin"}) - @MinNumber(1) - @Desc("The max time the potion will last for") - private int potionTicksMax = 155; - @DependsOn({"potionEffect", "potionTicksMax"}) - @MinNumber(1) - @Desc("The min time the potion will last for") - private int potionTicksMin = 75; - @Required - @MinNumber(0) - @Desc("The effect interval in milliseconds") - private int interval = 150; - @DependsOn({"particleEffect"}) - @MinNumber(0) - @MaxNumber(16) - @Desc("The effect distance start away") - private int particleAway = 5; - @Required - @MinNumber(1) - @Desc("The chance is 1 in CHANCE per interval") - private int chance = 50; - @Desc("Run commands, with configurable location parameters") - private IrisCommandRegistry commandRegistry = null; - - - public boolean canTick() { - return latch.aquire(() -> new ChronoLatch(interval)).flip(); - } - - public PotionEffectType getRealType() { - return pt.aquire(() -> - { - PotionEffectType t = PotionEffectType.LUCK; - - if (getPotionEffect().isEmpty()) { - return t; - } - - try { - for (PotionEffectType i : PotionEffectType.values()) { - if (i.getName().toUpperCase().replaceAll("\\Q \\E", "_").equals(getPotionEffect())) { - t = i; - - return t; - } - } - } catch (Throwable e) { - Iris.reportError(e); - - } - - Iris.warn("Unknown Potion Effect Type: " + getPotionEffect()); - - return t; - }); - } - - public void apply(Player p, Engine g) { - if (!canTick()) { - return; - } - - if (RNG.r.nextInt(chance) != 0) { - return; - } - - if (sound != null) { - Location part = p.getLocation().clone().add(RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance)); - - J.s(() -> p.playSound(part, getSound(), (float) volume, (float) RNG.r.d(minPitch, maxPitch))); - } - - if (particleEffect != null) { - Location part = p.getLocation().clone().add(p.getLocation().getDirection().clone().multiply(RNG.r.i(particleDistance) + particleAway)).clone().add(p.getLocation().getDirection().clone().rotateAroundY(Math.toRadians(90)).multiply(RNG.r.d(-particleDistanceWidth, particleDistanceWidth))); - - part.setY(Math.round(g.getHeight(part.getBlockX(), part.getBlockZ())) + 1); - part.add(RNG.r.d(), 0, RNG.r.d()); - int offset = p.getWorld().getMinHeight(); - if (extra != 0) { - J.s(() -> p.spawnParticle(particleEffect, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), - part.getZ(), - particleCount, - randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, - randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, - randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ, - extra)); - } else { - J.s(() -> p.spawnParticle(particleEffect, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), - particleCount, - randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, - randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, - randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ)); - } - } - - if (commandRegistry != null) { - commandRegistry.run(p); - } - - if (potionStrength > -1) { - if (p.hasPotionEffect(getRealType())) { - PotionEffect e = p.getPotionEffect(getRealType()); - if (e.getAmplifier() > getPotionStrength()) { - return; - } - - J.s(() -> p.removePotionEffect(getRealType())); - } - - J.s(() -> p.addPotionEffect(new PotionEffect(getRealType(), - RNG.r.i(Math.min(potionTicksMax, potionTicksMin), - Math.max(potionTicksMax, potionTicksMin)), - getPotionStrength(), - true, false, false))); - } - } - - public void apply(Entity p) { - if (!canTick()) { - return; - } - - if (RNG.r.nextInt(chance) != 0) { - return; - } - - if (sound != null) { - Location part = p.getLocation().clone().add(RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance)); - - J.s(() -> p.getWorld().playSound(part, getSound(), (float) volume, (float) RNG.r.d(minPitch, maxPitch))); - } - - if (particleEffect != null) { - Location part = p.getLocation().clone().add(0, 0.25, 0).add(new Vector(1, 1, 1).multiply(RNG.r.d())).subtract(new Vector(1, 1, 1).multiply(RNG.r.d())); - part.add(RNG.r.d(), 0, RNG.r.d()); - int offset = p.getWorld().getMinHeight(); - if (extra != 0) { - J.s(() -> p.getWorld().spawnParticle(particleEffect, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), - part.getZ(), - particleCount, - randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, - randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, - randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ, - extra)); - } else { - J.s(() -> p.getWorld().spawnParticle(particleEffect, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), - particleCount, - randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, - randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, - randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ)); - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisElipsoid.java b/core/src/main/java/com/volmit/iris/engine/object/IrisElipsoid.java deleted file mode 100644 index d902af348..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisElipsoid.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.matter.slices.CavernMatter; -import lombok.Data; - -@Snippet("carving-elipsoid") -@Desc("Represents an procedural eliptical shape") -@Data -public class IrisElipsoid implements IRare { - private transient final AtomicCache matterNodeCache = new AtomicCache<>(); - @Required - @Desc("Typically a 1 in RARITY on a per fork basis") - @MinNumber(1) - private int rarity = 1; - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - @Desc("The styled random radius for x") - private IrisStyledRange xRadius = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - @Desc("The styled random radius for y") - private IrisStyledRange yRadius = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - @Desc("The styled random radius for z") - private IrisStyledRange zRadius = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - - @SuppressWarnings("SuspiciousNameCombination") - public void generate(RNG rng, Engine engine, MantleWriter writer, int x, int y, int z) { - writer.setElipsoid(x, y, z, - xRadius.get(rng, z, y, engine.getData()), - yRadius.get(rng, x, z, engine.getData()), - zRadius.get(rng, y, x, engine.getData()), true, matterNodeCache.aquire(() -> CavernMatter.get(getCustomBiome(), 0))); - } - - public double maxSize() { - return Math.max(xRadius.getMax(), Math.max(yRadius.getMax(), zRadius.getMax())); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisEnchantment.java b/core/src/main/java/com/volmit/iris/engine/object/IrisEnchantment.java deleted file mode 100644 index 0a332d9fc..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisEnchantment.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.NamespacedKey; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.meta.EnchantmentStorageMeta; -import org.bukkit.inventory.meta.ItemMeta; - - -@Snippet("enchantment") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents an enchantment & level") -@Data -public class IrisEnchantment { - @Required - @RegistryListEnchantment - @Desc("The enchantment") - private String enchantment; - - @MinNumber(1) - @Desc("Minimum amount of this loot") - private int minLevel = 1; - - @MinNumber(1) - @Desc("Maximum amount of this loot") - private int maxLevel = 1; - - @MinNumber(0) - @MaxNumber(1) - @Desc("The chance that this enchantment is applied (0 to 1)") - private double chance = 1; - - public void apply(RNG rng, ItemMeta meta) { - try { - Enchantment enchant = Enchantment.getByKey(NamespacedKey.minecraft(getEnchantment())); - if (enchant == null) { - Iris.warn("Unknown Enchantment: " + getEnchantment()); - return; - } - if (rng.nextDouble() < chance) { - if (meta instanceof EnchantmentStorageMeta) { - ((EnchantmentStorageMeta) meta).addStoredEnchant(enchant, getLevel(rng), true); - return; - } - meta.addEnchant(enchant, getLevel(rng), true); - } - } catch (Throwable e) { - Iris.reportError(e); - - } - } - - public int getLevel(RNG rng) { - return rng.i(getMinLevel(), getMaxLevel()); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineStatistics.java b/core/src/main/java/com/volmit/iris/engine/object/IrisEngineStatistics.java deleted file mode 100644 index 883ef4025..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisEngineStatistics.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import lombok.Data; - -@Data -public class IrisEngineStatistics { - private int totalHotloads = 0; - private int chunksGenerated = 0; - private int IrisToUpgradedVersion = 0; - private int IrisCreationVersion = 0; - private int MinecraftVersion = 0; - - public void generatedChunk() { - chunksGenerated++; - } - - public void setUpgradedVersion(int i) { - IrisToUpgradedVersion = i; - } - public int getUpgradedVersion() { - return IrisToUpgradedVersion; - } - public void setVersion(int i) { - IrisCreationVersion = i; - } - - public int getVersion() { - return IrisCreationVersion; - } - - public void setMCVersion(int i) { - MinecraftVersion = i; - } - - public int getMCVersion() { - return MinecraftVersion; - } - - public void hotloaded() { - totalHotloads++; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisEntity.java b/core/src/main/java/com/volmit/iris/engine/object/IrisEntity.java deleted file mode 100644 index 10803f1bf..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisEntity.java +++ /dev/null @@ -1,481 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.plugin.Chunks; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.*; -import org.bukkit.attribute.Attributable; -import org.bukkit.entity.*; -import org.bukkit.entity.Panda.Gene; -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.loot.LootContext; -import org.bukkit.loot.LootTable; -import org.bukkit.loot.Lootable; -import org.bukkit.util.Vector; - -import java.util.Collection; -import java.util.Random; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import static com.volmit.iris.util.data.registry.Particles.ITEM; - -@SuppressWarnings("ALL") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor - -@Desc("Represents an iris entity.") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisEntity extends IrisRegistrant { - @Required - @Desc("The type of entity to spawn. To spawn a mythic mob, set this type to unknown and define mythic type.") - private EntityType type = EntityType.UNKNOWN; - - @Desc("The SpawnReason to spawn the entity with.") - private CreatureSpawnEvent.SpawnReason reason = CreatureSpawnEvent.SpawnReason.NATURAL; - - @Desc("The custom name of this entity") - private String customName = ""; - - @Desc("Should the name on this entity be visible even if you arent looking at it.") - private boolean customNameVisible = false; - - @Desc("If this entity type is a mob, should it be aware of it's surroundings & interact with the world.") - private boolean aware = true; - - @Desc("If this entity type is a creature, should it have ai goals.") - private boolean ai = true; - - @Desc("Should this entity be glowing") - private boolean glowing = false; - - @Desc("Should gravity apply to this entity") - private boolean gravity = true; - - @Desc("When an entity is invulnerable it can only be damaged by players increative mode.") - private boolean invulnerable = false; - - @Desc("When an entity is silent it will not produce any sound.") - private boolean silent = false; - - @Desc("Should this entity be allowed to pickup items") - private boolean pickupItems = false; - - @Desc("Should this entity be removed when far away") - private boolean removable = false; - - @Desc("Entity helmet equipment") - private IrisLoot helmet = null; - - @Desc("Entity chestplate equipment") - private IrisLoot chestplate = null; - - @Desc("Entity boots equipment") - private IrisLoot boots = null; - - @Desc("Entity leggings equipment") - private IrisLoot leggings = null; - - @Desc("Entity main hand equipment") - private IrisLoot mainHand = null; - - @Desc("Entity off hand equipment") - private IrisLoot offHand = null; - - @Desc("Make other entities ride this entity") - @ArrayType(min = 1, type = IrisEntity.class) - private KList passengers = new KList<>(); - - @Desc("Attribute modifiers for this entity") - @ArrayType(min = 1, type = IrisAttributeModifier.class) - private KList attributes = new KList<>(); - - @Desc("Loot tables for drops") - private IrisLootReference loot = new IrisLootReference(); - - @Desc("If specified, this entity will be leashed by this entity. I.e. THIS ENTITY Leashed by SPECIFIED. This has no effect on EnderDragons, Withers, Players, or Bats.Non-living entities excluding leashes will not persist as leashholders.") - private IrisEntity leashHolder = null; - - @Desc("If specified, this entity will spawn with an effect") - private IrisEffect spawnEffect = null; - - @Desc("Simply moves the entity from below the surface slowly out of the ground as a spawn-in effect") - private boolean spawnEffectRiseOutOfGround = false; - - @Desc("The main gene for a panda if the entity type is a panda") - private Gene pandaMainGene = Gene.NORMAL; - - @Desc("The hidden gene for a panda if the entity type is a panda") - private Gene pandaHiddenGene = Gene.NORMAL; - - @Desc("The this entity is ageable, set it's baby status") - private boolean baby = false; - - @Desc("If the entity should never be culled. Useful for Jigsaws") - private boolean keepEntity = false; - - @Desc("The surface type to spawn this mob on") - private IrisSurface surface = IrisSurface.LAND; - - @RegistryListSpecialEntity - @Desc("Create a mob from another plugin, such as Mythic Mobs. Should be in the format of a namespace of PluginName:MobName") - private String specialType = ""; - - @Desc("Set to true if you want to apply all of the settings here to the mob, even though an external plugin has already done so. Scripts are always applied.") - private boolean applySettingsToCustomMobAnyways = false; - - @Desc("Set the entity type to UNKNOWN, then define a script here which ends with the entity variable (the result). You can use location to find the target location. You can spawn any entity this way.\nFile extension: .spawn.kts") - @RegistryListResource(IrisScript.class) - private String spawnerScript = ""; - - @ArrayType(min = 1, type = String.class) - @Desc("Executed post spawn you can modify the entity however you want with it\nFile extension: .postspawn.kts") - @RegistryListResource(IrisScript.class) - private KList postSpawnScripts = new KList<>(); - - @ArrayType(min = 1, type = IrisCommand.class) - @Desc("Run raw commands when this entity is spawned. Use {x}, {y}, and {z} for location. /summon pig {x} {y} {z}") - private KList rawCommands = new KList<>(); - - public Entity spawn(Engine gen, Location at) { - return spawn(gen, at, new RNG(at.hashCode())); - } - - public Entity spawn(Engine gen, Location at, RNG rng) { - if (!Chunks.isSafe(at)) { - return null; - } - if (isSpawnEffectRiseOutOfGround()) { - AtomicReference f = new AtomicReference<>(at); - try { - J.sfut(() -> { - if (Chunks.hasPlayersNearby(f.get())) { - Location b = f.get().clone(); - Location start = new Location(b.getWorld(), b.getX(), b.getY() - 5, b.getZ()); - f.set(start); - } - }).get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - at = f.get(); - } - - Entity ee = doSpawn(at); - - if (ee == null && !Chunks.isSafe(at)) { - return null; - } - - if (!spawnerScript.isEmpty() && ee == null) { - synchronized (this) { - try { - ee = (Entity) gen.getExecution().spawnMob(spawnerScript, at); - } catch (Throwable ex) { - Iris.error("You must return an Entity in your scripts to use entity scripts!"); - ex.printStackTrace(); - } - } - } - - if (isSpecialType() && !applySettingsToCustomMobAnyways) { - return ee; - } - - if (ee == null) { - return null; - } - - Entity e = ee; - e.setCustomName(getCustomName() != null ? C.translateAlternateColorCodes('&', getCustomName()) : null); - e.setCustomNameVisible(isCustomNameVisible()); - e.setGlowing(isGlowing()); - e.setGravity(isGravity()); - e.setInvulnerable(isInvulnerable()); - e.setSilent(isSilent()); - e.setPersistent(isKeepEntity() || IrisSettings.get().getWorld().isForcePersistEntities()); - - int gg = 0; - for (IrisEntity i : passengers) { - Entity passenger = i.spawn(gen, at, rng.nextParallelRNG(234858 + gg++)); - if (!Bukkit.isPrimaryThread()) { - J.s(() -> e.addPassenger(passenger)); - } - } - - if (e instanceof Attributable) { - Attributable a = (Attributable) e; - - for (IrisAttributeModifier i : getAttributes()) { - i.apply(rng, a); - } - } - - if (e instanceof Lootable) { - Lootable l = (Lootable) e; - - if (getLoot().getTables().isNotEmpty()) { - Location finalAt = at; - l.setLootTable(new LootTable() { - @Override - public NamespacedKey getKey() { - return new NamespacedKey(Iris.instance, "loot-" + IrisEntity.this.hashCode()); - } - - @Override - public Collection populateLoot(Random random, LootContext context) { - KList items = new KList<>(); - - for (String fi : getLoot().getTables()) { - IrisLootTable i = gen.getData().getLootLoader().load(fi); - items.addAll(i.getLoot(gen.isStudio(), rng.nextParallelRNG(345911), InventorySlotType.STORAGE, finalAt.getWorld(), finalAt.getBlockX(), finalAt.getBlockY(), finalAt.getBlockZ())); - } - - return items; - } - - @Override - public void fillInventory(Inventory inventory, Random random, LootContext context) { - for (ItemStack i : populateLoot(random, context)) { - inventory.addItem(i); - } - - gen.scramble(inventory, rng); - } - }); - } - } - - if (e instanceof LivingEntity) { - LivingEntity l = (LivingEntity) e; - l.setAI(isAi()); - l.setCanPickupItems(isPickupItems()); - - if (getLeashHolder() != null) { - l.setLeashHolder(getLeashHolder().spawn(gen, at, rng.nextParallelRNG(234548))); - } - - l.setRemoveWhenFarAway(isRemovable()); - - if (getHelmet() != null && rng.i(1, getHelmet().getRarity()) == 1) { - l.getEquipment().setHelmet(getHelmet().get(gen.isStudio(), rng)); - } - - if (getChestplate() != null && rng.i(1, getChestplate().getRarity()) == 1) { - l.getEquipment().setChestplate(getChestplate().get(gen.isStudio(), rng)); - } - - if (getLeggings() != null && rng.i(1, getLeggings().getRarity()) == 1) { - l.getEquipment().setLeggings(getLeggings().get(gen.isStudio(), rng)); - } - - if (getBoots() != null && rng.i(1, getBoots().getRarity()) == 1) { - l.getEquipment().setBoots(getBoots().get(gen.isStudio(), rng)); - } - - if (getMainHand() != null && rng.i(1, getMainHand().getRarity()) == 1) { - l.getEquipment().setItemInMainHand(getMainHand().get(gen.isStudio(), rng)); - } - - if (getOffHand() != null && rng.i(1, getOffHand().getRarity()) == 1) { - l.getEquipment().setItemInOffHand(getOffHand().get(gen.isStudio(), rng)); - } - } - - if (e instanceof Ageable && isBaby()) { - ((Ageable) e).setBaby(); - } - - if (e instanceof Panda) { - ((Panda) e).setMainGene(getPandaMainGene()); - ((Panda) e).setMainGene(getPandaHiddenGene()); - } - - if (e instanceof Villager) { - Villager villager = (Villager) e; - villager.setRemoveWhenFarAway(false); - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> { - villager.setPersistent(true); - }, 1); - } - - if (e instanceof Mob) { - Mob m = (Mob) e; - m.setAware(isAware()); - } - - if (spawnEffect != null) { - spawnEffect.apply(e); - } - - if (postSpawnScripts.isNotEmpty()) { - synchronized (this) { - for (String i : postSpawnScripts) { - gen.getExecution().postSpawnMob(i, at, ee); - } - } - } - - if (rawCommands.isNotEmpty()) { - final Location fat = at; - rawCommands.forEach(r -> r.run(fat)); - } - - Location finalAt1 = at; - - J.s(() -> { - if (isSpawnEffectRiseOutOfGround() && e instanceof LivingEntity && Chunks.hasPlayersNearby(finalAt1)) { - Location start = finalAt1.clone(); - e.setInvulnerable(true); - ((LivingEntity) e).setAI(false); - ((LivingEntity) e).setCollidable(false); - ((LivingEntity) e).setNoDamageTicks(100000); - AtomicInteger t = new AtomicInteger(0); - AtomicInteger v = new AtomicInteger(0); - v.set(J.sr(() -> { - if (t.get() > 100) { - J.csr(v.get()); - return; - } - - t.incrementAndGet(); - if (e.getLocation().getBlock().getType().isSolid() || ((LivingEntity) e).getEyeLocation().getBlock().getType().isSolid()) { - e.teleport(start.add(new Vector(0, 0.1, 0))); - ItemStack itemCrackData = new ItemStack(((LivingEntity) e).getEyeLocation().clone().subtract(0, 2, 0).getBlock().getBlockData().getMaterial()); - e.getWorld().spawnParticle(ITEM, ((LivingEntity) e).getEyeLocation(), 6, 0.2, 0.4, 0.2, 0.06f, itemCrackData); - if (M.r(0.2)) { - e.getWorld().playSound(e.getLocation(), Sound.BLOCK_CHORUS_FLOWER_GROW, 0.8f, 0.1f); - } - } else { - J.csr(v.get()); - ((LivingEntity) e).setNoDamageTicks(0); - ((LivingEntity) e).setCollidable(true); - ((LivingEntity) e).setAI(true); - e.setInvulnerable(false); - } - }, 0)); - } - }); - - - return e; - } - - private int surfaceY(Location l) { - int m = l.getBlockY(); - - while (m-- > 0) { - Location ll = l.clone(); - ll.setY(m); - - if (ll.getBlock().getType().isSolid()) { - return m; - } - } - - return 0; - } - - private Entity doSpawn(Location at) { - if (!Chunks.isSafe(at)) { - return null; - } - - if (type.equals(EntityType.UNKNOWN) && !isSpecialType()) { - return null; - } - - if (!Bukkit.isPrimaryThread()) { - // Someone called spawn (worldedit maybe?) on a non server thread - // Due to the structure of iris, we will call it sync and busy wait until it's done. - AtomicReference ae = new AtomicReference<>(); - - try { - J.s(() -> ae.set(doSpawn(at))); - } catch (Throwable e) { - return null; - } - PrecisionStopwatch p = PrecisionStopwatch.start(); - - while (ae.get() == null) { - J.sleep(25); - - if (p.getMilliseconds() > 500) { - return null; - } - } - - return ae.get(); - } - - if (isSpecialType()) { - return Iris.service(ExternalDataSVC.class).spawnMob(at, Identifier.fromString(specialType)); - } - - - return INMS.get().spawnEntity(at, getType(), getReason()); - } - - public boolean isSpecialType() { - return specialType != null && !specialType.equals(""); - } - - @Override - public String getFolderName() { - return "entities"; - } - - @Override - public String getTypeName() { - return "Entity"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisEntitySpawn.java b/core/src/main/java/com/volmit/iris/engine/object/IrisEntitySpawn.java deleted file mode 100644 index a0eb9500d..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisEntitySpawn.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.math.Vector3d; -import com.volmit.iris.util.matter.MatterMarker; -import com.volmit.iris.util.matter.slices.MarkerMatter; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.entity.Entity; - -@Snippet("entity-spawn") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents an entity spawn during initial chunk generation") -@Data -public class IrisEntitySpawn implements IRare { - private final transient AtomicCache rng = new AtomicCache<>(); - private final transient AtomicCache ent = new AtomicCache<>(); - @RegistryListResource(IrisEntity.class) - @Required - @Desc("The entity") - private String entity = ""; - @Desc("The energy multiplier when calculating spawn energy usage") - private double energyMultiplier = 1; - @MinNumber(1) - @Desc("The 1 in RARITY chance for this entity to spawn") - private int rarity = 1; - @MinNumber(1) - @Desc("The minumum of this entity to spawn") - private int minSpawns = 1; - @MinNumber(1) - @Desc("The max of this entity to spawn") - private int maxSpawns = 1; - private transient IrisSpawner referenceSpawner; - private transient IrisMarker referenceMarker; - - public int spawn(Engine gen, Chunk c, RNG rng) { - int spawns = minSpawns == maxSpawns ? minSpawns : rng.i(Math.min(minSpawns, maxSpawns), Math.max(minSpawns, maxSpawns)); - int s = 0; - - if (spawns > 0) { - for (int id = 0; id < spawns; id++) { - int x = (c.getX() * 16) + rng.i(15); - int z = (c.getZ() * 16) + rng.i(15); - int h = gen.getHeight(x, z, true) + (gen.getWorld().tryGetRealWorld() ? gen.getWorld().realWorld().getMinHeight() : -64); - int hf = gen.getHeight(x, z, false) + (gen.getWorld().tryGetRealWorld() ? gen.getWorld().realWorld().getMinHeight() : -64); - Location l = switch (getReferenceSpawner().getGroup()) { - case NORMAL -> new Location(c.getWorld(), x, hf + 1, z); - case CAVE -> gen.getMantle().findMarkers(c.getX(), c.getZ(), MarkerMatter.CAVE_FLOOR) - .convert((i) -> i.toLocation(c.getWorld()).add(0, 1, 0)).getRandom(rng); - case UNDERWATER, BEACH -> new Location(c.getWorld(), x, rng.i(h + 1, hf), z); - }; - - if (l != null) { - if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) { - if (referenceSpawner.getAllowedLightLevels().contains(l.getBlock().getLightLevel())) { - if (spawn100(gen, l) != null) { - s++; - } - } - } else { - if (spawn100(gen, l) != null) { - s++; - } - } - } - } - } - - return s; - } - - public int spawn(Engine gen, IrisPosition c, RNG rng) { - int spawns = minSpawns == maxSpawns ? minSpawns : rng.i(Math.min(minSpawns, maxSpawns), Math.max(minSpawns, maxSpawns)); - int s = 0; - - if (!gen.getWorld().tryGetRealWorld()) { - return 0; - } - - World world = gen.getWorld().realWorld(); - if (spawns > 0) { - - if (referenceMarker != null && referenceMarker.shouldExhaust()) { - gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class); - } - - for (int id = 0; id < spawns; id++) { - Location l = c.toLocation(world).add(0, 1, 0); - - if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) { - if (referenceSpawner.getAllowedLightLevels().contains(l.getBlock().getLightLevel())) { - if (spawn100(gen, l, true) != null) { - s++; - } - } - } else { - if (spawn100(gen, l, true) != null) { - s++; - } - } - } - } - - return s; - } - - public IrisEntity getRealEntity(Engine g) { - return ent.aquire(() -> g.getData().getEntityLoader().load(getEntity())); - } - - public Entity spawn(Engine g, Location at) { - if (getRealEntity(g) == null) { - return null; - } - - if (rng.aquire(() -> new RNG(g.getSeedManager().getEntity())).i(1, getRarity()) == 1) { - return spawn100(g, at); - } - - return null; - } - - private Entity spawn100(Engine g, Location at) { - return spawn100(g, at, false); - } - - private Entity spawn100(Engine g, Location at, boolean ignoreSurfaces) { - try { - IrisEntity irisEntity = getRealEntity(g); - if (irisEntity == null) { // No entity - Iris.debug(" You are trying to spawn an entity that does not exist!"); - return null; - } - - if (!ignoreSurfaces && !irisEntity.getSurface().matches(at.clone().subtract(0, 1, 0).getBlock())) { - return null; - } - - Vector3d boundingBox = INMS.get().getBoundingbox(irisEntity.getType()); - if (!ignoreSurfaces && boundingBox != null) { - boolean isClearForSpawn = isAreaClearForSpawn(at, boundingBox); - if (!isClearForSpawn) { - return null; - } - } - - Entity e = irisEntity.spawn(g, at.add(0.5, 0.5, 0.5), rng.aquire(() -> new RNG(g.getSeedManager().getEntity()))); - if (e != null) { - Iris.debug("Spawned " + C.DARK_AQUA + "Entity<" + getEntity() + "> " + C.GREEN + e.getType() + C.LIGHT_PURPLE + " @ " + C.GRAY + e.getLocation().getX() + ", " + e.getLocation().getY() + ", " + e.getLocation().getZ()); - } - - return e; - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - Iris.error(" Failed to retrieve real entity @ " + at + " (entity: " + getEntity() + ")"); - return null; - } - } - - private boolean isAreaClearForSpawn(Location center, Vector3d boundingBox) { - World world = center.getWorld(); - int startX = center.getBlockX() - (int) (boundingBox.x / 2); - int endX = center.getBlockX() + (int) (boundingBox.x / 2); - int startY = center.getBlockY(); - int endY = center.getBlockY() + (int) boundingBox.y; - int startZ = center.getBlockZ() - (int) (boundingBox.z / 2); - int endZ = center.getBlockZ() + (int) (boundingBox.z / 2); - - for (int x = startX; x <= endX; x++) { - for (int y = startY; y <= endY; y++) { - for (int z = startZ; z <= endZ; z++) { - if (world.getBlockAt(x, y, z).getType() != Material.AIR) { - return false; - } - } - } - } - return true; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisExpressionLoad.java b/core/src/main/java/com/volmit/iris/engine/object/IrisExpressionLoad.java deleted file mode 100644 index c539cc3f3..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisExpressionLoad.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.stream.ProceduralStream; -import lombok.*; -import lombok.experimental.Accessors; - -@Snippet("expression-load") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a variable to use in your expression. Do not set the name to x, y, or z, also don't duplicate names.") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisExpressionLoad { - @Required - @Desc("The variable to assign this value to. Do not set the name to x, y, or z") - private String name = ""; - - @Desc("If the style value is not defined, this value will be used") - private double staticValue = -1; - - @Desc("If defined, this variable will use a generator style as it's value") - private IrisGeneratorStyle styleValue = null; - - @Desc("If defined, iris will use an internal stream from the engine as it's value") - private IrisEngineStreamType engineStreamValue = null; - - @Desc("If defined, iris will use an internal value from the engine as it's value") - private IrisEngineValueType engineValue = null; - - private transient AtomicCache> streamCache = new AtomicCache<>(); - private transient AtomicCache valueCache = new AtomicCache<>(); - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - private transient final KMap styleCache = new KMap<>(); - - public double getValue(RNG rng, IrisData data, double x, double z) { - if (engineValue != null) { - return valueCache.aquire(() -> engineValue.get(data.getEngine())); - } - - if (engineStreamValue != null) { - return streamCache.aquire(() -> engineStreamValue.get(data.getEngine())).get(x, z); - } - - if (styleValue != null) { - return styleCache.computeIfAbsent(rng.getSeed(), k -> styleValue.createNoCache(new RNG(k), data)) - .noise(x, z); - } - - return staticValue; - } - - public double getValue(RNG rng, IrisData data, double x, double y, double z) { - if (engineValue != null) { - return valueCache.aquire(() -> engineValue.get(data.getEngine())); - } - - if (engineStreamValue != null) { - return streamCache.aquire(() -> engineStreamValue.get(data.getEngine())).get(x, z); - } - - if (styleValue != null) { - return styleCache.computeIfAbsent(rng.getSeed(), k -> styleValue.createNoCache(new RNG(k), data)) - .noise(x, y, z); - } - - return staticValue; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisFluidBodies.java b/core/src/main/java/com/volmit/iris/engine/object/IrisFluidBodies.java deleted file mode 100644 index 69b44a65a..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisFluidBodies.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("fluid-bodies") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a fluid body configuration") -@Data -public class IrisFluidBodies { - @ArrayType(type = IrisRiver.class, min = 1) - @Desc("Define rivers") - private KList rivers = new KList<>(); - - @ArrayType(type = IrisLake.class, min = 1) - @Desc("Define lakes") - private KList lakes = new KList<>(); - - @BlockCoordinates - public void generate(MantleWriter writer, RNG rng, Engine engine, int x, int y, int z) { - if (rivers.isNotEmpty()) { - for (IrisRiver i : rivers) { - i.generate(writer, rng, engine, x, y, z); - } - } - - if (lakes.isNotEmpty()) { - for (IrisLake i : lakes) { - i.generate(writer, rng, engine, x, y, z); - } - } - } - - public int getMaxRange(IrisData data) { - int max = 0; - - for (IrisRiver i : rivers) { - max = Math.max(max, i.getSize(data)); - } - - for (IrisLake i : lakes) { - max = Math.max(max, i.getSize(data)); - } - - - return max; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisFontStyle.java b/core/src/main/java/com/volmit/iris/engine/object/IrisFontStyle.java deleted file mode 100644 index ebfe36389..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisFontStyle.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; - -@Desc("Represents a basic font style to apply to a font family") -public enum IrisFontStyle { - @Desc("Plain old text") - PLAIN, - - @Desc("Italicized Text") - ITALIC, - - @Desc("Bold Text") - BOLD, -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisGeneratorStyle.java b/core/src/main/java/com/volmit/iris/engine/object/IrisGeneratorStyle.java deleted file mode 100644 index 2e9996d6e..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisGeneratorStyle.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.noise.ExpressionNoise; -import com.volmit.iris.util.noise.ImageNoise; -import com.volmit.iris.util.noise.NoiseGenerator; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.util.Objects; - -@Snippet("style") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("A gen style") -@Data -public class IrisGeneratorStyle { - private final transient AtomicCache cng = new AtomicCache<>(); - @Desc("The chance is 1 in CHANCE per interval") - private NoiseStyle style = NoiseStyle.FLAT; - - @Desc("If set above 0, this style will be cellularized") - private double cellularFrequency = 0; - - @Desc("Cell zooms") - private double cellularZoom = 1; - @MinNumber(0.00001) - @Desc("The zoom of this style") - private double zoom = 1; - @Desc("Instead of using the style property, use a custom expression to represent this style.") - @RegistryListResource(IrisExpression.class) - private String expression = null; - @Desc("Use an Image map instead of a generated value") - private IrisImageMap imageMap = null; - @Desc("Instead of using the style property, use a custom noise generator to represent this style.\nFile extension: .noise.kts") - @RegistryListResource(IrisScript.class) - private String script = null; - @MinNumber(0.00001) - @Desc("The Output multiplier. Only used if parent is fracture.") - private double multiplier = 1; - @Desc("If set to true, each dimension will be fractured with a different order of input coordinates. This is usually 2 or 3 times slower than normal.") - private boolean axialFracturing = false; - @Desc("Apply a generator to the coordinate field fed into this parent generator. I.e. Distort your generator with another generator.") - private IrisGeneratorStyle fracture = null; - @MinNumber(0.01562) - @MaxNumber(64) - @Desc("The exponent") - private double exponent = 1; - @MinNumber(0) - @MaxNumber(8192) - @Desc("If the cache size is set above 0, this generator will be cached") - private int cacheSize = 0; - - public IrisGeneratorStyle(NoiseStyle s) { - this.style = s; - } - - public IrisGeneratorStyle zoomed(double z) { - this.zoom = z; - return this; - } - - public CNG createNoCache(RNG rng, IrisData data) { - return createNoCache(rng, data, false); - } - - - private int hash() { - return Objects.hash(expression, imageMap, multiplier, axialFracturing, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style); - } - - public CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached) { - String cacheKey = hash() + ""; - - CNG cng = null; - if (getExpression() != null) { - IrisExpression e = data.getExpressionLoader().load(getExpression()); - if (e != null) { - cng = new CNG(rng, new ExpressionNoise(rng, e), 1D, 1).bake(); - } - } else if (getImageMap() != null) { - cng = new CNG(rng, new ImageNoise(data, getImageMap()), 1D, 1).bake(); - } else if (getScript() != null) { - Object result = data.getEnvironment().createNoise(getScript(), rng); - if (result == null) Iris.warn("Failed to create noise from script: " + getScript()); - if (result instanceof NoiseGenerator generator) { - cng = new CNG(rng, generator, 1D, 1).bake(); - } - } - - if (cng == null) { - cng = style.create(rng).bake(); - } - - cng = cng.scale(1D / zoom).pow(exponent).bake(); - cng.setTrueFracturing(axialFracturing); - - if (fracture != null) { - cng.fractureWith(fracture.create(rng.nextParallelRNG(2934), data), fracture.getMultiplier()); - } - - if (cellularFrequency > 0) { - return cng.cellularize(rng.nextParallelRNG(884466), cellularFrequency).scale(1D / cellularZoom).bake(); - } - - return cng; - } - - public double warp(RNG rng, IrisData data, double value, double... coords) { - return create(rng, data).noise(coords) + value; - } - - public CNG create(RNG rng, IrisData data) { - return cng.aquire(() -> createNoCache(rng, data, true)); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean isFlat() { - return style.equals(NoiseStyle.FLAT); - } - - public double getMaxFractureDistance() { - return multiplier; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisInterpolator.java b/core/src/main/java/com/volmit/iris/engine/object/IrisInterpolator.java deleted file mode 100644 index 5834fc799..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisInterpolator.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MaxNumber; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.util.function.NoiseProvider; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.util.Objects; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Configures rotation for iris") -@Data -public class IrisInterpolator { - public static final IrisInterpolator DEFAULT = new IrisInterpolator(); - - @Required - @Desc("The interpolation method when two biomes use different heights but this same generator") - private InterpolationMethod function = InterpolationMethod.BILINEAR_STARCAST_6; - - @Required - @MinNumber(1) - @MaxNumber(8192) - @Desc("The range checked horizontally. Smaller ranges yeild more detail but are not as smooth.") - private double horizontalScale = 7; - - @Override - public int hashCode() { - return Objects.hash(horizontalScale, function); - } - - @Override - public boolean equals(Object o) { - if (o instanceof IrisInterpolator i) { - return i.getFunction().equals(function) && i.getHorizontalScale() == horizontalScale; - } - - return false; - } - - public double interpolate(double x, double z, NoiseProvider provider) { - return interpolate((int) Math.round(x), (int) Math.round(z), provider); - } - - public double interpolate(int x, int z, NoiseProvider provider) { - return IrisInterpolation.getNoise(getFunction(), x, z, getHorizontalScale(), provider); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisInterpolator3D.java b/core/src/main/java/com/volmit/iris/engine/object/IrisInterpolator3D.java deleted file mode 100644 index 7c3caf098..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisInterpolator3D.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.function.NoiseProvider3; -import com.volmit.iris.util.interpolation.InterpolationMethod3D; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("interpolator-3d") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Configures interpolatin in 3D") -@Data -public class IrisInterpolator3D { - @Required - @Desc("The interpolation method when two biomes use different heights but this same generator") - private InterpolationMethod3D function = InterpolationMethod3D.TRILINEAR; - - @Required - @MinNumber(1) - @MaxNumber(8192) - @Desc("The range checked in all dimensions. Smaller ranges yeild more detail but are not as smooth.") - private double scale = 4; - - public double interpolate(double x, double y, double z, NoiseProvider3 provider) { - return interpolate((int) Math.round(x), (int) Math.round(y), (int) Math.round(z), provider); - } - - public double interpolate(int x, int y, int z, NoiseProvider3 provider) { - return IrisInterpolation.getNoise3D(getFunction(), x, y, z, getScale(), provider); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawMinDistance.java b/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawMinDistance.java deleted file mode 100644 index 818f4b3dd..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawMinDistance.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.RegistryListResource; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.engine.object.annotations.Snippet; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("jigsaw-structure-min-distance") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents the min distance between jigsaw structure placements") -@Data -public class IrisJigsawMinDistance { - @Required - @RegistryListResource(IrisJigsawStructure.class) - @Desc("The structure to check against") - private String structure; - - @Required - @MinNumber(0) - @Desc("The min distance in blocks to a placed structure\nWARNING: The performance impact scales exponentially!") - private int distance; -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawPiece.java b/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawPiece.java deleted file mode 100644 index 09d94a6d4..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawPiece.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.RegistryListResource; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.util.BlockVector; - -import java.io.IOException; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor - -@Desc("Represents a structure tile") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisJigsawPiece extends IrisRegistrant { - @RegistryListResource(IrisObject.class) - @Required - @Desc("The object this piece represents") - private String object = ""; - - @ArrayType(type = IrisJigsawPieceConnector.class) - @Desc("The connectors this object contains") - private KList connectors = new KList<>(); - - @Desc("Configure everything about the object placement. Please don't define this unless you actually need it as using this option will slow down the jigsaw deign stage. Use this where you need it, just avoid using it everywhere to keep things fast.") - private IrisObjectPlacement placementOptions = new IrisObjectPlacement().setMode(ObjectPlaceMode.FAST_MAX_HEIGHT); - - private transient AtomicCache max2dDim = new AtomicCache<>(); - private transient AtomicCache max3dDim = new AtomicCache<>(); - - public int getMax2dDimension() { - return max2dDim.aquire(() -> { - try { - BlockVector v = IrisObject.sampleSize(getLoader().getObjectLoader().findFile(getObject())); - return Math.max(v.getBlockX(), v.getBlockZ()); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return 0; - }); - } - - public int getMax3dDimension() { - return max3dDim.aquire(() -> { - try { - BlockVector v = IrisObject.sampleSize(getLoader().getObjectLoader().findFile(getObject())); - return Math.max(Math.max(v.getBlockX(), v.getBlockZ()), v.getBlockY()); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return -1; - }); - } - - - public IrisJigsawPieceConnector getConnector(IrisPosition relativePosition) { - for (IrisJigsawPieceConnector i : connectors) { - if (i.getPosition().equals(relativePosition)) { - return i; - } - } - - return null; - } - - public IrisJigsawPiece copy() { - var gson = getLoader().getGson(); - IrisJigsawPiece copy = gson.fromJson(gson.toJson(this), IrisJigsawPiece.class); - copy.setLoader(getLoader()); - copy.setLoadKey(getLoadKey()); - copy.setLoadFile(getLoadFile()); - return copy; - } - - public boolean isTerminal() { - return connectors.size() == 1; - } - - public ObjectPlaceMode getPlaceMode() { - return getPlacementOptions().getMode(); - } - - @Override - public String getFolderName() { - return "jigsaw-pieces"; - } - - @Override - public String getTypeName() { - return "Jigsaw Piece"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawPieceConnector.java b/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawPieceConnector.java deleted file mode 100644 index f3afe074a..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawPieceConnector.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor - -@Snippet("connector") -@Desc("Represents a structure tile") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisJigsawPieceConnector { - @Required - @Desc("The name of this connector, such as entry, or table node. This is a name for organization. Other connectors can specifically use targetName to target a specific connector type. Multiple connectors can use the same name.") - private String name = ""; - - @Required - @Desc("Target a piece's connector with the specified name. For any piece's connector, define * or don't define it.") - private String targetName = "*"; - - @Desc("Rotates the placed piece on this connector. If rotation is enabled, this connector will effectivley rotate, if this connector is facing the Z direction, then the connected piece would rotate in the X,Y direction in 90 degree segments.") - private boolean rotateConnector = false; - - @Desc("If set to true, this connector is allowed to place pieces inside of it's own piece. For example if you are adding a light post, or house on top of a path piece, you would set this to true to allow the piece to collide with the path bounding box.") - private boolean innerConnector = false; - - @RegistryListResource(IrisJigsawPool.class) - @Desc("Pick piece pools to place onto this connector") - @ArrayType(type = String.class, min = 1) - @Required - private KList pools = new KList<>(); - - @RegistryListResource(IrisEntity.class) - @Desc("Pick an entity to spawn on this connector") - private String spawnEntity; - - @Desc("Stop the entity from despawning") - private boolean keepEntity; - - @MaxNumber(50) - @MinNumber(1) - @Desc("The amount of entities to spawn (must be a whole number)") - private int entityCount = 1; - - @Desc("The relative position this connector is located at for connecting to other pieces") - @Required - private IrisPosition position = new IrisPosition(0, 0, 0); - - @Desc("The relative position to this connector to place entities at") - @DependsOn({"spawnEntity"}) - private IrisPosition entityPosition = null; - - @Desc("The direction this connector is facing. If the direction is set to UP, then pieces will place ABOVE the connector.") - @Required - private IrisDirection direction = IrisDirection.UP_POSITIVE_Y; - - @Desc("Lock the Y position of this connector") - private boolean lockY = false; - - public String toString() { - return direction.getFace().name() + "@(" + position.getX() + "," + position.getY() + "," + position.getZ() + ")"; - } - - public IrisJigsawPieceConnector copy() { - IrisJigsawPieceConnector c = new IrisJigsawPieceConnector(); - c.setInnerConnector(isInnerConnector()); - c.setTargetName(getTargetName()); - c.setPosition(getPosition().copy()); - c.setDirection(getDirection()); - c.setRotateConnector(isRotateConnector()); - c.setName(getName()); - c.setSpawnEntity(getSpawnEntity()); - c.setPools(getPools().copy()); - return c; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawPool.java b/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawPool.java deleted file mode 100644 index 91cd1ae18..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawPool.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.RegistryListResource; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor - -@Desc("Represents a structure piece pool") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisJigsawPool extends IrisRegistrant { - @RegistryListResource(IrisJigsawPiece.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("A list of structure piece pools") - private KList pieces = new KList<>(); - - @Override - public String getFolderName() { - return "jigsaw-pools"; - } - - @Override - public String getTypeName() { - return "Jigsaw Pool"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawStructure.java b/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawStructure.java deleted file mode 100644 index ed7a61e53..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawStructure.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.engine.object.annotations.functions.StructureKeyFunction; -import com.volmit.iris.engine.object.annotations.functions.StructureKeyOrTagFunction; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor - -@Desc("Represents a jigsaw structure") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisJigsawStructure extends IrisRegistrant { - @RegistryListFunction(StructureKeyFunction.class) - @ArrayType(min = 1, type = String.class) - @Desc("The datapack structures. Randomply chooses a structure to place\nIgnores every other setting") - private KList datapackStructures = new KList<>(); - - @RegistryListResource(IrisJigsawPiece.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("The starting pieces. Randomly chooses a starting piece, then connects pieces using the pools define in the starting piece.") - private KList pieces = new KList<>(); - - @MaxNumber(32) - @MinNumber(1) - @Desc("The maximum pieces that can step out from the center piece") - private int maxDepth = 9; - - @Desc("Jigsaw grows the parallax layer which slows iris down a bit. Since there are so many pieces, Iris takes the avg piece size and calculates the parallax radius from that. Unless your structures are using only the biggest pieces, your structure should fit in the chosen size fine. If you are seeing cut-off parts of your structures or broken terrain, turn this option on. This option will pick the biggest piece dimensions and multiply it by your (maxDepth+1) * 2 as the size to grow the parallax layer by. But typically keep this off.") - private boolean useMaxPieceSizeForParallaxRadius = false; - - @Desc("If set to true, iris will look for any pieces with only one connector in valid pools for edge connectors and attach them to 'terminate' the paths/piece connectors. Essentially it caps off ends. For example in a village, Iris would add houses to the ends of roads where possible. For terminators to be selected, they can only have one connector or they wont be chosen.") - private boolean terminate = true; - - @RegistryListResource(IrisJigsawPool.class) - @Desc("The pool to use when terminating pieces") - private String terminatePool = null; - - @Desc("Override the y range instead of placing on the height map") - private IrisStyledRange overrideYRange = null; - - @Desc("Force Y to a specific value") - private int lockY = -1; - - @Desc("Set to true to prevent rotating the initial structure piece") - private boolean disableInitialRotation = false; - - @RegistryListFunction(StructureKeyOrTagFunction.class) - @Desc("The minecraft key to use when creating treasure maps") - private String structureKey = null; - - @Desc("Force Place the whole structure") - private boolean forcePlace = false; - - private transient AtomicCache maxDimension = new AtomicCache<>(); - - private void loadPool(String p, KList pools, KList pieces) { - if (p.isEmpty()) { - return; - } - - IrisJigsawPool pool = getLoader().getJigsawPoolLoader().load(p); - - if (pool == null) { - Iris.warn("Can't find jigsaw pool: " + p); - return; - } - - for (String i : pool.getPieces()) { - if (pieces.addIfMissing(i)) { - loadPiece(i, pools, pieces); - } - } - } - - private void loadPiece(String p, KList pools, KList pieces) { - IrisJigsawPiece piece = getLoader().getJigsawPieceLoader().load(p); - - if (piece == null) { - Iris.warn("Can't find jigsaw piece: " + p); - return; - } - - for (IrisJigsawPieceConnector i : piece.getConnectors()) { - for (String j : i.getPools()) { - if (pools.addIfMissing(j)) { - loadPool(j, pools, pieces); - } - } - } - } - - public int getMaxDimension() { - return maxDimension.aquire(() -> { - if (datapackStructures.isNotEmpty()) { - return 0; - } - - if (useMaxPieceSizeForParallaxRadius) { - int max = 0; - KList pools = new KList<>(); - KList pieces = new KList<>(); - - for (String i : getPieces()) { - loadPiece(i, pools, pieces); - } - - for (String i : pieces) { - max = Math.max(max, getLoader().getJigsawPieceLoader().load(i).getMax3dDimension()); - } - - return max * (((getMaxDepth() + 1) * 2) + 1); - } else { - KList pools = new KList<>(); - KList pieces = new KList<>(); - - for (String i : getPieces()) { - loadPiece(i, pools, pieces); - } - - if (pieces.isEmpty()) { - int max = 0; - for (String i : getPieces()) { - max = Math.max(max, getLoader().getJigsawPieceLoader().load(i).getMax2dDimension()); - } - return max; - } - - int avg = 0; - - for (String i : pieces) { - avg += getLoader().getJigsawPieceLoader().load(i).getMax2dDimension(); - } - - return (avg / (!pieces.isEmpty() ? pieces.size() : 1)) * (((getMaxDepth() + 1) * 2) + 1); - } - }); - } - - @Override - public String getFolderName() { - return "jigsaw-structures"; - } - - @Override - public String getTypeName() { - return "Jigsaw Structure"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawStructurePlacement.java b/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawStructurePlacement.java deleted file mode 100644 index 5f0220321..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisJigsawStructurePlacement.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("jigsaw-structure-placement") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a jigsaw structure placer") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisJigsawStructurePlacement implements IRare { - @RegistryListResource(IrisJigsawStructure.class) - @Required - @Desc("The structure to place") - private String structure; - - @Required - @Desc("The 1 in X chance rarity applies when generating multiple structures at once") - private int rarity = 100; - - @Required - @DependsOn({"spacing", "separation"}) - @Desc("The salt to use when generating the structure (to differentiate structures)") - @MinNumber(Long.MIN_VALUE) - @MaxNumber(Long.MAX_VALUE) - private long salt = 0; - - @Required - @MinNumber(0) - @DependsOn({"salt", "separation"}) - @Desc("Average distance in chunks between two neighboring generation attempts") - private int spacing = -1; - - @Required - @MinNumber(0) - @DependsOn({"salt", "spacing"}) - @Desc("Minimum distance in chunks between two neighboring generation attempts\nThe maximum distance of two neighboring generation attempts is 2*spacing - separation") - private int separation = -1; - - @Desc("The method used to spread the structure") - private SpreadType spreadType = SpreadType.LINEAR; - - @DependsOn({"spreadType"}) - @Desc("The noise style to use when spreadType is set to 'NOISE'\nThis ignores the spacing and separation parameters") - private IrisGeneratorStyle style = new IrisGeneratorStyle(); - - @DependsOn({"spreadType", "style"}) - @Desc("Threshold for noise style") - private double threshold = 0.5; - - @ArrayType(type = IrisJigsawMinDistance.class) - @Desc("List of minimum distances to check for") - private KList minDistances = new KList<>(); - - public KMap collectMinDistances() { - KMap map = new KMap<>(); - for (IrisJigsawMinDistance d : minDistances) { - map.compute(d.getStructure(), (k, v) -> v != null ? Math.min(toChunks(d.getDistance()), v) : toChunks(d.getDistance())); - } - return map; - } - - private int toChunks(int blocks) { - return (int) Math.ceil(blocks / 16d); - } - - private void calculateMissing(double divisor, long seed) { - if (salt != 0 && separation > 0 && spacing > 0) - return; - seed *= (long) structure.hashCode() * rarity; - if (salt == 0) { - salt = new RNG(seed).l(Integer.MIN_VALUE, Integer.MAX_VALUE); - } - - if (separation == -1 || spacing == -1) { - separation = (int) Math.round(rarity / divisor); - spacing = new RNG(seed).i(separation, separation * 2); - } - } - - @ChunkCoordinates - public boolean shouldPlace(IrisData data, double divisor, long seed, int x, int z) { - calculateMissing(divisor, seed); - if (spreadType != SpreadType.NOISE) - return shouldPlaceSpread(seed, x, z); - - return style.create(new RNG(seed + salt), data).noise(x, z) > threshold; - } - - private boolean shouldPlaceSpread(long seed, int x, int z) { - if (separation > spacing) { - separation = spacing; - Iris.warn("JigsawStructurePlacement: separation must be less than or equal to spacing"); - } - - int i = Math.floorDiv(x, spacing); - int j = Math.floorDiv(z, spacing); - RNG rng = new RNG(i * 341873128712L + j * 132897987541L + seed + salt); - - int k = spacing - separation; - int l = spreadType.apply(rng, k); - int m = spreadType.apply(rng, k); - return i * spacing + l == x && j * spacing + m == z; - } - - @Desc("Spread type") - public enum SpreadType { - @Desc("Linear spread") - LINEAR(RNG::i), - @Desc("Triangular spread") - TRIANGULAR((rng, bound) -> (rng.i(bound) + rng.i(bound)) / 2), - @Desc("Noise based spread\nThis ignores the spacing and separation parameters") - NOISE((rng, bound) -> 0); - private final SpreadMethod method; - - SpreadType(SpreadMethod method) { - this.method = method; - } - - public int apply(RNG rng, int bound) { - return method.apply(rng, bound); - } - } - - private interface SpreadMethod { - int apply(RNG rng, int bound); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisLake.java b/core/src/main/java/com/volmit/iris/engine/object/IrisLake.java deleted file mode 100644 index 49c306f17..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisLake.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("lake") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents an Iris Lake") -@Data -public class IrisLake implements IRare { - @Required - @Desc("Typically a 1 in RARITY on a per chunk/fork basis") - @MinNumber(1) - private int rarity = 15; - - @Desc("The width style of this lake") - private IrisShapedGeneratorStyle widthStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN.style(), 5, 9); - - @Desc("The depth style of this lake") - private IrisShapedGeneratorStyle depthStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN.style(), 4, 7); - - @Desc("Define the shape of this lake") - private IrisWorm worm = new IrisWorm(); - - @RegistryListResource(IrisBiome.class) - @Desc("Force this lake to only generate the specified custom biome") - private String customBiome = ""; - - public int getSize(IrisData data) { - return worm.getMaxDistance(); - } - - public void generate(MantleWriter writer, RNG rng, Engine engine, int x, int y, int z) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisLoot.java b/core/src/main/java/com/volmit/iris/engine/object/IrisLoot.java deleted file mode 100644 index ae7f5e121..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisLoot.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.DyeColor; -import org.bukkit.Material; -import org.bukkit.inventory.ItemFlag; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.Damageable; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.meta.LeatherArmorMeta; -import org.bukkit.material.Colorable; - -import java.awt.*; -import java.util.Optional; - -@Snippet("loot") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a loot entry") -@Data -public class IrisLoot { - private final transient AtomicCache chance = new AtomicCache<>(); - @Desc("The target inventory slot types to fill this loot with") - private InventorySlotType slotTypes = InventorySlotType.STORAGE; - @MinNumber(1) - @Desc("The sub rarity of this loot. Calculated after this loot table has been picked.") - private int rarity = 1; - @MinNumber(1) - @Desc("Minimum amount of this loot") - private int minAmount = 1; - @MinNumber(1) - @Desc("Maximum amount of this loot") - private int maxAmount = 1; - @MinNumber(1) - @Desc("The display name of this item") - private String displayName = null; - @MinNumber(0) - @MaxNumber(1) - @Desc("Minimum durability percent") - private double minDurability = 0; - @MinNumber(0) - @MaxNumber(1) - @Desc("Maximum durability percent") - private double maxDurability = 1; - @Desc("Define a custom model identifier 1.14+ only") - private Integer customModel = null; - @Desc("Set this to true to prevent it from being broken") - private boolean unbreakable = false; - @ArrayType(min = 1, type = ItemFlag.class) - @Desc("The item flags to add") - private KList itemFlags = new KList<>(); - @Desc("Apply enchantments to this item") - @ArrayType(min = 1, type = IrisEnchantment.class) - private KList enchantments = new KList<>(); - @Desc("Apply attribute modifiers to this item") - @ArrayType(min = 1, type = IrisAttributeModifier.class) - private KList attributes = new KList<>(); - @ArrayType(min = 1, type = String.class) - @Desc("Add lore to this item") - private KList lore = new KList<>(); - @RegistryListItemType - @Required - @Desc("This is the item or block type. Does not accept minecraft:*, only materials such as DIAMOND_SWORD or DIRT. The exception are modded materials, as they require a namespace.") - private String type = ""; - @Desc("The dye color") - private DyeColor dyeColor = null; - @Desc("The leather armor color") - private String leatherColor = null; - @Desc("Defines a custom NBT Tag for the item.") - private KMap customNbt; - - public Material getType() { - return B.getMaterial(type); - } - - public ItemStack get(boolean debug, RNG rng) { - try { - ItemStack is = getItemStack(rng); - if (is == null) - return new ItemStack(Material.AIR); - is.setItemMeta(applyProperties(is, rng, debug, null)); - return INMS.get().applyCustomNbt(is, customNbt); - } catch (Throwable e) { - Iris.reportError(e); - return new ItemStack(Material.AIR); - } - } - - public ItemStack get(boolean debug, boolean giveSomething, IrisLootTable table, RNG rng, int x, int y, int z) { - if (debug) { - chance.reset(); - } - - if (giveSomething || chance.aquire(() -> NoiseStyle.STATIC.create(rng)).fit(1, rarity * table.getRarity(), x, y, z) == 1) { - try { - ItemStack is = getItemStack(rng); - if (is == null) - return null; - is.setItemMeta(applyProperties(is, rng, debug, table)); - return INMS.get().applyCustomNbt(is, customNbt); - } catch (Throwable e) { - //Iris.reportError(e); - e.printStackTrace(); - } - } - - return null; - } - - // TODO Better Third Party Item Acquisition - private ItemStack getItemStack(RNG rng) { - if (!type.startsWith("minecraft:") && type.contains(":")) { - Optional opt = Iris.service(ExternalDataSVC.class).getItemStack(Identifier.fromString(type), customNbt); - if (opt.isEmpty()) { - Iris.warn("Unknown Material: " + type); - return new ItemStack(Material.AIR); - } - ItemStack is = opt.get(); - is.setAmount(Math.max(1, rng.i(getMinAmount(), getMaxAmount()))); - return is; - } - return new ItemStack(getType(), Math.max(1, rng.i(getMinAmount(), getMaxAmount()))); - } - - private ItemMeta applyProperties(ItemStack is, RNG rng, boolean debug, IrisLootTable table) { - ItemMeta m = is.getItemMeta(); - if (m == null) { - return null; - } - - for (IrisEnchantment i : getEnchantments()) { - i.apply(rng, m); - } - - for (IrisAttributeModifier i : getAttributes()) { - i.apply(rng, m); - } - - m.setUnbreakable(isUnbreakable()); - for (ItemFlag i : getItemFlags()) { - m.addItemFlags(i); - } - - if (getCustomModel() != null) { - m.setCustomModelData(getCustomModel()); - } - - if (is.getType().getMaxDurability() > 0 && m instanceof Damageable d) { - int max = is.getType().getMaxDurability(); - d.setDamage((int) Math.round(Math.max(0, Math.min(max, (1D - rng.d(getMinDurability(), getMaxDurability())) * max)))); - } - - if (getLeatherColor() != null && m instanceof LeatherArmorMeta leather) { - Color c = Color.decode(getLeatherColor()); - leather.setColor(org.bukkit.Color.fromRGB(c.getRed(), c.getGreen(), c.getBlue())); - } - - if (getDyeColor() != null && m instanceof Colorable colorable) { - colorable.setColor(getDyeColor()); - } - - if (displayName != null) { - m.setLocalizedName(C.translateAlternateColorCodes('&', displayName)); - m.setDisplayName(C.translateAlternateColorCodes('&', displayName)); - } - - KList lore = new KList<>(); - - getLore().forEach((i) -> - { - String mf = C.translateAlternateColorCodes('&', i); - - if (mf.length() > 24) { - for (String g : Form.wrapWords(mf, 24).split("\\Q\n\\E")) { - lore.add(g.trim()); - } - } else { - lore.add(mf); - } - }); - - if (debug) { - if (table == null) { - if (lore.isNotEmpty()) { - lore.add(C.GRAY + "--------------------"); - } - lore.add(C.GRAY + "1 in " + (getRarity()) + " Chance (" + Form.pc(1D / (getRarity()), 5) + ")"); - } else { - if (lore.isNotEmpty()) { - lore.add(C.GRAY + "--------------------"); - } - - lore.add(C.GRAY + "From: " + table.getName() + " (" + Form.pc(1D / table.getRarity(), 5) + ")"); - lore.add(C.GRAY + "1 in " + (table.getRarity() * getRarity()) + " Chance (" + Form.pc(1D / (table.getRarity() * getRarity()), 5) + ")"); - } - } - - m.setLore(lore); - - return m; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisLootMode.java b/core/src/main/java/com/volmit/iris/engine/object/IrisLootMode.java deleted file mode 100644 index b8d6b1341..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisLootMode.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; - -@Desc("A loot mode is used to describe what to do with the existing loot layers before adding this loot. Using ADD will simply add this table to the building list of tables (i.e. add dimension tables, region tables then biome tables). By using clear or replace, you remove the parent tables before and add just your tables.") -public enum IrisLootMode { - @Desc("Add to the existing parent loot tables") - ADD, - @Desc("Clear all loot tables then add this table") - CLEAR, - @Desc("Replace all loot tables with this table (same as clear)") - REPLACE, - @Desc("Only use when there was no loot table defined by an object") - FALLBACK -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisLootReference.java b/core/src/main/java/com/volmit/iris/engine/object/IrisLootReference.java deleted file mode 100644 index 55a837c94..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisLootReference.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.data.DataProvider; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("loot-registry") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a loot entry") -@Data -public class IrisLootReference { - private final transient AtomicCache> tt = new AtomicCache<>(); - @Desc("Add = add on top of parent tables, Replace = clear first then add these. Clear = Remove all and dont add loot from this or parent.") - private IrisLootMode mode = IrisLootMode.ADD; - @RegistryListResource(IrisLootTable.class) - @ArrayType(min = 1, type = String.class) - @Desc("Add loot table registries here") - private KList tables = new KList<>(); - @MinNumber(0) - @Desc("Increase the chance of loot in this area") - private double multiplier = 1D; - - public KList getLootTables(DataProvider g) { - return tt.aquire(() -> - { - KList t = new KList<>(); - - for (String i : tables) { - t.add(g.getData().getLootLoader().load(i)); - } - - return t; - }); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisLootTable.java b/core/src/main/java/com/volmit/iris/engine/object/IrisLootTable.java deleted file mode 100644 index 07e13e875..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisLootTable.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.inventory.ItemStack; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a loot table. Biomes, Regions & Objects can add or replace the virtual table with these loot tables") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisLootTable extends IrisRegistrant { - @Required - @Desc("The name of this loot table") - @MinNumber(2) - private String name = ""; - - @MinNumber(1) - @Desc("The rarity as in 1 in X chance") - private int rarity = 1; - - @MinNumber(1) - @Desc("The maximum amount of loot that can be picked in this table at a time.") - private int maxPicked = 5; - - @MinNumber(0) - @Desc("The minimum amount of loot that can be picked in this table at a time.") - private int minPicked = 1; - - @MinNumber(1) - @Desc("The maximum amount of tries to generate loot") - private int maxTries = 10; - - @Desc("The loot in this table") - @ArrayType(min = 1, type = IrisLoot.class) - private KList loot = new KList<>(); - - public KList getLoot(boolean debug, RNG rng, InventorySlotType slot, World world, int x, int y, int z) { - KList lootf = new KList<>(); - - int m = 0; - int c = 0; - int mx = rng.i(getMinPicked(), getMaxPicked()); - - while (m < mx && c++ < getMaxTries()) { - int num = rng.i(loot.size()); - - IrisLoot l = loot.get(num); - - if (l.getSlotTypes() == slot) { - ItemStack item = l.get(debug, false, this, rng, x, y, z); - - if (item != null && item.getType() != Material.AIR) { - lootf.add(item); - m++; - } - } - } - - return lootf; - } - - @Override - public String getFolderName() { - return "loot"; - } - - @Override - public String getTypeName() { - return "Loot"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisMaterialPalette.java b/core/src/main/java/com/volmit/iris/engine/object/IrisMaterialPalette.java deleted file mode 100644 index 830af6457..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisMaterialPalette.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -import java.util.Optional; - -@Snippet("palette") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("A palette of materials") -@Data -public class IrisMaterialPalette { - private final transient AtomicCache> blockData = new AtomicCache<>(); - private final transient AtomicCache layerGenerator = new AtomicCache<>(); - private final transient AtomicCache heightGenerator = new AtomicCache<>(); - @Desc("The style of noise") - private IrisGeneratorStyle style = NoiseStyle.STATIC.style(); - @MinNumber(0.0001) - @Desc("The terrain zoom mostly for zooming in on a wispy palette") - private double zoom = 5; - @Required - @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("The palette of blocks to be used in this layer") - private KList palette = new KList().qadd(new IrisBlockData("STONE")); - - public BlockData get(RNG rng, double x, double y, double z, IrisData rdata) { - if (getBlockData(rdata).isEmpty()) { - return null; - } - - if (getBlockData(rdata).size() == 1) { - return getBlockData(rdata).get(0); - } - - return getLayerGenerator(rng, rdata).fit(getBlockData(rdata), x / zoom, y / zoom, z / zoom); - } - - public Optional getTile(RNG rng, double x, double y, double z, IrisData rdata) { - if (getBlockData(rdata).isEmpty()) - return Optional.empty(); - - TileData tile = getBlockData(rdata).size() == 1 ? palette.get(0).tryGetTile(rdata) : palette.getRandom(rng).tryGetTile(rdata); - return tile != null ? Optional.of(tile) : Optional.empty(); - } - - public CNG getLayerGenerator(RNG rng, IrisData rdata) { - return layerGenerator.aquire(() -> - { - RNG rngx = rng.nextParallelRNG(-23498896 + getBlockData(rdata).size()); - return style.create(rngx, rdata); - }); - } - - public IrisMaterialPalette qclear() { - palette.clear(); - return this; - } - - public KList add(String b) { - palette.add(new IrisBlockData(b)); - - return palette; - } - - public IrisMaterialPalette qadd(String b) { - palette.add(new IrisBlockData(b)); - - return this; - } - - public KList getBlockData(IrisData rdata) { - return blockData.aquire(() -> - { - KList blockData = new KList<>(); - for (IrisBlockData ix : palette) { - BlockData bx = ix.getBlockData(rdata); - if (bx != null) { - for (int i = 0; i < ix.getWeight(); i++) { - blockData.add(bx); - } - } - } - - return blockData; - }); - } - - public IrisMaterialPalette zero() { - palette.clear(); - return this; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisModObjectPlacementRegionInjector.java b/core/src/main/java/com/volmit/iris/engine/object/IrisModObjectPlacementRegionInjector.java deleted file mode 100644 index 70b0e3d5e..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisModObjectPlacementRegionInjector.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("object-placement-region-injector") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("An object placement injector") -@Data -public class IrisModObjectPlacementRegionInjector { - @Required - @Desc("The biome to find") - @RegistryListResource(IrisRegion.class) - private String biome = ""; - - @Required - @Desc("A biome to inject into the region") - @ArrayType(type = IrisObjectPlacement.class, min = 1) - private KList place = new KList<>(); -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisModObjectReplacer.java b/core/src/main/java/com/volmit/iris/engine/object/IrisModObjectReplacer.java deleted file mode 100644 index 90a400da4..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisModObjectReplacer.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("object-replacer") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("A biome replacer") -@Data -public class IrisModObjectReplacer { - @Required - @Desc("A list of objects to find") - @RegistryListResource(IrisObject.class) - @ArrayType(type = String.class, min = 1) - private KList find = new KList<>(); - - @Required - @Desc("An object to replace it with") - @RegistryListResource(IrisBiome.class) - private String replace = ""; -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObject.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObject.java deleted file mode 100644 index fa3057dec..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObject.java +++ /dev/null @@ -1,1427 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.PlacedObject; -import com.volmit.iris.engine.framework.placer.HeightmapObjectPlacer; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.data.VectorMap; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.*; -import com.volmit.iris.util.matter.MatterMarker; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import com.volmit.iris.util.scheduling.jobs.Job; -import com.volmit.iris.util.stream.ProceduralStream; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.MultipleFacing; -import org.bukkit.block.data.Waterlogged; -import org.bukkit.block.data.type.Leaves; -import org.bukkit.util.BlockVector; -import org.bukkit.util.Vector; - -import java.io.*; -import java.util.*; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.BiConsumer; -import java.util.stream.StreamSupport; - -@Accessors(chain = true) -@EqualsAndHashCode(callSuper = false) -public class IrisObject extends IrisRegistrant { - protected static final Vector HALF = new Vector(0.5, 0.5, 0.5); - protected static final BlockData AIR = B.get("CAVE_AIR"); - protected static final BlockData VAIR = B.get("VOID_AIR"); - protected static final BlockData VAIR_DEBUG = B.get("COBWEB"); - protected static final BlockData[] SNOW_LAYERS = new BlockData[]{B.get("minecraft:snow[layers=1]"), B.get("minecraft:snow[layers=2]"), B.get("minecraft:snow[layers=3]"), B.get("minecraft:snow[layers=4]"), B.get("minecraft:snow[layers=5]"), B.get("minecraft:snow[layers=6]"), B.get("minecraft:snow[layers=7]"), B.get("minecraft:snow[layers=8]")}; - protected transient final Lock readLock; - protected transient final Lock writeLock; - @Getter - @Setter - protected transient volatile boolean smartBored = false; - @Setter - protected transient AtomicCache aabb = new AtomicCache<>(); - @Getter - private VectorMap blocks; - @Getter - private VectorMap states; - @Getter - @Setter - private int w; - @Getter - @Setter - private int d; - @Getter - @Setter - private int h; - @Getter - @Setter - private transient Vector3i center; - @Getter - private transient Vector3i shrinkOffset; - - public IrisObject(int w, int h, int d) { - blocks = new VectorMap<>(); - states = new VectorMap<>(); - this.w = w; - this.h = h; - this.d = d; - center = new Vector3i(w / 2, h / 2, d / 2); - shrinkOffset = new Vector3i(0, 0, 0); - var lock = new ReentrantReadWriteLock(); - readLock = lock.readLock(); - writeLock = lock.writeLock(); - } - - public IrisObject() { - this(0, 0, 0); - } - - public static BlockVector getCenterForSize(BlockVector size) { - return new BlockVector(size.getX() / 2, size.getY() / 2, size.getZ() / 2); - } - - public static AxisAlignedBB getAABBFor(BlockVector size) { - BlockVector center = new BlockVector(size.getX() / 2, size.getY() / 2, size.getZ() / 2); - return new AxisAlignedBB(new IrisPosition(new BlockVector(0, 0, 0).subtract(center).toBlockVector()), - new IrisPosition(new BlockVector(size.getX() - 1, size.getY() - 1, size.getZ() - 1).subtract(center).toBlockVector())); - } - - @SuppressWarnings({"resource", "RedundantSuppression"}) - public static BlockVector sampleSize(File file) throws IOException { - FileInputStream in = new FileInputStream(file); - DataInputStream din = new DataInputStream(in); - BlockVector bv = new BlockVector(din.readInt(), din.readInt(), din.readInt()); - Iris.later(din::close); - return bv; - } - - private static List blocksBetweenTwoPoints(Vector loc1, Vector loc2) { - List locations = new ArrayList<>(); - int topBlockX = Math.max(loc1.getBlockX(), loc2.getBlockX()); - int bottomBlockX = Math.min(loc1.getBlockX(), loc2.getBlockX()); - int topBlockY = Math.max(loc1.getBlockY(), loc2.getBlockY()); - int bottomBlockY = Math.min(loc1.getBlockY(), loc2.getBlockY()); - int topBlockZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ()); - int bottomBlockZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ()); - - for (int x = bottomBlockX; x <= topBlockX; x++) { - for (int z = bottomBlockZ; z <= topBlockZ; z++) { - for (int y = bottomBlockY; y <= topBlockY; y++) { - locations.add(new BlockVector(x, y, z)); - } - } - } - return locations; - } - - public AxisAlignedBB getAABB() { - return aabb.aquire(() -> getAABBFor(new BlockVector(w, h, d))); - } - - public void ensureSmartBored(boolean debug) { - if (smartBored) { - return; - } - - PrecisionStopwatch p = PrecisionStopwatch.start(); - BlockData vair = debug ? VAIR_DEBUG : VAIR; - writeLock.lock(); - AtomicInteger applied = new AtomicInteger(); - if (blocks.isEmpty()) { - writeLock.unlock(); - Iris.warn("Cannot Smart Bore " + getLoadKey() + " because it has 0 blocks in it."); - smartBored = true; - return; - } - - BlockVector max = new BlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE); - BlockVector min = new BlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); - - for (BlockVector i : blocks.keys()) { - max.setX(Math.max(i.getX(), max.getX())); - min.setX(Math.min(i.getX(), min.getX())); - max.setY(Math.max(i.getY(), max.getY())); - min.setY(Math.min(i.getY(), min.getY())); - max.setZ(Math.max(i.getZ(), max.getZ())); - min.setZ(Math.min(i.getZ(), min.getZ())); - } - - BurstExecutor burst = MultiBurst.burst.burst(); - - // Smash X - for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { - int finalRayY = rayY; - burst.queue(() -> { - for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; - - for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) { - if (blocks.containsKey(new Vector3i(ray, finalRayY, rayZ))) { - start = Math.min(ray, start); - end = Math.max(ray, end); - } - } - - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - for (int i = start; i <= end; i++) { - Vector3i v = new Vector3i(i, finalRayY, rayZ); - - if (!vair.equals(blocks.get(v))) { - blocks.computeIfAbsent(v, (vv) -> vair); - applied.getAndIncrement(); - } - } - } - } - }); - } - - // Smash Y - for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { - int finalRayX = rayX; - burst.queue(() -> { - for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; - - for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) { - if (blocks.containsKey(new Vector3i(finalRayX, ray, rayZ))) { - start = Math.min(ray, start); - end = Math.max(ray, end); - } - } - - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - for (int i = start; i <= end; i++) { - Vector3i v = new Vector3i(finalRayX, i, rayZ); - - if (!vair.equals(blocks.get(v))) { - blocks.computeIfAbsent(v, (vv) -> vair); - applied.getAndIncrement(); - } - } - } - } - }); - } - - // Smash Z - for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { - int finalRayX = rayX; - burst.queue(() -> { - for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; - - for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) { - if (blocks.containsKey(new Vector3i(finalRayX, rayY, ray))) { - start = Math.min(ray, start); - end = Math.max(ray, end); - } - } - - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - for (int i = start; i <= end; i++) { - Vector3i v = new Vector3i(finalRayX, rayY, i); - - if (!vair.equals(blocks.get(v))) { - blocks.computeIfAbsent(v, (vv) -> vair); - applied.getAndIncrement(); - } - } - } - } - }); - } - - burst.complete(); - smartBored = true; - writeLock.unlock(); - Iris.debug("Smart Bore: " + getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied.get()) + ")"); - } - - public synchronized IrisObject copy() { - IrisObject o = new IrisObject(w, h, d); - o.setLoadKey(o.getLoadKey()); - o.setLoader(getLoader()); - o.setLoadFile(getLoadFile()); - o.setCenter(getCenter().clone()); - - blocks.forEach((i, v) -> o.blocks.put(i.clone(), v.clone())); - states.forEach((i, v) -> o.states.put(i.clone(), v.clone())); - - return o; - } - - public void readLegacy(InputStream in) throws IOException { - DataInputStream din = new DataInputStream(in); - this.w = din.readInt(); - this.h = din.readInt(); - this.d = din.readInt(); - center = new Vector3i(w / 2, h / 2, d / 2); - int s = din.readInt(); - - for (int i = 0; i < s; i++) { - blocks.put(new Vector3i(din.readShort(), din.readShort(), din.readShort()), B.get(din.readUTF())); - } - - if (din.available() == 0) - return; - - try { - int size = din.readInt(); - - for (int i = 0; i < size; i++) { - states.put(new Vector3i(din.readShort(), din.readShort(), din.readShort()), TileData.read(din)); - } - } catch (Throwable e) { - Iris.reportError(e); - } - } - - public void read(InputStream in) throws Throwable { - DataInputStream din = new DataInputStream(in); - this.w = din.readInt(); - this.h = din.readInt(); - this.d = din.readInt(); - if (!din.readUTF().equals("Iris V2 IOB;")) { - throw new HeaderException(); - } - center = new Vector3i(w / 2, h / 2, d / 2); - int s = din.readShort(); - int i; - KList palette = new KList<>(); - - for (i = 0; i < s; i++) { - palette.add(din.readUTF()); - } - - s = din.readInt(); - - for (i = 0; i < s; i++) { - blocks.put(new Vector3i(din.readShort(), din.readShort(), din.readShort()), B.get(palette.get(din.readShort()))); - } - - s = din.readInt(); - - for (i = 0; i < s; i++) { - states.put(new Vector3i(din.readShort(), din.readShort(), din.readShort()), TileData.read(din)); - } - } - - public void write(OutputStream o) throws IOException { - DataOutputStream dos = new DataOutputStream(o); - dos.writeInt(w); - dos.writeInt(h); - dos.writeInt(d); - dos.writeUTF("Iris V2 IOB;"); - KList palette = new KList<>(); - - for (BlockData i : blocks.values()) { - palette.addIfMissing(i.getAsString()); - } - - dos.writeShort(palette.size()); - - for (String i : palette) { - dos.writeUTF(i); - } - - dos.writeInt(blocks.size()); - - for (var entry : blocks) { - var i = entry.getKey(); - dos.writeShort(i.getBlockX()); - dos.writeShort(i.getBlockY()); - dos.writeShort(i.getBlockZ()); - dos.writeShort(palette.indexOf(entry.getValue().getAsString())); - } - - dos.writeInt(states.size()); - for (var entry : states) { - var i = entry.getKey(); - dos.writeShort(i.getBlockX()); - dos.writeShort(i.getBlockY()); - dos.writeShort(i.getBlockZ()); - entry.getValue().toBinary(dos); - } - } - - public void write(OutputStream o, VolmitSender sender) throws IOException { - AtomicReference ref = new AtomicReference<>(); - CountDownLatch latch = new CountDownLatch(1); - new Job() { - private int total = blocks.size() * 3 + states.size(); - private int c = 0; - - @Override - public String getName() { - return "Saving Object"; - } - - @Override - public void execute() { - try { - DataOutputStream dos = new DataOutputStream(o); - dos.writeInt(w); - dos.writeInt(h); - dos.writeInt(d); - dos.writeUTF("Iris V2 IOB;"); - - KList palette = new KList<>(); - - for (BlockData i : blocks.values()) { - palette.addIfMissing(i.getAsString()); - ++c; - } - total -= blocks.size() - palette.size(); - - dos.writeShort(palette.size()); - - for (String i : palette) { - dos.writeUTF(i); - ++c; - } - - dos.writeInt(blocks.size()); - - for (var entry : blocks) { - var i = entry.getKey(); - dos.writeShort(i.getBlockX()); - dos.writeShort(i.getBlockY()); - dos.writeShort(i.getBlockZ()); - dos.writeShort(palette.indexOf(entry.getValue().getAsString())); - ++c; - } - - dos.writeInt(states.size()); - for (var entry : states) { - var i = entry.getKey(); - dos.writeShort(i.getBlockX()); - dos.writeShort(i.getBlockY()); - dos.writeShort(i.getBlockZ()); - entry.getValue().toBinary(dos); - ++c; - } - } catch (IOException e) { - ref.set(e); - } finally { - latch.countDown(); - } - } - - @Override - public void completeWork() {} - - @Override - public int getTotalWork() { - return total; - } - - @Override - public int getWorkCompleted() { - return c; - } - }.execute(sender, true, () -> {}); - - try { - latch.await(); - } catch (InterruptedException ignored) {} - if (ref.get() != null) - throw ref.get(); - } - - public void read(File file) throws IOException { - try (var fin = new BufferedInputStream(new FileInputStream(file))) { - read(fin); - } catch (Throwable e) { - if (!(e instanceof HeaderException)) - Iris.reportError(e); - try (var fin = new BufferedInputStream(new FileInputStream(file))) { - readLegacy(fin); - } - } - } - - public void write(File file) throws IOException { - if (file == null) { - return; - } - - FileOutputStream out = new FileOutputStream(file); - write(out); - out.close(); - } - - public void write(File file, VolmitSender sender) throws IOException { - if (file == null) { - return; - } - - FileOutputStream out = new FileOutputStream(file); - write(out, sender); - out.close(); - } - - public void shrinkwrap() { - if (blocks.isEmpty()) return; - BlockVector min = new BlockVector(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); - BlockVector max = new BlockVector(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); - - for (BlockVector i : blocks.keys()) { - min.setX(Math.min(min.getX(), i.getX())); - min.setY(Math.min(min.getY(), i.getY())); - min.setZ(Math.min(min.getZ(), i.getZ())); - max.setX(Math.max(max.getX(), i.getX())); - max.setY(Math.max(max.getY(), i.getY())); - max.setZ(Math.max(max.getZ(), i.getZ())); - } - - w = max.getBlockX() - min.getBlockX() + 1; - h = max.getBlockY() - min.getBlockY() + 1; - d = max.getBlockZ() - min.getBlockZ() + 1; - center = new Vector3i(w / 2, h / 2, d / 2); - - Vector3i offset = new Vector3i( - -center.getBlockX() - min.getBlockX(), - -center.getBlockY() - min.getBlockY(), - -center.getBlockZ() - min.getBlockZ() - ); - if (offset.getBlockX() == 0 && offset.getBlockY() == 0 && offset.getBlockZ() == 0) - return; - - VectorMap b = new VectorMap<>(); - VectorMap s = new VectorMap<>(); - - blocks.forEach((vector, data) -> { - vector.add(offset); - b.put(vector, data); - }); - - states.forEach((vector, data) -> { - vector.add(offset); - s.put(vector, data); - }); - - shrinkOffset = offset; - blocks = b; - states = s; - } - - public void clean() { - VectorMap d = new VectorMap<>(); - d.putAll(blocks); - - VectorMap dx = new VectorMap<>(); - dx.putAll(states); - - blocks = d; - states = dx; - } - - public Vector3i getSigned(int x, int y, int z) { - if (x >= w || y >= h || z >= d) { - throw new RuntimeException(x + " " + y + " " + z + " exceeds limit of " + w + " " + h + " " + d); - } - - return (Vector3i) new Vector3i(x, y, z).subtract(center); - } - - public void setUnsigned(int x, int y, int z, BlockData block) { - Vector3i v = getSigned(x, y, z); - - if (block == null) { - blocks.remove(v); - states.remove(v); - } else { - blocks.put(v, block); - } - } - - public void setUnsigned(int x, int y, int z, Block block, boolean legacy) { - Vector3i v = getSigned(x, y, z); - - if (block == null) { - blocks.remove(v); - states.remove(v); - } else { - BlockData data = block.getBlockData(); - blocks.put(v, data); - TileData state = TileData.getTileState(block, legacy); - if (state != null) { - Iris.debug("Saved State " + v); - states.put(v, state); - } - } - } - - public int place(int x, int z, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, IrisData rdata) { - return place(x, -1, z, placer, config, rng, rdata); - } - - public int place(int x, int z, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, CarveResult c, IrisData rdata) { - return place(x, -1, z, placer, config, rng, null, c, rdata); - } - - public int place(int x, int yv, int z, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, IrisData rdata) { - return place(x, yv, z, placer, config, rng, null, null, rdata); - } - - public int place(Location loc, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, IrisData rdata) { - return place(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), placer, config, rng, rdata); - } - - public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer listener, CarveResult c, IrisData rdata) { - IObjectPlacer placer = (config.getHeightmap() != null) ? new HeightmapObjectPlacer(oplacer.getEngine() == null ? IrisContext.get().getEngine() : oplacer.getEngine(), rng, x, yv, z, config, oplacer) : oplacer; - - if (rdata != null) { - // Slope condition - if (!config.getSlopeCondition().isDefault() && - !config.getSlopeCondition().isValid(rdata.getEngine().getComplex().getSlopeStream().get(x, z)) && !config.isForcePlace()) { - return -1; - } - -// if (config.getCarvingSupport().supportsSurface()) { -// int y = placer.getHighest(x, z, rdata); -// if (placer.isCarved(x, y, z)) { -// return -1; -// } -// } - - // Rotation calculation - int slopeRotationY = 0; - ProceduralStream heightStream = rdata.getEngine().getComplex().getHeightStream(); - if (config.isRotateTowardsSlope()) { - // Whichever side of the rectangle that bounds the object is lowest is the 'direction' of the slope (simply said). - double hNorth = heightStream.get(x, z + ((float) d) / 2); - double hEast = heightStream.get(x + ((float) w) / 2, z); - double hSouth = heightStream.get(x, z - ((float) d) / 2); - double hWest = heightStream.get(x - ((float) w) / 2, z); - double min = Math.min(Math.min(hNorth, hEast), Math.min(hSouth, hWest)); - if (min == hNorth) { - slopeRotationY = 0; - } else if (min == hEast) { - slopeRotationY = 90; - } else if (min == hSouth) { - slopeRotationY = 180; - } else if (min == hWest) { - slopeRotationY = 270; - } - - double newRotation = config.getRotation().getYAxis().getMin() + slopeRotationY; - if (newRotation == 0) { - config.getRotation().setYAxis(new IrisAxisRotationClamp(false, false, 0, 0, 90)); - config.getRotation().setEnabled(config.getRotation().canRotateX() || config.getRotation().canRotateZ()); - } else { - config.getRotation().setYAxis(new IrisAxisRotationClamp(true, false, newRotation, newRotation, 90)); - config.getRotation().setEnabled(true); - } - } - } - - if (config.isSmartBore()) { - ensureSmartBored(placer.isDebugSmartBore()); - } - - boolean warped = !config.getWarp().isFlat(); - boolean stilting = (config.getMode().equals(ObjectPlaceMode.STILT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT) || - config.getMode() == ObjectPlaceMode.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT || - config.getMode() == ObjectPlaceMode.CENTER_STILT); - KMap heightmap = config.getSnow() > 0 ? new KMap<>() : null; - int spinx = rng.imax() / 1000; - int spiny = rng.imax() / 1000; - int spinz = rng.imax() / 1000; - int rty = config.getRotation().rotate(new BlockVector(0, getCenter().getBlockY(), 0), spinx, spiny, spinz).getBlockY(); - int ty = config.getTranslate().translate(new BlockVector(0, getCenter().getBlockY(), 0), config.getRotation(), spinx, spiny, spinz).getBlockY(); - int y = -1; - int xx, zz; - int yrand = config.getTranslate().getYRandom(); - yrand = yrand > 0 ? rng.i(0, yrand) : yrand < 0 ? rng.i(yrand, 0) : yrand; - boolean bail = false; - - if (config.isFromBottom()) { - // todo Convert this to a mode and make it compatible with jigsaw - y = (getH() + 1) + rty; - if (!config.isForcePlace()) { - if (placer.isCarved(x, y, z) || - placer.isCarved(x, y - 1, z) || - placer.isCarved(x, y - 2, z) || - placer.isCarved(x, y - 3, z)) { - bail = true; - } - } - } else if (yv < 0) { - if (config.getMode().equals(ObjectPlaceMode.CENTER_HEIGHT) || config.getMode() == ObjectPlaceMode.CENTER_STILT) { - y = (c != null ? c.getSurface() : placer.getHighest(x, z, getLoader(), config.isUnderwater())) + rty; - if (!config.isForcePlace()) { - if (placer.isCarved(x, y, z) || placer.isCarved(x, y - 1, z) || placer.isCarved(x, y - 2, z) || placer.isCarved(x, y - 3, z)) { - bail = true; - } - } - } else if (config.getMode().equals(ObjectPlaceMode.MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.STILT)) { - BlockVector offset = new BlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - BlockVector rotatedDimensions = config.getRotation().rotate(new BlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone(); - int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX(); - int minX = Math.min(x - xLength, x + xLength); - int maxX = Math.max(x - xLength, x + xLength); - int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ(); - int minZ = Math.min(z - zLength, z + zLength); - int maxZ = Math.max(z - zLength, z + zLength); - for (int i = minX; i <= maxX; i++) { - for (int ii = minZ; ii <= maxZ; ii++) { - int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (placer.isCarved(i, h, ii) || placer.isCarved(i, h - 1, ii) || placer.isCarved(i, h - 2, ii) || placer.isCarved(i, h - 3, ii)) { - bail = true; - break; - } - } - if (h > y) - y = h; - } - } - } else if (config.getMode().equals(ObjectPlaceMode.FAST_MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT)) { - BlockVector offset = new BlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - BlockVector rotatedDimensions = config.getRotation().rotate(new BlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone(); - - int xRadius = (rotatedDimensions.getBlockX() / 2); - int xLength = xRadius + offset.getBlockX(); - int minX = Math.min(x - xLength, x + xLength); - int maxX = Math.max(x - xLength, x + xLength); - int zRadius = (rotatedDimensions.getBlockZ() / 2); - int zLength = zRadius + offset.getBlockZ(); - int minZ = Math.min(z - zLength, z + zLength); - int maxZ = Math.max(z - zLength, z + zLength); - - for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) { - for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { - int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (placer.isCarved(i, h, ii) || placer.isCarved(i, h - 1, ii) || placer.isCarved(i, h - 2, ii) || placer.isCarved(i, h - 3, ii)) { - bail = true; - break; - } - } - if (h > y) - y = h; - } - } - } else if (config.getMode().equals(ObjectPlaceMode.MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.MIN_STILT) { - y = rdata.getEngine().getHeight() + 1; - BlockVector offset = new BlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - BlockVector rotatedDimensions = config.getRotation().rotate(new BlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone(); - - int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX(); - int minX = Math.min(x - xLength, x + xLength); - int maxX = Math.max(x - xLength, x + xLength); - int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ(); - int minZ = Math.min(z - zLength, z + zLength); - int maxZ = Math.max(z - zLength, z + zLength); - for (int i = minX; i <= maxX; i++) { - for (int ii = minZ; ii <= maxZ; ii++) { - int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (placer.isCarved(i, h, ii) || placer.isCarved(i, h - 1, ii) || placer.isCarved(i, h - 2, ii) || placer.isCarved(i, h - 3, ii)) { - bail = true; - break; - } - } - if (h < y) { - y = h; - } - } - } - } else if (config.getMode().equals(ObjectPlaceMode.FAST_MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT) { - y = rdata.getEngine().getHeight() + 1; - BlockVector offset = new BlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - BlockVector rotatedDimensions = config.getRotation().rotate(new BlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone(); - - int xRadius = (rotatedDimensions.getBlockX() / 2); - int xLength = xRadius + offset.getBlockX(); - int minX = Math.min(x - xLength, x + xLength); - int maxX = Math.max(x - xLength, x + xLength); - int zRadius = (rotatedDimensions.getBlockZ() / 2); - int zLength = zRadius + offset.getBlockZ(); - int minZ = Math.min(z - zLength, z + zLength); - int maxZ = Math.max(z - zLength, z + zLength); - - for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) { - for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { - int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (placer.isCarved(i, h, ii) || placer.isCarved(i, h - 1, ii) || placer.isCarved(i, h - 2, ii) || placer.isCarved(i, h - 3, ii)) { - bail = true; - break; - } - } - if (h < y) { - y = h; - } - } - } - } else if (config.getMode().equals(ObjectPlaceMode.PAINT)) { - y = placer.getHighest(x, z, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (placer.isCarved(x, y, z) || placer.isCarved(x, y - 1, z) || placer.isCarved(x, y - 2, z) || placer.isCarved(x, y - 3, z)) { - bail = true; - } - } - } - } else { - y = yv; - if (!config.isForcePlace()) { - if (placer.isCarved(x, y, z) || placer.isCarved(x, y - 1, z) || placer.isCarved(x, y - 2, z) || placer.isCarved(x, y - 3, z)) { - bail = true; - } - } - } - - if (yv >= 0 && config.isBottom()) { - y += Math.floorDiv(h, 2); - if (!config.isForcePlace()) { - bail = placer.isCarved(x, y, z) || placer.isCarved(x, y - 1, z) || placer.isCarved(x, y - 2, z) || placer.isCarved(x, y - 3, z); - } - } - - if (bail && !config.isForcePlace()) { - return -1; - } - - if (yv < 0) { - if (!config.isForcePlace() && !config.isUnderwater() && !config.isOnwater() && placer.isUnderwater(x, z)) { - return -1; - } - } - - if (!config.isForcePlace() && c != null && Math.max(0, h + yrand + ty) + 1 >= c.getHeight()) { - return -1; - } - - if (!config.isForcePlace() && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) { - return -1; - } - - if (!config.isForcePlace() && !config.getClamp().canPlace(y + rty + ty, y - rty + ty)) { - return -1; - } - - if (!config.isForcePlace() && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) { - Engine engine = rdata.getEngine(); - BlockVector offset = new BlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - for (int i = x - Math.floorDiv(w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(w, 2) - (w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) { - for (int j = y - Math.floorDiv(h, 2) + (int) offset.getY(); j <= y + Math.floorDiv(h, 2) - (h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) { - for (int k = z - Math.floorDiv(d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(d, 2) - (d % 2 == 0 ? 1 : 0) + (int) offset.getX(); k++) { - PlacedObject p = engine.getObjectPlacement(i, j, k); - if (p == null) continue; - IrisObject o = p.getObject(); - if (o == null) continue; - String key = o.getLoadKey(); - if (key != null) { - if (config.getForbiddenCollisions().contains(key) && !config.getAllowedCollisions().contains(key)) { - // Iris.debug("%s collides with %s (%s / %s / %s)", getLoadKey(), key, i, j, k); - return -1; - } - } - } - } - } - } - - if (config.isBore()) { - BlockVector offset = new BlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - for (int i = x - Math.floorDiv(w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(w, 2) - (w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) { - for (int j = y - Math.floorDiv(h, 2) - config.getBoreExtendMinY() + (int) offset.getY(); j <= y + Math.floorDiv(h, 2) + config.getBoreExtendMaxY() - (h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) { - for (int k = z - Math.floorDiv(d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(d, 2) - (d % 2 == 0 ? 1 : 0) + (int) offset.getX(); k++) { - placer.set(i, j, k, AIR); - } - } - } - } - - int lowest = Integer.MAX_VALUE; - y += yrand; - readLock.lock(); - - KMap markers = null; - - try { - if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) { - markers = new KMap<>(); - var list = StreamSupport.stream(blocks.keys().spliterator(), false) - .collect(KList.collector()); - - for (IrisObjectMarker j : config.getMarkers()) { - IrisMarker marker = getLoader().getMarkerLoader().load(j.getMarker()); - - if (marker == null) { - continue; - } - - int max = j.getMaximumMarkers(); - for (BlockVector i : list.shuffle()) { - if (max <= 0) { - break; - } - - BlockData data = blocks.get(i); - - for (BlockData k : j.getMark(rdata)) { - if (max <= 0) { - break; - } - - if (j.isExact() ? k.matches(data) : k.getMaterial().equals(data.getMaterial())) { - boolean a = !blocks.containsKey((BlockVector) i.clone().add(new BlockVector(0, 1, 0))); - boolean fff = !blocks.containsKey((BlockVector) i.clone().add(new BlockVector(0, 2, 0))); - - if (!marker.isEmptyAbove() || (a && fff)) { - markers.put(i, j.getMarker()); - max--; - } - } - } - } - } - } - - for (var entry : blocks) { - var g = entry.getKey(); - BlockData d; - TileData tile = null; - - try { - d = entry.getValue(); - tile = states.get(g); - } catch (Throwable e) { - Iris.reportError(e); - Iris.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + getLoadKey() + " (cme)"); - d = AIR; - } - - if (d == null) { - Iris.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + getLoadKey() + " (null)"); - d = AIR; - } - - BlockVector i = g.clone(); - BlockData data = d.clone(); - i = config.getRotation().rotate(i.clone(), spinx, spiny, spinz).clone(); - i = config.getTranslate().translate(i.clone(), config.getRotation(), spinx, spiny, spinz).clone(); - - if (stilting && i.getBlockY() < lowest && !B.isAir(data)) { - lowest = i.getBlockY(); - } - - if (placer.isPreventingDecay() && (data) instanceof Leaves && !((Leaves) (data)).isPersistent()) { - ((Leaves) data).setPersistent(true); - } - - for (IrisObjectReplace j : config.getEdit()) { - if (rng.chance(j.getChance())) { - for (BlockData k : j.getFind(rdata)) { - if (j.isExact() ? k.matches(data) : k.getMaterial().equals(data.getMaterial())) { - BlockData newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata).clone(); - - if (newData.getMaterial() == data.getMaterial() && !(newData instanceof IrisCustomData || data instanceof IrisCustomData)) - data = data.merge(newData); - else - data = newData; - - Optional t = j.getReplace().getTile(rng, x, y, z, rdata); - if (t.isPresent()) { - tile = t.get(); - } - } - } - } - } - - data = config.getRotation().rotate(data, spinx, spiny, spinz); - xx = x + (int) Math.round(i.getX()); - - int yy = y + (int) Math.round(i.getY()); - zz = z + (int) Math.round(i.getZ()); - - if (warped) { - xx += config.warp(rng, i.getX() + x, i.getY() + y, i.getZ() + z, getLoader()); - zz += config.warp(rng, i.getZ() + z, i.getY() + y, i.getX() + x, getLoader()); - } - - if (yv < 0 && (config.getMode().equals(ObjectPlaceMode.PAINT)) && !B.isVineBlock(data)) { - yy = (int) Math.round(i.getY()) + Math.floorDiv(h, 2) + placer.getHighest(xx, zz, getLoader(), config.isUnderwater()); - } - - if (heightmap != null) { - Position2 pos = new Position2(xx, zz); - - if (!heightmap.containsKey(pos)) { - heightmap.put(pos, yy); - } - - if (heightmap.get(pos) < yy) { - heightmap.put(pos, yy); - } - } - - if (config.isMeld() && !placer.isSolid(xx, yy, zz)) { - continue; - } - - if ((config.isWaterloggable() || config.isUnderwater()) && yy <= placer.getFluidHeight() && data instanceof Waterlogged) { - // TODO Here - ((Waterlogged) data).setWaterlogged(true); - } - - if (B.isVineBlock(data)) { - MultipleFacing f = (MultipleFacing) data; - for (BlockFace face : f.getAllowedFaces()) { - BlockData facingBlock = placer.get(xx + face.getModX(), yy + face.getModY(), zz + face.getModZ()); - if (B.isSolid(facingBlock) && !B.isVineBlock(facingBlock)) { - f.setFace(face, true); - } - } - } - - if (listener != null) { - listener.accept(new BlockPosition(xx, yy, zz), data); - } - - if (markers != null && markers.containsKey(g)) { - placer.getEngine().getMantle().getMantle().set(xx, yy, zz, new MatterMarker(markers.get(g))); - } - - boolean wouldReplace = B.isSolid(placer.get(xx, yy, zz)) && B.isVineBlock(data); - boolean place = !data.getMaterial().equals(Material.AIR) && !data.getMaterial().equals(Material.CAVE_AIR) && !wouldReplace; - - if (data instanceof IrisCustomData || place) { - placer.set(xx, yy, zz, data); - if (tile != null) { - placer.setTile(xx, yy, zz, tile); - } - } - } - } catch (Throwable e) { - e.printStackTrace(); - Iris.reportError(e); - } - readLock.unlock(); - - if (stilting) { - readLock.lock(); - IrisStiltSettings settings = config.getStiltSettings(); - for (BlockVector g : blocks.keys()) { - BlockData d; - - if (settings == null || settings.getPalette() == null) { - try { - d = blocks.get(g); - } catch (Throwable e) { - Iris.reportError(e); - Iris.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + getLoadKey() + " (stilt cme)"); - d = AIR; - } - - if (d == null) { - Iris.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + getLoadKey() + " (stilt null)"); - d = AIR; - } - } else - d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata); - - - BlockVector i = g.clone(); - i = config.getRotation().rotate(i.clone(), spinx, spiny, spinz).clone(); - i = config.getTranslate().translate(i.clone(), config.getRotation(), spinx, spiny, spinz).clone(); - d = config.getRotation().rotate(d, spinx, spiny, spinz); - - if (i.getBlockY() != lowest) - continue; - - for (IrisObjectReplace j : config.getEdit()) { - if (rng.chance(j.getChance())) { - for (BlockData k : j.getFind(rdata)) { - if (j.isExact() ? k.matches(d) : k.getMaterial().equals(d.getMaterial())) { - BlockData newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata).clone(); - - if (newData.getMaterial() == d.getMaterial()) { - d = d.merge(newData); - } else { - d = newData; - } - } - } - } - } - - if (d == null || B.isAir(d)) - continue; - - xx = x + (int) Math.round(i.getX()); - zz = z + (int) Math.round(i.getZ()); - - if (warped) { - xx += config.warp(rng, i.getX() + x, i.getY() + y, i.getZ() + z, getLoader()); - zz += config.warp(rng, i.getZ() + z, i.getY() + y, i.getX() + x, getLoader()); - } - - int highest = placer.getHighest(xx, zz, getLoader(), true); - - if ((config.isWaterloggable() || config.isUnderwater()) && highest <= placer.getFluidHeight() && d instanceof Waterlogged) - ((Waterlogged) d).setWaterlogged(true); - - if (yv >= 0 && config.isBottom()) - y += Math.floorDiv(h, 2); - - int lowerBound = highest - 1; - if (settings != null) { - lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand()); - if (settings.getYMax() != 0) - lowerBound -= Math.min(config.getStiltSettings().getYMax() - (lowest + y - highest), 0); - } - for (int j = lowest + y; j > lowerBound; j--) { - if (B.isVineBlock(d)) { - MultipleFacing f = (MultipleFacing) d; - for (BlockFace face : f.getAllowedFaces()) { - BlockData facingBlock = placer.get(xx + face.getModX(), j + face.getModY(), zz + face.getModZ()); - if (B.isSolid(facingBlock) && !B.isVineBlock(facingBlock)) { - f.setFace(face, true); - } - } - } - placer.set(xx, j, zz, d); - } - - } - - readLock.unlock(); - } - - if (heightmap != null) { - RNG rngx = rng.nextParallelRNG(3468854); - - for (Position2 i : heightmap.k()) { - int vx = i.getX(); - int vy = heightmap.get(i); - int vz = i.getZ(); - - if (config.getSnow() > 0) { - int height = rngx.i(0, (int) (config.getSnow() * 7)); - placer.set(vx, vy + 1, vz, SNOW_LAYERS[Math.max(Math.min(height, 7), 0)]); - } - } - } - - return y; - } - - public IrisObject rotateCopy(IrisObjectRotation rt) { - IrisObject copy = copy(); - copy.rotate(rt, 0, 0, 0); - return copy; - } - - public void rotate(IrisObjectRotation r, int spinx, int spiny, int spinz) { - writeLock.lock(); - VectorMap d = new VectorMap<>(); - - for (var entry : blocks) { - d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), r.rotate(entry.getValue(), spinx, spiny, spinz)); - } - - VectorMap dx = new VectorMap<>(); - - for (var entry : states) { - dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue()); - } - - blocks = d; - states = dx; - shrinkwrap(); - writeLock.unlock(); - } - - public void place(Location at) { - readLock.lock(); - for (var entry : blocks) { - var i = entry.getKey(); - Block b = at.clone().add(0, getCenter().getY(), 0).add(i).getBlock(); - b.setBlockData(Objects.requireNonNull(entry.getValue()), false); - - if (states.containsKey(i)) { - Iris.info(Objects.requireNonNull(states.get(i)).toString()); - Objects.requireNonNull(states.get(i)).toBukkitTry(b); - } - } - readLock.unlock(); - } - - public void placeCenterY(Location at) { - readLock.lock(); - for (var entry : blocks) { - var i = entry.getKey(); - Block b = at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i).getBlock(); - b.setBlockData(Objects.requireNonNull(entry.getValue()), false); - - if (states.containsKey(i)) { - Objects.requireNonNull(states.get(i)).toBukkitTry(b); - } - } - readLock.unlock(); - } - - public void unplaceCenterY(Location at) { - readLock.lock(); - for (BlockVector i : blocks.keys()) { - at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i).getBlock().setBlockData(AIR, false); - } - readLock.unlock(); - } - - public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) { - Vector sm1 = new Vector(scale - 1, scale - 1, scale - 1); - scale = Math.max(0.001, Math.min(50, scale)); - if (scale < 1) { - scale = scale - 0.0001; - } - - IrisPosition l1 = getAABB().max(); - IrisPosition l2 = getAABB().min(); - VectorMap placeBlock = new VectorMap<>(); - - Vector center = getCenter(); - if (getH() == 2) { - center = center.setY(center.getBlockY() + 0.5); - } - if (getW() == 2) { - center = center.setX(center.getBlockX() + 0.5); - } - if (getD() == 2) { - center = center.setZ(center.getBlockZ() + 0.5); - } - - IrisObject oo = new IrisObject((int) Math.ceil((w * scale) + (scale * 2)), (int) Math.ceil((h * scale) + (scale * 2)), (int) Math.ceil((d * scale) + (scale * 2))); - - readLock.lock(); - for (var entry : blocks) { - BlockData bd = entry.getValue(); - placeBlock.put(entry.getKey().clone().add(HALF).subtract(center) - .multiply(scale).add(sm1).toBlockVector(), bd); - } - readLock.unlock(); - - for (var entry : placeBlock) { - BlockVector v = entry.getKey(); - if (scale > 1) { - for (BlockVector vec : blocksBetweenTwoPoints(v.clone().add(center), v.clone().add(center).add(sm1))) { - oo.blocks.put(vec, entry.getValue()); - } - } else { - oo.setUnsigned(v.getBlockX(), v.getBlockY(), v.getBlockZ(), entry.getValue()); - } - } - - if (scale > 1) { - switch (interpolation) { - case TRILINEAR -> oo.trilinear((int) Math.round(scale)); - case TRICUBIC -> oo.tricubic((int) Math.round(scale)); - case TRIHERMITE -> oo.trihermite((int) Math.round(scale)); - } - } - - return oo; - } - - public void trilinear(int rad) { - writeLock.lock(); - VectorMap v = blocks; - VectorMap b = new VectorMap<>(); - BlockVector min = getAABB().minbv(); - BlockVector max = getAABB().maxbv(); - - for (int x = min.getBlockX(); x <= max.getBlockX(); x++) { - for (int y = min.getBlockY(); y <= max.getBlockY(); y++) { - for (int z = min.getBlockZ(); z <= max.getBlockZ(); z++) { - if (IrisInterpolation.getTrilinear(x, y, z, rad, (xx, yy, zz) -> { - BlockData data = v.get(new BlockVector((int) xx, (int) yy, (int) zz)); - - if (data == null || data.getMaterial().isAir()) { - return 0; - } - - return 1; - }) >= 0.5) { - b.put(new BlockVector(x, y, z), nearestBlockData(x, y, z)); - } else { - b.put(new BlockVector(x, y, z), AIR); - } - } - } - } - - blocks = b; - writeLock.unlock(); - } - - public void tricubic(int rad) { - writeLock.lock(); - VectorMap v = blocks; - VectorMap b = new VectorMap<>(); - BlockVector min = getAABB().minbv(); - BlockVector max = getAABB().maxbv(); - - for (int x = min.getBlockX(); x <= max.getBlockX(); x++) { - for (int y = min.getBlockY(); y <= max.getBlockY(); y++) { - for (int z = min.getBlockZ(); z <= max.getBlockZ(); z++) { - if (IrisInterpolation.getTricubic(x, y, z, rad, (xx, yy, zz) -> { - BlockData data = v.get(new BlockVector((int) xx, (int) yy, (int) zz)); - - if (data == null || data.getMaterial().isAir()) { - return 0; - } - - return 1; - }) >= 0.5) { - b.put(new BlockVector(x, y, z), nearestBlockData(x, y, z)); - } else { - b.put(new BlockVector(x, y, z), AIR); - } - } - } - } - - blocks = b; - writeLock.unlock(); - } - - public void trihermite(int rad) { - trihermite(rad, 0D, 0D); - } - - public void trihermite(int rad, double tension, double bias) { - writeLock.lock(); - VectorMap v = blocks; - VectorMap b = new VectorMap<>(); - BlockVector min = getAABB().minbv(); - BlockVector max = getAABB().maxbv(); - - for (int x = min.getBlockX(); x <= max.getBlockX(); x++) { - for (int y = min.getBlockY(); y <= max.getBlockY(); y++) { - for (int z = min.getBlockZ(); z <= max.getBlockZ(); z++) { - if (IrisInterpolation.getTrihermite(x, y, z, rad, (xx, yy, zz) -> { - BlockData data = v.get(new BlockVector((int) xx, (int) yy, (int) zz)); - - if (data == null || data.getMaterial().isAir()) { - return 0; - } - - return 1; - }, tension, bias) >= 0.5) { - b.put(new BlockVector(x, y, z), nearestBlockData(x, y, z)); - } else { - b.put(new BlockVector(x, y, z), AIR); - } - } - } - } - - blocks = b; - writeLock.unlock(); - } - - private BlockData nearestBlockData(int x, int y, int z) { - BlockVector vv = new BlockVector(x, y, z); - readLock.lock(); - BlockData r = blocks.get(vv); - - if (r != null && !r.getMaterial().isAir()) { - return r; - } - - double d = Double.MAX_VALUE; - - for (var entry : blocks) { - BlockData dat = entry.getValue(); - - if (dat.getMaterial().isAir()) { - continue; - } - - double dx = entry.getKey().distanceSquared(vv); - - if (dx < d) { - d = dx; - r = dat; - } - } - readLock.unlock(); - - return r; - } - - public int volume() { - return blocks.size(); - } - - @Override - public String getFolderName() { - return "objects"; - } - - @Override - public String getTypeName() { - return "Object"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - } - - private static class HeaderException extends IOException { - public HeaderException() { - super("Invalid Header"); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectLimit.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectLimit.java deleted file mode 100644 index 9fac8a1bb..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectLimit.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MaxNumber; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.Snippet; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("object-limit") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisObjectLimit { - @MinNumber(0) - @MaxNumber(1024) - @Desc("The minimum height for placement (bottom of object)") - private int minimumHeight = -2048; //TODO: WARNING HEIGHT - - @MinNumber(0) - @MaxNumber(1024) - @Desc("The maximum height for placement (top of object)") - private int maximumHeight = 2048; //TODO: WARNING HEIGHT - - public boolean canPlace(int h, int l) { - return h <= maximumHeight && l >= minimumHeight; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectLoot.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectLoot.java deleted file mode 100644 index da09ae2fc..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectLoot.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -@Snippet("object-loot") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents loot within this object or jigsaw piece") -@Data -public class IrisObjectLoot implements IObjectLoot { - private final transient AtomicCache> filterCache = new AtomicCache<>(); - @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("The list of blocks this loot table should apply to") - private KList filter = new KList<>(); - @Desc("Exactly match the block data or not") - private boolean exact = false; - @Desc("The loot table name") - @Required - @RegistryListResource(IrisLootTable.class) - private String name; - @Desc("The weight of this loot table being chosen") - private int weight = 1; - - public KList getFilter(IrisData rdata) { - return filterCache.aquire(() -> - { - KList b = new KList<>(); - - for (IrisBlockData i : filter) { - BlockData bx = i.getBlockData(rdata); - - if (bx != null) { - b.add(bx); - } - } - - return b; - }); - } - - public boolean matchesFilter(IrisData manager, BlockData data) { - for (BlockData filterData : getFilter(manager)) { - if (filterData.matches(data)) return true; - } - return false; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectMarker.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectMarker.java deleted file mode 100644 index 7c870dd70..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectMarker.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -@Snippet("object-marker") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Find blocks to mark") -@Data -public class IrisObjectMarker { - private final transient AtomicCache> findData = new AtomicCache<>(); - @ArrayType(min = 1, type = IrisBlockData.class) - @Required - @Desc("Find block types to mark") - private KList mark = new KList<>(); - @MinNumber(1) - @MaxNumber(16) - @Desc("The maximum amount of markers to place. Use these sparingly!") - private int maximumMarkers = 8; - @Desc("If true, markers will only be placed if the block matches the mark list perfectly.") - private boolean exact = false; - @Required - @RegistryListResource(IrisMarker.class) - @Desc("The marker to add") - private String marker; - - public KList getMark(IrisData rdata) { - return findData.aquire(() -> - { - KList b = new KList<>(); - - for (IrisBlockData i : mark) { - BlockData bx = i.getBlockData(rdata); - - if (bx != null) { - b.add(bx); - } - } - - return b; - }); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacement.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacement.java deleted file mode 100644 index adab09743..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectPlacement.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.DataProvider; -import com.volmit.iris.util.data.WeightedRandom; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.TreeType; -import org.bukkit.block.data.BlockData; -import org.jetbrains.annotations.Nullable; - -import java.util.Optional; -import java.util.function.Function; - -@Snippet("object-placer") -@EqualsAndHashCode() -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents an iris object placer. It places objects.") -@Data -public class IrisObjectPlacement { - private final transient AtomicCache surfaceWarp = new AtomicCache<>(); - @RegistryListResource(IrisObject.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("List of objects to place") - private KList place = new KList<>(); - @Desc("Rotate this objects placement") - private IrisObjectRotation rotation = new IrisObjectRotation(); - @Desc("Limit the max height or min height of placement.") - private IrisObjectLimit clamp = new IrisObjectLimit(); - @MinNumber(0) - @MaxNumber(1) - @Desc("The maximum layer level of a snow filter overtop of this placement. Set to 0 to disable. Max of 1.") - private double snow = 0; - @Desc("Whether or not this object can be targeted by a dolphin.") - private boolean isDolphinTarget = false; - @Desc("The slope at which this object can be placed. Range from 0 to 10 by default. Calculated from a 3-block radius from the center of the object placement.") - private IrisSlopeClip slopeCondition = new IrisSlopeClip(); - @Desc("Set to true to add the rotation of the direction of the slope of the terrain (wherever the slope is going down) to the y-axis rotation of the object." + - "Rounded to 90 degrees. Adds the *min* rotation of the y axis as well (to still allow you to rotate objects nicely). Discards *max* and *interval* on *yaxis*") - private boolean rotateTowardsSlope = false; - @MinNumber(0) - @MaxNumber(1) - @Desc("The chance for this to place in a chunk. If you need multiple per chunk, set this to 1 and use density.") - private double chance = 1; - @MinNumber(1) - @Desc("If the chance check passes, place this many in a single chunk") - private int density = 1; - @Desc("If the chance check passes, and you specify this, it picks a number in the range based on noise, and 'density' is ignored.") - private IrisStyledRange densityStyle = null; - @Desc("When stilting is enabled, this object will define various properties related to it.") - private IrisStiltSettings stiltSettings; - @MaxNumber(64) - @MinNumber(0) - @Desc("When bore is enabled, expand max-y of the cuboid it removes") - private int boreExtendMaxY = 0; - @ArrayType(min = 1, type = IrisObjectMarker.class) - @Desc("Add markers to blocks in this object") - private KList markers = new KList<>(); - @MaxNumber(64) - @MinNumber(-1) - @Desc("When bore is enabled, lower min-y of the cuboid it removes") - private int boreExtendMinY = 0; - @Desc("If set to true, objects will place on the terrain height, ignoring the water surface.") - private boolean underwater = false; - @Desc("If set to true, objects will place in carvings (such as underground) or under an overhang.") - private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY; - @Desc("If this is defined, this object wont place on the terrain heightmap, but instead on this virtual heightmap") - private IrisNoiseGenerator heightmap; - @Desc("If set to true, Iris will try to fill the insides of 'rooms' and 'pockets' where air should fit based off of raytrace checks. This prevents a village house placing in an area where a tree already exists, and instead replaces the parts of the tree where the interior of the structure is. \n\nThis operation does not affect warmed-up generation speed however it does slow down loading objects.") - private boolean smartBore = false; - @Desc("If set to true, Blocks placed underwater that could be waterlogged are waterlogged.") - private boolean waterloggable = false; - @Desc("If set to true, objects will place on the fluid height level Such as boats.") - private boolean onwater = false; - @Desc("If set to true, this object will only place parts of itself where blocks already exist. Warning: Melding is very performance intensive!") - private boolean meld = false; - @Desc("If set to true, this object will get placed from the bottom of the world up") - private boolean fromBottom; - @Desc("If set to true, this object will place from the ground up instead of height checks when not y locked to the surface. This is not compatable with X and Z axis rotations (it may look off)") - private boolean bottom = false; - @Desc("If set to true, air will be placed before the schematic places.") - private boolean bore = false; - @Desc("Use a generator to warp the field of coordinates. Using simplex for example would make a square placement warp like a flag") - private IrisGeneratorStyle warp = new IrisGeneratorStyle(NoiseStyle.FLAT); - @Desc("If the place mode is set to CENTER_HEIGHT_RIGID and you have an X/Z translation, Turning on translate center will also translate the center height check.") - private boolean translateCenter = false; - @Desc("The placement mode") - private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT; - @ArrayType(min = 1, type = IrisObjectReplace.class) - @Desc("Find and replace blocks") - private KList edit = new KList<>(); - @Desc("Translate this object's placement") - private IrisObjectTranslate translate = new IrisObjectTranslate(); - @Desc("Scale Objects") - private IrisObjectScale scale = new IrisObjectScale(); - @ArrayType(min = 1, type = IrisObjectLoot.class) - @Desc("The loot tables to apply to these objects") - private KList loot = new KList<>(); - @ArrayType(min = 1, type = IrisObjectVanillaLoot.class) - @Desc("The vanilla loot tables to apply to these objects") - private KList vanillaLoot = new KList<>(); - @Desc("Whether the given loot tables override any and all other loot tables available in the dimension, region or biome.") - private boolean overrideGlobalLoot = false; - @Desc("This object / these objects override the following trees when they grow...") - @ArrayType(min = 1, type = IrisTree.class) - private KList trees = new KList<>(); - @RegistryListResource(IrisObject.class) - @ArrayType(type = String.class) - @Desc("List of objects to this object is allowed to collied with") - private KList allowedCollisions = new KList<>(); - @RegistryListResource(IrisObject.class) - @ArrayType(type = String.class) - @Desc("List of objects to this object is forbidden to collied with") - private KList forbiddenCollisions = new KList<>(); - @Desc("Ignore any placement restrictions for this object") - private boolean forcePlace = false; - private transient AtomicCache cache = new AtomicCache<>(); - - public IrisObjectPlacement toPlacement(String... place) { - IrisObjectPlacement p = new IrisObjectPlacement(); - p.setPlace(new KList<>(place)); - p.setTranslateCenter(translateCenter); - p.setMode(mode); - p.setEdit(edit); - p.setTranslate(translate); - p.setWarp(warp); - p.setBore(bore); - p.setMeld(meld); - p.setWaterloggable(waterloggable); - p.setOnwater(onwater); - p.setSmartBore(smartBore); - p.setCarvingSupport(carvingSupport); - p.setUnderwater(underwater); - p.setBoreExtendMaxY(boreExtendMaxY); - p.setBoreExtendMinY(boreExtendMinY); - p.setStiltSettings(stiltSettings); - p.setDensity(density); - p.setChance(chance); - p.setSnow(snow); - p.setClamp(clamp); - p.setRotation(rotation); - p.setLoot(loot); - return p; - } - - public CNG getSurfaceWarp(RNG rng, IrisData data) { - return surfaceWarp.aquire(() -> - getWarp().create(rng, data)); - } - - public double warp(RNG rng, double x, double y, double z, IrisData data) { - return getSurfaceWarp(rng, data).fitDouble(-(getWarp().getMultiplier() / 2D), (getWarp().getMultiplier() / 2D), x, y, z); - } - - public IrisObject getObject(DataProvider g, RNG random) { - if (place.isEmpty()) { - return null; - } - - return g.getData().getObjectLoader().load(place.get(random.nextInt(place.size()))); - } - - public boolean matches(IrisTreeSize size, TreeType type) { - for (IrisTree i : getTrees()) { - if (i.matches(size, type)) { - return true; - } - } - - return false; - } - - public int getDensity() { - if (densityStyle == null) { - return density; - } - return densityStyle.getMid(); - } - - public int getDensity(RNG rng, double x, double z, IrisData data) { - if (densityStyle == null) { - return density; - } - - return (int) Math.round(densityStyle.get(rng, x, z, data)); - } - - private TableCache getCache(IrisData manager) { - return cache.aquire(() -> { - TableCache cache = new TableCache(); - - cache.merge(getCache(manager, getVanillaLoot(), IrisObjectPlacement::getVanillaTable)); - cache.merge(getCache(manager, getLoot(), manager.getLootLoader()::load)); - - return cache; - }); - } - - private TableCache getCache(IrisData manager, KList list, Function loader) { - TableCache tc = new TableCache(); - - for (IObjectLoot loot : list) { - if (loot == null) - continue; - IrisLootTable table = loader.apply(loot.getName()); - if (table == null) { - Iris.warn("Couldn't find loot table " + loot.getName()); - continue; - } - - if (loot.getFilter().isEmpty()) //Table applies to all containers - { - tc.global.put(table, loot.getWeight()); - } else if (!loot.isExact()) //Table is meant to be by type - { - for (BlockData filterData : loot.getFilter(manager)) { - if (!tc.basic.containsKey(filterData.getMaterial())) { - tc.basic.put(filterData.getMaterial(), new WeightedRandom<>()); - } - - tc.basic.get(filterData.getMaterial()).put(table, loot.getWeight()); - } - } else //Filter is exact - { - for (BlockData filterData : loot.getFilter(manager)) { - if (!tc.exact.containsKey(filterData.getMaterial())) { - tc.exact.put(filterData.getMaterial(), new KMap<>()); - } - - if (!tc.exact.get(filterData.getMaterial()).containsKey(filterData)) { - tc.exact.get(filterData.getMaterial()).put(filterData, new WeightedRandom<>()); - } - - tc.exact.get(filterData.getMaterial()).get(filterData).put(table, loot.getWeight()); - } - } - } - return tc; - } - - @Nullable - private static IrisVanillaLootTable getVanillaTable(String name) { - return Optional.ofNullable(NamespacedKey.fromString(name)) - .map(Bukkit::getLootTable) - .map(IrisVanillaLootTable::new) - .orElse(null); - } - - /** - * Gets the loot table that should be used for the block - * - * @param data The block data of the block - * @param dataManager Iris Data Manager - * @return The loot table it should use. - */ - public IrisLootTable getTable(BlockData data, IrisData dataManager) { - TableCache cache = getCache(dataManager); - if (B.isStorageChest(data)) { - IrisLootTable picked = null; - if (cache.exact.containsKey(data.getMaterial()) && cache.exact.get(data.getMaterial()).containsKey(data)) { - picked = cache.exact.get(data.getMaterial()).get(data).pullRandom(); - } else if (cache.basic.containsKey(data.getMaterial())) { - picked = cache.basic.get(data.getMaterial()).pullRandom(); - } else if (cache.global.getSize() > 0) { - picked = cache.global.pullRandom(); - } - - return picked; - } - - return null; - } - - private static class TableCache { - final transient WeightedRandom global = new WeightedRandom<>(); - final transient KMap> basic = new KMap<>(); - final transient KMap>> exact = new KMap<>(); - - private void merge(TableCache other) { - global.merge(other.global); - basic.merge(other.basic, WeightedRandom::merge); - exact.merge(other.exact, (a, b) -> a.merge(b, WeightedRandom::merge)); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectReplace.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectReplace.java deleted file mode 100644 index 119da29a7..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectReplace.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -@Snippet("object-block-replacer") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Find and replace object materials") -@Data -public class IrisObjectReplace { - private final transient AtomicCache replaceGen = new AtomicCache<>(); - private final transient AtomicCache> findData = new AtomicCache<>(); - private final transient AtomicCache> replaceData = new AtomicCache<>(); - @ArrayType(min = 1, type = IrisBlockData.class) - @Required - @Desc("Find this block") - private KList find = new KList<>(); - @Required - @Desc("Replace it with this block palette") - private IrisMaterialPalette replace = new IrisMaterialPalette(); - @Desc("Exactly match the block data or not") - private boolean exact = false; - @MinNumber(0) - @MaxNumber(1) - @Desc("Modifies the chance the block is replaced") - private float chance = 1; - - public KList getFind(IrisData rdata) { - return findData.aquire(() -> - { - KList b = new KList<>(); - - for (IrisBlockData i : find) { - BlockData bx = i.getBlockData(rdata); - - if (bx != null) { - b.add(bx); - } - } - - return b; - }); - } - - public BlockData getReplace(RNG seed, double x, double y, double z, IrisData rdata) { - return getReplace().get(seed, x, y, z, rdata); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectRotation.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectRotation.java deleted file mode 100644 index 1dc57272e..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectRotation.java +++ /dev/null @@ -1,453 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Axis; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.*; -import org.bukkit.block.data.type.RedstoneWire; -import org.bukkit.block.data.type.Wall; -import org.bukkit.util.BlockVector; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Snippet("object-rotator") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Configures rotation for iris") -@Data -public class IrisObjectRotation { - private static final List WALL_FACES = List.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST); - - @Desc("If this rotator is enabled or not") - private boolean enabled = true; - - @Desc("The x axis rotation") - private IrisAxisRotationClamp xAxis = new IrisAxisRotationClamp(); - - @Desc("The y axis rotation") - private IrisAxisRotationClamp yAxis = new IrisAxisRotationClamp(true, false, 0, 0, 90); - - @Desc("The z axis rotation") - private IrisAxisRotationClamp zAxis = new IrisAxisRotationClamp(); - - public static IrisObjectRotation of(double x, double y, double z) { - IrisObjectRotation rt = new IrisObjectRotation(); - IrisAxisRotationClamp rtx = new IrisAxisRotationClamp(); - IrisAxisRotationClamp rty = new IrisAxisRotationClamp(); - IrisAxisRotationClamp rtz = new IrisAxisRotationClamp(); - rt.setEnabled(x != 0 || y != 0 || z != 0); - rt.setXAxis(rtx); - rt.setYAxis(rty); - rt.setZAxis(rtz); - rtx.setEnabled(x != 0); - rty.setEnabled(y != 0); - rtz.setEnabled(z != 0); - rtx.setInterval(90); - rty.setInterval(90); - rtz.setInterval(90); - rtx.minMax(x); - rty.minMax(y); - rtz.minMax(z); - - return rt; - } - - public double getYRotation(int spin) { - return getRotation(spin, yAxis); - } - - public double getXRotation(int spin) { - return getRotation(spin, xAxis); - } - - public double getZRotation(int spin) { - return getRotation(spin, zAxis); - } - - public IrisObject rotateCopy(IrisObject e) { - if (e == null) { - return null; - } - - return e.rotateCopy(this); - } - - public IrisJigsawPiece rotateCopy(IrisJigsawPiece v, IrisPosition offset) { - IrisJigsawPiece piece = v.copy(); - for (IrisJigsawPieceConnector i : piece.getConnectors()) { - i.setPosition(rotate(i.getPosition()).add(offset)); - i.setDirection(rotate(i.getDirection())); - } - try { - var translate = piece.getPlacementOptions().getTranslate(); - var pos = rotate(new IrisPosition(translate.getX(), translate.getY(), translate.getZ())).add(offset); - translate.setX(pos.getX()).setY(pos.getY()).setZ(pos.getZ()); - } catch (NullPointerException ignored) {} - - return piece; - } - - public BlockVector rotate(BlockVector direction) { - return rotate(direction, 0, 0, 0); - } - - public IrisDirection rotate(IrisDirection direction) { - BlockVector v = rotate(direction.toVector().toBlockVector()); - return IrisDirection.closest(v); - } - - public double getRotation(int spin, IrisAxisRotationClamp clamp) { - if (!enabled) { - return 0; - } - - if (!clamp.isEnabled()) { - return 0; - } - - return clamp.getRadians(spin); - } - - public BlockFace getFace(BlockVector v) { - int x = (int) Math.round(v.getX()); - int y = (int) Math.round(v.getY()); - int z = (int) Math.round(v.getZ()); - - if (x == 0 && z == -1) { - return BlockFace.NORTH; - } - - if (x == 0 && z == 1) { - return BlockFace.SOUTH; - } - - if (x == 1 && z == 0) { - return BlockFace.EAST; - } - - if (x == -1 && z == 0) { - return BlockFace.WEST; - } - - if (y > 0) { - return BlockFace.UP; - } - - if (y < 0) { - return BlockFace.DOWN; - } - - return BlockFace.SOUTH; - } - - public BlockFace getHexFace(BlockVector v) { - int x = v.getBlockX(); - int y = v.getBlockY(); - int z = v.getBlockZ(); - - if (x == 0 && z == -1) return BlockFace.NORTH; - if (x == 1 && z == -2) return BlockFace.NORTH_NORTH_EAST; - if (x == 1 && z == -1) return BlockFace.NORTH_EAST; - if (x == 2 && z == -1) return BlockFace.EAST_NORTH_EAST; - if (x == 1 && z == 0) return BlockFace.EAST; - if (x == 2 && z == 1) return BlockFace.EAST_SOUTH_EAST; - if (x == 1 && z == 1) return BlockFace.SOUTH_EAST; - if (x == 1 && z == 2) return BlockFace.SOUTH_SOUTH_EAST; - if (x == 0 && z == 1) return BlockFace.SOUTH; - if (x == -1 && z == 2) return BlockFace.SOUTH_SOUTH_WEST; - if (x == -1 && z == 1) return BlockFace.SOUTH_WEST; - if (x == -2 && z == 1) return BlockFace.WEST_SOUTH_WEST; - if (x == -1 && z == 0) return BlockFace.WEST; - if (x == -2 && z == -1) return BlockFace.WEST_NORTH_WEST; - if (x == -1 && z == -1) return BlockFace.NORTH_WEST; - if (x == -1 && z == -2) return BlockFace.NORTH_NORTH_WEST; - - if (y > 0) { - return BlockFace.UP; - } - - if (y < 0) { - return BlockFace.DOWN; - } - - return BlockFace.SOUTH; - } - - public BlockFace faceForAxis(Axis axis) { - return switch (axis) { - case X -> BlockFace.EAST; - case Y -> BlockFace.UP; - case Z -> BlockFace.NORTH; - }; - - } - - public Axis axisFor(BlockFace f) { - return switch (f) { - case NORTH, SOUTH -> Axis.Z; - case EAST, WEST -> Axis.X; - default -> Axis.Y; - }; - - } - - public Axis axisFor2D(BlockFace f) { - return switch (f) { - case EAST, WEST, UP, DOWN -> Axis.X; - default -> Axis.Z; - }; - - } - - public BlockData rotate(BlockData dd, int spinxx, int spinyy, int spinzz) { - BlockData d = dd; - try { - int spinx = (int) (90D * (Math.ceil(Math.abs((spinxx % 360D) / 90D)))); - int spiny = (int) (90D * (Math.ceil(Math.abs((spinyy % 360D) / 90D)))); - int spinz = (int) (90D * (Math.ceil(Math.abs((spinzz % 360D) / 90D)))); - - if (!canRotate()) { - return d; - } - - if (d instanceof Directional g) { - BlockFace f = g.getFacing(); - BlockVector bv = new BlockVector(f.getModX(), f.getModY(), f.getModZ()); - bv = rotate(bv.clone(), spinx, spiny, spinz); - BlockFace t = getFace(bv); - - if (g.getFaces().contains(t)) { - g.setFacing(t); - } else if (!g.getMaterial().isSolid()) { - d = null; - } - } else if (d instanceof Rotatable g) { - BlockFace f = g.getRotation(); - - BlockVector bv = new BlockVector(f.getModX(), 0, f.getModZ()); - bv = rotate(bv.clone(), spinx, spiny, spinz); - BlockFace face = getHexFace(bv); - - g.setRotation(face); - - } else if (d instanceof Orientable g) { - BlockFace f = getFace(g.getAxis()); - BlockVector bv = new BlockVector(f.getModX(), f.getModY(), f.getModZ()); - bv = rotate(bv.clone(), spinx, spiny, spinz); - Axis a = getAxis(bv); - - if (!a.equals(g.getAxis()) && g.getAxes().contains(a)) { - g.setAxis(a); - } - } else if (d instanceof MultipleFacing g) { - List faces = new KList<>(); - - for (BlockFace i : g.getFaces()) { - BlockVector bv = new BlockVector(i.getModX(), i.getModY(), i.getModZ()); - bv = rotate(bv.clone(), spinx, spiny, spinz); - BlockFace r = getFace(bv); - - if (g.getAllowedFaces().contains(r)) { - faces.add(r); - } - } - - for (BlockFace i : g.getFaces()) { - g.setFace(i, false); - } - - for (BlockFace i : faces) { - g.setFace(i, true); - } - } else if (d instanceof Wall wall) { - KMap faces = new KMap<>(); - - for (BlockFace i : WALL_FACES) { - Wall.Height h = wall.getHeight(i); - BlockVector bv = new BlockVector(i.getModX(), i.getModY(), i.getModZ()); - bv = rotate(bv.clone(), spinx, spiny, spinz); - BlockFace r = getFace(bv); - if (WALL_FACES.contains(r)) { - faces.put(r, h); - } - } - - for (BlockFace i : WALL_FACES) { - wall.setHeight(i, faces.getOrDefault(i, Wall.Height.NONE)); - } - } else if (d instanceof RedstoneWire wire) { - Map faces = new HashMap<>(); - - var allowed = wire.getAllowedFaces(); - for (BlockFace i : allowed) { - RedstoneWire.Connection connection = wire.getFace(i); - BlockVector bv = new BlockVector(i.getModX(), i.getModY(), i.getModZ()); - bv = rotate(bv.clone(), spinx, spiny, spinz); - BlockFace r = getFace(bv); - if (allowed.contains(r)) - faces.put(r, connection); - } - - for (BlockFace i : allowed) { - wire.setFace(i, faces.getOrDefault(i, RedstoneWire.Connection.NONE)); - } - } - } catch (Throwable e) { - Iris.reportError(e); - - } - - return d; - } - - public Axis getAxis(BlockVector v) { - if (Math.abs(v.getBlockX()) > Math.max(Math.abs(v.getBlockY()), Math.abs(v.getBlockZ()))) { - return Axis.X; - } - - if (Math.abs(v.getBlockY()) > Math.max(Math.abs(v.getBlockX()), Math.abs(v.getBlockZ()))) { - return Axis.Y; - } - - if (Math.abs(v.getBlockZ()) > Math.max(Math.abs(v.getBlockX()), Math.abs(v.getBlockY()))) { - return Axis.Z; - } - - return Axis.Y; - } - - private BlockFace getFace(Axis axis) { - return switch (axis) { - case X -> BlockFace.EAST; - case Y -> BlockFace.UP; - case Z -> BlockFace.SOUTH; - }; - } - - public IrisPosition rotate(IrisPosition b) { - return rotate(b, 0, 0, 0); - } - - public IrisPosition rotate(IrisPosition b, int spinx, int spiny, int spinz) { - return new IrisPosition(rotate(new BlockVector(b.getX(), b.getY(), b.getZ()), spinx, spiny, spinz)); - } - - public BlockVector rotate(BlockVector b, int spinx, int spiny, int spinz) { - if (!canRotate()) { - return b; - } - - BlockVector v = b.clone(); - - if (canRotateX()) { - if (getXAxis().isLocked()) { - if (Math.abs(getXAxis().getMax()) % 360D == 180D) { - v.setZ(-v.getZ()); - v.setY(-v.getY()); - } else if (getXAxis().getMax() % 360D == 90D || getXAxis().getMax() % 360D == -270D) { - double z = v.getZ(); - v.setZ(v.getY()); - v.setY(-z); - } else if (getXAxis().getMax() == -90D || getXAxis().getMax() % 360D == 270D) { - double z = v.getZ(); - v.setZ(-v.getY()); - v.setY(z); - } else { - v.rotateAroundX(getXRotation(spinx)); - } - } else { - v.rotateAroundX(getXRotation(spinx)); - } - } - - if (canRotateZ()) { - if (getZAxis().isLocked()) { - if (Math.abs(getZAxis().getMax()) % 360D == 180D) { - v.setY(-v.getY()); - v.setX(-v.getX()); - } else if (getZAxis().getMax() % 360D == 90D || getZAxis().getMax() % 360D == -270D) { - double y = v.getY(); - v.setY(v.getX()); - v.setX(-y); - } else if (getZAxis().getMax() == -90D || getZAxis().getMax() % 360D == 270D) { - double y = v.getY(); - v.setY(-v.getX()); - v.setX(y); - } else { - v.rotateAroundZ(getZRotation(spinz)); - } - } else { - v.rotateAroundY(getZRotation(spinz)); - } - } - - if (canRotateY()) { - if (getYAxis().isLocked()) { - if (Math.abs(getYAxis().getMax()) % 360D == 180D) { - v.setX(-v.getX()); - v.setZ(-v.getZ()); - } else if (getYAxis().getMax() % 360D == 90D || getYAxis().getMax() % 360D == -270D) { - double x = v.getX(); - v.setX(v.getZ()); - v.setZ(-x); - } else if (getYAxis().getMax() == -90D || getYAxis().getMax() % 360D == 270D) { - double x = v.getX(); - v.setX(-v.getZ()); - v.setZ(x); - } else { - v.rotateAroundY(getYRotation(spiny)); - } - } else { - v.rotateAroundY(getYRotation(spiny)); - } - } - - return v; - } - - public boolean canRotateX() { - return enabled && xAxis.isEnabled(); - } - - public boolean canRotateY() { - return enabled && yAxis.isEnabled(); - } - - public boolean canRotateZ() { - return enabled && zAxis.isEnabled(); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean canRotate() { - return canRotateX() || canRotateY() || canRotateZ(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectScale.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectScale.java deleted file mode 100644 index 0b8c116bf..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectScale.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MaxNumber; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("object-scale") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Scale objects") -@Data -public class IrisObjectScale { - private static ConcurrentLinkedHashMap> cache - = new ConcurrentLinkedHashMap.Builder>() - .initialCapacity(64) - .maximumWeightedCapacity(1024) - .concurrencyLevel(32) - .build(); - @MinNumber(1) - @MaxNumber(32) - @Desc("Iris Objects are scaled and cached to speed up placements. Because of this extra memory is used, so we evenly distribute variations across the defined scale range, then pick one randomly. If the differences is small, use a lower number. For more possibilities on the scale spectrum, increase this at the cost of memory.") - private int variations = 7; - @MinNumber(0.01) - @MaxNumber(50) - @Desc("The minimum scale") - private double minimumScale = 1; - @MinNumber(0.01) - @MaxNumber(50) - @Desc("The maximum height for placement (top of object)") - private double maximumScale = 1; - @Desc("If this object is scaled up beyond its origin size, specify a 3D interpolator") - private IrisObjectPlacementScaleInterpolator interpolation = IrisObjectPlacementScaleInterpolator.NONE; - - public boolean shouldScale() { - return ((minimumScale == maximumScale) && maximumScale == 1) || variations <= 0; - } - - public int getMaxSizeFor(int indim) { - return (int) (getMaxScale() * indim); - } - - public double getMaxScale() { - double mx = 0; - - for (double i = minimumScale; i < maximumScale; i += (maximumScale - minimumScale) / (double) (Math.min(variations, 32))) { - mx = i; - } - - return mx; - } - - public IrisObject get(RNG rng, IrisObject origin) { - if (shouldScale()) { - return origin; - } - - return cache.computeIfAbsent(origin, (k) -> { - KList c = new KList<>(); - for (double i = minimumScale; i < maximumScale; i += (maximumScale - minimumScale) / (double) (Math.min(variations, 32))) { - c.add(origin.scaled(i, getInterpolation())); - } - - return c; - }).getRandom(rng); - } - - public boolean canScaleBeyond() { - return shouldScale() && maximumScale > 1; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectTranslate.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectTranslate.java deleted file mode 100644 index af8b83e6d..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectTranslate.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.util.BlockVector; - -@Snippet("object-translator") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisObjectTranslate { - @MinNumber(-128) // TODO: WARNING HEIGHT - @MaxNumber(128) // TODO: WARNING HEIGHT - @Desc("The x shift in blocks") - private int x = 0; - - @Required - @MinNumber(-128) // TODO: WARNING HEIGHT - @MaxNumber(128) // TODO: WARNING HEIGHT - @Desc("The y shift in blocks") - private int y = 0; - - @MinNumber(-128) // TODO: WARNING HEIGHT - @MaxNumber(128) // TODO: WARNING HEIGHT - @Desc("Adds an additional amount of height randomly (translateY + rand(0 - yRandom))") - private int yRandom = 0; - - @MinNumber(-128) // TODO: WARNING HEIGHT - @MaxNumber(128) // TODO: WARNING HEIGHT - @Desc("The z shift in blocks") - private int z = 0; - - public boolean canTranslate() { - return x != 0 || y != 0 || z != 0; - } - - public BlockVector translate(BlockVector i) { - if (canTranslate()) { - return (BlockVector) i.clone().add(new BlockVector(x, y, z)); - } - - return i; - } - - public BlockVector translate(BlockVector clone, IrisObjectRotation rotation, int sx, int sy, int sz) { - if (canTranslate()) { - return (BlockVector) clone.clone().add(rotation.rotate(new BlockVector(x, y, z), sx, sy, sz)); - } - - return clone; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectVanillaLoot.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectVanillaLoot.java deleted file mode 100644 index bd93ea998..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectVanillaLoot.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.engine.object.annotations.functions.LootTableKeyFunction; -import com.volmit.iris.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -@Snippet("object-vanilla-loot") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents vanilla loot within this object or jigsaw piece") -@Data -public class IrisObjectVanillaLoot implements IObjectLoot { - private final transient AtomicCache> filterCache = new AtomicCache<>(); - @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("The list of blocks this loot table should apply to") - private KList filter = new KList<>(); - @Desc("Exactly match the block data or not") - private boolean exact = false; - @Desc("The vanilla loot table key") - @Required - @RegistryListFunction(LootTableKeyFunction.class) - private String name; - @Desc("The weight of this loot table being chosen") - private int weight = 1; - - public KList getFilter(IrisData rdata) { - return filterCache.aquire(() -> - { - KList b = new KList<>(); - - for (IrisBlockData i : filter) { - BlockData bx = i.getBlockData(rdata); - - if (bx != null) { - b.add(bx); - } - } - - return b; - }); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisOreGenerator.java b/core/src/main/java/com/volmit/iris/engine/object/IrisOreGenerator.java deleted file mode 100644 index 6ec359680..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisOreGenerator.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Ore Layer") -@Data -public class IrisOreGenerator { - @Desc("The palette of 'ore' generated") - private IrisMaterialPalette palette = new IrisMaterialPalette().qclear(); - @Desc("The generator style for the 'ore'") - private IrisGeneratorStyle chanceStyle = new IrisGeneratorStyle(NoiseStyle.STATIC); - @Desc("Will ores generate on the surface of the terrain layer") - private boolean generateSurface = false; - @Desc("Threshold for rate of generation") - private double threshold = 0.5; - @Desc("Height limit (min, max)") - private IrisRange range = new IrisRange(30, 80); - - private transient AtomicCache chanceCache = new AtomicCache<>(); - - public BlockData generate(int x, int y, int z, RNG rng, IrisData data) { - if (palette.getPalette().isEmpty()) { - return null; - } - - if (!range.contains(y)) { - return null; - } - - CNG chance = chanceCache.aquire(() -> chanceStyle.create(rng, data)); - - if (chance.noise(x, y, z) > threshold) { - return null; - } - - return palette.get(rng, x, y, z, data); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisPosition.java b/core/src/main/java/com/volmit/iris/engine/object/IrisPosition.java deleted file mode 100644 index 1f3f6bf28..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisPosition.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.util.BlockVector; -import org.bukkit.util.Vector; - -@Snippet("position-3d") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a position") -@Data -public class IrisPosition { - @Desc("The x position") - private int x = 0; - - @Desc("The y position") - private int y = 0; - - @Desc("The z position") - private int z = 0; - - public IrisPosition(BlockVector bv) { - this(bv.getBlockX(), bv.getBlockY(), bv.getBlockZ()); - } - - public IrisPosition(Location l) { - this(l.getBlockX(), l.getBlockY(), l.getBlockZ()); - } - - public IrisPosition(Vector v) { - this(v.getBlockX(), v.getBlockY(), v.getBlockZ()); - } - - public IrisPosition(double x, double y, double z) { - this((int) x, (int) y, (int) z); - } - - - public IrisPosition add(IrisPosition relativePosition) { - return new IrisPosition(relativePosition.x + x, relativePosition.y + y, relativePosition.z + z); - } - - public IrisPosition sub(IrisPosition relativePosition) { - return new IrisPosition(x - relativePosition.x, y - relativePosition.y, z - relativePosition.z); - } - - public Location toLocation(World world) { - return new Location(world, x, y, z); - } - - public IrisPosition copy() { - return new IrisPosition(x, y, z); - } - - @Override - public String toString() { - return "[" + getX() + "," + getY() + "," + getZ() + "]"; - } - - public boolean isLongerThan(IrisPosition s, int maxLength) { - return Math.abs(Math.pow(s.x - x, 2) + Math.pow(s.y - y, 2) + Math.pow(s.z - z, 2)) > maxLength * maxLength; - } - - public Vector toVector() { - return new Vector(x, y, z); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisPosition2D.java b/core/src/main/java/com/volmit/iris/engine/object/IrisPosition2D.java deleted file mode 100644 index e56aee810..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisPosition2D.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("position-2d") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a position") -@Data -public class IrisPosition2D { - @Desc("The x position") - private int x = 0; - - @Desc("The z position") - private int z = 0; -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisPotionEffect.java b/core/src/main/java/com/volmit/iris/engine/object/IrisPotionEffect.java deleted file mode 100644 index 73d06ebc5..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisPotionEffect.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.entity.LivingEntity; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor - -@Snippet("potion-effect") -@Desc("An iris potion effect") -@Data -public class IrisPotionEffect { - private final transient AtomicCache pt = new AtomicCache<>(); - @Required - @Desc("The potion effect to apply in this area") - private String potionEffect = ""; - @Required - @MinNumber(-1) - @MaxNumber(1024) - @Desc("The Potion Strength or -1 to disable") - private int strength = -1; - @Required - @MinNumber(1) - @Desc("The time the potion will last for") - private int ticks = 200; - @Desc("Is the effect ambient") - private boolean ambient = false; - @Desc("Is the effect showing particles") - private boolean particles = true; - - public PotionEffectType getRealType() { - return pt.aquire(() -> - { - PotionEffectType t = PotionEffectType.LUCK; - - if (getPotionEffect().isEmpty()) { - return t; - } - - try { - for (PotionEffectType i : PotionEffectType.values()) { - if (i.getName().toUpperCase().replaceAll("\\Q \\E", "_").equals(getPotionEffect())) { - t = i; - - return t; - } - } - } catch (Throwable e) { - Iris.reportError(e); - - } - - Iris.warn("Unknown Potion Effect Type: " + getPotionEffect()); - - return t; - }); - } - - public void apply(LivingEntity p) { - if (strength > -1) { - if (p.hasPotionEffect(getRealType())) { - PotionEffect e = p.getPotionEffect(getRealType()); - if (e.getAmplifier() > strength) { - return; - } - - p.removePotionEffect(getRealType()); - } - - p.addPotionEffect(new PotionEffect(getRealType(), ticks, strength, ambient, particles, false)); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisPreProcessors.java b/core/src/main/java/com/volmit/iris/engine/object/IrisPreProcessors.java deleted file mode 100644 index 294e22636..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisPreProcessors.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.engine.object.annotations.functions.ResourceLoadersFunction; -import com.volmit.iris.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -@Desc("Represents global preprocessors") -public class IrisPreProcessors { - @Required - @Desc("The preprocessor type") - @RegistryListFunction(ResourceLoadersFunction.class) - private String type = "dimension"; - - @Required - @Desc("The preprocessor scripts\nFile extension: .proc.kts") - @RegistryListResource(IrisScript.class) - @ArrayType(type = String.class, min = 1) - private KList scripts = new KList<>(); -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisPyramid.java b/core/src/main/java/com/volmit/iris/engine/object/IrisPyramid.java deleted file mode 100644 index 13fff726c..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisPyramid.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.matter.slices.CavernMatter; -import lombok.Data; - -@Snippet("carving-pyramid") -@Desc("Represents an procedural eliptical shape") -@Data -public class IrisPyramid implements IRare { - private transient final AtomicCache matterNodeCache = new AtomicCache<>(); - @Required - @Desc("Typically a 1 in RARITY on a per fork basis") - @MinNumber(1) - private int rarity = 1; - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - @Desc("The styled random radius for x") - private IrisStyledRange baseWidth = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - - public void generate(RNG rng, Engine engine, MantleWriter writer, int x, int y, int z) { - writer.setPyramid(x, y, z, matterNodeCache.aquire(() -> CavernMatter.get(getCustomBiome(), 0)), - (int) baseWidth.get(rng, z, y, engine.getData()), true); - } - - public double maxSize() { - return baseWidth.getMax(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisRareObject.java b/core/src/main/java/com/volmit/iris/engine/object/IrisRareObject.java deleted file mode 100644 index 3cb042989..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisRareObject.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("rare-object") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a structure tile") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisRareObject { - @Required - @MinNumber(1) - @Desc("The rarity is 1 in X") - private int rarity = 1; - - @RegistryListResource(IrisObject.class) - @Required - @Desc("The object to place if rarity check passed") - private String object = ""; -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisRavine.java b/core/src/main/java/com/volmit/iris/engine/object/IrisRavine.java deleted file mode 100644 index b7c453bd6..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisRavine.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MaxNumber; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.RegistryListResource; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@EqualsAndHashCode(callSuper = true) -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisRavine extends IrisRegistrant { - @Desc("Define the shape of this ravine (2d, ignores Y)") - private IrisWorm worm = new IrisWorm(); - - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - - @Desc("Define potential forking features") - private IrisCarving fork = new IrisCarving(); - - @Desc("The style used to determine the curvature of this worm's y") - private IrisShapedGeneratorStyle depthStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, 5, 18); - - @Desc("The style used to determine the curvature of this worm's y") - private IrisShapedGeneratorStyle baseWidthStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, 3, 6); - - @MinNumber(1) - @MaxNumber(100) - @Desc("The angle at which the ravine widens as it gets closer to the surface") - private double angle = 18; - - @MinNumber(1) - @MaxNumber(100) - @Desc("The angle at which the ravine widens as it gets closer to the surface") - private double topAngle = 38; - - @Desc("To fill this cave with lava, set the lava level to a height from the bottom most point of the cave.") - private int lavaLevel = -1; - - @Desc("How many worm nodes must be placed to actually generate a ravine? Higher reduces the chances but also reduces ravine 'holes'") - private int nodeThreshold = 5; - - @MinNumber(1) - @MaxNumber(8) - @Desc("The thickness of the ravine ribs") - private double ribThickness = 3; - - @Override - public String getFolderName() { - return "ravines"; - } - - @Override - public String getTypeName() { - return "Ravine"; - } - - public void generate(MantleWriter writer, RNG rng, Engine engine, int x, int y, int z) { - generate(writer, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, 0, -1); - } - - public void generate(MantleWriter writer, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint) { - KList pos = getWorm().generate(base.nextParallelRNG(879615), engine.getData(), writer, null, x, y, z, true, 0); - CNG dg = depthStyle.getGenerator().create(base.nextParallelRNG(7894156), engine.getData()); - CNG bw = baseWidthStyle.getGenerator().create(base.nextParallelRNG(15315456), engine.getData()); - int highestWater = Math.max(waterHint, -1); - boolean water = false; - - if (highestWater == -1) { - for (IrisPosition i : pos) { - int rsurface = y == -1 ? engine.getComplex().getHeightStream().get(x, z).intValue() : y; - int depth = (int) Math.round(dg.fitDouble(depthStyle.getMin(), depthStyle.getMax(), i.getX(), i.getZ())); - int surface = (int) Math.round(rsurface - depth * 0.45); - int yy = surface + depth; - int th = engine.getHeight(x, z, true); - - if (yy > th && th < engine.getDimension().getFluidHeight()) { - highestWater = Math.max(highestWater, yy); - water = true; - break; - } - } - } else { - water = true; - } - - MatterCavern c = new MatterCavern(true, customBiome, (byte) (water ? 1 : 0)); - MatterCavern l = new MatterCavern(true, customBiome, (byte) 2); - - if (pos.size() < nodeThreshold) { - return; - } - - for (IrisPosition p : pos) { - int rsurface = y == -1 ? engine.getComplex().getHeightStream().get(x, z).intValue() : y; - int depth = (int) Math.round(dg.fitDouble(depthStyle.getMin(), depthStyle.getMax(), p.getX(), p.getZ())); - int width = (int) Math.round(bw.fitDouble(baseWidthStyle.getMin(), baseWidthStyle.getMax(), p.getX(), p.getZ())); - int surface = (int) Math.round(rsurface - depth * 0.45); - - fork.doCarving(writer, rng, base, engine, p.getX(), rng.i(surface - depth, surface), p.getZ(), recursion, highestWater); - - for (int i = surface + depth; i >= surface; i--) { - if (i % ribThickness == 0) { - double v = width + ((((surface + depth) - i) * (angle / 360D))); - - if (v <= 0.25) { - break; - } - - if (i <= ribThickness + 2) { - break; - } - - if (lavaLevel >= 0 && i <= lavaLevel + (surface - depthStyle.getMid())) { - writer.setElipsoid(p.getX(), i, p.getZ(), v, ribThickness, v, true, l); - } else { - writer.setElipsoid(p.getX(), i, p.getZ(), v, ribThickness, v, true, c); - } - } - } - - for (int i = surface - depth; i <= surface; i++) { - if (i % ribThickness == 0) { - double v = width - ((((surface - depth) - i) * (angle / 360D))); - - if (v <= 0.25) { - break; - } - - if (i <= ribThickness + 2) { - break; - } - - if (lavaLevel >= 0 && i <= lavaLevel + (surface - depthStyle.getMid())) { - writer.setElipsoid(p.getX(), i, p.getZ(), v, ribThickness, v, true, l); - } else { - writer.setElipsoid(p.getX(), i, p.getZ(), v, ribThickness, v, true, c); - } - } - } - } - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } - - public int getMaxSize(IrisData data, int depth) { - return getWorm().getMaxDistance() + fork.getMaxRange(data, depth); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisRavinePlacer.java b/core/src/main/java/com/volmit/iris/engine/object/IrisRavinePlacer.java deleted file mode 100644 index ed9c01841..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisRavinePlacer.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.util.concurrent.atomic.AtomicBoolean; - -@Snippet("ravine-placer") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisRavinePlacer implements IRare { - private transient final AtomicCache ravineCache = new AtomicCache<>(); - private transient final AtomicBoolean fail = new AtomicBoolean(false); - @Required - @Desc("Typically a 1 in RARITY on a per chunk/fork basis") - @MinNumber(1) - private int rarity = 15; - @MinNumber(1) - @Required - @Desc("The ravine to place") - @RegistryListResource(IrisRavine.class) - private String ravine; - @MinNumber(1) - @MaxNumber(256) - @Desc("The maximum recursion depth") - private int maxRecursion = 100; - - public IrisRavine getRealRavine(IrisData data) { - return ravineCache.aquire(() -> data.getRavineLoader().load(getRavine())); - } - - public void generateRavine(MantleWriter mantle, RNG rng, Engine engine, int x, int y, int z) { - generateRavine(mantle, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, 0, -1); - } - - public void generateRavine(MantleWriter mantle, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint) { - if (fail.get()) { - return; - } - - if (rng.nextInt(rarity) != 0) { - return; - } - - IrisData data = engine.getData(); - IrisRavine ravine = getRealRavine(data); - - if (ravine == null) { - Iris.warn("Unable to locate ravine for generation!"); - fail.set(true); - return; - } - - try { - int xx = x + rng.nextInt(15); - int zz = z + rng.nextInt(15); - ravine.generate(mantle, rng, base, engine, xx, y, zz, recursion, waterHint); - } catch (Throwable e) { - e.printStackTrace(); - fail.set(true); - } - } - - public int getSize(IrisData data, int depth) { - return getRealRavine(data).getMaxSize(data, depth); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisRegion.java b/core/src/main/java/com/volmit/iris/engine/object/IrisRegion.java deleted file mode 100644 index 157cb61c9..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisRegion.java +++ /dev/null @@ -1,419 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.gui.components.RenderType; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.data.DataProvider; -import com.volmit.iris.util.data.VanillaBiomeMap; -import com.volmit.iris.util.inventorygui.RandomColor; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.block.data.BlockData; - -import java.awt.*; -import java.util.Random; - - -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents an iris region") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisRegion extends IrisRegistrant implements IRare { - private final transient AtomicCache> surfaceObjectsCache = new AtomicCache<>(); - private final transient AtomicCache> carveObjectsCache = new AtomicCache<>(); - private final transient AtomicCache> cacheRidge = new AtomicCache<>(); - private final transient AtomicCache> cacheSpot = new AtomicCache<>(); - private final transient AtomicCache shoreHeightGenerator = new AtomicCache<>(); - private final transient AtomicCache> realLandBiomes = new AtomicCache<>(); - private final transient AtomicCache> realLakeBiomes = new AtomicCache<>(); - private final transient AtomicCache> realRiverBiomes = new AtomicCache<>(); - private final transient AtomicCache> realSeaBiomes = new AtomicCache<>(); - private final transient AtomicCache> realShoreBiomes = new AtomicCache<>(); - private final transient AtomicCache> realCaveBiomes = new AtomicCache<>(); - private final transient AtomicCache lakeGen = new AtomicCache<>(); - private final transient AtomicCache riverGen = new AtomicCache<>(); - private final transient AtomicCache riverChanceGen = new AtomicCache<>(); - private final transient AtomicCache cacheColor = new AtomicCache<>(); - @MinNumber(2) - @Required - @Desc("The name of the region") - private String name = "A Region"; - @ArrayType(min = 1, type = IrisJigsawStructurePlacement.class) - @Desc("Jigsaw structures") - private KList jigsawStructures = new KList<>(); - @ArrayType(min = 1, type = IrisEffect.class) - @Desc("Effects are ambient effects such as potion effects, random sounds, or even particles around each player. All of these effects are played via packets so two players won't see/hear each others effects.\nDue to performance reasons, effects will play arround the player even if where the effect was played is no longer in the biome the player is in.") - private KList effects = new KList<>(); - @Desc("Spawn Entities in this region over time. Iris will continually replenish these mobs just like vanilla does.") - @ArrayType(min = 1, type = String.class) - @RegistryListResource(IrisSpawner.class) - private KList entitySpawners = new KList<>(); - @MinNumber(1) - @MaxNumber(128) - @Desc("The rarity of the region") - private int rarity = 1; - @ArrayType(min = 1, type = IrisBlockDrops.class) - @Desc("Define custom block drops for this region") - private KList blockDrops = new KList<>(); - @RegistryListResource(IrisSpawner.class) - @ArrayType(min = 1, type = IrisObjectPlacement.class) - @Desc("Objects define what schematics (iob files) iris will place in this region") - private KList objects = new KList<>(); - @MinNumber(0) - @Desc("The min shore height") - private double shoreHeightMin = 1.2; - @Desc("Reference loot tables in this area") - private IrisLootReference loot = new IrisLootReference(); - @MinNumber(0) - @Desc("The the max shore height") - private double shoreHeightMax = 3.2; - @MinNumber(0.0001) - @Desc("The varience of the shore height") - private double shoreHeightZoom = 3.14; - @MinNumber(0.0001) - @Desc("How large land biomes are in this region") - private double landBiomeZoom = 1; - @MinNumber(0.0001) - @Desc("How large shore biomes are in this region") - private double shoreBiomeZoom = 1; - @MinNumber(0.0001) - @Desc("How large sea biomes are in this region") - private double seaBiomeZoom = 1; - @MinNumber(0.0001) - @Desc("How large cave biomes are in this region") - private double caveBiomeZoom = 1; - @Desc("Carving configuration for the dimension") - private IrisCarving carving = new IrisCarving(); - @Desc("Configuration of fluid bodies such as rivers & lakes") - private IrisFluidBodies fluidBodies = new IrisFluidBodies(); - @RegistryListResource(IrisBiome.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("A list of root-level biomes in this region. Don't specify child biomes of other biomes here. Just the root parents.") - private KList landBiomes = new KList<>(); - @RegistryListResource(IrisBiome.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("A list of root-level biomes in this region. Don't specify child biomes of other biomes here. Just the root parents.") - private KList seaBiomes = new KList<>(); - @RegistryListResource(IrisBiome.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("A list of root-level biomes in this region. Don't specify child biomes of other biomes here. Just the root parents.") - private KList shoreBiomes = new KList<>(); - @RegistryListResource(IrisBiome.class) - @ArrayType(min = 1, type = String.class) - @Desc("A list of root-level biomes in this region. Don't specify child biomes of other biomes here. Just the root parents.") - private KList caveBiomes = new KList<>(); - @ArrayType(min = 1, type = IrisDepositGenerator.class) - @Desc("Define regional deposit generators that add onto the global deposit generators") - private KList deposits = new KList<>(); - @Desc("The style of rivers") - private IrisGeneratorStyle riverStyle = NoiseStyle.VASCULAR_THIN.style().zoomed(7.77); - @Desc("The style of lakes") - private IrisGeneratorStyle lakeStyle = NoiseStyle.CELLULAR_IRIS_THICK.style(); - @Desc("A color for visualizing this region with a color. I.e. #F13AF5. This will show up on the map.") - private String color = null; - @Desc("Collection of ores to be generated") - @ArrayType(type = IrisOreGenerator.class, min = 1) - private KList ores = new KList<>(); - - public BlockData generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) { - if (ores.isEmpty()) { - return null; - } - BlockData b = null; - for (IrisOreGenerator i : ores) { - if (i.isGenerateSurface() != surface) - continue; - - b = i.generate(x, y, z, rng, data); - if (b != null) { - return b; - } - } - return null; - } - - public String getName() { - return name; - } - - public KList getSurfaceObjects() { - return getSurfaceObjectsCache().aquire(() -> - { - KList o = getObjects().copy(); - - for (IrisObjectPlacement i : o.copy()) { - if (!i.getCarvingSupport().supportsSurface()) { - o.remove(i); - } - } - - return o; - }); - } - - public KList getCarvingObjects() { - return getCarveObjectsCache().aquire(() -> - { - KList o = getObjects().copy(); - - for (IrisObjectPlacement i : o.copy()) { - if (!i.getCarvingSupport().supportsCarving()) { - o.remove(i); - } - } - - return o; - }); - } - - public double getBiomeZoom(InferredType t) { - switch (t) { - case CAVE: - return caveBiomeZoom; - case LAND: - return landBiomeZoom; - case SEA: - return seaBiomeZoom; - case SHORE: - return shoreBiomeZoom; - default: - break; - } - - return 1; - } - - public CNG getShoreHeightGenerator() { - return shoreHeightGenerator.aquire(() -> - CNG.signature(new RNG((long) (getName().length() + getLandBiomeZoom() + getLandBiomes().size() + 3458612)))); - } - - public double getShoreHeight(double x, double z) { - return getShoreHeightGenerator().fitDouble(shoreHeightMin, shoreHeightMax, x / shoreHeightZoom, z / shoreHeightZoom); - } - - public KSet getAllBiomeIds() { - KSet names = new KSet<>(); - names.addAll(landBiomes); - names.addAll(caveBiomes); - names.addAll(seaBiomes); - names.addAll(shoreBiomes); - - return names; - } - - public KList getAllBiomes(DataProvider g) { - KMap b = new KMap<>(); - KSet names = getAllBiomeIds(); - - while (!names.isEmpty()) { - for (String i : new KList<>(names)) { - if (b.containsKey(i)) { - names.remove(i); - continue; - } - - IrisBiome biome = g.getData().getBiomeLoader().load(i); - - names.remove(i); - if (biome == null) { - continue; - } - - names.add(biome.getCarvingBiome()); - b.put(biome.getLoadKey(), biome); - names.addAll(biome.getChildren()); - } - } - - return b.v(); - } - - public KList getBiomes(DataProvider g, InferredType type) { - if (type.equals(InferredType.LAND)) { - return getRealLandBiomes(g); - } else if (type.equals(InferredType.SEA)) { - return getRealSeaBiomes(g); - } else if (type.equals(InferredType.SHORE)) { - return getRealShoreBiomes(g); - } else if (type.equals(InferredType.CAVE)) { - return getRealCaveBiomes(g); - } - - return new KList<>(); - } - - public KList getRealCaveBiomes(DataProvider g) { - return realCaveBiomes.aquire(() -> - { - KList realCaveBiomes = new KList<>(); - - for (String i : getCaveBiomes()) { - realCaveBiomes.add(g.getData().getBiomeLoader().load(i)); - } - - return realCaveBiomes; - }); - } - - public KList getRealShoreBiomes(DataProvider g) { - return realShoreBiomes.aquire(() -> - { - KList realShoreBiomes = new KList<>(); - - for (String i : getShoreBiomes()) { - realShoreBiomes.add(g.getData().getBiomeLoader().load(i)); - } - - return realShoreBiomes; - }); - } - - public KList getRealSeaBiomes(DataProvider g) { - return realSeaBiomes.aquire(() -> - { - KList realSeaBiomes = new KList<>(); - - for (String i : getSeaBiomes()) { - realSeaBiomes.add(g.getData().getBiomeLoader().load(i)); - } - - return realSeaBiomes; - }); - } - - public KList getRealLandBiomes(DataProvider g) { - return realLandBiomes.aquire(() -> - { - KList realLandBiomes = new KList<>(); - - for (String i : getLandBiomes()) { - realLandBiomes.add(g.getData().getBiomeLoader().load(i)); - } - - return realLandBiomes; - }); - } - - public KList getAllAnyBiomes() { - KMap b = new KMap<>(); - KSet names = new KSet<>(); - names.addAll(landBiomes); - names.addAll(caveBiomes); - names.addAll(seaBiomes); - names.addAll(shoreBiomes); - - while (!names.isEmpty()) { - for (String i : new KList<>(names)) { - if (b.containsKey(i)) { - names.remove(i); - continue; - } - - IrisBiome biome = IrisData.loadAnyBiome(i, getLoader()); - - names.remove(i); - if (biome == null) { - continue; - } - - names.add(biome.getCarvingBiome()); - b.put(biome.getLoadKey(), biome); - names.addAll(biome.getChildren()); - } - } - - return b.v(); - } - - public Color getColor(DataProvider dataProvider, RenderType type) { - return this.cacheColor.aquire(() -> { - if (this.color == null) { - Random rand = new Random(getName().hashCode() + getAllBiomeIds().hashCode()); - RandomColor randomColor = new RandomColor(rand); - - KList biomes = getRealLandBiomes(dataProvider); - - while (biomes.size() > 0) { - int index = rand.nextInt(biomes.size()); - IrisBiome biome = biomes.get(index); - - if (biome.getVanillaDerivative() != null) { - RandomColor.Color col = VanillaBiomeMap.getColorType(biome.getVanillaDerivative()); - RandomColor.Luminosity lum = VanillaBiomeMap.getColorLuminosity(biome.getVanillaDerivative()); - RandomColor.SaturationType sat = VanillaBiomeMap.getColorSaturatiom(biome.getVanillaDerivative()); - int newColorI = randomColor.randomColor(col, col == RandomColor.Color.MONOCHROME ? RandomColor.SaturationType.MONOCHROME : sat, lum); - return new Color(newColorI); - } - - biomes.remove(index); - } - - Iris.warn("Couldn't find a suitable color for region " + getName()); - return new Color(new RandomColor(rand).randomColor()); - } - - try { - return Color.decode(this.color); - } catch (NumberFormatException e) { - Iris.warn("Could not parse color \"" + this.color + "\" for region " + getName()); - return Color.WHITE; - } - }); - } - - public void pickRandomColor(DataProvider data) { - - } - - @Override - public String getFolderName() { - return "regions"; - } - - @Override - public String getTypeName() { - return "Region"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisRiver.java b/core/src/main/java/com/volmit/iris/engine/object/IrisRiver.java deleted file mode 100644 index 2e9ca75f2..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisRiver.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("river") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents an Iris river") -@Data -public class IrisRiver implements IRare { - @Required - @Desc("Typically a 1 in RARITY on a per chunk/fork basis") - @MinNumber(1) - private int rarity = 15; - - @Desc("The width style of this river") - private IrisStyledRange width = new IrisStyledRange(3, 6, NoiseStyle.PERLIN.style()); - - @Desc("Define the shape of this river") - private IrisWorm worm = new IrisWorm(); - - @RegistryListResource(IrisBiome.class) - @Desc("Force this river to only generate the specified custom biome") - private String customBiome = ""; - - @Desc("The width style of this lake") - private IrisShapedGeneratorStyle widthStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN.style(), 5, 9); - - @Desc("The depth style of this lake") - private IrisShapedGeneratorStyle depthStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN.style(), 4, 7); - - public int getSize(IrisData data) { - return worm.getMaxDistance(); - } - - public void generate(MantleWriter writer, RNG rng, Engine engine, int x, int y, int z) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisScript.java b/core/src/main/java/com/volmit/iris/engine/object/IrisScript.java deleted file mode 100644 index 7c28be980..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisScript.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@EqualsAndHashCode(callSuper = true) -@Data -public class IrisScript extends IrisRegistrant { - private final String source; - - public IrisScript() { - this(""); - } - - public IrisScript(String source) { - this.source = source; - } - - @Override - public String getFolderName() { - return "scripts"; - } - - @Override - public String getTypeName() { - return "Script"; - } - - public String toString() { - return source; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisSeed.java b/core/src/main/java/com/volmit/iris/engine/object/IrisSeed.java deleted file mode 100644 index 370ddac3e..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisSeed.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.math.RNG; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("color") -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Represents a color") -@Data -public class IrisSeed { - @Desc("The seed to use") - private long seed = 1337; - - @Desc("To calculate a seed Iris passes in it's natural seed for the current feature, then mixes it with your seed. Setting this to true ignores the parent seed and always uses your exact seed ignoring the input of Iris feature seeds. You can use this to match seeds on other generators.") - private boolean ignoreNaturalSeedInput = false; - - public long getSeed(long seed) { - return (seed * 47) + getSeed() + 29334667L; - } - - public RNG rng(long inseed) { - return new RNG(getSeed(inseed)); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisShapedGeneratorStyle.java b/core/src/main/java/com/volmit/iris/engine/object/IrisShapedGeneratorStyle.java deleted file mode 100644 index 990f3eb0b..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisShapedGeneratorStyle.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("shaped-style") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("This represents a generator with a min and max height") -@Data -public class IrisShapedGeneratorStyle { - @Required - @Desc("The generator id") - private IrisGeneratorStyle generator = new IrisGeneratorStyle(NoiseStyle.IRIS); - - @Required - @MinNumber(-2032) // TODO: WARNING HEIGHT - @MaxNumber(2032) // TODO: WARNING HEIGHT - @Desc("The min block value") - private int min = 0; - - @Required - @MinNumber(-2032) // TODO: WARNING HEIGHT - @MaxNumber(2032) // TODO: WARNING HEIGHT - @Desc("The max block value") - private int max = 0; - - public IrisShapedGeneratorStyle(NoiseStyle style, int min, int max) { - this(style); - this.min = min; - this.max = max; - } - - public IrisShapedGeneratorStyle(NoiseStyle style) { - this.generator = new IrisGeneratorStyle(style); - } - - public double get(RNG rng, IrisData data, double... dim) { - return generator.create(rng, data).fitDouble(min, max, dim); - } - - public boolean isFlat() { - return min == max || getGenerator().isFlat(); - } - - public int getMid() { - return (getMax() + getMin()) / 2; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisSphere.java b/core/src/main/java/com/volmit/iris/engine/object/IrisSphere.java deleted file mode 100644 index 434926bec..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisSphere.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.matter.slices.CavernMatter; -import lombok.Data; - -@Snippet("carving-sphere") -@Desc("Represents an procedural eliptical shape") -@Data -public class IrisSphere implements IRare { - private transient final AtomicCache matterNodeCache = new AtomicCache<>(); - @Required - @Desc("Typically a 1 in RARITY on a per fork basis") - @MinNumber(1) - private int rarity = 1; - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - @Desc("The styled random radius for x") - private IrisStyledRange radius = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - - public void generate(RNG rng, Engine engine, MantleWriter writer, int x, int y, int z) { - writer.setSphere(x, y, z, radius.get(rng, z, y, engine.getData()), true, matterNodeCache.aquire(() -> CavernMatter.get(getCustomBiome(), 0))); - } - - public double maxSize() { - return radius.getMax(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisStiltSettings.java b/core/src/main/java/com/volmit/iris/engine/object/IrisStiltSettings.java deleted file mode 100644 index ca4bd55a6..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisStiltSettings.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.MaxNumber; -import com.volmit.iris.engine.object.annotations.MinNumber; -import com.volmit.iris.engine.object.annotations.Snippet; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("stilt-settings") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Defines stilting behaviour.") -@Data -public class IrisStiltSettings { - @MinNumber(0) - @MaxNumber(64) - @Desc("Defines the maximum amount of blocks the object stilts verticially before overstilting and randomRange.") - private int yMax; - @MinNumber(0) - @MaxNumber(64) - @Desc("Defines the upper boundary for additional blocks after overstilting and/or maxStiltRange.") - private int yRand; - @MaxNumber(64) - @MinNumber(0) - @Desc("If the place mode is set to stilt, you can over-stilt it even further into the ground. Especially useful when using fast stilt due to inaccuracies.") - private int overStilt; - @Desc("If defined, stilting will be done using this block palette rather than the last layer of the object.") - private IrisMaterialPalette palette; - -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisStructurePopulator.java b/core/src/main/java/com/volmit/iris/engine/object/IrisStructurePopulator.java deleted file mode 100644 index 2c77da2ce..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisStructurePopulator.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineAssignedComponent; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.function.BiPredicate; - -public class IrisStructurePopulator extends EngineAssignedComponent { - private final CNG cng; - private final long mantle; - private final long jigsaw; - - public IrisStructurePopulator(Engine engine) { - super(engine, "Datapack Structures"); - mantle = engine.getSeedManager().getMantle(); - jigsaw = engine.getSeedManager().getJigsaw(); - cng = NoiseStyle.STATIC.create(new RNG(jigsaw)); - } - - @ChunkCoordinates - public void populateStructures(int x, int z, BiPredicate placer) { - int bX = x << 4, bZ = z << 4; - var dimension = getDimension(); - var region = getEngine().getRegion(bX + 8, bZ + 8); - var biome = getEngine().getSurfaceBiome(bX + 8, bZ + 8); - var loader = getData().getJigsawStructureLoader(); - - long seed = cng.fit(Integer.MIN_VALUE, Integer.MAX_VALUE, x, z); - if (dimension.getStronghold() != null) { - var list = getDimension().getStrongholds(mantle); - if (list != null && list.contains(new Position2(bX, bZ))) { - place(placer, loader.load(dimension.getStronghold()), new RNG(seed), true); - return; - } - } - - boolean placed = place(placer, biome.getJigsawStructures(), seed, x, z); - if (!placed) placed = place(placer, region.getJigsawStructures(), seed, x, z); - if (!placed) place(placer, dimension.getJigsawStructures(), seed, x, z); - } - - private boolean place(BiPredicate placer, KList placements, long seed, int x, int z) { - var placement = pick(placements, seed, x, z); - if (placement == null) return false; - return place(placer, getData().getJigsawStructureLoader().load(placement.getStructure()), new RNG(seed), false); - } - - @Nullable - @ChunkCoordinates - private IrisJigsawStructurePlacement pick(List structures, long seed, int x, int z) { - return IRare.pick(structures.stream() - .filter(p -> p.shouldPlace(getData(), getDimension().getJigsawStructureDivisor(), jigsaw, x, z)) - .toList(), new RNG(seed).nextDouble()); - } - - @ChunkCoordinates - private boolean place(BiPredicate placer, IrisJigsawStructure structure, RNG rng, boolean ignoreBiomes) { - if (structure == null || structure.getDatapackStructures().isEmpty()) return false; - var keys = structure.getDatapackStructures().shuffleCopy(rng); - while (keys.isNotEmpty()) { - String key = keys.removeFirst(); - if (key != null && placer.test(key, ignoreBiomes || structure.isForcePlace())) - return true; - } - return false; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisTree.java b/core/src/main/java/com/volmit/iris/engine/object/IrisTree.java deleted file mode 100644 index 1e8e8a8d6..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisTree.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.ArrayType; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Required; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.TreeType; - -@Snippet("tree") -@Accessors(chain = true) -@AllArgsConstructor -@NoArgsConstructor -@Desc("Tree replace options for this object placer") -@Data -public class IrisTree { - @Required - @Desc("The types of trees overwritten by this object") - @ArrayType(min = 1, type = TreeType.class) - private KList treeTypes; - - @Desc("If enabled, overrides any TreeType") - private boolean anyTree = false; - - @Required - @Desc("The size of the square of saplings this applies to (2 means a 2 * 2 sapling area)") - @ArrayType(min = 1, type = IrisTreeSize.class) - private KList sizes = new KList<>(); - - @Desc("If enabled, overrides trees of any size") - private boolean anySize; - - public boolean matches(IrisTreeSize size, TreeType type) { - if (!matchesSize(size)) { - return false; - } - - return matchesType(type); - } - - private boolean matchesSize(IrisTreeSize size) { - for (IrisTreeSize i : getSizes()) { - if ((i.getDepth() == size.getDepth() && i.getWidth() == size.getWidth()) || (i.getDepth() == size.getWidth() && i.getWidth() == size.getDepth())) { - return true; - } - } - - return false; - } - - private boolean matchesType(TreeType type) { - return getTreeTypes().contains(type); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisVanillaLootTable.java b/core/src/main/java/com/volmit/iris/engine/object/IrisVanillaLootTable.java deleted file mode 100644 index ee25d447b..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisVanillaLootTable.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.inventory.ItemStack; -import org.bukkit.loot.LootContext; -import org.bukkit.loot.LootTable; - -import java.io.File; - -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisVanillaLootTable extends IrisLootTable { - private final LootTable lootTable; - - @Override - public String getName() { - return "Vanilla " + lootTable.getKey(); - } - - @Override - public int getRarity() { - return 0; - } - - @Override - public int getMaxPicked() { - return 0; - } - - @Override - public int getMinPicked() { - return 0; - } - - @Override - public int getMaxTries() { - return 0; - } - - @Override - public KList getLoot() { - return new KList<>(); - } - - @Override - public KList getLoot(boolean debug, RNG rng, InventorySlotType slot, World world, int x, int y, int z) { - return new KList<>(lootTable.populateLoot(rng, new LootContext.Builder(new Location(world, x, y, z)).build())); - } - - @Override - public String getFolderName() { - throw new UnsupportedOperationException("VanillaLootTables do not have a folder name"); - } - - @Override - public String getTypeName() { - throw new UnsupportedOperationException("VanillaLootTables do not have a type name"); - } - - @Override - public File getLoadFile() { - throw new UnsupportedOperationException("VanillaLootTables do not have a load file"); - } - - @Override - public IrisData getLoader() { - throw new UnsupportedOperationException("VanillaLootTables do not have a loader"); - } - - @Override - public KList getPreprocessors() { - return new KList<>(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisVillagerOverride.java b/core/src/main/java/com/volmit/iris/engine/object/IrisVillagerOverride.java deleted file mode 100644 index e3c068a7e..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisVillagerOverride.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -@Snippet("villager-override") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Override cartographer map trades with others or disable the trade altogether") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisVillagerOverride { - @Desc(""" - Disable the trade altogether. - If a cartographer villager gets a new explorer map trade: - If this is enabled -> the trade is removed - If this is disabled -> the trade is replaced with the "override" setting below - Default is true, so if you omit this, trades will be removed.""") - private boolean disableTrade = true; - - @DependsOn("disableTrade") - @Required - @Desc(""" - The items to override the cartographer trade with. - By default, this is: - 3 emeralds + 3 glass blocks -> 1 spyglass. - Can trade 3 to 5 times""") - @ArrayType(min = 1, type = IrisVillagerTrade.class) - private KList items = new KList<>(new IrisVillagerTrade() - .setIngredient1(new ItemStack(Material.EMERALD, 3)) - .setIngredient2(new ItemStack(Material.GLASS, 3)) - .setResult(new ItemStack(Material.SPYGLASS)) - .setMinTrades(3) - .setMaxTrades(5)); - - public KList getValidItems() { - KList valid = new KList<>(); - getItems().stream().filter(IrisVillagerTrade::isValidItems).forEach(valid::add); - return valid.size() == 0 ? null : valid; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisVillagerTrade.java b/core/src/main/java/com/volmit/iris/engine/object/IrisVillagerTrade.java deleted file mode 100644 index a34a8915a..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisVillagerTrade.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.MerchantRecipe; - -import java.util.List; - - -@Snippet("villager-trade") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@SuppressWarnings("BooleanMethodIsAlwaysInverted") -@Desc("Represents a villager trade.") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisVillagerTrade { - - @Required - @RegistryListItemType - @Desc("The first, required, ingredient for the trade.\nNote: this MUST be an item, and may not be a non-obtainable block!") - private ItemStack ingredient1; - - @RegistryListItemType - @Desc("The second, optional, ingredient for the trade.\nNote: this MUST be an item, and may not be a non-obtainable block!") - private ItemStack ingredient2 = null; - - @Required - @RegistryListItemType - @Desc("The result of the trade.\nNote: this MUST be an item, and may not be a non-obtainable block!") - private ItemStack result; - - @Desc("The min amount of times this trade can be done. Default 3") - @MinNumber(1) - @MaxNumber(64) - private int minTrades = 3; - - @Desc("The max amount of times this trade can be done. Default 5") - @MinNumber(1) - @MaxNumber(64) - private int maxTrades = 5; - - /** - * @return true if:
- * ingredient 1 & result are non-null,
- * mintrades > 0, maxtrades > 0, maxtrades > mintrades, and
- * ingredient 1, (if defined ingredient 2) and the result are valid items - */ - public boolean isValidItems() { - KList warnings = new KList<>(); - if (ingredient1 == null) { - warnings.add("Ingredient 1 is null"); - } - - if (result == null) { - warnings.add("Result is null"); - } - - if (minTrades <= 0) { - warnings.add("Negative minimal trades"); - } - - if (maxTrades <= 0) { - warnings.add("Negative maximal trades"); - } - - if (minTrades > maxTrades) { - warnings.add("More minimal than maximal trades"); - } - - if (ingredient1 != null && !ingredient1.getType().isItem()) { - warnings.add("Ingredient 1 is not an item"); - } - - if (ingredient2 != null && !ingredient2.getType().isItem()) { - warnings.add("Ingredient 2 is not an item"); - } - - if (result != null && !result.getType().isItem()) { - warnings.add("Result is not an item"); - } - - if (warnings.isEmpty()) { - return true; - } else { - Iris.warn("Faulty item in cartographer item overrides: " + this); - warnings.forEach(w -> Iris.warn(" " + w)); - return false; - } - } - - /** - * Get the ingredients - * - * @return The list of 1 or 2 ingredients (depending on if ing2 is null) - */ - public List getIngredients() { - if (!isValidItems()) { - return null; - } - return ingredient2 == null ? new KList<>(ingredient1) : new KList<>(ingredient1, ingredient2); - } - - /** - * @return the amount of trades (RNG.r.i(min, max)) - */ - public int getAmount() { - return RNG.r.i(minTrades, maxTrades); - } - - /** - * @return the trade as a merchant recipe - */ - public MerchantRecipe convert() { - MerchantRecipe recipe = new MerchantRecipe(getResult(), getAmount()); - recipe.setIngredients(getIngredients()); - return recipe; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisWeather.java b/core/src/main/java/com/volmit/iris/engine/object/IrisWeather.java deleted file mode 100644 index 2f236647a..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisWeather.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import org.bukkit.World; - -@Desc("Represents a weather type") -public enum IrisWeather { - @Desc("Represents when weather is not causing downfall") - NONE, - - @Desc("Represents rain or snow") - DOWNFALL, - - @Desc("Represents rain or snow with thunder") - DOWNFALL_WITH_THUNDER, - - @Desc("Any weather") - ANY; - - public boolean is(World world) { - return switch (this) { - case NONE -> world.isClearWeather(); - case DOWNFALL -> world.hasStorm(); - case DOWNFALL_WITH_THUNDER -> world.hasStorm() && world.isThundering(); - case ANY -> true; - }; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisWorld.java b/core/src/main/java/com/volmit/iris/engine/object/IrisWorld.java deleted file mode 100644 index 19c8d348d..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisWorld.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.util.collection.KList; -import lombok.*; -import lombok.experimental.Accessors; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.generator.WorldInfo; - -import java.io.File; -import java.util.Collection; -import java.util.List; - -@Builder -@Data -@Accessors(chain = true, fluent = true) -public class IrisWorld { - private static final KList NO_PLAYERS = new KList<>(); - private static final KList NO_ENTITIES = new KList<>(); - private String name; - private File worldFolder; - - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - private long seed; - private World.Environment environment; - private World realWorld; - private int minHeight; - private int maxHeight; - - public static IrisWorld fromWorld(World world) { - return bindWorld(IrisWorld.builder().build(), world); - } - - private static IrisWorld bindWorld(IrisWorld iw, World world) { - return iw.name(world.getName()) - .worldFolder(world.getWorldFolder()) - .minHeight(world.getMinHeight()) - .maxHeight(world.getMaxHeight()) - .realWorld(world) - .environment(world.getEnvironment()); - } - - public long getRawWorldSeed() { - return seed; - } - - public void setRawWorldSeed(long seed) { - this.seed = seed; - } - - public boolean tryGetRealWorld() { - if (hasRealWorld()) { - return true; - } - - World w = Bukkit.getWorld(name); - - if (w != null) { - realWorld = w; - return true; - } - - return false; - } - - public boolean hasRealWorld() { - return realWorld != null; - } - - public List getPlayers() { - - if (hasRealWorld()) { - return realWorld().getPlayers(); - } - - return NO_PLAYERS; - } - - public void evacuate() { - if (hasRealWorld()) { - IrisToolbelt.evacuate(realWorld()); - } - } - - public void bind(WorldInfo worldInfo) { - name(worldInfo.getName()) - .worldFolder(new File(Bukkit.getWorldContainer(), worldInfo.getName())) - .minHeight(worldInfo.getMinHeight()) - .maxHeight(worldInfo.getMaxHeight()) - .environment(worldInfo.getEnvironment()); - } - - public void bind(World world) { - if (hasRealWorld()) { - return; - } - - bindWorld(this, world); - } - - public Location spawnLocation() { - if (hasRealWorld()) { - return realWorld().getSpawnLocation(); - } - - Iris.error("This world is not real yet, cannot get spawn location! HEADLESS!"); - return null; - } - - public Collection getEntitiesByClass(Class t) { - if (hasRealWorld()) { - return realWorld().getEntitiesByClass(t); - } - - return (KList) NO_ENTITIES; - } - - public int getHeight() { - return maxHeight - minHeight; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisWorm.java b/core/src/main/java/com/volmit/iris/engine/object/IrisWorm.java deleted file mode 100644 index e83fead07..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisWorm.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.object.annotations.Snippet; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.util.function.Consumer; - -@Snippet("worm") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Generate worms") -@Data -public class IrisWorm { - @Desc("The style used to determine the curvature of this worm's x") - private IrisShapedGeneratorStyle xStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, -2, 2); - - @Desc("The style used to determine the curvature of this worm's y") - private IrisShapedGeneratorStyle yStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, -2, 2); - - @Desc("The style used to determine the curvature of this worm's z") - private IrisShapedGeneratorStyle zStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, -2, 2); - - @Desc("The max block distance this worm can travel from its start. This can have performance implications at ranges over 1,000 blocks but it's not too serious, test.") - private int maxDistance = 128; - - @Desc("The iterations this worm can make") - private int maxIterations = 512; - - @Desc("By default if a worm loops back into itself, it stops at that point and does not continue. This is an optimization, to prevent this turn this option on.") - private boolean allowLoops = false; - - @Desc("The thickness of the worms. Each individual worm has the same thickness while traveling however, each spawned worm will vary in thickness.") - private IrisStyledRange girth = new IrisStyledRange().setMin(3).setMax(5) - .setStyle(new IrisGeneratorStyle(NoiseStyle.PERLIN)); - - public KList generate(RNG rng, IrisData data, MantleWriter writer, IrisRange verticalRange, int x, int y, int z, boolean breakSurface, double distance) { - int itr = maxIterations; - double jx, jy, jz; - double cx = x; - double cy = y; - double cz = z; - IrisPosition start = new IrisPosition(x, y, z); - KList pos = new KList<>(); - KSet check = allowLoops ? null : new KSet<>(); - CNG gx = xStyle.getGenerator().create(rng.nextParallelRNG(14567), data); - CNG gy = yStyle.getGenerator().create(rng.nextParallelRNG(64789), data); - CNG gz = zStyle.getGenerator().create(rng.nextParallelRNG(34790), data); - - while (itr-- > 0) { - IrisPosition current = new IrisPosition(Math.round(cx), Math.round(cy), Math.round(cz)); - pos.add(current); - - if (check != null) { - check.add(current); - } - - jx = gx.fitDouble(xStyle.getMin(), xStyle.getMax(), cx, cy, cz); - jy = gy.fitDouble(yStyle.getMin(), yStyle.getMax(), cx, cy, cz); - jz = gz.fitDouble(zStyle.getMin(), zStyle.getMax(), cx, cy, cz); - cx += jx; - cy += jy; - cz += jz; - IrisPosition next = new IrisPosition(Math.round(cx), Math.round(cy), Math.round(cz)); - - if (!breakSurface && writer.getEngineMantle().getHighest(next.getX(), next.getZ(), true) <= next.getY() + distance) { - break; - } - - if (verticalRange != null && !verticalRange.contains(next.getY())) { - break; - } - - if (!writer.isWithin((int) Math.round(cx), verticalRange != null ? (int) Math.round(cy) : 5, (int) Math.round(cz))) { - break; - } - - if (next.isLongerThan(start, maxDistance)) { - break; - } - - if (check != null && check.contains(next)) { - break; - } - } - - return pos; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/LegacyTileData.java b/core/src/main/java/com/volmit/iris/engine/object/LegacyTileData.java deleted file mode 100644 index 7cbcc094c..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/LegacyTileData.java +++ /dev/null @@ -1,282 +0,0 @@ -package com.volmit.iris.engine.object; - -import com.volmit.iris.core.nms.container.Pair; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.scheduling.J; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import lombok.ToString; -import org.apache.commons.io.function.IOFunction; -import org.bukkit.*; -import org.bukkit.block.*; -import org.bukkit.block.banner.Pattern; -import org.bukkit.block.banner.PatternType; -import org.bukkit.block.data.BlockData; -import org.bukkit.entity.EntityType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -@ToString -@EqualsAndHashCode(callSuper = false) -public class LegacyTileData extends TileData { - private static final Map>> legacy = Map.of( - 0, new Pair<>(SignHandler::fromBukkit, SignHandler::new), - 1, new Pair<>(SpawnerHandler::fromBukkit, SpawnerHandler::new), - 2, new Pair<>(BannerHandler::fromBukkit, BannerHandler::new)); - private static final AtomicCache> SIGNS = new AtomicCache<>(); - private final int id; - private final Handler handler; - - public LegacyTileData(DataInputStream in) throws IOException { - id = in.readShort(); - var factory = legacy.get(id); - if (factory == null) - throw new IOException("Unknown tile type: " + id); - handler = factory.getB().apply(in); - } - - private LegacyTileData(int id, Handler handler) { - this.id = id; - this.handler = handler; - } - - @Nullable - public static LegacyTileData fromBukkit(@NonNull BlockState tileState) { - var type = tileState.getType(); - for (var id : legacy.keySet()) { - var factory = legacy.get(id); - var handler = factory.getA().apply(tileState, type); - if (handler != null) - return new LegacyTileData(id, handler); - } - return null; - } - - @Override - public @NonNull KMap getProperties() { - return new KMap<>(); - } - - @Override - public @NonNull Material getMaterial() { - return handler.getMaterial(); - } - - @Override - public boolean isApplicable(BlockData data) { - return handler.isApplicable(data); - } - - @Override - public void toBukkit(Block block) { - J.s(() -> handler.toBukkit(block)); - } - - @Override - public void toBinary(DataOutputStream out) throws IOException { - out.writeShort(id); - handler.toBinary(out); - } - - @Override - public TileData clone() { - return this; - } - - private interface Handler { - Material getMaterial(); - boolean isApplicable(BlockData data); - void toBinary(DataOutputStream out) throws IOException; - void toBukkit(Block block); - } - - @FunctionalInterface - private interface Builder { - @Nullable Handler apply(@NonNull BlockState blockState, @NonNull Material type); - } - - @ToString - @EqualsAndHashCode - @AllArgsConstructor - private static class SignHandler implements Handler { - private final String line1; - private final String line2; - private final String line3; - private final String line4; - private final DyeColor dyeColor; - - private SignHandler(DataInputStream in) throws IOException { - line1 = in.readUTF(); - line2 = in.readUTF(); - line3 = in.readUTF(); - line4 = in.readUTF(); - dyeColor = DyeColor.values()[in.readByte()]; - } - - @SuppressWarnings("deprecation") - private static SignHandler fromBukkit(BlockState blockState, Material type) { - if (!signsTag().isTagged(type) || !(blockState instanceof Sign sign)) - return null; - return new SignHandler(sign.getLine(0), sign.getLine(1), sign.getLine(2), sign.getLine(3), sign.getColor()); - } - - @Override - public Material getMaterial() { - return Material.OAK_SIGN; - } - - @Override - public boolean isApplicable(BlockData data) { - return signsTag().isTagged(data.getMaterial()); - } - - @Override - public void toBinary(DataOutputStream out) throws IOException { - out.writeUTF(line1); - out.writeUTF(line2); - out.writeUTF(line3); - out.writeUTF(line4); - out.writeByte(dyeColor.ordinal()); - } - - @Override - public void toBukkit(Block block) { - Sign sign = (Sign) block.getState(); - sign.setLine(0, line1); - sign.setLine(1, line2); - sign.setLine(2, line3); - sign.setLine(3, line4); - sign.setColor(dyeColor); - sign.update(); - } - } - @ToString - @EqualsAndHashCode - @AllArgsConstructor - private static class SpawnerHandler implements Handler { - private final EntityType type; - - private SpawnerHandler(DataInputStream in) throws IOException { - type = EntityType.values()[in.readShort()]; - } - - private static SpawnerHandler fromBukkit(BlockState blockState, Material material) { - if (material != Material.SPAWNER || !(blockState instanceof CreatureSpawner spawner)) - return null; - return new SpawnerHandler(spawner.getSpawnedType()); - } - - @Override - public Material getMaterial() { - return Material.SPAWNER; - } - - @Override - public boolean isApplicable(BlockData data) { - return data.getMaterial() == Material.SPAWNER; - } - - @Override - public void toBinary(DataOutputStream out) throws IOException { - out.writeShort(type.ordinal()); - } - - @Override - public void toBukkit(Block block) { - CreatureSpawner spawner = (CreatureSpawner) block.getState(); - spawner.setSpawnedType(type); - spawner.update(); - } - } - @ToString - @EqualsAndHashCode - @AllArgsConstructor - private static class BannerHandler implements Handler { - private final KList patterns; - private final DyeColor baseColor; - - private BannerHandler(DataInputStream in) throws IOException { - baseColor = DyeColor.values()[in.readByte()]; - patterns = new KList<>(); - int listSize = in.readByte(); - for (int i = 0; i < listSize; i++) { - DyeColor color = DyeColor.values()[in.readByte()]; - PatternType pattern = PatternType.values()[in.readByte()]; - patterns.add(new Pattern(color, pattern)); - } - } - - private static BannerHandler fromBukkit(BlockState blockState, Material type) { - if (!Tag.BANNERS.isTagged(type) || !(blockState instanceof Banner banner)) - return null; - return new BannerHandler(new KList<>(banner.getPatterns()), banner.getBaseColor()); - } - - @Override - public Material getMaterial() { - return Material.WHITE_BANNER; - } - - @Override - public boolean isApplicable(BlockData data) { - return Tag.BANNERS.isTagged(data.getMaterial()); - } - - @Override - public void toBinary(DataOutputStream out) throws IOException { - out.writeByte(baseColor.ordinal()); - out.writeByte(patterns.size()); - for (Pattern i : patterns) { - out.writeByte(i.getColor().ordinal()); - out.writeByte(i.getPattern().ordinal()); - } - } - - @Override - public void toBukkit(Block block) { - Banner banner = (Banner) block.getState(); - banner.setBaseColor(baseColor); - banner.setPatterns(patterns); - banner.update(); - } - } - - private static Tag signsTag() { - return SIGNS.aquire(() -> { - var signs = Bukkit.getTag("blocks", NamespacedKey.minecraft("all_signs"), Material.class); - if (signs != null) - return signs; - return new Tag<>() { - @Override - public boolean isTagged(@NotNull Material item) { - return item.getKey().getKey().endsWith("_sign"); - } - - @NotNull - @Override - public Set getValues() { - return StreamSupport.stream(Registry.MATERIAL.spliterator(), false) - .filter(this::isTagged) - .collect(Collectors.toUnmodifiableSet()); - } - - @NotNull - @Override - public NamespacedKey getKey() { - return NamespacedKey.minecraft("all_signs"); - } - }; - }); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/ObjectPlaceMode.java b/core/src/main/java/com/volmit/iris/engine/object/ObjectPlaceMode.java deleted file mode 100644 index b632406e1..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/ObjectPlaceMode.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; - -@Desc("Object Place modes are useful for positioning objects just right. The default value is CENTER_HEIGHT.") -public enum ObjectPlaceMode { - @Desc("The default place mode. This mode picks a center point (where the center of the object will be) and takes the height. That height is used for the whole object.") - - CENTER_HEIGHT, - - @Desc("Samples a lot of points where the object will cover (horizontally) and picks the highest height, that height is then used to place the object. This mode is useful for preventing any part of your object from being buried though it will float off of cliffs.") - - MAX_HEIGHT, - - @Desc("Samples only 4 points where the object will cover (horizontally) and picks the highest height, that height is then used to place the object. This mode is useful for preventing any part of your object from being buried though it will float off of cliffs.\"") - - FAST_MAX_HEIGHT, - - @Desc("Samples a lot of points where the object will cover (horizontally) and picks the lowest height, that height is then used to place the object. This mode is useful for preventing any part of your object from overhanging a cliff though it gets buried a lot") - - MIN_HEIGHT, - - @Desc("Samples only 4 points where the object will cover (horizontally) and picks the lowest height, that height is then used to place the object. This mode is useful for preventing any part of your object from overhanging a cliff though it gets buried a lot") - - FAST_MIN_HEIGHT, - - @Desc("Stilting is MAX_HEIGHT but it repeats the bottom most block of your object until it hits the surface. This is expensive because it has to first sample every height value for each x,z position of your object. Avoid using this unless its structures for performance reasons.") - - STILT, - - @Desc("Just like stilting but very inaccurate. Useful for stilting a lot of objects without too much care on accuracy (you can use the over-stilt value to force stilts under ground further)") - - FAST_STILT, - - @Desc("Stilting is MIN_HEIGHT but it repeats the bottom most block of your object until it hits the surface. This is expensive because it has to first sample every height value for each x,z position of your object. Avoid using this unless its structures for performance reasons.") - - MIN_STILT, - - @Desc("Just like MIN_STILT but very inaccurate. Useful for stilting a lot of objects without too much care on accuracy (you can use the over-stilt value to force stilts under ground further)") - - FAST_MIN_STILT, - - @Desc("Stilting is CENTER_HEIGHT but it repeats the bottom most block of your object until it hits the surface. This is expensive because it has to first sample every height value for each x,z position of your object. Avoid using this unless its structures for performance reasons.") - - CENTER_STILT, - - @Desc("Samples the height of the terrain at every x,z position of your object and pushes it down to the surface. It's pretty much like a melt function over the terrain.") - - PAINT -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/StudioMode.java b/core/src/main/java/com/volmit/iris/engine/object/StudioMode.java deleted file mode 100644 index 40d931308..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/StudioMode.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.engine.platform.BukkitChunkGenerator; -import com.volmit.iris.engine.platform.studio.generators.BiomeBuffetGenerator; - -import java.util.function.Consumer; - -@Desc("Represents a studio mode") -public enum StudioMode { - NORMAL(i -> i.setStudioGenerator(null)), - BIOME_BUFFET_1x1(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 1))), - BIOME_BUFFET_3x3(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 3))), - BIOME_BUFFET_5x5(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 5))), - BIOME_BUFFET_9x9(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 9))), - BIOME_BUFFET_18x18(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 18))), - BIOME_BUFFET_36x36(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 36))), - REGION_BUFFET(i -> i.setStudioGenerator(null)), - OBJECT_BUFFET(i -> i.setStudioGenerator(null)), - - ; - - private final Consumer injector; - - StudioMode(Consumer injector) { - this.injector = injector; - } - - public void inject(BukkitChunkGenerator c) { - injector.accept(c); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/TileData.java b/core/src/main/java/com/volmit/iris/engine/object/TileData.java deleted file mode 100644 index 9c0809545..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/TileData.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.volmit.iris.Iris; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.util.collection.KMap; -import lombok.*; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.TileState; -import org.bukkit.block.data.BlockData; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@SuppressWarnings("ALL") -@Getter -@EqualsAndHashCode -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class TileData implements Cloneable { - private static final Gson gson = new GsonBuilder().disableHtmlEscaping().setLenient().create(); - - @NonNull - private Material material; - @NonNull - private KMap properties; - - public static boolean setTileState(Block block, TileData data) { - if (block.getState() instanceof TileState && data.isApplicable(block.getBlockData())) - return data.toBukkitTry(block); - return false; - } - - public static TileData getTileState(Block block, boolean useLegacy) { - if (!INMS.get().hasTile(block.getType())) - return null; - if (useLegacy) { - var legacy = LegacyTileData.fromBukkit(block.getState()); - if (legacy != null) - return legacy; - } - - return new TileData().fromBukkit(block); - } - - public static TileData read(DataInputStream in) throws IOException { - if (!in.markSupported()) - throw new IOException("Mark not supported"); - in.mark(Integer.MAX_VALUE); - try { - return new TileData( - Material.matchMaterial(in.readUTF()), - gson.fromJson(in.readUTF(), KMap.class)); - } catch (Throwable e) { - in.reset(); - return new LegacyTileData(in); - } finally { - in.mark(0); - } - } - - public boolean isApplicable(BlockData data) { - return material != null && data.getMaterial() == material; - } - - public void toBukkit(Block block) { - if (material == null) throw new IllegalStateException("Material not set"); - if (block.getType() != material) - throw new IllegalStateException("Material mismatch: " + block.getType() + " vs " + material); - INMS.get().deserializeTile(properties, block.getLocation()); - } - - public TileData fromBukkit(Block block) { - if (material != null && block.getType() != material) - throw new IllegalStateException("Material mismatch: " + block.getType() + " vs " + material); - if (material == null) material = block.getType(); - properties = INMS.get().serializeTile(block.getLocation()); - return this; - } - - public boolean toBukkitTry(Block block) { - try { - //noinspection unchecked - toBukkit(block); - return true; - } catch (Throwable e) { - Iris.reportError(e); - } - - return false; - } - - public boolean fromBukkitTry(Block block) { - try { - //noinspection unchecked - fromBukkit(block); - return true; - } catch (Throwable e) { - Iris.reportError(e); - - } - - return false; - } - - public void toBinary(DataOutputStream out) throws IOException { - out.writeUTF(material == null ? "" : material.getKey().toString()); - out.writeUTF(gson.toJson(properties)); - } - - @Override - public TileData clone() { - var clone = new TileData(); - clone.material = material; - clone.properties = properties.copy(); //TODO make a deep copy - return clone; - } - - @Override - public String toString() { - return material.getKey() + gson.toJson(properties); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListBiomeDownfallType.java b/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListBiomeDownfallType.java deleted file mode 100644 index 9b19e51e3..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListBiomeDownfallType.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object.annotations; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target({PARAMETER, TYPE, FIELD}) -public @interface RegistryListBiomeDownfallType { - -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListEnchantment.java b/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListEnchantment.java deleted file mode 100644 index 20bb033d3..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListEnchantment.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.volmit.iris.engine.object.annotations; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target({PARAMETER, TYPE, FIELD}) -public @interface RegistryListEnchantment { -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListFunction.java b/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListFunction.java deleted file mode 100644 index 04d9fd5b2..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/RegistryListFunction.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.volmit.iris.engine.object.annotations; - -import com.volmit.iris.engine.framework.ListFunction; -import com.volmit.iris.util.collection.KList; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target({PARAMETER, TYPE, FIELD}) -public @interface RegistryListFunction { - Class>> value(); -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/ComponentFlagFunction.java b/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/ComponentFlagFunction.java deleted file mode 100644 index fb8738ffe..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/ComponentFlagFunction.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.volmit.iris.engine.object.annotations.functions; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.ListFunction; -import com.volmit.iris.engine.mantle.ComponentFlag; -import com.volmit.iris.engine.mantle.MantleComponent; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.mantle.flag.MantleFlag; - -import java.util.Objects; - -public class ComponentFlagFunction implements ListFunction> { - @Override - public String key() { - return "component-flag"; - } - - @Override - public String fancyName() { - return "Component Flag"; - } - - @Override - public KList apply(IrisData data) { - var engine = data.getEngine(); - if (engine != null) return engine.getMantle().getComponentFlags().toStringList(); - return Iris.getClasses("com.volmit.iris.engine.mantle.components", ComponentFlag.class) - .stream() - .filter(MantleComponent.class::isAssignableFrom) - .map(c -> c.getDeclaredAnnotation(ComponentFlag.class)) - .filter(Objects::nonNull) - .map(ComponentFlag::value) - .map(MantleFlag::toString) - .collect(KList.collector()); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/LootTableKeyFunction.java b/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/LootTableKeyFunction.java deleted file mode 100644 index d77f49583..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/LootTableKeyFunction.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.volmit.iris.engine.object.annotations.functions; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.ListFunction; -import com.volmit.iris.util.collection.KList; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; -import org.bukkit.loot.LootTable; -import org.bukkit.loot.LootTables; - -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public class LootTableKeyFunction implements ListFunction> { - @Override - public String key() { - return "loot-table-key"; - } - - @Override - public String fancyName() { - return "LootTable Key"; - } - - @Override - public KList apply(IrisData data) { - return StreamSupport.stream(Registry.LOOT_TABLES.spliterator(), false) - .map(LootTables::getLootTable) - .map(LootTable::getKey) - .map(NamespacedKey::toString) - .collect(Collectors.toCollection(KList::new)); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/ResourceLoadersFunction.java b/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/ResourceLoadersFunction.java deleted file mode 100644 index 943a96113..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/ResourceLoadersFunction.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.volmit.iris.engine.object.annotations.functions; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.ResourceLoader; -import com.volmit.iris.engine.framework.ListFunction; -import com.volmit.iris.util.collection.KList; - -public class ResourceLoadersFunction implements ListFunction> { - @Override - public String key() { - return "resource-loader"; - } - - @Override - public String fancyName() { - return "Resource Loader"; - } - - @Override - public KList apply(IrisData data) { - return data.getLoaders() - .values() - .stream() - .filter(rl -> ResourceLoader.class.equals(rl.getClass())) - .map(ResourceLoader::getFolderName) - .collect(KList.collector()); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/StructureKeyFunction.java b/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/StructureKeyFunction.java deleted file mode 100644 index 492a5706a..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/StructureKeyFunction.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.volmit.iris.engine.object.annotations.functions; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.engine.framework.ListFunction; -import com.volmit.iris.util.collection.KList; - -public class StructureKeyFunction implements ListFunction> { - @Override - public String key() { - return "structure-key"; - } - - @Override - public String fancyName() { - return "Structure Key"; - } - - @Override - public KList apply(IrisData irisData) { - return INMS.get().getStructureKeys().removeWhere(t -> t.startsWith("#")); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/StructureKeyOrTagFunction.java b/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/StructureKeyOrTagFunction.java deleted file mode 100644 index 0d4db607a..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/annotations/functions/StructureKeyOrTagFunction.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.volmit.iris.engine.object.annotations.functions; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.engine.framework.ListFunction; -import com.volmit.iris.util.collection.KList; - -public class StructureKeyOrTagFunction implements ListFunction> { - @Override - public String key() { - return "structure-key-or-tag"; - } - - @Override - public String fancyName() { - return "Structure Key or Tag"; - } - - @Override - public KList apply(IrisData irisData) { - return INMS.get().getStructureKeys(); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterObject.java b/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterObject.java deleted file mode 100644 index 6484483e1..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterObject.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.volmit.iris.engine.object.matter; - -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.engine.object.IrisObject; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.matter.IrisMatter; -import com.volmit.iris.util.matter.Matter; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.io.File; -import java.io.IOException; - -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisMatterObject extends IrisRegistrant { - private final Matter matter; - - public IrisMatterObject() { - this(1, 1, 1); - } - - public IrisMatterObject(int w, int h, int d) { - this(new IrisMatter(w, h, d)); - } - - public IrisMatterObject(Matter matter) { - this.matter = matter; - } - - public static IrisMatterObject from(IrisObject object) { - return new IrisMatterObject(Matter.from(object)); - } - - public static IrisMatterObject from(File j) throws IOException, ClassNotFoundException { - return new IrisMatterObject(Matter.read(j)); - } - - @Override - public String getFolderName() { - return "matter"; - } - - @Override - public String getTypeName() { - return "Matter"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterPlacement.java b/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterPlacement.java deleted file mode 100644 index 51299f7ac..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterPlacement.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.object.matter; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.IrisEngine; -import com.volmit.iris.engine.object.IRare; -import com.volmit.iris.engine.object.IrisStyledRange; -import com.volmit.iris.engine.object.annotations.*; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.mantle.Mantle; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.matter.MatterSlice; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("matter-placer") -@EqualsAndHashCode() -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Represents an iris object placer. It places matter objects.") -@Data -public class IrisMatterPlacement implements IRare { - @RegistryListResource(IrisMatterObject.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("List of objects to place") - private KList place = new KList<>(); - - @MinNumber(0) - @Desc("The rarity of this object placing") - private int rarity = 0; - - @MinNumber(0) - @Desc("The styled density of this object") - private IrisStyledRange densityRange; - - @Desc("The absolute density for this object") - private double density = 1; - - @Desc("Translate this matter object before placement") - private IrisMatterTranslate translate; - - @Desc("Place this object on the surface height, bedrock or the sky, then use translate if need be.") - private IrisMatterPlacementLocation location = IrisMatterPlacementLocation.SURFACE; - - public void place(IrisEngine engine, IrisData data, RNG rng, int ax, int az) { - IrisMatterObject object = data.getMatterLoader().load(place.getRandom(rng)); - int x = ax; - int z = az; - int yoff = 0; - - if (translate != null) { - x += translate.xOffset(data, rng, x, z); - yoff += translate.yOffset(data, rng, x, z); - z += translate.zOffset(data, rng, x, z); - } - - int y = yoff + location.at(engine, x, z); - Mantle mantle = engine.getMantle().getMantle(); - - int xx = x; - int yy = y; - int zz = z; - - for (MatterSlice slice : object.getMatter().getSliceMap().values()) { - slice.iterate((mx, my, mz, v) -> { - mantle.set(xx + mx, yy + my, zz + mz, v); - }); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterPlacementLocation.java b/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterPlacementLocation.java deleted file mode 100644 index cf187a1fb..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterPlacementLocation.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.volmit.iris.engine.object.matter; - -import com.volmit.iris.engine.IrisEngine; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.util.function.Function3; - -@Desc("WHERE THINGS PLACE") -public enum IrisMatterPlacementLocation { - SURFACE((e, x, z) -> e.getHeight(x, z, true)), - SURFACE_ON_FLUID((e, x, z) -> e.getHeight(x, z, false)), - BEDROCK((e, x, z) -> 0), - SKY((e, x, z) -> e.getHeight()); - - private final Function3 computer; - - IrisMatterPlacementLocation(Function3 computer) { - this.computer = computer; - } - - public int at(IrisEngine engine, int x, int z) { - return computer.apply(engine, x, z); - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterTranslate.java b/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterTranslate.java deleted file mode 100644 index 65cf2dd3c..000000000 --- a/core/src/main/java/com/volmit/iris/engine/object/matter/IrisMatterTranslate.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.volmit.iris.engine.object.matter; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.object.IrisStyledRange; -import com.volmit.iris.engine.object.annotations.Desc; -import com.volmit.iris.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode() -@Accessors(chain = true) -@Desc("Represents a matter translator") -public class IrisMatterTranslate { - @Desc("For varied coordinate shifts use ranges not the literal coordinate") - private IrisStyledRange rangeX = null; - @Desc("For varied coordinate shifts use ranges not the literal coordinate") - private IrisStyledRange rangeY = null; - @Desc("For varied coordinate shifts use ranges not the literal coordinate") - private IrisStyledRange rangeZ = null; - @Desc("Define an absolute shift instead of varied.") - private int x = 0; - @Desc("Define an absolute shift instead of varied.") - private int y = 0; - @Desc("Define an absolute shift instead of varied.") - private int z = 0; - - public int xOffset(IrisData data, RNG rng, int rx, int rz) { - if (rangeX != null) { - return (int) Math.round(rangeX.get(rng, rx, rz, data)); - } - - return x; - } - - public int yOffset(IrisData data, RNG rng, int rx, int rz) { - if (rangeY != null) { - return (int) Math.round(rangeY.get(rng, rx, rz, data)); - } - - return y; - } - - public int zOffset(IrisData data, RNG rng, int rx, int rz) { - if (rangeZ != null) { - return (int) Math.round(rangeZ.get(rng, rx, rz, data)); - } - - return z; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java deleted file mode 100644 index 54bfefd5a..000000000 --- a/core/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.platform; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.IrisWorlds; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.service.StudioSVC; -import com.volmit.iris.engine.IrisEngine; -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.engine.data.chunk.TerrainChunk; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineTarget; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.engine.object.IrisWorld; -import com.volmit.iris.engine.object.StudioMode; -import com.volmit.iris.engine.platform.studio.StudioGenerator; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.data.IrisBiomeStorage; -import com.volmit.iris.util.hunk.view.BiomeGridHunkHolder; -import com.volmit.iris.util.hunk.view.ChunkDataHunkHolder; -import com.volmit.iris.util.io.ReactiveFolder; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.Looper; -import io.papermc.lib.PaperLib; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Setter; -import org.bukkit.*; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.world.WorldInitEvent; -import org.bukkit.generator.BiomeProvider; -import org.bukkit.generator.BlockPopulator; -import org.bukkit.generator.ChunkGenerator; -import org.bukkit.generator.WorldInfo; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.util.List; -import java.util.Random; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.ReentrantLock; - -@EqualsAndHashCode(callSuper = true) -@Data -public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChunkGenerator, Listener { - private static final int LOAD_LOCKS = Runtime.getRuntime().availableProcessors() * 4; - private final Semaphore loadLock; - private final IrisWorld world; - private final File dataLocation; - private final String dimensionKey; - private final ReactiveFolder folder; - private final ReentrantLock lock = new ReentrantLock(); - private final KList populators; - private final ChronoLatch hotloadChecker; - private final AtomicBoolean setup; - private final boolean studio; - private final AtomicInteger a = new AtomicInteger(0); - private final CompletableFuture spawnChunks = new CompletableFuture<>(); - private final AtomicCache targetCache = new AtomicCache<>(); - private volatile Engine engine; - private volatile Looper hotloader; - private volatile StudioMode lastMode; - private volatile DummyBiomeProvider dummyBiomeProvider; - @Setter - private volatile StudioGenerator studioGenerator; - - public BukkitChunkGenerator(IrisWorld world, boolean studio, File dataLocation, String dimensionKey) { - setup = new AtomicBoolean(false); - studioGenerator = null; - dummyBiomeProvider = new DummyBiomeProvider(); - populators = new KList<>(); - loadLock = new Semaphore(LOAD_LOCKS); - this.world = world; - this.hotloadChecker = new ChronoLatch(1000, false); - this.studio = studio; - this.dataLocation = dataLocation; - this.dimensionKey = dimensionKey; - this.folder = new ReactiveFolder(dataLocation, (_a, _b, _c) -> hotload()); - Bukkit.getServer().getPluginManager().registerEvents(this, Iris.instance); - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onWorldInit(WorldInitEvent event) { - if (!world.name().equals(event.getWorld().getName())) return; - Iris.instance.unregisterListener(this); - world.setRawWorldSeed(event.getWorld().getSeed()); - if (initialize(event.getWorld())) return; - - Iris.warn("Failed to get Engine for " + event.getWorld().getName() + " re-trying..."); - J.s(() -> { - if (!initialize(event.getWorld())) { - Iris.error("Failed to get Engine for " + event.getWorld().getName() + "!"); - } - }, 10); - } - - private boolean initialize(World world) { - Engine engine = getEngine(world); - if (engine == null) return false; - try { - INMS.get().inject(world.getSeed(), engine, world); - Iris.info("Injected Iris Biome Source into " + world.getName()); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Failed to inject biome source into " + world.getName()); - e.printStackTrace(); - } - spawnChunks.complete(INMS.get().getSpawnChunkCount(world)); - Iris.instance.unregisterListener(this); - IrisWorlds.get().put(world.getName(), dimensionKey); - return true; - } - - @Nullable - @Override - public Location getFixedSpawnLocation(@NotNull World world, @NotNull Random random) { - Location location = new Location(world, 0, 64, 0); - PaperLib.getChunkAtAsync(location) - .thenAccept(c -> { - World w = c.getWorld(); - if (!w.getSpawnLocation().equals(location)) - return; - w.setSpawnLocation(location.add(0, w.getHighestBlockYAt(location) - 64, 0)); - }); - return location; - } - - private void setupEngine() { - lastMode = StudioMode.NORMAL; - engine = new IrisEngine(getTarget(), studio); - populators.clear(); - targetCache.reset(); - } - - @NotNull - @Override - public EngineTarget getTarget() { - if (engine != null) return engine.getTarget(); - - return targetCache.aquire(() -> { - IrisData data = IrisData.get(dataLocation); - data.dump(); - data.clearLists(); - IrisDimension dimension = data.getDimensionLoader().load(dimensionKey); - - if (dimension == null) { - Iris.error("Oh No! There's no pack in " + data.getDataFolder().getPath() + " or... there's no dimension for the key " + dimensionKey); - IrisDimension test = IrisData.loadAnyDimension(dimensionKey, null); - - if (test != null) { - Iris.warn("Looks like " + dimensionKey + " exists in " + test.getLoadFile().getPath() + " "); - test = Iris.service(StudioSVC.class).installInto(Iris.getSender(), dimensionKey, dataLocation); - Iris.warn("Attempted to install into " + data.getDataFolder().getPath()); - - if (test != null) { - Iris.success("Woo! Patched the Engine!"); - dimension = test; - } else { - Iris.error("Failed to patch dimension!"); - throw new RuntimeException("Missing Dimension: " + dimensionKey); - } - } else { - Iris.error("Nope, you don't have an installation containing " + dimensionKey + " try downloading it?"); - throw new RuntimeException("Missing Dimension: " + dimensionKey); - } - } - - return new EngineTarget(world, dimension, data); - }); - } - - @Override - public void injectChunkReplacement(World world, int x, int z, Executor syncExecutor) { - try { - loadLock.acquire(); - IrisBiomeStorage st = new IrisBiomeStorage(); - TerrainChunk tc = TerrainChunk.createUnsafe(world, st); - this.world.bind(world); - getEngine().generate(x << 4, z << 4, tc, IrisSettings.get().getGenerator().useMulticore); - - Chunk c = PaperLib.getChunkAtAsync(world, x, z) - .thenApply(d -> { - Iris.tickets.addTicket(d); - - for (Entity ee : d.getEntities()) { - if (ee instanceof Player) { - continue; - } - - ee.remove(); - } - - return d; - }).get(); - - - KList> futures = new KList<>(1 + getEngine().getHeight() >> 4); - for (int i = getEngine().getHeight() >> 4; i >= 0; i--) { - int finalI = i << 4; - futures.add(CompletableFuture.runAsync(() -> { - for (int xx = 0; xx < 16; xx++) { - for (int yy = 0; yy < 16; yy++) { - for (int zz = 0; zz < 16; zz++) { - if (yy + finalI >= engine.getHeight() || yy + finalI < 0) { - continue; - } - int y = yy + finalI + world.getMinHeight(); - c.getBlock(xx, y, zz).setBlockData(tc.getBlockData(xx, y, zz), false); - } - } - } - }, syncExecutor)); - } - futures.add(CompletableFuture.runAsync(() -> INMS.get().placeStructures(c), syncExecutor)); - - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) - .thenRunAsync(() -> { - Iris.tickets.removeTicket(c); - engine.getWorldManager().onChunkLoad(c, true); - }, syncExecutor) - .get(); - Iris.debug("Regenerated " + x + " " + z); - - loadLock.release(); - } catch (Throwable e) { - loadLock.release(); - Iris.error("======================================"); - e.printStackTrace(); - Iris.reportErrorChunk(x, z, e, "CHUNK"); - Iris.error("======================================"); - - ChunkData d = Bukkit.createChunkData(world); - - for (int i = 0; i < 16; i++) { - for (int j = 0; j < 16; j++) { - d.setBlock(i, 0, j, Material.RED_GLAZED_TERRACOTTA.createBlockData()); - } - } - } - } - - private Engine getEngine(WorldInfo world) { - if (setup.get()) { - return getEngine(); - } - - lock.lock(); - - try { - if (setup.get()) { - return getEngine(); - } - - - getWorld().setRawWorldSeed(world.getSeed()); - setupEngine(); - setup.set(true); - this.hotloader = studio ? new Looper() { - @Override - protected long loop() { - if (hotloadChecker.flip()) { - folder.check(); - } - - return 250; - } - } : null; - - if (studio) { - hotloader.setPriority(Thread.MIN_PRIORITY); - hotloader.start(); - hotloader.setName(getTarget().getWorld().name() + " Hotloader"); - } - - return engine; - } finally { - lock.unlock(); - } - } - - @Override - public void close() { - withExclusiveControl(() -> { - if (isStudio()) { - hotloader.interrupt(); - } - - final Engine engine = getEngine(); - if (engine != null && !engine.isClosed()) - engine.close(); - folder.clear(); - populators.clear(); - - }); - } - - @Override - public boolean isStudio() { - return studio; - } - - @Override - public void hotload() { - if (!isStudio()) { - return; - } - - withExclusiveControl(() -> getEngine().hotload()); - } - - public void withExclusiveControl(Runnable r) { - J.a(() -> { - try { - loadLock.acquire(LOAD_LOCKS); - r.run(); - loadLock.release(LOAD_LOCKS); - } catch (Throwable e) { - Iris.reportError(e); - } - }); - } - - @Override - public void touch(World world) { - getEngine(world); - } - - @Override - public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) { - try { - Engine engine = getEngine(world); - computeStudioGenerator(); - TerrainChunk tc = TerrainChunk.create(d, new IrisBiomeStorage()); - this.world.bind(world); - if (studioGenerator != null) { - studioGenerator.generateChunk(engine, tc, x, z); - } else { - ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(tc); - BiomeGridHunkHolder biomes = new BiomeGridHunkHolder(tc, tc.getMinHeight(), tc.getMaxHeight()); - engine.generate(x << 4, z << 4, blocks, biomes, IrisSettings.get().getGenerator().useMulticore); - blocks.apply(); - biomes.apply(); - } - - Iris.debug("Generated " + x + " " + z); - } catch (Throwable e) { - Iris.error("======================================"); - e.printStackTrace(); - Iris.reportErrorChunk(x, z, e, "CHUNK"); - Iris.error("======================================"); - - for (int i = 0; i < 16; i++) { - for (int j = 0; j < 16; j++) { - d.setBlock(i, 0, j, Material.RED_GLAZED_TERRACOTTA.createBlockData()); - } - } - } - } - - @Override - public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) { - return 4; - } - - private void computeStudioGenerator() { - if (!getEngine().getDimension().getStudioMode().equals(lastMode)) { - lastMode = getEngine().getDimension().getStudioMode(); - getEngine().getDimension().getStudioMode().inject(this); - } - } - - @NotNull - @Override - public List getDefaultPopulators(@NotNull World world) { - return populators; - } - - @Override - public boolean isParallelCapable() { - return true; - } - - @Override - public boolean shouldGenerateCaves() { - return false; - } - - @Override - public boolean shouldGenerateDecorations() { - return false; - } - - @Override - public boolean shouldGenerateMobs() { - return false; - } - - @Override - public boolean shouldGenerateStructures() { - return false; - } - - @Override - public boolean shouldGenerateNoise() { - return false; - } - - @Override - public boolean shouldGenerateSurface() { - return false; - } - - @Override - public boolean shouldGenerateBedrock() { - return false; - } - - @Nullable - @Override - public BiomeProvider getDefaultBiomeProvider(@NotNull WorldInfo worldInfo) { - return dummyBiomeProvider; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/platform/DummyBiomeGrid.java b/core/src/main/java/com/volmit/iris/engine/platform/DummyBiomeGrid.java deleted file mode 100644 index 0d9db538f..000000000 --- a/core/src/main/java/com/volmit/iris/engine/platform/DummyBiomeGrid.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.volmit.iris.engine.platform; - -import org.bukkit.block.Biome; -import org.bukkit.generator.ChunkGenerator; -import org.jetbrains.annotations.NotNull; - -public class DummyBiomeGrid implements ChunkGenerator.BiomeGrid { - @NotNull - @Override - public Biome getBiome(int x, int z) { - return null; - } - - @NotNull - @Override - public Biome getBiome(int x, int y, int z) { - return null; - } - - @Override - public void setBiome(int x, int z, @NotNull Biome bio) { - - } - - @Override - public void setBiome(int x, int y, int z, @NotNull Biome bio) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/platform/DummyBiomeProvider.java b/core/src/main/java/com/volmit/iris/engine/platform/DummyBiomeProvider.java deleted file mode 100644 index 551d95583..000000000 --- a/core/src/main/java/com/volmit/iris/engine/platform/DummyBiomeProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.volmit.iris.engine.platform; - -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.util.collection.KList; -import org.bukkit.block.Biome; -import org.bukkit.generator.BiomeProvider; -import org.bukkit.generator.WorldInfo; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public class DummyBiomeProvider extends BiomeProvider { - private final List ALL = INMS.get().getBiomes(); - - @NotNull - @Override - public Biome getBiome(@NotNull WorldInfo worldInfo, int x, int y, int z) { - return Biome.PLAINS; - } - - @NotNull - @Override - public List getBiomes(@NotNull WorldInfo worldInfo) { - return ALL; - } -} diff --git a/core/src/main/java/com/volmit/iris/engine/platform/PlatformChunkGenerator.java b/core/src/main/java/com/volmit/iris/engine/platform/PlatformChunkGenerator.java deleted file mode 100644 index 93066fb62..000000000 --- a/core/src/main/java/com/volmit/iris/engine/platform/PlatformChunkGenerator.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.platform; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.EngineTarget; -import com.volmit.iris.engine.framework.Hotloadable; -import com.volmit.iris.util.data.DataProvider; -import org.bukkit.World; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; - -public interface PlatformChunkGenerator extends Hotloadable, DataProvider { - @Nullable - Engine getEngine(); - - @Override - default IrisData getData() { - return getTarget().getData(); - } - - @NotNull - EngineTarget getTarget(); - - void injectChunkReplacement(World world, int x, int z, Executor syncExecutor); - - void close(); - - boolean isStudio(); - - void touch(World world); - - CompletableFuture getSpawnChunks(); -} diff --git a/core/src/main/java/com/volmit/iris/engine/platform/studio/StudioGenerator.java b/core/src/main/java/com/volmit/iris/engine/platform/studio/StudioGenerator.java deleted file mode 100644 index 51d37d0a2..000000000 --- a/core/src/main/java/com/volmit/iris/engine/platform/studio/StudioGenerator.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.platform.studio; - -import com.volmit.iris.engine.data.chunk.TerrainChunk; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.WrongEngineBroException; - -public interface StudioGenerator { - void generateChunk(Engine engine, TerrainChunk tc, int x, int z) throws WrongEngineBroException; -} diff --git a/core/src/main/java/com/volmit/iris/engine/platform/studio/generators/BiomeBuffetGenerator.java b/core/src/main/java/com/volmit/iris/engine/platform/studio/generators/BiomeBuffetGenerator.java deleted file mode 100644 index 4e2c661d5..000000000 --- a/core/src/main/java/com/volmit/iris/engine/platform/studio/generators/BiomeBuffetGenerator.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.engine.platform.studio.generators; - -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.data.chunk.TerrainChunk; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.WrongEngineBroException; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.engine.platform.studio.EnginedStudioGenerator; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; - -import java.util.Objects; - -public class BiomeBuffetGenerator extends EnginedStudioGenerator { - private static final BlockData FLOOR = Material.BARRIER.createBlockData(); - private final IrisBiome[] biomes; - private final int width; - private final int biomeSize; - - public BiomeBuffetGenerator(Engine engine, int biomeSize) { - super(engine); - this.biomeSize = biomeSize; - biomes = engine.getDimension().getAllBiomes(engine).toArray(new IrisBiome[0]); - width = Math.max((int) Math.sqrt(biomes.length), 1); - } - - @Override - public void generateChunk(Engine engine, TerrainChunk tc, int x, int z) throws WrongEngineBroException { - int id = Cache.to1D(x / biomeSize, 0, z / biomeSize, width, 1); - - if (id >= 0 && id < biomes.length) { - IrisBiome biome = biomes[id]; - String foc = engine.getDimension().getFocus(); - - if (!Objects.equals(foc, biome.getLoadKey())) { - engine.getDimension().setFocus(biome.getLoadKey()); - engine.hotloadComplex(); - } - - engine.generate(x << 4, z << 4, tc, true); - } else { - tc.setRegion(0, 0, 0, 16, 1, 16, FLOOR); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/agent/Agent.java b/core/src/main/java/com/volmit/iris/util/agent/Agent.java deleted file mode 100644 index 5f823fc30..000000000 --- a/core/src/main/java/com/volmit/iris/util/agent/Agent.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.volmit.iris.util.agent; - -import com.volmit.iris.Iris; -import net.bytebuddy.agent.ByteBuddyAgent; -import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; - -import java.io.File; -import java.lang.instrument.Instrumentation; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; - -public class Agent { - private static final String NAME = "com.volmit.iris.util.agent.Installer"; - public static final File AGENT_JAR = new File(Iris.instance.getDataFolder(), "agent.jar"); - - public static ClassReloadingStrategy installed() { - return ClassReloadingStrategy.of(getInstrumentation()); - } - - public static Instrumentation getInstrumentation() { - Instrumentation instrumentation = doGetInstrumentation(); - if (instrumentation == null) throw new IllegalStateException("The agent is not initialized or unavailable"); - return instrumentation; - } - - public static boolean install() { - if (doGetInstrumentation() != null) - return true; - try { - Files.copy(Iris.instance.getResource("agent.jar"), AGENT_JAR.toPath(), StandardCopyOption.REPLACE_EXISTING); - Iris.info("Installing Java Agent..."); - ByteBuddyAgent.attach(AGENT_JAR, ByteBuddyAgent.ProcessProvider.ForCurrentVm.INSTANCE); - } catch (Throwable e) { - e.printStackTrace(); - } - return doGetInstrumentation() != null; - } - - private static Instrumentation doGetInstrumentation() { - try { - return (Instrumentation) Class.forName(NAME, true, ClassLoader.getSystemClassLoader()).getMethod("getInstrumentation").invoke(null); - } catch (Exception ex) { - return null; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/api/APIAwareBlock.java b/core/src/main/java/com/volmit/iris/util/api/APIAwareBlock.java deleted file mode 100644 index 4e0881db5..000000000 --- a/core/src/main/java/com/volmit/iris/util/api/APIAwareBlock.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.api; - -import org.bukkit.block.data.BlockData; - -@FunctionalInterface -public interface APIAwareBlock { - void onPlaced(BlockData block, String namespace, String key, int x, int y, int z); -} diff --git a/core/src/main/java/com/volmit/iris/util/api/APIWorldBlock.java b/core/src/main/java/com/volmit/iris/util/api/APIWorldBlock.java deleted file mode 100644 index 8eec1385a..000000000 --- a/core/src/main/java/com/volmit/iris/util/api/APIWorldBlock.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.api; - -import org.bukkit.block.Block; - -@FunctionalInterface -public interface APIWorldBlock { - void onWorldPlaced(Block block, String namespace, String key); -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/atomics/AtomicAverage.java b/core/src/main/java/com/volmit/iris/util/atomics/AtomicAverage.java deleted file mode 100644 index f3574db1d..000000000 --- a/core/src/main/java/com/volmit/iris/util/atomics/AtomicAverage.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.atomics; - -import com.google.common.util.concurrent.AtomicDoubleArray; -import com.volmit.iris.Iris; -import com.volmit.iris.util.data.DoubleArrayUtils; - -/** - * Provides an incredibly fast averaging object. It swaps values from a sum - * using an array. Averages do not use any form of looping. An average of 10,000 - * entries is the same speed as an average with 5 entries. - * - * @author cyberpwn - */ -public class AtomicAverage { - protected final AtomicDoubleArray values; - protected transient int cursor; - private transient double average; - private transient double lastSum; - private transient boolean dirty; - private transient boolean brandNew; - - /** - * Create an average holder - * - * @param size the size of entries to keep - */ - public AtomicAverage(int size) { - values = new AtomicDoubleArray(size); - DoubleArrayUtils.fill(values, 0); - brandNew = true; - average = 0; - cursor = 0; - lastSum = 0; - dirty = false; - } - - /** - * Put a value into the average (rolls over if full) - * - * @param i the value - */ - public synchronized void put(double i) { - - try { - dirty = true; - - if (brandNew) { - DoubleArrayUtils.fill(values, i); - lastSum = size() * i; - brandNew = false; - return; - } - - double current = values.get(cursor); - lastSum = (lastSum - current) + i; - values.set(cursor, i); - cursor = cursor + 1 < size() ? cursor + 1 : 0; - } catch (Throwable e) { - Iris.reportError(e); - - } - } - - /** - * Get the current average - * - * @return the average - */ - public double getAverage() { - if (dirty) { - calculateAverage(); - return getAverage(); - } - - return average; - } - - private void calculateAverage() { - average = lastSum / (double) size(); - dirty = false; - } - - public int size() { - return values.length(); - } - - public boolean isDirty() { - return dirty; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/atomics/AtomicRollingSequence.java b/core/src/main/java/com/volmit/iris/util/atomics/AtomicRollingSequence.java deleted file mode 100644 index 821f52eaf..000000000 --- a/core/src/main/java/com/volmit/iris/util/atomics/AtomicRollingSequence.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.atomics; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.M; - -public class AtomicRollingSequence extends AtomicAverage { - private double median; - private double max; - private double min; - private boolean dirtyMedian; - private int dirtyExtremes; - private boolean precision; - - public AtomicRollingSequence(int size) { - super(size); - median = 0; - min = 0; - max = 0; - setPrecision(false); - } - - public double addLast(int amt) { - double f = 0; - - for (int i = 0; i < Math.min(values.length(), amt); i++) { - f += values.get(i); - } - - return f; - } - - public boolean isPrecision() { - return precision; - } - - public void setPrecision(boolean p) { - this.precision = p; - } - - public double getMin() { - if (dirtyExtremes > (isPrecision() ? 0 : values.length())) { - resetExtremes(); - } - - return min; - } - - public double getMax() { - if (dirtyExtremes > (isPrecision() ? 0 : values.length())) { - resetExtremes(); - } - - return max; - } - - public double getMedian() { - if (dirtyMedian) { - recalculateMedian(); - } - - return median; - } - - private void recalculateMedian() { - median = new KList().forceAdd(values).sort().middleValue(); - dirtyMedian = false; - } - - public void resetExtremes() { - max = Integer.MIN_VALUE; - min = Integer.MAX_VALUE; - - for (int i = 0; i < values.length(); i++) { - double v = values.get(i); - max = M.max(max, v); - min = M.min(min, v); - } - - dirtyExtremes = 0; - } - - public void put(double i) { - super.put(i); - dirtyMedian = true; - dirtyExtremes++; - max = M.max(max, i); - min = M.min(min, i); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/board/Board.java b/core/src/main/java/com/volmit/iris/util/board/Board.java deleted file mode 100644 index eecbac731..000000000 --- a/core/src/main/java/com/volmit/iris/util/board/Board.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.board; - -import com.volmit.iris.util.format.C; -import lombok.NonNull; -import lombok.Setter; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.scoreboard.*; - -import java.util.Collections; -import java.util.List; -import java.util.function.UnaryOperator; -import java.util.stream.IntStream; - -/** - * @author Missionary (missionarymc@gmail.com) - * @since 3/23/2018 - */ -public class Board { - - private static final String[] CACHED_ENTRIES = new String[C.values().length]; - - private static final UnaryOperator APPLY_COLOR_TRANSLATION = s -> C.translateAlternateColorCodes('&', s); - - static { - IntStream.range(0, 15).forEach(i -> CACHED_ENTRIES[i] = C.values()[i].toString() + C.RESET); - } - - private final Player player; - private final Objective objective; - @Setter - private BoardSettings boardSettings; - private boolean ready; - - public Board(@NonNull final Player player, final BoardSettings boardSettings) { - this.player = player; - this.boardSettings = boardSettings; - var obj = getScoreboard().getObjective("board"); - this.objective = obj == null ? this.getScoreboard().registerNewObjective("board", Criteria.DUMMY, "Iris") : obj; - this.objective.setDisplaySlot(DisplaySlot.SIDEBAR); - Team team = getScoreboard().getTeam("board"); - team = team == null ? getScoreboard().registerNewTeam("board") : team; - team.setAllowFriendlyFire(true); - team.setCanSeeFriendlyInvisibles(false); - team.setPrefix(""); - team.setSuffix(""); - this.ready = true; - } - - public Scoreboard getScoreboard() { - return (player != null) ? player.getScoreboard() : null; - } - - public void remove() { - this.resetScoreboard(); - } - - public void update() { - // Checking if we are ready to start updating the Scoreboard. - if (!ready) { - return; - } - - // Making sure the player is connected. - if (!player.isOnline()) { - remove(); - return; - } - - // Making sure the Scoreboard Provider is set. - if (boardSettings == null) { - return; - } - - // Getting their Scoreboard display from the Scoreboard Provider. - final List entries = boardSettings.getBoardProvider().getLines(player); - entries.replaceAll(APPLY_COLOR_TRANSLATION); - - if (boardSettings.getScoreDirection() == ScoreDirection.UP) { - Collections.reverse(entries); - } - - // Setting the Scoreboard title - String title = boardSettings.getBoardProvider().getTitle(player); - if (title.length() > 32) { - Bukkit.getLogger().warning("The title " + title + " is over 32 characters in length, substringing to prevent errors."); - title = title.substring(0, 32); - } - objective.setDisplayName(C.translateAlternateColorCodes('&', title)); - - // Clearing previous Scoreboard values if entry sizes don't match. - if (this.getScoreboard().getEntries().size() != entries.size()) - this.getScoreboard().getEntries().forEach(this::removeEntry); - - // Setting Scoreboard lines. - for (int i = 0; i < entries.size(); i++) { - String str = entries.get(i); - BoardEntry entry = BoardEntry.translateToEntry(str); - Team team = getScoreboard().getTeam(CACHED_ENTRIES[i]); - - if (team == null) { - team = this.getScoreboard().registerNewTeam(CACHED_ENTRIES[i]); - team.addEntry(team.getName()); - } - - team.setPrefix(entry.getPrefix()); - team.setSuffix(entry.getSuffix()); - - switch (boardSettings.getScoreDirection()) { - case UP -> objective.getScore(team.getName()).setScore(1 + i); - case DOWN -> objective.getScore(team.getName()).setScore(15 - i); - } - } - } - - public void removeEntry(String id) { - this.getScoreboard().resetScores(id); - } - - public void resetScoreboard() { - ready = false; - player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/board/BoardEntry.java b/core/src/main/java/com/volmit/iris/util/board/BoardEntry.java deleted file mode 100644 index 14b7f3d71..000000000 --- a/core/src/main/java/com/volmit/iris/util/board/BoardEntry.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.board; - -import com.volmit.iris.util.format.C; -import lombok.Getter; -import org.apache.commons.lang.StringUtils; - -/** - * @author Missionary (missionarymc@gmail.com) - * @since 3/29/2018 - */ -@SuppressWarnings("ClassCanBeRecord") -public class BoardEntry { - - @Getter - private final String prefix, suffix; - - private BoardEntry(final String prefix, final String suffix) { - this.prefix = prefix; - this.suffix = suffix; - } - - public static BoardEntry translateToEntry(String input) { - if (input.isEmpty()) { - return new BoardEntry("", ""); - } - if (input.length() <= 16) { - return new BoardEntry(input, ""); - } else { - String prefix = input.substring(0, 16); - String suffix = ""; - - if (prefix.endsWith("\u00a7")) { - prefix = prefix.substring(0, prefix.length() - 1); - suffix = "\u00a7" + suffix; - } - - suffix = StringUtils.left(C.getLastColors(prefix) + suffix + input.substring(16), 16); - return new BoardEntry(prefix, suffix); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/board/BoardManager.java b/core/src/main/java/com/volmit/iris/util/board/BoardManager.java deleted file mode 100644 index fe61666de..000000000 --- a/core/src/main/java/com/volmit/iris/util/board/BoardManager.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.board; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitTask; - -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - - -public class BoardManager { - - private final JavaPlugin plugin; - private final Map scoreboards; - private final BukkitTask updateTask; - private BoardSettings boardSettings; - - - public BoardManager(JavaPlugin plugin, BoardSettings boardSettings) { - this.plugin = plugin; - this.boardSettings = boardSettings; - this.scoreboards = new ConcurrentHashMap<>(); - this.updateTask = new BoardUpdateTask(this).runTaskTimer(plugin, 2L, 20L); - plugin.getServer().getOnlinePlayers().forEach(this::setup); - } - - - public void setBoardSettings(BoardSettings boardSettings) { - this.boardSettings = boardSettings; - scoreboards.values().forEach(board -> board.setBoardSettings(boardSettings)); - } - - - public boolean hasBoard(Player player) { - return scoreboards.containsKey(player.getUniqueId()); - } - - - public Optional getBoard(Player player) { - return Optional.ofNullable(scoreboards.get(player.getUniqueId())); - } - - - public void setup(Player player) { - Optional.ofNullable(scoreboards.remove(player.getUniqueId())).ifPresent(Board::resetScoreboard); - if (player.getScoreboard().equals(Bukkit.getScoreboardManager().getMainScoreboard())) { - player.setScoreboard(Bukkit.getScoreboardManager().getNewScoreboard()); - } - scoreboards.put(player.getUniqueId(), new Board(player, boardSettings)); - } - - - public void remove(Player player) { - Optional.ofNullable(scoreboards.remove(player.getUniqueId())).ifPresent(Board::remove); - } - - - public Map getScoreboards() { - return Collections.unmodifiableMap(scoreboards); - } - - - public void onDisable() { - updateTask.cancel(); - plugin.getServer().getOnlinePlayers().forEach(this::remove); - scoreboards.clear(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/board/BoardProvider.java b/core/src/main/java/com/volmit/iris/util/board/BoardProvider.java deleted file mode 100644 index 5b13bbc86..000000000 --- a/core/src/main/java/com/volmit/iris/util/board/BoardProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.board; - -import org.bukkit.entity.Player; - -import java.util.List; - - -public interface BoardProvider { - - String getTitle(Player player); - - - List getLines(Player player); -} diff --git a/core/src/main/java/com/volmit/iris/util/board/BoardSettings.java b/core/src/main/java/com/volmit/iris/util/board/BoardSettings.java deleted file mode 100644 index 8ca30503c..000000000 --- a/core/src/main/java/com/volmit/iris/util/board/BoardSettings.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.board; - -import lombok.Builder; -import lombok.Getter; - - -@SuppressWarnings("ClassCanBeRecord") -@Getter -@Builder -public class BoardSettings { - - private final BoardProvider boardProvider; - - - private final ScoreDirection scoreDirection; -} diff --git a/core/src/main/java/com/volmit/iris/util/board/BoardUpdateTask.java b/core/src/main/java/com/volmit/iris/util/board/BoardUpdateTask.java deleted file mode 100644 index 1a54bf498..000000000 --- a/core/src/main/java/com/volmit/iris/util/board/BoardUpdateTask.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.board; - -import lombok.RequiredArgsConstructor; -import org.bukkit.Bukkit; -import org.bukkit.scheduler.BukkitRunnable; - -import java.util.UUID; -import java.util.function.Predicate; - -/** - * @author Missionary (missionarymc@gmail.com) - * @since 5/31/2018 - */ -@RequiredArgsConstructor -public class BoardUpdateTask extends BukkitRunnable { - - private static final Predicate PLAYER_IS_ONLINE = uuid -> Bukkit.getPlayer(uuid) != null; - - private final BoardManager boardManager; - - @Override - public void run() { - for (var entry : boardManager.getScoreboards().entrySet()) { - if (!PLAYER_IS_ONLINE.test(entry.getKey())) { - continue; - } - - entry.getValue().update(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/board/ScoreDirection.java b/core/src/main/java/com/volmit/iris/util/board/ScoreDirection.java deleted file mode 100644 index ab8610b93..000000000 --- a/core/src/main/java/com/volmit/iris/util/board/ScoreDirection.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.board; - -/** - * @author Missionary (missionarymc@gmail.com) - * @since 5/31/2018 - */ - -public enum ScoreDirection { - - UP, - - - DOWN -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/ArrayCache.java b/core/src/main/java/com/volmit/iris/util/cache/ArrayCache.java deleted file mode 100644 index e8d73dd62..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/ArrayCache.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import com.volmit.iris.util.hunk.bits.Writable; - -import java.io.DataOutputStream; -import java.io.IOException; - -public interface ArrayCache extends Writable { - static int zigZag(int coord, int size) { - if (coord < 0) { - coord = Math.abs(coord); - } - - if (coord % (size * 2) >= size) { - return (size) - (coord % size) - 1; - } else { - return coord % size; - } - } - - T get(int i); - - void set(int i, T t); - - void iset(int i, int v); - - int getWidth(); - - int getHeight(); - - void writeCache(DataOutputStream dos) throws IOException; - - default void set(int x, int y, T v) { - set((zigZag(y, getHeight()) * getWidth()) + zigZag(x, getWidth()), v); - } - - default T get(int x, int y) { - try { - return get((zigZag(y, getHeight()) * getWidth()) + zigZag(x, getWidth())); - } catch (Throwable e) { - e.printStackTrace(); - throw e; - } - } - - default void iset(int x, int y, int v) { - iset((zigZag(y, getHeight()) * getWidth()) + zigZag(x, getWidth()), v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/ByteBitCache.java b/core/src/main/java/com/volmit/iris/util/cache/ByteBitCache.java deleted file mode 100644 index eb701973f..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/ByteBitCache.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class ByteBitCache extends DataBitCache { - public ByteBitCache(int width, int height) { - super(width, height); - } - - @Override - public Integer readNodeData(DataInputStream din) throws IOException { - return (int) din.readByte(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Integer integer) throws IOException { - dos.writeByte(integer); - } - - @Override - public void iset(int i, int v) { - set(i, v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/ByteCache.java b/core/src/main/java/com/volmit/iris/util/cache/ByteCache.java deleted file mode 100644 index 6aba0b88a..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/ByteCache.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import lombok.Getter; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class ByteCache implements ArrayCache { - @Getter - private final int width; - @Getter - private final int height; - private final byte[] cache; - - public ByteCache(int width, int height) { - this.width = width; - this.height = height; - cache = new byte[width * height]; - } - - public void set(int i, Integer v) { - cache[i] = v.byteValue(); - } - - public Integer get(int i) { - return (int) cache[i]; - } - - @Override - public void writeCache(DataOutputStream dos) throws IOException { - dos.writeInt(width); - dos.writeInt(height); - - for (int i = 0; i < width * height; i++) { - dos.writeByte(get(i)); - } - } - - @Override - public Integer readNodeData(DataInputStream din) throws IOException { - return (int) din.readByte(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Integer integer) throws IOException { - dos.writeByte(integer); - } - - @Override - public void iset(int i, int v) { - set(i, v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/ChunkCache2D.java b/core/src/main/java/com/volmit/iris/util/cache/ChunkCache2D.java deleted file mode 100644 index f68dff1bf..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/ChunkCache2D.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.volmit.iris.util.cache; - -import com.volmit.iris.util.function.Function2; - -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; - -public class ChunkCache2D { - private static final boolean FAST = Boolean.getBoolean("iris.cache.fast"); - private static final boolean DYNAMIC = Boolean.getBoolean("iris.cache.dynamic"); - private static final VarHandle AA = MethodHandles.arrayElementVarHandle(Entry[].class); - - private final Entry[] cache; - - @SuppressWarnings({"unchecked"}) - public ChunkCache2D() { - this.cache = new Entry[256]; - if (DYNAMIC) return; - for (int i = 0; i < cache.length; i++) { - cache[i] = FAST ? new FastEntry<>() : new Entry<>(); - } - } - - @SuppressWarnings({"unchecked"}) - public T get(int x, int z, Function2 resolver) { - int key = ((z & 15) * 16) + (x & 15); - var entry = cache[key]; - if (entry == null) { - entry = FAST ? new FastEntry<>() : new Entry<>(); - if (!AA.compareAndSet(cache, key, null, entry)) { - entry = (Entry) AA.getVolatile(cache, key); - } - } - return entry.compute(x, z, resolver); - } - - private static class Entry { - protected volatile T t; - - protected T compute(int x, int z, Function2 resolver) { - if (t != null) return t; - synchronized (this) { - if (t == null) t = resolver.apply(x, z); - return t; - } - } - } - - private static class FastEntry extends Entry { - @Override - protected T compute(int x, int z, Function2 resolver) { - if (t != null) return t; - return t = resolver.apply(x, z); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/DataBitCache.java b/core/src/main/java/com/volmit/iris/util/cache/DataBitCache.java deleted file mode 100644 index ee334dff0..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/DataBitCache.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import com.volmit.iris.util.hunk.bits.DataContainer; -import lombok.Getter; - -import java.io.DataOutputStream; -import java.io.IOException; - -public abstract class DataBitCache implements ArrayCache { - @Getter - private final int width; - @Getter - private final int height; - private final DataContainer cache; - - public DataBitCache(int width, int height) { - this.width = width; - this.height = height; - cache = new DataContainer<>(this, width * height); - } - - public void set(int i, T v) { - cache.set(i, v); - } - - public T get(int i) { - return cache.get(i); - } - - @Override - public void writeCache(DataOutputStream dos) throws IOException { - dos.writeInt(width); - dos.writeInt(height); - cache.writeDos(dos); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/FloatBitCache.java b/core/src/main/java/com/volmit/iris/util/cache/FloatBitCache.java deleted file mode 100644 index 3c2849191..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/FloatBitCache.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class FloatBitCache extends DataBitCache { - public FloatBitCache(int width, int height) { - super(width, height); - } - - @Override - public Float readNodeData(DataInputStream din) throws IOException { - return din.readFloat(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Float integer) throws IOException { - dos.writeFloat(integer); - } - - @Override - public void iset(int i, int v) { - set(i, (float) v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/FloatCache.java b/core/src/main/java/com/volmit/iris/util/cache/FloatCache.java deleted file mode 100644 index a41d8f605..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/FloatCache.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import lombok.Getter; - -import java.io.*; - -public class FloatCache implements ArrayCache { - @Getter - private final int width; - @Getter - private final int height; - private final float[] cache; - - - public FloatCache(File file) throws IOException { - this(new DataInputStream(new FileInputStream(file))); - } - - public FloatCache(DataInputStream din) throws IOException { - this(din.readInt(), din.readInt()); - for (int i = 0; i < width * height; i++) { - cache[i] = din.readFloat(); - } - din.close(); - } - - public FloatCache(int width, int height) { - this.width = width; - this.height = height; - cache = new float[width * height]; - } - - public void set(int i, Float v) { - cache[i] = v; - } - - public Float get(int i) { - return cache[i]; - } - - @Override - public void writeCache(DataOutputStream dos) throws IOException { - dos.writeInt(width); - dos.writeInt(height); - - for (int i = 0; i < width * height; i++) { - dos.writeFloat(get(i)); - } - } - - @Override - public Float readNodeData(DataInputStream din) throws IOException { - return din.readFloat(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Float integer) throws IOException { - dos.writeFloat(integer); - } - - @Override - public void iset(int i, int v) { - set(i, (float) v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/IntBitCache.java b/core/src/main/java/com/volmit/iris/util/cache/IntBitCache.java deleted file mode 100644 index c06e62682..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/IntBitCache.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class IntBitCache extends DataBitCache { - public IntBitCache(int width, int height) { - super(width, height); - } - - @Override - public Integer readNodeData(DataInputStream din) throws IOException { - return din.readInt(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Integer integer) throws IOException { - dos.writeInt(integer); - } - - @Override - public void iset(int i, int v) { - set(i, v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/IntCache.java b/core/src/main/java/com/volmit/iris/util/cache/IntCache.java deleted file mode 100644 index 7e97484a3..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/IntCache.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import lombok.Getter; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class IntCache implements ArrayCache { - @Getter - private final int width; - @Getter - private final int height; - private final int[] cache; - - public IntCache(int width, int height) { - this.width = width; - this.height = height; - cache = new int[width * height]; - } - - public void set(int i, Integer v) { - cache[i] = v; - } - - public Integer get(int i) { - return cache[i]; - } - - @Override - public void writeCache(DataOutputStream dos) throws IOException { - dos.writeInt(width); - dos.writeInt(height); - - for (int i = 0; i < width * height; i++) { - dos.writeInt(get(i)); - } - } - - @Override - public Integer readNodeData(DataInputStream din) throws IOException { - return din.readInt(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Integer integer) throws IOException { - dos.writeInt(integer); - } - - @Override - public void iset(int i, int v) { - set(i, v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/ShortBitCache.java b/core/src/main/java/com/volmit/iris/util/cache/ShortBitCache.java deleted file mode 100644 index 4bb01d053..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/ShortBitCache.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class ShortBitCache extends DataBitCache { - public ShortBitCache(int width, int height) { - super(width, height); - } - - @Override - public Short readNodeData(DataInputStream din) throws IOException { - return din.readShort(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Short integer) throws IOException { - dos.writeShort(integer); - } - - @Override - public void iset(int i, int v) { - set(i, (short) v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/ShortCache.java b/core/src/main/java/com/volmit/iris/util/cache/ShortCache.java deleted file mode 100644 index 4b0ee84bf..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/ShortCache.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -import lombok.Getter; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class ShortCache implements ArrayCache { - @Getter - private final int width; - @Getter - private final int height; - private final short[] cache; - - public ShortCache(int width, int height) { - this.width = width; - this.height = height; - cache = new short[width * height]; - } - - public void set(int i, Short v) { - cache[i] = v; - } - - public Short get(int i) { - return cache[i]; - } - - @Override - public void writeCache(DataOutputStream dos) throws IOException { - dos.writeInt(width); - dos.writeInt(height); - - for (int i = 0; i < width * height; i++) { - dos.writeShort(get(i)); - } - } - - @Override - public Short readNodeData(DataInputStream din) throws IOException { - return din.readShort(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Short integer) throws IOException { - dos.writeShort(integer); - } - - @Override - public void iset(int i, int v) { - set(i, (short) v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/UByteBitCache.java b/core/src/main/java/com/volmit/iris/util/cache/UByteBitCache.java deleted file mode 100644 index 4a1309f7b..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/UByteBitCache.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -public class UByteBitCache extends ByteBitCache { - public UByteBitCache(int width, int height) { - super(width, height); - } - - @Override - public void set(int i, Integer v) { - super.set(i, v + Byte.MIN_VALUE); - } - - @Override - public Integer get(int i) { - return super.get(i) - Byte.MIN_VALUE; - } - - @Override - public void iset(int i, int v) { - set(i, v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/UByteCache.java b/core/src/main/java/com/volmit/iris/util/cache/UByteCache.java deleted file mode 100644 index 81f5c23cc..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/UByteCache.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.cache; - -public class UByteCache extends ByteCache { - public UByteCache(int width, int height) { - super(width, height); - } - - @Override - public void set(int i, Integer v) { - super.set(i, v + Byte.MIN_VALUE); - } - - @Override - public Integer get(int i) { - return super.get(i) - Byte.MIN_VALUE; - } - - @Override - public void iset(int i, int v) { - set(i, v); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/cache/WorldCache2D.java b/core/src/main/java/com/volmit/iris/util/cache/WorldCache2D.java deleted file mode 100644 index 1d0ea3f29..000000000 --- a/core/src/main/java/com/volmit/iris/util/cache/WorldCache2D.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.volmit.iris.util.cache; - -import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.util.function.Function2; - -public class WorldCache2D { - private final ConcurrentLinkedHashMap> chunks; - private final Function2 resolver; - - public WorldCache2D(Function2 resolver, int size) { - this.resolver = resolver; - chunks = new ConcurrentLinkedHashMap.Builder>() - .initialCapacity(size) - .maximumWeightedCapacity(size) - .concurrencyLevel(Math.max(32, Runtime.getRuntime().availableProcessors() * 4)) - .build(); - } - - public T get(int x, int z) { - ChunkCache2D chunk = chunks.computeIfAbsent(Cache.key(x >> 4, z >> 4), $ -> new ChunkCache2D<>()); - return chunk.get(x, z, resolver); - } - - public long getSize() { - return chunks.size() * 256L; - } - - public long getMaxSize() { - return chunks.capacity() * 256L; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/collection/GBiset.java b/core/src/main/java/com/volmit/iris/util/collection/GBiset.java deleted file mode 100644 index 80a252ee2..000000000 --- a/core/src/main/java/com/volmit/iris/util/collection/GBiset.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.collection; - - -import java.io.Serializable; - -/** - * A Biset - * - * @param
the first object type - * @param the second object type - * @author cyberpwn - */ -@SuppressWarnings("hiding") -public class GBiset implements Serializable { - private static final long serialVersionUID = 1L; - private A a; - private B b; - - /** - * Create a new Biset - * - * @param a the first object - * @param b the second object - */ - public GBiset(A a, B b) { - this.a = a; - this.b = b; - } - - /** - * Get the object of the type A - * - * @return the first object - */ - public A getA() { - return a; - } - - /** - * Set the first object - * - * @param a the first object A - */ - public void setA(A a) { - this.a = a; - } - - /** - * Get the second object - * - * @return the second object - */ - public B getB() { - return b; - } - - /** - * Set the second object - */ - public void setB(B b) { - this.b = b; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/collection/GListAdapter.java b/core/src/main/java/com/volmit/iris/util/collection/GListAdapter.java deleted file mode 100644 index 9869f4859..000000000 --- a/core/src/main/java/com/volmit/iris/util/collection/GListAdapter.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.collection; - - -import java.util.List; - -/** - * Adapts a list of objects into a list of other objects - * - * @param the from object in lists (the item INSIDE the list) - * @param the to object in lists (the item INSIDE the list) - * @author cyberpwn - */ -public abstract class GListAdapter { - /** - * Adapts a list of FROM to a list of TO - * - * @param from the from list - * @return the to list - */ - public List adapt(List from) { - List adapted = new KList<>(); - - for (FROM i : from) { - TO t = onAdapt(i); - - if (t != null) { - adapted.add(onAdapt(i)); - } - } - - return adapted; - } - - /** - * Adapts a list object FROM to TO for use with the adapt method - * - * @param from the from object - * @return the to object - */ - public abstract TO onAdapt(FROM from); -} diff --git a/core/src/main/java/com/volmit/iris/util/collection/KList.java b/core/src/main/java/com/volmit/iris/util/collection/KList.java deleted file mode 100644 index a22c562db..000000000 --- a/core/src/main/java/com/volmit/iris/util/collection/KList.java +++ /dev/null @@ -1,709 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.collection; - -import com.google.common.util.concurrent.AtomicDoubleArray; -import com.volmit.iris.util.function.NastyFunction; -import com.volmit.iris.util.json.JSONArray; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; - -import java.util.*; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collector; -import java.util.stream.Collectors; - -@SuppressWarnings("ALL") -public class KList extends ArrayList implements List { - private static final long serialVersionUID = -2892550695744823337L; - - @SafeVarargs - public KList(T... ts) { - super(); - add(ts); - } - - public KList() { - super(); - } - - public KList(int cap) { - super(cap); - } - - public KList(Collection values) { - super(); - add(values); - } - - public KList(Enumeration e) { - super(); - add(e); - } - - public static KList fromJSONAny(JSONArray oo) { - KList s = new KList(); - - for (int i = 0; i < oo.length(); i++) { - s.add(oo.get(i).toString()); - } - - return s; - } - - public static Collector> collector() { - return Collectors.toCollection(KList::new); - } - - public static KList asStringList(List oo) { - KList s = new KList(); - - for (Object i : oo) { - s.add(i.toString()); - } - - return s; - } - - public int indexOfAddIfNeeded(T v) { - addIfMissing(v); - return indexOf(v); - } - - public void addMultiple(T t, int c) { - for (int i = 0; i < c; i++) { - add(t); - } - } - - private KList add(Enumeration e) { - while (e.hasMoreElements()) { - add(e.nextElement()); - } - - return this; - } - - public KList add(Collection values) { - addAll(values); - return this; - } - - /** - * Create a Map out of this list where this list becomes the values of the - * returned map. You must specify each key for each value in this list. In the - * function, returning null will not add the keyval pair. - * - * @param the inferred key type - * @param f the function - * @return the new map - */ - public KMap asValues(Function f) { - KMap m = new KMap(); - forEach((i) -> m.putNonNull(f.apply(i), i)); - return m; - } - - /** - * Create a Map out of this list where this list becomes the keys of the - * returned map. You must specify each value for each key in this list. In the - * function, returning null will not add the keyval pair. - * - * @param the inferred value type - * @param f the function - * @return the new map - */ - public KMap asKeys(Function f) { - KMap m = new KMap(); - forEach((i) -> m.putNonNull(i, f.apply(i))); - return m; - } - - /** - * Cut this list into targetCount sublists - * - * @param targetCount the target count of sublists - * @return the list of sublists - */ - public KList> divide(int targetCount) { - return split(size() / targetCount); - } - - /** - * Split this list into a list of sublists with roughly targetSize elements of T - * per sublist - * - * @param targetSize the target size - * @return the list of sublists - */ - public KList> split(int targetSize) { - targetSize = targetSize < 1 ? 1 : targetSize; - KList> gg = new KList<>(); - KList b = new KList<>(); - - for (T i : this) { - if (b.size() >= targetSize) { - gg.add(b.copy()); - b.clear(); - } - - b.add(i); - } - - if (!b.isEmpty()) { - gg.add(b); - } - - return gg; - } - - /** - * Rewrite this list by checking each value and changing the value (or not). - * Return null to remove the element in the function - * - * @param t the function - * @return the same list (not a copy) - */ - public KList rewrite(Function t) { - KList m = copy(); - clear(); - - for (T i : m) { - addNonNull(t.apply(i)); - } - - return this; - } - - /** - * To array - * - * @return the array - */ - @SuppressWarnings("unchecked") - public T[] array() { - return (T[]) toArray(); - } - - /** - * Return a copy of this list - * - * @return the copy - */ - public KList copy() { - return new KList().add(this); - } - - /** - * Shuffle the list - * - * @return the same list - */ - public KList shuffle() { - Collections.shuffle(this); - return this; - } - - public KList shuffle(Random rng) { - Collections.shuffle(this, rng); - return this; - } - - /** - * Sort the list (based on toString comparison) - * - * @return the same list - */ - public KList sort() { - Collections.sort(this, (a, b) -> a.toString().compareTo(b.toString())); - return this; - } - - /** - * Reverse this list - * - * @return the same list - */ - public KList reverse() { - Collections.reverse(this); - return this; - } - - @Override - public String toString() { - return "[" + toString(", ") + "]"; - } - - /** - * Tostring with a seperator for each item in the list - * - * @param split the seperator - * @return the string representing this object - */ - public String toString(String split) { - if (isEmpty()) { - return ""; - } - - if (size() == 1) { - return get(0) + ""; - } - - StringBuilder b = new StringBuilder(); - - for (String i : toStringList()) { - b.append(split).append(i == null ? "null" : i); - } - - return b.substring(split.length()); - } - - /** - * Invoke tostring on each value in the list into a string list - * - * @return the string list - */ - public KList toStringList() { - return convert((t) -> t + ""); - } - - /** - * Add the contents of the given list (v) into this list using a converter - * - * @param the type of the forign list - * @param v the forign (given) list - * @param converter the converter that converts the forign type into this list type - * @return this list (builder) - */ - public KList addFrom(List v, Function converter) { - v.forEach((g) -> add(converter.apply(g))); - return this; - } - - /** - * Convert this list into another list type. Such as GList to - * GList. list.convert((i) -> "" + i); - */ - public KList convert(Function converter) { - KList v = new KList(); - forEach((t) -> v.addNonNull(converter.apply(t))); - return v; - } - - /** - * Convert this list into another list type. Such as GList to - * GList. list.convertNasty((i) -> "" + i); - */ - public KList convertNasty(NastyFunction converter) throws Throwable { - KList v = new KList(size()); - for (final var t : this) { - v.addNonNull(converter.run(t)); - } - return v; - } - - public KList removeWhere(Predicate t) { - for (T i : copy()) { - if (t.test(i)) { - remove(i); - } - } - - return this; - } - - /** - * Adds T to the list, ignores if null - * - * @param t the value to add - * @return the same list - */ - public KList addNonNull(T t) { - if (t != null) { - super.add(t); - } - - return this; - } - - /** - * Swaps the values of index a and b. For example "hello", "world", "!" swap(1, - * 2) would change the list to "hello", "!", "world" - * - * @param a the first index - * @param b the second index - * @return the same list (builder), not a copy - */ - public KList swapIndexes(int a, int b) { - T aa = remove(a); - T bb = get(b); - add(a, bb); - remove(b); - add(b, aa); - - return this; - } - - /** - * Remove a number of elements from the list - * - * @param t the elements - * @return this list - */ - @SuppressWarnings("unchecked") - public KList remove(T... t) { - for (T i : t) { - super.remove(i); - } - - return this; - } - - /** - * Add another glist's contents to this one (addall builder) - * - * @param t the list - * @return the same list - */ - public KList add(KList t) { - super.addAll(t); - return this; - } - - /** - * Add a number of values to this list - * - * @param t the list - * @return this list - */ - @SuppressWarnings("unchecked") - public KList add(T... t) { - for (T i : t) { - super.add(i); - } - - return this; - } - - /** - * Check if this list has an index at the given index - * - * @param index the given index - * @return true if size > index - */ - public boolean hasIndex(int index) { - return size() > index && index >= 0; - } - - /** - * Get the last index of this list (size - 1) - * - * @return the last index of this list - */ - public int last() { - return size() - 1; - } - - /** - * Deduplicate this list by converting to linked hash set and back - * - * @return the deduplicated list - */ - public KList dedupe() { - LinkedHashSet lhs = new LinkedHashSet(this); - return qclear().add(lhs); - } - - /** - * Clear this list (and return it) - * - * @return the same list - */ - public KList qclear() { - super.clear(); - return this; - } - - /** - * Simply !isEmpty() - * - * @return true if this list has 1 or more element(s) - */ - public boolean hasElements() { - return !isEmpty(); - } - - /** - * Simply !isEmpty() - * - * @return true if this list has 1 or more element(s) - */ - public boolean isNotEmpty() { - return !isEmpty(); - } - - /** - * Pop the first item off this list and return it - * - * @return the popped off item or null if the list is empty - */ - public T pop() { - if (isEmpty()) { - return null; - } - - return remove(0); - } - - /** - * Pop the last item off this list and return it - * - * @return the popped off item or null if the list is empty - */ - public T popLast() { - if (isEmpty()) { - return null; - } - - return remove(last()); - } - - public T popRandom() { - if (isEmpty()) { - return null; - } - - if (size() == 1) { - return pop(); - } - - return remove(M.irand(0, last())); - } - - public T popRandom(RNG rng) { - if (isEmpty()) { - return null; - } - - if (size() == 1) { - return pop(); - } - - return remove(rng.i(0, last())); - } - - public KList sub(int f, int t) { - KList g = new KList<>(); - - for (int i = f; i < M.min(size(), t); i++) { - g.add(get(i)); - } - - return g; - } - - public JSONArray toJSONStringArray() { - JSONArray j = new JSONArray(); - - for (Object i : this) { - j.put(i.toString()); - } - - return j; - } - - @SuppressWarnings("unchecked") - public KList forceAdd(Object[] values) { - for (Object i : values) { - add((T) i); - } - - return this; - } - - @SuppressWarnings("unchecked") - public KList forceAdd(int[] values) { - for (Object i : values) { - add((T) i); - } - - return this; - } - - @SuppressWarnings("unchecked") - public KList forceAdd(double[] values) { - for (Object i : values) { - add((T) i); - } - - return this; - } - - @SuppressWarnings("unchecked") - public KList forceAdd(AtomicDoubleArray values) { - for (int i = 0; i < values.length(); i++) { - add((T) ((Object) values.get(i))); - } - - return this; - } - - @SuppressWarnings("unchecked") - public KList forceAdd(float[] values) { - for (Object i : values) { - add((T) i); - } - - return this; - } - - @SuppressWarnings("unchecked") - public KList forceAdd(byte[] values) { - for (Object i : values) { - add((T) i); - } - - return this; - } - - @SuppressWarnings("unchecked") - public KList forceAdd(short[] values) { - for (Object i : values) { - add((T) i); - } - - return this; - } - - @SuppressWarnings("unchecked") - public KList forceAdd(long[] values) { - for (Object i : values) { - add((T) i); - } - - return this; - } - - @SuppressWarnings("unchecked") - public KList forceAdd(boolean[] values) { - for (Object i : values) { - add((T) i); - } - - return this; - } - - public T middleValue() { - return get(middleIndex()); - } - - private int middleIndex() { - return size() % 2 == 0 ? (size() / 2) : ((size() / 2) + 1); - } - - public T getRandom() { - if (isEmpty()) { - return null; - } - - if (size() == 1) { - return get(0); - } - - return get(M.irand(0, last())); - } - - public KList popRandom(RNG rng, int c) { - KList m = new KList<>(); - - for (int i = 0; i < c; i++) { - if (isEmpty()) { - break; - } - - m.add(popRandom()); - } - - return m; - } - - public T getRandom(RNG rng) { - if (isEmpty()) { - return null; - } - - if (size() == 1) { - return get(0); - } - - return get(rng.i(0, last())); - } - - public KList qdel(T t) { - remove(t); - return this; - } - - public KList qadd(T t) { - add(t); - return this; - } - - public KList qaddIfMissing(T t) { - addIfMissing(t); - return this; - } - - public KList removeDuplicates() { - KSet v = new KSet<>(); - v.addAll(this); - KList m = new KList<>(); - m.addAll(v); - return m; - } - - public boolean addIfMissing(T t) { - if (!contains(t)) { - add(t); - return true; - } - - return false; - } - - public void addAllIfMissing(KList t) { - for (T i : t) { - if (!contains(i)) { - add(i); - } - } - } - - public KList shuffleCopy(Random rng) { - KList t = copy(); - t.shuffle(rng); - return t; - } - - public KList qdrop() { - pop(); - return this; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/collection/KMap.java b/core/src/main/java/com/volmit/iris/util/collection/KMap.java deleted file mode 100644 index 4657b431f..000000000 --- a/core/src/main/java/com/volmit/iris/util/collection/KMap.java +++ /dev/null @@ -1,422 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.collection; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.function.Consumer2; -import com.volmit.iris.util.function.Consumer3; -import com.volmit.iris.util.scheduling.Queue; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; - -@SuppressWarnings("ALL") -public class KMap extends ConcurrentHashMap { - private static final long serialVersionUID = 7288942695300448163L; - - public KMap() { - this(16); - } - - public KMap(int initialCapacity) { - this(initialCapacity, 0.75f, 1); - } - - public KMap(int initialCapacity, float loadFactor, int concurrencyLevel) { - super(initialCapacity, loadFactor, concurrencyLevel); - } - - public KMap(Map gMap) { - this(); - put(gMap); - } - - public K getKey(V value) { - for (KeyPair i : keypair()) { - if (i.getV().equals(value)) { - return i.getK(); - } - } - - return null; - } - - /** - * Puts a value into a map-value-list based on the key such that if GMap> where V is GList - * - * @param the list type in the value type - * @param k the key to look for - * @param vs the values to put into the list of the given key - * @return the same list (builder) - */ - @SuppressWarnings("unchecked") - public KMap putValueList(K k, S... vs) { - try { - KMap> s = (KMap>) this; - - if (!s.containsKey(k)) { - s.put(k, new KList()); - } - - s.get(k).add(vs); - } catch (Throwable e) { - Iris.reportError(e); - - } - - return this; - } - - /** - * Returns a sorted list of keys from this map, based on the sorting order of - * the values. - * - * @return the value-sorted key list - */ - public KList sortK() { - KList k = new KList(); - KList v = v(); - - Collections.sort(v, new Comparator() { - @Override - public int compare(V v, V t1) { - return v.toString().compareTo(t1.toString()); - } - }); - - for (V i : v) { - for (K j : k()) { - if (get(j).equals(i)) { - k.add(j); - } - } - } - - k.dedupe(); - return k; - } - - /** - * Returns a sorted list of keys from this map, based on the sorting order of - * the values. Sorting is based on numerical values - * - * @return the value-sorted key list - */ - public KList sortKNumber() { - KList k = new KList(); - KList v = v(); - - Collections.sort(v, new Comparator() { - @Override - public int compare(V v, V t1) { - Number n1 = (Number) v; - Number n2 = (Number) t1; - - return (int) ((n1.doubleValue() - n2.doubleValue()) * 1000); - } - }); - - for (V i : v) { - for (K j : k()) { - if (get(j).equals(i)) { - k.add(j); - } - } - } - - k.dedupe(); - return k; - } - - /** - * Put another map's values into this map - * - * @param m the map to insert - * @return this map (builder) - */ - public KMap put(Map m) { - putAll(m); - return this; - } - - /** - * Merge with another map - * - * @param m the map to merge - * @return this map (builder) - */ - public KMap merge(KMap m, BiFunction merger) { - m.forEach((k, v) -> merge(k, v, merger)); - return this; - } - - /** - * Return a copy of this map - * - * @return the copied map - */ - public KMap copy() { - return new KMap(this); - } - - /** - * Loop through each keyvalue set (copy of it) with the map parameter - * - * @param f the function - * @return the same gmap - */ - public KMap rewrite(Consumer3> f) { - KMap m = copy(); - - for (K i : m.k()) { - f.accept(i, get(i), this); - } - - return this; - } - - /** - * Loop through each keyvalue set (copy of it) - * - * @param f the function - * @return the same gmap - */ - public KMap each(Consumer2 f) { - for (K i : k()) { - f.accept(i, get(i)); - } - - return this; - } - - /** - * Flip the hashmap and flatten the value list even if there are multiple keys - * - * @return the flipped and flattened hashmap - */ - public KMap flipFlatten() { - KMap> f = flip(); - KMap m = new KMap<>(); - - for (V i : f.k()) { - m.putNonNull(i, m.isEmpty() ? null : m.get(0)); - } - - return m; - } - - /** - * Flip the hashmap so keys are now list-keys in the value position - * - * @return the flipped hashmap - */ - public KMap> flip() { - KMap> flipped = new KMap>(); - - for (K i : keySet()) { - if (i == null) { - continue; - } - - if (!flipped.containsKey(get(i))) { - flipped.put(get(i), new KList()); - } - - flipped.get(get(i)).add(i); - } - - return flipped; - } - - /** - * Sort values based on the keys sorting order - * - * @return the values (sorted) - */ - public KList sortV() { - KList v = new KList(); - KList k = k(); - - Collections.sort(k, new Comparator() { - @Override - public int compare(K v, K t1) { - return v.toString().compareTo(t1.toString()); - } - }); - - for (K i : k) { - for (V j : v()) { - if (get(i).equals(j)) { - v.add(j); - } - } - } - - v.dedupe(); - return v; - } - - public KList sortVNoDedupe() { - KList v = new KList(); - KList k = k(); - - Collections.sort(k, new Comparator() { - @Override - public int compare(K v, K t1) { - return v.toString().compareTo(t1.toString()); - } - }); - - for (K i : k) { - for (V j : v()) { - if (get(i).equals(j)) { - v.add(j); - } - } - } - - return v; - } - - /** - * Get a copy of this maps keys - * - * @return the keys - */ - public KList k() { - KList k = new KList(); - Enumeration kk = keys(); - - while (kk.hasMoreElements()) { - K kkk = kk.nextElement(); - k.add(kkk); - } - - return k; - } - - /** - * Get a copy of this maps values - * - * @return the values - */ - public KList v() { - return new KList(values()); - } - - /** - * Still works as it normally should except it returns itself (builder) - * - * @param key the key - * @param value the value (single only supported) - */ - public KMap qput(K key, V value) { - super.put(key, value); - return this; - } - - /** - * Works just like put, except it wont put anything unless the key and value are - * nonnull - * - * @param key the nonnull key - * @param value the nonnull value - * @return the same map - */ - public KMap putNonNull(K key, V value) { - if (key != null || value != null) { - put(key, value); - } - - return this; - } - - public V putThen(K key, V valueIfKeyNotPresent) { - if (!containsKey(key)) { - put(key, valueIfKeyNotPresent); - } - - return get(key); - } - - /** - * Clear this map and return it - * - * @return the cleared map - */ - public KMap qclear() { - super.clear(); - return this; - } - - /** - * Convert this map to keypairs - * - * @return the keypair list - */ - public KList> keypair() { - KList> g = new KList<>(); - each((k, v) -> g.add(new KeyPair(k, v))); - return g; - } - - public KMap qclear(BiConsumer action) { - final Iterator> it = entrySet().iterator(); - while (it.hasNext()) { - final Map.Entry entry = it.next(); - it.remove(); - try { - action.accept(entry.getKey(), entry.getValue()); - } catch (Throwable e) { - Iris.reportError(e); - } - } - return this; - } - - /** - * Create a keypair queue - * - * @return the queue - */ - public Queue> enqueue() { - return Queue.create(keypair()); - } - - /** - * Create a key queue - * - * @return the queue - */ - public Queue enqueueKeys() { - return Queue.create(k()); - } - - /** - * Create a value queue - * - * @return the queue - */ - public Queue enqueueValues() { - return Queue.create(v()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/collection/KSet.java b/core/src/main/java/com/volmit/iris/util/collection/KSet.java deleted file mode 100644 index b695b0fd3..000000000 --- a/core/src/main/java/com/volmit/iris/util/collection/KSet.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.collection; - -import org.jetbrains.annotations.NotNull; - -import java.io.Serializable; -import java.util.AbstractSet; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.concurrent.ConcurrentHashMap; - -public class KSet extends AbstractSet implements Serializable { - private static final long serialVersionUID = 1L; - private final ConcurrentHashMap map; - - public KSet(Collection c) { - this(c.size()); - addAll(c); - } - - @SafeVarargs - public KSet(T... values) { - this(values.length); - addAll(Arrays.asList(values)); - } - - public KSet(int initialCapacity, float loadFactor) { - map = new ConcurrentHashMap<>(initialCapacity, loadFactor); - } - - public KSet(int initialCapacity) { - map = new ConcurrentHashMap<>(initialCapacity); - } - - public static KSet merge(Collection first, Collection second) { - var set = new KSet(); - set.addAll(first); - set.addAll(second); - return set; - } - - @Override - public int size() { - return map.size(); - } - - @Override - public boolean contains(Object o) { - return map.containsKey(o); - } - - @Override - public boolean add(T t) { - return map.putIfAbsent(t, Boolean.TRUE) == null; - } - - @Override - public boolean remove(Object o) { - return map.remove(o) != null; - } - - @Override - public void clear() { - map.clear(); - } - - @NotNull - @Override - public Iterator iterator() { - return map.keySet().iterator(); - } - - public KSet copy() { - return new KSet<>(this); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/collection/KeyPair.java b/core/src/main/java/com/volmit/iris/util/collection/KeyPair.java deleted file mode 100644 index 2dd2377c3..000000000 --- a/core/src/main/java/com/volmit/iris/util/collection/KeyPair.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.collection; - -/** - * Represents a keypair - * - * @param the key type - * @param the value type - * @author cyberpwn - */ -@SuppressWarnings("hiding") -public class KeyPair { - private K k; - private V v; - - /** - * Create a keypair - * - * @param k the key - * @param v the value - */ - public KeyPair(K k, V v) { - this.k = k; - this.v = v; - } - - public K getK() { - return k; - } - - public void setK(K k) { - this.k = k; - } - - public V getV() { - return v; - } - - public void setV(V v) { - this.v = v; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/collection/StateList.java b/core/src/main/java/com/volmit/iris/util/collection/StateList.java deleted file mode 100644 index b1a28997e..000000000 --- a/core/src/main/java/com/volmit/iris/util/collection/StateList.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.collection; - -public class StateList { - private final KList states; - - public StateList(String... states) { - this.states = new KList(states); - - if (getBits() > 64) { - throw new RuntimeException("StateLists cannot exceed 64 bits! You are trying to use " + getBits() + " bits!"); - } - } - - public StateList(Enum... states) { - this.states = new KList>().convert(Enum::name); - - if (getBits() > 64) { - throw new RuntimeException("StateLists cannot exceed 64 bits! You are trying to use " + getBits() + " bits!"); - } - } - - public long max() { - return (long) (Math.pow(2, getBits()) - 1); - } - - public KList getEnabled(long list) { - KList f = new KList<>(); - - for (String i : states) { - if (is(list, i)) { - f.add(i); - } - } - - return f; - } - - public long of(String... enabledStates) { - long b = 0; - - for (String i : enabledStates) { - b |= getBit(i); - } - - return b; - } - - public long set(long list, String state, boolean enabled) { - long bit = getBit(state); - boolean is = is(list, state); - - if (enabled && !is) { - return list | bit; - } else if (!enabled && is) { - return list ^ bit; - } - - return list; - } - - public boolean is(long list, String state) { - long bit = getBit(state); - - return bit > 0 && (list & bit) == bit; - } - - public boolean hasBit(String state) { - return getBit(state) > 0; - } - - public long getBit(String state) { - return getBit(states.indexOf(state)); - } - - public long getBit(int index) { - return (long) (index < 0 ? -1 : Math.pow(2, index)); - } - - public int getBytes() { - return getBits() == 0 ? 0 : ((getBits() >> 2) + 1); - } - - public int getBits() { - return states.size(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/context/IrisContext.java b/core/src/main/java/com/volmit/iris/util/context/IrisContext.java deleted file mode 100644 index 43423e3be..000000000 --- a/core/src/main/java/com/volmit/iris/util/context/IrisContext.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.context; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.engine.IrisComplex; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.scheduling.ChronoLatch; -import lombok.Data; - -@Data -public class IrisContext { - private static final KMap context = new KMap<>(); - private static final ChronoLatch cl = new ChronoLatch(60000); - private final Engine engine; - private ChunkContext chunkContext; - - public IrisContext(Engine engine) { - this.engine = engine; - } - - public static IrisContext getOr(Engine engine) { - IrisContext c = get(); - - if (c == null) { - c = new IrisContext(engine); - touch(c); - } - - return c; - } - - public static IrisContext get() { - return context.get(Thread.currentThread()); - } - - public static void touch(IrisContext c) { - context.put(Thread.currentThread(), c); - - if (!cl.couldFlip()) return; - synchronized (cl) { - if (cl.flip()) { - dereference(); - } - } - } - - public static synchronized void dereference() { - var it = context.entrySet().iterator(); - while (it.hasNext()) { - var entry = it.next(); - var thread = entry.getKey(); - var context = entry.getValue(); - if (thread == null || context == null) { - it.remove(); - continue; - } - - if (!thread.isAlive() || context.engine.isClosed()) { - Iris.debug("Dereferenced Context " + thread.getName() + " " + thread.threadId()); - it.remove(); - } - } - } - - public void touch() { - IrisContext.touch(this); - } - - public IrisData getData() { - return engine.getData(); - } - - public IrisComplex getComplex() { - return engine.getComplex(); - } - - public KMap asContext() { - var hash32 = engine.getHash32().getNow(null); - var dimension = engine.getDimension(); - var mantle = engine.getMantle(); - return new KMap() - .qput("studio", engine.isStudio()) - .qput("closed", engine.isClosed()) - .qput("pack", new KMap<>() - .qput("key", dimension == null ? "" : dimension.getLoadKey()) - .qput("version", dimension == null ? "" : dimension.getVersion()) - .qput("hash", hash32 == null ? "" : Long.toHexString(hash32))) - .qput("mantle", new KMap<>() - .qput("idle", mantle.getAdjustedIdleDuration()) - .qput("loaded", mantle.getLoadedRegionCount()) - .qput("queued", mantle.getUnloadRegionCount())); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/B.java b/core/src/main/java/com/volmit/iris/util/data/B.java deleted file mode 100644 index fa401fa44..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/B.java +++ /dev/null @@ -1,734 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.core.link.data.DataType; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.core.nms.container.BlockProperty; -import com.volmit.iris.core.service.ExternalDataSVC; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.registry.Materials; -import com.volmit.iris.util.scheduling.ChronoLatch; -import it.unimi.dsi.fastutil.ints.*; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.Waterlogged; -import org.bukkit.block.data.type.Leaves; -import org.bukkit.block.data.type.PointedDripstone; - -import java.util.*; -import java.util.stream.Collectors; - -import static org.bukkit.Material.*; - -public class B { - private static final KMap custom = new KMap<>(); - - private static final Material AIR_MATERIAL = Material.AIR; - private static final Material SHORT_GRASS = Materials.GRASS; - private static final BlockData AIR = AIR_MATERIAL.createBlockData(); - private static final IntSet foliageCache = buildFoliageCache(); - private static final IntSet deepslateCache = buildDeepslateCache(); - private static final Int2IntMap normal2DeepslateCache = buildNormal2DeepslateCache(); - private static final Int2IntMap deepslate2NormalCache = buildDeepslate2NormalCache(); - private static final IntSet decorantCache = buildDecorantCache(); - private static final IntSet storageCache = buildStorageCache(); - private static final IntSet storageChestCache = buildStorageChestCache(); - private static final IntSet litCache = buildLitCache(); - private static final ChronoLatch clw = new ChronoLatch(1000); - - private static IntSet buildFoliageCache() { - IntSet b = new IntOpenHashSet(); - Arrays.stream(new Material[]{ - POPPY, - DANDELION, - CORNFLOWER, - SWEET_BERRY_BUSH, - CRIMSON_ROOTS, - WARPED_ROOTS, - NETHER_SPROUTS, - ALLIUM, - AZURE_BLUET, - BLUE_ORCHID, - OXEYE_DAISY, - LILY_OF_THE_VALLEY, - WITHER_ROSE, - DARK_OAK_SAPLING, - ACACIA_SAPLING, - JUNGLE_SAPLING, - BIRCH_SAPLING, - SPRUCE_SAPLING, - OAK_SAPLING, - ORANGE_TULIP, - PINK_TULIP, - RED_TULIP, - WHITE_TULIP, - FERN, - LARGE_FERN, - SHORT_GRASS, - TALL_GRASS - }).forEach((i) -> b.add(i.ordinal())); - - return IntSets.unmodifiable(b); - } - - private static IntSet buildDeepslateCache() { - IntSet b = new IntOpenHashSet(); - Arrays.stream(new Material[]{ - DEEPSLATE, - DEEPSLATE_BRICKS, - DEEPSLATE_BRICK_SLAB, - DEEPSLATE_BRICK_STAIRS, - DEEPSLATE_BRICK_WALL, - DEEPSLATE_TILE_SLAB, - DEEPSLATE_TILES, - DEEPSLATE_TILE_STAIRS, - DEEPSLATE_TILE_WALL, - CRACKED_DEEPSLATE_TILES, - DEEPSLATE_COAL_ORE, - DEEPSLATE_IRON_ORE, - DEEPSLATE_COPPER_ORE, - DEEPSLATE_DIAMOND_ORE, - DEEPSLATE_EMERALD_ORE, - DEEPSLATE_GOLD_ORE, - DEEPSLATE_LAPIS_ORE, - DEEPSLATE_REDSTONE_ORE, - }).forEach((i) -> b.add(i.ordinal())); - - return IntSets.unmodifiable(b); - } - - private static Int2IntMap buildNormal2DeepslateCache() { - Int2IntMap b = new Int2IntOpenHashMap(); - - b.put(COAL_ORE.ordinal(), DEEPSLATE_COAL_ORE.ordinal()); - b.put(EMERALD_ORE.ordinal(), DEEPSLATE_EMERALD_ORE.ordinal()); - b.put(DIAMOND_ORE.ordinal(), DEEPSLATE_DIAMOND_ORE.ordinal()); - b.put(COPPER_ORE.ordinal(), DEEPSLATE_COPPER_ORE.ordinal()); - b.put(GOLD_ORE.ordinal(), DEEPSLATE_GOLD_ORE.ordinal()); - b.put(IRON_ORE.ordinal(), DEEPSLATE_IRON_ORE.ordinal()); - b.put(LAPIS_ORE.ordinal(), DEEPSLATE_LAPIS_ORE.ordinal()); - b.put(REDSTONE_ORE.ordinal(), DEEPSLATE_REDSTONE_ORE.ordinal()); - - return b; - } - - private static Int2IntMap buildDeepslate2NormalCache() { - Int2IntMap b = new Int2IntOpenHashMap(); - - b.put(DEEPSLATE_COAL_ORE.ordinal(), COAL_ORE.ordinal()); - b.put(DEEPSLATE_EMERALD_ORE.ordinal(), EMERALD_ORE.ordinal()); - b.put(DEEPSLATE_DIAMOND_ORE.ordinal(), DIAMOND_ORE.ordinal()); - b.put(DEEPSLATE_COPPER_ORE.ordinal(), COPPER_ORE.ordinal()); - b.put(DEEPSLATE_GOLD_ORE.ordinal(), GOLD_ORE.ordinal()); - b.put(DEEPSLATE_IRON_ORE.ordinal(), IRON_ORE.ordinal()); - b.put(DEEPSLATE_LAPIS_ORE.ordinal(), LAPIS_ORE.ordinal()); - b.put(DEEPSLATE_REDSTONE_ORE.ordinal(), REDSTONE_ORE.ordinal()); - - return b; - } - - private static IntSet buildDecorantCache() { - IntSet b = new IntOpenHashSet(); - Arrays.stream(new Material[]{ - SHORT_GRASS, - TALL_GRASS, - TALL_SEAGRASS, - FERN, - LARGE_FERN, - CORNFLOWER, - SUNFLOWER, - CHORUS_FLOWER, - POPPY, - DANDELION, - OXEYE_DAISY, - ORANGE_TULIP, - PINK_TULIP, - RED_TULIP, - WHITE_TULIP, - LILAC, - DEAD_BUSH, - SWEET_BERRY_BUSH, - ROSE_BUSH, - WITHER_ROSE, - ALLIUM, - BLUE_ORCHID, - LILY_OF_THE_VALLEY, - CRIMSON_FUNGUS, - WARPED_FUNGUS, - RED_MUSHROOM, - BROWN_MUSHROOM, - CRIMSON_ROOTS, - AZURE_BLUET, - WEEPING_VINES, - WEEPING_VINES_PLANT, - WARPED_ROOTS, - NETHER_SPROUTS, - TWISTING_VINES, - TWISTING_VINES_PLANT, - SUGAR_CANE, - WHEAT, - POTATOES, - CARROTS, - BEETROOTS, - NETHER_WART, - SEA_PICKLE, - SEAGRASS, - ACACIA_BUTTON, - BIRCH_BUTTON, - CRIMSON_BUTTON, - DARK_OAK_BUTTON, - JUNGLE_BUTTON, - OAK_BUTTON, - POLISHED_BLACKSTONE_BUTTON, - SPRUCE_BUTTON, - STONE_BUTTON, - WARPED_BUTTON, - TORCH, - SOUL_TORCH, - GLOW_LICHEN, - VINE, - SCULK_VEIN - }).forEach((i) -> b.add(i.ordinal())); - b.addAll(foliageCache); - - return IntSets.unmodifiable(b); - } - - private static IntSet buildLitCache() { - IntSet b = new IntOpenHashSet(); - Arrays.stream(new Material[]{ - GLOWSTONE, - AMETHYST_CLUSTER, - SMALL_AMETHYST_BUD, - MEDIUM_AMETHYST_BUD, - LARGE_AMETHYST_BUD, - END_ROD, - SOUL_SAND, - TORCH, - REDSTONE_TORCH, - SOUL_TORCH, - REDSTONE_WALL_TORCH, - WALL_TORCH, - SOUL_WALL_TORCH, - LANTERN, - CANDLE, - JACK_O_LANTERN, - REDSTONE_LAMP, - MAGMA_BLOCK, - LIGHT, - SHROOMLIGHT, - SEA_LANTERN, - SOUL_LANTERN, - FIRE, - SOUL_FIRE, - SEA_PICKLE, - BREWING_STAND, - REDSTONE_ORE, - }).forEach((i) -> b.add(i.ordinal())); - - return IntSets.unmodifiable(b); - } - - private static IntSet buildStorageCache() { - IntSet b = new IntOpenHashSet(); - Arrays.stream(new Material[]{ - CHEST, - SMOKER, - TRAPPED_CHEST, - SHULKER_BOX, - WHITE_SHULKER_BOX, - ORANGE_SHULKER_BOX, - MAGENTA_SHULKER_BOX, - LIGHT_BLUE_SHULKER_BOX, - YELLOW_SHULKER_BOX, - LIME_SHULKER_BOX, - PINK_SHULKER_BOX, - GRAY_SHULKER_BOX, - LIGHT_GRAY_SHULKER_BOX, - CYAN_SHULKER_BOX, - PURPLE_SHULKER_BOX, - BLUE_SHULKER_BOX, - BROWN_SHULKER_BOX, - GREEN_SHULKER_BOX, - RED_SHULKER_BOX, - BLACK_SHULKER_BOX, - BARREL, - DISPENSER, - DROPPER, - HOPPER, - FURNACE, - BLAST_FURNACE - }).forEach((i) -> b.add(i.ordinal())); - - return IntSets.unmodifiable(b); - } - - public static BlockData toDeepSlateOre(BlockData block, BlockData ore) { - int key = ore.getMaterial().ordinal(); - - if (isDeepSlate(block)) { - if (normal2DeepslateCache.containsKey(key)) { - return Material.values()[normal2DeepslateCache.get(key)].createBlockData(); - } - } else { - if (deepslate2NormalCache.containsKey(key)) { - return Material.values()[deepslate2NormalCache.get(key)].createBlockData(); - } - } - - return ore; - } - - public static boolean isDeepSlate(BlockData blockData) { - return deepslateCache.contains(blockData.getMaterial().ordinal()); - } - - public static boolean isOre(BlockData blockData) { - return blockData.getMaterial().name().endsWith("_ORE"); - } - - private static IntSet buildStorageChestCache() { - IntSet b = new IntOpenHashSet(storageCache); - b.remove(SMOKER.ordinal()); - b.remove(FURNACE.ordinal()); - b.remove(BLAST_FURNACE.ordinal()); - - return IntSets.unmodifiable(b); - } - - public static boolean canPlaceOnto(Material mat, Material onto) { - if ((onto.equals(CRIMSON_NYLIUM) || onto.equals(WARPED_NYLIUM)) && - (mat.equals(CRIMSON_FUNGUS) || mat.equals(CRIMSON_ROOTS) || mat.equals(WARPED_FUNGUS) || mat.equals(WARPED_ROOTS))) { - return true; - } - - if (isFoliage(mat)) { - if (!isFoliagePlantable(onto)) { - return false; - } - } - - if (onto.equals(Material.AIR) || - onto.equals(B.getMaterial("CAVE_AIR")) - || onto.equals(B.getMaterial("VOID_AIR"))) { - return false; - } - - if (onto.equals(Material.GRASS_BLOCK) && mat.equals(Material.DEAD_BUSH)) { - return false; - } - - if (onto.equals(Material.DIRT_PATH)) { - if (!mat.isSolid()) { - return false; - } - } - - if (onto.equals(Material.ACACIA_LEAVES) - || onto.equals(Material.BIRCH_LEAVES) - || onto.equals(Material.DARK_OAK_LEAVES) - || onto.equals(Material.JUNGLE_LEAVES) - || onto.equals(Material.OAK_LEAVES) - || onto.equals(Material.SPRUCE_LEAVES)) { - return mat.isSolid(); - } - - return true; - } - - public static boolean isFoliagePlantable(BlockData d) { - return d.getMaterial().equals(Material.GRASS_BLOCK) - || d.getMaterial().equals(Material.MOSS_BLOCK) - || d.getMaterial().equals(Material.ROOTED_DIRT) - || d.getMaterial().equals(Material.DIRT) - || d.getMaterial().equals(Material.COARSE_DIRT) - || d.getMaterial().equals(Material.PODZOL); - } - - public static boolean isFoliagePlantable(Material d) { - return d.equals(Material.GRASS_BLOCK) - || d.equals(Material.MOSS_BLOCK) - || d.equals(Material.DIRT) - || d.equals(TALL_GRASS) - || d.equals(TALL_SEAGRASS) - || d.equals(LARGE_FERN) - || d.equals(SUNFLOWER) - || d.equals(PEONY) - || d.equals(LILAC) - || d.equals(ROSE_BUSH) - || d.equals(Material.ROOTED_DIRT) - || d.equals(Material.COARSE_DIRT) - || d.equals(Material.PODZOL); - } - - public static boolean isWater(BlockData b) { - return b.getMaterial().equals(Material.WATER); - } - - public static BlockData getAir() { - return AIR; - } - - public static Material getMaterialOrNull(String bdx) { - try { - return Material.valueOf(bdx.trim().toUpperCase()); - } catch (Throwable e) { - Iris.reportError(e); - if (clw.flip()) { - Iris.warn("Unknown Material: " + bdx); - } - return null; - } - } - - public static Material getMaterial(String bdx) { - Material m = getMaterialOrNull(bdx); - - if (m == null) { - return AIR_MATERIAL; - } - - return m; - } - - public static boolean isSolid(BlockData mat) { - if (mat == null) - return false; - return mat.getMaterial().isSolid(); - } - - public static BlockData getOrNull(String bdxf, boolean warn) { - try { - String bd = bdxf.trim(); - - if (!custom.isEmpty() && custom.containsKey(bd)) { - return custom.get(bd); - } - - if (bd.startsWith("minecraft:cauldron[level=")) { - bd = bd.replaceAll("\\Q:cauldron[\\E", ":water_cauldron["); - } - - if (bd.equals("minecraft:grass_path")) { - return DIRT_PATH.createBlockData(); - } - - BlockData bdx = parseBlockData(bd, warn); - - if (bdx == null && warn) { - if (clw.flip()) { - Iris.warn("Unknown Block Data '" + bd + "'"); - } - return AIR; - } - - return bdx; - } catch (Throwable e) { - Iris.reportError(e); - - if (clw.flip()) { - Iris.warn("Unknown Block Data '" + bdxf + "'"); - } - } - - return null; - } - - public static BlockData getNoCompat(String bdxf) { - BlockData bd = getOrNull(bdxf, true); - - if (bd != null) { - return bd; - } - - return AIR; - } - - public static BlockData get(String bdxf) { - if (bdxf.contains(":")) { - if (bdxf.startsWith("minecraft:")) { - return Iris.compat.getBlock(bdxf); - } else { - return getNoCompat(bdxf); - } - } else { - return Iris.compat.getBlock(bdxf); - } - } - - private static synchronized BlockData createBlockData(String s, boolean warn) { - try { - return Bukkit.createBlockData(s); - } catch (IllegalArgumentException e) { - if (s.contains("[")) { - return createBlockData(s.split("\\Q[\\E")[0], warn); - } - } - - if (warn) { - Iris.error("Can't find block data for " + s); - } - return null; - } - - private static BlockData parseBlockData(String ix, boolean warn) { - try { - BlockData bx = null; - - if (!ix.startsWith("minecraft:") && ix.contains(":")) { - Identifier key = Identifier.fromString(ix); - Optional bd = Iris.service(ExternalDataSVC.class).getBlockData(key); - Iris.debug("Loading block data " + key); - if (bd.isPresent()) - bx = bd.get(); - } - - if (bx == null) { - try { - bx = createBlockData(ix.toLowerCase(), warn); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - if (bx == null) { - try { - bx = createBlockData("minecraft:" + ix.toLowerCase(), warn); - } catch (Throwable e) { - - } - } - - if (bx == null) { - try { - bx = Material.valueOf(ix.toUpperCase()).createBlockData(); - } catch (Throwable e) { - - } - } - - if (bx == null) { - return null; - } - - if (bx instanceof Leaves && IrisSettings.get().getGenerator().isPreventLeafDecay()) { - ((Leaves) bx).setPersistent(true); - } else if (bx instanceof Leaves) { - ((Leaves) bx).setPersistent(false); - } - - return bx; - } catch (Throwable e) { - if (clw.flip()) { - Iris.warn("Unknown Block Data: " + ix); - } - - String block = ix.contains(":") ? ix.split(":")[1].toLowerCase() : ix.toLowerCase(); - String state = block.contains("[") ? block.split("\\Q[\\E")[1].split("\\Q]\\E")[0] : ""; - Map stateMap = new HashMap<>(); - if (!state.equals("")) { - Arrays.stream(state.split(",")).forEach(s -> stateMap.put(s.split("=")[0], s.split("=")[1])); - } - block = block.split("\\Q[\\E")[0]; - - switch (block) { - case "cauldron" -> block = "water_cauldron"; - case "grass_path" -> block = "dirt_path"; - case "concrete" -> block = "white_concrete"; - case "wool" -> block = "white_wool"; - case "beetroots" -> { - if (stateMap.containsKey("age")) { - String updated = stateMap.get("age"); - switch (updated) { - case "7" -> updated = "3"; - case "3", "4", "5" -> updated = "2"; - case "1", "2" -> updated = "1"; - } - stateMap.put("age", updated); - } - } - } - - Map newStates = new HashMap<>(); - for (String key : stateMap.keySet()) { //Iterate through every state and check if its valid - try { - String newState = block + "[" + key + "=" + stateMap.get(key) + "]"; - createBlockData(newState, warn); - newStates.put(key, stateMap.get(key)); - - } catch (IllegalArgumentException ignored) { - } - } - - //Combine all the "good" states again - state = newStates.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).collect(Collectors.joining(",")); - if (!state.equals("")) state = "[" + state + "]"; - String newBlock = block + state; - Iris.debug("Converting " + ix + " to " + newBlock); - - try { - return createBlockData(newBlock, warn); - } catch (Throwable e1) { - Iris.reportError(e1); - } - - return null; - } - } - - public static boolean isStorage(BlockData mat) { - return storageCache.contains(mat.getMaterial().ordinal()); - } - - public static boolean isStorageChest(BlockData mat) { - return storageChestCache.contains(mat.getMaterial().ordinal()); - } - - public static boolean isLit(BlockData mat) { - return litCache.contains(mat.getMaterial().ordinal()); - } - - public static boolean isUpdatable(BlockData mat) { - return isStorage(mat) - || (mat instanceof PointedDripstone - && ((PointedDripstone) mat).getThickness().equals(PointedDripstone.Thickness.TIP)); - } - - public static boolean isFoliage(Material d) { - return foliageCache.contains(d.ordinal()); - } - - public static boolean isFoliage(BlockData d) { - return isFoliage(d.getMaterial()); - } - - public static boolean isDecorant(BlockData m) { - return decorantCache.contains(m.getMaterial().ordinal()); - } - - public static KList get(KList find) { - KList b = new KList<>(); - - for (String i : find) { - BlockData bd = get(i); - - if (bd != null) { - b.add(bd); - } - } - - return b; - } - - public static boolean isFluid(BlockData d) { - return d.getMaterial().equals(Material.WATER) || d.getMaterial().equals(Material.LAVA); - } - - public static boolean isAirOrFluid(BlockData d) { - return isAir(d) || isFluid(d); - } - - public static boolean isAir(BlockData d) { - if (d == null) { - return true; - } - - return d.getMaterial().equals(Material.AIR) || d.getMaterial().equals(Material.CAVE_AIR) || d.getMaterial().equals(Material.VOID_AIR); - } - - - public synchronized static String[] getBlockTypes() { - KList bt = new KList<>(); - - for (Material i : Material.values()) { - if (i.isBlock()) { - String v = i.createBlockData().getAsString(true); - - if (v.contains("[")) { - v = v.split("\\Q[\\E")[0]; - } - - bt.add(v); - } - } - - for (Identifier id : Iris.service(ExternalDataSVC.class).getAllIdentifiers(DataType.BLOCK)) - bt.add(id.toString()); - bt.addAll(custom.k()); - - return bt.toArray(new String[0]); - } - - public synchronized static KMap, List> getBlockStates() { - KMap, List> flipped = new KMap<>(); - INMS.get().getBlockProperties().forEach((k, v) -> { - flipped.computeIfAbsent(v, $ -> new KList<>()).add(k.getKey().toString()); - }); - - var emptyStates = flipped.computeIfAbsent(new KList<>(0), $ -> new KList<>()); - for (var pair : Iris.service(ExternalDataSVC.class).getAllBlockProperties()) { - if (pair.getB().isEmpty()) emptyStates.add(pair.getA().toString()); - else flipped.computeIfAbsent(pair.getB(), $ -> new KList<>()).add(pair.getA().toString()); - } - emptyStates.addAll(custom.k()); - - KMap, List> states = new KMap<>(); - flipped.forEach((k, v) -> { - var old = states.put(v, k); - if (old != null) Iris.error("Duplicate block state: " + v + " (" + old + " and " + k + ")"); - }); - - return states; - } - - public static String[] getItemTypes() { - KList bt = new KList<>(); - - for (Material i : Material.values()) { - String v = i.name().toLowerCase().trim(); - bt.add(v); - } - - for (Identifier id : Iris.service(ExternalDataSVC.class).getAllIdentifiers(DataType.ITEM)) - bt.add(id.toString()); - - return bt.toArray(new String[0]); - } - - public static boolean isWaterLogged(BlockData b) { - return (b instanceof Waterlogged) && ((Waterlogged) b).isWaterlogged(); - } - - public static void registerCustomBlockData(String namespace, String key, BlockData blockData) { - custom.put(namespace + ":" + key, blockData); - } - - public static boolean isVineBlock(BlockData data) { - return switch (data.getMaterial()) { - case VINE, SCULK_VEIN, GLOW_LICHEN -> true; - default -> false; - }; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/BiomeMap.java b/core/src/main/java/com/volmit/iris/util/data/BiomeMap.java deleted file mode 100644 index 1e39447fe..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/BiomeMap.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.engine.object.IrisBiome; - -public class BiomeMap { - private final IrisBiome[] height; - - public BiomeMap() { - height = new IrisBiome[256]; - } - - public void setBiome(int x, int z, IrisBiome h) { - height[x * 16 + z] = h; - } - - public IrisBiome getBiome(int x, int z) { - return height[x * 16 + z]; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/ChunkCache.java b/core/src/main/java/com/volmit/iris/util/data/ChunkCache.java deleted file mode 100644 index 9fb9dac5a..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/ChunkCache.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.util.function.Function2; - -import java.util.concurrent.atomic.AtomicReferenceArray; - -public class ChunkCache { - private final AtomicReferenceArray cache; - - public ChunkCache() { - cache = new AtomicReferenceArray<>(256); - } - - public T compute(int x, int z, Function2 function) { - T t = get(x & 15, z & 15); - - if (t == null) { - t = function.apply(x, z); - set(x & 15, z & 15, t); - } - - return t; - } - - private void set(int x, int z, T t) { - cache.set(x * 16 + z, t); - } - - private T get(int x, int z) { - return cache.get(x * 16 + z); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/ComplexCache.java b/core/src/main/java/com/volmit/iris/util/data/ComplexCache.java deleted file mode 100644 index 8478db011..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/ComplexCache.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.util.collection.KMap; - -public class ComplexCache { - private final KMap> chunks; - - public ComplexCache() { - chunks = new KMap<>(); - } - - public boolean has(int x, int z) { - return chunks.containsKey(Cache.key(x, z)); - } - - public void invalidate(int x, int z) { - chunks.remove(Cache.key(x, z)); - } - - public ChunkCache chunk(int x, int z) { - return chunks.computeIfAbsent(Cache.key(x, z), (f) -> new ChunkCache<>()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/Cuboid.java b/core/src/main/java/com/volmit/iris/util/data/Cuboid.java deleted file mode 100644 index ccd837eb2..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/Cuboid.java +++ /dev/null @@ -1,832 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.Direction; -import com.volmit.iris.util.math.Position2; -import org.bukkit.*; -import org.bukkit.block.Block; -import org.bukkit.configuration.serialization.ConfigurationSerializable; -import org.bukkit.entity.Entity; - -import java.util.*; - -/** - * Cuboids - * - * @author cyberpwn - */ -public class Cuboid implements Iterable, Cloneable, ConfigurationSerializable { - protected final String worldName; - protected int x1, y1, z1; - protected int x2, y2, z2; - - /** - * Construct a Cuboid given two Location objects which represent any two corners - * of the Cuboid. - * - * @param l1 one of the corners - * @param l2 the other corner - */ - public Cuboid(Location l1, Location l2) { - if (!l1.getWorld().equals(l2.getWorld())) { - throw new IllegalArgumentException("locations must be on the same world"); - } - - worldName = l1.getWorld().getName(); - x1 = Math.min(l1.getBlockX(), l2.getBlockX()); - y1 = Math.min(l1.getBlockY(), l2.getBlockY()); - z1 = Math.min(l1.getBlockZ(), l2.getBlockZ()); - x2 = Math.max(l1.getBlockX(), l2.getBlockX()); - y2 = Math.max(l1.getBlockY(), l2.getBlockY()); - z2 = Math.max(l1.getBlockZ(), l2.getBlockZ()); - } - - /** - * Construct a one-block Cuboid at the given Location of the Cuboid. - * - * @param l1 location of the Cuboid - */ - public Cuboid(Location l1) { - this(l1, l1); - } - - /** - * Copy constructor. - * - * @param other the Cuboid to copy - */ - public Cuboid(Cuboid other) { - this(other.getWorld().getName(), other.x1, other.y1, other.z1, other.x2, other.y2, other.z2); - } - - /** - * Construct a Cuboid in the given World and xyz co-ordinates - * - * @param world the Cuboid's world - * @param x1 X co-ordinate of corner 1 - * @param y1 Y co-ordinate of corner 1 - * @param z1 Z co-ordinate of corner 1 - * @param x2 X co-ordinate of corner 2 - * @param y2 Y co-ordinate of corner 2 - * @param z2 Z co-ordinate of corner 2 - */ - public Cuboid(World world, int x1, int y1, int z1, int x2, int y2, int z2) { - this.worldName = world.getName(); - this.x1 = Math.min(x1, x2); - this.x2 = Math.max(x1, x2); - this.y1 = Math.min(y1, y2); - this.y2 = Math.max(y1, y2); - this.z1 = Math.min(z1, z2); - this.z2 = Math.max(z1, z2); - } - - /** - * Construct a Cuboid in the given world name and xyz co-ordinates. - * - * @param worldName the Cuboid's world name - * @param x1 X co-ordinate of corner 1 - * @param y1 Y co-ordinate of corner 1 - * @param z1 Z co-ordinate of corner 1 - * @param x2 X co-ordinate of corner 2 - * @param y2 Y co-ordinate of corner 2 - * @param z2 Z co-ordinate of corner 2 - */ - private Cuboid(String worldName, int x1, int y1, int z1, int x2, int y2, int z2) { - this.worldName = worldName; - this.x1 = Math.min(x1, x2); - this.x2 = Math.max(x1, x2); - this.y1 = Math.min(y1, y2); - this.y2 = Math.max(y1, y2); - this.z1 = Math.min(z1, z2); - this.z2 = Math.max(z1, z2); - } - - public Cuboid(Map map) { - worldName = (String) map.get("worldName"); - x1 = (Integer) map.get("x1"); - x2 = (Integer) map.get("x2"); - y1 = (Integer) map.get("y1"); - y2 = (Integer) map.get("y2"); - z1 = (Integer) map.get("z1"); - z2 = (Integer) map.get("z2"); - } - - public KList getEntities() { - KList en = new KList<>(); - - for (Chunk i : getChunks()) { - for (Entity j : i.getEntities()) { - if (contains(j.getLocation())) { - en.add(j); - } - } - } - - return en; - } - - /** - * Set the locations - * - * @param l1 a - * @param l2 b - */ - public void set(Location l1, Location l2) { - x1 = Math.min(l1.getBlockX(), l2.getBlockX()); - y1 = Math.min(l1.getBlockY(), l2.getBlockY()); - z1 = Math.min(l1.getBlockZ(), l2.getBlockZ()); - x2 = Math.max(l1.getBlockX(), l2.getBlockX()); - y2 = Math.max(l1.getBlockY(), l2.getBlockY()); - z2 = Math.max(l1.getBlockZ(), l2.getBlockZ()); - } - - @Override - public Map serialize() { - Map map = new HashMap<>(); - map.put("worldName", worldName); - map.put("x1", x1); - map.put("y1", y1); - map.put("z1", z1); - map.put("x2", x2); - map.put("y2", y2); - map.put("z2", z2); - return map; - } - - public Cuboid flatten(int level) { - return new Cuboid(getWorld(), x1, level, z1, x2, level, z2); - } - - /** - * Get the Location of the lower northeast corner of the Cuboid (minimum XYZ - * co-ordinates). - * - * @return Location of the lower northeast corner - */ - public Location getLowerNE() { - return new Location(getWorld(), x1, y1, z1); - } - - /** - * Get the Location of the upper southwest corner of the Cuboid (maximum XYZ - * co-ordinates). - * - * @return Location of the upper southwest corner - */ - public Location getUpperSW() { - return new Location(getWorld(), x2, y2, z2); - } - - /** - * Get the the centre of the Cuboid - * - * @return Location at the centre of the Cuboid - */ - public Location getCenter() { - int x1 = getUpperX() + 1; - int y1 = getUpperY() + 1; - int z1 = getUpperZ() + 1; - return new Location(getWorld(), getLowerX() + (x1 - getLowerX()) / 2.0, getLowerY() + (y1 - getLowerY()) / 2.0, getLowerZ() + (z1 - getLowerZ()) / 2.0); - } - - /** - * Get the Cuboid's world. - * - * @return the World object representing this Cuboid's world - * @throws IllegalStateException if the world is not loaded - */ - public World getWorld() { - World world = Bukkit.getWorld(worldName); - if (world == null) { - throw new IllegalStateException("world '" + worldName + "' is not loaded"); - } - return world; - } - - /** - * Get the size of this Cuboid along the X axis - * - * @return Size of Cuboid along the X axis - */ - public int getSizeX() { - return (x2 - x1) + 1; - } - - /** - * Get the size of this Cuboid along the Y axis - * - * @return Size of Cuboid along the Y axis - */ - public int getSizeY() { - return (y2 - y1) + 1; - } - - /** - * Get the size of this Cuboid along the Z axis - * - * @return Size of Cuboid along the Z axis - */ - public int getSizeZ() { - return (z2 - z1) + 1; - } - - /** - * Get the cuboid dimensions - * - * @return the dimensions - */ - public Dimension getDimension() { - return new Dimension(getSizeX(), getSizeY(), getSizeZ()); - } - - /** - * Get the minimum X co-ordinate of this Cuboid - * - * @return the minimum X co-ordinate - */ - public int getLowerX() { - return x1; - } - - /** - * Get the minimum Y co-ordinate of this Cuboid - * - * @return the minimum Y co-ordinate - */ - public int getLowerY() { - return y1; - } - - /** - * Get the minimum Z co-ordinate of this Cuboid - * - * @return the minimum Z co-ordinate - */ - public int getLowerZ() { - return z1; - } - - /** - * Get the maximum X co-ordinate of this Cuboid - * - * @return the maximum X co-ordinate - */ - public int getUpperX() { - return x2; - } - - /** - * Get the maximum Y co-ordinate of this Cuboid - * - * @return the maximum Y co-ordinate - */ - public int getUpperY() { - return y2; - } - - /** - * Get the maximum Z co-ordinate of this Cuboid - * - * @return the maximum Z co-ordinate - */ - public int getUpperZ() { - return z2; - } - - /** - * Get the Blocks at the eight corners of the Cuboid. - * - * @return array of Block objects representing the Cuboid corners - */ - public Block[] corners() { - Block[] res = new Block[8]; - World w = getWorld(); - res[0] = w.getBlockAt(x1, y1, z1); - res[1] = w.getBlockAt(x1, y1, z2); - res[2] = w.getBlockAt(x1, y2, z1); - res[3] = w.getBlockAt(x1, y2, z2); - res[4] = w.getBlockAt(x2, y1, z1); - res[5] = w.getBlockAt(x2, y1, z2); - res[6] = w.getBlockAt(x2, y2, z1); - res[7] = w.getBlockAt(x2, y2, z2); - return res; - } - - /** - * Expand the Cuboid in the given direction by the given amount. Negative - * amounts will shrink the Cuboid in the given direction. Shrinking a cuboid's - * face past the opposite face is not an error and will return a valid Cuboid. - * - * @param dir the direction in which to expand - * @param amount the number of blocks by which to expand - * @return a new Cuboid expanded by the given direction and amount - */ - public Cuboid expand(CuboidDirection dir, int amount) { - return switch (dir) { - case North -> new Cuboid(worldName, x1 - amount, y1, z1, x2, y2, z2); - case South -> new Cuboid(worldName, x1, y1, z1, x2 + amount, y2, z2); - case East -> new Cuboid(worldName, x1, y1, z1 - amount, x2, y2, z2); - case West -> new Cuboid(worldName, x1, y1, z1, x2, y2, z2 + amount); - case Down -> new Cuboid(worldName, x1, y1 - amount, z1, x2, y2, z2); - case Up -> new Cuboid(worldName, x1, y1, z1, x2, y2 + amount, z2); - default -> throw new IllegalArgumentException("invalid direction " + dir); - }; - } - - public Cuboid expand(Direction dir, int amount) { - int ax = dir.toVector().getBlockX() == 1 ? amount : 0; - int sx = dir.toVector().getBlockX() == -1 ? -amount : 0; - int ay = dir.toVector().getBlockY() == 1 ? amount : 0; - int sy = dir.toVector().getBlockY() == -1 ? -amount : 0; - int az = dir.toVector().getBlockZ() == 1 ? amount : 0; - int sz = dir.toVector().getBlockZ() == -1 ? -amount : 0; - return new Cuboid(worldName, x1 + sx, y1 + sy, z1 + sz, x2 + ax, y2 + ay, z2 + az); - } - - /** - * Shift the Cuboid in the given direction by the given amount. - * - * @param dir the direction in which to shift - * @param amount the number of blocks by which to shift - * @return a new Cuboid shifted by the given direction and amount - */ - public Cuboid shift(CuboidDirection dir, int amount) { - return expand(dir, amount).expand(dir.opposite(), -amount); - } - - /** - * Outset (grow) the Cuboid in the given direction by the given amount. - * - * @param dir the direction in which to outset (must be Horizontal, Vertical, or - * Both) - * @param amount the number of blocks by which to outset - * @return a new Cuboid outset by the given direction and amount - */ - public Cuboid outset(CuboidDirection dir, int amount) { - Cuboid c = switch (dir) { - case Horizontal -> - expand(CuboidDirection.North, amount).expand(CuboidDirection.South, amount).expand(CuboidDirection.East, amount).expand(CuboidDirection.West, amount); - case Vertical -> expand(CuboidDirection.Down, amount).expand(CuboidDirection.Up, amount); - case Both -> outset(CuboidDirection.Horizontal, amount).outset(CuboidDirection.Vertical, amount); - default -> throw new IllegalArgumentException("invalid direction " + dir); - }; - return c; - } - - /** - * Inset (shrink) the Cuboid in the given direction by the given amount. - * Equivalent to calling outset() with a negative amount. - * - * @param dir the direction in which to inset (must be Horizontal, Vertical, or - * Both) - * @param amount the number of blocks by which to inset - * @return a new Cuboid inset by the given direction and amount - */ - public Cuboid inset(CuboidDirection dir, int amount) { - return outset(dir, -amount); - } - - /** - * Return true if the point at (x,y,z) is contained within this Cuboid. - * - * @param x the X co-ordinate - * @param y the Y co-ordinate - * @param z the Z co-ordinate - * @return true if the given point is within this Cuboid, false otherwise - */ - public boolean contains(int x, int y, int z) { - return x >= x1 && x <= x2 && y >= y1 && y <= y2 && z >= z1 && z <= z2; - } - - /** - * Check if the given Block is contained within this Cuboid. - * - * @param b the Block to check for - * @return true if the Block is within this Cuboid, false otherwise - */ - public boolean contains(Block b) { - return contains(b.getLocation()); - } - - /** - * Check if the given Location is contained within this Cuboid. - * - * @param l the Location to check for - * @return true if the Location is within this Cuboid, false otherwise - */ - public boolean contains(Location l) { - return worldName.equals(l.getWorld().getName()) && contains(l.getBlockX(), l.getBlockY(), l.getBlockZ()); - } - - /** - * Get the volume of this Cuboid. - * - * @return the Cuboid volume, in blocks - */ - public int volume() { - return getSizeX() * getSizeY() * getSizeZ(); - } - - /** - * Get the average light level of all empty (air) blocks in the Cuboid. Returns - * 0 if there are no empty blocks. - * - * @return the average light level of this Cuboid - */ - public byte averageLightLevel() { - long total = 0; - int n = 0; - for (Block b : this) { - if (b.isEmpty()) { - total += b.getLightLevel(); - ++n; - } - } - return n > 0 ? (byte) (total / n) : 0; - } - - /** - * Contract the Cuboid, returning a Cuboid with any air around the edges - * removed, just large enough to include all non-air blocks. - * - * @return a new Cuboid with no external air blocks - */ - public Cuboid contract() { - return this.contract(CuboidDirection.Down).contract(CuboidDirection.South).contract(CuboidDirection.East).contract(CuboidDirection.Up).contract(CuboidDirection.North).contract(CuboidDirection.West); - } - - /** - * Contract the Cuboid in the given direction, returning a new Cuboid which has - * no exterior empty space. E.g. a direction of Down will push the top face - * downwards as much as possible. - * - * @param dir the direction in which to contract - * @return a new Cuboid contracted in the given direction - */ - public Cuboid contract(CuboidDirection dir) { - Cuboid face = getFace(dir.opposite()); - switch (dir) { - case Down -> { - while (face.containsOnly(Material.AIR) && face.getLowerY() > this.getLowerY()) { - face = face.shift(CuboidDirection.Down, 1); - } - return new Cuboid(worldName, x1, y1, z1, x2, face.getUpperY(), z2); - } - case Up -> { - while (face.containsOnly(Material.AIR) && face.getUpperY() < this.getUpperY()) { - face = face.shift(CuboidDirection.Up, 1); - } - return new Cuboid(worldName, x1, face.getLowerY(), z1, x2, y2, z2); - } - case North -> { - while (face.containsOnly(Material.AIR) && face.getLowerX() > this.getLowerX()) { - face = face.shift(CuboidDirection.North, 1); - } - return new Cuboid(worldName, x1, y1, z1, face.getUpperX(), y2, z2); - } - case South -> { - while (face.containsOnly(Material.AIR) && face.getUpperX() < this.getUpperX()) { - face = face.shift(CuboidDirection.South, 1); - } - return new Cuboid(worldName, face.getLowerX(), y1, z1, x2, y2, z2); - } - case East -> { - while (face.containsOnly(Material.AIR) && face.getLowerZ() > this.getLowerZ()) { - face = face.shift(CuboidDirection.East, 1); - } - return new Cuboid(worldName, x1, y1, z1, x2, y2, face.getUpperZ()); - } - case West -> { - while (face.containsOnly(Material.AIR) && face.getUpperZ() < this.getUpperZ()) { - face = face.shift(CuboidDirection.West, 1); - } - return new Cuboid(worldName, x1, y1, face.getLowerZ(), x2, y2, z2); - } - default -> throw new IllegalArgumentException("Invalid direction " + dir); - } - } - - /** - * Get the Cuboid representing the face of this Cuboid. The resulting Cuboid - * will be one block thick in the axis perpendicular to the requested face. - * - * @param dir which face of the Cuboid to get - * @return the Cuboid representing this Cuboid's requested face - */ - public Cuboid getFace(CuboidDirection dir) { - return switch (dir) { - case Down -> new Cuboid(worldName, x1, y1, z1, x2, y1, z2); - case Up -> new Cuboid(worldName, x1, y2, z1, x2, y2, z2); - case North -> new Cuboid(worldName, x1, y1, z1, x1, y2, z2); - case South -> new Cuboid(worldName, x2, y1, z1, x2, y2, z2); - case East -> new Cuboid(worldName, x1, y1, z1, x2, y2, z1); - case West -> new Cuboid(worldName, x1, y1, z2, x2, y2, z2); - default -> throw new IllegalArgumentException("Invalid direction " + dir); - }; - } - - /** - * Check if the Cuboid contains only blocks of the given type - * - * @param material the material to check for - * @return true if this Cuboid contains only blocks of the given type - */ - public boolean containsOnly(Material material) { - for (Block b : this) { - if (b.getType() != material) { - return false; - } - } - return true; - } - - /** - * Get the Cuboid big enough to hold both this Cuboid and the given one. - * - * @param other the other Cuboid to include - * @return a new Cuboid large enough to hold this Cuboid and the given Cuboid - */ - public Cuboid getBoundingCuboid(Cuboid other) { - if (other == null) { - return this; - } - - int xMin = Math.min(getLowerX(), other.getLowerX()); - int yMin = Math.min(getLowerY(), other.getLowerY()); - int zMin = Math.min(getLowerZ(), other.getLowerZ()); - int xMax = Math.max(getUpperX(), other.getUpperX()); - int yMax = Math.max(getUpperY(), other.getUpperY()); - int zMax = Math.max(getUpperZ(), other.getUpperZ()); - - return new Cuboid(worldName, xMin, yMin, zMin, xMax, yMax, zMax); - } - - /** - * Get a block relative to the lower NE point of the Cuboid. - * - * @param x the X co-ordinate - * @param y the Y co-ordinate - * @param z the Z co-ordinate - * @return the block at the given position - */ - public Block getRelativeBlock(int x, int y, int z) { - return getWorld().getBlockAt(x1 + x, y1 + y, z1 + z); - } - - /** - * Get a block relative to the lower NE point of the Cuboid in the given World. - * This version of getRelativeBlock() should be used if being called many times, - * to avoid excessive calls to getWorld(). - * - * @param w the World - * @param x the X co-ordinate - * @param y the Y co-ordinate - * @param z the Z co-ordinate - * @return the block at the given position - */ - public Block getRelativeBlock(World w, int x, int y, int z) { - return w.getBlockAt(x1 + x, y1 + y, z1 + z); - } - - /** - * Get a list of the chunks which are fully or partially contained in this - * cuboid. - * - * @return a list of Chunk objects - */ - public List getChunks() { - List res = new ArrayList<>(); - - World w = getWorld(); - int x1 = getLowerX() & ~0xf; - int x2 = getUpperX() & ~0xf; - int z1 = getLowerZ() & ~0xf; - int z2 = getUpperZ() & ~0xf; - for (int x = x1; x <= x2; x += 16) { - for (int z = z1; z <= z2; z += 16) { - res.add(w.getChunkAt(x >> 4, z >> 4)); - } - } - return res; - } - - /* - Set all the blocks within the Cuboid to the given MaterialData, using a - MassBlockUpdate object for fast updates. - - @param mat - * the MaterialData to set - * @param mbu - * the MassBlockUpdate object - */ - - /** - * Reset the light level of all blocks within this Cuboid. - */ - - /* - * (non-Javadoc) - * - * @see java.lang.Iterable#iterator() - */ - @Override - public Iterator iterator() { - return new CuboidIterator(getWorld(), x1, y1, z1, x2, y2, z2); - } - - public Iterator chunkedIterator() { - return new ChunkedCuboidIterator(getWorld(), x1, y1, z1, x2, y2, z2); - } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#clone() - */ - @SuppressWarnings("MethodDoesntCallSuperMethod") - @Override - public Cuboid clone() { - return new Cuboid(this); - } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return "Cuboid: " + worldName + "," + x1 + "," + y1 + "," + z1 + "=>" + x2 + "," + y2 + "," + z2; - } - - public enum CuboidDirection { - - North, - East, - South, - West, - Up, - Down, - Horizontal, - Vertical, - Both, - Unknown; - - public CuboidDirection opposite() { - return switch (this) { - case North -> South; - case East -> West; - case South -> North; - case West -> East; - case Horizontal -> Vertical; - case Vertical -> Horizontal; - case Up -> Down; - case Down -> Up; - case Both -> Both; - default -> Unknown; - }; - } - } - - public static class CuboidIterator implements Iterator { - private final World w; - private final int baseX; - private final int baseY; - private final int baseZ; - private final int sizeX; - private final int sizeY; - private final int sizeZ; - private int x, y, z; - - public CuboidIterator(World w, int x1, int y1, int z1, int x2, int y2, int z2) { - this.w = w; - baseX = x1; - baseY = y1; - baseZ = z1; - sizeX = Math.abs(x2 - x1) + 1; - sizeY = Math.abs(y2 - y1) + 1; - sizeZ = Math.abs(z2 - z1) + 1; - x = y = z = 0; - } - - @Override - public boolean hasNext() { - return x < sizeX && y < sizeY && z < sizeZ; - } - - @Override - public Block next() { - Block b = w.getBlockAt(baseX + x, baseY + y, baseZ + z); - if (++x >= sizeX) { - x = 0; - if (++y >= sizeY) { - y = 0; - ++z; - } - } - return b; - } - - @Override - public void remove() { - // nop - } - } - - public static class ChunkedCuboidIterator implements Iterator { - private final World w; - private final int minRX, minY, minRZ, maxRX, maxY, maxRZ; - private final int minCX, minCZ, maxCX, maxCZ; - private int mX, mZ, bX, rX, rZ, y; - - private Position2 chunk; - private int cX, cZ; - - public ChunkedCuboidIterator(World w, int x1, int y1, int z1, int x2, int y2, int z2) { - this.w = w; - minY = Math.min(y1, y2); - maxY = Math.max(y1, y2); - int minX = Math.min(x1, x2); - int minZ = Math.min(z1, z2); - int maxX = Math.max(x1, x2); - int maxZ = Math.max(z1, z2); - minRX = minX & 15; - minRZ = minZ & 15; - maxRX = maxX & 15; - maxRZ = maxZ & 15; - - minCX = minX >> 4; - minCZ = minZ >> 4; - maxCX = maxX >> 4; - maxCZ = maxZ >> 4; - cX = minCX; - cZ = minCZ; - - rX = minX & 15; - rZ = minZ & 15; - y = minY; - } - - @Override - public boolean hasNext() { - return chunk != null || hasNextChunk(); - } - - public boolean hasNextChunk() { - return cX <= maxCX && cZ <= maxCZ; - } - - @Override - public Block next() { - if (chunk == null) { - chunk = new Position2(cX, cZ); - if (++cX > maxCX) { - cX = minCX; - cZ++; - } - - mX = chunk.getX() == maxCX ? maxRX : 15; - mZ = chunk.getZ() == maxCZ ? maxRZ : 15; - rX = bX = chunk.getX() == minCX ? minRX : 0; - rZ = chunk.getZ() == minCZ ? minRZ : 0; - } - - var b = w.getBlockAt((chunk.getX() << 4) + rX, y, (chunk.getZ() << 4) + rZ); - if (++y > maxY) { - y = minY; - if (++rX > mX) { - if (++rZ > mZ) { - chunk = null; - return b; - } - rX = bX; - } - } - - return b; - } - - @Override - public void remove() { - // nop - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/CuboidException.java b/core/src/main/java/com/volmit/iris/util/data/CuboidException.java deleted file mode 100644 index 990cefd45..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/CuboidException.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -/** - * Represents a cuboid exception - * - * @author cyberpwn - */ -public class CuboidException extends Exception { - private static final long serialVersionUID = 1L; - - public CuboidException(String string) { - super(string); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/DUTF.java b/core/src/main/java/com/volmit/iris/util/data/DUTF.java deleted file mode 100644 index b48bb7e7e..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/DUTF.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -/** - *

Encodes signed and unsigned values using a common variable-length - * scheme, found for example in - * - * Google's Protocol Buffers. It uses fewer bytes to encode smaller values, - * but will use slightly more bytes to encode large values.

- *

- *

Signed values are further encoded using so-called zig-zag encoding - * in order to make them "compatible" with variable-length encoding.

- */ -public final class DUTF { - - private DUTF() { - } - - public static void write(String s, DataOutputStream dos) throws IOException { - byte[] b = s.getBytes(StandardCharsets.UTF_8); - dos.writeShort(b.length); - dos.write(b); - } - - public static String read(DataInputStream din) throws IOException { - byte[] d = new byte[din.readShort()]; - din.read(d); - return new String(d, StandardCharsets.UTF_8); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/DataPalette.java b/core/src/main/java/com/volmit/iris/util/data/DataPalette.java deleted file mode 100644 index dcdfc546c..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/DataPalette.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.util.collection.KList; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class DataPalette { - private final KList palette; - - public DataPalette() { - this(new KList<>(16)); - } - - public DataPalette(KList palette) { - this.palette = palette; - } - - public static DataPalette getPalette(IOAdapter adapter, DataInputStream din) throws IOException { - KList palette = new KList<>(); - int s = din.readShort() - Short.MIN_VALUE; - - for (int i = 0; i < s; i++) { - palette.add(adapter.read(din)); - } - - return new DataPalette<>(palette); - } - - public KList getPalette() { - return palette; - } - - public T get(int index) { - synchronized (palette) { - if (!palette.hasIndex(index)) { - return null; - } - - return palette.get(index); - } - } - - public int getIndex(T t) { - int v = 0; - - synchronized (palette) { - v = palette.indexOf(t); - - if (v == -1) { - v = palette.size(); - palette.add(t); - } - } - - return v; - } - - public void write(IOAdapter adapter, DataOutputStream dos) throws IOException { - synchronized (palette) { - dos.writeShort(getPalette().size() + Short.MIN_VALUE); - - for (T t : palette) { - adapter.write(t, dos); - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/Dimension.java b/core/src/main/java/com/volmit/iris/util/data/Dimension.java deleted file mode 100644 index fac2bcb7f..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/Dimension.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -/** - * Dimensions - * - * @author cyberpwn - */ -public class Dimension { - private final int width; - private final int height; - private final int depth; - - /** - * Make a dimension - * - * @param width width of this (X) - * @param height the height (Y) - * @param depth the depth (Z) - */ - public Dimension(int width, int height, int depth) { - this.width = width; - this.height = height; - this.depth = depth; - } - - /** - * Make a dimension - * - * @param width width of this (X) - * @param height the height (Y) - */ - public Dimension(int width, int height) { - this.width = width; - this.height = height; - this.depth = 0; - } - - /** - * Get the direction of the flat part of this dimension (null if no thin - * face) - * - * @return the direction of the flat pane or null - */ - public DimensionFace getPane() { - if (width == 1) { - return DimensionFace.X; - } - - if (height == 1) { - return DimensionFace.Y; - } - - if (depth == 1) { - return DimensionFace.Z; - } - - return null; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public int getDepth() { - return depth; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/DimensionFace.java b/core/src/main/java/com/volmit/iris/util/data/DimensionFace.java deleted file mode 100644 index 500c76526..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/DimensionFace.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -/** - * Represents a dimension (coordinates not worlds) - * - * @author cyberpwn - */ -public enum DimensionFace { - /** - * The X dimension (width) - */ - X, - - /** - * The Y dimension (height) - */ - Y, - - /** - * The Z dimension (depth) - */ - Z -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/DoubleArrayUtils.java b/core/src/main/java/com/volmit/iris/util/data/DoubleArrayUtils.java deleted file mode 100644 index 11de75200..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/DoubleArrayUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - - -import com.google.common.util.concurrent.AtomicDoubleArray; - -import java.util.Arrays; - -public class DoubleArrayUtils { - public static void shiftRight(double[] values, double push) { - if (values.length - 2 + 1 >= 0) System.arraycopy(values, 0, values, 1, values.length - 2 + 1); - - values[0] = push; - } - - public static void wrapRight(double[] values) { - double last = values[values.length - 1]; - shiftRight(values, last); - } - - public static void fill(double[] values, double value) { - Arrays.fill(values, value); - } - - public static void fill(AtomicDoubleArray values, double value) { - for (int i = 0; i < values.length(); i++) { - values.set(i, value); - } - } - -} diff --git a/core/src/main/java/com/volmit/iris/util/data/Heafty.java b/core/src/main/java/com/volmit/iris/util/data/Heafty.java deleted file mode 100644 index 4e0cce01e..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/Heafty.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -@FunctionalInterface -public interface Heafty { - int getHeaft(); -} diff --git a/core/src/main/java/com/volmit/iris/util/data/HeightMap.java b/core/src/main/java/com/volmit/iris/util/data/HeightMap.java deleted file mode 100644 index 5b0a78f08..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/HeightMap.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import java.util.Arrays; - -public class HeightMap { - private final int[] height; - - public HeightMap() { - height = new int[256]; - Arrays.fill(height, 0); - } - - public void setHeight(int x, int z, int h) { - height[x * 16 + z] = (h); - } - - public int getHeight(int x, int z) { - return height[x * 16 + z]; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/IOAdapter.java b/core/src/main/java/com/volmit/iris/util/data/IOAdapter.java deleted file mode 100644 index 6d9903ff8..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/IOAdapter.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public interface IOAdapter { - void write(T t, DataOutputStream dos) throws IOException; - - T read(DataInputStream din) throws IOException; -} diff --git a/core/src/main/java/com/volmit/iris/util/data/InvertedBiomeGrid.java b/core/src/main/java/com/volmit/iris/util/data/InvertedBiomeGrid.java deleted file mode 100644 index 27e5d110b..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/InvertedBiomeGrid.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import org.bukkit.block.Biome; -import org.bukkit.generator.ChunkGenerator.BiomeGrid; - -public class InvertedBiomeGrid implements BiomeGrid { - private final BiomeGrid grid; - - public InvertedBiomeGrid(BiomeGrid real) { - this.grid = real; - } - - - @SuppressWarnings("deprecation") - @Override - public Biome getBiome(int arg0, int arg1) { - return grid.getBiome(arg0, arg1); - } - - - @Override - public Biome getBiome(int arg0, int arg1, int arg2) { - return grid.getBiome(arg0, 255 - arg1, arg2); - } - - @SuppressWarnings("deprecation") - @Override - public void setBiome(int arg0, int arg1, Biome arg2) { - grid.setBiome(arg0, arg1, arg2); - } - - @Override - public void setBiome(int arg0, int arg1, int arg2, Biome arg3) { - grid.setBiome(arg0, 255 - arg1, arg2, arg3); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/IrisBiomeStorage.java b/core/src/main/java/com/volmit/iris/util/data/IrisBiomeStorage.java deleted file mode 100644 index 2dc81c542..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/IrisBiomeStorage.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.util.math.IrisMathHelper; -import org.bukkit.block.Biome; -import org.bukkit.generator.ChunkGenerator.BiomeGrid; -import org.jetbrains.annotations.NotNull; - -public class IrisBiomeStorage implements BiomeGrid { - public static final int a; - public static final int b; - public static final int c; - private static final int e; - private static final int f; - - static { - e = (int) Math.round(Math.log(16.0) / Math.log(2.0)) - 2; - f = (int) Math.round(Math.log(256.0) / Math.log(2.0)) - 2; // TODO: WARNING HEIGHT - a = 1 << IrisBiomeStorage.e + IrisBiomeStorage.e + IrisBiomeStorage.f; - b = (1 << IrisBiomeStorage.e) - 1; - c = (1 << IrisBiomeStorage.f) - 1; - } - - private final Biome[] g; - - public IrisBiomeStorage(final Biome[] aBiome) { - this.g = aBiome; - } - - public IrisBiomeStorage() { - this(new Biome[IrisBiomeStorage.a]); - } - - public IrisBiomeStorage b() { - return new IrisBiomeStorage(this.g.clone()); - } - - public void inject(BiomeGrid grid) { - // TODO: WARNING HEIGHT - for (int i = 0; i < 256; i++) { - for (int j = 0; j < 16; j++) { - for (int k = 0; k < 16; k++) { - Biome b = getBiome(j, i, k); - - if (b == null || b.equals(Biome.THE_VOID)) { - continue; - } - - grid.setBiome(j, i, k, b); - } - } - } - } - - @NotNull - @Override - public Biome getBiome(int x, int z) { - return null; - } - - public Biome getBiome(final int x, final int y, final int z) { - final int l = x & IrisBiomeStorage.b; - final int i2 = IrisMathHelper.clamp(y, 0, IrisBiomeStorage.c); - final int j2 = z & IrisBiomeStorage.b; - return this.g[i2 << IrisBiomeStorage.e + IrisBiomeStorage.e | j2 << IrisBiomeStorage.e | l]; - } - - @Override - public void setBiome(int x, int z, @NotNull Biome bio) { - - } - - public void setBiome(final int x, final int y, final int z, final Biome biome) { - final int l = x & IrisBiomeStorage.b; - final int i2 = IrisMathHelper.clamp(y, 0, IrisBiomeStorage.c); - final int j2 = z & IrisBiomeStorage.b; - this.g[i2 << IrisBiomeStorage.e + IrisBiomeStorage.e | j2 << IrisBiomeStorage.e | l] = biome; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/KCache.java b/core/src/main/java/com/volmit/iris/util/data/KCache.java deleted file mode 100644 index 0226dedb7..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/KCache.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.github.benmanes.caffeine.cache.CacheLoader; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.LoadingCache; -import com.github.benmanes.caffeine.cache.Scheduler; -import com.volmit.iris.engine.framework.MeteredCache; -import com.volmit.iris.util.math.RollingSequence; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class KCache implements MeteredCache { - public static final ExecutorService EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); - - private final long max; - private final LoadingCache cache; - private final boolean fastDump; - private final RollingSequence msu = new RollingSequence(100); - private CacheLoader loader; - - public KCache(CacheLoader loader, long max) { - this(loader, max, false); - } - - public KCache(CacheLoader loader, long max, boolean fastDump) { - this.max = max; - this.fastDump = fastDump; - this.loader = loader; - this.cache = create(loader); - } - - private LoadingCache create(CacheLoader loader) { - return Caffeine - .newBuilder() - .maximumSize(max) - .scheduler(Scheduler.systemScheduler()) - .executor(EXECUTOR) - .initialCapacity((int) (max)) - .build((k) -> loader == null ? null : loader.load(k)); - } - - - public void setLoader(CacheLoader loader) { - this.loader = loader; - } - - public void invalidate(K k) { - cache.invalidate(k); - } - - public void invalidate() { - cache.invalidateAll(); - } - - public V get(K k) { - return cache.get(k); - } - - @Override - public long getSize() { - return cache.estimatedSize(); - } - - @Override - public KCache getRawCache() { - return this; - } - - @Override - public long getMaxSize() { - return max; - } - - @Override - public boolean isClosed() { - return false; - } - - public boolean contains(K next) { - return cache.getIfPresent(next) != null; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/MaterialBlock.java b/core/src/main/java/com/volmit/iris/util/data/MaterialBlock.java deleted file mode 100644 index dd6a3b580..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/MaterialBlock.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockState; - -/** - * Material blocks - * - * @author cyberpwn - */ -@SuppressWarnings("deprecation") -public class MaterialBlock { - private Material material; - private Byte data; - - /** - * Create a materialblock - * - * @param material the material - * @param data the data - */ - public MaterialBlock(Material material, Byte data) { - this.material = material; - this.data = data; - } - - public MaterialBlock(Material material) { - this.material = material; - data = 0; - } - - public MaterialBlock(Location location) { - this(location.getBlock()); - } - - public MaterialBlock(BlockState state) { - material = state.getType(); - data = state.getData().getData(); - } - - public MaterialBlock(Block block) { - material = block.getType(); - data = block.getData(); - } - - public MaterialBlock() { - material = Material.AIR; - data = 0; - } - - public Material getMaterial() { - return material; - } - - public void setMaterial(Material material) { - this.material = material; - } - - public Byte getData() { - return data; - } - - public void setData(Byte data) { - this.data = data; - } - - @Override - public String toString() { - if (getData() == 0) { - return getMaterial().toString(); - } - - return getMaterial().toString() + ":" + getData(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((data == null) ? 0 : data.hashCode()); - result = prime * result + ((material == null) ? 0 : material.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - MaterialBlock other = (MaterialBlock) obj; - if (data == null) { - if (other.data != null) { - return false; - } - } else if (!data.equals(other.data)) { - return false; - } - return material == other.material; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/NibbleArray.java b/core/src/main/java/com/volmit/iris/util/data/NibbleArray.java deleted file mode 100644 index 33dcc6585..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/NibbleArray.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.nio.ByteOrder; -import java.util.Arrays; -import java.util.StringJoiner; - -public class NibbleArray implements Writable { - private static final int[] MASKS = new int[8]; - - static { - for (int i = 0; i < MASKS.length; i++) { - MASKS[i] = maskFor(i); - } - } - - private final int size; - private final Object lock = new Object(); - private byte[] data; - private int depth; - private byte mask; - private transient int bitIndex, byteIndex, bitInByte; - - public NibbleArray(int capacity, DataInputStream in) throws IOException { - size = capacity; - read(in); - } - - public NibbleArray(int nibbleDepth, int capacity) { - if (nibbleDepth > 8 || nibbleDepth < 1) { - throw new IllegalArgumentException(); - } - - int neededBits = nibbleDepth * capacity; - - size = capacity; - depth = nibbleDepth; - data = new byte[(neededBits + neededBits % 8) / 8]; - mask = (byte) maskFor(nibbleDepth); - } - - public NibbleArray(int nibbleDepth, int capacity, NibbleArray existing) { - if (nibbleDepth > 8 || nibbleDepth < 1) { - throw new IllegalArgumentException(); - } - - int neededBits = nibbleDepth * capacity; - size = capacity; - depth = nibbleDepth; - data = new byte[(neededBits + neededBits % 8) / 8]; - mask = (byte) maskFor(nibbleDepth); - - for (int i = 0; i < Math.min(size, existing.size()); i++) { - set(i, existing.get(i)); - } - } - - public static int maskFor(int amountOfBits) { - return powerOfTwo(amountOfBits) - 1; - } - - public static int powerOfTwo(int power) { - int result = 1; - - for (int i = 0; i < power; i++) { - result *= 2; - } - - return result; - } - - public static String binaryString(byte b, ByteOrder byteOrder) { - String str = String.format("%8s", Integer.toBinaryString(b & 0xff)).replace(' ', '0'); - - return byteOrder.equals(ByteOrder.BIG_ENDIAN) ? str : reverse(str); - } - - public static String reverse(String str) { - return new StringBuilder(str).reverse().toString(); - } - - @Override - public void write(DataOutputStream o) throws IOException { - o.writeByte(depth + Byte.MIN_VALUE); - o.write(data); - } - - @Override - public void read(DataInputStream i) throws IOException { - depth = i.readByte() - Byte.MIN_VALUE; - int neededBits = depth * size; - data = new byte[(neededBits + neededBits % 8) / 8]; - mask = (byte) maskFor(depth); - i.read(data); - } - - public int size() { - return size; - } - - public byte get(int index) { - synchronized (lock) { - bitIndex = index * depth; - byteIndex = bitIndex >> 3; - bitInByte = bitIndex & 7; - int value = data[byteIndex] >> bitInByte; - - if (bitInByte + depth > 8) { - value |= data[byteIndex + 1] << bitInByte; - } - - return (byte) (value & mask); - } - } - - public byte get(int x, int y, int z) { - return get(index(x, y, z)); - } - - public int index(int x, int y, int z) { - return y << 8 | z << 4 | x; - } - - public void set(int x, int y, int z, int nibble) { - set(index(x, y, z), nibble); - } - - public void set(int index, int nibble) { - set(index, (byte) nibble); - } - - public void set(int index, byte nybble) { - synchronized (lock) { - bitIndex = index * depth; - byteIndex = bitIndex >> 3; - bitInByte = bitIndex & 7; - data[byteIndex] = (byte) (((~(data[byteIndex] & (mask << bitInByte)) & data[byteIndex]) | ((nybble & mask) << bitInByte)) & 0xff); - - if (bitInByte + depth > 8) { - data[byteIndex + 1] = (byte) (((~(data[byteIndex + 1] & MASKS[bitInByte + depth - 8]) & data[byteIndex + 1]) | ((nybble & mask) >> (8 - bitInByte))) & 0xff); - } - } - } - - public String toBitsString() { - return toBitsString(ByteOrder.BIG_ENDIAN); - } - - public String toBitsString(ByteOrder byteOrder) { - StringJoiner joiner = new StringJoiner(" "); - - for (byte datum : data) { - joiner.add(binaryString(datum, byteOrder)); - } - - return joiner.toString(); - } - - public void clear() { - Arrays.fill(data, (byte) 0); - } - - public void setAll(byte nibble) { - for (int i = 0; i < size; i++) { - set(i, nibble); - } - } - - public void setAll(int nibble) { - for (int i = 0; i < size; i++) { - set(i, (byte) nibble); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/NibbleDataPalette.java b/core/src/main/java/com/volmit/iris/util/data/NibbleDataPalette.java deleted file mode 100644 index 09fb3954a..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/NibbleDataPalette.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.util.collection.KList; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public abstract class NibbleDataPalette implements Writable { - private static final int DEFAULT_BITS_PER_BLOCK = 4; - private static final int CAPACITY = 4096; - private int bpb; - private NibbleArray data; - private KList palette; - - public NibbleDataPalette(T defaultValue) { - palette = new KList<>(); - bpb = DEFAULT_BITS_PER_BLOCK; - data = new NibbleArray(bpb, CAPACITY); - data.setAll(Byte.MIN_VALUE); - getPaletteId(defaultValue); - } - - public abstract T readType(DataInputStream i) throws IOException; - - public abstract void writeType(T t, DataOutputStream o) throws IOException; - - @Override - public void write(DataOutputStream o) throws IOException { - o.writeByte(bpb + Byte.MIN_VALUE); - o.writeByte(palette.size() + Byte.MIN_VALUE); - - for (T i : palette) { - writeType(i, o); - } - - data.write(o); - } - - @Override - public void read(DataInputStream i) throws IOException { - bpb = i.readByte() - Byte.MIN_VALUE; - palette = new KList<>(); - int v = i.readByte() - Byte.MIN_VALUE; - - for (int j = 0; j < v; j++) { - palette.add(readType(i)); - } - - data = new NibbleArray(CAPACITY, i); - } - - private void expand() { - if (bpb < 8) { - changeBitsPerBlock(bpb + 1); - } else { - throw new IndexOutOfBoundsException("The Data Palette can only handle at most 256 block types per 16x16x16 region. We cannot use more than 8 bits per block!"); - } - } - - public final void optimize() { - int targetBits = bpb; - int needed = palette.size(); - - for (int i = 1; i < bpb; i++) { - if (Math.pow(2, i) > needed) { - targetBits = i; - break; - } - } - - changeBitsPerBlock(targetBits); - } - - private void changeBitsPerBlock(int bits) { - bpb = bits; - data = new NibbleArray(bpb, CAPACITY, data); - } - - public final void set(int x, int y, int z, T d) { - data.set(getCoordinateIndex(x, y, z), getPaletteId(d)); - } - - public final T get(int x, int y, int z) { - return palette.get(data.get(getCoordinateIndex(x, y, z))); - } - - private int getPaletteId(T d) { - int index = palette.indexOf(d); - - if (index == -1) { - index = palette.size(); - palette.add(d); - - if (palette.size() > Math.pow(2, bpb)) { - expand(); - } - } - - return index + Byte.MIN_VALUE; - } - - private int getCoordinateIndex(int x, int y, int z) { - return y << 8 | z << 4 | x; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/Recycler.java b/core/src/main/java/com/volmit/iris/util/data/Recycler.java deleted file mode 100644 index fc1455607..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/Recycler.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.volmit.iris.util.data; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; - -public class Recycler { - private final List> pool; - private final Supplier factory; - - public Recycler(Supplier factory) { - pool = new CopyOnWriteArrayList<>(); - this.factory = factory; - } - - public int getFreeObjects() { - int m = 0; - RecycledObject o; - for (RecycledObject tRecycledObject : pool) { - o = tRecycledObject; - - if (!o.isUsed()) { - m++; - } - } - - return m; - } - - public int getUsedOjects() { - int m = 0; - RecycledObject o; - for (RecycledObject tRecycledObject : pool) { - o = tRecycledObject; - - if (o.isUsed()) { - m++; - } - } - - return m; - } - - public void dealloc(T t) { - RecycledObject o; - - for (RecycledObject tRecycledObject : pool) { - o = tRecycledObject; - if (o.isUsed() && System.identityHashCode(t) == System.identityHashCode(o.getObject())) { - o.dealloc(); - return; - } - } - } - - public T alloc() { - RecycledObject o; - - for (RecycledObject tRecycledObject : pool) { - o = tRecycledObject; - if (o.alloc()) { - return o.getObject(); - } - } - - expand(); - - return alloc(); - } - - public boolean contract() { - return contract(Math.max(getFreeObjects() / 2, Runtime.getRuntime().availableProcessors())); - } - - public boolean contract(int maxFree) { - int remove = getFreeObjects() - maxFree; - - if (remove > 0) { - RecycledObject o; - - for (int i = pool.size() - 1; i > 0; i--) { - o = pool.get(i); - if (!o.isUsed()) { - pool.remove(i); - remove--; - - if (remove <= 0) { - return true; - } - } - } - } - - return false; - } - - public void expand() { - if (pool.isEmpty()) { - expand(Runtime.getRuntime().availableProcessors()); - return; - } - - expand(getUsedOjects() + Runtime.getRuntime().availableProcessors()); - } - - public void expand(int by) { - for (int i = 0; i < by; i++) { - pool.add(new RecycledObject<>(factory.get())); - } - } - - public int size() { - return pool.size(); - } - - public void deallocAll() { - pool.clear(); - } - - public static class RecycledObject { - private final T object; - private final AtomicBoolean used; - - public RecycledObject(T object) { - this.object = object; - used = new AtomicBoolean(false); - } - - public T getObject() { - return object; - } - - public boolean isUsed() { - return used.get(); - } - - public void dealloc() { - used.set(false); - } - - public boolean alloc() { - if (used.get()) { - return false; - } - - used.set(true); - return true; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/Shrinkwrap.java b/core/src/main/java/com/volmit/iris/util/data/Shrinkwrap.java deleted file mode 100644 index 32d247767..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/Shrinkwrap.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -public class Shrinkwrap { - private T t; - - public Shrinkwrap(T t) { - set(t); - } - - public Shrinkwrap() { - this(null); - } - - public T get() { - return t; - } - - public void set(T t) { - this.t = t; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/VanillaBiomeMap.java b/core/src/main/java/com/volmit/iris/util/data/VanillaBiomeMap.java deleted file mode 100644 index bd1db8325..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/VanillaBiomeMap.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.inventorygui.RandomColor.Color; -import com.volmit.iris.util.inventorygui.RandomColor.Luminosity; -import com.volmit.iris.util.inventorygui.RandomColor.SaturationType; -import org.bukkit.block.Biome; - -public class VanillaBiomeMap { - - private static final KMap BIOME_HEX = new KMap<>(); - private static final KMap BIOME_COLOR = new KMap<>(); - private static final KMap BIOME_LUMINOSITY = new KMap<>(); - private static final KMap BIOME_SATURATION = new KMap<>(); - private static final KMap BIOME_IDs = new KMap<>(); - - static { - add(Biome.OCEAN, 0x000070, Color.BLUE, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.PLAINS, 0x8DB360, Color.GREEN, Luminosity.LIGHT, SaturationType.MEDIUM); - add(Biome.DESERT, 0xFA9418, Color.YELLOW, Luminosity.LIGHT, SaturationType.MEDIUM); - add(Biome.WINDSWEPT_HILLS, 0x606060, Color.MONOCHROME, Luminosity.BRIGHT, null); - add(Biome.FOREST, 0x056621, Color.GREEN, Luminosity.BRIGHT, null); - add(Biome.TAIGA, 0x0B6659, Color.GREEN, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.SWAMP, 0x07F9B2, Color.ORANGE, Luminosity.DARK, SaturationType.MEDIUM); - add(Biome.RIVER, 0x0000FF, Color.BLUE, Luminosity.LIGHT, SaturationType.LOW); - add(Biome.NETHER_WASTES, 0xBF3B3B, Color.RED, Luminosity.LIGHT, SaturationType.MEDIUM); - add(Biome.THE_END, 0x8080FF, Color.PURPLE, Luminosity.LIGHT, SaturationType.LOW); - add(Biome.FROZEN_OCEAN, 0x7070D6, Color.BLUE, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.FROZEN_RIVER, 0xA0A0FF, Color.BLUE, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.SNOWY_PLAINS, 0xFFFFFF, Color.MONOCHROME, Luminosity.LIGHT, null); - add(Biome.MUSHROOM_FIELDS, 0xFF00FF, Color.PURPLE, Luminosity.BRIGHT, null); - add(Biome.BEACH, 0xFADE55, Color.YELLOW, Luminosity.LIGHT, SaturationType.LOW); - add(Biome.JUNGLE, 0x537B09, Color.GREEN, Luminosity.BRIGHT, SaturationType.HIGH); - add(Biome.SPARSE_JUNGLE, 0x628B17, Color.GREEN, Luminosity.BRIGHT, SaturationType.HIGH); - add(Biome.DEEP_OCEAN, 0x000030, Color.BLUE, Luminosity.DARK, null); - add(Biome.STONY_SHORE, 0xA2A284, Color.GREEN, Luminosity.DARK, null); - add(Biome.SNOWY_BEACH, 0xFAF0C0, Color.YELLOW, Luminosity.LIGHT, null); - add(Biome.BIRCH_FOREST, 0x307444, Color.GREEN, Luminosity.LIGHT, null); - add(Biome.DARK_FOREST, 0x40511A, Color.GREEN, Luminosity.DARK, null); - add(Biome.SNOWY_TAIGA, 0x31554A, Color.BLUE, Luminosity.LIGHT, null); - add(Biome.OLD_GROWTH_PINE_TAIGA, 0x596651, Color.ORANGE, Luminosity.LIGHT, null); - add(Biome.WINDSWEPT_FOREST, 0x507050, Color.MONOCHROME, Luminosity.BRIGHT, null); - add(Biome.SAVANNA, 0xBDB25F, Color.GREEN, Luminosity.LIGHT, null); - add(Biome.SAVANNA_PLATEAU, 0xA79D64, Color.GREEN, Luminosity.LIGHT, null); - add(Biome.BADLANDS, 0xD94515, Color.ORANGE, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.WOODED_BADLANDS, 0xB09765, Color.ORANGE, Luminosity.BRIGHT, SaturationType.HIGH); - add(Biome.SMALL_END_ISLANDS, 0xff1a8c, Color.PURPLE, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.END_MIDLANDS, 0x8080FF, Color.YELLOW, Luminosity.LIGHT, SaturationType.LOW); - add(Biome.END_HIGHLANDS, 0x8080FF, Color.PURPLE, Luminosity.LIGHT, SaturationType.LOW); - add(Biome.END_BARRENS, 0x8080FF, Color.PURPLE, Luminosity.LIGHT, SaturationType.MEDIUM); - add(Biome.WARM_OCEAN, 0x0000AC, Color.BLUE, Luminosity.BRIGHT, SaturationType.LOW); - add(Biome.LUKEWARM_OCEAN, 0x000090, Color.BLUE, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.COLD_OCEAN, 0x202070, Color.BLUE, Luminosity.BRIGHT, SaturationType.HIGH); - add(Biome.DEEP_LUKEWARM_OCEAN, 0x000040, Color.BLUE, Luminosity.DARK, SaturationType.MEDIUM); - add(Biome.DEEP_COLD_OCEAN, 0x202038, Color.BLUE, Luminosity.DARK, SaturationType.HIGH); - add(Biome.DEEP_FROZEN_OCEAN, 0x404090, Color.BLUE, Luminosity.LIGHT, SaturationType.LOW); - add(Biome.THE_VOID, 0x000000, Color.MONOCHROME, Luminosity.DARK, null); - add(Biome.SUNFLOWER_PLAINS, 0xB5DB88, Color.GREEN, Luminosity.LIGHT, SaturationType.LOW); - add(Biome.WINDSWEPT_GRAVELLY_HILLS, 0x789878, Color.MONOCHROME, Luminosity.LIGHT, null); - add(Biome.FLOWER_FOREST, 0x2D8E49, Color.RED, Luminosity.LIGHT, SaturationType.LOW); - add(Biome.ICE_SPIKES, 0xB4DCDC, Color.BLUE, Luminosity.LIGHT, SaturationType.LOW); - add(Biome.OLD_GROWTH_BIRCH_FOREST, 0x589C6C, Color.GREEN, Luminosity.LIGHT, null); - add(Biome.OLD_GROWTH_SPRUCE_TAIGA, 0x818E79, Color.ORANGE, Luminosity.DARK, SaturationType.HIGH); - add(Biome.WINDSWEPT_SAVANNA, 0xE5DA87, Color.ORANGE, Luminosity.LIGHT, SaturationType.HIGH); - add(Biome.ERODED_BADLANDS, 0xFF6D3D, Color.ORANGE, Luminosity.LIGHT, SaturationType.HIGH); - add(Biome.BAMBOO_JUNGLE, 0x768E14, Color.GREEN, Luminosity.BRIGHT, SaturationType.HIGH); - add(Biome.SOUL_SAND_VALLEY, 0x5E3830, Color.BLUE, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.CRIMSON_FOREST, 0xDD0808, Color.RED, Luminosity.DARK, SaturationType.HIGH); - add(Biome.WARPED_FOREST, 0x49907B, Color.BLUE, Luminosity.BRIGHT, null); - add(Biome.BASALT_DELTAS, 0x403636, Color.MONOCHROME, Luminosity.DARK, null); - add(Biome.DRIPSTONE_CAVES, 0xcc6600, Color.ORANGE, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.LUSH_CAVES, 0x003300, Color.GREEN, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.MEADOW, 0xff00ff, Color.BLUE, Luminosity.BRIGHT, SaturationType.LOW); - add(Biome.GROVE, 0x80ff80, Color.GREEN, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.SNOWY_SLOPES, 0x00ffff, Color.BLUE, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.FROZEN_PEAKS, 0xA0A0A0, Color.MONOCHROME, Luminosity.LIGHT, null); - add(Biome.JAGGED_PEAKS, 0x3d7bc2, Color.MONOCHROME, Luminosity.BRIGHT, SaturationType.MEDIUM); - add(Biome.STONY_PEAKS, 0x888888, Color.MONOCHROME, Luminosity.LIGHT, null); - add(Biome.CUSTOM, 0xffffff, Color.MONOCHROME, Luminosity.DARK, SaturationType.MONOCHROME); - - } - - - private static void add(Biome biome, int color, Color randomColor, Luminosity luminosity, SaturationType saturation) { - BIOME_HEX.put(biome, color); - BIOME_COLOR.put(biome, randomColor); - if (luminosity != null) BIOME_LUMINOSITY.put(biome, luminosity); - if (saturation != null) BIOME_SATURATION.put(biome, saturation); - BIOME_IDs.put(biome, (short) biome.ordinal()); - } - - private static void add(Biome biome, int color, Color randomColor, Luminosity luminosity) { - add(biome, color, randomColor, luminosity, null); - } - - public static int getColor(Biome biome) { - return BIOME_HEX.get(biome); - } - - public static Color getColorType(Biome biome) { - return BIOME_COLOR.get(biome); - } - - public static Luminosity getColorLuminosity(Biome biome) { - return BIOME_LUMINOSITY.get(biome); - } - - public static SaturationType getColorSaturatiom(Biome biome) { - return BIOME_SATURATION.get(biome); - } - - public static short getId(Biome biome) { - return BIOME_IDs.get(biome); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/Varint.java b/core/src/main/java/com/volmit/iris/util/data/Varint.java deleted file mode 100644 index 9a276e68d..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/Varint.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; - -/** - *

Encodes signed and unsigned values using a common variable-length - * scheme, found for example in - * - * Google's Protocol Buffers. It uses fewer bytes to encode smaller values, - * but will use slightly more bytes to encode large values.

- *

- *

Signed values are further encoded using so-called zig-zag encoding - * in order to make them "compatible" with variable-length encoding.

- */ -public final class Varint { - - private Varint() { - } - - /** - * Encodes a value using the variable-length encoding from - * - * Google Protocol Buffers. It uses zig-zag encoding to efficiently - * encode signed values. If values are known to be nonnegative, - * {@link #writeUnsignedVarLong(long, DataOutput)} should be used. - * - * @param value value to encode - * @param out to writeNodeData bytes to - * @throws IOException if {@link DataOutput} throws {@link IOException} - */ - public static void writeSignedVarLong(long value, DataOutput out) throws IOException { - // Great trick from http://code.google.com/apis/protocolbuffers/docs/encoding.html#types - writeUnsignedVarLong((value << 1) ^ (value >> 63), out); - } - - /** - * Encodes a value using the variable-length encoding from - * - * Google Protocol Buffers. Zig-zag is not used, so input must not be negative. - * If values can be negative, use {@link #writeSignedVarLong(long, DataOutput)} - * instead. This method treats negative input as like a large unsigned value. - * - * @param value value to encode - * @param out to writeNodeData bytes to - * @throws IOException if {@link DataOutput} throws {@link IOException} - */ - public static void writeUnsignedVarLong(long value, DataOutput out) throws IOException { - while ((value & 0xFFFFFFFFFFFFFF80L) != 0L) { - out.writeByte(((int) value & 0x7F) | 0x80); - value >>>= 7; - } - out.writeByte((int) value & 0x7F); - } - - /** - * @see #writeSignedVarLong(long, DataOutput) - */ - public static void writeSignedVarInt(int value, DataOutput out) throws IOException { - // Great trick from http://code.google.com/apis/protocolbuffers/docs/encoding.html#types - writeUnsignedVarInt((value << 1) ^ (value >> 31), out); - } - - /** - * @see #writeUnsignedVarLong(long, DataOutput) - */ - public static void writeUnsignedVarInt(int value, DataOutput out) throws IOException { - while ((value & 0xFFFFFF80) != 0L) { - out.writeByte((value & 0x7F) | 0x80); - value >>>= 7; - } - out.writeByte(value & 0x7F); - } - - public static byte[] writeSignedVarInt(int value) { - // Great trick from http://code.google.com/apis/protocolbuffers/docs/encoding.html#types - return writeUnsignedVarInt((value << 1) ^ (value >> 31)); - } - - /** - * @see #writeUnsignedVarLong(long, DataOutput) - *

- * This one does not use streams and is much faster. - * Makes a single object each time, and that object is a primitive array. - */ - public static byte[] writeUnsignedVarInt(int value) { - byte[] byteArrayList = new byte[10]; - int i = 0; - while ((value & 0xFFFFFF80) != 0L) { - byteArrayList[i++] = ((byte) ((value & 0x7F) | 0x80)); - value >>>= 7; - } - byteArrayList[i] = ((byte) (value & 0x7F)); - byte[] out = new byte[i + 1]; - for (; i >= 0; i--) { - out[i] = byteArrayList[i]; - } - return out; - } - - /** - * @param in to read bytes from - * @return decode value - * @throws IOException if {@link DataInput} throws {@link IOException} - * @throws IllegalArgumentException if variable-length value does not terminate - * after 9 bytes have been read - * @see #writeSignedVarLong(long, DataOutput) - */ - public static long readSignedVarLong(DataInput in) throws IOException { - long raw = readUnsignedVarLong(in); - // This undoes the trick in writeSignedVarLong() - long temp = (((raw << 63) >> 63) ^ raw) >> 1; - // This extra step lets us deal with the largest signed values by treating - // negative results from read unsigned methods as like unsigned values - // Must re-flip the top bit if the original read value had it set. - return temp ^ (raw & (1L << 63)); - } - - /** - * @param in to read bytes from - * @return decode value - * @throws IOException if {@link DataInput} throws {@link IOException} - * @throws IllegalArgumentException if variable-length value does not terminate - * after 9 bytes have been read - * @see #writeUnsignedVarLong(long, DataOutput) - */ - public static long readUnsignedVarLong(DataInput in) throws IOException { - long value = 0L; - int i = 0; - long b; - while (((b = in.readByte()) & 0x80L) != 0) { - value |= (b & 0x7F) << i; - i += 7; - if (i > 63) { - throw new IllegalArgumentException("Variable length quantity is too long"); - } - } - return value | (b << i); - } - - /** - * @throws IllegalArgumentException if variable-length value does not terminate - * after 5 bytes have been read - * @throws IOException if {@link DataInput} throws {@link IOException} - * @see #readSignedVarLong(DataInput) - */ - public static int readSignedVarInt(DataInput in) throws IOException { - int raw = readUnsignedVarInt(in); - // This undoes the trick in writeSignedVarInt() - int temp = (((raw << 31) >> 31) ^ raw) >> 1; - // This extra step lets us deal with the largest signed values by treating - // negative results from read unsigned methods as like unsigned values. - // Must re-flip the top bit if the original read value had it set. - return temp ^ (raw & (1 << 31)); - } - - /** - * @throws IllegalArgumentException if variable-length value does not terminate - * after 5 bytes have been read - * @throws IOException if {@link DataInput} throws {@link IOException} - * @see #readUnsignedVarLong(DataInput) - */ - public static int readUnsignedVarInt(DataInput in) throws IOException { - int value = 0; - int i = 0; - int b; - while (((b = in.readByte()) & 0x80) != 0) { - value |= (b & 0x7F) << i; - i += 7; - if (i > 35) { - throw new IllegalArgumentException("Variable length quantity is too long"); - } - } - return value | (b << i); - } - - public static int readSignedVarInt(byte[] bytes) { - int raw = readUnsignedVarInt(bytes); - // This undoes the trick in writeSignedVarInt() - int temp = (((raw << 31) >> 31) ^ raw) >> 1; - // This extra step lets us deal with the largest signed values by treating - // negative results from read unsigned methods as like unsigned values. - // Must re-flip the top bit if the original read value had it set. - return temp ^ (raw & (1 << 31)); - } - - public static int readUnsignedVarInt(byte[] bytes) { - int value = 0; - int i = 0; - byte rb = Byte.MIN_VALUE; - for (byte b : bytes) { - rb = b; - if ((b & 0x80) == 0) { - break; - } - value |= (b & 0x7f) << i; - i += 7; - if (i > 35) { - throw new IllegalArgumentException("Variable length quantity is too long"); - } - } - return value | (rb << i); - } - -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/VectorMap.java b/core/src/main/java/com/volmit/iris/util/data/VectorMap.java deleted file mode 100644 index 0b3108252..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/VectorMap.java +++ /dev/null @@ -1,220 +0,0 @@ -package com.volmit.iris.util.data; - -import com.volmit.iris.util.collection.KMap; -import lombok.NonNull; -import org.bukkit.util.BlockVector; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Iterator; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Function; - -public class VectorMap implements Iterable> { - private final Map> map = new KMap<>(); - - public int size() { - return map.values().stream().mapToInt(Map::size).sum(); - } - - public boolean isEmpty() { - return map.values().stream().allMatch(Map::isEmpty); - } - - public boolean containsKey(@NonNull BlockVector vector) { - var chunk = map.get(chunk(vector)); - return chunk != null && chunk.containsKey(relative(vector)); - } - - public boolean containsValue(@NonNull T value) { - return map.values().stream().anyMatch(m -> m.containsValue(value)); - } - - public @Nullable T get(@NonNull BlockVector vector) { - var chunk = map.get(chunk(vector)); - return chunk == null ? null : chunk.get(relative(vector)); - } - - public @Nullable T put(@NonNull BlockVector vector, @NonNull T value) { - return map.computeIfAbsent(chunk(vector), k -> new KMap<>()) - .put(relative(vector), value); - } - - public @Nullable T computeIfAbsent(@NonNull BlockVector vector, @NonNull Function<@NonNull BlockVector, @NonNull T> mappingFunction) { - return map.computeIfAbsent(chunk(vector), k -> new KMap<>()) - .computeIfAbsent(relative(vector), $ -> mappingFunction.apply(vector)); - } - - public @Nullable T remove(@NonNull BlockVector vector) { - var chunk = map.get(chunk(vector)); - return chunk == null ? null : chunk.remove(relative(vector)); - } - - public void putAll(@NonNull VectorMap map) { - map.forEach(this::put); - } - - public void clear() { - map.clear(); - } - - public void forEach(@NonNull BiConsumer<@NonNull BlockVector, @NonNull T> consumer) { - map.forEach((chunk, values) -> { - int rX = chunk.x << 10; - int rY = chunk.y << 10; - int rZ = chunk.z << 10; - - values.forEach((relative, value) -> consumer.accept( - relative.resolve(rX, rY, rZ), - value - )); - }); - } - - private static Key chunk(BlockVector vector) { - return new Key(vector.getBlockX() >> 10, vector.getBlockY() >> 10, vector.getBlockZ() >> 10); - } - - private static Key relative(BlockVector vector) { - return new Key(vector.getBlockX() & 0x3FF, vector.getBlockY() & 0x3FF, vector.getBlockZ() & 0x3FF); - } - - @Override - public @NotNull EntryIterator iterator() { - return new EntryIterator(); - } - - public @NotNull KeyIterator keys() { - return new KeyIterator(); - } - - public @NotNull ValueIterator values() { - return new ValueIterator(); - } - - public class EntryIterator implements Iterator> { - private final Iterator>> chunkIterator = map.entrySet().iterator(); - private Iterator> relativeIterator; - private int rX, rY, rZ; - - @Override - public boolean hasNext() { - return relativeIterator != null && relativeIterator.hasNext() || chunkIterator.hasNext(); - } - - @Override - public Map.Entry next() { - if (relativeIterator == null || !relativeIterator.hasNext()) { - if (!chunkIterator.hasNext()) throw new IllegalStateException("No more elements"); - var chunk = chunkIterator.next(); - rX = chunk.getKey().x << 10; - rY = chunk.getKey().y << 10; - rZ = chunk.getKey().z << 10; - relativeIterator = chunk.getValue().entrySet().iterator(); - } - - var entry = relativeIterator.next(); - return Map.entry(entry.getKey().resolve(rX, rY, rZ), entry.getValue()); - } - - @Override - public void remove() { - if (relativeIterator == null) throw new IllegalStateException("No element to remove"); - relativeIterator.remove(); - } - } - - public class KeyIterator implements Iterator, Iterable { - private final Iterator>> chunkIterator = map.entrySet().iterator(); - private Iterator relativeIterator; - private int rX, rY, rZ; - - @Override - public boolean hasNext() { - return relativeIterator != null && relativeIterator.hasNext() || chunkIterator.hasNext(); - } - - @Override - public BlockVector next() { - if (relativeIterator == null || !relativeIterator.hasNext()) { - var chunk = chunkIterator.next(); - rX = chunk.getKey().x << 10; - rY = chunk.getKey().y << 10; - rZ = chunk.getKey().z << 10; - relativeIterator = chunk.getValue().keySet().iterator(); - } - - return relativeIterator.next().resolve(rX, rY, rZ); - } - - @Override - public void remove() { - if (relativeIterator == null) throw new IllegalStateException("No element to remove"); - relativeIterator.remove(); - } - - @Override - public @NotNull Iterator iterator() { - return this; - } - } - - public class ValueIterator implements Iterator, Iterable { - private final Iterator> chunkIterator = map.values().iterator(); - private Iterator relativeIterator; - - @Override - public boolean hasNext() { - return relativeIterator != null && relativeIterator.hasNext() || chunkIterator.hasNext(); - } - - @Override - public T next() { - if (relativeIterator == null || !relativeIterator.hasNext()) { - relativeIterator = chunkIterator.next().values().iterator(); - } - return relativeIterator.next(); - } - - @Override - public void remove() { - if (relativeIterator == null) throw new IllegalStateException("No element to remove"); - relativeIterator.remove(); - } - - @Override - public @NotNull Iterator iterator() { - return this; - } - } - - private static final class Key { - private final int x; - private final int y; - private final int z; - private final int hashCode; - - private Key(int x, int y, int z) { - this.x = x; - this.y = y; - this.z = z; - this.hashCode = (x << 20) | (y << 10) | z; - } - - private BlockVector resolve(int rX, int rY, int rZ) { - return new BlockVector(rX + x, rY + y, rZ + z); - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Key key)) return false; - return x == key.x && y == key.y && z == key.z; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/WeightMap.java b/core/src/main/java/com/volmit/iris/util/data/WeightMap.java deleted file mode 100644 index 7ab3d0a6f..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/WeightMap.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.util.collection.KMap; - -public class WeightMap extends KMap { - private static final long serialVersionUID = 87558033900969389L; - private boolean modified = false; - private double lastWeight = 0; - - public double getPercentChance(T t) { - if (totalWeight() <= 0) { - return 0; - } - - return getWeight(t) / totalWeight(); - } - - public void clear() { - modified = true; - } - - public WeightMap setWeight(T t, double weight) { - modified = true; - put(t, weight); - - return this; - } - - public double getWeight(T t) { - return get(t); - } - - public double totalWeight() { - if (!modified) { - return lastWeight; - } - - modified = false; - Shrinkwrap s = new Shrinkwrap<>(0D); - forEachKey(Integer.MAX_VALUE, (d) -> s.set(s.get() + 1)); - lastWeight = s.get(); - - return lastWeight; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/WeightedRandom.java b/core/src/main/java/com/volmit/iris/util/data/WeightedRandom.java deleted file mode 100644 index ba6cf07a6..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/WeightedRandom.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KeyPair; - -import java.util.Random; - -public class WeightedRandom { - - private final KList> weightedObjects = new KList<>(); - private final Random random; - private int totalWeight = 0; - - public WeightedRandom(Random random) { - this.random = random; - } - - public WeightedRandom() { - this.random = new Random(); - } - - public void put(T object, int weight) { - weightedObjects.add(new KeyPair<>(object, weight)); - totalWeight += weight; - } - - public WeightedRandom merge(WeightedRandom other) { - weightedObjects.addAll(other.weightedObjects); - totalWeight += other.totalWeight; - return this; - } - - public T pullRandom() { - int pull = random.nextInt(totalWeight); - int index = 0; - while (pull > 0) { - pull -= weightedObjects.get(index).getV(); - if (pull <= 0) break; - index++; - } - return weightedObjects.get(index).getK(); - } - - public int getSize() { - return weightedObjects.size(); - } - - public void shuffle() { - weightedObjects.shuffle(random); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/Writable.java b/core/src/main/java/com/volmit/iris/util/data/Writable.java deleted file mode 100644 index 9386e0fac..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/Writable.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public interface Writable { - void write(DataOutputStream o) throws IOException; - - void read(DataInputStream i) throws IOException; -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/BitStorage.java b/core/src/main/java/com/volmit/iris/util/data/palette/BitStorage.java deleted file mode 100644 index 22b965be9..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/BitStorage.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import org.apache.commons.lang3.Validate; - -import java.util.concurrent.atomic.AtomicLongArray; -import java.util.function.IntConsumer; - -public class BitStorage { - private static final int[] MAGIC = new int[]{ - -1, -1, 0, Integer.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Integer.MIN_VALUE, - 0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756, - 0, Integer.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0, - 390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378, - 306783378, 0, 286331153, 286331153, 0, Integer.MIN_VALUE, 0, 3, 252645135, 252645135, - 0, 238609294, 238609294, 0, 226050910, 226050910, 0, 214748364, 214748364, 0, - 204522252, 204522252, 0, 195225786, 195225786, 0, 186737708, 186737708, 0, 178956970, - 178956970, 0, 171798691, 171798691, 0, 165191049, 165191049, 0, 159072862, 159072862, - 0, 153391689, 153391689, 0, 148102320, 148102320, 0, 143165576, 143165576, 0, - 138547332, 138547332, 0, Integer.MIN_VALUE, 0, 4, 130150524, 130150524, 0, 126322567, - 126322567, 0, 122713351, 122713351, 0, 119304647, 119304647, 0, 116080197, 116080197, - 0, 113025455, 113025455, 0, 110127366, 110127366, 0, 107374182, 107374182, 0, - 104755299, 104755299, 0, 102261126, 102261126, 0, 99882960, 99882960, 0, 97612893, - 97612893, 0, 95443717, 95443717, 0, 93368854, 93368854, 0, 91382282, 91382282, - 0, 89478485, 89478485, 0, 87652393, 87652393, 0, 85899345, 85899345, 0, - 84215045, 84215045, 0, 82595524, 82595524, 0, 81037118, 81037118, 0, 79536431, - 79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303, - 0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0, - 70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Integer.MIN_VALUE, - 0, 5}; - - private final AtomicLongArray data; - private final int bits; - private final long mask; - private final int size; - private final int valuesPerLong; - private final int divideMul; - private final int divideAdd; - private final int divideShift; - - public BitStorage(int bits, int length) { - this(bits, length, (AtomicLongArray) null); - } - - public BitStorage(int bits, int length, long[] data) { - this(bits, length, atomic(data)); - } - - public BitStorage(int bits, int length, AtomicLongArray data) { - Validate.inclusiveBetween(1L, 32L, bits); - this.size = length; - this.bits = bits; - this.mask = (1L << bits) - 1L; - this.valuesPerLong = (char) (64 / bits); - int var3 = 3 * (this.valuesPerLong - 1); - this.divideMul = MAGIC[var3]; - this.divideAdd = MAGIC[var3 + 1]; - this.divideShift = MAGIC[var3 + 2]; - int var4 = (length + this.valuesPerLong - 1) / this.valuesPerLong; - if (data != null) { - if (data.length() != var4) { - throw new RuntimeException("NO!"); - } - this.data = data; - } else { - this.data = new AtomicLongArray(var4); - } - } - - private static AtomicLongArray atomic(long[] data) { - if (data == null) { - return null; - } - - AtomicLongArray d = new AtomicLongArray(data.length); - for (int i = 0; i < data.length; i++) { - d.set(i, data[i]); - } - - return d; - } - - private static long[] atomic(AtomicLongArray data) { - if (data == null) { - return null; - } - - long[] d = new long[data.length()]; - for (int i = 0; i < data.length(); i++) { - d[i] = data.get(i); - } - - return d; - } - - private int cellIndex(int var0) { - long var1 = Integer.toUnsignedLong(this.divideMul); - long var3 = Integer.toUnsignedLong(this.divideAdd); - return (int) (var0 * var1 + var3 >> 32L >> this.divideShift); - } - - public int getAndSet(int var0, int var1) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - Validate.inclusiveBetween(0L, this.mask, var1); - int var2 = cellIndex(var0); - long var3 = this.data.get(var2); - int var5 = (var0 - var2 * this.valuesPerLong) * this.bits; - int var6 = (int) (var3 >> var5 & this.mask); - this.data.set(var2, var3 & (this.mask << var5 ^ 0xFFFFFFFFFFFFFFFFL) | (var1 & this.mask) << var5); - return var6; - } - - public void set(int var0, int var1) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - Validate.inclusiveBetween(0L, this.mask, var1); - int var2 = cellIndex(var0); - long var3 = this.data.get(var2); - int var5 = (var0 - var2 * this.valuesPerLong) * this.bits; - - this.data.set(var2, var3 & (this.mask << var5 ^ 0xFFFFFFFFFFFFFFFFL) | (var1 & this.mask) << var5); - } - - public int get(int var0) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - int var1 = cellIndex(var0); - long var2 = this.data.get(var1); - int var4 = (var0 - var1 * this.valuesPerLong) * this.bits; - return (int) (var2 >> var4 & this.mask); - } - - public long[] getRaw() { - return atomic(data); - } - - public int getSize() { - return this.size; - } - - public int getBits() { - return this.bits; - } - - public void getAll(IntConsumer var0) { - int var1 = 0; - for (int i = 0; i < data.length(); i++) { - long var5 = data.get(i); - for (int var7 = 0; var7 < this.valuesPerLong; var7++) { - var0.accept((int) (var5 & this.mask)); - var5 >>= this.bits; - if (++var1 >= this.size) - return; - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/CountConsumer.java b/core/src/main/java/com/volmit/iris/util/data/palette/CountConsumer.java deleted file mode 100644 index 785997ed9..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/CountConsumer.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -@FunctionalInterface -public interface CountConsumer { - void accept(T paramT, int paramInt); -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/CrudeIncrementalIntIdentityHashBiMap.java b/core/src/main/java/com/volmit/iris/util/data/palette/CrudeIncrementalIntIdentityHashBiMap.java deleted file mode 100644 index 92e4c7fb2..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/CrudeIncrementalIntIdentityHashBiMap.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import com.google.common.collect.Iterators; - -import java.util.Iterator; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicIntegerArray; -import java.util.concurrent.atomic.AtomicReferenceArray; - -public class CrudeIncrementalIntIdentityHashBiMap implements IdMap { - public static final int NOT_FOUND = -1; - private static final Object EMPTY_SLOT = null; - private static final float LOADFACTOR = 0.8F; - private AtomicReferenceArray keys; - private AtomicIntegerArray values; - private AtomicReferenceArray byId; - private int nextId; - private int size; - - public CrudeIncrementalIntIdentityHashBiMap(int var0) { - var0 = (int) (var0 / 0.8F); - this.keys = new AtomicReferenceArray<>(var0); - this.values = new AtomicIntegerArray(var0); - this.byId = new AtomicReferenceArray<>(var0); - } - - public int getId(K var0) { - return getValue(indexOf(var0, hash(var0))); - } - - - public K byId(int var0) { - if (var0 < 0 || var0 >= this.byId.length()) { - return null; - } - return this.byId.get(var0); - } - - private int getValue(int var0) { - if (var0 == -1) { - return -1; - } - return this.values.get(var0); - } - - public boolean contains(K var0) { - return (getId(var0) != -1); - } - - public boolean contains(int var0) { - return (byId(var0) != null); - } - - public int add(K var0) { - int var1 = nextId(); - addMapping(var0, var1); - return var1; - } - - private int nextId() { - while (nextId < byId.length() && byId.get(nextId) != null) { - nextId++; - } - return nextId; - } - - private void grow(int var0) { - AtomicReferenceArray var1 = this.keys; - AtomicIntegerArray var2 = this.values; - this.keys = new AtomicReferenceArray<>(var0); - this.values = new AtomicIntegerArray(var0); - this.byId = new AtomicReferenceArray<>(var0); - this.nextId = 0; - this.size = 0; - for (int var3 = 0; var3 < var1.length(); var3++) { - if (var1.get(var3) != null) { - addMapping(var1.get(var3), var2.get(var3)); - } - } - } - - public void addMapping(K var0, int var1) { - int var2 = Math.max(var1, this.size + 1); - if (var2 >= this.keys.length() * 0.8F) { - int i = this.keys.length() << 1; - while (i < var1) - i <<= 1; - grow(i); - } - int var3 = findEmpty(hash(var0)); - this.keys.set(var3, var0); - this.values.set(var3, var1); - this.byId.set(var1, var0); - this.size++; - if (var1 == this.nextId) - this.nextId++; - } - - private int hash(K var0) { - return (Mth.murmurHash3Mixer(System.identityHashCode(var0)) & Integer.MAX_VALUE) % this.keys.length(); - } - - private int indexOf(K var0, int var1) { - int var2; - for (var2 = var1; var2 < this.keys.length(); var2++) { - if (this.keys.get(var2) == null) { - return 0; - } - if (this.keys.get(var2).equals(var0)) - return var2; - if (this.keys.get(var2) == EMPTY_SLOT) - return -1; - } - for (var2 = 0; var2 < var1; var2++) { - if (this.keys.get(var2).equals(var0)) - return var2; - if (this.keys.get(var2) == EMPTY_SLOT) - return -1; - } - return -1; - } - - private int findEmpty(int var0) { - int var1; - for (var1 = var0; var1 < this.keys.length(); var1++) { - if (this.keys.get(var1) == EMPTY_SLOT) - return var1; - } - for (var1 = 0; var1 < var0; var1++) { - if (this.keys.get(var1) == EMPTY_SLOT) - return var1; - } - throw new RuntimeException("Overflowed :("); - } - - public Iterator iterator() { - return Iterators.filter(new Iterator() { - int i = 0; - - @Override - public boolean hasNext() { - return i < byId.length() - 1; - } - - @Override - public K next() { - return byId.get(i++); - } - }, Objects::nonNull); - } - - public void clear() { - - for (int i = 0; i < Math.max(keys.length(), byId.length()); i++) { - if (i < keys.length() - 1) { - keys.set(i, null); - } - - if (i < byId.length() - 1) { - byId.set(i, null); - } - } - - this.nextId = 0; - this.size = 0; - } - - public int size() { - return this.size; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/GlobalPalette.java b/core/src/main/java/com/volmit/iris/util/data/palette/GlobalPalette.java deleted file mode 100644 index 5697f5c07..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/GlobalPalette.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import java.util.List; -import java.util.function.Predicate; - -public class GlobalPalette implements Palette { - private final IdMapper registry; - - private final T defaultValue; - - public GlobalPalette(T... f) { - IdMapper mapper = new IdMapper<>(); - for (T i : f) { - mapper.add(i); - } - registry = mapper; - defaultValue = f[0]; - } - - public GlobalPalette(IdMapper var0, T var1) { - this.registry = var0; - this.defaultValue = var1; - } - - public int idFor(T var0) { - int var1 = this.registry.getId(var0); - return (var1 == -1) ? 0 : var1; - } - - public boolean maybeHas(Predicate var0) { - return true; - } - - public T valueFor(int var0) { - T var1 = this.registry.byId(var0); - return (var1 == null) ? this.defaultValue : var1; - } - - public int getSize() { - return this.registry.size(); - } - - @Override - public void read(List fromList) { - - } - - @Override - public void write(List toList) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/HashMapPalette.java b/core/src/main/java/com/volmit/iris/util/data/palette/HashMapPalette.java deleted file mode 100644 index 97cf14800..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/HashMapPalette.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KMap; - -import java.util.List; - -public class HashMapPalette implements Palette { - private final KMap values; - private final PaletteResize resizeHandler; - private final int bits; - private int id; - - public HashMapPalette(int var1, PaletteResize var2) { - this.bits = var1; - this.resizeHandler = var2; - this.values = new KMap<>(); - id = 1; - } - - public int idFor(T var0) { - if (var0 == null) { - return 0; - } - - return this.values.computeIfAbsent(var0, (k) -> { - int newId = id++; - - if (newId >= 1 << this.bits) { - Iris.info(newId + " to..."); - newId = this.resizeHandler.onResize(this.bits + 1, var0); - Iris.info(newId + ".."); - } - - return newId; - }); - } - - public T valueFor(int var0) { - return this.values.getKey(var0); - } - - public int getSize() { - return this.values.size(); - } - - @Override - public void read(List data) { - data.forEach(this::idFor); - } - - @Override - public void write(List toList) { - toList.addAll(values.keySet()); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/IdMap.java b/core/src/main/java/com/volmit/iris/util/data/palette/IdMap.java deleted file mode 100644 index 4c74d39c5..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/IdMap.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -public interface IdMap extends Iterable { - int getId(T paramT); - - T byId(int paramInt); -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/IdMapper.java b/core/src/main/java/com/volmit/iris/util/data/palette/IdMapper.java deleted file mode 100644 index 77ddbfa8a..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/IdMapper.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import com.google.common.base.Predicates; -import com.google.common.collect.Iterators; -import com.google.common.collect.Lists; - -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.List; - -public class IdMapper implements IdMap { - public static final int DEFAULT = -1; - private final IdentityHashMap tToId; - private final List idToT; - private int nextId; - - public IdMapper(IdentityHashMap tToId, List idToT, int nextId) { - this.tToId = tToId; - this.idToT = idToT; - this.nextId = nextId; - } - - public IdMapper() { - this(512); - } - - public IdMapper(int var0) { - this.idToT = Lists.newArrayListWithExpectedSize(var0); - this.tToId = new IdentityHashMap<>(var0); - } - - public void addMapping(T var0, int var1) { - this.tToId.put(var0, Integer.valueOf(var1)); - while (this.idToT.size() <= var1) { - this.idToT.add(null); - } - this.idToT.set(var1, var0); - if (this.nextId <= var1) - this.nextId = var1 + 1; - } - - public void add(T var0) { - addMapping(var0, this.nextId); - } - - public int getId(T var0) { - Integer var1 = this.tToId.get(var0); - return (var1 == null) ? -1 : var1.intValue(); - } - - public final T byId(int var0) { - if (var0 >= 0 && var0 < this.idToT.size()) { - return this.idToT.get(var0); - } - return null; - } - - public Iterator iterator() { - return Iterators.filter(this.idToT.iterator(), Predicates.notNull()); - } - - public boolean contains(int var0) { - return (byId(var0) != null); - } - - public int size() { - return this.tToId.size(); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/LinearPalette.java b/core/src/main/java/com/volmit/iris/util/data/palette/LinearPalette.java deleted file mode 100644 index 25afb6a0b..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/LinearPalette.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import java.util.List; -import java.util.concurrent.atomic.AtomicReferenceArray; - -public class LinearPalette implements Palette { - private final AtomicReferenceArray values; - private final PaletteResize resizeHandler; - private final int bits; - private int size; - - public LinearPalette(int var1, PaletteResize var2) { - this.values = new AtomicReferenceArray<>(1 << var1); - this.bits = var1; - this.resizeHandler = var2; - } - - public int idFor(T var0) { - int var1; - for (var1 = 0; var1 < size; var1++) { - if (values.get(var1) == null && var0 == null) { - return var1; - } - - if (values.get(var1) != null && values.get(var1).equals(var0)) { - return var1; - } - } - var1 = size; - if (var1 < values.length()) { - values.set(var1, var0); - size++; - return var1; - } - return resizeHandler.onResize(bits + 1, var0); - } - - public T valueFor(int var0) { - if (var0 >= 0 && var0 < size) { - return this.values.get(var0); - } - return null; - } - - public int getSize() { - return size; - } - - @Override - public void read(List fromList) { - for (int i = 0; i < fromList.size(); i++) { - values.set(i, fromList.get(i)); - } - - size = fromList.size(); - } - - @Override - public void write(List toList) { - for (int i = 0; i < size; i++) { - T v = values.get(i); - toList.add(v); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/Mth.java b/core/src/main/java/com/volmit/iris/util/data/palette/Mth.java deleted file mode 100644 index 16ce7c89c..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/Mth.java +++ /dev/null @@ -1,712 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import java.util.Random; -import java.util.UUID; -import java.util.function.Consumer; -import java.util.function.Supplier; - -public class Mth { - public static final float PI = 3.1415927F; - public static final float HALF_PI = 1.5707964F; - public static final float TWO_PI = 6.2831855F; - public static final float DEG_TO_RAD = 0.017453292F; - public static final float RAD_TO_DEG = 57.295776F; - public static final float EPSILON = 1.0E-5F; - public static final float SQRT_OF_TWO = sqrt(2.0F); - private static final int BIG_ENOUGH_INT = 1024; - private static final float BIG_ENOUGH_FLOAT = 1024.0F; - private static final long UUID_VERSION = 61440L; - private static final long UUID_VERSION_TYPE_4 = 16384L; - private static final long UUID_VARIANT = -4611686018427387904L; - private static final long UUID_VARIANT_2 = -9223372036854775808L; - private static final float SIN_SCALE = 10430.378F; - - private static final float[] SIN; - private static final Random RANDOM = new Random(); - private static final int[] MULTIPLY_DE_BRUIJN_BIT_POSITION = new int[]{ - 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, - 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, - 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, - 10, 9}; - private static final double ONE_SIXTH = 0.16666666666666666D; - private static final int FRAC_EXP = 8; - private static final int LUT_SIZE = 257; - private static final double FRAC_BIAS = Double.longBitsToDouble(4805340802404319232L); - private static final double[] ASIN_TAB = new double[257]; - private static final double[] COS_TAB = new double[257]; - - static { - SIN = make(new float[65536], var0 -> { - for (int var1 = 0; var1 < var0.length; var1++) - var0[var1] = (float) Math.sin(var1 * Math.PI * 2.0D / 65536.0D); - }); - } - - static { - for (int var0 = 0; var0 < 257; var0++) { - double var1 = var0 / 256.0D; - double var3 = Math.asin(var1); - COS_TAB[var0] = Math.cos(var3); - ASIN_TAB[var0] = var3; - } - } - - public static T make(Supplier var0) { - return var0.get(); - } - - public static T make(T var0, Consumer var1) { - var1.accept(var0); - return var0; - } - - public static float sin(float var0) { - return SIN[(int) (var0 * 10430.378F) & 0xFFFF]; - } - - public static float cos(float var0) { - return SIN[(int) (var0 * 10430.378F + 16384.0F) & 0xFFFF]; - } - - public static float sqrt(float var0) { - return (float) Math.sqrt(var0); - } - - public static int floor(float var0) { - int var1 = (int) var0; - return (var0 < var1) ? (var1 - 1) : var1; - } - - public static int fastFloor(double var0) { - return (int) (var0 + 1024.0D) - 1024; - } - - public static int floor(double var0) { - int var2 = (int) var0; - return (var0 < var2) ? (var2 - 1) : var2; - } - - public static long lfloor(double var0) { - long var2 = (long) var0; - return (var0 < var2) ? (var2 - 1L) : var2; - } - - public static int absFloor(double var0) { - return (int) ((var0 >= 0.0D) ? var0 : (-var0 + 1.0D)); - } - - public static float abs(float var0) { - return Math.abs(var0); - } - - public static int abs(int var0) { - return Math.abs(var0); - } - - public static int ceil(float var0) { - int var1 = (int) var0; - return (var0 > var1) ? (var1 + 1) : var1; - } - - public static int ceil(double var0) { - int var2 = (int) var0; - return (var0 > var2) ? (var2 + 1) : var2; - } - - public static byte clamp(byte var0, byte var1, byte var2) { - if (var0 < var1) - return var1; - if (var0 > var2) - return var2; - return var0; - } - - public static int clamp(int var0, int var1, int var2) { - if (var0 < var1) - return var1; - if (var0 > var2) - return var2; - return var0; - } - - public static long clamp(long var0, long var2, long var4) { - if (var0 < var2) - return var2; - if (var0 > var4) - return var4; - return var0; - } - - public static float clamp(float var0, float var1, float var2) { - if (var0 < var1) - return var1; - if (var0 > var2) - return var2; - return var0; - } - - public static double clamp(double var0, double var2, double var4) { - if (var0 < var2) - return var2; - if (var0 > var4) - return var4; - return var0; - } - - public static double clampedLerp(double var0, double var2, double var4) { - if (var4 < 0.0D) - return var0; - if (var4 > 1.0D) - return var2; - return lerp(var4, var0, var2); - } - - public static float clampedLerp(float var0, float var1, float var2) { - if (var2 < 0.0F) - return var0; - if (var2 > 1.0F) - return var1; - return lerp(var2, var0, var1); - } - - public static double absMax(double var0, double var2) { - if (var0 < 0.0D) - var0 = -var0; - if (var2 < 0.0D) - var2 = -var2; - return (var0 > var2) ? var0 : var2; - } - - public static int intFloorDiv(int var0, int var1) { - return Math.floorDiv(var0, var1); - } - - public static int nextInt(Random var0, int var1, int var2) { - if (var1 >= var2) - return var1; - return var0.nextInt(var2 - var1 + 1) + var1; - } - - public static float nextFloat(Random var0, float var1, float var2) { - if (var1 >= var2) - return var1; - return var0.nextFloat() * (var2 - var1) + var1; - } - - public static double nextDouble(Random var0, double var1, double var3) { - if (var1 >= var3) - return var1; - return var0.nextDouble() * (var3 - var1) + var1; - } - - public static double average(long[] var0) { - long var1 = 0L; - for (long var6 : var0) - var1 += var6; - return var1 / var0.length; - } - - public static boolean equal(float var0, float var1) { - return (Math.abs(var1 - var0) < 1.0E-5F); - } - - public static boolean equal(double var0, double var2) { - return (Math.abs(var2 - var0) < 9.999999747378752E-6D); - } - - public static int positiveModulo(int var0, int var1) { - return Math.floorMod(var0, var1); - } - - public static float positiveModulo(float var0, float var1) { - return (var0 % var1 + var1) % var1; - } - - public static double positiveModulo(double var0, double var2) { - return (var0 % var2 + var2) % var2; - } - - public static int wrapDegrees(int var0) { - int var1 = var0 % 360; - if (var1 >= 180) - var1 -= 360; - if (var1 < -180) - var1 += 360; - return var1; - } - - public static float wrapDegrees(float var0) { - float var1 = var0 % 360.0F; - if (var1 >= 180.0F) - var1 -= 360.0F; - if (var1 < -180.0F) - var1 += 360.0F; - return var1; - } - - public static double wrapDegrees(double var0) { - double var2 = var0 % 360.0D; - if (var2 >= 180.0D) - var2 -= 360.0D; - if (var2 < -180.0D) - var2 += 360.0D; - return var2; - } - - public static float degreesDifference(float var0, float var1) { - return wrapDegrees(var1 - var0); - } - - public static float degreesDifferenceAbs(float var0, float var1) { - return abs(degreesDifference(var0, var1)); - } - - public static float rotateIfNecessary(float var0, float var1, float var2) { - float var3 = degreesDifference(var0, var1); - float var4 = clamp(var3, -var2, var2); - return var1 - var4; - } - - public static float approach(float var0, float var1, float var2) { - var2 = abs(var2); - if (var0 < var1) - return clamp(var0 + var2, var0, var1); - return clamp(var0 - var2, var1, var0); - } - - public static float approachDegrees(float var0, float var1, float var2) { - float var3 = degreesDifference(var0, var1); - return approach(var0, var0 + var3, var2); - } - - public static int getInt(String var0, int var1) { - return Integer.valueOf(var0, var1); - } - - public static int getInt(String var0, int var1, int var2) { - return Math.max(var2, getInt(var0, var1)); - } - - public static double getDouble(String var0, double var1) { - try { - return Double.parseDouble(var0); - } catch (Throwable var3) { - return var1; - } - } - - public static double getDouble(String var0, double var1, double var3) { - return Math.max(var3, getDouble(var0, var1)); - } - - public static int smallestEncompassingPowerOfTwo(int var0) { - int var1 = var0 - 1; - var1 |= var1 >> 1; - var1 |= var1 >> 2; - var1 |= var1 >> 4; - var1 |= var1 >> 8; - var1 |= var1 >> 16; - return var1 + 1; - } - - public static boolean isPowerOfTwo(int var0) { - return (var0 != 0 && (var0 & var0 - 1) == 0); - } - - public static int ceillog2(int var0) { - var0 = isPowerOfTwo(var0) ? var0 : smallestEncompassingPowerOfTwo(var0); - return MULTIPLY_DE_BRUIJN_BIT_POSITION[(int) (var0 * 125613361L >> 27L) & 0x1F]; - } - - public static int log2(int var0) { - return ceillog2(var0) - (isPowerOfTwo(var0) ? 0 : 1); - } - - public static int color(float var0, float var1, float var2) { - return color(floor(var0 * 255.0F), floor(var1 * 255.0F), floor(var2 * 255.0F)); - } - - public static int color(int var0, int var1, int var2) { - int var3 = var0; - var3 = (var3 << 8) + var1; - var3 = (var3 << 8) + var2; - return var3; - } - - public static int colorMultiply(int var0, int var1) { - int var2 = (var0 & 0xFF0000) >> 16; - int var3 = (var1 & 0xFF0000) >> 16; - int var4 = (var0 & 0xFF00) >> 8; - int var5 = (var1 & 0xFF00) >> 8; - int var6 = (var0 & 0xFF); - int var7 = (var1 & 0xFF); - int var8 = (int) (var2 * var3 / 255.0F); - int var9 = (int) (var4 * var5 / 255.0F); - int var10 = (int) (var6 * var7 / 255.0F); - return var0 & 0xFF000000 | var8 << 16 | var9 << 8 | var10; - } - - public static int colorMultiply(int var0, float var1, float var2, float var3) { - int var4 = (var0 & 0xFF0000) >> 16; - int var5 = (var0 & 0xFF00) >> 8; - int var6 = (var0 & 0xFF); - int var7 = (int) (var4 * var1); - int var8 = (int) (var5 * var2); - int var9 = (int) (var6 * var3); - return var0 & 0xFF000000 | var7 << 16 | var8 << 8 | var9; - } - - public static float frac(float var0) { - return var0 - floor(var0); - } - - public static double frac(double var0) { - return var0 - lfloor(var0); - } - - public static long getSeed(int var0, int var1, int var2) { - long var3 = (var0 * 3129871) ^ var2 * 116129781L ^ var1; - var3 = var3 * var3 * 42317861L + var3 * 11L; - return var3 >> 16L; - } - - public static UUID createInsecureUUID(Random var0) { - long var1 = var0.nextLong() & 0xFFFFFFFFFFFF0FFFL | 0x4000L; - long var3 = var0.nextLong() & 0x3FFFFFFFFFFFFFFFL | Long.MIN_VALUE; - return new UUID(var1, var3); - } - - public static UUID createInsecureUUID() { - return createInsecureUUID(RANDOM); - } - - public static double inverseLerp(double var0, double var2, double var4) { - return (var0 - var2) / (var4 - var2); - } - - public static double atan2(double var0, double var2) { - double var4 = var2 * var2 + var0 * var0; - if (Double.isNaN(var4)) - return Double.NaN; - boolean var6 = (var0 < 0.0D); - if (var6) - var0 = -var0; - boolean var7 = (var2 < 0.0D); - if (var7) - var2 = -var2; - boolean var8 = (var0 > var2); - if (var8) { - double d = var2; - var2 = var0; - var0 = d; - } - double var9 = fastInvSqrt(var4); - var2 *= var9; - var0 *= var9; - double var11 = FRAC_BIAS + var0; - int var13 = (int) Double.doubleToRawLongBits(var11); - double var14 = ASIN_TAB[var13]; - double var16 = COS_TAB[var13]; - double var18 = var11 - FRAC_BIAS; - double var20 = var0 * var16 - var2 * var18; - double var22 = (6.0D + var20 * var20) * var20 * 0.16666666666666666D; - double var24 = var14 + var22; - if (var8) - var24 = 1.5707963267948966D - var24; - if (var7) - var24 = Math.PI - var24; - if (var6) - var24 = -var24; - return var24; - } - - public static float fastInvSqrt(float var0) { - float var1 = 0.5F * var0; - int var2 = Float.floatToIntBits(var0); - var2 = 1597463007 - (var2 >> 1); - var0 = Float.intBitsToFloat(var2); - var0 *= 1.5F - var1 * var0 * var0; - return var0; - } - - public static double fastInvSqrt(double var0) { - double var2 = 0.5D * var0; - long var4 = Double.doubleToRawLongBits(var0); - var4 = 6910469410427058090L - (var4 >> 1L); - var0 = Double.longBitsToDouble(var4); - var0 *= 1.5D - var2 * var0 * var0; - return var0; - } - - public static float fastInvCubeRoot(float var0) { - int var1 = Float.floatToIntBits(var0); - var1 = 1419967116 - var1 / 3; - float var2 = Float.intBitsToFloat(var1); - var2 = 0.6666667F * var2 + 1.0F / 3.0F * var2 * var2 * var0; - var2 = 0.6666667F * var2 + 1.0F / 3.0F * var2 * var2 * var0; - return var2; - } - - public static int hsvToRgb(float var0, float var1, float var2) { - float var8, var9, var10; - int var11, var12, var13, var3 = (int) (var0 * 6.0F) % 6; - float var4 = var0 * 6.0F - var3; - float var5 = var2 * (1.0F - var1); - float var6 = var2 * (1.0F - var4 * var1); - float var7 = var2 * (1.0F - (1.0F - var4) * var1); - switch (var3) { - case 0: - var8 = var2; - var9 = var7; - var10 = var5; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 1: - var8 = var6; - var9 = var2; - var10 = var5; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 2: - var8 = var5; - var9 = var2; - var10 = var7; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 3: - var8 = var5; - var9 = var6; - var10 = var2; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 4: - var8 = var7; - var9 = var5; - var10 = var2; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 5: - var8 = var2; - var9 = var5; - var10 = var6; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - } - throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + var0 + ", " + var1 + ", " + var2); - } - - public static int murmurHash3Mixer(int var0) { - var0 ^= var0 >>> 16; - var0 *= -2048144789; - var0 ^= var0 >>> 13; - var0 *= -1028477387; - var0 ^= var0 >>> 16; - return var0; - } - - public static long murmurHash3Mixer(long var0) { - var0 ^= var0 >>> 33L; - var0 *= -49064778989728563L; - var0 ^= var0 >>> 33L; - var0 *= -4265267296055464877L; - var0 ^= var0 >>> 33L; - return var0; - } - - public static double[] cumulativeSum(double... var0) { - float var1 = 0.0F; - for (double var5 : var0) - var1 = (float) (var1 + var5); - int var2; - for (var2 = 0; var2 < var0.length; var2++) - var0[var2] = var0[var2] / var1; - for (var2 = 0; var2 < var0.length; var2++) - var0[var2] = ((var2 == 0) ? 0.0D : var0[var2 - 1]) + var0[var2]; - return var0; - } - - public static int getRandomForDistributionIntegral(Random var0, double[] var1) { - double var2 = var0.nextDouble(); - for (int var4 = 0; var4 < var1.length; var4++) { - if (var2 < var1[var4]) - return var4; - } - return var1.length; - } - - public static double[] binNormalDistribution(double var0, double var2, double var4, int var6, int var7) { - double[] var8 = new double[var7 - var6 + 1]; - int var9 = 0; - for (int var10 = var6; var10 <= var7; var10++) { - var8[var9] = Math.max(0.0D, var0 * - - StrictMath.exp(-(var10 - var4) * (var10 - var4) / 2.0D * var2 * var2)); - var9++; - } - return var8; - } - - public static double[] binBiModalNormalDistribution(double var0, double var2, double var4, double var6, double var8, double var10, int var12, int var13) { - double[] var14 = new double[var13 - var12 + 1]; - int var15 = 0; - for (int var16 = var12; var16 <= var13; var16++) { - var14[var15] = Math.max(0.0D, var0 * - - StrictMath.exp(-(var16 - var4) * (var16 - var4) / 2.0D * var2 * var2) + var6 * - StrictMath.exp(-(var16 - var10) * (var16 - var10) / 2.0D * var8 * var8)); - var15++; - } - return var14; - } - - public static double[] binLogDistribution(double var0, double var2, int var4, int var5) { - double[] var6 = new double[var5 - var4 + 1]; - int var7 = 0; - for (int var8 = var4; var8 <= var5; var8++) { - var6[var7] = Math.max(var0 * StrictMath.log(var8) + var2, 0.0D); - var7++; - } - return var6; - } - - public static float lerp(float var0, float var1, float var2) { - return var1 + var0 * (var2 - var1); - } - - public static double lerp(double var0, double var2, double var4) { - return var2 + var0 * (var4 - var2); - } - - public static double lerp2(double var0, double var2, double var4, double var6, double var8, double var10) { - return lerp(var2, - - lerp(var0, var4, var6), - lerp(var0, var8, var10)); - } - - public static double lerp3(double var0, double var2, double var4, double var6, double var8, double var10, double var12, double var14, double var16, double var18, double var20) { - return lerp(var4, - - lerp2(var0, var2, var6, var8, var10, var12), - lerp2(var0, var2, var14, var16, var18, var20)); - } - - public static double smoothstep(double var0) { - return var0 * var0 * var0 * (var0 * (var0 * 6.0D - 15.0D) + 10.0D); - } - - public static double smoothstepDerivative(double var0) { - return 30.0D * var0 * var0 * (var0 - 1.0D) * (var0 - 1.0D); - } - - public static int sign(double var0) { - if (var0 == 0.0D) - return 0; - return (var0 > 0.0D) ? 1 : -1; - } - - public static float rotLerp(float var0, float var1, float var2) { - return var1 + var0 * wrapDegrees(var2 - var1); - } - - public static float diffuseLight(float var0, float var1, float var2) { - return Math.min(var0 * var0 * 0.6F + var1 * var1 * (3.0F + var1) / 4.0F + var2 * var2 * 0.8F, 1.0F); - } - - @Deprecated - public static float rotlerp(float var0, float var1, float var2) { - float var3 = var1 - var0; - while (var3 < -180.0F) - var3 += 360.0F; - while (var3 >= 180.0F) - var3 -= 360.0F; - return var0 + var2 * var3; - } - - @Deprecated - public static float rotWrap(double var0) { - while (var0 >= 180.0D) - var0 -= 360.0D; - while (var0 < -180.0D) - var0 += 360.0D; - return (float) var0; - } - - public static float triangleWave(float var0, float var1) { - return (Math.abs(var0 % var1 - var1 * 0.5F) - var1 * 0.25F) / var1 * 0.25F; - } - - public static float square(float var0) { - return var0 * var0; - } - - public static double square(double var0) { - return var0 * var0; - } - - public static int square(int var0) { - return var0 * var0; - } - - public static double clampedMap(double var0, double var2, double var4, double var6, double var8) { - return clampedLerp(var6, var8, inverseLerp(var0, var2, var4)); - } - - public static double map(double var0, double var2, double var4, double var6, double var8) { - return lerp(inverseLerp(var0, var2, var4), var6, var8); - } - - public static double wobble(double var0) { - return var0 + (2.0D * (new Random(floor(var0 * 3000.0D))).nextDouble() - 1.0D) * 1.0E-7D / 2.0D; - } - - public static int roundToward(int var0, int var1) { - return (var0 + var1 - 1) / var1 * var1; - } - - public static int randomBetweenInclusive(Random var0, int var1, int var2) { - return var0.nextInt(var2 - var1 + 1) + var1; - } - - public static float randomBetween(Random var0, float var1, float var2) { - return var0.nextFloat() * (var2 - var1) + var1; - } - - public static float normal(Random var0, float var1, float var2) { - return var1 + (float) var0.nextGaussian() * var2; - } - - public static double length(int var0, double var1, int var3) { - return Math.sqrt((var0 * var0) + var1 * var1 + (var3 * var3)); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/Palette.java b/core/src/main/java/com/volmit/iris/util/data/palette/Palette.java deleted file mode 100644 index 27c8d4407..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/Palette.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import java.util.List; - -public interface Palette { - int idFor(T paramT); - - T valueFor(int paramInt); - - int getSize(); - - void read(List fromList); - - void write(List toList); -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/PaletteAccess.java b/core/src/main/java/com/volmit/iris/util/data/palette/PaletteAccess.java deleted file mode 100644 index b860edb21..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/PaletteAccess.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import com.volmit.iris.util.nbt.tag.CompoundTag; - -public interface PaletteAccess { - void setBlock(int x, int y, int z, CompoundTag data); - - CompoundTag getBlock(int x, int y, int z); - - void writeToSection(CompoundTag tag); - - void readFromSection(CompoundTag tag); -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/PaletteResize.java b/core/src/main/java/com/volmit/iris/util/data/palette/PaletteResize.java deleted file mode 100644 index b4fcade1b..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/PaletteResize.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -interface PaletteResize { - int onResize(int paramInt, T paramT); -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/PaletteType.java b/core/src/main/java/com/volmit/iris/util/data/palette/PaletteType.java deleted file mode 100644 index 039b821b7..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/PaletteType.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import com.volmit.iris.util.data.Varint; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -public interface PaletteType { - void writePaletteNode(DataOutputStream dos, T t) throws IOException; - - T readPaletteNode(DataInputStream din) throws IOException; - - default void writeList(DataOutputStream dos, List list) throws IOException { - Varint.writeUnsignedVarInt(list.size(), dos); - for (T i : list) { - writePaletteNode(dos, i); - } - } - - default List readList(DataInputStream din) throws IOException { - int v = Varint.readUnsignedVarInt(din); - List t = new ArrayList<>(); - - for (int i = 0; i < v; i++) { - t.add(readPaletteNode(din)); - } - - return t; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/PalettedContainer.java b/core/src/main/java/com/volmit/iris/util/data/palette/PalettedContainer.java deleted file mode 100644 index eb3601c32..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/PalettedContainer.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.math.M; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; - -import java.util.List; - -public class PalettedContainer implements PaletteResize { - public static final int GLOBAL_PALETTE_BITS = 9; - public static final int MIN_PALETTE_SIZE = 4; - private static final int SIZE = 4096; - private final PaletteResize dummyPaletteResize = (var0, var1) -> 0; - protected BitStorage storage; - private Palette palette; - private int bits; - - public PalettedContainer() { - setBits(4); - } - - private static int getIndex(int var0, int var1, int var2) { - return var1 << 8 | var2 << 4 | var0; - } - - private void setBits(int var0) { - if (var0 == this.bits) { - return; - } - this.bits = var0; - if (this.bits <= 4) { - this.bits = 4; - this.palette = new LinearPalette<>(this.bits, this); - } else { - this.palette = new HashMapPalette<>(this.bits, this); - } - - this.palette.idFor(null); - this.storage = new BitStorage(this.bits, 4096); - } - - public int onResize(int var0, T var1) { - BitStorage var2 = this.storage; - Palette var3 = this.palette; - setBits(var0); - for (int var4 = 0; var4 < var2.getSize(); var4++) { - T var5 = var3.valueFor(var2.get(var4)); - if (var5 != null) { - set(var4, var5); - } - } - - return this.palette.idFor(var1); - } - - public T getAndSet(int var0, int var1, int var2, T var3) { - return getAndSet(getIndex(var0, var1, var2), var3); - } - - public T getAndSetUnchecked(int var0, int var1, int var2, T var3) { - return getAndSet(getIndex(var0, var1, var2), var3); - } - - private T getAndSet(int var0, T var1) { - int var2 = this.palette.idFor(var1); - int var3 = this.storage.getAndSet(var0, var2); - return this.palette.valueFor(var3); - } - - public void set(int var0, int var1, int var2, T var3) { - set(getIndex(var0, var1, var2), var3); - } - - private void set(int var0, T var1) { - int var2 = this.palette.idFor(var1); - - if (M.r(0.003)) { - Iris.info("ID for " + var1 + " is " + var2 + " Palette: " + palette.getSize()); - } - - this.storage.set(var0, var2); - } - - public T get(int var0, int var1, int var2) { - return get(getIndex(var0, var1, var2)); - } - - protected T get(int var0) { - return this.palette.valueFor(this.storage.get(var0)); - } - - public void read(List palette, long[] data) { - int var2 = Math.max(4, Mth.ceillog2(palette.size())); - if (var2 != this.bits) { - setBits(var2); - } - - this.palette.read(palette); - int var3 = data.length * 64 / 4096; - if (var3 == this.bits) { - System.arraycopy(data, 0, this.storage.getRaw(), 0, data.length); - } else { - BitStorage var4 = new BitStorage(var3, 4096, data); - for (int var5 = 0; var5 < 4096; var5++) { - this.storage.set(var5, var4.get(var5)); - } - } - } - - public long[] write(List toList) { - HashMapPalette var3 = new HashMapPalette<>(this.bits, this.dummyPaletteResize); - T var4 = null; - int var5 = 0; - int[] var6 = new int[4096]; - for (int i = 0; i < 4096; i++) { - T t = get(i); - if (t != var4) { - var4 = t; - var5 = var3.idFor(t); - } - var6[i] = var5; - } - - var3.write(toList); - int var8 = Math.max(4, Mth.ceillog2(toList.size())); - BitStorage var9 = new BitStorage(var8, 4096); - for (int var10 = 0; var10 < var6.length; var10++) { - var9.set(var10, var6[var10]); - } - return var9.getRaw(); - } - - public void count(CountConsumer var0) { - Int2IntOpenHashMap int2IntOpenHashMap = new Int2IntOpenHashMap(); - this.storage.getAll(var1 -> int2IntOpenHashMap.put(var1, int2IntOpenHashMap.get(var1) + 1)); - int2IntOpenHashMap.int2IntEntrySet().forEach(var1 -> var0.accept(this.palette.valueFor(var1.getIntKey()), var1.getIntValue())); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/data/palette/QuartPos.java b/core/src/main/java/com/volmit/iris/util/data/palette/QuartPos.java deleted file mode 100644 index ca5cb8e45..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/palette/QuartPos.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.data.palette; - -public final class QuartPos { - public static final int BITS = 2; - - public static final int SIZE = 4; - - private static final int SECTION_TO_QUARTS_BITS = 2; - - public static int fromBlock(int var0) { - return var0 >> 2; - } - - public static int toBlock(int var0) { - return var0 << 2; - } - - public static int fromSection(int var0) { - return var0 << 2; - } - - public static int toSection(int var0) { - return var0 >> 2; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/data/registry/Materials.java b/core/src/main/java/com/volmit/iris/util/data/registry/Materials.java deleted file mode 100644 index fca4fb97b..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/registry/Materials.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.volmit.iris.util.data.registry; - -import org.bukkit.Material; - -import static com.volmit.iris.util.data.registry.RegistryUtil.find; - -public class Materials { - public static final Material GRASS = find(Material.class, "grass", "short_grass"); -} diff --git a/core/src/main/java/com/volmit/iris/util/data/registry/Particles.java b/core/src/main/java/com/volmit/iris/util/data/registry/Particles.java deleted file mode 100644 index 3bc67d663..000000000 --- a/core/src/main/java/com/volmit/iris/util/data/registry/Particles.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.volmit.iris.util.data.registry; - -import org.bukkit.Particle; - -import static com.volmit.iris.util.data.registry.RegistryUtil.find; - -public class Particles { - public static final Particle CRIT_MAGIC = find(Particle.class, "crit_magic", "crit"); - public static final Particle REDSTONE = find(Particle.class, "redstone", "dust"); - public static final Particle ITEM = find(Particle.class, "item_crack", "item"); -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/DecreeContext.java b/core/src/main/java/com/volmit/iris/util/decree/DecreeContext.java deleted file mode 100644 index b19fe32f4..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/DecreeContext.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree; - -import com.volmit.iris.util.plugin.VolmitSender; - -public class DecreeContext { - private static final ThreadLocal context = new ThreadLocal<>(); - - public static VolmitSender get() { - return context.get(); - } - - public static void touch(VolmitSender c) { - context.set(c); - } - - public static void remove() { - context.remove(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/DecreeContextHandler.java b/core/src/main/java/com/volmit/iris/util/decree/DecreeContextHandler.java deleted file mode 100644 index faa2a29b3..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/DecreeContextHandler.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.plugin.VolmitSender; - -public interface DecreeContextHandler { - KMap, DecreeContextHandler> contextHandlers = buildContextHandlers(); - - static KMap, DecreeContextHandler> buildContextHandlers() { - KMap, DecreeContextHandler> contextHandlers = new KMap<>(); - - try { - Iris.initialize("com.volmit.iris.util.decree.context").forEach((i) - -> contextHandlers.put(((DecreeContextHandler) i).getType(), (DecreeContextHandler) i)); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return contextHandlers; - } - - Class getType(); - - T handle(VolmitSender sender); -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/DecreeExecutor.java b/core/src/main/java/com/volmit/iris/util/decree/DecreeExecutor.java deleted file mode 100644 index 12404d1c0..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/DecreeExecutor.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.platform.PlatformChunkGenerator; -import com.volmit.iris.util.plugin.VolmitSender; -import org.bukkit.World; -import org.bukkit.entity.Player; - -public interface DecreeExecutor { - default VolmitSender sender() { - return DecreeContext.get(); - } - - default Player player() { - return sender().player(); - } - - default IrisData data() { - var access = access(); - if (access != null) { - return access.getData(); - } - return null; - } - - default Engine engine() { - if (sender().isPlayer() && IrisToolbelt.access(sender().player().getWorld()) != null) { - PlatformChunkGenerator gen = IrisToolbelt.access(sender().player().getWorld()); - if (gen != null) { - return gen.getEngine(); - } - } - - return null; - } - - default PlatformChunkGenerator access() { - if (sender().isPlayer()) { - return IrisToolbelt.access(world()); - } - return null; - } - - default World world() { - if (sender().isPlayer()) { - return sender().player().getWorld(); - } - return null; - } - - default T get(T v, T ifUndefined) { - return v == null ? ifUndefined : v; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/DecreeNode.java b/core/src/main/java/com/volmit/iris/util/decree/DecreeNode.java deleted file mode 100644 index 488108699..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/DecreeNode.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.annotations.Param; -import lombok.Data; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; - -@Data -public class DecreeNode { - private final Method method; - private final Object instance; - private final Decree decree; - - public DecreeNode(Object instance, Method method) { - this.instance = instance; - this.method = method; - this.decree = method.getDeclaredAnnotation(Decree.class); - if (decree == null) { - throw new RuntimeException("Cannot instantiate DecreeNode on method " + method.getName() + " in " + method.getDeclaringClass().getCanonicalName() + " not annotated by @Decree"); - } - } - - /** - * Get the parameters of this decree node - * - * @return The list of parameters if ALL are annotated by @{@link Param}, else null - */ - public KList getParameters() { - KList required = new KList<>(); - KList optional = new KList<>(); - - for (Parameter i : method.getParameters()) { - DecreeParameter p = new DecreeParameter(i); - if (p.isRequired()) { - required.add(p); - } else { - optional.add(p); - } - } - - required.addAll(optional); - - return required; - } - - public String getName() { - return decree.name().isEmpty() ? method.getName() : decree.name(); - } - - public DecreeOrigin getOrigin() { - return decree.origin(); - } - - public String getDescription() { - return decree.description().isEmpty() ? Decree.DEFAULT_DESCRIPTION : decree.description(); - } - - public KList getNames() { - KList d = new KList<>(); - d.add(getName()); - - for (String i : decree.aliases()) { - if (i.isEmpty()) { - continue; - } - - d.add(i); - } - - - d.removeDuplicates(); - return d; - } - - public boolean isSync() { - return decree.sync(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/DecreeOrigin.java b/core/src/main/java/com/volmit/iris/util/decree/DecreeOrigin.java deleted file mode 100644 index 5c09e0790..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/DecreeOrigin.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree; - -import com.volmit.iris.util.plugin.VolmitSender; - -public enum DecreeOrigin { - PLAYER, - CONSOLE, - /** - * Both the player and the console - */ - BOTH; - - /** - * Check if the origin is valid for a sender - * - * @param sender The sender to check - * @return True if valid for origin - */ - public boolean validFor(VolmitSender sender) { - if (sender.isPlayer()) { - return this.equals(PLAYER) || this.equals(BOTH); - } else { - return this.equals(CONSOLE) || this.equals(BOTH); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/DecreeParameter.java b/core/src/main/java/com/volmit/iris/util/decree/DecreeParameter.java deleted file mode 100644 index 3a977f87b..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/DecreeParameter.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree; - -import com.volmit.iris.engine.data.cache.AtomicCache; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.annotations.Param; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.decree.specialhandlers.DummyHandler; -import lombok.Data; - -import java.lang.reflect.Parameter; - -@Data -public class DecreeParameter { - private final Parameter parameter; - private final Param param; - private transient final AtomicCache> handlerCache = new AtomicCache<>(); - - public DecreeParameter(Parameter parameter) { - this.parameter = parameter; - this.param = parameter.getDeclaredAnnotation(Param.class); - if (param == null) { - throw new RuntimeException("Cannot instantiate DecreeParameter on " + parameter.getName() + " in method " + parameter.getDeclaringExecutable().getName() + "(...) in class " + parameter.getDeclaringExecutable().getDeclaringClass().getCanonicalName() + " not annotated by @Param"); - } - } - - public DecreeParameterHandler getHandler() { - return handlerCache.aquire(() -> { - try { - if (param.customHandler().equals(DummyHandler.class)) { - return DecreeSystem.getHandler(getType()); - } - - return param.customHandler().getConstructor().newInstance(); - } catch (Throwable e) { - e.printStackTrace(); - } - - return null; - }); - } - - public Class getType() { - return parameter.getType(); - } - - public String getName() { - return param.name().isEmpty() ? parameter.getName() : param.name(); - } - - public String getDescription() { - return param.description().isEmpty() ? Param.DEFAULT_DESCRIPTION : param.description(); - } - - public boolean isRequired() { - return !hasDefault(); - } - - public KList getNames() { - KList d = new KList<>(); - - for (String i : param.aliases()) { - if (i.isEmpty()) { - continue; - } - - d.add(i); - } - - d.add(getName()); - d.removeDuplicates(); - - return d; - } - - public Object getDefaultValue() throws DecreeParsingException { - return param.defaultValue().trim().isEmpty() ? null : getHandler().parse(param.defaultValue().trim(), true); - } - - public boolean hasDefault() { - return !param.defaultValue().trim().isEmpty(); - } - - public String example() { - KList ff = getHandler().getPossibilities(); - ff = ff != null ? ff : new KList<>(); - KList f = ff.convert((i) -> getHandler().toStringForce(i)); - if (f.isEmpty()) { - f = new KList<>(); - f.add(getHandler().getRandomDefault()); - } - - return f.getRandom(); - } - - public boolean isContextual() { - return param.contextual(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/DecreeParameterHandler.java b/core/src/main/java/com/volmit/iris/util/decree/DecreeParameterHandler.java deleted file mode 100644 index 097361799..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/DecreeParameterHandler.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; - -import java.util.concurrent.atomic.AtomicReference; - -public interface DecreeParameterHandler extends DecreeExecutor { - /** - * Should return the possible values for this type - * - * @return Possibilities for this type. - */ - KList getPossibilities(); - - default boolean isDummy() { - return false; - } - - /** - * Converting the type back to a string (inverse of the {@link #parse(String) parse} method) - * - * @param t The input of the designated type to convert to a String - * @return The resulting string - */ - String toString(T t); - - /** - * Forces conversion to the designated type before converting to a string using {@link #toString(T t)} - * - * @param t The object to convert to string (that should be of this type) - * @return The resulting string. - */ - default String toStringForce(Object t) { - return toString((T) t); - } - - /** - * Should parse a String into the designated type - * - * @param in The string to parse - * @return The value extracted from the string, of the designated type - * @throws DecreeParsingException Thrown when the parsing fails (ex: "oop" translated to an integer throws this) - */ - default T parse(String in) throws DecreeParsingException { - return parse(in, false); - } - - /** - * Should parse a String into the designated type. You can force it to not throw a whichexception - * - * @param in The string to parse - * @param force force an option instead of throwing decreewhich - * @return The value extracted from the string, of the designated type - * @throws DecreeParsingException Thrown when the parsing fails (ex: "oop" translated to an integer throws this) - */ - T parse(String in, boolean force) throws DecreeParsingException; - - /** - * Returns whether a certain type is supported by this handler
- * - * @param type The type to check - * @return True if supported, false if not - */ - boolean supports(Class type); - - /** - * The possible entries for the inputted string (support for autocomplete on partial entries) - * - * @param input The inputted string to check against - * @return A {@link KList} of possibilities - */ - default KList getPossibilities(String input) { - if (input.trim().isEmpty()) { - KList f = getPossibilities(); - return f == null ? new KList<>() : f; - } - - input = input.trim(); - KList possible = getPossibilities(); - KList matches = new KList<>(); - - if (possible == null || possible.isEmpty()) { - return matches; - } - - if (input.isEmpty()) { - return getPossibilities(); - } - - KList converted = possible.convert(v -> toString(v).trim()); - - for (int i = 0; i < converted.size(); i++) { - String g = converted.get(i); - // if - // G == I or - // I in G or - // G in I - if (g.equalsIgnoreCase(input) || g.toLowerCase().contains(input.toLowerCase()) || input.toLowerCase().contains(g.toLowerCase())) { - matches.add(possible.get(i)); - } - } - - return matches; - } - - default String getRandomDefault() { - return "NOEXAMPLE"; - } - - default double getMultiplier(AtomicReference g) { - double multiplier = 1; - String in = g.get(); - boolean valid = true; - while (valid) { - boolean trim = false; - if (in.toLowerCase().endsWith("k")) { - multiplier *= 1000; - trim = true; - } else if (in.toLowerCase().endsWith("m")) { - multiplier *= 1000000; - trim = true; - } else if (in.toLowerCase().endsWith("h")) { - multiplier *= 100; - trim = true; - } else if (in.toLowerCase().endsWith("c")) { - multiplier *= 16; - trim = true; - } else if (in.toLowerCase().endsWith("r")) { - multiplier *= (16 * 32); - trim = true; - } else { - valid = false; - } - - if (trim) { - in = in.substring(0, in.length() - 1); - } - } - - g.set(in); - return multiplier; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/DecreeSystem.java b/core/src/main/java/com/volmit/iris/util/decree/DecreeSystem.java deleted file mode 100644 index 5edacb51f..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/DecreeSystem.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.virtual.VirtualDecreeCommand; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.Sound; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public interface DecreeSystem extends CommandExecutor, TabCompleter { - KList> handlers = Iris.initialize("com.volmit.iris.util.decree.handlers", null).convert((i) -> (DecreeParameterHandler) i); - - static KList enhanceArgs(String[] args) { - return enhanceArgs(args, true); - } - - static KList enhanceArgs(String[] args, boolean trim) { - KList a = new KList<>(); - - if (args.length == 0) { - return a; - } - - StringBuilder flat = new StringBuilder(); - for (String i : args) { - if (trim) { - if (i.trim().isEmpty()) { - continue; - } - - flat.append(" ").append(i.trim()); - } else { - if (i.endsWith(" ")) { - flat.append(" ").append(i.trim()).append(" "); - } - } - } - - flat = new StringBuilder(flat.length() > 0 ? trim ? flat.toString().trim().length() > 0 ? flat.substring(1).trim() : flat.toString().trim() : flat.substring(1) : flat); - StringBuilder arg = new StringBuilder(); - boolean quoting = false; - - for (int x = 0; x < flat.length(); x++) { - char i = flat.charAt(x); - char j = x < flat.length() - 1 ? flat.charAt(x + 1) : i; - boolean hasNext = x < flat.length(); - - if (i == ' ' && !quoting) { - if (!arg.toString().trim().isEmpty() && trim) { - a.add(arg.toString().trim()); - arg = new StringBuilder(); - } - } else if (i == '"') { - if (!quoting && (arg.length() == 0)) { - quoting = true; - } else if (quoting) { - quoting = false; - - if (hasNext && j == ' ') { - if (!arg.toString().trim().isEmpty() && trim) { - a.add(arg.toString().trim()); - arg = new StringBuilder(); - } - } else if (!hasNext) { - if (!arg.toString().trim().isEmpty() && trim) { - a.add(arg.toString().trim()); - arg = new StringBuilder(); - } - } - } - } else { - arg.append(i); - } - } - - if (!arg.toString().trim().isEmpty() && trim) { - a.add(arg.toString().trim()); - } - - return a; - } - - /** - * Get the handler for the specified type - * - * @param type The type to handle - * @return The corresponding {@link DecreeParameterHandler}, or null - */ - static DecreeParameterHandler getHandler(Class type) { - for (DecreeParameterHandler i : handlers) { - if (i.supports(type)) { - return i; - } - } - Iris.error("Unhandled type in Decree Parameter: " + type.getName() + ". This is bad!"); - return null; - } - - /** - * The root class to start command searching from - */ - VirtualDecreeCommand getRoot(); - - default boolean call(VolmitSender sender, String[] args) { - DecreeContext.touch(sender); - try { - return getRoot().invoke(sender, enhanceArgs(args)); - } finally { - DecreeContext.remove(); - } - } - - @Nullable - @Override - default List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { - DecreeContext.touch(new VolmitSender(sender)); - try { - KList enhanced = new KList<>(args); - KList v = getRoot().tabComplete(enhanced, enhanced.toString(" ")); - v.removeDuplicates(); - - if (sender instanceof Player) { - if (IrisSettings.get().getGeneral().isCommandSounds()) { - ((Player) sender).playSound(((Player) sender).getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f)); - } - } - - return v; - } finally { - DecreeContext.remove(); - } - } - - @Override - default boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!sender.hasPermission("iris.all")) { - sender.sendMessage("You lack the Permission 'iris.all'"); - return true; - } - - J.aBukkit(() -> { - var volmit = new VolmitSender(sender); - if (!call(volmit, args)) { - - if (IrisSettings.get().getGeneral().isCommandSounds()) { - if (sender instanceof Player) { - ((Player) sender).playSound(((Player) sender).getLocation(), Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f); - ((Player) sender).playSound(((Player) sender).getLocation(), Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f); - } - } - - volmit.sendMessage(C.RED + "Unknown Iris Command"); - } else { - if (IrisSettings.get().getGeneral().isCommandSounds()) { - if (sender instanceof Player) { - ((Player) sender).playSound(((Player) sender).getLocation(), Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f); - ((Player) sender).playSound(((Player) sender).getLocation(), Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f); - } - } - } - }); - return true; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/annotations/Decree.java b/core/src/main/java/com/volmit/iris/util/decree/annotations/Decree.java deleted file mode 100644 index 37b6324ef..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/annotations/Decree.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.annotations; - -import com.volmit.iris.util.decree.DecreeOrigin; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface Decree { - - String DEFAULT_DESCRIPTION = "No Description Provided"; - - /** - * The name of this command, which is the Method's name by default - */ - String name() default ""; - - /** - * Only allow if studio mode is enabled - * - * @return defaults to false - */ - boolean studio() default false; - - /** - * If the node's functions MUST be run in sync, set this to true.
- * Defaults to false - */ - boolean sync() default false; - - /** - * The description of this command.
- * Is {@link #DEFAULT_DESCRIPTION} by default - */ - String description() default DEFAULT_DESCRIPTION; - - /** - * The origin this command must come from.
- * Must be elements of the {@link DecreeOrigin} enum
- * By default, is {@link DecreeOrigin#BOTH}, meaning both console & player can send the command - */ - DecreeOrigin origin() default DecreeOrigin.BOTH; - - /** - * The aliases of this parameter (instead of just the {@link #name() name} (if specified) or Method Name (name of - * method))
- * Can be initialized as just a string (ex. "alias") or as an array (ex. {"alias1", "alias2"})
- * If someone uses /plugin foo and you specify alias="f" here, /plugin f will do the exact same. - */ - String[] aliases() default ""; -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/annotations/Param.java b/core/src/main/java/com/volmit/iris/util/decree/annotations/Param.java deleted file mode 100644 index 382cc42f3..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/annotations/Param.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.annotations; - -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.specialhandlers.DummyHandler; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PARAMETER) -public @interface Param { - String DEFAULT_DESCRIPTION = "No Description Provided"; - - /** - * The main name of this command.
- * Required parameter.
- * This is what is used in game, alongside any (if specified) {@link #aliases() aliases} - */ - String name() default ""; - - /** - * The description of this parameter, used in help-popups in game.
- * The default value is {@link #DEFAULT_DESCRIPTION} - */ - String description() default DEFAULT_DESCRIPTION; - - /** - * The default value for this argument.
- * The entered string is parsed to the value similarly to how commandline-text would be.
- * Which indicates the variable MUST be defined by the person running the command.
- * If you define this, the variable automatically becomes non-required, but can still be set. - */ - String defaultValue() default ""; - - /** - * The aliases of this parameter (instead of just the {@link #name() name} (if specified) or Method Name (name of - * method))
- * Can be initialized as just a string (ex. "alias") or as an array (ex. {"alias1", "alias2"})
- * If someone uses /plugin foo bar=baz and you specify alias="b" here, /plugin foo b=baz will do the exact same. - */ - String[] aliases() default ""; - - /** - * Attempts to dynamically pull context from the player, default data or something else for supported types - */ - boolean contextual() default false; - - Class> customHandler() default DummyHandler.class; -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/context/BiomeContextHandler.java b/core/src/main/java/com/volmit/iris/util/decree/context/BiomeContextHandler.java deleted file mode 100644 index 3389a6a19..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/context/BiomeContextHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.context; - -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.object.IrisBiome; -import com.volmit.iris.util.decree.DecreeContextHandler; -import com.volmit.iris.util.plugin.VolmitSender; - -public class BiomeContextHandler implements DecreeContextHandler { - public Class getType() { - return IrisBiome.class; - } - - public IrisBiome handle(VolmitSender sender) { - if (sender.isPlayer() - && IrisToolbelt.isIrisWorld(sender.player().getWorld()) - && IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) { - return IrisToolbelt.access(sender.player().getWorld()).getEngine().getBiomeOrMantle(sender.player().getLocation()); - } - - return null; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/context/GeneratorContextHandler.java b/core/src/main/java/com/volmit/iris/util/decree/context/GeneratorContextHandler.java deleted file mode 100644 index d6843037d..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/context/GeneratorContextHandler.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.context; - -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.object.IrisGenerator; -import com.volmit.iris.util.decree.DecreeContextHandler; -import com.volmit.iris.util.plugin.VolmitSender; - -public class GeneratorContextHandler implements DecreeContextHandler { - @Override - public Class getType() { - return IrisGenerator.class; - } - - @Override - public IrisGenerator handle(VolmitSender sender) { - if (sender.isPlayer() - && IrisToolbelt.isIrisWorld(sender.player().getWorld()) - && IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) { - Engine engine = IrisToolbelt.access(sender.player().getWorld()).getEngine(); - return engine.getData().getGeneratorLoader().load(engine.getBiome(sender.player().getLocation()).getGenerators().getRandom().getGenerator()); - } - - return null; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/context/RegionContextHandler.java b/core/src/main/java/com/volmit/iris/util/decree/context/RegionContextHandler.java deleted file mode 100644 index 5ed09c879..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/context/RegionContextHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.context; - -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.object.IrisRegion; -import com.volmit.iris.util.decree.DecreeContextHandler; -import com.volmit.iris.util.plugin.VolmitSender; - -public class RegionContextHandler implements DecreeContextHandler { - public Class getType() { - return IrisRegion.class; - } - - public IrisRegion handle(VolmitSender sender) { - if (sender.isPlayer() - && IrisToolbelt.isIrisWorld(sender.player().getWorld()) - && IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) { - return IrisToolbelt.access(sender.player().getWorld()).getEngine().getRegion(sender.player().getLocation()); - } - - return null; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/context/WorldContextHandler.java b/core/src/main/java/com/volmit/iris/util/decree/context/WorldContextHandler.java deleted file mode 100644 index f340b716d..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/context/WorldContextHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.context; - -import com.volmit.iris.util.decree.DecreeContextHandler; -import com.volmit.iris.util.plugin.VolmitSender; -import org.bukkit.World; - -public class WorldContextHandler implements DecreeContextHandler { - public Class getType() { - return World.class; - } - - public World handle(VolmitSender sender) { - return sender.isPlayer() ? sender.player().getWorld() : null; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/exceptions/DecreeParsingException.java b/core/src/main/java/com/volmit/iris/util/decree/exceptions/DecreeParsingException.java deleted file mode 100644 index 01c02c231..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/exceptions/DecreeParsingException.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.exceptions; - -/** - * Thrown when a decree parameter is parsed, but parsing fails - */ -public class DecreeParsingException extends Exception { - public DecreeParsingException(String message) { - super(message); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/BlockVectorHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/BlockVectorHandler.java deleted file mode 100644 index 468050126..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/BlockVectorHandler.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeContext; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.DecreeSystem; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.plugin.VolmitSender; -import org.bukkit.FluidCollisionMode; -import org.bukkit.entity.Player; -import org.bukkit.util.BlockVector; - -public class BlockVectorHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - KList vx = new KList<>(); - VolmitSender s = DecreeContext.get(); - - if (s.isPlayer()) { - vx.add(s.player().getLocation().toVector().toBlockVector()); - } - - return vx; - } - - @Override - public String toString(BlockVector v) { - if (v.getY() == 0) { - return Form.f(v.getBlockX(), 2) + "," + Form.f(v.getBlockZ(), 2); - } - - return Form.f(v.getBlockX(), 2) + "," + Form.f(v.getBlockY(), 2) + "," + Form.f(v.getBlockZ(), 2); - } - - @Override - public BlockVector parse(String in, boolean force) throws DecreeParsingException { - try { - if (in.contains(",")) { - String[] comp = in.split("\\Q,\\E"); - - if (comp.length == 2) { - return new BlockVector(Integer.parseInt(comp[0].trim()), 0, Integer.parseInt(comp[1].trim())); - } else if (comp.length == 3) { - return new BlockVector(Integer.parseInt(comp[0].trim()), - Integer.parseInt(comp[1].trim()), - Integer.parseInt(comp[2].trim())); - } else { - throw new DecreeParsingException("Could not parse components for vector. You have " + comp.length + " components. Expected 2 or 3."); - } - } else if (in.equalsIgnoreCase("here") || in.equalsIgnoreCase("me") || in.equalsIgnoreCase("self")) { - if (!DecreeContext.get().isPlayer()) { - throw new DecreeParsingException("You cannot specify me,self,here as a console."); - } - - return DecreeContext.get().player().getLocation().toVector().toBlockVector(); - } else if (in.equalsIgnoreCase("look") || in.equalsIgnoreCase("cursor") || in.equalsIgnoreCase("crosshair")) { - if (!DecreeContext.get().isPlayer()) { - throw new DecreeParsingException("You cannot specify look,cursor,crosshair as a console."); - } - - return DecreeContext.get().player().getTargetBlockExact(256, FluidCollisionMode.NEVER).getLocation().toVector().toBlockVector(); - } else if (in.trim().toLowerCase().startsWith("player:")) { - String v = in.trim().split("\\Q:\\E")[1]; - - - KList px = DecreeSystem.getHandler(Player.class).getPossibilities(v); - - if (px != null && px.isNotEmpty()) { - return ((Player) px.get(0)).getLocation().toVector().toBlockVector(); - } else if (px == null || px.isEmpty()) { - throw new DecreeParsingException("Cannot find player: " + v); - } - } - } catch (Throwable e) { - throw new DecreeParsingException("Unable to get Vector for \"" + in + "\" because of an uncaught exception: " + e); - } - - return null; - } - - @Override - public boolean supports(Class type) { - return type.equals(BlockVector.class); - } - - @Override - public String getRandomDefault() { - return M.r(0.5) ? "0,0" : "0,0,0"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/BooleanHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/BooleanHandler.java deleted file mode 100644 index 5109425d9..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/BooleanHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.math.M; - -public class BooleanHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return null; - } - - @Override - public String toString(Boolean aByte) { - return aByte.toString(); - } - - @Override - public Boolean parse(String in, boolean force) throws DecreeParsingException { - try { - if (in.equals("null") || in.equals("other") || in.equals("flip")) { - return null; - } - return Boolean.parseBoolean(in); - } catch (Throwable e) { - throw new DecreeParsingException("Unable to parse boolean \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(Boolean.class) || type.equals(boolean.class); - } - - @Override - public String getRandomDefault() { - return M.r(0.5) + ""; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/ByteHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/ByteHandler.java deleted file mode 100644 index 9c4257529..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/ByteHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.math.RNG; - -public class ByteHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return null; - } - - @Override - public String toString(Byte aByte) { - return aByte.toString(); - } - - @Override - public Byte parse(String in, boolean force) throws DecreeParsingException { - try { - return Byte.parseByte(in); - } catch (Throwable e) { - throw new DecreeParsingException("Unable to parse byte \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(Byte.class) || type.equals(byte.class); - } - - @Override - public String getRandomDefault() { - return RNG.r.i(0, Byte.MAX_VALUE) + ""; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/CaveHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/CaveHandler.java deleted file mode 100644 index bd14aab68..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/CaveHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.engine.object.IrisCave; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; - -public class CaveHandler extends RegistrantHandler { - public CaveHandler() { - super(IrisCave.class, true); - } - - @Override - public String getRandomDefault() { - return "cave"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/DataVersionHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/DataVersionHandler.java deleted file mode 100644 index 53dff488e..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/DataVersionHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.core.nms.datapack.DataVersion; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; - -public class DataVersionHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return new KList<>(DataVersion.values()).qdel(DataVersion.UNSUPPORTED); - } - - @Override - public String toString(DataVersion version) { - return version.getVersion(); - } - - @Override - public DataVersion parse(String in, boolean force) throws DecreeParsingException { - if (in.equalsIgnoreCase("latest")) { - return DataVersion.getLatest(); - } - for (DataVersion v : DataVersion.values()) { - if (v.getVersion().equalsIgnoreCase(in)) { - return v; - } - } - throw new DecreeParsingException("Unable to parse data version \"" + in + "\""); - } - - @Override - public boolean supports(Class type) { - return DataVersion.class.equals(type); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/DimensionHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/DimensionHandler.java deleted file mode 100644 index d8eeb998c..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/DimensionHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; - -public class DimensionHandler extends RegistrantHandler { - public DimensionHandler() { - super(IrisDimension.class, false); - } - - @Override - public IrisDimension parse(String in, boolean force) throws DecreeParsingException { - if (in.equalsIgnoreCase("default")) { - return parse(IrisSettings.get().getGenerator().getDefaultWorldType()); - } - return super.parse(in, force); - } - - @Override - public String getRandomDefault() { - return "dimension"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/DoubleHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/DoubleHandler.java deleted file mode 100644 index 1918c6292..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/DoubleHandler.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.RNG; - -import java.util.concurrent.atomic.AtomicReference; - -public class DoubleHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return null; - } - - @Override - public Double parse(String in, boolean force) throws DecreeParsingException { - try { - AtomicReference r = new AtomicReference<>(in); - double m = getMultiplier(r); - return Double.parseDouble(r.get()) * m; - } catch (Throwable e) { - throw new DecreeParsingException("Unable to parse double \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(Double.class) || type.equals(double.class); - } - - @Override - public String toString(Double f) { - return f.toString(); - } - - @Override - public String getRandomDefault() { - return Form.f(RNG.r.d(0, 99.99), 1) + ""; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/FloatHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/FloatHandler.java deleted file mode 100644 index f4085e64f..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/FloatHandler.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.RNG; - -import java.util.concurrent.atomic.AtomicReference; - -public class FloatHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return null; - } - - @Override - public Float parse(String in, boolean force) throws DecreeParsingException { - try { - AtomicReference r = new AtomicReference<>(in); - double m = getMultiplier(r); - return (float) (Float.parseFloat(r.get()) * m); - } catch (Throwable e) { - throw new DecreeParsingException("Unable to parse float \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(Float.class) || type.equals(float.class); - } - - @Override - public String toString(Float f) { - return f.toString(); - } - - @Override - public String getRandomDefault() { - return Form.f(RNG.r.d(0, 99.99), 1) + ""; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/IntegerHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/IntegerHandler.java deleted file mode 100644 index c8579b9b0..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/IntegerHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.math.RNG; - -import java.util.concurrent.atomic.AtomicReference; - -public class IntegerHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return null; - } - - @Override - public Integer parse(String in, boolean force) throws DecreeParsingException { - try { - AtomicReference r = new AtomicReference<>(in); - double m = getMultiplier(r); - return (int) (Integer.valueOf(r.get()).doubleValue() * m); - } catch (Throwable e) { - throw new DecreeParsingException("Unable to parse integer \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(Integer.class) || type.equals(int.class); - } - - @Override - public String toString(Integer f) { - return f.toString(); - } - - @Override - public String getRandomDefault() { - return RNG.r.i(0, 99) + ""; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/JigsawPieceHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/JigsawPieceHandler.java deleted file mode 100644 index 3199d3039..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/JigsawPieceHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.engine.object.IrisJigsawPiece; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; - -public class JigsawPieceHandler extends RegistrantHandler { - public JigsawPieceHandler() { - super(IrisJigsawPiece.class, true); - } - - @Override - public String getRandomDefault() { - return "jigsaw-piece"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/JigsawPoolHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/JigsawPoolHandler.java deleted file mode 100644 index 83a14ab4c..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/JigsawPoolHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.engine.object.IrisJigsawPool; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; - -public class JigsawPoolHandler extends RegistrantHandler { - public JigsawPoolHandler() { - super(IrisJigsawPool.class, true); - } - - @Override - public String getRandomDefault() { - return "jigsaw-pool"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/JigsawStructureHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/JigsawStructureHandler.java deleted file mode 100644 index b7d6e35cd..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/JigsawStructureHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.engine.object.IrisJigsawStructure; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; - -public class JigsawStructureHandler extends RegistrantHandler { - public JigsawStructureHandler() { - super(IrisJigsawStructure.class, true); - } - - @Override - public String getRandomDefault() { - return "jigsaw-structure"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/LongHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/LongHandler.java deleted file mode 100644 index d973bc7ea..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/LongHandler.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.math.RNG; - -import java.util.concurrent.atomic.AtomicReference; - -public class LongHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return null; - } - - @Override - public Long parse(String in, boolean force) throws DecreeParsingException { - try { - AtomicReference r = new AtomicReference<>(in); - double m = getMultiplier(r); - if (m == 1) - return Long.parseLong(r.get()); - else - return (long) (Long.valueOf(r.get()).doubleValue() * m); - } catch (Throwable e) { - throw new DecreeParsingException("Unable to parse long \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(Long.class) || type.equals(long.class); - } - - @Override - public String toString(Long f) { - return f.toString(); - } - - @Override - public String getRandomDefault() { - return RNG.r.i(0, 99) + ""; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/PlayerHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/PlayerHandler.java deleted file mode 100644 index b0ff00ec8..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/PlayerHandler.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.ArrayList; -import java.util.stream.Collectors; - -public class PlayerHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return new KList<>(new ArrayList<>(Bukkit.getOnlinePlayers())); - } - - @Override - public String toString(Player player) { - return player.getName(); - } - - @Override - public Player parse(String in, boolean force) throws DecreeParsingException { - KList options = getPossibilities(in); - - if (options.isEmpty()) { - throw new DecreeParsingException("Unable to find Player \"" + in + "\""); - } - try { - return options.stream().filter((i) -> toString(i).equalsIgnoreCase(in)).collect(Collectors.toList()).get(0); - } catch (Throwable e) { - throw new DecreeParsingException("Unable to filter which Player \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(Player.class); - } - - @Override - public String getRandomDefault() { - return "playername"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/ScriptHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/ScriptHandler.java deleted file mode 100644 index f5eaf01a4..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/ScriptHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.engine.object.IrisScript; -import com.volmit.iris.util.decree.specialhandlers.RegistrantHandler; - -public class ScriptHandler extends RegistrantHandler { - public ScriptHandler() { - super(IrisScript.class, false); - } - - @Override - public String getRandomDefault() { - return "script"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/ShortHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/ShortHandler.java deleted file mode 100644 index 5d7cb6eaa..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/ShortHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.math.RNG; - -import java.util.concurrent.atomic.AtomicReference; - -public class ShortHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return null; - } - - @Override - public Short parse(String in, boolean force) throws DecreeParsingException { - try { - AtomicReference r = new AtomicReference<>(in); - double m = getMultiplier(r); - return (short) (Short.valueOf(r.get()).doubleValue() * m); - } catch (Throwable e) { - throw new DecreeParsingException("Unable to parse short \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(Short.class) || type.equals(short.class); - } - - @Override - public String toString(Short f) { - return f.toString(); - } - - @Override - public String getRandomDefault() { - return RNG.r.i(0, 99) + ""; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/StringHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/StringHandler.java deleted file mode 100644 index c4c6bf236..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/StringHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; - -/** - * Abstraction can sometimes breed stupidity - */ -public class StringHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return null; - } - - @Override - public String toString(String s) { - return s; - } - - @Override - public String parse(String in, boolean force) throws DecreeParsingException { - return in; - } - - @Override - public boolean supports(Class type) { - return type.equals(String.class); - } - - @Override - public String getRandomDefault() { - return new KList().qadd("text").qadd("string") - .qadd("blah").qadd("derp").qadd("yolo").getRandom(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/VectorHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/VectorHandler.java deleted file mode 100644 index b8c63c853..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/VectorHandler.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeContext; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.DecreeSystem; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.format.Form; -import org.bukkit.FluidCollisionMode; -import org.bukkit.entity.Player; -import org.bukkit.util.BlockVector; -import org.bukkit.util.Vector; - -public class VectorHandler implements DecreeParameterHandler { - - private static final KList randoms = new KList<>( - "here", - "0,0,0", - "0,0", - "look", - "player:" - ); - - @Override - public KList getPossibilities() { - return null; - } - - @Override - public String toString(Vector v) { - if (v.getY() == 0) { - return Form.f(v.getX(), 2) + "," + Form.f(v.getZ(), 2); - } - - return Form.f(v.getX(), 2) + "," + Form.f(v.getY(), 2) + "," + Form.f(v.getZ(), 2); - } - - @Override - public Vector parse(String in, boolean force) throws DecreeParsingException { - try { - if (in.contains(",")) { - String[] comp = in.split("\\Q,\\E"); - - if (comp.length == 2) { - return new BlockVector(Double.parseDouble(comp[0].trim()), 0, Double.parseDouble(comp[1].trim())); - } else if (comp.length == 3) { - return new BlockVector(Double.parseDouble(comp[0].trim()), - Double.parseDouble(comp[1].trim()), - Double.parseDouble(comp[2].trim())); - } else { - throw new DecreeParsingException("Could not parse components for vector. You have " + comp.length + " components. Expected 2 or 3."); - } - } else if (in.equalsIgnoreCase("here") || in.equalsIgnoreCase("me") || in.equalsIgnoreCase("self")) { - if (!DecreeContext.get().isPlayer()) { - throw new DecreeParsingException("You cannot specify me,self,here as a console."); - } - - return DecreeContext.get().player().getLocation().toVector(); - } else if (in.equalsIgnoreCase("look") || in.equalsIgnoreCase("cursor") || in.equalsIgnoreCase("crosshair")) { - if (!DecreeContext.get().isPlayer()) { - throw new DecreeParsingException("You cannot specify look,cursor,crosshair as a console."); - } - - return DecreeContext.get().player().getTargetBlockExact(256, FluidCollisionMode.NEVER).getLocation().toVector(); - } else if (in.trim().toLowerCase().startsWith("player:")) { - String v = in.trim().split("\\Q:\\E")[1]; - - - KList px = DecreeSystem.getHandler(Player.class).getPossibilities(v); - - if (px != null && px.isNotEmpty()) { - return ((Player) px.get(0)).getLocation().toVector(); - } else if (px == null || px.isEmpty()) { - throw new DecreeParsingException("Cannot find player: " + v); - } - } - } catch (Throwable e) { - throw new DecreeParsingException("Unable to get Vector for \"" + in + "\" because of an uncaught exception: " + e); - } - - return null; - } - - @Override - public boolean supports(Class type) { - return type.equals(Vector.class); - } - - @Override - public String getRandomDefault() { - return randoms.getRandom(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/handlers/WorldHandler.java b/core/src/main/java/com/volmit/iris/util/decree/handlers/WorldHandler.java deleted file mode 100644 index 658ba6939..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/handlers/WorldHandler.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.handlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import org.bukkit.Bukkit; -import org.bukkit.World; - -import java.util.stream.Collectors; - -public class WorldHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - KList options = new KList<>(); - for (World world : Bukkit.getWorlds()) { - if (!world.getName().toLowerCase().startsWith("iris/")) { - options.add(world); - } - } - return options; - } - - @Override - public String toString(World world) { - return world.getName(); - } - - @Override - public World parse(String in, boolean force) throws DecreeParsingException { - KList options = getPossibilities(in); - - if (options.isEmpty()) { - throw new DecreeParsingException("Unable to find World \"" + in + "\""); - } - try { - return options.stream().filter((i) -> toString(i).equalsIgnoreCase(in)).collect(Collectors.toList()).get(0); - } catch (Throwable e) { - throw new DecreeParsingException("Unable to filter which World \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(World.class); - } - - @Override - public String getRandomDefault() { - return "world"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/DummyHandler.java b/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/DummyHandler.java deleted file mode 100644 index 9491f7588..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/DummyHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.specialhandlers; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; - -public class DummyHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - return null; - } - - public boolean isDummy() { - return true; - } - - @Override - public String toString(Object o) { - return null; - } - - @Override - public Object parse(String in, boolean force) throws DecreeParsingException { - return null; - } - - @Override - public boolean supports(Class type) { - return false; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/NullableDimensionHandler.java b/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/NullableDimensionHandler.java deleted file mode 100644 index c8278558b..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/NullableDimensionHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.specialhandlers; - -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.engine.object.IrisDimension; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; - -public class NullableDimensionHandler extends RegistrantHandler { - public NullableDimensionHandler() { - super(IrisDimension.class, true); - } - - @Override - public IrisDimension parse(String in, boolean force) throws DecreeParsingException { - if (in.equalsIgnoreCase("default")) { - return parse(IrisSettings.get().getGenerator().getDefaultWorldType()); - } - return super.parse(in, force); - } - - @Override - public String getRandomDefault() { - return "dimension"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/NullablePlayerHandler.java b/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/NullablePlayerHandler.java deleted file mode 100644 index 3c7567140..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/NullablePlayerHandler.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.volmit.iris.util.decree.specialhandlers; - -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.decree.handlers.PlayerHandler; -import org.bukkit.entity.Player; - -public class NullablePlayerHandler extends PlayerHandler { - - @Override - public Player parse(String in, boolean force) throws DecreeParsingException { - return getPossibilities(in).stream().filter((i) -> toString(i).equalsIgnoreCase(in)).findFirst().orElse(null); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/ObjectHandler.java b/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/ObjectHandler.java deleted file mode 100644 index 84d3dceb0..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/ObjectHandler.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.specialhandlers; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; - -import java.io.File; -import java.util.stream.Collectors; - -public class ObjectHandler implements DecreeParameterHandler { - @Override - public KList getPossibilities() { - KList p = new KList<>(); - IrisData data = data(); - if (data != null) { - return new KList<>(data.getObjectLoader().getPossibleKeys()); - } - - //noinspection ConstantConditions - for (File i : Iris.instance.getDataFolder("packs").listFiles()) { - if (i.isDirectory()) { - data = IrisData.get(i); - p.add(data.getObjectLoader().getPossibleKeys()); - } - } - - return p; - } - - @Override - public String toString(String irisObject) { - return irisObject; - } - - @Override - public String parse(String in, boolean force) throws DecreeParsingException { - KList options = getPossibilities(in); - - if (options.isEmpty()) { - throw new DecreeParsingException("Unable to find Object \"" + in + "\""); - } - try { - return options.stream().filter((i) -> toString(i).equalsIgnoreCase(in)).collect(Collectors.toList()).get(0); - } catch (Throwable e) { - throw new DecreeParsingException("Unable to filter which Object \"" + in + "\""); - } - } - - @Override - public boolean supports(Class type) { - return type.equals(String.class); - } - - @Override - public String getRandomDefault() { - String f = getPossibilities().getRandom(); - - return f == null ? "object" : f; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/RegistrantHandler.java b/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/RegistrantHandler.java deleted file mode 100644 index 090407bba..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/specialhandlers/RegistrantHandler.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.volmit.iris.util.decree.specialhandlers; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.decree.DecreeParameterHandler; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; - -import java.io.File; -import java.util.HashSet; -import java.util.Set; - -public abstract class RegistrantHandler implements DecreeParameterHandler { - private final Class type; - private final String name; - private final boolean nullable; - - public RegistrantHandler(Class type, boolean nullable) { - this.type = type; - this.name = type.getSimpleName().replaceFirst("Iris", ""); - this.nullable = nullable; - } - - @Override - public KList getPossibilities() { - KList p = new KList<>(); - Set known = new HashSet<>(); - IrisData data = data(); - if (data != null) { - for (T j : data.getLoader(type).loadAll(data.getLoader(type).getPossibleKeys())) { - known.add(j.getLoadKey()); - p.add(j); - } - } - - //noinspection ConstantConditions - for (File i : Iris.instance.getDataFolder("packs").listFiles()) { - if (i.isDirectory()) { - data = IrisData.get(i); - for (T j : data.getLoader(type).loadAll(data.getLoader(type).getPossibleKeys())) { - if (known.add(j.getLoadKey())) - p.add(j); - } - } - } - - return p; - } - - @Override - public String toString(T t) { - return t != null ? t.getLoadKey() : "null"; - } - - @Override - public T parse(String in, boolean force) throws DecreeParsingException { - if (in.equals("null") && nullable) { - return null; - } - KList options = getPossibilities(in); - if (options.isEmpty()) { - throw new DecreeParsingException("Unable to find " + name + " \"" + in + "\""); - } - - return options.stream() - .filter((i) -> toString(i).equalsIgnoreCase(in)) - .findFirst() - .orElseThrow(() -> new DecreeParsingException("Unable to filter which " + name + " \"" + in + "\"")); - } - - @Override - public boolean supports(Class type) { - return type.equals(this.type); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/decree/virtual/VirtualDecreeCommand.java b/core/src/main/java/com/volmit/iris/util/decree/virtual/VirtualDecreeCommand.java deleted file mode 100644 index 4bd5e9168..000000000 --- a/core/src/main/java/com/volmit/iris/util/decree/virtual/VirtualDecreeCommand.java +++ /dev/null @@ -1,595 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.decree.virtual; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.decree.*; -import com.volmit.iris.util.decree.annotations.Decree; -import com.volmit.iris.util.decree.exceptions.DecreeParsingException; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.plugin.CommandDummy; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import lombok.Data; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Objects; -import java.util.stream.Collectors; - -@Data -public class VirtualDecreeCommand { - private final Class type; - private final VirtualDecreeCommand parent; - private final KList nodes; - private final DecreeNode node; - String[] gradients = new String[]{ - "", - "", - "", - "", - "", - "" - }; - private ChronoLatch cl = new ChronoLatch(1000); - - private VirtualDecreeCommand(Class type, VirtualDecreeCommand parent, KList nodes, DecreeNode node) { - this.parent = parent; - this.type = type; - this.nodes = nodes; - this.node = node; - } - - public static VirtualDecreeCommand createRoot(Object v) throws Throwable { - return createRoot(null, v); - } - - public static VirtualDecreeCommand createRoot(VirtualDecreeCommand parent, Object v) throws Throwable { - VirtualDecreeCommand c = new VirtualDecreeCommand(v.getClass(), parent, new KList<>(), null); - - for (Field i : v.getClass().getDeclaredFields()) { - if (Modifier.isStatic(i.getModifiers()) || Modifier.isFinal(i.getModifiers()) || Modifier.isTransient(i.getModifiers()) || Modifier.isVolatile(i.getModifiers())) { - continue; - } - - if (!i.getType().isAnnotationPresent(Decree.class)) { - continue; - } - - i.setAccessible(true); - Object childRoot = i.get(v); - - if (childRoot == null) { - childRoot = i.getType().getConstructor().newInstance(); - i.set(v, childRoot); - } - - c.getNodes().add(createRoot(c, childRoot)); - } - - for (Method i : v.getClass().getDeclaredMethods()) { - if (Modifier.isStatic(i.getModifiers()) || Modifier.isFinal(i.getModifiers()) || Modifier.isPrivate(i.getModifiers())) { - continue; - } - - if (!i.isAnnotationPresent(Decree.class)) { - continue; - } - - c.getNodes().add(new VirtualDecreeCommand(v.getClass(), c, new KList<>(), new DecreeNode(v, i))); - } - - return c; - } - - public void cacheAll() { - VolmitSender sender = new VolmitSender(new CommandDummy()); - - if (isNode()) { - sender.sendDecreeHelpNode(this); - } - - for (VirtualDecreeCommand j : nodes) { - j.cacheAll(); - } - } - - public String getPath() { - KList n = new KList<>(); - VirtualDecreeCommand cursor = this; - - while (cursor.getParent() != null) { - cursor = cursor.getParent(); - n.add(cursor.getName()); - } - - return "/" + n.reverse().qadd(getName()).toString(" "); - } - - public String getParentPath() { - return getParent().getPath(); - } - - public String getName() { - return isNode() ? getNode().getName() : getType().getDeclaredAnnotation(Decree.class).name(); - } - - private boolean isStudio() { - return isNode() ? getNode().getDecree().studio() : getType().getDeclaredAnnotation(Decree.class).studio(); - } - - public String getDescription() { - return isNode() ? getNode().getDescription() : getType().getDeclaredAnnotation(Decree.class).description(); - } - - public KList getNames() { - if (isNode()) { - return getNode().getNames(); - } - - Decree dc = getType().getDeclaredAnnotation(Decree.class); - KList d = new KList<>(); - d.add(dc.name()); - for (String i : dc.aliases()) { - if (i.isEmpty()) { - continue; - } - - d.add(i); - } - - d.removeDuplicates(); - - return d; - } - - public boolean isNode() { - return node != null; - } - - public KList tabComplete(KList args, String raw) { - KList skip = new KList<>(); - KList tabs = new KList<>(); - invokeTabComplete(args, skip, tabs, raw); - return tabs; - } - - private boolean invokeTabComplete(KList args, KList skip, KList tabs, String raw) { - if (isStudio() && !IrisSettings.get().getStudio().isStudio()) { - return false; - } - - if (isNode()) { - tab(args, tabs); - skip.add(hashCode()); - return false; - } - - if (args.isEmpty()) { - tab(args, tabs); - return true; - } - - String head = args.get(0); - - if (args.size() > 1 || head.endsWith(" ")) { - VirtualDecreeCommand match = matchNode(head, skip); - - if (match != null) { - args.pop(); - return match.invokeTabComplete(args, skip, tabs, raw); - } - - skip.add(hashCode()); - } else { - tab(args, tabs); - } - - return false; - } - - private void tab(KList args, KList tabs) { - String last = null; - KList ignore = new KList<>(); - Runnable la = () -> { - - }; - for (String a : args) { - la.run(); - last = a; - la = () -> { - if (isNode()) { - String sea = a.contains("=") ? a.split("\\Q=\\E")[0] : a; - sea = sea.trim(); - - searching: - for (DecreeParameter i : getNode().getParameters()) { - for (String m : i.getNames()) { - if (m.equalsIgnoreCase(sea) || m.toLowerCase().contains(sea.toLowerCase()) || sea.toLowerCase().contains(m.toLowerCase())) { - ignore.add(i); - continue searching; - } - } - } - } - }; - } - - if (last != null) { - if (isNode()) { - for (DecreeParameter i : getNode().getParameters()) { - if (ignore.contains(i)) { - continue; - } - - int g = 0; - - if (last.contains("=")) { - String[] vv = last.trim().split("\\Q=\\E"); - String vx = vv.length == 2 ? vv[1] : ""; - for (String f : i.getHandler().getPossibilities(vx).convert((v) -> i.getHandler().toStringForce(v))) { - g++; - tabs.add(i.getName() + "=" + f); - } - } else { - for (String f : i.getHandler().getPossibilities("").convert((v) -> i.getHandler().toStringForce(v))) { - g++; - tabs.add(i.getName() + "=" + f); - } - } - - if (g == 0) { - tabs.add(i.getName() + "="); - } - } - } else { - for (VirtualDecreeCommand i : getNodes()) { - String m = i.getName(); - if (m.equalsIgnoreCase(last) || m.toLowerCase().contains(last.toLowerCase()) || last.toLowerCase().contains(m.toLowerCase())) { - tabs.addAll(i.getNames()); - } - } - } - } - } - - /** - * Maps the input a player typed to the parameters of this command - * - * @param sender The sender - * @param in The input - * @return A map of all the parameter names and their values - */ - private KMap map(VolmitSender sender, KList in) { - KMap data = new KMap<>(); - KSet nowhich = new KSet<>(); - - KList unknownInputs = new KList<>(in.stream().filter(s -> !s.contains("=")).collect(Collectors.toList())); - KList knownInputs = new KList<>(in.stream().filter(s -> s.contains("=")).collect(Collectors.toList())); - - //Loop known inputs - for (int x = 0; x < knownInputs.size(); x++) { - String stringParam = knownInputs.get(x); - int original = in.indexOf(stringParam); - - String[] v = stringParam.split("\\Q=\\E"); - String key = v[0]; - String value = v[1]; - DecreeParameter param = null; - - //Find decree parameter from string param - for (DecreeParameter j : getNode().getParameters()) { - for (String k : j.getNames()) { - if (k.equalsIgnoreCase(key)) { - param = j; - break; - } - } - } - - //If it failed, see if we can find it by checking if the names contain the param - if (param == null) { - for (DecreeParameter j : getNode().getParameters()) { - for (String k : j.getNames()) { - if (k.toLowerCase().contains(key.toLowerCase()) || key.toLowerCase().contains(k.toLowerCase())) { - param = j; - break; - } - } - } - } - - //Still failed to find, error them - if (param == null) { - Iris.debug("Can't find parameter key for " + key + "=" + value + " in " + getPath()); - sender.sendMessage(C.YELLOW + "Unknown Parameter: " + key); - unknownInputs.add(value); //Add the value to the unknowns and see if we can assume it later - continue; - } - - key = param.getName(); - - try { - data.put(key, param.getHandler().parse(value, nowhich.contains(original))); //Parse and put - } catch (DecreeParsingException e) { - Iris.debug("Can't parse parameter value for " + key + "=" + value + " in " + getPath() + " using handler " + param.getHandler().getClass().getSimpleName()); - sender.sendMessage(C.RED + "Cannot convert \"" + value + "\" into a " + param.getType().getSimpleName()); - e.printStackTrace(); - return null; - } - } - - //Make a list of decree params that haven't been identified - KList decreeParameters = new KList<>(getNode().getParameters().stream().filter(param -> !data.contains(param.getName())).collect(Collectors.toList())); - - //Loop Unknown inputs - for (int x = 0; x < unknownInputs.size(); x++) { - String stringParam = unknownInputs.get(x); - int original = in.indexOf(stringParam); - try { - DecreeParameter par = decreeParameters.get(x); - - try { - data.put(par.getName(), par.getHandler().parse(stringParam, nowhich.contains(original))); - } catch (DecreeParsingException e) { - Iris.debug("Can't parse parameter value for " + par.getName() + "=" + stringParam + " in " + getPath() + " using handler " + par.getHandler().getClass().getSimpleName()); - sender.sendMessage(C.RED + "Cannot convert \"" + stringParam + "\" into a " + par.getType().getSimpleName()); - e.printStackTrace(); - return null; - } - } catch (IndexOutOfBoundsException e) { - sender.sendMessage(C.YELLOW + "Unknown Parameter: " + stringParam + " (" + Form.getNumberSuffixThStRd(x + 1) + " argument)"); - } - } - - return data; - } - - public boolean invoke(VolmitSender sender, KList realArgs) { - return invoke(sender, realArgs, new KList<>()); - } - - public boolean invoke(VolmitSender sender, KList args, KList skip) { - if (isStudio() && !IrisSettings.get().getStudio().isStudio()) { - sender.sendMessage(C.RED + "To use Iris Studio Commands, please enable studio in Iris/settings.json (settings auto-reload)"); - return false; - } - - DecreeOrigin origin = type.getDeclaredAnnotation(Decree.class).origin(); - if (!origin.validFor(sender)) { - sender.sendMessage(C.RED + "This command has to be sent from another origin: " + C.GOLD + origin); - return false; - } - - Iris.debug("@ " + getPath() + " with " + args.toString(", ")); - if (isNode()) { - Iris.debug("Invoke " + getPath() + "(" + args.toString(",") + ") at "); - if (invokeNode(sender, map(sender, args))) { - return true; - } - - skip.add(hashCode()); - return false; - } - - if (args.isEmpty()) { - sender.sendDecreeHelp(this); - - return true; - } else if (args.size() == 1) { - for (String i : args) { - if (i.startsWith("help=")) { - sender.sendDecreeHelp(this, Integer.parseInt(i.split("\\Q=\\E")[1]) - 1); - return true; - } - } - } - - String head = args.get(0); - VirtualDecreeCommand match = matchNode(head, skip); - - if (match != null) { - args.pop(); - return match.invoke(sender, args, skip); - } - - skip.add(hashCode()); - - return false; - } - - private boolean invokeNode(VolmitSender sender, KMap map) { - if (map == null) { - return false; - } - - Object[] params = new Object[getNode().getMethod().getParameterCount()]; - int vm = 0; - for (DecreeParameter i : getNode().getParameters()) { - Object value = map.get(i.getName()); - - try { - if (value == null && i.hasDefault()) { - value = i.getDefaultValue(); - } - } catch (DecreeParsingException e) { - Iris.debug("Can't parse parameter value for " + i.getName() + "=" + i.getParam().defaultValue() + " in " + getPath() + " using handler " + i.getHandler().getClass().getSimpleName()); - sender.sendMessage(C.RED + "Cannot convert \"" + i.getParam().defaultValue() + "\" into a " + i.getType().getSimpleName()); - return false; - } - - if (sender.isPlayer() && i.isContextual() && value == null) { - Iris.debug("Contextual!"); - DecreeContextHandler ch = DecreeContextHandler.contextHandlers.get(i.getType()); - - if (ch != null) { - value = ch.handle(sender); - - if (value != null) { - Iris.debug("Parameter \"" + i.getName() + "\" derived a value of \"" + i.getHandler().toStringForce(value) + "\" from " + ch.getClass().getSimpleName()); - } else { - Iris.debug("Parameter \"" + i.getName() + "\" could not derive a value from \"" + ch.getClass().getSimpleName()); - } - } else { - Iris.debug("Parameter \"" + i.getName() + "\" is contextual but has no context handler for \"" + i.getType().getCanonicalName() + "\""); - } - } - - if (i.hasDefault() && value == null) { - try { - Iris.debug("Parameter \"" + i.getName() + "\" is using default value \"" + i.getParam().defaultValue() + "\""); - value = i.getDefaultValue(); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - if (i.isRequired() && value == null) { - sender.sendMessage(C.RED + "Missing argument \"" + i.getName() + "\" (" + i.getType().getSimpleName() + ") as the " + Form.getNumberSuffixThStRd(vm + 1) + " argument."); - sender.sendDecreeHelpNode(this); - return false; - } - - params[vm] = value; - vm++; - } - - DecreeContext.touch(sender); - try { - Runnable rx = () -> { - DecreeContext.touch(sender); - try { - getNode().getMethod().setAccessible(true); - getNode().getMethod().invoke(getNode().getInstance(), params); - } catch (Throwable e) { - e.printStackTrace(); - throw new RuntimeException("Failed to execute "); // TODO: - } finally { - DecreeContext.remove(); - } - }; - - if (getNode().isSync()) { - J.s(rx); - } else { - rx.run(); - } - } finally { - DecreeContext.remove(); - } - - return true; - } - - public KList matchAllNodes(String in) { - KList g = new KList<>(); - - if (in.trim().isEmpty()) { - g.addAll(nodes); - return g; - } - - for (VirtualDecreeCommand i : nodes) { - if (i.matches(in)) { - g.add(i); - } - } - - for (VirtualDecreeCommand i : nodes) { - if (i.deepMatches(in)) { - g.add(i); - } - } - - g.removeDuplicates(); - return g; - } - - public VirtualDecreeCommand matchNode(String in, KList skip) { - if (in.trim().isEmpty()) { - return null; - } - - for (VirtualDecreeCommand i : nodes) { - if (skip.contains(i.hashCode())) { - continue; - } - - if (i.matches(in)) { - return i; - } - } - - for (VirtualDecreeCommand i : nodes) { - if (skip.contains(i.hashCode())) { - continue; - } - - if (i.deepMatches(in)) { - return i; - } - } - - return null; - } - - public boolean deepMatches(String in) { - KList a = getNames(); - - for (String i : a) { - if (i.toLowerCase().contains(in.toLowerCase()) || in.toLowerCase().contains(i.toLowerCase())) { - return true; - } - } - - return false; - } - - @Override - public int hashCode() { - return Objects.hash(getName(), getDescription(), getType(), getPath()); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof VirtualDecreeCommand)) { - return false; - } - return this.hashCode() == obj.hashCode(); - } - - public boolean matches(String in) { - KList a = getNames(); - - for (String i : a) { - if (i.equalsIgnoreCase(in)) { - return true; - } - } - - return false; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/documentation/BlockCoordinates.java b/core/src/main/java/com/volmit/iris/util/documentation/BlockCoordinates.java deleted file mode 100644 index e0e6b66e6..000000000 --- a/core/src/main/java/com/volmit/iris/util/documentation/BlockCoordinates.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.documentation; - -/** - * This method expects coords in block - */ -public @interface BlockCoordinates { - -} diff --git a/core/src/main/java/com/volmit/iris/util/documentation/ChunkCoordinates.java b/core/src/main/java/com/volmit/iris/util/documentation/ChunkCoordinates.java deleted file mode 100644 index 64a1a38b4..000000000 --- a/core/src/main/java/com/volmit/iris/util/documentation/ChunkCoordinates.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.documentation; - -/** - * This method expects blocks in chunk coordinates - */ -public @interface ChunkCoordinates { - -} diff --git a/core/src/main/java/com/volmit/iris/util/documentation/ChunkRelativeBlockCoordinates.java b/core/src/main/java/com/volmit/iris/util/documentation/ChunkRelativeBlockCoordinates.java deleted file mode 100644 index e5d86a9f9..000000000 --- a/core/src/main/java/com/volmit/iris/util/documentation/ChunkRelativeBlockCoordinates.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.documentation; - -/** - * This method expects coords in block - */ -public @interface ChunkRelativeBlockCoordinates { - -} diff --git a/core/src/main/java/com/volmit/iris/util/documentation/Exclusive.java b/core/src/main/java/com/volmit/iris/util/documentation/Exclusive.java deleted file mode 100644 index c7202aab4..000000000 --- a/core/src/main/java/com/volmit/iris/util/documentation/Exclusive.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.volmit.iris.util.documentation; - -/** - * This Argument is exclusive - */ -public @interface Exclusive { -} diff --git a/core/src/main/java/com/volmit/iris/util/documentation/Inclusive.java b/core/src/main/java/com/volmit/iris/util/documentation/Inclusive.java deleted file mode 100644 index 5e4f27599..000000000 --- a/core/src/main/java/com/volmit/iris/util/documentation/Inclusive.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.volmit.iris.util.documentation; - -/** - * This Argument is inclusive - */ -public @interface Inclusive { -} diff --git a/core/src/main/java/com/volmit/iris/util/documentation/RegionCoordinates.java b/core/src/main/java/com/volmit/iris/util/documentation/RegionCoordinates.java deleted file mode 100644 index 9d07a39e1..000000000 --- a/core/src/main/java/com/volmit/iris/util/documentation/RegionCoordinates.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.documentation; - -/** - * This method expects coordinates in region coords - */ -public @interface RegionCoordinates { - -} diff --git a/core/src/main/java/com/volmit/iris/util/exceptions/IrisException.java b/core/src/main/java/com/volmit/iris/util/exceptions/IrisException.java deleted file mode 100644 index 187bcc3d0..000000000 --- a/core/src/main/java/com/volmit/iris/util/exceptions/IrisException.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.exceptions; - -public class IrisException extends Exception { - public IrisException() { - super(); - } - - public IrisException(String message) { - super(message); - } - - public IrisException(Throwable message) { - super(message); - } - - public IrisException(String message, Throwable e) { - super(message, e); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/exceptions/MissingDimensionException.java b/core/src/main/java/com/volmit/iris/util/exceptions/MissingDimensionException.java deleted file mode 100644 index 368d2d09d..000000000 --- a/core/src/main/java/com/volmit/iris/util/exceptions/MissingDimensionException.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.exceptions; - -public class MissingDimensionException extends IrisException { - public MissingDimensionException() { - super(); - } - - public MissingDimensionException(String message) { - super(message); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/format/Form.java b/core/src/main/java/com/volmit/iris/util/format/Form.java deleted file mode 100644 index bb2e7ec27..000000000 --- a/core/src/main/java/com/volmit/iris/util/format/Form.java +++ /dev/null @@ -1,1233 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.format; - -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RollingSequence; - -import java.math.BigInteger; -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.*; -import java.util.Map.Entry; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Form { - private static final String[] NAMES = new String[]{"Thousand", "Million", "Billion", "Trillion", "Quadrillion", "Quintillion", "Sextillion", "Septillion", "Octillion", "Nonillion", "Decillion", "Undecillion", "Duodecillion", "Tredecillion", "Quattuordecillion", "Quindecillion", "Sexdecillion", "Septendecillion", "Octodecillion", "Novemdecillion", "Vigintillion",}; - private static final BigInteger THOUSAND = BigInteger.valueOf(1000); - private static final NavigableMap MAP; - private static NumberFormat NF; - private static DecimalFormat DF; - - static { - MAP = new TreeMap<>(); - for (int i = 0; i < NAMES.length; i++) { - MAP.put(THOUSAND.pow(i + 1), NAMES[i]); - } - } - - public static String getNumberSuffixThStRd(int day) { - if (day >= 11 && day <= 13) { - return Form.f(day) + "th"; - } - return switch (day % 10) { - case 1 -> Form.f(day) + "st"; - case 2 -> Form.f(day) + "nd"; - case 3 -> Form.f(day) + "rd"; - default -> Form.f(day) + "th"; - }; - } - - private static void instantiate() { - if (NF == null) { - NF = NumberFormat.getInstance(Locale.US); - } - } - - /** - * Scroll text - * - * @param smx the text - * @param viewport the viewport length - * @param time the timeline value - */ - public static String scroll(String smx, int viewport, long time) { - String src = Form.repeat(" ", viewport) + smx + Form.repeat(" ", viewport); - int len = src.length(); - int walk = (int) (time % (len - viewport)); - String base = src.substring(walk, M.min(walk + viewport, len - 1)); - base = base.length() < viewport ? base + Form.repeat(" ", (viewport - base.length()) - 3) : base; - - return base; - } - - /** - * Capitalize the first letter - * - * @param s the string - * @return the capitalized string - */ - public static String capitalize(String s) { - StringBuilder roll = new StringBuilder(); - boolean f = true; - - for (Character i : s.trim().toCharArray()) { - if (f) { - roll.append(Character.toUpperCase(i)); - f = false; - } else { - roll.append(i); - } - } - - return roll.toString(); - } - - /** - * Capitalize all words in the string - * - * @param s the string - * @return the capitalized string - */ - public static String capitalizeWords(String s) { - StringBuilder rollx = new StringBuilder(); - - for (String i : s.trim().split(" ")) { - rollx.append(" ").append(capitalize(i.trim())); - } - - return rollx.substring(1); - } - - /** - * Hard word wrap - * - * @param s the words - * @param len the length per line - * @return the wrapped string - */ - public static String wrap(String s, int len) { - return wrap(s, len, null, false); - } - - /** - * Soft Word wrap - * - * @param s the string - * @param len the length to wrap - * @return the wrapped string - */ - public static String wrapWords(String s, int len) { - return wrap(s, len, null, true); - } - - /** - * Wrap words - * - * @param s the string - * @param len the wrap length - * @param newLineSep the new line seperator - * @param soft should it be soft wrapped or hard wrapped? - * @return the wrapped words - */ - public static String wrap(String s, int len, String newLineSep, boolean soft) { - return wrap(s, len, newLineSep, soft, " "); - } - - public static String hardWrap(String s, int len) { - StringBuilder ss = new StringBuilder(); - - for (int i = 0; i < s.length(); i += len) { - if (i + len > s.length()) { - ss.append(s, i, s.length()); - break; - } - - ss.append(s, i, i + len).append("\n"); - } - - return ss.toString(); - } - - public static List hardWrapList(String s, int len) { - List l = new ArrayList<>(); - for (int i = 0; i < s.length(); i += len) { - if (i + len > s.length()) { - l.add(s.substring(i)); - break; - } - - l.add(s.substring(i, i + len)); - } - - return l; - } - - - /** - * Wrap words - * - * @param s the string - * @param len the length - * @param newLineSep the new line seperator - * @param soft soft or hard wrapping - * @param regex the regex - * @return the wrapped string - */ - public static String wrap(String s, int len, String newLineSep, boolean soft, String regex) { - if (s == null) { - return null; - } else { - if (newLineSep == null) { - newLineSep = "\n"; - } - - if (len < 1) { - len = 1; - } - - if (regex.trim().equals("")) { - regex = " "; - } - - Pattern arg4 = Pattern.compile(regex); - int arg5 = s.length(); - int arg6 = 0; - StringBuilder arg7 = new StringBuilder(arg5 + 32); - - while (arg6 < arg5) { - int arg8 = -1; - Matcher arg9 = arg4.matcher(s.substring(arg6, Math.min(arg6 + len + 1, arg5))); - if (arg9.find()) { - if (arg9.start() == 0) { - arg6 += arg9.end(); - continue; - } - - arg8 = arg9.start(); - } - - if (arg5 - arg6 <= len) { - break; - } - - while (arg9.find()) { - arg8 = arg9.start() + arg6; - } - - if (arg8 >= arg6) { - arg7.append(s, arg6, arg8); - arg7.append(newLineSep); - arg6 = arg8 + 1; - } else if (soft) { - arg7.append(s, arg6, len + arg6); - arg7.append(newLineSep); - arg6 += len; - } else { - arg9 = arg4.matcher(s.substring(arg6 + len)); - if (arg9.find()) { - arg8 = arg9.start() + arg6 + len; - } - - if (arg8 >= 0) { - arg7.append(s, arg6, arg8); - arg7.append(newLineSep); - arg6 = arg8 + 1; - } else { - arg7.append(s.substring(arg6)); - arg6 = arg5; - } - } - } - - arg7.append(s.substring(arg6)); - return arg7.toString(); - } - } - - /** - * Returns a fancy duration up to Years - * - * @param duration the duration in ms - * @return the fancy duration - */ - public static String duration(RollingSequence rollingSequence, long duration) { - String suffix = "Millisecond"; - double phantom = duration; - int div = 1000; - - if (phantom > div) { - phantom /= div; - suffix = "Second"; - div = 60; - - if (phantom > div) { - phantom /= div; - suffix = "Minute"; - - if (phantom > div) { - phantom /= div; - suffix = "Hour"; - div = 24; - - if (phantom > 24) { - phantom /= div; - suffix = "Day"; - div = 7; - - if (phantom > div) { - phantom /= div; - suffix = "Week"; - div = 4; - - if (phantom > div) { - phantom /= div; - suffix = "Month"; - div = 12; - - //noinspection IfStatementWithIdenticalBranches - if (phantom > div) { - phantom /= div; - suffix = "Year"; - return Form.fd(phantom, 0) + " " + suffix + ((int) phantom == 1 ? "" : "s"); - } else { - return Form.fd(phantom, 0) + " " + suffix + ((int) phantom == 1 ? "" : "s"); - } - } else { - return Form.fd(phantom, 0) + " " + suffix + ((int) phantom == 1 ? "" : "s"); - } - } else { - return Form.fd(phantom, 0) + " " + suffix + ((int) phantom == 1 ? "" : "s"); - } - } else { - return Form.fd(phantom, 0) + " " + suffix + ((int) phantom == 1 ? "" : "s"); - } - } else { - return Form.fd(phantom, 0) + " " + suffix + ((int) phantom == 1 ? "" : "s"); - } - } else { - return Form.fd(phantom, 0) + " " + suffix + ((int) phantom == 1 ? "" : "s"); - } - } else { - return "Under a Second"; - } - } - - /** - * Fixes the minute issue with formatting - * - * @param c the calendar - * @return the minute string - */ - public static String fmin(Calendar c) { - String s = c.get(Calendar.MINUTE) + ""; - if (s.length() == 1) { - return "0" + s; - } - - return s; - } - - /** - * Get a fancy time stamp - * - * @param time the stamp in time (ago) - * @return the fancy stamp in time (ago) - */ - public static String ago(long time) { - long current = M.ms(); - - if (time > current - TimeUnit.SECONDS.toMillis(30) && time < current) { - return "Just Now"; - } else if (time > current - TimeUnit.SECONDS.toMillis(60) && time < current) { - return "Seconds Ago"; - } else if (time > current - TimeUnit.MINUTES.toMillis(10) && time < current) { - return "Minutes Ago"; - } else { - Calendar now = Calendar.getInstance(); - Calendar c = Calendar.getInstance(); - c.setTimeInMillis(time); - boolean sameYear = now.get(Calendar.YEAR) == c.get(Calendar.YEAR); - boolean sameDay = now.get(Calendar.DAY_OF_YEAR) == c.get(Calendar.DAY_OF_YEAR); - - if (sameDay) { - int h = c.get(Calendar.HOUR); - h = h == 0 ? 12 : h; - - return "Today at " + h + ":" + fmin(c) + " " + (c.get(Calendar.AM_PM) == Calendar.PM ? "PM" : "AM"); - } else if (sameYear) { - boolean yesterday = now.get(Calendar.DAY_OF_YEAR) - 1 == c.get(Calendar.DAY_OF_YEAR); - - if (yesterday) { - int h = c.get(Calendar.HOUR); - h = h == 0 ? 12 : h; - - return "Yesterday at " + h + ":" + fmin(c) + " " + (c.get(Calendar.AM_PM) == Calendar.PM ? "PM" : "AM"); - } else { - int h = c.get(Calendar.HOUR); - h = h == 0 ? 12 : h; - String dow = switch (c.get(Calendar.DAY_OF_WEEK)) { - case Calendar.SUNDAY -> "Sunday"; - case Calendar.MONDAY -> "Monday"; - case Calendar.TUESDAY -> "Tuesday"; - case Calendar.WEDNESDAY -> "Wednesday"; - case Calendar.THURSDAY -> "Thursday"; - case Calendar.FRIDAY -> "Friday"; - case Calendar.SATURDAY -> "Saturday"; - default -> "Error Day"; - }; - - String monthName = "Error Month"; - int month = c.get(Calendar.MONTH); - - switch (month) { - case Calendar.JANUARY -> monthName = "Jan"; - case Calendar.FEBRUARY -> monthName = "Feb"; - case Calendar.MARCH -> monthName = "Mar"; - case Calendar.APRIL -> monthName = "Apr"; - case Calendar.MAY -> monthName = "May"; - case Calendar.JUNE -> monthName = "Jun"; - case Calendar.JULY -> monthName = "Jul"; - case Calendar.AUGUST -> monthName = "Aug"; - case Calendar.SEPTEMBER -> monthName = "Sep"; - case Calendar.OCTOBER -> monthName = "Oct"; - case Calendar.NOVEMBER -> monthName = "Nov"; - case Calendar.DECEMBER -> monthName = "Dec"; - } - - int dayOfMonth = c.get(Calendar.DAY_OF_MONTH); - String suffix = numberSuffix(dayOfMonth); - - return dow + ", " + monthName + " " + suffix + " at " + h + ":" + fmin(c) + " " + (c.get(Calendar.AM_PM) == Calendar.PM ? "PM" : "AM"); - } - } else { - int h = c.get(Calendar.HOUR); - h = h == 0 ? 12 : h; - String dow = switch (c.get(Calendar.DAY_OF_WEEK)) { - case Calendar.SUNDAY -> "Sunday"; - case Calendar.MONDAY -> "Monday"; - case Calendar.TUESDAY -> "Tuesday"; - case Calendar.WEDNESDAY -> "Wednesday"; - case Calendar.THURSDAY -> "Thursday"; - case Calendar.FRIDAY -> "Friday"; - case Calendar.SATURDAY -> "Saturday"; - default -> "Error Day"; - }; - - String monthName = "Error Month"; - int month = c.get(Calendar.MONTH); - - switch (month) { - case Calendar.JANUARY -> monthName = "Jan"; - case Calendar.FEBRUARY -> monthName = "Feb"; - case Calendar.MARCH -> monthName = "Mar"; - case Calendar.APRIL -> monthName = "Apr"; - case Calendar.MAY -> monthName = "May"; - case Calendar.JUNE -> monthName = "Jun"; - case Calendar.JULY -> monthName = "Jul"; - case Calendar.AUGUST -> monthName = "Aug"; - case Calendar.SEPTEMBER -> monthName = "Sep"; - case Calendar.OCTOBER -> monthName = "Oct"; - case Calendar.NOVEMBER -> monthName = "Nov"; - case Calendar.DECEMBER -> monthName = "Dec"; - } - - int dayOfMonth = c.get(Calendar.DAY_OF_MONTH); - String suffix = numberSuffix(dayOfMonth); - int year = c.get(Calendar.YEAR); - - return year + ", " + dow + ", " + monthName + " " + suffix + " at " + h + ":" + fmin(c) + " " + (c.get(Calendar.AM_PM) == Calendar.PM ? "PM" : "AM"); - } - } - } - - /** - * Get the suffix for a number i.e. 1st 2nd 3rd - * - * @param i the number - * @return the suffix - */ - public static String numberSuffix(int i) { - String[] sufixes = new String[]{"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"}; - return switch (i % 100) { - case 11, 12, 13 -> i + "th"; - default -> i + sufixes[i % 10]; - }; - } - - /** - * Get a high accuracy but limited range duration (accurate up to a couple - * minutes) - * - * @param ms the milliseconds (double) - * @param prec the precision (decimal format) - * @return the formatted string - */ - public static String duration(double ms, int prec) { - if (ms < 1000.0) { - return Form.f(ms, prec) + "ms"; - } - - if (ms / 1000.0 < 60.0) { - return Form.f(ms / 1000.0, prec) + "s"; - } - - if (ms / 1000.0 / 60.0 < 60.0) { - return Form.f(ms / 1000.0 / 60.0, prec) + "m"; - } - - if (ms / 1000.0 / 60.0 / 60.0 < 24.0) { - return Form.f(ms / 1000.0 / 60.0 / 60.0, prec) + " hours"; - } - - if (ms / 1000.0 / 60.0 / 60.0 / 24.0 < 7) { - return Form.f(ms / 1000.0 / 60.0 / 24.0, prec) + " days"; - } - - return Form.f(ms, prec) + "ms"; - } - - public static String duration(long ms) { - return duration(ms, 0); - } - - /** - * Get a duration from milliseconds up to days - * - * @param ms the ms - * @param prec the precision (decimal format) - * @return the formatted string - */ - public static String duration(long ms, int prec) { - if (ms < 1000.0) { - return Form.f(ms, prec) + "ms"; - } - - if (ms / 1000.0 < 60.0) { - return Form.f(ms / 1000.0, prec) + " seconds"; - } - - if (ms / 1000.0 / 60.0 < 60.0) { - return Form.f(ms / 1000.0 / 60.0, prec) + " minutes"; - } - - if (ms / 1000.0 / 60.0 / 60.0 < 24.0) { - return Form.f(ms / 1000.0 / 60.0 / 60.0, prec) + " hours"; - } - - if (ms / 1000.0 / 60.0 / 60.0 / 24.0 < 7) { - return Form.f(ms / 1000.0 / 60.0 / 60.0 / 24.0, prec) + " days"; - } - - return Form.f(ms, prec) + "ms"; - } - - /** - * Format a big value - * - * @param i the number - * @return the full value in string - */ - public static String b(int i) { - return b(new BigInteger(String.valueOf(i))); - } - - /** - * Format a big value - * - * @param i the number - * @return the full value in string - */ - public static String b(long i) { - return b(new BigInteger(String.valueOf(i))); - } - - /** - * Format a big value - * - * @param i the number - * @return the full value in string - */ - public static String b(double i) { - return b(new BigInteger(String.valueOf((long) i))); - } - - /** - * Format a big number - * - * @param number the big number - * @return the value in string - */ - public static String b(BigInteger number) { - Entry entry = MAP.floorEntry(number); - if (entry == null) { - return "Nearly nothing"; - } - - BigInteger key = entry.getKey(); - BigInteger d = key.divide(THOUSAND); - BigInteger m = number.divide(d); - float f = m.floatValue() / 1000.0f; - float rounded = ((int) (f * 100.0)) / 100.0f; - - if (rounded % 1 == 0) { - return ((int) rounded) + " " + entry.getValue(); - } - - return rounded + " " + entry.getValue(); - } - - /** - * Calculate a fancy string representation of a file size. Adds a suffix of B, - * KB, MB, GB, or TB - * - * @param s the size (in bytes) - * @return the string - */ - public static String fileSize(long s) { - return ofSize(s, 1000); - } - - /** - * ":", "a", "b", "c" -> a:b:c - * - * @param splitter the splitter that goes in between - * @param strings the strings - * @return the result - */ - public static String split(String splitter, String... strings) { - StringBuilder b = new StringBuilder(); - - for (String i : strings) { - b.append(splitter); - b.append(i); - } - - return b.substring(splitter.length()); - } - - /** - * Calculate a fancy string representation of a file size. Adds a suffix of B, - * KB, MB, GB, or TB - * - * @param s the size (in bytes) - * @return the string - */ - public static String memSize(long s) { - return ofSize(s, 1024); - } - - public static String memSize(long s, int dec) { - return ofSize(s, 1024, dec); - } - - /** - * Get the timestamp of the time t (ms since 1970) - * - * @param t the time - * @return the stamp - */ - @SuppressWarnings("deprecation") - public static String stamp(long t) { - Date d = new Date(t); - return d.getMonth() + "-" + d.getDate() + "-" + (d.getYear() + 1900) + " " + d.getHours() + "h " + d.getMinutes() + "m " + d.getSeconds() + "s "; - } - - @SuppressWarnings("deprecation") - public static String stampTime(long t) { - Date d = new Date(t); - - return Calendar.getInstance().get(Calendar.HOUR_OF_DAY) + ":" + forceDoubleDigit(d.getMinutes()) + ":" + forceDoubleDigit(d.getSeconds()); - } - - public static String forceDoubleDigit(int dig) { - if (dig < 10) { - return "0" + dig; - } - - return dig + ""; - } - - @SuppressWarnings("deprecation") - public static String stampDay(long t) { - Date d = new Date(t); - return d.getMonth() + "-" + d.getDate() + "-" + (d.getYear() + 1900); - } - - /** - * Calculate a fancy string representation of a size in B, KB, MB, GB, or TB - * with a special divisor. The divisor decides how much goes up in the suffix - * chain. - * - * @param s the size (in bytes) - * @param div the divisor - * @return the string - */ - public static String ofSize(long s, int div) { - double d = (double) s; - String sub = " Bytes"; - - if (d > div - 1) { - d /= div; - sub = " KB"; - - if (d > div - 1) { - d /= div; - sub = " MB"; - - if (d > div - 1) { - d /= div; - sub = " GB"; - - if (d > div - 1) { - d /= div; - sub = " TB"; - } - } - } - } - - if (sub.equals("GB") || sub.equals("TB")) { - return Form.f(d, 1) + sub; - } else { - return Form.f(d, 0) + sub; - } - } - - /** - * Calculate a fancy string representation of a size in B, KB, MB, GB, or TB - * with a special divisor. The divisor decides how much goes up in the suffix - * chain. - * - * @param s the size (in bytes) - * @param div the divisor - * @param dec the decimal places - * @return the string - */ - public static String ofSize(long s, int div, int dec) { - double d = (double) s; - String sub = "Bytes"; - - if (d > div - 1) { - d /= div; - sub = "KB"; - - if (d > div - 1) { - d /= div; - sub = "MB"; - - if (d > div - 1) { - d /= div; - sub = "GB"; - - if (d > div - 1) { - d /= div; - sub = "TB"; - } - } - } - } - - return Form.f(d, dec) + " " + sub; - } - - /** - * Calculate a fancy string representation of a size in Grams, KG, MG, GG, TG - * with a special divisor. The divisor decides how much goes up in the suffix - * chain. - * - * @param s the size (in bytes) - * @param div the divisor - * @param dec the decimal places - * @return the string - */ - public static String ofSizeMetricWeight(long s, int div, int dec) { - boolean neg = s < 0; - if (neg) { - s = -s; - } - double d = (double) s; - String sub = "Grams"; - - if (d > div - 1) { - d /= div; - sub = "KG"; - - if (d > div - 1) { - d /= div; - sub = "MG"; - - if (d > div - 1) { - d /= div; - sub = "GG"; - - if (d > div - 1) { - d /= div; - sub = "TG"; - } - } - } - } - - return (neg ? "-" : "") + Form.f(d, dec) + " " + sub; - } - - /** - * Trim a string to a length, then append ... at the end if it extends the limit - * - * @param s the string - * @param l the limit - * @return the modified string - */ - public static String trim(String s, int l) { - if (s.length() <= l) { - return s; - } - - return s.substring(0, l) + "..."; - } - - /** - * Get a class name into a configuration/filename key For example, - * PhantomController.class is converted to phantom-controller - * - * @param clazz the class - * @return the string representation - */ - public static String cname(String clazz) { - StringBuilder codeName = new StringBuilder(); - - for (Character i : clazz.toCharArray()) { - if (Character.isUpperCase(i)) { - codeName.append("-").append(Character.toLowerCase(i)); - } else { - codeName.append(i); - } - } - - if (codeName.toString().startsWith("-")) { - codeName = new StringBuilder(codeName.substring(1)); - } - - return codeName.toString(); - } - - /** - * Get a formatted representation of the memory given in megabytes - * - * @param mb the megabytes - * @return the string representation with suffixes - */ - public static String mem(long mb) { - if (mb < 1024) { - return f(mb) + " MB"; - } else { - return f(((double) mb / (double) 1024), 1) + " GB"; - } - } - - /** - * Get a formatted representation of the memory given in kilobytes - * - * @return the string representation with suffixes - */ - public static String memx(long kb) { - if (kb < 1024) { - return fd(kb, 2) + " KB"; - } else { - double mb = (double) kb / 1024.0; - - if (mb < 1024) { - return fd(mb, 2) + " MB"; - } else { - double gb = mb / 1024.0; - - return fd(gb, 2) + " GB"; - } - } - } - - /** - * Format a long. Changes -10334 into -10,334 - * - * @param i the number - * @return the string representation of the number - */ - public static String f(long i) { - instantiate(); - return NF.format(i); - } - - /** - * Format a number. Changes -10334 into -10,334 - * - * @param i the number - * @return the string representation of the number - */ - public static String f(int i) { - instantiate(); - return NF.format(i); - } - - /** - * Formats a double's decimals to a limit - * - * @param i the double - * @param p the number of decimal places to use - * @return the formated string - */ - public static String f(double i, int p) { - String form = "#"; - - if (p > 0) { - form = form + "." + repeat("#", p); - } - - DF = new DecimalFormat(form); - - return DF.format(i).replaceAll("\\Q,\\E", "."); - } - - /** - * Formats a double's decimals to a limit, however, this will add zeros to the - * decimal places that dont need to be placed down. 2.4343 formatted with 6 - * decimals gets returned as 2.434300 - * - * @param i the double - * @param p the number of decimal places to use - * @return the formated string - */ - public static String fd(double i, int p) { - String form = "0"; - - if (p > 0) { - form = form + "." + repeat("0", p); - } - - DF = new DecimalFormat(form); - - return DF.format(i); - } - - /** - * Formats a float's decimals to a limit - * - * @param i the float - * @param p the number of decimal places to use - * @return the formated string - */ - public static String f(float i, int p) { - String form = "#"; - - if (p > 0) { - form = form + "." + repeat("#", p); - } - - DF = new DecimalFormat(form); - - return DF.format(i); - } - - /** - * Formats a double's decimals (one decimal point) - * - * @param i the double - */ - public static String f(double i) { - return f(i, 1); - } - - /** - * Formats a float's decimals (one decimal point) - * - * @param i the float - */ - public static String f(float i) { - return f(i, 1); - } - - /** - * Get a percent representation of a double and decimal places (0.53) would - * return 53% - * - * @param i the double - * @param p the number of decimal points - * @return a string - */ - public static String pc(double i, int p) { - return f(i * 100.0, p) + "%"; - } - - /** - * Get a percent representation of a float and decimal places (0.53) would - * return 53% - * - * @param i the float - * @param p the number of decimal points - * @return a string - */ - public static String pc(float i, int p) { - return f(i * 100, p) + "%"; - } - - /** - * Get a percent representation of a double and zero decimal places (0.53) would - * return 53% - * - * @param i the double - * @return a string - */ - public static String pc(double i) { - return f(i * 100, 0) + "%"; - } - - /** - * Get a percent representation of a float and zero decimal places (0.53) would - * return 53% - * - * @param i the double - * @return a string - */ - public static String pc(float i) { - return f(i * 100, 0) + "%"; - } - - /** - * Get a percent as the percent of i out of "of" with custom decimal places - * - * @param i the percent out of - * @param of of of - * @param p the decimal places - * @return the string - */ - public static String pc(int i, int of, int p) { - return f(100.0 * (((double) i) / ((double) of)), p) + "%"; - } - - /** - * Get a percent as the percent of i out of "of" - * - * @param i the percent out of - * @param of of of - * @return the string - */ - public static String pc(int i, int of) { - return pc(i, of, 0); - } - - /** - * Get a percent as the percent of i out of "of" with custom decimal places - * - * @param i the percent out of - * @param of of of - * @param p the decimal places - * @return the string - */ - public static String pc(long i, long of, int p) { - return f(100.0 * (((double) i) / ((double) of)), p) + "%"; - } - - /** - * Get a percent as the percent of i out of "of" - * - * @param i the percent out of - * @param of of of - * @return the string - */ - public static String pc(long i, long of) { - return pc(i, of, 0); - } - - /** - * Milliseconds to seconds (double) - * - * @param ms the milliseconds - * @return a formatted string to milliseconds - */ - public static String msSeconds(long ms) { - return f((double) ms / 1000.0); - } - - /** - * Milliseconds to seconds (double) custom decimals - * - * @param ms the milliseconds - * @param p number of decimal points - * @return a formatted string to milliseconds - */ - public static String msSeconds(long ms, int p) { - return f((double) ms / 1000.0, p); - } - - /** - * nanoseconds to seconds (double) - * - * @return a formatted string to nanoseconds - */ - public static String nsMs(long ns) { - return f((double) ns / 1000000.0); - } - - /** - * nanoseconds to seconds (double) custom decimals - * - * @param p number of decimal points - * @return a formatted string to nanoseconds - */ - public static String nsMs(long ns, int p) { - return f((double) ns / 1000000.0, p); - } - - /** - * nanoseconds to seconds (double) custom decimals - * - * @param p number of decimal points - * @return a formatted string to nanoseconds - */ - public static String nsMsd(long ns, int p) { - return fd((double) ns / 1000000.0, p); - } - - /** - * Get roman numeral representation of the int - * - * @param num the int - * @return the numerals - */ - public static String toRoman(int num) { - LinkedHashMap roman_numerals = new LinkedHashMap<>(); - - roman_numerals.put("M", 1000); - roman_numerals.put("CM", 900); - roman_numerals.put("D", 500); - roman_numerals.put("CD", 400); - roman_numerals.put("C", 100); - roman_numerals.put("XC", 90); - roman_numerals.put("L", 50); - roman_numerals.put("XL", 40); - roman_numerals.put("X", 10); - roman_numerals.put("IX", 9); - roman_numerals.put("V", 5); - roman_numerals.put("IV", 4); - roman_numerals.put("I", 1); - - StringBuilder res = new StringBuilder(); - - for (Map.Entry entry : roman_numerals.entrySet()) { - int matches = num / entry.getValue(); - - res.append(repeat(entry.getKey(), matches)); - num = num % entry.getValue(); - } - - return res.toString(); - } - - /** - * Get the number representation from roman numerals. - * - * @param number the roman number - * @return the int representation - */ - public static int fromRoman(String number) { - if (number.isEmpty()) { - return 0; - } - - number = number.toUpperCase(); - - if (number.startsWith("M")) { - return 1000 + fromRoman(number.substring(1)); - } - - if (number.startsWith("CM")) { - return 900 + fromRoman(number.substring(2)); - } - - if (number.startsWith("D")) { - return 500 + fromRoman(number.substring(1)); - } - - if (number.startsWith("CD")) { - return 400 + fromRoman(number.substring(2)); - } - - if (number.startsWith("C")) { - return 100 + fromRoman(number.substring(1)); - } - - if (number.startsWith("XC")) { - return 90 + fromRoman(number.substring(2)); - } - - if (number.startsWith("L")) { - return 50 + fromRoman(number.substring(1)); - } - - if (number.startsWith("XL")) { - return 40 + fromRoman(number.substring(2)); - } - - if (number.startsWith("X")) { - return 10 + fromRoman(number.substring(1)); - } - - if (number.startsWith("IX")) { - return 9 + fromRoman(number.substring(2)); - } - - if (number.startsWith("V")) { - return 5 + fromRoman(number.substring(1)); - } - - if (number.startsWith("IV")) { - return 4 + fromRoman(number.substring(2)); - } - - if (number.startsWith("I")) { - return 1 + fromRoman(number.substring(1)); - } - - return 0; - } - - /** - * Repeat a string - * - * @param s the string - * @param n the amount of times to repeat - * @return the repeated string - */ - @SuppressWarnings("StringRepeatCanBeUsed") - public static String repeat(String s, int n) { - if (s == null) { - return null; - } - - final StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < n; i++) { - sb.append(s); - } - - return sb.toString(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/format/MemoryMonitor.java b/core/src/main/java/com/volmit/iris/util/format/MemoryMonitor.java deleted file mode 100644 index 63f175651..000000000 --- a/core/src/main/java/com/volmit/iris/util/format/MemoryMonitor.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.format; - -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.Looper; - -public class MemoryMonitor { - private final ChronoLatch cl; - private final RollingSequence pressureAvg; - private final Runtime runtime; - private Looper looper; - private long usedMemory; - private long garbageMemory; - private long garbageLast; - private long garbageBin; - private long pressure; - - public MemoryMonitor(int sampleDelay) { - this.runtime = Runtime.getRuntime(); - usedMemory = -1; - pressureAvg = new RollingSequence(Math.max(Math.min(100, 1000 / sampleDelay), 3)); - garbageBin = 0; - garbageMemory = -1; - cl = new ChronoLatch(1000); - garbageLast = 0; - pressure = 0; - - looper = new Looper() { - @Override - protected long loop() { - sample(); - return sampleDelay; - } - }; - looper.setPriority(Thread.MIN_PRIORITY); - looper.setName("Memory Monitor"); - looper.start(); - } - - public long getGarbageBytes() { - return garbageMemory; - } - - public long getUsedBytes() { - return usedMemory; - } - - public long getMaxBytes() { - return runtime.maxMemory(); - } - - public long getPressure() { - return (long) pressureAvg.getAverage(); - } - - public double getUsagePercent() { - return usedMemory / (double) getMaxBytes(); - } - - @SuppressWarnings("IfStatementWithIdenticalBranches") - private void sample() { - long used = getVMUse(); - if (usedMemory == -1) { - usedMemory = used; - garbageMemory = 0; - return; - } - - if (used < usedMemory) { - usedMemory = used; - } else { - garbageMemory = used - usedMemory; - } - - long g = garbageMemory - garbageLast; - - if (g >= 0) { - garbageBin += g; - garbageLast = garbageMemory; - } else { - garbageMemory = 0; - garbageLast = 0; - } - - if (cl.flip()) { - if (garbageMemory > 0) { - pressure = garbageBin; - garbageBin = 0; - } else { - pressure = 0; - garbageBin = 0; - } - } - - pressureAvg.put(pressure); - } - - private long getVMUse() { - return runtime.totalMemory() - runtime.freeMemory(); - } - - public void close() { - if (looper != null) { - looper.interrupt(); - looper = null; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Consumer2.java b/core/src/main/java/com/volmit/iris/util/function/Consumer2.java deleted file mode 100644 index c068da44a..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Consumer2.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@SuppressWarnings({"hiding", "RedundantSuppression"}) -@FunctionalInterface -public interface Consumer2 { - void accept(A a, B b); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Consumer2IO.java b/core/src/main/java/com/volmit/iris/util/function/Consumer2IO.java deleted file mode 100644 index 1ce4f7590..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Consumer2IO.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -import java.io.IOException; - -@SuppressWarnings({"hiding", "RedundantSuppression"}) -@FunctionalInterface -public interface Consumer2IO { - void accept(A a, B b) throws IOException; -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Consumer3.java b/core/src/main/java/com/volmit/iris/util/function/Consumer3.java deleted file mode 100644 index a6748afd6..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Consumer3.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@SuppressWarnings("ALL") -@FunctionalInterface -public interface Consumer3 { - void accept(A a, B b, C c); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Consumer4.java b/core/src/main/java/com/volmit/iris/util/function/Consumer4.java deleted file mode 100644 index 3a96b10b1..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Consumer4.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface Consumer4 { - void accept(A a, B b, C c, D d); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Consumer4IO.java b/core/src/main/java/com/volmit/iris/util/function/Consumer4IO.java deleted file mode 100644 index 4a3246e1c..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Consumer4IO.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -import java.io.IOException; - -@FunctionalInterface -public interface Consumer4IO { - void accept(A a, B b, C c, D d) throws IOException; -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Consumer5.java b/core/src/main/java/com/volmit/iris/util/function/Consumer5.java deleted file mode 100644 index 2101f8f0d..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Consumer5.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface Consumer5 { - void accept(A a, B b, C c, D d, E e); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Consumer6.java b/core/src/main/java/com/volmit/iris/util/function/Consumer6.java deleted file mode 100644 index b852bd97e..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Consumer6.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface Consumer6 { - void accept(A a, B b, C c, D d, E e, F f); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Consumer8.java b/core/src/main/java/com/volmit/iris/util/function/Consumer8.java deleted file mode 100644 index 996031e04..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Consumer8.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface Consumer8 { - void accept(A a, B b, C c, D d, E e, F f, G g, H h); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Function2.java b/core/src/main/java/com/volmit/iris/util/function/Function2.java deleted file mode 100644 index 94c0215ad..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Function2.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface Function2 { - R apply(A a, B b); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Function3.java b/core/src/main/java/com/volmit/iris/util/function/Function3.java deleted file mode 100644 index 9801bc068..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Function3.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface Function3 { - R apply(A a, B b, C c); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Function4.java b/core/src/main/java/com/volmit/iris/util/function/Function4.java deleted file mode 100644 index 5e0351b31..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Function4.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface Function4 { - R apply(A a, B b, C c, D d); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/NastyFunction.java b/core/src/main/java/com/volmit/iris/util/function/NastyFunction.java deleted file mode 100644 index dd056c169..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/NastyFunction.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface NastyFunction { - R run(T t) throws Throwable; -} diff --git a/core/src/main/java/com/volmit/iris/util/function/NastyFuture.java b/core/src/main/java/com/volmit/iris/util/function/NastyFuture.java deleted file mode 100644 index 51db74ba1..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/NastyFuture.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -public interface NastyFuture { - R run(); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/NastyRunnable.java b/core/src/main/java/com/volmit/iris/util/function/NastyRunnable.java deleted file mode 100644 index 816080153..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/NastyRunnable.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -public interface NastyRunnable { - void run() throws Throwable; -} diff --git a/core/src/main/java/com/volmit/iris/util/function/NastySupplier.java b/core/src/main/java/com/volmit/iris/util/function/NastySupplier.java deleted file mode 100644 index a8b753d33..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/NastySupplier.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -public interface NastySupplier { - T get() throws Throwable; -} diff --git a/core/src/main/java/com/volmit/iris/util/function/NoiseInjector.java b/core/src/main/java/com/volmit/iris/util/function/NoiseInjector.java deleted file mode 100644 index 6a0135929..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/NoiseInjector.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface NoiseInjector { - double[] combine(double src, double value); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/NoiseProvider.java b/core/src/main/java/com/volmit/iris/util/function/NoiseProvider.java deleted file mode 100644 index fe881fc2e..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/NoiseProvider.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface NoiseProvider { - double noise(double x, double z); -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/function/NoiseProvider3.java b/core/src/main/java/com/volmit/iris/util/function/NoiseProvider3.java deleted file mode 100644 index 7120c0910..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/NoiseProvider3.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -@FunctionalInterface -public interface NoiseProvider3 { - double noise(double x, double y, double z); -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/function/Supplier2.java b/core/src/main/java/com/volmit/iris/util/function/Supplier2.java deleted file mode 100644 index fbe4c5d9b..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Supplier2.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -public interface Supplier2 { - void get(T t, TT tt); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Supplier3.java b/core/src/main/java/com/volmit/iris/util/function/Supplier3.java deleted file mode 100644 index f79aed6da..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Supplier3.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -public interface Supplier3 { - void get(T t, TT tt, TTT ttt); -} diff --git a/core/src/main/java/com/volmit/iris/util/function/Supplier3R.java b/core/src/main/java/com/volmit/iris/util/function/Supplier3R.java deleted file mode 100644 index 77e8c9128..000000000 --- a/core/src/main/java/com/volmit/iris/util/function/Supplier3R.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.function; - -public interface Supplier3R { - TTTT get(T t, TT tt, TTT ttt); -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/Hunk.java b/core/src/main/java/com/volmit/iris/util/hunk/Hunk.java deleted file mode 100644 index 125268e57..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/Hunk.java +++ /dev/null @@ -1,1463 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.IrisPosition; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.function.*; -import com.volmit.iris.util.hunk.storage.*; -import com.volmit.iris.util.hunk.view.*; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.interpolation.InterpolationMethod3D; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.math.BlockPosition; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.MultiBurst; -import com.volmit.iris.util.stream.interpolation.Interpolated; -import org.bukkit.Chunk; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; -import org.bukkit.generator.ChunkGenerator.BiomeGrid; -import org.bukkit.generator.ChunkGenerator.ChunkData; - -import java.io.IOException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; - -@SuppressWarnings("ALL") -public interface Hunk { - /** - * Create a hunk view from a source hunk. This view reads and writes through to - * the source hunk. Its is not a copy. - * - * @param the type - * @param src the source hunk - * @return the hunk view - */ - static Hunk view(Hunk src) { - return new HunkView(src); - } - - static Hunk convertedReadView(Hunk src, Function reader) { - return new FunctionalHunkView(src, reader, null); - } - - static Hunk convertedWriteView(Hunk src, Function writer) { - return new FunctionalHunkView(src, null, writer); - } - - static Hunk convertedReadWriteView(Hunk src, Function reader, Function writer) { - return new FunctionalHunkView(src, reader, writer); - } - - static Hunk view(BiomeGrid biome, int minHeight, int maxHeight) { - return new BiomeGridHunkView(biome, minHeight, maxHeight); - } - - static Hunk fringe(Hunk i, Hunk o) { - return new FringedHunkView<>(i, o); - } - - static Hunk view(ChunkData src) { - return new ChunkDataHunkView(src); - } - - static Hunk viewBlocks(Chunk src) { - return new ChunkHunkView(src); - } - - static Hunk viewBiomes(Chunk src) { - return new ChunkBiomeHunkView(src); - } - - static Hunk newHunk(int w, int h, int d) { - return newArrayHunk(w, h, d); - } - - @SafeVarargs - static Hunk newCombinedHunk(Hunk... hunks) { - return newCombinedArrayHunk(hunks); - } - - static Hunk newArrayHunk(int w, int h, int d) { - return new ArrayHunk<>(w, h, d); - } - - @SafeVarargs - static Hunk newCombinedArrayHunk(Hunk... hunks) { - return combined(Hunk::newArrayHunk, hunks); - } - - static Hunk newSynchronizedArrayHunk(int w, int h, int d) { - return new SynchronizedArrayHunk<>(w, h, d); - } - - @SafeVarargs - static Hunk newCombinedSynchronizedArrayHunk(Hunk... hunks) { - return combined(Hunk::newSynchronizedArrayHunk, hunks); - } - - static Hunk newMappedHunk(int w, int h, int d) { - return new MappedHunk<>(w, h, d); - } - - static Hunk newMappedHunkSynced(int w, int h, int d) { - return new MappedHunk(w, h, d).synchronize(); - } - - @SafeVarargs - static Hunk newCombinedMappedHunk(Hunk... hunks) { - return combined(Hunk::newMappedHunk, hunks); - } - - static Hunk newAtomicHunk(int w, int h, int d) { - return new AtomicHunk<>(w, h, d); - } - - @SafeVarargs - static Hunk newCombinedAtomicHunk(Hunk... hunks) { - return combined(Hunk::newAtomicHunk, hunks); - } - - static Hunk newAtomicDoubleHunk(int w, int h, int d) { - return new AtomicDoubleHunk(w, h, d); - } - - @SafeVarargs - static Hunk newCombinedAtomicDoubleHunk(Hunk... hunks) { - return combined(Hunk::newAtomicDoubleHunk, hunks); - } - - static Hunk newAtomicLongHunk(int w, int h, int d) { - return new AtomicLongHunk(w, h, d); - } - - @SafeVarargs - static Hunk newCombinedAtomicLongHunk(Hunk... hunks) { - return combined(Hunk::newAtomicLongHunk, hunks); - } - - static Hunk newAtomicIntegerHunk(int w, int h, int d) { - return new AtomicIntegerHunk(w, h, d); - } - - @SafeVarargs - static Hunk newCombinedAtomicIntegerHunk(Hunk... hunks) { - return combined(Hunk::newAtomicIntegerHunk, hunks); - } - - /** - * Creates a new bounding hunk from the given hunks - * - * @param the type - * @param factory the factory that creates a hunk - * @param hunks the hunks - * @return the new bounding hunk - */ - @SafeVarargs - static Hunk combined(Function3> factory, Hunk... hunks) { - int w = 0; - int h = 0; - int d = 0; - - for (Hunk i : hunks) { - w = Math.max(w, i.getWidth()); - h = Math.max(h, i.getHeight()); - d = Math.max(d, i.getDepth()); - } - - Hunk b = factory.apply(w, h, d); - - for (Hunk i : hunks) { - b.insert(i); - } - - return b; - } - - static void computeDual2D(int parallelism, Hunk a, Hunk b, Consumer5, Hunk> v) { - if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight() || a.getDepth() != b.getDepth()) { - throw new RuntimeException("Hunk sizes must match!"); - } - - if (a.get2DDimension(parallelism) == 1) { - v.accept(0, 0, 0, a, b); - return; - } - - BurstExecutor e = MultiBurst.burst.burst(parallelism); - KList rq = new KList(parallelism); - getDualSections2D(parallelism, a, b, (xx, yy, zz, ha, hr, r) -> e.queue(() -> - { - v.accept(xx, yy, zz, ha, hr); - - synchronized (rq) { - rq.add(r); - } - }), (x, y, z, hax, hbx) -> - { - a.insert(x, y, z, hax); - b.insert(x, y, z, hbx); - }); - e.complete(); - rq.forEach(Runnable::run); - return; - } - - static void getDualSections2D(int sections, Hunk a, Hunk b, Consumer6, Hunk, Runnable> v, Consumer5, Hunk> inserterAB) { - if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight() || a.getDepth() != b.getDepth()) { - throw new RuntimeException("Hunk sizes must match!"); - } - - int dim = a.get2DDimension(sections); - - if (sections <= 1) { - getDualSection(0, 0, 0, a.getWidth(), a.getHeight(), a.getDepth(), a, b, (ha, hr, r) -> v.accept(0, 0, 0, ha, hr, r), inserterAB); - return; - } - - int w = a.getWidth() / dim; - int wr = a.getWidth() - (w * dim); - int d = a.getDepth() / dim; - int dr = a.getDepth() - (d * dim); - int i, j; - - for (i = 0; i < a.getWidth(); i += w) { - int ii = i; - - for (j = 0; j < a.getDepth(); j += d) { - int jj = j; - getDualSection(i, 0, j, i + w + (i == 0 ? wr : 0), a.getHeight(), j + d + (j == 0 ? dr : 0), a, b, (ha, hr, r) -> v.accept(ii, 0, jj, ha, hr, r), inserterAB); - i = i == 0 ? i + wr : i; - j = j == 0 ? j + dr : j; - } - } - } - - static void getDualSection(int x, int y, int z, int x1, int y1, int z1, Hunk a, Hunk b, Consumer3, Hunk, Runnable> v, Consumer5, Hunk> inserter) { - Hunk copya = a.crop(x, y, z, x1, y1, z1); - Hunk copyb = b.crop(x, y, z, x1, y1, z1); - v.accept(copya, copyb, () -> inserter.accept(x, y, z, copya, copyb)); - } - - /** - * Create a hunk that is optimized for specific uses - * - * @param w width - * @param h height - * @param d depth - * @param type the class type - * @param packed if the hunk is generally more than 50% full (non-null nodes) - * @param concurrent if this hunk must be thread safe - * @param the type - * @return the hunk - */ - static Hunk newHunk(int w, int h, int d, Class type, boolean packed, boolean concurrent) { - if (type.equals(Double.class)) { - return concurrent ? - packed ? (Hunk) newAtomicDoubleHunk(w, h, d) : newMappedHunk(w, h, d) - : packed ? newArrayHunk(w, h, d) : newMappedHunkSynced(w, h, d); - } - - if (type.equals(Integer.class)) { - return concurrent ? - packed ? (Hunk) newAtomicIntegerHunk(w, h, d) : newMappedHunk(w, h, d) - : packed ? newArrayHunk(w, h, d) : newMappedHunkSynced(w, h, d); - } - - if (type.equals(Long.class)) { - return concurrent ? - packed ? (Hunk) newAtomicLongHunk(w, h, d) : newMappedHunk(w, h, d) - : packed ? newArrayHunk(w, h, d) : newMappedHunkSynced(w, h, d); - } - - return concurrent ? - packed ? newAtomicHunk(w, h, d) : newMappedHunk(w, h, d) - : packed ? newArrayHunk(w, h, d) : newMappedHunkSynced(w, h, d); - } - - static IrisPosition rotatedBounding(int w, int h, int d, double x, double y, double z) { - int[] iii = {0, 0, 0}; - int[] aaa = {w, h, d}; - int[] aai = {w, h, 0}; - int[] iaa = {0, h, d}; - int[] aia = {w, 0, d}; - int[] iai = {0, h, 0}; - int[] iia = {0, 0, d}; - int[] aii = {w, 0, 0}; - rotate(x, y, z, iii); - rotate(x, y, z, aaa); - rotate(x, y, z, aai); - rotate(x, y, z, iaa); - rotate(x, y, z, aia); - rotate(x, y, z, iai); - rotate(x, y, z, iia); - rotate(x, y, z, aii); - int maxX = max(iii[0], aaa[0], aai[0], iaa[0], aia[0], iai[0], iia[0], aii[0]); - int minX = min(iii[0], aaa[0], aai[0], iaa[0], aia[0], iai[0], iia[0], aii[0]); - int maxY = max(iii[1], aaa[1], aai[1], iaa[1], aia[1], iai[1], iia[1], aii[1]); - int minY = min(iii[1], aaa[1], aai[1], iaa[1], aia[1], iai[1], iia[1], aii[1]); - int maxZ = max(iii[2], aaa[2], aai[2], iaa[2], aia[2], iai[2], iia[2], aii[2]); - int minZ = min(iii[2], aaa[2], aai[2], iaa[2], aia[2], iai[2], iia[2], aii[2]); - return new IrisPosition(maxX - minX, maxY - minY, maxZ - minZ); - } - - static int max(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) { - return Math.max(Math.max(Math.max(a5, a6), Math.max(a7, a8)), Math.max(Math.max(a1, a2), Math.max(a3, a4))); - } - - static int min(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) { - return Math.min(Math.min(Math.min(a5, a6), Math.min(a7, a8)), Math.min(Math.min(a1, a2), Math.min(a3, a4))); - } - - static void rotate(double x, double y, double z, int[] c) { - if (x % 360 != 0) { - rotateAroundX(Math.toRadians(x), c); - } - - if (y % 360 != 0) { - rotateAroundY(Math.toRadians(y), c); - } - - if (z % 360 != 0) { - rotateAroundZ(Math.toRadians(z), c); - } - } - - static void rotateAroundX(double a, int[] c) { - rotateAroundX(Math.cos(a), Math.sin(a), c); - } - - static void rotateAroundX(double cos, double sin, int[] c) { - int y = (int) Math.floor(cos * (double) (c[1] + 0.5) - sin * (double) (c[2] + 0.5)); - int z = (int) Math.floor(sin * (double) (c[1] + 0.5) + cos * (double) (c[2] + 0.5)); - c[1] = y; - c[2] = z; - } - - static void rotateAroundY(double a, int[] c) { - rotateAroundY(Math.cos(a), Math.sin(a), c); - } - - static void rotateAroundY(double cos, double sin, int[] c) { - int x = (int) Math.floor(cos * (double) (c[0] + 0.5) + sin * (double) (c[2] + 0.5)); - int z = (int) Math.floor(-sin * (double) (c[0] + 0.5) + cos * (double) (c[2] + 0.5)); - c[0] = x; - c[2] = z; - } - - static void rotateAroundZ(double a, int[] c) { - rotateAroundZ(Math.cos(a), Math.sin(a), c); - } - - static void rotateAroundZ(double cos, double sin, int[] c) { - int x = (int) Math.floor(cos * (double) (c[0] + 0.5) - sin * (double) (c[1] + 0.5)); - int y = (int) Math.floor(sin * (double) (c[0] + 0.5) + cos * (double) (c[1] + 0.5)); - c[0] = x; - c[1] = y; - } - - default boolean isMapped() { - return false; - } - - default int getEntryCount() { - return getWidth() * getHeight() * getDepth(); - } - - default Hunk listen(Consumer4 l) { - return new ListeningHunk<>(this, l); - } - - default Hunk synchronize() { - return new SynchronizedHunkView<>(this); - } - - default Hunk trackWrite(AtomicBoolean b) { - return new WriteTrackHunk(this, b); - } - - default Hunk readOnly() { - return new ReadOnlyHunk<>(this); - } - - default int getNonNullEntries() { - AtomicInteger count = new AtomicInteger(); - iterate((x, y, z, v) -> count.getAndAdd(1)); - - return count.get(); - } - - default boolean isAtomic() { - return false; - } - - default Hunk invertY() { - return new InvertedHunkView(this); - } - - default int getMaximumDimension() { - return Math.max(getWidth(), Math.max(getHeight(), getDepth())); - } - - default int getIdeal2DParallelism() { - return getMax2DParallelism() / 4; - } - - default int getIdeal3DParallelism() { - return getMax3DParallelism() / 8; - } - - default int getMinimumDimension() { - return Math.min(getWidth(), Math.min(getHeight(), getDepth())); - } - - default int getMax2DParallelism() { - return (int) Math.pow(getMinimumDimension() / 2f, 2); - } - - default int getMax3DParallelism() { - return (int) Math.pow(getMinimumDimension() / 2f, 3); - } - - default int filterDimension(int dim) { - if (dim <= 1) { - return 1; - } - - dim = dim % 2 != 0 ? dim + 1 : dim; - - if (dim > getMinimumDimension() / 2) { - if (dim <= 2) { - return 1; - } - - dim -= 2; - } - - return dim; - } - - default int get2DDimension(int sections) { - if (sections <= 1) { - return 1; - } - - return filterDimension((int) Math.ceil(Math.sqrt(sections))); - } - - default int get3DDimension(int sections) { - if (sections <= 1) { - return 1; - } - - return filterDimension((int) Math.ceil(Math.cbrt(sections))); - } - - /** - * Iterate surfaces on 2d. Raytraces with a front and a back which stretches - * through surfaces. Essentially what is returned is the following (in - * order)
- *
- *

- * The predicate is used to determine if the given block type is solid or not. - * - *
- *
- * ================================================
- * AX, AZ: Hunk Relative X and Z - * - *
- *
- * HX, HZ: Hunk Positional X and Z (in its parent hunk) - * - *
- *
- * TOP: The top of this surface (top+1 is air above a surface) - * - *
- *
- * BOTTOM: The bottom of this surface (bottom is the lowest SOLID surface before - * either air or bedrock going down further) - * - *
- *
- * LAST_BOTTOM: The previous bottom. If your surface is the top surface, this - * will be -1 as there is no bottom-of-surface above you. However if you are not - * the top surface, this value is equal to the next solid layer above TOP, such - * that ((LAST_BOTTOM - 1) - (TOP + 1)) is how many air blocks are between your - * surface and the surface above you - * - *
- *
- * HUNK: The hunk to set data to.
- * ================================================
- *
- * If we assume your chunk coordinates are x and z, then
- *
- * bX = (x * 16)
- * bZ = (z * 16)
- *
- * (ax, az, hx, hz, top, bottom, lastBottom, hunk) {
- * actualBlockX = ax+hx;
- * actualBlockZ = az+hz;
- *
- * hunkX = ax;
- * hunkZ = az;
- *
- * hunk.set(hunkX, ?, hunkZ, noise(actualBlockX, ?, actualBlockZ));
- * }
- * - * @param p the predicate - * @param c the consumer - * @return this - */ - default Hunk iterateSurfaces2D(Predicate p, Consumer8> c) { - return iterateSurfaces2D(getIdeal2DParallelism(), p, c); - } - - /** - * Iterate surfaces on 2d. Raytraces with a front and a back which stretches - * through surfaces. Essentially what is returned is the following (in - * order)
- *
- *

- * The predicate is used to determine if the given block type is solid or not. - * - *
- *
- * ================================================
- * AX, AZ: Hunk Relative X and Z - * - *
- *
- * HX, HZ: Hunk Positional X and Z (in its parent hunk) - * - *
- *
- * TOP: The top of this surface (top+1 is air above a surface) - * - *
- *
- * BOTTOM: The bottom of this surface (bottom is the lowest SOLID surface before - * either air or bedrock going down further) - * - *
- *
- * LAST_BOTTOM: The previous bottom. If your surface is the top surface, this - * will be -1 as there is no bottom-of-surface above you. However if you are not - * the top surface, this value is equal to the next solid layer above TOP, such - * that ((LAST_BOTTOM - 1) - (TOP + 1)) is how many air blocks are between your - * surface and the surface above you - * - *
- *
- * HUNK: The hunk to set data to.
- * ================================================
- *
- * If we assume your chunk coordinates are x and z, then
- *
- * bX = (x * 16)
- * bZ = (z * 16)
- *
- * (ax, az, hx, hz, top, bottom, lastBottom, hunk) {
- * actualBlockX = ax+hx;
- * actualBlockZ = az+hz;
- *
- * hunkX = ax;
- * hunkZ = az;
- *
- * hunk.set(hunkX, ?, hunkZ, noise(actualBlockX, ?, actualBlockZ));
- * }
- * - * @param parallelism the ideal threads to use on this - * @param p the predicate - * @param c the consumer - * @return this - */ - default Hunk iterateSurfaces2D(int parallelism, Predicate p, Consumer8> c) { - iterate2DTop(parallelism, (ax, az, hox, hoz, h) -> - { - int last = -1; - int in = getHeight() - 1; - boolean hitting = false; - for (int i = getHeight() - 1; i >= 0; i--) { - boolean solid = p.test(h.get(ax, i, az)); - - if (!hitting && solid) { - in = i; - hitting = true; - } else if (hitting && !solid) { - hitting = false; - c.accept(ax, az, hox, hoz, in, i - 1, last, h); - last = i - 1; - } - } - - if (hitting) { - c.accept(ax, az, hox, hoz, in, 0, last, h); - } - }); - - return this; - } - - /** - * Iterate on the xz top of this hunk. When using this consumer, given - *

- * consumer: (ax, az, hx, hz, hunk) - *

- * hunk.set(ax, ?, az, NOISE.get(ax+hx, az+hz)); - * - * @param c the consumer hunkX, hunkZ, hunkOffsetX, hunkOffsetZ. - * @return this - */ - default Hunk iterate2DTop(Consumer5> c) { - return iterate2DTop(getIdeal2DParallelism(), c); - } - - default Hunk drift(int x, int y, int z) { - return new DriftHunkView<>(this, x, y, z); - } - - /** - * Iterate on the xz top of this hunk. When using this consumer, given - *

- * consumer: (ax, az, hx, hz, hunk) - *

- * hunk.set(ax, ?, az, NOISE.get(ax+hx, az+hz)); - * - * @param parallelism the target parallelism value or 0 to disable - * @param c the consumer hunkX, hunkZ, hunkOffsetX, hunkOffsetZ. - * @return this - */ - default Hunk iterate2DTop(int parallelism, Consumer5> c) { - compute2D(parallelism, (x, y, z, h) -> - { - for (int i = 0; i < h.getWidth(); i++) { - for (int k = 0; k < h.getDepth(); k++) { - c.accept(i, k, x, z, h); - } - } - }); - - return this; - } - - default Hunk iterate(Predicate p, Consumer3 c) { - return iterate(getIdeal3DParallelism(), p, c); - } - - default Hunk iterate(int parallelism, Predicate p, Consumer3 c) { - iterate(parallelism, (x, y, z, t) -> - { - if (p.test(t)) { - c.accept(x, y, z); - } - }); - - return this; - } - - default Hunk iterate(Predicate p, Consumer4 c) { - return iterate(getIdeal3DParallelism(), p, c); - } - - default Hunk iterate(int parallelism, Predicate p, Consumer4 c) { - iterate(parallelism, (x, y, z, t) -> - { - if (p.test(t)) { - c.accept(x, y, z, t); - } - }); - - return this; - } - - default Hunk iterate(Consumer3 c) { - return iterate(getIdeal3DParallelism(), c); - } - - default Hunk iterateSync(Consumer3 c) { - for (int i = 0; i < getWidth(); i++) { - for (int j = 0; j < getHeight(); j++) { - for (int k = 0; k < getDepth(); k++) { - c.accept(i, j, k); - } - } - } - - return this; - } - - default Hunk iterateSync(Consumer4 c) { - for (int i = 0; i < getWidth(); i++) { - for (int j = 0; j < getHeight(); j++) { - for (int k = 0; k < getDepth(); k++) { - c.accept(i, j, k, get(i, j, k)); - } - } - } - - return this; - } - - default Hunk updateSync(Function4 c) { - for (int i = 0; i < getWidth(); i++) { - for (int j = 0; j < getHeight(); j++) { - for (int k = 0; k < getDepth(); k++) { - set(i, j, k, c.apply(i, j, k, get(i, j, k))); - } - } - } - - return this; - } - - default Hunk iterateSyncIO(Consumer4IO c) throws IOException { - for (int i = 0; i < getWidth(); i++) { - for (int j = 0; j < getHeight(); j++) { - for (int k = 0; k < getDepth(); k++) { - c.accept(i, j, k, get(i, j, k)); - } - } - } - - return this; - } - - default Hunk iterate(int parallelism, Consumer3 c) { - compute3D(parallelism, (x, y, z, h) -> - { - for (int i = 0; i < h.getWidth(); i++) { - for (int j = 0; j < h.getHeight(); j++) { - for (int k = 0; k < h.getDepth(); k++) { - c.accept(i + x, j + y, k + z); - } - } - } - }); - - return this; - } - - default Hunk iterate(Consumer4 c) { - return iterate(getIdeal3DParallelism(), c); - } - - default Hunk iterate(int parallelism, Consumer4 c) { - compute3D(parallelism, (x, y, z, h) -> - { - for (int i = 0; i < h.getWidth(); i++) { - for (int j = 0; j < h.getHeight(); j++) { - for (int k = 0; k < h.getDepth(); k++) { - c.accept(i + x, j + y, k + z, h.get(i, j, k)); - } - } - } - }); - - return this; - } - - default Hunk compute2D(Consumer4> v) { - return compute2D(getIdeal2DParallelism(), v); - } - - default Hunk compute2D(int parallelism, Consumer4> v) { - if (get2DDimension(parallelism) == 1) { - v.accept(0, 0, 0, this); - return this; - } - - BurstExecutor e = MultiBurst.burst.burst(parallelism); - - if (isAtomic()) { - getSectionsAtomic2D(parallelism, (xx, yy, zz, h) -> e.queue(() -> - { - v.accept(xx, yy, zz, h); - })); - - e.complete(); - } else { - KList rq = new KList(parallelism); - - getSections2D(parallelism, (xx, yy, zz, h, r) -> e.queue(() -> - { - v.accept(xx, yy, zz, h); - - synchronized (rq) { - rq.add(r); - } - }), this::insert); - - e.complete(); - rq.forEach(Runnable::run); - } - - return this; - } - - default Hunk compute2DYRange(int parallelism, int ymin, int ymax, Consumer4> v) { - if (get2DDimension(parallelism) == 1) { - v.accept(0, 0, 0, this); - return this; - } - - BurstExecutor e = MultiBurst.burst.burst(parallelism); - KList rq = new KList(parallelism); - getSections2DYLimit(parallelism, ymin, ymax, (xx, yy, zz, h, r) -> e.queue(() -> - { - v.accept(xx, yy, zz, h); - - synchronized (rq) { - rq.add(r); - } - }), this::insert); - e.complete(); - rq.forEach(Runnable::run); - return this; - } - - default Hunk compute3D(Consumer4> v) { - return compute3D(getIdeal3DParallelism(), v); - } - - default Hunk compute3D(int parallelism, Consumer4> v) { - if (get3DDimension(parallelism) == 1) { - v.accept(0, 0, 0, this); - return this; - } - - BurstExecutor e = MultiBurst.burst.burst(parallelism); - KList rq = new KList(parallelism); - getSections3D(parallelism, (xx, yy, zz, h, r) -> e.queue(() -> - { - v.accept(xx, yy, zz, h); - synchronized (rq) { - rq.add(r); - } - }), this::insert); - e.complete(); - rq.forEach(Runnable::run); - return this; - } - - default Hunk getSections2D(int sections, Consumer5, Runnable> v) { - return getSections2D(sections, v, this::insert); - } - - default Hunk getSectionsAtomic2D(int sections, Consumer4> v) { - int dim = get2DDimension(sections); - - if (sections <= 1) { - getAtomicSection(0, 0, 0, getWidth(), getHeight(), getDepth(), (hh) -> v.accept(0, 0, 0, hh)); - return this; - } - - int w = getWidth() / dim; - int wr = getWidth() - (w * dim); - int d = getDepth() / dim; - int dr = getDepth() - (d * dim); - int i, j; - - for (i = 0; i < getWidth(); i += w) { - int ii = i; - - for (j = 0; j < getDepth(); j += d) { - int jj = j; - getAtomicSection(i, 0, j, i + w + (i == 0 ? wr : 0), getHeight(), j + d + (j == 0 ? dr : 0), (h) -> v.accept(ii, 0, jj, h)); - i = i == 0 ? i + wr : i; - j = j == 0 ? j + dr : j; - } - } - - return this; - } - - default Hunk getSections2D(int sections, Consumer5, Runnable> v, Consumer4> inserter) { - int dim = get2DDimension(sections); - - if (sections <= 1) { - getSection(0, 0, 0, getWidth(), getHeight(), getDepth(), (hh, r) -> v.accept(0, 0, 0, hh, r), inserter); - return this; - } - - int w = getWidth() / dim; - int wr = getWidth() - (w * dim); - int d = getDepth() / dim; - int dr = getDepth() - (d * dim); - int i, j; - - for (i = 0; i < getWidth(); i += w) { - int ii = i; - - for (j = 0; j < getDepth(); j += d) { - int jj = j; - getSection(i, 0, j, i + w + (i == 0 ? wr : 0), getHeight(), j + d + (j == 0 ? dr : 0), (h, r) -> v.accept(ii, 0, jj, h, r), inserter); - i = i == 0 ? i + wr : i; - j = j == 0 ? j + dr : j; - } - } - - return this; - } - - default Hunk getSections2DYLimit(int sections, int ymin, int ymax, Consumer5, Runnable> v, Consumer4> inserter) { - int dim = get2DDimension(sections); - - if (sections <= 1) { - getSection(0, 0, 0, getWidth(), getHeight(), getDepth(), (hh, r) -> v.accept(0, 0, 0, hh, r), inserter); - return this; - } - - int w = getWidth() / dim; - int wr = getWidth() - (w * dim); - int d = getDepth() / dim; - int dr = getDepth() - (d * dim); - int i, j; - - for (i = 0; i < getWidth(); i += w) { - int ii = i; - - for (j = 0; j < getDepth(); j += d) { - int jj = j; - getSection(i, ymin, j, i + w + (i == 0 ? wr : 0), ymax, j + d + (j == 0 ? dr : 0), (h, r) -> v.accept(ii, ymin, jj, h, r), inserter); - i = i == 0 ? i + wr : i; - j = j == 0 ? j + dr : j; - } - } - - return this; - } - - default Hunk getSections3D(int sections, Consumer5, Runnable> v) { - return getSections3D(sections, v, (xx, yy, zz, c) -> insert(xx, yy, zz, c)); - } - - default Hunk getSections3D(int sections, Consumer5, Runnable> v, Consumer4> inserter) { - int dim = get3DDimension(sections); - - if (sections <= 1) { - getSection(0, 0, 0, getWidth(), getHeight(), getDepth(), (hh, r) -> v.accept(0, 0, 0, hh, r), inserter); - return this; - } - - int w = getWidth() / dim; - int h = getHeight() / dim; - int d = getDepth() / dim; - int wr = getWidth() - (w * dim); - int hr = getHeight() - (h * dim); - int dr = getDepth() - (d * dim); - int i, j, k; - - for (i = 0; i < getWidth(); i += w) { - int ii = i; - - for (j = 0; j < getHeight(); j += d) { - int jj = j; - - for (k = 0; k < getDepth(); k += d) { - int kk = k; - getSection(ii, jj, kk, i + w + (i == 0 ? wr : 0), j + h + (j == 0 ? hr : 0), k + d + (k == 0 ? dr : 0), (hh, r) -> v.accept(ii, jj, kk, hh, r), inserter); - i = i == 0 ? i + wr : i; - j = j == 0 ? j + hr : j; - k = k == 0 ? k + dr : k; - } - } - } - - return this; - } - - default Hunk getSection(int x, int y, int z, int x1, int y1, int z1, Consumer2, Runnable> v) { - return getSection(x, y, z, x1, y1, z1, v, (xx, yy, zz, c) -> insert(xx, yy, zz, c)); - } - - default Hunk getSection(int x, int y, int z, int x1, int y1, int z1, Consumer2, Runnable> v, Consumer4> inserter) { - Hunk copy = crop(x, y, z, x1, y1, z1); - v.accept(copy, () -> inserter.accept(x, y, z, copy)); - return this; - } - - default Hunk getAtomicSection(int x, int y, int z, int x1, int y1, int z1, Consumer> v) { - Hunk copy = croppedView(x, y, z, x1, y1, z1); - v.accept(copy); - return this; - } - - /** - * Create a new hunk from a section of this hunk. - * - * @param x1 The min x (inclusive) - * @param y1 The min y (inclusive) - * @param z1 The min z (inclusive) - * @param x2 The max x (exclusive) - * @param y2 The max y (exclusive) - * @param z2 The max z (exclusive) - * @return the new hunk (x2-x1, y2-y1, z2-z1) - */ - default ArrayHunk crop(int x1, int y1, int z1, int x2, int y2, int z2) { - ArrayHunk h = new ArrayHunk(x2 - x1, y2 - y1, z2 - z1); - - for (int i = x1; i < x2; i++) { - for (int j = y1; j < y2; j++) { - for (int k = z1; k < z2; k++) { - h.setRaw(i - x1, j - y1, k - z1, getRaw(i, j, k)); - } - } - } - - return h; - } - - /** - * Create a new view of this same hunk from a section of this hunk. - * Modifications are routed to this hunk! - * - * @param x1 The min x (inclusive) - * @param y1 The min y (inclusive) - * @param z1 The min z (inclusive) - * @param x2 The max x (exclusive) - * @param y2 The max y (exclusive) - * @param z2 The max z (exclusive) - * @return the cropped view of this hunk (x2-x1, y2-y1, z2-z1) - */ - default Hunk croppedView(int x1, int y1, int z1, int x2, int y2, int z2) { - return new HunkView(this, x2 - x1, y2 - y1, z2 - z1, x1, y1, z1); - } - - /** - * @return The X length - */ - int getWidth(); - - /** - * @return The Z length - */ - int getDepth(); - - /** - * @return The Y length - */ - int getHeight(); - - /** - * Set a region - * - * @param x1 inclusive 1st x - * @param y1 inclusive 1st y - * @param z1 inclusive 1st z - * @param x2 inclusive 2nd x - * @param y2 inclusive 2nd y - * @param z2 inclusive 2nd z - * @param t the value to set - */ - default void set(int x1, int y1, int z1, int x2, int y2, int z2, T t) { - for (int i = x1; i <= x2; i++) { - for (int j = y1; j <= y2; j++) { - for (int k = z1; k <= z2; k++) { - setRaw(i, j, k, t); - } - } - } - } - - /** - * Get the value to the closest valid position - * - * @param x the x - * @param y the y - * @param z the z - * @return the value closest to the border of the hunk - */ - default T getClosest(int x, int y, int z) { - return getRaw(x >= getWidth() ? getWidth() - 1 : x < 0 ? 0 : x, y >= getHeight() ? getHeight() - 1 : y < 0 ? 0 : y, z >= getDepth() ? getDepth() - 1 : z < 0 ? 0 : z); - } - - default BlockPosition getCenter() { - return new BlockPosition(getCenterX(), getCenterY(), getCenterZ()); - } - - default int getCenterX() { - return (int) Math.floor(getWidth() / 2); - } - - default int getCenterY() { - return (int) Math.floor(getHeight() / 2); - } - - default int getCenterZ() { - return (int) Math.floor(getDepth() / 2); - } - - default void fill(T t) { - set(0, 0, 0, getWidth() - 1, getHeight() - 1, getDepth() - 1, t); - } - - /** - * Get a 1 node thick hunk representing the face of this hunk - * - * @param f the face - * @return the hunk view of this hunk - */ - default Hunk viewFace(HunkFace f) { - switch (f) { - case BOTTOM: - return croppedView(0, 0, 0, getWidth() - 1, 0, getDepth() - 1); - case EAST: - return croppedView(getWidth() - 1, 0, 0, getWidth() - 1, getHeight() - 1, getDepth() - 1); - case NORTH: - return croppedView(0, 0, 0, getWidth() - 1, getHeight() - 1, 0); - case SOUTH: - return croppedView(0, 0, 0, 0, getHeight() - 1, getDepth() - 1); - case TOP: - return croppedView(0, getHeight() - 1, 0, getWidth() - 1, getHeight() - 1, getDepth() - 1); - case WEST: - return croppedView(0, 0, getDepth() - 1, getWidth() - 1, getHeight() - 1, getDepth() - 1); - default: - break; - } - - return null; - } - - /** - * Crop (copy) a 1 node thick hunk representing the face of this hunk - * - * @param f the face - * @return the hunk copy (face) of this hunk - */ - default Hunk cropFace(HunkFace f) { - switch (f) { - case BOTTOM: - return crop(0, 0, 0, getWidth() - 1, 0, getDepth() - 1); - case EAST: - return crop(getWidth() - 1, 0, 0, getWidth() - 1, getHeight() - 1, getDepth() - 1); - case NORTH: - return crop(0, 0, 0, getWidth() - 1, getHeight() - 1, 0); - case SOUTH: - return crop(0, 0, 0, 0, getHeight() - 1, getDepth() - 1); - case TOP: - return crop(0, getHeight() - 1, 0, getWidth() - 1, getHeight() - 1, getDepth() - 1); - case WEST: - return crop(0, 0, getDepth() - 1, getWidth() - 1, getHeight() - 1, getDepth() - 1); - default: - break; - } - - return null; - } - - /** - * Set a value at the given position - * - * @param x the x - * @param y the y - * @param z the z - * @param t the value - */ - default void set(int x, int y, int z, T t) { - if (!contains(x, y, z)) { - Iris.warn("OUT OF BOUNDS " + x + " " + y + " " + z + " in bounds " + getWidth() + " " + getHeight() + " " + getDepth()); - return; - } - - setRaw(x, y, z, t); - } - - default void setIfExists(int x, int y, int z, T t) { - if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight() || z < 0 || z >= getDepth()) { - return; - } - - setRaw(x, y, z, t); - } - - default T getIfExists(int x, int y, int z, T t) { - if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight() || z < 0 || z >= getDepth()) { - return t; - } - - return getOr(x, y, z, t); - } - - default T getIfExists(int x, int y, int z) { - return getIfExists(x, y, z, null); - } - - /** - * Set a value at the given position without checking coordinate bounds - * - * @param x the x - * @param y the y - * @param z the z - * @param t the value - */ - void setRaw(int x, int y, int z, T t); - - /** - * Get a value at the given position without checking coordinate bounds - * - * @param x the x - * @param y the y - * @param z the z - * @return the value or null - */ - T getRaw(int x, int y, int z); - - /** - * Get a value at the given position - * - * @param x the x - * @param y the y - * @param z the z - * @return the value or null - */ - default T get(int x, int y, int z) { - return getRaw(x, y, z); - } - - default T getOr(int x, int y, int z, T t) { - T v = getRaw(x, y, z); - - if (v == null) { - return t; - } - - return v; - } - - /** - * Insert a hunk into this one with an offset the inserted hunk - * - * @param offX the offset from zero for x - * @param offY the offset from zero for y - * @param offZ the offset from zero for z - * @param hunk the hunk to insert - */ - default void insert(int offX, int offY, int offZ, Hunk hunk) { - insert(offX, offY, offZ, hunk, false); - } - - default void insertSoftly(int offX, int offY, int offZ, Hunk hunk, Predicate shouldOverwrite) { - insertSoftly(offX, offY, offZ, hunk, false, shouldOverwrite); - } - - /** - * Insert a hunk into this one - * - * @param hunk the hunk to insert - */ - default void insert(Hunk hunk) { - insert(0, 0, 0, hunk, false); - } - - /** - * Returns the source of this hunk view. This could return another hunk view, - * not an actual source, however it does return it's underlying data source. If - * this hunk is a data source and not a view, it will return null. - * - * @return the source or null if this is already the source - */ - default Hunk getSource() { - return null; - } - - /** - * Insert a hunk into this one - * - * @param hunk the hunk to insert - * @param inverted invert the inserted hunk or not - */ - default void insert(Hunk hunk, boolean inverted) { - insert(0, 0, 0, hunk, inverted); - } - - /** - * Insert a hunk into this one with an offset and possibly inverting the y of - * the inserted hunk - * - * @param offX the offset from zero for x - * @param offY the offset from zero for y - * @param offZ the offset from zero for z - * @param hunk the hunk to insert - * @param invertY should the inserted hunk be inverted - */ - default void insert(int offX, int offY, int offZ, Hunk hunk, boolean invertY) { - for (int i = offX; i < offX + hunk.getWidth(); i++) { - for (int j = offY; j < offY + hunk.getHeight(); j++) { - for (int k = offZ; k < offZ + hunk.getDepth(); k++) { - setRaw(i, j, k, hunk.getRaw(i - offX, j - offY, k - offZ)); - } - } - } - } - - /** - * Insert a hunk into this one with an offset and possibly inverting the y of. Will never insert a node if its - * already used - * the inserted hunk - * - * @param offX the offset from zero for x - * @param offY the offset from zero for y - * @param offZ the offset from zero for z - * @param hunk the hunk to insert - * @param invertY should the inserted hunk be inverted - */ - default void insertSoftly(int offX, int offY, int offZ, Hunk hunk, boolean invertY, Predicate shouldOverwrite) { - for (int i = offX; i < offX + hunk.getWidth(); i++) { - for (int j = offY; j < offY + hunk.getHeight(); j++) { - for (int k = offZ; k < offZ + hunk.getDepth(); k++) { - if (shouldOverwrite.test(getRaw(i, j, k))) { - setRaw(i, j, k, hunk.getRaw(i - offX, j - offY, k - offZ)); - } - } - } - } - } - - /** - * Acts like fill, however if used by a mapped hunk, will simply clear it - * - * @param b the data to use for fill - */ - default void empty(T b) { - fill(b); - } - - /** - * Take a hunk and scale it up using interpolation - * - * @param scale the scale - * @param d the interpolation method - * @param interpolated the interpolated value converter - * @return the new hunk - */ - default Hunk interpolate3D(double scale, InterpolationMethod3D d, Interpolated interpolated) { - Hunk t = Hunk.newArrayHunk((int) (getWidth() * scale), (int) (getHeight() * scale), (int) (getDepth() * scale)); - NoiseProvider3 n3 = (x, y, z) -> interpolated.toDouble( - t.get((int) (x / scale), - (int) (y / scale), - (int) (z / scale))); - - for (int i = 0; i < t.getWidth(); i++) { - for (int j = 0; j < t.getHeight(); j++) { - for (int k = 0; k < t.getDepth(); k++) { - t.set(i, j, k, interpolated.fromDouble(IrisInterpolation.getNoise3D(d, i, j, k, scale, n3))); - } - } - } - - return t; - } - - /** - * Take a hunk and scale it up using interpolation - * 2D, (using only x and z) assumes the height is 1 - * - * @param scale the scale - * @param d the interpolation method - * @param interpolated the interpolated value converter - * @return the new hunk - */ - default Hunk interpolate2D(double scale, InterpolationMethod d, Interpolated interpolated) { - Hunk t = Hunk.newArrayHunk((int) (getWidth() * scale), 1, (int) (getDepth() * scale)); - NoiseProvider n2 = (x, z) -> interpolated.toDouble( - t.get((int) (x / scale), - 0, - (int) (z / scale))); - - for (int i = 0; i < t.getWidth(); i++) { - for (int j = 0; j < t.getDepth(); j++) { - t.set(i, 0, j, interpolated.fromDouble(IrisInterpolation.getNoise(d, i, j, scale, n2))); - } - } - - return t; - } - - default Hunk rotate(double x, double y, double z, Supplier3R> builder) { - int w = getWidth(); - int h = getHeight(); - int d = getDepth(); - int i, j, k; - int[] c = {w / 2, h / 2, d / 2}; - int[] b = {0, 0, 0}; - int[] iii = {0, 0, 0}; - int[] aaa = {w, h, d}; - int[] aai = {w, h, 0}; - int[] iaa = {0, h, d}; - int[] aia = {w, 0, d}; - int[] iai = {0, h, 0}; - int[] iia = {0, 0, d}; - int[] aii = {w, 0, 0}; - rotate(x, y, z, iii); - rotate(x, y, z, aaa); - rotate(x, y, z, aai); - rotate(x, y, z, iaa); - rotate(x, y, z, aia); - rotate(x, y, z, iai); - rotate(x, y, z, iia); - rotate(x, y, z, aii); - int maxX = max(iii[0], aaa[0], aai[0], iaa[0], aia[0], iai[0], iia[0], aii[0]); - int minX = min(iii[0], aaa[0], aai[0], iaa[0], aia[0], iai[0], iia[0], aii[0]); - int maxY = max(iii[1], aaa[1], aai[1], iaa[1], aia[1], iai[1], iia[1], aii[1]); - int minY = min(iii[1], aaa[1], aai[1], iaa[1], aia[1], iai[1], iia[1], aii[1]); - int maxZ = max(iii[2], aaa[2], aai[2], iaa[2], aia[2], iai[2], iia[2], aii[2]); - int minZ = min(iii[2], aaa[2], aai[2], iaa[2], aia[2], iai[2], iia[2], aii[2]); - Hunk r = builder.get(maxX - minX, maxY - minY, maxZ - minZ); - int[] cr = {(maxX - minX) / 2, (maxY - minY) / 2, (maxZ - minZ) / 2}; - - for (i = 0; i < w; i++) { - for (j = 0; j < h; j++) { - for (k = 0; k < d; k++) { - b[0] = i - c[0]; - b[1] = j - c[1]; - b[2] = k - c[2]; - rotate(x, y, z, b); - - try { - r.set(b[0] + cr[0], b[1] + cr[1], b[2] + cr[2], get(i, j, k)); - } catch (Throwable e) { - - } - } - } - } - - return r; - } - - default boolean isEmpty() { - return false; - } - - default boolean contains(int x, int y, int z) { - return x < getWidth() && x >= 0 && y < getHeight() && y >= 0 && z < getDepth() && z >= 0; - } - - default int volume() { - return getWidth() * getDepth() * getHeight(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/HunkFace.java b/core/src/main/java/com/volmit/iris/util/hunk/HunkFace.java deleted file mode 100644 index 8a245c7fc..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/HunkFace.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk; - -public enum HunkFace { - TOP, - BOTTOM, - EAST, - WEST, - NORTH, - SOUTH -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/HunkFactory.java b/core/src/main/java/com/volmit/iris/util/hunk/HunkFactory.java deleted file mode 100644 index a120cb454..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/HunkFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk; - -@FunctionalInterface -public interface HunkFactory { - Hunk create(int w, int h, int d); -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/bits/DataBits.java b/core/src/main/java/com/volmit/iris/util/hunk/bits/DataBits.java deleted file mode 100644 index 4ed18fa2d..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/bits/DataBits.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.bits; - -import com.volmit.iris.util.data.Varint; -import lombok.Getter; -import org.apache.commons.lang3.Validate; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.concurrent.atomic.AtomicLongArray; -import java.util.function.IntConsumer; - -public class DataBits { - private static final int[] MAGIC = new int[]{ - -1, -1, 0, Integer.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Integer.MIN_VALUE, - 0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756, - 0, Integer.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0, - 390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378, - 306783378, 0, 286331153, 286331153, 0, Integer.MIN_VALUE, 0, 3, 252645135, 252645135, - 0, 238609294, 238609294, 0, 226050910, 226050910, 0, 214748364, 214748364, 0, - 204522252, 204522252, 0, 195225786, 195225786, 0, 186737708, 186737708, 0, 178956970, - 178956970, 0, 171798691, 171798691, 0, 165191049, 165191049, 0, 159072862, 159072862, - 0, 153391689, 153391689, 0, 148102320, 148102320, 0, 143165576, 143165576, 0, - 138547332, 138547332, 0, Integer.MIN_VALUE, 0, 4, 130150524, 130150524, 0, 126322567, - 126322567, 0, 122713351, 122713351, 0, 119304647, 119304647, 0, 116080197, 116080197, - 0, 113025455, 113025455, 0, 110127366, 110127366, 0, 107374182, 107374182, 0, - 104755299, 104755299, 0, 102261126, 102261126, 0, 99882960, 99882960, 0, 97612893, - 97612893, 0, 95443717, 95443717, 0, 93368854, 93368854, 0, 91382282, 91382282, - 0, 89478485, 89478485, 0, 87652393, 87652393, 0, 85899345, 85899345, 0, - 84215045, 84215045, 0, 82595524, 82595524, 0, 81037118, 81037118, 0, 79536431, - 79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303, - 0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0, - 70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Integer.MIN_VALUE, - 0, 5}; - - private final AtomicLongArray data; - @Getter - private final int bits; - private final long mask; - @Getter - private final int size; - private final int valuesPerLong; - private final int divideMul; - private final int divideAdd; - private final int divideShift; - - public DataBits(int bits, int length) { - this(bits, length, (AtomicLongArray) null); - } - - public DataBits(int bits, int length, DataInputStream din) throws IOException { - this(bits, length, longs(din, dataLength(bits, length))); - } - - public DataBits(int bits, int length, AtomicLongArray data) { - Validate.inclusiveBetween(1L, 32L, bits); - this.size = length; - this.bits = bits; - this.mask = (1L << bits) - 1L; - this.valuesPerLong = (char) (64 / bits); - int var3 = 3 * (valuesPerLong - 1); - this.divideMul = MAGIC[var3]; - this.divideAdd = MAGIC[var3 + 1]; - this.divideShift = MAGIC[var3 + 2]; - int var4 = (length + valuesPerLong - 1) / valuesPerLong; - - if (data != null) { - if (data.length() != var4) { - throw new RuntimeException("NO! Trying to load " + data.length() + " into actual size of " + var4 + " because length: " + length + " (bits: " + bits + ")"); - } - this.data = data; - } else { - this.data = new AtomicLongArray(var4); - } - } - - private static int dataLength(int bits, int length) { - return (length + ((char) (64 / bits)) - 1) / ((char) (64 / bits)); - } - - private static AtomicLongArray longs(DataInputStream din, int longSize) throws IOException { - AtomicLongArray a = new AtomicLongArray(longSize); - - for (int i = 0; i < a.length(); i++) { - a.set(i, Varint.readUnsignedVarLong(din)); - } - - return a; - } - - public String toString() { - return "DBits: " + size + "/" + bits + "[" + data.length() + "]"; - } - - private int cellIndex(int var0) { - long var1 = Integer.toUnsignedLong(this.divideMul); - long var3 = Integer.toUnsignedLong(this.divideAdd); - return (int) (var0 * var1 + var3 >> 32L >> this.divideShift); - } - - @SuppressWarnings("PointlessBitwiseExpression") - public int getAndSet(int var0, int var1) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - Validate.inclusiveBetween(0L, this.mask, var1); - int var2 = cellIndex(var0); - long var3 = this.data.get(var2); - int var5 = (var0 - var2 * this.valuesPerLong) * this.bits; - int var6 = (int) (var3 >> var5 & this.mask); - this.data.set(var2, var3 & (this.mask << var5 ^ 0xFFFFFFFFFFFFFFFFL) | (var1 & this.mask) << var5); - return var6; - } - - @SuppressWarnings("PointlessBitwiseExpression") - public void set(int var0, int var1) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - Validate.inclusiveBetween(0L, this.mask, var1); - int var2 = cellIndex(var0); - long var3 = this.data.get(var2); - int var5 = (var0 - var2 * this.valuesPerLong) * this.bits; - - this.data.set(var2, var3 & (this.mask << var5 ^ 0xFFFFFFFFFFFFFFFFL) | (var1 & this.mask) << var5); - } - - public int get(int var0) { - Validate.inclusiveBetween(0L, (size - 1), var0); - int var1 = cellIndex(var0); - long var2 = this.data.get(var1); - int var4 = (var0 - var1 * valuesPerLong) * this.bits; - return (int) (var2 >> var4 & mask); - } - - public AtomicLongArray getRaw() { - return data; - } - - public DataBits setBits(int newBits) { - if (bits != newBits) { - DataBits newData = new DataBits(newBits, size); - - for (int i = 0; i < size; i++) { - newData.set(i, get(i)); - } - - return newData; - } - - return this; - } - - public void getAll(IntConsumer var0) { - int var1 = 0; - for (int i = 0; i < data.length(); i++) { - long var5 = data.get(i); - for (int var7 = 0; var7 < valuesPerLong; var7++) { - var0.accept((int) (var5 & mask)); - var5 >>= bits; - if (++var1 >= size) { - return; - } - } - } - } - - public void write(DataOutputStream dos) throws IOException { - for (int i = 0; i < data.length(); i++) { - Varint.writeUnsignedVarLong(data.get(i), dos); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/bits/DataContainer.java b/core/src/main/java/com/volmit/iris/util/hunk/bits/DataContainer.java deleted file mode 100644 index 2d02875fa..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/bits/DataContainer.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.bits; - -import com.volmit.iris.util.data.Varint; -import it.unimi.dsi.fastutil.ints.*; - -import java.io.*; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -public class DataContainer { - private static final boolean TRIM = Boolean.getBoolean("iris.trim-palette"); - protected static final int INITIAL_BITS = 3; - protected static final int LINEAR_BITS_LIMIT = 4; - protected static final int LINEAR_INITIAL_LENGTH = (int) Math.pow(2, LINEAR_BITS_LIMIT) + 2; - protected static final int[] BIT = computeBitLimits(); - private final Lock read, write; - - private volatile Palette palette; - private volatile DataBits data; - private final int length; - private final Writable writer; - - public DataContainer(Writable writer, int length) { - var lock = new ReentrantReadWriteLock(); - this.read = lock.readLock(); - this.write = lock.writeLock(); - - this.writer = writer; - this.length = length; - this.data = new DataBits(INITIAL_BITS, length); - this.palette = newPalette(INITIAL_BITS); - } - - public DataContainer(DataInputStream din, Writable writer) throws IOException { - var lock = new ReentrantReadWriteLock(); - this.read = lock.readLock(); - this.write = lock.writeLock(); - - this.writer = writer; - this.length = Varint.readUnsignedVarInt(din); - this.palette = newPalette(din); - this.data = new DataBits(palette.bits(), length, din); - trim(); - } - - private static int[] computeBitLimits() { - int[] m = new int[16]; - - for (int i = 0; i < m.length; i++) { - m[i] = (int) Math.pow(2, i); - } - - return m; - } - - protected static int bits(int size) { - if (DataContainer.BIT[INITIAL_BITS] >= size) { - return INITIAL_BITS; - } - - for (int i = 0; i < DataContainer.BIT.length; i++) { - if (DataContainer.BIT[i] >= size) { - return i; - } - } - - return DataContainer.BIT.length - 1; - } - - public String toString() { - return "DataContainer <" + length + " x " + data.getBits() + " bits> -> Palette<" + palette.getClass().getSimpleName().replaceAll("\\QPalette\\E", "") + ">: " + palette.size() + - " " + data.toString() + " PalBit: " + palette.bits(); - } - - public byte[] write() throws IOException { - ByteArrayOutputStream boas = new ByteArrayOutputStream(); - write(boas); - return boas.toByteArray(); - } - - public void write(OutputStream out) throws IOException { - writeDos(new DataOutputStream(out)); - } - - public void writeDos(DataOutputStream dos) throws IOException { - write.lock(); - try { - trim(); - Varint.writeUnsignedVarInt(length, dos); - Varint.writeUnsignedVarInt(palette.size(), dos); - palette.iterateIO((data, __) -> writer.writeNodeData(dos, data)); - data.write(dos); - dos.flush(); - } finally { - write.unlock(); - } - } - - private Palette newPalette(DataInputStream din) throws IOException { - int paletteSize = Varint.readUnsignedVarInt(din); - Palette d = newPalette(bits(paletteSize + 1)); - d.from(paletteSize, writer, din); - return d; - } - - private Palette newPalette(int bits) { - if (bits <= LINEAR_BITS_LIMIT) { - return new LinearPalette<>(LINEAR_INITIAL_LENGTH); - } - - return new HashPalette<>(); - } - - public void set(int position, T t) { - int id; - - read.lock(); - try { - id = palette.id(t); - if (id == -1) { - id = palette.add(t); - if (palette.bits() == data.getBits()) { - data.set(position, id); - return; - } - } - } finally { - read.unlock(); - } - - write.lock(); - try { - updateBits(); - data.set(position, id); - } finally { - write.unlock(); - } - } - - @SuppressWarnings("NonAtomicOperationOnVolatileField") - private void updateBits() { - int bits = palette.bits(); - if (bits == data.getBits()) - return; - - if (data.getBits() <= LINEAR_BITS_LIMIT != bits <= LINEAR_BITS_LIMIT) { - palette = newPalette(bits).from(palette); - } - - data = data.setBits(bits); - } - - public T get(int position) { - read.lock(); - try { - int id = data.get(position); - - if (id <= 0) { - return null; - } - - return palette.get(id); - } finally { - read.unlock(); - } - } - - public int size() { - return data.getSize(); - } - - private void trim() { - var ints = new Int2IntRBTreeMap(); - for (int i = 0; i < length; i++) { - int x = data.get(i); - if (x <= 0) continue; - ints.put(x, x); - } - if (ints.size() == palette.size()) - return; - - int bits = bits(ints.size() + 1); - var trimmed = newPalette(bits); - ints.replaceAll((k, v) -> trimmed.add(palette.get(k))); - var tBits = new DataBits(bits, length); - for (int i = 0; i < length; i++) { - tBits.set(i, ints.get(data.get(i))); - } - - data = tBits; - palette = trimmed; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/bits/HashPalette.java b/core/src/main/java/com/volmit/iris/util/hunk/bits/HashPalette.java deleted file mode 100644 index cf31e6c37..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/bits/HashPalette.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.bits; - -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.function.Consumer2; - -import java.io.DataInputStream; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.concurrent.atomic.AtomicInteger; - -public class HashPalette implements Palette { - private final Object lock = new Object(); - private final KMap palette; - private final KMap lookup; - private final AtomicInteger size; - - public HashPalette() { - this.size = new AtomicInteger(1); - this.palette = new KMap<>(); - this.lookup = new KMap<>(); - } - - @Override - public T get(int id) { - if (id <= 0 || id >= size.get()) { - return null; - } - - return lookup.get(id); - } - - @Override - public int add(T t) { - if (t == null) { - return 0; - } - - return palette.computeIfAbsent(t, $ -> { - synchronized (lock) { - int index = size.getAndIncrement(); - lookup.put(index, t); - return index; - } - }); - } - - @Override - public int id(T t) { - if (t == null) { - return 0; - } - - Integer v = palette.get(t); - return v != null ? v : -1; - } - - @Override - public int size() { - return size.get() - 1; - } - - @Override - public void iterate(Consumer2 c) { - synchronized (lock) { - for (int i = 1; i < size.get(); i++) { - c.accept(lookup.get(i), i); - } - } - } - - @Override - public Palette from(Palette oldPalette) { - oldPalette.iterate((t, i) -> { - if (t == null) throw new NullPointerException("Null palette entries are not allowed!"); - lookup.put(i, t); - palette.put(t, i); - }); - size.set(oldPalette.size() + 1); - return this; - } - - @Override - public Palette from(int size, Writable writable, DataInputStream in) throws IOException { - for (int i = 1; i <= size; i++) { - T t = writable.readNodeData(in); - if (t == null) throw new NullPointerException("Null palette entries are not allowed!"); - lookup.put(i, t); - palette.put(t, i); - } - this.size.set(size + 1); - return this; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/bits/LinearPalette.java b/core/src/main/java/com/volmit/iris/util/hunk/bits/LinearPalette.java deleted file mode 100644 index a93b4887c..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/bits/LinearPalette.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.bits; - -import com.volmit.iris.util.function.Consumer2; -import lombok.Synchronized; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReferenceArray; - -public class LinearPalette implements Palette { - private volatile AtomicReferenceArray palette; - private final AtomicInteger size; - - public LinearPalette(int initialSize) { - this.size = new AtomicInteger(1); - this.palette = new AtomicReferenceArray<>(initialSize); - palette.set(0, null); - } - - @Override - public T get(int id) { - if (id < 0 || id >= size.get()) { - return null; - } - - return palette.get(id); - } - - @Override - public int add(T t) { - int index = size.getAndIncrement(); - if (palette.length() <= index) - grow(index); - palette.set(index, t); - return index; - } - - private synchronized void grow(int lastIndex) { - if (palette.length() > lastIndex) - return; - - AtomicReferenceArray a = new AtomicReferenceArray<>(lastIndex + 1); - for (int i = 0; i < palette.length(); i++) { - a.set(i, palette.get(i)); - } - - palette = a; - } - - @Override - public int id(T t) { - if (t == null) { - return 0; - } - - for (int i = 1; i < size.get(); i++) { - if (t.equals(palette.get(i))) { - return i; - } - } - - return -1; - } - - @Override - public int size() { - return size.get() - 1; - } - - @Override - public void iterate(Consumer2 c) { - for (int i = 1; i <= size(); i++) { - c.accept(palette.get(i), i); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/bits/Palette.java b/core/src/main/java/com/volmit/iris/util/hunk/bits/Palette.java deleted file mode 100644 index 92a190576..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/bits/Palette.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.bits; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.function.Consumer2; -import com.volmit.iris.util.function.Consumer2IO; - -import java.io.DataInputStream; -import java.io.IOException; - -public interface Palette { - T get(int id); - - int add(T t); - - int id(T t); - - int size(); - - default int bits() { - return DataContainer.bits(size() + 1); - } - - void iterate(Consumer2 c); - - default void iterateIO(Consumer2IO c) { - iterate((a, b) -> { - try { - c.accept(a, b); - } catch (IOException e) { - e.printStackTrace(); - } - }); - } - - default Palette from(int size, Writable writable, DataInputStream in) throws IOException { - for (int i = 0; i < size; i++) { - add(writable.readNodeData(in)); - } - - return this; - } - - default Palette from(Palette oldPalette) { - oldPalette.iterate((k, v) -> add(k)); - return this; - } - - default KList list() { - KList t = new KList<>(); - iterate((tx, __) -> t.add(tx)); - return t; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/bits/Writable.java b/core/src/main/java/com/volmit/iris/util/hunk/bits/Writable.java deleted file mode 100644 index 45523f248..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/bits/Writable.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.bits; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public interface Writable { - T readNodeData(DataInputStream din) throws IOException; - - void writeNodeData(DataOutputStream dos, T t) throws IOException; -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/MappedHunk.java b/core/src/main/java/com/volmit/iris/util/hunk/storage/MappedHunk.java deleted file mode 100644 index 553f8c9c6..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/MappedHunk.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.storage; - -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.function.Consumer4; -import com.volmit.iris.util.function.Consumer4IO; -import com.volmit.iris.util.hunk.Hunk; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.io.IOException; -import java.util.Map; - -@SuppressWarnings({"Lombok"}) -@Data -@EqualsAndHashCode(callSuper = false) -public class MappedHunk extends StorageHunk implements Hunk { - private final Map data; - - public MappedHunk(int w, int h, int d) { - super(w, h, d); - data = new KMap<>(); - } - - public int getEntryCount() { - return data.size(); - } - - public boolean isMapped() { - return true; - } - - public boolean isEmpty() { - return data.isEmpty(); - } - - @Override - public void setRaw(int x, int y, int z, T t) { - if (t == null) { - data.remove(index(x, y, z)); - return; - } - - data.put(index(x, y, z), t); - } - - private Integer index(int x, int y, int z) { - return (z * getWidth() * getHeight()) + (y * getWidth()) + x; - } - - @Override - public synchronized Hunk iterateSync(Consumer4 c) { - int idx, z; - - for (Map.Entry g : data.entrySet()) { - idx = g.getKey(); - z = idx / (getWidth() * getHeight()); - idx -= (z * getWidth() * getHeight()); - c.accept(idx % getWidth(), idx / getWidth(), z, g.getValue()); - } - - return this; - } - - @Override - public synchronized Hunk iterateSyncIO(Consumer4IO c) throws IOException { - int idx, z; - - for (Map.Entry g : data.entrySet()) { - idx = g.getKey(); - z = idx / (getWidth() * getHeight()); - idx -= (z * getWidth() * getHeight()); - c.accept(idx % getWidth(), idx / getWidth(), z, g.getValue()); - } - - return this; - } - - @Override - public void empty(T b) { - data.clear(); - } - - @Override - public T getRaw(int x, int y, int z) { - return data.get(index(x, y, z)); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/MappedSyncHunk.java b/core/src/main/java/com/volmit/iris/util/hunk/storage/MappedSyncHunk.java deleted file mode 100644 index 947ec77b3..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/MappedSyncHunk.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.storage; - -import com.volmit.iris.util.function.Consumer4; -import com.volmit.iris.util.function.Consumer4IO; -import com.volmit.iris.util.hunk.Hunk; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -@SuppressWarnings({"Lombok"}) -@Data -@EqualsAndHashCode(callSuper = false) -public class MappedSyncHunk extends StorageHunk implements Hunk { - private final Map data; - - public MappedSyncHunk(int w, int h, int d) { - super(w, h, d); - data = new HashMap<>(); - } - - public int getEntryCount() { - return data.size(); - } - - public boolean isMapped() { - return true; - } - - public boolean isEmpty() { - synchronized (data) { - return data.isEmpty(); - } - } - - @Override - public void setRaw(int x, int y, int z, T t) { - synchronized (data) { - if (t == null) { - data.remove(index(x, y, z)); - return; - } - - data.put(index(x, y, z), t); - } - } - - private Integer index(int x, int y, int z) { - return (z * getWidth() * getHeight()) + (y * getWidth()) + x; - } - - @Override - public synchronized Hunk iterateSync(Consumer4 c) { - synchronized (data) { - int idx, z; - - for (Map.Entry g : data.entrySet()) { - idx = g.getKey(); - z = idx / (getWidth() * getHeight()); - idx -= (z * getWidth() * getHeight()); - c.accept(idx % getWidth(), idx / getWidth(), z, g.getValue()); - } - - return this; - } - } - - @Override - public synchronized Hunk iterateSyncIO(Consumer4IO c) throws IOException { - synchronized (data) { - int idx, z; - - for (Map.Entry g : data.entrySet()) { - idx = g.getKey(); - z = idx / (getWidth() * getHeight()); - idx -= (z * getWidth() * getHeight()); - c.accept(idx % getWidth(), idx / getWidth(), z, g.getValue()); - } - - return this; - } - } - - @Override - public void empty(T b) { - synchronized (data) { - data.clear(); - } - } - - @Override - public T getRaw(int x, int y, int z) { - synchronized (data) { - return data.get(index(x, y, z)); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/PaletteHunk.java b/core/src/main/java/com/volmit/iris/util/hunk/storage/PaletteHunk.java deleted file mode 100644 index ee9b13c0a..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/PaletteHunk.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.storage; - -import com.volmit.iris.util.function.Consumer4; -import com.volmit.iris.util.function.Consumer4IO; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.hunk.bits.DataContainer; -import com.volmit.iris.util.hunk.bits.Writable; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.io.IOException; - -@SuppressWarnings({"Lombok"}) -@Data -@EqualsAndHashCode(callSuper = false) -public class PaletteHunk extends StorageHunk implements Hunk { - private DataContainer data; - - public PaletteHunk(int w, int h, int d, Writable writer) { - super(w, h, d); - data = new DataContainer<>(writer, w * h * d); - } - - public void setPalette(DataContainer c) { - data = c; - } - - public boolean isMapped() { - return false; - } - - private int index(int x, int y, int z) { - return (z * getWidth() * getHeight()) + (y * getWidth()) + x; - } - - @Override - public synchronized Hunk iterateSync(Consumer4 c) { - for (int i = 0; i < getWidth(); i++) { - for (int j = 0; j < getHeight(); j++) { - for (int k = 0; k < getDepth(); k++) { - T t = getRaw(i, j, k); - if (t != null) { - c.accept(i, j, k, t); - } - } - } - } - return this; - } - - @Override - public synchronized Hunk iterateSyncIO(Consumer4IO c) throws IOException { - for (int i = 0; i < getWidth(); i++) { - for (int j = 0; j < getHeight(); j++) { - for (int k = 0; k < getDepth(); k++) { - T t = getRaw(i, j, k); - if (t != null) { - c.accept(i, j, k, t); - } - } - } - } - return this; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - data.set(index(x, y, z), t); - } - - @Override - public T getRaw(int x, int y, int z) { - return data.get(index(x, y, z)); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/storage/PaletteOrHunk.java b/core/src/main/java/com/volmit/iris/util/hunk/storage/PaletteOrHunk.java deleted file mode 100644 index ea7c4e30e..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/storage/PaletteOrHunk.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.storage; - -import com.volmit.iris.util.function.Consumer4; -import com.volmit.iris.util.function.Consumer4IO; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.hunk.bits.DataContainer; -import com.volmit.iris.util.hunk.bits.Writable; - -import java.io.IOException; -import java.util.function.Supplier; - -public abstract class PaletteOrHunk extends StorageHunk implements Hunk, Writable { - private final Hunk hunk; - - public PaletteOrHunk(int width, int height, int depth, boolean allow, Supplier> factory) { - super(width, height, depth); - hunk = (allow && (width * height * depth <= 4096)) ? new PaletteHunk<>(width, height, depth, this) : factory.get(); - } - - public DataContainer palette() { - return isPalette() ? ((PaletteHunk) hunk).getData() : null; - } - - public boolean isPalette() { - return hunk instanceof PaletteHunk; - } - - public void setPalette(DataContainer c) { - if (isPalette()) { - ((PaletteHunk) hunk).setPalette(c); - } - } - - @Override - public void setRaw(int x, int y, int z, T t) { - hunk.setRaw(x, y, z, t); - } - - @Override - public T getRaw(int x, int y, int z) { - return hunk.getRaw(x, y, z); - } - - public int getEntryCount() { - return hunk.getEntryCount(); - } - - public boolean isMapped() { - return hunk.isMapped(); - } - - public boolean isEmpty() { - return hunk.isMapped(); - } - - @Override - public synchronized Hunk iterateSync(Consumer4 c) { - hunk.iterateSync(c); - return this; - } - - @Override - public synchronized Hunk iterateSyncIO(Consumer4IO c) throws IOException { - hunk.iterateSyncIO(c); - return this; - } - - @Override - public void empty(T b) { - hunk.empty(b); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/BiomeGridHunkHolder.java b/core/src/main/java/com/volmit/iris/util/hunk/view/BiomeGridHunkHolder.java deleted file mode 100644 index 99faeeeec..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/BiomeGridHunkHolder.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.engine.data.chunk.LinkedTerrainChunk; -import com.volmit.iris.util.hunk.storage.AtomicHunk; -import lombok.Getter; -import org.bukkit.block.Biome; -import org.bukkit.generator.ChunkGenerator.BiomeGrid; - -@SuppressWarnings("ClassCanBeRecord") -public class BiomeGridHunkHolder extends AtomicHunk { - @Getter - private final BiomeGrid chunk; - private final int minHeight; - private final int maxHeight; - - public BiomeGridHunkHolder(BiomeGrid chunk, int minHeight, int maxHeight) { - super(16, maxHeight - minHeight, 16); - this.chunk = chunk; - this.minHeight = minHeight; - this.maxHeight = maxHeight; - } - - @Override - public int getWidth() { - return 16; - } - - @Override - public int getDepth() { - return 16; - } - - @Override - public int getHeight() { - return maxHeight - minHeight; - } - - public void apply() { - for (int i = 0; i < getHeight(); i++) { - for (int j = 0; j < getWidth(); j++) { - for (int k = 0; k < getDepth(); k++) { - Biome b = super.getRaw(j, i, k); - - if (b != null) { - chunk.setBiome(j, i + minHeight, k, b); - } - } - } - } - } - - @Override - public Biome getRaw(int x, int y, int z) { - Biome b = super.getRaw(x, y, z); - - return b != null ? b : Biome.PLAINS; - } - - public void forceBiomeBaseInto(int x, int y, int z, Object somethingVeryDirty) { - if (chunk instanceof LinkedTerrainChunk) { - INMS.get().forceBiomeInto(x, y + minHeight, z, somethingVeryDirty, ((LinkedTerrainChunk) chunk).getRawBiome()); - return; - } - INMS.get().forceBiomeInto(x, y + minHeight, z, somethingVeryDirty, chunk); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/BiomeGridHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/BiomeGridHunkView.java deleted file mode 100644 index 19007c184..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/BiomeGridHunkView.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.engine.data.chunk.LinkedTerrainChunk; -import com.volmit.iris.util.hunk.Hunk; -import lombok.Getter; -import org.bukkit.block.Biome; -import org.bukkit.generator.ChunkGenerator.BiomeGrid; - -@SuppressWarnings("ClassCanBeRecord") -public class BiomeGridHunkView implements Hunk { - @Getter - private final BiomeGrid chunk; - private final int minHeight; - private final int maxHeight; - private int highest = -1000; - - public BiomeGridHunkView(BiomeGrid chunk, int minHeight, int maxHeight) { - this.chunk = chunk; - this.minHeight = minHeight; - this.maxHeight = maxHeight; - } - - @Override - public int getWidth() { - return 16; - } - - @Override - public int getDepth() { - return 16; - } - - @Override - public int getHeight() { - return maxHeight - minHeight; - } - - @Override - public void setRaw(int x, int y, int z, Biome t) { - chunk.setBiome(x, y + minHeight, z, t); - - if (y > highest) { - highest = y; - } - } - - @Override - public Biome getRaw(int x, int y, int z) { - return chunk.getBiome(x, y + minHeight, z); - } - - public void forceBiomeBaseInto(int x, int y, int z, Object somethingVeryDirty) { - if (chunk instanceof LinkedTerrainChunk) { - INMS.get().forceBiomeInto(x, y + minHeight, z, somethingVeryDirty, ((LinkedTerrainChunk) chunk).getRawBiome()); - return; - } - INMS.get().forceBiomeInto(x, y + minHeight, z, somethingVeryDirty, chunk); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkBiomeHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkBiomeHunkView.java deleted file mode 100644 index 97c732eeb..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkBiomeHunkView.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.service.EditSVC; -import com.volmit.iris.util.hunk.Hunk; -import org.bukkit.Chunk; -import org.bukkit.block.Biome; - -@SuppressWarnings("ClassCanBeRecord") -public class ChunkBiomeHunkView implements Hunk { - private final Chunk chunk; - - public ChunkBiomeHunkView(Chunk chunk) { - this.chunk = chunk; - } - - @Override - public int getWidth() { - return 16; - } - - @Override - public int getDepth() { - return 16; - } - - @Override - public int getHeight() { - return chunk.getWorld().getMaxHeight(); - } - - @Override - public void setRaw(int x, int y, int z, Biome t) { - if (t == null) { - return; - } - - Iris.service(EditSVC.class).setBiome(chunk.getWorld(), x + (chunk.getX() * 16), y, z + (chunk.getZ() * 16), t); - } - - @Override - public Biome getRaw(int x, int y, int z) { - return Iris.service(EditSVC.class) - .getBiome(chunk.getWorld(), x + (chunk.getX() * 16), y, z + (chunk.getZ() * 16)); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkDataHunkHolder.java b/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkDataHunkHolder.java deleted file mode 100644 index 37529d0c7..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkDataHunkHolder.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.storage.AtomicHunk; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; -import org.bukkit.generator.ChunkGenerator.ChunkData; - -@SuppressWarnings("ClassCanBeRecord") -public class ChunkDataHunkHolder extends AtomicHunk { - private static final BlockData AIR = Material.AIR.createBlockData(); - private final ChunkData chunk; - - public ChunkDataHunkHolder(ChunkData chunk) { - super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16); - this.chunk = chunk; - } - - @Override - public int getWidth() { - return 16; - } - - @Override - public int getDepth() { - return 16; - } - - @Override - public int getHeight() { - return chunk.getMaxHeight() - chunk.getMinHeight(); - } - - @Override - public BlockData getRaw(int x, int y, int z) { - BlockData b = super.getRaw(x, y, z); - - return b != null ? b : AIR; - } - - public void apply() { - for (int i = 0; i < getHeight(); i++) { - for (int j = 0; j < getWidth(); j++) { - for (int k = 0; k < getDepth(); k++) { - BlockData b = super.getRaw(j, i, k); - - if (b != null) { - chunk.setBlock(j, i + chunk.getMinHeight(), k, b); - } - } - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkDataHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkDataHunkView.java deleted file mode 100644 index b27c22fa1..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkDataHunkView.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.hunk.Hunk; -import org.bukkit.block.data.BlockData; -import org.bukkit.generator.ChunkGenerator.ChunkData; - -@SuppressWarnings("ClassCanBeRecord") -public class ChunkDataHunkView implements Hunk { - private static final BlockData AIR = B.getAir(); - private final ChunkData chunk; - - public ChunkDataHunkView(ChunkData chunk) { - this.chunk = chunk; - } - - @Override - public int getWidth() { - return 16; - } - - @Override - public int getDepth() { - return 16; - } - - @Override - public int getHeight() { - return chunk.getMaxHeight() - chunk.getMinHeight(); - } - - @Override - public void set(int x1, int y1, int z1, int x2, int y2, int z2, BlockData t) { - if (t == null) { - return; - } - - chunk.setRegion(x1, y1 + chunk.getMinHeight(), z1, x2, y2 + chunk.getMinHeight(), z2, t); - } - - - public BlockData get(int x, int y, int z) { - return getRaw(x, y, z); - } - - public void set(int x, int y, int z, BlockData t) { - setRaw(x, y, z, t); - } - - @Override - public void setRaw(int x, int y, int z, BlockData t) { - if (t == null) { - return; - } - - try { - if (t instanceof IrisCustomData d) - t = d.getBase(); - chunk.setBlock(x, y + chunk.getMinHeight(), z, t); - } catch (Throwable ignored) { - - } - } - - @Override - public BlockData getRaw(int x, int y, int z) { - try { - - return chunk.getBlockData(x, y + chunk.getMinHeight(), z); - } catch (Throwable e) { - - } - - return AIR; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkHunkView.java deleted file mode 100644 index f5d1df9f2..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/ChunkHunkView.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.service.EditSVC; -import com.volmit.iris.util.hunk.Hunk; -import org.bukkit.Chunk; -import org.bukkit.block.data.BlockData; - -@SuppressWarnings("ClassCanBeRecord") -public class ChunkHunkView implements Hunk { - private final Chunk chunk; - - public ChunkHunkView(Chunk chunk) { - this.chunk = chunk; - } - - @Override - public int getWidth() { - return 16; - } - - @Override - public int getDepth() { - return 16; - } - - @Override - public int getHeight() { - return chunk.getWorld().getMaxHeight(); - } - - @Override - public void setRaw(int x, int y, int z, BlockData t) { - if (t == null) { - return; - } - - Iris.service(EditSVC.class).set(chunk.getWorld(), x + (chunk.getX() * 16), y, z + (chunk.getZ() * 16), t); - } - - @Override - public BlockData getRaw(int x, int y, int z) { - return Iris.service(EditSVC.class).get(chunk.getWorld(), x + (chunk.getX() * 16), y, z + (chunk.getZ() * 16)); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/DriftHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/DriftHunkView.java deleted file mode 100644 index 4c1242aef..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/DriftHunkView.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -@SuppressWarnings("ClassCanBeRecord") -public class DriftHunkView implements Hunk { - private final int ox; - private final int oy; - private final int oz; - private final Hunk src; - - public DriftHunkView(Hunk src, int ox, int oy, int oz) { - this.src = src; - this.ox = ox; - this.oy = oy; - this.oz = oz; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - src.setRaw(x + ox, y + oy, z + oz, t); - } - - @Override - public T getRaw(int x, int y, int z) { - return src.getRaw(x + ox, y + oy, z + oz); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/FringedHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/FringedHunkView.java deleted file mode 100644 index 2349d3646..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/FringedHunkView.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -@SuppressWarnings("ClassCanBeRecord") -public class FringedHunkView implements Hunk { - private final Hunk src; - private final Hunk out; - - public FringedHunkView(Hunk src, Hunk out) { - this.src = src; - this.out = out; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - out.setRaw(x, y, z, t); - } - - @Override - public T getRaw(int x, int y, int z) { - return src.getRaw(x, y, z); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/FunctionalHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/FunctionalHunkView.java deleted file mode 100644 index 86a4c281f..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/FunctionalHunkView.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -import java.util.function.Function; - -public class FunctionalHunkView implements Hunk { - private final Hunk src; - private final Function converter; - private final Function backConverter; - - public FunctionalHunkView(Hunk src, Function converter, Function backConverter) { - this.src = src; - this.converter = converter; - this.backConverter = backConverter; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - if (backConverter == null) { - throw new UnsupportedOperationException("You cannot writeNodeData to this hunk (Read Only)"); - } - - src.setRaw(x, y, z, backConverter.apply(t)); - } - - @Override - public T getRaw(int x, int y, int z) { - if (converter == null) { - throw new UnsupportedOperationException("You cannot read this hunk (Write Only)"); - } - - return converter.apply(src.getRaw(x, y, z)); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public Hunk getSource() { - throw new UnsupportedOperationException("You cannot read this hunk's source because it's a different type."); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/HunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/HunkView.java deleted file mode 100644 index ca4cf4dc2..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/HunkView.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -public class HunkView implements Hunk { - private final int ox; - private final int oy; - private final int oz; - private final int w; - private final int h; - private final int d; - private final Hunk src; - - public HunkView(Hunk src) { - this(src, src.getWidth(), src.getHeight(), src.getDepth()); - } - - public HunkView(Hunk src, int w, int h, int d) { - this(src, w, h, d, 0, 0, 0); - } - - public HunkView(Hunk src, int w, int h, int d, int ox, int oy, int oz) { - this.src = src; - this.w = w; - this.h = h; - this.d = d; - this.ox = ox; - this.oy = oy; - this.oz = oz; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - src.setRaw(x + ox, y + oy, z + oz, t); - } - - @Override - public T getRaw(int x, int y, int z) { - return src.getRaw(x + ox, y + oy, z + oz); - } - - @Override - public int getWidth() { - return w; - } - - @Override - public int getDepth() { - return d; - } - - @Override - public int getHeight() { - return h; - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/InvertedHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/InvertedHunkView.java deleted file mode 100644 index fc45ab195..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/InvertedHunkView.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -@SuppressWarnings("ClassCanBeRecord") -public class InvertedHunkView implements Hunk { - private final Hunk src; - - public InvertedHunkView(Hunk src) { - this.src = src; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - src.setRaw(x, (getHeight() - 1) - y, z, t); - } - - @Override - public T getRaw(int x, int y, int z) { - return src.getRaw(x, y, z); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/ListeningHunk.java b/core/src/main/java/com/volmit/iris/util/hunk/view/ListeningHunk.java deleted file mode 100644 index f33fb5734..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/ListeningHunk.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.function.Consumer4; -import com.volmit.iris.util.hunk.Hunk; - -@SuppressWarnings("ClassCanBeRecord") -public class ListeningHunk implements Hunk { - private final Hunk src; - private final Consumer4 listener; - - public ListeningHunk(Hunk src, Consumer4 listener) { - this.src = src; - this.listener = listener; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - listener.accept(x, y, z, t); - src.setRaw(x, y, z, t); - } - - @Override - public T getRaw(int x, int y, int z) { - return src.getRaw(x, y, z); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/ReadOnlyHunk.java b/core/src/main/java/com/volmit/iris/util/hunk/view/ReadOnlyHunk.java deleted file mode 100644 index 8448b0468..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/ReadOnlyHunk.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -@SuppressWarnings("ClassCanBeRecord") -public class ReadOnlyHunk implements Hunk { - private final Hunk src; - - public ReadOnlyHunk(Hunk src) { - this.src = src; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - throw new IllegalStateException("This hunk is read only!"); - } - - @Override - public T getRaw(int x, int y, int z) { - return src.getRaw(x, y, z); - } - - @Override - public void set(int x1, int y1, int z1, int x2, int y2, int z2, T t) { - throw new IllegalStateException("This hunk is read only!"); - } - - @Override - public void fill(T t) { - throw new IllegalStateException("This hunk is read only!"); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/RotatedXHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/RotatedXHunkView.java deleted file mode 100644 index b18fa3a93..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/RotatedXHunkView.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -public class RotatedXHunkView implements Hunk { - private final Hunk src; - private final double sin; - private final double cos; - - public RotatedXHunkView(Hunk src, double deg) { - this.src = src; - this.sin = Math.sin(Math.toRadians(deg)); - this.cos = Math.cos(Math.toRadians(deg)); - } - - @Override - public void setRaw(int x, int y, int z, T t) { - int yc = (int) Math.round(cos * (getHeight() / 2f) - sin * (getDepth() / 2f)); - int zc = (int) Math.round(sin * (getHeight() / 2f) + cos * (getDepth() / 2f)); - src.setIfExists(x, - (int) Math.round(cos * (y - yc) - sin * (z - zc)) - yc, - (int) Math.round(sin * y - yc + cos * (z - zc)) - zc, - t); - } - - @Override - public T getRaw(int x, int y, int z) { - int yc = (int) Math.round(cos * (getHeight() / 2f) - sin * (getDepth() / 2f)); - int zc = (int) Math.round(sin * (getHeight() / 2f) + cos * (getDepth() / 2f)); - return src.getIfExists(x, - (int) Math.round(cos * (y - yc) - sin * (z - zc)) - yc, - (int) Math.round(sin * y - yc + cos * (z - zc)) - zc - ); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/RotatedYHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/RotatedYHunkView.java deleted file mode 100644 index 566cdc2b4..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/RotatedYHunkView.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -public class RotatedYHunkView implements Hunk { - private final Hunk src; - private final double sin; - private final double cos; - - public RotatedYHunkView(Hunk src, double deg) { - this.src = src; - this.sin = Math.sin(Math.toRadians(deg)); - this.cos = Math.cos(Math.toRadians(deg)); - } - - @Override - public void setRaw(int x, int y, int z, T t) { - int xc = (int) Math.round(cos * (getWidth() / 2f) + sin * (getDepth() / 2f)); - int zc = (int) Math.round(-sin * (getWidth() / 2f) + cos * (getDepth() / 2f)); - src.setIfExists((int) - Math.round(cos * (x - xc) + sin * (z - zc)) - xc, - y, - (int) Math.round(-sin * (x - xc) + cos * (z - zc)) - zc, t); - } - - @Override - public T getRaw(int x, int y, int z) { - int xc = (int) Math.round(cos * (getWidth() / 2f) + sin * (getDepth() / 2f)); - int zc = (int) Math.round(-sin * (getWidth() / 2f) + cos * (getDepth() / 2f)); - return src.getIfExists( - (int) Math.round(cos * (x - xc) + sin * (z - zc)) - xc, - y, - (int) Math.round(-sin * (x - xc) + cos * (z - zc)) - zc - ); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/RotatedZHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/RotatedZHunkView.java deleted file mode 100644 index ff68d26cf..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/RotatedZHunkView.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -public class RotatedZHunkView implements Hunk { - private final Hunk src; - private final double sin; - private final double cos; - - public RotatedZHunkView(Hunk src, double deg) { - this.src = src; - this.sin = Math.sin(Math.toRadians(deg)); - this.cos = Math.cos(Math.toRadians(deg)); - } - - @Override - public void setRaw(int x, int y, int z, T t) { - int xc = (int) Math.round(cos * (getWidth() / 2f) - sin * (getHeight() / 2f)); - int yc = (int) Math.round(sin * (getWidth() / 2f) + cos * (getHeight() / 2f)); - src.setIfExists((int) Math.round(cos * (x - xc) - sin * (y - yc)) - xc, (int) Math.round(sin * (x - xc) + cos * (y - yc)) - yc, z, t); - } - - @Override - public T getRaw(int x, int y, int z) { - int xc = (int) Math.round(cos * (getWidth() / 2f) - sin * (getHeight() / 2f)); - int yc = (int) Math.round(sin * (getWidth() / 2f) + cos * (getHeight() / 2f)); - return src.getIfExists((int) Math.round(cos * (x - xc) - sin * (y - yc)) - xc, - (int) Math.round(sin * (x - xc) + cos * (y - yc)) - yc - , z); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/SynchronizedHunkView.java b/core/src/main/java/com/volmit/iris/util/hunk/view/SynchronizedHunkView.java deleted file mode 100644 index 5198c0c5a..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/SynchronizedHunkView.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -@SuppressWarnings("ClassCanBeRecord") -public class SynchronizedHunkView implements Hunk { - private final Hunk src; - - public SynchronizedHunkView(Hunk src) { - this.src = src; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - synchronized (src) { - src.setRaw(x, y, z, t); - } - } - - @Override - public T getRaw(int x, int y, int z) { - return src.getRaw(x, y, z); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/hunk/view/WriteTrackHunk.java b/core/src/main/java/com/volmit/iris/util/hunk/view/WriteTrackHunk.java deleted file mode 100644 index ee7b68c33..000000000 --- a/core/src/main/java/com/volmit/iris/util/hunk/view/WriteTrackHunk.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.hunk.view; - -import com.volmit.iris.util.hunk.Hunk; - -import java.util.concurrent.atomic.AtomicBoolean; - -@SuppressWarnings("ClassCanBeRecord") -public class WriteTrackHunk implements Hunk { - private final Hunk src; - private final AtomicBoolean b; - - public WriteTrackHunk(Hunk src, AtomicBoolean b) { - this.src = src; - this.b = b; - } - - @Override - public void setRaw(int x, int y, int z, T t) { - if (!b.get()) { - b.set(true); - } - - src.setRaw(x, y, z, t); - } - - @Override - public T getRaw(int x, int y, int z) { - return src.getRaw(x, y, z); - } - - @Override - public int getWidth() { - return src.getWidth(); - } - - @Override - public int getHeight() { - return src.getHeight(); - } - - @Override - public int getDepth() { - return src.getDepth(); - } - - @Override - public Hunk getSource() { - return src; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/interpolation/CompiledStarcast.java b/core/src/main/java/com/volmit/iris/util/interpolation/CompiledStarcast.java deleted file mode 100644 index b466ffd8c..000000000 --- a/core/src/main/java/com/volmit/iris/util/interpolation/CompiledStarcast.java +++ /dev/null @@ -1,10289 +0,0 @@ -package com.volmit.iris.util.interpolation; - -import com.volmit.iris.util.function.NoiseProvider; - -public class CompiledStarcast { - private static final float[] MAGIC = {0.0F, 1.0F, 1.2246469E-16F, -1.0F - , 0.8660254F, -0.5F, -0.8660254F, 6.123234E-17F - , -1.8369701E-16F, 0.95105654F, 0.309017F, 0.58778524F - , -0.809017F, -0.58778524F, -0.95105654F, 0.5F - , 0.777146F, 0.6293204F, 0.9781476F, -0.20791169F - , 0.4539905F, -0.8910065F, -0.40673664F, -0.9135454F - , -0.9659258F, -0.25881904F, -0.052335955F, 0.9986295F - , 0.70710677F, -0.70710677F, 0.64278764F, 0.76604444F - , 0.9848077F, 0.17364818F, 0.34202015F, -0.9396926F - , -0.34202015F, -0.9848077F, -0.64278764F, 0.809017F - , -0.309017F, 0.52991927F, 0.8480481F, 0.89879405F - , 0.43837115F, 0.9945219F, -0.104528464F, 0.7880108F - , -0.6156615F, -0.9781476F, -0.6946584F, -0.7193398F - , -0.9702957F, -0.2419219F, -0.1391731F, 0.99026805F - , 0.8910065F, 0.98768836F, 0.15643446F, -0.15643446F - , -0.98768836F, -0.4539905F, 0.42261827F, 0.9063078F - , 0.9659258F, 0.25881904F, -0.17364818F, 0.81915206F - , -0.57357645F, 0.087155744F, -0.9961947F, 0.40673664F - , 0.9135454F, 0.7431448F, 0.6691306F, 0.20791169F - , -0.9945219F, -0.7431448F, 0.37460658F, 0.92718387F - , 0.6946584F, 0.7193398F, 0.99939084F, 0.034899496F - , 0.9396926F, -0.6691306F, -0.89879405F, 0.06975647F - , -0.9975641F, -0.76604444F, -0.88294756F, -0.46947157F - , -0.9612617F, 0.27563736F, -0.7880108F, 0.6156615F - , 0.35836795F, 0.9335804F, 0.104528464F, 0.54463905F - , -0.83867055F, -0.777146F, -0.6293204F, -0.9986295F - , 0.052335955F, 0.2923717F, 0.9563047F, 0.5591929F - , 0.82903755F, 0.9961947F, 0.8746197F, -0.4848096F - , -0.12186934F, -0.99254614F, -0.656059F, -0.7547096F - , -0.8480481F, -0.52991927F, -0.99939084F, -0.94551855F - , 0.32556817F, -0.60181504F, 0.7986355F, 0.9612617F - , -0.37460658F, -0.82903755F, 0.2419219F, 0.9702957F - , 0.46947157F, 0.88294756F, -0.034899496F, -0.27563736F - , -0.06975647F, 0.22495106F, 0.97437006F, 0.9998477F - , -0.017452406F, 0.60181504F, -0.7986355F, 0.190809F - , -0.98162717F, -0.92050487F, -0.39073113F, -0.8746197F - , 0.4848096F, 0.98162717F, 0.83867055F, 0.57357645F - , 0.8571673F, -0.5150381F, -0.9563047F, -0.2923717F - , 0.7547096F, -0.32556817F, 0.94551855F, 0.1391731F - , -0.92718387F, 0.12186934F, 0.99254614F, 0.656059F - , 0.7313537F, -0.6819984F, -0.9063078F, -0.42261827F - , -0.190809F, -0.8571673F, 0.5150381F, -0.087155744F - , -0.81915206F, 0.9975641F, -0.43837115F, -0.5591929F - , -0.99026805F, -0.35836795F, -0.54463905F, -0.9335804F}; - - public static float getStarcast(float x, float z, float r, float checks, NoiseProvider n) { - if (checks >= 1 && checks <= 128) { - if (checks < 2) { - return sc1(x, z, r, n); - } - if (checks < 3) { - return sc2(x, z, r, n); - } - if (checks < 4) { - return sc3(x, z, r, n); - } - if (checks < 5) { - return sc4(x, z, r, n); - } - if (checks < 6) { - return sc5(x, z, r, n); - } - if (checks < 7) { - return sc6(x, z, r, n); - } - if (checks < 8) { - return sc7(x, z, r, n); - } - if (checks < 9) { - return sc8(x, z, r, n); - } - if (checks < 10) { - return sc9(x, z, r, n); - } - if (checks < 11) { - return sc10(x, z, r, n); - } - if (checks < 12) { - return sc11(x, z, r, n); - } - if (checks < 13) { - return sc12(x, z, r, n); - } - if (checks < 14) { - return sc13(x, z, r, n); - } - if (checks < 15) { - return sc14(x, z, r, n); - } - if (checks < 16) { - return sc15(x, z, r, n); - } - if (checks < 17) { - return sc16(x, z, r, n); - } - if (checks < 18) { - return sc17(x, z, r, n); - } - if (checks < 19) { - return sc18(x, z, r, n); - } - if (checks < 20) { - return sc19(x, z, r, n); - } - if (checks < 21) { - return sc20(x, z, r, n); - } - if (checks < 22) { - return sc21(x, z, r, n); - } - if (checks < 23) { - return sc22(x, z, r, n); - } - if (checks < 24) { - return sc23(x, z, r, n); - } - if (checks < 25) { - return sc24(x, z, r, n); - } - if (checks < 26) { - return sc25(x, z, r, n); - } - if (checks < 27) { - return sc26(x, z, r, n); - } - if (checks < 28) { - return sc27(x, z, r, n); - } - if (checks < 29) { - return sc28(x, z, r, n); - } - if (checks < 30) { - return sc29(x, z, r, n); - } - if (checks < 31) { - return sc30(x, z, r, n); - } - if (checks < 32) { - return sc31(x, z, r, n); - } - if (checks < 33) { - return sc32(x, z, r, n); - } - if (checks < 34) { - return sc33(x, z, r, n); - } - if (checks < 35) { - return sc34(x, z, r, n); - } - if (checks < 36) { - return sc35(x, z, r, n); - } - if (checks < 37) { - return sc36(x, z, r, n); - } - if (checks < 38) { - return sc37(x, z, r, n); - } - if (checks < 39) { - return sc38(x, z, r, n); - } - if (checks < 40) { - return sc39(x, z, r, n); - } - if (checks < 41) { - return sc40(x, z, r, n); - } - if (checks < 42) { - return sc41(x, z, r, n); - } - if (checks < 43) { - return sc42(x, z, r, n); - } - if (checks < 44) { - return sc43(x, z, r, n); - } - if (checks < 45) { - return sc44(x, z, r, n); - } - if (checks < 46) { - return sc45(x, z, r, n); - } - if (checks < 47) { - return sc46(x, z, r, n); - } - if (checks < 48) { - return sc47(x, z, r, n); - } - if (checks < 49) { - return sc48(x, z, r, n); - } - if (checks < 50) { - return sc49(x, z, r, n); - } - if (checks < 51) { - return sc50(x, z, r, n); - } - if (checks < 52) { - return sc51(x, z, r, n); - } - if (checks < 53) { - return sc52(x, z, r, n); - } - if (checks < 54) { - return sc53(x, z, r, n); - } - if (checks < 55) { - return sc54(x, z, r, n); - } - if (checks < 56) { - return sc55(x, z, r, n); - } - if (checks < 57) { - return sc56(x, z, r, n); - } - if (checks < 58) { - return sc57(x, z, r, n); - } - if (checks < 59) { - return sc58(x, z, r, n); - } - if (checks < 60) { - return sc59(x, z, r, n); - } - if (checks < 61) { - return sc60(x, z, r, n); - } - if (checks < 62) { - return sc61(x, z, r, n); - } - if (checks < 63) { - return sc62(x, z, r, n); - } - if (checks < 64) { - return sc63(x, z, r, n); - } - if (checks < 65) { - return sc64(x, z, r, n); - } - if (checks < 66) { - return sc65(x, z, r, n); - } - if (checks < 67) { - return sc66(x, z, r, n); - } - if (checks < 68) { - return sc67(x, z, r, n); - } - if (checks < 69) { - return sc68(x, z, r, n); - } - if (checks < 70) { - return sc69(x, z, r, n); - } - if (checks < 71) { - return sc70(x, z, r, n); - } - if (checks < 72) { - return sc71(x, z, r, n); - } - if (checks < 73) { - return sc72(x, z, r, n); - } - if (checks < 74) { - return sc73(x, z, r, n); - } - if (checks < 75) { - return sc74(x, z, r, n); - } - if (checks < 76) { - return sc75(x, z, r, n); - } - if (checks < 77) { - return sc76(x, z, r, n); - } - if (checks < 78) { - return sc77(x, z, r, n); - } - if (checks < 79) { - return sc78(x, z, r, n); - } - if (checks < 80) { - return sc79(x, z, r, n); - } - if (checks < 81) { - return sc80(x, z, r, n); - } - if (checks < 82) { - return sc81(x, z, r, n); - } - if (checks < 83) { - return sc82(x, z, r, n); - } - if (checks < 84) { - return sc83(x, z, r, n); - } - if (checks < 85) { - return sc84(x, z, r, n); - } - if (checks < 86) { - return sc85(x, z, r, n); - } - if (checks < 87) { - return sc86(x, z, r, n); - } - if (checks < 88) { - return sc87(x, z, r, n); - } - if (checks < 89) { - return sc88(x, z, r, n); - } - if (checks < 90) { - return sc89(x, z, r, n); - } - if (checks < 91) { - return sc90(x, z, r, n); - } - if (checks < 92) { - return sc91(x, z, r, n); - } - if (checks < 93) { - return sc92(x, z, r, n); - } - if (checks < 94) { - return sc93(x, z, r, n); - } - if (checks < 95) { - return sc94(x, z, r, n); - } - if (checks < 96) { - return sc95(x, z, r, n); - } - if (checks < 97) { - return sc96(x, z, r, n); - } - if (checks < 98) { - return sc97(x, z, r, n); - } - if (checks < 99) { - return sc98(x, z, r, n); - } - if (checks < 100) { - return sc99(x, z, r, n); - } - if (checks < 101) { - return sc100(x, z, r, n); - } - if (checks < 102) { - return sc101(x, z, r, n); - } - if (checks < 103) { - return sc102(x, z, r, n); - } - if (checks < 104) { - return sc103(x, z, r, n); - } - if (checks < 105) { - return sc104(x, z, r, n); - } - if (checks < 106) { - return sc105(x, z, r, n); - } - if (checks < 107) { - return sc106(x, z, r, n); - } - if (checks < 108) { - return sc107(x, z, r, n); - } - if (checks < 109) { - return sc108(x, z, r, n); - } - if (checks < 110) { - return sc109(x, z, r, n); - } - if (checks < 111) { - return sc110(x, z, r, n); - } - if (checks < 112) { - return sc111(x, z, r, n); - } - if (checks < 113) { - return sc112(x, z, r, n); - } - if (checks < 114) { - return sc113(x, z, r, n); - } - if (checks < 115) { - return sc114(x, z, r, n); - } - if (checks < 116) { - return sc115(x, z, r, n); - } - if (checks < 117) { - return sc116(x, z, r, n); - } - if (checks < 118) { - return sc117(x, z, r, n); - } - if (checks < 119) { - return sc118(x, z, r, n); - } - if (checks < 120) { - return sc119(x, z, r, n); - } - if (checks < 121) { - return sc120(x, z, r, n); - } - if (checks < 122) { - return sc121(x, z, r, n); - } - if (checks < 123) { - return sc122(x, z, r, n); - } - if (checks < 124) { - return sc123(x, z, r, n); - } - if (checks < 125) { - return sc124(x, z, r, n); - } - if (checks < 126) { - return sc125(x, z, r, n); - } - if (checks < 127) { - return sc126(x, z, r, n); - } - if (checks < 128) { - return sc127(x, z, r, n); - } - return sc128(x, z, r, n); - } - - float m = 360F / checks; - float v = 0; - - for (int i = 0; i < 360; i += m) { - float sin = (float) Math.sin(Math.toRadians(i)); - float cos = (float) Math.cos(Math.toRadians(i)); - float cx = x + ((r * cos) - (r * sin)); - float cz = z + ((r * sin) + (r * cos)); - v += n.noise(cx, cz); - } - - return v / checks; - } - - private static float sc1(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1]))))) * 1.0F; - } - - private static float sc2(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3]))))) * 0.5F; - } - - private static float sc3(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5]))))) * 0.33333334F; - } - - private static float sc4(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8]))))) * 0.25F; - } - - private static float sc5(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10]))))) * 0.2F; - } - - private static float sc6(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15]))))) * 0.16666667F; - } - - private static float sc7(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.14285715F; - } - - private static float sc8(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28]))))) * 0.125F; - } - - private static float sc9(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31]))))) * 0.11111111F; - } - - private static float sc10(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39]))))) * 0.1F; - } - - private static float sc11(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.09090909F; - } - - private static float sc12(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4]))))) * 0.083333336F; - } - - private static float sc13(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57]))))) * 0.07692308F; - } - - private static float sc14(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32]))))) * 0.071428575F; - } - - private static float sc15(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72]))))) * 0.06666667F; - } - - private static float sc16(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.0625F; - } - - private static float sc17(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.05882353F; - } - - private static float sc18(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84]))))) * 0.055555556F; - } - - private static float sc19(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9]))))) * 0.05263158F; - } - - private static float sc20(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9]))))) * 0.05F; - } - - private static float sc21(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[106]) - (r * MAGIC[105])), z + ((r * MAGIC[105]) + (r * MAGIC[106])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[111]) - (r * MAGIC[110])), z + ((r * MAGIC[110]) + (r * MAGIC[111])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[113]) - (r * MAGIC[112])), z + ((r * MAGIC[112]) + (r * MAGIC[113])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[115]) - (r * MAGIC[114])), z + ((r * MAGIC[114]) + (r * MAGIC[115])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[120]) - (r * MAGIC[119])), z + ((r * MAGIC[119]) + (r * MAGIC[120])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[122]) - (r * MAGIC[121])), z + ((r * MAGIC[121]) + (r * MAGIC[122])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.04761905F; - } - - private static float sc22(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.045454547F; - } - - private static float sc23(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64]))))) * 0.04347826F; - } - - private static float sc24(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64]))))) * 0.041666668F; - } - - private static float sc25(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32]))))) * 0.04F; - } - - private static float sc26(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[134]) - (r * MAGIC[133])), z + ((r * MAGIC[133]) + (r * MAGIC[134])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[136]) - (r * MAGIC[135])), z + ((r * MAGIC[135]) + (r * MAGIC[136])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[137])), z + ((r * MAGIC[137]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[140]) - (r * MAGIC[139])), z + ((r * MAGIC[139]) + (r * MAGIC[140])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[115]) - (r * MAGIC[114])), z + ((r * MAGIC[114]) + (r * MAGIC[115])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[142]) - (r * MAGIC[141])), z + ((r * MAGIC[141]) + (r * MAGIC[142])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[144]) - (r * MAGIC[143])), z + ((r * MAGIC[143]) + (r * MAGIC[144])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57]))))) * 0.03846154F; - } - - private static float sc27(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[134]) - (r * MAGIC[133])), z + ((r * MAGIC[133]) + (r * MAGIC[134])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[136]) - (r * MAGIC[135])), z + ((r * MAGIC[135]) + (r * MAGIC[136])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[137])), z + ((r * MAGIC[137]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[140]) - (r * MAGIC[139])), z + ((r * MAGIC[139]) + (r * MAGIC[140])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[115]) - (r * MAGIC[114])), z + ((r * MAGIC[114]) + (r * MAGIC[115])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[142]) - (r * MAGIC[141])), z + ((r * MAGIC[141]) + (r * MAGIC[142])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[144]) - (r * MAGIC[143])), z + ((r * MAGIC[143]) + (r * MAGIC[144])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57]))))) * 0.037037037F; - } - - private static float sc28(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18]))))) * 0.035714287F; - } - - private static float sc29(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18]))))) * 0.03448276F; - } - - private static float sc30(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18]))))) * 0.033333335F; - } - - private static float sc31(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[145]) - (r * MAGIC[139])), z + ((r * MAGIC[139]) + (r * MAGIC[145])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[133]) - (r * MAGIC[134])), z + ((r * MAGIC[134]) + (r * MAGIC[133])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[149]) - (r * MAGIC[148])), z + ((r * MAGIC[148]) + (r * MAGIC[149])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[137])), z + ((r * MAGIC[137]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[113]) - (r * MAGIC[112])), z + ((r * MAGIC[112]) + (r * MAGIC[113])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[143]) - (r * MAGIC[111])), z + ((r * MAGIC[111]) + (r * MAGIC[143])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[151]) - (r * MAGIC[150])), z + ((r * MAGIC[150]) + (r * MAGIC[151])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[152]) - (r * MAGIC[114])), z + ((r * MAGIC[114]) + (r * MAGIC[152])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[154]) - (r * MAGIC[153])), z + ((r * MAGIC[153]) + (r * MAGIC[154])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.032258064F; - } - - private static float sc32(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[145]) - (r * MAGIC[139])), z + ((r * MAGIC[139]) + (r * MAGIC[145])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[133]) - (r * MAGIC[134])), z + ((r * MAGIC[134]) + (r * MAGIC[133])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[149]) - (r * MAGIC[148])), z + ((r * MAGIC[148]) + (r * MAGIC[149])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[137])), z + ((r * MAGIC[137]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[113]) - (r * MAGIC[112])), z + ((r * MAGIC[112]) + (r * MAGIC[113])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[143]) - (r * MAGIC[111])), z + ((r * MAGIC[111]) + (r * MAGIC[143])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[151]) - (r * MAGIC[150])), z + ((r * MAGIC[150]) + (r * MAGIC[151])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[152]) - (r * MAGIC[114])), z + ((r * MAGIC[114]) + (r * MAGIC[152])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[154]) - (r * MAGIC[153])), z + ((r * MAGIC[153]) + (r * MAGIC[154])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.03125F; - } - - private static float sc33(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32]))))) * 0.030303031F; - } - - private static float sc34(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32]))))) * 0.029411765F; - } - - private static float sc35(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32]))))) * 0.028571429F; - } - - private static float sc36(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32]))))) * 0.027777778F; - } - - private static float sc37(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57]))))) * 0.027027028F; - } - - private static float sc38(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57]))))) * 0.02631579F; - } - - private static float sc39(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57]))))) * 0.025641026F; - } - - private static float sc40(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57]))))) * 0.025F; - } - - private static float sc41(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.024390243F; - } - - private static float sc42(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.023809524F; - } - - private static float sc43(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.023255814F; - } - - private static float sc44(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.022727273F; - } - - private static float sc45(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55]))))) * 0.022222223F; - } - - private static float sc46(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[158]) - (r * MAGIC[157])), z + ((r * MAGIC[157]) + (r * MAGIC[158])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[159]) - (r * MAGIC[152])), z + ((r * MAGIC[152]) + (r * MAGIC[159])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[133]) - (r * MAGIC[134])), z + ((r * MAGIC[134]) + (r * MAGIC[133])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[136]) - (r * MAGIC[135])), z + ((r * MAGIC[135]) + (r * MAGIC[136])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[111]) - (r * MAGIC[110])), z + ((r * MAGIC[110]) + (r * MAGIC[111])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[161]) - (r * MAGIC[160])), z + ((r * MAGIC[160]) + (r * MAGIC[161])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[119]) - (r * MAGIC[120])), z + ((r * MAGIC[120]) + (r * MAGIC[119])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[141]) - (r * MAGIC[142])), z + ((r * MAGIC[142]) + (r * MAGIC[141])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[121])), z + ((r * MAGIC[121]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[164]) - (r * MAGIC[140])), z + ((r * MAGIC[140]) + (r * MAGIC[164])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[105]) - (r * MAGIC[150])), z + ((r * MAGIC[150]) + (r * MAGIC[105])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[166]) - (r * MAGIC[165])), z + ((r * MAGIC[165]) + (r * MAGIC[166])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[148]) - (r * MAGIC[149])), z + ((r * MAGIC[149]) + (r * MAGIC[148])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[106]) - (r * MAGIC[151])), z + ((r * MAGIC[151]) + (r * MAGIC[106])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.02173913F; - } - - private static float sc47(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[158]) - (r * MAGIC[157])), z + ((r * MAGIC[157]) + (r * MAGIC[158])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[159]) - (r * MAGIC[152])), z + ((r * MAGIC[152]) + (r * MAGIC[159])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[133]) - (r * MAGIC[134])), z + ((r * MAGIC[134]) + (r * MAGIC[133])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[136]) - (r * MAGIC[135])), z + ((r * MAGIC[135]) + (r * MAGIC[136])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[111]) - (r * MAGIC[110])), z + ((r * MAGIC[110]) + (r * MAGIC[111])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[161]) - (r * MAGIC[160])), z + ((r * MAGIC[160]) + (r * MAGIC[161])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[119]) - (r * MAGIC[120])), z + ((r * MAGIC[120]) + (r * MAGIC[119])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[141]) - (r * MAGIC[142])), z + ((r * MAGIC[142]) + (r * MAGIC[141])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[121])), z + ((r * MAGIC[121]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[164]) - (r * MAGIC[140])), z + ((r * MAGIC[140]) + (r * MAGIC[164])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[105]) - (r * MAGIC[150])), z + ((r * MAGIC[150]) + (r * MAGIC[105])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[166]) - (r * MAGIC[165])), z + ((r * MAGIC[165]) + (r * MAGIC[166])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[148]) - (r * MAGIC[149])), z + ((r * MAGIC[149]) + (r * MAGIC[148])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[106]) - (r * MAGIC[151])), z + ((r * MAGIC[151]) + (r * MAGIC[106])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.021276595F; - } - - private static float sc48(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[158]) - (r * MAGIC[157])), z + ((r * MAGIC[157]) + (r * MAGIC[158])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[159]) - (r * MAGIC[152])), z + ((r * MAGIC[152]) + (r * MAGIC[159])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[133]) - (r * MAGIC[134])), z + ((r * MAGIC[134]) + (r * MAGIC[133])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[136]) - (r * MAGIC[135])), z + ((r * MAGIC[135]) + (r * MAGIC[136])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[111]) - (r * MAGIC[110])), z + ((r * MAGIC[110]) + (r * MAGIC[111])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[161]) - (r * MAGIC[160])), z + ((r * MAGIC[160]) + (r * MAGIC[161])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[119]) - (r * MAGIC[120])), z + ((r * MAGIC[120]) + (r * MAGIC[119])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[141]) - (r * MAGIC[142])), z + ((r * MAGIC[142]) + (r * MAGIC[141])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[121])), z + ((r * MAGIC[121]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[164]) - (r * MAGIC[140])), z + ((r * MAGIC[140]) + (r * MAGIC[164])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[105]) - (r * MAGIC[150])), z + ((r * MAGIC[150]) + (r * MAGIC[105])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[166]) - (r * MAGIC[165])), z + ((r * MAGIC[165]) + (r * MAGIC[166])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[148]) - (r * MAGIC[149])), z + ((r * MAGIC[149]) + (r * MAGIC[148])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[106]) - (r * MAGIC[151])), z + ((r * MAGIC[151]) + (r * MAGIC[106])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.020833334F; - } - - private static float sc49(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[158]) - (r * MAGIC[157])), z + ((r * MAGIC[157]) + (r * MAGIC[158])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[159]) - (r * MAGIC[152])), z + ((r * MAGIC[152]) + (r * MAGIC[159])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[133]) - (r * MAGIC[134])), z + ((r * MAGIC[134]) + (r * MAGIC[133])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[136]) - (r * MAGIC[135])), z + ((r * MAGIC[135]) + (r * MAGIC[136])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[111]) - (r * MAGIC[110])), z + ((r * MAGIC[110]) + (r * MAGIC[111])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[161]) - (r * MAGIC[160])), z + ((r * MAGIC[160]) + (r * MAGIC[161])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[119]) - (r * MAGIC[120])), z + ((r * MAGIC[120]) + (r * MAGIC[119])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[141]) - (r * MAGIC[142])), z + ((r * MAGIC[142]) + (r * MAGIC[141])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[121])), z + ((r * MAGIC[121]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[164]) - (r * MAGIC[140])), z + ((r * MAGIC[140]) + (r * MAGIC[164])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[105]) - (r * MAGIC[150])), z + ((r * MAGIC[150]) + (r * MAGIC[105])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[166]) - (r * MAGIC[165])), z + ((r * MAGIC[165]) + (r * MAGIC[166])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[148]) - (r * MAGIC[149])), z + ((r * MAGIC[149]) + (r * MAGIC[148])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[106]) - (r * MAGIC[151])), z + ((r * MAGIC[151]) + (r * MAGIC[106])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.020408163F; - } - - private static float sc50(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[158]) - (r * MAGIC[157])), z + ((r * MAGIC[157]) + (r * MAGIC[158])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[159]) - (r * MAGIC[152])), z + ((r * MAGIC[152]) + (r * MAGIC[159])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[133]) - (r * MAGIC[134])), z + ((r * MAGIC[134]) + (r * MAGIC[133])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[136]) - (r * MAGIC[135])), z + ((r * MAGIC[135]) + (r * MAGIC[136])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[111]) - (r * MAGIC[110])), z + ((r * MAGIC[110]) + (r * MAGIC[111])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[161]) - (r * MAGIC[160])), z + ((r * MAGIC[160]) + (r * MAGIC[161])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[119]) - (r * MAGIC[120])), z + ((r * MAGIC[120]) + (r * MAGIC[119])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[141]) - (r * MAGIC[142])), z + ((r * MAGIC[142]) + (r * MAGIC[141])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[121])), z + ((r * MAGIC[121]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[164]) - (r * MAGIC[140])), z + ((r * MAGIC[140]) + (r * MAGIC[164])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[105]) - (r * MAGIC[150])), z + ((r * MAGIC[150]) + (r * MAGIC[105])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[166]) - (r * MAGIC[165])), z + ((r * MAGIC[165]) + (r * MAGIC[166])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[148]) - (r * MAGIC[149])), z + ((r * MAGIC[149]) + (r * MAGIC[148])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[106]) - (r * MAGIC[151])), z + ((r * MAGIC[151]) + (r * MAGIC[106])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.02F; - } - - private static float sc51(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[158]) - (r * MAGIC[157])), z + ((r * MAGIC[157]) + (r * MAGIC[158])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[159]) - (r * MAGIC[152])), z + ((r * MAGIC[152]) + (r * MAGIC[159])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[133]) - (r * MAGIC[134])), z + ((r * MAGIC[134]) + (r * MAGIC[133])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[136]) - (r * MAGIC[135])), z + ((r * MAGIC[135]) + (r * MAGIC[136])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[111]) - (r * MAGIC[110])), z + ((r * MAGIC[110]) + (r * MAGIC[111])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[161]) - (r * MAGIC[160])), z + ((r * MAGIC[160]) + (r * MAGIC[161])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[119]) - (r * MAGIC[120])), z + ((r * MAGIC[120]) + (r * MAGIC[119])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[141]) - (r * MAGIC[142])), z + ((r * MAGIC[142]) + (r * MAGIC[141])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[138]) - (r * MAGIC[121])), z + ((r * MAGIC[121]) + (r * MAGIC[138])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[164]) - (r * MAGIC[140])), z + ((r * MAGIC[140]) + (r * MAGIC[164])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[105]) - (r * MAGIC[150])), z + ((r * MAGIC[150]) + (r * MAGIC[105])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[166]) - (r * MAGIC[165])), z + ((r * MAGIC[165]) + (r * MAGIC[166])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[148]) - (r * MAGIC[149])), z + ((r * MAGIC[149]) + (r * MAGIC[148])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[106]) - (r * MAGIC[151])), z + ((r * MAGIC[151]) + (r * MAGIC[106])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.019607844F; - } - - private static float sc52(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45]))))) * 0.01923077F; - } - - private static float sc53(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45]))))) * 0.018867925F; - } - - private static float sc54(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45]))))) * 0.018518519F; - } - - private static float sc55(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45]))))) * 0.018181818F; - } - - private static float sc56(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45]))))) * 0.017857144F; - } - - private static float sc57(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45]))))) * 0.01754386F; - } - - private static float sc58(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45]))))) * 0.01724138F; - } - - private static float sc59(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45]))))) * 0.016949153F; - } - - private static float sc60(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45]))))) * 0.016666668F; - } - - private static float sc61(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.016393442F; - } - - private static float sc62(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.016129032F; - } - - private static float sc63(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.015873017F; - } - - private static float sc64(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.015625F; - } - - private static float sc65(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.015384615F; - } - - private static float sc66(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.015151516F; - } - - private static float sc67(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.014925373F; - } - - private static float sc68(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.014705882F; - } - - private static float sc69(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.014492754F; - } - - private static float sc70(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.014285714F; - } - - private static float sc71(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.014084507F; - } - - private static float sc72(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[109])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[109])), z + ((r * MAGIC[109]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[63])), z + ((r * MAGIC[63]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[67])), z + ((r * MAGIC[67]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[147])), z + ((r * MAGIC[147]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[62])), z + ((r * MAGIC[62]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[69])), z + ((r * MAGIC[69]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[70]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[70])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[162]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[162])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[168]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[168])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[68]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[68])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[163]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[163])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[167]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[167])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[69]) - (r * MAGIC[70])), z + ((r * MAGIC[70]) + (r * MAGIC[69])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[62]) - (r * MAGIC[162])), z + ((r * MAGIC[162]) + (r * MAGIC[62])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[147]) - (r * MAGIC[168])), z + ((r * MAGIC[168]) + (r * MAGIC[147])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[67]) - (r * MAGIC[68])), z + ((r * MAGIC[68]) + (r * MAGIC[67])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[63]) - (r * MAGIC[163])), z + ((r * MAGIC[163]) + (r * MAGIC[63])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[109]) - (r * MAGIC[167])), z + ((r * MAGIC[167]) + (r * MAGIC[109]))))) * 0.013888889F; - } - - private static float sc73(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.01369863F; - } - - private static float sc74(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.013513514F; - } - - private static float sc75(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.013333334F; - } - - private static float sc76(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.013157895F; - } - - private static float sc77(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.012987013F; - } - - private static float sc78(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.012820513F; - } - - private static float sc79(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.012658228F; - } - - private static float sc80(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.0125F; - } - - private static float sc81(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.012345679F; - } - - private static float sc82(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.0121951215F; - } - - private static float sc83(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.012048192F; - } - - private static float sc84(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.011904762F; - } - - private static float sc85(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.011764706F; - } - - private static float sc86(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.011627907F; - } - - private static float sc87(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.011494253F; - } - - private static float sc88(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.011363637F; - } - - private static float sc89(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.011235955F; - } - - private static float sc90(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169]))))) * 0.011111111F; - } - - private static float sc91(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.010989011F; - } - - private static float sc92(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.010869565F; - } - - private static float sc93(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.010752688F; - } - - private static float sc94(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.010638298F; - } - - private static float sc95(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.010526316F; - } - - private static float sc96(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.010416667F; - } - - private static float sc97(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.010309278F; - } - - private static float sc98(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.010204081F; - } - - private static float sc99(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.01010101F; - } - - private static float sc100(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.01F; - } - - private static float sc101(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.00990099F; - } - - private static float sc102(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.009803922F; - } - - private static float sc103(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.009708738F; - } - - private static float sc104(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.009615385F; - } - - private static float sc105(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.00952381F; - } - - private static float sc106(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.009433962F; - } - - private static float sc107(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.009345794F; - } - - private static float sc108(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.009259259F; - } - - private static float sc109(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.0091743115F; - } - - private static float sc110(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.009090909F; - } - - private static float sc111(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.009009009F; - } - - private static float sc112(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.008928572F; - } - - private static float sc113(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.0088495575F; - } - - private static float sc114(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.00877193F; - } - - private static float sc115(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.008695652F; - } - - private static float sc116(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.00862069F; - } - - private static float sc117(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.008547009F; - } - - private static float sc118(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.008474576F; - } - - private static float sc119(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.008403362F; - } - - private static float sc120(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[27])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[27])), z + ((r * MAGIC[27]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[57])), z + ((r * MAGIC[57]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[64])), z + ((r * MAGIC[64]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[97])), z + ((r * MAGIC[97]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[56])), z + ((r * MAGIC[56]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[146])), z + ((r * MAGIC[146]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[16])), z + ((r * MAGIC[16]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[28])), z + ((r * MAGIC[28]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[17])), z + ((r * MAGIC[17]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[99])), z + ((r * MAGIC[99]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[20])), z + ((r * MAGIC[20]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[96])), z + ((r * MAGIC[96]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[65])), z + ((r * MAGIC[65]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[58])), z + ((r * MAGIC[58]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[104])), z + ((r * MAGIC[104]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[103]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[103])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[60]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[60])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[24]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[24])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[175]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[175])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[21]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[21])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[100]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[100])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[101]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[101])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[29]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[29])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[102]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[102])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[174]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[174])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[61]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[61])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[173]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[173])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[25]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[25])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[59]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[59])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[26]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[26])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[104]) - (r * MAGIC[103])), z + ((r * MAGIC[103]) + (r * MAGIC[104])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[58]) - (r * MAGIC[60])), z + ((r * MAGIC[60]) + (r * MAGIC[58])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[65]) - (r * MAGIC[24])), z + ((r * MAGIC[24]) + (r * MAGIC[65])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[96]) - (r * MAGIC[175])), z + ((r * MAGIC[175]) + (r * MAGIC[96])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[20]) - (r * MAGIC[21])), z + ((r * MAGIC[21]) + (r * MAGIC[20])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[99]) - (r * MAGIC[100])), z + ((r * MAGIC[100]) + (r * MAGIC[99])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[17]) - (r * MAGIC[101])), z + ((r * MAGIC[101]) + (r * MAGIC[17])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[28]) - (r * MAGIC[29])), z + ((r * MAGIC[29]) + (r * MAGIC[28])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[16]) - (r * MAGIC[102])), z + ((r * MAGIC[102]) + (r * MAGIC[16])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[146]) - (r * MAGIC[174])), z + ((r * MAGIC[174]) + (r * MAGIC[146])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[56]) - (r * MAGIC[61])), z + ((r * MAGIC[61]) + (r * MAGIC[56])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[97]) - (r * MAGIC[173])), z + ((r * MAGIC[173]) + (r * MAGIC[97])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[64]) - (r * MAGIC[25])), z + ((r * MAGIC[25]) + (r * MAGIC[64])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[57]) - (r * MAGIC[59])), z + ((r * MAGIC[59]) + (r * MAGIC[57])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[27]) - (r * MAGIC[26])), z + ((r * MAGIC[26]) + (r * MAGIC[27]))))) * 0.008333334F; - } - - private static float sc121(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[82])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[82]))))) * 0.008264462F; - } - - private static float sc122(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[82])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[82]))))) * 0.008196721F; - } - - private static float sc123(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[82])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[82]))))) * 0.008130081F; - } - - private static float sc124(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[82])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[82]))))) * 0.008064516F; - } - - private static float sc125(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[82])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[82]))))) * 0.008F; - } - - private static float sc126(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[82])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[82]))))) * 0.007936508F; - } - - private static float sc127(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[82])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[82]))))) * 0.007874016F; - } - - private static float sc128(float x, float z, float r, NoiseProvider n) { - return (((float) n.noise(x + ((r * MAGIC[1]) - (r * MAGIC[0])), z + ((r * MAGIC[0]) + (r * MAGIC[1])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[82])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[7]) - (r * MAGIC[1])), z + ((r * MAGIC[1]) + (r * MAGIC[7])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[82])), z + ((r * MAGIC[82]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[169])), z + ((r * MAGIC[169]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[45])), z + ((r * MAGIC[45]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[55])), z + ((r * MAGIC[55]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[32])), z + ((r * MAGIC[32]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[18])), z + ((r * MAGIC[18]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[127])), z + ((r * MAGIC[127]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[123])), z + ((r * MAGIC[123]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[9])), z + ((r * MAGIC[9]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[84])), z + ((r * MAGIC[84]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[79])), z + ((r * MAGIC[79]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[72])), z + ((r * MAGIC[72]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[43])), z + ((r * MAGIC[43]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[129])), z + ((r * MAGIC[129]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[4])), z + ((r * MAGIC[4]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[42])), z + ((r * MAGIC[42]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[108])), z + ((r * MAGIC[108]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[39])), z + ((r * MAGIC[39]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[47])), z + ((r * MAGIC[47]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[31])), z + ((r * MAGIC[31]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[73])), z + ((r * MAGIC[73]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[81])), z + ((r * MAGIC[81]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[80])), z + ((r * MAGIC[80]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[74])), z + ((r * MAGIC[74]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[30])), z + ((r * MAGIC[30]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[95])), z + ((r * MAGIC[95]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[11])), z + ((r * MAGIC[11]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[107])), z + ((r * MAGIC[107]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[41])), z + ((r * MAGIC[41]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[15])), z + ((r * MAGIC[15]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[128])), z + ((r * MAGIC[128]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[44])), z + ((r * MAGIC[44]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[71])), z + ((r * MAGIC[71]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[78])), z + ((r * MAGIC[78]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[34])), z + ((r * MAGIC[34]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[10])), z + ((r * MAGIC[10]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[93])), z + ((r * MAGIC[93]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[126])), z + ((r * MAGIC[126]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[75])), z + ((r * MAGIC[75]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[33])), z + ((r * MAGIC[33]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[155])), z + ((r * MAGIC[155]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[98])), z + ((r * MAGIC[98]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[87])), z + ((r * MAGIC[87]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[83])), z + ((r * MAGIC[83]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[3]) - (r * MAGIC[2])), z + ((r * MAGIC[2]) + (r * MAGIC[3])))) - + ((float) n.noise(x + ((r * MAGIC[118]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[118])))) - + ((float) n.noise(x + ((r * MAGIC[88]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[88])))) - + ((float) n.noise(x + ((r * MAGIC[76]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[76])))) - + ((float) n.noise(x + ((r * MAGIC[172]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[172])))) - + ((float) n.noise(x + ((r * MAGIC[37]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[37])))) - + ((float) n.noise(x + ((r * MAGIC[49]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[49])))) - + ((float) n.noise(x + ((r * MAGIC[52]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[52])))) - + ((float) n.noise(x + ((r * MAGIC[92]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[92])))) - + ((float) n.noise(x + ((r * MAGIC[14]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[14])))) - + ((float) n.noise(x + ((r * MAGIC[35]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[35])))) - + ((float) n.noise(x + ((r * MAGIC[156]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[156])))) - + ((float) n.noise(x + ((r * MAGIC[23]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[23])))) - + ((float) n.noise(x + ((r * MAGIC[86]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[86])))) - + ((float) n.noise(x + ((r * MAGIC[90]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[90])))) - + ((float) n.noise(x + ((r * MAGIC[6]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[6])))) - + ((float) n.noise(x + ((r * MAGIC[116]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[116])))) - + ((float) n.noise(x + ((r * MAGIC[125]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[125])))) - + ((float) n.noise(x + ((r * MAGIC[12]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[12])))) - + ((float) n.noise(x + ((r * MAGIC[94]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[94])))) - + ((float) n.noise(x + ((r * MAGIC[89]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[89])))) - + ((float) n.noise(x + ((r * MAGIC[77]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[77])))) - + ((float) n.noise(x + ((r * MAGIC[51]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[51])))) - + ((float) n.noise(x + ((r * MAGIC[50]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[50])))) - + ((float) n.noise(x + ((r * MAGIC[85]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[85])))) - + ((float) n.noise(x + ((r * MAGIC[38]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[38])))) - + ((float) n.noise(x + ((r * MAGIC[48]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[48])))) - + ((float) n.noise(x + ((r * MAGIC[13]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[13])))) - + ((float) n.noise(x + ((r * MAGIC[171]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[171])))) - + ((float) n.noise(x + ((r * MAGIC[117]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[117])))) - + ((float) n.noise(x + ((r * MAGIC[5]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[5])))) - + ((float) n.noise(x + ((r * MAGIC[91]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[91])))) - + ((float) n.noise(x + ((r * MAGIC[170]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[170])))) - + ((float) n.noise(x + ((r * MAGIC[22]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[22])))) - + ((float) n.noise(x + ((r * MAGIC[124]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[124])))) - + ((float) n.noise(x + ((r * MAGIC[36]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[36])))) - + ((float) n.noise(x + ((r * MAGIC[40]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[40])))) - + ((float) n.noise(x + ((r * MAGIC[131]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[131])))) - + ((float) n.noise(x + ((r * MAGIC[53]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[53])))) - + ((float) n.noise(x + ((r * MAGIC[19]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[19])))) - + ((float) n.noise(x + ((r * MAGIC[66]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[66])))) - + ((float) n.noise(x + ((r * MAGIC[54]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[54])))) - + ((float) n.noise(x + ((r * MAGIC[46]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[46])))) - + ((float) n.noise(x + ((r * MAGIC[132]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[132])))) - + ((float) n.noise(x + ((r * MAGIC[130]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[130])))) - + ((float) n.noise(x + ((r * MAGIC[8]) - (r * MAGIC[3])), z + ((r * MAGIC[3]) + (r * MAGIC[8])))) - + ((float) n.noise(x + ((r * MAGIC[83]) - (r * MAGIC[118])), z + ((r * MAGIC[118]) + (r * MAGIC[83])))) - + ((float) n.noise(x + ((r * MAGIC[87]) - (r * MAGIC[88])), z + ((r * MAGIC[88]) + (r * MAGIC[87])))) - + ((float) n.noise(x + ((r * MAGIC[98]) - (r * MAGIC[76])), z + ((r * MAGIC[76]) + (r * MAGIC[98])))) - + ((float) n.noise(x + ((r * MAGIC[155]) - (r * MAGIC[172])), z + ((r * MAGIC[172]) + (r * MAGIC[155])))) - + ((float) n.noise(x + ((r * MAGIC[33]) - (r * MAGIC[37])), z + ((r * MAGIC[37]) + (r * MAGIC[33])))) - + ((float) n.noise(x + ((r * MAGIC[75]) - (r * MAGIC[49])), z + ((r * MAGIC[49]) + (r * MAGIC[75])))) - + ((float) n.noise(x + ((r * MAGIC[126]) - (r * MAGIC[52])), z + ((r * MAGIC[52]) + (r * MAGIC[126])))) - + ((float) n.noise(x + ((r * MAGIC[93]) - (r * MAGIC[92])), z + ((r * MAGIC[92]) + (r * MAGIC[93])))) - + ((float) n.noise(x + ((r * MAGIC[10]) - (r * MAGIC[14])), z + ((r * MAGIC[14]) + (r * MAGIC[10])))) - + ((float) n.noise(x + ((r * MAGIC[34]) - (r * MAGIC[35])), z + ((r * MAGIC[35]) + (r * MAGIC[34])))) - + ((float) n.noise(x + ((r * MAGIC[78]) - (r * MAGIC[156])), z + ((r * MAGIC[156]) + (r * MAGIC[78])))) - + ((float) n.noise(x + ((r * MAGIC[71]) - (r * MAGIC[23])), z + ((r * MAGIC[23]) + (r * MAGIC[71])))) - + ((float) n.noise(x + ((r * MAGIC[44]) - (r * MAGIC[86])), z + ((r * MAGIC[86]) + (r * MAGIC[44])))) - + ((float) n.noise(x + ((r * MAGIC[128]) - (r * MAGIC[90])), z + ((r * MAGIC[90]) + (r * MAGIC[128])))) - + ((float) n.noise(x + ((r * MAGIC[15]) - (r * MAGIC[6])), z + ((r * MAGIC[6]) + (r * MAGIC[15])))) - + ((float) n.noise(x + ((r * MAGIC[41]) - (r * MAGIC[116])), z + ((r * MAGIC[116]) + (r * MAGIC[41])))) - + ((float) n.noise(x + ((r * MAGIC[107]) - (r * MAGIC[125])), z + ((r * MAGIC[125]) + (r * MAGIC[107])))) - + ((float) n.noise(x + ((r * MAGIC[11]) - (r * MAGIC[12])), z + ((r * MAGIC[12]) + (r * MAGIC[11])))) - + ((float) n.noise(x + ((r * MAGIC[95]) - (r * MAGIC[94])), z + ((r * MAGIC[94]) + (r * MAGIC[95])))) - + ((float) n.noise(x + ((r * MAGIC[30]) - (r * MAGIC[89])), z + ((r * MAGIC[89]) + (r * MAGIC[30])))) - + ((float) n.noise(x + ((r * MAGIC[74]) - (r * MAGIC[77])), z + ((r * MAGIC[77]) + (r * MAGIC[74])))) - + ((float) n.noise(x + ((r * MAGIC[80]) - (r * MAGIC[51])), z + ((r * MAGIC[51]) + (r * MAGIC[80])))) - + ((float) n.noise(x + ((r * MAGIC[81]) - (r * MAGIC[50])), z + ((r * MAGIC[50]) + (r * MAGIC[81])))) - + ((float) n.noise(x + ((r * MAGIC[73]) - (r * MAGIC[85])), z + ((r * MAGIC[85]) + (r * MAGIC[73])))) - + ((float) n.noise(x + ((r * MAGIC[31]) - (r * MAGIC[38])), z + ((r * MAGIC[38]) + (r * MAGIC[31])))) - + ((float) n.noise(x + ((r * MAGIC[47]) - (r * MAGIC[48])), z + ((r * MAGIC[48]) + (r * MAGIC[47])))) - + ((float) n.noise(x + ((r * MAGIC[39]) - (r * MAGIC[13])), z + ((r * MAGIC[13]) + (r * MAGIC[39])))) - + ((float) n.noise(x + ((r * MAGIC[108]) - (r * MAGIC[171])), z + ((r * MAGIC[171]) + (r * MAGIC[108])))) - + ((float) n.noise(x + ((r * MAGIC[42]) - (r * MAGIC[117])), z + ((r * MAGIC[117]) + (r * MAGIC[42])))) - + ((float) n.noise(x + ((r * MAGIC[4]) - (r * MAGIC[5])), z + ((r * MAGIC[5]) + (r * MAGIC[4])))) - + ((float) n.noise(x + ((r * MAGIC[129]) - (r * MAGIC[91])), z + ((r * MAGIC[91]) + (r * MAGIC[129])))) - + ((float) n.noise(x + ((r * MAGIC[43]) - (r * MAGIC[170])), z + ((r * MAGIC[170]) + (r * MAGIC[43])))) - + ((float) n.noise(x + ((r * MAGIC[72]) - (r * MAGIC[22])), z + ((r * MAGIC[22]) + (r * MAGIC[72])))) - + ((float) n.noise(x + ((r * MAGIC[79]) - (r * MAGIC[124])), z + ((r * MAGIC[124]) + (r * MAGIC[79])))) - + ((float) n.noise(x + ((r * MAGIC[84]) - (r * MAGIC[36])), z + ((r * MAGIC[36]) + (r * MAGIC[84])))) - + ((float) n.noise(x + ((r * MAGIC[9]) - (r * MAGIC[40])), z + ((r * MAGIC[40]) + (r * MAGIC[9])))) - + ((float) n.noise(x + ((r * MAGIC[123]) - (r * MAGIC[131])), z + ((r * MAGIC[131]) + (r * MAGIC[123])))) - + ((float) n.noise(x + ((r * MAGIC[127]) - (r * MAGIC[53])), z + ((r * MAGIC[53]) + (r * MAGIC[127])))) - + ((float) n.noise(x + ((r * MAGIC[18]) - (r * MAGIC[19])), z + ((r * MAGIC[19]) + (r * MAGIC[18])))) - + ((float) n.noise(x + ((r * MAGIC[32]) - (r * MAGIC[66])), z + ((r * MAGIC[66]) + (r * MAGIC[32])))) - + ((float) n.noise(x + ((r * MAGIC[55]) - (r * MAGIC[54])), z + ((r * MAGIC[54]) + (r * MAGIC[55])))) - + ((float) n.noise(x + ((r * MAGIC[45]) - (r * MAGIC[46])), z + ((r * MAGIC[46]) + (r * MAGIC[45])))) - + ((float) n.noise(x + ((r * MAGIC[169]) - (r * MAGIC[132])), z + ((r * MAGIC[132]) + (r * MAGIC[169])))) - + ((float) n.noise(x + ((r * MAGIC[82]) - (r * MAGIC[130])), z + ((r * MAGIC[130]) + (r * MAGIC[82]))))) * 0.0078125F; - } - - -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/interpolation/IrisInterpolation.java b/core/src/main/java/com/volmit/iris/util/interpolation/IrisInterpolation.java deleted file mode 100644 index a2f1ef134..000000000 --- a/core/src/main/java/com/volmit/iris/util/interpolation/IrisInterpolation.java +++ /dev/null @@ -1,1067 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.interpolation; - -import com.google.common.util.concurrent.AtomicDouble; -import com.volmit.iris.engine.object.NoiseStyle; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.function.Consumer2; -import com.volmit.iris.util.function.NoiseProvider; -import com.volmit.iris.util.function.NoiseProvider3; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; - -import java.math.BigDecimal; -import java.util.HashMap; - -public class IrisInterpolation { - public static CNG cng = NoiseStyle.SIMPLEX.create(new RNG()); - - public static double bezier(double t) { - return t * t * (3.0d - 2.0d * t); - } - - public static double parametric(double t, double alpha) { - double sqt = Math.pow(t, alpha); - return sqt / (alpha * (sqt - Math.pow(t, alpha - 1)) + 1.0d); - } - - public static float lerpf(float a, float b, float f) { - return a + (f * (b - a)); - } - - public static double lerpBezier(double a, double b, double f) { - return a + (bezier(f) * (b - a)); - } - - public static double sinCenter(double f) { - return Math.sin(f * Math.PI); - } - - public static double lerpCenterSinBezier(double a, double b, double f) { - return lerpBezier(a, b, sinCenter(f)); - } - - public static double lerpCenterSin(double a, double b, double f) { - return lerpBezier(a, b, sinCenter(f)); - } - - public static double lerpParametric(double a, double b, double f, double v) { - return a + (parametric(f, v) * (b - a)); - } - - public static double blerp(double a, double b, double c, double d, double tx, double ty, InterpolationType type) { - if (type.equals(InterpolationType.LINEAR)) { - return blerp(a, b, c, d, tx, ty); - } - - if (type.equals(InterpolationType.BEZIER)) { - return blerpBezier(a, b, c, d, tx, ty); - } - - if (type.equals(InterpolationType.PARAMETRIC_2)) { - return blerpParametric(a, b, c, d, tx, ty, 2); - } - - if (type.equals(InterpolationType.PARAMETRIC_4)) { - return blerpParametric(a, b, c, d, tx, ty, 4); - } - - return 0; - } - - public static double blerpBezier(double a, double b, double c, double d, double tx, double ty) { - return lerpBezier(lerpBezier(a, b, tx), lerpBezier(c, d, tx), ty); - } - - public static double blerpSinCenter(double a, double b, double c, double d, double tx, double ty) { - return lerpCenterSin(lerpCenterSin(a, b, tx), lerpCenterSin(c, d, tx), ty); - } - - public static double blerpParametric(double a, double b, double c, double d, double tx, double ty, double v) { - return lerpParametric(lerpParametric(a, b, tx, v), lerpParametric(c, d, tx, v), ty, v); - } - - public static double hermiteBezier(double p0, double p1, double p2, double p3, double mu, double tension, double bias) { - return bezier(hermite(p0, p1, p2, p3, mu, tension, bias)); - } - - public static double hermiteParametric(double p0, double p1, double p2, double p3, double mu, double tension, double bias, double a) { - return parametric(hermite(p0, p1, p2, p3, mu, tension, bias), a); - } - - public static double bihermiteBezier(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy, double tension, double bias) { - //@builder - return hermiteBezier( - hermiteBezier(p00, p01, p02, p03, muy, tension, bias), - hermiteBezier(p10, p11, p12, p13, muy, tension, bias), - hermiteBezier(p20, p21, p22, p23, muy, tension, bias), - hermiteBezier(p30, p31, p32, p33, muy, tension, bias), - mux, tension, bias); - //@done - } - - public static double bihermiteParametric(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy, double tension, double bias, double a) { - //@builder - return hermiteParametric( - hermiteParametric(p00, p01, p02, p03, muy, tension, bias, a), - hermiteParametric(p10, p11, p12, p13, muy, tension, bias, a), - hermiteParametric(p20, p21, p22, p23, muy, tension, bias, a), - hermiteParametric(p30, p31, p32, p33, muy, tension, bias, a), - mux, tension, bias, a); - //@done - } - - public static double cubicBezier(double p0, double p1, double p2, double p3, double mu) { - return bezier(cubic(p0, p1, p2, p3, mu)); - } - - public static double cubicParametric(double p0, double p1, double p2, double p3, double mu, double a) { - return parametric(cubic(p0, p1, p2, p3, mu), a); - } - - public static double hermite(double p0, double p1, double p2, double p3, double mu, double tension, double bias) { - double m0, m1, mu2, mu3; - double a0, a1, a2, a3; - - mu2 = mu * mu; - mu3 = mu2 * mu; - m0 = (p1 - p0) * (1 + bias) * (1 - tension) / 2; - m0 += (p2 - p1) * (1 - bias) * (1 - tension) / 2; - m1 = (p2 - p1) * (1 + bias) * (1 - tension) / 2; - m1 += (p3 - p2) * (1 - bias) * (1 - tension) / 2; - a0 = 2 * mu3 - 3 * mu2 + 1; - a1 = mu3 - 2 * mu2 + mu; - a2 = mu3 - mu2; - a3 = -2 * mu3 + 3 * mu2; - - return (a0 * p1 + a1 * m0 + a2 * m1 + a3 * p2); - } - - public static double cubic(double p0, double p1, double p2, double p3, double mu) { - double a0, a1, a2, a3, mu2; - - mu2 = mu * mu; - a0 = p3 - p2 - p0 + p1; - a1 = p0 - p1 - a0; - a2 = p2 - p0; - a3 = p1; - - return a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3; - } - - public static double bicubic(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy) { - //@builder - return cubic( - cubic(p00, p01, p02, p03, muy), - cubic(p10, p11, p12, p13, muy), - cubic(p20, p21, p22, p23, muy), - cubic(p30, p31, p32, p33, muy), - mux); - //@done - } - - public static double bihermite(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy, double tension, double bias) { - //@builder - return hermite( - hermite(p00, p01, p02, p03, muy, tension, bias), - hermite(p10, p11, p12, p13, muy, tension, bias), - hermite(p20, p21, p22, p23, muy, tension, bias), - hermite(p30, p31, p32, p33, muy, tension, bias), - mux, tension, bias); - //@done - } - - public static double trihermite(double p000, double p001, double p002, double p003, double p010, double p011, double p012, double p013, double p020, double p021, double p022, double p023, double p030, double p031, double p032, double p033, double p100, double p101, double p102, double p103, double p110, double p111, double p112, double p113, double p120, double p121, double p122, double p123, double p130, double p131, double p132, double p133, double p200, double p201, double p202, double p203, double p210, double p211, double p212, double p213, double p220, double p221, double p222, double p223, double p230, double p231, double p232, double p233, double p300, double p301, double p302, double p303, double p310, double p311, double p312, double p313, double p320, double p321, double p322, double p323, double p330, double p331, double p332, double p333, double mux, double muy, double muz, double tension, double bias) { - //@builder - return hermite( - bihermite(p000, p001, p002, p003, - p010, p011, p012, p013, - p020, p021, p022, p023, - p030, p031, p032, p033, - mux, muy, tension, bias), - bihermite(p100, p101, p102, p103, - p110, p111, p112, p113, - p120, p121, p122, p123, - p130, p131, p132, p133, - mux, muy, tension, bias), - bihermite(p200, p201, p202, p203, - p210, p211, p212, p213, - p220, p221, p222, p223, - p230, p231, p232, p233, - mux, muy, tension, bias), - bihermite(p300, p301, p302, p303, - p310, p311, p312, p313, - p320, p321, p322, p323, - p330, p331, p332, p333, - mux, muy, tension, bias), - muz, tension, bias); - //@done - } - - public static double tricubic(double p000, - double p001, - double p002, - double p003, - double p010, - double p011, - double p012, double p013, double p020, double p021, double p022, double p023, double p030, double p031, double p032, double p033, double p100, double p101, double p102, double p103, double p110, double p111, double p112, double p113, double p120, double p121, double p122, double p123, double p130, double p131, double p132, double p133, double p200, double p201, double p202, double p203, double p210, double p211, double p212, double p213, double p220, double p221, double p222, double p223, double p230, double p231, double p232, double p233, double p300, double p301, double p302, double p303, double p310, double p311, double p312, double p313, double p320, double p321, double p322, double p323, double p330, double p331, double p332, double p333, double mux, double muy, double muz) { - //@builder - return cubic( - bicubic(p000, p001, p002, p003, - p010, p011, p012, p013, - p020, p021, p022, p023, - p030, p031, p032, p033, - mux, muy), - bicubic(p100, p101, p102, p103, - p110, p111, p112, p113, - p120, p121, p122, p123, - p130, p131, p132, p133, - mux, muy), - bicubic(p200, p201, p202, p203, - p210, p211, p212, p213, - p220, p221, p222, p223, - p230, p231, p232, p233, - mux, muy), - bicubic(p300, p301, p302, p303, - p310, p311, p312, p313, - p320, p321, p322, p323, - p330, p331, p332, p333, - mux, muy), - muz); - //@done - } - - public static double lerp(double a, double b, double f) { - return a + (f * (b - a)); - } - - public static double blerp(double a, double b, double c, double d, double tx, double ty) { - return lerp(lerp(a, b, tx), lerp(c, d, tx), ty); - } - - public static double trilerp(double v1, double v2, double v3, double v4, double v5, double v6, double v7, double v8, double x, double y, double z) { - return lerp(blerp(v1, v2, v3, v4, x, y), blerp(v5, v6, v7, v8, x, y), z); - } - - public static double bicubicBezier(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy) { - //@builder - return cubicBezier( - cubicBezier(p00, p01, p02, p03, muy), - cubicBezier(p10, p11, p12, p13, muy), - cubicBezier(p20, p21, p22, p23, muy), - cubicBezier(p30, p31, p32, p33, muy), - mux); - //@done - } - - public static double bicubicParametric(double p00, double p01, double p02, double p03, double p10, double p11, double p12, double p13, double p20, double p21, double p22, double p23, double p30, double p31, double p32, double p33, double mux, double muy, double a) { - //@builder - return cubicParametric( - cubicParametric(p00, p01, p02, p03, muy, a), - cubicParametric(p10, p11, p12, p13, muy, a), - cubicParametric(p20, p21, p22, p23, muy, a), - cubicParametric(p30, p31, p32, p33, muy, a), - mux, a); - //@done - } - - public static int getRadiusFactor(int coord, double radius) { - if (radius == 2) { - return coord >> 1; - } - if (radius == 4) { - return coord >> 2; - } - if (radius == 8) { - return coord >> 3; - } - if (radius == 16) { - return coord >> 4; - } - if (radius == 32) { - return coord >> 5; - } - if (radius == 64) { - return coord >> 6; - } - if (radius == 128) { - return coord >> 7; - } - if (radius == 256) { - return coord >> 8; - } - if (radius == 512) { - return coord >> 9; - } - if (radius == 1024) { - return coord >> 10; - } - return (int) Math.floor(coord / radius); - } - - public static double getBilinearNoise(int x, int z, double rad, NoiseProvider n) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return blerp( - n.noise(x1, z1), - n.noise(x2, z1), - n.noise(x1, z2), - n.noise(x2, z2), - px, pz); - //@done - } - - public static void test(String m, Consumer2 f) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - - for (int i = 0; i < 8192; i++) { - f.accept(i, -i * 234); - } - - p.end(); - - System.out.println(m + ": " + Form.duration(p.getMilliseconds(), 8)); - } - - public static void printOptimizedSrc(boolean arrays) { - System.out.println(generateOptimizedStarcast(3, arrays)); - System.out.println(generateOptimizedStarcast(5, arrays)); - System.out.println(generateOptimizedStarcast(6, arrays)); - System.out.println(generateOptimizedStarcast(7, arrays)); - System.out.println(generateOptimizedStarcast(9, arrays)); - System.out.println(generateOptimizedStarcast(12, arrays)); - System.out.println(generateOptimizedStarcast(24, arrays)); - System.out.println(generateOptimizedStarcast(32, arrays)); - System.out.println(generateOptimizedStarcast(48, arrays)); - System.out.println(generateOptimizedStarcast(64, arrays)); - } - - public static String generateOptimizedStarcast(double checks, boolean array) { - double m = (360 / checks); - int ig = 0; - int igx = 0; - StringBuilder fb = new StringBuilder(); - StringBuilder sb = new StringBuilder(); - - if (array) { - fb.append("private static final double[] F").append((int) checks).append("A = {"); - } - - sb.append("private static double sc").append((int) checks).append("(int x, int z, double r, NoiseProvider n) {\n return ("); - for (int i = 0; i < 360; i += m) { - double sin = Math.sin(Math.toRadians(i)); - double cos = Math.cos(Math.toRadians(i)); - String cof = new BigDecimal(cos).toPlainString(); - String sif = new BigDecimal(sin).toPlainString(); - String cc = array ? "F" + (int) checks + "A[" + (igx++) + "]" : "F" + (int) checks + "C" + ig; - String ss = array ? "F" + (int) checks + "A[" + (igx++) + "]" : "F" + (int) checks + "S" + ig; - - if (array) { - fb.append(ig > 0 ? (ig % 6 == 0 ? ",\n" : ",") : "").append(cof).append(",").append(sif); - } else { - fb.append("private static final double ").append(cc).append(" = ").append(cof).append(";\n"); - fb.append("private static final double ").append(ss).append(" = ").append(sif).append(";\n"); - } - - sb.append(ig > 0 ? "\n +" : "").append("n.noise(x + ((r * ").append(cc).append(") - (r * ").append(ss).append(")), z + ((r * ").append(ss).append(") + (r * ").append(cc).append(")))"); - ig++; - } - - if (array) { - fb.append("};"); - } - - sb.append(")/").append(checks).append("D;\n}"); - return fb + "\n" + sb; - } - - public static double getStarcast3D(int x, int y, int z, double rad, double checks, NoiseProvider3 n) { - return (Starcast.starcast(x, z, rad, checks, (xx, zz) -> n.noise(xx, y, zz)) - + Starcast.starcast(x, y, rad, checks, (xx, yy) -> n.noise(xx, yy, z)) - + Starcast.starcast(y, z, rad, checks, (yy, zz) -> n.noise(x, yy, zz))) / 3D; - } - - public static double getBilinearBezierNoise(int x, int z, double rad, NoiseProvider n) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return blerpBezier( - n.noise(x1, z1), - n.noise(x2, z1), - n.noise(x1, z2), - n.noise(x2, z2), - px, pz); - //@done - } - - public static double getBilinearParametricNoise(int x, int z, double rad, NoiseProvider n, double a) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return blerpParametric( - n.noise(x1, z1), - n.noise(x2, z1), - n.noise(x1, z2), - n.noise(x2, z2), - px, pz, a); - //@done - } - - public static double getTrilinear(int x, int y, int z, double rad, NoiseProvider3 n) { - return getTrilinear(x, y, z, rad, rad, rad, n); - } - - public static double getTrilinear(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { - int fx = getRadiusFactor(x, radx); - int fy = getRadiusFactor(y, rady); - int fz = getRadiusFactor(z, radz); - int x1 = (int) Math.round(fx * radx); - int y1 = (int) Math.round(fy * rady); - int z1 = (int) Math.round(fz * radz); - int x2 = (int) Math.round((fx + 1) * radx); - int y2 = (int) Math.round((fy + 1) * rady); - int z2 = (int) Math.round((fz + 1) * radz); - double px = rangeScale(0, 1, x1, x2, x); - double py = rangeScale(0, 1, y1, y2, y); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return trilerp( - n.noise(x1, y1, z1), - n.noise(x2, y1, z1), - n.noise(x1, y2, z1), - n.noise(x2, y2, z1), - n.noise(x1, y1, z2), - n.noise(x2, y1, z2), - n.noise(x1, y2, z2), - n.noise(x2, y2, z2), - px, py, pz); - //@done - } - - public static double getTricubic(int x, int y, int z, double rad, NoiseProvider3 n) { - return getTricubic(x, y, z, rad, rad, rad, n); - } - - public static double getTricubic(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { - int fx = getRadiusFactor(x, radx); - int fy = getRadiusFactor(y, rady); - int fz = getRadiusFactor(z, radz); - int x0 = (int) Math.round((fx - 1) * radx); - int y0 = (int) Math.round((fy - 1) * rady); - int z0 = (int) Math.round((fz - 1) * radz); - int x1 = (int) Math.round(fx * radx); - int y1 = (int) Math.round(fy * rady); - int z1 = (int) Math.round(fz * radz); - int x2 = (int) Math.round((fx + 1) * radx); - int y2 = (int) Math.round((fy + 1) * rady); - int z2 = (int) Math.round((fz + 1) * radz); - int x3 = (int) Math.round((fx + 2) * radx); - int y3 = (int) Math.round((fy + 2) * rady); - int z3 = (int) Math.round((fz + 2) * radz); - double px = rangeScale(0, 1, x1, x2, x); - double py = rangeScale(0, 1, y1, y2, y); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - //!!!!!!!!!!!!!!!!!! 2 1 3 - - return tricubic( - n.noise(x0, y0, z0), - n.noise(x0, y1, z0), - n.noise(x0, y2, z0), - n.noise(x0, y3, z0), - n.noise(x1, y0, z0), - n.noise(x1, y1, z0), - n.noise(x1, y2, z0), - n.noise(x1, y3, z0), - n.noise(x2, y0, z0), - n.noise(x2, y1, z0), - n.noise(x2, y2, z0), - n.noise(x2, y3, z0), - n.noise(x3, y0, z0), - n.noise(x3, y1, z0), - n.noise(x3, y2, z0), - n.noise(x3, y3, z0), - n.noise(x0, y0, z1), - n.noise(x0, y1, z1), - n.noise(x0, y2, z1), - n.noise(x0, y3, z1), - n.noise(x1, y0, z1), - n.noise(x1, y1, z1), - n.noise(x1, y2, z1), - n.noise(x1, y3, z1), - n.noise(x2, y0, z1), - n.noise(x2, y1, z1), - n.noise(x2, y2, z1), - n.noise(x2, y3, z1), - n.noise(x3, y0, z1), - n.noise(x3, y1, z1), - n.noise(x3, y2, z1), - n.noise(x3, y3, z1), - n.noise(x0, y0, z2), - n.noise(x0, y1, z2), - n.noise(x0, y2, z2), - n.noise(x0, y3, z2), - n.noise(x1, y0, z2), - n.noise(x1, y1, z2), - n.noise(x1, y2, z2), - n.noise(x1, y3, z2), - n.noise(x2, y0, z2), - n.noise(x2, y1, z2), - n.noise(x2, y2, z2), - n.noise(x2, y3, z2), - n.noise(x3, y0, z2), - n.noise(x3, y1, z2), - n.noise(x3, y2, z2), - n.noise(x3, y3, z2), - n.noise(x0, y0, z3), - n.noise(x0, y1, z3), - n.noise(x0, y2, z3), - n.noise(x0, y3, z3), - n.noise(x1, y0, z3), - n.noise(x1, y1, z3), - n.noise(x1, y2, z3), - n.noise(x1, y3, z3), - n.noise(x2, y0, z3), - n.noise(x2, y1, z3), - n.noise(x2, y2, z3), - n.noise(x2, y3, z3), - n.noise(x3, y0, z3), - n.noise(x3, y1, z3), - n.noise(x3, y2, z3), - n.noise(x3, y3, z3), - px, py, pz); - //@done - } - - public static double getTrihermite(int x, int y, int z, double rad, NoiseProvider3 n, double tension, double bias) { - return getTrihermite(x, y, z, rad, rad, rad, n, tension, bias); - } - - public static double getTrihermite(int x, int y, int z, double rad, NoiseProvider3 n) { - return getTrihermite(x, y, z, rad, rad, rad, n, 0D, 0D); - } - - public static double getTrihermite(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { - return getTrihermite(x, y, z, radx, rady, radz, n, 0D, 0D); - } - - public static double getTrihermite(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n, double tension, double bias) { - int fx = getRadiusFactor(x, radx); - int fy = getRadiusFactor(y, rady); - int fz = getRadiusFactor(z, radz); - int x0 = (int) Math.round((fx - 1) * radx); - int y0 = (int) Math.round((fy - 1) * rady); - int z0 = (int) Math.round((fz - 1) * radz); - int x1 = (int) Math.round(fx * radx); - int y1 = (int) Math.round(fy * rady); - int z1 = (int) Math.round(fz * radz); - int x2 = (int) Math.round((fx + 1) * radx); - int y2 = (int) Math.round((fy + 1) * rady); - int z2 = (int) Math.round((fz + 1) * radz); - int x3 = (int) Math.round((fx + 2) * radx); - int y3 = (int) Math.round((fy + 2) * rady); - int z3 = (int) Math.round((fz + 2) * radz); - double px = rangeScale(0, 1, x1, x2, x); - double py = rangeScale(0, 1, y1, y2, y); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - //!!!!!!!!!!!!!!!!!! 2 1 3 - - return trihermite( - n.noise(x0, y0, z0), - n.noise(x0, y1, z0), - n.noise(x0, y2, z0), - n.noise(x0, y3, z0), - n.noise(x1, y0, z0), - n.noise(x1, y1, z0), - n.noise(x1, y2, z0), - n.noise(x1, y3, z0), - n.noise(x2, y0, z0), - n.noise(x2, y1, z0), - n.noise(x2, y2, z0), - n.noise(x2, y3, z0), - n.noise(x3, y0, z0), - n.noise(x3, y1, z0), - n.noise(x3, y2, z0), - n.noise(x3, y3, z0), - n.noise(x0, y0, z1), - n.noise(x0, y1, z1), - n.noise(x0, y2, z1), - n.noise(x0, y3, z1), - n.noise(x1, y0, z1), - n.noise(x1, y1, z1), - n.noise(x1, y2, z1), - n.noise(x1, y3, z1), - n.noise(x2, y0, z1), - n.noise(x2, y1, z1), - n.noise(x2, y2, z1), - n.noise(x2, y3, z1), - n.noise(x3, y0, z1), - n.noise(x3, y1, z1), - n.noise(x3, y2, z1), - n.noise(x3, y3, z1), - n.noise(x0, y0, z2), - n.noise(x0, y1, z2), - n.noise(x0, y2, z2), - n.noise(x0, y3, z2), - n.noise(x1, y0, z2), - n.noise(x1, y1, z2), - n.noise(x1, y2, z2), - n.noise(x1, y3, z2), - n.noise(x2, y0, z2), - n.noise(x2, y1, z2), - n.noise(x2, y2, z2), - n.noise(x2, y3, z2), - n.noise(x3, y0, z2), - n.noise(x3, y1, z2), - n.noise(x3, y2, z2), - n.noise(x3, y3, z2), - n.noise(x0, y0, z3), - n.noise(x0, y1, z3), - n.noise(x0, y2, z3), - n.noise(x0, y3, z3), - n.noise(x1, y0, z3), - n.noise(x1, y1, z3), - n.noise(x1, y2, z3), - n.noise(x1, y3, z3), - n.noise(x2, y0, z3), - n.noise(x2, y1, z3), - n.noise(x2, y2, z3), - n.noise(x2, y3, z3), - n.noise(x3, y0, z3), - n.noise(x3, y1, z3), - n.noise(x3, y2, z3), - n.noise(x3, y3, z3), - px, py, pz, tension, bias); - //@done - } - - public static double getBilinearCenterSineNoise(int x, int z, double rad, NoiseProvider n) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return blerpSinCenter( - n.noise(x1, z1), - n.noise(x2, z1), - n.noise(x1, z2), - n.noise(x2, z2), - px, pz); - //@done - } - - public static double getBicubicNoise(int x, int z, double rad, NoiseProvider n) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x0 = (int) Math.round((fx - 1) * rad); - int z0 = (int) Math.round((fz - 1) * rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - int x3 = (int) Math.round((fx + 2) * rad); - int z3 = (int) Math.round((fz + 2) * rad); - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return bicubic( - n.noise(x0, z0), - n.noise(x0, z1), - n.noise(x0, z2), - n.noise(x0, z3), - n.noise(x1, z0), - n.noise(x1, z1), - n.noise(x1, z2), - n.noise(x1, z3), - n.noise(x2, z0), - n.noise(x2, z1), - n.noise(x2, z2), - n.noise(x2, z3), - n.noise(x3, z0), - n.noise(x3, z1), - n.noise(x3, z2), - n.noise(x3, z3), - px, pz); - //@done - } - - public static double getBicubicBezierNoise(int x, int z, double rad, NoiseProvider n) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x0 = (int) Math.round((fx - 1) * rad); - int z0 = (int) Math.round((fz - 1) * rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - int x3 = (int) Math.round((fx + 2) * rad); - int z3 = (int) Math.round((fz + 2) * rad); - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return bicubicBezier( - n.noise(x0, z0), - n.noise(x0, z1), - n.noise(x0, z2), - n.noise(x0, z3), - n.noise(x1, z0), - n.noise(x1, z1), - n.noise(x1, z2), - n.noise(x1, z3), - n.noise(x2, z0), - n.noise(x2, z1), - n.noise(x2, z2), - n.noise(x2, z3), - n.noise(x3, z0), - n.noise(x3, z1), - n.noise(x3, z2), - n.noise(x3, z3), - px, pz); - //@done - } - - public static double getBicubicParametricNoise(int x, int z, double rad, NoiseProvider n, double a) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x0 = (int) Math.round((fx - 1) * rad); - int z0 = (int) Math.round((fz - 1) * rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - int x3 = (int) Math.round((fx + 2) * rad); - int z3 = (int) Math.round((fz + 2) * rad); - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return bicubicParametric( - n.noise(x0, z0), - n.noise(x0, z1), - n.noise(x0, z2), - n.noise(x0, z3), - n.noise(x1, z0), - n.noise(x1, z1), - n.noise(x1, z2), - n.noise(x1, z3), - n.noise(x2, z0), - n.noise(x2, z1), - n.noise(x2, z2), - n.noise(x2, z3), - n.noise(x3, z0), - n.noise(x3, z1), - n.noise(x3, z2), - n.noise(x3, z3), - px, pz, a); - //@done - } - - public static double getHermiteNoise(int x, int z, double rad, NoiseProvider n) { - return getHermiteNoise(x, z, rad, n, 0.5, 0); - } - - public static double getHermiteBezierNoise(int x, int z, double rad, NoiseProvider n) { - return getHermiteBezierNoise(x, z, rad, n, 0.5, 0); - } - - public static double getHermiteParametricNoise(int x, int z, double rad, NoiseProvider n, double a) { - return getHermiteParametricNoise(x, z, rad, n, 0.5, 0, a); - } - - public static double getHermiteNoise(int x, int z, double rad, NoiseProvider n, double t, double b) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x0 = (int) Math.round((fx - 1) * rad); - int z0 = (int) Math.round((fz - 1) * rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - int x3 = (int) Math.round((fx + 2) * rad); - int z3 = (int) Math.round((fz + 2) * rad); - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return bihermite( - n.noise(x0, z0), - n.noise(x0, z1), - n.noise(x0, z2), - n.noise(x0, z3), - n.noise(x1, z0), - n.noise(x1, z1), - n.noise(x1, z2), - n.noise(x1, z3), - n.noise(x2, z0), - n.noise(x2, z1), - n.noise(x2, z2), - n.noise(x2, z3), - n.noise(x3, z0), - n.noise(x3, z1), - n.noise(x3, z2), - n.noise(x3, z3), - px, pz, t, b); - //@done - } - - public static double getHermiteBezierNoise(int x, int z, double rad, NoiseProvider n, double t, double b) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x0 = (int) Math.round((fx - 1) * rad); - int z0 = (int) Math.round((fz - 1) * rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - int x3 = (int) Math.round((fx + 2) * rad); - int z3 = (int) Math.round((fz + 2) * rad); - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return bihermiteBezier( - n.noise(x0, z0), - n.noise(x0, z1), - n.noise(x0, z2), - n.noise(x0, z3), - n.noise(x1, z0), - n.noise(x1, z1), - n.noise(x1, z2), - n.noise(x1, z3), - n.noise(x2, z0), - n.noise(x2, z1), - n.noise(x2, z2), - n.noise(x2, z3), - n.noise(x3, z0), - n.noise(x3, z1), - n.noise(x3, z2), - n.noise(x3, z3), - px, pz, t, b); - //@done - } - - public static double getHermiteParametricNoise(int x, int z, double rad, NoiseProvider n, double t, double b, double a) { - int fx = getRadiusFactor(x, rad); - int fz = getRadiusFactor(z, rad); - int x0 = (int) Math.round((fx - 1) * rad); - int z0 = (int) Math.round((fz - 1) * rad); - int x1 = (int) Math.round(fx * rad); - int z1 = (int) Math.round(fz * rad); - int x2 = (int) Math.round((fx + 1) * rad); - int z2 = (int) Math.round((fz + 1) * rad); - int x3 = (int) Math.round((fx + 2) * rad); - int z3 = (int) Math.round((fz + 2) * rad); - double px = rangeScale(0, 1, x1, x2, x); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return bihermiteParametric( - n.noise(x0, z0), - n.noise(x0, z1), - n.noise(x0, z2), - n.noise(x0, z3), - n.noise(x1, z0), - n.noise(x1, z1), - n.noise(x1, z2), - n.noise(x1, z3), - n.noise(x2, z0), - n.noise(x2, z1), - n.noise(x2, z2), - n.noise(x2, z3), - n.noise(x3, z0), - n.noise(x3, z1), - n.noise(x3, z2), - n.noise(x3, z3), - px, pz, t, b, a); - //@done - } - - public static double getRealRadius(InterpolationMethod method, double h) { - AtomicDouble rad = new AtomicDouble(h); - AtomicDouble accessX = new AtomicDouble(); - AtomicDouble accessZ = new AtomicDouble(); - NoiseProvider np = (x1, z1) -> { - double d = Math.max(Math.abs(x1), Math.abs(z1)); - if (d > rad.get()) { - rad.set(d); - } - return 0; - }; - getNoise(method, 0, 0, h, np); - return rad.get(); - } - - public static double getNoise3D(InterpolationMethod3D method, int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { - return switch (method) { - case TRILINEAR -> getTrilinear(x, y, z, radx, rady, radz, n); - case TRICUBIC -> getTricubic(x, y, z, radx, rady, radz, n); - case TRIHERMITE -> getTrihermite(x, y, z, radx, rady, radz, n); - case TRISTARCAST_3 -> getStarcast3D(x, y, z, radx, 3D, n); - case TRISTARCAST_6 -> getStarcast3D(x, y, z, radx, 6D, n); - case TRISTARCAST_9 -> getStarcast3D(x, y, z, radx, 9D, n); - case TRISTARCAST_12 -> getStarcast3D(x, y, z, radx, 12D, n); - case TRILINEAR_TRISTARCAST_3 -> - getStarcast3D(x, y, z, radx, 3D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); - case TRILINEAR_TRISTARCAST_6 -> - getStarcast3D(x, y, z, radx, 6D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); - case TRILINEAR_TRISTARCAST_9 -> - getStarcast3D(x, y, z, radx, 9D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); - case TRILINEAR_TRISTARCAST_12 -> - getStarcast3D(x, y, z, radx, 12D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); - case NONE -> n.noise(x, y, z); - }; - } - - public static Hunk getNoise3D(InterpolationMethod3D method, int xo, int yo, int zo, int w, int h, int d, double rad, NoiseProvider3 n) { - return getNoise3D(method, xo, yo, zo, w, h, d, rad, rad, rad, n); - } - - /** - * Get the interpolated 3D noise within a given cuboid size with offsets - * - * @param method the interpolation method to use - * @param xo the x offset for noise - * @param yo the y offset for noise - * @param zo the z offset for noise - * @param w the width of the result - * @param h the height of the result - * @param d the depth of the result - * @param radX the interpolation radius for the x axis - * @param radY the interpolation radius for the y axis - * @param radZ the interpolation radius for the z axis - * @param n the noise provider - * @return the resulting hunk of noise - */ - public static Hunk getNoise3D(InterpolationMethod3D method, int xo, int yo, int zo, int w, int h, int d, double radX, double radY, double radZ, NoiseProvider3 n) { - Hunk hunk = Hunk.newAtomicDoubleHunk(w, h, d); - HashMap cache = new HashMap<>(); - int i, j, k; - - for (i = 0; i < w; i++) { - int fi = i; - for (j = 0; j < h; j++) { - int fj = j; - for (k = 0; k < d; k++) { - int fk = k; - hunk.set(i, j, k, cache.computeIfAbsent((k * w * h) + (j * w) + i, (p) - -> getNoise3D(method, fi + xo, fj + yo, fk + zo, - radX, radY, radZ, n))); - } - } - } - - return hunk; - } - - public static double getNoise3D(InterpolationMethod3D method, int x, int y, int z, double rad, NoiseProvider3 n) { - return getNoise3D(method, x, y, z, rad, rad, rad, n); - } - - public static double getNoise(InterpolationMethod method, int x, int z, double h, NoiseProvider noise) { - HashMap cache = new HashMap<>(64); - NoiseProvider n = (x1, z1) -> cache.computeIfAbsent(new NoiseKey(x1 - x, z1 - z), k -> noise.noise(x1, z1)); - - if (method.equals(InterpolationMethod.BILINEAR)) { - return getBilinearNoise(x, z, h, n); - } else if (method.equals(InterpolationMethod.STARCAST_3)) { - return Starcast.starcast(x, z, h, 3D, n); - } else if (method.equals(InterpolationMethod.STARCAST_6)) { - return Starcast.starcast(x, z, h, 6D, n); - } else if (method.equals(InterpolationMethod.STARCAST_9)) { - return Starcast.starcast(x, z, h, 9D, n); - } else if (method.equals(InterpolationMethod.STARCAST_12)) { - return Starcast.starcast(x, z, h, 12D, n); - } else if (method.equals(InterpolationMethod.BILINEAR_STARCAST_3)) { - return Starcast.starcast(x, z, h, 3D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); - } else if (method.equals(InterpolationMethod.BILINEAR_STARCAST_6)) { - return Starcast.starcast(x, z, h, 6D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); - } else if (method.equals(InterpolationMethod.BILINEAR_STARCAST_9)) { - return Starcast.starcast(x, z, h, 9D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); - } else if (method.equals(InterpolationMethod.BILINEAR_STARCAST_12)) { - return Starcast.starcast(x, z, h, 12D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); - } else if (method.equals(InterpolationMethod.HERMITE_STARCAST_3)) { - return Starcast.starcast(x, z, h, 3D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); - } else if (method.equals(InterpolationMethod.HERMITE_STARCAST_6)) { - return Starcast.starcast(x, z, h, 6D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); - } else if (method.equals(InterpolationMethod.HERMITE_STARCAST_9)) { - return Starcast.starcast(x, z, h, 9D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); - } else if (method.equals(InterpolationMethod.HERMITE_STARCAST_12)) { - return Starcast.starcast(x, z, h, 12D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); - } else if (method.equals(InterpolationMethod.BILINEAR_BEZIER)) { - return getBilinearBezierNoise(x, z, h, n); - } else if (method.equals(InterpolationMethod.BILINEAR_PARAMETRIC_2)) { - return getBilinearParametricNoise(x, z, h, n, 2); - } else if (method.equals(InterpolationMethod.BILINEAR_PARAMETRIC_4)) { - return getBilinearParametricNoise(x, z, h, n, 4); - } else if (method.equals(InterpolationMethod.BILINEAR_PARAMETRIC_1_5)) { - return getBilinearParametricNoise(x, z, h, n, 1.5); - } else if (method.equals(InterpolationMethod.BICUBIC)) { - return getBilinearNoise(x, z, h, n); - } else if (method.equals(InterpolationMethod.HERMITE)) { - return getHermiteNoise(x, z, h, n); - } else if (method.equals(InterpolationMethod.HERMITE_TENSE)) { - return getHermiteNoise(x, z, h, n, 0.8D, 0D); - } else if (method.equals(InterpolationMethod.CATMULL_ROM_SPLINE)) { - return getHermiteNoise(x, z, h, n, 1D, 0D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE)) { - return getHermiteNoise(x, z, h, n, 0D, 0D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE_HALF_NEGATIVE_BIAS)) { - return getHermiteNoise(x, z, h, n, 0D, -0.5D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE_HALF_POSITIVE_BIAS)) { - return getHermiteNoise(x, z, h, n, 0D, 0.5D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE_FULL_NEGATIVE_BIAS)) { - return getHermiteNoise(x, z, h, n, 0D, -1D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE_FULL_POSITIVE_BIAS)) { - return getHermiteNoise(x, z, h, n, 0D, 1D); - } - - return n.noise(x, z); - } - - public static double rangeScale(double amin, double amax, double bmin, double bmax, double b) { - return amin + ((amax - amin) * ((b - bmin) / (bmax - bmin))); - } - - public record NoiseKey(double x, double z) { - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/interpolation/Starcast.java b/core/src/main/java/com/volmit/iris/util/interpolation/Starcast.java deleted file mode 100644 index e3061d939..000000000 --- a/core/src/main/java/com/volmit/iris/util/interpolation/Starcast.java +++ /dev/null @@ -1,734 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.interpolation; - -import com.volmit.iris.util.function.NoiseProvider; - -public class Starcast { - private static final double F3C0 = 1; - private static final double F3S0 = 0; - private static final double F3C1 = -0.4999999999999997779553950749686919152736663818359375; - private static final double F3S1 = 0.86602540378443870761060452423407696187496185302734375; - private static final double F3C2 = -0.500000000000000444089209850062616169452667236328125; - private static final double F3S2 = -0.86602540378443837454369713668711483478546142578125; - private static final double F5C0 = 1; - private static final double F5S0 = 0; - private static final double F5C1 = 0.30901699437494745126286943559534847736358642578125; - private static final double F5S1 = 0.95105651629515353118193843329208903014659881591796875; - private static final double F5C2 = -0.80901699437494734024056697307969443500041961669921875; - private static final double F5S2 = 0.58778525229247324812575925534474663436412811279296875; - private static final double F5C3 = -0.80901699437494756228517189811100251972675323486328125; - private static final double F5S3 = -0.58778525229247302608115433031343854963779449462890625; - private static final double F5C4 = 0.3090169943749472292182645105640403926372528076171875; - private static final double F5S4 = -0.951056516295153642204240895807743072509765625; - private static final double F6C0 = 1; - private static final double F6S0 = 0; - private static final double F6C1 = 0.50000000000000011102230246251565404236316680908203125; - private static final double F6S1 = 0.8660254037844385965883020617184229195117950439453125; - private static final double F6C2 = -0.4999999999999997779553950749686919152736663818359375; - private static final double F6S2 = 0.86602540378443870761060452423407696187496185302734375; - private static final double F6C3 = -1; - private static final double F6S3 = 0.000000000000000122464679914735320717376402945839660462569212467758006379625612680683843791484832763671875; - private static final double F6C4 = -0.500000000000000444089209850062616169452667236328125; - private static final double F6S4 = -0.86602540378443837454369713668711483478546142578125; - private static final double F6C5 = 0.50000000000000011102230246251565404236316680908203125; - private static final double F6S5 = -0.8660254037844385965883020617184229195117950439453125; - private static final double F7C0 = 1; - private static final double F7S0 = 0; - private static final double F7C1 = 0.6293203910498375019955119569203816354274749755859375; - private static final double F7S1 = 0.77714596145697090179282895405776798725128173828125; - private static final double F7C2 = -0.207911690817759342575499204031075350940227508544921875; - private static final double F7S2 = 0.9781476007338056888329447247087955474853515625; - private static final double F7C3 = -0.891006524188367787786546614370308816432952880859375; - private static final double F7S3 = 0.45399049973954685999188995992881245911121368408203125; - private static final double F7C4 = -0.91354545764260086659902526662335731089115142822265625; - private static final double F7S4 = -0.406736643075800208269043878317461349070072174072265625; - private static final double F7C5 = -0.25881904510252062845410137015278451144695281982421875; - private static final double F7S5 = -0.96592582628906831221371476203785277903079986572265625; - private static final double F7C6 = 0.587785252292472915058851867797784507274627685546875; - private static final double F7S6 = -0.80901699437494756228517189811100251972675323486328125; - private static final double F7C7 = 0.99862953475457383323288240717374719679355621337890625; - private static final double F7S7 = -0.052335956242944368932423770957029773853719234466552734375; - private static final double F9C0 = 1; - private static final double F9S0 = 0; - private static final double F9C1 = 0.76604444311897801345168090847437269985675811767578125; - private static final double F9S1 = 0.642787609686539251896419955301098525524139404296875; - private static final double F9C2 = 0.17364817766693041445336120887077413499355316162109375; - private static final double F9S2 = 0.98480775301220802031565426659653894603252410888671875; - private static final double F9C3 = -0.4999999999999997779553950749686919152736663818359375; - private static final double F9S3 = 0.86602540378443870761060452423407696187496185302734375; - private static final double F9C4 = -0.93969262078590831688273965482949279248714447021484375; - private static final double F9S4 = 0.342020143325668879441536773811094462871551513671875; - private static final double F9C5 = -0.939692620785908427905042117345146834850311279296875; - private static final double F9S5 = -0.3420201433256686573969318487797863781452178955078125; - private static final double F9C6 = -0.500000000000000444089209850062616169452667236328125; - private static final double F9S6 = -0.86602540378443837454369713668711483478546142578125; - private static final double F9C7 = 0.17364817766692997036415135880815796554088592529296875; - private static final double F9S7 = -0.98480775301220813133795672911219298839569091796875; - private static final double F9C8 = 0.76604444311897779140707598344306461513042449951171875; - private static final double F9S8 = -0.64278760968653958496332734284806065261363983154296875; - private static final double F12C0 = 1; - private static final double F12S0 = 0; - private static final double F12C1 = 0.86602540378443870761060452423407696187496185302734375; - private static final double F12S1 = 0.499999999999999944488848768742172978818416595458984375; - private static final double F12C2 = 0.50000000000000011102230246251565404236316680908203125; - private static final double F12S2 = 0.8660254037844385965883020617184229195117950439453125; - private static final double F12C3 = 0.0000000000000000612323399573676603586882014729198302312846062338790031898128063403419218957424163818359375; - private static final double F12S3 = 1; - private static final double F12C4 = -0.4999999999999997779553950749686919152736663818359375; - private static final double F12S4 = 0.86602540378443870761060452423407696187496185302734375; - private static final double F12C5 = -0.86602540378443870761060452423407696187496185302734375; - private static final double F12S5 = 0.499999999999999944488848768742172978818416595458984375; - private static final double F12C6 = -1; - private static final double F12S6 = 0.000000000000000122464679914735320717376402945839660462569212467758006379625612680683843791484832763671875; - private static final double F12C7 = -0.8660254037844385965883020617184229195117950439453125; - private static final double F12S7 = -0.50000000000000011102230246251565404236316680908203125; - private static final double F12C8 = -0.500000000000000444089209850062616169452667236328125; - private static final double F12S8 = -0.86602540378443837454369713668711483478546142578125; - private static final double F12C9 = -0.00000000000000018369701987210296875011296034045003113559498615810217092558787044254131615161895751953125; - private static final double F12S9 = -1; - private static final double F12C10 = 0.50000000000000011102230246251565404236316680908203125; - private static final double F12S10 = -0.8660254037844385965883020617184229195117950439453125; - private static final double F12C11 = 0.86602540378443837454369713668711483478546142578125; - private static final double F12S11 = -0.500000000000000444089209850062616169452667236328125; - private static final double F24C0 = 1; - private static final double F24S0 = 0; - private static final double F24C1 = 0.96592582628906831221371476203785277903079986572265625; - private static final double F24S1 = 0.25881904510252073947640383266843855381011962890625; - private static final double F24C2 = 0.86602540378443870761060452423407696187496185302734375; - private static final double F24S2 = 0.499999999999999944488848768742172978818416595458984375; - private static final double F24C3 = 0.70710678118654757273731092936941422522068023681640625; - private static final double F24S3 = 0.707106781186547461715008466853760182857513427734375; - private static final double F24C4 = 0.50000000000000011102230246251565404236316680908203125; - private static final double F24S4 = 0.8660254037844385965883020617184229195117950439453125; - private static final double F24C5 = 0.25881904510252073947640383266843855381011962890625; - private static final double F24S5 = 0.96592582628906831221371476203785277903079986572265625; - private static final double F24C6 = 0.0000000000000000612323399573676603586882014729198302312846062338790031898128063403419218957424163818359375; - private static final double F24S6 = 1; - private static final double F24C7 = -0.25881904510252085049870629518409259617328643798828125; - private static final double F24S7 = 0.96592582628906831221371476203785277903079986572265625; - private static final double F24C8 = -0.4999999999999997779553950749686919152736663818359375; - private static final double F24S8 = 0.86602540378443870761060452423407696187496185302734375; - private static final double F24C9 = -0.707106781186547461715008466853760182857513427734375; - private static final double F24S9 = 0.70710678118654757273731092936941422522068023681640625; - private static final double F24C10 = -0.86602540378443870761060452423407696187496185302734375; - private static final double F24S10 = 0.499999999999999944488848768742172978818416595458984375; - private static final double F24C11 = -0.965925826289068201191412299522198736667633056640625; - private static final double F24S11 = 0.258819045102521017032159988957573659718036651611328125; - private static final double F24C12 = -1; - private static final double F24S12 = 0.000000000000000122464679914735320717376402945839660462569212467758006379625612680683843791484832763671875; - private static final double F24C13 = -0.96592582628906831221371476203785277903079986572265625; - private static final double F24S13 = -0.258819045102520794987555063926265574991703033447265625; - private static final double F24C14 = -0.8660254037844385965883020617184229195117950439453125; - private static final double F24S14 = -0.50000000000000011102230246251565404236316680908203125; - private static final double F24C15 = -0.7071067811865476837596133918850682675838470458984375; - private static final double F24S15 = -0.707106781186547461715008466853760182857513427734375; - private static final double F24C16 = -0.500000000000000444089209850062616169452667236328125; - private static final double F24S16 = -0.86602540378443837454369713668711483478546142578125; - private static final double F24C17 = -0.25881904510252062845410137015278451144695281982421875; - private static final double F24S17 = -0.96592582628906831221371476203785277903079986572265625; - private static final double F24C18 = -0.00000000000000018369701987210296875011296034045003113559498615810217092558787044254131615161895751953125; - private static final double F24S18 = -1; - private static final double F24C19 = 0.258819045102520295387193982605822384357452392578125; - private static final double F24S19 = -0.9659258262890684232360172245535068213939666748046875; - private static final double F24C20 = 0.50000000000000011102230246251565404236316680908203125; - private static final double F24S20 = -0.8660254037844385965883020617184229195117950439453125; - private static final double F24C21 = 0.70710678118654735069270600433810614049434661865234375; - private static final double F24S21 = -0.7071067811865476837596133918850682675838470458984375; - private static final double F24C22 = 0.86602540378443837454369713668711483478546142578125; - private static final double F24S22 = -0.500000000000000444089209850062616169452667236328125; - private static final double F24C23 = 0.96592582628906831221371476203785277903079986572265625; - private static final double F24S23 = -0.258819045102520683965252601410611532628536224365234375; - private static final double F32C0 = 1; - private static final double F32S0 = 0; - private static final double F32C1 = 0.9816271834476639757127713892259635031223297119140625; - private static final double F32S1 = 0.1908089953765448043565555735767702572047710418701171875; - private static final double F32C2 = 0.92718385456678742428948680753819644451141357421875; - private static final double F32S2 = 0.374606593415912014766178117497474886476993560791015625; - private static final double F32C3 = 0.838670567945424050293468098971061408519744873046875; - private static final double F32S3 = 0.54463903501502708426329490976058878004550933837890625; - private static final double F32C4 = 0.71933980033865119185776393351261503994464874267578125; - private static final double F32S4 = 0.6946583704589972541043607634492218494415283203125; - private static final double F32C5 = 0.57357643635104615942310601894860155880451202392578125; - private static final double F32S5 = 0.81915204428899179855960710483486764132976531982421875; - private static final double F32C6 = 0.406736643075800208269043878317461349070072174072265625; - private static final double F32S6 = 0.91354545764260086659902526662335731089115142822265625; - private static final double F32C7 = 0.224951054343864920159745679484331049025058746337890625; - private static final double F32S7 = 0.97437006478523524588553073044749908149242401123046875; - private static final double F32C8 = 0.034899496702501080214187112460422213189303874969482421875; - private static final double F32S8 = 0.99939082701909576211818375668372027575969696044921875; - private static final double F32C9 = -0.1564344650402308134484741231062798760831356048583984375; - private static final double F32S9 = 0.987688340595137770350220307591371238231658935546875; - private static final double F32C10 = -0.342020143325668712908083080037613399326801300048828125; - private static final double F32S10 = 0.939692620785908427905042117345146834850311279296875; - private static final double F32C11 = -0.515038074910054266553061097511090338230133056640625; - private static final double F32S11 = 0.8571673007021123336102164103067480027675628662109375; - private static final double F32C12 = -0.66913060635885823757007528911344707012176513671875; - private static final double F32S12 = 0.7431448254773942441175904605188407003879547119140625; - private static final double F32C13 = -0.79863551004729294024997443557367660105228424072265625; - private static final double F32S13 = 0.60181502315204815634075430352822877466678619384765625; - private static final double F32C14 = -0.8987940462991670376169395240140147507190704345703125; - private static final double F32S14 = 0.43837114678907729281576166613376699388027191162109375; - private static final double F32C15 = -0.965925826289068201191412299522198736667633056640625; - private static final double F32S15 = 0.258819045102521017032159988957573659718036651611328125; - private static final double F32C16 = -0.9975640502598241976528470331686548888683319091796875; - private static final double F32S16 = 0.06975647374412552448319502218510024249553680419921875; - private static final double F32C17 = -0.992546151641322094150154953240416944026947021484375; - private static final double F32S17 = -0.12186934340514730956694933183825924061238765716552734375; - private static final double F32C18 = -0.951056516295153642204240895807743072509765625; - private static final double F32S18 = -0.309016994374947284729415741821867413818836212158203125; - private static final double F32C19 = -0.87461970713939585220231265338952653110027313232421875; - private static final double F32S19 = -0.484809620246336947513299264755914919078350067138671875; - private static final double F32C20 = -0.76604444311897801345168090847437269985675811767578125; - private static final double F32S20 = -0.642787609686539251896419955301098525524139404296875; - private static final double F32C21 = -0.62932039104983783506241934446734376251697540283203125; - private static final double F32S21 = -0.77714596145697056872592156651080586016178131103515625; - private static final double F32C22 = -0.469471562785890750291173389996401965618133544921875; - private static final double F32S22 = -0.88294759285892698841280434862710535526275634765625; - private static final double F32C23 = -0.29237170472273710242205879694665782153606414794921875; - private static final double F32S23 = -0.9563047559630353244841671767062507569789886474609375; - private static final double F32C24 = -0.104528463267653359825004599770181812345981597900390625; - private static final double F32S24 = -0.99452189536827340088365190240438096225261688232421875; - private static final double F32C25 = 0.0871557427476578883140945208651828579604625701904296875; - private static final double F32S25 = -0.9961946980917455451987052583717741072177886962890625; - private static final double F32C26 = 0.275637355816999385371701691838097758591175079345703125; - private static final double F32S26 = -0.96126169593831878312784056106465868651866912841796875; - private static final double F32C27 = 0.45399049973954663794728503489750437438488006591796875; - private static final double F32S27 = -0.89100652418836789880884907688596285879611968994140625; - private static final double F32C28 = 0.61566147532565851374641852089553140103816986083984375; - private static final double F32S28 = -0.7880107536067217921527117141522467136383056640625; - private static final double F32C29 = 0.7547095802227719030241814834880642592906951904296875; - private static final double F32S29 = -0.65605902899050738685815531425760127604007720947265625; - private static final double F32C30 = 0.86602540378443837454369713668711483478546142578125; - private static final double F32S30 = -0.500000000000000444089209850062616169452667236328125; - private static final double F32C31 = 0.94551857559931684615861513520940206944942474365234375; - private static final double F32S31 = -0.3255681544571566998769185374840162694454193115234375; - private static final double F32C32 = 0.99026806874157025095684048210387118160724639892578125; - private static final double F32S32 = -0.1391731009600658819369556340461713261902332305908203125; - private static final double F48C0 = 1; - private static final double F48S0 = 0; - private static final double F48C1 = 0.99254615164132198312785249072476290166378021240234375; - private static final double F48S1 = 0.12186934340514747610040302561174030415713787078857421875; - private static final double F48C2 = 0.97029572627599647294260876151383854448795318603515625; - private static final double F48S2 = 0.2419218955996677300479547056966111995279788970947265625; - private static final double F48C3 = 0.93358042649720174299687869279296137392520904541015625; - private static final double F48S3 = 0.3583679495453002683547083506709896028041839599609375; - private static final double F48C4 = 0.88294759285892698841280434862710535526275634765625; - private static final double F48S4 = 0.469471562785890805802324621254228986799716949462890625; - private static final double F48C5 = 0.81915204428899179855960710483486764132976531982421875; - private static final double F48S5 = 0.57357643635104604840080355643294751644134521484375; - private static final double F48C6 = 0.7431448254773942441175904605188407003879547119140625; - private static final double F48S6 = 0.66913060635885823757007528911344707012176513671875; - private static final double F48C7 = 0.656059028990507275835852851741947233676910400390625; - private static final double F48S7 = 0.75470958022277201404648394600371830165386199951171875; - private static final double F48C8 = 0.5591929034707467938147829045192338526248931884765625; - private static final double F48S8 = 0.82903757255504173517834942686022259294986724853515625; - private static final double F48C9 = 0.453990499739546804480738728670985437929630279541015625; - private static final double F48S9 = 0.891006524188367787786546614370308816432952880859375; - private static final double F48C10 = 0.342020143325668823930385542553267441689968109130859375; - private static final double F48S10 = 0.93969262078590831688273965482949279248714447021484375; - private static final double F48C11 = 0.224951054343864920159745679484331049025058746337890625; - private static final double F48S11 = 0.97437006478523524588553073044749908149242401123046875; - private static final double F48C12 = 0.10452846326765345696951925447137909941375255584716796875; - private static final double F48S12 = 0.9945218953682732898613494398887269198894500732421875; - private static final double F48C13 = -0.01745240643728347695873281963940826244652271270751953125; - private static final double F48S13 = 0.9998476951563912695775115935248322784900665283203125; - private static final double F48C14 = -0.139173100960065354581018937096814624965190887451171875; - private static final double F48S14 = 0.9902680687415703619791429446195252239704132080078125; - private static final double F48C15 = -0.25881904510252085049870629518409259617328643798828125; - private static final double F48S15 = 0.96592582628906831221371476203785277903079986572265625; - private static final double F48C16 = -0.37460659341591207027732934875530190765857696533203125; - private static final double F48S16 = 0.92718385456678742428948680753819644451141357421875; - private static final double F48C17 = -0.4848096202463370030244504960137419402599334716796875; - private static final double F48S17 = 0.87461970713939585220231265338952653110027313232421875; - private static final double F48C18 = -0.58778525229247302608115433031343854963779449462890625; - private static final double F48S18 = 0.80901699437494745126286943559534847736358642578125; - private static final double F48C19 = -0.6819983600624983655080768585321493446826934814453125; - private static final double F48S19 = 0.73135370161917057085787519099540077149868011474609375; - private static final double F48C20 = -0.76604444311897790242937844595871865749359130859375; - private static final double F48S20 = 0.6427876096865394739410248803324066102504730224609375; - private static final double F48C21 = -0.83867056794542393927116563645540736615657806396484375; - private static final double F48S21 = 0.54463903501502730630789983479189686477184295654296875; - private static final double F48C22 = -0.8987940462991670376169395240140147507190704345703125; - private static final double F48S22 = 0.43837114678907729281576166613376699388027191162109375; - private static final double F48C23 = -0.94551857559931684615861513520940206944942474365234375; - private static final double F48S23 = 0.32556815445715658885461607496836222708225250244140625; - private static final double F48C24 = -0.9781476007338056888329447247087955474853515625; - private static final double F48S24 = 0.2079116908177593148199235884021618403494358062744140625; - private static final double F48C25 = -0.9961946980917455451987052583717741072177886962890625; - private static final double F48S25 = 0.087155742747658193625426292783231474459171295166015625; - private static final double F48C26 = -0.99939082701909576211818375668372027575969696044921875; - private static final double F48S26 = -0.034899496702500899802945610872484394349157810211181640625; - private static final double F48C27 = -0.987688340595137770350220307591371238231658935546875; - private static final double F48S27 = -0.156434465040230730181747276219539344310760498046875; - private static final double F48C28 = -0.9612616959383188941501430235803127288818359375; - private static final double F48S28 = -0.27563735581699899679364307303330861032009124755859375; - private static final double F48C29 = -0.92050485345244037471701403774204663932323455810546875; - private static final double F48S29 = -0.390731128489273549231342030907399021089076995849609375; - private static final double F48C30 = -0.8660254037844385965883020617184229195117950439453125; - private static final double F48S30 = -0.50000000000000011102230246251565404236316680908203125; - private static final double F48C31 = -0.798635510047292829227671973058022558689117431640625; - private static final double F48S31 = -0.60181502315204837838535922855953685939311981201171875; - private static final double F48C32 = -0.71933980033865108083546147099696099758148193359375; - private static final double F48S32 = -0.69465837045899736512666322596487589180469512939453125; - private static final double F48C33 = -0.62932039104983783506241934446734376251697540283203125; - private static final double F48S33 = -0.77714596145697056872592156651080586016178131103515625; - private static final double F48C34 = -0.52991926423320501182701036668731831014156341552734375; - private static final double F48S34 = -0.84804809615642595677087456351728178560733795166015625; - private static final double F48C35 = -0.42261826174069916373099431439186446368694305419921875; - private static final double F48S35 = -0.90630778703665004769618462887592613697052001953125; - private static final double F48C36 = -0.30901699437494756228517189811100251972675323486328125; - private static final double F48S36 = -0.95105651629515353118193843329208903014659881591796875; - private static final double F48C37 = -0.1908089953765446100675262641743756830692291259765625; - private static final double F48S37 = -0.9816271834476639757127713892259635031223297119140625; - private static final double F48C38 = -0.069756473744125579994346253442927263677120208740234375; - private static final double F48S38 = -0.9975640502598241976528470331686548888683319091796875; - private static final double F48C39 = 0.05233595624294394565989563261609873734414577484130859375; - private static final double F48S39 = -0.99862953475457383323288240717374719679355621337890625; - private static final double F48C40 = 0.17364817766692997036415135880815796554088592529296875; - private static final double F48S40 = -0.98480775301220813133795672911219298839569091796875; - private static final double F48C41 = 0.292371704722736713844000178141868673264980316162109375; - private static final double F48S41 = -0.95630475596303543550646963922190479934215545654296875; - private static final double F48C42 = 0.406736643075799764179834028254845179617404937744140625; - private static final double F48S42 = -0.91354545764260108864363019165466539561748504638671875; - private static final double F48C43 = 0.51503807491005415553075863499543629586696624755859375; - private static final double F48S43 = -0.8571673007021123336102164103067480027675628662109375; - private static final double F48C44 = 0.61566147532565851374641852089553140103816986083984375; - private static final double F48S44 = -0.7880107536067217921527117141522467136383056640625; - private static final double F48C45 = 0.70710678118654735069270600433810614049434661865234375; - private static final double F48S45 = -0.7071067811865476837596133918850682675838470458984375; - private static final double F48C46 = 0.7880107536067220141973166391835547983646392822265625; - private static final double F48S46 = -0.61566147532565818067951113334856927394866943359375; - private static final double F48C47 = 0.857167300702112111565611485275439918041229248046875; - private static final double F48S47 = -0.5150380749100544885976660225423984229564666748046875; - private static final double F48C48 = 0.9135454576426009776213277291390113532543182373046875; - private static final double F48S48 = -0.40673664307580015275789264705963432788848876953125; - private static final double F48C49 = 0.9563047559630353244841671767062507569789886474609375; - private static final double F48S49 = -0.292371704722737157933210028204484842717647552490234375; - private static final double F48C50 = 0.98480775301220802031565426659653894603252410888671875; - private static final double F48S50 = -0.1736481776669303866977855932418606244027614593505859375; - private static final double F48C51 = 0.99862953475457383323288240717374719679355621337890625; - private static final double F48S51 = -0.052335956242944368932423770957029773853719234466552734375; - private static final double F64C0 = 1; - private static final double F64S0 = 0; - private static final double F64C1 = 0.9961946980917455451987052583717741072177886962890625; - private static final double F64S1 = 0.0871557427476581658698506771543179638683795928955078125; - private static final double F64C2 = 0.98480775301220802031565426659653894603252410888671875; - private static final double F64S2 = 0.1736481776669303311866343619840336032211780548095703125; - private static final double F64C3 = 0.96592582628906831221371476203785277903079986572265625; - private static final double F64S3 = 0.25881904510252073947640383266843855381011962890625; - private static final double F64C4 = 0.939692620785908427905042117345146834850311279296875; - private static final double F64S4 = 0.342020143325668712908083080037613399326801300048828125; - private static final double F64C5 = 0.90630778703664993667388216636027209460735321044921875; - private static final double F64S5 = 0.422618261740699441286750470680999569594860076904296875; - private static final double F64C6 = 0.86602540378443870761060452423407696187496185302734375; - private static final double F64S6 = 0.499999999999999944488848768742172978818416595458984375; - private static final double F64C7 = 0.81915204428899179855960710483486764132976531982421875; - private static final double F64S7 = 0.57357643635104604840080355643294751644134521484375; - private static final double F64C8 = 0.76604444311897801345168090847437269985675811767578125; - private static final double F64S8 = 0.642787609686539251896419955301098525524139404296875; - private static final double F64C9 = 0.70710678118654757273731092936941422522068023681640625; - private static final double F64S9 = 0.707106781186547461715008466853760182857513427734375; - private static final double F64C10 = 0.64278760968653936291872241781675256788730621337890625; - private static final double F64S10 = 0.76604444311897801345168090847437269985675811767578125; - private static final double F64C11 = 0.57357643635104615942310601894860155880451202392578125; - private static final double F64S11 = 0.81915204428899179855960710483486764132976531982421875; - private static final double F64C12 = 0.50000000000000011102230246251565404236316680908203125; - private static final double F64S12 = 0.8660254037844385965883020617184229195117950439453125; - private static final double F64C13 = 0.422618261740699441286750470680999569594860076904296875; - private static final double F64S13 = 0.90630778703664993667388216636027209460735321044921875; - private static final double F64C14 = 0.342020143325668823930385542553267441689968109130859375; - private static final double F64S14 = 0.93969262078590831688273965482949279248714447021484375; - private static final double F64C15 = 0.25881904510252073947640383266843855381011962890625; - private static final double F64S15 = 0.96592582628906831221371476203785277903079986572265625; - private static final double F64C16 = 0.17364817766693041445336120887077413499355316162109375; - private static final double F64S16 = 0.98480775301220802031565426659653894603252410888671875; - private static final double F64C17 = 0.087155742747658138114275061525404453277587890625; - private static final double F64S17 = 0.9961946980917455451987052583717741072177886962890625; - private static final double F64C18 = 0.0000000000000000612323399573676603586882014729198302312846062338790031898128063403419218957424163818359375; - private static final double F64S18 = 1; - private static final double F64C19 = -0.08715574274765823525878971622660174034535884857177734375; - private static final double F64S19 = 0.9961946980917455451987052583717741072177886962890625; - private static final double F64C20 = -0.1736481776669303034310587463551200926303863525390625; - private static final double F64S20 = 0.98480775301220802031565426659653894603252410888671875; - private static final double F64C21 = -0.25881904510252085049870629518409259617328643798828125; - private static final double F64S21 = 0.96592582628906831221371476203785277903079986572265625; - private static final double F64C22 = -0.342020143325668712908083080037613399326801300048828125; - private static final double F64S22 = 0.939692620785908427905042117345146834850311279296875; - private static final double F64C23 = -0.422618261740699330264448008165345527231693267822265625; - private static final double F64S23 = 0.90630778703665004769618462887592613697052001953125; - private static final double F64C24 = -0.4999999999999997779553950749686919152736663818359375; - private static final double F64S24 = 0.86602540378443870761060452423407696187496185302734375; - private static final double F64C25 = -0.57357643635104615942310601894860155880451202392578125; - private static final double F64S25 = 0.8191520442889916875373046423192135989665985107421875; - private static final double F64C26 = -0.64278760968653936291872241781675256788730621337890625; - private static final double F64S26 = 0.76604444311897801345168090847437269985675811767578125; - private static final double F64C27 = -0.707106781186547461715008466853760182857513427734375; - private static final double F64S27 = 0.70710678118654757273731092936941422522068023681640625; - private static final double F64C28 = -0.76604444311897790242937844595871865749359130859375; - private static final double F64S28 = 0.6427876096865394739410248803324066102504730224609375; - private static final double F64C29 = -0.81915204428899190958190956735052168369293212890625; - private static final double F64S29 = 0.57357643635104593737850109391729347407817840576171875; - private static final double F64C30 = -0.86602540378443870761060452423407696187496185302734375; - private static final double F64S30 = 0.499999999999999944488848768742172978818416595458984375; - private static final double F64C31 = -0.90630778703664993667388216636027209460735321044921875; - private static final double F64S31 = 0.4226182617406994967979017019388265907764434814453125; - private static final double F64C32 = -0.93969262078590831688273965482949279248714447021484375; - private static final double F64S32 = 0.342020143325668879441536773811094462871551513671875; - private static final double F64C33 = -0.965925826289068201191412299522198736667633056640625; - private static final double F64S33 = 0.258819045102521017032159988957573659718036651611328125; - private static final double F64C34 = -0.98480775301220802031565426659653894603252410888671875; - private static final double F64S34 = 0.1736481776669302756754831307262065820395946502685546875; - private static final double F64C35 = -0.9961946980917455451987052583717741072177886962890625; - private static final double F64S35 = 0.087155742747658193625426292783231474459171295166015625; - private static final double F64C36 = -1; - private static final double F64S36 = 0.000000000000000122464679914735320717376402945839660462569212467758006379625612680683843791484832763671875; - private static final double F64C37 = -0.9961946980917455451987052583717741072177886962890625; - private static final double F64S37 = -0.0871557427476579438252457521230098791420459747314453125; - private static final double F64C38 = -0.98480775301220802031565426659653894603252410888671875; - private static final double F64S38 = -0.173648177666930469964512440128601156175136566162109375; - private static final double F64C39 = -0.96592582628906831221371476203785277903079986572265625; - private static final double F64S39 = -0.258819045102520794987555063926265574991703033447265625; - private static final double F64C40 = -0.939692620785908427905042117345146834850311279296875; - private static final double F64S40 = -0.3420201433256686573969318487797863781452178955078125; - private static final double F64C41 = -0.90630778703665004769618462887592613697052001953125; - private static final double F64S41 = -0.42261826174069927475329677690751850605010986328125; - private static final double F64C42 = -0.8660254037844385965883020617184229195117950439453125; - private static final double F64S42 = -0.50000000000000011102230246251565404236316680908203125; - private static final double F64C43 = -0.81915204428899179855960710483486764132976531982421875; - private static final double F64S43 = -0.57357643635104615942310601894860155880451202392578125; - private static final double F64C44 = -0.76604444311897801345168090847437269985675811767578125; - private static final double F64S44 = -0.642787609686539251896419955301098525524139404296875; - private static final double F64C45 = -0.7071067811865476837596133918850682675838470458984375; - private static final double F64S45 = -0.707106781186547461715008466853760182857513427734375; - private static final double F64C46 = -0.6427876096865394739410248803324066102504730224609375; - private static final double F64S46 = -0.76604444311897790242937844595871865749359130859375; - private static final double F64C47 = -0.57357643635104638146771094397990964353084564208984375; - private static final double F64S47 = -0.81915204428899157651500217980355955660343170166015625; - private static final double F64C48 = -0.500000000000000444089209850062616169452667236328125; - private static final double F64S48 = -0.86602540378443837454369713668711483478546142578125; - private static final double F64C49 = -0.42261826174069916373099431439186446368694305419921875; - private static final double F64S49 = -0.90630778703665004769618462887592613697052001953125; - private static final double F64C50 = -0.34202014332566854637462938626413233578205108642578125; - private static final double F64S50 = -0.939692620785908427905042117345146834850311279296875; - private static final double F64C51 = -0.25881904510252062845410137015278451144695281982421875; - private static final double F64S51 = -0.96592582628906831221371476203785277903079986572265625; - private static final double F64C52 = -0.1736481776669303311866343619840336032211780548095703125; - private static final double F64S52 = -0.98480775301220802031565426659653894603252410888671875; - private static final double F64C53 = -0.08715574274765824913657752404105849564075469970703125; - private static final double F64S53 = -0.9961946980917455451987052583717741072177886962890625; - private static final double F64C54 = -0.00000000000000018369701987210296875011296034045003113559498615810217092558787044254131615161895751953125; - private static final double F64S54 = -1; - private static final double F64C55 = 0.0871557427476578883140945208651828579604625701904296875; - private static final double F64S55 = -0.9961946980917455451987052583717741072177886962890625; - private static final double F64C56 = 0.17364817766692997036415135880815796554088592529296875; - private static final double F64S56 = -0.98480775301220813133795672911219298839569091796875; - private static final double F64C57 = 0.258819045102520295387193982605822384357452392578125; - private static final double F64S57 = -0.9659258262890684232360172245535068213939666748046875; - private static final double F64C58 = 0.34202014332566899046383923632674850523471832275390625; - private static final double F64S58 = -0.93969262078590831688273965482949279248714447021484375; - private static final double F64C59 = 0.42261826174069960782020416445448063313961029052734375; - private static final double F64S59 = -0.90630778703664993667388216636027209460735321044921875; - private static final double F64C60 = 0.50000000000000011102230246251565404236316680908203125; - private static final double F64S60 = -0.8660254037844385965883020617184229195117950439453125; - private static final double F64C61 = 0.57357643635104604840080355643294751644134521484375; - private static final double F64S61 = -0.81915204428899179855960710483486764132976531982421875; - private static final double F64C62 = 0.642787609686539251896419955301098525524139404296875; - private static final double F64S62 = -0.7660444431189781244739833709900267422199249267578125; - private static final double F64C63 = 0.70710678118654735069270600433810614049434661865234375; - private static final double F64S63 = -0.7071067811865476837596133918850682675838470458984375; - private static final double F64C64 = 0.76604444311897779140707598344306461513042449951171875; - private static final double F64S64 = -0.64278760968653958496332734284806065261363983154296875; - private static final double F64C65 = 0.81915204428899157651500217980355955660343170166015625; - private static final double F64S65 = -0.573576436351046492490013406495563685894012451171875; - private static final double F64C66 = 0.86602540378443837454369713668711483478546142578125; - private static final double F64S66 = -0.500000000000000444089209850062616169452667236328125; - private static final double F64C67 = 0.90630778703665004769618462887592613697052001953125; - private static final double F64S67 = -0.422618261740699219242145545649691484868526458740234375; - private static final double F64C68 = 0.939692620785908427905042117345146834850311279296875; - private static final double F64S68 = -0.342020143325668601885780617521959356963634490966796875; - private static final double F64C69 = 0.96592582628906831221371476203785277903079986572265625; - private static final double F64S69 = -0.258819045102520683965252601410611532628536224365234375; - private static final double F64C70 = 0.98480775301220802031565426659653894603252410888671875; - private static final double F64S70 = -0.1736481776669303866977855932418606244027614593505859375; - private static final double F64C71 = 0.9961946980917455451987052583717741072177886962890625; - private static final double F64S71 = -0.08715574274765831852551656311334227211773395538330078125; - - public static double starcast(int x, int z, double r, double checks, boolean optimized, NoiseProvider n) { - return CompiledStarcast.getStarcast((float) x, (float) z, (float) r, (float) checks, n); - } - - public static double starcast(int x, int z, double r, double checks, NoiseProvider n) { - return starcast(x, z, r, checks, true, n); - } - - private static double sc3(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F3C0) - (r * F3S0)), z + ((r * F3S0) + (r * F3C0))) - + n.noise(x + ((r * F3C1) - (r * F3S1)), z + ((r * F3S1) + (r * F3C1))) - + n.noise(x + ((r * F3C2) - (r * F3S2)), z + ((r * F3S2) + (r * F3C2)))) / 3.0D; - } - - private static double sc5(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F5C0) - (r * F5S0)), z + ((r * F5S0) + (r * F5C0))) - + n.noise(x + ((r * F5C1) - (r * F5S1)), z + ((r * F5S1) + (r * F5C1))) - + n.noise(x + ((r * F5C2) - (r * F5S2)), z + ((r * F5S2) + (r * F5C2))) - + n.noise(x + ((r * F5C3) - (r * F5S3)), z + ((r * F5S3) + (r * F5C3))) - + n.noise(x + ((r * F5C4) - (r * F5S4)), z + ((r * F5S4) + (r * F5C4)))) / 5.0D; - } - - private static double sc6(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F6C0) - (r * F6S0)), z + ((r * F6S0) + (r * F6C0))) - + n.noise(x + ((r * F6C1) - (r * F6S1)), z + ((r * F6S1) + (r * F6C1))) - + n.noise(x + ((r * F6C2) - (r * F6S2)), z + ((r * F6S2) + (r * F6C2))) - + n.noise(x + ((r * F6C3) - (r * F6S3)), z + ((r * F6S3) + (r * F6C3))) - + n.noise(x + ((r * F6C4) - (r * F6S4)), z + ((r * F6S4) + (r * F6C4))) - + n.noise(x + ((r * F6C5) - (r * F6S5)), z + ((r * F6S5) + (r * F6C5)))) / 6.0D; - } - - private static double sc7(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F7C0) - (r * F7S0)), z + ((r * F7S0) + (r * F7C0))) - + n.noise(x + ((r * F7C1) - (r * F7S1)), z + ((r * F7S1) + (r * F7C1))) - + n.noise(x + ((r * F7C2) - (r * F7S2)), z + ((r * F7S2) + (r * F7C2))) - + n.noise(x + ((r * F7C3) - (r * F7S3)), z + ((r * F7S3) + (r * F7C3))) - + n.noise(x + ((r * F7C4) - (r * F7S4)), z + ((r * F7S4) + (r * F7C4))) - + n.noise(x + ((r * F7C5) - (r * F7S5)), z + ((r * F7S5) + (r * F7C5))) - + n.noise(x + ((r * F7C6) - (r * F7S6)), z + ((r * F7S6) + (r * F7C6))) - + n.noise(x + ((r * F7C7) - (r * F7S7)), z + ((r * F7S7) + (r * F7C7)))) / 7.0D; - } - - private static double sc9(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F9C0) - (r * F9S0)), z + ((r * F9S0) + (r * F9C0))) - + n.noise(x + ((r * F9C1) - (r * F9S1)), z + ((r * F9S1) + (r * F9C1))) - + n.noise(x + ((r * F9C2) - (r * F9S2)), z + ((r * F9S2) + (r * F9C2))) - + n.noise(x + ((r * F9C3) - (r * F9S3)), z + ((r * F9S3) + (r * F9C3))) - + n.noise(x + ((r * F9C4) - (r * F9S4)), z + ((r * F9S4) + (r * F9C4))) - + n.noise(x + ((r * F9C5) - (r * F9S5)), z + ((r * F9S5) + (r * F9C5))) - + n.noise(x + ((r * F9C6) - (r * F9S6)), z + ((r * F9S6) + (r * F9C6))) - + n.noise(x + ((r * F9C7) - (r * F9S7)), z + ((r * F9S7) + (r * F9C7))) - + n.noise(x + ((r * F9C8) - (r * F9S8)), z + ((r * F9S8) + (r * F9C8)))) / 9.0D; - } - - private static double sc12(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F12C0) - (r * F12S0)), z + ((r * F12S0) + (r * F12C0))) - + n.noise(x + ((r * F12C1) - (r * F12S1)), z + ((r * F12S1) + (r * F12C1))) - + n.noise(x + ((r * F12C2) - (r * F12S2)), z + ((r * F12S2) + (r * F12C2))) - + n.noise(x + ((r * F12C3) - (r * F12S3)), z + ((r * F12S3) + (r * F12C3))) - + n.noise(x + ((r * F12C4) - (r * F12S4)), z + ((r * F12S4) + (r * F12C4))) - + n.noise(x + ((r * F12C5) - (r * F12S5)), z + ((r * F12S5) + (r * F12C5))) - + n.noise(x + ((r * F12C6) - (r * F12S6)), z + ((r * F12S6) + (r * F12C6))) - + n.noise(x + ((r * F12C7) - (r * F12S7)), z + ((r * F12S7) + (r * F12C7))) - + n.noise(x + ((r * F12C8) - (r * F12S8)), z + ((r * F12S8) + (r * F12C8))) - + n.noise(x + ((r * F12C9) - (r * F12S9)), z + ((r * F12S9) + (r * F12C9))) - + n.noise(x + ((r * F12C10) - (r * F12S10)), z + ((r * F12S10) + (r * F12C10))) - + n.noise(x + ((r * F12C11) - (r * F12S11)), z + ((r * F12S11) + (r * F12C11)))) / 12.0D; - } - - private static double sc24(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F24C0) - (r * F24S0)), z + ((r * F24S0) + (r * F24C0))) - + n.noise(x + ((r * F24C1) - (r * F24S1)), z + ((r * F24S1) + (r * F24C1))) - + n.noise(x + ((r * F24C2) - (r * F24S2)), z + ((r * F24S2) + (r * F24C2))) - + n.noise(x + ((r * F24C3) - (r * F24S3)), z + ((r * F24S3) + (r * F24C3))) - + n.noise(x + ((r * F24C4) - (r * F24S4)), z + ((r * F24S4) + (r * F24C4))) - + n.noise(x + ((r * F24C5) - (r * F24S5)), z + ((r * F24S5) + (r * F24C5))) - + n.noise(x + ((r * F24C6) - (r * F24S6)), z + ((r * F24S6) + (r * F24C6))) - + n.noise(x + ((r * F24C7) - (r * F24S7)), z + ((r * F24S7) + (r * F24C7))) - + n.noise(x + ((r * F24C8) - (r * F24S8)), z + ((r * F24S8) + (r * F24C8))) - + n.noise(x + ((r * F24C9) - (r * F24S9)), z + ((r * F24S9) + (r * F24C9))) - + n.noise(x + ((r * F24C10) - (r * F24S10)), z + ((r * F24S10) + (r * F24C10))) - + n.noise(x + ((r * F24C11) - (r * F24S11)), z + ((r * F24S11) + (r * F24C11))) - + n.noise(x + ((r * F24C12) - (r * F24S12)), z + ((r * F24S12) + (r * F24C12))) - + n.noise(x + ((r * F24C13) - (r * F24S13)), z + ((r * F24S13) + (r * F24C13))) - + n.noise(x + ((r * F24C14) - (r * F24S14)), z + ((r * F24S14) + (r * F24C14))) - + n.noise(x + ((r * F24C15) - (r * F24S15)), z + ((r * F24S15) + (r * F24C15))) - + n.noise(x + ((r * F24C16) - (r * F24S16)), z + ((r * F24S16) + (r * F24C16))) - + n.noise(x + ((r * F24C17) - (r * F24S17)), z + ((r * F24S17) + (r * F24C17))) - + n.noise(x + ((r * F24C18) - (r * F24S18)), z + ((r * F24S18) + (r * F24C18))) - + n.noise(x + ((r * F24C19) - (r * F24S19)), z + ((r * F24S19) + (r * F24C19))) - + n.noise(x + ((r * F24C20) - (r * F24S20)), z + ((r * F24S20) + (r * F24C20))) - + n.noise(x + ((r * F24C21) - (r * F24S21)), z + ((r * F24S21) + (r * F24C21))) - + n.noise(x + ((r * F24C22) - (r * F24S22)), z + ((r * F24S22) + (r * F24C22))) - + n.noise(x + ((r * F24C23) - (r * F24S23)), z + ((r * F24S23) + (r * F24C23)))) / 24.0D; - } - - private static double sc32(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F32C0) - (r * F32S0)), z + ((r * F32S0) + (r * F32C0))) - + n.noise(x + ((r * F32C1) - (r * F32S1)), z + ((r * F32S1) + (r * F32C1))) - + n.noise(x + ((r * F32C2) - (r * F32S2)), z + ((r * F32S2) + (r * F32C2))) - + n.noise(x + ((r * F32C3) - (r * F32S3)), z + ((r * F32S3) + (r * F32C3))) - + n.noise(x + ((r * F32C4) - (r * F32S4)), z + ((r * F32S4) + (r * F32C4))) - + n.noise(x + ((r * F32C5) - (r * F32S5)), z + ((r * F32S5) + (r * F32C5))) - + n.noise(x + ((r * F32C6) - (r * F32S6)), z + ((r * F32S6) + (r * F32C6))) - + n.noise(x + ((r * F32C7) - (r * F32S7)), z + ((r * F32S7) + (r * F32C7))) - + n.noise(x + ((r * F32C8) - (r * F32S8)), z + ((r * F32S8) + (r * F32C8))) - + n.noise(x + ((r * F32C9) - (r * F32S9)), z + ((r * F32S9) + (r * F32C9))) - + n.noise(x + ((r * F32C10) - (r * F32S10)), z + ((r * F32S10) + (r * F32C10))) - + n.noise(x + ((r * F32C11) - (r * F32S11)), z + ((r * F32S11) + (r * F32C11))) - + n.noise(x + ((r * F32C12) - (r * F32S12)), z + ((r * F32S12) + (r * F32C12))) - + n.noise(x + ((r * F32C13) - (r * F32S13)), z + ((r * F32S13) + (r * F32C13))) - + n.noise(x + ((r * F32C14) - (r * F32S14)), z + ((r * F32S14) + (r * F32C14))) - + n.noise(x + ((r * F32C15) - (r * F32S15)), z + ((r * F32S15) + (r * F32C15))) - + n.noise(x + ((r * F32C16) - (r * F32S16)), z + ((r * F32S16) + (r * F32C16))) - + n.noise(x + ((r * F32C17) - (r * F32S17)), z + ((r * F32S17) + (r * F32C17))) - + n.noise(x + ((r * F32C18) - (r * F32S18)), z + ((r * F32S18) + (r * F32C18))) - + n.noise(x + ((r * F32C19) - (r * F32S19)), z + ((r * F32S19) + (r * F32C19))) - + n.noise(x + ((r * F32C20) - (r * F32S20)), z + ((r * F32S20) + (r * F32C20))) - + n.noise(x + ((r * F32C21) - (r * F32S21)), z + ((r * F32S21) + (r * F32C21))) - + n.noise(x + ((r * F32C22) - (r * F32S22)), z + ((r * F32S22) + (r * F32C22))) - + n.noise(x + ((r * F32C23) - (r * F32S23)), z + ((r * F32S23) + (r * F32C23))) - + n.noise(x + ((r * F32C24) - (r * F32S24)), z + ((r * F32S24) + (r * F32C24))) - + n.noise(x + ((r * F32C25) - (r * F32S25)), z + ((r * F32S25) + (r * F32C25))) - + n.noise(x + ((r * F32C26) - (r * F32S26)), z + ((r * F32S26) + (r * F32C26))) - + n.noise(x + ((r * F32C27) - (r * F32S27)), z + ((r * F32S27) + (r * F32C27))) - + n.noise(x + ((r * F32C28) - (r * F32S28)), z + ((r * F32S28) + (r * F32C28))) - + n.noise(x + ((r * F32C29) - (r * F32S29)), z + ((r * F32S29) + (r * F32C29))) - + n.noise(x + ((r * F32C30) - (r * F32S30)), z + ((r * F32S30) + (r * F32C30))) - + n.noise(x + ((r * F32C31) - (r * F32S31)), z + ((r * F32S31) + (r * F32C31))) - + n.noise(x + ((r * F32C32) - (r * F32S32)), z + ((r * F32S32) + (r * F32C32)))) / 32.0D; - } - - private static double sc48(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F48C0) - (r * F48S0)), z + ((r * F48S0) + (r * F48C0))) - + n.noise(x + ((r * F48C1) - (r * F48S1)), z + ((r * F48S1) + (r * F48C1))) - + n.noise(x + ((r * F48C2) - (r * F48S2)), z + ((r * F48S2) + (r * F48C2))) - + n.noise(x + ((r * F48C3) - (r * F48S3)), z + ((r * F48S3) + (r * F48C3))) - + n.noise(x + ((r * F48C4) - (r * F48S4)), z + ((r * F48S4) + (r * F48C4))) - + n.noise(x + ((r * F48C5) - (r * F48S5)), z + ((r * F48S5) + (r * F48C5))) - + n.noise(x + ((r * F48C6) - (r * F48S6)), z + ((r * F48S6) + (r * F48C6))) - + n.noise(x + ((r * F48C7) - (r * F48S7)), z + ((r * F48S7) + (r * F48C7))) - + n.noise(x + ((r * F48C8) - (r * F48S8)), z + ((r * F48S8) + (r * F48C8))) - + n.noise(x + ((r * F48C9) - (r * F48S9)), z + ((r * F48S9) + (r * F48C9))) - + n.noise(x + ((r * F48C10) - (r * F48S10)), z + ((r * F48S10) + (r * F48C10))) - + n.noise(x + ((r * F48C11) - (r * F48S11)), z + ((r * F48S11) + (r * F48C11))) - + n.noise(x + ((r * F48C12) - (r * F48S12)), z + ((r * F48S12) + (r * F48C12))) - + n.noise(x + ((r * F48C13) - (r * F48S13)), z + ((r * F48S13) + (r * F48C13))) - + n.noise(x + ((r * F48C14) - (r * F48S14)), z + ((r * F48S14) + (r * F48C14))) - + n.noise(x + ((r * F48C15) - (r * F48S15)), z + ((r * F48S15) + (r * F48C15))) - + n.noise(x + ((r * F48C16) - (r * F48S16)), z + ((r * F48S16) + (r * F48C16))) - + n.noise(x + ((r * F48C17) - (r * F48S17)), z + ((r * F48S17) + (r * F48C17))) - + n.noise(x + ((r * F48C18) - (r * F48S18)), z + ((r * F48S18) + (r * F48C18))) - + n.noise(x + ((r * F48C19) - (r * F48S19)), z + ((r * F48S19) + (r * F48C19))) - + n.noise(x + ((r * F48C20) - (r * F48S20)), z + ((r * F48S20) + (r * F48C20))) - + n.noise(x + ((r * F48C21) - (r * F48S21)), z + ((r * F48S21) + (r * F48C21))) - + n.noise(x + ((r * F48C22) - (r * F48S22)), z + ((r * F48S22) + (r * F48C22))) - + n.noise(x + ((r * F48C23) - (r * F48S23)), z + ((r * F48S23) + (r * F48C23))) - + n.noise(x + ((r * F48C24) - (r * F48S24)), z + ((r * F48S24) + (r * F48C24))) - + n.noise(x + ((r * F48C25) - (r * F48S25)), z + ((r * F48S25) + (r * F48C25))) - + n.noise(x + ((r * F48C26) - (r * F48S26)), z + ((r * F48S26) + (r * F48C26))) - + n.noise(x + ((r * F48C27) - (r * F48S27)), z + ((r * F48S27) + (r * F48C27))) - + n.noise(x + ((r * F48C28) - (r * F48S28)), z + ((r * F48S28) + (r * F48C28))) - + n.noise(x + ((r * F48C29) - (r * F48S29)), z + ((r * F48S29) + (r * F48C29))) - + n.noise(x + ((r * F48C30) - (r * F48S30)), z + ((r * F48S30) + (r * F48C30))) - + n.noise(x + ((r * F48C31) - (r * F48S31)), z + ((r * F48S31) + (r * F48C31))) - + n.noise(x + ((r * F48C32) - (r * F48S32)), z + ((r * F48S32) + (r * F48C32))) - + n.noise(x + ((r * F48C33) - (r * F48S33)), z + ((r * F48S33) + (r * F48C33))) - + n.noise(x + ((r * F48C34) - (r * F48S34)), z + ((r * F48S34) + (r * F48C34))) - + n.noise(x + ((r * F48C35) - (r * F48S35)), z + ((r * F48S35) + (r * F48C35))) - + n.noise(x + ((r * F48C36) - (r * F48S36)), z + ((r * F48S36) + (r * F48C36))) - + n.noise(x + ((r * F48C37) - (r * F48S37)), z + ((r * F48S37) + (r * F48C37))) - + n.noise(x + ((r * F48C38) - (r * F48S38)), z + ((r * F48S38) + (r * F48C38))) - + n.noise(x + ((r * F48C39) - (r * F48S39)), z + ((r * F48S39) + (r * F48C39))) - + n.noise(x + ((r * F48C40) - (r * F48S40)), z + ((r * F48S40) + (r * F48C40))) - + n.noise(x + ((r * F48C41) - (r * F48S41)), z + ((r * F48S41) + (r * F48C41))) - + n.noise(x + ((r * F48C42) - (r * F48S42)), z + ((r * F48S42) + (r * F48C42))) - + n.noise(x + ((r * F48C43) - (r * F48S43)), z + ((r * F48S43) + (r * F48C43))) - + n.noise(x + ((r * F48C44) - (r * F48S44)), z + ((r * F48S44) + (r * F48C44))) - + n.noise(x + ((r * F48C45) - (r * F48S45)), z + ((r * F48S45) + (r * F48C45))) - + n.noise(x + ((r * F48C46) - (r * F48S46)), z + ((r * F48S46) + (r * F48C46))) - + n.noise(x + ((r * F48C47) - (r * F48S47)), z + ((r * F48S47) + (r * F48C47))) - + n.noise(x + ((r * F48C48) - (r * F48S48)), z + ((r * F48S48) + (r * F48C48))) - + n.noise(x + ((r * F48C49) - (r * F48S49)), z + ((r * F48S49) + (r * F48C49))) - + n.noise(x + ((r * F48C50) - (r * F48S50)), z + ((r * F48S50) + (r * F48C50))) - + n.noise(x + ((r * F48C51) - (r * F48S51)), z + ((r * F48S51) + (r * F48C51)))) / 48.0D; - } - - private static double sc64(int x, int z, double r, NoiseProvider n) { - return (n.noise(x + ((r * F64C0) - (r * F64S0)), z + ((r * F64S0) + (r * F64C0))) - + n.noise(x + ((r * F64C1) - (r * F64S1)), z + ((r * F64S1) + (r * F64C1))) - + n.noise(x + ((r * F64C2) - (r * F64S2)), z + ((r * F64S2) + (r * F64C2))) - + n.noise(x + ((r * F64C3) - (r * F64S3)), z + ((r * F64S3) + (r * F64C3))) - + n.noise(x + ((r * F64C4) - (r * F64S4)), z + ((r * F64S4) + (r * F64C4))) - + n.noise(x + ((r * F64C5) - (r * F64S5)), z + ((r * F64S5) + (r * F64C5))) - + n.noise(x + ((r * F64C6) - (r * F64S6)), z + ((r * F64S6) + (r * F64C6))) - + n.noise(x + ((r * F64C7) - (r * F64S7)), z + ((r * F64S7) + (r * F64C7))) - + n.noise(x + ((r * F64C8) - (r * F64S8)), z + ((r * F64S8) + (r * F64C8))) - + n.noise(x + ((r * F64C9) - (r * F64S9)), z + ((r * F64S9) + (r * F64C9))) - + n.noise(x + ((r * F64C10) - (r * F64S10)), z + ((r * F64S10) + (r * F64C10))) - + n.noise(x + ((r * F64C11) - (r * F64S11)), z + ((r * F64S11) + (r * F64C11))) - + n.noise(x + ((r * F64C12) - (r * F64S12)), z + ((r * F64S12) + (r * F64C12))) - + n.noise(x + ((r * F64C13) - (r * F64S13)), z + ((r * F64S13) + (r * F64C13))) - + n.noise(x + ((r * F64C14) - (r * F64S14)), z + ((r * F64S14) + (r * F64C14))) - + n.noise(x + ((r * F64C15) - (r * F64S15)), z + ((r * F64S15) + (r * F64C15))) - + n.noise(x + ((r * F64C16) - (r * F64S16)), z + ((r * F64S16) + (r * F64C16))) - + n.noise(x + ((r * F64C17) - (r * F64S17)), z + ((r * F64S17) + (r * F64C17))) - + n.noise(x + ((r * F64C18) - (r * F64S18)), z + ((r * F64S18) + (r * F64C18))) - + n.noise(x + ((r * F64C19) - (r * F64S19)), z + ((r * F64S19) + (r * F64C19))) - + n.noise(x + ((r * F64C20) - (r * F64S20)), z + ((r * F64S20) + (r * F64C20))) - + n.noise(x + ((r * F64C21) - (r * F64S21)), z + ((r * F64S21) + (r * F64C21))) - + n.noise(x + ((r * F64C22) - (r * F64S22)), z + ((r * F64S22) + (r * F64C22))) - + n.noise(x + ((r * F64C23) - (r * F64S23)), z + ((r * F64S23) + (r * F64C23))) - + n.noise(x + ((r * F64C24) - (r * F64S24)), z + ((r * F64S24) + (r * F64C24))) - + n.noise(x + ((r * F64C25) - (r * F64S25)), z + ((r * F64S25) + (r * F64C25))) - + n.noise(x + ((r * F64C26) - (r * F64S26)), z + ((r * F64S26) + (r * F64C26))) - + n.noise(x + ((r * F64C27) - (r * F64S27)), z + ((r * F64S27) + (r * F64C27))) - + n.noise(x + ((r * F64C28) - (r * F64S28)), z + ((r * F64S28) + (r * F64C28))) - + n.noise(x + ((r * F64C29) - (r * F64S29)), z + ((r * F64S29) + (r * F64C29))) - + n.noise(x + ((r * F64C30) - (r * F64S30)), z + ((r * F64S30) + (r * F64C30))) - + n.noise(x + ((r * F64C31) - (r * F64S31)), z + ((r * F64S31) + (r * F64C31))) - + n.noise(x + ((r * F64C32) - (r * F64S32)), z + ((r * F64S32) + (r * F64C32))) - + n.noise(x + ((r * F64C33) - (r * F64S33)), z + ((r * F64S33) + (r * F64C33))) - + n.noise(x + ((r * F64C34) - (r * F64S34)), z + ((r * F64S34) + (r * F64C34))) - + n.noise(x + ((r * F64C35) - (r * F64S35)), z + ((r * F64S35) + (r * F64C35))) - + n.noise(x + ((r * F64C36) - (r * F64S36)), z + ((r * F64S36) + (r * F64C36))) - + n.noise(x + ((r * F64C37) - (r * F64S37)), z + ((r * F64S37) + (r * F64C37))) - + n.noise(x + ((r * F64C38) - (r * F64S38)), z + ((r * F64S38) + (r * F64C38))) - + n.noise(x + ((r * F64C39) - (r * F64S39)), z + ((r * F64S39) + (r * F64C39))) - + n.noise(x + ((r * F64C40) - (r * F64S40)), z + ((r * F64S40) + (r * F64C40))) - + n.noise(x + ((r * F64C41) - (r * F64S41)), z + ((r * F64S41) + (r * F64C41))) - + n.noise(x + ((r * F64C42) - (r * F64S42)), z + ((r * F64S42) + (r * F64C42))) - + n.noise(x + ((r * F64C43) - (r * F64S43)), z + ((r * F64S43) + (r * F64C43))) - + n.noise(x + ((r * F64C44) - (r * F64S44)), z + ((r * F64S44) + (r * F64C44))) - + n.noise(x + ((r * F64C45) - (r * F64S45)), z + ((r * F64S45) + (r * F64C45))) - + n.noise(x + ((r * F64C46) - (r * F64S46)), z + ((r * F64S46) + (r * F64C46))) - + n.noise(x + ((r * F64C47) - (r * F64S47)), z + ((r * F64S47) + (r * F64C47))) - + n.noise(x + ((r * F64C48) - (r * F64S48)), z + ((r * F64S48) + (r * F64C48))) - + n.noise(x + ((r * F64C49) - (r * F64S49)), z + ((r * F64S49) + (r * F64C49))) - + n.noise(x + ((r * F64C50) - (r * F64S50)), z + ((r * F64S50) + (r * F64C50))) - + n.noise(x + ((r * F64C51) - (r * F64S51)), z + ((r * F64S51) + (r * F64C51))) - + n.noise(x + ((r * F64C52) - (r * F64S52)), z + ((r * F64S52) + (r * F64C52))) - + n.noise(x + ((r * F64C53) - (r * F64S53)), z + ((r * F64S53) + (r * F64C53))) - + n.noise(x + ((r * F64C54) - (r * F64S54)), z + ((r * F64S54) + (r * F64C54))) - + n.noise(x + ((r * F64C55) - (r * F64S55)), z + ((r * F64S55) + (r * F64C55))) - + n.noise(x + ((r * F64C56) - (r * F64S56)), z + ((r * F64S56) + (r * F64C56))) - + n.noise(x + ((r * F64C57) - (r * F64S57)), z + ((r * F64S57) + (r * F64C57))) - + n.noise(x + ((r * F64C58) - (r * F64S58)), z + ((r * F64S58) + (r * F64C58))) - + n.noise(x + ((r * F64C59) - (r * F64S59)), z + ((r * F64S59) + (r * F64C59))) - + n.noise(x + ((r * F64C60) - (r * F64S60)), z + ((r * F64S60) + (r * F64C60))) - + n.noise(x + ((r * F64C61) - (r * F64S61)), z + ((r * F64S61) + (r * F64C61))) - + n.noise(x + ((r * F64C62) - (r * F64S62)), z + ((r * F64S62) + (r * F64C62))) - + n.noise(x + ((r * F64C63) - (r * F64S63)), z + ((r * F64S63) + (r * F64C63))) - + n.noise(x + ((r * F64C64) - (r * F64S64)), z + ((r * F64S64) + (r * F64C64))) - + n.noise(x + ((r * F64C65) - (r * F64S65)), z + ((r * F64S65) + (r * F64C65))) - + n.noise(x + ((r * F64C66) - (r * F64S66)), z + ((r * F64S66) + (r * F64C66))) - + n.noise(x + ((r * F64C67) - (r * F64S67)), z + ((r * F64S67) + (r * F64C67))) - + n.noise(x + ((r * F64C68) - (r * F64S68)), z + ((r * F64S68) + (r * F64C68))) - + n.noise(x + ((r * F64C69) - (r * F64S69)), z + ((r * F64S69) + (r * F64C69))) - + n.noise(x + ((r * F64C70) - (r * F64S70)), z + ((r * F64S70) + (r * F64C70))) - + n.noise(x + ((r * F64C71) - (r * F64S71)), z + ((r * F64S71) + (r * F64C71)))) / 64.0D; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/Element.java b/core/src/main/java/com/volmit/iris/util/inventorygui/Element.java deleted file mode 100644 index af0006ed5..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/Element.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.data.MaterialBlock; -import com.volmit.iris.util.scheduling.Callback; -import org.bukkit.inventory.ItemStack; - -@SuppressWarnings("ALL") -public interface Element { - MaterialBlock getMaterial(); - - Element setMaterial(MaterialBlock b); - - boolean isEnchanted(); - - Element setEnchanted(boolean enchanted); - - String getId(); - - String getName(); - - Element setName(String name); - - double getProgress(); - - Element setProgress(double progress); - - short getEffectiveDurability(); - - int getCount(); - - Element setCount(int c); - - ItemStack computeItemStack(); - - Element setBackground(boolean bg); - - boolean isBackgrond(); - - Element addLore(String loreLine); - - KList getLore(); - - Element call(ElementEvent event, Element context); - - Element onLeftClick(Callback clicked); - - Element onRightClick(Callback clicked); - - Element onShiftLeftClick(Callback clicked); - - Element onShiftRightClick(Callback clicked); - - Element onDraggedInto(Callback into); - - Element onOtherDraggedInto(Callback other); -} diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/ElementEvent.java b/core/src/main/java/com/volmit/iris/util/inventorygui/ElementEvent.java deleted file mode 100644 index 8c373938a..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/ElementEvent.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -/** - * Element Event. - * - * @author cyberpwn - */ -public enum ElementEvent { - LEFT, - RIGHT, - SHIFT_LEFT, - SHIFT_RIGHT, - DRAG_INTO, - OTHER_DRAG_INTO -} diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/RandomColor.java b/core/src/main/java/com/volmit/iris/util/inventorygui/RandomColor.java deleted file mode 100644 index a143a0aab..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/RandomColor.java +++ /dev/null @@ -1,527 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Random; - -/** - * Credit to https://github.com/lzyzsd/AndroidRandomColor - */ -public class RandomColor { - - public static int hueOffset = 0; - private final Random random; - private final HashMap colors = new HashMap<>(); - - public RandomColor() { - loadColorBounds(); - random = new Random(); - } - - public RandomColor(long seed) { - loadColorBounds(); - random = new Random(); - random.setSeed(seed); - } - - public RandomColor(Random random) { - loadColorBounds(); - this.random = random; - } - - private int getColor(int hue, int saturation, int brightness) { - return java.awt.Color.getHSBColor((float) (hue + hueOffset % 360) / 360, (float) saturation / 100, (float) brightness / 100).getRGB(); - } - - public int randomColor() { - return randomColor(0, null, null); - } - - public int randomColor(int value, SaturationType saturationType, Luminosity luminosity) { - int hue = value; - hue = pickHue(hue); - int saturation = pickSaturation(hue, saturationType, luminosity); - int brightness = pickBrightness(hue, saturation, luminosity); - - return getColor(hue, saturation, brightness); - } - - public int randomColor(Color color, SaturationType saturationType, Luminosity luminosity) { - int hue = pickHue(color.name()); - int saturation = pickSaturation(hue, saturationType, luminosity); - int brightness = pickBrightness(hue, saturation, luminosity); - - return getColor(hue, saturation, brightness); - } - - public int[] randomColor(int count) { - if (count <= 0) { - throw new IllegalArgumentException("count must be greater than 0"); - } - - int[] colors = new int[count]; - for (int i = 0; i < count; i++) { - colors[i] = randomColor(); - } - - return colors; - } - - public int randomColor(Color color) { - int hue = pickHue(color.name()); - int saturation = pickSaturation(color, null, null); - int brightness = pickBrightness(color, saturation, null); - - int colorValue = getColor(hue, saturation, brightness); - return colorValue; - } - - public int[] random(Color color, int count) { - if (count <= 0) { - throw new IllegalArgumentException("count must be greater than 0"); - } - - int[] colors = new int[count]; - for (int i = 0; i < count; i++) { - colors[i] = randomColor(color); - } - - return colors; - } - - private int pickHue(int hue) { - Range hueRange = getHueRange(hue); - return doPickHue(hueRange); - } - - private int doPickHue(Range hueRange) { - int hue = randomWithin(hueRange); - - // Instead of storing red as two seperate ranges, - // we group them, using negative numbers - if (hue < 0) { - hue = 360 + hue; - } - - return hue; - } - - private int pickHue(String name) { - Range hueRange = getHueRange(name); - return doPickHue(hueRange); - } - - private Range getHueRange(int number) { - if (number < 360 && number > 0) { - return new Range(number, number); - } - - return new Range(0, 360); - } - - private Range getHueRange(String name) { - if (colors.containsKey(name)) { - return colors.get(name).getHueRange(); - } - - return new Range(0, 360); - } - - private int pickSaturation(int hue, SaturationType saturationType, Luminosity luminosity) { - return pickSaturation(getColorInfo(hue), saturationType, luminosity); - } - - private int pickSaturation(Color color, SaturationType saturationType, Luminosity luminosity) { - ColorInfo colorInfo = colors.get(color.name()); - return pickSaturation(colorInfo, saturationType, luminosity); - } - - private int pickSaturation(ColorInfo colorInfo, SaturationType saturationType, Luminosity luminosity) { - if (saturationType != null) { - switch (saturationType) { - case RANDOM: - return randomWithin(new Range(0, 100)); - case MONOCHROME: - return 0; - case HIGH: - return randomWithin(new Range(75, 100)); - case MEDIUM: - return randomWithin(new Range(55, 75)); - case LOW: - return randomWithin(new Range(35, 55)); - } - } - - if (colorInfo == null) { - return 0; - } - - Range saturationRange = colorInfo.getSaturationRange(); - - int min = saturationRange.start; - int max = saturationRange.end; - - if (luminosity != null) { - switch (luminosity) { - case LIGHT: - min = 55; - break; - case BRIGHT: - min = max - 10; - break; - case DARK: - max = 55; - break; - } - } - - return randomWithin(new Range(min, max)); - } - - private int pickBrightness(int hue, int saturation, Luminosity luminosity) { - ColorInfo colorInfo = getColorInfo(hue); - - return pickBrightness(colorInfo, saturation, luminosity); - } - - private int pickBrightness(Color color, int saturation, Luminosity luminosity) { - ColorInfo colorInfo = colors.get(color.name()); - - return pickBrightness(colorInfo, saturation, luminosity); - } - - private int pickBrightness(ColorInfo colorInfo, int saturation, Luminosity luminosity) { - int min = getMinimumBrightness(colorInfo, saturation), - max = 100; - - if (luminosity != null) { - switch (luminosity) { - - case DARK: - max = min + 20; - break; - - case LIGHT: - min = (max + min) / 2; - break; - - case RANDOM: - min = 0; - max = 100; - break; - } - } - - return randomWithin(new Range(min, max)); - } - - private int getMinimumBrightness(ColorInfo colorInfo, int saturation) { - if (colorInfo == null) { - return 0; - } - - List lowerBounds = colorInfo.getLowerBounds(); - for (int i = 0; i < lowerBounds.size() - 1; i++) { - - int s1 = lowerBounds.get(i).start, - v1 = lowerBounds.get(i).end; - - if (i == lowerBounds.size() - 1) { - break; - } - int s2 = lowerBounds.get(i + 1).start, - v2 = lowerBounds.get(i + 1).end; - - if (saturation >= s1 && saturation <= s2) { - - float m = (v2 - v1) / (float) (s2 - s1), - b = v1 - m * s1; - - return (int) (m * saturation + b); - } - - } - - return 0; - } - - private ColorInfo getColorInfo(int hue) { - // Maps red colors to make picking hue easier - if (hue >= 334 && hue <= 360) { - hue -= 360; - } - - for (String key : colors.keySet()) { - ColorInfo colorInfo = colors.get(key); - if (colorInfo.getHueRange() != null && colorInfo.getHueRange().contain(hue)) { - return colorInfo; - } - } - - return null; - } - - private int randomWithin(Range range) { - return (int) Math.floor(range.start + random.nextDouble() * (range.end + 1 - range.start)); - } - - public void defineColor(String name, Range hueRange, List lowerBounds) { - int sMin = lowerBounds.get(0).start; - int sMax = lowerBounds.get(lowerBounds.size() - 1).start; - int bMin = lowerBounds.get(lowerBounds.size() - 1).end; - int bMax = lowerBounds.get(0).end; - - colors.put(name, new ColorInfo(hueRange, new Range(sMin, sMax), new Range(bMin, bMax), lowerBounds)); - } - - private void loadColorBounds() { - List lowerBounds1 = new ArrayList<>(); - lowerBounds1.add(new Range(0, 0)); - lowerBounds1.add(new Range(100, 0)); - defineColor( - Color.MONOCHROME.name(), - new Range(0, 0), - lowerBounds1 - ); - - List lowerBounds2 = new ArrayList<>(); - lowerBounds2.add(new Range(20, 100)); - lowerBounds2.add(new Range(30, 92)); - lowerBounds2.add(new Range(40, 89)); - lowerBounds2.add(new Range(50, 85)); - lowerBounds2.add(new Range(60, 78)); - lowerBounds2.add(new Range(70, 70)); - lowerBounds2.add(new Range(80, 60)); - lowerBounds2.add(new Range(90, 55)); - lowerBounds2.add(new Range(100, 50)); - defineColor( - Color.RED.name(), - new Range(-26, 18), - lowerBounds2 - ); - - List lowerBounds3 = new ArrayList(); - lowerBounds3.add(new Range(20, 100)); - lowerBounds3.add(new Range(30, 93)); - lowerBounds3.add(new Range(40, 88)); - lowerBounds3.add(new Range(50, 86)); - lowerBounds3.add(new Range(60, 85)); - lowerBounds3.add(new Range(70, 70)); - lowerBounds3.add(new Range(100, 70)); - defineColor( - Color.ORANGE.name(), - new Range(19, 46), - lowerBounds3 - ); - - List lowerBounds4 = new ArrayList<>(); - lowerBounds4.add(new Range(25, 100)); - lowerBounds4.add(new Range(40, 94)); - lowerBounds4.add(new Range(50, 89)); - lowerBounds4.add(new Range(60, 86)); - lowerBounds4.add(new Range(70, 84)); - lowerBounds4.add(new Range(80, 82)); - lowerBounds4.add(new Range(90, 80)); - lowerBounds4.add(new Range(100, 75)); - - defineColor( - Color.YELLOW.name(), - new Range(47, 62), - lowerBounds4 - ); - - List lowerBounds5 = new ArrayList<>(); - lowerBounds5.add(new Range(30, 100)); - lowerBounds5.add(new Range(40, 90)); - lowerBounds5.add(new Range(50, 85)); - lowerBounds5.add(new Range(60, 81)); - lowerBounds5.add(new Range(70, 74)); - lowerBounds5.add(new Range(80, 64)); - lowerBounds5.add(new Range(90, 50)); - lowerBounds5.add(new Range(100, 40)); - - defineColor( - Color.GREEN.name(), - new Range(63, 178), - lowerBounds5 - ); - - List lowerBounds6 = new ArrayList<>(); - lowerBounds6.add(new Range(20, 100)); - lowerBounds6.add(new Range(30, 86)); - lowerBounds6.add(new Range(40, 80)); - lowerBounds6.add(new Range(50, 74)); - lowerBounds6.add(new Range(60, 60)); - lowerBounds6.add(new Range(70, 52)); - lowerBounds6.add(new Range(80, 44)); - lowerBounds6.add(new Range(90, 39)); - lowerBounds6.add(new Range(100, 35)); - - defineColor( - Color.BLUE.name(), - new Range(179, 257), - lowerBounds6 - ); - - List lowerBounds7 = new ArrayList<>(); - lowerBounds7.add(new Range(20, 100)); - lowerBounds7.add(new Range(30, 87)); - lowerBounds7.add(new Range(40, 79)); - lowerBounds7.add(new Range(50, 70)); - lowerBounds7.add(new Range(60, 65)); - lowerBounds7.add(new Range(70, 59)); - lowerBounds7.add(new Range(80, 52)); - lowerBounds7.add(new Range(90, 45)); - lowerBounds7.add(new Range(100, 42)); - - defineColor( - Color.PURPLE.name(), - new Range(258, 282), - lowerBounds7 - ); - - List lowerBounds8 = new ArrayList<>(); - lowerBounds8.add(new Range(20, 100)); - lowerBounds8.add(new Range(30, 90)); - lowerBounds8.add(new Range(40, 86)); - lowerBounds8.add(new Range(60, 84)); - lowerBounds8.add(new Range(80, 80)); - lowerBounds8.add(new Range(90, 75)); - lowerBounds8.add(new Range(100, 73)); - - defineColor( - Color.PINK.name(), - new Range(283, 334), - lowerBounds8 - ); - } - - public enum SaturationType { - RANDOM, MONOCHROME, HIGH, LOW, MEDIUM - } - - public enum Luminosity { - BRIGHT, LIGHT, DARK, RANDOM - } - - public enum Color { - MONOCHROME, RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE, PINK - } - - public static class ColorInfo { - Range hueRange; - Range saturationRange; - Range brightnessRange; - List lowerBounds; - - public ColorInfo(Range hueRange, Range saturationRange, Range brightnessRange, List lowerBounds) { - this.hueRange = hueRange; - this.saturationRange = saturationRange; - this.brightnessRange = brightnessRange; - this.lowerBounds = lowerBounds; - } - - public Range getHueRange() { - return hueRange; - } - - public void setHueRange(Range hueRange) { - this.hueRange = hueRange; - } - - public Range getSaturationRange() { - return saturationRange; - } - - public void setSaturationRange(Range saturationRange) { - this.saturationRange = saturationRange; - } - - public Range getBrightnessRange() { - return brightnessRange; - } - - public void setBrightnessRange(Range brightnessRange) { - this.brightnessRange = brightnessRange; - } - - public List getLowerBounds() { - return lowerBounds; - } - - public void setLowerBounds(List lowerBounds) { - this.lowerBounds = lowerBounds; - } - } - - public static class Range { - int start; - int end; - - public Range(int start, int end) { - this.start = start; - this.end = end; - } - - public boolean contain(int value) { - return value >= start && value <= end; - } - - @Override - public String toString() { - return "start: " + start + " end: " + end; - } - } - - public static class Options { - int hue; - SaturationType saturationType; - Luminosity luminosity; - - public int getHue() { - return hue; - } - - public void setHue(int hue) { - this.hue = hue; - } - - public SaturationType getSaturationType() { - return saturationType; - } - - public void setSaturationType(SaturationType saturationType) { - this.saturationType = saturationType; - } - - public Luminosity getLuminosity() { - return luminosity; - } - - public void setLuminosity(Luminosity luminosity) { - this.luminosity = luminosity; - } - } - -} - - diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/UIElement.java b/core/src/main/java/com/volmit/iris/util/inventorygui/UIElement.java deleted file mode 100644 index acadadd37..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/UIElement.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.data.MaterialBlock; -import com.volmit.iris.util.scheduling.Callback; -import org.bukkit.Material; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; - -public class UIElement implements Element { - private final String id; - private final KList lore; - private MaterialBlock material; - private boolean enchanted; - private String name; - private double progress; - private boolean bg; - private Callback eLeft; - private Callback eRight; - private Callback eShiftLeft; - private Callback eShiftRight; - private Callback eDraggedInto; - private Callback eOtherDraggedInto; - private int count; - - public UIElement(String id) { - this.id = id; - lore = new KList<>(); - enchanted = false; - count = 1; - material = new MaterialBlock(Material.AIR); - } - - @Override - public MaterialBlock getMaterial() { - return material; - } - - @Override - public UIElement setMaterial(MaterialBlock material) { - this.material = material; - return this; - } - - public Double clip(double value, double min, double max) { - return Math.min(max, Math.max(min, value)); - } - - @Override - public boolean isEnchanted() { - return enchanted; - } - - @Override - public UIElement setEnchanted(boolean enchanted) { - this.enchanted = enchanted; - return this; - } - - @Override - public String getId() { - return id; - } - - @Override - public String getName() { - return name; - } - - @Override - public UIElement setName(String name) { - this.name = name; - return this; - } - - @Override - public KList getLore() { - return lore; - } - - @Override - public UIElement onLeftClick(Callback clicked) { - eLeft = clicked; - return this; - } - - @Override - public UIElement onRightClick(Callback clicked) { - eRight = clicked; - return this; - } - - @Override - public UIElement onShiftLeftClick(Callback clicked) { - eShiftLeft = clicked; - return this; - } - - @Override - public UIElement onShiftRightClick(Callback clicked) { - eShiftRight = clicked; - return this; - } - - @Override - public UIElement onDraggedInto(Callback into) { - eDraggedInto = into; - return this; - } - - @Override - public UIElement onOtherDraggedInto(Callback other) { - eOtherDraggedInto = other; - return this; - } - - @Override - public Element call(ElementEvent event, Element context) { - try { - switch (event) { - case DRAG_INTO -> { - eDraggedInto.run(context); - return this; - } - case LEFT -> { - eLeft.run(context); - return this; - } - case OTHER_DRAG_INTO -> { - eOtherDraggedInto.run(context); - return this; - } - case RIGHT -> { - eRight.run(context); - return this; - } - case SHIFT_LEFT -> { - eShiftLeft.run(context); - return this; - } - case SHIFT_RIGHT -> { - eShiftRight.run(context); - return this; - } - } - } catch (NullPointerException e) { - Iris.reportError(e); - - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return this; - } - - @Override - public Element addLore(String loreLine) { - getLore().add(loreLine); - return this; - } - - @Override - public Element setBackground(boolean bg) { - this.bg = bg; - return this; - } - - @Override - public boolean isBackgrond() { - return bg; - } - - @Override - public Element setCount(int c) { - count = clip(c, 1, 64).intValue(); - return this; - } - - @Override - public int getCount() { - return count; - } - - @SuppressWarnings("deprecation") - @Override - public ItemStack computeItemStack() { - try { - ItemStack is = new ItemStack(getMaterial().getMaterial(), getCount(), getEffectiveDurability()); - ItemMeta im = is.getItemMeta(); - im.setDisplayName(getName()); - im.setLore(getLore().copy()); - - if (isEnchanted()) { - im.addEnchant(Enchantment.FIRE_ASPECT, 1, true); - } - - is.setItemMeta(im); - return is; - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return null; - } - - @Override - public Element setProgress(double progress) { - this.progress = clip(progress, 0D, 1D); - return this; - } - - @Override - public double getProgress() { - return progress; - } - - @Override - public short getEffectiveDurability() { - if (getMaterial().getMaterial().getMaxDurability() == 0) { - return 0; - } else { - int prog = (int) ((double) getMaterial().getMaterial().getMaxDurability() * (1D - getProgress())); - return clip(prog, 1, (getMaterial().getMaterial().getMaxDurability() - 1)).shortValue(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/UIStaticDecorator.java b/core/src/main/java/com/volmit/iris/util/inventorygui/UIStaticDecorator.java deleted file mode 100644 index 8f10c8db3..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/UIStaticDecorator.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -import com.volmit.iris.util.data.MaterialBlock; -import org.bukkit.Material; - -@SuppressWarnings("ClassCanBeRecord") -public class UIStaticDecorator implements WindowDecorator { - private final Element element; - - public UIStaticDecorator(Element element) { - this.element = element == null ? new UIElement("bg").setMaterial(new MaterialBlock(Material.AIR)) : element; - } - - @Override - public Element onDecorateBackground(Window window, int position, int row) { - return element; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/UIVoidDecorator.java b/core/src/main/java/com/volmit/iris/util/inventorygui/UIVoidDecorator.java deleted file mode 100644 index 62d5ed317..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/UIVoidDecorator.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -public class UIVoidDecorator implements WindowDecorator { - @Override - public Element onDecorateBackground(Window window, int position, int row) { - return null; - } -} - diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/UIWindow.java b/core/src/main/java/com/volmit/iris/util/inventorygui/UIWindow.java deleted file mode 100644 index e1c17ea1c..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/UIWindow.java +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.scheduling.Callback; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.HandlerList; -import org.bukkit.event.Listener; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; - -public class UIWindow implements Window, Listener { - private final Player viewer; - private final KMap elements; - private WindowDecorator decorator; - private Callback eClose; - private WindowResolution resolution; - private String title; - private boolean visible; - private int viewportPosition; - private int viewportSize; - private int highestRow; - private Inventory inventory; - private int clickcheck; - private boolean doubleclicked; - - public UIWindow(Player viewer) { - clickcheck = 0; - doubleclicked = false; - this.viewer = viewer; - this.elements = new KMap<>(); - setTitle(""); - setDecorator(new UIVoidDecorator()); - setResolution(WindowResolution.W9_H6); - setViewportHeight(clip(3, 1, getResolution().getMaxHeight()).intValue()); - setViewportPosition(0); - } - - @EventHandler - public void on(InventoryClickEvent e) { - if (!e.getWhoClicked().equals(viewer)) { - return; - } - - if (!isVisible()) { - return; - } - - // 1.14 bukkit api change, removed getTitle() and getName() from Inventory.class - if (!viewer.getOpenInventory().getTitle().equals(title)) { - return; - } - - if (e.getClickedInventory() == null) { - return; - } - - if (!e.getView().getType().equals(getResolution().getType())) { - return; - } - - if (e.getClickedInventory().getType().equals(getResolution().getType())) { - Element element = getElement(getLayoutPosition(e.getSlot()), getLayoutRow(e.getSlot())); - - switch (e.getAction()) { - case CLONE_STACK: - case UNKNOWN: - case SWAP_WITH_CURSOR: - case PLACE_SOME: - case PLACE_ONE: - case PLACE_ALL: - case PICKUP_SOME: - case PICKUP_ONE: - case PICKUP_HALF: - case PICKUP_ALL: - case NOTHING: - case MOVE_TO_OTHER_INVENTORY: - case HOTBAR_SWAP: - case HOTBAR_MOVE_AND_READD: - case DROP_ONE_SLOT: - case DROP_ONE_CURSOR: - case DROP_ALL_SLOT: - case DROP_ALL_CURSOR: - case COLLECT_TO_CURSOR: - break; - } - - switch (e.getClick()) { - case DOUBLE_CLICK: - doubleclicked = true; - break; - case LEFT: - - clickcheck++; - - if (clickcheck == 1) { - J.s(() -> - { - if (clickcheck == 1) { - clickcheck = 0; - - if (element != null) { - element.call(ElementEvent.LEFT, element); - } - } - }); - } else if (clickcheck == 2) { - J.s(() -> - { - if (doubleclicked) { - doubleclicked = false; - } else { - scroll(1); - } - - clickcheck = 0; - }); - } - - break; - case RIGHT: - if (element != null) { - element.call(ElementEvent.RIGHT, element); - } else { - scroll(-1); - } - break; - case SHIFT_LEFT: - if (element != null) { - element.call(ElementEvent.SHIFT_LEFT, element); - } - break; - case SHIFT_RIGHT: - if (element != null) { - element.call(ElementEvent.SHIFT_RIGHT, element); - } - break; - case SWAP_OFFHAND: - case UNKNOWN: - case WINDOW_BORDER_RIGHT: - case WINDOW_BORDER_LEFT: - case NUMBER_KEY: - case MIDDLE: - case DROP: - case CREATIVE: - case CONTROL_DROP: - default: - break; - } - } - - e.setCancelled(true); - - } - - @EventHandler - public void on(InventoryCloseEvent e) { - if (!e.getPlayer().equals(viewer)) { - return; - } - - if (!e.getPlayer().getOpenInventory().getTitle().equals(title)) { - return; - } - - if (isVisible()) { - close(); - callClosed(); - } - } - - @Override - public WindowDecorator getDecorator() { - return decorator; - } - - @Override - public UIWindow setDecorator(WindowDecorator decorator) { - this.decorator = decorator; - return this; - } - - @Override - public UIWindow close() { - setVisible(false); - return this; - } - - @Override - public UIWindow open() { - setVisible(true); - return this; - } - - @Override - public boolean isVisible() { - return visible; - } - - @Override - public UIWindow setVisible(boolean visible) { - if (isVisible() == visible) { - return this; - } - - if (visible) { - Bukkit.getPluginManager().registerEvents(this, Iris.instance); - - if (getResolution().getType().equals(InventoryType.CHEST)) { - inventory = Bukkit.createInventory(null, getViewportHeight() * 9, getTitle()); - } else { - inventory = Bukkit.createInventory(null, getResolution().getType(), getTitle()); - } - - viewer.openInventory(inventory); - this.visible = visible; - updateInventory(); - } else { - this.visible = visible; - HandlerList.unregisterAll(this); - viewer.closeInventory(); - } - - this.visible = visible; - return this; - } - - @Override - public int getViewportPosition() { - return viewportPosition; - } - - @Override - public UIWindow setViewportPosition(int viewportPosition) { - this.viewportPosition = viewportPosition; - scroll(0); - updateInventory(); - - return this; - } - - @Override - public int getMaxViewportPosition() { - return Math.max(0, highestRow - getViewportHeight()); - } - - @Override - public UIWindow scroll(int direction) { - viewportPosition = (int) clip(viewportPosition + direction, 0, getMaxViewportPosition()).doubleValue(); - updateInventory(); - - return this; - } - - @Override - public int getViewportHeight() { - return viewportSize; - } - - @Override - public UIWindow setViewportHeight(int height) { - viewportSize = (int) clip(height, 1, getResolution().getMaxHeight()).doubleValue(); - - if (isVisible()) { - reopen(); - } - - return this; - } - - @Override - public String getTitle() { - return title; - } - - @Override - public UIWindow setTitle(String title) { - this.title = title; - - if (isVisible()) { - reopen(); - } - - return this; - } - - @Override - public UIWindow setElement(int position, int row, Element e) { - if (row > highestRow) { - highestRow = row; - } - - elements.put(getRealPosition((int) clip(position, -getResolution().getMaxWidthOffset(), getResolution().getMaxWidthOffset()).doubleValue(), row), e); - updateInventory(); - return this; - } - - @Override - public Element getElement(int position, int row) { - return elements.get(getRealPosition((int) clip(position, -getResolution().getMaxWidthOffset(), getResolution().getMaxWidthOffset()).doubleValue(), row)); - } - - @Override - public Player getViewer() { - return viewer; - } - - @Override - public UIWindow onClosed(Callback window) { - eClose = window; - return this; - } - - @Override - public int getViewportSlots() { - return getViewportHeight() * getResolution().getWidth(); - } - - @Override - public int getLayoutRow(int viewportSlottedPosition) { - return getRow(getRealLayoutPosition(viewportSlottedPosition)); - } - - @Override - public int getLayoutPosition(int viewportSlottedPosition) { - return getPosition(viewportSlottedPosition); - } - - @Override - public int getRealLayoutPosition(int viewportSlottedPosition) { - return getRealPosition(getPosition(viewportSlottedPosition), getRow(viewportSlottedPosition) + getViewportPosition()); - } - - @Override - public int getRealPosition(int position, int row) { - return (int) (((row * getResolution().getWidth()) + getResolution().getMaxWidthOffset()) + clip(position, -getResolution().getMaxWidthOffset(), getResolution().getMaxWidthOffset())); - } - - @Override - public int getRow(int realPosition) { - return realPosition / getResolution().getWidth(); - } - - @Override - public int getPosition(int realPosition) { - return (realPosition % getResolution().getWidth()) - getResolution().getMaxWidthOffset(); - } - - @Override - public Window callClosed() { - if (eClose != null) { - eClose.run(this); - } - - return this; - } - - @Override - public boolean hasElement(int position, int row) { - return getElement(position, row) != null; - } - - @Override - public WindowResolution getResolution() { - return resolution; - } - - public Double clip(double value, double min, double max) { - return Math.min(max, Math.max(min, value)); - } - - @Override - public Window setResolution(WindowResolution resolution) { - close(); - this.resolution = resolution; - setViewportHeight((int) clip(getViewportHeight(), 1, getResolution().getMaxHeight()).doubleValue()); - return this; - } - - @Override - public Window clearElements() { - highestRow = 0; - elements.clear(); - updateInventory(); - return this; - } - - @Override - public Window updateInventory() { - if (isVisible()) { - ItemStack[] is = inventory.getContents(); - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") KSet isf = new KSet<>(); - - for (int i = 0; i < is.length; i++) { - ItemStack isc = is[i]; - ItemStack isx = computeItemStack(i); - int layoutRow = getLayoutRow(i); - int layoutPosition = getLayoutPosition(i); - - if (isx != null && !hasElement(layoutPosition, layoutRow)) { - ItemStack gg = isx.clone(); - gg.setAmount(gg.getAmount() + 1); - isf.add(gg); - } - - if (((isc == null) != (isx == null)) || isx != null && isc != null && !isc.equals(isx)) { - inventory.setItem(i, isx); - } - } - } - - return this; - } - - @Override - public ItemStack computeItemStack(int viewportSlot) { - int layoutRow = getLayoutRow(viewportSlot); - int layoutPosition = getLayoutPosition(viewportSlot); - Element e = hasElement(layoutPosition, layoutRow) ? getElement(layoutPosition, layoutRow) : getDecorator().onDecorateBackground(this, layoutPosition, layoutRow); - - if (e != null) { - return e.computeItemStack(); - } - - return null; - } - - @Override - public Window reopen() { - return this.close().open(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/Window.java b/core/src/main/java/com/volmit/iris/util/inventorygui/Window.java deleted file mode 100644 index e2ee91ac6..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/Window.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -import com.volmit.iris.util.scheduling.Callback; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; - -public interface Window { - WindowDecorator getDecorator(); - - Window setDecorator(WindowDecorator decorator); - - WindowResolution getResolution(); - - Window setResolution(WindowResolution resolution); - - Window clearElements(); - - Window close(); - - Window open(); - - Window callClosed(); - - Window updateInventory(); - - ItemStack computeItemStack(int viewportSlot); - - int getLayoutRow(int viewportSlottedPosition); - - int getLayoutPosition(int viewportSlottedPosition); - - int getRealLayoutPosition(int viewportSlottedPosition); - - int getRealPosition(int position, int row); - - int getRow(int realPosition); - - int getPosition(int realPosition); - - boolean isVisible(); - - Window setVisible(boolean visible); - - int getViewportPosition(); - - Window setViewportPosition(int position); - - int getViewportSlots(); - - int getMaxViewportPosition(); - - Window scroll(int direction); - - int getViewportHeight(); - - Window setViewportHeight(int height); - - String getTitle(); - - Window setTitle(String title); - - boolean hasElement(int position, int row); - - Window setElement(int position, int row, Element e); - - Element getElement(int position, int row); - - Player getViewer(); - - Window reopen(); - - Window onClosed(Callback window); -} diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/WindowDecorator.java b/core/src/main/java/com/volmit/iris/util/inventorygui/WindowDecorator.java deleted file mode 100644 index c0ec1006d..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/WindowDecorator.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -public interface WindowDecorator { - Element onDecorateBackground(Window window, int position, int row); -} diff --git a/core/src/main/java/com/volmit/iris/util/inventorygui/WindowResolution.java b/core/src/main/java/com/volmit/iris/util/inventorygui/WindowResolution.java deleted file mode 100644 index 01c8c3d6b..000000000 --- a/core/src/main/java/com/volmit/iris/util/inventorygui/WindowResolution.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.inventorygui; - -import org.bukkit.event.inventory.InventoryType; - -public enum WindowResolution { - W9_H6(9, 6, InventoryType.CHEST), - W5_H1(5, 1, InventoryType.HOPPER), - W3_H3(3, 3, InventoryType.DROPPER); - - private final int width; - private final int maxHeight; - private final InventoryType type; - - WindowResolution(int w, int h, InventoryType type) { - this.width = w; - this.maxHeight = h; - this.type = type; - } - - public int getMaxWidthOffset() { - return (getWidth() - 1) / 2; - } - - public int getWidth() { - return width; - } - - public int getMaxHeight() { - return maxHeight; - } - - public InventoryType getType() { - return type; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/io/Converter.java b/core/src/main/java/com/volmit/iris/util/io/Converter.java deleted file mode 100644 index 01e326a44..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/Converter.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import java.io.File; - -public interface Converter { - String getInExtension(); - - @SuppressWarnings("SameReturnValue") - String getOutExtension(); - - void convert(File in, File out); -} diff --git a/core/src/main/java/com/volmit/iris/util/io/CountingDataInputStream.java b/core/src/main/java/com/volmit/iris/util/io/CountingDataInputStream.java deleted file mode 100644 index 65fe8a36d..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/CountingDataInputStream.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.volmit.iris.util.io; - -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; - -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; - -public class CountingDataInputStream extends DataInputStream { - private final Counter counter; - - private CountingDataInputStream(@NotNull InputStream in) { - super(in); - if (!(in instanceof Counter c)) - throw new IllegalArgumentException("Underlying stream must be a Counter"); - this.counter = c; - } - - public static CountingDataInputStream wrap(@NotNull InputStream in) { - return new CountingDataInputStream(new Counter(in)); - } - - public long count() { - return counter.count; - } - - public void skipTo(long target) throws IOException { - skipNBytes(Math.max(target - counter.count, 0)); - } - - @RequiredArgsConstructor - private static class Counter extends InputStream { - private final InputStream in; - private long count; - private long mark = -1; - private int markLimit = 0; - - @Override - public int read() throws IOException { - int i = in.read(); - if (i != -1) count(1); - return i; - } - - @Override - public int read(byte @NotNull [] b, int off, int len) throws IOException { - int i = in.read(b, off, len); - if (i != -1) count(i); - return i; - } - - private void count(int i) { - count = Math.addExact(count, i); - if (mark == -1) - return; - - markLimit -= i; - if (markLimit <= 0) - mark = -1; - } - - @Override - public boolean markSupported() { - return in.markSupported(); - } - - @Override - public synchronized void mark(int readlimit) { - if (!in.markSupported()) return; - in.mark(readlimit); - if (readlimit <= 0) { - mark = -1; - markLimit = 0; - return; - } - - mark = count; - markLimit = readlimit; - } - - @Override - public synchronized void reset() throws IOException { - in.reset(); - count = mark; - } - - @Override - public void close() throws IOException { - in.close(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/io/CustomOutputStream.java b/core/src/main/java/com/volmit/iris/util/io/CustomOutputStream.java deleted file mode 100644 index b1cb2a268..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/CustomOutputStream.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.zip.GZIPOutputStream; - -public class CustomOutputStream extends GZIPOutputStream { - public CustomOutputStream(OutputStream out, int level) throws IOException { - super(out); - def.setLevel(level); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/io/FileWatcher.java b/core/src/main/java/com/volmit/iris/util/io/FileWatcher.java deleted file mode 100644 index 8a588d186..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/FileWatcher.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import java.io.File; - -public class FileWatcher { - protected final File file; - private long lastModified; - private long size; - - public FileWatcher(File file) { - this.file = file; - readProperties(); - } - - protected void readProperties() { - boolean exists = file.exists(); - lastModified = exists ? file.lastModified() : -1; - size = exists ? file.isDirectory() ? -2 : file.length() : -1; - } - - public boolean checkModified() { - long m = lastModified; - long g = size; - boolean mod = false; - readProperties(); - - if (lastModified != m || g != size) { - mod = true; - } - - return mod; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/io/FolderWatcher.java b/core/src/main/java/com/volmit/iris/util/io/FolderWatcher.java deleted file mode 100644 index ec1c2708b..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/FolderWatcher.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; - -import java.io.File; - -public class FolderWatcher extends FileWatcher { - private KMap watchers; - private KList changed; - private KList created; - private KList deleted; - - public FolderWatcher(File file) { - super(file); - } - - protected void readProperties() { - if (watchers == null) { - watchers = new KMap<>(); - changed = new KList<>(); - created = new KList<>(); - deleted = new KList<>(); - } - - if (file.isDirectory()) { - for (File i : file.listFiles()) { - if (!watchers.containsKey(i)) { - watchers.put(i, new FolderWatcher(i)); - } - } - - for (File i : watchers.k()) { - if (!i.exists()) { - watchers.remove(i); - } - } - } else { - super.readProperties(); - } - } - - public boolean checkModified() { - changed.clear(); - created.clear(); - deleted.clear(); - - if (file.isDirectory()) { - KMap w = watchers.copy(); - readProperties(); - - for (File i : w.keySet()) { - if (!watchers.containsKey(i)) { - deleted.add(i); - } - } - - for (File i : watchers.keySet()) { - if (!w.containsKey(i)) { - created.add(i); - } else { - FolderWatcher fw = watchers.get(i); - if (fw.checkModified()) { - changed.add(fw.file); - } - - changed.addAll(fw.getChanged()); - created.addAll(fw.getCreated()); - deleted.addAll(fw.getDeleted()); - } - } - - return !changed.isEmpty() || !created.isEmpty() || !deleted.isEmpty(); - } - - return super.checkModified(); - } - - public boolean checkModifiedFast() { - if (watchers == null || watchers.isEmpty()) { - return checkModified(); - } - - changed.clear(); - created.clear(); - deleted.clear(); - - if (file.isDirectory()) { - for (File i : watchers.keySet()) { - FolderWatcher fw = watchers.get(i); - if (fw.checkModifiedFast()) { - changed.add(fw.file); - } - - changed.addAll(fw.getChanged()); - created.addAll(fw.getCreated()); - deleted.addAll(fw.getDeleted()); - } - - return !changed.isEmpty() || !created.isEmpty() || !deleted.isEmpty(); - } - - return super.checkModified(); - } - - public KMap getWatchers() { - return watchers; - } - - public KList getChanged() { - return changed; - } - - public KList getCreated() { - return created; - } - - public KList getDeleted() { - return deleted; - } - - public void clear() { - watchers.clear(); - changed.clear(); - deleted.clear(); - created.clear(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/io/IO.java b/core/src/main/java/com/volmit/iris/util/io/IO.java deleted file mode 100644 index b4bfe489d..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/IO.java +++ /dev/null @@ -1,1724 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.volmit.iris.Iris; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.scheduling.J; -import org.apache.commons.io.function.IOConsumer; -import org.apache.commons.io.function.IOFunction; -import lombok.SneakyThrows; -import org.dom4j.Document; -import org.dom4j.DocumentHelper; -import org.dom4j.io.OutputFormat; -import org.dom4j.io.SAXReader; -import org.dom4j.io.XMLWriter; - -import java.io.*; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.channels.OverlappingFileLockException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.*; -import java.util.function.Consumer; -import java.util.zip.*; - -@SuppressWarnings("ALL") -public class IO { - /** - * The Unix directory separator character. - */ - public static final char DIR_SEPARATOR_UNIX = '/'; - /** - * The Windows directory separator character. - */ - public static final char DIR_SEPARATOR_WINDOWS = '\\'; - /** - * The system directory separator character. - */ - public static final char DIR_SEPARATOR = File.separatorChar; - /** - * The Unix line separator string. - */ - public static final String LINE_SEPARATOR_UNIX = "\n"; - /** - * The Windows line separator string. - */ - public static final String LINE_SEPARATOR_WINDOWS = "\r\n"; - /** - * The system line separator string. - */ - public static final String LINE_SEPARATOR; - - /** - * The default buffer size to use. - */ - private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; - private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); - - static { - // avoid security issues - StringWriter buf = new StringWriter(4); - PrintWriter out = new PrintWriter(buf); - out.println(); - LINE_SEPARATOR = buf.toString(); - } - - public static String decompress(String gz) throws IOException { - ByteArrayInputStream bin = new ByteArrayInputStream(Base64.getUrlDecoder().decode(gz)); - GZIPInputStream gzi = new GZIPInputStream(bin); - ByteArrayOutputStream boas = new ByteArrayOutputStream(); - IO.fullTransfer(gzi, boas, 256); - gzi.close(); - - return boas.toString(); - } - - public static byte[] sdecompress(String compressed) throws IOException { - ByteArrayInputStream bin = new ByteArrayInputStream(Base64.getUrlDecoder().decode(compressed)); - GZIPInputStream gzi = new GZIPInputStream(bin); - ByteArrayOutputStream boas = new ByteArrayOutputStream(); - IO.fullTransfer(gzi, boas, 256); - gzi.close(); - - return boas.toByteArray(); - } - - public static String encode(byte[] data) { - return Base64.getUrlEncoder().encodeToString(data); - } - - public static byte[] decode(String u) { - return Base64.getUrlDecoder().decode(u); - } - - public static String hash(String b) { - try { - MessageDigest d = MessageDigest.getInstance("SHA-256"); - return bytesToHex(d.digest(b.getBytes(StandardCharsets.UTF_8))); - } catch (NoSuchAlgorithmException e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return "¯\\_(ツ)_/¯"; - } - - public static long hashRecursive(File... bases) { - LinkedList files = new LinkedList<>(); - Set processed = new HashSet<>(); - Arrays.parallelSort(bases, Comparator.comparing(File::getName)); - files.addAll(Arrays.asList(bases)); - - try { - CRC32 crc = new CRC32(); - while (!files.isEmpty()) { - File file = files.removeFirst(); - if (!processed.add(file)) - continue; - - if (file.isDirectory()) { - File[] arr = file.listFiles(); - if (arr == null) - continue; - - Arrays.parallelSort(arr, Comparator.comparing(File::getName)); - files.addAll(Arrays.asList(arr)); - continue; - } - - try (var din = new CheckedInputStream(readDeterministic(file), crc)) { - fullTransfer(din, new VoidOutputStream(), 8192); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - return crc.getValue(); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return 0; - } - - public static InputStream readDeterministic(File file) throws IOException { - if (!file.getName().endsWith(".json")) - return new FileInputStream(file); - - JsonElement json; - try (FileReader reader = new FileReader(file)) { - json = JsonParser.parseReader(reader); - } catch (Throwable e) { - throw new IOException("Failed to read json file " + file, e); - } - - var queue = new LinkedList(); - queue.add(json); - - while (!queue.isEmpty()) { - var element = queue.pop(); - Collection add = List.of(); - - if (element instanceof JsonObject obj) { - var map = obj.asMap(); - var sorted = new TreeMap<>(map); - map.clear(); - map.putAll(sorted); - - add = sorted.values(); - } else if (element instanceof JsonArray array) { - add = array.asList(); - } - - add.stream().filter(e -> e.isJsonObject() || e.isJsonArray()).forEach(queue::add); - } - - return toInputStream(json.toString()); - } - - public static String hash(File b) { - try { - MessageDigest d = MessageDigest.getInstance("SHA-256"); - DigestInputStream din = new DigestInputStream(readDeterministic(b), d); - fullTransfer(din, new VoidOutputStream(), 8192); - din.close(); - return bytesToHex(din.getMessageDigest().digest()); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return "¯\\_(ツ)_/¯"; - } - - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - - return new String(hexChars).toUpperCase(); - } - - public static String print(byte[] bytes) { - return Form.memSize(bytes.length, 2) + "[" + bytesToHex(bytes) + "]"; - } - - public static String longsToHex(long[] bytes) { - byte[] v = new byte[bytes.length * 8]; - - for (int i = 0; i < bytes.length; i++) { - v[i * 8] = (byte) (bytes[i] >>> 56); - v[(i * 8) + 1] = (byte) (bytes[i] >>> 48); - v[(i * 8) + 2] = (byte) (bytes[i] >>> 40); - v[(i * 8) + 3] = (byte) (bytes[i] >>> 32); - v[(i * 8) + 4] = (byte) (bytes[i] >>> 24); - v[(i * 8) + 5] = (byte) (bytes[i] >>> 16); - v[(i * 8) + 6] = (byte) (bytes[i] >>> 8); - v[(i * 8) + 7] = (byte) (bytes[i] >>> 0); - } - - return bytesToHex(v); - } - - /** - * Transfers the length of the buffer amount of data from the input stream to - * the output stream - * - * @param in the input - * @param out the output - * @return the actual transfered amount - * @throws IOException shit happens - */ - public static int transfer(InputStream in, OutputStream out, byte[] buffer) throws IOException { - int r = in.read(buffer); - - if (r != -1) { - out.write(buffer, 0, r); - } - - return r; - } - - /** - * Transfers the length of the buffer amount of data from the input stream to - * the output stream - * - * @param in the input - * @param out the output - * @param targetBuffer the buffer and size to use - * @param totalSize the total amount to transfer - * @return the actual transfered amount - * @throws IOException shit happens - */ - public static long transfer(InputStream in, OutputStream out, int targetBuffer, long totalSize) throws IOException { - long total = totalSize; - long wrote = 0; - byte[] buf = new byte[targetBuffer]; - int r = 0; - - while ((r = in.read(buf, 0, (int) (total < targetBuffer ? total : targetBuffer))) != -1) { - total -= r; - out.write(buf, 0, r); - wrote += r; - - if (total <= 0) { - break; - } - } - - return wrote; - } - - /** - * Fully move data from a finite inputstream to an output stream using a buffer - * size of 8192. This does NOT close streams. - * - * @return total size transfered - */ - public static long fillTransfer(InputStream in, OutputStream out) throws IOException { - return fullTransfer(in, out, 8192); - } - - public static void deleteUp(File f) { - if (f.exists()) { - f.delete(); - - if (f.getParentFile().list().length == 0) { - deleteUp(f.getParentFile()); - } - } - } - - /** - * Fully move data from a finite inputstream to an output stream using a given - * buffer size. This does NOT close streams. - * - * @param in the input stream to read from - * @param out the output stream to writeNodeData to - * @param bufferSize the target buffer size - * @return total size transfered - * @throws IOException shit happens - */ - public static long fullTransfer(InputStream in, OutputStream out, int bufferSize) throws IOException { - long wrote = 0; - byte[] buf = new byte[bufferSize]; - int r = 0; - - while ((r = in.read(buf)) != -1) { - out.write(buf, 0, r); - wrote += r; - } - - return wrote; - } - - /** - * Recursive delete (deleting folders) - * - * @param f the file to delete (and subfiles if folder) - */ - public static void delete(File f) { - if (f == null || !f.exists()) { - return; - } - - if (f.isDirectory()) { - for (File i : f.listFiles()) { - delete(i); - } - } - - f.delete(); - } - - public static long size(File file) { - long s = 0; - - if (file.exists()) { - if (file.isDirectory()) { - for (File i : file.listFiles()) { - s += size(i); - } - } else { - s += file.length(); - } - } - - return s; - } - - public static long count(File file) { - long s = 0; - - if (file.exists()) { - if (file.isDirectory()) { - for (File i : file.listFiles()) { - s += count(i); - } - } else { - s++; - } - } - - return s; - } - - public static long transfer(InputStream in, OutputStream out, byte[] buf, int totalSize) throws IOException { - long total = totalSize; - long wrote = 0; - int r = 0; - - while ((r = in.read(buf, 0, (int) (total < buf.length ? total : buf.length))) != -1) { - total -= r; - out.write(buf, 0, r); - wrote += r; - - if (total <= 0) { - break; - } - } - - return wrote; - } - - public static void readEntry(File zipfile, String entryname, Consumer v) throws IOException { - ZipFile file = new ZipFile(zipfile); - Throwable x = null; - - try { - Enumeration entries = file.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - - if (entryname.equals(entry.getName())) { - InputStream in = file.getInputStream(entry); - v.accept(in); - } - } - } catch (Exception ex) { - Iris.reportError(ex); - x = ex.getCause(); - } finally { - file.close(); - } - - if (x != null) { - throw new IOException("Failed to read zip entry, however it has been closed safely.", x); - } - } - - public static void writeAll(File f, Object c) throws IOException { - try { - f.getParentFile().mkdirs(); - } catch (Throwable ignored) { - - } - - PrintWriter pw = new PrintWriter(new FileWriter(f)); - pw.println(c.toString()); - pw.close(); - } - - public static void writeAll(File f, OutputStream c) throws IOException { - f.getParentFile().mkdirs(); - FileInputStream in = new FileInputStream(f); - IO.fullTransfer(in, c, 8192); - in.close(); - } - - public static String readAll(File f) throws IOException { - FileReader fr; - try { - fr = new FileReader(f); - } catch (IOException e) { - Iris.reportError(e); - throw e; - } - BufferedReader bu = new BufferedReader(fr); - StringBuilder c = new StringBuilder(); - String l = ""; - - while ((l = bu.readLine()) != null) { - c.append(l).append("\n"); - } - - bu.close(); - - return c.toString(); - } - - public static String readAll(InputStream in) throws IOException { - BufferedReader bu = new BufferedReader(new InputStreamReader(in)); - StringBuilder c = new StringBuilder(); - String l = ""; - - while ((l = bu.readLine()) != null) { - c.append(l).append("\n"); - } - - bu.close(); - - return c.toString(); - } - - /** - * Implements the same behaviour as the "touch" utility on Unix. It creates a - * new file with size 0 or, if the file exists already, it is opened and closed - * without modifying it, but updating the file date and time. - * - * @param file the File to touch - * @throws IOException If an I/O problem occurs - */ - public static void touch(File file) throws IOException { - if (!file.exists()) { - OutputStream out = new FileOutputStream(file); - out.close(); - } - file.setLastModified(System.currentTimeMillis()); - } - - /** - * Copies a file to a new location preserving the file date. - *

- * This method copies the contents of the specified source file to the specified - * destination file. The directory holding the destination file is created if it - * does not exist. If the destination file exists, then this method will - * overwrite it. - * - * @param srcFile an existing file to copy, must not be null - * @param destFile the new file, must not be null - * @throws NullPointerException if source or destination is null - * @throws IOException if source or destination is invalid - * @throws IOException if an IO error occurs during copying - */ - public static void copyFile(File srcFile, File destFile) throws IOException { - copyFile(srcFile, destFile, true); - } - - /** - * Copies a file to a new location. - *

- * This method copies the contents of the specified source file to the specified - * destination file. The directory holding the destination file is created if it - * does not exist. If the destination file exists, then this method will - * overwrite it. - * - * @param srcFile an existing file to copy, must not be null - * @param destFile the new file, must not be null - * @param preserveFileDate true if the file date of the copy should be the same as the - * original - * @throws NullPointerException if source or destination is null - * @throws IOException if source or destination is invalid - * @throws IOException if an IO error occurs during copying - */ - public static void copyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException { - if (srcFile == null) { - throw new NullPointerException("Source must not be null"); - } - if (destFile == null) { - throw new NullPointerException("Destination must not be null"); - } - if (!srcFile.exists()) { - throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); - } - if (srcFile.isDirectory()) { - throw new IOException("Source '" + srcFile + "' exists but is a directory"); - } - if (srcFile.getCanonicalPath().equals(destFile.getCanonicalPath())) { - throw new IOException("Source '" + srcFile + "' and destination '" + destFile + "' are the same"); - } - if (destFile.getParentFile() != null && !destFile.getParentFile().exists()) { - if (!destFile.getParentFile().mkdirs()) { - throw new IOException("Destination '" + destFile + "' directory cannot be created"); - } - } - if (destFile.exists() && !destFile.canWrite()) { - throw new IOException("Destination '" + destFile + "' exists but is read-only"); - } - doCopyFile(srcFile, destFile, preserveFileDate); - } - - // ----------------------------------------------------------------------- - - /** - * Internal copy file method. - * - * @param srcFile the validated source file, not null - * @param destFile the validated destination file, not null - * @param preserveFileDate whether to preserve the file date - * @throws IOException if an error occurs - */ - private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException { - if (destFile.exists() && destFile.isDirectory()) { - throw new IOException("Destination '" + destFile + "' exists but is a directory"); - } - - FileInputStream input = new FileInputStream(srcFile); - try { - FileOutputStream output = new FileOutputStream(destFile); - try { - IO.copy(input, output); - } finally { - output.close(); - } - } finally { - input.close(); - } - - if (srcFile.length() != destFile.length()) { - throw new IOException("Failed to copy full contents from '" + srcFile + "' to '" + destFile + "'"); - } - if (preserveFileDate) { - destFile.setLastModified(srcFile.lastModified()); - } - } - - public static void copyDirectory(Path source, Path target) throws IOException { - Files.walk(source).forEach(sourcePath -> { - Path targetPath = target.resolve(source.relativize(sourcePath)); - - try { - if (Files.isDirectory(sourcePath)) { - if (!Files.exists(targetPath)) { - Files.createDirectories(targetPath); - } - } else { - Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); - } - } catch (IOException e) { - Iris.error("Failed to copy " + targetPath); - e.printStackTrace(); - } - }); - } - - /** - * Unconditionally close an Reader. - *

- * Equivalent to {@link Reader#close()}, except any exceptions will be ignored. - * This is typically used in finally blocks. - * - * @param input the Reader to close, may be null or already closed - */ - public static void closeQuietly(Reader input) { - try { - if (input != null) { - input.close(); - } - } catch (IOException e) { - Iris.reportError(e); - // ignore - } - } - - /** - * Unconditionally close a Writer. - *

- * Equivalent to {@link Writer#close()}, except any exceptions will be ignored. - * This is typically used in finally blocks. - * - * @param output the Writer to close, may be null or already closed - */ - public static void closeQuietly(Writer output) { - try { - if (output != null) { - output.close(); - } - } catch (IOException e) { - Iris.reportError(e); - // ignore - } - } - - /** - * Unconditionally close an InputStream. - *

- * Equivalent to {@link InputStream#close()}, except any exceptions will be - * ignored. This is typically used in finally blocks. - * - * @param input the InputStream to close, may be null or already closed - */ - public static void closeQuietly(InputStream input) { - try { - if (input != null) { - input.close(); - } - } catch (IOException e) { - Iris.reportError(e); - // ignore - } - } - - // read toByteArray - // ----------------------------------------------------------------------- - - /** - * Unconditionally close an OutputStream. - *

- * Equivalent to {@link OutputStream#close()}, except any exceptions will be - * ignored. This is typically used in finally blocks. - * - * @param output the OutputStream to close, may be null or already closed - */ - public static void closeQuietly(OutputStream output) { - try { - if (output != null) { - output.close(); - } - } catch (IOException e) { - Iris.reportError(e); - // ignore - } - } - - /** - * Get the contents of an InputStream as a byte[]. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - * - * @param input the InputStream to read from - * @return the requested byte array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - */ - public static byte[] toByteArray(InputStream input) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - copy(input, output); - return output.toByteArray(); - } - - /** - * Get the contents of a Reader as a byte[] using the - * default character encoding of the platform. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedReader. - * - * @param input the Reader to read from - * @return the requested byte array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - */ - public static byte[] toByteArray(Reader input) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - copy(input, output); - return output.toByteArray(); - } - - /** - * Get the contents of a Reader as a byte[] using the - * specified character encoding. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedReader. - * - * @param input the Reader to read from - * @param encoding the encoding to use, null means platform default - * @return the requested byte array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static byte[] toByteArray(Reader input, String encoding) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - copy(input, output, encoding); - return output.toByteArray(); - } - - // read char[] - // ----------------------------------------------------------------------- - - /** - * Get the contents of a String as a byte[] using the - * default character encoding of the platform. - *

- * This is the same as {@link String#getBytes()}. - * - * @param input the String to convert - * @return the requested byte array - * @deprecated Use {@link String#getBytes()} - */ - @Deprecated - public static byte[] toByteArray(String input) { - return input.getBytes(); - } - - /** - * Get the contents of an InputStream as a character array using - * the default character encoding of the platform. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - * - * @param is the InputStream to read from - * @return the requested character array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static char[] toCharArray(InputStream is) throws IOException { - CharArrayWriter output = new CharArrayWriter(); - copy(is, output); - return output.toCharArray(); - } - - /** - * Get the contents of an InputStream as a character array using - * the specified character encoding. - *

- * Character encoding names can be found at - * IANA. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - * - * @param is the InputStream to read from - * @param encoding the encoding to use, null means platform default - * @return the requested character array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static char[] toCharArray(InputStream is, String encoding) throws IOException { - CharArrayWriter output = new CharArrayWriter(); - copy(is, output, encoding); - return output.toCharArray(); - } - - // read toString - // ----------------------------------------------------------------------- - - /** - * Get the contents of a Reader as a character array. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedReader. - * - * @param input the Reader to read from - * @return the requested character array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static char[] toCharArray(Reader input) throws IOException { - CharArrayWriter sw = new CharArrayWriter(); - copy(input, sw); - return sw.toCharArray(); - } - - /** - * Get the contents of an InputStream as a String using the default - * character encoding of the platform. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - * - * @param input the InputStream to read from - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - */ - public static String toString(InputStream input) throws IOException { - StringWriter sw = new StringWriter(); - copy(input, sw); - return sw.toString(); - } - - /** - * Get the contents of an InputStream as a String using the - * specified character encoding. - *

- * Character encoding names can be found at - * IANA. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - * - * @param input the InputStream to read from - * @param encoding the encoding to use, null means platform default - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - */ - public static String toString(InputStream input, String encoding) throws IOException { - StringWriter sw = new StringWriter(); - copy(input, sw, encoding); - return sw.toString(); - } - - /** - * Get the contents of a Reader as a String. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedReader. - * - * @param input the Reader to read from - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - */ - public static String toString(Reader input) throws IOException { - StringWriter sw = new StringWriter(); - copy(input, sw); - return sw.toString(); - } - - /** - * Get the contents of a byte[] as a String using the default - * character encoding of the platform. - * - * @param input the byte array to read from - * @return the requested String - * @deprecated Use {@link String#String(byte[])} - */ - @Deprecated - public static String toString(byte[] input) { - return new String(input); - } - - // readLines - // ----------------------------------------------------------------------- - - /** - * Get the contents of a byte[] as a String using the specified - * character encoding. - *

- * Character encoding names can be found at - * IANA. - * - * @param input the byte array to read from - * @param encoding the encoding to use, null means platform default - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs (never occurs) - * @deprecated Use {@link String#String(byte[], String)} - */ - @Deprecated - public static String toString(byte[] input, String encoding) throws IOException { - if (encoding == null) { - return new String(input); - } else { - return new String(input, encoding); - } - } - - /** - * Get the contents of an InputStream as a list of Strings, one - * entry per line, using the default character encoding of the platform. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - * - * @param input the InputStream to read from, not null - * @return the list of Strings, never null - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static List readLines(InputStream input) throws IOException { - InputStreamReader reader = new InputStreamReader(input); - return readLines(reader); - } - - /** - * Get the contents of an InputStream as a list of Strings, one - * entry per line, using the specified character encoding. - *

- * Character encoding names can be found at - * IANA. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - * - * @param input the InputStream to read from, not null - * @param encoding the encoding to use, null means platform default - * @return the list of Strings, never null - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static List readLines(InputStream input, String encoding) throws IOException { - if (encoding == null) { - return readLines(input); - } else { - InputStreamReader reader = new InputStreamReader(input, encoding); - return readLines(reader); - } - } - - // ----------------------------------------------------------------------- - - /** - * Get the contents of a Reader as a list of Strings, one entry per - * line. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedReader. - * - * @param input the Reader to read from, not null - * @return the list of Strings, never null - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static List readLines(Reader input) throws IOException { - BufferedReader reader = new BufferedReader(input); - List list = new ArrayList(); - String line = reader.readLine(); - while (line != null) { - list.add(line); - line = reader.readLine(); - } - return list; - } - - /** - * Convert the specified string to an input stream, encoded as bytes using the - * default character encoding of the platform. - * - * @param input the string to convert - * @return an input stream - * @since Commons IO 1.1 - */ - public static InputStream toInputStream(String input) { - byte[] bytes = input.getBytes(); - return new ByteArrayInputStream(bytes); - } - - // writeNodeData byte[] - // ----------------------------------------------------------------------- - - /** - * Convert the specified string to an input stream, encoded as bytes using the - * specified character encoding. - *

- * Character encoding names can be found at - * IANA. - * - * @param input the string to convert - * @param encoding the encoding to use, null means platform default - * @return an input stream - * @throws IOException if the encoding is invalid - * @since Commons IO 1.1 - */ - public static InputStream toInputStream(String input, String encoding) throws IOException { - byte[] bytes = encoding != null ? input.getBytes(encoding) : input.getBytes(); - return new ByteArrayInputStream(bytes); - } - - /** - * Writes bytes from a byte[] to an OutputStream. - * - * @param data the byte array to writeNodeData, do not modify during output, null ignored - * @param output the OutputStream to writeNodeData to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(byte[] data, OutputStream output) throws IOException { - if (data != null) { - output.write(data); - } - } - - /** - * Writes bytes from a byte[] to chars on a Writer - * using the default character encoding of the platform. - *

- * This method uses {@link String#String(byte[])}. - * - * @param data the byte array to writeNodeData, do not modify during output, null ignored - * @param output the Writer to writeNodeData to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(byte[] data, Writer output) throws IOException { - if (data != null) { - output.write(new String(data)); - } - } - - // writeNodeData char[] - // ----------------------------------------------------------------------- - - /** - * Writes bytes from a byte[] to chars on a Writer - * using the specified character encoding. - *

- * Character encoding names can be found at - * IANA. - *

- * This method uses {@link String#String(byte[], String)}. - * - * @param data the byte array to writeNodeData, do not modify during output, null ignored - * @param output the Writer to writeNodeData to - * @param encoding the encoding to use, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(byte[] data, Writer output, String encoding) throws IOException { - if (data != null) { - if (encoding == null) { - write(data, output); - } else { - output.write(new String(data, encoding)); - } - } - } - - /** - * Writes chars from a char[] to a Writer using the - * default character encoding of the platform. - * - * @param data the char array to writeNodeData, do not modify during output, null ignored - * @param output the Writer to writeNodeData to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(char[] data, Writer output) throws IOException { - if (data != null) { - output.write(data); - } - } - - /** - * Writes chars from a char[] to bytes on an - * OutputStream. - *

- * This method uses {@link String#String(char[])} and {@link String#getBytes()}. - * - * @param data the char array to writeNodeData, do not modify during output, null ignored - * @param output the OutputStream to writeNodeData to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(char[] data, OutputStream output) throws IOException { - if (data != null) { - output.write(new String(data).getBytes()); - } - } - - // writeNodeData String - // ----------------------------------------------------------------------- - - /** - * Writes chars from a char[] to bytes on an - * OutputStream using the specified character encoding. - *

- * Character encoding names can be found at - * IANA. - *

- * This method uses {@link String#String(char[])} and - * {@link String#getBytes(String)}. - * - * @param data the char array to writeNodeData, do not modify during output, null ignored - * @param output the OutputStream to writeNodeData to - * @param encoding the encoding to use, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(char[] data, OutputStream output, String encoding) throws IOException { - if (data != null) { - if (encoding == null) { - write(data, output); - } else { - output.write(new String(data).getBytes(encoding)); - } - } - } - - /** - * Writes chars from a String to a Writer. - * - * @param data the String to writeNodeData, null ignored - * @param output the Writer to writeNodeData to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(String data, Writer output) throws IOException { - if (data != null) { - output.write(data); - } - } - - /** - * Writes chars from a String to bytes on an - * OutputStream using the default character encoding of the - * platform. - *

- * This method uses {@link String#getBytes()}. - * - * @param data the String to writeNodeData, null ignored - * @param output the OutputStream to writeNodeData to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(String data, OutputStream output) throws IOException { - if (data != null) { - output.write(data.getBytes()); - } - } - - // writeNodeData StringBuffer - // ----------------------------------------------------------------------- - - /** - * Writes chars from a String to bytes on an - * OutputStream using the specified character encoding. - *

- * Character encoding names can be found at - * IANA. - *

- * This method uses {@link String#getBytes(String)}. - * - * @param data the String to writeNodeData, null ignored - * @param output the OutputStream to writeNodeData to - * @param encoding the encoding to use, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(String data, OutputStream output, String encoding) throws IOException { - if (data != null) { - if (encoding == null) { - write(data, output); - } else { - output.write(data.getBytes(encoding)); - } - } - } - - /** - * Writes chars from a StringBuffer to a Writer. - * - * @param data the StringBuffer to writeNodeData, null ignored - * @param output the Writer to writeNodeData to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(StringBuffer data, Writer output) throws IOException { - if (data != null) { - output.write(data.toString()); - } - } - - /** - * Writes chars from a StringBuffer to bytes on an - * OutputStream using the default character encoding of the - * platform. - *

- * This method uses {@link String#getBytes()}. - * - * @param data the StringBuffer to writeNodeData, null ignored - * @param output the OutputStream to writeNodeData to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(StringBuffer data, OutputStream output) throws IOException { - if (data != null) { - output.write(data.toString().getBytes()); - } - } - - // writeLines - // ----------------------------------------------------------------------- - - /** - * Writes chars from a StringBuffer to bytes on an - * OutputStream using the specified character encoding. - *

- * Character encoding names can be found at - * IANA. - *

- * This method uses {@link String#getBytes(String)}. - * - * @param data the StringBuffer to writeNodeData, null ignored - * @param output the OutputStream to writeNodeData to - * @param encoding the encoding to use, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void write(StringBuffer data, OutputStream output, String encoding) throws IOException { - if (data != null) { - if (encoding == null) { - write(data, output); - } else { - output.write(data.toString().getBytes(encoding)); - } - } - } - - /** - * Writes the toString() value of each item in a collection to an - * OutputStream line by line, using the default character encoding - * of the platform and the specified line ending. - * - * @param lines the lines to writeNodeData, null entries produce blank lines - * @param lineEnding the line separator to use, null is system default - * @param output the OutputStream to writeNodeData to, not null, not closed - * @throws NullPointerException if the output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void writeLines(Collection lines, String lineEnding, OutputStream output) throws IOException { - if (lines == null) { - return; - } - if (lineEnding == null) { - lineEnding = LINE_SEPARATOR; - } - for (Iterator it = lines.iterator(); it.hasNext(); ) { - Object line = it.next(); - if (line != null) { - output.write(line.toString().getBytes()); - } - output.write(lineEnding.getBytes()); - } - } - - /** - * Writes the toString() value of each item in a collection to an - * OutputStream line by line, using the specified character - * encoding and the specified line ending. - *

- * Character encoding names can be found at - * IANA. - * - * @param lines the lines to writeNodeData, null entries produce blank lines - * @param lineEnding the line separator to use, null is system default - * @param output the OutputStream to writeNodeData to, not null, not closed - * @param encoding the encoding to use, null means platform default - * @throws NullPointerException if the output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void writeLines(Collection lines, String lineEnding, OutputStream output, String encoding) throws IOException { - if (encoding == null) { - writeLines(lines, lineEnding, output); - } else { - if (lines == null) { - return; - } - if (lineEnding == null) { - lineEnding = LINE_SEPARATOR; - } - for (Iterator it = lines.iterator(); it.hasNext(); ) { - Object line = it.next(); - if (line != null) { - output.write(line.toString().getBytes(encoding)); - } - output.write(lineEnding.getBytes(encoding)); - } - } - } - - // copy from InputStream - // ----------------------------------------------------------------------- - - /** - * Writes the toString() value of each item in a collection to a - * Writer line by line, using the specified line ending. - * - * @param lines the lines to writeNodeData, null entries produce blank lines - * @param lineEnding the line separator to use, null is system default - * @param writer the Writer to writeNodeData to, not null, not closed - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void writeLines(Collection lines, String lineEnding, Writer writer) throws IOException { - if (lines == null) { - return; - } - if (lineEnding == null) { - lineEnding = LINE_SEPARATOR; - } - for (Iterator it = lines.iterator(); it.hasNext(); ) { - Object line = it.next(); - if (line != null) { - writer.write(line.toString()); - } - writer.write(lineEnding); - } - } - - /** - * Copy bytes from an InputStream to an OutputStream. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - *

- * Large streams (over 2GB) will return a bytes copied value of -1 - * after the copy has completed since the correct number of bytes cannot be - * returned as an int. For large streams use the - * copyLarge(InputStream, OutputStream) method. - * - * @param input the InputStream to read from - * @param output the OutputStream to writeNodeData to - * @return the number of bytes copied - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @throws ArithmeticException if the byte count is too large - * @since Commons IO 1.1 - */ - public static int copy(InputStream input, OutputStream output) throws IOException { - long count = copyLarge(input, output); - if (count > Integer.MAX_VALUE) { - return -1; - } - return (int) count; - } - - /** - * Copy bytes from a large (over 2GB) InputStream to an - * OutputStream. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - * - * @param input the InputStream to read from - * @param output the OutputStream to writeNodeData to - * @return the number of bytes copied - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.3 - */ - public static long copyLarge(InputStream input, OutputStream output) throws IOException { - byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; - long count = 0; - int n = 0; - while (-1 != (n = input.read(buffer))) { - output.write(buffer, 0, n); - count += n; - } - return count; - } - - /** - * Copy bytes from an InputStream to chars on a Writer - * using the default character encoding of the platform. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - *

- * This method uses {@link InputStreamReader}. - * - * @param input the InputStream to read from - * @param output the Writer to writeNodeData to - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void copy(InputStream input, Writer output) throws IOException { - InputStreamReader in = new InputStreamReader(input); - copy(in, output); - } - - // copy from Reader - // ----------------------------------------------------------------------- - - /** - * Copy bytes from an InputStream to chars on a Writer - * using the specified character encoding. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - *

- * Character encoding names can be found at - * IANA. - *

- * This method uses {@link InputStreamReader}. - * - * @param input the InputStream to read from - * @param output the Writer to writeNodeData to - * @param encoding the encoding to use, null means platform default - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void copy(InputStream input, Writer output, String encoding) throws IOException { - if (encoding == null) { - copy(input, output); - } else { - InputStreamReader in = new InputStreamReader(input, encoding); - copy(in, output); - } - } - - /** - * Copy chars from a Reader to a Writer. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedReader. - *

- * Large streams (over 2GB) will return a chars copied value of -1 - * after the copy has completed since the correct number of chars cannot be - * returned as an int. For large streams use the - * copyLarge(Reader, Writer) method. - * - * @param input the Reader to read from - * @param output the Writer to writeNodeData to - * @return the number of characters copied - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @throws ArithmeticException if the character count is too large - * @since Commons IO 1.1 - */ - public static int copy(Reader input, Writer output) throws IOException { - long count = copyLarge(input, output); - if (count > Integer.MAX_VALUE) { - return -1; - } - return (int) count; - } - - /** - * Copy chars from a large (over 2GB) Reader to a - * Writer. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedReader. - * - * @param input the Reader to read from - * @param output the Writer to writeNodeData to - * @return the number of characters copied - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.3 - */ - public static long copyLarge(Reader input, Writer output) throws IOException { - char[] buffer = new char[DEFAULT_BUFFER_SIZE]; - long count = 0; - int n = 0; - while (-1 != (n = input.read(buffer))) { - output.write(buffer, 0, n); - count += n; - } - return count; - } - - /** - * Copy chars from a Reader to bytes on an - * OutputStream using the default character encoding of the - * platform, and calling flush. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedReader. - *

- * Due to the implementation of OutputStreamWriter, this method performs a - * flush. - *

- * This method uses {@link OutputStreamWriter}. - * - * @param input the Reader to read from - * @param output the OutputStream to writeNodeData to - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void copy(Reader input, OutputStream output) throws IOException { - OutputStreamWriter out = new OutputStreamWriter(output); - copy(input, out); - // have to flush here. - out.flush(); - } - - // content equals - // ----------------------------------------------------------------------- - - /** - * Copy chars from a Reader to bytes on an - * OutputStream using the specified character encoding, and calling - * flush. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedReader. - *

- * Character encoding names can be found at - * IANA. - *

- * Due to the implementation of OutputStreamWriter, this method performs a - * flush. - *

- * This method uses {@link OutputStreamWriter}. - * - * @param input the Reader to read from - * @param output the OutputStream to writeNodeData to - * @param encoding the encoding to use, null means platform default - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static void copy(Reader input, OutputStream output, String encoding) throws IOException { - if (encoding == null) { - copy(input, output); - } else { - OutputStreamWriter out = new OutputStreamWriter(output, encoding); - copy(input, out); - // we have to flush here. - out.flush(); - } - } - - /** - * Compare the contents of two Streams to determine if they are equal or not. - *

- * This method buffers the input internally using - * BufferedInputStream if they are not already buffered. - * - * @param input1 the first stream - * @param input2 the second stream - * @return true if the content of the streams are equal or they both don't - * exist, false otherwise - * @throws NullPointerException if either input is null - * @throws IOException if an I/O error occurs - */ - public static boolean contentEquals(InputStream input1, InputStream input2) throws IOException { - if (!(input1 instanceof BufferedInputStream)) { - input1 = new BufferedInputStream(input1); - } - if (!(input2 instanceof BufferedInputStream)) { - input2 = new BufferedInputStream(input2); - } - - int ch = input1.read(); - while (-1 != ch) { - int ch2 = input2.read(); - if (ch != ch2) { - return false; - } - ch = input1.read(); - } - - int ch2 = input2.read(); - return (ch2 == -1); - } - - /** - * Compare the contents of two Readers to determine if they are equal or not. - *

- * This method buffers the input internally using BufferedReader if - * they are not already buffered. - * - * @param input1 the first reader - * @param input2 the second reader - * @return true if the content of the readers are equal or they both don't - * exist, false otherwise - * @throws NullPointerException if either input is null - * @throws IOException if an I/O error occurs - * @since Commons IO 1.1 - */ - public static boolean contentEquals(Reader input1, Reader input2) throws IOException { - if (!(input1 instanceof BufferedReader)) { - input1 = new BufferedReader(input1); - } - if (!(input2 instanceof BufferedReader)) { - input2 = new BufferedReader(input2); - } - - int ch = input1.read(); - while (-1 != ch) { - int ch2 = input2.read(); - if (ch != ch2) { - return false; - } - ch = input1.read(); - } - - int ch2 = input2.read(); - return (ch2 == -1); - } - - @SneakyThrows - public static void write(File file, Document doc) { - file.getParentFile().mkdirs(); - try (var writer = new FileWriter(file)) { - new XMLWriter(writer, OutputFormat.createPrettyPrint()) - .write(doc); - } - } - - @SneakyThrows - public static Document read(File file) { - if (file.exists()) return new SAXReader().read(file); - var doc = DocumentHelper.createDocument(); - doc.addElement("project") - .addAttribute("version", "4"); - return doc; - } - - public static void write(File file, IOFunction builder, IOConsumer action) throws IOException { - File dir = new File(file.getParentFile(), ".tmp"); - dir.mkdirs(); - dir.deleteOnExit(); - File temp = File.createTempFile("iris",".bin", dir); - try (var target = FileChannel.open(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.SYNC)) { - lock(target); - - try (var out = builder.apply(new FileOutputStream(temp))) { - action.accept(out); - } - Files.copy(temp.toPath(), Channels.newOutputStream(target)); - target.truncate(temp.length()); - } finally { - temp.delete(); - } - } - - public static FileLock lock(FileChannel channel) throws IOException { - while (true) { - try { - return channel.lock(); - } catch (OverlappingFileLockException e) {} - J.sleep(1); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/io/IORunnable.java b/core/src/main/java/com/volmit/iris/util/io/IORunnable.java deleted file mode 100644 index ba2064491..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/IORunnable.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import java.io.IOException; - -public interface IORunnable { - void run() throws IOException; -} diff --git a/core/src/main/java/com/volmit/iris/util/io/InstanceState.java b/core/src/main/java/com/volmit/iris/util/io/InstanceState.java deleted file mode 100644 index 0b1c0dd48..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/InstanceState.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import com.volmit.iris.util.math.RNG; - -import java.io.File; -import java.io.IOException; - -public class InstanceState { - public static int getInstanceId() { - try { - return Integer.parseInt(IO.readAll(instanceFile()).trim()); - } catch (Throwable e) { - e.printStackTrace(); - } - - return -1; - } - - public static void updateInstanceId() { - try { - IO.writeAll(instanceFile(), RNG.r.imax() + ""); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static File instanceFile() { - File f = new File("plugins/Iris/cache/instance"); - f.getParentFile().mkdirs(); - return f; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/io/JarScanner.java b/core/src/main/java/com/volmit/iris/util/io/JarScanner.java deleted file mode 100644 index cf8aa3271..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/JarScanner.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KSet; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -public class JarScanner { - private final KSet> classes; - private final File jar; - private final String superPackage; - private final boolean report; - - /** - * Create a scanner - * - * @param jar the path to the jar - */ - public JarScanner(File jar, String superPackage, boolean report) { - this.jar = jar; - this.classes = new KSet<>(); - this.superPackage = superPackage; - this.report = report; - } - - public JarScanner(File jar, String superPackage) { - this(jar, superPackage, true); - } - - /** - * Scan the jar - * - * @throws IOException bad things happen - */ - public void scan() throws IOException { - classes.clear(); - FileInputStream fin = new FileInputStream(jar); - ZipInputStream zip = new ZipInputStream(fin); - - for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { - if (!entry.isDirectory() && entry.getName().endsWith(".class")) { - if (entry.getName().contains("$")) { - continue; - } - - String c = entry.getName().replaceAll("/", ".").replace(".class", ""); - - if (c.startsWith(superPackage)) { - try { - Class clazz = Class.forName(c); - classes.add(clazz); - } catch (Throwable e) { - if (!report) continue; - Iris.reportError(e); - e.printStackTrace(); - } - } - } - } - - zip.close(); - } - - public void scanAll() throws IOException { - classes.clear(); - FileInputStream fin = new FileInputStream(jar); - ZipInputStream zip = new ZipInputStream(fin); - for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { - if (!entry.isDirectory() && entry.getName().endsWith(".class")) { - String c = entry.getName().replaceAll("/", ".").replace(".class", ""); - - if (c.startsWith(superPackage)) { - try { - Class clazz = Class.forName(c); - classes.add(clazz); - } catch (Throwable e) { - if (!report) continue; - Iris.reportError(e); - e.printStackTrace(); - } - } - } - } - } - - /** - * Get the scanned clases - * - * @return a gset of classes - */ - public KSet> getClasses() { - return classes; - } - - /** - * Get the file object for the jar - * - * @return a file object representing the jar - */ - public File getJar() { - return jar; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/io/ReactiveFolder.java b/core/src/main/java/com/volmit/iris/util/io/ReactiveFolder.java deleted file mode 100644 index 5bf0d6eb6..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/ReactiveFolder.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.function.Consumer3; - -import java.io.File; - -public class ReactiveFolder { - private final File folder; - private final Consumer3, KList, KList> hotload; - private FolderWatcher fw; - private int checkCycle = 0; - - public ReactiveFolder(File folder, Consumer3, KList, KList> hotload) { - this.folder = folder; - this.hotload = hotload; - this.fw = new FolderWatcher(folder); - fw.checkModified(); - } - - public void checkIgnore() { - fw = new FolderWatcher(folder); - } - - public boolean check() { - checkCycle++; - boolean modified = false; - - if (checkCycle % 3 == 0 ? fw.checkModified() : fw.checkModifiedFast()) { - for (File i : fw.getCreated()) { - if (i.getName().endsWith(".iob") || i.getName().endsWith(".json") || i.getName().endsWith(".kts")) { - if (i.getPath().contains(".iris") || i.getName().endsWith(".gradle.kts")) { - continue; - } - - modified = true; - break; - } - } - - if (!modified) { - for (File i : fw.getChanged()) { - if (i.getPath().contains(".iris") || i.getName().endsWith(".gradle.kts")) { - continue; - } - - if (i.getName().endsWith(".iob") || i.getName().endsWith(".json") || i.getName().endsWith(".kts")) { - modified = true; - break; - } - } - } - - if (!modified) { - for (File i : fw.getDeleted()) { - if (i.getPath().contains(".iris") || i.getName().endsWith(".gradle.kts")) { - continue; - } - - if (i.getName().endsWith(".iob") || i.getName().endsWith(".json") || i.getName().endsWith(".kts")) { - modified = true; - break; - } - } - } - } - - if (modified) { - hotload.accept(fw.getCreated(), fw.getChanged(), fw.getDeleted()); - } - - return fw.checkModified(); - } - - public void clear() { - fw.clear(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/io/VoidOutputStream.java b/core/src/main/java/com/volmit/iris/util/io/VoidOutputStream.java deleted file mode 100644 index 7855e9908..000000000 --- a/core/src/main/java/com/volmit/iris/util/io/VoidOutputStream.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.io; - -import java.io.OutputStream; - -public class VoidOutputStream extends OutputStream { - @Override - public void write(int b) { - // poof - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/HTTP.java b/core/src/main/java/com/volmit/iris/util/json/HTTP.java deleted file mode 100644 index 2b3099522..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/HTTP.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -import java.util.Iterator; - -/** - * Convert an HTTP header to a JSONObject and back. - * - * @author JSON.org - * @version 2014-05-03 - */ -public class HTTP { - - /** - * Carriage return/line feed. - */ - public static final String CRLF = "\r\n"; - - /** - * Convert an HTTP header string into a JSONObject. It can be a request - * header or a response header. A request header will contain - * - *

-     * {
-     *    Method: "POST" (for example),
-     *    "Request-URI": "/" (for example),
-     *    "HTTP-Version": "HTTP/1.1" (for example)
-     * }
-     * 
- *

- * A response header will contain - * - *

-     * {
-     *    "HTTP-Version": "HTTP/1.1" (for example),
-     *    "Status-Code": "200" (for example),
-     *    "Reason-Phrase": "OK" (for example)
-     * }
-     * 
- *

- * In addition, the other parameters in the header will be captured, using - * the HTTP field names as JSON names, so that - * - *

-     *    Date: Sun, 26 May 2002 18:06:04 GMT
-     *    Cookie: Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s
-     *    Cache-Control: no-cache
-     * 
- *

- * become - * - *

-     * {...
-     *    Date: "Sun, 26 May 2002 18:06:04 GMT",
-     *    Cookie: "Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s",
-     *    "Cache-Control": "no-cache",
-     * ...}
-     * 
- *

- * It does no further checking or conversion. It does not parse dates. It - * does not do '%' transforms on URLs. - * - * @param string An HTTP header string. - * @return A JSONObject containing the elements and attributes of the XML - * string. - */ - public static JSONObject toJSONObject(String string) throws JSONException { - JSONObject jo = new JSONObject(); - HTTPTokener x = new HTTPTokener(string); - String token; - - token = x.nextToken(); - if (token.toUpperCase().startsWith("HTTP")) { - - // Response - - jo.put("HTTP-Version", token); - jo.put("Status-Code", x.nextToken()); - jo.put("Reason-Phrase", x.nextTo('\0')); - x.next(); - - } else { - - // Request - - jo.put("Method", token); - jo.put("Request-URI", x.nextToken()); - jo.put("HTTP-Version", x.nextToken()); - } - - // Fields - - while (x.more()) { - String name = x.nextTo(':'); - x.next(':'); - jo.put(name, x.nextTo('\0')); - x.next(); - } - return jo; - } - - /** - * Convert a JSONObject into an HTTP header. A request header must contain - * - *

-     * {
-     *    Method: "POST" (for example),
-     *    "Request-URI": "/" (for example),
-     *    "HTTP-Version": "HTTP/1.1" (for example)
-     * }
-     * 
- *

- * A response header must contain - * - *

-     * {
-     *    "HTTP-Version": "HTTP/1.1" (for example),
-     *    "Status-Code": "200" (for example),
-     *    "Reason-Phrase": "OK" (for example)
-     * }
-     * 
- *

- * Any other members of the JSONObject will be output as HTTP fields. The - * result will end with two CRLF pairs. - * - * @param jo A JSONObject - * @return An HTTP header string. - * @throws JSONException if the object does not contain enough information. - */ - public static String toString(JSONObject jo) throws JSONException { - Iterator keys = jo.keys(); - String string; - StringBuilder sb = new StringBuilder(); - if (jo.has("Status-Code") && jo.has("Reason-Phrase")) { - sb.append(jo.getString("HTTP-Version")); - sb.append(' '); - sb.append(jo.getString("Status-Code")); - sb.append(' '); - sb.append(jo.getString("Reason-Phrase")); - } else if (jo.has("Method") && jo.has("Request-URI")) { - sb.append(jo.getString("Method")); - sb.append(' '); - sb.append('"'); - sb.append(jo.getString("Request-URI")); - sb.append('"'); - sb.append(' '); - sb.append(jo.getString("HTTP-Version")); - } else { - throw new JSONException("Not enough material for an HTTP header."); - } - sb.append(CRLF); - while (keys.hasNext()) { - string = keys.next(); - if (!"HTTP-Version".equals(string) && !"Status-Code".equals(string) && !"Reason-Phrase".equals(string) && !"Method".equals(string) && !"Request-URI".equals(string) && !jo.isNull(string)) { - sb.append(string); - sb.append(": "); - sb.append(jo.getString(string)); - sb.append(CRLF); - } - } - sb.append(CRLF); - return sb.toString(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/HTTPTokener.java b/core/src/main/java/com/volmit/iris/util/json/HTTPTokener.java deleted file mode 100644 index d2f4a6a64..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/HTTPTokener.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -/** - * The HTTPTokener extends the JSONTokener to provide additional methods for the - * parsing of HTTP headers. - * - * @author JSON.org - * @version 2014-05-03 - */ -public class HTTPTokener extends JSONTokener { - - /** - * Construct an HTTPTokener from a string. - * - * @param string A source string. - */ - public HTTPTokener(String string) { - super(string); - } - - /** - * Get the next token or string. This is used in parsing HTTP headers. - * - * @return A String. - */ - public String nextToken() throws JSONException { - char c; - char q; - StringBuilder sb = new StringBuilder(); - do { - c = next(); - } while (Character.isWhitespace(c)); - if (c == '"' || c == '\'') { - q = c; - for (; ; ) { - c = next(); - if (c < ' ') { - throw syntaxError("Unterminated string."); - } - if (c == q) { - return sb.toString(); - } - sb.append(c); - } - } - for (; ; ) { - if (c == 0 || Character.isWhitespace(c)) { - return sb.toString(); - } - sb.append(c); - c = next(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/JSONArray.java b/core/src/main/java/com/volmit/iris/util/json/JSONArray.java deleted file mode 100644 index 62b09b3c8..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/JSONArray.java +++ /dev/null @@ -1,1030 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -import com.volmit.iris.Iris; - -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.lang.reflect.Array; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; - -/** - * A JSONArray is an ordered sequence of values. Its external text form is a - * string wrapped in square brackets with commas separating the values. The - * internal form is an object having get and opt - * methods for accessing the values by index, and put methods for - * adding or replacing values. The values can be any of these types: - * Boolean, JSONArray, JSONObject, - * Number, String, or the - * JSONObject.NULL object. - *

- * The constructor can convert a JSON text into a Java object. The - * toString method converts to JSON text. - *

- * A get method returns a value if one can be found, and throws an - * exception if one cannot be found. An opt method returns a - * default value instead of throwing an exception, and so is useful for - * obtaining optional values. - *

- * The generic get() and opt() methods return an - * object which you can cast or query for type. There are also typed - * get and opt methods that do type checking and type - * coercion for you. - *

- * The texts produced by the toString methods strictly conform to - * JSON syntax rules. The constructors are more forgiving in the texts they will - * accept: - *

    - *
  • An extra , (comma) may appear just - * before the closing bracket.
  • - *
  • The null value will be inserted when there is , - *  (comma) elision.
  • - *
  • Strings may be quoted with ' (single - * quote).
  • - *
  • Strings do not need to be quoted at all if they do not begin with a quote - * or single quote, and if they do not contain leading or trailing spaces, and - * if they do not contain any of these characters: - * { } [ ] / \ : , # and if they do not look like numbers and if - * they are not the reserved words true, false, or - * null.
  • - *
- * - * @author JSON.org - * @version 2015-07-22 - */ -@SuppressWarnings("ALL") -public class JSONArray implements Iterable { - - /** - * The arrayList where the JSONArray's properties are kept. - */ - private final ArrayList myArrayList; - - /** - * Construct an empty JSONArray. - */ - public JSONArray() { - this.myArrayList = new ArrayList(); - } - - /** - * Construct a JSONArray from a JSONTokener. - * - * @param x A JSONTokener - * @throws JSONException If there is a syntax error. - */ - public JSONArray(JSONTokener x) throws JSONException { - this(); - if (x.nextClean() != '[') { - throw x.syntaxError("A JSONArray text must start with '['"); - } - if (x.nextClean() != ']') { - x.back(); - for (; ; ) { - if (x.nextClean() == ',') { - x.back(); - this.myArrayList.add(JSONObject.NULL); - } else { - x.back(); - this.myArrayList.add(x.nextValue()); - } - switch (x.nextClean()) { - case ',': - if (x.nextClean() == ']') { - return; - } - x.back(); - break; - case ']': - return; - default: - throw x.syntaxError("Expected a ',' or ']'"); - } - } - } - } - - /** - * Construct a JSONArray from a source JSON text. - * - * @param source A string that begins with [ (left - * bracket) and ends with ]   - * (right bracket). - * @throws JSONException If there is a syntax error. - */ - public JSONArray(String source) throws JSONException { - this(new JSONTokener(source)); - } - - /** - * Construct a JSONArray from a Collection. - * - * @param collection A Collection. - */ - public JSONArray(Collection collection) { - this.myArrayList = new ArrayList(); - if (collection != null) { - Iterator iter = collection.iterator(); - while (iter.hasNext()) { - this.myArrayList.add(JSONObject.wrap(iter.next())); - } - } - } - - /** - * Construct a JSONArray from an array - * - * @throws JSONException If not an array. - */ - public JSONArray(Object array) throws JSONException { - this(); - if (array.getClass().isArray()) { - int length = Array.getLength(array); - for (int i = 0; i < length; i += 1) { - this.put(JSONObject.wrap(Array.get(array, i))); - } - } else { - throw new JSONException("JSONArray initial value should be a string or collection or array."); - } - } - - @Override - public Iterator iterator() { - return myArrayList.iterator(); - } - - /** - * Get the object value associated with an index. - * - * @param index The index must be between 0 and length() - 1. - * @return An object value. - * @throws JSONException If there is no value for the index. - */ - public Object get(int index) throws JSONException { - Object object = this.opt(index); - if (object == null) { - throw new JSONException("JSONArray[" + index + "] not found."); - } - return object; - } - - /** - * Get the boolean value associated with an index. The string values "true" - * and "false" are converted to boolean. - * - * @param index The index must be between 0 and length() - 1. - * @return The truth. - * @throws JSONException If there is no value for the index or if the value is not - * convertible to boolean. - */ - public boolean getBoolean(int index) throws JSONException { - Object object = this.get(index); - if (object.equals(Boolean.FALSE) || (object instanceof String && ((String) object).equalsIgnoreCase("false"))) { - return false; - } else if (object.equals(Boolean.TRUE) || (object instanceof String && ((String) object).equalsIgnoreCase("true"))) { - return true; - } - throw new JSONException("JSONArray[" + index + "] is not a boolean."); - } - - /** - * Get the double value associated with an index. - * - * @param index The index must be between 0 and length() - 1. - * @return The value. - * @throws JSONException If the key is not found or if the value cannot be converted - * to a number. - */ - public double getDouble(int index) throws JSONException { - Object object = this.get(index); - try { - return object instanceof Number ? ((Number) object).doubleValue() : Double.parseDouble((String) object); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONArray[" + index + "] is not a number."); - } - } - - /** - * Get the enum value associated with an index. - * - * @param clazz The type of enum to retrieve. - * @param index The index must be between 0 and length() - 1. - * @return The enum value at the index location - * @throws JSONException if the key is not found or if the value cannot be converted - * to an enum. - */ - public > E getEnum(Class clazz, int index) throws JSONException { - E val = optEnum(clazz, index); - if (val == null) { - // JSONException should really take a throwable argument. - // If it did, I would re-implement this with the Enum.valueOf - // method and place any thrown exception in the JSONException - throw new JSONException("JSONObject[" + JSONObject.quote(Integer.toString(index)) + "] is not an enum of type " + JSONObject.quote(clazz.getSimpleName()) + "."); - } - return val; - } - - /** - * Get the BigDecimal value associated with an index. - * - * @param index The index must be between 0 and length() - 1. - * @return The value. - * @throws JSONException If the key is not found or if the value cannot be converted - * to a BigDecimal. - */ - public BigDecimal getBigDecimal(int index) throws JSONException { - Object object = this.get(index); - try { - return new BigDecimal(object.toString()); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONArray[" + index + "] could not convert to BigDecimal."); - } - } - - /** - * Get the BigInteger value associated with an index. - * - * @param index The index must be between 0 and length() - 1. - * @return The value. - * @throws JSONException If the key is not found or if the value cannot be converted - * to a BigInteger. - */ - public BigInteger getBigInteger(int index) throws JSONException { - Object object = this.get(index); - try { - return new BigInteger(object.toString()); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONArray[" + index + "] could not convert to BigInteger."); - } - } - - /** - * Get the int value associated with an index. - * - * @param index The index must be between 0 and length() - 1. - * @return The value. - * @throws JSONException If the key is not found or if the value is not a number. - */ - public int getInt(int index) throws JSONException { - Object object = this.get(index); - try { - return object instanceof Number ? ((Number) object).intValue() : Integer.parseInt((String) object); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONArray[" + index + "] is not a number."); - } - } - - /** - * Get the JSONArray associated with an index. - * - * @param index The index must be between 0 and length() - 1. - * @return A JSONArray value. - * @throws JSONException If there is no value for the index. or if the value is not a - * JSONArray - */ - public JSONArray getJSONArray(int index) throws JSONException { - Object object = this.get(index); - if (object instanceof JSONArray) { - return (JSONArray) object; - } - throw new JSONException("JSONArray[" + index + "] is not a JSONArray."); - } - - /** - * Get the JSONObject associated with an index. - * - * @param index subscript - * @return A JSONObject value. - * @throws JSONException If there is no value for the index or if the value is not a - * JSONObject - */ - public JSONObject getJSONObject(int index) throws JSONException { - Object object = this.get(index); - if (object instanceof JSONObject) { - return (JSONObject) object; - } - throw new JSONException("JSONArray[" + index + "] is not a JSONObject."); - } - - /** - * Get the long value associated with an index. - * - * @param index The index must be between 0 and length() - 1. - * @return The value. - * @throws JSONException If the key is not found or if the value cannot be converted - * to a number. - */ - public long getLong(int index) throws JSONException { - Object object = this.get(index); - try { - return object instanceof Number ? ((Number) object).longValue() : Long.parseLong((String) object); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONArray[" + index + "] is not a number."); - } - } - - /** - * Get the string associated with an index. - * - * @param index The index must be between 0 and length() - 1. - * @return A string value. - * @throws JSONException If there is no string value for the index. - */ - public String getString(int index) throws JSONException { - Object object = this.get(index); - if (object instanceof String) { - return (String) object; - } - throw new JSONException("JSONArray[" + index + "] not a string."); - } - - /** - * Determine if the value is null. - * - * @param index The index must be between 0 and length() - 1. - * @return true if the value at the index is null, or if there is no value. - */ - public boolean isNull(int index) { - return JSONObject.NULL.equals(this.opt(index)); - } - - /** - * Make a string from the contents of this JSONArray. The - * separator string is inserted between each element. Warning: - * This method assumes that the data structure is acyclical. - * - * @param separator A string that will be inserted between the elements. - * @return a string. - * @throws JSONException If the array contains an invalid number. - */ - public String join(String separator) throws JSONException { - int len = this.length(); - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < len; i += 1) { - if (i > 0) { - sb.append(separator); - } - sb.append(JSONObject.valueToString(this.myArrayList.get(i))); - } - return sb.toString(); - } - - /** - * Get the number of elements in the JSONArray, included nulls. - * - * @return The length (or size). - */ - public int length() { - return this.myArrayList.size(); - } - - /** - * Get the optional object value associated with an index. - * - * @param index The index must be between 0 and length() - 1. - * @return An object value, or null if there is no object at that index. - */ - public Object opt(int index) { - return (index < 0 || index >= this.length()) ? null : this.myArrayList.get(index); - } - - /** - * Get the optional boolean value associated with an index. It returns false - * if there is no value at that index, or if the value is not Boolean.TRUE - * or the String "true". - * - * @param index The index must be between 0 and length() - 1. - * @return The truth. - */ - public boolean optBoolean(int index) { - return this.optBoolean(index, false); - } - - /** - * Get the optional boolean value associated with an index. It returns the - * defaultValue if there is no value at that index or if it is not a Boolean - * or the String "true" or "false" (case insensitive). - * - * @param index The index must be between 0 and length() - 1. - * @param defaultValue A boolean default. - * @return The truth. - */ - public boolean optBoolean(int index, boolean defaultValue) { - try { - return this.getBoolean(index); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get the optional double value associated with an index. NaN is returned - * if there is no value for the index, or if the value is not a number and - * cannot be converted to a number. - * - * @param index The index must be between 0 and length() - 1. - * @return The value. - */ - public double optDouble(int index) { - return this.optDouble(index, Double.NaN); - } - - /** - * Get the optional double value associated with an index. The defaultValue - * is returned if there is no value for the index, or if the value is not a - * number and cannot be converted to a number. - * - * @param index subscript - * @param defaultValue The default value. - * @return The value. - */ - public double optDouble(int index, double defaultValue) { - try { - return this.getDouble(index); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get the optional int value associated with an index. Zero is returned if - * there is no value for the index, or if the value is not a number and - * cannot be converted to a number. - * - * @param index The index must be between 0 and length() - 1. - * @return The value. - */ - public int optInt(int index) { - return this.optInt(index, 0); - } - - /** - * Get the optional int value associated with an index. The defaultValue is - * returned if there is no value for the index, or if the value is not a - * number and cannot be converted to a number. - * - * @param index The index must be between 0 and length() - 1. - * @param defaultValue The default value. - * @return The value. - */ - public int optInt(int index, int defaultValue) { - try { - return this.getInt(index); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get the enum value associated with a key. - * - * @param clazz The type of enum to retrieve. - * @param index The index must be between 0 and length() - 1. - * @return The enum value at the index location or null if not found - */ - public > E optEnum(Class clazz, int index) { - return this.optEnum(clazz, index, null); - } - - /** - * Get the enum value associated with a key. - * - * @param clazz The type of enum to retrieve. - * @param index The index must be between 0 and length() - 1. - * @param defaultValue The default in case the value is not found - * @return The enum value at the index location or defaultValue if the value - * is not found or cannot be assigned to clazz - */ - public > E optEnum(Class clazz, int index, E defaultValue) { - try { - Object val = this.opt(index); - if (JSONObject.NULL.equals(val)) { - return defaultValue; - } - if (clazz.isAssignableFrom(val.getClass())) { - // we just checked it! - @SuppressWarnings("unchecked") - E myE = (E) val; - return myE; - } - return Enum.valueOf(clazz, val.toString()); - } catch (IllegalArgumentException e) { - Iris.reportError(e); - return defaultValue; - } catch (NullPointerException e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get the optional BigInteger value associated with an index. The - * defaultValue is returned if there is no value for the index, or if the - * value is not a number and cannot be converted to a number. - * - * @param index The index must be between 0 and length() - 1. - * @param defaultValue The default value. - * @return The value. - */ - public BigInteger optBigInteger(int index, BigInteger defaultValue) { - try { - return this.getBigInteger(index); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get the optional BigDecimal value associated with an index. The - * defaultValue is returned if there is no value for the index, or if the - * value is not a number and cannot be converted to a number. - * - * @param index The index must be between 0 and length() - 1. - * @param defaultValue The default value. - * @return The value. - */ - public BigDecimal optBigDecimal(int index, BigDecimal defaultValue) { - try { - return this.getBigDecimal(index); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get the optional JSONArray associated with an index. - * - * @param index subscript - * @return A JSONArray value, or null if the index has no value, or if the - * value is not a JSONArray. - */ - public JSONArray optJSONArray(int index) { - Object o = this.opt(index); - return o instanceof JSONArray ? (JSONArray) o : null; - } - - /** - * Get the optional JSONObject associated with an index. Null is returned if - * the key is not found, or null if the index has no value, or if the value - * is not a JSONObject. - * - * @param index The index must be between 0 and length() - 1. - * @return A JSONObject value. - */ - public JSONObject optJSONObject(int index) { - Object o = this.opt(index); - return o instanceof JSONObject ? (JSONObject) o : null; - } - - /** - * Get the optional long value associated with an index. Zero is returned if - * there is no value for the index, or if the value is not a number and - * cannot be converted to a number. - * - * @param index The index must be between 0 and length() - 1. - * @return The value. - */ - public long optLong(int index) { - return this.optLong(index, 0); - } - - /** - * Get the optional long value associated with an index. The defaultValue is - * returned if there is no value for the index, or if the value is not a - * number and cannot be converted to a number. - * - * @param index The index must be between 0 and length() - 1. - * @param defaultValue The default value. - * @return The value. - */ - public long optLong(int index, long defaultValue) { - try { - return this.getLong(index); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get the optional string value associated with an index. It returns an - * empty string if there is no value at that index. If the value is not a - * string and is not null, then it is coverted to a string. - * - * @param index The index must be between 0 and length() - 1. - * @return A String value. - */ - public String optString(int index) { - return this.optString(index, ""); - } - - /** - * Get the optional string associated with an index. The defaultValue is - * returned if the key is not found. - * - * @param index The index must be between 0 and length() - 1. - * @param defaultValue The default value. - * @return A String value. - */ - public String optString(int index, String defaultValue) { - Object object = this.opt(index); - return JSONObject.NULL.equals(object) ? defaultValue : object.toString(); - } - - /** - * Append a boolean value. This increases the array's length by one. - * - * @param value A boolean value. - * @return this. - */ - public JSONArray put(boolean value) { - this.put(value ? Boolean.TRUE : Boolean.FALSE); - return this; - } - - /** - * Put a value in the JSONArray, where the value will be a JSONArray which - * is produced from a Collection. - * - * @param value A Collection value. - * @return this. - */ - public JSONArray put(Collection value) { - this.put(new JSONArray(value)); - return this; - } - - /** - * Append a double value. This increases the array's length by one. - * - * @param value A double value. - * @return this. - * @throws JSONException if the value is not finite. - */ - public JSONArray put(double value) throws JSONException { - Double d = Double.valueOf(value); - JSONObject.testValidity(d); - this.put(d); - return this; - } - - /** - * Append an int value. This increases the array's length by one. - * - * @param value An int value. - * @return this. - */ - public JSONArray put(int value) { - this.put(Integer.valueOf(value)); - return this; - } - - /** - * Append an long value. This increases the array's length by one. - * - * @param value A long value. - * @return this. - */ - public JSONArray put(long value) { - this.put(Long.valueOf(value)); - return this; - } - - /** - * Put a value in the JSONArray, where the value will be a JSONObject which - * is produced from a Map. - * - * @param value A Map value. - * @return this. - */ - public JSONArray put(Map value) { - this.put(new JSONObject(value)); - return this; - } - - /** - * Append an object value. This increases the array's length by one. - * - * @param value An object value. The value should be a Boolean, Double, - * Integer, JSONArray, JSONObject, Long, or String, or the - * JSONObject.NULL object. - * @return this. - */ - public JSONArray put(Object value) { - this.myArrayList.add(value); - return this; - } - - /** - * Put or replace a boolean value in the JSONArray. If the index is greater - * than the length of the JSONArray, then null elements will be added as - * necessary to pad it out. - * - * @param index The subscript. - * @param value A boolean value. - * @return this. - * @throws JSONException If the index is negative. - */ - public JSONArray put(int index, boolean value) throws JSONException { - this.put(index, value ? Boolean.TRUE : Boolean.FALSE); - return this; - } - - /** - * Put a value in the JSONArray, where the value will be a JSONArray which - * is produced from a Collection. - * - * @param index The subscript. - * @param value A Collection value. - * @return this. - * @throws JSONException If the index is negative or if the value is not finite. - */ - public JSONArray put(int index, Collection value) throws JSONException { - this.put(index, new JSONArray(value)); - return this; - } - - /** - * Put or replace a double value. If the index is greater than the length of - * the JSONArray, then null elements will be added as necessary to pad it - * out. - * - * @param index The subscript. - * @param value A double value. - * @return this. - * @throws JSONException If the index is negative or if the value is not finite. - */ - public JSONArray put(int index, double value) throws JSONException { - this.put(index, Double.valueOf(value)); - return this; - } - - /** - * Put or replace an int value. If the index is greater than the length of - * the JSONArray, then null elements will be added as necessary to pad it - * out. - * - * @param index The subscript. - * @param value An int value. - * @return this. - * @throws JSONException If the index is negative. - */ - public JSONArray put(int index, int value) throws JSONException { - this.put(index, Integer.valueOf(value)); - return this; - } - - /** - * Put or replace a long value. If the index is greater than the length of - * the JSONArray, then null elements will be added as necessary to pad it - * out. - * - * @param index The subscript. - * @param value A long value. - * @return this. - * @throws JSONException If the index is negative. - */ - public JSONArray put(int index, long value) throws JSONException { - this.put(index, Long.valueOf(value)); - return this; - } - - /** - * Put a value in the JSONArray, where the value will be a JSONObject that - * is produced from a Map. - * - * @param index The subscript. - * @param value The Map value. - * @return this. - * @throws JSONException If the index is negative or if the the value is an invalid - * number. - */ - public JSONArray put(int index, Map value) throws JSONException { - this.put(index, new JSONObject(value)); - return this; - } - - /** - * Put or replace an object value in the JSONArray. If the index is greater - * than the length of the JSONArray, then null elements will be added as - * necessary to pad it out. - * - * @param index The subscript. - * @param value The value to put into the array. The value should be a - * Boolean, Double, Integer, JSONArray, JSONObject, Long, or - * String, or the JSONObject.NULL object. - * @return this. - * @throws JSONException If the index is negative or if the the value is an invalid - * number. - */ - public JSONArray put(int index, Object value) throws JSONException { - JSONObject.testValidity(value); - if (index < 0) { - throw new JSONException("JSONArray[" + index + "] not found."); - } - if (index < this.length()) { - this.myArrayList.set(index, value); - } else { - while (index != this.length()) { - this.put(JSONObject.NULL); - } - this.put(value); - } - return this; - } - - /** - * Remove an index and close the hole. - * - * @param index The index of the element to be removed. - * @return The value that was associated with the index, or null if there - * was no value. - */ - public Object remove(int index) { - return index >= 0 && index < this.length() ? this.myArrayList.remove(index) : null; - } - - /** - * Determine if two JSONArrays are similar. They must contain similar - * sequences. - * - * @param other The other JSONArray - * @return true if they are equal - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean similar(Object other) { - if (!(other instanceof JSONArray)) { - return false; - } - int len = this.length(); - if (len != ((JSONArray) other).length()) { - return false; - } - for (int i = 0; i < len; i += 1) { - Object valueThis = this.get(i); - Object valueOther = ((JSONArray) other).get(i); - if (valueThis instanceof JSONObject) { - if (!((JSONObject) valueThis).similar(valueOther)) { - return false; - } - } else if (valueThis instanceof JSONArray) { - if (!((JSONArray) valueThis).similar(valueOther)) { - return false; - } - } else if (!valueThis.equals(valueOther)) { - return false; - } - } - return true; - } - - /** - * Produce a JSONObject by combining a JSONArray of names with the values of - * this JSONArray. - * - * @param names A JSONArray containing a list of key strings. These will be - * paired with the values. - * @return A JSONObject, or null if there are no names or if this JSONArray - * has no values. - * @throws JSONException If any of the names are null. - */ - public JSONObject toJSONObject(JSONArray names) throws JSONException { - if (names == null || names.length() == 0 || this.length() == 0) { - return null; - } - JSONObject jo = new JSONObject(); - for (int i = 0; i < names.length(); i += 1) { - jo.put(names.getString(i), this.opt(i)); - } - return jo; - } - - /** - * Make a JSON text of this JSONArray. For compactness, no unnecessary - * whitespace is added. If it is not possible to produce a syntactically - * correct JSON text then null will be returned instead. This could occur if - * the array contains an invalid number. - *

- * Warning: This method assumes that the data structure is acyclical. - * - * @return a printable, displayable, transmittable representation of the - * array. - */ - @Override - public String toString() { - try { - return this.toString(0); - } catch (Exception e) { - Iris.reportError(e); - return null; - } - } - - /** - * Make a prettyprinted JSON text of this JSONArray. Warning: This method - * assumes that the data structure is acyclical. - * - * @param indentFactor The number of spaces to add to each level of indentation. - * @return a printable, displayable, transmittable representation of the - * object, beginning with [ (left - * bracket) and ending with ]   - * (right bracket). - */ - public String toString(int indentFactor) throws JSONException { - StringWriter sw = new StringWriter(); - synchronized (sw.getBuffer()) { - return this.write(sw, indentFactor, 0).toString(); - } - } - - /** - * Write the contents of the JSONArray as JSON text to a writer. For - * compactness, no whitespace is added. - *

- * Warning: This method assumes that the data structure is acyclical. - * - * @return The writer. - */ - public Writer write(Writer writer) throws JSONException { - return this.write(writer, 0, 0); - } - - /** - * Write the contents of the JSONArray as JSON text to a writer. For - * compactness, no whitespace is added. - *

- * Warning: This method assumes that the data structure is acyclical. - * - * @param indentFactor The number of spaces to add to each level of indentation. - * @param indent The indention of the top level. - * @return The writer. - */ - Writer write(Writer writer, int indentFactor, int indent) throws JSONException { - try { - boolean commanate = false; - int length = this.length(); - writer.write('['); - - if (length == 1) { - JSONObject.writeValue(writer, this.myArrayList.get(0), indentFactor, indent); - } else if (length != 0) { - final int newindent = indent + indentFactor; - - for (int i = 0; i < length; i += 1) { - if (commanate) { - writer.write(','); - } - if (indentFactor > 0) { - writer.write('\n'); - } - JSONObject.indent(writer, newindent); - JSONObject.writeValue(writer, this.myArrayList.get(i), indentFactor, newindent); - commanate = true; - } - if (indentFactor > 0) { - writer.write('\n'); - } - JSONObject.indent(writer, indent); - } - writer.write(']'); - return writer; - } catch (IOException e) { - throw new JSONException(e); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/JSONException.java b/core/src/main/java/com/volmit/iris/util/json/JSONException.java deleted file mode 100644 index 2f7f5ac8a..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/JSONException.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -/** - * The JSONException is thrown by the JSON.org classes when things are amiss. - * - * @author JSON.org - * @version 2014-05-03 - */ -public class JSONException extends RuntimeException { - private static final long serialVersionUID = 0; - private Throwable cause; - - /** - * Constructs a JSONException with an explanatory message. - * - * @param message Detail about the reason for the exception. - */ - public JSONException(String message) { - super(message); - } - - /** - * Constructs a new JSONException with the specified cause. - * - * @param cause The cause. - */ - public JSONException(Throwable cause) { - super(cause.getMessage()); - this.cause = cause; - } - - /** - * Returns the cause of this exception or null if the cause is nonexistent - * or unknown. - * - * @return the cause of this exception or null if the cause is nonexistent - * or unknown. - */ - @Override - public Throwable getCause() { - return this.cause; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/JSONML.java b/core/src/main/java/com/volmit/iris/util/json/JSONML.java deleted file mode 100644 index 091f7d08b..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/JSONML.java +++ /dev/null @@ -1,447 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -import java.util.Iterator; - -/** - * This provides static methods to convert an XML text into a JSONArray or - * JSONObject, and to covert a JSONArray or JSONObject into an XML text using - * the JsonML transform. - * - * @author JSON.org - * @version 2014-05-03 - */ -public class JSONML { - - /** - * Parse XML values and store them in a JSONArray. - * - * @param x The XMLTokener containing the source string. - * @param arrayForm true if array form, false if object form. - * @param ja The JSONArray that is containing the current tag or null if we - * are at the outermost level. - * @return A JSONArray if the value is the outermost tag, otherwise null. - */ - private static Object parse(XMLTokener x, boolean arrayForm, JSONArray ja) throws JSONException { - String attribute; - char c; - String closeTag = null; - int i; - JSONArray newja = null; - JSONObject newjo = null; - Object token; - String tagName = null; - - // Test for and skip past these forms: - // - // - // - // - - while (true) { - if (!x.more()) { - throw x.syntaxError("Bad XML"); - } - token = x.nextContent(); - if (token == XML.LT) { - token = x.nextToken(); - if (token instanceof Character) { - if (token == XML.SLASH) { - - // Close tag "); - } else { - x.back(); - } - } else if (c == '[') { - token = x.nextToken(); - if (token.equals("CDATA") && x.next() == '[') { - if (ja != null) { - ja.put(x.nextCDATA()); - } - } else { - throw x.syntaxError("Expected 'CDATA['"); - } - } else { - i = 1; - do { - token = x.nextMeta(); - if (token == null) { - throw x.syntaxError("Missing '>' after ' 0); - } - } else if (token == XML.QUEST) { - - // "); - } else { - throw x.syntaxError("Misshaped tag"); - } - - // Open tag < - - } else { - if (!(token instanceof String)) { - throw x.syntaxError("Bad tagName '" + token + "'."); - } - tagName = (String) token; - newja = new JSONArray(); - newjo = new JSONObject(); - if (arrayForm) { - newja.put(tagName); - if (ja != null) { - ja.put(newja); - } - } else { - newjo.put("tagName", tagName); - if (ja != null) { - ja.put(newjo); - } - } - token = null; - for (; ; ) { - if (token == null) { - token = x.nextToken(); - } - if (token == null) { - throw x.syntaxError("Misshaped tag"); - } - if (!(token instanceof String)) { - break; - } - - // attribute = value - - attribute = (String) token; - if (!arrayForm && ("tagName".equals(attribute) || "childNode".equals(attribute))) { - throw x.syntaxError("Reserved attribute."); - } - token = x.nextToken(); - if (token == XML.EQ) { - token = x.nextToken(); - if (!(token instanceof String)) { - throw x.syntaxError("Missing value"); - } - newjo.accumulate(attribute, XML.stringToValue((String) token)); - token = null; - } else { - newjo.accumulate(attribute, ""); - } - } - if (arrayForm && newjo.length() > 0) { - newja.put(newjo); - } - - // Empty tag <.../> - - if (token == XML.SLASH) { - if (x.nextToken() != XML.GT) { - throw x.syntaxError("Misshaped tag"); - } - if (ja == null) { - if (arrayForm) { - return newja; - } else { - return newjo; - } - } - - // Content, between <...> and - - } else { - if (token != XML.GT) { - throw x.syntaxError("Misshaped tag"); - } - closeTag = (String) parse(x, arrayForm, newja); - if (closeTag != null) { - if (!closeTag.equals(tagName)) { - throw x.syntaxError("Mismatched '" + tagName + "' and '" + closeTag + "'"); - } - tagName = null; - if (!arrayForm && newja.length() > 0) { - newjo.put("childNodes", newja); - } - if (ja == null) { - if (arrayForm) { - return newja; - } else { - return newjo; - } - } - } - } - } - } else { - if (ja != null) { - ja.put(token instanceof String ? XML.stringToValue((String) token) : token); - } - } - } - } - - /** - * Convert a well-formed (but not necessarily valid) XML string into a - * JSONArray using the JsonML transform. Each XML tag is represented as a - * JSONArray in which the first element is the tag name. If the tag has - * attributes, then the second element will be JSONObject containing the - * name/value pairs. If the tag contains children, then strings and - * JSONArrays will represent the child tags. Comments, prologs, DTDs, and - * <[ [ ]]> are ignored. - * - * @param string The source string. - * @return A JSONArray containing the structured data from the XML string. - */ - public static JSONArray toJSONArray(String string) throws JSONException { - return toJSONArray(new XMLTokener(string)); - } - - /** - * Convert a well-formed (but not necessarily valid) XML string into a - * JSONArray using the JsonML transform. Each XML tag is represented as a - * JSONArray in which the first element is the tag name. If the tag has - * attributes, then the second element will be JSONObject containing the - * name/value pairs. If the tag contains children, then strings and - * JSONArrays will represent the child content and tags. Comments, prologs, - * DTDs, and <[ [ ]]> are ignored. - * - * @param x An XMLTokener. - * @return A JSONArray containing the structured data from the XML string. - */ - public static JSONArray toJSONArray(XMLTokener x) throws JSONException { - return (JSONArray) parse(x, true, null); - } - - /** - * Convert a well-formed (but not necessarily valid) XML string into a - * JSONObject using the JsonML transform. Each XML tag is represented as a - * JSONObject with a "tagName" property. If the tag has attributes, then the - * attributes will be in the JSONObject as properties. If the tag contains - * children, the object will have a "childNodes" property which will be an - * array of strings and JsonML JSONObjects. - *

- * Comments, prologs, DTDs, and <[ [ ]]> are ignored. - * - * @param x An XMLTokener of the XML source text. - * @return A JSONObject containing the structured data from the XML string. - */ - public static JSONObject toJSONObject(XMLTokener x) throws JSONException { - return (JSONObject) parse(x, false, null); - } - - /** - * Convert a well-formed (but not necessarily valid) XML string into a - * JSONObject using the JsonML transform. Each XML tag is represented as a - * JSONObject with a "tagName" property. If the tag has attributes, then the - * attributes will be in the JSONObject as properties. If the tag contains - * children, the object will have a "childNodes" property which will be an - * array of strings and JsonML JSONObjects. - *

- * Comments, prologs, DTDs, and <[ [ ]]> are ignored. - * - * @param string The XML source text. - * @return A JSONObject containing the structured data from the XML string. - */ - public static JSONObject toJSONObject(String string) throws JSONException { - return toJSONObject(new XMLTokener(string)); - } - - /** - * Reverse the JSONML transformation, making an XML text from a JSONArray. - * - * @param ja A JSONArray. - * @return An XML string. - */ - public static String toString(JSONArray ja) throws JSONException { - int i; - JSONObject jo; - String key; - Iterator keys; - int length; - Object object; - StringBuilder sb = new StringBuilder(); - String tagName; - String value; - - // Emit = length) { - sb.append('/'); - } else { - sb.append('>'); - do { - object = ja.get(i); - i += 1; - if (object != null) { - if (object instanceof String) { - sb.append(XML.escape(object.toString())); - } else if (object instanceof JSONObject) { - sb.append(toString((JSONObject) object)); - } else if (object instanceof JSONArray) { - sb.append(toString((JSONArray) object)); - } else { - sb.append(object); - } - } - } while (i < length); - sb.append('<'); - sb.append('/'); - sb.append(tagName); - } - sb.append('>'); - return sb.toString(); - } - - /** - * Reverse the JSONML transformation, making an XML text from a JSONObject. - * The JSONObject must contain a "tagName" property. If it has children, - * then it must have a "childNodes" property containing an array of objects. - * The other properties are attributes with string values. - * - * @param jo A JSONObject. - * @return An XML string. - */ - public static String toString(JSONObject jo) throws JSONException { - StringBuilder sb = new StringBuilder(); - int i; - JSONArray ja; - String key; - Iterator keys; - int length; - Object object; - String tagName; - String value; - - // Emit '); - length = ja.length(); - for (i = 0; i < length; i += 1) { - object = ja.get(i); - if (object != null) { - if (object instanceof String) { - sb.append(XML.escape(object.toString())); - } else if (object instanceof JSONObject) { - sb.append(toString((JSONObject) object)); - } else if (object instanceof JSONArray) { - sb.append(toString((JSONArray) object)); - } else { - sb.append(object); - } - } - } - sb.append('<'); - sb.append('/'); - sb.append(tagName); - } - sb.append('>'); - return sb.toString(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/JSONObject.java b/core/src/main/java/com/volmit/iris/util/json/JSONObject.java deleted file mode 100644 index 05aa7f2bb..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/JSONObject.java +++ /dev/null @@ -1,1697 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - -import com.volmit.iris.Iris; - -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.*; -import java.util.Map.Entry; - -/** - * A JSONObject is an unordered collection of name/value pairs. Its external - * form is a string wrapped in curly braces with colons between the names and - * values, and commas between the values and names. The internal form is an - * object having get and opt methods for accessing the - * values by name, and put methods for adding or replacing values - * by name. The values can be any of these types: Boolean, - * JSONArray, JSONObject, Number, - * String, or the JSONObject.NULL object. A JSONObject - * constructor can be used to convert an external form JSON text into an - * internal form whose values can be retrieved with the get and - * opt methods, or to convert values into a JSON text using the - * put and toString methods. A get method - * returns a value if one can be found, and throws an exception if one cannot be - * found. An opt method returns a default value instead of throwing - * an exception, and so is useful for obtaining optional values. - *

- * The generic get() and opt() methods return an - * object, which you can cast or query for type. There are also typed - * get and opt methods that do type checking and type - * coercion for you. The opt methods differ from the get methods in that they do - * not throw. Instead, they return a specified value, such as null. - *

- * The put methods add or replace values in an object. For example, - * - *

- * myString = new JSONObject().put("JSON", "Hello, World!").toString();
- * 
- *

- * produces the string {"JSON": "Hello, World"}. - *

- * The texts produced by the toString methods strictly conform to - * the JSON syntax rules. The constructors are more forgiving in the texts they - * will accept: - *

    - *
  • An extra , (comma) may appear just - * before the closing brace.
  • - *
  • Strings may be quoted with ' (single - * quote).
  • - *
  • Strings do not need to be quoted at all if they do not begin with a quote - * or single quote, and if they do not contain leading or trailing spaces, and - * if they do not contain any of these characters: - * { } [ ] / \ : , # and if they do not look like numbers and if - * they are not the reserved words true, false, or - * null.
  • - *
- * - * @author JSON.org - * @version 2015-07-22 - */ -@SuppressWarnings("ALL") -public class JSONObject { - /** - * It is sometimes more convenient and less ambiguous to have a - * NULL object than to use Java's null value. - * JSONObject.NULL.equals(null) returns true. - * JSONObject.NULL.toString() returns "null". - */ - public static final Object NULL = new Null(); - /** - * The map where the JSONObject's properties are kept. - */ - private final LinkedHashMap map; - - /** - * Construct an empty JSONObject. - */ - public JSONObject() { - this.map = new LinkedHashMap<>(); - } - - /** - * Construct a JSONObject from a subset of another JSONObject. An array of - * strings is used to identify the keys that should be copied. Missing keys - * are ignored. - * - * @param jo A JSONObject. - * @param names An array of strings. - * @throws JSONException If a value is a non-finite number or if a name is - * duplicated. - */ - public JSONObject(JSONObject jo, String[] names) { - this(); - for (int i = 0; i < names.length; i += 1) { - try { - this.putOnce(names[i], jo.opt(names[i])); - } catch (Exception e) { - Iris.reportError(e); - } - } - } - - /** - * Construct a JSONObject from a JSONTokener. - * - * @param x A JSONTokener object containing the source string. - * @throws JSONException If there is a syntax error in the source string or a - * duplicated key. - */ - public JSONObject(JSONTokener x) throws JSONException { - this(); - char c; - String key; - - if (x.nextClean() != '{') { - throw x.syntaxError("A JSONObject text must begin with '{'"); - } - for (; ; ) { - c = x.nextClean(); - switch (c) { - case 0: - throw x.syntaxError("A JSONObject text must end with '}'"); - case '}': - return; - default: - x.back(); - key = x.nextValue().toString(); - } - - // The key is followed by ':'. - - c = x.nextClean(); - if (c != ':') { - throw x.syntaxError("Expected a ':' after a key"); - } - this.putOnce(key, x.nextValue()); - - // Pairs are separated by ','. - - switch (x.nextClean()) { - case ';': - case ',': - if (x.nextClean() == '}') { - return; - } - x.back(); - break; - case '}': - return; - default: - throw x.syntaxError("Expected a ',' or '}'"); - } - } - } - - /** - * Construct a JSONObject from a Map. - * - * @param map A map object that can be used to initialize the contents of - * the JSONObject. - */ - public JSONObject(Map map) { - this.map = new LinkedHashMap<>(); - if (map != null) { - Iterator> i = map.entrySet().iterator(); - while (i.hasNext()) { - Entry entry = i.next(); - Object value = entry.getValue(); - if (value != null) { - this.map.put(entry.getKey(), wrap(value)); - } - } - } - } - - /** - * Construct a JSONObject from an Object using bean getters. It reflects on - * all of the public methods of the object. For each of the methods with no - * parameters and a name starting with "get" or - * "is" followed by an uppercase letter, the method is invoked, - * and a key and the value returned from the getter method are put into the - * new JSONObject. - *

- * The key is formed by removing the "get" or "is" - * prefix. If the second remaining character is not upper case, then the - * first character is converted to lower case. - *

- * For example, if an object has a method named "getName", and - * if the result of calling object.getName() is - * "Larry Fine", then the JSONObject will contain - * "name": "Larry Fine". - * - * @param bean An object that has getter methods that should be used to make - * a JSONObject. - */ - public JSONObject(Object bean) { - this(); - this.populateMap(bean); - } - - /** - * Construct a JSONObject from an Object, using reflection to find the - * public members. The resulting JSONObject's keys will be the strings from - * the names array, and the values will be the field values associated with - * those keys in the object. If a key is not found or not visible, then it - * will not be copied into the new JSONObject. - * - * @param object An object that has fields that should be used to make a - * JSONObject. - * @param names An array of strings, the names of the fields to be obtained - * from the object. - */ - public JSONObject(Object object, String[] names) { - this(); - Class c = object.getClass(); - for (int i = 0; i < names.length; i += 1) { - String name = names[i]; - try { - this.putOpt(name, c.getField(name).get(object)); - } catch (Exception e) { - Iris.reportError(e); - } - } - } - - /** - * Construct a JSONObject from a source JSON text string. This is the most - * commonly used JSONObject constructor. - * - * @param source A string beginning with { (left - * brace) and ending with }   - * (right brace). - * @throws JSONException If there is a syntax error in the source string or a - * duplicated key. - */ - public JSONObject(String source) throws JSONException { - this(new JSONTokener(source)); - } - - /** - * Construct a JSONObject from a ResourceBundle. - * - * @param baseName The ResourceBundle base name. - * @param locale The Locale to load the ResourceBundle for. - * @throws JSONException If any JSONExceptions are detected. - */ - public JSONObject(String baseName, Locale locale) throws JSONException { - this(); - ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, Thread.currentThread().getContextClassLoader()); - - // Iterate through the keys in the bundle. - - Enumeration keys = bundle.getKeys(); - while (keys.hasMoreElements()) { - Object key = keys.nextElement(); - if (key != null) { - - // Go through the path, ensuring that there is a nested - // JSONObject for each - // segment except the last. Add the value using the last - // segment's name into - // the deepest nested JSONObject. - - String[] path = ((String) key).split("\\."); - int last = path.length - 1; - JSONObject target = this; - for (int i = 0; i < last; i += 1) { - String segment = path[i]; - JSONObject nextTarget = target.optJSONObject(segment); - if (nextTarget == null) { - nextTarget = new JSONObject(); - target.put(segment, nextTarget); - } - target = nextTarget; - } - target.put(path[last], bundle.getString((String) key)); - } - } - } - - /** - * Produce a string from a double. The string "null" will be returned if the - * number is not finite. - * - * @param d A double. - * @return A String. - */ - public static String doubleToString(double d) { - if (Double.isInfinite(d) || Double.isNaN(d)) { - return "null"; - } - - // Shave off trailing zeros and decimal point, if possible. - - String string = Double.toString(d); - if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) { - while (string.endsWith("0")) { - string = string.substring(0, string.length() - 1); - } - if (string.endsWith(".")) { - string = string.substring(0, string.length() - 1); - } - } - return string; - } - - /** - * Get an array of field names from a JSONObject. - * - * @return An array of field names, or null if there are no names. - */ - public static String[] getNames(JSONObject jo) { - int length = jo.length(); - if (length == 0) { - return null; - } - Iterator iterator = jo.keys(); - String[] names = new String[length]; - int i = 0; - while (iterator.hasNext()) { - names[i] = iterator.next(); - i += 1; - } - return names; - } - - /** - * Get an array of field names from an Object. - * - * @return An array of field names, or null if there are no names. - */ - public static String[] getNames(Object object) { - if (object == null) { - return null; - } - Class klass = object.getClass(); - Field[] fields = klass.getFields(); - int length = fields.length; - if (length == 0) { - return null; - } - String[] names = new String[length]; - for (int i = 0; i < length; i += 1) { - names[i] = fields[i].getName(); - } - return names; - } - - /** - * Produce a string from a Number. - * - * @param number A Number - * @return A String. - * @throws JSONException If n is a non-finite number. - */ - public static String numberToString(Number number) throws JSONException { - if (number == null) { - throw new JSONException("Null pointer"); - } - testValidity(number); - - // Shave off trailing zeros and decimal point, if possible. - - String string = number.toString(); - if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) { - while (string.endsWith("0")) { - string = string.substring(0, string.length() - 1); - } - if (string.endsWith(".")) { - string = string.substring(0, string.length() - 1); - } - } - return string; - } - - /** - * Produce a string in double quotes with backslash sequences in all the - * right places. A backslash will be inserted within = '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) { - w.write("\\u"); - hhhh = Integer.toHexString(c); - w.write("0000", 0, 4 - hhhh.length()); - w.write(hhhh); - } else { - w.write(c); - } - } - } - w.write('"'); - return w; - } - - /** - * Try to convert a string into a number, boolean, or null. If the string - * can't be converted, return the string. - * - * @param string A String. - * @return A simple JSON value. - */ - public static Object stringToValue(String string) { - Double d; - if (string.equals("")) { - return string; - } - if (string.equalsIgnoreCase("true")) { - return Boolean.TRUE; - } - if (string.equalsIgnoreCase("false")) { - return Boolean.FALSE; - } - if (string.equalsIgnoreCase("null")) { - return JSONObject.NULL; - } - - /* - * If it might be a number, try converting it. If a number cannot be - * produced, then the value will just be a string. - */ - - char b = string.charAt(0); - if ((b >= '0' && b <= '9') || b == '-') { - try { - if (string.indexOf('.') > -1 || string.indexOf('e') > -1 || string.indexOf('E') > -1) { - d = Double.valueOf(string); - if (!d.isInfinite() && !d.isNaN()) { - return d; - } - } else { - Long myLong = Long.valueOf(string); - if (string.equals(myLong.toString())) { - if (myLong == myLong.intValue()) { - return myLong.intValue(); - } else { - return myLong; - } - } - } - } catch (Exception e) { - Iris.reportError(e); - } - } - return string; - } - - /** - * Throw an exception if the object is a NaN or infinite number. - * - * @param o The object to test. - * @throws JSONException If o is a non-finite number. - */ - public static void testValidity(Object o) throws JSONException { - if (o != null) { - if (o instanceof Double) { - if (((Double) o).isInfinite() || ((Double) o).isNaN()) { - throw new JSONException("JSON does not allow non-finite numbers."); - } - } else if (o instanceof Float) { - if (((Float) o).isInfinite() || ((Float) o).isNaN()) { - throw new JSONException("JSON does not allow non-finite numbers."); - } - } - } - } - - /** - * Make a JSON text of an Object value. If the object has an - * value.toJSONString() method, then that method will be used to produce the - * JSON text. The method is required to produce a strictly conforming text. - * If the object does not contain a toJSONString method (which is the most - * common case), then a text will be produced by other means. If the value - * is an array or Collection, then a JSONArray will be made from it and its - * toJSONString method will be called. If the value is a MAP, then a - * JSONObject will be made from it and its toJSONString method will be - * called. Otherwise, the value's toString method will be called, and the - * result will be quoted. - * - *

- * Warning: This method assumes that the data structure is acyclical. - * - * @param value The value to be serialized. - * @return a printable, displayable, transmittable representation of the - * object, beginning with { (left - * brace) and ending with } (right - * brace). - * @throws JSONException If the value is or contains an invalid number. - */ - public static String valueToString(Object value) throws JSONException { - if (value == null || value.equals(null)) { - return "null"; - } - if (value instanceof JSONString) { - Object object; - try { - object = ((JSONString) value).toJSONString(); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException(e); - } - if (object instanceof String) { - return (String) object; - } - throw new JSONException("Bad value from toJSONString: " + object); - } - if (value instanceof Number) { - return numberToString((Number) value); - } - if (value instanceof Boolean || value instanceof JSONObject || value instanceof JSONArray) { - return value.toString(); - } - if (value instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) value; - return new JSONObject(map).toString(); - } - if (value instanceof Collection) { - @SuppressWarnings("unchecked") - Collection coll = (Collection) value; - return new JSONArray(coll).toString(); - } - if (value.getClass().isArray()) { - return new JSONArray(value).toString(); - } - return quote(value.toString()); - } - - /** - * Wrap an object, if necessary. If the object is null, return the NULL - * object. If it is an array or collection, wrap it in a JSONArray. If it is - * a map, wrap it in a JSONObject. If it is a standard property (Double, - * String, et al) then it is already wrapped. Otherwise, if it comes from - * one of the java packages, turn it into a string. And if it doesn't, try - * to wrap it in a JSONObject. If the wrapping fails, then null is returned. - * - * @param object The object to wrap - * @return The wrapped value - */ - public static Object wrap(Object object) { - try { - if (object == null) { - return NULL; - } - if (object instanceof JSONObject || object instanceof JSONArray || NULL.equals(object) || object instanceof JSONString || object instanceof Byte || object instanceof Character || object instanceof Short || object instanceof Integer || object instanceof Long || object instanceof Boolean || object instanceof Float || object instanceof Double || object instanceof String || object instanceof BigInteger || object instanceof BigDecimal) { - return object; - } - - if (object instanceof Collection) { - @SuppressWarnings("unchecked") - Collection coll = (Collection) object; - return new JSONArray(coll); - } - if (object.getClass().isArray()) { - return new JSONArray(object); - } - if (object instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) object; - return new JSONObject(map); - } - Package objectPackage = object.getClass().getPackage(); - String objectPackageName = objectPackage != null ? objectPackage.getName() : ""; - if (objectPackageName.startsWith("java.") || objectPackageName.startsWith("javax.") || object.getClass().getClassLoader() == null) { - return object.toString(); - } - return new JSONObject(object); - } catch (Exception e) { - Iris.reportError(e); - return null; - } - } - - static final Writer writeValue(Writer writer, Object value, int indentFactor, int indent) throws JSONException, IOException { - if (value == null || value.equals(null)) { - writer.write("null"); - } else if (value instanceof JSONObject) { - ((JSONObject) value).write(writer, indentFactor, indent); - } else if (value instanceof JSONArray) { - ((JSONArray) value).write(writer, indentFactor, indent); - } else if (value instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) value; - new JSONObject(map).write(writer, indentFactor, indent); - } else if (value instanceof Collection) { - @SuppressWarnings("unchecked") - Collection coll = (Collection) value; - new JSONArray(coll).write(writer, indentFactor, indent); - } else if (value.getClass().isArray()) { - new JSONArray(value).write(writer, indentFactor, indent); - } else if (value instanceof Number) { - writer.write(numberToString((Number) value)); - } else if (value instanceof Boolean) { - writer.write(value.toString()); - } else if (value instanceof JSONString) { - Object o; - try { - o = ((JSONString) value).toJSONString(); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException(e); - } - writer.write(o != null ? o.toString() : quote(value.toString())); - } else { - quote(value.toString(), writer); - } - return writer; - } - - static final void indent(Writer writer, int indent) throws IOException { - for (int i = 0; i < indent; i += 1) { - writer.write(' '); - } - } - - /** - * Accumulate values under a key. It is similar to the put method except - * that if there is already an object stored under the key then a JSONArray - * is stored under the key to hold all of the accumulated values. If there - * is already a JSONArray, then the new value is appended to it. In - * contrast, the put method replaces the previous value. - *

- * If only one value is accumulated that is not a JSONArray, then the result - * will be the same as using put. But if multiple values are accumulated, - * then the result will be like append. - * - * @param key A key string. - * @param value An object to be accumulated under the key. - * @return this. - * @throws JSONException If the value is an invalid number or if the key is null. - */ - public JSONObject accumulate(String key, Object value) throws JSONException { - testValidity(value); - Object object = this.opt(key); - if (object == null) { - this.put(key, value instanceof JSONArray ? new JSONArray().put(value) : value); - } else if (object instanceof JSONArray) { - ((JSONArray) object).put(value); - } else { - this.put(key, new JSONArray().put(object).put(value)); - } - return this; - } - - /** - * Append values to the array under a key. If the key does not exist in the - * JSONObject, then the key is put in the JSONObject with its value being a - * JSONArray containing the value parameter. If the key was already - * associated with a JSONArray, then the value parameter is appended to it. - * - * @param key A key string. - * @param value An object to be accumulated under the key. - * @return this. - * @throws JSONException If the key is null or if the current value associated with - * the key is not a JSONArray. - */ - public JSONObject append(String key, Object value) throws JSONException { - testValidity(value); - Object object = this.opt(key); - if (object == null) { - this.put(key, new JSONArray().put(value)); - } else if (object instanceof JSONArray) { - this.put(key, ((JSONArray) object).put(value)); - } else { - throw new JSONException("JSONObject[" + key + "] is not a JSONArray."); - } - return this; - } - - /** - * Get the value object associated with a key. - * - * @param key A key string. - * @return The object associated with the key. - * @throws JSONException if the key is not found. - */ - public Object get(String key) throws JSONException { - if (key == null) { - throw new JSONException("Null key."); - } - Object object = this.opt(key); - if (object == null) { - throw new JSONException("JSONObject[" + quote(key) + "] not found."); - } - return object; - } - - /** - * Get the enum value associated with a key. - * - * @param clazz The type of enum to retrieve. - * @param key A key string. - * @return The enum value associated with the key - * @throws JSONException if the key is not found or if the value cannot be converted - * to an enum. - */ - public > E getEnum(Class clazz, String key) throws JSONException { - E val = optEnum(clazz, key); - if (val == null) { - // JSONException should really take a throwable argument. - // If it did, I would re-implement this with the Enum.valueOf - // method and place any thrown exception in the JSONException - throw new JSONException("JSONObject[" + quote(key) + "] is not an enum of type " + quote(clazz.getSimpleName()) + "."); - } - return val; - } - - /** - * Get the boolean value associated with a key. - * - * @param key A key string. - * @return The truth. - * @throws JSONException if the value is not a Boolean or the String "true" or - * "false". - */ - public boolean getBoolean(String key) throws JSONException { - Object object = this.get(key); - if (object.equals(Boolean.FALSE) || (object instanceof String && ((String) object).equalsIgnoreCase("false"))) { - return false; - } else if (object.equals(Boolean.TRUE) || (object instanceof String && ((String) object).equalsIgnoreCase("true"))) { - return true; - } - throw new JSONException("JSONObject[" + quote(key) + "] is not a Boolean."); - } - - /** - * Get the BigInteger value associated with a key. - * - * @param key A key string. - * @return The numeric value. - * @throws JSONException if the key is not found or if the value cannot be converted - * to BigInteger. - */ - public BigInteger getBigInteger(String key) throws JSONException { - Object object = this.get(key); - try { - return new BigInteger(object.toString()); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONObject[" + quote(key) + "] could not be converted to BigInteger."); - } - } - - /** - * Get the BigDecimal value associated with a key. - * - * @param key A key string. - * @return The numeric value. - * @throws JSONException if the key is not found or if the value cannot be converted - * to BigDecimal. - */ - public BigDecimal getBigDecimal(String key) throws JSONException { - Object object = this.get(key); - try { - return new BigDecimal(object.toString()); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONObject[" + quote(key) + "] could not be converted to BigDecimal."); - } - } - - /** - * Get the double value associated with a key. - * - * @param key A key string. - * @return The numeric value. - * @throws JSONException if the key is not found or if the value is not a Number - * object and cannot be converted to a number. - */ - public double getDouble(String key) throws JSONException { - Object object = this.get(key); - try { - return object instanceof Number ? ((Number) object).doubleValue() : Double.parseDouble((String) object); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONObject[" + quote(key) + "] is not a number."); - } - } - - /** - * Get the int value associated with a key. - * - * @param key A key string. - * @return The integer value. - * @throws JSONException if the key is not found or if the value cannot be converted - * to an integer. - */ - public int getInt(String key) throws JSONException { - Object object = this.get(key); - try { - return object instanceof Number ? ((Number) object).intValue() : Integer.parseInt((String) object); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONObject[" + quote(key) + "] is not an int."); - } - } - - /** - * Get the JSONArray value associated with a key. - * - * @param key A key string. - * @return A JSONArray which is the value. - * @throws JSONException if the key is not found or if the value is not a JSONArray. - */ - public JSONArray getJSONArray(String key) throws JSONException { - Object object = this.get(key); - if (object instanceof JSONArray) { - return (JSONArray) object; - } - throw new JSONException("JSONObject[" + quote(key) + "] is not a JSONArray."); - } - - /** - * Get the JSONObject value associated with a key. - * - * @param key A key string. - * @return A JSONObject which is the value. - * @throws JSONException if the key is not found or if the value is not a JSONObject. - */ - public JSONObject getJSONObject(String key) throws JSONException { - Object object = this.get(key); - if (object instanceof JSONObject) { - return (JSONObject) object; - } - throw new JSONException("JSONObject[" + quote(key) + "] is not a JSONObject."); - } - - /** - * Get the long value associated with a key. - * - * @param key A key string. - * @return The long value. - * @throws JSONException if the key is not found or if the value cannot be converted - * to a long. - */ - public long getLong(String key) throws JSONException { - Object object = this.get(key); - try { - return object instanceof Number ? ((Number) object).longValue() : Long.parseLong((String) object); - } catch (Exception e) { - Iris.reportError(e); - throw new JSONException("JSONObject[" + quote(key) + "] is not a long."); - } - } - - /** - * Get the string associated with a key. - * - * @param key A key string. - * @return A string which is the value. - * @throws JSONException if there is no string value for the key. - */ - public String getString(String key) throws JSONException { - Object object = this.get(key); - if (object instanceof String) { - return (String) object; - } - throw new JSONException("JSONObject[" + quote(key) + "] not a string."); - } - - /** - * Determine if the JSONObject contains a specific key. - * - * @param key A key string. - * @return true if the key exists in the JSONObject. - */ - public boolean has(String key) { - return this.map.containsKey(key); - } - - /** - * Increment a property of a JSONObject. If there is no such property, - * create one with a value of 1. If there is such a property, and if it is - * an Integer, Long, Double, or Float, then add one to it. - * - * @param key A key string. - * @return this. - * @throws JSONException If there is already a property with this name that is not an - * Integer, Long, Double, or Float. - */ - public JSONObject increment(String key) throws JSONException { - Object value = this.opt(key); - if (value == null) { - this.put(key, 1); - } else if (value instanceof BigInteger) { - this.put(key, ((BigInteger) value).add(BigInteger.ONE)); - } else if (value instanceof BigDecimal) { - this.put(key, ((BigDecimal) value).add(BigDecimal.ONE)); - } else if (value instanceof Integer) { - this.put(key, (Integer) value + 1); - } else if (value instanceof Long) { - this.put(key, (Long) value + 1); - } else if (value instanceof Double) { - this.put(key, (Double) value + 1); - } else if (value instanceof Float) { - this.put(key, (Float) value + 1); - } else { - throw new JSONException("Unable to increment [" + quote(key) + "]."); - } - return this; - } - - /** - * Determine if the value associated with the key is null or if there is no - * value. - * - * @param key A key string. - * @return true if there is no value associated with the key or if the value - * is the JSONObject.NULL object. - */ - public boolean isNull(String key) { - return JSONObject.NULL.equals(this.opt(key)); - } - - /** - * Get an enumeration of the keys of the JSONObject. - * - * @return An iterator of the keys. - */ - public Iterator keys() { - return this.keySet().iterator(); - } - - /** - * Get a set of keys of the JSONObject. - * - * @return A keySet. - */ - public Set keySet() { - return this.map.keySet(); - } - - /** - * Get the number of keys stored in the JSONObject. - * - * @return The number of keys in the JSONObject. - */ - public int length() { - return this.map.size(); - } - - /** - * Produce a JSONArray containing the names of the elements of this - * JSONObject. - * - * @return A JSONArray containing the key strings, or null if the JSONObject - * is empty. - */ - public JSONArray names() { - JSONArray ja = new JSONArray(); - Iterator keys = this.keys(); - while (keys.hasNext()) { - ja.put(keys.next()); - } - return ja.length() == 0 ? null : ja; - } - - /** - * Get an optional value associated with a key. - * - * @param key A key string. - * @return An object which is the value, or null if there is no value. - */ - public Object opt(String key) { - return key == null ? null : this.map.get(key); - } - - /** - * Get the enum value associated with a key. - * - * @param clazz The type of enum to retrieve. - * @param key A key string. - * @return The enum value associated with the key or null if not found - */ - public > E optEnum(Class clazz, String key) { - return this.optEnum(clazz, key, null); - } - - /** - * Get the enum value associated with a key. - * - * @param clazz The type of enum to retrieve. - * @param key A key string. - * @param defaultValue The default in case the value is not found - * @return The enum value associated with the key or defaultValue if the - * value is not found or cannot be assigned to clazz - */ - public > E optEnum(Class clazz, String key, E defaultValue) { - try { - Object val = this.opt(key); - if (NULL.equals(val)) { - return defaultValue; - } - if (clazz.isAssignableFrom(val.getClass())) { - // we just checked it! - @SuppressWarnings("unchecked") - E myE = (E) val; - return myE; - } - return Enum.valueOf(clazz, val.toString()); - } catch (IllegalArgumentException e) { - Iris.reportError(e); - return defaultValue; - } catch (NullPointerException e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get an optional boolean associated with a key. It returns false if there - * is no such key, or if the value is not Boolean.TRUE or the String "true". - * - * @param key A key string. - * @return The truth. - */ - public boolean optBoolean(String key) { - return this.optBoolean(key, false); - } - - /** - * Get an optional boolean associated with a key. It returns the - * defaultValue if there is no such key, or if it is not a Boolean or the - * String "true" or "false" (case insensitive). - * - * @param key A key string. - * @param defaultValue The default. - * @return The truth. - */ - public boolean optBoolean(String key, boolean defaultValue) { - try { - return this.getBoolean(key); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get an optional double associated with a key, or NaN if there is no such - * key or if its value is not a number. If the value is a string, an attempt - * will be made to evaluate it as a number. - * - * @param key A string which is the key. - * @return An object which is the value. - */ - public double optDouble(String key) { - return this.optDouble(key, Double.NaN); - } - - /** - * Get an optional BigInteger associated with a key, or the defaultValue if - * there is no such key or if its value is not a number. If the value is a - * string, an attempt will be made to evaluate it as a number. - * - * @param key A key string. - * @param defaultValue The default. - * @return An object which is the value. - */ - public BigInteger optBigInteger(String key, BigInteger defaultValue) { - try { - return this.getBigInteger(key); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get an optional BigDecimal associated with a key, or the defaultValue if - * there is no such key or if its value is not a number. If the value is a - * string, an attempt will be made to evaluate it as a number. - * - * @param key A key string. - * @param defaultValue The default. - * @return An object which is the value. - */ - public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { - try { - return this.getBigDecimal(key); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get an optional double associated with a key, or the defaultValue if - * there is no such key or if its value is not a number. If the value is a - * string, an attempt will be made to evaluate it as a number. - * - * @param key A key string. - * @param defaultValue The default. - * @return An object which is the value. - */ - public double optDouble(String key, double defaultValue) { - try { - return this.getDouble(key); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get an optional int value associated with a key, or zero if there is no - * such key or if the value is not a number. If the value is a string, an - * attempt will be made to evaluate it as a number. - * - * @param key A key string. - * @return An object which is the value. - */ - public int optInt(String key) { - return this.optInt(key, 0); - } - - /** - * Get an optional int value associated with a key, or the default if there - * is no such key or if the value is not a number. If the value is a string, - * an attempt will be made to evaluate it as a number. - * - * @param key A key string. - * @param defaultValue The default. - * @return An object which is the value. - */ - public int optInt(String key, int defaultValue) { - try { - return this.getInt(key); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get an optional JSONArray associated with a key. It returns null if there - * is no such key, or if its value is not a JSONArray. - * - * @param key A key string. - * @return A JSONArray which is the value. - */ - public JSONArray optJSONArray(String key) { - Object o = this.opt(key); - return o instanceof JSONArray ? (JSONArray) o : null; - } - - /** - * Get an optional JSONObject associated with a key. It returns null if - * there is no such key, or if its value is not a JSONObject. - * - * @param key A key string. - * @return A JSONObject which is the value. - */ - public JSONObject optJSONObject(String key) { - Object object = this.opt(key); - return object instanceof JSONObject ? (JSONObject) object : null; - } - - /** - * Get an optional long value associated with a key, or zero if there is no - * such key or if the value is not a number. If the value is a string, an - * attempt will be made to evaluate it as a number. - * - * @param key A key string. - * @return An object which is the value. - */ - public long optLong(String key) { - return this.optLong(key, 0); - } - - /** - * Get an optional long value associated with a key, or the default if there - * is no such key or if the value is not a number. If the value is a string, - * an attempt will be made to evaluate it as a number. - * - * @param key A key string. - * @param defaultValue The default. - * @return An object which is the value. - */ - public long optLong(String key, long defaultValue) { - try { - return this.getLong(key); - } catch (Exception e) { - Iris.reportError(e); - return defaultValue; - } - } - - /** - * Get an optional string associated with a key. It returns an empty string - * if there is no such key. If the value is not a string and is not null, - * then it is converted to a string. - * - * @param key A key string. - * @return A string which is the value. - */ - public String optString(String key) { - return this.optString(key, ""); - } - - /** - * Get an optional string associated with a key. It returns the defaultValue - * if there is no such key. - * - * @param key A key string. - * @param defaultValue The default. - * @return A string which is the value. - */ - public String optString(String key, String defaultValue) { - Object object = this.opt(key); - return NULL.equals(object) ? defaultValue : object.toString(); - } - - private void populateMap(Object bean) { - Class klass = bean.getClass(); - - // If klass is a System class then set includeSuperClass to false. - - boolean includeSuperClass = klass.getClassLoader() != null; - - Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); - for (int i = 0; i < methods.length; i += 1) { - try { - Method method = methods[i]; - if (Modifier.isPublic(method.getModifiers())) { - String name = method.getName(); - String key = ""; - if (name.startsWith("get")) { - if ("getClass".equals(name) || "getDeclaringClass".equals(name)) { - key = ""; - } else { - key = name.substring(3); - } - } else if (name.startsWith("is")) { - key = name.substring(2); - } - if (key.length() > 0 && Character.isUpperCase(key.charAt(0)) && method.getParameterTypes().length == 0) { - if (key.length() == 1) { - key = key.toLowerCase(); - } else if (!Character.isUpperCase(key.charAt(1))) { - key = key.substring(0, 1).toLowerCase() + key.substring(1); - } - - Object result = method.invoke(bean, (Object[]) null); - if (result != null) { - this.map.put(key, wrap(result)); - } - } - } - } catch (Exception e) { - Iris.reportError(e); - } - } - } - - /** - * Put a key/boolean pair in the JSONObject. - * - * @param key A key string. - * @param value A boolean which is the value. - * @return this. - * @throws JSONException If the key is null. - */ - public JSONObject put(String key, boolean value) throws JSONException { - this.put(key, value ? Boolean.TRUE : Boolean.FALSE); - return this; - } - - /** - * Put a key/value pair in the JSONObject, where the value will be a - * JSONArray which is produced from a Collection. - * - * @param key A key string. - * @param value A Collection value. - * @return this. - */ - public JSONObject put(String key, Collection value) throws JSONException { - this.put(key, new JSONArray(value)); - return this; - } - - /** - * Put a key/double pair in the JSONObject. - * - * @param key A key string. - * @param value A double which is the value. - * @return this. - * @throws JSONException If the key is null or if the number is invalid. - */ - public JSONObject put(String key, double value) throws JSONException { - this.put(key, Double.valueOf(value)); - return this; - } - - /** - * Put a key/int pair in the JSONObject. - * - * @param key A key string. - * @param value An int which is the value. - * @return this. - * @throws JSONException If the key is null. - */ - public JSONObject put(String key, int value) throws JSONException { - this.put(key, Integer.valueOf(value)); - return this; - } - - /** - * Put a key/long pair in the JSONObject. - * - * @param key A key string. - * @param value A long which is the value. - * @return this. - * @throws JSONException If the key is null. - */ - public JSONObject put(String key, long value) throws JSONException { - this.put(key, Long.valueOf(value)); - return this; - } - - /** - * Put a key/value pair in the JSONObject, where the value will be a - * JSONObject which is produced from a Map. - * - * @param key A key string. - * @param value A Map value. - * @return this. - */ - public JSONObject put(String key, Map value) throws JSONException { - this.put(key, new JSONObject(value)); - return this; - } - - /** - * Put a key/value pair in the JSONObject. If the value is null, then the - * key will be removed from the JSONObject if it is present. - * - * @param key A key string. - * @param value An object which is the value. It should be of one of these - * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, - * String, or the JSONObject.NULL object. - * @return this. - * @throws JSONException If the value is non-finite number or if the key is null. - */ - public JSONObject put(String key, Object value) throws JSONException { - if (key == null) { - throw new NullPointerException("Null key."); - } - if (value != null) { - testValidity(value); - this.map.put(key, value); - } else { - this.remove(key); - } - return this; - } - - /** - * Put a key/value pair in the JSONObject, but only if the key and the value - * are both non-null, and only if there is not already a member with that - * name. - * - * @param key string - * @param value object - * @return this. - * @throws JSONException if the key is a duplicate - */ - public JSONObject putOnce(String key, Object value) throws JSONException { - if (key != null && value != null) { - if (this.opt(key) != null) { - throw new JSONException("Duplicate key \"" + key + "\""); - } - this.put(key, value); - } - return this; - } - - /** - * Put a key/value pair in the JSONObject, but only if the key and the value - * are both non-null. - * - * @param key A key string. - * @param value An object which is the value. It should be of one of these - * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, - * String, or the JSONObject.NULL object. - * @return this. - * @throws JSONException If the value is a non-finite number. - */ - public JSONObject putOpt(String key, Object value) throws JSONException { - if (key != null && value != null) { - this.put(key, value); - } - return this; - } - - /** - * Remove a name and its value, if present. - * - * @param key The name to be removed. - * @return The value that was associated with the name, or null if there was - * no value. - */ - public Object remove(String key) { - return this.map.remove(key); - } - - /** - * Determine if two JSONObjects are similar. They must contain the same set - * of names which must be associated with similar values. - * - * @param other The other JSONObject - * @return true if they are equal - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean similar(Object other) { - try { - if (!(other instanceof JSONObject)) { - return false; - } - Set set = this.keySet(); - if (!set.equals(((JSONObject) other).keySet())) { - return false; - } - Iterator iterator = set.iterator(); - while (iterator.hasNext()) { - String name = iterator.next(); - Object valueThis = this.get(name); - Object valueOther = ((JSONObject) other).get(name); - if (valueThis instanceof JSONObject) { - if (!((JSONObject) valueThis).similar(valueOther)) { - return false; - } - } else if (valueThis instanceof JSONArray) { - if (!((JSONArray) valueThis).similar(valueOther)) { - return false; - } - } else if (!valueThis.equals(valueOther)) { - return false; - } - } - return true; - } catch (Throwable e) { - Iris.reportError(e); - return false; - } - } - - /** - * Produce a JSONArray containing the values of the members of this - * JSONObject. - * - * @param names A JSONArray containing a list of key strings. This determines - * the sequence of the values in the result. - * @return A JSONArray of values. - * @throws JSONException If any of the values are non-finite numbers. - */ - public JSONArray toJSONArray(JSONArray names) throws JSONException { - if (names == null || names.length() == 0) { - return null; - } - JSONArray ja = new JSONArray(); - for (int i = 0; i < names.length(); i += 1) { - ja.put(this.opt(names.getString(i))); - } - return ja; - } - - /** - * Make a JSON text of this JSONObject. For compactness, no whitespace is - * added. If this would not result in a syntactically correct JSON text, - * then null will be returned instead. - *

- * Warning: This method assumes that the data structure is acyclical. - * - * @return a printable, displayable, portable, transmittable representation - * of the object, beginning with { (left - * brace) and ending with } (right - * brace). - */ - public String toString() { - try { - return this.toString(0); - } catch (Exception e) { - Iris.reportError(e); - return null; - } - } - - /** - * Make a prettyprinted JSON text of this JSONObject. - *

- * Warning: This method assumes that the data structure is acyclical. - * - * @param indentFactor The number of spaces to add to each level of indentation. - * @return a printable, displayable, portable, transmittable representation - * of the object, beginning with { (left - * brace) and ending with } (right - * brace). - * @throws JSONException If the object contains an invalid number. - */ - public String toString(int indentFactor) throws JSONException { - StringWriter w = new StringWriter(); - synchronized (w.getBuffer()) { - return this.write(w, indentFactor, 0).toString(); - } - } - - /** - * Write the contents of the JSONObject as JSON text to a writer. For - * compactness, no whitespace is added. - *

- * Warning: This method assumes that the data structure is acyclical. - * - * @return The writer. - */ - public Writer write(Writer writer) throws JSONException { - return this.write(writer, 0, 0); - } - - /** - * Write the contents of the JSONObject as JSON text to a writer. For - * compactness, no whitespace is added. - *

- * Warning: This method assumes that the data structure is acyclical. - * - * @return The writer. - */ - Writer write(Writer writer, int indentFactor, int indent) throws JSONException { - try { - boolean commanate = false; - final int length = this.length(); - Iterator keys = this.keys(); - writer.write('{'); - - if (length == 1) { - Object key = keys.next(); - writer.write(quote(key.toString())); - writer.write(':'); - if (indentFactor > 0) { - writer.write(' '); - } - writeValue(writer, this.map.get(key), indentFactor, indent); - } else if (length != 0) { - final int newindent = indent + indentFactor; - while (keys.hasNext()) { - Object key = keys.next(); - if (commanate) { - writer.write(','); - } - if (indentFactor > 0) { - writer.write('\n'); - } - indent(writer, newindent); - writer.write(quote(key.toString())); - writer.write(':'); - if (indentFactor > 0) { - writer.write(' '); - } - writeValue(writer, this.map.get(key), indentFactor, newindent); - commanate = true; - } - if (indentFactor > 0) { - writer.write('\n'); - } - indent(writer, indent); - } - writer.write('}'); - return writer; - } catch (IOException e) { - Iris.reportError(e); - throw new JSONException(e); - } - } - - /** - * JSONObject.NULL is equivalent to the value that JavaScript calls null, - * whilst Java's null is equivalent to the value that JavaScript calls - * undefined. - */ - private static final class Null { - - /** - * There is only intended to be a single instance of the NULL object, so - * the clone method returns itself. - * - * @return NULL. - */ - @Override - protected final Object clone() { - return this; - } - - /** - * A Null object is equal to the null value and to itself. - * - * @param object An object to test for nullness. - * @return true if the object parameter is the JSONObject.NULL object or - * null. - */ - @Override - public boolean equals(Object object) { - return object == null || object == this; - } - - /** - * Get the "null" string value. - * - * @return The string "null". - */ - public String toString() { - return "null"; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/JSONString.java b/core/src/main/java/com/volmit/iris/util/json/JSONString.java deleted file mode 100644 index 0facad9fa..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/JSONString.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -/** - * The JSONString interface allows a toJSONString() - * method so that a class can change the behavior of - * JSONObject.toString(), JSONArray.toString(), and - * JSONWriter.value(Object). The - * toJSONString method will be used instead of the default behavior - * of using the Object's toString() method and quoting the result. - */ -public interface JSONString { - /** - * The toJSONString method allows a class to produce its own - * JSON serialization. - * - * @return A strictly syntactically correct JSON text. - */ - String toJSONString(); -} diff --git a/core/src/main/java/com/volmit/iris/util/json/JSONStringer.java b/core/src/main/java/com/volmit/iris/util/json/JSONStringer.java deleted file mode 100644 index a9ab7ed27..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/JSONStringer.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -import java.io.StringWriter; - -/** - * JSONStringer provides a quick and convenient way of producing JSON text. The - * texts produced strictly conform to JSON syntax rules. No whitespace is added, - * so the results are ready for transmission or storage. Each instance of - * JSONStringer can produce one JSON text. - *

- * A JSONStringer instance provides a value method for appending - * values to the text, and a key method for adding keys before - * values in objects. There are array and endArray - * methods that make and bound array values, and object and - * endObject methods which make and bound object values. All of - * these methods return the JSONWriter instance, permitting cascade style. For - * example, - * - *

- * myString = new JSONStringer().object().key("JSON").value("Hello, World!").endObject().toString();
- * 
- *

- * which produces the string - * - *

- * {"JSON":"Hello, World!"}
- * 
- *

- * The first method called must be array or object. - * There are no methods for adding commas or colons. JSONStringer adds them for - * you. Objects and arrays can be nested up to 20 levels deep. - *

- * This can sometimes be easier than using a JSONObject to build a string. - * - * @author JSON.org - * @version 2008-09-18 - */ -public class JSONStringer extends JSONWriter { - /** - * Make a fresh JSONStringer. It can be used to build one JSON text. - */ - public JSONStringer() { - super(new StringWriter()); - } - - /** - * Return the JSON text. This method is used to obtain the product of the - * JSONStringer instance. It will return null if there was a - * problem in the construction of the JSON text (such as the calls to - * array were not properly balanced with calls to - * endArray). - * - * @return The JSON text. - */ - public String toString() { - return this.mode == 'd' ? this.writer.toString() : null; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/JSONTokener.java b/core/src/main/java/com/volmit/iris/util/json/JSONTokener.java deleted file mode 100644 index 6049a4d1b..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/JSONTokener.java +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -import com.volmit.iris.Iris; - -import java.io.*; - -/** - * A JSONTokener takes a source string and extracts characters and tokens from - * it. It is used by the JSONObject and JSONArray constructors to parse JSON - * source strings. - * - * @author JSON.org - * @version 2014-05-03 - */ -@SuppressWarnings("ALL") -public class JSONTokener { - - private final Reader reader; - private long character; - private boolean eof; - private long index; - private long line; - private char previous; - private boolean usePrevious; - - /** - * Construct a JSONTokener from a Reader. - * - * @param reader A reader. - */ - public JSONTokener(Reader reader) { - this.reader = reader.markSupported() ? reader : new BufferedReader(reader); - this.eof = false; - this.usePrevious = false; - this.previous = 0; - this.index = 0; - this.character = 1; - this.line = 1; - } - - /** - * Construct a JSONTokener from an InputStream. - * - * @param inputStream The source. - */ - public JSONTokener(InputStream inputStream) throws JSONException { - this(new InputStreamReader(inputStream)); - } - - /** - * Construct a JSONTokener from a string. - * - * @param s A source string. - */ - public JSONTokener(String s) { - this(new StringReader(s)); - } - - /** - * Get the hex value of a character (base16). - * - * @param c A character between '0' and '9' or between 'A' and 'F' or - * between 'a' and 'f'. - * @return An int between 0 and 15, or -1 if c was not a hex digit. - */ - public static int dehexchar(char c) { - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'A' && c <= 'F') { - return c - ('A' - 10); - } - if (c >= 'a' && c <= 'f') { - return c - ('a' - 10); - } - return -1; - } - - /** - * Back up one character. This provides a sort of lookahead capability, so - * that you can test for a digit or letter before attempting to parse the - * next number or identifier. - */ - public void back() throws JSONException { - if (this.usePrevious || this.index <= 0) { - throw new JSONException("Stepping back two steps is not supported"); - } - this.index -= 1; - this.character -= 1; - this.usePrevious = true; - this.eof = false; - } - - public boolean end() { - return this.eof && !this.usePrevious; - } - - /** - * Determine if the source string still contains characters that next() can - * consume. - * - * @return true if not yet at the end of the source. - */ - public boolean more() throws JSONException { - this.next(); - if (this.end()) { - return false; - } - this.back(); - return true; - } - - /** - * Get the next character in the source string. - * - * @return The next character, or 0 if past the end of the source string. - */ - public char next() throws JSONException { - int c; - if (this.usePrevious) { - this.usePrevious = false; - c = this.previous; - } else { - try { - c = this.reader.read(); - } catch (IOException e) { - Iris.reportError(e); - throw new JSONException(e); - } - - if (c <= 0) { // End of stream - this.eof = true; - c = 0; - } - } - this.index += 1; - if (this.previous == '\r') { - this.line += 1; - this.character = c == '\n' ? 0 : 1; - } else if (c == '\n') { - this.line += 1; - this.character = 0; - } else { - this.character += 1; - } - this.previous = (char) c; - return this.previous; - } - - /** - * Consume the next character, and check that it matches a specified - * character. - * - * @param c The character to match. - * @return The character. - * @throws JSONException if the character does not match. - */ - public char next(char c) throws JSONException { - char n = this.next(); - if (n != c) { - throw this.syntaxError("Expected '" + c + "' and instead saw '" + n + "'"); - } - return n; - } - - /** - * Get the next n characters. - * - * @param n The number of characters to take. - * @return A string of n characters. - * @throws JSONException Substring bounds error if there are not n characters - * remaining in the source string. - */ - public String next(int n) throws JSONException { - if (n == 0) { - return ""; - } - - char[] chars = new char[n]; - int pos = 0; - - while (pos < n) { - chars[pos] = this.next(); - if (this.end()) { - throw this.syntaxError("Substring bounds error"); - } - pos += 1; - } - return new String(chars); - } - - /** - * Get the next char in the string, skipping whitespace. - * - * @return A character, or 0 if there are no more characters. - */ - public char nextClean() throws JSONException { - for (; ; ) { - char c = this.next(); - if (c == 0 || c > ' ') { - return c; - } - } - } - - /** - * Return the characters up to the next close quote character. Backslash - * processing is done. The formal JSON format does not allow strings in - * single quotes, but an implementation is allowed to accept them. - * - * @param quote The quoting character, either "  - * (double quote) or '  - * (single quote). - * @return A String. - * @throws JSONException Unterminated string. - */ - public String nextString(char quote) throws JSONException { - char c; - StringBuilder sb = new StringBuilder(); - for (; ; ) { - c = this.next(); - switch (c) { - case 0: - case '\n': - case '\r': - throw this.syntaxError("Unterminated string"); - case '\\': - c = this.next(); - switch (c) { - case 'b': - sb.append('\b'); - break; - case 't': - sb.append('\t'); - break; - case 'n': - sb.append('\n'); - break; - case 'f': - sb.append('\f'); - break; - case 'r': - sb.append('\r'); - break; - case 'u': - sb.append((char) Integer.parseInt(this.next(4), 16)); - break; - case '"': - case '\'': - case '\\': - case '/': - sb.append(c); - break; - default: - throw this.syntaxError("Illegal escape."); - } - break; - default: - if (c == quote) { - return sb.toString(); - } - sb.append(c); - } - } - } - - /** - * Get the text up but not including the specified character or the end of - * line, whichever comes first. - * - * @param delimiter A delimiter character. - * @return A string. - */ - public String nextTo(char delimiter) throws JSONException { - StringBuilder sb = new StringBuilder(); - for (; ; ) { - char c = this.next(); - if (c == delimiter || c == 0 || c == '\n' || c == '\r') { - if (c != 0) { - this.back(); - } - return sb.toString().trim(); - } - sb.append(c); - } - } - - /** - * Get the text up but not including one of the specified delimiter - * characters or the end of line, whichever comes first. - * - * @param delimiters A set of delimiter characters. - * @return A string, trimmed. - */ - public String nextTo(String delimiters) throws JSONException { - char c; - StringBuilder sb = new StringBuilder(); - for (; ; ) { - c = this.next(); - if (delimiters.indexOf(c) >= 0 || c == 0 || c == '\n' || c == '\r') { - if (c != 0) { - this.back(); - } - return sb.toString().trim(); - } - sb.append(c); - } - } - - /** - * Get the next value. The value can be a Boolean, Double, Integer, - * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. - * - * @return An object. - * @throws JSONException If syntax error. - */ - public Object nextValue() throws JSONException { - char c = this.nextClean(); - String string; - - switch (c) { - case '"': - case '\'': - return this.nextString(c); - case '{': - this.back(); - return new JSONObject(this); - case '[': - this.back(); - return new JSONArray(this); - } - - /* - * Handle unquoted text. This could be the values true, false, or null, - * or it can be a number. An implementation (such as this one) is - * allowed to also accept non-standard forms. - * - * Accumulate characters until we reach the end of the text or a - * formatting character. - */ - - StringBuilder sb = new StringBuilder(); - while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { - sb.append(c); - c = this.next(); - } - this.back(); - - string = sb.toString().trim(); - if ("".equals(string)) { - throw this.syntaxError("Missing value"); - } - return JSONObject.stringToValue(string); - } - - /** - * Skip characters until the next character is the requested character. If - * the requested character is not found, no characters are skipped. - * - * @param to A character to skip to. - * @return The requested character, or zero if the requested character is - * not found. - */ - public char skipTo(char to) throws JSONException { - char c; - try { - long startIndex = this.index; - long startCharacter = this.character; - long startLine = this.line; - this.reader.mark(1000000); - do { - c = this.next(); - if (c == 0) { - this.reader.reset(); - this.index = startIndex; - this.character = startCharacter; - this.line = startLine; - return c; - } - } while (c != to); - } catch (IOException e) { - Iris.reportError(e); - throw new JSONException(e); - } - this.back(); - return c; - } - - /** - * Make a JSONException to signal a syntax error. - * - * @param message The error message. - * @return A JSONException object, suitable for throwing - */ - public JSONException syntaxError(String message) { - return new JSONException(message + this); - } - - /** - * Make a printable string of this JSONTokener. - * - * @return " at {index} [character {character} line {line}]" - */ - public String toString() { - return " at " + this.index + " [character " + this.character + " line " + this.line + "]"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/JSONWriter.java b/core/src/main/java/com/volmit/iris/util/json/JSONWriter.java deleted file mode 100644 index e9d212b68..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/JSONWriter.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -import com.volmit.iris.Iris; - -import java.io.IOException; -import java.io.Writer; - -/** - * JSONWriter provides a quick and convenient way of producing JSON text. The - * texts produced strictly conform to JSON syntax rules. No whitespace is added, - * so the results are ready for transmission or storage. Each instance of - * JSONWriter can produce one JSON text. - *

- * A JSONWriter instance provides a value method for appending - * values to the text, and a key method for adding keys before - * values in objects. There are array and endArray - * methods that make and bound array values, and object and - * endObject methods which make and bound object values. All of - * these methods return the JSONWriter instance, permitting a cascade style. For - * example, - * - *

- * new JSONWriter(myWriter).object().key("JSON").value("Hello, World!").endObject();
- * 
- *

- * which writes - * - *

- * {"JSON":"Hello, World!"}
- * 
- *

- * The first method called must be array or object. - * There are no methods for adding commas or colons. JSONWriter adds them for - * you. Objects and arrays can be nested up to 20 levels deep. - *

- * This can sometimes be easier than using a JSONObject to build a string. - * - * @author JSON.org - * @version 2011-11-24 - */ -public class JSONWriter { - private static final int maxdepth = 200; - /** - * The writer that will receive the output. - */ - protected final Writer writer; - /** - * The object/array stack. - */ - private final JSONObject[] stack; - /** - * The current mode. Values: 'a' (array), 'd' (done), 'i' (initial), 'k' - * (key), 'o' (object). - */ - protected char mode; - /** - * The comma flag determines if a comma should be output before the next - * value. - */ - private boolean comma; - /** - * The stack top index. A value of 0 indicates that the stack is empty. - */ - private int top; - - /** - * Make a fresh JSONWriter. It can be used to build one JSON text. - */ - public JSONWriter(Writer w) { - this.comma = false; - this.mode = 'i'; - this.stack = new JSONObject[maxdepth]; - this.top = 0; - this.writer = w; - } - - /** - * Append a value. - * - * @param string A string value. - * @return this - * @throws JSONException If the value is out of sequence. - */ - private JSONWriter append(String string) throws JSONException { - if (string == null) { - throw new JSONException("Null pointer"); - } - if (this.mode == 'o' || this.mode == 'a') { - try { - if (this.comma && this.mode == 'a') { - this.writer.write(','); - } - this.writer.write(string); - } catch (IOException e) { - Iris.reportError(e); - throw new JSONException(e); - } - if (this.mode == 'o') { - this.mode = 'k'; - } - this.comma = true; - return this; - } - throw new JSONException("Value out of sequence."); - } - - /** - * Begin appending a new array. All values until the balancing - * endArray will be appended to this array. The - * endArray method must be called to mark the array's end. - * - * @return this - * @throws JSONException If the nesting is too deep, or if the object is started in - * the wrong place (for example as a key or after the end of the - * outermost array or object). - */ - public JSONWriter array() throws JSONException { - if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { - this.push(null); - this.append("["); - this.comma = false; - return this; - } - throw new JSONException("Misplaced array."); - } - - /** - * End something. - * - * @param mode Mode - * @param c Closing character - * @return this - * @throws JSONException If unbalanced. - */ - private JSONWriter end(char mode, char c) throws JSONException { - if (this.mode != mode) { - throw new JSONException(mode == 'a' ? "Misplaced endArray." : "Misplaced endObject."); - } - this.pop(mode); - try { - this.writer.write(c); - } catch (IOException e) { - Iris.reportError(e); - throw new JSONException(e); - } - this.comma = true; - return this; - } - - /** - * End an array. This method most be called to balance calls to - * array. - * - * @return this - * @throws JSONException If incorrectly nested. - */ - public JSONWriter endArray() throws JSONException { - return this.end('a', ']'); - } - - /** - * End an object. This method most be called to balance calls to - * object. - * - * @return this - * @throws JSONException If incorrectly nested. - */ - public JSONWriter endObject() throws JSONException { - return this.end('k', '}'); - } - - /** - * Append a key. The key will be associated with the next value. In an - * object, every value must be preceded by a key. - * - * @param string A key string. - * @return this - * @throws JSONException If the key is out of place. For example, keys do not belong - * in arrays or if the key is null. - */ - public JSONWriter key(String string) throws JSONException { - if (string == null) { - throw new JSONException("Null key."); - } - if (this.mode == 'k') { - try { - this.stack[this.top - 1].putOnce(string, Boolean.TRUE); - if (this.comma) { - this.writer.write(','); - } - this.writer.write(JSONObject.quote(string)); - this.writer.write(':'); - this.comma = false; - this.mode = 'o'; - return this; - } catch (IOException e) { - Iris.reportError(e); - throw new JSONException(e); - } - } - throw new JSONException("Misplaced key."); - } - - /** - * Begin appending a new object. All keys and values until the balancing - * endObject will be appended to this object. The - * endObject method must be called to mark the object's end. - * - * @return this - * @throws JSONException If the nesting is too deep, or if the object is started in - * the wrong place (for example as a key or after the end of the - * outermost array or object). - */ - public JSONWriter object() throws JSONException { - if (this.mode == 'i') { - this.mode = 'o'; - } - if (this.mode == 'o' || this.mode == 'a') { - this.append("{"); - this.push(new JSONObject()); - this.comma = false; - return this; - } - throw new JSONException("Misplaced object."); - - } - - /** - * Pop an array or object scope. - * - * @param c The scope to close. - * @throws JSONException If nesting is wrong. - */ - private void pop(char c) throws JSONException { - if (this.top <= 0) { - throw new JSONException("Nesting error."); - } - char m = this.stack[this.top - 1] == null ? 'a' : 'k'; - if (m != c) { - throw new JSONException("Nesting error."); - } - this.top -= 1; - this.mode = this.top == 0 ? 'd' : this.stack[this.top - 1] == null ? 'a' : 'k'; - } - - /** - * Push an array or object scope. - * - * @param jo The scope to open. - * @throws JSONException If nesting is too deep. - */ - private void push(JSONObject jo) throws JSONException { - if (this.top >= maxdepth) { - throw new JSONException("Nesting too deep."); - } - this.stack[this.top] = jo; - this.mode = jo == null ? 'a' : 'k'; - this.top += 1; - } - - /** - * Append either the value true or the value false - * . - * - * @param b A boolean. - * @return this - */ - public JSONWriter value(boolean b) throws JSONException { - return this.append(b ? "true" : "false"); - } - - /** - * Append a double value. - * - * @param d A double. - * @return this - * @throws JSONException If the number is not finite. - */ - public JSONWriter value(double d) throws JSONException { - return this.value(Double.valueOf(d)); - } - - /** - * Append a long value. - * - * @param l A long. - * @return this - */ - public JSONWriter value(long l) throws JSONException { - return this.append(Long.toString(l)); - } - - /** - * Append an object value. - * - * @param object The object to append. It can be null, or a Boolean, Number, - * String, JSONObject, or JSONArray, or an object that implements - * JSONString. - * @return this - * @throws JSONException If the value is out of sequence. - */ - public JSONWriter value(Object object) throws JSONException { - return this.append(JSONObject.valueToString(object)); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/XML.java b/core/src/main/java/com/volmit/iris/util/json/XML.java deleted file mode 100644 index 13e665eff..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/XML.java +++ /dev/null @@ -1,489 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -import com.volmit.iris.Iris; - -import java.util.Iterator; - -/** - * This provides static methods to convert an XML text into a JSONObject, and to - * covert a JSONObject into an XML text. - * - * @author JSON.org - * @version 2014-05-03 - */ -public class XML { - - /** - * The Character '&'. - */ - public static final Character AMP = '&'; - - /** - * The Character '''. - */ - public static final Character APOS = '\''; - - /** - * The Character '!'. - */ - public static final Character BANG = '!'; - - /** - * The Character '='. - */ - public static final Character EQ = '='; - - /** - * The Character '>'. - */ - public static final Character GT = '>'; - - /** - * The Character '<'. - */ - public static final Character LT = '<'; - - /** - * The Character '?'. - */ - public static final Character QUEST = '?'; - - /** - * The Character '"'. - */ - public static final Character QUOT = '"'; - - /** - * The Character '/'. - */ - public static final Character SLASH = '/'; - - /** - * Replace special characters with XML escapes: - * - *

-     * & (ampersand) is replaced by &amp;
-     * < (less than) is replaced by &lt;
-     * > (greater than) is replaced by &gt;
-     * " (double quote) is replaced by &quot;
-     * 
- * - * @param string The string to be escaped. - * @return The escaped string. - */ - public static String escape(String string) { - StringBuilder sb = new StringBuilder(string.length()); - for (int i = 0, length = string.length(); i < length; i++) { - char c = string.charAt(i); - switch (c) { - case '&' -> sb.append("&"); - case '<' -> sb.append("<"); - case '>' -> sb.append(">"); - case '"' -> sb.append("""); - case '\'' -> sb.append("'"); - default -> sb.append(c); - } - } - return sb.toString(); - } - - /** - * Throw an exception if the string contains whitespace. Whitespace is not - * allowed in tagNames and attributes. - * - * @param string A string. - */ - public static void noSpace(String string) throws JSONException { - int i, length = string.length(); - if (length == 0) { - throw new JSONException("Empty string."); - } - for (i = 0; i < length; i += 1) { - if (Character.isWhitespace(string.charAt(i))) { - throw new JSONException("'" + string + "' contains a space character."); - } - } - } - - /** - * Scan the content following the named tag, attaching it to the context. - * - * @param x The XMLTokener containing the source string. - * @param context The JSONObject that will include the new material. - * @param name The tag name. - * @return true if the close tag is processed. - */ - private static boolean parse(XMLTokener x, JSONObject context, String name) throws JSONException { - char c; - int i; - JSONObject jsonobject = null; - String string; - String tagName; - Object token; - - // Test for and skip past these forms: - // - // - // - // - // Report errors for these forms: - // <> - // <= - // << - - token = x.nextToken(); - - // "); - return false; - } - x.back(); - } else if (c == '[') { - token = x.nextToken(); - if ("CDATA".equals(token)) { - if (x.next() == '[') { - string = x.nextCDATA(); - if (string.length() > 0) { - context.accumulate("content", string); - } - return false; - } - } - throw x.syntaxError("Expected 'CDATA['"); - } - i = 1; - do { - token = x.nextMeta(); - if (token == null) { - throw x.syntaxError("Missing '>' after ' 0); - return false; - } else if (token == QUEST) { - - // "); - return false; - } else if (token == SLASH) { - - // Close tag - - } else if (token == SLASH) { - if (x.nextToken() != GT) { - throw x.syntaxError("Misshaped tag"); - } - if (jsonobject.length() > 0) { - context.accumulate(tagName, jsonobject); - } else { - context.accumulate(tagName, ""); - } - return false; - - // Content, between <...> and - - } else if (token == GT) { - for (; ; ) { - token = x.nextContent(); - if (token == null) { - if (tagName != null) { - throw x.syntaxError("Unclosed tag " + tagName); - } - return false; - } else if (token instanceof String) { - string = (String) token; - if (string.length() > 0) { - jsonobject.accumulate("content", XML.stringToValue(string)); - } - - // Nested element - - } else if (token == LT) { - if (parse(x, jsonobject, tagName)) { - if (jsonobject.length() == 0) { - context.accumulate(tagName, ""); - } else if (jsonobject.length() == 1 && jsonobject.opt("content") != null) { - context.accumulate(tagName, jsonobject.opt("content")); - } else { - context.accumulate(tagName, jsonobject); - } - return false; - } - } - } - } else { - throw x.syntaxError("Misshaped tag"); - } - } - } - } - - /** - * Try to convert a string into a number, boolean, or null. If the string - * can't be converted, return the string. This is much less ambitious than - * JSONObject.stringToValue, especially because it does not attempt to - * convert plus forms, octal forms, hex forms, or E forms lacking decimal - * points. - * - * @param string A String. - * @return A simple JSON value. - */ - public static Object stringToValue(String string) { - if ("true".equalsIgnoreCase(string)) { - return Boolean.TRUE; - } - if ("false".equalsIgnoreCase(string)) { - return Boolean.FALSE; - } - if ("null".equalsIgnoreCase(string)) { - return JSONObject.NULL; - } - - // If it might be a number, try converting it, first as a Long, and then - // as a - // Double. If that doesn't work, return the string. - - try { - char initial = string.charAt(0); - if (initial == '-' || (initial >= '0' && initial <= '9')) { - Long value = Long.valueOf(string); - if (value.toString().equals(string)) { - return value; - } - } - } catch (Exception ignore) { - Iris.reportError(ignore); - try { - Double value = Double.valueOf(string); - if (value.toString().equals(string)) { - return value; - } - } catch (Exception e) { - Iris.reportError(e); - } - } - return string; - } - - /** - * Convert a well-formed (but not necessarily valid) XML string into a - * JSONObject. Some information may be lost in this transformation because - * JSON is a data format and XML is a document format. XML uses elements, - * attributes, and content text, while JSON uses unordered collections of - * name/value pairs and arrays of values. JSON does not does not like to - * distinguish between elements and attributes. Sequences of similar - * elements are represented as JSONArrays. Content text may be placed in a - * "content" member. Comments, prologs, DTDs, and <[ [ ]]> - * are ignored. - * - * @param string The source string. - * @return A JSONObject containing the structured data from the XML string. - */ - public static JSONObject toJSONObject(String string) throws JSONException { - JSONObject jo = new JSONObject(); - XMLTokener x = new XMLTokener(string); - while (x.more() && x.skipPast("<")) { - parse(x, jo, null); - } - return jo; - } - - /** - * Convert a JSONObject into a well-formed, element-normal XML string. - * - * @param object A JSONObject. - * @return A string. - */ - public static String toString(Object object) throws JSONException { - return toString(object, null); - } - - /** - * Convert a JSONObject into a well-formed, element-normal XML string. - * - * @param object A JSONObject. - * @param tagName The optional name of the enclosing tag. - * @return A string. - */ - public static String toString(Object object, String tagName) throws JSONException { - StringBuilder sb = new StringBuilder(); - int i; - JSONArray ja; - JSONObject jo; - String key; - Iterator keys; - int length; - String string; - Object value; - if (object instanceof JSONObject) { - - // Emit - - if (tagName != null) { - sb.append('<'); - sb.append(tagName); - sb.append('>'); - } - - // Loop thru the keys. - - jo = (JSONObject) object; - keys = jo.keys(); - while (keys.hasNext()) { - key = keys.next(); - value = jo.opt(key); - if (value == null) { - value = ""; - } - string = value instanceof String ? (String) value : null; - - // Emit content in body - - if ("content".equals(key)) { - if (value instanceof JSONArray) { - ja = (JSONArray) value; - length = ja.length(); - for (i = 0; i < length; i += 1) { - if (i > 0) { - sb.append('\n'); - } - sb.append(escape(ja.get(i).toString())); - } - } else { - sb.append(escape(value.toString())); - } - - // Emit an array of similar keys - - } else if (value instanceof JSONArray) { - ja = (JSONArray) value; - length = ja.length(); - for (i = 0; i < length; i += 1) { - value = ja.get(i); - if (value instanceof JSONArray) { - sb.append('<'); - sb.append(key); - sb.append('>'); - sb.append(toString(value)); - sb.append("'); - } else { - sb.append(toString(value, key)); - } - } - } else if ("".equals(value)) { - sb.append('<'); - sb.append(key); - sb.append("/>"); - - // Emit a new tag - - } else { - sb.append(toString(value, key)); - } - } - if (tagName != null) { - - // Emit the close tag - - sb.append("'); - } - return sb.toString(); - - // XML does not have good support for arrays. If an array appears in - // a place - // where XML is lacking, synthesize an element. - - } else { - if (object.getClass().isArray()) { - object = new JSONArray(object); - } - if (object instanceof JSONArray) { - ja = (JSONArray) object; - length = ja.length(); - for (i = 0; i < length; i += 1) { - sb.append(toString(ja.opt(i), tagName == null ? "array" : tagName)); - } - return sb.toString(); - } else { - string = (object == null) ? "null" : escape(object.toString()); - return (tagName == null) ? "\"" + string + "\"" : (string.length() == 0) ? "<" + tagName + "/>" : "<" + tagName + ">" + string + ""; - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/json/XMLTokener.java b/core/src/main/java/com/volmit/iris/util/json/XMLTokener.java deleted file mode 100644 index a4e214064..000000000 --- a/core/src/main/java/com/volmit/iris/util/json/XMLTokener.java +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.json; - - -/** - * The XMLTokener extends the JSONTokener to provide additional methods for the - * parsing of XML texts. - * - * @author JSON.org - * @version 2014-05-03 - */ -public class XMLTokener extends JSONTokener { - - /** - * The table of entity values. It initially contains Character values for - * amp, apos, gt, lt, quot. - */ - public static final java.util.HashMap entity; - - static { - entity = new java.util.HashMap<>(8); - entity.put("amp", XML.AMP); - entity.put("apos", XML.APOS); - entity.put("gt", XML.GT); - entity.put("lt", XML.LT); - entity.put("quot", XML.QUOT); - } - - /** - * Construct an XMLTokener from a string. - * - * @param s A source string. - */ - public XMLTokener(String s) { - super(s); - } - - /** - * Get the text in the CDATA block. - * - * @return The string up to the ]]>. - * @throws JSONException If the ]]> is not found. - */ - public String nextCDATA() throws JSONException { - char c; - int i; - StringBuilder sb = new StringBuilder(); - for (; ; ) { - c = next(); - if (end()) { - throw syntaxError("Unclosed CDATA"); - } - sb.append(c); - i = sb.length() - 3; - if (i >= 0 && sb.charAt(i) == ']' && sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') { - sb.setLength(i); - return sb.toString(); - } - } - } - - /** - * Get the next XML outer token, trimming whitespace. There are two kinds of - * tokens: the '<' character which begins a markup tag, and the content text - * between markup tags. - * - * @return A string, or a '<' Character, or null if there is no more source - * text. - */ - public Object nextContent() throws JSONException { - char c; - StringBuilder sb; - do { - c = next(); - } while (Character.isWhitespace(c)); - if (c == 0) { - return null; - } - if (c == '<') { - return XML.LT; - } - sb = new StringBuilder(); - for (; ; ) { - if (c == '<' || c == 0) { - back(); - return sb.toString().trim(); - } - if (c == '&') { - sb.append(nextEntity(c)); - } else { - sb.append(c); - } - c = next(); - } - } - - /** - * Return the next entity. These entities are translated to Characters: - * & ' > < ". - * - * @param ampersand An ampersand character. - * @return A Character or an entity String if the entity is not recognized. - * @throws JSONException If missing ';' in XML entity. - */ - public Object nextEntity(char ampersand) throws JSONException { - StringBuilder sb = new StringBuilder(); - for (; ; ) { - char c = next(); - if (Character.isLetterOrDigit(c) || c == '#') { - sb.append(Character.toLowerCase(c)); - } else if (c == ';') { - break; - } else { - throw syntaxError("Missing ';' in XML entity: &" + sb); - } - } - String string = sb.toString(); - Object object = entity.get(string); - return object != null ? object : ampersand + string + ";"; - } - - /** - * Returns the next XML meta token. This is used for skipping over - * and structures. - * - * @return Syntax characters (< > / = ! ?) are returned as - * Character, and strings and names are returned as Boolean. We - * don't care what the values actually are. - * @throws JSONException If a string is not properly closed or if the XML is badly - * structured. - */ - public Object nextMeta() throws JSONException { - char c; - char q; - do { - c = next(); - } while (Character.isWhitespace(c)); - switch (c) { - case 0: - throw syntaxError("Misshaped meta tag"); - case '<': - return XML.LT; - case '>': - return XML.GT; - case '/': - return XML.SLASH; - case '=': - return XML.EQ; - case '!': - return XML.BANG; - case '?': - return XML.QUEST; - case '"': - case '\'': - q = c; - for (; ; ) { - c = next(); - if (c == 0) { - throw syntaxError("Unterminated string"); - } - if (c == q) { - return Boolean.TRUE; - } - } - default: - for (; ; ) { - c = next(); - if (Character.isWhitespace(c)) { - return Boolean.TRUE; - } - switch (c) { - case 0, '<', '>', '/', '=', '!', '?', '"', '\'' -> { - back(); - return Boolean.TRUE; - } - } - } - } - } - - /** - * Get the next XML Token. These tokens are found inside of angle brackets. - * It may be one of these characters: / > = ! ? or it may be a - * string wrapped in single quotes or double quotes, or it may be a name. - * - * @return a String or a Character. - * @throws JSONException If the XML is not well formed. - */ - public Object nextToken() throws JSONException { - char c; - char q; - StringBuilder sb; - do { - c = next(); - } while (Character.isWhitespace(c)); - switch (c) { - case 0: - throw syntaxError("Misshaped element"); - case '<': - throw syntaxError("Misplaced '<'"); - case '>': - return XML.GT; - case '/': - return XML.SLASH; - case '=': - return XML.EQ; - case '!': - return XML.BANG; - case '?': - return XML.QUEST; - - // Quoted string - - case '"': - case '\'': - q = c; - sb = new StringBuilder(); - for (; ; ) { - c = next(); - if (c == 0) { - throw syntaxError("Unterminated string"); - } - if (c == q) { - return sb.toString(); - } - if (c == '&') { - sb.append(nextEntity(c)); - } else { - sb.append(c); - } - } - default: - - // Name - - sb = new StringBuilder(); - for (; ; ) { - sb.append(c); - c = next(); - if (Character.isWhitespace(c)) { - return sb.toString(); - } - switch (c) { - case 0: - return sb.toString(); - case '>': - case '/': - case '=': - case '!': - case '?': - case '[': - case ']': - back(); - return sb.toString(); - case '<': - case '"': - case '\'': - throw syntaxError("Bad character in a name"); - } - } - } - } - - /** - * Skip characters until past the requested string. If it is not found, we - * are left at the end of the source with a result of false. - * - * @param to A string to skip past. - */ - public boolean skipPast(String to) throws JSONException { - boolean b; - char c; - int i; - int j; - int offset = 0; - int length = to.length(); - char[] circle = new char[length]; - - /* - * First fill the circle buffer with as many characters as are in the to - * string. If we reach an early end, bail. - */ - - for (i = 0; i < length; i += 1) { - c = next(); - if (c == 0) { - return false; - } - circle[i] = c; - } - - /* We will loop, possibly for all of the remaining characters. */ - - for (; ; ) { - j = offset; - b = true; - - /* Compare the circle buffer with the to string. */ - - for (i = 0; i < length; i += 1) { - if (circle[j] != to.charAt(i)) { - b = false; - break; - } - j += 1; - if (j >= length) { - j -= length; - } - } - - /* If we exit the loop with b intact, then victory is ours. */ - - if (b) { - return true; - } - - /* - * Get the next character. If there isn't one, then defeat is ours. - */ - - c = next(); - if (c == 0) { - return false; - } - /* - * Shove the character in the circle buffer and advance the circle - * offset. The offset is mod n. - */ - circle[offset] = c; - offset += 1; - if (offset >= length) { - offset -= length; - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java b/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java deleted file mode 100644 index 46185cc05..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java +++ /dev/null @@ -1,773 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.mantle; - -import com.google.common.util.concurrent.AtomicDouble; -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.mantle.EngineMantle; -import com.volmit.iris.engine.mantle.MantleWriter; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.documentation.BlockCoordinates; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.documentation.RegionCoordinates; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.function.Consumer3; -import com.volmit.iris.util.function.Consumer4; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.mantle.io.IOWorker; -import com.volmit.iris.util.mantle.flag.MantleFlag; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.matter.Matter; -import com.volmit.iris.util.matter.MatterSlice; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.parallel.HyperLock; -import com.volmit.iris.util.parallel.MultiBurst; -import lombok.Getter; -import org.bukkit.Chunk; - -import java.io.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * The mantle can store any type of data slice anywhere and manage regions & IO on it's own. - * This class is fully thread safe read & writeNodeData - */ - -public class Mantle { - private static final int LOCK_SIZE = Short.MAX_VALUE; - - private final File dataFolder; - @Getter - private final int worldHeight; - private final KMap lastUse; - private final KMap loadedRegions; - private final HyperLock hyperLock; - private final AtomicBoolean closed; - private final MultiBurst ioBurst; - private final Semaphore ioTrim; - private final Semaphore ioTectonicUnload; - private final IOWorker worker; - private final AtomicDouble adjustedIdleDuration; - private final KSet toUnload; - - /** - * Create a new mantle - * - * @param dataFolder the data folder - * @param worldHeight the world's height (in blocks) - */ - @BlockCoordinates - public Mantle(File dataFolder, int worldHeight) { - this.hyperLock = new HyperLock(); - this.closed = new AtomicBoolean(false); - this.dataFolder = dataFolder; - this.worldHeight = worldHeight; - this.ioTrim = new Semaphore(LOCK_SIZE, true); - this.ioTectonicUnload = new Semaphore(LOCK_SIZE, true); - loadedRegions = new KMap<>(); - lastUse = new KMap<>(); - ioBurst = MultiBurst.ioBurst; - adjustedIdleDuration = new AtomicDouble(0); - toUnload = new KSet<>(); - worker = new IOWorker(dataFolder, worldHeight); - Iris.debug("Opened The Mantle " + C.DARK_AQUA + dataFolder.getAbsolutePath()); - } - - /** - * Get the file for a region - * - * @param folder the folder - * @param x the x coord - * @param z the z coord - * @return the file - */ - public static File fileForRegion(File folder, int x, int z) { - return fileForRegion(folder, key(x, z), true); - } - - /** - * Get the file for the given region - * - * @param folder the data folder - * @param key the region key - * @return the file - */ - public static File fileForRegion(File folder, Long key, boolean convert) { - File f = oldFileForRegion(folder, key); - File fv = new File(folder, "pv." + key + ".ttp.lz4b"); - if (f.exists() && !fv.exists() && convert) - return f; - - if (!fv.getParentFile().exists()) { - fv.getParentFile().mkdirs(); - } - return fv; - } - - - /** - * Get the old file for the given region - * - * @param folder the data folder - * @param key the region key - * @return the file - */ - public static File oldFileForRegion(File folder, Long key) { - return new File(folder, "p." + key + ".ttp.lz4b"); - } - - /** - * Get the long value representing a chunk or region coordinate - * - * @param x the x - * @param z the z - * @return the value - */ - public static Long key(int x, int z) { - return Cache.key(x, z); - } - - /** - * Raise a flag if it is lowered currently, If the flag was raised, execute the runnable - * - * @param x the chunk x - * @param z the chunk z - * @param flag the flag to raise - * @param r the runnable to fire if the flag is now raised (and was previously lowered) - */ - @ChunkCoordinates - public void raiseFlag(int x, int z, MantleFlag flag, Runnable r) { - if (!hasFlag(x, z, flag)) { - flag(x, z, flag, true); - r.run(); - } - } - - /** - * Obtain a cached writer which only contains cached chunks. - * This avoids locking on regions when writing to lots of chunks - * - * @param x the x chunk - * @param z the z chunk - * @param radius the radius chunks - * @return the writer - */ - @ChunkCoordinates - public MantleWriter write(EngineMantle engineMantle, int x, int z, int radius, boolean multicore) { - return new MantleWriter(engineMantle, this, x, z, radius, multicore); - } - - /** - * Lower a flag if it is raised. If the flag was lowered (meaning it was previously raised), execute the runnable - * - * @param x the chunk x - * @param z the chunk z - * @param flag the flag to lower - * @param r the runnable that is fired if the flag was raised but is now lowered - */ - @ChunkCoordinates - public void lowerFlag(int x, int z, MantleFlag flag, Runnable r) { - if (hasFlag(x, z, flag)) { - flag(x, z, flag, false); - r.run(); - } - } - - @ChunkCoordinates - public MantleChunk getChunk(int x, int z) { - return get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31); - } - - public void getChunks(final int minChunkX, - final int maxChunkX, - final int minChunkZ, - final int maxChunkZ, - int parallelism, - final Consumer3 consumer - ) { - if (parallelism <= 0) parallelism = 1; - final var lock = new Semaphore(parallelism); - - final int minRegionX = minChunkX >> 5; - final int maxRegionX = maxChunkX >> 5; - final int minRegionZ = minChunkZ >> 5; - final int maxRegionZ = maxChunkZ >> 5; - - final int minRelativeX = minChunkX & 31; - final int maxRelativeX = maxChunkX & 31; - final int minRelativeZ = minChunkZ & 31; - final int maxRelativeZ = maxChunkZ & 31; - - final AtomicReference error = new AtomicReference<>(); - for (int rX = minRegionX; rX <= maxRegionX; rX++) { - final int minX = rX == minRegionX ? minRelativeX : 0; - final int maxX = rX == maxRegionX ? maxRelativeX : 31; - for (int rZ = minRegionZ; rZ <= maxRegionZ; rZ++) { - final int minZ = rZ == minRegionZ ? minRelativeZ : 0; - final int maxZ = rZ == maxRegionZ ? maxRelativeZ : 31; - final int realX = rX << 5; - final int realZ = rZ << 5; - - lock.acquireUninterruptibly(); - final var e = error.get(); - if (e != null) { - if (e instanceof RuntimeException ex) throw ex; - else if (e instanceof Error ex) throw ex; - else throw new RuntimeException(error.get()); - } - - getFuture(rX, rZ) - .thenAccept(region -> { - final MantleChunk zero = region.getOrCreate(0, 0).use(); - try { - for (int xx = minX; xx <= maxX; xx++) { - for (int zz = minZ; zz <= maxZ; zz++) { - consumer.accept(realX + xx, realZ + zz, region.getOrCreate(xx, zz)); - } - } - } finally { - zero.release(); - } - }) - .exceptionally(ex -> { - error.set(ex); - return null; - }) - .thenRun(lock::release); - } - } - - lock.acquireUninterruptibly(parallelism); - } - - /** - * Flag or unflag a chunk - * - * @param x the chunk x - * @param z the chunk z - * @param flag the flag - * @param flagged should it be set to flagged or not - */ - @ChunkCoordinates - public void flag(int x, int z, MantleFlag flag, boolean flagged) { - get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).flag(flag, flagged); - } - - public void deleteChunk(int x, int z) { - get(x >> 5, z >> 5).delete(x & 31, z & 31); - } - - /** - * Check very quickly if a tectonic plate exists via cached or the file system - * - * @param x the x region coordinate - * @param z the z region coordinate - * @return true if it exists - */ - @RegionCoordinates - public boolean hasTectonicPlate(int x, int z) { - Long k = key(x, z); - return loadedRegions.containsKey(k) || fileForRegion(dataFolder, k, true).exists(); - } - - /** - * Iterate data in a chunk - * - * @param x the chunk x - * @param z the chunk z - * @param type the type of data to iterate - * @param iterator the iterator (x,y,z,data) -> do stuff - * @param the type of data to iterate - */ - @ChunkCoordinates - public void iterateChunk(int x, int z, Class type, Consumer4 iterator) { - /*if(!hasTectonicPlate(x >> 5, z >> 5)) { - return; - }*/ - - get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).iterate(type, iterator); - } - - /** - * Does this chunk have a flag on it? - * - * @param x the x - * @param z the z - * @param flag the flag to test - * @return true if it's flagged - */ - @ChunkCoordinates - public boolean hasFlag(int x, int z, MantleFlag flag) { - if (!hasTectonicPlate(x >> 5, z >> 5)) { - return false; - } - - return get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).isFlagged(flag); - } - - /** - * Set data T at the given block position. This method will attempt to find a - * Tectonic Plate either by loading it or creating a new one. This method uses - * the hyper lock packaged with each Mantle. The hyperlock allows locking of multiple - * threads at a single region while still allowing other threads to continue - * reading & writing other regions. Hyperlocks are slow sync, but in multicore - * environments, they drastically speed up loading & saving large counts of plates - * - * @param x the block's x coordinate - * @param y the block's y coordinate - * @param z the block's z coordinate - * @param t the data to set at the block - * @param the type of data (generic method) - */ - @BlockCoordinates - public void set(int x, int y, int z, T t) { - if (closed.get()) { - throw new RuntimeException("The Mantle is closed"); - } - - if (y < 0 || y >= worldHeight) { - return; - } - - Matter matter = get((x >> 4) >> 5, (z >> 4) >> 5) - .getOrCreate((x >> 4) & 31, (z >> 4) & 31) - .getOrCreate(y >> 4); - matter.slice(matter.getClass(t)) - .set(x & 15, y & 15, z & 15, t); - } - - @BlockCoordinates - public void remove(int x, int y, int z, Class t) { - if (closed.get()) { - throw new RuntimeException("The Mantle is closed"); - } - - if (y < 0 || y >= worldHeight) { - return; - } - - Matter matter = get((x >> 4) >> 5, (z >> 4) >> 5) - .getOrCreate((x >> 4) & 31, (z >> 4) & 31) - .getOrCreate(y >> 4); - matter.slice(t) - .set(x & 15, y & 15, z & 15, null); - } - - /** - * Gets the data at the current block position This method will attempt to find a - * Tectonic Plate either by loading it or creating a new one. This method uses - * the hyper lock packaged with each Mantle. The hyperlock allows locking of multiple - * threads at a single region while still allowing other threads to continue - * reading & writing other regions. Hyperlocks are slow sync, but in multicore - * environments, they drastically speed up loading & saving large counts of plates - * - * @param x the block's x coordinate - * @param y the block's y coordinate - * @param z the block's z coordinate - * @param t the class representing the type of data being requested - * @param the type assumed from the provided class - * @return the returned result (or null) if it doesnt exist - */ - @SuppressWarnings("unchecked") - @BlockCoordinates - public T get(int x, int y, int z, Class t) { - if (closed.get()) { - throw new RuntimeException("The Mantle is closed"); - } - - if (!hasTectonicPlate((x >> 4) >> 5, (z >> 4) >> 5)) { - return null; - } - - if (y < 0 || y >= worldHeight) { - return null; - } - - return (T) get((x >> 4) >> 5, (z >> 4) >> 5) - .getOrCreate((x >> 4) & 31, (z >> 4) & 31) - .getOrCreate(y >> 4).slice(t) - .get(x & 15, y & 15, z & 15); - } - - /** - * Is this mantle closed - * - * @return true if it is - */ - public boolean isClosed() { - return closed.get(); - } - - public void set(int x, int y, int z, Matter matter) { - for (MatterSlice i : matter.getSliceMap().values()) { - i.iterate((mx, my, mz, v) -> set(mx + x, my + y, mz + z, v)); - } - } - - /** - * Closes the Mantle. By closing the mantle, you can no longer read or writeNodeData - * any data to the mantle or it's Tectonic Plates. Closing will also flush any - * loaded regions to the disk in parallel. - */ - public synchronized void close() { - Iris.debug("Closing The Mantle " + C.DARK_AQUA + dataFolder.getAbsolutePath()); - if (closed.getAndSet(true)) { - return; - } - - hyperLock.disable(); - BurstExecutor b = ioBurst.burst(toUnload.size()); - loadedRegions.forEach((i, plate) -> b.queue(() -> { - try { - plate.close(); - worker.write(fileForRegion(dataFolder, i, false).getName(), plate); - oldFileForRegion(dataFolder, i).delete(); - } catch (Throwable e) { - Iris.error("Failed to write Tectonic Plate " + C.DARK_GREEN + Cache.keyX(i) + " " + Cache.keyZ(i)); - Iris.reportError(e); - e.printStackTrace(); - } - })); - loadedRegions.clear(); - - try { - b.complete(); - } catch (Throwable e) { - Iris.reportError(e); - } - try { - worker.close(); - } catch (Throwable e) { - Iris.reportError(e); - } - - IO.delete(new File(dataFolder, ".tmp")); - Iris.debug("The Mantle has Closed " + C.DARK_AQUA + dataFolder.getAbsolutePath()); - } - - /** - * Save & unload regions that have not been used for more than the - * specified amount of milliseconds - * - * @param baseIdleDuration the duration - */ - public synchronized void trim(long baseIdleDuration, int tectonicLimit) { - if (closed.get()) { - throw new RuntimeException("The Mantle is closed"); - } - - double idleDuration = baseIdleDuration; - if (loadedRegions.size() > tectonicLimit) { - // todo update this correctly and maybe do something when its above a 100% - idleDuration = Math.max(idleDuration - (1000 * (((loadedRegions.size() - tectonicLimit) / (double) tectonicLimit) * 100) * 0.4), 4000); - } - adjustedIdleDuration.set(idleDuration); - - ioTrim.acquireUninterruptibly(LOCK_SIZE); - try { - Iris.debug("Trimming Tectonic Plates older than " + Form.duration(idleDuration, 0)); - - if (lastUse.isEmpty()) return; - double unloadTime = M.ms() - idleDuration; - for (long id : lastUse.keySet()) { - hyperLock.withLong(id, () -> { - Long lastUseTime = lastUse.get(id); - if (lastUseTime != null && lastUseTime < unloadTime) { - toUnload.add(id); - Iris.debug("Tectonic Region added to unload"); - } - }); - } - } catch (Throwable e) { - Iris.reportError(e); - } finally { - ioTrim.release(LOCK_SIZE); - } - } - - public synchronized int unloadTectonicPlate(int tectonicLimit) { - if (closed.get()) { - throw new RuntimeException("The Mantle is closed"); - } - - AtomicInteger i = new AtomicInteger(); - BurstExecutor burst = ioBurst.burst(toUnload.size()); - burst.setMulticore(toUnload.size() > tectonicLimit); - - ioTectonicUnload.acquireUninterruptibly(LOCK_SIZE); - try { - double unloadTime = M.ms() - adjustedIdleDuration.get(); - for (long id : toUnload) { - burst.queue(() -> hyperLock.withLong(id, () -> { - TectonicPlate m = loadedRegions.get(id); - if (m == null) { - Iris.debug("Tectonic Plate was added to unload while not loaded " + C.DARK_GREEN + Cache.keyX(id) + " " + Cache.keyZ(id)); - toUnload.remove(id); - return; - } - - var used = lastUse.getOrDefault(id, 0L); - if (!toUnload.contains(id) || used >= unloadTime) { - return; - } - - if (m.inUse()) { - Iris.debug("Tectonic Plate was added to unload while in use " + C.DARK_GREEN + m.getX() + " " + m.getZ()); - use(id); - return; - } - - try { - m.close(); - worker.write(fileForRegion(dataFolder, id, false).getName(), m); - oldFileForRegion(dataFolder, id).delete(); - loadedRegions.remove(id, m); - lastUse.remove(id); - toUnload.remove(id); - i.incrementAndGet(); - Iris.debug("Unloaded Tectonic Plate " + C.DARK_GREEN + Cache.keyX(id) + " " + Cache.keyZ(id)); - } catch (IOException | InterruptedException e) { - Iris.reportError(e); - } - })); - } - burst.complete(); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - burst.complete(); - } finally { - ioTectonicUnload.release(LOCK_SIZE); - } - return i.get(); - } - - - /** - * This retreives a future of the Tectonic Plate at the given coordinates. - * All methods accessing tectonic plates should go through this method - * - * @param x the region x - * @param z the region z - * @return the future of a tectonic plate. - */ - @RegionCoordinates - private TectonicPlate get(int x, int z) { - boolean trim = ioTrim.tryAcquire(); - boolean unload = ioTectonicUnload.tryAcquire(); - try { - if (!trim || !unload) { - try { - return getSafe(x, z).get(); - } catch (Throwable e) { - e.printStackTrace(); - } - } else { - Long key = key(x, z); - TectonicPlate p = loadedRegions.get(key); - - if (p != null && !p.isClosed()) { - use(key); - return p; - } - } - - try { - return getSafe(x, z).get(); - } catch (InterruptedException e) { - Iris.warn("Failed to get Tectonic Plate " + x + " " + z + " Due to a thread intterruption (hotload?)"); - Iris.reportError(e); - } catch (ExecutionException e) { - Iris.warn("Failed to get Tectonic Plate " + x + " " + z + " Due to a thread execution exception (engine close?)"); - Iris.reportError(e); - } catch (Throwable e) { - Iris.warn("Failed to get Tectonic Plate " + x + " " + z + " Due to a unknown exception"); - Iris.reportError(e); - } - } finally { - if (trim) ioTrim.release(); - if (unload) ioTectonicUnload.release(); - } - - Iris.warn("Retrying to get " + x + " " + z + " Mantle Region"); - return get(x, z); - } - - private CompletableFuture getFuture(int x, int z) { - final boolean trim = ioTrim.tryAcquire(); - final boolean unload = ioTectonicUnload.tryAcquire(); - final Function release = p -> { - if (trim) ioTrim.release(); - if (unload) ioTectonicUnload.release(); - return p; - }; - - final Supplier> fallback = () -> getSafe(x, z) - .exceptionally(e -> { - if (e instanceof InterruptedException) { - Iris.warn("Failed to get Tectonic Plate " + x + " " + z + " Due to a thread intterruption (hotload?)"); - Iris.reportError(e); - } else { - Iris.warn("Failed to get Tectonic Plate " + x + " " + z + " Due to a unknown exception"); - Iris.reportError(e); - e.printStackTrace(); - } - return null; - }) - .thenCompose(p -> { - release.apply(p); - if (p != null) return CompletableFuture.completedFuture(p); - Iris.warn("Retrying to get " + x + " " + z + " Mantle Region"); - return getFuture(x, z); - }); - - if (!trim || !unload) { - return getSafe(x, z) - .thenApply(release) - .exceptionallyCompose(e -> { - e.printStackTrace(); - return fallback.get(); - }); - } - - Long key = key(x, z); - TectonicPlate p = loadedRegions.get(key); - if (p != null && !p.isClosed()) { - use(key); - return CompletableFuture.completedFuture(release.apply(p)); - } - - return fallback.get(); - } - - /** - * This retreives a future of the Tectonic Plate at the given coordinates. - * All methods accessing tectonic plates should go through this method - * - * @param x the region x - * @param z the region z - * @return the future of a tectonic plate. - */ - @RegionCoordinates - protected CompletableFuture getSafe(int x, int z) { - return ioBurst.completableFuture(() -> hyperLock.withResult(x, z, () -> { - Long k = key(x, z); - use(k); - TectonicPlate r = loadedRegions.get(k); - if (r != null && !r.isClosed()) { - return r; - } - - TectonicPlate region; - File file = fileForRegion(dataFolder, x, z); - if (file.exists()) { - try { - Iris.addPanic("reading.tectonic-plate", file.getAbsolutePath()); - region = worker.read(file.getName()); - - if (region.getX() != x || region.getZ() != z) { - Iris.warn("Loaded Tectonic Plate " + x + "," + z + " but read it as " + region.getX() + "," + region.getZ() + "... Assuming " + x + "," + z); - } - - loadedRegions.put(k, region); - Iris.debug("Loaded Tectonic Plate " + C.DARK_GREEN + x + " " + z + C.DARK_AQUA + " " + file.getName()); - } catch (Throwable e) { - Iris.error("Failed to read Tectonic Plate " + file.getAbsolutePath() + " creating a new chunk instead."); - Iris.reportError(e); - if (!(e instanceof EOFException)) { - e.printStackTrace(); - } - Iris.panic(); - region = new TectonicPlate(worldHeight, x, z); - loadedRegions.put(k, region); - Iris.debug("Created new Tectonic Plate (Due to Load Failure) " + C.DARK_GREEN + x + " " + z); - } - - use(k); - return region; - } - - region = new TectonicPlate(worldHeight, x, z); - loadedRegions.put(k, region); - Iris.debug("Created new Tectonic Plate " + C.DARK_GREEN + x + " " + z); - use(k); - return region; - })); - } - - private void use(Long key) { - lastUse.put(key, M.ms()); - toUnload.remove(key); - } - - public void saveAll() { - - } - - public MantleChunk getChunk(Chunk e) { - return getChunk(e.getX(), e.getZ()); - } - - public void deleteChunkSlice(int x, int z, Class c) { - if (IrisToolbelt.isRetainingMantleDataForSlice(c.getCanonicalName())) { - return; - } - - getChunk(x, z).deleteSlices(c); - } - - public int getLoadedRegionCount() { - return loadedRegions.size(); - } - - public int getUnloadRegionCount() { - return toUnload.size(); - } - - public double getAdjustedIdleDuration() { - return adjustedIdleDuration.get(); - } - - public void set(int x, int y, int z, MatterSlice slice) { - if (slice.isEmpty()) { - return; - } - - slice.iterateSync((xx, yy, zz, t) -> set(x + xx, y + yy, z + zz, t)); - } - - public boolean isLoaded(Chunk c) { - return loadedRegions.containsKey(key(c.getX() >> 5, c.getZ() >> 5)); - } - - public boolean shouldReduce(Engine engine) { - return !engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio(); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java b/core/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java deleted file mode 100644 index eac477711..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.mantle; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.documentation.ChunkRelativeBlockCoordinates; -import com.volmit.iris.util.function.Consumer4; -import com.volmit.iris.util.io.CountingDataInputStream; -import com.volmit.iris.util.matter.IrisMatter; -import com.volmit.iris.util.matter.Matter; -import com.volmit.iris.util.matter.MatterSlice; -import lombok.Getter; -import lombok.SneakyThrows; -import org.jetbrains.annotations.Nullable; - -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReferenceArray; - -/** - * Represents a mantle chunk. Mantle chunks contain sections of matter (see matter api) - * Mantle Chunks are fully atomic & thread safe - */ -public class MantleChunk extends FlaggedChunk { - @Getter - private final int x; - @Getter - private final int z; - private final AtomicReferenceArray sections; - private final Semaphore ref = new Semaphore(Integer.MAX_VALUE, true); - private final AtomicBoolean closed = new AtomicBoolean(false); - - /** - * Create a mantle chunk - * - * @param sectionHeight the height of the world in sections (blocks >> 4) - */ - @ChunkCoordinates - public MantleChunk(int sectionHeight, int x, int z) { - sections = new AtomicReferenceArray<>(sectionHeight); - this.x = x; - this.z = z; - } - - /** - * Load a mantle chunk from a data stream - * - * @param sectionHeight the height of the world in sections (blocks >> 4) - * @param din the data input - * @throws IOException shit happens - * @throws ClassNotFoundException shit happens - */ - public MantleChunk(int version, int sectionHeight, CountingDataInputStream din) throws IOException { - this(sectionHeight, din.readByte(), din.readByte()); - int s = din.readByte(); - readFlags(version, din); - - for (int i = 0; i < s; i++) { - Iris.addPanic("read.section", "Section[" + i + "]"); - long size = din.readInt(); - if (size == 0) continue; - long start = din.count(); - if (i >= sectionHeight) { - din.skipTo(start + size); - continue; - } - - try { - sections.set(i, Matter.readDin(din)); - } catch (IOException e) { - long end = start + size; - Iris.error("Failed to read chunk section, skipping it."); - Iris.addPanic("read.byte.range", start + " " + end); - Iris.addPanic("read.byte.current", din.count() + ""); - Iris.reportError(e); - e.printStackTrace(); - Iris.panic(); - - din.skipTo(end); - TectonicPlate.addError(); - } - if (din.count() != start + size) { - throw new IOException("Chunk section read size mismatch!"); - } - } - } - - @SneakyThrows - public void close() { - closed.set(true); - ref.acquire(Integer.MAX_VALUE); - ref.release(Integer.MAX_VALUE); - } - - public boolean inUse() { - return ref.availablePermits() < Integer.MAX_VALUE; - } - - public MantleChunk use() { - if (closed.get()) throw new IllegalStateException("Chunk is closed!"); - ref.acquireUninterruptibly(); - if (closed.get()) { - ref.release(); - throw new IllegalStateException("Chunk is closed!"); - } - return this; - } - - public void release() { - ref.release(); - } - - public void copyFrom(MantleChunk chunk) { - use(); - super.copyFrom(chunk, () -> { - for (int i = 0; i < sections.length(); i++) { - sections.set(i, chunk.get(i)); - } - }); - release(); - } - - /** - * Check if a section exists (same as get(section) != null) - * - * @param section the section (0 - (worldHeight >> 4)) - * @return true if it exists - */ - @ChunkCoordinates - public boolean exists(int section) { - return get(section) != null; - } - - /** - * Get thje matter at the given section or null if it doesnt exist - * - * @param section the section (0 - (worldHeight >> 4)) - * @return the matter or null if it doesnt exist - */ - @ChunkCoordinates - public Matter get(int section) { - return sections.get(section); - } - - @Nullable - @ChunkRelativeBlockCoordinates - @SuppressWarnings("unchecked") - public T get(int x, int y, int z, Class type) { - return (T) getOrCreate(y >> 4) - .slice(type) - .get(x & 15, y & 15, z & 15); - } - - /** - * Clear all matter from this chunk - */ - public void clear() { - for (int i = 0; i < sections.length(); i++) { - delete(i); - } - } - - /** - * Delete the matter from the given section - * - * @param section the section (0 - (worldHeight >> 4)) - */ - @ChunkCoordinates - public void delete(int section) { - sections.set(section, null); - } - - /** - * Get or create a new matter section at the given section - * - * @param section the section (0 - (worldHeight >> 4)) - * @return the matter - */ - @ChunkCoordinates - public Matter getOrCreate(int section) { - final Matter matter = get(section); - if (matter != null) return matter; - - final Matter instance = new IrisMatter(16, 16, 16); - final Matter value = sections.compareAndExchange(section, null, instance); - return value == null ? instance : value; - } - - /** - * Write this chunk to a data stream - * - * @param dos the stream - * @throws IOException shit happens - */ - public void write(DataOutputStream dos) throws IOException { - close(); - dos.writeByte(x); - dos.writeByte(z); - dos.writeByte(sections.length()); - writeFlags(dos); - - var bytes = new ByteArrayOutputStream(8192); - var sub = new DataOutputStream(bytes); - for (int i = 0; i < sections.length(); i++) { - trimSlice(i); - - if (exists(i)) { - try { - Matter matter = get(i); - matter.writeDos(sub); - dos.writeInt(bytes.size()); - bytes.writeTo(dos); - } finally { - bytes.reset(); - } - } else { - dos.writeInt(0); - } - } - } - - private void trimSlice(int i) { - if (exists(i)) { - Matter m = get(i); - - if (m.getSliceMap().isEmpty()) { - sections.set(i, null); - } else { - m.trimSlices(); - if (m.getSliceMap().isEmpty()) { - sections.set(i, null); - } - } - } - } - - public void iterate(Class type, Consumer4 iterator) { - for (int i = 0; i < sections.length(); i++) { - int bs = (i << 4); - Matter matter = get(i); - - if (matter != null) { - MatterSlice t = matter.getSlice(type); - - if (t != null) { - t.iterateSync((a, b, c, f) -> iterator.accept(a, b + bs, c, f)); - } - } - } - } - - public void deleteSlices(Class c) { - if (IrisToolbelt.isRetainingMantleDataForSlice(c.getCanonicalName())) - return; - for (int i = 0; i < sections.length(); i++) { - Matter m = sections.get(i); - if (m != null && m.hasSlice(c)) { - m.deleteSlice(c); - } - } - } - - public void trimSlices() { - for (int i = 0; i < sections.length(); i++) { - if (exists(i)) { - trimSlice(i); - } - } - } - - @Override - public boolean isClosed() { - return closed.get(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/TectonicPlate.java b/core/src/main/java/com/volmit/iris/util/mantle/TectonicPlate.java deleted file mode 100644 index 402212301..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/TectonicPlate.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.mantle; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.EnginePanic; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.util.data.Varint; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.io.CountingDataInputStream; -import lombok.Getter; - -import java.io.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReferenceArray; - -/** - * Tectonic Plates are essentially representations of regions in minecraft. - * Tectonic Plates are fully atomic & thread safe - */ -public class TectonicPlate { - private static final ThreadLocal errors = ThreadLocal.withInitial(() -> false); - public static final int MISSING = -1; - public static final int CURRENT = 1; - - private final int sectionHeight; - private final AtomicReferenceArray chunks; - private final AtomicBoolean closed; - - @Getter - private final int x; - - @Getter - private final int z; - - /** - * Create a new tectonic plate - * - * @param worldHeight the height of the world - */ - public TectonicPlate(int worldHeight, int x, int z) { - this.sectionHeight = worldHeight >> 4; - this.chunks = new AtomicReferenceArray<>(1024); - this.closed = new AtomicBoolean(false); - this.x = x; - this.z = z; - } - - /** - * Load a tectonic plate from a data stream - * - * @param worldHeight the height of the world - * @param din the data input - * @throws IOException shit happens yo - */ - public TectonicPlate(int worldHeight, CountingDataInputStream din, boolean versioned) throws IOException { - this(worldHeight, din.readInt(), din.readInt()); - if (!din.markSupported()) - throw new IOException("Mark not supported!"); - - int v = versioned ? Varint.readUnsignedVarInt(din) : MISSING; - for (int i = 0; i < chunks.length(); i++) { - long size = din.readInt(); - if (size == 0) continue; - long start = din.count(); - - try { - Iris.addPanic("read-chunk", "Chunk[" + i + "]"); - chunks.set(i, new MantleChunk(v, sectionHeight, din)); - EnginePanic.saveLast(); - } catch (Throwable e) { - long end = start + size; - Iris.error("Failed to read chunk, creating a new chunk instead."); - Iris.addPanic("read.byte.range", start + " " + end); - Iris.addPanic("read.byte.current", din.count() + ""); - Iris.reportError(e); - e.printStackTrace(); - Iris.panic(); - - din.skipTo(end); - TectonicPlate.addError(); - } - } - } - - public boolean inUse() { - for (int i = 0; i < chunks.length(); i++) { - MantleChunk chunk = chunks.get(i); - if (chunk != null && chunk.inUse()) - return true; - } - return false; - } - - public void close() throws InterruptedException { - closed.set(true); - for (int i = 0; i < chunks.length(); i++) { - MantleChunk chunk = chunks.get(i); - if (chunk != null) { - chunk.close(); - } - } - } - - public boolean isClosed() { - return closed.get(); - } - - /** - * Check if a chunk exists in this plate or not (same as get(x, z) != null) - * - * @param x the chunk relative x (0-31) - * @param z the chunk relative z (0-31) - * @return true if the chunk exists - */ - @ChunkCoordinates - public boolean exists(int x, int z) { - return get(x, z) != null; - } - - /** - * Get a chunk at the given coordinates or null if it doesnt exist - * - * @param x the chunk relative x (0-31) - * @param z the chunk relative z (0-31) - * @return the chunk or null if it doesnt exist - */ - @ChunkCoordinates - public MantleChunk get(int x, int z) { - return chunks.get(index(x, z)); - } - - /** - * Clear all chunks from this tectonic plate - */ - public void clear() { - for (int i = 0; i < chunks.length(); i++) { - chunks.set(i, null); - } - } - - /** - * Delete a chunk from this tectonic plate - * - * @param x the chunk relative x (0-31) - * @param z the chunk relative z (0-31) - */ - @ChunkCoordinates - public void delete(int x, int z) { - chunks.set(index(x, z), null); - } - - /** - * Get a tectonic plate, or create one and insert it & return it if it diddnt exist - * - * @param x the chunk relative x (0-31) - * @param z the chunk relative z (0-31) - * @return the chunk (read or created & inserted) - */ - @ChunkCoordinates - public MantleChunk getOrCreate(int x, int z) { - final int index = index(x, z); - final MantleChunk chunk = chunks.get(index); - if (chunk != null) return chunk; - - final MantleChunk instance = new MantleChunk(sectionHeight, x & 31, z & 31); - final MantleChunk value = chunks.compareAndExchange(index, null, instance); - return value == null ? instance : value; - } - - @ChunkCoordinates - private int index(int x, int z) { - return Cache.to1D(x, z, 0, 32, 32); - } - - /** - * Write this tectonic plate to a data stream - * - * @param dos the data output - * @throws IOException shit happens - */ - public void write(DataOutputStream dos) throws IOException { - dos.writeInt(x); - dos.writeInt(z); - Varint.writeUnsignedVarInt(CURRENT, dos); - - var bytes = new ByteArrayOutputStream(8192); - var sub = new DataOutputStream(bytes); - for (int i = 0; i < chunks.length(); i++) { - MantleChunk chunk = chunks.get(i); - - if (chunk != null) { - try { - chunk.write(sub); - dos.writeInt(bytes.size()); - bytes.writeTo(dos); - } finally { - bytes.reset(); - } - } else { - dos.writeInt(0); - } - } - } - - public static void addError() { - errors.set(true); - } - - public static boolean hasError() { - try { - return errors.get(); - } finally { - errors.remove(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/flag/CustomFlag.java b/core/src/main/java/com/volmit/iris/util/mantle/flag/CustomFlag.java deleted file mode 100644 index 5b239e1a1..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/flag/CustomFlag.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.volmit.iris.util.mantle.flag; - -import org.jetbrains.annotations.NotNull; - -record CustomFlag(String name, int ordinal) implements MantleFlag { - - @Override - public @NotNull String toString() { - return name; - } - - @Override - public boolean isCustom() { - return false; - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof CustomFlag that)) - return false; - return ordinal == that.ordinal; - } - - @Override - public int hashCode() { - return ordinal; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/flag/MantleFlag.java b/core/src/main/java/com/volmit/iris/util/mantle/flag/MantleFlag.java deleted file mode 100644 index 77aaa8e89..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/flag/MantleFlag.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.mantle.flag; - -import org.jetbrains.annotations.Contract; - -public sealed interface MantleFlag permits CustomFlag, ReservedFlag { - int MIN_ORDINAL = 64; - int MAX_ORDINAL = 255; - - MantleFlag OBJECT = ReservedFlag.OBJECT; - MantleFlag UPDATE = ReservedFlag.UPDATE; - MantleFlag JIGSAW = ReservedFlag.JIGSAW; - MantleFlag FEATURE = ReservedFlag.FEATURE; - MantleFlag INITIAL_SPAWNED = ReservedFlag.INITIAL_SPAWNED; - MantleFlag REAL = ReservedFlag.REAL; - MantleFlag CARVED = ReservedFlag.CARVED; - MantleFlag FLUID_BODIES = ReservedFlag.FLUID_BODIES; - MantleFlag INITIAL_SPAWNED_MARKER = ReservedFlag.INITIAL_SPAWNED_MARKER; - MantleFlag CLEANED = ReservedFlag.CLEANED; - MantleFlag PLANNED = ReservedFlag.PLANNED; - MantleFlag ETCHED = ReservedFlag.ETCHED; - MantleFlag TILE = ReservedFlag.TILE; - MantleFlag CUSTOM = ReservedFlag.CUSTOM; - MantleFlag DISCOVERED = ReservedFlag.DISCOVERED; - MantleFlag CUSTOM_ACTIVE = ReservedFlag.CUSTOM_ACTIVE; - MantleFlag SCRIPT = ReservedFlag.SCRIPT; - - int RESERVED_FLAGS = ReservedFlag.values().length; - - String name(); - int ordinal(); - - boolean isCustom(); - - @Contract(value = "_ -> new", pure = true) - static MantleFlag of(int ordinal) { - if (ordinal < MIN_ORDINAL || ordinal > MAX_ORDINAL) - throw new IllegalArgumentException("Ordinal must be between " + MIN_ORDINAL + " and " + MAX_ORDINAL); - return new CustomFlag("CUSTOM:"+ordinal, ordinal); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/flag/MantleFlagAdapter.java b/core/src/main/java/com/volmit/iris/util/mantle/flag/MantleFlagAdapter.java deleted file mode 100644 index c56916df8..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/flag/MantleFlagAdapter.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.volmit.iris.util.mantle.flag; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; - -public class MantleFlagAdapter extends TypeAdapter { - private static final String CUSTOM = "CUSTOM:"; - private static final int CUSTOM_LENGTH = CUSTOM.length(); - - @Override - public void write(JsonWriter out, MantleFlag value) throws IOException { - if (value == null) out.nullValue(); - else out.value(value.toString()); - } - - @Override - public MantleFlag read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - String s = in.nextString(); - if (s.startsWith(CUSTOM) && s.length() > CUSTOM_LENGTH) - return MantleFlag.of(Integer.parseInt(s.substring(CUSTOM_LENGTH))); - return ReservedFlag.valueOf(s); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/flag/ReservedFlag.java b/core/src/main/java/com/volmit/iris/util/mantle/flag/ReservedFlag.java deleted file mode 100644 index 6fb0c97b9..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/flag/ReservedFlag.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.volmit.iris.util.mantle.flag; - -public enum ReservedFlag implements MantleFlag { - OBJECT, - UPDATE, - JIGSAW, - FEATURE, - INITIAL_SPAWNED, - REAL, - CARVED, - FLUID_BODIES, - INITIAL_SPAWNED_MARKER, - CLEANED, - PLANNED, - ETCHED, - TILE, - CUSTOM, - DISCOVERED, - CUSTOM_ACTIVE, - SCRIPT; - - @Override - public boolean isCustom() { - return false; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/io/DelegateStream.java b/core/src/main/java/com/volmit/iris/util/mantle/io/DelegateStream.java deleted file mode 100644 index 2ccbbbe14..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/io/DelegateStream.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.mantle.io; - -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; - -public class DelegateStream { - - public static InputStream read(FileChannel channel) throws IOException { - channel.position(0); - return new Input(channel); - } - - public static OutputStream write(FileChannel channel) throws IOException { - channel.position(0); - return new Output(channel); - } - - private static class Input extends InputStream { - private final InputStream delegate; - - private Input(FileChannel channel) { - this.delegate = Channels.newInputStream(channel); - } - - @Override - public int available() throws IOException { - return delegate.available(); - } - - @Override - public int read() throws IOException { - return delegate.read(); - } - - @Override - public int read(byte @NotNull [] b, int off, int len) throws IOException { - return delegate.read(b, off, len); - } - - @Override - public byte @NotNull [] readAllBytes() throws IOException { - return delegate.readAllBytes(); - } - - @Override - public int readNBytes(byte[] b, int off, int len) throws IOException { - return delegate.readNBytes(b, off, len); - } - - @Override - public byte @NotNull [] readNBytes(int len) throws IOException { - return delegate.readNBytes(len); - } - - @Override - public long skip(long n) throws IOException { - return delegate.skip(n); - } - - @Override - public void skipNBytes(long n) throws IOException { - delegate.skipNBytes(n); - } - - @Override - public long transferTo(OutputStream out) throws IOException { - return delegate.transferTo(out); - } - } - - private static class Output extends OutputStream { - private final FileChannel channel; - private final OutputStream delegate; - - private Output(FileChannel channel) { - this.channel = channel; - this.delegate = Channels.newOutputStream(channel); - } - - @Override - public void write(int b) throws IOException { - delegate.write(b); - } - - @Override - public void write(byte @NotNull [] b, int off, int len) throws IOException { - delegate.write(b, off, len); - } - - @Override - public void flush() throws IOException { - channel.truncate(channel.position()); - } - - @Override - public void close() throws IOException { - channel.force(true); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/io/Holder.java b/core/src/main/java/com/volmit/iris/util/mantle/io/Holder.java deleted file mode 100644 index fa36ce9c5..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/io/Holder.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.mantle.io; - -import com.volmit.iris.util.io.IO; - -import java.io.IOException; -import java.nio.channels.FileChannel; -import java.util.concurrent.Semaphore; - -class Holder { - private final FileChannel channel; - private final Semaphore semaphore = new Semaphore(1); - private volatile boolean closed; - - Holder(FileChannel channel) throws IOException { - this.channel = channel; - IO.lock(channel); - } - - SynchronizedChannel acquire() { - semaphore.acquireUninterruptibly(); - if (closed) { - semaphore.release(); - return null; - } - - return new SynchronizedChannel(channel, semaphore); - } - - void close() throws IOException { - semaphore.acquireUninterruptibly(); - try { - if (closed) return; - closed = true; - channel.close(); - } finally { - semaphore.release(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/io/IOWorker.java b/core/src/main/java/com/volmit/iris/util/mantle/io/IOWorker.java deleted file mode 100644 index d2edb4995..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/io/IOWorker.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.mantle.io; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.io.CountingDataInputStream; -import com.volmit.iris.util.mantle.TectonicPlate; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; -import net.jpountz.lz4.LZ4BlockInputStream; -import net.jpountz.lz4.LZ4BlockOutputStream; - -import java.io.*; -import java.nio.channels.FileChannel; -import java.nio.file.*; -import java.util.Objects; -import java.util.Set; - -public class IOWorker { - private static final Set OPTIONS = Set.of(StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.SYNC); - private static final int MAX_CACHE_SIZE = 128; - - private final Path root; - private final File tmp; - private final int worldHeight; - - private final Object2ObjectLinkedOpenHashMap cache = new Object2ObjectLinkedOpenHashMap<>(); - - public IOWorker(File root, int worldHeight) { - this.root = root.toPath(); - this.tmp = new File(root, ".tmp"); - this.worldHeight = worldHeight; - } - - public TectonicPlate read(final String name) throws IOException { - PrecisionStopwatch p = PrecisionStopwatch.start(); - try (var channel = getChannel(name)) { - var raw = channel.read(); - var lz4 = new LZ4BlockInputStream(raw); - var buffered = new BufferedInputStream(lz4); - try (var in = CountingDataInputStream.wrap(buffered)) { - return new TectonicPlate(worldHeight, in, name.startsWith("pv.")); - } finally { - if (TectonicPlate.hasError() && IrisSettings.get().getGeneral().isDumpMantleOnError()) { - File dump = Iris.instance.getDataFolder("dump", name + ".bin"); - Files.copy(new LZ4BlockInputStream(channel.read()), dump.toPath(), StandardCopyOption.REPLACE_EXISTING); - } else { - Iris.debug("Read Tectonic Plate " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(p.getMilliseconds(), 2)); - } - } - } - } - - public void write(final String name, final TectonicPlate plate) throws IOException { - PrecisionStopwatch p = PrecisionStopwatch.start(); - try (var channel = getChannel(name)) { - tmp.mkdirs(); - File file = File.createTempFile("iris", ".bin", tmp); - try { - try (var tmp = new DataOutputStream(new LZ4BlockOutputStream(new FileOutputStream(file)))) { - plate.write(tmp); - } - - try (var out = channel.write()) { - Files.copy(file.toPath(), out); - out.flush(); - } - } finally { - file.delete(); - } - } - Iris.debug("Saved Tectonic Plate " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(p.getMilliseconds(), 2)); - } - - public void close() throws IOException { - synchronized (cache) { - for (Holder h : cache.values()) { - h.close(); - } - - cache.clear(); - } - } - - private SynchronizedChannel getChannel(final String name) throws IOException { - PrecisionStopwatch p = PrecisionStopwatch.start(); - try { - synchronized (cache) { - Holder holder = cache.getAndMoveToFirst(name); - if (holder != null) { - var channel = holder.acquire(); - if (channel != null) { - return channel; - } - } - - if (cache.size() >= MAX_CACHE_SIZE) { - var last = cache.removeLast(); - last.close(); - } - - - holder = new Holder(FileChannel.open(root.resolve(name), OPTIONS)); - cache.putAndMoveToFirst(name, holder); - return Objects.requireNonNull(holder.acquire()); - } - } finally { - Iris.debug("Acquired Channel for " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(p.getMilliseconds(), 2)); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/mantle/io/SynchronizedChannel.java b/core/src/main/java/com/volmit/iris/util/mantle/io/SynchronizedChannel.java deleted file mode 100644 index a26d02967..000000000 --- a/core/src/main/java/com/volmit/iris/util/mantle/io/SynchronizedChannel.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.mantle.io; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.channels.FileChannel; -import java.util.concurrent.Semaphore; - -public class SynchronizedChannel implements Closeable { - private final FileChannel channel; - private final Semaphore lock; - private transient boolean closed; - - SynchronizedChannel(FileChannel channel, Semaphore lock) { - this.channel = channel; - this.lock = lock; - } - - public InputStream read() throws IOException { - if (closed) throw new IOException("Channel is closed!"); - return DelegateStream.read(channel); - } - - public OutputStream write() throws IOException { - if (closed) throw new IOException("Channel is closed!"); - return DelegateStream.write(channel); - } - - @Override - public void close() throws IOException { - if (closed) return; - closed = true; - lock.release(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/AlignedPoint.java b/core/src/main/java/com/volmit/iris/util/math/AlignedPoint.java deleted file mode 100644 index 15ce5cb72..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/AlignedPoint.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -public class AlignedPoint { - private double x; - private double y; - private double z; - - public double getX() { - return x; - } - - public double getY() { - return y; - } - - public double getZ() { - return z; - } - -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/math/Average.java b/core/src/main/java/com/volmit/iris/util/math/Average.java deleted file mode 100644 index 1ac83cf37..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Average.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.util.data.DoubleArrayUtils; - -/** - * Provides an incredibly fast averaging object. It swaps values from a sum - * using an array. Averages do not use any form of looping. An average of 10,000 - * entries is the same speed as an average with 5 entries. - * - * @author cyberpwn - */ -public class Average { - protected final double[] values; - protected int cursor; - private double average; - private double lastSum; - private boolean dirty; - private boolean brandNew; - - /** - * Create an average holder - * - * @param size the size of entries to keep - */ - public Average(int size) { - values = new double[size]; - DoubleArrayUtils.fill(values, 0); - brandNew = true; - average = 0; - cursor = 0; - lastSum = 0; - dirty = false; - } - - /** - * Put a value into the average (rolls over if full) - * - * @param i the value - */ - public void put(double i) { - - dirty = true; - - if (brandNew) { - DoubleArrayUtils.fill(values, i); - lastSum = size() * i; - brandNew = false; - return; - } - - double current = values[cursor]; - lastSum = (lastSum - current) + i; - values[cursor] = i; - cursor = cursor + 1 < size() ? cursor + 1 : 0; - } - - /** - * Get the current average - * - * @return the average - */ - public double getAverage() { - if (dirty) { - calculateAverage(); - return getAverage(); - } - - return average; - } - - private void calculateAverage() { - average = lastSum / (double) size(); - dirty = false; - } - - public int size() { - return values.length; - } - - public boolean isDirty() { - return dirty; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/BlockPosition.java b/core/src/main/java/com/volmit/iris/util/math/BlockPosition.java deleted file mode 100644 index 9ca1363f8..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/BlockPosition.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import lombok.Data; -import org.bukkit.World; -import org.bukkit.block.Block; - -import java.util.Objects; - -@Data -public class BlockPosition { - //Magic numbers - private static final int m1 = 1 + MathHelper.f(MathHelper.c(30000000)); - private static final int m2 = 64 - (m1 * 2); - private static final long m3 = m1 + m2; - private static final long m5 = (1L << m2) - 1L; - private static final long m4 = (1L << m1) - 1L; - private static final long m6 = (1L << m1) - 1L; - private int x; - private int y; - private int z; - - public BlockPosition(int x, int y, int z) { - this.x = x; - this.y = y; - this.z = z; - } - - public static long toLong(int x, int y, int z) { - long var3 = 0L; - var3 |= (x & m4) << m3; - var3 |= (y & m5); - var3 |= (z & m6) << m2; - return var3; - } - - @Override - public int hashCode() { - return Objects.hash(x, y, z); - } - - public boolean equals(Object o) { - if (o == null) { - return false; - } - - if (o instanceof BlockPosition ot) { - - return ot.x == x && ot.y == y && ot.z == z; - } - - return false; - } - - public int getChunkX() { - return x >> 4; - } - - public int getChunkZ() { - return z >> 4; - } - - public boolean is(int x, int z) { - return this.x == x && this.z == z; - } - - public boolean is(int x, int y, int z) { - return this.x == x && this.y == y && this.z == z; - } - - public long asLong() { - return toLong(getX(), getY(), getZ()); - } - - public Block toBlock(World world) { - return world.getBlockAt(x, y, z); - } - - public BlockPosition add(int x, int y, int z) { - return new BlockPosition(this.x + x, this.y + y, this.z + z); - } - - public void min(BlockPosition i) { - setX(Math.min(i.getX(), getX())); - setY(Math.min(i.getY(), getY())); - setZ(Math.min(i.getZ(), getZ())); - } - - public void max(BlockPosition i) { - setX(Math.max(i.getX(), getX())); - setY(Math.max(i.getY(), getY())); - setZ(Math.max(i.getZ(), getZ())); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/CDou.java b/core/src/main/java/com/volmit/iris/util/math/CDou.java deleted file mode 100644 index 02964d4ef..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/CDou.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -@SuppressWarnings("ALL") -public class CDou { - private final double max; - private double number; - - public CDou(double max) { - number = 0; - this.max = max; - } - - public CDou set(double n) { - number = n; - circ(); - return this; - } - - public CDou add(double a) { - number += a; - circ(); - return this; - } - - public CDou sub(double a) { - number -= a; - circ(); - return this; - } - - public double get() { - return number; - } - - public void circ() { - if (number < 0) { - number = max - (Math.abs(number) > max ? max : Math.abs(number)); - } - - number = number % (max); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/DOP.java b/core/src/main/java/com/volmit/iris/util/math/DOP.java deleted file mode 100644 index 1ea0a4a36..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/DOP.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import org.bukkit.util.Vector; - -public abstract class DOP { - private final String type; - - public DOP(String type) { - this.type = type; - } - - public abstract Vector op(Vector v); - - public String getType() { - return type; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Direction.java b/core/src/main/java/com/volmit/iris/util/math/Direction.java deleted file mode 100644 index 9ca17b149..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Direction.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.util.collection.GBiset; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.Cuboid.CuboidDirection; -import org.bukkit.Axis; -import org.bukkit.block.BlockFace; -import org.bukkit.util.Vector; - -import java.util.Map; - -/** - * Directions - * - * @author cyberpwn - */ -public enum Direction { - U(0, 1, 0, CuboidDirection.Up), - D(0, -1, 0, CuboidDirection.Down), - N(0, 0, -1, CuboidDirection.North), - S(0, 0, 1, CuboidDirection.South), - E(1, 0, 0, CuboidDirection.East), - W(-1, 0, 0, CuboidDirection.West); - - private static KMap, DOP> permute = null; - - private final int x; - private final int y; - private final int z; - private final CuboidDirection f; - - Direction(int x, int y, int z, CuboidDirection f) { - this.x = x; - this.y = y; - this.z = z; - this.f = f; - } - - public static Direction getDirection(BlockFace f) { - return switch (f) { - case DOWN -> D; - case EAST, EAST_SOUTH_EAST, EAST_NORTH_EAST -> E; - case NORTH, NORTH_WEST, NORTH_NORTH_WEST, NORTH_NORTH_EAST, NORTH_EAST -> N; - case SELF, UP -> U; - case SOUTH, SOUTH_WEST, SOUTH_SOUTH_WEST, SOUTH_SOUTH_EAST, SOUTH_EAST -> S; - case WEST, WEST_SOUTH_WEST, WEST_NORTH_WEST -> W; - }; - - } - - public static Direction closest(Vector v) { - double m = Double.MAX_VALUE; - Direction s = null; - - for (Direction i : values()) { - Vector x = i.toVector(); - double g = x.dot(v); - - if (g < m) { - m = g; - s = i; - } - } - - return s; - } - - public static Direction closest(Vector v, Direction... d) { - double m = Double.MAX_VALUE; - Direction s = null; - - for (Direction i : d) { - Vector x = i.toVector(); - double g = x.distance(v); - - if (g < m) { - m = g; - s = i; - } - } - - return s; - } - - public static Direction closest(Vector v, KList d) { - double m = Double.MAX_VALUE; - Direction s = null; - - for (Direction i : d) { - Vector x = i.toVector(); - double g = x.distance(v); - - if (g < m) { - m = g; - s = i; - } - } - - return s; - } - - public static KList news() { - return new KList().add(N, E, W, S); - } - - public static Direction getDirection(Vector v) { - Vector k = VectorMath.triNormalize(v.clone().normalize()); - - for (Direction i : udnews()) { - if (i.x == k.getBlockX() && i.y == k.getBlockY() && i.z == k.getBlockZ()) { - return i; - } - } - - return Direction.N; - } - - public static KList udnews() { - return new KList().add(U, D, N, E, W, S); - } - - /** - * Get the directional value from the given byte from common directional blocks - * (MUST BE BETWEEN 0 and 5 INCLUSIVE) - * - * @param b the byte - * @return the direction or null if the byte is outside of the inclusive range - * 0-5 - */ - public static Direction fromByte(byte b) { - if (b > 5 || b < 0) { - return null; - } - - if (b == 0) { - return D; - } else if (b == 1) { - return U; - } else if (b == 2) { - return N; - } else if (b == 3) { - return S; - } else if (b == 4) { - return W; - } else { - return E; - } - } - - public static void calculatePermutations() { - if (permute != null) { - return; - } - - permute = new KMap<>(); - - for (Direction i : udnews()) { - for (Direction j : udnews()) { - GBiset b = new GBiset<>(i, j); - - if (i.equals(j)) { - permute.put(b, new DOP("DIRECT") { - @Override - public Vector op(Vector v) { - return v; - } - }); - } else if (i.reverse().equals(j)) { - if (i.isVertical()) { - permute.put(b, new DOP("R180CCZ") { - @Override - public Vector op(Vector v) { - return VectorMath.rotate90CCZ(VectorMath.rotate90CCZ(v)); - } - }); - } else { - permute.put(b, new DOP("R180CCY") { - @Override - public Vector op(Vector v) { - return VectorMath.rotate90CCY(VectorMath.rotate90CCY(v)); - } - }); - } - } else if (getDirection(VectorMath.rotate90CX(i.toVector())).equals(j)) { - permute.put(b, new DOP("R90CX") { - @Override - public Vector op(Vector v) { - return VectorMath.rotate90CX(v); - } - }); - } else if (getDirection(VectorMath.rotate90CCX(i.toVector())).equals(j)) { - permute.put(b, new DOP("R90CCX") { - @Override - public Vector op(Vector v) { - return VectorMath.rotate90CCX(v); - } - }); - } else if (getDirection(VectorMath.rotate90CY(i.toVector())).equals(j)) { - permute.put(b, new DOP("R90CY") { - @Override - public Vector op(Vector v) { - return VectorMath.rotate90CY(v); - } - }); - } else if (getDirection(VectorMath.rotate90CCY(i.toVector())).equals(j)) { - permute.put(b, new DOP("R90CCY") { - @Override - public Vector op(Vector v) { - return VectorMath.rotate90CCY(v); - } - }); - } else if (getDirection(VectorMath.rotate90CZ(i.toVector())).equals(j)) { - permute.put(b, new DOP("R90CZ") { - @Override - public Vector op(Vector v) { - return VectorMath.rotate90CZ(v); - } - }); - } else if (getDirection(VectorMath.rotate90CCZ(i.toVector())).equals(j)) { - permute.put(b, new DOP("R90CCZ") { - @Override - public Vector op(Vector v) { - return VectorMath.rotate90CCZ(v); - } - }); - } else { - permute.put(b, new DOP("FAIL") { - @Override - public Vector op(Vector v) { - return v; - } - }); - } - } - } - } - - @Override - public String toString() { - return switch (this) { - case D -> "Down"; - case E -> "East"; - case N -> "North"; - case S -> "South"; - case U -> "Up"; - case W -> "West"; - }; - - } - - public boolean isVertical() { - return equals(D) || equals(U); - } - - public Vector toVector() { - return new Vector(x, y, z); - } - - public boolean isCrooked(Direction to) { - if (equals(to.reverse())) { - return false; - } - - return !equals(to); - } - - public Vector angle(Vector initial, Direction d) { - calculatePermutations(); - - for (Map.Entry, DOP> entry : permute.entrySet()) { - GBiset i = entry.getKey(); - if (i.getA().equals(this) && i.getB().equals(d)) { - return entry.getValue().op(initial); - } - } - - return initial; - } - - public Direction reverse() { - switch (this) { - case D: - return U; - case E: - return W; - case N: - return S; - case S: - return N; - case U: - return D; - case W: - return E; - default: - break; - } - - return null; - } - - public int x() { - return x; - } - - public int y() { - return y; - } - - public int z() { - return z; - } - - public CuboidDirection f() { - return f; - } - - /** - * Get the byte value represented in some directional blocks - * - * @return the byte value - */ - public byte byteValue() { - switch (this) { - case D: - return 0; - case E: - return 5; - case N: - return 2; - case S: - return 3; - case U: - return 1; - case W: - return 4; - default: - break; - } - - return -1; - } - - public BlockFace getFace() { - return switch (this) { - case D -> BlockFace.DOWN; - case E -> BlockFace.EAST; - case N -> BlockFace.NORTH; - case S -> BlockFace.SOUTH; - case U -> BlockFace.UP; - case W -> BlockFace.WEST; - }; - - } - - public Axis getAxis() { - return switch (this) { - case D, U -> Axis.Y; - case E, W -> Axis.X; - case N, S -> Axis.Z; - }; - - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/FinalInteger.java b/core/src/main/java/com/volmit/iris/util/math/FinalInteger.java deleted file mode 100644 index dd4fd2527..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/FinalInteger.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.util.scheduling.Wrapper; - -/** - * Represents a number that can be finalized and be changed - * - * @author cyberpwn - */ -public class FinalInteger extends Wrapper { - public FinalInteger(Integer t) { - super(t); - } - - /** - * Add to this value - * - * @param i the number to add to this value (value = value + i) - */ - public void add(int i) { - set(get() + i); - } - - /** - * Subtract from this value - * - * @param i the number to subtract from this value (value = value - i) - */ - public void sub(int i) { - set(get() - i); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/INode.java b/core/src/main/java/com/volmit/iris/util/math/INode.java deleted file mode 100644 index db739855a..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/INode.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import lombok.Data; -import org.bukkit.util.Vector; - -@Data -public class INode { - - private Vector position; - private double tension; - private double bias; - private double continuity; - - public INode() { - this(new Vector(0, 0, 0)); - } - - public INode(INode other) { - this.position = other.position; - - this.tension = other.tension; - this.bias = other.bias; - this.continuity = other.continuity; - } - - public INode(Vector position) { - this.position = position; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/math/IrisMathHelper.java b/core/src/main/java/com/volmit/iris/util/math/IrisMathHelper.java deleted file mode 100644 index fea29342e..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/IrisMathHelper.java +++ /dev/null @@ -1,453 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import java.util.Random; -import java.util.UUID; -import java.util.function.IntPredicate; - -public class IrisMathHelper { - public static final float a; - private static final Random c; - private static final int[] d; - private static final double e; - private static final double[] f; - private static final double[] g; - - static { - a = c(2.0f); - c = new Random(); - d = new int[]{0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; - e = Double.longBitsToDouble(4805340802404319232L); - f = new double[257]; - g = new double[257]; - for (int var2 = 0; var2 < 257; ++var2) { - final double var3 = var2 / 256.0; - final double var4 = Math.asin(var3); - IrisMathHelper.g[var2] = Math.cos(var4); - IrisMathHelper.f[var2] = var4; - } - } - - public static float c(final float var0) { - return (float) Math.sqrt(var0); - } - - public static float sqrt(final double var0) { - return (float) Math.sqrt(var0); - } - - public static int d(final float var0) { - final int var = (int) var0; - return (var0 < var) ? (var - 1) : var; - } - - public static int floor(final double var0) { - final int var = (int) var0; - return (var0 < var) ? (var - 1) : var; - } - - public static long d(final double var0) { - final long var = (long) var0; - return (var0 < var) ? (var - 1L) : var; - } - - public static float e(final float var0) { - return Math.abs(var0); - } - - public static int a(final int var0) { - return Math.abs(var0); - } - - public static int f(final float var0) { - final int var = (int) var0; - return (var0 > var) ? (var + 1) : var; - } - - public static int f(final double var0) { - final int var = (int) var0; - return (var0 > var) ? (var + 1) : var; - } - - public static int clamp(final int var0, final int var1, final int var2) { - if (var0 < var1) { - return var1; - } - return Math.min(var0, var2); - } - - public static float a(final float var0, final float var1, final float var2) { - if (var0 < var1) { - return var1; - } - return Math.min(var0, var2); - } - - public static double a(final double var0, final double var2, final double var4) { - if (var0 < var2) { - return var2; - } - return Math.min(var0, var4); - } - - public static double b(final double var0, final double var2, final double var4) { - if (var4 < 0.0) { - return var0; - } - if (var4 > 1.0) { - return var2; - } - return d(var4, var0, var2); - } - - public static double a(double var0, double var2) { - if (var0 < 0.0) { - var0 = -var0; - } - if (var2 < 0.0) { - var2 = -var2; - } - return Math.max(var0, var2); - } - - public static int a(final int var0, final int var1) { - return Math.floorDiv(var0, var1); - } - - public static int nextInt(final Random var0, final int var1, final int var2) { - if (var1 >= var2) { - return var1; - } - return var0.nextInt(var2 - var1 + 1) + var1; - } - - public static float a(final Random var0, final float var1, final float var2) { - if (var1 >= var2) { - return var1; - } - return var0.nextFloat() * (var2 - var1) + var1; - } - - public static double a(final Random var0, final double var1, final double var3) { - if (var1 >= var3) { - return var1; - } - return var0.nextDouble() * (var3 - var1) + var1; - } - - public static double a(final long[] var0) { - long var = 0L; - for (final long var2 : var0) { - var += var2; - } - return var / (double) var0.length; - } - - public static boolean b(final double var0, final double var2) { - return Math.abs(var2 - var0) < 9.999999747378752E-6; - } - - public static int b(final int var0, final int var1) { - return Math.floorMod(var0, var1); - } - - public static float g(final float var0) { - float var = var0 % 360.0f; - if (var >= 180.0f) { - var -= 360.0f; - } - if (var < -180.0f) { - var += 360.0f; - } - return var; - } - - public static double g(final double var0) { - double var = var0 % 360.0; - if (var >= 180.0) { - var -= 360.0; - } - if (var < -180.0) { - var += 360.0; - } - return var; - } - - public static float c(final float var0, final float var1) { - return g(var1 - var0); - } - - public static float d(final float var0, final float var1) { - return e(c(var0, var1)); - } - - public static float b(final float var0, final float var1, final float var2) { - final float var3 = c(var0, var1); - final float var4 = a(var3, -var2, var2); - return var1 - var4; - } - - public static float c(final float var0, final float var1, float var2) { - var2 = e(var2); - if (var0 < var1) { - return a(var0 + var2, var0, var1); - } - return a(var0 - var2, var1, var0); - } - - public static float d(final float var0, final float var1, final float var2) { - final float var3 = c(var0, var1); - return c(var0, var0 + var3, var2); - } - - public static int c(final int var0) { - int var = var0 - 1; - var |= var >> 1; - var |= var >> 2; - var |= var >> 4; - var |= var >> 8; - var |= var >> 16; - return var + 1; - } - - public static boolean d(final int var0) { - return var0 != 0 && (var0 & var0 - 1) == 0x0; - } - - public static int e(int var0) { - var0 = (d(var0) ? var0 : c(var0)); - return IrisMathHelper.d[(int) (var0 * 125613361L >> 27) & 0x1F]; - } - - public static int f(final int var0) { - return e(var0) - (d(var0) ? 0 : 1); - } - - public static int c(final int var0, int var1) { - if (var1 == 0) { - return 0; - } - if (var0 == 0) { - return var1; - } - if (var0 < 0) { - var1 *= -1; - } - final int var2 = var0 % var1; - if (var2 == 0) { - return var0; - } - return var0 + var1 - var2; - } - - public static float h(final float var0) { - return var0 - d(var0); - } - - public static double h(final double var0) { - return var0 - d(var0); - } - - public static long c(final int var0, final int var1, final int var2) { - long var3 = (var0 * 3129871L) ^ var2 * 116129781L ^ (long) var1; - var3 = var3 * var3 * 42317861L + var3 * 11L; - return var3 >> 16; - } - - public static UUID a(final Random var0) { - final long var = (var0.nextLong() & 0xFFFFFFFFFFFF0FFFL) | 0x4000L; - final long var2 = (var0.nextLong() & 0x3FFFFFFFFFFFFFFFL) | Long.MIN_VALUE; - return new UUID(var, var2); - } - - public static UUID a() { - return a(IrisMathHelper.c); - } - - public static double c(final double var0, final double var2, final double var4) { - return (var0 - var2) / (var4 - var2); - } - - public static double d(double var0, double var2) { - final double var3 = var2 * var2 + var0 * var0; - if (Double.isNaN(var3)) { - return Double.NaN; - } - final boolean var4 = var0 < 0.0; - if (var4) { - var0 = -var0; - } - final boolean var5 = var2 < 0.0; - if (var5) { - var2 = -var2; - } - final boolean var6 = var0 > var2; - if (var6) { - final double var7 = var2; - var2 = var0; - var0 = var7; - } - final double var7 = i(var3); - var2 *= var7; - var0 *= var7; - final double var8 = IrisMathHelper.e + var0; - final int var9 = (int) Double.doubleToRawLongBits(var8); - final double var10 = IrisMathHelper.f[var9]; - final double var11 = IrisMathHelper.g[var9]; - final double var12 = var8 - IrisMathHelper.e; - final double var13 = var0 * var11 - var2 * var12; - final double var14 = (6.0 + var13 * var13) * var13 * 0.16666666666666666; - double var15 = var10 + var14; - if (var6) { - var15 = 1.5707963267948966 - var15; - } - if (var5) { - var15 = 3.141592653589793 - var15; - } - if (var4) { - var15 = -var15; - } - return var15; - } - - public static double i(double var0) { - final double var = 0.5 * var0; - long var2 = Double.doubleToRawLongBits(var0); - var2 = 6910469410427058090L - (var2 >> 1); - var0 = Double.longBitsToDouble(var2); - var0 *= 1.5 - var * var0 * var0; - return var0; - } - - public static int f(final float var0, final float var1, final float var2) { - final int var3 = (int) (var0 * 6.0f) % 6; - final float var4 = var0 * 6.0f - var3; - final float var5 = var2 * (1.0f - var1); - final float var6 = var2 * (1.0f - var4 * var1); - final float var7 = var2 * (1.0f - (1.0f - var4) * var1); - float var8 = 0.0f; - float var9 = 0.0f; - float var10 = 0.0f; - switch (var3) { - case 0 -> { - var8 = var2; - var9 = var7; - var10 = var5; - } - case 1 -> { - var8 = var6; - var9 = var2; - var10 = var5; - } - case 2 -> { - var8 = var5; - var9 = var2; - var10 = var7; - } - case 3 -> { - var8 = var5; - var9 = var6; - var10 = var2; - } - case 4 -> { - var8 = var7; - var9 = var5; - var10 = var2; - } - case 5 -> { - var8 = var2; - var9 = var5; - var10 = var6; - } - default -> - throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + var0 + ", " + var1 + ", " + var2); - } - final int var11 = clamp((int) (var8 * 255.0f), 0, 255); - final int var12 = clamp((int) (var9 * 255.0f), 0, 255); - final int var13 = clamp((int) (var10 * 255.0f), 0, 255); - return var11 << 16 | var12 << 8 | var13; - } - - public static int g(int var0) { - var0 ^= var0 >>> 16; - var0 *= -2048144789; - var0 ^= var0 >>> 13; - var0 *= -1028477387; - var0 ^= var0 >>> 16; - return var0; - } - - public static int a(int var0, final int var1, final IntPredicate var2) { - int var3 = var1 - var0; - while (var3 > 0) { - final int var4 = var3 / 2; - final int var5 = var0 + var4; - if (var2.test(var5)) { - var3 = var4; - } else { - var0 = var5 + 1; - var3 -= var4 + 1; - } - } - return var0; - } - - public static float g(final float var0, final float var1, final float var2) { - return var1 + var0 * (var2 - var1); - } - - public static double d(final double var0, final double var2, final double var4) { - return var2 + var0 * (var4 - var2); - } - - public static double a(final double var0, final double var2, final double var4, final double var6, final double var8, final double var10) { - return d(var2, d(var0, var4, var6), d(var0, var8, var10)); - } - - public static double a(final double var0, final double var2, final double var4, final double var6, final double var8, final double var10, final double var12, final double var14, final double var16, final double var18, final double var20) { - return d(var4, a(var0, var2, var6, var8, var10, var12), a(var0, var2, var14, var16, var18, var20)); - } - - public static double j(final double var0) { - return var0 * var0 * var0 * (var0 * (var0 * 6.0 - 15.0) + 10.0); - } - - public static int k(final double var0) { - if (var0 == 0.0) { - return 0; - } - return (var0 > 0.0) ? 1 : -1; - } - - @Deprecated - public static float j(final float var0, final float var1, final float var2) { - float var3; - for (var3 = var1 - var0; var3 < -180.0f; var3 += 360.0f) { - } - while (var3 >= 180.0f) { - var3 -= 360.0f; - } - return var0 + var2 * var3; - } - - public static float k(final float var0) { - return var0 * var0; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/math/KochanekBartelsInterpolation.java b/core/src/main/java/com/volmit/iris/util/math/KochanekBartelsInterpolation.java deleted file mode 100644 index ffd08c444..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/KochanekBartelsInterpolation.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import org.bukkit.util.Vector; - -import java.util.Collections; -import java.util.List; - -public class KochanekBartelsInterpolation implements PathInterpolation { - - private List nodes; - private Vector[] coeffA; - private Vector[] coeffB; - private Vector[] coeffC; - private Vector[] coeffD; - private double scaling; - - public KochanekBartelsInterpolation() { - setNodes(Collections.emptyList()); - } - - @Override - public void setNodes(List nodes) { - this.nodes = nodes; - recalc(); - } - - private void recalc() { - final int nNodes = nodes.size(); - coeffA = new Vector[nNodes]; - coeffB = new Vector[nNodes]; - coeffC = new Vector[nNodes]; - coeffD = new Vector[nNodes]; - - if (nNodes == 0) { - return; - } - - INode nodeB = nodes.get(0); - double tensionB = nodeB.getTension(); - double biasB = nodeB.getBias(); - double continuityB = nodeB.getContinuity(); - for (int i = 0; i < nNodes; ++i) { - final double tensionA = tensionB; - final double biasA = biasB; - final double continuityA = continuityB; - - if (i + 1 < nNodes) { - nodeB = nodes.get(i + 1); - tensionB = nodeB.getTension(); - biasB = nodeB.getBias(); - continuityB = nodeB.getContinuity(); - } - - // Kochanek-Bartels tangent coefficients - final double ta = (1 - tensionA) * (1 + biasA) * (1 + continuityA) / 2; // Factor for lhs of d[i] - final double tb = (1 - tensionA) * (1 - biasA) * (1 - continuityA) / 2; // Factor for rhs of d[i] - final double tc = (1 - tensionB) * (1 + biasB) * (1 - continuityB) / 2; // Factor for lhs of d[i+1] - final double td = (1 - tensionB) * (1 - biasB) * (1 + continuityB) / 2; // Factor for rhs of d[i+1] - - coeffA[i] = linearCombination(i, -ta, ta - tb - tc + 2, tb + tc - td - 2, td); - coeffB[i] = linearCombination(i, 2 * ta, -2 * ta + 2 * tb + tc - 3, -2 * tb - tc + td + 3, -td); - coeffC[i] = linearCombination(i, -ta, ta - tb, tb, 0); - //coeffD[i] = linearCombination(i, 0, 1, 0, 0); - coeffD[i] = retrieve(i); // this is an optimization - } - - scaling = nodes.size() - 1; - } - - /** - * Returns the linear combination of the given coefficients with the nodes adjacent to baseIndex. - * - * @param baseIndex node index - * @param f1 coefficient for baseIndex-1 - * @param f2 coefficient for baseIndex - * @param f3 coefficient for baseIndex+1 - * @param f4 coefficient for baseIndex+2 - * @return linear combination of nodes[n-1..n+2] with f1..4 - */ - private Vector linearCombination(int baseIndex, double f1, double f2, double f3, double f4) { - final Vector r1 = retrieve(baseIndex - 1).multiply(f1); - final Vector r2 = retrieve(baseIndex).multiply(f2); - final Vector r3 = retrieve(baseIndex + 1).multiply(f3); - final Vector r4 = retrieve(baseIndex + 2).multiply(f4); - - return r1.add(r2).add(r3).add(r4); - } - - /** - * Retrieves a node. Indexes are clamped to the valid range. - * - * @param index node index to retrieve - * @return nodes[clamp(0, nodes.length - 1)] - */ - private Vector retrieve(int index) { - if (index < 0) { - return fastRetrieve(0); - } - - if (index >= nodes.size()) { - return fastRetrieve(nodes.size() - 1); - } - - return fastRetrieve(index); - } - - private Vector fastRetrieve(int index) { - return nodes.get(index).getPosition(); - } - - @Override - public Vector getPosition(double position) { - if (coeffA == null) { - throw new IllegalStateException("Must call setNodes first."); - } - - if (position > 1) { - return null; - } - - position *= scaling; - - final int index = (int) Math.floor(position); - final double remainder = position - index; - - final Vector a = coeffA[index]; - final Vector b = coeffB[index]; - final Vector c = coeffC[index]; - final Vector d = coeffD[index]; - - return a.multiply(remainder).add(b).multiply(remainder).add(c).multiply(remainder).add(d); - } - - @Override - public Vector get1stDerivative(double position) { - if (coeffA == null) { - throw new IllegalStateException("Must call setNodes first."); - } - - if (position > 1) { - return null; - } - - position *= scaling; - - final int index = (int) Math.floor(position); - //final double remainder = position - index; - - final Vector a = coeffA[index]; - final Vector b = coeffB[index]; - final Vector c = coeffC[index]; - - return a.multiply(1.5 * position - 3.0 * index).add(b).multiply(2.0 * position).add(a.multiply(1.5 * index).subtract(b).multiply(2.0 * index)).add(c).multiply(scaling); - } - - @Override - public double arcLength(double positionA, double positionB) { - if (coeffA == null) { - throw new IllegalStateException("Must call setNodes first."); - } - - if (positionA > positionB) { - return arcLength(positionB, positionA); - } - - positionA *= scaling; - positionB *= scaling; - - final int indexA = (int) Math.floor(positionA); - final double remainderA = positionA - indexA; - - final int indexB = (int) Math.floor(positionB); - final double remainderB = positionB - indexB; - - return arcLengthRecursive(indexA, remainderA, indexB, remainderB); - } - - /** - * Assumes a < b. - */ - private double arcLengthRecursive(int indexLeft, double remainderLeft, int indexRight, double remainderRight) { - switch (indexRight - indexLeft) { - case 0: - return arcLengthRecursive(indexLeft, remainderLeft, remainderRight); - - case 1: - // This case is merely a speed-up for a very common case - return arcLengthRecursive(indexLeft, remainderLeft, 1.0) - + arcLengthRecursive(indexRight, 0.0, remainderRight); - - default: - return arcLengthRecursive(indexLeft, remainderLeft, indexRight - 1, 1.0) - + arcLengthRecursive(indexRight, 0.0, remainderRight); - } - } - - private double arcLengthRecursive(int index, double remainderLeft, double remainderRight) { - final Vector a = coeffA[index].multiply(3.0); - final Vector b = coeffB[index].multiply(2.0); - final Vector c = coeffC[index]; - - final int nPoints = 8; - - double accum = a.multiply(remainderLeft).add(b).multiply(remainderLeft).add(c).length() / 2.0; - for (int i = 1; i < nPoints - 1; ++i) { - double t = ((double) i) / nPoints; - t = (remainderRight - remainderLeft) * t + remainderLeft; - accum += a.multiply(t).add(b).multiply(t).add(c).length(); - } - - accum += a.multiply(remainderRight).add(b).multiply(remainderRight).add(c).length() / 2.0; - return accum * (remainderRight - remainderLeft) / nPoints; - } - - @Override - public int getSegment(double position) { - if (coeffA == null) { - throw new IllegalStateException("Must call setNodes first."); - } - - if (position > 1) { - return Integer.MAX_VALUE; - } - - position *= scaling; - - return (int) Math.floor(position); - } - -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/math/M.java b/core/src/main/java/com/volmit/iris/util/math/M.java deleted file mode 100644 index 7dc527d4e..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/M.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; -import java.util.regex.Matcher; - -/** - * Math - * - * @author cyberpwn - */ -public class M { - private static final int precision = 128; - private static final int modulus = 360 * precision; - private static final float[] sin = new float[modulus]; - public static int tick = 0; - - static { - for (int i = 0; i < sin.length; i++) { - sin[i] = (float) Math.sin((i * Math.PI) / (precision * 180)); - } - } - - /** - * Scales B by an external range change so that
- *
- * BMIN < B < BMAX
- * AMIN < RESULT < AMAX
- *
- * So Given rangeScale(0, 20, 0, 10, 5) -> 10
- * 0 < 5 < 10
- * 0 < ? < 20
- *
- * would return 10 - * - * @param amin the resulting minimum - * @param amax the resulting maximum - * @param bmin the initial minimum - * @param bmax the initial maximum - * @param b the initial value - * @return the resulting value - */ - public static double rangeScale(double amin, double amax, double bmin, double bmax, double b) { - return amin + ((amax - amin) * ((b - bmin) / (bmax - bmin))); - } - - /** - * Get the percent (inverse lerp) from "from" to "to" where "at". - *

- * If from = 0 and to = 100 and at = 25 then it would return 0.25 - * - * @param from the from - * @param to the to - * @param at the at - * @return the percent - */ - public static double lerpInverse(double from, double to, double at) { - return M.rangeScale(0, 1, from, to, at); - } - - /** - * Linear interpolation from a to b where f is the percent across - * - * @param a the first pos (0) - * @param b the second pos (1) - * @param f the percent - * @return the value - */ - public static double lerp(double a, double b, double f) { - return a + (f * (b - a)); - } - - /** - * Bilinear interpolation - * - * @param a the first point (0, 0) - * @param b the second point (1, 0) - * @param c the third point (0, 1) - * @param d the fourth point (1, 1) - * @return the bilerped value - */ - public static double bilerp(double a, double b, double c, double d, double x, double y) { - return lerp(lerp(a, b, x), lerp(c, d, x), y); - } - - /** - * Trilinear interpolation - * - * @param a the first point (0, 0, 0) - * @param b the second point (1, 0, 0) - * @param c the third point (0, 0, 1) - * @param d the fourth point (1, 0, 1) - * @param e the fifth point (0, 1, 0) - * @param f the sixth point (1, 1, 0) - * @param g the seventh point (0, 1, 1) - * @param h the eighth point (1, 1, 1) - * @param x the x - * @param y the y - * @param z the z - * @return the trilerped value - */ - public static double trilerp(double a, double b, double c, double d, double e, double f, double g, double h, double x, double y, double z) { - return lerp(bilerp(a, b, c, d, x, y), bilerp(e, f, g, h, x, y), z); - } - - /** - * Clip a value - * - * @param value the value - * @param min the min - * @param max the max - * @return the clipped value - */ - @SuppressWarnings("unchecked") - public static T clip(T value, T min, T max) { - return (T) Double.valueOf(Math.min(max.doubleValue(), Math.max(min.doubleValue(), value.doubleValue()))); - } - - /** - * Get true or false based on random percent - * - * @param d between 0 and 1 - * @return true if true - */ - public static boolean r(Double d) { - //noinspection ReplaceNullCheck - if (d == null) { - return Math.random() < 0.5; - } - - return Math.random() < d; - } - - /** - * Get the ticks per second from a time in nanoseconds, the rad can be used for - * multiple ticks - * - * @param ns the time in nanoseconds - * @param rad the radius of the time - * @return the ticks per second in double form - */ - public static double tps(long ns, int rad) { - return (20.0 * (ns / 50000000.0)) / rad; - } - - /** - * Get the number of ticks from a time in nanoseconds - * - * @param ns the nanoseconds - * @return the amount of ticks - */ - public static double ticksFromNS(long ns) { - return (ns / 50000000.0); - } - - /** - * Get a random int from to (inclusive) - * - * @param f the from - * @param t the to - * @return the value - */ - public static int irand(int f, int t) { - return f + (int) (Math.random() * ((t - f) + 1)); - } - - /** - * Get a random float from to (inclusive) - * - * @param f the from - * @param t the to - * @return the value - */ - public static float frand(float f, float t) { - return f + (float) (Math.random() * ((t - f) + 1)); - } - - /** - * Get a random double from to (inclusive) - * - * @param f the from - * @param t the to - * @return the value - */ - public static double drand(double f, double t) { - return f + (Math.random() * ((t - f) + 1)); - } - - /** - * Get system Nanoseconds - * - * @return nanoseconds (current) - */ - public static long ns() { - return System.nanoTime(); - } - - /** - * Get the current millisecond time - * - * @return milliseconds - */ - public static long ms() { - return System.currentTimeMillis(); - } - - /** - * Fast sin function - * - * @param a the number - * @return the sin - */ - public static float sin(float a) { - return sinLookup((int) (a * precision + 0.5f)); - } - - /** - * Fast cos function - * - * @param a the number - * @return the cos - */ - public static float cos(float a) { - return sinLookup((int) ((a + 90f) * precision + 0.5f)); - } - - /** - * Fast tan function - * - * @param a the number - * @return the tan - */ - public static float tan(float a) { - float c = cos(a); - return sin(a) / (c == 0 ? 0.0000001f : c); - } - - /** - * Biggest number - * - * @return the biggest one - */ - @SuppressWarnings("unchecked") - public static T max(T... doubles) { - double max = Double.MIN_VALUE; - - for (T i : doubles) { - if (i.doubleValue() > max) { - max = i.doubleValue(); - } - } - - return (T) Double.valueOf(max); - } - - /** - * Smallest number - * - * @param doubles the numbers - * @return the smallest one - */ - @SuppressWarnings("unchecked") - public static T min(T... doubles) { - double min = Double.MAX_VALUE; - - for (T i : doubles) { - if (i.doubleValue() < min) { - min = i.doubleValue(); - } - } - - return (T) Double.valueOf(min); - } - - /** - * Evaluates an expression using javascript engine and returns the double - * result. This can take variable parameters, so you need to define them. - * Parameters are defined as $[0-9]. For example evaluate("4$0/$1", 1, 2); This - * makes the expression (4x1)/2 == 2. Keep note that you must use 0-9, you - * cannot skip, or start at a number other than 0. - * - * @param expression the expression with variables - * @param args the arguments/variables - * @return the resulting double value - * @throws ScriptException ... gg - * @throws IndexOutOfBoundsException learn to count - */ - public static double evaluate(String expression, Double... args) throws ScriptException, IndexOutOfBoundsException { - for (int i = 0; i < args.length; i++) { - String current = "$" + i; - - if (expression.contains(current)) { - expression = expression.replaceAll(Matcher.quoteReplacement(current), args[i] + ""); - } - } - - return evaluate(expression); - } - - /** - * Evaluates an expression using javascript engine and returns the double - * - * @param expression the mathimatical expression - * @return the double result - * @throws ScriptException ... gg - */ - public static double evaluate(String expression) throws ScriptException { - ScriptEngineManager mgr = new ScriptEngineManager(); - ScriptEngine scriptEngine = mgr.getEngineByName("JavaScript"); - - return Double.parseDouble(scriptEngine.eval(expression).toString()); - } - - /** - * is the number "is" within from-to - * - * @param from the lower end - * @param to the upper end - * @param is the check - * @return true if its within - */ - public static boolean within(int from, int to, int is) { - return is >= from && is <= to; - } - - /** - * Get the amount of days past since the epoch time (1970 jan 1 utc) - * - * @return the epoch days - */ - public static long epochDays() { - return epochDays(M.ms()); - } - - /** - * Get the amount of days past since the epoch time (1970 jan 1 utc) - * - * @param ms the time in milliseconds - * @return the epoch days - */ - private static long epochDays(long ms) { - return ms / 1000 / 60 / 60 / 24; - } - - private static float sinLookup(int a) { - return a >= 0 ? sin[a % (modulus)] : -sin[-a % (modulus)]; - } - - public static boolean interval(int tickInterval) { - return tick % (tickInterval <= 0 ? 1 : tickInterval) == 0; - } - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/MathHelper.java b/core/src/main/java/com/volmit/iris/util/math/MathHelper.java deleted file mode 100644 index e38338a83..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/MathHelper.java +++ /dev/null @@ -1,728 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import java.util.Random; -import java.util.UUID; -import java.util.function.IntPredicate; - -public class MathHelper { - public static final float a = 3.1415927F; - public static final float b = 1.5707964F; - public static final float c = 6.2831855F; - public static final float d = 0.017453292F; - public static final float e = 57.295776F; - public static final float f = 1.0E-5F; - public static final float g = c(2.0F); - private static final int h = 1024; - private static final float i = 1024.0F; - private static final long j = 61440L; - private static final long k = 16384L; - private static final long l = -4611686018427387904L; - private static final long m = -9223372036854775808L; - private static final float n = 10430.378F; - private static final Random p = new Random(); - private static final int[] q = new int[]{0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; - private static final double r = 0.16666666666666666D; - private static final int s = 8; - private static final int t = 257; - private static final double u = Double.longBitsToDouble(4805340802404319232L); - private static final double[] v = new double[257]; - private static final double[] w = new double[257]; - - static { - for (int var0 = 0; var0 < 257; ++var0) { - double var1 = (double) var0 / 256.0D; - double var3 = Math.asin(var1); - w[var0] = Math.cos(var3); - v[var0] = var3; - } - - } - - public MathHelper() { - } - - public static float c(float var0) { - return (float) Math.sqrt(var0); - } - - public static int d(float var0) { - int var1 = (int) var0; - return var0 < (float) var1 ? var1 - 1 : var1; - } - - public static int a(double var0) { - return (int) (var0 + 1024.0D) - 1024; - } - - public static int floor(double var0) { - int var2 = (int) var0; - return var0 < (double) var2 ? var2 - 1 : var2; - } - - public static long c(double var0) { - long var2 = (long) var0; - return var0 < (double) var2 ? var2 - 1L : var2; - } - - public static int d(double var0) { - return (int) (var0 >= 0.0D ? var0 : -var0 + 1.0D); - } - - public static float e(float var0) { - return Math.abs(var0); - } - - public static int a(int var0) { - return Math.abs(var0); - } - - public static int f(float var0) { - int var1 = (int) var0; - return var0 > (float) var1 ? var1 + 1 : var1; - } - - public static int e(double var0) { - int var2 = (int) var0; - return var0 > (double) var2 ? var2 + 1 : var2; - } - - public static byte a(byte var0, byte var1, byte var2) { - if (var0 < var1) { - return var1; - } else { - return var0 > var2 ? var2 : var0; - } - } - - public static int clamp(int var0, int var1, int var2) { - if (var0 < var1) { - return var1; - } else { - return Math.min(var0, var2); - } - } - - public static long a(long var0, long var2, long var4) { - if (var0 < var2) { - return var2; - } else { - return Math.min(var0, var4); - } - } - - public static float a(float var0, float var1, float var2) { - if (var0 < var1) { - return var1; - } else { - return Math.min(var0, var2); - } - } - - public static double a(double var0, double var2, double var4) { - if (var0 < var2) { - return var2; - } else { - return Math.min(var0, var4); - } - } - - public static double b(double var0, double var2, double var4) { - if (var4 < 0.0D) { - return var0; - } else { - return var4 > 1.0D ? var2 : d(var4, var0, var2); - } - } - - public static float b(float var0, float var1, float var2) { - if (var2 < 0.0F) { - return var0; - } else { - return var2 > 1.0F ? var1 : h(var2, var0, var1); - } - } - - public static double a(double var0, double var2) { - if (var0 < 0.0D) { - var0 = -var0; - } - - if (var2 < 0.0D) { - var2 = -var2; - } - - return Math.max(var0, var2); - } - - public static int a(int var0, int var1) { - return Math.floorDiv(var0, var1); - } - - public static int nextInt(Random var0, int var1, int var2) { - return var1 >= var2 ? var1 : var0.nextInt(var2 - var1 + 1) + var1; - } - - public static float a(Random var0, float var1, float var2) { - return var1 >= var2 ? var1 : var0.nextFloat() * (var2 - var1) + var1; - } - - public static double a(Random var0, double var1, double var3) { - return var1 >= var3 ? var1 : var0.nextDouble() * (var3 - var1) + var1; - } - - public static double a(long[] var0) { - long var1 = 0L; - long[] var3 = var0; - int var4 = var0.length; - - for (int var5 = 0; var5 < var4; ++var5) { - long var6 = var3[var5]; - var1 += var6; - } - - return (double) var1 / (double) var0.length; - } - - public static boolean a(float var0, float var1) { - return Math.abs(var1 - var0) < 1.0E-5F; - } - - public static boolean b(double var0, double var2) { - return Math.abs(var2 - var0) < 9.999999747378752E-6D; - } - - public static int b(int var0, int var1) { - return Math.floorMod(var0, var1); - } - - public static float b(float var0, float var1) { - return (var0 % var1 + var1) % var1; - } - - public static double c(double var0, double var2) { - return (var0 % var2 + var2) % var2; - } - - public static int b(int var0) { - int var1 = var0 % 360; - if (var1 >= 180) { - var1 -= 360; - } - - if (var1 < -180) { - var1 += 360; - } - - return var1; - } - - public static float g(float var0) { - float var1 = var0 % 360.0F; - if (var1 >= 180.0F) { - var1 -= 360.0F; - } - - if (var1 < -180.0F) { - var1 += 360.0F; - } - - return var1; - } - - public static double f(double var0) { - double var2 = var0 % 360.0D; - if (var2 >= 180.0D) { - var2 -= 360.0D; - } - - if (var2 < -180.0D) { - var2 += 360.0D; - } - - return var2; - } - - public static float c(float var0, float var1) { - return g(var1 - var0); - } - - public static float d(float var0, float var1) { - return e(c(var0, var1)); - } - - public static float c(float var0, float var1, float var2) { - float var3 = c(var0, var1); - float var4 = a(var3, -var2, var2); - return var1 - var4; - } - - public static float d(float var0, float var1, float var2) { - var2 = e(var2); - return var0 < var1 ? a(var0 + var2, var0, var1) : a(var0 - var2, var1, var0); - } - - public static float e(float var0, float var1, float var2) { - float var3 = c(var0, var1); - return d(var0, var0 + var3, var2); - } - - public static double a(String var0, double var1) { - try { - return Double.parseDouble(var0); - } catch (Throwable var4) { - return var1; - } - } - - public static double a(String var0, double var1, double var3) { - return Math.max(var3, a(var0, var1)); - } - - public static int c(int var0) { - int var1 = var0 - 1; - var1 |= var1 >> 1; - var1 |= var1 >> 2; - var1 |= var1 >> 4; - var1 |= var1 >> 8; - var1 |= var1 >> 16; - return var1 + 1; - } - - public static boolean d(int var0) { - return var0 != 0 && (var0 & var0 - 1) == 0; - } - - public static int e(int var0) { - var0 = d(var0) ? var0 : c(var0); - return q[(int) ((long) var0 * 125613361L >> 27) & 31]; - } - - public static int f(int var0) { - return e(var0) - (d(var0) ? 0 : 1); - } - - public static int f(float var0, float var1, float var2) { - return b(d(var0 * 255.0F), d(var1 * 255.0F), d(var2 * 255.0F)); - } - - public static int b(int var0, int var1, int var2) { - int var3 = (var0 << 8) + var1; - var3 = (var3 << 8) + var2; - return var3; - } - - public static int c(int var0, int var1) { - int var2 = (var0 & 16711680) >> 16; - int var3 = (var1 & 16711680) >> 16; - int var4 = (var0 & '\uff00') >> 8; - int var5 = (var1 & '\uff00') >> 8; - int var6 = (var0 & 255); - int var7 = (var1 & 255); - int var8 = (int) ((float) var2 * (float) var3 / 255.0F); - int var9 = (int) ((float) var4 * (float) var5 / 255.0F); - int var10 = (int) ((float) var6 * (float) var7 / 255.0F); - return var0 & -16777216 | var8 << 16 | var9 << 8 | var10; - } - - public static int a(int var0, float var1, float var2, float var3) { - int var4 = (var0 & 16711680) >> 16; - int var5 = (var0 & '\uff00') >> 8; - int var6 = (var0 & 255); - int var7 = (int) ((float) var4 * var1); - int var8 = (int) ((float) var5 * var2); - int var9 = (int) ((float) var6 * var3); - return var0 & -16777216 | var7 << 16 | var8 << 8 | var9; - } - - public static float h(float var0) { - return var0 - (float) d(var0); - } - - public static double g(double var0) { - return var0 - (double) c(var0); - } - - public static long c(int var0, int var1, int var2) { - long var3 = (long) (var0 * 3129871) ^ (long) var2 * 116129781L ^ (long) var1; - var3 = var3 * var3 * 42317861L + var3 * 11L; - return var3 >> 16; - } - - public static UUID a(Random var0) { - long var1 = var0.nextLong() & -61441L | 16384L; - long var3 = var0.nextLong() & 4611686018427387903L | -9223372036854775808L; - return new UUID(var1, var3); - } - - public static UUID a() { - return a(p); - } - - public static double c(double var0, double var2, double var4) { - return (var0 - var2) / (var4 - var2); - } - - public static double d(double var0, double var2) { - double var4 = var2 * var2 + var0 * var0; - if (Double.isNaN(var4)) { - return 0.0D; - } else { - boolean var6 = var0 < 0.0D; - if (var6) { - var0 = -var0; - } - - boolean var7 = var2 < 0.0D; - if (var7) { - var2 = -var2; - } - - boolean var8 = var0 > var2; - double var9; - if (var8) { - var9 = var2; - var2 = var0; - var0 = var9; - } - - var9 = h(var4); - var2 *= var9; - var0 *= var9; - double var11 = u + var0; - int var13 = (int) Double.doubleToRawLongBits(var11); - double var14 = v[var13]; - double var16 = w[var13]; - double var18 = var11 - u; - double var20 = var0 * var16 - var2 * var18; - double var22 = (6.0D + var20 * var20) * var20 * 0.16666666666666666D; - double var24 = var14 + var22; - if (var8) { - var24 = 1.5707963267948966D - var24; - } - - if (var7) { - var24 = 3.141592653589793D - var24; - } - - if (var6) { - var24 = -var24; - } - - return var24; - } - } - - public static float i(float var0) { - float var1 = 0.5F * var0; - int var2 = Float.floatToIntBits(var0); - var2 = 1597463007 - (var2 >> 1); - var0 = Float.intBitsToFloat(var2); - var0 *= 1.5F - var1 * var0 * var0; - return var0; - } - - public static double h(double var0) { - double var2 = 0.5D * var0; - long var4 = Double.doubleToRawLongBits(var0); - var4 = 6910469410427058090L - (var4 >> 1); - var0 = Double.longBitsToDouble(var4); - var0 *= 1.5D - var2 * var0 * var0; - return var0; - } - - public static float j(float var0) { - int var1 = Float.floatToIntBits(var0); - var1 = 1419967116 - var1 / 3; - float var2 = Float.intBitsToFloat(var1); - var2 = 0.6666667F * var2 + 1.0F / (3.0F * var2 * var2 * var0); - var2 = 0.6666667F * var2 + 1.0F / (3.0F * var2 * var2 * var0); - return var2; - } - - public static int g(float var0, float var1, float var2) { - int var3 = (int) (var0 * 6.0F) % 6; - float var4 = var0 * 6.0F - (float) var3; - float var5 = var2 * (1.0F - var1); - float var6 = var2 * (1.0F - var4 * var1); - float var7 = var2 * (1.0F - (1.0F - var4) * var1); - float var8; - float var9; - float var10; - switch (var3) { - case 0: - var8 = var2; - var9 = var7; - var10 = var5; - break; - case 1: - var8 = var6; - var9 = var2; - var10 = var5; - break; - case 2: - var8 = var5; - var9 = var2; - var10 = var7; - break; - case 3: - var8 = var5; - var9 = var6; - var10 = var2; - break; - case 4: - var8 = var7; - var9 = var5; - var10 = var2; - break; - case 5: - var8 = var2; - var9 = var5; - var10 = var6; - break; - default: - throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + var0 + ", " + var1 + ", " + var2); - } - - int var11 = clamp((int) (var8 * 255.0F), 0, 255); - int var12 = clamp((int) (var9 * 255.0F), 0, 255); - int var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - } - - public static int g(int var0) { - var0 ^= var0 >>> 16; - var0 *= -2048144789; - var0 ^= var0 >>> 13; - var0 *= -1028477387; - var0 ^= var0 >>> 16; - return var0; - } - - public static long a(long var0) { - var0 ^= var0 >>> 33; - var0 *= -49064778989728563L; - var0 ^= var0 >>> 33; - var0 *= -4265267296055464877L; - var0 ^= var0 >>> 33; - return var0; - } - - public static double[] a(double... var0) { - float var1 = 0.0F; - double[] var2f = var0; - int var3 = var0.length; - - for (int var4 = 0; var4 < var3; ++var4) { - double var5 = var2f[var4]; - var1 = (float) ((double) var1 + var5); - } - - int var2; - for (var2 = 0; var2 < var0.length; ++var2) { - var0[var2] /= var1; - } - - for (var2 = 0; var2 < var0.length; ++var2) { - var0[var2] += var2 == 0 ? 0.0D : var0[var2 - 1]; - } - - return var0; - } - - public static int a(Random var0, double[] var1) { - double var2 = var0.nextDouble(); - - for (int var4 = 0; var4 < var1.length; ++var4) { - if (var2 < var1[var4]) { - return var4; - } - } - - return var1.length; - } - - public static double[] a(double var0, double var2, double var4, int var6, int var7) { - double[] var8 = new double[var7 - var6 + 1]; - int var9 = 0; - - for (int var10 = var6; var10 <= var7; ++var10) { - var8[var9] = Math.max(0.0D, var0 * StrictMath.exp(-((double) var10 - var4) * ((double) var10 - var4) / (2.0D * var2 * var2))); - ++var9; - } - - return var8; - } - - public static double[] a(double var0, double var2, double var4, double var6, double var8, double var10, int var12, int var13) { - double[] var14 = new double[var13 - var12 + 1]; - int var15 = 0; - - for (int var16 = var12; var16 <= var13; ++var16) { - var14[var15] = Math.max(0.0D, var0 * StrictMath.exp(-((double) var16 - var4) * ((double) var16 - var4) / (2.0D * var2 * var2)) + var6 * StrictMath.exp(-((double) var16 - var10) * ((double) var16 - var10) / (2.0D * var8 * var8))); - ++var15; - } - - return var14; - } - - public static double[] a(double var0, double var2, int var4, int var5) { - double[] var6 = new double[var5 - var4 + 1]; - int var7 = 0; - - for (int var8 = var4; var8 <= var5; ++var8) { - var6[var7] = Math.max(var0 * StrictMath.log(var8) + var2, 0.0D); - ++var7; - } - - return var6; - } - - public static int a(int var0, int var1, IntPredicate var2) { - int var3 = var1 - var0; - - while (var3 > 0) { - int var4 = var3 / 2; - int var5 = var0 + var4; - if (var2.test(var5)) { - var3 = var4; - } else { - var0 = var5 + 1; - var3 -= var4 + 1; - } - } - - return var0; - } - - public static float h(float var0, float var1, float var2) { - return var1 + var0 * (var2 - var1); - } - - public static double d(double var0, double var2, double var4) { - return var2 + var0 * (var4 - var2); - } - - public static double a(double var0, double var2, double var4, double var6, double var8, double var10) { - return d(var2, d(var0, var4, var6), d(var0, var8, var10)); - } - - public static double a(double var0, double var2, double var4, double var6, double var8, double var10, double var12, double var14, double var16, double var18, double var20) { - return d(var4, a(var0, var2, var6, var8, var10, var12), a(var0, var2, var14, var16, var18, var20)); - } - - public static double i(double var0) { - return var0 * var0 * var0 * (var0 * (var0 * 6.0D - 15.0D) + 10.0D); - } - - public static double j(double var0) { - return 30.0D * var0 * var0 * (var0 - 1.0D) * (var0 - 1.0D); - } - - public static int k(double var0) { - if (var0 == 0.0D) { - return 0; - } else { - return var0 > 0.0D ? 1 : -1; - } - } - - public static float i(float var0, float var1, float var2) { - return var1 + var0 * g(var2 - var1); - } - - public static float j(float var0, float var1, float var2) { - return Math.min(var0 * var0 * 0.6F + var1 * var1 * ((3.0F + var1) / 4.0F) + var2 * var2 * 0.8F, 1.0F); - } - - @Deprecated - public static float k(float var0, float var1, float var2) { - float var3; - for (var3 = var1 - var0; var3 < -180.0F; var3 += 360.0F) { - } - - while (var3 >= 180.0F) { - var3 -= 360.0F; - } - - return var0 + var2 * var3; - } - - @Deprecated - public static float l(double var0) { - while (var0 >= 180.0D) { - var0 -= 360.0D; - } - - while (var0 < -180.0D) { - var0 += 360.0D; - } - - return (float) var0; - } - - public static float e(float var0, float var1) { - return (Math.abs(var0 % var1 - var1 * 0.5F) - var1 * 0.25F) / (var1 * 0.25F); - } - - public static float k(float var0) { - return var0 * var0; - } - - public static double m(double var0) { - return var0 * var0; - } - - public static int h(int var0) { - return var0 * var0; - } - - public static double a(double var0, double var2, double var4, double var6, double var8) { - return b(var6, var8, c(var0, var2, var4)); - } - - public static double b(double var0, double var2, double var4, double var6, double var8) { - return d(c(var0, var2, var4), var6, var8); - } - - public static double n(double var0) { - return var0 + (2.0D * (new Random(floor(var0 * 3000.0D))).nextDouble() - 1.0D) * 1.0E-7D / 2.0D; - } - - public static int d(int var0, int var1) { - return (var0 + var1 - 1) / var1 * var1; - } - - public static int b(Random var0, int var1, int var2) { - return var0.nextInt(var2 - var1 + 1) + var1; - } - - public static float b(Random var0, float var1, float var2) { - return var0.nextFloat() * (var2 - var1) + var1; - } - - public static float c(Random var0, float var1, float var2) { - return var1 + (float) var0.nextGaussian() * var2; - } - - public static double a(int var0, double var1, int var3) { - return Math.sqrt((double) (var0 * var0) + var1 * var1 + (double) (var3 * var3)); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/math/PathInterpolation.java b/core/src/main/java/com/volmit/iris/util/math/PathInterpolation.java deleted file mode 100644 index 6deea6b48..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/PathInterpolation.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import org.bukkit.util.Vector; - -import java.util.List; - -public interface PathInterpolation { - - /** - * Sets nodes to be used by subsequent calls to - * {@link #getPosition(double)} and the other methods. - * - * @param nodes the nodes - */ - void setNodes(List nodes); - - /** - * Gets the result of f(position). - * - * @param position the position to interpolate - * @return the result - */ - Vector getPosition(double position); - - /** - * Gets the result of f'(position). - * - * @param position the position to interpolate - * @return the result - */ - Vector get1stDerivative(double position); - - /** - * Gets the result of ∫ab|f'(t)| dt.
- * That means it calculates the arc length (in meters) between positionA - * and positionB. - * - * @param positionA lower limit - * @param positionB upper limit - * @return the arc length - */ - double arcLength(double positionA, double positionB); - - /** - * Get the segment position. - * - * @param position the position - * @return the segment position - */ - int getSegment(double position); - -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/math/Point3d.java b/core/src/main/java/com/volmit/iris/util/math/Point3d.java deleted file mode 100644 index e3b934ada..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Point3d.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -/** - * A 3 element point that is represented by double precision floating point - * x,y,z coordinates. - */ -public class Point3d extends Tuple3d implements java.io.Serializable { - - // Compatible with 1.1 - static final long serialVersionUID = 5718062286069042927L; - - /** - * Constructs and initializes a Point3d from the specified xyz coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - */ - public Point3d(double x, double y, double z) { - super(x, y, z); - } - - - /** - * Constructs and initializes a Point3d from the array of length 3. - * - * @param p the array of length 3 containing xyz in order - */ - public Point3d(double[] p) { - super(p); - } - - - /** - * Constructs and initializes a Point3d from the specified Point3d. - * - * @param p1 the Point3d containing the initialization x y z data - */ - public Point3d(Point3d p1) { - super(p1); - } - - - /** - * Constructs and initializes a Point3d from the specified Point3f. - * - * @param p1 the Point3f containing the initialization x y z data - */ - public Point3d(Point3f p1) { - super(p1); - } - - - /** - * Constructs and initializes a Point3d from the specified Tuple3f. - * - * @param t1 the Tuple3f containing the initialization x y z data - */ - public Point3d(Tuple3f t1) { - super(t1); - } - - - /** - * Constructs and initializes a Point3d from the specified Tuple3d. - * - * @param t1 the Tuple3d containing the initialization x y z data - */ - public Point3d(Tuple3d t1) { - super(t1); - } - - - /** - * Constructs and initializes a Point3d to (0,0,0). - */ - public Point3d() { - super(); - } - - - /** - * Returns the square of the distance between this point and point p1. - * - * @param p1 the other point - * @return the square of the distance - */ - public final double distanceSquared(Point3d p1) { - double dx, dy, dz; - - dx = this.x - p1.x; - dy = this.y - p1.y; - dz = this.z - p1.z; - return (dx * dx + dy * dy + dz * dz); - } - - - /** - * Returns the distance between this point and point p1. - * - * @param p1 the other point - * @return the distance - */ - public final double distance(Point3d p1) { - double dx, dy, dz; - - dx = this.x - p1.x; - dy = this.y - p1.y; - dz = this.z - p1.z; - return Math.sqrt(dx * dx + dy * dy + dz * dz); - } - - - /** - * Computes the L-1 (Manhattan) distance between this point and - * point p1. The L-1 distance is equal to: - * abs(x1-x2) + abs(y1-y2) + abs(z1-z2). - * - * @param p1 the other point - * @return the L-1 distance - */ - public final double distanceL1(Point3d p1) { - return Math.abs(this.x - p1.x) + Math.abs(this.y - p1.y) + - Math.abs(this.z - p1.z); - } - - - /** - * Computes the L-infinite distance between this point and - * point p1. The L-infinite distance is equal to - * MAX[abs(x1-x2), abs(y1-y2), abs(z1-z2)]. - * - * @param p1 the other point - * @return the L-infinite distance - */ - public final double distanceLinf(Point3d p1) { - double tmp; - tmp = Math.max(Math.abs(this.x - p1.x), Math.abs(this.y - p1.y)); - - return Math.max(tmp, Math.abs(this.z - p1.z)); - } - - - /** - * Multiplies each of the x,y,z components of the Point4d parameter - * by 1/w and places the projected values into this point. - * - * @param p1 the source Point4d, which is not modified - */ - public final void project(Point4d p1) { - double oneOw; - - oneOw = 1 / p1.w; - x = p1.x * oneOw; - y = p1.y * oneOw; - z = p1.z * oneOw; - - } - - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Point3f.java b/core/src/main/java/com/volmit/iris/util/math/Point3f.java deleted file mode 100644 index 1c82b7e6d..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Point3f.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -/** - * A 3 element point that is represented by single precision floating point - * x,y,z coordinates. - */ -public class Point3f extends Tuple3f implements java.io.Serializable { - - - // Compatible with 1.1 - static final long serialVersionUID = -8689337816398030143L; - - /** - * Constructs and initializes a Point3f from the specified xyz coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - */ - public Point3f(float x, float y, float z) { - super(x, y, z); - } - - - /** - * Constructs and initializes a Point3f from the array of length 3. - * - * @param p the array of length 3 containing xyz in order - */ - public Point3f(float[] p) { - super(p); - } - - - /** - * Constructs and initializes a Point3f from the specified Point3f. - * - * @param p1 the Point3f containing the initialization x y z data - */ - public Point3f(Point3f p1) { - super(p1); - } - - - /** - * Constructs and initializes a Point3f from the specified Point3d. - * - * @param p1 the Point3d containing the initialization x y z data - */ - public Point3f(Point3d p1) { - super(p1); - } - - - /** - * Constructs and initializes a Point3f from the specified Tuple3f. - * - * @param t1 the Tuple3f containing the initialization x y z data - */ - public Point3f(Tuple3f t1) { - super(t1); - } - - - /** - * Constructs and initializes a Point3f from the specified Tuple3d. - * - * @param t1 the Tuple3d containing the initialization x y z data - */ - public Point3f(Tuple3d t1) { - super(t1); - } - - - /** - * Constructs and initializes a Point3f to (0,0,0). - */ - public Point3f() { - super(); - } - - - /** - * Computes the square of the distance between this point and - * point p1. - * - * @param p1 the other point - * @return the square of the distance - */ - public final float distanceSquared(Point3f p1) { - float dx, dy, dz; - - dx = this.x - p1.x; - dy = this.y - p1.y; - dz = this.z - p1.z; - return dx * dx + dy * dy + dz * dz; - } - - - /** - * Computes the distance between this point and point p1. - * - * @param p1 the other point - * @return the distance - */ - public final float distance(Point3f p1) { - float dx, dy, dz; - - dx = this.x - p1.x; - dy = this.y - p1.y; - dz = this.z - p1.z; - return (float) Math.sqrt(dx * dx + dy * dy + dz * dz); - } - - - /** - * Computes the L-1 (Manhattan) distance between this point and - * point p1. The L-1 distance is equal to: - * abs(x1-x2) + abs(y1-y2) + abs(z1-z2). - * - * @param p1 the other point - * @return the L-1 distance - */ - public final float distanceL1(Point3f p1) { - return (Math.abs(this.x - p1.x) + Math.abs(this.y - p1.y) + Math.abs(this.z - p1.z)); - } - - - /** - * Computes the L-infinite distance between this point and - * point p1. The L-infinite distance is equal to - * MAX[abs(x1-x2), abs(y1-y2), abs(z1-z2)]. - * - * @param p1 the other point - * @return the L-infinite distance - */ - public final float distanceLinf(Point3f p1) { - float tmp; - tmp = Math.max(Math.abs(this.x - p1.x), Math.abs(this.y - p1.y)); - return (Math.max(tmp, Math.abs(this.z - p1.z))); - - } - - - /** - * Multiplies each of the x,y,z components of the Point4f parameter - * by 1/w and places the projected values into this point. - * - * @param p1 the source Point4f, which is not modified - */ - public final void project(Point4f p1) { - float oneOw; - - oneOw = 1 / p1.w; - x = p1.x * oneOw; - y = p1.y * oneOw; - z = p1.z * oneOw; - - } - - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Point4d.java b/core/src/main/java/com/volmit/iris/util/math/Point4d.java deleted file mode 100644 index fdfb72551..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Point4d.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -/** - * A 4 element vector represented by double precision floating point - * x,y,z,w coordinates. - */ -public class Point4d extends Tuple4d implements java.io.Serializable { - - // Compatible with 1.1 - static final long serialVersionUID = 1733471895962736949L; - - - /** - * Constructs and initializes a Point4d from the specified xyzw coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - * @param w the w coordinate - */ - public Point4d(double x, double y, double z, double w) { - super(x, y, z, w); - } - - /** - * Constructs and initializes a Point4d from the coordinates contained - * in the array. - * - * @param p the array of length 4 containing xyzw in order - */ - public Point4d(double[] p) { - super(p); - } - - - /** - * Constructs and initializes a Point4d from the specified Point4d. - * - * @param p1 the Point4d containing the initialization x y z w data - */ - public Point4d(Point4d p1) { - super(p1); - } - - - /** - * Constructs and initializes a Point4d from the specified Point4f. - * - * @param p1 the Point4f containing the initialization x y z w data - */ - public Point4d(Point4f p1) { - super(p1); - } - - - /** - * Constructs and initializes a Point4d from the specified Tuple4f. - * - * @param t1 the Tuple4f containing the initialization x y z w data - */ - public Point4d(Tuple4f t1) { - super(t1); - } - - - /** - * Constructs and initializes a Point4d from the specified Tuple4d. - * - * @param t1 the Tuple4d containing the initialization x y z w data - */ - public Point4d(Tuple4d t1) { - super(t1); - } - - - /** - * Constructs and initializes a Point4d from the specified Tuple3d. - * The x,y,z components of this point are set to the corresponding - * components of tuple t1. The w component of this point - * is set to 1. - * - * @param t1 the tuple to be copied - * @since vecmath 1.2 - */ - public Point4d(Tuple3d t1) { - super(t1.x, t1.y, t1.z, 1.0); - } - - - /** - * Constructs and initializes a Point4d to (0,0,0,0). - */ - public Point4d() { - super(); - } - - - /** - * Sets the x,y,z components of this point to the corresponding - * components of tuple t1. The w component of this point - * is set to 1. - * - * @param t1 the tuple to be copied - * @since vecmath 1.2 - */ - public final void set(Tuple3d t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - this.w = 1.0; - } - - - /** - * Returns the square of the distance between this point and point p1. - * - * @param p1 the first point - * @return the square of distance between this point and point p1 - */ - public final double distanceSquared(Point4d p1) { - double dx, dy, dz, dw; - - dx = this.x - p1.x; - dy = this.y - p1.y; - dz = this.z - p1.z; - dw = this.w - p1.w; - return (dx * dx + dy * dy + dz * dz + dw * dw); - } - - - /** - * Returns the distance between this point and point p1. - * - * @param p1 the first point - * @return the distance between these this point and point p1. - */ - public final double distance(Point4d p1) { - double dx, dy, dz, dw; - - dx = this.x - p1.x; - dy = this.y - p1.y; - dz = this.z - p1.z; - dw = this.w - p1.w; - return Math.sqrt(dx * dx + dy * dy + dz * dz + dw * dw); - } - - - /** - * Computes the L-1 (Manhattan) distance between this point and - * point p1. The L-1 distance is equal to: - * abs(x1-x2) + abs(y1-y2) + abs(z1-z2) + abs(w1-w2). - * - * @param p1 the other point - * @return the L-1 distance - */ - public final double distanceL1(Point4d p1) { - return Math.abs(this.x - p1.x) + Math.abs(this.y - p1.y) + - Math.abs(this.z - p1.z) + Math.abs(this.w - p1.w); - } - - /** - * Computes the L-infinite distance between this point and - * point p1. The L-infinite distance is equal to - * MAX[abs(x1-x2), abs(y1-y2), abs(z1-z2), abs(w1-w2)]. - * - * @param p1 the other point - * @return the L-infinite distance - */ - public final double distanceLinf(Point4d p1) { - double t1, t2; - t1 = Math.max(Math.abs(this.x - p1.x), Math.abs(this.y - p1.y)); - t2 = Math.max(Math.abs(this.z - p1.z), Math.abs(this.w - p1.w)); - - return Math.max(t1, t2); - } - - /** - * Multiplies each of the x,y,z components of the Point4d parameter - * by 1/w, places the projected values into this point, and places - * a 1 as the w parameter of this point. - * - * @param p1 the source Point4d, which is not modified - */ - public final void project(Point4d p1) { - double oneOw; - - oneOw = 1 / p1.w; - x = p1.x * oneOw; - y = p1.y * oneOw; - z = p1.z * oneOw; - w = 1.0; - - } - - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Point4f.java b/core/src/main/java/com/volmit/iris/util/math/Point4f.java deleted file mode 100644 index df98cbe08..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Point4f.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -/** - * A 4 element point represented by single precision floating point x,y,z,w - * coordinates. - */ -public class Point4f extends Tuple4f implements java.io.Serializable { - - - // Compatible with 1.1 - static final long serialVersionUID = 4643134103185764459L; - - /** - * Constructs and initializes a Point4f from the specified xyzw coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - * @param w the w coordinate - */ - public Point4f(float x, float y, float z, float w) { - super(x, y, z, w); - } - - - /** - * Constructs and initializes a Point4f from the array of length 4. - * - * @param p the array of length 4 containing xyzw in order - */ - public Point4f(float[] p) { - super(p); - } - - - /** - * Constructs and initializes a Point4f from the specified Point4f. - * - * @param p1 the Point4f containing the initialization x y z w data - */ - public Point4f(Point4f p1) { - super(p1); - } - - - /** - * Constructs and initializes a Point4f from the specified Point4d. - * - * @param p1 the Point4d containing the initialization x y z w data - */ - public Point4f(Point4d p1) { - super(p1); - } - - - /** - * Constructs and initializes a Point4f from the specified Tuple4f. - * - * @param t1 the Tuple4f containing the initialization x y z w data - */ - public Point4f(Tuple4f t1) { - super(t1); - } - - - /** - * Constructs and initializes a Point4f from the specified Tuple4d. - * - * @param t1 the Tuple4d containing the initialization x y z w data - */ - public Point4f(Tuple4d t1) { - super(t1); - } - - - /** - * Constructs and initializes a Point4f from the specified Tuple3f. - * The x,y,z components of this point are set to the corresponding - * components of tuple t1. The w component of this point - * is set to 1. - * - * @param t1 the tuple to be copied - * @since vecmath 1.2 - */ - public Point4f(Tuple3f t1) { - super(t1.x, t1.y, t1.z, 1.0f); - } - - - /** - * Constructs and initializes a Point4f to (0,0,0,0). - */ - public Point4f() { - super(); - } - - - /** - * Sets the x,y,z components of this point to the corresponding - * components of tuple t1. The w component of this point - * is set to 1. - * - * @param t1 the tuple to be copied - * @since vecmath 1.2 - */ - public final void set(Tuple3f t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - this.w = 1.0f; - } - - - /** - * Computes the square of the distance between this point and point p1. - * - * @param p1 the other point - * @return the square of distance between these two points as a float - */ - public final float distanceSquared(Point4f p1) { - float dx, dy, dz, dw; - - dx = this.x - p1.x; - dy = this.y - p1.y; - dz = this.z - p1.z; - dw = this.w - p1.w; - return (dx * dx + dy * dy + dz * dz + dw * dw); - } - - - /** - * Computes the distance between this point and point p1. - * - * @param p1 the other point - * @return the distance between the two points - */ - public final float distance(Point4f p1) { - float dx, dy, dz, dw; - - dx = this.x - p1.x; - dy = this.y - p1.y; - dz = this.z - p1.z; - dw = this.w - p1.w; - return (float) Math.sqrt(dx * dx + dy * dy + dz * dz + dw * dw); - } - - - /** - * Computes the L-1 (Manhattan) distance between this point and - * point p1. The L-1 distance is equal to: - * abs(x1-x2) + abs(y1-y2) + abs(z1-z2) + abs(w1-w2). - * - * @param p1 the other point - * @return the L-1 distance - */ - public final float distanceL1(Point4f p1) { - return (Math.abs(this.x - p1.x) + Math.abs(this.y - p1.y) + Math.abs(this.z - p1.z) + Math.abs(this.w - p1.w)); - } - - - /** - * Computes the L-infinite distance between this point and - * point p1. The L-infinite distance is equal to - * MAX[abs(x1-x2), abs(y1-y2), abs(z1-z2), abs(w1-w2)]. - * - * @param p1 the other point - * @return the L-infinite distance - */ - public final float distanceLinf(Point4f p1) { - float t1, t2; - t1 = Math.max(Math.abs(this.x - p1.x), Math.abs(this.y - p1.y)); - t2 = Math.max(Math.abs(this.z - p1.z), Math.abs(this.w - p1.w)); - - return (Math.max(t1, t2)); - - } - - /** - * Multiplies each of the x,y,z components of the Point4f parameter - * by 1/w, places the projected values into this point, and places - * a 1 as the w parameter of this point. - * - * @param p1 the source Point4f, which is not modified - */ - public final void project(Point4f p1) { - float oneOw; - - oneOw = 1 / p1.w; - x = p1.x * oneOw; - y = p1.y * oneOw; - z = p1.z * oneOw; - w = 1.0f; - - } - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Position2.java b/core/src/main/java/com/volmit/iris/util/math/Position2.java deleted file mode 100644 index 2175118ee..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Position2.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.engine.object.IrisPosition; -import org.bukkit.util.Vector; - -public class Position2 { - private int x; - private int z; - - public Position2(int x, int z) { - this.x = x; - this.z = z; - } - - public Position2(Vector center) { - this.x = center.getBlockX(); - this.z = center.getBlockZ(); - } - - public int getX() { - return x; - } - - public void setX(int x) { - this.x = x; - } - - public int getZ() { - return z; - } - - public void setZ(int z) { - this.z = z; - } - - public String toString() { - return "[" + x + "," + z + "]"; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + x; - result = prime * result + z; - return result; - } - - public Position2 regionToChunk() { - return new Position2(x << 5, z << 5); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof Position2 other)) { - return false; - } - return x == other.x && z == other.z; - } - - public double distance(Position2 center) { - return Math.pow(center.getX() - x, 2) + Math.pow(center.getZ() - z, 2); - } - - public Position2 add(int x, int z) { - return new Position2(this.x + x, this.z + z); - } - - public Position2 blockToChunk() { - return new Position2(x >> 4, z >> 4); - } - - public IrisPosition toIris() { - return new IrisPosition(x, 23, z); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/RNG.java b/core/src/main/java/com/volmit/iris/util/math/RNG.java deleted file mode 100644 index 0c735854a..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/RNG.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.util.documentation.Exclusive; -import com.volmit.iris.util.documentation.Inclusive; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Random; -import java.util.UUID; - -public class RNG extends Random { - public static final RNG r = new RNG(); - private static final char[] CHARGEN = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-=!@#$%^&*()_+`~[];',./<>?:\\\"{}|\\\\".toCharArray(); - private static final long serialVersionUID = 5222938581174415179L; - private final long sx; - - public RNG() { - super(); - sx = 0; - } - - public RNG(long seed) { - super(seed); - this.sx = seed; - } - - /** - * Creates a seed (long) from the hash of the seed string - * - * @param seed the seed (string) - */ - public RNG(String seed) { - this(UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).getLeastSignificantBits() + UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).getMostSignificantBits() + (seed.length() * 32564L)); - } - - public RNG nextParallelRNG(int signature) { - return new RNG(sx + signature); - } - - public RNG nextParallelRNG(long signature) { - return new RNG(sx + signature); - } - - public String s(int length) { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < length; i++) { - sb.append(c()); - } - - return sb.toString(); - } - - public char c() { - return CHARGEN[i(CHARGEN.length - 1)]; - } - - /** - * Pick a random enum - * - * @param t the enum class - * @return the enum - */ - public T e(Class t) { - T[] c = t.getEnumConstants(); - return c[i(c.length)]; - } - - public boolean b() { - return nextBoolean(); - } - - public boolean b(double percent) { - return d() > percent; - } - - public short si(@Inclusive int lowerBound, @Inclusive int upperBound) { - return (short) (lowerBound + (nextFloat() * ((upperBound - lowerBound) + 1))); - } - - public short si(@Inclusive int upperBound) { - return si(0, upperBound); - } - - public short si() { - return si(1); - } - - public float f(@Inclusive float lowerBound, @Exclusive float upperBound) { - return lowerBound + (nextFloat() * ((upperBound - lowerBound))); - } - - public float f(@Exclusive float upperBound) { - return f(0, upperBound); - } - - public float f() { - return f(1); - } - - public double d(@Inclusive double lowerBound, @Exclusive double upperBound) { - if (lowerBound > upperBound) { - return M.lerp(upperBound, lowerBound, nextDouble()); - } - - return M.lerp(lowerBound, upperBound, nextDouble()); - } - - public double d(@Exclusive double upperBound) { - return d(0, upperBound); - } - - public double d() { - return d(1); - } - - public int i(@Inclusive int lowerBound, @Exclusive int upperBound) { - return (int) Math.floor(d(lowerBound, upperBound)); - } - - public int i(@Exclusive int upperBound) { - return i(Math.min(upperBound, 0), Math.max(0, upperBound)); - } - - public long l(@Inclusive long lowerBound, @Exclusive long upperBound) { - return Math.round(d(lowerBound, upperBound)); - } - - public long l(@Exclusive long upperBound) { - return l(0, upperBound); - } - - public int imax() { - return i(Integer.MIN_VALUE, Integer.MAX_VALUE); - } - - public long lmax() { - return l(Long.MIN_VALUE, Long.MAX_VALUE); - } - - public float fmax() { - return f(Float.MIN_VALUE, Float.MAX_VALUE); - } - - public double dmax() { - return d(Double.MIN_VALUE, Double.MAX_VALUE); - } - - public short simax() { - return si(Short.MIN_VALUE, Short.MAX_VALUE); - } - - public boolean chance(double chance) { - return chance >= nextDouble(); - } - - public T pick(List pieces) { - if (pieces.isEmpty()) { - return null; - } - - if (pieces.size() == 1) { - return pieces.get(0); - } - - return pieces.get(nextInt(pieces.size())); - } - - public long getSeed() { - return sx; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/RNGV2.java b/core/src/main/java/com/volmit/iris/util/math/RNGV2.java deleted file mode 100644 index 2ded4af70..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/RNGV2.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.volmit.iris.util.math; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Random; -import java.util.UUID; - -import java.security.SecureRandom; -import java.util.List; -import java.util.UUID; -import java.nio.charset.StandardCharsets; - -public class RNGV2 extends SecureRandom { - private static final long serialVersionUID = 5222938581174415179L; - private static final char[] CHARGEN = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-=!@#$%^&*()_+`~[];',./<>?:\\\"{}|\\\\".toCharArray(); - private final long sx; - - // Constructor with no seed - public RNGV2() { - super(); - sx = 0; - } - - public RNGV2(long seed) { - super(); - this.setSeed(seed); - this.sx = seed; - } - - // Constructor with a string seed - public RNGV2(String seed) { - this(UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).getLeastSignificantBits() + - UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).getMostSignificantBits() + - (seed.length() * 32564L)); - } - - public RNGV2 nextParallelRNG(int signature) { - return new RNGV2(sx + signature); - } - - public RNGV2 nextParallelRNG(long signature) { - return new RNGV2(sx + signature); - } - - public String s(int length) { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < length; i++) { - sb.append(c()); - } - - return sb.toString(); - } - - public char c() { - return CHARGEN[i(CHARGEN.length - 1)]; - } - - // Pick a random enum - public T e(Class t) { - T[] c = t.getEnumConstants(); - return c[i(c.length)]; - } - - public boolean b() { - return nextBoolean(); - } - - public boolean b(double percent) { - return d() > percent; - } - - public short si(int lowerBound, int upperBound) { - return (short) (lowerBound + (nextFloat() * ((upperBound - lowerBound) + 1))); - } - - public short si(int upperBound) { - return si(0, upperBound); - } - - public short si() { - return si(1); - } - - public float f(float lowerBound, float upperBound) { - return lowerBound + (nextFloat() * ((upperBound - lowerBound))); - } - - public float f(float upperBound) { - return f(0, upperBound); - } - - public float f() { - return f(1); - } - - public double d(double lowerBound, double upperBound) { - return lowerBound + (nextDouble() * (upperBound - lowerBound)); - } - - public double d(double upperBound) { - return d(0, upperBound); - } - - public double d() { - return nextDouble(); - } - - public int i(int lowerBound, int upperBound) { - if (lowerBound >= upperBound) { - throw new IllegalArgumentException("Upper bound must be greater than lower bound"); - } - return lowerBound + this.nextInt(upperBound - lowerBound + 1); - } - - public int i(int upperBound) { - return i(0, upperBound); - } - - public long l(long lowerBound, long upperBound) { - return Math.round(d(lowerBound, upperBound)); - } - - public long l(long upperBound) { - return l(0, upperBound); - } - - public int imax() { - return i(Integer.MIN_VALUE, Integer.MAX_VALUE); - } - - public long lmax() { - return l(Long.MIN_VALUE, Long.MAX_VALUE); - } - - public float fmax() { - return f(Float.MIN_VALUE, Float.MAX_VALUE); - } - - public double dmax() { - return d(Double.MIN_VALUE, Double.MAX_VALUE); - } - - public short simax() { - return si(Short.MIN_VALUE, Short.MAX_VALUE); - } - - public boolean chance(double chance) { - return nextDouble() <= chance; - } - - public T pick(List pieces) { - if (pieces.isEmpty()) { - return null; - } - - if (pieces.size() == 1) { - return pieces.get(0); - } - - return pieces.get(this.nextInt(pieces.size())); - } - - public long getSeed() { - return sx; - } -} - diff --git a/core/src/main/java/com/volmit/iris/util/math/RollingSequence.java b/core/src/main/java/com/volmit/iris/util/math/RollingSequence.java deleted file mode 100644 index 777a51955..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/RollingSequence.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.util.collection.KList; - -public class RollingSequence extends Average { - private double median; - private double max; - private double min; - private boolean dirtyMedian; - private int dirtyExtremes; - private boolean precision; - - public RollingSequence(int size) { - super(size); - median = 0; - min = 0; - max = 0; - setPrecision(false); - } - - public double addLast(int amt) { - double f = 0; - - for (int i = 0; i < Math.min(values.length, amt); i++) { - f += values[i]; - } - - return f; - } - - public boolean isPrecision() { - return precision; - } - - public void setPrecision(boolean p) { - this.precision = p; - } - - public double getMin() { - if (dirtyExtremes > (isPrecision() ? 0 : values.length)) { - resetExtremes(); - } - - return min; - } - - public double getMax() { - if (dirtyExtremes > (isPrecision() ? 0 : values.length)) { - resetExtremes(); - } - - return max; - } - - public double getMedian() { - if (dirtyMedian) { - recalculateMedian(); - } - - return median; - } - - private void recalculateMedian() { - median = new KList().forceAdd(values).sort().middleValue(); - dirtyMedian = false; - } - - public void resetExtremes() { - max = Integer.MIN_VALUE; - min = Integer.MAX_VALUE; - - for (double i : values) { - max = M.max(max, i); - min = M.min(min, i); - } - - dirtyExtremes = 0; - } - - public void put(double i) { - super.put(i); - dirtyMedian = true; - dirtyExtremes++; - max = M.max(max, i); - min = M.min(min, i); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Spiral.java b/core/src/main/java/com/volmit/iris/util/math/Spiral.java deleted file mode 100644 index 04cb92a9f..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Spiral.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; - -import java.util.Iterator; - -/** - * Represents a spiraler which can start from any point within Long.MAX_VALUE by Long.MAX_VALUE and iterate anywhere. - */ -@Data -@RequiredArgsConstructor -public class Spiral implements Iterable { - private final Position2 start; - private final long max; - - @SuppressWarnings("ConstantConditions") - public static Position2 next(Position2 p) { - int x = p.getX(); - int z = p.getZ(); - int ax = Math.abs(x); - int az = Math.abs(z); - - if (x == 0 && z == 0) { - return p.add(1, 0); - } - - if (ax == az) { - if (x > 0 && z > 0) return left(p); - else if (x < 0 && z > 0) return down(p); - else if (x < 0 && z < 0) return right(p); - else if (x > 0 && z < 0) return up(p); - } else { - if (x > z && ax > az) return up(p); - else if (x < z && ax < az) return left(p); - else if (x < z && ax > az) return down(p); - else if (x > z && ax < az) return right(p); - } - - return p; - } - - public static Spiral from(Position2 p, long iterations) { - return new Spiral(p, iterations); - } - - private static Position2 down(Position2 p) { - return p.add(0, -1); - } - - private static Position2 up(Position2 p) { - return p.add(0, 1); - } - - private static Position2 left(Position2 p) { - return p.add(-1, 0); - } - - private static Position2 right(Position2 p) { - return p.add(1, 0); - } - - @NotNull - @Override - public Iterator iterator() { - return new SpiralIterator(this, 0, start); - } - - @AllArgsConstructor - static class SpiralIterator implements Iterator { - private final Spiral s; - private long itr = 0; - private Position2 cursor; - - @Override - public boolean hasNext() { - return itr < s.getMax(); - } - - @Override - public Position2 next() { - Position2 m = cursor; - cursor = Spiral.next(cursor); - itr++; - return m; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Spiraled.java b/core/src/main/java/com/volmit/iris/util/math/Spiraled.java deleted file mode 100644 index b9b6e8cff..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Spiraled.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -@FunctionalInterface -public interface Spiraled { - void on(int x, int z); -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Spiraler.java b/core/src/main/java/com/volmit/iris/util/math/Spiraler.java deleted file mode 100644 index f6f52f2bd..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Spiraler.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -@SuppressWarnings("EmptyMethod") -public class Spiraler { - private final Spiraled spiraled; - int x, z, dx, dz, sizeX, sizeZ, t, maxI, i; - int ox, oz; - - public Spiraler(int sizeX, int sizeZ, Spiraled spiraled) { - ox = 0; - oz = 0; - this.spiraled = spiraled; - retarget(sizeX, sizeZ); - } - - static void Spiral(int X, int Y) { - - } - - public void drain() { - while (hasNext()) { - next(); - } - } - - public Spiraler setOffset(int ox, int oz) { - this.ox = ox; - this.oz = oz; - return this; - } - - public void retarget(int sizeX, int sizeZ) { - this.sizeX = sizeX; - this.sizeZ = sizeZ; - x = z = dx = 0; - dz = -1; - i = 0; - t = Math.max(sizeX, sizeZ); - maxI = t * t; - } - - public boolean hasNext() { - return i < maxI; - } - - public void next() { - if ((-sizeX / 2 <= x) && (x <= sizeX / 2) && (-sizeZ / 2 <= z) && (z <= sizeZ / 2)) { - spiraled.on(x + ox, z + oz); - } - - if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) { - t = dx; - dx = -dz; - dz = t; - } - x += dx; - z += dz; - i++; - } - - public int count() { - int c = 0; - while (hasNext()) { - next(); - c++; - } - - return c; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Tuple2d.java b/core/src/main/java/com/volmit/iris/util/math/Tuple2d.java deleted file mode 100644 index e6b02d5c1..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Tuple2d.java +++ /dev/null @@ -1,562 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.Iris; - -/** - * A generic 2-element tuple that is represented by double-precision - * floating point x,y coordinates. - */ -public abstract class Tuple2d implements java.io.Serializable, Cloneable { - - static final long serialVersionUID = 6205762482756093838L; - - /** - * The x coordinate. - */ - public double x; - - /** - * The y coordinate. - */ - public double y; - - - /** - * Constructs and initializes a Tuple2d from the specified xy coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - */ - public Tuple2d(double x, double y) { - this.x = x; - this.y = y; - } - - - /** - * Constructs and initializes a Tuple2d from the specified array. - * - * @param t the array of length 2 containing xy in order - */ - public Tuple2d(double[] t) { - this.x = t[0]; - this.y = t[1]; - } - - - /** - * Constructs and initializes a Tuple2d from the specified Tuple2d. - * - * @param t1 the Tuple2d containing the initialization x y data - */ - public Tuple2d(Tuple2d t1) { - this.x = t1.x; - this.y = t1.y; - } - - - /** - * Constructs and initializes a Tuple2d from the specified Tuple2f. - * - * @param t1 the Tuple2f containing the initialization x y data - */ - public Tuple2d(Tuple2f t1) { - this.x = t1.x; - this.y = t1.y; - } - - /** - * Constructs and initializes a Tuple2d to (0,0). - */ - public Tuple2d() { - this.x = 0.0; - this.y = 0.0; - } - - - /** - * Sets the value of this tuple to the specified xy coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - */ - public final void set(double x, double y) { - this.x = x; - this.y = y; - } - - - /** - * Sets the value of this tuple from the 2 values specified in - * the array. - * - * @param t the array of length 2 containing xy in order - */ - public final void set(double[] t) { - this.x = t[0]; - this.y = t[1]; - } - - - /** - * Sets the value of this tuple to the value of the Tuple2d argument. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple2d t1) { - this.x = t1.x; - this.y = t1.y; - } - - - /** - * Sets the value of this tuple to the value of Tuple2f t1. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple2f t1) { - this.x = t1.x; - this.y = t1.y; - } - - /** - * Copies the value of the elements of this tuple into the array t. - * - * @param t the array that will contain the values of the vector - */ - public final void get(double[] t) { - t[0] = this.x; - t[1] = this.y; - } - - - /** - * Sets the value of this tuple to the vector sum of tuples t1 and t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void add(Tuple2d t1, Tuple2d t2) { - this.x = t1.x + t2.x; - this.y = t1.y + t2.y; - } - - - /** - * Sets the value of this tuple to the vector sum of itself and tuple t1. - * - * @param t1 the other tuple - */ - public final void add(Tuple2d t1) { - this.x += t1.x; - this.y += t1.y; - } - - - /** - * Sets the value of this tuple to the vector difference of - * tuple t1 and t2 (this = t1 - t2). - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void sub(Tuple2d t1, Tuple2d t2) { - this.x = t1.x - t2.x; - this.y = t1.y - t2.y; - } - - - /** - * Sets the value of this tuple to the vector difference of - * itself and tuple t1 (this = this - t1). - * - * @param t1 the other vector - */ - public final void sub(Tuple2d t1) { - this.x -= t1.x; - this.y -= t1.y; - } - - - /** - * Sets the value of this tuple to the negation of tuple t1. - * - * @param t1 the source vector - */ - public final void negate(Tuple2d t1) { - this.x = -t1.x; - this.y = -t1.y; - } - - - /** - * Negates the value of this vector in place. - */ - public final void negate() { - this.x = -this.x; - this.y = -this.y; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of tuple t1. - * - * @param s the scalar value - * @param t1 the source tuple - */ - public final void scale(double s, Tuple2d t1) { - this.x = s * t1.x; - this.y = s * t1.y; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of itself. - * - * @param s the scalar value - */ - public final void scale(double s) { - this.x *= s; - this.y *= s; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of tuple t1 and then adds tuple t2 (this = s*t1 + t2). - * - * @param s the scalar value - * @param t1 the tuple to be multipled - * @param t2 the tuple to be added - */ - public final void scaleAdd(double s, Tuple2d t1, Tuple2d t2) { - this.x = s * t1.x + t2.x; - this.y = s * t1.y + t2.y; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of itself and then adds tuple t1 (this = s*this + t1). - * - * @param s the scalar value - * @param t1 the tuple to be added - */ - public final void scaleAdd(double s, Tuple2d t1) { - this.x = s * this.x + t1.x; - this.y = s * this.y + t1.y; - } - - - /** - * Returns a hash code value based on the data values in this - * object. Two different Tuple2d objects with identical data values - * (i.e., Tuple2d.equals returns true) will return the same hash - * code value. Two objects with different data members may return the - * same hash value, although this is not likely. - * - * @return the integer hash code value - */ - public int hashCode() { - long bits = 1L; - bits = 31L * bits + VecMathUtil.doubleToLongBits(x); - bits = 31L * bits + VecMathUtil.doubleToLongBits(y); - return (int) (bits ^ (bits >> 32)); - } - - - /** - * Returns true if all of the data members of Tuple2d t1 are - * equal to the corresponding data members in this Tuple2d. - * - * @param t1 the vector with which the comparison is made - * @return true or false - */ - public boolean equals(Tuple2d t1) { - try { - return (this.x == t1.x && this.y == t1.y); - } catch (NullPointerException e2) { - Iris.reportError(e2); - return false; - } - - } - - /** - * Returns true if the Object t1 is of type Tuple2d and all of the - * data members of t1 are equal to the corresponding data members in - * this Tuple2d. - * - * @param t1 the object with which the comparison is made - * @return true or false - */ - public boolean equals(Object t1) { - try { - Tuple2d t2 = (Tuple2d) t1; - return (this.x == t2.x && this.y == t2.y); - } catch (NullPointerException | ClassCastException e2) { - Iris.reportError(e2); - return false; - } - - } - - /** - * Returns true if the L-infinite distance between this tuple - * and tuple t1 is less than or equal to the epsilon parameter, - * otherwise returns false. The L-infinite - * distance is equal to MAX[abs(x1-x2), abs(y1-y2)]. - * - * @param t1 the tuple to be compared to this tuple - * @param epsilon the threshold value - * @return true or false - */ - public boolean epsilonEquals(Tuple2d t1, double epsilon) { - double diff; - - diff = x - t1.x; - if (Double.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = y - t1.y; - if (Double.isNaN(diff)) return false; - return !((diff < 0 ? -diff : diff) > epsilon); - } - - /** - * Returns a string that contains the values of this Tuple2d. - * The form is (x,y). - * - * @return the String representation - */ - public String toString() { - return ("(" + this.x + ", " + this.y + ")"); - } - - - /** - * Clamps the tuple parameter to the range [low, high] and - * places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clamp(double min, double max, Tuple2d t) { - if (t.x > max) { - x = max; - } else x = Math.max(t.x, min); - - if (t.y > max) { - y = max; - } else y = Math.max(t.y, min); - - } - - - /** - * Clamps the minimum value of the tuple parameter to the min - * parameter and places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMin(double min, Tuple2d t) { - x = Math.max(t.x, min); - - y = Math.max(t.y, min); - - } - - - /** - * Clamps the maximum value of the tuple parameter to the max - * parameter and places the values into this tuple. - * - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMax(double max, Tuple2d t) { - x = Math.min(t.x, max); - - y = Math.min(t.y, max); - - } - - - /** - * Sets each component of the tuple parameter to its absolute - * value and places the modified values into this tuple. - * - * @param t the source tuple, which will not be modified - */ - public final void absolute(Tuple2d t) { - x = Math.abs(t.x); - y = Math.abs(t.y); - } - - - /** - * Clamps this tuple to the range [low, high]. - * - * @param min the lowest value in this tuple after clamping - * @param max the highest value in this tuple after clamping - */ - public final void clamp(double min, double max) { - if (x > max) { - x = max; - } else if (x < min) { - x = min; - } - - if (y > max) { - y = max; - } else if (y < min) { - y = min; - } - - } - - - /** - * Clamps the minimum value of this tuple to the min parameter. - * - * @param min the lowest value in this tuple after clamping - */ - public final void clampMin(double min) { - if (x < min) x = min; - if (y < min) y = min; - } - - - /** - * Clamps the maximum value of this tuple to the max parameter. - * - * @param max the highest value in the tuple after clamping - */ - public final void clampMax(double max) { - if (x > max) x = max; - if (y > max) y = max; - } - - - /** - * Sets each component of this tuple to its absolute value. - */ - public final void absolute() { - x = Math.abs(x); - y = Math.abs(y); - } - - - /** - * Linearly interpolates between tuples t1 and t2 and places the - * result into this tuple: this = (1-alpha)*t1 + alpha*t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - * @param alpha the alpha interpolation parameter - */ - public final void interpolate(Tuple2d t1, Tuple2d t2, double alpha) { - this.x = (1 - alpha) * t1.x + alpha * t2.x; - this.y = (1 - alpha) * t1.y + alpha * t2.y; - } - - - /** - * Linearly interpolates between this tuple and tuple t1 and - * places the result into this tuple: this = (1-alpha)*this + alpha*t1. - * - * @param t1 the first tuple - * @param alpha the alpha interpolation parameter - */ - public final void interpolate(Tuple2d t1, double alpha) { - this.x = (1 - alpha) * this.x + alpha * t1.x; - this.y = (1 - alpha) * this.y + alpha * t1.y; - - } - - /** - * Creates a new object of the same class as this object. - * - * @return a clone of this instance. - * @throws OutOfMemoryError if there is not enough memory. - * @see java.lang.Cloneable - * @since vecmath 1.3 - */ - public Object clone() { - // Since there are no arrays we can just use Object.clone() - try { - return super.clone(); - } catch (CloneNotSupportedException e) { - Iris.reportError(e); - // this shouldn't happen, since we are Cloneable - throw new InternalError(); - } - } - - - /** - * Get the x coordinate. - * - * @return the x coordinate. - * @since vecmath 1.5 - */ - public final double getX() { - return x; - } - - - /** - * Set the x coordinate. - * - * @param x value to x coordinate. - * @since vecmath 1.5 - */ - public final void setX(double x) { - this.x = x; - } - - - /** - * Get the y coordinate. - * - * @return the y coordinate. - * @since vecmath 1.5 - */ - public final double getY() { - return y; - } - - - /** - * Set the y coordinate. - * - * @param y value to y coordinate. - * @since vecmath 1.5 - */ - public final void setY(double y) { - this.y = y; - } - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Tuple2f.java b/core/src/main/java/com/volmit/iris/util/math/Tuple2f.java deleted file mode 100644 index 1fd2da63d..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Tuple2f.java +++ /dev/null @@ -1,565 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.Iris; - -/** - * A generic 2-element tuple that is represented by single-precision - * floating point x,y coordinates. - */ -public abstract class Tuple2f implements java.io.Serializable, Cloneable { - - static final long serialVersionUID = 9011180388985266884L; - - /** - * The x coordinate. - */ - public float x; - - /** - * The y coordinate. - */ - public float y; - - - /** - * Constructs and initializes a Tuple2f from the specified xy coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - */ - public Tuple2f(float x, float y) { - this.x = x; - this.y = y; - } - - - /** - * Constructs and initializes a Tuple2f from the specified array. - * - * @param t the array of length 2 containing xy in order - */ - public Tuple2f(float[] t) { - this.x = t[0]; - this.y = t[1]; - } - - - /** - * Constructs and initializes a Tuple2f from the specified Tuple2f. - * - * @param t1 the Tuple2f containing the initialization x y data - */ - public Tuple2f(Tuple2f t1) { - this.x = t1.x; - this.y = t1.y; - } - - - /** - * Constructs and initializes a Tuple2f from the specified Tuple2d. - * - * @param t1 the Tuple2d containing the initialization x y data - */ - public Tuple2f(Tuple2d t1) { - this.x = (float) t1.x; - this.y = (float) t1.y; - } - - - /** - * Constructs and initializes a Tuple2f to (0,0). - */ - public Tuple2f() { - this.x = (float) 0.0; - this.y = (float) 0.0; - } - - - /** - * Sets the value of this tuple to the specified xy coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - */ - public final void set(float x, float y) { - this.x = x; - this.y = y; - } - - - /** - * Sets the value of this tuple from the 2 values specified in - * the array. - * - * @param t the array of length 2 containing xy in order - */ - public final void set(float[] t) { - this.x = t[0]; - this.y = t[1]; - } - - - /** - * Sets the value of this tuple to the value of the Tuple2f argument. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple2f t1) { - this.x = t1.x; - this.y = t1.y; - } - - - /** - * Sets the value of this tuple to the value of the Tuple2d argument. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple2d t1) { - this.x = (float) t1.x; - this.y = (float) t1.y; - } - - - /** - * Copies the value of the elements of this tuple into the array t. - * - * @param t the array that will contain the values of the vector - */ - public final void get(float[] t) { - t[0] = this.x; - t[1] = this.y; - } - - - /** - * Sets the value of this tuple to the vector sum of tuples t1 and t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void add(Tuple2f t1, Tuple2f t2) { - this.x = t1.x + t2.x; - this.y = t1.y + t2.y; - } - - - /** - * Sets the value of this tuple to the vector sum of itself and tuple t1. - * - * @param t1 the other tuple - */ - public final void add(Tuple2f t1) { - this.x += t1.x; - this.y += t1.y; - } - - - /** - * Sets the value of this tuple to the vector difference of - * tuple t1 and t2 (this = t1 - t2). - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void sub(Tuple2f t1, Tuple2f t2) { - this.x = t1.x - t2.x; - this.y = t1.y - t2.y; - } - - - /** - * Sets the value of this tuple to the vector difference of - * itself and tuple t1 (this = this - t1). - * - * @param t1 the other tuple - */ - public final void sub(Tuple2f t1) { - this.x -= t1.x; - this.y -= t1.y; - } - - - /** - * Sets the value of this tuple to the negation of tuple t1. - * - * @param t1 the source tuple - */ - public final void negate(Tuple2f t1) { - this.x = -t1.x; - this.y = -t1.y; - } - - - /** - * Negates the value of this vector in place. - */ - public final void negate() { - this.x = -this.x; - this.y = -this.y; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of tuple t1. - * - * @param s the scalar value - * @param t1 the source tuple - */ - public final void scale(float s, Tuple2f t1) { - this.x = s * t1.x; - this.y = s * t1.y; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of itself. - * - * @param s the scalar value - */ - public final void scale(float s) { - this.x *= s; - this.y *= s; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of tuple t1 and then adds tuple t2 (this = s*t1 + t2). - * - * @param s the scalar value - * @param t1 the tuple to be multipled - * @param t2 the tuple to be added - */ - public final void scaleAdd(float s, Tuple2f t1, Tuple2f t2) { - this.x = s * t1.x + t2.x; - this.y = s * t1.y + t2.y; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of itself and then adds tuple t1 (this = s*this + t1). - * - * @param s the scalar value - * @param t1 the tuple to be added - */ - public final void scaleAdd(float s, Tuple2f t1) { - this.x = s * this.x + t1.x; - this.y = s * this.y + t1.y; - } - - - /** - * Returns a hash code value based on the data values in this - * object. Two different Tuple2f objects with identical data values - * (i.e., Tuple2f.equals returns true) will return the same hash - * code value. Two objects with different data members may return the - * same hash value, although this is not likely. - * - * @return the integer hash code value - */ - public int hashCode() { - long bits = 1L; - bits = 31L * bits + (long) VecMathUtil.floatToIntBits(x); - bits = 31L * bits + (long) VecMathUtil.floatToIntBits(y); - return (int) (bits ^ (bits >> 32)); - } - - - /** - * Returns true if all of the data members of Tuple2f t1 are - * equal to the corresponding data members in this Tuple2f. - * - * @param t1 the vector with which the comparison is made - * @return true or false - */ - public boolean equals(Tuple2f t1) { - try { - return (this.x == t1.x && this.y == t1.y); - } catch (NullPointerException e2) { - Iris.reportError(e2); - return false; - } - - } - - /** - * Returns true if the Object t1 is of type Tuple2f and all of the - * data members of t1 are equal to the corresponding data members in - * this Tuple2f. - * - * @param t1 the object with which the comparison is made - * @return true or false - */ - public boolean equals(Object t1) { - try { - Tuple2f t2 = (Tuple2f) t1; - return (this.x == t2.x && this.y == t2.y); - } catch (NullPointerException | ClassCastException e2) { - Iris.reportError(e2); - return false; - } - - } - - /** - * Returns true if the L-infinite distance between this tuple - * and tuple t1 is less than or equal to the epsilon parameter, - * otherwise returns false. The L-infinite - * distance is equal to MAX[abs(x1-x2), abs(y1-y2)]. - * - * @param t1 the tuple to be compared to this tuple - * @param epsilon the threshold value - * @return true or false - */ - public boolean epsilonEquals(Tuple2f t1, float epsilon) { - float diff; - - diff = x - t1.x; - if (Float.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = y - t1.y; - if (Float.isNaN(diff)) return false; - return !((diff < 0 ? -diff : diff) > epsilon); - } - - /** - * Returns a string that contains the values of this Tuple2f. - * The form is (x,y). - * - * @return the String representation - */ - public String toString() { - return ("(" + this.x + ", " + this.y + ")"); - } - - - /** - * Clamps the tuple parameter to the range [low, high] and - * places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clamp(float min, float max, Tuple2f t) { - if (t.x > max) { - x = max; - } else x = Math.max(t.x, min); - - if (t.y > max) { - y = max; - } else y = Math.max(t.y, min); - - } - - - /** - * Clamps the minimum value of the tuple parameter to the min - * parameter and places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMin(float min, Tuple2f t) { - x = Math.max(t.x, min); - - y = Math.max(t.y, min); - - } - - - /** - * Clamps the maximum value of the tuple parameter to the max - * parameter and places the values into this tuple. - * - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMax(float max, Tuple2f t) { - x = Math.min(t.x, max); - - y = Math.min(t.y, max); - - } - - - /** - * Sets each component of the tuple parameter to its absolute - * value and places the modified values into this tuple. - * - * @param t the source tuple, which will not be modified - */ - public final void absolute(Tuple2f t) { - x = Math.abs(t.x); - y = Math.abs(t.y); - } - - - /** - * Clamps this tuple to the range [low, high]. - * - * @param min the lowest value in this tuple after clamping - * @param max the highest value in this tuple after clamping - */ - public final void clamp(float min, float max) { - if (x > max) { - x = max; - } else if (x < min) { - x = min; - } - - if (y > max) { - y = max; - } else if (y < min) { - y = min; - } - - } - - - /** - * Clamps the minimum value of this tuple to the min parameter. - * - * @param min the lowest value in this tuple after clamping - */ - public final void clampMin(float min) { - if (x < min) x = min; - if (y < min) y = min; - } - - - /** - * Clamps the maximum value of this tuple to the max parameter. - * - * @param max the highest value in the tuple after clamping - */ - public final void clampMax(float max) { - if (x > max) x = max; - if (y > max) y = max; - } - - - /** - * Sets each component of this tuple to its absolute value. - */ - public final void absolute() { - x = Math.abs(x); - y = Math.abs(y); - } - - - /** - * Linearly interpolates between tuples t1 and t2 and places the - * result into this tuple: this = (1-alpha)*t1 + alpha*t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - * @param alpha the alpha interpolation parameter - */ - public final void interpolate(Tuple2f t1, Tuple2f t2, float alpha) { - this.x = (1 - alpha) * t1.x + alpha * t2.x; - this.y = (1 - alpha) * t1.y + alpha * t2.y; - - } - - - /** - * Linearly interpolates between this tuple and tuple t1 and - * places the result into this tuple: this = (1-alpha)*this + alpha*t1. - * - * @param t1 the first tuple - * @param alpha the alpha interpolation parameter - */ - public final void interpolate(Tuple2f t1, float alpha) { - - this.x = (1 - alpha) * this.x + alpha * t1.x; - this.y = (1 - alpha) * this.y + alpha * t1.y; - - } - - /** - * Creates a new object of the same class as this object. - * - * @return a clone of this instance. - * @throws OutOfMemoryError if there is not enough memory. - * @see java.lang.Cloneable - * @since vecmath 1.3 - */ - public Object clone() { - // Since there are no arrays we can just use Object.clone() - try { - return super.clone(); - } catch (CloneNotSupportedException e) { - Iris.reportError(e); - // this shouldn't happen, since we are Cloneable - throw new InternalError(); - } - } - - - /** - * Get the x coordinate. - * - * @return the x coordinate. - * @since vecmath 1.5 - */ - public final float getX() { - return x; - } - - - /** - * Set the x coordinate. - * - * @param x value to x coordinate. - * @since vecmath 1.5 - */ - public final void setX(float x) { - this.x = x; - } - - - /** - * Get the y coordinate. - * - * @return the y coordinate. - * @since vecmath 1.5 - */ - public final float getY() { - return y; - } - - - /** - * Set the y coordinate. - * - * @param y value to y coordinate. - * @since vecmath 1.5 - */ - public final void setY(float y) { - this.y = y; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Tuple3d.java b/core/src/main/java/com/volmit/iris/util/math/Tuple3d.java deleted file mode 100644 index ea4415335..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Tuple3d.java +++ /dev/null @@ -1,719 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.Iris; - -/** - * A generic 3-element tuple that is represented by double-precision - * floating point x,y,z coordinates. - */ -public abstract class Tuple3d implements java.io.Serializable, Cloneable { - - static final long serialVersionUID = 5542096614926168415L; - - /** - * The x coordinate. - */ - public double x; - - /** - * The y coordinate. - */ - public double y; - - /** - * The z coordinate. - */ - public double z; - - - /** - * Constructs and initializes a Tuple3d from the specified xyz coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - */ - public Tuple3d(double x, double y, double z) { - this.x = x; - this.y = y; - this.z = z; - } - - /** - * Constructs and initializes a Tuple3d from the array of length 3. - * - * @param t the array of length 3 containing xyz in order - */ - public Tuple3d(double[] t) { - this.x = t[0]; - this.y = t[1]; - this.z = t[2]; - } - - /** - * Constructs and initializes a Tuple3d from the specified Tuple3d. - * - * @param t1 the Tuple3d containing the initialization x y z data - */ - public Tuple3d(Tuple3d t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - } - - /** - * Constructs and initializes a Tuple3d from the specified Tuple3f. - * - * @param t1 the Tuple3f containing the initialization x y z data - */ - public Tuple3d(Tuple3f t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - } - - /** - * Constructs and initializes a Tuple3d to (0,0,0). - */ - public Tuple3d() { - this.x = 0.0; - this.y = 0.0; - this.z = 0.0; - } - - /** - * Sets the value of this tuple to the specified xyz coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - */ - public final void set(double x, double y, double z) { - this.x = x; - this.y = y; - this.z = z; - } - - /** - * Sets the value of this tuple to the value of the xyz coordinates - * located in the array of length 3. - * - * @param t the array of length 3 containing xyz in order - */ - public final void set(double[] t) { - this.x = t[0]; - this.y = t[1]; - this.z = t[2]; - } - - /** - * Sets the value of this tuple to the value of tuple t1. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple3d t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - } - - /** - * Sets the value of this tuple to the value of tuple t1. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple3f t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - } - - /** - * Copies the x,y,z coordinates of this tuple into the array t - * of length 3. - * - * @param t the target array - */ - public final void get(double[] t) { - t[0] = this.x; - t[1] = this.y; - t[2] = this.z; - } - - - /** - * Copies the x,y,z coordinates of this tuple into the tuple t. - * - * @param t the Tuple3d object into which the values of this object are copied - */ - public final void get(Tuple3d t) { - t.x = this.x; - t.y = this.y; - t.z = this.z; - } - - - /** - * Sets the value of this tuple to the sum of tuples t1 and t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void add(Tuple3d t1, Tuple3d t2) { - this.x = t1.x + t2.x; - this.y = t1.y + t2.y; - this.z = t1.z + t2.z; - } - - - /** - * Sets the value of this tuple to the sum of itself and t1. - * - * @param t1 the other tuple - */ - public final void add(Tuple3d t1) { - this.x += t1.x; - this.y += t1.y; - this.z += t1.z; - } - - /** - * Sets the value of this tuple to the difference of tuples - * t1 and t2 (this = t1 - t2). - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void sub(Tuple3d t1, Tuple3d t2) { - this.x = t1.x - t2.x; - this.y = t1.y - t2.y; - this.z = t1.z - t2.z; - } - - /** - * Sets the value of this tuple to the difference - * of itself and t1 (this = this - t1). - * - * @param t1 the other tuple - */ - public final void sub(Tuple3d t1) { - this.x -= t1.x; - this.y -= t1.y; - this.z -= t1.z; - } - - - /** - * Sets the value of this tuple to the negation of tuple t1. - * - * @param t1 the source tuple - */ - public final void negate(Tuple3d t1) { - this.x = -t1.x; - this.y = -t1.y; - this.z = -t1.z; - } - - - /** - * Negates the value of this tuple in place. - */ - public final void negate() { - this.x = -this.x; - this.y = -this.y; - this.z = -this.z; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of tuple t1. - * - * @param s the scalar value - * @param t1 the source tuple - */ - public final void scale(double s, Tuple3d t1) { - this.x = s * t1.x; - this.y = s * t1.y; - this.z = s * t1.z; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of itself. - * - * @param s the scalar value - */ - public final void scale(double s) { - this.x *= s; - this.y *= s; - this.z *= s; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of tuple t1 and then adds tuple t2 (this = s*t1 + t2). - * - * @param s the scalar value - * @param t1 the tuple to be multipled - * @param t2 the tuple to be added - */ - public final void scaleAdd(double s, Tuple3d t1, Tuple3d t2) { - this.x = s * t1.x + t2.x; - this.y = s * t1.y + t2.y; - this.z = s * t1.z + t2.z; - } - - - /** - * @deprecated Use scaleAdd(double,Tuple3d) instead - */ - @Deprecated - public final void scaleAdd(double s, Tuple3f t1) { - scaleAdd(s, new Point3d(t1)); - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of itself and then adds tuple t1 (this = s*this + t1). - * - * @param s the scalar value - * @param t1 the tuple to be added - */ - public final void scaleAdd(double s, Tuple3d t1) { - this.x = s * this.x + t1.x; - this.y = s * this.y + t1.y; - this.z = s * this.z + t1.z; - } - - - /** - * Returns a string that contains the values of this Tuple3d. - * The form is (x,y,z). - * - * @return the String representation - */ - public String toString() { - return "(" + this.x + ", " + this.y + ", " + this.z + ")"; - } - - - /** - * Returns a hash code value based on the data values in this - * object. Two different Tuple3d objects with identical data values - * (i.e., Tuple3d.equals returns true) will return the same hash - * code value. Two objects with different data members may return the - * same hash value, although this is not likely. - * - * @return the integer hash code value - */ - public int hashCode() { - long bits = 1L; - bits = 31L * bits + VecMathUtil.doubleToLongBits(x); - bits = 31L * bits + VecMathUtil.doubleToLongBits(y); - bits = 31L * bits + VecMathUtil.doubleToLongBits(z); - return (int) (bits ^ (bits >> 32)); - } - - - /** - * Returns true if all of the data members of Tuple3d t1 are - * equal to the corresponding data members in this Tuple3d. - * - * @param t1 the tuple with which the comparison is made - * @return true or false - */ - public boolean equals(Tuple3d t1) { - try { - return (this.x == t1.x && this.y == t1.y && this.z == t1.z); - } catch (NullPointerException e2) { - Iris.reportError(e2); - return false; - } - } - - /** - * Returns true if the Object t1 is of type Tuple3d and all of the - * data members of t1 are equal to the corresponding data members in - * this Tuple3d. - * - * @param t1 the Object with which the comparison is made - * @return true or false - */ - public boolean equals(Object t1) { - try { - Tuple3d t2 = (Tuple3d) t1; - return (this.x == t2.x && this.y == t2.y && this.z == t2.z); - } catch (ClassCastException | NullPointerException e1) { - Iris.reportError(e1); - return false; - } - - } - - /** - * Returns true if the L-infinite distance between this tuple - * and tuple t1 is less than or equal to the epsilon parameter, - * otherwise returns false. The L-infinite - * distance is equal to MAX[abs(x1-x2), abs(y1-y2), abs(z1-z2)]. - * - * @param t1 the tuple to be compared to this tuple - * @param epsilon the threshold value - * @return true or false - */ - public boolean epsilonEquals(Tuple3d t1, double epsilon) { - double diff; - - diff = x - t1.x; - if (Double.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = y - t1.y; - if (Double.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = z - t1.z; - if (Double.isNaN(diff)) return false; - return !((diff < 0 ? -diff : diff) > epsilon); - - } - - - /** - * @deprecated Use clamp(double,double,Tuple3d) instead - */ - @Deprecated - public final void clamp(float min, float max, Tuple3d t) { - clamp(min, (double) max, t); - } - - - /** - * Clamps the tuple parameter to the range [low, high] and - * places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clamp(double min, double max, Tuple3d t) { - if (t.x > max) { - x = max; - } else x = Math.max(t.x, min); - - if (t.y > max) { - y = max; - } else y = Math.max(t.y, min); - - if (t.z > max) { - z = max; - } else z = Math.max(t.z, min); - - } - - - /** - * @deprecated Use clampMin(double,Tuple3d) instead - */ - @Deprecated - public final void clampMin(float min, Tuple3d t) { - clampMin((double) min, t); - } - - - /** - * Clamps the minimum value of the tuple parameter to the min - * parameter and places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMin(double min, Tuple3d t) { - x = Math.max(t.x, min); - - y = Math.max(t.y, min); - - z = Math.max(t.z, min); - - } - - - /** - * @deprecated Use clampMax(double,Tuple3d) instead - */ - @Deprecated - public final void clampMax(float max, Tuple3d t) { - clampMax((double) max, t); - } - - - /** - * Clamps the maximum value of the tuple parameter to the max - * parameter and places the values into this tuple. - * - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMax(double max, Tuple3d t) { - x = Math.min(t.x, max); - - y = Math.min(t.y, max); - - z = Math.min(t.z, max); - - } - - - /** - * Sets each component of the tuple parameter to its absolute - * value and places the modified values into this tuple. - * - * @param t the source tuple, which will not be modified - */ - public final void absolute(Tuple3d t) { - x = Math.abs(t.x); - y = Math.abs(t.y); - z = Math.abs(t.z); - - } - - - /** - * @deprecated Use clamp(double,double) instead - */ - @Deprecated - public final void clamp(float min, float max) { - clamp(min, (double) max); - } - - - /** - * Clamps this tuple to the range [low, high]. - * - * @param min the lowest value in this tuple after clamping - * @param max the highest value in this tuple after clamping - */ - public final void clamp(double min, double max) { - if (x > max) { - x = max; - } else if (x < min) { - x = min; - } - - if (y > max) { - y = max; - } else if (y < min) { - y = min; - } - - if (z > max) { - z = max; - } else if (z < min) { - z = min; - } - - } - - - /** - * @deprecated Use clampMin(double) instead - */ - @Deprecated - public final void clampMin(float min) { - clampMin((double) min); - } - - - /** - * Clamps the minimum value of this tuple to the min parameter. - * - * @param min the lowest value in this tuple after clamping - */ - public final void clampMin(double min) { - if (x < min) x = min; - if (y < min) y = min; - if (z < min) z = min; - } - - - /** - * @deprecated Use clampMax(double) instead - */ - @Deprecated - public final void clampMax(float max) { - clampMax((double) max); - } - - - /** - * Clamps the maximum value of this tuple to the max parameter. - * - * @param max the highest value in the tuple after clamping - */ - public final void clampMax(double max) { - if (x > max) x = max; - if (y > max) y = max; - if (z > max) z = max; - } - - - /** - * Sets each component of this tuple to its absolute value. - */ - public final void absolute() { - x = Math.abs(x); - y = Math.abs(y); - z = Math.abs(z); - } - - - /** - * @deprecated Use interpolate(Tuple3d,Tuple3d,double) instead - */ - @Deprecated - public final void interpolate(Tuple3d t1, Tuple3d t2, float alpha) { - interpolate(t1, t2, (double) alpha); - } - - - /** - * Linearly interpolates between tuples t1 and t2 and places the - * result into this tuple: this = (1-alpha)*t1 + alpha*t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - * @param alpha the alpha interpolation parameter - */ - public final void interpolate(Tuple3d t1, Tuple3d t2, double alpha) { - this.x = (1 - alpha) * t1.x + alpha * t2.x; - this.y = (1 - alpha) * t1.y + alpha * t2.y; - this.z = (1 - alpha) * t1.z + alpha * t2.z; - } - - - /** - * @deprecated Use interpolate(Tuple3d,double) instead - */ - @Deprecated - public final void interpolate(Tuple3d t1, float alpha) { - interpolate(t1, (double) alpha); - } - - - /** - * Linearly interpolates between this tuple and tuple t1 and - * places the result into this tuple: this = (1-alpha)*this + alpha*t1. - * - * @param t1 the first tuple - * @param alpha the alpha interpolation parameter - */ - public final void interpolate(Tuple3d t1, double alpha) { - this.x = (1 - alpha) * this.x + alpha * t1.x; - this.y = (1 - alpha) * this.y + alpha * t1.y; - this.z = (1 - alpha) * this.z + alpha * t1.z; - } - - /** - * Creates a new object of the same class as this object. - * - * @return a clone of this instance. - * @throws OutOfMemoryError if there is not enough memory. - * @see java.lang.Cloneable - * @since vecmath 1.3 - */ - public Object clone() { - // Since there are no arrays we can just use Object.clone() - try { - return super.clone(); - } catch (CloneNotSupportedException e) { - Iris.reportError(e); - // this shouldn't happen, since we are Cloneable - throw new InternalError(); - } - } - - /** - * Get the x coordinate. - * - * @return the x coordinate. - * @since vecmath 1.5 - */ - public final double getX() { - return x; - } - - - /** - * Set the x coordinate. - * - * @param x value to x coordinate. - * @since vecmath 1.5 - */ - public final void setX(double x) { - this.x = x; - } - - - /** - * Get the y coordinate. - * - * @return the y coordinate. - * @since vecmath 1.5 - */ - public final double getY() { - return y; - } - - - /** - * Set the y coordinate. - * - * @param y value to y coordinate. - * @since vecmath 1.5 - */ - public final void setY(double y) { - this.y = y; - } - - /** - * Get the z coordinate. - * - * @return the z coordinate. - * @since vecmath 1.5 - */ - public final double getZ() { - return z; - } - - - /** - * Set the z coordinate. - * - * @param z value to z coordinate. - * @since vecmath 1.5 - */ - public final void setZ(double z) { - this.z = z; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Tuple3f.java b/core/src/main/java/com/volmit/iris/util/math/Tuple3f.java deleted file mode 100644 index 51c2d0fe4..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Tuple3f.java +++ /dev/null @@ -1,656 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.Iris; - -/** - * A generic 3-element tuple that is represented by single precision-floating - * point x,y,z coordinates. - */ -public abstract class Tuple3f implements java.io.Serializable, Cloneable { - - static final long serialVersionUID = 5019834619484343712L; - - /** - * The x coordinate. - */ - public float x; - - /** - * The y coordinate. - */ - public float y; - - /** - * The z coordinate. - */ - public float z; - - - /** - * Constructs and initializes a Tuple3f from the specified xyz coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - */ - public Tuple3f(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - } - - - /** - * Constructs and initializes a Tuple3f from the array of length 3. - * - * @param t the array of length 3 containing xyz in order - */ - public Tuple3f(float[] t) { - this.x = t[0]; - this.y = t[1]; - this.z = t[2]; - } - - - /** - * Constructs and initializes a Tuple3f from the specified Tuple3f. - * - * @param t1 the Tuple3f containing the initialization x y z data - */ - public Tuple3f(Tuple3f t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - } - - - /** - * Constructs and initializes a Tuple3f from the specified Tuple3d. - * - * @param t1 the Tuple3d containing the initialization x y z data - */ - public Tuple3f(Tuple3d t1) { - this.x = (float) t1.x; - this.y = (float) t1.y; - this.z = (float) t1.z; - } - - - /** - * Constructs and initializes a Tuple3f to (0,0,0). - */ - public Tuple3f() { - this.x = 0.0f; - this.y = 0.0f; - this.z = 0.0f; - } - - - /** - * Returns a string that contains the values of this Tuple3f. - * The form is (x,y,z). - * - * @return the String representation - */ - public String toString() { - return "(" + this.x + ", " + this.y + ", " + this.z + ")"; - } - - - /** - * Sets the value of this tuple to the specified xyz coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - */ - public final void set(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - } - - - /** - * Sets the value of this tuple to the xyz coordinates specified in - * the array of length 3. - * - * @param t the array of length 3 containing xyz in order - */ - public final void set(float[] t) { - this.x = t[0]; - this.y = t[1]; - this.z = t[2]; - } - - - /** - * Sets the value of this tuple to the value of tuple t1. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple3f t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - } - - - /** - * Sets the value of this tuple to the value of tuple t1. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple3d t1) { - this.x = (float) t1.x; - this.y = (float) t1.y; - this.z = (float) t1.z; - } - - - /** - * Gets the value of this tuple and copies the values into t. - * - * @param t the array of length 3 into which the values are copied - */ - public final void get(float[] t) { - t[0] = this.x; - t[1] = this.y; - t[2] = this.z; - } - - - /** - * Gets the value of this tuple and copies the values into t. - * - * @param t the Tuple3f object into which the values of this object are copied - */ - public final void get(Tuple3f t) { - t.x = this.x; - t.y = this.y; - t.z = this.z; - } - - - /** - * Sets the value of this tuple to the vector sum of tuples t1 and t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void add(Tuple3f t1, Tuple3f t2) { - this.x = t1.x + t2.x; - this.y = t1.y + t2.y; - this.z = t1.z + t2.z; - } - - - /** - * Sets the value of this tuple to the vector sum of itself and tuple t1. - * - * @param t1 the other tuple - */ - public final void add(Tuple3f t1) { - this.x += t1.x; - this.y += t1.y; - this.z += t1.z; - } - - - /** - * Sets the value of this tuple to the vector difference - * of tuples t1 and t2 (this = t1 - t2). - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void sub(Tuple3f t1, Tuple3f t2) { - this.x = t1.x - t2.x; - this.y = t1.y - t2.y; - this.z = t1.z - t2.z; - } - - - /** - * Sets the value of this tuple to the vector difference of - * itself and tuple t1 (this = this - t1) . - * - * @param t1 the other tuple - */ - public final void sub(Tuple3f t1) { - this.x -= t1.x; - this.y -= t1.y; - this.z -= t1.z; - } - - - /** - * Sets the value of this tuple to the negation of tuple t1. - * - * @param t1 the source tuple - */ - public final void negate(Tuple3f t1) { - this.x = -t1.x; - this.y = -t1.y; - this.z = -t1.z; - } - - - /** - * Negates the value of this tuple in place. - */ - public final void negate() { - this.x = -this.x; - this.y = -this.y; - this.z = -this.z; - } - - - /** - * Sets the value of this vector to the scalar multiplication - * of tuple t1. - * - * @param s the scalar value - * @param t1 the source tuple - */ - public final void scale(float s, Tuple3f t1) { - this.x = s * t1.x; - this.y = s * t1.y; - this.z = s * t1.z; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of the scale factor with this. - * - * @param s the scalar value - */ - public final void scale(float s) { - this.x *= s; - this.y *= s; - this.z *= s; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of tuple t1 and then adds tuple t2 (this = s*t1 + t2). - * - * @param s the scalar value - * @param t1 the tuple to be scaled and added - * @param t2 the tuple to be added without a scale - */ - public final void scaleAdd(float s, Tuple3f t1, Tuple3f t2) { - this.x = s * t1.x + t2.x; - this.y = s * t1.y + t2.y; - this.z = s * t1.z + t2.z; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of itself and then adds tuple t1 (this = s*this + t1). - * - * @param s the scalar value - * @param t1 the tuple to be added - */ - public final void scaleAdd(float s, Tuple3f t1) { - this.x = s * this.x + t1.x; - this.y = s * this.y + t1.y; - this.z = s * this.z + t1.z; - } - - - /** - * Returns true if the Object t1 is of type Tuple3f and all of the - * data members of t1 are equal to the corresponding data members in - * this Tuple3f. - * - * @param t1 the vector with which the comparison is made - * @return true or false - */ - public boolean equals(Tuple3f t1) { - try { - return (this.x == t1.x && this.y == t1.y && this.z == t1.z); - } catch (NullPointerException e2) { - Iris.reportError(e2); - return false; - } - } - - /** - * Returns true if the Object t1 is of type Tuple3f and all of the - * data members of t1 are equal to the corresponding data members in - * this Tuple3f. - * - * @param t1 the Object with which the comparison is made - * @return true or false - */ - public boolean equals(Object t1) { - try { - Tuple3f t2 = (Tuple3f) t1; - return (this.x == t2.x && this.y == t2.y && this.z == t2.z); - } catch (NullPointerException | ClassCastException e2) { - Iris.reportError(e2); - return false; - } - } - - - /** - * Returns true if the L-infinite distance between this tuple - * and tuple t1 is less than or equal to the epsilon parameter, - * otherwise returns false. The L-infinite - * distance is equal to MAX[abs(x1-x2), abs(y1-y2), abs(z1-z2)]. - * - * @param t1 the tuple to be compared to this tuple - * @param epsilon the threshold value - * @return true or false - */ - public boolean epsilonEquals(Tuple3f t1, float epsilon) { - float diff; - - diff = x - t1.x; - if (Float.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = y - t1.y; - if (Float.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = z - t1.z; - if (Float.isNaN(diff)) return false; - return !((diff < 0 ? -diff : diff) > epsilon); - - } - - - /** - * Returns a hash code value based on the data values in this - * object. Two different Tuple3f objects with identical data values - * (i.e., Tuple3f.equals returns true) will return the same hash - * code value. Two objects with different data members may return the - * same hash value, although this is not likely. - * - * @return the integer hash code value - */ - public int hashCode() { - long bits = 1L; - bits = 31L * bits + (long) VecMathUtil.floatToIntBits(x); - bits = 31L * bits + (long) VecMathUtil.floatToIntBits(y); - bits = 31L * bits + (long) VecMathUtil.floatToIntBits(z); - return (int) (bits ^ (bits >> 32)); - } - - - /** - * Clamps the tuple parameter to the range [low, high] and - * places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clamp(float min, float max, Tuple3f t) { - if (t.x > max) { - x = max; - } else x = Math.max(t.x, min); - - if (t.y > max) { - y = max; - } else y = Math.max(t.y, min); - - if (t.z > max) { - z = max; - } else z = Math.max(t.z, min); - - } - - - /** - * Clamps the minimum value of the tuple parameter to the min - * parameter and places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMin(float min, Tuple3f t) { - x = Math.max(t.x, min); - - y = Math.max(t.y, min); - - z = Math.max(t.z, min); - - } - - - /** - * Clamps the maximum value of the tuple parameter to the max - * parameter and places the values into this tuple. - * - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMax(float max, Tuple3f t) { - x = Math.min(t.x, max); - - y = Math.min(t.y, max); - - z = Math.min(t.z, max); - - } - - - /** - * Sets each component of the tuple parameter to its absolute - * value and places the modified values into this tuple. - * - * @param t the source tuple, which will not be modified - */ - public final void absolute(Tuple3f t) { - x = Math.abs(t.x); - y = Math.abs(t.y); - z = Math.abs(t.z); - } - - - /** - * Clamps this tuple to the range [low, high]. - * - * @param min the lowest value in this tuple after clamping - * @param max the highest value in this tuple after clamping - */ - public final void clamp(float min, float max) { - if (x > max) { - x = max; - } else if (x < min) { - x = min; - } - - if (y > max) { - y = max; - } else if (y < min) { - y = min; - } - - if (z > max) { - z = max; - } else if (z < min) { - z = min; - } - - } - - - /** - * Clamps the minimum value of this tuple to the min parameter. - * - * @param min the lowest value in this tuple after clamping - */ - public final void clampMin(float min) { - if (x < min) x = min; - if (y < min) y = min; - if (z < min) z = min; - - } - - - /** - * Clamps the maximum value of this tuple to the max parameter. - * - * @param max the highest value in the tuple after clamping - */ - public final void clampMax(float max) { - if (x > max) x = max; - if (y > max) y = max; - if (z > max) z = max; - - } - - - /** - * Sets each component of this tuple to its absolute value. - */ - public final void absolute() { - x = Math.abs(x); - y = Math.abs(y); - z = Math.abs(z); - - } - - - /** - * Linearly interpolates between tuples t1 and t2 and places the - * result into this tuple: this = (1-alpha)*t1 + alpha*t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - * @param alpha the alpha interpolation parameter - */ - public final void interpolate(Tuple3f t1, Tuple3f t2, float alpha) { - this.x = (1 - alpha) * t1.x + alpha * t2.x; - this.y = (1 - alpha) * t1.y + alpha * t2.y; - this.z = (1 - alpha) * t1.z + alpha * t2.z; - - - } - - - /** - * Linearly interpolates between this tuple and tuple t1 and - * places the result into this tuple: this = (1-alpha)*this + alpha*t1. - * - * @param t1 the first tuple - * @param alpha the alpha interpolation parameter - */ - public final void interpolate(Tuple3f t1, float alpha) { - this.x = (1 - alpha) * this.x + alpha * t1.x; - this.y = (1 - alpha) * this.y + alpha * t1.y; - this.z = (1 - alpha) * this.z + alpha * t1.z; - - - } - - /** - * Creates a new object of the same class as this object. - * - * @return a clone of this instance. - * @throws OutOfMemoryError if there is not enough memory. - * @see java.lang.Cloneable - * @since vecmath 1.3 - */ - public Object clone() { - // Since there are no arrays we can just use Object.clone() - try { - return super.clone(); - } catch (CloneNotSupportedException e) { - Iris.reportError(e); - // this shouldn't happen, since we are Cloneable - throw new InternalError(); - } - } - - - /** - * Get the x coordinate. - * - * @return the x coordinate. - * @since vecmath 1.5 - */ - public final float getX() { - return x; - } - - - /** - * Set the x coordinate. - * - * @param x value to x coordinate. - * @since vecmath 1.5 - */ - public final void setX(float x) { - this.x = x; - } - - - /** - * Get the y coordinate. - * - * @return the y coordinate. - * @since vecmath 1.5 - */ - public final float getY() { - return y; - } - - - /** - * Set the y coordinate. - * - * @param y value to y coordinate. - * @since vecmath 1.5 - */ - public final void setY(float y) { - this.y = y; - } - - /** - * Get the z coordinate. - * - * @return the z coordinate - * @since vecmath 1.5 - */ - public final float getZ() { - return z; - } - - - /** - * Set the Z coordinate. - * - * @param z value to z coordinate. - * @since vecmath 1.5 - */ - public final void setZ(float z) { - this.z = z; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Tuple4d.java b/core/src/main/java/com/volmit/iris/util/math/Tuple4d.java deleted file mode 100644 index e3d30fe90..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Tuple4d.java +++ /dev/null @@ -1,817 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.Iris; - -/** - * A 4 element tuple represented by double precision floating point - * x,y,z,w coordinates. - */ -public abstract class Tuple4d implements java.io.Serializable, Cloneable { - - static final long serialVersionUID = -4748953690425311052L; - - /** - * The x coordinate. - */ - public double x; - - /** - * The y coordinate. - */ - public double y; - - /** - * The z coordinate. - */ - public double z; - - /** - * The w coordinate. - */ - public double w; - - - /** - * Constructs and initializes a Tuple4d from the specified xyzw coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - * @param w the w coordinate - */ - public Tuple4d(double x, double y, double z, double w) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - - /** - * Constructs and initializes a Tuple4d from the coordinates contained - * in the array. - * - * @param t the array of length 4 containing xyzw in order - */ - public Tuple4d(double[] t) { - this.x = t[0]; - this.y = t[1]; - this.z = t[2]; - this.w = t[3]; - } - - - /** - * Constructs and initializes a Tuple4d from the specified Tuple4d. - * - * @param t1 the Tuple4d containing the initialization x y z w data - */ - public Tuple4d(Tuple4d t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - this.w = t1.w; - } - - - /** - * Constructs and initializes a Tuple4d from the specified Tuple4f. - * - * @param t1 the Tuple4f containing the initialization x y z w data - */ - public Tuple4d(Tuple4f t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - this.w = t1.w; - } - - - /** - * Constructs and initializes a Tuple4d to (0,0,0,0). - */ - public Tuple4d() { - this.x = 0.0; - this.y = 0.0; - this.z = 0.0; - this.w = 0.0; - } - - - /** - * Sets the value of this tuple to the specified xyzw coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - * @param w the w coordinate - */ - public final void set(double x, double y, double z, double w) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - - /** - * Sets the value of this tuple to the specified xyzw coordinates. - * - * @param t the array of length 4 containing xyzw in order - */ - public final void set(double[] t) { - this.x = t[0]; - this.y = t[1]; - this.z = t[2]; - this.w = t[3]; - } - - - /** - * Sets the value of this tuple to the value of tuple t1. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple4d t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - this.w = t1.w; - } - - - /** - * Sets the value of this tuple to the value of tuple t1. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple4f t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - this.w = t1.w; - } - - - /** - * Gets the value of this tuple and places it into the array t of - * length four in x,y,z,w order. - * - * @param t the array of length four - */ - public final void get(double[] t) { - t[0] = this.x; - t[1] = this.y; - t[2] = this.z; - t[3] = this.w; - } - - - /** - * Gets the value of this tuple and places it into the Tuple4d - * argument of - * length four in x,y,z,w order. - * - * @param t the Tuple into which the values will be copied - */ - public final void get(Tuple4d t) { - t.x = this.x; - t.y = this.y; - t.z = this.z; - t.w = this.w; - } - - - /** - * Sets the value of this tuple to the tuple sum of tuples t1 and t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void add(Tuple4d t1, Tuple4d t2) { - this.x = t1.x + t2.x; - this.y = t1.y + t2.y; - this.z = t1.z + t2.z; - this.w = t1.w + t2.w; - } - - - /** - * Sets the value of this tuple to the sum of itself and tuple t1. - * - * @param t1 the other tuple - */ - public final void add(Tuple4d t1) { - this.x += t1.x; - this.y += t1.y; - this.z += t1.z; - this.w += t1.w; - } - - - /** - * Sets the value of this tuple to the difference - * of tuples t1 and t2 (this = t1 - t2). - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void sub(Tuple4d t1, Tuple4d t2) { - this.x = t1.x - t2.x; - this.y = t1.y - t2.y; - this.z = t1.z - t2.z; - this.w = t1.w - t2.w; - } - - - /** - * Sets the value of this tuple to the difference of itself - * and tuple t1 (this = this - t1). - * - * @param t1 the other tuple - */ - public final void sub(Tuple4d t1) { - this.x -= t1.x; - this.y -= t1.y; - this.z -= t1.z; - this.w -= t1.w; - } - - - /** - * Sets the value of this tuple to the negation of tuple t1. - * - * @param t1 the source tuple - */ - public final void negate(Tuple4d t1) { - this.x = -t1.x; - this.y = -t1.y; - this.z = -t1.z; - this.w = -t1.w; - } - - - /** - * Negates the value of this tuple in place. - */ - public final void negate() { - this.x = -this.x; - this.y = -this.y; - this.z = -this.z; - this.w = -this.w; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of the scale factor with the tuple t1. - * - * @param s the scalar value - * @param t1 the source tuple - */ - public final void scale(double s, Tuple4d t1) { - this.x = s * t1.x; - this.y = s * t1.y; - this.z = s * t1.z; - this.w = s * t1.w; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of the scale factor with this. - * - * @param s the scalar value - */ - public final void scale(double s) { - this.x *= s; - this.y *= s; - this.z *= s; - this.w *= s; - } - - - /** - * Sets the value of this tuple to the scalar multiplication by s - * of tuple t1 plus tuple t2 (this = s*t1 + t2). - * - * @param s the scalar value - * @param t1 the tuple to be multipled - * @param t2 the tuple to be added - */ - public final void scaleAdd(double s, Tuple4d t1, Tuple4d t2) { - this.x = s * t1.x + t2.x; - this.y = s * t1.y + t2.y; - this.z = s * t1.z + t2.z; - this.w = s * t1.w + t2.w; - } - - - /** - * @deprecated Use scaleAdd(double,Tuple4d) instead - */ - @Deprecated - public final void scaleAdd(float s, Tuple4d t1) { - scaleAdd((double) s, t1); - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of itself and then adds tuple t1 (this = s*this + t1). - * - * @param s the scalar value - * @param t1 the tuple to be added - */ - public final void scaleAdd(double s, Tuple4d t1) { - this.x = s * this.x + t1.x; - this.y = s * this.y + t1.y; - this.z = s * this.z + t1.z; - this.w = s * this.w + t1.w; - } - - - /** - * Returns a string that contains the values of this Tuple4d. - * The form is (x,y,z,w). - * - * @return the String representation - */ - public String toString() { - return "(" + this.x + ", " + this.y + ", " + this.z + ", " + this.w + ")"; - } - - - /** - * Returns true if all of the data members of Tuple4d t1 are - * equal to the corresponding data members in this Tuple4d. - * - * @param t1 the tuple with which the comparison is made - * @return true or false - */ - public boolean equals(Tuple4d t1) { - try { - return (this.x == t1.x && this.y == t1.y && this.z == t1.z - && this.w == t1.w); - } catch (NullPointerException e2) { - Iris.reportError(e2); - return false; - } - } - - /** - * Returns true if the Object t1 is of type Tuple4d and all of the - * data members of t1 are equal to the corresponding data members in - * this Tuple4d. - * - * @param t1 the object with which the comparison is made - * @return true or false - */ - public boolean equals(Object t1) { - try { - - Tuple4d t2 = (Tuple4d) t1; - return (this.x == t2.x && this.y == t2.y && - this.z == t2.z && this.w == t2.w); - } catch (NullPointerException | ClassCastException e2) { - Iris.reportError(e2); - return false; - } - } - - - /** - * Returns true if the L-infinite distance between this tuple - * and tuple t1 is less than or equal to the epsilon parameter, - * otherwise returns false. The L-infinite - * distance is equal to - * MAX[abs(x1-x2), abs(y1-y2), abs(z1-z2), abs(w1-w2)]. - * - * @param t1 the tuple to be compared to this tuple - * @param epsilon the threshold value - * @return true or false - */ - public boolean epsilonEquals(Tuple4d t1, double epsilon) { - double diff; - - diff = x - t1.x; - if (Double.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = y - t1.y; - if (Double.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = z - t1.z; - if (Double.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = w - t1.w; - if (Double.isNaN(diff)) return false; - return !((diff < 0 ? -diff : diff) > epsilon); - - } - - - /** - * Returns a hash code value based on the data values in this - * object. Two different Tuple4d objects with identical data values - * (i.e., Tuple4d.equals returns true) will return the same hash - * code value. Two objects with different data members may return the - * same hash value, although this is not likely. - * - * @return the integer hash code value - */ - public int hashCode() { - long bits = 1L; - bits = 31L * bits + VecMathUtil.doubleToLongBits(x); - bits = 31L * bits + VecMathUtil.doubleToLongBits(y); - bits = 31L * bits + VecMathUtil.doubleToLongBits(z); - bits = 31L * bits + VecMathUtil.doubleToLongBits(w); - return (int) (bits ^ (bits >> 32)); - } - - - /** - * @deprecated Use clamp(double,double,Tuple4d) instead - */ - @Deprecated - public final void clamp(float min, float max, Tuple4d t) { - clamp(min, (double) max, t); - } - - - /** - * Clamps the tuple parameter to the range [low, high] and - * places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clamp(double min, double max, Tuple4d t) { - if (t.x > max) { - x = max; - } else x = Math.max(t.x, min); - - if (t.y > max) { - y = max; - } else y = Math.max(t.y, min); - - if (t.z > max) { - z = max; - } else z = Math.max(t.z, min); - - if (t.w > max) { - w = max; - } else w = Math.max(t.w, min); - - } - - - /** - * @deprecated Use clampMin(double,Tuple4d) instead - */ - @Deprecated - public final void clampMin(float min, Tuple4d t) { - clampMin((double) min, t); - } - - - /** - * Clamps the minimum value of the tuple parameter to the min - * parameter and places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMin(double min, Tuple4d t) { - x = Math.max(t.x, min); - - y = Math.max(t.y, min); - - z = Math.max(t.z, min); - - w = Math.max(t.w, min); - - } - - - /** - * @deprecated Use clampMax(double,Tuple4d) instead - */ - @Deprecated - public final void clampMax(float max, Tuple4d t) { - clampMax((double) max, t); - } - - - /** - * Clamps the maximum value of the tuple parameter to the max - * parameter and places the values into this tuple. - * - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMax(double max, Tuple4d t) { - x = Math.min(t.x, max); - - y = Math.min(t.y, max); - - z = Math.min(t.z, max); - - if (t.w > max) { - w = max; - } else { - w = t.z; - } - - } - - - /** - * Sets each component of the tuple parameter to its absolute - * value and places the modified values into this tuple. - * - * @param t the source tuple, which will not be modified - */ - public final void absolute(Tuple4d t) { - x = Math.abs(t.x); - y = Math.abs(t.y); - z = Math.abs(t.z); - w = Math.abs(t.w); - - } - - - /** - * @deprecated Use clamp(double,double) instead - */ - @Deprecated - public final void clamp(float min, float max) { - clamp(min, (double) max); - } - - - /** - * Clamps this tuple to the range [low, high]. - * - * @param min the lowest value in this tuple after clamping - * @param max the highest value in this tuple after clamping - */ - public final void clamp(double min, double max) { - if (x > max) { - x = max; - } else if (x < min) { - x = min; - } - - if (y > max) { - y = max; - } else if (y < min) { - y = min; - } - - if (z > max) { - z = max; - } else if (z < min) { - z = min; - } - - if (w > max) { - w = max; - } else if (w < min) { - w = min; - } - - } - - - /** - * @deprecated Use clampMin(double) instead - */ - @Deprecated - public final void clampMin(float min) { - clampMin((double) min); - } - - - /** - * Clamps the minimum value of this tuple to the min parameter. - * - * @param min the lowest value in this tuple after clamping - */ - public final void clampMin(double min) { - if (x < min) x = min; - if (y < min) y = min; - if (z < min) z = min; - if (w < min) w = min; - } - - - /** - * @deprecated Use clampMax(double) instead - */ - @Deprecated - public final void clampMax(float max) { - clampMax((double) max); - } - - - /** - * Clamps the maximum value of this tuple to the max parameter. - * - * @param max the highest value in the tuple after clamping - */ - public final void clampMax(double max) { - if (x > max) x = max; - if (y > max) y = max; - if (z > max) z = max; - if (w > max) w = max; - - } - - - /** - * Sets each component of this tuple to its absolute value. - */ - public final void absolute() { - x = Math.abs(x); - y = Math.abs(y); - z = Math.abs(z); - w = Math.abs(w); - - } - - - /** - * @deprecated Use interpolate(Tuple4d,Tuple4d,double) instead - */ - @Deprecated - public void interpolate(Tuple4d t1, Tuple4d t2, float alpha) { - interpolate(t1, t2, (double) alpha); - } - - - /** - * Linearly interpolates between tuples t1 and t2 and places the - * result into this tuple: this = (1-alpha)*t1 + alpha*t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - * @param alpha the alpha interpolation parameter - */ - public void interpolate(Tuple4d t1, Tuple4d t2, double alpha) { - this.x = (1 - alpha) * t1.x + alpha * t2.x; - this.y = (1 - alpha) * t1.y + alpha * t2.y; - this.z = (1 - alpha) * t1.z + alpha * t2.z; - this.w = (1 - alpha) * t1.w + alpha * t2.w; - } - - - /** - * @deprecated Use interpolate(Tuple4d,double) instead - */ - @Deprecated - public void interpolate(Tuple4d t1, float alpha) { - interpolate(t1, (double) alpha); - } - - - /** - * Linearly interpolates between this tuple and tuple t1 and - * places the result into this tuple: this = (1-alpha)*this + alpha*t1. - * - * @param t1 the first tuple - * @param alpha the alpha interpolation parameter - */ - public void interpolate(Tuple4d t1, double alpha) { - this.x = (1 - alpha) * this.x + alpha * t1.x; - this.y = (1 - alpha) * this.y + alpha * t1.y; - this.z = (1 - alpha) * this.z + alpha * t1.z; - this.w = (1 - alpha) * this.w + alpha * t1.w; - } - - /** - * Creates a new object of the same class as this object. - * - * @return a clone of this instance. - * @throws OutOfMemoryError if there is not enough memory. - * @see java.lang.Cloneable - * @since vecmath 1.3 - */ - public Object clone() { - // Since there are no arrays we can just use Object.clone() - try { - return super.clone(); - } catch (CloneNotSupportedException e) { - Iris.reportError(e); - // this shouldn't happen, since we are Cloneable - throw new InternalError(); - } - } - - /** - * Get the x coordinate. - * - * @return the x coordinate. - * @since vecmath 1.5 - */ - public final double getX() { - return x; - } - - - /** - * Set the x coordinate. - * - * @param x value to x coordinate. - * @since vecmath 1.5 - */ - public final void setX(double x) { - this.x = x; - } - - - /** - * Get the y coordinate. - * - * @return the y coordinate. - * @since vecmath 1.5 - */ - public final double getY() { - return y; - } - - - /** - * Set the y coordinate. - * - * @param y value to y coordinate. - * @since vecmath 1.5 - */ - public final void setY(double y) { - this.y = y; - } - - /** - * Get the z coordinate. - * - * @return the z coordinate. - * @since vecmath 1.5 - */ - public final double getZ() { - return z; - } - - - /** - * Set the z coordinate. - * - * @param z value to z coordinate. - * @since vecmath 1.5 - */ - public final void setZ(double z) { - this.z = z; - } - - - /** - * Get the w coordinate. - * - * @return the w coordinate. - * @since vecmath 1.5 - */ - public final double getW() { - return w; - } - - - /** - * Set the w coordinate. - * - * @param w value to w coordinate. - * @since vecmath 1.5 - */ - public final void setW(double w) { - this.w = w; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Tuple4f.java b/core/src/main/java/com/volmit/iris/util/math/Tuple4f.java deleted file mode 100644 index 6f8e90155..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Tuple4f.java +++ /dev/null @@ -1,732 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.Iris; - -/** - * A 4-element tuple represented by single-precision floating point x,y,z,w - * coordinates. - */ -public abstract class Tuple4f implements java.io.Serializable, Cloneable { - - static final long serialVersionUID = 7068460319248845763L; - - /** - * The x coordinate. - */ - public float x; - - /** - * The y coordinate. - */ - public float y; - - /** - * The z coordinate. - */ - public float z; - - /** - * The w coordinate. - */ - public float w; - - - /** - * Constructs and initializes a Tuple4f from the specified xyzw coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - * @param w the w coordinate - */ - public Tuple4f(float x, float y, float z, float w) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - - /** - * Constructs and initializes a Tuple4f from the array of length 4. - * - * @param t the array of length 4 containing xyzw in order - */ - public Tuple4f(float[] t) { - this.x = t[0]; - this.y = t[1]; - this.z = t[2]; - this.w = t[3]; - } - - - /** - * Constructs and initializes a Tuple4f from the specified Tuple4f. - * - * @param t1 the Tuple4f containing the initialization x y z w data - */ - public Tuple4f(Tuple4f t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - this.w = t1.w; - } - - - /** - * Constructs and initializes a Tuple4f from the specified Tuple4d. - * - * @param t1 the Tuple4d containing the initialization x y z w data - */ - public Tuple4f(Tuple4d t1) { - this.x = (float) t1.x; - this.y = (float) t1.y; - this.z = (float) t1.z; - this.w = (float) t1.w; - } - - - /** - * Constructs and initializes a Tuple4f to (0,0,0,0). - */ - public Tuple4f() { - this.x = 0.0f; - this.y = 0.0f; - this.z = 0.0f; - this.w = 0.0f; - } - - - /** - * Sets the value of this tuple to the specified xyzw coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - * @param w the w coordinate - */ - public final void set(float x, float y, float z, float w) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - - /** - * Sets the value of this tuple to the specified coordinates in the - * array of length 4. - * - * @param t the array of length 4 containing xyzw in order - */ - public final void set(float[] t) { - this.x = t[0]; - this.y = t[1]; - this.z = t[2]; - this.w = t[3]; - } - - - /** - * Sets the value of this tuple to the value of tuple t1. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple4f t1) { - this.x = t1.x; - this.y = t1.y; - this.z = t1.z; - this.w = t1.w; - } - - - /** - * Sets the value of this tuple to the value of tuple t1. - * - * @param t1 the tuple to be copied - */ - public final void set(Tuple4d t1) { - this.x = (float) t1.x; - this.y = (float) t1.y; - this.z = (float) t1.z; - this.w = (float) t1.w; - } - - - /** - * Copies the values of this tuple into the array t. - * - * @param t the array - */ - public final void get(float[] t) { - t[0] = this.x; - t[1] = this.y; - t[2] = this.z; - t[3] = this.w; - } - - - /** - * Copies the values of this tuple into the tuple t. - * - * @param t the target tuple - */ - public final void get(Tuple4f t) { - t.x = this.x; - t.y = this.y; - t.z = this.z; - t.w = this.w; - } - - - /** - * Sets the value of this tuple to the sum of tuples t1 and t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void add(Tuple4f t1, Tuple4f t2) { - this.x = t1.x + t2.x; - this.y = t1.y + t2.y; - this.z = t1.z + t2.z; - this.w = t1.w + t2.w; - } - - - /** - * Sets the value of this tuple to the sum of itself and t1. - * - * @param t1 the other tuple - */ - public final void add(Tuple4f t1) { - this.x += t1.x; - this.y += t1.y; - this.z += t1.z; - this.w += t1.w; - } - - - /** - * Sets the value of this tuple to the difference - * of tuples t1 and t2 (this = t1 - t2). - * - * @param t1 the first tuple - * @param t2 the second tuple - */ - public final void sub(Tuple4f t1, Tuple4f t2) { - this.x = t1.x - t2.x; - this.y = t1.y - t2.y; - this.z = t1.z - t2.z; - this.w = t1.w - t2.w; - } - - - /** - * Sets the value of this tuple to the difference - * of itself and t1 (this = this - t1). - * - * @param t1 the other tuple - */ - public final void sub(Tuple4f t1) { - this.x -= t1.x; - this.y -= t1.y; - this.z -= t1.z; - this.w -= t1.w; - } - - - /** - * Sets the value of this tuple to the negation of tuple t1. - * - * @param t1 the source tuple - */ - public final void negate(Tuple4f t1) { - this.x = -t1.x; - this.y = -t1.y; - this.z = -t1.z; - this.w = -t1.w; - } - - - /** - * Negates the value of this tuple in place. - */ - public final void negate() { - this.x = -this.x; - this.y = -this.y; - this.z = -this.z; - this.w = -this.w; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of tuple t1. - * - * @param s the scalar value - * @param t1 the source tuple - */ - public final void scale(float s, Tuple4f t1) { - this.x = s * t1.x; - this.y = s * t1.y; - this.z = s * t1.z; - this.w = s * t1.w; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of the scale factor with this. - * - * @param s the scalar value - */ - public final void scale(float s) { - this.x *= s; - this.y *= s; - this.z *= s; - this.w *= s; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of tuple t1 plus tuple t2 (this = s*t1 + t2). - * - * @param s the scalar value - * @param t1 the tuple to be multipled - * @param t2 the tuple to be added - */ - public final void scaleAdd(float s, Tuple4f t1, Tuple4f t2) { - this.x = s * t1.x + t2.x; - this.y = s * t1.y + t2.y; - this.z = s * t1.z + t2.z; - this.w = s * t1.w + t2.w; - } - - - /** - * Sets the value of this tuple to the scalar multiplication - * of itself and then adds tuple t1 (this = s*this + t1). - * - * @param s the scalar value - * @param t1 the tuple to be added - */ - public final void scaleAdd(float s, Tuple4f t1) { - this.x = s * this.x + t1.x; - this.y = s * this.y + t1.y; - this.z = s * this.z + t1.z; - this.w = s * this.w + t1.w; - } - - - /** - * Returns a string that contains the values of this Tuple4f. - * The form is (x,y,z,w). - * - * @return the String representation - */ - public String toString() { - return "(" + this.x + ", " + this.y + ", " + this.z + ", " + this.w + ")"; - } - - /** - * Returns true if all of the data members of Tuple4f t1 are - * equal to the corresponding data members in this Tuple4f. - * - * @param t1 the vector with which the comparison is made - * @return true or false - */ - public boolean equals(Tuple4f t1) { - try { - return (this.x == t1.x && this.y == t1.y && this.z == t1.z - && this.w == t1.w); - } catch (NullPointerException e2) { - Iris.reportError(e2); - return false; - } - } - - /** - * Returns true if the Object t1 is of type Tuple4f and all of the - * data members of t1 are equal to the corresponding data members in - * this Tuple4f. - * - * @param t1 the object with which the comparison is made - * @return true or false - */ - public boolean equals(Object t1) { - try { - Tuple4f t2 = (Tuple4f) t1; - return (this.x == t2.x && this.y == t2.y && - this.z == t2.z && this.w == t2.w); - } catch (NullPointerException | ClassCastException e2) { - Iris.reportError(e2); - return false; - } - } - - - /** - * Returns true if the L-infinite distance between this tuple - * and tuple t1 is less than or equal to the epsilon parameter, - * otherwise returns false. The L-infinite - * distance is equal to - * MAX[abs(x1-x2), abs(y1-y2), abs(z1-z2), abs(w1-w2)]. - * - * @param t1 the tuple to be compared to this tuple - * @param epsilon the threshold value - * @return true or false - */ - public boolean epsilonEquals(Tuple4f t1, float epsilon) { - float diff; - - diff = x - t1.x; - if (Float.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = y - t1.y; - if (Float.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = z - t1.z; - if (Float.isNaN(diff)) return false; - if ((diff < 0 ? -diff : diff) > epsilon) return false; - - diff = w - t1.w; - if (Float.isNaN(diff)) return false; - return !((diff < 0 ? -diff : diff) > epsilon); - } - - - /** - * Returns a hash code value based on the data values in this - * object. Two different Tuple4f objects with identical data values - * (i.e., Tuple4f.equals returns true) will return the same hash - * code value. Two objects with different data members may return the - * same hash value, although this is not likely. - * - * @return the integer hash code value - */ - public int hashCode() { - long bits = 1L; - bits = 31L * bits + (long) VecMathUtil.floatToIntBits(x); - bits = 31L * bits + (long) VecMathUtil.floatToIntBits(y); - bits = 31L * bits + (long) VecMathUtil.floatToIntBits(z); - bits = 31L * bits + (long) VecMathUtil.floatToIntBits(w); - return (int) (bits ^ (bits >> 32)); - } - - - /** - * Clamps the tuple parameter to the range [low, high] and - * places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clamp(float min, float max, Tuple4f t) { - if (t.x > max) { - x = max; - } else x = Math.max(t.x, min); - - if (t.y > max) { - y = max; - } else y = Math.max(t.y, min); - - if (t.z > max) { - z = max; - } else z = Math.max(t.z, min); - - if (t.w > max) { - w = max; - } else w = Math.max(t.w, min); - - } - - - /** - * Clamps the minimum value of the tuple parameter to the min - * parameter and places the values into this tuple. - * - * @param min the lowest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMin(float min, Tuple4f t) { - x = Math.max(t.x, min); - - y = Math.max(t.y, min); - - z = Math.max(t.z, min); - - w = Math.max(t.w, min); - - - } - - - /** - * Clamps the maximum value of the tuple parameter to the max - * parameter and places the values into this tuple. - * - * @param max the highest value in the tuple after clamping - * @param t the source tuple, which will not be modified - */ - public final void clampMax(float max, Tuple4f t) { - x = Math.min(t.x, max); - - y = Math.min(t.y, max); - - z = Math.min(t.z, max); - - if (t.w > max) { - w = max; - } else { - w = t.z; - } - - } - - - /** - * Sets each component of the tuple parameter to its absolute - * value and places the modified values into this tuple. - * - * @param t the source tuple, which will not be modified - */ - public final void absolute(Tuple4f t) { - x = Math.abs(t.x); - y = Math.abs(t.y); - z = Math.abs(t.z); - w = Math.abs(t.w); - } - - - /** - * Clamps this tuple to the range [low, high]. - * - * @param min the lowest value in this tuple after clamping - * @param max the highest value in this tuple after clamping - */ - public final void clamp(float min, float max) { - if (x > max) { - x = max; - } else if (x < min) { - x = min; - } - - if (y > max) { - y = max; - } else if (y < min) { - y = min; - } - - if (z > max) { - z = max; - } else if (z < min) { - z = min; - } - - if (w > max) { - w = max; - } else if (w < min) { - w = min; - } - - } - - - /** - * Clamps the minimum value of this tuple to the min parameter. - * - * @param min the lowest value in this tuple after clamping - */ - public final void clampMin(float min) { - if (x < min) x = min; - if (y < min) y = min; - if (z < min) z = min; - if (w < min) w = min; - - } - - - /** - * Clamps the maximum value of this tuple to the max parameter. - * - * @param max the highest value in the tuple after clamping - */ - public final void clampMax(float max) { - if (x > max) x = max; - if (y > max) y = max; - if (z > max) z = max; - if (w > max) w = max; - - } - - - /** - * Sets each component of this tuple to its absolute value. - */ - public final void absolute() { - x = Math.abs(x); - y = Math.abs(y); - z = Math.abs(z); - w = Math.abs(w); - } - - - /** - * Linearly interpolates between tuples t1 and t2 and places the - * result into this tuple: this = (1-alpha)*t1 + alpha*t2. - * - * @param t1 the first tuple - * @param t2 the second tuple - * @param alpha the alpha interpolation parameter - */ - public void interpolate(Tuple4f t1, Tuple4f t2, float alpha) { - this.x = (1 - alpha) * t1.x + alpha * t2.x; - this.y = (1 - alpha) * t1.y + alpha * t2.y; - this.z = (1 - alpha) * t1.z + alpha * t2.z; - this.w = (1 - alpha) * t1.w + alpha * t2.w; - - } - - - /** - * Linearly interpolates between this tuple and tuple t1 and - * places the result into this tuple: this = (1-alpha)*this + alpha*t1. - * - * @param t1 the first tuple - * @param alpha the alpha interpolation parameter - */ - public void interpolate(Tuple4f t1, float alpha) { - this.x = (1 - alpha) * this.x + alpha * t1.x; - this.y = (1 - alpha) * this.y + alpha * t1.y; - this.z = (1 - alpha) * this.z + alpha * t1.z; - this.w = (1 - alpha) * this.w + alpha * t1.w; - - } - - /** - * Creates a new object of the same class as this object. - * - * @return a clone of this instance. - * @throws OutOfMemoryError if there is not enough memory. - * @see java.lang.Cloneable - * @since vecmath 1.3 - */ - public Object clone() { - // Since there are no arrays we can just use Object.clone() - try { - return super.clone(); - } catch (CloneNotSupportedException e) { - Iris.reportError(e); - // this shouldn't happen, since we are Cloneable - throw new InternalError(); - } - } - - /** - * Get the x coordinate. - * - * @return the x coordinate. - * @since vecmath 1.5 - */ - public final float getX() { - return x; - } - - - /** - * Set the x coordinate. - * - * @param x value to x coordinate. - * @since vecmath 1.5 - */ - public final void setX(float x) { - this.x = x; - } - - - /** - * Get the y coordinate. - * - * @return the y coordinate. - * @since vecmath 1.5 - */ - public final float getY() { - return y; - } - - - /** - * Set the y coordinate. - * - * @param y value to y coordinate. - * @since vecmath 1.5 - */ - public final void setY(float y) { - this.y = y; - } - - /** - * Get the z coordinate. - * - * @return the z coordinate. - * @since vecmath 1.5 - */ - public final float getZ() { - return z; - } - - - /** - * Set the z coordinate. - * - * @param z value to z coordinate. - * @since vecmath 1.5 - */ - public final void setZ(float z) { - this.z = z; - } - - - /** - * Get the w coordinate. - * - * @return the w coordinate. - * @since vecmath 1.5 - */ - public final float getW() { - return w; - } - - - /** - * Set the w coordinate. - * - * @param w value to w coordinate. - * @since vecmath 1.5 - */ - public final void setW(float w) { - this.w = w; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/VecMathUtil.java b/core/src/main/java/com/volmit/iris/util/math/VecMathUtil.java deleted file mode 100644 index 8c3a23e0a..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/VecMathUtil.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -/** - * Utility vecmath class used when computing the hash code for vecmath - * objects containing float or double values. This fixes Issue 36. - */ -class VecMathUtil { - /** - * Do not construct an instance of this class. - */ - private VecMathUtil() { - } - - /** - * Returns the representation of the specified floating-point - * value according to the IEEE 754 floating-point "single format" - * bit layout, after first mapping -0.0 to 0.0. This method is - * identical to Float.floatToIntBits(float) except that an integer - * value of 0 is returned for a floating-point value of - * -0.0f. This is done for the purpose of computing a hash code - * that satisfies the contract of hashCode() and equals(). The - * equals() method in each vecmath class does a pair-wise "==" - * test on each floating-point field in the class (e.g., x, y, and - * z for a Tuple3f). Since 0.0f == -0.0f returns true, - * we must also return the same hash code for two objects, one of - * which has a field with a value of -0.0f and the other of which - * has a cooresponding field with a value of 0.0f. - * - * @param f an input floating-point number - * @return the integer bits representing that floating-point - * number, after first mapping -0.0f to 0.0f - */ - static int floatToIntBits(float f) { - // Check for +0 or -0 - if (f == 0.0f) { - return 0; - } else { - return Float.floatToIntBits(f); - } - } - - /** - * Returns the representation of the specified floating-point - * value according to the IEEE 754 floating-point "double format" - * bit layout, after first mapping -0.0 to 0.0. This method is - * identical to Double.doubleToLongBits(double) except that an - * integer value of 0L is returned for a floating-point value of - * -0.0. This is done for the purpose of computing a hash code - * that satisfies the contract of hashCode() and equals(). The - * equals() method in each vecmath class does a pair-wise "==" - * test on each floating-point field in the class (e.g., x, y, and - * z for a Tuple3d). Since 0.0 == -0.0 returns true, we - * must also return the same hash code for two objects, one of - * which has a field with a value of -0.0 and the other of which - * has a cooresponding field with a value of 0.0. - * - * @param d an input double precision floating-point number - * @return the integer bits representing that floating-point - * number, after first mapping -0.0f to 0.0f - */ - static long doubleToLongBits(double d) { - // Check for +0 or -0 - if (d == 0.0) { - return 0L; - } else { - return Double.doubleToLongBits(d); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Vector2d.java b/core/src/main/java/com/volmit/iris/util/math/Vector2d.java deleted file mode 100644 index ac09bd107..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Vector2d.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -/** - * A 2-element vector that is represented by double-precision floating - * point x,y coordinates. - */ -public class Vector2d extends Tuple2d implements java.io.Serializable { - - // Combatible with 1.1 - static final long serialVersionUID = 8572646365302599857L; - - /** - * Constructs and initializes a Vector2d from the specified xy coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - */ - public Vector2d(double x, double y) { - super(x, y); - } - - - /** - * Constructs and initializes a Vector2d from the specified array. - * - * @param v the array of length 2 containing xy in order - */ - public Vector2d(double[] v) { - super(v); - } - - - /** - * Constructs and initializes a Vector2d from the specified Vector2d. - * - * @param v1 the Vector2d containing the initialization x y data - */ - public Vector2d(Vector2d v1) { - super(v1); - } - - - /** - * Constructs and initializes a Vector2d from the specified Vector2f. - * - * @param v1 the Vector2f containing the initialization x y data - */ - public Vector2d(Vector2f v1) { - super(v1); - } - - - /** - * Constructs and initializes a Vector2d from the specified Tuple2d. - * - * @param t1 the Tuple2d containing the initialization x y data - */ - public Vector2d(Tuple2d t1) { - super(t1); - } - - - /** - * Constructs and initializes a Vector2d from the specified Tuple2f. - * - * @param t1 the Tuple2f containing the initialization x y data - */ - public Vector2d(Tuple2f t1) { - super(t1); - } - - - /** - * Constructs and initializes a Vector2d to (0,0). - */ - public Vector2d() { - super(); - } - - - /** - * Computes the dot product of the this vector and vector v1. - * - * @param v1 the other vector - */ - public final double dot(Vector2d v1) { - return (this.x * v1.x + this.y * v1.y); - } - - - /** - * Returns the length of this vector. - * - * @return the length of this vector - */ - public final double length() { - return Math.sqrt(this.x * this.x + this.y * this.y); - } - - /** - * Returns the squared length of this vector. - * - * @return the squared length of this vector - */ - public final double lengthSquared() { - return (this.x * this.x + this.y * this.y); - } - - /** - * Sets the value of this vector to the normalization of vector v1. - * - * @param v1 the un-normalized vector - */ - public final void normalize(Vector2d v1) { - double norm; - - norm = 1.0 / Math.sqrt(v1.x * v1.x + v1.y * v1.y); - this.x = v1.x * norm; - this.y = v1.y * norm; - } - - /** - * Normalizes this vector in place. - */ - public final void normalize() { - double norm; - - norm = 1.0 / Math.sqrt(this.x * this.x + this.y * this.y); - this.x *= norm; - this.y *= norm; - } - - - /** - * Returns the angle in radians between this vector and the vector - * parameter; the return value is constrained to the range [0,PI]. - * - * @param v1 the other vector - * @return the angle in radians in the range [0,PI] - */ - public final double angle(Vector2d v1) { - double vDot = this.dot(v1) / (this.length() * v1.length()); - if (vDot < -1.0) vDot = -1.0; - if (vDot > 1.0) vDot = 1.0; - return Math.acos(vDot); - - } - - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Vector2f.java b/core/src/main/java/com/volmit/iris/util/math/Vector2f.java deleted file mode 100644 index 823fe1234..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Vector2f.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -/** - * A 2-element vector that is represented by single-precision floating - * point x,y coordinates. - */ -public class Vector2f extends Tuple2f implements java.io.Serializable { - - // Combatible with 1.1 - static final long serialVersionUID = -2168194326883512320L; - - /** - * Constructs and initializes a Vector2f from the specified xy coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - */ - public Vector2f(float x, float y) { - super(x, y); - } - - - /** - * Constructs and initializes a Vector2f from the specified array. - * - * @param v the array of length 2 containing xy in order - */ - public Vector2f(float[] v) { - super(v); - } - - - /** - * Constructs and initializes a Vector2f from the specified Vector2f. - * - * @param v1 the Vector2f containing the initialization x y data - */ - public Vector2f(Vector2f v1) { - super(v1); - } - - - /** - * Constructs and initializes a Vector2f from the specified Vector2d. - * - * @param v1 the Vector2d containing the initialization x y data - */ - public Vector2f(Vector2d v1) { - super(v1); - } - - - /** - * Constructs and initializes a Vector2f from the specified Tuple2f. - * - * @param t1 the Tuple2f containing the initialization x y data - */ - public Vector2f(Tuple2f t1) { - super(t1); - } - - - /** - * Constructs and initializes a Vector2f from the specified Tuple2d. - * - * @param t1 the Tuple2d containing the initialization x y data - */ - public Vector2f(Tuple2d t1) { - super(t1); - } - - - /** - * Constructs and initializes a Vector2f to (0,0). - */ - public Vector2f() { - super(); - } - - - /** - * Computes the dot product of the this vector and vector v1. - * - * @param v1 the other vector - */ - public final float dot(Vector2f v1) { - return (this.x * v1.x + this.y * v1.y); - } - - - /** - * Returns the length of this vector. - * - * @return the length of this vector - */ - public final float length() { - return (float) Math.sqrt(this.x * this.x + this.y * this.y); - } - - /** - * Returns the squared length of this vector. - * - * @return the squared length of this vector - */ - public final float lengthSquared() { - return (this.x * this.x + this.y * this.y); - } - - /** - * Sets the value of this vector to the normalization of vector v1. - * - * @param v1 the un-normalized vector - */ - public final void normalize(Vector2f v1) { - float norm; - - norm = (float) (1.0 / Math.sqrt(v1.x * v1.x + v1.y * v1.y)); - this.x = v1.x * norm; - this.y = v1.y * norm; - } - - /** - * Normalizes this vector in place. - */ - public final void normalize() { - float norm; - - norm = (float) - (1.0 / Math.sqrt(this.x * this.x + this.y * this.y)); - this.x *= norm; - this.y *= norm; - } - - - /** - * Returns the angle in radians between this vector and the vector - * parameter; the return value is constrained to the range [0,PI]. - * - * @param v1 the other vector - * @return the angle in radians in the range [0,PI] - */ - public final float angle(Vector2f v1) { - double vDot = this.dot(v1) / (this.length() * v1.length()); - if (vDot < -1.0) vDot = -1.0; - if (vDot > 1.0) vDot = 1.0; - return ((float) (Math.acos(vDot))); - } - - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Vector3d.java b/core/src/main/java/com/volmit/iris/util/math/Vector3d.java deleted file mode 100644 index cfc39b52a..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Vector3d.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -/** - * A 3-element vector that is represented by double-precision floating point - * x,y,z coordinates. If this value represents a normal, then it should - * be normalized. - */ -public class Vector3d extends Tuple3d implements java.io.Serializable { - - // Combatible with 1.1 - static final long serialVersionUID = 3761969948420550442L; - - /** - * Constructs and initializes a Vector3d from the specified xyz coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - */ - public Vector3d(double x, double y, double z) { - super(x, y, z); - } - - - /** - * Constructs and initializes a Vector3d from the array of length 3. - * - * @param v the array of length 3 containing xyz in order - */ - public Vector3d(double[] v) { - super(v); - } - - - /** - * Constructs and initializes a Vector3d from the specified Vector3d. - * - * @param v1 the Vector3d containing the initialization x y z data - */ - public Vector3d(Vector3d v1) { - super(v1); - } - - - /** - * Constructs and initializes a Vector3d from the specified Vector3f. - * - * @param v1 the Vector3f containing the initialization x y z data - */ - public Vector3d(Vector3f v1) { - super(v1); - } - - - /** - * Constructs and initializes a Vector3d from the specified Tuple3f. - * - * @param t1 the Tuple3f containing the initialization x y z data - */ - public Vector3d(Tuple3f t1) { - super(t1); - } - - - /** - * Constructs and initializes a Vector3d from the specified Tuple3d. - * - * @param t1 the Tuple3d containing the initialization x y z data - */ - public Vector3d(Tuple3d t1) { - super(t1); - } - - - /** - * Constructs and initializes a Vector3d to (0,0,0). - */ - public Vector3d() { - super(); - } - - - /** - * Sets this vector to the vector cross product of vectors v1 and v2. - * - * @param v1 the first vector - * @param v2 the second vector - */ - public final void cross(Vector3d v1, Vector3d v2) { - double x, y; - - x = v1.y * v2.z - v1.z * v2.y; - y = v2.x * v1.z - v2.z * v1.x; - this.z = v1.x * v2.y - v1.y * v2.x; - this.x = x; - this.y = y; - } - - - /** - * Sets the value of this vector to the normalization of vector v1. - * - * @param v1 the un-normalized vector - */ - public final void normalize(Vector3d v1) { - double norm; - - norm = 1.0 / Math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z); - this.x = v1.x * norm; - this.y = v1.y * norm; - this.z = v1.z * norm; - } - - - /** - * Normalizes this vector in place. - */ - public final void normalize() { - double norm; - - norm = 1.0 / Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); - this.x *= norm; - this.y *= norm; - this.z *= norm; - } - - - /** - * Returns the dot product of this vector and vector v1. - * - * @param v1 the other vector - * @return the dot product of this and v1 - */ - public final double dot(Vector3d v1) { - return (this.x * v1.x + this.y * v1.y + this.z * v1.z); - } - - - /** - * Returns the squared length of this vector. - * - * @return the squared length of this vector - */ - public final double lengthSquared() { - return (this.x * this.x + this.y * this.y + this.z * this.z); - } - - - /** - * Returns the length of this vector. - * - * @return the length of this vector - */ - public final double length() { - return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); - } - - - /** - * Returns the angle in radians between this vector and the vector - * parameter; the return value is constrained to the range [0,PI]. - * - * @param v1 the other vector - * @return the angle in radians in the range [0,PI] - */ - public final double angle(Vector3d v1) { - double vDot = this.dot(v1) / (this.length() * v1.length()); - if (vDot < -1.0) vDot = -1.0; - if (vDot > 1.0) vDot = 1.0; - return Math.acos(vDot); - } - - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Vector3f.java b/core/src/main/java/com/volmit/iris/util/math/Vector3f.java deleted file mode 100644 index 0640acfd3..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Vector3f.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -/** - * A 3-element vector that is represented by single-precision floating point - * x,y,z coordinates. If this value represents a normal, then it should - * be normalized. - */ -public class Vector3f extends Tuple3f implements java.io.Serializable { - - // Combatible with 1.1 - static final long serialVersionUID = -7031930069184524614L; - - /** - * Constructs and initializes a Vector3f from the specified xyz coordinates. - * - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - */ - public Vector3f(float x, float y, float z) { - super(x, y, z); - } - - - /** - * Constructs and initializes a Vector3f from the array of length 3. - * - * @param v the array of length 3 containing xyz in order - */ - public Vector3f(float[] v) { - super(v); - } - - - /** - * Constructs and initializes a Vector3f from the specified Vector3f. - * - * @param v1 the Vector3f containing the initialization x y z data - */ - public Vector3f(Vector3f v1) { - super(v1); - } - - - /** - * Constructs and initializes a Vector3f from the specified Vector3d. - * - * @param v1 the Vector3d containing the initialization x y z data - */ - public Vector3f(Vector3d v1) { - super(v1); - } - - - /** - * Constructs and initializes a Vector3f from the specified Tuple3f. - * - * @param t1 the Tuple3f containing the initialization x y z data - */ - public Vector3f(Tuple3f t1) { - super(t1); - } - - - /** - * Constructs and initializes a Vector3f from the specified Tuple3d. - * - * @param t1 the Tuple3d containing the initialization x y z data - */ - public Vector3f(Tuple3d t1) { - super(t1); - } - - - /** - * Constructs and initializes a Vector3f to (0,0,0). - */ - public Vector3f() { - super(); - } - - - /** - * Returns the squared length of this vector. - * - * @return the squared length of this vector - */ - public final float lengthSquared() { - return (this.x * this.x + this.y * this.y + this.z * this.z); - } - - /** - * Returns the length of this vector. - * - * @return the length of this vector - */ - public final float length() { - return (float) - Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); - } - - - /** - * Sets this vector to be the vector cross product of vectors v1 and v2. - * - * @param v1 the first vector - * @param v2 the second vector - */ - public final void cross(Vector3f v1, Vector3f v2) { - float x, y; - - x = v1.y * v2.z - v1.z * v2.y; - y = v2.x * v1.z - v2.z * v1.x; - this.z = v1.x * v2.y - v1.y * v2.x; - this.x = x; - this.y = y; - } - - /** - * Computes the dot product of this vector and vector v1. - * - * @param v1 the other vector - * @return the dot product of this vector and v1 - */ - public final float dot(Vector3f v1) { - return (this.x * v1.x + this.y * v1.y + this.z * v1.z); - } - - /** - * Sets the value of this vector to the normalization of vector v1. - * - * @param v1 the un-normalized vector - */ - public final void normalize(Vector3f v1) { - float norm; - - norm = (float) (1.0 / Math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z)); - this.x = v1.x * norm; - this.y = v1.y * norm; - this.z = v1.z * norm; - } - - /** - * Normalizes this vector in place. - */ - public final void normalize() { - float norm; - - norm = (float) - (1.0 / Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)); - this.x *= norm; - this.y *= norm; - this.z *= norm; - } - - - /** - * Returns the angle in radians between this vector and the vector - * parameter; the return value is constrained to the range [0,PI]. - * - * @param v1 the other vector - * @return the angle in radians in the range [0,PI] - */ - public final float angle(Vector3f v1) { - double vDot = this.dot(v1) / (this.length() * v1.length()); - if (vDot < -1.0) vDot = -1.0; - if (vDot > 1.0) vDot = 1.0; - return ((float) (Math.acos(vDot))); - } - -} diff --git a/core/src/main/java/com/volmit/iris/util/math/Vector3i.java b/core/src/main/java/com/volmit/iris/util/math/Vector3i.java deleted file mode 100644 index 3aaf11650..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/Vector3i.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.volmit.iris.util.math; - -import org.bukkit.util.BlockVector; -import org.bukkit.util.Vector; -import org.jetbrains.annotations.NotNull; - -public class Vector3i extends BlockVector { - public Vector3i(int x, int y, int z) { - super(x, y, z); - } - - public Vector3i(Vector vec) { - super(vec); - } - - @NotNull - @Override - public Vector3i clone() { - return (Vector3i) super.clone(); - } - - @Override - public int hashCode() { - return (int) x ^ ((int) z << 12) ^ ((int) y << 24); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/math/VectorMath.java b/core/src/main/java/com/volmit/iris/util/math/VectorMath.java deleted file mode 100644 index 7716f5ee9..000000000 --- a/core/src/main/java/com/volmit/iris/util/math/VectorMath.java +++ /dev/null @@ -1,617 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.math; - -import com.volmit.iris.util.collection.GListAdapter; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.Form; -import org.bukkit.Axis; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.block.BlockFace; -import org.bukkit.entity.Entity; -import org.bukkit.util.Vector; - -/** - * Vector utilities - * - * @author cyberpwn - */ -public class VectorMath { - public static Vector scaleStatic(Axis x, Vector v, double amt) { - return switch (x) { - case X -> scaleX(v, amt); - case Y -> scaleY(v, amt); - case Z -> scaleZ(v, amt); - }; - - } - - public static Vector scaleX(Vector v, double amt) { - double x = v.getX(); - double y = v.getY(); - double z = v.getZ(); - double rx = x == 0 ? 1 : amt / x; - - return new Vector(x * rx, y * rx, z * rx); - } - - public static Vector scaleY(Vector v, double amt) { - double x = v.getX(); - double y = v.getY(); - double z = v.getZ(); - double rx = y == 0 ? 1 : amt / y; - - return new Vector(x * rx, y * rx, z * rx); - } - - public static Vector scaleZ(Vector v, double amt) { - double x = v.getX(); - double y = v.getY(); - double z = v.getZ(); - double rx = z == 0 ? 1 : amt / z; - - return new Vector(x * rx, y * rx, z * rx); - } - - public static Vector reverseXZ(Vector v) { - v.setX(-v.getX()); - v.setZ(-v.getZ()); - return v; - } - - public static boolean isLookingNear(Location a, Location b, double maxOff) { - Vector perfect = VectorMath.direction(a, b); - Vector actual = a.getDirection(); - - return perfect.distance(actual) <= maxOff; - } - - public static Vector rotate(Direction current, Direction to, Vector v) { - if (current.equals(to)) { - return v; - } else if (current.equals(to.reverse())) { - if (current.isVertical()) { - return new Vector(v.getX(), -v.getY(), v.getZ()); - } else { - return new Vector(-v.getX(), v.getY(), -v.getZ()); - } - } else { - Vector c = current.toVector().clone().add(to.toVector()); - - if (c.getX() == 0) { - if (c.getY() != c.getZ()) { - return rotate90CX(v); - } - - return rotate90CCX(v); - } else if (c.getY() == 0) { - if (c.getX() != c.getZ()) { - return rotate90CY(v); - } - - return rotate90CCY(v); - } else if (c.getZ() == 0) { - if (c.getX() != c.getY()) { - return rotate90CZ(v); - } - - return rotate90CCZ(v); - } - } - - return v; - } - - // Y X 0 0 - // X Z 0 0 - - // 0 X Y 0 - // 0 Z X 0 - - public static Vector rotate(Direction current, Direction to, Vector v, int w, int h, int d) { - if (current.equals(to)) { - return v; - } else if (current.equals(to.reverse())) { - if (current.isVertical()) { - return new Vector(v.getX(), -v.getY() + h, v.getZ()); - } else { - return new Vector(-v.getX() + w, v.getY(), -v.getZ() + d); - } - } else { - Vector c = current.toVector().clone().add(to.toVector()); - - if (c.getX() == 0) { - if (c.getY() != c.getZ()) { - return rotate90CX(v, d); - } - - return rotate90CCX(v, h); - } else if (c.getY() == 0) { - if (c.getX() != c.getZ()) { - return rotate90CY(v, d); - } - - return rotate90CCY(v, w); - } else if (c.getZ() == 0) { - if (c.getX() != c.getY()) { - return rotate90CZ(v, w); - } - - return rotate90CCZ(v, h); - } - } - - return v; - } - - public static Vector rotate90CX(Vector v) { - return new Vector(v.getX(), -v.getZ(), v.getY()); - } - - public static Vector rotate90CCX(Vector v) { - return new Vector(v.getX(), v.getZ(), -v.getY()); - } - - public static Vector rotate90CY(Vector v) { - return new Vector(-v.getZ(), v.getY(), v.getX()); - } - - public static Vector rotate90CCY(Vector v) { - return new Vector(v.getZ(), v.getY(), -v.getX()); - } - - public static Vector rotate90CZ(Vector v) { - return new Vector(v.getY(), -v.getX(), v.getZ()); - } - - public static Vector rotate90CCZ(Vector v) { - return new Vector(-v.getY(), v.getX(), v.getZ()); - } - - public static Vector rotate90CX(Vector v, int s) { - return new Vector(v.getX(), -v.getZ() + s, v.getY()); - } - - public static Vector rotate90CCX(Vector v, int s) { - return new Vector(v.getX(), v.getZ(), -v.getY() + s); - } - - public static Vector rotate90CY(Vector v, int s) { - return new Vector(-v.getZ() + s, v.getY(), v.getX()); - } - - public static Vector rotate90CCY(Vector v, int s) { - return new Vector(v.getZ(), v.getY(), -v.getX() + s); - } - - public static Vector rotate90CZ(Vector v, int s) { - return new Vector(v.getY(), -v.getX() + s, v.getZ()); - } - - public static Vector rotate90CCZ(Vector v, int s) { - return new Vector(-v.getY() + s, v.getX(), v.getZ()); - } - - public static Vector getAxis(Direction current, Direction to) { - if (current.equals(Direction.U) || current.equals(Direction.D)) { - if (to.equals(Direction.U) || to.equals(Direction.D)) { - return new Vector(1, 0, 0); - } else { - if (current.equals(Direction.N) || current.equals(Direction.S)) { - return Direction.E.toVector(); - } else { - return Direction.S.toVector(); - } - } - } - - return new Vector(0, 1, 0); - } - - private static double round(double value, int precision) { - return Double.parseDouble(Form.f(value, precision)); - } - - public static Vector clip(Vector v, int decimals) { - v.setX(round(v.getX(), decimals)); - v.setY(round(v.getY(), decimals)); - v.setZ(round(v.getZ(), decimals)); - return v; - } - - public static Vector rotateVectorCC(Vector vec, Vector axis, double deg) { - double theta = Math.toRadians(deg); - double x, y, z; - double u, v, w; - x = vec.getX(); - y = vec.getY(); - z = vec.getZ(); - u = axis.getX(); - v = axis.getY(); - w = axis.getZ(); - double f = u * x + v * y + w * z; - double xPrime = u * (f) * (1d - Math.cos(theta)) + x * Math.cos(theta) + (-w * y + v * z) * Math.sin(theta); - double yPrime = v * (f) * (1d - Math.cos(theta)) + y * Math.cos(theta) + (w * x - u * z) * Math.sin(theta); - double zPrime = w * (f) * (1d - Math.cos(theta)) + z * Math.cos(theta) + (-v * x + u * y) * Math.sin(theta); - - return clip(new Vector(xPrime, yPrime, zPrime), 4); - } - - /** - * Get all SIMPLE block faces from a more specific block face (SOUTH_EAST) = - * (south, east) - * - * @param f the block face - * @return multiple faces, or one if the face is already simple - */ - public static KList split(BlockFace f) { - KList faces = new KList<>(); - - switch (f) { - case DOWN: - faces.add(BlockFace.DOWN); - break; - case EAST: - faces.add(BlockFace.EAST); - break; - case EAST_NORTH_EAST: - faces.add(BlockFace.EAST); - faces.add(BlockFace.EAST); - faces.add(BlockFace.NORTH); - break; - case EAST_SOUTH_EAST: - faces.add(BlockFace.EAST); - faces.add(BlockFace.EAST); - faces.add(BlockFace.SOUTH); - break; - case NORTH: - faces.add(BlockFace.NORTH); - break; - case NORTH_EAST: - faces.add(BlockFace.NORTH); - faces.add(BlockFace.EAST); - break; - case NORTH_NORTH_EAST: - faces.add(BlockFace.NORTH); - faces.add(BlockFace.NORTH); - faces.add(BlockFace.EAST); - break; - case NORTH_NORTH_WEST: - faces.add(BlockFace.NORTH); - faces.add(BlockFace.NORTH); - faces.add(BlockFace.WEST); - break; - case NORTH_WEST: - faces.add(BlockFace.NORTH); - faces.add(BlockFace.WEST); - break; - case SELF: - faces.add(BlockFace.SELF); - break; - case SOUTH: - faces.add(BlockFace.SOUTH); - break; - case SOUTH_EAST: - faces.add(BlockFace.SOUTH); - faces.add(BlockFace.EAST); - break; - case SOUTH_SOUTH_EAST: - faces.add(BlockFace.SOUTH); - faces.add(BlockFace.SOUTH); - faces.add(BlockFace.EAST); - break; - case SOUTH_SOUTH_WEST: - faces.add(BlockFace.SOUTH); - faces.add(BlockFace.SOUTH); - faces.add(BlockFace.WEST); - break; - case SOUTH_WEST: - faces.add(BlockFace.SOUTH); - faces.add(BlockFace.WEST); - break; - case UP: - faces.add(BlockFace.UP); - break; - case WEST: - faces.add(BlockFace.WEST); - break; - case WEST_NORTH_WEST: - faces.add(BlockFace.WEST); - faces.add(BlockFace.WEST); - faces.add(BlockFace.NORTH); - break; - case WEST_SOUTH_WEST: - faces.add(BlockFace.WEST); - faces.add(BlockFace.WEST); - faces.add(BlockFace.SOUTH); - break; - default: - break; - } - - return faces; - } - - /** - * Get a normalized vector going from a location to another - * - * @param from from here - * @param to to here - * @return the normalized vector direction - */ - public static Vector direction(Location from, Location to) { - return to.clone().subtract(from.clone()).toVector().normalize(); - } - - public static Vector directionNoNormal(Location from, Location to) { - return to.clone().subtract(from.clone()).toVector(); - } - - /** - * Get the vector direction from the yaw and pitch - * - * @param yaw the yaw - * @param pitch the pitch - * @return the vector - */ - public static Vector toVector(float yaw, float pitch) { - return new Vector(Math.cos(pitch) * Math.cos(yaw), Math.sin(pitch), Math.cos(pitch) * Math.sin(-yaw)); - } - - /** - * Add an impulse (force) to an entity - * - * @param e the entity - * @param v the vector - */ - public static void impulse(Entity e, Vector v) { - impulse(e, v, 1.0); - } - - /** - * Add an impulse (force) on an entity - * - * @param e the entity - * @param v the vector - * @param effectiveness the effectiveness - */ - public static void impulse(Entity e, Vector v, double effectiveness) { - Vector vx = e.getVelocity(); - vx.add(v.clone().multiply(effectiveness)); - e.setVelocity(vx); - } - - /** - * Reverse a direction - * - * @param v the direction - * @return the reversed direction - */ - public static Vector reverse(Vector v) { - if (v.getX() != 0) { - v.setX(-v.getX()); - } - - if (v.getY() != 0) { - v.setY(-v.getY()); - } - - if (v.getZ() != 0) { - v.setZ(-v.getZ()); - } - - return v; - } - - /** - * Get a speed value from a vector (velocity) - * - * @param v the vector - * @return the speed - */ - public static double getSpeed(Vector v) { - Vector vi = new Vector(0, 0, 0); - Vector vt = new Vector(0, 0, 0).add(v); - - return vi.distance(vt); - } - - /** - * Shift all vectors based on the given vector - * - * @param vector the vector direction to shift the vectors - * @param vectors the vectors to be shifted - * @return the shifted vectors - */ - public static KList shift(Vector vector, KList vectors) { - return new KList<>(new GListAdapter() { - @Override - public Vector onAdapt(Vector from) { - return from.add(vector); - } - }.adapt(vectors)); - } - - /** - * Attempt to get the blockFace for the vector (will be tri-normalized) - * - * @param v the vector - * @return the block face or null - */ - public static BlockFace getBlockFace(Vector v) { - Vector p = triNormalize(v); - - for (BlockFace i : BlockFace.values()) { - if (p.getX() == i.getModX() && p.getY() == i.getModY() && p.getZ() == i.getModZ()) { - return i; - } - } - - for (BlockFace i : BlockFace.values()) { - if (p.getX() == i.getModX() && p.getZ() == i.getModZ()) { - return i; - } - } - - for (BlockFace i : BlockFace.values()) { - if (p.getY() == i.getModY() && p.getZ() == i.getModZ()) { - return i; - } - } - - for (BlockFace i : BlockFace.values()) { - if (p.getX() == i.getModX() || p.getY() == i.getModY()) { - return i; - } - } - - for (BlockFace i : BlockFace.values()) { - if (p.getX() == i.getModX() || p.getY() == i.getModY() || p.getZ() == i.getModZ()) { - return i; - } - } - - return null; - } - - /** - * Angle the vector in a self relative direction - * - * @param v the initial direction - * @param amt the amount to shift in the direction - * @return the shifted direction - */ - public static Vector angleLeft(Vector v, float amt) { - Location l = new Location(Bukkit.getWorlds().get(0), 0, 0, 0); - l.setDirection(v); - float y = l.getYaw(); - float p = l.getPitch(); - CDou cy = new CDou(360); - CDou cp = new CDou(180); - cy.set(y); - cp.set(p); - cy.sub(amt); - l.setYaw((float) cy.get()); - l.setPitch((float) cp.get()); - - return l.getDirection(); - } - - /** - * Angle the vector in a self relative direction - * - * @param v the initial direction - * @param amt the amount to shift in the direction - * @return the shifted direction - */ - public static Vector angleRight(Vector v, float amt) { - Location l = new Location(Bukkit.getWorlds().get(0), 0, 0, 0); - l.setDirection(v); - float y = l.getYaw(); - float p = l.getPitch(); - CDou cy = new CDou(360); - CDou cp = new CDou(180); - cy.set(y); - cp.set(p); - cy.add(amt); - l.setYaw((float) cy.get()); - l.setPitch((float) cp.get()); - - return l.getDirection(); - } - - /** - * Angle the vector in a self relative direction - * - * @param v the initial direction - * @param amt the amount to shift in the direction - * @return the shifted direction - */ - public static Vector angleUp(Vector v, float amt) { - Location l = new Location(Bukkit.getWorlds().get(0), 0, 0, 0); - l.setDirection(v); - float y = l.getYaw(); - float p = l.getPitch(); - CDou cy = new CDou(360); - cy.set(y); - l.setYaw((float) cy.get()); - l.setPitch(Math.max(-90, p - amt)); - - return l.getDirection(); - } - - /** - * Angle the vector in a self relative direction - * - * @param v the initial direction - * @param amt the amount to shift in the direction - * @return the shifted direction - */ - public static Vector angleDown(Vector v, float amt) { - Location l = new Location(Bukkit.getWorlds().get(0), 0, 0, 0); - l.setDirection(v); - float y = l.getYaw(); - float p = l.getPitch(); - CDou cy = new CDou(360); - cy.set(y); - l.setYaw((float) cy.get()); - l.setPitch(Math.min(90, p + amt)); - - return l.getDirection(); - } - - /** - * (clone) Force normalize the vector into three points, 1, 0, or -1. If the - * value is > 0.333 (1) if the value is less than -0.333 (-1) else 0 - * - * @param direction the direction - * @return the vector - */ - public static Vector triNormalize(Vector direction) { - Vector v = direction.clone(); - v.normalize(); - - if (v.getX() > 0.333) { - v.setX(1); - } else if (v.getX() < -0.333) { - v.setX(-1); - } else { - v.setX(0); - } - - if (v.getY() > 0.333) { - v.setY(1); - } else if (v.getY() < -0.333) { - v.setY(-1); - } else { - v.setY(0); - } - - if (v.getZ() > 0.333) { - v.setZ(1); - } else if (v.getZ() < -0.333) { - v.setZ(-1); - } else { - v.setZ(0); - } - - return v; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/IrisMatter.java b/core/src/main/java/com/volmit/iris/util/matter/IrisMatter.java deleted file mode 100644 index 233a9173e..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/IrisMatter.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.json.JSONObject; -import com.volmit.iris.util.plugin.VolmitSender; -import lombok.Getter; - -import java.util.Objects; - -public class IrisMatter extends IrisRegistrant implements Matter { - protected static final KMap, MatterSlice> slicers = buildSlicers(); - - @Getter - private final MatterHeader header; - - @Getter - private final int width; - - @Getter - private final int height; - - @Getter - private final int depth; - - @Getter - private final KMap, MatterSlice> sliceMap; - - public IrisMatter(int width, int height, int depth) { - if (width < 1 || height < 1 || depth < 1) { - throw new RuntimeException("Invalid Matter Size " + width + "x" + height + "x" + depth); - } - - this.width = width; - this.height = height; - this.depth = depth; - this.header = new MatterHeader(); - this.sliceMap = new KMap<>(); - } - - private static KMap, MatterSlice> buildSlicers() { - KMap, MatterSlice> c = new KMap<>(); - for (Object i : Iris.initialize("com.volmit.iris.util.matter.slices", Sliced.class)) { - MatterSlice s = (MatterSlice) i; - c.put(s.getType(), s); - } - - return c; - } - - @Override - @SuppressWarnings("unchecked") - public MatterSlice slice(Class c) { - return (MatterSlice) sliceMap.computeIfAbsent(c, $ -> Objects.requireNonNull(createSlice(c, this), "Bad slice " + c.getCanonicalName())); - } - - @Override - public MatterSlice createSlice(Class type, Matter m) { - MatterSlice slice = slicers.get(type); - - if (slice == null) { - return null; - } - - try { - return slice.getClass().getConstructor(int.class, int.class, int.class).newInstance(getWidth(), getHeight(), getDepth()); - } catch (Throwable e) { - e.printStackTrace(); - } - - return null; - } - - @Override - public String getFolderName() { - return "matter"; - } - - @Override - public String getTypeName() { - return "matter"; - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/Matter.java b/core/src/main/java/com/volmit/iris/util/matter/Matter.java deleted file mode 100644 index 384835bbf..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/Matter.java +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.IrisObject; -import com.volmit.iris.engine.object.IrisPosition; -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.io.CountingDataInputStream; -import com.volmit.iris.util.mantle.TectonicPlate; -import com.volmit.iris.util.math.BlockPosition; -import org.bukkit.World; -import org.bukkit.block.data.BlockData; -import org.bukkit.entity.Entity; -import org.bukkit.util.BlockVector; - -import java.io.*; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - -/** - * When Red Matter isn't enough - *

- * UVI width - * UVI height - * UVI depth - * UVI sliceCount - * UTF author - * UVL createdAt - * UVI version - * UTF sliceType (canonical class name) - * UVI nodeCount (for each slice) - * UVI position [(z * w * h) + (y * w) + x] - * ??? nodeData - */ -public interface Matter { - int VERSION = 1; - - static long convert(File folder) { - if (folder.isDirectory()) { - long v = 0; - - for (File i : folder.listFiles()) { - v += convert(i); - } - - return v; - } else { - IrisObject object = new IrisObject(1, 1, 1); - try { - long fs = folder.length(); - object.read(folder); - Matter.from(object).write(folder); - Iris.info("Converted " + folder.getPath() + " Saved " + (fs - folder.length())); - } catch (Throwable e) { - Iris.error("Failed to convert " + folder.getPath()); - e.printStackTrace(); - } - } - - return 0; - } - - static Matter from(IrisObject object) { - object.clean(); - object.shrinkwrap(); - BlockVector min = new BlockVector(); - Matter m = new IrisMatter(Math.max(object.getW(), 1) + 1, Math.max(object.getH(), 1) + 1, Math.max(object.getD(), 1) + 1); - - for (BlockVector i : object.getBlocks().keys()) { - min.setX(Math.min(min.getX(), i.getX())); - min.setY(Math.min(min.getY(), i.getY())); - min.setZ(Math.min(min.getZ(), i.getZ())); - } - - for (BlockVector i : object.getBlocks().keys()) { - m.slice(BlockData.class).set(i.getBlockX() - min.getBlockX(), i.getBlockY() - min.getBlockY(), i.getBlockZ() - min.getBlockZ(), object.getBlocks().get(i)); - } - - return m; - } - - static Matter read(File f) throws IOException { - try (var in = new FileInputStream(f)) { - return read(in); - } - } - - static Matter read(InputStream in) throws IOException { - return read(in, (b) -> new IrisMatter(b.getX(), b.getY(), b.getZ())); - } - - static Matter readDin(CountingDataInputStream in) throws IOException { - return readDin(in, (b) -> new IrisMatter(b.getX(), b.getY(), b.getZ())); - } - - /** - * Reads the input stream into a matter object using a matter factory. - * Does not close the input stream. Be a man, close it yourself. - * - * @param in the input stream - * @param matterFactory the matter factory (size) -> new MatterImpl(size); - * @return the matter object - * @throws IOException shit happens yo - */ - static Matter read(InputStream in, Function matterFactory) throws IOException { - return readDin(CountingDataInputStream.wrap(in), matterFactory); - } - - static Matter readDin(CountingDataInputStream din, Function matterFactory) throws IOException { - Matter matter = matterFactory.apply(new BlockPosition( - din.readInt(), - din.readInt(), - din.readInt())); - Iris.addPanic("read.matter.size", matter.getWidth() + "x" + matter.getHeight() + "x" + matter.getDepth()); - int sliceCount = din.readByte(); - Iris.addPanic("read.matter.slicecount", sliceCount + ""); - - matter.getHeader().read(din); - Iris.addPanic("read.matter.header", matter.getHeader().toString()); - - for (int i = 0; i < sliceCount; i++) { - long size = din.readInt(); - if (size == 0) continue; - long start = din.count(); - long end = start + size; - - Iris.addPanic("read.matter.slice", i + ""); - try { - String cn = din.readUTF(); - Iris.addPanic("read.matter.slice.class", cn); - - Class type = Class.forName(cn); - MatterSlice slice = matter.createSlice(type, matter); - slice.read(din); - if (din.count() < end) throw new IOException("Matter slice read size mismatch!"); - matter.putSlice(type, slice); - } catch (Throwable e) { - if (!(e instanceof ClassNotFoundException)) { - Iris.error("Failed to read matter slice, skipping it."); - Iris.addPanic("read.byte.range", start + " " + end); - Iris.addPanic("read.byte.current", din.count() + ""); - Iris.reportError(e); - e.printStackTrace(); - Iris.panic(); - TectonicPlate.addError(); - } - din.skipTo(end); - } - - if (din.count() != end) { - throw new IOException("Matter slice read size mismatch!"); - } - } - - return matter; - } - - default Matter copy() { - Matter m = new IrisMatter(getWidth(), getHeight(), getDepth()); - getSliceMap().forEach((k, v) -> m.slice(k).forceInject(v)); - return m; - } - - /** - * Get the header information - * - * @return the header info - */ - MatterHeader getHeader(); - - /** - * Get the width of this matter - * - * @return the width - */ - int getWidth(); - - /** - * Get the height of this matter - * - * @return the height - */ - int getHeight(); - - /** - * Get the depth of this matter - * - * @return the depth - */ - int getDepth(); - - /** - * Get the center of this matter - * - * @return the center - */ - default BlockPosition getCenter() { - return new BlockPosition(getCenterX(), getCenterY(), getCenterZ()); - } - - /** - * Create a slice from the given type (full is false) - * - * @param type the type class - * @param matter the matter this slice will go into (size provider) - * @param the type - * @return the slice (or null if not supported) - */ - MatterSlice createSlice(Class type, Matter matter); - - /** - * Get the size of this matter - * - * @return the size - */ - default BlockPosition getSize() { - return new BlockPosition(getWidth(), getHeight(), getDepth()); - } - - /** - * Get the center X of this matter - * - * @return the center X - */ - default int getCenterX() { - return (int) Math.round(getWidth() / 2D); - } - - /** - * Get the center Y of this matter - * - * @return the center Y - */ - default int getCenterY() { - return (int) Math.round(getHeight() / 2D); - } - - /** - * Get the center Z of this matter - * - * @return the center Z - */ - default int getCenterZ() { - return (int) Math.round(getDepth() / 2D); - } - - /** - * Return the slice for the given type - * - * @param t the type class - * @param the type - * @return the slice or null - */ - default MatterSlice getSlice(Class t) { - return (MatterSlice) getSliceMap().get(t); - } - - /** - * Delete the slice for the given type - * - * @param c the type class - * @param the type - * @return the deleted slice, or null if it diddn't exist - */ - default MatterSlice deleteSlice(Class c) { - return (MatterSlice) getSliceMap().remove(c); - } - - /** - * Put a given slice type - * - * @param c the slice type class - * @param slice the slice to assign to the type - * @param the slice type - * @return the overwritten slice if there was an existing slice of that type - */ - default MatterSlice putSlice(Class c, MatterSlice slice) { - return (MatterSlice) getSliceMap().put(c, slice); - } - - default Class getClass(Object w) { - Class c = w.getClass(); - - if (w instanceof World) { - c = World.class; - } else if (w instanceof BlockData) { - c = BlockData.class; - } else if (w instanceof Entity) { - c = Entity.class; - } - - return c; - } - - default MatterSlice slice(Class c) { - MatterSlice slice = (MatterSlice) getSlice(c); - if (slice == null) { - slice = (MatterSlice) createSlice(c, this); - - if (slice == null) { - try { - throw new RuntimeException("Bad slice " + c.getCanonicalName()); - } catch (Throwable e) { - e.printStackTrace(); - } - - return null; - } - - putSlice(c, slice); - } - - return slice; - } - - /** - * Rotate a matter object into a new object - * - * @param x the x rotation (degrees) - * @param y the y rotation (degrees) - * @param z the z rotation (degrees) - * @return the new rotated matter object - */ - default Matter rotate(double x, double y, double z) { - IrisPosition rs = Hunk.rotatedBounding(getWidth(), getHeight(), getDepth(), x, y, z); - Matter n = new IrisMatter(rs.getX(), rs.getY(), rs.getZ()); - n.getHeader().setAuthor(getHeader().getAuthor()); - n.getHeader().setCreatedAt(getHeader().getCreatedAt()); - - for (Class i : getSliceTypes()) { - getSlice(i).rotateSliceInto(n, x, y, z); - } - - return n; - } - - /** - * Check if a slice exists for a given type - * - * @param c the slice class type - * @return true if it exists - */ - default boolean hasSlice(Class c) { - return getSlice(c) != null; - } - - /** - * Remove all slices - */ - default void clearSlices() { - getSliceMap().clear(); - } - - /** - * Get the set backing the slice map keys (slice types) - * - * @return the slice types - */ - default Set> getSliceTypes() { - return getSliceMap().keySet(); - } - - /** - * Get all slices - * - * @return the real slice map - */ - Map, MatterSlice> getSliceMap(); - - default void write(File f) throws IOException { - OutputStream out = new FileOutputStream(f); - write(out); - out.close(); - } - - /** - * Remove any slices that are empty - */ - default void trimSlices() { - Set> drop = null; - - for (Class i : getSliceTypes()) { - if (getSlice(i).getEntryCount() == 0) { - if (drop == null) { - drop = new KSet<>(); - } - - drop.add(i); - } - } - - if (drop != null) { - for (Class i : drop) { - deleteSlice(i); - } - } - } - - /** - * Writes the data to the output stream. The data will be flushed to the provided output - * stream however the provided stream will NOT BE CLOSED, so be sure to actually close it - * - * @param out the output stream - * @throws IOException shit happens yo - */ - default void write(OutputStream out) throws IOException { - writeDos(new DataOutputStream(out)); - } - - default void writeDos(DataOutputStream dos) throws IOException { - trimSlices(); - dos.writeInt(getWidth()); - dos.writeInt(getHeight()); - dos.writeInt(getDepth()); - dos.writeByte(getSliceTypes().size()); - getHeader().write(dos); - - var bytes = new ByteArrayOutputStream(1024); - var sub = new DataOutputStream(bytes); - for (Class i : getSliceTypes()) { - try { - getSlice(i).write(sub); - dos.writeInt(bytes.size()); - bytes.writeTo(dos); - } finally { - bytes.reset(); - } - } - } - - default int getTotalCount() { - int m = 0; - - for (MatterSlice i : getSliceMap().values()) { - m += i.getEntryCount(); - } - - return m; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterBiomeInject.java b/core/src/main/java/com/volmit/iris/util/matter/MatterBiomeInject.java deleted file mode 100644 index 038dad4ba..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterBiomeInject.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import lombok.AllArgsConstructor; -import lombok.Data; -import org.bukkit.block.Biome; - -@Data -@AllArgsConstructor -public class MatterBiomeInject { - private final boolean custom; - private final Integer biomeId; - private final Biome biome; -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterCavern.java b/core/src/main/java/com/volmit/iris/util/matter/MatterCavern.java deleted file mode 100644 index fc86d2cf9..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterCavern.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class MatterCavern { - private final boolean cavern; - private final String customBiome; - private final byte liquid; // 0 none 1 water 2 lava - - public boolean isAir() { - return liquid == 0; - } - - public boolean isWater() { - return liquid == 1; - } - - public boolean isLava() { - return liquid == 2; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterEntity.java b/core/src/main/java/com/volmit/iris/util/matter/MatterEntity.java deleted file mode 100644 index 52fb3958a..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterEntity.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import com.volmit.iris.util.nbt.tag.CompoundTag; -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class MatterEntity { - private final double xOff; - private final double yOff; - private final double zOff; - private final CompoundTag entityData; -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterEntityGroup.java b/core/src/main/java/com/volmit/iris/util/matter/MatterEntityGroup.java deleted file mode 100644 index c04ec7166..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterEntityGroup.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import com.volmit.iris.util.collection.KList; -import lombok.Data; - -@Data -public class MatterEntityGroup { - private final KList entities = new KList<>(); -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterFilter.java b/core/src/main/java/com/volmit/iris/util/matter/MatterFilter.java deleted file mode 100644 index 3f4dac01e..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -@FunctionalInterface -public interface MatterFilter { - T update(int x, int y, int z, T t); -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterFluidBody.java b/core/src/main/java/com/volmit/iris/util/matter/MatterFluidBody.java deleted file mode 100644 index 116b4e8da..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterFluidBody.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class MatterFluidBody { - private final boolean body; - private final String customBiome; - private final boolean lava; -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterHeader.java b/core/src/main/java/com/volmit/iris/util/matter/MatterHeader.java deleted file mode 100644 index 73fc78d16..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterHeader.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import com.volmit.iris.util.math.M; -import lombok.Data; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Data -public class MatterHeader { - private String author = ""; - private long createdAt = M.ms(); - private int version = Matter.VERSION; - - public void write(DataOutputStream out) throws IOException { - out.writeUTF(author); - out.writeLong(createdAt); - out.writeShort(version); - } - - public void read(DataInputStream din) throws IOException { - setAuthor(din.readUTF()); - setCreatedAt(din.readLong()); - setVersion(din.readShort()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterMarker.java b/core/src/main/java/com/volmit/iris/util/matter/MatterMarker.java deleted file mode 100644 index dab5ff6ac..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterMarker.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class MatterMarker { - private final String tag; -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterPalette.java b/core/src/main/java/com/volmit/iris/util/matter/MatterPalette.java deleted file mode 100644 index a06e636d1..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterPalette.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import com.volmit.iris.util.data.DataPalette; -import com.volmit.iris.util.data.IOAdapter; -import com.volmit.iris.util.data.Varint; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class MatterPalette implements IOAdapter { - private final MatterSlice slice; - private final DataPalette palette; - - public MatterPalette(MatterSlice slice) { - this.slice = slice; - palette = new DataPalette(); - } - - public MatterPalette(MatterSlice slice, DataInputStream din) throws IOException { - this.slice = slice; - palette = DataPalette.getPalette(this, din); - } - - public void writeNode(T t, DataOutputStream dos) throws IOException { - Varint.writeUnsignedVarInt(palette.getIndex(t), dos); - } - - public T readNode(DataInputStream din) throws IOException { - return palette.get(Varint.readUnsignedVarInt(din)); - } - - public void writePalette(DataOutputStream dos) throws IOException { - palette.write(this, dos); - } - - @Override - public void write(T t, DataOutputStream dos) throws IOException { - slice.writeNode(t, dos); - } - - @Override - public T read(DataInputStream din) throws IOException { - return slice.readNode(din); - } - - public void assign(T b) { - palette.getIndex(b); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterPlacer.java b/core/src/main/java/com/volmit/iris/util/matter/MatterPlacer.java deleted file mode 100644 index 55b9b1b13..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterPlacer.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import com.volmit.iris.util.mantle.Mantle; - -public interface MatterPlacer { - int getHeight(int x, int z, boolean ignoreFluid); - - Mantle getMantle(); - - default void set(int x, int y, int z, T t) { - getMantle().set(x, y, z, t); - } - - default T get(int x, int y, int z, Class t) { - return getMantle().get(x, y, z, t); - } - - default void set(int x, int y, int z, Matter matter) { - for (MatterSlice i : matter.getSliceMap().values()) { - set(x, y, z, i); - } - } - - default void set(int x, int y, int z, MatterSlice slice) { - getMantle().set(x, y, z, slice); - } - - default int getHeight(int x, int z) { - return getHeight(x, z, true); - } - - default int getHeightOrFluid(int x, int z) { - return getHeight(x, z, false); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterReader.java b/core/src/main/java/com/volmit/iris/util/matter/MatterReader.java deleted file mode 100644 index 4d0b6516c..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterReader.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -@FunctionalInterface -public interface MatterReader { - T readMatter(W w, int x, int y, int z); -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterSlice.java b/core/src/main/java/com/volmit/iris/util/matter/MatterSlice.java deleted file mode 100644 index 9f77edc86..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterSlice.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.util.data.Varint; -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.data.palette.PaletteType; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.hunk.bits.DataContainer; -import com.volmit.iris.util.hunk.bits.Writable; -import com.volmit.iris.util.hunk.storage.PaletteOrHunk; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.block.data.BlockData; -import org.bukkit.entity.Entity; -import org.bukkit.util.BlockVector; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public interface MatterSlice extends Hunk, PaletteType, Writable { - Class getType(); - - Palette getGlobalPalette(); - - @Override - default void writePaletteNode(DataOutputStream dos, T s) throws IOException { - writeNode(s, dos); - } - - @Override - default void writeNodeData(DataOutputStream dos, T s) throws IOException { - writeNode(s, dos); - } - - @Override - default T readPaletteNode(DataInputStream din) throws IOException { - return readNode(din); - } - - @Override - default T readNodeData(DataInputStream din) throws IOException { - return readNode(din); - } - - default void applyFilter(MatterFilter filter) { - updateSync(filter::update); - } - - default void inject(MatterSlice slice) { - iterateSync(slice::set); - } - - default void forceInject(MatterSlice slice) { - inject((MatterSlice) slice); - } - - void writeNode(T b, DataOutputStream dos) throws IOException; - - T readNode(DataInputStream din) throws IOException; - - MatterWriter writeInto(Class mediumType); - - MatterReader readFrom(Class mediumType); - - default Class getClass(Object w) { - Class c = w.getClass(); - - if (w instanceof World) { - c = World.class; - } else if (w instanceof BlockData) { - c = BlockData.class; - } else if (w instanceof Entity) { - c = Entity.class; - } - - return c; - } - - default boolean writeInto(Location location) { - return writeInto(location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ()); - } - - default boolean writeInto(W w, int x, int y, int z) { - MatterWriter injector = (MatterWriter) writeInto(getClass(w)); - - if (injector == null) { - return false; - } - - iterateSync((a, b, c, t) -> injector.writeMatter(w, t, a + x, b + y, c + z)); - - return true; - } - - default boolean readFrom(Location location) { - return readFrom(location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ()); - } - - default boolean readFrom(W w, int x, int y, int z) { - MatterReader ejector = (MatterReader) readFrom(getClass(w)); - - if (ejector == null) { - return false; - } - - for (int i = x; i < x + getWidth(); i++) { - for (int j = y; j < y + getHeight(); j++) { - for (int k = z; k < z + getDepth(); k++) { - T v = ejector.readMatter(w, i, j, k); - - if (v != null) { - set(i - x, j - y, k - z, v); - } - } - } - } - - return true; - } - - default boolean canWrite(Class mediumType) { - return writeInto(mediumType) != null; - } - - default boolean canRead(Class mediumType) { - return readFrom(mediumType) != null; - } - - default int getBitsPer(int needed) { - int target = 1; - for (int i = 1; i < 8; i++) { - if (Math.pow(2, i) > needed) { - target = i; - break; - } - } - - return target; - } - - default void write(DataOutputStream dos) throws IOException { - dos.writeUTF(getType().getCanonicalName()); - - if ((this instanceof PaletteOrHunk f && f.isPalette())) { - f.palette().writeDos(dos); - return; - } - - int w = getWidth(); - int h = getHeight(); - MatterPalette palette = new MatterPalette(this); - iterateSync((x, y, z, b) -> palette.assign(b)); - palette.writePalette(dos); - dos.writeBoolean(isMapped()); - - if (isMapped()) { - Varint.writeUnsignedVarInt(getEntryCount(), dos); - iterateSyncIO((x, y, z, b) -> { - Varint.writeUnsignedVarInt(Cache.to1D(x, y, z, w, h), dos); - palette.writeNode(b, dos); - }); - } else { - iterateSyncIO((x, y, z, b) -> palette.writeNode(b, dos)); - } - } - - default void read(DataInputStream din) throws IOException { - if ((this instanceof PaletteOrHunk f && f.isPalette())) { - f.setPalette(new DataContainer<>(din, this)); - return; - } - - int w = getWidth(); - int h = getHeight(); - MatterPalette palette = new MatterPalette(this, din); - if (din.readBoolean()) { - int nodes = Varint.readUnsignedVarInt(din); - int[] pos; - - while (nodes-- > 0) { - pos = Cache.to3D(Varint.readUnsignedVarInt(din), w, h); - setRaw(pos[0], pos[1], pos[2], palette.readNode(din)); - } - } else { - iterateSyncIO((x, y, z, b) -> setRaw(x, y, z, palette.readNode(din))); - } - } - - default void rotateSliceInto(Matter n, double x, double y, double z) { - rotate(x, y, z, (_x, _y, _z) -> n.slice(getType())); - } - - default boolean containsKey(BlockVector v) { - return get(v.getBlockX(), v.getBlockY(), v.getBlockZ()) != null; - } - - default void put(BlockVector v, T d) { - set(v.getBlockX(), v.getBlockY(), v.getBlockZ(), d); - } - - default T get(BlockVector v) { - return get(v.getBlockX(), v.getBlockY(), v.getBlockZ()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterStructurePOI.java b/core/src/main/java/com/volmit/iris/util/matter/MatterStructurePOI.java deleted file mode 100644 index 45f844fc7..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterStructurePOI.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.volmit.iris.util.matter; - -import lombok.Data; - -import java.util.Map; - -@Data -public class MatterStructurePOI { - - public static final MatterStructurePOI BURIED_TREASURE = new MatterStructurePOI("buried_treasure"); - - private static final MatterStructurePOI UNKNOWN = new MatterStructurePOI("unknown"); - private static final Map VALUES = Map.of( - "buried_treasure", BURIED_TREASURE - ); - - private final String type; - - public static MatterStructurePOI get(String id) { - MatterStructurePOI poi = VALUES.get(id); - return poi != null ? poi : new MatterStructurePOI(id); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterUpdate.java b/core/src/main/java/com/volmit/iris/util/matter/MatterUpdate.java deleted file mode 100644 index 10f2cb2bc..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterUpdate.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class MatterUpdate { - private final boolean update; -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/matter/MatterWriter.java b/core/src/main/java/com/volmit/iris/util/matter/MatterWriter.java deleted file mode 100644 index b4135cb5d..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/MatterWriter.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -@FunctionalInterface -public interface MatterWriter { - void writeMatter(W w, T d, int x, int y, int z); -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/Sliced.java b/core/src/main/java/com/volmit/iris/util/matter/Sliced.java deleted file mode 100644 index d310a1081..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/Sliced.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -public @interface Sliced { - -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/BiomeInjectMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/BiomeInjectMatter.java deleted file mode 100644 index 68879a545..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/BiomeInjectMatter.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.MatterBiomeInject; -import com.volmit.iris.util.matter.Sliced; -import org.bukkit.block.Biome; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class BiomeInjectMatter extends RawMatter { - public BiomeInjectMatter() { - this(1, 1, 1); - } - - public BiomeInjectMatter(int width, int height, int depth) { - super(width, height, depth, MatterBiomeInject.class); - } - - public static MatterBiomeInject get(Biome biome) { - return get(false, 0, biome); - } - - public static MatterBiomeInject get(int customBiome) { - return get(true, customBiome, null); - } - - public static MatterBiomeInject get(boolean custom, int customBiome, Biome biome) { - return new MatterBiomeInject(custom, customBiome, biome); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(MatterBiomeInject b, DataOutputStream dos) throws IOException { - dos.writeBoolean(b.isCustom()); - - if (b.isCustom()) { - dos.writeShort(b.getBiomeId()); - } else { - dos.writeByte(b.getBiome().ordinal()); - } - } - - @Override - public MatterBiomeInject readNode(DataInputStream din) throws IOException { - boolean b = din.readBoolean(); - int id = b ? din.readShort() : 0; - Biome biome = !b ? Biome.values()[din.readByte()] : Biome.PLAINS; - - return new MatterBiomeInject(b, id, biome); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/BlockMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/BlockMatter.java deleted file mode 100644 index 15afb4376..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/BlockMatter.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.data.IrisCustomData; -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.data.BlockData; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class BlockMatter extends RawMatter { - public static final BlockData AIR = Material.AIR.createBlockData(); - - public BlockMatter() { - this(1, 1, 1); - } - - public BlockMatter(int width, int height, int depth) { - super(width, height, depth, BlockData.class); - registerWriter(World.class, ((w, d, x, y, z) -> { - if (d instanceof IrisCustomData c) - w.getBlockAt(x, y, z).setBlockData(c.getBase()); - else w.getBlockAt(x, y, z).setBlockData(d); - })); - registerReader(World.class, (w, x, y, z) -> { - BlockData d = w.getBlockAt(x, y, z).getBlockData(); - return d.getMaterial().isAir() ? null : d; - }); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(BlockData b, DataOutputStream dos) throws IOException { - dos.writeUTF(b.getAsString(true)); - } - - @Override - public BlockData readNode(DataInputStream din) throws IOException { - return B.get(din.readUTF()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/BooleanMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/BooleanMatter.java deleted file mode 100644 index 643c953a5..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/BooleanMatter.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class BooleanMatter extends RawMatter { - public BooleanMatter() { - this(1, 1, 1); - } - - public BooleanMatter(int width, int height, int depth) { - super(width, height, depth, Boolean.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(Boolean b, DataOutputStream dos) throws IOException { - dos.writeBoolean(b); - } - - @Override - public Boolean readNode(DataInputStream din) throws IOException { - return din.readBoolean(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/CavernMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/CavernMatter.java deleted file mode 100644 index eb0acfd81..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/CavernMatter.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.MatterCavern; -import com.volmit.iris.util.matter.Sliced; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class CavernMatter extends RawMatter { - public static final MatterCavern EMPTY = new MatterCavern(false, "", (byte) 0); - public static final MatterCavern BASIC = new MatterCavern(true, "", (byte) 0); - - public CavernMatter() { - this(1, 1, 1); - } - - public CavernMatter(int width, int height, int depth) { - super(width, height, depth, MatterCavern.class); - } - - public static MatterCavern get(String customBiome, int liquid) { - return new MatterCavern(true, customBiome, (byte) liquid); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(MatterCavern b, DataOutputStream dos) throws IOException { - dos.writeBoolean(b.isCavern()); - dos.writeUTF(b.getCustomBiome()); - dos.writeByte(b.getLiquid()); - } - - @Override - public MatterCavern readNode(DataInputStream din) throws IOException { - boolean b = din.readBoolean(); - String v = din.readUTF(); - byte l = din.readByte(); - - return new MatterCavern(b, v, l); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/CompoundMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/CompoundMatter.java deleted file mode 100644 index 288f2d332..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/CompoundMatter.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.matter.Sliced; -import com.volmit.iris.util.nbt.tag.CompoundTag; - -@Sliced -public class CompoundMatter extends NBTMatter { - public static final CompoundTag EMPTY = new CompoundTag(); - - public CompoundMatter() { - this(1, 1, 1); - } - - public CompoundMatter(int width, int height, int depth) { - super(width, height, depth, CompoundTag.class, EMPTY); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/IdentifierMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/IdentifierMatter.java deleted file mode 100644 index 5777c6320..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/IdentifierMatter.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.core.link.Identifier; -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class IdentifierMatter extends RawMatter { - - public IdentifierMatter() { - this(1, 1, 1); - } - - public IdentifierMatter(int width, int height, int depth) { - super(width, height, depth, Identifier.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(Identifier b, DataOutputStream dos) throws IOException { - dos.writeUTF(b.toString()); - } - - @Override - public Identifier readNode(DataInputStream din) throws IOException { - return Identifier.fromString(din.readUTF()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/IntMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/IntMatter.java deleted file mode 100644 index c655cb158..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/IntMatter.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.Varint; -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class IntMatter extends RawMatter { - public IntMatter() { - this(1, 1, 1); - } - - public IntMatter(int width, int height, int depth) { - super(width, height, depth, Integer.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(Integer b, DataOutputStream dos) throws IOException { - Varint.writeSignedVarInt(b, dos); - } - - @Override - public Integer readNode(DataInputStream din) throws IOException { - return Varint.readSignedVarInt(din); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/JigsawPieceMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/JigsawPieceMatter.java deleted file mode 100644 index 05cc08385..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/JigsawPieceMatter.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; -import com.volmit.iris.util.matter.slices.container.JigsawPieceContainer; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class JigsawPieceMatter extends RawMatter { - public JigsawPieceMatter() { - this(1,1,1); - } - - public JigsawPieceMatter(int width, int height, int depth) { - super(width, height, depth, JigsawPieceContainer.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(JigsawPieceContainer b, DataOutputStream dos) throws IOException { - dos.writeUTF(b.getLoadKey()); - } - - @Override - public JigsawPieceContainer readNode(DataInputStream din) throws IOException { - return new JigsawPieceContainer(din.readUTF()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/JigsawStructureMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/JigsawStructureMatter.java deleted file mode 100644 index 0e573945e..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/JigsawStructureMatter.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; -import com.volmit.iris.util.matter.slices.container.JigsawStructureContainer; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class JigsawStructureMatter extends RawMatter { - public JigsawStructureMatter() { - this(1,1,1); - } - - public JigsawStructureMatter(int width, int height, int depth) { - super(width, height, depth, JigsawStructureContainer.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(JigsawStructureContainer b, DataOutputStream dos) throws IOException { - dos.writeUTF(b.getLoadKey()); - } - - @Override - public JigsawStructureContainer readNode(DataInputStream din) throws IOException { - return new JigsawStructureContainer(din.readUTF()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/JigsawStructuresMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/JigsawStructuresMatter.java deleted file mode 100644 index 55803b946..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/JigsawStructuresMatter.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; -import com.volmit.iris.util.matter.slices.container.JigsawStructuresContainer; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class JigsawStructuresMatter extends RawMatter { - public JigsawStructuresMatter() { - this(1, 1, 1); - } - - public JigsawStructuresMatter(int width, int height, int depth) { - super(width, height, depth, JigsawStructuresContainer.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(JigsawStructuresContainer b, DataOutputStream dos) throws IOException { - b.write(dos); - } - - @Override - public JigsawStructuresContainer readNode(DataInputStream din) throws IOException { - return new JigsawStructuresContainer(din); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/LongMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/LongMatter.java deleted file mode 100644 index 3cd175015..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/LongMatter.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.Varint; -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class LongMatter extends RawMatter { - public LongMatter() { - this(1, 1, 1); - } - - public LongMatter(int width, int height, int depth) { - super(width, height, depth, Long.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(Long b, DataOutputStream dos) throws IOException { - Varint.writeSignedVarLong(b, dos); - } - - @Override - public Long readNode(DataInputStream din) throws IOException { - return Varint.readSignedVarLong(din); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/MarkerMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/MarkerMatter.java deleted file mode 100644 index 60e44edbd..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/MarkerMatter.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.MatterMarker; -import com.volmit.iris.util.matter.Sliced; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class MarkerMatter extends RawMatter { - public static final MatterMarker NONE = new MatterMarker("none"); - public static final MatterMarker CAVE_FLOOR = new MatterMarker("cave_floor"); - public static final MatterMarker CAVE_CEILING = new MatterMarker("cave_ceiling"); - private static final KMap markers = new KMap<>(); - - public MarkerMatter() { - this(1, 1, 1); - } - - public MarkerMatter(int width, int height, int depth) { - super(width, height, depth, MatterMarker.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(MatterMarker b, DataOutputStream dos) throws IOException { - dos.writeUTF(b.getTag()); - } - - @Override - public MatterMarker readNode(DataInputStream din) throws IOException { - return markers.computeIfAbsent(din.readUTF(), MatterMarker::new); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/NBTMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/NBTMatter.java deleted file mode 100644 index 81bd0f6bb..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/NBTMatter.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.nbt.io.NBTUtil; -import com.volmit.iris.util.nbt.tag.Tag; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class NBTMatter> extends RawMatter { - public NBTMatter(int width, int height, int depth, Class c, T e) { - super(width, height, depth, c); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(T b, DataOutputStream dos) throws IOException { - NBTUtil.write(b, dos, false); - } - - @Override - public T readNode(DataInputStream din) throws IOException { - return (T) NBTUtil.read(din, false).getTag(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/RawMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/RawMatter.java deleted file mode 100644 index 746b2e338..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/RawMatter.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.hunk.storage.MappedHunk; -import com.volmit.iris.util.hunk.storage.PaletteOrHunk; -import com.volmit.iris.util.matter.MatterReader; -import com.volmit.iris.util.matter.MatterSlice; -import com.volmit.iris.util.matter.MatterWriter; -import lombok.Getter; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public abstract class RawMatter extends PaletteOrHunk implements MatterSlice { - protected final KMap, MatterWriter> writers; - protected final KMap, MatterReader> readers; - @Getter - private final Class type; - - public RawMatter(int width, int height, int depth, Class type) { - super(width, height, depth, true, () -> new MappedHunk<>(width, height, depth)); - writers = new KMap<>(); - readers = new KMap<>(); - this.type = type; - } - - protected void registerWriter(Class mediumType, MatterWriter injector) { - writers.put(mediumType, injector); - } - - protected void registerReader(Class mediumType, MatterReader injector) { - readers.put(mediumType, injector); - } - - @Override - public MatterWriter writeInto(Class mediumType) { - return (MatterWriter) writers.get(mediumType); - } - - @Override - public MatterReader readFrom(Class mediumType) { - return (MatterReader) readers.get(mediumType); - } - - @Override - public abstract void writeNode(T b, DataOutputStream dos) throws IOException; - - @Override - public abstract T readNode(DataInputStream din) throws IOException; -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/RegistryMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/RegistryMatter.java deleted file mode 100644 index d61773049..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/RegistryMatter.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.core.loader.IrisRegistrant; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.data.palette.Palette; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class RegistryMatter extends RawMatter { - public RegistryMatter(int width, int height, int depth, Class c, T e) { - super(width, height, depth, c); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(T b, DataOutputStream dos) throws IOException { - dos.writeUTF(b.getLoadKey()); - } - - @Override - public T readNode(DataInputStream din) throws IOException { - IrisContext context = IrisContext.get(); - return (T) context.getData().getLoaders().get(getType()).load(din.readUTF()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/StringMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/StringMatter.java deleted file mode 100644 index 5039bd96b..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/StringMatter.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.Sliced; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class StringMatter extends RawMatter { - public StringMatter() { - this(1, 1, 1); - } - - public StringMatter(int width, int height, int depth) { - super(width, height, depth, String.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(String b, DataOutputStream dos) throws IOException { - dos.writeUTF(b); - } - - @Override - public String readNode(DataInputStream din) throws IOException { - return din.readUTF(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/StructurePOIMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/StructurePOIMatter.java deleted file mode 100644 index a91dc172d..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/StructurePOIMatter.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.MatterStructurePOI; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class StructurePOIMatter extends RawMatter { - - public StructurePOIMatter() { - super(1, 1, 1, MatterStructurePOI.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(MatterStructurePOI b, DataOutputStream dos) throws IOException { - dos.writeUTF(b.getType()); - } - - @Override - public MatterStructurePOI readNode(DataInputStream din) throws IOException { - return MatterStructurePOI.get(din.readUTF()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/UpdateMatter.java b/core/src/main/java/com/volmit/iris/util/matter/slices/UpdateMatter.java deleted file mode 100644 index 3c1b7ba6c..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/UpdateMatter.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.matter.slices; - -import com.volmit.iris.util.data.palette.GlobalPalette; -import com.volmit.iris.util.data.palette.Palette; -import com.volmit.iris.util.matter.MatterUpdate; -import com.volmit.iris.util.matter.Sliced; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public class UpdateMatter extends RawMatter { - public static final MatterUpdate ON = new MatterUpdate(true); - public static final MatterUpdate OFF = new MatterUpdate(false); - private static final Palette GLOBAL = new GlobalPalette<>(OFF, ON); - - public UpdateMatter() { - this(1, 1, 1); - } - - public UpdateMatter(int width, int height, int depth) { - super(width, height, depth, MatterUpdate.class); - } - - @Override - public Palette getGlobalPalette() { - return GLOBAL; - } - - @Override - public void writeNode(MatterUpdate b, DataOutputStream dos) throws IOException { - dos.writeBoolean(b.isUpdate()); - } - - @Override - public MatterUpdate readNode(DataInputStream din) throws IOException { - return din.readBoolean() ? ON : OFF; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/container/JigsawPieceContainer.java b/core/src/main/java/com/volmit/iris/util/matter/slices/container/JigsawPieceContainer.java deleted file mode 100644 index 08e569d5f..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/container/JigsawPieceContainer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.volmit.iris.util.matter.slices.container; - -import com.volmit.iris.engine.object.IrisJigsawPiece; - -public class JigsawPieceContainer extends RegistrantContainer { - public JigsawPieceContainer(String loadKey) { - super(IrisJigsawPiece.class, loadKey); - } - - public static JigsawPieceContainer toContainer(IrisJigsawPiece piece) { - return new JigsawPieceContainer(piece.getLoadKey()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/container/JigsawStructureContainer.java b/core/src/main/java/com/volmit/iris/util/matter/slices/container/JigsawStructureContainer.java deleted file mode 100644 index bd581ede3..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/container/JigsawStructureContainer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.volmit.iris.util.matter.slices.container; - -import com.volmit.iris.engine.object.IrisJigsawStructure; - -public class JigsawStructureContainer extends RegistrantContainer { - public JigsawStructureContainer(String loadKey) { - super(IrisJigsawStructure.class, loadKey); - } - - public static JigsawStructureContainer toContainer(IrisJigsawStructure structure) { - return new JigsawStructureContainer(structure.getLoadKey()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/container/JigsawStructuresContainer.java b/core/src/main/java/com/volmit/iris/util/matter/slices/container/JigsawStructuresContainer.java deleted file mode 100644 index 68d0f388f..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/container/JigsawStructuresContainer.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.volmit.iris.util.matter.slices.container; - -import com.volmit.iris.engine.object.IrisJigsawStructure; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.math.Position2; -import org.jetbrains.annotations.Unmodifiable; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class JigsawStructuresContainer { - private final Map> map = new KMap<>(); - - public JigsawStructuresContainer() {} - - public JigsawStructuresContainer(DataInputStream din) throws IOException { - int s0 = din.readInt(); - for (int i = 0; i < s0; i++) { - int s1 = din.readInt(); - KList list = new KList<>(s1); - for (int j = 0; j < s1; j++) { - list.add(new Position2(din.readInt(), din.readInt())); - } - map.put(din.readUTF(), list); - } - } - - public void write(DataOutputStream dos) throws IOException { - dos.writeInt(map.size()); - for (String key : map.keySet()) { - List list = map.get(key); - dos.writeInt(list.size()); - for (Position2 pos : list) { - dos.writeInt(pos.getX()); - dos.writeInt(pos.getZ()); - } - dos.writeUTF(key); - } - } - - @Unmodifiable - public Set getStructures() { - return Collections.unmodifiableSet(map.keySet()); - } - - @Unmodifiable - public List getPositions(String structure) { - return Collections.unmodifiableList(map.get(structure)); - } - - @ChunkCoordinates - public void add(IrisJigsawStructure structure, Position2 pos) { - map.computeIfAbsent(structure.getLoadKey(), k -> new KList<>()).add(pos); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/matter/slices/container/RegistrantContainer.java b/core/src/main/java/com/volmit/iris/util/matter/slices/container/RegistrantContainer.java deleted file mode 100644 index 0ab161e57..000000000 --- a/core/src/main/java/com/volmit/iris/util/matter/slices/container/RegistrantContainer.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.volmit.iris.util.matter.slices.container; - -import com.volmit.iris.core.loader.IrisData; -import com.volmit.iris.core.loader.IrisRegistrant; - -public abstract class RegistrantContainer { - private final Class type; - private final String loadKey; - - public RegistrantContainer(Class type, String loadKey) { - this.type = type; - this.loadKey = loadKey; - } - - public T load(IrisData data) { - return (T) data.getLoaders().get(type).load(loadKey); - } - - public String getLoadKey() { - return loadKey; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/misc/ServerProperties.java b/core/src/main/java/com/volmit/iris/util/misc/ServerProperties.java deleted file mode 100644 index fb9a31c73..000000000 --- a/core/src/main/java/com/volmit/iris/util/misc/ServerProperties.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.volmit.iris.util.misc; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Properties; - -public class ServerProperties { - public static final Properties DATA = new Properties(); - public static final File SERVER_PROPERTIES; - public static final File BUKKIT_YML; - - public static final String LEVEL_NAME; - - static { - String[] args = ProcessHandle.current() - .info() - .arguments() - .orElse(new String[0]); - - String propertiesPath = "server.properties"; - String bukkitYml = "bukkit.yml"; - String levelName = null; - - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - String next = i < args.length - 1 ? args[i + 1] : null; - - propertiesPath = parse(arg, next, propertiesPath, "-c", "--config"); - bukkitYml = parse(arg, next, bukkitYml, "-b", "--bukkit-settings"); - levelName = parse(arg, next, levelName, "-w", "--level-name", "--world"); - } - - SERVER_PROPERTIES = new File(propertiesPath); - BUKKIT_YML = new File(bukkitYml); - try (FileInputStream in = new FileInputStream(SERVER_PROPERTIES)){ - DATA.load(in); - } catch (IOException e) { - throw new RuntimeException(e); - } - - if (levelName != null) LEVEL_NAME = levelName; - else LEVEL_NAME = DATA.getProperty("level-name", "world"); - } - - private static String parse( - @NotNull String current, - @Nullable String next, - String fallback, - @NotNull String @NotNull ... keys - ) { - for (String k : keys) { - if (current.equals(k) && next != null) - return next; - if (current.startsWith(k + "=") && current.length() > k.length() + 1) - return current.substring(k.length() + 1); - } - return fallback; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/misc/SlimJar.java b/core/src/main/java/com/volmit/iris/util/misc/SlimJar.java deleted file mode 100644 index 4cee3df77..000000000 --- a/core/src/main/java/com/volmit/iris/util/misc/SlimJar.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.volmit.iris.util.misc; - -import com.volmit.iris.Iris; -import io.github.slimjar.app.builder.ApplicationBuilder; -import io.github.slimjar.app.builder.SpigotApplicationBuilder; -import io.github.slimjar.injector.loader.factory.InjectableFactory; -import io.github.slimjar.logging.ProcessLogger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantLock; - -import static com.volmit.iris.Iris.instance; - -public class SlimJar { - private static final boolean DEBUG = Boolean.getBoolean("iris.debug-slimjar"); - private static final boolean DISABLE_REMAPPER = Boolean.getBoolean("iris.disable-remapper"); - - private static final ReentrantLock lock = new ReentrantLock(); - private static final AtomicBoolean loaded = new AtomicBoolean(); - - public static void load() { - if (loaded.get()) return; - lock.lock(); - - try { - if (loaded.getAndSet(true)) return; - final var downloadPath = instance.getDataFolder("cache", "libraries").toPath(); - final var logger = instance.getLogger(); - - logger.info("Loading libraries..."); - try { - new SpigotApplicationBuilder(instance) - .downloadDirectoryPath(downloadPath) - .debug(DEBUG) - .remap(!DISABLE_REMAPPER) - .build(); - } catch (Throwable e) { - Iris.warn("Failed to inject the library loader, falling back to application builder"); - ApplicationBuilder.appending(instance.getName()) - .injectableFactory(InjectableFactory.selecting(InjectableFactory.ERROR, InjectableFactory.INJECTABLE, InjectableFactory.WRAPPED, InjectableFactory.UNSAFE)) - .downloadDirectoryPath(downloadPath) - .logger(new ProcessLogger() { - @Override - public void info(@NotNull String message, @Nullable Object... args) { - if (!DEBUG) return; - instance.getLogger().info(message.formatted(args)); - } - - @Override - public void error(@NotNull String message, @Nullable Object... args) { - instance.getLogger().severe(message.formatted(args)); - } - - @Override - public void debug(@NotNull String message, @Nullable Object... args) { - if (!DEBUG) return; - instance.getLogger().info(message.formatted(args)); - } - }) - .build(); - } - logger.info("Libraries loaded successfully!"); - } finally { - lock.unlock(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTDeserializer.java b/core/src/main/java/com/volmit/iris/util/nbt/io/NBTDeserializer.java deleted file mode 100644 index b76d2c418..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTDeserializer.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.engine.data.io.Deserializer; -import com.volmit.iris.util.nbt.tag.Tag; - -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.GZIPInputStream; - -public class NBTDeserializer implements Deserializer { - - private final boolean compressed; - - public NBTDeserializer() { - this(true); - } - - public NBTDeserializer(boolean compressed) { - this.compressed = compressed; - } - - @Override - public NamedTag fromStream(InputStream stream) throws IOException { - NBTInputStream nbtIn; - if (compressed) { - nbtIn = new NBTInputStream(new GZIPInputStream(stream)); - } else { - nbtIn = new NBTInputStream(stream); - } - return nbtIn.readTag(Tag.DEFAULT_MAX_DEPTH); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTInputStream.java b/core/src/main/java/com/volmit/iris/util/nbt/io/NBTInputStream.java deleted file mode 100644 index 7a6dd167b..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTInputStream.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.engine.data.io.ExceptionBiFunction; -import com.volmit.iris.engine.data.io.MaxDepthIO; -import com.volmit.iris.util.nbt.tag.*; - -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; - -public class NBTInputStream extends DataInputStream implements MaxDepthIO { - - private static final Map, IOException>> readers = new HashMap<>(); - private static final Map> idClassMapping = new HashMap<>(); - - static { - put(EndTag.ID, (i, d) -> EndTag.INSTANCE, EndTag.class); - put(ByteTag.ID, (i, d) -> readByte(i), ByteTag.class); - put(ShortTag.ID, (i, d) -> readShort(i), ShortTag.class); - put(IntTag.ID, (i, d) -> readInt(i), IntTag.class); - put(LongTag.ID, (i, d) -> readLong(i), LongTag.class); - put(FloatTag.ID, (i, d) -> readFloat(i), FloatTag.class); - put(DoubleTag.ID, (i, d) -> readDouble(i), DoubleTag.class); - put(ByteArrayTag.ID, (i, d) -> readByteArray(i), ByteArrayTag.class); - put(StringTag.ID, (i, d) -> readString(i), StringTag.class); - put(ListTag.ID, NBTInputStream::readListTag, ListTag.class); - put(CompoundTag.ID, NBTInputStream::readCompound, CompoundTag.class); - put(IntArrayTag.ID, (i, d) -> readIntArray(i), IntArrayTag.class); - put(LongArrayTag.ID, (i, d) -> readLongArray(i), LongArrayTag.class); - } - - public NBTInputStream(InputStream in) { - super(in); - } - - private static void put(byte id, ExceptionBiFunction, IOException> reader, Class clazz) { - readers.put(id, reader); - idClassMapping.put(id, clazz); - } - - private static ByteTag readByte(NBTInputStream in) throws IOException { - return new ByteTag(in.readByte()); - } - - private static ShortTag readShort(NBTInputStream in) throws IOException { - return new ShortTag(in.readShort()); - } - - private static IntTag readInt(NBTInputStream in) throws IOException { - return new IntTag(in.readInt()); - } - - private static LongTag readLong(NBTInputStream in) throws IOException { - return new LongTag(in.readLong()); - } - - private static FloatTag readFloat(NBTInputStream in) throws IOException { - return new FloatTag(in.readFloat()); - } - - private static DoubleTag readDouble(NBTInputStream in) throws IOException { - return new DoubleTag(in.readDouble()); - } - - private static StringTag readString(NBTInputStream in) throws IOException { - return new StringTag(in.readUTF()); - } - - private static ByteArrayTag readByteArray(NBTInputStream in) throws IOException { - ByteArrayTag bat = new ByteArrayTag(new byte[in.readInt()]); - in.readFully(bat.getValue()); - return bat; - } - - private static IntArrayTag readIntArray(NBTInputStream in) throws IOException { - int l = in.readInt(); - int[] data = new int[l]; - IntArrayTag iat = new IntArrayTag(data); - for (int i = 0; i < l; i++) { - data[i] = in.readInt(); - } - return iat; - } - - private static LongArrayTag readLongArray(NBTInputStream in) throws IOException { - int l = in.readInt(); - long[] data = new long[l]; - LongArrayTag iat = new LongArrayTag(data); - for (int i = 0; i < l; i++) { - data[i] = in.readLong(); - } - return iat; - } - - private static ListTag readListTag(NBTInputStream in, int maxDepth) throws IOException { - byte listType = in.readByte(); - ListTag list = ListTag.createUnchecked(idClassMapping.get(listType)); - int length = in.readInt(); - if (length < 0) { - length = 0; - } - for (int i = 0; i < length; i++) { - list.addUnchecked(in.readTag(listType, in.decrementMaxDepth(maxDepth))); - } - return list; - } - - private static CompoundTag readCompound(NBTInputStream in, int maxDepth) throws IOException { - CompoundTag comp = new CompoundTag(); - for (int id = in.readByte() & 0xFF; id != 0; id = in.readByte() & 0xFF) { - String key = in.readUTF(); - Tag element = in.readTag((byte) id, in.decrementMaxDepth(maxDepth)); - comp.put(key, element); - } - return comp; - } - - public NamedTag readTag(int maxDepth) throws IOException { - byte id = readByte(); - return new NamedTag(readUTF(), readTag(id, maxDepth)); - } - - public Tag readRawTag(int maxDepth) throws IOException { - byte id = readByte(); - return readTag(id, maxDepth); - } - - private Tag readTag(byte type, int maxDepth) throws IOException { - ExceptionBiFunction, IOException> f; - if ((f = readers.get(type)) == null) { - throw new IOException("invalid tag id \"" + type + "\""); - } - return f.accept(this, maxDepth); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTOutputStream.java b/core/src/main/java/com/volmit/iris/util/nbt/io/NBTOutputStream.java deleted file mode 100644 index 246549da5..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTOutputStream.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.engine.data.io.ExceptionTriConsumer; -import com.volmit.iris.engine.data.io.MaxDepthIO; -import com.volmit.iris.util.nbt.tag.*; - -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; - -public class NBTOutputStream extends DataOutputStream implements MaxDepthIO { - - private static final Map, Integer, IOException>> writers = new HashMap<>(); - private static final Map, Byte> classIdMapping = new HashMap<>(); - - static { - put(EndTag.ID, (o, t, d) -> { - }, EndTag.class); - put(ByteTag.ID, (o, t, d) -> writeByte(o, t), ByteTag.class); - put(ShortTag.ID, (o, t, d) -> writeShort(o, t), ShortTag.class); - put(IntTag.ID, (o, t, d) -> writeInt(o, t), IntTag.class); - put(LongTag.ID, (o, t, d) -> writeLong(o, t), LongTag.class); - put(FloatTag.ID, (o, t, d) -> writeFloat(o, t), FloatTag.class); - put(DoubleTag.ID, (o, t, d) -> writeDouble(o, t), DoubleTag.class); - put(ByteArrayTag.ID, (o, t, d) -> writeByteArray(o, t), ByteArrayTag.class); - put(StringTag.ID, (o, t, d) -> writeString(o, t), StringTag.class); - put(ListTag.ID, NBTOutputStream::writeList, ListTag.class); - put(CompoundTag.ID, NBTOutputStream::writeCompound, CompoundTag.class); - put(IntArrayTag.ID, (o, t, d) -> writeIntArray(o, t), IntArrayTag.class); - put(LongArrayTag.ID, (o, t, d) -> writeLongArray(o, t), LongArrayTag.class); - } - - public NBTOutputStream(OutputStream out) { - super(out); - } - - private static void put(byte id, ExceptionTriConsumer, Integer, IOException> f, Class clazz) { - writers.put(id, f); - classIdMapping.put(clazz, id); - } - - static byte idFromClass(Class clazz) { - Byte id = classIdMapping.get(clazz); - if (id == null) { - throw new IllegalArgumentException("unknown Tag class " + clazz.getName()); - } - return id; - } - - private static void writeByte(NBTOutputStream out, Tag tag) throws IOException { - out.writeByte(((ByteTag) tag).asByte()); - } - - private static void writeShort(NBTOutputStream out, Tag tag) throws IOException { - out.writeShort(((ShortTag) tag).asShort()); - } - - private static void writeInt(NBTOutputStream out, Tag tag) throws IOException { - out.writeInt(((IntTag) tag).asInt()); - } - - private static void writeLong(NBTOutputStream out, Tag tag) throws IOException { - out.writeLong(((LongTag) tag).asLong()); - } - - private static void writeFloat(NBTOutputStream out, Tag tag) throws IOException { - out.writeFloat(((FloatTag) tag).asFloat()); - } - - private static void writeDouble(NBTOutputStream out, Tag tag) throws IOException { - out.writeDouble(((DoubleTag) tag).asDouble()); - } - - private static void writeString(NBTOutputStream out, Tag tag) throws IOException { - out.writeUTF(((StringTag) tag).getValue()); - } - - private static void writeByteArray(NBTOutputStream out, Tag tag) throws IOException { - out.writeInt(((ByteArrayTag) tag).length()); - out.write(((ByteArrayTag) tag).getValue()); - } - - private static void writeIntArray(NBTOutputStream out, Tag tag) throws IOException { - out.writeInt(((IntArrayTag) tag).length()); - for (int i : ((IntArrayTag) tag).getValue()) { - out.writeInt(i); - } - } - - private static void writeLongArray(NBTOutputStream out, Tag tag) throws IOException { - out.writeInt(((LongArrayTag) tag).length()); - for (long l : ((LongArrayTag) tag).getValue()) { - out.writeLong(l); - } - } - - private static void writeList(NBTOutputStream out, Tag tag, int maxDepth) throws IOException { - out.writeByte(idFromClass(((ListTag) tag).getTypeClass())); - out.writeInt(((ListTag) tag).size()); - for (Tag t : ((ListTag) tag)) { - out.writeRawTag(t, out.decrementMaxDepth(maxDepth)); - } - } - - private static void writeCompound(NBTOutputStream out, Tag tag, int maxDepth) throws IOException { - for (Map.Entry> entry : (CompoundTag) tag) { - if (entry.getValue().getID() == 0) { - throw new IOException("end tag not allowed"); - } - out.writeByte(entry.getValue().getID()); - out.writeUTF(entry.getKey()); - out.writeRawTag(entry.getValue(), out.decrementMaxDepth(maxDepth)); - } - out.writeByte(0); - } - - public void writeTag(NamedTag tag, int maxDepth) throws IOException { - writeByte(tag.getTag().getID()); - if (tag.getTag().getID() != 0) { - writeUTF(tag.getName() == null ? "" : tag.getName()); - } - writeRawTag(tag.getTag(), maxDepth); - } - - public void writeTag(Tag tag, int maxDepth) throws IOException { - writeByte(tag.getID()); - if (tag.getID() != 0) { - writeUTF(""); - } - writeRawTag(tag, maxDepth); - } - - public void writeRawTag(Tag tag, int maxDepth) throws IOException { - ExceptionTriConsumer, Integer, IOException> f; - if ((f = writers.get(tag.getID())) == null) { - throw new IOException("invalid tag \"" + tag.getID() + "\""); - } - f.accept(this, tag, maxDepth); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTSerializer.java b/core/src/main/java/com/volmit/iris/util/nbt/io/NBTSerializer.java deleted file mode 100644 index 95773380b..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTSerializer.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.engine.data.io.Serializer; -import com.volmit.iris.util.nbt.tag.Tag; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.zip.GZIPOutputStream; - -public class NBTSerializer implements Serializer { - - private final boolean compressed; - - public NBTSerializer() { - this(true); - } - - public NBTSerializer(boolean compressed) { - this.compressed = compressed; - } - - @Override - public void toStream(NamedTag object, OutputStream out) throws IOException { - NBTOutputStream nbtOut; - if (compressed) { - nbtOut = new NBTOutputStream(new GZIPOutputStream(out, true)); - } else { - nbtOut = new NBTOutputStream(out); - } - nbtOut.writeTag(object, Tag.DEFAULT_MAX_DEPTH); - nbtOut.flush(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTUtil.java b/core/src/main/java/com/volmit/iris/util/nbt/io/NBTUtil.java deleted file mode 100644 index bbf5050d1..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/NBTUtil.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.util.nbt.tag.Tag; - -import java.io.*; -import java.util.zip.GZIPInputStream; - -public final class NBTUtil { - - private NBTUtil() { - } - - public static void write(NamedTag tag, File file, boolean compressed) throws IOException { - try (FileOutputStream fos = new FileOutputStream(file)) { - new NBTSerializer(compressed).toStream(tag, fos); - } - } - - public static void write(NamedTag tag, OutputStream out, boolean compressed) throws IOException { - new NBTSerializer(compressed).toStream(tag, out); - } - - public static void write(Tag tag, OutputStream out, boolean compressed) throws IOException { - write(new NamedTag(null, tag), out, compressed); - } - - public static void write(NamedTag tag, String file, boolean compressed) throws IOException { - write(tag, new File(file), compressed); - } - - public static void write(NamedTag tag, File file) throws IOException { - write(tag, file, true); - } - - public static void write(NamedTag tag, String file) throws IOException { - write(tag, new File(file), true); - } - - public static void write(Tag tag, File file, boolean compressed) throws IOException { - write(new NamedTag(null, tag), file, compressed); - } - - public static void write(Tag tag, String file, boolean compressed) throws IOException { - write(new NamedTag(null, tag), new File(file), compressed); - } - - public static void write(Tag tag, File file) throws IOException { - write(new NamedTag(null, tag), file, true); - } - - public static void write(Tag tag, String file) throws IOException { - write(new NamedTag(null, tag), new File(file), true); - } - - public static NamedTag read(File file, boolean compressed) throws IOException { - try (FileInputStream fis = new FileInputStream(file)) { - return new NBTDeserializer(compressed).fromStream(fis); - } - } - - public static NamedTag read(InputStream in, boolean compressed) throws IOException { - return new NBTDeserializer(compressed).fromStream(in); - } - - public static NamedTag read(String file, boolean compressed) throws IOException { - return read(new File(file), compressed); - } - - public static NamedTag read(File file) throws IOException { - try (FileInputStream fis = new FileInputStream(file)) { - return new NBTDeserializer(false).fromStream(detectDecompression(fis)); - } - } - - public static NamedTag read(String file) throws IOException { - return read(new File(file)); - } - - private static InputStream detectDecompression(InputStream is) throws IOException { - PushbackInputStream pbis = new PushbackInputStream(is, 2); - int signature = (pbis.read() & 0xFF) + (pbis.read() << 8); - pbis.unread(signature >> 8); - pbis.unread(signature & 0xFF); - if (signature == GZIPInputStream.GZIP_MAGIC) { - return new GZIPInputStream(pbis); - } - return pbis; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/NamedTag.java b/core/src/main/java/com/volmit/iris/util/nbt/io/NamedTag.java deleted file mode 100644 index 904617a71..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/NamedTag.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.util.nbt.tag.Tag; - -public class NamedTag { - - private String name; - private Tag tag; - - public NamedTag(String name, Tag tag) { - this.name = name; - this.tag = tag; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Tag getTag() { - return tag; - } - - public void setTag(Tag tag) { - this.tag = tag; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/ParseException.java b/core/src/main/java/com/volmit/iris/util/nbt/io/ParseException.java deleted file mode 100644 index 5ffa564b8..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/ParseException.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import java.io.IOException; - -public class ParseException extends IOException { - - public ParseException(String msg) { - super(msg); - } - - public ParseException(String msg, String value, int index) { - super(msg + " at: " + formatError(value, index)); - } - - private static String formatError(String value, int index) { - StringBuilder builder = new StringBuilder(); - int i = Math.min(value.length(), index); - if (i > 35) { - builder.append("..."); - } - builder.append(value, Math.max(0, i - 35), i); - builder.append("<--[HERE]"); - return builder.toString(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTDeserializer.java b/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTDeserializer.java deleted file mode 100644 index 8bccdf57e..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTDeserializer.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.engine.data.io.StringDeserializer; -import com.volmit.iris.util.nbt.tag.Tag; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.Reader; -import java.util.stream.Collectors; - -public class SNBTDeserializer implements StringDeserializer> { - - @Override - public Tag fromReader(Reader reader) throws IOException { - return fromReader(reader, Tag.DEFAULT_MAX_DEPTH); - } - - public Tag fromReader(Reader reader, int maxDepth) throws IOException { - BufferedReader bufferedReader; - if (reader instanceof BufferedReader) { - bufferedReader = (BufferedReader) reader; - } else { - bufferedReader = new BufferedReader(reader); - } - return SNBTParser.parse(bufferedReader.lines().collect(Collectors.joining()), maxDepth); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTParser.java b/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTParser.java deleted file mode 100644 index 03124dc12..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTParser.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.io.MaxDepthIO; -import com.volmit.iris.util.nbt.tag.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -public final class SNBTParser implements MaxDepthIO { - - private static final Pattern - FLOAT_LITERAL_PATTERN = Pattern.compile("^[-+]?(?:\\d+\\.?|\\d*\\.\\d+)(?:e[-+]?\\d+)?f$", Pattern.CASE_INSENSITIVE), - DOUBLE_LITERAL_PATTERN = Pattern.compile("^[-+]?(?:\\d+\\.?|\\d*\\.\\d+)(?:e[-+]?\\d+)?d$", Pattern.CASE_INSENSITIVE), - DOUBLE_LITERAL_NO_SUFFIX_PATTERN = Pattern.compile("^[-+]?(?:\\d+\\.|\\d*\\.\\d+)(?:e[-+]?\\d+)?$", Pattern.CASE_INSENSITIVE), - BYTE_LITERAL_PATTERN = Pattern.compile("^[-+]?\\d+b$", Pattern.CASE_INSENSITIVE), - SHORT_LITERAL_PATTERN = Pattern.compile("^[-+]?\\d+s$", Pattern.CASE_INSENSITIVE), - INT_LITERAL_PATTERN = Pattern.compile("^[-+]?\\d+$", Pattern.CASE_INSENSITIVE), - LONG_LITERAL_PATTERN = Pattern.compile("^[-+]?\\d+l$", Pattern.CASE_INSENSITIVE), - NUMBER_PATTERN = Pattern.compile("^[-+]?\\d+$"); - - private final StringPointer ptr; - - private SNBTParser(String string) { - this.ptr = new StringPointer(string); - } - - public static Tag parse(String string, int maxDepth) throws ParseException { - SNBTParser parser = new SNBTParser(string); - Tag tag = parser.parseAnything(maxDepth); - parser.ptr.skipWhitespace(); - if (parser.ptr.hasNext()) { - throw parser.ptr.parseException("invalid characters after end of snbt"); - } - return tag; - } - - public static Tag parse(String string) throws ParseException { - return parse(string, Tag.DEFAULT_MAX_DEPTH); - } - - private Tag parseAnything(int maxDepth) throws ParseException { - ptr.skipWhitespace(); - switch (ptr.currentChar()) { - case '{': - return parseCompoundTag(maxDepth); - case '[': - if (ptr.hasCharsLeft(2) && ptr.lookAhead(1) != '"' && ptr.lookAhead(2) == ';') { - return parseNumArray(); - } - return parseListTag(maxDepth); - } - return parseStringOrLiteral(); - } - - private Tag parseStringOrLiteral() throws ParseException { - ptr.skipWhitespace(); - if (ptr.currentChar() == '"') { - return new StringTag(ptr.parseQuotedString()); - } - String s = ptr.parseSimpleString(); - if (s.isEmpty()) { - throw new ParseException("expected non empty value"); - } - if (FLOAT_LITERAL_PATTERN.matcher(s).matches()) { - return new FloatTag(Float.parseFloat(s.substring(0, s.length() - 1))); - } else if (BYTE_LITERAL_PATTERN.matcher(s).matches()) { - try { - return new ByteTag(Byte.parseByte(s.substring(0, s.length() - 1))); - } catch (NumberFormatException ex) { - Iris.reportError(ex); - throw ptr.parseException("byte not in range: \"" + s.substring(0, s.length() - 1) + "\""); - } - } else if (SHORT_LITERAL_PATTERN.matcher(s).matches()) { - try { - return new ShortTag(Short.parseShort(s.substring(0, s.length() - 1))); - } catch (NumberFormatException ex) { - Iris.reportError(ex); - throw ptr.parseException("short not in range: \"" + s.substring(0, s.length() - 1) + "\""); - } - } else if (LONG_LITERAL_PATTERN.matcher(s).matches()) { - try { - return new LongTag(Long.parseLong(s.substring(0, s.length() - 1))); - } catch (NumberFormatException ex) { - Iris.reportError(ex); - throw ptr.parseException("long not in range: \"" + s.substring(0, s.length() - 1) + "\""); - } - } else if (INT_LITERAL_PATTERN.matcher(s).matches()) { - try { - return new IntTag(Integer.parseInt(s)); - } catch (NumberFormatException ex) { - Iris.reportError(ex); - throw ptr.parseException("int not in range: \"" + s.substring(0, s.length() - 1) + "\""); - } - } else if (DOUBLE_LITERAL_PATTERN.matcher(s).matches()) { - return new DoubleTag(Double.parseDouble(s.substring(0, s.length() - 1))); - } else if (DOUBLE_LITERAL_NO_SUFFIX_PATTERN.matcher(s).matches()) { - return new DoubleTag(Double.parseDouble(s)); - } else if ("true".equalsIgnoreCase(s)) { - return new ByteTag(true); - } else if ("false".equalsIgnoreCase(s)) { - return new ByteTag(false); - } - return new StringTag(s); - } - - private CompoundTag parseCompoundTag(int maxDepth) throws ParseException { - ptr.expectChar('{'); - - CompoundTag compoundTag = new CompoundTag(); - - ptr.skipWhitespace(); - while (ptr.hasNext() && ptr.currentChar() != '}') { - ptr.skipWhitespace(); - String key = ptr.currentChar() == '"' ? ptr.parseQuotedString() : ptr.parseSimpleString(); - if (key.isEmpty()) { - throw new ParseException("empty keys are not allowed"); - } - ptr.expectChar(':'); - - compoundTag.put(key, parseAnything(decrementMaxDepth(maxDepth))); - - if (!ptr.nextArrayElement()) { - break; - } - } - ptr.expectChar('}'); - return compoundTag; - } - - private ListTag parseListTag(int maxDepth) throws ParseException { - ptr.expectChar('['); - ptr.skipWhitespace(); - ListTag list = ListTag.createUnchecked(EndTag.class); - while (ptr.currentChar() != ']') { - Tag element = parseAnything(decrementMaxDepth(maxDepth)); - try { - list.addUnchecked(element); - } catch (IllegalArgumentException ex) { - Iris.reportError(ex); - throw ptr.parseException(ex.getMessage()); - } - if (!ptr.nextArrayElement()) { - break; - } - } - ptr.expectChar(']'); - return list; - } - - private ArrayTag parseNumArray() throws ParseException { - ptr.expectChar('['); - char arrayType = ptr.next(); - ptr.expectChar(';'); - ptr.skipWhitespace(); - switch (arrayType) { - case 'B': - return parseByteArrayTag(); - case 'I': - return parseIntArrayTag(); - case 'L': - return parseLongArrayTag(); - } - throw new ParseException("invalid array type '" + arrayType + "'"); - } - - private ByteArrayTag parseByteArrayTag() throws ParseException { - List byteList = new ArrayList<>(); - while (ptr.currentChar() != ']') { - String s = ptr.parseSimpleString(); - ptr.skipWhitespace(); - if (NUMBER_PATTERN.matcher(s).matches()) { - try { - byteList.add(Byte.parseByte(s)); - } catch (NumberFormatException ex) { - Iris.reportError(ex); - throw ptr.parseException("byte not in range: \"" + s + "\""); - } - } else { - throw ptr.parseException("invalid byte in ByteArrayTag: \"" + s + "\""); - } - if (!ptr.nextArrayElement()) { - break; - } - } - ptr.expectChar(']'); - byte[] bytes = new byte[byteList.size()]; - for (int i = 0; i < byteList.size(); i++) { - bytes[i] = byteList.get(i); - } - return new ByteArrayTag(bytes); - } - - private IntArrayTag parseIntArrayTag() throws ParseException { - List intList = new ArrayList<>(); - while (ptr.currentChar() != ']') { - String s = ptr.parseSimpleString(); - ptr.skipWhitespace(); - if (NUMBER_PATTERN.matcher(s).matches()) { - try { - intList.add(Integer.parseInt(s)); - } catch (NumberFormatException ex) { - Iris.reportError(ex); - throw ptr.parseException("int not in range: \"" + s + "\""); - } - } else { - throw ptr.parseException("invalid int in IntArrayTag: \"" + s + "\""); - } - if (!ptr.nextArrayElement()) { - break; - } - } - ptr.expectChar(']'); - return new IntArrayTag(intList.stream().mapToInt(i -> i).toArray()); - } - - private LongArrayTag parseLongArrayTag() throws ParseException { - List longList = new ArrayList<>(); - while (ptr.currentChar() != ']') { - String s = ptr.parseSimpleString(); - ptr.skipWhitespace(); - if (NUMBER_PATTERN.matcher(s).matches()) { - try { - longList.add(Long.parseLong(s)); - } catch (NumberFormatException ex) { - Iris.reportError(ex); - throw ptr.parseException("long not in range: \"" + s + "\""); - } - } else { - throw ptr.parseException("invalid long in LongArrayTag: \"" + s + "\""); - } - if (!ptr.nextArrayElement()) { - break; - } - } - ptr.expectChar(']'); - return new LongArrayTag(longList.stream().mapToLong(l -> l).toArray()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTSerializer.java b/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTSerializer.java deleted file mode 100644 index 0ae5250e7..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTSerializer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.engine.data.io.StringSerializer; -import com.volmit.iris.util.nbt.tag.Tag; - -import java.io.IOException; -import java.io.Writer; - -public class SNBTSerializer implements StringSerializer> { - - @Override - public void toWriter(Tag tag, Writer writer) throws IOException { - SNBTWriter.write(tag, writer); - } - - public void toWriter(Tag tag, Writer writer, int maxDepth) throws IOException { - SNBTWriter.write(tag, writer, maxDepth); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTUtil.java b/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTUtil.java deleted file mode 100644 index 250b8781f..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTUtil.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.util.nbt.tag.Tag; - -import java.io.IOException; - -public class SNBTUtil { - - public static String toSNBT(Tag tag) throws IOException { - return new SNBTSerializer().toString(tag); - } - - public static Tag fromSNBT(String string) throws IOException { - return new SNBTDeserializer().fromString(string); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTWriter.java b/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTWriter.java deleted file mode 100644 index adcf5a429..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/SNBTWriter.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -import com.volmit.iris.engine.data.io.MaxDepthIO; -import com.volmit.iris.util.nbt.tag.*; - -import java.io.IOException; -import java.io.Writer; -import java.lang.reflect.Array; -import java.util.Map; -import java.util.regex.Pattern; - -/** - * SNBTWriter creates an SNBT String. - */ -@SuppressWarnings("ClassCanBeRecord") -public final class SNBTWriter implements MaxDepthIO { - - private static final Pattern NON_QUOTE_PATTERN = Pattern.compile("[a-zA-Z_.+\\-]+"); - - private final Writer writer; - - private SNBTWriter(Writer writer) { - this.writer = writer; - } - - public static void write(Tag tag, Writer writer, int maxDepth) throws IOException { - new SNBTWriter(writer).writeAnything(tag, maxDepth); - } - - public static void write(Tag tag, Writer writer) throws IOException { - write(tag, writer, Tag.DEFAULT_MAX_DEPTH); - } - - public static String escapeString(String s) { - if (!NON_QUOTE_PATTERN.matcher(s).matches()) { - StringBuilder sb = new StringBuilder(); - sb.append('"'); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c == '\\' || c == '"') { - sb.append('\\'); - } - sb.append(c); - } - sb.append('"'); - return sb.toString(); - } - return s; - } - - private void writeAnything(Tag tag, int maxDepth) throws IOException { - switch (tag.getID()) { - case EndTag.ID: - //do nothing - break; - case ByteTag.ID: - writer.append(Byte.toString(((ByteTag) tag).asByte())).write('b'); - break; - case ShortTag.ID: - writer.append(Short.toString(((ShortTag) tag).asShort())).write('s'); - break; - case IntTag.ID: - writer.write(Integer.toString(((IntTag) tag).asInt())); - break; - case LongTag.ID: - writer.append(Long.toString(((LongTag) tag).asLong())).write('l'); - break; - case FloatTag.ID: - writer.append(Float.toString(((FloatTag) tag).asFloat())).write('f'); - break; - case DoubleTag.ID: - writer.append(Double.toString(((DoubleTag) tag).asDouble())).write('d'); - break; - case ByteArrayTag.ID: - writeArray(((ByteArrayTag) tag).getValue(), ((ByteArrayTag) tag).length(), "B"); - break; - case StringTag.ID: - writer.write(escapeString(((StringTag) tag).getValue())); - break; - case ListTag.ID: - writer.write('['); - for (int i = 0; i < ((ListTag) tag).size(); i++) { - writer.write(i == 0 ? "" : ","); - writeAnything(((ListTag) tag).get(i), decrementMaxDepth(maxDepth)); - } - writer.write(']'); - break; - case CompoundTag.ID: - writer.write('{'); - boolean first = true; - for (Map.Entry> entry : (CompoundTag) tag) { - writer.write(first ? "" : ","); - writer.append(escapeString(entry.getKey())).write(':'); - writeAnything(entry.getValue(), decrementMaxDepth(maxDepth)); - first = false; - } - writer.write('}'); - break; - case IntArrayTag.ID: - writeArray(((IntArrayTag) tag).getValue(), ((IntArrayTag) tag).length(), "I"); - break; - case LongArrayTag.ID: - writeArray(((LongArrayTag) tag).getValue(), ((LongArrayTag) tag).length(), "L"); - break; - default: - throw new IOException("unknown tag with id \"" + tag.getID() + "\""); - } - } - - private void writeArray(Object array, int length, String prefix) throws IOException { - writer.append('[').append(prefix).write(';'); - for (int i = 0; i < length; i++) { - writer.append(i == 0 ? "" : ",").write(Array.get(array, i).toString()); - } - writer.write(']'); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/io/StringPointer.java b/core/src/main/java/com/volmit/iris/util/nbt/io/StringPointer.java deleted file mode 100644 index 539b1d477..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/io/StringPointer.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.io; - -public class StringPointer { - - private final String value; - private int index; - - public StringPointer(String value) { - this.value = value; - } - - private static boolean isSimpleChar(char c) { - return c >= 'a' && c <= 'z' - || c >= 'A' && c <= 'Z' - || c >= '0' && c <= '9' - || c == '-' - || c == '+' - || c == '.' - || c == '_'; - } - - public String parseSimpleString() { - int oldIndex = index; - while (hasNext() && isSimpleChar(currentChar())) { - index++; - } - return value.substring(oldIndex, index); - } - - public String parseQuotedString() throws ParseException { - int oldIndex = ++index; //ignore beginning quotes - StringBuilder sb = null; - boolean escape = false; - while (hasNext()) { - char c = next(); - if (escape) { - if (c != '\\' && c != '"') { - throw parseException("invalid escape of '" + c + "'"); - } - escape = false; - } else { - if (c == '\\') { //escape - escape = true; - if (sb != null) { - continue; - } - sb = new StringBuilder(value.substring(oldIndex, index - 1)); - continue; - } - if (c == '"') { - return sb == null ? value.substring(oldIndex, index - 1) : sb.toString(); - } - } - if (sb != null) { - sb.append(c); - } - } - throw parseException("missing end quote"); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean nextArrayElement() { - skipWhitespace(); - if (hasNext() && currentChar() == ',') { - index++; - skipWhitespace(); - return true; - } - return false; - } - - public void expectChar(char c) throws ParseException { - skipWhitespace(); - boolean hasNext = hasNext(); - if (hasNext && currentChar() == c) { - index++; - return; - } - throw parseException("expected '" + c + "' but got " + (hasNext ? "'" + currentChar() + "'" : "EOF")); - } - - public void skipWhitespace() { - while (hasNext() && Character.isWhitespace(currentChar())) { - index++; - } - } - - public boolean hasNext() { - return index < value.length(); - } - - public boolean hasCharsLeft(int num) { - return this.index + num < value.length(); - } - - public char currentChar() { - return value.charAt(index); - } - - public char next() { - return value.charAt(index++); - } - - public void skip(int offset) { - index += offset; - } - - public char lookAhead(int offset) { - return value.charAt(index + offset); - } - - public ParseException parseException(String msg) { - return new ParseException(msg, value, index); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/Chunk.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/Chunk.java deleted file mode 100644 index f74cdcf7f..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/Chunk.java +++ /dev/null @@ -1,622 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.nbt.io.NBTDeserializer; -import com.volmit.iris.util.nbt.io.NBTSerializer; -import com.volmit.iris.util.nbt.io.NamedTag; -import com.volmit.iris.util.nbt.mca.palette.MCABiomeContainer; -import com.volmit.iris.util.nbt.tag.CompoundTag; -import com.volmit.iris.util.nbt.tag.ListTag; -import org.bukkit.World; - -import java.io.*; - -import static com.volmit.iris.util.nbt.mca.LoadFlags.*; -import static org.bukkit.Bukkit.getServer; - -public class Chunk { - public static final int DEFAULT_DATA_VERSION = 2730; - private final KMap sections = new KMap<>(); - private boolean partial; - private int lastMCAUpdate; - private CompoundTag data; - private int dataVersion; - private int nativeIrisVersion; - private long lastUpdate; - private long inhabitedTime; - private MCABiomeContainer biomes; - private CompoundTag heightMaps; - private CompoundTag carvingMasks; - private ListTag entities; - private ListTag tileEntities; - private ListTag tileTicks; - private ListTag liquidTicks; - private ListTag> lights; - private ListTag> liquidsToBeTicked; - private ListTag> toBeTicked; - private ListTag> postProcessing; - private String status; - private CompoundTag structures; - - Chunk(int lastMCAUpdate) { - this.lastMCAUpdate = lastMCAUpdate; - } - - /** - * Create a new chunk based on raw base data from a region file. - * - * @param data The raw base data to be used. - */ - public Chunk(CompoundTag data) { - this.data = data; - initReferences(ALL_DATA); - setStatus("full"); - } - - public static Chunk newChunk() { - World mainWorld = getServer().getWorlds().get(0); - Chunk c = new Chunk(0); - c.dataVersion = DEFAULT_DATA_VERSION; - c.data = new CompoundTag(); - c.biomes = INMS.get().newBiomeContainer(mainWorld.getMinHeight(), mainWorld.getMaxHeight()); - c.data.put("Level", defaultLevel()); - c.status = "full"; - return c; - } - - public static void injectIrisData(Chunk c) { - World mainWorld = getServer().getWorlds().get(0); - c.data.put("Iris", nativeIrisVersion()); - } - - private static CompoundTag defaultLevel() { - CompoundTag level = new CompoundTag(); - level.putString("Status", "full"); - level.putString("Generator", "Iris Headless " + Iris.instance.getDescription().getVersion()); - return level; - } - - private static CompoundTag nativeIrisVersion() { - CompoundTag level = new CompoundTag(); - level.putString("Generator", "Iris " + Iris.instance.getDescription().getVersion()); - return level; - } - - private void initReferences(long loadFlags) { - if (data == null) { - throw new NullPointerException("data cannot be null"); - } - - CompoundTag level = data; - - World mainWorld = getServer().getWorlds().get(0); - dataVersion = data.getInt("DataVersion"); - inhabitedTime = level.getLong("InhabitedTime"); - lastUpdate = level.getLong("LastUpdate"); - if ((loadFlags & BIOMES) != 0) { - biomes = INMS.get().newBiomeContainer(mainWorld.getMinHeight(), mainWorld.getMaxHeight(), level.getIntArray("Biomes")); - } - if ((loadFlags & HEIGHTMAPS) != 0) { - heightMaps = level.getCompoundTag("Heightmaps"); - } - if ((loadFlags & CARVING_MASKS) != 0) { - carvingMasks = level.getCompoundTag("CarvingMasks"); - } - if ((loadFlags & ENTITIES) != 0) { - entities = level.containsKey("Entities") ? level.getListTag("Entities").asCompoundTagList() : null; - } - if ((loadFlags & TILE_ENTITIES) != 0) { - tileEntities = level.containsKey("TileEntities") ? level.getListTag("TileEntities").asCompoundTagList() : null; - } - if ((loadFlags & TILE_TICKS) != 0) { - tileTicks = level.containsKey("TileTicks") ? level.getListTag("TileTicks").asCompoundTagList() : null; - } - if ((loadFlags & LIQUID_TICKS) != 0) { - liquidTicks = level.containsKey("LiquidTicks") ? level.getListTag("LiquidTicks").asCompoundTagList() : null; - } - if ((loadFlags & LIGHTS) != 0) { - lights = level.containsKey("Lights") ? level.getListTag("Lights").asListTagList() : null; - } - if ((loadFlags & LIQUIDS_TO_BE_TICKED) != 0) { - liquidsToBeTicked = level.containsKey("LiquidsToBeTicked") ? level.getListTag("LiquidsToBeTicked").asListTagList() : null; - } - if ((loadFlags & TO_BE_TICKED) != 0) { - toBeTicked = level.containsKey("ToBeTicked") ? level.getListTag("ToBeTicked").asListTagList() : null; - } - if ((loadFlags & POST_PROCESSING) != 0) { - postProcessing = level.containsKey("PostProcessing") ? level.getListTag("PostProcessing").asListTagList() : null; - } - status = level.getString("Status"); - if ((loadFlags & STRUCTURES) != 0) { - structures = level.getCompoundTag("Structures"); - } - if ((loadFlags & (BLOCK_LIGHTS | BLOCK_STATES | SKY_LIGHT)) != 0 && level.containsKey("Sections")) { - for (CompoundTag section : level.getListTag("Sections").asCompoundTagList()) { - int sectionIndex = section.getByte("Y"); - if (sectionIndex > 15 || sectionIndex < 0) { - continue; - } - Section newSection = new Section(section, dataVersion, loadFlags); - if (newSection.isEmpty()) { - continue; - } - sections.put(sectionIndex, newSection); - } - } - - // If we haven't requested the full set of data we can drop the underlying raw data to let the GC handle it. - if (loadFlags != ALL_DATA) { - data = null; - partial = true; - } else { - partial = false; - } - } - - /** - * Serializes this chunk to a RandomAccessFile. - * - * @param raf The RandomAccessFile to be written to. - * @param xPos The x-coordinate of the chunk. - * @param zPos The z-coodrinate of the chunk. - * @return The amount of bytes written to the RandomAccessFile. - * @throws UnsupportedOperationException When something went wrong during writing. - * @throws IOException When something went wrong during writing. - */ - public int serialize(RandomAccessFile raf, int xPos, int zPos) throws IOException { - if (partial) { - throw new UnsupportedOperationException("Partially loaded chunks cannot be serialized"); - } - ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); - try (BufferedOutputStream nbtOut = new BufferedOutputStream(CompressionType.ZLIB.compress(baos))) { - new NBTSerializer(false).toStream(new NamedTag(null, updateHandle(xPos, zPos)), nbtOut); - } - - byte[] rawData = baos.toByteArray(); - raf.writeInt(rawData.length + 1); // including the byte to store the compression type - raf.writeByte(CompressionType.ZLIB.getID()); - raf.write(rawData); - return rawData.length + 5; - } - - /** - * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. - * - * @param raf The RandomAccessFile to read the chunk data from. - * @throws IOException When something went wrong during reading. - */ - public void deserialize(RandomAccessFile raf) throws IOException { - deserialize(raf, ALL_DATA); - } - - /** - * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. - * - * @param raf The RandomAccessFile to read the chunk data from. - * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded - * @throws IOException When something went wrong during reading. - */ - public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { - byte compressionTypeByte = raf.readByte(); - CompressionType compressionType = CompressionType.getFromID(compressionTypeByte); - if (compressionType == null) { - throw new IOException("invalid compression type " + compressionTypeByte); - } - BufferedInputStream dis = new BufferedInputStream(compressionType.decompress(new FileInputStream(raf.getFD()))); - NamedTag tag = new NBTDeserializer(false).fromStream(dis); - if (tag != null && tag.getTag() instanceof CompoundTag) { - data = (CompoundTag) tag.getTag(); - initReferences(loadFlags); - } else { - throw new IOException("invalid data tag: " + (tag == null ? "null" : tag.getClass().getName())); - } - } - - /** - * Fetches a biome id at a specific block in this chunk. - * The coordinates can be absolute coordinates or relative to the region or chunk. - * - * @param blockX The x-coordinate of the block. - * @param blockY The y-coordinate of the block. - * @param blockZ The z-coordinate of the block. - * @return The biome id or -1 if the biomes are not correctly initialized. - */ - public synchronized int getBiomeAt(int blockX, int blockY, int blockZ) { - return biomes.getBiome(blockX, blockY, blockZ); - } - - /** - * Sets a biome id at a specific block column. - * The coordinates can be absolute coordinates or relative to the region or chunk. - * - * @param blockX The x-coordinate of the block column. - * @param blockZ The z-coordinate of the block column. - * @param biomeID The biome id to be set. - * When set to a negative number, Minecraft will replace it with the block column's default biome. - */ - public synchronized void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { - biomes.setBiome(blockX, blockY, blockZ, biomeID); - } - - int getBiomeIndex(int biomeX, int biomeY, int biomeZ) { - return biomeY * 64 + biomeZ * 4 + biomeX; - } - - public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { - int s = MCAUtil.blockToChunk(blockY); - - Section section = sections.get(s); - if (section == null) { - return null; - } - return section.getBlockStateAt(blockX, blockY, blockZ); - } - - /** - * Sets a block state at a specific location. - * The block coordinates can be absolute or relative to the region or chunk. - * - * @param blockX The x-coordinate of the block. - * @param blockY The y-coordinate of the block. - * @param blockZ The z-coordinate of the block. - * @param state The block state to be set. - * @param cleanup When true, it will cleanup all palettes of this chunk. - * This option should only be used moderately to avoid unnecessary recalculation of the palette indices. - * Recalculating the Palette should only be executed once right before saving the Chunk to file. - */ - public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { - int sectionIndex = MCAUtil.blockToChunk(blockY); - - Section section = sections.get(sectionIndex); - if (section == null) { - section = Section.newSection(); - sections.put(sectionIndex, section); - } - section.setBlockStateAt(blockX, blockY, blockZ, state, cleanup); - } - - /** - * @return The DataVersion of this chunk. - */ - public int getDataVersion() { - return dataVersion; - } - - /** - * Sets the DataVersion of this chunk. This does not check if the data of this chunk conforms - * to that DataVersion, that is the responsibility of the developer. - * - * @param dataVersion The DataVersion to be set. - */ - public void setDataVersion(int dataVersion) { - this.dataVersion = dataVersion; - } - - /** - * @return The timestamp when this region file was last updated in seconds since 1970-01-01. - */ - public int getLastMCAUpdate() { - return lastMCAUpdate; - } - - /** - * Sets the timestamp when this region file was last updated in seconds since 1970-01-01. - * - * @param lastMCAUpdate The time in seconds since 1970-01-01. - */ - public void setLastMCAUpdate(int lastMCAUpdate) { - this.lastMCAUpdate = lastMCAUpdate; - } - - /** - * @return The generation station of this chunk. - */ - public String getStatus() { - return status; - } - - /** - * Sets the generation status of this chunk. - * - * @param status The generation status of this chunk. - */ - public void setStatus(String status) { - this.status = status; - } - - /** - * Fetches the section at the given y-coordinate. - * - * @param sectionY The y-coordinate of the section in this chunk ranging from 0 to 15. - * @return The Section. - */ - public Section getSection(int sectionY) { - return sections.get(sectionY); - } - - /** - * Sets a section at a givesn y-coordinate - * - * @param sectionY The y-coordinate of the section in this chunk ranging from 0 to 15. - * @param section The section to be set. - */ - public void setSection(int sectionY, Section section) { - sections.put(sectionY, section); - } - - /** - * @return The timestamp when this chunk was last updated as a UNIX timestamp. - */ - public long getLastUpdate() { - return lastUpdate; - } - - /** - * Sets the time when this chunk was last updated as a UNIX timestamp. - * - * @param lastUpdate The UNIX timestamp. - */ - public void setLastUpdate(long lastUpdate) { - this.lastUpdate = lastUpdate; - } - - /** - * @return The cumulative amount of time players have spent in this chunk in ticks. - */ - public long getInhabitedTime() { - return inhabitedTime; - } - - /** - * Sets the cumulative amount of time players have spent in this chunk in ticks. - * - * @param inhabitedTime The time in ticks. - */ - public void setInhabitedTime(long inhabitedTime) { - this.inhabitedTime = inhabitedTime; - } - - /** - * @return The height maps of this chunk. - */ - public CompoundTag getHeightMaps() { - return heightMaps; - } - - /** - * Sets the height maps of this chunk. - * - * @param heightMaps The height maps. - */ - public void setHeightMaps(CompoundTag heightMaps) { - this.heightMaps = heightMaps; - } - - /** - * @return The carving masks of this chunk. - */ - public CompoundTag getCarvingMasks() { - return carvingMasks; - } - - /** - * Sets the carving masks of this chunk. - * - * @param carvingMasks The carving masks. - */ - public void setCarvingMasks(CompoundTag carvingMasks) { - this.carvingMasks = carvingMasks; - } - - /** - * @return The entities of this chunk. - */ - public ListTag getEntities() { - return entities; - } - - /** - * Sets the entities of this chunk. - * - * @param entities The entities. - */ - public void setEntities(ListTag entities) { - this.entities = entities; - } - - /** - * @return The tile entities of this chunk. - */ - public ListTag getTileEntities() { - return tileEntities; - } - - /** - * Sets the tile entities of this chunk. - * - * @param tileEntities The tile entities of this chunk. - */ - public void setTileEntities(ListTag tileEntities) { - this.tileEntities = tileEntities; - } - - /** - * @return The tile ticks of this chunk. - */ - public ListTag getTileTicks() { - return tileTicks; - } - - /** - * Sets the tile ticks of this chunk. - * - * @param tileTicks Thee tile ticks. - */ - public void setTileTicks(ListTag tileTicks) { - this.tileTicks = tileTicks; - } - - /** - * @return The liquid ticks of this chunk. - */ - public ListTag getLiquidTicks() { - return liquidTicks; - } - - /** - * Sets the liquid ticks of this chunk. - * - * @param liquidTicks The liquid ticks. - */ - public void setLiquidTicks(ListTag liquidTicks) { - this.liquidTicks = liquidTicks; - } - - /** - * @return The light sources in this chunk. - */ - public ListTag> getLights() { - return lights; - } - - /** - * Sets the light sources in this chunk. - * - * @param lights The light sources. - */ - public void setLights(ListTag> lights) { - this.lights = lights; - } - - /** - * @return THe liquids to be ticked in this chunk. - */ - public ListTag> getLiquidsToBeTicked() { - return liquidsToBeTicked; - } - - /** - * Sets the liquids to be ticked in this chunk. - * - * @param liquidsToBeTicked The liquids to be ticked. - */ - public void setLiquidsToBeTicked(ListTag> liquidsToBeTicked) { - this.liquidsToBeTicked = liquidsToBeTicked; - } - - /** - * @return Stuff to be ticked in this chunk. - */ - public ListTag> getToBeTicked() { - return toBeTicked; - } - - /** - * Sets stuff to be ticked in this chunk. - * - * @param toBeTicked The stuff to be ticked. - */ - public void setToBeTicked(ListTag> toBeTicked) { - this.toBeTicked = toBeTicked; - } - - /** - * @return Things that are in post processing in this chunk. - */ - public ListTag> getPostProcessing() { - return postProcessing; - } - - /** - * Sets things to be post processed in this chunk. - * - * @param postProcessing The things to be post processed. - */ - public void setPostProcessing(ListTag> postProcessing) { - this.postProcessing = postProcessing; - } - - /** - * @return Data about structures in this chunk. - */ - public CompoundTag getStructures() { - return structures; - } - - /** - * Sets data about structures in this chunk. - * - * @param structures The data about structures. - */ - public void setStructures(CompoundTag structures) { - this.structures = structures; - } - - int getBlockIndex(int blockX, int blockZ) { - return (blockZ & 0xF) * 16 + (blockX & 0xF); - } - - public void cleanupPalettesAndBlockStates() { - for (Section section : sections.values()) { - if (section != null) { - section.cleanupPaletteAndBlockStates(); - } - } - } - - public CompoundTag updateHandle(int xPos, int zPos) { - data.putInt("DataVersion", dataVersion); - CompoundTag level = data.getCompoundTag("Level"); - level.putInt("xPos", xPos); - level.putInt("zPos", zPos); - level.putLong("LastUpdate", lastUpdate); - level.putLong("InhabitedTime", inhabitedTime); - level.putIntArray("Biomes", biomes.getData()); - if (heightMaps != null) level.put("Heightmaps", heightMaps); - if (carvingMasks != null) level.put("CarvingMasks", carvingMasks); - if (entities != null) level.put("Entities", entities); - if (tileEntities != null) level.put("TileEntities", tileEntities); - if (tileTicks != null) level.put("TileTicks", tileTicks); - if (liquidTicks != null) level.put("LiquidTicks", liquidTicks); - if (lights != null) level.put("Lights", lights); - if (liquidsToBeTicked != null) level.put("LiquidsToBeTicked", liquidsToBeTicked); - if (toBeTicked != null) level.put("ToBeTicked", toBeTicked); - if (postProcessing != null) level.put("PostProcessing", postProcessing); - level.putString("Status", status); - if (structures != null) level.put("Structures", structures); - ListTag sections = new ListTag<>(CompoundTag.class); - - for (int i : this.sections.keySet()) { - if (this.sections.get(i) != null) { - sections.add(this.sections.get(i).updateHandle(i)); - } - } - - level.put("Sections", sections); - return data; - } - - public int sectionCount() { - return sections.size(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/CompressionType.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/CompressionType.java deleted file mode 100644 index 083561581..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/CompressionType.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import java.util.zip.InflaterInputStream; - -public enum CompressionType { - - NONE(0, t -> t, t -> t), - GZIP(1, GZIPOutputStream::new, GZIPInputStream::new), - ZLIB(2, DeflaterOutputStream::new, InflaterInputStream::new); - - private final byte id; - private final ExceptionFunction compressor; - private final ExceptionFunction decompressor; - - CompressionType(int id, - ExceptionFunction compressor, - ExceptionFunction decompressor) { - this.id = (byte) id; - this.compressor = compressor; - this.decompressor = decompressor; - } - - public static CompressionType getFromID(byte id) { - for (CompressionType c : CompressionType.values()) { - if (c.id == id) { - return c; - } - } - return null; - } - - public byte getID() { - return id; - } - - public OutputStream compress(OutputStream out) throws IOException { - return compressor.accept(out); - } - - public InputStream decompress(InputStream in) throws IOException { - return decompressor.accept(in); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/ExceptionFunction.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/ExceptionFunction.java deleted file mode 100644 index 3138bc33d..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/ExceptionFunction.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca; - -@FunctionalInterface -public interface ExceptionFunction { - - R accept(T t) throws E; -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/LoadFlags.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/LoadFlags.java deleted file mode 100644 index 234c6156d..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/LoadFlags.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca; - -public class LoadFlags { - - public static final long BIOMES = 0x0001; - public static final long HEIGHTMAPS = 0x0002; - public static final long CARVING_MASKS = 0x0004; - public static final long ENTITIES = 0x0008; - public static final long TILE_ENTITIES = 0x0010; - public static final long TILE_TICKS = 0x0040; - public static final long LIQUID_TICKS = 0x0080; - public static final long TO_BE_TICKED = 0x0100; - public static final long POST_PROCESSING = 0x0200; - public static final long STRUCTURES = 0x0400; - public static final long BLOCK_LIGHTS = 0x0800; - public static final long BLOCK_STATES = 0x1000; - public static final long SKY_LIGHT = 0x2000; - public static final long LIGHTS = 0x4000; - public static final long LIQUIDS_TO_BE_TICKED = 0x8000; - - public static final long ALL_DATA = 0xffffffffffffffffL; - - -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/MCAFile.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/MCAFile.java deleted file mode 100644 index 47a878b90..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/MCAFile.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.Position2; -import com.volmit.iris.util.nbt.tag.CompoundTag; -import com.volmit.iris.util.scheduling.J; - -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicReferenceArray; - -@SuppressWarnings("ALL") -public class MCAFile { - - /** - * The default chunk data version used when no custom version is supplied. - */ - public static final int DEFAULT_DATA_VERSION = 1628; - - private final int regionX; - private final int regionZ; - private AtomicReferenceArray chunks; - private ConcurrentLinkedQueue afterSave; - - /** - * MCAFile represents a world save file used by Minecraft to store world - * data on the hard drive. - * This constructor needs the x- and z-coordinates of the stored region, - * which can usually be taken from the file name {@code r.x.z.mca} - * - * @param regionX The x-coordinate of this region. - * @param regionZ The z-coordinate of this region. - */ - public MCAFile(int regionX, int regionZ) { - this.regionX = regionX; - this.regionZ = regionZ; - afterSave = new ConcurrentLinkedQueue<>(); - } - - /** - * Calculates the index of a chunk from its x- and z-coordinates in this region. - * This works with absolute and relative coordinates. - * - * @param chunkX The x-coordinate of the chunk. - * @param chunkZ The z-coordinate of the chunk. - * @return The index of this chunk. - */ - public static int getChunkIndex(int chunkX, int chunkZ) { - return (chunkX & 0x1F) + (chunkZ & 0x1F) * 32; - } - - /** - * Reads an .mca file from a {@code RandomAccessFile} into this object. - * This method does not perform any cleanups on the data. - * - * @param raf The {@code RandomAccessFile} to read from. - * @throws IOException If something went wrong during deserialization. - */ - public void deserialize(RandomAccessFile raf) throws IOException { - deserialize(raf, LoadFlags.ALL_DATA); - } - - /** - * Reads an .mca file from a {@code RandomAccessFile} into this object. - * This method does not perform any cleanups on the data. - * - * @param raf The {@code RandomAccessFile} to read from. - * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded - * @throws IOException If something went wrong during deserialization. - */ - public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { - chunks = new AtomicReferenceArray<>(1024); - for (int i = 0; i < 1024; i++) { - raf.seek(i * 4); - int offset = raf.read() << 16; - offset |= (raf.read() & 0xFF) << 8; - offset |= raf.read() & 0xFF; - if (raf.readByte() == 0) { - continue; - } - raf.seek(4096 + i * 4); - int timestamp = raf.readInt(); - Chunk chunk = new Chunk(timestamp); - raf.seek(4096L * offset + 4); //+4: skip data size - chunk.deserialize(raf, loadFlags); - chunks.set(i, chunk); - } - } - - public KList samplePositions(RandomAccessFile raf) throws IOException { - KList p2 = new KList<>(); - chunks = new AtomicReferenceArray<>(1024); - int x = 0; - int z = 0; - for (int i = 0; i < 1024; i++) { - x++; - z++; - - raf.seek(i * 4); - int offset = raf.read() << 16; - offset |= (raf.read() & 0xFF) << 8; - offset |= raf.read() & 0xFF; - if (raf.readByte() == 0) { - continue; - } - p2.add(new Position2(x & 31, (z / 31) & 31)); - } - return p2; - } - - public AtomicReferenceArray getChunks() { - return chunks; - } - - /** - * Calls {@link MCAFile#serialize(RandomAccessFile, boolean)} without updating any timestamps. - * - * @param raf The {@code RandomAccessFile} to writeNodeData to. - * @return The amount of chunks written to the file. - * @throws IOException If something went wrong during serialization. - * @see MCAFile#serialize(RandomAccessFile, boolean) - */ - public int serialize(RandomAccessFile raf) throws IOException { - return serialize(raf, false); - } - - /** - * Serializes this object to an .mca file. - * This method does not perform any cleanups on the data. - * - * @param raf The {@code RandomAccessFile} to writeNodeData to. - * @param changeLastUpdate Whether it should update all timestamps that show - * when this file was last updated. - * @return The amount of chunks written to the file. - * @throws IOException If something went wrong during serialization. - */ - public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOException { - int globalOffset = 2; - int lastWritten = 0; - int timestamp = (int) (System.currentTimeMillis() / 1000L); - int chunksWritten = 0; - int chunkXOffset = MCAUtil.regionToChunk(regionX); - int chunkZOffset = MCAUtil.regionToChunk(regionZ); - - if (chunks == null) { - return 0; - } - - for (int cx = 0; cx < 32; cx++) { - for (int cz = 0; cz < 32; cz++) { - int index = getChunkIndex(cx, cz); - Chunk chunk = chunks.get(index); - if (chunk == null) { - continue; - } - raf.seek(4096L * globalOffset); - lastWritten = chunk.serialize(raf, chunkXOffset + cx, chunkZOffset + cz); - - if (lastWritten == 0) { - continue; - } - - chunksWritten++; - - int sectors = (lastWritten >> 12) + (lastWritten % 4096 == 0 ? 0 : 1); - - raf.seek(index * 4L); - raf.writeByte(globalOffset >>> 16); - raf.writeByte(globalOffset >> 8 & 0xFF); - raf.writeByte(globalOffset & 0xFF); - raf.writeByte(sectors); - - // writeNodeData timestamp - raf.seek(index * 4L + 4096); - raf.writeInt(changeLastUpdate ? timestamp : chunk.getLastMCAUpdate()); - - globalOffset += sectors; - } - } - - // padding - if (lastWritten % 4096 != 0) { - raf.seek(globalOffset * 4096L - 1); - raf.write(0); - } - - J.a(() -> { - afterSave.forEach(i -> i.run()); - }, 20); - - return chunksWritten; - } - - /** - * Set a specific Chunk at a specific index. The index must be in range of 0 - 1023. - * - * @param index The index of the Chunk. - * @param chunk The Chunk to be set. - * @throws IndexOutOfBoundsException If index is not in the range. - */ - public void setChunk(int index, Chunk chunk) { - checkIndex(index); - if (chunks == null) { - chunks = new AtomicReferenceArray<>(1024); - } - chunks.set(index, chunk); - } - - /** - * Set a specific Chunk at a specific chunk location. - * The x- and z-value can be absolute chunk coordinates or they can be relative to the region origin. - * - * @param chunkX The x-coordinate of the Chunk. - * @param chunkZ The z-coordinate of the Chunk. - * @param chunk The chunk to be set. - */ - public void setChunk(int chunkX, int chunkZ, Chunk chunk) { - setChunk(getChunkIndex(chunkX, chunkZ), chunk); - } - - /** - * Returns the chunk data of a chunk at a specific index in this file. - * - * @param index The index of the chunk in this file. - * @return The chunk data. - */ - public Chunk getChunk(int index) { - checkIndex(index); - if (chunks == null) { - return null; - } - return chunks.get(index); - } - - /** - * Returns the chunk data of a chunk in this file. - * - * @param chunkX The x-coordinate of the chunk. - * @param chunkZ The z-coordinate of the chunk. - * @return The chunk data. - */ - public Chunk getChunk(int chunkX, int chunkZ) { - return getChunk(getChunkIndex(chunkX, chunkZ)); - } - - public boolean hasChunk(int chunkX, int chunkZ) { - return getChunk(chunkX, chunkZ) != null; - } - - private int checkIndex(int index) { - if (index < 0 || index > 1023) { - throw new IndexOutOfBoundsException(); - } - return index; - } - - private Chunk createChunkIfMissing(int blockX, int blockZ) { - int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); - Chunk chunk = getChunk(chunkX, chunkZ); - if (chunk == null) { - chunk = Chunk.newChunk(); - setChunk(getChunkIndex(chunkX, chunkZ), chunk); - } - return chunk; - } - - public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { - createChunkIfMissing(blockX, blockZ).setBiomeAt(blockX, blockY, blockZ, biomeID); - } - - /** - * Fetches the biome id at a specific block. - * - * @param blockX The x-coordinate of the block. - * @param blockY The y-coordinate of the block. - * @param blockZ The z-coordinate of the block. - * @return The biome id if the chunk exists and the chunk has biomes, otherwise -1. - */ - public int getBiomeAt(int blockX, int blockY, int blockZ) { - int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); - Chunk chunk = getChunk(getChunkIndex(chunkX, chunkZ)); - if (chunk == null) { - return -1; - } - return chunk.getBiomeAt(blockX, blockY, blockZ); - } - - /** - * Set a block state at a specific block location. - * The block coordinates can be absolute coordinates or they can be relative to the region. - * - * @param blockX The x-coordinate of the block. - * @param blockY The y-coordinate of the block. - * @param blockZ The z-coordinate of the block. - * @param state The block state to be set. - * @param cleanup Whether the Palette and the BLockStates should be recalculated after adding the block state. - */ - public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { - createChunkIfMissing(blockX, blockZ).setBlockStateAt(blockX, blockY, blockZ, state, cleanup); - } - - /** - * Fetches a block state at a specific block location. - * The block coordinates can be absolute coordinates or they can be relative to the region. - * - * @param blockX The x-coordinate of the block. - * @param blockY The y-coordinate of the block. - * @param blockZ The z-coordinate of the block. - * @return The block state or null if the chunk or the section do not exist. - */ - public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { - int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); - Chunk chunk = getChunk(chunkX, chunkZ); - if (chunk == null) { - return null; - } - return chunk.getBlockStateAt(blockX, blockY, blockZ); - } - - public void afterSave(Runnable o) { - afterSave.add(o); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/NBTWorld.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/NBTWorld.java deleted file mode 100644 index bc77bf299..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/NBTWorld.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.data.B; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.nbt.tag.CompoundTag; -import com.volmit.iris.util.nbt.tag.StringTag; -import com.volmit.iris.util.parallel.HyperLock; -import org.bukkit.NamespacedKey; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; - -import java.io.File; -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -public class NBTWorld { - private static final BlockData AIR = B.get("AIR"); - private static final Map blockDataCache = new KMap<>(); - private static final Function BLOCK_DATA_COMPUTE = (blockData) -> { - CompoundTag s = new CompoundTag(); - String data = blockData.getAsString(true); - NamespacedKey key = blockData.getMaterial().getKey(); - s.putString("Name", key.getNamespace() + ":" + key.getKey()); - - if (data.contains("[")) { - String raw = data.split("\\Q[\\E")[1].replaceAll("\\Q]\\E", ""); - CompoundTag props = new CompoundTag(); - if (raw.contains(",")) { - for (String i : raw.split("\\Q,\\E")) { - String[] m = i.split("\\Q=\\E"); - String k = m[0]; - String v = m[1]; - props.put(k, new StringTag(v)); - } - } else { - String[] m = raw.split("\\Q=\\E"); - String k = m[0]; - String v = m[1]; - props.put(k, new StringTag(v)); - } - s.put("Properties", props); - } - - return s; - }; - private static final Map biomeIds = computeBiomeIDs(); - private final KMap loadedRegions; - private final HyperLock hyperLock = new HyperLock(); - private final KMap lastUse; - private final File worldFolder; - private final ExecutorService saveQueue; - - public NBTWorld(File worldFolder) { - this.worldFolder = worldFolder; - this.loadedRegions = new KMap<>(); - this.lastUse = new KMap<>(); - saveQueue = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r); - t.setName("Iris MCA Writer"); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }); - } - - public static BlockData getBlockData(CompoundTag tag) { - if (tag == null) { - return B.getAir(); - } - - StringBuilder p = new StringBuilder(tag.getString("Name")); - - if (tag.containsKey("Properties")) { - CompoundTag props = tag.getCompoundTag("Properties"); - p.append('['); - - for (String i : props.keySet()) { - p.append(i).append('=').append(props.getString(i)).append(','); - } - - p.deleteCharAt(p.length() - 1).append(']'); - } - - BlockData b = B.getOrNull(p.toString(), true); - - if (b == null) { - return B.getAir(); - } - - return b; - } - - public static CompoundTag getCompound(BlockData bd) { - return blockDataCache.computeIfAbsent(bd, BLOCK_DATA_COMPUTE); - } - - private static Map computeBiomeIDs() { - Map biomeIds = new KMap<>(); - - for (Biome biome : Biome.values()) { - if (!biome.name().equals("CUSTOM")) { - biomeIds.put(biome, INMS.get().getBiomeId(biome)); - } - } - - return biomeIds; - } - - public void close() { - - for (Long i : loadedRegions.k()) { - queueSaveUnload(Cache.keyX(i), Cache.keyZ(i)); - } - - saveQueue.shutdown(); - try { - while (!saveQueue.awaitTermination(3, TimeUnit.SECONDS)) { - Iris.info("Still Waiting to save MCA Files..."); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - public void flushNow() { - for (Long i : loadedRegions.k()) { - doSaveUnload(Cache.keyX(i), Cache.keyZ(i)); - } - } - - public void queueSaveUnload(int x, int z) { - saveQueue.submit(() -> doSaveUnload(x, z)); - } - - public void doSaveUnload(int x, int z) { - MCAFile f = getMCAOrNull(x, z); - if (f != null) { - unloadRegion(x, z); - } - - saveRegion(x, z, f); - } - - public void save() { - boolean saving = true; - - for (Long i : loadedRegions.k()) { - int x = Cache.keyX(i); - int z = Cache.keyZ(i); - - if (!lastUse.containsKey(i)) { - lastUse.put(i, M.ms()); - } - - if (shouldUnload(x, z)) { - queueSaveUnload(x, z); - } - } - - Iris.debug("Regions: " + C.GOLD + loadedRegions.size() + C.LIGHT_PURPLE); - } - - public void queueSave() { - - } - - public synchronized void unloadRegion(int x, int z) { - long key = Cache.key(x, z); - loadedRegions.remove(key); - lastUse.remove(key); - Iris.debug("Unloaded Region " + C.GOLD + x + " " + z); - } - - public void saveRegion(int x, int z) { - long k = Cache.key(x, z); - MCAFile mca = getMCAOrNull(x, z); - try { - MCAUtil.write(mca, getRegionFile(x, z), true); - Iris.debug("Saved Region " + C.GOLD + x + " " + z); - } catch (IOException e) { - Iris.error("Failed to save region " + getRegionFile(x, z).getPath()); - e.printStackTrace(); - } - } - - public void saveRegion(int x, int z, MCAFile mca) { - try { - MCAUtil.write(mca, getRegionFile(x, z), true); - Iris.debug("Saved Region " + C.GOLD + x + " " + z); - } catch (IOException e) { - Iris.error("Failed to save region " + getRegionFile(x, z).getPath()); - e.printStackTrace(); - } - } - - public boolean shouldUnload(int x, int z) { - return getIdleDuration(x, z) > 60000; - } - - public File getRegionFile(int x, int z) { - return new File(worldFolder, "region/r." + x + "." + z + ".mca"); - } - - public BlockData getBlockData(int x, int y, int z) { - try { - CompoundTag tag = getChunkSection(x >> 4, y >> 4, z >> 4).getBlockStateAt(x & 15, y & 15, z & 15); - - if (tag == null) { - return AIR; - } - - return getBlockData(tag); - } catch (Throwable e) { - Iris.reportError(e); - - } - return AIR; - } - - public void setBlockData(int x, int y, int z, BlockData data) { - getChunkSection(x >> 4, y >> 4, z >> 4).setBlockStateAt(x & 15, y & 15, z & 15, getCompound(data), false); - } - - public int getBiomeId(Biome b) { - return biomeIds.get(b); - } - - public void setBiome(int x, int y, int z, Biome biome) { - getChunk(x >> 4, z >> 4).setBiomeAt(x & 15, y, z & 15, biomeIds.get(biome)); - } - - public Section getChunkSection(int x, int y, int z) { - Chunk c = getChunk(x, z); - Section s = c.getSection(y); - - if (s == null) { - s = Section.newSection(); - c.setSection(y, s); - } - - return s; - } - - public Chunk getChunk(int x, int z) { - return getChunk(getMCA(x >> 5, z >> 5), x, z); - } - - public Chunk getChunk(MCAFile mca, int x, int z) { - Chunk c = mca.getChunk(x & 31, z & 31); - - if (c == null) { - c = Chunk.newChunk(); - mca.setChunk(x & 31, z & 31, c); - } - - return c; - } - - public Chunk getNewChunk(MCAFile mca, int x, int z) { - Chunk c = Chunk.newChunk(); - mca.setChunk(x & 31, z & 31, c); - - return c; - } - - public long getIdleDuration(int x, int z) { - return hyperLock.withResult(x, z, () -> { - Long l = lastUse.get(Cache.key(x, z)); - return l == null ? 0 : (M.ms() - l); - }); - } - - public MCAFile getMCA(int x, int z) { - long key = Cache.key(x, z); - - return hyperLock.withResult(x, z, () -> { - lastUse.put(key, M.ms()); - - MCAFile mcaf = loadedRegions.get(key); - - if (mcaf == null) { - mcaf = new MCAFile(x, z); - loadedRegions.put(key, mcaf); - } - - return mcaf; - }); - } - - public MCAFile getMCAOrNull(int x, int z) { - long key = Cache.key(x, z); - - return hyperLock.withResult(x, z, () -> { - if (loadedRegions.containsKey(key)) { - lastUse.put(key, M.ms()); - return loadedRegions.get(key); - } - - return null; - }); - } - - public int size() { - return loadedRegions.size(); - } - - public boolean isLoaded(int x, int z) { - return loadedRegions.containsKey(Cache.key(x, z)); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/Section.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/Section.java deleted file mode 100644 index f58f74faf..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/Section.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca; - -import com.volmit.iris.core.nms.INMS; -import com.volmit.iris.util.nbt.mca.palette.MCAPaletteAccess; -import com.volmit.iris.util.nbt.tag.ByteArrayTag; -import com.volmit.iris.util.nbt.tag.CompoundTag; -import com.volmit.iris.util.nbt.tag.ListTag; - - -public class Section { - private CompoundTag data; - private MCAPaletteAccess palette; - private byte[] blockLight; - private byte[] skyLight; - - public Section(CompoundTag sectionRoot, int dataVersion) { - this(sectionRoot, dataVersion, LoadFlags.ALL_DATA); - } - - public Section(CompoundTag sectionRoot, int dataVersion, long loadFlags) { - data = sectionRoot; - ListTag rawPalette = sectionRoot.getListTag("Palette"); - if (rawPalette == null) { - return; - } - palette = INMS.get().createPalette(); - palette.readFromSection(sectionRoot); - ByteArrayTag blockLight = sectionRoot.getByteArrayTag("BlockLight"); - ByteArrayTag skyLight = sectionRoot.getByteArrayTag("SkyLight"); - this.blockLight = blockLight != null ? blockLight.getValue() : null; - this.skyLight = skyLight != null ? skyLight.getValue() : null; - } - - Section() { - } - - /** - * Creates an empty Section with base values. - * - * @return An empty Section - */ - public static Section newSection() { - Section s = new Section(); - s.data = new CompoundTag(); - s.palette = INMS.get().createPalette(); - return s; - } - - /** - * Checks whether the data of this Section is empty. - * - * @return true if empty - */ - public boolean isEmpty() { - return data == null; - } - - /** - * Fetches a block state based on a block location from this section. - * The coordinates represent the location of the block inside of this Section. - * - * @param blockX The x-coordinate of the block in this Section - * @param blockY The y-coordinate of the block in this Section - * @param blockZ The z-coordinate of the block in this Section - * @return The block state data of this block. - */ - public synchronized CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { - synchronized (palette) { - return palette.getBlock(blockX & 15, blockY & 15, blockZ & 15); - } - } - - /** - * Attempts to add a block state for a specific block location in this Section. - * - * @param blockX The x-coordinate of the block in this Section - * @param blockY The y-coordinate of the block in this Section - * @param blockZ The z-coordinate of the block in this Section - * @param state The block state to be set - */ - public synchronized void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { - synchronized (palette) { - palette.setBlock(blockX & 15, blockY & 15, blockZ & 15, state); - } - } - - /** - * This method recalculates the palette and its indices. - * This should only be used moderately to avoid unnecessary recalculation of the palette indices. - * Recalculating the Palette should only be executed once right before saving the Section to file. - */ - public void cleanupPaletteAndBlockStates() { - - } - - /** - * @return The block light array of this Section - */ - public synchronized byte[] getBlockLight() { - return blockLight; - } - - /** - * Sets the block light array for this section. - * - * @param blockLight The block light array - * @throws IllegalArgumentException When the length of the array is not 2048 - */ - public synchronized void setBlockLight(byte[] blockLight) { - if (blockLight != null && blockLight.length != 2048) { - throw new IllegalArgumentException("BlockLight array must have a length of 2048"); - } - this.blockLight = blockLight; - } - - /** - * @return The sky light values of this Section - */ - public synchronized byte[] getSkyLight() { - return skyLight; - } - - /** - * Sets the sky light values of this section. - * - * @param skyLight The custom sky light values - * @throws IllegalArgumentException If the length of the array is not 2048 - */ - public synchronized void setSkyLight(byte[] skyLight) { - if (skyLight != null && skyLight.length != 2048) { - throw new IllegalArgumentException("SkyLight array must have a length of 2048"); - } - this.skyLight = skyLight; - } - - /** - * Updates the raw CompoundTag that this Section is based on. - * This must be called before saving a Section to disk if the Section was manually created - * to set the Y of this Section. - * - * @param y The Y-value of this Section - * @return A reference to the raw CompoundTag this Section is based on - */ - public synchronized CompoundTag updateHandle(int y) { - data.putByte("Y", (byte) y); - - if (palette != null) { - synchronized (palette) { - palette.writeToSection(data); - } - } - if (blockLight != null) { - data.putByteArray("BlockLight", blockLight); - } - if (skyLight != null) { - data.putByteArray("SkyLight", skyLight); - } - return data; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCABiomeContainer.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCABiomeContainer.java deleted file mode 100644 index 60ff7eb26..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCABiomeContainer.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -public interface MCABiomeContainer { - int[] getData(); - - void setBiome(int x, int y, int z, int id); - - int getBiome(int x, int y, int z); -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCABitStorage.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCABitStorage.java deleted file mode 100644 index b3413bf5b..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCABitStorage.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import org.apache.commons.lang3.Validate; - -import java.util.function.IntConsumer; - -public class MCABitStorage { - private static final int[] MAGIC = new int[]{ - -1, -1, 0, Integer.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Integer.MIN_VALUE, - 0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756, - 0, Integer.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0, - 390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378, - 306783378, 0, 286331153, 286331153, 0, Integer.MIN_VALUE, 0, 3, 252645135, 252645135, - 0, 238609294, 238609294, 0, 226050910, 226050910, 0, 214748364, 214748364, 0, - 204522252, 204522252, 0, 195225786, 195225786, 0, 186737708, 186737708, 0, 178956970, - 178956970, 0, 171798691, 171798691, 0, 165191049, 165191049, 0, 159072862, 159072862, - 0, 153391689, 153391689, 0, 148102320, 148102320, 0, 143165576, 143165576, 0, - 138547332, 138547332, 0, Integer.MIN_VALUE, 0, 4, 130150524, 130150524, 0, 126322567, - 126322567, 0, 122713351, 122713351, 0, 119304647, 119304647, 0, 116080197, 116080197, - 0, 113025455, 113025455, 0, 110127366, 110127366, 0, 107374182, 107374182, 0, - 104755299, 104755299, 0, 102261126, 102261126, 0, 99882960, 99882960, 0, 97612893, - 97612893, 0, 95443717, 95443717, 0, 93368854, 93368854, 0, 91382282, 91382282, - 0, 89478485, 89478485, 0, 87652393, 87652393, 0, 85899345, 85899345, 0, - 84215045, 84215045, 0, 82595524, 82595524, 0, 81037118, 81037118, 0, 79536431, - 79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303, - 0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0, - 70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Integer.MIN_VALUE, - 0, 5}; - - private final long[] data; - - private final int bits; - - private final long mask; - - private final int size; - - private final int valuesPerLong; - - private final int divideMul; - - private final int divideAdd; - - private final int divideShift; - - public MCABitStorage(int bits, int length) { - this(bits, length, null); - } - - public MCABitStorage(int bits, int length, long[] data) { - Validate.inclusiveBetween(1L, 32L, bits); - this.size = length; - this.bits = bits; - this.mask = (1L << bits) - 1L; - this.valuesPerLong = (char) (64 / bits); - int var3 = 3 * (this.valuesPerLong - 1); - this.divideMul = MAGIC[var3]; - this.divideAdd = MAGIC[var3 + 1]; - this.divideShift = MAGIC[var3 + 2]; - int var4 = (length + this.valuesPerLong - 1) / this.valuesPerLong; - if (data != null) { - if (data.length != var4) - throw new RuntimeException("NO!"); - this.data = data; - } else { - this.data = new long[var4]; - } - } - - private int cellIndex(int var0) { - long var1 = Integer.toUnsignedLong(this.divideMul); - long var3 = Integer.toUnsignedLong(this.divideAdd); - return (int) (var0 * var1 + var3 >> 32L >> this.divideShift); - } - - public int getAndSet(int var0, int var1) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - Validate.inclusiveBetween(0L, this.mask, var1); - int var2 = cellIndex(var0); - long var3 = this.data[var2]; - int var5 = (var0 - var2 * this.valuesPerLong) * this.bits; - int var6 = (int) (var3 >> var5 & this.mask); - this.data[var2] = var3 & (this.mask << var5 ^ 0xFFFFFFFFFFFFFFFFL) | (var1 & this.mask) << var5; - return var6; - } - - public void set(int var0, int var1) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - Validate.inclusiveBetween(0L, this.mask, var1); - int var2 = cellIndex(var0); - long var3 = this.data[var2]; - int var5 = (var0 - var2 * this.valuesPerLong) * this.bits; - this.data[var2] = var3 & (this.mask << var5 ^ 0xFFFFFFFFFFFFFFFFL) | (var1 & this.mask) << var5; - } - - public int get(int var0) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - int var1 = cellIndex(var0); - long var2 = this.data[var1]; - int var4 = (var0 - var1 * this.valuesPerLong) * this.bits; - return (int) (var2 >> var4 & this.mask); - } - - public long[] getRaw() { - return this.data; - } - - public int getSize() { - return this.size; - } - - public int getBits() { - return this.bits; - } - - public void getAll(IntConsumer var0) { - int var1 = 0; - for (long var5 : this.data) { - for (int var7 = 0; var7 < this.valuesPerLong; var7++) { - var0.accept((int) (var5 & this.mask)); - var5 >>= this.bits; - if (++var1 >= this.size) - return; - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAChunkBiomeContainer.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAChunkBiomeContainer.java deleted file mode 100644 index 862574300..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAChunkBiomeContainer.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.Arrays; - -public class MCAChunkBiomeContainer { - private static final Logger LOGGER = LogManager.getLogger(); - private static final int WIDTH_BITS = MCAMth.ceillog2(16) - 2; - private static final int HORIZONTAL_MASK = (1 << WIDTH_BITS) - 1; - private static final int PACKED_X_LENGTH = 1 + MCAMth.log2(MCAMth.smallestEncompassingPowerOfTwo(30000000)); - private static final int PACKED_Z_LENGTH = PACKED_X_LENGTH; - public static final int PACKED_Y_LENGTH = 64 - PACKED_X_LENGTH - PACKED_Z_LENGTH; - public static final int MAX_SIZE = 1 << WIDTH_BITS + WIDTH_BITS + PACKED_Y_LENGTH - 2; - - public final MCAIdMap biomeRegistry; - - private final T[] biomes; - - private final int quartMinY; - - private final int quartHeight; - - protected MCAChunkBiomeContainer(MCAIdMap registry, int minHeight, int maxHeight, T[] abiomebase) { - this.biomeRegistry = registry; - this.biomes = abiomebase; - this.quartMinY = MCAQuartPos.fromBlock(minHeight); - this.quartHeight = MCAQuartPos.fromBlock(maxHeight) - 1; - } - - public MCAChunkBiomeContainer(MCAIdMap registry, int min, int max) { - this(registry, min, max, new int[(1 << WIDTH_BITS + WIDTH_BITS) * ceilDiv(max - min, 4)]); - } - - public MCAChunkBiomeContainer(MCAIdMap registry, int minHeight, int maxHeight, int[] aint) { - this(registry, minHeight, maxHeight, (T[]) new Object[aint.length]); - int i = -1; - for (int j = 0; j < this.biomes.length; j++) { - int k = aint[j]; - T biomebase = registry.byId(k); - if (biomebase == null) { - if (i == -1) - i = j; - this.biomes[j] = registry.byId(0); - } else { - this.biomes[j] = biomebase; - } - } - if (i != -1) - LOGGER.warn("Invalid biome data received, starting from {}: {}", Integer.valueOf(i), Arrays.toString(aint)); - } - - private static int ceilDiv(int i, int j) { - return (i + j - 1) / j; - } - - public int[] writeBiomes() { - int[] aint = new int[this.biomes.length]; - for (int i = 0; i < this.biomes.length; i++) - aint[i] = this.biomeRegistry.getId(this.biomes[i]); - return aint; - } - - public T getBiome(int i, int j, int k) { - int l = i & HORIZONTAL_MASK; - int i1 = MCAMth.clamp(j - this.quartMinY, 0, this.quartHeight); - int j1 = k & HORIZONTAL_MASK; - return this.biomes[i1 << WIDTH_BITS + WIDTH_BITS | j1 << WIDTH_BITS | l]; - } - - public void setBiome(int i, int j, int k, T biome) { - int l = i & HORIZONTAL_MASK; - int i1 = MCAMth.clamp(j - this.quartMinY, 0, this.quartHeight); - int j1 = k & HORIZONTAL_MASK; - this.biomes[i1 << WIDTH_BITS + WIDTH_BITS | j1 << WIDTH_BITS | l] = biome; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCACountConsumer.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCACountConsumer.java deleted file mode 100644 index 89e4a24ce..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCACountConsumer.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -@FunctionalInterface -public interface MCACountConsumer { - void accept(T paramT, int paramInt); -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCACrudeIncrementalIntIdentityHashBiMap.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCACrudeIncrementalIntIdentityHashBiMap.java deleted file mode 100644 index a59784f5d..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCACrudeIncrementalIntIdentityHashBiMap.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import com.google.common.base.Predicates; -import com.google.common.collect.Iterators; - -import java.util.Arrays; -import java.util.Iterator; - -public class MCACrudeIncrementalIntIdentityHashBiMap implements MCAIdMap { - public static final int NOT_FOUND = -1; - - private static final Object EMPTY_SLOT = null; - - private static final float LOADFACTOR = 0.8F; - - private K[] keys; - - private int[] values; - - private K[] byId; - - private int nextId; - - private int size; - - public MCACrudeIncrementalIntIdentityHashBiMap(int var0) { - var0 = (int) (var0 / 0.8F); - this.keys = (K[]) new Object[var0]; - this.values = new int[var0]; - this.byId = (K[]) new Object[var0]; - } - - public int getId(K var0) { - return getValue(indexOf(var0, hash(var0))); - } - - - public K byId(int var0) { - if (var0 < 0 || var0 >= this.byId.length) - return null; - return this.byId[var0]; - } - - private int getValue(int var0) { - if (var0 == -1) - return -1; - return this.values[var0]; - } - - public boolean contains(K var0) { - return (getId(var0) != -1); - } - - public boolean contains(int var0) { - return (byId(var0) != null); - } - - public int add(K var0) { - int var1 = nextId(); - addMapping(var0, var1); - return var1; - } - - private int nextId() { - while (this.nextId < this.byId.length && this.byId[this.nextId] != null) - this.nextId++; - return this.nextId; - } - - private void grow(int var0) { - K[] var1 = this.keys; - int[] var2 = this.values; - this.keys = (K[]) new Object[var0]; - this.values = new int[var0]; - this.byId = (K[]) new Object[var0]; - this.nextId = 0; - this.size = 0; - for (int var3 = 0; var3 < var1.length; var3++) { - if (var1[var3] != null) - addMapping(var1[var3], var2[var3]); - } - } - - public void addMapping(K var0, int var1) { - int var2 = Math.max(var1, this.size + 1); - if (var2 >= this.keys.length * 0.8F) { - int i = this.keys.length << 1; - while (i < var1) - i <<= 1; - grow(i); - } - int var3 = findEmpty(hash(var0)); - this.keys[var3] = var0; - this.values[var3] = var1; - this.byId[var1] = var0; - this.size++; - if (var1 == this.nextId) - this.nextId++; - } - - private int hash(K var0) { - return (MCAMth.murmurHash3Mixer(System.identityHashCode(var0)) & Integer.MAX_VALUE) % this.keys.length; - } - - private int indexOf(K var0, int var1) { - int var2; - for (var2 = var1; var2 < this.keys.length; var2++) { - if (this.keys[var2] == var0) - return var2; - if (this.keys[var2] == EMPTY_SLOT) - return -1; - } - for (var2 = 0; var2 < var1; var2++) { - if (this.keys[var2] == var0) - return var2; - if (this.keys[var2] == EMPTY_SLOT) - return -1; - } - return -1; - } - - private int findEmpty(int var0) { - int var1; - for (var1 = var0; var1 < this.keys.length; var1++) { - if (this.keys[var1] == EMPTY_SLOT) - return var1; - } - for (var1 = 0; var1 < var0; var1++) { - if (this.keys[var1] == EMPTY_SLOT) - return var1; - } - throw new RuntimeException("Overflowed :("); - } - - public Iterator iterator() { - return (Iterator) Iterators.filter(Iterators.forArray((Object[]) this.byId), Predicates.notNull()); - } - - public void clear() { - Arrays.fill(this.keys, null); - Arrays.fill(this.byId, null); - this.nextId = 0; - this.size = 0; - } - - public int size() { - return this.size; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAGlobalPalette.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAGlobalPalette.java deleted file mode 100644 index a7ce1296c..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAGlobalPalette.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import com.volmit.iris.util.nbt.tag.ListTag; - -import java.util.function.Predicate; - -public class MCAGlobalPalette implements MCAPalette { - private final MCAIdMapper registry; - - private final T defaultValue; - - public MCAGlobalPalette(MCAIdMapper var0, T var1) { - this.registry = var0; - this.defaultValue = var1; - } - - public int idFor(T var0) { - int var1 = this.registry.getId(var0); - return (var1 == -1) ? 0 : var1; - } - - public boolean maybeHas(Predicate var0) { - return true; - } - - public T valueFor(int var0) { - T var1 = this.registry.byId(var0); - return (var1 == null) ? this.defaultValue : var1; - } - - public int getSize() { - return this.registry.size(); - } - - public void read(ListTag var0) { - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAHashMapPalette.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAHashMapPalette.java deleted file mode 100644 index 45adf1abf..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAHashMapPalette.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import com.volmit.iris.util.nbt.tag.CompoundTag; -import com.volmit.iris.util.nbt.tag.ListTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -public class MCAHashMapPalette implements MCAPalette { - private final MCAIdMapper registry; - - private final MCACrudeIncrementalIntIdentityHashBiMap values; - - private final MCAPaletteResize resizeHandler; - - private final Function reader; - - private final Function writer; - - private final int bits; - - public MCAHashMapPalette(MCAIdMapper var0, int var1, MCAPaletteResize var2, Function var3, Function var4) { - this.registry = var0; - this.bits = var1; - this.resizeHandler = var2; - this.reader = var3; - this.writer = var4; - this.values = new MCACrudeIncrementalIntIdentityHashBiMap(1 << var1); - } - - public int idFor(T var0) { - int var1 = this.values.getId(var0); - if (var1 == -1) { - var1 = this.values.add(var0); - if (var1 >= 1 << this.bits) - var1 = this.resizeHandler.onResize(this.bits + 1, var0); - } - return var1; - } - - public boolean maybeHas(Predicate var0) { - for (int var1 = 0; var1 < getSize(); var1++) { - if (var0.test(this.values.byId(var1))) - return true; - } - return false; - } - - public T valueFor(int var0) { - return this.values.byId(var0); - } - - public int getSize() { - return this.values.size(); - } - - public void read(ListTag var0) { - this.values.clear(); - for (int var1 = 0; var1 < var0.size(); var1++) - this.values.add(this.reader.apply((CompoundTag) var0.get(var1))); - } - - public void write(ListTag var0) { - for (int var1 = 0; var1 < getSize(); var1++) - var0.add(this.writer.apply(this.values.byId(var1))); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAIdMap.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAIdMap.java deleted file mode 100644 index 047821a9e..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAIdMap.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -public interface MCAIdMap extends Iterable { - int getId(T paramT); - - T byId(int paramInt); -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAIdMapper.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAIdMapper.java deleted file mode 100644 index 2793413d2..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAIdMapper.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import com.google.common.base.Predicates; -import com.google.common.collect.Iterators; -import com.google.common.collect.Lists; -import it.unimi.dsi.fastutil.Hash; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap; - -import java.util.Iterator; -import java.util.List; - -public class MCAIdMapper implements MCAIdMap { - public static final int DEFAULT = -1; - private final Object2IntMap tToId; - private final List idToT; - private int nextId; - - public MCAIdMapper(Object2IntMap tToId, List idToT, int nextId) { - this.tToId = tToId; - this.idToT = idToT; - this.nextId = nextId; - } - - public MCAIdMapper() { - this(512); - } - - public MCAIdMapper(int var0) { - this.idToT = Lists.newArrayListWithExpectedSize(var0); - this.tToId = new Object2IntOpenCustomHashMap<>(var0, IdentityStrategy.INSTANCE); - } - - public void addMapping(T var0, int var1) { - this.tToId.put(var0, Integer.valueOf(var1)); - while (this.idToT.size() <= var1) - this.idToT.add(null); - this.idToT.set(var1, var0); - if (this.nextId <= var1) - this.nextId = var1 + 1; - } - - public void add(T var0) { - addMapping(var0, this.nextId); - } - - public int getId(T var0) { - Integer var1 = this.tToId.get(var0); - return (var1 == null) ? -1 : var1.intValue(); - } - - public final T byId(int var0) { - if (var0 >= 0 && var0 < this.idToT.size()) - return this.idToT.get(var0); - return null; - } - - public Iterator iterator() { - return Iterators.filter(this.idToT.iterator(), Predicates.notNull()); - } - - public boolean contains(int var0) { - return (byId(var0) != null); - } - - public int size() { - return this.tToId.size(); - } - - enum IdentityStrategy implements Hash.Strategy { - INSTANCE; - - IdentityStrategy() { - } - - public int hashCode(Object var0) { - return System.identityHashCode(var0); - } - - public boolean equals(Object var0, Object var1) { - return var0 == var1; - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCALinearPalette.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCALinearPalette.java deleted file mode 100644 index 7f91dfc36..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCALinearPalette.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import com.volmit.iris.util.nbt.tag.CompoundTag; -import com.volmit.iris.util.nbt.tag.ListTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -public class MCALinearPalette implements MCAPalette { - private final MCAIdMapper registry; - - private final T[] values; - - private final MCAPaletteResize resizeHandler; - - private final Function reader; - - private final int bits; - - private int size; - - public MCALinearPalette(MCAIdMapper var0, int var1, MCAPaletteResize var2, Function var3) { - this.registry = var0; - this.values = (T[]) new Object[1 << var1]; - this.bits = var1; - this.resizeHandler = var2; - this.reader = var3; - } - - public int idFor(T var0) { - int var1; - for (var1 = 0; var1 < this.size; var1++) { - if (this.values[var1] == var0) - return var1; - } - var1 = this.size; - if (var1 < this.values.length) { - this.values[var1] = var0; - this.size++; - return var1; - } - return this.resizeHandler.onResize(this.bits + 1, var0); - } - - public boolean maybeHas(Predicate var0) { - for (int var1 = 0; var1 < this.size; var1++) { - if (var0.test(this.values[var1])) - return true; - } - return false; - } - - public T valueFor(int var0) { - if (var0 >= 0 && var0 < this.size) - return this.values[var0]; - return null; - } - - public int getSize() { - return this.size; - } - - public void read(ListTag var0) { - for (int var1 = 0; var1 < var0.size(); var1++) { - this.values[var1] = this.reader.apply((CompoundTag) var0.get(var1)); - } - this.size = var0.size(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAMth.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAMth.java deleted file mode 100644 index 26ea63441..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAMth.java +++ /dev/null @@ -1,712 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import java.util.Random; -import java.util.UUID; -import java.util.function.Consumer; -import java.util.function.Supplier; - -public class MCAMth { - public static final float PI = 3.1415927F; - public static final float HALF_PI = 1.5707964F; - public static final float TWO_PI = 6.2831855F; - public static final float DEG_TO_RAD = 0.017453292F; - public static final float RAD_TO_DEG = 57.295776F; - public static final float EPSILON = 1.0E-5F; - public static final float SQRT_OF_TWO = sqrt(2.0F); - private static final int BIG_ENOUGH_INT = 1024; - private static final float BIG_ENOUGH_FLOAT = 1024.0F; - private static final long UUID_VERSION = 61440L; - private static final long UUID_VERSION_TYPE_4 = 16384L; - private static final long UUID_VARIANT = -4611686018427387904L; - private static final long UUID_VARIANT_2 = -9223372036854775808L; - private static final float SIN_SCALE = 10430.378F; - - private static final float[] SIN; - private static final Random RANDOM = new Random(); - private static final int[] MULTIPLY_DE_BRUIJN_BIT_POSITION = new int[]{ - 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, - 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, - 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, - 10, 9}; - private static final double ONE_SIXTH = 0.16666666666666666D; - private static final int FRAC_EXP = 8; - private static final int LUT_SIZE = 257; - private static final double FRAC_BIAS = Double.longBitsToDouble(4805340802404319232L); - private static final double[] ASIN_TAB = new double[257]; - private static final double[] COS_TAB = new double[257]; - - static { - SIN = make(new float[65536], var0 -> { - for (int var1 = 0; var1 < var0.length; var1++) - var0[var1] = (float) Math.sin(var1 * Math.PI * 2.0D / 65536.0D); - }); - } - - static { - for (int var0 = 0; var0 < 257; var0++) { - double var1 = var0 / 256.0D; - double var3 = Math.asin(var1); - COS_TAB[var0] = Math.cos(var3); - ASIN_TAB[var0] = var3; - } - } - - public static T make(Supplier var0) { - return var0.get(); - } - - public static T make(T var0, Consumer var1) { - var1.accept(var0); - return var0; - } - - public static float sin(float var0) { - return SIN[(int) (var0 * 10430.378F) & 0xFFFF]; - } - - public static float cos(float var0) { - return SIN[(int) (var0 * 10430.378F + 16384.0F) & 0xFFFF]; - } - - public static float sqrt(float var0) { - return (float) Math.sqrt(var0); - } - - public static int floor(float var0) { - int var1 = (int) var0; - return (var0 < var1) ? (var1 - 1) : var1; - } - - public static int fastFloor(double var0) { - return (int) (var0 + 1024.0D) - 1024; - } - - public static int floor(double var0) { - int var2 = (int) var0; - return (var0 < var2) ? (var2 - 1) : var2; - } - - public static long lfloor(double var0) { - long var2 = (long) var0; - return (var0 < var2) ? (var2 - 1L) : var2; - } - - public static int absFloor(double var0) { - return (int) ((var0 >= 0.0D) ? var0 : (-var0 + 1.0D)); - } - - public static float abs(float var0) { - return Math.abs(var0); - } - - public static int abs(int var0) { - return Math.abs(var0); - } - - public static int ceil(float var0) { - int var1 = (int) var0; - return (var0 > var1) ? (var1 + 1) : var1; - } - - public static int ceil(double var0) { - int var2 = (int) var0; - return (var0 > var2) ? (var2 + 1) : var2; - } - - public static byte clamp(byte var0, byte var1, byte var2) { - if (var0 < var1) - return var1; - if (var0 > var2) - return var2; - return var0; - } - - public static int clamp(int var0, int var1, int var2) { - if (var0 < var1) - return var1; - if (var0 > var2) - return var2; - return var0; - } - - public static long clamp(long var0, long var2, long var4) { - if (var0 < var2) - return var2; - if (var0 > var4) - return var4; - return var0; - } - - public static float clamp(float var0, float var1, float var2) { - if (var0 < var1) - return var1; - if (var0 > var2) - return var2; - return var0; - } - - public static double clamp(double var0, double var2, double var4) { - if (var0 < var2) - return var2; - if (var0 > var4) - return var4; - return var0; - } - - public static double clampedLerp(double var0, double var2, double var4) { - if (var4 < 0.0D) - return var0; - if (var4 > 1.0D) - return var2; - return lerp(var4, var0, var2); - } - - public static float clampedLerp(float var0, float var1, float var2) { - if (var2 < 0.0F) - return var0; - if (var2 > 1.0F) - return var1; - return lerp(var2, var0, var1); - } - - public static double absMax(double var0, double var2) { - if (var0 < 0.0D) - var0 = -var0; - if (var2 < 0.0D) - var2 = -var2; - return (var0 > var2) ? var0 : var2; - } - - public static int intFloorDiv(int var0, int var1) { - return Math.floorDiv(var0, var1); - } - - public static int nextInt(Random var0, int var1, int var2) { - if (var1 >= var2) - return var1; - return var0.nextInt(var2 - var1 + 1) + var1; - } - - public static float nextFloat(Random var0, float var1, float var2) { - if (var1 >= var2) - return var1; - return var0.nextFloat() * (var2 - var1) + var1; - } - - public static double nextDouble(Random var0, double var1, double var3) { - if (var1 >= var3) - return var1; - return var0.nextDouble() * (var3 - var1) + var1; - } - - public static double average(long[] var0) { - long var1 = 0L; - for (long var6 : var0) - var1 += var6; - return var1 / var0.length; - } - - public static boolean equal(float var0, float var1) { - return (Math.abs(var1 - var0) < 1.0E-5F); - } - - public static boolean equal(double var0, double var2) { - return (Math.abs(var2 - var0) < 9.999999747378752E-6D); - } - - public static int positiveModulo(int var0, int var1) { - return Math.floorMod(var0, var1); - } - - public static float positiveModulo(float var0, float var1) { - return (var0 % var1 + var1) % var1; - } - - public static double positiveModulo(double var0, double var2) { - return (var0 % var2 + var2) % var2; - } - - public static int wrapDegrees(int var0) { - int var1 = var0 % 360; - if (var1 >= 180) - var1 -= 360; - if (var1 < -180) - var1 += 360; - return var1; - } - - public static float wrapDegrees(float var0) { - float var1 = var0 % 360.0F; - if (var1 >= 180.0F) - var1 -= 360.0F; - if (var1 < -180.0F) - var1 += 360.0F; - return var1; - } - - public static double wrapDegrees(double var0) { - double var2 = var0 % 360.0D; - if (var2 >= 180.0D) - var2 -= 360.0D; - if (var2 < -180.0D) - var2 += 360.0D; - return var2; - } - - public static float degreesDifference(float var0, float var1) { - return wrapDegrees(var1 - var0); - } - - public static float degreesDifferenceAbs(float var0, float var1) { - return abs(degreesDifference(var0, var1)); - } - - public static float rotateIfNecessary(float var0, float var1, float var2) { - float var3 = degreesDifference(var0, var1); - float var4 = clamp(var3, -var2, var2); - return var1 - var4; - } - - public static float approach(float var0, float var1, float var2) { - var2 = abs(var2); - if (var0 < var1) - return clamp(var0 + var2, var0, var1); - return clamp(var0 - var2, var1, var0); - } - - public static float approachDegrees(float var0, float var1, float var2) { - float var3 = degreesDifference(var0, var1); - return approach(var0, var0 + var3, var2); - } - - public static int getInt(String var0, int var1) { - return Integer.valueOf(var0, var1); - } - - public static int getInt(String var0, int var1, int var2) { - return Math.max(var2, getInt(var0, var1)); - } - - public static double getDouble(String var0, double var1) { - try { - return Double.parseDouble(var0); - } catch (Throwable var3) { - return var1; - } - } - - public static double getDouble(String var0, double var1, double var3) { - return Math.max(var3, getDouble(var0, var1)); - } - - public static int smallestEncompassingPowerOfTwo(int var0) { - int var1 = var0 - 1; - var1 |= var1 >> 1; - var1 |= var1 >> 2; - var1 |= var1 >> 4; - var1 |= var1 >> 8; - var1 |= var1 >> 16; - return var1 + 1; - } - - public static boolean isPowerOfTwo(int var0) { - return (var0 != 0 && (var0 & var0 - 1) == 0); - } - - public static int ceillog2(int var0) { - var0 = isPowerOfTwo(var0) ? var0 : smallestEncompassingPowerOfTwo(var0); - return MULTIPLY_DE_BRUIJN_BIT_POSITION[(int) (var0 * 125613361L >> 27L) & 0x1F]; - } - - public static int log2(int var0) { - return ceillog2(var0) - (isPowerOfTwo(var0) ? 0 : 1); - } - - public static int color(float var0, float var1, float var2) { - return color(floor(var0 * 255.0F), floor(var1 * 255.0F), floor(var2 * 255.0F)); - } - - public static int color(int var0, int var1, int var2) { - int var3 = var0; - var3 = (var3 << 8) + var1; - var3 = (var3 << 8) + var2; - return var3; - } - - public static int colorMultiply(int var0, int var1) { - int var2 = (var0 & 0xFF0000) >> 16; - int var3 = (var1 & 0xFF0000) >> 16; - int var4 = (var0 & 0xFF00) >> 8; - int var5 = (var1 & 0xFF00) >> 8; - int var6 = (var0 & 0xFF); - int var7 = (var1 & 0xFF); - int var8 = (int) (var2 * var3 / 255.0F); - int var9 = (int) (var4 * var5 / 255.0F); - int var10 = (int) (var6 * var7 / 255.0F); - return var0 & 0xFF000000 | var8 << 16 | var9 << 8 | var10; - } - - public static int colorMultiply(int var0, float var1, float var2, float var3) { - int var4 = (var0 & 0xFF0000) >> 16; - int var5 = (var0 & 0xFF00) >> 8; - int var6 = (var0 & 0xFF); - int var7 = (int) (var4 * var1); - int var8 = (int) (var5 * var2); - int var9 = (int) (var6 * var3); - return var0 & 0xFF000000 | var7 << 16 | var8 << 8 | var9; - } - - public static float frac(float var0) { - return var0 - floor(var0); - } - - public static double frac(double var0) { - return var0 - lfloor(var0); - } - - public static long getSeed(int var0, int var1, int var2) { - long var3 = (var0 * 3129871) ^ var2 * 116129781L ^ var1; - var3 = var3 * var3 * 42317861L + var3 * 11L; - return var3 >> 16L; - } - - public static UUID createInsecureUUID(Random var0) { - long var1 = var0.nextLong() & 0xFFFFFFFFFFFF0FFFL | 0x4000L; - long var3 = var0.nextLong() & 0x3FFFFFFFFFFFFFFFL | Long.MIN_VALUE; - return new UUID(var1, var3); - } - - public static UUID createInsecureUUID() { - return createInsecureUUID(RANDOM); - } - - public static double inverseLerp(double var0, double var2, double var4) { - return (var0 - var2) / (var4 - var2); - } - - public static double atan2(double var0, double var2) { - double var4 = var2 * var2 + var0 * var0; - if (Double.isNaN(var4)) - return Double.NaN; - boolean var6 = (var0 < 0.0D); - if (var6) - var0 = -var0; - boolean var7 = (var2 < 0.0D); - if (var7) - var2 = -var2; - boolean var8 = (var0 > var2); - if (var8) { - double d = var2; - var2 = var0; - var0 = d; - } - double var9 = fastInvSqrt(var4); - var2 *= var9; - var0 *= var9; - double var11 = FRAC_BIAS + var0; - int var13 = (int) Double.doubleToRawLongBits(var11); - double var14 = ASIN_TAB[var13]; - double var16 = COS_TAB[var13]; - double var18 = var11 - FRAC_BIAS; - double var20 = var0 * var16 - var2 * var18; - double var22 = (6.0D + var20 * var20) * var20 * 0.16666666666666666D; - double var24 = var14 + var22; - if (var8) - var24 = 1.5707963267948966D - var24; - if (var7) - var24 = Math.PI - var24; - if (var6) - var24 = -var24; - return var24; - } - - public static float fastInvSqrt(float var0) { - float var1 = 0.5F * var0; - int var2 = Float.floatToIntBits(var0); - var2 = 1597463007 - (var2 >> 1); - var0 = Float.intBitsToFloat(var2); - var0 *= 1.5F - var1 * var0 * var0; - return var0; - } - - public static double fastInvSqrt(double var0) { - double var2 = 0.5D * var0; - long var4 = Double.doubleToRawLongBits(var0); - var4 = 6910469410427058090L - (var4 >> 1L); - var0 = Double.longBitsToDouble(var4); - var0 *= 1.5D - var2 * var0 * var0; - return var0; - } - - public static float fastInvCubeRoot(float var0) { - int var1 = Float.floatToIntBits(var0); - var1 = 1419967116 - var1 / 3; - float var2 = Float.intBitsToFloat(var1); - var2 = 0.6666667F * var2 + 1.0F / 3.0F * var2 * var2 * var0; - var2 = 0.6666667F * var2 + 1.0F / 3.0F * var2 * var2 * var0; - return var2; - } - - public static int hsvToRgb(float var0, float var1, float var2) { - float var8, var9, var10; - int var11, var12, var13, var3 = (int) (var0 * 6.0F) % 6; - float var4 = var0 * 6.0F - var3; - float var5 = var2 * (1.0F - var1); - float var6 = var2 * (1.0F - var4 * var1); - float var7 = var2 * (1.0F - (1.0F - var4) * var1); - switch (var3) { - case 0: - var8 = var2; - var9 = var7; - var10 = var5; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 1: - var8 = var6; - var9 = var2; - var10 = var5; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 2: - var8 = var5; - var9 = var2; - var10 = var7; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 3: - var8 = var5; - var9 = var6; - var10 = var2; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 4: - var8 = var7; - var9 = var5; - var10 = var2; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 5: - var8 = var2; - var9 = var5; - var10 = var6; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - } - throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + var0 + ", " + var1 + ", " + var2); - } - - public static int murmurHash3Mixer(int var0) { - var0 ^= var0 >>> 16; - var0 *= -2048144789; - var0 ^= var0 >>> 13; - var0 *= -1028477387; - var0 ^= var0 >>> 16; - return var0; - } - - public static long murmurHash3Mixer(long var0) { - var0 ^= var0 >>> 33L; - var0 *= -49064778989728563L; - var0 ^= var0 >>> 33L; - var0 *= -4265267296055464877L; - var0 ^= var0 >>> 33L; - return var0; - } - - public static double[] cumulativeSum(double... var0) { - float var1 = 0.0F; - for (double var5 : var0) - var1 = (float) (var1 + var5); - int var2; - for (var2 = 0; var2 < var0.length; var2++) - var0[var2] = var0[var2] / var1; - for (var2 = 0; var2 < var0.length; var2++) - var0[var2] = ((var2 == 0) ? 0.0D : var0[var2 - 1]) + var0[var2]; - return var0; - } - - public static int getRandomForDistributionIntegral(Random var0, double[] var1) { - double var2 = var0.nextDouble(); - for (int var4 = 0; var4 < var1.length; var4++) { - if (var2 < var1[var4]) - return var4; - } - return var1.length; - } - - public static double[] binNormalDistribution(double var0, double var2, double var4, int var6, int var7) { - double[] var8 = new double[var7 - var6 + 1]; - int var9 = 0; - for (int var10 = var6; var10 <= var7; var10++) { - var8[var9] = Math.max(0.0D, var0 * - - StrictMath.exp(-(var10 - var4) * (var10 - var4) / 2.0D * var2 * var2)); - var9++; - } - return var8; - } - - public static double[] binBiModalNormalDistribution(double var0, double var2, double var4, double var6, double var8, double var10, int var12, int var13) { - double[] var14 = new double[var13 - var12 + 1]; - int var15 = 0; - for (int var16 = var12; var16 <= var13; var16++) { - var14[var15] = Math.max(0.0D, var0 * - - StrictMath.exp(-(var16 - var4) * (var16 - var4) / 2.0D * var2 * var2) + var6 * - StrictMath.exp(-(var16 - var10) * (var16 - var10) / 2.0D * var8 * var8)); - var15++; - } - return var14; - } - - public static double[] binLogDistribution(double var0, double var2, int var4, int var5) { - double[] var6 = new double[var5 - var4 + 1]; - int var7 = 0; - for (int var8 = var4; var8 <= var5; var8++) { - var6[var7] = Math.max(var0 * StrictMath.log(var8) + var2, 0.0D); - var7++; - } - return var6; - } - - public static float lerp(float var0, float var1, float var2) { - return var1 + var0 * (var2 - var1); - } - - public static double lerp(double var0, double var2, double var4) { - return var2 + var0 * (var4 - var2); - } - - public static double lerp2(double var0, double var2, double var4, double var6, double var8, double var10) { - return lerp(var2, - - lerp(var0, var4, var6), - lerp(var0, var8, var10)); - } - - public static double lerp3(double var0, double var2, double var4, double var6, double var8, double var10, double var12, double var14, double var16, double var18, double var20) { - return lerp(var4, - - lerp2(var0, var2, var6, var8, var10, var12), - lerp2(var0, var2, var14, var16, var18, var20)); - } - - public static double smoothstep(double var0) { - return var0 * var0 * var0 * (var0 * (var0 * 6.0D - 15.0D) + 10.0D); - } - - public static double smoothstepDerivative(double var0) { - return 30.0D * var0 * var0 * (var0 - 1.0D) * (var0 - 1.0D); - } - - public static int sign(double var0) { - if (var0 == 0.0D) - return 0; - return (var0 > 0.0D) ? 1 : -1; - } - - public static float rotLerp(float var0, float var1, float var2) { - return var1 + var0 * wrapDegrees(var2 - var1); - } - - public static float diffuseLight(float var0, float var1, float var2) { - return Math.min(var0 * var0 * 0.6F + var1 * var1 * (3.0F + var1) / 4.0F + var2 * var2 * 0.8F, 1.0F); - } - - @Deprecated - public static float rotlerp(float var0, float var1, float var2) { - float var3 = var1 - var0; - while (var3 < -180.0F) - var3 += 360.0F; - while (var3 >= 180.0F) - var3 -= 360.0F; - return var0 + var2 * var3; - } - - @Deprecated - public static float rotWrap(double var0) { - while (var0 >= 180.0D) - var0 -= 360.0D; - while (var0 < -180.0D) - var0 += 360.0D; - return (float) var0; - } - - public static float triangleWave(float var0, float var1) { - return (Math.abs(var0 % var1 - var1 * 0.5F) - var1 * 0.25F) / var1 * 0.25F; - } - - public static float square(float var0) { - return var0 * var0; - } - - public static double square(double var0) { - return var0 * var0; - } - - public static int square(int var0) { - return var0 * var0; - } - - public static double clampedMap(double var0, double var2, double var4, double var6, double var8) { - return clampedLerp(var6, var8, inverseLerp(var0, var2, var4)); - } - - public static double map(double var0, double var2, double var4, double var6, double var8) { - return lerp(inverseLerp(var0, var2, var4), var6, var8); - } - - public static double wobble(double var0) { - return var0 + (2.0D * (new Random(floor(var0 * 3000.0D))).nextDouble() - 1.0D) * 1.0E-7D / 2.0D; - } - - public static int roundToward(int var0, int var1) { - return (var0 + var1 - 1) / var1 * var1; - } - - public static int randomBetweenInclusive(Random var0, int var1, int var2) { - return var0.nextInt(var2 - var1 + 1) + var1; - } - - public static float randomBetween(Random var0, float var1, float var2) { - return var0.nextFloat() * (var2 - var1) + var1; - } - - public static float normal(Random var0, float var1, float var2) { - return var1 + (float) var0.nextGaussian() * var2; - } - - public static double length(int var0, double var1, int var3) { - return Math.sqrt((var0 * var0) + var1 * var1 + (var3 * var3)); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPalette.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPalette.java deleted file mode 100644 index 2c815141c..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPalette.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import com.volmit.iris.util.nbt.tag.ListTag; - -import java.util.function.Predicate; - -public interface MCAPalette { - int idFor(T paramT); - - boolean maybeHas(Predicate paramPredicate); - - T valueFor(int paramInt); - - int getSize(); - - void read(ListTag paramListTag); -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPaletteAccess.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPaletteAccess.java deleted file mode 100644 index e72a7d246..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPaletteAccess.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import com.volmit.iris.util.nbt.tag.CompoundTag; - -public interface MCAPaletteAccess { - void setBlock(int x, int y, int z, CompoundTag data); - - CompoundTag getBlock(int x, int y, int z); - - void writeToSection(CompoundTag tag); - - void readFromSection(CompoundTag tag); -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPaletteResize.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPaletteResize.java deleted file mode 100644 index d6ca278d5..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPaletteResize.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -interface MCAPaletteResize { - int onResize(int paramInt, T paramT); -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPalettedContainer.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPalettedContainer.java deleted file mode 100644 index 941c37447..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAPalettedContainer.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import com.volmit.iris.util.nbt.tag.CompoundTag; -import com.volmit.iris.util.nbt.tag.ListTag; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; - -import java.util.function.Function; -import java.util.function.Predicate; - -public class MCAPalettedContainer implements MCAPaletteResize { - public static final int GLOBAL_PALETTE_BITS = 9; - public static final int MIN_PALETTE_SIZE = 4; - private static final int SIZE = 4096; - private final MCAPalette globalPalette; - - private final MCAPaletteResize dummyPaletteResize = (var0, var1) -> 0; - - private final MCAIdMapper registry; - - private final Function reader; - - private final Function writer; - - private final T defaultValue; - - protected MCABitStorage storage; - - private MCAPalette palette; - - private int bits; - - public MCAPalettedContainer(MCAPalette var0, MCAIdMapper var1, Function var2, Function var3, T var4) { - this.globalPalette = var0; - this.registry = var1; - this.reader = var2; - this.writer = var3; - this.defaultValue = var4; - setBits(4); - } - - private static int getIndex(int var0, int var1, int var2) { - return var1 << 8 | var2 << 4 | var0; - } - - private void setBits(int var0) { - if (var0 == this.bits) - return; - this.bits = var0; - if (this.bits <= 4) { - this.bits = 4; - this.palette = new MCALinearPalette<>(this.registry, this.bits, this, this.reader); - } else if (this.bits < 9) { - this.palette = new MCAHashMapPalette<>(this.registry, this.bits, this, this.reader, this.writer); - } else { - this.palette = this.globalPalette; - this.bits = MCAMth.ceillog2(this.registry.size()); - } - this.palette.idFor(this.defaultValue); - this.storage = new MCABitStorage(this.bits, 4096); - } - - public int onResize(int var0, T var1) { - MCABitStorage var2 = this.storage; - MCAPalette var3 = this.palette; - setBits(var0); - for (int var4 = 0; var4 < var2.getSize(); var4++) { - T var5 = var3.valueFor(var2.get(var4)); - if (var5 != null) - set(var4, var5); - } - return this.palette.idFor(var1); - } - - public T getAndSet(int var0, int var1, int var2, T var3) { - return getAndSet(getIndex(var0, var1, var2), var3); - } - - public T getAndSetUnchecked(int var0, int var1, int var2, T var3) { - return getAndSet(getIndex(var0, var1, var2), var3); - } - - private T getAndSet(int var0, T var1) { - int var2 = this.palette.idFor(var1); - int var3 = this.storage.getAndSet(var0, var2); - T var4 = this.palette.valueFor(var3); - return (var4 == null) ? this.defaultValue : var4; - } - - public void set(int var0, int var1, int var2, T var3) { - set(getIndex(var0, var1, var2), var3); - } - - private void set(int var0, T var1) { - int var2 = this.palette.idFor(var1); - this.storage.set(var0, var2); - } - - public T get(int var0, int var1, int var2) { - return get(getIndex(var0, var1, var2)); - } - - protected T get(int var0) { - T var1 = this.palette.valueFor(this.storage.get(var0)); - return (var1 == null) ? this.defaultValue : var1; - } - - public void read(ListTag var0, long[] var1) { - int var2 = Math.max(4, MCAMth.ceillog2(var0.size())); - if (var2 != this.bits) - setBits(var2); - this.palette.read(var0); - int var3 = var1.length * 64 / 4096; - if (this.palette == this.globalPalette) { - MCAPalette var4 = new MCAHashMapPalette<>(this.registry, var2, this.dummyPaletteResize, this.reader, this.writer); - var4.read(var0); - MCABitStorage var5 = new MCABitStorage(var2, 4096, var1); - for (int var6 = 0; var6 < 4096; var6++) - this.storage.set(var6, this.globalPalette.idFor(var4.valueFor(var5.get(var6)))); - } else if (var3 == this.bits) { - System.arraycopy(var1, 0, this.storage.getRaw(), 0, var1.length); - } else { - MCABitStorage var4 = new MCABitStorage(var3, 4096, var1); - for (int var5 = 0; var5 < 4096; var5++) - this.storage.set(var5, var4.get(var5)); - } - } - - public void write(CompoundTag var0, String var1, String var2) { - MCAHashMapPalette var3 = new MCAHashMapPalette<>(this.registry, this.bits, this.dummyPaletteResize, this.reader, this.writer); - T var4 = this.defaultValue; - int var5 = var3.idFor(this.defaultValue); - int[] var6 = new int[4096]; - for (int i = 0; i < 4096; i++) { - T t = get(i); - if (t != var4) { - var4 = t; - var5 = var3.idFor(t); - } - var6[i] = var5; - } - ListTag paletteList = (ListTag) ListTag.createUnchecked(CompoundTag.class); - var3.write(paletteList); - var0.put(var1, paletteList); - int var8 = Math.max(4, MCAMth.ceillog2(paletteList.size())); - MCABitStorage var9 = new MCABitStorage(var8, 4096); - for (int var10 = 0; var10 < var6.length; var10++) { - var9.set(var10, var6[var10]); - } - var0.putLongArray(var2, var9.getRaw()); - } - - public boolean maybeHas(Predicate var0) { - return this.palette.maybeHas(var0); - } - - public void count(MCACountConsumer var0) { - Int2IntOpenHashMap int2IntOpenHashMap = new Int2IntOpenHashMap(); - this.storage.getAll(var1 -> int2IntOpenHashMap.put(var1, int2IntOpenHashMap.get(var1) + 1)); - int2IntOpenHashMap.int2IntEntrySet().forEach(var1 -> var0.accept(this.palette.valueFor(var1.getIntKey()), var1.getIntValue())); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAQuartPos.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAQuartPos.java deleted file mode 100644 index a98f4d976..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAQuartPos.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -public final class MCAQuartPos { - public static final int BITS = 2; - - public static final int SIZE = 4; - - private static final int SECTION_TO_QUARTS_BITS = 2; - - public static int fromBlock(int var0) { - return var0 >> 2; - } - - public static int toBlock(int var0) { - return var0 << 2; - } - - public static int fromSection(int var0) { - return var0 << 2; - } - - public static int toSection(int var0) { - return var0 >> 2; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAWrappedPalettedContainer.java b/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAWrappedPalettedContainer.java deleted file mode 100644 index fcd7edd5c..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/mca/palette/MCAWrappedPalettedContainer.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.mca.palette; - -import com.volmit.iris.util.nbt.tag.CompoundTag; -import lombok.RequiredArgsConstructor; - -import java.util.function.Function; - -@RequiredArgsConstructor -public class MCAWrappedPalettedContainer implements MCAPaletteAccess { - private final MCAPalettedContainer container; - private final Function reader; - private final Function writer; - - public void setBlock(int x, int y, int z, CompoundTag data) { - container.set(x, y, z, writer.apply(data)); - } - - public CompoundTag getBlock(int x, int y, int z) { - return reader.apply(container.get(x, y, z)); - } - - public void writeToSection(CompoundTag tag) { - container.write(tag, "Palette", "BlockStates"); - } - - public void readFromSection(CompoundTag tag) { - container.read(tag.getListTag("Palette"), tag.getLongArrayTag("BlockStates").getValue()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/ArrayTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/ArrayTag.java deleted file mode 100644 index 9ec1bddc6..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/ArrayTag.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -import java.lang.reflect.Array; - -/** - * ArrayTag is an abstract representation of any NBT array tag. - * For implementations see {@link ByteArrayTag}, {@link IntArrayTag}, {@link LongArrayTag}. - * - * @param The array type. - */ -public abstract class ArrayTag extends Tag { - - public ArrayTag(T value) { - super(value); - if (!value.getClass().isArray()) { - throw new UnsupportedOperationException("type of array tag must be an array"); - } - } - - public int length() { - return Array.getLength(getValue()); - } - - @Override - public T getValue() { - return super.getValue(); - } - - @Override - public void setValue(T value) { - super.setValue(value); - } - - @Override - public String valueToString(int maxDepth) { - return arrayToString("", ""); - } - - protected String arrayToString(@SuppressWarnings("SameParameterValue") String prefix, @SuppressWarnings("SameParameterValue") String suffix) { - StringBuilder sb = new StringBuilder("[").append(prefix).append("".equals(prefix) ? "" : ";"); - for (int i = 0; i < length(); i++) { - sb.append(i == 0 ? "" : ",").append(Array.get(getValue(), i)).append(suffix); - } - sb.append("]"); - return sb.toString(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/ByteArrayTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/ByteArrayTag.java deleted file mode 100644 index 8b94784c3..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/ByteArrayTag.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -import java.util.Arrays; - -public class ByteArrayTag extends ArrayTag implements Comparable { - - public static final byte ID = 7; - public static final byte[] ZERO_VALUE = new byte[0]; - - public ByteArrayTag() { - super(ZERO_VALUE); - } - - public ByteArrayTag(byte[] value) { - super(value); - } - - @Override - public byte getID() { - return ID; - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && Arrays.equals(getValue(), ((ByteArrayTag) other).getValue()); - } - - @Override - public int hashCode() { - return Arrays.hashCode(getValue()); - } - - @Override - public int compareTo(ByteArrayTag other) { - return Integer.compare(length(), other.length()); - } - - @Override - public ByteArrayTag clone() { - return new ByteArrayTag(Arrays.copyOf(getValue(), length())); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/ByteTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/ByteTag.java deleted file mode 100644 index cabd9a107..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/ByteTag.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -public class ByteTag extends NumberTag implements Comparable { - - public static final byte ID = 1; - public static final byte ZERO_VALUE = 0; - - public ByteTag() { - super(ZERO_VALUE); - } - - public ByteTag(byte value) { - super(value); - } - - public ByteTag(boolean value) { - super((byte) (value ? 1 : 0)); - } - - @Override - public byte getID() { - return ID; - } - - public boolean asBoolean() { - return getValue() > 0; - } - - public void setValue(byte value) { - super.setValue(value); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && asByte() == ((ByteTag) other).asByte(); - } - - @Override - public int compareTo(ByteTag other) { - return getValue().compareTo(other.getValue()); - } - - @Override - public ByteTag clone() { - return new ByteTag(getValue()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/CompoundTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/CompoundTag.java deleted file mode 100644 index dab272071..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/CompoundTag.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -import com.volmit.iris.engine.data.io.MaxDepthIO; -import com.volmit.iris.util.collection.KMap; - -import java.util.*; -import java.util.function.BiConsumer; - -@SuppressWarnings("ALL") -public class CompoundTag extends Tag>> implements Iterable>>, Comparable, MaxDepthIO { - - public static final byte ID = 10; - - public CompoundTag() { - super(createEmptyValue()); - } - - private static Map> createEmptyValue() { - return new KMap<>(); - } - - @Override - public byte getID() { - return ID; - } - - public int size() { - return getValue().size(); - } - - public Tag remove(String key) { - return getValue().remove(key); - } - - public void clear() { - getValue().clear(); - } - - public boolean containsKey(String key) { - return getValue().containsKey(key); - } - - public boolean containsValue(Tag value) { - return getValue().containsValue(value); - } - - public Collection> values() { - return getValue().values(); - } - - public Set keySet() { - return getValue().keySet(); - } - - public Set>> entrySet() { - return new NonNullEntrySet<>(getValue().entrySet()); - } - - @Override - public Iterator>> iterator() { - return entrySet().iterator(); - } - - public void forEach(BiConsumer> action) { - getValue().forEach(action); - } - - public > C get(String key, Class type) { - Tag t = getValue().get(key); - if (t != null) { - return type.cast(t); - } - return null; - } - - public Tag get(String key) { - return getValue().get(key); - } - - public ByteTag getByteTag(String key) { - return get(key, ByteTag.class); - } - - public ShortTag getShortTag(String key) { - return get(key, ShortTag.class); - } - - public IntTag getIntTag(String key) { - return get(key, IntTag.class); - } - - public LongTag getLongTag(String key) { - return get(key, LongTag.class); - } - - public FloatTag getFloatTag(String key) { - return get(key, FloatTag.class); - } - - public DoubleTag getDoubleTag(String key) { - return get(key, DoubleTag.class); - } - - public StringTag getStringTag(String key) { - return get(key, StringTag.class); - } - - public ByteArrayTag getByteArrayTag(String key) { - return get(key, ByteArrayTag.class); - } - - public IntArrayTag getIntArrayTag(String key) { - return get(key, IntArrayTag.class); - } - - public LongArrayTag getLongArrayTag(String key) { - return get(key, LongArrayTag.class); - } - - public ListTag getListTag(String key) { - return get(key, ListTag.class); - } - - public CompoundTag getCompoundTag(String key) { - return get(key, CompoundTag.class); - } - - public boolean getBoolean(String key) { - Tag t = get(key); - return t instanceof ByteTag && ((ByteTag) t).asByte() > 0; - } - - public byte getByte(String key) { - ByteTag t = getByteTag(key); - return t == null ? ByteTag.ZERO_VALUE : t.asByte(); - } - - public short getShort(String key) { - ShortTag t = getShortTag(key); - return t == null ? ShortTag.ZERO_VALUE : t.asShort(); - } - - public int getInt(String key) { - IntTag t = getIntTag(key); - return t == null ? IntTag.ZERO_VALUE : t.asInt(); - } - - public long getLong(String key) { - LongTag t = getLongTag(key); - return t == null ? LongTag.ZERO_VALUE : t.asLong(); - } - - public float getFloat(String key) { - FloatTag t = getFloatTag(key); - return t == null ? FloatTag.ZERO_VALUE : t.asFloat(); - } - - public double getDouble(String key) { - DoubleTag t = getDoubleTag(key); - return t == null ? DoubleTag.ZERO_VALUE : t.asDouble(); - } - - public String getString(String key) { - StringTag t = getStringTag(key); - return t == null ? StringTag.ZERO_VALUE : t.getValue(); - } - - public byte[] getByteArray(String key) { - ByteArrayTag t = getByteArrayTag(key); - return t == null ? ByteArrayTag.ZERO_VALUE : t.getValue(); - } - - public int[] getIntArray(String key) { - IntArrayTag t = getIntArrayTag(key); - return t == null ? IntArrayTag.ZERO_VALUE : t.getValue(); - } - - public long[] getLongArray(String key) { - LongArrayTag t = getLongArrayTag(key); - return t == null ? LongArrayTag.ZERO_VALUE : t.getValue(); - } - - public Tag put(String key, Tag tag) { - return getValue().put(Objects.requireNonNull(key), Objects.requireNonNull(tag)); - } - - public Tag putBoolean(String key, boolean value) { - return put(key, new ByteTag(value)); - } - - public Tag putByte(String key, byte value) { - return put(key, new ByteTag(value)); - } - - public Tag putShort(String key, short value) { - return put(key, new ShortTag(value)); - } - - public Tag putInt(String key, int value) { - return put(key, new IntTag(value)); - } - - public Tag putLong(String key, long value) { - return put(key, new LongTag(value)); - } - - public Tag putFloat(String key, float value) { - return put(key, new FloatTag(value)); - } - - public Tag putDouble(String key, double value) { - return put(key, new DoubleTag(value)); - } - - public Tag putString(String key, String value) { - return put(key, new StringTag(value)); - } - - public Tag putByteArray(String key, byte[] value) { - return put(key, new ByteArrayTag(value)); - } - - public Tag putIntArray(String key, int[] value) { - return put(key, new IntArrayTag(value)); - } - - public Tag putLongArray(String key, long[] value) { - return put(key, new LongArrayTag(value)); - } - - @Override - public String valueToString(int maxDepth) { - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry> e : getValue().entrySet()) { - sb.append(first ? "" : ",") - .append(escapeString(e.getKey(), false)).append(":") - .append(e.getValue().toString(decrementMaxDepth(maxDepth))); - first = false; - } - sb.append("}"); - return sb.toString(); - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!super.equals(other) || size() != ((CompoundTag) other).size()) { - return false; - } - for (Map.Entry> e : getValue().entrySet()) { - Tag v; - if ((v = ((CompoundTag) other).get(e.getKey())) == null || !e.getValue().equals(v)) { - return false; - } - } - return true; - } - - @Override - public int compareTo(CompoundTag o) { - return Integer.compare(size(), o.getValue().size()); - } - - @Override - public CompoundTag clone() { - CompoundTag copy = new CompoundTag(); - for (Map.Entry> e : getValue().entrySet()) { - copy.put(e.getKey(), e.getValue().clone()); - } - return copy; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/DoubleTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/DoubleTag.java deleted file mode 100644 index cc66c0d50..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/DoubleTag.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -public class DoubleTag extends NumberTag implements Comparable { - - public static final byte ID = 6; - public static final double ZERO_VALUE = 0.0D; - - public DoubleTag() { - super(ZERO_VALUE); - } - - public DoubleTag(double value) { - super(value); - } - - @Override - public byte getID() { - return ID; - } - - public void setValue(double value) { - super.setValue(value); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && getValue().equals(((DoubleTag) other).getValue()); - } - - @Override - public int compareTo(DoubleTag other) { - return getValue().compareTo(other.getValue()); - } - - @Override - public DoubleTag clone() { - return new DoubleTag(getValue()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/EndTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/EndTag.java deleted file mode 100644 index 69bf51145..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/EndTag.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -public final class EndTag extends Tag { - - public static final byte ID = 0; - public static final EndTag INSTANCE = new EndTag(); - - private EndTag() { - super(null); - } - - @Override - public byte getID() { - return ID; - } - - @Override - protected Void checkValue(Void value) { - return value; - } - - @Override - public String valueToString(int maxDepth) { - return "\"end\""; - } - - @Override - public EndTag clone() { - return INSTANCE; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/FloatTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/FloatTag.java deleted file mode 100644 index 1f626de61..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/FloatTag.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -public class FloatTag extends NumberTag implements Comparable { - - public static final byte ID = 5; - public static final float ZERO_VALUE = 0.0F; - - public FloatTag() { - super(ZERO_VALUE); - } - - public FloatTag(float value) { - super(value); - } - - @Override - public byte getID() { - return ID; - } - - public void setValue(float value) { - super.setValue(value); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && getValue().equals(((FloatTag) other).getValue()); - } - - @Override - public int compareTo(FloatTag other) { - return getValue().compareTo(other.getValue()); - } - - @Override - public FloatTag clone() { - return new FloatTag(getValue()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/IntArrayTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/IntArrayTag.java deleted file mode 100644 index 9fde7135b..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/IntArrayTag.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -import java.util.Arrays; - -public class IntArrayTag extends ArrayTag implements Comparable { - - public static final byte ID = 11; - public static final int[] ZERO_VALUE = new int[0]; - - public IntArrayTag() { - super(ZERO_VALUE); - } - - public IntArrayTag(int[] value) { - super(value); - } - - @Override - public byte getID() { - return ID; - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && Arrays.equals(getValue(), ((IntArrayTag) other).getValue()); - } - - @Override - public int hashCode() { - return Arrays.hashCode(getValue()); - } - - @Override - public int compareTo(IntArrayTag other) { - return Integer.compare(length(), other.length()); - } - - @Override - public IntArrayTag clone() { - return new IntArrayTag(Arrays.copyOf(getValue(), length())); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/IntTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/IntTag.java deleted file mode 100644 index 3949bb98e..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/IntTag.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -public class IntTag extends NumberTag implements Comparable { - - public static final byte ID = 3; - public static final int ZERO_VALUE = 0; - - public IntTag() { - super(ZERO_VALUE); - } - - public IntTag(int value) { - super(value); - } - - @Override - public byte getID() { - return ID; - } - - public void setValue(int value) { - super.setValue(value); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && asInt() == ((IntTag) other).asInt(); - } - - @Override - public int compareTo(IntTag other) { - return getValue().compareTo(other.getValue()); - } - - @Override - public IntTag clone() { - return new IntTag(getValue()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/ListTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/ListTag.java deleted file mode 100644 index d096a6f52..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/ListTag.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -import com.volmit.iris.engine.data.io.MaxDepthIO; -import com.volmit.iris.util.collection.KList; - -import java.util.*; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -/** - * ListTag represents a typed List in the nbt structure. - * An empty {@link ListTag} created using {@link ListTag#createUnchecked(Class)} will be of unknown type - * and returns an {@link EndTag}{@code .class} in {@link ListTag#getTypeClass()}. - * The type of an empty untyped {@link ListTag} can be set by using any of the {@code add()} - * methods or any of the {@code as...List()} methods. - */ -@SuppressWarnings("ALL") -public class ListTag> extends Tag> implements Iterable, Comparable>, MaxDepthIO { - - public static final byte ID = 9; - - private Class typeClass = null; - - private ListTag() { - super(createEmptyValue(3)); - } - - /** - * @param typeClass The exact class of the elements - * @throws IllegalArgumentException When {@code typeClass} is {@link EndTag}{@code .class} - * @throws NullPointerException When {@code typeClass} is {@code null} - */ - public ListTag(Class typeClass) throws IllegalArgumentException, NullPointerException { - super(createEmptyValue(3)); - if (typeClass == EndTag.class) { - throw new IllegalArgumentException("cannot create ListTag with EndTag elements"); - } - this.typeClass = Objects.requireNonNull(typeClass); - } - - /** - *

Creates a non-type-safe ListTag. Its element type will be set after the first - * element was added.

- * - *

This is an internal helper method for cases where the element type is not known - * at construction time. Use {@link #ListTag(Class)} when the type is known.

- * - * @return A new non-type-safe ListTag - */ - public static ListTag createUnchecked(Class typeClass) { - ListTag list = new ListTag<>(); - list.typeClass = typeClass; - return list; - } - - /** - *

Creates an empty mutable list to be used as empty value of ListTags.

- * - * @param Type of the list elements - * @param initialCapacity The initial capacity of the returned List - * @return An instance of {@link List} with an initial capacity of 3 - */ - private static List createEmptyValue(@SuppressWarnings("SameParameterValue") int initialCapacity) { - return new KList<>(initialCapacity); - } - - public ListTag makeAtomic() { - setValue(new CopyOnWriteArrayList<>(getValue())); - return this; - } - - @Override - public byte getID() { - return ID; - } - - public Class getTypeClass() { - return typeClass == null ? EndTag.class : typeClass; - } - - public int size() { - return getValue().size(); - } - - public T remove(int index) { - return getValue().remove(index); - } - - public void clear() { - getValue().clear(); - } - - public boolean contains(T t) { - return getValue().contains(t); - } - - public boolean containsAll(Collection> tags) { - return getValue().containsAll(tags); - } - - public void sort(Comparator comparator) { - getValue().sort(comparator); - } - - @Override - public Iterator iterator() { - return getValue().iterator(); - } - - @Override - public void forEach(Consumer action) { - getValue().forEach(action); - } - - public T set(int index, T t) { - return getValue().set(index, Objects.requireNonNull(t)); - } - - /** - * Adds a Tag to this ListTag after the last index. - * - * @param t The element to be added. - */ - public void add(T t) { - add(size(), t); - } - - public void add(int index, T t) { - Objects.requireNonNull(t); - if (typeClass == null || typeClass == EndTag.class) { - typeClass = t.getClass(); - } else if (typeClass != t.getClass()) { - throw new ClassCastException( - String.format("cannot add %s to ListTag<%s>", - t.getClass().getSimpleName(), - typeClass.getSimpleName())); - } - getValue().add(index, t); - } - - public void addAll(Collection t) { - for (T tt : t) { - add(tt); - } - } - - public void addAll(int index, Collection t) { - int i = 0; - for (T tt : t) { - add(index + i, tt); - i++; - } - } - - public void addBoolean(boolean value) { - addUnchecked(new ByteTag(value)); - } - - public void addByte(byte value) { - addUnchecked(new ByteTag(value)); - } - - public void addShort(short value) { - addUnchecked(new ShortTag(value)); - } - - public void addInt(int value) { - addUnchecked(new IntTag(value)); - } - - public void addLong(long value) { - addUnchecked(new LongTag(value)); - } - - public void addFloat(float value) { - addUnchecked(new FloatTag(value)); - } - - public void addDouble(double value) { - addUnchecked(new DoubleTag(value)); - } - - public void addString(String value) { - addUnchecked(new StringTag(value)); - } - - public void addByteArray(byte[] value) { - addUnchecked(new ByteArrayTag(value)); - } - - public void addIntArray(int[] value) { - addUnchecked(new IntArrayTag(value)); - } - - public void addLongArray(long[] value) { - addUnchecked(new LongArrayTag(value)); - } - - public T get(int index) { - return getValue().get(index); - } - - public int indexOf(T t) { - return getValue().indexOf(t); - } - - @SuppressWarnings("unchecked") - public > ListTag asTypedList(Class type) { - checkTypeClass(type); - typeClass = type; - return (ListTag) this; - } - - public ListTag asByteTagList() { - return asTypedList(ByteTag.class); - } - - public ListTag asShortTagList() { - return asTypedList(ShortTag.class); - } - - public ListTag asIntTagList() { - return asTypedList(IntTag.class); - } - - public ListTag asLongTagList() { - return asTypedList(LongTag.class); - } - - public ListTag asFloatTagList() { - return asTypedList(FloatTag.class); - } - - public ListTag asDoubleTagList() { - return asTypedList(DoubleTag.class); - } - - public ListTag asStringTagList() { - return asTypedList(StringTag.class); - } - - public ListTag asByteArrayTagList() { - return asTypedList(ByteArrayTag.class); - } - - public ListTag asIntArrayTagList() { - return asTypedList(IntArrayTag.class); - } - - public ListTag asLongArrayTagList() { - return asTypedList(LongArrayTag.class); - } - - @SuppressWarnings("unchecked") - public ListTag> asListTagList() { - checkTypeClass(ListTag.class); - typeClass = ListTag.class; - return (ListTag>) this; - } - - public ListTag asCompoundTagList() { - return asTypedList(CompoundTag.class); - } - - @Override - public String valueToString(int maxDepth) { - StringBuilder sb = new StringBuilder("{\"type\":\"").append(getTypeClass().getSimpleName()).append("\",\"list\":["); - for (int i = 0; i < size(); i++) { - sb.append(i > 0 ? "," : "").append(get(i).valueToString(decrementMaxDepth(maxDepth))); - } - sb.append("]}"); - return sb.toString(); - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!super.equals(other) || size() != ((ListTag) other).size() || getTypeClass() != ((ListTag) other).getTypeClass()) { - return false; - } - for (int i = 0; i < size(); i++) { - if (!get(i).equals(((ListTag) other).get(i))) { - return false; - } - } - return true; - } - - @Override - public int hashCode() { - return Objects.hash(getValue().hashCode()); - } - - @Override - public int compareTo(ListTag o) { - return Integer.compare(size(), o.getValue().size()); - } - - @SuppressWarnings("unchecked") - @Override - public ListTag clone() { - ListTag copy = new ListTag<>(); - // assure type safety for clone - copy.typeClass = typeClass; - for (T t : getValue()) { - copy.add((T) t.clone()); - } - return copy; - } - - //TODO: make private - @SuppressWarnings("unchecked") - public void addUnchecked(Tag tag) { - if (typeClass != null && typeClass != tag.getClass() && typeClass != EndTag.class) { - throw new IllegalArgumentException(String.format( - "cannot add %s to ListTag<%s>", - tag.getClass().getSimpleName(), typeClass.getSimpleName())); - } - add(size(), (T) tag); - } - - private void checkTypeClass(Class clazz) { - if (typeClass != null && typeClass != EndTag.class && typeClass != clazz) { - throw new ClassCastException(String.format( - "cannot cast ListTag<%s> to ListTag<%s>", - typeClass.getSimpleName(), clazz.getSimpleName())); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/LongArrayTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/LongArrayTag.java deleted file mode 100644 index 4707b5f55..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/LongArrayTag.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -import java.util.Arrays; - -public class LongArrayTag extends ArrayTag implements Comparable { - - public static final byte ID = 12; - public static final long[] ZERO_VALUE = new long[0]; - - public LongArrayTag() { - super(ZERO_VALUE); - } - - public LongArrayTag(long[] value) { - super(value); - } - - @Override - public byte getID() { - return ID; - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && Arrays.equals(getValue(), ((LongArrayTag) other).getValue()); - } - - @Override - public int hashCode() { - return Arrays.hashCode(getValue()); - } - - @Override - public int compareTo(LongArrayTag other) { - return Integer.compare(length(), other.length()); - } - - @Override - public LongArrayTag clone() { - return new LongArrayTag(Arrays.copyOf(getValue(), length())); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/LongTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/LongTag.java deleted file mode 100644 index d240b158f..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/LongTag.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -public class LongTag extends NumberTag implements Comparable { - - public static final byte ID = 4; - public static final long ZERO_VALUE = 0L; - - public LongTag() { - super(ZERO_VALUE); - } - - public LongTag(long value) { - super(value); - } - - @Override - public byte getID() { - return ID; - } - - public void setValue(long value) { - super.setValue(value); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && asLong() == ((LongTag) other).asLong(); - } - - @Override - public int compareTo(LongTag other) { - return getValue().compareTo(other.getValue()); - } - - @Override - public LongTag clone() { - return new LongTag(getValue()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/NonNullEntrySet.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/NonNullEntrySet.java deleted file mode 100644 index 3053f757b..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/NonNullEntrySet.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -/** - * A decorator for the Set returned by CompoundTag#entrySet() - * that disallows setting null values. - */ -@SuppressWarnings("ClassCanBeRecord") -class NonNullEntrySet implements Set> { - - private final Set> set; - - NonNullEntrySet(Set> set) { - this.set = set; - } - - @Override - public int size() { - return set.size(); - } - - @Override - public boolean isEmpty() { - return set.isEmpty(); - } - - @Override - public boolean contains(Object o) { - return set.contains(o); - } - - @Override - public Iterator> iterator() { - return new NonNullEntrySetIterator(set.iterator()); - } - - @Override - public Object[] toArray() { - return set.toArray(); - } - - @Override - public T[] toArray(T[] a) { - return set.toArray(a); - } - - @Override - public boolean add(Map.Entry kvEntry) { - return set.add(kvEntry); - } - - @Override - public boolean remove(Object o) { - return set.remove(o); - } - - @Override - public boolean containsAll(Collection c) { - return set.containsAll(c); - } - - @Override - public boolean addAll(Collection> c) { - return set.addAll(c); - } - - @Override - public boolean retainAll(Collection c) { - return set.retainAll(c); - } - - @Override - public boolean removeAll(Collection c) { - return set.removeAll(c); - } - - @Override - public void clear() { - set.clear(); - } - - class NonNullEntrySetIterator implements Iterator> { - - private final Iterator> iterator; - - NonNullEntrySetIterator(Iterator> iterator) { - this.iterator = iterator; - } - - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - public Map.Entry next() { - return new NonNullEntry(iterator.next()); - } - } - - class NonNullEntry implements Map.Entry { - - private final Map.Entry entry; - - NonNullEntry(Map.Entry entry) { - this.entry = entry; - } - - @Override - public K getKey() { - return entry.getKey(); - } - - @Override - public V getValue() { - return entry.getValue(); - } - - @Override - public V setValue(V value) { - if (value == null) { - throw new NullPointerException(getClass().getSimpleName() + " does not allow setting null"); - } - return entry.setValue(value); - } - - @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") - @Override - public boolean equals(Object o) { - return entry.equals(o); - } - - @Override - public int hashCode() { - return entry.hashCode(); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/NumberTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/NumberTag.java deleted file mode 100644 index ca66a70f1..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/NumberTag.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -public abstract class NumberTag> extends Tag { - - public NumberTag(T value) { - super(value); - } - - public byte asByte() { - return getValue().byteValue(); - } - - public short asShort() { - return getValue().shortValue(); - } - - public int asInt() { - return getValue().intValue(); - } - - public long asLong() { - return getValue().longValue(); - } - - public float asFloat() { - return getValue().floatValue(); - } - - public double asDouble() { - return getValue().doubleValue(); - } - - @Override - public String valueToString(int maxDepth) { - return getValue().toString(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/ShortTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/ShortTag.java deleted file mode 100644 index b92e939fe..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/ShortTag.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -public class ShortTag extends NumberTag implements Comparable { - - public static final byte ID = 2; - public static final short ZERO_VALUE = 0; - - public ShortTag() { - super(ZERO_VALUE); - } - - public ShortTag(short value) { - super(value); - } - - @Override - public byte getID() { - return ID; - } - - public void setValue(short value) { - super.setValue(value); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && asShort() == ((ShortTag) other).asShort(); - } - - @Override - public int compareTo(ShortTag other) { - return getValue().compareTo(other.getValue()); - } - - @Override - public ShortTag clone() { - return new ShortTag(getValue()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/StringTag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/StringTag.java deleted file mode 100644 index b135dc53d..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/StringTag.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -public class StringTag extends Tag implements Comparable { - - public static final byte ID = 8; - public static final String ZERO_VALUE = ""; - - public StringTag() { - super(ZERO_VALUE); - } - - public StringTag(String value) { - super(value); - } - - @Override - public byte getID() { - return ID; - } - - @Override - public String getValue() { - return super.getValue(); - } - - @Override - public void setValue(String value) { - super.setValue(value); - } - - @Override - public String valueToString(int maxDepth) { - return escapeString(getValue(), false); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && getValue().equals(((StringTag) other).getValue()); - } - - @Override - public int compareTo(StringTag o) { - return getValue().compareTo(o.getValue()); - } - - @Override - public StringTag clone() { - return new StringTag(getValue()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/nbt/tag/Tag.java b/core/src/main/java/com/volmit/iris/util/nbt/tag/Tag.java deleted file mode 100644 index 5c9cf6c56..000000000 --- a/core/src/main/java/com/volmit/iris/util/nbt/tag/Tag.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.nbt.tag; - -import com.volmit.iris.engine.data.io.MaxDepthReachedException; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Base class for all NBT tags. - * - *

Nesting

- *

All methods serializing instances or deserializing data track the nesting levels to prevent - * circular references or malicious data which could, when deserialized, result in thousands - * of instances causing a denial of service.

- * - *

These methods have a parameter for the maximum nesting depth they are allowed to traverse. A - * value of {@code 0} means that only the object itself, but no nested objects may be processed. - * If an instance is nested further than allowed, a {@link MaxDepthReachedException} will be thrown. - * Providing a negative maximum nesting depth will cause an {@code IllegalArgumentException} - * to be thrown.

- * - *

Some methods do not provide a parameter to specify the maximum nesting depth, but instead use - * {@link #DEFAULT_MAX_DEPTH}, which is also the maximum used by Minecraft. This is documented for - * the respective methods.

- * - *

If custom NBT tags contain objects other than NBT tags, which can be nested as well, then there - * is no guarantee that {@code MaxDepthReachedException}s are thrown for them. The respective class - * will document this behavior accordingly.

- * - * @param The type of the contained value - */ -public abstract class Tag implements Cloneable { - - /** - * The default maximum depth of the NBT structure. - */ - public static final int DEFAULT_MAX_DEPTH = 512; - - private static final Map ESCAPE_CHARACTERS; - private static final Pattern ESCAPE_PATTERN = Pattern.compile("[\\\\\n\t\r\"]"); - private static final Pattern NON_QUOTE_PATTERN = Pattern.compile("[a-zA-Z0-9_\\-+]+"); - - static { - final Map temp = new HashMap<>(); - temp.put("\\", "\\\\\\\\"); - temp.put("\n", "\\\\n"); - temp.put("\t", "\\\\t"); - temp.put("\r", "\\\\r"); - temp.put("\"", "\\\\\""); - //noinspection Java9CollectionFactory - ESCAPE_CHARACTERS = Collections.unmodifiableMap(temp); - } - - private T value; - - /** - * Initializes this Tag with some value. If the value is {@code null}, it will - * throw a {@code NullPointerException} - * - * @param value The value to be set for this Tag. - */ - public Tag(T value) { - setValue(value); - } - - /** - * Escapes a string to fit into a JSON-like string representation for Minecraft - * or to create the JSON string representation of a Tag returned from {@link Tag#toString()} - * - * @param s The string to be escaped. - * @param lenient {@code true} if it should force double quotes ({@code "}) at the start and - * the end of the string. - * @return The escaped string. - */ - @SuppressWarnings("StringBufferMayBeStringBuilder") - protected static String escapeString(String s, @SuppressWarnings("SameParameterValue") boolean lenient) { - StringBuffer sb = new StringBuffer(); - Matcher m = ESCAPE_PATTERN.matcher(s); - while (m.find()) { - m.appendReplacement(sb, ESCAPE_CHARACTERS.get(m.group())); - } - m.appendTail(sb); - m = NON_QUOTE_PATTERN.matcher(s); - if (!lenient || !m.matches()) { - sb.insert(0, "\"").append("\""); - } - return sb.toString(); - } - - /** - * @return This Tag's ID, usually used for serialization and deserialization. - */ - public abstract byte getID(); - - /** - * @return The value of this Tag. - */ - public T getValue() { - return value; - } - - /** - * Sets the value for this Tag directly. - * - * @param value The value to be set. - * @throws NullPointerException If the value is null - */ - protected void setValue(T value) { - this.value = checkValue(value); - } - - /** - * Checks if the value {@code value} is {@code null}. - * - * @param value The value to check - * @return The parameter {@code value} - * @throws NullPointerException If {@code value} was {@code null} - */ - protected T checkValue(T value) { - return Objects.requireNonNull(value); - } - - /** - * Calls {@link Tag#toString(int)} with an initial depth of {@code 0}. - * - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. - * @see Tag#toString(int) - */ - @Override - public final String toString() { - return toString(DEFAULT_MAX_DEPTH); - } - - /** - * Creates a string representation of this Tag in a valid JSON format. - * - * @param maxDepth The maximum nesting depth. - * @return The string representation of this Tag. - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. - */ - public String toString(int maxDepth) { - return "{\"type\":\"" + getClass().getSimpleName() + "\"," + - "\"value\":" + valueToString(maxDepth) + "}"; - } - - /** - * Calls {@link Tag#valueToString(int)} with {@link Tag#DEFAULT_MAX_DEPTH}. - * - * @return The string representation of the value of this Tag. - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. - */ - public String valueToString() { - return valueToString(DEFAULT_MAX_DEPTH); - } - - /** - * Returns a JSON representation of the value of this Tag. - * - * @param maxDepth The maximum nesting depth. - * @return The string representation of the value of this Tag. - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. - */ - public abstract String valueToString(int maxDepth); - - /** - * Returns whether this Tag and some other Tag are equal. - * They are equal if {@code other} is not {@code null} and they are of the same class. - * Custom Tag implementations should overwrite this but check the result - * of this {@code super}-method while comparing. - * - * @param other The Tag to compare to. - * @return {@code true} if they are equal based on the conditions mentioned above. - */ - @Override - public boolean equals(Object other) { - return other != null && getClass() == other.getClass(); - } - - /** - * Calculates the hash code of this Tag. Tags which are equal according to {@link Tag#equals(Object)} - * must return an equal hash code. - * - * @return The hash code of this Tag. - */ - @Override - public int hashCode() { - return value.hashCode(); - } - - /** - * Creates a clone of this Tag. - * - * @return A clone of this Tag. - */ - public abstract Tag clone(); -} diff --git a/core/src/main/java/com/volmit/iris/util/network/DL.java b/core/src/main/java/com/volmit/iris/util/network/DL.java deleted file mode 100644 index 5eccf5a7d..000000000 --- a/core/src/main/java/com/volmit/iris/util/network/DL.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.network; - -import com.volmit.iris.util.collection.KSet; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.scheduling.ChronoLatch; - -import java.io.*; -import java.net.URL; -import java.net.URLConnection; -import java.util.Arrays; - -public abstract class DL { - protected File d; - protected URL u; - protected ChronoLatch latch; - protected KSet flags; - protected MeteredOutputStream o; - protected DownloadState state; - protected int timeout; - protected long size; - protected long start; - protected long downloaded; - protected long currentChunk; - protected long lastChunk; - protected long bps; - protected int bufferSize; - protected long lastPull; - protected DownloadMonitor m; - - public DL(URL u, File d, DownloadFlag... downloadFlags) { - this.d = d; - this.u = u; - size = -1; - lastPull = -1; - downloaded = 0; - bufferSize = 8192 * 32; - currentChunk = 0; - lastChunk = -1; - bps = -1; - start = -1; - timeout = 10000; - state = DownloadState.NEW; - flags = new KSet<>(); - latch = new ChronoLatch(500); - - flags.addAll(Arrays.asList(downloadFlags)); - } - - public void monitor(DownloadMonitor m) { - this.m = m; - } - - public void update() { - if (m != null) { - m.onUpdate(state, getProgress(), getElapsed(), getTimeLeft(), bps, getDiskBytesPerSecond(), size, downloaded, bufferSize, getBufferUse()); - } - } - - public boolean hasFlag(DownloadFlag f) { - return flags.contains(f); - } - - public boolean isState(DownloadState s) { - return state.equals(s); - } - - protected void state(DownloadState s) { - this.state = s; - update(); - } - - public int getBufferSize() { - return bufferSize; - } - - public void start() throws IOException { - if (!isState(DownloadState.NEW)) { - throw new DownloadException("Cannot start download while " + state.toString()); - } - - state(DownloadState.STARTING); - - if (hasFlag(DownloadFlag.CALCULATE_SIZE)) { - size = calculateSize(); - } - - start = System.currentTimeMillis(); - downloaded = 0; - bps = 0; - lastChunk = System.currentTimeMillis(); - o = new MeteredOutputStream(new FileOutputStream(d), 100); - openStream(); - state(DownloadState.DOWNLOADING); - } - - protected abstract long download() throws IOException; - - protected abstract void openStream() throws IOException; - - protected abstract void closeStream() throws IOException; - - public void downloadChunk() throws IOException { - if (!isState(DownloadState.DOWNLOADING)) { - throw new DownloadException("Cannot download while " + state.toString()); - } - - long d = download(); - lastPull = d; - - if (d < 0) { - finishDownload(); - return; - } - - downloaded += d; - currentChunk += d; - - double chunkTime = (double) (System.currentTimeMillis() - lastChunk) / 1000D; - bps = (long) ((double) currentChunk / chunkTime); - - if (latch.flip()) { - update(); - } - } - - public double getBufferUse() { - return (double) lastPull / (double) bufferSize; - } - - private void finishDownload() throws IOException { - if (!isState(DownloadState.NEW)) { - throw new DownloadException("Cannot finish download while " + state.toString()); - } - - closeStream(); - o.close(); - state(DownloadState.COMPLETE); - } - - public long getElapsed() { - return System.currentTimeMillis() - start; - } - - public long getRemaining() { - return size - downloaded; - } - - public long getTimeLeft() { - return (long) (((double) getRemaining() / (double) bps) * 1000D); - } - - public long getDiskBytesPerSecond() { - if (o == null) { - return -1; - } - - return o.getBps(); - } - - public long getBytesPerSecond() { - return bps; - } - - public double getProgress() { - return hasProgress() ? ((double) downloaded / (double) size) : -1D; - } - - public boolean hasProgress() { - return size > 0; - } - - private long calculateSize() throws IOException { - URLConnection c = u.openConnection(); - c.setConnectTimeout(timeout); - c.setReadTimeout(timeout); - c.connect(); - return c.getContentLengthLong(); - } - - public enum DownloadFlag { - CALCULATE_SIZE - } - - public enum DownloadState { - NEW, - STARTING, - DOWNLOADING, - FINALIZING, - COMPLETE, - FAILED - } - - public static class ThrottledDownload extends Download { - private final long mbps; - - public ThrottledDownload(URL u, File d, long mbps, DownloadFlag... downloadFlags) { - super(u, d, downloadFlags); - this.mbps = mbps; - } - - @Override - protected long download() throws IOException { - if (getBytesPerSecond() > mbps) { - try { - Thread.sleep(40); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - return IO.transfer(in, o, 8192, mbps / 20); - } - - return IO.transfer(in, o, 8192, bufferSize); - } - } - - public static class DoubleBufferedDownload extends Download { - protected BufferedOutputStream os; - - public DoubleBufferedDownload(URL u, File d, DownloadFlag... downloadFlags) { - super(u, d, downloadFlags); - } - - @Override - protected void openStream() throws IOException { - os = new BufferedOutputStream(o, 8192 * 16); - in = new BufferedInputStream(u.openStream(), 8192 * 16); - buf = new byte[8192 * 2]; - } - } - - public static class Download extends DL { - protected InputStream in; - protected byte[] buf; - - public Download(URL u, File d, DownloadFlag... downloadFlags) { - super(u, d, downloadFlags); - } - - @Override - protected long download() throws IOException { - return IO.transfer(in, o, buf, bufferSize); - } - - @Override - protected void openStream() throws IOException { - in = u.openStream(); - buf = new byte[8192]; - } - - @Override - protected void closeStream() throws IOException { - in.close(); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/network/DownloadException.java b/core/src/main/java/com/volmit/iris/util/network/DownloadException.java deleted file mode 100644 index 970f6691e..000000000 --- a/core/src/main/java/com/volmit/iris/util/network/DownloadException.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.network; - -import java.io.IOException; - -public class DownloadException extends IOException { - private static final long serialVersionUID = 5137918663903349839L; - - public DownloadException() { - super(); - } - - public DownloadException(String message, Throwable cause) { - super(message, cause); - } - - public DownloadException(String message) { - super(message); - } - - public DownloadException(Throwable cause) { - super(cause); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/network/DownloadMonitor.java b/core/src/main/java/com/volmit/iris/util/network/DownloadMonitor.java deleted file mode 100644 index 585ab84db..000000000 --- a/core/src/main/java/com/volmit/iris/util/network/DownloadMonitor.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.network; - -@FunctionalInterface -public interface DownloadMonitor { - void onUpdate(DL.DownloadState state, double progress, long elapsed, long estimated, long bps, long iobps, long size, long downloaded, long buffer, double bufferuse); -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/network/MeteredInputStream.java b/core/src/main/java/com/volmit/iris/util/network/MeteredInputStream.java deleted file mode 100644 index a7d37ddf3..000000000 --- a/core/src/main/java/com/volmit/iris/util/network/MeteredInputStream.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.network; - -import java.io.IOException; -import java.io.InputStream; - -public class MeteredInputStream extends InputStream { - private final InputStream os; - private long written; - private long totalWritten; - private long since; - private boolean auto; - private long interval; - private long bps; - - public MeteredInputStream(InputStream os, long interval) { - this.os = os; - written = 0; - totalWritten = 0; - auto = true; - this.interval = interval; - bps = 0; - since = System.currentTimeMillis(); - } - - public MeteredInputStream(InputStream os) { - this(os, 100); - auto = false; - } - - @Override - public int read() throws IOException { - written++; - totalWritten++; - - if (auto && System.currentTimeMillis() - getSince() > interval) { - pollRead(); - } - - return os.read(); - } - - public long getSince() { - return since; - } - - public long getRead() { - return written; - } - - public long pollRead() { - long w = written; - written = 0; - double secondsElapsedSince = (double) (System.currentTimeMillis() - since) / 1000.0; - bps = (long) ((double) w / secondsElapsedSince); - since = System.currentTimeMillis(); - - return w; - } - - public void close() throws IOException { - os.close(); - } - - public boolean isAuto() { - return auto; - } - - public void setAuto(boolean auto) { - this.auto = auto; - } - - public long getInterval() { - return interval; - } - - public void setInterval(long interval) { - this.interval = interval; - } - - public long getTotalRead() { - return totalWritten; - } - - public long getBps() { - return bps; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/network/MeteredOutputStream.java b/core/src/main/java/com/volmit/iris/util/network/MeteredOutputStream.java deleted file mode 100644 index 105f4f56e..000000000 --- a/core/src/main/java/com/volmit/iris/util/network/MeteredOutputStream.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.network; - -import java.io.IOException; -import java.io.OutputStream; - -public class MeteredOutputStream extends OutputStream { - private final OutputStream os; - private long written; - private long totalWritten; - private long since; - private boolean auto; - private long interval; - private long bps; - - public MeteredOutputStream(OutputStream os, long interval) { - this.os = os; - written = 0; - totalWritten = 0; - auto = true; - this.interval = interval; - bps = 0; - since = System.currentTimeMillis(); - } - - public MeteredOutputStream(OutputStream os) { - this(os, 100); - auto = false; - } - - @Override - public void write(int b) throws IOException { - os.write(b); - written++; - totalWritten++; - - if (auto && System.currentTimeMillis() - getSince() > interval) { - pollWritten(); - } - } - - public long getSince() { - return since; - } - - public long getWritten() { - return written; - } - - public long pollWritten() { - long w = written; - written = 0; - double secondsElapsedSince = (double) (System.currentTimeMillis() - since) / 1000.0; - bps = (long) ((double) w / secondsElapsedSince); - since = System.currentTimeMillis(); - return w; - } - - public void close() throws IOException { - os.close(); - } - - public boolean isAuto() { - return auto; - } - - public void setAuto(boolean auto) { - this.auto = auto; - } - - public long getInterval() { - return interval; - } - - public void setInterval(long interval) { - this.interval = interval; - } - - public long getTotalWritten() { - return totalWritten; - } - - public long getBps() { - return bps; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/noise/CNG.java b/core/src/main/java/com/volmit/iris/util/noise/CNG.java deleted file mode 100644 index a17fee3f0..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/CNG.java +++ /dev/null @@ -1,527 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.IRare; -import com.volmit.iris.engine.object.NoiseStyle; -import com.volmit.iris.util.cache.FloatCache; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.function.NoiseInjector; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import com.volmit.iris.util.stream.ProceduralStream; -import com.volmit.iris.util.stream.arithmetic.FittedStream; -import com.volmit.iris.util.stream.sources.CNGStream; -import lombok.Data; - -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.List; - -@Data -public class CNG { - public static final NoiseInjector ADD = (s, v) -> new double[]{s + v, 1}; - public static final NoiseInjector SRC_SUBTRACT = (s, v) -> new double[]{s - v < 0 ? 0 : s - v, -1}; - public static final NoiseInjector DST_SUBTRACT = (s, v) -> new double[]{v - s < 0 ? 0 : s - v, -1}; - public static final NoiseInjector MULTIPLY = (s, v) -> new double[]{s * v, 0}; - public static final NoiseInjector MAX = (s, v) -> new double[]{Math.max(s, v), 0}; - public static final NoiseInjector MIN = (s, v) -> new double[]{Math.min(s, v), 0}; - public static final NoiseInjector SRC_MOD = (s, v) -> new double[]{s % v, 0}; - public static final NoiseInjector SRC_POW = (s, v) -> new double[]{Math.pow(s, v), 0}; - public static final NoiseInjector DST_MOD = (s, v) -> new double[]{v % s, 0}; - public static final NoiseInjector DST_POW = (s, v) -> new double[]{Math.pow(v, s), 0}; - public static long hits = 0; - public static long creates = 0; - private final double opacity; - private double scale; - private double bakedScale; - private double fscale; - private boolean trueFracturing = false; - private KList children; - private CNG fracture; - private FloatCache cache; - private NoiseGenerator generator; - private NoiseInjector injector; - private RNG rng; - private boolean noscale; - private int oct; - private double patch; - private double up; - private double down; - private double power; - private NoiseStyle leakStyle; - private ProceduralStream customGenerator; - - public CNG(RNG random) { - this(random, 1); - } - - public CNG(RNG random, int octaves) { - this(random, 1D, octaves); - } - - public CNG(RNG random, double opacity, int octaves) { - this(random, NoiseType.SIMPLEX, opacity, octaves); - } - - public CNG(RNG random, NoiseType type, double opacity, int octaves) { - this(random, type.create(random.nextParallelRNG((long) ((1113334944L * opacity) + 12922 + octaves)).lmax()), opacity, octaves); - } - - public CNG(RNG random, NoiseGenerator generator, double opacity, int octaves) { - customGenerator = null; - creates++; - noscale = generator.isNoScale(); - this.oct = octaves; - this.rng = random; - power = 1; - scale = 1; - patch = 1; - bakedScale = 1; - fscale = 1; - down = 0; - up = 0; - fracture = null; - this.generator = generator; - this.opacity = opacity; - this.injector = ADD; - - if (generator instanceof OctaveNoise) { - ((OctaveNoise) generator).setOctaves(octaves); - } - } - - public static CNG signature(RNG rng) { - return signature(rng, NoiseType.SIMPLEX); - } - - public static CNG signatureHalf(RNG rng) { - return signatureHalf(rng, NoiseType.SIMPLEX); - } - - public static CNG signatureThick(RNG rng) { - return signatureThick(rng, NoiseType.SIMPLEX); - } - - public static CNG signatureDouble(RNG rng) { - return signatureDouble(rng, NoiseType.SIMPLEX); - } - - public static CNG signatureDouble(RNG rng, NoiseType t) { - return signatureThick(rng, t).fractureWith(signature(rng.nextParallelRNG(4956)), 93); - } - - public static CNG signatureDoubleFast(RNG rng, NoiseType t, NoiseType f) { - return signatureThickFast(rng, t, f) - .fractureWith(signatureFast(rng.nextParallelRNG(4956), t, f), 93); - } - - public static CNG signature(RNG rng, NoiseType t) { - // @NoArgsConstructor - return new CNG(rng.nextParallelRNG(17), t, 1D, 1).fractureWith(new CNG(rng.nextParallelRNG(18), 1, 1).scale(0.9).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.21).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.9), 620), 145), 44).bake(); - // @done - } - - public static CNG signaturePerlin(RNG rng) { - return signaturePerlin(rng, NoiseType.PERLIN); - } - - public static CNG signaturePerlin(RNG rng, NoiseType t) { - // @NoArgsConstructor - return new CNG(rng.nextParallelRNG(124996), t, 1D, 1) - .fractureWith(new CNG(rng.nextParallelRNG(18), NoiseType.PERLIN, 1, 1).scale(1.25), 250) - .bake(); - // @done - } - - public static CNG signatureFast(RNG rng, NoiseType t, NoiseType f) { - // @NoArgsConstructor - return new CNG(rng.nextParallelRNG(17), t, 1D, 1) - .fractureWith(new CNG(rng.nextParallelRNG(18), f, 1, 1) - .scale(0.9) - .fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1) - .scale(0.21) - .fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1).scale(0.9), 620), 145), 44) - .bake(); - // @done - } - - public static CNG signatureThick(RNG rng, NoiseType t) { - // @NoArgsConstructor - return new CNG(rng.nextParallelRNG(133), t, 1D, 1).fractureWith(new CNG(rng.nextParallelRNG(18), 1, 1).scale(0.5).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.11).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.4), 620), 145), 44).bake(); - // @done - } - - public static CNG signatureThickFast(RNG rng, NoiseType t, NoiseType f) { - // @NoArgsConstructor - return new CNG(rng.nextParallelRNG(133), t, 1D, 1) - .fractureWith(new CNG(rng.nextParallelRNG(18), f, 1, 1) - .scale(0.5).fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1) - .scale(0.11).fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1) - .scale(0.4), 620), 145), 44).bake(); - // @done - } - - public static CNG signatureHalf(RNG rng, NoiseType t) { - // @NoArgsConstructor - return new CNG(rng.nextParallelRNG(127), t, 1D, 1).fractureWith(new CNG(rng.nextParallelRNG(18), 1, 1).scale(0.9).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.21).fractureWith(new CNG(rng.nextParallelRNG(20), 1, 1).scale(0.9), 420), 99), 22).bake(); - // @done - } - - public static CNG signatureHalfFast(RNG rng, NoiseType t, NoiseType f) { - // @NoArgsConstructor - return new CNG(rng.nextParallelRNG(127), t, 1D, 1) - .fractureWith(new CNG(rng.nextParallelRNG(18), f, 1, 1).scale(0.9) - .fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1).scale(0.21) - .fractureWith(new CNG(rng.nextParallelRNG(20), f, 1, 1).scale(0.9), 420), 99), 22).bake(); - // @done - } - - public static void main(String[] a) { - CNG cng = NoiseStyle.SIMPLEX.create(new RNG(1234)); - PrecisionStopwatch p = PrecisionStopwatch.start(); - double r = 0; - - for (int i = 0; i < 30000000 * 10; i++) { - r += cng.fit(-1000, 1000, i, i); - } - - System.out.println(Form.duration(p.getMilliseconds(), 10) + " merged = " + r); - } - - public CNG cellularize(RNG seed, double freq) { - FastNoise cellularFilter = new FastNoise(seed.imax()); - cellularFilter.SetNoiseType(FastNoise.NoiseType.Cellular); - cellularFilter.SetCellularReturnType(FastNoise.CellularReturnType.CellValue); - cellularFilter.SetCellularDistanceFunction(FastNoise.CellularDistanceFunction.Manhattan); - cellularFilter.SetFrequency((float) freq * 0.01f); - - ProceduralStream str = stream(); - - return new CNG(seed, new NoiseGenerator() { - @Override - public double noise(double x) { - return noise(x, 0); - } - - @Override - public double noise(double x, double z) { - return (cellularFilter.GetCellular((float) x, (float) z, str, 1) * 0.5) + 0.5D; - } - - @Override - public double noise(double x, double y, double z) { - return noise(x, y + z); - } - }, 1D, 1); - } - - public CNG cached(int size, String key, File cacheFolder) { - if (size <= 0) { - return this; - } - - cache = null; - - File f = new File(new File(cacheFolder, ".cache"), key + ".cnm"); - FloatCache fbc; - boolean cached = false; - if (f.exists()) { - try { - fbc = new FloatCache(f); - cached = true; - } catch (IOException e) { - fbc = new FloatCache(size, size); - } - } else { - fbc = new FloatCache(size, size); - } - - if (!cached) { - for (int i = 0; i < size; i++) { - for (int j = 0; j < size; j++) { - fbc.set(i, j, (float) noise(i, j)); - } - } - - try { - f.getParentFile().mkdirs(); - FileOutputStream fos = new FileOutputStream(f); - DataOutputStream dos = new DataOutputStream(fos); - fbc.writeCache(dos); - dos.close(); - Iris.info("Saved Noise Cache " + f.getName()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - cache = fbc; - return this; - } - - public NoiseGenerator getGen() { - return generator; - } - - public ProceduralStream stream() { - return new CNGStream(this); - } - - public ProceduralStream stream(double min, double max) { - return new FittedStream<>(stream(), min, max); - } - - public CNG bake() { - bakedScale *= scale; - scale = 1; - return this; - } - - public CNG child(CNG c) { - if (children == null) { - children = new KList<>(); - } - - children.add(c); - return this; - } - - public RNG getRNG() { - return rng; - } - - public CNG fractureWith(CNG c, double scale) { - fracture = c; - fscale = scale; - return this; - } - - public CNG scale(double c) { - scale = c; - return this; - } - - public CNG patch(double c) { - patch = c; - return this; - } - - public CNG up(double c) { - up = c; - return this; - } - - public CNG down(double c) { - down = c; - return this; - } - - public CNG injectWith(NoiseInjector i) { - injector = i; - return this; - } - - public T fitRarity(KList b, double... dim) { - if (b.size() == 0) { - return null; - } - - if (b.size() == 1) { - return b.get(0); - } - - KList rarityMapped = new KList<>(); - boolean o = false; - int max = 1; - for (T i : b) { - if (i.getRarity() > max) { - max = i.getRarity(); - } - } - - max++; - - for (T i : b) { - for (int j = 0; j < max - i.getRarity(); j++) { - //noinspection AssignmentUsedAsCondition - if (o = !o) { - rarityMapped.add(i); - } else { - rarityMapped.add(0, i); - } - } - } - - if (rarityMapped.size() == 1) { - return rarityMapped.get(0); - } - - if (rarityMapped.isEmpty()) { - throw new RuntimeException("BAD RARITY MAP! RELATED TO: " + b.toString(", or possibly ")); - } - - return fit(rarityMapped, dim); - } - - public T fit(T[] v, double... dim) { - if (v.length == 0) { - return null; - } - - if (v.length == 1) { - return v[0]; - } - - return v[fit(0, v.length - 1, dim)]; - } - - public T fit(List v, double... dim) { - if (v.size() == 0) { - return null; - } - - if (v.size() == 1) { - return v.get(0); - } - - try { - return v.get(fit(0, v.size() - 1, dim)); - } catch (Throwable e) { - Iris.reportError(e); - } - - return v.get(0); - } - - public int fit(int min, int max, double... dim) { - if (min == max) { - return min; - } - - double noise = noise(dim); - - return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); - } - - public int fit(double min, double max, double... dim) { - if (min == max) { - return (int) Math.round(min); - } - - double noise = noise(dim); - - return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); - } - - public double fitDouble(double min, double max, double... dim) { - if (min == max) { - return min; - } - - double noise = noise(dim); - - return IrisInterpolation.lerp(min, max, noise); - } - - private double getNoise(double... dim) { - double scale = noscale ? 1 : this.bakedScale * this.scale; - - if (fracture == null || noscale) { - return generator.noise( - (dim.length > 0 ? dim[0] : 0D) * scale, - (dim.length > 1 ? dim[1] : 0D) * scale, - (dim.length > 2 ? dim[2] : 0D) * scale) * opacity; - } - - if (fracture.isTrueFracturing()) { - double x = dim.length > 0 ? dim[0] + ((fracture.noise(dim) - 0.5) * fscale) : 0D; - double y = dim.length > 1 ? dim[1] + ((fracture.noise(dim[1], dim[0]) - 0.5) * fscale) : 0D; - double z = dim.length > 2 ? dim[2] + ((fracture.noise(dim[2], dim[0], dim[1]) - 0.5) * fscale) : 0D; - return generator.noise(x * scale, y * scale, z * scale) * opacity; - } - - double f = fracture.noise(dim) * fscale; - double x = dim.length > 0 ? dim[0] + f : 0D; - double y = dim.length > 1 ? dim[1] - f : 0D; - double z = dim.length > 2 ? dim[2] - f : 0D; - return generator.noise(x * scale, y * scale, z * scale) * opacity; - } - - public double invertNoise(double... dim) { - if (dim.length == 1) { - return noise(-dim[0]); - } else if (dim.length == 2) { - return noise(dim[1], dim[0]); - } else if (dim.length == 3) { - return noise(dim[1], dim[2], dim[0]); - } - - return noise(dim); - } - - public double noiseSym(double... dim) { - return (noise(dim) * 2) - 1; - } - - public double noise(double... dim) { - if (cache != null && dim.length == 2) { - return cache.get((int) dim[0], (int) dim[1]); - } - - double n = getNoise(dim); - n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; - double m = 1; - hits += oct; - if (children == null) { - return (n - down + up) * patch; - } - - for (CNG i : children) { - double[] r = injector.combine(n, i.noise(dim)); - n = r[0]; - m += r[1]; - } - - return ((n / m) - down + up) * patch; - } - - public CNG pow(double power) { - this.power = power; - return this; - } - - public CNG oct(int octaves) { - oct = octaves; - return this; - } - - public double getScale() { - return scale; - } - - public boolean isStatic() { - return generator != null && generator.isStatic(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/noise/CachedNoise.java b/core/src/main/java/com/volmit/iris/util/noise/CachedNoise.java deleted file mode 100644 index 44ca3c1ef..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/CachedNoise.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -public class CachedNoise implements NoiseGenerator { - private final CachedNoiseMap n; - - public CachedNoise(NoiseGenerator generator, int size) { - n = new CachedNoiseMap(size, generator); - } - - @Override - public double noise(double x) { - return n.get((int) Math.round(x), 0); - } - - @Override - public double noise(double x, double z) { - return n.get((int) Math.round(x), (int) Math.round(z)); - } - - @Override - public double noise(double x, double y, double z) { - return n.get((int) Math.round(x), (int) Math.round(z)); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/noise/CachedNoiseMap.java b/core/src/main/java/com/volmit/iris/util/noise/CachedNoiseMap.java deleted file mode 100644 index 2fae085b3..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/CachedNoiseMap.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.hunk.bits.Writable; -import com.volmit.iris.util.matter.IrisMatter; -import com.volmit.iris.util.matter.Matter; -import com.volmit.iris.util.matter.MatterSlice; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; - -public class CachedNoiseMap implements Writable { - private final Matter noise; - private final MatterSlice slice; - - public CachedNoiseMap(int size, NoiseGenerator cng) { - noise = new IrisMatter(size, size, 1); - slice = noise.slice(Integer.class); - - for (int i = 0; i < slice.getWidth(); i++) { - for (int j = 0; j < slice.getHeight(); j++) { - set(i, j, cng.noise(i, j)); - } - } - } - - public CachedNoiseMap(File file) throws IOException, ClassNotFoundException { - noise = Matter.read(file); - slice = noise.slice(Integer.class); - } - - void write(File file) throws IOException { - noise.write(file); - } - - void set(int x, int y, double value) { - slice.set(x % slice.getWidth(), y % slice.getHeight(), 0, Float.floatToIntBits((float) value)); - } - - double get(int x, int y) { - Integer i = slice.get(x % slice.getWidth(), y % slice.getHeight(), 0); - - if (i == null) { - return 0; - } - - return Float.intBitsToFloat(i); - } - - @Override - public Integer readNodeData(DataInputStream din) throws IOException { - return din.readInt(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Integer integer) throws IOException { - dos.writeInt(integer); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/noise/CloverNoise.java b/core/src/main/java/com/volmit/iris/util/noise/CloverNoise.java deleted file mode 100644 index c2171e2b1..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/CloverNoise.java +++ /dev/null @@ -1,1046 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -public class CloverNoise implements NoiseGenerator { - private final Noise2D n2; - private final Noise3D n3; - - public CloverNoise(long seed) { - n2 = new CloverNoise.Noise2D(seed); - n3 = new CloverNoise.Noise3D(seed); - } - - @Override - public double noise(double x) { - return n2.noise(x, 0); - } - - @Override - public double noise(double x, double z) { - return n2.noise(x, z); - } - - @Override - public double noise(double x, double y, double z) { - if (z == 0) { - return n2.noise(x, y); - } - - return n3.noise(x, y, z); - } - - /** - * Java implementation of 2D Clover Noise. See https://github.com/ValgoBoi/clover-noise - */ - public static class Noise2D { - private static final long HASH_A = 25214903917L; - private static final long HASH_C = 11L; - private static final long HASH_M = 0x1000000000000L; - private static final double POINT_SPREAD = 0.3; - private static final double CURL_DX = 0.0001; - private final long seed; - - /** - * Constructs a new 2D Clover Noise generator with a specific seed. - * - * @param seed The seed for the noise generator. - */ - public Noise2D(long seed) { - this.seed = seed; - } - - /** - * Constructs a new 2D Clover Noise generator, with the seed automatically set to the system time. - */ - public Noise2D() { - this(System.currentTimeMillis()); - } - - private long doHash(long input, long seed) { - input += seed; - - if (input < 0) { - input += HASH_M; - } - - input *= HASH_A; - input += HASH_C; - input %= HASH_M; - - return input; - } - - private double hash(Vector2 position) { - long hash = doHash(seed, (long) Math.floor(position.getX())); - hash = doHash(hash, (long) Math.floor(position.getY())); - hash = doHash(hash, hash * (long) Math.floor(position.getX() + position.getY())); - if (hash < 0) { - hash += HASH_M; - } - - return (double) hash / HASH_M; - } - - private Vector2 offset(Vector2 position) { - double hash = hash(position); - double scale = Math.floor(hash * 50 + 1) / 100; - Vector2 offset = new Vector2(Math.sin(hash * Math.PI * 100), Math.cos(hash * Math.PI * 100)).mult(scale).add(0.5); - return position.add(offset.mult(POINT_SPREAD * 2)).add(0.5 - POINT_SPREAD); - } - - /** - * Generates 2D Clover Noise at a specific point. - * - * @param p The point to generate noise at. - * @return The value of noise, from 0 to 1. - */ - public double noise(Vector2 p) { - Vector2 p_floor = p.floor(); - - Vector2 c_11 = offset(p_floor); - Vector2 c_10 = offset(p_floor.add(0, -1)); - Vector2 c_12 = offset(p_floor.add(0, 1)); - Vector2 c_01 = offset(p_floor.add(-1, 0)); - Vector2 c_21 = offset(p_floor.add(1, 0)); - - Vector2 d_p_c11 = p.sub(c_11).yx(); - Vector2 m_p_c11 = d_p_c11.mult(c_11); - - double side_nx = m_p_c11.sub(d_p_c11.mult(c_01)).ymx(); - double side_px = m_p_c11.sub(d_p_c11.mult(c_21)).ymx(); - - Vector2 a, c, d; - - if (side_nx < 0 && p.x < c_11.x || side_px > 0 && p.x > c_11.x) { - double side_py = m_p_c11.sub(d_p_c11.mult(c_12)).ymx(); - - if (side_py > 0) { - a = c_12; - c = c_01; - d = new Vector2(-1, 1); - } else { - a = c_21; - c = c_12; - d = new Vector2(1, 1); - } - } else { - double side_ny = m_p_c11.sub(d_p_c11.mult(c_10)).ymx(); - - if (side_ny > 0) { - a = c_10; - c = c_21; - d = new Vector2(1, -1); - } else { - a = c_01; - c = c_10; - d = new Vector2(-1, -1); - } - } - - d = offset(p_floor.add(d)); - - Vector2 f = a; - Vector2 g = c; - Vector2 h = d; - - Vector2 ac = a.sub(c); - Vector2 bd = c_11.sub(d); - - if (ac.x * ac.x + ac.y * ac.y < bd.x * bd.x + bd.y * bd.y) { - Vector2 pa = p.sub(a); - - if (pa.x * ac.y - pa.y * ac.x > 0.) { - h = c_11; - } - } else { - Vector2 pb = p.sub(c_11); - - if (pb.x * bd.y - pb.y * bd.x > 0) { - f = c_11; - } else { - g = c_11; - } - } - - Vector2 bc_v0 = g.sub(f); - Vector2 bc_v1 = h.sub(f); - Vector2 bc_v2 = p.sub(f); - double den = 1 / (bc_v0.x * bc_v1.y - bc_v1.x * bc_v0.y); - double v = (bc_v2.x * bc_v1.y - bc_v1.x * bc_v2.y) * den; - double w = (bc_v0.x * bc_v2.y - bc_v2.x * bc_v0.y) * den; - double u = 1 - v - w; - - v = v * v * v; - w = w * w * w; - u = u * u * u; - double s = 1 / (u + v + w); - v *= s; - w *= s; - u *= s; - - double fv = hash(f.floor()); - double gv = hash(g.floor()); - double hv = hash(h.floor()); - - return u * fv + v * gv + w * hv; - } - - /** - * Generates 2D Clover Noise at a specific point. - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @return The value of noise, from 0 to 1. - */ - public double noise(double x, double y) { - return noise(new Vector2(x, y)); - } - - /** - * Generates fractal 2D Clover Noise at a specific point. - * - * @param p The point to generate noise at. - * @param iterations The number of iterations for the fractal noise. - * @return The value of noise, from 0 to 1. - */ - public double fractalNoise(Vector2 p, int iterations) { - double total = 0; - double weight = 1; - double div = 0; - - for (int i = 0; i < iterations; i++) { - total += noise(p.mult(1 / weight)) * weight; - div += weight; - - weight *= 0.4; - } - - return total / div; - } - - /** - * Generates fractal 2D Clover Noise at a specific point. - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @param iterations The number of iterations for the fractal noise. - * @return The value of noise, from 0 to 1. - */ - public double fractalNoise(double x, double y, int iterations) { - return fractalNoise(new Vector2(x, y), iterations); - } - - /** - * Generates curl 2D Clover Noise at a specific point. - * - * @param p The point to generate noise at. - * @return The value of curl noise, a normalized 2D vector. - */ - public Vector2 curlNoise(Vector2 p) { - double v = noise(p); - double x = noise(p.add(CURL_DX, 0)); - double y = noise(p.add(0, CURL_DX)); - - return new Vector2(v - x, v - y).normalize(); - } - - /** - * Generates curl 2D Clover Noise at a specific point. - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @return The value of curl noise, a normalized 2D vector. - */ - public Vector2 curlNoise(double x, double y) { - return curlNoise(new Vector2(x, y)); - } - - /** - * Generates fractal curl 2D Clover Noise at a specific point. - * - * @param p The point to generate noise at. - * @param iterations The number of iterations for the fractal noise. - * @return The value of curl noise, a normalized 2D vector. - */ - public Vector2 fractalCurlNoise(Vector2 p, int iterations) { - double v = fractalNoise(p, iterations); - double x = fractalNoise(p.add(CURL_DX, 0), iterations); - double y = fractalNoise(p.add(0, CURL_DX), iterations); - - return new Vector2(v - x, v - y).normalize(); - } - - /** - * Generates fractal curl 2D Clover Noise at a specific point. - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @param iterations The number of iterations for the fractal noise. - * @return The value of curl noise, a normalized 2D vector. - */ - public Vector2 fractalCurlNoise(double x, double y, int iterations) { - return fractalCurlNoise(new Vector2(x, y), iterations); - } - - /** - * Generates 2D Frost Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/frost.md - * - * @param p The point to generate noise at. - * @return The value of noise, from 0 to 1. - */ - public double frostNoise(Vector2 p) { - Vector2 curl_1 = fractalCurlNoise(p, 3).mult(fractalNoise(p, 2) * 0.4 + 0.3); - Vector2 p_1 = p.add(curl_1); - Vector2 curl_2 = fractalCurlNoise(p_1, 4).mult(fractalNoise(p_1, 3) * 0.1 + 0.05); - Vector2 p_2 = p_1.add(curl_2); - - return fractalNoise(p_2, 5) - fractalNoise(p_1, 3) * 0.5 + fractalNoise(p, 2) * 0.3; - } - - /** - * Generates 2D Frost Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/frost.md - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @return The value of noise, from 0 to 1. - */ - public double frostNoise(double x, double y) { - return frostNoise(new Vector2(x, y)); - } - - /** - * Generates 2D Marble Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/marble.md - * - * @param p The point to generate noise at. - * @return The value of noise, from 0 to 1. - */ - public double marbleNoise(Vector2 p) { - Vector2 p_2 = p.mult(0.6); - double d1 = Math.max(1 - Math.abs(fractalNoise(p_2.add(100), 4) - fractalNoise(p_2.add(200), 3)) * 3, 0); - d1 = d1 * d1 * d1; - d1 *= fractalNoise(p_2.add(300), 2) * 0.3; - - Vector2 curl_1 = fractalCurlNoise(p.add(400), 3).mult(fractalNoise(p.add(500), 2) * 0.05); - - Vector2 p_3 = p.mult(1.2); - double d2 = Math.max(1 - Math.abs(fractalNoise(p_3.add(curl_1).add(600), 4) - fractalNoise(p_3.add(curl_1).add(700), 3)) * 2, 0); - d2 = d2 * d2 * d2; - d2 *= fractalNoise(p_3.add(800), 2) * 0.5; - - double v = 1 - fractalNoise(p.add(900), 5); - v = 1 - v * v * v; - - return Math.min(Math.max(v - d1 - d2, 0), 1); - } - - /** - * Generates 2D Marble Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/marble.md - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @return The value of noise, from 0 to 1. - */ - public double marbleNoise(double x, double y) { - return marbleNoise(new Vector2(x, y)); - } - } - - public static class Noise3D { - private static final long HASH_A = 25214903917L; - private static final long HASH_C = 11L; - private static final long HASH_M = 0x1000000000000L; - private static final double POINT_SPREAD = 0.2; - private static final double CURL_DX = 0.0001; - private final long seed; - - public Noise3D(long seed) { - this.seed = seed; - } - - public Noise3D() { - this(System.currentTimeMillis()); - } - - private long doHash(long input, long seed) { - input += seed; - - if (input < 0) { - input += HASH_M; - } - - input *= HASH_A; - input += HASH_C; - input %= HASH_M; - - return input; - } - - private double hash(Vector3 position) { - long hash = doHash(seed, (long) Math.floor(position.getX())); - hash = doHash(hash, (long) Math.floor(position.getY())); - hash = doHash(hash, (long) Math.floor(position.getZ())); - hash = doHash(hash, hash * (long) Math.floor(position.getX() + position.getY() + position.getZ())); - if (hash < 0) { - hash += HASH_M; - } - - return (double) hash / HASH_M; - } - - private Vector3 offset(Vector3 position) { - double hash = hash(position); - double theta = hash * Math.PI * 2000; - double height = (((Math.floor(hash * 1000) + 0.5) / 100) % 1 - 0.5) * Math.PI / 2; - double layer = Math.floor(hash * 10 + 1) / 10; - Vector3 offset = new Vector3(Math.sin(theta) * Math.cos(height), Math.sin(height), Math.cos(theta) * Math.cos(height)).mult(layer).add(0.5); - return position.add(offset.mult(POINT_SPREAD * 2).add(0.5 - POINT_SPREAD)); - } - - private boolean boundary(Vector3 p, Vector3 c_00, Vector3 c_10, Vector3 c_20, Vector3 c_01, Vector3 c_11, Vector3 c_21, Vector3 c_02, Vector3 c_12, Vector3 c_22) { - Vector2 d_p_c11 = p.yx().sub(c_11.yx()); - Vector2 m_p_c11 = d_p_c11.mult(c_11.xy()); - - double side_nx = m_p_c11.sub(d_p_c11.mult(c_01.xy())).ymx(); - double side_px = m_p_c11.sub(d_p_c11.mult(c_21.xy())).ymx(); - - Vector3 a, b, c, d; - - if (side_nx < 0 && p.x < c_11.x || side_px > 0 && p.x >= c_11.x) { - double side_py = m_p_c11.sub(d_p_c11.mult(c_12.xy())).ymx(); - - if (side_py > 0.) { - a = c_01; - b = c_02; - c = c_12; - d = c_11; - } else { - a = c_11; - b = c_12; - c = c_22; - d = c_21; - } - } else { - double side_ny = m_p_c11.sub(d_p_c11.mult(c_10.xy())).ymx(); - - if (side_ny > 0.) { - a = c_10; - b = c_11; - c = c_21; - d = c_20; - } else { - a = c_00; - b = c_01; - c = c_11; - d = c_10; - } - } - - Vector3 f = a; - Vector3 g = c; - Vector3 h = d; - - Vector3 ac = a.sub(c); - Vector3 pa = p.sub(a); - - if (pa.x * ac.y - pa.y * ac.x > 0) { - h = b; - } - - Vector2 bc_v0 = g.xy().sub(f.xy()); - Vector2 bc_v1 = h.xy().sub(f.xy()); - Vector2 bc_v2 = p.xy().sub(f.xy()); - double den = 1 / (bc_v0.x * bc_v1.y - bc_v1.x * bc_v0.y); - double v = (bc_v2.x * bc_v1.y - bc_v1.x * bc_v2.y) * den; - double w = (bc_v0.x * bc_v2.y - bc_v2.x * bc_v0.y) * den; - double u = 1 - v - w; - - return p.z < u * f.z + v * g.z + w * h.z; - } - - /** - * Generates 3D Clover Noise at a specific point. - * - * @param p The point to generate noise at. - * @return The value of noise, from 0 to 1. - */ - public double noise(Vector3 p) { - Vector3 p_floor = p.floor(); - - Vector3 c_111 = offset(p_floor); - Vector3 c_100 = offset(p_floor.add(0, -1, -1)); - Vector3 c_010 = offset(p_floor.add(-1, 0, -1)); - Vector3 c_110 = offset(p_floor.add(0, 0, -1)); - Vector3 c_210 = offset(p_floor.add(1, 0, -1)); - Vector3 c_120 = offset(p_floor.add(0, 1, -1)); - Vector3 c_001 = offset(p_floor.add(-1, -1, 0)); - Vector3 c_101 = offset(p_floor.add(0, -1, 0)); - Vector3 c_201 = offset(p_floor.add(1, -1, 0)); - Vector3 c_011 = offset(p_floor.add(-1, 0, 0)); - Vector3 c_211 = offset(p_floor.add(1, 0, 0)); - Vector3 c_021 = offset(p_floor.add(-1, 1, 0)); - Vector3 c_121 = offset(p_floor.add(0, 1, 0)); - Vector3 c_221 = offset(p_floor.add(1, 1, 0)); - Vector3 c_102 = offset(p_floor.add(0, -1, 1)); - Vector3 c_012 = offset(p_floor.add(-1, 0, 1)); - Vector3 c_112 = offset(p_floor.add(0, 0, 1)); - Vector3 c_212 = offset(p_floor.add(1, 0, 1)); - Vector3 c_122 = offset(p_floor.add(0, 1, 1)); - - boolean x_bound = boundary(p.yzx(), c_100.yzx(), c_110.yzx(), c_120.yzx(), c_101.yzx(), c_111.yzx(), c_121.yzx(), c_102.yzx(), c_112.yzx(), c_122.yzx()); - boolean y_bound = boundary(p.xzy(), c_010.xzy(), c_110.xzy(), c_210.xzy(), c_011.xzy(), c_111.xzy(), c_211.xzy(), c_012.xzy(), c_112.xzy(), c_212.xzy()); - boolean z_bound = boundary(p, c_001, c_101, c_201, c_011, c_111, c_211, c_021, c_121, c_221); - - Vector3 a, b, c, d, e, f, g, h; - - if (x_bound) { - if (y_bound) { - if (z_bound) { - a = offset(p_floor.add(-1, -1, -1)); - b = c_001; - c = c_010; - d = c_011; - e = c_100; - f = c_101; - g = c_110; - h = c_111; - } else { - a = c_001; - b = offset(p_floor.add(-1, -1, 1)); - c = c_011; - d = c_012; - e = c_101; - f = c_102; - g = c_111; - h = c_112; - } - } else { - if (z_bound) { - a = c_010; - b = c_011; - c = offset(p_floor.add(-1, 1, -1)); - d = c_021; - e = c_110; - f = c_111; - g = c_120; - h = c_121; - } else { - a = c_011; - b = c_012; - c = c_021; - d = offset(p_floor.add(-1, 1, 1)); - e = c_111; - f = c_112; - g = c_121; - h = c_122; - } - } - } else { - if (y_bound) { - if (z_bound) { - a = c_100; - b = c_101; - c = c_110; - d = c_111; - e = offset(p_floor.add(1, -1, -1)); - f = c_201; - g = c_210; - h = c_211; - } else { - a = c_101; - b = c_102; - c = c_111; - d = c_112; - e = c_201; - f = offset(p_floor.add(1, -1, 1)); - g = c_211; - h = c_212; - } - } else { - if (z_bound) { - a = c_110; - b = c_111; - c = c_120; - d = c_121; - e = c_210; - f = c_211; - g = offset(p_floor.add(1, 1, -1)); - h = c_221; - } else { - a = c_111; - b = c_112; - c = c_121; - d = c_122; - e = c_211; - f = c_212; - g = c_221; - h = offset(p_floor.add(1, 1, 1)); - } - } - } - - Vector3 ah = a.sub(h); - Vector3 pa = p.sub(a); - - double plane_b = ah.cross(b.sub(h)).mult(pa).xpypz(); - double plane_c = ah.cross(c.sub(h)).mult(pa).xpypz(); - double plane_d = ah.cross(d.sub(h)).mult(pa).xpypz(); - double plane_e = ah.cross(e.sub(h)).mult(pa).xpypz(); - double plane_f = ah.cross(f.sub(h)).mult(pa).xpypz(); - double plane_g = ah.cross(g.sub(h)).mult(pa).xpypz(); - - Vector3 i, j, k, l; - - i = a; - j = h; - - if (plane_b > 0 && plane_d <= 0) { - k = b; - l = d; - } else if (plane_d > 0 && plane_c <= 0) { - k = d; - l = c; - } else if (plane_c > 0 && plane_g <= 0) { - k = c; - l = g; - } else if (plane_g > 0 && plane_e <= 0) { - k = g; - l = e; - } else if (plane_e > 0 && plane_f <= 0) { - k = e; - l = f; - } else { - k = f; - l = b; - } - - Vector3 bc_ap = p.sub(i); - Vector3 bc_bp = p.sub(j); - - Vector3 bc_ab = j.sub(i); - Vector3 bc_ac = k.sub(i); - Vector3 bc_ad = l.sub(i); - - Vector3 bc_bc = k.sub(j); - Vector3 bc_bd = l.sub(j); - - double bc_va6 = bc_bp.mult(bc_bd.cross(bc_bc)).xpypz(); - double bc_vb6 = bc_ap.mult(bc_ac.cross(bc_ad)).xpypz(); - double bc_vc6 = bc_ap.mult(bc_ad.cross(bc_ab)).xpypz(); - double bc_vd6 = bc_ap.mult(bc_ab.cross(bc_ac)).xpypz(); - double bc_v6 = 1 / bc_ab.mult(bc_ac.cross(bc_ad)).xpypz(); - - double v = bc_va6 * bc_v6; - double w = bc_vb6 * bc_v6; - double t = bc_vc6 * bc_v6; - double u = bc_vd6 * bc_v6; - - double fiu = u * u * u * (1 - v * w * t); - double fiv = v * v * v * (1 - u * w * t); - double fiw = w * w * w * (1 - v * u * t); - double fit = t * t * t * (1 - v * w * u); - double s = fiu + fiv + fiw + fit; - fiu /= s; - fiv /= s; - fiw /= s; - fit /= s; - - double iv = hash(i.floor()); - double jv = hash(j.floor()); - double kv = hash(k.floor()); - double lv = hash(l.floor()); - - return fiv * iv + fiw * jv + fit * kv + fiu * lv; - } - - /** - * Generates 3D Clover Noise at a specific point. - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @param z The z coordinate of the point. - * @return The value of noise, from 0 to 1. - */ - public double noise(double x, double y, double z) { - return noise(new Vector3(x, y, z)); - } - - /** - * Generates fractal 3D Clover Noise at a specific point. - * - * @param p The point to generate noise at. - * @param iterations The number of iterations for the fractal noise. - * @return The value of noise, from 0 to 1. - */ - public double fractalNoise(Vector3 p, int iterations) { - double total = 0; - double weight = 1; - double div = 0; - - for (int i = 0; i < iterations; i++) { - total += noise(p.mult(1 / weight)) * weight; - div += weight; - - weight *= 0.4; - } - - return total / div; - } - - /** - * Generates fractal 3D Clover Noise at a specific point. - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @param z The z coordinate of the point. - * @param iterations The number of iterations for the fractal noise. - * @return The value of noise, from 0 to 1. - */ - public double fractalNoise(double x, double y, double z, int iterations) { - return fractalNoise(new Vector3(x, y, z), iterations); - } - - /** - * Generates curl 3D Clover Noise at a specific point. - * - * @param p The point to generate noise at. - * @return The value of curl noise, a normalized 3D vector. - */ - public Vector3 curlNoise(Vector3 p) { - double v = noise(p); - double x = noise(p.add(CURL_DX, 0, 0)); - double y = noise(p.add(0, CURL_DX, 0)); - double z = noise(p.add(0, 0, CURL_DX)); - - return new Vector3(v - x, v - y, v - z).normalize(); - } - - /** - * Generates curl 3D Clover Noise at a specific point. - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @param z The z coordinate of the point. - * @return The value of curl noise, a normalized 3D vector. - */ - public Vector3 curlNoise(double x, double y, double z) { - return curlNoise(new Vector3(x, y, z)); - } - - /** - * Generates fractal curl 3D Clover Noise at a specific point. - * - * @param p The point to generate noise at. - * @param iterations The number of iterations for the fractal noise. - * @return The value of curl noise, a normalized 3D vector. - */ - public Vector3 fractalCurlNoise(Vector3 p, int iterations) { - double v = fractalNoise(p, iterations); - double x = fractalNoise(p.add(CURL_DX, 0, 0), iterations); - double y = fractalNoise(p.add(0, CURL_DX, 0), iterations); - double z = fractalNoise(p.add(0, 0, CURL_DX), iterations); - - return new Vector3(v - x, v - y, v - z).normalize(); - } - - /** - * Generates fractal curl 3D Clover Noise at a specific point. - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @param z The z coordinate of the point. - * @param iterations The number of iterations for the fractal noise. - * @return The value of curl noise, a normalized 3D vector. - */ - public Vector3 fractalCurlNoise(double x, double y, double z, int iterations) { - return fractalCurlNoise(new Vector3(x, y, z), iterations); - } - - /** - * Generates 3D Frost Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/frost.md - * - * @param p The point to generate noise at. - * @return The value of noise, from 0 to 1. - */ - public double frostNoise(Vector3 p) { - Vector3 curl_1 = fractalCurlNoise(p, 3).mult(fractalNoise(p, 2) * 0.4 + 0.3); - Vector3 p_1 = p.add(curl_1); - Vector3 curl_2 = fractalCurlNoise(p_1, 4).mult(fractalNoise(p_1, 3) * 0.1 + 0.05); - Vector3 p_2 = p_1.add(curl_2); - - return fractalNoise(p_2, 5) - fractalNoise(p_1, 3) * 0.5 + fractalNoise(p, 2) * 0.3; - } - - /** - * Generates 3D Frost Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/frost.md - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @param z The z coordinate of the point. - * @return The value of noise, from 0 to 1. - */ - public double frostNoise(double x, double y, double z) { - return frostNoise(new Vector3(x, y, z)); - } - - /** - * Generates 3D Marble Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/marble.md - * - * @param p The point to generate noise at. - * @return The value of noise, from 0 to 1. - */ - public double marbleNoise(Vector3 p) { - Vector3 p_2 = p.mult(0.6); - double d1 = Math.max(1 - Math.abs(fractalNoise(p_2.add(100), 4) - fractalNoise(p_2.add(200), 3)) * 3, 0); - d1 = d1 * d1 * d1; - d1 *= fractalNoise(p_2.add(300), 2) * 0.3; - - Vector3 curl_1 = fractalCurlNoise(p.add(400), 3).mult(fractalNoise(p.add(500), 2) * 0.05); - - Vector3 p_3 = p.mult(1.2); - double d2 = Math.max(1 - Math.abs(fractalNoise(p_3.add(curl_1).add(600), 4) - fractalNoise(p_3.add(curl_1).add(700), 3)) * 2, 0); - d2 = d2 * d2 * d2; - d2 *= fractalNoise(p_3.add(800), 2) * 0.5; - - double v = 1 - fractalNoise(p.add(900), 5); - v = 1 - v * v * v; - - return Math.min(Math.max(v - d1 - d2, 0), 1); - } - - /** - * Generates 3D Marble Noise at a specific point. See https://github.com/ValgoBoi/clover-noise/blob/master/variations/marble.md - * - * @param x The x coordinate of the point. - * @param y The y coordinate of the point. - * @param z The z coordinate of the point. - * @return The value of noise, from 0 to 1. - */ - public double marbleNoise(double x, double y, double z) { - return marbleNoise(new Vector3(x, y, z)); - } - } - - /** - * A 2-dimensional Vector, with methods relevant to the Clover Noise algorithm. - */ - public static class Vector2 { - private double x, y; - - public Vector2(double x, double y) { - this.x = x; - this.y = y; - } - - public Vector2() { - this(0, 0); - } - - public double getX() { - return x; - } - - public void setX(double x) { - this.x = x; - } - - public double getY() { - return y; - } - - public void setY(double y) { - this.y = y; - } - - public double ymx() { - return y - x; - } - - public Vector2 yx() { - return new Vector2(y, x); - } - - public Vector2 negate() { - return new Vector2(-x, -y); - } - - public Vector2 floor() { - return new Vector2(Math.floor(x), Math.floor(y)); - } - - public Vector2 normalize() { - double len2 = x * x + y * y; - double len = Math.sqrt(len2); - - return new Vector2(x / len, y / len); - } - - public Vector2 add(Vector2 a) { - return new Vector2(x + a.x, y + a.y); - } - - public Vector2 add(double xa, double ya) { - return add(new Vector2(xa, ya)); - } - - public Vector2 add(double a) { - return add(a, a); - } - - public Vector2 sub(Vector2 s) { - return add(s.negate()); - } - - public Vector2 sub(double xs, double ys) { - return sub(new Vector2(xs, ys)); - } - - public Vector2 sub(double s) { - return sub(s, s); - } - - public Vector2 mult(Vector2 m) { - return new Vector2(x * m.x, y * m.y); - } - - public Vector2 mult(double xm, double ym) { - return mult(new Vector2(xm, ym)); - } - - public Vector2 mult(double m) { - return mult(m, m); - } - } - - /** - * A 3-dimensional Vector, with methods relevant to the Clover Noise algorithm. - */ - public static class Vector3 { - double x, y, z; - - public Vector3(double x, double y, double z) { - this.x = x; - this.y = y; - this.z = z; - } - - public Vector3() { - this(0, 0, 0); - } - - public double getX() { - return x; - } - - public void setX(double x) { - this.x = x; - } - - public double getY() { - return y; - } - - public void setY(double y) { - this.y = y; - } - - public double getZ() { - return z; - } - - public void setZ(double z) { - this.z = z; - } - - public Vector2 xy() { - return new Vector2(x, y); - } - - public Vector2 yx() { - return new Vector2(y, x); - } - - public Vector3 yzx() { - return new Vector3(y, z, x); - } - - public Vector3 xzy() { - return new Vector3(x, z, y); - } - - public double xpypz() { - return x + y + z; - } - - public Vector3 negate() { - return new Vector3(-x, -y, -z); - } - - public Vector3 floor() { - return new Vector3(Math.floor(x), Math.floor(y), Math.floor(z)); - } - - public Vector3 normalize() { - double len2 = x * x + y * y + z * z; - double len = Math.sqrt(len2); - - return new Vector3(x / len, y / len, z / len); - } - - public Vector3 add(Vector3 a) { - return new Vector3(x + a.x, y + a.y, z + a.z); - } - - public Vector3 add(double xa, double ya, double za) { - return add(new Vector3(xa, ya, za)); - } - - public Vector3 add(double a) { - return add(a, a, a); - } - - public Vector3 sub(Vector3 s) { - return add(s.negate()); - } - - public Vector3 sub(double xs, double ys, double zs) { - return sub(new Vector3(xs, ys, zs)); - } - - public Vector3 sub(double s) { - return sub(s, s, s); - } - - public Vector3 mult(Vector3 m) { - return new Vector3(x * m.x, y * m.y, z * m.z); - } - - public Vector3 mult(double mx, double my, double mz) { - return mult(new Vector3(mx, my, mz)); - } - - public Vector3 mult(double m) { - return mult(m, m, m); - } - - public Vector3 cross(Vector3 c) { - return new Vector3(y * c.z - z * c.y, z * c.x - x * c.z, x * c.y - y * c.x); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/noise/NoiseGenerator.java b/core/src/main/java/com/volmit/iris/util/noise/NoiseGenerator.java deleted file mode 100644 index 4574e9eb1..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/NoiseGenerator.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.stream.ProceduralStream; -import com.volmit.iris.util.stream.interpolation.Interpolated; - -public interface NoiseGenerator { - double noise(double x); - - double noise(double x, double z); - - double noise(double x, double y, double z); - - default boolean isStatic() { - return false; - } - - default boolean isNoScale() { - return false; - } - - default ProceduralStream stream() { - return ProceduralStream.of(this::noise, this::noise, Interpolated.DOUBLE); - } - - default OffsetNoiseGenerator offset(long seed) { - return new OffsetNoiseGenerator(this, seed); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/noise/NoiseType.java b/core/src/main/java/com/volmit/iris/util/noise/NoiseType.java deleted file mode 100644 index 20f89ca8b..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/NoiseType.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.interpolation.InterpolationMethod; - -public enum NoiseType { - WHITE(WhiteNoise::new), - WHITE_BILINEAR((s) -> new InterpolatedNoise(s, WHITE, InterpolationMethod.BILINEAR)), - WHITE_BICUBIC((s) -> new InterpolatedNoise(s, WHITE, InterpolationMethod.BICUBIC)), - WHITE_HERMITE((s) -> new InterpolatedNoise(s, WHITE, InterpolationMethod.HERMITE)), - SIMPLEX(SimplexNoise::new), - PERLIN(seed -> new PerlinNoise(seed).hermite()), - FRACTAL_BILLOW_SIMPLEX(FractalBillowSimplexNoise::new), - FRACTAL_BILLOW_PERLIN(FractalBillowPerlinNoise::new), - FRACTAL_FBM_SIMPLEX(FractalFBMSimplexNoise::new), - FRACTAL_RIGID_MULTI_SIMPLEX(FractalRigidMultiSimplexNoise::new), - FLAT(FlatNoise::new), - CELLULAR(CellularNoise::new), - CELLULAR_BILINEAR((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR)), - CELLULAR_BILINEAR_STARCAST_3((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR_STARCAST_3)), - CELLULAR_BILINEAR_STARCAST_6((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR_STARCAST_6)), - CELLULAR_BILINEAR_STARCAST_9((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR_STARCAST_9)), - CELLULAR_BILINEAR_STARCAST_12((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BILINEAR_STARCAST_12)), - CELLULAR_BICUBIC((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.BICUBIC)), - CELLULAR_HERMITE((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE)), - CELLULAR_STARCAST_3((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.STARCAST_3)), - CELLULAR_STARCAST_6((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.STARCAST_6)), - CELLULAR_STARCAST_9((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.STARCAST_9)), - CELLULAR_STARCAST_12((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.STARCAST_12)), - CELLULAR_HERMITE_STARCAST_3((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE_STARCAST_3)), - CELLULAR_HERMITE_STARCAST_6((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE_STARCAST_6)), - CELLULAR_HERMITE_STARCAST_9((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE_STARCAST_9)), - CELLULAR_HERMITE_STARCAST_12((s) -> new InterpolatedNoise(s, CELLULAR, InterpolationMethod.HERMITE_STARCAST_12)), - GLOB(GlobNoise::new), - CUBIC(CubicNoise::new), - FRACTAL_CUBIC(FractalCubicNoise::new), - CELLULAR_HEIGHT(CellHeightNoise::new), - CLOVER(CloverNoise::new), - CLOVER_BILINEAR((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR)), - CLOVER_BILINEAR_STARCAST_3((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR_STARCAST_3)), - CLOVER_BILINEAR_STARCAST_6((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR_STARCAST_6)), - CLOVER_BILINEAR_STARCAST_9((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR_STARCAST_9)), - CLOVER_BILINEAR_STARCAST_12((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BILINEAR_STARCAST_12)), - CLOVER_BICUBIC((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.BICUBIC)), - CLOVER_HERMITE((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE)), - CLOVER_STARCAST_3((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.STARCAST_3)), - CLOVER_STARCAST_6((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.STARCAST_6)), - CLOVER_STARCAST_9((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.STARCAST_9)), - CLOVER_STARCAST_12((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.STARCAST_12)), - CLOVER_HERMITE_STARCAST_3((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE_STARCAST_3)), - CLOVER_HERMITE_STARCAST_6((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE_STARCAST_6)), - CLOVER_HERMITE_STARCAST_9((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE_STARCAST_9)), - CLOVER_HERMITE_STARCAST_12((s) -> new InterpolatedNoise(s, CLOVER, InterpolationMethod.HERMITE_STARCAST_12)), - VASCULAR(VascularNoise::new); - - private final NoiseFactory f; - - NoiseType(NoiseFactory f) { - this.f = f; - } - - public NoiseGenerator create(long seed) { - if (IrisSettings.get().getGenerator().offsetNoiseTypes) return f.create(seed).offset(seed); - else return f.create(seed); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/noise/OffsetNoiseGenerator.java b/core/src/main/java/com/volmit/iris/util/noise/OffsetNoiseGenerator.java deleted file mode 100644 index 97e3088c0..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/OffsetNoiseGenerator.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.volmit.iris.util.noise; - - -import com.volmit.iris.util.math.RNG; -import org.jetbrains.annotations.NotNull; - - -public class OffsetNoiseGenerator implements NoiseGenerator { - private final NoiseGenerator base; - private final double ox, oz; - - public OffsetNoiseGenerator(NoiseGenerator base, long seed) { - this.base = base; - RNG rng = new RNG(seed); - ox = rng.nextInt(Short.MIN_VALUE, Short.MAX_VALUE); - oz = rng.nextInt(Short.MIN_VALUE, Short.MAX_VALUE); - } - - @Override - public double noise(double x) { - return base.noise(x + ox); - } - - @Override - public double noise(double x, double z) { - return base.noise(x + ox, z + oz); - } - - @Override - public double noise(double x, double y, double z) { - return base.noise(x + ox, y, z + oz); - } - - @Override - public boolean isNoScale() { - return base.isNoScale(); - } - - @Override - public boolean isStatic() { - return base.isStatic(); - } - - @NotNull - public NoiseGenerator getBase() { - return base; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/noise/PerlinNoise.java b/core/src/main/java/com/volmit/iris/util/noise/PerlinNoise.java deleted file mode 100644 index 7681f99c5..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/PerlinNoise.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.math.RNG; - -public class PerlinNoise implements NoiseGenerator, OctaveNoise { - private final FastNoiseDouble n; - private int octaves; - - public PerlinNoise(long seed) { - this.n = new FastNoiseDouble(new RNG(seed).lmax()); - octaves = 1; - } - - public double f(double v) { - return (v / 2D) + 0.5D; - } - - @Override - public double noise(double x) { - if (octaves <= 1) { - return f(n.GetPerlin(x, 0)); - } - - double f = 1; - double m = 0; - double v = 0; - - for (int i = 0; i < octaves; i++) { - v += n.GetPerlin((x * (f == 1 ? f++ : (f *= 2))), 0) * f; - m += f; - } - - return f(v / m); - } - - @Override - public double noise(double x, double z) { - if (octaves <= 1) { - return f(n.GetPerlin(x, z)); - } - double f = 1; - double m = 0; - double v = 0; - - for (int i = 0; i < octaves; i++) { - f = f == 1 ? f + 1 : f * 2; - v += n.GetPerlin((x * f), (z * f)) * f; - m += f; - } - - return f(v / m); - } - - @Override - public double noise(double x, double y, double z) { - if (octaves <= 1) { - return f(n.GetPerlin(x, y, z)); - } - double f = 1; - double m = 0; - double v = 0; - - for (int i = 0; i < octaves; i++) { - f = f == 1 ? f + 1 : f * 2; - v += n.GetPerlin((x * f), (y * f), (z * f)) * f; - m += f; - } - - return f(v / m); - } - - @Override - public void setOctaves(int o) { - octaves = o; - } - - public NoiseGenerator hermite() { - n.m_longerp = FastNoiseDouble.Longerp.Hermite; - return this; - } - - public NoiseGenerator quad() { - n.m_longerp = FastNoiseDouble.Longerp.Qulongic; - return this; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/noise/RarityCellGenerator.java b/core/src/main/java/com/volmit/iris/util/noise/RarityCellGenerator.java deleted file mode 100644 index 268f2bfde..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/RarityCellGenerator.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -import com.volmit.iris.engine.object.IRare; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.RNG; - -public class RarityCellGenerator extends CellGenerator { - public RarityCellGenerator(RNG rng) { - super(rng); - } - - public T get(double x, double z, KList b) { - if (b.size() == 0) { - return null; - } - - if (b.size() == 1) { - return b.get(0); - } - - KList rarityMapped = new KList<>(); - boolean o = false; - int max = 1; - for (T i : b) { - if (i.getRarity() > max) { - max = i.getRarity(); - } - } - - max++; - - for (T i : b) { - for (int j = 0; j < max - i.getRarity(); j++) { - //noinspection AssignmentUsedAsCondition - if (o = !o) { - rarityMapped.add(i); - } else { - rarityMapped.add(0, i); - } - } - } - - if (rarityMapped.size() == 1) { - return rarityMapped.get(0); - } - - if (rarityMapped.isEmpty()) { - throw new RuntimeException("BAD RARITY MAP! RELATED TO: " + b.toString(", or possibly ")); - } - - return rarityMapped.get(getIndex(x, z, rarityMapped.size())); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/noise/SimplexNoise.java b/core/src/main/java/com/volmit/iris/util/noise/SimplexNoise.java deleted file mode 100644 index 5ad097b4f..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/SimplexNoise.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.math.RNG; - -public class SimplexNoise implements NoiseGenerator, OctaveNoise { - private final FastNoiseDouble n; - private int octaves; - - public SimplexNoise(long seed) { - this.n = new FastNoiseDouble(new RNG(seed).lmax()); - octaves = 1; - } - - public double f(double v) { - return (v / 2D) + 0.5D; - } - - @Override - public double noise(double x) { - if (octaves <= 1) { - return f(n.GetSimplex(x, 0d)); - } - - double f = 1; - double m = 0; - double v = 0; - - for (int i = 0; i < octaves; i++) { - v += n.GetSimplex((x * (f == 1 ? f++ : (f *= 2))), 0d) * f; - m += f; - } - - return f(v / m); - } - - @Override - public double noise(double x, double z) { - if (octaves <= 1) { - return f(n.GetSimplex(x, z)); - } - double f = 1; - double m = 0; - double v = 0; - - for (int i = 0; i < octaves; i++) { - f = f == 1 ? f + 1 : f * 2; - v += n.GetSimplex((x * f), (z * f)) * f; - m += f; - } - - return f(v / m); - } - - @Override - public double noise(double x, double y, double z) { - if (octaves <= 1) { - return f(n.GetSimplex(x, y, z)); - } - double f = 1; - double m = 0; - double v = 0; - - for (int i = 0; i < octaves; i++) { - f = f == 1 ? f + 1 : f * 2; - v += n.GetSimplex((x * f), (y * f), (z * f)) * f; - m += f; - } - - return f(v / m); - } - - @Override - public void setOctaves(int o) { - octaves = o; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/noise/VascularNoise.java b/core/src/main/java/com/volmit/iris/util/noise/VascularNoise.java deleted file mode 100644 index 6a979d08c..000000000 --- a/core/src/main/java/com/volmit/iris/util/noise/VascularNoise.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.noise; - -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; - -public class VascularNoise implements NoiseGenerator { - private final FastNoiseDouble n; - - public VascularNoise(long seed) { - this.n = new FastNoiseDouble(new RNG(seed).lmax()); - n.setNoiseType(FastNoiseDouble.NoiseType.Cellular); - n.setCellularReturnType(FastNoiseDouble.CellularReturnType.Distance2Sub); - n.setCellularDistanceFunction(FastNoiseDouble.CellularDistanceFunction.Natural); - } - - private double filter(double noise) { - return M.clip((noise / 2D) + 0.5D, 0D, 1D); - } - - @Override - public double noise(double x) { - return filter(n.GetCellular(x, 0)); - } - - @Override - public double noise(double x, double z) { - return filter(n.GetCellular(x, z)); - } - - @Override - public double noise(double x, double y, double z) { - return filter(n.GetCellular(x, y, z)); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/parallel/AtomicBooleanArray.java b/core/src/main/java/com/volmit/iris/util/parallel/AtomicBooleanArray.java deleted file mode 100644 index 575634831..000000000 --- a/core/src/main/java/com/volmit/iris/util/parallel/AtomicBooleanArray.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.volmit.iris.util.parallel; - -import java.io.Serializable; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; - -public class AtomicBooleanArray implements Serializable { - private static final VarHandle AA = MethodHandles.arrayElementVarHandle(boolean[].class); - private final boolean[] array; - - public AtomicBooleanArray(int length) { - array = new boolean[length]; - } - - public final int length() { - return array.length; - } - - public final boolean get(int index) { - return (boolean) AA.getVolatile(array, index); - } - - public final void set(int index, boolean newValue) { - AA.setVolatile(array, index, newValue); - } - - public final boolean getAndSet(int index, boolean newValue) { - return (boolean) AA.getAndSet(array, index, newValue); - } - - public final boolean compareAndSet(int index, boolean expectedValue, boolean newValue) { - return (boolean) AA.compareAndSet(array, index, expectedValue, newValue); - } - - @Override - public String toString() { - int iMax = array.length - 1; - if (iMax == -1) - return "[]"; - - StringBuilder b = new StringBuilder(); - b.append('['); - for (int i = 0; ; i++) { - b.append(get(i)); - if (i == iMax) - return b.append(']').toString(); - b.append(',').append(' '); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/parallel/BurstExecutor.java b/core/src/main/java/com/volmit/iris/util/parallel/BurstExecutor.java deleted file mode 100644 index dc512a9f3..000000000 --- a/core/src/main/java/com/volmit/iris/util/parallel/BurstExecutor.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.parallel; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; -import lombok.Getter; -import lombok.Setter; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; - -@SuppressWarnings("ALL") -public class BurstExecutor { - private final ExecutorService executor; - @Getter - private final KList> futures; - @Setter - private boolean multicore = true; - - public BurstExecutor(ExecutorService executor, int burstSizeEstimate) { - this.executor = executor; - futures = new KList>(burstSizeEstimate); - } - - @SuppressWarnings("UnusedReturnValue") - public Future queue(Runnable r) { - if (!multicore) { - r.run(); - return CompletableFuture.completedFuture(null); - } - - synchronized (futures) { - - Future c = executor.submit(r); - futures.add(c); - return c; - } - } - - public BurstExecutor queue(List r) { - if (!multicore) { - for (Runnable i : new KList<>(r)) { - i.run(); - } - - return this; - } - - synchronized (futures) { - for (Runnable i : new KList<>(r)) { - queue(i); - } - } - - return this; - } - - public BurstExecutor queue(Runnable[] r) { - if (!multicore) { - for (Runnable i : new KList<>(r)) { - i.run(); - } - - return this; - } - - synchronized (futures) { - for (Runnable i : r) { - queue(i); - } - } - - return this; - } - - public void complete() { - if (!multicore) { - return; - } - - synchronized (futures) { - if (futures.isEmpty()) { - return; - } - - try { - for (Future i : futures) { - i.get(); - } - - futures.clear(); - } catch (InterruptedException | ExecutionException e) { - Iris.reportError(e); - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/parallel/BurstedHunk.java b/core/src/main/java/com/volmit/iris/util/parallel/BurstedHunk.java deleted file mode 100644 index abe41466c..000000000 --- a/core/src/main/java/com/volmit/iris/util/parallel/BurstedHunk.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.parallel; - - -import com.volmit.iris.util.hunk.Hunk; - -public interface BurstedHunk extends Hunk { - int getOffsetX(); - - int getOffsetY(); - - int getOffsetZ(); -} diff --git a/core/src/main/java/com/volmit/iris/util/parallel/GridLock.java b/core/src/main/java/com/volmit/iris/util/parallel/GridLock.java deleted file mode 100644 index 1afafe7fe..000000000 --- a/core/src/main/java/com/volmit/iris/util/parallel/GridLock.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.parallel; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.function.NastyRunnable; -import com.volmit.iris.util.hunk.Hunk; -import com.volmit.iris.util.io.IORunnable; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Supplier; - -public class GridLock { - private final Hunk locks; - - public GridLock(int x, int z) { - this.locks = Hunk.newAtomicHunk(x, 1, z); - locks.iterateSync((a, b, c) -> locks.set(a, b, c, new ReentrantLock())); - } - - public void with(int x, int z, Runnable r) { - lock(x, z); - r.run(); - unlock(x, z); - } - - public void withNasty(int x, int z, NastyRunnable r) throws Throwable { - lock(x, z); - r.run(); - unlock(x, z); - } - - public void withIO(int x, int z, IORunnable r) throws IOException { - lock(x, z); - r.run(); - unlock(x, z); - } - - public T withResult(int x, int z, Supplier r) { - lock(x, z); - T t = r.get(); - unlock(x, z); - return t; - } - - public void withAll(Runnable r) { - locks.iterateSync((a, b, c, d) -> d.lock()); - r.run(); - locks.iterateSync((a, b, c, d) -> d.unlock()); - } - - public T withAllResult(Supplier r) { - locks.iterateSync((a, b, c, d) -> d.lock()); - T t = r.get(); - locks.iterateSync((a, b, c, d) -> d.unlock()); - - return t; - } - - public boolean tryLock(int x, int z) { - return locks.get(x, 0, z).tryLock(); - } - - public boolean tryLock(int x, int z, long timeout) { - try { - return locks.get(x, 0, z).tryLock(timeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Iris.reportError(e); - } - - return false; - } - - public void lock(int x, int z) { - locks.get(x, 0, z).lock(); - } - - public void unlock(int x, int z) { - locks.get(x, 0, z).unlock(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/parallel/HyperLock.java b/core/src/main/java/com/volmit/iris/util/parallel/HyperLock.java deleted file mode 100644 index 158a55e9d..000000000 --- a/core/src/main/java/com/volmit/iris/util/parallel/HyperLock.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.parallel; - -import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.util.function.NastyRunnable; -import com.volmit.iris.util.function.NastySupplier; -import com.volmit.iris.util.io.IORunnable; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Supplier; - -public class HyperLock { - private final ConcurrentLinkedHashMap locks; - private boolean enabled = true; - private boolean fair = false; - - public HyperLock() { - this(1024, false); - } - - public HyperLock(int capacity) { - this(capacity, false); - } - - public HyperLock(int capacity, boolean fair) { - this.fair = fair; - locks = new ConcurrentLinkedHashMap.Builder() - .initialCapacity(capacity) - .maximumWeightedCapacity(capacity) - .listener((k, v) -> { - if (v.isLocked() || v.isHeldByCurrentThread()) { - Iris.warn("InfiniLock Eviction of " + k + " still has locks on it!"); - } - }) - .concurrencyLevel(32) - .build(); - } - - public void with(int x, int z, Runnable r) { - lock(x, z); - try { - r.run(); - } finally { - unlock(x, z); - } - } - - public void withLong(long k, Runnable r) { - int x = Cache.keyX(k), z = Cache.keyZ(k); - lock(x, z); - try { - r.run(); - } finally { - unlock(x, z); - } - } - - public void withNasty(int x, int z, NastyRunnable r) throws Throwable { - lock(x, z); - Throwable ee = null; - try { - r.run(); - } catch (Throwable e) { - ee = e; - } finally { - unlock(x, z); - - if (ee != null) { - throw ee; - } - } - } - - public void withIO(int x, int z, IORunnable r) throws IOException { - lock(x, z); - IOException ee = null; - try { - r.run(); - } catch (IOException e) { - ee = e; - } finally { - unlock(x, z); - - if (ee != null) { - throw ee; - } - } - } - - public T withResult(int x, int z, Supplier r) { - lock(x, z); - try { - return r.get(); - } finally { - unlock(x, z); - } - } - - public T withNastyResult(int x, int z, NastySupplier r) throws Throwable { - lock(x, z); - try { - return r.get(); - } finally { - unlock(x, z); - } - } - - public boolean tryLock(int x, int z) { - return getLock(x, z).tryLock(); - } - - public boolean tryLock(int x, int z, long timeout) { - try { - return getLock(x, z).tryLock(timeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Iris.reportError(e); - } - - return false; - } - - private ReentrantLock getLock(int x, int z) { - return locks.computeIfAbsent(Cache.key(x, z), k -> new ReentrantLock(fair)); - } - - public void lock(int x, int z) { - if (!enabled) { - return; - } - - getLock(x, z).lock(); - } - - public void unlock(int x, int z) { - if (!enabled) { - return; - } - - getLock(x, z).unlock(); - } - - public void disable() { - enabled = false; - locks.values().forEach(ReentrantLock::lock); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java b/core/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java deleted file mode 100644 index 71eb45dfe..000000000 --- a/core/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.parallel; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import kotlinx.coroutines.CoroutineDispatcher; -import kotlinx.coroutines.ExecutorsKt; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.IntSupplier; - -public class MultiBurst implements ExecutorService { - private static final long TIMEOUT = Long.getLong("iris.burst.timeout", 15000); - public static final MultiBurst burst = new MultiBurst(); - public static final MultiBurst ioBurst = new MultiBurst("Iris IO", () -> IrisSettings.get().getConcurrency().getIoParallelism()); - private final AtomicLong last; - private final String name; - private final int priority; - private final IntSupplier parallelism; - private final Object lock = new Object(); - private volatile ExecutorService service; - private volatile CoroutineDispatcher dispatcher; - - public MultiBurst() { - this("Iris"); - } - - public MultiBurst(String name) { - this(name, Thread.MIN_PRIORITY, () -> IrisSettings.get().getConcurrency().getParallelism()); - } - - public MultiBurst(String name, IntSupplier parallelism) { - this(name, Thread.MIN_PRIORITY, parallelism); - } - - public MultiBurst(String name, int priority, IntSupplier parallelism) { - this.name = name; - this.priority = priority; - this.parallelism = parallelism; - last = new AtomicLong(M.ms()); - } - - private ExecutorService getService() { - last.set(M.ms()); - if (service != null && !service.isShutdown()) - return service; - - synchronized (lock) { - if (service != null && !service.isShutdown()) - return service; - - service = new ForkJoinPool(IrisSettings.getThreadCount(parallelism.getAsInt()), - new ForkJoinPool.ForkJoinWorkerThreadFactory() { - int m = 0; - - @Override - public ForkJoinWorkerThread newThread(ForkJoinPool pool) { - final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); - worker.setPriority(priority); - worker.setName(name + " " + ++m); - return worker; - } - }, - (t, e) -> e.printStackTrace(), true); - dispatcher = ExecutorsKt.from(service); - return service; - } - } - - public CoroutineDispatcher getDispatcher() { - getService(); - return dispatcher; - } - - public void burst(Runnable... r) { - burst(r.length).queue(r).complete(); - } - - public void burst(boolean multicore, Runnable... r) { - if (multicore) { - burst(r); - } else { - sync(r); - } - } - - public void burst(List r) { - burst(r.size()).queue(r).complete(); - } - - public void burst(boolean multicore, List r) { - if (multicore) { - burst(r); - } else { - sync(r); - } - } - - private void sync(List r) { - for (Runnable i : new KList<>(r)) { - i.run(); - } - } - - public void sync(Runnable... r) { - for (Runnable i : r) { - i.run(); - } - } - - public void sync(KList r) { - for (Runnable i : r) { - i.run(); - } - } - - public BurstExecutor burst(int estimate) { - return new BurstExecutor(getService(), estimate); - } - - public BurstExecutor burst() { - return burst(16); - } - - public BurstExecutor burst(boolean multicore) { - BurstExecutor b = burst(); - b.setMulticore(multicore); - return b; - } - - public Future lazySubmit(Callable o) { - return getService().submit(o); - } - - public void lazy(Runnable o) { - getService().execute(o); - } - - public Future future(Runnable o) { - return getService().submit(o); - } - - public Future complete(Runnable o) { - return getService().submit(o); - } - - public Future completeValue(Callable o) { - return getService().submit(o); - } - - public CompletableFuture completableFuture(Callable o) { - CompletableFuture f = new CompletableFuture<>(); - getService().submit(() -> { - try { - f.complete(o.call()); - } catch (Exception e) { - f.completeExceptionally(e); - } - }); - return f; - } - - @Override - public void shutdown() { - close(); - } - - @NotNull - @Override - public List shutdownNow() { - close(); - return List.of(); - } - - @Override - public boolean isShutdown() { - return service == null || service.isShutdown(); - } - - @Override - public boolean isTerminated() { - return service == null || service.isTerminated(); - } - - @Override - public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException { - return service == null || service.awaitTermination(timeout, unit); - } - - @Override - public void execute(@NotNull Runnable command) { - getService().execute(command); - } - - @NotNull - @Override - public Future submit(@NotNull Callable task) { - return getService().submit(task); - } - - @NotNull - @Override - public Future submit(@NotNull Runnable task, T result) { - return getService().submit(task, result); - } - - @NotNull - @Override - public Future submit(@NotNull Runnable task) { - return getService().submit(task); - } - - @NotNull - @Override - public List> invokeAll(@NotNull Collection> tasks) throws InterruptedException { - return getService().invokeAll(tasks); - } - - @NotNull - @Override - public List> invokeAll(@NotNull Collection> tasks, long timeout, @NotNull TimeUnit unit) throws InterruptedException { - return getService().invokeAll(tasks, timeout, unit); - } - - @NotNull - @Override - public T invokeAny(@NotNull Collection> tasks) throws InterruptedException, ExecutionException { - return getService().invokeAny(tasks); - } - - @Override - public T invokeAny(@NotNull Collection> tasks, long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - return getService().invokeAny(tasks, timeout, unit); - } - - public void close() { - if (service != null) { - close(service); - } - } - - public static void close(ExecutorService service) { - service.shutdown(); - PrecisionStopwatch p = PrecisionStopwatch.start(); - try { - while (!service.awaitTermination(1, TimeUnit.SECONDS)) { - Iris.info("Still waiting to shutdown burster..."); - if (p.getMilliseconds() > TIMEOUT) { - Iris.warn("Forcing Shutdown..."); - - try { - service.shutdownNow(); - } catch (Throwable e) { - - } - - break; - } - } - } catch (Throwable e) { - e.printStackTrace(); - Iris.reportError(e); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/parallel/NOOPGridLock.java b/core/src/main/java/com/volmit/iris/util/parallel/NOOPGridLock.java deleted file mode 100644 index bc6ee8f2b..000000000 --- a/core/src/main/java/com/volmit/iris/util/parallel/NOOPGridLock.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.parallel; - -import com.volmit.iris.util.function.NastyRunnable; -import com.volmit.iris.util.io.IORunnable; - -import java.io.IOException; -import java.util.function.Supplier; - -public class NOOPGridLock extends GridLock { - public NOOPGridLock(int x, int z) { - super(x, z); - } - - @Override - public void with(int x, int z, Runnable r) { - r.run(); - } - - @Override - public void withNasty(int x, int z, NastyRunnable r) throws Throwable { - r.run(); - } - - @Override - public void withIO(int x, int z, IORunnable r) throws IOException { - r.run(); - } - - @Override - public T withResult(int x, int z, Supplier r) { - return r.get(); - } - - @Override - public void withAll(Runnable r) { - r.run(); - } - - @Override - public T withAllResult(Supplier r) { - return r.get(); - } - - @Override - public boolean tryLock(int x, int z) { - return true; - } - - @Override - public boolean tryLock(int x, int z, long timeout) { - return true; - } - - @Override - public void lock(int x, int z) { - - } - - @Override - public void unlock(int x, int z) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/util/parallel/StreamUtils.java b/core/src/main/java/com/volmit/iris/util/parallel/StreamUtils.java deleted file mode 100644 index dad656936..000000000 --- a/core/src/main/java/com/volmit/iris/util/parallel/StreamUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.volmit.iris.util.parallel; - -import com.volmit.iris.util.math.Position2; -import lombok.SneakyThrows; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -public class StreamUtils { - - public static Stream streamRadius(int x, int z, int radius) { - return streamRadius(x, z, radius, radius); - } - - public static Stream streamRadius(int x, int z, int radiusX, int radiusZ) { - return IntStream.rangeClosed(-radiusX, radiusX) - .mapToObj(xx -> IntStream.rangeClosed(-radiusZ, radiusZ) - .mapToObj(zz -> new Position2(x + xx, z + zz))) - .flatMap(Function.identity()); - } - - public static void forEach(Stream stream, Function> mapper, Consumer consumer, @Nullable MultiBurst burst) { - forEach(stream.flatMap(mapper), consumer, burst); - } - - @SneakyThrows - public static void forEach(Stream stream, Consumer task, @Nullable MultiBurst burst) { - if (burst == null) stream.forEach(task); - else { - var list = stream.toList(); - var exec = burst.burst(list.size()); - list.forEach(val -> exec.queue(() -> task.accept(val))); - exec.complete(); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/parallel/SyncExecutor.java b/core/src/main/java/com/volmit/iris/util/parallel/SyncExecutor.java deleted file mode 100644 index 00966b226..000000000 --- a/core/src/main/java/com/volmit/iris/util/parallel/SyncExecutor.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.volmit.iris.util.parallel; - -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.scheduling.SR; -import org.jetbrains.annotations.NotNull; - -import java.util.Queue; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; - -public class SyncExecutor implements Executor, AutoCloseable { - private final CountDownLatch latch = new CountDownLatch(1); - private final Queue queue = new ConcurrentLinkedQueue<>(); - private final AtomicBoolean closed = new AtomicBoolean(false); - - public SyncExecutor(int msPerTick) { - new SR() { - @Override - public void run() { - var time = M.ms() + msPerTick; - while (time > M.ms()) { - Runnable r = queue.poll(); - if (r == null) break; - r.run(); - } - - if (closed.get() && queue.isEmpty()) { - cancel(); - latch.countDown(); - } - } - }; - } - - @Override - public void execute(@NotNull Runnable command) { - if (closed.get()) throw new IllegalStateException("Executor is closed!"); - queue.add(command); - } - - @Override - public void close() throws Exception { - closed.set(true); - latch.await(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/CancellableTask.java b/core/src/main/java/com/volmit/iris/util/plugin/CancellableTask.java deleted file mode 100644 index 2889b8758..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/CancellableTask.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -public interface CancellableTask { - void cancel(); -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/Command.java b/core/src/main/java/com/volmit/iris/util/plugin/Command.java deleted file mode 100644 index c223c84b4..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/Command.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.util.format.C; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target(FIELD) -public @interface Command { - String value() default ""; - -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/CommandDummy.java b/core/src/main/java/com/volmit/iris/util/plugin/CommandDummy.java deleted file mode 100644 index c46289bf6..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/CommandDummy.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.permissions.Permission; -import org.bukkit.permissions.PermissionAttachment; -import org.bukkit.permissions.PermissionAttachmentInfo; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Set; -import java.util.UUID; - -public class CommandDummy implements CommandSender { - @Override - public void sendMessage(@NotNull String message) { - - } - - @Override - public void sendMessage(@NotNull String... messages) { - - } - - @Override - public void sendMessage(@Nullable UUID sender, @NotNull String message) { - - } - - @Override - public void sendMessage(@Nullable UUID sender, @NotNull String... messages) { - - } - - @NotNull - @Override - public Server getServer() { - return null; - } - - @NotNull - @Override - public String getName() { - return null; - } - - @NotNull - @Override - public Spigot spigot() { - return null; - } - - @Override - public boolean isPermissionSet(@NotNull String name) { - return false; - } - - @Override - public boolean isPermissionSet(@NotNull Permission perm) { - return false; - } - - @Override - public boolean hasPermission(@NotNull String name) { - return false; - } - - @Override - public boolean hasPermission(@NotNull Permission perm) { - return false; - } - - @NotNull - @Override - public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) { - return null; - } - - @NotNull - @Override - public PermissionAttachment addAttachment(@NotNull Plugin plugin) { - return null; - } - - @Nullable - @Override - public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) { - return null; - } - - @Nullable - @Override - public PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) { - return null; - } - - @Override - public void removeAttachment(@NotNull PermissionAttachment attachment) { - - } - - @Override - public void recalculatePermissions() { - - } - - @NotNull - @Override - public Set getEffectivePermissions() { - return null; - } - - @Override - public boolean isOp() { - return false; - } - - @Override - public void setOp(boolean value) { - - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/Control.java b/core/src/main/java/com/volmit/iris/util/plugin/Control.java deleted file mode 100644 index ed71caad8..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/Control.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target(FIELD) -public @interface Control { - -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/Controller.java b/core/src/main/java/com/volmit/iris/util/plugin/Controller.java deleted file mode 100644 index 0aa777552..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/Controller.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.Iris; - -public abstract class Controller implements IController { - private final String name; - private int tickRate; - - public Controller() { - name = getClass().getSimpleName().replaceAll("Controller", "") + " Controller"; - tickRate = -1; - } - - protected void setTickRate(@SuppressWarnings("SameParameterValue") int rate) { - this.tickRate = rate; - } - - protected void disableTicking() { - setTickRate(-1); - } - - @Override - public void l(Object l) { - Iris.info("[" + getName() + "]: " + l); - } - - @Override - public void w(Object l) { - Iris.warn("[" + getName() + "]: " + l); - } - - @Override - public void f(Object l) { - Iris.error("[" + getName() + "]: " + l); - } - - @Override - public void v(Object l) { - Iris.verbose("[" + getName() + "]: " + l); - } - - @Override - public String getName() { - return name; - } - - @Override - public abstract void start(); - - @Override - public abstract void stop(); - - @Override - public abstract void tick(); - - @Override - public int getTickInterval() { - return tickRate; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/ICommand.java b/core/src/main/java/com/volmit/iris/util/plugin/ICommand.java deleted file mode 100644 index 067198766..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/ICommand.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.util.collection.KList; - -/** - * Represents a pawn command - * - * @author cyberpwn - */ -public interface ICommand { - KList getRequiredPermissions(); - - /** - * Get the name of this command (node) - * - * @return the node - */ - String getNode(); - - /** - * Get all (realized) nodes of this command - * - * @return the nodes - */ - KList getNodes(); - - /** - * Get all (every) node in this command - * - * @return all nodes - */ - KList getAllNodes(); - - /** - * Add a node to this command - * - * @param node the node - */ - void addNode(String node); - - /** - * Handle a command. If this is a subcommand, parameters after the subcommand - * will be adapted in args for you - * - * @param sender the volume sender (pre-tagged) - * @param args the arguments after this command node - * @return return true to mark it as handled - */ - boolean handle(VolmitSender sender, String[] args); - - KList handleTab(VolmitSender sender, String[] args); -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/IController.java b/core/src/main/java/com/volmit/iris/util/plugin/IController.java deleted file mode 100644 index 401780733..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/IController.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import org.bukkit.event.Listener; - -@SuppressWarnings("EmptyMethod") -public interface IController extends Listener { - String getName(); - - void start(); - - void stop(); - - void tick(); - - int getTickInterval(); - - void l(Object l); - - void w(Object l); - - void f(Object l); - - void v(Object l); -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/Instance.java b/core/src/main/java/com/volmit/iris/util/plugin/Instance.java deleted file mode 100644 index 9eac62fc7..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/Instance.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target(FIELD) -public @interface Instance { - -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/MortarCommand.java b/core/src/main/java/com/volmit/iris/util/plugin/MortarCommand.java deleted file mode 100644 index 8e30d0186..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/MortarCommand.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.C; -import org.bukkit.Sound; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.util.Comparator; - -/** - * Represents a pawn command - * - * @author cyberpwn - */ -public abstract class MortarCommand implements ICommand { - private final KList children; - private final KList nodes; - private final KList requiredPermissions; - private final String node; - private String category; - private String description; - - /** - * Override this with a super constructor as most commands shouldn't change - * these parameters - * - * @param node the node (primary node) i.e. volume - * @param nodes the aliases. i.e. v, vol, bile - */ - public MortarCommand(String node, String... nodes) { - category = ""; - this.node = node; - this.nodes = new KList<>(nodes); - requiredPermissions = new KList<>(); - children = buildChildren(); - description = "No Description"; - } - - @Override - public KList handleTab(VolmitSender sender, String[] args) { - KList v = new KList<>(); - if (args.length == 0) { - for (MortarCommand i : getChildren()) { - v.add(i.getNode()); - } - } - - addTabOptions(sender, args, v); - - if (v.isEmpty()) { - return null; - } - - if (sender.isPlayer() && IrisSettings.get().getGeneral().isCommandSounds()) { - sender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 0.25f, 1.7f); - } - - return v; - } - - public abstract void addTabOptions(VolmitSender sender, String[] args, KList list); - - public void printHelp(VolmitSender sender) { - boolean b = false; - - for (MortarCommand i : getChildren()) { - for (String j : i.getRequiredPermissions()) { - if (!sender.hasPermission(j)) { - } - } - - b = true; - - sender.sendMessage("" + C.GREEN + i.getNode() + " " + "" + (getArgsUsage().trim().isEmpty() ? "" : (C.WHITE + i.getArgsUsage())) + C.GRAY + " - " + i.getDescription()); - } - - if (!b) { - sender.sendMessage("There are either no sub-commands or you do not have permission to use them."); - } - - if (sender.isPlayer() && IrisSettings.get().getGeneral().isCommandSounds()) { - sender.playSound(Sound.ITEM_BOOK_PAGE_TURN, 0.28f, 1.4f); - sender.playSound(Sound.ITEM_AXE_STRIP, 0.35f, 1.7f); - } - } - - protected abstract String getArgsUsage(); - - public String getDescription() { - return description; - } - - protected void setDescription(String description) { - this.description = description; - } - - protected void requiresPermission(MortarPermission node) { - if (node == null) { - return; - } - - requiresPermission(node.toString()); - } - - protected void requiresPermission(String node) { - if (node == null) { - return; - } - - requiredPermissions.add(node); - } - - public void rejectAny(int past, VolmitSender sender, String[] a) { - if (a.length > past) { - int p = past; - - StringBuilder m = new StringBuilder(); - - for (String i : a) { - p--; - if (p < 0) { - m.append(i).append(", "); - } - } - - if (!m.toString().trim().isEmpty()) { - sender.sendMessage("Parameters Ignored: " + m); - } - } - } - - @Override - public String getNode() { - return node; - } - - @Override - public KList getNodes() { - return nodes; - } - - @Override - public KList getAllNodes() { - return getNodes().copy().qadd(getNode()); - } - - @Override - public void addNode(String node) { - getNodes().add(node); - } - - public KList getChildren() { - return children; - } - - private KList buildChildren() { - KList p = new KList<>(); - - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Command.class)) { - try { - i.setAccessible(true); - MortarCommand pc = (MortarCommand) i.getType().getConstructor().newInstance(); - Command c = i.getAnnotation(Command.class); - - if (!c.value().trim().isEmpty()) { - pc.setCategory(c.value().trim()); - } else { - pc.setCategory(getCategory()); - } - - p.add(pc); - } catch (IllegalArgumentException | IllegalAccessException | InstantiationException | - InvocationTargetException | NoSuchMethodException | SecurityException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - } - - p.sort(Comparator.comparing(MortarCommand::getNode)); - - return p; - } - - @Override - public KList getRequiredPermissions() { - return requiredPermissions; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/MortarPermission.java b/core/src/main/java/com/volmit/iris/util/plugin/MortarPermission.java deleted file mode 100644 index 3466db3c9..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/MortarPermission.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; -import org.bukkit.command.CommandSender; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; - -public abstract class MortarPermission { - private MortarPermission parent; - - public MortarPermission() { - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Permission.class)) { - try { - MortarPermission px = (MortarPermission) i.getType().getConstructor().newInstance(); - px.setParent(this); - i.set(Modifier.isStatic(i.getModifiers()) ? null : this, px); - } catch (IllegalArgumentException | IllegalAccessException | InstantiationException | - InvocationTargetException | NoSuchMethodException | SecurityException e) { - e.printStackTrace(); - Iris.reportError(e); - } - } - } - } - - public KList getChildren() { - KList p = new KList<>(); - - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Permission.class)) { - try { - p.add((MortarPermission) i.get(Modifier.isStatic(i.getModifiers()) ? null : this)); - } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { - e.printStackTrace(); - Iris.reportError(e); - } - } - } - - return p; - } - - public String getFullNode() { - if (hasParent()) { - return getParent().getFullNode() + "." + getNode(); - } - - return getNode(); - } - - protected abstract String getNode(); - - public abstract String getDescription(); - - public abstract boolean isDefault(); - - @Override - public String toString() { - return getFullNode(); - } - - public boolean hasParent() { - return getParent() != null; - } - - public MortarPermission getParent() { - return parent; - } - - public void setParent(MortarPermission parent) { - this.parent = parent; - } - - public boolean has(CommandSender sender) { - return sender.hasPermission(getFullNode()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/Permission.java b/core/src/main/java/com/volmit/iris/util/plugin/Permission.java deleted file mode 100644 index e13a18a3a..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/Permission.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target(FIELD) -public @interface Permission { - -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/PluginRegistry.java b/core/src/main/java/com/volmit/iris/util/plugin/PluginRegistry.java deleted file mode 100644 index a762f2ed9..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/PluginRegistry.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class PluginRegistry { - private final KMap registry = new KMap<>(); - @Getter - private final String namespace; - - public void unregisterAll() { - registry.clear(); - } - - public KList getRegistries() { - return registry.k(); - } - - public T get(String s) { - if (!registry.containsKey(s)) { - return null; - } - - return registry.get(s); - } - - public void register(String s, T t) { - registry.put(s, t); - } - - public void unregister(String s) { - registry.remove(s); - } - - public T resolve(String id) { - if (registry.isEmpty()) { - return null; - } - - return registry.get(id); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/PluginRegistryGroup.java b/core/src/main/java/com/volmit/iris/util/plugin/PluginRegistryGroup.java deleted file mode 100644 index f6de8db3e..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/PluginRegistryGroup.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; - -public class PluginRegistryGroup { - private final KMap> registries = new KMap<>(); - - public T resolve(String namespace, String id) { - if (registries.isEmpty()) { - return null; - } - - PluginRegistry r = registries.get(namespace); - if (r == null) { - return null; - } - - return r.resolve(id); - } - - public void clearRegistries() { - registries.clear(); - } - - public void removeRegistry(String namespace) { - registries.remove(namespace); - } - - public PluginRegistry getRegistry(String namespace) { - return registries.computeIfAbsent(namespace, PluginRegistry::new); - } - - public KList compile() { - KList l = new KList<>(); - registries.values().forEach((i) - -> i.getRegistries().forEach((j) - -> l.add(i.getNamespace() + ":" + j))); - return l; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/RouterCommand.java b/core/src/main/java/com/volmit/iris/util/plugin/RouterCommand.java deleted file mode 100644 index e412a3ddc..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/RouterCommand.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; - -/** - * Assistive command router - * - * @author cyberpwn - */ -public class RouterCommand extends org.bukkit.command.Command { - private final CommandExecutor ex; - private String usage; - - /** - * The router command routes commands to bukkit executors - * - * @param realCommand the real command - * @param ex the executor - */ - public RouterCommand(ICommand realCommand, CommandExecutor ex) { - super(realCommand.getNode().toLowerCase()); - setAliases(realCommand.getNodes()); - - this.ex = ex; - } - - - @Override - public Command setUsage(String u) { - this.usage = u; - return this; - } - - - @Override - public String getUsage() { - return usage; - } - - @Override - public boolean execute(CommandSender sender, String commandLabel, String[] args) { - return ex.onCommand(sender, this, commandLabel, args); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/VirtualCommand.java b/core/src/main/java/com/volmit/iris/util/plugin/VirtualCommand.java deleted file mode 100644 index 789c88661..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/VirtualCommand.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.reflect.V; -import org.bukkit.Bukkit; -import org.bukkit.Sound; -import org.bukkit.command.CommandSender; - -import java.lang.reflect.Field; - -/** - * Represents a virtual command. A chain of iterative processing through - * subcommands. - * - * @author cyberpwn - */ -public class VirtualCommand { - private final ICommand command; - private final String tag; - - private final KMap, VirtualCommand> children; - - public VirtualCommand(ICommand command) { - this(command, ""); - } - - public VirtualCommand(ICommand command, String tag) { - this.command = command; - children = new KMap<>(); - this.tag = tag; - - for (Field i : command.getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Command.class)) { - try { - Command cc = i.getAnnotation(Command.class); - ICommand cmd = (ICommand) i.getType().getConstructor().newInstance(); - new V(command, true, true).set(i.getName(), cmd); - children.put(cmd.getAllNodes(), new VirtualCommand(cmd, cc.value().trim().isEmpty() ? tag : cc.value().trim())); - } catch (Exception e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - } - } - - public String getTag() { - return tag; - } - - public ICommand getCommand() { - return command; - } - - public KMap, VirtualCommand> getChildren() { - return children; - } - - public boolean hit(CommandSender sender, KList chain) { - return hit(sender, chain, null); - } - - public boolean hit(CommandSender sender, KList chain, String label) { - VolmitSender vs = new VolmitSender(sender); - vs.setTag(tag); - - if (label != null) { - vs.setCommand(label); - } - - if (chain.isEmpty()) { - if (!checkPermissions(sender, command)) { - return true; - } - - return command.handle(vs, new String[0]); - } - - String nl = chain.get(0); - - for (KList i : children.k()) { - for (String j : i) { - if (j.equalsIgnoreCase(nl)) { - vs.setCommand(chain.get(0)); - VirtualCommand cmd = children.get(i); - KList c = chain.copy(); - c.remove(0); - if (cmd.hit(sender, c, vs.getCommand())) { - if (vs.isPlayer() && IrisSettings.get().getGeneral().isCommandSounds()) { - vs.player().getWorld().playSound(vs.player().getLocation(), Sound.ITEM_AXE_STRIP, 0.35f, 1.8f); - } - - return true; - } - } - } - } - - if (!checkPermissions(sender, command)) { - return true; - } - - return command.handle(vs, chain.toArray(new String[0])); - } - - public KList hitTab(CommandSender sender, KList chain, String label) { - VolmitSender vs = new VolmitSender(sender); - vs.setTag(tag); - - if (label != null) - vs.setCommand(label); - - if (chain.isEmpty()) { - if (!checkPermissions(sender, command)) { - return null; - } - - return command.handleTab(vs, new String[0]); - } - - String nl = chain.get(0); - - for (KList i : children.k()) { - for (String j : i) { - if (j.equalsIgnoreCase(nl)) { - vs.setCommand(chain.get(0)); - VirtualCommand cmd = children.get(i); - KList c = chain.copy(); - c.remove(0); - KList v = cmd.hitTab(sender, c, vs.getCommand()); - if (v != null) { - return v; - } - } - } - } - - if (!checkPermissions(sender, command)) { - return null; - } - - return command.handleTab(vs, chain.toArray(new String[0])); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private boolean checkPermissions(CommandSender sender, ICommand command2) { - boolean failed = false; - - for (String i : command.getRequiredPermissions()) { - if (!sender.hasPermission(i)) { - failed = true; - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> sender.sendMessage("- " + C.WHITE + i), 0); - } - } - - if (failed) { - sender.sendMessage("Insufficient Permissions"); - return false; - } - - return true; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/VolmitPlugin.java b/core/src/main/java/com/volmit/iris/util/plugin/VolmitPlugin.java deleted file mode 100644 index 130f8b686..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/VolmitPlugin.java +++ /dev/null @@ -1,545 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.reflect.V; -import com.volmit.iris.util.scheduling.J; -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.*; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.event.HandlerList; -import org.bukkit.event.Listener; -import org.bukkit.permissions.PermissionDefault; -import org.bukkit.plugin.java.JavaPlugin; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -@SuppressWarnings("EmptyMethod") -public abstract class VolmitPlugin extends JavaPlugin implements Listener { - public static final boolean bad = false; - private KMap, VirtualCommand> commands; - private KList commandCache; - private KList permissionCache; - - public File getJarFile() { - return getFile(); - } - - public void l(Object l) { - Iris.info("[" + getName() + "]: " + l); - } - - public void w(Object l) { - Iris.warn("[" + getName() + "]: " + l); - } - - public void f(Object l) { - Iris.error("[" + getName() + "]: " + l); - } - - public void v(Object l) { - Iris.verbose("[" + getName() + "]: " + l); - } - - public void onEnable() { - registerInstance(); - registerPermissions(); - registerCommands(); - J.a(this::outputInfo); - registerListener(this); - start(); - } - - public void unregisterAll() { - unregisterListeners(); - unregisterCommands(); - unregisterPermissions(); - unregisterInstance(); - } - - private void outputInfo() { - try { - IO.delete(getDataFolder("info")); - getDataFolder("info").mkdirs(); - outputPluginInfo(); - outputCommandInfo(); - outputPermissionInfo(); - } catch (Throwable e) { - Iris.reportError(e); - - } - } - - private void outputPermissionInfo() throws IOException { - FileConfiguration fc = new YamlConfiguration(); - - for (MortarPermission i : permissionCache) { - chain(i, fc); - } - - fc.save(getDataFile("info", "permissions.yml")); - } - - private void chain(MortarPermission i, FileConfiguration fc) { - KList ff = new KList<>(); - - for (MortarPermission j : i.getChildren()) { - ff.add(j.getFullNode()); - } - - fc.set(i.getFullNode().replaceAll("\\Q.\\E", ",") + "." + "description", i.getDescription()); - fc.set(i.getFullNode().replaceAll("\\Q.\\E", ",") + "." + "default", i.isDefault()); - fc.set(i.getFullNode().replaceAll("\\Q.\\E", ",") + "." + "children", ff); - - for (MortarPermission j : i.getChildren()) { - chain(j, fc); - } - } - - private void outputCommandInfo() throws IOException { - FileConfiguration fc = new YamlConfiguration(); - - for (MortarCommand i : commandCache) { - chain(i, "/", fc); - } - - fc.save(getDataFile("info", "commands.yml")); - } - - private void chain(MortarCommand i, String c, FileConfiguration fc) { - String n = c + (c.length() == 1 ? "" : " ") + i.getNode(); - fc.set(n + "." + "description", i.getDescription()); - fc.set(n + "." + "required-permissions", i.getRequiredPermissions()); - fc.set(n + "." + "aliases", i.getAllNodes()); - - for (MortarCommand j : i.getChildren()) { - chain(j, n, fc); - } - } - - private void outputPluginInfo() throws IOException { - FileConfiguration fc = new YamlConfiguration(); - fc.set("version", getDescription().getVersion()); - fc.set("name", getDescription().getName()); - fc.save(getDataFile("info", "plugin.yml")); - } - - private void registerPermissions() { - permissionCache = new KList<>(); - - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Permission.class)) { - try { - i.setAccessible(true); - MortarPermission pc = (MortarPermission) i.getType().getConstructor().newInstance(); - i.set(Modifier.isStatic(i.getModifiers()) ? null : this, pc); - registerPermission(pc); - permissionCache.add(pc); - v("Registered Permissions " + pc.getFullNode() + " (" + i.getName() + ")"); - } catch (IllegalArgumentException | IllegalAccessException | InstantiationException | - InvocationTargetException | NoSuchMethodException | SecurityException e) { - Iris.reportError(e); - w("Failed to register permission (field " + i.getName() + ")"); - e.printStackTrace(); - } - } - } - - for (org.bukkit.permissions.Permission i : computePermissions()) { - try { - Bukkit.getPluginManager().addPermission(i); - } catch (Throwable e) { - Iris.reportError(e); - - } - } - } - - private KList computePermissions() { - KList g = new KList<>(); - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Permission.class)) { - try { - MortarPermission x = (MortarPermission) i.get(Modifier.isStatic(i.getModifiers()) ? null : this); - g.add(toPermission(x)); - g.addAll(computePermissions(x)); - } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - } - - return g.removeDuplicates(); - } - - private KList computePermissions(MortarPermission p) { - KList g = new KList<>(); - - if (p == null) { - return g; - } - - for (MortarPermission i : p.getChildren()) { - if (i == null) { - continue; - } - - g.add(toPermission(i)); - g.addAll(computePermissions(i)); - } - - return g; - } - - private org.bukkit.permissions.Permission toPermission(MortarPermission p) { - if (p == null) { - return null; - } - - org.bukkit.permissions.Permission perm = new org.bukkit.permissions.Permission(p.getFullNode() + (p.hasParent() ? "" : ".*")); - perm.setDescription(p.getDescription() == null ? "" : p.getDescription()); - perm.setDefault(p.isDefault() ? PermissionDefault.TRUE : PermissionDefault.OP); - - for (MortarPermission i : p.getChildren()) { - perm.getChildren().put(i.getFullNode(), true); - } - - return perm; - } - - private void registerPermission(MortarPermission pc) { - - } - - @Override - public void onDisable() { - stop(); - Bukkit.getScheduler().cancelTasks(this); - unregisterListener(this); - unregisterAll(); - } - - private void tickController(IController i) { - if (bad) { - return; - } - - if (i.getTickInterval() < 0) { - return; - } - - M.tick++; - if (M.interval(i.getTickInterval())) { - try { - i.tick(); - } catch (Throwable e) { - w("Failed to tick controller " + i.getName()); - e.printStackTrace(); - Iris.reportError(e); - } - } - } - - private void registerInstance() { - if (bad) { - return; - } - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Instance.class)) { - try { - i.setAccessible(true); - i.set(Modifier.isStatic(i.getModifiers()) ? null : this, this); - v("Registered Instance " + i.getName()); - } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { - w("Failed to register instance (field " + i.getName() + ")"); - e.printStackTrace(); - Iris.reportError(e); - } - } - } - } - - private void unregisterInstance() { - if (bad) { - return; - } - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Instance.class)) { - try { - i.setAccessible(true); - i.set(Modifier.isStatic(i.getModifiers()) ? null : this, null); - v("Unregistered Instance " + i.getName()); - } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { - w("Failed to unregister instance (field " + i.getName() + ")"); - e.printStackTrace(); - Iris.reportError(e); - } - } - } - } - - private void registerCommands() { - if (bad) { - return; - } - commands = new KMap<>(); - commandCache = new KList<>(); - - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(com.volmit.iris.util.plugin.Command.class)) { - try { - i.setAccessible(true); - MortarCommand pc = (MortarCommand) i.getType().getConstructor().newInstance(); - com.volmit.iris.util.plugin.Command c = i.getAnnotation(com.volmit.iris.util.plugin.Command.class); - registerCommand(pc, c.value()); - commandCache.add(pc); - v("Registered Commands /" + pc.getNode() + " (" + i.getName() + ")"); - } catch (IllegalArgumentException | IllegalAccessException | InstantiationException | - InvocationTargetException | NoSuchMethodException | SecurityException e) { - w("Failed to register command (field " + i.getName() + ")"); - e.printStackTrace(); - Iris.reportError(e); - } - } - } - } - - - @Override - public List onTabComplete(CommandSender sender, Command command, - String alias, String[] args) { - KList chain = new KList<>(); - - for (String i : args) { - if (i.trim().isEmpty()) { - continue; - } - - chain.add(i.trim()); - } - - for (KList i : commands.k()) { - for (String j : i) { - if (j.equalsIgnoreCase(alias)) { - VirtualCommand cmd = commands.get(i); - - List v = cmd.hitTab(sender, chain.copy(), alias); - if (v != null) { - return v; - } - } - } - } - - return super.onTabComplete(sender, command, alias, args); - } - - @Override - public boolean onCommand(CommandSender sender, org.bukkit.command.Command command, String label, String[] args) { - if (bad) { - return false; - } - - KList chain = new KList<>(); - chain.add(args); - - for (KList i : commands.k()) { - for (String j : i) { - if (j.equalsIgnoreCase(label)) { - VirtualCommand cmd = commands.get(i); - - if (cmd.hit(sender, chain.copy(), label)) { - return true; - } - } - } - } - - return false; - } - - public void registerCommand(ICommand cmd) { - registerCommand(cmd, ""); - } - - public void registerCommand(ICommand cmd, String subTag) { - if (bad) { - return; - } - - commands.put(cmd.getAllNodes(), new VirtualCommand(cmd, subTag.trim().isEmpty() ? getTag() : getTag(subTag.trim()))); - PluginCommand cc = getCommand(cmd.getNode().toLowerCase()); - - if (cc != null) { - cc.setExecutor(this); - cc.setUsage(getName() + ":" + getClass().toString() + ":" + cmd.getNode()); - } else { - RouterCommand r = new RouterCommand(cmd, this); - r.setUsage(getName() + ":" + getClass().toString()); - ((CommandMap) new V(Bukkit.getServer()).get("commandMap")).register("", r); - } - } - - public void unregisterCommand(ICommand cmd) { - if (bad) { - return; - } - try { - SimpleCommandMap m = new V(Bukkit.getServer()).get("commandMap"); - - Map k = new V(m).get("knownCommands"); - - for (Iterator> it = k.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - if (entry.getValue() instanceof Command) { - org.bukkit.command.Command c = entry.getValue(); - String u = c.getUsage(); - - if (u != null && u.equals(getName() + ":" + getClass().toString() + ":" + cmd.getNode())) { - if (c.unregister(m)) { - it.remove(); - v("Unregistered Command /" + cmd.getNode()); - } else { - Bukkit.getConsoleSender().sendMessage(getTag() + "Failed to unregister command " + c.getName()); - } - } - } - } - } catch (Throwable e) { - e.printStackTrace(); - Iris.reportError(e); - } - } - - public String getTag() { - if (bad) { - return ""; - } - return getTag(""); - } - - public void registerListener(Listener l) { - Iris.debug("Register Listener " + l.getClass().getSimpleName()); - Bukkit.getPluginManager().registerEvents(l, this); - } - - public void unregisterListener(Listener l) { - Iris.debug("Register Listener " + l.getClass().getSimpleName()); - HandlerList.unregisterAll(l); - } - - public void unregisterListeners() { - if (bad) { - return; - } - HandlerList.unregisterAll((Listener) this); - } - - public void unregisterCommands() { - if (bad) { - return; - } - for (VirtualCommand i : commands.v()) { - try { - unregisterCommand(i.getCommand()); - } catch (Throwable e) { - Iris.reportError(e); - - } - } - } - - private void unregisterPermissions() { - if (bad) { - return; - } - for (org.bukkit.permissions.Permission i : computePermissions()) { - Bukkit.getPluginManager().removePermission(i); - v("Unregistered Permission " + i.getName()); - } - } - - public File getDataFile(String... strings) { - File f = new File(getDataFolder(), new KList<>(strings).toString(File.separator)); - f.getParentFile().mkdirs(); - return f; - } - - public File getDataFileList(String pre, String[] strings) { - KList v = new KList<>(strings); - v.add(0, pre); - File f = new File(getDataFolder(), v.toString(File.separator)); - f.getParentFile().mkdirs(); - return f; - } - - public File getDataFolder(String... strings) { - if (strings.length == 0) { - return super.getDataFolder(); - } - - File f = new File(getDataFolder(), new KList<>(strings).toString(File.separator)); - f.mkdirs(); - - return f; - } - - public File getDataFolderNoCreate(String... strings) { - if (strings.length == 0) { - return super.getDataFolder(); - } - - File f = new File(getDataFolder(), new KList<>(strings).toString(File.separator)); - - return f; - } - - public File getDataFolderList(String pre, String[] strings) { - KList v = new KList<>(strings); - v.add(0, pre); - if (v.size() == 0) { - return super.getDataFolder(); - } - File f = new File(getDataFolder(), v.toString(File.separator)); - f.mkdirs(); - - return f; - } - - public abstract void start(); - - public abstract void stop(); - - public abstract String getTag(String subTag); -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/VolmitSender.java b/core/src/main/java/com/volmit/iris/util/plugin/VolmitSender.java deleted file mode 100644 index dccb95ce7..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/VolmitSender.java +++ /dev/null @@ -1,604 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.plugin; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.decree.DecreeParameter; -import com.volmit.iris.util.decree.virtual.VirtualDecreeCommand; -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.scheduling.J; -import lombok.Getter; -import lombok.Setter; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.title.Title; -import org.bukkit.Server; -import org.bukkit.Sound; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.permissions.Permission; -import org.bukkit.permissions.PermissionAttachment; -import org.bukkit.permissions.PermissionAttachmentInfo; -import org.bukkit.plugin.Plugin; - -import java.time.Duration; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Represents a volume sender. A command sender with extra crap in it - * - * @author cyberpwn - */ -public class VolmitSender implements CommandSender { - @Getter - private static final KMap helpCache = new KMap<>(); - private final CommandSender s; - private String tag; - @Getter - @Setter - private String command; - - /** - * Wrap a command sender - * - * @param s the command sender - */ - public VolmitSender(CommandSender s) { - tag = ""; - this.s = s; - } - - public VolmitSender(CommandSender s, String tag) { - this.tag = tag; - this.s = s; - } - - public static long getTick() { - return M.ms() / 16; - } - - public static String pulse(String colorA, String colorB, double speed) { - return ""; - } - - public static String pulse(double speed) { - return Form.f(invertSpread((((getTick() * 15D * speed) % 1000D) / 1000D)), 3).replaceAll("\\Q,\\E", ".").replaceAll("\\Q?\\E", "-"); - } - - public static double invertSpread(double v) { - return ((1D - v) * 2D) - 1D; - } - - public static KList paginate(KList all, int linesPerPage, int page, AtomicBoolean hasNext) { - int totalPages = (int) Math.ceil((double) all.size() / linesPerPage); - page = page < 0 ? 0 : page >= totalPages ? totalPages - 1 : page; - hasNext.set(page < totalPages - 1); - KList d = new KList<>(); - - for (int i = linesPerPage * page; i < Math.min(all.size(), linesPerPage * (page + 1)); i++) { - d.add(all.get(i)); - } - - return d; - } - - /** - * Get the command tag - * - * @return the command tag - */ - public String getTag() { - return tag; - } - - /** - * Set a command tag (prefix for sendMessage) - * - * @param tag the tag - */ - public void setTag(String tag) { - this.tag = tag; - } - - /** - * Is this sender a player? - * - * @return true if it is - */ - public boolean isPlayer() { - return getS() instanceof Player; - } - - /** - * Force cast to player (be sure to check first) - * - * @return a casted player - */ - public Player player() { - return (Player) getS(); - } - - /** - * Get the origin sender this object is wrapping - * - * @return the command sender - */ - public CommandSender getS() { - return s; - } - - @Override - public boolean isPermissionSet(String name) { - return s.isPermissionSet(name); - } - - @Override - public boolean isPermissionSet(Permission perm) { - return s.isPermissionSet(perm); - } - - @Override - public boolean hasPermission(String name) { - return s.hasPermission(name); - } - - @Override - public boolean hasPermission(Permission perm) { - return s.hasPermission(perm); - } - - @Override - public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) { - return s.addAttachment(plugin, name, value); - } - - @Override - public PermissionAttachment addAttachment(Plugin plugin) { - return s.addAttachment(plugin); - } - - @Override - public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) { - return s.addAttachment(plugin, name, value, ticks); - } - - @Override - public PermissionAttachment addAttachment(Plugin plugin, int ticks) { - return s.addAttachment(plugin, ticks); - } - - @Override - public void removeAttachment(PermissionAttachment attachment) { - s.removeAttachment(attachment); - } - - @Override - public void recalculatePermissions() { - s.recalculatePermissions(); - } - - @Override - public Set getEffectivePermissions() { - return s.getEffectivePermissions(); - } - - @Override - public boolean isOp() { - return s.isOp(); - } - - @Override - public void setOp(boolean value) { - s.setOp(value); - } - - public void hr() { - s.sendMessage("========================================================"); - } - - public void sendTitle(String title, String subtitle, int i, int s, int o) { - Iris.audiences.player(player()).showTitle(Title.title( - createComponent(title), - createComponent(subtitle), - Title.Times.times(Duration.ofMillis(i), Duration.ofMillis(s), Duration.ofMillis(o)))); - } - - public void sendProgress(double percent, String thing) { - //noinspection IfStatementWithIdenticalBranches - if (percent < 0) { - int l = 44; - int g = (int) (1D * l); - sendTitle(C.IRIS + thing + " ", 0, 500, 250); - sendActionNoProcessing("" + "" + pulse("#00ff80", "#00373d", 1D) + " " + Form.repeat(" ", g) + "" + Form.repeat(" ", l - g)); - } else { - int l = 44; - int g = (int) (percent * l); - sendTitle(C.IRIS + thing + " " + C.BLUE + "" + Form.pc(percent, 0), 0, 500, 250); - sendActionNoProcessing("" + "" + pulse("#00ff80", "#00373d", 1D) + " " + Form.repeat(" ", g) + "" + Form.repeat(" ", l - g)); - } - } - - public void sendAction(String action) { - Iris.audiences.player(player()).sendActionBar(createNoPrefixComponent(action)); - } - - public void sendActionNoProcessing(String action) { - Iris.audiences.player(player()).sendActionBar(createNoPrefixComponentNoProcessing(action)); - } - - public void sendTitle(String subtitle, int i, int s, int o) { - Iris.audiences.player(player()).showTitle(Title.title( - createNoPrefixComponent(" "), - createNoPrefixComponent(subtitle), - Title.Times.times(Duration.ofMillis(i), Duration.ofMillis(s), Duration.ofMillis(o)))); - } - - private Component createNoPrefixComponent(String message) { - if (!IrisSettings.get().getGeneral().canUseCustomColors(this)) { - String t = C.translateAlternateColorCodes('&', MiniMessage.miniMessage().stripTags(message)); - return MiniMessage.miniMessage().deserialize(C.mini(t)); - } - - String t = C.translateAlternateColorCodes('&', message); - String a = C.aura(t, IrisSettings.get().getGeneral().getSpinh(), IrisSettings.get().getGeneral().getSpins(), IrisSettings.get().getGeneral().getSpinb(), 0.36); - return MiniMessage.miniMessage().deserialize(a); - } - - private Component createNoPrefixComponentNoProcessing(String message) { - return MiniMessage.builder().postProcessor(c -> c).build().deserialize(C.mini(message)); - } - - private Component createComponent(String message) { - if (!IrisSettings.get().getGeneral().canUseCustomColors(this)) { - String t = C.translateAlternateColorCodes('&', MiniMessage.miniMessage().stripTags(getTag() + message)); - return MiniMessage.miniMessage().deserialize(C.mini(t)); - } - - String t = C.translateAlternateColorCodes('&', getTag() + message); - String a = C.aura(t, IrisSettings.get().getGeneral().getSpinh(), IrisSettings.get().getGeneral().getSpins(), IrisSettings.get().getGeneral().getSpinb()); - return MiniMessage.miniMessage().deserialize(a); - } - - private Component createComponentRaw(String message) { - if (!IrisSettings.get().getGeneral().canUseCustomColors(this)) { - String t = C.translateAlternateColorCodes('&', MiniMessage.miniMessage().stripTags(getTag() + message)); - return MiniMessage.miniMessage().deserialize(C.mini(t)); - } - - String t = C.translateAlternateColorCodes('&', getTag() + message); - return MiniMessage.miniMessage().deserialize(C.mini(t)); - } - - public void showWaiting(String passive, CompletableFuture f) { - AtomicInteger v = new AtomicInteger(); - AtomicReference g = new AtomicReference<>(); - v.set(J.ar(() -> { - if (f.isDone() && g.get() != null) { - J.car(v.get()); - sendAction(" "); - return; - } - - sendProgress(-1, passive); - }, 0)); - J.a(() -> { - try { - g.set(f.get()); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - }); - - } - - @Override - public void sendMessage(String message) { - if (s instanceof CommandDummy) { - return; - } - - if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { - s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message)); - return; - } - - if (message.contains("")) { - s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message.replaceAll("\\Q\\E", ""))); - return; - } - - try { - Iris.audiences.sender(s).sendMessage(createComponent(message)); - } catch (Throwable e) { - String t = C.translateAlternateColorCodes('&', getTag() + message); - String a = C.aura(t, IrisSettings.get().getGeneral().getSpinh(), IrisSettings.get().getGeneral().getSpins(), IrisSettings.get().getGeneral().getSpinb()); - - Iris.debug("Failure to parse " + a); - s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message)); - } - } - - public void sendMessageBasic(String message) { - s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message)); - } - - public void sendMessageRaw(String message) { - if (s instanceof CommandDummy) { - return; - } - - if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { - s.sendMessage(C.translateAlternateColorCodes('&', message)); - return; - } - - if (message.contains("")) { - s.sendMessage(message.replaceAll("\\Q\\E", "")); - return; - } - - try { - Iris.audiences.sender(s).sendMessage(createComponentRaw(message)); - } catch (Throwable e) { - String t = C.translateAlternateColorCodes('&', getTag() + message); - String a = C.aura(t, IrisSettings.get().getGeneral().getSpinh(), IrisSettings.get().getGeneral().getSpins(), IrisSettings.get().getGeneral().getSpinb()); - - Iris.debug("Failure to parse " + a); - s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message)); - } - } - - @Override - public void sendMessage(String[] messages) { - for (String str : messages) - sendMessage(str); - } - - @Override - public void sendMessage(UUID uuid, String message) { - sendMessage(message); - } - - @Override - public void sendMessage(UUID uuid, String[] messages) { - sendMessage(messages); - } - - @Override - public Server getServer() { - return s.getServer(); - } - - @Override - public String getName() { - return s.getName(); - } - - @Override - public Spigot spigot() { - return s.spigot(); - } - - private String pickRandoms(int max, VirtualDecreeCommand i) { - KList m = new KList<>(); - for (int ix = 0; ix < max; ix++) { - m.add((i.isNode() - ? (i.getNode().getParameters().isNotEmpty()) - ? "<#aebef2>✦ <#5ef288>" - + i.getParentPath() - + " <#42ecf5>" - + i.getName() + " " - + i.getNode().getParameters().shuffleCopy(RNG.r).convert((f) - -> (f.isRequired() || RNG.r.b(0.5) - ? "<#f2e15e>" + f.getNames().getRandom() + "=" - + "<#d665f0>" + f.example() - : "")) - .toString(" ") - : "" - : "")); - } - - return m.removeDuplicates().convert((iff) -> iff.replaceAll("\\Q \\E", " ")).toString("\n"); - } - - public void sendHeader(String name, int overrideLength) { - int len = overrideLength; - int h = name.length() + 2; - String s = Form.repeat(" ", len - h - 4); - String si = Form.repeat("(", 3); - String so = Form.repeat(")", 3); - String sf = "["; - String se = "]"; - - if (name.trim().isEmpty()) { - sendMessageRaw("" + sf + s + "" + s + se); - } else { - sendMessageRaw("" + sf + s + si + " " + name + " " + so + s + se); - } - } - - public void sendHeader(String name) { - sendHeader(name, 44); - } - - public void sendDecreeHelp(VirtualDecreeCommand v) { - sendDecreeHelp(v, 0); - } - - public void sendDecreeHelp(VirtualDecreeCommand v, int page) { - if (!isPlayer()) { - for (VirtualDecreeCommand i : v.getNodes()) { - sendDecreeHelpNode(i); - } - - return; - } - - int m = v.getNodes().size(); - - sendMessageRaw("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); - - if (v.getNodes().isNotEmpty()) { - sendHeader(v.getPath() + (page > 0 ? (" {" + (page + 1) + "}") : "")); - if (isPlayer() && v.getParent() != null) { - sendMessageRaw("Click to go back to <#3299bf>" + Form.capitalize(v.getParent().getName()) + " Help" + "'><#f58571>〈 Back"); - } - - AtomicBoolean next = new AtomicBoolean(false); - for (VirtualDecreeCommand i : paginate(v.getNodes(), 17, page, next)) { - sendDecreeHelpNode(i); - } - - String s = ""; - int l = 75 - (page > 0 ? 10 : 0) - (next.get() ? 10 : 0); - - if (page > 0) { - s += "Click to go back to page " + page + "'>〈 Page " + page + " "; - } - - s += "" + Form.repeat(" ", l) + ""; - - if (next.get()) { - s += " Click to go to back to page " + (page + 2) + "'>Page " + (page + 2) + " ❭"; - } - - sendMessageRaw(s); - } else { - sendMessage(C.RED + "There are no subcommands in this group! Contact support, this is a command design issue!"); - } - } - - public void sendDecreeHelpNode(VirtualDecreeCommand i) { - if (isPlayer() || s instanceof CommandDummy) { - sendMessageRaw(helpCache.computeIfAbsent(i.getPath(), (k) -> { - String newline = "\n"; - - /// Command - // Contains main command & aliases - String realText = i.getPath() + " >" + "<#46826a>⇀ " + i.getName(); - String hoverTitle = i.getNames().copy().reverse().convert((f) -> "<#42ecf5>" + f).toString(", "); - String description = "<#3fe05a>✎ <#6ad97d>" + i.getDescription(); - String usage = "<#bbe03f>✒ <#a8e0a2>"; - String onClick; - if (i.isNode()) { - if (i.getNode().getParameters().isEmpty()) { - usage += "There are no parameters. Click to type command."; - onClick = "suggest_command"; - } else { - usage += "Hover over all of the parameters to learn more."; - onClick = "suggest_command"; - } - } else { - usage += "This is a command category. Click to run."; - onClick = "run_command"; - } - - String suggestion = ""; - String suggestions = ""; - if (i.isNode() && i.getNode().getParameters().isNotEmpty()) { - suggestion += newline + "<#aebef2>✦ <#5ef288>" + i.getParentPath() + " <#42ecf5>" + i.getName() + " " - + i.getNode().getParameters().convert((f) -> "<#d665f0>" + f.example()).toString(" "); - suggestions += newline + "" + pickRandoms(Math.min(i.getNode().getParameters().size() + 1, 5), i); - } - - /// Params - StringBuilder nodes = new StringBuilder(); - if (i.isNode()) { - for (DecreeParameter p : i.getNode().getParameters()) { - - String nTitle = "" + p.getName(); - String nHoverTitle = p.getNames().convert((ff) -> "<#d665f0>" + ff).toString(", "); - String nDescription = "<#3fe05a>✎ <#6ad97d>" + p.getDescription(); - String nUsage; - String fullTitle; - Iris.debug("Contextual: " + p.isContextual() + " / player: " + isPlayer()); - if (p.isContextual() && (isPlayer() || s instanceof CommandDummy)) { - fullTitle = "<#ffcc00>[" + nTitle + "<#ffcc00>] "; - nUsage = "<#ff9900>➱ <#ffcc00>The value may be derived from environment context."; - } else if (p.isRequired()) { - fullTitle = "[" + nTitle + "] "; - nUsage = "<#db4321>⚠ <#faa796>This parameter is required."; - } else if (p.hasDefault()) { - fullTitle = "<#4f4f4f>⊰" + nTitle + "<#4f4f4f>⊱"; - nUsage = "<#2181db>✔ <#78dcf0>Defaults to \"" + p.getParam().defaultValue() + "\" if undefined."; - } else { - fullTitle = "<#4f4f4f>⊰" + nTitle + "<#4f4f4f>⊱"; - nUsage = "<#a73abd>✔ <#78dcf0>This parameter is optional."; - } - String type = "<#cc00ff>✢ <#ff33cc>This parameter is of type " + p.getType().getSimpleName() + "."; - - nodes - .append("") - .append(fullTitle) - .append(""); - } - } else { - nodes = new StringBuilder(" - Category of Commands"); - } - - /// Wrapper - String wrapper = - "" + - "" + - "" + - " " + - nodes; - - return wrapper; - })); - } else { - sendMessage(i.getPath()); - } - } - - public void playSound(Sound sound, float volume, float pitch) { - if (isPlayer()) { - player().playSound(player().getLocation(), sound, volume, pitch); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/chunk/ChunkTickets.java b/core/src/main/java/com/volmit/iris/util/plugin/chunk/ChunkTickets.java deleted file mode 100644 index 320c74436..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/chunk/ChunkTickets.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.volmit.iris.util.plugin.chunk; - -import com.volmit.iris.Iris; -import lombok.NonNull; -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.world.WorldLoadEvent; -import org.bukkit.event.world.WorldUnloadEvent; - -import java.util.HashMap; -import java.util.Map; - -public class ChunkTickets implements Listener { - private final Map holders = new HashMap<>(); - - public ChunkTickets() { - Iris.instance.registerListener(this); - Bukkit.getWorlds().forEach(w -> holders.put(w, new TicketHolder(w))); - } - - public TicketHolder getHolder(@NonNull World world) { - return holders.get(world); - } - - public void addTicket(@NonNull Chunk chunk) { - addTicket(chunk.getWorld(), chunk.getX(), chunk.getZ()); - } - - public void addTicket(@NonNull World world, int x, int z) { - var holder = getHolder(world); - if (holder != null) holder.addTicket(x, z); - } - - public boolean removeTicket(@NonNull Chunk chunk) { - return removeTicket(chunk.getWorld(), chunk.getX(), chunk.getZ()); - } - - public boolean removeTicket(@NonNull World world, int x, int z) { - var holder = getHolder(world); - if (holder != null) return holder.removeTicket(x, z); - return false; - } - - @EventHandler(priority = EventPriority.LOWEST) - public void on(@NonNull WorldLoadEvent event) { - holders.put(event.getWorld(), new TicketHolder(event.getWorld())); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void on(@NonNull WorldUnloadEvent event) { - holders.remove(event.getWorld()); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/plugin/chunk/TicketHolder.java b/core/src/main/java/com/volmit/iris/util/plugin/chunk/TicketHolder.java deleted file mode 100644 index abf521c91..000000000 --- a/core/src/main/java/com/volmit/iris/util/plugin/chunk/TicketHolder.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.volmit.iris.util.plugin.chunk; - -import com.volmit.iris.Iris; -import com.volmit.iris.engine.data.cache.Cache; -import com.volmit.iris.util.collection.KMap; -import lombok.NonNull; -import org.bukkit.Chunk; -import org.bukkit.World; - -public class TicketHolder { - private final World world; - private final KMap tickets = new KMap<>(); - - public TicketHolder(@NonNull World world) { - this.world = world; - } - - public void addTicket(@NonNull Chunk chunk) { - if (chunk.getWorld() != world) return; - addTicket(chunk.getX(), chunk.getZ()); - } - - public void addTicket(int x, int z) { - tickets.compute(Cache.key(x, z), ($, ref) -> { - if (ref == null) { - world.addPluginChunkTicket(x, z, Iris.instance); - return 1L; - } - return ++ref; - }); - } - - public boolean removeTicket(@NonNull Chunk chunk) { - if (chunk.getWorld() != world) return false; - return removeTicket(chunk.getX(), chunk.getZ()); - } - - public boolean removeTicket(int x, int z) { - return tickets.compute(Cache.key(x, z), ($, ref) -> { - if (ref == null) return null; - if (--ref <= 0) { - world.removePluginChunkTicket(x, z, Iris.instance); - return null; - } - return ref; - }) == null; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/profile/LoadBalancer.java b/core/src/main/java/com/volmit/iris/util/profile/LoadBalancer.java deleted file mode 100644 index c92f57a2f..000000000 --- a/core/src/main/java/com/volmit/iris/util/profile/LoadBalancer.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.volmit.iris.util.profile; - - -import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.util.math.M; -import lombok.Getter; -import lombok.Setter; - -import java.util.concurrent.Semaphore; - -@Getter -public class LoadBalancer extends MsptTimings { - private final Semaphore semaphore; - private final int maxPermits; - private final double range; - @Setter - private int minMspt, maxMspt; - private int permits, lastMspt; - private long lastTime = M.ms(); - - public LoadBalancer(Semaphore semaphore, int maxPermits, IrisSettings.MsRange range) { - this(semaphore, maxPermits, range.getMin(), range.getMax()); - } - - public LoadBalancer(Semaphore semaphore, int maxPermits, int minMspt, int maxMspt) { - this.semaphore = semaphore; - this.maxPermits = maxPermits; - this.minMspt = minMspt; - this.maxMspt = maxMspt; - this.range = maxMspt - minMspt; - setName("LoadBalancer"); - start(); - } - - @Override - protected void update(int raw) { - lastTime = M.ms(); - int mspt = raw; - if (mspt < lastMspt) { - int min = (int) Math.max(lastMspt * IrisSettings.get().getUpdater().getChunkLoadSensitivity(), 1); - mspt = Math.max(mspt, min); - } - lastMspt = mspt; - mspt = Math.max(mspt - minMspt, 0); - double percent = mspt / range; - - int target = (int) (maxPermits * percent); - target = Math.min(target, maxPermits - 20); - - int diff = target - permits; - permits = target; - - if (diff == 0) return; - Iris.debug("Adjusting load to %s (%s) permits (%s mspt, %.2f)".formatted(target, diff, raw, percent)); - - if (diff > 0) semaphore.acquireUninterruptibly(diff); - else semaphore.release(Math.abs(diff)); - } - - public void close() { - interrupt(); - semaphore.release(permits); - } - - public void setRange(IrisSettings.MsRange range) { - minMspt = range.getMin(); - maxMspt = range.getMax(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/profile/MsptTimings.java b/core/src/main/java/com/volmit/iris/util/profile/MsptTimings.java deleted file mode 100644 index 2d098e1e7..000000000 --- a/core/src/main/java/com/volmit/iris/util/profile/MsptTimings.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.volmit.iris.util.profile; - -import com.volmit.iris.util.math.M; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.Looper; -import org.bukkit.Bukkit; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; - -public abstract class MsptTimings extends Looper { - private final AtomicInteger currentTick = new AtomicInteger(0); - private int lastTick, lastMspt; - private long lastTime; - private int taskId = -1; - - public MsptTimings() { - setName("MsptTimings"); - setPriority(9); - setDaemon(true); - } - - public static MsptTimings of(Consumer update) { - return new Simple(update); - } - - @Override - protected final long loop() { - if (startTickTask()) - return 200; - - long now = M.ms(); - int tick = currentTick.get(); - int deltaTick = tick - lastTick; - if (deltaTick == 0) - return 200; - lastTick = tick; - int deltaTime = (int) (now - lastTime); - lastTime = now; - int mspt = deltaTime / deltaTick; - mspt -= 50; - mspt = Math.max(mspt, 0); - lastMspt = mspt; - update(mspt); - return 200; - } - - public final int getMspt() { - return lastMspt; - } - - protected abstract void update(int mspt); - - private boolean startTickTask() { - if (taskId != -1 && (Bukkit.getScheduler().isQueued(taskId) || Bukkit.getScheduler().isCurrentlyRunning(taskId))) - return false; - - taskId = J.sr(() -> { - if (isInterrupted()) { - J.csr(taskId); - return; - } - - currentTick.incrementAndGet(); - }, 1); - return taskId != -1; - } - - private static class Simple extends MsptTimings { - private final Consumer update; - - private Simple(Consumer update) { - this.update = update; - start(); - } - - @Override - protected void update(int mspt) { - if (update == null) - return; - update.accept(mspt); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/reflect/KeyedType.java b/core/src/main/java/com/volmit/iris/util/reflect/KeyedType.java deleted file mode 100644 index 1316e7c15..000000000 --- a/core/src/main/java/com/volmit/iris/util/reflect/KeyedType.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.volmit.iris.util.reflect; - -import com.google.gson.Gson; -import com.google.gson.TypeAdapter; -import com.google.gson.reflect.TypeToken; -import com.volmit.iris.util.data.registry.RegistryTypeAdapter; -import com.volmit.iris.util.data.registry.RegistryUtil; -import org.bukkit.Keyed; - -public class KeyedType { - private static final boolean KEYED_ENABLED = Boolean.getBoolean("iris.keyed-types"); - private static final boolean KEYED_LENIENT = Boolean.getBoolean("iris.keyed-lenient"); - - public static String[] values(Class type) { - if (!isKeyed(type)) return new String[0]; - if (!KEYED_ENABLED) return OldEnum.values(type); - return RegistryUtil.lookup(type) - .map() - .keySet() - .stream() - .map(Object::toString) - .toArray(String[]::new); - } - - public static boolean isKeyed(Class type) { - if (KEYED_ENABLED) { - if (KEYED_LENIENT) return !RegistryUtil.lookup(type).isEmpty(); - else return Keyed.class.isAssignableFrom(type); - } else return OldEnum.isOldEnum(type); - } - - @SuppressWarnings("unchecked") - public static TypeAdapter createTypeAdapter(Gson gson, TypeToken type) { - if (!isKeyed(type.getRawType())) return null; - return (TypeAdapter) (KEYED_ENABLED ? RegistryTypeAdapter.of(type.getRawType()) : OldEnum.create(type.getRawType())); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/reflect/OldEnum.java b/core/src/main/java/com/volmit/iris/util/reflect/OldEnum.java deleted file mode 100644 index 7a7500915..000000000 --- a/core/src/main/java/com/volmit/iris/util/reflect/OldEnum.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.volmit.iris.util.reflect; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.Objects; - -@Deprecated(since = "3.7.1") -public class OldEnum { - - private static final Class oldEnum; - private static final MethodHandle name; - - public static boolean exists() { - return oldEnum != null; - } - - public static boolean isOldEnum(Class c) { - return oldEnum != null && oldEnum.isAssignableFrom(c); - } - - public static T valueOf(Class c, String name) { - return valueOf(c, name, name.replace(".", "_")); - } - - public static T valueOf(Class c, String... names) { - for (final String name : names) { - try { - return (T) c.getDeclaredField(name).get(null); - } catch (Throwable ignored) {} - } - return null; - } - - public static String name(Object o) { - try { - return (String) name.invoke(o); - } catch (Throwable e) { - return null; - } - } - - public static String[] values(Class clazz) { - if (!isOldEnum(clazz)) return new String[0]; - return Arrays.stream(clazz.getDeclaredFields()) - .filter(f -> Modifier.isStatic(f.getModifiers())) - .filter(f -> Modifier.isFinal(f.getModifiers())) - .map(f -> { - try { - return name(f.get(null)); - } catch (Throwable ignored) { - return null; - } - }) - .filter(Objects::nonNull) - .toArray(String[]::new); - } - - public static TypeAdapter create(Class type) { - if (!isOldEnum(type)) - return null; - - return new TypeAdapter<>() { - - @Override - public void write(JsonWriter out, T value) throws IOException { - out.value(name(value)); - } - - @Override - public T read(JsonReader in) throws IOException { - return valueOf(type, in.nextString()); - } - }; - } - - static { - Class clazz = null; - MethodHandle method = null; - try { - clazz = Class.forName("org.bukkit.util.OldEnum"); - method = MethodHandles.lookup().findVirtual(clazz, "name", MethodType.methodType(String.class)); - } catch (Throwable ignored) {} - - if (clazz == null || method == null) { - oldEnum = null; - name = null; - } else { - oldEnum = clazz; - name = method; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/reflect/ShadeFix.java b/core/src/main/java/com/volmit/iris/util/reflect/ShadeFix.java deleted file mode 100644 index 204b1f8be..000000000 --- a/core/src/main/java/com/volmit/iris/util/reflect/ShadeFix.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.reflect; - -public class ShadeFix { - public static void fix(Class c) { - c.getCanonicalName(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/reflect/V.java b/core/src/main/java/com/volmit/iris/util/reflect/V.java deleted file mode 100644 index 919092365..000000000 --- a/core/src/main/java/com/volmit/iris/util/reflect/V.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.reflect; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; - -import java.lang.annotation.Annotation; - -public class V { - private final Object o; - private boolean local; - private boolean suppress = false; - - public V(Class c, Object... parameters) { - this.o = Violator.construct(c, parameters); - this.local = true; - } - - public V(Object o) { - this.o = o; - this.local = true; - } - - public V(Object o, boolean local, boolean suppress) { - this(o); - this.local = local; - this.suppress = suppress; - } - - public V(Object o, boolean local) { - this(o); - this.local = local; - } - - public T get(Class t) { - try { - return local ? Violator.getDeclaredAnnotation(o.getClass(), t) : Violator.getAnnotation(o.getClass(), t); - } catch (Throwable e) { - Iris.reportError(e); - if (!suppress) { - e.printStackTrace(); - } - } - - return null; - } - - public T get(Class t, String mn, Class... pars) { - try { - return local ? Violator.getDeclaredAnnotation(Violator.getDeclaredMethod(o.getClass(), mn, pars), t) : Violator.getAnnotation(Violator.getMethod(o.getClass(), mn, pars), t); - } catch (Throwable e) { - Iris.reportError(e); - if (!suppress) { - e.printStackTrace(); - } - } - - return null; - } - - public T get(Class t, String mn) { - try { - return local ? Violator.getDeclaredAnnotation(Violator.getDeclaredField(o.getClass(), mn), t) : Violator.getAnnotation(Violator.getField(o.getClass(), mn), t); - } catch (Throwable e) { - Iris.reportError(e); - if (!suppress) { - e.printStackTrace(); - } - } - - return null; - } - - @SuppressWarnings("unchecked") - public T get(String field) { - try { - return (T) (local ? Violator.getDeclaredField(o.getClass(), field) : Violator.getField(o.getClass(), field)).get(o); - } catch (Throwable e) { - Iris.reportError(e); - if (!suppress) { - e.printStackTrace(); - } - } - - return null; - } - - public Object getSelf() { - return o; - } - - public Object invoke(String method, Object... parameters) { - KList> par = new KList<>(); - - for (Object i : parameters) { - par.add(i.getClass()); - } - - try { - return (local ? Violator.getDeclaredMethod(o.getClass(), method, par.toArray(new Class[0])) : Violator.getMethod(o.getClass(), method, par.toArray(new Class[0]))).invoke(o, parameters); - } catch (Throwable e) { - Iris.reportError(e); - if (!suppress) { - e.printStackTrace(); - } - } - - return null; - } - - public void set(String field, Object value) { - try { - // https://github.com/VolmitSoftware/Mortar/issues/5 - (local ? Violator.getDeclaredField(o.getClass(), field) : Violator.getField(o.getClass(), field)).set(o, value); - } catch (Throwable e) { - Iris.reportError(e); - if (!suppress) { - e.printStackTrace(); - } - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/reflect/Violator.java b/core/src/main/java/com/volmit/iris/util/reflect/Violator.java deleted file mode 100644 index bc29f19a7..000000000 --- a/core/src/main/java/com/volmit/iris/util/reflect/Violator.java +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.reflect; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.concurrent.ConcurrentSkipListMap; - -public class Violator { - protected static final ConcurrentSkipListMap nodes = new ConcurrentSkipListMap<>(); - - private static String id(Object o, Object h) { - if (o instanceof Field) { - return id(((Field) o).getDeclaringClass(), null) + "." + ((Field) o).getName(); - } - - if (o instanceof String) { - return (String) o; - } - - if (o instanceof Class) { - return ((Class) o).getCanonicalName(); - } - - if (o instanceof Constructor co) { - - StringBuilder mx = new StringBuilder(); - - for (Class i : co.getParameterTypes()) { - mx.append(",").append(i.getCanonicalName()); - } - - mx = new StringBuilder(mx.length() >= 1 ? mx.substring(1) : mx.toString()); - - return id(co.getDeclaringClass(), null) + "(" + mx + ")"; - } - - if (o instanceof Method) { - StringBuilder mx = new StringBuilder(); - - for (Class i : ((Method) o).getParameterTypes()) { - mx.append(",").append(i.getCanonicalName()); - } - - mx = new StringBuilder(mx.length() >= 1 ? mx.substring(1) : mx.toString()); - - return id(((Method) o).getDeclaringClass(), null) + "." + ((Method) o).getName() + "(" + mx + ")"; - } - - if (o instanceof Annotation a) { - return "@" + a.annotationType().getCanonicalName() + "[" + id(h, null) + "]"; - } - - return o.hashCode() + o.toString(); - } - - private static void p(String n, Object o) { - nodes.put(n, o); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private static boolean h(String n) { - return nodes.containsKey(n); - } - - private static Object g(String n) { - return nodes.get(n); - } - - public static Constructor getConstructor(Class c, Class... params) throws NoSuchMethodException, SecurityException { - StringBuilder mx = new StringBuilder(); - - for (Class i : params) { - mx.append(",").append(i.getCanonicalName()); - } - - mx = new StringBuilder(mx.length() >= 1 ? mx.substring(1) : mx.toString()); - - if (!h(id(c, null) + "(" + mx + ")")) { - Constructor co = c.getConstructor(params); - co.setAccessible(true); - p(id(co, null), co); - } - - return (Constructor) g(id(c, null) + "(" + mx + ")"); - } - - @SuppressWarnings("rawtypes") - public static Field getField(Class c, String name) throws Throwable { - if (!h(id(c, null) + "." + name)) { - try { - Field f = c.getField(name); - f.setAccessible(true); - p(id(c, null) + "." + name, f); - } catch (NoSuchFieldException e) { - Iris.reportError(e); - Class s = c.getSuperclass(); - if (null == s) { - throw e; - } - Field f = s.getField(name); - f.setAccessible(true); - p(id(c, null) + "." + name, f); - } - } - - return (Field) g(id(c, null) + "." + name); - } - - @SuppressWarnings("rawtypes") - public static Field getDeclaredField(Class c, String name) throws Throwable { - if (!h(id(c, null) + "." + name)) { - try { - Field f = c.getDeclaredField(name); - f.setAccessible(true); - p(id(c, null) + "." + name, f); - } catch (NoSuchFieldException e) { - Iris.reportError(e); - Class s = c.getSuperclass(); - if (null == s) { - throw e; - } - Field f = s.getDeclaredField(name); - f.setAccessible(true); - p(id(c, null) + "." + name, f); - } - } - - return (Field) g(id(c, null) + "." + name); - } - - public static Method getMethod(Class c, String name, Class... pars) throws Throwable { - String iv = ""; - StringBuilder mx = new StringBuilder(); - - for (Class i : pars) { - mx.append(",").append(i.getCanonicalName()); - } - - mx = new StringBuilder(mx.length() >= 1 ? mx.substring(1) : mx.toString()); - iv = id(c, null) + "." + name + "(" + mx + ")"; - - if (!h(iv)) { - Method f = c.getMethod(name, pars); - f.setAccessible(true); - p(iv, f); - } - - return (Method) g(iv); - } - - @SuppressWarnings("unchecked") - public static T construct(Class c, Object... parameters) { - KList> cv = new KList<>(); - - for (Object i : parameters) { - cv.add(i.getClass()); - } - - try { - Constructor co = getConstructor(c, cv.toArray(new Class[0])); - return (T) co.newInstance(parameters); - } catch (Exception e) { - Iris.reportError(e); - e.printStackTrace(); - } - - return null; - } - - public static Method getDeclaredMethod(Class c, String name, Class... pars) throws Throwable { - String iv = ""; - StringBuilder mx = new StringBuilder(); - - for (Class i : pars) { - mx.append(",").append(i.getCanonicalName()); - } - - mx = new StringBuilder(mx.length() >= 1 ? mx.substring(1) : mx.toString()); - iv = id(c, null) + "." + name + "(" + mx + ")"; - - if (!h(iv)) { - Method f = c.getDeclaredMethod(name, pars); - f.setAccessible(true); - p(iv, f); - } - - return (Method) g(iv); - } - - @SuppressWarnings("unchecked") - public static T getAnnotation(Class c, Class a) { - if (!h("@" + a.getCanonicalName() + "[" + c.getCanonicalName() + "]")) { - T f = c.getAnnotation(a); - p(id(f, c), f); - } - - return (T) g("@" + a.getCanonicalName() + "[" + c.getCanonicalName() + "]"); - } - - @SuppressWarnings("unchecked") - public static T getDeclaredAnnotation(Class c, Class a) { - if (!h("@" + a.getCanonicalName() + "[" + c.getCanonicalName() + "]")) { - T f = c.getDeclaredAnnotation(a); - p(id(f, c), f); - } - - return (T) g("@" + a.getCanonicalName() + "[" + c.getCanonicalName() + "]"); - } - - @SuppressWarnings("unchecked") - public static T getAnnotation(Field c, Class a) { - if (!h("@" + a.getCanonicalName() + "[" + id(c, null) + "]")) { - T f = c.getAnnotation(a); - p(id(f, c), f); - } - - return (T) g("@" + a.getCanonicalName() + "[" + id(c, null) + "]"); - } - - @SuppressWarnings("unchecked") - public static T getDeclaredAnnotation(Field c, Class a) { - if (!h("@" + a.getCanonicalName() + "[" + id(c, null) + "]")) { - T f = c.getDeclaredAnnotation(a); - p(id(f, c), f); - } - - return (T) g("@" + a.getCanonicalName() + "[" + id(c, null) + "]"); - } - - @SuppressWarnings("unchecked") - public static T getAnnotation(Method c, Class a) { - if (!h("@" + a.getCanonicalName() + "[" + id(c, null) + "]")) { - T f = c.getAnnotation(a); - p(id(f, c), f); - } - - return (T) g("@" + a.getCanonicalName() + "[" + id(c, null) + "]"); - } - - @SuppressWarnings("unchecked") - public static T getDeclaredAnnotation(Method c, Class a) { - if (!h("@" + a.getCanonicalName() + "[" + id(c, null) + "]")) { - T f = c.getDeclaredAnnotation(a); - p(id(f, c), f); - - Iris.debug("Set as " + id(f, c) + " as " + ("@" + a.getCanonicalName() + "[" + id(c, null) + "]")); - } - - return (T) g("@" + a.getCanonicalName() + "[" + id(c, null) + "]"); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/reflect/WrappedField.java b/core/src/main/java/com/volmit/iris/util/reflect/WrappedField.java deleted file mode 100644 index 0aad874ad..000000000 --- a/core/src/main/java/com/volmit/iris/util/reflect/WrappedField.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.volmit.iris.util.reflect; - -import com.volmit.iris.Iris; - -import java.lang.reflect.Field; - -public class WrappedField { - - private final Field field; - - public WrappedField(Class origin, String methodName) { - Field f = null; - try { - f = origin.getDeclaredField(methodName); - f.setAccessible(true); - } catch (NoSuchFieldException e) { - Iris.error("Failed to created WrappedField %s#%s: %s%s", origin.getSimpleName(), methodName, e.getClass().getSimpleName(), e.getMessage().equals("") ? "" : " | " + e.getMessage()); - } - this.field = f; - } - - public T get() { - return get(null); - } - - public T get(C instance) { - if (field == null) { - return null; - } - - try { - return (T) field.get(instance); - } catch (IllegalAccessException e) { - Iris.error("Failed to get WrappedField %s#%s: %s%s", field.getDeclaringClass().getSimpleName(), field.getName(), e.getClass().getSimpleName(), e.getMessage().equals("") ? "" : " | " + e.getMessage()); - return null; - } - } - - public void set(T value) throws IllegalAccessException { - set(null, value); - } - - public void set(C instance, T value) throws IllegalAccessException { - if (field == null) { - return; - } - - field.set(instance, value); - } - - public boolean hasFailed() { - return field == null; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/reflect/WrappedReturningMethod.java b/core/src/main/java/com/volmit/iris/util/reflect/WrappedReturningMethod.java deleted file mode 100644 index 338044781..000000000 --- a/core/src/main/java/com/volmit/iris/util/reflect/WrappedReturningMethod.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.volmit.iris.util.reflect; - -import com.volmit.iris.Iris; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public final class WrappedReturningMethod { - - private final Method method; - - public WrappedReturningMethod(Class origin, String methodName, Class... paramTypes) { - Method m = null; - try { - m = origin.getDeclaredMethod(methodName, paramTypes); - m.setAccessible(true); - } catch (NoSuchMethodException e) { - Iris.error("Failed to created WrappedMethod %s#%s: %s%s", origin.getSimpleName(), methodName, e.getClass().getSimpleName(), e.getMessage().equals("") ? "" : " | " + e.getMessage()); - } - this.method = m; - } - - public R invoke(Object... args) { - return invoke(null, args); - } - - public R invoke(C instance, Object... args) { - if (method == null) { - return null; - } - - try { - return (R) method.invoke(instance, args); - } catch (InvocationTargetException | IllegalAccessException e) { - Iris.error("Failed to invoke WrappedMethod %s#%s: %s%s", method.getDeclaringClass().getSimpleName(), method.getName(), e.getClass().getSimpleName(), e.getMessage().equals("") ? "" : " | " + e.getMessage()); - return null; - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/AR.java b/core/src/main/java/com/volmit/iris/util/scheduling/AR.java deleted file mode 100644 index b5fda2c52..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/AR.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.util.plugin.CancellableTask; - -public abstract class AR implements Runnable, CancellableTask { - private int id = 0; - - public AR() { - this(0); - } - - public AR(int interval) { - id = J.ar(this, interval); - } - - @Override - public void cancel() { - J.car(id); - } - - public int getId() { - return id; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/Callback.java b/core/src/main/java/com/volmit/iris/util/scheduling/Callback.java deleted file mode 100644 index 2979e2f5c..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/Callback.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -/** - * Callback for async workers - * - * @param the type of object to be returned in the runnable - * @author cyberpwn - */ -@FunctionalInterface -public interface Callback { - /** - * Called when the callback calls back... - * - * @param t the object to be called back - */ - void run(T t); -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/ChronoLatch.java b/core/src/main/java/com/volmit/iris/util/scheduling/ChronoLatch.java deleted file mode 100644 index 096ebcd91..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/ChronoLatch.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -public class ChronoLatch { - private final long interval; - private long since; - - public ChronoLatch(long interval, boolean openedAtStart) { - this.interval = interval; - since = System.currentTimeMillis() - (openedAtStart ? interval * 2 : 0); - } - - public ChronoLatch(long interval) { - this(interval, true); - } - - public void flipDown() { - since = System.currentTimeMillis(); - } - - public boolean couldFlip() { - return System.currentTimeMillis() - since > interval; - } - - public boolean flip() { - if (System.currentTimeMillis() - since > interval) { - since = System.currentTimeMillis(); - return true; - } - - return false; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/Contained.java b/core/src/main/java/com/volmit/iris/util/scheduling/Contained.java deleted file mode 100644 index a010dd1f0..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/Contained.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import java.util.function.Function; - -public class Contained { - private T t; - - public void mod(Function x) { - set(x.apply(t)); - } - - public T get() { - return t; - } - - public void set(T t) { - this.t = t; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/GroupedExecutor.java b/core/src/main/java/com/volmit/iris/util/scheduling/GroupedExecutor.java deleted file mode 100644 index eb86f5e2d..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/GroupedExecutor.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.function.NastyRunnable; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory; -import java.util.concurrent.ForkJoinWorkerThread; - -public class GroupedExecutor { - private final ExecutorService service; - private final KMap mirror; - private int xc; - - public GroupedExecutor(int threadLimit, int priority, String name) { - xc = 1; - mirror = new KMap<>(); - - if (threadLimit == 1) { - service = Executors.newSingleThreadExecutor((r) -> - { - Thread t = new Thread(r); - t.setName(name); - t.setPriority(priority); - - return t; - }); - } else if (threadLimit > 1) { - final ForkJoinWorkerThreadFactory factory = pool -> { - final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); - worker.setName(name + " " + xc++); - worker.setPriority(priority); - return worker; - }; - - service = new ForkJoinPool(threadLimit, factory, null, false); - } else { - service = Executors.newCachedThreadPool((r) -> - { - Thread t = new Thread(r); - t.setName(name + " " + xc++); - t.setPriority(priority); - - return t; - }); - } - } - - public void waitFor(String g) { - if (g == null) { - return; - } - - if (!mirror.containsKey(g)) { - return; - } - - while (true) { - if (mirror.get(g) == 0) { - break; - } - } - } - - public void queue(String q, NastyRunnable r) { - mirror.compute(q, (k, v) -> k == null || v == null ? 1 : v + 1); - service.execute(() -> - { - try { - r.run(); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - - mirror.computeIfPresent(q, (k, v) -> v - 1); - }); - } - - public void close() { - J.a(() -> - { - J.sleep(100); - service.shutdown(); - }); - } - - public void closeNow() { - service.shutdown(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/IrisLock.java b/core/src/main/java/com/volmit/iris/util/scheduling/IrisLock.java deleted file mode 100644 index ab0fe0c63..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/IrisLock.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.Iris; -import lombok.Data; -import lombok.experimental.Accessors; - -import java.util.concurrent.locks.ReentrantLock; - -@Data -@Accessors( - chain = true -) -public class IrisLock { - private transient final ReentrantLock lock; - private transient final String name; - private transient boolean disabled = false; - - public IrisLock(String name) { - this.name = name; - lock = new ReentrantLock(false); - } - - public void lock() { - if (disabled) { - return; - } - - lock.lock(); - } - - public void unlock() { - if (disabled) { - return; - } - try { - lock.unlock(); - } catch (Throwable e) { - Iris.reportError(e); - - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/J.java b/core/src/main/java/com/volmit/iris/util/scheduling/J.java deleted file mode 100644 index 550b94092..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/J.java +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.function.NastyFunction; -import com.volmit.iris.util.function.NastyFuture; -import com.volmit.iris.util.function.NastyRunnable; -import com.volmit.iris.util.function.NastySupplier; -import com.volmit.iris.util.math.FinalInteger; -import com.volmit.iris.util.parallel.MultiBurst; -import org.bukkit.Bukkit; - -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -@SuppressWarnings("ALL") -public class J { - private static int tid = 0; - private static KList afterStartup = new KList<>(); - private static KList afterStartupAsync = new KList<>(); - private static boolean started = false; - - public static void dofor(int a, Function c, int ch, Consumer d) { - for (int i = a; c.apply(i); i += ch) { - c.apply(i); - } - } - - public static boolean doif(Supplier c, Runnable g) { - try { - if (c.get()) { - g.run(); - return true; - } - } catch (NullPointerException e) { - Iris.reportError(e); - // TODO: Fix this because this is just a suppression for an NPE on g - return false; - } - - return false; - } - - public static void arun(Runnable a) { - MultiBurst.burst.lazy(() -> { - try { - a.run(); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Failed to run async task"); - e.printStackTrace(); - } - }); - } - - public static void a(Runnable a) { - MultiBurst.burst.lazy(() -> { - try { - a.run(); - } catch (Throwable e) { - Iris.reportError(e); - Iris.error("Failed to run async task"); - e.printStackTrace(); - } - }); - } - - public static void aBukkit(Runnable a) { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - return; - } - Bukkit.getScheduler().scheduleAsyncDelayedTask(Iris.instance, a); - } - - public static Future a(Callable a) { - return MultiBurst.burst.lazySubmit(a); - } - - public static void attemptAsync(NastyRunnable r) { - J.a(() -> J.attempt(r)); - } - - public static R attemptResult(NastyFuture r, R onError) { - try { - return r.run(); - } catch (Throwable e) { - Iris.reportError(e); - - } - - return onError; - } - - public static R attemptFunction(NastyFunction r, T param, R onError) { - try { - return r.run(param); - } catch (Throwable e) { - Iris.reportError(e); - - } - - return onError; - } - - public static boolean sleep(long ms) { - return J.attempt(() -> Thread.sleep(ms)); - } - - public static boolean attempt(NastyRunnable r) { - return attemptCatch(r) == null; - } - - public static T attemptResult(NastySupplier r) { - try { - return r.get(); - } catch (Throwable e) { - return null; - } - } - - public static Throwable attemptCatch(NastyRunnable r) { - try { - r.run(); - } catch (Throwable e) { - return e; - } - - return null; - } - - public static T attempt(Supplier t, T i) { - try { - return t.get(); - } catch (Throwable e) { - Iris.reportError(e); - return i; - } - } - - /** - * Dont call this unless you know what you are doing! - */ - public static void executeAfterStartupQueue() { - if (started) { - return; - } - - started = true; - - for (Runnable r : afterStartup) { - s(r); - } - - for (Runnable r : afterStartupAsync) { - a(r); - } - - afterStartup = null; - afterStartupAsync = null; - } - - /** - * Schedule a sync task to be run right after startup. If the server has already - * started ticking, it will simply run it in a sync task. - *

- * If you dont know if you should queue this or not, do so, it's pretty - * forgiving. - * - * @param r the runnable - */ - public static void ass(Runnable r) { - if (started) { - s(r); - } else { - afterStartup.add(r); - } - } - - /** - * Schedule an async task to be run right after startup. If the server has - * already started ticking, it will simply run it in an async task. - *

- * If you dont know if you should queue this or not, do so, it's pretty - * forgiving. - * - * @param r the runnable - */ - public static void asa(Runnable r) { - if (started) { - a(r); - } else { - afterStartupAsync.add(r); - } - } - - /** - * Queue a sync task - * - * @param r the runnable - */ - public static void s(Runnable r) { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - return; - } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r); - } - - public static CompletableFuture sfut(Runnable r) { - CompletableFuture f = new CompletableFuture(); - - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - return null; - } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> { - r.run(); - f.complete(null); - }); - return f; - } - - public static CompletableFuture sfut(Supplier r) { - CompletableFuture f = new CompletableFuture<>(); - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - return null; - } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> { - try { - f.complete(r.get()); - } catch (Throwable e) { - f.completeExceptionally(e); - } - }); - return f; - } - - public static CompletableFuture sfut(Runnable r, int delay) { - CompletableFuture f = new CompletableFuture(); - - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - return null; - } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> { - r.run(); - f.complete(null); - }, delay); - return f; - } - - public static CompletableFuture afut(Runnable r) { - CompletableFuture f = new CompletableFuture(); - J.a(() -> { - r.run(); - f.complete(null); - }); - return f; - } - - /** - * Queue a sync task - * - * @param r the runnable - * @param delay the delay to wait in ticks before running - */ - public static void s(Runnable r, int delay) { - try { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - return; - } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r, delay); - } catch (Throwable e) { - Iris.reportError(e); - } - } - - /** - * Cancel a sync repeating task - * - * @param id the task id - */ - public static void csr(int id) { - Bukkit.getScheduler().cancelTask(id); - } - - /** - * Start a sync repeating task - * - * @param r the runnable - * @param interval the interval - * @return the task id - */ - public static int sr(Runnable r, int interval) { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - return -1; - } - return Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, r, 0, interval); - } - - /** - * Start a sync repeating task for a limited amount of ticks - * - * @param r the runnable - * @param interval the interval in ticks - * @param intervals the maximum amount of intervals to run - */ - public static void sr(Runnable r, int interval, int intervals) { - FinalInteger fi = new FinalInteger(0); - - new SR() { - @Override - public void run() { - fi.add(1); - r.run(); - - if (fi.get() >= intervals) { - cancel(); - } - } - }; - } - - /** - * Call an async task dealyed - * - * @param r the runnable - * @param delay the delay to wait before running - */ - @SuppressWarnings("deprecation") - public static void a(Runnable r, int delay) { - if (Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - Bukkit.getScheduler().scheduleAsyncDelayedTask(Iris.instance, r, delay); - } - } - - /** - * Cancel an async repeat task - * - * @param id the id - */ - public static void car(int id) { - Bukkit.getScheduler().cancelTask(id); - } - - /** - * Start an async repeat task - * - * @param r the runnable - * @param interval the interval in ticks - * @return the task id - */ - @SuppressWarnings("deprecation") - public static int ar(Runnable r, int interval) { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - return -1; - } - return Bukkit.getScheduler().scheduleAsyncRepeatingTask(Iris.instance, r, 0, interval); - } - - /** - * Start an async repeating task for a limited time - * - * @param r the runnable - * @param interval the interval - * @param intervals the intervals to run - */ - public static void ar(Runnable r, int interval, int intervals) { - FinalInteger fi = new FinalInteger(0); - - new AR() { - @Override - public void run() { - fi.add(1); - r.run(); - - if (fi.get() >= intervals) { - cancel(); - } - } - }; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/Looper.java b/core/src/main/java/com/volmit/iris/util/scheduling/Looper.java deleted file mode 100644 index 72ba9ff03..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/Looper.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.service.PreservationSVC; - -public abstract class Looper extends Thread { - @SuppressWarnings("BusyWait") - public void run() { - Iris.service(PreservationSVC.class).register(this); - while (!interrupted()) { - try { - long m = loop(); - - if (m < 0) { - break; - } - - //noinspection BusyWait - Thread.sleep(m); - } catch (InterruptedException e) { - break; - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - Iris.debug("Iris Thread " + getName() + " Shutdown."); - } - - protected abstract long loop(); -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/O.java b/core/src/main/java/com/volmit/iris/util/scheduling/O.java deleted file mode 100644 index 3c97d1c99..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/O.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.util.collection.KList; - -public class O implements Observable { - private T t = null; - private KList> observers; - - @Override - public T get() { - return t; - } - - @Override - public O set(T t) { - this.t = t; - - if (observers != null && observers.hasElements()) { - observers.forEach((o) -> o.onChanged(t, t)); - } - - return this; - } - - @Override - public boolean has() { - return t != null; - } - - @Override - public O clearObservers() { - observers.clear(); - return this; - } - - @Override - public O observe(Observer t) { - if (observers == null) { - observers = new KList<>(); - } - - observers.add(t); - - return this; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/Observable.java b/core/src/main/java/com/volmit/iris/util/scheduling/Observable.java deleted file mode 100644 index 3fbd672cc..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/Observable.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -public interface Observable { - T get(); - - Observable set(T t); - - boolean has(); - - Observable clearObservers(); - - Observable observe(Observer t); -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/Observer.java b/core/src/main/java/com/volmit/iris/util/scheduling/Observer.java deleted file mode 100644 index 49a378a54..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/Observer.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -@FunctionalInterface -public interface Observer { - void onChanged(T from, T to); -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/PrecisionStopwatch.java b/core/src/main/java/com/volmit/iris/util/scheduling/PrecisionStopwatch.java deleted file mode 100644 index 4fa5b3b3c..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/PrecisionStopwatch.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -public class PrecisionStopwatch { - private long nanos; - private long startNano; - private long millis; - private long startMillis; - private double time; - private boolean profiling; - - public PrecisionStopwatch() { - reset(); - profiling = false; - } - - public static PrecisionStopwatch start() { - PrecisionStopwatch p = new PrecisionStopwatch(); - p.begin(); - - return p; - } - - public void begin() { - profiling = true; - startNano = System.nanoTime(); - startMillis = System.currentTimeMillis(); - } - - public void end() { - if (!profiling) { - return; - } - - profiling = false; - nanos = System.nanoTime() - startNano; - millis = System.currentTimeMillis() - startMillis; - time = (double) nanos / 1000000.0; - time = (double) millis - time > 1.01 ? millis : time; - } - - public void reset() { - nanos = -1; - millis = -1; - startNano = -1; - startMillis = -1; - time = -0; - profiling = false; - } - - public double getTicks() { - return getMilliseconds() / 50.0; - } - - public double getSeconds() { - return getMilliseconds() / 1000.0; - } - - public double getMinutes() { - return getSeconds() / 60.0; - } - - public double getHours() { - return getMinutes() / 60.0; - } - - public double getMilliseconds() { - nanos = System.nanoTime() - startNano; - millis = System.currentTimeMillis() - startMillis; - time = (double) nanos / 1000000.0; - time = (double) millis - time > 1.01 ? millis : time; - return time; - } - - public long getNanoseconds() { - return (long) (time * 1000000.0); - } - - public long getNanos() { - return nanos; - } - - public long getStartNano() { - return startNano; - } - - public long getMillis() { - return millis; - } - - public long getStartMillis() { - return startMillis; - } - - public double getTime() { - return time; - } - - public boolean isProfiling() { - return profiling; - } - - public void rewind(long l) { - startMillis -= l; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/Queue.java b/core/src/main/java/com/volmit/iris/util/scheduling/Queue.java deleted file mode 100644 index c25bb715d..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/Queue.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.util.collection.KList; - -@SuppressWarnings("ALL") -public interface Queue { - static Queue create(KList t) { - return new ShurikenQueue().queue(t); - } - - @SuppressWarnings("unchecked") - static Queue create(T... t) { - return new ShurikenQueue().queue(new KList().add(t)); - } - - Queue queue(T t); - - Queue queue(KList t); - - boolean hasNext(int amt); - - boolean hasNext(); - - T next(); - - KList next(int amt); - - Queue clear(); - - int size(); - - boolean contains(T p); -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/QueueExecutor.java b/core/src/main/java/com/volmit/iris/util/scheduling/QueueExecutor.java deleted file mode 100644 index 0bb094b58..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/QueueExecutor.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.Iris; - -public class QueueExecutor extends Looper { - private final Queue queue; - private boolean shutdown; - - public QueueExecutor() { - queue = new ShurikenQueue<>(); - shutdown = false; - } - - public Queue queue() { - return queue; - } - - @Override - protected long loop() { - while (queue.hasNext()) { - try { - queue.next().run(); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - if (shutdown && !queue.hasNext()) { - interrupt(); - return -1; - } - - return Math.max(500, (long) getRunTime() * 10); - } - - public double getRunTime() { - return 0; - } - - public void shutdown() { - shutdown = true; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/S.java b/core/src/main/java/com/volmit/iris/util/scheduling/S.java deleted file mode 100644 index a6d37809f..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/S.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -public abstract class S implements Runnable { - public S() { - J.s(this); - } - - public S(int delay) { - J.s(this, delay); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/SR.java b/core/src/main/java/com/volmit/iris/util/scheduling/SR.java deleted file mode 100644 index bbb41cd85..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/SR.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.util.plugin.CancellableTask; - -public abstract class SR implements Runnable, CancellableTask { - private int id = 0; - - public SR() { - this(0); - } - - public SR(int interval) { - id = J.sr(this, interval); - } - - @Override - public void cancel() { - J.csr(id); - } - - public int getId() { - return id; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/ShurikenQueue.java b/core/src/main/java/com/volmit/iris/util/scheduling/ShurikenQueue.java deleted file mode 100644 index 996574bef..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/ShurikenQueue.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.util.collection.KList; - -public class ShurikenQueue implements Queue { - private KList queue; - private boolean randomPop; - private boolean reversePop; - - public ShurikenQueue() { - clear(); - } - - public ShurikenQueue responsiveMode() { - reversePop = true; - return this; - } - - public ShurikenQueue randomMode() { - randomPop = true; - return this; - } - - @Override - public ShurikenQueue queue(T t) { - queue.add(t); - return this; - } - - @Override - public ShurikenQueue queue(KList t) { - queue.add(t); - return this; - } - - @Override - public boolean hasNext(int amt) { - return queue.size() >= amt; - } - - @Override - public boolean hasNext() { - return !queue.isEmpty(); - } - - @Override - public T next() { - return reversePop ? queue.popLast() : randomPop ? queue.popRandom() : queue.pop(); - } - - @Override - public KList next(int amt) { - KList t = new KList<>(); - - for (int i = 0; i < amt; i++) { - if (!hasNext()) { - break; - } - - t.add(next()); - } - - return t; - } - - @Override - public ShurikenQueue clear() { - queue = new KList<>(); - return this; - } - - @Override - public int size() { - return queue.size(); - } - - @Override - public boolean contains(T p) { - return queue.contains(p); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/Switch.java b/core/src/main/java/com/volmit/iris/util/scheduling/Switch.java deleted file mode 100644 index a4e25e8a2..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/Switch.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -public class Switch { - private volatile boolean b; - - /** - * Defaulted off - */ - public Switch() { - b = false; - } - - public void flip() { - b = true; - } - - public boolean isFlipped() { - return b; - } - - public void reset() { - b = false; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/TaskExecutor.java b/core/src/main/java/com/volmit/iris/util/scheduling/TaskExecutor.java deleted file mode 100644 index a890a10a2..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/TaskExecutor.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.function.NastyRunnable; -import com.volmit.iris.util.math.M; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory; -import java.util.concurrent.ForkJoinWorkerThread; - -public class TaskExecutor { - private final ExecutorService service; - private int xc; - - public TaskExecutor(int threadLimit, int priority, String name) { - xc = 1; - - if (threadLimit == 1) { - service = Executors.newSingleThreadExecutor((r) -> - { - Thread t = new Thread(r); - t.setName(name); - t.setPriority(priority); - - return t; - }); - } else if (threadLimit > 1) { - final ForkJoinWorkerThreadFactory factory = pool -> { - final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); - worker.setName(name + " " + xc++); - worker.setPriority(priority); - return worker; - }; - - service = new ForkJoinPool(threadLimit, factory, null, false); - } else { - service = Executors.newCachedThreadPool((r) -> - { - Thread t = new Thread(r); - t.setName(name + " " + xc++); - t.setPriority(priority); - - return t; - }); - } - } - - public TaskGroup startWork() { - return new TaskGroup(this); - } - - public void close() { - J.a(() -> - { - J.sleep(10000); - service.shutdown(); - }); - } - - public void closeNow() { - service.shutdown(); - } - - public enum TaskState { - QUEUED, - RUNNING, - COMPLETED, - FAILED - } - - public static class TaskGroup { - private final KList tasks; - private final TaskExecutor e; - - public TaskGroup(TaskExecutor e) { - tasks = new KList<>(); - this.e = e; - } - - public TaskGroup queue(NastyRunnable... r) { - for (NastyRunnable i : r) { - tasks.add(new AssignedTask(i)); - } - - return this; - } - - public TaskGroup queue(KList r) { - for (NastyRunnable i : r) { - tasks.add(new AssignedTask(i)); - } - - return this; - } - - public TaskResult execute() { - double timeElapsed = 0; - int tasksExecuted = 0; - int tasksFailed = 0; - int tasksCompleted = 0; - tasks.forEach((t) -> t.go(e)); - long msv = M.ns(); - - waiting: - while (true) { - try { - //noinspection BusyWait - Thread.sleep(0); - } catch (InterruptedException ignored) { - - } - - for (AssignedTask i : tasks) { - if (i.state.equals(TaskState.QUEUED) || i.state.equals(TaskState.RUNNING)) { - continue waiting; - } - } - - timeElapsed = (double) (M.ns() - msv) / 1000000D; - - for (AssignedTask i : tasks) { - if (i.state.equals(TaskState.COMPLETED)) { - tasksCompleted++; - } else { - tasksFailed++; - } - - tasksExecuted++; - } - - break; - } - - return new TaskResult(timeElapsed, tasksExecuted, tasksFailed, tasksCompleted); - } - } - - @SuppressWarnings("ClassCanBeRecord") - @ToString - public static class TaskResult { - public final double timeElapsed; - public final int tasksExecuted; - public final int tasksFailed; - public final int tasksCompleted; - - public TaskResult(double timeElapsed, int tasksExecuted, int tasksFailed, int tasksCompleted) { - this.timeElapsed = timeElapsed; - this.tasksExecuted = tasksExecuted; - this.tasksFailed = tasksFailed; - this.tasksCompleted = tasksCompleted; - } - } - - public static class AssignedTask { - @Getter - private final NastyRunnable task; - @Getter - @Setter - private TaskState state; - - public AssignedTask(NastyRunnable task) { - this.task = task; - state = TaskState.QUEUED; - } - - public void go(TaskExecutor e) { - e.service.execute(() -> - { - state = TaskState.RUNNING; - try { - task.run(); - state = TaskState.COMPLETED; - } catch (Throwable ex) { - Iris.reportError(ex); - ex.printStackTrace(); - Iris.reportError(ex); - state = TaskState.FAILED; - } - }); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/ThreadMonitor.java b/core/src/main/java/com/volmit/iris/util/scheduling/ThreadMonitor.java deleted file mode 100644 index 6d167b9c1..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/ThreadMonitor.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.RollingSequence; - -/** - * Not particularly efficient or perfectly accurate but is great at fast thread - * switching detection - * - * @author dan - */ -public class ThreadMonitor extends Thread { - private final Thread monitor; - private final ChronoLatch cl; - private final RollingSequence sq = new RollingSequence(3); - int cycles = 0; - private boolean running; - private State lastState; - private PrecisionStopwatch st; - - private ThreadMonitor(Thread monitor) { - running = true; - st = PrecisionStopwatch.start(); - this.monitor = monitor; - lastState = State.NEW; - cl = new ChronoLatch(1000); - start(); - } - - public static ThreadMonitor bind(Thread monitor) { - return new ThreadMonitor(monitor); - } - - public void run() { - while (running) { - try { - //noinspection BusyWait - Thread.sleep(0); - State s = monitor.getState(); - if (lastState != s) { - cycles++; - pushState(s); - } - - lastState = s; - - if (cl.flip()) { - Iris.info("Cycles: " + Form.f(cycles) + " (" + Form.duration(sq.getAverage(), 2) + ")"); - } - } catch (Throwable e) { - Iris.reportError(e); - running = false; - break; - } - } - } - - public void pushState(State s) { - if (s != State.RUNNABLE) { - if (st != null) { - sq.put(st.getMilliseconds()); - } - } else { - - st = PrecisionStopwatch.start(); - } - } - - public void unbind() { - running = false; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/Wrapper.java b/core/src/main/java/com/volmit/iris/util/scheduling/Wrapper.java deleted file mode 100644 index 2e15751ca..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/Wrapper.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling; - -public class Wrapper { - private T t; - - public Wrapper(T t) { - set(t); - } - - public void set(T t) { - this.t = t; - } - - public T get() { - return t; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((t == null) ? 0 : t.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof Wrapper other)) { - return false; - } - - if (t == null) { - return other.t == null; - } else return t.equals(other.t); - } - - @Override - public String toString() { - if (t != null) { - return get().toString(); - } - - return super.toString() + " (null)"; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/Job.java b/core/src/main/java/com/volmit/iris/util/scheduling/jobs/Job.java deleted file mode 100644 index a378770b5..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/Job.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.scheduling.jobs; - -import com.volmit.iris.util.format.C; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.plugin.VolmitSender; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; - -import java.util.concurrent.CompletableFuture; - -public interface Job { - String getName(); - - void execute(); - - void completeWork(); - - int getTotalWork(); - - default int getWorkRemaining() { - return getTotalWork() - getWorkCompleted(); - } - - int getWorkCompleted(); - - default String getProgressString() { - return Form.pc(getProgress(), 0); - } - - default double getProgress() { - return (double) getWorkCompleted() / (double) getTotalWork(); - } - - - default void execute(VolmitSender sender) { - execute(sender, () -> { - }); - } - - - default void execute(VolmitSender sender, Runnable whenComplete) { - execute(sender, false, whenComplete); - } - - default void execute(VolmitSender sender, boolean silentMsg, Runnable whenComplete) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - CompletableFuture f = J.afut(this::execute); - int c = J.ar(() -> { - if (sender.isPlayer()) { - sender.sendProgress(getProgress(), getName()); - } else { - sender.sendMessage(getName() + ": " + getProgressString()); - } - }, sender.isPlayer() ? 0 : 20); - f.whenComplete((fs, ff) -> { - J.car(c); - if (!silentMsg) { - sender.sendMessage(C.AQUA + "Completed " + getName() + " in " + Form.duration(p.getMilliseconds(), 1)); - } - whenComplete.run(); - }); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/ParallelRadiusJob.java b/core/src/main/java/com/volmit/iris/util/scheduling/jobs/ParallelRadiusJob.java deleted file mode 100644 index 71df2a3f5..000000000 --- a/core/src/main/java/com/volmit/iris/util/scheduling/jobs/ParallelRadiusJob.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.volmit.iris.util.scheduling.jobs; - -import com.volmit.iris.util.math.Spiraler; -import com.volmit.iris.util.parallel.MultiBurst; -import lombok.SneakyThrows; -import lombok.Synchronized; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicInteger; - -public abstract class ParallelRadiusJob implements Job { - private final ExecutorService service; - private final AtomicInteger completed; - private volatile int radiusX, radiusZ; - private volatile int offsetX, offsetZ; - private volatile int total; - private final Semaphore lock; - private final int lockSize; - - public ParallelRadiusJob(int concurrent) { - this(concurrent, MultiBurst.burst); - } - - public ParallelRadiusJob(int concurrent, ExecutorService service) { - this.service = service; - completed = new AtomicInteger(0); - lock = new Semaphore(concurrent); - lockSize = concurrent; - } - - public ParallelRadiusJob retarget(int radius, int offsetX, int offsetZ) { - return retarget(radius, radius, offsetX, offsetZ); - } - - @Synchronized - public ParallelRadiusJob retarget(int radiusX, int radiusZ, int offsetX, int offsetZ) { - completed.set(0); - this.radiusX = radiusX; - this.radiusZ = radiusZ; - this.offsetX = offsetX; - this.offsetZ = offsetZ; - total = (radiusX * 2 + 1) * (radiusZ * 2 + 1); - return this; - } - - @Override - @SneakyThrows - @Synchronized - public void execute() { - new Spiraler(radiusX * 2 + 3, radiusZ * 2 + 3, this::submit).drain(); - lock.acquire(lockSize); - lock.release(lockSize); - } - - @SneakyThrows - private void submit(int x, int z) { - if (Math.abs(x) > radiusX || Math.abs(z) > radiusZ) return; - lock.acquire(); - service.submit(() -> { - try { - execute(x + offsetX, z + offsetZ); - } finally { - completeWork(); - } - }); - } - - protected abstract void execute(int x, int z); - - @Override - public void completeWork() { - completed.incrementAndGet(); - lock.release(); - } - - @Override - public int getTotalWork() { - return total; - } - - @Override - public int getWorkCompleted() { - return completed.get(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/sentry/IrisLogger.java b/core/src/main/java/com/volmit/iris/util/sentry/IrisLogger.java deleted file mode 100644 index e51d407c5..000000000 --- a/core/src/main/java/com/volmit/iris/util/sentry/IrisLogger.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.volmit.iris.util.sentry; - -import com.volmit.iris.Iris; -import io.sentry.ILogger; -import io.sentry.SentryLevel; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.PrintWriter; -import java.io.StringWriter; - -public class IrisLogger implements ILogger { - @Override - public void log(@NotNull SentryLevel level, @NotNull String message, @Nullable Object... args) { - Iris.msg(String.format("%s: %s", level, String.format(message, args))); - } - - @Override - public void log(@NotNull SentryLevel level, @NotNull String message, @Nullable Throwable throwable) { - if (throwable == null) { - log(level, message); - } else { - Iris.msg(String.format("%s: %s\n%s", level, String.format(message, throwable), captureStackTrace(throwable))); - } - } - - @Override - public void log(@NotNull SentryLevel level, @Nullable Throwable throwable, @NotNull String message, @Nullable Object... args) { - if (throwable == null) { - log(level, message, args); - } else { - Iris.msg(String.format("%s: %s\n%s", level, String.format(message, throwable), captureStackTrace(throwable))); - } - } - - @Override - public boolean isEnabled(@Nullable SentryLevel level) { - return true; - } - - private @NotNull String captureStackTrace(@NotNull Throwable throwable) { - StringWriter stringWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(stringWriter); - throwable.printStackTrace(printWriter); - return stringWriter.toString(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/ArraySignificance.java b/core/src/main/java/com/volmit/iris/util/stream/ArraySignificance.java deleted file mode 100644 index 12c8dbcb1..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/ArraySignificance.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.stream; - -import com.volmit.iris.util.collection.KList; - -public class ArraySignificance implements Significance { - private final KList types; - private final KList significance; - private final T significant; - - public ArraySignificance(KList types, KList significance, T significant) { - this.types = types; - this.significance = significance; - this.significant = significant; - } - - public ArraySignificance(KList types, KList significance) { - this.types = types; - this.significance = significance; - double s = 0; - int v = 0; - for (int i = 0; i < significance.size(); i++) { - if (significance.get(i) > s) { - s = significance.get(i); - v = i; - } - } - - significant = types.get(v); - } - - @Override - public KList getFactorTypes() { - return types; - } - - @Override - public double getSignificance(T t) { - for (int i = 0; i < types.size(); i++) { - if (types.get(i).equals(t)) { - return significance.get(i); - } - } - - return 0; - } - - @Override - public T getMostSignificantType() { - return significant; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/BasicLayer.java b/core/src/main/java/com/volmit/iris/util/stream/BasicLayer.java deleted file mode 100644 index cffdd2fdf..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/BasicLayer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.stream; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class BasicLayer implements ProceduralLayer { - private final long seed; - private final double zoom; - private final double offsetX; - private final double offsetY; - private final double offsetZ; - - public BasicLayer(long seed, double zoom) { - this(seed, zoom, 0D, 0D, 0D); - } - - public BasicLayer(long seed) { - this(seed, 1D); - } - - public BasicLayer() { - this(1337); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/ProceduralLayer.java b/core/src/main/java/com/volmit/iris/util/stream/ProceduralLayer.java deleted file mode 100644 index 5ae3f9c23..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/ProceduralLayer.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.stream; - -public interface ProceduralLayer { - long getSeed(); - - double getOffsetX(); - - double getOffsetY(); - - double getOffsetZ(); - - double getZoom(); -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/Significance.java b/core/src/main/java/com/volmit/iris/util/stream/Significance.java deleted file mode 100644 index 28937a641..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/Significance.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.stream; - -import com.volmit.iris.util.collection.KList; - -public interface Significance { - KList getFactorTypes(); - - double getSignificance(T t); - - T getMostSignificantType(); -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/interpolation/TriHermiteStream.java b/core/src/main/java/com/volmit/iris/util/stream/interpolation/TriHermiteStream.java deleted file mode 100644 index 079675397..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/interpolation/TriHermiteStream.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.stream.interpolation; - -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; - -public class TriHermiteStream extends BasicStream implements Interpolator { - private final int rx; - private final int ry; - private final int rz; - private final double tension; - private final double bias; - - public TriHermiteStream(ProceduralStream stream, int rx, int ry, int rz, double tension, double bias) { - super(stream); - this.rx = rx; - this.ry = ry; - this.rz = rz; - this.tension = tension; - this.bias = bias; - } - - public T interpolate(double x, double y, double z) { - int fx = (int) Math.floor(x / rx); - int fy = (int) Math.floor(y / ry); - int fz = (int) Math.floor(z / rz); - int x0 = Math.round((fx - 1) * rx); - int y0 = Math.round((fy - 1) * ry); - int z0 = Math.round((fz - 1) * rz); - int x1 = Math.round(fx * rx); - int y1 = Math.round(fy * ry); - int z1 = Math.round(fz * rz); - int x2 = Math.round((fx + 1) * rx); - int y2 = Math.round((fy + 1) * ry); - int z2 = Math.round((fz + 1) * rz); - int x3 = Math.round((fx + 2) * rx); - int y3 = Math.round((fy + 2) * ry); - int z3 = Math.round((fz + 2) * rz); - double px = IrisInterpolation.rangeScale(0, 1, x1, x2, x); - double py = IrisInterpolation.rangeScale(0, 1, y1, y2, y); - double pz = IrisInterpolation.rangeScale(0, 1, z1, z2, z); - - //@builder - return getTypedSource().fromDouble(IrisInterpolation.trihermite( - getTypedSource().getDouble(x0, y0, z0), - getTypedSource().getDouble(x0, y0, z1), - getTypedSource().getDouble(x0, y0, z2), - getTypedSource().getDouble(x0, y0, z3), - getTypedSource().getDouble(x1, y0, z0), - getTypedSource().getDouble(x1, y0, z1), - getTypedSource().getDouble(x1, y0, z2), - getTypedSource().getDouble(x1, y0, z3), - getTypedSource().getDouble(x2, y0, z0), - getTypedSource().getDouble(x2, y0, z1), - getTypedSource().getDouble(x2, y0, z2), - getTypedSource().getDouble(x2, y0, z3), - getTypedSource().getDouble(x3, y0, z0), - getTypedSource().getDouble(x3, y0, z1), - getTypedSource().getDouble(x3, y0, z2), - getTypedSource().getDouble(x3, y0, z3), - getTypedSource().getDouble(x0, y1, z0), - getTypedSource().getDouble(x0, y1, z1), - getTypedSource().getDouble(x0, y1, z2), - getTypedSource().getDouble(x0, y1, z3), - getTypedSource().getDouble(x1, y1, z0), - getTypedSource().getDouble(x1, y1, z1), - getTypedSource().getDouble(x1, y1, z2), - getTypedSource().getDouble(x1, y1, z3), - getTypedSource().getDouble(x2, y1, z0), - getTypedSource().getDouble(x2, y1, z1), - getTypedSource().getDouble(x2, y1, z2), - getTypedSource().getDouble(x2, y1, z3), - getTypedSource().getDouble(x3, y1, z0), - getTypedSource().getDouble(x3, y1, z1), - getTypedSource().getDouble(x3, y1, z2), - getTypedSource().getDouble(x3, y1, z3), - getTypedSource().getDouble(x0, y2, z0), - getTypedSource().getDouble(x0, y2, z1), - getTypedSource().getDouble(x0, y2, z2), - getTypedSource().getDouble(x0, y2, z3), - getTypedSource().getDouble(x1, y2, z0), - getTypedSource().getDouble(x1, y2, z1), - getTypedSource().getDouble(x1, y2, z2), - getTypedSource().getDouble(x1, y2, z3), - getTypedSource().getDouble(x2, y2, z0), - getTypedSource().getDouble(x2, y2, z1), - getTypedSource().getDouble(x2, y2, z2), - getTypedSource().getDouble(x2, y2, z3), - getTypedSource().getDouble(x3, y2, z0), - getTypedSource().getDouble(x3, y2, z1), - getTypedSource().getDouble(x3, y2, z2), - getTypedSource().getDouble(x3, y2, z3), - getTypedSource().getDouble(x0, y3, z0), - getTypedSource().getDouble(x0, y3, z1), - getTypedSource().getDouble(x0, y3, z2), - getTypedSource().getDouble(x0, y3, z3), - getTypedSource().getDouble(x1, y3, z0), - getTypedSource().getDouble(x1, y3, z1), - getTypedSource().getDouble(x1, y3, z2), - getTypedSource().getDouble(x1, y3, z3), - getTypedSource().getDouble(x2, y3, z0), - getTypedSource().getDouble(x2, y3, z1), - getTypedSource().getDouble(x2, y3, z2), - getTypedSource().getDouble(x2, y3, z3), - getTypedSource().getDouble(x3, y3, z0), - getTypedSource().getDouble(x3, y3, z1), - getTypedSource().getDouble(x3, y3, z2), - getTypedSource().getDouble(x3, y3, z3), - px, pz, py, tension, bias)); - //@done - } - - @Override - public double toDouble(T t) { - return getTypedSource().toDouble(t); - } - - @Override - public T fromDouble(double d) { - return getTypedSource().fromDouble(d); - } - - @Override - public T get(double x, double z) { - return interpolate(x, 0, z); - } - - @Override - public T get(double x, double y, double z) { - return interpolate(x, y, z); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/utility/CachedStream2D.java b/core/src/main/java/com/volmit/iris/util/stream/utility/CachedStream2D.java deleted file mode 100644 index b5f3bedd2..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/utility/CachedStream2D.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.stream.utility; - -import com.volmit.iris.Iris; -import com.volmit.iris.core.service.PreservationSVC; -import com.volmit.iris.engine.framework.Engine; -import com.volmit.iris.engine.framework.MeteredCache; -import com.volmit.iris.util.cache.WorldCache2D; -import com.volmit.iris.util.data.KCache; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; - -public class CachedStream2D extends BasicStream implements ProceduralStream, MeteredCache { - private final ProceduralStream stream; - private final WorldCache2D cache; - private final Engine engine; - private final boolean chunked = true; - - public CachedStream2D(String name, Engine engine, ProceduralStream stream, int size) { - super(); - this.stream = stream; - this.engine = engine; - cache = new WorldCache2D<>(stream::get, size); - Iris.service(PreservationSVC.class).registerCache(this); - } - - @Override - public double toDouble(T t) { - return stream.toDouble(t); - } - - @Override - public T fromDouble(double d) { - return stream.fromDouble(d); - } - - @Override - public T get(double x, double z) { - //return stream.get(x, z); - return cache.get((int) x, (int) z); - } - - @Override - public T get(double x, double y, double z) { - return stream.get(x, y, z); - } - - @Override - public long getSize() { - return cache.getSize(); - } - - @Override - public KCache getRawCache() { - return null; - } - - @Override - public long getMaxSize() { - return cache.getMaxSize(); - } - - @Override - public boolean isClosed() { - return engine.isClosed(); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/utility/ContextInjectingStream.java b/core/src/main/java/com/volmit/iris/util/stream/utility/ContextInjectingStream.java deleted file mode 100644 index 5e0ae3bb8..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/utility/ContextInjectingStream.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.volmit.iris.util.stream.utility; - -import com.volmit.iris.util.context.ChunkContext; -import com.volmit.iris.util.context.IrisContext; -import com.volmit.iris.util.function.Function3; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; - -public class ContextInjectingStream extends BasicStream { - private final Function3 contextAccessor; - - public ContextInjectingStream(ProceduralStream stream, Function3 contextAccessor) { - super(stream); - this.contextAccessor = contextAccessor; - } - - @Override - public T get(double x, double z) { - IrisContext context = IrisContext.get(); - - if (context != null) { - ChunkContext chunkContext = context.getChunkContext(); - - if (chunkContext != null && (int) x >> 4 == chunkContext.getX() >> 4 && (int) z >> 4 == chunkContext.getZ() >> 4) { - T t = contextAccessor.apply(chunkContext, (int) x & 15, (int) z & 15); - - if (t != null) { - return t; - } - } - } - - return getTypedSource().get(x, z); - } - - @Override - public T get(double x, double y, double z) { - return getTypedSource().get(x, y, z); - } - - @Override - public double toDouble(T t) { - return getTypedSource().toDouble(t); - } - - @Override - public T fromDouble(double d) { - return getTypedSource().fromDouble(d); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/utility/ProfiledStream.java b/core/src/main/java/com/volmit/iris/util/stream/utility/ProfiledStream.java deleted file mode 100644 index e1002e545..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/utility/ProfiledStream.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.stream.utility; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.math.RollingSequence; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; -import lombok.Data; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; - -public class ProfiledStream extends BasicStream { - public static final AtomicInteger ids = new AtomicInteger(); - public static final KList> profiles = new KList<>(); - private final int id; - private final RollingSequence metrics; - - public ProfiledStream(ProceduralStream stream, int memory) { - super(stream); - this.metrics = new RollingSequence(memory); - this.id = ids.getAndAdd(1); - profiles.add(this); - } - - public static void print(Consumer printer, ProceduralStream stream) { - KList tails = getTails(stream); - int ind = tails.size(); - for (ProfiledTail i : tails) { - printer.accept(Form.repeat(" ", ind) + i); - ind--; - } - } - - private static KList> getAllChildren(ProceduralStream s) { - KList> v = new KList<>(); - ProceduralStream cursor = s; - - for (int i = 0; i < 32; i++) { - v.add(cursor); - cursor = nextChuld(cursor); - - if (cursor == null) { - break; - } - } - - return v; - } - - private static ProceduralStream nextChuld(ProceduralStream s) { - ProceduralStream v = s.getTypedSource(); - return v == null ? s.getSource() : v; - } - - private static ProfiledTail getTail(ProceduralStream t) { - if (t instanceof ProfiledStream s) { - - return new ProfiledTail(s.getId(), s.getMetrics(), s.getClass().getSimpleName().replaceAll("\\QStream\\E", "")); - } - - return null; - } - - private static KList getTails(ProceduralStream t) { - KList tails = new KList<>(); - - for (ProceduralStream v : getAllChildren(t)) { - ProfiledTail p = getTail(v); - - if (p != null) { - tails.add(p); - } - } - - if (tails.isEmpty()) { - return null; - } - - ProfiledTail cursor = tails.popLast(); - KList tailx = new KList<>(); - tailx.add(cursor); - - while (tails.isNotEmpty()) { - tailx.add(cursor); - ProfiledTail parent = tails.popLast(); - parent.setChild(cursor); - cursor = parent; - tailx.add(cursor); - } - - return tailx; - } - - public int getId() { - return id; - } - - @Override - public double toDouble(T t) { - return getTypedSource().toDouble(t); - } - - @Override - public T fromDouble(double d) { - return getTypedSource().fromDouble(d); - } - - @Override - public T get(double x, double z) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - T t = getTypedSource().get(x, z); - try { - metrics.put(p.getMilliseconds()); - } catch (Throwable e) { - Iris.reportError(e); - } - - return t; - } - - @Override - public T get(double x, double y, double z) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - T t = getTypedSource().get(x, y, z); - try { - metrics.put(p.getMilliseconds()); - } catch (Throwable e) { - Iris.reportError(e); - } - - return t; - } - - public RollingSequence getMetrics() { - return metrics; - } - - @Data - private static class ProfiledTail { - private final int id; - private final RollingSequence metrics; - private final String name; - private ProfiledTail child; - - public ProfiledTail(int id, RollingSequence metrics, String name) { - this.id = id; - this.metrics = metrics; - this.name = name; - } - - public String toString() { - return id + "-" + name + ": " + Form.duration(metrics.getAverage(), 2); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/utility/SemaphoreStream.java b/core/src/main/java/com/volmit/iris/util/stream/utility/SemaphoreStream.java deleted file mode 100644 index fb3800c9c..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/utility/SemaphoreStream.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.volmit.iris.util.stream.utility; - -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; - -import java.util.concurrent.Semaphore; - -public class SemaphoreStream extends BasicStream { - private final Semaphore semaphore; - - public SemaphoreStream(ProceduralStream stream, int permits) { - super(stream); - this.semaphore = new Semaphore(permits); - } - - @Override - public double toDouble(T t) { - return getTypedSource().toDouble(t); - } - - @Override - public T fromDouble(double d) { - return getTypedSource().fromDouble(d); - } - - @Override - public T get(double x, double z) { - try { - semaphore.acquire(); - T t = getTypedSource().get(x, z); - semaphore.release(); - return t; - } catch (InterruptedException e) { - e.printStackTrace(); - } - - return null; - } - - @Override - public T get(double x, double y, double z) { - try { - semaphore.acquire(); - T t = getTypedSource().get(x, y, z); - semaphore.release(); - return t; - } catch (InterruptedException e) { - e.printStackTrace(); - } - - return null; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/stream/utility/WasteDetector.java b/core/src/main/java/com/volmit/iris/util/stream/utility/WasteDetector.java deleted file mode 100644 index 498a93d03..000000000 --- a/core/src/main/java/com/volmit/iris/util/stream/utility/WasteDetector.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.volmit.iris.util.stream.utility; - -import com.volmit.iris.Iris; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.stream.BasicStream; -import com.volmit.iris.util.stream.ProceduralStream; - -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -public class WasteDetector extends BasicStream { - public static final boolean checking = false; - private static final KMap allAccesses = new KMap<>(); - private static final KMap> allThrows = new KMap<>(); - private final AtomicInteger accesses; - private final String name; - - public WasteDetector(ProceduralStream stream, String name) { - super(stream); - this.name = name; - accesses = new AtomicInteger(0); - } - - public static void printAll() { - if (checking) { - Iris.warn("========================================================="); - for (String i : allAccesses.sortKNumber().reverse()) { - Iris.warn(i + ": " + allAccesses.get(i) + " Time(s)"); - } - Iris.warn("========================================================="); - for (String i : allAccesses.sortKNumber().reverse()) { - Iris.warn("======== " + i + " ========"); - for (Throwable j : allThrows.get(i)) { - j.printStackTrace(); - } - Iris.warn("---------------------------------------------------------"); - } - Iris.warn("========================================================="); - } - } - - @Override - public T get(double x, double z) { - if (checking) { - if (x == 7 && z == 7) { - // AHHHAAA! - allAccesses.compute(name, (k, v) -> v == null ? 1 : v + 1); - try { - throw new RuntimeException(); - } catch (RuntimeException e) { - allThrows.computeIfAbsent(name, (k) -> new KList<>()).add(e); - } - } - - } - return getTypedSource().get(x, z); - } - - @Override - public T get(double x, double y, double z) { - return getTypedSource().get(x, y, z); - } - - @Override - public double toDouble(T t) { - return getTypedSource().toDouble(t); - } - - @Override - public T fromDouble(double d) { - return getTypedSource().fromDouble(d); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/U.java b/core/src/main/java/com/volmit/iris/util/uniques/U.java deleted file mode 100644 index 738c186f4..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/U.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.volmit.iris.util.uniques; - -import java.io.File; - -public class U { - public static void main(String[] a) { - UniqueRenderer r = new UniqueRenderer("helloworld", 2560, 1440); - - r.writeCollectionFrames(new File("collection"), 1, 1024); - - System.exit(0); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/UBufferedImage.java b/core/src/main/java/com/volmit/iris/util/uniques/UBufferedImage.java deleted file mode 100644 index 21c5df154..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/UBufferedImage.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.volmit.iris.util.uniques; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.ColorModel; -import java.awt.image.WritableRaster; - -public class UBufferedImage implements UImage { - private final BufferedImage buf; - - public UBufferedImage(BufferedImage buf) { - this.buf = buf; - } - - @Override - public int getWidth() { - return buf.getWidth(); - } - - @Override - public int getHeight() { - return buf.getHeight(); - } - - @Override - public UImage copy() { - ColorModel cm = buf.getColorModel(); - boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); - WritableRaster raster = buf.copyData(null); - return new UBufferedImage(new BufferedImage(cm, raster, isAlphaPremultiplied, null)); - } - - @Override - public Color get(int x, int y) { - return new Color(buf.getRGB(x, y)); - } - - @Override - public void set(int x, int y, Color color) { - try { - buf.setRGB(x, y, color.getRGB()); - } catch (Throwable e) { - - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/UFeature.java b/core/src/main/java/com/volmit/iris/util/uniques/UFeature.java deleted file mode 100644 index 1da6de733..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/UFeature.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.volmit.iris.util.uniques; - -import com.volmit.iris.util.function.NoiseInjector; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; - -import java.awt.*; -import java.util.List; -import java.util.function.Consumer; - -public interface UFeature { - List injectors = List.of( - CNG.ADD, - CNG.DST_MOD, - CNG.DST_POW, - CNG.DST_SUBTRACT, - CNG.MAX, - CNG.MIN, - CNG.SRC_MOD, - CNG.SRC_POW, - CNG.SRC_SUBTRACT, - CNG.MULTIPLY - ); - - void render(UImage image, RNG rng, double time, Consumer progressor, UFeatureMeta meta); - - default Color color(CNG hue, CNG saturation, CNG brightness, double x, double y, double t) { - return Color.getHSBColor((float) hue.fitDouble(0, 1, x + t, y + t), - (float) saturation.fitDouble(0, 1, x + t, y + t), - (float) brightness.fitDouble(0, 1, x + t, y + t)); - } - - default InterpolationMethod interpolator(RNG rng) { - return rng.pick( - UniqueRenderer.renderer.getInterpolators() - ); - } - - default CNG generator(String key, RNG rng, double scaleMod, long salt, UFeatureMeta meta) { - return generator(key, rng, scaleMod, rng.i(1, 3), rng.i(1, 5), salt, meta); - } - - default CNG generator(String key, RNG rng, double scaleMod, int fractures, int composites, long salt, UFeatureMeta meta) { - RNG rngg = rng.nextParallelRNG(salt); - CNG cng = rng.pick(UniqueRenderer.renderer.getStyles()).create(rngg).oct(rng.i(1, 5)); - RNG rngf = rngg.nextParallelRNG(-salt); - cng.scale(rngf.d(0.33 * scaleMod, 1.66 * scaleMod)); - - if (fractures > 0) { - cng.fractureWith(generator(null, rngf.nextParallelRNG(salt + fractures), scaleMod / rng.d(4, 17), fractures - 1, composites, salt + fractures + 55, null), scaleMod * rngf.nextDouble(16, 256)); - } - - for (int i = 0; i < composites; i++) { - CNG sub = generator(null, rngf.nextParallelRNG(salt + fractures), scaleMod * rngf.d(0.4, 3.3), fractures / 3, 0, salt + fractures + composites + 78, null); - sub.setInjector(rng.pick(injectors)); - cng.child(sub); - } - - if (key != null && meta != null) { - meta.registerGenerator(key, cng); - } - return cng; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/UFeatureMeta.java b/core/src/main/java/com/volmit/iris/util/uniques/UFeatureMeta.java deleted file mode 100644 index d4cf8e4a5..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/UFeatureMeta.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.volmit.iris.util.uniques; - -import com.volmit.iris.engine.object.NoiseStyle; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@NoArgsConstructor -public class UFeatureMeta { - private KMap interpolators; - private KMap generators; - private String feature; - - public void registerInterpolator(String key, InterpolationMethod method, double radius) { - if (interpolators == null) { - interpolators = new KMap<>(); - } - interpolators.put(key, new UFeatureMetaInterpolator(method, radius)); - } - - public void registerGenerator(String key, CNG cng) { - if (generators == null) { - generators = new KMap<>(); - } - generators.put(key, buildGenerator(cng)); - } - - public UFeatureMetaGenerator buildGenerator(CNG cng) { - UFeatureMetaGenerator g = new UFeatureMetaGenerator(); - g.setScale(cng.getScale()); - g.setOctaves(cng.getOct()); - - if (cng.getFracture() != null) { - g.setFracture(buildGenerator(cng.getFracture())); - g.setFractureMultiplier(cng.getFscale()); - } - - if (cng.getChildren() != null && cng.getChildren().isNotEmpty()) { - g.setChildren(new KList<>()); - - for (CNG i : cng.getChildren()) { - g.getChildren().add(buildGenerator(i)); - } - } - - if (cng.getInjector() == CNG.ADD) { - g.setParentInject("add"); - } else if (cng.getInjector() == CNG.SRC_SUBTRACT) { - g.setParentInject("src_subtract"); - } else if (cng.getInjector() == CNG.DST_SUBTRACT) { - g.setParentInject("dst_subtract"); - } else if (cng.getInjector() == CNG.MULTIPLY) { - g.setParentInject("multiply"); - } else if (cng.getInjector() == CNG.MAX) { - g.setParentInject("max"); - } else if (cng.getInjector() == CNG.MIN) { - g.setParentInject("min"); - } else if (cng.getInjector() == CNG.SRC_MOD) { - g.setParentInject("src_mod"); - } else if (cng.getInjector() == CNG.SRC_POW) { - g.setParentInject("src_pow"); - } else if (cng.getInjector() == CNG.DST_MOD) { - g.setParentInject("dst_mod"); - } else if (cng.getInjector() == CNG.DST_POW) { - g.setParentInject("dst_pow"); - } - - return g; - } - - public boolean isEmpty() { - return (interpolators == null || interpolators.isEmpty()) && (generators == null || generators.isEmpty()); - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - static class UFeatureMetaInterpolator { - private InterpolationMethod interpolator; - private double radius; - } - - @Data - @NoArgsConstructor - static class UFeatureMetaGenerator { - private NoiseStyle style; - private int octaves = 1; - private double scale = 1; - private String parentInject; - private UFeatureMetaGenerator fracture; - private Double fractureMultiplier; - private List children; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/UImage.java b/core/src/main/java/com/volmit/iris/util/uniques/UImage.java deleted file mode 100644 index 0b9b63fe3..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/UImage.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.volmit.iris.util.uniques; - -import java.awt.*; - -public interface UImage { - int getWidth(); - - int getHeight(); - - default boolean isInBounds(int x, int y) { - return x >= 0 && x < getWidth() && y >= 0 && y < getHeight(); - } - - UImage copy(); - - Color get(int x, int y); - - void set(int x, int y, Color color); -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/UMeta.java b/core/src/main/java/com/volmit/iris/util/uniques/UMeta.java deleted file mode 100644 index 773f428eb..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/UMeta.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.volmit.iris.util.uniques; - -import com.google.gson.GsonBuilder; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.io.IO; -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -@Data -@NoArgsConstructor -public class UMeta { - private transient BufferedImage image; - private KMap features; - private long id; - private double time; - private int width; - private int height; - - public void registerFeature(String key, UFeatureMeta feature) { - if (features == null) { - features = new KMap<>(); - } - - features.put(key, feature); - } - - public void export(File destination) throws IOException { - - for (String i : features.k()) { - if (features.get(i).isEmpty()) { - features.remove(i); - } - } - - width = image.getWidth(); - height = image.getHeight(); - ImageIO.write(image, "PNG", destination); - IO.writeAll(new File(destination.getParentFile(), destination.getName() + ".json"), new GsonBuilder().setPrettyPrinting().create().toJson(this)); - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/UniqueRenderer.java b/core/src/main/java/com/volmit/iris/util/uniques/UniqueRenderer.java deleted file mode 100644 index 633b52e1c..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/UniqueRenderer.java +++ /dev/null @@ -1,289 +0,0 @@ -package com.volmit.iris.util.uniques; - -import com.volmit.iris.engine.object.NoiseStyle; -import com.volmit.iris.util.collection.KList; -import com.volmit.iris.util.collection.KMap; -import com.volmit.iris.util.format.Form; -import com.volmit.iris.util.function.NoiseProvider; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.parallel.BurstExecutor; -import com.volmit.iris.util.scheduling.ChronoLatch; -import com.volmit.iris.util.scheduling.J; -import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import com.volmit.iris.util.stream.ProceduralStream; -import com.volmit.iris.util.uniques.features.*; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; - -public class UniqueRenderer { - static final List backgrounds = List.of(new UFWarpedBackground()); - static final List interpolators = List.of(new UFInterpolator(), new UFNOOP()); - static final List features = List.of(new UFWarpedLines(), new UFWarpedDisc(), new UFWarpedDots(), new UFWarpedCircle()); - static UniqueRenderer renderer; - private final String seed; - private final ProceduralStream spatialSeed; - private final int width; - private final int height; - private final KMap writing = new KMap<>(); - private final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); - private final KList sortedStyles = new KList(); - private final KList sortedInterpolators = new KList(); - int cores = Runtime.getRuntime().availableProcessors(); - - public UniqueRenderer(String seed, int width, int height) { - renderer = this; - computeNoiseStyles(3000, 2); - computeInterpolationMethods(3000, 2); - this.seed = seed; - this.width = width; - this.height = height; - spatialSeed = NoiseStyle.FRACTAL_WATER.stream(new RNG(seed)).convert((d) -> new RNG(Math.round(seed.hashCode() + (d * 934321234D)))); - new Thread(() -> { - while (true) { - J.sleep(5000); - - if (!writing.isEmpty()) { - System.out.println(Form.repeat("\n", 60)); - System.out.println(Form.memSize(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), 2) + " of " + Form.memSize(Runtime.getRuntime().totalMemory(), 2)); - KMap c = writing.copy(); - - for (String i : writing.k().sort()) { - String prog = ""; - String f = writing.get(i); - - if (f.contains("%")) { - String v = f.split("\\Q%\\E")[0]; - try { - prog = drawProgress(Double.valueOf(Integer.parseInt(v.substring(v.length() - 2))) / 100D, 30); - } catch (Throwable e) { - try { - prog = drawProgress(Double.valueOf(Integer.parseInt(v.substring(v.length() - 1))) / 100D, 30); - } catch (Throwable ee) { - try { - prog = drawProgress(Double.valueOf(Integer.parseInt(v.substring(v.length() - 3))) / 100D, 30); - } catch (Throwable eee) { - - } - } - } - } - - System.out.println(prog + " " + i + " => " + f); - } - } - } - }).start(); - } - - public UMeta renderFrameBuffer(long id, double t) { - UMeta meta = new UMeta(); - meta.setId(id); - meta.setTime(t); - RNG rng = spatialSeed.get(id, id + ((id * id) % (id / 3D))); - RNG rngbg = spatialSeed.get(id, -id + ((id * id) % (id / 4D))); - BufferedImage buf = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - BufferedImage bufFG = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - UImage image = new UBufferedImage(buf); - UImage imageFG = new UBufferedImage(bufFG); - ChronoLatch cl = new ChronoLatch(250); - UFeature background = rng.pick(backgrounds); - UFeature interpolator = rng.pick(interpolators); - UFeature foreground = rng.pick(features); - UFeature foregroundInterpolator = rng.pick(interpolators); - UFeatureMeta backgroundMeta = new UFeatureMeta(); - UFeatureMeta foregroundMeta = new UFeatureMeta(); - UFeatureMeta backgroundInterpolatorMeta = new UFeatureMeta(); - UFeatureMeta foregroundInterpolatorMeta = new UFeatureMeta(); - background.render(image, rngbg, t, (p) -> { - if (cl.flip()) { - writing.put("#" + id + ":" + t, Form.pc(p / 4D) + " [" + background.getClass().getSimpleName() + " " + Form.pc(p) + "]"); - } - }, backgroundMeta); - backgroundMeta.setFeature(background.getClass().getSimpleName()); - meta.registerFeature("background", backgroundMeta); - interpolator.render(image, rng, t, (p) -> { - if (cl.flip()) { - writing.put("#" + id + ":" + t, Form.pc(0.25 + (p / 4d)) + " [" + interpolator.getClass().getSimpleName() + " " + Form.pc(p) + "]"); - } - }, backgroundInterpolatorMeta); - backgroundInterpolatorMeta.setFeature(interpolator.getClass().getSimpleName()); - meta.registerFeature("backgroundInterpolator", backgroundInterpolatorMeta); - foreground.render(imageFG, rng, t, (p) -> { - if (cl.flip()) { - writing.put("#" + id + ":" + t, Form.pc(0.5 + (p / 4d)) + " [" + foreground.getClass().getSimpleName() + " " + Form.pc(p) + "]"); - } - }, foregroundMeta); - foregroundMeta.setFeature(foreground.getClass().getSimpleName()); - meta.registerFeature("foreground", foregroundMeta); - overlay(imageFG, bufFG, image); - foregroundInterpolator.render(image, rng, t, (p) -> { - if (cl.flip()) { - writing.put("#" + id + ":" + t, Form.pc(0.75 + (p / 4d)) + " [" + interpolator.getClass().getSimpleName() + " " + Form.pc(p) + "]"); - } - }, foregroundInterpolatorMeta); - foregroundInterpolatorMeta.setFeature(foregroundInterpolator.getClass().getSimpleName()); - meta.registerFeature("foregroundInterpolator", foregroundInterpolatorMeta); - overlay(imageFG, bufFG, image); - meta.setImage(buf); - writing.remove("#" + id + ":" + t); - return meta; - } - - private void overlay(UImage layer, BufferedImage layerBuf, UImage onto) { - for (int i = 0; i < onto.getWidth(); i++) { - for (int j = 0; j < onto.getHeight(); j++) { - if (layerBuf.getRGB(i, j) != 0) { - onto.set(i, j, layer.get(i, j)); - } - } - } - } - - private String drawProgress(double progress, int len) { - int max = len; - int in = (int) Math.round(progress * max); - max -= in; - - return "[" + Form.repeat("=", in) + Form.repeat(" ", max) + "]"; - } - - private void computeNoiseStyles(double time, double scope) { - List allowedStyles = new KList<>(NoiseStyle.values()); - allowedStyles.remove(NoiseStyle.FLAT); - KMap speeds = new KMap<>(); - double allocateMS = time; - double maxTestDuration = allocateMS / allowedStyles.size(); - System.out.println("Running Noise Style Benchmark for " + Form.duration(allocateMS, 0) + "."); - System.out.println("Benchmarking " + allowedStyles.size() + " + Noise Styles for " + Form.duration(maxTestDuration, 1) + " each."); - System.out.println(); - - for (NoiseStyle i : allowedStyles) { - int score = 0; - CNG cng = i.create(new RNG("renderspeedtest")); - PrecisionStopwatch p = PrecisionStopwatch.start(); - double g = 0; - while (p.getMilliseconds() < maxTestDuration) { - cng.noise(g, -g * 2); - g += 0.1; - g *= 1.25; - score++; - } - - speeds.put(i, score); - } - - for (NoiseStyle i : speeds.sortKNumber()) { - System.out.println(Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")) + " => " + Form.f(speeds.get(i))); - } - System.out.println(); - int takeUpTo = (int) Math.max(1, scope * speeds.size()); - System.out.println("Choosing the fastest " + Form.pc(scope) + " styles (" + takeUpTo + ")"); - - for (NoiseStyle i : speeds.sortKNumber().reverse()) { - if (takeUpTo-- <= 0) { - break; - } - - sortedStyles.add(i); - System.out.println("- " + Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "))); - } - } - - private void computeInterpolationMethods(double time, double scope) { - List allowedStyles = new KList<>(InterpolationMethod.values()); - allowedStyles.remove(InterpolationMethod.NONE); - KMap speeds = new KMap<>(); - double allocateMS = time; - double maxTestDuration = allocateMS / allowedStyles.size(); - System.out.println("Running Interpolation Method Benchmark for " + Form.duration(allocateMS, 0) + "."); - System.out.println("Benchmarking " + allowedStyles.size() + " + Interpolation Methods for " + Form.duration(maxTestDuration, 1) + " each."); - System.out.println(); - - RNG r = new RNG("renderspeedtestinterpolation"); - CNG cng = NoiseStyle.SIMPLEX.create(r); - NoiseProvider np = (x, z) -> cng.noise(x, z); - - for (InterpolationMethod i : allowedStyles) { - int score = 0; - - PrecisionStopwatch p = PrecisionStopwatch.start(); - double g = 0; - while (p.getMilliseconds() < maxTestDuration) { - IrisInterpolation.getNoise(i, (int) g, (int) (-g * 2.225), r.d(4, 64), np); - cng.noise(g, -g * 2); - g += 1.1; - g *= 1.25; - score++; - } - - speeds.put(i, score); - } - - for (InterpolationMethod i : speeds.sortKNumber()) { - System.out.println(Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")) + " => " + Form.f(speeds.get(i))); - } - System.out.println(); - int takeUpTo = (int) Math.max(1, scope * speeds.size()); - System.out.println("Choosing the fastest " + Form.pc(scope) + " interpolators (" + takeUpTo + ")"); - - for (InterpolationMethod i : speeds.sortKNumber().reverse()) { - if (takeUpTo-- <= 0) { - break; - } - - sortedInterpolators.add(i); - System.out.println("- " + Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "))); - } - } - - public void writeCollectionFrames(File folder, int fromId, int toId) { - folder.mkdirs(); - BurstExecutor burst = new BurstExecutor(executor, Math.min(toId - fromId, 1000)); - burst.setMulticore(true); - AtomicInteger ai = new AtomicInteger(0); - int max = toId - fromId; - - for (int i = fromId; i <= toId; i++) { - int ii = i; - burst.queue(() -> { - - writing.put("!#[" + fromId + "-" + toId + "] Collection", ai.get() + " of " + max + " (" + Form.pc(ai.get() / (double) max, 0) + ")"); - writeFrame(new File(folder, ii + ".png"), ii, 0); - ai.incrementAndGet(); - writing.put("!#[" + fromId + "-" + toId + "] Collection", ai.get() + " of " + max + " (" + Form.pc(ai.get() / (double) max, 0) + ")"); - }); - } - - burst.complete(); - writing.remove("!#[" + fromId + "-" + toId + "] Collection"); - } - - public void writeFrame(File destination, long id, double t) { - try { - renderFrameBuffer(id, t).export(destination); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - public void report(String s) { - System.out.println(s); - } - - public KList getStyles() { - return sortedStyles; - } - - public List getInterpolators() { - return sortedInterpolators; - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/features/UFInterpolator.java b/core/src/main/java/com/volmit/iris/util/uniques/features/UFInterpolator.java deleted file mode 100644 index 6a8d3fa86..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/features/UFInterpolator.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.volmit.iris.util.uniques.features; - -import com.volmit.iris.util.function.NoiseProvider; -import com.volmit.iris.util.interpolation.InterpolationMethod; -import com.volmit.iris.util.interpolation.IrisInterpolation; -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.uniques.UFeature; -import com.volmit.iris.util.uniques.UFeatureMeta; -import com.volmit.iris.util.uniques.UImage; - -import java.awt.*; -import java.util.function.Consumer; - -public class UFInterpolator implements UFeature { - @Override - public void render(UImage image, RNG rng, double t, Consumer progressor, UFeatureMeta meta) { - UImage ref = image.copy(); - CNG rmod = generator("interpolator_radius", rng, 1, 33004, meta); - - NoiseProvider nHue = (x, y) -> { - int ix = Math.abs(((int) x) % ref.getWidth()); - int iy = Math.abs(((int) y) % ref.getHeight()); - Color color = ref.get(ix, iy); - float[] hsv = new float[3]; - Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getGreen(), hsv); - return hsv[0]; - }; - NoiseProvider nSat = (x, y) -> { - int ix = Math.abs(((int) x) % ref.getWidth()); - int iy = Math.abs(((int) y) % ref.getHeight()); - Color color = ref.get(ix, iy); - float[] hsv = new float[3]; - Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getGreen(), hsv); - return hsv[1]; - }; - NoiseProvider nBri = (x, y) -> { - int ix = Math.abs(((int) x) % ref.getWidth()); - int iy = Math.abs(((int) y) % ref.getHeight()); - Color color = ref.get(ix, iy); - float[] hsv = new float[3]; - Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getGreen(), hsv); - return hsv[2]; - }; - InterpolationMethod method = interpolator(rng); - int sizeMin = Math.min(image.getWidth(), image.getHeight()); - double radius = Math.max(4, rmod.fit(sizeMin / 256, sizeMin / 4, t * rng.d(0.03, 1.25), t * rng.d(0.01, 2.225))); - for (int i = 0; i < image.getWidth(); i++) { - for (int j = 0; j < image.getHeight(); j++) { - image.set(i, j, Color.getHSBColor( - (float) Math.max(Math.min(1D, IrisInterpolation.getNoise(method, i, j, radius, nHue)), 0D), - (float) Math.max(Math.min(1D, IrisInterpolation.getNoise(method, i, j, radius, nSat)), 0D), - (float) Math.max(Math.min(1D, IrisInterpolation.getNoise(method, i, j, radius, nBri)), 0D) - )); - } - - progressor.accept(i / (double) image.getWidth()); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/features/UFNOOP.java b/core/src/main/java/com/volmit/iris/util/uniques/features/UFNOOP.java deleted file mode 100644 index 084bdda34..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/features/UFNOOP.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.volmit.iris.util.uniques.features; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.uniques.UFeature; -import com.volmit.iris.util.uniques.UFeatureMeta; -import com.volmit.iris.util.uniques.UImage; - -import java.util.function.Consumer; - -public class UFNOOP implements UFeature { - @Override - public void render(UImage image, RNG rng, double t, Consumer progressor, UFeatureMeta meta) { - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedBackground.java b/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedBackground.java deleted file mode 100644 index e16f1dced..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedBackground.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.volmit.iris.util.uniques.features; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.uniques.UFeature; -import com.volmit.iris.util.uniques.UFeatureMeta; -import com.volmit.iris.util.uniques.UImage; - -import java.util.function.Consumer; - -public class UFWarpedBackground implements UFeature { - @Override - public void render(UImage image, RNG rng, double t, Consumer progressor, UFeatureMeta meta) { - CNG hue = generator("color_hue", rng, rng.d(0.001, rng.d(2, 5)), rng.i(0, 3), rng.i(0, 3), 31007, meta); - CNG sat = generator("color_sat", rng, rng.d(0.001, rng.d(2, 5)), rng.i(0, 2), rng.i(0, 2), 33004, meta); - CNG bri = generator("color_bri", rng, rng.d(0.001, rng.d(2, 5)), rng.i(0, 1), rng.i(0, 1), 32005, meta).patch(0.145); - double tcf = rng.d(0.15, 0.55); - - for (int i = 0; i < image.getWidth(); i++) { - for (int j = 0; j < image.getHeight(); j++) { - image.set(i, j, color(hue, sat, bri, i, j, tcf * t)); - } - - progressor.accept(i / (double) image.getWidth()); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedCircle.java b/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedCircle.java deleted file mode 100644 index e8c4df9c9..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedCircle.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.volmit.iris.util.uniques.features; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.uniques.UFeature; -import com.volmit.iris.util.uniques.UFeatureMeta; -import com.volmit.iris.util.uniques.UImage; - -import java.util.function.Consumer; - -public class UFWarpedCircle implements UFeature { - @Override - public void render(UImage image, RNG rng, double t, Consumer progressor, UFeatureMeta meta) { - double r = Math.min(image.getWidth(), image.getHeight()) / 2.5D; - double i, angle, x1, y1; - CNG xShift = generator("x_warp", rng, 0.6, 1001, meta); - CNG yShift = generator("y_warp", rng, 0.6, 1002, meta); - CNG hue = generator("color_hue", rng, rng.d(0.25, 2.5), 1003, meta); - CNG sat = generator("color_sat", rng, rng.d(0.25, 2.5), 1004, meta); - CNG bri = generator("color_bri", rng, rng.d(0.25, 2.5), 1005, meta); - double tcf = rng.d(0.75, 11.25); - double rcf = rng.d(7.75, 16.25); - int x = image.getWidth() / 2; - int y = image.getHeight() / 2; - - for (int d = 0; d < 256; d++) { - r -= Math.min(image.getWidth(), image.getHeight()) / 300D; - - if (r <= 0) { - return; - } - - for (i = 0; i < 360; i += 0.1) { - angle = i; - x1 = r * Math.cos(angle * Math.PI / 180); - y1 = r * Math.sin(angle * Math.PI / 180); - image.set((int) Math.round(x + x1 + xShift.fit(-r / 2, r / 2, x1 + (t + (d * 8)), -y1 + (t + (d * 8)))), - (int) Math.round(y + y1 + yShift.fit(-r / 2, r / 2, y1 + (t + (d * 8)), -x1 + (t + (d * 8)))), - color(hue, sat, bri, x1, y1, (t * tcf) + (d * rcf))); - } - - progressor.accept(d / 256D); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedDisc.java b/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedDisc.java deleted file mode 100644 index 83e9b24d8..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedDisc.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.volmit.iris.util.uniques.features; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.uniques.UFeature; -import com.volmit.iris.util.uniques.UFeatureMeta; -import com.volmit.iris.util.uniques.UImage; - -import java.util.function.Consumer; - -public class UFWarpedDisc implements UFeature { - @Override - public void render(UImage image, RNG rng, double t, Consumer progressor, UFeatureMeta meta) { - double r = Math.min(image.getWidth(), image.getHeight()) / 3D; - CNG xShift = generator("x_warp", rng, 0.6, 1001, meta); - CNG yShift = generator("y_warp", rng, 0.6, 1002, meta); - CNG hue = generator("color_hue", rng, rng.d(0.25, 2.5), 1003, meta); - CNG sat = generator("color_sat", rng, rng.d(0.25, 2.5), 1004, meta); - CNG bri = generator("color_bri", rng, rng.d(0.25, 2.5), 1005, meta); - double tcf = rng.d(0.75, 11.25); - int x = image.getWidth() / 2; - int y = image.getHeight() / 2; - - for (int i = (int) (x - r); i < x + r; i++) { - for (int j = (int) (y - r); j < y + r; j++) { - if (image.isInBounds(i, j) && Math.pow(x - i, 2) + Math.pow(y - j, 2) <= r * r) { - image.set(Math.round(i + xShift.fit(-r / 2, r / 2, i + t, -j + t)), - Math.round(j + yShift.fit(-r / 2, r / 2, j + t, -i + t)), - color(hue, sat, bri, i, j, tcf * t)); - } - } - - progressor.accept(i / (x + r)); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedDots.java b/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedDots.java deleted file mode 100644 index 55e933015..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedDots.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.volmit.iris.util.uniques.features; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.uniques.UFeature; -import com.volmit.iris.util.uniques.UFeatureMeta; -import com.volmit.iris.util.uniques.UImage; - -import java.awt.*; -import java.util.function.Consumer; - -public class UFWarpedDots implements UFeature { - @Override - public void render(UImage image, RNG rng, double t, Consumer progressor, UFeatureMeta meta) { - CNG genX = generator("x_pos", rng, 4, 2000, meta); - CNG genY = generator("y_pos", rng, 4, 2001, meta); - - double tcf = rng.d(0.75, 11.25); - - for (int i = 1; i <= 8; i++) { - CNG xShift = generator("x_warp_" + i, rng, 2, 2006 + i, meta); - CNG yShift = generator("y_warp_" + i, rng, 2, 2007 + i, meta); - CNG hue = generator("color_hue_" + i, rng, rng.d(0.55, 3.5), 2003 + i, meta); - CNG sat = generator("color_sat_" + i, rng, rng.d(0.55, 3.5), 2004 + i, meta); - CNG bri = generator("color_bri_" + i, rng, rng.d(0.55, 3.5), 2005 + i, meta); - int x = genX.fit(0, image.getWidth(), i * 128, i * 5855); - int y = genY.fit(0, image.getHeight(), i * 128, i * 5855); - Color color = color(hue, sat, bri, x, y, t); - double r = Math.max(genX.fit(image.getWidth() / 10, image.getWidth() / 6, x, y), genY.fit(image.getHeight() / 10, image.getHeight() / 6, x, y)); - - for (int j = (int) (x - r); j < x + r; j++) { - for (int k = (int) (y - r); k < y + r; k++) { - if (image.isInBounds(j, k) && Math.pow(x - j, 2) + Math.pow(y - k, 2) <= r * r) { - image.set(Math.round(j + xShift.fit(-r / 2, r / 2, j + t, -k + t)), - Math.round(k + yShift.fit(-r / 2, r / 2, k + t, -j + t)), - color(hue, sat, bri, j, k, tcf * t)); - } - } - } - - progressor.accept(i / 32D); - } - } -} diff --git a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedLines.java b/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedLines.java deleted file mode 100644 index 640610b72..000000000 --- a/core/src/main/java/com/volmit/iris/util/uniques/features/UFWarpedLines.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.volmit.iris.util.uniques.features; - -import com.volmit.iris.util.math.RNG; -import com.volmit.iris.util.noise.CNG; -import com.volmit.iris.util.uniques.UFeature; -import com.volmit.iris.util.uniques.UFeatureMeta; -import com.volmit.iris.util.uniques.UImage; - -import java.util.function.Consumer; - -public class UFWarpedLines implements UFeature { - @Override - public void render(UImage image, RNG rng, double t, Consumer progressor, UFeatureMeta meta) { - for (int g = 1; g < 5; g++) { - CNG xShift = generator("x_warp_" + g, rng, 0.6, 1001 * g, meta); - CNG yShift = generator("y_warp_" + g, rng, 0.6, 1002 * g, meta); - CNG cX = generator("x_clip_" + g, rng, rng.d(0.035, 0.6), 77001 * g, meta); - CNG cY = generator("y_clip" + g, rng, rng.d(0.035, 0.6), 77002, meta); - CNG hue = generator("color_hue_" + g, rng, rng.d(0.25, 2.5), 1003 * g, meta); - CNG sat = generator("color_sat_" + g, rng, rng.d(0.25, 2.5), 1004 * g, meta); - CNG bri = generator("color_bri_" + g, rng, rng.d(0.25, 2.5), 1005 * g, meta); - double tcf = rng.d(0.75, 11.25 + (g * 4)); - double rcf = rng.d(7.75, 16.25 + (g * 5)); - double xcf = rng.d(0.15, 0.55 + (g * 0.645)); - double w = rng.d(64, 186 + (g * 8)); - double ww = image.getWidth() / rng.d(3, 9); - double hh = image.getHeight() / rng.d(3, 9); - boolean wh = rng.nextBoolean(); - double sa = rng.d(0.35, 0.66); - double sb = rng.d(0.35, 0.66); - - for (int i = 0; i < image.getWidth(); i += (wh ? image.getWidth() / w : 1)) { - for (int j = 0; j < image.getHeight(); j += (!wh ? image.getHeight() / w : 1)) { - if (cX.fitDouble(0, 1, i, -j, t * xcf) > sa && cY.fitDouble(0, 1, -j, i, t * xcf) > sb) { - image.set(Math.round(i + xShift.fit(-ww, ww, i, j, (t * rcf))), - Math.round(j + yShift.fit(-hh, hh, -j, i, (t * rcf))), - color(hue, sat, bri, i, j, (t * tcf))); - } - } - } - } - } -} diff --git a/core/src/main/kotlin/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.kt b/core/src/main/kotlin/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.kt deleted file mode 100644 index ba8575102..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.kt +++ /dev/null @@ -1,232 +0,0 @@ -package com.volmit.iris.core.pregenerator.cache - -import com.volmit.iris.Iris -import com.volmit.iris.util.data.Varint -import com.volmit.iris.util.documentation.ChunkCoordinates -import com.volmit.iris.util.documentation.RegionCoordinates -import com.volmit.iris.util.io.IO -import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import net.jpountz.lz4.LZ4BlockInputStream -import net.jpountz.lz4.LZ4BlockOutputStream -import java.io.* - -class PregenCacheImpl( - private val directory: File, - private val maxSize: Int -) : PregenCache { - private val cache = Object2ObjectLinkedOpenHashMap, Plate>() - - @ChunkCoordinates - override fun isChunkCached(x: Int, z: Int): Boolean { - return this[x shr 10, z shr 10].isCached( - (x shr 5) and 31, - (z shr 5) and 31 - ) { isCached(x and 31, z and 31) } - } - - @RegionCoordinates - override fun isRegionCached(x: Int, z: Int): Boolean { - return this[x shr 5, z shr 5].isCached( - x and 31, - z and 31, - Region::isCached - ) - } - - @ChunkCoordinates - override fun cacheChunk(x: Int, z: Int) { - this[x shr 10, z shr 10].cache( - (x shr 5) and 31, - (z shr 5) and 31 - ) { cache(x and 31, z and 31) } - } - - @RegionCoordinates - override fun cacheRegion(x: Int, z: Int) { - this[x shr 5, z shr 5].cache( - x and 31, - z and 31, - Region::cache - ) - } - - override fun write() { - if (cache.isEmpty()) return - runBlocking { - for (plate in cache.values) { - if (!plate.dirty) continue - launch(dispatcher) { - writePlate(plate) - } - } - } - } - - override fun trim(unloadDuration: Long) { - if (cache.isEmpty()) return - val threshold = System.currentTimeMillis() - unloadDuration - runBlocking { - val it = cache.values.iterator() - while (it.hasNext()) { - val plate = it.next() - if (plate.lastAccess < threshold) it.remove() - launch(dispatcher) { - writePlate(plate) - } - } - } - } - - private operator fun get(x: Int, z: Int): Plate { - val key = x to z - val plate = cache.getAndMoveToFirst(key) - if (plate != null) return plate - return readPlate(x, z).also { - cache.putAndMoveToFirst(key, it) - runBlocking { - while (cache.size > maxSize) { - val plate = cache.removeLast() - launch(dispatcher) { - writePlate(plate) - } - } - } - } - } - - private fun readPlate(x: Int, z: Int): Plate { - val file = fileForPlate(x, z) - if (!file.exists()) return Plate(x, z) - try { - DataInputStream(LZ4BlockInputStream(file.inputStream())).use { - return readPlate(x, z, it) - } - } catch (e: IOException) { - Iris.error("Failed to read pregen cache $file") - e.printStackTrace() - Iris.reportError(e) - } - return Plate(x, z) - } - - private fun writePlate(plate: Plate) { - if (!plate.dirty) return - val file = fileForPlate(plate.x, plate.z) - try { - IO.write(file, { DataOutputStream(LZ4BlockOutputStream(it)) }, plate::write) - plate.dirty = false - } catch (e: IOException) { - Iris.error("Failed to write preen cache $file") - e.printStackTrace() - Iris.reportError(e) - } - } - - private fun fileForPlate(x: Int, z: Int): File { - check(!(!directory.exists() && !directory.mkdirs())) { "Cannot create directory: " + directory.absolutePath } - return File(directory, "c.$x.$z.lz4b") - } - - private class Plate( - val x: Int, - val z: Int, - private var count: Short = 0, - private var regions: Array? = arrayOfNulls(1024) - ) { - var dirty: Boolean = false - var lastAccess: Long = System.currentTimeMillis() - - fun cache(x: Int, z: Int, predicate: Region.() -> Boolean): Boolean { - lastAccess = System.currentTimeMillis() - if (count == SIZE) return false - val region = regions!!.run { this[x * 32 + z] ?: Region().also { this[x * 32 + z] = it } } - if (!region.predicate()) return false - if (++count == SIZE) regions = null - dirty = true - return true - } - - fun isCached(x: Int, z: Int, predicate: Region.() -> Boolean): Boolean { - lastAccess = System.currentTimeMillis() - if (count == SIZE) return true - val region = regions!![x * 32 + z] ?: return false - return region.predicate() - } - - fun write(dos: DataOutput) { - Varint.writeSignedVarInt(count.toInt(), dos) - regions?.forEach { - dos.writeBoolean(it == null) - it?.write(dos) - } - } - } - - private class Region( - private var count: Short = 0, - private var words: LongArray? = LongArray(64) - ) { - fun cache(): Boolean { - if (count == SIZE) return false - count = SIZE - words = null - return true - } - - fun cache(x: Int, z: Int): Boolean { - if (count == SIZE) return false - val words = words ?: return false - val i = x * 32 + z - val w = i shr 6 - val b = 1L shl (i and 63) - - val cur = (words[w] and b) != 0L - if (cur) return false - - if (++count == SIZE) { - this.words = null - return true - } else { - words[w] = words[w] or b - return false - } - } - - fun isCached(): Boolean = count == SIZE - fun isCached(x: Int, z: Int): Boolean { - val i = x * 32 + z - return count == SIZE || (words!![i shr 6] and (1L shl (i and 63))) != 0L - } - - @Throws(IOException::class) - fun write(dos: DataOutput) { - Varint.writeSignedVarInt(count.toInt(), dos) - words?.forEach { Varint.writeUnsignedVarLong(it, dos) } - } - } - - companion object { - private val dispatcher = Dispatchers.IO.limitedParallelism(4) - private const val SIZE: Short = 1024 - - @Throws(IOException::class) - private fun readPlate(x: Int, z: Int, din: DataInput): Plate { - val count = Varint.readSignedVarInt(din) - if (count == 1024) return Plate(x, z, SIZE, null) - return Plate(x, z, count.toShort(), Array(1024) { - if (din.readBoolean()) null - else readRegion(din) - }) - } - - @Throws(IOException::class) - private fun readRegion(din: DataInput): Region { - val count = Varint.readSignedVarInt(din) - return if (count == 1024) Region(SIZE, null) - else Region(count.toShort(), LongArray(64) { Varint.readUnsignedVarLong(din) }) - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/safeguard/IrisSafeguard.kt b/core/src/main/kotlin/com/volmit/iris/core/safeguard/IrisSafeguard.kt deleted file mode 100644 index 0c9d66c4e..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/safeguard/IrisSafeguard.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.volmit.iris.core.safeguard - -import com.volmit.iris.Iris -import com.volmit.iris.core.IrisSettings -import com.volmit.iris.core.safeguard.task.Diagnostic -import com.volmit.iris.core.safeguard.task.Task -import com.volmit.iris.core.safeguard.task.ValueWithDiagnostics -import com.volmit.iris.core.safeguard.task.tasks -import com.volmit.iris.util.format.C -import com.volmit.iris.util.scheduling.J -import org.bukkit.Bukkit -import java.util.* - -object IrisSafeguard { - @Volatile - private var forceShutdown = false - private var results: Map> = emptyMap() - private var context: Map = emptyMap() - private var attachment: Map> = emptyMap() - private var mode = Mode.STABLE - private var count = 0 - - @JvmStatic - fun execute() { - val results = LinkedHashMap>(tasks.size) - val context = LinkedHashMap(tasks.size) - val attachment = LinkedHashMap>(tasks.size) - var mode = Mode.STABLE - var count = 0 - for (task in tasks) { - var result: ValueWithDiagnostics - try { - result = task.run() - } catch (e: Throwable) { - Iris.reportError(e) - result = ValueWithDiagnostics( - Mode.WARNING, - Diagnostic(Diagnostic.Logger.ERROR, "Error while running task ${task.id}", e) - ) - } - mode = mode.highest(result.value) - results[task] = result - context[task.id] = result.value.id - attachment[task.id] = result.diagnostics.flatMap { it.toString().split('\n') } - if (result.value != Mode.STABLE) count++ - } - - this.results = Collections.unmodifiableMap(results) - this.context = Collections.unmodifiableMap(context) - this.attachment = Collections.unmodifiableMap(attachment) - this.mode = mode - this.count = count - } - - @JvmStatic - fun mode() = mode - - @JvmStatic - fun asContext() = context - - @JvmStatic - fun asAttachment() = attachment - - @JvmStatic - fun splash() { - Iris.instance.splash() - printReports() - printFooter() - } - - @JvmStatic - fun printReports() { - when (mode) { - Mode.STABLE -> Iris.info(C.BLUE.toString() + "0 Conflicts found") - Mode.WARNING -> Iris.warn(C.GOLD.toString() + "%s Issues found", count) - Mode.UNSTABLE -> Iris.error(C.DARK_RED.toString() + "%s Issues found", count) - } - - results.values.forEach { it.log(withStackTrace = true) } - } - - @JvmStatic - fun printFooter() { - when (mode) { - Mode.STABLE -> Iris.info(C.BLUE.toString() + "Iris is running Stable") - Mode.WARNING -> warning() - Mode.UNSTABLE -> unstable() - } - } - - @JvmStatic - fun isForceShutdown() = forceShutdown - - private fun warning() { - Iris.warn(C.GOLD.toString() + "Iris is running in Warning Mode") - - Iris.warn("") - Iris.warn(C.DARK_GRAY.toString() + "--==<" + C.GOLD + " IMPORTANT " + C.DARK_GRAY + ">==--") - Iris.warn(C.GOLD.toString() + "Iris is running in warning mode which may cause the following issues:") - Iris.warn("- Data Loss") - Iris.warn("- Errors") - Iris.warn("- Broken worlds") - Iris.warn("- Unexpected behavior.") - Iris.warn("- And perhaps further complications.") - Iris.warn("") - } - - private fun unstable() { - Iris.error(C.DARK_RED.toString() + "Iris is running in Unstable Mode") - - Iris.error("") - Iris.error(C.DARK_GRAY.toString() + "--==<" + C.RED + " IMPORTANT " + C.DARK_GRAY + ">==--") - Iris.error("Iris is running in unstable mode which may cause the following issues:") - Iris.error(C.DARK_RED.toString() + "Server Issues") - Iris.error("- Server won't boot") - Iris.error("- Data Loss") - Iris.error("- Unexpected behavior.") - Iris.error("- And More...") - Iris.error(C.DARK_RED.toString() + "World Issues") - Iris.error("- Worlds can't load due to corruption.") - Iris.error("- Worlds may slowly corrupt until they can't load.") - Iris.error("- World data loss.") - Iris.error("- And More...") - Iris.error(C.DARK_RED.toString() + "ATTENTION: " + C.RED + "While running Iris in unstable mode, you won't be eligible for support.") - - if (IrisSettings.get().general.isDoomsdayAnnihilationSelfDestructMode) { - Iris.error(C.DARK_RED.toString() + "Boot Unstable is set to true, continuing with the startup process in 10 seconds.") - J.sleep(10000L) - } else { - Iris.error(C.DARK_RED.toString() + "Go to plugins/iris/settings.json and set DoomsdayAnnihilationSelfDestructMode to true if you wish to proceed.") - Iris.error(C.DARK_RED.toString() + "The server will shutdown in 10 seconds.") - J.sleep(10000L) - Iris.error(C.DARK_RED.toString() + "Shutting down server.") - forceShutdown = true - try { - Bukkit.getPluginManager().disablePlugins() - } finally { - Runtime.getRuntime().halt(42) - } - } - Iris.info("") - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/safeguard/Mode.kt b/core/src/main/kotlin/com/volmit/iris/core/safeguard/Mode.kt deleted file mode 100644 index 6a5b282b2..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/safeguard/Mode.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.volmit.iris.core.safeguard - -import com.volmit.iris.BuildConstants -import com.volmit.iris.Iris -import com.volmit.iris.core.IrisSettings -import com.volmit.iris.util.format.C -import com.volmit.iris.util.format.Form - -enum class Mode(private val color: C) { - STABLE(C.IRIS), - WARNING(C.GOLD), - UNSTABLE(C.RED); - - val id = name.lowercase() - - fun highest(m: Mode): Mode { - return if (m.ordinal > ordinal) m else this - } - - fun tag(subTag: String?): String { - if (subTag == null || subTag.isBlank()) return wrap("Iris") + C.GRAY + ": " - return wrap("Iris") + " " + wrap(subTag) + C.GRAY + ": " - } - - private fun wrap(tag: String?): String { - return C.BOLD.toString() + "" + C.DARK_GRAY + "[" + C.BOLD + color + tag + C.BOLD + C.DARK_GRAY + "]" + C.RESET - } - - fun trySplash() { - if (!IrisSettings.get().general.isSplashLogoStartup) return - splash() - } - - fun splash() { - val padd = Form.repeat(" ", 8) - val padd2 = Form.repeat(" ", 4) - - val splash = arrayOf( - padd + C.GRAY + " @@@@@@@@@@@@@@" + C.DARK_GRAY + "@@@", - padd + C.GRAY + " @@&&&&&&&&&" + C.DARK_GRAY + "&&&&&&" + color + " .(((()))). ", - padd + C.GRAY + "@@@&&&&&&&&" + C.DARK_GRAY + "&&&&&" + color + " .((((((())))))). ", - padd + C.GRAY + "@@@&&&&&" + C.DARK_GRAY + "&&&&&&&" + color + " ((((((((())))))))) " + C.GRAY + " @", - padd + C.GRAY + "@@@&&&&" + C.DARK_GRAY + "@@@@@&" + color + " ((((((((-))))))))) " + C.GRAY + " @@", - padd + C.GRAY + "@@@&&" + color + " ((((((({ })))))))) " + C.GRAY + " &&@@@", - padd + C.GRAY + "@@" + color + " ((((((((-))))))))) " + C.DARK_GRAY + "&@@@@@" + C.GRAY + "&&&&@@@", - padd + C.GRAY + "@" + color + " ((((((((())))))))) " + C.DARK_GRAY + "&&&&&" + C.GRAY + "&&&&&&&@@@", - padd + C.GRAY + "" + color + " '((((((()))))))' " + C.DARK_GRAY + "&&&&&" + C.GRAY + "&&&&&&&&@@@", - padd + C.GRAY + "" + color + " '(((())))' " + C.DARK_GRAY + "&&&&&&&&" + C.GRAY + "&&&&&&&@@", - padd + C.GRAY + " " + C.DARK_GRAY + "@@@" + C.GRAY + "@@@@@@@@@@@@@@", - ) - - val info = arrayOf( - "", - "", - "", - "", - "", - padd2 + color + " Iris", - padd2 + C.GRAY + " by " + color + "Volmit Software", - padd2 + C.GRAY + " v" + color + Iris.instance.description.version, - padd2 + C.GRAY + " c" + color + BuildConstants.COMMIT + C.GRAY + "/" + color + BuildConstants.ENVIRONMENT, - ) - - - val builder = StringBuilder("\n\n") - for (i in splash.indices) { - builder.append(splash[i]) - if (i < info.size) { - builder.append(info[i]) - } - builder.append("\n") - } - - Iris.info(builder.toString()) - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Task.kt b/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Task.kt deleted file mode 100644 index aa0dd9202..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Task.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.volmit.iris.core.safeguard.task - -import com.volmit.iris.core.safeguard.Mode -import com.volmit.iris.util.format.Form -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadOnlyProperty - -abstract class Task( - val id: String, - val name: String = Form.capitalizeWords(id.replace(" ", "_").lowercase()), -) { - - abstract fun run(): ValueWithDiagnostics - - companion object { - fun of(id: String, name: String = id, action: () -> ValueWithDiagnostics) = object : Task(id, name) { - override fun run() = action() - } - - fun of(id: String, action: () -> ValueWithDiagnostics) = object : Task(id) { - override fun run() = action() - } - - fun task(action: () -> ValueWithDiagnostics) = PropertyDelegateProvider> { _, _ -> - ReadOnlyProperty { _, property -> of(property.name, action) } - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Tasks.kt b/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Tasks.kt deleted file mode 100644 index 54961fc0f..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Tasks.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.volmit.iris.core.safeguard.task - -import com.volmit.iris.Iris -import com.volmit.iris.core.IrisWorlds -import com.volmit.iris.core.nms.INMS -import com.volmit.iris.core.nms.v1X.NMSBinding1X -import com.volmit.iris.core.safeguard.Mode -import com.volmit.iris.core.safeguard.Mode.* -import com.volmit.iris.core.safeguard.task.Diagnostic.Logger.* -import com.volmit.iris.core.safeguard.task.Task.Companion.of -import com.volmit.iris.util.agent.Agent -import com.volmit.iris.util.misc.getHardware -import org.bukkit.Bukkit -import java.util.stream.Collectors -import javax.tools.ToolProvider -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadOnlyProperty - -private val memory by task { - val mem = getHardware.getProcessMemory() - if (mem >= 5999) STABLE.withDiagnostics() - else STABLE.withDiagnostics( - WARN.create("Low Memory"), - WARN.create("- 6GB+ Ram is recommended"), - WARN.create("- Process Memory: $mem MB") - ) -} - -private val incompatibilities by task { - val plugins = mutableSetOf("dynmap", "Stratos") - plugins.removeIf { server.pluginManager.getPlugin(it) == null } - - if (plugins.isEmpty()) STABLE.withDiagnostics() - else { - val diagnostics = mutableListOf() - if ("dynmap" in plugins) diagnostics.addAll( - ERROR.create("Dynmap"), - ERROR.create("- The plugin Dynmap is not compatible with the server."), - ERROR.create("- If you want to have a map plugin like Dynmap, consider Bluemap.") - ) - if ("Stratos" in plugins) diagnostics.addAll( - ERROR.create("Stratos"), - ERROR.create("- Iris is not compatible with other worldgen plugins.") - ) - WARNING.withDiagnostics(diagnostics) - } -} - -private val software by task { - val supported = setOf( - "purpur", - "pufferfish", - "paper", - "spigot", - "bukkit" - ) - - if (supported.any { server.name.contains(it, true) }) STABLE.withDiagnostics() - else WARNING.withDiagnostics( - WARN.create("Unsupported Server Software"), - WARN.create("- Please consider using Paper or Purpur instead.") - ) -} - -private val version by task { - val parts = Iris.instance.description.version.split('-') - val minVersion = parts[1] - val maxVersion = parts[2] - - if (INMS.get() !is NMSBinding1X) STABLE.withDiagnostics() - else UNSTABLE.withDiagnostics( - ERROR.create("Server Version"), - ERROR.create("- Iris only supports $minVersion > $maxVersion") - ) -} - -private val injection by task { - if (!Agent.install()) UNSTABLE.withDiagnostics( - ERROR.create("Java Agent"), - ERROR.create("- Please enable dynamic agent loading by adding -XX:+EnableDynamicAgentLoading to your jvm arguments."), - ERROR.create("- or add the jvm argument -javaagent:" + Agent.AGENT_JAR.path) - ) - else if (!INMS.get().injectBukkit()) UNSTABLE.withDiagnostics( - ERROR.create("Code Injection"), - ERROR.create("- Failed to inject code. Please contact support") - ) - else STABLE.withDiagnostics() -} - -private val dimensionTypes by task { - val keys = IrisWorlds.get() - .dimensions - .map { it.dimensionTypeKey } - .collect(Collectors.toSet()) - - if (!INMS.get().missingDimensionTypes(*keys.toTypedArray())) STABLE.withDiagnostics() - else UNSTABLE.withDiagnostics( - ERROR.create("Dimension Types"), - ERROR.create("- Required Iris dimension types were not loaded."), - ERROR.create("- If this still happens after a restart please contact support.") - ) -} - -private val diskSpace by task { - if (server.worldContainer.freeSpace.toDouble().div(0x4000_0000) > 3) STABLE.withDiagnostics() - else WARNING.withDiagnostics( - WARN.create("Insufficient Disk Space"), - WARN.create("- 3GB of free space is required for Iris to function.") - ) -} - -private val java by task { - val version = Iris.getJavaVersion() - val jdk = runCatching { ToolProvider.getSystemJavaCompiler() }.getOrNull() != null - if (version in setOf(21) && jdk) STABLE.withDiagnostics() - else WARNING.withDiagnostics( - WARN.create("Unsupported Java version"), - WARN.create("- Please consider using JDK 21 Instead of ${if(jdk) "JDK" else "JRE"} $version") - ) -} - - -val tasks = listOf( - memory, - incompatibilities, - software, - version, - injection, - dimensionTypes, - diskSpace, - java, -) - -private val server get() = Bukkit.getServer() -private fun MutableList.addAll(vararg values: T) = values.forEach(this::add) -fun task(action: () -> ValueWithDiagnostics) = PropertyDelegateProvider> { _, _ -> - ReadOnlyProperty { _, property -> of(property.name, action) } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/ValueWithDiagnostics.kt b/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/ValueWithDiagnostics.kt deleted file mode 100644 index d388f3d26..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/ValueWithDiagnostics.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.volmit.iris.core.safeguard.task - -import com.volmit.iris.Iris -import com.volmit.iris.util.format.C -import java.io.ByteArrayOutputStream -import java.io.PrintStream - -data class ValueWithDiagnostics( - val value: T, - val diagnostics: List -) { - constructor(value: T, vararg diagnostics: Diagnostic) : this(value, diagnostics.toList()) - - @JvmOverloads - fun log( - withException: Boolean = true, - withStackTrace: Boolean = false - ) { - diagnostics.forEach { it.log(withException, withStackTrace) } - } -} - -data class Diagnostic @JvmOverloads constructor( - val logger: Logger = Logger.ERROR, - val message: String, - val exception: Throwable? = null -) { - - enum class Logger( - private val logger: (String) -> Unit - ) { - DEBUG(Iris::debug), - RAW(Iris::msg), - INFO(Iris::info), - WARN(Iris::warn), - ERROR(Iris::error); - - fun print(message: String) = message.split('\n').forEach(logger) - fun create(message: String, exception: Throwable? = null) = Diagnostic(this, message, exception) - } - - @JvmOverloads - fun log( - withException: Boolean = true, - withStackTrace: Boolean = false - ) { - logger.print(render(withException, withStackTrace)) - } - - fun render( - withException: Boolean = true, - withStackTrace: Boolean = false - ): String = buildString { - append(message) - if (withException && exception != null) { - append(": ") - append(exception) - if (withStackTrace) { - ByteArrayOutputStream().use { os -> - val ps = PrintStream(os) - exception.printStackTrace(ps) - ps.flush() - append("\n") - append(os.toString()) - } - } - } - } - - override fun toString(): String = C.strip(render()) -} - -fun T.withDiagnostics(vararg diagnostics: Diagnostic) = ValueWithDiagnostics(this, diagnostics.toList()) -fun T.withDiagnostics(diagnostics: List) = ValueWithDiagnostics(this, diagnostics) \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/ChunkUpdateScript.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/ChunkUpdateScript.kt deleted file mode 100644 index 4bad3524c..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/ChunkUpdateScript.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.base - -import com.volmit.iris.core.scripting.func.UpdateExecutor -import com.volmit.iris.util.mantle.MantleChunk -import org.bukkit.Chunk -import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.ScriptCompilationConfiguration -import kotlin.script.experimental.api.providedProperties - -@KotlinScript(fileExtension = "update.kts", compilationConfiguration = ChunkUpdateScriptDefinition::class) -abstract class ChunkUpdateScript - -object ChunkUpdateScriptDefinition : ScriptCompilationConfiguration(listOf(EngineScriptDefinition), { - providedProperties( - "mantleChunk" to MantleChunk::class, - "chunk" to Chunk::class, - "executor" to UpdateExecutor::class - ) -}) { - private fun readResolve(): Any = MobSpawningScriptDefinition -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/DataScript.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/DataScript.kt deleted file mode 100644 index 1a180701c..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/DataScript.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.base - -import com.volmit.iris.core.loader.IrisData -import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.ScriptCompilationConfiguration -import kotlin.script.experimental.api.providedProperties - -@KotlinScript(fileExtension = "data.kts", compilationConfiguration = DataScriptDefinition::class) -abstract class DataScript - -object DataScriptDefinition : ScriptCompilationConfiguration(listOf(SimpleScriptDefinition), { - providedProperties("data" to IrisData::class) -}) { - private fun readResolve(): Any = DataScriptDefinition -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/EngineScript.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/EngineScript.kt deleted file mode 100644 index 8a091a50f..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/EngineScript.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.base - -import com.volmit.iris.core.scripting.func.BiomeLookup -import com.volmit.iris.engine.IrisComplex -import com.volmit.iris.engine.framework.Engine -import com.volmit.iris.engine.`object`.IrisDimension -import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.ScriptCompilationConfiguration -import kotlin.script.experimental.api.providedProperties - -@KotlinScript(fileExtension = "engine.kts", compilationConfiguration = EngineScriptDefinition::class) -abstract class EngineScript - -object EngineScriptDefinition : ScriptCompilationConfiguration(listOf(DataScriptDefinition), { - providedProperties( - "engine" to Engine::class, - "seed" to Long::class, - "dimension" to IrisDimension::class, - "complex" to IrisComplex::class, - "biome" to BiomeLookup::class, - ) -}) { - - private fun readResolve(): Any = EngineScriptDefinition -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/MobSpawningScript.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/MobSpawningScript.kt deleted file mode 100644 index f9add58dc..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/MobSpawningScript.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.base - -import org.bukkit.Location -import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.ScriptCompilationConfiguration -import kotlin.script.experimental.api.providedProperties - -@KotlinScript(fileExtension = "spawn.kts", compilationConfiguration = MobSpawningScriptDefinition::class) -abstract class MobSpawningScript - -object MobSpawningScriptDefinition : ScriptCompilationConfiguration(listOf(EngineScriptDefinition), { - providedProperties("location" to Location::class) -}) { - private fun readResolve(): Any = MobSpawningScriptDefinition -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/NoiseScript.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/NoiseScript.kt deleted file mode 100644 index c8a7056fa..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/NoiseScript.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.base - -import com.volmit.iris.util.math.RNG -import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.ScriptCompilationConfiguration -import kotlin.script.experimental.api.providedProperties - -@KotlinScript(fileExtension = "noise.kts", compilationConfiguration = NoiseScriptDefinition::class) -abstract class NoiseScript - -object NoiseScriptDefinition : ScriptCompilationConfiguration(listOf(DataScriptDefinition), { - providedProperties("rng" to RNG::class) -}) { - - private fun readResolve(): Any = NoiseScriptDefinition -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/PostMobSpawningScript.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/PostMobSpawningScript.kt deleted file mode 100644 index 02b485bed..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/PostMobSpawningScript.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.base - -import org.bukkit.entity.Entity -import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.ScriptCompilationConfiguration -import kotlin.script.experimental.api.providedProperties - -@KotlinScript(fileExtension = "postspawn.kts", compilationConfiguration = PostMobSpawningScriptDefinition::class) -abstract class PostMobSpawningScript - -object PostMobSpawningScriptDefinition : ScriptCompilationConfiguration(listOf(MobSpawningScriptDefinition), { - providedProperties("entity" to Entity::class) -}) { - private fun readResolve(): Any = PostMobSpawningScriptDefinition -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/PreprocessorScript.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/PreprocessorScript.kt deleted file mode 100644 index c8941bb8d..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/PreprocessorScript.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.base - -import com.volmit.iris.core.loader.IrisRegistrant -import com.volmit.iris.engine.framework.Engine -import com.volmit.iris.engine.`object`.IrisDimension -import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.ScriptCompilationConfiguration -import kotlin.script.experimental.api.providedProperties - -@KotlinScript(fileExtension = "proc.kts", compilationConfiguration = PreprocessorScriptDefinition::class) -abstract class PreprocessorScript - -object PreprocessorScriptDefinition : ScriptCompilationConfiguration(listOf(DataScriptDefinition), { - providedProperties( - "engine" to Engine::class, - "seed" to Long::class, - "dimension" to IrisDimension::class, - "object" to IrisRegistrant::class - ) -}) { - private fun readResolve(): Any = PreprocessorScriptDefinition -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/SimpleScript.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/SimpleScript.kt deleted file mode 100644 index 5839bbcc0..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/base/SimpleScript.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.base - -import com.volmit.iris.core.scripting.kotlin.runner.configure -import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.ScriptCompilationConfiguration -import kotlin.script.experimental.api.defaultImports -import kotlin.script.experimental.dependencies.DependsOn -import kotlin.script.experimental.dependencies.Repository -import kotlin.script.experimental.jvm.dependenciesFromClassContext -import kotlin.script.experimental.jvm.jvm - -@KotlinScript(fileExtension = "simple.kts", compilationConfiguration = SimpleScriptDefinition::class) -abstract class SimpleScript - -object SimpleScriptDefinition : ScriptCompilationConfiguration({ - defaultImports( - DependsOn::class.qualifiedName!!, - Repository::class.qualifiedName!!, - "com.volmit.iris.Iris.info", - "com.volmit.iris.Iris.debug", - "com.volmit.iris.Iris.warn", - "com.volmit.iris.Iris.error" - ) - - jvm { - dependenciesFromClassContext(KotlinScript::class, wholeClasspath = true) - dependenciesFromClassContext(SimpleScript::class, wholeClasspath = true) - } - - configure() -}) { - private fun readResolve(): Any = SimpleScriptDefinition -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/environment/IrisExecutionEnvironment.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/environment/IrisExecutionEnvironment.kt deleted file mode 100644 index 2114f29d4..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/environment/IrisExecutionEnvironment.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.environment - -import com.volmit.iris.core.loader.IrisRegistrant -import com.volmit.iris.core.scripting.environment.EngineEnvironment -import com.volmit.iris.core.scripting.func.BiomeLookup -import com.volmit.iris.core.scripting.func.UpdateExecutor -import com.volmit.iris.core.scripting.kotlin.base.ChunkUpdateScript -import com.volmit.iris.core.scripting.kotlin.base.EngineScript -import com.volmit.iris.core.scripting.kotlin.base.MobSpawningScript -import com.volmit.iris.core.scripting.kotlin.base.PostMobSpawningScript -import com.volmit.iris.core.scripting.kotlin.base.PreprocessorScript -import com.volmit.iris.core.scripting.kotlin.environment.IrisSimpleExecutionEnvironment -import com.volmit.iris.core.scripting.kotlin.runner.ScriptRunner -import com.volmit.iris.engine.framework.Engine -import com.volmit.iris.util.mantle.MantleChunk -import org.bukkit.Chunk -import org.bukkit.Location -import org.bukkit.entity.Entity -import java.io.File - -class IrisExecutionEnvironment internal constructor( - private val engine: Engine, - parent: ScriptRunner?, -) : IrisPackExecutionEnvironment(engine.data, parent), EngineEnvironment { - constructor(engine: Engine) : this(engine, null) - override fun getEngine() = engine - - override fun execute(script: String) = - execute(script, EngineScript::class.java, engine.parameters()) - - override fun evaluate(script: String) = - evaluate(script, EngineScript::class.java, engine.parameters()) - - override fun spawnMob(script: String, location: Location) = - evaluate(script, MobSpawningScript::class.java, engine.parameters("location" to location)) - - override fun postSpawnMob(script: String, location: Location, mob: Entity) = - execute(script, PostMobSpawningScript::class.java, engine.parameters("location" to location, "entity" to mob)) - - override fun preprocessObject(script: String, `object`: IrisRegistrant) = - execute(script, PreprocessorScript::class.java, engine.limitedParameters("object" to `object`)) - - override fun updateChunk(script: String, mantleChunk: MantleChunk, chunk: Chunk, executor: UpdateExecutor) = - execute(script, ChunkUpdateScript::class.java, engine.parameters("mantleChunk" to mantleChunk, "chunk" to chunk, "executor" to executor)) - - private fun Engine.limitedParameters(vararg values: Pair): Map { - return mapOf( - "data" to data, - "engine" to this, - "seed" to seedManager.seed, - "dimension" to dimension, - *values, - ) - } - - private fun Engine.parameters(vararg values: Pair): Map { - return limitedParameters( - "complex" to complex, - "biome" to BiomeLookup(::getSurfaceBiome), - *values, - ) - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/environment/IrisPackExecutionEnvironment.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/environment/IrisPackExecutionEnvironment.kt deleted file mode 100644 index 412695f46..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/environment/IrisPackExecutionEnvironment.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.environment - -import com.volmit.iris.core.loader.IrisData -import com.volmit.iris.core.scripting.environment.EngineEnvironment -import com.volmit.iris.core.scripting.environment.PackEnvironment -import com.volmit.iris.core.scripting.kotlin.base.DataScript -import com.volmit.iris.core.scripting.kotlin.base.NoiseScript -import com.volmit.iris.core.scripting.kotlin.runner.Script -import com.volmit.iris.core.scripting.kotlin.runner.ScriptRunner -import com.volmit.iris.core.scripting.kotlin.runner.valueOrThrow -import com.volmit.iris.engine.framework.Engine -import com.volmit.iris.util.math.RNG -import kotlin.reflect.KClass - -open class IrisPackExecutionEnvironment internal constructor( - private val data: IrisData, - parent: ScriptRunner? -) : IrisSimpleExecutionEnvironment(data.dataFolder, parent), PackEnvironment { - constructor(data: IrisData) : this(data, null) - - override fun getData() = data - - override fun compile(script: String, type: KClass<*>): Script { - val loaded = data.scriptLoader.load(script) - return compileCache.get(script) - .computeIfAbsent(type) { _ -> runner.compile(type, loaded.loadFile, loaded.source) } - .valueOrThrow("Failed to compile script $script") - } - - override fun execute(script: String) = - execute(script, DataScript::class.java, data.parameters()) - - override fun evaluate(script: String) = - evaluate(script, DataScript::class.java, data.parameters()) - - override fun createNoise(script: String, rng: RNG) = - evaluate(script, NoiseScript::class.java, data.parameters("rng" to rng)) - - override fun with(engine: Engine) = - IrisExecutionEnvironment(engine, runner) - - private fun IrisData.parameters(vararg values: Pair): Map { - return mapOf( - "data" to this, - *values, - ) - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/environment/IrisSimpleExecutionEnvironment.kt b/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/environment/IrisSimpleExecutionEnvironment.kt deleted file mode 100644 index 274807eee..000000000 --- a/core/src/main/kotlin/com/volmit/iris/core/scripting/kotlin/environment/IrisSimpleExecutionEnvironment.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.volmit.iris.core.scripting.kotlin.environment - -import com.volmit.iris.Iris -import com.volmit.iris.core.IrisSettings -import com.volmit.iris.core.scripting.environment.SimpleEnvironment -import com.volmit.iris.core.scripting.kotlin.base.* -import com.volmit.iris.core.scripting.kotlin.runner.FileComponents -import com.volmit.iris.core.scripting.kotlin.runner.Script -import com.volmit.iris.core.scripting.kotlin.runner.ScriptRunner -import com.volmit.iris.core.scripting.kotlin.runner.classpath -import com.volmit.iris.core.scripting.kotlin.runner.value -import com.volmit.iris.core.scripting.kotlin.runner.valueOrThrow -import com.volmit.iris.util.collection.KMap -import com.volmit.iris.util.data.KCache -import com.volmit.iris.util.format.C -import java.io.File -import kotlin.reflect.KClass -import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.ResultWithDiagnostics -import kotlin.text.split - -open class IrisSimpleExecutionEnvironment internal constructor( - baseDir: File, - parent: ScriptRunner? -) : SimpleEnvironment { - @JvmOverloads - constructor(baseDir: File = File(".").absoluteFile) : this(baseDir, null) - protected val compileCache = KCache, ResultWithDiagnostics

- * Character encoding names can be found at - * IANA. - *

Dye ColorColor
%1$sTest String